diff --git a/litellm/integrations/shadow_eval_logger.py b/litellm/integrations/shadow_eval_logger.py index 3a2d7ee33bb..8db9535ee9f 100644 --- a/litellm/integrations/shadow_eval_logger.py +++ b/litellm/integrations/shadow_eval_logger.py @@ -134,7 +134,9 @@ def _chat_request_from_anthropic_messages( ) ) translated, _ = adapter.translate_anthropic_to_openai(request) # pyright: ignore[reportArgumentType] # wire-body mapping is the surface's native request shape; the adapter is duck-typed and read-only here - return translated + if request.get("thinking"): + return translated + return MappingProxyType({k: v for k, v in translated.items() if k != "output_config"}) def _chat_request_from_responses( diff --git a/litellm/llms/anthropic/messages/adapters/handler.py b/litellm/llms/anthropic/messages/adapters/handler.py index 6658e3fd897..1b7e94061ec 100644 --- a/litellm/llms/anthropic/messages/adapters/handler.py +++ b/litellm/llms/anthropic/messages/adapters/handler.py @@ -607,6 +607,7 @@ class LiteLLMMessagesToCompletionTransformationHandler: from litellm.litellm_core_utils.litellm_logging import ( Logging as LiteLLMLoggingObject, ) + from litellm.responses.bridge_check import str_keyed_mapping request_data: Final[dict[str, object]] = { "model": model, @@ -646,7 +647,8 @@ class LiteLLMMessagesToCompletionTransformationHandler: request_data["output_config"] = extra_kwargs["output_config"] custom_llm_provider: Final = extra_kwargs.get("custom_llm_provider") - base_model: Final = extra_kwargs.get("base_model") + model_info: Final = str_keyed_mapping(extra_kwargs.get("model_info")) + base_model: Final = extra_kwargs.get("base_model") or (model_info.get("base_model") if model_info else None) ( openai_request, tool_name_mapping, diff --git a/tests/unit/integrations/test_shadow_eval_logger.py b/tests/unit/integrations/test_shadow_eval_logger.py index 55796f7bbcf..6dc404f0254 100644 --- a/tests/unit/integrations/test_shadow_eval_logger.py +++ b/tests/unit/integrations/test_shadow_eval_logger.py @@ -405,6 +405,34 @@ class TestSurfaceNormalization: assert "stream" not in shadow_call assert shadow_call["metadata"][INTERNAL_CALL_ORIGIN_METADATA_KEY] == SHADOW_EVAL_ROUTER_CALL_ORIGIN + @pytest.mark.parametrize( + ("thinking", "forwards_effort"), + [ + pytest.param(None, False, id="no_thinking"), + pytest.param({"type": "enabled", "budget_tokens": 1024}, True, id="thinking"), + ], + ) + async def test_anthropic_arm_sends_a_bedrock_claude_effort_to_the_shadow_model_only_with_thinking( + self, thinking: Mapping[str, object] | None, forwards_effort: bool + ): + hook_kwargs = _success_kwargs(call_type="anthropic_messages") + hook_kwargs["model"] = "converse/us.anthropic.claude-sonnet-4-6" + hook_kwargs["messages"] = [{"role": "user", "content": [{"type": "text", "text": "hi"}]}] + hook_kwargs["litellm_params"]["proxy_server_request"] = { + "body": { + "model": "converse-sonnet", + "messages": [], + "max_tokens": 2048, + "output_config": {"effort": "high"}, + **({"thinking": thinking} if thinking else {}), + } + } + + _, router = await self._drive(hook_kwargs, RESPONSE) + + shadow_call = router.acompletion.call_args_list[0].kwargs + assert shadow_call.get("output_config") == ({"effort": "high"} if forwards_effort else None) + async def test_responses_arm_translates_wire_body_params_and_drops_surface_only_keys(self): from litellm.types.llms.openai import ResponsesAPIResponse diff --git a/tests/unit/llms/anthropic/messages/adapters/test_anthropic_messages_adapters_transformation.py b/tests/unit/llms/anthropic/messages/adapters/test_anthropic_messages_adapters_transformation.py index 9808b2dfb8e..05f6e61db0c 100644 --- a/tests/unit/llms/anthropic/messages/adapters/test_anthropic_messages_adapters_transformation.py +++ b/tests/unit/llms/anthropic/messages/adapters/test_anthropic_messages_adapters_transformation.py @@ -5560,19 +5560,28 @@ _APPLICATION_INFERENCE_PROFILE: Final = ( @pytest.mark.asyncio @pytest.mark.parametrize( - ("thinking", "drop_params", "base_model", "forwards_effort"), + ("thinking", "drop_params", "deployment_kwargs", "forwards_effort"), [ - pytest.param(None, False, None, True, id="no_thinking"), - pytest.param(None, True, None, False, id="no_thinking_drop_params"), - pytest.param({"type": "enabled", "budget_tokens": 1024}, True, None, True, id="thinking_drop_params"), - pytest.param(None, True, _CONVERSE_EFFORT_MODEL, True, id="no_thinking_drop_params_base_model"), + pytest.param(None, False, {}, True, id="no_thinking"), + pytest.param(None, True, {}, False, id="no_thinking_drop_params"), + pytest.param({"type": "enabled", "budget_tokens": 1024}, True, {}, True, id="thinking_drop_params"), + pytest.param( + None, True, {"base_model": _CONVERSE_EFFORT_MODEL}, True, id="no_thinking_drop_params_base_model" + ), + pytest.param( + None, + True, + {"model_info": {"base_model": _CONVERSE_EFFORT_MODEL}}, + True, + id="no_thinking_drop_params_model_info_base_model", + ), ], ) async def test_bedrock_application_inference_profile_loses_the_effort_only_when_unresolved_without_thinking( _converse_route: respx.Route, thinking: Mapping[str, object] | None, drop_params: bool, - base_model: str | None, + deployment_kwargs: Mapping[str, object], forwards_effort: bool, ) -> None: await litellm.anthropic.messages.acreate( @@ -5582,7 +5591,7 @@ async def test_bedrock_application_inference_profile_loses_the_effort_only_when_ output_config={"effort": "high", "format": _CITY_FORMAT}, drop_params=drop_params, **({"thinking": thinking} if thinking else {}), - **({"base_model": base_model} if base_model else {}), + **deployment_kwargs, ) sent: Final = _sent_converse_fields(_converse_route)