diff --git a/plugins/web-ui/src/model-options.ts b/plugins/web-ui/src/model-options.ts index 5ee2c799..1b5752c5 100644 --- a/plugins/web-ui/src/model-options.ts +++ b/plugins/web-ui/src/model-options.ts @@ -78,7 +78,7 @@ function buildOption( id: string, harnessId = "pi", qualified = false, - catalog: Readonly> = {}, + catalog: Readonly> = {}, ): ModelOption | null { try { const dynamic = catalog[id]; @@ -101,7 +101,7 @@ function buildOptions( ids: readonly string[], harnessId = "pi", qualified = false, - catalog: Readonly> = {}, + catalog: Readonly> = {}, ): ModelOption[] { const seen = new Set(); const out: ModelOption[] = []; @@ -154,7 +154,7 @@ export function applyPickerModelIds(ids: readonly string[] | null | undefined, b export function runtimeModelOptions( approvedHarnesses: readonly string[], modelsByHarness: Readonly>, - catalog: Readonly> = {}, + catalog: Readonly> = {}, ): ModelOption[] { const options = approvedHarnesses.flatMap((harnessId) => { const configured = buildOptions(modelsByHarness[harnessId] ?? [], harnessId, true, catalog); @@ -170,7 +170,7 @@ export function applyRuntimeOptions( approvedHarnesses: readonly string[], modelsByHarness: Readonly>, effective: { harnessId: string; modelId: string }, - catalog: Readonly> = {}, + catalog: Readonly> = {}, ): void { const options = runtimeModelOptions(approvedHarnesses, modelsByHarness, catalog); const applied = { options, defaultValue: `${effective.harnessId}:${effective.modelId}` }; diff --git a/plugins/web-ui/src/pi-models.ts b/plugins/web-ui/src/pi-models.ts index ca2128a3..3b770166 100644 --- a/plugins/web-ui/src/pi-models.ts +++ b/plugins/web-ui/src/pi-models.ts @@ -22,7 +22,7 @@ function builtinModel(id: string): PiModel | undefined { return undefined; } -export function getBaseModel(id: string, fallback?: { name: string; provider: string }): PiModel { +export function getBaseModel(id: string, fallback?: { name: string; provider: string; api?: string }): PiModel { const builtin = builtinModel(id); if (builtin) return builtin; const clone = CLONE_TEMPLATES[id]; @@ -30,9 +30,16 @@ export function getBaseModel(id: string, fallback?: { name: string; provider: st const template = builtinModel(clone.template); if (template) return cloneModel(template, id, clone.name); } - if (fallback?.provider === "openrouter") { - const template = getModel("openrouter", "openrouter/auto" as Parameters[1]) as PiModel | undefined; - if (template) return cloneModel(template, id, fallback.name); + if (fallback) { + const templateId = fallback.api === "anthropic-messages" ? "claude-sonnet-4-6" : "gpt-5.5"; + const template = + fallback.provider === "openrouter" + ? (getModel("openrouter", "openrouter/auto" as Parameters[1]) as PiModel | undefined) + : builtinModel(templateId); + if (template) { + const cloned = cloneModel(template, id, fallback.name); + return { ...cloned, provider: fallback.provider, ...(fallback.api ? { api: fallback.api } : {}) } as PiModel; + } } throw new Error(`Unsupported model: ${id}`); } diff --git a/plugins/web-ui/test/pi-models.test.ts b/plugins/web-ui/test/pi-models.test.ts index 8f586d46..476ba813 100644 --- a/plugins/web-ui/test/pi-models.test.ts +++ b/plugins/web-ui/test/pi-models.test.ts @@ -40,3 +40,25 @@ test("fast-mode support is fed from core's runtime config, not a hardcoded clien assert.equal(modelSupportsFastMode(null, "claude-haiku-4-5"), false); assert.equal(modelSupportsFastMode(null, undefined), false); }); + +test("web UI resolves custom-provider models regardless of provider id", () => { + const openaiProtocol = getBaseModel("acme-large", { + name: "Acme Large", + provider: "acme", + api: "openai-completions", + }); + assert.equal(openaiProtocol.id, "acme-large"); + assert.equal(openaiProtocol.name, "Acme Large"); + assert.equal(openaiProtocol.provider, "acme"); + assert.equal(openaiProtocol.api, "openai-completions"); + + const anthropicProtocol = getBaseModel("vendor-messages-model", { + name: "Vendor Messages Model", + provider: "vendor", + api: "anthropic-messages", + }); + assert.equal(anthropicProtocol.provider, "vendor"); + assert.equal(anthropicProtocol.api, "anthropic-messages"); + + assert.throws(() => getBaseModel("still-unknown"), /Unsupported/); +}); diff --git a/src/api/routes/surface.ts b/src/api/routes/surface.ts index 3fbe270a..f957c76e 100644 --- a/src/api/routes/surface.ts +++ b/src/api/routes/surface.ts @@ -1164,10 +1164,12 @@ async function runtimeConfigBody(ctx: ApiCtx, scope: ScopeId): Promise { - const model = catalog.find((candidate) => candidate.id === id); - if (model) return [[id, { name: model.name, provider: model.provider }]]; const resolved = resolveModel(id); - return resolved ? [[id, { name: resolved.name, provider: resolved.provider }]] : []; + const model = catalog.find((candidate) => candidate.id === id); + const name = model?.name ?? resolved?.name; + const provider = model?.provider ?? resolved?.provider; + if (!name || !provider) return []; + return [[id, { name, provider, ...(resolved?.api ? { api: resolved.api } : {}) }]]; }), ); return {