fix Azure AI Anthropic api-key header and passthrough cost calculation (#17656)

* refactor: remove api-key conversion logic for Azure Anthropic

Co-authored-by: Erdem Halil <erdemhalil@users.noreply.github.com>

* fix(passthrough): pass custom_llm_provider to completion_cost for Azure AI Anthropic

The passthrough logging for Anthropic was failing when using Azure AI Anthropic
because the completion_cost function was not receiving the custom_llm_provider
parameter, causing it to fail with "LLM Provider NOT provided" error.

This fix:
- Retrieves custom_llm_provider from logging_obj.model_call_details
- Prepends provider prefix to model name for cost calculation
- Passes both formatted model and custom_llm_provider to completion_cost
- Centralizes provider prefix logic in _create_anthropic_response_logging_payload

This ensures cost calculation works correctly for Azure AI Anthropic requests
with models like azure_ai/claude-sonnet-4-5_gb_20250929.

Co-authored-by: Erdem Halil <erdemhalil@users.noreply.github.com>

* test: add unit tests for Azure AI Anthropic fixes

- Add tests for custom_llm_provider cost calculation in passthrough logging
- Add tests for ProviderConfigManager returning AzureAnthropicMessagesConfig
- Update existing tests to reflect removal of api-key to x-api-key conversion

Co-authored-by: Erdem Halil <erdemhalil@users.noreply.github.com>

---------

Co-authored-by: Erdem Halil <erdemhalil@users.noreply.github.com>
This commit is contained in:
Emil Svensson 2025-12-09 03:50:26 +01:00 • committed by GitHub
parent c9f88bf2c7
commit 61e737e361
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 209 additions and 36 deletions

View file

@ -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"

View file

@ -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")

View file

@ -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,

View file

@ -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

View file

@ -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

View file

@ -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"""

View file

@ -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
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"