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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion DESCRIPTION
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down
1 change: 1 addition & 0 deletions NAMESPACE
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
1 change: 1 addition & 0 deletions NEWS.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
# ellmer (development version)

* 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).
Expand Down
168 changes: 168 additions & 0 deletions R/provider-azure-anthropic.R
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
#' @include provider-azure.R
#' @include provider-claude.R
NULL

# https://learn.microsoft.com/en-us/azure/foundry/foundry-models/concepts/endpoints

#' Chat with an Anthropic Claude model hosted on Azure AI Foundry
#'
#' [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 **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
#' @family chatbots
#' @export
#' @examples
#' \dontrun{
#' chat <- chat_azure_anthropic(
#' endpoint = "https://your-project.services.ai.azure.com/anthropic",
#' model = "your-deployment-name"
#' )
#' chat$chat("Tell me three jokes about statisticians")
#' }
chat_azure_anthropic <- function(
endpoint = azure_anthropic_endpoint(),
model,
params = 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)
check_string(model)
params <- params %||% params()
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(gsub("/$", "", endpoint), "/v1"),
model = model,
params = params,
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
)

azure_anthropic_endpoint <- function() {
key_get("AZURE_ANTHROPIC_ENDPOINT")
}

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.")
}
1 change: 1 addition & 0 deletions man/chat_anthropic.Rd

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions man/chat_aws_bedrock.Rd

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

116 changes: 116 additions & 0 deletions man/chat_azure_anthropic.Rd

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions man/chat_azure_openai.Rd

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions man/chat_cloudflare.Rd

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions man/chat_databricks.Rd

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions man/chat_deepseek.Rd

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions man/chat_github.Rd

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions man/chat_google_gemini.Rd

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions man/chat_groq.Rd

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions man/chat_huggingface.Rd

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading
Loading