From 2e735a7daf33f7c55ec144e705406139d4fd46f3 Mon Sep 17 00:00:00 2001 From: boggedbrush <90526147+boggedbrush@users.noreply.github.com> Date: Wed, 29 Apr 2026 15:10:32 -0400 Subject: [PATCH] feat: add Claude Opus 4.7 support --- README.md | 2 +- .../src/provider/Layers/ClaudeProvider.ts | 40 +++++++++++-------- apps/server/src/serverSettings.test.ts | 12 +++++- apps/server/src/serverSettings.ts | 4 +- .../components/chat/ModelPickerContent.tsx | 17 ++++---- .../chat/ProviderModelPicker.browser.tsx | 26 ++++++++++++ .../components/chat/ProviderModelPicker.tsx | 4 ++ .../chat/modelPickerModelHighlights.ts | 2 +- .../src/components/chat/providerIconUtils.ts | 2 +- .../settings/SettingsModelsSection.tsx | 13 +++--- .../settings/SettingsPanelPrimitives.tsx | 1 + packages/contracts/src/model.ts | 4 +- packages/shared/src/model.test.ts | 18 +++++---- 13 files changed, 99 insertions(+), 46 deletions(-) diff --git a/README.md b/README.md index 3b707140..6a585caa 100644 --- a/README.md +++ b/README.md @@ -285,7 +285,7 @@ This snippet mirrors the current settings schema and built-in model defaults in - Mode overrides are stored as `askModelSelection`, `planModelSelection`, `codeModelSelection`, and `reviewModelSelection`. - Default provider models are `gpt-5.4` for Codex and `claude-sonnet-4-6` for Claude. - Git text generation and prompt enhancement currently default to `gpt-5.4-mini`. -- Other built-in models currently exposed include `gpt-5.3-codex`, `gpt-5.3-codex-spark`, `claude-opus-4-6`, and `claude-haiku-4-5`. +- Other built-in models currently exposed include `gpt-5.3-codex`, `gpt-5.3-codex-spark`, `claude-opus-4-7`, `claude-opus-4-6`, and `claude-haiku-4-5`.
diff --git a/apps/server/src/provider/Layers/ClaudeProvider.ts b/apps/server/src/provider/Layers/ClaudeProvider.ts index b62d21e3..de6f7225 100644 --- a/apps/server/src/provider/Layers/ClaudeProvider.ts +++ b/apps/server/src/provider/Layers/ClaudeProvider.ts @@ -37,27 +37,35 @@ const DEFAULT_CLAUDE_MODEL_CAPABILITIES: ModelCapabilities = { }; const PROVIDER = "claudeAgent" as const; +const CLAUDE_OPUS_CAPABILITIES = { + reasoningEffortLevels: [ + { value: "low", label: "Low" }, + { value: "medium", label: "Medium" }, + { value: "high", label: "High", isDefault: true }, + { value: "max", label: "Max" }, + { value: "ultrathink", label: "Ultrathink" }, + ], + supportsFastMode: true, + supportsThinkingToggle: false, + contextWindowOptions: [ + { value: "200k", label: "200k", isDefault: true }, + { value: "1m", label: "1M" }, + ], + promptInjectedEffortLevels: ["ultrathink"], +} satisfies ModelCapabilities; + const BUILT_IN_MODELS: ReadonlyArray = [ + { + slug: "claude-opus-4-7", + name: "Claude Opus 4.7", + isCustom: false, + capabilities: CLAUDE_OPUS_CAPABILITIES, + }, { slug: "claude-opus-4-6", name: "Claude Opus 4.6", isCustom: false, - capabilities: { - reasoningEffortLevels: [ - { value: "low", label: "Low" }, - { value: "medium", label: "Medium" }, - { value: "high", label: "High", isDefault: true }, - { value: "max", label: "Max" }, - { value: "ultrathink", label: "Ultrathink" }, - ], - supportsFastMode: true, - supportsThinkingToggle: false, - contextWindowOptions: [ - { value: "200k", label: "200k", isDefault: true }, - { value: "1m", label: "1M" }, - ], - promptInjectedEffortLevels: ["ultrathink"], - } satisfies ModelCapabilities, + capabilities: CLAUDE_OPUS_CAPABILITIES, }, { slug: "claude-sonnet-4-6", diff --git a/apps/server/src/serverSettings.test.ts b/apps/server/src/serverSettings.test.ts index d9ca3e39..1c727d7e 100644 --- a/apps/server/src/serverSettings.test.ts +++ b/apps/server/src/serverSettings.test.ts @@ -500,11 +500,21 @@ it.layer(NodeServices.layer)("server settings", (it) => { undefined, ); assert.equal(next.modelSelectionPresets.claudeAgent["starter-claude-free"]?.name, "Free"); + assert.deepEqual( + next.modelSelectionPresets.claudeAgent["starter-claude-pro"]?.planModelSelection, + { + provider: "claudeAgent", + model: "claude-haiku-4-5", + options: { + thinking: true, + }, + }, + ); assert.deepEqual( next.modelSelectionPresets.claudeAgent["starter-claude-max-20x"]?.reviewModelSelection, { provider: "claudeAgent", - model: "claude-opus-4-6", + model: "claude-opus-4-7", options: { effort: "high", }, diff --git a/apps/server/src/serverSettings.ts b/apps/server/src/serverSettings.ts index 342ee8dc..4cd1d887 100644 --- a/apps/server/src/serverSettings.ts +++ b/apps/server/src/serverSettings.ts @@ -255,7 +255,7 @@ const BUILT_IN_MODEL_SELECTION_PRESETS: { }), makeClaudePreset("starter-claude-pro", "Pro", { ask: { model: "claude-haiku-4-5", thinking: false }, - plan: { model: "claude-sonnet-4-6", effort: "low" }, + plan: { model: "claude-haiku-4-5", thinking: true }, code: { model: "claude-sonnet-4-6", effort: "low" }, review: { model: "claude-sonnet-4-6", effort: "medium" }, }), @@ -269,7 +269,7 @@ const BUILT_IN_MODEL_SELECTION_PRESETS: { ask: { model: "claude-sonnet-4-6", effort: "low" }, plan: { model: "claude-sonnet-4-6", effort: "high" }, code: { model: "claude-sonnet-4-6", effort: "medium" }, - review: { model: "claude-opus-4-6", effort: "high" }, + review: { model: "claude-opus-4-7", effort: "high" }, }), ], }; diff --git a/apps/web/src/components/chat/ModelPickerContent.tsx b/apps/web/src/components/chat/ModelPickerContent.tsx index 991f1c95..ca175c8b 100644 --- a/apps/web/src/components/chat/ModelPickerContent.tsx +++ b/apps/web/src/components/chat/ModelPickerContent.tsx @@ -39,6 +39,7 @@ export const ModelPickerContent = memo(function ModelPickerContent(props: { providers?: ReadonlyArray; keybindings?: ResolvedKeybindingsConfig; modelOptionsByProvider: Record>; + includeAutoModel?: boolean; showAsAuto?: boolean; terminalOpen?: boolean; onRequestClose?: () => void; @@ -127,13 +128,15 @@ export const ModelPickerContent = memo(function ModelPickerContent(props: { return []; } - return [autoModelByProvider[providerKind as ProviderKind], ...models].map((model) => - Object.assign({}, model, { - provider: providerKind as ProviderKind, - }), + const provider = providerKind as ProviderKind; + const providerModels = + props.includeAutoModel === false ? models : [autoModelByProvider[provider], ...models]; + + return providerModels.map((model) => + Object.assign({}, model, { provider }), ) satisfies Array; }); - }, [autoModelByProvider, props.modelOptionsByProvider, readyProviderSet]); + }, [autoModelByProvider, props.includeAutoModel, props.modelOptionsByProvider, readyProviderSet]); const filteredModels = useMemo(() => { let result = flatModels; @@ -409,9 +412,9 @@ export const ModelPickerContent = memo(function ModelPickerContent(props: { isLocked ? "flex-col" : "flex-row", )} > - {isLocked && LockedProviderIcon && props.lockedProvider ? ( + {isLocked && props.lockedProvider ? (
- + {LockedProviderIcon ? : null} {PROVIDER_DISPLAY_NAMES[props.lockedProvider]} diff --git a/apps/web/src/components/chat/ProviderModelPicker.browser.tsx b/apps/web/src/components/chat/ProviderModelPicker.browser.tsx index 280e5322..55c97745 100644 --- a/apps/web/src/components/chat/ProviderModelPicker.browser.tsx +++ b/apps/web/src/components/chat/ProviderModelPicker.browser.tsx @@ -121,6 +121,7 @@ async function mountPicker(props: { providers?: ReadonlyArray; keybindings?: ResolvedKeybindingsConfig; triggerVariant?: "ghost" | "outline"; + includeAutoModel?: boolean; showAsAuto?: boolean; }) { const host = document.createElement("div"); @@ -141,6 +142,9 @@ async function mountPicker(props: { providers={providers} modelOptionsByProvider={modelOptionsByProvider} {...(props.keybindings ? { keybindings: props.keybindings } : {})} + {...(props.includeAutoModel !== undefined + ? { includeAutoModel: props.includeAutoModel } + : {})} {...(props.showAsAuto !== undefined ? { showAsAuto: props.showAsAuto } : {})} triggerVariant={props.triggerVariant} onProviderModelChange={onProviderModelChange} @@ -287,6 +291,28 @@ describe("ProviderModelPicker", () => { } }); + it("hides Auto when auto model selection is disabled", async () => { + const mounted = await mountPicker({ + provider: "claudeAgent", + model: "claude-opus-4-6", + lockedProvider: "claudeAgent", + includeAutoModel: false, + }); + + try { + await page.getByRole("button").click(); + + await vi.waitFor(() => { + expect(getVisibleModelNames().some((name) => name.includes("Auto"))).toBe(false); + expect(getVisibleModelNames().some((name) => name.includes("Claude Sonnet 4.6"))).toBe( + true, + ); + }); + } finally { + await mounted.cleanup(); + } + }); + it("shows jump shortcut labels when keybindings are provided", async () => { const mounted = await mountPicker({ provider: "codex", diff --git a/apps/web/src/components/chat/ProviderModelPicker.tsx b/apps/web/src/components/chat/ProviderModelPicker.tsx index 74db8970..c18511b0 100644 --- a/apps/web/src/components/chat/ProviderModelPicker.tsx +++ b/apps/web/src/components/chat/ProviderModelPicker.tsx @@ -33,6 +33,7 @@ export const ProviderModelPicker = memo(function ProviderModelPicker(props: { activeProviderIconClassName?: string; compact?: boolean; disabled?: boolean; + includeAutoModel?: boolean; showAsAuto?: boolean; terminalOpen?: boolean; open?: boolean; @@ -159,6 +160,9 @@ export const ProviderModelPicker = memo(function ProviderModelPicker(props: { {...(props.providers ? { providers: props.providers } : {})} {...(props.keybindings ? { keybindings: props.keybindings } : {})} modelOptionsByProvider={props.modelOptionsByProvider} + {...(props.includeAutoModel !== undefined + ? { includeAutoModel: props.includeAutoModel } + : {})} {...(props.showAsAuto !== undefined ? { showAsAuto: props.showAsAuto } : {})} terminalOpen={props.terminalOpen ?? false} onRequestClose={() => setIsMenuOpen(false)} diff --git a/apps/web/src/components/chat/modelPickerModelHighlights.ts b/apps/web/src/components/chat/modelPickerModelHighlights.ts index 9afc33a5..35a81d7c 100644 --- a/apps/web/src/components/chat/modelPickerModelHighlights.ts +++ b/apps/web/src/components/chat/modelPickerModelHighlights.ts @@ -4,7 +4,7 @@ import type { ProviderKind } from "@t3tools/contracts"; * Keep release-callout logic centralized so future model launches do not need * ad hoc badge checks spread across the picker rows. */ -const NEW_MODEL_KEYS = new Set([]); +const NEW_MODEL_KEYS = new Set(["claudeAgent:claude-opus-4-7"]); export function isModelPickerNewModel(provider: ProviderKind, slug: string): boolean { return NEW_MODEL_KEYS.has(`${provider}:${slug}`); diff --git a/apps/web/src/components/chat/providerIconUtils.ts b/apps/web/src/components/chat/providerIconUtils.ts index ca530527..d78eacfa 100644 --- a/apps/web/src/components/chat/providerIconUtils.ts +++ b/apps/web/src/components/chat/providerIconUtils.ts @@ -10,7 +10,7 @@ export const PROVIDER_ICON_BY_PROVIDER: Record = export const PROVIDER_TINT_CLASS_BY_PROVIDER: Record = { codex: "text-neutral-900 dark:text-white", - claudeAgent: "text-orange-500 dark:text-orange-300", + claudeAgent: "text-[#CC7C5E]", cursor: "text-violet-500 dark:text-violet-400", }; diff --git a/apps/web/src/components/settings/SettingsModelsSection.tsx b/apps/web/src/components/settings/SettingsModelsSection.tsx index 5716dbce..05ece28e 100644 --- a/apps/web/src/components/settings/SettingsModelsSection.tsx +++ b/apps/web/src/components/settings/SettingsModelsSection.tsx @@ -22,7 +22,6 @@ import { import { ensureNativeApi } from "../../nativeApi"; import { useServerProviders } from "../../rpc/serverState"; import type { SettingsUpdatePatch } from "../../hooks/useSettings"; -import { cn } from "../../lib/utils"; import { Button } from "../ui/button"; import { ClaudeAI, OpenAI } from "../Icons"; import { Select, SelectItem, SelectPopup, SelectTrigger } from "../ui/select"; @@ -237,8 +236,8 @@ const BUILT_IN_PRESET_MODE_SELECTIONS: Readonly< }, planModelSelection: { provider: "claudeAgent", - model: "claude-sonnet-4-6", - options: { effort: "low" }, + model: "claude-haiku-4-5", + options: { thinking: true }, }, codeModelSelection: { provider: "claudeAgent", @@ -291,7 +290,7 @@ const BUILT_IN_PRESET_MODE_SELECTIONS: Readonly< }, reviewModelSelection: { provider: "claudeAgent", - model: "claude-opus-4-6", + model: "claude-opus-4-7", options: { effort: "high" }, }, }, @@ -325,10 +324,8 @@ function ProviderPresetLabel({ provider }: { provider: ProviderKind }) { diff --git a/apps/web/src/components/settings/SettingsPanelPrimitives.tsx b/apps/web/src/components/settings/SettingsPanelPrimitives.tsx index 8f448695..6ee2172c 100644 --- a/apps/web/src/components/settings/SettingsPanelPrimitives.tsx +++ b/apps/web/src/components/settings/SettingsPanelPrimitives.tsx @@ -111,6 +111,7 @@ export function ModelSelectionControl({ lockedProvider={lockedProvider ?? null} providers={providers} modelOptionsByProvider={modelOptionsByProvider} + includeAutoModel={false} triggerVariant="outline" triggerClassName="min-w-0 max-w-none shrink-0 text-foreground/90 hover:text-foreground" onProviderModelChange={onProviderModelChange} diff --git a/packages/contracts/src/model.ts b/packages/contracts/src/model.ts index e62a957e..db5b2b95 100644 --- a/packages/contracts/src/model.ts +++ b/packages/contracts/src/model.ts @@ -73,7 +73,9 @@ export const MODEL_SLUG_ALIASES_BY_PROVIDER: Record { it("maps known aliases to canonical slugs", () => { expect(normalizeModelSlug("5.3")).toBe("gpt-5.3-codex"); expect(normalizeModelSlug("sonnet", "claudeAgent")).toBe("claude-sonnet-4-6"); + expect(normalizeModelSlug("opus", "claudeAgent")).toBe("claude-opus-4-7"); + expect(normalizeModelSlug("opus-4.6", "claudeAgent")).toBe("claude-opus-4-6"); }); it("normalizes auto model selection case-insensitively", () => { @@ -319,29 +321,29 @@ describe("resolveApiModelId", () => { expect( resolveApiModelId({ provider: "claudeAgent", - model: "claude-opus-4-6", + model: "claude-opus-4-7", options: { contextWindow: "1m" }, }), - ).toBe("claude-opus-4-6[1m]"); + ).toBe("claude-opus-4-7[1m]"); }); it("returns the model as-is for 200k context window", () => { expect( resolveApiModelId({ provider: "claudeAgent", - model: "claude-opus-4-6", + model: "claude-opus-4-7", options: { contextWindow: "200k" }, }), - ).toBe("claude-opus-4-6"); + ).toBe("claude-opus-4-7"); }); it("returns the model as-is when no context window is set", () => { - expect(resolveApiModelId({ provider: "claudeAgent", model: "claude-opus-4-6" })).toBe( - "claude-opus-4-6", + expect(resolveApiModelId({ provider: "claudeAgent", model: "claude-opus-4-7" })).toBe( + "claude-opus-4-7", ); expect( - resolveApiModelId({ provider: "claudeAgent", model: "claude-opus-4-6", options: {} }), - ).toBe("claude-opus-4-6"); + resolveApiModelId({ provider: "claudeAgent", model: "claude-opus-4-7", options: {} }), + ).toBe("claude-opus-4-7"); }); it("returns the model as-is for Codex selections", () => {