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
This commit is contained in:
roboomp
2026-06-23 01:35:42 +00:00
parent c686c1c803
commit 6b3c7ad376
4 changed files with 66 additions and 5 deletions
+1 -1
View File
@@ -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
+24 -4
View File
@@ -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;
@@ -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<string, string> | 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";
@@ -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();
});
});