This commit is contained in:
hcl 2026-09-24 23:27:50 +00:00 • committed by GitHub
commit 004c2c4385
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 92 additions and 42 deletions

View file

@ -675,12 +675,29 @@ def filter_exceptions_from_params(data: object, max_depth: int = 20) -> Any:
return data
# MCP handler plumbing — internal params that must never be forwarded as
# provider kwargs. Also registered in `all_litellm_params` (the repo-wide param
# registry, per review), but the filter is scoped to this subset: callers like
# fallback_utils pass the result straight back into `litellm.acompletion`, so
# filtering the full `all_litellm_params` (api_key/num_retries/...) would break
# the call, and knobs like stream_chunk_size are read downstream (converse
# streaming) so they are not internal. See #30301.
MCP_INTERNAL_REQUEST_KEYS: Final = frozenset(
(
"skip_mcp_handler",
"mcp_handler_context",
"_skip_mcp_handler",
)
)
def filter_internal_params(data: dict, additional_internal_params: set | None = None) -> dict:
"""
Filter out LiteLLM internal parameters that shouldn't be sent to provider APIs.
This removes internal/MCP-related parameters that are used by LiteLLM internally
but should not be included in API requests to providers.
The base set is `MCP_INTERNAL_REQUEST_KEYS` (pure plumbing, also registered in
`all_litellm_params`); callers may pass extra names via
`additional_internal_params` for provider-specific knobs.
Args:
data: Dictionary of parameters to filter
@ -692,18 +709,8 @@ def filter_internal_params(data: dict, additional_internal_params: set | None =
if not isinstance(data, dict):
return data
# Known internal parameters that should never be sent to provider APIs
internal_params: Final = {
"skip_mcp_handler",
"mcp_handler_context",
"_skip_mcp_handler",
}
internal_params: Final = MCP_INTERNAL_REQUEST_KEYS | frozenset(additional_internal_params or ())
# Add any additional internal params if provided
if additional_internal_params:
internal_params.update(additional_internal_params)
# Filter out internal parameters
return {k: v for k, v in data.items() if k not in internal_params}

View file

@ -4095,6 +4095,10 @@ all_litellm_params = (
"adaptive_router_default_model",
"quality_router_config",
"quality_router_default_model",
# MCP handler plumbing — internal, must never leak into a provider body (#30301)
"skip_mcp_handler",
"mcp_handler_context",
"_skip_mcp_handler",
]
+ list(StandardCallbackDynamicParams.__annotations__.keys())
+ list(CustomPricingLiteLLMParams.model_fields.keys())

View file

@ -7,11 +7,13 @@ import pytest
from litellm.litellm_core_utils.core_helpers import (
_FINISH_REASON_MAP,
MCP_INTERNAL_REQUEST_KEYS,
RESPONSE_COST_HEADER,
bind_budget_reservation_to_callbacks,
budget_reservation_from_metadata,
drop_params_env_flag,
drop_params_flag,
filter_internal_params,
get_or_create_metadata_bucket,
get_provider_response_headers_from_hidden_params,
map_finish_reason,
@ -22,7 +24,11 @@ from litellm.litellm_core_utils.core_helpers import (
unbind_budget_reservation_from_callbacks,
)
from litellm.proxy._types import UserAPIKeyAuth
from litellm.types.utils import ImageResponse, TranscriptionResponse
from litellm.types.utils import (
ImageResponse,
TranscriptionResponse,
all_litellm_params,
)
class TestBudgetReservationBinding:
@ -183,9 +189,7 @@ class TestMapFinishReasonAnthropic:
("content_filtered", "content_filter"),
],
)
def test_anthropic_finish_reasons(
self, provider_reason: str, expected: str
) -> None:
def test_anthropic_finish_reasons(self, provider_reason: str, expected: str) -> None:
assert map_finish_reason(provider_reason) == expected
def test_refusal(self):
@ -256,9 +260,7 @@ class TestMapFinishReasonZhipu:
class TestMapFinishReasonOpenAIPassthrough:
@pytest.mark.parametrize(
"reason", ["stop", "length", "tool_calls", "function_call", "content_filter"]
)
@pytest.mark.parametrize("reason", ["stop", "length", "tool_calls", "function_call", "content_filter"])
def test_openai_values_pass_through(self, reason):
assert map_finish_reason(reason) == reason
@ -269,9 +271,7 @@ class TestMapFinishReasonGenericError:
assert map_finish_reason("error") == "stop"
def test_lowercase_error_does_not_warn(self, mocker):
warn = mocker.patch(
"litellm.litellm_core_utils.core_helpers.verbose_logger.warning"
)
warn = mocker.patch("litellm.litellm_core_utils.core_helpers.verbose_logger.warning")
assert map_finish_reason("error") == "stop"
warn.assert_not_called()
@ -289,8 +289,7 @@ class TestFinishReasonMapOutputsAreValid:
"""Every value in _FINISH_REASON_MAP must be a valid OpenAI finish reason."""
for provider_reason, openai_reason in _FINISH_REASON_MAP.items():
assert openai_reason in VALID_OPENAI_FINISH_REASONS, (
f"Mapped value '{openai_reason}' (from '{provider_reason}') "
f"is not a valid OpenAI finish reason"
f"Mapped value '{openai_reason}' (from '{provider_reason}') is not a valid OpenAI finish reason"
)
@ -300,28 +299,18 @@ class TestRedactNestedMatchAndRegexKeys:
"assessments": [
{
"sensitiveInformationPolicy": {
"piiEntities": [
{"type": "NAME", "match": "secret-name", "action": "BLOCKED"}
]
},
"wordPolicy": {
"customWords": [{"match": "badword", "action": "BLOCKED"}]
"piiEntities": [{"type": "NAME", "match": "secret-name", "action": "BLOCKED"}]
},
"wordPolicy": {"customWords": [{"match": "badword", "action": "BLOCKED"}]},
}
],
"regex": "should-redact-key-named-regex",
}
out = redact_nested_match_and_regex_keys(payload)
assert out["assessments"][0]["sensitiveInformationPolicy"]["piiEntities"][0][
"match"
] == "[REDACTED]"
assert out["assessments"][0]["wordPolicy"]["customWords"][0]["match"] == (
"[REDACTED]"
)
assert out["assessments"][0]["sensitiveInformationPolicy"]["piiEntities"][0]["match"] == "[REDACTED]"
assert out["assessments"][0]["wordPolicy"]["customWords"][0]["match"] == ("[REDACTED]")
assert out["regex"] == "[REDACTED]"
assert payload["assessments"][0]["sensitiveInformationPolicy"]["piiEntities"][
0
]["match"] == "secret-name"
assert payload["assessments"][0]["sensitiveInformationPolicy"]["piiEntities"][0]["match"] == "secret-name"
def test_passes_through_none_and_str(self):
assert redact_nested_match_and_regex_keys(None) is None
@ -484,13 +473,17 @@ class TestIsExpectedClientError:
assert is_expected_client_error(over_budget) is True
litellm_limit = RateLimitError(
message="key over rpm", llm_provider="anthropic", model="claude-haiku-4-5",
message="key over rpm",
llm_provider="anthropic",
model="claude-haiku-4-5",
category=RateLimitErrorCategory.LITELLM_RATE_LIMIT,
)
assert is_expected_client_error(litellm_limit) is True
vendor_limit = RateLimitError(
message="rate limited upstream", llm_provider="anthropic", model="claude-haiku-4-5",
message="rate limited upstream",
llm_provider="anthropic",
model="claude-haiku-4-5",
category=RateLimitErrorCategory.VENDOR_RATE_LIMIT,
)
assert is_expected_client_error(vendor_limit) is False
@ -557,3 +550,49 @@ class TestProviderResponseHeadersInHiddenParams:
assert get_provider_response_headers_from_hidden_params(sibling) is None
assert "additional_headers" not in sibling._hidden_params
class TestFilterInternalParams:
# MCP handler plumbing keys, also registered in all_litellm_params (#30301)
INTERNAL_KEYS = {
"skip_mcp_handler",
"mcp_handler_context",
"_skip_mcp_handler",
}
def test_registry_strips_every_known_internal_key(self):
seeded = {k: "leak" for k in self.INTERNAL_KEYS}
seeded["model"] = "gpt-4"
seeded["temperature"] = 0.2
out = filter_internal_params(seeded)
for k in self.INTERNAL_KEYS:
assert k not in out, f"{k} leaked through filter_internal_params"
assert out == {"model": "gpt-4", "temperature": 0.2}
def test_internal_keys_are_registered_in_all_litellm_params(self):
for k in self.INTERNAL_KEYS:
assert k in all_litellm_params, f"{k} missing from all_litellm_params"
def test_stream_chunk_size_is_not_filtered(self):
# stream_chunk_size is read downstream (converse streaming), not internal
assert filter_internal_params({"stream_chunk_size": 2048}) == {"stream_chunk_size": 2048}
def test_additional_internal_params_layer_on_top(self):
out = filter_internal_params(
{"keep": 1, "skip_mcp_handler": 2, "provider_only": 3},
additional_internal_params={"provider_only"},
)
assert out == {"keep": 1}
def test_registry_not_mutated_by_additional_params(self):
baseline = set(MCP_INTERNAL_REQUEST_KEYS)
filter_internal_params({"x": 1}, additional_internal_params={"adhoc_key"})
assert MCP_INTERNAL_REQUEST_KEYS == baseline
def test_non_dict_passes_through(self):
assert filter_internal_params("not-a-dict") == "not-a-dict"
assert filter_internal_params([1, 2, 3]) == [1, 2, 3]
def test_existing_mcp_keys_still_filtered(self):
out = filter_internal_params({"skip_mcp_handler": True, "mcp_handler_context": {}, "model": "gpt-4"})
assert out == {"model": "gpt-4"}