diff --git a/package-lock.json b/package-lock.json index 9a2bfb4a..42eda063 100644 --- a/package-lock.json +++ b/package-lock.json @@ -6205,12 +6205,12 @@ }, "packages/agent": { "name": "@onkernel/cua-agent", - "version": "0.8.0", + "version": "0.9.0", "license": "MIT", "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" }, @@ -6224,7 +6224,7 @@ }, "packages/ai": { "name": "@onkernel/cua-ai", - "version": "0.8.0", + "version": "0.9.0", "license": "MIT", "dependencies": { "@earendil-works/pi-ai": "0.80.10", @@ -6241,13 +6241,13 @@ }, "packages/cli": { "name": "@onkernel/cua-cli", - "version": "0.6.0", + "version": "0.7.0", "license": "MIT", "dependencies": { "@earendil-works/pi-coding-agent": "0.80.10", "@earendil-works/pi-tui": "0.80.10", - "@onkernel/cua-agent": "0.8.0", - "@onkernel/cua-ai": "0.8.0", + "@onkernel/cua-agent": "0.9.0", + "@onkernel/cua-ai": "0.9.0", "@onkernel/sdk": "0.49.0" }, "bin": { diff --git a/packages/agent/CHANGELOG.md b/packages/agent/CHANGELOG.md index 2be5b70e..647db16d 100644 --- a/packages/agent/CHANGELOG.md +++ b/packages/agent/CHANGELOG.md @@ -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 diff --git a/packages/agent/examples/shared/tools.ts b/packages/agent/examples/shared/tools.ts index 0c6b6f7f..7bf13770 100644 --- a/packages/agent/examples/shared/tools.ts +++ b/packages/agent/examples/shared/tools.ts @@ -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": diff --git a/packages/agent/package.json b/packages/agent/package.json index 8ae77b39..a2e7cd25 100644 --- a/packages/agent/package.json +++ b/packages/agent/package.json @@ -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", @@ -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" }, diff --git a/packages/agent/src/agent.ts b/packages/agent/src/agent.ts index 7024e281..d766d178 100644 --- a/packages/agent/src/agent.ts +++ b/packages/agent/src/agent.ts @@ -26,6 +26,9 @@ import { cuaModels, type CuaIncomingToolPlan, type CuaModelRef, + findCuaAnnotation, + getCuaModel, + parseCuaModelRef, type CuaSimpleStreamOptions, getCuaEnvApiKey, type ImageContent, @@ -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({ @@ -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 { + 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, diff --git a/packages/agent/src/tool-manager.ts b/packages/agent/src/tool-manager.ts index e20c854a..4e42043a 100644 --- a/packages/agent/src/tool-manager.ts +++ b/packages/agent/src/tool-manager.ts @@ -4,6 +4,7 @@ import type { Api, Model } from "@earendil-works/pi-ai"; import { callerToolIdentity, compileCuaToolCatalog, + getCuaModel, isCuaToolSpec, modelSupportsDeferredTools, type CuaCatalogToolInput, @@ -68,6 +69,7 @@ export class CuaToolManager { readonly resources: CuaExecutionResources, model: CuaModelRef | Model, requestedTools: readonly CuaAgentTool[], + private readonly resolveModel: (model: CuaModelRef) => Model = getCuaModel, ) { this.current = this.prepare(model, requestedTools); } @@ -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) => { diff --git a/packages/agent/test/agent.test.ts b/packages/agent/test/agent.test.ts index 344d19f9..f437923b 100644 --- a/packages/agent/test/agent.test.ts +++ b/packages/agent/test/agent.test.ts @@ -2,6 +2,7 @@ import { describe, expect, it, vi } from "vitest"; import { createAssistantMessageEventStream, createCuaModels, + getCuaModel, cua, type AssistantMessage, type Context, @@ -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); diff --git a/packages/agent/test/example-provider-matrix.test.ts b/packages/agent/test/example-provider-matrix.test.ts index 0c0e2ffa..1fc91b6f 100644 --- a/packages/agent/test/example-provider-matrix.test.ts +++ b/packages/agent/test/example-provider-matrix.test.ts @@ -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", ]; @@ -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) { diff --git a/packages/ai/CHANGELOG.md b/packages/ai/CHANGELOG.md index ad78531b..66e58bd6 100644 --- a/packages/ai/CHANGELOG.md +++ b/packages/ai/CHANGELOG.md @@ -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, diff --git a/packages/ai/docs/supported-models.md b/packages/ai/docs/supported-models.md index 4797c4fc..612e3c12 100644 --- a/packages/ai/docs/supported-models.md +++ b/packages/ai/docs/supported-models.md @@ -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 diff --git a/packages/ai/package.json b/packages/ai/package.json index 05d43c74..1053a833 100644 --- a/packages/ai/package.json +++ b/packages/ai/package.json @@ -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", diff --git a/packages/ai/src/api-keys.ts b/packages/ai/src/api-keys.ts index 41db3109..58bef140 100644 --- a/packages/ai/src/api-keys.ts +++ b/packages/ai/src/api-keys.ts @@ -15,6 +15,7 @@ const CUA_PROVIDER_API_KEY_ENV_VARS: Record = { meta: ["META_API_KEY"], xai: ["XAI_API_KEY"], moonshotai: ["MOONSHOT_API_KEY"], + openrouter: ["OPENROUTER_API_KEY"], tzafon: ["TZAFON_API_KEY"], yutori: ["YUTORI_API_KEY"], }; diff --git a/packages/ai/src/models.ts b/packages/ai/src/models.ts index 40fe9a5f..2b8f72a5 100644 --- a/packages/ai/src/models.ts +++ b/packages/ai/src/models.ts @@ -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}`; @@ -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. @@ -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; } +const KIMI_K3_CAPABILITIES: CuaModelCapabilities = Object.freeze({ + acceptsComplexSchemas: true, + acceptsLargeSchemas: false, + serializesStateMutations: true, +}); + /** * Per-provider computer-use support annotations. * @@ -88,7 +103,10 @@ export const CUA_MODEL_ANNOTATIONS: Record[]> = { 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"), @@ -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): 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(); @@ -305,7 +336,7 @@ function anthropicOpus5Model(): Model { }; } -function cuaModel(provider: Exclude, id: string, name: string): Model { +function cuaModel(provider: Exclude, id: string, name: string): Model { const base = { id, name, diff --git a/packages/ai/src/tool-catalog.ts b/packages/ai/src/tool-catalog.ts index b3d2e96a..9b8f3f8b 100644 --- a/packages/ai/src/tool-catalog.ts +++ b/packages/ai/src/tool-catalog.ts @@ -1,7 +1,7 @@ import type { Api, Model, Tool } from "@earendil-works/pi-ai"; import type { CuaAction } from "./actions/index"; import type { CuaModelRef } from "./models"; -import { getCuaModel, providerForModel, routeCuaApi } from "./models"; +import { cuaModelCapabilities, getCuaModel, providerForModel, routeCuaApi } from "./models"; import { anthropicAdaptiveThinkingOnPayload } from "./providers/anthropic/adaptive-thinking"; import { supportsAnthropicNativeBrowser, @@ -400,16 +400,6 @@ function nameCollision( return new Error(`tool name "${name}" is requested by both "${first.identity}" and "${second.identity}"${suffix}`); } -/** Providers whose function-calling APIs accept CUA's nested union schemas. */ -const COMPLEX_SCHEMA_PROVIDERS: readonly string[] = ["openai", "anthropic", "meta", "xai", "moonshotai"]; - -/** - * Providers that additionally accept a `largeSchema` declaration. Moonshot is - * absent deliberately: its API accepts `browser_wait_for` (~15KB) but rejects - * the request immediately once `browser_act` (~124KB) is attached. - */ -const LARGE_SCHEMA_PROVIDERS: readonly string[] = ["openai", "anthropic", "meta", "xai"]; - function validateToolCompatibility(model: Model, entry: CuaCatalogEntryDraft): void { const binding = entry.providerBinding; if (entry.origin === "provider-native" && !/^https:\/\//.test(entry.source ?? "")) { @@ -422,10 +412,11 @@ function validateToolCompatibility(model: Model, entry: CuaCatalogEntryDraf throw new Error(`${entry.identity} requires a ${required} model; selected ${model.provider}:${model.id}`); } } - if (entry.complexSchema && !COMPLEX_SCHEMA_PROVIDERS.includes(model.provider)) { + const capabilities = cuaModelCapabilities(model); + if (entry.complexSchema && !capabilities.acceptsComplexSchemas) { throw new Error(`provider ${model.provider} does not accept the schema used by "${entry.name}" (${entry.identity})`); } - if (entry.largeSchema && !LARGE_SCHEMA_PROVIDERS.includes(model.provider)) { + if (entry.largeSchema && !capabilities.acceptsLargeSchemas) { throw new Error(`provider ${model.provider} does not accept the schema size of "${entry.name}" (${entry.identity})`); } if (binding?.kind === "anthropic-native") validateAnthropicNativeModel(model, entry.identity); @@ -547,7 +538,7 @@ function compilePayloadTransforms( const google = entries.filter((entry) => entry.providerBinding?.kind === "google-native"); if (google.length > 0) transforms.push(createGoogleTransform(google)); - if (["meta", "xai", "moonshotai"].includes(model.provider) && entries.some((entry) => entry.stateMutating)) { + if (cuaModelCapabilities(model).serializesStateMutations && entries.some((entry) => entry.stateMutating)) { transforms.push({ identity: `provider.${model.provider}.serial-tool-calls`, writes: ["parallel_tool_calls"], diff --git a/packages/ai/test/api-keys.test.ts b/packages/ai/test/api-keys.test.ts index bdbd7ec9..205be36a 100644 --- a/packages/ai/test/api-keys.test.ts +++ b/packages/ai/test/api-keys.test.ts @@ -15,6 +15,7 @@ const ENV_KEYS = [ "META_API_KEY", "XAI_API_KEY", "MOONSHOT_API_KEY", + "OPENROUTER_API_KEY", "TZAFON_API_KEY", "YUTORI_API_KEY", ] as const; @@ -38,6 +39,7 @@ describe("cua api key helpers", () => { expect(cuaApiKeyEnvVarsForProvider("xai")).toEqual(["XAI_API_KEY"]); expect(cuaApiKeyEnvVarsForProvider("moonshotai")).toEqual(["MOONSHOT_API_KEY"]); expect(cuaApiKeyEnvVarsForProvider("moonshot")).toEqual(["MOONSHOT_API_KEY"]); + expect(cuaApiKeyEnvVarsForProvider("openrouter")).toEqual(["OPENROUTER_API_KEY"]); expect(cuaApiKeyEnvVarsForProvider("unknown")).toEqual([]); }); @@ -58,6 +60,8 @@ describe("cua api key helpers", () => { expect(getCuaEnvApiKeyForModel("xai:grok-4.5")).toBe("xai"); process.env.MOONSHOT_API_KEY = "moonshot"; expect(getCuaEnvApiKeyForModel("moonshotai:kimi-k3")).toBe("moonshot"); + process.env.OPENROUTER_API_KEY = "openrouter"; + expect(getCuaEnvApiKeyForModel("openrouter:moonshotai/kimi-k3")).toBe("openrouter"); }); it("throws readable errors when missing", () => { diff --git a/packages/ai/test/models.test.ts b/packages/ai/test/models.test.ts index 79693f63..9744c398 100644 --- a/packages/ai/test/models.test.ts +++ b/packages/ai/test/models.test.ts @@ -29,7 +29,7 @@ describe("CUA model refs", () => { it("names the valid providers in the unsupported-provider error", () => { expect(() => parseCuaModelRef("bogus:model")).toThrow( - 'unsupported CUA provider "bogus" (expected one of: openai, anthropic, google, meta, xai, moonshotai, tzafon, yutori)', + 'unsupported CUA provider "bogus" (expected one of: openai, anthropic, google, meta, xai, moonshotai, openrouter, tzafon, yutori)', ); }); @@ -101,6 +101,14 @@ describe("CUA model refs", () => { ]); }); + it("uses pi-ai's Kimi catalog entries for both transports", () => { + const direct = getCuaModel("moonshotai:kimi-k3"); + const routed = getCuaModel("openrouter:moonshotai/kimi-k3"); + expect(direct).toMatchObject({ provider: "moonshotai", id: "kimi-k3", api: "openai-completions" }); + expect(routed).toMatchObject({ provider: "openrouter", id: "moonshotai/kimi-k3", api: "openai-completions" }); + expect(listCuaModels("openrouter").map((model) => model.ref)).toContain("openrouter:moonshotai/kimi-k3"); + }); + it("uses pi-ai's Kimi catalog entry without CUA routing overrides", () => { expect(cuaOverrideModels("moonshotai")).toEqual([]); const kimi = getCuaModel("moonshotai:kimi-k3"); diff --git a/packages/ai/test/providers.test.ts b/packages/ai/test/providers.test.ts index 4aacb988..2faf9e71 100644 --- a/packages/ai/test/providers.test.ts +++ b/packages/ai/test/providers.test.ts @@ -10,7 +10,7 @@ import { describe("createCuaModels", () => { it("registers the CUA-only providers alongside pi's builtins", () => { const models = createCuaModels(); - for (const id of ["openai", "anthropic", "google", "meta", "xai", "moonshotai", "tzafon", "yutori"]) { + for (const id of ["openai", "anthropic", "google", "meta", "xai", "moonshotai", "openrouter", "tzafon", "yutori"]) { const provider = models.getProvider(id); expect(provider, id).toBeDefined(); expect(provider?.stream).toBeTypeOf("function"); @@ -23,6 +23,8 @@ describe("createCuaModels", () => { expect(models.getModel("meta", "muse-spark-1.1")?.api).toBe("meta-responses"); expect(models.getModel("xai", "grok-4.5")?.api).toBe("openai-responses"); expect(models.getModel("moonshotai", "kimi-k3")?.api).toBe("openai-completions"); + expect(models.getModel("openrouter", "moonshotai/kimi-k3")?.api).toBe("openai-completions"); + expect(models.getProvider("openrouter")?.baseUrl).toBe("https://openrouter.ai/api/v1"); const tzafonIds = models.getModels("tzafon").map((m) => m.id); expect(tzafonIds).toContain("tzafon.northstar-cua-fast"); const yutoriIds = models.getModels("yutori").map((m) => m.id); diff --git a/packages/ai/test/tool-catalog.test.ts b/packages/ai/test/tool-catalog.test.ts index 52252d0c..2e70fb27 100644 --- a/packages/ai/test/tool-catalog.test.ts +++ b/packages/ai/test/tool-catalog.test.ts @@ -282,8 +282,15 @@ describe("compileCuaToolCatalog", () => { expect(catalog.incoming.googleExcludedNames).toEqual(expectedExcludedNames); }); + it("rejects browser_act but accepts browser primitives for both Kimi transports", () => { + for (const model of ["moonshotai:kimi-k3", "openrouter:moonshotai/kimi-k3"] as const) { + expect(() => compile(model, [cua.tools.browser.act()])).toThrow(/schema size/); + expect(() => compile(model, cua.toolsets.browser())).not.toThrow(); + } + }); + it("serializes state-mutating Meta/xAI/Moonshot catalogs with serial tool calls", async () => { - for (const model of ["meta:muse-spark-1.1", "xai:grok-4.5", "moonshotai:kimi-k3"] as const) { + for (const model of ["meta:muse-spark-1.1", "xai:grok-4.5", "moonshotai:kimi-k3", "openrouter:moonshotai/kimi-k3"] as const) { const catalog = compile(model, cua.toolsets.browser()); await expect(catalog.payload.apply({ parallel_tool_calls: true }, catalog.model)).resolves.toMatchObject({ parallel_tool_calls: false }); } @@ -311,6 +318,16 @@ describe("compileCuaToolCatalog", () => { it("rejects partial n1 selection and incompatible model changes", () => { expect(() => compile("yutori:n1-latest", cua.providers.yutori.toolsets.n1().slice(0, 1))).toThrow(/complete .*n1\(\)/); + const nativeTools: Array<[CuaToolSpec[], string]> = [ + [[cua.providers.anthropic.tools.browser()], "anthropic"], + [[cua.providers.openai.tools.computer()], "openai"], + [[cua.providers.google.toolsets.browser()[0]!], "google"], + [[cua.providers.tzafon.tools.computer()], "tzafon"], + [cua.providers.yutori.toolsets.n1(), "yutori"], + ]; + for (const [tools, provider] of nativeTools) { + expect(() => compile("openrouter:moonshotai/kimi-k3", tools)).toThrow(new RegExp(`requires a ${provider} model`)); + } const requested = [cua.providers.anthropic.tools.browser()]; expect(() => compile("openai:gpt-5.5", requested)).toThrow(/requires a anthropic model/); }); diff --git a/packages/cli/CHANGELOG.md b/packages/cli/CHANGELOG.md index 85d9dcc1..7579acad 100644 --- a/packages/cli/CHANGELOG.md +++ b/packages/cli/CHANGELOG.md @@ -1,5 +1,17 @@ # Changelog +## 0.7.0 - 2026-08-03 + +- Add `openrouter:moonshotai/kimi-k3` model selection and + `cua models -p openrouter`, authenticated with `OPENROUTER_API_KEY`. +- Use the same browser-primitives-only interaction catalog for Kimi K3 through + Moonshot and OpenRouter; neither transport receives the unsupported + `browser_act` schema. +- Resolve model references through the harness's `Models` collection so CLI + model selection, authentication, and streaming use the same concrete pi-ai + model. +- Update `@onkernel/cua-ai` and `@onkernel/cua-agent` to 0.9.0. + ## 0.6.0 - 2026-07-31 Breaking: the CLI now assembles one explicit model-specific tool list. diff --git a/packages/cli/README.md b/packages/cli/README.md index 63bab90e..8fe43f0f 100644 --- a/packages/cli/README.md +++ b/packages/cli/README.md @@ -138,7 +138,7 @@ Run `cua models` to list every supported `-m` / `--model` value and the provider it routes to. Filter by provider with `cua models -p openai`, `cua models -p anthropic`, `cua models -p google` (alias: `gemini`), `cua models -p meta`, `cua models -p xai`, `cua models -p moonshotai` -(alias: `moonshot`), or `cua models -p yutori`. +(alias: `moonshot`), `cua models -p openrouter`, or `cua models -p yutori`. `-m` / `--model` accepts a provider-qualified `provider:model` ref (e.g. `openai:gpt-5.6-sol`) or a bare model id when it matches exactly one catalog @@ -158,6 +158,7 @@ Configuration is by environment variable. There is no config file. | `META_API_KEY` | Meta Model API key (required when `-m meta:…`) | | `XAI_API_KEY` | xAI API key (required when `-m xai:…`) | | `MOONSHOT_API_KEY` | Moonshot AI API key (required when `-m moonshotai:…`) | +| `OPENROUTER_API_KEY` | OpenRouter API key (required when `-m openrouter:…`) | | `TZAFON_API_KEY` | Tzafon API key (required when `-m tzafon:…`) | | `YUTORI_API_KEY` | Yutori API key (required when `-m yutori:…`) | | `KERNEL_BASE_URL` | override Kernel base URL | diff --git a/packages/cli/package.json b/packages/cli/package.json index b407ade0..f0189bad 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -1,6 +1,6 @@ { "name": "@onkernel/cua-cli", - "version": "0.6.0", + "version": "0.7.0", "description": "Kernel-cloud-browser computer-use TUI built on @onkernel/cua-agent and pi-tui", "license": "MIT", "type": "module", @@ -37,8 +37,8 @@ "dependencies": { "@earendil-works/pi-coding-agent": "0.80.10", "@earendil-works/pi-tui": "0.80.10", - "@onkernel/cua-agent": "0.8.0", - "@onkernel/cua-ai": "0.8.0", + "@onkernel/cua-agent": "0.9.0", + "@onkernel/cua-ai": "0.9.0", "@onkernel/sdk": "0.49.0" }, "devDependencies": { diff --git a/packages/cli/src/cli-harness.ts b/packages/cli/src/cli-harness.ts index 13da071a..053e94d3 100644 --- a/packages/cli/src/cli-harness.ts +++ b/packages/cli/src/cli-harness.ts @@ -57,7 +57,7 @@ Usage: cua models --json Options: - -p, --provider Filter by provider: openai | anthropic | google | gemini | meta | xai | moonshotai | tzafon | yutori + -p, --provider Filter by provider: openai | anthropic | google | gemini | meta | xai | moonshotai | openrouter | tzafon | yutori --json Output JSON -h, --help Show this help `; diff --git a/packages/cli/src/cli.ts b/packages/cli/src/cli.ts index 76fce5c4..9dbb6b48 100644 --- a/packages/cli/src/cli.ts +++ b/packages/cli/src/cli.ts @@ -100,6 +100,7 @@ Environment: META_API_KEY Meta Model API key (required when -m meta:…) XAI_API_KEY xAI API key (required when -m xai:…) MOONSHOT_API_KEY Moonshot AI API key (required when -m moonshotai:…) + OPENROUTER_API_KEY OpenRouter API key (required when -m openrouter:…) TZAFON_API_KEY Tzafon API key (required when -m tzafon:…) YUTORI_API_KEY Yutori API key (required when -m yutori:…) KERNEL_BASE_URL Override Kernel base URL diff --git a/packages/cli/src/harness.ts b/packages/cli/src/harness.ts index 4893b342..5d8585b7 100644 --- a/packages/cli/src/harness.ts +++ b/packages/cli/src/harness.ts @@ -106,7 +106,8 @@ export function defaultInteractionTools(model: CuaModelRef): CuaAgentTool[] { case "xai": return structuredBrowserTools(); case "moonshotai": - // Moonshot's API rejects the request outright once `browser_act`'s + case "openrouter": + // Kimi's API rejects the request outright once `browser_act`'s // schema is attached, so Kimi keeps the browser primitives only. return cua.toolsets.browser(); } diff --git a/packages/cli/test/harness-assembly.test.ts b/packages/cli/test/harness-assembly.test.ts index d96bfe9a..1c13e1f7 100644 --- a/packages/cli/test/harness-assembly.test.ts +++ b/packages/cli/test/harness-assembly.test.ts @@ -30,11 +30,13 @@ describe("buildCuaHarness", () => { expect(tools[0]).toMatchObject({ name: "browser_snapshot", origin: "cua" }); expect(tools.at(-1)?.name).toBe("browser_act"); } - // Moonshot's API rejects the request once browser_act's schema is attached. - const moonshotNames = defaultInteractionTools("moonshotai:kimi-k3").map((tool) => tool.name); - expect(moonshotNames[0]).toBe("browser_snapshot"); - expect(moonshotNames).not.toContain("browser_act"); - expect(moonshotNames).toContain("browser_wait_for"); + // Kimi's API rejects the request once browser_act's schema is attached. + for (const model of ["moonshotai:kimi-k3", "openrouter:moonshotai/kimi-k3"] as const) { + const kimiNames = defaultInteractionTools(model).map((tool) => tool.name); + expect(kimiNames[0]).toBe("browser_snapshot"); + expect(kimiNames).not.toContain("browser_act"); + expect(kimiNames).toContain("browser_wait_for"); + } expect(defaultInteractionTools("tzafon:tzafon.northstar-cua-fast")[0]?.name).toBe("computer"); expect(defaultInteractionTools("yutori:n1.5-latest").map((tool) => tool.name)).toEqual([ ...cua.providers.yutori.toolsets.n15Core().map((tool) => tool.name), diff --git a/packages/cli/test/harness-models.test.ts b/packages/cli/test/harness-models.test.ts index 266e851c..4e1377c8 100644 --- a/packages/cli/test/harness-models.test.ts +++ b/packages/cli/test/harness-models.test.ts @@ -35,6 +35,8 @@ describe("resolveCuaModelRef", () => { expect(listSupportedModels("xai").map((model) => model.ref)).toEqual(["xai:grok-4.5"]); expect(listSupportedModels("moonshotai").map((model) => model.ref)).toEqual(["moonshotai:kimi-k3"]); expect(listSupportedModels("moonshot").map((model) => model.ref)).toEqual(["moonshotai:kimi-k3"]); + expect(listSupportedModels("openrouter").map((model) => model.ref)).toEqual(["openrouter:moonshotai/kimi-k3"]); + expect(resolveCuaModelRef("openrouter:moonshotai/kimi-k3")).toBe("openrouter:moonshotai/kimi-k3"); }); it("treats 'gemini' as an alias for google when filtering", () => {