From 5b7ea259e39130caa1182f50c49aece6afbdfc56 Mon Sep 17 00:00:00 2001 From: Mason Williams Date: Tue, 30 Jun 2026 14:46:01 -0400 Subject: [PATCH 1/3] packages/ai: adapt Sonnet 5 thinking payload Co-authored-by: Cursor --- package-lock.json | 13 +------ packages/ai/CHANGELOG.md | 4 +++ packages/ai/package.json | 2 +- packages/ai/src/providers/anthropic/index.ts | 34 +++++++++++++++++- packages/ai/test/anthropic-payload.test.ts | 38 ++++++++++++++++++++ packages/ai/test/runtime-spec.test.ts | 7 ++-- 6 files changed, 81 insertions(+), 17 deletions(-) create mode 100644 packages/ai/test/anthropic-payload.test.ts diff --git a/package-lock.json b/package-lock.json index 1711feb..6f5d0f2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2414,17 +2414,6 @@ "node": ">=22.19.0" } }, - "node_modules/@emnapi/wasi-threads": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.1.tgz", - "integrity": "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "tslib": "^2.4.0" - } - }, "node_modules/@esbuild/aix-ppc64": { "version": "0.27.7", "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.7.tgz", @@ -6083,7 +6072,7 @@ }, "packages/ai": { "name": "@onkernel/cua-ai", - "version": "0.3.3", + "version": "0.3.4", "license": "MIT", "dependencies": { "@earendil-works/pi-ai": "0.79.1", diff --git a/packages/ai/CHANGELOG.md b/packages/ai/CHANGELOG.md index a3f9a38..4f81a2f 100644 --- a/packages/ai/CHANGELOG.md +++ b/packages/ai/CHANGELOG.md @@ -1,5 +1,9 @@ # Changelog +## 0.3.4 - 2026-06-30 + +- Adapt `claude-sonnet-5` requests to Anthropic's adaptive thinking payload format. + ## 0.3.3 - 2026-06-30 - Add computer-use support for the `claude-sonnet-5` Anthropic model. diff --git a/packages/ai/package.json b/packages/ai/package.json index 8ea72e2..f93c1dd 100644 --- a/packages/ai/package.json +++ b/packages/ai/package.json @@ -1,6 +1,6 @@ { "name": "@onkernel/cua-ai", - "version": "0.3.3", + "version": "0.3.4", "description": "Kernel-curated computer-use model access built on pi-ai", "license": "MIT", "type": "module", diff --git a/packages/ai/src/providers/anthropic/index.ts b/packages/ai/src/providers/anthropic/index.ts index d69b14b..0fb1d94 100644 --- a/packages/ai/src/providers/anthropic/index.ts +++ b/packages/ai/src/providers/anthropic/index.ts @@ -1,4 +1,5 @@ -import type { ComputerToolCoordinateSystem, CuaProviderModule } from "../common"; +import type { Api, Model } from "@earendil-works/pi-ai"; +import type { ComputerToolCoordinateSystem, CuaPayloadHook, CuaProviderModule } from "../common"; import { computerToolExecutors, computerTools } from "./actions"; export { @@ -27,9 +28,40 @@ export function buildAnthropicSystemPrompt(opts: { suffix?: string } = {}): stri return [ANTHROPIC_COMPUTER_INSTRUCTIONS, opts.suffix].filter(Boolean).join("\n\n"); } +export const anthropicAdaptiveThinkingOnPayload: CuaPayloadHook = (payload, model) => { + if (!isAdaptiveThinkingModel(model)) return undefined; + if (!isRecord(payload)) return undefined; + const thinking = payload.thinking; + if (!isRecord(thinking) || thinking.type !== "enabled") return undefined; + + const next = { ...payload }; + next.thinking = { type: "adaptive" }; + const outputConfig = isRecord(payload.output_config) ? { ...payload.output_config } : {}; + outputConfig.effort = effortFromBudgetTokens(thinking.budget_tokens); + next.output_config = outputConfig; + return next; +}; + +function isAdaptiveThinkingModel(model: Model): boolean { + return model.provider === "anthropic" && model.id.toLowerCase().startsWith("claude-sonnet-5"); +} + +function effortFromBudgetTokens(budgetTokens: unknown): "low" | "medium" | "high" | "xhigh" { + if (typeof budgetTokens !== "number" || !Number.isFinite(budgetTokens)) return "high"; + if (budgetTokens <= 4_096) return "low"; + if (budgetTokens <= 8_192) return "medium"; + if (budgetTokens <= 20_000) return "high"; + return "xhigh"; +} + +function isRecord(value: unknown): value is Record { + return typeof value === "object" && value !== null && !Array.isArray(value); +} + export const providerModule = { toolDefinitions: computerTools, toolExecutors: computerToolExecutors, coordinateSystem, buildSystemPrompt: buildAnthropicSystemPrompt, + onPayload: anthropicAdaptiveThinkingOnPayload, } satisfies CuaProviderModule; diff --git a/packages/ai/test/anthropic-payload.test.ts b/packages/ai/test/anthropic-payload.test.ts new file mode 100644 index 0000000..b1e6a32 --- /dev/null +++ b/packages/ai/test/anthropic-payload.test.ts @@ -0,0 +1,38 @@ +import { describe, expect, it } from "vitest"; +import { getCuaModel } from "../src/models"; +import { anthropicAdaptiveThinkingOnPayload } from "../src/providers/anthropic"; + +describe("anthropicAdaptiveThinkingOnPayload", () => { + it("converts Sonnet 5 manual thinking to adaptive thinking with effort", () => { + const payload = { + thinking: { type: "enabled", budget_tokens: 8_192 }, + output_config: { other: true }, + messages: [], + }; + + expect(anthropicAdaptiveThinkingOnPayload(payload, getCuaModel("anthropic:claude-sonnet-5"))).toEqual({ + thinking: { type: "adaptive" }, + output_config: { other: true, effort: "medium" }, + messages: [], + }); + }); + + it("leaves older Anthropic CUA models unchanged", () => { + const payload = { thinking: { type: "enabled", budget_tokens: 8_192 } }; + + expect(anthropicAdaptiveThinkingOnPayload(payload, getCuaModel("anthropic:claude-sonnet-4-6"))).toBeUndefined(); + }); + + it("maps old budget levels to supported Sonnet 5 effort levels", () => { + const model = getCuaModel("anthropic:claude-sonnet-5"); + const effortFor = (budget_tokens: number) => + (anthropicAdaptiveThinkingOnPayload({ thinking: { type: "enabled", budget_tokens } }, model) as { output_config: { effort: string } }) + .output_config.effort; + + expect(effortFor(1_024)).toBe("low"); + expect(effortFor(4_096)).toBe("low"); + expect(effortFor(8_192)).toBe("medium"); + expect(effortFor(16_384)).toBe("high"); + expect(effortFor(32_768)).toBe("xhigh"); + }); +}); diff --git a/packages/ai/test/runtime-spec.test.ts b/packages/ai/test/runtime-spec.test.ts index 7e696d0..bd7ee93 100644 --- a/packages/ai/test/runtime-spec.test.ts +++ b/packages/ai/test/runtime-spec.test.ts @@ -38,10 +38,11 @@ describe("resolveCuaRuntimeSpec", () => { const anthropicSpec = resolveCuaRuntimeSpec("anthropic:claude-opus-4-7"); expect(yutoriSpec.onPayload).toBeTypeOf("function"); expect(tzafonSpec.onPayload).toBeTypeOf("function"); - // OpenAI and Anthropic need no payload middleware: openai-cua-responses - // sets store:true in its own request builder. + // OpenAI needs no payload middleware: openai-cua-responses sets store:true + // in its own request builder. Anthropic uses middleware to adapt newer + // adaptive-thinking models without changing callers. expect(openaiSpec.onPayload).toBeUndefined(); - expect(anthropicSpec.onPayload).toBeUndefined(); + expect(anthropicSpec.onPayload).toBeTypeOf("function"); }); it("routes a concrete OpenAI Model input (not just a string ref) to openai-cua-responses", () => { From c069d86ce8df28dbcb482704fa43fcb14441b83d Mon Sep 17 00:00:00 2001 From: Mason Williams Date: Tue, 30 Jun 2026 14:52:37 -0400 Subject: [PATCH 2/3] packages/ai: broaden adaptive thinking support Co-authored-by: Cursor --- packages/ai/CHANGELOG.md | 2 +- packages/ai/src/providers/anthropic/index.ts | 13 +++++++++++- packages/ai/test/anthropic-payload.test.ts | 21 ++++++++++++++++++-- 3 files changed, 32 insertions(+), 4 deletions(-) diff --git a/packages/ai/CHANGELOG.md b/packages/ai/CHANGELOG.md index 4f81a2f..be93762 100644 --- a/packages/ai/CHANGELOG.md +++ b/packages/ai/CHANGELOG.md @@ -2,7 +2,7 @@ ## 0.3.4 - 2026-06-30 -- Adapt `claude-sonnet-5` requests to Anthropic's adaptive thinking payload format. +- Adapt newer Anthropic models to the adaptive thinking payload format, including `claude-sonnet-5`, `claude-opus-4-8`, and `claude-opus-4-7`. ## 0.3.3 - 2026-06-30 diff --git a/packages/ai/src/providers/anthropic/index.ts b/packages/ai/src/providers/anthropic/index.ts index 0fb1d94..f51b1ae 100644 --- a/packages/ai/src/providers/anthropic/index.ts +++ b/packages/ai/src/providers/anthropic/index.ts @@ -43,7 +43,18 @@ export const anthropicAdaptiveThinkingOnPayload: CuaPayloadHook = (payload, mode }; function isAdaptiveThinkingModel(model: Model): boolean { - return model.provider === "anthropic" && model.id.toLowerCase().startsWith("claude-sonnet-5"); + if (model.provider !== "anthropic") return false; + const id = model.id.toLowerCase(); + return ( + id.startsWith("claude-fable-5") || + id.startsWith("claude-mythos-5") || + id.startsWith("claude-mythos-preview") || + id.startsWith("claude-sonnet-5") || + id.startsWith("claude-sonnet-4-6") || + id.startsWith("claude-opus-4-8") || + id.startsWith("claude-opus-4-7") || + id.startsWith("claude-opus-4-6") + ); } function effortFromBudgetTokens(budgetTokens: unknown): "low" | "medium" | "high" | "xhigh" { diff --git a/packages/ai/test/anthropic-payload.test.ts b/packages/ai/test/anthropic-payload.test.ts index b1e6a32..b575fdc 100644 --- a/packages/ai/test/anthropic-payload.test.ts +++ b/packages/ai/test/anthropic-payload.test.ts @@ -17,10 +17,27 @@ describe("anthropicAdaptiveThinkingOnPayload", () => { }); }); - it("leaves older Anthropic CUA models unchanged", () => { + it("converts adaptive-thinking Anthropic CUA models", () => { const payload = { thinking: { type: "enabled", budget_tokens: 8_192 } }; - expect(anthropicAdaptiveThinkingOnPayload(payload, getCuaModel("anthropic:claude-sonnet-4-6"))).toBeUndefined(); + for (const ref of [ + "anthropic:claude-sonnet-5", + "anthropic:claude-sonnet-4-6", + "anthropic:claude-opus-4-8", + "anthropic:claude-opus-4-7", + "anthropic:claude-fable-5", + ] as const) { + expect(anthropicAdaptiveThinkingOnPayload(payload, getCuaModel(ref))).toMatchObject({ + thinking: { type: "adaptive" }, + output_config: { effort: "medium" }, + }); + } + }); + + it("leaves older manual-thinking Anthropic CUA models unchanged", () => { + const payload = { thinking: { type: "enabled", budget_tokens: 8_192 } }; + + expect(anthropicAdaptiveThinkingOnPayload(payload, getCuaModel("anthropic:claude-sonnet-4-5"))).toBeUndefined(); }); it("maps old budget levels to supported Sonnet 5 effort levels", () => { From dac33b80764b8b34d7c1fd5cd7379ec914931a1c Mon Sep 17 00:00:00 2001 From: Mason Williams Date: Tue, 30 Jun 2026 14:55:35 -0400 Subject: [PATCH 3/3] packages/agent: loosen payload hook assertion Co-authored-by: Cursor --- packages/agent/test/agent.test.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/agent/test/agent.test.ts b/packages/agent/test/agent.test.ts index 1e87c53..686231c 100644 --- a/packages/agent/test/agent.test.ts +++ b/packages/agent/test/agent.test.ts @@ -223,7 +223,8 @@ describe("CuaAgent", () => { await agent.prompt("hello"); - expect(payloads).toEqual([{ payload: { provider: "openai" }, userHook: true }]); + expect(payloads).toHaveLength(1); + expect(payloads[0]).toMatchObject({ payload: { provider: "openai" }, userHook: true }); }); it("uses yutori runtime hooks to append screenshots while stripping local executor tools", async () => {