diff --git a/open-sse/executors/claudeIdentity.ts b/open-sse/executors/claudeIdentity.ts index da79dfb37c..ef78832a90 100644 --- a/open-sse/executors/claudeIdentity.ts +++ b/open-sse/executors/claudeIdentity.ts @@ -285,18 +285,29 @@ export function parseUpstreamMetadataUserId( // ---------- anthropic-beta selector -------------------------------------- /** - * Models that support the heavy-agent beta tier (context-1m, effort, - * advanced-tool-use). Only Opus/Sonnet are eligible — Haiku with OAuth - * authentication rejects `context-1m-2025-08-07` with - * "This authentication style is incompatible with the long context beta header" - * (issue #2454). Matches real Claude Desktop, which omits these flags for Haiku. + * Models that support the heavy-agent beta tier (effort, advanced-tool-use). + * Only Opus/Sonnet are eligible — Haiku with OAuth authentication rejects + * heavy agent flags (issue #2454). Matches real Claude Code captures. */ const HEAVY_AGENT_BETA_MODEL_PREFIXES = ["claude-opus", "claude-sonnet"]; +/** + * Models that support the context-1m beta tier. Only Opus is eligible; + * Sonnet trips long-context credit gates under OAuth full-agent traffic. + */ +const CONTEXT_1M_BETA_MODEL_PREFIXES = ["claude-opus"]; -function isHeavyAgentModel(model: unknown): boolean { +function matchesModelPrefix(model: unknown, prefixes: string[]): boolean { if (typeof model !== "string") return false; const normalized = model.toLowerCase(); - return HEAVY_AGENT_BETA_MODEL_PREFIXES.some((prefix) => normalized.includes(prefix)); + return prefixes.some((prefix) => normalized.includes(prefix)); +} + +function isHeavyAgentModel(model: unknown): boolean { + return matchesModelPrefix(model, HEAVY_AGENT_BETA_MODEL_PREFIXES); +} + +function isContext1mModel(model: unknown): boolean { + return matchesModelPrefix(model, CONTEXT_1M_BETA_MODEL_PREFIXES); } /** @@ -304,9 +315,10 @@ function isHeavyAgentModel(model: unknown): boolean { * uses three patterns: minimal probe, structured-output, and full agent. * Sending the full set on every shape is itself a fingerprint. * - * The heavy-agent flags (context-1m, effort, advanced-tool-use) are gated on - * the model as well as the shape: Haiku must never receive `context-1m`, which - * Anthropic rejects under OAuth authentication. + * The heavy-agent flags are gated on the model as well as the shape. In direct + * Claude Code captures, Sonnet receives effort/advanced-tool-use but not + * context-1m; sending context-1m to Sonnet trips Anthropic's long-context credit + * gate for accounts where direct Claude Code works. */ export function selectBetaFlags( body: Record | null | undefined, @@ -325,24 +337,27 @@ export function selectBetaFlags( const isFullAgent = hasTools && hasSystem; const effectiveModel = model ?? (typeof b.model === "string" ? b.model : ""); const isHeavyAgent = isFullAgent && isHeavyAgentModel(effectiveModel); + const isContext1m = isFullAgent && isContext1mModel(effectiveModel); const flags: string[] = []; if (isFullAgent) flags.push("claude-code-20250219"); flags.push("oauth-2025-04-20"); - if (isHeavyAgent) flags.push("context-1m-2025-08-07"); + if (isContext1m) flags.push("context-1m-2025-08-07"); flags.push( "interleaved-thinking-2025-05-14", - "redact-thinking-2026-02-12", + "thinking-token-count-2026-05-13", "context-management-2025-06-27", "prompt-caching-scope-2026-01-05" ); if (hasStructuredOutput || isFullAgent) flags.push("advisor-tool-2026-03-01"); if (hasStructuredOutput && !isFullAgent) flags.push("structured-outputs-2025-12-15"); // extended-cache-ttl is sent for all full-agent shapes (incl. Haiku); the - // heavier advanced-tool-use / effort flags are Opus/Sonnet-only. - if (isFullAgent) flags.push("extended-cache-ttl-2025-04-11", "cache-diagnosis-2026-04-07"); + // heavier afk-mode / advanced-tool-use / effort flags are Opus/Sonnet-only. + if (isFullAgent) { + flags.push("extended-cache-ttl-2025-04-11", "cache-diagnosis-2026-04-07"); + } if (isHeavyAgent) { - flags.push("advanced-tool-use-2025-11-20", "effort-2025-11-24"); + flags.push("afk-mode-2026-01-31", "advanced-tool-use-2025-11-20", "effort-2025-11-24"); } return flags.join(","); } diff --git a/open-sse/services/claudeCodeCompatible.ts b/open-sse/services/claudeCodeCompatible.ts index dad7da6f6f..33b6ad1989 100644 --- a/open-sse/services/claudeCodeCompatible.ts +++ b/open-sse/services/claudeCodeCompatible.ts @@ -50,13 +50,7 @@ const CLAUDE_CODE_COMPATIBLE_DEFAULT_SYSTEM_BLOCKS = [ text: "You are a Claude agent, built on Anthropic's Claude Agent SDK.", }, ]; -const CONTEXT_1M_SUPPORTED_MODELS = [ - "claude-opus-4-7", - "claude-opus-4-6", - "claude-sonnet-4-6", - "claude-sonnet-4-5", - "claude-sonnet-4", -]; +const CONTEXT_1M_SUPPORTED_MODELS = ["claude-opus-4-7", "claude-opus-4-6"]; export const CLAUDE_CODE_COMPATIBLE_STAINLESS_TIMEOUT_SECONDS = getStainlessTimeoutSeconds( process.env ); diff --git a/tests/unit/claude-beta-flags-2454.test.ts b/tests/unit/claude-beta-flags-2454.test.ts index 3dbd3e4e9d..318f2881c9 100644 --- a/tests/unit/claude-beta-flags-2454.test.ts +++ b/tests/unit/claude-beta-flags-2454.test.ts @@ -5,8 +5,9 @@ const { selectBetaFlags } = await import("../../open-sse/executors/claudeIdentit // Regression for #2454: Haiku with OAuth rejects `context-1m-2025-08-07` with // 400 "This authentication style is incompatible with the long context beta header". -// The heavy-agent beta tier (context-1m, effort, advanced-tool-use) must be gated on +// The heavy-agent beta tier (effort, advanced-tool-use) must be gated on // Opus/Sonnet only; Haiku still receives the general full-agent flags. +// context-1m is Opus-only in captured Claude Code traffic. function fullAgentBody(model: string) { return { @@ -19,6 +20,7 @@ function fullAgentBody(model: string) { test("#2454 Haiku full-agent omits heavy-agent beta flags", () => { const flags = selectBetaFlags(fullAgentBody("claude-haiku-4-5-20251001")); assert.ok(!flags.includes("context-1m-2025-08-07"), "Haiku must NOT receive context-1m"); + assert.ok(!flags.includes("afk-mode-2026-01-31"), "Haiku must NOT receive afk-mode"); assert.ok(!flags.includes("effort-2025-11-24"), "Haiku must NOT receive effort"); assert.ok( !flags.includes("advanced-tool-use-2025-11-20"), @@ -31,11 +33,14 @@ test("#2454 Haiku full-agent omits heavy-agent beta flags", () => { assert.ok(flags.includes("extended-cache-ttl-2025-04-11")); }); -test("#2454 Sonnet full-agent includes heavy-agent beta flags", () => { +test("#2454 Sonnet full-agent includes heavy-agent flags but omits context-1m", () => { const flags = selectBetaFlags(fullAgentBody("claude-sonnet-4-6")); - assert.ok(flags.includes("context-1m-2025-08-07"), "Sonnet should receive context-1m"); + assert.ok(!flags.includes("context-1m-2025-08-07"), "Sonnet must NOT receive context-1m"); assert.ok(flags.includes("effort-2025-11-24")); assert.ok(flags.includes("advanced-tool-use-2025-11-20")); + assert.ok(flags.includes("thinking-token-count-2026-05-13")); + assert.ok(flags.includes("afk-mode-2026-01-31")); + assert.ok(!flags.includes("redact-thinking-2026-02-12")); }); test("#2454 Opus full-agent includes heavy-agent beta flags", () => { diff --git a/tests/unit/executor-default-base.test.ts b/tests/unit/executor-default-base.test.ts index ebfeecd209..057916bf6c 100644 --- a/tests/unit/executor-default-base.test.ts +++ b/tests/unit/executor-default-base.test.ts @@ -569,7 +569,7 @@ test("DefaultExecutor.execute uses CC-compatible connection defaults to append 1 assert.equal(calls[0].headers["anthropic-beta"].includes(CONTEXT_1M_BETA_HEADER), false); assert.equal(calls[1].headers["anthropic-beta"].includes(CONTEXT_1M_BETA_HEADER), true); - assert.equal(calls[2].headers["anthropic-beta"], CONTEXT_1M_BETA_HEADER); + assert.equal(calls[2].headers["anthropic-beta"], undefined); }); test("DefaultExecutor.execute only injects adaptive thinking defaults for Claude models that support x-high effort", async () => {