diff --git a/.agents/skills/update-models/SKILL.md b/.agents/skills/update-models/SKILL.md index f02b894..f09e343 100644 --- a/.agents/skills/update-models/SKILL.md +++ b/.agents/skills/update-models/SKILL.md @@ -1,6 +1,6 @@ --- name: update-models -description: Discover latest OpenAI, Anthropic, Google/Gemini, Meta, xAI, Tzafon, and Yutori models and verify computer-use support. Use when updating CUA model defaults, checking new model releases, auditing provider-native computer tool actions, or comparing provider metadata, official examples, and smoke-test results. +description: Discover latest OpenAI, Anthropic, Google/Gemini, Meta, xAI, Moonshot, Tzafon, and Yutori models and verify computer-use support. Use when updating CUA model defaults, checking new model releases, auditing provider-native computer tool actions, or comparing provider metadata, official examples, and smoke-test results. --- # Update Models @@ -9,14 +9,14 @@ Use this workflow to keep CUA current with provider model releases and computer- ## Quick Start -1. Verify credentials are available: `OPENAI_API_KEY`, `ANTHROPIC_API_KEY`, `GOOGLE_API_KEY` or `GEMINI_API_KEY`, `META_API_KEY`, `XAI_API_KEY`, `TZAFON_API_KEY`, and `YUTORI_API_KEY`. +1. Verify credentials are available: `OPENAI_API_KEY`, `ANTHROPIC_API_KEY`, `GOOGLE_API_KEY` or `GEMINI_API_KEY`, `META_API_KEY`, `XAI_API_KEY`, `MOONSHOT_API_KEY`, `TZAFON_API_KEY`, and `YUTORI_API_KEY`. 2. If credentials live in `~/AGENTS.md`, load them into the current shell without printing them: ```bash eval "$(python3 - <<'PY' import pathlib, re, shlex text = pathlib.Path('~/AGENTS.md').expanduser().read_text() -for key in ['OPENAI_API_KEY', 'ANTHROPIC_API_KEY', 'GOOGLE_API_KEY', 'META_API_KEY', 'XAI_API_KEY', 'TZAFON_API_KEY', 'YUTORI_API_KEY']: +for key in ['OPENAI_API_KEY', 'ANTHROPIC_API_KEY', 'GOOGLE_API_KEY', 'META_API_KEY', 'XAI_API_KEY', 'MOONSHOT_API_KEY', 'TZAFON_API_KEY', 'YUTORI_API_KEY']: m = re.search(r'export\s+' + re.escape(key) + r'=(?:"([^"]+)"|([^\s\n]+))', text) if m: print(f'export {key}={shlex.quote(m.group(1) or m.group(2))}') @@ -114,6 +114,16 @@ xAI: - Use CUA's normalized 0-1000 coordinate instructions, `parallel_tool_calls: false`, `store: true`, and `previous_response_id` for browser loops. xAI accepts encrypted reasoning replay in these requests. - Use `reasoning: { effort: "low" }` for low-latency smoke tests; Grok 4.5 also supports `medium` and `high`, cannot disable reasoning, and defaults to `high`. +Moonshot: + +- Discover with the OpenAI SDK against `https://api.moonshot.ai/v1` using `MOONSHOT_API_KEY`. +- Smoke-test the chat completions API with screenshot input and explicit function tools matching CUA's canonical actions. +- Pass condition: response `choices[0].message.tool_calls[]` contains one of the supplied browser actions. +- Treat Kimi computer use as custom-function-tool support, not a provider-native computer tool. Moonshot documents vision input and function calling but no coordinate protocol. +- Kimi grounding emits width/height fractions from 0 to 1 regardless of prompt or schema wording; keep CUA's fractional coordinate contract and verify emitted values stay in 0-1. +- Set `parallel_tool_calls: false` because browser actions mutate shared state. There is no response threading; the full context replays each turn. +- Kimi K3 launched with max-only thinking effort. pi-ai's registry entry clamps other levels away; re-check `thinkingLevelMap` when Moonshot ships low/high modes. + Tzafon: - Discover with `new Lightcone({ apiKey }).models.list()` from `@tzafon/lightcone` when available. @@ -187,6 +197,7 @@ All CUA model and adapter support lives in `packages/ai` (`@onkernel/cua-ai`). W - Anthropic: update the `ANTHROPIC_CUA_ACTION_TYPES` set in `packages/ai/src/providers/anthropic/actions.ts` and `index.ts`. The computer tool version and `computer-use-*` beta header are selected by `pi-ai` per model, so a new dated tool version usually means bumping `@earendil-works/pi-ai`, not editing this package. - Gemini: update `packages/ai/src/providers/gemini/index.ts`, including coordinate handling if needed. - xAI: update `packages/ai/src/providers/xai/index.ts` and `provider.ts`, including normalized coordinate instructions, Responses threading, and reasoning compatibility. + - Moonshot: update `packages/ai/src/providers/moonshot/index.ts`, including the fractional coordinate instructions and payload middleware. Streaming rides pi-ai's builtin `openai-completions` transport, so wire-format changes usually mean bumping `@earendil-works/pi-ai`. - Tzafon: update `packages/ai/src/providers/tzafon/index.ts` and `provider.ts`, including coordinate/action handling. - Yutori: update `packages/ai/src/providers/yutori/actions.ts`, `index.ts`, and `provider.ts`, including payload filtering and coordinate/action handling. - Shared canonical action semantics go in `packages/ai/src/providers/common.ts`. diff --git a/.agents/skills/update-models/reference/README.md b/.agents/skills/update-models/reference/README.md index 7bb813d..d2e6c09 100644 --- a/.agents/skills/update-models/reference/README.md +++ b/.agents/skills/update-models/reference/README.md @@ -13,10 +13,11 @@ These scripts support the `update-models` skill. Run them from the repository ro - `GOOGLE_API_KEY` or `GEMINI_API_KEY` - `META_API_KEY` - `XAI_API_KEY` + - `MOONSHOT_API_KEY` - `TZAFON_API_KEY` - `YUTORI_API_KEY` -The scripts never print API keys. Smoke tests are non-destructive: they ask each model to emit a computer-use tool call, then inspect the response without executing the action. Meta and xAI use supplied function tools; other providers may use provider-native computer tools. +The scripts never print API keys. Smoke tests are non-destructive: they ask each model to emit a computer-use tool call, then inspect the response without executing the action. Meta, xAI, and Moonshot use supplied function tools; other providers may use provider-native computer tools. ## Common Commands diff --git a/.agents/skills/update-models/reference/audit-official-examples.ts b/.agents/skills/update-models/reference/audit-official-examples.ts index cc6a4b0..1005b43 100644 --- a/.agents/skills/update-models/reference/audit-official-examples.ts +++ b/.agents/skills/update-models/reference/audit-official-examples.ts @@ -5,7 +5,7 @@ import { basename, join, resolve } from "node:path"; import { spawnSync } from "node:child_process"; import process from "node:process"; -type Provider = "openai" | "anthropic" | "gemini" | "meta" | "xai" | "yutori"; +type Provider = "openai" | "anthropic" | "gemini" | "meta" | "xai" | "moonshot" | "yutori"; interface ExampleRepo { provider: Provider; @@ -83,6 +83,7 @@ const ACTION_REGEXES: Record = { gemini: [/\b(open_web_browser|open_web|wait_5_seconds|go_back|go_forward|search|navigate|click_at|hover_at|type_text_at|key_combination|scroll_document|scroll_at|drag_and_drop)\b/g], meta: [/\b(screenshot|left_click|right_click|middle_click|double_click|triple_click|left_click_drag|mouse_move|key|type|scroll|hold_key|wait|left_mouse_down|left_mouse_up)\b/g], xai: [/\b(screenshot|click|double_click|mouse_down|mouse_up|scroll|type|keypress|drag|move|wait)\b/g], + moonshot: [/\b(screenshot|click|double_click|mouse_down|mouse_up|scroll|type|keypress|drag|move|wait)\b/g], yutori: [/\b(left_click|double_click|triple_click|right_click|scroll|type|key_press|hover|drag|wait|refresh|go_back|goto_url|mouse_move|middle_click|mouse_down|mouse_up|go_forward|hold_key|extract_elements|find|set_element_value|execute_js)\b/g], }; diff --git a/.agents/skills/update-models/reference/discover-models.ts b/.agents/skills/update-models/reference/discover-models.ts index 097c87f..f243e6c 100644 --- a/.agents/skills/update-models/reference/discover-models.ts +++ b/.agents/skills/update-models/reference/discover-models.ts @@ -6,7 +6,7 @@ import { join } from "node:path"; import process from "node:process"; import { parse as parseToml } from "smol-toml"; -type Provider = "openai" | "anthropic" | "gemini" | "meta" | "xai" | "tzafon" | "yutori"; +type Provider = "openai" | "anthropic" | "gemini" | "meta" | "xai" | "moonshot" | "tzafon" | "yutori"; interface Args { provider: Provider | "all"; @@ -39,7 +39,7 @@ interface ModelResult { cua?: Record; } -const PROVIDERS: Provider[] = ["openai", "anthropic", "gemini", "meta", "xai", "tzafon", "yutori"]; +const PROVIDERS: Provider[] = ["openai", "anthropic", "gemini", "meta", "xai", "moonshot", "tzafon", "yutori"]; const TZAFON_KNOWN_MODELS = [ "tzafon.northstar-cua-fast", ]; @@ -110,7 +110,7 @@ function usage(): never { npx tsx .agents/skills/update-models/reference/discover-models.ts --provider openai --models gpt-5.5,gpt-5.4 Options: - --provider + --provider --models Smoke-test explicit models instead of inferred candidates. --candidate-limit Max inferred candidates per provider. Default: 20. --no-smoke Only list metadata. @@ -140,6 +140,7 @@ async function runProvider(provider: Provider, args: Args): Promise> { + const OpenAI = await importDefault("openai", "OpenAI"); + const client = new OpenAI({ apiKey: process.env.MOONSHOT_API_KEY, baseURL: "https://api.moonshot.ai/v1" }); + const rawModels = await collectAsync(client.models.list()); + const models: ModelResult[] = rawModels.map((m) => ({ + id: String(m.id), + display_name: String(m.id), + created_at: typeof m.created === "number" && m.created > 0 ? new Date(m.created * 1000).toISOString() : null, + raw: m, + supports_generation: String(m.id).toLowerCase().startsWith("kimi-"), + model_docs: { + url: "https://platform.kimi.ai/docs/api/tool-use", + chat_completions_endpoint: "supported", + function_calling: "supported", + image_input: "verify-per-model", + coordinate_space: "CUA-defined 0-1 fractions", + }, + })); + const candidates = explicitOrCandidates( + args, + models + .filter((model) => model.supports_generation) + .sort(compareMoonshotCandidates) + .map((model) => model.id), + ); + if (args.smoke) { + await Promise.all(candidates.map(async (id) => { + const model = models.find((candidate) => candidate.id === id) ?? { id, display_name: id, supports_generation: true }; + model.computer_use = await smokeMoonshot(client, id); + if (!models.find((candidate) => candidate.id === id)) models.unshift(model); + })); + } + await annotateCuaSupport("moonshot", models); + return { provider: "moonshot", metadata_source: "Moonshot models.list()", models, candidates }; +} + +async function smokeMoonshot(client: any, model: string): Promise { + try { + const screenshot = await readFile(fixtureScreenshotPath()); + const response = await client.chat.completions.create({ + model, + parallel_tool_calls: false, + max_tokens: 8192, + messages: [ + { + role: "system", + content: "Coordinates are fractions of the screenshot, normalized from 0 to 1.", + }, + { + role: "user", + content: [ + { type: "text", text: "Call the click tool for the sign in link. Do not answer only in text." }, + { type: "image_url", image_url: { url: `data:image/png;base64,${screenshot.toString("base64")}` } }, + ], + }, + ], + tools: [{ + type: "function", + function: { + name: "click", + description: "Click at coordinates given as 0-1 fractions of the screenshot.", + parameters: { + type: "object", + properties: { x: { type: "number" }, y: { type: "number" } }, + required: ["x", "y"], + additionalProperties: false, + }, + }, + }], + }); + const calls: any[] = response.choices?.[0]?.message?.tool_calls ?? []; + return { + status: calls.length > 0 ? "pass" : "inconclusive", + tool_name: "function_tools", + tool_version: null, + beta_header: null, + observed_actions: unique(calls.map((call) => call?.function?.name).filter(Boolean)), + response_item_types: unique([response.choices?.[0]?.finish_reason].filter(Boolean)), + coordinate_space: "0-1 fractions", + error: null, + }; + } catch (err) { + return smokeError(err, { tool_name: "function_tools" }); + } +} + +function compareMoonshotCandidates(a: ModelResult, b: ModelResult): number { + if (a.id === "kimi-k3") return -1; + if (b.id === "kimi-k3") return 1; + return String(b.created_at ?? "").localeCompare(String(a.created_at ?? "")); +} + function compareXaiCandidates(a: ModelResult, b: ModelResult): number { if (a.id === "grok-4.5") return -1; if (b.id === "grok-4.5") return 1; @@ -839,7 +932,7 @@ const TZAFON_FUNCTION_TOOLS = [ ]; async function annotateCuaSupport(provider: Provider, models: ModelResult[]): Promise { - const piProvider = provider === "gemini" ? "google" : provider; + const piProvider = provider === "gemini" ? "google" : provider === "moonshot" ? "moonshotai" : provider; const getBuiltinModel = await import("@earendil-works/pi-ai/providers/all").then((mod) => mod.getBuiltinModel).catch(() => undefined); for (const model of models) { const inRegistry = getBuiltinModel ? !!getBuiltinModel(piProvider as never, model.id as never) : false; diff --git a/.agents/skills/update-models/reference/provider-doc-drift.ts b/.agents/skills/update-models/reference/provider-doc-drift.ts index 46b153c..95ea4be 100644 --- a/.agents/skills/update-models/reference/provider-doc-drift.ts +++ b/.agents/skills/update-models/reference/provider-doc-drift.ts @@ -2,7 +2,7 @@ import { readFile, writeFile } from "node:fs/promises"; import process from "node:process"; -type Provider = "openai" | "anthropic" | "gemini" | "meta" | "xai" | "yutori"; +type Provider = "openai" | "anthropic" | "gemini" | "meta" | "xai" | "moonshot" | "yutori"; interface Args { examples: string; @@ -40,6 +40,11 @@ const DOCS: Record = { "https://docs.x.ai/developers/tools/function-calling", "https://docs.x.ai/developers/model-capabilities/images/understanding", ], + moonshot: [ + "https://www.kimi.com/blog/kimi-k3", + "https://platform.kimi.ai/docs/api/tool-use", + "https://platform.kimi.ai/docs/guide/use-kimi-vision-model", + ], yutori: [ "https://docs.yutori.com/reference/navigator", "https://docs.yutori.com/reference/n1", @@ -54,6 +59,7 @@ const LOCAL_FILES: Record = { gemini: "packages/ai/src/providers/gemini/index.ts", meta: "packages/ai/src/providers/meta/index.ts", xai: "packages/ai/src/providers/xai/index.ts", + moonshot: "packages/ai/src/providers/moonshot/index.ts", yutori: "packages/ai/src/providers/yutori/actions.ts", }; @@ -63,6 +69,7 @@ const ACTION_REGEXES: Record = { gemini: /\b(open_web_browser|open_web|wait_5_seconds|go_back|go_forward|search|navigate|click_at|hover_at|type_text_at|key_combination|scroll_document|scroll_at|drag_and_drop)\b/g, meta: /\b(click|double_click|mouse_down|mouse_up|scroll|type|wait|keypress|drag|move|screenshot|goto|back|forward|url|cursor_position|left_click|right_click|middle_click|triple_click|left_click_drag|mouse_move|key|hold_key|left_mouse_down|left_mouse_up)\b/g, xai: /\b(click|double_click|mouse_down|mouse_up|scroll|type|wait|keypress|drag|move|screenshot|goto|back|forward|url|cursor_position)\b/g, + moonshot: /\b(click|double_click|mouse_down|mouse_up|scroll|type|wait|keypress|drag|move|screenshot|goto|back|forward|url|cursor_position)\b/g, yutori: /\b(left_click|double_click|triple_click|right_click|scroll|type|key_press|hover|drag|wait|refresh|go_back|go_forward|goto_url|mouse_move|middle_click|mouse_down|mouse_up|hold_key|extract_elements|find|set_element_value|execute_js)\b/g, }; @@ -137,8 +144,8 @@ async function checkProvider(provider: Provider, examples: any): Promise models.getModels(provider), getModel: (provider, id) => models.getModel(provider, id), refresh: (provider) => models.refresh(provider), - getAuth: (model) => models.getAuth(model), + getAuth: (input, overrides) => models.getAuth(input as never, overrides), + checkAuth: (providerId) => models.checkAuth(providerId), + getAvailable: (providerId) => models.getAvailable(providerId), + login: (providerId, type, interaction) => models.login(providerId, type, interaction), + logout: (providerId) => models.logout(providerId), stream, complete, streamSimple, diff --git a/packages/agent/src/provider-retry.ts b/packages/agent/src/provider-retry.ts index 3bfa848..bfb27f1 100644 --- a/packages/agent/src/provider-retry.ts +++ b/packages/agent/src/provider-retry.ts @@ -88,7 +88,11 @@ export function withProviderRetryModels(models: Models, policy: ResolvedRetryPol getModels: (provider) => models.getModels(provider), getModel: (provider, id) => models.getModel(provider, id), refresh: (provider) => models.refresh(provider), - getAuth: (model) => models.getAuth(model), + getAuth: (input, overrides) => models.getAuth(input as never, overrides), + checkAuth: (providerId) => models.checkAuth(providerId), + getAvailable: (providerId) => models.getAvailable(providerId), + login: (providerId, type, interaction) => models.login(providerId, type, interaction), + logout: (providerId) => models.logout(providerId), stream: (model, context, options) => models.stream(model, context, options), complete: (model, context, options) => models.complete(model, context, options), streamSimple, diff --git a/packages/agent/test/e2e.live.test.ts b/packages/agent/test/e2e.live.test.ts index b3fe495..2b51c6c 100644 --- a/packages/agent/test/e2e.live.test.ts +++ b/packages/agent/test/e2e.live.test.ts @@ -22,6 +22,7 @@ type ProviderCase = { | "google:gemini-3-flash-preview" | "meta:muse-spark-1.1" | "xai:grok-4.5" + | "moonshotai:kimi-k3" | "tzafon:tzafon.northstar-cua-fast" | "yutori:n1.5-latest"; prompt: string; @@ -107,6 +108,19 @@ const cases: ProviderCase[] = [ expectToolCalls: true, timeoutMs: 180_000, }, + { + name: "moonshotai", + apiKeyEnvVar: "MOONSHOT_API_KEY", + modelRef: "moonshotai:kimi-k3", + prompt: [ + "Use the tool named `screenshot` exactly once to inspect the browser.", + "Pass empty arguments (`{}`).", + "Do not call any other tools.", + "After the tool result, provide a one-sentence summary.", + ].join("\n"), + expectToolCalls: true, + timeoutMs: 180_000, + }, { name: "tzafon", apiKeyEnvVar: "TZAFON_API_KEY", diff --git a/packages/ai/CHANGELOG.md b/packages/ai/CHANGELOG.md index b76207b..e635e1c 100644 --- a/packages/ai/CHANGELOG.md +++ b/packages/ai/CHANGELOG.md @@ -1,5 +1,20 @@ # Changelog +## 0.7.0 - 2026-07-17 + +- Added Moonshot Kimi K3 computer-use support: `moonshotai:kimi-k3` + (`moonshot:` accepted as a ref alias), authenticated with + `MOONSHOT_API_KEY`. Kimi streams through the OpenAI-compatible chat + completions transport with ordinary function tools. +- New `moonshot` provider namespace following the standard conventions + (`computerTools`, `coordinateSystem`, `buildMoonshotSystemPrompt`, + `providerModule`, …). Kimi's coordinate contract is normalized 0–1 + width/height fractions, matching the model's native visual grounding; + payload middleware disables parallel tool calls. +- Bumped `@earendil-works/pi-ai` to 0.80.10 (carries the Kimi K3 registry + entry). Grok 4.5's registry api id is now `openai-responses`; CUA routing + behavior is unchanged. + ## 0.6.0 - 2026-07-10 Breaking: response threading is now configured per request instead of through diff --git a/packages/ai/README.md b/packages/ai/README.md index 219df16..6b630d1 100644 --- a/packages/ai/README.md +++ b/packages/ai/README.md @@ -22,6 +22,7 @@ check these environment variables, in order: | `google` | `GOOGLE_API_KEY`, `GEMINI_API_KEY` | | `meta` | `META_API_KEY` | | `xai` | `XAI_API_KEY` | +| `moonshotai` | `MOONSHOT_API_KEY` | | `tzafon` | `TZAFON_API_KEY` | | `yutori` | `YUTORI_API_KEY` | @@ -214,6 +215,7 @@ getCuaModel("anthropic:claude-opus-4-7"); getCuaModel("google:gemini-3-flash-preview"); getCuaModel("meta:muse-spark-1.1"); getCuaModel("xai:grok-4.5"); +getCuaModel("moonshotai:kimi-k3"); // "moonshot:" is accepted as an alias getCuaModel("tzafon:tzafon.northstar-cua-fast"); getCuaModel("yutori:n1.5-latest"); ``` @@ -230,7 +232,7 @@ list of CUA-supporting models per provider. `CuaProvider` is the string union of provider IDs this package targets: ```ts -type CuaProvider = "openai" | "anthropic" | "google" | "meta" | "xai" | "tzafon" | "yutori"; +type CuaProvider = "openai" | "anthropic" | "google" | "meta" | "xai" | "moonshotai" | "tzafon" | "yutori"; ``` The IDs match pi-ai's `Model.provider` values exactly. `providerForModel(model)` @@ -377,6 +379,7 @@ Per-provider canonical action subsets (each namespace exports its list as | `gemini` | all 16 | | `meta` | all 16 | | `xai` | all 16 | +| `moonshot` | all 16 | | `tzafon` | all 16 (replaced on the wire by Tzafon's native `computer_use` tool) | | `yutori` | 13 — everything except `screenshot`, `url`, `cursor_position` (local mirrors only) | @@ -413,6 +416,7 @@ Current coordinate contracts: - `gemini`: normalized coordinates in the 0-999 range ([source](https://ai.google.dev/gemini-api/docs/computer-use)) - `meta`: normalized coordinates in the 0-1000 range ([source](https://dev.meta.ai/docs/getting-started/cookbook/computer-use-macos)) - `xai`: CUA-defined normalized coordinates in the 0-1000 range; Grok exposes image input and function calling rather than a native coordinate protocol ([source](https://docs.x.ai/developers/grok-4-5)) +- `moonshot`: CUA-defined normalized coordinates as 0-1 fractions; Kimi's visual grounding natively emits width/height fractions and Moonshot documents function calling and image input rather than a coordinate protocol ([source](https://platform.kimi.ai/docs/api/tool-use)) - `yutori`: normalized coordinates in the 0-1000 range ([source](https://docs.yutori.com/reference/navigator), [SDK helper](https://github.com/yutori-ai/yutori-sdk-python/blob/main/yutori/navigator/coordinates.py)) - `tzafon`: normalized coordinates in the 0-999 range ([source](https://docs.lightcone.ai/guides/coordinates/), [model card](https://huggingface.co/Tzafon/Northstar-CUA-Fast)) @@ -490,7 +494,7 @@ type CuaNavigationInput = { ## Provider Namespaces Every provider namespace (`openai`, `anthropic`, `gemini`, `meta`, `xai`, -`tzafon`, `yutori`) follows one convention: +`moonshot`, `tzafon`, `yutori`) follows one convention: - `computerTools(options?)` and `computerToolExecutors(options?)` - `createActionSchema(actions?)` — TypeBox schema for the provider's subset @@ -520,6 +524,9 @@ Provider-specific extras: `streamXaiResponses`, `streamSimpleXaiResponses`, `XaiResponsesOptions`), with serial function tools, encrypted reasoning replay, and `previous_response_id` threading +- `moonshot`: no stream adapter — Kimi flows through pi's builtin + `openai-completions` transport; the provider module's payload middleware + disables parallel tool calls - `tzafon`: the `tzafon-responses` stream adapter (`TZAFON_RESPONSES_API`, `streamTzafonResponses`, `streamSimpleTzafonResponses`, `TzafonResponsesOptions` with `keepToolNames`), `tzafonComputerUseOnPayload` diff --git a/packages/ai/docs/supported-models.md b/packages/ai/docs/supported-models.md index 2151cc7..b7be7aa 100644 --- a/packages/ai/docs/supported-models.md +++ b/packages/ai/docs/supported-models.md @@ -88,6 +88,25 @@ Grok 4.5 model metadata. Source: [Grok 4.5 docs](https://docs.x.ai/developers/grok-4-5), [function calling](https://docs.x.ai/developers/tools/function-calling), and [image understanding](https://docs.x.ai/developers/model-capabilities/images/understanding). +## `moonshotai` + +Coordinates: normalized 0–1 fractions (CUA function-tool contract) + +Model refs use the `moonshotai:` prefix; `moonshot:` is accepted as an alias. + +Exact IDs: + +- `kimi-k3` + +Kimi K3 uses Moonshot's OpenAI-compatible chat completions API with screenshot +input and ordinary function tools. Moonshot does not define a native computer +tool or coordinate protocol; Kimi's visual grounding emits width/height +fractions from 0 to 1, so CUA declares that contract and scales to viewport +pixels at execution. K3 launched with max-only thinking effort — other levels +are clamped away until 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). + ## `tzafon` Coordinates: normalized 0–999 diff --git a/packages/ai/examples/quickstart.ts b/packages/ai/examples/quickstart.ts index 0ac928d..7423a2a 100644 --- a/packages/ai/examples/quickstart.ts +++ b/packages/ai/examples/quickstart.ts @@ -12,6 +12,7 @@ import { // google:gemini-3-flash-preview GOOGLE_API_KEY // meta:muse-spark-1.1 META_API_KEY // xai:grok-4.5 XAI_API_KEY +// moonshotai:kimi-k3 MOONSHOT_API_KEY // tzafon:tzafon.northstar-cua-fast TZAFON_API_KEY // yutori:n1.5-latest YUTORI_API_KEY const modelRef = (process.env.CUA_MODEL ?? "openai:gpt-5.5") as CuaModelRef; diff --git a/packages/ai/package.json b/packages/ai/package.json index e349b65..6b299e1 100644 --- a/packages/ai/package.json +++ b/packages/ai/package.json @@ -1,6 +1,6 @@ { "name": "@onkernel/cua-ai", - "version": "0.6.0", + "version": "0.7.0", "description": "Kernel-curated computer-use model access built on pi-ai", "license": "MIT", "type": "module", @@ -41,7 +41,7 @@ "test:integration": "vitest --run --config vitest.integration.config.ts" }, "dependencies": { - "@earendil-works/pi-ai": "0.80.6", + "@earendil-works/pi-ai": "0.80.10", "@tzafon/lightcone": "^0.7.0", "openai": "^6.26.0" }, diff --git a/packages/ai/src/api-keys.ts b/packages/ai/src/api-keys.ts index 443ff13..41db310 100644 --- a/packages/ai/src/api-keys.ts +++ b/packages/ai/src/api-keys.ts @@ -14,19 +14,24 @@ const CUA_PROVIDER_API_KEY_ENV_VARS: Record = { google: ["GOOGLE_API_KEY", "GEMINI_API_KEY"], meta: ["META_API_KEY"], xai: ["XAI_API_KEY"], + moonshotai: ["MOONSHOT_API_KEY"], tzafon: ["TZAFON_API_KEY"], yutori: ["YUTORI_API_KEY"], }; /** * List the environment variables checked for a provider's API key, in - * precedence order. Accepts `"gemini"` as an alias for `"google"`; returns an - * empty list for unknown providers. + * precedence order. Accepts `"gemini"` as an alias for `"google"` and + * `"moonshot"` as an alias for `"moonshotai"`; returns an empty list for + * unknown providers. */ export function cuaApiKeyEnvVarsForProvider(provider: string): readonly string[] { if (provider === "gemini") { return CUA_PROVIDER_API_KEY_ENV_VARS.google; } + if (provider === "moonshot") { + return CUA_PROVIDER_API_KEY_ENV_VARS.moonshotai; + } return CUA_PROVIDER_API_KEY_ENV_VARS[provider as keyof typeof CUA_PROVIDER_API_KEY_ENV_VARS] ?? []; } diff --git a/packages/ai/src/index.ts b/packages/ai/src/index.ts index 6186410..eafd77f 100644 --- a/packages/ai/src/index.ts +++ b/packages/ai/src/index.ts @@ -8,6 +8,7 @@ export * from "./providers/common"; export * as anthropic from "./providers/anthropic/index"; export * as gemini from "./providers/gemini/index"; export * as meta from "./providers/meta/index"; +export * as moonshot from "./providers/moonshot/index"; export * as openai from "./providers/openai/index"; export * as tzafon from "./providers/tzafon/index"; export * as xai from "./providers/xai/index"; diff --git a/packages/ai/src/models.ts b/packages/ai/src/models.ts index 320569d..194a1fe 100644 --- a/packages/ai/src/models.ts +++ b/packages/ai/src/models.ts @@ -5,7 +5,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" | "tzafon" | "yutori"; +export type CuaProvider = "openai" | "anthropic" | "google" | "meta" | "xai" | "moonshotai" | "tzafon" | "yutori"; /** Provider-qualified model reference, e.g. `"openai:gpt-5.5"` or `"google:gemini-3-flash-preview"`. */ export type CuaModelRef = `${CuaProvider}:${string}`; @@ -22,7 +22,7 @@ export interface CuaModelInfo { } /** All providers this package curates computer-use models for. */ -export const CUA_PROVIDERS: readonly CuaProvider[] = ["openai", "anthropic", "google", "meta", "xai", "tzafon", "yutori"]; +export const CUA_PROVIDERS: readonly CuaProvider[] = ["openai", "anthropic", "google", "meta", "xai", "moonshotai", "tzafon", "yutori"]; /** * How a {@link CuaModelAnnotation} matches model ids. @@ -86,6 +86,12 @@ export const CUA_MODEL_ANNOTATIONS: Record[]> = { openai: [], anthropic: [], google: [], - // pi-ai 0.80.6 predates Meta's models.dev catalog entry. + // pi-ai 0.80.10 still lacks Meta's models.dev catalog entry. meta: [cuaModel("meta", "muse-spark-1.1", "Muse Spark 1.1")], xai: [], + moonshotai: [], 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"), @@ -133,8 +140,9 @@ export function cuaOverrideModels(provider: CuaProvider): readonly Model[] * Split a provider-qualified ref like `"openai:gpt-5.5"` into its parts. * * `"gemini:"` is accepted as an alias for the canonical `"google:"` prefix - * and normalizes to provider `"google"`. Throws when the ref is unqualified - * or names an unsupported provider. + * and normalizes to provider `"google"`; `"moonshot:"` likewise normalizes + * to `"moonshotai"`. Throws when the ref is unqualified or names an + * unsupported provider. */ export function parseCuaModelRef(ref: string): { provider: CuaProvider; model: string } { const idx = ref.indexOf(":"); @@ -142,7 +150,7 @@ export function parseCuaModelRef(ref: string): { provider: CuaProvider; model: s throw new Error(`CUA model ref must be provider-qualified as ":"; got "${ref}"`); } const prefix = ref.slice(0, idx); - const provider = prefix === "gemini" ? "google" : prefix; + const provider = prefix === "gemini" ? "google" : prefix === "moonshot" ? "moonshotai" : prefix; const model = ref.slice(idx + 1); if (!isCuaProvider(provider)) { throw new Error(`unsupported CUA provider "${prefix}" (expected one of: ${CUA_PROVIDERS.join(", ")})`); @@ -223,7 +231,7 @@ export function routeCuaApi(model: Model): Model { ...model.cost, tiers: [{ inputTokensAbove: 200_000, input: 4, output: 12, cacheRead: 1, cacheWrite: 0 }], }, - compat: { supportsDeveloperRole: false, sendSessionIdHeader: false, supportsLongCacheRetention: false }, + compat: { supportsDeveloperRole: false, sessionAffinityFormat: "openai-nosession", supportsLongCacheRetention: false }, }; } return model; @@ -273,7 +281,7 @@ function isCuaFamilyMatch(id: string, family: string): boolean { .every((segment) => /^\d+$/.test(segment)); } -function cuaModel(provider: Exclude, id: string, name: string): Model { +function cuaModel(provider: Exclude, id: string, name: string): Model { const base = { id, name, @@ -301,7 +309,7 @@ function cuaModel(provider: Exclude, id: string, name: strin cost: { input: 1.25, output: 4.25, cacheRead: 0.15, cacheWrite: 0 }, contextWindow: 1_048_576, maxTokens: 128_000, - compat: { supportsDeveloperRole: true, sendSessionIdHeader: false, supportsLongCacheRetention: true }, + compat: { supportsDeveloperRole: true, sessionAffinityFormat: "openai-nosession", supportsLongCacheRetention: true }, } as Model; case "tzafon": return { ...base, api: "tzafon-responses", baseUrl: "https://api.lightcone.ai", contextWindow: 128_000, maxTokens: 4_096 } as Model; diff --git a/packages/ai/src/providers.ts b/packages/ai/src/providers.ts index 4eafc3a..0dc3904 100644 --- a/packages/ai/src/providers.ts +++ b/packages/ai/src/providers.ts @@ -36,6 +36,8 @@ import { streamSimpleYutori, streamYutori, YUTORI_CHAT_COMPLETIONS_API } from ". * (pi's builtin only reads `GEMINI_API_KEY`). * - `xai` intercepts `xai-cua-responses` so Grok can use stateful Responses * tool loops while preserving pi's builtin xAI auth and catalog. + * - `moonshotai` is pi's builtin provider untouched: Kimi streams through the + * plain OpenAI-compatible chat completions transport with `MOONSHOT_API_KEY`. * - `meta`, `tzafon`, and `yutori` are CUA-only providers pi does not ship. * * Each call returns an independent collection; register additional providers diff --git a/packages/ai/src/providers/moonshot/index.ts b/packages/ai/src/providers/moonshot/index.ts new file mode 100644 index 0000000..fe5b37d --- /dev/null +++ b/packages/ai/src/providers/moonshot/index.ts @@ -0,0 +1,63 @@ +import { assertComputerModeOnly, computerToolExecutors, computerTools } from "../common"; +import type { ComputerToolCoordinateSystem, ComputerToolsOptions, CuaPayloadHook, CuaProviderModule } from "../common"; + +export { + CUA_ACTION_TYPES as MOONSHOT_CUA_ACTION_TYPES, + computerToolExecutors, + computerTools, + createCuaActionSchema as createActionSchema, +} from "../common"; +export type { + CuaAction as MoonshotAction, + ComputerToolsOptions, +} from "../common"; + +/** + * Instructions for driving Kimi through CUA's custom function tools. + * + * Moonshot documents {@link https://platform.moonshot.ai/docs/guide/use-kimi-vision-model | image input} + * and {@link https://platform.moonshot.ai/docs/api/tool-use | tool calling} on its + * OpenAI-compatible API, but does not define a native computer tool or + * coordinate space. + */ +export const MOONSHOT_COMPUTER_INSTRUCTIONS = `You control a Kernel cloud browser through individual browser tools. Coordinates are fractions of the most recent screenshot, normalized from 0 to 1, with (0, 0) at the top left and (1, 1) at the bottom right. Base each action on the latest observed state and request a screenshot when you need a fresh view.`; + +/** + * Build Moonshot's default system prompt for computer-use runs. + */ +export function buildMoonshotSystemPrompt(opts: { suffix?: string } = {}): string { + return [MOONSHOT_COMPUTER_INSTRUCTIONS, opts.suffix].filter(Boolean).join("\n\n"); +} + +/** + * Return the normalized coordinate contract CUA supplies to Kimi function tools. + * + * Kimi's visual grounding natively emits width/height fractions: probed with + * real screenshots, Kimi answers 0-1 fractional coordinates regardless of + * whether the prompt or tool schema asks for pixels or a 0-1000 grid (e.g. + * (0.927, 0.293) for a link whose center is at that exact fraction). CUA + * declares the contract the model actually honors and scales at execution. + */ +export function coordinateSystem(): ComputerToolCoordinateSystem { + return { type: "normalized", range: [0, 1] }; +} + +// Browser actions mutate shared state, so ask the API for serial tool calls. +const onPayload: CuaPayloadHook = (payload) => { + if (!payload || typeof payload !== "object") return undefined; + return { ...(payload as Record), parallel_tool_calls: false }; +}; + +export const providerModule = { + toolDefinitions: (options?: ComputerToolsOptions) => { + assertComputerModeOnly("moonshotai", options); + return computerTools(options); + }, + toolExecutors: (options?: ComputerToolsOptions) => { + assertComputerModeOnly("moonshotai", options); + return computerToolExecutors(options); + }, + coordinateSystem, + buildSystemPrompt: buildMoonshotSystemPrompt, + onPayload, +} satisfies CuaProviderModule; diff --git a/packages/ai/src/runtime-spec.ts b/packages/ai/src/runtime-spec.ts index 25742ae..2777ac8 100644 --- a/packages/ai/src/runtime-spec.ts +++ b/packages/ai/src/runtime-spec.ts @@ -6,6 +6,7 @@ import { providerModule as anthropic } from "./providers/anthropic/index"; import { createNativeToolOnPayload, nativeApiForToolType, nativeToolExecutors } from "./providers/anthropic/native"; import { providerModule as gemini } from "./providers/gemini/index"; import { providerModule as meta } from "./providers/meta/index"; +import { providerModule as moonshot } from "./providers/moonshot/index"; import { providerModule as openai } from "./providers/openai/index"; import { providerModule as tzafon } from "./providers/tzafon/index"; import { providerModule as xai } from "./providers/xai/index"; @@ -24,6 +25,7 @@ const PROVIDERS = { google: gemini, meta, xai, + moonshotai: moonshot, tzafon, yutori, } satisfies Record; diff --git a/packages/ai/test/api-keys.test.ts b/packages/ai/test/api-keys.test.ts index c40bf3e..bdbd7ec 100644 --- a/packages/ai/test/api-keys.test.ts +++ b/packages/ai/test/api-keys.test.ts @@ -14,6 +14,7 @@ const ENV_KEYS = [ "GEMINI_API_KEY", "META_API_KEY", "XAI_API_KEY", + "MOONSHOT_API_KEY", "TZAFON_API_KEY", "YUTORI_API_KEY", ] as const; @@ -35,6 +36,8 @@ describe("cua api key helpers", () => { expect(cuaApiKeyEnvVarsForProvider("gemini")).toEqual(["GOOGLE_API_KEY", "GEMINI_API_KEY"]); expect(cuaApiKeyEnvVarsForProvider("meta")).toEqual(["META_API_KEY"]); expect(cuaApiKeyEnvVarsForProvider("xai")).toEqual(["XAI_API_KEY"]); + expect(cuaApiKeyEnvVarsForProvider("moonshotai")).toEqual(["MOONSHOT_API_KEY"]); + expect(cuaApiKeyEnvVarsForProvider("moonshot")).toEqual(["MOONSHOT_API_KEY"]); expect(cuaApiKeyEnvVarsForProvider("unknown")).toEqual([]); }); @@ -53,6 +56,8 @@ describe("cua api key helpers", () => { expect(getCuaEnvApiKeyForModel("meta:muse-spark-1.1")).toBe("meta"); process.env.XAI_API_KEY = "xai"; expect(getCuaEnvApiKeyForModel("xai:grok-4.5")).toBe("xai"); + process.env.MOONSHOT_API_KEY = "moonshot"; + expect(getCuaEnvApiKeyForModel("moonshotai:kimi-k3")).toBe("moonshot"); }); it("throws readable errors when missing", () => { diff --git a/packages/ai/test/computer-tool.integration.test.ts b/packages/ai/test/computer-tool.integration.test.ts index 88b63ad..96b8ca7 100644 --- a/packages/ai/test/computer-tool.integration.test.ts +++ b/packages/ai/test/computer-tool.integration.test.ts @@ -13,6 +13,7 @@ import { getCuaEnvApiKey, getCuaModel, meta, + moonshot, openai, tzafon, xai, @@ -74,6 +75,17 @@ const cases: ProviderCase[] = [ systemPrompt: "Coordinates are normalized from 0 to 1000 relative to the screenshot.", extraOptions: { reasoningEffort: "off" }, }, + { + provider: "moonshotai", + envVar: "MOONSHOT_API_KEY", + modelRef: "moonshotai:kimi-k3", + tools: () => moonshot.computerTools({ actions: ["click"] }), + coordinateRange: [0, 1], + requireToolCalls: true, + systemPrompt: "Coordinates are fractions of the screenshot, normalized from 0 to 1.", + // K3 launched max-thinking-only; leave headroom for reasoning tokens. + extraOptions: { maxTokens: 8192 }, + }, { provider: "tzafon", envVar: "TZAFON_API_KEY", diff --git a/packages/ai/test/models.test.ts b/packages/ai/test/models.test.ts index 0f9e8f0..30865ed 100644 --- a/packages/ai/test/models.test.ts +++ b/packages/ai/test/models.test.ts @@ -2,6 +2,7 @@ import { describe, expect, it } from "vitest"; import { CUA_MODEL_ANNOTATIONS, CUA_PROVIDERS, + type CuaModelRef, cuaOverrideModels, findCuaAnnotation, formatCuaModelRef, @@ -27,7 +28,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, tzafon, yutori)', + 'unsupported CUA provider "bogus" (expected one of: openai, anthropic, google, meta, xai, moonshotai, tzafon, yutori)', ); }); @@ -77,6 +78,25 @@ describe("CUA model refs", () => { ]); }); + it("uses pi-ai's Kimi catalog entry without CUA routing overrides", () => { + expect(cuaOverrideModels("moonshotai")).toEqual([]); + const kimi = getCuaModel("moonshotai:kimi-k3"); + expect(kimi.provider).toBe("moonshotai"); + expect(kimi.api).toBe("openai-completions"); + expect(kimi.baseUrl).toBe("https://api.moonshot.ai/v1"); + expect(kimi.input).toContain("image"); + expect(kimi.contextWindow).toBe(1_048_576); + expect(kimi.maxTokens).toBe(131_072); + // K3 launched with max-only thinking effort; pi-ai clamps the rest away. + expect(kimi.thinkingLevelMap?.max).toBe("max"); + expect(kimi.thinkingLevelMap?.high).toBeNull(); + }); + + it("accepts the moonshot: alias for moonshotai refs", () => { + expect(parseCuaModelRef("moonshot:kimi-k3")).toEqual({ provider: "moonshotai", model: "kimi-k3" }); + expect(getCuaModel("moonshot:kimi-k3" as CuaModelRef).id).toBe("kimi-k3"); + }); + it("loads supported custom provider models without explicit registration", () => { expect(getCuaModel("tzafon:tzafon.northstar-cua-fast").api).toBe("tzafon-responses"); expect(getCuaModel("yutori:n1.5-latest").api).toBe("yutori-chat-completions"); @@ -146,6 +166,9 @@ describe("CUA support annotations", () => { expect(findCuaAnnotation("xai", "grok-4.5")).toBeDefined(); expect(findCuaAnnotation("xai", "grok-4.5-latest")).toBeUndefined(); expect(findCuaAnnotation("xai", "grok-4.3")).toBeUndefined(); + expect(findCuaAnnotation("moonshotai", "kimi-k3")).toBeDefined(); + expect(findCuaAnnotation("moonshotai", "kimi-k2.5")).toBeUndefined(); + expect(findCuaAnnotation("moonshotai", "kimi-latest")).toBeUndefined(); expect(findCuaAnnotation("google", "gemini-3.1-flash-lite")).toBeDefined(); expect(findCuaAnnotation("yutori", "n1.5-latest")).toBeDefined(); expect(findCuaAnnotation("tzafon", "tzafon.northstar-cua-fast")).toBeDefined(); diff --git a/packages/ai/test/provider-module.test.ts b/packages/ai/test/provider-module.test.ts index b92a0f0..d9b3307 100644 --- a/packages/ai/test/provider-module.test.ts +++ b/packages/ai/test/provider-module.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from "vitest"; -import { anthropic, CUA_COMPUTER_ACTION_TYPES, CUA_PROVIDERS, type CuaProvider, gemini, meta, openai, tzafon, xai, yutori } from "../src/index"; +import { anthropic, CUA_COMPUTER_ACTION_TYPES, CUA_PROVIDERS, type CuaProvider, gemini, meta, moonshot, openai, tzafon, xai, yutori } from "../src/index"; import type { CuaProviderModule } from "../src/providers/common"; const MODULES: Record = { @@ -8,6 +8,7 @@ const MODULES: Record = { google: gemini, meta, xai, + moonshotai: moonshot, tzafon, yutori, }; @@ -18,6 +19,7 @@ const NAMESPACES: Record; pref google: { namespace: gemini, prefix: "GEMINI" }, meta: { namespace: meta, prefix: "META" }, xai: { namespace: xai, prefix: "XAI" }, + moonshotai: { namespace: moonshot, prefix: "MOONSHOT" }, tzafon: { namespace: tzafon, prefix: "TZAFON" }, yutori: { namespace: yutori, prefix: "YUTORI" }, }; @@ -59,6 +61,7 @@ describe("provider modules satisfy the uniform contract", () => { it.each([ ["meta", meta.providerModule], ["xai", xai.providerModule], + ["moonshotai", moonshot.providerModule], ])("%s restricts its normalized coordinate contract to computer mode", (_provider, providerModule) => { expect(() => providerModule.toolDefinitions({ mode: "browser" })).toThrow(/computer only/); expect(() => providerModule.toolDefinitions({ mode: "hybrid" })).toThrow(/computer only/); diff --git a/packages/ai/test/providers.test.ts b/packages/ai/test/providers.test.ts index 4afd551..8c261a5 100644 --- a/packages/ai/test/providers.test.ts +++ b/packages/ai/test/providers.test.ts @@ -8,7 +8,7 @@ const YUTORI_CHAT_COMPLETIONS_API = yutori.YUTORI_CHAT_COMPLETIONS_API; describe("createCuaModels", () => { it("registers the CUA-only providers alongside pi's builtins", () => { const models = createCuaModels(); - for (const id of ["openai", "anthropic", "google", "meta", "xai", "tzafon", "yutori"]) { + for (const id of ["openai", "anthropic", "google", "meta", "xai", "moonshotai", "tzafon", "yutori"]) { const provider = models.getProvider(id); expect(provider, id).toBeDefined(); expect(provider?.stream).toBeTypeOf("function"); @@ -19,7 +19,8 @@ describe("createCuaModels", () => { it("lists CUA provider catalogs", () => { const models = createCuaModels(); expect(models.getModel("meta", "muse-spark-1.1")?.api).toBe("meta-responses"); - expect(models.getModel("xai", "grok-4.5")?.api).toBe("openai-completions"); + expect(models.getModel("xai", "grok-4.5")?.api).toBe("openai-responses"); + expect(models.getModel("moonshotai", "kimi-k3")?.api).toBe("openai-completions"); 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/runtime-spec.test.ts b/packages/ai/test/runtime-spec.test.ts index b5c8e75..459ea6a 100644 --- a/packages/ai/test/runtime-spec.test.ts +++ b/packages/ai/test/runtime-spec.test.ts @@ -45,6 +45,16 @@ describe("resolveCuaRuntimeSpec", () => { expect(openaiSpec.onPayload).toBeUndefined(); expect(xaiSpec.onPayload).toBeUndefined(); expect(anthropicSpec.onPayload).toBeTypeOf("function"); + // Kimi streams through pi's plain chat-completions transport, so its + // serial tool-call constraint rides on runtime payload middleware. + const moonshotSpec = resolveCuaRuntimeSpec("moonshotai:kimi-k3"); + expect(moonshotSpec.onPayload).toBeTypeOf("function"); + }); + + it("disables parallel tool calls on Kimi payloads", async () => { + const spec = resolveCuaRuntimeSpec("moonshotai:kimi-k3"); + const payload = await spec.onPayload?.({ model: "kimi-k3", messages: [] }, spec.model); + expect(payload).toMatchObject({ model: "kimi-k3", parallel_tool_calls: false }); }); it("routes concrete Responses models to provider-specific APIs", () => { diff --git a/packages/cli/README.md b/packages/cli/README.md index 4e0d3bd..95d9db1 100644 --- a/packages/cli/README.md +++ b/packages/cli/README.md @@ -47,6 +47,7 @@ cua --print --model anthropic:claude-opus-4-7 "..." cua --print --model google:gemini-3-flash-preview "..." cua --print --model meta:muse-spark-1.1 "..." cua --print --model xai:grok-4.5 "..." +cua --print --model moonshotai:kimi-k3 "..." cua --print --model yutori:n1.5-latest "..." # Named sessions (browser stays alive across calls): @@ -70,7 +71,8 @@ cua --session abc12345 # by id prefix 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`, or `cua models -p yutori`. +`cua models -p meta`, `cua models -p xai`, `cua models -p moonshotai` +(alias: `moonshot`), or `cua models -p yutori`. `-m` / `--model` accepts a provider-qualified `provider:model` ref (e.g. `openai:gpt-5.5`) or a bare model id when it matches exactly one catalog @@ -89,6 +91,7 @@ Configuration is by environment variable. There is no config file. | `GEMINI_API_KEY` | alias of `GOOGLE_API_KEY` | | `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:…`) | | `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 | @@ -97,12 +100,13 @@ Configuration is by environment variable. There is no config file. | `GOOGLE_BASE_URL` | override Google base URL | | `META_BASE_URL` | override Meta Model API base URL | | `XAI_BASE_URL` | override xAI API base URL | +| `MOONSHOTAI_BASE_URL` | override Moonshot AI base URL | | `TZAFON_BASE_URL` | override Tzafon base URL | | `YUTORI_BASE_URL` | override Yutori base URL | | `XDG_DATA_HOME` | sessions dir base (defaults to `~/.local/share`) | | `CUA_IMAGE_PROTOCOL` | force inline image protocol (`kitty`/`iterm2`/`none`/`auto`) | -Use `--thinking ` (`off | minimal | low | medium | high | xhigh`, +Use `--thinking ` (`off | minimal | low | medium | high | xhigh | max`, default `low`) for providers that support reasoning effort. ## Playwright escape hatch diff --git a/packages/cli/package.json b/packages/cli/package.json index 3eeee43..f41846f 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -1,6 +1,6 @@ { "name": "@onkernel/cua-cli", - "version": "0.4.0", + "version": "0.5.0", "description": "Kernel-cloud-browser computer-use TUI built on @onkernel/cua-agent and pi-tui", "license": "MIT", "type": "module", @@ -34,10 +34,10 @@ "typecheck": "tsc -b" }, "dependencies": { - "@earendil-works/pi-coding-agent": "0.80.6", - "@earendil-works/pi-tui": "0.80.6", - "@onkernel/cua-agent": "0.6.0", - "@onkernel/cua-ai": "0.6.0", + "@earendil-works/pi-coding-agent": "0.80.10", + "@earendil-works/pi-tui": "0.80.10", + "@onkernel/cua-agent": "0.7.0", + "@onkernel/cua-ai": "0.7.0", "@onkernel/sdk": "0.49.0" }, "devDependencies": { diff --git a/packages/cli/src/cli-harness.ts b/packages/cli/src/cli-harness.ts index a928352..628cf55 100644 --- a/packages/cli/src/cli-harness.ts +++ b/packages/cli/src/cli-harness.ts @@ -60,7 +60,7 @@ Usage: cua models --json Options: - -p, --provider Filter by provider: openai | anthropic | google | gemini | meta | xai | tzafon | yutori + -p, --provider Filter by provider: openai | anthropic | google | gemini | meta | xai | moonshotai | tzafon | yutori --json Output JSON -h, --help Show this help `; @@ -526,7 +526,7 @@ export function parseNativeTool(raw: string | undefined): CuaNativeToolSpec | un throw new Error(`invalid --native-tool value "${raw}"; expected one of: computer_20260701 | browser_20260701`); } -function mapThinkingLevel(raw: string | undefined): "off" | "minimal" | "low" | "medium" | "high" | "xhigh" { +function mapThinkingLevel(raw: string | undefined): "off" | "minimal" | "low" | "medium" | "high" | "xhigh" | "max" { const v = (raw ?? "low").trim().toLowerCase(); switch (v) { case "off": @@ -540,12 +540,14 @@ function mapThinkingLevel(raw: string | undefined): "off" | "minimal" | "low" | return "high"; case "xhigh": return "xhigh"; + case "max": + return "max"; case "low": case "": return "low"; default: throw new Error( - `invalid --thinking value "${raw}"; expected one of: off | minimal | low | medium | high | xhigh`, + `invalid --thinking value "${raw}"; expected one of: off | minimal | low | medium | high | xhigh | max`, ); } } diff --git a/packages/cli/src/cli.ts b/packages/cli/src/cli.ts index 2da5b82..41cd1b9 100644 --- a/packages/cli/src/cli.ts +++ b/packages/cli/src/cli.ts @@ -55,9 +55,10 @@ Options: google: google:gemini-3-flash-preview meta: meta:muse-spark-1.1 xai: xai:grok-4.5 + moonshot: moonshotai:kimi-k3 tzafon: tzafon:tzafon.northstar-cua-fast yutori: yutori:n1.5-latest - --thinking Thinking level: off | minimal | low | medium | high | xhigh + --thinking Thinking level: off | minimal | low | medium | high | xhigh | max (default: low; applies to providers that support it) --profile Kernel browser profile to load --proxy Kernel proxy to route the browser through @@ -105,6 +106,7 @@ Environment: GEMINI_API_KEY Alias for GOOGLE_API_KEY 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:…) 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 @@ -113,6 +115,7 @@ Environment: GOOGLE_BASE_URL Override Google base URL META_BASE_URL Override Meta Model API base URL XAI_BASE_URL Override xAI API base URL + MOONSHOTAI_BASE_URL Override Moonshot AI base URL TZAFON_BASE_URL Override Tzafon base URL YUTORI_BASE_URL Override Yutori base URL XDG_DATA_HOME Sessions are stored under \$XDG_DATA_HOME/cua/sessions @@ -204,10 +207,10 @@ function parseCliArgs(argv: string[]): CliFlags { const maxSteps = maxStepsRaw ? Number(maxStepsRaw) : undefined; const thinkingRaw = parsed.values.thinking as string | undefined; if (thinkingRaw !== undefined) { - const allowed = new Set(["off", "none", "minimal", "low", "medium", "high", "xhigh"]); + const allowed = new Set(["off", "none", "minimal", "low", "medium", "high", "xhigh", "max"]); if (!allowed.has(thinkingRaw.trim().toLowerCase())) { throw new Error( - `invalid --thinking value "${thinkingRaw}"; expected one of: off | minimal | low | medium | high | xhigh`, + `invalid --thinking value "${thinkingRaw}"; expected one of: off | minimal | low | medium | high | xhigh | max`, ); } } diff --git a/packages/cli/src/harness-models.ts b/packages/cli/src/harness-models.ts index 68d358a..18f1d69 100644 --- a/packages/cli/src/harness-models.ts +++ b/packages/cli/src/harness-models.ts @@ -39,12 +39,13 @@ export function resolveCuaModelRef(input: string | undefined): CuaModelRef { } /** - * List supported models, optionally filtered to a provider. Accepts either - * the canonical `"google"` or the CLI-friendly `"gemini"` alias. + * List supported models, optionally filtered to a provider. Accepts the + * canonical `"google"`/`"moonshotai"` ids or the CLI-friendly `"gemini"`/ + * `"moonshot"` aliases. */ export function listSupportedModels(provider?: string): CuaModelInfo[] { if (!provider) return listCuaModels(); - const normalized = provider === "gemini" ? "google" : provider; + const normalized = provider === "gemini" ? "google" : provider === "moonshot" ? "moonshotai" : provider; if (!isCuaProvider(normalized)) { throw new Error(`unknown provider "${provider}"`); } diff --git a/packages/cli/test/cli-executor.test.ts b/packages/cli/test/cli-executor.test.ts index b47e39d..ef34fd6 100644 --- a/packages/cli/test/cli-executor.test.ts +++ b/packages/cli/test/cli-executor.test.ts @@ -24,6 +24,7 @@ const PROVIDER_ENV_KEYS = [ "GEMINI_API_KEY", "META_API_KEY", "XAI_API_KEY", + "MOONSHOT_API_KEY", "TZAFON_API_KEY", "YUTORI_API_KEY", ]; diff --git a/packages/cli/test/harness-models.test.ts b/packages/cli/test/harness-models.test.ts index eb7c012..a9a9cf8 100644 --- a/packages/cli/test/harness-models.test.ts +++ b/packages/cli/test/harness-models.test.ts @@ -11,12 +11,15 @@ describe("resolveCuaModelRef", () => { expect(resolveCuaModelRef("openai:gpt-5.5")).toBe("openai:gpt-5.5"); expect(resolveCuaModelRef("meta:muse-spark-1.1")).toBe("meta:muse-spark-1.1"); expect(resolveCuaModelRef("xai:grok-4.5")).toBe("xai:grok-4.5"); + expect(resolveCuaModelRef("moonshotai:kimi-k3")).toBe("moonshotai:kimi-k3"); + expect(resolveCuaModelRef("moonshot:kimi-k3")).toBe("moonshotai:kimi-k3"); }); it("accepts bare ids when they match exactly one catalog entry", () => { expect(resolveCuaModelRef("gpt-5.5")).toBe("openai:gpt-5.5"); expect(resolveCuaModelRef("muse-spark-1.1")).toBe("meta:muse-spark-1.1"); expect(resolveCuaModelRef("grok-4.5")).toBe("xai:grok-4.5"); + expect(resolveCuaModelRef("kimi-k3")).toBe("moonshotai:kimi-k3"); }); it("throws on unknown bare ids", () => { @@ -26,6 +29,8 @@ describe("resolveCuaModelRef", () => { it("filters custom provider catalogs", () => { expect(listSupportedModels("meta").map((model) => model.ref)).toEqual(["meta:muse-spark-1.1"]); 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"]); }); it("treats 'gemini' as an alias for google when filtering", () => {