mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-09-19 13:23:50 +03:00
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:
committed by
GitHub
parent
93be9d544c
commit
fbe195d69e
@@ -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
|
||||
@@ -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 };
|
||||
|
||||
|
||||
172
tests/unit/opencode-catalog-display-names-13168.test.ts
Normal file
172
tests/unit/opencode-catalog-display-names-13168.test.ts
Normal 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 });
|
||||
}
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user