mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-10 03:28:53 +00:00
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:
parent
c9f88bf2c7
commit
61e737e361
7 changed files with 209 additions and 36 deletions
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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"""
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
Loading…
Add table
Reference in a new issue