Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/resync-models-2026-09-28.md
Original file line number Diff line number Diff line change
@@ -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.
30 changes: 30 additions & 0 deletions src/models/catalog.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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": {
Expand Down Expand Up @@ -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<string, unknown>();
const cache = {
Expand Down
6 changes: 6 additions & 0 deletions src/models/catalog.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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";
}
22 changes: 22 additions & 0 deletions src/models/metadata.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
74 changes: 74 additions & 0 deletions src/models/metadata.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<Record<"opencode" | "opencode-go", Readonly<Record<string, ModelSource>>>> = {
opencode: {},
"opencode-go": {
Expand All @@ -59,6 +63,76 @@ const SUPPLEMENTAL_MODELS: Readonly<Record<"opencode" | "opencode-go", Readonly<
modalities: { input: ["text"] },
reasoning_options: [{ type: "effort", values: ["none", "low", "high"] }],
},
"deepseek-flash": {
id: "deepseek-flash",
name: "DeepSeek V4.1 Flash",
family: "deepseek-flash",
limit: { context: 1_000_000, output: 393_216 },
reasoning: true,
reasoning_options: [{ type: "toggle" }, { type: "effort", values: ["low", "high", "max"] }],
tool_call: true,
attachment: true,
modalities: { input: ["text", "image"] },
cost: { input: 0.15, output: 0.6, cache_read: 0.003 },
},
"minimax-m2.5": {
id: "minimax-m2.5",
name: "MiniMax-M2.5",
family: "minimax",
limit: { context: 204_800, output: 131_072 },
reasoning: true,
reasoning_options: [],
tool_call: true,
modalities: { input: ["text"] },
cost: { input: 0.3, output: 1.2, cache_read: 0.06 },
},
"kimi-k2.5": {
id: "kimi-k2.5",
name: "Kimi K2.5",
family: "kimi-k2",
limit: { context: 262_144, output: 65_536 },
reasoning: true,
reasoning_options: [{ type: "toggle" }],
tool_call: true,
attachment: true,
modalities: { input: ["text", "image", "video"] },
cost: { input: 0.6, output: 3, cache_read: 0.08 },
},
"glm-5.1": {
id: "glm-5.1",
name: "GLM-5.1",
family: "glm",
limit: { context: 204_800, output: 131_072 },
reasoning: true,
reasoning_options: [{ type: "toggle" }],
tool_call: true,
modalities: { input: ["text"] },
cost: { input: 1.4, output: 4.4, cache_read: 0.26 },
},
"glm-5": {
id: "glm-5",
name: "GLM-5",
family: "glm",
limit: { context: 204_800, output: 131_072 },
reasoning: true,
reasoning_options: [{ type: "toggle" }],
tool_call: true,
modalities: { input: ["text"] },
cost: { input: 1, output: 3.2, cache_read: 0.2 },
},
"qwen3.5-plus": {
id: "qwen3.5-plus",
name: "Qwen3.5 Plus",
family: "qwen3.5",
limit: { context: 262_144, output: 65_536 },
reasoning: true,
reasoning_options: [{ type: "toggle" }, { type: "budget_tokens", max: 81_920 }],
tool_call: true,
attachment: true,
modalities: { input: ["text", "image", "video"] },
provider: { npm: "@ai-sdk/anthropic" },
cost: { input: 0.2, output: 1.2, cache_read: 0.02 },
},
},
};

Expand Down
Loading