fix(routers): accept catalog ids that begin with the router's own slug

↗ view on GitHub · Amal · 2026-08-18 · 434e6b68

Saving OpenRouter's "openrouter/auto" (or Vercel's "vercel/v0-1.5-md")
used to 400 the entire profile PATCH, and the Word add-in sent a
different model string than the web app for the same stored selection.

WHY THIS MATTERS
Router catalogs are not namespaced away from the router's own brand:
OpenRouter really lists "openrouter/auto" and Vercel lists "vercel/v0-*"
models. The backend validator stripped a leading "<router>/" from every
submitted id BEFORE checking the vendor/model shape, so "openrouter/auto"
became "auto", failed the shape check, was dropped, tripped the
length-mismatch guard, and the whole settings save failed - a user could
not select those models at all.

WHAT IS THE CANONICAL STORED FORM
The database row stores the router's RAW catalog id (what the gateway's
/models endpoint returns), and the app-level model id is always
"<router-slug>/<catalog-id>" built by plain prefixing - so OpenRouter's
"openrouter/auto" travels as "openrouter/openrouter/auto". The adapter
strips exactly one namespace segment before calling upstream, which
round-trips this correctly (pinned by new llmModels tests).

HOW IT WORKS
- backend normalizeRouterModels (and the frontend settings typeahead's
  new normalizeTypedModelId twin) strip the router prefix only when the
  remainder is still a full vendor/model id; otherwise the raw id is
  validated and stored verbatim.
- the Word add-in's ModelToggle no longer "defensively" strips an inner
  router prefix when building option ids; both clients now share the
  same prefix-verbatim option builders, and a new cross-package parity
  test (frontend/src/wordAddin/catalogParity.test.ts) pins that the two
  clients emit the identical composer model string for the same stored
  selection.

Tests: userRouterModels.test.ts (PATCH accepts "openrouter/auto" /
"vercel/v0-1.5-md", still canonicalizes composer-form ids),
RouterSettingsSection.test.tsx (typing "openrouter/auto" + Enter adds it
verbatim), llmModels.test.ts (resolveModel + single-segment strip),
catalogParity.test.ts (web/add-in composer-string parity). The backend
and UI tests fail against the previous unconditional strip.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Repository open-legal-products/mike
Author Amal <mamalanand3@gmail.com>
Authored
Committed
Parents 83b5ce05
Stats 8 files changed , +434 , -17
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-434e6b68.md from inside the repo you want the change in.

⬇ Download capture-commit-434e6b68.md