diff --git a/litellm/llms/azure_ai/anthropic/messages_transformation.py b/litellm/llms/azure_ai/anthropic/messages_transformation.py index 55818cc07d6..73dc84167ab 100644 --- a/litellm/llms/azure_ai/anthropic/messages_transformation.py +++ b/litellm/llms/azure_ai/anthropic/messages_transformation.py @@ -48,12 +48,7 @@ class AzureAnthropicMessagesConfig(AnthropicMessagesConfig): headers = BaseAzureLLM._base_validate_azure_environment( headers=headers, litellm_params=litellm_params_obj ) - - # Azure Anthropic uses x-api-key header (not api-key) - # Convert api-key to x-api-key if present - if "api-key" in headers and "x-api-key" not in headers: - headers["x-api-key"] = headers.pop("api-key") - + # Set anthropic-version header if "anthropic-version" not in headers: headers["anthropic-version"] = "2023-06-01" diff --git a/litellm/llms/azure_ai/anthropic/transformation.py b/litellm/llms/azure_ai/anthropic/transformation.py index 150ad0a48b2..ebefbd3bf7f 100644 --- a/litellm/llms/azure_ai/anthropic/transformation.py +++ b/litellm/llms/azure_ai/anthropic/transformation.py @@ -55,11 +55,6 @@ class AzureAnthropicConfig(AnthropicConfig): headers = BaseAzureLLM._base_validate_azure_environment( headers=headers, litellm_params=litellm_params_obj ) - - # Azure Anthropic uses x-api-key header (not api-key) - # Convert api-key to x-api-key if present - if "api-key" in headers and "x-api-key" not in headers: - headers["x-api-key"] = headers.pop("api-key") # Get tools and other anthropic-specific setup tools = optional_params.get("tools") diff --git a/litellm/proxy/pass_through_endpoints/llm_provider_handlers/anthropic_passthrough_logging_handler.py b/litellm/proxy/pass_through_endpoints/llm_provider_handlers/anthropic_passthrough_logging_handler.py index 9b6e22b8196..b990f4ca6e9 100644 --- a/litellm/proxy/pass_through_endpoints/llm_provider_handlers/anthropic_passthrough_logging_handler.py +++ b/litellm/proxy/pass_through_endpoints/llm_provider_handlers/anthropic_passthrough_logging_handler.py @@ -96,9 +96,20 @@ class AnthropicPassthroughLoggingHandler: handles streaming and non-streaming responses """ try: + # Get custom_llm_provider from logging object if available (e.g., azure_ai for Azure Anthropic) + custom_llm_provider = logging_obj.model_call_details.get( + "custom_llm_provider" + ) + + # Prepend custom_llm_provider to model if not already present + model_for_cost = model + if custom_llm_provider and not model.startswith(f"{custom_llm_provider}/"): + model_for_cost = f"{custom_llm_provider}/{model}" + response_cost = litellm.completion_cost( completion_response=litellm_model_response, - model=model, + model=model_for_cost, + custom_llm_provider=custom_llm_provider, ) kwargs["response_cost"] = response_cost @@ -157,19 +168,14 @@ class AnthropicPassthroughLoggingHandler: """ model = request_body.get("model", "") - # Dheck if it's available in the logging object + # Check if it's available in the logging object if ( not model and hasattr(litellm_logging_obj, "model_call_details") and litellm_logging_obj.model_call_details.get("model") ): model = cast(str, litellm_logging_obj.model_call_details.get("model")) - custom_llm_provider = litellm_logging_obj.model_call_details.get( - "custom_llm_provider" - ) - if custom_llm_provider and not model.startswith(custom_llm_provider): - model = f"{custom_llm_provider}/{model}" complete_streaming_response = ( AnthropicPassthroughLoggingHandler._build_complete_streaming_response( all_chunks=all_chunks, diff --git a/litellm/utils.py b/litellm/utils.py index 00fc61b2288..9279703af1a 100644 --- a/litellm/utils.py +++ b/litellm/utils.py @@ -7391,12 +7391,19 @@ class ProviderConfigManager: return BedrockModelInfo.get_bedrock_provider_config_for_messages_api(model) elif litellm.LlmProviders.VERTEX_AI == provider: - if "claude" in model: + if "claude" in model.lower(): from litellm.llms.vertex_ai.vertex_ai_partner_models.anthropic.experimental_pass_through.transformation import ( VertexAIPartnerModelsAnthropicMessagesConfig, ) return VertexAIPartnerModelsAnthropicMessagesConfig() + elif litellm.LlmProviders.AZURE_AI == provider: + if "claude" in model.lower(): + from litellm.llms.azure_ai.anthropic.messages_transformation import ( + AzureAnthropicMessagesConfig, + ) + + return AzureAnthropicMessagesConfig() return None @staticmethod diff --git a/tests/test_litellm/llms/azure_ai/claude/test_azure_anthropic_messages_transformation.py b/tests/test_litellm/llms/azure_ai/claude/test_azure_anthropic_messages_transformation.py index ae8c35b2679..d78a638fd89 100644 --- a/tests/test_litellm/llms/azure_ai/claude/test_azure_anthropic_messages_transformation.py +++ b/tests/test_litellm/llms/azure_ai/claude/test_azure_anthropic_messages_transformation.py @@ -55,12 +55,11 @@ class TestAzureAnthropicMessagesConfig: assert isinstance(call_args[1]["litellm_params"], GenericLiteLLMParams) assert call_args[1]["litellm_params"].api_key == "test-api-key" assert "anthropic-version" in result - assert "x-api-key" in result - assert result["x-api-key"] == "test-api-key" - assert "api-key" not in result + # api-key header is preserved as-is (no conversion to x-api-key) + assert "api-key" in result - def test_validate_anthropic_messages_environment_converts_api_key_to_x_api_key(self): - """Test that api-key header is converted to x-api-key""" + def test_validate_anthropic_messages_environment_preserves_api_key_header(self): + """Test that api-key header is preserved as-is (Azure handles the header internally)""" config = AzureAnthropicMessagesConfig() headers = {} model = "claude-sonnet-4-5" @@ -80,10 +79,9 @@ class TestAzureAnthropicMessagesConfig: litellm_params=litellm_params, ) - # Verify api-key was converted to x-api-key - assert "x-api-key" in result - assert result["x-api-key"] == "test-api-key" - assert "api-key" not in result + # Verify api-key header is preserved as-is + assert "api-key" in result + assert result["api-key"] == "test-api-key" def test_validate_anthropic_messages_environment_sets_headers(self): """Test that required headers are set""" @@ -110,7 +108,8 @@ class TestAzureAnthropicMessagesConfig: assert result["anthropic-version"] == "2023-06-01" assert "content-type" in result assert result["content-type"] == "application/json" - assert "x-api-key" in result + # api-key header is preserved as-is + assert "api-key" in result def test_get_complete_url_with_base_url(self): """Test get_complete_url with base URL""" @@ -239,3 +238,47 @@ class TestAzureAnthropicMessagesConfig: assert "tools" in params assert "tool_choice" in params + +class TestProviderConfigManagerAzureAnthropicMessages: + """Test ProviderConfigManager returns correct config for Azure AI Anthropic Messages API""" + + def test_get_provider_anthropic_messages_config_returns_azure_config(self): + """Test that ProviderConfigManager returns AzureAnthropicMessagesConfig for azure_ai provider with claude model""" + import litellm + from litellm.utils import ProviderConfigManager + + config = ProviderConfigManager.get_provider_anthropic_messages_config( + model="claude-sonnet-4-5_gb_20250929", + provider=litellm.LlmProviders.AZURE_AI, + ) + + assert config is not None + assert isinstance(config, AzureAnthropicMessagesConfig) + + def test_get_provider_anthropic_messages_config_case_insensitive_model_name(self): + """Test that model name check is case insensitive""" + import litellm + from litellm.utils import ProviderConfigManager + + # Test with uppercase CLAUDE + config = ProviderConfigManager.get_provider_anthropic_messages_config( + model="CLAUDE-SONNET-4-5", + provider=litellm.LlmProviders.AZURE_AI, + ) + + assert config is not None + assert isinstance(config, AzureAnthropicMessagesConfig) + + def test_get_provider_anthropic_messages_config_returns_none_for_non_claude_model( + self, + ): + """Test that ProviderConfigManager returns None for non-claude model on azure_ai""" + import litellm + from litellm.utils import ProviderConfigManager + + config = ProviderConfigManager.get_provider_anthropic_messages_config( + model="gpt-4o", + provider=litellm.LlmProviders.AZURE_AI, + ) + + assert config is None diff --git a/tests/test_litellm/llms/azure_ai/claude/test_azure_anthropic_transformation.py b/tests/test_litellm/llms/azure_ai/claude/test_azure_anthropic_transformation.py index f2c75cf1a61..1a20806243f 100644 --- a/tests/test_litellm/llms/azure_ai/claude/test_azure_anthropic_transformation.py +++ b/tests/test_litellm/llms/azure_ai/claude/test_azure_anthropic_transformation.py @@ -103,8 +103,8 @@ class TestAzureAnthropicConfig: call_args = mock_validate.call_args assert call_args[1]["litellm_params"].api_key == "provided-api-key" - def test_validate_environment_converts_api_key_to_x_api_key(self): - """Test that api-key header is converted to x-api-key (Azure Anthropic uses x-api-key)""" + def test_validate_environment_preserves_api_key_header(self): + """Test that api-key header is preserved as-is (Azure handles the header internally)""" config = AzureAnthropicConfig() headers = {} model = "claude-sonnet-4-5" @@ -127,10 +127,9 @@ class TestAzureAnthropicConfig: litellm_params=litellm_params, ) - # Verify api-key was converted to x-api-key - assert "x-api-key" in result - assert result["x-api-key"] == "test-api-key" - assert "api-key" not in result + # Verify api-key header is preserved as-is + assert "api-key" in result + assert result["api-key"] == "test-api-key" def test_validate_environment_sets_anthropic_version(self): """Test that anthropic-version header is set""" diff --git a/tests/test_litellm/proxy/pass_through_endpoints/llm_provider_handlers/test_anthropic_passthrough_logging_handler.py b/tests/test_litellm/proxy/pass_through_endpoints/llm_provider_handlers/test_anthropic_passthrough_logging_handler.py index e54e537eed1..59ab5068fa1 100644 --- a/tests/test_litellm/proxy/pass_through_endpoints/llm_provider_handlers/test_anthropic_passthrough_logging_handler.py +++ b/tests/test_litellm/proxy/pass_through_endpoints/llm_provider_handlers/test_anthropic_passthrough_logging_handler.py @@ -151,4 +151,132 @@ class TestAnthropicLoggingHandlerModelFallback: if not model and hasattr(logging_obj, 'model_call_details') and logging_obj.model_call_details.get('model'): model = logging_obj.model_call_details.get('model') - assert model == "" # Should remain empty \ No newline at end of file + assert model == "" # Should remain empty + + +class TestAzureAnthropicCostCalculation: + """Test the custom_llm_provider cost calculation logic for Azure AI Anthropic.""" + + def _create_mock_logging_obj( + self, model: str = None, custom_llm_provider: str = None + ) -> LiteLLMLoggingObj: + """Create a mock logging object with optional model and custom_llm_provider""" + mock_logging_obj = MagicMock() + mock_model_call_details = {} + if model: + mock_model_call_details["model"] = model + if custom_llm_provider: + mock_model_call_details["custom_llm_provider"] = custom_llm_provider + mock_logging_obj.model_call_details = mock_model_call_details + mock_logging_obj.litellm_call_id = "test-call-id" + return mock_logging_obj + + @patch("litellm.completion_cost") + def test_cost_calculation_with_azure_ai_custom_llm_provider( + self, mock_completion_cost + ): + """Test that custom_llm_provider is passed to completion_cost for Azure AI Anthropic""" + from litellm.types.utils import ModelResponse + from datetime import datetime + + mock_completion_cost.return_value = 0.001 + + logging_obj = self._create_mock_logging_obj( + model="claude-sonnet-4-5_gb_20250929", custom_llm_provider="azure_ai" + ) + + mock_response = MagicMock(spec=ModelResponse) + mock_response.id = "test-id" + mock_response.model = "claude-sonnet-4-5_gb_20250929" + + kwargs = {} + start_time = datetime.now() + end_time = datetime.now() + + AnthropicPassthroughLoggingHandler._create_anthropic_response_logging_payload( + litellm_model_response=mock_response, + model="claude-sonnet-4-5_gb_20250929", + kwargs=kwargs, + start_time=start_time, + end_time=end_time, + logging_obj=logging_obj, + ) + + # Verify completion_cost was called with the correct parameters + mock_completion_cost.assert_called_once() + call_kwargs = mock_completion_cost.call_args[1] + assert call_kwargs["model"] == "azure_ai/claude-sonnet-4-5_gb_20250929" + assert call_kwargs["custom_llm_provider"] == "azure_ai" + + @patch("litellm.completion_cost") + def test_cost_calculation_without_custom_llm_provider(self, mock_completion_cost): + """Test that cost calculation works without custom_llm_provider (standard Anthropic)""" + from litellm.types.utils import ModelResponse + from datetime import datetime + + mock_completion_cost.return_value = 0.001 + + # No custom_llm_provider in model_call_details + logging_obj = self._create_mock_logging_obj(model="claude-3-sonnet-20240229") + + mock_response = MagicMock(spec=ModelResponse) + mock_response.id = "test-id" + mock_response.model = "claude-3-sonnet-20240229" + + kwargs = {} + start_time = datetime.now() + end_time = datetime.now() + + AnthropicPassthroughLoggingHandler._create_anthropic_response_logging_payload( + litellm_model_response=mock_response, + model="claude-3-sonnet-20240229", + kwargs=kwargs, + start_time=start_time, + end_time=end_time, + logging_obj=logging_obj, + ) + + # Verify completion_cost was called without provider prefix + mock_completion_cost.assert_called_once() + call_kwargs = mock_completion_cost.call_args[1] + assert call_kwargs["model"] == "claude-3-sonnet-20240229" + assert call_kwargs["custom_llm_provider"] is None + + @patch("litellm.completion_cost") + def test_cost_calculation_does_not_duplicate_provider_prefix( + self, mock_completion_cost + ): + """Test that provider prefix is not duplicated if already present in model name""" + from litellm.types.utils import ModelResponse + from datetime import datetime + + mock_completion_cost.return_value = 0.001 + + logging_obj = self._create_mock_logging_obj( + model="azure_ai/claude-sonnet-4-5_gb_20250929", + custom_llm_provider="azure_ai", + ) + + mock_response = MagicMock(spec=ModelResponse) + mock_response.id = "test-id" + mock_response.model = "azure_ai/claude-sonnet-4-5_gb_20250929" + + kwargs = {} + start_time = datetime.now() + end_time = datetime.now() + + # Model already has the provider prefix + AnthropicPassthroughLoggingHandler._create_anthropic_response_logging_payload( + litellm_model_response=mock_response, + model="azure_ai/claude-sonnet-4-5_gb_20250929", + kwargs=kwargs, + start_time=start_time, + end_time=end_time, + logging_obj=logging_obj, + ) + + # Verify provider prefix was not duplicated + mock_completion_cost.assert_called_once() + call_kwargs = mock_completion_cost.call_args[1] + assert call_kwargs["model"] == "azure_ai/claude-sonnet-4-5_gb_20250929" + assert call_kwargs["custom_llm_provider"] == "azure_ai" \ No newline at end of file