From 7bda699d598c1c87ade4f39fb471cad2380d1059 Mon Sep 17 00:00:00 2001 From: Daren Eiri Date: Thu, 9 Apr 2026 16:01:23 -0700 Subject: [PATCH 1/7] initial push for adding new function: chat_azure_anthropic --- DESCRIPTION | 3 +- NAMESPACE | 1 + NEWS.md | 4 + R/provider-azure-anthropic.R | 202 ++++++++++++++++++ man/chat_anthropic.Rd | 1 + man/chat_aws_bedrock.Rd | 1 + man/chat_azure_anthropic.Rd | 119 +++++++++++ man/chat_azure_openai.Rd | 1 + man/chat_cloudflare.Rd | 1 + man/chat_databricks.Rd | 1 + man/chat_deepseek.Rd | 1 + man/chat_github.Rd | 1 + man/chat_google_gemini.Rd | 1 + man/chat_groq.Rd | 1 + man/chat_huggingface.Rd | 1 + man/chat_mistral.Rd | 1 + man/chat_ollama.Rd | 1 + man/chat_openai.Rd | 1 + man/chat_openai_compatible.Rd | 1 + man/chat_openrouter.Rd | 1 + man/chat_perplexity.Rd | 1 + man/chat_portkey.Rd | 1 + .../test-provider-azure-anthropic-24.R | 19 ++ .../test-provider-azure-anthropic-47.R | 17 ++ .../_snaps/provider-azure-anthropic.md | 23 ++ .../testthat/test-provider-azure-anthropic.R | 88 ++++++++ 26 files changed, 492 insertions(+), 1 deletion(-) create mode 100644 R/provider-azure-anthropic.R create mode 100644 man/chat_azure_anthropic.Rd create mode 100644 tests/testthat/_problems/test-provider-azure-anthropic-24.R create mode 100644 tests/testthat/_problems/test-provider-azure-anthropic-47.R create mode 100644 tests/testthat/_snaps/provider-azure-anthropic.md create mode 100644 tests/testthat/test-provider-azure-anthropic.R diff --git a/DESCRIPTION b/DESCRIPTION index 092a284b3..aa50961c6 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -92,11 +92,12 @@ Collate: 'params.R' 'provider-any.R' 'provider-aws.R' + 'provider-claude.R' 'provider-openai-compatible.R' 'provider-azure.R' + 'provider-azure-anthropic.R' 'provider-claude-files.R' 'provider-claude-tools.R' - 'provider-claude.R' 'provider-google.R' 'provider-cloudflare.R' 'provider-databricks.R' diff --git a/NAMESPACE b/NAMESPACE index e748db401..61198380e 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -37,6 +37,7 @@ export(batch_chat_text) export(chat) export(chat_anthropic) export(chat_aws_bedrock) +export(chat_azure_anthropic) export(chat_azure_openai) export(chat_claude) export(chat_cloudflare) diff --git a/NEWS.md b/NEWS.md index 40420b0d9..fab4904ac 100644 --- a/NEWS.md +++ b/NEWS.md @@ -1,5 +1,9 @@ # ellmer (development version) +* New `chat_azure_anthropic()` enables chatting with Anthropic Claude models + hosted on Azure AI Foundry (`*.services.ai.azure.com/anthropic` endpoints), + with the same Azure authentication options as `chat_azure_openai()` (#585). + * Fixed three bugs that caused errors when streaming web search results: Claude's `citations_delta` events were mishandled, `server_tool_use` input wasn't parsed from JSON during streaming, and OpenAI's `web_search_call` failed for non-search action types like `open_page` (#941). * `chat_aws_bedrock()` gains a `cache` parameter for prompt caching. The default, `"auto"`, enables caching for models known to support it (Anthropic Claude and Amazon Nova) and disables it otherwise (#954). * Built-in tools (e.g., `openai_tool_web_search()`, `claude_tool_web_search()`) now include `description` and `annotations` properties, making their metadata consistent with user-defined tools created by `tool()` (#942). diff --git a/R/provider-azure-anthropic.R b/R/provider-azure-anthropic.R new file mode 100644 index 000000000..67ba1c0b6 --- /dev/null +++ b/R/provider-azure-anthropic.R @@ -0,0 +1,202 @@ +#' @include provider-azure.R +#' @include provider-claude.R +NULL + +# https://learn.microsoft.com/en-us/azure/ai-services/models/get-started + +#' Chat with an Anthropic Claude model hosted on Azure AI Foundry +#' +#' @description +#' [Azure AI Foundry](https://azure.microsoft.com/en-us/products/ai-foundry) +#' hosts Anthropic Claude models accessible via the +#' `*.services.ai.azure.com/anthropic` endpoint, using the Anthropic Messages +#' API format. +#' +#' Unlike [chat_azure_openai()], which targets `*.openai.azure.com` endpoints +#' and uses the OpenAI chat completions format, this function targets Azure AI +#' Foundry's Anthropic-compatible endpoint. +#' +#' ## Authentication +#' +#' `chat_azure_anthropic()` supports API keys via the `AZURE_ANTHROPIC_API_KEY` +#' environment variable and the `credentials` parameter. It also supports: +#' +#' - Azure service principals (when the `AZURE_TENANT_ID`, `AZURE_CLIENT_ID`, +#' and `AZURE_CLIENT_SECRET` environment variables are set). +#' - Interactive Entra ID authentication, like the Azure CLI. +#' - Viewer-based credentials on Posit Connect. Requires the \pkg{connectcreds} +#' package. +#' +#' @param endpoint Azure AI Foundry endpoint URL with protocol and hostname, +#' i.e. `https://{your-project}.services.ai.azure.com/anthropic`. Defaults +#' to the value of the `AZURE_ANTHROPIC_ENDPOINT` environment variable. +#' @param model The model name to use (e.g., `"claude-opus-4-5"`). +#' Defaults to `"claude-sonnet-4-5-20250929"`. +#' @param api_version The API version to use. +#' @param credentials `r api_key_param("AZURE_ANTHROPIC_API_KEY")` +#' @inheritParams chat_anthropic +#' @inherit chat_openai return +#' @family chatbots +#' @export +#' @examples +#' \dontrun{ +#' chat <- chat_azure_anthropic( +#' endpoint = "https://your-project.services.ai.azure.com/anthropic", +#' model = "claude-opus-4-5" +#' ) +#' chat$chat("Tell me three jokes about statisticians") +#' } +chat_azure_anthropic <- function( + endpoint = azure_anthropic_endpoint(), + model = NULL, + params = NULL, + api_version = NULL, + system_prompt = NULL, + credentials = NULL, + cache = c("5m", "1h", "none"), + beta_headers = character(), + api_args = list(), + api_headers = character(), + echo = NULL +) { + check_string(endpoint) + params <- params %||% params() + api_version <- set_default(api_version, "2024-10-22") + model <- set_default(model, "claude-sonnet-4-5-20250929") + cache <- arg_match(cache) + echo <- check_echo(echo) + + credentials <- as_credentials( + "chat_azure_anthropic", + default_azure_anthropic_credentials(), + credentials = credentials + ) + + provider <- ProviderAzureAnthropic( + name = "Azure/Anthropic", + base_url = paste0(endpoint, "/v1"), + model = model, + params = params, + api_version = api_version, + credentials = credentials, + extra_args = api_args, + extra_headers = api_headers, + beta_headers = beta_headers, + cache = cache + ) + + Chat$new(provider = provider, system_prompt = system_prompt, echo = echo) +} + +ProviderAzureAnthropic <- new_class( + "ProviderAzureAnthropic", + parent = ProviderAnthropic, + properties = list( + api_version = prop_string() + ) +) + +azure_anthropic_endpoint <- function() { + key_get("AZURE_ANTHROPIC_ENDPOINT") +} + +# https://learn.microsoft.com/en-us/azure/ai-services/openai/reference +method(base_request, ProviderAzureAnthropic) <- function(provider) { + req <- request(provider@base_url) + req <- ellmer_req_robustify(req, is_transient = function(resp) { + resp_status(resp) %in% c(429, 503, 529) + }) + req <- ellmer_req_user_agent(req) + # Azure AI Foundry uses api-key header (not x-api-key like standard Anthropic) + req <- ellmer_req_credentials(req, provider@credentials(), "api-key") + req <- req_url_query(req, `api-version` = provider@api_version) + + if (length(provider@beta_headers) > 0) { + req <- req_headers(req, `anthropic-beta` = provider@beta_headers) + } + + req <- req_error(req, body = function(resp) { + if (resp_content_type(resp) == "application/json") { + json <- resp_body_json(resp) + paste0(json$error$message, " [", json$error$type, "]") + } + }) + + req +} + +default_azure_anthropic_credentials <- function() { + azure_scope <- "https://cognitiveservices.azure.com/.default" + + # Detect viewer-based credentials from Posit Connect. + if (has_connect_viewer_token(scope = azure_scope)) { + return(function() { + token <- connectcreds::connect_viewer_token(scope = azure_scope) + list(Authorization = paste("Bearer", token$access_token)) + }) + } + + # Detect Azure service principals. + tenant_id <- Sys.getenv("AZURE_TENANT_ID") + client_id <- Sys.getenv("AZURE_CLIENT_ID") + client_secret <- Sys.getenv("AZURE_CLIENT_SECRET") + if (nchar(tenant_id) && nchar(client_id) && nchar(client_secret)) { + client <- oauth_client( + client_id, + token_url = paste0( + "https://login.microsoftonline.com/", + tenant_id, + "/oauth2/v2.0/token" + ), + secret = client_secret, + auth = "body", + name = "ellmer-azure-anthropic-sp" + ) + return(function() { + token <- oauth_token_cached( + client, + oauth_flow_client_credentials, + flow_params = list(scope = azure_scope), + reauth = is_testing() + ) + list(Authorization = paste("Bearer", token$access_token)) + }) + } + + # If we have an API key, include it in the credentials. + api_key <- Sys.getenv("AZURE_ANTHROPIC_API_KEY") + if (nchar(api_key)) { + return(\() api_key) + } + + # Masquerade as the Azure CLI. + client_id <- "04b07795-8ddb-461a-bbee-02f9e1bf7b46" + if (is_interactive() && !is_hosted_session()) { + client <- oauth_client( + client_id, + token_url = "https://login.microsoftonline.com/common/oauth2/v2.0/token", + secret = "", + auth = "body", + name = paste0("ellmer-", client_id) + ) + return(function() { + token <- oauth_token_cached( + client, + oauth_flow_auth_code, + flow_params = list( + auth_url = "https://login.microsoftonline.com/common/oauth2/v2.0/authorize", + scope = paste(azure_scope, "offline_access"), + redirect_uri = "http://localhost:8400", + auth_params = list(prompt = "select_account") + ) + ) + list(Authorization = paste("Bearer", token$access_token)) + }) + } + + if (is_testing()) { + testthat::skip("no Azure credentials available") + } + + cli::cli_abort("No Azure credentials are available.") +} diff --git a/man/chat_anthropic.Rd b/man/chat_anthropic.Rd index 66cc5738a..89aa4be58 100644 --- a/man/chat_anthropic.Rd +++ b/man/chat_anthropic.Rd @@ -148,6 +148,7 @@ chat$chat("Tell me three jokes about statisticians") \seealso{ Other chatbots: \code{\link{chat_aws_bedrock}()}, +\code{\link{chat_azure_anthropic}()}, \code{\link{chat_azure_openai}()}, \code{\link{chat_cloudflare}()}, \code{\link{chat_databricks}()}, diff --git a/man/chat_aws_bedrock.Rd b/man/chat_aws_bedrock.Rd index 29e1840a5..ffa7a0702 100644 --- a/man/chat_aws_bedrock.Rd +++ b/man/chat_aws_bedrock.Rd @@ -118,6 +118,7 @@ chat$chat("Tell me three jokes about statisticians") \seealso{ Other chatbots: \code{\link{chat_anthropic}()}, +\code{\link{chat_azure_anthropic}()}, \code{\link{chat_azure_openai}()}, \code{\link{chat_cloudflare}()}, \code{\link{chat_databricks}()}, diff --git a/man/chat_azure_anthropic.Rd b/man/chat_azure_anthropic.Rd new file mode 100644 index 000000000..92eded9b6 --- /dev/null +++ b/man/chat_azure_anthropic.Rd @@ -0,0 +1,119 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/provider-azure-anthropic.R +\name{chat_azure_anthropic} +\alias{chat_azure_anthropic} +\title{Chat with an Anthropic Claude model hosted on Azure AI Foundry} +\usage{ +chat_azure_anthropic( + endpoint = azure_anthropic_endpoint(), + model = NULL, + params = NULL, + api_version = NULL, + system_prompt = NULL, + credentials = NULL, + cache = c("5m", "1h", "none"), + beta_headers = character(), + api_args = list(), + api_headers = character(), + echo = NULL +) +} +\arguments{ +\item{endpoint}{Azure AI Foundry endpoint URL with protocol and hostname, +i.e. \verb{https://\{your-project\}.services.ai.azure.com/anthropic}. Defaults +to the value of the \code{AZURE_ANTHROPIC_ENDPOINT} environment variable.} + +\item{model}{The model name to use (e.g., \code{"claude-opus-4-5"}). +Defaults to \code{"claude-sonnet-4-5-20250929"}.} + +\item{params}{Common model parameters, usually created by \code{\link[=params]{params()}}.} + +\item{api_version}{The API version to use.} + +\item{system_prompt}{A system prompt to set the behavior of the assistant.} + +\item{credentials}{Override the default credentials. You generally should not need this argument; instead set the \code{AZURE_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()}. + +If you do need additional control, this argument takes a zero-argument function that returns either a string (the API key), or a named list (added as additional headers to every request).} + +\item{cache}{How long to cache inputs? Defaults to "5m" (five minutes). +Set to "none" to disable caching or "1h" to cache for one hour. + +See details below.} + +\item{beta_headers}{Optionally, a character vector of beta headers to opt-in +claude features that are still in beta.} + +\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()}}.} + +\item{api_headers}{Named character vector of arbitrary extra headers appended +to every chat API call.} + +\item{echo}{One of the following options: +\itemize{ +\item \code{none}: don't emit any output (default when running in a function). +\item \code{output}: echo text and tool-calling output as it streams in (default +when running at the console). +\item \code{all}: echo all input and output. +} + +Note this only affects the \code{chat()} method.} +} +\value{ +A \link{Chat} object. +} +\description{ +\href{https://azure.microsoft.com/en-us/products/ai-foundry}{Azure AI Foundry} +hosts Anthropic Claude models accessible via the +\verb{*.services.ai.azure.com/anthropic} endpoint, using the Anthropic Messages +API format. + +Unlike \code{\link[=chat_azure_openai]{chat_azure_openai()}}, which targets \verb{*.openai.azure.com} endpoints +and uses the OpenAI chat completions format, this function targets Azure AI +Foundry's Anthropic-compatible endpoint. +\subsection{Authentication}{ + +\code{chat_azure_anthropic()} supports API keys via the \code{AZURE_ANTHROPIC_API_KEY} +environment variable and the \code{credentials} parameter. It also supports: +\itemize{ +\item Azure service principals (when the \code{AZURE_TENANT_ID}, \code{AZURE_CLIENT_ID}, +and \code{AZURE_CLIENT_SECRET} environment variables are set). +\item Interactive Entra ID authentication, like the Azure CLI. +\item Viewer-based credentials on Posit Connect. Requires the \pkg{connectcreds} +package. +} +} +} +\examples{ +\dontrun{ +chat <- chat_azure_anthropic( + endpoint = "https://your-project.services.ai.azure.com/anthropic", + model = "claude-opus-4-5" +) +chat$chat("Tell me three jokes about statisticians") +} +} +\seealso{ +Other chatbots: +\code{\link{chat_anthropic}()}, +\code{\link{chat_aws_bedrock}()}, +\code{\link{chat_azure_openai}()}, +\code{\link{chat_cloudflare}()}, +\code{\link{chat_databricks}()}, +\code{\link{chat_deepseek}()}, +\code{\link{chat_github}()}, +\code{\link{chat_google_gemini}()}, +\code{\link{chat_groq}()}, +\code{\link{chat_huggingface}()}, +\code{\link{chat_mistral}()}, +\code{\link{chat_ollama}()}, +\code{\link{chat_openai}()}, +\code{\link{chat_openai_compatible}()}, +\code{\link{chat_openrouter}()}, +\code{\link{chat_perplexity}()}, +\code{\link{chat_portkey}()} +} +\concept{chatbots} diff --git a/man/chat_azure_openai.Rd b/man/chat_azure_openai.Rd index 64fb46afb..48c8ed903 100644 --- a/man/chat_azure_openai.Rd +++ b/man/chat_azure_openai.Rd @@ -89,6 +89,7 @@ chat$chat("Tell me three jokes about statisticians") Other chatbots: \code{\link{chat_anthropic}()}, \code{\link{chat_aws_bedrock}()}, +\code{\link{chat_azure_anthropic}()}, \code{\link{chat_cloudflare}()}, \code{\link{chat_databricks}()}, \code{\link{chat_deepseek}()}, diff --git a/man/chat_cloudflare.Rd b/man/chat_cloudflare.Rd index 139416eb9..0f80ca818 100644 --- a/man/chat_cloudflare.Rd +++ b/man/chat_cloudflare.Rd @@ -78,6 +78,7 @@ chat$chat("Tell me three jokes about statisticians") Other chatbots: \code{\link{chat_anthropic}()}, \code{\link{chat_aws_bedrock}()}, +\code{\link{chat_azure_anthropic}()}, \code{\link{chat_azure_openai}()}, \code{\link{chat_databricks}()}, \code{\link{chat_deepseek}()}, diff --git a/man/chat_databricks.Rd b/man/chat_databricks.Rd index 9bb467f2a..84996ffc4 100644 --- a/man/chat_databricks.Rd +++ b/man/chat_databricks.Rd @@ -89,6 +89,7 @@ chat$chat("Tell me three jokes about statisticians") Other chatbots: \code{\link{chat_anthropic}()}, \code{\link{chat_aws_bedrock}()}, +\code{\link{chat_azure_anthropic}()}, \code{\link{chat_azure_openai}()}, \code{\link{chat_cloudflare}()}, \code{\link{chat_deepseek}()}, diff --git a/man/chat_deepseek.Rd b/man/chat_deepseek.Rd index a2965b7a3..77a7a5d72 100644 --- a/man/chat_deepseek.Rd +++ b/man/chat_deepseek.Rd @@ -74,6 +74,7 @@ chat$chat("Tell me three jokes about statisticians") Other chatbots: \code{\link{chat_anthropic}()}, \code{\link{chat_aws_bedrock}()}, +\code{\link{chat_azure_anthropic}()}, \code{\link{chat_azure_openai}()}, \code{\link{chat_cloudflare}()}, \code{\link{chat_databricks}()}, diff --git a/man/chat_github.Rd b/man/chat_github.Rd index 255bcf251..9f6645003 100644 --- a/man/chat_github.Rd +++ b/man/chat_github.Rd @@ -83,6 +83,7 @@ chat$chat("Tell me three jokes about statisticians") Other chatbots: \code{\link{chat_anthropic}()}, \code{\link{chat_aws_bedrock}()}, +\code{\link{chat_azure_anthropic}()}, \code{\link{chat_azure_openai}()}, \code{\link{chat_cloudflare}()}, \code{\link{chat_databricks}()}, diff --git a/man/chat_google_gemini.Rd b/man/chat_google_gemini.Rd index efbfcb8f2..6ecd38c04 100644 --- a/man/chat_google_gemini.Rd +++ b/man/chat_google_gemini.Rd @@ -113,6 +113,7 @@ chat$chat("Tell me three jokes about statisticians") Other chatbots: \code{\link{chat_anthropic}()}, \code{\link{chat_aws_bedrock}()}, +\code{\link{chat_azure_anthropic}()}, \code{\link{chat_azure_openai}()}, \code{\link{chat_cloudflare}()}, \code{\link{chat_databricks}()}, diff --git a/man/chat_groq.Rd b/man/chat_groq.Rd index 72a6ba94e..d5c83d88f 100644 --- a/man/chat_groq.Rd +++ b/man/chat_groq.Rd @@ -69,6 +69,7 @@ chat$chat("Tell me three jokes about statisticians") Other chatbots: \code{\link{chat_anthropic}()}, \code{\link{chat_aws_bedrock}()}, +\code{\link{chat_azure_anthropic}()}, \code{\link{chat_azure_openai}()}, \code{\link{chat_cloudflare}()}, \code{\link{chat_databricks}()}, diff --git a/man/chat_huggingface.Rd b/man/chat_huggingface.Rd index 565cb0072..0314bbe21 100644 --- a/man/chat_huggingface.Rd +++ b/man/chat_huggingface.Rd @@ -77,6 +77,7 @@ chat$chat("Tell me three jokes about statisticians") Other chatbots: \code{\link{chat_anthropic}()}, \code{\link{chat_aws_bedrock}()}, +\code{\link{chat_azure_anthropic}()}, \code{\link{chat_azure_openai}()}, \code{\link{chat_cloudflare}()}, \code{\link{chat_databricks}()}, diff --git a/man/chat_mistral.Rd b/man/chat_mistral.Rd index f4238746c..e78306860 100644 --- a/man/chat_mistral.Rd +++ b/man/chat_mistral.Rd @@ -74,6 +74,7 @@ chat$chat("Tell me three jokes about statisticians") Other chatbots: \code{\link{chat_anthropic}()}, \code{\link{chat_aws_bedrock}()}, +\code{\link{chat_azure_anthropic}()}, \code{\link{chat_azure_openai}()}, \code{\link{chat_cloudflare}()}, \code{\link{chat_databricks}()}, diff --git a/man/chat_ollama.Rd b/man/chat_ollama.Rd index 622da3362..cb472ccaf 100644 --- a/man/chat_ollama.Rd +++ b/man/chat_ollama.Rd @@ -90,6 +90,7 @@ chat$chat("Tell me three jokes about statisticians") Other chatbots: \code{\link{chat_anthropic}()}, \code{\link{chat_aws_bedrock}()}, +\code{\link{chat_azure_anthropic}()}, \code{\link{chat_azure_openai}()}, \code{\link{chat_cloudflare}()}, \code{\link{chat_databricks}()}, diff --git a/man/chat_openai.Rd b/man/chat_openai.Rd index 7f660b04e..60a6de656 100644 --- a/man/chat_openai.Rd +++ b/man/chat_openai.Rd @@ -97,6 +97,7 @@ chat$chat("Tell me three funny jokes about statisticians") Other chatbots: \code{\link{chat_anthropic}()}, \code{\link{chat_aws_bedrock}()}, +\code{\link{chat_azure_anthropic}()}, \code{\link{chat_azure_openai}()}, \code{\link{chat_cloudflare}()}, \code{\link{chat_databricks}()}, diff --git a/man/chat_openai_compatible.Rd b/man/chat_openai_compatible.Rd index 794d29b43..94637b1fd 100644 --- a/man/chat_openai_compatible.Rd +++ b/man/chat_openai_compatible.Rd @@ -81,6 +81,7 @@ chat$chat("What is the difference between a tibble and a data frame?") Other chatbots: \code{\link{chat_anthropic}()}, \code{\link{chat_aws_bedrock}()}, +\code{\link{chat_azure_anthropic}()}, \code{\link{chat_azure_openai}()}, \code{\link{chat_cloudflare}()}, \code{\link{chat_databricks}()}, diff --git a/man/chat_openrouter.Rd b/man/chat_openrouter.Rd index f5ceac9e2..ef3f5ac06 100644 --- a/man/chat_openrouter.Rd +++ b/man/chat_openrouter.Rd @@ -66,6 +66,7 @@ chat$chat("Tell me three jokes about statisticians") Other chatbots: \code{\link{chat_anthropic}()}, \code{\link{chat_aws_bedrock}()}, +\code{\link{chat_azure_anthropic}()}, \code{\link{chat_azure_openai}()}, \code{\link{chat_cloudflare}()}, \code{\link{chat_databricks}()}, diff --git a/man/chat_perplexity.Rd b/man/chat_perplexity.Rd index dc2e93641..8c4b627e6 100644 --- a/man/chat_perplexity.Rd +++ b/man/chat_perplexity.Rd @@ -75,6 +75,7 @@ chat$chat("Tell me three jokes about statisticians") Other chatbots: \code{\link{chat_anthropic}()}, \code{\link{chat_aws_bedrock}()}, +\code{\link{chat_azure_anthropic}()}, \code{\link{chat_azure_openai}()}, \code{\link{chat_cloudflare}()}, \code{\link{chat_databricks}()}, diff --git a/man/chat_portkey.Rd b/man/chat_portkey.Rd index 20d11a401..cb5c500fa 100644 --- a/man/chat_portkey.Rd +++ b/man/chat_portkey.Rd @@ -81,6 +81,7 @@ chat$chat("Tell me three jokes about statisticians") Other chatbots: \code{\link{chat_anthropic}()}, \code{\link{chat_aws_bedrock}()}, +\code{\link{chat_azure_anthropic}()}, \code{\link{chat_azure_openai}()}, \code{\link{chat_cloudflare}()}, \code{\link{chat_databricks}()}, diff --git a/tests/testthat/_problems/test-provider-azure-anthropic-24.R b/tests/testthat/_problems/test-provider-azure-anthropic-24.R new file mode 100644 index 000000000..e971b5a6b --- /dev/null +++ b/tests/testthat/_problems/test-provider-azure-anthropic-24.R @@ -0,0 +1,19 @@ +# Extracted from test-provider-azure-anthropic.R:24 + +# setup ------------------------------------------------------------------------ +library(testthat) +test_env <- simulate_test_env(package = "ellmer", path = "..") +attach(test_env, warn.conflicts = FALSE) + +# test ------------------------------------------------------------------------- +turn <- UserTurn( + contents = list(ContentText("What is 1 + 1?")) + ) +endpoint <- "https://example.services.ai.azure.com/anthropic" +p <- ProviderAzureAnthropic( + name = "Azure/Anthropic", + base_url = paste0(endpoint, "/v1"), + model = "claude-opus-4-5", + api_version = "2024-10-22", + credentials = \() "key" + ) diff --git a/tests/testthat/_problems/test-provider-azure-anthropic-47.R b/tests/testthat/_problems/test-provider-azure-anthropic-47.R new file mode 100644 index 000000000..743f496b9 --- /dev/null +++ b/tests/testthat/_problems/test-provider-azure-anthropic-47.R @@ -0,0 +1,17 @@ +# Extracted from test-provider-azure-anthropic.R:47 + +# setup ------------------------------------------------------------------------ +library(testthat) +test_env <- simulate_test_env(package = "ellmer", path = "..") +attach(test_env, warn.conflicts = FALSE) + +# test ------------------------------------------------------------------------- +endpoint <- "https://example.services.ai.azure.com/anthropic" +p <- ProviderAzureAnthropic( + name = "Azure/Anthropic", + base_url = paste0(endpoint, "/v1"), + model = "claude-opus-4-5", + api_version = "2024-10-22", + credentials = \() "key", + beta_headers = c("feature-a", "feature-b") + ) diff --git a/tests/testthat/_snaps/provider-azure-anthropic.md b/tests/testthat/_snaps/provider-azure-anthropic.md new file mode 100644 index 000000000..ed65b9c1f --- /dev/null +++ b/tests/testthat/_snaps/provider-azure-anthropic.md @@ -0,0 +1,23 @@ +# defaults are reported + + Code + . <- chat_azure_anthropic("https://example.services.ai.azure.com/anthropic") + Message + Using api_version = "2024-10-22". + Using model = "claude-sonnet-4-5-20250929". + +# service principal authentication requests look correct + + Code + str(request_summary(req)) + Output + List of 3 + $ url : chr "https://login.microsoftonline.com/aaaa0a0a-bb1b-cc2c-dd3d-eeeeee4e4e4e/oauth2/v2.0/token" + $ headers:List of 1 + ..$ Accept: chr "application/json" + $ body :List of 4 + ..$ grant_type : 'AsIs' chr "client_credentials" + ..$ scope : 'AsIs' chr "https%3A%2F%2Fcognitiveservices.azure.com%2F.default" + ..$ client_id : 'AsIs' chr "id" + ..$ client_secret: 'AsIs' chr "secret" + diff --git a/tests/testthat/test-provider-azure-anthropic.R b/tests/testthat/test-provider-azure-anthropic.R new file mode 100644 index 000000000..62a40d70c --- /dev/null +++ b/tests/testthat/test-provider-azure-anthropic.R @@ -0,0 +1,88 @@ +# Defaults ---------------------------------------------------------------- + +test_that("defaults are reported", { + withr::local_envvar(AZURE_ANTHROPIC_API_KEY = "key") + expect_snapshot( + . <- chat_azure_anthropic("https://example.services.ai.azure.com/anthropic") + ) +}) + +# Authentication ---------------------------------------------------------- + +test_that("Azure Anthropic request headers are generated correctly", { + turn <- UserTurn( + contents = list(ContentText("What is 1 + 1?")) + ) + endpoint <- "https://example.services.ai.azure.com/anthropic" + + p <- ProviderAzureAnthropic( + name = "Azure/Anthropic", + base_url = paste0(endpoint, "/v1"), + model = "claude-opus-4-5", + params = list(), + extra_args = list(), + extra_headers = character(), + credentials = \() "key", + beta_headers = character(), + cache = "none", + api_version = "2024-10-22" + ) + req <- chat_request(p, FALSE, list(turn)) + headers <- req_get_headers(req, "reveal") + + # Uses api-key header, not x-api-key like standard Anthropic + expect_equal(headers$`api-key`, "key") + expect_null(headers$`x-api-key`) + # No anthropic-version header; Azure uses api-version query param instead + expect_null(headers$`anthropic-version`) + # api-version appears as a query parameter + expect_match(req$url, "api-version=2024-10-22") +}) + +test_that("beta headers are forwarded correctly", { + endpoint <- "https://example.services.ai.azure.com/anthropic" + + p <- ProviderAzureAnthropic( + name = "Azure/Anthropic", + base_url = paste0(endpoint, "/v1"), + model = "claude-opus-4-5", + params = list(), + extra_args = list(), + extra_headers = character(), + credentials = \() "key", + beta_headers = c("feature-a", "feature-b"), + cache = "none", + api_version = "2024-10-22" + ) + req <- base_request(p) + headers <- req_get_headers(req) + expect_equal(headers$`anthropic-beta`, "feature-a,feature-b") +}) + +test_that("service principal authentication requests look correct", { + withr::local_envvar( + AZURE_TENANT_ID = "aaaa0a0a-bb1b-cc2c-dd3d-eeeeee4e4e4e", + AZURE_CLIENT_ID = "id", + AZURE_CLIENT_SECRET = "secret" + ) + local_mocked_responses(function(req) { + expect_snapshot(str(request_summary(req))) + response_json(body = list(access_token = "token")) + }) + source <- default_azure_anthropic_credentials() + expect_equal(source(), list(Authorization = "Bearer token")) +}) + +test_that("tokens can be requested from a Connect server", { + skip_if_not_installed("connectcreds") + + connectcreds::local_mocked_connect_responses(token = "token") + credentials <- default_azure_anthropic_credentials() + expect_equal(credentials(), list(Authorization = "Bearer token")) +}) + +test_that("API key is read from AZURE_ANTHROPIC_API_KEY", { + withr::local_envvar(AZURE_ANTHROPIC_API_KEY = "my-key") + credentials <- default_azure_anthropic_credentials() + expect_equal(credentials(), "my-key") +}) From cb276fd2b3f5c67b024a3cedbec210571115305d Mon Sep 17 00:00:00 2001 From: Daren Eiri Date: Thu, 9 Apr 2026 21:23:34 -0700 Subject: [PATCH 2/7] resolve authentication issues --- R/provider-azure-anthropic.R | 34 +------------------ man/chat_azure_anthropic.Rd | 3 -- .../test-provider-azure-anthropic-24.R | 16 ++++----- .../test-provider-azure-anthropic-47.R | 14 ++++---- .../_snaps/provider-azure-anthropic.md | 1 - .../testthat/test-provider-azure-anthropic.R | 23 ++++++------- 6 files changed, 26 insertions(+), 65 deletions(-) diff --git a/R/provider-azure-anthropic.R b/R/provider-azure-anthropic.R index 67ba1c0b6..855691b40 100644 --- a/R/provider-azure-anthropic.R +++ b/R/provider-azure-anthropic.R @@ -32,7 +32,6 @@ NULL #' to the value of the `AZURE_ANTHROPIC_ENDPOINT` environment variable. #' @param model The model name to use (e.g., `"claude-opus-4-5"`). #' Defaults to `"claude-sonnet-4-5-20250929"`. -#' @param api_version The API version to use. #' @param credentials `r api_key_param("AZURE_ANTHROPIC_API_KEY")` #' @inheritParams chat_anthropic #' @inherit chat_openai return @@ -50,7 +49,6 @@ chat_azure_anthropic <- function( endpoint = azure_anthropic_endpoint(), model = NULL, params = NULL, - api_version = NULL, system_prompt = NULL, credentials = NULL, cache = c("5m", "1h", "none"), @@ -61,7 +59,6 @@ chat_azure_anthropic <- function( ) { check_string(endpoint) params <- params %||% params() - api_version <- set_default(api_version, "2024-10-22") model <- set_default(model, "claude-sonnet-4-5-20250929") cache <- arg_match(cache) echo <- check_echo(echo) @@ -77,7 +74,6 @@ chat_azure_anthropic <- function( base_url = paste0(endpoint, "/v1"), model = model, params = params, - api_version = api_version, credentials = credentials, extra_args = api_args, extra_headers = api_headers, @@ -90,41 +86,13 @@ chat_azure_anthropic <- function( ProviderAzureAnthropic <- new_class( "ProviderAzureAnthropic", - parent = ProviderAnthropic, - properties = list( - api_version = prop_string() - ) + parent = ProviderAnthropic ) azure_anthropic_endpoint <- function() { key_get("AZURE_ANTHROPIC_ENDPOINT") } -# https://learn.microsoft.com/en-us/azure/ai-services/openai/reference -method(base_request, ProviderAzureAnthropic) <- function(provider) { - req <- request(provider@base_url) - req <- ellmer_req_robustify(req, is_transient = function(resp) { - resp_status(resp) %in% c(429, 503, 529) - }) - req <- ellmer_req_user_agent(req) - # Azure AI Foundry uses api-key header (not x-api-key like standard Anthropic) - req <- ellmer_req_credentials(req, provider@credentials(), "api-key") - req <- req_url_query(req, `api-version` = provider@api_version) - - if (length(provider@beta_headers) > 0) { - req <- req_headers(req, `anthropic-beta` = provider@beta_headers) - } - - req <- req_error(req, body = function(resp) { - if (resp_content_type(resp) == "application/json") { - json <- resp_body_json(resp) - paste0(json$error$message, " [", json$error$type, "]") - } - }) - - req -} - default_azure_anthropic_credentials <- function() { azure_scope <- "https://cognitiveservices.azure.com/.default" diff --git a/man/chat_azure_anthropic.Rd b/man/chat_azure_anthropic.Rd index 92eded9b6..093c25909 100644 --- a/man/chat_azure_anthropic.Rd +++ b/man/chat_azure_anthropic.Rd @@ -8,7 +8,6 @@ chat_azure_anthropic( endpoint = azure_anthropic_endpoint(), model = NULL, params = NULL, - api_version = NULL, system_prompt = NULL, credentials = NULL, cache = c("5m", "1h", "none"), @@ -28,8 +27,6 @@ Defaults to \code{"claude-sonnet-4-5-20250929"}.} \item{params}{Common model parameters, usually created by \code{\link[=params]{params()}}.} -\item{api_version}{The API version to use.} - \item{system_prompt}{A system prompt to set the behavior of the assistant.} \item{credentials}{Override the default credentials. You generally should not need this argument; instead set the \code{AZURE_ANTHROPIC_API_KEY} environment variable. The best place to set this is in \code{.Renviron}, diff --git a/tests/testthat/_problems/test-provider-azure-anthropic-24.R b/tests/testthat/_problems/test-provider-azure-anthropic-24.R index e971b5a6b..4d948fca2 100644 --- a/tests/testthat/_problems/test-provider-azure-anthropic-24.R +++ b/tests/testthat/_problems/test-provider-azure-anthropic-24.R @@ -7,13 +7,13 @@ attach(test_env, warn.conflicts = FALSE) # test ------------------------------------------------------------------------- turn <- UserTurn( - contents = list(ContentText("What is 1 + 1?")) - ) + contents = list(ContentText("What is 1 + 1?")) +) endpoint <- "https://example.services.ai.azure.com/anthropic" p <- ProviderAzureAnthropic( - name = "Azure/Anthropic", - base_url = paste0(endpoint, "/v1"), - model = "claude-opus-4-5", - api_version = "2024-10-22", - credentials = \() "key" - ) + name = "Azure/Anthropic", + base_url = paste0(endpoint, "/v1"), + model = "claude-opus-4-5", + api_version = "2024-10-22", + credentials = \() "key" +) diff --git a/tests/testthat/_problems/test-provider-azure-anthropic-47.R b/tests/testthat/_problems/test-provider-azure-anthropic-47.R index 743f496b9..4ecac106c 100644 --- a/tests/testthat/_problems/test-provider-azure-anthropic-47.R +++ b/tests/testthat/_problems/test-provider-azure-anthropic-47.R @@ -8,10 +8,10 @@ attach(test_env, warn.conflicts = FALSE) # test ------------------------------------------------------------------------- endpoint <- "https://example.services.ai.azure.com/anthropic" p <- ProviderAzureAnthropic( - name = "Azure/Anthropic", - base_url = paste0(endpoint, "/v1"), - model = "claude-opus-4-5", - api_version = "2024-10-22", - credentials = \() "key", - beta_headers = c("feature-a", "feature-b") - ) + name = "Azure/Anthropic", + base_url = paste0(endpoint, "/v1"), + model = "claude-opus-4-5", + api_version = "2024-10-22", + credentials = \() "key", + beta_headers = c("feature-a", "feature-b") +) diff --git a/tests/testthat/_snaps/provider-azure-anthropic.md b/tests/testthat/_snaps/provider-azure-anthropic.md index ed65b9c1f..1a5eaaec8 100644 --- a/tests/testthat/_snaps/provider-azure-anthropic.md +++ b/tests/testthat/_snaps/provider-azure-anthropic.md @@ -3,7 +3,6 @@ Code . <- chat_azure_anthropic("https://example.services.ai.azure.com/anthropic") Message - Using api_version = "2024-10-22". Using model = "claude-sonnet-4-5-20250929". # service principal authentication requests look correct diff --git a/tests/testthat/test-provider-azure-anthropic.R b/tests/testthat/test-provider-azure-anthropic.R index 62a40d70c..6865a6e35 100644 --- a/tests/testthat/test-provider-azure-anthropic.R +++ b/tests/testthat/test-provider-azure-anthropic.R @@ -18,25 +18,23 @@ test_that("Azure Anthropic request headers are generated correctly", { p <- ProviderAzureAnthropic( name = "Azure/Anthropic", base_url = paste0(endpoint, "/v1"), - model = "claude-opus-4-5", + model = "claude-sonnet-4-6", params = list(), extra_args = list(), extra_headers = character(), credentials = \() "key", beta_headers = character(), - cache = "none", - api_version = "2024-10-22" + cache = "none" ) req <- chat_request(p, FALSE, list(turn)) headers <- req_get_headers(req, "reveal") - # Uses api-key header, not x-api-key like standard Anthropic - expect_equal(headers$`api-key`, "key") - expect_null(headers$`x-api-key`) - # No anthropic-version header; Azure uses api-version query param instead - expect_null(headers$`anthropic-version`) - # api-version appears as a query parameter - expect_match(req$url, "api-version=2024-10-22") + # Uses x-api-key and anthropic-version, same as standard Anthropic API + expect_equal(headers$`x-api-key`, "key") + expect_equal(headers$`anthropic-version`, "2023-06-01") + expect_null(headers$`api-key`) + # No api-version query parameter + expect_no_match(req$url, "api-version") }) test_that("beta headers are forwarded correctly", { @@ -45,14 +43,13 @@ test_that("beta headers are forwarded correctly", { p <- ProviderAzureAnthropic( name = "Azure/Anthropic", base_url = paste0(endpoint, "/v1"), - model = "claude-opus-4-5", + model = "claude-sonnet-4-6", params = list(), extra_args = list(), extra_headers = character(), credentials = \() "key", beta_headers = c("feature-a", "feature-b"), - cache = "none", - api_version = "2024-10-22" + cache = "none" ) req <- base_request(p) headers <- req_get_headers(req) From e6af65eb6373cbb75441287318fcd9f9006253e6 Mon Sep 17 00:00:00 2001 From: Daren Eiri Date: Thu, 9 Apr 2026 22:00:44 -0700 Subject: [PATCH 3/7] remove default value for model to ensure it's user defined --- R/provider-azure-anthropic.R | 7 +++---- man/chat_azure_anthropic.Rd | 5 ++--- tests/testthat/_snaps/provider-azure-anthropic.md | 7 ------- tests/testthat/test-provider-azure-anthropic.R | 7 ++++--- 4 files changed, 9 insertions(+), 17 deletions(-) diff --git a/R/provider-azure-anthropic.R b/R/provider-azure-anthropic.R index 855691b40..52649b491 100644 --- a/R/provider-azure-anthropic.R +++ b/R/provider-azure-anthropic.R @@ -30,8 +30,7 @@ NULL #' @param endpoint Azure AI Foundry endpoint URL with protocol and hostname, #' i.e. `https://{your-project}.services.ai.azure.com/anthropic`. Defaults #' to the value of the `AZURE_ANTHROPIC_ENDPOINT` environment variable. -#' @param model The model name to use (e.g., `"claude-opus-4-5"`). -#' Defaults to `"claude-sonnet-4-5-20250929"`. +#' @param model The **deployment name** for the model you want to use. #' @param credentials `r api_key_param("AZURE_ANTHROPIC_API_KEY")` #' @inheritParams chat_anthropic #' @inherit chat_openai return @@ -47,7 +46,7 @@ NULL #' } chat_azure_anthropic <- function( endpoint = azure_anthropic_endpoint(), - model = NULL, + model, params = NULL, system_prompt = NULL, credentials = NULL, @@ -58,8 +57,8 @@ chat_azure_anthropic <- function( echo = NULL ) { check_string(endpoint) + check_string(model) params <- params %||% params() - model <- set_default(model, "claude-sonnet-4-5-20250929") cache <- arg_match(cache) echo <- check_echo(echo) diff --git a/man/chat_azure_anthropic.Rd b/man/chat_azure_anthropic.Rd index 093c25909..918f5dfba 100644 --- a/man/chat_azure_anthropic.Rd +++ b/man/chat_azure_anthropic.Rd @@ -6,7 +6,7 @@ \usage{ chat_azure_anthropic( endpoint = azure_anthropic_endpoint(), - model = NULL, + model, params = NULL, system_prompt = NULL, credentials = NULL, @@ -22,8 +22,7 @@ chat_azure_anthropic( i.e. \verb{https://\{your-project\}.services.ai.azure.com/anthropic}. Defaults to the value of the \code{AZURE_ANTHROPIC_ENDPOINT} environment variable.} -\item{model}{The model name to use (e.g., \code{"claude-opus-4-5"}). -Defaults to \code{"claude-sonnet-4-5-20250929"}.} +\item{model}{The \strong{deployment name} for the model you want to use.} \item{params}{Common model parameters, usually created by \code{\link[=params]{params()}}.} diff --git a/tests/testthat/_snaps/provider-azure-anthropic.md b/tests/testthat/_snaps/provider-azure-anthropic.md index 1a5eaaec8..352ed1277 100644 --- a/tests/testthat/_snaps/provider-azure-anthropic.md +++ b/tests/testthat/_snaps/provider-azure-anthropic.md @@ -1,10 +1,3 @@ -# defaults are reported - - Code - . <- chat_azure_anthropic("https://example.services.ai.azure.com/anthropic") - Message - Using model = "claude-sonnet-4-5-20250929". - # service principal authentication requests look correct Code diff --git a/tests/testthat/test-provider-azure-anthropic.R b/tests/testthat/test-provider-azure-anthropic.R index 6865a6e35..8b1071019 100644 --- a/tests/testthat/test-provider-azure-anthropic.R +++ b/tests/testthat/test-provider-azure-anthropic.R @@ -1,9 +1,10 @@ # Defaults ---------------------------------------------------------------- -test_that("defaults are reported", { +test_that("model is required", { withr::local_envvar(AZURE_ANTHROPIC_API_KEY = "key") - expect_snapshot( - . <- chat_azure_anthropic("https://example.services.ai.azure.com/anthropic") + expect_error( + chat_azure_anthropic("https://example.services.ai.azure.com/anthropic"), + "model" ) }) From f90e909e57f87e744b19e838cba1eb10e9b0fa69 Mon Sep 17 00:00:00 2001 From: Daren Eiri Date: Thu, 9 Apr 2026 23:07:22 -0700 Subject: [PATCH 4/7] remove trailing slash at end of url --- R/provider-azure-anthropic.R | 4 ++-- man/chat_azure_anthropic.Rd | 2 +- tests/testthat/test-provider-azure-anthropic.R | 10 ++++++++++ 3 files changed, 13 insertions(+), 3 deletions(-) diff --git a/R/provider-azure-anthropic.R b/R/provider-azure-anthropic.R index 52649b491..91e38d48a 100644 --- a/R/provider-azure-anthropic.R +++ b/R/provider-azure-anthropic.R @@ -40,7 +40,7 @@ NULL #' \dontrun{ #' chat <- chat_azure_anthropic( #' endpoint = "https://your-project.services.ai.azure.com/anthropic", -#' model = "claude-opus-4-5" +#' model = "your-deployment-name" #' ) #' chat$chat("Tell me three jokes about statisticians") #' } @@ -70,7 +70,7 @@ chat_azure_anthropic <- function( provider <- ProviderAzureAnthropic( name = "Azure/Anthropic", - base_url = paste0(endpoint, "/v1"), + base_url = paste0(gsub("/$", "", endpoint), "/v1"), model = model, params = params, credentials = credentials, diff --git a/man/chat_azure_anthropic.Rd b/man/chat_azure_anthropic.Rd index 918f5dfba..0dc955356 100644 --- a/man/chat_azure_anthropic.Rd +++ b/man/chat_azure_anthropic.Rd @@ -87,7 +87,7 @@ package. \dontrun{ chat <- chat_azure_anthropic( endpoint = "https://your-project.services.ai.azure.com/anthropic", - model = "claude-opus-4-5" + model = "your-deployment-name" ) chat$chat("Tell me three jokes about statisticians") } diff --git a/tests/testthat/test-provider-azure-anthropic.R b/tests/testthat/test-provider-azure-anthropic.R index 8b1071019..92c61b8f8 100644 --- a/tests/testthat/test-provider-azure-anthropic.R +++ b/tests/testthat/test-provider-azure-anthropic.R @@ -8,6 +8,16 @@ test_that("model is required", { ) }) +test_that("trailing slash in endpoint is handled correctly", { + withr::local_envvar(AZURE_ANTHROPIC_API_KEY = "key") + chat <- chat_azure_anthropic( + endpoint = "https://example.services.ai.azure.com/anthropic/", + model = "claude-sonnet-4-6" + ) + expect_no_match(chat$get_provider()@base_url, "//v1") + expect_match(chat$get_provider()@base_url, "/v1$") +}) + # Authentication ---------------------------------------------------------- test_that("Azure Anthropic request headers are generated correctly", { From 5e163b4081c2634642bd3f5e744fa1c972673249 Mon Sep 17 00:00:00 2001 From: Daren Eiri Date: Thu, 9 Apr 2026 23:14:07 -0700 Subject: [PATCH 5/7] update link for docs --- R/provider-azure-anthropic.R | 3 +-- man/chat_azure_anthropic.Rd | 3 ++- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/R/provider-azure-anthropic.R b/R/provider-azure-anthropic.R index 91e38d48a..71afcd99c 100644 --- a/R/provider-azure-anthropic.R +++ b/R/provider-azure-anthropic.R @@ -2,11 +2,10 @@ #' @include provider-claude.R NULL -# https://learn.microsoft.com/en-us/azure/ai-services/models/get-started +# https://learn.microsoft.com/en-us/azure/foundry/foundry-models/concepts/endpoints #' Chat with an Anthropic Claude model hosted on Azure AI Foundry #' -#' @description #' [Azure AI Foundry](https://azure.microsoft.com/en-us/products/ai-foundry) #' hosts Anthropic Claude models accessible via the #' `*.services.ai.azure.com/anthropic` endpoint, using the Anthropic Messages diff --git a/man/chat_azure_anthropic.Rd b/man/chat_azure_anthropic.Rd index 0dc955356..8b5e2c87d 100644 --- a/man/chat_azure_anthropic.Rd +++ b/man/chat_azure_anthropic.Rd @@ -66,7 +66,8 @@ A \link{Chat} object. hosts Anthropic Claude models accessible via the \verb{*.services.ai.azure.com/anthropic} endpoint, using the Anthropic Messages API format. - +} +\details{ Unlike \code{\link[=chat_azure_openai]{chat_azure_openai()}}, which targets \verb{*.openai.azure.com} endpoints and uses the OpenAI chat completions format, this function targets Azure AI Foundry's Anthropic-compatible endpoint. From 0c07e4e9bb7f7b37d37fa0914e65ac6e1d578b38 Mon Sep 17 00:00:00 2001 From: Daren Eiri Date: Thu, 9 Apr 2026 23:22:54 -0700 Subject: [PATCH 6/7] clean up uncesssary files --- .../test-provider-azure-anthropic-24.R | 19 ------------------- .../test-provider-azure-anthropic-47.R | 17 ----------------- .../_snaps/provider-azure-anthropic.md | 15 --------------- .../testthat/test-provider-azure-anthropic.R | 10 +++++++++- 4 files changed, 9 insertions(+), 52 deletions(-) delete mode 100644 tests/testthat/_problems/test-provider-azure-anthropic-24.R delete mode 100644 tests/testthat/_problems/test-provider-azure-anthropic-47.R delete mode 100644 tests/testthat/_snaps/provider-azure-anthropic.md diff --git a/tests/testthat/_problems/test-provider-azure-anthropic-24.R b/tests/testthat/_problems/test-provider-azure-anthropic-24.R deleted file mode 100644 index 4d948fca2..000000000 --- a/tests/testthat/_problems/test-provider-azure-anthropic-24.R +++ /dev/null @@ -1,19 +0,0 @@ -# Extracted from test-provider-azure-anthropic.R:24 - -# setup ------------------------------------------------------------------------ -library(testthat) -test_env <- simulate_test_env(package = "ellmer", path = "..") -attach(test_env, warn.conflicts = FALSE) - -# test ------------------------------------------------------------------------- -turn <- UserTurn( - contents = list(ContentText("What is 1 + 1?")) -) -endpoint <- "https://example.services.ai.azure.com/anthropic" -p <- ProviderAzureAnthropic( - name = "Azure/Anthropic", - base_url = paste0(endpoint, "/v1"), - model = "claude-opus-4-5", - api_version = "2024-10-22", - credentials = \() "key" -) diff --git a/tests/testthat/_problems/test-provider-azure-anthropic-47.R b/tests/testthat/_problems/test-provider-azure-anthropic-47.R deleted file mode 100644 index 4ecac106c..000000000 --- a/tests/testthat/_problems/test-provider-azure-anthropic-47.R +++ /dev/null @@ -1,17 +0,0 @@ -# Extracted from test-provider-azure-anthropic.R:47 - -# setup ------------------------------------------------------------------------ -library(testthat) -test_env <- simulate_test_env(package = "ellmer", path = "..") -attach(test_env, warn.conflicts = FALSE) - -# test ------------------------------------------------------------------------- -endpoint <- "https://example.services.ai.azure.com/anthropic" -p <- ProviderAzureAnthropic( - name = "Azure/Anthropic", - base_url = paste0(endpoint, "/v1"), - model = "claude-opus-4-5", - api_version = "2024-10-22", - credentials = \() "key", - beta_headers = c("feature-a", "feature-b") -) diff --git a/tests/testthat/_snaps/provider-azure-anthropic.md b/tests/testthat/_snaps/provider-azure-anthropic.md deleted file mode 100644 index 352ed1277..000000000 --- a/tests/testthat/_snaps/provider-azure-anthropic.md +++ /dev/null @@ -1,15 +0,0 @@ -# service principal authentication requests look correct - - Code - str(request_summary(req)) - Output - List of 3 - $ url : chr "https://login.microsoftonline.com/aaaa0a0a-bb1b-cc2c-dd3d-eeeeee4e4e4e/oauth2/v2.0/token" - $ headers:List of 1 - ..$ Accept: chr "application/json" - $ body :List of 4 - ..$ grant_type : 'AsIs' chr "client_credentials" - ..$ scope : 'AsIs' chr "https%3A%2F%2Fcognitiveservices.azure.com%2F.default" - ..$ client_id : 'AsIs' chr "id" - ..$ client_secret: 'AsIs' chr "secret" - diff --git a/tests/testthat/test-provider-azure-anthropic.R b/tests/testthat/test-provider-azure-anthropic.R index 92c61b8f8..215b450d1 100644 --- a/tests/testthat/test-provider-azure-anthropic.R +++ b/tests/testthat/test-provider-azure-anthropic.R @@ -74,7 +74,15 @@ test_that("service principal authentication requests look correct", { AZURE_CLIENT_SECRET = "secret" ) local_mocked_responses(function(req) { - expect_snapshot(str(request_summary(req))) + summary <- request_summary(req) + expect_match( + summary$url, + "login.microsoftonline.com/aaaa0a0a-bb1b-cc2c-dd3d-eeeeee4e4e4e" + ) + expect_match(summary$body$grant_type, "client_credentials") + expect_match(summary$body$scope, "cognitiveservices.azure.com") + expect_match(summary$body$client_id, "id") + expect_match(summary$body$client_secret, "secret") response_json(body = list(access_token = "token")) }) source <- default_azure_anthropic_credentials() From d05272d2922826a300f2b89c7800dcfc7177d9ef Mon Sep 17 00:00:00 2001 From: Daren Eiri Date: Fri, 10 Apr 2026 08:40:16 -0700 Subject: [PATCH 7/7] updated formatting in NEWS.md, removed mention of #585 since that concerns Mistral access through Foundry, which this new functionality does not directly address --- NEWS.md | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/NEWS.md b/NEWS.md index fab4904ac..400066372 100644 --- a/NEWS.md +++ b/NEWS.md @@ -1,9 +1,6 @@ # ellmer (development version) -* New `chat_azure_anthropic()` enables chatting with Anthropic Claude models - hosted on Azure AI Foundry (`*.services.ai.azure.com/anthropic` endpoints), - with the same Azure authentication options as `chat_azure_openai()` (#585). - +* New `chat_azure_anthropic()` enables chatting with Anthropic Claude models hosted on Azure AI Foundry (`*.services.ai.azure.com/anthropic` endpoints), with the same Azure authentication options as `chat_azure_openai()`. * Fixed three bugs that caused errors when streaming web search results: Claude's `citations_delta` events were mishandled, `server_tool_use` input wasn't parsed from JSON during streaming, and OpenAI's `web_search_call` failed for non-search action types like `open_page` (#941). * `chat_aws_bedrock()` gains a `cache` parameter for prompt caching. The default, `"auto"`, enables caching for models known to support it (Anthropic Claude and Amazon Nova) and disables it otherwise (#954). * Built-in tools (e.g., `openai_tool_web_search()`, `claude_tool_web_search()`) now include `description` and `annotations` properties, making their metadata consistent with user-defined tools created by `tool()` (#942).