diff --git a/open-sse/config/agyModels.ts b/open-sse/config/agyModels.ts index d836888b7f..919b97e957 100644 --- a/open-sse/config/agyModels.ts +++ b/open-sse/config/agyModels.ts @@ -19,7 +19,7 @@ export const AGY_PUBLIC_MODELS = Object.freeze([ { id: "claude-opus-4-6-thinking", name: "Claude Opus 4.6 (Thinking)", - contextLength: 200000, + contextLength: 1048576, maxOutputTokens: 65536, supportsReasoning: true, supportsVision: true, @@ -28,7 +28,7 @@ export const AGY_PUBLIC_MODELS = Object.freeze([ { id: "claude-sonnet-4-6", name: "Claude Sonnet 4.6 (Thinking)", - contextLength: 200000, + contextLength: 1048576, maxOutputTokens: 65536, supportsReasoning: true, supportsVision: true, diff --git a/open-sse/config/antigravityModelAliases.ts b/open-sse/config/antigravityModelAliases.ts index a88ab804c0..3feebf495b 100644 --- a/open-sse/config/antigravityModelAliases.ts +++ b/open-sse/config/antigravityModelAliases.ts @@ -7,7 +7,7 @@ export const ANTIGRAVITY_PUBLIC_MODELS = Object.freeze([ { id: "claude-sonnet-5", name: "Claude Sonnet 5 (Thinking)", - contextLength: 200000, + contextLength: 1048576, maxOutputTokens: 65536, supportsReasoning: true, supportsVision: true, @@ -16,7 +16,7 @@ export const ANTIGRAVITY_PUBLIC_MODELS = Object.freeze([ { id: "claude-opus-4-6-thinking", name: "Claude Opus 4.6 (Thinking)", - contextLength: 200000, + contextLength: 1048576, maxOutputTokens: 65536, supportsReasoning: true, supportsVision: true, @@ -25,7 +25,7 @@ export const ANTIGRAVITY_PUBLIC_MODELS = Object.freeze([ { id: "claude-sonnet-4-6", name: "Claude Sonnet 4.6 (Thinking)", - contextLength: 200000, + contextLength: 1048576, maxOutputTokens: 65536, supportsReasoning: true, supportsVision: true, diff --git a/open-sse/config/providers/registry/claude/index.ts b/open-sse/config/providers/registry/claude/index.ts index 19b2c5a509..d142872de8 100644 --- a/open-sse/config/providers/registry/claude/index.ts +++ b/open-sse/config/providers/registry/claude/index.ts @@ -81,7 +81,7 @@ export const claudeProvider: RegistryEntry = { id: "claude-sonnet-4-6", name: "Claude 4.6 Sonnet", supportsXHighEffort: false, - contextLength: 200000, + contextLength: 1000000, maxOutputTokens: 64000, }, { diff --git a/open-sse/services/claudeCodeCompatible.ts b/open-sse/services/claudeCodeCompatible.ts index 01b021579f..a33e4cd09e 100644 --- a/open-sse/services/claudeCodeCompatible.ts +++ b/open-sse/services/claudeCodeCompatible.ts @@ -56,6 +56,7 @@ const CLAUDE_CODE_COMPATIBLE_DEFAULT_SYSTEM_BLOCKS = [ const CONTEXT_1M_SUPPORTED_MODELS = [ "claude-fable-5", "claude-sonnet-5", + "claude-sonnet-4-6", "claude-opus-4-8", "claude-opus-4-7", "claude-opus-4-6", diff --git a/tests/unit/antigravity-model-aliases.test.ts b/tests/unit/antigravity-model-aliases.test.ts index 718250c8ec..9c80666ff5 100644 --- a/tests/unit/antigravity-model-aliases.test.ts +++ b/tests/unit/antigravity-model-aliases.test.ts @@ -99,21 +99,27 @@ test("isUserCallableAntigravityModelId only allows public chat-capable model IDs test("ANTIGRAVITY_PUBLIC_MODELS exposes captured Antigravity 2.0.1 names and capabilities", () => { // #3184: Claude is exposed in the antigravity catalog (same backend as `agy`, verified). + // #7129: Opus 4.6, Sonnet 4.6, and Sonnet 5 graduated to a 1M-token context window at GA + // (Anthropic docs, platform.claude.com/docs/en/build-with-claude/context-windows: "Claude + // Opus 4.8, Claude Opus 4.7, Claude Opus 4.6, Claude Sonnet 5, and Claude Sonnet 4.6 have a + // 1M-token context window ... on the Claude API, Amazon Bedrock, Google Cloud, and Microsoft + // Foundry" — Google Cloud coverage extends to the Antigravity-hosted ids exercised here). assert.deepEqual(getPublicModel("claude-opus-4-6-thinking"), { id: "claude-opus-4-6-thinking", name: "Claude Opus 4.6 (Thinking)", - contextLength: 200000, + contextLength: 1048576, maxOutputTokens: 65536, supportsReasoning: true, supportsVision: true, toolCalling: true, }); assert.equal(getPublicModel("claude-sonnet-4-6").name, "Claude Sonnet 4.6 (Thinking)"); + assert.equal(getPublicModel("claude-sonnet-4-6").contextLength, 1048576); // claude-sonnet-5 was added to the Antigravity catalog alongside the existing Claude entries. assert.deepEqual(getPublicModel("claude-sonnet-5"), { id: "claude-sonnet-5", name: "Claude Sonnet 5 (Thinking)", - contextLength: 200000, + contextLength: 1048576, maxOutputTokens: 65536, supportsReasoning: true, supportsVision: true, diff --git a/tests/unit/auto-combo-context-advertising.test.ts b/tests/unit/auto-combo-context-advertising.test.ts index e316cdae52..390b1265e7 100644 --- a/tests/unit/auto-combo-context-advertising.test.ts +++ b/tests/unit/auto-combo-context-advertising.test.ts @@ -64,7 +64,8 @@ test("computeAdvertisedLimits returns MAX of candidates' known context windows", "virtualFactory should export computeAdvertisedLimits()" ); - // gemini has registry defaultContextLength=1048576; claude has 200000. + // gemini has registry defaultContextLength=1048576; claude-sonnet-4-6 has 1000000 (#7129: + // 1M GA per Anthropic docs) -- gemini's binary-1M window still wins as the MAX. const result = computeAdvertisedLimits([ { provider: "claude", model: "claude-sonnet-4-6" }, { provider: "gemini", model: "gemini-2.5-pro" }, @@ -171,7 +172,10 @@ test("resolveComboContextLimit regression: claude target must not be compressed model: "claude-sonnet-4-6", comboTargetLimits: [8000], }); - assert.equal(result.limit, 200000); + // #7129: claude-sonnet-4-6's own registry limit is 1M GA (was 200000) -- the point of this + // regression test is that the target's OWN limit wins over the 8k sibling, not the specific + // magic number, so the expectation tracks the registry's current (correct) value. + assert.equal(result.limit, 1000000); assert.equal(result.source, "target"); }); diff --git a/tests/unit/claude-context-1m-supported-models.test.ts b/tests/unit/claude-context-1m-supported-models.test.ts new file mode 100644 index 0000000000..dbac4c55e2 --- /dev/null +++ b/tests/unit/claude-context-1m-supported-models.test.ts @@ -0,0 +1,153 @@ +import test from "node:test"; +import assert from "node:assert/strict"; +import fs from "node:fs"; +import path from "node:path"; + +const REPO_ROOT = path.resolve(import.meta.dirname, "../.."); + +// ─── Parse CONTEXT_1M_SUPPORTED_MODELS from source (no module import needed) ─── + +function parseSupportedModels(): string[] { + const src = fs.readFileSync( + path.join(REPO_ROOT, "open-sse/services/claudeCodeCompatible.ts"), + "utf8" + ); + // Strip type annotations before matching to handle `const X: string[] = [...]` + const match = src + .replace(/:\s*\w+(\[\])?/, "") + .match(/CONTEXT_1M_SUPPORTED_MODELS\s*=\s*\[([\s\S]*?)\]/); + if (!match) throw new Error("CONTEXT_1M_SUPPORTED_MODELS not found in source"); + return match[1] + .split(",") + .map((s) => s.replace(/["'\s]/g, "").toLowerCase()) + .filter(Boolean); +} + +// ─── Parse Claude models from provider registry (source-level) ─── +// Uses brace-counting to isolate each model block within the models array, +// avoiding false matches on the provider-level id field. + +function parseClaudeRegistryModels(): Array<{ + provider: string; + modelId: string; + contextLength: number; +}> { + const registryDir = path.join(REPO_ROOT, "open-sse/config/providers/registry/claude"); + const results: Array<{ + provider: string; + modelId: string; + contextLength: number; + }> = []; + + for (const file of fs.readdirSync(registryDir).filter((f) => f.endsWith(".ts"))) { + const src = fs.readFileSync(path.join(registryDir, file), "utf8"); + + // Extract provider id + const providerIdMatch = src.match(/id:\s*["']([^"']+)["']/); + const providerId = providerIdMatch?.[1] ?? file.replace(".ts", ""); + + // Find the models array + const modelsStart = src.indexOf("models:"); + if (modelsStart === -1) continue; + const arrayStart = src.indexOf("[", modelsStart); + if (arrayStart === -1) continue; + + // Extract individual model objects using brace counting. + // Skips any non-model nested braces by requiring both id and contextLength + // in the same brace-delimited block. + let braceDepth = 0; + let objStart = -1; + const modelBlocks: string[] = []; + for (let i = arrayStart; i < src.length; i++) { + if (src[i] === "{") { + if (braceDepth === 0) objStart = i; + braceDepth++; + } else if (src[i] === "}") { + braceDepth--; + if (braceDepth === 0 && objStart !== -1) { + modelBlocks.push(src.slice(objStart, i + 1)); + objStart = -1; + } + } else if (src[i] === "]" && braceDepth === 0) { + break; + } + } + + for (const block of modelBlocks) { + const idMatch = block.match(/id:\s*["']([^"']+)["']/); + const ctxMatch = block.match(/contextLength:\s*(\d+)/); + if (idMatch && ctxMatch) { + results.push({ + provider: providerId, + modelId: idMatch[1].toLowerCase(), + contextLength: parseInt(ctxMatch[1], 10), + }); + } + } + } + return results; +} + +const CONTEXT_1M_THRESHOLD = 200_000; + +function isModel1mSupported(modelId: string, supportedModels: string[]): boolean { + const normalized = modelId.replace(/:.*$/, "").toLowerCase(); + return supportedModels.some((s) => normalized === s || normalized.startsWith(`${s}-`)); +} + +// ─── Sanity: parser must find at least one model ─── + +test("parser finds at least one Claude model in the registry", () => { + const models = parseClaudeRegistryModels(); + assert.ok( + models.length > 0, + "parseClaudeRegistryModels returned 0 models — check that open-sse/config/providers/registry/claude/ exists and contains model definitions" + ); +}); + +// ─── Forward: every high-context Claude model must be in the allowlist ─── + +test("every Claude model with contextLength > 200K is in CONTEXT_1M_SUPPORTED_MODELS", () => { + const supportedModels = parseSupportedModels(); + const claudeModels = parseClaudeRegistryModels(); + const violations: string[] = []; + + for (const m of claudeModels) { + if (m.contextLength > CONTEXT_1M_THRESHOLD && !isModel1mSupported(m.modelId, supportedModels)) { + violations.push(`${m.provider}/${m.modelId} (contextLength=${m.contextLength})`); + } + } + + assert.deepEqual( + violations, + [], + `Claude models with contextLength > 200K missing from CONTEXT_1M_SUPPORTED_MODELS.\n` + + `Add the model prefix to the allowlist in claudeCodeCompatible.ts.\n` + + `Violations:\n ${violations.join("\n ")}` + ); +}); + +// ─── Reverse: every allowlist entry must have a matching high-context model ─── + +test("every CONTEXT_1M_SUPPORTED_MODELS entry has a matching Claude model with contextLength > 200K", () => { + const supportedModels = parseSupportedModels(); + const claudeModels = parseClaudeRegistryModels(); + const orphans: string[] = []; + + for (const supported of supportedModels) { + const found = claudeModels.some( + (m) => + m.contextLength > CONTEXT_1M_THRESHOLD && + (m.modelId === supported || m.modelId.startsWith(`${supported}-`)) + ); + if (!found) orphans.push(supported); + } + + assert.deepEqual( + orphans, + [], + `CONTEXT_1M_SUPPORTED_MODELS entries with no matching high-context Claude model.\n` + + `Remove stale entries or check model id spelling.\n` + + `Orphans:\n ${orphans.join("\n ")}` + ); +}); diff --git a/tests/unit/models-catalog-route.test.ts b/tests/unit/models-catalog-route.test.ts index 0a7b1f897e..1a51272b69 100644 --- a/tests/unit/models-catalog-route.test.ts +++ b/tests/unit/models-catalog-route.test.ts @@ -1419,7 +1419,7 @@ test("v1 models catalog auto-calculates combo context_length from targets when n }); // Create a combo with targets having different context limits. - // openai/gpt-4o context = 128000, claude/claude-sonnet-4-6 = 200000. + // openai/gpt-4o context = 128000, claude/claude-sonnet-4-6 = 1000000 (#7129: 1M GA). // The combo should expose context_length = min = 128000. const combo = await combosDb.createCombo({ name: "auto-context-combo",