fix(routers): enforce the user's saved selection on the env-key spend path

↗ view on GitHub · Amal · 2026-08-18 · af64aca5

resolveModel accepts router-prefixed ids by SHAPE alone, so any
authenticated user could hand-craft a chat/title/tabular request naming
any `openrouter/x/y` or `vercel/x/y` model and it would run - on the
operator's env key when one is configured.

WHY THIS MATTERS (what a cost-abuse vector via unvalidated model ids is)
On server-key deployments the operator pays per token for whatever model
a request names. The Settings UI curates a small allowlist per user, but
nothing at request time consulted it: the model string in the request
body went straight to the gateway. A user (or a leaked JWT) could point
every request at the most expensive frontier model on the gateway's
catalog and run up the operator's bill - a classic unvalidated-
identifier cost-abuse vector. First-party models don't have this
problem because resolveModel checks them against a closed catalog.

HOW IT WORKS
- routerModels.resolveRequestedModel is the single request-time choke
  point: resolve the id as before, then, only for router-prefixed
  results, require membership in that user's saved user_router_models
  selection; otherwise warn and degrade to the caller's default -
  exactly the path an invalid model id already takes. BYOK users get the
  same rule (uniform semantics; their saved list is one Settings save
  away).
- runLLMStream (chat, project chat, Word chat, tabular streams) awaits
  it where it previously called resolveModel inline.
- getUserModelSettings applies the same membership guard to the stored
  title/tabular preferences using the selections it already fetched, so
  the profile row can't smuggle an unselected router model either.

Tests: streamingModelAllowlist.test.ts (saved member passes through to
the adapter; non-member falls back to the default, with and without a
BYOK key; first-party models skip the lookup) and userSettings.test.ts
(stored router preference outside the selection falls back). Verified by
stashing this fix: 4 of the new tests fail on the ported code because
the unselected router model reaches the adapter / settings unchanged.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Repository open-legal-products/mike
Author Amal <mamalanand3@gmail.com>
Authored
Committed
Parents 434e6b68
Stats 5 files changed , +304 , -7
Part of Harden model-router selection and failure handling

Capture this commit into my fork

Download a Markdown prompt that tells Claude how to port this exact commit into your working tree. Run it via claude -p < capture-commit-af64aca5.md from inside the repo you want the change in.

⬇ Download capture-commit-af64aca5.md