diff --git a/.changeset/resync-models-2026-09-28.md b/.changeset/resync-models-2026-09-28.md new file mode 100644 index 0000000..76b7a1f --- /dev/null +++ b/.changeset/resync-models-2026-09-28.md @@ -0,0 +1,5 @@ +--- +"opencode-bridge-copilot-chat": patch +--- + +Mirror canonical upstream metadata for discovery-only Go models before models.dev catalogs them: `deepseek-flash` (DeepSeek V4.1 Flash with limits, image input, reasoning efforts, and published costs), plus the anonymous-discovery ids `minimax-m2.5`, `kimi-k2.5`, `glm-5.1`, `glm-5`, and `qwen3.5-plus`. Also filter internal OpenCode smoke-test ids (`test*`) that leak into authenticated Zen discovery, instead of listing them as unenriched picker entries. \ No newline at end of file diff --git a/src/models/catalog.test.ts b/src/models/catalog.test.ts index f45ef70..3e9e86f 100644 --- a/src/models/catalog.test.ts +++ b/src/models/catalog.test.ts @@ -11,6 +11,16 @@ test("filters deprecated and paid Zen models when free-only is enabled", () => { assert.deepEqual(models.map((model) => model.id), ["free"]); }); +test("filters internal smoke-test ids leaked into discovery", () => { + const models = modelsFromProvider("zen", "opencode", { api: "https://example.test/v1" }, { + test: { id: "test" }, + "test-novita-dsf4.1": { id: "test-novita-dsf4.1" }, + "test-model": { id: "test-model" }, + tester: { id: "tester", name: "Tester" }, + }, false); + assert.deepEqual(models.map((model) => model.id), ["tester"]); +}); + test("preserves live reasoning options for the per-model thinking picker", () => { const [model] = modelsFromProvider("go", "opencode-go", {}, { "qwen3.7-max": { @@ -86,6 +96,26 @@ test("enriches discovery-only Go models with supplemental metadata", async () => assert.equal(preview?.cost, undefined); }); +test("enriches discovery-only Go models with mirrored supplemental metadata", async () => { + const catalog = new ModelCatalog(async (input) => String(input).endsWith("/models") + ? Response.json({ data: [{ id: "deepseek-flash" }] }) + : Response.json({ "opencode-go": { id: "opencode-go", models: {} } })); + const models = await catalog.refresh("go", { mode: "go", token: "token" }, false); + assert.deepEqual(models.map((model) => model.id), ["deepseek-flash"]); + const flash = models[0]; + assert.deepEqual({ name: flash.name, family: flash.family, context: flash.contextLength, output: flash.maxOutputTokens, image: flash.imageInput, reasoning: flash.reasoning, tools: flash.toolCalling, cost: flash.cost, endpoint: flash.endpoint }, { + name: "DeepSeek V4.1 Flash", + family: "deepseek-flash", + context: 1_000_000, + output: 393_216, + image: true, + reasoning: true, + tools: true, + cost: { input: 0.15, output: 0.6, cacheRead: 0.003 }, + endpoint: "chat-completions", + }); +}); + test("restores a recent authenticated catalog cache when refresh fails", async () => { const values = new Map(); const cache = { diff --git a/src/models/catalog.ts b/src/models/catalog.ts index 1d98e20..2ae378d 100644 --- a/src/models/catalog.ts +++ b/src/models/catalog.ts @@ -168,6 +168,7 @@ export function modelsFromProvider( ): OpenCodeModel[] { return Object.entries(sources).flatMap(([rawId, source]) => { if (source.status === "deprecated" || source.disabled === true) return []; + if (isInternalTestModel(rawId)) return []; if (freeOnly && (source.cost?.input ?? 1) > 0) return []; const packageName = source.provider?.npm ?? provider.npm; const baseUrl = source.provider?.api ?? provider.api ?? apiBaseForMode(mode === "console" ? "zen" : mode); @@ -268,6 +269,11 @@ function positive(value: number | undefined, fallback: number): number { return typeof value === "number" && Number.isFinite(value) && value > 0 ? Math.floor(value) : fallback; } +/** Internal OpenCode smoke-test ids that leak into authenticated discovery. */ +function isInternalTestModel(id: string): boolean { + return /^test(?:[-_.]|$)/i.test(id.trim()); +} + function familyOf(id: string): string { return id.split(/[/:.-]/)[0].toLowerCase() || "opencode"; } diff --git a/src/models/metadata.test.ts b/src/models/metadata.test.ts index 92a0348..a0d22ed 100644 --- a/src/models/metadata.test.ts +++ b/src/models/metadata.test.ts @@ -43,6 +43,28 @@ test("supplements discovery-only Go models that models.dev has not cataloged", ( assert.equal(snapshot.providers.opencode?.models?.["hy3-preview"], undefined); }); +test("mirrors canonical upstream entries for discovery-only Go models", () => { + const snapshot = normalizeModelsDevSnapshot({ "opencode-go": { id: "opencode-go", models: {} } }, 123); + const go = snapshot.providers["opencode-go"]?.models ?? {}; + const flash = go["deepseek-flash"]; + assert.equal(flash?.name, "DeepSeek V4.1 Flash"); + assert.equal(flash?.limit?.context, 1_000_000); + assert.equal(flash?.limit?.output, 393_216); + assert.deepEqual(flash?.cost, { input: 0.15, output: 0.6, cacheRead: 0.003 }); + assert.equal(flash?.reasoning, true); + assert.equal(flash?.tool_call, true); + const qwen = go["qwen3.5-plus"]; + assert.equal(qwen?.provider?.npm, "@ai-sdk/anthropic"); + assert.equal(qwen?.limit?.context, 262_144); + assert.equal(qwen?.limit?.output, 65_536); + for (const id of ["minimax-m2.5", "kimi-k2.5", "glm-5.1", "glm-5"]) { + assert.equal(go[id]?.reasoning, true, id); + assert.equal(go[id]?.tool_call, true, id); + assert.ok(go[id]?.limit?.context, id); + } + assert.equal(snapshot.providers.opencode?.models?.["deepseek-flash"], undefined); +}); + test("lets canonical models.dev entries override supplemental metadata", () => { const canonical = { id: "hy3-preview", name: "Canonical Hy3 preview", limit: { context: 999_999 }, reasoning: true, tool_call: true }; const snapshot = normalizeModelsDevSnapshot({ "opencode-go": { id: "opencode-go", models: { "hy3-preview": canonical } } }, 123); diff --git a/src/models/metadata.ts b/src/models/metadata.ts index 33754fa..b561100 100644 --- a/src/models/metadata.ts +++ b/src/models/metadata.ts @@ -46,6 +46,10 @@ export interface ModelsDevSnapshot { // Models served by OpenCode discovery before models.dev catalogs them. Each // entry mirrors the closest sibling model, is only used when models.dev lacks // the id, and is superseded by the canonical entry once it lands upstream. +// Mirrors only carry data that a canonical upstream entry already publishes +// for the same model; nothing is guessed. `deepseek-flash` mirrors the +// DeepSeek provider entry, and the anonymous-discovery Go ids mirror their +// Zen provider entries. const SUPPLEMENTAL_MODELS: Readonly>>> = { opencode: {}, "opencode-go": { @@ -59,6 +63,76 @@ const SUPPLEMENTAL_MODELS: Readonly