From 98037bdee74750f238f8101d987efdecf390bb13 Mon Sep 17 00:00:00 2001 From: Arul Sharma <31745423+arul28@users.noreply.github.com> Date: Tue, 1 Sep 2026 15:25:54 -0400 Subject: [PATCH 1/2] feat(desktop): add Claude Fable 5.1 and restack the catalog Problem: ADE still listed Fable 5 and Opus 4.7 1M after Anthropic shipped Fable 5.1. Cause: The Claude registry, CLI aliases, iOS catalog, and related surfaces had not been updated for the 5.1 launch. Change and boundary: Fable 5.1 is the default Claude model. Pickers list Fable 5.1, Opus 5, Sonnet 5, Haiku 4.5, then Opus 4.8 without a 1M suffix. Retired Fable 5 and Opus 4.7 ids, including 1M aliases, resolve forward. Cursor and Droid stay discovery-driven; Droid does not invent a static Fable 5.1 factory row. Historical Fable 5 usage keeps the old cache-read rate. Verification: desktop vitest 365 catalog/picker tests, 4 Claude alias session tests, 80 registry/Droid/OpenCode tests; ADE CLI TUI 165 tests. Co-authored-by: Cursor --- CHANGELOG.md | 4 + CLAUDE.md | 2 +- .../src/tuiClient/__tests__/appInput.test.ts | 4 +- .../ModelPicker/modelPickerLayout.test.ts | 5 +- .../resources/voice/voice-glossary.json | 1 + .../main/services/ai/claudeModelUtils.test.ts | 10 +- .../src/main/services/ai/claudeModelUtils.ts | 4 +- .../services/chat/agentChatService.test.ts | 12 +-- .../main/services/chat/agentChatService.ts | 39 ++----- .../chat/droidModelsDiscovery.test.ts | 24 ++++- .../services/chat/droidModelsDiscovery.ts | 28 ++++- .../opencode/openCodeInventory.test.ts | 48 ++++++++- .../services/opencode/openCodeInventory.ts | 102 ++++-------------- .../src/main/services/usage/usagePricing.ts | 3 +- .../shared/ModelPicker/ModelPicker.test.tsx | 4 +- .../shared/ModelPicker/modelCatalog.test.ts | 8 +- .../components/terminals/cliLaunch.test.ts | 10 +- .../src/renderer/lib/modelOptions.test.ts | 2 +- apps/desktop/src/shared/claudeCliModels.ts | 39 +++---- apps/desktop/src/shared/modelProfiles.test.ts | 6 +- apps/desktop/src/shared/modelRegistry.test.ts | 45 ++++---- apps/desktop/src/shared/modelRegistry.ts | 87 +++++++++------ apps/ios/ADE/Resources/VoiceGlossary.json | 1 + .../Views/Components/ADEDesignSystem.swift | 26 ++--- .../Work/WorkChatComposerAndInputViews.swift | 4 +- .../ios/ADE/Views/Work/WorkModelCatalog.swift | 67 +++++++++--- apps/ios/ADETests/ADETests.swift | 24 +++-- docs/ARCHITECTURE.md | 4 +- docs/features/chat/README.md | 4 +- docs/features/chat/agent-routing.md | 13 ++- .../sync-and-multi-device/ios-companion.md | 6 +- 31 files changed, 351 insertions(+), 285 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index bc031ba1f2..30f0b5ed8b 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 f46de5b606..fb2cd2c1d2 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 86176254fb..46bc70527b 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 4677a1fb60..92a3d351b8 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 ef85720a2e..1b89c90ff6 100644 --- a/apps/desktop/resources/voice/voice-glossary.json +++ b/apps/desktop/resources/voice/voice-glossary.json @@ -15,6 +15,7 @@ "Claude Opus 4.8", "Claude Sonnet 5", "Claude Haiku 4.5", + "Claude Fable 5.1", "Claude Fable 5", "GPT-5.6 Sol", "GPT-5.6 Terra", diff --git a/apps/desktop/src/main/services/ai/claudeModelUtils.test.ts b/apps/desktop/src/main/services/ai/claudeModelUtils.test.ts index c048662cf5..899d3ff6bc 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 7eb5a94f2d..aa949fef10 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 7ffd72db9e..2ef850e489 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 d069060364..fe102929ae 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 0b4a47e248..d0cb6a6d02 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 b99d574758..30b2d081c8 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 ce0c961740..0099ca6b1c 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,30 @@ describe("openCodeInventory", () => { fast: {}, }, }, + "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 +432,7 @@ 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.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 4c61127a1b..61ed629ea5 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 f5e0bc1773..45882ea1b0 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,7 @@ const STATIC_TOKEN_PRICES: Record = { */ const BUILTIN_PRICING_ALIASES: Record = { auto: "claude-sonnet-5", - fable: "claude-fable-5", + fable: "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/renderer/components/shared/ModelPicker/ModelPicker.test.tsx b/apps/desktop/src/renderer/components/shared/ModelPicker/ModelPicker.test.tsx index 9e31f87800..6a141a0f91 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 6d1acbe2c3..1377e9dd00 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 b297d37bf1..38ed77ef43 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 24a6489c7b..44d04f073b 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 86935fb160..4a1d372cce 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 fb8f121e7d..9fc7412b47 100644 --- a/apps/desktop/src/shared/modelRegistry.test.ts +++ b/apps/desktop/src/shared/modelRegistry.test.ts @@ -134,7 +134,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 +393,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 +440,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 +452,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 +468,16 @@ 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("maps removed Sonnet aliases forward without listing Sonnet 4.6 as a row", () => { @@ -555,7 +550,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 +559,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 2a9d1a699a..c8f2f722a1 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" @@ -1029,10 +1036,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 +1433,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 +1900,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 +1938,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 c50d44ddc0..9e34e566b7 100644 --- a/apps/ios/ADE/Resources/VoiceGlossary.json +++ b/apps/ios/ADE/Resources/VoiceGlossary.json @@ -16,6 +16,7 @@ "Claude Opus 4.8", "Claude Sonnet 5", "Claude Haiku 4.5", + "Claude Fable 5.1", "Claude Fable 5", "GPT-5.6 Sol", "GPT-5.6 Terra", diff --git a/apps/ios/ADE/Views/Components/ADEDesignSystem.swift b/apps/ios/ADE/Views/Components/ADEDesignSystem.swift index 3fad638193..3c88a7f220 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,7 +220,11 @@ 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", "anthropic/claude-fable-5-1", "anthropic/claude-fable-5-1-api", + "claude-fable-5-1", "anthropic/claude-fable-5", "anthropic/claude-fable-5-api", "claude-fable-5": + append("anthropic/claude-fable-5-1") + append("anthropic/claude-fable-5-1-api") + append("claude-fable-5-1") append("anthropic/claude-fable-5") append("anthropic/claude-fable-5-api") append("claude-fable-5") @@ -241,8 +243,8 @@ enum ADEColor { 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": append("anthropic/claude-sonnet-5") @@ -291,6 +293,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 +305,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 fbbd567cec..9fd73f575c 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 ee848419fa..32d0a9af58 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,8 +999,10 @@ 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": + 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": @@ -987,7 +1016,7 @@ private func workCanonicalClaudeRegistryId(for raw: String) -> String? { 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": return "anthropic/claude-sonnet-5" @@ -1000,8 +1029,10 @@ 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": + 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": @@ -1015,7 +1046,7 @@ private func workClaudeRuntimeModelId(for raw: String) -> String? { 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": return "claude-sonnet-5" @@ -1105,8 +1136,10 @@ 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", "anthropic/claude-fable-5-1", "claude-fable-5-1", + "opencode/anthropic/claude-fable-5-1", "anthropic/claude-fable-5", "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": @@ -1114,13 +1147,13 @@ func workKnownModelDisplayName(_ raw: String?) -> String? { 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" + 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": return "Claude Sonnet 5" diff --git a/apps/ios/ADETests/ADETests.swift b/apps/ios/ADETests/ADETests.swift index c27385a700..7ce81eb12c 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,25 @@ 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")) 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 +20499,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 +20550,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 +20565,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"]) @@ -21314,7 +21316,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 2363f7becc..d1f18ab3af 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 5a69443fd5..aee11ea072 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 bd40849800..017a2437e6 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 e9690312cf..14a1cf7731 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 From 8f5cc8f89c7a4b2878d45b30c1f1a5dabb153ccc Mon Sep 17 00:00:00 2001 From: Arul Sharma <31745423+arul28@users.noreply.github.com> Date: Tue, 1 Sep 2026 16:01:59 -0400 Subject: [PATCH 2/2] =?UTF-8?q?ship:=20iteration=202=20=E2=80=94=20fix=20F?= =?UTF-8?q?able=205.1=20pricing=20aliases=20and=20iOS=20glossary=20cap?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Problem: CI rejected unpriced Fable short ids and a 101-term voice glossary; OpenCode still kept Opus 4.8 1M duplicate rows. Change and boundary: Price fable-5.1/fable-5-1/fable-5/fable-5.0 at Fable 5.1 rates without aliasing historical claude-fable-5. Canonicalize Opus 4.8 1M runtime ids. Map OpenCode Claude ids on iOS. Drop the retired Claude Fable 5 glossary term to stay within the Speech Analyzer cap. Verification: desktop vitest 245 catalog/pricing/OpenCode/Droid tests on the touched files. Co-authored-by: Cursor --- .../resources/voice/voice-glossary.json | 1 - .../opencode/openCodeInventory.test.ts | 24 +++++++++++ .../src/main/services/usage/usagePricing.ts | 4 ++ .../usage/usageTrackingService.test.ts | 9 +++++ apps/desktop/src/shared/modelRegistry.test.ts | 13 ++++++ apps/desktop/src/shared/modelRegistry.ts | 8 ++++ apps/ios/ADE/Resources/VoiceGlossary.json | 1 - .../Views/Components/ADEDesignSystem.swift | 27 ++++++++++--- .../ios/ADE/Views/Work/WorkModelCatalog.swift | 40 ++++++++++++------- apps/ios/ADETests/ADETests.swift | 7 ++++ 10 files changed, 113 insertions(+), 21 deletions(-) diff --git a/apps/desktop/resources/voice/voice-glossary.json b/apps/desktop/resources/voice/voice-glossary.json index 1b89c90ff6..af7d415dad 100644 --- a/apps/desktop/resources/voice/voice-glossary.json +++ b/apps/desktop/resources/voice/voice-glossary.json @@ -16,7 +16,6 @@ "Claude Sonnet 5", "Claude Haiku 4.5", "Claude Fable 5.1", - "Claude Fable 5", "GPT-5.6 Sol", "GPT-5.6 Terra", "GPT-5.6 Luna", diff --git a/apps/desktop/src/main/services/opencode/openCodeInventory.test.ts b/apps/desktop/src/main/services/opencode/openCodeInventory.test.ts index 0099ca6b1c..72d19b8c81 100644 --- a/apps/desktop/src/main/services/opencode/openCodeInventory.test.ts +++ b/apps/desktop/src/main/services/opencode/openCodeInventory.test.ts @@ -389,6 +389,28 @@ 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", @@ -432,6 +454,8 @@ 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"); diff --git a/apps/desktop/src/main/services/usage/usagePricing.ts b/apps/desktop/src/main/services/usage/usagePricing.ts index 45882ea1b0..ef751d86fd 100644 --- a/apps/desktop/src/main/services/usage/usagePricing.ts +++ b/apps/desktop/src/main/services/usage/usagePricing.ts @@ -146,6 +146,10 @@ const STATIC_TOKEN_PRICES: Record = { const BUILTIN_PRICING_ALIASES: Record = { auto: "claude-sonnet-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 10f733fcd4..aaa09c08de 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/shared/modelRegistry.test.ts b/apps/desktop/src/shared/modelRegistry.test.ts index 9fc7412b47..f84f40edba 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"; @@ -480,6 +481,18 @@ describe("modelRegistry", () => { 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", () => { expect(MODEL_REGISTRY.some((model) => model.id === "anthropic/claude-sonnet-4-6")).toBe(false); expect(resolveModelAlias("anthropic/claude-sonnet-4-6")?.id).toBe("anthropic/claude-sonnet-5"); diff --git a/apps/desktop/src/shared/modelRegistry.ts b/apps/desktop/src/shared/modelRegistry.ts index c8f2f722a1..c1d8ee709d 100644 --- a/apps/desktop/src/shared/modelRegistry.ts +++ b/apps/desktop/src/shared/modelRegistry.ts @@ -1029,6 +1029,14 @@ export 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" diff --git a/apps/ios/ADE/Resources/VoiceGlossary.json b/apps/ios/ADE/Resources/VoiceGlossary.json index 9e34e566b7..fd20c06e85 100644 --- a/apps/ios/ADE/Resources/VoiceGlossary.json +++ b/apps/ios/ADE/Resources/VoiceGlossary.json @@ -17,7 +17,6 @@ "Claude Sonnet 5", "Claude Haiku 4.5", "Claude Fable 5.1", - "Claude Fable 5", "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 3c88a7f220..74f70146d9 100644 --- a/apps/ios/ADE/Views/Components/ADEDesignSystem.swift +++ b/apps/ios/ADE/Views/Components/ADEDesignSystem.swift @@ -220,25 +220,34 @@ 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", "fable-5.1", "fable-5-1", "anthropic/claude-fable-5-1", "anthropic/claude-fable-5-1-api", - "claude-fable-5-1", "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") @@ -246,10 +255,18 @@ enum ADEColor { 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 } diff --git a/apps/ios/ADE/Views/Work/WorkModelCatalog.swift b/apps/ios/ADE/Views/Work/WorkModelCatalog.swift index 32d0a9af58..5b84e507e4 100644 --- a/apps/ios/ADE/Views/Work/WorkModelCatalog.swift +++ b/apps/ios/ADE/Views/Work/WorkModelCatalog.swift @@ -1001,7 +1001,8 @@ private func workCanonicalClaudeRegistryId(for raw: String) -> String? { switch raw.trimmingCharacters(in: .whitespacesAndNewlines).lowercased() { 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": + "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", @@ -1009,7 +1010,8 @@ private func workCanonicalClaudeRegistryId(for raw: String) -> String? { 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": @@ -1018,9 +1020,11 @@ private func workCanonicalClaudeRegistryId(for raw: String) -> String? { "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-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 @@ -1031,7 +1035,8 @@ private func workClaudeRuntimeModelId(for raw: String) -> String? { switch raw.trimmingCharacters(in: .whitespacesAndNewlines).lowercased() { 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": + "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", @@ -1039,7 +1044,8 @@ private func workClaudeRuntimeModelId(for raw: String) -> String? { 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": @@ -1048,9 +1054,11 @@ private func workClaudeRuntimeModelId(for raw: String) -> String? { "opus-4-6-1m", "claude-opus-4-6-1m", "claude-opus-4-6[1m]", "anthropic/claude-opus-4-6-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 @@ -1136,9 +1144,10 @@ struct WorkModelIdMatcher { func workKnownModelDisplayName(_ raw: String?) -> String? { switch raw?.trimmingCharacters(in: .whitespacesAndNewlines).lowercased() ?? "" { - case "fable", "fable-5.1", "fable-5-1", "anthropic/claude-fable-5-1", "claude-fable-5-1", - "opencode/anthropic/claude-fable-5-1", "anthropic/claude-fable-5", "claude-fable-5", - "opencode/anthropic/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", @@ -1146,7 +1155,8 @@ func workKnownModelDisplayName(_ raw: String?) -> String? { 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]": + "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": @@ -1155,9 +1165,11 @@ func workKnownModelDisplayName(_ raw: String?) -> String? { "opus-4-6-1m", "anthropic/claude-opus-4-6-1m", "claude-opus-4-6-1m", "claude-opus-4-6[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 7ce81eb12c..34b02e4e35 100644 --- a/apps/ios/ADETests/ADETests.swift +++ b/apps/ios/ADETests/ADETests.swift @@ -20488,6 +20488,12 @@ final class ADETests: XCTestCase { XCTAssertEqual(fable?.tagline, "Flagship · 1M context") 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"]) @@ -21307,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"))