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
12 changes: 6 additions & 6 deletions package-lock.json

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

12 changes: 12 additions & 0 deletions packages/agent/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,17 @@
# Changelog

## 0.9.0 - 2026-08-03

- Add OpenRouter Kimi K3 support through `@onkernel/cua-ai` 0.9.0,
including the browser-primitives-only example catalog used by the provider
matrix.
- Resolve `CuaAgentHarness` string model references against its supplied
`Models` collection during construction and `setModel()`, while preserving
the curated CUA model gate and fallback support for CUA model overrides.
- Keep `CuaAgent` aligned with pi's low-level `Agent`: callers can pass a
concrete OpenRouter model and inject `models.streamSimple` without adding a
`Models` dependency to the agent API.

## 0.8.0 - 2026-07-31

Breaking: `CuaAgent` and `CuaAgentHarness` now require one exact `tools` list and
Expand Down
3 changes: 2 additions & 1 deletion packages/agent/examples/shared/tools.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,8 @@ export function toolsForModel(model: CuaModelRef): CuaAgentTool[] {
// plus verified dependent plans.
return structuredBrowserTools();
case "moonshotai":
// Same as meta/xai, minus browser_act: Moonshot's API rejects that tool's
case "openrouter":
// Same as meta/xai, minus browser_act: Kimi's API rejects that tool's
// oversized schema, so Kimi gets the browser primitives only.
return cua.toolsets.browser();
case "tzafon":
Expand Down
4 changes: 2 additions & 2 deletions packages/agent/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@onkernel/cua-agent",
"version": "0.8.0",
"version": "0.9.0",
"description": "Kernel browser computer-use Agent and AgentHarness classes built on pi-agent-core",
"license": "MIT",
"type": "module",
Expand Down Expand Up @@ -45,7 +45,7 @@
"dependencies": {
"@earendil-works/pi-agent-core": "0.80.10",
"@earendil-works/pi-ai": "0.80.10",
"@onkernel/cua-ai": "0.8.0",
"@onkernel/cua-ai": "0.9.0",
"@onkernel/sdk": "0.49.0",
"sharp": "^0.35.3"
},
Expand Down
11 changes: 10 additions & 1 deletion packages/agent/src/agent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,9 @@ import {
cuaModels,
type CuaIncomingToolPlan,
type CuaModelRef,
findCuaAnnotation,
getCuaModel,
parseCuaModelRef,
type CuaSimpleStreamOptions,
getCuaEnvApiKey,
type ImageContent,
Expand Down Expand Up @@ -326,8 +329,8 @@ export class CuaAgentHarness<
const imageReplayLimit = resolveToolResultImageReplayLimit(toolResultImageReplayLimit);
const useResponseThreading = resolveResponseThreading(responseThreading);
const resources = new CuaExecutionResources({ browser, client });
const manager = new CuaToolManager(resources, model, requestedTools);
const retrying = withProviderRetryModels(models ?? cuaModels(), resolveProviderRetryPolicy(retry));
const manager = new CuaToolManager(resources, model, requestedTools, (ref) => resolveModelFromCollection(ref, retrying));
const catalogModels = withCatalogModels(retrying, manager, imageReplayLimit, useResponseThreading);
const materialized = manager.agentTools();
const core = new AgentHarness<TSkill, TPromptTemplate, AgentTool>({
Expand Down Expand Up @@ -447,6 +450,12 @@ export class CuaAgentHarness<

const defaultCuaStream: StreamFn = (model, context, options) => cuaModels().streamSimple(model, context, options);

function resolveModelFromCollection(ref: CuaModelRef, models: Models): Model<Api> {
const { provider, model: id } = parseCuaModelRef(ref);
if (!findCuaAnnotation(provider, id)) throw new Error(`unsupported CUA model "${ref}"`);
return models.getModel(provider, id) ?? getCuaModel(ref);
}

function withCatalogModels(
models: Models,
manager: CuaToolManager,
Expand Down
8 changes: 7 additions & 1 deletion packages/agent/src/tool-manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import type { Api, Model } from "@earendil-works/pi-ai";
import {
callerToolIdentity,
compileCuaToolCatalog,
getCuaModel,
isCuaToolSpec,
modelSupportsDeferredTools,
type CuaCatalogToolInput,
Expand Down Expand Up @@ -68,6 +69,7 @@ export class CuaToolManager {
readonly resources: CuaExecutionResources,
model: CuaModelRef | Model<Api>,
requestedTools: readonly CuaAgentTool[],
private readonly resolveModel: (model: CuaModelRef) => Model<Api> = getCuaModel,
) {
this.current = this.prepare(model, requestedTools);
}
Expand Down Expand Up @@ -131,7 +133,11 @@ export class CuaToolManager {
implementations.set(identity, tool.execute);
}
}
const catalog = compileCuaToolCatalog({ model, requestedTools: inputs, viewport: this.resources.viewport });
const catalog = compileCuaToolCatalog({
model: typeof model === "string" ? this.resolveModel(model) : model,
requestedTools: inputs,
viewport: this.resources.viewport,
});

const fingerprints: string[] = [];
const wrapped = catalog.entries.map((entry) => {
Expand Down
15 changes: 15 additions & 0 deletions packages/agent/test/agent.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { describe, expect, it, vi } from "vitest";
import {
createAssistantMessageEventStream,
createCuaModels,
getCuaModel,
cua,
type AssistantMessage,
type Context,
Expand Down Expand Up @@ -400,6 +401,20 @@ describe("CuaAgent explicit tools", () => {
});

describe("CuaAgentHarness explicit tools", () => {
it("resolves annotated refs from supplied models for construction and setModel", async () => {
const models = createCuaModels();
const openai = models.getProvider("openai")!;
const first = { ...getCuaModel("openai:gpt-5.5"), baseUrl: "https://first.example" };
const second = { ...getCuaModel("openai:gpt-5.6-sol"), baseUrl: "https://second.example" };
models.setProvider({ ...openai, getModels: () => [first, second] });
const services = await harnessServices();
const harness = new CuaAgentHarness({ ...services, browser, client, models, model: "openai:gpt-5.5", tools: [] });
expect(harness.getModel()).toBe(first);
await harness.setModel("openai:gpt-5.6-sol");
expect(harness.getModel()).toBe(second);
expect(() => new CuaAgentHarness({ ...services, browser, client, models, model: "openai:gpt-4o", tools: [] })).toThrow(/unsupported CUA model/);
});

it("uses composition, hides active-tool APIs, and supports an empty catalog", async () => {
const harness = new CuaAgentHarness({ ...(await harnessServices()), browser, client, model: "openai:gpt-5.5", tools: [] });
expect(harness).not.toBeInstanceOf(AgentHarness);
Expand Down
16 changes: 10 additions & 6 deletions packages/agent/test/example-provider-matrix.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ const models: readonly CuaModelRef[] = [
"meta:muse-spark-1.1",
"xai:grok-4.5",
"moonshotai:kimi-k3",
"openrouter:moonshotai/kimi-k3",
"tzafon:tzafon.northstar-cua-fast",
"yutori:n1.5-latest",
];
Expand All @@ -37,12 +38,15 @@ describe("example provider matrix tool policy", () => {
}
});

it("omits browser_act for Moonshot, whose API rejects its schema size", () => {
const names = toolsForModel("moonshotai:kimi-k3").map((tool) => tool.name);
expect(names).toContain("browser_snapshot");
expect(names).toContain("browser_wait_for");
expect(names).not.toContain("browser_act");
});
it.each(["moonshotai:kimi-k3", "openrouter:moonshotai/kimi-k3"] as const)(
"omits browser_act for Kimi, whose API rejects its schema size (%s)",
(model) => {
const names = toolsForModel(model).map((tool) => tool.name);
expect(names).toContain("browser_snapshot");
expect(names).toContain("browser_wait_for");
expect(names).not.toContain("browser_act");
},
);

it("still advertises browser_act where the provider accepts it", () => {
for (const model of ["openai:gpt-5.6-sol", "meta:muse-spark-1.1", "xai:grok-4.5"] as const) {
Expand Down
13 changes: 13 additions & 0 deletions packages/ai/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,18 @@
# Changelog

## 0.9.0 - 2026-08-03

- Add Kimi K3 through OpenRouter as `openrouter:moonshotai/kimi-k3`,
authenticated with `OPENROUTER_API_KEY`, while retaining direct Moonshot access
through `moonshotai:kimi-k3`.
- Share Kimi K3's CUA capability metadata across both transports: nested browser
schemas are accepted, the larger `browser_act` schema is rejected, and
state-mutating function tools disable parallel calls.
- Resolve schema and tool-call compatibility from concrete model capabilities
instead of provider-wide allowlists. Existing provider-family defaults are
preserved, and provider-native tools remain restricted to their native
transports.

## 0.8.0 - 2026-07-31

Breaking: agent tools are now one explicit, identity-keyed catalog. The mode,
Expand Down
16 changes: 16 additions & 0 deletions packages/ai/docs/supported-models.md
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,22 @@ Moonshot ships them.

Source: [Kimi K3 announcement](https://www.kimi.com/blog/kimi-k3), [tool use](https://platform.kimi.ai/docs/api/tool-use), and [vision input](https://platform.kimi.ai/docs/guide/use-kimi-vision-model).

## `openrouter`

CLI default interaction: CUA browser primitives only. OpenRouter's Kimi K3
route accepts complex function schemas but rejects the larger `browser_act`
schema, and state-mutating calls are serialized.

Model refs use the `openrouter:` prefix:

- `moonshotai/kimi-k3`

Kimi K3 uses OpenRouter's OpenAI-compatible chat completions API with ordinary
CUA browser function tools. OpenRouter does not expose the provider-native
computer tools declared by other CUA providers.

Source: [OpenRouter model page](https://openrouter.ai/moonshotai/kimi-k3).

## `tzafon`

Coordinates: normalized 0–999
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.8.0",
"version": "0.9.0",
"description": "Kernel-curated computer-use model access built on pi-ai",
"license": "MIT",
"type": "module",
Expand Down
1 change: 1 addition & 0 deletions packages/ai/src/api-keys.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ const CUA_PROVIDER_API_KEY_ENV_VARS: Record<CuaProvider, readonly string[]> = {
meta: ["META_API_KEY"],
xai: ["XAI_API_KEY"],
moonshotai: ["MOONSHOT_API_KEY"],
openrouter: ["OPENROUTER_API_KEY"],
tzafon: ["TZAFON_API_KEY"],
yutori: ["YUTORI_API_KEY"],
};
Expand Down
39 changes: 35 additions & 4 deletions packages/ai/src/models.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { OPENAI_CUA_RESPONSES_API } from "./providers/openai/provider";
import { XAI_CUA_RESPONSES_API } from "./providers/xai/provider";

/** Providers with curated computer-use model support. */
export type CuaProvider = "openai" | "anthropic" | "google" | "meta" | "xai" | "moonshotai" | "tzafon" | "yutori";
export type CuaProvider = "openai" | "anthropic" | "google" | "meta" | "xai" | "moonshotai" | "openrouter" | "tzafon" | "yutori";

/** Provider-qualified model reference, e.g. `"openai:gpt-5.6-sol"` or `"google:gemini-3.6-flash"`. */
export type CuaModelRef = `${CuaProvider}:${string}`;
Expand All @@ -23,7 +23,7 @@ export interface CuaModelInfo {
}

/** All providers this package curates computer-use models for. */
export const CUA_PROVIDERS: readonly CuaProvider[] = ["openai", "anthropic", "google", "meta", "xai", "moonshotai", "tzafon", "yutori"];
export const CUA_PROVIDERS: readonly CuaProvider[] = ["openai", "anthropic", "google", "meta", "xai", "moonshotai", "openrouter", "tzafon", "yutori"];

/**
* How a {@link CuaModelAnnotation} matches model ids.
Expand All @@ -38,13 +38,28 @@ export type CuaModelMatch =
| { readonly kind: "exact"; readonly id: string }
| { readonly kind: "family"; readonly family: string };

/** CUA tool-catalog capabilities for a concrete model. */
export interface CuaModelCapabilities {
readonly acceptsComplexSchemas: boolean;
readonly acceptsLargeSchemas: boolean;
readonly serializesStateMutations: boolean;
}

/** One CUA-support annotation: a model-id match plus the official source documenting support. */
export interface CuaModelAnnotation {
readonly match: CuaModelMatch;
/** URL of the provider documentation establishing computer-use support. */
readonly source: string;
/** Optional tool-catalog capabilities that describe which CUA schemas and state mutations the model supports. */
readonly capabilities?: CuaModelCapabilities;
Comment thread
cursor[bot] marked this conversation as resolved.
}

const KIMI_K3_CAPABILITIES: CuaModelCapabilities = Object.freeze({
acceptsComplexSchemas: true,
acceptsLargeSchemas: false,
serializesStateMutations: true,
});

/**
* Per-provider computer-use support annotations.
*
Expand Down Expand Up @@ -88,7 +103,10 @@ export const CUA_MODEL_ANNOTATIONS: Record<CuaProvider, readonly CuaModelAnnotat
// OpenAI-compatible API, not a provider-native computer tool. K3 ships
// native vision plus screenshot-grounded agentic tool use.
moonshotai: [
{ match: { kind: "exact", id: "kimi-k3" }, source: "https://www.kimi.com/blog/kimi-k3" },
{ match: { kind: "exact", id: "kimi-k3" }, source: "https://www.kimi.com/blog/kimi-k3", capabilities: KIMI_K3_CAPABILITIES },
],
openrouter: [
{ match: { kind: "exact", id: "moonshotai/kimi-k3" }, source: "https://openrouter.ai/moonshotai/kimi-k3", capabilities: KIMI_K3_CAPABILITIES },
],
tzafon: [
{ match: { kind: "exact", id: "tzafon.northstar-cua-fast" }, source: "https://huggingface.co/Tzafon/Northstar-CUA-Fast" },
Expand Down Expand Up @@ -123,6 +141,7 @@ const CUA_MODEL_OVERRIDES: Record<CuaProvider, readonly Model<Api>[]> = {
meta: [cuaModel("meta", "muse-spark-1.1", "Muse Spark 1.1")],
xai: [],
moonshotai: [],
openrouter: [],
tzafon: [
cuaModel("tzafon", "tzafon.northstar-cua-fast", "Tzafon Northstar CUA Fast"),
cuaModel("tzafon", "tzafon.northstar-cua-fast-1.6", "Tzafon Northstar CUA Fast 1.6"),
Expand Down Expand Up @@ -261,6 +280,18 @@ function supportsCuaProvider(provider: CuaProvider, modelId: string): boolean {
return findCuaAnnotation(provider, modelId) !== undefined;
}

/** Return tool-catalog capabilities for a model, using annotation or provider defaults. */
export function cuaModelCapabilities(model: Model<Api>): CuaModelCapabilities {
const annotation = isCuaProvider(model.provider) ? findCuaAnnotation(model.provider, model.id) : undefined;
if (annotation?.capabilities) return annotation.capabilities;
const acceptsComplexSchemas = ["openai", "anthropic", "meta", "xai", "moonshotai"].includes(model.provider);
return {
acceptsComplexSchemas,
acceptsLargeSchemas: acceptsComplexSchemas && model.provider !== "moonshotai",
serializesStateMutations: ["meta", "xai", "moonshotai"].includes(model.provider),
};
}

/** Find the CUA-support annotation covering a model id, if any. */
export function findCuaAnnotation(provider: CuaProvider, modelId: string): CuaModelAnnotation | undefined {
const id = modelId.toLowerCase();
Expand Down Expand Up @@ -305,7 +336,7 @@ function anthropicOpus5Model(): Model<Api> {
};
}

function cuaModel(provider: Exclude<CuaProvider, "xai" | "moonshotai">, id: string, name: string): Model<Api> {
function cuaModel(provider: Exclude<CuaProvider, "xai" | "moonshotai" | "openrouter">, id: string, name: string): Model<Api> {
const base = {
id,
name,
Expand Down
Loading
Loading