diff --git a/.agents/docs/agent-adapters.md b/.agents/docs/agent-adapters.md index 793a5910..bcb04219 100644 --- a/.agents/docs/agent-adapters.md +++ b/.agents/docs/agent-adapters.md @@ -53,7 +53,7 @@ Model/effort lists below are the **statically declared defaults**. Several provi | Provider | Models | Efforts | Live Input | Structured Session | | ------------ | ------------------------------------------------------ | ---------------------------------------- | --------------------- | ---------------------- | -| Claude | opus-4-8, opus-4-7, opus-4-6, sonnet, haiku | low, medium, high, xHigh, max, ultracode | terminal | No | +| Claude | opus-4-8, fable-5, opus-4-7, opus-4-6, sonnet, haiku | low, medium, high, xHigh, max, ultracode | terminal | No | | Codex | (probed dynamically via app-server) | (probed dynamically) | terminal / GUI server | Yes (stdio app-server) | | Gemini | (probed dynamically via ACP) | (probed dynamically) | terminal | No | | Copilot | (probed via ACP) | (probed via ACP) | terminal | Yes (ACP) | diff --git a/package.json b/package.json index 5f802669..a70aaba5 100644 --- a/package.json +++ b/package.json @@ -52,7 +52,7 @@ }, "dependencies": { "@agentclientprotocol/sdk": "^0.21.1", - "@anthropic-ai/claude-agent-sdk": "^0.3.154", + "@anthropic-ai/claude-agent-sdk": "^0.3.170", "@chenglou/pretext": "^0.0.6", "@dnd-kit/abstract": "^0.4.0", "@dnd-kit/collision": "^0.4.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 2cb9af4f..714a2b13 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -18,8 +18,8 @@ importers: specifier: ^0.21.1 version: 0.21.1(zod@4.4.2) '@anthropic-ai/claude-agent-sdk': - specifier: ^0.3.154 - version: 0.3.154(@anthropic-ai/sdk@0.93.0(zod@4.4.2))(@modelcontextprotocol/sdk@1.29.0(zod@4.4.2))(zod@4.4.2) + specifier: ^0.3.170 + version: 0.3.170(@anthropic-ai/sdk@0.93.0(zod@4.4.2))(@modelcontextprotocol/sdk@1.29.0(zod@4.4.2))(zod@4.4.2) '@chenglou/pretext': specifier: ^0.0.6 version: 0.0.6 @@ -358,52 +358,52 @@ packages: '@antfu/install-pkg@1.1.0': resolution: {integrity: sha512-MGQsmw10ZyI+EJo45CdSER4zEb+p31LpDAFp2Z3gkSd1yqVZGi0Ebx++YTEMonJy4oChEMLsxZ64j8FH6sSqtQ==} - '@anthropic-ai/claude-agent-sdk-darwin-arm64@0.3.154': - resolution: {integrity: sha512-oFW3LD5lYrKAU+AKu27Z8hrzqkrh362qQrwi/i3DxGcud9BXUycsXYjShpDj3D3JZu169UzZuSPhx1Wajmbiwg==} + '@anthropic-ai/claude-agent-sdk-darwin-arm64@0.3.170': + resolution: {integrity: sha512-rwfgArIa5WI0QPNqFsRBgvtSI0mrtpynUm0oK6+l6/KX4hcgnYGEzciZR1bOeD9/7sSZlTdIgt+T9alKeZmXcg==} cpu: [arm64] os: [darwin] - '@anthropic-ai/claude-agent-sdk-darwin-x64@0.3.154': - resolution: {integrity: sha512-5BgWEueP+cqoctWjZYhCbyltuaV/N2DmKDXD3/69cKaVmJp8XL9OCzlq/HEirA/+Ssjskx6hDUBaOcpuZ3iwQA==} + '@anthropic-ai/claude-agent-sdk-darwin-x64@0.3.170': + resolution: {integrity: sha512-0e58h8UQMtsQxLGIv9r4foxfBFWKZ7NeDtoplLhuD7EwQonehomw1sBXCch77t/IfUS+q5vQ5zv+fOGmap5nLQ==} cpu: [x64] os: [darwin] - '@anthropic-ai/claude-agent-sdk-linux-arm64-musl@0.3.154': - resolution: {integrity: sha512-o2bCQN4Xn3UqCLErC5m4T7u0yYArJYmgFCUFnA6K96DdW2RERvx+gTKXxWuHEBkDO+eMoHLHLxk0u2jGES00Ng==} + '@anthropic-ai/claude-agent-sdk-linux-arm64-musl@0.3.170': + resolution: {integrity: sha512-SRYfQcsXlOq+CD/FqkQBTSHbaD++w73GnnO+NUV9adLYrca3kfetRwWT1iguY1cNS0l34dCR3rlzCPq78vg1Jg==} cpu: [arm64] os: [linux] libc: [musl] - '@anthropic-ai/claude-agent-sdk-linux-arm64@0.3.154': - resolution: {integrity: sha512-rRkW4SBL3W7zQvKscCIfIGlmoeuTbMV6dXFbPdmpRGvmYZIs79RpzO6xrGBnnhmm+B7znQ9oHAnffi/2FBgJbA==} + '@anthropic-ai/claude-agent-sdk-linux-arm64@0.3.170': + resolution: {integrity: sha512-gLbaFqcGppFJQd4DLNV4IXoeahejT/p2/M8bSSvRDbla9GOsBr1AxV5XLRyBn1e7xFGozZIAIQr3+1chp7NJgQ==} cpu: [arm64] os: [linux] libc: [glibc] - '@anthropic-ai/claude-agent-sdk-linux-x64-musl@0.3.154': - resolution: {integrity: sha512-zA7S8Lm6O4QBsUpbhiOht8BgiXHOBBFUIo8ZLK6r5wAatK3Q44syWVxICeyCnR6wqfnkf3cugCw27ycS6vVgaA==} + '@anthropic-ai/claude-agent-sdk-linux-x64-musl@0.3.170': + resolution: {integrity: sha512-m4+I0qBEk7cxRKS+pL+eoWXbXTFOAo83fQ0tQvap4z/mDMm06IWJtEPoYTaMBwsp32GJWLkHWKbZSBCHZnp2DQ==} cpu: [x64] os: [linux] libc: [musl] - '@anthropic-ai/claude-agent-sdk-linux-x64@0.3.154': - resolution: {integrity: sha512-GpiFF8Ez6PbM3m0gqtCo/FKM346qyRdP7VhbmJzdnbNKTiiUZ66vDQyEUPZPCG24ZkrG4m96KpRIUwY08rHiNg==} + '@anthropic-ai/claude-agent-sdk-linux-x64@0.3.170': + resolution: {integrity: sha512-Xl/m7TaSC3T5IDBdHrZQ9fCQYyDmPELN34CL+MoyPIf7uSmuZnjE9fUOqDh2Rv26JxWssi1M6X+BBvVuKd6Cpg==} cpu: [x64] os: [linux] libc: [glibc] - '@anthropic-ai/claude-agent-sdk-win32-arm64@0.3.154': - resolution: {integrity: sha512-cDW1YFbU/PJFlrGXhlAGcbkXt80sEO6WtnH8nN8YHXLn5NWduy2q7o/qC6i8XozgvRGf6t/eMoH7IasGIEDhDw==} + '@anthropic-ai/claude-agent-sdk-win32-arm64@0.3.170': + resolution: {integrity: sha512-IG+8isJNNJKbnnhO7m+PGhfVCg+XoQ/MDxGde5eigFI0WsEfitjuWSWwx82bT9ghxI1aa6qNvI+UPgPcZuo5Fg==} cpu: [arm64] os: [win32] - '@anthropic-ai/claude-agent-sdk-win32-x64@0.3.154': - resolution: {integrity: sha512-tSKaIIpL72OPg3WfzZTCIl8OJgcbq4qieu8/fDWjsdeQuari9gQMIuEflFphk9HqNsxpSmDqKi8Sm5mW2V566Q==} + '@anthropic-ai/claude-agent-sdk-win32-x64@0.3.170': + resolution: {integrity: sha512-7cuqSKbHVItPGVwRbd3A0BEJwcNtc7Fhoh6qHN4C6yrmjSrvdYYx3MLvq/VI768/RoG7mAMDxb+j7WfEfoP9BA==} cpu: [x64] os: [win32] - '@anthropic-ai/claude-agent-sdk@0.3.154': - resolution: {integrity: sha512-iEn25urI2QrMPFIhId3h7v/7EG5gsmF7ooe+6EvsAosePeLmpVVerp5nXtHnlmBkMinLecurcPA+OddKw76jYw==} + '@anthropic-ai/claude-agent-sdk@0.3.170': + resolution: {integrity: sha512-pAvhfk+iTodXZ6RF18Kz7BEUWFjL7EcR3tKuhUNdPpE1NAYCR3mSHGbafi72JsrNwKEDIs7FU31z3fqhwy8QzA==} engines: {node: '>=18.0.0'} peerDependencies: '@anthropic-ai/sdk': '>=0.93.0' @@ -6393,44 +6393,44 @@ snapshots: package-manager-detector: 1.6.0 tinyexec: 1.1.2 - '@anthropic-ai/claude-agent-sdk-darwin-arm64@0.3.154': + '@anthropic-ai/claude-agent-sdk-darwin-arm64@0.3.170': optional: true - '@anthropic-ai/claude-agent-sdk-darwin-x64@0.3.154': + '@anthropic-ai/claude-agent-sdk-darwin-x64@0.3.170': optional: true - '@anthropic-ai/claude-agent-sdk-linux-arm64-musl@0.3.154': + '@anthropic-ai/claude-agent-sdk-linux-arm64-musl@0.3.170': optional: true - '@anthropic-ai/claude-agent-sdk-linux-arm64@0.3.154': + '@anthropic-ai/claude-agent-sdk-linux-arm64@0.3.170': optional: true - '@anthropic-ai/claude-agent-sdk-linux-x64-musl@0.3.154': + '@anthropic-ai/claude-agent-sdk-linux-x64-musl@0.3.170': optional: true - '@anthropic-ai/claude-agent-sdk-linux-x64@0.3.154': + '@anthropic-ai/claude-agent-sdk-linux-x64@0.3.170': optional: true - '@anthropic-ai/claude-agent-sdk-win32-arm64@0.3.154': + '@anthropic-ai/claude-agent-sdk-win32-arm64@0.3.170': optional: true - '@anthropic-ai/claude-agent-sdk-win32-x64@0.3.154': + '@anthropic-ai/claude-agent-sdk-win32-x64@0.3.170': optional: true - '@anthropic-ai/claude-agent-sdk@0.3.154(@anthropic-ai/sdk@0.93.0(zod@4.4.2))(@modelcontextprotocol/sdk@1.29.0(zod@4.4.2))(zod@4.4.2)': + '@anthropic-ai/claude-agent-sdk@0.3.170(@anthropic-ai/sdk@0.93.0(zod@4.4.2))(@modelcontextprotocol/sdk@1.29.0(zod@4.4.2))(zod@4.4.2)': dependencies: '@anthropic-ai/sdk': 0.93.0(zod@4.4.2) '@modelcontextprotocol/sdk': 1.29.0(zod@4.4.2) zod: 4.4.2 optionalDependencies: - '@anthropic-ai/claude-agent-sdk-darwin-arm64': 0.3.154 - '@anthropic-ai/claude-agent-sdk-darwin-x64': 0.3.154 - '@anthropic-ai/claude-agent-sdk-linux-arm64': 0.3.154 - '@anthropic-ai/claude-agent-sdk-linux-arm64-musl': 0.3.154 - '@anthropic-ai/claude-agent-sdk-linux-x64': 0.3.154 - '@anthropic-ai/claude-agent-sdk-linux-x64-musl': 0.3.154 - '@anthropic-ai/claude-agent-sdk-win32-arm64': 0.3.154 - '@anthropic-ai/claude-agent-sdk-win32-x64': 0.3.154 + '@anthropic-ai/claude-agent-sdk-darwin-arm64': 0.3.170 + '@anthropic-ai/claude-agent-sdk-darwin-x64': 0.3.170 + '@anthropic-ai/claude-agent-sdk-linux-arm64': 0.3.170 + '@anthropic-ai/claude-agent-sdk-linux-arm64-musl': 0.3.170 + '@anthropic-ai/claude-agent-sdk-linux-x64': 0.3.170 + '@anthropic-ai/claude-agent-sdk-linux-x64-musl': 0.3.170 + '@anthropic-ai/claude-agent-sdk-win32-arm64': 0.3.170 + '@anthropic-ai/claude-agent-sdk-win32-x64': 0.3.170 '@anthropic-ai/sdk@0.93.0(zod@4.4.2)': dependencies: diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 9a8d32c2..3ff5c1ae 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -50,15 +50,15 @@ nodeLinker: hoisted minimumReleaseAgeExclude: - electron@41.7.0 - - "@anthropic-ai/claude-agent-sdk-darwin-arm64@0.3.154" - - "@anthropic-ai/claude-agent-sdk-darwin-x64@0.3.154" - - "@anthropic-ai/claude-agent-sdk-linux-arm64-musl@0.3.154" - - "@anthropic-ai/claude-agent-sdk-linux-arm64@0.3.154" - - "@anthropic-ai/claude-agent-sdk-linux-x64-musl@0.3.154" - - "@anthropic-ai/claude-agent-sdk-linux-x64@0.3.154" - - "@anthropic-ai/claude-agent-sdk-win32-arm64@0.3.154" - - "@anthropic-ai/claude-agent-sdk-win32-x64@0.3.154" - - "@anthropic-ai/claude-agent-sdk@0.3.154" + - "@anthropic-ai/claude-agent-sdk-darwin-arm64@0.3.170" + - "@anthropic-ai/claude-agent-sdk-darwin-x64@0.3.170" + - "@anthropic-ai/claude-agent-sdk-linux-arm64-musl@0.3.170" + - "@anthropic-ai/claude-agent-sdk-linux-arm64@0.3.170" + - "@anthropic-ai/claude-agent-sdk-linux-x64-musl@0.3.170" + - "@anthropic-ai/claude-agent-sdk-linux-x64@0.3.170" + - "@anthropic-ai/claude-agent-sdk-win32-arm64@0.3.170" + - "@anthropic-ai/claude-agent-sdk-win32-x64@0.3.170" + - "@anthropic-ai/claude-agent-sdk@0.3.170" # Already present in the lockfile when the supply-chain guard was added. - "react@19.2.7" - "react-dom@19.2.7" diff --git a/src/renderer/components/providers/claude/index.test.tsx b/src/renderer/components/providers/claude/index.test.tsx new file mode 100644 index 00000000..abdd20c7 --- /dev/null +++ b/src/renderer/components/providers/claude/index.test.tsx @@ -0,0 +1,71 @@ +import { describe, expect, it } from "vitest"; +import type { AgentCapability } from "@/shared/contracts"; +import { getComposerControls } from "../ProviderIcon"; +import type { ComposerControl } from "@/renderer/components/thread/ThreadComposer"; +import "./index"; + +const capabilities = { + models: [ + { id: "claude-fable-5", label: "Fable 5" }, + { id: "haiku", label: "Haiku" }, + ], + efforts: ["low", "medium", "high", "xHigh", "max", "ultracode"], + defaultEffort: "high", + modelEfforts: { + "claude-fable-5": ["low", "medium", "high", "xHigh", "max", "ultracode"], + haiku: [], + }, + modes: ["agent", "plan"], + approvalPolicies: [ + { id: "default", label: "Default" }, + { id: "auto", label: "Auto mode" }, + { id: "bypassPermissions", label: "Bypass Permissions" }, + ], + sandboxModes: [], + supportsResume: true, + supportsDirectInput: true, + liveInputMode: "terminal", + presentationMode: "terminal", + settingDefs: [], +} as AgentCapability; + +function isPermissionControl( + control: ComposerControl, +): control is ComposerControl & { iconKind: "permission" } { + return "iconKind" in control && control.iconKind === "permission"; +} + +describe("Claude composer controls", () => { + it("allows auto permissions for Fable 5", () => { + const controls = getComposerControls("claude")?.({ + capabilities, + config: { model: "claude-fable-5" }, + isDisabled: false, + onConfigChange: () => undefined, + }); + + const permission = controls?.find(isPermissionControl); + expect(permission && "options" in permission ? permission.options : []).toContainEqual({ + id: "auto", + label: "Auto mode", + }); + }); + + it("filters auto permissions for Haiku", () => { + const controls = getComposerControls("claude")?.({ + capabilities, + config: { model: "haiku", approvalPolicy: "auto" }, + isDisabled: false, + onConfigChange: () => undefined, + }); + + const permission = controls?.find(isPermissionControl); + expect(permission && "options" in permission ? permission.options : []).not.toContainEqual({ + id: "auto", + label: "Auto mode", + }); + expect(permission && "value" in permission ? permission.value : undefined).toBe( + "bypassPermissions", + ); + }); +}); diff --git a/src/renderer/components/providers/claude/index.tsx b/src/renderer/components/providers/claude/index.tsx index b15cadfc..1fd44701 100644 --- a/src/renderer/components/providers/claude/index.tsx +++ b/src/renderer/components/providers/claude/index.tsx @@ -32,17 +32,19 @@ registerConflictResolverDefaults("claude", { effort: "high", }); +// Auto mode is only supported for Sonnet 4.6+ and Opus 4.6+. +// Filter it out for Haiku and other models that don't support it. +const AUTO_CAPABLE_MODELS = new Set([ + "sonnet", + "claude-fable-5", + "claude-opus-4-6", + "claude-opus-4-7", + "claude-opus-4-8", +]); + registerComposerControls("claude", ({ capabilities, config, isDisabled, onConfigChange }) => { const isPlanMode = (config.mode ?? "agent") !== "agent"; - // Auto mode is only supported for Sonnet 4.6+ and Opus 4.6+. - // Filter it out for Haiku and other models that don't support it. - const AUTO_CAPABLE_MODELS = new Set([ - "sonnet", - "claude-opus-4-6", - "claude-opus-4-7", - "claude-opus-4-8", - ]); const modelSupportsAuto = !config.model || AUTO_CAPABLE_MODELS.has(config.model); const filteredPolicies = modelSupportsAuto ? capabilities.approvalPolicies diff --git a/src/renderer/components/providers/triggerWords.test.ts b/src/renderer/components/providers/triggerWords.test.ts index e47e15f8..36ed7494 100644 --- a/src/renderer/components/providers/triggerWords.test.ts +++ b/src/renderer/components/providers/triggerWords.test.ts @@ -9,6 +9,7 @@ function words(kind: string | undefined, model: string | undefined): string[] { describe("getTriggerWords", () => { it("leaves workflow as plain text for Claude models", () => { expect(words("claude", "claude-opus-4-8")).toEqual([]); + expect(words("claude", "claude-fable-5")).toEqual([]); expect(words("claude", "claude-opus-4-7")).toEqual([]); expect(words("claude", "claude-opus-4-6")).toEqual([]); expect(words("claude", "sonnet")).toEqual([]); diff --git a/src/supervisor/agents/claude/claude.test.ts b/src/supervisor/agents/claude/claude.test.ts index afd6badd..a223bc31 100644 --- a/src/supervisor/agents/claude/claude.test.ts +++ b/src/supervisor/agents/claude/claude.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it } from "vitest"; import { createClaudeAdapter } from "./index"; -import { parseClaudeAuthStatusJson } from "./detection"; +import { claudeCapabilities, parseClaudeAuthStatusJson } from "./detection"; import type { OscNotification, OscTitle } from "@/shared/osc"; import type { ProjectLocation, ThreadConfig } from "@/shared/contracts"; @@ -131,6 +131,26 @@ describe("createClaudeAdapter structured sessions", () => { }); }); +describe("claudeCapabilities", () => { + it("advertises Fable 5 as a 1M-only non-fast model guarded by the probe", () => { + expect(claudeCapabilities.models).toContainEqual({ id: "claude-fable-5", label: "Fable 5" }); + expect(claudeCapabilities.modelEfforts["claude-fable-5"]).toEqual([ + "low", + "medium", + "high", + "xHigh", + "max", + "ultracode", + ]); + expect(claudeCapabilities.modelContextSizes?.["claude-fable-5"]).toEqual(["1m"]); + expect(claudeCapabilities.fastModels).not.toContain("claude-fable-5"); + }); + + it("lists Fable 5 first so the latest model is the default for new threads", () => { + expect(claudeCapabilities.models[0]).toEqual({ id: "claude-fable-5", label: "Fable 5" }); + }); +}); + describe("createClaudeAdapter buildAcpLogoutCommand", () => { it("returns `claude auth logout` so the Settings logout button can drive it", async () => { const adapter = createClaudeAdapter(); diff --git a/src/supervisor/agents/claude/detection.ts b/src/supervisor/agents/claude/detection.ts index 3fdbdbe9..b00d201a 100644 --- a/src/supervisor/agents/claude/detection.ts +++ b/src/supervisor/agents/claude/detection.ts @@ -20,19 +20,24 @@ const CLAUDE_BUILT_IN_SLASH_COMMANDS: AgentCapability["slashCommands"] = [ }, ]; +/** Effort tiers shared by the frontier models (Opus 4.7/4.8 and Fable 5). */ +const PREMIUM_EFFORT_TIERS = ["low", "medium", "high", "xHigh", "max", "ultracode"]; + export const claudeCapabilities: AgentCapability = { models: [ + { id: "claude-fable-5", label: "Fable 5" }, { id: "claude-opus-4-8", label: "Opus 4.8" }, { id: "claude-opus-4-7", label: "Opus 4.7" }, { id: "claude-opus-4-6", label: "Opus 4.6" }, { id: "sonnet", label: "Sonnet" }, { id: "haiku", label: "Haiku" }, ], - efforts: ["low", "medium", "high", "xHigh", "max", "ultracode"], + efforts: PREMIUM_EFFORT_TIERS, defaultEffort: "high", modelEfforts: { - "claude-opus-4-8": ["low", "medium", "high", "xHigh", "max", "ultracode"], - "claude-opus-4-7": ["low", "medium", "high", "xHigh", "max", "ultracode"], + "claude-fable-5": PREMIUM_EFFORT_TIERS, + "claude-opus-4-8": PREMIUM_EFFORT_TIERS, + "claude-opus-4-7": PREMIUM_EFFORT_TIERS, "claude-opus-4-6": ["low", "medium", "high", "max"], haiku: [], sonnet: ["low", "medium", "high", "max"], @@ -45,6 +50,7 @@ export const claudeCapabilities: AgentCapability = { // to 1M (the long-context build users select these for); Sonnet defaults to // 200k because the 1M tier is billed per-token at premium rates. modelContextSizes: { + "claude-fable-5": ["1m"], "claude-opus-4-8": ["1m", "200k"], "claude-opus-4-7": ["1m", "200k"], "claude-opus-4-6": ["1m", "200k"], diff --git a/src/supervisor/agents/claude/probe.test.ts b/src/supervisor/agents/claude/probe.test.ts index 997dcd1f..7a6ee9de 100644 --- a/src/supervisor/agents/claude/probe.test.ts +++ b/src/supervisor/agents/claude/probe.test.ts @@ -97,23 +97,35 @@ beforeEach(() => { }); describe("claudeCapabilitiesFromCliVersion", () => { - it("hides Opus 4.7 and 4.8 when CLI is below 2.1.111", () => { + it("hides Fable 5, Opus 4.7, and Opus 4.8 when CLI is below 2.1.111", () => { const p = claudeCapabilitiesFromCliVersion("2.1.110"); + expect(p?.models?.map((m) => m.id)).not.toContain("claude-fable-5"); expect(p?.models?.map((m) => m.id)).not.toContain("claude-opus-4-7"); expect(p?.models?.map((m) => m.id)).not.toContain("claude-opus-4-8"); + expect(p?.modelContextSizes && "claude-fable-5" in p.modelContextSizes).toBe(false); expect(p?.modelEfforts && "claude-opus-4-7" in p.modelEfforts).toBe(false); expect(p?.models?.map((m) => m.id)).toContain("claude-opus-4-6"); }); - it("hides only Opus 4.8 when CLI supports Opus 4.7 but not 4.8", () => { + it("hides Fable 5 and Opus 4.8 when CLI supports Opus 4.7 but not Opus 4.8", () => { const p = claudeCapabilitiesFromCliVersion("2.1.153"); expect(p?.models?.map((m) => m.id)).toContain("claude-opus-4-7"); expect(p?.models?.map((m) => m.id)).toContain("claude-opus-4-6"); + expect(p?.models?.map((m) => m.id)).not.toContain("claude-fable-5"); expect(p?.models?.map((m) => m.id)).not.toContain("claude-opus-4-8"); }); - it("returns undefined when CLI supports Opus 4.8", () => { - expect(claudeCapabilitiesFromCliVersion("2.1.154")).toBeUndefined(); + it("hides only Fable 5 when CLI supports Opus 4.8 but not Fable 5", () => { + const p = claudeCapabilitiesFromCliVersion("2.1.169"); + expect(p?.models?.map((m) => m.id)).toContain("claude-opus-4-8"); + expect(p?.models?.map((m) => m.id)).toContain("claude-opus-4-7"); + expect(p?.models?.map((m) => m.id)).not.toContain("claude-fable-5"); + expect(p?.modelEfforts && "claude-fable-5" in p.modelEfforts).toBe(false); + expect(p?.modelContextSizes && "claude-fable-5" in p.modelContextSizes).toBe(false); + }); + + it("returns undefined when CLI supports Fable 5", () => { + expect(claudeCapabilitiesFromCliVersion("2.1.170")).toBeUndefined(); expect(claudeCapabilitiesFromCliVersion("3.0.0")).toBeUndefined(); }); diff --git a/src/supervisor/agents/claude/probe.ts b/src/supervisor/agents/claude/probe.ts index 6d3d42dd..0964d676 100644 --- a/src/supervisor/agents/claude/probe.ts +++ b/src/supervisor/agents/claude/probe.ts @@ -22,13 +22,16 @@ const CLAUDE_TERMINAL_AUTH_METHOD: AgentAuthMethod = { const MIN_CLAUDE_OPUS_47_CLI = [2, 1, 111] as const; const MIN_CLAUDE_OPUS_48_CLI = [2, 1, 154] as const; +const MIN_CLAUDE_FABLE_5_CLI = [2, 1, 170] as const; const OPUS_48_MODEL_ID = "claude-opus-4-8"; const OPUS_47_MODEL_ID = "claude-opus-4-7"; +const FABLE_5_MODEL_ID = "claude-fable-5"; const CLAUDE_SEMVER_RE = /(\d+)\.(\d+)\.(\d+)/; /** Built-in catalog (CLI `--model` ids) merged with semver gate + SDK slash commands. */ const BUILTIN_MODELS: AgentCapability["models"] = [ + { id: FABLE_5_MODEL_ID, label: "Fable 5" }, { id: OPUS_48_MODEL_ID, label: "Opus 4.8" }, { id: OPUS_47_MODEL_ID, label: "Opus 4.7" }, { id: "claude-opus-4-6", label: "Opus 4.6" }, @@ -36,12 +39,32 @@ const BUILTIN_MODELS: AgentCapability["models"] = [ { id: "haiku", label: "Haiku" }, ]; +/** Effort tiers shared by the frontier models (Opus 4.7/4.8 and Fable 5). */ +const PREMIUM_EFFORT_TIERS = ["low", "medium", "high", "xHigh", "max", "ultracode"]; + const BUILTIN_MODEL_EFFORTS: AgentCapability["modelEfforts"] = { + [FABLE_5_MODEL_ID]: PREMIUM_EFFORT_TIERS, + [OPUS_48_MODEL_ID]: PREMIUM_EFFORT_TIERS, + [OPUS_47_MODEL_ID]: PREMIUM_EFFORT_TIERS, "claude-opus-4-6": ["low", "medium", "high", "max"], haiku: [], sonnet: ["low", "medium", "high", "max"], }; +const BUILTIN_MODEL_CONTEXT_SIZES: NonNullable = { + [FABLE_5_MODEL_ID]: ["1m"], + [OPUS_48_MODEL_ID]: ["1m", "200k"], + [OPUS_47_MODEL_ID]: ["1m", "200k"], + "claude-opus-4-6": ["1m", "200k"], + sonnet: ["200k", "1m"], +}; + +const BUILTIN_FAST_MODELS: NonNullable = [ + OPUS_48_MODEL_ID, + OPUS_47_MODEL_ID, + "claude-opus-4-6", +]; + function parseSemverTriplet(version: string): [number, number, number] | null { const m = CLAUDE_SEMVER_RE.exec(version.trim()); if (!m) return null; @@ -63,16 +86,20 @@ export function claudeCapabilitiesFromCliVersion( if (!triplet) return undefined; const hiddenModelIds = new Set(); + if (!semverGte(triplet, MIN_CLAUDE_FABLE_5_CLI)) hiddenModelIds.add(FABLE_5_MODEL_ID); if (!semverGte(triplet, MIN_CLAUDE_OPUS_48_CLI)) hiddenModelIds.add(OPUS_48_MODEL_ID); if (!semverGte(triplet, MIN_CLAUDE_OPUS_47_CLI)) hiddenModelIds.add(OPUS_47_MODEL_ID); if (hiddenModelIds.size === 0) return undefined; const models = BUILTIN_MODELS.filter((m) => !hiddenModelIds.has(m.id)); const modelEfforts = { ...BUILTIN_MODEL_EFFORTS }; + const modelContextSizes = { ...BUILTIN_MODEL_CONTEXT_SIZES }; for (const modelId of hiddenModelIds) { delete modelEfforts[modelId]; + delete modelContextSizes[modelId]; } - return { models, modelEfforts }; + const fastModels = BUILTIN_FAST_MODELS.filter((modelId) => !hiddenModelIds.has(modelId)); + return { models, modelEfforts, modelContextSizes, fastModels }; } export function mapClaudeSlashCommands(