From 6b3c7ad3766cbe092a5786b3ebd1f080a32069c5 Mon Sep 17 00:00:00 2001 From: roboomp Date: Tue, 23 Jun 2026 01:35:42 +0000 Subject: [PATCH] fix(ai): advertised context-management beta for api-key thinking Anthropic rejects context_management.clear_thinking_20251015 without the context-management-2025-06-27 beta header. OAuth requests carried it via claudeCodeAgentBetaDefaults; API-key requests pushed the new context_management field without the beta, so the server rejected them. Push the beta into extraBetas alongside the field for every API-key thinking request, and exclude the GitHub Copilot proxy (which strips Anthropic betas and demotes thinking blocks upstream) from emitting the field. Fixes #3288 --- packages/ai/CHANGELOG.md | 2 +- packages/ai/src/providers/anthropic.ts | 28 ++++++++++++--- packages/ai/test/anthropic-alignment.test.ts | 35 +++++++++++++++++++ .../ai/test/github-copilot-reasoning.test.ts | 6 ++++ 4 files changed, 66 insertions(+), 5 deletions(-) diff --git a/packages/ai/CHANGELOG.md b/packages/ai/CHANGELOG.md index 6aad3230a..f167fe2d3 100644 --- a/packages/ai/CHANGELOG.md +++ b/packages/ai/CHANGELOG.md @@ -4,7 +4,7 @@ ### Fixed -- Fixed Anthropic-compatible thinking requests sending replayed thinking blocks without `context_management.keep: "all"`, preserving multi-turn reasoning context for API-key providers. ([#3288](https://github.com/can1357/oh-my-pi/issues/3288)) +- Fixed Anthropic-compatible thinking requests sending replayed thinking blocks without `context_management.keep: "all"`, preserving multi-turn reasoning context for API-key providers. API-key requests now also advertise the required `context-management-2025-06-27` beta header so the field is honored instead of rejected. GitHub Copilot's Anthropic proxy is excluded because it strips Anthropic betas and demotes thinking blocks to text upstream. ([#3288](https://github.com/can1357/oh-my-pi/issues/3288)) ## [16.1.15] - 2026-06-22 diff --git a/packages/ai/src/providers/anthropic.ts b/packages/ai/src/providers/anthropic.ts index f681f7f54..7543d0594 100644 --- a/packages/ai/src/providers/anthropic.ts +++ b/packages/ai/src/providers/anthropic.ts @@ -120,10 +120,11 @@ export function buildBetaHeader(baseBetas: readonly string[], extraBetas: readon } const midConversationSystemBeta = "mid-conversation-system-2026-04-07"; +const contextManagementBeta = "context-management-2025-06-27"; const claudeCodeUtilityBetaDefaults = [ "oauth-2025-04-20", "interleaved-thinking-2025-05-14", - "context-management-2025-06-27", + contextManagementBeta, "prompt-caching-scope-2026-01-05", "structured-outputs-2025-12-15", ] as const; @@ -131,7 +132,7 @@ const claudeCodeAgentBetaDefaults = [ "claude-code-20250219", "oauth-2025-04-20", "interleaved-thinking-2025-05-14", - "context-management-2025-06-27", + contextManagementBeta, "prompt-caching-scope-2026-01-05", midConversationSystemBeta, "advanced-tool-use-2025-11-20", @@ -1680,6 +1681,20 @@ const streamAnthropicOnce = ( // carry it in the Claude Code list). extraBetas.push(midConversationSystemBeta); } + // `context_management.clear_thinking_20251015` requires this beta. OAuth + // requests carry it in `claudeCodeAgentBetaDefaults`; API-key requests + // need it added explicitly so the field is honored instead of rejected + // (#3288). Skip Copilot — its proxy strips Anthropic betas and the + // upstream compat flag demotes thinking blocks to text, so there is + // nothing for `keep: "all"` to preserve. + if ( + model.reasoning && + options?.thinkingEnabled && + model.provider !== "github-copilot" && + !extraBetas.includes(contextManagementBeta) + ) { + extraBetas.push(contextManagementBeta); + } const created = createClient(model, { model, @@ -2950,8 +2965,13 @@ function buildParams( // strip the replayed thinking blocks `replayUnsignedThinking` puts back // on the wire, so the model loses the prior reasoning chain across turns // and the KV cache misses every turn (#3288). Narrowing this guard back - // to `isOAuthToken` regresses every API-key thinking provider. - const shouldKeepThinkingContext = thinking?.type === "adaptive" || thinking?.type === "enabled"; + // to `isOAuthToken` regresses every API-key thinking provider. Skip + // Copilot — its proxy strips Anthropic betas (so the required + // `context-management-2025-06-27` header never lands) and the compat + // flag demotes thinking blocks to text, so `keep: "all"` is a no-op + // that risks the proxy rejecting an unrecognized field. + const shouldKeepThinkingContext = + model.provider !== "github-copilot" && (thinking?.type === "adaptive" || thinking?.type === "enabled"); const contextManagement = shouldKeepThinkingContext ? { edits: [{ type: "clear_thinking_20251015" as const, keep: "all" as const }] } : undefined; diff --git a/packages/ai/test/anthropic-alignment.test.ts b/packages/ai/test/anthropic-alignment.test.ts index 47a2714fc..f4efa80f7 100644 --- a/packages/ai/test/anthropic-alignment.test.ts +++ b/packages/ai/test/anthropic-alignment.test.ts @@ -406,6 +406,41 @@ describe("Anthropic request fingerprint alignment", () => { expect(capturedBeta).toContain("mid-conversation-system-2026-04-07"); }); + it("adds the context-management beta to API-key thinking requests", async () => { + let capturedBeta: string | undefined; + const fetchMock = (async (_input: string | URL | Request, init?: RequestInit) => { + capturedBeta = (init?.headers as Record | undefined)?.["anthropic-beta"]; + return new Response( + JSON.stringify({ type: "error", error: { type: "invalid_request_error", message: "captured" } }), + { status: 400, headers: { "Content-Type": "application/json" } }, + ); + }) as typeof fetch; + + // `context_management.clear_thinking_20251015` is rejected without + // the `context-management-2025-06-27` beta. OAuth requests carry it + // via `claudeCodeAgentBetaDefaults`; API-key requests must add it + // explicitly whenever thinking is enabled so the field is honored + // instead of dropped on the floor (#3288). + await streamAnthropic( + ANTHROPIC_MODEL, + { systemPrompt: ["Stay concise."], messages: [{ role: "user", content: "Hi", timestamp: Date.now() }] }, + { apiKey: "sk-ant-api-test", thinkingEnabled: true, fetch: fetchMock }, + ).result(); + + expect(capturedBeta).toContain("context-management-2025-06-27"); + + capturedBeta = undefined; + await streamAnthropic( + ANTHROPIC_MODEL, + { systemPrompt: ["Stay concise."], messages: [{ role: "user", content: "Hi", timestamp: Date.now() }] }, + { apiKey: "sk-ant-api-test", thinkingEnabled: false, fetch: fetchMock }, + ).result(); + + // No context_management field is sent when thinking is disabled, so the + // beta MUST NOT be advertised either. + expect(capturedBeta ?? "").not.toContain("context-management-2025-06-27"); + }); + it("billing-header fingerprint uses first user message, not leading developer message", async () => { const userText = "Hello from user with enough chars padding here"; diff --git a/packages/ai/test/github-copilot-reasoning.test.ts b/packages/ai/test/github-copilot-reasoning.test.ts index 6360c40dd..e96a2c710 100644 --- a/packages/ai/test/github-copilot-reasoning.test.ts +++ b/packages/ai/test/github-copilot-reasoning.test.ts @@ -56,9 +56,15 @@ describe("GitHub Copilot reasoning request construction", () => { const payload = (await captureAnthropicPayload(model)) as { thinking?: { type?: string }; output_config?: { effort?: string }; + context_management?: unknown; }; expect(payload.thinking).toEqual({ type: "adaptive" }); expect(payload.output_config).toEqual({ effort: "high" }); + // The Copilot Anthropic proxy strips Anthropic betas and demotes + // thinking blocks to text upstream — the `context_management` field + // would have no replayed thinking to keep and risks proxy rejection + // of an unrecognized field. The field MUST NOT be sent (#3288). + expect(payload.context_management).toBeUndefined(); }); });