Found by the QA reviewer of helmcode/nan-devops#358, verified against the LiteLLM build running in prod. Neither is a regression from that PR: both are open on main today. They are filed here rather than fixed there because #358 touches router_settings.fallbacks and these live on two other surfaces.
1. model_group_alias overrides a real model group of the same name
Router._common_checks_available_deployment resolves _get_model_from_alias(model=model) (router.py:10465) before consulting self.model_names. So an alias wins over a real group with the same name:
model_group_alias: {qwen3.6: "glm5.3"}
serves glm5.3 deployments to anyone asking for qwen3.6.
Why that is a metering hole, not just a routing surprise: qwen3.6 is not gated, so async_pre_call_hook returns at if model not in GATED_MODELS: return data before it extracts any identity or calls /api/internal/usage/check. The member is admitted with no cap and no entitlement check, then served a premium paid group. And async_function_with_fallbacks takes model_group = kwargs.get("model"), the un-aliased name, while _update_kwargs_before_fallbacks stamps metadata.model_group with the original, so the meter still sees qwen3.6.
Unlike the fallback path, this needs no failover at all. It is the normal path.
model_group_alias is a literal sibling of fallbacks: in the same router_settings block and is already populated in both tenants (glm5.2: "glm5.3"). Every invariant added in #358 reads only router_settings.fallbacks.
Suggested guard: reject any alias whose KEY is in _ZERO_COST_LOCAL, and require that the TARGET of an alias whose key is gated be canonicalised or free, with the same rule the existing _unsafe loop uses.
2. default_fallbacks materialises the "*" entry that #358 refuses
Router.__init__ (594-599):
if default_fallbacks is not None or litellm.default_fallbacks is not None:
_fallbacks = default_fallbacks or litellm.default_fallbacks
if self.fallbacks is not None:
self.fallbacks.append({"*": _fallbacks})
So default_fallbacks: ["glm5.3"] produces exactly the "*" catch-all that #358's _bad_shape invariant rejects, by a supported and documented key the suite cannot see because it only reads fallbacks.
It is worse than a hand-written "*". _has_default_fallbacks() / _get_first_default_fallback() are consulted inside _common_checks_available_deployment (10519-10530): when the requested group has zero healthy deployments, the router reassigns model = fallback_model there, during deployment selection. That path never goes through run_async_fallback, so there is no model_group re-stamp and the swap is invisible to the meter. Concretely: a vLLM outage leaves qwen3.6 with no healthy deployments and a paid default gets served, metered as qwen3.6.
context_window_fallbacks and content_policy_fallbacks are the same class. Both were probed and pass green today. They resolve through _get_fallback_model_group_from_fallbacks ahead of fallbacks for their two error classes (router.py:6221-6278), and context_window_fallbacks does not even go through validate_fallbacks.
Neither is configured in either tenant right now.
Suggested guard: refuse default_fallbacks, context_window_fallbacks and content_policy_fallbacks in router_settings of both values files by shape, or run them through the same invariants as fallbacks.
Why this is its own issue
The invariants in #358 have now been through eight review rounds and nine distinct routes, every one of them the same root cause: the test suite resolved or assumed something the router does not. Two reviewers independently observed that the block is outgrowing its home (281 lines under a single heading, checks stacked far from the rationale that justifies them).
The right shape for this is probably not more invariants bolted onto one test file, but a single router_settings linter that parses both values files and models the router's resolution once, tested against the installed wheel. That is a design decision worth making deliberately rather than in the tail of a three-line config fix.
Related
Found by the QA reviewer of helmcode/nan-devops#358, verified against the LiteLLM build running in prod. Neither is a regression from that PR: both are open on
maintoday. They are filed here rather than fixed there because #358 touchesrouter_settings.fallbacksand these live on two other surfaces.1.
model_group_aliasoverrides a real model group of the same nameRouter._common_checks_available_deploymentresolves_get_model_from_alias(model=model)(router.py:10465) before consultingself.model_names. So an alias wins over a real group with the same name:serves glm5.3 deployments to anyone asking for
qwen3.6.Why that is a metering hole, not just a routing surprise:
qwen3.6is not gated, soasync_pre_call_hookreturns atif model not in GATED_MODELS: return databefore it extracts any identity or calls/api/internal/usage/check. The member is admitted with no cap and no entitlement check, then served a premium paid group. Andasync_function_with_fallbackstakesmodel_group = kwargs.get("model"), the un-aliased name, while_update_kwargs_before_fallbacksstampsmetadata.model_groupwith the original, so the meter still seesqwen3.6.Unlike the fallback path, this needs no failover at all. It is the normal path.
model_group_aliasis a literal sibling offallbacks:in the samerouter_settingsblock and is already populated in both tenants (glm5.2: "glm5.3"). Every invariant added in #358 reads onlyrouter_settings.fallbacks.Suggested guard: reject any alias whose KEY is in
_ZERO_COST_LOCAL, and require that the TARGET of an alias whose key is gated be canonicalised or free, with the same rule the existing_unsafeloop uses.2.
default_fallbacksmaterialises the"*"entry that #358 refusesRouter.__init__(594-599):So
default_fallbacks: ["glm5.3"]produces exactly the"*"catch-all that #358's_bad_shapeinvariant rejects, by a supported and documented key the suite cannot see because it only readsfallbacks.It is worse than a hand-written
"*"._has_default_fallbacks()/_get_first_default_fallback()are consulted inside_common_checks_available_deployment(10519-10530): when the requested group has zero healthy deployments, the router reassignsmodel = fallback_modelthere, during deployment selection. That path never goes throughrun_async_fallback, so there is nomodel_groupre-stamp and the swap is invisible to the meter. Concretely: a vLLM outage leavesqwen3.6with no healthy deployments and a paid default gets served, metered asqwen3.6.context_window_fallbacksandcontent_policy_fallbacksare the same class. Both were probed and pass green today. They resolve through_get_fallback_model_group_from_fallbacksahead offallbacksfor their two error classes (router.py:6221-6278), andcontext_window_fallbacksdoes not even go throughvalidate_fallbacks.Neither is configured in either tenant right now.
Suggested guard: refuse
default_fallbacks,context_window_fallbacksandcontent_policy_fallbacksinrouter_settingsof both values files by shape, or run them through the same invariants asfallbacks.Why this is its own issue
The invariants in #358 have now been through eight review rounds and nine distinct routes, every one of them the same root cause: the test suite resolved or assumed something the router does not. Two reviewers independently observed that the block is outgrowing its home (281 lines under a single heading, checks stacked far from the rationale that justifies them).
The right shape for this is probably not more invariants bolted onto one test file, but a single
router_settingslinter that parses both values files and models the router's resolution once, tested against the installed wheel. That is a design decision worth making deliberately rather than in the tail of a three-line config fix.Related