Added support for azure anthopic models via chat completion

This commit is contained in:
Sameer Kankute 2025-11-19 11:55:38 +05:30
parent afc9a763cb
commit 29057ba6ad
14 changed files with 1318 additions and 9 deletions

View file

@ -17,11 +17,11 @@ LiteLLM supports all anthropic models.
| Property | Details |
|-------|-------|
| Description | Claude is a highly performant, trustworthy, and intelligent AI platform built by Anthropic. Claude excels at tasks involving language, reasoning, analysis, coding, and more. |
| Provider Route on LiteLLM | `anthropic/` (add this prefix to the model name, to route any requests to Anthropic - e.g. `anthropic/claude-3-5-sonnet-20240620`) |
| Provider Doc | [Anthropic ↗](https://docs.anthropic.com/en/docs/build-with-claude/overview) |
| API Endpoint for Provider | https://api.anthropic.com |
| Supported Endpoints | `/chat/completions` |
| Description | Claude is a highly performant, trustworthy, and intelligent AI platform built by Anthropic. Claude excels at tasks involving language, reasoning, analysis, coding, and more. Also available via Azure Foundry. |
| Provider Route on LiteLLM | `anthropic/` (add this prefix to the model name, to route any requests to Anthropic - e.g. `anthropic/claude-3-5-sonnet-20240620`). For Azure Foundry deployments, use `azure/claude-*` (see [Azure Anthropic documentation](../providers/azure/azure_anthropic)) |
| Provider Doc | [Anthropic ↗](https://docs.anthropic.com/en/docs/build-with-claude/overview), [Azure Foundry Claude ↗](https://learn.microsoft.com/en-us/azure/ai-services/foundry-models/claude) |
| API Endpoint for Provider | https://api.anthropic.com (or Azure Foundry endpoint: `https://<resource-name>.services.ai.azure.com/anthropic`) |
| Supported Endpoints | `/chat/completions`, `/v1/messages` (passthrough) |
## Supported OpenAI Parameters
@ -59,6 +59,22 @@ os.environ["ANTHROPIC_API_KEY"] = "your-api-key"
# os.environ["LITELLM_ANTHROPIC_DISABLE_URL_SUFFIX"] = "true" # [OPTIONAL] Disable automatic URL suffix appending
```
:::tip Azure Foundry Support
Claude models are also available via Microsoft Azure Foundry. Use the `azure/` prefix instead of `anthropic/` and configure Azure authentication. See the [Azure Anthropic documentation](../providers/azure/azure_anthropic) for details.
Example:
```python
response = completion(
model="azure/claude-sonnet-4-5",
api_base="https://<resource-name>.services.ai.azure.com/anthropic",
api_key="your-azure-api-key",
messages=[{"role": "user", "content": "Hello!"}]
)
```
:::
### Custom API Base
When using a custom API base for Anthropic (e.g., a proxy or custom endpoint), LiteLLM automatically appends the appropriate suffix (`/v1/messages` or `/v1/complete`) to your base URL.

View file

@ -9,10 +9,10 @@ import TabItem from '@theme/TabItem';
| Property | Details |
|-------|-------|
| Description | Azure OpenAI Service provides REST API access to OpenAI's powerful language models including o1, o1-mini, GPT-5, GPT-4o, GPT-4o mini, GPT-4 Turbo with Vision, GPT-4, GPT-3.5-Turbo, and Embeddings model series |
| Provider Route on LiteLLM | `azure/`, [`azure/o_series/`](#o-series-models), [`azure/gpt5_series/`](#gpt-5-models) |
| Supported Operations | [`/chat/completions`](#azure-openai-chat-completion-models), [`/responses`](./azure_responses), [`/completions`](#azure-instruct-models), [`/embeddings`](./azure_embedding), [`/audio/speech`](azure_speech), [`/audio/transcriptions`](../audio_transcription), `/fine_tuning`, [`/batches`](#azure-batches-api), `/files`, [`/images`](../image_generation#azure-openai-image-generation-models) |
| Link to Provider Doc | [Azure OpenAI ↗](https://learn.microsoft.com/en-us/azure/ai-services/openai/overview)
| Description | Azure OpenAI Service provides REST API access to OpenAI's powerful language models including o1, o1-mini, GPT-5, GPT-4o, GPT-4o mini, GPT-4 Turbo with Vision, GPT-4, GPT-3.5-Turbo, and Embeddings model series. Also supports Claude models via Azure Foundry. |
| Provider Route on LiteLLM | `azure/`, [`azure/o_series/`](#o-series-models), [`azure/gpt5_series/`](#gpt-5-models), [`azure/claude-*`](./azure_anthropic) (Claude models via Azure Foundry) |
| Supported Operations | [`/chat/completions`](#azure-openai-chat-completion-models), [`/responses`](./azure_responses), [`/completions`](#azure-instruct-models), [`/embeddings`](./azure_embedding), [`/audio/speech`](azure_speech), [`/audio/transcriptions`](../audio_transcription), `/fine_tuning`, [`/batches`](#azure-batches-api), `/files`, [`/images`](../image_generation#azure-openai-image-generation-models), [`/anthropic/v1/messages`](./azure_anthropic) (Claude passthrough) |
| Link to Provider Doc | [Azure OpenAI ↗](https://learn.microsoft.com/en-us/azure/ai-services/openai/overview), [Azure Foundry Claude ↗](https://learn.microsoft.com/en-us/azure/ai-services/foundry-models/claude)
## API Keys, Params
api_key, api_base, api_version etc can be passed directly to `litellm.completion` - see here or set as `litellm.api_key` params see here
@ -27,6 +27,12 @@ os.environ["AZURE_AD_TOKEN"] = ""
os.environ["AZURE_API_TYPE"] = ""
```
:::info Azure Foundry Claude Models
Azure also supports Claude models via Azure Foundry. Use `azure/claude-*` model names (e.g., `azure/claude-sonnet-4-5`) with Azure authentication. See the [Azure Anthropic documentation](./azure_anthropic) for details.
:::
## **Usage - LiteLLM Python SDK**
<a target="_blank" href="https://colab.research.google.com/github/BerriAI/litellm/blob/main/cookbook/LiteLLM_Azure_OpenAI.ipynb">
<img src="https://colab.research.google.com/assets/colab-badge.svg" alt="Open In Colab"/>

View file

@ -0,0 +1,378 @@
import Tabs from '@theme/Tabs';
import TabItem from '@theme/TabItem';
# Azure Anthropic (Claude via Azure Foundry)
LiteLLM supports Claude models deployed via Microsoft Azure Foundry, including Claude Sonnet 4.5, Claude Haiku 4.5, and Claude Opus 4.1.
## Available Models
Azure Foundry supports the following Claude models:
- `claude-sonnet-4-5` - Anthropic's most capable model for building real-world agents and handling complex, long-horizon tasks
- `claude-haiku-4-5` - Near-frontier performance with the right speed and cost for high-volume use cases
- `claude-opus-4-1` - Industry leader for coding, delivering sustained performance on long-running tasks
| Property | Details |
|-------|-------|
| Description | Claude models deployed via Microsoft Azure Foundry. Uses the same API as Anthropic's Messages API but with Azure authentication. |
| Provider Route on LiteLLM | `azure/` (add this prefix to Claude model names - e.g. `azure/claude-sonnet-4-5`) |
| Provider Doc | [Azure Foundry Claude Models ↗](https://learn.microsoft.com/en-us/azure/ai-services/foundry-models/claude) |
| API Endpoint | `https://<resource-name>.services.ai.azure.com/anthropic/v1/messages` |
| Supported Endpoints | `/chat/completions`, `/anthropic/v1/messages` (passthrough) |
## Key Features
- **Extended thinking**: Enhanced reasoning capabilities for complex tasks
- **Image and text input**: Strong vision capabilities for analyzing charts, graphs, technical diagrams, and reports
- **Code generation**: Advanced thinking with code generation, analysis, and debugging (Claude Sonnet 4.5 and Claude Opus 4.1)
- **Same API as Anthropic**: All request/response transformations are identical to the main Anthropic provider
## Authentication
Azure Anthropic supports two authentication methods:
1. **API Key**: Use the `api-key` header
2. **Azure AD Token**: Use `Authorization: Bearer <token>` header (Microsoft Entra ID)
## API Keys and Configuration
```python
import os
# Option 1: API Key authentication
os.environ["AZURE_API_KEY"] = "your-azure-api-key"
os.environ["AZURE_API_BASE"] = "https://<resource-name>.services.ai.azure.com/anthropic"
# Option 2: Azure AD Token authentication
os.environ["AZURE_AD_TOKEN"] = "your-azure-ad-token"
os.environ["AZURE_API_BASE"] = "https://<resource-name>.services.ai.azure.com/anthropic"
# Optional: Azure AD Token Provider (for automatic token refresh)
os.environ["AZURE_TENANT_ID"] = "your-tenant-id"
os.environ["AZURE_CLIENT_ID"] = "your-client-id"
os.environ["AZURE_CLIENT_SECRET"] = "your-client-secret"
os.environ["AZURE_SCOPE"] = "https://cognitiveservices.azure.com/.default"
```
## Usage - LiteLLM Python SDK
### Basic Completion
```python
from litellm import completion
# Set environment variables
os.environ["AZURE_API_KEY"] = "your-azure-api-key"
os.environ["AZURE_API_BASE"] = "https://<resource-name>.services.ai.azure.com/anthropic"
# Make a completion request
response = completion(
model="azure/claude-sonnet-4-5",
messages=[
{"role": "user", "content": "What are 3 things to visit in Seattle?"}
],
max_tokens=1000,
temperature=0.7,
)
print(response)
```
### Completion with API Key Parameter
```python
import litellm
response = litellm.completion(
model="azure/claude-sonnet-4-5",
api_base="https://<resource-name>.services.ai.azure.com/anthropic",
api_key="your-azure-api-key",
messages=[
{"role": "user", "content": "Hello!"}
],
max_tokens=1000,
)
```
### Completion with Azure AD Token
```python
import litellm
response = litellm.completion(
model="azure/claude-sonnet-4-5",
api_base="https://<resource-name>.services.ai.azure.com/anthropic",
azure_ad_token="your-azure-ad-token",
messages=[
{"role": "user", "content": "Hello!"}
],
max_tokens=1000,
)
```
### Streaming
```python
from litellm import completion
response = completion(
model="azure/claude-sonnet-4-5",
messages=[
{"role": "user", "content": "Write a short story"}
],
stream=True,
max_tokens=1000,
)
for chunk in response:
if chunk.choices[0].delta.content:
print(chunk.choices[0].delta.content, end="", flush=True)
```
### Tool Calling
```python
from litellm import completion
response = completion(
model="azure/claude-sonnet-4-5",
messages=[
{"role": "user", "content": "What's the weather in Seattle?"}
],
tools=[
{
"type": "function",
"function": {
"name": "get_weather",
"description": "Get the current weather in a given location",
"parameters": {
"type": "object",
"properties": {
"location": {
"type": "string",
"description": "The city and state, e.g. San Francisco, CA"
}
},
"required": ["location"]
}
}
}
],
tool_choice="auto",
max_tokens=1000,
)
print(response)
```
## Usage - LiteLLM Proxy Server
### 1. Save key in your environment
```bash
export AZURE_API_KEY="your-azure-api-key"
export AZURE_API_BASE="https://<resource-name>.services.ai.azure.com/anthropic"
```
### 2. Configure the proxy
```yaml
model_list:
- model_name: claude-sonnet-4-5
litellm_params:
model: azure/claude-sonnet-4-5
api_base: https://<resource-name>.services.ai.azure.com/anthropic
api_key: os.environ/AZURE_API_KEY
```
### 3. Test it
<Tabs>
<TabItem value="curl" label="curl">
```bash
curl --location 'http://0.0.0.0:4000/chat/completions' \
--header 'Content-Type: application/json' \
--data '{
"model": "claude-sonnet-4-5",
"messages": [
{
"role": "user",
"content": "Hello!"
}
],
"max_tokens": 1000
}'
```
</TabItem>
<TabItem value="openai" label="OpenAI Python SDK">
```python
from openai import OpenAI
client = OpenAI(
api_key="anything",
base_url="http://0.0.0.0:4000"
)
response = client.chat.completions.create(
model="claude-sonnet-4-5",
messages=[
{"role": "user", "content": "Hello!"}
],
max_tokens=1000
)
print(response)
```
</TabItem>
</Tabs>
## Messages API Passthrough
Azure Anthropic also supports the native Anthropic Messages API via passthrough. The endpoint structure is the same as Anthropic's `/v1/messages` API.
### Using Anthropic SDK
```python
from anthropic import Anthropic
client = Anthropic(
api_key="your-azure-api-key",
base_url="https://<resource-name>.services.ai.azure.com/anthropic"
)
response = client.messages.create(
model="claude-sonnet-4-5",
max_tokens=1000,
messages=[
{"role": "user", "content": "Hello, world"}
]
)
print(response)
```
### Using LiteLLM Proxy Passthrough
```bash
curl --request POST \
--url http://0.0.0.0:4000/anthropic/v1/messages \
--header 'accept: application/json' \
--header 'content-type: application/json' \
--header "Authorization: bearer sk-anything" \
--data '{
"model": "claude-sonnet-4-5",
"max_tokens": 1024,
"messages": [
{"role": "user", "content": "Hello, world"}
]
}'
```
## Supported OpenAI Parameters
Azure Anthropic supports the same parameters as the main Anthropic provider:
```
"stream",
"stop",
"temperature",
"top_p",
"max_tokens",
"max_completion_tokens",
"tools",
"tool_choice",
"extra_headers",
"parallel_tool_calls",
"response_format",
"user",
"thinking",
"reasoning_effort"
```
:::info
Azure Anthropic API requires `max_tokens` to be passed. LiteLLM automatically passes `max_tokens=4096` when no `max_tokens` are provided.
:::
## Differences from Standard Anthropic Provider
The only difference between Azure Anthropic and the standard Anthropic provider is authentication:
- **Standard Anthropic**: Uses `x-api-key` header
- **Azure Anthropic**: Uses `api-key` header or `Authorization: Bearer <token>` for Azure AD authentication
All other request/response transformations, tool calling, streaming, and feature support are identical.
## API Base URL Format
The API base URL should follow this format:
```
https://<resource-name>.services.ai.azure.com/anthropic
```
LiteLLM will automatically append `/v1/messages` if not already present in the URL.
## Example: Full Configuration
```python
import os
from litellm import completion
# Configure Azure Anthropic
os.environ["AZURE_API_KEY"] = "your-azure-api-key"
os.environ["AZURE_API_BASE"] = "https://my-resource.services.ai.azure.com/anthropic"
# Make a request
response = completion(
model="azure/claude-sonnet-4-5",
messages=[
{"role": "system", "content": "You are a helpful assistant."},
{"role": "user", "content": "Explain quantum computing in simple terms."}
],
max_tokens=1000,
temperature=0.7,
stream=False,
)
print(response.choices[0].message.content)
```
## Troubleshooting
### Missing API Base Error
If you see an error about missing API base, ensure you've set:
```python
os.environ["AZURE_API_BASE"] = "https://<resource-name>.services.ai.azure.com/anthropic"
```
Or pass it directly:
```python
response = completion(
model="azure/claude-sonnet-4-5",
api_base="https://<resource-name>.services.ai.azure.com/anthropic",
# ...
)
```
### Authentication Errors
- **API Key**: Ensure `AZURE_API_KEY` is set or passed as `api_key` parameter
- **Azure AD Token**: Ensure `AZURE_AD_TOKEN` is set or passed as `azure_ad_token` parameter
- **Token Provider**: For automatic token refresh, configure `AZURE_TENANT_ID`, `AZURE_CLIENT_ID`, and `AZURE_CLIENT_SECRET`
## Related Documentation
- [Anthropic Provider Documentation](./anthropic.md) - For standard Anthropic API usage
- [Azure OpenAI Documentation](./azure.md) - For Azure OpenAI models
- [Azure Authentication Guide](../secret_managers/azure_key_vault.md) - For Azure AD token setup

View file

@ -1108,6 +1108,7 @@ from .llms.openrouter.chat.transformation import OpenrouterConfig
from .llms.datarobot.chat.transformation import DataRobotConfig
from .llms.anthropic.chat.transformation import AnthropicConfig
from .llms.anthropic.common_utils import AnthropicModelInfo
from .llms.azure.anthropic.transformation import AzureAnthropicConfig
from .llms.groq.stt.transformation import GroqSTTConfig
from .llms.anthropic.completion.transformation import AnthropicTextConfig
from .llms.triton.completion.transformation import TritonConfig

View file

@ -22,6 +22,19 @@ def _is_non_openai_azure_model(model: str) -> bool:
return False
def _is_azure_anthropic_model(model: str) -> Optional[str]:
try:
model_parts = model.split("/", 1)
if len(model_parts) > 1:
model_name = model_parts[1].lower()
# Check if model name contains claude
if "claude" in model_name or model_name.startswith("claude"):
return model_parts[1] # Return model name without "azure/" prefix
except Exception:
pass
return None
def handle_cohere_chat_model_custom_llm_provider(
model: str, custom_llm_provider: Optional[str] = None
) -> Tuple[str, Optional[str]]:
@ -123,6 +136,11 @@ def get_llm_provider( # noqa: PLR0915
# AZURE AI-Studio Logic - Azure AI Studio supports AZURE/Cohere
# If User passes azure/command-r-plus -> we should send it to cohere_chat/command-r-plus
if model.split("/", 1)[0] == "azure":
# Check if it's an Azure Anthropic model (claude models)
azure_anthropic_model = _is_azure_anthropic_model(model)
if azure_anthropic_model:
custom_llm_provider = "azure_anthropic"
return azure_anthropic_model, custom_llm_provider, dynamic_api_key, api_base
if _is_non_openai_azure_model(model):
custom_llm_provider = "openai"
return model, custom_llm_provider, dynamic_api_key, api_base

View file

@ -0,0 +1,8 @@
"""
Azure Anthropic provider - supports Claude models via Azure Foundry
"""
from .handler import AzureAnthropicChatCompletion
from .transformation import AzureAnthropicConfig
__all__ = ["AzureAnthropicChatCompletion", "AzureAnthropicConfig"]

View file

@ -0,0 +1,236 @@
"""
Azure Anthropic handler - reuses AnthropicChatCompletion logic with Azure authentication
"""
import copy
import json
from typing import TYPE_CHECKING, Any, Callable, Dict, List, Optional, Union
import httpx
import litellm
from litellm.llms.anthropic.chat.handler import AnthropicChatCompletion
from litellm.llms.custom_httpx.http_handler import (
AsyncHTTPHandler,
HTTPHandler,
get_async_httpx_client,
)
from litellm.types.utils import ModelResponse
from litellm.utils import CustomStreamWrapper
from .transformation import AzureAnthropicConfig
if TYPE_CHECKING:
from litellm.litellm_core_utils.streaming_handler import CustomStreamWrapper as CustomStreamWrapperType
from litellm.llms.base_llm.chat.transformation import BaseConfig
class AzureAnthropicChatCompletion(AnthropicChatCompletion):
"""
Azure Anthropic chat completion handler.
Reuses all Anthropic logic but with Azure authentication.
"""
def __init__(self) -> None:
super().__init__()
def completion(
self,
model: str,
messages: list,
api_base: str,
custom_llm_provider: str,
custom_prompt_dict: dict,
model_response: ModelResponse,
print_verbose: Callable,
encoding,
api_key,
logging_obj,
optional_params: dict,
timeout: Union[float, httpx.Timeout],
litellm_params: dict,
acompletion=None,
logger_fn=None,
headers={},
client=None,
):
"""
Completion method that uses Azure authentication instead of Anthropic's x-api-key.
All other logic is the same as AnthropicChatCompletion.
"""
from litellm.utils import ProviderConfigManager
optional_params = copy.deepcopy(optional_params)
stream = optional_params.pop("stream", None)
json_mode: bool = optional_params.pop("json_mode", False)
is_vertex_request: bool = optional_params.pop("is_vertex_request", False)
_is_function_call = False
messages = copy.deepcopy(messages)
# Use AzureAnthropicConfig instead of AnthropicConfig
headers = AzureAnthropicConfig().validate_environment(
api_key=api_key,
headers=headers,
model=model,
messages=messages,
optional_params={**optional_params, "is_vertex_request": is_vertex_request},
litellm_params=litellm_params,
)
config = ProviderConfigManager.get_provider_chat_config(
model=model,
provider=litellm.types.utils.LlmProviders(custom_llm_provider),
)
if config is None:
raise ValueError(
f"Provider config not found for model: {model} and provider: {custom_llm_provider}"
)
data = config.transform_request(
model=model,
messages=messages,
optional_params=optional_params,
litellm_params=litellm_params,
headers=headers,
)
## LOGGING
logging_obj.pre_call(
input=messages,
api_key=api_key,
additional_args={
"complete_input_dict": data,
"api_base": api_base,
"headers": headers,
},
)
print_verbose(f"_is_function_call: {_is_function_call}")
if acompletion is True:
if (
stream is True
): # if function call - fake the streaming (need complete blocks for output parsing in openai format)
print_verbose("makes async azure anthropic streaming POST request")
data["stream"] = stream
return self.acompletion_stream_function(
model=model,
messages=messages,
data=data,
api_base=api_base,
custom_prompt_dict=custom_prompt_dict,
model_response=model_response,
print_verbose=print_verbose,
encoding=encoding,
api_key=api_key,
logging_obj=logging_obj,
optional_params=optional_params,
stream=stream,
_is_function_call=_is_function_call,
json_mode=json_mode,
litellm_params=litellm_params,
logger_fn=logger_fn,
headers=headers,
timeout=timeout,
client=(
client
if client is not None and isinstance(client, AsyncHTTPHandler)
else None
),
)
else:
return self.acompletion_function(
model=model,
messages=messages,
data=data,
api_base=api_base,
custom_prompt_dict=custom_prompt_dict,
model_response=model_response,
print_verbose=print_verbose,
encoding=encoding,
api_key=api_key,
provider_config=config,
logging_obj=logging_obj,
optional_params=optional_params,
stream=stream,
_is_function_call=_is_function_call,
litellm_params=litellm_params,
logger_fn=logger_fn,
headers=headers,
client=client,
json_mode=json_mode,
timeout=timeout,
)
else:
## COMPLETION CALL
if (
stream is True
): # if function call - fake the streaming (need complete blocks for output parsing in openai format)
data["stream"] = stream
# Import the make_sync_call from parent
from litellm.llms.anthropic.chat.handler import make_sync_call
completion_stream, response_headers = make_sync_call(
client=client,
api_base=api_base,
headers=headers, # type: ignore
data=json.dumps(data),
model=model,
messages=messages,
logging_obj=logging_obj,
timeout=timeout,
json_mode=json_mode,
)
from litellm.llms.anthropic.common_utils import process_anthropic_headers
return CustomStreamWrapper(
completion_stream=completion_stream,
model=model,
custom_llm_provider="azure_anthropic",
logging_obj=logging_obj,
_response_headers=process_anthropic_headers(response_headers),
)
else:
if client is None or not isinstance(client, HTTPHandler):
from litellm.llms.custom_httpx.http_handler import _get_httpx_client
client = _get_httpx_client(params={"timeout": timeout})
else:
client = client
try:
response = client.post(
api_base,
headers=headers,
data=json.dumps(data),
timeout=timeout,
)
except Exception as e:
from litellm.llms.anthropic.common_utils import AnthropicError
status_code = getattr(e, "status_code", 500)
error_headers = getattr(e, "headers", None)
error_text = getattr(e, "text", str(e))
error_response = getattr(e, "response", None)
if error_headers is None and error_response:
error_headers = getattr(error_response, "headers", None)
if error_response and hasattr(error_response, "text"):
error_text = getattr(error_response, "text", error_text)
raise AnthropicError(
message=error_text,
status_code=status_code,
headers=error_headers,
)
return config.transform_response(
model=model,
raw_response=response,
model_response=model_response,
logging_obj=logging_obj,
api_key=api_key,
request_data=data,
messages=messages,
optional_params=optional_params,
litellm_params=litellm_params,
encoding=encoding,
json_mode=json_mode,
)

View file

@ -0,0 +1,96 @@
"""
Azure Anthropic transformation config - extends AnthropicConfig with Azure authentication
"""
from typing import TYPE_CHECKING, Any, Dict, List, Optional, Union
import litellm
from litellm.llms.anthropic.chat.transformation import AnthropicConfig
from litellm.llms.azure.common_utils import BaseAzureLLM, get_azure_ad_token
from litellm.types.llms.openai import AllMessageValues
from litellm.types.router import GenericLiteLLMParams
if TYPE_CHECKING:
pass
class AzureAnthropicConfig(AnthropicConfig):
"""
Azure Anthropic configuration that extends AnthropicConfig.
The only difference is authentication - Azure uses api-key header or Azure AD token
instead of x-api-key header.
"""
@property
def custom_llm_provider(self) -> Optional[str]:
return "azure_anthropic"
def validate_environment(
self,
headers: dict,
model: str,
messages: List[AllMessageValues],
optional_params: dict,
litellm_params: Union[dict, GenericLiteLLMParams],
api_key: Optional[str] = None,
api_base: Optional[str] = None,
) -> Dict:
"""
Validate environment and set up Azure authentication headers.
Azure supports:
1. API key via 'api-key' header
2. Azure AD token via 'Authorization: Bearer <token>' header
"""
# Convert dict to GenericLiteLLMParams if needed
if isinstance(litellm_params, dict):
# Ensure api_key is included if provided
if api_key and "api_key" not in litellm_params:
litellm_params = {**litellm_params, "api_key": api_key}
litellm_params_obj = GenericLiteLLMParams(**litellm_params)
else:
litellm_params_obj = litellm_params or GenericLiteLLMParams()
# Set api_key if provided and not already set
if api_key and not litellm_params_obj.api_key:
litellm_params_obj.api_key = api_key
# Use Azure authentication logic
headers = BaseAzureLLM._base_validate_azure_environment(
headers=headers, litellm_params=litellm_params_obj
)
# Get tools and other anthropic-specific setup
tools = optional_params.get("tools")
prompt_caching_set = self.is_cache_control_set(messages=messages)
computer_tool_used = self.is_computer_tool_used(tools=tools)
mcp_server_used = self.is_mcp_server_used(
mcp_servers=optional_params.get("mcp_servers")
)
pdf_used = self.is_pdf_used(messages=messages)
file_id_used = self.is_file_id_used(messages=messages)
user_anthropic_beta_headers = self._get_user_anthropic_beta_headers(
anthropic_beta_header=headers.get("anthropic-beta")
)
# Get anthropic headers (but we'll replace x-api-key with Azure auth)
anthropic_headers = self.get_anthropic_headers(
computer_tool_used=computer_tool_used,
prompt_caching_set=prompt_caching_set,
pdf_used=pdf_used,
api_key=api_key or "", # Azure auth is already in headers
file_id_used=file_id_used,
is_vertex_request=optional_params.get("is_vertex_request", False),
user_anthropic_beta_headers=user_anthropic_beta_headers,
mcp_server_used=mcp_server_used,
)
# Remove x-api-key from anthropic headers since Azure uses different auth
anthropic_headers.pop("x-api-key", None)
# Merge headers - Azure auth (api-key or Authorization) takes precedence
headers = {**anthropic_headers, **headers}
# Ensure anthropic-version header is set
if "anthropic-version" not in headers:
headers["anthropic-version"] = "2023-06-01"
return headers

View file

@ -152,6 +152,7 @@ from .litellm_core_utils.prompt_templates.factory import (
)
from .litellm_core_utils.streaming_chunk_builder_utils import ChunkProcessor
from .llms.anthropic.chat import AnthropicChatCompletion
from .llms.azure.anthropic.handler import AzureAnthropicChatCompletion
from .llms.azure.audio_transcriptions import AzureAudioTranscription
from .llms.azure.azure import AzureChatCompletion, _check_dynamic_azure_params
from .llms.azure.chat.o_series_handler import AzureOpenAIO1ChatCompletion
@ -253,6 +254,7 @@ openai_image_variations = OpenAIImageVariationsHandler()
groq_chat_completions = GroqChatCompletion()
azure_ai_embedding = AzureAIEmbedding()
anthropic_chat_completions = AnthropicChatCompletion()
azure_anthropic_chat_completions = AzureAnthropicChatCompletion()
azure_chat_completions = AzureChatCompletion()
azure_o1_chat_completions = AzureOpenAIO1ChatCompletion()
azure_text_completions = AzureTextCompletion()
@ -2353,6 +2355,62 @@ def completion( # type: ignore # noqa: PLR0915
original_response=response,
)
response = response
elif custom_llm_provider == "azure_anthropic":
# Azure Anthropic uses same API as Anthropic but with Azure authentication
api_key = (
api_key
or litellm.azure_key
or litellm.api_key
or get_secret("AZURE_API_KEY")
or get_secret("AZURE_OPENAI_API_KEY")
)
custom_prompt_dict = custom_prompt_dict or litellm.custom_prompt_dict
# Azure Foundry endpoint format: https://<resource-name>.services.ai.azure.com/anthropic/v1/messages
api_base = (
api_base
or litellm.api_base
or get_secret("AZURE_API_BASE")
)
if api_base is None:
raise ValueError(
"Missing Azure API Base - Please set `api_base` or `AZURE_API_BASE` environment variable. "
"Expected format: https://<resource-name>.services.ai.azure.com/anthropic"
)
# Ensure the URL ends with /v1/messages
if not api_base.endswith("/v1/messages"):
if not api_base.endswith("/anthropic"):
api_base = api_base.rstrip("/") + "/anthropic"
api_base = api_base.rstrip("/") + "/v1/messages"
response = azure_anthropic_chat_completions.completion(
model=model,
messages=messages,
api_base=api_base,
acompletion=acompletion,
custom_prompt_dict=litellm.custom_prompt_dict,
model_response=model_response,
print_verbose=print_verbose,
optional_params=optional_params,
litellm_params=litellm_params,
logger_fn=logger_fn,
encoding=encoding, # for calculating input/output tokens
api_key=api_key,
logging_obj=logging,
headers=headers,
timeout=timeout,
client=client,
custom_llm_provider=custom_llm_provider,
)
if optional_params.get("stream", False) or acompletion is True:
## LOGGING
logging.post_call(
input=messages,
api_key=api_key,
original_response=response,
)
response = response
elif custom_llm_provider == "nlp_cloud":
nlp_cloud_key = (
api_key

View file

@ -2545,6 +2545,7 @@ class LlmProviders(str, Enum):
AZURE = "azure"
AZURE_TEXT = "azure_text"
AZURE_AI = "azure_ai"
AZURE_ANTHROPIC = "azure_anthropic"
SAGEMAKER = "sagemaker"
SAGEMAKER_CHAT = "sagemaker_chat"
BEDROCK = "bedrock"

View file

@ -7148,6 +7148,8 @@ class ProviderConfigManager:
return litellm.AzureAIStudioConfig()
elif litellm.LlmProviders.AZURE_TEXT == provider:
return litellm.AzureOpenAITextConfig()
elif litellm.LlmProviders.AZURE_ANTHROPIC == provider:
return litellm.AzureAnthropicConfig()
elif litellm.LlmProviders.HOSTED_VLLM == provider:
return litellm.HostedVLLMChatConfig()
elif litellm.LlmProviders.NLP_CLOUD == provider:

View file

@ -0,0 +1,216 @@
import os
import sys
sys.path.insert(
0, os.path.abspath(os.path.join(os.path.dirname(__file__), "../../../../.."))
)
import json
from unittest.mock import MagicMock, patch
import pytest
from litellm.llms.azure.anthropic.handler import AzureAnthropicChatCompletion
from litellm.types.utils import ModelResponse
class TestAzureAnthropicChatCompletion:
def test_inherits_from_anthropic_chat_completion(self):
"""Test that AzureAnthropicChatCompletion inherits from AnthropicChatCompletion"""
handler = AzureAnthropicChatCompletion()
assert isinstance(handler, AzureAnthropicChatCompletion)
# Check that it has methods from parent class
assert hasattr(handler, "acompletion_function")
assert hasattr(handler, "acompletion_stream_function")
@patch("litellm.utils.ProviderConfigManager")
@patch("litellm.llms.azure.anthropic.handler.AzureAnthropicConfig")
def test_completion_uses_azure_anthropic_config(self, mock_azure_config, mock_provider_manager):
"""Test that completion method uses AzureAnthropicConfig"""
handler = AzureAnthropicChatCompletion()
mock_config = MagicMock()
mock_config.transform_request.return_value = {"model": "claude-sonnet-4-5", "messages": []}
mock_config.transform_response.return_value = ModelResponse()
mock_config_instance = MagicMock()
mock_config_instance.validate_environment.return_value = {"api-key": "test-api-key", "anthropic-version": "2023-06-01"}
mock_azure_config.return_value = mock_config_instance
mock_provider_manager.get_provider_chat_config.return_value = mock_config
model = "claude-sonnet-4-5"
messages = [{"role": "user", "content": "Hello"}]
api_base = "https://test.services.ai.azure.com/anthropic/v1/messages"
custom_llm_provider = "azure_anthropic"
custom_prompt_dict = {}
model_response = ModelResponse()
print_verbose = MagicMock()
encoding = MagicMock()
api_key = "test-api-key"
logging_obj = MagicMock()
optional_params = {}
timeout = 60.0
litellm_params = {"api_key": "test-api-key"}
headers = {}
with patch.object(
handler, "acompletion_function", return_value=ModelResponse()
) as mock_acompletion:
handler.completion(
model=model,
messages=messages,
api_base=api_base,
custom_llm_provider=custom_llm_provider,
custom_prompt_dict=custom_prompt_dict,
model_response=model_response,
print_verbose=print_verbose,
encoding=encoding,
api_key=api_key,
logging_obj=logging_obj,
optional_params=optional_params,
timeout=timeout,
litellm_params=litellm_params,
headers=headers,
acompletion=True,
)
# Verify AzureAnthropicConfig was used
mock_azure_config.assert_called_once()
mock_config_instance.validate_environment.assert_called_once()
@patch("litellm.llms.anthropic.chat.handler.make_sync_call")
@patch("litellm.utils.ProviderConfigManager")
@patch("litellm.llms.azure.anthropic.handler.AzureAnthropicConfig")
def test_completion_streaming(self, mock_azure_config, mock_provider_manager, mock_make_sync_call):
# Note: decorators are applied in reverse order
"""Test completion with streaming"""
handler = AzureAnthropicChatCompletion()
mock_config = MagicMock()
mock_config.transform_request.return_value = {
"model": "claude-sonnet-4-5",
"messages": [],
"stream": True,
}
mock_config_instance = MagicMock()
mock_config_instance.validate_environment.return_value = {"api-key": "test-api-key", "anthropic-version": "2023-06-01"}
mock_azure_config.return_value = mock_config_instance
mock_provider_manager.get_provider_chat_config.return_value = mock_config
# Mock streaming response
mock_stream = MagicMock()
mock_headers = MagicMock()
mock_make_sync_call.return_value = (mock_stream, mock_headers)
model = "claude-sonnet-4-5"
messages = [{"role": "user", "content": "Hello"}]
api_base = "https://test.services.ai.azure.com/anthropic/v1/messages"
custom_llm_provider = "azure_anthropic"
custom_prompt_dict = {}
model_response = ModelResponse()
print_verbose = MagicMock()
encoding = MagicMock()
api_key = "test-api-key"
logging_obj = MagicMock()
optional_params = {"stream": True}
timeout = 60.0
litellm_params = {"api_key": "test-api-key"}
headers = {}
result = handler.completion(
model=model,
messages=messages,
api_base=api_base,
custom_llm_provider=custom_llm_provider,
custom_prompt_dict=custom_prompt_dict,
model_response=model_response,
print_verbose=print_verbose,
encoding=encoding,
api_key=api_key,
logging_obj=logging_obj,
optional_params=optional_params,
timeout=timeout,
litellm_params=litellm_params,
headers=headers,
acompletion=False,
)
# Verify streaming was handled
mock_make_sync_call.assert_called_once()
assert result is not None
@patch("litellm.llms.custom_httpx.http_handler._get_httpx_client")
@patch("litellm.utils.ProviderConfigManager")
@patch("litellm.llms.azure.anthropic.handler.AzureAnthropicConfig")
def test_completion_non_streaming(self, mock_azure_config, mock_provider_manager, mock_get_client):
# Note: decorators are applied in reverse order
"""Test completion without streaming"""
handler = AzureAnthropicChatCompletion()
mock_config = MagicMock()
mock_config.transform_request.return_value = {
"model": "claude-sonnet-4-5",
"messages": [],
}
mock_response = ModelResponse()
mock_config.transform_response.return_value = mock_response
mock_config_instance = MagicMock()
mock_config_instance.validate_environment.return_value = {"api-key": "test-api-key", "anthropic-version": "2023-06-01"}
mock_azure_config.return_value = mock_config_instance
mock_provider_manager.get_provider_chat_config.return_value = mock_config
model = "claude-sonnet-4-5"
messages = [{"role": "user", "content": "Hello"}]
api_base = "https://test.services.ai.azure.com/anthropic/v1/messages"
custom_llm_provider = "azure_anthropic"
custom_prompt_dict = {}
model_response = ModelResponse()
print_verbose = MagicMock()
encoding = MagicMock()
api_key = "test-api-key"
logging_obj = MagicMock()
optional_params = {}
timeout = 60.0
litellm_params = {"api_key": "test-api-key"}
headers = {}
# Mock HTTP client
mock_client = MagicMock()
mock_response_obj = MagicMock()
mock_response_obj.status_code = 200
mock_response_obj.text = json.dumps({
"id": "test-id",
"model": "claude-sonnet-4-5",
"content": [{"type": "text", "text": "Hello!"}],
"stop_reason": "end_turn",
"usage": {"input_tokens": 10, "output_tokens": 5},
})
mock_response_obj.json.return_value = {
"id": "test-id",
"model": "claude-sonnet-4-5",
"content": [{"type": "text", "text": "Hello!"}],
"stop_reason": "end_turn",
"usage": {"input_tokens": 10, "output_tokens": 5},
}
mock_client.post.return_value = mock_response_obj
mock_get_client.return_value = mock_client
result = handler.completion(
model=model,
messages=messages,
api_base=api_base,
custom_llm_provider=custom_llm_provider,
custom_prompt_dict=custom_prompt_dict,
model_response=model_response,
print_verbose=print_verbose,
encoding=encoding,
api_key=api_key,
logging_obj=logging_obj,
optional_params=optional_params,
timeout=timeout,
litellm_params=litellm_params,
headers=headers,
client=None, # Let it create the client
acompletion=False,
)
# Verify non-streaming was handled
mock_client.post.assert_called_once()
assert result is not None

View file

@ -0,0 +1,82 @@
import os
import sys
sys.path.insert(
0, os.path.abspath(os.path.join(os.path.dirname(__file__), "../../../../.."))
)
import pytest
from litellm.litellm_core_utils.get_llm_provider_logic import _is_azure_anthropic_model, get_llm_provider
class TestAzureAnthropicProviderRouting:
def test_is_azure_anthropic_model_with_claude(self):
"""Test _is_azure_anthropic_model detects Claude models"""
# Test various Claude model names
assert _is_azure_anthropic_model("azure/claude-sonnet-4-5") == "claude-sonnet-4-5"
assert _is_azure_anthropic_model("azure/claude-opus-4-1") == "claude-opus-4-1"
assert _is_azure_anthropic_model("azure/claude-haiku-4-5") == "claude-haiku-4-5"
assert _is_azure_anthropic_model("azure/claude-3-5-sonnet") == "claude-3-5-sonnet"
assert _is_azure_anthropic_model("azure/claude-3-opus") == "claude-3-opus"
def test_is_azure_anthropic_model_case_insensitive(self):
"""Test _is_azure_anthropic_model is case insensitive"""
assert _is_azure_anthropic_model("azure/CLAUDE-sonnet-4-5") == "CLAUDE-sonnet-4-5"
assert _is_azure_anthropic_model("azure/Claude-Sonnet-4-5") == "Claude-Sonnet-4-5"
def test_is_azure_anthropic_model_with_non_claude(self):
"""Test _is_azure_anthropic_model returns None for non-Claude models"""
assert _is_azure_anthropic_model("azure/gpt-4") is None
assert _is_azure_anthropic_model("azure/gpt-35-turbo") is None
assert _is_azure_anthropic_model("azure/command-r-plus") is None
def test_is_azure_anthropic_model_with_invalid_format(self):
"""Test _is_azure_anthropic_model handles invalid formats"""
assert _is_azure_anthropic_model("azure") is None
assert _is_azure_anthropic_model("claude-sonnet-4-5") is None
assert _is_azure_anthropic_model("") is None
def test_get_llm_provider_routes_azure_claude_to_azure_anthropic(self):
"""Test that get_llm_provider routes azure/claude-* models to azure_anthropic"""
model, provider, dynamic_api_key, api_base = get_llm_provider(
model="azure/claude-sonnet-4-5"
)
assert provider == "azure_anthropic"
assert model == "claude-sonnet-4-5" # Should strip "azure/" prefix
def test_get_llm_provider_routes_azure_claude_opus(self):
"""Test routing for Claude Opus models"""
model, provider, dynamic_api_key, api_base = get_llm_provider(
model="azure/claude-opus-4-1"
)
assert provider == "azure_anthropic"
assert model == "claude-opus-4-1"
def test_get_llm_provider_routes_azure_claude_haiku(self):
"""Test routing for Claude Haiku models"""
model, provider, dynamic_api_key, api_base = get_llm_provider(
model="azure/claude-haiku-4-5"
)
assert provider == "azure_anthropic"
assert model == "claude-haiku-4-5"
def test_get_llm_provider_does_not_route_non_claude_azure_models(self):
"""Test that non-Claude Azure models are not routed to azure_anthropic"""
model, provider, dynamic_api_key, api_base = get_llm_provider(
model="azure/gpt-4"
)
assert provider != "azure_anthropic"
# Should be routed to regular azure provider
assert provider == "azure" or provider == "openai"
def test_get_llm_provider_with_custom_llm_provider_override(self):
"""Test that custom_llm_provider parameter can override routing"""
model, provider, dynamic_api_key, api_base = get_llm_provider(
model="azure/claude-sonnet-4-5", custom_llm_provider="azure"
)
# When custom_llm_provider is explicitly set, it should be respected
# But the routing logic should still detect it as azure_anthropic
# This depends on the order of checks in get_llm_provider
assert provider in ["azure_anthropic", "azure"]

View file

@ -0,0 +1,191 @@
import os
import sys
sys.path.insert(
0, os.path.abspath(os.path.join(os.path.dirname(__file__), "../../../../.."))
)
import pytest
from unittest.mock import MagicMock, patch
from litellm.llms.azure.anthropic.transformation import AzureAnthropicConfig
from litellm.types.router import GenericLiteLLMParams
class TestAzureAnthropicConfig:
def test_custom_llm_provider(self):
"""Test that custom_llm_provider returns 'azure_anthropic'"""
config = AzureAnthropicConfig()
assert config.custom_llm_provider == "azure_anthropic"
def test_validate_environment_with_dict_litellm_params(self):
"""Test validate_environment with dict litellm_params"""
config = AzureAnthropicConfig()
headers = {}
model = "claude-sonnet-4-5"
messages = [{"role": "user", "content": "Hello"}]
optional_params = {}
litellm_params = {"api_key": "test-api-key"}
api_key = "test-api-key"
with patch(
"litellm.llms.azure.common_utils.BaseAzureLLM._base_validate_azure_environment"
) as mock_validate:
mock_validate.return_value = {"api-key": "test-api-key"}
result = config.validate_environment(
headers=headers,
model=model,
messages=messages,
optional_params=optional_params,
litellm_params=litellm_params,
api_key=api_key,
)
# Verify that dict was converted to GenericLiteLLMParams
call_args = mock_validate.call_args
assert isinstance(call_args[1]["litellm_params"], GenericLiteLLMParams)
assert call_args[1]["litellm_params"].api_key == "test-api-key"
assert "anthropic-version" in result
def test_validate_environment_with_generic_litellm_params(self):
"""Test validate_environment with GenericLiteLLMParams object"""
config = AzureAnthropicConfig()
headers = {}
model = "claude-sonnet-4-5"
messages = [{"role": "user", "content": "Hello"}]
optional_params = {}
litellm_params = GenericLiteLLMParams(api_key="test-api-key")
api_key = "test-api-key"
with patch(
"litellm.llms.azure.common_utils.BaseAzureLLM._base_validate_azure_environment"
) as mock_validate:
mock_validate.return_value = {"api-key": "test-api-key"}
result = config.validate_environment(
headers=headers,
model=model,
messages=messages,
optional_params=optional_params,
litellm_params=litellm_params,
api_key=api_key,
)
# Verify that GenericLiteLLMParams was passed through
call_args = mock_validate.call_args
assert isinstance(call_args[1]["litellm_params"], GenericLiteLLMParams)
assert "anthropic-version" in result
def test_validate_environment_sets_api_key_in_litellm_params(self):
"""Test that api_key parameter is set in litellm_params if provided"""
config = AzureAnthropicConfig()
headers = {}
model = "claude-sonnet-4-5"
messages = [{"role": "user", "content": "Hello"}]
optional_params = {}
litellm_params = {} # Empty dict, no api_key
api_key = "provided-api-key"
with patch(
"litellm.llms.azure.common_utils.BaseAzureLLM._base_validate_azure_environment"
) as mock_validate:
mock_validate.return_value = {"api-key": "provided-api-key"}
config.validate_environment(
headers=headers,
model=model,
messages=messages,
optional_params=optional_params,
litellm_params=litellm_params,
api_key=api_key,
)
# Verify that api_key was set in litellm_params
call_args = mock_validate.call_args
assert call_args[1]["litellm_params"].api_key == "provided-api-key"
def test_validate_environment_removes_x_api_key(self):
"""Test that x-api-key header is removed (Azure uses api-key instead)"""
config = AzureAnthropicConfig()
headers = {}
model = "claude-sonnet-4-5"
messages = [{"role": "user", "content": "Hello"}]
optional_params = {}
litellm_params = {"api_key": "test-api-key"}
with patch(
"litellm.llms.azure.common_utils.BaseAzureLLM._base_validate_azure_environment"
) as mock_validate:
mock_validate.return_value = {"api-key": "test-api-key"}
with patch.object(
config, "get_anthropic_headers", return_value={"x-api-key": "should-be-removed"}
):
result = config.validate_environment(
headers=headers,
model=model,
messages=messages,
optional_params=optional_params,
litellm_params=litellm_params,
)
# Verify x-api-key was removed
assert "x-api-key" not in result
assert "api-key" in result
def test_validate_environment_sets_anthropic_version(self):
"""Test that anthropic-version header is set"""
config = AzureAnthropicConfig()
headers = {}
model = "claude-sonnet-4-5"
messages = [{"role": "user", "content": "Hello"}]
optional_params = {}
litellm_params = {"api_key": "test-api-key"}
with patch(
"litellm.llms.azure.common_utils.BaseAzureLLM._base_validate_azure_environment"
) as mock_validate:
mock_validate.return_value = {"api-key": "test-api-key"}
with patch.object(config, "get_anthropic_headers", return_value={}):
result = config.validate_environment(
headers=headers,
model=model,
messages=messages,
optional_params=optional_params,
litellm_params=litellm_params,
)
assert result["anthropic-version"] == "2023-06-01"
def test_validate_environment_preserves_existing_anthropic_version(self):
"""Test that existing anthropic-version header is preserved"""
config = AzureAnthropicConfig()
headers = {"anthropic-version": "2024-01-01"}
model = "claude-sonnet-4-5"
messages = [{"role": "user", "content": "Hello"}]
optional_params = {}
litellm_params = {"api_key": "test-api-key"}
with patch(
"litellm.llms.azure.common_utils.BaseAzureLLM._base_validate_azure_environment"
) as mock_validate:
mock_validate.return_value = {"api-key": "test-api-key", "anthropic-version": "2024-01-01"}
with patch.object(config, "get_anthropic_headers", return_value={"anthropic-version": "2024-01-01"}):
result = config.validate_environment(
headers=headers,
model=model,
messages=messages,
optional_params=optional_params,
litellm_params=litellm_params,
)
assert result["anthropic-version"] == "2024-01-01"
def test_inherits_anthropic_config_methods(self):
"""Test that AzureAnthropicConfig inherits methods from AnthropicConfig"""
config = AzureAnthropicConfig()
# Test that it has AnthropicConfig methods
assert hasattr(config, "get_anthropic_headers")
assert hasattr(config, "is_cache_control_set")
assert hasattr(config, "is_computer_tool_used")
assert hasattr(config, "transform_request")
assert hasattr(config, "transform_response")