diff --git a/litellm/llms/anthropic/chat/transformation.py b/litellm/llms/anthropic/chat/transformation.py index f9cb1cbf091..0b2f75e352b 100644 --- a/litellm/llms/anthropic/chat/transformation.py +++ b/litellm/llms/anthropic/chat/transformation.py @@ -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", diff --git a/litellm/llms/anthropic/common_utils.py b/litellm/llms/anthropic/common_utils.py index c1e9f08a8a3..dd9b22f3d5d 100644 --- a/litellm/llms/anthropic/common_utils.py +++ b/litellm/llms/anthropic/common_utils.py @@ -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() diff --git a/litellm/llms/anthropic/pass_through/messages/transformation.py b/litellm/llms/anthropic/pass_through/messages/transformation.py index cb96708f200..640736d6463 100644 --- a/litellm/llms/anthropic/pass_through/messages/transformation.py +++ b/litellm/llms/anthropic/pass_through/messages/transformation.py @@ -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( diff --git a/litellm/llms/bedrock/chat/converse_transformation.py b/litellm/llms/bedrock/chat/converse_transformation.py index b6264f664d3..1f09b3724f1 100644 --- a/litellm/llms/bedrock/chat/converse_transformation.py +++ b/litellm/llms/bedrock/chat/converse_transformation.py @@ -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( diff --git a/litellm/llms/bedrock/chat/invoke_transformations/anthropic_claude3_transformation.py b/litellm/llms/bedrock/chat/invoke_transformations/anthropic_claude3_transformation.py index 6f43e625d73..04747405375 100644 --- a/litellm/llms/bedrock/chat/invoke_transformations/anthropic_claude3_transformation.py +++ b/litellm/llms/bedrock/chat/invoke_transformations/anthropic_claude3_transformation.py @@ -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 diff --git a/litellm/llms/bedrock/messages/invoke_transformations/anthropic_claude3_transformation.py b/litellm/llms/bedrock/messages/invoke_transformations/anthropic_claude3_transformation.py index 8c5ef7138ad..88ae0d4d514 100644 --- a/litellm/llms/bedrock/messages/invoke_transformations/anthropic_claude3_transformation.py +++ b/litellm/llms/bedrock/messages/invoke_transformations/anthropic_claude3_transformation.py @@ -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) diff --git a/litellm/llms/databricks/chat/transformation.py b/litellm/llms/databricks/chat/transformation.py index 2c04b32b1da..2145c19a71a 100644 --- a/litellm/llms/databricks/chat/transformation.py +++ b/litellm/llms/databricks/chat/transformation.py @@ -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( diff --git a/litellm/llms/vertex_ai/vertex_ai_partner_models/anthropic/transformation.py b/litellm/llms/vertex_ai/vertex_ai_partner_models/anthropic/transformation.py index f7a1e007c96..29253359003 100644 --- a/litellm/llms/vertex_ai/vertex_ai_partner_models/anthropic/transformation.py +++ b/litellm/llms/vertex_ai/vertex_ai_partner_models/anthropic/transformation.py @@ -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 diff --git a/tests/integration/messages_endpoint/providers/anthropic/reasoning/test_anthropic_reasoning_request_translation_wire.py b/tests/integration/messages_endpoint/providers/anthropic/reasoning/test_anthropic_reasoning_request_translation_wire.py index 99b03b0d820..35c96ae1745 100644 --- a/tests/integration/messages_endpoint/providers/anthropic/reasoning/test_anthropic_reasoning_request_translation_wire.py +++ b/tests/integration/messages_endpoint/providers/anthropic/reasoning/test_anthropic_reasoning_request_translation_wire.py @@ -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"), ( diff --git a/tests/integration/providers/test_bedrock_claude_thinking_wire.py b/tests/integration/providers/test_bedrock_claude_thinking_wire.py index d3673a55660..3925ef708b0 100644 --- a/tests/integration/providers/test_bedrock_claude_thinking_wire.py +++ b/tests/integration/providers/test_bedrock_claude_thinking_wire.py @@ -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} diff --git a/tests/unit/llms/anthropic/test_anthropic_common_utils.py b/tests/unit/llms/anthropic/test_anthropic_common_utils.py index 230168b9216..f1d8d46216b 100644 --- a/tests/unit/llms/anthropic/test_anthropic_common_utils.py +++ b/tests/unit/llms/anthropic/test_anthropic_common_utils.py @@ -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)) diff --git a/tests/unit/llms/bedrock/messages/invoke_transformations/test_anthropic_claude3_transformation.py b/tests/unit/llms/bedrock/messages/invoke_transformations/test_anthropic_claude3_transformation.py index cb12b39c0c9..dffe2c36e20 100644 --- a/tests/unit/llms/bedrock/messages/invoke_transformations/test_anthropic_claude3_transformation.py +++ b/tests/unit/llms/bedrock/messages/invoke_transformations/test_anthropic_claude3_transformation.py @@ -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))