fix(capabilities): resolve models.dev specialty rows across provider keys (#8017) (#8023)

Co-authored-by: RaviTharuma <ravitharuma@users.noreply.github.com>
Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
This commit is contained in:
Ravi Tharuma
2026-07-22 12:09:12 +02:00
committed by GitHub
parent 9020ed53f9
commit 0a7a46f3da
2 changed files with 153 additions and 25 deletions

View File

@@ -11,6 +11,7 @@ import {
type ModelSpec,
} from "@/shared/constants/modelSpecs";
import { getSyncedCapability } from "@/lib/modelsDevSync";
import { MODELS_DEV_PROVIDER_MAP } from "@/lib/modelsDevSync/transform";
import { getModelContextOverride } from "@/lib/db/modelContextOverrides";
import { getModelCapabilityOverride } from "@/lib/db/modelCapabilityOverrides";
import { isVisionModelId } from "@/shared/constants/visionModels";
@@ -227,6 +228,20 @@ function stripLatestAlias(modelId: string | null): string | null {
return stripped && stripped !== modelId ? stripped : null;
}
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).
const out = new Set<string>();
for (const [modelsDevId, omniIds] of Object.entries(MODELS_DEV_PROVIDER_MAP)) {
if (omniIds.includes(provider) || modelsDevId === provider) {
out.add(modelsDevId);
for (const id of omniIds) out.add(id);
}
}
return [...out];
}
function getSyncedCapabilityForResolved(
provider: string | null,
model: string | null,
@@ -234,35 +249,36 @@ function getSyncedCapabilityForResolved(
): SyncedCapabilities {
if (!provider || !model) return null;
const direct = getSyncedCapability(provider, model);
if (direct) return direct;
const modelCandidates = Array.from(
new Set(
[model, rawModel, getStaticSpecCanonicalModelId(model, rawModel)]
.filter((value): value is string => typeof value === "string" && value.length > 0)
.flatMap((candidate) => {
const values = [candidate];
const stripped = stripLatestAlias(candidate);
if (stripped) values.push(stripped);
// models.dev often stores OpenAI-family specialty models as qualified
// ids under another mapped provider, e.g. vercel + "openai/whisper-1".
if (!candidate.includes("/")) {
values.push(`${provider}/${candidate}`);
}
return values;
})
)
);
if (rawModel && rawModel !== model) {
const raw = getSyncedCapability(provider, rawModel);
if (raw) return raw;
}
// Include common host providers that re-publish OpenAI specialty models under
// qualified ids (observed: vercel/openai/whisper-1, vercel/openai/tts-1).
const providerCandidates = Array.from(
new Set([provider, ...reverseModelsDevProviders(provider), "vercel"])
);
const canonical = getStaticSpecCanonicalModelId(model, rawModel);
if (canonical && canonical !== model) {
const byCanonical = getSyncedCapability(provider, canonical);
if (byCanonical) return byCanonical;
}
// #4073: models.dev catalogs some `-latest` aliases under their short id
// (e.g. Mistral `pixtral-12b-latest` is stored as `pixtral-12b`). When every
// exact lookup above misses, retry once with a trailing `-latest` stripped so
// the synced metadata (`attachment` / image modalities) still wins over the
// last-resort #4071 model-id heuristic. Only fires as a fallback, so models
// whose `-latest` id IS stored verbatim (e.g. `pixtral-large-latest`) keep
// resolving directly above.
for (const candidate of [model, rawModel]) {
const base = stripLatestAlias(candidate);
if (base && base !== model && base !== rawModel) {
const byAlias = getSyncedCapability(provider, base);
if (byAlias) return byAlias;
for (const prov of providerCandidates) {
for (const mid of modelCandidates) {
const found = getSyncedCapability(prov, mid);
if (found) return found;
}
}
return null;
}

View File

@@ -0,0 +1,112 @@
import assert from "node:assert/strict";
import { describe, it, before, after } from "node:test";
import { getResolvedModelCapabilities } from "../../src/lib/modelCapabilities.ts";
import {
saveModelsDevCapabilities,
clearModelsDevCapabilities,
type CapabilitiesByProvider,
} from "../../src/lib/modelsDevSync.ts";
describe("models.dev specialty key resolution (#8017)", () => {
before(() => {
// Store specialty rows the way production models.dev currently does:
// under provider=vercel with qualified openai/* model ids.
const capabilities: CapabilitiesByProvider = {
vercel: {
"openai/whisper-1": {
tool_call: false,
reasoning: false,
attachment: false,
structured_output: null,
temperature: true,
modalities_input: '["audio"]',
modalities_output: '["text"]',
knowledge_cutoff: null,
release_date: null,
last_updated: null,
status: null,
family: null,
open_weights: null,
limit_context: 0,
limit_input: null,
limit_output: 0,
interleaved_field: null,
last_synced: new Date().toISOString(),
},
"openai/tts-1": {
tool_call: false,
reasoning: false,
attachment: false,
structured_output: null,
temperature: true,
modalities_input: '["text"]',
modalities_output: '["audio"]',
knowledge_cutoff: null,
release_date: null,
last_updated: null,
status: null,
family: null,
open_weights: null,
limit_context: 0,
limit_input: null,
limit_output: 0,
interleaved_field: null,
last_synced: new Date().toISOString(),
},
},
openai: {
"gpt-4o": {
tool_call: true,
reasoning: false,
attachment: true,
structured_output: true,
temperature: true,
modalities_input: '["text","image","pdf"]',
modalities_output: '["text"]',
knowledge_cutoff: null,
release_date: null,
last_updated: null,
status: null,
family: null,
open_weights: null,
limit_context: 128000,
limit_input: null,
limit_output: 16384,
interleaved_field: null,
last_synced: new Date().toISOString(),
},
},
};
saveModelsDevCapabilities(capabilities);
});
after(() => {
try {
clearModelsDevCapabilities();
} catch {
// ignore cleanup failures in unit isolation
}
});
it("resolves openai/whisper-1 against vercel/openai/whisper-1", () => {
const md = getResolvedModelCapabilities({ provider: "openai", model: "whisper-1" });
assert.deepEqual(md.modalitiesInput, ["audio"]);
assert.deepEqual(md.modalitiesOutput, ["text"]);
assert.equal(md.toolCalling, false);
assert.equal(md.reasoning, false);
});
it("resolves openai/tts-1 against vercel/openai/tts-1", () => {
const md = getResolvedModelCapabilities({ provider: "openai", model: "tts-1" });
assert.deepEqual(md.modalitiesInput, ["text"]);
assert.deepEqual(md.modalitiesOutput, ["audio"]);
assert.equal(md.toolCalling, false);
});
it("still resolves direct openai/gpt-4o keys", () => {
const md = getResolvedModelCapabilities({ provider: "openai", model: "gpt-4o" });
assert.equal(md.toolCalling, true);
assert.equal(md.contextWindow, 128000);
assert.deepEqual(md.modalitiesInput, ["text", "image", "pdf"]);
});
});