From 5f8de8c3f4ca0316196439712bcd01ca68bbbb26 Mon Sep 17 00:00:00 2001 From: Sally O'Malley Date: Wed, 8 Apr 2026 20:10:20 -0400 Subject: [PATCH] fix openrouter model picker refs (#63416) * fix openrouter model picker refs Signed-off-by: sallyom * test(ui): cover openrouter slash-id /model resolution --------- Signed-off-by: sallyom Co-authored-by: Vignesh Natarajan --- ui/src/ui/chat-model-ref.test.ts | 32 ++++++++++++++++++- ui/src/ui/chat-model-ref.ts | 23 ++++++++++++- .../chat/slash-command-executor.node.test.ts | 31 ++++++++++++++++++ 3 files changed, 84 insertions(+), 2 deletions(-) diff --git a/ui/src/ui/chat-model-ref.test.ts b/ui/src/ui/chat-model-ref.test.ts index 4e26a913c14..15b8e871ce8 100644 --- a/ui/src/ui/chat-model-ref.test.ts +++ b/ui/src/ui/chat-model-ref.test.ts @@ -35,6 +35,37 @@ describe("chat-model-ref helpers", () => { ); }); + it("prefixes provider-native model ids that already contain slashes", () => { + expect( + buildChatModelOption({ + id: "google/gemma-4-26b-a4b-it", + provider: "openrouter", + } as never), + ).toEqual({ + value: "openrouter/google/gemma-4-26b-a4b-it", + label: "google/gemma-4-26b-a4b-it · openrouter", + }); + expect( + resolvePreferredServerChatModel("Google/Gemma-4-26b-a4b-it", "openrouter", [ + { id: "google/gemma-4-26b-a4b-it", provider: "openrouter" } as never, + ]), + ).toEqual({ + value: "openrouter/google/gemma-4-26b-a4b-it", + source: "server", + }); + }); + + it("reuses already-qualified catalog ids without double-prefixing them", () => { + expect( + resolvePreferredServerChatModel("OpenAI/GPT-4O", "openai", [ + { id: "openai/gpt-4o", provider: "openai" } as never, + ]), + ).toEqual({ + value: "openai/gpt-4o", + source: "server", + }); + }); + it("normalizes raw overrides when the catalog match is unique", () => { expect(normalizeChatModelOverrideValue(createChatModelOverride("gpt-5-mini"), catalog)).toBe( "openai/gpt-5-mini", @@ -102,7 +133,6 @@ describe("chat-model-ref helpers", () => { ).toEqual({ value: "openai/gpt-5-mini", source: "server", - reason: "ambiguous", }); }); }); diff --git a/ui/src/ui/chat-model-ref.ts b/ui/src/ui/chat-model-ref.ts index 849bb679704..c045fcc8cf1 100644 --- a/ui/src/ui/chat-model-ref.ts +++ b/ui/src/ui/chat-model-ref.ts @@ -113,6 +113,20 @@ export function resolvePreferredServerChatModel( if (!trimmedModel) { return { value: "", source: "empty", reason: "empty" }; } + const trimmedProvider = provider?.trim(); + if (trimmedProvider) { + const providerMatch = catalog.find( + (entry) => + entry.provider?.trim().toLowerCase() === trimmedProvider.toLowerCase() && + entry.id.trim().toLowerCase() === trimmedModel.toLowerCase(), + ); + if (providerMatch) { + return { + value: buildChatModelOption(providerMatch).value, + source: "server", + }; + } + } const overrideResolution = resolveChatModelOverride( createChatModelOverride(trimmedModel), @@ -151,8 +165,15 @@ export function formatChatModelDisplay(value: string): string { export function buildChatModelOption(entry: ModelCatalogEntry): { value: string; label: string } { const provider = entry.provider?.trim(); + const value = (() => { + if (!provider) { + return entry.id; + } + const providerPrefix = `${provider.toLowerCase()}/`; + return entry.id.toLowerCase().startsWith(providerPrefix) ? entry.id : `${provider}/${entry.id}`; + })(); return { - value: buildQualifiedChatModelValue(entry.id, provider), + value, label: provider ? `${entry.id} · ${provider}` : entry.id, }; } diff --git a/ui/src/ui/chat/slash-command-executor.node.test.ts b/ui/src/ui/chat/slash-command-executor.node.test.ts index cbe6e926773..dac59631837 100644 --- a/ui/src/ui/chat/slash-command-executor.node.test.ts +++ b/ui/src/ui/chat/slash-command-executor.node.test.ts @@ -359,6 +359,37 @@ describe("executeSlashCommand directives", () => { }); }); + it("keeps openrouter-prefixed refs when patched model ids include slashes", async () => { + const request = vi.fn(async (method: string, _payload?: unknown) => { + if (method === "sessions.patch") { + return createResolvedModelPatch("google/gemma-4-26b-a4b-it", "openrouter"); + } + throw new Error(`unexpected method: ${method}`); + }); + + const result = await executeSlashCommand( + { request } as unknown as GatewayBrowserClient, + "main", + "model", + "google/gemma-4-26b-a4b-it", + { + chatModelCatalog: [ + { + id: "google/gemma-4-26b-a4b-it", + name: "Gemma 4 26B", + provider: "openrouter", + }, + ], + }, + ); + + expect(result.sessionPatch?.modelOverride).toEqual({ + kind: "qualified", + value: "openrouter/google/gemma-4-26b-a4b-it", + }); + expect(request).toHaveBeenCalledTimes(1); + }); + it("falls back to the patched server provider when catalog lookup fails", async () => { const request = vi.fn(async (method: string, _payload?: unknown) => { if (method === "sessions.patch") {