diff --git a/src/renderer/components/common/ProviderModelMenu/parts/modelShortcutLabel.test.ts b/src/renderer/components/common/ProviderModelMenu/parts/modelShortcutLabel.test.ts index f2be2eba4..743cb9fa6 100644 --- a/src/renderer/components/common/ProviderModelMenu/parts/modelShortcutLabel.test.ts +++ b/src/renderer/components/common/ProviderModelMenu/parts/modelShortcutLabel.test.ts @@ -36,6 +36,9 @@ describe("formatShortcutModelLabel", () => { expect(formatCursorBaseModelLabel("default")).toBe("Auto"); expect(formatCursorBaseModelLabel("composer-2.5")).toBe("Composer 2.5"); expect(formatCursorBaseModelLabel("claude-opus-4-1")).toBe("Opus 4.1"); + expect(formatCursorBaseModelLabel("claude-sonnet-4-5-20250929")).toBe("Sonnet 4.5 (20250929)"); + expect(formatCursorBaseModelLabel("gpt-5.4-mini")).toBe("GPT-5.4 Mini"); + expect(formatCursorBaseModelLabel("gpt-5.6-luna")).toBe("GPT-5.6 Luna"); expect(formatCursorBaseModelLabel("gemini-3-pro")).toBe("Gemini 3 Pro"); expect(formatCursorBaseModelLabel("mystery-model")).toBe("Mystery Model"); expect(formatCursorBaseModelLabel("mystery-model", "Mystery model")).toBe("Mystery model"); diff --git a/src/shared/modelLabels.ts b/src/shared/modelLabels.ts index 8818ec02b..9c1e6faed 100644 --- a/src/shared/modelLabels.ts +++ b/src/shared/modelLabels.ts @@ -74,16 +74,18 @@ export function formatCursorBaseModelLabel(baseId: string, fallbackLabel?: strin const codex = formatCodexFamilyModelLabel(baseId); if (codex) return codex; - const gpt = /^gpt-(\d+(?:\.\d+)?)(?:-(mini|nano))?$/i.exec(baseId); + const gpt = /^gpt-(\d+(?:\.\d+)?)(?:-([a-z]+))?$/i.exec(baseId); if (gpt) { const suffix = gpt[2] ? ` ${capitalizeSegment(gpt[2])}` : ""; return `GPT-${gpt[1]}${suffix}`; } - const claude = /^claude-(opus|sonnet|haiku)-(\d+)(?:-(\d+))?$/i.exec(baseId); + // Accepts an optional 8-digit snapshot date: "claude-sonnet-4-5-20250929". + const claude = /^claude-(opus|sonnet|haiku)-(\d+)(?:-(\d{1,2}))?(?:-(\d{8}))?$/i.exec(baseId); if (claude) { const version = claude[3] ? `${claude[2]}.${claude[3]}` : claude[2]!; - return `${capitalizeSegment(claude[1]!)} ${version}`; + const snapshot = claude[4] ? ` (${claude[4]})` : ""; + return `${capitalizeSegment(claude[1]!)} ${version}${snapshot}`; } const family = /^(gemini|grok|kimi)-(.+)$/i.exec(baseId); diff --git a/src/supervisor/agents/pi/detection.ts b/src/supervisor/agents/pi/detection.ts index 701ef3163..c7c0d2207 100644 --- a/src/supervisor/agents/pi/detection.ts +++ b/src/supervisor/agents/pi/detection.ts @@ -2,6 +2,7 @@ import { existsSync, readFileSync } from "node:fs"; import { homedir } from "node:os"; import { join } from "node:path"; import type { AgentCapability, AgentProviderMetadata, ProjectLocation } from "@/shared/contracts"; +import { formatCursorBaseModelLabel } from "@/shared/modelLabels"; import { batchWslCommandsAsync, envVarAuthProbe, @@ -76,6 +77,14 @@ export interface PiCliModel { reasoning: boolean; } +/** Display label for a Pi model id ("anthropic/claude-sonnet-4-5" → "Sonnet 4.5"). */ +export function humanizePiModelId(id: string): string { + // Take the segment after the last "/" so nested router ids + // ("openrouter/anthropic/claude-sonnet-4-5") still hit the canonical rules. + const slash = id.lastIndexOf("/"); + return formatCursorBaseModelLabel(slash >= 0 ? id.slice(slash + 1) : id); +} + export function parsePiModelList(stdout: string): PiCliModel[] { return stdout .split(/\r?\n/u) @@ -104,7 +113,7 @@ async function probePiCapabilities( ); const base: CapabilitiesProbeResult = { ...piDefaultCapabilities, - models: models.map((model) => ({ id: model.id, label: model.id.split("/", 2)[1]! })), + models: models.map((model) => ({ id: model.id, label: humanizePiModelId(model.id) })), efforts: [...new Set(Object.values(modelEfforts).flat())], modelEfforts, ...(models.length > 0 ? { authState: "authenticated" as const } : {}), diff --git a/src/supervisor/agents/pi/pi.test.ts b/src/supervisor/agents/pi/pi.test.ts index 75716af8c..36ca9a76f 100644 --- a/src/supervisor/agents/pi/pi.test.ts +++ b/src/supervisor/agents/pi/pi.test.ts @@ -2,7 +2,12 @@ import { describe, expect, it } from "vitest"; import type { ProjectLocation, ThreadConfig } from "@/shared/contracts"; import { createKnownSessionRef } from "../base"; import { buildPiArgs, buildPiOneShotArgs, splitPiModelId } from "./argv"; -import { parsePiModelList, piDefaultCapabilities, piDetectionSpec } from "./detection"; +import { + humanizePiModelId, + parsePiModelList, + piDefaultCapabilities, + piDetectionSpec, +} from "./detection"; import { createPiAdapter } from "./index"; const location = { kind: "posix", path: "/tmp/pi-project" } as ProjectLocation; @@ -163,4 +168,15 @@ openai gpt-4.1-mini 1M 32K no yes`), { id: "openai/gpt-4.1-mini", reasoning: false }, ]); }); + + it("beautifies probed model ids into picker labels", () => { + expect(humanizePiModelId("anthropic/claude-sonnet-4-5")).toBe("Sonnet 4.5"); + expect(humanizePiModelId("anthropic/claude-sonnet-4-5-20250929")).toBe("Sonnet 4.5 (20250929)"); + expect(humanizePiModelId("openai/gpt-5.4-mini")).toBe("GPT-5.4 Mini"); + expect(humanizePiModelId("openai-codex/gpt-5.3-codex-spark")).toBe("Codex 5.3 Spark"); + expect(humanizePiModelId("openai/gpt-5.6-luna")).toBe("GPT-5.6 Luna"); + expect(humanizePiModelId("google/gemini-3-pro")).toBe("Gemini 3 Pro"); + expect(humanizePiModelId("openrouter/anthropic/claude-sonnet-4-5")).toBe("Sonnet 4.5"); + expect(humanizePiModelId("xai/grok-code-fast-1")).toBe("Grok Code Fast 1"); + }); });