From ba37dd88dc9a789a05c4532f14ce6d026795e93a Mon Sep 17 00:00:00 2001 From: Jasper <241105014+jipeng6036-del@users.noreply.github.com> Date: Fri, 9 Oct 2026 04:24:40 -0700 Subject: [PATCH] fix: distinguish model options with duplicate labels Signed-off-by: Jasper <241105014+jipeng6036-del@users.noreply.github.com> --- src/session-model.ts | 12 ++++- src/tests/model-presentation.test.ts | 68 +++++++++++++++++++++++++++- 2 files changed, 77 insertions(+), 3 deletions(-) diff --git a/src/session-model.ts b/src/session-model.ts index 56b616b4..5d8af570 100644 --- a/src/session-model.ts +++ b/src/session-model.ts @@ -496,12 +496,20 @@ export async function getAvailableModels( for (const name of displayNames) { displayNameCounts.set(name, (displayNameCounts.get(name) ?? 0) + 1); } + const names = models.map((model, index) => + displayNameCounts.get(displayNames[index]) === 1 ? displayNames[index] : model.displayName, + ); + const nameCounts = new Map(); + for (const name of names) { + nameCounts.set(name, (nameCounts.get(name) ?? 0) + 1); + } return { availableModels: models.map((model, index) => ({ modelId: model.value, - name: - displayNameCounts.get(displayNames[index]) === 1 ? displayNames[index] : model.displayName, + // Allowlisted aliases can inherit the same SDK label. Keep every ID + // selectable, but distinguish the rows for clients that show only names. + name: nameCounts.get(names[index]) === 1 ? names[index] : `${names[index]} (${model.value})`, description: model.description, })), currentModelId: currentModel.value, diff --git a/src/tests/model-presentation.test.ts b/src/tests/model-presentation.test.ts index e8ee2edf..ad6b403b 100644 --- a/src/tests/model-presentation.test.ts +++ b/src/tests/model-presentation.test.ts @@ -1,6 +1,10 @@ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import { query, type ModelInfo, type Query } from "@anthropic-ai/claude-agent-sdk"; -import { buildModelConfigOption, getAvailableModels } from "../session-model.js"; +import { + applyAvailableModelsAllowlist, + buildModelConfigOption, + getAvailableModels, +} from "../session-model.js"; const OPUS: ModelInfo = { value: "opus[1m]", @@ -151,6 +155,68 @@ describe("versioned model display names", () => { ); expect(state.availableModels.map((model) => model.name)).toEqual(["Opus (1M context)", "Opus"]); }); + + it("distinguishes allowlisted aliases and pins that inherit the same SDK label", async () => { + const sdkModels: ModelInfo[] = [ + { value: "default", displayName: "Default (recommended)", description: "" }, + { + value: "opus", + resolvedModel: "claude-opus-5-5", + displayName: "Opus 5.5", + description: "For complex work and everyday tasks", + }, + { value: "haiku", displayName: "Haiku", description: "Fast" }, + ]; + const ids = ["claude-opus-5-5[1m]", "claude-opus-5-5", "opus[1m]", "opus", "haiku"]; + const models = applyAvailableModelsAllowlist(sdkModels, ids); + const setModel = vi.fn(); + const state = await getAvailableModels( + { setModel } as unknown as Query, + models, + sdkModels, + { getSettings: () => ({}) }, + { log: vi.fn(), error: vi.fn() }, + false, + "model-presentation-test", + ); + + const expected = [ + "Default (recommended)", + ...ids.slice(0, 4).map((id) => `Opus 5.5 (${id})`), + "Haiku", + ]; + expect(state.availableModels.map((model) => model.name)).toEqual(expected); + expect(state.availableModels.map((model) => model.modelId)).toEqual(["default", ...ids]); + expect(state.currentModelId).toBe("default"); + expect(state.availableModels.slice(1, 5).map((model) => model.description)).toEqual( + Array(4).fill(sdkModels[1].description), + ); + const option = buildModelConfigOption(state, models, false); + expect(option).toMatchObject({ currentValue: "default" }); + expect(option.type === "select" && option.options.map((model) => model.name)).toEqual(expected); + expect(sdkModels[1].displayName).toBe("Opus 5.5"); + expect(setModel).not.toHaveBeenCalled(); + }); + + it("distinguishes identical custom labels without changing their model IDs", async () => { + const models: ModelInfo[] = [ + { value: "provider-a", displayName: "My model", description: "First endpoint" }, + { value: "provider-b", displayName: "My model", description: "Second endpoint" }, + ]; + const state = await getAvailableModels( + {} as Query, + models, + models, + { getSettings: () => ({}) }, + { log: vi.fn(), error: vi.fn() }, + false, + "model-presentation-test", + ); + expect(state.availableModels).toEqual([ + { modelId: "provider-a", name: "My model (provider-a)", description: "First endpoint" }, + { modelId: "provider-b", name: "My model (provider-b)", description: "Second endpoint" }, + ]); + }); }); // Account-dependent model availability requires the same authenticated environment