From 43eb4707901ebaf3f0cd48d449439918c278c282 Mon Sep 17 00:00:00 2001 From: "Bob.Hou" <364432949@qq.com> Date: Sat, 18 Jul 2026 14:13:57 -0400 Subject: [PATCH] fix(models): update Anthropic model contextLength to 1M (#7129) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * chore(ci): add .mergify.yml to main — Mergify only reads config from the default branch (#7168) * fix(ci): add the auto-enqueue pull_request_rule to the Mergify config (queue_conditions alone are eligibility-only) (#7179) * fix(ci): migrate Mergify auto-enqueue to merge_protections_settings.auto_merge_conditions (rules-based path is EOL 2026-07-16) (#7216) * fix(ci): drop Mergify batch settings (batching is a paid-tier feature; free plan queue is serial) (#7220) * fix(ci): merge queue tolerates the advisory dast-smoke failure (its GH-hosted build hang dequeued every attempt) (#7225) * test(ci): make the #6634 selfref guard hermetic — main's copy hard-fails every PR (#7341) main's copy of this test still does git I/O inside a unit test: const baseSrc = git(['show', 'origin/main:' + FILE]); Runners check out a shallow single ref, so origin/main does not resolve and the test dies with 'fatal: invalid object name origin/main'. Every PR into main fails Unit Tests (7/8) on it — today that is #7313, #7315, #7316, #7334, #7336 and #7337, six PRs red on a defect none of them introduced. #7313 has no other red at all. release/v3.8.49 already carries a fix (2e42b8efc, #7174: try/catch, fetch origin/main on demand, t.skip() when unreachable), but it only reaches main at release time — so main stays broken for the whole cycle. Cherry-picking it would also import a new problem: PR Test Policy classifies t.skip() as a silenced assertion, which we watched it correctly catch on #7300 today. This is the hermetic version instead (ported from #7327, which does the same for the release branch): read the file straight off disk, compare against an empty base so baseTaut/baseExtTaut are 0 — the strictest possible comparison point — and call evaluateMasking() directly. No git ref, no fetch, no skip, nothing the runner's checkout depth can break. The #6634 regression stays covered: the guard's logic lives in SELF_TEST_FIXTURE_RE (check-test-masking.mjs:337), not in the test. Proven both ways on main before committing — neutralise SELF_TEST_FIXTURE_RE to /$^/ and the test FAILS; restore it and it passes 2/2, with check-test-masking.mjs left byte-identical. Co-authored-by: growab * chore(quality): tighten main's coverage baseline to the CI's real numbers (#7347) main's ratchet had been failing --require-tighten on every PR: 11 metrics improved but the baseline was never tightened. Same class as the #6634 selfref guard — an infra fix that lands only on the release branch leaves main red for the whole cycle, and every PR into main pays for it. Values are the merged-coverage numbers from a run on main itself (a local run measures ~68% vs CI's ~80%; the baseline's own note warns about that gap). Only the 11 coverage values change — gitleaks and semgrepFindings keep main's own state. No changelog fragment: #7326 carries it on release/v3.8.49, and a second one here would double the entry at release time. * fix(models): update Anthropic model contextLength to 1M Opus 4.6, Sonnet 4.6, and Sonnet 5 all have 1M context window since GA (2026-03-13). Update: - agyModels + antigravityModelAliases: 200000 -> 1048576 (binary 1M, matches existing convention in those files) - claude registry: 200000 -> 1000000 (decimal 1M, matches other entries in that file) Older models (opus-4-5, sonnet-4-5, haiku-4-5) and defaultContextLength: 200000 left untouched. Signed-off-by: Minxi Hou * fix(claude): add sonnet-4-6 to CONTEXT_1M_SUPPORTED_MODELS Sonnet 4.6 has 1M context GA since 2026-02-17. Without this entry, the CC-compatible wire image omits the context-1m beta header for Sonnet 4.6 requests, causing large-context requests to be rejected by Anthropic. * test: regression test for CONTEXT_1M_SUPPORTED_MODELS allowlist Verify that every Claude model with contextLength > 200K has a matching entry in the beta header allowlist, and vice versa. Parses source files directly (no module imports needed). Signed-off-by: Minxi Hou * test: improve regression test with empty-registry guard Add sanity check that parser finds at least one model. Handle type annotations in CONTEXT_1M_SUPPORTED_MODELS assignment. Remove weaker subagent test. Signed-off-by: Minxi Hou * test: update stale contextLength expectations for the 1M Sonnet/Opus 4.6 fix Three pre-existing tests hardcoded the old 200000 contextLength for claude-opus-4-6-thinking / claude-sonnet-4-6 / claude-sonnet-5, which this PR's own registry change (agyModels.ts, antigravityModelAliases.ts, claude registry) legitimately bumped to 1M GA: - antigravity-model-aliases.test.ts: deepEqual assertions for claude-opus-4-6-thinking and claude-sonnet-5 in the Antigravity catalog expected contextLength: 200000; now 1048576. Added an explicit contextLength assertion for claude-sonnet-4-6 too. - auto-combo-context-advertising.test.ts: resolveComboContextLimit regression test asserted the claude target's own limit as 200000; the test's intent (own limit wins over an 8k sibling, not compressed) is unchanged, only the registry's current correct value (1000000) needed updating. Also refreshed a stale comment in the MAX-of-candidates test (functionally unaffected, since gemini's 1048576 already wins either way). - models-catalog-route.test.ts: refreshed a stale inline comment (the actual assertion checks the MIN across combo targets, 128000, unaffected by the 1M bump). Verified via Anthropic's own 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" -- matches this PR's claimed 2026-03-13 GA date exactly, and Google Cloud coverage corroborates the Antigravity-hosted ids. Independently corroborated for the Antigravity-specific path by unrelated third-party projects fixing the same gap (earendil-works/pi#2209, badlogic/pi-mono#2194). Swept the full test suite for other contextLength/contextWindow assertions against these three model ids (grep + targeted runs across models-catalog-route, model-capabilities-registry, t31-t33-t34-t38-model-specs, executor-antigravity, agy-provider, provider-models-config, model-metadata-registry, claude-web-sonnet5-registry, combo-routing-engine, command-code-executor, combo-lockout-quota-reset -- 157 tests green); the Bedrock-hosted registry and modelSpecs.ts module were already correctly at 1000000 for these models, reinforcing this PR's direction. Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> --------- Signed-off-by: Minxi Hou Co-authored-by: Diego Rodrigues de Sa e Souza <8016841+diegosouzapw@users.noreply.github.com> Co-authored-by: growab Co-authored-by: Diego Rodrigues de Sa e Souza --- open-sse/config/agyModels.ts | 4 +- open-sse/config/antigravityModelAliases.ts | 6 +- .../config/providers/registry/claude/index.ts | 2 +- open-sse/services/claudeCodeCompatible.ts | 1 + tests/unit/antigravity-model-aliases.test.ts | 10 +- .../auto-combo-context-advertising.test.ts | 8 +- ...claude-context-1m-supported-models.test.ts | 153 ++++++++++++++++++ tests/unit/models-catalog-route.test.ts | 2 +- 8 files changed, 175 insertions(+), 11 deletions(-) create mode 100644 tests/unit/claude-context-1m-supported-models.test.ts 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",