diff --git a/docs/my-website/blog/anthropic_opus_4_5_and_advanced_features/index.md b/docs/my-website/blog/anthropic_opus_4_5_and_advanced_features/index.md index b545e93618..997f5a0b2d 100644 --- a/docs/my-website/blog/anthropic_opus_4_5_and_advanced_features/index.md +++ b/docs/my-website/blog/anthropic_opus_4_5_and_advanced_features/index.md @@ -327,6 +327,81 @@ curl --location 'http://0.0.0.0:4000/v1/messages' \ +## 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://.services.ai.azure.com/anthropic` (LiteLLM appends `/v1/messages` automatically) and authenticate with `AZURE_AI_API_KEY` or an Azure AD token. + + + + +```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) +``` + + + + +**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 + }' +``` + + + ## Tool Search {#tool-search} diff --git a/docs/my-website/docs/providers/azure/azure_anthropic.md b/docs/my-website/docs/providers/azure/azure_anthropic.md index 771912646b..4c722b3039 100644 --- a/docs/my-website/docs/providers/azure/azure_anthropic.md +++ b/docs/my-website/docs/providers/azure/azure_anthropic.md @@ -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://.services.ai.azure.com/anthropic/v1/messages` | | Supported Endpoints | `/chat/completions`, `/anthropic/v1/messages`| @@ -68,7 +68,7 @@ os.environ["AZURE_API_BASE"] = "https://.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://.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://.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://.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://.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://.services.ai.azure.com/anthropic", # ... ) diff --git a/litellm/__init__.py b/litellm/__init__.py index bbe17c97b7..dfe35e3325 100644 --- a/litellm/__init__.py +++ b/litellm/__init__.py @@ -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 diff --git a/litellm/litellm_core_utils/get_llm_provider_logic.py b/litellm/litellm_core_utils/get_llm_provider_logic.py index 3bb1e4afb9..4d29a74ddb 100644 --- a/litellm/litellm_core_utils/get_llm_provider_logic.py +++ b/litellm/litellm_core_utils/get_llm_provider_logic.py @@ -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 diff --git a/litellm/llms/azure/anthropic/__init__.py b/litellm/llms/azure_ai/anthropic/__init__.py similarity index 100% rename from litellm/llms/azure/anthropic/__init__.py rename to litellm/llms/azure_ai/anthropic/__init__.py diff --git a/litellm/llms/azure/anthropic/handler.py b/litellm/llms/azure_ai/anthropic/handler.py similarity index 93% rename from litellm/llms/azure/anthropic/handler.py rename to litellm/llms/azure_ai/anthropic/handler.py index cf4765190c..fe4524fd5b 100644 --- a/litellm/llms/azure/anthropic/handler.py +++ b/litellm/llms/azure_ai/anthropic/handler.py @@ -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), ) diff --git a/litellm/llms/azure/anthropic/messages_transformation.py b/litellm/llms/azure_ai/anthropic/messages_transformation.py similarity index 100% rename from litellm/llms/azure/anthropic/messages_transformation.py rename to litellm/llms/azure_ai/anthropic/messages_transformation.py diff --git a/litellm/llms/azure/anthropic/transformation.py b/litellm/llms/azure_ai/anthropic/transformation.py similarity index 82% rename from litellm/llms/azure/anthropic/transformation.py rename to litellm/llms/azure_ai/anthropic/transformation.py index 9bc4f13056..150ad0a48b 100644 --- a/litellm/llms/azure/anthropic/transformation.py +++ b/litellm/llms/azure_ai/anthropic/transformation.py @@ -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 + diff --git a/litellm/main.py b/litellm/main.py index afc7a36fb4..1936101ef9 100644 --- a/litellm/main.py +++ b/litellm/main.py @@ -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://.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://.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() diff --git a/litellm/model_prices_and_context_window_backup.json b/litellm/model_prices_and_context_window_backup.json index 8ef4d66381..f4f6b94fd1 100644 --- a/litellm/model_prices_and_context_window_backup.json +++ b/litellm/model_prices_and_context_window_backup.json @@ -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, diff --git a/litellm/types/utils.py b/litellm/types/utils.py index 37a476b33f..2456c87044 100644 --- a/litellm/types/utils.py +++ b/litellm/types/utils.py @@ -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" diff --git a/litellm/utils.py b/litellm/utils.py index 18c2e781be..b3b114071b 100644 --- a/litellm/utils.py +++ b/litellm/utils.py @@ -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 diff --git a/model_prices_and_context_window.json b/model_prices_and_context_window.json index 8ef4d66381..f4f6b94fd1 100644 --- a/model_prices_and_context_window.json +++ b/model_prices_and_context_window.json @@ -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, diff --git a/tests/test_litellm/llms/azure/anthropic/test_azure_anthropic_provider_config.py b/tests/test_litellm/llms/azure/anthropic/test_azure_anthropic_provider_config.py deleted file mode 100644 index db118154ee..0000000000 --- a/tests/test_litellm/llms/azure/anthropic/test_azure_anthropic_provider_config.py +++ /dev/null @@ -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) - diff --git a/tests/test_litellm/llms/azure/anthropic/test_azure_anthropic_provider_routing.py b/tests/test_litellm/llms/azure/anthropic/test_azure_anthropic_provider_routing.py deleted file mode 100644 index a7aa298317..0000000000 --- a/tests/test_litellm/llms/azure/anthropic/test_azure_anthropic_provider_routing.py +++ /dev/null @@ -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"] - diff --git a/tests/test_litellm/llms/azure/anthropic/test_azure_anthropic_handler.py b/tests/test_litellm/llms/azure_ai/claude/test_azure_anthropic_handler.py similarity index 92% rename from tests/test_litellm/llms/azure/anthropic/test_azure_anthropic_handler.py rename to tests/test_litellm/llms/azure_ai/claude/test_azure_anthropic_handler.py index bb5d1f9933..ddfad420d0 100644 --- a/tests/test_litellm/llms/azure/anthropic/test_azure_anthropic_handler.py +++ b/tests/test_litellm/llms/azure_ai/claude/test_azure_anthropic_handler.py @@ -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 diff --git a/tests/test_litellm/llms/azure/anthropic/test_azure_anthropic_messages_transformation.py b/tests/test_litellm/llms/azure_ai/claude/test_azure_anthropic_messages_transformation.py similarity index 99% rename from tests/test_litellm/llms/azure/anthropic/test_azure_anthropic_messages_transformation.py rename to tests/test_litellm/llms/azure_ai/claude/test_azure_anthropic_messages_transformation.py index abed1a7852..ae8c35b267 100644 --- a/tests/test_litellm/llms/azure/anthropic/test_azure_anthropic_messages_transformation.py +++ b/tests/test_litellm/llms/azure_ai/claude/test_azure_anthropic_messages_transformation.py @@ -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 diff --git a/tests/test_litellm/llms/azure_ai/claude/test_azure_anthropic_provider_routing.py b/tests/test_litellm/llms/azure_ai/claude/test_azure_anthropic_provider_routing.py new file mode 100644 index 0000000000..fdc2daf09f --- /dev/null +++ b/tests/test_litellm/llms/azure_ai/claude/test_azure_anthropic_provider_routing.py @@ -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" + diff --git a/tests/test_litellm/llms/azure/anthropic/test_azure_anthropic_transformation.py b/tests/test_litellm/llms/azure_ai/claude/test_azure_anthropic_transformation.py similarity index 97% rename from tests/test_litellm/llms/azure/anthropic/test_azure_anthropic_transformation.py rename to tests/test_litellm/llms/azure_ai/claude/test_azure_anthropic_transformation.py index f26831e919..f2c75cf1a6 100644 --- a/tests/test_litellm/llms/azure/anthropic/test_azure_anthropic_transformation.py +++ b/tests/test_litellm/llms/azure_ai/claude/test_azure_anthropic_transformation.py @@ -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"""