diff --git a/apps/ade-cli/src/adeRpcServer.test.ts b/apps/ade-cli/src/adeRpcServer.test.ts index 41f1834b1..7f6b28956 100644 --- a/apps/ade-cli/src/adeRpcServer.test.ts +++ b/apps/ade-cli/src/adeRpcServer.test.ts @@ -1559,7 +1559,7 @@ describe("adeRpcServer", () => { return await callTool(handler, "spawn_agent", { laneId: "lane-1", provider: "claude", - model: "claude-sonnet-4-6", + model: "claude-sonnet-5", prompt: "Implement API wiring", title: "Orchestrator Spawn" }); @@ -1574,7 +1574,7 @@ describe("adeRpcServer", () => { tracked: true, toolType: "claude-orchestrated", command: claudePath, - args: expect.arrayContaining(["--model", "claude-sonnet-4-6", "--permission-mode", "default"]), + args: expect.arrayContaining(["--model", "claude-sonnet-5", "--permission-mode", "default"]), env: expect.objectContaining({ ADE_DEFAULT_ROLE: "agent", }), @@ -1993,7 +1993,7 @@ describe("adeRpcServer", () => { return await callTool(handler, "spawn_agent", { laneId: "lane-1", provider: "claude", - model: "claude-sonnet-4-6", + model: "claude-sonnet-5", prompt: "Implement API wiring", title: "Orchestrator Spawn", runId: "run-1", @@ -2083,7 +2083,7 @@ describe("adeRpcServer", () => { const response = await callTool(handler, "spawn_agent", { laneId: "lane-1", provider: "claude", - model: "claude-sonnet-4-6", + model: "claude-sonnet-5", permissionMode: "config-toml", prompt: "Implement API wiring", }); @@ -2106,7 +2106,7 @@ describe("adeRpcServer", () => { const response = await callTool(handler, "spawn_agent", { laneId: "lane-1", provider: "claude", - model: "claude-sonnet-4-6", + model: "claude-sonnet-5", prompt: "Implement API wiring", title: "Orchestrator Spawn", runId: "run-1", diff --git a/apps/ade-cli/src/cli.test.ts b/apps/ade-cli/src/cli.test.ts index dc78aea30..28a40f28c 100644 --- a/apps/ade-cli/src/cli.test.ts +++ b/apps/ade-cli/src/cli.test.ts @@ -4448,7 +4448,7 @@ describe("ADE CLI", () => { id: "legacy-rule", actions: [ { type: "create-lane", laneNameTemplate: "{{trigger.issue.title}}" }, - { type: "agent-session", modelId: "claude-opus-4-7" }, + { type: "agent-session", modelId: "claude-opus-4-8" }, ], }); const plan = buildCliPlan(["automations", "create", "--text", draft]); diff --git a/apps/ade-cli/src/headlessLinearServices.test.ts b/apps/ade-cli/src/headlessLinearServices.test.ts index 7954b26ff..a1665846b 100644 --- a/apps/ade-cli/src/headlessLinearServices.test.ts +++ b/apps/ade-cli/src/headlessLinearServices.test.ts @@ -545,7 +545,7 @@ describe("headlessLinearServices", () => { expect(codex.model).toBe("gpt-5.5"); expect(codex.modelId).toBe("openai/gpt-5.5"); - expect(claude.model).toBe("opus-4.8-1m"); + expect(claude.model).toBe("claude-opus-4-8"); expect(claude.modelId).toBe("anthropic/claude-opus-4-8"); services.dispose(); diff --git a/apps/ade-cli/src/services/modelPickerStore.test.ts b/apps/ade-cli/src/services/modelPickerStore.test.ts index 6e40bbcac..2e2565969 100644 --- a/apps/ade-cli/src/services/modelPickerStore.test.ts +++ b/apps/ade-cli/src/services/modelPickerStore.test.ts @@ -57,14 +57,14 @@ describe("modelPickerStore (db-backed)", () => { const { db, root } = await makeDb(); const store = createModelPickerStore({ db, legacyFilePath: noMigration(root) }); - const first = store.toggleFavorite("claude-opus-4-7"); - expect(first).toEqual({ favorites: ["claude-opus-4-7"], isFavorite: true }); + const first = store.toggleFavorite("claude-opus-4-8"); + expect(first).toEqual({ favorites: ["claude-opus-4-8"], isFavorite: true }); const second = store.toggleFavorite("gpt-5"); - expect(second.favorites).toEqual(["claude-opus-4-7", "gpt-5"]); + expect(second.favorites).toEqual(["claude-opus-4-8", "gpt-5"]); expect(second.isFavorite).toBe(true); - const third = store.toggleFavorite("claude-opus-4-7"); + const third = store.toggleFavorite("claude-opus-4-8"); expect(third).toEqual({ favorites: ["gpt-5"], isFavorite: false }); }); diff --git a/apps/ade-cli/src/tuiClient/__tests__/RightPane.test.tsx b/apps/ade-cli/src/tuiClient/__tests__/RightPane.test.tsx index e1cf0f4e7..34639a37b 100644 --- a/apps/ade-cli/src/tuiClient/__tests__/RightPane.test.tsx +++ b/apps/ade-cli/src/tuiClient/__tests__/RightPane.test.tsx @@ -109,7 +109,7 @@ describe("RightPane chat info", () => { { }} modelPickerInputs={{ models: [ - { id: "anthropic/claude-sonnet-4-6", displayName: "Claude Sonnet 4.6", isDefault: true }, + { id: "anthropic/claude-sonnet-5", displayName: "Claude Sonnet 5", isDefault: true }, ], favorites: [], recents: [], - activeModelId: "anthropic/claude-sonnet-4-6", + activeModelId: "anthropic/claude-sonnet-5", activeReasoningEffort: "high", }} focused @@ -829,7 +829,7 @@ describe("RightPane setup panes", () => { const frame = stripAnsi(result.lastFrame() ?? ""); expect(frame).toContain("MODEL"); - expect(frame).toContain("Claude Sonnet 4.6"); + expect(frame).toContain("Claude Sonnet 5"); // Reasoning is shown in the settings footer, not as an inline per-model // "think high" chip (that detail was removed from model rows). expect(frame).toContain("reasoning"); diff --git a/apps/ade-cli/src/tuiClient/__tests__/TerminalPane.test.tsx b/apps/ade-cli/src/tuiClient/__tests__/TerminalPane.test.tsx index e4b107891..34a16d0c1 100644 --- a/apps/ade-cli/src/tuiClient/__tests__/TerminalPane.test.tsx +++ b/apps/ade-cli/src/tuiClient/__tests__/TerminalPane.test.tsx @@ -414,7 +414,7 @@ describe("TerminalPane", () => { "││What's new│", "│Welcome back Arul!│Added `claude agents --json` to list live Claude sessions", "│ ▐▛███▜▌│Statusline JSON input now includes GitHub repo and PR information", - "│Sonnet 4.6 with low effort · Claude Max · ││", + "│Sonnet 5 with low effort · Claude Max · ││", "│~/Projects/ADE/.ade/worktrees/scratch-tui-claude-audit││", "○ low · /effort", "❯ sayokthenstop", diff --git a/apps/ade-cli/src/tuiClient/__tests__/adeApi.test.ts b/apps/ade-cli/src/tuiClient/__tests__/adeApi.test.ts index 7a7a5db8d..9cd899e83 100644 --- a/apps/ade-cli/src/tuiClient/__tests__/adeApi.test.ts +++ b/apps/ade-cli/src/tuiClient/__tests__/adeApi.test.ts @@ -591,7 +591,7 @@ describe("startCliTerminalSession", () => { provider: "claude", laneId: "lane-1", title: "Claude smoke", - model: "anthropic/claude-sonnet-4-6", + model: "anthropic/claude-sonnet-5", reasoningEffort: "low", permissionMode: "auto", initialInput: "Hello", @@ -606,7 +606,7 @@ describe("startCliTerminalSession", () => { laneId: "lane-1", provider: "claude", title: "Claude smoke", - model: "anthropic/claude-sonnet-4-6", + model: "anthropic/claude-sonnet-5", reasoningEffort: "low", permissionMode: "auto", initialInput: "Hello", diff --git a/apps/ade-cli/src/tuiClient/__tests__/appInput.test.ts b/apps/ade-cli/src/tuiClient/__tests__/appInput.test.ts index d82b9c32d..2d28be2f2 100644 --- a/apps/ade-cli/src/tuiClient/__tests__/appInput.test.ts +++ b/apps/ade-cli/src/tuiClient/__tests__/appInput.test.ts @@ -627,7 +627,7 @@ describe("right pane context defaults", () => { function chatInfoForContext(): ChatInfoSnapshot { return { provider: "claude", - modelLabel: "Claude Sonnet 4.6", + modelLabel: "Claude Sonnet 5", laneLabel: "Lane one", contextPercent: null, tokenSummary: null, @@ -662,7 +662,7 @@ describe("right pane context defaults", () => { newChatSetup: { laneId: lane.id, laneLabel: lane.name, - rows: [{ kind: "model", label: "Model", value: "Claude Sonnet 4.6", cyclable: true }], + rows: [{ kind: "model", label: "Model", value: "Claude Sonnet 5", cyclable: true }], }, unavailableLaneIds: new Set(), }); diff --git a/apps/ade-cli/src/tuiClient/__tests__/chatInfo.test.ts b/apps/ade-cli/src/tuiClient/__tests__/chatInfo.test.ts index 797df5033..4f6d95266 100644 --- a/apps/ade-cli/src/tuiClient/__tests__/chatInfo.test.ts +++ b/apps/ade-cli/src/tuiClient/__tests__/chatInfo.test.ts @@ -12,7 +12,7 @@ function session(overrides: Partial = {}): AgentChatSes id: "session-1", laneId: "lane-1", provider: "claude", - model: "claude-opus-4-7", + model: "claude-opus-4-8", title: null, createdAt: "2026-05-18T12:00:00.000Z", updatedAt: "2026-05-18T12:00:00.000Z", @@ -91,7 +91,7 @@ describe("deriveChatInfoSnapshot", () => { ], activeSession: null, provider: "claude", - modelLabel: "claude-opus-4-7", + modelLabel: "claude-opus-4-8", laneLabel: null, snapshots: [], tokenStats: null, @@ -110,7 +110,7 @@ describe("deriveChatInfoSnapshot", () => { events: [], activeSession: null, provider: "claude", - modelLabel: "claude-opus-4-7", + modelLabel: "claude-opus-4-8", laneLabel: null, snapshots: [], tokenStats: tokenStats({ @@ -143,7 +143,7 @@ describe("deriveChatInfoSnapshot", () => { events, activeSession: null, provider: "claude", - modelLabel: "claude-opus-4-7", + modelLabel: "claude-opus-4-8", laneLabel: "lane-1", snapshots: [], tokenStats: null, @@ -159,7 +159,7 @@ describe("deriveChatInfoSnapshot", () => { events: [], activeSession: session({ provider: "codex" }), provider: "claude", - modelLabel: "claude-opus-4-7", + modelLabel: "claude-opus-4-8", laneLabel: "fixing-cli-send-error", snapshots: [ { id: "x1", name: "delegated", kind: "subagent", status: "running", summary: "" }, diff --git a/apps/ade-cli/src/tuiClient/components/ModelPicker/modelPickerLayout.test.ts b/apps/ade-cli/src/tuiClient/components/ModelPicker/modelPickerLayout.test.ts index f4752bb4f..d9d77d827 100644 --- a/apps/ade-cli/src/tuiClient/components/ModelPicker/modelPickerLayout.test.ts +++ b/apps/ade-cli/src/tuiClient/components/ModelPicker/modelPickerLayout.test.ts @@ -21,7 +21,7 @@ const modelState: Pick { const models: AgentChatModelInfo[] = [ modelInfo({ id: "anthropic/claude-opus-4-8", displayName: "Claude Opus 4.8 1M" }), - modelInfo({ id: "anthropic/claude-sonnet-4-6", displayName: "Claude Sonnet 4.6" }), + modelInfo({ id: "anthropic/claude-sonnet-5", displayName: "Claude Sonnet 5" }), modelInfo({ id: "openai/gpt-5", displayName: "GPT-5" }), ]; @@ -116,12 +116,12 @@ describe("buildModelPickerLayout", () => { key: "default", label: "Anthropic", models: [{ - id: "anthropic/claude-sonnet-4-6", - runtimeModelId: "claude-sonnet-4-6", + id: "anthropic/claude-sonnet-5", + runtimeModelId: "claude-sonnet-5", provider: "anthropic", providerKey: "anthropic", groupKey: "anthropic", - displayName: "Claude Sonnet 4.6", + displayName: "Claude Sonnet 5", isDefault: true, isAvailable: true, }], @@ -141,7 +141,7 @@ describe("buildModelPickerLayout", () => { focusedIndex: 0, searchMode: false, }); - expect(layout.entries.some((entry) => entry.modelId === "anthropic/claude-sonnet-4-6")).toBe(true); + expect(layout.entries.some((entry) => entry.modelId === "anthropic/claude-sonnet-5")).toBe(true); expect(layout.entries.every((entry) => entry.family === "claude")).toBe(true); }); @@ -316,7 +316,7 @@ describe("modelPickerController", () => { models: [], catalog: null, favorites: ["openai/gpt-5.5"], - recents: ["anthropic/claude-sonnet-4-6"], + recents: ["anthropic/claude-sonnet-5"], modelState, aiStatus: null, })).toMatchObject({ diff --git a/apps/desktop/resources/voice/voice-glossary.json b/apps/desktop/resources/voice/voice-glossary.json index 68ac091f1..5ca494dd4 100644 --- a/apps/desktop/resources/voice/voice-glossary.json +++ b/apps/desktop/resources/voice/voice-glossary.json @@ -13,7 +13,7 @@ "Haiku", "Fable", "Claude Opus 4.8", - "Claude Sonnet 4.6", + "Claude Sonnet 5", "Claude Haiku 4.5", "Claude Fable 5", "GPT-5.5", diff --git a/apps/desktop/src/main/services/ai/aiIntegrationService.test.ts b/apps/desktop/src/main/services/ai/aiIntegrationService.test.ts index 40e62ca56..e9c4484d6 100644 --- a/apps/desktop/src/main/services/ai/aiIntegrationService.test.ts +++ b/apps/desktop/src/main/services/ai/aiIntegrationService.test.ts @@ -298,7 +298,7 @@ describe("aiIntegrationService", () => { taskType: "planning", prompt: "Plan this task", cwd: "/tmp", - model: "anthropic/claude-sonnet-4-6" + model: "anthropic/claude-sonnet-5" }); expect(mockState.runProviderTask).toHaveBeenCalledTimes(1); diff --git a/apps/desktop/src/main/services/ai/aiIntegrationService.ts b/apps/desktop/src/main/services/ai/aiIntegrationService.ts index 2a7ef951e..e099a0054 100644 --- a/apps/desktop/src/main/services/ai/aiIntegrationService.ts +++ b/apps/desktop/src/main/services/ai/aiIntegrationService.ts @@ -201,7 +201,7 @@ const DEFAULT_AI_FEATURE_FLAGS: Record = { initial_context: true, }; -const DEFAULT_CLAUDE_TASK_MODEL_ID = getDefaultModelDescriptor("claude")?.id ?? "anthropic/claude-sonnet-4-6"; +const DEFAULT_CLAUDE_TASK_MODEL_ID = getDefaultModelDescriptor("claude")?.id ?? "anthropic/claude-sonnet-5"; const DEFAULT_CODEX_TASK_MODEL_ID = getDefaultModelDescriptor("codex")?.id ?? "openai/gpt-5.5"; const TASK_DEFAULTS: Record = { @@ -1416,7 +1416,7 @@ export function createAiIntegrationService(args: { if (exact) return exact.id; } - // Resolve from alias (e.g. "sonnet" -> "anthropic/claude-sonnet-4-6") + // Resolve from alias (e.g. "sonnet" -> "anthropic/claude-sonnet-5") if (requestedModelHint) { const resolved = resolveModelAlias(requestedModelHint); if (resolved) return resolved.id; diff --git a/apps/desktop/src/main/services/ai/claudeModelUtils.test.ts b/apps/desktop/src/main/services/ai/claudeModelUtils.test.ts index 6fdd808f8..342d33375 100644 --- a/apps/desktop/src/main/services/ai/claudeModelUtils.test.ts +++ b/apps/desktop/src/main/services/ai/claudeModelUtils.test.ts @@ -8,10 +8,15 @@ describe("resolveClaudeCliModel", () => { expect(resolveClaudeCliModel("claude-opus-4-8[1m]")).toBe("claude-opus-4-8"); }); - it("keeps Opus 4.7 and Opus 4.7 1M aliases on their existing CLI aliases", () => { - expect(resolveClaudeCliModel("claude-opus-4-7")).toBe("opus"); - expect(resolveClaudeCliModel("claude-opus-4-7-1m")).toBe("opus[1m]"); - expect(resolveClaudeCliModel("claude-opus-4-7[1m]")).toBe("opus[1m]"); - expect(resolveClaudeCliModel("opus-11m")).toBe("opus"); + it("maps removed basic Opus and Sonnet aliases to supported Claude CLI model ids", () => { + expect(resolveClaudeCliModel("claude-opus-4-7")).toBe("claude-opus-4-8"); + expect(resolveClaudeCliModel("claude-sonnet-4-6")).toBe("claude-sonnet-5"); + expect(resolveClaudeCliModel("sonnet")).toBe("claude-sonnet-5"); + }); + + it("keeps Opus 4.7 1M aliases on their explicit 1M CLI id", () => { + expect(resolveClaudeCliModel("claude-opus-4-7-1m")).toBe("claude-opus-4-7[1m]"); + expect(resolveClaudeCliModel("claude-opus-4-7[1m]")).toBe("claude-opus-4-7[1m]"); + expect(resolveClaudeCliModel("opus-11m")).toBe("claude-opus-4-8"); }); }); diff --git a/apps/desktop/src/main/services/ai/claudeModelUtils.ts b/apps/desktop/src/main/services/ai/claudeModelUtils.ts index 57613def3..8d171fc6b 100644 --- a/apps/desktop/src/main/services/ai/claudeModelUtils.ts +++ b/apps/desktop/src/main/services/ai/claudeModelUtils.ts @@ -3,10 +3,11 @@ import { getDefaultModelDescriptor, getModelById, resolveModelAlias } from "../. /** * Normalize arbitrary Claude model strings into CLI-safe values accepted by - * Claude Code (`claude-opus-4-8`, `opus`, `opus[1m]`, `sonnet`, `haiku`) where possible. + * Claude Code (`claude-fable-5`, `claude-opus-4-8`, `claude-sonnet-5`, + * `claude-haiku-4-5`, `claude-opus-4-7[1m]`) where possible. */ export function resolveClaudeCliModel(model: string | null | undefined): string { - return resolveClaudeCliModelAlias(model, "sonnet") ?? "sonnet"; + return resolveClaudeCliModelAlias(model, "claude-sonnet-5") ?? "claude-sonnet-5"; } /** diff --git a/apps/desktop/src/main/services/ai/providerTaskRunner.test.ts b/apps/desktop/src/main/services/ai/providerTaskRunner.test.ts index 2b6ec88f5..1668e3143 100644 --- a/apps/desktop/src/main/services/ai/providerTaskRunner.test.ts +++ b/apps/desktop/src/main/services/ai/providerTaskRunner.test.ts @@ -133,7 +133,7 @@ describe("runProviderTask", () => { descriptor: { family: "anthropic", isCliWrapped: true, - providerModelId: "claude-sonnet-4-6", + providerModelId: "claude-sonnet-5", } as any, prompt: "Summarize the worktree state.", feature: "unit-test", diff --git a/apps/desktop/src/main/services/ai/tools/ctoOperatorTools.ts b/apps/desktop/src/main/services/ai/tools/ctoOperatorTools.ts index f0c73c40c..fd6927f1c 100644 --- a/apps/desktop/src/main/services/ai/tools/ctoOperatorTools.ts +++ b/apps/desktop/src/main/services/ai/tools/ctoOperatorTools.ts @@ -334,7 +334,7 @@ export function createCtoOperatorTools(deps: CtoOperatorToolDeps): Record { try { - // Resolve model: supports full IDs (anthropic/claude-sonnet-4-6), short IDs (sonnet), and aliases (opus) + // Resolve model: supports full IDs (anthropic/claude-sonnet-5), short IDs (sonnet), and aliases (opus) const rawModelId = modelId?.trim() || null; const descriptor = rawModelId ? resolveModelDescriptor(rawModelId) : null; const selectedModelId = descriptor?.id ?? rawModelId ?? deps.defaultModelId ?? null; diff --git a/apps/desktop/src/main/services/ai/tools/orchestrationTools.test.ts b/apps/desktop/src/main/services/ai/tools/orchestrationTools.test.ts index 64aad92e4..d2f106fee 100644 --- a/apps/desktop/src/main/services/ai/tools/orchestrationTools.test.ts +++ b/apps/desktop/src/main/services/ai/tools/orchestrationTools.test.ts @@ -352,14 +352,14 @@ describe("spawnAgent tool", () => { expect(createArgs.orchestrationRole).toBe("worker"); expect(createArgs.orchestrationStepId).toBe("T-1"); expect(createArgs.provider).toBe("claude"); - expect(createArgs.model).toBe("claude-sonnet-4-6"); + expect(createArgs.model).toBe("claude-sonnet-5"); expect(createArgs.claudePermissionMode).toBe("bypassPermissions"); const manifest = setup.svc.getManifestForRun(setup.runId)!; expect(manifest.agents.some((a) => a.sessionId === "S-spawned-1")).toBe(true); const spawned = manifest.agents.find((a) => a.sessionId === "S-spawned-1")!; expect(spawned.spawnFingerprint).toMatchObject({ provider: "claude", - modelId: "claude-sonnet-4-6", + modelId: "claude-sonnet-5", routingKey: "fallback", }); expect(setup.chat.sendMessage).toHaveBeenCalledTimes(1); diff --git a/apps/desktop/src/main/services/automations/automationService.ts b/apps/desktop/src/main/services/automations/automationService.ts index 3bfdec839..b7ffdf870 100644 --- a/apps/desktop/src/main/services/automations/automationService.ts +++ b/apps/desktop/src/main/services/automations/automationService.ts @@ -223,7 +223,7 @@ type WatchedFileRoot = { const DEFAULT_AUTOMATION_CHAT_MODEL_ID = getDefaultModelDescriptor("opencode")?.id ?? getDefaultModelDescriptor("claude")?.id - ?? "anthropic/claude-sonnet-4-6"; + ?? "anthropic/claude-sonnet-5"; type AutomationRunRow = { id: string; diff --git a/apps/desktop/src/main/services/chat/agentChatService.test.ts b/apps/desktop/src/main/services/chat/agentChatService.test.ts index d7b8ce3b4..ca61a556b 100644 --- a/apps/desktop/src/main/services/chat/agentChatService.test.ts +++ b/apps/desktop/src/main/services/chat/agentChatService.test.ts @@ -732,8 +732,8 @@ vi.mock("./droidSdkPool", () => ({ const initialSettings = (args.settings ?? {}) as Record; const availableModels = [ { id: "claude-opus-4-6", displayName: "Claude Opus 4.6" }, - { id: "custom:claude-sonnet-4-6-thinking-32000", displayName: "Custom Claude Sonnet 4.6 Thinking" }, - { id: "custom:Claude-Sonnet-4.6-(High)-1", displayName: "Claude Sonnet 4.6 (High)" }, + { id: "custom:claude-sonnet-5-thinking-32000", displayName: "Custom Claude Sonnet 5 Thinking" }, + { id: "custom:Claude-Sonnet-5-(High)-1", displayName: "Claude Sonnet 5 (High)" }, ]; const pooled = { process: { exitCode: null, killed: false }, @@ -1729,7 +1729,7 @@ describe("createAgentChatService", () => { laneId: "lane-1", provider: "opencode", model: "", - modelId: "opencode/anthropic/claude-sonnet-4-6", + modelId: "opencode/anthropic/claude-sonnet-5", }); expect(session).toBeDefined(); @@ -1781,12 +1781,12 @@ describe("createAgentChatService", () => { laneId: "lane-1", provider: "claude", model: undefined, - modelId: "anthropic/claude-sonnet-4-6", + modelId: "anthropic/claude-sonnet-5", } as any); expect(session.provider).toBe("claude"); - expect(session.modelId).toBe("anthropic/claude-sonnet-4-6"); - expect(session.model).toBe("sonnet"); + expect(session.modelId).toBe("anthropic/claude-sonnet-5"); + expect(session.model).toBe("claude-sonnet-5"); }); it("keeps legacy bracketed 1M model aliases on Claude Opus 4.7 1M", async () => { @@ -1802,11 +1802,12 @@ describe("createAgentChatService", () => { }); it.each([ - { reportedModel: "opus", usageModel: "claude-opus-4-8" }, - { reportedModel: "claude-opus-4-7-1m", usageModel: "claude-opus-4-7-1m" }, - ])("preserves Claude Opus 4.8 in done events when the SDK reports $reportedModel", async ({ + { reportedModel: "opus", usageModel: "claude-opus-4-8", expectedModel: "opus" }, + { reportedModel: "claude-opus-4-7-1m", usageModel: "claude-opus-4-7-1m", expectedModel: "claude-opus-4-8" }, + ])("preserves the Claude Opus 4.8 modelId in done events when the SDK reports $reportedModel", async ({ reportedModel, usageModel, + expectedModel, }) => { const events: AgentChatEventEnvelope[] = []; let streamCall = 0; @@ -1878,10 +1879,85 @@ describe("createAgentChatService", () => { const doneEvent = events.filter((event) => event.event.type === "done").at(-1); expect(doneEvent?.event.type).toBe("done"); - expect((doneEvent!.event as any).model).toBe("claude-opus-4-8"); + expect((doneEvent!.event as any).model).toBe(expectedModel); expect((doneEvent!.event as any).modelId).toBe("anthropic/claude-opus-4-8"); }); + it("preserves selected Claude Opus 4.7 1M metadata when the SDK reports bare Opus 4.7", async () => { + const events: AgentChatEventEnvelope[] = []; + let streamCall = 0; + vi.mocked(claudeSdkCreateSessionCompat).mockReturnValue({ + send: vi.fn().mockResolvedValue(undefined), + stream: vi.fn(() => (async function* () { + streamCall += 1; + if (streamCall === 1) { + yield { + type: "system", + subtype: "init", + session_id: "sdk-opus-4-7-1m", + model: "claude-opus-4-7", + slash_commands: [], + }; + yield { + type: "result", + subtype: "success", + is_error: false, + session_id: "sdk-opus-4-7-1m", + usage: { input_tokens: 1, output_tokens: 1 }, + modelUsage: { "claude-opus-4-7": { input_tokens: 1, output_tokens: 1 } }, + }; + return; + } + yield { + type: "system", + subtype: "init", + session_id: "sdk-opus-4-7-1m", + model: "claude-opus-4-7", + slash_commands: [], + }; + yield { + type: "assistant", + message: { + model: "claude-opus-4-7", + content: [{ type: "text", text: "Done" }], + usage: { input_tokens: 1, output_tokens: 1 }, + }, + }; + yield { + type: "result", + subtype: "success", + is_error: false, + session_id: "sdk-opus-4-7-1m", + usage: { input_tokens: 1, output_tokens: 1 }, + modelUsage: { "claude-opus-4-7": { input_tokens: 1, output_tokens: 1 } }, + }; + })()), + close: vi.fn(), + sessionId: "sdk-opus-4-7-1m", + setPermissionMode: vi.fn().mockResolvedValue(undefined), + } as any); + + const { service } = createService({ + onEvent: (event: AgentChatEventEnvelope) => events.push(event), + }); + const session = await service.createSession({ + laneId: "lane-1", + provider: "claude", + model: "claude-opus-4-7[1m]", + modelId: "anthropic/claude-opus-4-7-1m", + }); + + await service.runSessionTurn({ + sessionId: session.id, + text: "Report the selected model.", + }); + + const doneEvent = events.filter((event) => event.event.type === "done").at(-1); + expect(doneEvent?.event.type).toBe("done"); + expect((doneEvent!.event as any).model).toBe("claude-opus-4-7[1m]"); + expect((doneEvent!.event as any).modelId).toBe("anthropic/claude-opus-4-7-1m"); + }); + it("fast-fails a logged-out Claude turn into the inline re-login card", async () => { const events: AgentChatEventEnvelope[] = []; let streamCall = 0; @@ -1956,7 +2032,7 @@ describe("createAgentChatService", () => { laneId: "lane-1", provider: "opencode", model: "", - modelId: "opencode/anthropic/claude-sonnet-4-6", + modelId: "opencode/anthropic/claude-sonnet-5", title: " Pearl UI Audit ", }); @@ -2273,10 +2349,10 @@ describe("createAgentChatService", () => { fastMode: true, }); - // Opus 4.7 also supports fast mode, so the toggle should survive the switch. + // Opus 4.8 also supports fast mode, so the toggle should survive the switch. await service.updateSession({ sessionId: session.id, - modelId: "anthropic/claude-opus-4-7", + modelId: "anthropic/claude-opus-4-8", }); expect((await service.getSessionSummary(session.id))?.fastMode).toBe(true); @@ -2296,10 +2372,10 @@ describe("createAgentChatService", () => { fastMode: true, }); - // Sonnet 4.6 has no "fast" service tier, so the toggle must be dropped. + // Sonnet 5 has no "fast" service tier, so the toggle must be dropped. await service.updateSession({ sessionId: session.id, - modelId: "anthropic/claude-sonnet-4-6", + modelId: "anthropic/claude-sonnet-5", }); expect((await service.getSessionSummary(session.id))?.fastMode).not.toBe(true); @@ -2487,7 +2563,7 @@ describe("createAgentChatService", () => { laneId: "lane-1", provider: "claude", model: "sonnet", - modelId: "anthropic/claude-sonnet-4-6", + modelId: "anthropic/claude-sonnet-5", interactionMode: "orchestrator-lead", orchestrationRunId: created.runId, orchestrationRole: "lead", @@ -2530,7 +2606,7 @@ describe("createAgentChatService", () => { laneId: "lane-1", provider: "claude", model: "sonnet", - modelId: "anthropic/claude-sonnet-4-6", + modelId: "anthropic/claude-sonnet-5", interactionMode: "orchestrator-lead", }); @@ -2572,7 +2648,7 @@ describe("createAgentChatService", () => { laneId: "lane-1", provider: "claude", model: "sonnet", - modelId: "anthropic/claude-sonnet-4-6", + modelId: "anthropic/claude-sonnet-5", orchestrationRole: "lead", }); @@ -2839,7 +2915,7 @@ describe("createAgentChatService", () => { laneId: "lane-1", provider: "opencode", model: "", - modelId: "opencode/anthropic/claude-sonnet-4-6", + modelId: "opencode/anthropic/claude-sonnet-5", }); expect(session.sessionProfile).toBe("workflow"); @@ -2851,7 +2927,7 @@ describe("createAgentChatService", () => { laneId: "lane-1", provider: "opencode", model: "", - modelId: "opencode/anthropic/claude-sonnet-4-6", + modelId: "opencode/anthropic/claude-sonnet-5", sessionProfile: "light", }); @@ -2864,7 +2940,7 @@ describe("createAgentChatService", () => { laneId: "lane-1", provider: "opencode", model: "", - modelId: "opencode/anthropic/claude-sonnet-4-6", + modelId: "opencode/anthropic/claude-sonnet-5", reasoningEffort: " HIGH ", }); @@ -2877,7 +2953,7 @@ describe("createAgentChatService", () => { laneId: "lane-1", provider: "opencode", model: "", - modelId: "opencode/anthropic/claude-sonnet-4-6", + modelId: "opencode/anthropic/claude-sonnet-5", sessionProfile: "light", }); @@ -2890,7 +2966,7 @@ describe("createAgentChatService", () => { laneId: "lane-1", provider: "opencode", model: "", - modelId: "opencode/anthropic/claude-sonnet-4-6", + modelId: "opencode/anthropic/claude-sonnet-5", surface: "automation", }); @@ -2914,7 +2990,7 @@ describe("createAgentChatService", () => { laneId: "lane-1", provider: "opencode", model: "", - modelId: "opencode/anthropic/claude-sonnet-4-6", + modelId: "opencode/anthropic/claude-sonnet-5", identityKey: "cto", }); @@ -2927,7 +3003,7 @@ describe("createAgentChatService", () => { laneId: "lane-1", provider: "opencode", model: "", - modelId: "opencode/anthropic/claude-sonnet-4-6", + modelId: "opencode/anthropic/claude-sonnet-5", }); const chatSessionsDir = path.join(tmpRoot, ".ade", "cache", "chat-sessions"); @@ -2945,7 +3021,7 @@ describe("createAgentChatService", () => { laneId: "lane-1", provider: "opencode", model: "", - modelId: "opencode/anthropic/claude-sonnet-4-6", + modelId: "opencode/anthropic/claude-sonnet-5", sessionProfile: "light", }); @@ -2972,7 +3048,7 @@ describe("createAgentChatService", () => { laneId: "lane-1", provider: "opencode", model: "", - modelId: "opencode/anthropic/claude-sonnet-4-6", + modelId: "opencode/anthropic/claude-sonnet-5", })).rejects.toThrow(/worktree is unavailable/i); }); }); @@ -3375,7 +3451,7 @@ describe("createAgentChatService", () => { laneId: "lane-1", provider: "claude", model: "sonnet", - modelId: "anthropic/claude-sonnet-4-6", + modelId: "anthropic/claude-sonnet-5", interactionMode: "default", claudePermissionMode: "default", permissionMode: "default", @@ -3383,7 +3459,7 @@ describe("createAgentChatService", () => { const result = await service.handoffSession({ sourceSessionId: source.id, - targetModelId: "anthropic/claude-sonnet-4-6", + targetModelId: "anthropic/claude-sonnet-5", claudePermissionMode: "plan", permissionMode: "plan", }); @@ -3431,7 +3507,7 @@ describe("createAgentChatService", () => { laneId: "lane-1", provider: "claude", model: "sonnet", - modelId: "anthropic/claude-sonnet-4-6", + modelId: "anthropic/claude-sonnet-5", interactionMode: "default", claudePermissionMode: "default", permissionMode: "default", @@ -3444,7 +3520,7 @@ describe("createAgentChatService", () => { const result = await service.handoffSession({ sourceSessionId: source.id, - targetModelId: "anthropic/claude-sonnet-4-6", + targetModelId: "anthropic/claude-sonnet-5", mode: "fork", claudePermissionMode: "plan", permissionMode: "plan", @@ -3765,8 +3841,8 @@ describe("createAgentChatService", () => { const session = await service.createSession({ laneId: "lane-1", provider: "droid", - model: "custom:claude-sonnet-4-6-thinking-32000", - modelId: "droid/custom:claude-sonnet-4-6-thinking-32000", + model: "custom:claude-sonnet-5-thinking-32000", + modelId: "droid/custom:claude-sonnet-5-thinking-32000", interactionMode: "orchestrator-lead", orchestrationRunId: created.runId, orchestrationRole: "lead", @@ -3936,7 +4012,7 @@ describe("createAgentChatService", () => { laneId: "lane-1", provider: "opencode", model: "", - modelId: "opencode/anthropic/claude-sonnet-4-6", + modelId: "opencode/anthropic/claude-sonnet-5", }); const sessions = await service.listSessions(); @@ -3979,7 +4055,7 @@ describe("createAgentChatService", () => { laneId: "lane-1", provider: "opencode", model: "", - modelId: "opencode/anthropic/claude-sonnet-4-6", + modelId: "opencode/anthropic/claude-sonnet-5", identityKey: "cto", }); @@ -3997,7 +4073,7 @@ describe("createAgentChatService", () => { laneId: "lane-1", provider: "opencode", model: "", - modelId: "opencode/anthropic/claude-sonnet-4-6", + modelId: "opencode/anthropic/claude-sonnet-5", surface: "automation", }); @@ -4015,7 +4091,7 @@ describe("createAgentChatService", () => { laneId: "lane-1", provider: "opencode", model: "", - modelId: "opencode/anthropic/claude-sonnet-4-6", + modelId: "opencode/anthropic/claude-sonnet-5", }); await service.archiveSession({ sessionId: session.id }); @@ -4114,7 +4190,7 @@ describe("createAgentChatService", () => { laneId: "lane-2", provider: "opencode", model: "", - modelId: "opencode/anthropic/claude-sonnet-4-6", + modelId: "opencode/anthropic/claude-sonnet-5", identityKey: "cto", }); @@ -5016,7 +5092,7 @@ describe("createAgentChatService", () => { subtype: "model_refusal_fallback", session_id: "sdk-session-events", original_model: "claude-opus-4-8", - fallback_model: "claude-sonnet-4-6", + fallback_model: "claude-sonnet-5", api_refusal_category: "cyber", api_refusal_explanation: "The original model refused.", content: "Retrying on fallback model.", @@ -5117,7 +5193,7 @@ describe("createAgentChatService", () => { )).toBe(true); expect(notices.some((event) => event.status === "model_refusal_fallback" - && event.message === "Claude retried with claude-sonnet-4-6 after claude-opus-4-8 refused the request.", + && event.message === "Claude retried with claude-sonnet-5 after claude-opus-4-8 refused the request.", )).toBe(true); const refusalFallbackNotice = notices.find((event) => event.status === "model_refusal_fallback"); expect(refusalFallbackNotice?.detail).toContain("retracted 2 SDK messages: refused-message-1, refused-tool-result-1"); @@ -5945,7 +6021,7 @@ describe("createAgentChatService", () => { laneId: "lane-1", provider: "opencode", model: "", - modelId: "opencode/anthropic/claude-sonnet-4-6", + modelId: "opencode/anthropic/claude-sonnet-5", }); const summary = await service.getSessionSummary(created.id); @@ -5979,7 +6055,7 @@ describe("createAgentChatService", () => { laneId: "lane-1", provider: "opencode", model: "", - modelId: "opencode/anthropic/claude-sonnet-4-6", + modelId: "opencode/anthropic/claude-sonnet-5", }); const caps = service.getSessionCapabilities({ sessionId: session.id }); @@ -6036,7 +6112,7 @@ describe("createAgentChatService", () => { laneId: "lane-1", provider: "opencode", model: "", - modelId: "opencode/anthropic/claude-sonnet-4-6", + modelId: "opencode/anthropic/claude-sonnet-5", }); const subagents = service.listSubagents({ sessionId: session.id }); @@ -6752,7 +6828,7 @@ describe("createAgentChatService", () => { laneId: "lane-1", provider: "opencode", model: "", - modelId: "opencode/anthropic/claude-sonnet-4-6", + modelId: "opencode/anthropic/claude-sonnet-5", }); const commands = service.getSlashCommands({ sessionId: session.id }); @@ -6870,8 +6946,8 @@ describe("createAgentChatService", () => { const session = await service.createSession({ laneId: "lane-1", provider: "droid", - model: "custom:claude-sonnet-4-6-thinking-32000", - modelId: "droid/custom:claude-sonnet-4-6-thinking-32000", + model: "custom:claude-sonnet-5-thinking-32000", + modelId: "droid/custom:claude-sonnet-5-thinking-32000", }); const commands = service.getSlashCommands({ sessionId: session.id }); @@ -7010,7 +7086,7 @@ describe("createAgentChatService", () => { laneId: "lane-1", provider: "opencode", model: "", - modelId: "opencode/anthropic/claude-sonnet-4-6", + modelId: "opencode/anthropic/claude-sonnet-5", }); const commands = service.getSlashCommands({ sessionId: session.id }); @@ -7287,8 +7363,8 @@ describe("createAgentChatService", () => { const session = await service.createSession({ laneId: "lane-1", provider: "claude", - model: "claude-sonnet-4-6", - modelId: "anthropic/claude-sonnet-4-6", + model: "claude-sonnet-5", + modelId: "anthropic/claude-sonnet-5", }); await service.sendMessage({ @@ -7322,8 +7398,8 @@ describe("createAgentChatService", () => { const session = await service.createSession({ laneId: "lane-1", provider: "claude", - model: "claude-sonnet-4-6", - modelId: "anthropic/claude-sonnet-4-6", + model: "claude-sonnet-5", + modelId: "anthropic/claude-sonnet-5", }); await expect(service.sendMessage({ @@ -7382,8 +7458,8 @@ describe("createAgentChatService", () => { const session = await service.createSession({ laneId: "lane-1", provider: "claude", - model: "claude-sonnet-4-6", - modelId: "anthropic/claude-sonnet-4-6", + model: "claude-sonnet-5", + modelId: "anthropic/claude-sonnet-5", }); await service.sendMessage({ @@ -7563,7 +7639,7 @@ describe("createAgentChatService", () => { laneId: "lane-1", provider: "opencode", model: "", - modelId: "opencode/anthropic/claude-sonnet-4-6", + modelId: "opencode/anthropic/claude-sonnet-5", }); await service.sendMessage({ sessionId: session.id, text: "Use runtime title." }, { awaitDispatch: true }); @@ -7580,8 +7656,8 @@ describe("createAgentChatService", () => { const session = await service.createSession({ laneId: "lane-1", provider: "droid", - model: "custom:claude-sonnet-4-6-thinking-32000", - modelId: "droid/custom:claude-sonnet-4-6-thinking-32000", + model: "custom:claude-sonnet-5-thinking-32000", + modelId: "droid/custom:claude-sonnet-5-thinking-32000", }); await service.sendMessage({ sessionId: session.id, text: "Use SDK title." }, { awaitDispatch: true }); @@ -7611,7 +7687,7 @@ describe("createAgentChatService", () => { laneId: "lane-1", provider: "opencode", model: "", - modelId: "opencode/anthropic/claude-sonnet-4-6", + modelId: "opencode/anthropic/claude-sonnet-5", }); events.length = 0; @@ -7637,7 +7713,7 @@ describe("createAgentChatService", () => { laneId: "lane-1", provider: "opencode", model: "", - modelId: "opencode/anthropic/claude-sonnet-4-6", + modelId: "opencode/anthropic/claude-sonnet-5", }); events.length = 0; @@ -7662,7 +7738,7 @@ describe("createAgentChatService", () => { laneId: "lane-1", provider: "opencode", model: "", - modelId: "opencode/anthropic/claude-sonnet-4-6", + modelId: "opencode/anthropic/claude-sonnet-5", }); const updated = await service.updateSession({ @@ -7682,7 +7758,7 @@ describe("createAgentChatService", () => { laneId: "lane-1", provider: "opencode", model: "", - modelId: "opencode/anthropic/claude-sonnet-4-6", + modelId: "opencode/anthropic/claude-sonnet-5", }); await service.updateSession({ @@ -7701,7 +7777,7 @@ describe("createAgentChatService", () => { laneId: "lane-1", provider: "opencode", model: "", - modelId: "opencode/anthropic/claude-sonnet-4-6", + modelId: "opencode/anthropic/claude-sonnet-5", }); const updated = await service.updateSession({ @@ -7718,7 +7794,7 @@ describe("createAgentChatService", () => { laneId: "lane-1", provider: "opencode", model: "", - modelId: "opencode/anthropic/claude-sonnet-4-6", + modelId: "opencode/anthropic/claude-sonnet-5", }); const updated = await service.updateSession({ @@ -7735,7 +7811,7 @@ describe("createAgentChatService", () => { laneId: "lane-1", provider: "opencode", model: "", - modelId: "opencode/anthropic/claude-sonnet-4-6", + modelId: "opencode/anthropic/claude-sonnet-5", }); await expect( @@ -7752,7 +7828,7 @@ describe("createAgentChatService", () => { laneId: "lane-1", provider: "opencode", model: "", - modelId: "opencode/anthropic/claude-sonnet-4-6", + modelId: "opencode/anthropic/claude-sonnet-5", }); await expect( @@ -7769,7 +7845,7 @@ describe("createAgentChatService", () => { laneId: "lane-1", provider: "opencode", model: "", - modelId: "opencode/anthropic/claude-sonnet-4-6", + modelId: "opencode/anthropic/claude-sonnet-5", }); const updated = await service.updateSession({ @@ -8011,7 +8087,7 @@ describe("createAgentChatService", () => { laneId: "lane-1", provider: "opencode", model: "", - modelId: "opencode/anthropic/claude-sonnet-4-6", + modelId: "opencode/anthropic/claude-sonnet-5", }); try { @@ -8205,8 +8281,8 @@ describe("createAgentChatService", () => { const session = await service.createSession({ laneId: "lane-1", provider: "droid", - model: "custom:claude-sonnet-4-6-thinking-32000", - modelId: "droid/custom:claude-sonnet-4-6-thinking-32000", + model: "custom:claude-sonnet-5-thinking-32000", + modelId: "droid/custom:claude-sonnet-5-thinking-32000", }); try { @@ -8318,7 +8394,7 @@ describe("createAgentChatService", () => { laneId: "lane-1", provider: "opencode", model: "", - modelId: "opencode/anthropic/claude-sonnet-4-6", + modelId: "opencode/anthropic/claude-sonnet-5", }); await service.runSessionTurn({ @@ -8342,7 +8418,7 @@ describe("createAgentChatService", () => { laneId: "lane-1", provider: "opencode", model: "", - modelId: "opencode/anthropic/claude-sonnet-4-6", + modelId: "opencode/anthropic/claude-sonnet-5", }); await service.dispose({ sessionId: session.id }); @@ -8358,7 +8434,7 @@ describe("createAgentChatService", () => { laneId: "lane-1", provider: "opencode", model: "", - modelId: "opencode/anthropic/claude-sonnet-4-6", + modelId: "opencode/anthropic/claude-sonnet-5", }); expect(service.getSlashCommands({ sessionId: session.id })).toEqual( @@ -8421,7 +8497,7 @@ describe("createAgentChatService", () => { laneId: "lane-1", provider: "opencode", model: "", - modelId: "opencode/anthropic/claude-sonnet-4-6", + modelId: "opencode/anthropic/claude-sonnet-5", }); await service.runSessionTurn({ @@ -8461,14 +8537,14 @@ describe("createAgentChatService", () => { laneId: "lane-1", provider: "opencode", model: "", - modelId: "opencode/anthropic/claude-sonnet-4-6", + modelId: "opencode/anthropic/claude-sonnet-5", }); mockState.uuidCounter = 10; // avoid collision await service.createSession({ laneId: "lane-1", provider: "opencode", model: "", - modelId: "opencode/anthropic/claude-sonnet-4-6", + modelId: "opencode/anthropic/claude-sonnet-5", }); // Should not throw @@ -8508,7 +8584,7 @@ describe("createAgentChatService", () => { laneId: "lane-1", provider: "opencode", model: "", - modelId: "opencode/anthropic/claude-sonnet-4-6", + modelId: "opencode/anthropic/claude-sonnet-5", }); const turn = service.runSessionTurn({ @@ -8585,7 +8661,7 @@ describe("createAgentChatService", () => { laneId: "lane-1", provider: "opencode", model: "", - modelId: "opencode/anthropic/claude-sonnet-4-6", + modelId: "opencode/anthropic/claude-sonnet-5", }); const metadataPath = path.join(tmpRoot, ".ade", "cache", "chat-sessions", `${session.id}.json`); @@ -8615,7 +8691,7 @@ describe("createAgentChatService", () => { laneId: "lane-1", provider: "opencode", model: "", - modelId: "opencode/anthropic/claude-sonnet-4-6", + modelId: "opencode/anthropic/claude-sonnet-5", }); await service.deleteSession({ sessionId: session.id }); @@ -8674,7 +8750,7 @@ describe("createAgentChatService", () => { laneId: "lane-1", provider: "opencode", model: "", - modelId: "opencode/anthropic/claude-sonnet-4-6", + modelId: "opencode/anthropic/claude-sonnet-5", }); const mainTranscriptPath = sessionService.get(session.id)?.transcriptPath ?? ""; @@ -8736,7 +8812,7 @@ describe("createAgentChatService", () => { laneId: "lane-1", provider: "opencode", model: "", - modelId: "opencode/anthropic/claude-sonnet-4-6", + modelId: "opencode/anthropic/claude-sonnet-5", }); mockState.uuidCounter = 100; @@ -8744,7 +8820,7 @@ describe("createAgentChatService", () => { laneId: "lane-1", provider: "opencode", model: "", - modelId: "opencode/anthropic/claude-sonnet-4-6", + modelId: "opencode/anthropic/claude-sonnet-5", }); expect(s1.id).not.toBe(s2.id); @@ -11640,7 +11716,7 @@ describe("createAgentChatService", () => { laneId: "lane-1", provider: "claude", model: "sonnet", - modelId: "anthropic/claude-sonnet-4-6", + modelId: "anthropic/claude-sonnet-5", permissionMode: "edit", claudePermissionMode: "acceptEdits", }); @@ -12140,7 +12216,7 @@ describe("createAgentChatService", () => { const { service } = createService(); // Should not throw await expect( - service.warmupModel({ sessionId: "no-such-session", modelId: "opencode/anthropic/claude-sonnet-4-6" }), + service.warmupModel({ sessionId: "no-such-session", modelId: "opencode/anthropic/claude-sonnet-5" }), ).resolves.toBeUndefined(); }); @@ -12150,12 +12226,12 @@ describe("createAgentChatService", () => { laneId: "lane-1", provider: "opencode", model: "", - modelId: "opencode/anthropic/claude-sonnet-4-6", + modelId: "opencode/anthropic/claude-sonnet-5", }); // A non-anthropic-cli model should be a no-op await expect( - service.warmupModel({ sessionId: session.id, modelId: "opencode/anthropic/claude-sonnet-4-6" }), + service.warmupModel({ sessionId: session.id, modelId: "opencode/anthropic/claude-sonnet-5" }), ).resolves.toBeUndefined(); }); @@ -12165,16 +12241,16 @@ describe("createAgentChatService", () => { laneId: "lane-1", provider: "opencode", model: "", - modelId: "opencode/anthropic/claude-sonnet-4-6", + modelId: "opencode/anthropic/claude-sonnet-5", }); await expect( - service.warmupModel({ sessionId: session.id, modelId: "anthropic/claude-sonnet-4-6" }), + service.warmupModel({ sessionId: session.id, modelId: "anthropic/claude-sonnet-5" }), ).resolves.toBeUndefined(); const summary = await service.getSessionSummary(session.id); expect(summary?.provider).toBe("opencode"); - expect(summary?.modelId).toBe("opencode/anthropic/claude-sonnet-4-6"); + expect(summary?.modelId).toBe("opencode/anthropic/claude-sonnet-5"); }); }); @@ -12313,7 +12389,7 @@ describe("createAgentChatService", () => { laneId: "lane-1", provider: "opencode", model: "", - modelId: "opencode/anthropic/claude-sonnet-4-6", + modelId: "opencode/anthropic/claude-sonnet-5", }); const transcript = await service.getChatTranscript({ sessionId: session.id }); @@ -13363,7 +13439,7 @@ describe("createAgentChatService", () => { laneId: "lane-1", provider: "opencode", model: "", - modelId: "opencode/anthropic/claude-sonnet-4-6", + modelId: "opencode/anthropic/claude-sonnet-5", surface: "automation", automationId: "auto-1", automationRunId: "run-1", @@ -13392,7 +13468,7 @@ describe("createAgentChatService", () => { laneId: "lane-1", provider: "opencode", model: "", - modelId: "opencode/anthropic/claude-sonnet-4-6", + modelId: "opencode/anthropic/claude-sonnet-5", capabilityMode: "cto", } as any); @@ -13406,7 +13482,7 @@ describe("createAgentChatService", () => { laneId: "lane-1", provider: "opencode", model: "", - modelId: "opencode/anthropic/claude-sonnet-4-6", + modelId: "opencode/anthropic/claude-sonnet-5", }); // executionMode defaults to null or undefined for new sessions @@ -13512,7 +13588,7 @@ describe("createAgentChatService", () => { laneId: "lane-1", provider: "opencode", model: "", - modelId: "opencode/anthropic/claude-sonnet-4-6", + modelId: "opencode/anthropic/claude-sonnet-5", }); expect(session.status).toBe("idle"); @@ -13524,7 +13600,7 @@ describe("createAgentChatService", () => { laneId: "lane-1", provider: "opencode", model: "", - modelId: "opencode/anthropic/claude-sonnet-4-6", + modelId: "opencode/anthropic/claude-sonnet-5", }); expect(session.completion).toBeNull(); @@ -13542,7 +13618,7 @@ describe("createAgentChatService", () => { laneId: "lane-1", provider: "opencode", model: "", - modelId: "opencode/anthropic/claude-sonnet-4-6", + modelId: "opencode/anthropic/claude-sonnet-5", }); expect(session.interactionMode == null).toBe(true); @@ -16443,7 +16519,7 @@ describe("createAgentChatService", () => { const updated = await service.updateSession({ sessionId: session.id, - modelId: "anthropic/claude-sonnet-4-6", + modelId: "anthropic/claude-sonnet-5", }); expect(updated.provider).toBe("claude"); @@ -17747,7 +17823,7 @@ describe("createAgentChatService", () => { laneId: "lane-1", provider: "opencode", model: "", - modelId: "opencode/anthropic/claude-sonnet-4-6", + modelId: "opencode/anthropic/claude-sonnet-5", }); await service.dispose({ sessionId: session.id }); @@ -19906,8 +19982,8 @@ describe("createAgentChatService", () => { const session = await service.createSession({ laneId: "lane-1", provider: "claude", - model: "claude-sonnet-4-6", - modelId: "anthropic/claude-sonnet-4-6", + model: "claude-sonnet-5", + modelId: "anthropic/claude-sonnet-5", }); const sendPromise = service.sendMessage({ @@ -20006,8 +20082,8 @@ describe("createAgentChatService", () => { const session = await service.createSession({ laneId: "lane-1", provider: "claude", - model: "claude-sonnet-4-6", - modelId: "anthropic/claude-sonnet-4-6", + model: "claude-sonnet-5", + modelId: "anthropic/claude-sonnet-5", }); await service.runSessionTurn({ @@ -20096,8 +20172,8 @@ describe("createAgentChatService", () => { const session = await service.createSession({ laneId: "lane-1", provider: "claude", - model: "claude-sonnet-4-6", - modelId: "anthropic/claude-sonnet-4-6", + model: "claude-sonnet-5", + modelId: "anthropic/claude-sonnet-5", }); await service.runSessionTurn({ @@ -20165,8 +20241,8 @@ describe("createAgentChatService", () => { const session = await service.createSession({ laneId: "lane-1", provider: "claude", - model: "claude-sonnet-4-6", - modelId: "anthropic/claude-sonnet-4-6", + model: "claude-sonnet-5", + modelId: "anthropic/claude-sonnet-5", }); await service.runSessionTurn({ @@ -20249,8 +20325,8 @@ describe("createAgentChatService", () => { const session = await service.createSession({ laneId: "lane-1", provider: "claude", - model: "claude-sonnet-4-6", - modelId: "anthropic/claude-sonnet-4-6", + model: "claude-sonnet-5", + modelId: "anthropic/claude-sonnet-5", }); await service.runSessionTurn({ @@ -20335,8 +20411,8 @@ describe("createAgentChatService", () => { const session = await service.createSession({ laneId: "lane-1", provider: "claude", - model: "claude-sonnet-4-6", - modelId: "anthropic/claude-sonnet-4-6", + model: "claude-sonnet-5", + modelId: "anthropic/claude-sonnet-5", }); await service.runSessionTurn({ @@ -20412,8 +20488,8 @@ describe("createAgentChatService", () => { const session = await service.createSession({ laneId: "lane-1", provider: "claude", - model: "claude-sonnet-4-6", - modelId: "anthropic/claude-sonnet-4-6", + model: "claude-sonnet-5", + modelId: "anthropic/claude-sonnet-5", }); await service.runSessionTurn({ @@ -20491,8 +20567,8 @@ describe("createAgentChatService", () => { const session = await service.createSession({ laneId: "lane-1", provider: "claude", - model: "claude-sonnet-4-6", - modelId: "anthropic/claude-sonnet-4-6", + model: "claude-sonnet-5", + modelId: "anthropic/claude-sonnet-5", }); await service.runSessionTurn({ @@ -20617,8 +20693,8 @@ describe("createAgentChatService", () => { const session = await service.createSession({ laneId: "lane-1", provider: "claude", - model: "claude-sonnet-4-6", - modelId: "anthropic/claude-sonnet-4-6", + model: "claude-sonnet-5", + modelId: "anthropic/claude-sonnet-5", }); sessionId = session.id; @@ -20728,8 +20804,8 @@ describe("createAgentChatService", () => { const session = await service.createSession({ laneId: "lane-1", provider: "claude", - model: "claude-sonnet-4-6", - modelId: "anthropic/claude-sonnet-4-6", + model: "claude-sonnet-5", + modelId: "anthropic/claude-sonnet-5", permissionMode: "plan", }); @@ -21671,8 +21747,8 @@ describe("createAgentChatService", () => { const session = await service.createSession({ laneId: "lane-1", provider: "droid", - model: "custom:claude-sonnet-4-6-thinking-32000", - modelId: "droid/custom:claude-sonnet-4-6-thinking-32000", + model: "custom:claude-sonnet-5-thinking-32000", + modelId: "droid/custom:claude-sonnet-5-thinking-32000", }); await service.sendMessage({ @@ -21689,14 +21765,14 @@ describe("createAgentChatService", () => { const updated = await service.getSessionSummary(session.id); expect(mockState.droidAcquireCalls[0]?.settings).toMatchObject({ - modelId: "custom:claude-sonnet-4-6-thinking-32000", + modelId: "custom:claude-sonnet-5-thinking-32000", }); expect(mockState.droidSettingsUpdates.at(-1)).toMatchObject({ - modelId: "custom:claude-sonnet-4-6-thinking-32000", + modelId: "custom:claude-sonnet-5-thinking-32000", interactionMode: "auto", }); expect(mockState.droidPromptCalls[0]?.settings).toMatchObject({ - modelId: "custom:claude-sonnet-4-6-thinking-32000", + modelId: "custom:claude-sonnet-5-thinking-32000", }); expect(vi.mocked(buildCodingAgentSystemPrompt)).toHaveBeenCalledWith(expect.objectContaining({ runtime: "droid-sdk", @@ -21708,10 +21784,10 @@ describe("createAgentChatService", () => { expect(settingsOrder).toBeDefined(); expect(firstPromptOrder).toBeDefined(); expect(settingsOrder).toBeLessThan(firstPromptOrder!); - expect(updated?.model).toBe("custom:claude-sonnet-4-6-thinking-32000"); - expect(updated?.modelId).toBe("droid/custom:claude-sonnet-4-6-thinking-32000"); - expect(doneEvent.event.model).toBe("custom:claude-sonnet-4-6-thinking-32000"); - expect(doneEvent.event.modelId).toBe("droid/custom:claude-sonnet-4-6-thinking-32000"); + expect(updated?.model).toBe("custom:claude-sonnet-5-thinking-32000"); + expect(updated?.modelId).toBe("droid/custom:claude-sonnet-5-thinking-32000"); + expect(doneEvent.event.model).toBe("custom:claude-sonnet-5-thinking-32000"); + expect(doneEvent.event.modelId).toBe("droid/custom:claude-sonnet-5-thinking-32000"); }); it("uses Droid spec mode for ADE plan mode", async () => { @@ -21723,8 +21799,8 @@ describe("createAgentChatService", () => { const session = await service.createSession({ laneId: "lane-1", provider: "droid", - model: "custom:claude-sonnet-4-6-thinking-32000", - modelId: "droid/custom:claude-sonnet-4-6-thinking-32000", + model: "custom:claude-sonnet-5-thinking-32000", + modelId: "droid/custom:claude-sonnet-5-thinking-32000", interactionMode: "plan", }); @@ -21739,14 +21815,14 @@ describe("createAgentChatService", () => { ); expect(mockState.droidAcquireCalls[0]?.settings).toMatchObject({ - modelId: "custom:claude-sonnet-4-6-thinking-32000", + modelId: "custom:claude-sonnet-5-thinking-32000", interactionMode: "spec", - specModeModelId: "custom:claude-sonnet-4-6-thinking-32000", + specModeModelId: "custom:claude-sonnet-5-thinking-32000", }); expect(mockState.droidSettingsUpdates.at(-1)).toMatchObject({ - modelId: "custom:claude-sonnet-4-6-thinking-32000", + modelId: "custom:claude-sonnet-5-thinking-32000", interactionMode: "spec", - specModeModelId: "custom:claude-sonnet-4-6-thinking-32000", + specModeModelId: "custom:claude-sonnet-5-thinking-32000", }); }); @@ -21756,8 +21832,8 @@ describe("createAgentChatService", () => { const session = await service.createSession({ laneId: "lane-1", provider: "droid", - model: "custom:claude-sonnet-4-6-thinking-32000", - modelId: "droid/custom:claude-sonnet-4-6-thinking-32000", + model: "custom:claude-sonnet-5-thinking-32000", + modelId: "droid/custom:claude-sonnet-5-thinking-32000", }); const persisted = readPersistedChatState(session.id); @@ -21768,7 +21844,7 @@ describe("createAgentChatService", () => { await service.warmupModel({ sessionId: session.id, - modelId: "droid/custom:claude-sonnet-4-6-thinking-32000", + modelId: "droid/custom:claude-sonnet-5-thinking-32000", }); const updated = await service.getSessionSummary(session.id); @@ -21778,11 +21854,11 @@ describe("createAgentChatService", () => { workspacePath: fs.realpathSync(tmpRoot), }); expect(mockState.droidSettingsUpdates.at(-1)).toMatchObject({ - modelId: "custom:claude-sonnet-4-6-thinking-32000", + modelId: "custom:claude-sonnet-5-thinking-32000", }); expect(mockState.droidNewSessionCalls).toHaveLength(0); - expect(updated?.model).toBe("custom:claude-sonnet-4-6-thinking-32000"); - expect(updated?.modelId).toBe("droid/custom:claude-sonnet-4-6-thinking-32000"); + expect(updated?.model).toBe("custom:claude-sonnet-5-thinking-32000"); + expect(updated?.modelId).toBe("droid/custom:claude-sonnet-5-thinking-32000"); }); it("surfaces structured Droid SDK failures without collapsing them to [object Object]", async () => { @@ -21800,8 +21876,8 @@ describe("createAgentChatService", () => { const session = await service.createSession({ laneId: "lane-1", provider: "droid", - model: "custom:claude-sonnet-4-6-thinking-32000", - modelId: "droid/custom:claude-sonnet-4-6-thinking-32000", + model: "custom:claude-sonnet-5-thinking-32000", + modelId: "droid/custom:claude-sonnet-5-thinking-32000", }); await service.sendMessage({ diff --git a/apps/desktop/src/main/services/chat/agentChatService.ts b/apps/desktop/src/main/services/chat/agentChatService.ts index f269df1e2..146979828 100644 --- a/apps/desktop/src/main/services/chat/agentChatService.ts +++ b/apps/desktop/src/main/services/chat/agentChatService.ts @@ -1898,7 +1898,7 @@ const DEFAULT_CURSOR_DESCRIPTOR = getDefaultModelDescriptor("cursor"); const DEFAULT_DROID_DESCRIPTOR = getDefaultModelDescriptor("droid"); const DEFAULT_CODEX_MODEL = DEFAULT_CODEX_DESCRIPTOR?.providerModelId ?? "gpt-5.5"; const DEFAULT_CLAUDE_MODEL = DEFAULT_CLAUDE_DESCRIPTOR?.providerModelId ?? DEFAULT_CLAUDE_DESCRIPTOR?.shortId ?? "sonnet"; -const DEFAULT_OPENCODE_MODEL_ID = DEFAULT_OPENCODE_DESCRIPTOR?.id ?? "anthropic/claude-sonnet-4-6"; +const DEFAULT_OPENCODE_MODEL_ID = DEFAULT_OPENCODE_DESCRIPTOR?.id ?? "anthropic/claude-sonnet-5"; const DEFAULT_CURSOR_MODEL = DEFAULT_CURSOR_DESCRIPTOR?.providerModelId ?? "auto"; const DEFAULT_DROID_MODEL = DEFAULT_DROID_DESCRIPTOR?.providerModelId ?? "claude-sonnet-4-5-20250929"; const DEFAULT_REASONING_EFFORT = "medium"; @@ -3309,6 +3309,15 @@ function resolveClaudeCliModelIdFromRuntimeValue(model: string): string | undefi })?.id; } +function isBareClaudeOpus47RuntimeValue(model: string): boolean { + const normalized = model.trim().toLowerCase() + .replace(/^anthropic\//, "") + .replace(/-api$/, ""); + return normalized === "claude-opus-4-7" + || normalized === "opus-4-7" + || normalized === "opus-4.7"; +} + function resolveModelIdFromStoredValue( model: string, providerHint?: AgentChatProvider, @@ -3388,17 +3397,17 @@ function resolveClaudeTurnModelPayload( || selectedDescriptor?.shortId === "opus-1m" || selectedDescriptor?.providerModelId.toLowerCase().includes("[1m]"); const selectedIsOpus48 = selectedDescriptor?.id === "anthropic/claude-opus-4-8"; - const shouldPreserveSelectedModel = (reportedModelId: string | undefined): boolean => { + const shouldPreserveSelectedModel = (reportedModelId: string | undefined, reportedModel?: string): boolean => { if (!reportedModelId || reportedModelId === session.modelId) return false; const reportedDescriptor = getModelById(reportedModelId) ?? resolveModelAlias(reportedModelId); if (selectedIsOpus48) { return ( - reportedDescriptor?.id === "anthropic/claude-opus-4-7" - || reportedDescriptor?.id === "anthropic/claude-opus-4-7-1m" + reportedDescriptor?.id === "anthropic/claude-opus-4-7-1m" ); } if (!selectedIsOpusOneMillion) return false; - return reportedDescriptor?.id === "anthropic/claude-opus-4-7"; + if (reportedModel && isBareClaudeOpus47RuntimeValue(reportedModel)) return true; + return reportedDescriptor?.id === "anthropic/claude-opus-4-7-1m"; }; for (const candidate of candidates) { @@ -3409,14 +3418,19 @@ function resolveClaudeTurnModelPayload( resolveClaudeCliModelIdFromRuntimeValue(normalized) ?? resolveClaudeCliModelIdFromRuntimeValue(normalizedCliModel); if (resolvedCliModelId) { - if (shouldPreserveSelectedModel(resolvedCliModelId)) return sessionPayload; - return { model: normalized, modelId: resolvedCliModelId }; + if (shouldPreserveSelectedModel(resolvedCliModelId, normalized)) return sessionPayload; + const descriptor = getModelById(resolvedCliModelId); + const reportedMatchesCanonical = descriptor?.providerModelId === normalizedCliModel; + return { + model: reportedMatchesCanonical ? normalizedCliModel : normalized, + modelId: resolvedCliModelId, + }; } const resolvedModelId = resolveModelIdFromStoredValue(normalized, "claude") ?? resolveModelIdFromStoredValue(normalizedCliModel, "claude"); if (resolvedModelId) { - if (shouldPreserveSelectedModel(resolvedModelId)) return sessionPayload; + if (shouldPreserveSelectedModel(resolvedModelId, normalized)) return sessionPayload; return { model: normalized, modelId: resolvedModelId }; } return { model: normalized }; diff --git a/apps/desktop/src/main/services/chat/cursorModelsDiscovery.test.ts b/apps/desktop/src/main/services/chat/cursorModelsDiscovery.test.ts index 2be597913..2f016defb 100644 --- a/apps/desktop/src/main/services/chat/cursorModelsDiscovery.test.ts +++ b/apps/desktop/src/main/services/chat/cursorModelsDiscovery.test.ts @@ -66,7 +66,7 @@ describe("parseCursorCliModelsStdout", () => { "", "auto - Auto (current)", "composer-2 - Composer 2", - "claude-4.6-sonnet-medium - Sonnet 4.6 1M", + "claude-4.6-sonnet-medium - Sonnet 5 1M", ].join("\n"); const rows = parseCursorCliModelsStdout(raw); @@ -94,7 +94,7 @@ describe("parseCursorCliModelsStdout", () => { }); resolveModels([ - { id: "claude-4.6-sonnet-medium", displayName: "Sonnet 4.6 Medium" }, + { id: "claude-4.6-sonnet-medium", displayName: "Sonnet 5 Medium" }, { id: "auto", displayName: "Auto" }, ]); await new Promise((resolve) => setTimeout(resolve, 0)); @@ -123,7 +123,7 @@ describe("parseCursorCliModelsStdout", () => { expect(cursorModelsListMock).toHaveBeenCalledWith({ apiKey: "crsr_test" }); }); resolveModels([ - { id: "claude-4.6-sonnet-medium", displayName: "Sonnet 4.6 Medium" }, + { id: "claude-4.6-sonnet-medium", displayName: "Sonnet 5 Medium" }, { id: "auto", displayName: "Auto" }, ]); await new Promise((resolve) => setTimeout(resolve, 0)); @@ -137,7 +137,7 @@ describe("parseCursorCliModelsStdout", () => { it("probes exact Cursor SDK models when requested", async () => { cursorModelsListMock.mockResolvedValue([ - { id: "claude-4.6-sonnet-medium", displayName: "Sonnet 4.6 Medium" }, + { id: "claude-4.6-sonnet-medium", displayName: "Sonnet 5 Medium" }, { id: "composer-2", displayName: "Composer 2", aliases: ["composer-latest"] }, { id: "auto", displayName: "Auto" }, ]); diff --git a/apps/desktop/src/main/services/chat/droidModelsDiscovery.test.ts b/apps/desktop/src/main/services/chat/droidModelsDiscovery.test.ts index 8c8370877..4b8ca00ec 100644 --- a/apps/desktop/src/main/services/chat/droidModelsDiscovery.test.ts +++ b/apps/desktop/src/main/services/chat/droidModelsDiscovery.test.ts @@ -80,7 +80,7 @@ describe("parseDroidExecHelpModels", () => { "Usage: droid exec [options] [prompt]", "", "Custom Models:", - " custom:claude-sonnet-4-6-thinking-32000 Claude Sonnet 4.6 (High)", + " custom:claude-sonnet-5-thinking-32000 Claude Sonnet 5 (High)", " custom:gpt-5.4(xhigh) GPT-5.4 (XHigh)", "", "Model details:", @@ -88,8 +88,8 @@ describe("parseDroidExecHelpModels", () => { expect(parseDroidExecHelpModels(raw)).toEqual([ { - id: "custom:claude-sonnet-4-6-thinking-32000", - displayName: "Claude Sonnet 4.6 (High)", + id: "custom:claude-sonnet-5-thinking-32000", + displayName: "Claude Sonnet 5 (High)", }, { id: "custom:gpt-5.4(xhigh)", @@ -106,8 +106,8 @@ describe("discoverDroidCliModelDescriptors", () => { initResult: { availableModels: [ { - id: "claude-sonnet-4-6", - displayName: "Claude Sonnet 4.6", + id: "claude-sonnet-5", + displayName: "Claude Sonnet 5", }, { id: "custom:gpt-5.4(xhigh)", @@ -126,7 +126,7 @@ describe("discoverDroidCliModelDescriptors", () => { })); expect(close).toHaveBeenCalled(); expect(descriptors.map((descriptor) => descriptor.id)).toEqual([ - "droid/claude-sonnet-4-6", + "droid/claude-sonnet-5", "droid/custom:gpt-5.4(xhigh)", ]); expect(descriptors[1]).toMatchObject({ @@ -135,6 +135,109 @@ describe("discoverDroidCliModelDescriptors", () => { }); }); + it("normalizes removed Droid factory model IDs before surfacing them", async () => { + mockCreateSession.mockResolvedValueOnce({ + initResult: { + availableModels: [ + { id: "claude-sonnet-4-6", displayName: "Claude Sonnet 4.6" }, + { id: "sonnet-4-6", displayName: "Sonnet 4.6" }, + { id: "claude-opus-4-7", displayName: "Claude Opus 4.7" }, + { id: "opus-4-7", displayName: "Opus 4.7" }, + { id: "claude-opus-4-6-fast", displayName: "Claude Opus 4.6 Fast Mode" }, + { id: "opus", displayName: "Opus" }, + { id: "claude-opus-4-8", displayName: "Claude Opus 4.8 1M" }, + { id: "claude-sonnet-5", displayName: "Claude Sonnet 5" }, + ], + }, + close: vi.fn(async () => {}), + }); + + const descriptors = await discoverDroidCliModelDescriptors("/mock/bin/droid"); + + expect(descriptors.map((descriptor) => descriptor.id)).toEqual([ + "droid/claude-opus-4-8", + "droid/claude-sonnet-5", + ]); + expect(descriptors.map((descriptor) => descriptor.displayName)).toEqual([ + "Opus 4.8 1M", + "Sonnet 5 (1.2x)", + ]); + expect(descriptors.map((descriptor) => descriptor.providerModelId)).toEqual([ + "claude-opus-4-8", + "claude-sonnet-5", + ]); + }); + + it("reroutes removed Droid runtime ids when canonical replacements are absent", async () => { + mockCreateSession.mockResolvedValueOnce({ + initResult: { + availableModels: [ + { id: "sonnet-4-6", displayName: "Sonnet 4.6", noImageSupport: true }, + { id: "claude-opus-4-6", displayName: "Claude Opus 4.6" }, + { id: "claude-opus-4-6-fast", displayName: "Claude Opus 4.6 Fast Mode" }, + { id: "opus-4-6", displayName: "Opus 4.6" }, + { id: "opus", displayName: "Opus" }, + ], + }, + close: vi.fn(async () => {}), + }); + + const descriptors = await discoverDroidCliModelDescriptors("/mock/bin/droid"); + + expect(descriptors.find((descriptor) => descriptor.id === "droid/claude-sonnet-5")).toMatchObject({ + displayName: "Sonnet 5 (1.2x)", + providerModelId: "claude-sonnet-5", + capabilities: expect.objectContaining({ + vision: true, + reasoning: true, + }), + reasoningTiers: ["low", "medium", "high", "max"], + }); + expect(descriptors.find((descriptor) => descriptor.id === "droid/claude-opus-4-8")).toMatchObject({ + displayName: "Opus 4.8 1M", + providerModelId: "claude-opus-4-8", + capabilities: expect.objectContaining({ + vision: true, + reasoning: true, + }), + reasoningTiers: ["low", "medium", "high", "xhigh", "max", "ultracode"], + serviceTiers: ["fast"], + }); + }); + + it("prefers canonical Droid rows over normalized retired aliases", async () => { + mockCreateSession.mockResolvedValueOnce({ + initResult: { + availableModels: [ + { + id: "sonnet-4-6", + displayName: "Sonnet 4.6", + noImageSupport: true, + }, + { + id: "claude-sonnet-5", + displayName: "Claude Sonnet 5", + supportedReasoningEfforts: ["high", "max"], + defaultReasoningEffort: "high", + }, + ], + }, + close: vi.fn(async () => {}), + }); + + const descriptors = await discoverDroidCliModelDescriptors("/mock/bin/droid"); + + expect(descriptors.filter((descriptor) => descriptor.id === "droid/claude-sonnet-5")).toHaveLength(1); + expect(descriptors.find((descriptor) => descriptor.id === "droid/claude-sonnet-5")).toMatchObject({ + providerModelId: "claude-sonnet-5", + reasoningTiers: ["high", "max"], + capabilities: expect.objectContaining({ + vision: true, + reasoning: true, + }), + }); + }); + it("preserves Droid SDK reasoning and media metadata without exposing tier as a toggle", async () => { const close = vi.fn(async () => {}); mockCreateSession.mockResolvedValueOnce({ @@ -173,8 +276,8 @@ describe("discoverDroidCliModelDescriptors", () => { JSON.stringify({ custom_models: [ { - model: "claude-sonnet-4-6-thinking-32000", - model_display_name: "Claude Sonnet 4.6 (High)", + model: "claude-sonnet-5-thinking-32000", + model_display_name: "Claude Sonnet 5 (High)", }, ], }), @@ -184,8 +287,8 @@ describe("discoverDroidCliModelDescriptors", () => { initResult: { availableModels: [ { - id: "claude-sonnet-4-6", - displayName: "Claude Sonnet 4.6", + id: "claude-sonnet-5", + displayName: "Claude Sonnet 5", }, ], }, @@ -195,12 +298,115 @@ describe("discoverDroidCliModelDescriptors", () => { const descriptors = await discoverDroidCliModelDescriptors("/mock/bin/droid"); expect(descriptors.map((descriptor) => descriptor.id)).toEqual([ - "droid/claude-sonnet-4-6", - "droid/custom:claude-sonnet-4-6-thinking-32000", + "droid/claude-sonnet-5", + "droid/custom:claude-sonnet-5-thinking-32000", ]); expect(descriptors[1]).toMatchObject({ - displayName: "Claude Sonnet 4.6 (High)", + displayName: "Claude Sonnet 5 (High)", + customProxy: true, + }); + }); + + it("normalizes retired Factory config custom aliases while preserving the custom proxy", async () => { + fs.mkdirSync(path.join(tmpHome, ".factory"), { recursive: true }); + fs.writeFileSync( + path.join(tmpHome, ".factory", "config.json"), + JSON.stringify({ + custom_models: [ + { + model: "sonnet-4-6", + model_display_name: "Retired Sonnet custom alias", + }, + { + model: "opus-4-7", + model_display_name: "Retired Opus custom alias", + }, + { + model: "custom-real-model", + model_display_name: "Custom Real Model", + }, + ], + }), + "utf8", + ); + mockCreateSession.mockResolvedValueOnce({ + initResult: { + availableModels: [], + }, + close: vi.fn(async () => {}), + }); + + const descriptors = await discoverDroidCliModelDescriptors("/mock/bin/droid"); + + expect(new Set(descriptors.map((descriptor) => descriptor.id))).toEqual(new Set([ + "droid/custom:claude-opus-4-8", + "droid/custom:claude-sonnet-5", + "droid/custom:custom-real-model", + ])); + expect(descriptors.find((descriptor) => descriptor.id === "droid/custom:claude-sonnet-5")).toMatchObject({ + providerModelId: "custom:claude-sonnet-5", + displayName: "Sonnet 5 (1.2x)", + customProxy: true, + contextWindow: 1_000_000, + maxOutputTokens: 128_000, + reasoningTiers: ["low", "medium", "high", "max"], + }); + expect(descriptors.find((descriptor) => descriptor.id === "droid/custom:claude-opus-4-8")).toMatchObject({ + providerModelId: "custom:claude-opus-4-8", + displayName: "Opus 4.8 1M", + customProxy: true, + contextWindow: 1_000_000, + maxOutputTokens: 128_000, + serviceTiers: ["fast"], + reasoningTiers: ["low", "medium", "high", "xhigh", "max", "ultracode"], + }); + expect(descriptors.find((descriptor) => descriptor.id === "droid/custom:custom-real-model")).toMatchObject({ + displayName: "Custom Real Model", + customProxy: true, + }); + }); + + it("adds canonical metadata to Factory config custom models that already use canonical ids", async () => { + fs.mkdirSync(path.join(tmpHome, ".factory"), { recursive: true }); + fs.writeFileSync( + path.join(tmpHome, ".factory", "config.json"), + JSON.stringify({ + custom_models: [ + { + model: "claude-sonnet-5", + model_display_name: "Canonical Sonnet custom proxy", + }, + { + model: "claude-opus-4-8", + model_display_name: "Canonical Opus custom proxy", + }, + ], + }), + "utf8", + ); + mockCreateSession.mockResolvedValueOnce({ + initResult: { + availableModels: [], + }, + close: vi.fn(async () => {}), + }); + + const descriptors = await discoverDroidCliModelDescriptors("/mock/bin/droid"); + + expect(descriptors.find((descriptor) => descriptor.id === "droid/custom:claude-sonnet-5")).toMatchObject({ + providerModelId: "custom:claude-sonnet-5", + customProxy: true, + contextWindow: 1_000_000, + maxOutputTokens: 128_000, + reasoningTiers: ["low", "medium", "high", "max"], + }); + expect(descriptors.find((descriptor) => descriptor.id === "droid/custom:claude-opus-4-8")).toMatchObject({ + providerModelId: "custom:claude-opus-4-8", customProxy: true, + contextWindow: 1_000_000, + maxOutputTokens: 128_000, + serviceTiers: ["fast"], + reasoningTiers: ["low", "medium", "high", "xhigh", "max", "ultracode"], }); }); @@ -208,9 +414,9 @@ describe("discoverDroidCliModelDescriptors", () => { vi.useFakeTimers(); vi.setSystemTime(new Date("2026-06-10T00:00:00.000Z")); try { - mockCreateSession.mockResolvedValueOnce(sessionWithModels(["claude-sonnet-4-6"])); + mockCreateSession.mockResolvedValueOnce(sessionWithModels(["claude-sonnet-5"])); const seeded = await discoverDroidCliModelDescriptors("/mock/bin/droid"); - expect(seeded.map((d) => d.id)).toEqual(["droid/claude-sonnet-4-6"]); + expect(seeded.map((d) => d.id)).toEqual(["droid/claude-sonnet-5"]); expect(mockCreateSession).toHaveBeenCalledTimes(1); // Generic readiness invalidation ages the cache without dropping rows. @@ -222,14 +428,14 @@ describe("discoverDroidCliModelDescriptors", () => { // A passive read past the 120s window still returns the cached rows // synchronously and kicks off exactly one background SDK session. const stale = await discoverDroidCliModelDescriptors("/mock/bin/droid", { mode: "cached-or-fallback" }); - expect(stale.map((d) => d.id)).toEqual(["droid/claude-sonnet-4-6"]); + expect(stale.map((d) => d.id)).toEqual(["droid/claude-sonnet-5"]); expect(mockCreateSession).toHaveBeenCalledTimes(2); // Backoff: a second passive read inside the same freshness window must // NOT spawn another session, even though the cache is still aged and the // warm just failed — without backoff a broken droid gets a session per call. const again = await discoverDroidCliModelDescriptors("/mock/bin/droid", { mode: "cached-or-fallback" }); - expect(again.map((d) => d.id)).toEqual(["droid/claude-sonnet-4-6"]); + expect(again.map((d) => d.id)).toEqual(["droid/claude-sonnet-5"]); expect(mockCreateSession).toHaveBeenCalledTimes(2); } finally { vi.useRealTimers(); diff --git a/apps/desktop/src/main/services/chat/droidModelsDiscovery.ts b/apps/desktop/src/main/services/chat/droidModelsDiscovery.ts index c22d272f9..16bfb49f7 100644 --- a/apps/desktop/src/main/services/chat/droidModelsDiscovery.ts +++ b/apps/desktop/src/main/services/chat/droidModelsDiscovery.ts @@ -16,6 +16,9 @@ export type DroidExecHelpModelRow = { /** True when sourced from ~/.factory/config.json (vibeproxy / custom proxy). */ customProxy?: boolean; reasoningTiers?: string[]; + serviceTiers?: string[]; + contextWindow?: number; + maxOutputTokens?: number; capabilities?: Partial; }; type DroidCliModelDiscoveryMode = "probe" | "cached-or-fallback"; @@ -29,6 +32,13 @@ const TTL_MS = 120_000; // the background) so passive consumers never lose models between probes. const POSITIVE_TTL_MS = 6 * 60 * 60_000; +const CANONICAL_DROID_ANTHROPIC_CAPABILITIES: Partial = { + tools: true, + vision: true, + reasoning: true, + streaming: true, +}; + export function parseDroidExecHelpModels(stdout: string): DroidExecHelpModelRow[] { const lines = stdout.split(/\r?\n/); const rows: DroidExecHelpModelRow[] = []; @@ -244,6 +254,62 @@ function readSdkModelRows(initResult: unknown): DroidExecHelpModelRow[] { return rows; } +function canonicalDroidReplacementForAlias( + id: string, + options?: { customProxy?: boolean }, +): DroidExecHelpModelRow | null { + const normalized = id.trim().toLowerCase(); + const idPrefix = options?.customProxy ? "custom:" : ""; + const customProxy = options?.customProxy ? { customProxy: true } : {}; + if ( + normalized === "claude-sonnet-4-6" + || normalized === "sonnet-4-6" + || (options?.customProxy && normalized === "claude-sonnet-5") + ) { + return { + id: `${idPrefix}claude-sonnet-5`, + displayName: "Sonnet 5 (1.2x)", + ...customProxy, + contextWindow: 1_000_000, + maxOutputTokens: 128_000, + capabilities: CANONICAL_DROID_ANTHROPIC_CAPABILITIES, + reasoningTiers: ["low", "medium", "high", "max"], + }; + } + if ( + normalized === "claude-opus-4-7" + || normalized === "opus-4-7" + || normalized === "opus-4.7" + || normalized === "claude-opus-4-6" + || normalized === "claude-opus-4-6-fast" + || normalized === "opus-4-6" + || normalized === "opus-4.6" + || normalized === "opus" + || (options?.customProxy && normalized === "claude-opus-4-8") + ) { + return { + id: `${idPrefix}claude-opus-4-8`, + displayName: "Opus 4.8 1M", + ...customProxy, + contextWindow: 1_000_000, + maxOutputTokens: 128_000, + capabilities: CANONICAL_DROID_ANTHROPIC_CAPABILITIES, + reasoningTiers: ["low", "medium", "high", "xhigh", "max", "ultracode"], + serviceTiers: ["fast"], + }; + } + return null; +} + +function normalizeDroidDiscoveredModel(row: DroidExecHelpModelRow): DroidExecHelpModelRow { + const id = row.id.trim().toLowerCase(); + const customProxy = row.customProxy === true && id.startsWith("custom:"); + const normalizedCustomId = customProxy ? id.slice("custom:".length) : id; + const replacement = canonicalDroidReplacementForAlias(normalizedCustomId, { customProxy }); + if (replacement) return replacement; + return row; +} + async function listDroidModelsFromSdk(droidPath: string): Promise { const now = Date.now(); const controller = new AbortController(); @@ -363,17 +429,35 @@ export async function discoverDroidCliModelDescriptors( // models appear even when the CLI help output doesn't list them. const customRows = await readFactoryConfigCustomModels(); - const seen = new Set(); const descriptors: ModelDescriptor[] = []; - for (const row of [...baseRows, ...customRows]) { + const descriptorIds = new Map(); + const descriptorPreferredDuplicateSources = new Map(); + for (const rawRow of [...baseRows, ...customRows]) { + const row = normalizeDroidDiscoveredModel(rawRow); const trimmed = String(row.id ?? "").trim(); - if (!trimmed || seen.has(trimmed)) continue; - seen.add(trimmed); - descriptors.push(createDynamicDroidCliModelDescriptor(trimmed, row.displayName, { + if (!trimmed) continue; + const descriptorKey = trimmed.toLowerCase(); + const preferredDuplicateSource = rawRow.id.trim().toLowerCase() === trimmed.toLowerCase(); + const descriptor = createDynamicDroidCliModelDescriptor(trimmed, row.displayName, { customProxy: row.customProxy, ...(row.reasoningTiers?.length ? { reasoningTiers: row.reasoningTiers } : {}), + ...(row.serviceTiers?.length ? { serviceTiers: row.serviceTiers } : {}), + ...(row.contextWindow ? { contextWindow: row.contextWindow } : {}), + ...(row.maxOutputTokens ? { maxOutputTokens: row.maxOutputTokens } : {}), ...(row.capabilities ? { capabilities: row.capabilities } : {}), - })); + }); + const existingIndex = descriptorIds.get(descriptorKey); + if (existingIndex !== undefined) { + const existingPreferred = descriptorPreferredDuplicateSources.get(descriptorKey) ?? false; + if (!existingPreferred && preferredDuplicateSource) { + descriptors[existingIndex] = descriptor; + descriptorPreferredDuplicateSources.set(descriptorKey, true); + } + continue; + } + descriptorIds.set(descriptorKey, descriptors.length); + descriptorPreferredDuplicateSources.set(descriptorKey, preferredDuplicateSource); + descriptors.push(descriptor); } return sortDroidCliDescriptorsForPicker(descriptors); } diff --git a/apps/desktop/src/main/services/config/projectConfigService.test.ts b/apps/desktop/src/main/services/config/projectConfigService.test.ts index 38528d8e9..e3f86e34a 100644 --- a/apps/desktop/src/main/services/config/projectConfigService.test.ts +++ b/apps/desktop/src/main/services/config/projectConfigService.test.ts @@ -889,7 +889,7 @@ describe("projectConfigService - automation execution", () => { mission: { title: "Run mission" }, }, modelConfig: { - orchestratorModel: { modelId: "anthropic/claude-sonnet-4-6", thinkingLevel: "medium" }, + orchestratorModel: { modelId: "anthropic/claude-sonnet-5", thinkingLevel: "medium" }, profileId: "legacy-profile", }, }, @@ -984,7 +984,7 @@ describe("projectConfigService - automation execution", () => { }); expect(customRule.prompt).toBe("Run mission"); expect(customRule.modelConfig).toMatchObject({ - modelId: "anthropic/claude-sonnet-4-6", + modelId: "anthropic/claude-sonnet-5", thinkingLevel: "medium", }); expect(presetRule.execution).toMatchObject({ diff --git a/apps/desktop/src/main/services/conflicts/conflictService.test.ts b/apps/desktop/src/main/services/conflicts/conflictService.test.ts index e93b7f443..3823fe2d3 100644 --- a/apps/desktop/src/main/services/conflicts/conflictService.test.ts +++ b/apps/desktop/src/main/services/conflicts/conflictService.test.ts @@ -687,7 +687,7 @@ describe("conflictService conflict context integrity", () => { sourceLaneIds: ["lane-1"], targetLaneId: "lane-target", scenario: "single-merge", - model: "anthropic/claude-sonnet-4-6", + model: "anthropic/claude-sonnet-5", reasoningEffort: "high", permissionMode: "full_edit", originSurface: "queue", @@ -699,7 +699,7 @@ describe("conflictService conflict context integrity", () => { const preparedSummary = service.listExternalResolverRuns({ laneId: "lane-1" }).find((entry) => entry.runId === prepared.runId); expect(preparedSummary?.originSurface).toBe("queue"); expect(preparedSummary?.originRunId).toBe("run-1"); - expect(preparedSummary?.model).toBe("anthropic/claude-sonnet-4-6"); + expect(preparedSummary?.model).toBe("anthropic/claude-sonnet-5"); expect(preparedSummary?.reasoningEffort).toBe("high"); expect(preparedSummary?.permissionMode).toBe("full_edit"); expect(preparedSummary?.resolverContextKey).toBeTruthy(); diff --git a/apps/desktop/src/main/services/cto/ctoStateService.ts b/apps/desktop/src/main/services/cto/ctoStateService.ts index 6335b89de..f7ec60e77 100644 --- a/apps/desktop/src/main/services/cto/ctoStateService.ts +++ b/apps/desktop/src/main/services/cto/ctoStateService.ts @@ -182,7 +182,7 @@ function buildCtoEnvironmentKnowledge(): string { " - spawnChat: Creates a native ADE chat session with AI, streaming, tool approval, and service integration. Use when the user wants an AI agent, a chat, or AI-powered work.", " - createTerminal: Opens a shell (PTY) for raw CLI commands. Use when the user wants a terminal, shell, or to run a specific CLI tool.", " - spawnChat creates ADE-managed agent chats. createTerminal opens a raw shell for CLI commands. When the user says 'start a chat' or 'launch an agent', prefer spawnChat unless they explicitly ask for a terminal.", - " - Example: 'Launch a chat with opus' → spawnChat({ modelId: 'anthropic/claude-opus-4-7', ... }). 'Open a terminal' → createTerminal. 'Run npm test' → createTerminal({ startupCommand: 'npm test' }).", + " - Example: 'Launch a chat with opus' → spawnChat({ modelId: 'anthropic/claude-opus-4-8', ... }). 'Open a terminal' → createTerminal. 'Run npm test' → createTerminal({ startupCommand: 'npm test' }).", "", "Tool calling convention:", " - Use the `ade` CLI per the ADE CLI operating guidance in your doctrine.", diff --git a/apps/desktop/src/main/services/feedback/feedbackReporterService.test.ts b/apps/desktop/src/main/services/feedback/feedbackReporterService.test.ts index ce210606b..fdb23f800 100644 --- a/apps/desktop/src/main/services/feedback/feedbackReporterService.test.ts +++ b/apps/desktop/src/main/services/feedback/feedbackReporterService.test.ts @@ -52,7 +52,7 @@ describe("createFeedbackReporterService", () => { }); const draft = await service.prepareDraft({ - modelId: "anthropic/claude-opus-4-7", + modelId: "anthropic/claude-opus-4-8", draftInput: { category: "bug", summary: "Failed submissions should show the error reason.", @@ -135,7 +135,7 @@ describe("createFeedbackReporterService", () => { additionalContext: "", }, userDescription: "## Summary\n\nPosting should preserve the prepared draft.", - modelId: "anthropic/claude-opus-4-7", + modelId: "anthropic/claude-opus-4-8", reasoningEffort: null, title: "Preserve reviewed drafts when GitHub posting fails", body: "## Description\n\nDeterministic body.", diff --git a/apps/desktop/src/main/services/opencode/openCodeInventory.test.ts b/apps/desktop/src/main/services/opencode/openCodeInventory.test.ts index ca583edb1..8080d0782 100644 --- a/apps/desktop/src/main/services/opencode/openCodeInventory.test.ts +++ b/apps/desktop/src/main/services/opencode/openCodeInventory.test.ts @@ -153,6 +153,19 @@ describe("openCodeInventory", () => { name: "Claude Sonnet 4.6", tool_call: true, reasoning: true, + limit: { context: 200000, output: 32000 }, + }, + "claude-sonnet-5": { + id: "claude-sonnet-5", + name: "Claude Sonnet 5", + tool_call: true, + reasoning: true, + }, + "claude-opus-4-7": { + id: "claude-opus-4-7", + name: "Claude Opus 4.7", + tool_call: true, + reasoning: true, }, }, }, @@ -167,11 +180,217 @@ describe("openCodeInventory", () => { force: true, }); - expect(result.catalogModelIds).toContain("opencode/anthropic/claude-sonnet-4-6"); - expect(result.modelIds).not.toContain("opencode/anthropic/claude-sonnet-4-6"); + expect(result.catalogModelIds).toContain("opencode/anthropic/claude-sonnet-5"); + expect(result.catalogModelIds).toContain("opencode/anthropic/claude-opus-4-8"); + expect(result.catalogModelIds).not.toContain("opencode/anthropic/claude-sonnet-4-6"); + expect(result.catalogModelIds).not.toContain("opencode/anthropic/claude-opus-4-7"); + expect(result.descriptors.find((descriptor) => descriptor.id === "opencode/anthropic/claude-sonnet-5")).toMatchObject({ + contextWindow: 1_000_000, + maxOutputTokens: 128_000, + }); + expect(result.modelIds).not.toContain("opencode/anthropic/claude-sonnet-5"); expect(result.providers.find((provider) => provider.id === "anthropic")?.connected).toBe(false); }); + it("prefers canonical OpenCode model rows over normalized retired aliases", async () => { + const logger = { warn: vi.fn() } as any; + mockState.providerList.mockResolvedValueOnce({ + data: { + connected: ["anthropic"], + all: [ + { + id: "anthropic", + name: "Anthropic", + models: { + "claude-sonnet-4-6": { + id: "claude-sonnet-4-6", + name: "Claude Sonnet 4.6", + capabilities: { + reasoning: false, + toolcall: false, + }, + variants: { + stale: {}, + }, + }, + "claude-sonnet-5": { + id: "claude-sonnet-5", + name: "Claude Sonnet 5", + capabilities: { + reasoning: true, + toolcall: true, + input: { image: true }, + }, + variants: { + high: {}, + fast: {}, + }, + }, + "claude-opus-4-7": { + id: "claude-opus-4-7", + name: "Claude Opus 4.7", + capabilities: { + reasoning: false, + toolcall: false, + }, + variants: { + stale: {}, + }, + }, + "claude-opus-4-6": { + id: "claude-opus-4-6", + name: "Claude Opus 4.6", + capabilities: { + reasoning: false, + toolcall: false, + }, + variants: { + stale: {}, + }, + }, + "opus-4-6": { + id: "opus-4-6", + name: "Opus 4.6", + capabilities: { + reasoning: false, + toolcall: false, + }, + variants: { + stale: {}, + }, + }, + "opus-4.6": { + id: "opus-4.6", + name: "Opus 4.6", + capabilities: { + reasoning: false, + toolcall: false, + }, + variants: { + stale: {}, + }, + }, + "claude-opus-4-8": { + id: "claude-opus-4-8", + name: "Claude Opus 4.8 1M", + capabilities: { + reasoning: true, + toolcall: true, + input: { image: true }, + }, + variants: { + max: {}, + fast: {}, + }, + }, + }, + }, + ], + }, + } as any); + + const result = await probeOpenCodeProviderInventory({ + projectRoot: "/repo", + projectConfig: { ai: {} }, + logger, + force: true, + }); + + const descriptor = result.descriptors.find((entry) => entry.id === "opencode/anthropic/claude-sonnet-5"); + const opusDescriptor = result.descriptors.find((entry) => entry.id === "opencode/anthropic/claude-opus-4-8"); + expect(result.descriptors.filter((entry) => entry.id === "opencode/anthropic/claude-sonnet-5")).toHaveLength(1); + expect(result.descriptors.filter((entry) => entry.id === "opencode/anthropic/claude-opus-4-8")).toHaveLength(1); + expect(result.modelIds).not.toContain("opencode/anthropic/claude-opus-4-6"); + expect(result.modelIds).not.toContain("opencode/anthropic/opus-4-6"); + expect(result.modelIds).not.toContain("opencode/anthropic/opus-4.6"); + expect(descriptor?.capabilities).toMatchObject({ + tools: true, + vision: true, + reasoning: true, + }); + expect(descriptor?.reasoningTiers).toEqual(["high"]); + expect(descriptor?.serviceTiers).toEqual(["fast"]); + expect(opusDescriptor?.capabilities).toMatchObject({ + tools: true, + vision: true, + reasoning: true, + }); + expect(opusDescriptor?.reasoningTiers).toEqual(["max"]); + expect(opusDescriptor?.serviceTiers).toEqual(["fast"]); + }); + + it("normalizes retired-only Anthropic OpenCode rows to canonical launch ids", async () => { + const logger = { warn: vi.fn() } as any; + mockState.providerList.mockResolvedValueOnce({ + data: { + connected: ["anthropic"], + all: [ + { + id: "anthropic", + name: "Anthropic", + models: { + "claude-sonnet-4-6": { + id: "claude-sonnet-4-6", + name: "Claude Sonnet 4.6", + capabilities: { + reasoning: false, + toolcall: false, + }, + }, + "opus": { + id: "opus", + name: "Opus", + capabilities: { + reasoning: false, + toolcall: false, + }, + }, + }, + }, + ], + }, + } as any); + + const result = await probeOpenCodeProviderInventory({ + projectRoot: "/repo", + projectConfig: { ai: {} }, + logger, + force: true, + }); + + expect(result.modelIds).toContain("opencode/anthropic/claude-sonnet-5"); + expect(result.modelIds).toContain("opencode/anthropic/claude-opus-4-8"); + expect(result.modelIds).not.toContain("opencode/anthropic/claude-sonnet-4-6"); + expect(result.modelIds).not.toContain("opencode/anthropic/opus"); + expect(result.descriptors.find((entry) => entry.id === "opencode/anthropic/claude-sonnet-5")).toMatchObject({ + displayName: "Claude Sonnet 5", + openCodeModelId: "claude-sonnet-5", + providerModelId: "anthropic/claude-sonnet-5", + contextWindow: 1_000_000, + maxOutputTokens: 128_000, + capabilities: expect.objectContaining({ + tools: true, + vision: true, + reasoning: true, + }), + reasoningTiers: ["low", "medium", "high", "max"], + }); + expect(result.descriptors.find((entry) => entry.id === "opencode/anthropic/claude-opus-4-8")).toMatchObject({ + displayName: "Claude Opus 4.8 1M", + openCodeModelId: "claude-opus-4-8", + providerModelId: "anthropic/claude-opus-4-8", + contextWindow: 1_000_000, + maxOutputTokens: 128_000, + capabilities: expect.objectContaining({ + tools: true, + vision: true, + reasoning: true, + }), + reasoningTiers: ["low", "medium", "high", "xhigh", "max", "ultracode"], + serviceTiers: ["fast"], + }); + }); + it("classifies OpenCode SDK model variants and v2 capabilities", async () => { const logger = { warn: vi.fn() } as any; mockState.providerList.mockResolvedValueOnce({ diff --git a/apps/desktop/src/main/services/opencode/openCodeInventory.ts b/apps/desktop/src/main/services/opencode/openCodeInventory.ts index 89f74c1e5..f750c9868 100644 --- a/apps/desktop/src/main/services/opencode/openCodeInventory.ts +++ b/apps/desktop/src/main/services/opencode/openCodeInventory.ts @@ -5,6 +5,7 @@ import { createDynamicOpenCodeModelDescriptor, isLocalProviderFamily, replaceDynamicOpenCodeModelDescriptors, + type ModelCapabilities, type ModelDescriptor, } from "../../../shared/modelRegistry"; import { stableStringify } from "../shared/utils"; @@ -98,6 +99,13 @@ const OPENCODE_SERVICE_VARIANT_ALIASES: Record = { fast: "fast", }; +const CANONICAL_ANTHROPIC_MODEL_CAPABILITIES: ModelCapabilities = { + tools: true, + vision: true, + reasoning: true, + streaming: true, +}; + function addUnique(out: string[], value: string): void { if (!out.some((entry) => entry.trim().toLowerCase() === value)) out.push(value); } @@ -154,6 +162,77 @@ function readOpenCodeModelCapabilities(model: Record): { }; } +function normalizeOpenCodeProviderModel( + providerId: string, + modelId: string, + _availableProviderModelIds: Set, + displayName?: string, +): { + modelId: string; + displayName?: string; + contextWindow?: number; + maxOutputTokens?: number; + reasoningTiers?: string[]; + serviceTiers?: string[]; + capabilities?: ModelCapabilities; + preferredDuplicateSource: boolean; +} { + if (providerId.trim().toLowerCase() !== "anthropic") { + return { modelId, ...(displayName ? { displayName } : {}), preferredDuplicateSource: true }; + } + const normalized = modelId.trim().toLowerCase(); + const preferredDuplicateSource = normalized === "claude-sonnet-5" || normalized === "claude-opus-4-8"; + if (normalized === "claude-sonnet-5") { + return { + modelId: "claude-sonnet-5", + displayName: displayName ?? "Claude Sonnet 5", + contextWindow: 1_000_000, + maxOutputTokens: 128_000, + preferredDuplicateSource, + }; + } + if (normalized === "claude-opus-4-8") { + return { + modelId: "claude-opus-4-8", + displayName: displayName ?? "Claude Opus 4.8 1M", + contextWindow: 1_000_000, + maxOutputTokens: 128_000, + preferredDuplicateSource, + }; + } + if (normalized === "claude-sonnet-4-6" || normalized === "sonnet-4-6") { + return { + modelId: "claude-sonnet-5", + displayName: "Claude Sonnet 5", + contextWindow: 1_000_000, + maxOutputTokens: 128_000, + capabilities: CANONICAL_ANTHROPIC_MODEL_CAPABILITIES, + reasoningTiers: ["low", "medium", "high", "max"], + preferredDuplicateSource, + }; + } + if ( + normalized === "claude-opus-4-7" + || normalized === "opus-4-7" + || normalized === "claude-opus-4-6" + || normalized === "opus-4-6" + || normalized === "opus-4.6" + || normalized === "opus" + ) { + return { + modelId: "claude-opus-4-8", + displayName: "Claude Opus 4.8 1M", + contextWindow: 1_000_000, + maxOutputTokens: 128_000, + capabilities: CANONICAL_ANTHROPIC_MODEL_CAPABILITIES, + reasoningTiers: ["low", "medium", "high", "xhigh", "max", "ultracode"], + serviceTiers: ["fast"], + preferredDuplicateSource, + }; + } + return { modelId, ...(displayName ? { displayName } : {}), preferredDuplicateSource }; +} + function openCodeSdkErrorMessage(error: unknown): string | null { if (!error || typeof error !== "object") return null; const record = error as Record; @@ -243,6 +322,8 @@ export async function probeOpenCodeProviderInventory(args: { } const connected = new Set(data.connected); const descriptors: ModelDescriptor[] = []; + const descriptorIds = new Map(); + const descriptorPreferredDuplicateSources = new Map(); const availableProviderModelCounts = new Map(); // Build a set of loaded local model IDs so we can filter out unloaded models @@ -270,6 +351,14 @@ export async function probeOpenCodeProviderInventory(args: { // local-provider catalog; only show models ADE just discovered as loaded. if (isLocal && !discoveryExists) continue; const models = provider.models ?? {}; + const availableProviderModelIds = new Set( + Object.values(models) + .map((model) => { + const record = model as Record; + return typeof record.id === "string" ? record.id.trim().toLowerCase() : ""; + }) + .filter(Boolean), + ); for (const model of Object.values(models)) { const modelRecord = model as Record; const mid = typeof modelRecord.id === "string" ? modelRecord.id.trim() : ""; @@ -277,7 +366,8 @@ export async function probeOpenCodeProviderInventory(args: { // For local providers, only include models that are actively loaded. if (discoveryExists && (!allowedModels || !allowedModels.has(mid))) continue; const variants = classifyOpenCodeVariants(modelRecord); - const displayName = typeof modelRecord.name === "string" && modelRecord.name.trim().length ? modelRecord.name.trim() : undefined; + const rawDisplayName = typeof modelRecord.name === "string" && modelRecord.name.trim().length ? modelRecord.name.trim() : undefined; + const normalizedModel = normalizeOpenCodeProviderModel(provider.id, mid, availableProviderModelIds, rawDisplayName); const limit = typeof modelRecord.limit === "object" && modelRecord.limit ? modelRecord.limit as { context?: number; output?: number } : null; @@ -287,18 +377,36 @@ export async function probeOpenCodeProviderInventory(args: { const out = typeof limit?.output === "number" ? Number(limit.output) : undefined; - descriptors.push( - createDynamicOpenCodeModelDescriptor("", { - openCodeProviderId: provider.id, - openCodeModelId: mid, - ...(displayName ? { displayName } : {}), - ...(Number.isFinite(ctx) && (ctx as number) > 0 ? { contextWindow: ctx as number } : {}), - ...(Number.isFinite(out) && (out as number) > 0 ? { maxOutputTokens: out as number } : {}), - ...(variants.reasoningTiers.length ? { reasoningTiers: variants.reasoningTiers } : {}), - ...(variants.serviceTiers.length ? { serviceTiers: variants.serviceTiers } : {}), - capabilities: readOpenCodeModelCapabilities(modelRecord), - }), - ); + const descriptor = createDynamicOpenCodeModelDescriptor("", { + openCodeProviderId: provider.id, + openCodeModelId: normalizedModel.modelId, + ...(normalizedModel.displayName ? { displayName: normalizedModel.displayName } : {}), + ...(normalizedModel.contextWindow + ? { contextWindow: normalizedModel.contextWindow } + : Number.isFinite(ctx) && (ctx as number) > 0 ? { contextWindow: ctx as number } : {}), + ...(normalizedModel.maxOutputTokens + ? { maxOutputTokens: normalizedModel.maxOutputTokens } + : Number.isFinite(out) && (out as number) > 0 ? { maxOutputTokens: out as number } : {}), + ...(variants.reasoningTiers.length ? { reasoningTiers: variants.reasoningTiers } : {}), + ...(variants.serviceTiers.length ? { serviceTiers: variants.serviceTiers } : {}), + ...(normalizedModel.reasoningTiers?.length ? { reasoningTiers: normalizedModel.reasoningTiers } : {}), + ...(normalizedModel.serviceTiers?.length ? { serviceTiers: normalizedModel.serviceTiers } : {}), + capabilities: normalizedModel.capabilities ?? readOpenCodeModelCapabilities(modelRecord), + }); + const existingIndex = descriptorIds.get(descriptor.id); + if (existingIndex !== undefined) { + if ( + normalizedModel.preferredDuplicateSource + && descriptorPreferredDuplicateSources.get(descriptor.id) !== true + ) { + descriptors[existingIndex] = descriptor; + descriptorPreferredDuplicateSources.set(descriptor.id, true); + } + continue; + } + descriptorIds.set(descriptor.id, descriptors.length); + descriptorPreferredDuplicateSources.set(descriptor.id, normalizedModel.preferredDuplicateSource); + descriptors.push(descriptor); if (connected.has(provider.id)) { availableProviderModelCounts.set( provider.id, diff --git a/apps/desktop/src/main/services/orchestration/orchestrationService.test.ts b/apps/desktop/src/main/services/orchestration/orchestrationService.test.ts index 65386590f..8bb5c4a25 100644 --- a/apps/desktop/src/main/services/orchestration/orchestrationService.test.ts +++ b/apps/desktop/src/main/services/orchestration/orchestrationService.test.ts @@ -909,7 +909,7 @@ function resolveModel( const MODEL_FALLBACK: ModelSelection = { provider: "claude", - modelId: "claude-sonnet-4-6", + modelId: "claude-sonnet-5", reasoningEffort: null, }; @@ -958,7 +958,7 @@ describe("model routing precedence", () => { it("caller fallback when routing is empty", () => { const res = resolveModel({}, "validator", "anything", MODEL_FALLBACK); - expect(res.selection.modelId).toBe("claude-sonnet-4-6"); + expect(res.selection.modelId).toBe("claude-sonnet-5"); expect(res.routingKey).toBe("fallback"); }); @@ -1986,7 +1986,7 @@ describe("orchestration watcher resilience", () => { briefText: BRIEF, spawnFingerprint: { provider: "claude", - modelId: "claude-sonnet-4-6", + modelId: "claude-sonnet-5", reasoningEffort: null, resolvedAt: "now", routingKey: "default", diff --git a/apps/desktop/src/main/services/orchestration/runtimeProfile.ts b/apps/desktop/src/main/services/orchestration/runtimeProfile.ts index f6d47d1f1..8bd2aef2f 100644 --- a/apps/desktop/src/main/services/orchestration/runtimeProfile.ts +++ b/apps/desktop/src/main/services/orchestration/runtimeProfile.ts @@ -26,7 +26,7 @@ export function resolveOrchestrationModel( if (r.default) return { ...r.default, routingKey: "default" }; return { provider: "claude", - modelId: "claude-sonnet-4-6", + modelId: "claude-sonnet-5", reasoningEffort: null, fastMode: false, routingKey: "fallback", diff --git a/apps/desktop/src/main/services/prs/prMergeQueue.test.ts b/apps/desktop/src/main/services/prs/prMergeQueue.test.ts index ce1ab0da5..0e7a78cfb 100644 --- a/apps/desktop/src/main/services/prs/prMergeQueue.test.ts +++ b/apps/desktop/src/main/services/prs/prMergeQueue.test.ts @@ -281,7 +281,7 @@ describe("queueLandingService", () => { cwdLaneId: "lane-1", integrationLaneId: null, scenario: "single-merge", - model: "anthropic/claude-sonnet-4-6", + model: "anthropic/claude-sonnet-5", reasoningEffort: "medium", permissionMode: "guarded_edit", command: [], @@ -339,7 +339,7 @@ describe("queueLandingService", () => { groupId: "group-2", method: "squash", autoResolve: true, - resolverModel: "anthropic/claude-sonnet-4-6", + resolverModel: "anthropic/claude-sonnet-5", reasoningEffort: "medium", originSurface: "queue", originLabel: "Queue B", @@ -353,7 +353,7 @@ describe("queueLandingService", () => { expect(runExternalResolver).toHaveBeenCalledTimes(1); expect(runExternalResolver.mock.calls[0]?.[0]).toMatchObject({ originSurface: "queue", - model: "anthropic/claude-sonnet-4-6", + model: "anthropic/claude-sonnet-5", }); expect(completed.entries[0]?.resolvedByAi).toBe(true); expect(land).toHaveBeenCalledTimes(2); diff --git a/apps/desktop/src/main/services/usage/usagePricing.ts b/apps/desktop/src/main/services/usage/usagePricing.ts index c6346b17c..4617dbdb9 100644 --- a/apps/desktop/src/main/services/usage/usagePricing.ts +++ b/apps/desktop/src/main/services/usage/usagePricing.ts @@ -47,6 +47,7 @@ const STATIC_TOKEN_PRICES: Record = { "claude-opus-4-7": tokenPrice(5, 25, 0.5, 6.25), "claude-opus-4-6": tokenPrice(5, 25, 0.5, 6.25), "claude-opus-4-5": tokenPrice(5, 25, 0.5, 6.25), + "claude-sonnet-5": tokenPrice(2, 10, 0.2, 2.5), "claude-sonnet-4-6": tokenPrice(3, 15, 0.3, 3.75), "claude-sonnet-4-5": tokenPrice(3, 15, 0.3, 3.75), "claude-sonnet-4": tokenPrice(3, 15, 0.3, 3.75), @@ -94,14 +95,14 @@ const STATIC_TOKEN_PRICES: Record = { }; const BUILTIN_PRICING_ALIASES: Record = { - auto: "claude-sonnet-4-5", + auto: "claude-sonnet-5", fable: "claude-fable-5", "anthropic--claude-4.6-opus": "claude-opus-4-6", - "anthropic--claude-4.6-sonnet": "claude-sonnet-4-6", + "anthropic--claude-4.6-sonnet": "claude-sonnet-5", "anthropic--claude-4.5-opus": "claude-opus-4-5", "anthropic--claude-4.5-sonnet": "claude-sonnet-4-5", "anthropic--claude-4.5-haiku": "claude-haiku-4-5", - "claude-sonnet-4.6": "claude-sonnet-4-6", + "claude-sonnet-4.6": "claude-sonnet-5", "claude-sonnet-4.5": "claude-sonnet-4-5", "claude-opus-4.7": "claude-opus-4-7", "claude-opus-4.6": "claude-opus-4-6", @@ -124,11 +125,11 @@ const BUILTIN_PRICING_ALIASES: Record = { "claude-4-sonnet-thinking": "claude-sonnet-4-5", "claude-4.5-sonnet": "claude-sonnet-4-5", "claude-4.5-sonnet-thinking": "claude-sonnet-4-5", - "claude-4.6-sonnet": "claude-sonnet-4-6", - "claude-4.6-sonnet-high": "claude-sonnet-4-6", - "claude-4.6-sonnet-low": "claude-sonnet-4-6", - "claude-4.6-sonnet-thinking": "claude-sonnet-4-6", - "claude-4.6-sonnet-high-thinking": "claude-sonnet-4-6", + "claude-4.6-sonnet": "claude-sonnet-5", + "claude-4.6-sonnet-high": "claude-sonnet-5", + "claude-4.6-sonnet-low": "claude-sonnet-5", + "claude-4.6-sonnet-thinking": "claude-sonnet-5", + "claude-4.6-sonnet-high-thinking": "claude-sonnet-5", "claude-4-opus": "claude-opus-4", "claude-4.5-opus": "claude-opus-4-5", "claude-4.5-opus-high": "claude-opus-4-5", @@ -147,9 +148,9 @@ const BUILTIN_PRICING_ALIASES: Record = { "claude-4.6-haiku": "claude-haiku-4-5", "composer-1": "claude-sonnet-4-5", "composer-1.5": "claude-sonnet-4-5", - "composer-2": "claude-sonnet-4-6", - "composer-2.5": "claude-sonnet-4-6", - "composer-latest": "claude-sonnet-4-6", + "composer-2": "claude-sonnet-5", + "composer-2.5": "claude-sonnet-5", + "composer-latest": "claude-sonnet-5", "gpt-5-fast": "gpt-5", "gpt-5.2-low": "gpt-5", "gpt-5.1-codex-high": "gpt-5.3-codex", diff --git a/apps/desktop/src/main/services/usage/usageTrackingService.test.ts b/apps/desktop/src/main/services/usage/usageTrackingService.test.ts index 32fc46e7d..5872dc0c0 100644 --- a/apps/desktop/src/main/services/usage/usageTrackingService.test.ts +++ b/apps/desktop/src/main/services/usage/usageTrackingService.test.ts @@ -473,6 +473,14 @@ describe("resolveTokenPrice", () => { expect(price.input).toBe(3 / 1_000_000); }); + it("uses Sonnet 5 introductory pricing for the static fallback", () => { + const price = resolveTokenPrice("claude-sonnet-5"); + expect(price.input).toBe(2 / 1_000_000); + expect(price.output).toBe(10 / 1_000_000); + expect(price.cacheRead).toBe(0.2 / 1_000_000); + expect(price.cacheWrite).toBe(2.5 / 1_000_000); + }); + it("returns haiku pricing for haiku models", () => { const price = resolveTokenPrice("claude-haiku-3"); expect(price.input).toBe(1 / 1_000_000); @@ -521,7 +529,7 @@ describe("resolveTokenPrice", () => { const price = resolveTokenPrice("cursor-auto"); expect(price.input).toBe(3 / 1_000_000); expect(price.output).toBe(15 / 1_000_000); - expect(resolveTokenPrice("composer-2.5").input).toBe(3 / 1_000_000); + expect(resolveTokenPrice("composer-2.5").input).toBe(2 / 1_000_000); }); it("prices every ADE Claude and Codex registry id and alias without dynamic pricing", () => { @@ -1605,7 +1613,7 @@ describe("scanDroidLogs", () => { fs.writeFileSync( sessionPath.replace(/\.jsonl$/, ".settings.json"), JSON.stringify({ - model: "custom:[anthropic]-claude-sonnet-4-6-20260501", + model: "custom:[anthropic]-claude-sonnet-5-20260501", tokenUsage: { inputTokens: 101, outputTokens: 41, @@ -1620,7 +1628,7 @@ describe("scanDroidLogs", () => { expect(entries).toHaveLength(2); expect(entries[0]).toMatchObject({ - model: "claude-sonnet-4-6", + model: "claude-sonnet-5", inputTokens: 50, outputTokens: 21, cachedTokens: 4, @@ -1634,7 +1642,7 @@ describe("scanDroidLogs", () => { billableCachedTokens: 5, cacheWriteTokens: 3, }); - expect(aggregateCosts(entries, "droid").tokenBreakdownByPreset?.all?.["claude-sonnet-4-6"]?.costUsd).toBeGreaterThan(0); + expect(aggregateCosts(entries, "droid").tokenBreakdownByPreset?.all?.["claude-sonnet-5"]?.costUsd).toBeGreaterThan(0); } finally { fs.rmSync(tmpDir, { recursive: true, force: true }); } diff --git a/apps/desktop/src/main/utils/terminalSessionSignals.test.ts b/apps/desktop/src/main/utils/terminalSessionSignals.test.ts index e51361cce..6f7dddbfe 100644 --- a/apps/desktop/src/main/utils/terminalSessionSignals.test.ts +++ b/apps/desktop/src/main/utils/terminalSessionSignals.test.ts @@ -143,11 +143,11 @@ describe("terminalSessionSignals", () => { reasoningEffort: "xhigh", }); expect(parseTrackedCliLaunchConfig( - "ADE_DROID_SETTINGS=\"$(mktemp \"${TMPDIR:-/tmp}/ade-droid-settings.XXXXXX.json\")\" && printf %s \"{\\\"sessionDefaultSettings\\\":{\\\"interactionMode\\\":\\\"spec\\\",\\\"autonomyLevel\\\":\\\"off\\\",\\\"specModeModel\\\":\\\"claude-sonnet-4-6\\\",\\\"specModeReasoningEffort\\\":\\\"high\\\"}}\" > \"$ADE_DROID_SETTINGS\" && droid --settings \"$ADE_DROID_SETTINGS\"", + "ADE_DROID_SETTINGS=\"$(mktemp \"${TMPDIR:-/tmp}/ade-droid-settings.XXXXXX.json\")\" && printf %s \"{\\\"sessionDefaultSettings\\\":{\\\"interactionMode\\\":\\\"spec\\\",\\\"autonomyLevel\\\":\\\"off\\\",\\\"specModeModel\\\":\\\"claude-sonnet-5\\\",\\\"specModeReasoningEffort\\\":\\\"high\\\"}}\" > \"$ADE_DROID_SETTINGS\" && droid --settings \"$ADE_DROID_SETTINGS\"", "droid", )).toEqual({ permissionMode: "plan", - model: "claude-sonnet-4-6", + model: "claude-sonnet-5", reasoningEffort: "high", }); expect(parseTrackedCliLaunchConfig("OPENCODE_CONFIG_CONTENT='{\"permission\":{\"*\":\"ask\",\"edit\":\"allow\"}}' opencode", "opencode")).toEqual({ @@ -216,7 +216,7 @@ describe("terminalSessionSignals", () => { targetId: "claude-session-1", launch: { permissionMode: "default" }, }, { model: "anthropic/claude-haiku-4-5", reasoningEffort: "low", permissionMode: "auto", prompt: "keep going" })).toBe( - "claude --permission-mode auto --model haiku --effort low --resume claude-session-1 'keep going'", + "claude --permission-mode auto --model claude-haiku-4-5 --effort low --resume claude-session-1 'keep going'", ); expect(buildTrackedCliResumeCommand({ diff --git a/apps/desktop/src/renderer/browserMock.ts b/apps/desktop/src/renderer/browserMock.ts index 39317b166..9b5691458 100644 --- a/apps/desktop/src/renderer/browserMock.ts +++ b/apps/desktop/src/renderer/browserMock.ts @@ -55,7 +55,7 @@ const resolvedArg2 = const DEFAULT_BROWSER_MOCK_CODEX_MODEL = getDefaultModelDescriptor("codex")?.id ?? "openai/gpt-5.5"; const DEFAULT_BROWSER_MOCK_CLAUDE_MODEL = - getDefaultModelDescriptor("claude")?.id ?? "anthropic/claude-sonnet-4-6"; + getDefaultModelDescriptor("claude")?.id ?? "anthropic/claude-sonnet-5"; const BROWSER_MOCK_PREVIEW_CAPABILITY_UNSUPPORTED = { platform: "darwin", supported: false, @@ -336,10 +336,10 @@ const MOCK_ORCH_MANIFEST: any = { { id: "wrapup", title: "Wrap-up", status: "pending" }, ], agents: [ - { sessionId: MOCK_ORCH_LEAD_SESSION, role: "lead", goalSummary: "Coordinate auth + dashboard implementation", status: "running", lastHeartbeatAt: fiveMinAgo, spawnedAt: oneHourAgo, spawnFingerprint: { provider: "claude", modelId: "claude-sonnet-4-6", resolvedAt: oneHourAgo, routingKey: "default" } }, - { sessionId: MOCK_ORCH_WORKER_1, role: "worker", tag: "auth", displayName: "Auth flow worker", goalSummary: "Build OAuth login with Google + GitHub providers", status: "running", currentStepId: "T-auth-oauth", lastHeartbeatAt: fiveMinAgo, spawnedAt: thirtyMinAgo, spawnFingerprint: { provider: "claude", modelId: "claude-sonnet-4-6", resolvedAt: thirtyMinAgo, routingKey: "byTag" } }, + { sessionId: MOCK_ORCH_LEAD_SESSION, role: "lead", goalSummary: "Coordinate auth + dashboard implementation", status: "running", lastHeartbeatAt: fiveMinAgo, spawnedAt: oneHourAgo, spawnFingerprint: { provider: "claude", modelId: "claude-sonnet-5", resolvedAt: oneHourAgo, routingKey: "default" } }, + { sessionId: MOCK_ORCH_WORKER_1, role: "worker", tag: "auth", displayName: "Auth flow worker", goalSummary: "Build OAuth login with Google + GitHub providers", status: "running", currentStepId: "T-auth-oauth", lastHeartbeatAt: fiveMinAgo, spawnedAt: thirtyMinAgo, spawnFingerprint: { provider: "claude", modelId: "claude-sonnet-5", resolvedAt: thirtyMinAgo, routingKey: "byTag" } }, { sessionId: MOCK_ORCH_WORKER_2, role: "worker", tag: "dashboard", displayName: "Dashboard worker", goalSummary: "Build post-login dashboard with activity feed and quick actions", status: "running", currentStepId: "T-dash-layout", lastHeartbeatAt: tenMinAgo, spawnedAt: thirtyMinAgo, spawnFingerprint: { provider: "codex", modelId: "openai/o3", resolvedAt: thirtyMinAgo, routingKey: "byRoleTag" } }, - { sessionId: MOCK_ORCH_VALIDATOR, role: "validator", tag: "quality", displayName: "Quality validator", goalSummary: "Verify test coverage, type safety, and security review", status: "pending", spawnedAt: thirtyMinAgo, spawnFingerprint: { provider: "claude", modelId: "claude-sonnet-4-6", resolvedAt: thirtyMinAgo, routingKey: "byRole" } }, + { sessionId: MOCK_ORCH_VALIDATOR, role: "validator", tag: "quality", displayName: "Quality validator", goalSummary: "Verify test coverage, type safety, and security review", status: "pending", spawnedAt: thirtyMinAgo, spawnFingerprint: { provider: "claude", modelId: "claude-sonnet-5", resolvedAt: thirtyMinAgo, routingKey: "byRole" } }, ], tasks: [ { id: "T-auth-oauth", phaseId: "developing", title: "OAuth provider integration", description: "Implement Google and GitHub OAuth flows with passport.js", status: "in_progress", tag: "auth", labels: ["backend", "security"], priority: "high", estimatedComplexity: "medium", filesHint: ["src/auth/oauth.ts", "src/auth/providers/google.ts", "src/auth/providers/github.ts"], assigneeSessionId: MOCK_ORCH_WORKER_1, claimedAt: thirtyMinAgo, validationGate: { required: true, stepIds: ["VS-auth-security"] }, attempts: [{ id: "att-1", sessionId: MOCK_ORCH_WORKER_1, startedAt: thirtyMinAgo, outcome: "succeeded" }] }, @@ -357,9 +357,9 @@ const MOCK_ORCH_MANIFEST: any = { checklist: [], }, modelRouting: { - default: { provider: "claude", modelId: "claude-sonnet-4-6" }, - byRole: { validator: { provider: "claude", modelId: "claude-sonnet-4-6", reasoningEffort: "high" } }, - byTag: { auth: { provider: "claude", modelId: "claude-sonnet-4-6" } }, + default: { provider: "claude", modelId: "claude-sonnet-5" }, + byRole: { validator: { provider: "claude", modelId: "claude-sonnet-5", reasoningEffort: "high" } }, + byTag: { auth: { provider: "claude", modelId: "claude-sonnet-5" } }, byRoleTag: { "worker:dashboard": { provider: "codex", modelId: "openai/o3" } }, }, assets: [ @@ -427,7 +427,7 @@ const MOCK_ORCH_SESSIONS: any[] = [ goal: "Coordinate auth + dashboard implementation", toolType: "claude-chat", title: "Orchestrator Lead", status: "running", startedAt: oneHourAgo, endedAt: null, archivedAt: null, exitCode: null, transcriptPath: null, headShaStart: null, headShaEnd: null, lastOutputPreview: "Workers spawned. Auth and dashboard streams running in parallel.", summary: null, runtimeState: "running", resumeCommand: null, - resumeMetadata: { provider: "claude", targetKind: "session", targetId: MOCK_ORCH_LEAD_SESSION, modelId: "claude-sonnet-4-6", model: "Sonnet 4.6", interactionMode: "orchestrator-lead", launch: {} }, + resumeMetadata: { provider: "claude", targetKind: "session", targetId: MOCK_ORCH_LEAD_SESSION, modelId: "claude-sonnet-5", model: "Sonnet 5", interactionMode: "orchestrator-lead", launch: {} }, orchestrationRunId: MOCK_ORCH_RUN_ID, orchestrationRole: "lead", orchestrationBundlePath: MOCK_ORCH_BUNDLE, }, { @@ -435,7 +435,7 @@ const MOCK_ORCH_SESSIONS: any[] = [ goal: "Build OAuth login with Google + GitHub providers", toolType: "claude-chat", title: "Auth flow worker", status: "running", startedAt: thirtyMinAgo, endedAt: null, archivedAt: null, exitCode: null, transcriptPath: null, headShaStart: null, headShaEnd: null, lastOutputPreview: "Implementing Google OAuth callback handler…", summary: null, runtimeState: "running", resumeCommand: null, - resumeMetadata: { provider: "claude", targetKind: "session", targetId: MOCK_ORCH_WORKER_1, modelId: "claude-sonnet-4-6", model: "Sonnet 4.6", interactionMode: "orchestrator-worker", launch: {} }, + resumeMetadata: { provider: "claude", targetKind: "session", targetId: MOCK_ORCH_WORKER_1, modelId: "claude-sonnet-5", model: "Sonnet 5", interactionMode: "orchestrator-worker", launch: {} }, orchestrationRunId: MOCK_ORCH_RUN_ID, orchestrationRole: "worker", orchestrationTag: "auth", orchestrationStepId: "T-auth-oauth", orchestrationParentSessionId: MOCK_ORCH_LEAD_SESSION, orchestrationBundlePath: MOCK_ORCH_BUNDLE, }, { @@ -451,7 +451,7 @@ const MOCK_ORCH_SESSIONS: any[] = [ goal: "Verify test coverage and security review", toolType: "claude-chat", title: "Quality validator", status: "idle", startedAt: thirtyMinAgo, endedAt: null, archivedAt: null, exitCode: null, transcriptPath: null, headShaStart: null, headShaEnd: null, lastOutputPreview: "Waiting for development phase to complete…", summary: null, runtimeState: "idle", resumeCommand: null, - resumeMetadata: { provider: "claude", targetKind: "session", targetId: MOCK_ORCH_VALIDATOR, modelId: "claude-sonnet-4-6", model: "Sonnet 4.6", interactionMode: "orchestrator-validator", launch: {} }, + resumeMetadata: { provider: "claude", targetKind: "session", targetId: MOCK_ORCH_VALIDATOR, modelId: "claude-sonnet-5", model: "Sonnet 5", interactionMode: "orchestrator-validator", launch: {} }, orchestrationRunId: MOCK_ORCH_RUN_ID, orchestrationRole: "validator", orchestrationTag: "quality", orchestrationParentSessionId: MOCK_ORCH_LEAD_SESSION, orchestrationBundlePath: MOCK_ORCH_BUNDLE, }, ]; @@ -2323,7 +2323,7 @@ const BUILTIN_MOCK_QUEUE_STATE: Record = { autoResolve: true, ciGating: true, resolverProvider: "claude", - resolverModel: "anthropic/claude-sonnet-4-6", + resolverModel: "anthropic/claude-sonnet-5", reasoningEffort: "medium", permissionMode: "guarded_edit", confidenceThreshold: null, @@ -2364,7 +2364,7 @@ const BUILTIN_MOCK_QUEUE_STATE: Record = { autoResolve: false, ciGating: true, resolverProvider: null, - resolverModel: "anthropic/claude-sonnet-4-6", + resolverModel: "anthropic/claude-sonnet-5", reasoningEffort: "medium", permissionMode: "guarded_edit", confidenceThreshold: null, @@ -2856,7 +2856,7 @@ if (typeof window !== "undefined" && shouldInstallBrowserMock(window)) { }, models: { claude: [], codex: [], cursor: [], droid: [] }, availableModelIds: [ - "anthropic/claude-sonnet-4-6", + "anthropic/claude-sonnet-5", "anthropic/claude-haiku-4-5", "openai/gpt-5-codex", ], @@ -3060,7 +3060,7 @@ if (typeof window !== "undefined" && shouldInstallBrowserMock(window)) { laneCleanup: {}, ai: { orchestrator: { - defaultOrchestratorModel: { modelId: "anthropic/claude-sonnet-4-6" }, + defaultOrchestratorModel: { modelId: "anthropic/claude-sonnet-5" }, teammatePlanMode: "auto", }, permissions: { @@ -3085,9 +3085,9 @@ if (typeof window !== "undefined" && shouldInstallBrowserMock(window)) { laneOverlayPolicies: [], git: { autoRebaseOnHeadChange: false }, ai: { - featureModelOverrides: { pr_descriptions: "anthropic/claude-sonnet-4-6" }, + featureModelOverrides: { pr_descriptions: "anthropic/claude-sonnet-5" }, orchestrator: { - defaultOrchestratorModel: { modelId: "anthropic/claude-sonnet-4-6" }, + defaultOrchestratorModel: { modelId: "anthropic/claude-sonnet-5" }, teammatePlanMode: "auto", }, permissions: { @@ -3603,7 +3603,7 @@ if (typeof window !== "undefined" && shouldInstallBrowserMock(window)) { }, executor: { mode: "automation-bot" }, modelConfig: { - modelId: "anthropic/claude-sonnet-4-6", + modelId: "anthropic/claude-sonnet-5", thinkingLevel: "medium", }, permissionConfig: { @@ -3720,8 +3720,8 @@ if (typeof window !== "undefined" && shouldInstallBrowserMock(window)) { sessionId: "chat-auto-1", laneId: "lane-1", provider: "claude", - model: "Claude Sonnet 4.6", - modelId: "anthropic/claude-sonnet-4-6", + model: "Claude Sonnet 5", + modelId: "anthropic/claude-sonnet-5", title: "PR follow-up thread", surface: "automation", automationId: "auto-session-review", diff --git a/apps/desktop/src/renderer/components/automations/RulesTab.tsx b/apps/desktop/src/renderer/components/automations/RulesTab.tsx index 269ed3228..5f6bcd935 100644 --- a/apps/desktop/src/renderer/components/automations/RulesTab.tsx +++ b/apps/desktop/src/renderer/components/automations/RulesTab.tsx @@ -34,7 +34,7 @@ import { CtoInfoChip, EmptyStateHint } from "./EmptyStateHint"; const DEFAULT_MODEL_ID = getDefaultModelDescriptor("opencode")?.id ?? getDefaultModelDescriptor("claude")?.id - ?? "anthropic/claude-sonnet-4-6"; + ?? "anthropic/claude-sonnet-5"; function createBlankDraft(): AutomationRuleDraft { return { diff --git a/apps/desktop/src/renderer/components/automations/TemplatesTab.tsx b/apps/desktop/src/renderer/components/automations/TemplatesTab.tsx index 5632722d6..6bbe90721 100644 --- a/apps/desktop/src/renderer/components/automations/TemplatesTab.tsx +++ b/apps/desktop/src/renderer/components/automations/TemplatesTab.tsx @@ -16,12 +16,12 @@ import { TemplateCard, type AutomationTemplate } from "./components/TemplateCard type TemplateDefinition = AutomationTemplate & { draft: Omit }; const DEFAULT_MODEL = { - modelId: "anthropic/claude-sonnet-4-6" as const, + modelId: "anthropic/claude-sonnet-5" as const, thinkingLevel: "medium" as const, }; const DEFAULT_MODEL_HIGH = { - modelId: "anthropic/claude-sonnet-4-6" as const, + modelId: "anthropic/claude-sonnet-5" as const, thinkingLevel: "high" as const, }; diff --git a/apps/desktop/src/renderer/components/chat/AgentChatComposer.test.tsx b/apps/desktop/src/renderer/components/chat/AgentChatComposer.test.tsx index c4ccfd007..d7de3e811 100644 --- a/apps/desktop/src/renderer/components/chat/AgentChatComposer.test.tsx +++ b/apps/desktop/src/renderer/components/chat/AgentChatComposer.test.tsx @@ -500,8 +500,8 @@ describe("AgentChatComposer", () => { it("renders Claude mode dropdown without a Chat toggle", () => { renderComposer({ sessionProvider: "claude", - modelId: "anthropic/claude-sonnet-4-6", - availableModelIds: ["anthropic/claude-sonnet-4-6"], + modelId: "anthropic/claude-sonnet-5", + availableModelIds: ["anthropic/claude-sonnet-5"], }); expect(screen.queryByRole("button", { name: "Chat" })).toBeNull(); @@ -523,8 +523,8 @@ describe("AgentChatComposer", () => { const onClaudePermissionModeChange = vi.fn(); renderComposer({ sessionProvider: "claude", - modelId: "anthropic/claude-sonnet-4-6", - availableModelIds: ["anthropic/claude-sonnet-4-6"], + modelId: "anthropic/claude-sonnet-5", + availableModelIds: ["anthropic/claude-sonnet-5"], onInteractionModeChange, onClaudePermissionModeChange, }); @@ -541,8 +541,8 @@ describe("AgentChatComposer", () => { const onClaudePermissionModeChange = vi.fn(); renderComposer({ sessionProvider: "claude", - modelId: "anthropic/claude-sonnet-4-6", - availableModelIds: ["anthropic/claude-sonnet-4-6"], + modelId: "anthropic/claude-sonnet-5", + availableModelIds: ["anthropic/claude-sonnet-5"], onInteractionModeChange, onClaudePermissionModeChange, }); @@ -560,8 +560,8 @@ describe("AgentChatComposer", () => { const onClaudePermissionModeChange = vi.fn(); renderComposer({ sessionProvider: "claude", - modelId: "anthropic/claude-sonnet-4-6", - availableModelIds: ["anthropic/claude-sonnet-4-6"], + modelId: "anthropic/claude-sonnet-5", + availableModelIds: ["anthropic/claude-sonnet-5"], onClaudeModeChange, onInteractionModeChange, onClaudePermissionModeChange, @@ -967,8 +967,8 @@ describe("AgentChatComposer", () => { renderComposer({ turnActive: true, sessionProvider: "claude", - modelId: "anthropic/claude-sonnet-4-6", - availableModelIds: ["anthropic/claude-sonnet-4-6"], + modelId: "anthropic/claude-sonnet-5", + availableModelIds: ["anthropic/claude-sonnet-5"], }); expect((screen.getByLabelText("Upload file from disk") as HTMLButtonElement).disabled).toBe(false); @@ -1610,7 +1610,7 @@ describe("AgentChatComposer", () => { parallelLaunchStatus: "Creating child lanes…", parallelModelSlots: [ { modelId: "openai/gpt-5.4", reasoningEffort: "high" }, - { modelId: "anthropic/claude-sonnet-4-6", reasoningEffort: "medium" }, + { modelId: "anthropic/claude-sonnet-5", reasoningEffort: "medium" }, { modelId: "openai/gpt-5.4-mini", reasoningEffort: "low" }, ], }); diff --git a/apps/desktop/src/renderer/components/chat/AgentChatMessageList.test.tsx b/apps/desktop/src/renderer/components/chat/AgentChatMessageList.test.tsx index 477163bbc..b49e54d96 100644 --- a/apps/desktop/src/renderer/components/chat/AgentChatMessageList.test.tsx +++ b/apps/desktop/src/renderer/components/chat/AgentChatMessageList.test.tsx @@ -625,14 +625,14 @@ describe("AgentChatMessageList transcript rendering", () => { type: "done", turnId: "turn-1", status: "interrupted", - modelId: "anthropic/claude-sonnet-4-6", + modelId: "anthropic/claude-sonnet-5", }, }, ]); // The end-of-turn divider shows the model attribution (styled span) plus the // non-completed status for interrupted/failed turns. - expect(screen.getAllByText(/Claude Sonnet 4\.6/).length).toBeGreaterThan(0); + expect(screen.getAllByText(/Claude Sonnet 5/).length).toBeGreaterThan(0); expect(screen.getByText("interrupted")).toBeTruthy(); }); @@ -674,7 +674,7 @@ describe("AgentChatMessageList transcript rendering", () => { timestamp: "2026-03-17T10:00:00.000Z", event: { type: "error", - message: "Authentication failed for Claude Sonnet 4.6.", + message: "Authentication failed for Claude Sonnet 5.", detail: "API Error: 401 Invalid authentication credentials", errorInfo: { category: "agent_cli_auth", @@ -2158,13 +2158,13 @@ describe("AgentChatMessageList transcript rendering", () => { type: "done", turnId: "turn-claude-runtime", status: "failed", - model: "claude-haiku-4-5", + model: "claude-haiku-4-5-20260707", modelId: "anthropic/claude-haiku-4-5", }, }, ]); - expect(screen.getAllByText("Claude Haiku 4.5 (claude-haiku-4-5)").length).toBeGreaterThan(0); + expect(screen.getAllByText("Claude Haiku 4.5 (claude-haiku-4-5-20260707)").length).toBeGreaterThan(0); }); it("surfaces the latest turn task rollup and inline file changes", () => { @@ -2262,7 +2262,7 @@ describe("AgentChatMessageList transcript rendering", () => { type: "done", turnId: "turn-9", status: "interrupted", - modelId: "anthropic/claude-sonnet-4-6", + modelId: "anthropic/claude-sonnet-5", }, }, ]); @@ -2270,7 +2270,7 @@ describe("AgentChatMessageList transcript rendering", () => { expect(rendered.container.textContent).toMatch(/Task list/); expect(rendered.container.textContent).toMatch(/1\/1 complete/); // Model attribution surfaces on the end-of-turn divider for non-completed turns. - expect(screen.getAllByText(/Claude Sonnet 4\.6/).length).toBeGreaterThanOrEqual(1); + expect(screen.getAllByText(/Claude Sonnet 5/).length).toBeGreaterThanOrEqual(1); }); // "keeps reasoning blocks separated" and "keeps live thinking collapsed" diff --git a/apps/desktop/src/renderer/components/chat/AgentChatPane.test.tsx b/apps/desktop/src/renderer/components/chat/AgentChatPane.test.tsx index 763ae48ac..6d982d758 100644 --- a/apps/desktop/src/renderer/components/chat/AgentChatPane.test.tsx +++ b/apps/desktop/src/renderer/components/chat/AgentChatPane.test.tsx @@ -221,12 +221,12 @@ function seedRuntimeModelCatalog(): void { key: "default", label: "Claude", models: [{ - id: "anthropic/claude-sonnet-4-6", - runtimeModelId: "claude-sonnet-4-6", + id: "anthropic/claude-sonnet-5", + runtimeModelId: "claude-sonnet-5", provider: "claude", providerKey: "claude", groupKey: "claude", - displayName: "Claude Sonnet 4.6", + displayName: "Claude Sonnet 5", isDefault: true, isAvailable: true, }], @@ -544,7 +544,7 @@ function installAdeMocks(options?: { agentChat: { models: vi.fn().mockImplementation(async ({ provider }: { provider: string }) => { if (provider === "codex") return [{ id: "gpt-5.4" }]; - if (provider === "claude") return options?.includeClaudeModel ? [{ id: "anthropic/claude-sonnet-4-6" }] : []; + if (provider === "claude") return options?.includeClaudeModel ? [{ id: "anthropic/claude-sonnet-5" }] : []; if (provider === "cursor") return options?.cursorModels ?? []; if (provider === "opencode") return [{ id: "openai/gpt-5.4-mini" }]; return []; @@ -1388,8 +1388,8 @@ describe("AgentChatPane submit recovery", () => { it("resends the latest user message for the selected session after auth retry", async () => { const session = buildSession("session-1", { provider: "claude", - model: "claude-sonnet-4-6", - modelId: "anthropic/claude-sonnet-4-6", + model: "claude-sonnet-5", + modelId: "anthropic/claude-sonnet-5", status: "idle", }); const transcript = [ @@ -1456,7 +1456,7 @@ describe("AgentChatPane submit recovery", () => { detectedAuth: [ { type: "cli-subscription", cli: "claude", authenticated: true }, ], - availableModelIds: ["anthropic/claude-sonnet-4-6"], + availableModelIds: ["anthropic/claude-sonnet-5"], }); window.ade.ai.getStatus = getStatus as any; seedDrawerStore(); @@ -1504,8 +1504,8 @@ describe("AgentChatPane submit recovery", () => { it("rejects the auth retry when a turn becomes active before resend", async () => { const session = buildSession("session-1", { provider: "claude", - model: "claude-sonnet-4-6", - modelId: "anthropic/claude-sonnet-4-6", + model: "claude-sonnet-5", + modelId: "anthropic/claude-sonnet-5", status: "idle", }); const transcript = `${JSON.stringify({ @@ -1566,8 +1566,8 @@ describe("AgentChatPane submit recovery", () => { it("dispatches auth recovery once after a later successful turn", async () => { const session = buildSession("session-1", { provider: "claude", - model: "claude-sonnet-4-6", - modelId: "anthropic/claude-sonnet-4-6", + model: "claude-sonnet-5", + modelId: "anthropic/claude-sonnet-5", status: "idle", }); const transcript = [ @@ -1576,7 +1576,7 @@ describe("AgentChatPane submit recovery", () => { timestamp: "2026-03-24T05:57:45.700Z", event: { type: "error", - message: "Authentication failed for Claude Sonnet 4.6.", + message: "Authentication failed for Claude Sonnet 5.", turnId: "turn-1", errorInfo: { category: "agent_cli_auth", @@ -1630,11 +1630,11 @@ describe("AgentChatPane submit recovery", () => { seedRuntimeModelCatalog(); renderParallelDraftPane({ - availableModelIdsOverride: ["anthropic/claude-sonnet-4-6"], + availableModelIdsOverride: ["anthropic/claude-sonnet-5"], }); expect(await screen.findByText("Start a new conversation")).toBeTruthy(); - const includedModelLabel = getModelById("anthropic/claude-sonnet-4-6")?.displayName ?? "Claude Sonnet 4.6"; + const includedModelLabel = getModelById("anthropic/claude-sonnet-5")?.displayName ?? "Claude Sonnet 5"; const trigger = await screen.findByRole("button", { name: /^Select model/ }); fireEvent.pointerDown(trigger, { button: 0 }); fireEvent.click(trigger); @@ -1705,7 +1705,7 @@ describe("AgentChatPane submit recovery", () => { laneId="lane-1" initialSessionId={session.sessionId} initialSessionSummary={session} - availableModelIdsOverride={["anthropic/claude-sonnet-4-6"]} + availableModelIdsOverride={["anthropic/claude-sonnet-5"]} /> , ); @@ -1924,7 +1924,7 @@ describe("AgentChatPane submit recovery", () => { fireEvent.change(textbox, { target: { value: "Launch on the selected lane and model." } }); const modelTrigger = await screen.findByRole("button", { name: /^Select model/ }); - const claudeLabel = getModelById("anthropic/claude-sonnet-4-6")?.displayName ?? "Claude Sonnet 4.6"; + const claudeLabel = getModelById("anthropic/claude-sonnet-5")?.displayName ?? "Claude Sonnet 5"; fireEvent.pointerDown(modelTrigger, { button: 0 }); fireEvent.click(modelTrigger); fireEvent.click(await screen.findByRole("tab", { name: /^Anthropic$/i })); @@ -1950,7 +1950,7 @@ describe("AgentChatPane submit recovery", () => { expect(create).toHaveBeenCalledWith(expect.objectContaining({ laneId: "lane-2", provider: "claude", - modelId: "anthropic/claude-sonnet-4-6", + modelId: "anthropic/claude-sonnet-5", })); }); }); @@ -1969,14 +1969,14 @@ describe("AgentChatPane submit recovery", () => { }); renderParallelDraftPane({ - availableModelIdsOverride: ["anthropic/claude-sonnet-4-6"], + availableModelIdsOverride: ["anthropic/claude-sonnet-5"], }); const modelTrigger = await screen.findByRole("button", { name: /^Select model/ }); fireEvent.pointerDown(modelTrigger, { button: 0 }); fireEvent.click(modelTrigger); fireEvent.click(await screen.findByRole("tab", { name: /^Anthropic$/i })); - await clickEnabledModelOption(/Claude Sonnet 4\.6/i); + await clickEnabledModelOption(/Claude Sonnet 5/i); await waitFor(() => { expect(window.ade.agentChat.slashCommands).toHaveBeenCalledWith(expect.objectContaining({ @@ -2568,8 +2568,8 @@ describe("AgentChatPane submit recovery", () => { const session = buildSession("session-1", { status: "idle", provider: "claude", - model: "claude-sonnet-4-6", - modelId: "anthropic/claude-sonnet-4-6", + model: "claude-sonnet-5", + modelId: "anthropic/claude-sonnet-5", permissionMode: "default", interactionMode: "default", claudePermissionMode: "default", @@ -2757,8 +2757,8 @@ describe("AgentChatPane submit recovery", () => { const session = buildSession("session-1", { status: "idle", provider: "claude", - model: "claude-sonnet-4-6", - modelId: "anthropic/claude-sonnet-4-6", + model: "claude-sonnet-5", + modelId: "anthropic/claude-sonnet-5", permissionMode: "edit", interactionMode: "default", claudePermissionMode: "default", @@ -2807,8 +2807,8 @@ describe("AgentChatPane submit recovery", () => { const session = buildSession("session-1", { status: "idle", provider: "claude", - model: "claude-sonnet-4-6", - modelId: "anthropic/claude-sonnet-4-6", + model: "claude-sonnet-5", + modelId: "anthropic/claude-sonnet-5", permissionMode: "plan", interactionMode: "plan", // The Claude mode picker writes "plan" into claudePermissionMode too, so @@ -3058,8 +3058,8 @@ describe("AgentChatPane submit recovery", () => { it("shows the Claude login prompt when the loaded chat history contains auth notices", async () => { const session = buildSession("session-1", { provider: "claude", - model: "claude-sonnet-4-6", - modelId: "anthropic/claude-sonnet-4-6", + model: "claude-sonnet-5", + modelId: "anthropic/claude-sonnet-5", status: "idle", }); installAdeMocks({ @@ -3123,7 +3123,7 @@ describe("AgentChatPane submit recovery", () => { const trigger = await screen.findByRole("button", { name: /^Select model/ }); const currentLabel = getModelById(session.modelId ?? "")?.displayName ?? session.modelId ?? ""; - const nextLabel = getModelById("anthropic/claude-sonnet-4-6")?.displayName ?? "Claude Sonnet 4.6"; + const nextLabel = getModelById("anthropic/claude-sonnet-5")?.displayName ?? "Claude Sonnet 5"; const nextLabelPattern = new RegExp(escapeRegExp(nextLabel), "i"); expect(trigger.textContent ?? "").toContain(currentLabel); @@ -3135,7 +3135,7 @@ describe("AgentChatPane submit recovery", () => { await waitFor(() => { expect(updateSession).toHaveBeenCalledWith(expect.objectContaining({ sessionId: session.sessionId, - modelId: "anthropic/claude-sonnet-4-6", + modelId: "anthropic/claude-sonnet-5", })); }); expect(screen.getByRole("button", { name: /^Select model/ }).textContent ?? "").toContain(currentLabel); @@ -3144,8 +3144,8 @@ describe("AgentChatPane submit recovery", () => { const updatedSession: AgentChatSessionSummary = { ...session, provider: "claude", - model: "claude-sonnet-4-6", - modelId: "anthropic/claude-sonnet-4-6", + model: "claude-sonnet-5", + modelId: "anthropic/claude-sonnet-5", reasoningEffort: "medium", permissionMode: "default", interactionMode: "default", @@ -3160,7 +3160,7 @@ describe("AgentChatPane submit recovery", () => { await waitFor(() => { expect(warmupModel).toHaveBeenCalledWith({ sessionId: session.sessionId, - modelId: "anthropic/claude-sonnet-4-6", + modelId: "anthropic/claude-sonnet-5", }); }); }); @@ -3180,7 +3180,7 @@ describe("AgentChatPane submit recovery", () => { const trigger = await screen.findByRole("button", { name: /^Select model/ }); const currentLabel = getModelById(session.modelId ?? "")?.displayName ?? session.modelId ?? ""; - const nextLabel = getModelById("anthropic/claude-sonnet-4-6")?.displayName ?? "Claude Sonnet 4.6"; + const nextLabel = getModelById("anthropic/claude-sonnet-5")?.displayName ?? "Claude Sonnet 5"; const nextLabelPattern = new RegExp(escapeRegExp(nextLabel), "i"); expect(trigger.textContent ?? "").toContain(currentLabel); @@ -3192,7 +3192,7 @@ describe("AgentChatPane submit recovery", () => { await waitFor(() => { expect(updateSession).toHaveBeenCalledWith(expect.objectContaining({ sessionId: session.sessionId, - modelId: "anthropic/claude-sonnet-4-6", + modelId: "anthropic/claude-sonnet-5", })); }); await waitFor(() => { @@ -3413,7 +3413,7 @@ describe("AgentChatPane submit recovery", () => { session: buildCreatedSession("session-2", { provider: "claude", model: "sonnet", - modelId: "anthropic/claude-sonnet-4-6", + modelId: "anthropic/claude-sonnet-5", interactionMode: "plan", permissionMode: "plan", }), @@ -3430,7 +3430,7 @@ describe("AgentChatPane submit recovery", () => { const handoffMenu = handoffTextEl.parentElement!.parentElement!; expect(handoffMenu).toBeTruthy(); fireEvent.click(within(handoffMenu as HTMLElement).getByRole("button", { name: /^Select model/ })); - const claudeLabel = getModelById("anthropic/claude-sonnet-4-6")?.displayName ?? "Claude Sonnet 4.6"; + const claudeLabel = getModelById("anthropic/claude-sonnet-5")?.displayName ?? "Claude Sonnet 5"; fireEvent.click(await screen.findByRole("tab", { name: /^Anthropic$/i })); await clickEnabledModelOption(new RegExp(escapeRegExp(claudeLabel), "i")); expect(screen.getByText("Fork keeps the complete Claude transcript through the SDK. Brief sends a summary as the first message.")).toBeTruthy(); @@ -3443,7 +3443,7 @@ describe("AgentChatPane submit recovery", () => { await waitFor(() => { expect(handoff).toHaveBeenCalledWith(expect.objectContaining({ sourceSessionId: session.sessionId, - targetModelId: "anthropic/claude-sonnet-4-6", + targetModelId: "anthropic/claude-sonnet-5", mode: "brief", claudePermissionMode: "plan", permissionMode: "plan", @@ -3459,7 +3459,7 @@ describe("AgentChatPane submit recovery", () => { session: buildCreatedSession("session-2", { provider: "claude", model: "sonnet", - modelId: "anthropic/claude-sonnet-4-6", + modelId: "anthropic/claude-sonnet-5", }), usedFallbackSummary: false, }, @@ -3474,7 +3474,7 @@ describe("AgentChatPane submit recovery", () => { const handoffMenu = handoffTextEl2.parentElement!.parentElement!; expect(handoffMenu).toBeTruthy(); fireEvent.click(within(handoffMenu as HTMLElement).getByRole("button", { name: /^Select model/ })); - const claudeLabel = getModelById("anthropic/claude-sonnet-4-6")?.displayName ?? "Claude Sonnet 4.6"; + const claudeLabel = getModelById("anthropic/claude-sonnet-5")?.displayName ?? "Claude Sonnet 5"; fireEvent.click(await screen.findByRole("tab", { name: /^Anthropic$/i })); await clickEnabledModelOption(new RegExp(escapeRegExp(claudeLabel), "i")); fireEvent.click(await screen.findByRole("button", { name: "Fork full history" })); @@ -3482,7 +3482,7 @@ describe("AgentChatPane submit recovery", () => { await waitFor(() => { expect(handoff).toHaveBeenCalledWith(expect.objectContaining({ sourceSessionId: session.sessionId, - targetModelId: "anthropic/claude-sonnet-4-6", + targetModelId: "anthropic/claude-sonnet-5", mode: "fork", })); }); @@ -4021,7 +4021,7 @@ describe("AgentChatPane submit recovery", () => { renderAutoCreateDraftPane({ orchestratorEnabled: true }); const modelTrigger = await screen.findByRole("button", { name: /^Select model/ }); - const claudeLabel = getModelById("anthropic/claude-sonnet-4-6")?.displayName ?? "Claude Sonnet 4.6"; + const claudeLabel = getModelById("anthropic/claude-sonnet-5")?.displayName ?? "Claude Sonnet 5"; fireEvent.pointerDown(modelTrigger, { button: 0 }); fireEvent.click(modelTrigger); fireEvent.click(await screen.findByRole("tab", { name: /^Anthropic$/i })); @@ -4060,7 +4060,7 @@ describe("AgentChatPane submit recovery", () => { renderAutoCreateDraftPane({ orchestratorEnabled: true }); const modelTrigger = await screen.findByRole("button", { name: /^Select model/ }); - const claudeLabel = getModelById("anthropic/claude-sonnet-4-6")?.displayName ?? "Claude Sonnet 4.6"; + const claudeLabel = getModelById("anthropic/claude-sonnet-5")?.displayName ?? "Claude Sonnet 5"; fireEvent.pointerDown(modelTrigger, { button: 0 }); fireEvent.click(modelTrigger); fireEvent.click(await screen.findByRole("tab", { name: /^Anthropic$/i })); @@ -4094,7 +4094,7 @@ describe("AgentChatPane submit recovery", () => { renderAutoCreateDraftPane({ orchestratorEnabled: true }); const modelTrigger = await screen.findByRole("button", { name: /^Select model/ }); - const claudeLabel = getModelById("anthropic/claude-sonnet-4-6")?.displayName ?? "Claude Sonnet 4.6"; + const claudeLabel = getModelById("anthropic/claude-sonnet-5")?.displayName ?? "Claude Sonnet 5"; fireEvent.pointerDown(modelTrigger, { button: 0 }); fireEvent.click(modelTrigger); fireEvent.click(await screen.findByRole("tab", { name: /^Anthropic$/i })); @@ -5472,7 +5472,7 @@ describe("AgentChatPane submit recovery", () => { ].map(encodeURIComponent).join(":"); window.localStorage.setItem(launchConfigKey, JSON.stringify({ version: 1, - modelId: "anthropic/claude-sonnet-4-6", + modelId: "anthropic/claude-sonnet-5", reasoningEffort: null, fastMode: true, executionMode: "focused", @@ -5502,7 +5502,7 @@ describe("AgentChatPane submit recovery", () => { , ); - await screen.findByRole("button", { name: /current: Claude Sonnet 4\.6/i }); + await screen.findByRole("button", { name: /current: Claude Sonnet 5/i }); expect(screen.queryByRole("button", { name: /Fast mode/i })).toBeNull(); const textbox = await screen.findByRole("textbox"); @@ -6012,8 +6012,8 @@ describe("AgentChatPane submit recovery", () => { it("keeps the Claude login prompt pinned in compact grid tiles", async () => { const session = buildSession("grid-claude-auth", { provider: "claude", - model: "claude-sonnet-4-6", - modelId: "anthropic/claude-sonnet-4-6", + model: "claude-sonnet-5", + modelId: "anthropic/claude-sonnet-5", status: "idle", title: "Claude auth grid tile", }); @@ -6023,7 +6023,7 @@ describe("AgentChatPane submit recovery", () => { sequence: 1, event: { type: "error", - message: "Authentication failed for Claude Sonnet 4.6.", + message: "Authentication failed for Claude Sonnet 5.", turnId: "turn-grid-auth", errorInfo: { category: "agent_cli_auth", @@ -6325,7 +6325,7 @@ describe("AgentChatPane submit recovery", () => { renderParallelDraftPane({ availableModelIdsOverride: [ "openai/gpt-5.4", - "anthropic/claude-sonnet-4-6", + "anthropic/claude-sonnet-5", ], }); @@ -6340,7 +6340,7 @@ describe("AgentChatPane submit recovery", () => { fireEvent.click(screen.getAllByRole("button", { name: "Configure" })[1]!); const modelTrigger = await screen.findByRole("button", { name: /^Select model/ }); - const claudeLabel = getModelById("anthropic/claude-sonnet-4-6")?.displayName ?? "Claude Sonnet 4.6"; + const claudeLabel = getModelById("anthropic/claude-sonnet-5")?.displayName ?? "Claude Sonnet 5"; fireEvent.pointerDown(modelTrigger, { button: 0 }); fireEvent.click(modelTrigger); fireEvent.click(await screen.findByRole("tab", { name: /^Anthropic$/i })); @@ -6361,12 +6361,12 @@ describe("AgentChatPane submit recovery", () => { // Child lanes are created instantly with the deterministic base name… expect(createChild.mock.calls.map(([args]) => args.name)).toEqual([ "fix-login-bug-codex-gpt-5-4", - "fix-login-bug-claude-sonnet", + "fix-login-bug-claude-sonnet-5", ]); // …then renamed to the AI base name in the background. await waitFor(() => { expect(renameLane).toHaveBeenCalledWith({ laneId: "lane-child-1", name: "fix-login-codex-gpt-5-4" }); - expect(renameLane).toHaveBeenCalledWith({ laneId: "lane-child-2", name: "fix-login-claude-sonnet" }); + expect(renameLane).toHaveBeenCalledWith({ laneId: "lane-child-2", name: "fix-login-claude-sonnet-5" }); }); await waitFor(() => { @@ -6383,7 +6383,7 @@ describe("AgentChatPane submit recovery", () => { expect(create).toHaveBeenNthCalledWith(2, expect.objectContaining({ laneId: "lane-child-2", provider: "claude", - modelId: "anthropic/claude-sonnet-4-6", + modelId: "anthropic/claude-sonnet-5", })); expect(send).toHaveBeenNthCalledWith(1, expect.objectContaining({ sessionId: "session-lane-child-1", @@ -6501,7 +6501,7 @@ describe("AgentChatPane submit recovery", () => { renderParallelDraftPane({ availableModelIdsOverride: [ "openai/gpt-5.4", - "anthropic/claude-sonnet-4-6", + "anthropic/claude-sonnet-5", ], }); @@ -6516,7 +6516,7 @@ describe("AgentChatPane submit recovery", () => { fireEvent.click(screen.getAllByRole("button", { name: "Configure" })[1]!); fireEvent.click(await screen.findByRole("button", { name: /^Select model/ })); fireEvent.click(await screen.findByRole("tab", { name: /^Anthropic$/i })); - await clickEnabledModelOption(/Claude Sonnet 4\.6/i); + await clickEnabledModelOption(/Claude Sonnet 5/i); fireEvent.change(screen.getByRole("textbox"), { target: { value: "Fix the login bug" } }); fireEvent.click(await screen.findByRole("button", { name: /Send to lanes/i })); diff --git a/apps/desktop/src/renderer/components/chat/ModelPickerPendingInput.test.tsx b/apps/desktop/src/renderer/components/chat/ModelPickerPendingInput.test.tsx index d7d8c1ecb..993c41286 100644 --- a/apps/desktop/src/renderer/components/chat/ModelPickerPendingInput.test.tsx +++ b/apps/desktop/src/renderer/components/chat/ModelPickerPendingInput.test.tsx @@ -24,8 +24,8 @@ import type { vi.mock("../shared/ModelPicker/ModelPicker", () => ({ ModelPicker: ({ value, onChange }: { value: string; onChange: (id: string) => void }) => (
-
@@ -110,7 +110,7 @@ describe("PrResolverLaunchControls", () => { await user.click(screen.getByRole("button", { name: "Pick Sonnet" })); - expect(props.onModelChange).toHaveBeenCalledWith("anthropic/claude-sonnet-4-6"); + expect(props.onModelChange).toHaveBeenCalledWith("anthropic/claude-sonnet-5"); expect(props.onReasoningEffortChange).toHaveBeenCalledWith("medium"); expect(props.onPermissionModeChange).toHaveBeenCalledWith("default"); }); diff --git a/apps/desktop/src/renderer/components/prs/state/PrsContext.tsx b/apps/desktop/src/renderer/components/prs/state/PrsContext.tsx index ab2772898..4648a2e76 100644 --- a/apps/desktop/src/renderer/components/prs/state/PrsContext.tsx +++ b/apps/desktop/src/renderer/components/prs/state/PrsContext.tsx @@ -303,7 +303,7 @@ function readPersistedModel(): string { } catch { /* ignore */ } - return "anthropic/claude-sonnet-4-6"; + return "anthropic/claude-sonnet-5"; } function readPersistedReasoningLevel(): string { diff --git a/apps/desktop/src/renderer/components/settings/ChatAppearancePreview.tsx b/apps/desktop/src/renderer/components/settings/ChatAppearancePreview.tsx index e50e56047..3c06c7c20 100644 --- a/apps/desktop/src/renderer/components/settings/ChatAppearancePreview.tsx +++ b/apps/desktop/src/renderer/components/settings/ChatAppearancePreview.tsx @@ -104,7 +104,7 @@ const PREVIEW_USAGE_MODEL: Record< { Logo: React.ComponentType<{ size?: number; className?: string }>; label: string } > = { codex: { Logo: CodexLogo, label: "gpt-5.5" }, - claude: { Logo: ClaudeLogo, label: "Claude Opus 4.7" }, + claude: { Logo: ClaudeLogo, label: "Claude Sonnet 5" }, opencode: { Logo: OpenCodeLogo, label: "local · runtime" }, cursor: { Logo: CursorAgentLogo, label: "cursor · auto" }, droid: { Logo: DroidLogo, label: "droid · factory" }, diff --git a/apps/desktop/src/renderer/components/shared/ModelPicker/ModelPicker.test.tsx b/apps/desktop/src/renderer/components/shared/ModelPicker/ModelPicker.test.tsx index d12c07f09..ab42bc936 100644 --- a/apps/desktop/src/renderer/components/shared/ModelPicker/ModelPicker.test.tsx +++ b/apps/desktop/src/renderer/components/shared/ModelPicker/ModelPicker.test.tsx @@ -160,9 +160,9 @@ import { import { resetRuntimeCatalogDescriptorCacheForTests } from "./modelCatalog"; const SONNET: ModelDescriptor = { - id: "anthropic/claude-sonnet-4-6", + id: "anthropic/claude-sonnet-5", shortId: "sonnet", - displayName: "Claude Sonnet 4.6", + displayName: "Claude Sonnet 5", family: "anthropic", authTypes: ["cli-subscription"], contextWindow: 200_000, @@ -211,9 +211,9 @@ const GPT: ModelDescriptor = { }; const OPENCODE_MODEL: ModelDescriptor = { - id: "opencode/anthropic/claude-sonnet-4-6", - shortId: "claude-sonnet-4-6", - displayName: "Claude Sonnet 4.6 via OpenCode", + id: "opencode/anthropic/claude-sonnet-5", + shortId: "claude-sonnet-5", + displayName: "Claude Sonnet 5 via OpenCode", family: "opencode", authTypes: ["api-key"], contextWindow: 200_000, @@ -222,9 +222,9 @@ const OPENCODE_MODEL: ModelDescriptor = { reasoningTiers: ["low", "medium", "high"], color: "#D97706", providerRoute: "opencode", - providerModelId: "anthropic/claude-sonnet-4-6", + providerModelId: "anthropic/claude-sonnet-5", openCodeProviderId: "anthropic", - openCodeModelId: "claude-sonnet-4-6", + openCodeModelId: "claude-sonnet-5", isCliWrapped: false, }; @@ -897,7 +897,7 @@ describe("ModelPicker", () => { models: [ { id: OPENCODE_MODEL.id, - runtimeModelId: "claude-sonnet-4-6", + runtimeModelId: "claude-sonnet-5", provider: "opencode" as const, providerKey: "opencode", groupKey: "opencode" as const, diff --git a/apps/desktop/src/renderer/components/shared/ModelPicker/ReasoningEffortPicker.test.tsx b/apps/desktop/src/renderer/components/shared/ModelPicker/ReasoningEffortPicker.test.tsx index 7cb140e30..d58c80505 100644 --- a/apps/desktop/src/renderer/components/shared/ModelPicker/ReasoningEffortPicker.test.tsx +++ b/apps/desktop/src/renderer/components/shared/ModelPicker/ReasoningEffortPicker.test.tsx @@ -59,7 +59,7 @@ import { } from "./modelCatalog"; import type { AgentChatModelCatalog } from "../../../../shared/types"; -const ANTHROPIC_MODEL_ID = "anthropic/claude-sonnet-4-6"; +const ANTHROPIC_MODEL_ID = "anthropic/claude-sonnet-5"; const OPENCODE_MODEL_ID = "opencode/some-model-without-reasoning"; beforeEach(() => { diff --git a/apps/desktop/src/renderer/components/shared/ModelPicker/modelCatalog.test.ts b/apps/desktop/src/renderer/components/shared/ModelPicker/modelCatalog.test.ts index e5d3fbfb0..b90611aa6 100644 --- a/apps/desktop/src/renderer/components/shared/ModelPicker/modelCatalog.test.ts +++ b/apps/desktop/src/renderer/components/shared/ModelPicker/modelCatalog.test.ts @@ -14,7 +14,7 @@ describe("mergeSelectorModels", () => { it("re-buckets OpenCode-routed models into the 'opencode' family so they appear under one rail", () => { const ids = [ - "opencode/anthropic/claude-sonnet-4-6", + "opencode/anthropic/claude-sonnet-5", "opencode/openai/gpt-5.4", ]; const merged = mergeSelectorModels(ids, undefined, undefined, "available-only"); @@ -26,17 +26,17 @@ describe("mergeSelectorModels", () => { }); it("keeps openCodeProviderId intact so rows can render per-sub-provider logos", () => { - const ids = ["opencode/anthropic/claude-sonnet-4-6"]; + const ids = ["opencode/anthropic/claude-sonnet-5"]; const merged = mergeSelectorModels(ids, undefined, undefined, "available-only"); - const model = merged.find((m) => m.id === "opencode/anthropic/claude-sonnet-4-6"); + const model = merged.find((m) => m.id === "opencode/anthropic/claude-sonnet-5"); expect(model).toBeDefined(); expect(model?.openCodeProviderId).toBe("anthropic"); }); it("does not change the family of non-OpenCode models", () => { - const ids = ["anthropic/claude-sonnet-4-6"]; + const ids = ["anthropic/claude-sonnet-5"]; const merged = mergeSelectorModels(ids, undefined, undefined, "available-only"); - const model = merged.find((m) => m.id === "anthropic/claude-sonnet-4-6"); + const model = merged.find((m) => m.id === "anthropic/claude-sonnet-5"); expect(model).toBeDefined(); expect(model?.family).toBe("anthropic"); }); @@ -141,7 +141,7 @@ describe("mergeSelectorModels", () => { provider: "cursor", providerKey: "cursor", groupKey: "cursor", - displayName: "Claude Sonnet 4.6", + displayName: "Claude Sonnet 5", isDefault: false, isAvailable: true, supportsReasoning: true, diff --git a/apps/desktop/src/renderer/components/shared/ModelPicker/modelOrdering.test.ts b/apps/desktop/src/renderer/components/shared/ModelPicker/modelOrdering.test.ts index 0e377620b..e4e583dec 100644 --- a/apps/desktop/src/renderer/components/shared/ModelPicker/modelOrdering.test.ts +++ b/apps/desktop/src/renderer/components/shared/ModelPicker/modelOrdering.test.ts @@ -7,12 +7,12 @@ describe("sortModelItems", () => { const items = [ { modelId: "anthropic/claude-opus-4-8", label: "opus" }, { modelId: "openai/gpt-5", label: "gpt" }, - { modelId: "anthropic/claude-sonnet-4-6", label: "sonnet" }, + { modelId: "anthropic/claude-sonnet-5", label: "sonnet" }, ]; expect(sortModelItems(items).map((i) => i.modelId)).toEqual([ "anthropic/claude-opus-4-8", "openai/gpt-5", - "anthropic/claude-sonnet-4-6", + "anthropic/claude-sonnet-5", ]); }); @@ -20,7 +20,7 @@ describe("sortModelItems", () => { const items = [ { modelId: "openai/gpt-5" }, { modelId: "anthropic/claude-opus-4-8" }, - { modelId: "anthropic/claude-sonnet-4-6" }, + { modelId: "anthropic/claude-sonnet-5" }, ]; const sorted = sortModelItems(items, { favoriteModelIds: ["anthropic/claude-opus-4-8"], @@ -29,7 +29,7 @@ describe("sortModelItems", () => { expect(sorted.map((i) => i.modelId)).toEqual([ "anthropic/claude-opus-4-8", "openai/gpt-5", - "anthropic/claude-sonnet-4-6", + "anthropic/claude-sonnet-5", ]); }); diff --git a/apps/desktop/src/renderer/components/shared/ModelPicker/modelPickerSearch.test.ts b/apps/desktop/src/renderer/components/shared/ModelPicker/modelPickerSearch.test.ts index eddaff0bf..576588ac2 100644 --- a/apps/desktop/src/renderer/components/shared/ModelPicker/modelPickerSearch.test.ts +++ b/apps/desktop/src/renderer/components/shared/ModelPicker/modelPickerSearch.test.ts @@ -74,7 +74,7 @@ describe("scoreModelPickerSearch", () => { { family: "anthropic", providerDisplayName: "Claude", - name: "Claude Opus 4.7", + name: "Claude Opus 4.8 1M", isFavorite: true, }, "opu", @@ -83,7 +83,7 @@ describe("scoreModelPickerSearch", () => { { family: "cursor", providerDisplayName: "Cursor", - name: "Opus 4.5", + name: "Claude Opus 4.5", }, "opu", ); diff --git a/apps/desktop/src/renderer/components/shared/providerModelSelectorGrouping.test.ts b/apps/desktop/src/renderer/components/shared/providerModelSelectorGrouping.test.ts index 6e605c4c3..4f77187d3 100644 --- a/apps/desktop/src/renderer/components/shared/providerModelSelectorGrouping.test.ts +++ b/apps/desktop/src/renderer/components/shared/providerModelSelectorGrouping.test.ts @@ -4,8 +4,8 @@ import { buildProviderGroupBlocks, createModelOrderMap } from "./providerModelSe describe("providerModelSelectorGrouping", () => { it("splits Droid custom models from Factory-provided family buckets", () => { - const custom = createDynamicDroidCliModelDescriptor("custom:claude-sonnet-4-6-thinking-32000"); - const factoryProvided = createDynamicDroidCliModelDescriptor("claude-sonnet-4-6"); + const custom = createDynamicDroidCliModelDescriptor("custom:claude-sonnet-5-thinking-32000"); + const factoryProvided = createDynamicDroidCliModelDescriptor("claude-sonnet-5"); const droidGroup = buildProviderGroupBlocks([custom, factoryProvided], createModelOrderMap()) .find((group) => group.key === "droid"); @@ -15,9 +15,9 @@ describe("providerModelSelectorGrouping", () => { "Anthropic (Droid)", "Custom models", ]); - expect(provider?.subsections[0]?.models.map((model) => model.id)).toEqual(["droid/claude-sonnet-4-6"]); + expect(provider?.subsections[0]?.models.map((model) => model.id)).toEqual(["droid/claude-sonnet-5"]); expect(provider?.subsections[1]?.models.map((model) => model.id)).toEqual([ - "droid/custom:claude-sonnet-4-6-thinking-32000", + "droid/custom:claude-sonnet-5-thinking-32000", ]); }); }); diff --git a/apps/desktop/src/renderer/components/terminals/WorkViewArea.test.tsx b/apps/desktop/src/renderer/components/terminals/WorkViewArea.test.tsx index 5ba4d5aa2..df24f61da 100644 --- a/apps/desktop/src/renderer/components/terminals/WorkViewArea.test.tsx +++ b/apps/desktop/src/renderer/components/terminals/WorkViewArea.test.tsx @@ -183,7 +183,7 @@ beforeEach(() => { ]); } return Promise.resolve([ - { id: "sonnet", displayName: "Claude Sonnet 4.6", isDefault: true }, + { id: "sonnet", displayName: "Claude Sonnet 5", isDefault: true }, { id: "haiku", displayName: "Claude Haiku 4.5", isDefault: false }, ]); }); diff --git a/apps/desktop/src/renderer/components/terminals/cliLaunch.test.ts b/apps/desktop/src/renderer/components/terminals/cliLaunch.test.ts index 62e8b5b13..ab79af3c6 100644 --- a/apps/desktop/src/renderer/components/terminals/cliLaunch.test.ts +++ b/apps/desktop/src/renderer/components/terminals/cliLaunch.test.ts @@ -268,14 +268,14 @@ describe("buildTrackedCliStartupCommand", () => { expect(launch.args).toEqual(expect.arrayContaining([ "--model", - "fable", + "claude-fable-5", "--effort", "xhigh", "--settings", JSON.stringify({ fastMode: true, ultracode: true }), ])); expect(launch.args).not.toEqual(expect.arrayContaining(["--effort", "ultracode"])); - expect(launch.startupCommand).toContain("--model fable"); + expect(launch.startupCommand).toContain("--model claude-fable-5"); expect(launch.startupCommand).toContain("--effort xhigh"); expect(launch.startupCommand).toContain("ultracode"); }); @@ -525,7 +525,7 @@ describe("buildTrackedCliStartupCommand", () => { const launch = buildTrackedCliLaunchCommand({ provider: "droid", permissionMode: "edit", - model: "droid/claude-sonnet-4-6", + model: "droid/claude-sonnet-5", reasoningEffort: "high", initialPrompt: "Run the Droid path.", }); @@ -534,7 +534,7 @@ describe("buildTrackedCliStartupCommand", () => { expect(launch.startupCommand).toContain("droid --settings \"$ADE_DROID_SETTINGS\""); expect(launch.startupCommand).toContain("Run the Droid path."); expect(launch.startupCommand).toContain("ADE session guidance"); - expect(launch.startupCommand).toContain("\\\"model\\\":\\\"claude-sonnet-4-6\\\""); + expect(launch.startupCommand).toContain("\\\"model\\\":\\\"claude-sonnet-5\\\""); expect(launch.startupCommand).toContain("\\\"reasoningEffort\\\":\\\"high\\\""); expect(launch.startupCommand).toContain("\\\"autonomyLevel\\\":\\\"low\\\""); expect(launch.env?.[ADE_AGENT_SKILLS_DIRS_ENV]).toContain("agent-skills"); @@ -545,7 +545,7 @@ describe("buildTrackedCliStartupCommand", () => { const launch = buildTrackedCliLaunchCommand({ provider: "droid", permissionMode: "edit", - model: "droid/claude-sonnet-4-6", + model: "droid/claude-sonnet-5", reasoningEffort: "high", initialPrompt: "Run the Droid path.", }); @@ -554,7 +554,7 @@ describe("buildTrackedCliStartupCommand", () => { expect(launch.startupCommand).toContain("$env:ADE_DROID_SETTINGS"); expect(launch.startupCommand).toContain("& 'droid' '--settings' $env:ADE_DROID_SETTINGS"); expect(launch.startupCommand).toContain("Run the Droid path."); - expect(launch.startupCommand).toContain("\"model\":\"claude-sonnet-4-6\""); + expect(launch.startupCommand).toContain("\"model\":\"claude-sonnet-5\""); }); }); @@ -736,7 +736,7 @@ describe("tracked CLI resume helpers", () => { targetId: "claude-session-1", launch: { permissionMode: "default" }, }, { model: "anthropic/claude-haiku-4-5", reasoningEffort: "low", permissionMode: "auto" })).toBe( - "claude --permission-mode auto --model haiku --effort low --resume claude-session-1", + "claude --permission-mode auto --model claude-haiku-4-5 --effort low --resume claude-session-1", ); expect(buildTrackedCliResumeCommand({ @@ -767,7 +767,7 @@ describe("tracked CLI resume helpers", () => { reasoningEffort: "ultracode", }, })).toBe( - "claude --permission-mode default --model fable --effort xhigh --settings \"{\\\"ultracode\\\":true}\" --resume claude-session-1", + "claude --permission-mode default --model claude-fable-5 --effort xhigh --settings \"{\\\"ultracode\\\":true}\" --resume claude-session-1", ); expect(buildTrackedCliResumeCommand({ diff --git a/apps/desktop/src/renderer/lib/claudeAuthPrompt.test.ts b/apps/desktop/src/renderer/lib/claudeAuthPrompt.test.ts index ab0b42d8b..4db70593d 100644 --- a/apps/desktop/src/renderer/lib/claudeAuthPrompt.test.ts +++ b/apps/desktop/src/renderer/lib/claudeAuthPrompt.test.ts @@ -53,7 +53,7 @@ describe("claude auth prompt helpers", () => { it("detects ADE's own classified auth message despite reversed word order", () => { // "Authentication failed" precedes "Claude" here, so the claude-first // patterns miss it — the dedicated "failed for" pattern catches it. - expect(textHasClaudeAuthError("Authentication failed for Claude Sonnet 4.6. Check your API key in Settings.")).toBe(true); + expect(textHasClaudeAuthError("Authentication failed for Claude Sonnet 5. Check your API key in Settings.")).toBe(true); }); it("does not treat other service auth failures as Claude login failures", () => { diff --git a/apps/desktop/src/renderer/lib/modelOptions.test.ts b/apps/desktop/src/renderer/lib/modelOptions.test.ts index 46afc2f15..24a6489c7 100644 --- a/apps/desktop/src/renderer/lib/modelOptions.test.ts +++ b/apps/desktop/src/renderer/lib/modelOptions.test.ts @@ -44,7 +44,7 @@ describe("describeModelSource", () => { }); it("returns 'CLI subscription' for CLI-wrapped models", () => { - const descriptor = getModelById("anthropic/claude-sonnet-4-6"); + const descriptor = getModelById("anthropic/claude-sonnet-5"); expect(descriptor).toBeTruthy(); expect(descriptor!.isCliWrapped).toBe(true); expect(describeModelSource(descriptor!)).toBe("CLI subscription"); @@ -393,7 +393,7 @@ describe("deriveConfiguredModelOptions", () => { detectedAuth: [{ type: "cli-subscription", cli: "claude", authenticated: true }], }); const options = deriveConfiguredModelOptions(status); - const sonnet = options.find((o) => o.id === "anthropic/claude-sonnet-4-6"); + const sonnet = options.find((o) => o.id === "anthropic/claude-sonnet-5"); expect(sonnet).toBeTruthy(); expect(sonnet!.description).toBe("anthropic (CLI subscription)"); }); @@ -403,9 +403,9 @@ describe("deriveConfiguredModelOptions", () => { detectedAuth: [{ type: "cli-subscription", cli: "claude", authenticated: true }], }); const options = deriveConfiguredModelOptions(status); - const sonnet = options.find((o) => o.id === "anthropic/claude-sonnet-4-6"); + const sonnet = options.find((o) => o.id === "anthropic/claude-sonnet-5"); expect(sonnet).toBeTruthy(); - expect(sonnet!.label).toBe("Claude Sonnet 4.6"); + expect(sonnet!.label).toBe("Claude Sonnet 5"); }); it("preserves dynamic local model ids as options", () => { @@ -427,7 +427,7 @@ describe("deriveConfiguredModelOptions", () => { describe("includeSelectedModelOption", () => { const baseOptions = [ - { id: "anthropic/claude-sonnet-4-6", label: "Claude Sonnet 4.6", description: "anthropic (CLI subscription)" }, + { id: "anthropic/claude-sonnet-5", label: "Claude Sonnet 5", description: "anthropic (CLI subscription)" }, ]; it("returns options unchanged when selectedModelId is null", () => { @@ -451,7 +451,7 @@ describe("includeSelectedModelOption", () => { }); it("returns options unchanged when selectedModelId already exists", () => { - const result = includeSelectedModelOption(baseOptions, "anthropic/claude-sonnet-4-6"); + const result = includeSelectedModelOption(baseOptions, "anthropic/claude-sonnet-5"); expect(result).toBe(baseOptions); }); @@ -476,8 +476,8 @@ describe("includeSelectedModelOption", () => { }); it("works with an empty options array", () => { - const result = includeSelectedModelOption([], "anthropic/claude-sonnet-4-6"); + const result = includeSelectedModelOption([], "anthropic/claude-sonnet-5"); expect(result.length).toBe(1); - expect(result[0].id).toBe("anthropic/claude-sonnet-4-6"); + expect(result[0].id).toBe("anthropic/claude-sonnet-5"); }); }); diff --git a/apps/desktop/src/shared/chatModelSwitching.test.ts b/apps/desktop/src/shared/chatModelSwitching.test.ts index 6f41d479a..7e62088a1 100644 --- a/apps/desktop/src/shared/chatModelSwitching.test.ts +++ b/apps/desktop/src/shared/chatModelSwitching.test.ts @@ -6,15 +6,15 @@ describe("chatModelSwitching", () => { expect( filterChatModelIdsForSession({ availableModelIds: [ - "anthropic/claude-sonnet-4-6", + "anthropic/claude-sonnet-5", "openai/gpt-5.4", "openai/gpt-5.3-codex", ], - activeSessionModelId: "anthropic/claude-sonnet-4-6", + activeSessionModelId: "anthropic/claude-sonnet-5", hasConversation: true, }), ).toEqual([ - "anthropic/claude-sonnet-4-6", + "anthropic/claude-sonnet-5", "openai/gpt-5.4", "openai/gpt-5.3-codex", ]); @@ -23,13 +23,13 @@ describe("chatModelSwitching", () => { it("keeps the active session model visible when it fell out of the catalog", () => { expect( filterChatModelIdsForSession({ - availableModelIds: ["anthropic/claude-sonnet-4-6", "openai/gpt-5.4"], + availableModelIds: ["anthropic/claude-sonnet-5", "openai/gpt-5.4"], activeSessionModelId: "openai/gpt-5.2", hasConversation: true, }), ).toEqual([ "openai/gpt-5.2", - "anthropic/claude-sonnet-4-6", + "anthropic/claude-sonnet-5", "openai/gpt-5.4", ]); }); @@ -37,11 +37,11 @@ describe("chatModelSwitching", () => { it("can leave the active session model out when a caller supplies a strict allowlist", () => { expect( filterChatModelIdsForSession({ - availableModelIds: ["anthropic/claude-sonnet-4-6"], + availableModelIds: ["anthropic/claude-sonnet-5"], activeSessionModelId: "openai/gpt-5.4", hasConversation: true, includeActiveSessionModel: false, }), - ).toEqual(["anthropic/claude-sonnet-4-6"]); + ).toEqual(["anthropic/claude-sonnet-5"]); }); }); diff --git a/apps/desktop/src/shared/claudeCliModels.ts b/apps/desktop/src/shared/claudeCliModels.ts index 22edb612d..8a61363d0 100644 --- a/apps/desktop/src/shared/claudeCliModels.ts +++ b/apps/desktop/src/shared/claudeCliModels.ts @@ -1,11 +1,16 @@ -export type ClaudeCliModelAlias = "fable" | "opus" | "opus[1m]" | "claude-opus-4-8" | "sonnet" | "haiku"; +export type ClaudeCliModelAlias = + | "claude-fable-5" + | "claude-opus-4-8" + | "claude-sonnet-5" + | "claude-haiku-4-5" + | "claude-opus-4-7[1m]"; export const CLAUDE_CLI_MODEL_ALIAS_MAP: Readonly> = { - fable: "fable", - "claude-fable-5": "fable", - "anthropic/claude-fable-5": "fable", - "anthropic/claude-fable-5-api": "fable", - opus: "opus", + fable: "claude-fable-5", + "claude-fable-5": "claude-fable-5", + "anthropic/claude-fable-5": "claude-fable-5", + "anthropic/claude-fable-5-api": "claude-fable-5", + opus: "claude-opus-4-8", "opus-4.8": "claude-opus-4-8", "opus-4-8": "claude-opus-4-8", "opus-4.8-1m": "claude-opus-4-8", @@ -17,30 +22,34 @@ export const CLAUDE_CLI_MODEL_ALIAS_MAP: Readonly { describe("findModel", () => { it("returns a ModelEntry for a known model", () => { - const entry = findModel("anthropic/claude-sonnet-4-6"); + const entry = findModel("anthropic/claude-sonnet-5"); expect(entry).toBeDefined(); expect(entry!.provider).toBe("claude"); expect(entry!.displayName).toContain("Sonnet"); @@ -148,7 +148,7 @@ describe("thinking levels", () => { describe("modelConfigToServiceModel", () => { it("returns the modelId when it is a non-empty string", () => { - expect(modelConfigToServiceModel({ modelId: "anthropic/claude-opus-4-7" })).toBe("anthropic/claude-opus-4-7"); + expect(modelConfigToServiceModel({ modelId: "anthropic/claude-opus-4-8" })).toBe("anthropic/claude-opus-4-8"); }); it("falls back to default codex model when modelId is empty and provider is codex", () => { @@ -156,16 +156,16 @@ describe("modelConfigToServiceModel", () => { expect(result).toMatch(/^openai\//); }); - it("falls back to claude-sonnet-4-6 when modelId is empty and provider is claude", () => { - expect(modelConfigToServiceModel({ provider: "claude", modelId: "" })).toBe("anthropic/claude-sonnet-4-6"); + it("falls back to the default Claude model when modelId is empty and provider is claude", () => { + expect(modelConfigToServiceModel({ provider: "claude", modelId: "" })).toBe("anthropic/claude-fable-5"); }); - it("falls back to claude-sonnet-4-6 when modelId is whitespace only", () => { - expect(modelConfigToServiceModel({ provider: "claude", modelId: " " })).toBe("anthropic/claude-sonnet-4-6"); + it("falls back to the default Claude model when modelId is whitespace only", () => { + expect(modelConfigToServiceModel({ provider: "claude", modelId: " " })).toBe("anthropic/claude-fable-5"); }); - it("falls back to claude-sonnet-4-6 when provider is unknown and modelId is empty", () => { - expect(modelConfigToServiceModel({ provider: "unknown", modelId: "" })).toBe("anthropic/claude-sonnet-4-6"); + it("falls back to the default Claude model when provider is unknown and modelId is empty", () => { + expect(modelConfigToServiceModel({ provider: "unknown", modelId: "" })).toBe("anthropic/claude-fable-5"); }); }); @@ -221,7 +221,7 @@ describe("thinkingLevelToReasoningEffort", () => { describe("MODEL_PRICING", () => { it("returns pricing for a known providerModelId via proxy get", () => { - const pricing = MODEL_PRICING["sonnet"]; + const pricing = MODEL_PRICING["claude-sonnet-5"]; expect(pricing).toBeDefined(); expect(typeof pricing.input).toBe("number"); expect(typeof pricing.output).toBe("number"); @@ -232,12 +232,12 @@ describe("MODEL_PRICING", () => { }); it("supports 'in' operator via has trap", () => { - expect("sonnet" in MODEL_PRICING).toBe(true); + expect("claude-sonnet-5" in MODEL_PRICING).toBe(true); expect("nonexistent-model-xyz" in MODEL_PRICING).toBe(false); }); it("getOwnPropertyDescriptor returns pricing for known models", () => { - const desc = Object.getOwnPropertyDescriptor(MODEL_PRICING, "sonnet"); + const desc = Object.getOwnPropertyDescriptor(MODEL_PRICING, "claude-sonnet-5"); expect(desc).toBeDefined(); expect(desc!.value).toEqual(expect.objectContaining({ input: expect.any(Number), output: expect.any(Number) })); }); diff --git a/apps/desktop/src/shared/modelProfiles.ts b/apps/desktop/src/shared/modelProfiles.ts index 271b81b6c..15af72474 100644 --- a/apps/desktop/src/shared/modelProfiles.ts +++ b/apps/desktop/src/shared/modelProfiles.ts @@ -43,7 +43,7 @@ function descriptorToEntry(d: ModelDescriptor, overrides?: { recommended?: boole }; } -const DEFAULT_CLAUDE_MODEL_ID = getDefaultModelDescriptor("claude")?.id ?? "anthropic/claude-sonnet-4-6"; +const DEFAULT_CLAUDE_MODEL_ID = getDefaultModelDescriptor("claude")?.id ?? "anthropic/claude-sonnet-5"; const DEFAULT_CODEX_MODEL_ID = getDefaultModelDescriptor("codex")?.id ?? "openai/gpt-5.5"; // CLI-wrapped Anthropic models (claude provider) @@ -110,7 +110,7 @@ export function modelConfigToServiceModel(config: ModelConfig): string { const modelId = config.modelId?.trim(); if (modelId && modelId.length > 0) return modelId; if (config.provider === "codex") return DEFAULT_CODEX_MODEL_ID; - return "anthropic/claude-sonnet-4-6"; + return DEFAULT_CLAUDE_MODEL_ID; } /** Convert ThinkingLevel to reasoning effort string for AI service */ diff --git a/apps/desktop/src/shared/modelRegistry.test.ts b/apps/desktop/src/shared/modelRegistry.test.ts index 493e71ccb..66f3b2406 100644 --- a/apps/desktop/src/shared/modelRegistry.test.ts +++ b/apps/desktop/src/shared/modelRegistry.test.ts @@ -41,6 +41,33 @@ describe("modelRegistry", () => { expect(d.openCodeModelId).toBe("openai/gpt-oss-20b"); }); + it("canonicalizes persisted OpenCode Anthropic aliases before launch", () => { + const sonnet = getModelById("opencode/anthropic/claude-sonnet-4-6"); + const opus = getModelById("opencode/anthropic/opus-4.6"); + + expect(sonnet).toMatchObject({ + id: "opencode/anthropic/claude-sonnet-5", + displayName: "Claude Sonnet 5", + providerModelId: "anthropic/claude-sonnet-5", + openCodeModelId: "claude-sonnet-5", + contextWindow: 1_000_000, + maxOutputTokens: 128_000, + capabilities: expect.objectContaining({ tools: true, vision: true, reasoning: true }), + reasoningTiers: ["low", "medium", "high", "max"], + }); + expect(opus).toMatchObject({ + id: "opencode/anthropic/claude-opus-4-8", + displayName: "Claude Opus 4.8 1M", + providerModelId: "anthropic/claude-opus-4-8", + openCodeModelId: "claude-opus-4-8", + contextWindow: 1_000_000, + maxOutputTokens: 128_000, + capabilities: expect.objectContaining({ tools: true, vision: true, reasoning: true }), + reasoningTiers: ["low", "medium", "high", "xhigh", "max", "ultracode"], + serviceTiers: ["fast"], + }); + }); + it("resolves runtime-discovered local model ids", () => { const descriptor = resolveModelDescriptor("ollama/qwen2.5-coder:32b"); expect(descriptor).toBeTruthy(); @@ -236,11 +263,12 @@ describe("modelRegistry", () => { }); describe("Claude descriptors", () => { - it("adds Fable 5 above Opus 4.8 in the Claude model registry", () => { - expect(MODEL_REGISTRY.filter((model) => model.family === "anthropic").slice(0, 4).map((model) => model.id)).toEqual([ + it("orders the Claude model registry for picker display", () => { + expect(MODEL_REGISTRY.filter((model) => model.family === "anthropic").slice(0, 5).map((model) => model.id)).toEqual([ "anthropic/claude-fable-5", "anthropic/claude-opus-4-8", - "anthropic/claude-opus-4-7", + "anthropic/claude-sonnet-5", + "anthropic/claude-haiku-4-5", "anthropic/claude-opus-4-7-1m", ]); const fable = getModelById("anthropic/claude-fable-5"); @@ -277,13 +305,21 @@ describe("modelRegistry", () => { expect(getDefaultModelDescriptor("claude")?.id).toBe("anthropic/claude-fable-5"); }); - it("keeps Opus 4.7 and Opus 4.7 1M as distinct selectable models", () => { - expect(getModelById("anthropic/claude-opus-4-7")).toMatchObject({ - displayName: "Claude Opus 4.7", - shortId: "opus", - providerModelId: "claude-opus-4-7", - contextWindow: 200_000, + it("uses the exact Claude Sonnet 5 runtime model id", () => { + const sonnet = getModelById("anthropic/claude-sonnet-5"); + expect(sonnet).toBeTruthy(); + expect(sonnet).toMatchObject({ + displayName: "Claude Sonnet 5", + shortId: "sonnet", + providerModelId: "claude-sonnet-5", + contextWindow: 1_000_000, + maxOutputTokens: 128_000, }); + expect(getRuntimeModelRefForDescriptor(sonnet!, "claude")).toBe("claude-sonnet-5"); + }); + + it("removes Opus 4.7 basic while keeping Opus 4.7 1M selectable", () => { + expect(MODEL_REGISTRY.some((model) => model.id === "anthropic/claude-opus-4-7")).toBe(false); expect(getModelById("anthropic/claude-opus-4-7-1m")).toMatchObject({ displayName: "Claude Opus 4.7 1M", shortId: "opus-1m", @@ -291,21 +327,24 @@ describe("modelRegistry", () => { contextWindow: 1_000_000, serviceTiers: ["fast"], }); - expect(resolveModelAlias("opus")?.id).toBe("anthropic/claude-opus-4-7"); + expect(resolveModelAlias("opus")?.id).toBe("anthropic/claude-opus-4-8"); expect(resolveModelAlias("opus[1m]")?.id).toBe("anthropic/claude-opus-4-7-1m"); - expect(resolveModelAlias("anthropic/claude-opus-4-7")?.id).toBe("anthropic/claude-opus-4-7"); + expect(resolveModelAlias("anthropic/claude-opus-4-6")?.id).toBe("anthropic/claude-opus-4-8"); + expect(resolveModelAlias("anthropic/claude-opus-4-7")?.id).toBe("anthropic/claude-opus-4-8"); + expect(resolveModelAlias("anthropic/claude-opus-4-6-1m")?.id).toBe("anthropic/claude-opus-4-7-1m"); expect(resolveModelAlias("anthropic/claude-opus-4-7-1m")?.id).toBe("anthropic/claude-opus-4-7-1m"); + expect(getModelById("claude-opus-4-6")?.id).toBe("anthropic/claude-opus-4-8"); + expect(getModelById("claude-opus-4-6[1m]")?.id).toBe("anthropic/claude-opus-4-7-1m"); }); - it("keeps claude-opus-4-6 ids as compatibility aliases to their existing 4.7 targets", () => { - expect(resolveModelAlias("anthropic/claude-opus-4-6")?.id).toBe("anthropic/claude-opus-4-7"); - expect(resolveModelAlias("anthropic/claude-opus-4-6-1m")?.id).toBe("anthropic/claude-opus-4-7-1m"); - expect(getModelById("anthropic/claude-opus-4-6")?.id).toBe("anthropic/claude-opus-4-7"); - expect(getModelById("anthropic/claude-opus-4-6-1m")?.id).toBe("anthropic/claude-opus-4-7-1m"); + it("maps removed Sonnet aliases forward without listing Sonnet 4.6 as a row", () => { + expect(MODEL_REGISTRY.some((model) => model.id === "anthropic/claude-sonnet-4-6")).toBe(false); + expect(resolveModelAlias("anthropic/claude-sonnet-4-6")?.id).toBe("anthropic/claude-sonnet-5"); + expect(getModelById("anthropic/claude-sonnet-4-6")?.id).toBe("anthropic/claude-sonnet-5"); }); it("does not advertise Claude Fast mode on non-Opus models", () => { - expect(getModelById("anthropic/claude-sonnet-4-6")?.serviceTiers).toBeUndefined(); + expect(getModelById("anthropic/claude-sonnet-5")?.serviceTiers).toBeUndefined(); expect(getModelById("anthropic/claude-haiku-4-5")?.serviceTiers).toBeUndefined(); }); }); @@ -347,21 +386,57 @@ describe("modelRegistry", () => { }); it("formats Droid custom thinking models with the expected display label", () => { - const descriptor = getModelById("droid/custom:claude-sonnet-4-6-thinking-32000"); + const descriptor = getModelById("droid/custom:claude-sonnet-5-thinking-32000"); expect(descriptor).toBeTruthy(); - expect(descriptor?.displayName).toBe("Claude Sonnet 4.6 (High)"); + expect(descriptor?.displayName).toBe("Claude Sonnet 5 (High)"); }); + it("canonicalizes persisted Droid Anthropic aliases before launch", () => { + const sonnet = getModelById("droid/claude-sonnet-4-6"); + const opus = getModelById("droid/opus-4-6"); + + expect(sonnet).toMatchObject({ + id: "droid/claude-sonnet-5", + providerModelId: "claude-sonnet-5", + displayName: "Sonnet 5 (1.2x)", + contextWindow: 1_000_000, + maxOutputTokens: 128_000, + reasoningTiers: ["low", "medium", "high", "max"], + }); + expect(opus).toMatchObject({ + id: "droid/claude-opus-4-8", + providerModelId: "claude-opus-4-8", + displayName: "Opus 4.8 1M", + contextWindow: 1_000_000, + maxOutputTokens: 128_000, + reasoningTiers: ["low", "medium", "high", "xhigh", "max", "ultracode"], + serviceTiers: ["fast"], + }); + expect(getModelById("droid/claude-opus-4-6-fast")).toMatchObject({ + id: "droid/claude-opus-4-8", + providerModelId: "claude-opus-4-8", + displayName: "Opus 4.8 1M", + }); + }); + + it("keeps Droid custom models in their own picker group", () => { - expect(droidCliLineGroupFromModelId("custom:claude-sonnet-4-6-thinking-32000")).toBe("custom"); + expect(droidCliLineGroupFromModelId("custom:claude-sonnet-5-thinking-32000")).toBe("custom"); expect(droidCliLineGroupFromModelId("custom:gpt-5.4(xhigh)")).toBe("custom"); expect(droidCliLineGroupLabel("custom")).toBe("Custom models"); }); it("uses compact Droid factory labels that match the CLI picker", () => { - const descriptor = getModelById("droid/claude-sonnet-4-6"); + const descriptor = getModelById("droid/claude-sonnet-5"); expect(descriptor).toBeTruthy(); - expect(descriptor?.displayName).toBe("Sonnet 4.6 (1.2x)"); + expect(descriptor?.displayName).toBe("Sonnet 5 (1.2x)"); + }); + + it("keeps Sonnet 5 registry pricing aligned with current intro pricing", () => { + expect(getModelById("anthropic/claude-sonnet-5")).toMatchObject({ + inputPricePer1M: 2, + outputPricePer1M: 10, + }); }); describe("ensureOpenCodeBaseURL", () => { diff --git a/apps/desktop/src/shared/modelRegistry.ts b/apps/desktop/src/shared/modelRegistry.ts index 4b42d3bfc..d91439f2b 100644 --- a/apps/desktop/src/shared/modelRegistry.ts +++ b/apps/desktop/src/shared/modelRegistry.ts @@ -256,6 +256,7 @@ export const MODEL_REGISTRY: ModelDescriptor[] = [ shortId: "opus-4.8-1m", aliases: [ "claude-opus-4-8", + "opus", "opus-4.8", "opus-4-8", "opus-4.8-1m", @@ -265,6 +266,16 @@ export const MODEL_REGISTRY: ModelDescriptor[] = [ "claude-opus-4-8[1m]", "anthropic/claude-opus-4-8-1m", "anthropic/claude-opus-4-8-api", + "opus-4.7", + "opus-4-7", + "opus-4.6", + "opus-4-6", + "claude-opus-4-6", + "claude-opus-4-7", + "anthropic/claude-opus-4-6", + "anthropic/claude-opus-4-6-api", + "anthropic/claude-opus-4-7", + "anthropic/claude-opus-4-7-api", ], displayName: "Claude Opus 4.8 1M", family: "anthropic", @@ -284,35 +295,66 @@ export const MODEL_REGISTRY: ModelDescriptor[] = [ costTier: "very_high", }, { - id: "anthropic/claude-opus-4-7", - shortId: "opus", - aliases: ["claude-opus-4-7", "anthropic/claude-opus-4-6", "claude-opus-4-6"], - displayName: "Claude Opus 4.7", + id: "anthropic/claude-sonnet-5", + shortId: "sonnet", + aliases: [ + "sonnet", + "sonnet-5", + "claude-sonnet-5", + "anthropic/claude-sonnet-5-api", + "sonnet-4-6", + "claude-sonnet-4-6", + "anthropic/claude-sonnet-4-6", + "anthropic/claude-sonnet-4-6-api", + ], + displayName: "Claude Sonnet 5", family: "anthropic", authTypes: ["cli-subscription"], - contextWindow: 200_000, + contextWindow: 1_000_000, maxOutputTokens: 128_000, capabilities: ALL_CAPS, - reasoningTiers: ["low", "medium", "high", "xhigh", "max"], - serviceTiers: ["fast"], - color: "#D97706", + reasoningTiers: ["low", "medium", "high", "max"], + color: "#8B5CF6", providerRoute: "claude-cli", - providerModelId: "claude-opus-4-7", + providerModelId: "claude-sonnet-5", cliCommand: "claude", isCliWrapped: true, - inputPricePer1M: 5, - outputPricePer1M: 25, - costTier: "very_high", + inputPricePer1M: 2, + outputPricePer1M: 10, + costTier: "medium", + }, + { + id: "anthropic/claude-haiku-4-5", + shortId: "haiku", + aliases: ["claude-haiku-4-5", "claude-haiku-4-5-20251001", "anthropic/claude-haiku-4-5-api"], + displayName: "Claude Haiku 4.5", + family: "anthropic", + authTypes: ["cli-subscription"], + contextWindow: 200_000, + maxOutputTokens: 64_000, + capabilities: NO_REASONING, + color: "#06B6D4", + providerRoute: "claude-cli", + providerModelId: "claude-haiku-4-5", + cliCommand: "claude", + isCliWrapped: true, + inputPricePer1M: 1, + outputPricePer1M: 5, + costTier: "low", }, { id: "anthropic/claude-opus-4-7-1m", shortId: "opus-1m", aliases: [ "opus[1m]", + "opus-1m", + "opus-4-6-1m", "claude-opus-4-7[1m]", - "anthropic/claude-opus-4-6-1m", - "claude-opus-4-6-1m", + "claude-opus-4-7-1m", "claude-opus-4-6[1m]", + "claude-opus-4-6-1m", + "anthropic/claude-opus-4-6-1m", + "anthropic/claude-opus-4-7-1m", ], displayName: "Claude Opus 4.7 1M", family: "anthropic", @@ -331,43 +373,6 @@ export const MODEL_REGISTRY: ModelDescriptor[] = [ outputPricePer1M: 25, costTier: "very_high", }, - { - id: "anthropic/claude-sonnet-4-6", - shortId: "sonnet", - displayName: "Claude Sonnet 4.6", - family: "anthropic", - authTypes: ["cli-subscription"], - contextWindow: 200_000, - maxOutputTokens: 32_000, - capabilities: ALL_CAPS, - reasoningTiers: ["low", "medium", "high", "max"], - color: "#8B5CF6", - providerRoute: "claude-cli", - providerModelId: "sonnet", - cliCommand: "claude", - isCliWrapped: true, - inputPricePer1M: 3, - outputPricePer1M: 15, - costTier: "medium", - }, - { - id: "anthropic/claude-haiku-4-5", - shortId: "haiku", - displayName: "Claude Haiku 4.5", - family: "anthropic", - authTypes: ["cli-subscription"], - contextWindow: 200_000, - maxOutputTokens: 32_000, - capabilities: NO_REASONING, - color: "#06B6D4", - providerRoute: "claude-cli", - providerModelId: "haiku", - cliCommand: "claude", - isCliWrapped: true, - inputPricePer1M: 1, - outputPricePer1M: 5, - costTier: "low", - }, // ---- OpenAI (CLI-wrapped via codex) ---- // ADE codex chat surfaces use real OpenAI model ids as the canonical @@ -705,6 +710,58 @@ export function decodeOpenCodeRegistryId(id: string): { openCodeProviderId: stri } } +function normalizeAnthropicRuntimeAlias(modelId: string): { + modelId: string; + displayName: string; + contextWindow: number; + maxOutputTokens: number; + capabilities: ModelCapabilities; + reasoningTiers?: string[]; + serviceTiers?: string[]; + wasAlias: boolean; +} | null { + const normalized = modelId.trim().toLowerCase(); + if (!normalized.length) return null; + if ( + normalized === "claude-sonnet-5" + || normalized === "claude-sonnet-4-6" + || normalized === "sonnet-4-6" + ) { + return { + modelId: "claude-sonnet-5", + displayName: "Claude Sonnet 5", + contextWindow: 1_000_000, + maxOutputTokens: 128_000, + capabilities: ALL_CAPS, + reasoningTiers: ["low", "medium", "high", "max"], + wasAlias: normalized !== "claude-sonnet-5", + }; + } + if ( + normalized === "claude-opus-4-8" + || normalized === "claude-opus-4-7" + || normalized === "opus-4-7" + || normalized === "opus-4.7" + || normalized === "claude-opus-4-6" + || normalized === "claude-opus-4-6-fast" + || normalized === "opus-4-6" + || normalized === "opus-4.6" + || normalized === "opus" + ) { + return { + modelId: "claude-opus-4-8", + displayName: "Claude Opus 4.8 1M", + contextWindow: 1_000_000, + maxOutputTokens: 128_000, + capabilities: ALL_CAPS, + reasoningTiers: ["low", "medium", "high", "xhigh", "max", "ultracode"], + serviceTiers: ["fast"], + wasAlias: normalized !== "claude-opus-4-8", + }; + } + return null; +} + function formatOpenCodeDisplayName(modelId: string): string { return modelId .split(/[-_/]+/g) @@ -759,21 +816,36 @@ export function createDynamicOpenCodeModelDescriptor( options?: DynamicOpenCodeModelDescriptorOptions, ): ModelDescriptor { const opPid = options?.openCodeProviderId?.trim(); - const opMid = options?.openCodeModelId?.trim(); + const rawOpMid = options?.openCodeModelId?.trim(); + const anthropicRuntime = opPid?.toLowerCase() === "anthropic" && rawOpMid + ? normalizeAnthropicRuntimeAlias(rawOpMid) + : null; + const opMid = anthropicRuntime?.modelId ?? rawOpMid; const normalizedModelId = modelId.trim(); const usesPairedIds = Boolean(opPid && opMid); const id = usesPairedIds ? encodeOpenCodeRegistryId(opPid!, opMid!) : `opencode/${normalizedModelId}`; const shortId = usesPairedIds ? opMid! : normalizedModelId; const providerModelId = usesPairedIds ? `${opPid}/${opMid}` : normalizedModelId; const displayName = - options?.displayName?.trim() + (anthropicRuntime?.wasAlias ? anthropicRuntime.displayName : undefined) + || options?.displayName?.trim() + || anthropicRuntime?.displayName || (usesPairedIds ? formatOpenCodeDisplayName(opMid!) : formatOpenCodeDisplayName(normalizedModelId)); + const canonicalCapabilities = anthropicRuntime && (anthropicRuntime.wasAlias || !options?.capabilities) + ? anthropicRuntime.capabilities + : undefined; const capabilities: ModelCapabilities = { - tools: options?.capabilities?.tools ?? true, - vision: options?.capabilities?.vision ?? false, - reasoning: options?.capabilities?.reasoning ?? true, - streaming: options?.capabilities?.streaming ?? true, + tools: canonicalCapabilities?.tools ?? options?.capabilities?.tools ?? true, + vision: canonicalCapabilities?.vision ?? options?.capabilities?.vision ?? false, + reasoning: canonicalCapabilities?.reasoning ?? options?.capabilities?.reasoning ?? true, + streaming: canonicalCapabilities?.streaming ?? options?.capabilities?.streaming ?? true, }; + const reasoningTiers = anthropicRuntime && (anthropicRuntime.wasAlias || !options?.reasoningTiers?.length) + ? anthropicRuntime.reasoningTiers + : options?.reasoningTiers; + const serviceTiers = anthropicRuntime && (anthropicRuntime.wasAlias || !options?.serviceTiers?.length) + ? anthropicRuntime.serviceTiers + : options?.serviceTiers; const aliases = options?.aliases?.map((alias) => alias.trim()).filter(Boolean) ?? []; const family: ProviderFamily = (opPid && OPENCODE_PROVIDER_FAMILY_MAP[opPid]) || "opencode"; const isLocal = opPid ? LOCAL_OPENCODE_PROVIDERS.has(opPid) : false; @@ -785,15 +857,15 @@ export function createDynamicOpenCodeModelDescriptor( displayName, family, authTypes, - contextWindow: options?.contextWindow ?? 200_000, - maxOutputTokens: options?.maxOutputTokens ?? 32_000, + contextWindow: options?.contextWindow ?? anthropicRuntime?.contextWindow ?? 200_000, + maxOutputTokens: options?.maxOutputTokens ?? anthropicRuntime?.maxOutputTokens ?? 32_000, capabilities, color, providerRoute: "opencode", providerModelId, ...(usesPairedIds ? { openCodeProviderId: opPid, openCodeModelId: opMid } : {}), - ...(options?.reasoningTiers?.length ? { reasoningTiers: [...options.reasoningTiers] } : {}), - ...(options?.serviceTiers?.length ? { serviceTiers: [...options.serviceTiers] } : {}), + ...(reasoningTiers?.length ? { reasoningTiers: [...reasoningTiers] } : {}), + ...(serviceTiers?.length ? { serviceTiers: [...serviceTiers] } : {}), ...(aliases.length ? { aliases } : {}), ...(isLocal || options?.harnessProfile ? { harnessProfile: options?.harnessProfile ?? "guarded" } : {}), isCliWrapped: false, @@ -1063,11 +1135,13 @@ function normalizeDroidEffortLabel(value: string): string { const KNOWN_DROID_COMPACT_DISPLAY_NAMES: Record = { "claude-fable-5": "Fable 5", + "claude-opus-4-8": "Opus 4.8 1M", "claude-opus-4-5-20251101": "Opus 4.5 (2x)", "claude-opus-4-6": "Opus 4.6 (2x)", "claude-opus-4-6-fast": "Opus 4.6 Fast Mode (12x)", "claude-sonnet-4-5-20250929": "Sonnet 4.5 (1.2x)", - "claude-sonnet-4-6": "Sonnet 4.6 (1.2x)", + "claude-sonnet-5": "Sonnet 5 (1.2x)", + "claude-sonnet-4-6": "Sonnet 5 (1.2x)", "claude-haiku-4-5-20251001": "Haiku 4.5 (0.4x)", "gpt-5.1": "GPT-5.1 (0.5x)", "gpt-5.2": "GPT-5.2 (0.7x)", @@ -1137,36 +1211,49 @@ export function createDynamicDroidCliModelDescriptor( customProxy?: boolean; reasoningTiers?: string[]; serviceTiers?: string[]; + contextWindow?: number; + maxOutputTokens?: number; capabilities?: Partial; }, ): ModelDescriptor { - const trimmedProviderModelId = providerModelId.trim(); + const rawProviderModelId = providerModelId.trim(); + const canonicalDroid = normalizeAnthropicRuntimeAlias(rawProviderModelId); + const trimmedProviderModelId = canonicalDroid?.modelId ?? rawProviderModelId; const id = `droid/${trimmedProviderModelId}`; const knownCompact = KNOWN_DROID_COMPACT_DISPLAY_NAMES[trimmedProviderModelId.toLowerCase()]; const display = - knownCompact + canonicalDroid?.wasAlias + ? knownCompact ?? canonicalDroid.displayName + : knownCompact ? knownCompact : typeof cliDisplayName === "string" && cliDisplayName.trim().length ? cliDisplayName.trim() : formatDroidCliFallbackDisplayName(trimmedProviderModelId); + const reasoningTiers = options?.reasoningTiers?.length + ? options.reasoningTiers + : canonicalDroid?.reasoningTiers; + const serviceTiers = options?.serviceTiers?.length + ? options.serviceTiers + : canonicalDroid?.serviceTiers; return { id, shortId: trimmedProviderModelId, displayName: display, family: "factory", authTypes: ["cli-subscription"], - contextWindow: 200_000, - maxOutputTokens: 32_000, + contextWindow: options?.contextWindow ?? canonicalDroid?.contextWindow ?? 200_000, + maxOutputTokens: options?.maxOutputTokens ?? canonicalDroid?.maxOutputTokens ?? 32_000, capabilities: { ...ALL_CAPS, + ...(canonicalDroid?.capabilities ?? {}), ...(options?.capabilities ?? {}), }, color: colorForDroidModelId(trimmedProviderModelId), providerRoute: "droid-cli", providerModelId: trimmedProviderModelId, cliCommand: "droid", - ...(options?.reasoningTiers?.length ? { reasoningTiers: [...options.reasoningTiers] } : {}), - ...(options?.serviceTiers?.length ? { serviceTiers: [...options.serviceTiers] } : {}), + ...(reasoningTiers?.length ? { reasoningTiers: [...reasoningTiers] } : {}), + ...(serviceTiers?.length ? { serviceTiers: [...serviceTiers] } : {}), isCliWrapped: true, ...(options?.customProxy ? { customProxy: true } : {}), }; @@ -1416,7 +1503,7 @@ export function getRuntimeModelRefForDescriptor( ): string { const provider = providerHint ?? resolveProviderGroupForModel(descriptor); if (provider === "claude") { - return descriptor.shortId; + return descriptor.providerModelId; } if (provider === "codex" || provider === "cursor" || provider === "droid") { return descriptor.providerModelId; @@ -1504,7 +1591,7 @@ function pickDefaultOpenCodeModel(models: ModelDescriptor[]): ModelDescriptor | return pickPreferredModel(models, [ (model) => model.family === "openai" && /\bgpt-5\.4\b/i.test(`${model.displayName} ${model.providerModelId}`), (model) => model.id === "opencode/anthropic/claude-fable-5" || (model.family === "anthropic" && /\bfable\b/i.test(`${model.displayName} ${model.providerModelId}`)), - (model) => model.id === "opencode/anthropic/claude-sonnet-4-6" || (model.family === "anthropic" && model.providerRoute === "opencode"), + (model) => model.id === "opencode/anthropic/claude-sonnet-5" || (model.family === "anthropic" && model.providerRoute === "opencode"), (model) => model.family === "anthropic" && /\bsonnet\b/i.test(model.displayName), (model) => model.family === "anthropic", (model) => model.family === "openai", @@ -1606,7 +1693,7 @@ export function enrichModelRegistry(enrichments: Map): // --------------------------------------------------------------------------- /** - * Get pricing for a model by its providerModelId (e.g. "claude-sonnet-4-6"). + * Get pricing for a model by its providerModelId (e.g. "claude-sonnet-5"). * Returns per-million-token pricing. Checks dynamic overrides first, * then falls back to the static pricing in MODEL_REGISTRY. */ diff --git a/apps/desktop/src/shared/types/config.ts b/apps/desktop/src/shared/types/config.ts index 979524268..958916290 100644 --- a/apps/desktop/src/shared/types/config.ts +++ b/apps/desktop/src/shared/types/config.ts @@ -1346,7 +1346,7 @@ export type AiConfig = { apiKeys?: Record; localProviders?: AiLocalProviderConfigs; workerSafety?: WorkerSafetyPolicy; - /** Per-feature model overrides, e.g. { pr_descriptions: "claude-sonnet-4-6" } */ + /** Per-feature model overrides, e.g. { pr_descriptions: "claude-sonnet-5" } */ featureModelOverrides?: Partial>; /** Per-feature reasoning effort overrides */ featureReasoningOverrides?: Partial>; diff --git a/apps/desktop/src/shared/types/core.ts b/apps/desktop/src/shared/types/core.ts index 7bcd11cd3..ecca0716d 100644 --- a/apps/desktop/src/shared/types/core.ts +++ b/apps/desktop/src/shared/types/core.ts @@ -423,7 +423,7 @@ export type ProjectIcon = { export type ProviderMode = "guest" | "subscription"; -/** Universal model identifier, e.g. "anthropic/claude-sonnet-4-6" or "openai/gpt-5.3-codex" */ +/** Universal model identifier, e.g. "anthropic/claude-sonnet-5" or "openai/gpt-5.3-codex" */ export type ModelId = string; // react-resizable-panels uses a map of panel id -> percentage (0..100) diff --git a/apps/desktop/src/shared/types/models.ts b/apps/desktop/src/shared/types/models.ts index f2984b549..4b4b63dbd 100644 --- a/apps/desktop/src/shared/types/models.ts +++ b/apps/desktop/src/shared/types/models.ts @@ -9,7 +9,7 @@ export type ThinkingLevel = "none" | "minimal" | "low" | "medium" | "high" | "ma export type ModelConfig = { /** Optional provider hint; routing is resolved from modelId via model registry. */ provider?: ModelProvider; - /** Canonical registry model id (for example: anthropic/claude-sonnet-4-6, openai/gpt-5.3-codex). */ + /** Canonical registry model id (for example: anthropic/claude-sonnet-5, openai/gpt-5.3-codex). */ modelId: string; thinkingLevel?: ThinkingLevel; }; diff --git a/apps/ios/ADE/Resources/VoiceGlossary.json b/apps/ios/ADE/Resources/VoiceGlossary.json index 68ac091f1..5ca494dd4 100644 --- a/apps/ios/ADE/Resources/VoiceGlossary.json +++ b/apps/ios/ADE/Resources/VoiceGlossary.json @@ -13,7 +13,7 @@ "Haiku", "Fable", "Claude Opus 4.8", - "Claude Sonnet 4.6", + "Claude Sonnet 5", "Claude Haiku 4.5", "Claude Fable 5", "GPT-5.5", diff --git a/apps/ios/ADE/Shared/ADESharedModels.swift b/apps/ios/ADE/Shared/ADESharedModels.swift index 7b131dcf1..7956b90e2 100644 --- a/apps/ios/ADE/Shared/ADESharedModels.swift +++ b/apps/ios/ADE/Shared/ADESharedModels.swift @@ -13,7 +13,7 @@ public struct AgentSnapshot: Codable, Hashable, Identifiable, Sendable { /// Provider slug: "claude", "codex", "cursor", "opencode", "google", /// "mistral", "deepseek", "xai", "groq". Keyed into `ADESharedTheme`. public let provider: String - /// Specific model id (e.g. "claude-sonnet-4-6"). Rendered as the LA + /// Specific model id (e.g. "claude-sonnet-5"). Rendered as the LA /// subtitle in place of the generic provider slug when present. public let modelId: String? /// Lane name for this session ("Primary", "feature/x"). Used by the diff --git a/apps/ios/ADE/Views/Components/ADEDesignSystem.swift b/apps/ios/ADE/Views/Components/ADEDesignSystem.swift index 8401cdc69..3de5d0dcb 100644 --- a/apps/ios/ADE/Views/Components/ADEDesignSystem.swift +++ b/apps/ios/ADE/Views/Components/ADEDesignSystem.swift @@ -114,7 +114,7 @@ enum ADEColor { /// Per-model brand colors mirroring desktop's MODEL_REGISTRY entries. /// Keep in sync with apps/desktop/src/shared/modelRegistry.ts. - /// Keys cover both the registry id ("anthropic/claude-opus-4-7") and shortId ("opus"). + /// Keys cover both the registry id ("anthropic/claude-sonnet-5") and shortId ("sonnet"). private static let modelColors: [String: UInt32] = [ // Anthropic "anthropic/claude-fable-5": 0xD97706, @@ -123,15 +123,13 @@ enum ADEColor { "fable": 0xD97706, "anthropic/claude-opus-4-8": 0xD97706, "claude-opus-4-8": 0xD97706, - "anthropic/claude-opus-4-7": 0xD97706, - "claude-opus-4-7": 0xD97706, - "opus": 0xD97706, "anthropic/claude-opus-4-7-1m": 0xB45309, "claude-opus-4-7-1m": 0xB45309, "opus-1m": 0xB45309, "opus[1m]": 0xB45309, "claude-opus-4-7[1m]": 0xB45309, - "anthropic/claude-sonnet-4-6": 0x8B5CF6, + "anthropic/claude-sonnet-5": 0x8B5CF6, + "claude-sonnet-5": 0x8B5CF6, "sonnet": 0x8B5CF6, "anthropic/claude-haiku-4-5": 0x06B6D4, "haiku": 0x06B6D4, @@ -189,11 +187,16 @@ enum ADEColor { append("anthropic/claude-opus-4-8") append("claude-opus-4-8") case "opus", "anthropic/claude-opus-4-7", "claude-opus-4-7": - append("anthropic/claude-opus-4-7") - append("opus") + append("anthropic/claude-opus-4-8") + append("claude-opus-4-8") case "opus[1m]", "opus-1m", "anthropic/claude-opus-4-7-1m", "claude-opus-4-7-1m", "claude-opus-4-7[1m]": append("anthropic/claude-opus-4-7-1m") append("opus-1m") + case "sonnet", "anthropic/claude-sonnet-5", "claude-sonnet-5", + "anthropic/claude-sonnet-4-6", "claude-sonnet-4-6": + append("anthropic/claude-sonnet-5") + append("claude-sonnet-5") + append("sonnet") default: break } @@ -231,7 +234,7 @@ enum ADEColor { /// Per-model reasoning tiers mirroring desktop's `reasoningTiers` field in /// `apps/desktop/src/shared/modelRegistry.ts`. Keys cover both the registry - /// id ("anthropic/claude-opus-4-7") and shortId ("opus") so lookups work + /// id ("anthropic/claude-sonnet-5") and shortId ("sonnet") so lookups work /// against either form of `chatSummary.modelId`. Keep this aligned with /// `apps/desktop/src/shared/modelRegistry.ts` so the mobile composer exposes /// the same effort actions as desktop and the ADE TUI. @@ -243,15 +246,13 @@ enum ADEColor { "fable": ["low", "medium", "high", "xhigh", "max", "ultracode"], "anthropic/claude-opus-4-8": ["low", "medium", "high", "xhigh", "max", "ultracode"], "claude-opus-4-8": ["low", "medium", "high", "xhigh", "max", "ultracode"], - "anthropic/claude-opus-4-7": ["low", "medium", "high", "xhigh", "max"], - "claude-opus-4-7": ["low", "medium", "high", "xhigh", "max"], - "opus": ["low", "medium", "high", "xhigh", "max"], "anthropic/claude-opus-4-7-1m": ["low", "medium", "high", "xhigh", "max"], "claude-opus-4-7-1m": ["low", "medium", "high", "xhigh", "max"], "opus-1m": ["low", "medium", "high", "xhigh", "max"], "opus[1m]": ["low", "medium", "high", "xhigh", "max"], "claude-opus-4-7[1m]": ["low", "medium", "high", "xhigh", "max"], - "anthropic/claude-sonnet-4-6": ["low", "medium", "high", "max"], + "anthropic/claude-sonnet-5": ["low", "medium", "high", "max"], + "claude-sonnet-5": ["low", "medium", "high", "max"], "sonnet": ["low", "medium", "high", "max"], // Claude Haiku intentionally absent — no reasoning tiers. // OpenAI / Codex diff --git a/apps/ios/ADE/Views/Cto/CtoIdentityEditor.swift b/apps/ios/ADE/Views/Cto/CtoIdentityEditor.swift index 9cb764038..9015bd2b0 100644 --- a/apps/ios/ADE/Views/Cto/CtoIdentityEditor.swift +++ b/apps/ios/ADE/Views/Cto/CtoIdentityEditor.swift @@ -67,7 +67,7 @@ struct CtoIdentityEditor: View { .textInputAutocapitalization(.never) .disableAutocorrection(true) .font(.system(.body, design: .monospaced)) - TextField("claude-sonnet-4-6", text: $localModel) + TextField("claude-sonnet-5", text: $localModel) .textInputAutocapitalization(.never) .disableAutocorrection(true) .font(.system(.body, design: .monospaced)) diff --git a/apps/ios/ADE/Views/Hub/HubComposerDrawer.swift b/apps/ios/ADE/Views/Hub/HubComposerDrawer.swift index c0d7f44b8..112aa1b80 100644 --- a/apps/ios/ADE/Views/Hub/HubComposerDrawer.swift +++ b/apps/ios/ADE/Views/Hub/HubComposerDrawer.swift @@ -60,7 +60,7 @@ struct HubInlineComposer: View { // the provider/model onChange handlers don't reset runtimeMode on first // layout — same gotcha the in-project new-chat screen guards against). @State private var provider: String = "claude" - @State private var modelId: String = "claude-sonnet-4-6" + @State private var modelId: String = "claude-sonnet-5" @State private var runtimeMode: String = "default" @State private var reasoningEffort: String = "" @State private var codexFastMode: Bool = false @@ -95,7 +95,7 @@ struct HubInlineComposer: View { self._expanded = expanded self.onCreated = onCreated var restoredProvider = "claude" - var restoredModelId = "claude-sonnet-4-6" + var restoredModelId = "claude-sonnet-5" if let saved = WorkComposerPreferences.load() { restoredProvider = saved.provider restoredModelId = saved.modelId @@ -1056,8 +1056,8 @@ private func hubDefaultChatModelId(provider: String) -> String { switch hubNormalizedChatProvider(provider) { case "codex": return workDefaultCatalogModelId(provider: "codex") ?? "gpt-5.5" case "cursor": return "auto" - case "opencode": return "opencode/anthropic/claude-sonnet-4-6" - default: return "claude-sonnet-4-6" + case "opencode": return "opencode/anthropic/claude-sonnet-5" + default: return "claude-sonnet-5" } } @@ -1096,13 +1096,6 @@ private func hubPrettyModelName(_ model: String) -> String { return known } let lower = trimmed.lowercased() - switch lower { - case "opus": return "Claude Opus 4.7" - case "opus[1m]", "opus-1m": return "Claude Opus 4.7 1M" - case "sonnet": return "Claude Sonnet 4.6" - case "haiku": return "Claude Haiku 4.5" - default: break - } if lower.hasPrefix("claude-") { let tail = trimmed.dropFirst("claude-".count) let joined = tail.split(separator: "-").map { part -> String in diff --git a/apps/ios/ADE/Views/Work/WorkChatComposerAndInputViews.swift b/apps/ios/ADE/Views/Work/WorkChatComposerAndInputViews.swift index 01a19ab44..f67dc29f3 100644 --- a/apps/ios/ADE/Views/Work/WorkChatComposerAndInputViews.swift +++ b/apps/ios/ADE/Views/Work/WorkChatComposerAndInputViews.swift @@ -195,7 +195,9 @@ private func workModelRefsLookFastCapable(_ rawRefs: [String?]) -> Bool { "gpt-5.4", "gpt-5.4-codex", "openai/gpt-5.4", "openai/gpt-5.4-codex", "opencode/openai/gpt-5.4", "fable", "claude-fable-5", "anthropic/claude-fable-5", "opencode/anthropic/claude-fable-5", - "opus", "claude-opus-4-7", "anthropic/claude-opus-4-7", "opencode/anthropic/claude-opus-4-7", + "opus", "claude-opus-4-8", "anthropic/claude-opus-4-8", "opencode/anthropic/claude-opus-4-8", + "opus-4-7", "claude-opus-4-7", "anthropic/claude-opus-4-7", "opencode/anthropic/claude-opus-4-7", + "opencode/anthropic/opus", "opus-1m", "opus[1m]", "claude-opus-4-7-1m", "anthropic/claude-opus-4-7-1m", "opus-4.8", "opus-4-8", "claude-opus-4-8", "anthropic/claude-opus-4-8", "opencode/anthropic/claude-opus-4-8", @@ -556,7 +558,7 @@ struct WorkComposerChipStrip: View { } private func prettyModelName(_ model: String) -> String { - // Match the desktop composer's model label: "Claude Sonnet 4.6" / + // Match the desktop composer's model label: "Claude Sonnet 5" / // "GPT-5.4" instead of a bare short id. Host-reported // `chatSummary.model` is usually just "sonnet" / "opus" / "haiku" for // Claude and the full long form for Codex, so we special-case the @@ -567,14 +569,6 @@ struct WorkComposerChipStrip: View { return known } let lower = trimmed.lowercased() - - switch lower { - case "opus": return "Claude Opus 4.7" - case "opus[1m]", "opus-1m": return "Claude Opus 4.7 1M" - case "sonnet": return "Claude Sonnet 4.6" - case "haiku": return "Claude Haiku 4.5" - default: break - } if lower.hasPrefix("claude-") { let tail = trimmed.dropFirst("claude-".count) return "Claude " + beautifyModelSegment(String(tail)) diff --git a/apps/ios/ADE/Views/Work/WorkChatHeaderAndMessageViews.swift b/apps/ios/ADE/Views/Work/WorkChatHeaderAndMessageViews.swift index 94d6b0af5..1d580be0c 100644 --- a/apps/ios/ADE/Views/Work/WorkChatHeaderAndMessageViews.swift +++ b/apps/ios/ADE/Views/Work/WorkChatHeaderAndMessageViews.swift @@ -290,7 +290,7 @@ struct WorkChatMessageBubble: View { /// Active session model id, used to resolve the per-model accent for the /// model badge chip and card border tint. @Environment(\.workChatModelId) private var sessionModelId - /// Pretty model label ("Claude Sonnet 4.6"), injected by the session view + /// Pretty model label ("Claude Sonnet 5"), injected by the session view /// so each bubble doesn't have to recompute the same string. @Environment(\.workChatModelLabel) private var sessionModelLabel diff --git a/apps/ios/ADE/Views/Work/WorkModelCatalog.swift b/apps/ios/ADE/Views/Work/WorkModelCatalog.swift index 2e46f8015..d0b9f6776 100644 --- a/apps/ios/ADE/Views/Work/WorkModelCatalog.swift +++ b/apps/ios/ADE/Views/Work/WorkModelCatalog.swift @@ -242,10 +242,9 @@ private func workCuratedModelCatalogGroups() -> [WorkModelCatalogGroup] { models: [ WorkModelOption(id: "claude-fable-5", displayName: "Claude Fable 5", tier: .flagship, tagline: "Flagship · 1M context", provider: "claude", serviceTiers: ["fast"]), WorkModelOption(id: "claude-opus-4-8", displayName: "Claude Opus 4.8 1M", tier: .flagship, tagline: "Flagship · 1M context", provider: "claude", serviceTiers: ["fast"]), - WorkModelOption(id: "claude-opus-4-7", displayName: "Claude Opus 4.7", tier: .flagship, tagline: "Flagship · best for complex reasoning", provider: "claude", serviceTiers: ["fast"]), - WorkModelOption(id: "claude-opus-4-7-1m", displayName: "Claude Opus 4.7 1M", tier: .flagship, tagline: "1M-token context window", provider: "claude", serviceTiers: ["fast"]), - WorkModelOption(id: "claude-sonnet-4-6", displayName: "Claude Sonnet 4.6", tier: .balanced, tagline: "Balanced · great default for coding", provider: "claude"), + WorkModelOption(id: "claude-sonnet-5", displayName: "Claude Sonnet 5", tier: .balanced, tagline: "Balanced · 1M context", provider: "claude"), WorkModelOption(id: "claude-haiku-4-5", displayName: "Claude Haiku 4.5", tier: .fast, tagline: "Fastest · cheapest", provider: "claude"), + WorkModelOption(id: "claude-opus-4-7-1m", displayName: "Claude Opus 4.7 1M", tier: .flagship, tagline: "1M-token context window", provider: "claude", serviceTiers: ["fast"]), ] ) ] @@ -280,8 +279,7 @@ private func workCuratedModelCatalogGroups() -> [WorkModelCatalogGroup] { models: [ WorkModelOption(id: "claude-fable-5-thinking-high", displayName: "Claude Fable 5 Thinking High", tier: .flagship, tagline: "Cursor-routed Fable", provider: "claude"), WorkModelOption(id: "claude-fable-5-thinking-xhigh", displayName: "Claude Fable 5 Thinking XHigh", tier: .flagship, tagline: "Cursor-routed Fable", provider: "claude"), - WorkModelOption(id: "claude-4.6-sonnet-thinking", displayName: "Sonnet 4.6 · Thinking", tier: .reasoning, tagline: "Extended reasoning", provider: "claude"), - WorkModelOption(id: "claude-4.6-sonnet", displayName: "Sonnet 4.6", tier: .balanced, tagline: "Fast coding default", provider: "claude"), + WorkModelOption(id: "cursor/claude-4.6-sonnet-medium", displayName: "Claude Sonnet 5", tier: .balanced, tagline: "Balanced · 1M context", provider: "cursor"), ] ), WorkModelProvider( @@ -313,7 +311,7 @@ private func workCuratedModelCatalogGroups() -> [WorkModelCatalogGroup] { WorkModelOption(id: "claude-opus-4-6", displayName: "Opus 4.6 (2x)", tier: .flagship, tagline: "Flagship reasoning · 2x usage", provider: "claude"), WorkModelOption(id: "claude-opus-4-6-fast", displayName: "Opus 4.6 Fast Mode (12x)", tier: .flagship, tagline: "Faster Opus · 12x usage", provider: "claude"), WorkModelOption(id: "claude-opus-4-5-20251101", displayName: "Opus 4.5 (2x)", tier: .flagship, tagline: "Prior-gen Opus", provider: "claude"), - WorkModelOption(id: "claude-sonnet-4-6", displayName: "Sonnet 4.6 (1.2x)", tier: .balanced, tagline: "Balanced default", provider: "claude"), + WorkModelOption(id: "claude-sonnet-5", displayName: "Sonnet 5 (1.2x)", tier: .balanced, tagline: "Balanced default", provider: "claude"), WorkModelOption(id: "claude-sonnet-4-5-20250929", displayName: "Sonnet 4.5 (1.2x)", tier: .balanced, tagline: "Prior-gen Sonnet", provider: "claude"), WorkModelOption(id: "claude-haiku-4-5-20251001", displayName: "Haiku 4.5 (0.4x)", tier: .fast, tagline: "Fastest Anthropic", provider: "claude"), ] @@ -362,10 +360,10 @@ private func workCuratedModelCatalogGroups() -> [WorkModelCatalogGroup] { displayName: "Anthropic", models: [ WorkModelOption(id: "opencode/anthropic/claude-fable-5", displayName: "Claude Fable 5", tier: .flagship, tagline: "Flagship · 1M context", provider: "claude", serviceTiers: ["fast"]), - WorkModelOption(id: "opencode/anthropic/claude-sonnet-4-6", displayName: "Claude Sonnet 4.6", tier: .balanced, tagline: "Balanced coder", provider: "claude"), WorkModelOption(id: "opencode/anthropic/claude-opus-4-8", displayName: "Claude Opus 4.8 1M", tier: .flagship, tagline: "Flagship reasoning · 1M context", provider: "claude", serviceTiers: ["fast"]), - WorkModelOption(id: "opencode/anthropic/claude-opus-4-7", displayName: "Claude Opus 4.7", tier: .flagship, tagline: "Flagship reasoning", provider: "claude", serviceTiers: ["fast"]), + WorkModelOption(id: "opencode/anthropic/claude-sonnet-5", displayName: "Claude Sonnet 5", tier: .balanced, tagline: "Balanced coder · 1M context", provider: "claude"), WorkModelOption(id: "opencode/anthropic/claude-haiku-4-5", displayName: "Claude Haiku 4.5", tier: .fast, tagline: "Fastest Anthropic", provider: "claude"), + WorkModelOption(id: "opencode/anthropic/claude-opus-4-7-1m", displayName: "Claude Opus 4.7 1M", tier: .flagship, tagline: "1M-token context window", provider: "claude", serviceTiers: ["fast"]), ] ), WorkModelProvider( @@ -626,12 +624,15 @@ private func workCanonicalClaudeRegistryId(for raw: String) -> String? { "opus-4.8", "opus-4-8", "opus-4.8-1m", "opus-4.8[1m]", "opus-4-8-1m", "claude-opus-4-8-1m", "claude-opus-4-8[1m]", "anthropic/claude-opus-4-8-1m": return "anthropic/claude-opus-4-8" - case "opus", "claude-opus-4-7", "anthropic/claude-opus-4-7": - return "anthropic/claude-opus-4-7" - case "opus[1m]", "opus-1m", "claude-opus-4-7-1m", "claude-opus-4-7[1m]", "anthropic/claude-opus-4-7-1m": + case "opus", "claude-opus-4-7", "anthropic/claude-opus-4-7", + "opus-4.6", "opus-4-6", "claude-opus-4-6", "anthropic/claude-opus-4-6": + return "anthropic/claude-opus-4-8" + case "opus[1m]", "opus-1m", "claude-opus-4-7-1m", "claude-opus-4-7[1m]", "anthropic/claude-opus-4-7-1m", + "opus-4-6-1m", "claude-opus-4-6-1m", "claude-opus-4-6[1m]", "anthropic/claude-opus-4-6-1m": return "anthropic/claude-opus-4-7-1m" - case "sonnet", "claude-sonnet-4-6", "anthropic/claude-sonnet-4-6": - return "anthropic/claude-sonnet-4-6" + case "sonnet", "claude-sonnet-5", "anthropic/claude-sonnet-5", + "claude-sonnet-4-6", "anthropic/claude-sonnet-4-6": + return "anthropic/claude-sonnet-5" case "haiku", "claude-haiku-4-5", "anthropic/claude-haiku-4-5": return "anthropic/claude-haiku-4-5" default: @@ -647,14 +648,17 @@ private func workClaudeRuntimeModelId(for raw: String) -> String? { "opus-4.8", "opus-4-8", "opus-4.8-1m", "opus-4.8[1m]", "opus-4-8-1m", "claude-opus-4-8-1m", "claude-opus-4-8[1m]", "anthropic/claude-opus-4-8-1m": return "claude-opus-4-8" - case "opus", "claude-opus-4-7", "anthropic/claude-opus-4-7": - return "claude-opus-4-7" - case "opus[1m]", "opus-1m", "claude-opus-4-7-1m", "claude-opus-4-7[1m]", "anthropic/claude-opus-4-7-1m": + case "opus", "claude-opus-4-7", "anthropic/claude-opus-4-7", + "opus-4.6", "opus-4-6", "claude-opus-4-6", "anthropic/claude-opus-4-6": + return "claude-opus-4-8" + case "opus[1m]", "opus-1m", "claude-opus-4-7-1m", "claude-opus-4-7[1m]", "anthropic/claude-opus-4-7-1m", + "opus-4-6-1m", "claude-opus-4-6-1m", "claude-opus-4-6[1m]", "anthropic/claude-opus-4-6-1m": return "claude-opus-4-7[1m]" - case "sonnet", "claude-sonnet-4-6", "anthropic/claude-sonnet-4-6": - return "sonnet" + case "sonnet", "claude-sonnet-5", "anthropic/claude-sonnet-5", + "claude-sonnet-4-6", "anthropic/claude-sonnet-4-6": + return "claude-sonnet-5" case "haiku", "claude-haiku-4-5", "anthropic/claude-haiku-4-5": - return "haiku" + return "claude-haiku-4-5" default: return nil } @@ -712,12 +716,15 @@ func workKnownModelDisplayName(_ raw: String?) -> String? { "opus-4.8", "opus-4-8", "opus-4.8-1m", "opus-4.8[1m]", "opus-4-8-1m", "anthropic/claude-opus-4-8-1m", "claude-opus-4-8-1m", "claude-opus-4-8[1m]": return "Claude Opus 4.8 1M" - case "opus", "anthropic/claude-opus-4-7", "claude-opus-4-7": - return "Claude Opus 4.7" - case "opus[1m]", "opus-1m", "anthropic/claude-opus-4-7-1m", "claude-opus-4-7-1m", "claude-opus-4-7[1m]": + case "opus", "anthropic/claude-opus-4-7", "claude-opus-4-7", + "opus-4.6", "opus-4-6", "anthropic/claude-opus-4-6", "claude-opus-4-6": + return "Claude Opus 4.8 1M" + case "opus[1m]", "opus-1m", "anthropic/claude-opus-4-7-1m", "claude-opus-4-7-1m", "claude-opus-4-7[1m]", + "opus-4-6-1m", "anthropic/claude-opus-4-6-1m", "claude-opus-4-6-1m", "claude-opus-4-6[1m]": return "Claude Opus 4.7 1M" - case "sonnet", "anthropic/claude-sonnet-4-6", "claude-sonnet-4-6": - return "Claude Sonnet 4.6" + case "sonnet", "anthropic/claude-sonnet-5", "claude-sonnet-5", "cursor/claude-4.6-sonnet-medium", + "anthropic/claude-sonnet-4-6", "claude-sonnet-4-6": + return "Claude Sonnet 5" case "haiku", "anthropic/claude-haiku-4-5", "claude-haiku-4-5": return "Claude Haiku 4.5" case "gpt-5.5", "gpt-5.5-codex", "openai/gpt-5.5", "openai/gpt-5.5-codex": diff --git a/apps/ios/ADE/Views/Work/WorkNewChatScreen.swift b/apps/ios/ADE/Views/Work/WorkNewChatScreen.swift index 68d5961e5..761c4169f 100644 --- a/apps/ios/ADE/Views/Work/WorkNewChatScreen.swift +++ b/apps/ios/ADE/Views/Work/WorkNewChatScreen.swift @@ -492,7 +492,7 @@ struct WorkNewChatScreen: View { @State private var selectedLaneId: String = "" @State private var provider: String = "claude" - @State private var modelId: String = "claude-sonnet-4-6" + @State private var modelId: String = "claude-sonnet-5" @State private var busy: Bool = false @State private var errorMessage: String? @State private var modelPickerPresented = false @@ -528,7 +528,7 @@ struct WorkNewChatScreen: View { // (rather than assigning in onAppear) avoids the provider/model onChange // handlers firing and resetting runtimeMode back to the provider default. var restoredProvider = "claude" - var restoredModelId = "claude-sonnet-4-6" + var restoredModelId = "claude-sonnet-5" if let saved = WorkComposerPreferences.load() { restoredProvider = saved.provider restoredModelId = saved.modelId @@ -790,13 +790,6 @@ struct WorkNewChatScreen: View { return known } let lower = trimmed.lowercased() - switch lower { - case "opus": return "Claude Opus 4.7" - case "opus[1m]", "opus-1m": return "Claude Opus 4.7 1M" - case "sonnet": return "Claude Sonnet 4.6" - case "haiku": return "Claude Haiku 4.5" - default: break - } if lower.hasPrefix("claude-") { let tail = trimmed.dropFirst("claude-".count) let joined = tail.split(separator: "-").map { part -> String in @@ -1036,8 +1029,8 @@ private func workDefaultNewChatModelId(provider: String) -> String { switch workNormalizedNewChatProvider(provider) { case "codex": return workDefaultCatalogModelId(provider: "codex") ?? "gpt-5.5" case "cursor": return "auto" - case "opencode": return "opencode/anthropic/claude-sonnet-4-6" - default: return "claude-sonnet-4-6" + case "opencode": return "opencode/anthropic/claude-sonnet-5" + default: return "claude-sonnet-5" } } diff --git a/apps/ios/ADE/Views/Work/WorkPreviews.swift b/apps/ios/ADE/Views/Work/WorkPreviews.swift index 605ef5ebc..10659aabe 100644 --- a/apps/ios/ADE/Views/Work/WorkPreviews.swift +++ b/apps/ios/ADE/Views/Work/WorkPreviews.swift @@ -43,8 +43,8 @@ private enum WorkPreviewData { sessionId: "preview-chat-session", laneId: lane.id, provider: "claude", - model: "claude-sonnet-4-6", - modelId: "claude-sonnet-4-6", + model: "claude-sonnet-5", + modelId: "anthropic/claude-sonnet-5", sessionProfile: nil, title: "Fix iOS Work tab lag", goal: "Make the Work tab responsive on iPhone", @@ -276,8 +276,8 @@ private enum WorkPreviewData { costUsd: 0.0842 ), turnId: "turn-1", - model: "claude-sonnet-4-6", - modelId: "anthropic/claude-sonnet-4-6" + model: "claude-sonnet-5", + modelId: "anthropic/claude-sonnet-5" ) ), ] @@ -331,8 +331,8 @@ private enum WorkPreviewData { sessionId: sessionId, laneId: lane.id, provider: "claude", - model: "claude-sonnet-4-6", - modelId: "claude-sonnet-4-6", + model: "claude-sonnet-5", + modelId: "anthropic/claude-sonnet-5", sessionProfile: nil, title: title, goal: goal, diff --git a/apps/ios/ADE/Views/Work/WorkTimelineHelpers.swift b/apps/ios/ADE/Views/Work/WorkTimelineHelpers.swift index 83e064669..1a0e5c4d8 100644 --- a/apps/ios/ADE/Views/Work/WorkTimelineHelpers.swift +++ b/apps/ios/ADE/Views/Work/WorkTimelineHelpers.swift @@ -2032,7 +2032,7 @@ private func workTurnModelMetadata( } /// Beautify a host-supplied model id into the label used on chips and turn -/// separators. Mirrors the desktop composer's display: "Claude Sonnet 4.6", +/// separators. Mirrors the desktop composer's display: "Claude Sonnet 5", /// "GPT-5.4", etc., so iOS and desktop read the same. func prettyWorkChatModelName(_ raw: String) -> String { let trimmed = raw.trimmingCharacters(in: .whitespacesAndNewlines) @@ -2041,14 +2041,9 @@ func prettyWorkChatModelName(_ raw: String) -> String { return known } switch trimmed.lowercased() { - case "opus": return "Claude Opus 4.7" - case "opus[1m]", "opus-1m": return "Claude Opus 4.7 1M" - case "sonnet": return "Claude Sonnet 4.6" - case "haiku": return "Claude Haiku 4.5" + case let lower where lower.hasPrefix("claude-"): + return "Claude " + beautifyWorkModelSegment(String(trimmed.dropFirst("claude-".count))) default: - if trimmed.lowercased().hasPrefix("claude-") { - return "Claude " + beautifyWorkModelSegment(String(trimmed.dropFirst("claude-".count))) - } return beautifyWorkModelSegment(trimmed) } } diff --git a/apps/ios/ADETests/ADETests.swift b/apps/ios/ADETests/ADETests.swift index 3530462a5..1e1c3a4ac 100644 --- a/apps/ios/ADETests/ADETests.swift +++ b/apps/ios/ADETests/ADETests.swift @@ -7249,21 +7249,21 @@ final class ADETests: XCTestCase { // one the drawer opened on (the hub project-switch regression). A CLI-capable // model honors a stored CLI choice. XCTAssertTrue( - workModelAllowedForAvailabilityMode(modelId: "claude-sonnet-4-6", provider: "claude", mode: .cli), - "precondition: claude-sonnet-4-6 must be CLI-capable" + workModelAllowedForAvailabilityMode(modelId: "claude-sonnet-5", provider: "claude", mode: .cli), + "precondition: claude-sonnet-5 must be CLI-capable" ) WorkNewSessionModePreferences.save(.chat, projectId: "proj-a") WorkNewSessionModePreferences.save(.cli, projectId: "proj-b") XCTAssertEqual( WorkNewSessionModePreferences.resolvedMode( stored: WorkNewSessionModePreferences.load(projectId: "proj-a"), - modelId: "claude-sonnet-4-6", provider: "claude"), + modelId: "claude-sonnet-5", provider: "claude"), .chat ) XCTAssertEqual( WorkNewSessionModePreferences.resolvedMode( stored: WorkNewSessionModePreferences.load(projectId: "proj-b"), - modelId: "claude-sonnet-4-6", provider: "claude"), + modelId: "claude-sonnet-5", provider: "claude"), .cli ) @@ -7271,7 +7271,7 @@ final class ADETests: XCTestCase { XCTAssertEqual( WorkNewSessionModePreferences.resolvedMode( stored: WorkNewSessionModePreferences.load(projectId: "proj-unset"), - modelId: "claude-sonnet-4-6", provider: "claude"), + modelId: "claude-sonnet-5", provider: "claude"), .chat ) @@ -9824,7 +9824,7 @@ final class ADETests: XCTestCase { "sessionId": "chat-legacy", "laneId": "lane-1", "provider": "claude", - "model": "claude-sonnet-4-6", + "model": "claude-sonnet-5", "status": "active", "startedAt": "2026-03-25T00:00:00.000Z", "lastActivityAt": "2026-03-25T00:00:01.000Z", @@ -9904,12 +9904,18 @@ final class ADETests: XCTestCase { model: "opencode/openai/gpt-5.4", status: "idle" ) + let openCodeLegacyOpus = makeAgentChatSessionSummary( + provider: "opencode", + model: "opencode/anthropic/claude-opus-4-7", + status: "idle" + ) XCTAssertTrue(workChatComposerSupportsFastMode(codex)) XCTAssertFalse(workChatComposerSupportsFastMode(codexMini)) XCTAssertTrue(workChatComposerSupportsFastMode(claudeOpus)) XCTAssertFalse(workChatComposerSupportsFastMode(claude)) XCTAssertTrue(workChatComposerSupportsFastMode(openCode)) + XCTAssertTrue(workChatComposerSupportsFastMode(openCodeLegacyOpus)) } func testAgentChatModelInfoDetectsFastServiceTier() throws { @@ -9933,8 +9939,8 @@ final class ADETests: XCTestCase { XCTAssertEqual(info.cursorAvailability, CursorModelAvailability(cli: true, sdk: false)) let plainData = try JSONSerialization.data(withJSONObject: [ - "id": "claude-sonnet-4-6", - "displayName": "Sonnet 4.6", + "id": "claude-sonnet-5", + "displayName": "Sonnet 5", "isDefault": false, ]) let plain = try JSONDecoder().decode(AgentChatModelInfo.self, from: plainData) @@ -11123,19 +11129,19 @@ final class ADETests: XCTestCase { func testWorkModelCatalogInjectsMissingModelIntoMatchingProviderGroup() { XCTAssertEqual( - workModelCatalogGroupKey(for: "opencode/anthropic/claude-sonnet-4-6", currentProvider: "anthropic"), + workModelCatalogGroupKey(for: "opencode/anthropic/claude-sonnet-5", currentProvider: "anthropic"), "opencode" ) let groups = workModelCatalogGroups( - currentModelId: "opencode/anthropic/claude-sonnet-4-6", + currentModelId: "opencode/anthropic/claude-sonnet-5", currentProvider: "anthropic" ) let opencodeGroup = groups.first(where: { $0.key == "opencode" }) let anthropicProvider = opencodeGroup?.providers.first(where: { $0.key == "anthropic" }) XCTAssertEqual( - anthropicProvider?.models.filter { $0.id == "opencode/anthropic/claude-sonnet-4-6" }.count, + anthropicProvider?.models.filter { $0.id == "opencode/anthropic/claude-sonnet-5" }.count, 1 ) } @@ -11172,10 +11178,11 @@ final class ADETests: XCTestCase { XCTAssertEqual(ADEColor.reasoningTiers(for: "fable"), ["low", "medium", "high", "xhigh", "max", "ultracode"]) XCTAssertEqual(ADEColor.reasoningTiers(for: "anthropic/claude-opus-4-8"), ["low", "medium", "high", "xhigh", "max", "ultracode"]) XCTAssertEqual(ADEColor.reasoningTiers(for: "claude-opus-4-8"), ["low", "medium", "high", "xhigh", "max", "ultracode"]) - XCTAssertEqual(ADEColor.reasoningTiers(for: "anthropic/claude-opus-4-7"), ["low", "medium", "high", "xhigh", "max"]) - XCTAssertEqual(ADEColor.reasoningTiers(for: "claude-opus-4-7"), ["low", "medium", "high", "xhigh", "max"]) - XCTAssertEqual(ADEColor.reasoningTiers(for: "opus"), ["low", "medium", "high", "xhigh", "max"]) - XCTAssertEqual(ADEColor.reasoningTiers(for: "anthropic/claude-sonnet-4-6"), ["low", "medium", "high", "max"]) + XCTAssertEqual(ADEColor.reasoningTiers(for: "anthropic/claude-opus-4-7"), ["low", "medium", "high", "xhigh", "max", "ultracode"]) + XCTAssertEqual(ADEColor.reasoningTiers(for: "claude-opus-4-7"), ["low", "medium", "high", "xhigh", "max", "ultracode"]) + XCTAssertEqual(ADEColor.reasoningTiers(for: "opus"), ["low", "medium", "high", "xhigh", "max", "ultracode"]) + XCTAssertEqual(ADEColor.reasoningTiers(for: "opus[1m]"), ["low", "medium", "high", "xhigh", "max"]) + XCTAssertEqual(ADEColor.reasoningTiers(for: "anthropic/claude-sonnet-5"), ["low", "medium", "high", "max"]) XCTAssertNil(ADEColor.reasoningTiers(for: "claude-haiku-4-5")) XCTAssertEqual(ADEColor.reasoningTiers(for: "openai/gpt-5.3-codex-spark"), ["low", "medium", "high", "xhigh"]) XCTAssertEqual(ADEColor.reasoningTiers(for: "gpt-5.2"), ["low", "medium", "high", "xhigh"]) @@ -11216,8 +11223,8 @@ final class ADETests: XCTestCase { ], "cursor": [ AgentChatModelInfo( - id: "claude-4.6-sonnet", - displayName: "Sonnet 4.6", + id: "claude-sonnet-5", + displayName: "Sonnet 5", description: nil, isDefault: false, reasoningEfforts: nil, @@ -11260,6 +11267,21 @@ final class ADETests: XCTestCase { XCTAssertEqual(cursorGroup?.providers.first?.models.first?.provider, "claude") } + func testCuratedCursorSonnetUsesCursorRuntimeDescriptorId() { + let cursorGroup = workModelCatalogGroups(currentModelId: "", currentProvider: "cursor") + .first(where: { $0.key == "cursor" }) + let sonnet = cursorGroup? + .providers + .first(where: { $0.key == "anthropic" })? + .models + .first(where: { $0.displayName == "Claude Sonnet 5" }) + + XCTAssertEqual(sonnet?.id, "cursor/claude-4.6-sonnet-medium") + XCTAssertEqual(sonnet?.provider, "cursor") + XCTAssertEqual(workResolveCliProvider(for: sonnet?.id ?? "", provider: "cursor"), "cursor") + XCTAssertEqual(workKnownModelDisplayName(sonnet?.id), "Claude Sonnet 5") + } + func testWorkModelCatalogFiltersCursorModelsByChatAndCliAvailability() { let groups = [ WorkModelCatalogGroup( @@ -11314,8 +11336,8 @@ final class ADETests: XCTestCase { displayName: "Anthropic", models: [ WorkModelOption( - id: "claude-sonnet-4-6", - displayName: "Claude Sonnet 4.6", + id: "claude-sonnet-5", + displayName: "Claude Sonnet 5", tier: .balanced, tagline: "Claude", provider: "claude" @@ -11348,15 +11370,15 @@ final class ADETests: XCTestCase { .first? .models .map(\.id), - ["claude-sonnet-4-6"] + ["claude-sonnet-5"] ) } func testWorkResolveCliProviderMapsModelFamiliesLikeDesktop() { - XCTAssertEqual(workResolveCliProvider(for: "claude-sonnet-4-6", provider: "claude"), "claude") + XCTAssertEqual(workResolveCliProvider(for: "claude-sonnet-5", provider: "claude"), "claude") XCTAssertEqual(workResolveCliProvider(for: "gpt-5.5", provider: "codex"), "codex") XCTAssertEqual(workResolveCliProvider(for: "auto", provider: "cursor"), "cursor") - XCTAssertEqual(workResolveCliProvider(for: "opencode/anthropic/claude-sonnet-4-6", provider: "opencode"), "opencode") + XCTAssertEqual(workResolveCliProvider(for: "opencode/anthropic/claude-sonnet-5", provider: "opencode"), "opencode") } func testWorkModelCatalogTreatsCodexRuntimeAndRegistryIdsAsSameModel() { @@ -11395,6 +11417,16 @@ final class ADETests: XCTestCase { XCTAssertEqual(prettyWorkChatModelName("openai/gpt-5.5"), "GPT-5.5") } + func testWorkModelCatalogTreatsOpus46AliasesAsMigratedRows() { + XCTAssertTrue(workModelIdsEquivalent("claude-opus-4-6", "claude-opus-4-8")) + XCTAssertTrue(workModelIdsEquivalent("anthropic/claude-opus-4-6", "anthropic/claude-opus-4-8")) + XCTAssertTrue(workModelIdsEquivalent("opus-4-6", "claude-opus-4-8")) + XCTAssertTrue(workModelIdsEquivalent("opus-4.6", "claude-opus-4-8")) + XCTAssertTrue(workModelIdsEquivalent("claude-opus-4-6-1m", "claude-opus-4-7-1m")) + XCTAssertTrue(workModelIdsEquivalent("claude-opus-4-6[1m]", "claude-opus-4-7-1m")) + XCTAssertEqual(workKnownModelDisplayName("anthropic/claude-opus-4-6"), "Claude Opus 4.8 1M") + } + func testExtractWorkNavigationTargetsFindsFilePathsAndPullRequestNumbers() { let targets = extractWorkNavigationTargets( from: #"Updated apps/ios/ADE/Views/WorkTabView.swift and docs/plan.md before opening PR #145. See src/main.ts too."# @@ -11798,7 +11830,7 @@ final class ADETests: XCTestCase { sessionId: "chat-1", timestamp: "2026-03-25T00:00:03.000Z", sequence: 3, - event: .done(status: "completed", summary: "Completed\nclaude-sonnet-4-6", usage: nil, turnId: "turn-1", model: "claude-sonnet-4-6", modelId: "anthropic/claude-sonnet-4-6") + event: .done(status: "completed", summary: "Completed\nclaude-sonnet-5", usage: nil, turnId: "turn-1", model: "claude-sonnet-5", modelId: "anthropic/claude-sonnet-5") ), WorkChatEnvelope( sessionId: "chat-1", @@ -11834,7 +11866,7 @@ final class ADETests: XCTestCase { return message } XCTAssertEqual(assistantMessages.map(\.turnProvider), ["claude", "codex"]) - XCTAssertEqual(assistantMessages.map(\.turnModelId), ["anthropic/claude-sonnet-4-6", "openai/gpt-5.4-mini"]) + XCTAssertEqual(assistantMessages.map(\.turnModelId), ["anthropic/claude-sonnet-5", "openai/gpt-5.4-mini"]) let separated = injectWorkTurnSeparators( into: timeline, @@ -11846,7 +11878,7 @@ final class ADETests: XCTestCase { return separator } - XCTAssertEqual(separators.map(\.modelLabel), ["Claude Sonnet 4.6", "GPT 5.4 Mini"]) + XCTAssertEqual(separators.map(\.modelLabel), ["Claude Sonnet 5", "GPT 5.4 Mini"]) XCTAssertEqual(separators.map(\.provider), ["claude", "codex"]) let endMarkers = separated.compactMap { entry -> WorkTurnEndMarker? in @@ -11856,7 +11888,7 @@ final class ADETests: XCTestCase { XCTAssertEqual(endMarkers.map(\.turnId), ["turn-1", "turn-2"]) XCTAssertEqual(endMarkers.map(\.workedDurationLabel), ["2s", "2s"]) XCTAssertEqual(endMarkers.map(\.status), ["completed", "completed"]) - XCTAssertEqual(endMarkers.map(\.modelLabel), ["Claude Sonnet 4.6", "GPT 5.4 Mini"]) + XCTAssertEqual(endMarkers.map(\.modelLabel), ["Claude Sonnet 5", "GPT 5.4 Mini"]) XCTAssertEqual(endMarkers.map(\.provider), ["claude", "codex"]) let turnOrder = separated.compactMap { entry -> String? in @@ -14661,7 +14693,7 @@ final class ADETests: XCTestCase { "reasoningEffort": "high", "fastMode": true }, - "availableModels": ["gpt-5.4", "claude-sonnet-4-6"] + "availableModels": ["gpt-5.4", "claude-sonnet-5"] } } } @@ -14679,7 +14711,7 @@ final class ADETests: XCTestCase { XCTAssertEqual(model.workDescription, "Build the orchestration roster.") XCTAssertEqual(model.filesHint, ["OrchestrationPanel.tsx", "TaskCard.tsx"]) XCTAssertEqual(model.dependsOn, ["planning-rounds", "model-routing"]) - XCTAssertEqual(model.availableModelIds, ["gpt-5.4", "claude-sonnet-4-6"]) + XCTAssertEqual(model.availableModelIds, ["gpt-5.4", "claude-sonnet-5"]) XCTAssertFalse( Mirror(reflecting: model).children.contains { $0.label == "suggested" }, "Mobile model-selection gates should not carry a suggested route." @@ -14782,7 +14814,7 @@ final class ADETests: XCTestCase { "sessionId": "sess-orch-1", "laneId": "lane-1", "provider": "claude", - "model": "claude-sonnet-4-6", + "model": "claude-sonnet-5", "status": "running", "startedAt": "2026-05-25T00:00:00.000Z", "lastActivityAt": "2026-05-25T00:01:00.000Z", @@ -14854,7 +14886,7 @@ final class ADETests: XCTestCase { "sessionId": "sess-full-orch", "laneId": "lane-2", "provider": "claude", - "model": "claude-sonnet-4-6", + "model": "claude-sonnet-5", "status": "running", "createdAt": "2026-05-25T00:00:00.000Z", "lastActivityAt": "2026-05-25T00:02:00.000Z", diff --git a/apps/ios/ADEWidgets/ADEAgentActivityWidget.swift b/apps/ios/ADEWidgets/ADEAgentActivityWidget.swift index 7b3de2bf7..aaf80a756 100644 --- a/apps/ios/ADEWidgets/ADEAgentActivityWidget.swift +++ b/apps/ios/ADEWidgets/ADEAgentActivityWidget.swift @@ -346,7 +346,7 @@ private extension ADEAgentRunsAttributes.ContentState { activeCount: 2, runs: [ .init(id: "a", title: "Refactor sync transport", phase: "running", model: "gpt-5-codex", lane: "Primary", detail: "editing SyncService.swift"), - .init(id: "b", title: "Audit pairing", phase: "running", model: "claude-sonnet-4-6", lane: "feat/pair"), + .init(id: "b", title: "Audit pairing", phase: "running", model: "claude-sonnet-5", lane: "feat/pair"), ] ) } diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index f0d2fa85e..198de26fc 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -390,10 +390,10 @@ Agent tools are split by domain: `apps/desktop/src/shared/modelRegistry.ts` + `apps/desktop/src/shared/modelProfiles.ts`: -- `MODEL_REGISTRY` — static CLI-wrapped entries + dynamically populated API-key/local entries. The Claude block starts with the new Opus 4.8 1M entry (`anthropic/claude-opus-4-8`, display name `Claude Opus 4.8 1M`, provider model `claude-opus-4-8`, 1,000,000 context / 128,000 max output), while preserving the existing Claude Opus 4.7 and Opus 4.7 1M entries (`anthropic/claude-opus-4-7-1m`, aliases `opus[1m]` / `claude-opus-4-7[1m]`). Opus entries advertise the full `low|medium|high|xhigh|max` reasoning tier set and `costTier: "very_high"`. `ModelDescriptor.serviceTiers?: string[]` advertises optional service tiers (today: `"fast"`, set on Codex GPT 5.4 / 5.5 entries and dynamic Cursor SDK/CLI rows) that the UI's Fast Mode toggle keys off. Codex maps it to the JSON-RPC `serviceTier` argument; Cursor SDK maps it through discovered model parameters, and Cursor CLI launches use the matching fast model alias when present. +- `MODEL_REGISTRY` — static CLI-wrapped entries + dynamically populated API-key/local entries. The Claude block is ordered for every picker as Fable 5, Opus 4.8 1M, Sonnet 5, Haiku 4.5, then Opus 4.7 1M. Sonnet 5 uses provider model `claude-sonnet-5` (1,000,000 context / 128,000 max output); removed Sonnet 4.6 and basic Opus 4.7 ids resolve forward as compatibility aliases but do not appear as selectable rows. Opus 4.7 1M remains available as `anthropic/claude-opus-4-7-1m` with aliases `opus[1m]` / `claude-opus-4-7[1m]`. `ModelDescriptor.serviceTiers?: string[]` advertises optional service tiers (today: `"fast"`, set on Fable/Opus, Codex GPT 5.4 / 5.5 entries, and dynamic Cursor SDK/CLI rows) that the UI's Fast Mode toggle keys off. Codex maps it to the JSON-RPC `serviceTier` argument; Cursor SDK maps it through discovered model parameters, and Cursor CLI launches use the matching fast model alias when present. - `ModelProviderGroup` = `"claude" | "codex" | "opencode" | "cursor" | "droid"`. Cursor and Droid each have their own top-level provider group used by the model picker, identity routing, and tracked CLI provider catalog. - Helpers: `getModelById`, `getModelPricing`, `updateModelPricingInRegistry`, `replaceDynamicOpenCodeModelDescriptors`, `resolveProviderGroupForModel`, `resolveModelDescriptorForProvider`, `getRuntimeModelRefForDescriptor`, `modelSupportsServiceTier(descriptor, tier)` / `modelSupportsFastMode(descriptor)`. -- Reasoning tier passthrough (`providerOptions.ts`) maps tier strings directly to each provider's native config (`thinking.type`, `reasoningEffort`, `thinkingConfig.thinkingLevel`, etc.) — no arbitrary token budgets. Claude Opus registry rows advertise `low | medium | high | xhigh | max`, while the global Claude quick-pick ladder remains `low | medium | high | max`. +- Reasoning tier passthrough (`providerOptions.ts`) maps tier strings directly to each provider's native config (`thinking.type`, `reasoningEffort`, `thinkingConfig.thinkingLevel`, etc.) — no arbitrary token budgets. Claude Fable and Opus 4.8 rows advertise `low | medium | high | xhigh | max | ultracode`; Sonnet 5 advertises `low | medium | high | max`. Interactive chat (Terminals, Work), CTO delegation, and automation-launched agent sessions flow through the unified executor with the same permission plumbing. Related feature docs: [Chat](./features/chat/README.md), [Agents](./features/agents/README.md), [CTO](./features/cto/README.md), and [Automations](./features/automations/README.md). diff --git a/docs/features/chat/README.md b/docs/features/chat/README.md index 3808de2d5..3660eda32 100644 --- a/docs/features/chat/README.md +++ b/docs/features/chat/README.md @@ -39,9 +39,9 @@ machinery layered on top. | `apps/desktop/src/main/services/chat/droidSdkWorker.ts` | Node worker that hosts `@factory/droid-sdk`. Streams SDK events back to the main process and forwards permission / ask-user prompts back through the JSON-line protocol. | | `apps/desktop/src/main/services/chat/droidSdkProtocol.ts` | Worker IPC types: `DroidSdkSessionSettings` (autonomy level, interaction mode, reasoning effort), `DroidSdkReasoningEffort`, `DroidSdkPermissionRequest`/`Decision`, `DroidSdkAskUserRequest`/`Response`, `DroidSdkReady` (handshake with `availableModels`), and `DroidSdkSendPrompt`. | | `apps/desktop/src/main/services/chat/droidSdkEventMapper.ts` | Per-session `DroidSdkEventMapperState` + `mapDroidSdkMessageToChatEvents` / `mapDroidSdkRunResultToDoneEvent`. Tracks streaming text/thinking item ids, maps tool calls and results, and surfaces token usage. Replaces the deleted `droidAcpPool.ts` + `droidAcpEventMapper` path. | -| `apps/desktop/src/main/services/chat/droidModelsDiscovery.ts` | SDK-driven model probe (`listDroidModelsFromSdk`) plus the `~/.factory/config.json` custom-proxy merge. Exposes `discoverDroidSdkModelDescriptors` (alias for the legacy `discoverDroidCliModelDescriptors` while callers migrate). | +| `apps/desktop/src/main/services/chat/droidModelsDiscovery.ts` | SDK-driven model probe (`listDroidModelsFromSdk`) plus the `~/.factory/config.json` custom-proxy merge. Normalizes retired factory Claude ids (Sonnet 4.6 -> Sonnet 5, basic Opus 4.7 -> Opus 4.8) before descriptors reach desktop, mobile, or TUI model pickers. Exposes `discoverDroidSdkModelDescriptors` (alias for the legacy `discoverDroidCliModelDescriptors` while callers migrate). | | `apps/desktop/src/main/services/opencode/openCodeBinaryManager.ts` | Resolves the OpenCode CLI: PATH first, then the bundled `node_modules/.bin/opencode`. Cache entries are re-validated with `canRunBinaryCandidate` on every lookup so user installs after launch are picked up; missing-binary lookups are intentionally not cached. `clearOpenCodeBinaryCache()` is wired into the AI integration's full cache reset. | -| `apps/desktop/src/main/services/opencode/openCodeInventory.ts` | OpenCode provider/model probe. Now classifies model variants into `reasoningTiers` + `serviceTiers` (alias map covering `minimal`/`mini`/`med`/`xhigh`/`extra-high`), reads `capabilities` (tools/vision/reasoning) into descriptor capabilities, and tracks both `modelIds` (connected providers only) and `catalogModelIds` (the full browseable catalog). `OpenCodeProviderInfo.availableModelCount` exposes the connected count separately from `modelCount`. | +| `apps/desktop/src/main/services/opencode/openCodeInventory.ts` | OpenCode provider/model probe. Now classifies model variants into `reasoningTiers` + `serviceTiers` (alias map covering `minimal`/`mini`/`med`/`xhigh`/`extra-high`), reads `capabilities` (tools/vision/reasoning) into descriptor capabilities, and tracks both `modelIds` (connected providers only) and `catalogModelIds` (the full browseable catalog). Anthropic rows normalize retired Sonnet 4.6 / basic Opus 4.7 ids to Sonnet 5 / Opus 4.8 so runtime catalogs cannot reintroduce removed picker rows. `OpenCodeProviderInfo.availableModelCount` exposes the connected count separately from `modelCount`. | | `apps/desktop/src/shared/chatTranscript.ts` | Pure JSON-lines parser for `AgentChatEventEnvelope` values. Used by both the main process and the renderer. | | `apps/desktop/src/shared/chatSubagents.ts` | Cross-target subagent helpers: `buildSubagentPaneRows`, `selectedSubagentSnapshot`, `subagentIndexForPaneLine`, `subagentPaneSelectableLineOffsets`, `buildSubagentTranscriptEvents`, `isLifecycleEventForSnapshot`, plus the `latestPlan` derivation. Both the desktop `ChatSubagentsPanel` and the `apps/ade-cli/src/tuiClient/subagentPane.ts` / `chatInfo.ts` modules re-export from here so the desktop pane and the terminal TUI render the same roster, transcript filter, and plan summary. | | `apps/desktop/src/main/services/chat/claudeWorkflowProgress.ts` | Defensive normalizer for the Claude Agent SDK's undocumented `workflow_progress` snapshot on `system:task_progress` (Workflow orchestration runs). Parses phases + per-agent entries (caps counts, clips previews, drops malformed entries, unknown states degrade to queued/running; unparseable snapshots return undefined so the generic task rendering is untouched), then `planClaudeWorkflowAgentTransitions` diffs each cumulative tick against per-task emit state to fan out `subagent_started/progress/result` events under a stable `::a` identity with the emitted agentId latched at first emission. Consumed by `agentChatService`'s `task_progress`/`task_notification` handlers and the interrupt path (which close still-running agents as `stopped`). | diff --git a/docs/features/chat/agent-routing.md b/docs/features/chat/agent-routing.md index 5bbc715e7..99a65cdf5 100644 --- a/docs/features/chat/agent-routing.md +++ b/docs/features/chat/agent-routing.md @@ -93,14 +93,14 @@ defaults to `guarded` unless explicitly whitelisted. Claude's global quick-pick vocabulary is `low | medium | high | max` (`CLAUDE_THINKING_LEVELS` in `shared/modelProfiles.ts`), while Opus registry rows can advertise the richer `low|medium|high|xhigh|max` -set for model-specific pickers. The Claude registry now lists the -new Opus 4.8 1M entry first (`anthropic/claude-opus-4-8`, display -name `Claude Opus 4.8 1M`, provider model `claude-opus-4-8`, -1,000,000-token context, 128 k output, tier `very_high`) and keeps -the existing Opus 4.7 and Opus 4.7 1M entries intact (`opus` still -targets 4.7; `opus[1m]` / `opus-1m` still target 4.7 1M). Passthrough -to the provider config is unchanged (the tier string is forwarded -directly to the CLI / SDK — no synthesized token budgets). +set for model-specific pickers. The Claude registry is ordered as +Fable 5, Opus 4.8 1M, Sonnet 5, Haiku 4.5, then Opus 4.7 1M. +Sonnet 5 selects provider model `claude-sonnet-5`; retired Sonnet 4.6 +ids resolve forward for compatibility and no longer appear as picker +rows. The basic Opus 4.7 row is also removed; its old aliases resolve +to Opus 4.8, while `opus[1m]` / `opus-1m` still target Opus 4.7 1M. +Passthrough to the provider config is unchanged (the tier string is +forwarded directly to the CLI / SDK, with no synthesized token budgets). ## Auth and credentials diff --git a/docs/features/sync-and-multi-device/ios-companion.md b/docs/features/sync-and-multi-device/ios-companion.md index e4af99434..c952436fb 100644 --- a/docs/features/sync-and-multi-device/ios-companion.md +++ b/docs/features/sync-and-multi-device/ios-companion.md @@ -1280,11 +1280,14 @@ reflected in the phone's UI on the next descriptor read. (`WorkModelCatalog.swift`, mirroring desktop's `resolveCliProviderForModel`), so adding a provider means updating both the runtime registry and the phone's model-catalog grouping - together; `shell` remains valid runtime-side but the phone no longer - offers a plain-shell launch. `SyncStartCliSessionArgs` also - carries an optional `reasoningEffort` field that the runtime forwards - to `buildTrackedCliLaunchCommand`, so the phone can launch a Codex - / Claude CLI session at a non-default effort tier without going + together; the Claude picker order mirrors desktop (Fable 5, Opus + 4.8 1M, Sonnet 5, Haiku 4.5, Opus 4.7 1M) and legacy Sonnet 4.6 / + basic Opus 4.7 selections normalize forward instead of appearing as + rows. `shell` remains valid runtime-side but the phone no longer + offers a plain-shell launch. `SyncStartCliSessionArgs` also carries + an optional `reasoningEffort` field that the runtime forwards to + `buildTrackedCliLaunchCommand`, so the phone can launch a Codex / + Claude CLI session at a non-default effort tier without going through the desktop. - **Codex CLI launches receive the initial prompt as argv, not PTY echo.** Other providers still receive `initialInput` as bytes typed