fix(opencode): preserve catalog display names (#13168)

* fix(opencode): preserve catalog display names

* test(cli): cover OpenCode catalog display-name precedence

Adds the automated unit test the PR body's manual smoke check
(Auto Chat / DeepSeek V4 Pro) was standing in for, covering all four
name-precedence branches: existing custom name, catalog display_name,
native catalog name with owned_by prefix stripped, and the auto/* readable
fallback. Also adds the changelog.d/fixes/ fragment.

Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>

---------

Co-authored-by: ginettododo <117327638+ginettododo@users.noreply.github.com>
Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
This commit is contained in:
Domenico Massafra
2026-09-18 17:24:03 +02:00
committed by GitHub
parent 93be9d544c
commit fbe195d69e
3 changed files with 191 additions and 2 deletions

View File

@@ -0,0 +1 @@
- **fix(cli):** OpenCode config generator preserves catalog display names — custom names win, then `display_name`/native `name` (with the `owned_by/` prefix stripped once), then a readable label for `auto/*` ids, instead of always showing the raw model id ([#13168](https://github.com/diegosouzapw/OmniRoute/pull/13168)) — thanks @domenicomassafra

View File

@@ -40,6 +40,8 @@ export function assertSafeCatalogUrl(rawUrl: string): URL {
interface CatalogModelEntry {
id: string;
owned_by?: string;
name?: string;
display_name?: string;
/** OpenAI-compatible field name; some upstreams return this. */
context_length?: number;
max_context_window_tokens?: number;
@@ -254,8 +256,22 @@ function buildModelEntry(
catalog: CatalogModelEntry | undefined,
existing: ExistingModelEntry | undefined
): ExistingModelEntry {
// Carry over user-set "name" first; fall back to id when absent.
const name = (typeof existing?.name === "string" && existing.name.trim()) || id;
// Carry over user-set names first, then native catalog display metadata.
// Technical ids remain map keys; names are presentation only.
const catalogName = catalog?.display_name ?? catalog?.name;
const nativeName = typeof catalogName === "string" ? catalogName.trim() : "";
const providerPrefix = catalog?.owned_by ? `${catalog.owned_by}/` : "";
const modelName = nativeName.startsWith(providerPrefix)
? nativeName.slice(providerPrefix.length)
: nativeName;
const autoName = id.startsWith("auto/")
? `Auto ${id.slice("auto/".length).replace(/(^|[-_])([a-z])/g, (_, separator, letter) => `${separator === "" ? "" : " "}${letter.toUpperCase()}`)}`
: "";
const name =
(typeof existing?.name === "string" && existing.name.trim() !== id && existing.name.trim()) ||
autoName ||
modelName ||
id;
const entry: ExistingModelEntry = { name };

View File

@@ -0,0 +1,172 @@
import { describe, it } from "node:test";
import assert from "node:assert";
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
/**
* Regression guard for #13168: the OpenCode config generator used to emit
* the raw catalog model id as the display name, so `auto/chat` stayed
* `auto/chat` and provider-prefixed catalog labels duplicated the provider
* name. `buildModelEntry()`'s name precedence (highest to lowest) is:
* 1. an existing custom name already set by the user in opencode.json
* 2. a readable label for `auto/*` ids (OmniRoute's own virtual routing ids)
* 3. the catalog's `display_name` (falling back to `name`), with a leading
* `owned_by/` prefix stripped once
* 4. the raw catalog/model id, when nothing else is known
*/
describe("opencode config generator — catalog display-name precedence (#13168)", () => {
function makeCatalogResponse(models: unknown[]): unknown {
return { object: "list", data: models };
}
function stubFetchOnce(body: unknown, status = 200) {
const original = globalThis.fetch;
// @ts-ignore — globalThis.fetch signature is compatible for our purposes
globalThis.fetch = (async () => {
return new Response(JSON.stringify(body), {
status,
headers: { "content-type": "application/json" },
});
}) as typeof fetch;
return {
restore: () => {
globalThis.fetch = original;
},
};
}
function writeTempConfig(config: unknown): string {
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "opencode-config-13168-"));
const file = path.join(dir, "opencode.json");
fs.writeFileSync(file, JSON.stringify(config), "utf8");
return file;
}
it("1. an existing custom name wins over any catalog name", async () => {
const configPath = writeTempConfig({
provider: {
omniroute: {
models: {
"vendor/model-a": { name: "My Custom Label" },
},
},
},
});
const stub = stubFetchOnce(
makeCatalogResponse([
{ id: "vendor/model-a", owned_by: "vendor", display_name: "Ignored Catalog Name" },
])
);
try {
const { generateOpencodeConfig } =
await import("../../src/lib/cli-helper/config-generator/opencode.ts");
const out = await generateOpencodeConfig({
baseUrl: "http://localhost:20128",
apiKey: "sk-test",
configPath,
});
const cfg = JSON.parse(out);
assert.strictEqual(cfg.provider.omniroute.models["vendor/model-a"].name, "My Custom Label");
} finally {
stub.restore();
fs.rmSync(path.dirname(configPath), { recursive: true, force: true });
}
});
it("2. catalog display_name is preferred over the raw id and native name, stripping the owned_by prefix", async () => {
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "opencode-config-13168-"));
const configPath = path.join(tmpDir, "missing.json");
const stub = stubFetchOnce(
makeCatalogResponse([
{
id: "vendor/model-b",
owned_by: "vendor",
display_name: "vendor/Pretty Name",
name: "raw-native-name",
},
])
);
try {
const { generateOpencodeConfig } =
await import("../../src/lib/cli-helper/config-generator/opencode.ts");
const out = await generateOpencodeConfig({
baseUrl: "http://localhost:20128",
apiKey: "sk-test",
configPath,
});
const cfg = JSON.parse(out);
assert.strictEqual(cfg.provider.omniroute.models["vendor/model-b"].name, "Pretty Name");
} finally {
stub.restore();
fs.rmSync(tmpDir, { recursive: true, force: true });
}
});
it("3. catalog native `name` is used when display_name is absent", async () => {
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "opencode-config-13168-"));
const configPath = path.join(tmpDir, "missing.json");
const stub = stubFetchOnce(
makeCatalogResponse([
{ id: "vendor/model-c", owned_by: "vendor", name: "vendor/Native Label" },
])
);
try {
const { generateOpencodeConfig } =
await import("../../src/lib/cli-helper/config-generator/opencode.ts");
const out = await generateOpencodeConfig({
baseUrl: "http://localhost:20128",
apiKey: "sk-test",
configPath,
});
const cfg = JSON.parse(out);
assert.strictEqual(cfg.provider.omniroute.models["vendor/model-c"].name, "Native Label");
} finally {
stub.restore();
fs.rmSync(tmpDir, { recursive: true, force: true });
}
});
it("4. `auto/*` ids fall back to a readable label when no catalog metadata names them", async () => {
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "opencode-config-13168-"));
const configPath = path.join(tmpDir, "missing.json");
const stub = stubFetchOnce(makeCatalogResponse([{ id: "auto/chat-turbo" }]));
try {
const { generateOpencodeConfig } =
await import("../../src/lib/cli-helper/config-generator/opencode.ts");
const out = await generateOpencodeConfig({
baseUrl: "http://localhost:20128",
apiKey: "sk-test",
configPath,
});
const cfg = JSON.parse(out);
assert.strictEqual(cfg.provider.omniroute.models["auto/chat-turbo"].name, "Auto Chat Turbo");
} finally {
stub.restore();
fs.rmSync(tmpDir, { recursive: true, force: true });
}
});
it("5. raw catalog model id is used when neither custom name, display_name, name, nor auto/* apply", async () => {
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "opencode-config-13168-"));
const configPath = path.join(tmpDir, "missing.json");
const stub = stubFetchOnce(makeCatalogResponse([{ id: "custom-vendor/raw-model-id" }]));
try {
const { generateOpencodeConfig } =
await import("../../src/lib/cli-helper/config-generator/opencode.ts");
const out = await generateOpencodeConfig({
baseUrl: "http://localhost:20128",
apiKey: "sk-test",
configPath,
});
const cfg = JSON.parse(out);
assert.strictEqual(
cfg.provider.omniroute.models["custom-vendor/raw-model-id"].name,
"custom-vendor/raw-model-id"
);
} finally {
stub.restore();
fs.rmSync(tmpDir, { recursive: true, force: true });
}
});
});