fix(proxy)!: honor litellm_settings.drop_params for Claude Code and Codex requests (#49058)

* fix(proxy): honor litellm_settings.drop_params for Claude Code and Codex requests

The agentic CLI drop_params default checked the operator's config with
isinstance(config, dict), but ProxyConfig.config is a MappingProxyType, so the
check never matched and litellm_settings.drop_params could not turn the
default off. Read it as a Mapping so an operator who sets drop_params, true or
false, keeps the decision on /v1/messages, chat, and count_tokens

* fix(proxy): keep the Claude Code and Codex drop_params default unless the admin's value reads as false

A drop_params: true saved through /config/update never reaches the global setting, so treating the key's presence as an opt-out made Claude Code and Codex requests answer 400 there. Only a value that reads as false now turns the default off; true, empty, or missing keeps it.

* test(proxy): cover string drop_params values in the Claude Code and Codex default rows

Adds rows for "false", " False ", "true" and an unreadable "maybe" under litellm_settings, so swapping the value check for a plain is-not-False comparison fails the suite

* docs(proxy): say only a false operator drop_params turns off the agentic CLI default

---------

Co-authored-by: mateo-berri <277851410+mateo-berri@users.noreply.github.com>
This commit is contained in:
simonp9 2026-10-29 11:11:07 -07:00 • committed by GitHub
parent 4a4c9cf7e3
commit 2e5cc9e08f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 56 additions and 27 deletions

View file

@ -49,7 +49,7 @@ from litellm.constants import (
X_LITELLM_DISABLE_CALLBACKS,
)
from litellm.integrations.custom_guardrail import without_server_streaming_classification
from litellm.litellm_core_utils.core_helpers import is_codex_user_agent
from litellm.litellm_core_utils.core_helpers import is_codex_user_agent, normalize_drop_params
from litellm.litellm_core_utils.credential_accessor import CredentialAccessor
from litellm.litellm_core_utils.initialize_dynamic_callback_params import (
TRUSTED_CALLBACK_VARS_FIELD,
@ -1102,18 +1102,17 @@ def should_auto_drop_params_for_agentic_cli(
) -> bool:
"""drop_params defaults to on for agentic CLIs so their client-specific
params (e.g. Claude Code's thinking, Codex's service_tier) don't fail
requests routed to providers that reject them. An explicit drop_params
from the caller or in the operator's ``litellm_settings`` always wins
over this default."""
requests routed to providers that reject them. A drop_params from the
caller wins over this default, and the operator's ``litellm_settings``
turns it off only with a value that reads as false."""
from litellm.llms.anthropic.common_utils import is_claude_code_user_agent
if not (is_claude_code_user_agent(user_agent) or is_codex_user_agent(user_agent)):
return False
if caller_set_drop_params:
return False
config: Final = getattr(proxy_config, "config", None)
litellm_settings: Final = config.get("litellm_settings") if isinstance(config, dict) else None
return not (isinstance(litellm_settings, dict) and "drop_params" in litellm_settings)
litellm_settings: Final = _request_fields(proxy_config.config.get("litellm_settings"))
return normalize_drop_params(litellm_settings.get("drop_params")) is not False
def safe_add_api_version_from_query_params(data: dict, request: Request):

View file

@ -695,6 +695,32 @@ async def test_count_tokens_drops_malformed_tools_when_drop_params_is_set(
assert counted.messages == messages
@pytest.mark.asyncio
async def test_count_tokens_keeps_claude_code_malformed_tools_when_operator_sets_drop_params_false() -> None:
import litellm.proxy.anthropic_endpoints.endpoints as ep
import litellm.proxy.proxy_server as proxy_server
from litellm.proxy._types import UserAPIKeyAuth
operator_config: Final = proxy_server.ProxyConfig()
operator_config.update_config_state(config={"litellm_settings": {"drop_params": False}})
request: Final = MagicMock()
request.headers = {"user-agent": "claude-cli/2.1.0 (external, cli)"}
counter: Final = AsyncMock(return_value=_SEVEN_TOKENS)
body: Final = {"model": "claude-sonnet", "messages": [{"role": "user", "content": "hi"}], "tools": 5}
with (
patch.object(ep, "read_request_body", new=AsyncMock(return_value=body)), # test-quality-ok: endpoint reads the body via a module function; no injection seam
patch.object(proxy_server, "count_request_tokens", new=counter), # test-quality-ok: module global imported at call time; the test asserts it is never reached
patch.object(proxy_server, "llm_router", new=_claude_router()), # test-quality-ok: module global imported at call time; no injection seam on this route
patch.object(proxy_server, "proxy_config", new=operator_config), # test-quality-ok: module global imported at call time; no injection seam on this route
):
response: Final = await ep.count_tokens(request=request, user_api_key_dict=UserAPIKeyAuth())
assert response.status_code == 400
assert json.loads(response.body)["error"]["type"] == "invalid_request_error"
counter.assert_not_awaited()
@pytest.mark.asyncio
async def test_count_tokens_drops_malformed_tools_the_counted_deployment_drops_even_with_fallbacks() -> None:
import litellm.proxy.anthropic_endpoints.endpoints as ep

View file

@ -7024,24 +7024,30 @@ def _make_request_mock(path: str, headers: dict) -> MagicMock:
@pytest.mark.asyncio
@pytest.mark.parametrize(
"user_agent, request_drop_params, operator_drop_params, expected_drop_params",
"user_agent, request_drop_params, operator_settings, expected_drop_params",
[
("claude-cli/2.0.69 (external, cli)", None, None, True),
("claude-cli/1.0.44 (external, sdk-py)", None, None, True),
("claude-cli/2.0.69 (external, cli)", False, None, False),
("claude-cli/2.0.69 (external, cli)", None, False, None),
("claude-cli/2.0.69 (external, cli)", None, True, None),
("codex_cli_rs/0.144.5 (Mac OS 26.4.0; arm64) WezTerm", None, None, True),
("codex_exec/0.144.5 (Mac OS 26.4.0; arm64) WarpTerminal (codex_exec; 0.144.5)", None, None, True),
("codex_vscode/0.144.5 (Mac OS 26.4.0; arm64) vscode/1.104.1", None, None, True),
("codex_exec/0.144.5 (Mac OS 26.4.0; arm64)", False, None, False),
("codex_exec/0.144.5 (Mac OS 26.4.0; arm64)", None, True, None),
("PostmanRuntime/7.53.0", None, None, None),
(None, None, None, None),
("claude-cli/2.0.69 (external, cli)", None, {}, True),
("claude-cli/1.0.44 (external, sdk-py)", None, {}, True),
("claude-cli/2.0.69 (external, cli)", False, {}, False),
("claude-cli/2.0.69 (external, cli)", None, {"drop_params": False}, None),
("claude-cli/2.0.69 (external, cli)", None, {"drop_params": True}, True),
("claude-cli/2.0.69 (external, cli)", None, {"drop_params": None}, True),
("claude-cli/2.0.69 (external, cli)", None, {"drop_params": "false"}, None),
("claude-cli/2.0.69 (external, cli)", None, {"drop_params": "maybe"}, True),
("codex_cli_rs/0.144.5 (Mac OS 26.4.0; arm64) WezTerm", None, {}, True),
("codex_exec/0.144.5 (Mac OS 26.4.0; arm64) WarpTerminal (codex_exec; 0.144.5)", None, {}, True),
("codex_vscode/0.144.5 (Mac OS 26.4.0; arm64) vscode/1.104.1", None, {}, True),
("codex_exec/0.144.5 (Mac OS 26.4.0; arm64)", False, {}, False),
("codex_exec/0.144.5 (Mac OS 26.4.0; arm64)", None, {"drop_params": False}, None),
("codex_exec/0.144.5 (Mac OS 26.4.0; arm64)", None, {"drop_params": True}, True),
("codex_exec/0.144.5 (Mac OS 26.4.0; arm64)", None, {"drop_params": " False "}, None),
("codex_exec/0.144.5 (Mac OS 26.4.0; arm64)", None, {"drop_params": "true"}, True),
("PostmanRuntime/7.53.0", None, {}, None),
(None, None, {}, None),
],
)
async def test_add_litellm_data_to_request_agentic_cli_drop_params(
user_agent, request_drop_params, operator_drop_params, expected_drop_params
user_agent, request_drop_params, operator_settings, expected_drop_params
):
"""Claude Code sends Anthropic-specific params and Codex sends
service_tier, both of which fail on providers that reject them, so those
@ -7058,12 +7064,10 @@ async def test_add_litellm_data_to_request_agentic_cli_drop_params(
if request_drop_params is not None:
data["drop_params"] = request_drop_params
proxy_config = MagicMock()
proxy_config.config = (
{"litellm_settings": {"drop_params": operator_drop_params}}
if operator_drop_params is not None
else {"litellm_settings": {}}
)
from litellm.proxy.proxy_server import ProxyConfig
proxy_config = ProxyConfig()
proxy_config.update_config_state(config={"litellm_settings": operator_settings})
updated = await add_litellm_data_to_request(
data=data,