fix(anthropic-messages): apply top-level additional_drop_params on /v1/messages

`async_anthropic_messages_handler` only honored nested (JSONPath) entries in
`additional_drop_params`; top-level keys were silently filtered out. Unlike
`/v1/chat/completions`, this path has no prior top-level drop step, so those
keys leaked all the way into the signed request body.

This broke Claude Code v2.1+ against Bedrock: the client automatically
enables the `context-management-2025-06-27` Anthropic beta, which injects a
top-level `context_management` field. Bedrock rejects it:

    {"message": "context_management: Extra inputs are not permitted"}

Setting `additional_drop_params: ["context_management"]` had no effect on
this code path, even though it works for `/v1/chat/completions`.

The fix dispatches by path type: nested paths go through
`delete_nested_value`, top-level paths through `dict.pop`. `utils.py` is
unchanged — its nested-only filter is intentional because the top-level
drop is already done earlier by `add_provider_specific_params_to_optional_params`.

Fixes #25931
This commit is contained in:
Llugaes 2026-04-23 17:32:41 +08:00
parent e9e86ed956
commit e05d677b70
2 changed files with 166 additions and 6 deletions

View file

@ -1966,7 +1966,9 @@ class BaseLLMHTTPHandler:
custom_llm_provider=custom_llm_provider,
)
# Apply additional_drop_params for nested field removal
# Apply additional_drop_params — top-level and nested fields.
# Unlike /v1/chat/completions, this path has no prior top-level drop
# step, so we must handle both here. See issue #25931.
additional_drop_params = litellm_params.get("additional_drop_params")
if additional_drop_params:
from litellm.litellm_core_utils.dot_notation_indexing import (
@ -1974,11 +1976,13 @@ class BaseLLMHTTPHandler:
is_nested_path,
)
nested_paths = [p for p in additional_drop_params if is_nested_path(p)]
for path in nested_paths:
anthropic_messages_optional_request_params = delete_nested_value(
anthropic_messages_optional_request_params, path
)
for path in additional_drop_params:
if is_nested_path(path):
anthropic_messages_optional_request_params = delete_nested_value(
anthropic_messages_optional_request_params, path
)
else:
anthropic_messages_optional_request_params.pop(path, None)
# Prepare request body
request_body = anthropic_messages_provider_config.transform_anthropic_messages_request(

View file

@ -0,0 +1,156 @@
"""
Regression tests for issue #25931.
`async_anthropic_messages_handler` (the /v1/messages Bedrock/Anthropic path)
must honor `additional_drop_params` for both top-level and nested field names.
Before the fix, only nested paths (containing `.` or `[`) were dropped, so
top-level fields like `context_management` injected automatically by
Claude Code v2.1+ via the `context-management-2025-06-27` Anthropic beta
leaked through to Bedrock and produced:
{"message": "context_management: Extra inputs are not permitted"}
"""
import os
import sys
from unittest.mock import MagicMock
import pytest
sys.path.insert(0, os.path.abspath("../../../.."))
from litellm.llms.custom_httpx.llm_http_handler import BaseLLMHTTPHandler
class _StopAfterDrop(Exception):
"""Sentinel raised by the mocked transform to halt the handler after drop."""
def _build_provider_config_mock(captured: dict) -> MagicMock:
provider_config = MagicMock()
provider_config.validate_anthropic_messages_environment.return_value = (
{},
"http://test-base",
)
def _capture_transform(**kwargs):
captured["optional_params"] = kwargs[
"anthropic_messages_optional_request_params"
]
raise _StopAfterDrop
provider_config.transform_anthropic_messages_request.side_effect = (
_capture_transform
)
return provider_config
def _build_logging_obj_mock() -> MagicMock:
logging_obj = MagicMock()
logging_obj.model_call_details = {}
return logging_obj
@pytest.mark.asyncio
async def test_top_level_additional_drop_params_applied_on_messages_path():
"""Top-level keys (e.g. `context_management`) must be dropped from
the params forwarded to the provider transform.
Regression for https://github.com/BerriAI/litellm/issues/25931."""
handler = BaseLLMHTTPHandler()
captured: dict = {}
params = {
"context_management": {
"edits": [{"type": "clear_thinking_20251015"}],
},
"max_tokens": 1024,
}
litellm_params = {"additional_drop_params": ["context_management"]}
with pytest.raises(_StopAfterDrop):
await handler.async_anthropic_messages_handler(
model="us.anthropic.claude-sonnet-4-6",
messages=[{"role": "user", "content": "hi"}],
anthropic_messages_provider_config=_build_provider_config_mock(captured),
anthropic_messages_optional_request_params=params,
custom_llm_provider="bedrock",
litellm_params=litellm_params,
logging_obj=_build_logging_obj_mock(),
)
forwarded = captured["optional_params"]
assert "context_management" not in forwarded, (
"top-level `context_management` should be stripped from params before "
"transform_anthropic_messages_request — otherwise Bedrock returns 400"
)
assert forwarded["max_tokens"] == 1024, "unrelated fields must be preserved"
@pytest.mark.asyncio
async def test_nested_additional_drop_params_still_work():
"""Nested JSONPath entries must continue to work alongside top-level drops."""
handler = BaseLLMHTTPHandler()
captured: dict = {}
params = {
"tools": [
{"name": "t0", "input_examples": ["ex0"]},
{"name": "t1", "input_examples": ["ex1"]},
],
"max_tokens": 1024,
}
litellm_params = {"additional_drop_params": ["tools[*].input_examples"]}
with pytest.raises(_StopAfterDrop):
await handler.async_anthropic_messages_handler(
model="us.anthropic.claude-sonnet-4-6",
messages=[{"role": "user", "content": "hi"}],
anthropic_messages_provider_config=_build_provider_config_mock(captured),
anthropic_messages_optional_request_params=params,
custom_llm_provider="bedrock",
litellm_params=litellm_params,
logging_obj=_build_logging_obj_mock(),
)
forwarded = captured["optional_params"]
assert forwarded["max_tokens"] == 1024
assert [t["name"] for t in forwarded["tools"]] == ["t0", "t1"]
for tool in forwarded["tools"]:
assert "input_examples" not in tool
@pytest.mark.asyncio
async def test_mixed_top_level_and_nested_drop_params():
"""Top-level and nested drops specified in the same list should both apply."""
handler = BaseLLMHTTPHandler()
captured: dict = {}
params = {
"context_management": {"edits": []},
"tools": [{"name": "t", "input_examples": ["ex"]}],
"max_tokens": 1024,
}
litellm_params = {
"additional_drop_params": [
"context_management",
"tools[*].input_examples",
],
}
with pytest.raises(_StopAfterDrop):
await handler.async_anthropic_messages_handler(
model="us.anthropic.claude-sonnet-4-6",
messages=[{"role": "user", "content": "hi"}],
anthropic_messages_provider_config=_build_provider_config_mock(captured),
anthropic_messages_optional_request_params=params,
custom_llm_provider="bedrock",
litellm_params=litellm_params,
logging_obj=_build_logging_obj_mock(),
)
forwarded = captured["optional_params"]
assert "context_management" not in forwarded
assert "input_examples" not in forwarded["tools"][0]
assert forwarded["max_tokens"] == 1024