Merge pull request #17202 from BerriAI/litellm_azure_ai_anthropic_support

(Bug)Migrate Anthropic provider to azure ai
This commit is contained in:
Sameer Kankute
2025-11-28 08:14:14 +05:30
committed by GitHub
19 changed files with 304 additions and 312 deletions
@@ -327,6 +327,81 @@ curl --location 'http://0.0.0.0:4000/v1/messages' \
</TabItem>
</Tabs>
## Usage - Azure Anthropic (Azure Foundry Claude)
LiteLLM funnels Azure Claude deployments through the `azure_ai/` provider so Claude Opus models on Azure Foundry keep working with Tool Search, Effort, streaming, and the rest of the advanced feature set. Point `AZURE_AI_API_BASE` to `https://<resource>.services.ai.azure.com/anthropic` (LiteLLM appends `/v1/messages` automatically) and authenticate with `AZURE_AI_API_KEY` or an Azure AD token.
<Tabs>
<TabItem value="sdk" label="LiteLLM Python SDK">
```python
import os
from litellm import completion
# Configure Azure credentials
os.environ["AZURE_AI_API_KEY"] = "your-azure-ai-api-key"
os.environ["AZURE_AI_API_BASE"] = "https://my-resource.services.ai.azure.com/anthropic"
response = completion(
model="azure_ai/claude-opus-4-1",
messages=[{"role": "user", "content": "Explain how Azure Anthropic hosts Claude Opus differently from the public Anthropic API."}],
max_tokens=1200,
temperature=0.7,
stream=True,
)
for chunk in response:
if chunk.choices[0].delta.content:
print(chunk.choices[0].delta.content, end="", flush=True)
```
</TabItem>
<TabItem value="proxy" label="LiteLLM Proxy">
**1. Set environment variables**
```bash
export AZURE_AI_API_KEY="your-azure-ai-api-key"
export AZURE_AI_API_BASE="https://my-resource.services.ai.azure.com/anthropic"
```
**2. Configure the proxy**
```yaml
model_list:
- model_name: claude-4-azure
litellm_params:
model: azure_ai/claude-opus-4-1
api_key: os.environ/AZURE_AI_API_KEY
api_base: os.environ/AZURE_AI_API_BASE
```
**3. Start LiteLLM**
```bash
litellm --config /path/to/config.yaml
```
**4. Test the Azure Claude route**
```bash
curl --location 'http://0.0.0.0:4000/chat/completions' \
--header 'Content-Type: application/json' \
--header 'Authorization: Bearer $LITELLM_KEY' \
--data '{
"model": "claude-4-azure",
"messages": [
{
"role": "user",
"content": "How do I use Claude Opus 4 via Azure Anthropic in LiteLLM?"
}
],
"max_tokens": 1024
}'
```
</TabItem>
</Tabs>
## Tool Search {#tool-search}
@@ -16,7 +16,7 @@ Azure Foundry supports the following Claude models:
| 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 Route on LiteLLM | `azure_ai/` (add this prefix to Claude model names - e.g. `azure_ai/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`|
@@ -68,7 +68,7 @@ os.environ["AZURE_API_BASE"] = "https://<resource-name>.services.ai.azure.com/an
# Make a completion request
response = completion(
model="azure/claude-sonnet-4-5",
model="azure_ai/claude-sonnet-4-5",
messages=[
{"role": "user", "content": "What are 3 things to visit in Seattle?"}
],
@@ -85,7 +85,7 @@ print(response)
import litellm
response = litellm.completion(
model="azure/claude-sonnet-4-5",
model="azure_ai/claude-sonnet-4-5",
api_base="https://<resource-name>.services.ai.azure.com/anthropic",
api_key="your-azure-api-key",
messages=[
@@ -101,7 +101,7 @@ response = litellm.completion(
import litellm
response = litellm.completion(
model="azure/claude-sonnet-4-5",
model="azure_ai/claude-sonnet-4-5",
api_base="https://<resource-name>.services.ai.azure.com/anthropic",
azure_ad_token="your-azure-ad-token",
messages=[
@@ -117,7 +117,7 @@ response = litellm.completion(
from litellm import completion
response = completion(
model="azure/claude-sonnet-4-5",
model="azure_ai/claude-sonnet-4-5",
messages=[
{"role": "user", "content": "Write a short story"}
],
@@ -136,7 +136,7 @@ for chunk in response:
from litellm import completion
response = completion(
model="azure/claude-sonnet-4-5",
model="azure_ai/claude-sonnet-4-5",
messages=[
{"role": "user", "content": "What's the weather in Seattle?"}
],
@@ -181,7 +181,7 @@ export AZURE_API_BASE="https://<resource-name>.services.ai.azure.com/anthropic"
model_list:
- model_name: claude-sonnet-4-5
litellm_params:
model: azure/claude-sonnet-4-5
model: azure_ai/claude-sonnet-4-5
api_base: https://<resource-name>.services.ai.azure.com/anthropic
api_key: os.environ/AZURE_API_KEY
```
@@ -331,7 +331,7 @@ os.environ["AZURE_API_BASE"] = "https://my-resource.services.ai.azure.com/anthro
# Make a request
response = completion(
model="azure/claude-sonnet-4-5",
model="azure_ai/claude-sonnet-4-5",
messages=[
{"role": "system", "content": "You are a helpful assistant."},
{"role": "user", "content": "Explain quantum computing in simple terms."}
@@ -358,7 +358,7 @@ Or pass it directly:
```python
response = completion(
model="azure/claude-sonnet-4-5",
model="azure_ai/claude-sonnet-4-5",
api_base="https://<resource-name>.services.ai.azure.com/anthropic",
# ...
)
+1 -1
View File
@@ -1125,7 +1125,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.azure_ai.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,17 +22,16 @@ def _is_non_openai_azure_model(model: str) -> bool:
return False
def _is_azure_anthropic_model(model: str) -> Optional[str]:
def _is_azure_claude_model(model: str) -> bool:
"""
Check if a model name contains 'claude' (case-insensitive).
Used to detect Claude models that need Anthropic-specific handling.
"""
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
model_lower = model.lower()
return "claude" in model_lower or model_lower.startswith("claude")
except Exception:
pass
return None
return False
def handle_cohere_chat_model_custom_llm_provider(
@@ -136,11 +135,6 @@ 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
@@ -7,7 +7,6 @@ from typing import TYPE_CHECKING, Callable, Union
import httpx
import litellm
from litellm.llms.anthropic.chat.handler import AnthropicChatCompletion
from litellm.llms.custom_httpx.http_handler import (
AsyncHTTPHandler,
@@ -55,7 +54,6 @@ class AzureAnthropicChatCompletion(AnthropicChatCompletion):
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)
@@ -64,8 +62,10 @@ class AzureAnthropicChatCompletion(AnthropicChatCompletion):
_is_function_call = False
messages = copy.deepcopy(messages)
# Use AzureAnthropicConfig instead of AnthropicConfig
headers = AzureAnthropicConfig().validate_environment(
# Use AzureAnthropicConfig for both azure_anthropic and azure_ai Claude models
config = AzureAnthropicConfig()
headers = config.validate_environment(
api_key=api_key,
headers=headers,
model=model,
@@ -74,15 +74,6 @@ class AzureAnthropicChatCompletion(AnthropicChatCompletion):
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,
@@ -183,7 +174,7 @@ class AzureAnthropicChatCompletion(AnthropicChatCompletion):
return CustomStreamWrapper(
completion_stream=completion_stream,
model=model,
custom_llm_provider="azure_anthropic",
custom_llm_provider="azure_ai",
logging_obj=logging_obj,
_response_headers=process_anthropic_headers(response_headers),
)
@@ -21,7 +21,7 @@ class AzureAnthropicConfig(AnthropicConfig):
@property
def custom_llm_provider(self) -> Optional[str]:
return "azure_anthropic"
return "azure_ai"
def validate_environment(
self,
@@ -94,3 +94,29 @@ class AzureAnthropicConfig(AnthropicConfig):
return headers
def transform_request(
self,
model: str,
messages: List[AllMessageValues],
optional_params: dict,
litellm_params: dict,
headers: dict,
) -> dict:
"""
Transform request using parent AnthropicConfig, then remove extra_body if present.
Azure Anthropic doesn't support extra_body parameter.
"""
# Call parent transform_request
data = super().transform_request(
model=model,
messages=messages,
optional_params=optional_params,
litellm_params=litellm_params,
headers=headers,
)
# Remove extra_body if present (Azure Anthropic doesn't support it)
data.pop("extra_body", None)
return data
+99 -109
View File
@@ -58,9 +58,11 @@ from litellm import ( # type: ignore
get_litellm_params,
get_optional_params,
)
# Logging is imported lazily when needed to avoid loading litellm_logging at import time
if TYPE_CHECKING:
from litellm.litellm_core_utils.litellm_logging import Logging
from litellm.constants import (
DEFAULT_MOCK_RESPONSE_COMPLETION_TOKEN_COUNT,
DEFAULT_MOCK_RESPONSE_PROMPT_TOKEN_COUNT,
@@ -107,7 +109,6 @@ from litellm.utils import (
ProviderConfigManager,
Usage,
_get_model_info_helper,
get_requester_metadata,
add_provider_specific_params_to_optional_params,
async_mock_completion_streaming_obj,
convert_to_model_response_object,
@@ -120,6 +121,7 @@ from litellm.utils import (
get_optional_params_embeddings,
get_optional_params_image_gen,
get_optional_params_transcription,
get_requester_metadata,
get_secret,
get_standard_openai_params,
mock_completion_streaming_obj,
@@ -154,11 +156,11 @@ 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
from .llms.azure.completion.handler import AzureTextCompletion
from .llms.azure_ai.anthropic.handler import AzureAnthropicChatCompletion
from .llms.azure_ai.embed import AzureAIEmbedding
from .llms.bedrock.chat import BedrockConverseLLM, BedrockLLM
from .llms.bedrock.embed.embedding import BedrockEmbedding
@@ -1686,57 +1688,109 @@ def completion( # type: ignore # noqa: PLR0915
elif custom_llm_provider == "azure_ai":
from litellm.llms.azure_ai.common_utils import AzureFoundryModelInfo
api_base = AzureFoundryModelInfo.get_api_base(api_base)
# set API KEY
api_key = AzureFoundryModelInfo.get_api_key(api_key)
headers = headers or litellm.headers
if extra_headers is not None:
optional_params["extra_headers"] = extra_headers
## FOR COHERE
if "command-r" in model: # make sure tool call in messages are str
messages = stringify_json_tool_call_content(messages=messages)
## COMPLETION CALL
try:
response = base_llm_http_handler.completion(
# Check if this is a Claude model - route to Azure Anthropic handler
model_lower = model.lower()
if "claude" in model_lower:
# Use Azure Anthropic handler for Claude models
api_base = AzureFoundryModelInfo.get_api_base(api_base)
if api_base is None:
raise ValueError(
"Azure Anthropic requests require an api_base. "
"Set `api_base` or the AZURE_AI_API_BASE env var."
)
api_key = AzureFoundryModelInfo.get_api_key(api_key)
# Ensure the URL ends with /v1/messages for Anthropic
if api_base:
api_base = api_base.rstrip("/")
if not api_base.endswith("/v1/messages"):
if "/anthropic" in api_base:
parts = api_base.split("/anthropic", 1)
api_base = parts[0] + "/anthropic"
else:
api_base = api_base + "/anthropic"
api_base = api_base + "/v1/messages"
response = azure_anthropic_chat_completions.completion(
model=model,
messages=messages,
headers=headers,
model_response=model_response,
api_key=api_key,
api_base=api_base,
acompletion=acompletion,
logging_obj=logging,
custom_prompt_dict=litellm.custom_prompt_dict,
model_response=model_response,
print_verbose=print_verbose,
optional_params=optional_params,
litellm_params=litellm_params,
shared_session=shared_session,
timeout=timeout, # type: ignore
client=client, # pass AsyncOpenAI, OpenAI client
custom_llm_provider=custom_llm_provider,
logger_fn=logger_fn,
encoding=encoding,
stream=stream,
)
except Exception as e:
## LOGGING - log the original exception returned
logging.post_call(
input=messages,
api_key=api_key,
original_response=str(e),
additional_args={"headers": headers},
logging_obj=logging,
headers=headers,
timeout=timeout,
client=client,
custom_llm_provider=custom_llm_provider,
)
raise e
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
else:
# Non-Claude models use standard Azure AI flow
api_base = AzureFoundryModelInfo.get_api_base(api_base)
# set API KEY
api_key = AzureFoundryModelInfo.get_api_key(api_key)
if optional_params.get("stream", False):
## LOGGING
logging.post_call(
input=messages,
api_key=api_key,
original_response=response,
additional_args={"headers": headers},
)
headers = headers or litellm.headers
if extra_headers is not None:
optional_params["extra_headers"] = extra_headers
## FOR COHERE
if "command-r" in model: # make sure tool call in messages are str
messages = stringify_json_tool_call_content(messages=messages)
## COMPLETION CALL
try:
response = base_llm_http_handler.completion(
model=model,
messages=messages,
headers=headers,
model_response=model_response,
api_key=api_key,
api_base=api_base,
acompletion=acompletion,
logging_obj=logging,
optional_params=optional_params,
litellm_params=litellm_params,
shared_session=shared_session,
timeout=timeout, # type: ignore
client=client, # pass AsyncOpenAI, OpenAI client
custom_llm_provider=custom_llm_provider,
encoding=encoding,
stream=stream,
)
except Exception as e:
## LOGGING - log the original exception returned
logging.post_call(
input=messages,
api_key=api_key,
original_response=str(e),
additional_args={"headers": headers},
)
raise e
if optional_params.get("stream", False):
## LOGGING
logging.post_call(
input=messages,
api_key=api_key,
original_response=response,
additional_args={"headers": headers},
)
elif (
custom_llm_provider == "text-completion-openai"
or "ft:babbage-002" in model
@@ -2359,70 +2413,6 @@ 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
api_base = api_base.rstrip("/")
if api_base.endswith("/v1/messages"):
pass
elif api_base.endswith("/anthropic/v1/messages"):
pass
else:
if "/anthropic" in api_base:
parts = api_base.split("/anthropic", 1)
api_base = parts[0] + "/anthropic"
else:
api_base = api_base + "/anthropic"
api_base = api_base + "/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
@@ -6236,9 +6226,9 @@ async def ahealth_check(
"x-ms-region": str,
}
"""
from litellm.litellm_core_utils.health_check_helpers import HealthCheckHelpers
from litellm.litellm_core_utils.cached_imports import get_litellm_logging_class
from litellm.litellm_core_utils.health_check_helpers import HealthCheckHelpers
# Use cached import helper to lazy-load Logging class (only loads when function is called)
Logging = get_litellm_logging_class()
@@ -1151,7 +1151,7 @@
},
"azure/claude-haiku-4-5": {
"input_cost_per_token": 1e-06,
"litellm_provider": "azure_anthropic",
"litellm_provider": "azure_ai",
"max_input_tokens": 200000,
"max_output_tokens": 64000,
"max_tokens": 64000,
@@ -1169,7 +1169,7 @@
},
"azure/claude-opus-4-1": {
"input_cost_per_token": 1.5e-05,
"litellm_provider": "azure_anthropic",
"litellm_provider": "azure_ai",
"max_input_tokens": 200000,
"max_output_tokens": 32000,
"max_tokens": 32000,
@@ -1187,7 +1187,7 @@
},
"azure/claude-sonnet-4-5": {
"input_cost_per_token": 3e-06,
"litellm_provider": "azure_anthropic",
"litellm_provider": "azure_ai",
"max_input_tokens": 200000,
"max_output_tokens": 64000,
"max_tokens": 64000,
-1
View File
@@ -2575,7 +2575,6 @@ 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"
-8
View File
@@ -7151,8 +7151,6 @@ 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:
@@ -7346,12 +7344,6 @@ class ProviderConfigManager:
)
return VertexAIPartnerModelsAnthropicMessagesConfig()
elif litellm.LlmProviders.AZURE_ANTHROPIC == provider:
from litellm.llms.azure.anthropic.messages_transformation import (
AzureAnthropicMessagesConfig,
)
return AzureAnthropicMessagesConfig()
return None
@staticmethod
+3 -3
View File
@@ -1151,7 +1151,7 @@
},
"azure/claude-haiku-4-5": {
"input_cost_per_token": 1e-06,
"litellm_provider": "azure_anthropic",
"litellm_provider": "azure_ai",
"max_input_tokens": 200000,
"max_output_tokens": 64000,
"max_tokens": 64000,
@@ -1169,7 +1169,7 @@
},
"azure/claude-opus-4-1": {
"input_cost_per_token": 1.5e-05,
"litellm_provider": "azure_anthropic",
"litellm_provider": "azure_ai",
"max_input_tokens": 200000,
"max_output_tokens": 32000,
"max_tokens": 32000,
@@ -1187,7 +1187,7 @@
},
"azure/claude-sonnet-4-5": {
"input_cost_per_token": 3e-06,
"litellm_provider": "azure_anthropic",
"litellm_provider": "azure_ai",
"max_input_tokens": 200000,
"max_output_tokens": 64000,
"max_tokens": 64000,
@@ -1,59 +0,0 @@
import os
import sys
sys.path.insert(
0, os.path.abspath(os.path.join(os.path.dirname(__file__), "../../../../.."))
)
from unittest.mock import patch
import pytest
import litellm
from litellm.types.utils import LlmProviders
from litellm.utils import ProviderConfigManager
class TestAzureAnthropicProviderConfig:
def test_get_provider_anthropic_messages_config_returns_azure_config(self):
"""Test that get_provider_anthropic_messages_config returns AzureAnthropicMessagesConfig for azure_anthropic provider"""
from litellm.llms.azure.anthropic.messages_transformation import (
AzureAnthropicMessagesConfig,
)
config = ProviderConfigManager.get_provider_anthropic_messages_config(
model="claude-sonnet-4-5",
provider=LlmProviders.AZURE_ANTHROPIC,
)
assert config is not None
assert isinstance(config, AzureAnthropicMessagesConfig)
def test_get_provider_anthropic_messages_config_returns_anthropic_config_for_anthropic_provider(self):
"""Test that get_provider_anthropic_messages_config returns AnthropicMessagesConfig for anthropic provider"""
from litellm.llms.azure.anthropic.messages_transformation import (
AzureAnthropicMessagesConfig,
)
config = ProviderConfigManager.get_provider_anthropic_messages_config(
model="claude-sonnet-4-5",
provider=LlmProviders.ANTHROPIC,
)
# Should return AnthropicMessagesConfig, not AzureAnthropicMessagesConfig
assert config is not None
assert not isinstance(config, AzureAnthropicMessagesConfig)
assert isinstance(config, litellm.AnthropicMessagesConfig)
def test_get_provider_chat_config_returns_azure_anthropic_config(self):
"""Test that get_provider_chat_config returns AzureAnthropicConfig for azure_anthropic provider"""
from litellm.llms.azure.anthropic.transformation import AzureAnthropicConfig
config = ProviderConfigManager.get_provider_chat_config(
model="claude-sonnet-4-5",
provider=LlmProviders.AZURE_ANTHROPIC,
)
assert config is not None
assert isinstance(config, AzureAnthropicConfig)
@@ -1,82 +0,0 @@
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"]
@@ -10,7 +10,7 @@ from unittest.mock import MagicMock, patch
import pytest
from litellm.llms.azure.anthropic.handler import AzureAnthropicChatCompletion
from litellm.llms.azure_ai.anthropic.handler import AzureAnthropicChatCompletion
from litellm.types.utils import ModelResponse
@@ -24,7 +24,7 @@ class TestAzureAnthropicChatCompletion:
assert hasattr(handler, "acompletion_stream_function")
@patch("litellm.utils.ProviderConfigManager")
@patch("litellm.llms.azure.anthropic.handler.AzureAnthropicConfig")
@patch("litellm.llms.azure_ai.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()
@@ -33,6 +33,7 @@ class TestAzureAnthropicChatCompletion:
mock_config.transform_response.return_value = ModelResponse()
mock_config_instance = MagicMock()
mock_config_instance.validate_environment.return_value = {"x-api-key": "test-api-key", "anthropic-version": "2023-06-01"}
mock_config_instance.transform_request.return_value = {"model": "claude-sonnet-4-5", "messages": []}
mock_azure_config.return_value = mock_config_instance
mock_provider_manager.get_provider_chat_config.return_value = mock_config
@@ -78,7 +79,7 @@ class TestAzureAnthropicChatCompletion:
@patch("litellm.llms.anthropic.chat.handler.make_sync_call")
@patch("litellm.utils.ProviderConfigManager")
@patch("litellm.llms.azure.anthropic.handler.AzureAnthropicConfig")
@patch("litellm.llms.azure_ai.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"""
@@ -91,6 +92,11 @@ class TestAzureAnthropicChatCompletion:
}
mock_config_instance = MagicMock()
mock_config_instance.validate_environment.return_value = {"x-api-key": "test-api-key", "anthropic-version": "2023-06-01"}
mock_config_instance.transform_request.return_value = {
"model": "claude-sonnet-4-5",
"messages": [],
"stream": True,
}
mock_azure_config.return_value = mock_config_instance
mock_provider_manager.get_provider_chat_config.return_value = mock_config
@@ -138,7 +144,7 @@ class TestAzureAnthropicChatCompletion:
@patch("litellm.llms.custom_httpx.http_handler._get_httpx_client")
@patch("litellm.utils.ProviderConfigManager")
@patch("litellm.llms.azure.anthropic.handler.AzureAnthropicConfig")
@patch("litellm.llms.azure_ai.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"""
@@ -152,6 +158,10 @@ class TestAzureAnthropicChatCompletion:
mock_config.transform_response.return_value = mock_response
mock_config_instance = MagicMock()
mock_config_instance.validate_environment.return_value = {"x-api-key": "test-api-key", "anthropic-version": "2023-06-01"}
mock_config_instance.transform_request.return_value = {
"model": "claude-sonnet-4-5",
"messages": [],
}
mock_azure_config.return_value = mock_config_instance
mock_provider_manager.get_provider_chat_config.return_value = mock_config
@@ -9,7 +9,7 @@ from unittest.mock import MagicMock, patch
import pytest
from litellm.llms.azure.anthropic.messages_transformation import (
from litellm.llms.azure_ai.anthropic.messages_transformation import (
AzureAnthropicMessagesConfig,
)
from litellm.types.router import GenericLiteLLMParams
@@ -0,0 +1,56 @@
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_claude_model,
get_llm_provider,
)
class TestAzureAnthropicProviderRouting:
def test_is_azure_claude_model_with_claude(self):
"""Test _is_azure_claude_model detects Claude models"""
# Test various Claude model names
assert _is_azure_claude_model("claude-sonnet-4-5") is True
assert _is_azure_claude_model("claude-opus-4-1") is True
assert _is_azure_claude_model("claude-haiku-4-5") is True
assert _is_azure_claude_model("claude-3-5-sonnet") is True
assert _is_azure_claude_model("claude-3-opus") is True
def test_is_azure_claude_model_case_insensitive(self):
"""Test _is_azure_claude_model is case insensitive"""
assert _is_azure_claude_model("CLAUDE-sonnet-4-5") is True
assert _is_azure_claude_model("Claude-Sonnet-4-5") is True
def test_is_azure_claude_model_with_non_claude(self):
"""Test _is_azure_claude_model returns False for non-Claude models"""
assert _is_azure_claude_model("gpt-4") is False
assert _is_azure_claude_model("gpt-35-turbo") is False
assert _is_azure_claude_model("command-r-plus") is False
def test_is_azure_claude_model_with_invalid_format(self):
"""Test _is_azure_claude_model handles invalid formats"""
assert _is_azure_claude_model("") is False
def test_get_llm_provider_routes_azure_ai_claude_to_azure_ai(self):
"""Test that azure_ai/claude-* models route through azure_ai"""
model, provider, dynamic_api_key, api_base = get_llm_provider(
model="azure_ai/claude-sonnet-4-5"
)
assert provider == "azure_ai"
assert model == "claude-sonnet-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_ai"""
model, provider, dynamic_api_key, api_base = get_llm_provider(
model="azure/gpt-4"
)
# Should be routed to regular azure provider
assert provider == "azure" or provider == "openai"
@@ -9,15 +9,15 @@ from unittest.mock import MagicMock, patch
import pytest
from litellm.llms.azure.anthropic.transformation import AzureAnthropicConfig
from litellm.llms.azure_ai.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'"""
"""Test that custom_llm_provider returns 'azure_ai'"""
config = AzureAnthropicConfig()
assert config.custom_llm_provider == "azure_anthropic"
assert config.custom_llm_provider == "azure_ai"
def test_validate_environment_with_dict_litellm_params(self):
"""Test validate_environment with dict litellm_params"""