fix(anthropic): filter Converse body betas, keep Foundry auto betas, read the bundled beta config for row checks

This commit is contained in:
mateo-berri 2026-10-28 18:00:34 -07:00
parent 39b9b8d787
commit aedc352aaa
6 changed files with 87 additions and 7 deletions

View file

@ -242,7 +242,10 @@ def get_provider_name(provider: str) -> str:
def has_beta_headers_config(provider: str) -> bool:
return get_provider_name(provider) in _load_beta_headers_config()
config: Final = _bundled_beta_headers_config()
aliases: Final = config.get("provider_aliases", {})
canonical_provider: Final = (aliases.get(provider) if isinstance(aliases, Mapping) else None) or provider
return canonical_provider in config
def filter_and_transform_beta_headers(

View file

@ -110,7 +110,8 @@ class AzureAnthropicConfig(AnthropicConfig):
thinking=optional_params.get("thinking"), # pyright: ignore[reportUnknownMemberType, reportUnknownArgumentType] # optional_params is the base config's bare dict
)
# Merge headers - Azure auth (api-key or Authorization) takes precedence
headers = {**anthropic_headers, **headers}
merged_beta_header: Final = {k: v for k, v in anthropic_headers.items() if k == "anthropic-beta"}
headers = {**anthropic_headers, **headers, **merged_beta_header}
# Ensure anthropic-version header is set
if "anthropic-version" not in headers:

View file

@ -18,6 +18,7 @@ from typing_extensions import ReadOnly, TypedDict
import litellm
from litellm._logging import verbose_logger
from litellm.anthropic_beta_gated_fields import active_beta_values, log_beta_gated_strips, strip_beta_gated_thinking
from litellm.anthropic_beta_headers_manager import filter_and_transform_beta_headers
from litellm.constants import (
BEDROCK_MIN_THINKING_BUDGET_TOKENS,
RESPONSE_FORMAT_TOOL_NAME,
@ -1830,11 +1831,14 @@ class AmazonConverseConfig(BaseConfig):
base_model: Final = BedrockModelInfo.get_base_model(model)
header_beta_list: Final = get_anthropic_beta_from_headers(headers or {})
client_beta_list: Final = header_beta_list or sorted(
active_beta_values(
{},
additional_request_params.get("anthropic_beta"), # pyright: ignore[reportUnknownMemberType, reportUnknownArgumentType] # additional_request_params is a bare dict
)
client_beta_list: Final = header_beta_list or filter_and_transform_beta_headers(
sorted(
active_beta_values(
{},
additional_request_params.get("anthropic_beta"), # pyright: ignore[reportUnknownMemberType, reportUnknownArgumentType] # additional_request_params is a bare dict
)
),
provider="bedrock_converse",
)
eager_beta: Final = (
(ANTHROPIC_FINE_GRAINED_TOOL_STREAMING_BETA_HEADER,)

View file

@ -7,9 +7,11 @@ from unittest.mock import patch
import httpx
import pytest
import respx
from pydantic import JsonValue
import litellm
from litellm import anthropic_beta_headers_manager
from litellm.litellm_core_utils.litellm_logging import Logging
from litellm.llms.anthropic.messages.native.transformation import (
AnthropicMessagesConfig,
@ -521,3 +523,29 @@ def test_a_provider_without_a_beta_headers_row_forwards_beta_gated_fields_under_
)
assert request["thinking"] == thinking
assert request["messages"][-1]["output_config"] == {"effort": "low"}
def test_a_first_party_messages_request_never_fetches_the_remote_beta_headers_config(
monkeypatch: pytest.MonkeyPatch,
) -> None:
monkeypatch.delenv("LITELLM_LOCAL_ANTHROPIC_BETA_HEADERS", raising=False)
monkeypatch.setattr(anthropic_beta_headers_manager, "_BETA_HEADERS_CONFIG", None)
with respx.mock(assert_all_called=False) as router:
remote_config: Final = router.get(litellm.anthropic_beta_headers_url).mock(
return_value=httpx.Response(200, json={})
)
request: Final = AnthropicMessagesConfig().transform_anthropic_messages_request(
model="claude-opus-4-8",
messages=[
{"role": "user", "content": "hi"},
{"role": "system", "content": "be terse", "clear_at": "end_turn"},
{"role": "user", "content": "go"},
],
anthropic_messages_optional_request_params={"max_tokens": 64},
litellm_params=GenericLiteLLMParams(drop_params=True),
headers={},
)
assert request["messages"][1] == {"role": "system", "content": "be terse"}
assert not remote_config.called

View file

@ -310,3 +310,34 @@ def test_completion_adds_the_binding_beta_so_block_binding_is_kept(
sent: Final = json.loads(route.calls.last.request.content)
assert sent["thinking"] == {"type": "enabled", "budget_tokens": 1024, "block_binding": block_binding}
assert "thinking-binding-controls-2026-08-01" in route.calls.last.request.headers["anthropic-beta"].split(",")
@respx.mock
def test_completion_sends_the_binding_beta_next_to_the_callers_own_betas(local_beta_headers_config: None) -> None:
route: Final = respx.post("https://example-resource.services.ai.azure.com/anthropic/v1/messages").mock(
return_value=Response(
200,
json={
"id": "msg_bound",
"type": "message",
"role": "assistant",
"model": "claude-sonnet-4-6",
"content": [{"type": "text", "text": "Hello"}],
"stop_reason": "end_turn",
"stop_sequence": None,
"usage": {"input_tokens": 1, "output_tokens": 1},
},
)
)
litellm.completion(
model="azure_ai/claude-sonnet-4-6",
messages=[{"role": "user", "content": "hi"}],
api_base="https://example-resource.services.ai.azure.com/anthropic",
api_key="azure-test-key",
thinking={"type": "enabled", "budget_tokens": 1024, "block_binding": {"prefix_mismatch_behavior": "error"}},
extra_headers={"anthropic-beta": "context-1m-2025-08-07"},
)
sent_betas: Final = set(route.calls.last.request.headers["anthropic-beta"].split(","))
assert {"context-1m-2025-08-07", "thinking-binding-controls-2026-08-01"} <= sent_betas

View file

@ -10884,6 +10884,19 @@ def test_converse_keeps_beta_gated_thinking_fields_the_body_anthropic_beta_carri
}
def test_converse_drops_body_betas_the_bedrock_converse_row_rejects_when_adding_thinking_betas(
local_beta_headers_config: None,
) -> None:
fields: Final = _converse_bound_thinking_request(
None, body_betas=("per-turn-control-2026-07-01", "context-1m-2025-08-07")
)
assert sorted(fields["anthropic_beta"]) == [
"context-1m-2025-08-07",
"thinking-binding-controls-2026-08-01",
"thinking-display-updates-2026-08-18",
]
def test_converse_sends_only_the_header_betas_when_header_and_body_both_carry_betas(
monkeypatch: pytest.MonkeyPatch,
) -> None: