mirror of
https://github.com/tiennm99/litellm.git
synced 2026-08-09 00:26:01 +00:00
Merge pull request #17202 from BerriAI/litellm_azure_ai_anthropic_support
(Bug)Migrate Anthropic provider to azure ai
This commit is contained in:
@@ -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
@@ -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
|
||||
|
||||
+5
-14
@@ -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),
|
||||
)
|
||||
+27
-1
@@ -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
@@ -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,
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"]
|
||||
|
||||
+14
-4
@@ -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
|
||||
|
||||
+1
-1
@@ -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"
|
||||
|
||||
+3
-3
@@ -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"""
|
||||
Reference in New Issue
Block a user