diff --git a/apps/marketing/src/content/docs/guides/ai-features.md b/apps/marketing/src/content/docs/guides/ai-features.md index aded351dd..a1f9ab3a5 100644 --- a/apps/marketing/src/content/docs/guides/ai-features.md +++ b/apps/marketing/src/content/docs/guides/ai-features.md @@ -18,10 +18,19 @@ Requires the `claude` CLI installed and authenticated. Uses Claude Code's full s **Models:** -- Sonnet 4.6 (default) +- Sonnet 5 (default) +- Fable 5 +- Opus 5 +- Opus 4.8 +- Sonnet 4.6 +- Opus 4.7 - Opus 4.6 - Haiku 4.5 +Opus 4.8, Opus 4.7, Opus 4.6, and Sonnet 4.6 are also offered in `(1M)` context +variants. The 5-series models use their 1M context window by default, so they +have no separate variant. + ### Codex (via Codex SDK) Requires the `codex` CLI installed and authenticated. The AI operates in a sandboxed read-only mode with the diff context injected as a system prompt prefix. diff --git a/packages/ai/ai.test.ts b/packages/ai/ai.test.ts index 8cf3e453a..e0ed678c7 100644 --- a/packages/ai/ai.test.ts +++ b/packages/ai/ai.test.ts @@ -888,6 +888,32 @@ describe("resolveSDKModel", () => { expect(resolveSDKModel("claude-sonnet-4-6[1m]", bedrockEnv)).toBe(SONNET_ARN); }); + // Opus 5 is a bare alias like every other picker entry: it must NOT be + // mistaken for a cloud identifier (no `arn:` / `anthropic.` prefix), and the + // family matcher must still route it to the configured Opus ARN on + // Bedrock/Vertex. A matcher that keyed on "opus-4" would silently drop it. + test("maps the bare Opus 5 alias to the configured Opus ARN", () => { + expect(resolveSDKModel("claude-opus-5", bedrockEnv)).toBe(OPUS_ARN); + expect( + resolveSDKModel("claude-opus-5", { + CLAUDE_CODE_USE_VERTEX: "true", + ANTHROPIC_DEFAULT_OPUS_MODEL: OPUS_ARN, + }), + ).toBe(OPUS_ARN); + }); + + test("off Bedrock/Vertex: returns the Opus 5 alias unchanged", () => { + expect(resolveSDKModel("claude-opus-5", {})).toBe("claude-opus-5"); + }); + + // Fable has no ANTHROPIC_DEFAULT_*_MODEL env var of its own (Claude Code + // only defines OPUS/SONNET/HAIKU), so on Bedrock it falls through to + // ANTHROPIC_MODEL rather than matching a family. Pinned so the fallthrough + // stays deliberate rather than looking like a missed family branch. + test("falls back to ANTHROPIC_MODEL for the Fable alias on Bedrock", () => { + expect(resolveSDKModel("claude-fable-5", bedrockEnv)).toBe(OPUS_ARN); + }); + test("passes through an identifier that is already an ARN", () => { expect(resolveSDKModel(SONNET_ARN, bedrockEnv)).toBe(SONNET_ARN); }); diff --git a/packages/ai/providers/claude-agent-sdk.ts b/packages/ai/providers/claude-agent-sdk.ts index 3af4e3068..c4ea45c9a 100644 --- a/packages/ai/providers/claude-agent-sdk.ts +++ b/packages/ai/providers/claude-agent-sdk.ts @@ -165,6 +165,7 @@ export class ClaudeAgentSDKProvider implements AIProvider { }; readonly models = [ { id: 'claude-fable-5', label: 'Fable 5' }, + { id: 'claude-opus-5', label: 'Opus 5' }, { id: 'claude-opus-4-8', label: 'Opus 4.8' }, { id: 'claude-opus-4-8[1m]', label: 'Opus 4.8 (1M)' }, { id: 'claude-sonnet-5', label: 'Sonnet 5', default: true }, diff --git a/packages/server/agent-review-message.test.ts b/packages/server/agent-review-message.test.ts index 8554123e4..b30e91589 100644 --- a/packages/server/agent-review-message.test.ts +++ b/packages/server/agent-review-message.test.ts @@ -286,6 +286,16 @@ describe("getLocalDiffInstruction", () => { }); describe("buildClaudeCommand", () => { + test("defaults to Opus 5 when the caller passes no model", () => { + const command = buildClaudeCommand("review").command; + expect(command[command.indexOf("--model") + 1]).toBe("claude-opus-5"); + }); + + test("still honours an explicitly requested model", () => { + const command = buildClaudeCommand("review", "claude-sonnet-5").command; + expect(command[command.indexOf("--model") + 1]).toBe("claude-sonnet-5"); + }); + test("allows read-only JJ commands", () => { const command = buildClaudeCommand("review").command; const allowedTools = command[command.indexOf("--allowedTools") + 1]; diff --git a/packages/server/claude-review.ts b/packages/server/claude-review.ts index 28fe57aef..98b6914a7 100644 --- a/packages/server/claude-review.ts +++ b/packages/server/claude-review.ts @@ -217,7 +217,7 @@ export interface ClaudeCommandResult { * Build the `claude -p` command. Prompt is passed via stdin, not as a * positional arg — avoids quoting issues, argv limits, and variadic flag conflicts. */ -export function buildClaudeCommand(prompt: string, model: string = "claude-opus-4-7", effort?: string): ClaudeCommandResult { +export function buildClaudeCommand(prompt: string, model: string = "claude-opus-5", effort?: string): ClaudeCommandResult { const allowedTools = [ "Agent", "Read", "Glob", "Grep", // GitHub CLI diff --git a/packages/ui/components/AgentsTab.test.ts b/packages/ui/components/AgentsTab.test.ts index 07c618905..722ef6884 100644 --- a/packages/ui/components/AgentsTab.test.ts +++ b/packages/ui/components/AgentsTab.test.ts @@ -1,8 +1,35 @@ import { describe, expect, test } from "bun:test"; -import { CODEX_MODELS, codexReasoningOptions } from "./AgentsTab"; +import { CLAUDE_MODELS, CODEX_MODELS, TOUR_CLAUDE_MODELS, codexReasoningOptions } from "./AgentsTab"; const catalogEntry = (value: string) => CODEX_MODELS.find((m) => m.value === value); +describe("CLAUDE_MODELS catalog", () => { + test("offers the current 5-series flagships", () => { + for (const value of ["claude-fable-5", "claude-opus-5", "claude-sonnet-5"]) { + expect(CLAUDE_MODELS.some((m) => m.value === value)).toBe(true); + } + expect(CLAUDE_MODELS.find((m) => m.value === "claude-opus-5")?.label).toBe("Opus 5"); + }); + + // The 5-series models are 1M-context by default, so they carry no `[1m]` + // variant — that suffix exists only to opt the older 4.x models into the + // larger window. Pinned so a future entry doesn't invent `claude-opus-5[1m]`. + test("5-series entries have no [1m] context variant", () => { + for (const { value } of CLAUDE_MODELS) { + if (/-5(\[|$)/.test(value)) expect(value).not.toContain("[1m]"); + } + }); + + test("tour/guide catalog inherits every review model plus the latest aliases", () => { + for (const { value } of CLAUDE_MODELS) { + expect(TOUR_CLAUDE_MODELS.some((m) => m.value === value)).toBe(true); + } + for (const alias of ["sonnet", "opus", "fable"]) { + expect(TOUR_CLAUDE_MODELS.some((m) => m.value === alias)).toBe(true); + } + }); +}); + test("uses the canonical GPT-5.6 Sol model ID", () => { expect(catalogEntry("gpt-5.6-sol")?.label).toBe("GPT-5.6 Sol"); expect(CODEX_MODELS.some(({ value }) => value === "gpt-5.6")).toBe(false); diff --git a/packages/ui/components/AgentsTab.tsx b/packages/ui/components/AgentsTab.tsx index b0e772f7f..600da1a03 100644 --- a/packages/ui/components/AgentsTab.tsx +++ b/packages/ui/components/AgentsTab.tsx @@ -30,6 +30,7 @@ export type { AgentLaunchParams } from '../hooks/useAgentJobs'; export const CLAUDE_MODELS: Array<{ value: string; label: string }> = [ { value: 'claude-fable-5', label: 'Fable 5' }, + { value: 'claude-opus-5', label: 'Opus 5' }, { value: 'claude-opus-4-8', label: 'Opus 4.8' }, { value: 'claude-opus-4-8[1m]', label: 'Opus 4.8 (1M)' }, { value: 'claude-sonnet-5', label: 'Sonnet 5' }, diff --git a/packages/ui/hooks/useAgentSettings.ts b/packages/ui/hooks/useAgentSettings.ts index d29ea6045..dfe0edcdf 100644 --- a/packages/ui/hooks/useAgentSettings.ts +++ b/packages/ui/hooks/useAgentSettings.ts @@ -16,7 +16,7 @@ const COOKIE_KEY = 'plannotator.agents'; // would clobber writes exactly like before. const settingsListeners = new Set<(s: AgentSettingsState) => void>(); -export const DEFAULT_CLAUDE_MODEL = 'claude-opus-4-7'; +export const DEFAULT_CLAUDE_MODEL = 'claude-opus-5'; export const DEFAULT_CLAUDE_EFFORT = 'high'; // gpt-5.3-codex is deprecated (ChatGPT-account Codex rejects it outright) — // default to the current flagship everywhere.