diff --git a/packages/protocol/src/agent-types.ts b/packages/protocol/src/agent-types.ts index 9f90cc65e8..fc95ae4eb9 100644 --- a/packages/protocol/src/agent-types.ts +++ b/packages/protocol/src/agent-types.ts @@ -77,6 +77,7 @@ export interface AgentModelDefinition { label: string; description?: string; isDefault?: boolean; + contextWindowMaxTokens?: number; metadata?: AgentMetadata; thinkingOptions?: AgentSelectOption[]; defaultThinkingOptionId?: string; diff --git a/packages/protocol/src/messages.providers-snapshot.test.ts b/packages/protocol/src/messages.providers-snapshot.test.ts index 1b066062d4..f58b3867c8 100644 --- a/packages/protocol/src/messages.providers-snapshot.test.ts +++ b/packages/protocol/src/messages.providers-snapshot.test.ts @@ -48,6 +48,7 @@ describe("provider snapshot message schemas", () => { id: "MiniMax-M2.7", label: "MiniMax-M2.7", isDefault: true, + contextWindowMaxTokens: 200_000, thinkingOptions: [ { id: "off", label: "Off" }, { id: "max", label: "Max", isDefault: true }, @@ -62,6 +63,7 @@ describe("provider snapshot message schemas", () => { id: "MiniMax-M2.7", label: "MiniMax-M2.7", isDefault: true, + contextWindowMaxTokens: 200_000, thinkingOptions: [ { id: "off", label: "Off" }, { id: "max", label: "Max", isDefault: true }, diff --git a/packages/protocol/src/messages.ts b/packages/protocol/src/messages.ts index 7eb0edaa18..d720e82c68 100644 --- a/packages/protocol/src/messages.ts +++ b/packages/protocol/src/messages.ts @@ -242,6 +242,7 @@ const AgentModelDefinitionSchema: z.ZodType = z label: z.string(), description: z.string().optional(), isDefault: z.boolean().optional(), + contextWindowMaxTokens: z.number().optional(), metadata: z.record(z.string(), z.unknown()).optional(), thinkingOptions: z.array(AgentSelectOptionSchema).optional(), defaultThinkingOptionId: z.string().optional(), diff --git a/packages/server/src/server/agent/agent-sdk-types.ts b/packages/server/src/server/agent/agent-sdk-types.ts index 56c437ef08..9f00a656bb 100644 --- a/packages/server/src/server/agent/agent-sdk-types.ts +++ b/packages/server/src/server/agent/agent-sdk-types.ts @@ -78,6 +78,7 @@ export interface AgentModelDefinition { label: string; description?: string; isDefault?: boolean; + contextWindowMaxTokens?: number; metadata?: AgentMetadata; thinkingOptions?: AgentSelectOption[]; defaultThinkingOptionId?: string; diff --git a/packages/server/src/server/agent/providers/claude/agent.ts b/packages/server/src/server/agent/providers/claude/agent.ts index 9d4b6c4ba0..427a24ce59 100644 --- a/packages/server/src/server/agent/providers/claude/agent.ts +++ b/packages/server/src/server/agent/providers/claude/agent.ts @@ -29,7 +29,11 @@ import { mapTaskNotificationSystemRecordToToolCall, mapTaskNotificationUserContentToToolCall, } from "./task-notification-tool-call.js"; -import { getClaudeModelsWithSettings, normalizeClaudeRuntimeModelId } from "./models.js"; +import { + findClaudeModel, + getClaudeModelsWithSettings, + normalizeClaudeRuntimeModelId, +} from "./models.js"; import { parsePartialJsonObject } from "./partial-json.js"; import { ClaudeSidechainTracker } from "./sidechain-tracker.js"; import { buildClaudeFeatures, claudeModelSupportsFastMode } from "./feature-definitions.js"; @@ -1626,24 +1630,6 @@ function extractContextWindowSize(modelUsage: unknown): number | undefined { return maxContextWindow; } -function resolveInitialContextWindowSize(modelId: string | null | undefined): number | undefined { - const normalized = typeof modelId === "string" ? modelId.trim().toLowerCase() : ""; - if (!normalized) { - return undefined; - } - if (normalized.includes("[1m]") || normalized.includes("context-1m")) { - return 1_000_000; - } - const catalogModelId = normalizeClaudeRuntimeModelId(normalized); - if (catalogModelId === "claude-fable-5" || catalogModelId === "claude-sonnet-5") { - return 1_000_000; - } - if (/(?:^|[~/_-])(?:claude[-_ ]*)?(opus|sonnet|haiku)(?:$|[-_ ./])/.test(normalized)) { - return 200_000; - } - return undefined; -} - function readStreamRequestInputTokens(event: Record): number | undefined { const messageUsage = toObjectRecord(toObjectRecord(event.message)?.usage); if (!messageUsage) { @@ -1942,7 +1928,7 @@ class ClaudeAgentSession implements AgentSession { this.queryFactory = options.queryFactory; this.resolveBinary = options.resolveBinary; this.contextUsage = new ClaudeContextUsageState( - resolveInitialContextWindowSize(this.config.model), + findClaudeModel(this.config.model)?.contextWindowMaxTokens, ); const handle = options.handle; @@ -2188,7 +2174,7 @@ class ClaudeAgentSession implements AgentSession { await this.applyFastModeFeature(false, activeQuery); } this.contextUsage.setInitialContextWindowMaxTokens( - resolveInitialContextWindowSize(this.config.model), + findClaudeModel(this.config.model)?.contextWindowMaxTokens, ); this.lastOptionsModel = normalizedModelId ?? this.lastOptionsModel; this.lastRuntimeModel = 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 500ca942c9..539d47bf15 100644 --- a/packages/server/src/server/agent/providers/claude/models.test.ts +++ b/packages/server/src/server/agent/providers/claude/models.test.ts @@ -5,7 +5,7 @@ import { afterEach, describe, expect, it, vi } from "vitest"; import { createTestLogger } from "../../../../test-utils/test-logger.js"; import { ClaudeAgentClient } from "./agent.js"; -import { getClaudeModels, normalizeClaudeRuntimeModelId } from "./models.js"; +import { findClaudeModel, getClaudeModels, normalizeClaudeRuntimeModelId } from "./models.js"; const createdClaudeConfigDirs: string[] = []; @@ -56,6 +56,22 @@ describe("getClaudeModels", () => { expect(defaults[0].id).toBe("claude-opus-4-8"); }); + it("keeps fixed context windows on the model entries", () => { + const modelsById = new Map(getClaudeModels().map((model) => [model.id, model])); + + expect(modelsById.get("claude-fable-5")?.contextWindowMaxTokens).toBe(1_000_000); + expect(modelsById.get("claude-opus-4-8[1m]")?.contextWindowMaxTokens).toBe(1_000_000); + expect(modelsById.get("claude-opus-4-8")?.contextWindowMaxTokens).toBe(200_000); + expect(modelsById.get("claude-sonnet-5")?.contextWindowMaxTokens).toBe(1_000_000); + expect(modelsById.get("claude-sonnet-4-6")?.contextWindowMaxTokens).toBe(200_000); + expect(modelsById.get("claude-haiku-4-5")?.contextWindowMaxTokens).toBe(200_000); + }); + + it("finds models through the runtime normalizer", () => { + expect(findClaudeModel("claude-sonnet-5-20260101")?.id).toBe("claude-sonnet-5"); + expect(findClaudeModel("claude-sonnet-5[1m]")?.contextWindowMaxTokens).toBe(1_000_000); + }); + it("returns fresh copies each call", () => { const a = getClaudeModels(); const b = getClaudeModels(); diff --git a/packages/server/src/server/agent/providers/claude/models.ts b/packages/server/src/server/agent/providers/claude/models.ts index b978b4641b..b240b36f84 100644 --- a/packages/server/src/server/agent/providers/claude/models.ts +++ b/packages/server/src/server/agent/providers/claude/models.ts @@ -31,6 +31,7 @@ const CLAUDE_MODELS: AgentModelDefinition[] = [ id: "claude-fable-5", label: "Fable 5", description: "Fable 5 · Most powerful model", + contextWindowMaxTokens: 1_000_000, thinkingOptions: [...CLAUDE_ULTRACODE_THINKING_OPTIONS], }, { @@ -38,6 +39,7 @@ const CLAUDE_MODELS: AgentModelDefinition[] = [ 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], }, { @@ -46,6 +48,7 @@ const CLAUDE_MODELS: AgentModelDefinition[] = [ label: "Opus 4.8", description: "Opus 4.8 · Latest release", isDefault: true, + contextWindowMaxTokens: 200_000, thinkingOptions: [...CLAUDE_ULTRACODE_THINKING_OPTIONS], }, { @@ -53,6 +56,7 @@ const CLAUDE_MODELS: AgentModelDefinition[] = [ id: "claude-sonnet-5", label: "Sonnet 5", description: "Sonnet 5 · Efficient for routine tasks", + contextWindowMaxTokens: 1_000_000, thinkingOptions: [...CLAUDE_EXTENDED_THINKING_OPTIONS], }, { @@ -60,6 +64,7 @@ const CLAUDE_MODELS: AgentModelDefinition[] = [ 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], }, { @@ -67,6 +72,7 @@ const CLAUDE_MODELS: AgentModelDefinition[] = [ id: "claude-opus-4-7", label: "Opus 4.7", description: "Opus 4.7 · Previous release", + contextWindowMaxTokens: 200_000, thinkingOptions: [...CLAUDE_EXTENDED_THINKING_OPTIONS], }, { @@ -74,6 +80,7 @@ const CLAUDE_MODELS: AgentModelDefinition[] = [ 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], }, { @@ -81,6 +88,7 @@ const CLAUDE_MODELS: AgentModelDefinition[] = [ 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], }, { @@ -88,6 +96,7 @@ const CLAUDE_MODELS: AgentModelDefinition[] = [ 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], }, { @@ -95,6 +104,7 @@ const CLAUDE_MODELS: AgentModelDefinition[] = [ id: "claude-sonnet-4-6", label: "Sonnet 4.6", description: "Sonnet 4.6 · Best for everyday tasks", + contextWindowMaxTokens: 200_000, thinkingOptions: [...CLAUDE_THINKING_OPTIONS], }, { @@ -102,6 +112,7 @@ const CLAUDE_MODELS: AgentModelDefinition[] = [ id: "claude-haiku-4-5", label: "Haiku 4.5", description: "Haiku 4.5 · Fastest for quick answers", + contextWindowMaxTokens: 200_000, }, ]; @@ -117,6 +128,14 @@ export function getClaudeModels(): AgentModelDefinition[] { return CLAUDE_MODELS.map((model) => ({ ...model })); } +export function findClaudeModel(modelId: string | null | undefined): AgentModelDefinition | null { + const normalizedModelId = normalizeClaudeRuntimeModelId(modelId); + if (!normalizedModelId) { + return null; + } + return CLAUDE_MODELS.find((model) => model.id === normalizedModelId) ?? null; +} + export async function getClaudeModelsWithSettings( logger: Logger, configDir?: string,