From 6cad24eec05be2720794f5568dba2aa3bdb742db Mon Sep 17 00:00:00 2001 From: Diego Rodrigues de Sa e Souza Date: Sat, 25 Jul 2026 04:57:15 -0300 Subject: [PATCH] fix(api): probe provider alias in models.dev reverse capability lookup (#8429) (#8506) reverseModelsDevProviders() only matched MODELS_DEV_PROVIDER_MAP entries by the canonical OmniRoute provider id, but the map's RHS for the OAuth CLI providers (codex/claude) only lists their alias (cx/cc), never the canonical id itself. Since the models.dev sync job writes model_capabilities rows under openai/cx and anthropic/cc (never codex/claude), and the auto-combo gate canonicalizes a codex/... or claude/... target's provider to "codex"/"claude" before the lookup, the synced capability row was unreachable for those two providers. Also probe the provider's alias (via the already-imported PROVIDER_ID_TO_ALIAS) when scanning the map, so a canonical id like "codex"/"claude" still matches entries keyed only by their alias. Co-authored-by: ikelvingo --- .../8429-capability-alias-canonicalization.md | 1 + src/lib/modelCapabilities.ts | 14 +++- ...o-8429-capability-canonicalization.test.ts | 74 +++++++++++++++++++ 3 files changed, 88 insertions(+), 1 deletion(-) create mode 100644 changelog.d/fixes/8429-capability-alias-canonicalization.md create mode 100644 tests/unit/repro-8429-capability-canonicalization.test.ts diff --git a/changelog.d/fixes/8429-capability-alias-canonicalization.md b/changelog.d/fixes/8429-capability-alias-canonicalization.md new file mode 100644 index 0000000000..6d8d49d8e1 --- /dev/null +++ b/changelog.d/fixes/8429-capability-alias-canonicalization.md @@ -0,0 +1 @@ +- fix(api): reach synced model_capabilities rows for canonical provider ids that only appear as an alias in MODELS_DEV_PROVIDER_MAP, e.g. codex/claude (#8429) diff --git a/src/lib/modelCapabilities.ts b/src/lib/modelCapabilities.ts index 79a1a75f2d..f4bb9868f6 100644 --- a/src/lib/modelCapabilities.ts +++ b/src/lib/modelCapabilities.ts @@ -281,9 +281,21 @@ function reverseModelsDevProviders(provider: string): string[] { // models.dev may store capabilities under a different OmniRoute provider id // that also maps from the same upstream models.dev provider. Build reverse // candidates from MODELS_DEV_PROVIDER_MAP (e.g. openai ↔ cx). + // + // MODELS_DEV_PROVIDER_MAP's RHS is inconsistent: most providers list their + // canonical id directly, but the OAuth CLI providers (codex/claude) only + // list their alias (cx/cc), never the canonical id. Also probe the + // provider's alias so a canonical id like "codex"/"claude" still matches + // the map entries keyed only by "cx"/"cc" (#8429). const out = new Set(); + const providerAlias = PROVIDER_ID_TO_ALIAS[provider] || provider; for (const [modelsDevId, omniIds] of Object.entries(MODELS_DEV_PROVIDER_MAP)) { - if (omniIds.includes(provider) || modelsDevId === provider) { + if ( + omniIds.includes(provider) || + omniIds.includes(providerAlias) || + modelsDevId === provider || + modelsDevId === providerAlias + ) { out.add(modelsDevId); for (const id of omniIds) out.add(id); } diff --git a/tests/unit/repro-8429-capability-canonicalization.test.ts b/tests/unit/repro-8429-capability-canonicalization.test.ts new file mode 100644 index 0000000000..5e36bd57be --- /dev/null +++ b/tests/unit/repro-8429-capability-canonicalization.test.ts @@ -0,0 +1,74 @@ +import test from "node:test"; +import assert from "node:assert/strict"; +import fs from "node:fs"; +import os from "node:os"; +import path from "node:path"; + +const TEST_DATA_DIR = fs.mkdtempSync(path.join(os.tmpdir(), "omniroute-repro-8429-")); +process.env.DATA_DIR = TEST_DATA_DIR; + +const core = await import("../../src/lib/db/core.ts"); +const modelsDevSync = await import("../../src/lib/modelsDevSync.ts"); +const modelCapabilities = await import("../../src/lib/modelCapabilities.ts"); + +function buildCapability(overrides: Record = {}) { + return { + tool_call: null, reasoning: null, attachment: null, structured_output: null, + temperature: null, modalities_input: "[]", modalities_output: "[]", + knowledge_cutoff: null, release_date: null, last_updated: null, status: null, + family: null, open_weights: null, limit_context: null, limit_input: null, + limit_output: null, interleaved_field: null, ...overrides, + }; +} + +function resetStorage() { + core.resetDbInstance(); + fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true }); + fs.mkdirSync(TEST_DATA_DIR, { recursive: true }); +} + +test.beforeEach(() => { resetStorage(); }); +test.after(() => { + core.resetDbInstance(); + fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true }); +}); + +test("#8429: synced model_capabilities row written under models.dev mapping is unreachable via the canonical 'codex' provider id", () => { + modelsDevSync.saveModelsDevCapabilities({ + openai: { "gpt-6-codex-preview": buildCapability({ tool_call: false }) }, + cx: { "gpt-6-codex-preview": buildCapability({ tool_call: false }) }, + }); + const viaAlias = modelsDevSync.getSyncedCapability("cx", "gpt-6-codex-preview"); + assert.equal(viaAlias?.tool_call, false, "row must exist under the alias 'cx'"); + + const resolved = modelCapabilities.getResolvedModelCapabilities({ + provider: "codex", + model: "gpt-6-codex-preview", + }); + + assert.equal( + resolved.supportsTools, + false, + `expected synced tool_call=false to be resolved for provider "codex" (it was ${resolved.supportsTools})` + ); +}); + +test("#8429: synced model_capabilities row is unreachable via the canonical 'claude' provider id (same class, alias 'cc')", () => { + modelsDevSync.saveModelsDevCapabilities({ + anthropic: { "claude-preview-9-9": buildCapability({ tool_call: false }) }, + cc: { "claude-preview-9-9": buildCapability({ tool_call: false }) }, + }); + const viaAlias = modelsDevSync.getSyncedCapability("cc", "claude-preview-9-9"); + assert.equal(viaAlias?.tool_call, false, "row must exist under the alias 'cc'"); + + const resolved = modelCapabilities.getResolvedModelCapabilities({ + provider: "claude", + model: "claude-preview-9-9", + }); + + assert.equal( + resolved.supportsTools, + false, + `expected synced tool_call=false to be resolved for provider "claude" (it was ${resolved.supportsTools})` + ); +});