fix: stop provider-scoped headers leaking across fallback hops

completion() aliased the caller's header mapping instead of copying it,
then merged the provider-scoped headers into that same object. The router
shares one header dict across fallback attempts, so the credential written
on an Anthropic attempt was still present when a later Bedrock or Vertex
attempt read the dict, defeating the provider scoping.

Copy the mapping before merging so each attempt sees only its own headers.
This commit is contained in:
mateo-berri 2026-08-21 19:58:19 -07:00
parent b1b29e5cb0
commit 43143933d9
2 changed files with 48 additions and 2 deletions

View file

@ -5100,8 +5100,7 @@ def completion(
ensure_alternating_roles: Final[bool | None] = kwargs.get("ensure_alternating_roles", None)
user_continue_message: Final[ChatCompletionUserMessage | None] = kwargs.get("user_continue_message", None)
assistant_continue_message: ChatCompletionAssistantMessage | None = kwargs.get("assistant_continue_message", None)
if headers is None:
headers = {}
headers = {} if headers is None else dict(headers)
if extra_headers is not None:
headers.update(extra_headers)
# Inject proxy auth headers if configured

View file

@ -2754,3 +2754,50 @@ def test_completion_default_api_base_sends_prompt_cache_breakpoint_for_gpt_5_6()
{"type": "text", "text": "sys", "prompt_cache_breakpoint": {"mode": "explicit"}}
]
assert request_body["extra_body"]["prompt_cache_options"] == {"mode": "explicit"}
_SUBSCRIPTION_OAUTH_CREDENTIAL = "Bearer sk-ant-oat01-fake-subscription-token-for-testing-0123456789"
def _scoped_headers_for_oauth_request():
from litellm.types.utils import ProviderSpecificHeader
return [
ProviderSpecificHeader(
custom_llm_provider="anthropic,bedrock,vertex_ai",
extra_headers={"anthropic-version": "2023-06-01"},
),
ProviderSpecificHeader(
custom_llm_provider="anthropic",
extra_headers={"authorization": _SUBSCRIPTION_OAUTH_CREDENTIAL},
),
]
def _run_anthropic_hop_with_shared_headers(shared_headers):
litellm.completion(
model="anthropic/claude-3-5-sonnet-20240620",
messages=[{"role": "user", "content": "Say OK"}],
extra_headers=shared_headers,
provider_specific_header=_scoped_headers_for_oauth_request(),
api_key="sk-fake-anthropic-key",
mock_response="OK",
)
def test_completion_does_not_mutate_caller_supplied_headers():
shared_headers = {"x-tenant": "acme"}
_run_anthropic_hop_with_shared_headers(shared_headers)
assert shared_headers == {"x-tenant": "acme"}
def test_anthropic_oauth_credential_does_not_persist_into_next_provider_hop():
shared_headers = {"x-tenant": "acme"}
_run_anthropic_hop_with_shared_headers(shared_headers)
leaked = [name for name, value in shared_headers.items() if value == _SUBSCRIPTION_OAUTH_CREDENTIAL]
assert leaked == []
assert "anthropic-version" not in shared_headers