mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-10 22:41:41 +00:00
fix(router): honor allowed_openai_params when gating tier params
This commit is contained in:
parent
66a2a6ecb1
commit
5338f293aa
2 changed files with 74 additions and 13 deletions
|
|
@ -10846,11 +10846,20 @@ class Router:
|
|||
}
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def _declared_param_allowlist(params: Mapping[str, object]) -> frozenset[str]:
|
||||
declared: Final = params.get("allowed_openai_params")
|
||||
if not isinstance(declared, (list, tuple, set, frozenset)):
|
||||
return frozenset()
|
||||
return frozenset(entry for entry in declared if isinstance(entry, str))
|
||||
|
||||
@staticmethod
|
||||
def _deployment_accepts_param(deployment: DeploymentTypedDict, group: str, param: str) -> bool:
|
||||
deployment_params: Final = deployment.get("litellm_params")
|
||||
if not deployment_params:
|
||||
return True
|
||||
if param in Router._declared_param_allowlist(deployment_params):
|
||||
return True
|
||||
deployment_model_info: Final = deployment.get("model_info")
|
||||
base_model: Final = (
|
||||
deployment_model_info.get("base_model") if deployment_model_info else None
|
||||
|
|
@ -10872,7 +10881,9 @@ class Router:
|
|||
return True
|
||||
return supported is None or param in supported
|
||||
|
||||
def _tier_params_the_target_accepts(self, model: str, tier_params: Mapping[str, object]) -> Mapping[str, object]:
|
||||
def _tier_params_the_target_accepts(
|
||||
self, model: str, tier_params: Mapping[str, object], request_kwargs: Mapping[str, object]
|
||||
) -> Mapping[str, object]:
|
||||
"""Drop an OpenAI param that no deployment behind ``model`` declares.
|
||||
|
||||
A tier's litellm_params are an operator override applied to every request the tier routes,
|
||||
|
|
@ -10896,11 +10907,19 @@ class Router:
|
|||
A param survives if ANY deployment could take it, because routing has not chosen one yet,
|
||||
and it survives both an unresolvable provider and a group with no deployments, because a
|
||||
best-effort filter must never narrow what the request already did.
|
||||
|
||||
allowed_openai_params is the documented escape hatch for an outdated or incomplete
|
||||
supported-params list: request-time validation extends the supported list with it before
|
||||
comparing. The filter asks the same question, so a param named by the allowlist on the tier
|
||||
overlay, the request, or a deployment's own litellm_params is never a drop candidate.
|
||||
"""
|
||||
deployments: Final = self.get_model_list(model_name=model) or ()
|
||||
if not deployments:
|
||||
return tier_params
|
||||
candidates: Final = provider_rejectable_params(tier_params) - self.TIER_PARAMS_NEVER_DROPPED
|
||||
allowlisted: Final = self._declared_param_allowlist(tier_params) | self._declared_param_allowlist(
|
||||
request_kwargs
|
||||
)
|
||||
candidates: Final = provider_rejectable_params(tier_params) - self.TIER_PARAMS_NEVER_DROPPED - allowlisted
|
||||
unsupported: Final = frozenset(
|
||||
param
|
||||
for param in candidates
|
||||
|
|
@ -11971,7 +11990,9 @@ class Router:
|
|||
messages = pre_routing_hook_response.messages
|
||||
if pre_routing_hook_response.litellm_params:
|
||||
request_kwargs.update(
|
||||
self._tier_params_the_target_accepts(model, pre_routing_hook_response.litellm_params)
|
||||
self._tier_params_the_target_accepts(
|
||||
model, pre_routing_hook_response.litellm_params, request_kwargs
|
||||
)
|
||||
)
|
||||
#########################################################
|
||||
|
||||
|
|
@ -12084,7 +12105,9 @@ class Router:
|
|||
messages = pre_routing_hook_response.messages
|
||||
if pre_routing_hook_response.litellm_params:
|
||||
request_kwargs.update(
|
||||
self._tier_params_the_target_accepts(model, pre_routing_hook_response.litellm_params)
|
||||
self._tier_params_the_target_accepts(
|
||||
model, pre_routing_hook_response.litellm_params, request_kwargs
|
||||
)
|
||||
)
|
||||
|
||||
# 2. Get healthy deployments
|
||||
|
|
|
|||
|
|
@ -11216,14 +11216,14 @@ class TestTierParamsTheTargetAccepts:
|
|||
def test_drops_a_param_no_deployment_declares(self):
|
||||
router = self._router("novita/moonshotai/kimi-k3")
|
||||
|
||||
accepted = router._tier_params_the_target_accepts("tiered", {"reasoning_effort": "max"})
|
||||
accepted = router._tier_params_the_target_accepts("tiered", {"reasoning_effort": "max"}, {})
|
||||
|
||||
assert accepted == {}
|
||||
|
||||
def test_keeps_a_param_the_deployment_declares(self):
|
||||
router = self._router("fireworks_ai/kimi-k3")
|
||||
|
||||
accepted = router._tier_params_the_target_accepts("tiered", {"reasoning_effort": "max"})
|
||||
accepted = router._tier_params_the_target_accepts("tiered", {"reasoning_effort": "max"}, {})
|
||||
|
||||
assert accepted == {"reasoning_effort": "max"}
|
||||
|
||||
|
|
@ -11245,7 +11245,7 @@ class TestTierParamsTheTargetAccepts:
|
|||
configuration the request needs while never touching what the provider would reject."""
|
||||
router = self._router("novita/moonshotai/kimi-k3")
|
||||
|
||||
accepted = router._tier_params_the_target_accepts("tiered", {control: value, "reasoning_effort": "max"})
|
||||
accepted = router._tier_params_the_target_accepts("tiered", {control: value, "reasoning_effort": "max"}, {})
|
||||
|
||||
assert accepted == {control: value}
|
||||
|
||||
|
|
@ -11254,7 +11254,7 @@ class TestTierParamsTheTargetAccepts:
|
|||
[
|
||||
("additional_drop_params", ["seed"]),
|
||||
("drop_params", True),
|
||||
("allowed_openai_params", ["reasoning_effort"]),
|
||||
("allowed_openai_params", ["seed"]),
|
||||
("api_version", "2024-02-01"),
|
||||
("metadata", {"tier": "complex"}),
|
||||
],
|
||||
|
|
@ -11265,10 +11265,48 @@ class TestTierParamsTheTargetAccepts:
|
|||
drop_params or additional_drop_params would silently disable the operator's sanitization."""
|
||||
router = self._router("novita/moonshotai/kimi-k3")
|
||||
|
||||
accepted = router._tier_params_the_target_accepts("tiered", {control: value, "reasoning_effort": "max"})
|
||||
accepted = router._tier_params_the_target_accepts("tiered", {control: value, "reasoning_effort": "max"}, {})
|
||||
|
||||
assert accepted == {control: value}
|
||||
|
||||
def test_tier_allowlist_protects_the_param_it_names(self):
|
||||
"""allowed_openai_params is the documented escape hatch for an incomplete supported-params
|
||||
list, and request-time validation extends the supported list with it, so a param the tier
|
||||
both sets and allowlists would never 400 and must not be dropped."""
|
||||
router = self._router("novita/moonshotai/kimi-k3")
|
||||
|
||||
accepted = router._tier_params_the_target_accepts(
|
||||
"tiered", {"reasoning_effort": "max", "allowed_openai_params": ["reasoning_effort"]}, {}
|
||||
)
|
||||
|
||||
assert accepted == {"reasoning_effort": "max", "allowed_openai_params": ["reasoning_effort"]}
|
||||
|
||||
def test_request_allowlist_protects_the_param_it_names(self):
|
||||
router = self._router("novita/moonshotai/kimi-k3")
|
||||
|
||||
accepted = router._tier_params_the_target_accepts(
|
||||
"tiered", {"reasoning_effort": "max"}, {"allowed_openai_params": ["reasoning_effort"]}
|
||||
)
|
||||
|
||||
assert accepted == {"reasoning_effort": "max"}
|
||||
|
||||
def test_allowlist_protects_only_the_params_it_names(self):
|
||||
router = self._router("novita/moonshotai/kimi-k3")
|
||||
|
||||
accepted = router._tier_params_the_target_accepts(
|
||||
"tiered", {"reasoning_effort": "max", "allowed_openai_params": ["seed"]}, {}
|
||||
)
|
||||
|
||||
assert accepted == {"allowed_openai_params": ["seed"]}
|
||||
|
||||
def test_deployment_accepts_param_honors_deployment_allowlist(self):
|
||||
deployment = {
|
||||
"model_name": "x",
|
||||
"litellm_params": {"model": "novita/moonshotai/kimi-k3", "allowed_openai_params": ["reasoning_effort"]},
|
||||
}
|
||||
|
||||
assert litellm.Router._deployment_accepts_param(deployment, "x", "reasoning_effort") is True
|
||||
|
||||
def test_keeps_a_token_ceiling_the_provider_spells_differently(self):
|
||||
"""petals lists max_tokens but not max_completion_tokens. A tier ceiling in the unsupported
|
||||
spelling is a cost bound: dropping it would let a caller's larger max_tokens through where
|
||||
|
|
@ -11276,7 +11314,7 @@ class TestTierParamsTheTargetAccepts:
|
|||
router = self._router("petals/petals-team/StableBeluga2")
|
||||
|
||||
accepted = router._tier_params_the_target_accepts(
|
||||
"tiered", {"max_completion_tokens": 100, "reasoning_effort": "max"}
|
||||
"tiered", {"max_completion_tokens": 100, "reasoning_effort": "max"}, {}
|
||||
)
|
||||
|
||||
assert accepted == {"max_completion_tokens": 100}
|
||||
|
|
@ -11288,7 +11326,7 @@ class TestTierParamsTheTargetAccepts:
|
|||
router = self._router("ai21/jamba-1.5-mini")
|
||||
|
||||
accepted = router._tier_params_the_target_accepts(
|
||||
"tiered", {"extra_headers": {"x-tenant": "acme"}, "reasoning_effort": "max"}
|
||||
"tiered", {"extra_headers": {"x-tenant": "acme"}, "reasoning_effort": "max"}, {}
|
||||
)
|
||||
|
||||
assert accepted == {"extra_headers": {"x-tenant": "acme"}}
|
||||
|
|
@ -11302,7 +11340,7 @@ class TestTierParamsTheTargetAccepts:
|
|||
]
|
||||
)
|
||||
|
||||
accepted = router._tier_params_the_target_accepts("tiered", {"reasoning_effort": "max"})
|
||||
accepted = router._tier_params_the_target_accepts("tiered", {"reasoning_effort": "max"}, {})
|
||||
|
||||
assert accepted == {"reasoning_effort": "max"}
|
||||
|
||||
|
|
@ -11347,6 +11385,6 @@ class TestTierParamsTheTargetAccepts:
|
|||
"""An unresolvable target must never narrow what the request already did."""
|
||||
router = self._router("fireworks_ai/kimi-k3")
|
||||
|
||||
accepted = router._tier_params_the_target_accepts("no-such-group", {"reasoning_effort": "max"})
|
||||
accepted = router._tier_params_the_target_accepts("no-such-group", {"reasoning_effort": "max"}, {})
|
||||
|
||||
assert accepted == {"reasoning_effort": "max"}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue