Skip to content

router_settings: model_group_alias and default_fallbacks can serve paid inference unmetered, and no invariant reads them #77

Description

@sre-helmcode

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

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions