fix(anthropic): drop unknown legacy thinking keys under drop_params when rebuilding adaptive thinking (#46741)

* fix(anthropic): drop unknown legacy thinking keys under drop_params when rebuilding adaptive thinking

* fix(bedrock): drop unknown thinking keys under drop_params on display updates rebuilds

Bedrock Invoke copied every caller thinking key when adding display updates,
so an unknown key still reached the provider with drop_params on. Filter that
copy the same way, and cover each chat call site plus the global
litellm.drop_params path in tests

* fix(anthropic): drop budget_tokens from rebuilt adaptive thinking under drop_params

* test(anthropic): cover drop_params sources for rebuilt adaptive thinking on /v1/messages and Bedrock Invoke
This commit is contained in:
Mateo Wang 2026-10-15 23:09:30 +00:00 • committed by GitHub
parent 8007773ec5
commit d489c04554
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
12 changed files with 335 additions and 32 deletions

View file

@ -1878,7 +1878,10 @@ class AnthropicConfig(AnthropicModelInfo, BaseConfig):
else:
optional_params["thinking"] = value
AnthropicModelInfo.translate_legacy_thinking_for_adaptive_model(
model=model, optional_params=optional_params, custom_llm_provider=self._resolved_provider
model=model,
optional_params=optional_params,
custom_llm_provider=self._resolved_provider,
drop_params=drop_params,
)
elif param == "reasoning_effort":
# Accept both string ("low") and dict ({"effort": "low",

View file

@ -110,7 +110,7 @@ _INFERENCE_PROFILE_MINOR_RE: Final = re.compile(r":\d+$")
_DATED_RELEASE_SUFFIX_RE: Final = re.compile(r"-\d{8}$")
_DOTTED_VERSION_RE: Final = re.compile(r"(\d)\.(\d)")
_CLAUDE_CODE_BILLING_HEADER_PREFIX: Final = "x-anthropic-billing-header:"
_OBJECT_MAPPING_ADAPTER: Final = TypeAdapter(dict[object, object])
OBJECT_MAPPING_ADAPTER: Final = TypeAdapter(dict[object, object])
_CLAUDE_CODE_OBJECT_LIST_ADAPTER: Final = TypeAdapter(list[object])
_DECODED_JSON: Final = TypeAdapter(object)
@ -168,7 +168,7 @@ def is_claude_code_user_agent(user_agent: str) -> bool:
def validated_object_mapping(value: object) -> dict[object, object] | None:
try:
return _OBJECT_MAPPING_ADAPTER.validate_python(value)
return OBJECT_MAPPING_ADAPTER.validate_python(value)
except ValidationError:
return None
@ -925,6 +925,7 @@ class AnthropicModelInfo(BaseLLMModelInfo):
model: str,
optional_params: MutableMapping[str, object], # mutable-ok: in-place out-param like the sibling helpers
custom_llm_provider: str,
drop_params: bool,
) -> None:
"""Translate legacy ``thinking.type=enabled`` to adaptive for the
adaptive-thinking models that reject it (4.7+ and the 5 families).
@ -950,10 +951,10 @@ class AnthropicModelInfo(BaseLLMModelInfo):
existing_output_config: Final = optional_params.get("output_config")
display: Final = thinking.get("display")
supported_display: Final = {"display": display} if display in ("summarized", "omitted") else {}
carried_thinking_keys: Final = {
key: value for key, value in thinking.items() if key not in ("type", "budget_tokens", "display")
}
optional_params["thinking"] = {**carried_thinking_keys, "type": "adaptive", **supported_display}
carried_keys: Final = carried_thinking_keys(
thinking, rebuilt_keys=frozenset(("type", "budget_tokens", "display")), drop_params=drop_params
)
optional_params["thinking"] = {**carried_keys, "type": "adaptive", **supported_display}
optional_params["output_config"] = {
"effort": effort,
**(existing_output_config if isinstance(existing_output_config, dict) else MappingProxyType({})),
@ -1621,6 +1622,22 @@ def is_anthropic_invalid_thinking_block_error(error_text: str) -> bool:
return "must contain thinking" in lower
_ADAPTIVE_THINKING_KEYS: Final = (
AnthropicThinkingParam.__required_keys__ | AnthropicThinkingParam.__optional_keys__
) - {"budget_tokens"}
def carried_thinking_keys(
thinking: Mapping[object, object], rebuilt_keys: frozenset[str], drop_params: bool
) -> Mapping[object, object]:
drops_unknown_keys: Final = drop_params or litellm.drop_params is True
return {
key: value
for key, value in thinking.items()
if key not in rebuilt_keys and (key in _ADAPTIVE_THINKING_KEYS or not drops_unknown_keys)
}
def is_anthropic_thinking_binding_mismatch_error(error_text: str) -> bool:
"""messages.N.content.M: Invalid `signature` in `thinking` block. The block is bound to a different conversation."""
return "bound to a different conversation" in error_text.lower()

View file

@ -608,6 +608,7 @@ class AnthropicMessagesConfig(BaseAnthropicMessagesConfig):
model=model,
optional_params=anthropic_messages_optional_request_params,
custom_llm_provider=self._resolved_provider,
drop_params=getattr(litellm_params, "drop_params", None) is True,
)
self._translate_adaptive_effort_for_non_adaptive_model(

View file

@ -1159,7 +1159,10 @@ class AmazonConverseConfig(BaseConfig):
else:
optional_params["thinking"] = value
AnthropicModelInfo.translate_legacy_thinking_for_adaptive_model(
model=model, optional_params=optional_params, custom_llm_provider="bedrock"
model=model,
optional_params=optional_params,
custom_llm_provider="bedrock",
drop_params=drop_params,
)
elif param == "reasoning_effort" and isinstance(value, str) and drop_reasoning_effort_param:
verbose_logger.debug(

View file

@ -13,7 +13,11 @@ from litellm.litellm_core_utils.prompt_templates.image_handling import (
convert_url_to_base64,
)
from litellm.llms.anthropic.chat.transformation import AnthropicConfig
from litellm.llms.anthropic.common_utils import AnthropicModelInfo
from litellm.llms.anthropic.common_utils import (
OBJECT_MAPPING_ADAPTER,
AnthropicModelInfo,
carried_thinking_keys,
)
from litellm.llms.bedrock.chat.invoke_transformations.base_invoke_transformation import (
AmazonInvokeConfig,
)
@ -114,7 +118,7 @@ class AmazonAnthropicClaudeConfig(AmazonInvokeConfig, AnthropicConfig):
)
AnthropicModelInfo.translate_legacy_thinking_for_adaptive_model(
model=model, optional_params=mapped_params, custom_llm_provider="bedrock"
model=model, optional_params=mapped_params, custom_llm_provider="bedrock", drop_params=drop_params
)
translated_thinking: Final = mapped_params.get("thinking")
if (
@ -122,7 +126,14 @@ class AmazonAnthropicClaudeConfig(AmazonInvokeConfig, AnthropicConfig):
and isinstance(translated_thinking, dict)
and translated_thinking.get("type") == "adaptive"
):
mapped_params["thinking"] = {**translated_thinking, "display": "updates"}
mapped_params["thinking"] = {
**carried_thinking_keys(
OBJECT_MAPPING_ADAPTER.validate_python(translated_thinking),
rebuilt_keys=frozenset(("display",)),
drop_params=drop_params,
),
"display": "updates",
}
return mapped_params

View file

@ -17,7 +17,12 @@ from litellm.llms.anthropic.chat.transformation import (
DROP_UNSUPPORTED_OUTPUT_CONFIG_WARNING,
AnthropicConfig,
)
from litellm.llms.anthropic.common_utils import AnthropicModelInfo, validated_object_mapping
from litellm.llms.anthropic.common_utils import (
OBJECT_MAPPING_ADAPTER,
AnthropicModelInfo,
carried_thinking_keys,
validated_object_mapping,
)
from litellm.llms.anthropic.pass_through.messages.transformation import (
AnthropicMessagesConfig,
)
@ -250,6 +255,7 @@ class AmazonAnthropicClaudeMessagesConfig(
self,
anthropic_messages_request: dict,
model: str,
drop_params: bool,
) -> bool:
"""
Bedrock rejects ``clear_thinking_20251015`` context-management edits unless
@ -286,7 +292,9 @@ class AmazonAnthropicClaudeMessagesConfig(
return False
if t == "enabled":
budget_tokens: Final = self._resolve_clear_thinking_budget_tokens(thinking.get("budget_tokens"))
self._inject_adaptive_thinking_for_clear_thinking(anthropic_messages_request, budget_tokens, model)
self._inject_adaptive_thinking_for_clear_thinking(
anthropic_messages_request, budget_tokens, model, drop_params
)
return True
verbose_logger.debug(
"Bedrock clear_thinking_20251015: replacing thinking=%s with minimal thinking config",
@ -305,7 +313,7 @@ class AmazonAnthropicClaudeMessagesConfig(
return False
if is_adaptive_thinking_model:
self._inject_adaptive_thinking_for_clear_thinking(anthropic_messages_request, budget, model)
self._inject_adaptive_thinking_for_clear_thinking(anthropic_messages_request, budget, model, drop_params)
return True
anthropic_messages_request["thinking"] = {
@ -338,7 +346,7 @@ class AmazonAnthropicClaudeMessagesConfig(
return "low"
def _inject_adaptive_thinking_for_clear_thinking(
self, anthropic_messages_request: dict, budget_tokens: int, model: str
self, anthropic_messages_request: dict, budget_tokens: int, model: str, drop_params: bool
) -> None:
"""Adaptive-thinking models (Opus 4.7/4.8, Fable 5) reject
``thinking.type=enabled`` on Bedrock. Use ``thinking.type=adaptive`` plus
@ -349,7 +357,9 @@ class AmazonAnthropicClaudeMessagesConfig(
output_config = {}
output_config.setdefault("effort", self._effort_from_thinking_budget(budget_tokens))
thinking: Final = validated_object_mapping(anthropic_messages_request.get("thinking"))
carried: Final = {key: value for key, value in (thinking or {}).items() if key not in ("type", "budget_tokens")}
carried: Final = carried_thinking_keys(
thinking or {}, rebuilt_keys=frozenset(("type", "budget_tokens")), drop_params=drop_params
)
anthropic_messages_request["output_config"] = output_config
anthropic_messages_request["thinking"] = {**carried, "type": "adaptive"}
verbose_logger.debug(
@ -664,6 +674,7 @@ class AmazonAnthropicClaudeMessagesConfig(
) -> dict:
requested_thinking: Final = anthropic_messages_optional_request_params.get("thinking")
requested_display_updates: Final = AnthropicModelInfo().is_thinking_display_updates_used(requested_thinking)
drop_params: Final = getattr(litellm_params, "drop_params", None) is True
self._clamp_adaptive_reasoning_effort_for_bedrock(
model=model,
optional_params=anthropic_messages_optional_request_params,
@ -682,7 +693,14 @@ class AmazonAnthropicClaudeMessagesConfig(
and isinstance(translated_thinking, dict)
and translated_thinking.get("type") == "adaptive"
):
anthropic_messages_request["thinking"] = {**translated_thinking, "display": "updates"}
anthropic_messages_request["thinking"] = {
**carried_thinking_keys(
OBJECT_MAPPING_ADAPTER.validate_python(translated_thinking),
rebuilt_keys=frozenset(("display",)),
drop_params=drop_params,
),
"display": "updates",
}
self._normalize_system_role_messages(anthropic_messages_request, model=model)
#########################################################
############## BEDROCK Invoke SPECIFIC TRANSFORMATION ###
@ -705,6 +723,7 @@ class AmazonAnthropicClaudeMessagesConfig(
injected_thinking_for_clear_thinking: Final = self._ensure_thinking_for_clear_thinking_context_management(
anthropic_messages_request=anthropic_messages_request,
model=model,
drop_params=drop_params,
)
# 4. Remove `ttl` field from cache_control in messages (Bedrock doesn't support it for older models)

View file

@ -393,7 +393,7 @@ class DatabricksConfig(DatabricksBase, OpenAILikeChatConfig, AnthropicConfig):
mapped_params: Final = super().map_openai_params(non_default_params, optional_params, model, drop_params)
if "claude" in model:
AnthropicConfig.translate_legacy_thinking_for_adaptive_model(
model=model, optional_params=mapped_params, custom_llm_provider="databricks"
model=model, optional_params=mapped_params, custom_llm_provider="databricks", drop_params=drop_params
)
if "tools" in mapped_params:
mapped_params["tools"] = self._map_openai_to_dbrx_tool(

View file

@ -185,7 +185,7 @@ class VertexAIAnthropicConfig(AnthropicConfig):
)
AnthropicModelInfo.translate_legacy_thinking_for_adaptive_model(
model=model, optional_params=mapped_params, custom_llm_provider="vertex_ai"
model=model, optional_params=mapped_params, custom_llm_provider="vertex_ai", drop_params=drop_params
)
return mapped_params

View file

@ -1,5 +1,6 @@
import uuid
from collections.abc import Mapping
from types import MappingProxyType
from typing import Final
import pytest
@ -15,7 +16,15 @@ def _claude_code_turn(sent: Mapping[str, JsonValue]) -> dict[str, JsonValue]:
return {**without_reasoning, "stream": False, **sent}
def _forward(gateway: Gateway, upstream_model: str, sent: Mapping[str, JsonValue]) -> cc.Forwarded:
def _forward(
gateway: Gateway,
upstream_model: str,
sent: Mapping[str, JsonValue],
*,
deployment: Mapping[str, JsonValue] = MappingProxyType({}),
gateway_only: Mapping[str, JsonValue] = MappingProxyType({}),
from_claude_code: bool = True,
) -> cc.Forwarded:
client_body: Final = _claude_code_turn(sent)
def respond(request: Request) -> Reply:
@ -30,13 +39,13 @@ def _forward(gateway: Gateway, upstream_model: str, sent: Mapping[str, JsonValue
with wire_server(respond) as wire, gateway.scenario() as scenario:
model: Final = scenario.model(
model=f"anthropic/{upstream_model}", api_base=wire.url, api_key=cc.ANTHROPIC_API_KEY
model=f"anthropic/{upstream_model}", api_base=wire.url, api_key=cc.ANTHROPIC_API_KEY, **deployment
)
response: Final = gateway.request(
"POST",
"/v1/messages",
{**client_body, "model": model},
headers=cc.cli_headers(gateway.key, cc.FRONTIER_CLI_BETA),
{**client_body, **gateway_only, "model": model},
headers=cc.cli_headers(gateway.key, cc.FRONTIER_CLI_BETA) if from_claude_code else None,
)
assert response.status_code == 200, response.text
received: Final = wire.drain()
@ -194,6 +203,51 @@ def test_legacy_thinking_budget_becomes_adaptive_effort_only_on_models_that_reje
assert forwarded.reasoning_betas == cc.CLAUDE_CODE_REASONING_BETAS, forwarded.reasoning_betas
UNKNOWN_THINKING_KEY: Final = MappingProxyType({"not_a_thinking_key": True})
BLOCK_BINDING: Final = MappingProxyType({"prefix_mismatch_behavior": "error"})
@pytest.mark.parametrize(
("from_claude_code", "deployment", "gateway_only", "carried_unknown_key"),
(
pytest.param(False, {}, {"drop_params": True}, {}, id="request-drop-params-drops-the-unknown-key"),
pytest.param(False, {"drop_params": True}, {}, {}, id="deployment-drop-params-drops-the-unknown-key"),
pytest.param(
False, {}, {}, dict(UNKNOWN_THINKING_KEY), id="without-drop-params-the-unknown-key-reaches-anthropic"
),
pytest.param(True, {}, {}, {}, id="claude-code-gets-drop-params-by-default-and-drops-the-unknown-key"),
),
)
def test_rebuilt_legacy_thinking_drops_unknown_keys_only_under_drop_params(
gateway: Gateway,
from_claude_code: bool,
deployment: dict[str, JsonValue],
gateway_only: dict[str, JsonValue],
carried_unknown_key: dict[str, JsonValue],
) -> None:
sent: Final = {
"thinking": {
"type": "enabled",
"budget_tokens": 2048,
"block_binding": dict(BLOCK_BINDING),
**UNKNOWN_THINKING_KEY,
}
}
forwarded: Final = _forward(
gateway,
"claude-opus-4-7",
sent,
deployment=deployment,
gateway_only=gateway_only,
from_claude_code=from_claude_code,
)
assert forwarded.reasoning == {
"thinking": {"type": "adaptive", "block_binding": dict(BLOCK_BINDING), **carried_unknown_key},
"output_config": {"effort": "medium"},
}, forwarded
assert forwarded.other_changes == {}, forwarded.other_changes
@pytest.mark.parametrize(
("upstream_model", "sent", "received"),
(

View file

@ -4,6 +4,7 @@ from typing import Final
import pytest
from integration._support.client import Gateway
from integration._support.wire import Reply, Request, wire_server
from pydantic import JsonValue
MODEL: Final = "bedrock/invoke/us.anthropic.claude-opus-4-8"
TOKEN: Final = "synthetic-bedrock-bearer"
@ -58,3 +59,49 @@ def test_prefixed_opus_4_8_reasoning_effort_reaches_bedrock_as_adaptive_thinking
assert response.json()["choices"][0]["message"]["content"] == "adaptive thinking control"
assert response.json()["usage"]["prompt_tokens"] == 12 and response.json()["usage"]["completion_tokens"] == 5
assert len(wire.drain()) == 1
BLOCK_BINDING: Final = {"prefix_mismatch_behavior": "error"}
DISPLAY_UPDATES_THINKING: Final = {"type": "adaptive", "display": "updates", "block_binding": BLOCK_BINDING}
NON_ADAPTIVE_THINKING_KEYS: Final = {"budget_tokens": 2048, "not_a_thinking_key": True}
def recorded_reply(_request: Request) -> Reply:
return Reply(body=RESPONSE)
@pytest.mark.parametrize("route", ("/v1/chat/completions", "/v1/messages"))
@pytest.mark.parametrize(
("client_drop_params", "carried_keys"),
(
pytest.param({"drop_params": True}, {}, id="drop-params-sends-only-adaptive-fields"),
pytest.param({}, NON_ADAPTIVE_THINKING_KEYS, id="without-drop-params-forwards-every-key"),
),
)
def test_bedrock_display_updates_rebuild_drops_non_adaptive_thinking_keys_only_under_drop_params(
gateway: Gateway, route: str, client_drop_params: dict[str, JsonValue], carried_keys: dict[str, JsonValue]
) -> None:
with wire_server(recorded_reply) as wire, gateway.scenario() as scenario:
model: Final = scenario.model(
model=MODEL,
api_key=TOKEN,
aws_region_name="us-east-1",
api_base=wire.url,
aws_bedrock_runtime_endpoint=wire.url,
)
response: Final = gateway.request(
"POST",
route,
{
"model": model,
"messages": [{"role": "user", "content": "synthetic display updates request"}],
"max_tokens": 4096,
"thinking": {**DISPLAY_UPDATES_THINKING, **NON_ADAPTIVE_THINKING_KEYS},
**client_drop_params,
},
)
assert response.status_code == 200, response.text
received: Final = wire.drain()
assert len(received) == 1, received
assert received[0].target == "/model/us.anthropic.claude-opus-4-8/invoke", received[0].target
assert json.loads(received[0].body)["thinking"] == {**DISPLAY_UPDATES_THINKING, **carried_keys}

View file

@ -2700,7 +2700,9 @@ class TestWifZeroBehaviorChange:
assert "ANTHROPIC_SERVICE_ACCOUNT_ID" in exc_info.value.message
assert "ANTHROPIC_IDENTITY_TOKEN_FILE" in exc_info.value.message
def test_a_token_file_credential_missing_an_id_names_the_id_not_the_key(self, clean_anthropic_env: None, tmp_path: Path):
def test_a_token_file_credential_missing_an_id_names_the_id_not_the_key(
self, clean_anthropic_env: None, tmp_path: Path
):
import litellm
from litellm.llms.anthropic.common_utils import AnthropicModelInfo
@ -4254,13 +4256,17 @@ def test_shared_legacy_thinking_translation_preserves_supported_display(
model="claude-opus-5",
optional_params=optional_params,
custom_llm_provider="azure_ai",
drop_params=False,
)
assert optional_params["thinking"] == expected_thinking
@pytest.mark.usefixtures("local_model_cost_map")
def test_shared_legacy_thinking_translation_carries_block_binding() -> None:
@pytest.mark.parametrize(("drop_params", "expected_unknown_keys"), ((True, {}), (False, {"not_a_thinking_key": True})))
def test_shared_legacy_thinking_translation_carries_block_binding(
drop_params: bool, expected_unknown_keys: dict[str, object]
) -> None:
from litellm.llms.anthropic.common_utils import AnthropicModelInfo
optional_params: Final = {
@ -4268,6 +4274,7 @@ def test_shared_legacy_thinking_translation_carries_block_binding() -> None:
"type": "enabled",
"budget_tokens": 2048,
"block_binding": {"prefix_mismatch_behavior": "error"},
"not_a_thinking_key": True,
},
}
@ -4275,14 +4282,96 @@ def test_shared_legacy_thinking_translation_carries_block_binding() -> None:
model="claude-opus-5",
optional_params=optional_params,
custom_llm_provider="azure_ai",
drop_params=drop_params,
)
assert optional_params["thinking"] == {
"type": "adaptive",
"block_binding": {"prefix_mismatch_behavior": "error"},
**expected_unknown_keys,
}
@pytest.mark.usefixtures("local_model_cost_map")
@pytest.mark.parametrize(
("custom_llm_provider", "model", "requested_thinking", "rejected_keys", "expected_thinking"),
(
(
"anthropic",
"claude-opus-5-5",
{"type": "enabled", "budget_tokens": 2048},
{"not_a_thinking_key": True},
{"type": "adaptive"},
),
(
"bedrock",
"converse/us.anthropic.claude-opus-5-5",
{"type": "enabled", "budget_tokens": 2048},
{"not_a_thinking_key": True},
{"type": "adaptive"},
),
(
"bedrock",
"invoke/us.anthropic.claude-opus-5-5",
{"type": "enabled", "budget_tokens": 2048},
{"not_a_thinking_key": True},
{"type": "adaptive"},
),
(
"bedrock",
"invoke/us.anthropic.claude-opus-5-5",
{"type": "adaptive", "display": "updates"},
{"not_a_thinking_key": True, "budget_tokens": 2048},
{"type": "adaptive", "display": "updates"},
),
(
"vertex_ai",
"claude-opus-5-5",
{"type": "enabled", "budget_tokens": 2048},
{"not_a_thinking_key": True},
{"type": "adaptive"},
),
(
"databricks",
"databricks-claude-opus-5-5",
{"type": "enabled", "budget_tokens": 2048},
{"not_a_thinking_key": True},
{"type": "adaptive"},
),
),
ids=(
"anthropic",
"bedrock_converse",
"bedrock_invoke",
"bedrock_invoke_display_updates",
"vertex_ai",
"databricks",
),
)
@pytest.mark.parametrize("drop_params_source", ("request", "global", None))
def test_chat_thinking_drops_unknown_keys_only_under_drop_params(
custom_llm_provider: str,
model: str,
requested_thinking: dict[str, object],
rejected_keys: dict[str, object],
expected_thinking: dict[str, object],
drop_params_source: str | None,
monkeypatch: pytest.MonkeyPatch,
) -> None:
block_binding: Final = {"prefix_mismatch_behavior": "error"}
monkeypatch.setattr(litellm, "drop_params", drop_params_source == "global")
optional_params: Final = litellm.utils.get_optional_params(
model=model,
custom_llm_provider=custom_llm_provider,
thinking={**requested_thinking, "block_binding": block_binding, **rejected_keys},
drop_params=drop_params_source == "request",
)
kept_rejected_keys: Final = {} if drop_params_source else rejected_keys
assert optional_params["thinking"] == {**expected_thinking, "block_binding": block_binding, **kept_rejected_keys}
@pytest.mark.usefixtures("local_model_cost_map", "local_beta_headers_config")
@pytest.mark.parametrize("action", (None, "tool_addition", "tool_removal"))
@pytest.mark.parametrize("explicit_beta", (False, True))

View file

@ -2047,7 +2047,7 @@ def test_bedrock_clear_thinking_injects_adaptive_with_effort_for_adaptive_models
}
changed = cfg._ensure_thinking_for_clear_thinking_context_management(
anthropic_messages_request=request, model=model
anthropic_messages_request=request, model=model, drop_params=False
)
assert changed is True
@ -2070,7 +2070,7 @@ def test_bedrock_clear_thinking_converts_legacy_enabled_budget_to_effort():
}
changed = cfg._ensure_thinking_for_clear_thinking_context_management(
anthropic_messages_request=request, model="us.anthropic.claude-opus-4-8"
anthropic_messages_request=request, model="us.anthropic.claude-opus-4-8", drop_params=False
)
assert changed is True
@ -2098,7 +2098,7 @@ def test_bedrock_clear_thinking_keeps_enabled_for_non_adaptive_models():
}
changed = cfg._ensure_thinking_for_clear_thinking_context_management(
anthropic_messages_request=request, model="anthropic.claude-opus-4-5-v1:0"
anthropic_messages_request=request, model="anthropic.claude-opus-4-5-v1:0", drop_params=False
)
assert changed is True
@ -2605,7 +2605,9 @@ def test_inject_adaptive_thinking_preserves_existing_effort():
cfg = AmazonAnthropicClaudeMessagesConfig()
request = {"output_config": {"effort": "max", "other": "keep"}}
cfg._inject_adaptive_thinking_for_clear_thinking(request, budget_tokens=24000, model="us.anthropic.claude-fable-5")
cfg._inject_adaptive_thinking_for_clear_thinking(
request, budget_tokens=24000, model="us.anthropic.claude-fable-5", drop_params=False
)
assert request["thinking"] == {"type": "adaptive"}
assert request["output_config"] == {"effort": "max", "other": "keep"}
@ -2622,7 +2624,7 @@ def test_bedrock_clear_thinking_noops_when_thinking_already_adaptive():
}
changed = cfg._ensure_thinking_for_clear_thinking_context_management(
anthropic_messages_request=request, model="us.anthropic.claude-fable-5"
anthropic_messages_request=request, model="us.anthropic.claude-fable-5", drop_params=False
)
assert changed is False
@ -2642,7 +2644,7 @@ def test_bedrock_clear_thinking_replaces_disabled_thinking_on_adaptive_model():
}
changed = cfg._ensure_thinking_for_clear_thinking_context_management(
anthropic_messages_request=request, model="us.anthropic.claude-fable-5"
anthropic_messages_request=request, model="us.anthropic.claude-fable-5", drop_params=False
)
assert changed is True
@ -2662,7 +2664,7 @@ def test_bedrock_clear_thinking_leaves_enabled_thinking_on_non_adaptive_model():
}
changed = cfg._ensure_thinking_for_clear_thinking_context_management(
anthropic_messages_request=request, model="anthropic.claude-opus-4-5-v1:0"
anthropic_messages_request=request, model="anthropic.claude-opus-4-5-v1:0", drop_params=False
)
assert changed is False
@ -3727,6 +3729,63 @@ def test_bedrock_messages_keeps_block_binding_with_display_updates(
assert result.get("thinking") == {"type": "adaptive", "display": "updates", "block_binding": BLOCK_BINDING}
@pytest.mark.usefixtures("local_model_cost_map", "local_beta_headers_config")
@pytest.mark.parametrize(
("model", "requested_thinking", "rejected_keys", "extra_params", "expected_thinking"),
(
(
"eu.anthropic.claude-opus-5",
{"type": "enabled", "budget_tokens": 2048},
{"not_a_thinking_key": True},
{},
{"type": "adaptive"},
),
(
"us.anthropic.claude-opus-4-6",
{"type": "enabled", "budget_tokens": 2048},
{"not_a_thinking_key": True},
{"context_management": {"edits": [{"type": "clear_thinking_20251015"}]}},
{"type": "adaptive"},
),
(
"eu.anthropic.claude-opus-5",
{"type": "adaptive", "display": "updates"},
{"not_a_thinking_key": True, "budget_tokens": 2048},
{},
{"type": "adaptive", "display": "updates"},
),
),
ids=("legacy", "legacy_clear_thinking", "adaptive_display_updates"),
)
@pytest.mark.parametrize("drop_params_source", ("request", "global", None))
def test_bedrock_messages_thinking_drops_unknown_keys_only_under_drop_params(
model: str,
requested_thinking: dict[str, object],
rejected_keys: dict[str, object],
extra_params: dict[str, object],
expected_thinking: dict[str, object],
drop_params_source: str | None,
monkeypatch: pytest.MonkeyPatch,
) -> None:
from litellm.types.router import GenericLiteLLMParams
monkeypatch.setattr(litellm, "drop_params", drop_params_source == "global")
result: Final = AmazonAnthropicClaudeMessagesConfig().transform_anthropic_messages_request(
model=model,
messages=[{"role": "user", "content": "Reply with OK"}],
anthropic_messages_optional_request_params={
"max_tokens": 4096,
"thinking": {**requested_thinking, "block_binding": BLOCK_BINDING, **rejected_keys},
**extra_params,
},
litellm_params=GenericLiteLLMParams(drop_params=drop_params_source == "request"),
headers={},
)
kept_rejected_keys: Final = {} if drop_params_source else rejected_keys
assert result.get("thinking") == {**expected_thinking, "block_binding": BLOCK_BINDING, **kept_rejected_keys}
@pytest.mark.usefixtures("local_model_cost_map", "local_beta_headers_config")
@pytest.mark.parametrize("action", (None, "tool_addition", "tool_removal"))
@pytest.mark.parametrize("explicit_beta", (False, True))