From 7cb0ccba60da03d2e4e2de40bba47c7ff0f87e40 Mon Sep 17 00:00:00 2001 From: Mohamed Boudra Date: Fri, 3 Jul 2026 12:49:59 +0200 Subject: [PATCH] Show Claude Ultra Code where supported (#1872) * fix(providers): show Claude Ultra Code by capability Expose Ultra Code for xhigh-capable Claude models and keep first-party Claude model metadata in one manifest so future model updates do not need scattered feature lists. * fix(providers): preserve Claude fast mode aliases Normalize versioned Claude model IDs through the manifest before checking fast-mode support so dated Opus IDs keep the same feature surface as their base model. * fix(providers): keep Claude fast mode first-party Anchor Claude model normalization so custom settings models that merely contain first-party IDs do not inherit manifest-only fast mode. * fix(providers): preserve Claude runtime aliases Keep Fast Mode capability checks strict to manifest IDs while allowing provider-form Claude runtime model strings to resolve known model metadata. --- docs/providers.md | 2 + .../agent/providers/claude/agent.test.ts | 31 +- .../server/agent/providers/claude/agent.ts | 7 +- .../providers/claude/feature-definitions.ts | 21 +- .../agent/providers/claude/model-manifest.ts | 282 ++++++++++++++++++ .../agent/providers/claude/models.test.ts | 63 ++++ .../server/agent/providers/claude/models.ts | 158 +--------- 7 files changed, 388 insertions(+), 176 deletions(-) create mode 100644 packages/server/src/server/agent/providers/claude/model-manifest.ts diff --git a/docs/providers.md b/docs/providers.md index 90edbde87..6af942119 100644 --- a/docs/providers.md +++ b/docs/providers.md @@ -18,6 +18,8 @@ Implement the `AgentClient` and `AgentSession` interfaces from `agent-sdk-types. Existing direct providers: `claude` (in `providers/claude/agent.ts`), `codex` (`codex-app-server-agent.ts`), `opencode` (`opencode-agent.ts`), `pi` (`providers/pi/agent.ts`), and `omp` (a Pi-compatible built-in backed by the Pi adapter). The dev-only `mock` provider (`mock-load-test-agent.ts`) is also direct. +Claude first-party model metadata lives in `packages/server/src/server/agent/providers/claude/model-manifest.ts`. When adding or updating a Claude model, update that manifest only; the model picker thinking options and Claude-specific feature gates are derived from the manifest. Do not add model-specific Claude capability lists in feature code. + Paseo tools are not implemented as MCP tools internally. They live in a shared tool catalog under `packages/server/src/server/agent/tools/`; MCP is only the fallback adapter. A provider that can register runtime tools directly should set `supportsNativePaseoTools: true` and consume `launchContext.paseoTools` in `createSession`/`resumeSession`. When native tools are present, `AgentManager` strips the internal Paseo MCP server from the provider launch config so the provider does not receive the same tools twice. Providers that only know MCP should keep `supportsMcpServers: true` and let the daemon inject `/mcp/agents`. Pi is a process-backed provider. Paseo requires the user to have the `pi` binary installed and talks to it through `pi --mode rpc`; the server package does not embed Pi's SDK/runtime packages. diff --git a/packages/server/src/server/agent/providers/claude/agent.test.ts b/packages/server/src/server/agent/providers/claude/agent.test.ts index 69222e1e6..20f8f5235 100644 --- a/packages/server/src/server/agent/providers/claude/agent.test.ts +++ b/packages/server/src/server/agent/providers/claude/agent.test.ts @@ -438,7 +438,7 @@ describe("ClaudeAgentClient.fetchCatalog", () => { } }); - test("exposes Ultracode only on Claude models that support it", async () => { + test("exposes Ultra Code on xhigh-capable Claude models", async () => { const emptyConfigDir = await fs.mkdtemp(path.join(os.tmpdir(), "paseo-claude-models-empty-")); try { const client = new ClaudeAgentClient({ @@ -459,8 +459,9 @@ describe("ClaudeAgentClient.fetchCatalog", () => { expect(getThinkingIds("claude-opus-4-8[1m]")).toContain("ultracode"); expect(getThinkingIds("claude-opus-4-8")).toContain("ultracode"); expect(getThinkingIds("claude-sonnet-5")).toContain("xhigh"); - expect(getThinkingIds("claude-sonnet-5")).not.toContain("ultracode"); - expect(getThinkingIds("claude-opus-4-7")).not.toContain("ultracode"); + expect(getThinkingIds("claude-sonnet-5")).toContain("ultracode"); + expect(getThinkingIds("claude-opus-4-7[1m]")).toContain("ultracode"); + expect(getThinkingIds("claude-opus-4-7")).toContain("ultracode"); expect(getThinkingIds("claude-sonnet-4-6")).not.toContain("ultracode"); } finally { await fs.rm(emptyConfigDir, { recursive: true, force: true }); @@ -597,6 +598,30 @@ describe("ClaudeAgentSession features", () => { }), ).resolves.toEqual([expect.objectContaining({ id: "fast_mode", value: false })]); + await expect( + client.listFeatures({ + provider: "claude", + cwd: process.cwd(), + model: "claude-opus-4-8[1m]", + }), + ).resolves.toEqual([expect.objectContaining({ id: "fast_mode", value: false })]); + + await expect( + client.listFeatures({ + provider: "claude", + cwd: process.cwd(), + model: "claude-opus-4-8-20260101", + }), + ).resolves.toEqual([expect.objectContaining({ id: "fast_mode", value: false })]); + + await expect( + client.listFeatures({ + provider: "claude", + cwd: process.cwd(), + model: "openrouter/anthropic/claude-opus-4-8", + }), + ).resolves.toEqual([]); + await expect( client.listFeatures({ provider: "claude", diff --git a/packages/server/src/server/agent/providers/claude/agent.ts b/packages/server/src/server/agent/providers/claude/agent.ts index 427a24ce5..4b2d63627 100644 --- a/packages/server/src/server/agent/providers/claude/agent.ts +++ b/packages/server/src/server/agent/providers/claude/agent.ts @@ -34,6 +34,7 @@ import { getClaudeModelsWithSettings, normalizeClaudeRuntimeModelId, } from "./models.js"; +import { CLAUDE_ULTRACODE_THINKING_OPTION_ID } from "./model-manifest.js"; import { parsePartialJsonObject } from "./partial-json.js"; import { ClaudeSidechainTracker } from "./sidechain-tracker.js"; import { buildClaudeFeatures, claudeModelSupportsFastMode } from "./feature-definitions.js"; @@ -373,7 +374,7 @@ interface ClaudeAgentSessionOptions { } type ClaudeThinkingEffort = "low" | "medium" | "high" | "xhigh" | "max"; -type ClaudeThinkingOption = ClaudeThinkingEffort | "ultracode"; +type ClaudeThinkingOption = ClaudeThinkingEffort | typeof CLAUDE_ULTRACODE_THINKING_OPTION_ID; function resolvePathEnvKey(): "Path" | "PATH" | null { if (process.env["Path"] !== undefined) return "Path"; @@ -421,7 +422,7 @@ function isClaudeThinkingEffort(value: string | null | undefined): value is Clau } function isClaudeThinkingOption(value: string | null | undefined): value is ClaudeThinkingOption { - return value === "ultracode" || isClaudeThinkingEffort(value); + return value === CLAUDE_ULTRACODE_THINKING_OPTION_ID || isClaudeThinkingEffort(value); } interface ClaudeOptionsLogSummary { @@ -2864,7 +2865,7 @@ class ClaudeAgentSession implements AgentSession { this.config.thinkingOptionId && this.config.thinkingOptionId !== "default" ? this.config.thinkingOptionId : undefined; - if (thinkingOptionId === "ultracode") { + if (thinkingOptionId === CLAUDE_ULTRACODE_THINKING_OPTION_ID) { return { thinking: { type: "adaptive" }, effort: "xhigh", ultracode: true }; } if (thinkingOptionId && isClaudeThinkingEffort(thinkingOptionId)) { diff --git a/packages/server/src/server/agent/providers/claude/feature-definitions.ts b/packages/server/src/server/agent/providers/claude/feature-definitions.ts index 39a795930..63feb4088 100644 --- a/packages/server/src/server/agent/providers/claude/feature-definitions.ts +++ b/packages/server/src/server/agent/providers/claude/feature-definitions.ts @@ -1,10 +1,5 @@ import type { AgentFeature, AgentFeatureToggle } from "../../agent-sdk-types.js"; - -const CLAUDE_FAST_MODE_SUPPORTED_MODEL_PREFIXES = [ - "claude-opus-4-8", - "claude-opus-4-7", - "claude-opus-4-6", -] as const; +import { claudeManifestModelSupportsFastMode } from "./model-manifest.js"; export const CLAUDE_FAST_MODE_FEATURE: Omit = { type: "toggle", @@ -15,20 +10,8 @@ export const CLAUDE_FAST_MODE_FEATURE: Omit = { icon: "zap", }; -function normalizeClaudeModelId(modelId: string | null | undefined): string | null { - const normalized = typeof modelId === "string" ? modelId.trim() : ""; - return normalized.length > 0 ? normalized : null; -} - export function claudeModelSupportsFastMode(modelId: string | null | undefined): boolean { - const normalizedModelId = normalizeClaudeModelId(modelId); - if (!normalizedModelId) { - return false; - } - - return CLAUDE_FAST_MODE_SUPPORTED_MODEL_PREFIXES.some( - (prefix) => normalizedModelId === prefix || normalizedModelId.startsWith(`${prefix}[`), - ); + return claudeManifestModelSupportsFastMode(modelId); } export function buildClaudeFeatures(input: { diff --git a/packages/server/src/server/agent/providers/claude/model-manifest.ts b/packages/server/src/server/agent/providers/claude/model-manifest.ts new file mode 100644 index 000000000..e8262f4cf --- /dev/null +++ b/packages/server/src/server/agent/providers/claude/model-manifest.ts @@ -0,0 +1,282 @@ +import type { AgentModelDefinition, AgentSelectOption } from "../../agent-sdk-types.js"; + +type ClaudeEffortLevel = "low" | "medium" | "high" | "xhigh" | "max"; + +interface ClaudeModelManifestEntry { + id: string; + label: string; + description: string; + isDefault?: boolean; + contextWindowMaxTokens?: number; + effortLevels?: readonly ClaudeEffortLevel[]; + supportsFastMode?: boolean; +} + +const CLAUDE_EFFORT_LEVELS = { + standard: ["low", "medium", "high", "max"], + xhigh: ["low", "medium", "high", "xhigh", "max"], +} as const satisfies Record; + +const CLAUDE_EFFORT_LABELS = { + low: "Low", + medium: "Medium", + high: "High", + xhigh: "Extra High", + max: "Max", +} as const satisfies Record; + +export const CLAUDE_ULTRACODE_THINKING_OPTION_ID = "ultracode"; + +export const CLAUDE_MODEL_MANIFEST = [ + { + id: "claude-fable-5", + label: "Fable 5", + description: "Fable 5 · Most powerful model", + contextWindowMaxTokens: 1_000_000, + effortLevels: CLAUDE_EFFORT_LEVELS.xhigh, + }, + { + id: "claude-opus-4-8[1m]", + label: "Opus 4.8 1M", + description: "Opus 4.8 with 1M context window", + contextWindowMaxTokens: 1_000_000, + effortLevels: CLAUDE_EFFORT_LEVELS.xhigh, + supportsFastMode: true, + }, + { + id: "claude-opus-4-8", + label: "Opus 4.8", + description: "Opus 4.8 · Latest release", + isDefault: true, + contextWindowMaxTokens: 200_000, + effortLevels: CLAUDE_EFFORT_LEVELS.xhigh, + supportsFastMode: true, + }, + { + id: "claude-sonnet-5", + label: "Sonnet 5", + description: "Sonnet 5 · Best for everyday tasks", + contextWindowMaxTokens: 1_000_000, + effortLevels: CLAUDE_EFFORT_LEVELS.xhigh, + }, + { + id: "claude-opus-4-7[1m]", + label: "Opus 4.7 1M", + description: "Opus 4.7 with 1M context window", + contextWindowMaxTokens: 1_000_000, + effortLevels: CLAUDE_EFFORT_LEVELS.xhigh, + supportsFastMode: true, + }, + { + id: "claude-opus-4-7", + label: "Opus 4.7", + description: "Opus 4.7 · Previous release", + contextWindowMaxTokens: 200_000, + effortLevels: CLAUDE_EFFORT_LEVELS.xhigh, + supportsFastMode: true, + }, + { + id: "claude-opus-4-6[1m]", + label: "Opus 4.6 1M", + description: "Opus 4.6 with 1M context window", + contextWindowMaxTokens: 1_000_000, + effortLevels: CLAUDE_EFFORT_LEVELS.standard, + supportsFastMode: true, + }, + { + id: "claude-opus-4-6", + label: "Opus 4.6", + description: "Opus 4.6 · Most capable for complex work", + contextWindowMaxTokens: 200_000, + effortLevels: CLAUDE_EFFORT_LEVELS.standard, + supportsFastMode: true, + }, + { + id: "claude-sonnet-4-6[1m]", + label: "Sonnet 4.6 1M", + description: "Sonnet 4.6 with 1M context window", + contextWindowMaxTokens: 1_000_000, + effortLevels: CLAUDE_EFFORT_LEVELS.standard, + }, + { + id: "claude-sonnet-4-6", + label: "Sonnet 4.6", + description: "Sonnet 4.6 · Best for everyday tasks", + contextWindowMaxTokens: 200_000, + effortLevels: CLAUDE_EFFORT_LEVELS.standard, + }, + { + id: "claude-haiku-4-5", + label: "Haiku 4.5", + description: "Haiku 4.5 · Fastest for quick answers", + contextWindowMaxTokens: 200_000, + }, +] as const satisfies readonly ClaudeModelManifestEntry[]; + +function buildThinkingOptions( + effortLevels: readonly ClaudeEffortLevel[] | undefined, +): AgentSelectOption[] | undefined { + if (!effortLevels) { + return undefined; + } + + const options: AgentSelectOption[] = effortLevels.map((id) => ({ + id, + label: CLAUDE_EFFORT_LABELS[id], + })); + + if (effortLevels.includes("xhigh")) { + options.push({ id: CLAUDE_ULTRACODE_THINKING_OPTION_ID, label: "Ultra Code" }); + } + + return options; +} + +export function getClaudeManifestModels(): AgentModelDefinition[] { + return CLAUDE_MODEL_MANIFEST.map((model) => { + const thinkingOptions = buildThinkingOptions( + "effortLevels" in model ? model.effortLevels : undefined, + ); + return { + provider: "claude", + id: model.id, + label: model.label, + description: model.description, + ...("isDefault" in model && model.isDefault ? { isDefault: true } : {}), + ...(model.contextWindowMaxTokens !== undefined + ? { contextWindowMaxTokens: model.contextWindowMaxTokens } + : {}), + ...(thinkingOptions ? { thinkingOptions } : {}), + }; + }); +} + +export function isClaudeManifestModelId(modelId: string): boolean { + return CLAUDE_MODEL_MANIFEST.some((model) => model.id === modelId); +} + +export function claudeManifestModelSupportsFastMode(modelId: string | null | undefined): boolean { + const normalizedModelId = normalizeClaudeManifestModelId(modelId); + if (!normalizedModelId) { + return false; + } + return CLAUDE_MODEL_MANIFEST.some( + (model) => + model.id === normalizedModelId && + "supportsFastMode" in model && + model.supportsFastMode === true, + ); +} + +/** + * Normalize first-party Claude model IDs for manifest capability checks. Provider-prefixed + * runtime IDs intentionally use normalizeClaudeRuntimeModelId instead. + */ +export function normalizeClaudeManifestModelId(value: string | null | undefined): string | null { + const trimmed = typeof value === "string" ? value.trim() : ""; + if (!trimmed) { + return null; + } + + if (isClaudeManifestModelId(trimmed)) { + return trimmed; + } + + const singleSegmentMatch = trimmed.match( + /^(?:claude[-_ ])?(fable|opus|sonnet|haiku)[-_ ]+(\d+)(\[1m\])?(?:[-_ ]+\d{8})?$/i, + ); + if (singleSegmentMatch) { + return normalizeSingleSegmentClaudeModelId( + singleSegmentMatch[1], + singleSegmentMatch[2], + trimmed.toLowerCase().includes("[1m]"), + ); + } + + const runtimeMatch = trimmed.match( + /^(?:claude[-_ ])?(opus|sonnet|haiku)[-_ ]+(\d+)[-.](\d+)(\[1m\])?(?:[-_ ]+\d{8})?$/i, + ); + if (!runtimeMatch) { + return null; + } + + return normalizeMajorMinorClaudeModelId( + runtimeMatch[1], + runtimeMatch[2], + runtimeMatch[3], + Boolean(runtimeMatch[4]), + ); +} + +/** + * Normalize a Claude Code runtime/config model string to a known manifest ID. + * Runtime metadata may include provider prefixes such as Bedrock model IDs; feature + * gates should use normalizeClaudeManifestModelId instead. + */ +export function normalizeClaudeRuntimeModelId(value: string | null | undefined): string | null { + const normalizedManifestModelId = normalizeClaudeManifestModelId(value); + if (normalizedManifestModelId) { + return normalizedManifestModelId; + } + + const trimmed = typeof value === "string" ? value.trim() : ""; + if (!trimmed) { + return null; + } + + const singleSegmentMatch = trimmed.match( + /claude[-_ ](fable|opus|sonnet|haiku)[-_ ]+(\d+)(\[1m\])?/i, + ); + if (singleSegmentMatch) { + const normalizedModelId = normalizeSingleSegmentClaudeModelId( + singleSegmentMatch[1], + singleSegmentMatch[2], + Boolean(singleSegmentMatch[3]), + ); + if (normalizedModelId) { + return normalizedModelId; + } + } + + const runtimeMatch = trimmed.match( + /claude[-_ ](opus|sonnet|haiku)[-_ ]+(\d+)[-.](\d+)(\[1m\])?/i, + ); + if (!runtimeMatch) { + return null; + } + + return normalizeMajorMinorClaudeModelId( + runtimeMatch[1], + runtimeMatch[2], + runtimeMatch[3], + Boolean(runtimeMatch[4]), + ); +} + +function normalizeSingleSegmentClaudeModelId( + familyValue: string, + major: string, + hasOneMillionContext: boolean, +): string | null { + const family = familyValue.toLowerCase(); + const suffix = hasOneMillionContext ? "[1m]" : ""; + const candidates = [`claude-${family}-${major}${suffix}`, `claude-${family}-${major}`]; + for (const candidate of candidates) { + if (isClaudeManifestModelId(candidate)) { + return candidate; + } + } + return null; +} + +function normalizeMajorMinorClaudeModelId( + familyValue: string, + major: string, + minor: string, + hasOneMillionContext: boolean, +): string | null { + const family = familyValue.toLowerCase(); + const suffix = hasOneMillionContext ? "[1m]" : ""; + const candidate = `claude-${family}-${major}-${minor}${suffix}`; + return isClaudeManifestModelId(candidate) ? candidate : null; +} diff --git a/packages/server/src/server/agent/providers/claude/models.test.ts b/packages/server/src/server/agent/providers/claude/models.test.ts index ec66447b3..3a922b5b1 100644 --- a/packages/server/src/server/agent/providers/claude/models.test.ts +++ b/packages/server/src/server/agent/providers/claude/models.test.ts @@ -5,6 +5,11 @@ import { afterEach, describe, expect, it, vi } from "vitest"; import { createTestLogger } from "../../../../test-utils/test-logger.js"; import { ClaudeAgentClient } from "./agent.js"; +import { + CLAUDE_ULTRACODE_THINKING_OPTION_ID, + claudeManifestModelSupportsFastMode, + normalizeClaudeManifestModelId, +} from "./model-manifest.js"; import { findClaudeModel, getClaudeModels, normalizeClaudeRuntimeModelId } from "./models.js"; const createdClaudeConfigDirs: string[] = []; @@ -78,6 +83,41 @@ describe("getClaudeModels", () => { ); }); + it("derives thinking options from model effort capabilities", () => { + const models = new Map(getClaudeModels().map((model) => [model.id, model])); + + expect(models.get("claude-sonnet-5")?.thinkingOptions?.map((option) => option.id)).toEqual([ + "low", + "medium", + "high", + "xhigh", + "max", + CLAUDE_ULTRACODE_THINKING_OPTION_ID, + ]); + expect( + models + .get("claude-sonnet-5") + ?.thinkingOptions?.find((option) => option.id === CLAUDE_ULTRACODE_THINKING_OPTION_ID) + ?.label, + ).toBe("Ultra Code"); + + expect(models.get("claude-opus-4-7")?.thinkingOptions?.map((option) => option.id)).toEqual([ + "low", + "medium", + "high", + "xhigh", + "max", + CLAUDE_ULTRACODE_THINKING_OPTION_ID, + ]); + expect(models.get("claude-sonnet-4-6")?.thinkingOptions?.map((option) => option.id)).toEqual([ + "low", + "medium", + "high", + "max", + ]); + expect(models.get("claude-haiku-4-5")?.thinkingOptions).toBeUndefined(); + }); + it("returns fresh copies each call", () => { const a = getClaudeModels(); const b = getClaudeModels(); @@ -254,11 +294,34 @@ describe("normalizeClaudeRuntimeModelId", () => { expect(normalizeClaudeRuntimeModelId("gpt-5")).toBeNull(); expect(normalizeClaudeRuntimeModelId("random")).toBeNull(); }); + + it("normalizes provider-form runtime model strings", () => { + expect(normalizeClaudeRuntimeModelId("openrouter/anthropic/claude-opus-4-8")).toBe( + "claude-opus-4-8", + ); + expect(normalizeClaudeRuntimeModelId("us.anthropic.claude-opus-4-8[1m]")).toBe( + "claude-opus-4-8[1m]", + ); + expect(normalizeClaudeRuntimeModelId("us.anthropic.claude-opus-4-8-20260101")).toBe( + "claude-opus-4-8", + ); + }); }); describe("findClaudeModel", () => { it("resolves runtime model IDs to catalog entries", () => { expect(findClaudeModel("claude-sonnet-5-20260101")?.id).toBe("claude-sonnet-5"); expect(findClaudeModel("claude-sonnet-5[1m]")?.contextWindowMaxTokens).toBe(1_000_000); + expect(findClaudeModel("us.anthropic.claude-opus-4-8[1m]")?.contextWindowMaxTokens).toBe( + 1_000_000, + ); + }); +}); + +describe("claudeManifestModelSupportsFastMode", () => { + it("keeps fast mode strict to first-party manifest model IDs", () => { + expect(normalizeClaudeManifestModelId("openrouter/anthropic/claude-opus-4-8")).toBeNull(); + expect(claudeManifestModelSupportsFastMode("openrouter/anthropic/claude-opus-4-8")).toBe(false); + expect(claudeManifestModelSupportsFastMode("claude-opus-4-8-20260101")).toBe(true); }); }); diff --git a/packages/server/src/server/agent/providers/claude/models.ts b/packages/server/src/server/agent/providers/claude/models.ts index f64120775..0e82cb018 100644 --- a/packages/server/src/server/agent/providers/claude/models.ts +++ b/packages/server/src/server/agent/providers/claude/models.ts @@ -4,117 +4,10 @@ import * as path from "node:path"; import type { Logger } from "pino"; import type { AgentModelDefinition } from "../../agent-sdk-types.js"; - -const CLAUDE_THINKING_OPTIONS = [ - { id: "low", label: "Low" }, - { id: "medium", label: "Medium" }, - { id: "high", label: "High" }, - { id: "max", label: "Max" }, -] as const; - -const CLAUDE_EXTENDED_THINKING_OPTIONS = [ - { id: "low", label: "Low" }, - { id: "medium", label: "Medium" }, - { id: "high", label: "High" }, - { id: "xhigh", label: "Extra High" }, - { id: "max", label: "Max" }, -] as const; - -const CLAUDE_ULTRACODE_THINKING_OPTIONS = [ - ...CLAUDE_EXTENDED_THINKING_OPTIONS, - { id: "ultracode", label: "Ultracode" }, -] as const; - -const CLAUDE_MODELS: AgentModelDefinition[] = [ - { - provider: "claude", - id: "claude-fable-5", - label: "Fable 5", - description: "Fable 5 · Most powerful model", - contextWindowMaxTokens: 1_000_000, - thinkingOptions: [...CLAUDE_ULTRACODE_THINKING_OPTIONS], - }, - { - provider: "claude", - id: "claude-opus-4-8[1m]", - label: "Opus 4.8 1M", - description: "Opus 4.8 with 1M context window", - contextWindowMaxTokens: 1_000_000, - thinkingOptions: [...CLAUDE_ULTRACODE_THINKING_OPTIONS], - }, - { - provider: "claude", - id: "claude-opus-4-8", - label: "Opus 4.8", - description: "Opus 4.8 · Latest release", - isDefault: true, - contextWindowMaxTokens: 200_000, - thinkingOptions: [...CLAUDE_ULTRACODE_THINKING_OPTIONS], - }, - { - provider: "claude", - id: "claude-sonnet-5", - label: "Sonnet 5", - description: "Sonnet 5 · Best for everyday tasks", - contextWindowMaxTokens: 1_000_000, - thinkingOptions: [...CLAUDE_EXTENDED_THINKING_OPTIONS], - }, - { - provider: "claude", - id: "claude-opus-4-7[1m]", - label: "Opus 4.7 1M", - description: "Opus 4.7 with 1M context window", - contextWindowMaxTokens: 1_000_000, - thinkingOptions: [...CLAUDE_EXTENDED_THINKING_OPTIONS], - }, - { - provider: "claude", - id: "claude-opus-4-7", - label: "Opus 4.7", - description: "Opus 4.7 · Previous release", - contextWindowMaxTokens: 200_000, - thinkingOptions: [...CLAUDE_EXTENDED_THINKING_OPTIONS], - }, - { - provider: "claude", - id: "claude-opus-4-6[1m]", - label: "Opus 4.6 1M", - description: "Opus 4.6 with 1M context window", - contextWindowMaxTokens: 1_000_000, - thinkingOptions: [...CLAUDE_THINKING_OPTIONS], - }, - { - provider: "claude", - id: "claude-opus-4-6", - label: "Opus 4.6", - description: "Opus 4.6 · Most capable for complex work", - contextWindowMaxTokens: 200_000, - thinkingOptions: [...CLAUDE_THINKING_OPTIONS], - }, - { - provider: "claude", - id: "claude-sonnet-4-6[1m]", - label: "Sonnet 4.6 1M", - description: "Sonnet 4.6 with 1M context window", - contextWindowMaxTokens: 1_000_000, - thinkingOptions: [...CLAUDE_THINKING_OPTIONS], - }, - { - provider: "claude", - id: "claude-sonnet-4-6", - label: "Sonnet 4.6", - description: "Sonnet 4.6 · Best for everyday tasks", - contextWindowMaxTokens: 200_000, - thinkingOptions: [...CLAUDE_THINKING_OPTIONS], - }, - { - provider: "claude", - id: "claude-haiku-4-5", - label: "Haiku 4.5", - description: "Haiku 4.5 · Fastest for quick answers", - contextWindowMaxTokens: 200_000, - }, -]; +import { + getClaudeManifestModels, + normalizeClaudeRuntimeModelId as normalizeClaudeManifestRuntimeModelId, +} from "./model-manifest.js"; const CLAUDE_SETTINGS_MODEL_ENV_KEYS = [ "ANTHROPIC_MODEL", @@ -125,7 +18,7 @@ const CLAUDE_SETTINGS_MODEL_ENV_KEYS = [ ] as const; export function getClaudeModels(): AgentModelDefinition[] { - return CLAUDE_MODELS.map((model) => ({ ...model })); + return getClaudeManifestModels(); } export function findClaudeModel( @@ -135,7 +28,7 @@ export function findClaudeModel( if (!normalizedModelId) { return undefined; } - return CLAUDE_MODELS.find((model) => model.id === normalizedModelId); + return getClaudeModels().find((model) => model.id === normalizedModelId); } export async function getClaudeModelsWithSettings( @@ -236,42 +129,5 @@ function isRecord(value: unknown): value is Record { * Handles the `[1m]` suffix that the SDK appends for 1M context sessions. */ export function normalizeClaudeRuntimeModelId(value: string | null | undefined): string | null { - const trimmed = typeof value === "string" ? value.trim() : ""; - if (!trimmed) { - return null; - } - - // Check for exact match first (handles claude-opus-4-6[1m] directly) - if (CLAUDE_MODELS.some((model) => model.id === trimmed)) { - return trimmed; - } - - // Match single-segment major-version model names such as claude-fable-5 - // and claude-sonnet-5, including dated runtime strings from the SDK. - const singleSegmentMatch = trimmed.match(/(?:claude-)?(fable|opus|sonnet|haiku)[-_ ]+(\d+)/i); - if (singleSegmentMatch) { - const family = singleSegmentMatch[1].toLowerCase(); - const major = singleSegmentMatch[2]; - const suffix = trimmed.toLowerCase().includes("[1m]") ? "[1m]" : ""; - const candidates = [`claude-${family}-${major}${suffix}`, `claude-${family}-${major}`]; - for (const candidate of candidates) { - if (CLAUDE_MODELS.some((model) => model.id === candidate)) { - return candidate; - } - } - } - - // Match: claude-{family}-{major}-{minor}[1m]? possibly followed by a date suffix - const runtimeMatch = trimmed.match( - /(?:claude-)?(opus|sonnet|haiku)[-_ ]+(\d+)[-.](\d+)(\[1m\])?/i, - ); - if (!runtimeMatch) { - return null; - } - - const family = runtimeMatch[1].toLowerCase(); - const major = runtimeMatch[2]; - const minor = runtimeMatch[3]; - const suffix = runtimeMatch[4] ?? ""; - return `claude-${family}-${major}-${minor}${suffix}`; + return normalizeClaudeManifestRuntimeModelId(value); }