Repository navigation
fix(api): list registry-only Claude models (opus-5/sonnet-5) in /v1/models when a sync omits them - #9169
Conversation
…odels when a sync omits them The catalog builder skipped every static registry model for any non-curated provider that had synced >=1 live model, keeping only effort variants. That silently dropped brand-new registry models the upstream provider's live /v1/models does not advertise yet. cheaper-inference#9167: claude-opus-5 / claude-sonnet-5 are defined in the Claude registry, modelSpecs and pricing, and the connection can route them (a custom claude-opus-5[1m] on the same connection was served), but they were missing from GET /v1/models while claude-opus-4-8 (which the sync returns) survived. Skip a static entry only when the synced set actually carries that id, keyed by the same canonicalProviderId the static loop reads. Extracted the decision into shouldSkipStaticForSynced (pure, unit-tested) and added a regression test covering the unit cases plus an end-to-end /v1/models assertion.
CI failures are pre-existing on the base, not from this PRI reproduced every red check against a pristine Unit Tests fast-path (2/4, 3/4, 4/4)The failing tests are all in the Claude-Code-headers / executor / Codex-discovery / golden-snapshot area — none touch the model catalog. Run on the unmodified base: Files: Fast Quality GatesThe file-size baseline gate flags two files that are not in this PR's diff: This PR's complete diff is only: so the frozen-size drift in This PR's own testsAlso green: Happy to rebase once the base-branch failures and the file-size baseline are sorted, or to fold a baseline bump into this PR if you'd prefer — just say which you want. |
|
Superseded by #9217 — closing. #9217 ( I verified #9217 resolves the #9167 case rather than assuming it: I ran this PR's end-to-end assertion (seed an active So Thanks for the quick turnaround on #9217. |
Summary
GET /v1/modelssilently dropped built-in registry modelsclaude-opus-5andclaude-sonnet-5for theclaude/ccprovider, even though they are fully defined in the Claude registry,modelSpecs, and pricing — while sibling models likeclaude-opus-4-8were served.Root cause is in the
/v1/modelscatalog builder (src/app/api/v1/models/catalog.ts). When a non-curated provider has synced ≥1 live model, the static-registry loop skipped every static entry for that provider (keeping only effort variants), on the assumption that the synced list fully replaces the static registry:But the synced list is the provider's live
/v1/models, which lags OmniRoute's registry for brand-new models. So a model that exists in the registry but not yet in the upstream live list (Opus 5 / Sonnet 5) was dropped wholesale.claude-opus-4-8survived only because the sync happens to return it.This PR changes the skip so a static entry is dropped only when the synced set actually carries that same id (i.e. only when it is genuinely being replaced). Registry-only models then survive; their reasoning-effort variants follow automatically because
appendClaudeEffortVariantsderives them from the surviving base entry.The decision is extracted into a pure, unit-tested helper
shouldSkipStaticForSynced, and the synced-id index is keyed by the samecanonicalProviderIdthe static loop reads, so the lookup and the existingprovidersWithSyncedModels.has(canonicalProviderId)check always agree.providersWithSyncedModelsis left untouched (the OpenRouter block depends on its semantics).Reproduction (live, v3.8.49–v3.8.50)
omniroute models/ dashboard list "Claude Opus 5" and "Claude Sonnet 5".GET /v1/modelsomitscc/claude-opus-5,claude/claude-opus-5,cc/claude-sonnet-5,claude/claude-sonnet-5while servingcc/claude-opus-4-8,cc/claude-sonnet-4-6, etc.claude-opus-5[1m]on the same connection is served — proving the connection can route Opus 5 and isolating the bug to the static-vs-synced catalog path (not entitlement/routing).Related Issues
Validation
GET /v1/models)tests/unit/catalog-registry-only-survives-sync-9167.test.ts(4 pure-helper cases + 1 end-to-endgetUnifiedModelsResponseassertion) — all pass; the e2e assertion fails on the pre-fix condition (verified by reverting the guard) and passes with the fix.noauth-imported-models-3200,claude-web-sonnet5-registry-6209,synced-model-hide-persist-3782,catalog-helpers-extraction,8327-models-owned-by-prefix(23/23 pass).npm run lint— ESLint with the repo suppressions baseline is clean on the changed files (the singleno-restricted-importsnote oncatalog.tsline 3 is pre-existing and already in the baseline). Prettier clean on all changed files.release/v3.8.50, HEADec150a0).Commands run:
Tests Added Or Updated
tests/unit/catalog-registry-only-survives-sync-9167.test.ts:claudeOAuth connection + a synced set containingclaude-opus-4-8but notclaude-opus-5, then assertsGET /v1/modelsincludesclaude/claude-opus-5andclaude/claude-sonnet-5, still includes the syncedclaude/claude-opus-4-8, and does not duplicate it.Coverage Notes
src/(catalog builder). The new pure helpershouldSkipStaticForSyncedis fully branch-covered by the four unit cases; the modified skip site is exercised by the end-to-end test throughgetUnifiedModelsResponse.Reviewer Notes
canonicalProviderId(resolved with the sameresolveCanonicalProviderId(alias, providerId)the synced loop uses) so it agrees with theprovidersWithSyncedModels.has(canonicalProviderId)check. Forclauderaw id === canonical; the canonical keying is what keeps this correct for compatible providers with UUID raw ids.providersWithSyncedModelssemantics are unchanged (the OpenRouter block at!providersWithSyncedModels.has("openrouter")relies on them).dedupeExactCatalogIdsremains as an independent backstop but the logic does not rely on it.modelSpecsentries forclaude-opus-5/claude-sonnet-5(both present withsupportsThinking: true) so their effort variants and enrichment work — verified insrc/shared/constants/modelSpecs.ts.