Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions DESCRIPTION
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,7 @@ Collate:
'tokens.R'
'tools-built-in.R'
'tools-def-auto.R'
'utils-auth.R'
'utils-callbacks.R'
'utils-cat.R'
'utils-merge.R'
Expand Down
1 change: 1 addition & 0 deletions NEWS.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
# ellmer (development version)

* `chat_*()` functions now use a credentials functions instead of an `api_key` (#613). This means that the `api_key` is never stored in the provider object (which might be saved to disk), but is instead retrieved on demand as needed. You generally shouldn't need to use the `credentials` argument, but when you do, you should use it to dynamically retrieve the API key from some other source (i.e. never inline a secret directly into a function call).
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
* `chat_*()` functions now use a credentials functions instead of an `api_key` (#613). This means that the `api_key` is never stored in the provider object (which might be saved to disk), but is instead retrieved on demand as needed. You generally shouldn't need to use the `credentials` argument, but when you do, you should use it to dynamically retrieve the API key from some other source (i.e. never inline a secret directly into a function call).
* `chat_*()` functions now use a `credentials` function instead of an `api_key` (#613). This means that API keys are never stored in the chat object (which might be saved to disk), but is instead retrieved on demand as needed. You generally shouldn't need to use the `credentials` argument, but when you do, you should use it to dynamically retrieve the API key from some other source (i.e. never inline a secret directly into a function call).

* The following deprecated functions/arguments/methods have now been removed:
* `Chat$extract_data()` -> `chat$chat_structured()` (0.2.0)
* `Chat$extract_data_async()` -> `chat$chat_structured_async()` (0.2.0)
Expand Down
6 changes: 0 additions & 6 deletions R/httr2.R
Original file line number Diff line number Diff line change
Expand Up @@ -93,12 +93,6 @@ ellmer_req_robustify <- function(req, is_transient = NULL, after = NULL) {
req
}

ellmer_req_credentials <- function(req, credentials_fun) {
# TODO: simplify once req_headers_redacted() supports !!!
credentials <- credentials_fun()
req_headers(req, !!!credentials, .redact = names(credentials))
}

ellmer_req_user_agent <- function(req, override = "") {
ua <- if (identical(override, "")) ellmer_user_agent() else override
req_user_agent(req, ua)
Expand Down
20 changes: 14 additions & 6 deletions R/provider-anthropic.R
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,8 @@ NULL
#' @inheritParams chat_openai
#' @inherit chat_openai return
#' @param model `r param_model("claude-sonnet-4-20250514", "anthropic")`
#' @param api_key `r api_key_param("ANTHROPIC_API_KEY")`
#' @param api_key `r lifecycle::badge("deprecated")` Use `credentials` instead.
#' @param credentials `r api_key_param("ANTHROPIC_API_KEY")`
#' @param beta_headers Optionally, a character vector of beta headers to opt-in
#' claude features that are still in beta.
#' @param api_headers Named character vector of arbitrary extra headers appended
Expand All @@ -35,14 +36,22 @@ chat_anthropic <- function(
api_args = list(),
base_url = "https://api.anthropic.com/v1",
beta_headers = character(),
api_key = anthropic_key(),
api_key = NULL,
credentials = NULL,
api_headers = character(),
echo = NULL
) {
echo <- check_echo(echo)

model <- set_default(model, "claude-sonnet-4-20250514")

credentials <- as_credentials(
"chat_anthropic",
function() anthropic_key(),
credentials = credentials,
api_key = api_key
)

provider <- ProviderAnthropic(
name = "Anthropic",
model = model,
Expand All @@ -51,7 +60,7 @@ chat_anthropic <- function(
extra_headers = api_headers,
base_url = base_url,
beta_headers = beta_headers,
api_key = api_key
credentials = credentials
)

Chat$new(provider = provider, system_prompt = system_prompt, echo = echo)
Expand All @@ -77,7 +86,6 @@ ProviderAnthropic <- new_class(
"ProviderAnthropic",
parent = Provider,
properties = list(
prop_redacted("api_key"),
beta_headers = class_character
)
)
Expand All @@ -94,7 +102,7 @@ method(base_request, ProviderAnthropic) <- function(provider) {
# <https://docs.anthropic.com/en/api/versioning>
req <- req_headers(req, `anthropic-version` = "2023-06-01")
# <https://docs.anthropic.com/en/api/getting-started#authentication>
req <- req_headers_redacted(req, `x-api-key` = provider@api_key)
req <- ellmer_req_credentials(req, provider@credentials(), "x-api-key")

# <https://docs.anthropic.com/en/api/rate-limits>
# <https://docs.anthropic.com/en/api/errors#http-errors>
Expand Down Expand Up @@ -520,7 +528,7 @@ models_anthropic <- function(
name = "Anthropic",
model = "",
base_url = base_url,
api_key = api_key
credentials = function() api_key
)

req <- base_request(provider)
Expand Down
51 changes: 21 additions & 30 deletions R/provider-azure.R
Original file line number Diff line number Diff line change
Expand Up @@ -28,13 +28,8 @@ NULL
#' @param model The **deployment id** for the model you want to use.
#' @param deployment_id `r lifecycle::badge("deprecated")` Use `model` instead.
#' @param api_version The API version to use.
#' @param api_key `r api_key_param("AZURE_OPENAI_API_KEY")`
#' @param credentials A list of authentication headers to pass into
#' [`httr2::req_headers()`], a function that returns them, or `NULL` to use
#' `api_key` to generate these headers instead. This is an escape
#' hatch that allows users to incorporate Azure credentials generated by other
#' packages into \pkg{ellmer}, or to manage the lifetime of credentials that
#' need to be refreshed.
#' @param api_key `r lifecycle::badge("deprecated")` Use `credentials` instead.
#' @param credentials `r api_key_param("AZURE_OPENAI_API_KEY")`
#' @inheritParams chat_openai
#' @inherit chat_openai return
#' @family chatbots
Expand Down Expand Up @@ -69,46 +64,43 @@ chat_azure_openai <- function(
check_string(model)
params <- params %||% params()
api_version <- set_default(api_version, "2024-10-21")
check_string(api_key, allow_null = TRUE)
api_key <- api_key %||% Sys.getenv("AZURE_OPENAI_API_KEY")
echo <- check_echo(echo)

if (is_list(credentials)) {
static_credentials <- force(credentials)
credentials <- function() static_credentials
}
check_function(credentials, allow_null = TRUE)
credentials <- credentials %||% default_azure_credentials(api_key)
credentials <- as_credentials(
"chat_azure_openai",
default_azure_credentials(),
credentials = credentials,
api_key = api_key
)

echo <- check_echo(echo)

provider <- ProviderAzureOpenAI(
name = "Azure/OpenAI",
base_url = paste0(endpoint, "/openai/deployments/", model),
model = model,
params = params,
api_version = api_version,
api_key = api_key,
credentials = credentials,
extra_args = api_args,
extra_headers = api_headers
)
Chat$new(provider = provider, system_prompt = system_prompt, echo = echo)
}


chat_azure_openai_test <- function(
system_prompt = NULL,
params = NULL,
...,
echo = "none"
) {
api_key <- key_get("AZURE_OPENAI_API_KEY")
credentials <- \() key_get("AZURE_OPENAI_API_KEY")
default_params <- params(seed = 1014, temperature = 0)
params <- modify_list(default_params, params %||% params())

chat_azure_openai(
...,
system_prompt = system_prompt,
api_key = api_key,
credentials = credentials,
endpoint = "https://ai-hwickhamai260967855527.openai.azure.com",
model = "gpt-4o-mini",
params = params,
Expand All @@ -120,7 +112,6 @@ ProviderAzureOpenAI <- new_class(
"ProviderAzureOpenAI",
parent = ProviderOpenAI,
properties = list(
credentials = class_function,
api_version = prop_string()
)
)
Expand All @@ -132,14 +123,13 @@ azure_endpoint <- function() {

# https://learn.microsoft.com/en-us/azure/ai-services/openai/reference#chat-completions
method(base_request, ProviderAzureOpenAI) <- function(provider) {
req <- base_request(super(provider, ProviderOpenAI))
req <- req_headers(req, Authorization = NULL)
req <- request(provider@base_url)
req <- ellmer_req_robustify(req)
req <- ellmer_req_user_agent(req)
req <- base_request_error(provider, req)

req <- req_url_query(req, `api-version` = provider@api_version)
if (nchar(provider@api_key)) {
req <- req_headers_redacted(req, `api-key` = provider@api_key)
}
req <- ellmer_req_credentials(req, provider@credentials)
req <- ellmer_req_credentials(req, provider@credentials(), "api-key")
req
}

Expand Down Expand Up @@ -170,7 +160,7 @@ method(base_request_error, ProviderAzureOpenAI) <- function(provider, req) {
})
}

default_azure_credentials <- function(api_key = NULL) {
default_azure_credentials <- function() {
azure_openai_scope <- "https://cognitiveservices.azure.com/.default"

# Detect viewer-based credentials from Posit Connect.
Expand Down Expand Up @@ -211,9 +201,10 @@ default_azure_credentials <- function(api_key = NULL) {
})
}

# If we have an API key, rely on that for credentials.
# If we have an API key, include it in the credentials.
api_key <- Sys.getenv("AZURE_OPENAI_API_KEY")
if (nchar(api_key)) {
return(function() list())
return(\() api_key)
}

# Masquerade as the Azure CLI.
Expand Down
18 changes: 12 additions & 6 deletions R/provider-cloudflare.R
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,10 @@ NULL
#'
#' @family chatbots
#' @param model `r param_model("meta-llama/Llama-3.3-70b-instruct-fp8-fast")`
#' @param api_key `r api_key_param("CLOUDFLARE_API_KEY")`
#' @param account The Cloudflare account ID. Taken from the
#' `CLOUDFLARE_ACCOUNT_ID` env var, if defined.
#' @param api_key The API key to use for authentication. You generally should
#' not supply this directly, but instead set the `HUGGINGFACE_API_KEY` environment
#' variable.
#' @param api_key `r lifecycle::badge("deprecated")` Use `credentials` instead.
#' @param credentials `r api_key_param("CLOUDFLARE_API_KEY")`
#' @export
#' @inheritParams chat_openai
#' @inherit chat_openai return
Expand All @@ -33,7 +31,8 @@ chat_cloudflare <- function(
account = cloudflare_account(),
system_prompt = NULL,
params = NULL,
api_key = cloudflare_key(),
api_key = NULL,
credentials = NULL,
model = NULL,
api_args = list(),
echo = NULL,
Expand All @@ -45,6 +44,13 @@ chat_cloudflare <- function(
echo <- check_echo(echo)
params <- params %||% params()

credentials <- as_credentials(
"chat_cloudflare",
function() cloudflare_key(),
credentials = credentials,
api_key = api_key
)

# https://developers.cloudflare.com/workers-ai/configuration/open-ai-compatibility/
cloudflare_api <- "https://api.cloudflare.com/client/v4/accounts/"
base_url <- paste0(cloudflare_api, cloudflare_account(), "/ai/v1/")
Expand All @@ -54,7 +60,7 @@ chat_cloudflare <- function(
base_url = base_url,
model = model,
params = params,
api_key = api_key,
credentials = credentials,
extra_args = api_args,
extra_headers = api_headers
)
Expand Down
8 changes: 2 additions & 6 deletions R/provider-databricks.R
Original file line number Diff line number Diff line change
Expand Up @@ -77,23 +77,19 @@ chat_databricks <- function(
params = params,
extra_args = api_args,
credentials = credentials,
# Databricks APIs use bearer tokens, not API keys, but we need to pass an
# empty string here anyway to make S7::validate() happy.
api_key = "",
extra_headers = api_headers
)
Chat$new(provider = provider, system_prompt = system_prompt, echo = echo)
}

ProviderDatabricks <- new_class(
"ProviderDatabricks",
parent = ProviderOpenAI,
properties = list(credentials = class_function)
parent = ProviderOpenAI
)

method(base_request, ProviderDatabricks) <- function(provider) {
req <- request(provider@base_url)
req <- ellmer_req_credentials(req, provider@credentials)
req <- ellmer_req_credentials(req, provider@credentials())
req <- ellmer_req_robustify(req)
req <- ellmer_req_user_agent(req, databricks_user_agent())
req <- base_request_error(provider, req)
Expand Down
15 changes: 12 additions & 3 deletions R/provider-deepseek.R
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,8 @@ NULL
#' @export
#' @family chatbots
#' @inheritParams chat_openai
#' @param api_key `r api_key_param("DEEPSEEK_API_KEY")`
#' @param api_key `r lifecycle::badge("deprecated")` Use `credentials` instead.
#' @param credentials `r api_key_param("DEEPSEEK_API_KEY")`
#' @param base_url The base URL to the endpoint; the default uses DeepSeek.
#' @param model `r param_model("deepseek-chat")`
#' @inherit chat_openai return
Expand All @@ -26,7 +27,8 @@ NULL
chat_deepseek <- function(
system_prompt = NULL,
base_url = "https://api.deepseek.com",
api_key = deepseek_key(),
api_key = NULL,
credentials = NULL,
model = NULL,
params = NULL,
api_args = list(),
Expand All @@ -36,6 +38,13 @@ chat_deepseek <- function(
model <- set_default(model, "deepseek-chat")
echo <- check_echo(echo)

credentials <- as_credentials(
"chat_deepseek",
function() deepseek_key(),
credentials = credentials,
api_key = api_key
)

params <- params %||% params()

provider <- ProviderDeepSeek(
Expand All @@ -44,7 +53,7 @@ chat_deepseek <- function(
model = model,
params = params,
extra_args = api_args,
api_key = api_key,
credentials = credentials,
extra_headers = api_headers
)
Chat$new(provider = provider, system_prompt = system_prompt, echo = echo)
Expand Down
Loading
Loading