refactor(auth): build normalized headers immutably

This commit is contained in:
Yujong Lee 2026-09-04 12:11:37 -07:00
parent 209531ac17
commit 7dc8df7eda
7 changed files with 48 additions and 41 deletions

View file

@ -718,11 +718,11 @@ class BaseAzureLLM(BaseOpenAILLM):
(value for name, value in headers.items() if name.lower() == "api-key" and str(value).strip()),
None,
)
non_auth_headers: Final = {
name: value for name, value in headers.items() if name.lower() not in {"api-key", "authorization"}
}
non_auth_headers: Final = MappingProxyType(
{name: value for name, value in headers.items() if name.lower() not in ("api-key", "authorization")}
)
if forwarded_api_key is not None:
return {**non_auth_headers, "api-key": forwarded_api_key}
return MappingProxyType({**non_auth_headers, "api-key": forwarded_api_key}).copy()
api_key: Final = (
litellm_params.api_key
@ -733,15 +733,15 @@ class BaseAzureLLM(BaseOpenAILLM):
)
if api_key:
return {**non_auth_headers, "api-key": api_key}
return MappingProxyType({**non_auth_headers, "api-key": api_key}).copy()
### Fallback to Azure AD token-based authentication if no API key is available
### Retrieves Azure AD token and adds it to the Authorization header
azure_ad_token: Final = get_azure_ad_token(litellm_params)
if azure_ad_token:
return {**non_auth_headers, "Authorization": f"Bearer {azure_ad_token}"}
return MappingProxyType({**non_auth_headers, "Authorization": f"Bearer {azure_ad_token}"}).copy()
return non_auth_headers
return non_auth_headers.copy()
@staticmethod
def _get_base_azure_url(

View file

@ -2,6 +2,7 @@
Azure Anthropic messages transformation config - extends AnthropicMessagesConfig with Azure authentication
"""
from types import MappingProxyType
from typing import Any, Final
from litellm.llms.anthropic.experimental_pass_through.messages.transformation import (
@ -57,16 +58,18 @@ class AzureAnthropicMessagesConfig(AnthropicMessagesConfig):
None,
)
if azure_api_key is not None:
headers = {
**{
non_auth_headers: Final = MappingProxyType(
{
name: value
for name, value in headers.items()
if name.lower() not in {"api-key", "x-api-key", "authorization"}
},
"x-api-key": azure_api_key,
}
if name.lower() not in ("api-key", "x-api-key", "authorization")
}
)
headers = MappingProxyType({**non_auth_headers, "x-api-key": azure_api_key}).copy()
elif any(name.lower() == "authorization" for name in headers):
headers = {name: value for name, value in headers.items() if name.lower() != "x-api-key"}
headers = MappingProxyType(
{name: value for name, value in headers.items() if name.lower() != "x-api-key"}
).copy()
# Set anthropic-version header
if "anthropic-version" not in headers:

View file

@ -2,6 +2,7 @@
Azure Anthropic transformation config - extends AnthropicConfig with Azure authentication
"""
from types import MappingProxyType
from typing import Final
from litellm.llms.anthropic.chat.transformation import AnthropicConfig
@ -80,14 +81,14 @@ class AzureAnthropicConfig(AnthropicConfig):
None,
)
if azure_api_key is not None:
headers = {
**{
non_auth_headers: Final = MappingProxyType(
{
name: value
for name, value in headers.items()
if name.lower() not in {"api-key", "x-api-key", "authorization"}
},
"x-api-key": azure_api_key,
}
if name.lower() not in ("api-key", "x-api-key", "authorization")
}
)
headers = MappingProxyType({**non_auth_headers, "x-api-key": azure_api_key}).copy()
# Get tools and other anthropic-specific setup
tools: Final = optional_params.get("tools")
@ -114,7 +115,9 @@ class AzureAnthropicConfig(AnthropicConfig):
# Merge headers - Azure auth (api-key or Authorization) takes precedence
headers = {**anthropic_headers, **headers}
if "Authorization" in headers:
headers = {name: value for name, value in headers.items() if name.lower() != "x-api-key"}
headers = MappingProxyType(
{name: value for name, value in headers.items() if name.lower() != "x-api-key"}
).copy()
# Ensure anthropic-version header is set
if "anthropic-version" not in headers:

View file

@ -1,6 +1,7 @@
import copy
import enum
import re
from types import MappingProxyType
from typing import TYPE_CHECKING, Final, cast
from urllib.parse import urlparse
@ -81,14 +82,14 @@ class AzureAIStudioConfig(OpenAIConfig):
api_key: str | None = None,
api_base: str | None = None,
) -> dict:
non_auth_headers: Final = {
name: value for name, value in headers.items() if name.lower() not in {"api-key", "authorization"}
}
non_auth_headers: Final = MappingProxyType(
{name: value for name, value in headers.items() if name.lower() not in ("api-key", "authorization")}
)
if api_key:
if api_base and self._should_use_api_key_header(api_base):
headers = {**non_auth_headers, "api-key": api_key}
headers = MappingProxyType({**non_auth_headers, "api-key": api_key}).copy()
else:
headers = {**non_auth_headers, "Authorization": f"Bearer {api_key}"}
headers = MappingProxyType({**non_auth_headers, "Authorization": f"Bearer {api_key}"}).copy()
else:
# No api_key provided — fall back to Azure AD token-based auth
litellm_params_obj = GenericLiteLLMParams(**(litellm_params if isinstance(litellm_params, dict) else {}))

View file

@ -4,6 +4,7 @@ Support for Mistral Voxtral audio transcription via ``/v1/audio/transcriptions``
API reference: https://docs.mistral.ai/api/#tag/audio/operation/audio_transcriptions_v1_audio_transcriptions_post
"""
from types import MappingProxyType
from typing import Final
import httpx
@ -86,10 +87,10 @@ class MistralAudioTranscriptionConfig(BaseAudioTranscriptionConfig):
"Authorization": f"Bearer {api_key}",
"accept": "application/json",
}
non_auth_headers: Final = {
name: value for name, value in (headers or {}).items() if name.lower() != "authorization"
}
return {**non_auth_headers, **default_headers}
non_auth_headers: Final = MappingProxyType(
{name: value for name, value in headers.items() if name.lower() != "authorization"}
)
return MappingProxyType({**non_auth_headers, **default_headers}).copy()
def transform_audio_transcription_request(
self,

View file

@ -1,3 +1,4 @@
from types import MappingProxyType
from typing import TYPE_CHECKING, Any, Final
import httpx
@ -55,7 +56,9 @@ class _BaseReductoOCRConfig(BaseOCRConfig):
"Missing REDUCTO_API_KEY - set it in the environment or pass api_key to litellm.ocr()/litellm.aocr()"
)
non_auth_headers: Final = {name: value for name, value in headers.items() if name.lower() != "authorization"}
non_auth_headers: Final = MappingProxyType(
{name: value for name, value in headers.items() if name.lower() != "authorization"}
)
return {
**non_auth_headers,
"Authorization": f"Bearer {resolved_key}",

View file

@ -5,7 +5,7 @@ sys.path.insert(
0, os.path.abspath(os.path.join(os.path.dirname(__file__), "../../../../.."))
)
from unittest.mock import MagicMock, patch
from unittest.mock import patch
import pytest
@ -118,8 +118,7 @@ class TestAzureAnthropicConfig:
call_args = mock_validate.call_args
assert call_args[1]["litellm_params"].api_key == "provided-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)"""
def test_validate_environment_converts_api_key_header(self):
config = AzureAnthropicConfig()
headers = {}
model = "claude-sonnet-4-5"
@ -140,9 +139,8 @@ class TestAzureAnthropicConfig:
litellm_params=litellm_params,
)
# Verify api-key header is preserved as-is
assert "api-key" in result
assert result["api-key"] == "test-api-key"
assert result["x-api-key"] == "test-api-key"
assert "api-key" not in result
def test_validate_environment_sets_anthropic_version(self):
"""Test that anthropic-version header is set"""
@ -217,8 +215,8 @@ class TestAzureAnthropicConfig:
See: https://github.com/BerriAI/litellm/issues/XXXX
"""
config = AzureAnthropicConfig()
messages = [{"role": "user", "content": "Hello"}]
optional_params = {
"max_tokens": 100,
}
@ -260,8 +258,8 @@ class TestAzureAnthropicConfig:
def test_context_management_compact_beta_header(self):
"""Test that context_management with compact adds the correct beta header for Azure AI"""
config = AzureAnthropicConfig()
messages = [{"role": "user", "content": "Hello"}]
optional_params = {
"context_management": {"edits": [{"type": "compact_20260112"}]},
"max_tokens": 100,
@ -289,7 +287,6 @@ class TestAzureAnthropicConfig:
"""Test that compact beta header is added to headers for Azure AI"""
config = AzureAnthropicConfig()
messages = [{"role": "user", "content": "Hello"}]
optional_params = {
"context_management": {"edits": [{"type": "compact_20260112"}]},
"max_tokens": 100,
@ -428,7 +425,6 @@ class TestAzureAnthropicConfig:
"""Test that context_management with both compact and other edits adds both beta headers"""
config = AzureAnthropicConfig()
messages = [{"role": "user", "content": "Hello"}]
optional_params = {
"context_management": {
"edits": [