diff --git a/litellm/llms/anthropic/experimental_pass_through/adapters/handler.py b/litellm/llms/anthropic/experimental_pass_through/adapters/handler.py index 87a29ca50ba..53795108a91 100644 --- a/litellm/llms/anthropic/experimental_pass_through/adapters/handler.py +++ b/litellm/llms/anthropic/experimental_pass_through/adapters/handler.py @@ -34,8 +34,18 @@ if TYPE_CHECKING: from litellm.proxy._types import UserAPIKeyAuth from litellm.router import Router -# Anthropic-only keys already mapped by the translator; strip on extra_kwargs re-merge. -ANTHROPIC_ONLY_REQUEST_KEYS: Final[frozenset[str]] = frozenset({"output_config"}) +# Anthropic-only keys to strip on extra_kwargs re-merge: +# - "output_config": already mapped by the translator (output_config.effort -> +# reasoning_effort, output_config.format -> response_format). +# - "diagnostics": a client-diagnostics-only field (sent by Claude Code) with no +# completion()/OpenAI equivalent. Unlike the native Anthropic Messages passthrough +# (AnthropicMessagesConfig.transform_anthropic_messages_request), which only forwards +# keys declared on AnthropicMessagesRequestOptionalParams and so drops it silently, +# this bridge re-merges every extra kwarg the translator didn't consume. Forwarding +# it verbatim reaches non-Anthropic backends (e.g. Bedrock Converse via +# additionalModelRequestFields) as an unrecognized field and 400s with "Extra inputs +# are not permitted". +ANTHROPIC_ONLY_REQUEST_KEYS: Final[frozenset[str]] = frozenset({"output_config", "diagnostics"}) _AnthropicMessages: TypeAlias = "list[dict[str, object]]" _AnthropicSystem: TypeAlias = "str | list[dict[str, object]] | None" diff --git a/tests/test_litellm/llms/anthropic/experimental_pass_through/adapters/test_handler_output_config_passthrough.py b/tests/test_litellm/llms/anthropic/experimental_pass_through/adapters/test_handler_output_config_passthrough.py index a944afc6152..f1431d1d17e 100644 --- a/tests/test_litellm/llms/anthropic/experimental_pass_through/adapters/test_handler_output_config_passthrough.py +++ b/tests/test_litellm/llms/anthropic/experimental_pass_through/adapters/test_handler_output_config_passthrough.py @@ -237,3 +237,50 @@ class TestPromptCacheOptionsForwarded: result = _call_prepare(extra_kwargs={"prompt_cache_options": {"mode": "explicit"}}, model="gpt-5.6") completion_kwargs = result[0] if isinstance(result, tuple) else result assert completion_kwargs["prompt_cache_options"] == {"mode": "explicit"} + + +class TestDiagnosticsStrippedFromCompletionKwargs: + """``diagnostics`` (sent by Claude Code as a client-diagnostics-only field, + with no completion()/OpenAI equivalent) must not survive the + post-translation re-merge into ``completion_kwargs`` — non-Anthropic + backends reached via this bridge (e.g. Bedrock Converse via + ``additionalModelRequestFields``) reject an unrecognized field with 400 + "Extra inputs are not permitted". Unlike the native Anthropic Messages + passthrough (``AnthropicMessagesConfig.transform_anthropic_messages_request``), + which only forwards keys declared on + ``AnthropicMessagesRequestOptionalParams`` and so drops ``diagnostics`` + silently, this bridge re-merges every extra kwarg the translator didn't + consume, so the strip has to be explicit here too.""" + + def test_diagnostics_is_stripped(self): + extra_kwargs = { + "custom_llm_provider": "bedrock", + "diagnostics": {"trace_id": "abc123"}, + } + + result = _call_prepare(extra_kwargs=extra_kwargs) + completion_kwargs = result[0] if isinstance(result, tuple) else result + + assert "diagnostics" not in completion_kwargs, ( + "Raw diagnostics must not be forwarded — non-Anthropic backends " + "reject it with 400 'Extra inputs are not permitted'" + ) + + def test_contains_diagnostics(self): + assert "diagnostics" in ANTHROPIC_ONLY_REQUEST_KEYS + + def test_other_extra_kwargs_still_passed_through_alongside_diagnostics(self): + """Regression guard: the strip must be narrow. Unrelated fields like + ``timeout`` continue to flow through even when ``diagnostics`` is + also present.""" + extra_kwargs = { + "custom_llm_provider": "bedrock", + "diagnostics": {"trace_id": "abc123"}, + "timeout": 30, + } + + result = _call_prepare(extra_kwargs=extra_kwargs) + completion_kwargs = result[0] if isinstance(result, tuple) else result + + assert "diagnostics" not in completion_kwargs + assert completion_kwargs.get("timeout") == 30