Skip to content
Open
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
8 changes: 4 additions & 4 deletions plugins/web-ui/src/model-options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ function buildOption(
id: string,
harnessId = "pi",
qualified = false,
catalog: Readonly<Record<string, { name: string; provider: string }>> = {},
catalog: Readonly<Record<string, { name: string; provider: string; api?: string }>> = {},
): ModelOption | null {
Comment on lines 79 to 82
try {
const dynamic = catalog[id];
Expand All @@ -101,7 +101,7 @@ function buildOptions(
ids: readonly string[],
harnessId = "pi",
qualified = false,
catalog: Readonly<Record<string, { name: string; provider: string }>> = {},
catalog: Readonly<Record<string, { name: string; provider: string; api?: string }>> = {},
): ModelOption[] {
const seen = new Set<string>();
const out: ModelOption[] = [];
Expand Down Expand Up @@ -154,7 +154,7 @@ export function applyPickerModelIds(ids: readonly string[] | null | undefined, b
export function runtimeModelOptions(
approvedHarnesses: readonly string[],
modelsByHarness: Readonly<Record<string, readonly string[]>>,
catalog: Readonly<Record<string, { name: string; provider: string }>> = {},
catalog: Readonly<Record<string, { name: string; provider: string; api?: string }>> = {},
): ModelOption[] {
const options = approvedHarnesses.flatMap((harnessId) => {
const configured = buildOptions(modelsByHarness[harnessId] ?? [], harnessId, true, catalog);
Expand All @@ -170,7 +170,7 @@ export function applyRuntimeOptions(
approvedHarnesses: readonly string[],
modelsByHarness: Readonly<Record<string, readonly string[]>>,
effective: { harnessId: string; modelId: string },
catalog: Readonly<Record<string, { name: string; provider: string }>> = {},
catalog: Readonly<Record<string, { name: string; provider: string; api?: string }>> = {},
): void {
const options = runtimeModelOptions(approvedHarnesses, modelsByHarness, catalog);
const applied = { options, defaultValue: `${effective.harnessId}:${effective.modelId}` };
Expand Down
15 changes: 11 additions & 4 deletions plugins/web-ui/src/pi-models.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,17 +22,24 @@ 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];
if (clone) {
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<typeof getModel>[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<typeof getModel>[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;
}
Comment on lines +25 to +42
}
throw new Error(`Unsupported model: ${id}`);
}
Expand Down
22 changes: 22 additions & 0 deletions plugins/web-ui/test/pi-models.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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/);
});
8 changes: 5 additions & 3 deletions src/api/routes/surface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1164,10 +1164,12 @@ async function runtimeConfigBody(ctx: ApiCtx, scope: ScopeId): Promise<Record<st
const advertisedModelIds = new Set(Object.values(modelsByHarness).flat());
const modelCatalog = Object.fromEntries(
[...advertisedModelIds].flatMap((id) => {
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 {
Expand Down