diff --git a/DESCRIPTION b/DESCRIPTION index b833fe96..504841e2 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -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' diff --git a/NEWS.md b/NEWS.md index 1d06ae12..8d1de26c 100644 --- a/NEWS.md +++ b/NEWS.md @@ -1,5 +1,6 @@ # ellmer (development version) +* `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). * `chat_databricks()` lifts many of its restrictions now that the DataBrick's API is more OpenAI compatible (#757). * New `Chat$get_cost_details()` to get turn-by-token token usage + costs (#812). * Updated pricing data (#790). diff --git a/R/httr2.R b/R/httr2.R index ebe56fc8..7a20e283 100644 --- a/R/httr2.R +++ b/R/httr2.R @@ -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) diff --git a/R/provider-anthropic.R b/R/provider-anthropic.R index 0a550093..1877be02 100644 --- a/R/provider-anthropic.R +++ b/R/provider-anthropic.R @@ -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 @@ -35,7 +36,8 @@ 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 ) { @@ -43,6 +45,13 @@ chat_anthropic <- function( 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, @@ -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) @@ -77,7 +86,6 @@ ProviderAnthropic <- new_class( "ProviderAnthropic", parent = Provider, properties = list( - prop_redacted("api_key"), beta_headers = class_character ) ) @@ -94,7 +102,7 @@ method(base_request, ProviderAnthropic) <- function(provider) { # req <- req_headers(req, `anthropic-version` = "2023-06-01") # - req <- req_headers_redacted(req, `x-api-key` = provider@api_key) + req <- ellmer_req_credentials(req, provider@credentials(), "x-api-key") # # @@ -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) diff --git a/R/provider-azure.R b/R/provider-azure.R index 9d9a4f12..634e3198 100644 --- a/R/provider-azure.R +++ b/R/provider-azure.R @@ -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 @@ -69,16 +64,15 @@ 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", @@ -86,7 +80,6 @@ chat_azure_openai <- function( model = model, params = params, api_version = api_version, - api_key = api_key, credentials = credentials, extra_args = api_args, extra_headers = api_headers @@ -94,21 +87,20 @@ chat_azure_openai <- function( 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, @@ -120,7 +112,6 @@ ProviderAzureOpenAI <- new_class( "ProviderAzureOpenAI", parent = ProviderOpenAI, properties = list( - credentials = class_function, api_version = prop_string() ) ) @@ -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 } @@ -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. @@ -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. diff --git a/R/provider-cloudflare.R b/R/provider-cloudflare.R index c86126ed..1c23079c 100644 --- a/R/provider-cloudflare.R +++ b/R/provider-cloudflare.R @@ -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 @@ -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, @@ -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/") @@ -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 ) diff --git a/R/provider-databricks.R b/R/provider-databricks.R index adfa8227..9737dbf4 100644 --- a/R/provider-databricks.R +++ b/R/provider-databricks.R @@ -72,9 +72,6 @@ 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) @@ -82,13 +79,12 @@ chat_databricks <- function( 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) diff --git a/R/provider-deepseek.R b/R/provider-deepseek.R index 5a61e69e..e66b37e4 100644 --- a/R/provider-deepseek.R +++ b/R/provider-deepseek.R @@ -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 @@ -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(), @@ -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( @@ -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) diff --git a/R/provider-github.R b/R/provider-github.R index cf1bd598..379c2170 100644 --- a/R/provider-github.R +++ b/R/provider-github.R @@ -13,7 +13,8 @@ #' used in \pkg{ellmer} v0.3.0 and earlier. #' #' @family chatbots -#' @param api_key `r api_key_param("GITHUB_PAT")` +#' @param api_key `r lifecycle::badge("deprecated")` Use `credentials` instead. +#' @param credentials `r api_key_param("GITHUB_PAT")` #' @param model `r param_model("gpt-4o")` #' @param params Common model parameters, usually created by [params()]. #' @export @@ -27,7 +28,8 @@ chat_github <- function( system_prompt = NULL, base_url = "https://models.github.ai/inference/", - api_key = github_key(), + api_key = NULL, + credentials = NULL, model = NULL, params = NULL, api_args = list(), @@ -39,13 +41,20 @@ chat_github <- function( model <- set_default(model, "gpt-4.1") echo <- check_echo(echo) + credentials <- as_credentials( + "chat_github", + function() github_key(), + credentials = credentials, + api_key = api_key + ) + # https://docs.github.com/en/rest/models/inference?apiVersion=2022-11-28 params <- params %||% params() chat_openai( system_prompt = system_prompt, base_url = base_url, - api_key = api_key, + credentials = credentials, model = model, params = params, api_args = api_args, @@ -70,13 +79,20 @@ github_key <- function() { #' @export models_github <- function( base_url = "https://models.github.ai/", - api_key = github_key() + api_key = NULL, + credentials = NULL ) { + credentials <- as_credentials( + "models_github", + function() github_key(), + credentials = credentials, + api_key = api_key + ) + provider <- ProviderOpenAI( name = "github", model = "", - base_url = base_url, - api_key = api_key + credentials = credentials ) req <- base_request(provider) diff --git a/R/provider-google-upload.R b/R/provider-google-upload.R index e57fceb4..586a7f2b 100644 --- a/R/provider-google-upload.R +++ b/R/provider-google-upload.R @@ -31,9 +31,15 @@ google_upload <- function( path, base_url = "https://generativelanguage.googleapis.com/", api_key = NULL, + credentials = NULL, mime_type = NULL ) { - credentials <- default_google_credentials(api_key, variant = "gemini") + credentials <- as_credentials( + "google_upload", + default_google_credentials(variant = "gemini"), + credentials = credentials, + api_key = api_key + ) mime_type <- mime_type %||% guess_mime_type(path) @@ -60,7 +66,7 @@ google_upload_init <- function(path, base_url, credentials, mime_type) { display_name <- basename(path) req <- request(base_url) - req <- ellmer_req_credentials(req, credentials) + req <- ellmer_req_credentials(req, credentials(), "x-goog-api-key") req <- req_url_path_append(req, "upload/v1beta/files") req <- req_headers( req, @@ -79,7 +85,7 @@ google_upload_send <- function(upload_url, path, credentials) { file_size <- file.size(path) req <- request(upload_url) - req <- ellmer_req_credentials(req, credentials) + req <- ellmer_req_credentials(req, credentials(), "x-goog-api-key") req <- req_headers( req, "Content-Length" = toString(file_size), @@ -95,7 +101,7 @@ google_upload_send <- function(upload_url, path, credentials) { google_upload_status <- function(uri, credentials) { req <- request(uri) - req <- ellmer_req_credentials(req, credentials) + req <- ellmer_req_credentials(req, credentials(), "x-goog-api-key") resp <- req_perform(req) resp_body_json(resp) diff --git a/R/provider-google.R b/R/provider-google.R index 8ae92809..acb2fd6a 100644 --- a/R/provider-google.R +++ b/R/provider-google.R @@ -15,14 +15,17 @@ NULL #' #' ## Authentication #' By default, `chat_google_gemini()` will use Google's default application -#' credentials if there is no API key provided. This requires the \pkg{gargle} -#' package. +#' credentials. This requires the \pkg{gargle} package. #' -#' It can also pick up on viewer-based credentials on Posit Connect. This in -#' turn requires the \pkg{connectcreds} package. +#' Alternatively, you can use an API key by setting env var `GOOGLE_API_KEY` or, +#' for `chat_google_gemini()` only, `GEMINI_API_KEY`. #' -#' @param api_key `r api_key_param("GOOGLE_API_KEY")` -#' For Gemini, you can alternatively set `GEMINI_API_KEY`. +#' Finally these functions will also pick up on viewer-based credentials on +#' Posit Connect. This requires the \pkg{connectcreds} package. +#' +#' @param api_key `r lifecycle::badge("deprecated")` Use `credentials` instead. +#' @param credentials A function that returns a list of authentication headers +#' or `NULL`, the default, to use ambient credentials. See above for details. #' @param model `r param_model("gemini-2.5-flash", "google_gemini")` #' @inheritParams chat_openai #' @inherit chat_openai return @@ -37,6 +40,7 @@ chat_google_gemini <- function( system_prompt = NULL, base_url = "https://generativelanguage.googleapis.com/v1beta/", api_key = NULL, + credentials = NULL, model = NULL, params = NULL, api_args = list(), @@ -45,7 +49,13 @@ chat_google_gemini <- function( ) { model <- set_default(model, "gemini-2.5-flash") echo <- check_echo(echo) - credentials <- default_google_credentials(api_key, variant = "gemini") + + credentials <- as_credentials( + "chat_google_gemini", + default_google_credentials(variant = "gemini"), + credentials = credentials, + api_key = api_key + ) provider <- ProviderGoogleGemini( name = "Google/Gemini", @@ -54,7 +64,6 @@ chat_google_gemini <- function( params = params %||% params(), extra_args = api_args, extra_headers = api_headers, - api_key = api_key, credentials = credentials ) Chat$new(provider = provider, system_prompt = system_prompt, echo = echo) @@ -122,8 +131,6 @@ ProviderGoogleGemini <- new_class( "ProviderGoogleGemini", parent = Provider, properties = list( - api_key = prop_string(allow_null = TRUE), - credentials = class_function | NULL, model = prop_string() ) ) @@ -132,7 +139,7 @@ ProviderGoogleGemini <- new_class( method(base_request, ProviderGoogleGemini) <- function(provider) { req <- request(provider@base_url) - req <- ellmer_req_credentials(req, provider@credentials) + req <- ellmer_req_credentials(req, provider@credentials(), "x-goog-api-key") req <- ellmer_req_robustify(req) req <- ellmer_req_user_agent(req) req <- req_error(req, body = function(resp) { @@ -611,12 +618,19 @@ merge_gemini_chunks <- merge_objects( ) default_google_credentials <- function( - api_key = NULL, error_call = caller_env(), variant = c("gemini", "vertex") ) { variant <- arg_match(variant) + api_key <- Sys.getenv("GOOGLE_API_KEY") + if (variant == "gemini" && api_key == "") { + api_key <- Sys.getenv("GEMINI_API_KEY") + } + if (nzchar(api_key)) { + return(\() api_key) + } + gemini_scope <- switch( variant, gemini = "https://www.googleapis.com/auth/generative-language.retriever", @@ -624,18 +638,6 @@ default_google_credentials <- function( vertex = "https://www.googleapis.com/auth/cloud-platform" ) - check_string(api_key, allow_null = TRUE, call = error_call) - api_key <- api_key %||% Sys.getenv("GOOGLE_API_KEY") - if (variant == "gemini" && api_key == "") { - api_key <- Sys.getenv("GEMINI_API_KEY") - } - - if (nzchar(api_key)) { - return(function() { - list("x-goog-api-key" = api_key) - }) - } - # Detect viewer-based credentials from Posit Connect. if (has_connect_viewer_token(scope = gemini_scope)) { return(function() { @@ -705,20 +707,30 @@ default_google_credentials <- function( #' @rdname chat_google_gemini models_google_gemini <- function( base_url = "https://generativelanguage.googleapis.com/v1beta/", - api_key = NULL + api_key = NULL, + credentials = NULL ) { check_string(base_url) - check_string(api_key, allow_null = TRUE) - models_google(base_url, api_key, variant = "gemini") + credentials <- as_credentials( + "models_google_gemini", + default_google_credentials(variant = "gemini"), + credentials = credentials, + api_key = api_key + ) + + models_google(base_url, credentials = credentials, variant = "gemini") } #' @rdname chat_google_gemini #' @export -models_google_vertex <- function(location, project_id) { +models_google_vertex <- function(location, project_id, credentials = NULL) { check_string(location) check_string(project_id) + credentials <- credentials %||% default_google_credentials(variant = "vertex") + check_credentials(credentials) + base_url <- paste_c( c("https://", google_location(location), "aiplatform.googleapis.com"), "/v1beta1", @@ -730,16 +742,11 @@ models_google_vertex <- function(location, project_id) { models_google <- function( base_url = "https://generativelanguage.googleapis.com/v1beta/", - api_key = NULL, + credentials, project_id = NULL, variant = c("gemini", "vertex") ) { variant <- arg_match(variant) - credentials <- switch( - variant, - vertex = default_google_credentials(variant = "vertex"), - gemini = default_google_credentials(api_key, variant = "gemini") - ) provider <- ProviderGoogleGemini( name = "Google/Gemini", diff --git a/R/provider-groq.R b/R/provider-groq.R index 15dac28a..48e3c9ae 100644 --- a/R/provider-groq.R +++ b/R/provider-groq.R @@ -15,7 +15,8 @@ NULL #' #' @export #' @family chatbots -#' @param api_key `r api_key_param("GROQ_API_KEY")` +#' @param api_key `r lifecycle::badge("deprecated")` Use `credentials` instead. +#' @param credentials `r api_key_param("GROQ_API_KEY")` #' @param model `r param_model("llama3-8b-8192")` #' @param params Common model parameters, usually created by [params()]. #' @inheritParams chat_openai @@ -28,7 +29,8 @@ NULL chat_groq <- function( system_prompt = NULL, base_url = "https://api.groq.com/openai/v1", - api_key = groq_key(), + api_key = NULL, + credentials = NULL, model = NULL, params = NULL, api_args = list(), @@ -38,6 +40,13 @@ chat_groq <- function( model <- set_default(model, "llama3-8b-8192") echo <- check_echo(echo) + credentials <- as_credentials( + "chat_groq", + function() groq_key(), + credentials = credentials, + api_key = api_key + ) + # https://console.groq.com/docs/api-reference#chat-create (same as OpenAI) params <- params %||% params() @@ -47,7 +56,7 @@ chat_groq <- 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) diff --git a/R/provider-huggingface.R b/R/provider-huggingface.R index 705fd318..f291983c 100644 --- a/R/provider-huggingface.R +++ b/R/provider-huggingface.R @@ -19,9 +19,8 @@ #' #' @family chatbots #' @param model `r param_model("meta-llama/Llama-3.1-8B-Instruct")` -#' @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("HUGGINGFACE_API_KEY")` #' @export #' @inheritParams chat_openai #' @inherit chat_openai return @@ -33,7 +32,8 @@ chat_huggingface <- function( system_prompt = NULL, params = NULL, - api_key = hf_key(), + api_key = NULL, + credentials = NULL, model = NULL, api_args = list(), echo = NULL, @@ -43,6 +43,13 @@ chat_huggingface <- function( echo <- check_echo(echo) params <- params %||% params() + credentials <- as_credentials( + "chat_huggingface", + function() hf_key(), + credentials = credentials, + api_key = api_key + ) + # https://huggingface.co/docs/inference-providers/en/index?python-clients=requests#http--curl base_url <- "https://router.huggingface.co/v1/" @@ -52,7 +59,7 @@ chat_huggingface <- 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) diff --git a/R/provider-mistral.R b/R/provider-mistral.R index 938433f0..a063e9f7 100644 --- a/R/provider-mistral.R +++ b/R/provider-mistral.R @@ -11,7 +11,8 @@ #' @export #' @family chatbots #' @param model `r param_model("mistral-large-latest")` -#' @param api_key `r api_key_param("MISTRAL_API_KEY")` +#' @param api_key `r lifecycle::badge("deprecated")` Use `credentials` instead. +#' @param credentials `r api_key_param("MISTRAL_API_KEY")` #' @inheritParams chat_openai #' @inherit chat_openai return #' @examples @@ -22,7 +23,8 @@ chat_mistral <- function( system_prompt = NULL, params = NULL, - api_key = mistral_key(), + api_key = NULL, + credentials = NULL, model = NULL, api_args = list(), echo = NULL, @@ -32,13 +34,20 @@ chat_mistral <- function( model <- set_default(model, "mistral-large-latest") echo <- check_echo(echo) + credentials <- as_credentials( + "chat_mistral", + function() mistral_key(), + credentials = credentials, + api_key = api_key + ) + provider <- ProviderMistral( name = "Mistral", base_url = mistral_base_url, 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) @@ -130,7 +139,7 @@ models_mistral <- function(api_key = mistral_key()) { name = "Mistral", model = "", base_url = mistral_base_url, - api_key = api_key + credentials = function() api_key ) req <- base_request(provider) diff --git a/R/provider-ollama.R b/R/provider-ollama.R index fe68a077..eb10b475 100644 --- a/R/provider-ollama.R +++ b/R/provider-ollama.R @@ -22,12 +22,14 @@ #' #' @inheritParams chat_openai #' @param model `r param_model(NULL, "ollama")` -#' @param api_key Ollama doesn't require an API key for local usage and in most -#' cases you do not need to provide an `api_key`. +#' @param api_key `r lifecycle::badge("deprecated")` Use `credentials` instead. +#' @param credentials Ollama doesn't require credentials for local usage and in most +#' cases you do not need to provide `credentials`. #' #' However, if you're accessing an Ollama instance hosted behind a reverse #' proxy or secured endpoint that enforces bearer‐token authentication, you -#' can set `api_key` (or the `OLLAMA_API_KEY` environment variable). +#' can set the `OLLAMA_API_KEY` environment variable or provide a callback +#' function to `credentials`. #' @param params Common model parameters, usually created by [params()]. #' @inherit chat_openai return #' @family chatbots @@ -45,6 +47,7 @@ chat_ollama <- function( api_args = list(), echo = NULL, api_key = NULL, + credentials = NULL, api_headers = character() ) { if (!has_ollama(base_url)) { @@ -70,15 +73,22 @@ chat_ollama <- function( echo <- check_echo(echo) + # ollama doesn't require an API key for local usage, but one might be needed + # if ollama is served behind a proxy (see #501) + credentials <- as_credentials( + "chat_ollama", + function() Sys.getenv("OLLAMA_API_KEY", ""), + credentials = credentials, + api_key = api_key + ) + provider <- ProviderOllama( name = "Ollama", base_url = file.path(base_url, "v1"), ## the v1 portion of the path is added for openAI compatible API model = model, params = params %||% params(), extra_args = api_args, - # ollama doesn't require an API key for local usage, but one might be needed - # if ollama is served behind a proxy (see #501) - api_key = api_key %||% Sys.getenv("OLLAMA_API_KEY", "ollama"), + credentials = credentials, extra_headers = api_headers ) @@ -89,7 +99,6 @@ ProviderOllama <- new_class( "ProviderOllama", parent = ProviderOpenAI, properties = list( - prop_redacted("api_key"), model = prop_string() ) ) diff --git a/R/provider-openai-responses.R b/R/provider-openai-responses.R index 1970824d..eb31e0af 100644 --- a/R/provider-openai-responses.R +++ b/R/provider-openai-responses.R @@ -32,7 +32,7 @@ NULL chat_openai_responses <- function( system_prompt = NULL, base_url = Sys.getenv("OPENAI_BASE_URL", "https://api.openai.com/v1"), - api_key = openai_key(), + credentials = NULL, model = NULL, params = NULL, api_args = list(), @@ -42,6 +42,9 @@ chat_openai_responses <- function( model <- set_default(model, "gpt-4.1") echo <- check_echo(echo) + credentials <- credentials %||% \() paste0("Bearer ", openai_key()) + check_credentials(credentials) + provider <- ProviderOpenAIResponses( name = "OpenAI", base_url = base_url, @@ -49,7 +52,7 @@ chat_openai_responses <- function( params = params %||% params(), extra_args = api_args, extra_headers = api_headers, - api_key = api_key + credentials = credentials ) Chat$new(provider = provider, system_prompt = system_prompt, echo = echo) } @@ -75,11 +78,7 @@ chat_openai_responses_test <- function( ProviderOpenAIResponses <- new_class( "ProviderOpenAIResponses", parent = ProviderOpenAI, - properties = list( - prop_redacted("api_key"), - # no longer used by OpenAI itself; but subclasses still need it - seed = prop_number_whole(allow_null = TRUE) - ) + properties = list() ) # Chat endpoint ---------------------------------------------------------------- diff --git a/R/provider-openai.R b/R/provider-openai.R index f7067fe6..95979929 100644 --- a/R/provider-openai.R +++ b/R/provider-openai.R @@ -15,7 +15,8 @@ NULL #' #' @param system_prompt A system prompt to set the behavior of the assistant. #' @param base_url The base URL to the endpoint; the default uses OpenAI. -#' @param api_key `r api_key_param("OPENAI_API_KEY")` +#' @param api_key `r lifecycle::badge("deprecated")` Use `credentials` instead. +#' @param credentials `r api_key_param("OPENAI_API_KEY")` #' @param model `r param_model("gpt-4.1", "openai")` #' @param params Common model parameters, usually created by [params()]. #' @param api_args Named list of arbitrary extra arguments appended to the body @@ -46,7 +47,8 @@ NULL chat_openai <- function( system_prompt = NULL, base_url = Sys.getenv("OPENAI_BASE_URL", "https://api.openai.com/v1"), - api_key = openai_key(), + api_key = NULL, + credentials = NULL, model = NULL, params = NULL, api_args = list(), @@ -56,6 +58,14 @@ chat_openai <- function( model <- set_default(model, "gpt-4.1") echo <- check_echo(echo) + credentials <- as_credentials( + "chat_openai", + function() paste0("Bearer ", openai_key()), + credentials = credentials, + api_key = api_key, + token = TRUE + ) + provider <- ProviderOpenAI( name = "OpenAI", base_url = base_url, @@ -63,7 +73,7 @@ chat_openai <- function( params = params %||% params(), extra_args = api_args, extra_headers = api_headers, - api_key = api_key + credentials = credentials ) Chat$new(provider = provider, system_prompt = system_prompt, echo = echo) } @@ -89,10 +99,7 @@ chat_openai_test <- function( ProviderOpenAI <- new_class( "ProviderOpenAI", - parent = Provider, - properties = list( - prop_redacted("api_key") - ) + parent = Provider ) openai_key_exists <- function() { @@ -107,7 +114,7 @@ openai_key <- function() { method(base_request, ProviderOpenAI) <- function(provider) { req <- request(provider@base_url) - req <- req_auth_bearer_token(req, provider@api_key) + req <- ellmer_req_credentials(req, provider@credentials(), "Authorization") req <- ellmer_req_robustify(req) req <- ellmer_req_user_agent(req) req <- base_request_error(provider, req) @@ -534,13 +541,21 @@ method(batch_result_turn, ProviderOpenAI) <- function( #' @export models_openai <- function( base_url = "https://api.openai.com/v1", - api_key = openai_key() + api_key = NULL, + credentials = NULL ) { + credentials <- as_credentials( + "models_openai", + function() paste0("Bearer ", openai_key()), + credentials = credentials, + api_key = api_key + ) + provider <- ProviderOpenAI( name = "OpenAI", model = "", base_url = base_url, - api_key = api_key + credentials = credentials ) req <- base_request(provider) diff --git a/R/provider-openrouter.R b/R/provider-openrouter.R index cd7e9bfb..928f5bfe 100644 --- a/R/provider-openrouter.R +++ b/R/provider-openrouter.R @@ -11,7 +11,8 @@ NULL #' #' @export #' @family chatbots -#' @param api_key `r api_key_param("OPENROUTER_API_KEY")` +#' @param api_key `r lifecycle::badge("deprecated")` Use `credentials` instead. +#' @param credentials `r api_key_param("OPENROUTER_API_KEY")` #' @param model `r param_model("gpt-4o")` #' @param params Common model parameters, usually created by [params()]. #' @inheritParams chat_openai @@ -23,7 +24,8 @@ NULL #' } chat_openrouter <- function( system_prompt = NULL, - api_key = openrouter_key(), + api_key = NULL, + credentials = NULL, model = NULL, params = NULL, api_args = list(), @@ -33,6 +35,13 @@ chat_openrouter <- function( model <- set_default(model, "gpt-4o") echo <- check_echo(echo) + credentials <- as_credentials( + "chat_openrouter", + function() openrouter_key(), + credentials = credentials, + api_key = api_key + ) + params <- params %||% params() provider <- ProviderOpenRouter( @@ -41,7 +50,7 @@ chat_openrouter <- 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) diff --git a/R/provider-perplexity.R b/R/provider-perplexity.R index 43d27a51..73274a45 100644 --- a/R/provider-perplexity.R +++ b/R/provider-perplexity.R @@ -16,7 +16,8 @@ NULL #' #' @export #' @family chatbots -#' @param api_key `r api_key_param("PERPLEXITY_API_KEY")` +#' @param api_key `r lifecycle::badge("deprecated")` Use `credentials` instead. +#' @param credentials `r api_key_param("PERPLEXITY_API_KEY")` #' @param model `r param_model("llama-3.1-sonar-small-128k-online")` #' @param params Common model parameters, usually created by [params()]. #' @inheritParams chat_openai @@ -29,7 +30,8 @@ NULL chat_perplexity <- function( system_prompt = NULL, base_url = "https://api.perplexity.ai/", - api_key = perplexity_key(), + api_key = NULL, + credentials = NULL, model = NULL, params = NULL, api_args = list(), @@ -39,6 +41,13 @@ chat_perplexity <- function( model <- set_default(model, "llama-3.1-sonar-small-128k-online") echo <- check_echo(echo) + credentials <- as_credentials( + "chat_perplexity", + function() perplexity_key(), + credentials = credentials, + api_key = api_key + ) + params <- params %||% params() provider <- ProviderPerplexity( @@ -47,7 +56,7 @@ chat_perplexity <- 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) diff --git a/R/provider-portkey.R b/R/provider-portkey.R index cec95f43..e00c73cf 100644 --- a/R/provider-portkey.R +++ b/R/provider-portkey.R @@ -10,7 +10,8 @@ #' stored inside Portkey application. #' #' @family chatbots -#' @param api_key `r api_key_param("PORTKEY_API_KEY")` +#' @param api_key `r lifecycle::badge("deprecated")` Use `credentials` instead. +#' @param credentials `r api_key_param("PORTKEY_API_KEY")` #' @param model `r param_model("gpt-4o", "openai")` #' @param virtual_key A virtual identifier storing LLM provider's API key. See #' [documentation](https://portkey.ai/docs/product/ai-gateway/virtual-keys). @@ -26,7 +27,8 @@ chat_portkey <- function( system_prompt = NULL, base_url = "https://api.portkey.ai/v1", - api_key = portkey_key(), + api_key = NULL, + credentials = NULL, virtual_key = portkey_virtual_key(), model = NULL, params = NULL, @@ -37,6 +39,13 @@ chat_portkey <- function( model <- set_default(model, "gpt-4o") echo <- check_echo(echo) + credentials <- as_credentials( + "chat_portkey", + function() portkey_key(), + credentials = credentials, + api_key = api_key + ) + params <- params %||% params() provider <- ProviderPortkeyAI( name = "PortkeyAI", @@ -44,7 +53,7 @@ chat_portkey <- function( model = model, params = params, extra_args = api_args, - api_key = api_key, + credentials = credentials, virtual_key = virtual_key, extra_headers = api_headers ) @@ -90,11 +99,13 @@ portkey_virtual_key <- function() { method(base_request, ProviderPortkeyAI) <- function(provider) { req <- request(provider@base_url) - req <- httr2::req_headers( + + req <- ellmer_req_credentials( req, - `x-portkey-api-key` = provider@api_key, - `x-portkey-virtual-key` = provider@virtual_key + provider@credentials(), + "x-portkey-api-key" ) + req <- httr2::req_headers(req, `x-portkey-virtual-key` = provider@virtual_key) req <- ellmer_req_robustify(req) req <- ellmer_req_user_agent(req) req <- base_request_error(provider, req) @@ -113,7 +124,7 @@ models_portkey <- function( name = "PortkeyAI", model = "", base_url = base_url, - api_key = api_key, + credentials = function() api_key, virtual_key = virtual_key ) diff --git a/R/provider-snowflake.R b/R/provider-snowflake.R index ea515571..24c2dd01 100644 --- a/R/provider-snowflake.R +++ b/R/provider-snowflake.R @@ -53,12 +53,8 @@ chat_snowflake <- function( params <- params %||% params() echo <- check_echo(echo) - if (is_list(credentials)) { - static_credentials <- force(credentials) - credentials <- function(account) static_credentials - } - check_function(credentials, allow_null = TRUE) credentials <- credentials %||% default_snowflake_credentials(account) + check_credentials(credentials) provider <- ProviderSnowflakeCortex( name = "Snowflake/Cortex", @@ -68,8 +64,6 @@ chat_snowflake <- function( model = model, params = params, extra_args = api_args, - # We need an empty api_key for S7 validation. - api_key = "", extra_headers = api_headers ) @@ -80,14 +74,13 @@ ProviderSnowflakeCortex <- new_class( "ProviderSnowflakeCortex", parent = ProviderOpenAI, properties = list( - account = prop_string(), - credentials = class_function + account = prop_string() ) ) method(base_request, ProviderSnowflakeCortex) <- 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) # Snowflake uses the User Agent header to identify "parter applications", so # identify requests as coming from "r_ellmer" (unless an explicit partner diff --git a/R/provider-vllm.R b/R/provider-vllm.R index 51287292..39da5d74 100644 --- a/R/provider-vllm.R +++ b/R/provider-vllm.R @@ -10,7 +10,8 @@ NULL #' `chat_vllm()` to connect to endpoints powered by vLLM. #' #' @inheritParams chat_openai -#' @param api_key `r api_key_param("VLLM_API_KEY")` +#' @param api_key `r lifecycle::badge("deprecated")` Use `credentials` instead. +#' @param credentials `r api_key_param("VLLM_API_KEY")` #' @param model `r param_model(NULL, "vllm")` #' @param params Common model parameters, usually created by [params()]. #' @inherit chat_openai return @@ -26,14 +27,23 @@ chat_vllm <- function( model, params = NULL, api_args = list(), - api_key = vllm_key(), + api_key = NULL, + credentials = NULL, echo = NULL, api_headers = character() ) { check_string(base_url) - check_string(api_key) + + credentials <- as_credentials( + "chat_vllm", + function() vllm_key(), + credentials = credentials, + api_key = api_key + ) + + check_string(credentials()) if (missing(model)) { - models <- models_vllm(base_url, api_key)$id + models <- models_vllm(base_url, credentials = credentials)$id cli::cli_abort(c( "Must specify {.arg model}.", i = "Available models: {.str {models}}." @@ -50,7 +60,7 @@ chat_vllm <- 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) @@ -89,9 +99,16 @@ vllm_key <- function() { #' @export #' @rdname chat_vllm -models_vllm <- function(base_url, api_key = vllm_key()) { +models_vllm <- function(base_url, api_key = NULL, credentials = NULL) { + credentials <- as_credentials( + "models_vllm", + function() vllm_key(), + credentials = credentials, + api_key = api_key + ) + req <- request(base_url) - req <- req_auth_bearer_token(req, api_key) + req <- req_auth_bearer_token(req, credentials()) req <- req_url_path_append(req, "/v1/models") resp <- req_perform(req) json <- resp_body_json(resp) diff --git a/R/provider.R b/R/provider.R index 2cfbb0dd..360433b5 100644 --- a/R/provider.R +++ b/R/provider.R @@ -17,6 +17,9 @@ NULL #' @param model Name of the model. #' @param base_url The base URL for the API. #' @param params A list of standard parameters created by [params()]. +#' @param credentials A zero-argument function that returns the credentials to use +#' for authentication. Can either return a string, representing an API key, +#' or a named list of headers. #' @param extra_args Arbitrary extra arguments to be included in the request body. #' @param extra_headers Arbitrary extra headers to be added to the request. #' @return An S7 Provider object. @@ -34,7 +37,8 @@ Provider <- new_class( base_url = prop_string(), params = class_list, extra_args = class_list, - extra_headers = class_character + extra_headers = class_character, + credentials = class_function | NULL ) ) diff --git a/R/utils-auth.R b/R/utils-auth.R new file mode 100644 index 00000000..669c1f81 --- /dev/null +++ b/R/utils-auth.R @@ -0,0 +1,71 @@ +as_credentials <- function( + fun_name, + default, + credentials = NULL, + api_key = NULL, + token = FALSE, + env = caller_env(), + user_env = caller_env(2) +) { + if (is.null(credentials) && is.null(api_key)) { + default + } else if (!is.null(credentials) && is.null(api_key)) { + check_credentials(credentials, error_call = env) + credentials + } else if (is.null(credentials) && !is.null(api_key)) { + check_string(api_key, allow_null = TRUE, call = env) + + lifecycle::deprecate_warn( + "0.4.0", + paste0(fun_name, "(api_key)"), + paste0(fun_name, "(credentials)"), + env = env, + user_env = user_env + ) + if (token) { + function() paste0("Bearer ", api_key) + } else { + function() api_key + } + } else { + cli::cli_abort( + "Must supply one of {.arg api_key} or {.arg credentials}.", + call = env + ) + } +} + +check_credentials <- function(credentials, error_call = caller_env()) { + check_function(credentials, allow_null = TRUE, call = error_call) + if (length(formals(credentials)) != 0) { + cli::cli_abort( + "{.arg credentials} must not have arguments.", + call = error_call + ) + } + + creds <- credentials() + if (!is_string(creds) && !(is_named(creds) && is.list(creds))) { + stop_input_type( + creds, + c("a string", "a named list"), + call = error_call, + arg = "credentials()" + ) + } + + invisible() +} + +ellmer_req_credentials <- function(req, credentials, key_name = NULL) { + if (is_string(credentials)) { + if (is.null(key_name)) { + cli::cli_abort( + "A `credentials()` function that returns a string is not supported by this provider.", + call = NULL + ) + } + credentials <- set_names(list(credentials), key_name) + } + req_headers_redacted(req, !!!credentials) +} diff --git a/R/utils.R b/R/utils.R index b413b3ff..940b29bb 100644 --- a/R/utils.R +++ b/R/utils.R @@ -168,10 +168,10 @@ cli_escape <- function(x) { api_key_param <- function(key) { paste_c( - "API key to use for authentication.\n", + "A function that returns the API key to use for authentication.\n", "\n", c( - "You generally should not supply this directly, but instead set the ", + "You generally should not need this argument; instead set the ", c("`", key, "`"), " environment variable.\n" ), diff --git a/man/Provider.Rd b/man/Provider.Rd index fb65eeb1..38187849 100644 --- a/man/Provider.Rd +++ b/man/Provider.Rd @@ -10,7 +10,8 @@ Provider( base_url = stop("Required"), params = list(), extra_args = list(), - extra_headers = character(0) + extra_headers = character(0), + credentials = function() NULL ) } \arguments{ @@ -25,6 +26,10 @@ Provider( \item{extra_args}{Arbitrary extra arguments to be included in the request body.} \item{extra_headers}{Arbitrary extra headers to be added to the request.} + +\item{credentials}{A zero-argument function that returns the credentials to use +for authentication. Can either return a string, representing an API key, +or a named list of headers.} } \value{ An S7 Provider object. diff --git a/man/chat_anthropic.Rd b/man/chat_anthropic.Rd index 6d558414..508c541b 100644 --- a/man/chat_anthropic.Rd +++ b/man/chat_anthropic.Rd @@ -13,7 +13,8 @@ chat_anthropic( 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 ) @@ -25,7 +26,8 @@ chat_claude( 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 ) @@ -53,9 +55,11 @@ with \code{\link[=modifyList]{modifyList()}}.} \item{beta_headers}{Optionally, a character vector of beta headers to opt-in claude features that are still in beta.} -\item{api_key}{API key to use for authentication. +\item{api_key}{\ifelse{html}{\href{https://lifecycle.r-lib.org/articles/stages.html#deprecated}{\figure{lifecycle-deprecated.svg}{options: alt='[Deprecated]'}}}{\strong{[Deprecated]}} Use \code{credentials} instead.} -You generally should not supply this directly, but instead set the \code{ANTHROPIC_API_KEY} environment variable. +\item{credentials}{A function that returns the API key to use for authentication. + +You generally should not need this argument; instead set the \code{ANTHROPIC_API_KEY} environment variable. The best place to set this is in \code{.Renviron}, which you can easily edit by calling \code{usethis::edit_r_environ()}.} diff --git a/man/chat_azure_openai.Rd b/man/chat_azure_openai.Rd index c7e1c9e9..39e4f802 100644 --- a/man/chat_azure_openai.Rd +++ b/man/chat_azure_openai.Rd @@ -31,19 +31,14 @@ value of the \code{AZURE_OPENAI_ENDPOINT} environment variable.} \item{system_prompt}{A system prompt to set the behavior of the assistant.} -\item{api_key}{API key to use for authentication. +\item{api_key}{\ifelse{html}{\href{https://lifecycle.r-lib.org/articles/stages.html#deprecated}{\figure{lifecycle-deprecated.svg}{options: alt='[Deprecated]'}}}{\strong{[Deprecated]}} Use \code{credentials} instead.} -You generally should not supply this directly, but instead set the \code{AZURE_OPENAI_API_KEY} environment variable. +\item{credentials}{A function that returns the API key to use for authentication. + +You generally should not need this argument; instead set the \code{AZURE_OPENAI_API_KEY} environment variable. The best place to set this is in \code{.Renviron}, which you can easily edit by calling \code{usethis::edit_r_environ()}.} -\item{credentials}{A list of authentication headers to pass into -\code{\link[httr2:req_headers]{httr2::req_headers()}}, a function that returns them, or \code{NULL} to use -\code{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.} - \item{api_args}{Named list of arbitrary extra arguments appended to the body of every chat API call. Combined with the body object generated by ellmer with \code{\link[=modifyList]{modifyList()}}.} diff --git a/man/chat_cloudflare.Rd b/man/chat_cloudflare.Rd index f1327e34..f2c7e1cf 100644 --- a/man/chat_cloudflare.Rd +++ b/man/chat_cloudflare.Rd @@ -8,7 +8,8 @@ chat_cloudflare( account = cloudflare_account(), system_prompt = NULL, params = NULL, - api_key = cloudflare_key(), + api_key = NULL, + credentials = NULL, model = NULL, api_args = list(), echo = NULL, @@ -23,9 +24,13 @@ chat_cloudflare( \item{params}{Common model parameters, usually created by \code{\link[=params]{params()}}.} -\item{api_key}{The API key to use for authentication. You generally should -not supply this directly, but instead set the \code{HUGGINGFACE_API_KEY} environment -variable.} +\item{api_key}{\ifelse{html}{\href{https://lifecycle.r-lib.org/articles/stages.html#deprecated}{\figure{lifecycle-deprecated.svg}{options: alt='[Deprecated]'}}}{\strong{[Deprecated]}} Use \code{credentials} instead.} + +\item{credentials}{A function that returns the API key to use for authentication. + +You generally should not need this argument; instead set the \code{CLOUDFLARE_API_KEY} environment variable. +The best place to set this is in \code{.Renviron}, +which you can easily edit by calling \code{usethis::edit_r_environ()}.} \item{model}{The model to use for the chat (defaults to "meta-llama/Llama-3.3-70b-instruct-fp8-fast"). We regularly update the default, so we strongly recommend explicitly specifying a model for anything other than casual use.} diff --git a/man/chat_deepseek.Rd b/man/chat_deepseek.Rd index fd858476..8bc14a39 100644 --- a/man/chat_deepseek.Rd +++ b/man/chat_deepseek.Rd @@ -7,7 +7,8 @@ chat_deepseek( 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(), @@ -20,9 +21,11 @@ chat_deepseek( \item{base_url}{The base URL to the endpoint; the default uses DeepSeek.} -\item{api_key}{API key to use for authentication. +\item{api_key}{\ifelse{html}{\href{https://lifecycle.r-lib.org/articles/stages.html#deprecated}{\figure{lifecycle-deprecated.svg}{options: alt='[Deprecated]'}}}{\strong{[Deprecated]}} Use \code{credentials} instead.} -You generally should not supply this directly, but instead set the \code{DEEPSEEK_API_KEY} environment variable. +\item{credentials}{A function that returns the API key to use for authentication. + +You generally should not need this argument; instead set the \code{DEEPSEEK_API_KEY} environment variable. The best place to set this is in \code{.Renviron}, which you can easily edit by calling \code{usethis::edit_r_environ()}.} diff --git a/man/chat_github.Rd b/man/chat_github.Rd index 9cfa700a..c9370345 100644 --- a/man/chat_github.Rd +++ b/man/chat_github.Rd @@ -8,7 +8,8 @@ chat_github( system_prompt = NULL, base_url = "https://models.github.ai/inference/", - api_key = github_key(), + api_key = NULL, + credentials = NULL, model = NULL, params = NULL, api_args = list(), @@ -16,16 +17,22 @@ chat_github( api_headers = character() ) -models_github(base_url = "https://models.github.ai/", api_key = github_key()) +models_github( + base_url = "https://models.github.ai/", + api_key = NULL, + credentials = NULL +) } \arguments{ \item{system_prompt}{A system prompt to set the behavior of the assistant.} \item{base_url}{The base URL to the endpoint; the default uses OpenAI.} -\item{api_key}{API key to use for authentication. +\item{api_key}{\ifelse{html}{\href{https://lifecycle.r-lib.org/articles/stages.html#deprecated}{\figure{lifecycle-deprecated.svg}{options: alt='[Deprecated]'}}}{\strong{[Deprecated]}} Use \code{credentials} instead.} + +\item{credentials}{A function that returns the API key to use for authentication. -You generally should not supply this directly, but instead set the \code{GITHUB_PAT} environment variable. +You generally should not need this argument; instead set the \code{GITHUB_PAT} environment variable. The best place to set this is in \code{.Renviron}, which you can easily edit by calling \code{usethis::edit_r_environ()}.} diff --git a/man/chat_google_gemini.Rd b/man/chat_google_gemini.Rd index e5f502f5..04452b22 100644 --- a/man/chat_google_gemini.Rd +++ b/man/chat_google_gemini.Rd @@ -11,6 +11,7 @@ chat_google_gemini( system_prompt = NULL, base_url = "https://generativelanguage.googleapis.com/v1beta/", api_key = NULL, + credentials = NULL, model = NULL, params = NULL, api_args = list(), @@ -31,22 +32,21 @@ chat_google_vertex( models_google_gemini( base_url = "https://generativelanguage.googleapis.com/v1beta/", - api_key = NULL + api_key = NULL, + credentials = NULL ) -models_google_vertex(location, project_id) +models_google_vertex(location, project_id, credentials = NULL) } \arguments{ \item{system_prompt}{A system prompt to set the behavior of the assistant.} \item{base_url}{The base URL to the endpoint; the default uses OpenAI.} -\item{api_key}{API key to use for authentication. +\item{api_key}{\ifelse{html}{\href{https://lifecycle.r-lib.org/articles/stages.html#deprecated}{\figure{lifecycle-deprecated.svg}{options: alt='[Deprecated]'}}}{\strong{[Deprecated]}} Use \code{credentials} instead.} -You generally should not supply this directly, but instead set the \code{GOOGLE_API_KEY} environment variable. -The best place to set this is in \code{.Renviron}, -which you can easily edit by calling \code{usethis::edit_r_environ()}. -For Gemini, you can alternatively set \code{GEMINI_API_KEY}.} +\item{credentials}{A function that returns a list of authentication headers +or \code{NULL}, the default, to use ambient credentials. See above for details.} \item{model}{The model to use for the chat (defaults to "gemini-2.5-flash"). We regularly update the default, so we strongly recommend explicitly specifying a model for anything other than casual use. @@ -88,11 +88,13 @@ Use \code{\link[=google_upload]{google_upload()}} to upload files (PDFs, images, \subsection{Authentication}{ By default, \code{chat_google_gemini()} will use Google's default application -credentials if there is no API key provided. This requires the \pkg{gargle} -package. +credentials. This requires the \pkg{gargle} package. + +Alternatively, you can use an API key by setting env var \code{GOOGLE_API_KEY} or, +for \code{chat_google_gemini()} only, \code{GEMINI_API_KEY}. -It can also pick up on viewer-based credentials on Posit Connect. This in -turn requires the \pkg{connectcreds} package. +Finally these functions will also pick up on viewer-based credentials on +Posit Connect. This requires the \pkg{connectcreds} package. } } \examples{ diff --git a/man/chat_groq.Rd b/man/chat_groq.Rd index 29c11658..8f2e906f 100644 --- a/man/chat_groq.Rd +++ b/man/chat_groq.Rd @@ -7,7 +7,8 @@ chat_groq( system_prompt = NULL, base_url = "https://api.groq.com/openai/v1", - api_key = groq_key(), + api_key = NULL, + credentials = NULL, model = NULL, params = NULL, api_args = list(), @@ -20,9 +21,11 @@ chat_groq( \item{base_url}{The base URL to the endpoint; the default uses OpenAI.} -\item{api_key}{API key to use for authentication. +\item{api_key}{\ifelse{html}{\href{https://lifecycle.r-lib.org/articles/stages.html#deprecated}{\figure{lifecycle-deprecated.svg}{options: alt='[Deprecated]'}}}{\strong{[Deprecated]}} Use \code{credentials} instead.} -You generally should not supply this directly, but instead set the \code{GROQ_API_KEY} environment variable. +\item{credentials}{A function that returns the API key to use for authentication. + +You generally should not need this argument; instead set the \code{GROQ_API_KEY} environment variable. The best place to set this is in \code{.Renviron}, which you can easily edit by calling \code{usethis::edit_r_environ()}.} diff --git a/man/chat_huggingface.Rd b/man/chat_huggingface.Rd index 0fa982dd..1390612d 100644 --- a/man/chat_huggingface.Rd +++ b/man/chat_huggingface.Rd @@ -7,7 +7,8 @@ chat_huggingface( system_prompt = NULL, params = NULL, - api_key = hf_key(), + api_key = NULL, + credentials = NULL, model = NULL, api_args = list(), echo = NULL, @@ -19,9 +20,13 @@ chat_huggingface( \item{params}{Common model parameters, usually created by \code{\link[=params]{params()}}.} -\item{api_key}{The API key to use for authentication. You generally should -not supply this directly, but instead set the \code{HUGGINGFACE_API_KEY} environment -variable.} +\item{api_key}{\ifelse{html}{\href{https://lifecycle.r-lib.org/articles/stages.html#deprecated}{\figure{lifecycle-deprecated.svg}{options: alt='[Deprecated]'}}}{\strong{[Deprecated]}} Use \code{credentials} instead.} + +\item{credentials}{A function that returns the API key to use for authentication. + +You generally should not need this argument; instead set the \code{HUGGINGFACE_API_KEY} environment variable. +The best place to set this is in \code{.Renviron}, +which you can easily edit by calling \code{usethis::edit_r_environ()}.} \item{model}{The model to use for the chat (defaults to "meta-llama/Llama-3.1-8B-Instruct"). We regularly update the default, so we strongly recommend explicitly specifying a model for anything other than casual use.} diff --git a/man/chat_mistral.Rd b/man/chat_mistral.Rd index e79c937b..ca07c301 100644 --- a/man/chat_mistral.Rd +++ b/man/chat_mistral.Rd @@ -8,7 +8,8 @@ chat_mistral( system_prompt = NULL, params = NULL, - api_key = mistral_key(), + api_key = NULL, + credentials = NULL, model = NULL, api_args = list(), echo = NULL, @@ -22,9 +23,11 @@ models_mistral(api_key = mistral_key()) \item{params}{Common model parameters, usually created by \code{\link[=params]{params()}}.} -\item{api_key}{API key to use for authentication. +\item{api_key}{\ifelse{html}{\href{https://lifecycle.r-lib.org/articles/stages.html#deprecated}{\figure{lifecycle-deprecated.svg}{options: alt='[Deprecated]'}}}{\strong{[Deprecated]}} Use \code{credentials} instead.} -You generally should not supply this directly, but instead set the \code{MISTRAL_API_KEY} environment variable. +\item{credentials}{A function that returns the API key to use for authentication. + +You generally should not need this argument; instead set the \code{MISTRAL_API_KEY} environment variable. The best place to set this is in \code{.Renviron}, which you can easily edit by calling \code{usethis::edit_r_environ()}.} diff --git a/man/chat_ollama.Rd b/man/chat_ollama.Rd index 61c2fbb3..71a8f597 100644 --- a/man/chat_ollama.Rd +++ b/man/chat_ollama.Rd @@ -13,6 +13,7 @@ chat_ollama( api_args = list(), echo = NULL, api_key = NULL, + credentials = NULL, api_headers = character() ) @@ -42,12 +43,15 @@ when running at the console). Note this only affects the \code{chat()} method.} -\item{api_key}{Ollama doesn't require an API key for local usage and in most -cases you do not need to provide an \code{api_key}. +\item{api_key}{\ifelse{html}{\href{https://lifecycle.r-lib.org/articles/stages.html#deprecated}{\figure{lifecycle-deprecated.svg}{options: alt='[Deprecated]'}}}{\strong{[Deprecated]}} Use \code{credentials} instead.} + +\item{credentials}{Ollama doesn't require credentials for local usage and in most +cases you do not need to provide \code{credentials}. However, if you're accessing an Ollama instance hosted behind a reverse proxy or secured endpoint that enforces bearer‐token authentication, you -can set \code{api_key} (or the \code{OLLAMA_API_KEY} environment variable).} +can set the \code{OLLAMA_API_KEY} environment variable or provide a callback +function to \code{credentials}.} \item{api_headers}{Named character vector of arbitrary extra headers appended to every chat API call.} diff --git a/man/chat_openai.Rd b/man/chat_openai.Rd index d7401329..e6f73081 100644 --- a/man/chat_openai.Rd +++ b/man/chat_openai.Rd @@ -8,7 +8,8 @@ chat_openai( system_prompt = NULL, base_url = Sys.getenv("OPENAI_BASE_URL", "https://api.openai.com/v1"), - api_key = openai_key(), + api_key = NULL, + credentials = NULL, model = NULL, params = NULL, api_args = list(), @@ -16,16 +17,22 @@ chat_openai( echo = c("none", "output", "all") ) -models_openai(base_url = "https://api.openai.com/v1", api_key = openai_key()) +models_openai( + base_url = "https://api.openai.com/v1", + api_key = NULL, + credentials = NULL +) } \arguments{ \item{system_prompt}{A system prompt to set the behavior of the assistant.} \item{base_url}{The base URL to the endpoint; the default uses OpenAI.} -\item{api_key}{API key to use for authentication. +\item{api_key}{\ifelse{html}{\href{https://lifecycle.r-lib.org/articles/stages.html#deprecated}{\figure{lifecycle-deprecated.svg}{options: alt='[Deprecated]'}}}{\strong{[Deprecated]}} Use \code{credentials} instead.} + +\item{credentials}{A function that returns the API key to use for authentication. -You generally should not supply this directly, but instead set the \code{OPENAI_API_KEY} environment variable. +You generally should not need this argument; instead set the \code{OPENAI_API_KEY} environment variable. The best place to set this is in \code{.Renviron}, which you can easily edit by calling \code{usethis::edit_r_environ()}.} diff --git a/man/chat_openai_responses.Rd b/man/chat_openai_responses.Rd index 3da36fed..3548e272 100644 --- a/man/chat_openai_responses.Rd +++ b/man/chat_openai_responses.Rd @@ -7,7 +7,7 @@ chat_openai_responses( system_prompt = NULL, base_url = Sys.getenv("OPENAI_BASE_URL", "https://api.openai.com/v1"), - api_key = openai_key(), + credentials = NULL, model = NULL, params = NULL, api_args = list(), @@ -20,9 +20,9 @@ chat_openai_responses( \item{base_url}{The base URL to the endpoint; the default uses OpenAI.} -\item{api_key}{API key to use for authentication. +\item{credentials}{A function that returns the API key to use for authentication. -You generally should not supply this directly, but instead set the \code{OPENAI_API_KEY} environment variable. +You generally should not need this argument; instead set the \code{OPENAI_API_KEY} environment variable. The best place to set this is in \code{.Renviron}, which you can easily edit by calling \code{usethis::edit_r_environ()}.} diff --git a/man/chat_openrouter.Rd b/man/chat_openrouter.Rd index ba540ef2..7553af08 100644 --- a/man/chat_openrouter.Rd +++ b/man/chat_openrouter.Rd @@ -6,7 +6,8 @@ \usage{ chat_openrouter( system_prompt = NULL, - api_key = openrouter_key(), + api_key = NULL, + credentials = NULL, model = NULL, params = NULL, api_args = list(), @@ -17,9 +18,11 @@ chat_openrouter( \arguments{ \item{system_prompt}{A system prompt to set the behavior of the assistant.} -\item{api_key}{API key to use for authentication. +\item{api_key}{\ifelse{html}{\href{https://lifecycle.r-lib.org/articles/stages.html#deprecated}{\figure{lifecycle-deprecated.svg}{options: alt='[Deprecated]'}}}{\strong{[Deprecated]}} Use \code{credentials} instead.} -You generally should not supply this directly, but instead set the \code{OPENROUTER_API_KEY} environment variable. +\item{credentials}{A function that returns the API key to use for authentication. + +You generally should not need this argument; instead set the \code{OPENROUTER_API_KEY} environment variable. The best place to set this is in \code{.Renviron}, which you can easily edit by calling \code{usethis::edit_r_environ()}.} diff --git a/man/chat_perplexity.Rd b/man/chat_perplexity.Rd index 69fd236b..c7268031 100644 --- a/man/chat_perplexity.Rd +++ b/man/chat_perplexity.Rd @@ -7,7 +7,8 @@ chat_perplexity( system_prompt = NULL, base_url = "https://api.perplexity.ai/", - api_key = perplexity_key(), + api_key = NULL, + credentials = NULL, model = NULL, params = NULL, api_args = list(), @@ -20,9 +21,11 @@ chat_perplexity( \item{base_url}{The base URL to the endpoint; the default uses OpenAI.} -\item{api_key}{API key to use for authentication. +\item{api_key}{\ifelse{html}{\href{https://lifecycle.r-lib.org/articles/stages.html#deprecated}{\figure{lifecycle-deprecated.svg}{options: alt='[Deprecated]'}}}{\strong{[Deprecated]}} Use \code{credentials} instead.} -You generally should not supply this directly, but instead set the \code{PERPLEXITY_API_KEY} environment variable. +\item{credentials}{A function that returns the API key to use for authentication. + +You generally should not need this argument; instead set the \code{PERPLEXITY_API_KEY} environment variable. The best place to set this is in \code{.Renviron}, which you can easily edit by calling \code{usethis::edit_r_environ()}.} diff --git a/man/chat_portkey.Rd b/man/chat_portkey.Rd index f5c9ddcb..e8d5e0fd 100644 --- a/man/chat_portkey.Rd +++ b/man/chat_portkey.Rd @@ -8,7 +8,8 @@ chat_portkey( system_prompt = NULL, base_url = "https://api.portkey.ai/v1", - api_key = portkey_key(), + api_key = NULL, + credentials = NULL, virtual_key = portkey_virtual_key(), model = NULL, params = NULL, @@ -28,9 +29,11 @@ models_portkey( \item{base_url}{The base URL to the endpoint; the default uses OpenAI.} -\item{api_key}{API key to use for authentication. +\item{api_key}{\ifelse{html}{\href{https://lifecycle.r-lib.org/articles/stages.html#deprecated}{\figure{lifecycle-deprecated.svg}{options: alt='[Deprecated]'}}}{\strong{[Deprecated]}} Use \code{credentials} instead.} + +\item{credentials}{A function that returns the API key to use for authentication. -You generally should not supply this directly, but instead set the \code{PORTKEY_API_KEY} environment variable. +You generally should not need this argument; instead set the \code{PORTKEY_API_KEY} environment variable. The best place to set this is in \code{.Renviron}, which you can easily edit by calling \code{usethis::edit_r_environ()}.} diff --git a/man/chat_vllm.Rd b/man/chat_vllm.Rd index e8fe5b7d..41cf250e 100644 --- a/man/chat_vllm.Rd +++ b/man/chat_vllm.Rd @@ -11,12 +11,13 @@ chat_vllm( model, params = NULL, api_args = list(), - api_key = vllm_key(), + api_key = NULL, + credentials = NULL, echo = NULL, api_headers = character() ) -models_vllm(base_url, api_key = vllm_key()) +models_vllm(base_url, api_key = NULL, credentials = NULL) } \arguments{ \item{base_url}{The base URL to the endpoint; the default uses OpenAI.} @@ -32,9 +33,11 @@ Use \code{models_vllm()} to see all options.} of every chat API call. Combined with the body object generated by ellmer with \code{\link[=modifyList]{modifyList()}}.} -\item{api_key}{API key to use for authentication. +\item{api_key}{\ifelse{html}{\href{https://lifecycle.r-lib.org/articles/stages.html#deprecated}{\figure{lifecycle-deprecated.svg}{options: alt='[Deprecated]'}}}{\strong{[Deprecated]}} Use \code{credentials} instead.} -You generally should not supply this directly, but instead set the \code{VLLM_API_KEY} environment variable. +\item{credentials}{A function that returns the API key to use for authentication. + +You generally should not need this argument; instead set the \code{VLLM_API_KEY} environment variable. The best place to set this is in \code{.Renviron}, which you can easily edit by calling \code{usethis::edit_r_environ()}.} diff --git a/man/google_upload.Rd b/man/google_upload.Rd index b4526c89..a8c7de39 100644 --- a/man/google_upload.Rd +++ b/man/google_upload.Rd @@ -8,6 +8,7 @@ google_upload( path, base_url = "https://generativelanguage.googleapis.com/", api_key = NULL, + credentials = NULL, mime_type = NULL ) } @@ -16,12 +17,10 @@ google_upload( \item{base_url}{The base URL to the endpoint; the default uses OpenAI.} -\item{api_key}{API key to use for authentication. +\item{api_key}{\ifelse{html}{\href{https://lifecycle.r-lib.org/articles/stages.html#deprecated}{\figure{lifecycle-deprecated.svg}{options: alt='[Deprecated]'}}}{\strong{[Deprecated]}} Use \code{credentials} instead.} -You generally should not supply this directly, but instead set the \code{GOOGLE_API_KEY} environment variable. -The best place to set this is in \code{.Renviron}, -which you can easily edit by calling \code{usethis::edit_r_environ()}. -For Gemini, you can alternatively set \code{GEMINI_API_KEY}.} +\item{credentials}{A function that returns a list of authentication headers +or \code{NULL}, the default, to use ambient credentials. See above for details.} \item{mime_type}{Optionally, specify the mime type of the file. If not specified, will be guesses from the file extension.} diff --git a/tests/testthat/_snaps/utils-auth.md b/tests/testthat/_snaps/utils-auth.md new file mode 100644 index 00000000..fa4a72f5 --- /dev/null +++ b/tests/testthat/_snaps/utils-auth.md @@ -0,0 +1,35 @@ +# api_key is deprecated + + Code + chat <- chat_openai_test(api_key = "abc") + Condition + Warning: + The `api_key` argument of `chat_openai()` is deprecated as of ellmer 0.4.0. + i Please use the `credentials` argument instead. + +# errors if both credentials and api_key are provided + + Code + chat_openai_test(credentials = "abc", api_key = "def") + Condition + Error in `chat_openai()`: + ! Must supply one of `api_key` or `credentials`. + +# verifies all properties of credentials + + Code + chat_openai_test(credentials = 1) + Condition + Error in `chat_openai()`: + ! `credentials` must be a function or `NULL`, not the number 1. + Code + chat_openai_test(credentials = function(a, b) a + b) + Condition + Error in `chat_openai()`: + ! `credentials` must not have arguments. + Code + chat_openai_test(credentials = function() 1) + Condition + Error in `chat_openai()`: + ! `credentials()` must be a string or a named list, not the number 1. + diff --git a/tests/testthat/test-batch-chat.R b/tests/testthat/test-batch-chat.R index 4757265f..5ef2aa03 100644 --- a/tests/testthat/test-batch-chat.R +++ b/tests/testthat/test-batch-chat.R @@ -45,7 +45,7 @@ test_that("errors if chat/provider/prompts don't match previous run", { test_that("can override hash check", { chat <- chat_openai_test(system_prompt = "Be cool") - prompts <- list() + prompts <- list("x") path <- test_path("batch/state-capitals.json") expect_snapshot(. <- batch_chat(chat, prompts, path, ignore_hash = TRUE)) }) @@ -130,8 +130,7 @@ test_that("informative error for bad inputs", { chat_ollama$.__enclos_env__$private$provider <- ProviderOllama( "ollama", "model", - "base_url", - api_key = "api_key" + "base_url" ) expect_snapshot(error = TRUE, { diff --git a/tests/testthat/test-parallel-chat.R b/tests/testthat/test-parallel-chat.R index 69cb03e8..a5e2ad1c 100644 --- a/tests/testthat/test-parallel-chat.R +++ b/tests/testthat/test-parallel-chat.R @@ -136,7 +136,7 @@ test_that("can get tokens & cost", { test_that("handles errors and NULLs in parallel functions", { chat <- chat_openai( - api_key = "test-key", + credentials = \() "test-key", base_url = "http://localhost:1234", model = "mock" ) diff --git a/tests/testthat/test-provider-azure.R b/tests/testthat/test-provider-azure.R index c9074583..a80db610 100644 --- a/tests/testthat/test-provider-azure.R +++ b/tests/testthat/test-provider-azure.R @@ -64,8 +64,7 @@ test_that("Azure request headers are generated correctly", { base_url = base_url, model = deployment_id, api_version = "2024-06-01", - api_key = "key", - credentials = default_azure_credentials("key") + credentials = \() "key" ) req <- chat_request(p, FALSE, list(turn)) expect_snapshot(str(req_get_headers(req, "reveal"))) diff --git a/tests/testthat/test-provider-ollama.R b/tests/testthat/test-provider-ollama.R index 2f8f6ad0..0a46cfaa 100644 --- a/tests/testthat/test-provider-ollama.R +++ b/tests/testthat/test-provider-ollama.R @@ -55,7 +55,7 @@ test_that("supports tool calling", { # Custom ----------------------------------------------------------------- test_that("as_json specialised for Ollama", { - stub <- ProviderOllama(name = "", base_url = "", api_key = "", model = "") + stub <- ProviderOllama(name = "", base_url = "", model = "") expect_snapshot( as_json(stub, type_object(.additional_properties = TRUE)), diff --git a/tests/testthat/test-provider-openai.R b/tests/testthat/test-provider-openai.R index 15adcaa7..68d9b771 100644 --- a/tests/testthat/test-provider-openai.R +++ b/tests/testthat/test-provider-openai.R @@ -97,7 +97,7 @@ test_that("structured data work with and without wrapper", { # Custom ----------------------------------------------------------------- test_that("as_json specialised for OpenAI", { - stub <- ProviderOpenAI(name = "", base_url = "", api_key = "", model = "") + stub <- ProviderOpenAI(name = "", base_url = "", model = "") expect_snapshot( as_json(stub, type_object(.additional_properties = TRUE)), diff --git a/tests/testthat/test-provider.R b/tests/testthat/test-provider.R index a88991d6..fdfd1319 100644 --- a/tests/testthat/test-provider.R +++ b/tests/testthat/test-provider.R @@ -1,5 +1,5 @@ test_that("ContentJson converted to ContentText", { - test_provider <- ProviderOpenAI("test", "model", "base_url", api_key = "key") + test_provider <- ProviderOpenAI("test", "model", "base_url") expect_equal( as_json(test_provider, ContentJson(list(x = 1))), list(type = "text", text = "{\"x\":1}") diff --git a/tests/testthat/test-utils-auth.R b/tests/testthat/test-utils-auth.R new file mode 100644 index 00000000..0869ad0c --- /dev/null +++ b/tests/testthat/test-utils-auth.R @@ -0,0 +1,21 @@ +# tests use `chat_openai()` to verify environments flow through correctly + +test_that("api_key is deprecated", { + expect_snapshot(chat <- chat_openai_test(api_key = "abc")) + expect_equal(chat$get_provider()@credentials(), "Bearer abc") +}) + +test_that("errors if both credentials and api_key are provided", { + expect_snapshot( + chat_openai_test(credentials = "abc", api_key = "def"), + error = TRUE + ) +}) + +test_that("verifies all properties of credentials", { + expect_snapshot(error = TRUE, { + chat_openai_test(credentials = 1) + chat_openai_test(credentials = \(a, b) a + b) + chat_openai_test(credentials = \() 1) + }) +})