Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 1 addition & 12 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion packages/agent/test/agent.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 () => {
Expand Down
4 changes: 4 additions & 0 deletions packages/ai/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
# Changelog

## 0.3.4 - 2026-06-30

- 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

- Add computer-use support for the `claude-sonnet-5` Anthropic model.
Expand Down
2 changes: 1 addition & 1 deletion packages/ai/package.json
Original file line number Diff line number Diff line change
@@ -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",
Expand Down
45 changes: 44 additions & 1 deletion packages/ai/src/providers/anthropic/index.ts
Original file line number Diff line number Diff line change
@@ -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 {
Expand Down Expand Up @@ -27,9 +28,51 @@ 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;
};

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing TSDoc on payload hook

Low Severity

The new exported anthropicAdaptiveThinkingOnPayload hook has no TSDoc describing when it rewrites thinking for Sonnet 5, how output_config.effort is derived, or that it returns undefined to leave other models and payloads unchanged.

Fix in Cursor Fix in Web

Triggered by learned rule: Exported types, interfaces, and classes require TSDoc

Reviewed by Cursor Bugbot for commit 5b7ea25. Configure here.


function isAdaptiveThinkingModel(model: Model<Api>): boolean {
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" {
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<string, unknown> {
return typeof value === "object" && value !== null && !Array.isArray(value);
}

export const providerModule = {
toolDefinitions: computerTools,
toolExecutors: computerToolExecutors,
coordinateSystem,
buildSystemPrompt: buildAnthropicSystemPrompt,
onPayload: anthropicAdaptiveThinkingOnPayload,
} satisfies CuaProviderModule;
55 changes: 55 additions & 0 deletions packages/ai/test/anthropic-payload.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
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("converts adaptive-thinking Anthropic CUA models", () => {
const payload = { thinking: { type: "enabled", budget_tokens: 8_192 } };

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", () => {
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");
});
});
7 changes: 4 additions & 3 deletions packages/ai/test/runtime-spec.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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", () => {
Expand Down
Loading