diff --git a/CHANGELOG.md b/CHANGELOG.md index bc031ba1f..30f0b5ed8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Claude models + +- Claude Fable 5.1 is the default Claude model. Pickers list Fable 5.1, Opus 5, Sonnet 5, Haiku 4.5, then Opus 4.8. Opus 4.8 drops the 1M suffix. Fable 5 and Opus 4.7 ids, including the old 1M aliases, resolve forward and no longer appear as rows. + ## [1.2.70] - 2026-09-01 ### Cursor chat reliability diff --git a/CLAUDE.md b/CLAUDE.md index f46de5b60..fb2cd2c1d 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1,5 +1,5 @@ ## Model roles (Claude-specific) -If you are Fable 5, you are always the PM/coordinator. You research, decide, and write the specs; you do not implement by default. Delegate the implementation to Claude Opus 5 subagents — medium effort for routine tasks, high effort for hard ones. Judge each result on its merits; if a subagent's output misses the bar, tighten the spec or raise the effort and redo it. +If you are Fable 5.1, you are always the PM/coordinator. You research, decide, and write the specs; you do not implement by default. Delegate the implementation to Claude Opus 5 subagents — medium effort for routine tasks, high effort for hard ones. Judge each result on its merits; if a subagent's output misses the bar, tighten the spec or raise the effort and redo it. If you are any other Claude model, do the work yourself, and use Opus 5 subagents when you need parallelism or an independent second opinion. diff --git a/apps/ade-cli/src/tuiClient/__tests__/appInput.test.ts b/apps/ade-cli/src/tuiClient/__tests__/appInput.test.ts index 86176254f..46bc70527 100644 --- a/apps/ade-cli/src/tuiClient/__tests__/appInput.test.ts +++ b/apps/ade-cli/src/tuiClient/__tests__/appInput.test.ts @@ -2579,7 +2579,7 @@ describe("notice scoping", () => { it("keeps the chat-or-global fallback when not in a draft", () => { const notices = [ notice("Reconnected to the ADE runtime.", null), - notice("Model set to Claude Opus 4.8 1M.", "chat-1"), + notice("Model set to Claude Opus 4.8.", "chat-1"), notice("Other chat feedback.", "chat-2"), ]; const visible = selectVisibleNotices({ @@ -2591,7 +2591,7 @@ describe("notice scoping", () => { }); expect(visible.map((entry) => entry.text)).toEqual([ "Reconnected to the ADE runtime.", - "Model set to Claude Opus 4.8 1M.", + "Model set to Claude Opus 4.8.", ]); }); 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 4677a1fb6..92a3d351b 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-opus-4-8", displayName: "Claude Opus 4.8" }), modelInfo({ id: "anthropic/claude-sonnet-5", displayName: "Claude Sonnet 5" }), modelInfo({ id: "openai/gpt-5", displayName: "GPT-5" }), ]; @@ -128,12 +128,11 @@ describe("buildModelPickerLayout", () => { searchMode: false, }); expect(layout.entries.map((entry) => entry.modelId)).toEqual([ - "anthropic/claude-fable-5", + "anthropic/claude-fable-5-1", "anthropic/claude-opus-5", "anthropic/claude-sonnet-5", "anthropic/claude-haiku-4-5", "anthropic/claude-opus-4-8", - "anthropic/claude-opus-4-7-1m", ]); expect(layout.entries.every((entry) => entry.family === "claude")).toBe(true); }); diff --git a/apps/desktop/resources/voice/voice-glossary.json b/apps/desktop/resources/voice/voice-glossary.json index ef85720a2..af7d415da 100644 --- a/apps/desktop/resources/voice/voice-glossary.json +++ b/apps/desktop/resources/voice/voice-glossary.json @@ -15,7 +15,7 @@ "Claude Opus 4.8", "Claude Sonnet 5", "Claude Haiku 4.5", - "Claude Fable 5", + "Claude Fable 5.1", "GPT-5.6 Sol", "GPT-5.6 Terra", "GPT-5.6 Luna", diff --git a/apps/desktop/src/main/services/ai/claudeModelUtils.test.ts b/apps/desktop/src/main/services/ai/claudeModelUtils.test.ts index c048662cf..899d3ff6b 100644 --- a/apps/desktop/src/main/services/ai/claudeModelUtils.test.ts +++ b/apps/desktop/src/main/services/ai/claudeModelUtils.test.ts @@ -8,7 +8,7 @@ describe("resolveClaudeCliModel", () => { expect(resolveClaudeCliModel("opus")).toBe("claude-opus-5"); }); - it("normalizes Opus 4.8 1M aliases to the exact Opus 4.8 model", () => { + it("normalizes Opus 4.8 aliases to the exact Opus 4.8 model", () => { expect(resolveClaudeCliModel("claude-opus-4-8")).toBe("claude-opus-4-8"); expect(resolveClaudeCliModel("opus-4.8-1m")).toBe("claude-opus-4-8"); expect(resolveClaudeCliModel("claude-opus-4-8[1m]")).toBe("claude-opus-4-8"); @@ -20,9 +20,11 @@ describe("resolveClaudeCliModel", () => { 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]"); + it("maps retired Opus 4.7 1M and Fable 5 aliases onto current CLI ids", () => { + expect(resolveClaudeCliModel("claude-opus-4-7-1m")).toBe("claude-opus-4-8"); + expect(resolveClaudeCliModel("claude-opus-4-7[1m]")).toBe("claude-opus-4-8"); expect(resolveClaudeCliModel("opus-11m")).toBe("claude-opus-4-8"); + expect(resolveClaudeCliModel("fable")).toBe("claude-fable-5-1"); + expect(resolveClaudeCliModel("claude-fable-5")).toBe("claude-fable-5-1"); }); }); diff --git a/apps/desktop/src/main/services/ai/claudeModelUtils.ts b/apps/desktop/src/main/services/ai/claudeModelUtils.ts index 7eb5a94f2..aa949fef1 100644 --- a/apps/desktop/src/main/services/ai/claudeModelUtils.ts +++ b/apps/desktop/src/main/services/ai/claudeModelUtils.ts @@ -3,8 +3,8 @@ import { getDefaultModelDescriptor, getModelById, resolveModelAlias } from "../. /** * Normalize arbitrary Claude model strings into CLI-safe values accepted by - * Claude Code (`claude-fable-5`, `claude-opus-5`, `claude-opus-4-8`, `claude-sonnet-5`, - * `claude-haiku-4-5`, `claude-opus-4-7[1m]`) where possible. + * Claude Code (`claude-fable-5-1`, `claude-opus-5`, `claude-opus-4-8`, `claude-sonnet-5`, + * `claude-haiku-4-5`) where possible. */ export function resolveClaudeCliModel(model: string | null | undefined): string { return resolveClaudeCliModelAlias(model, "claude-sonnet-5") ?? "claude-sonnet-5"; diff --git a/apps/desktop/src/main/services/chat/agentChatService.test.ts b/apps/desktop/src/main/services/chat/agentChatService.test.ts index 7ffd72db9..2ef850e48 100644 --- a/apps/desktop/src/main/services/chat/agentChatService.test.ts +++ b/apps/desktop/src/main/services/chat/agentChatService.test.ts @@ -3122,7 +3122,7 @@ describe("createAgentChatService", () => { expect(session.model).toBe("claude-sonnet-5"); }); - it("keeps legacy bracketed 1M model aliases on Claude Opus 4.7 1M", async () => { + it("maps retired Claude Opus 4.7 1M aliases onto Opus 4.8", async () => { const { service } = createService(); const session = await service.createSession({ laneId: "lane-1", @@ -3130,8 +3130,8 @@ describe("createAgentChatService", () => { model: "claude-opus-4-7[1m]", }); - expect(session.modelId).toBe("anthropic/claude-opus-4-7-1m"); - expect(session.model).toBe("claude-opus-4-7[1m]"); + expect(session.modelId).toBe("anthropic/claude-opus-4-8"); + expect(session.model).toBe("claude-opus-4-8"); }); it.each([ @@ -3216,7 +3216,7 @@ describe("createAgentChatService", () => { 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 () => { + it("maps retired Claude Opus 4.7 1M sessions onto Opus 4.8 even when the SDK reports bare Opus 4.7", async () => { const events: AgentChatEventEnvelope[] = []; let streamCall = 0; vi.mocked(claudeSdkCreateSessionCompat).mockReturnValue({ @@ -3287,8 +3287,8 @@ 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-7[1m]"); - expect((doneEvent!.event as any).modelId).toBe("anthropic/claude-opus-4-7-1m"); + expect((doneEvent!.event as any).model).toBe("claude-opus-4-8"); + expect((doneEvent!.event as any).modelId).toBe("anthropic/claude-opus-4-8"); }); it("suppresses Claude EDE diagnostics without hiding real result errors", async () => { diff --git a/apps/desktop/src/main/services/chat/agentChatService.ts b/apps/desktop/src/main/services/chat/agentChatService.ts index d06906036..fe102929a 100644 --- a/apps/desktop/src/main/services/chat/agentChatService.ts +++ b/apps/desktop/src/main/services/chat/agentChatService.ts @@ -5610,10 +5610,9 @@ function resolveClaudeCliModelIdFromRuntimeValue(model: string): string | undefi const descriptors = listModelDescriptorsForProvider("claude"); const exactMatch = descriptors.find((descriptor) => { - const descriptorShortId = descriptor.shortId.toLowerCase(); const candidates = new Set([ descriptor.id.toLowerCase(), - descriptorShortId, + descriptor.shortId.toLowerCase(), descriptor.providerModelId.toLowerCase(), descriptor.id.toLowerCase().replace(/^anthropic\//, ""), ...(descriptor.aliases ?? []).map((alias) => alias.toLowerCase()), @@ -5623,22 +5622,16 @@ function resolveClaudeCliModelIdFromRuntimeValue(model: string): string | undefi }); if (exactMatch) return exactMatch.id; + // Prefix-match dated snapshots (`claude-opus-5-20260301`) against the + // canonical provider id, not the short id. Short id `opus` would otherwise + // swallow every `claude-opus-*` runtime string, including retired 4.7 ids. return descriptors.find((descriptor) => { - const descriptorShortId = descriptor.shortId.toLowerCase(); - return normalizedWithoutProvider === `claude-${descriptorShortId}` - || normalizedWithoutProvider.startsWith(`claude-${descriptorShortId}-`); + const providerModelId = descriptor.providerModelId.toLowerCase(); + return normalizedWithoutProvider === providerModelId + || normalizedWithoutProvider.startsWith(`${providerModelId}-`); })?.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 isBareClaudeOpusRuntimeAlias(model: string): boolean { return model.trim().toLowerCase() .replace(/^anthropic\//, "") @@ -5770,21 +5763,7 @@ function resolveClaudeTurnModelPayload( } else if (sessionModelId) { selectedDescriptor = getModelById(sessionModelId); } - const selectedIsOpusOneMillion = - selectedDescriptor?.id === "anthropic/claude-opus-4-7-1m" - || selectedDescriptor?.shortId === "opus-1m" - || selectedDescriptor?.providerModelId.toLowerCase().includes("[1m]"); const selectedIsOpus48 = selectedDescriptor?.id === "anthropic/claude-opus-4-8"; - 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-1m"; - } - if (!selectedIsOpusOneMillion) return false; - if (reportedModel && isBareClaudeOpus47RuntimeValue(reportedModel)) return true; - return reportedDescriptor?.id === "anthropic/claude-opus-4-7-1m"; - }; for (const candidate of candidates) { const normalized = normalizeReportedModelName(candidate); @@ -5797,7 +5776,7 @@ function resolveClaudeTurnModelPayload( if (selectedIsOpus48 && isBareClaudeOpusRuntimeAlias(normalized)) { return sessionPayload; } - if (shouldPreserveSelectedModel(resolvedCliModelId, normalized)) return sessionPayload; + if (sessionModelId && resolvedCliModelId === sessionModelId) return sessionPayload; const descriptor = getModelById(resolvedCliModelId); const reportedMatchesCanonical = descriptor?.providerModelId === normalizedCliModel; return { @@ -5809,7 +5788,7 @@ function resolveClaudeTurnModelPayload( resolveModelIdFromStoredValue(normalized, "claude") ?? resolveModelIdFromStoredValue(normalizedCliModel, "claude"); if (resolvedModelId) { - if (shouldPreserveSelectedModel(resolvedModelId, normalized)) return sessionPayload; + if (sessionModelId && resolvedModelId === sessionModelId) return sessionPayload; return { model: normalized, modelId: resolvedModelId }; } return { model: normalized }; diff --git a/apps/desktop/src/main/services/chat/droidModelsDiscovery.test.ts b/apps/desktop/src/main/services/chat/droidModelsDiscovery.test.ts index 0b4a47e24..d0cb6a6d0 100644 --- a/apps/desktop/src/main/services/chat/droidModelsDiscovery.test.ts +++ b/apps/desktop/src/main/services/chat/droidModelsDiscovery.test.ts @@ -138,23 +138,28 @@ describe("discoverDroidCliModelDescriptors", () => { { 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-fable-5", displayName: "Claude Fable 5" }, + { id: "claude-fable-5-1", displayName: "Claude Fable 5.1" }, + { id: "claude-opus-4-8", displayName: "Claude Opus 4.8" }, { id: "claude-sonnet-5", displayName: "Claude Sonnet 5" }, ])); const descriptors = await discoverDroidCliModelDescriptors("/mock/bin/droid"); expect(descriptors.map((descriptor) => descriptor.id)).toEqual([ + "droid/claude-fable-5-1", "droid/claude-opus-4-8", "droid/claude-opus-5", "droid/claude-sonnet-5", ]); expect(descriptors.map((descriptor) => descriptor.displayName)).toEqual([ - "Opus 4.8 1M", + "Fable 5.1", + "Opus 4.8", "Opus 5", "Sonnet 5 (1.2x)", ]); expect(descriptors.map((descriptor) => descriptor.providerModelId)).toEqual([ + "claude-fable-5-1", "claude-opus-4-8", "claude-opus-5", "claude-sonnet-5", @@ -168,6 +173,7 @@ describe("discoverDroidCliModelDescriptors", () => { { 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" }, + { id: "claude-fable-5", displayName: "Claude Fable 5" }, ])); const descriptors = await discoverDroidCliModelDescriptors("/mock/bin/droid"); @@ -192,12 +198,20 @@ describe("discoverDroidCliModelDescriptors", () => { defaultReasoningEffort: "high", }); expect(descriptors.find((descriptor) => descriptor.id === "droid/claude-opus-5")?.serviceTiers).toBeUndefined(); + expect(descriptors.find((descriptor) => descriptor.id === "droid/claude-fable-5-1")).toMatchObject({ + displayName: "Fable 5.1", + providerModelId: "claude-fable-5-1", + defaultReasoningEffort: "high", + reasoningTiers: ["low", "medium", "high", "xhigh", "max", "ultracode"], + }); }); it("prefers canonical Droid rows over normalized retired aliases", async () => { mockSpawnAsync.mockResolvedValueOnce(helpFromModels([ { id: "sonnet-4-6", displayName: "Sonnet 4.6" }, { id: "claude-sonnet-5", displayName: "Claude Sonnet 5" }, + { id: "claude-fable-5", displayName: "Claude Fable 5" }, + { id: "claude-fable-5-1", displayName: "Claude Fable 5.1" }, ])); const descriptors = await discoverDroidCliModelDescriptors("/mock/bin/droid"); @@ -206,6 +220,10 @@ describe("discoverDroidCliModelDescriptors", () => { expect(descriptors.find((descriptor) => descriptor.id === "droid/claude-sonnet-5")).toMatchObject({ providerModelId: "claude-sonnet-5", }); + expect(descriptors.filter((descriptor) => descriptor.id === "droid/claude-fable-5-1")).toHaveLength(1); + expect(descriptors.find((descriptor) => descriptor.id === "droid/claude-fable-5-1")).toMatchObject({ + providerModelId: "claude-fable-5-1", + }); }); it("does not invent SDK-only reasoning metadata from CLI help rows", async () => { @@ -298,7 +316,7 @@ describe("discoverDroidCliModelDescriptors", () => { }); expect(descriptors.find((descriptor) => descriptor.id === "droid/custom:claude-opus-4-8")).toMatchObject({ providerModelId: "custom:claude-opus-4-8", - displayName: "Opus 4.8 1M", + displayName: "Opus 4.8", customProxy: true, contextWindow: 1_000_000, maxOutputTokens: 128_000, diff --git a/apps/desktop/src/main/services/chat/droidModelsDiscovery.ts b/apps/desktop/src/main/services/chat/droidModelsDiscovery.ts index b99d57475..30b2d081c 100644 --- a/apps/desktop/src/main/services/chat/droidModelsDiscovery.ts +++ b/apps/desktop/src/main/services/chat/droidModelsDiscovery.ts @@ -191,6 +191,26 @@ function canonicalDroidReplacementForAlias( const normalized = id.trim().toLowerCase(); const idPrefix = options?.customProxy ? "custom:" : ""; const customProxy = options?.customProxy ? { customProxy: true } : {}; + if ( + normalized === "claude-fable-5" + || normalized === "fable" + || normalized === "fable-5" + || normalized === "fable-5.0" + || normalized === "fable-5.1" + || normalized === "fable-5-1" + || (options?.customProxy && normalized === "claude-fable-5-1") + ) { + return { + id: `${idPrefix}claude-fable-5-1`, + displayName: "Fable 5.1", + ...customProxy, + contextWindow: 1_000_000, + maxOutputTokens: 128_000, + capabilities: CANONICAL_DROID_ANTHROPIC_CAPABILITIES, + reasoningTiers: ["low", "medium", "high", "xhigh", "max", "ultracode"], + defaultReasoningEffort: "high", + }; + } if ( normalized === "claude-sonnet-4-6" || normalized === "sonnet-4-6" @@ -229,11 +249,15 @@ function canonicalDroidReplacementForAlias( || normalized === "claude-opus-4-6-fast" || normalized === "opus-4-6" || normalized === "opus-4.6" + || normalized === "claude-opus-4-7-1m" + || normalized === "claude-opus-4-7[1m]" + || normalized === "opus-1m" + || normalized === "opus[1m]" || (options?.customProxy && normalized === "claude-opus-4-8") ) { return { id: `${idPrefix}claude-opus-4-8`, - displayName: "Opus 4.8 1M", + displayName: "Opus 4.8", ...customProxy, contextWindow: 1_000_000, maxOutputTokens: 128_000, @@ -365,7 +389,6 @@ export async function discoverDroidCliModelDescriptors( const row = normalizeDroidDiscoveredModel(rawRow); const trimmed = String(row.id ?? "").trim(); if (!trimmed) continue; - const descriptorKey = trimmed.toLowerCase(); const preferredDuplicateSource = rawRow.id.trim().toLowerCase() === trimmed.toLowerCase(); const descriptor = createDynamicDroidCliModelDescriptor(trimmed, row.displayName, { customProxy: row.customProxy, @@ -376,6 +399,7 @@ export async function discoverDroidCliModelDescriptors( ...(row.maxOutputTokens ? { maxOutputTokens: row.maxOutputTokens } : {}), ...(row.capabilities ? { capabilities: row.capabilities } : {}), }); + const descriptorKey = descriptor.id.toLowerCase(); const existingIndex = descriptorIds.get(descriptorKey); if (existingIndex !== undefined) { const existingPreferred = descriptorPreferredDuplicateSources.get(descriptorKey) ?? false; diff --git a/apps/desktop/src/main/services/opencode/openCodeInventory.test.ts b/apps/desktop/src/main/services/opencode/openCodeInventory.test.ts index ce0c96174..72d19b8c8 100644 --- a/apps/desktop/src/main/services/opencode/openCodeInventory.test.ts +++ b/apps/desktop/src/main/services/opencode/openCodeInventory.test.ts @@ -228,6 +228,24 @@ describe("openCodeInventory", () => { tool_call: true, reasoning: true, }, + "opus-4.7": { + id: "opus-4.7", + name: "Opus 4.7", + tool_call: true, + reasoning: true, + }, + "claude-fable-5": { + id: "claude-fable-5", + name: "Claude Fable 5", + tool_call: true, + reasoning: true, + }, + "claude-fable-5-1": { + id: "claude-fable-5-1", + name: "Claude Fable 5.1", + tool_call: true, + reasoning: true, + }, }, }, ], @@ -243,8 +261,11 @@ describe("openCodeInventory", () => { expect(result.descriptors.map((descriptor) => descriptor.id)).toContain("opencode/anthropic/claude-sonnet-5"); expect(result.descriptors.map((descriptor) => descriptor.id)).toContain("opencode/anthropic/claude-opus-4-8"); + expect(result.descriptors.map((descriptor) => descriptor.id)).toContain("opencode/anthropic/claude-fable-5-1"); expect(result.descriptors.map((descriptor) => descriptor.id)).not.toContain("opencode/anthropic/claude-sonnet-4-6"); expect(result.descriptors.map((descriptor) => descriptor.id)).not.toContain("opencode/anthropic/claude-opus-4-7"); + expect(result.descriptors.map((descriptor) => descriptor.id)).not.toContain("opencode/anthropic/claude-fable-5"); + expect(result.descriptors.filter((descriptor) => descriptor.id === "opencode/anthropic/claude-fable-5-1")).toHaveLength(1); expect(result.descriptors.find((descriptor) => descriptor.id === "opencode/anthropic/claude-sonnet-5")).toMatchObject({ contextWindow: 1_000_000, maxOutputTokens: 128_000, @@ -357,7 +378,7 @@ describe("openCodeInventory", () => { }, "claude-opus-4-8": { id: "claude-opus-4-8", - name: "Claude Opus 4.8 1M", + name: "Claude Opus 4.8", capabilities: { reasoning: true, toolcall: true, @@ -368,6 +389,52 @@ describe("openCodeInventory", () => { fast: {}, }, }, + "claude-opus-4-8-1m": { + id: "claude-opus-4-8-1m", + name: "Claude Opus 4.8 1M", + capabilities: { + reasoning: false, + toolcall: false, + }, + variants: { + stale: {}, + }, + }, + "opus-4.8-1m": { + id: "opus-4.8-1m", + name: "Opus 4.8 1M", + capabilities: { + reasoning: false, + toolcall: false, + }, + variants: { + stale: {}, + }, + }, + "claude-fable-5": { + id: "claude-fable-5", + name: "Claude Fable 5", + capabilities: { + reasoning: false, + toolcall: false, + }, + variants: { + stale: {}, + }, + }, + "claude-fable-5-1": { + id: "claude-fable-5-1", + name: "Claude Fable 5.1", + capabilities: { + reasoning: true, + toolcall: true, + input: { image: true }, + }, + variants: { + high: {}, + fast: {}, + }, + }, }, }, ], @@ -387,6 +454,9 @@ describe("openCodeInventory", () => { 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-5")).toHaveLength(1); expect(result.descriptors.filter((entry) => entry.id === "opencode/anthropic/claude-opus-4-8")).toHaveLength(1); + expect(result.descriptors.map((entry) => entry.id)).not.toContain("opencode/anthropic/claude-opus-4-8-1m"); + expect(result.descriptors.map((entry) => entry.id)).not.toContain("opencode/anthropic/opus-4.8-1m"); + expect(result.descriptors.filter((entry) => entry.id === "opencode/anthropic/claude-fable-5-1")).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"); diff --git a/apps/desktop/src/main/services/opencode/openCodeInventory.ts b/apps/desktop/src/main/services/opencode/openCodeInventory.ts index 4c61127a1..61ed629ea 100644 --- a/apps/desktop/src/main/services/opencode/openCodeInventory.ts +++ b/apps/desktop/src/main/services/opencode/openCodeInventory.ts @@ -7,6 +7,7 @@ import type { EffectiveProjectConfig, ProjectConfigFile } from "../../../shared/ import { createDynamicOpenCodeModelDescriptor, isLocalProviderFamily, + normalizeAnthropicRuntimeAlias, replaceDynamicOpenCodeModelDescriptors, type ModelCapabilities, type ModelDescriptor, @@ -197,13 +198,6 @@ 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); } @@ -278,83 +272,25 @@ function normalizeOpenCodeProviderModel( 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-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-opus-5") { - return { - modelId: "claude-opus-5", - displayName: displayName ?? "Claude Opus 5", - 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 === "opus" - ) { - return { - modelId: "claude-opus-5", - displayName: "Claude Opus 5", - contextWindow: 1_000_000, - maxOutputTokens: 128_000, - capabilities: CANONICAL_ANTHROPIC_MODEL_CAPABILITIES, - reasoningTiers: ["low", "medium", "high", "xhigh", "max"], - defaultReasoningEffort: "high", - serviceTiers: ["fast"], - preferredDuplicateSource, - }; - } - if ( - normalized === "claude-opus-4-7" - || normalized === "opus-4-7" - || normalized === "claude-opus-4-6" - || normalized === "opus-4-6" - || normalized === "opus-4.6" - ) { - 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, - }; + const canonical = normalizeAnthropicRuntimeAlias(modelId); + if (!canonical) { + return { modelId, ...(displayName ? { displayName } : {}), preferredDuplicateSource: false }; } - return { modelId, ...(displayName ? { displayName } : {}), preferredDuplicateSource }; + return { + modelId: canonical.modelId, + displayName: canonical.wasAlias ? canonical.displayName : (displayName ?? canonical.displayName), + contextWindow: canonical.contextWindow, + maxOutputTokens: canonical.maxOutputTokens, + ...(canonical.wasAlias + ? { + reasoningTiers: canonical.reasoningTiers, + defaultReasoningEffort: canonical.defaultReasoningEffort, + serviceTiers: canonical.serviceTiers, + capabilities: canonical.capabilities, + } + : {}), + preferredDuplicateSource: !canonical.wasAlias, + }; } function openCodeSdkErrorMessage(error: unknown): string | null { diff --git a/apps/desktop/src/main/services/usage/usagePricing.ts b/apps/desktop/src/main/services/usage/usagePricing.ts index f5e0bc177..ef751d86f 100644 --- a/apps/desktop/src/main/services/usage/usagePricing.ts +++ b/apps/desktop/src/main/services/usage/usagePricing.ts @@ -67,6 +67,7 @@ const STATIC_TOKEN_PRICES: Record = { "claude-3-5-sonnet": tokenPrice(3, 15, 0.3, 3.75), "claude-3-7-sonnet": tokenPrice(3, 15, 0.3, 3.75), "claude-3-haiku": tokenPrice(0.25, 1.25, 0.03, 0.3), + "claude-fable-5-1": tokenPrice(10, 50, 0.25, 12.5), "claude-fable-5": tokenPrice(10, 50, 1, 12.5), "claude-opus-5": tokenPrice(5, 25, 0.5, 6.25), "claude-opus-4-1": tokenPrice(15, 75, 1.5, 18.75), @@ -144,7 +145,11 @@ const STATIC_TOKEN_PRICES: Record = { */ const BUILTIN_PRICING_ALIASES: Record = { auto: "claude-sonnet-5", - fable: "claude-fable-5", + fable: "claude-fable-5-1", + "fable-5.1": "claude-fable-5-1", + "fable-5-1": "claude-fable-5-1", + "fable-5": "claude-fable-5-1", + "fable-5.0": "claude-fable-5-1", sol: "gpt-5.6-sol", terra: "gpt-5.6-terra", luna: "gpt-5.6-luna", diff --git a/apps/desktop/src/main/services/usage/usageTrackingService.test.ts b/apps/desktop/src/main/services/usage/usageTrackingService.test.ts index 10f733fcd..aaa09c08d 100644 --- a/apps/desktop/src/main/services/usage/usageTrackingService.test.ts +++ b/apps/desktop/src/main/services/usage/usageTrackingService.test.ts @@ -1043,6 +1043,15 @@ describe("resolveTokenPrice", () => { expect(price.cacheWrite).toBe(6.25 / 1_000_000); }); + it("uses Fable 5.1 pricing for short aliases and keeps historical Fable 5 cache read", () => { + expect(resolveTokenPrice("claude-fable-5-1").cacheRead).toBe(0.25 / 1_000_000); + expect(resolveTokenPrice("fable-5.1").cacheRead).toBe(0.25 / 1_000_000); + expect(resolveTokenPrice("fable-5-1").cacheRead).toBe(0.25 / 1_000_000); + expect(resolveTokenPrice("fable-5").cacheRead).toBe(0.25 / 1_000_000); + expect(resolveTokenPrice("fable-5.0").cacheRead).toBe(0.25 / 1_000_000); + expect(resolveTokenPrice("claude-fable-5").cacheRead).toBe(1 / 1_000_000); + }); + it("returns sonnet pricing for sonnet models", () => { const price = resolveTokenPrice("claude-3-5-sonnet"); expect(price.input).toBe(3 / 1_000_000); 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 9e31f8780..6a141a0f9 100644 --- a/apps/desktop/src/renderer/components/shared/ModelPicker/ModelPicker.test.tsx +++ b/apps/desktop/src/renderer/components/shared/ModelPicker/ModelPicker.test.tsx @@ -191,8 +191,8 @@ const SONNET: ModelDescriptor = { const OPUS: ModelDescriptor = { id: "anthropic/claude-opus-4-8", - shortId: "opus-4.8-1m", - displayName: "Claude Opus 4.8 1M", + shortId: "opus-4.8", + displayName: "Claude Opus 4.8", family: "anthropic", authTypes: ["cli-subscription"], contextWindow: 1_000_000, 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 6d1acbe2c..1377e9dd0 100644 --- a/apps/desktop/src/renderer/components/shared/ModelPicker/modelCatalog.test.ts +++ b/apps/desktop/src/renderer/components/shared/ModelPicker/modelCatalog.test.ts @@ -320,14 +320,14 @@ describe("model picker search", () => { const opus = { family: "opencode" as const, providerDisplayName: "opencode", - name: "Claude Opus 4.8 1M", + name: "Claude Opus 4.8", subProvider: "GitHub Copilot", aliases: ["opus-latest"], }; it("builds provider-agnostic searchable text", () => { expect(buildModelPickerSearchText(opus)) - .toBe("claude opus 4.8 1m github copilot opencode opencode opus-latest"); + .toBe("claude opus 4.8 github copilot opencode opencode opus-latest"); }); it("requires every query token while tolerating typos", () => { @@ -346,13 +346,13 @@ describe("model picker search", () => { const favoriteScore = scoreModelPickerSearch({ family: "anthropic", providerDisplayName: "Claude", - name: "Claude Opus 4.8 1M", + name: "Claude Opus 4.8", isFavorite: true, }, "opus 4.8"); const nonFavoriteExactScore = scoreModelPickerSearch({ family: "cursor", providerDisplayName: "Cursor", - name: "Opus 4.8 1M", + name: "Opus 4.8", }, "opus 4.8"); expect(exactScore).not.toBeNull(); diff --git a/apps/desktop/src/renderer/components/terminals/cliLaunch.test.ts b/apps/desktop/src/renderer/components/terminals/cliLaunch.test.ts index b297d37bf..38ed77ef4 100644 --- a/apps/desktop/src/renderer/components/terminals/cliLaunch.test.ts +++ b/apps/desktop/src/renderer/components/terminals/cliLaunch.test.ts @@ -694,21 +694,21 @@ describe("buildTrackedCliStartupCommand", () => { provider: "claude", permissionMode: "default", sessionId: "00000000-0000-0000-0000-000000000001", - model: "anthropic/claude-fable-5", + model: "anthropic/claude-fable-5-1", reasoningEffort: "ultracode", fastMode: true, }); expect(launch.args).toEqual(expect.arrayContaining([ "--model", - "claude-fable-5", + "claude-fable-5-1", "--effort", "xhigh", "--settings", JSON.stringify({ fastMode: true, ultracode: true }), ])); expect(launch.args).not.toEqual(expect.arrayContaining(["--effort", "ultracode"])); - expect(launch.startupCommand).toContain("--model claude-fable-5"); + expect(launch.startupCommand).toContain("--model claude-fable-5-1"); expect(launch.startupCommand).toContain("--effort xhigh"); expect(launch.startupCommand).toContain("ultracode"); }); @@ -1501,11 +1501,11 @@ describe("tracked CLI resume helpers", () => { targetId: "claude-session-1", launch: { permissionMode: "default", - model: "anthropic/claude-fable-5", + model: "anthropic/claude-fable-5-1", reasoningEffort: "ultracode", }, })).toBe( - "claude --permission-mode default --model claude-fable-5 --effort xhigh --settings \"{\\\"ultracode\\\":true}\" --resume claude-session-1", + "claude --permission-mode default --model claude-fable-5-1 --effort xhigh --settings \"{\\\"ultracode\\\":true}\" --resume claude-session-1", ); expect(buildTrackedCliResumeCommand({ diff --git a/apps/desktop/src/renderer/lib/modelOptions.test.ts b/apps/desktop/src/renderer/lib/modelOptions.test.ts index 24a6489c7..44d04f073 100644 --- a/apps/desktop/src/renderer/lib/modelOptions.test.ts +++ b/apps/desktop/src/renderer/lib/modelOptions.test.ts @@ -459,7 +459,7 @@ describe("includeSelectedModelOption", () => { const result = includeSelectedModelOption(baseOptions, "anthropic/claude-opus-4-8"); expect(result.length).toBe(baseOptions.length + 1); expect(result[0].id).toBe("anthropic/claude-opus-4-8"); - expect(result[0].label).toBe("Claude Opus 4.8 1M"); + expect(result[0].label).toBe("Claude Opus 4.8"); // Original options follow expect(result.slice(1)).toEqual(baseOptions); }); diff --git a/apps/desktop/src/shared/claudeCliModels.ts b/apps/desktop/src/shared/claudeCliModels.ts index 86935fb16..4a1d372cc 100644 --- a/apps/desktop/src/shared/claudeCliModels.ts +++ b/apps/desktop/src/shared/claudeCliModels.ts @@ -1,16 +1,22 @@ export type ClaudeCliModelAlias = - | "claude-fable-5" + | "claude-fable-5-1" | "claude-opus-5" | "claude-opus-4-8" | "claude-sonnet-5" - | "claude-haiku-4-5" - | "claude-opus-4-7[1m]"; + | "claude-haiku-4-5"; export const CLAUDE_CLI_MODEL_ALIAS_MAP: Readonly> = { - 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", + fable: "claude-fable-5-1", + "fable-5.1": "claude-fable-5-1", + "fable-5-1": "claude-fable-5-1", + "fable-5": "claude-fable-5-1", + "fable-5.0": "claude-fable-5-1", + "claude-fable-5-1": "claude-fable-5-1", + "anthropic/claude-fable-5-1": "claude-fable-5-1", + "anthropic/claude-fable-5-1-api": "claude-fable-5-1", + "claude-fable-5": "claude-fable-5-1", + "anthropic/claude-fable-5": "claude-fable-5-1", + "anthropic/claude-fable-5-api": "claude-fable-5-1", opus: "claude-opus-5", "opus-5": "claude-opus-5", "opus-5.0": "claude-opus-5", @@ -33,12 +39,12 @@ export const CLAUDE_CLI_MODEL_ALIAS_MAP: Readonly { }); 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"); + expect(modelConfigToServiceModel({ provider: "claude", modelId: "" })).toBe("anthropic/claude-fable-5-1"); }); it("falls back to the default Claude model when modelId is whitespace only", () => { - expect(modelConfigToServiceModel({ provider: "claude", modelId: " " })).toBe("anthropic/claude-fable-5"); + expect(modelConfigToServiceModel({ provider: "claude", modelId: " " })).toBe("anthropic/claude-fable-5-1"); }); 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"); + expect(modelConfigToServiceModel({ provider: "unknown", modelId: "" })).toBe("anthropic/claude-fable-5-1"); }); }); diff --git a/apps/desktop/src/shared/modelRegistry.test.ts b/apps/desktop/src/shared/modelRegistry.test.ts index fb8f121e7..f84f40edb 100644 --- a/apps/desktop/src/shared/modelRegistry.test.ts +++ b/apps/desktop/src/shared/modelRegistry.test.ts @@ -28,6 +28,7 @@ import { resolveModelDescriptorForProvider, resolveModelSlug, selectSupportedReasoningEffort, + normalizeAnthropicRuntimeAlias, } from "./modelRegistry"; import type { ModelDescriptor, ProviderFamily } from "./modelRegistry"; import { describeModelSource } from "../renderer/lib/modelOptions"; @@ -134,7 +135,7 @@ describe("modelRegistry", () => { }); expect(opus).toMatchObject({ id: "opencode/anthropic/claude-opus-4-8", - displayName: "Claude Opus 4.8 1M", + displayName: "Claude Opus 4.8", providerModelId: "anthropic/claude-opus-4-8", openCodeModelId: "claude-opus-4-8", contextWindow: 1_000_000, @@ -393,29 +394,30 @@ describe("modelRegistry", () => { describe("Claude descriptors", () => { it("orders the Claude model registry for picker display", () => { - expect(MODEL_REGISTRY.filter((model) => model.family === "anthropic").slice(0, 6).map((model) => model.id)).toEqual([ - "anthropic/claude-fable-5", + expect(MODEL_REGISTRY.filter((model) => model.family === "anthropic").slice(0, 5).map((model) => model.id)).toEqual([ + "anthropic/claude-fable-5-1", "anthropic/claude-opus-5", "anthropic/claude-sonnet-5", "anthropic/claude-haiku-4-5", "anthropic/claude-opus-4-8", - "anthropic/claude-opus-4-7-1m", ]); - const fable = getModelById("anthropic/claude-fable-5"); + const fable = getModelById("anthropic/claude-fable-5-1"); expect(fable).toBeTruthy(); expect(fable).toMatchObject({ - displayName: "Claude Fable 5", + displayName: "Claude Fable 5.1", shortId: "fable", family: "anthropic", providerRoute: "claude-cli", - providerModelId: "claude-fable-5", + providerModelId: "claude-fable-5-1", contextWindow: 1_000_000, maxOutputTokens: 128_000, inputPricePer1M: 10, outputPricePer1M: 50, + defaultReasoningEffort: "high", }); expect(fable?.reasoningTiers).toEqual(["low", "medium", "high", "xhigh", "max", "ultracode"]); - expect(resolveModelAlias("fable")?.id).toBe("anthropic/claude-fable-5"); + expect(resolveModelAlias("fable")?.id).toBe("anthropic/claude-fable-5-1"); + expect(resolveModelAlias("claude-fable-5")?.id).toBe("anthropic/claude-fable-5-1"); const opus5 = getModelById("anthropic/claude-opus-5"); expect(opus5).toBeTruthy(); @@ -439,8 +441,8 @@ describe("modelRegistry", () => { const opus48 = getModelById("anthropic/claude-opus-4-8"); expect(opus48).toBeTruthy(); expect(opus48).toMatchObject({ - displayName: "Claude Opus 4.8 1M", - shortId: "opus-4.8-1m", + displayName: "Claude Opus 4.8", + shortId: "opus-4.8", family: "anthropic", providerRoute: "claude-cli", providerModelId: "claude-opus-4-8", @@ -451,7 +453,7 @@ describe("modelRegistry", () => { }); expect(opus48?.reasoningTiers).toEqual(["low", "medium", "high", "xhigh", "max", "ultracode"]); expect(opus48?.serviceTiers).toEqual(["fast"]); - expect(getDefaultModelDescriptor("claude")?.id).toBe("anthropic/claude-fable-5"); + expect(getDefaultModelDescriptor("claude")?.id).toBe("anthropic/claude-fable-5-1"); }); it("uses the exact Claude Sonnet 5 runtime model id", () => { @@ -467,22 +469,28 @@ describe("modelRegistry", () => { expect(getRuntimeModelRefForDescriptor(sonnet!, "claude")).toBe("claude-sonnet-5"); }); - it("removes Opus 4.7 basic while keeping Opus 4.7 1M selectable", () => { + it("drops Opus 4.7 entirely and forwards its aliases to Opus 4.8", () => { 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", - providerModelId: "claude-opus-4-7[1m]", - contextWindow: 1_000_000, - serviceTiers: ["fast"], - }); - expect(resolveModelAlias("opus[1m]")?.id).toBe("anthropic/claude-opus-4-7-1m"); + expect(MODEL_REGISTRY.some((model) => model.id === "anthropic/claude-opus-4-7-1m")).toBe(false); + expect(resolveModelAlias("opus[1m]")?.id).toBe("anthropic/claude-opus-4-8"); 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(resolveModelAlias("anthropic/claude-opus-4-6-1m")?.id).toBe("anthropic/claude-opus-4-8"); + expect(resolveModelAlias("anthropic/claude-opus-4-7-1m")?.id).toBe("anthropic/claude-opus-4-8"); 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"); + expect(getModelById("claude-opus-4-6[1m]")?.id).toBe("anthropic/claude-opus-4-8"); + }); + + it("canonicalizes Anthropic runtime aliases including Opus 4.8 1M forms", () => { + expect(normalizeAnthropicRuntimeAlias("claude-fable-5")?.modelId).toBe("claude-fable-5-1"); + expect(normalizeAnthropicRuntimeAlias("fable-5.0")?.modelId).toBe("claude-fable-5-1"); + expect(normalizeAnthropicRuntimeAlias("opus-4.8")?.modelId).toBe("claude-opus-4-8"); + expect(normalizeAnthropicRuntimeAlias("opus-4.8-1m")?.modelId).toBe("claude-opus-4-8"); + expect(normalizeAnthropicRuntimeAlias("claude-opus-4-8-1m")?.modelId).toBe("claude-opus-4-8"); + expect(normalizeAnthropicRuntimeAlias("claude-opus-4-8[1m]")?.modelId).toBe("claude-opus-4-8"); + expect(normalizeAnthropicRuntimeAlias("anthropic/claude-opus-4-8-1m")?.modelId).toBe("claude-opus-4-8"); + expect(normalizeAnthropicRuntimeAlias("claude-opus-4-8")?.wasAlias).toBe(false); + expect(normalizeAnthropicRuntimeAlias("opus-4.8-1m")?.wasAlias).toBe(true); }); it("maps removed Sonnet aliases forward without listing Sonnet 4.6 as a row", () => { @@ -555,7 +563,7 @@ describe("modelRegistry", () => { expect(opus).toMatchObject({ id: "droid/claude-opus-4-8", providerModelId: "claude-opus-4-8", - displayName: "Opus 4.8 1M", + displayName: "Opus 4.8", contextWindow: 1_000_000, maxOutputTokens: 128_000, reasoningTiers: ["low", "medium", "high", "xhigh", "max", "ultracode"], @@ -564,7 +572,7 @@ describe("modelRegistry", () => { 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", + displayName: "Opus 4.8", }); expect(opus5).toMatchObject({ id: "droid/claude-opus-5", diff --git a/apps/desktop/src/shared/modelRegistry.ts b/apps/desktop/src/shared/modelRegistry.ts index 2a9d1a699..c1d8ee709 100644 --- a/apps/desktop/src/shared/modelRegistry.ts +++ b/apps/desktop/src/shared/modelRegistry.ts @@ -250,24 +250,32 @@ export const MODEL_REGISTRY: ModelDescriptor[] = [ // Claude chat surfaces use the native Agent SDK effort ladder. Keep these // tiers aligned with the launch validation path. { - id: "anthropic/claude-fable-5", + id: "anthropic/claude-fable-5-1", shortId: "fable", aliases: [ "fable", + "fable-5.1", + "fable-5-1", + "claude-fable-5-1", + "anthropic/claude-fable-5-1-api", + "fable-5", + "fable-5.0", "claude-fable-5", + "anthropic/claude-fable-5", "anthropic/claude-fable-5-api", ], - displayName: "Claude Fable 5", + displayName: "Claude Fable 5.1", family: "anthropic", authTypes: ["cli-subscription"], contextWindow: 1_000_000, maxOutputTokens: 128_000, capabilities: ALL_CAPS, reasoningTiers: ["low", "medium", "high", "xhigh", "max", "ultracode"], + defaultReasoningEffort: "high", serviceTiers: ["fast"], color: "#D97706", providerRoute: "claude-cli", - providerModelId: "claude-fable-5", + providerModelId: "claude-fable-5-1", cliCommand: "claude", isCliWrapped: true, inputPricePer1M: 10, @@ -353,7 +361,7 @@ export const MODEL_REGISTRY: ModelDescriptor[] = [ }, { id: "anthropic/claude-opus-4-8", - shortId: "opus-4.8-1m", + shortId: "opus-4.8", aliases: [ "claude-opus-4-8", "opus-4.8", @@ -375,28 +383,6 @@ export const MODEL_REGISTRY: ModelDescriptor[] = [ "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", - authTypes: ["cli-subscription"], - contextWindow: 1_000_000, - maxOutputTokens: 128_000, - capabilities: ALL_CAPS, - reasoningTiers: ["low", "medium", "high", "xhigh", "max", "ultracode"], - serviceTiers: ["fast"], - color: "#D97706", - providerRoute: "claude-cli", - providerModelId: "claude-opus-4-8", - cliCommand: "claude", - isCliWrapped: true, - inputPricePer1M: 5, - outputPricePer1M: 25, - costTier: "very_high", - }, - { - id: "anthropic/claude-opus-4-7-1m", - shortId: "opus-1m", - aliases: [ "opus[1m]", "opus-1m", "opus-4-6-1m", @@ -407,17 +393,17 @@ export const MODEL_REGISTRY: ModelDescriptor[] = [ "anthropic/claude-opus-4-6-1m", "anthropic/claude-opus-4-7-1m", ], - displayName: "Claude Opus 4.7 1M", + displayName: "Claude Opus 4.8", family: "anthropic", authTypes: ["cli-subscription"], contextWindow: 1_000_000, maxOutputTokens: 128_000, capabilities: ALL_CAPS, - reasoningTiers: ["low", "medium", "high", "xhigh", "max"], + reasoningTiers: ["low", "medium", "high", "xhigh", "max", "ultracode"], serviceTiers: ["fast"], - color: "#B45309", + color: "#D97706", providerRoute: "claude-cli", - providerModelId: "claude-opus-4-7[1m]", + providerModelId: "claude-opus-4-8", cliCommand: "claude", isCliWrapped: true, inputPricePer1M: 5, @@ -973,7 +959,7 @@ export function decodeOpenCodeRegistryId(id: string): { openCodeProviderId: stri } } -function normalizeAnthropicRuntimeAlias(modelId: string): { +export function normalizeAnthropicRuntimeAlias(modelId: string): { modelId: string; displayName: string; contextWindow: number; @@ -986,6 +972,27 @@ function normalizeAnthropicRuntimeAlias(modelId: string): { } | null { const normalized = modelId.trim().toLowerCase(); if (!normalized.length) return null; + if ( + normalized === "claude-fable-5-1" + || normalized === "fable" + || normalized === "fable-5.1" + || normalized === "fable-5-1" + || normalized === "fable-5" + || normalized === "fable-5.0" + || normalized === "claude-fable-5" + ) { + return { + modelId: "claude-fable-5-1", + displayName: "Claude Fable 5.1", + contextWindow: 1_000_000, + maxOutputTokens: 128_000, + capabilities: ALL_CAPS, + reasoningTiers: ["low", "medium", "high", "xhigh", "max", "ultracode"], + defaultReasoningEffort: "high", + serviceTiers: ["fast"], + wasAlias: normalized !== "claude-fable-5-1", + }; + } if ( normalized === "claude-sonnet-5" || normalized === "claude-sonnet-4-6" @@ -1022,6 +1029,14 @@ function normalizeAnthropicRuntimeAlias(modelId: string): { } if ( normalized === "claude-opus-4-8" + || normalized === "opus-4.8" + || normalized === "opus-4-8" + || normalized === "opus-4.8-1m" + || normalized === "opus-4.8[1m]" + || normalized === "opus-4-8-1m" + || normalized === "claude-opus-4-8-1m" + || normalized === "claude-opus-4-8[1m]" + || normalized === "anthropic/claude-opus-4-8-1m" || normalized === "claude-opus-4-7" || normalized === "opus-4-7" || normalized === "opus-4.7" @@ -1029,10 +1044,17 @@ function normalizeAnthropicRuntimeAlias(modelId: string): { || normalized === "claude-opus-4-6-fast" || normalized === "opus-4-6" || normalized === "opus-4.6" + || normalized === "opus[1m]" + || normalized === "opus-1m" + || normalized === "opus-4-6-1m" + || normalized === "claude-opus-4-7[1m]" + || normalized === "claude-opus-4-7-1m" + || normalized === "claude-opus-4-6[1m]" + || normalized === "claude-opus-4-6-1m" ) { return { modelId: "claude-opus-4-8", - displayName: "Claude Opus 4.8 1M", + displayName: "Claude Opus 4.8", contextWindow: 1_000_000, maxOutputTokens: 128_000, capabilities: ALL_CAPS, @@ -1419,9 +1441,10 @@ function normalizeDroidEffortLabel(value: string): string { } const KNOWN_DROID_COMPACT_DISPLAY_NAMES: Record = { + "claude-fable-5-1": "Fable 5.1", "claude-fable-5": "Fable 5", "claude-opus-5": "Opus 5", - "claude-opus-4-8": "Opus 4.8 1M", + "claude-opus-4-8": "Opus 4.8", "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)", @@ -1885,6 +1908,9 @@ function pickPreferredModel( function pickDefaultClaudeModel(models: ModelDescriptor[]): ModelDescriptor | undefined { return pickPreferredModel(models, [ + (model) => + model.id === "anthropic/claude-fable-5-1" + || /fable-5-1|fable 5\.1/i.test(`${model.displayName} ${model.providerModelId}`), (model) => /\bfable\b/i.test(model.displayName) || /\bfable\b/i.test(model.providerModelId), (model) => /\bsonnet\b/i.test(model.displayName) || /\bsonnet\b/i.test(model.providerModelId), (model) => /\bopus\b/i.test(model.displayName) || /\bopus\b/i.test(model.providerModelId), @@ -1920,7 +1946,8 @@ function pickDefaultCodexModel(models: ModelDescriptor[]): ModelDescriptor | und function pickDefaultOpenCodeModel(models: ModelDescriptor[]): ModelDescriptor | undefined { 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-fable-5-1" || (model.family === "anthropic" && /fable-5-1|fable 5\.1/i.test(`${model.displayName} ${model.providerModelId}`)), + (model) => model.family === "anthropic" && /\bfable\b/i.test(`${model.displayName} ${model.providerModelId}`), (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", diff --git a/apps/ios/ADE/Resources/VoiceGlossary.json b/apps/ios/ADE/Resources/VoiceGlossary.json index c50d44ddc..fd20c06e8 100644 --- a/apps/ios/ADE/Resources/VoiceGlossary.json +++ b/apps/ios/ADE/Resources/VoiceGlossary.json @@ -16,7 +16,7 @@ "Claude Opus 4.8", "Claude Sonnet 5", "Claude Haiku 4.5", - "Claude Fable 5", + "Claude Fable 5.1", "GPT-5.6 Sol", "GPT-5.6 Terra", "GPT-5.6 Luna", diff --git a/apps/ios/ADE/Views/Components/ADEDesignSystem.swift b/apps/ios/ADE/Views/Components/ADEDesignSystem.swift index 3fad63819..74f70146d 100644 --- a/apps/ios/ADE/Views/Components/ADEDesignSystem.swift +++ b/apps/ios/ADE/Views/Components/ADEDesignSystem.swift @@ -139,6 +139,9 @@ enum ADEColor { /// Keys cover both the registry id ("anthropic/claude-sonnet-5") and shortId ("sonnet"). private static let modelColors: [String: UInt32] = [ // Anthropic + "anthropic/claude-fable-5-1": 0xD97706, + "anthropic/claude-fable-5-1-api": 0xD97706, + "claude-fable-5-1": 0xD97706, "anthropic/claude-fable-5": 0xD97706, "anthropic/claude-fable-5-api": 0xD97706, "claude-fable-5": 0xD97706, @@ -148,11 +151,6 @@ enum ADEColor { "opus": 0xD97706, "anthropic/claude-opus-4-8": 0xD97706, "claude-opus-4-8": 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-5": 0x8B5CF6, "claude-sonnet-5": 0x8B5CF6, "sonnet": 0x8B5CF6, @@ -222,32 +220,53 @@ enum ADEColor { case "gpt-5.4-mini", "gpt-5.4-mini-codex", "openai/gpt-5.4-mini", "openai/gpt-5.4-mini-codex": append("openai/gpt-5.4-mini") append("gpt-5.4-mini") - case "fable", "anthropic/claude-fable-5", "anthropic/claude-fable-5-api", "claude-fable-5": + case "fable", "fable-5.1", "fable-5-1", "fable-5", "fable-5.0", + "anthropic/claude-fable-5-1", "anthropic/claude-fable-5-1-api", + "claude-fable-5-1", "opencode/anthropic/claude-fable-5-1", + "anthropic/claude-fable-5", "anthropic/claude-fable-5-api", "claude-fable-5", + "opencode/anthropic/claude-fable-5": + append("anthropic/claude-fable-5-1") + append("anthropic/claude-fable-5-1-api") + append("claude-fable-5-1") + append("opencode/anthropic/claude-fable-5-1") append("anthropic/claude-fable-5") append("anthropic/claude-fable-5-api") append("claude-fable-5") + append("opencode/anthropic/claude-fable-5") append("fable") case "opus", "opus-5", "opus-5.0", "opus-5-0", - "anthropic/claude-opus-5", "anthropic/claude-opus-5-api", "claude-opus-5": + "anthropic/claude-opus-5", "anthropic/claude-opus-5-api", "claude-opus-5", + "opencode/anthropic/opus", "opencode/anthropic/claude-opus-5": append("anthropic/claude-opus-5") append("claude-opus-5") + append("opencode/anthropic/claude-opus-5") append("opus") case "anthropic/claude-opus-4-8", "anthropic/claude-opus-4-8-api", "claude-opus-4-8", "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]": + "anthropic/claude-opus-4-8-1m", "claude-opus-4-8-1m", "claude-opus-4-8[1m]", + "opencode/anthropic/claude-opus-4-8": append("anthropic/claude-opus-4-8") append("claude-opus-4-8") + append("opencode/anthropic/claude-opus-4-8") case "anthropic/claude-opus-4-7", "anthropic/claude-opus-4-7-api", "claude-opus-4-7": 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") + append("anthropic/claude-opus-4-8") + append("claude-opus-4-8") case "sonnet", "anthropic/claude-sonnet-5", "claude-sonnet-5", - "anthropic/claude-sonnet-4-6", "claude-sonnet-4-6": + "anthropic/claude-sonnet-4-6", "claude-sonnet-4-6", + "opencode/anthropic/claude-sonnet-5": append("anthropic/claude-sonnet-5") append("claude-sonnet-5") + append("opencode/anthropic/claude-sonnet-5") append("sonnet") + case "haiku", "anthropic/claude-haiku-4-5", "claude-haiku-4-5", + "opencode/anthropic/claude-haiku-4-5": + append("anthropic/claude-haiku-4-5") + append("claude-haiku-4-5") + append("opencode/anthropic/claude-haiku-4-5") + append("haiku") default: break } @@ -291,6 +310,9 @@ enum ADEColor { /// the same effort actions as desktop and the ADE TUI. private static let modelReasoningTiers: [String: [String]] = [ // Claude + "anthropic/claude-fable-5-1": ["low", "medium", "high", "xhigh", "max", "ultracode"], + "anthropic/claude-fable-5-1-api": ["low", "medium", "high", "xhigh", "max", "ultracode"], + "claude-fable-5-1": ["low", "medium", "high", "xhigh", "max", "ultracode"], "anthropic/claude-fable-5": ["low", "medium", "high", "xhigh", "max", "ultracode"], "anthropic/claude-fable-5-api": ["low", "medium", "high", "xhigh", "max", "ultracode"], "claude-fable-5": ["low", "medium", "high", "xhigh", "max", "ultracode"], @@ -300,11 +322,6 @@ enum ADEColor { "opus": ["low", "medium", "high", "xhigh", "max"], "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-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-5": ["low", "medium", "high", "max"], "claude-sonnet-5": ["low", "medium", "high", "max"], "sonnet": ["low", "medium", "high", "max"], diff --git a/apps/ios/ADE/Views/Work/WorkChatComposerAndInputViews.swift b/apps/ios/ADE/Views/Work/WorkChatComposerAndInputViews.swift index fbbd567ce..9fd73f575 100644 --- a/apps/ios/ADE/Views/Work/WorkChatComposerAndInputViews.swift +++ b/apps/ios/ADE/Views/Work/WorkChatComposerAndInputViews.swift @@ -211,7 +211,9 @@ private func workModelRefsLookFastCapable(_ rawRefs: [String?]) -> Bool { "gpt-5.5", "gpt-5.5-codex", "openai/gpt-5.5", "openai/gpt-5.5-codex", "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", + "fable", "fable-5.1", "fable-5-1", "claude-fable-5-1", "anthropic/claude-fable-5-1", + "opencode/anthropic/claude-fable-5-1", + "claude-fable-5", "anthropic/claude-fable-5", "opencode/anthropic/claude-fable-5", "opus", "opus-5", "opus-5.0", "opus-5-0", "claude-opus-5", "anthropic/claude-opus-5", "anthropic/claude-opus-5-api", "opencode/anthropic/claude-opus-5", "opus-4-7", "claude-opus-4-7", "anthropic/claude-opus-4-7", diff --git a/apps/ios/ADE/Views/Work/WorkModelCatalog.swift b/apps/ios/ADE/Views/Work/WorkModelCatalog.swift index ee848419f..5b84e507e 100644 --- a/apps/ios/ADE/Views/Work/WorkModelCatalog.swift +++ b/apps/ios/ADE/Views/Work/WorkModelCatalog.swift @@ -301,6 +301,15 @@ struct WorkModelCatalogGroupLegacyView: Identifiable, Hashable { private let workModelGroupOrder = ["claude", "codex", "pi", "cursor", "droid", "opencode", "ollama", "lmstudio"] +private func workClaudeFableReasoningEfforts() -> [AgentChatModelReasoningEffort] { + workClaudeOpus5ReasoningEfforts() + [ + AgentChatModelReasoningEffort( + effort: "ultracode", + description: "Maximum Claude Code reasoning for long-horizon work" + ), + ] +} + private func workClaudeOpus5ReasoningEfforts() -> [AgentChatModelReasoningEffort] { [ AgentChatModelReasoningEffort(effort: "low", description: "Fast answers with light reasoning"), @@ -428,7 +437,16 @@ private func workCuratedModelCatalogGroups() -> [WorkModelCatalogGroup] { key: "anthropic", displayName: "Anthropic", models: [ - WorkModelOption(id: "claude-fable-5", displayName: "Claude Fable 5", tier: .flagship, tagline: "Flagship · 1M context", provider: "claude", serviceTiers: ["fast"]), + WorkModelOption( + id: "claude-fable-5-1", + displayName: "Claude Fable 5.1", + tier: .flagship, + tagline: "Flagship · 1M context", + provider: "claude", + reasoningEfforts: workClaudeFableReasoningEfforts(), + defaultReasoningEffort: "high", + serviceTiers: ["fast"] + ), WorkModelOption( id: "claude-opus-5", displayName: "Claude Opus 5", @@ -441,8 +459,7 @@ private func workCuratedModelCatalogGroups() -> [WorkModelCatalogGroup] { ), 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-8", displayName: "Claude Opus 4.8 1M", tier: .flagship, tagline: "Previous Opus · 1M context", 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-opus-4-8", displayName: "Claude Opus 4.8", tier: .flagship, tagline: "Previous Opus · 1M context", provider: "claude", serviceTiers: ["fast"]), ] ) ] @@ -505,6 +522,8 @@ private func workCuratedModelCatalogGroups() -> [WorkModelCatalogGroup] { key: "anthropic", displayName: "Anthropic", models: [ + WorkModelOption(id: "claude-fable-5-1-thinking-high", displayName: "Claude Fable 5.1 Thinking High", tier: .flagship, tagline: "Cursor-routed Fable", provider: "claude"), + WorkModelOption(id: "claude-fable-5-1-thinking-xhigh", displayName: "Claude Fable 5.1 Thinking XHigh", tier: .flagship, tagline: "Cursor-routed Fable", provider: "claude"), 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: "cursor/claude-4.6-sonnet-medium", displayName: "Claude Sonnet 5", tier: .balanced, tagline: "Balanced · 1M context", provider: "cursor"), @@ -596,7 +615,16 @@ private func workCuratedModelCatalogGroups() -> [WorkModelCatalogGroup] { key: "anthropic", 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-fable-5-1", + displayName: "Claude Fable 5.1", + tier: .flagship, + tagline: "Flagship · 1M context", + provider: "claude", + reasoningEfforts: workClaudeFableReasoningEfforts(), + defaultReasoningEffort: "high", + serviceTiers: ["fast"] + ), WorkModelOption( id: "opencode/anthropic/claude-opus-5", displayName: "Claude Opus 5", @@ -609,8 +637,7 @@ private func workCuratedModelCatalogGroups() -> [WorkModelCatalogGroup] { ), 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-8", displayName: "Claude Opus 4.8 1M", tier: .flagship, tagline: "Previous Opus · 1M context", provider: "claude", serviceTiers: ["fast"]), - 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"]), + WorkModelOption(id: "opencode/anthropic/claude-opus-4-8", displayName: "Claude Opus 4.8", tier: .flagship, tagline: "Previous Opus · 1M context", provider: "claude", serviceTiers: ["fast"]), ] ), WorkModelProvider( @@ -972,26 +999,32 @@ private func workModelLookupKeys(_ raw: String?) -> [String] { private func workCanonicalClaudeRegistryId(for raw: String) -> String? { switch raw.trimmingCharacters(in: .whitespacesAndNewlines).lowercased() { - case "fable", "claude-fable-5", "anthropic/claude-fable-5", "anthropic/claude-fable-5-api": - return "anthropic/claude-fable-5" + case "fable", "fable-5.1", "fable-5-1", "claude-fable-5-1", "anthropic/claude-fable-5-1", + "anthropic/claude-fable-5-1-api", "fable-5", "fable-5.0", + "claude-fable-5", "anthropic/claude-fable-5", "anthropic/claude-fable-5-api", + "opencode/anthropic/claude-fable-5-1", "opencode/anthropic/claude-fable-5": + return "anthropic/claude-fable-5-1" case "opus", "opus-5", "opus-5.0", "opus-5-0", "claude-opus-5", "anthropic/claude-opus-5", "anthropic/claude-opus-5-api", "opencode/anthropic/opus", "opencode/anthropic/claude-opus-5": return "anthropic/claude-opus-5" case "claude-opus-4-8", "anthropic/claude-opus-4-8", "anthropic/claude-opus-4-8-api", "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": + "claude-opus-4-8-1m", "claude-opus-4-8[1m]", "anthropic/claude-opus-4-8-1m", + "opencode/anthropic/claude-opus-4-8": return "anthropic/claude-opus-4-8" case "claude-opus-4-7", "anthropic/claude-opus-4-7", "anthropic/claude-opus-4-7-api", "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" + return "anthropic/claude-opus-4-8" case "sonnet", "claude-sonnet-5", "anthropic/claude-sonnet-5", - "claude-sonnet-4-6", "anthropic/claude-sonnet-4-6": + "claude-sonnet-4-6", "anthropic/claude-sonnet-4-6", + "opencode/anthropic/claude-sonnet-5": return "anthropic/claude-sonnet-5" - case "haiku", "claude-haiku-4-5", "anthropic/claude-haiku-4-5": + case "haiku", "claude-haiku-4-5", "anthropic/claude-haiku-4-5", + "opencode/anthropic/claude-haiku-4-5": return "anthropic/claude-haiku-4-5" default: return nil @@ -1000,26 +1033,32 @@ private func workCanonicalClaudeRegistryId(for raw: String) -> String? { private func workClaudeRuntimeModelId(for raw: String) -> String? { switch raw.trimmingCharacters(in: .whitespacesAndNewlines).lowercased() { - case "fable", "claude-fable-5", "anthropic/claude-fable-5", "anthropic/claude-fable-5-api": - return "claude-fable-5" + case "fable", "fable-5.1", "fable-5-1", "claude-fable-5-1", "anthropic/claude-fable-5-1", + "anthropic/claude-fable-5-1-api", "fable-5", "fable-5.0", + "claude-fable-5", "anthropic/claude-fable-5", "anthropic/claude-fable-5-api", + "opencode/anthropic/claude-fable-5-1", "opencode/anthropic/claude-fable-5": + return "claude-fable-5-1" case "opus", "opus-5", "opus-5.0", "opus-5-0", "claude-opus-5", "anthropic/claude-opus-5", "anthropic/claude-opus-5-api", "opencode/anthropic/opus", "opencode/anthropic/claude-opus-5": return "claude-opus-5" case "claude-opus-4-8", "anthropic/claude-opus-4-8", "anthropic/claude-opus-4-8-api", "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": + "claude-opus-4-8-1m", "claude-opus-4-8[1m]", "anthropic/claude-opus-4-8-1m", + "opencode/anthropic/claude-opus-4-8": return "claude-opus-4-8" case "claude-opus-4-7", "anthropic/claude-opus-4-7", "anthropic/claude-opus-4-7-api", "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]" + return "claude-opus-4-8" case "sonnet", "claude-sonnet-5", "anthropic/claude-sonnet-5", - "claude-sonnet-4-6", "anthropic/claude-sonnet-4-6": + "claude-sonnet-4-6", "anthropic/claude-sonnet-4-6", + "opencode/anthropic/claude-sonnet-5": return "claude-sonnet-5" - case "haiku", "claude-haiku-4-5", "anthropic/claude-haiku-4-5": + case "haiku", "claude-haiku-4-5", "anthropic/claude-haiku-4-5", + "opencode/anthropic/claude-haiku-4-5": return "claude-haiku-4-5" default: return nil @@ -1105,26 +1144,32 @@ struct WorkModelIdMatcher { func workKnownModelDisplayName(_ raw: String?) -> String? { switch raw?.trimmingCharacters(in: .whitespacesAndNewlines).lowercased() ?? "" { - case "fable", "anthropic/claude-fable-5", "claude-fable-5", "opencode/anthropic/claude-fable-5": - return "Claude Fable 5" + case "fable", "fable-5.1", "fable-5-1", "fable-5", "fable-5.0", + "anthropic/claude-fable-5-1", "anthropic/claude-fable-5-1-api", "claude-fable-5-1", + "opencode/anthropic/claude-fable-5-1", "anthropic/claude-fable-5", "anthropic/claude-fable-5-api", + "claude-fable-5", "opencode/anthropic/claude-fable-5": + return "Claude Fable 5.1" case "opus", "opus-5", "opus-5.0", "opus-5-0", "anthropic/claude-opus-5", "anthropic/claude-opus-5-api", "claude-opus-5", "opencode/anthropic/opus", "opencode/anthropic/claude-opus-5": return "Claude Opus 5" case "anthropic/claude-opus-4-8", "anthropic/claude-opus-4-8-api", "claude-opus-4-8", "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" + "anthropic/claude-opus-4-8-1m", "claude-opus-4-8-1m", "claude-opus-4-8[1m]", + "opencode/anthropic/claude-opus-4-8": + return "Claude Opus 4.8" case "anthropic/claude-opus-4-7", "anthropic/claude-opus-4-7-api", "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" + return "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]", "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" + return "Claude Opus 4.8" case "sonnet", "anthropic/claude-sonnet-5", "claude-sonnet-5", "cursor/claude-4.6-sonnet-medium", - "anthropic/claude-sonnet-4-6", "claude-sonnet-4-6": + "anthropic/claude-sonnet-4-6", "claude-sonnet-4-6", + "opencode/anthropic/claude-sonnet-5": return "Claude Sonnet 5" - case "haiku", "anthropic/claude-haiku-4-5", "claude-haiku-4-5": + case "haiku", "anthropic/claude-haiku-4-5", "claude-haiku-4-5", + "opencode/anthropic/claude-haiku-4-5": return "Claude Haiku 4.5" case "sol", "gpt-5.6-sol", "openai/gpt-5.6-sol": return "GPT-5.6 Sol" diff --git a/apps/ios/ADETests/ADETests.swift b/apps/ios/ADETests/ADETests.swift index c27385a70..34b02e4e3 100644 --- a/apps/ios/ADETests/ADETests.swift +++ b/apps/ios/ADETests/ADETests.swift @@ -20452,7 +20452,7 @@ final class ADETests: XCTestCase { let groups = workModelCatalogGroups(currentModelId: "", currentProvider: "codex") let claudeGroup = groups.first(where: { $0.key == "claude" }) let anthropicProvider = claudeGroup?.providers.first(where: { $0.key == "anthropic" }) - let fable = anthropicProvider?.models.first(where: { $0.id == "claude-fable-5" }) + let fable = anthropicProvider?.models.first(where: { $0.id == "claude-fable-5-1" }) let opus5 = anthropicProvider?.models.first(where: { $0.id == "claude-opus-5" }) let opus48 = anthropicProvider?.models.first(where: { $0.id == "claude-opus-4-8" }) let openCodeAnthropic = groups @@ -20469,26 +20469,31 @@ final class ADETests: XCTestCase { let gpt55 = openAIProvider?.models.first(where: { $0.id == "gpt-5.5" }) XCTAssertEqual(anthropicProvider?.models.map(\.id), [ - "claude-fable-5", + "claude-fable-5-1", "claude-opus-5", "claude-sonnet-5", "claude-haiku-4-5", "claude-opus-4-8", - "claude-opus-4-7-1m", ]) XCTAssertEqual(openCodeAnthropic?.models.map(\.id), [ - "opencode/anthropic/claude-fable-5", + "opencode/anthropic/claude-fable-5-1", "opencode/anthropic/claude-opus-5", "opencode/anthropic/claude-sonnet-5", "opencode/anthropic/claude-haiku-4-5", "opencode/anthropic/claude-opus-4-8", - "opencode/anthropic/claude-opus-4-7-1m", ]) - XCTAssertEqual(workDefaultCatalogModelId(provider: "claude"), "claude-fable-5") - XCTAssertEqual(fable?.displayName, "Claude Fable 5") + XCTAssertEqual(workDefaultCatalogModelId(provider: "claude"), "claude-fable-5-1") + XCTAssertEqual(fable?.displayName, "Claude Fable 5.1") XCTAssertEqual(fable?.tier, .flagship) XCTAssertEqual(fable?.tagline, "Flagship · 1M context") - XCTAssertNotNil(ADEColor.modelBrand(for: "claude-fable-5")) + XCTAssertEqual(fable?.defaultReasoningEffort, "high") + XCTAssertNotNil(ADEColor.modelBrand(for: "claude-fable-5-1")) + XCTAssertTrue(workModelIdsEquivalent("opencode/anthropic/claude-fable-5-1", "claude-fable-5-1")) + XCTAssertTrue(workModelIdsEquivalent("fable-5", "claude-fable-5-1")) + XCTAssertEqual(workKnownModelDisplayName("fable-5"), "Claude Fable 5.1") + XCTAssertEqual(workKnownModelDisplayName("fable-5.0"), "Claude Fable 5.1") + XCTAssertEqual(workKnownModelDisplayName("opencode/anthropic/claude-fable-5-1"), "Claude Fable 5.1") + XCTAssertNotNil(ADEColor.modelBrand(for: "opencode/anthropic/claude-fable-5-1")) XCTAssertEqual(opus5?.displayName, "Claude Opus 5") XCTAssertEqual(opus5?.tagline, "Agentic coding · 1M context") XCTAssertEqual(opus5?.reasoningEfforts.map(\.effort), ["low", "medium", "high", "xhigh", "max"]) @@ -20500,7 +20505,7 @@ final class ADETests: XCTestCase { XCTAssertEqual(droidOpus5?.reasoningEfforts.map(\.effort), ["low", "medium", "high", "xhigh", "max"]) XCTAssertEqual(droidOpus5?.defaultReasoningEffort, "high") XCTAssertFalse(droidOpus5?.supportsCodexFastMode == true) - XCTAssertEqual(opus48?.displayName, "Claude Opus 4.8 1M") + XCTAssertEqual(opus48?.displayName, "Claude Opus 4.8") XCTAssertEqual(opus48?.tier, .flagship) XCTAssertEqual(opus48?.tagline, "Previous Opus · 1M context") XCTAssertNotNil(ADEColor.modelBrand(for: "claude-opus-4-8")) @@ -20551,6 +20556,9 @@ final class ADETests: XCTestCase { } func testMobileComposerReasoningTiersMirrorDesktopRegistry() { + XCTAssertEqual(ADEColor.reasoningTiers(for: "anthropic/claude-fable-5-1"), ["low", "medium", "high", "xhigh", "max", "ultracode"]) + XCTAssertEqual(ADEColor.reasoningTiers(for: "anthropic/claude-fable-5-1-api"), ["low", "medium", "high", "xhigh", "max", "ultracode"]) + XCTAssertEqual(ADEColor.reasoningTiers(for: "claude-fable-5-1"), ["low", "medium", "high", "xhigh", "max", "ultracode"]) XCTAssertEqual(ADEColor.reasoningTiers(for: "anthropic/claude-fable-5"), ["low", "medium", "high", "xhigh", "max", "ultracode"]) XCTAssertEqual(ADEColor.reasoningTiers(for: "anthropic/claude-fable-5-api"), ["low", "medium", "high", "xhigh", "max", "ultracode"]) XCTAssertEqual(ADEColor.reasoningTiers(for: "claude-fable-5"), ["low", "medium", "high", "xhigh", "max", "ultracode"]) @@ -20563,7 +20571,7 @@ final class ADETests: XCTestCase { 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", "ultracode"]) XCTAssertEqual(ADEColor.reasoningTiers(for: "claude-opus-4-7"), ["low", "medium", "high", "xhigh", "max", "ultracode"]) - XCTAssertEqual(ADEColor.reasoningTiers(for: "opus[1m]"), ["low", "medium", "high", "xhigh", "max"]) + XCTAssertEqual(ADEColor.reasoningTiers(for: "opus[1m]"), ["low", "medium", "high", "xhigh", "max", "ultracode"]) XCTAssertEqual(ADEColor.reasoningTiers(for: "anthropic/claude-sonnet-5"), ["low", "medium", "high", "max"]) XCTAssertNil(ADEColor.reasoningTiers(for: "claude-haiku-4-5")) XCTAssertEqual(ADEColor.reasoningTiers(for: "sol"), ["low", "medium", "high", "xhigh", "max", "ultra"]) @@ -21305,6 +21313,7 @@ final class ADETests: XCTestCase { XCTAssertTrue(workModelIdsEquivalent("anthropic/claude-opus-5-api", "claude-opus-5")) XCTAssertTrue(workModelIdsEquivalent("opencode/anthropic/opus", "claude-opus-5")) XCTAssertTrue(workModelIdsEquivalent("opencode/anthropic/claude-opus-5", "claude-opus-5")) + XCTAssertTrue(workModelIdsEquivalent("opencode/anthropic/claude-opus-4-8", "claude-opus-4-8")) XCTAssertEqual(workKnownModelDisplayName("anthropic/claude-opus-5-api"), "Claude Opus 5") XCTAssertEqual(workKnownModelDisplayName("opencode/anthropic/opus"), "Claude Opus 5") XCTAssertTrue(workModelIdsEquivalent("claude-opus-4-6", "claude-opus-4-8")) @@ -21314,7 +21323,7 @@ final class ADETests: XCTestCase { 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") + XCTAssertEqual(workKnownModelDisplayName("anthropic/claude-opus-4-6"), "Claude Opus 4.8") } func testExtractWorkNavigationTargetsFindsFilePathsAndPullRequestNumbers() { diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index 2363f7bec..d1f18ab3a 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -717,10 +717,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 OpenAI/Codex block is ordered GPT-5.6 Sol, Terra, Luna, then the retained GPT-5.5 and older rows; `pickDefaultCodexModel` chooses the newest Sol row, so new Codex sessions default to GPT-5.6 Sol. All three GPT-5.6 descriptors have a 372k context window and advertise Fast. Sol and Terra expose `low | medium | high | xhigh | max | ultra`; Luna exposes `low | medium | high | xhigh | max`. The product labels those tiers Light, Medium, High, Extra High, Max, and (for Sol/Terra) Ultra. Runtime app-server ladders pass through in their advertised order, including Max. `defaultReasoningEffort` is `low` for Sol and `medium` for Terra/Luna and is honored by desktop, TUI, iOS, CTO, review, and handoff pickers. The Claude block is ordered for every picker as Fable 5, Opus 5, Sonnet 5, Haiku 4.5, Opus 4.8 1M, then Opus 4.7 1M. Opus 5 uses provider model `claude-opus-5` (1,000,000 context / 128,000 max output), defaults to `high` effort, and advertises Fast. 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. The generic `opus` alias resolves to Opus 5. Opus 4.8 remains selectable after Haiku, while 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, GPT-5.6 and older fast-capable Codex 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 OpenAI/Codex block is ordered GPT-5.6 Sol, Terra, Luna, then the retained GPT-5.5 and older rows; `pickDefaultCodexModel` chooses the newest Sol row, so new Codex sessions default to GPT-5.6 Sol. All three GPT-5.6 descriptors have a 372k context window and advertise Fast. Sol and Terra expose `low | medium | high | xhigh | max | ultra`; Luna exposes `low | medium | high | xhigh | max`. The product labels those tiers Light, Medium, High, Extra High, Max, and (for Sol/Terra) Ultra. Runtime app-server ladders pass through in their advertised order, including Max. `defaultReasoningEffort` is `low` for Sol and `medium` for Terra/Luna and is honored by desktop, TUI, iOS, CTO, review, and handoff pickers. The Claude block is ordered for every picker as Fable 5.1, Opus 5, Sonnet 5, Haiku 4.5, then Opus 4.8. Fable 5.1 uses provider model `claude-fable-5-1` (1,000,000 context / 128,000 max output), defaults to `high` effort, and advertises Fast plus `ultracode`. Opus 5 uses provider model `claude-opus-5` (1,000,000 context / 128,000 max output), defaults to `high` effort, and advertises Fast. Sonnet 5 uses provider model `claude-sonnet-5` (1,000,000 context / 128,000 max output); removed Sonnet 4.6, Fable 5, basic Opus 4.7, and Opus 4.7 1M ids resolve forward as compatibility aliases but do not appear as selectable rows. The generic `opus` alias resolves to Opus 5. Opus 4.8 remains selectable after Haiku and is labelled without a 1M suffix; retired `opus[1m]` / `claude-opus-4-7[1m]` aliases resolve to it. `ModelDescriptor.serviceTiers?: string[]` advertises optional service tiers (today: `"fast"`, set on Fable/Opus, GPT-5.6 and older fast-capable Codex 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" | "pi"`. Cursor, Droid, and Pi 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 5 and Opus 4.7 advertise `low | medium | high | xhigh | max`; Fable and Opus 4.8 advertise `low | medium | high | xhigh | max | ultracode`; Sonnet 5 advertises `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 Opus 5 advertises `low | medium | high | xhigh | max`; Fable 5.1 and Opus 4.8 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 5a69443fd..aee11ea07 100644 --- a/docs/features/chat/README.md +++ b/docs/features/chat/README.md @@ -87,7 +87,7 @@ for its separate RPC, sync, storage, and UI contracts. | `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. User images are materialized here from attachment paths (`workerAttachmentImages.ts`) rather than as base64 on the JSON IPC pipe. | | `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`. User images on send are path (or tiny inline `data`) references (`DroidSdkUserImage`), not inlined screenshot bytes; remote `url` images are not part of the union because Droid's stream API has no URL form. | | `apps/desktop/src/main/services/chat/droidSdkEventMapper.ts` | Per-session `DroidSdkEventMapperState` + `mapDroidSdkMessageToChatEvents` / `mapDroidSdkRunResultToDoneEvent`. Tracks streaming text/thinking/image item ids, maps tool calls and results, maps `mission_worker_started` / `mission_worker_completed` notifications to provider-neutral subagent lifecycle events keyed by worker session id, surfaces image content as compact generation rows, and reports token usage. Replaces the deleted `droidAcpPool.ts` + `droidAcpEventMapper` path. | -| `apps/desktop/src/main/services/chat/droidModelsDiscovery.ts` | SDK-driven model probe (`listDroidModelsFromSdk`) plus the `/config.json` custom-proxy merge (`~/.factory` unless `FACTORY_HOME_OVERRIDE` is set — see [Provider config homes](agent-routing.md#provider-config-homes)). Normalizes the generic `opus` row to Opus 5 with its `high` default reasoning effort and Fast capability, while retired factory Claude ids still resolve forward (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/chat/droidModelsDiscovery.ts` | SDK-driven model probe (`listDroidModelsFromSdk`) plus the `/config.json` custom-proxy merge (`~/.factory` unless `FACTORY_HOME_OVERRIDE` is set — see [Provider config homes](agent-routing.md#provider-config-homes)). Normalizes the generic `opus` row to Opus 5 with its `high` default reasoning effort and Fast capability, while retired factory Claude ids still resolve forward (Sonnet 4.6 -> Sonnet 5, basic Opus 4.7 / Opus 4.7 1M -> 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/chat/piSdkPool.ts` | Pi adapter. Forks `piSdkWorker` per session key, exposes `acquirePiSdkConnection` / `releasePiSdkConnection`, and proxies prompts, model/thinking changes, compaction, inventory reads, `login` / `cancelLogin`, and `respondToUi`. Also routes the reverse-RPC UI channel onto `bridge.onUiRequest` / `onUiNotice` / `onUiCancel`; when no `onUiRequest` handler is installed the pool answers `{ ok: false }` immediately, so an unattended worker fails closed instead of hanging a turn. | | `apps/desktop/src/main/services/chat/piSdkWorker.ts` | Node worker that hosts the user's own Pi installation (resolved at runtime, never statically imported). Owns the Pi agent session, the model runtime, `ModelRuntime.login`, tool assembly (`ask_user` plus approval-gated rebuilds of Pi's built-ins), extension binding, and the settings manager that pins `projectTrusted: false`. User images are materialized here from attachment paths (`workerAttachmentImages.ts`) rather than as base64 on the JSON IPC pipe. | | `apps/desktop/src/main/services/chat/piSdkProtocol.ts` | Worker IPC types and validators, at protocol version 2. Adds the `ui_request` / `ui_notice` / `ui_cancel` / `ui_response` frames and `login` / `login_cancel` on top of version 1, plus the `extensions` / `askUserTool` / `approvalTools` init flags and the `extensions` / `extensionsError` / `ungateableTools` fields on `PiSdkReady`. User images on send/steer/follow_up are path (or tiny inline `data`) references, not inlined screenshot bytes; remote `url` images are rejected because Pi's prompt API has no URL form. Every frame is validated in both directions. | @@ -98,7 +98,7 @@ for its separate RPC, sync, storage, and UI contracts. | `apps/desktop/src/main/services/chat/piSessionOwnership.ts` | The `.ade-owner` durable claim — `{ owner, ownerSessionId }`, never removed, because chat and the tracked CLI share one store and creation-time proximity cannot tell their sessions apart. `piSessionIsAdoptableByTerminal` leaves unclaimed sessions adoptable (a `pi` run started outside ADE) and `piSessionCouldBelongToTerminal` rejects a stored resume pointer at a session older than the terminal itself. | | `apps/desktop/src/main/services/ai/piAuthService.ts` | In-app Pi sign-in on a dedicated inventory-only worker: provider enumeration, one flow per provider, prompt/notice fan-out through `addPiAuthStatusListener`, prompt answers, cancellation, and a 10-minute bound. A user-pressed cancel gives Pi `PI_LOGIN_CANCEL_GRACE_MS` to report a login it had already completed; a supersede (a replacement attempt) settles the outgoing flow at once and silently, so it cannot clear the card the newer attempt owns. Relays credentials, never retains them. | | `apps/desktop/src/main/services/opencode/openCodeBinaryManager.ts` | Resolves the OpenCode CLI, and the order is **pinned runtime first**: the machine tools cache (`cachedToolEntryPath("opencode")`), then the bundled platform package, and only then a user-installed binary on PATH / `~/.opencode/bin`. `ADE_DISABLE_BUNDLED_OPENCODE=1` skips both pinned candidates and selects the user install outright; `ADE_OPENCODE_BUNDLE_ROOT` is a hermetic override that suppresses the cache and the user install as well. `resolveOpenCodeBinary` returns the `OpenCodeBinarySource` that won, so callers can tell a pinned runtime from a user install. 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 exposes `modelIds` — the selectable ids for connected providers only. (Descriptors are still registered for every catalog entry, so an id from an unconnected provider resolves; it just is not offered as a pick.) Anthropic rows normalize generic `opus` to Opus 5 with its `high` default reasoning effort and Fast capability; retired Sonnet 4.6 / basic Opus 4.7 ids still resolve to Sonnet 5 / Opus 4.8 so runtime catalogs cannot reintroduce removed picker rows. `OpenCodeProviderInfo.availableModelCount` exposes the connected count separately from `modelCount`. **Cross-launch persistence:** `persistOpenCodeInventory(projectRoot, providers)` writes each successful probe's provider list (keyed by project root, with `savedAt`) to `opencode-inventory-cache.json` under Electron `userData` (override via `ADE_OPENCODE_INVENTORY_CACHE_FILE`); on a cold start the Settings page reloads that persisted list flagged stale (`opencodeProvidersStale`) so the ~160-provider chip cloud renders immediately instead of blanking until the first live probe (stale-while-revalidate). Writes are best-effort and never break the probe. | +| `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 exposes `modelIds` — the selectable ids for connected providers only. (Descriptors are still registered for every catalog entry, so an id from an unconnected provider resolves; it just is not offered as a pick.) Anthropic rows normalize generic `opus` to Opus 5 with its `high` default reasoning effort and Fast capability; retired Sonnet 4.6 / Fable 5 / Opus 4.7 ids still resolve to Sonnet 5 / Fable 5.1 / Opus 4.8 so runtime catalogs cannot reintroduce removed picker rows. `OpenCodeProviderInfo.availableModelCount` exposes the connected count separately from `modelCount`. **Cross-launch persistence:** `persistOpenCodeInventory(projectRoot, providers)` writes each successful probe's provider list (keyed by project root, with `savedAt`) to `opencode-inventory-cache.json` under Electron `userData` (override via `ADE_OPENCODE_INVENTORY_CACHE_FILE`); on a cold start the Settings page reloads that persisted list flagged stale (`opencodeProvidersStale`) so the ~160-provider chip cloud renders immediately instead of blanking until the first live probe (stale-while-revalidate). Writes are best-effort and never break the probe. | | `apps/desktop/src/main/services/opencode/openCodeRuntime.ts` | OpenCode server session runtime: the single `@opencode-ai/sdk/v2/client` every OpenCode call goes through, session handles over shared/dedicated server leases, the `buildOpenCodeConfig` / `OPENCODE_CONFIG_CONTENT` permission config (`OpenCodePermissionKey`), prompt assembly (`buildOpenCodePromptParts`), event-stream helpers, and the one-shot `runOpenCodeTextPrompt` behind `runProviderTask`. Three contracts are load-bearing. **Re-attach is 404-gated:** when a persisted session id fails `session.get`, only a confirmed "session does not exist" (`isOpenCodeNotFoundError`, HTTP 404 / `NotFoundError`, walking a bounded chain of `cause`/`body`/`error`/`data` wrapper shapes with any explicit non-404 status sealing the walk) may fall through to fresh-session creation; any other failure closes the server lease and surfaces, because silently starting an empty session would strand the user's thread. **System prompts ride the prompt body:** ADE's system prompt travels on the prompt body's first-class `system` field and is never injected as a synthetic/ignored text part — OpenCode drops `ignored` parts from model context entirely, so a part-shaped transport silently never reaches the model. **The `/event` subscription is bounded:** `openCodeEventStream` passes `OPENCODE_SSE_MAX_RETRY_ATTEMPTS` and forwards `onSseError`, because the generated SSE client reconnects forever by default and OpenCode sends no event ids, so a reconnect replays nothing. | | `apps/desktop/src/main/services/opencode/openCodeAuthService.ts` | Drives the managed OpenCode server's auth API for subscription connect + API-key seeding, reusing the shared inventory server lease (never spawning its own process). `listAuthMethods` reads `GET /provider/auth`; `startOAuth` authorizes (`POST /provider/{id}/oauth/authorize`), opens the returned URL, and polls `provider.list().connected` every 2s until connected or a 5-min timeout, re-probing inventory on success; `cancelOAuth` stops the poller; `setProviderKey` does `PUT /auth/{id}` and mirrors the key into ADE's `apiKeyStore` so it is re-injected on future launches. One flow per `providerId` at a time (a new start supersedes the prior). Transitions are published through `addOpenCodeOAuthStatusListener` (`pending`/`connected`/`cancelled`/`timeout`/`failed`), a multi-sink fan-out so the same event reaches desktop windows and the remote/web runtime event buffer. Seeded credentials land in ADE's isolated managed OpenCode dir (XDG roots under `userData/opencode-runtime/xdg-v*`), never the user's `~/.local/share/opencode`. | | `apps/desktop/src/shared/chatTranscript.ts` | Pure JSON-lines parser for `AgentChatEventEnvelope` values. Used by both the main process and the renderer. | diff --git a/docs/features/chat/agent-routing.md b/docs/features/chat/agent-routing.md index bd4084980..017a2437e 100644 --- a/docs/features/chat/agent-routing.md +++ b/docs/features/chat/agent-routing.md @@ -102,18 +102,21 @@ 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 model descriptors advertise their provider-native ladders to model-specific -pickers. Opus 5 and Opus 4.7 expose `low|medium|high|xhigh|max`; Fable +pickers. Opus 5 exposes `low|medium|high|xhigh|max`; Fable 5.1 and Opus 4.8 add `ultracode`; Sonnet 5 exposes `low|medium|high|max`; Haiku 4.5 has no reasoning control. The Claude registry is ordered as -Fable 5, Opus 5, Sonnet 5, Haiku 4.5, Opus 4.8 1M, then Opus 4.7 1M. +Fable 5.1, Opus 5, Sonnet 5, Haiku 4.5, then Opus 4.8. Opus 5 selects provider model `claude-opus-5`, defaults to `high` effort, and exposes `low|medium|high|xhigh|max` plus Fast Mode. +Fable 5.1 selects provider model `claude-fable-5-1`, defaults to `high` +effort, and exposes `low|medium|high|xhigh|max|ultracode` plus Fast Mode. 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 the generic `opus` alias selects Opus 5 and -`opus[1m]` / `opus-1m` still target Opus 4.7 1M. +rows. The basic Opus 4.7 row and the Opus 4.7 1M row are both removed; +their old aliases, including `opus[1m]` / `opus-1m`, resolve to Opus 4.8. +The generic `opus` alias selects Opus 5. Retired Fable 5 ids resolve to +Fable 5.1. Opus 4.8 is labelled without a 1M suffix. Passthrough to the provider config is unchanged (the tier string is forwarded directly to the CLI / SDK, with no synthesized token budgets). diff --git a/docs/features/sync-and-multi-device/ios-companion.md b/docs/features/sync-and-multi-device/ios-companion.md index e9690312c..14a1cf773 100644 --- a/docs/features/sync-and-multi-device/ios-companion.md +++ b/docs/features/sync-and-multi-device/ios-companion.md @@ -3481,9 +3481,9 @@ the stats and shows update guidance. (`WorkModelCatalog.swift`, mirroring desktop's `resolveCliProviderForModel`), so adding a provider means updating both the runtime registry and the phone's model-catalog grouping - together; the Claude picker order mirrors desktop (Fable 5, Opus 5, - Sonnet 5, Haiku 4.5, Opus 4.8 1M, Opus 4.7 1M) and legacy Sonnet 4.6 / - basic Opus 4.7 selections normalize forward instead of appearing as + together; the Claude picker order mirrors desktop (Fable 5.1, Opus 5, + Sonnet 5, Haiku 4.5, Opus 4.8) and legacy Sonnet 4.6 / + Fable 5 / Opus 4.7 selections normalize forward instead of appearing as rows, while the generic `opus` alias resolves to Opus 5. The OpenAI picker always promotes GPT-5.6 Sol, Terra, Luna in that order even when a host returns another order; Sol is the fallback default