mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-10 22:41:41 +00:00
fix(passthrough): drop top-level additional_drop_params on /v1/messages (#31645)
* fix(passthrough): drop top-level additional_drop_params on /v1/messages On the Anthropic Messages pass-through path, additional_drop_params only stripped nested dotted paths, so plain top-level keys like `thinking` and `context_management` were forwarded to the provider. Bedrock rejects these with "Extra inputs are not permitted", returning a 400 to Claude App/CLI even when the user configured `additional_drop_params: ["thinking"]`. delete_nested_value already handles plain top-level fields, so route every drop param through it and remove the nested-only filter. Fixes #25931. * fix(passthrough): drop thinking for bedrock inference-profile ARNs on /v1/messages Opaque Bedrock Application Inference Profile ARNs contain neither "anthropic" nor "claude", so is_anthropic_claude_model returned False and the thinking param was rewritten to reasoning_effort before additional_drop_params ran. That made additional_drop_params: ["thinking"] a no-op for the converse-ARN form, and the Bedrock Converse transform re-expanded reasoning_effort back into additionalModelRequestFields.thinking, so the request 400'd. Extend the thinking-translation gates to also accept bedrock ARNs via the existing is_bedrock_arn_model helper, mirroring the cache_control path, so thinking is preserved as thinking and additional_drop_params can drop it.
This commit is contained in:
parent
ddba2e2b15
commit
26ee5dd597
4 changed files with 124 additions and 10 deletions
|
|
@ -673,7 +673,9 @@ class LiteLLMAnthropicMessagesAdapter:
|
|||
Returns:
|
||||
Dict with either 'thinking' or 'reasoning_effort' key
|
||||
"""
|
||||
if LiteLLMAnthropicMessagesAdapter.is_anthropic_claude_model(model):
|
||||
if LiteLLMAnthropicMessagesAdapter.is_anthropic_claude_model(
|
||||
model
|
||||
) or LiteLLMAnthropicMessagesAdapter.is_bedrock_arn_model(model):
|
||||
return {"thinking": thinking}
|
||||
else:
|
||||
reasoning_effort = LiteLLMAnthropicMessagesAdapter.translate_anthropic_thinking_to_reasoning_effort(
|
||||
|
|
@ -965,7 +967,7 @@ class LiteLLMAnthropicMessagesAdapter:
|
|||
return
|
||||
|
||||
model = new_kwargs.get("model", "")
|
||||
if self.is_anthropic_claude_model(model):
|
||||
if self.is_anthropic_claude_model(model) or self.is_bedrock_arn_model(model):
|
||||
new_kwargs["thinking"] = thinking # type: ignore
|
||||
return
|
||||
|
||||
|
|
|
|||
|
|
@ -2001,16 +2001,11 @@ class BaseLLMHTTPHandler:
|
|||
custom_llm_provider=custom_llm_provider,
|
||||
)
|
||||
|
||||
# Apply additional_drop_params for nested field removal
|
||||
additional_drop_params = litellm_params.get("additional_drop_params")
|
||||
additional_drop_params: list[str] = litellm_params.get("additional_drop_params") or []
|
||||
if additional_drop_params:
|
||||
from litellm.litellm_core_utils.dot_notation_indexing import (
|
||||
delete_nested_value,
|
||||
is_nested_path,
|
||||
)
|
||||
from litellm.litellm_core_utils.dot_notation_indexing import delete_nested_value
|
||||
|
||||
nested_paths = [p for p in additional_drop_params if is_nested_path(p)]
|
||||
for path in nested_paths:
|
||||
for path in additional_drop_params:
|
||||
anthropic_messages_optional_request_params = delete_nested_value(
|
||||
anthropic_messages_optional_request_params, path
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1508,6 +1508,44 @@ def test_cache_control_fix_does_not_broaden_claude_detection():
|
|||
)
|
||||
|
||||
|
||||
def test_thinking_preserved_for_bedrock_arn_inference_profile():
|
||||
"""
|
||||
Regression: opaque Bedrock Application Inference Profile ARNs hide the underlying
|
||||
Claude model name, so on /v1/messages a `thinking` param must be preserved as
|
||||
`thinking` (not rewritten to `reasoning_effort`). Otherwise `additional_drop_params:
|
||||
["thinking"]` runs after the rewrite and has nothing left to drop, and the Bedrock
|
||||
Converse body re-expands reasoning_effort back into additionalModelRequestFields.thinking.
|
||||
"""
|
||||
adapter = LiteLLMAnthropicMessagesAdapter()
|
||||
thinking = {"type": "enabled", "budget_tokens": 1024}
|
||||
|
||||
new_kwargs = {"model": CACHE_CONTROL_BEDROCK_ARN_MODEL}
|
||||
adapter._translate_thinking_to_openai(cast(Any, {"thinking": thinking}), cast(Any, new_kwargs))
|
||||
|
||||
assert new_kwargs["thinking"] == thinking
|
||||
assert "reasoning_effort" not in new_kwargs
|
||||
|
||||
assert LiteLLMAnthropicMessagesAdapter.translate_thinking_for_model(thinking, CACHE_CONTROL_BEDROCK_ARN_MODEL) == {
|
||||
"thinking": thinking
|
||||
}
|
||||
|
||||
|
||||
def test_thinking_still_translated_to_reasoning_effort_for_non_claude_model():
|
||||
"""
|
||||
The bedrock-ARN gate must not broaden to every model: a genuine non-Claude model
|
||||
still has `thinking` converted to `reasoning_effort` so it does not hit an
|
||||
UnsupportedParamsError downstream.
|
||||
"""
|
||||
adapter = LiteLLMAnthropicMessagesAdapter()
|
||||
thinking = {"type": "enabled", "budget_tokens": 1024}
|
||||
|
||||
new_kwargs = {"model": CACHE_CONTROL_NON_ANTHROPIC_MODEL}
|
||||
adapter._translate_thinking_to_openai(cast(Any, {"thinking": thinking}), cast(Any, new_kwargs))
|
||||
|
||||
assert "thinking" not in new_kwargs
|
||||
assert new_kwargs["reasoning_effort"] == "low"
|
||||
|
||||
|
||||
def test_cache_control_preserved_in_image_content_for_claude():
|
||||
"""Cache control should be preserved in image content for Claude models."""
|
||||
anthropic_messages = [
|
||||
|
|
|
|||
|
|
@ -645,6 +645,85 @@ async def test_async_anthropic_messages_handler_header_priority():
|
|||
assert captured_headers["X-Provider-Only"] == "keep-this-too"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_async_anthropic_messages_handler_drops_top_level_and_nested_params():
|
||||
"""
|
||||
Regression for LIT-3988 / GitHub #25931: on the /v1/messages path,
|
||||
additional_drop_params must strip plain top-level keys (e.g. `thinking`,
|
||||
`context_management`) before the provider transform runs, not only nested
|
||||
dotted paths. Bedrock rejects these fields, so leaving them in produces a 400.
|
||||
"""
|
||||
handler = BaseLLMHTTPHandler()
|
||||
|
||||
mock_config = Mock()
|
||||
mock_config.validate_anthropic_messages_environment = Mock(
|
||||
return_value=({"x-api-key": "test-key"}, "https://api.anthropic.com")
|
||||
)
|
||||
|
||||
captured = {}
|
||||
|
||||
def capture_transform(*args, **kwargs):
|
||||
captured["optional_params"] = kwargs["anthropic_messages_optional_request_params"]
|
||||
return {"model": "claude-opus-4-7", "messages": []}
|
||||
|
||||
mock_config.transform_anthropic_messages_request = capture_transform
|
||||
|
||||
mock_client = AsyncMock()
|
||||
mock_response = Mock()
|
||||
mock_response.status_code = 200
|
||||
mock_response.json.return_value = {
|
||||
"id": "msg_123",
|
||||
"type": "message",
|
||||
"role": "assistant",
|
||||
"content": [{"type": "text", "text": "Hello!"}],
|
||||
"model": "claude-opus-4-7",
|
||||
"stop_reason": "end_turn",
|
||||
}
|
||||
mock_client.post = AsyncMock(return_value=mock_response)
|
||||
|
||||
mock_logging_obj = Mock()
|
||||
mock_logging_obj.update_from_kwargs = Mock()
|
||||
mock_logging_obj.model_call_details = {}
|
||||
mock_logging_obj.stream = False
|
||||
|
||||
optional_params = {
|
||||
"max_tokens": 1024,
|
||||
"thinking": {"type": "enabled", "budget_tokens": 2048},
|
||||
"context_management": {"edits": [{"type": "clear_thinking_20251015"}]},
|
||||
"metadata": {"user_id": "u1", "drop_me": "x"},
|
||||
}
|
||||
|
||||
with patch(
|
||||
"litellm.litellm_core_utils.get_provider_specific_headers.ProviderSpecificHeaderUtils.get_provider_specific_headers"
|
||||
) as mock_provider_headers:
|
||||
mock_provider_headers.return_value = None
|
||||
try:
|
||||
await handler.async_anthropic_messages_handler(
|
||||
model="claude-opus-4-7",
|
||||
messages=[{"role": "user", "content": "Hello"}],
|
||||
anthropic_messages_provider_config=mock_config,
|
||||
anthropic_messages_optional_request_params=optional_params,
|
||||
custom_llm_provider="bedrock",
|
||||
litellm_params=GenericLiteLLMParams(
|
||||
additional_drop_params=[
|
||||
"thinking",
|
||||
"context_management",
|
||||
"metadata.drop_me",
|
||||
]
|
||||
),
|
||||
logging_obj=mock_logging_obj,
|
||||
client=mock_client,
|
||||
)
|
||||
except Exception:
|
||||
pass # drop runs before the mocked sign_request; the capture is what we assert on
|
||||
|
||||
transformed = captured["optional_params"]
|
||||
assert "thinking" not in transformed
|
||||
assert "context_management" not in transformed
|
||||
assert transformed["max_tokens"] == 1024
|
||||
assert transformed["metadata"] == {"user_id": "u1"}
|
||||
|
||||
|
||||
def test_google_genai_streaming_hidden_params_model_info_and_router_fallback():
|
||||
logging_obj = Mock()
|
||||
logging_obj.get_router_model_id = Mock(return_value="router-model-id")
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue