fix(router): honor allowed_openai_params when gating tier params

This commit is contained in:
Tin Chi Lo 2026-08-28 10:58:58 -07:00
parent 66a2a6ecb1
commit 5338f293aa
2 changed files with 74 additions and 13 deletions

View file

@ -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

View file

@ -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"}