mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-29 01:42:19 +00:00
Merge 2f74a51360 into 4584958574
This commit is contained in:
commit
004c2c4385
3 changed files with 92 additions and 42 deletions
|
|
@ -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}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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())
|
||||
|
|
|
|||
|
|
@ -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"}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue