mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-09 03:18:44 +00:00
Added support for azure anthopic models via chat completion
This commit is contained in:
parent
afc9a763cb
commit
29057ba6ad
14 changed files with 1318 additions and 9 deletions
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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"/>
|
||||
|
|
|
|||
378
docs/my-website/docs/providers/azure/azure_anthropic.md
Normal file
378
docs/my-website/docs/providers/azure/azure_anthropic.md
Normal 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
|
||||
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
8
litellm/llms/azure/anthropic/__init__.py
Normal file
8
litellm/llms/azure/anthropic/__init__.py
Normal 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"]
|
||||
|
||||
236
litellm/llms/azure/anthropic/handler.py
Normal file
236
litellm/llms/azure/anthropic/handler.py
Normal 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,
|
||||
)
|
||||
|
||||
96
litellm/llms/azure/anthropic/transformation.py
Normal file
96
litellm/llms/azure/anthropic/transformation.py
Normal 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
|
||||
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
@ -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"]
|
||||
|
||||
|
|
@ -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")
|
||||
|
||||
Loading…
Add table
Reference in a new issue