diff --git a/packages/ai/CHANGELOG.md b/packages/ai/CHANGELOG.md index 25512e93e..fcb87a210 100644 --- a/packages/ai/CHANGELOG.md +++ b/packages/ai/CHANGELOG.md @@ -12,6 +12,10 @@ - Added AIML API as an OpenAI-compatible provider preset with `AIMLAPI_API_KEY` discovery ([#2105](https://github.com/can1357/oh-my-pi/issues/2105)). +### Fixed + +- Pinned a regression test against issue [#2123](https://github.com/can1357/oh-my-pi/issues/2123): OAuth requests to adaptive-thinking Claude Opus models (4.6+) ship a `context_management.edits[clear_thinking_20251015]` block paired with the `thinking` field, but the eager-todo prelude (and other paths that force `tool_choice` to `tool`/`any` on the first user turn) route through `disableThinkingIfToolChoiceForced`, which would strip `params.thinking` while leaving the orphan `context_management` behind. The Anthropic API then rejected the request with `400 ... clear_thinking_20251015 strategy requires thinking to be enabled or adaptive`. The fix that lands in [15.10.5] now drops both fields together; the new test locks the contract so the strategy can never outlive its enabling `thinking` payload again. + ## [15.10.5] - 2026-06-08 ### Breaking Changes diff --git a/packages/ai/test/issue-2123-repro.test.ts b/packages/ai/test/issue-2123-repro.test.ts new file mode 100644 index 000000000..f94e3d061 --- /dev/null +++ b/packages/ai/test/issue-2123-repro.test.ts @@ -0,0 +1,104 @@ +/** + * Issue #2123 — `Error while using Claude Opus models` + * + * Reporter (omp 15.10.4, Windows, Claude Pro/Max OAuth): a fresh chat on + * Claude Opus 4.6 fails immediately with + * `400 ... clear_thinking_20251015 strategy requires thinking to be enabled or adaptive`. + * + * Root cause: OAuth requests on adaptive-thinking Opus models attach a + * `context_management.edits[clear_thinking_20251015]` block. On the very + * first user turn the coding-agent's eager-todo prelude pins a forced + * `tool_choice: { type: "tool", name: "todo" }`, which routes through + * `disableThinkingIfToolChoiceForced` in `providers/anthropic.ts`. The + * 15.10.4 implementation stripped `params.thinking` but left + * `params.context_management` in place, sending an orphan strategy that + * the Anthropic API rejects with the exact message above. + * + * Fix: when forced tool_choice removes `thinking`, also remove + * `context_management` so the wire payload satisfies the + * "thinking enabled or adaptive" precondition (i.e. neither is sent, and + * the strategy goes with them). + */ +import { describe, expect, it } from "bun:test"; +import { Effort } from "@oh-my-pi/pi-ai/effort"; +import { streamAnthropic } from "@oh-my-pi/pi-ai/providers/anthropic"; +import type { Context, Model, Tool } from "@oh-my-pi/pi-ai/types"; + +const OPUS_46_OAUTH: Model<"anthropic-messages"> = { + id: "claude-opus-4-6", + name: "Claude Opus 4.6", + api: "anthropic-messages", + provider: "anthropic", + baseUrl: "https://api.anthropic.com", + reasoning: true, + input: ["text", "image"], + cost: { input: 5, output: 25, cacheRead: 0.5, cacheWrite: 6.25 }, + contextWindow: 1_000_000, + maxTokens: 128_000, + thinking: { mode: "anthropic-adaptive", minLevel: Effort.Minimal, maxLevel: Effort.XHigh }, +}; + +const todoTool: Tool = { + name: "todo", + description: "Manage a phased task list", + parameters: { + type: "object", + properties: { ops: { type: "array", items: { type: "object" } } }, + required: ["ops"], + } as unknown as Tool["parameters"], +}; + +const firstTurnContext: Context = { + systemPrompt: ["Stay concise."], + messages: [{ role: "user", content: "Plan my migration", timestamp: Date.now() }], + tools: [todoTool], +}; + +function abortedSignal(): AbortSignal { + const controller = new AbortController(); + controller.abort(); + return controller.signal; +} + +type CapturedPayload = { + thinking?: unknown; + context_management?: unknown; + tool_choice?: { type?: string; name?: string }; +}; + +function capturePayload(toolChoice: "any" | { type: "tool"; name: string }): Promise { + const { promise, resolve } = Promise.withResolvers(); + void streamAnthropic(OPUS_46_OAUTH, firstTurnContext, { + apiKey: "sk-ant-oat-test", + isOAuth: true, + signal: abortedSignal(), + thinkingEnabled: true, + reasoning: Effort.High, + toolChoice, + onPayload: payload => { + resolve(payload as CapturedPayload); + return undefined; + }, + }); + return promise; +} + +describe("issue #2123 — OAuth Opus + forced tool_choice must strip context_management with thinking", () => { + it("strips context_management when forced named tool_choice deletes adaptive thinking", async () => { + // Mirrors the eager-todo prelude: first user turn on Opus pins + // tool_choice: { type: "tool", name: "" } while adaptive + // thinking is still requested at default `high` effort. + const payload = await capturePayload({ type: "tool", name: "todo" }); + expect(payload.thinking).toBeUndefined(); + expect(payload.context_management).toBeUndefined(); + }); + + it("strips context_management when forced `any` tool_choice deletes adaptive thinking", async () => { + // Plan-mode and other "must call something" enforcement paths use + // the broader tool_choice "any" (Anthropic's `any`). Same invariant: + // orphan context_management must NOT survive the strip. + const payload = await capturePayload("any"); + expect(payload.thinking).toBeUndefined(); + expect(payload.context_management).toBeUndefined(); + }); +});