fix(ai): skipped context management for injected clients

Injected Anthropic clients bypass buildAnthropicClientOptions, so this package cannot add the context-management beta header their SDK instance would need before accepting context_management.clear_thinking_20251015.

Omit context_management for options.client requests while preserving thinking itself, and add regression coverage for injected-client payload shaping.

Fixes #3288
This commit is contained in:
roboomp
2026-06-23 01:42:53 +00:00
parent 6b3c7ad376
commit 5291b2f5a0
3 changed files with 39 additions and 6 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. 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))
- 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. Injected SDK clients and GitHub Copilot's Anthropic proxy are excluded because this code path cannot add the beta to caller-owned clients, while Copilot 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
+8 -5
View File
@@ -2966,12 +2966,15 @@ function buildParams(
// 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. 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.
// injected clients because this code cannot add the required
// `context-management-2025-06-27` beta to caller-owned SDK clients. Skip
// Copilot because its proxy strips Anthropic betas and demotes thinking
// blocks to text upstream, so `keep: "all"` is a no-op that risks proxy
// rejection of an unrecognized field.
const shouldKeepThinkingContext =
model.provider !== "github-copilot" && (thinking?.type === "adaptive" || thinking?.type === "enabled");
!options?.client &&
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;
@@ -576,6 +576,36 @@ describe("anthropic stream envelope handling", () => {
expect(capturedParams?.tools?.map(tool => tool.name)).toEqual(["web_search"]);
expect(capturedOptions?.headers).toEqual({ "X-Umans-Websearch-Provider": "exa" });
});
it("does not send context_management through injected clients", async () => {
type CapturedPayload = {
thinking?: { type?: string };
context_management?: unknown;
};
let capturedParams: CapturedPayload | undefined;
const client: AnthropicMessagesClientLike = {
messages: {
create(params) {
capturedParams = params as CapturedPayload;
return createMockRequest(createTextSuccessEvents("done"));
},
},
};
const stream = streamAnthropic(model, context, {
client,
thinkingEnabled: true,
});
const events: AssistantMessageEvent[] = [];
for await (const event of stream) {
events.push(event);
}
const result = await stream.result();
expect(result.content).toEqual([{ type: "text", text: "done" }]);
expect(capturedParams?.thinking?.type).toBe("enabled");
expect(capturedParams?.context_management).toBeUndefined();
});
it("unwraps thinking blocks that Anthropic streams with literal thinking tags", async () => {
const wrappedThinking =
"<thinking>\n<thinking>\nCheck logs before accepting container health.\n</thinking></thinking>";