openvibers/openvibe.sdk Commits
0.67.0: bot.machines.chat asks a machine's own model (kind ai) and aiModels is the owner's switch; Machine types with computer.ai and ai_speed (#91)
Changes
@@@ -3,6 +3,17 @@All notable changes to `openvibe-sdk`. The package follows semver; while it is `0.x`, a minorrelease may change an API and says so here.++## 0.67.0 (2026-10-27)++++- **`openvibe-sdk/bot`: a machine's own models** (AI on your own machines; OpenVibe.Node v0.27.0's ai plugin).++ - `bot.machines.chat(id, { model, messages, max_tokens?, temperature? })` asks one of them for one reply (bot.command@1++ kind `ai`) and answers `{ model, message, usage?, ms? }`. A machine that does not answer throws an `OpenVibeError`++ with code `bot.ai_nack`.++ - `bot.machines.aiModels(id, on)` is the owner's "Let my AI use this computer's models".++- Types: `Machine` for `machines.list()`, with `computer.ai` (the models and whether a server answers) and++ `computer.ai_speed` (how fast the last model answered there, Node v0.29.0); `MachineChatInput` and `MachineChatResult`;++ `MachineCommandInput.kind` takes `ai`.++## 0.66.0 (2026-10-27)- **`openvibe-sdk/bot`: machines and desks** (the Desks plan; the roadmap's SDK section). `bot.machines.list()`,@@@ -2,14 +2,14 @@> Supported browser and server clients for the OpenVibe platform.--**Status:** alpha, v0.66.0 (roadmap Wave 2; developer apps from Wave 20; Media objects v2 from Wave 4; the Tools platform API; Search, Chat and AI clients from WS-F task 4; per-actor limits from WS-R task 4). Every release is tagged (see [CHANGELOG.md](CHANGELOG.md)). Tested against local stub servers and the built-in mock platform only, never against the live platform. Services pin release tags; each repository's `STATUS.json` names the one it uses.++**Status:** alpha, v0.67.0 (roadmap Wave 2; developer apps from Wave 20; Media objects v2 from Wave 4; the Tools platform API; Search, Chat and AI clients from WS-F task 4; per-actor limits from WS-R task 4). Every release is tagged (see [CHANGELOG.md](CHANGELOG.md)). Tested against local stub servers and the built-in mock platform only, never against the live platform. Services pin release tags; each repository's `STATUS.json` names the one it uses.**Plan:** OpenVibe End-to-End Realignment & Implementation Plan, revision 3 (20 Sep 2026), §3.2; roadmap §30.**License:** MIT ([LICENSE](LICENSE)). This package is a library that apps outside the network embed, so it uses MIT. The OpenVibe services themselves stay AGPL-3.0.**If a capability is not in the SDK, it is not public.** Apps call services through `openvibe-sdk` and never build internal routes themselves. A route with no SDK wrapper is internal, even when you can reach it, and it can change without notice. To make a capability public, first define it in OpenVibe.Contracts, then wrap it here.```bash--npm install https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.66.0++npm install https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.67.0```It has no runtime dependencies. It needs Node ≥ 20, or any browser with `fetch`, Web Crypto and `TextDecoder`. There is no build step. The package is CommonJS with ESM entry points (`import` works). Each subpath has its own `.d.ts`. For a page with no bundler, `browser/openvibe-sdk.mjs` is one self-contained ES module of the browser-safe subpaths (see [Browser without a bundler](#browser-without-a-bundler)). It does not depend on `openvibe-contracts` at runtime: it copies the contract types it uses (from Contracts v0.86.0, a devDependency the tests check them against).@@@ -3,7 +3,7 @@"stage": "alpha","code": true,"wave": "W2",-- "version": "0.66.0",++ "version": "0.67.0","plan": "OpenVibe End-to-End Realignment & Implementation Plan, revision 3 (20 Sep 2026), §3.2; roadmap §30","deployed": false,"deployment": "none: a package released as git tags; services and apps pin a tag",@@@ -1,4 +1,4 @@--// Generated by scripts/browser-bundle.js from browser.js (openvibe-sdk 0.66.0, MIT). Do not edit.++// Generated by scripts/browser-bundle.js from browser.js (openvibe-sdk 0.67.0, MIT). Do not edit.// One self-contained ES module: core at the top level plus the browser-safe subpaths as namespaces// (auth, registry, modules, realtime, media, community, bot, search, themes, chat, ai, jobs, tools, projects, geo). No server-only code: no service tokens, JWT verification, events, identity./* eslint-disable */@@@ -43,7 +43,7 @@ const { paginate, offsetPager } = __load(7);const semver = __load(6);/** The package version (test/version.test.js keeps it equal to package.json). */--const SDK_VERSION = '0.66.0';++const SDK_VERSION = '0.67.0';module.exports = {createClient,@@@ -1780,6 +1780,11 @@ function (module, exports) {* input and exec on it. A desk command that the machine does not do throws an OpenVibeError with code `bot.desk_nack`* (the machine's own fault code is `fault`), so a desk reads as plain awaits.*++ * A machine's own models (AI on your own machines, contracts 0.219.0, OpenVibe.Node v0.27.0's ai plugin):++ * `machines.chat(id, { model, messages, max_tokens?, temperature? })` asks one of them for one reply (kind ai) and answers++ * `{ model, message: { role, content }, usage?, ms? }`; a machine that does not answer throws `bot.ai_nack`.++ * `machines.aiModels(id, on)` is the owner's "Let my AI use this computer's models".++ ** Not wrapped: the operator WebSocket (/control — Realtime frames bot.command@1), device* bootstrap (POST /devices/bind, a Network node token) and the internal Run -> Bot job dispatch* (bot.job.dispatch is service-to-service and is never delegated to people or apps). The signed@@@ -1883,6 +1888,21 @@ function createBotClient(client, defaults = {}) {command: (id, input, o = {}) => call({ method: 'POST', path: `/api/v1/machines/${enc(id)}/commands`, json: input }, o),/** { desk, person_driving, last_person_at } — whether a person is using that desk now (the person always wins). */deskTurn: (id, desk, o = {}) => call({ path: `/api/v1/machines/${enc(id)}/desk-turn`, query: { desk } }, o),++ /**++ * One reply from a model on the machine (kind ai): { model, messages, max_tokens?, temperature? } ->++ * { model, message: { role, content }, usage?, ms? }. Its models are machines.list()'s computer.ai.models, and++ * how fast they answer there computer.ai_speed. A machine that does not answer throws (code bot.ai_nack).++ */++ async chat(id, { model, messages, max_tokens, temperature } = {}, o = {}) {++ if (typeof model !== 'string' || !model) throw new TypeError('model is one of the machine\'s models (computer.ai.models)');++ if (!Array.isArray(messages) || !messages.length) throw new TypeError('messages are [{ role, content }, …]');++ const value = { model, messages, ...(Number.isInteger(max_tokens) ? { max_tokens } : {}), ...(typeof temperature === 'number' ? { temperature } : {}) };++ const r = await machines.command(id, { kind: 'ai', value, ms: o.ms || 120000 }, o);++ if (r && r.result === 'ack') return r.data === undefined ? {} : r.data;++ throw new OpenVibeError({ code: 'bot.ai_nack', status: 409, message: `the machine's model did not answer: ${(r && (r.reason || r.result)) || 'no answer'}`, detail: r && r.reason });++ },++ /** { machine } — the owner's "Let my AI use this computer's models" on or off (OpenVibe.AI, for them only). */++ aiModels: (id, on, o = {}) => call({ method: 'POST', path: `/api/v1/machines/${enc(id)}/ai-models`, json: { on: on === true } }, o),/*** A desk of your own on a machine (the Desks plan): a screen or a browser alone, its own files and sign-ins.* const desk = bot.machines.desk(machineId, { kind: 'browser', profile: 'agent' });@@@ -1,12 +1,12 @@{"name": "openvibe-sdk",-- "version": "0.66.0",++ "version": "0.67.0","lockfileVersion": 3,"requires": true,"packages": {"": {"name": "openvibe-sdk",-- "version": "0.66.0",++ "version": "0.67.0","license": "MIT","bin": {"openvibe-download": "bin/openvibe-download.js"@@@ -1,6 +1,6 @@{"name": "openvibe-sdk",-- "version": "0.66.0",++ "version": "0.67.0","description": "Supported browser and server clients for the OpenVibe platform: auth (incl. developer apps), registry discovery, identity, user modules, events, realtime, media, community, Bot robots, search, chat, AI runs, Tools (registry, run API and jobs), VIP memberships and developer projects, the async PostgreSQL data layer (db) and the Valkey cache, limits store, queue and pub/sub, with deadlines, safe retries, problem errors and trace propagation.","main": "index.js","types": "types/index.d.ts",@@@ -21,6 +21,11 @@* input and exec on it. A desk command that the machine does not do throws an OpenVibeError with code `bot.desk_nack`* (the machine's own fault code is `fault`), so a desk reads as plain awaits.*++ * A machine's own models (AI on your own machines, contracts 0.219.0, OpenVibe.Node v0.27.0's ai plugin):++ * `machines.chat(id, { model, messages, max_tokens?, temperature? })` asks one of them for one reply (kind ai) and answers++ * `{ model, message: { role, content }, usage?, ms? }`; a machine that does not answer throws `bot.ai_nack`.++ * `machines.aiModels(id, on)` is the owner's "Let my AI use this computer's models".++ ** Not wrapped: the operator WebSocket (/control — Realtime frames bot.command@1), device* bootstrap (POST /devices/bind, a Network node token) and the internal Run -> Bot job dispatch* (bot.job.dispatch is service-to-service and is never delegated to people or apps). The signed@@@ -124,6 +129,21 @@ function createBotClient(client, defaults = {}) {command: (id, input, o = {}) => call({ method: 'POST', path: `/api/v1/machines/${enc(id)}/commands`, json: input }, o),/** { desk, person_driving, last_person_at } — whether a person is using that desk now (the person always wins). */deskTurn: (id, desk, o = {}) => call({ path: `/api/v1/machines/${enc(id)}/desk-turn`, query: { desk } }, o),++ /**++ * One reply from a model on the machine (kind ai): { model, messages, max_tokens?, temperature? } ->++ * { model, message: { role, content }, usage?, ms? }. Its models are machines.list()'s computer.ai.models, and++ * how fast they answer there computer.ai_speed. A machine that does not answer throws (code bot.ai_nack).++ */++ async chat(id, { model, messages, max_tokens, temperature } = {}, o = {}) {++ if (typeof model !== 'string' || !model) throw new TypeError('model is one of the machine\'s models (computer.ai.models)');++ if (!Array.isArray(messages) || !messages.length) throw new TypeError('messages are [{ role, content }, …]');++ const value = { model, messages, ...(Number.isInteger(max_tokens) ? { max_tokens } : {}), ...(typeof temperature === 'number' ? { temperature } : {}) };++ const r = await machines.command(id, { kind: 'ai', value, ms: o.ms || 120000 }, o);++ if (r && r.result === 'ack') return r.data === undefined ? {} : r.data;++ throw new OpenVibeError({ code: 'bot.ai_nack', status: 409, message: `the machine's model did not answer: ${(r && (r.reason || r.result)) || 'no answer'}`, detail: r && r.reason });++ },++ /** { machine } — the owner's "Let my AI use this computer's models" on or off (OpenVibe.AI, for them only). */++ aiModels: (id, on, o = {}) => call({ method: 'POST', path: `/api/v1/machines/${enc(id)}/ai-models`, json: { on: on === true } }, o),/*** A desk of your own on a machine (the Desks plan): a screen or a browser alone, its own files and sign-ins.* const desk = bot.machines.desk(machineId, { kind: 'browser', profile: 'agent' });@@@ -8,7 +8,7 @@ const { paginate, offsetPager } = require('./paginate');const semver = require('./semver');/** The package version (test/version.test.js keeps it equal to package.json). */--const SDK_VERSION = '0.66.0';++const SDK_VERSION = '0.67.0';module.exports = {createClient,@@@ -67,12 +67,15 @@ function server() {if (m === 'GET' && p === '/api/v1/kits/starter') return send(res, 200, { kit: { id: 'starter' } });if (m === 'GET' && p === '/api/v1/machines') return send(res, 200, { machines: [{ id: DEV, name: 'desk-host', online: true, computer: { desk: { named: true, kinds: ['browser'] } } }] });if (m === 'GET' && p === `/api/v1/machines/${DEV}/computer`) return send(res, 200, { machine: { id: DEV } });++ if (m === 'POST' && p === `/api/v1/machines/${DEV}/ai-models`) return send(res, 200, { machine: { id: DEV, ai_models: JSON.parse(body.toString() || '{}').on === true } });if (m === 'GET' && p === `/api/v1/machines/${DEV}/desk-turn`) return send(res, 200, { desk: u.searchParams.get('desk'), person_driving: false, last_person_at: null });if (m === 'POST' && p === `/api/v1/machines/${DEV}/commands`) {const input = JSON.parse(body.toString() || '{}');seenCommands.push(input);if (input.kind === 'browser' && input.value.action === 'click') return send(res, 200, { machine_id: DEV, result: 'nack', reason: 'nothing on the page matches #missing' });if (input.kind === 'desk') return send(res, 200, { machine_id: DEV, result: 'ack', data: { running: input.value.action === 'start', desk: input.value.desk, kind: input.value.kind || 'desktop' } });++ if (input.kind === 'ai' && input.value.model === 'busy') return send(res, 200, { machine_id: DEV, result: 'nack', reason: 'the model is answering another request on this machine' });++ if (input.kind === 'ai') return send(res, 200, { machine_id: DEV, result: 'ack', data: { model: input.value.model, message: { role: 'assistant', content: 'A lock one thread holds.' }, usage: { completion_tokens: 6 }, ms: 400 } });return send(res, 200, { machine_id: DEV, result: 'ack', data: { url: 'https://example.org/', title: 'Example' } });}if (p.startsWith('/api/v1/machines/')) return problem(res, 404, 'bot.device_not_found', 'no such machine of yours');@@@ -107,6 +110,19 @@ run([assert.throws(() => bot.machines.desk(DEV, { name: 'private' }), TypeError, 'only a named desk of your own');} finally { await srv.close(); }}],++ ['a machine\'s own models: one reply (kind ai), a nack thrown, the owner\'s switch', async () => {++ const srv = await server();++ try {++ const bot = createBotClient(createClient({ baseUrls: { bot: srv.url }, token: 'svc' }), { actingSubject: USR });++ const out = await bot.machines.chat(DEV, { model: 'llama3.1:8b', messages: [{ role: 'user', content: 'What is a mutex?' }], max_tokens: 80 });++ assert.deepStrictEqual(out, { model: 'llama3.1:8b', message: { role: 'assistant', content: 'A lock one thread holds.' }, usage: { completion_tokens: 6 }, ms: 400 });++ assert.deepStrictEqual(seenCommands.at(-1), { kind: 'ai', value: { model: 'llama3.1:8b', messages: [{ role: 'user', content: 'What is a mutex?' }], max_tokens: 80 }, ms: 120000 });++ await assert.rejects(() => bot.machines.chat(DEV, { model: 'busy', messages: [{ role: 'user', content: 'x' }] }), (e) => e.code === 'bot.ai_nack' && /another request/.test(e.message));++ await assert.rejects(() => bot.machines.chat(DEV, { model: '', messages: [] }), TypeError);++ assert.strictEqual((await bot.machines.aiModels(DEV, true)).machine.ai_models, true);++ assert.strictEqual((await bot.machines.aiModels(DEV, false)).machine.ai_models, false);++ } finally { await srv.close(); }++ }],['a service acting for a person sends X-OV-Subject, its token and an Idempotency-Key', async () => {const srv = await server();const bot = createBotClient(createClient({ baseUrls: { bot: srv.url }, token: 'svc-token' }), { actingSubject: USR });@@@ -81,7 +81,18 @@ export interface PairInput {capabilities?: Record<string, unknown>;name?: string;}--export interface MachineCommandInput { id?: string; kind: 'exec' | 'fs' | 'screen' | 'input' | 'browser' | 'probe' | 'desk'; value: Record<string, unknown>; ms?: number; }++export interface MachineCommandInput { id?: string; kind: 'exec' | 'fs' | 'screen' | 'input' | 'browser' | 'probe' | 'desk' | 'ai'; value: Record<string, unknown>; ms?: number; }++/** A machine's own models (OpenVibe.Node's ai plugin): what it offers and how fast its last model answered there. */++export interface MachineAi { models: string[]; ready: boolean; max_tokens?: number; }++export interface MachineAiSpeed { model: string; tokens_per_s: number; }++export interface Machine {++ id: string; name: string | null; online: boolean;++ agent_computer?: boolean; ai_models?: boolean; watch_probe?: boolean;++ computer: ({ ai?: MachineAi | null; ai_speed?: MachineAiSpeed | null; desks_running?: number | null; [field: string]: unknown }) | null;++ [field: string]: unknown;++}++export interface MachineChatInput { model: string; messages: Array<{ role: 'system' | 'user' | 'assistant'; content: string }>; max_tokens?: number; temperature?: number; }++export interface MachineChatResult { model: string; message: { role: 'assistant'; content: string }; usage?: { prompt_tokens?: number; completion_tokens?: number; total_tokens?: number }; finish_reason?: string; ms?: number; }export interface MachineCommandResult { machine_id?: string; id?: string; result: 'ack' | 'nack' | 'expired' | 'refused' | string; data?: Record<string, unknown>; code?: string; reason?: string | null; [field: string]: unknown; }export interface DeskOptions { name?: string; kind?: 'browser' | 'desktop'; profile?: string; width?: number; height?: number; }export interface Desk {@@@ -97,11 +108,13 @@ export interface Desk {turn(): Promise<{ desk: string; person_driving: boolean; last_person_at: string | null }>;}export interface MachinesApi {-- list(opts?: CallOptions): Promise<{ machines: Array<Record<string, unknown>> }>;++ list(opts?: CallOptions): Promise<{ machines: Machine[] }>;computer(id: string, opts?: CallOptions): Promise<{ machine: Record<string, unknown> } | null>;command(id: string, input: MachineCommandInput, opts?: CallOptions): Promise<MachineCommandResult>;deskTurn(id: string, desk: string, opts?: CallOptions): Promise<{ desk: string; person_driving: boolean; last_person_at: string | null }>;desk(id: string, options?: DeskOptions, opts?: CallOptions): Desk;++ chat(id: string, input: MachineChatInput, opts?: CallOptions & { ms?: number }): Promise<MachineChatResult>;++ aiModels(id: string, on: boolean, opts?: CallOptions): Promise<{ machine: Machine }>;}export interface BotClient {robots: RobotsApi;
@@ -3,6 +3,17 @@
@@ -3,6 +3,17 @@
All notable changes to `openvibe-sdk`. The package follows semver; while it is `0.x`, a minor
All notable changes to `openvibe-sdk`. The package follows semver; while it is `0.x`, a minor
release may change an API and says so here.
release may change an API and says so here.
+## 0.67.0 (2026-10-27)
+
+- **`openvibe-sdk/bot`: a machine's own models** (AI on your own machines; OpenVibe.Node v0.27.0's ai plugin).
+ - `bot.machines.chat(id, { model, messages, max_tokens?, temperature? })` asks one of them for one reply (bot.command@1
+ kind `ai`) and answers `{ model, message, usage?, ms? }`. A machine that does not answer throws an `OpenVibeError`
+ with code `bot.ai_nack`.
+ - `bot.machines.aiModels(id, on)` is the owner's "Let my AI use this computer's models".
+- Types: `Machine` for `machines.list()`, with `computer.ai` (the models and whether a server answers) and
+ `computer.ai_speed` (how fast the last model answered there, Node v0.29.0); `MachineChatInput` and `MachineChatResult`;
+ `MachineCommandInput.kind` takes `ai`.
+
## 0.66.0 (2026-10-27)
## 0.66.0 (2026-10-27)
- **`openvibe-sdk/bot`: machines and desks** (the Desks plan; the roadmap's SDK section). `bot.machines.list()`,
- **`openvibe-sdk/bot`: machines and desks** (the Desks plan; the roadmap's SDK section). `bot.machines.list()`,
@@ -2,14 +2,14 @@
@@ -2,14 +2,14 @@
> Supported browser and server clients for the OpenVibe platform.
> Supported browser and server clients for the OpenVibe platform.
-**Status:** alpha, v0.66.0 (roadmap Wave 2; developer apps from Wave 20; Media objects v2 from Wave 4; the Tools platform API; Search, Chat and AI clients from WS-F task 4; per-actor limits from WS-R task 4). Every release is tagged (see [CHANGELOG.md](CHANGELOG.md)). Tested against local stub servers and the built-in mock platform only, never against the live platform. Services pin release tags; each repository's `STATUS.json` names the one it uses.
+**Status:** alpha, v0.67.0 (roadmap Wave 2; developer apps from Wave 20; Media objects v2 from Wave 4; the Tools platform API; Search, Chat and AI clients from WS-F task 4; per-actor limits from WS-R task 4). Every release is tagged (see [CHANGELOG.md](CHANGELOG.md)). Tested against local stub servers and the built-in mock platform only, never against the live platform. Services pin release tags; each repository's `STATUS.json` names the one it uses.
**Plan:** OpenVibe End-to-End Realignment & Implementation Plan, revision 3 (20 Sep 2026), §3.2; roadmap §30.
**Plan:** OpenVibe End-to-End Realignment & Implementation Plan, revision 3 (20 Sep 2026), §3.2; roadmap §30.
**License:** MIT ([LICENSE](LICENSE)). This package is a library that apps outside the network embed, so it uses MIT. The OpenVibe services themselves stay AGPL-3.0.
**License:** MIT ([LICENSE](LICENSE)). This package is a library that apps outside the network embed, so it uses MIT. The OpenVibe services themselves stay AGPL-3.0.
**If a capability is not in the SDK, it is not public.** Apps call services through `openvibe-sdk` and never build internal routes themselves. A route with no SDK wrapper is internal, even when you can reach it, and it can change without notice. To make a capability public, first define it in OpenVibe.Contracts, then wrap it here.
**If a capability is not in the SDK, it is not public.** Apps call services through `openvibe-sdk` and never build internal routes themselves. A route with no SDK wrapper is internal, even when you can reach it, and it can change without notice. To make a capability public, first define it in OpenVibe.Contracts, then wrap it here.
```bash
```bash
-npm install https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.66.0
+npm install https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.67.0
```
```
It has no runtime dependencies. It needs Node ≥ 20, or any browser with `fetch`, Web Crypto and `TextDecoder`. There is no build step. The package is CommonJS with ESM entry points (`import` works). Each subpath has its own `.d.ts`. For a page with no bundler, `browser/openvibe-sdk.mjs` is one self-contained ES module of the browser-safe subpaths (see [Browser without a bundler](#browser-without-a-bundler)). It does not depend on `openvibe-contracts` at runtime: it copies the contract types it uses (from Contracts v0.86.0, a devDependency the tests check them against).
It has no runtime dependencies. It needs Node ≥ 20, or any browser with `fetch`, Web Crypto and `TextDecoder`. There is no build step. The package is CommonJS with ESM entry points (`import` works). Each subpath has its own `.d.ts`. For a page with no bundler, `browser/openvibe-sdk.mjs` is one self-contained ES module of the browser-safe subpaths (see [Browser without a bundler](#browser-without-a-bundler)). It does not depend on `openvibe-contracts` at runtime: it copies the contract types it uses (from Contracts v0.86.0, a devDependency the tests check them against).
@@ -3,7 +3,7 @@
@@ -3,7 +3,7 @@
"stage": "alpha",
"stage": "alpha",
"code": true,
"code": true,
"wave": "W2",
"wave": "W2",
- "version": "0.66.0",
+ "version": "0.67.0",
"plan": "OpenVibe End-to-End Realignment & Implementation Plan, revision 3 (20 Sep 2026), §3.2; roadmap §30",
"plan": "OpenVibe End-to-End Realignment & Implementation Plan, revision 3 (20 Sep 2026), §3.2; roadmap §30",
"deployed": false,
"deployed": false,
"deployment": "none: a package released as git tags; services and apps pin a tag",
"deployment": "none: a package released as git tags; services and apps pin a tag",
@@ -1,4 +1,4 @@
@@ -1,4 +1,4 @@
-// Generated by scripts/browser-bundle.js from browser.js (openvibe-sdk 0.66.0, MIT). Do not edit.
+// Generated by scripts/browser-bundle.js from browser.js (openvibe-sdk 0.67.0, MIT). Do not edit.
// One self-contained ES module: core at the top level plus the browser-safe subpaths as namespaces
// One self-contained ES module: core at the top level plus the browser-safe subpaths as namespaces
// (auth, registry, modules, realtime, media, community, bot, search, themes, chat, ai, jobs, tools, projects, geo). No server-only code: no service tokens, JWT verification, events, identity.
// (auth, registry, modules, realtime, media, community, bot, search, themes, chat, ai, jobs, tools, projects, geo). No server-only code: no service tokens, JWT verification, events, identity.
/* eslint-disable */
/* eslint-disable */
@@ -43,7 +43,7 @@ const { paginate, offsetPager } = __load(7);
@@ -43,7 +43,7 @@ const { paginate, offsetPager } = __load(7);
const semver = __load(6);
const semver = __load(6);
/** The package version (test/version.test.js keeps it equal to package.json). */
/** The package version (test/version.test.js keeps it equal to package.json). */
-const SDK_VERSION = '0.66.0';
+const SDK_VERSION = '0.67.0';
module.exports = {
module.exports = {
createClient,
createClient,
@@ -1780,6 +1780,11 @@ function (module, exports) {
@@ -1780,6 +1780,11 @@ function (module, exports) {
* input and exec on it. A desk command that the machine does not do throws an OpenVibeError with code `bot.desk_nack`
* input and exec on it. A desk command that the machine does not do throws an OpenVibeError with code `bot.desk_nack`
* (the machine's own fault code is `fault`), so a desk reads as plain awaits.
* (the machine's own fault code is `fault`), so a desk reads as plain awaits.
*
*
+ * A machine's own models (AI on your own machines, contracts 0.219.0, OpenVibe.Node v0.27.0's ai plugin):
+ * `machines.chat(id, { model, messages, max_tokens?, temperature? })` asks one of them for one reply (kind ai) and answers
+ * `{ model, message: { role, content }, usage?, ms? }`; a machine that does not answer throws `bot.ai_nack`.
+ * `machines.aiModels(id, on)` is the owner's "Let my AI use this computer's models".
+ *
* Not wrapped: the operator WebSocket (/control — Realtime frames bot.command@1), device
* Not wrapped: the operator WebSocket (/control — Realtime frames bot.command@1), device
* bootstrap (POST /devices/bind, a Network node token) and the internal Run -> Bot job dispatch
* bootstrap (POST /devices/bind, a Network node token) and the internal Run -> Bot job dispatch
* (bot.job.dispatch is service-to-service and is never delegated to people or apps). The signed
* (bot.job.dispatch is service-to-service and is never delegated to people or apps). The signed
@@ -1883,6 +1888,21 @@ function createBotClient(client, defaults = {}) {
@@ -1883,6 +1888,21 @@ function createBotClient(client, defaults = {}) {
command: (id, input, o = {}) => call({ method: 'POST', path: `/api/v1/machines/${enc(id)}/commands`, json: input }, o),
command: (id, input, o = {}) => call({ method: 'POST', path: `/api/v1/machines/${enc(id)}/commands`, json: input }, o),
/** { desk, person_driving, last_person_at } — whether a person is using that desk now (the person always wins). */
/** { desk, person_driving, last_person_at } — whether a person is using that desk now (the person always wins). */
deskTurn: (id, desk, o = {}) => call({ path: `/api/v1/machines/${enc(id)}/desk-turn`, query: { desk } }, o),
deskTurn: (id, desk, o = {}) => call({ path: `/api/v1/machines/${enc(id)}/desk-turn`, query: { desk } }, o),
+ /**
+ * One reply from a model on the machine (kind ai): { model, messages, max_tokens?, temperature? } ->
+ * { model, message: { role, content }, usage?, ms? }. Its models are machines.list()'s computer.ai.models, and
+ * how fast they answer there computer.ai_speed. A machine that does not answer throws (code bot.ai_nack).
+ */
+ async chat(id, { model, messages, max_tokens, temperature } = {}, o = {}) {
+ if (typeof model !== 'string' || !model) throw new TypeError('model is one of the machine\'s models (computer.ai.models)');
+ if (!Array.isArray(messages) || !messages.length) throw new TypeError('messages are [{ role, content }, …]');
+ const value = { model, messages, ...(Number.isInteger(max_tokens) ? { max_tokens } : {}), ...(typeof temperature === 'number' ? { temperature } : {}) };
+ const r = await machines.command(id, { kind: 'ai', value, ms: o.ms || 120000 }, o);
+ if (r && r.result === 'ack') return r.data === undefined ? {} : r.data;
+ throw new OpenVibeError({ code: 'bot.ai_nack', status: 409, message: `the machine's model did not answer: ${(r && (r.reason || r.result)) || 'no answer'}`, detail: r && r.reason });
+ },
+ /** { machine } — the owner's "Let my AI use this computer's models" on or off (OpenVibe.AI, for them only). */
+ aiModels: (id, on, o = {}) => call({ method: 'POST', path: `/api/v1/machines/${enc(id)}/ai-models`, json: { on: on === true } }, o),
/**
/**
* A desk of your own on a machine (the Desks plan): a screen or a browser alone, its own files and sign-ins.
* A desk of your own on a machine (the Desks plan): a screen or a browser alone, its own files and sign-ins.
* const desk = bot.machines.desk(machineId, { kind: 'browser', profile: 'agent' });
* const desk = bot.machines.desk(machineId, { kind: 'browser', profile: 'agent' });
@@ -1,12 +1,12 @@
@@ -1,12 +1,12 @@
{
{
"name": "openvibe-sdk",
"name": "openvibe-sdk",
- "version": "0.66.0",
+ "version": "0.67.0",
"lockfileVersion": 3,
"lockfileVersion": 3,
"requires": true,
"requires": true,
"packages": {
"packages": {
"": {
"": {
"name": "openvibe-sdk",
"name": "openvibe-sdk",
- "version": "0.66.0",
+ "version": "0.67.0",
"license": "MIT",
"license": "MIT",
"bin": {
"bin": {
"openvibe-download": "bin/openvibe-download.js"
"openvibe-download": "bin/openvibe-download.js"
@@ -1,6 +1,6 @@
@@ -1,6 +1,6 @@
{
{
"name": "openvibe-sdk",
"name": "openvibe-sdk",
- "version": "0.66.0",
+ "version": "0.67.0",
"description": "Supported browser and server clients for the OpenVibe platform: auth (incl. developer apps), registry discovery, identity, user modules, events, realtime, media, community, Bot robots, search, chat, AI runs, Tools (registry, run API and jobs), VIP memberships and developer projects, the async PostgreSQL data layer (db) and the Valkey cache, limits store, queue and pub/sub, with deadlines, safe retries, problem errors and trace propagation.",
"description": "Supported browser and server clients for the OpenVibe platform: auth (incl. developer apps), registry discovery, identity, user modules, events, realtime, media, community, Bot robots, search, chat, AI runs, Tools (registry, run API and jobs), VIP memberships and developer projects, the async PostgreSQL data layer (db) and the Valkey cache, limits store, queue and pub/sub, with deadlines, safe retries, problem errors and trace propagation.",
"main": "index.js",
"main": "index.js",
"types": "types/index.d.ts",
"types": "types/index.d.ts",
@@ -21,6 +21,11 @@
@@ -21,6 +21,11 @@
* input and exec on it. A desk command that the machine does not do throws an OpenVibeError with code `bot.desk_nack`
* input and exec on it. A desk command that the machine does not do throws an OpenVibeError with code `bot.desk_nack`
* (the machine's own fault code is `fault`), so a desk reads as plain awaits.
* (the machine's own fault code is `fault`), so a desk reads as plain awaits.
*
*
+ * A machine's own models (AI on your own machines, contracts 0.219.0, OpenVibe.Node v0.27.0's ai plugin):
+ * `machines.chat(id, { model, messages, max_tokens?, temperature? })` asks one of them for one reply (kind ai) and answers
+ * `{ model, message: { role, content }, usage?, ms? }`; a machine that does not answer throws `bot.ai_nack`.
+ * `machines.aiModels(id, on)` is the owner's "Let my AI use this computer's models".
+ *
* Not wrapped: the operator WebSocket (/control — Realtime frames bot.command@1), device
* Not wrapped: the operator WebSocket (/control — Realtime frames bot.command@1), device
* bootstrap (POST /devices/bind, a Network node token) and the internal Run -> Bot job dispatch
* bootstrap (POST /devices/bind, a Network node token) and the internal Run -> Bot job dispatch
* (bot.job.dispatch is service-to-service and is never delegated to people or apps). The signed
* (bot.job.dispatch is service-to-service and is never delegated to people or apps). The signed
@@ -124,6 +129,21 @@ function createBotClient(client, defaults = {}) {
@@ -124,6 +129,21 @@ function createBotClient(client, defaults = {}) {
command: (id, input, o = {}) => call({ method: 'POST', path: `/api/v1/machines/${enc(id)}/commands`, json: input }, o),
command: (id, input, o = {}) => call({ method: 'POST', path: `/api/v1/machines/${enc(id)}/commands`, json: input }, o),
/** { desk, person_driving, last_person_at } — whether a person is using that desk now (the person always wins). */
/** { desk, person_driving, last_person_at } — whether a person is using that desk now (the person always wins). */
deskTurn: (id, desk, o = {}) => call({ path: `/api/v1/machines/${enc(id)}/desk-turn`, query: { desk } }, o),
deskTurn: (id, desk, o = {}) => call({ path: `/api/v1/machines/${enc(id)}/desk-turn`, query: { desk } }, o),
+ /**
+ * One reply from a model on the machine (kind ai): { model, messages, max_tokens?, temperature? } ->
+ * { model, message: { role, content }, usage?, ms? }. Its models are machines.list()'s computer.ai.models, and
+ * how fast they answer there computer.ai_speed. A machine that does not answer throws (code bot.ai_nack).
+ */
+ async chat(id, { model, messages, max_tokens, temperature } = {}, o = {}) {
+ if (typeof model !== 'string' || !model) throw new TypeError('model is one of the machine\'s models (computer.ai.models)');
+ if (!Array.isArray(messages) || !messages.length) throw new TypeError('messages are [{ role, content }, …]');
+ const value = { model, messages, ...(Number.isInteger(max_tokens) ? { max_tokens } : {}), ...(typeof temperature === 'number' ? { temperature } : {}) };
+ const r = await machines.command(id, { kind: 'ai', value, ms: o.ms || 120000 }, o);
+ if (r && r.result === 'ack') return r.data === undefined ? {} : r.data;
+ throw new OpenVibeError({ code: 'bot.ai_nack', status: 409, message: `the machine's model did not answer: ${(r && (r.reason || r.result)) || 'no answer'}`, detail: r && r.reason });
+ },
+ /** { machine } — the owner's "Let my AI use this computer's models" on or off (OpenVibe.AI, for them only). */
+ aiModels: (id, on, o = {}) => call({ method: 'POST', path: `/api/v1/machines/${enc(id)}/ai-models`, json: { on: on === true } }, o),
/**
/**
* A desk of your own on a machine (the Desks plan): a screen or a browser alone, its own files and sign-ins.
* A desk of your own on a machine (the Desks plan): a screen or a browser alone, its own files and sign-ins.
* const desk = bot.machines.desk(machineId, { kind: 'browser', profile: 'agent' });
* const desk = bot.machines.desk(machineId, { kind: 'browser', profile: 'agent' });
@@ -8,7 +8,7 @@ const { paginate, offsetPager } = require('./paginate');
@@ -8,7 +8,7 @@ const { paginate, offsetPager } = require('./paginate');
const semver = require('./semver');
const semver = require('./semver');
/** The package version (test/version.test.js keeps it equal to package.json). */
/** The package version (test/version.test.js keeps it equal to package.json). */
-const SDK_VERSION = '0.66.0';
+const SDK_VERSION = '0.67.0';
module.exports = {
module.exports = {
createClient,
createClient,
@@ -67,12 +67,15 @@ function server() {
@@ -67,12 +67,15 @@ function server() {
if (m === 'GET' && p === '/api/v1/kits/starter') return send(res, 200, { kit: { id: 'starter' } });
if (m === 'GET' && p === '/api/v1/kits/starter') return send(res, 200, { kit: { id: 'starter' } });
if (m === 'GET' && p === '/api/v1/machines') return send(res, 200, { machines: [{ id: DEV, name: 'desk-host', online: true, computer: { desk: { named: true, kinds: ['browser'] } } }] });
if (m === 'GET' && p === '/api/v1/machines') return send(res, 200, { machines: [{ id: DEV, name: 'desk-host', online: true, computer: { desk: { named: true, kinds: ['browser'] } } }] });
if (m === 'GET' && p === `/api/v1/machines/${DEV}/computer`) return send(res, 200, { machine: { id: DEV } });
if (m === 'GET' && p === `/api/v1/machines/${DEV}/computer`) return send(res, 200, { machine: { id: DEV } });
+ if (m === 'POST' && p === `/api/v1/machines/${DEV}/ai-models`) return send(res, 200, { machine: { id: DEV, ai_models: JSON.parse(body.toString() || '{}').on === true } });
if (m === 'GET' && p === `/api/v1/machines/${DEV}/desk-turn`) return send(res, 200, { desk: u.searchParams.get('desk'), person_driving: false, last_person_at: null });
if (m === 'GET' && p === `/api/v1/machines/${DEV}/desk-turn`) return send(res, 200, { desk: u.searchParams.get('desk'), person_driving: false, last_person_at: null });
if (m === 'POST' && p === `/api/v1/machines/${DEV}/commands`) {
if (m === 'POST' && p === `/api/v1/machines/${DEV}/commands`) {
const input = JSON.parse(body.toString() || '{}');
const input = JSON.parse(body.toString() || '{}');
seenCommands.push(input);
seenCommands.push(input);
if (input.kind === 'browser' && input.value.action === 'click') return send(res, 200, { machine_id: DEV, result: 'nack', reason: 'nothing on the page matches #missing' });
if (input.kind === 'browser' && input.value.action === 'click') return send(res, 200, { machine_id: DEV, result: 'nack', reason: 'nothing on the page matches #missing' });
if (input.kind === 'desk') return send(res, 200, { machine_id: DEV, result: 'ack', data: { running: input.value.action === 'start', desk: input.value.desk, kind: input.value.kind || 'desktop' } });
if (input.kind === 'desk') return send(res, 200, { machine_id: DEV, result: 'ack', data: { running: input.value.action === 'start', desk: input.value.desk, kind: input.value.kind || 'desktop' } });
+ if (input.kind === 'ai' && input.value.model === 'busy') return send(res, 200, { machine_id: DEV, result: 'nack', reason: 'the model is answering another request on this machine' });
+ if (input.kind === 'ai') return send(res, 200, { machine_id: DEV, result: 'ack', data: { model: input.value.model, message: { role: 'assistant', content: 'A lock one thread holds.' }, usage: { completion_tokens: 6 }, ms: 400 } });
return send(res, 200, { machine_id: DEV, result: 'ack', data: { url: 'https://example.org/', title: 'Example' } });
return send(res, 200, { machine_id: DEV, result: 'ack', data: { url: 'https://example.org/', title: 'Example' } });
}
}
if (p.startsWith('/api/v1/machines/')) return problem(res, 404, 'bot.device_not_found', 'no such machine of yours');
if (p.startsWith('/api/v1/machines/')) return problem(res, 404, 'bot.device_not_found', 'no such machine of yours');
@@ -107,6 +110,19 @@ run([
@@ -107,6 +110,19 @@ run([
assert.throws(() => bot.machines.desk(DEV, { name: 'private' }), TypeError, 'only a named desk of your own');
assert.throws(() => bot.machines.desk(DEV, { name: 'private' }), TypeError, 'only a named desk of your own');
} finally { await srv.close(); }
} finally { await srv.close(); }
}],
}],
+ ['a machine\'s own models: one reply (kind ai), a nack thrown, the owner\'s switch', async () => {
+ const srv = await server();
+ try {
+ const bot = createBotClient(createClient({ baseUrls: { bot: srv.url }, token: 'svc' }), { actingSubject: USR });
+ const out = await bot.machines.chat(DEV, { model: 'llama3.1:8b', messages: [{ role: 'user', content: 'What is a mutex?' }], max_tokens: 80 });
+ assert.deepStrictEqual(out, { model: 'llama3.1:8b', message: { role: 'assistant', content: 'A lock one thread holds.' }, usage: { completion_tokens: 6 }, ms: 400 });
+ assert.deepStrictEqual(seenCommands.at(-1), { kind: 'ai', value: { model: 'llama3.1:8b', messages: [{ role: 'user', content: 'What is a mutex?' }], max_tokens: 80 }, ms: 120000 });
+ await assert.rejects(() => bot.machines.chat(DEV, { model: 'busy', messages: [{ role: 'user', content: 'x' }] }), (e) => e.code === 'bot.ai_nack' && /another request/.test(e.message));
+ await assert.rejects(() => bot.machines.chat(DEV, { model: '', messages: [] }), TypeError);
+ assert.strictEqual((await bot.machines.aiModels(DEV, true)).machine.ai_models, true);
+ assert.strictEqual((await bot.machines.aiModels(DEV, false)).machine.ai_models, false);
+ } finally { await srv.close(); }
+ }],
['a service acting for a person sends X-OV-Subject, its token and an Idempotency-Key', async () => {
['a service acting for a person sends X-OV-Subject, its token and an Idempotency-Key', async () => {
const srv = await server();
const srv = await server();
const bot = createBotClient(createClient({ baseUrls: { bot: srv.url }, token: 'svc-token' }), { actingSubject: USR });
const bot = createBotClient(createClient({ baseUrls: { bot: srv.url }, token: 'svc-token' }), { actingSubject: USR });
@@ -81,7 +81,18 @@ export interface PairInput {
@@ -81,7 +81,18 @@ export interface PairInput {
capabilities?: Record<string, unknown>;
capabilities?: Record<string, unknown>;
name?: string;
name?: string;
}
}
-export interface MachineCommandInput { id?: string; kind: 'exec' | 'fs' | 'screen' | 'input' | 'browser' | 'probe' | 'desk'; value: Record<string, unknown>; ms?: number; }
+export interface MachineCommandInput { id?: string; kind: 'exec' | 'fs' | 'screen' | 'input' | 'browser' | 'probe' | 'desk' | 'ai'; value: Record<string, unknown>; ms?: number; }
+/** A machine's own models (OpenVibe.Node's ai plugin): what it offers and how fast its last model answered there. */
+export interface MachineAi { models: string[]; ready: boolean; max_tokens?: number; }
+export interface MachineAiSpeed { model: string; tokens_per_s: number; }
+export interface Machine {
+ id: string; name: string | null; online: boolean;
+ agent_computer?: boolean; ai_models?: boolean; watch_probe?: boolean;
+ computer: ({ ai?: MachineAi | null; ai_speed?: MachineAiSpeed | null; desks_running?: number | null; [field: string]: unknown }) | null;
+ [field: string]: unknown;
+}
+export interface MachineChatInput { model: string; messages: Array<{ role: 'system' | 'user' | 'assistant'; content: string }>; max_tokens?: number; temperature?: number; }
+export interface MachineChatResult { model: string; message: { role: 'assistant'; content: string }; usage?: { prompt_tokens?: number; completion_tokens?: number; total_tokens?: number }; finish_reason?: string; ms?: number; }
export interface MachineCommandResult { machine_id?: string; id?: string; result: 'ack' | 'nack' | 'expired' | 'refused' | string; data?: Record<string, unknown>; code?: string; reason?: string | null; [field: string]: unknown; }
export interface MachineCommandResult { machine_id?: string; id?: string; result: 'ack' | 'nack' | 'expired' | 'refused' | string; data?: Record<string, unknown>; code?: string; reason?: string | null; [field: string]: unknown; }
export interface DeskOptions { name?: string; kind?: 'browser' | 'desktop'; profile?: string; width?: number; height?: number; }
export interface DeskOptions { name?: string; kind?: 'browser' | 'desktop'; profile?: string; width?: number; height?: number; }
export interface Desk {
export interface Desk {
@@ -97,11 +108,13 @@ export interface Desk {
@@ -97,11 +108,13 @@ export interface Desk {
turn(): Promise<{ desk: string; person_driving: boolean; last_person_at: string | null }>;
turn(): Promise<{ desk: string; person_driving: boolean; last_person_at: string | null }>;
}
}
export interface MachinesApi {
export interface MachinesApi {
- list(opts?: CallOptions): Promise<{ machines: Array<Record<string, unknown>> }>;
+ list(opts?: CallOptions): Promise<{ machines: Machine[] }>;
computer(id: string, opts?: CallOptions): Promise<{ machine: Record<string, unknown> } | null>;
computer(id: string, opts?: CallOptions): Promise<{ machine: Record<string, unknown> } | null>;
command(id: string, input: MachineCommandInput, opts?: CallOptions): Promise<MachineCommandResult>;
command(id: string, input: MachineCommandInput, opts?: CallOptions): Promise<MachineCommandResult>;
deskTurn(id: string, desk: string, opts?: CallOptions): Promise<{ desk: string; person_driving: boolean; last_person_at: string | null }>;
deskTurn(id: string, desk: string, opts?: CallOptions): Promise<{ desk: string; person_driving: boolean; last_person_at: string | null }>;
desk(id: string, options?: DeskOptions, opts?: CallOptions): Desk;
desk(id: string, options?: DeskOptions, opts?: CallOptions): Desk;
+ chat(id: string, input: MachineChatInput, opts?: CallOptions & { ms?: number }): Promise<MachineChatResult>;
+ aiModels(id: string, on: boolean, opts?: CallOptions): Promise<{ machine: Machine }>;
}
}
export interface BotClient {
export interface BotClient {
robots: RobotsApi;
robots: RobotsApi;