Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
@@ -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.
4 changes: 2 additions & 2 deletions apps/ade-cli/src/tuiClient/__tests__/appInput.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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({
Expand All @@ -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.",
]);
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ const modelState: Pick<AdeCodeModelState, "modelId" | "reasoningEffort" | "inter

describe("buildModelPickerLayout", () => {
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" }),
];
Expand Down Expand Up @@ -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);
});
Expand Down
2 changes: 1 addition & 1 deletion apps/desktop/resources/voice/voice-glossary.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
"Claude Opus 4.8",
"Claude Sonnet 5",
"Claude Haiku 4.5",
"Claude Fable 5",
"Claude Fable 5.1",
"GPT-5.6 Sol",
"GPT-5.6 Terra",
"GPT-5.6 Luna",
Expand Down
10 changes: 6 additions & 4 deletions apps/desktop/src/main/services/ai/claudeModelUtils.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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");
Expand All @@ -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");
});
});
4 changes: 2 additions & 2 deletions apps/desktop/src/main/services/ai/claudeModelUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down
12 changes: 6 additions & 6 deletions apps/desktop/src/main/services/chat/agentChatService.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3122,16 +3122,16 @@ 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",
provider: "claude",
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([
Expand Down Expand Up @@ -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({
Expand Down Expand Up @@ -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 () => {
Expand Down
39 changes: 9 additions & 30 deletions apps/desktop/src/main/services/chat/agentChatService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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()),
Expand All @@ -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\//, "")
Expand Down Expand Up @@ -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);
Expand All @@ -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 {
Expand All @@ -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 };
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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");
Expand All @@ -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");
Expand All @@ -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 () => {
Expand Down Expand Up @@ -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,
Expand Down
28 changes: 26 additions & 2 deletions apps/desktop/src/main/services/chat/droidModelsDiscovery.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand All @@ -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;
Expand Down
Loading
Loading