openvibers/openvibe.sdk Commits
0.71.0: openvibe-sdk/bench, the Bench client (#98)
Changes
@@@ -3,6 +3,10 @@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.71.0 (2026-10-28)++++- **`openvibe-sdk/bench`** (BENCH-4, plan §4.12): browser-safe Bench client for kinds, tasks, pinned rounds, outside entries, judgements, boards, model summaries, streamed open JSONL datasets and private evaluations. `mcpTools()` lists the live Bench MCP tools through `POST /mcp`. Calls use the `bench` service origin and `openvibe.bench` audience; optional `actingSubject` sends `X-OV-Subject`. HTTP errors remain `OpenVibeError`.++## 0.70.0 (2026-10-28)- **`openvibe-sdk/search`: the Search Fabric** (Contracts 0.225.0 `search.fabric.query`; plan 2026-10-28 §4.5). One@@@ -2,14 +2,14 @@> Supported browser and server clients for the OpenVibe platform.--**Status:** alpha, v0.70.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.71.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.70.0++npm install https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.71.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).@@@ -174,6 +174,7 @@ Until OpenVibe.Tools serves the gateway facade (Tools S6), point the jobs client| `openvibe-sdk/community` | both | `createCommunityClient(client, {actingSubject?, origin?, sourceRef?, staff?})`: `pastes.list/iterate/get/create/update/delete/fork/like/copy/versions/byUser/config`, `pastes.comments.list/create/delete`, `as(subject)` || `openvibe-sdk/bot` | both | `createBotClient(client, {actingSubject?})`: OpenVibe.Bot robots. `robots.list/get/create/update/delete/pairingCode`, `robots.operators.list/add/remove`, `robots.devices`, `robots.streaming.get/set`, `robots.audit`/`iterateAudit`, `robots.estop`/`clearEstop`, `robots.command` (`bot.command@1`), `devices.rotate/revoke`, public `profiles.list/get` and `kits.list/get`, `pair` (the agent's one-time code, never retried), `as(subject)`; a service acts for a person with `X-OV-Subject` (capabilities `bot.robot.read`/`manage`/`control`, `bot.device.connect`) || `openvibe-sdk/themes` | both | `createThemesClient(client)`: `catalog({mode, search, sort, limit})` (seasonal themes carry `season`), `get(idOrSlug)` (null when missing), `export(idOrSlug)`, `active()` (`{theme_id, custom_variables, display}`), `set(themeId, {custom})`, `setDisplay({motion, text, seasonal})`, `seasons({date})`, `submissions()`, `submit(theme)`, `import(file)`. In the browser: `followTheme(fn)` calls back now and on every theme, display or season change with `readTheme()` (`{id, mode, tokens, display: {motion, text, textScale, seasonal, calm}, season, seasonMark}`) and returns `stop()`, for a canvas or game to keep up; `applyTheme(vars, {root})`, `seasonOn(date, seasons)`, `onColor(hex)`, `DISPLAY_OPTIONS`, `TOKENS` |++| `openvibe-sdk/bench` | both | `createBenchClient(client, {actingSubject?})`: `kinds.list/get`, `tasks.create/get`, `rounds.create/get`, `entries.submit`, `judgements.next/cast`, `boards.get`, `models.get`, `datasets.get/iterate`, `evals.create/get/run/results`, `mcpTools()`. Audience `openvibe.bench`; service id `bench`. || `openvibe-sdk/search` | both | `createSearchClient(client, {actingSubject?})`: `query(text, {owner, type, lang, filter, facets, limit, cursor})`, `iterate(text, {…, max})`, `suggest(text, {owner, type, limit})`, `document(owner, type, id)` (null when missing or not visible) || `openvibe-sdk/limits` | node | `createActorLimiter({limits: {minute, hour, day}, actor?, maxActors?, onLimited?})` → `limits(name, {minute?, hour?, day?})` Express middleware: per-actor fixed windows at a capability boundary (service principal, else the signed-in person, else `ip:`), 429 problem+json `rate_limited` with Retry-After before the handler; `defaultActor(req)`, `.stats()` || `openvibe-sdk/account-data` | node | `createAccountData({db, service, tables: [{table, subject, value?, file?, columns?, erase?}], extraExport?, extraErase?})` → `.apply(event, {send})`, `.consumer({secrets, send, onEvent?})` (a POST /internal/events handler), `.exportPart(subject)`, `.erase(subjects)`, `.ensureSchema()`; `createNetworkSender({networkInternalUrl, clientId, clientSecret})`; `startSubscriptions({eventsUrl, endpoint, secret, networkInternalUrl, clientId, clientSecret})` (the two Events subscriptions, created at boot when missing): a service's share of account export and deletion (ADR-033), idempotent per export/deletion id |@@@ -3,7 +3,7 @@"stage": "alpha","code": true,"wave": "W2",-- "version": "0.70.0",++ "version": "0.71.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",@@@ -15,7 +15,7 @@"events: publish, pull and checkpoints by opaque cursor only, app events, webhook verification (v2), outbox and inbox","realtime SSE (resumes from the opaque cursor, dedupes by event_id)","media files and objects v2",-- "community, bot, search, chat, ai, jobs, tools, projects, modules, identity, openre, geo, vip, quest, play and download clients",++ "community, bot, search, bench, chat, ai, jobs, tools, projects, modules, identity, openre, geo, vip, quest, play and download clients","bot (openvibe-sdk/bot): OpenVibe.Bot robots, operators, devices, streaming, commands and audit, the public kits and profiles, and pairing","per-actor limiter for services (openvibe-sdk/limits)","no-looks (openvibe-sdk/no-looks): public AI text never describes a person, one filter and corpus for every service",@@@ -32,5 +32,5 @@"the telemetry sink (openvibe-sdk/telemetry): telemetrySchema() DDL, createPgTelemetrySink() and pruneTelemetry(), a per-service PostgreSQL table every service stores platform.telemetry-sample@1 into","resources (openvibe-sdk/resources): the ADR-048 resource index fan-out and the idempotent control client"],-- "updated": "2026-10-27"++ "updated": "2026-10-28"}@@@ -15,6 +15,7 @@ module.exports = {community: require('./src/community'),bot: require('./src/bot'),search: require('./src/search'),++ bench: require('./src/bench'),themes: require('./src/themes'),chat: require('./src/chat'),ai: require('./src/ai'),@@@ -1,6 +1,6 @@--// Generated by scripts/browser-bundle.js from browser.js (openvibe-sdk 0.70.0, MIT). Do not edit.++// Generated by scripts/browser-bundle.js from browser.js (openvibe-sdk 0.71.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.++// (auth, registry, modules, realtime, media, community, bot, search, bench, themes, chat, ai, jobs, tools, projects, geo). No server-only code: no service tokens, JWT verification, events, identity./* eslint-disable */const __defs = [// ── 0: browser.js ──────────────────────────────────────────────────@@@ -22,13 +22,14 @@ module.exports = {community: __load(14),bot: __load(15),search: __load(16),-- themes: __load(17),-- chat: __load(18),-- ai: __load(19),-- jobs: __load(20),-- tools: __load(22),-- projects: __load(23),-- geo: __load(24),++ bench: __load(17),++ themes: __load(18),++ chat: __load(19),++ ai: __load(20),++ jobs: __load(21),++ tools: __load(23),++ projects: __load(24),++ geo: __load(25),};},// ── 1: src/core/index.js ───────────────────────────────────────────@@@ -43,7 +44,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.70.0';++const SDK_VERSION = '0.71.0';module.exports = {createClient,@@@ -2090,7 +2091,108 @@ function createSearchClient(client, defaults = {}) {module.exports = { createSearchClient, queryOf, fabricOf };},--// ── 17: src/themes.js ───────────────────────────────────────────────++// ── 17: src/bench.js ────────────────────────────────────────────────++function (module, exports) {++'use strict';++/** openvibe-sdk/bench: OpenVibe.Bench's JSON API and streamed open dataset. Browser-safe. */++const { isActingSubjectId } = __load(5);++const { OpenVibeError } = __load(3);++++const enc = encodeURIComponent;++const path = (part, id) => `/api/v1/${part}/${enc(id)}`;++++function createBenchClient(client, defaults = {}) {++ function options(opts = {}, perCall = {}) {++ const acting = perCall.actingSubject !== undefined ? perCall.actingSubject : defaults.actingSubject;++ const headers = { ...opts.headers };++ if (acting) {++ if (!isActingSubjectId(acting)) throw new TypeError('actingSubject must be a usr_… or gst_… subject id');++ headers['X-OV-Subject'] = acting;++ }++ return { service: 'bench', baseUrl: defaults.baseUrl, audience: 'openvibe.bench', ...opts, headers, signal: perCall.signal };++ }++ const json = (opts, perCall) => client.json(options(opts, perCall));++ const post = (route, body, o) => json({ method: 'POST', path: route, json: body }, o);++ const datasetPath = (kind) => `${path('datasets', kind)}.jsonl`;++++ return {++ kinds: {++ list: (o = {}) => json({ path: '/api/v1/kinds' }, o),++ get: (kind, o = {}) => json({ path: path('kinds', kind) }, o),++ },++ tasks: {++ create: (kind, task, o = {}) => post(`${path('kinds', kind)}/tasks`, task, o),++ get: (id, o = {}) => json({ path: path('tasks', id) }, o),++ },++ rounds: {++ create: (taskId, round, o = {}) => post(`${path('tasks', taskId)}/rounds`, round, o),++ get: (id, o = {}) => json({ path: path('rounds', id) }, o),++ },++ entries: {++ submit: (roundId, entry, o = {}) => post(`${path('rounds', roundId)}/entries`, entry, o),++ },++ judgements: {++ next: ({ kind, ...o } = {}) => json({ path: '/api/v1/judgements/next', query: { kind } }, o),++ cast: (roundId, judgement, o = {}) => post(`${path('rounds', roundId)}/judgements`, judgement, o),++ },++ boards: {++ get: (kind, { type, category, period, ...o } = {}) => json({ path: path('boards', kind), query: { type, category, period } }, o),++ },++ models: {++ get: (model, o = {}) => json({ path: path('models', model) }, o),++ },++ datasets: {++ get: (kind, o = {}) => client.json(options({ path: datasetPath(kind), responseType: 'text' }, o)),++ async iterate(kind, onLine, o = {}) {++ if (typeof onLine !== 'function') throw new TypeError('onLine must be a function');++ const { response } = await client.request(options({ path: datasetPath(kind), responseType: 'response' }, o));++ const reader = response.body.getReader();++ const decoder = new TextDecoder();++ let pending = '';++ let line = 0;++ const emit = async (s) => {++ if (s.endsWith('\r')) s = s.slice(0, -1);++ if (!s.trim()) return;++ line++;++ let value;++ try { value = JSON.parse(s); }++ catch (cause) { throw new OpenVibeError({ code: 'sdk.bad_response', message: `Bench dataset has invalid JSON at line ${line}`, cause }); }++ await onLine(value);++ };++ try {++ for (;;) {++ const { value, done } = await reader.read();++ if (done) break;++ pending += decoder.decode(value, { stream: true });++ let pos;++ while ((pos = pending.indexOf('\n')) !== -1) {++ await emit(pending.slice(0, pos));++ pending = pending.slice(pos + 1);++ }++ }++ pending += decoder.decode();++ await emit(pending);++ } finally { reader.releaseLock(); }++ },++ },++ evals: {++ create: (evaluation, o = {}) => post('/api/v1/evals', evaluation, o),++ get: (id, o = {}) => json({ path: path('evals', id) }, o),++ run: (id, run, o = {}) => post(`${path('evals', id)}/run`, run, o),++ results: (id, o = {}) => json({ path: `${path('evals', id)}/results` }, o),++ },++ async mcpTools(o = {}) {++ const data = await post('/mcp', { jsonrpc: '2.0', id: 1, method: 'tools/list', params: {} }, o);++ if (data && data.error) throw new OpenVibeError({ code: 'sdk.bad_response', message: data.error.message || 'Bench MCP tools/list failed', detail: data.error.message });++ if (!data || !data.result || !Array.isArray(data.result.tools)) throw new OpenVibeError({ code: 'sdk.bad_response', message: 'Bench MCP tools/list returned no tools' });++ return data.result.tools;++ },++ };++}++++module.exports = { createBenchClient };++},++// ── 18: src/themes.js ───────────────────────────────────────────────function (module, exports) {'use strict';/**@@@ -2240,7 +2342,7 @@ function followTheme(fn, opts = {}) {module.exports = { createThemesClient, followTheme, readTheme, applyTheme, seasonOn, onColor, DISPLAY_OPTIONS, TOKENS };},--// ── 18: src/chat.js ─────────────────────────────────────────────────++// ── 19: src/chat.js ─────────────────────────────────────────────────function (module, exports) {'use strict';/**@@@ -2482,7 +2584,7 @@ function createFeedPublisher(client, { feed, key, baseUrl } = {}) {module.exports = { createChatClient, createInboxVisitor, createFeedPublisher };},--// ── 19: src/ai.js ───────────────────────────────────────────────────++// ── 20: src/ai.js ───────────────────────────────────────────────────function (module, exports) {'use strict';/**@@@ -2557,7 +2659,7 @@ function createAiClient(client, defaults = {}) {module.exports = { createAiClient, TERMINAL, OPS };},--// ── 20: src/jobs.js ─────────────────────────────────────────────────++// ── 21: src/jobs.js ─────────────────────────────────────────────────function (module, exports) {'use strict';/**@@@ -2595,7 +2697,7 @@ function (module, exports) {*/const { OpenVibeError, isOpenVibeError } = __load(3);const { newIdempotencyKey } = __load(5);--const { appendFiles } = __load(21);++const { appendFiles } = __load(22);const { parseSSE } = __load(11);const TERMINAL = new Set(['succeeded', 'failed', 'cancelled']);@@@ -2759,7 +2861,7 @@ function createJobsClient(client, { baseUrl, service = 'tools', audience = 'openmodule.exports = { createJobsClient, isTerminal, TERMINAL_STATES: [...TERMINAL] };},--// ── 21: src/core/form.js ────────────────────────────────────────────++// ── 22: src/core/form.js ────────────────────────────────────────────function (module, exports) {'use strict';/**@@@ -2793,7 +2895,7 @@ function appendFiles(form, list, where) {module.exports = { isBlob, isUpload, appendFiles };},--// ── 22: src/tools.js ────────────────────────────────────────────────++// ── 23: src/tools.js ────────────────────────────────────────────────function (module, exports) {'use strict';/**@@@ -2827,8 +2929,8 @@ function (module, exports) {*/const { OpenVibeError, isOpenVibeError } = __load(3);const { newIdempotencyKey } = __load(5);--const { isUpload, appendFiles } = __load(21);--const { createJobsClient } = __load(20);++const { isUpload, appendFiles } = __load(22);++const { createJobsClient } = __load(21);const enc = encodeURIComponent;const ID_RE = /^[a-z][a-z0-9-]{0,39}$/;@@@ -3078,7 +3180,7 @@ function createToolsClient(client, { baseUrl, service = 'tools', audience = 'opemodule.exports = { createToolsClient, ToolRunError, isToolRunError };},--// ── 23: src/projects.js ─────────────────────────────────────────────++// ── 24: src/projects.js ─────────────────────────────────────────────function (module, exports) {'use strict';/**@@@ -3194,7 +3296,7 @@ function createProjectsClient(client, { baseUrl } = {}) {module.exports = { createProjectsClient };},--// ── 24: src/geo.js ──────────────────────────────────────────────────++// ── 25: src/geo.js ──────────────────────────────────────────────────function (module, exports) {'use strict';/**@@@ -3274,7 +3376,7 @@ export const {startSpan, contextFromHeaders, ulid, newEventId, newIdempotencyKey, isActingSubjectId,satisfiesRange, compareVersions, CONTRACTS_RANGE, DEFAULT_NETWORK, SDK_VERSION, auth,registry, modules, realtime, media, community, bot,-- search, themes, chat, ai, jobs, tools,-- projects, geo,++ search, bench, themes, chat, ai, jobs,++ tools, projects, geo,} = sdk;export default sdk;@@@ -43,6 +43,11 @@"verifyUserToken"]},++ "bench": {++ "object": [++ "createBenchClient"++ ]++ },"blog": {"object": ["createBlogClient"@@@ -374,6 +379,9 @@"startAuthorization": "function","unverifiedClaims": "function"},++ "openvibe-sdk/bench": {++ "createBenchClient": "function"++ },"openvibe-sdk/blog": {"createBlogClient": "function"},nnew file mode 100644@@@ -0,0 +1,7 @@++// Generated by scripts/esm.js from src/bench.js. Do not edit.++import cjs from '../src/bench.js';++++export const {++ createBenchClient,++} = cjs;++export default cjs;@@@ -6,12 +6,12 @@ export const {startSpan, contextFromHeaders, ulid, newEventId, newIdempotencyKey, isActingSubjectId,satisfiesRange, compareVersions, CONTRACTS_RANGE, DEFAULT_NETWORK, SDK_VERSION, auth,registry, identity, modules, events, realtime, media,-- community, bot, search, themes, chat, ai,-- jobs, tools, projects, resources, vip, quest,-- live, inventory, tips, blog, reviews, wiki,-- coupons, deals, trade, space, host, news,-- food, actor, run, video, work, zone,-- play, download, openre, geo, usage, commerce,-- telemetry,++ community, bot, search, bench, themes, chat,++ ai, jobs, tools, projects, resources, vip,++ quest, live, inventory, tips, blog, reviews,++ wiki, coupons, deals, trade, space, host,++ news, food, actor, run, video, work,++ zone, play, download, openre, geo, usage,++ commerce, telemetry,} = cjs;export default cjs;@@@ -18,6 +18,7 @@ module.exports = {community: require('./src/community'),bot: require('./src/bot'),search: require('./src/search'),++ bench: require('./src/bench'),themes: require('./src/themes'),chat: require('./src/chat'),ai: require('./src/ai'),@@@ -1,12 +1,12 @@{"name": "openvibe-sdk",-- "version": "0.70.0",++ "version": "0.71.0","lockfileVersion": 3,"requires": true,"packages": {"": {"name": "openvibe-sdk",-- "version": "0.70.0",++ "version": "0.71.0","license": "MIT","bin": {"openvibe-download": "bin/openvibe-download.js"@@@ -1,6 +1,6 @@{"name": "openvibe-sdk",-- "version": "0.70.0",++ "version": "0.71.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",@@@ -87,6 +87,11 @@"import": "./esm/search.mjs","default": "./src/search.js"},++ "./bench": {++ "types": "./types/bench.d.ts",++ "import": "./esm/bench.mjs",++ "default": "./src/bench.js"++ },"./themes": {"types": "./types/themes.d.ts","import": "./esm/themes.mjs",@@@ -27,6 +27,7 @@ const ENTRIES = {'community.mjs': '../src/community.js','bot.mjs': '../src/bot.js','search.mjs': '../src/search.js',++ 'bench.mjs': '../src/bench.js','themes.mjs': '../src/themes.js','limits.mjs': '../src/limits.js','account-data.mjs': '../src/account-data.js',nnew file mode 100644@@@ -0,0 +1,98 @@++'use strict';++/** openvibe-sdk/bench: OpenVibe.Bench's JSON API and streamed open dataset. Browser-safe. */++const { isActingSubjectId } = require('./core/ids');++const { OpenVibeError } = require('./core/errors');++++const enc = encodeURIComponent;++const path = (part, id) => `/api/v1/${part}/${enc(id)}`;++++function createBenchClient(client, defaults = {}) {++ function options(opts = {}, perCall = {}) {++ const acting = perCall.actingSubject !== undefined ? perCall.actingSubject : defaults.actingSubject;++ const headers = { ...opts.headers };++ if (acting) {++ if (!isActingSubjectId(acting)) throw new TypeError('actingSubject must be a usr_… or gst_… subject id');++ headers['X-OV-Subject'] = acting;++ }++ return { service: 'bench', baseUrl: defaults.baseUrl, audience: 'openvibe.bench', ...opts, headers, signal: perCall.signal };++ }++ const json = (opts, perCall) => client.json(options(opts, perCall));++ const post = (route, body, o) => json({ method: 'POST', path: route, json: body }, o);++ const datasetPath = (kind) => `${path('datasets', kind)}.jsonl`;++++ return {++ kinds: {++ list: (o = {}) => json({ path: '/api/v1/kinds' }, o),++ get: (kind, o = {}) => json({ path: path('kinds', kind) }, o),++ },++ tasks: {++ create: (kind, task, o = {}) => post(`${path('kinds', kind)}/tasks`, task, o),++ get: (id, o = {}) => json({ path: path('tasks', id) }, o),++ },++ rounds: {++ create: (taskId, round, o = {}) => post(`${path('tasks', taskId)}/rounds`, round, o),++ get: (id, o = {}) => json({ path: path('rounds', id) }, o),++ },++ entries: {++ submit: (roundId, entry, o = {}) => post(`${path('rounds', roundId)}/entries`, entry, o),++ },++ judgements: {++ next: ({ kind, ...o } = {}) => json({ path: '/api/v1/judgements/next', query: { kind } }, o),++ cast: (roundId, judgement, o = {}) => post(`${path('rounds', roundId)}/judgements`, judgement, o),++ },++ boards: {++ get: (kind, { type, category, period, ...o } = {}) => json({ path: path('boards', kind), query: { type, category, period } }, o),++ },++ models: {++ get: (model, o = {}) => json({ path: path('models', model) }, o),++ },++ datasets: {++ get: (kind, o = {}) => client.json(options({ path: datasetPath(kind), responseType: 'text' }, o)),++ async iterate(kind, onLine, o = {}) {++ if (typeof onLine !== 'function') throw new TypeError('onLine must be a function');++ const { response } = await client.request(options({ path: datasetPath(kind), responseType: 'response' }, o));++ const reader = response.body.getReader();++ const decoder = new TextDecoder();++ let pending = '';++ let line = 0;++ const emit = async (s) => {++ if (s.endsWith('\r')) s = s.slice(0, -1);++ if (!s.trim()) return;++ line++;++ let value;++ try { value = JSON.parse(s); }++ catch (cause) { throw new OpenVibeError({ code: 'sdk.bad_response', message: `Bench dataset has invalid JSON at line ${line}`, cause }); }++ await onLine(value);++ };++ try {++ for (;;) {++ const { value, done } = await reader.read();++ if (done) break;++ pending += decoder.decode(value, { stream: true });++ let pos;++ while ((pos = pending.indexOf('\n')) !== -1) {++ await emit(pending.slice(0, pos));++ pending = pending.slice(pos + 1);++ }++ }++ pending += decoder.decode();++ await emit(pending);++ } finally { reader.releaseLock(); }++ },++ },++ evals: {++ create: (evaluation, o = {}) => post('/api/v1/evals', evaluation, o),++ get: (id, o = {}) => json({ path: path('evals', id) }, o),++ run: (id, run, o = {}) => post(`${path('evals', id)}/run`, run, o),++ results: (id, o = {}) => json({ path: `${path('evals', id)}/results` }, o),++ },++ async mcpTools(o = {}) {++ const data = await post('/mcp', { jsonrpc: '2.0', id: 1, method: 'tools/list', params: {} }, o);++ if (data && data.error) throw new OpenVibeError({ code: 'sdk.bad_response', message: data.error.message || 'Bench MCP tools/list failed', detail: data.error.message });++ if (!data || !data.result || !Array.isArray(data.result.tools)) throw new OpenVibeError({ code: 'sdk.bad_response', message: 'Bench MCP tools/list returned no tools' });++ return data.result.tools;++ },++ };++}++++module.exports = { createBenchClient };@@@ -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.70.0';++const SDK_VERSION = '0.71.0';module.exports = {createClient,nnew file mode 100644@@@ -0,0 +1,102 @@++'use strict';++/** Bench API routes, envelopes, streaming JSONL, MCP, auth and problem errors. */++const assert = require('node:assert/strict');++const { stubServer, send, problem, run } = require('./helpers');++const { createClient, OpenVibeError } = require('../src/core');++const { createBenchClient } = require('../src/bench');++++const USR = 'usr_01JAB2C3D4E5F6G7H8J9K0MNPQ';++++async function bench() {++ return stubServer((req, res, body) => {++ const u = new URL(req.url, 'http://x');++ if (u.pathname === '/api/v1/datasets/vision.jsonl') {++ res.writeHead(200, { 'Content-Type': 'application/x-ndjson' });++ res.write('{"type":"task","id":"t1"}\n{"type":"rou');++ res.end('nd","id":"r1"}\n');++ return;++ }++ if (u.pathname === '/api/v1/kinds/missing') return problem(res, 404, 'bench.kind_not_found', 'No such kind.');++ if (u.pathname === '/mcp') return send(res, 200, { jsonrpc: '2.0', id: 1, result: { tools: [{ name: 'bench.list_kinds', inputSchema: { type: 'object' } }] } });++ const responses = {++ '/api/v1/kinds': { count: 1, kinds: [{ id: 'vision' }] },++ '/api/v1/kinds/vision': { kind: { id: 'vision' } },++ '/api/v1/kinds/vision/tasks': { task: { id: 't1' } },++ '/api/v1/tasks/t1': { task: { id: 't1' } },++ '/api/v1/tasks/t1/rounds': { round: { id: 'r1' } },++ '/api/v1/rounds/r1': { round: { id: 'r1' } },++ '/api/v1/rounds/r1/entries': { entry: { id: 'e1' } },++ '/api/v1/judgements/next': { pair: null, detail: 'No round' },++ '/api/v1/rounds/r1/judgements': { judgement: { id: 'j1' } },++ '/api/v1/boards/vision': { board: { type: 'pareto', rows: [] } },++ '/api/v1/models/model%2Fone': { model: 'model/one', ratings: [], entries: [], total_cost_usd: 0 },++ '/api/v1/evals': { eval: { id: 'v1' } },++ '/api/v1/evals/v1': { eval: { id: 'v1' } },++ '/api/v1/evals/v1/run': { run: { id: 'run1' } },++ '/api/v1/evals/v1/results': { eval: { id: 'v1' }, runs: [] },++ };++ if (Object.hasOwn(responses, u.pathname)) return send(res, req.method === 'POST' ? 201 : 200, responses[u.pathname]);++ return problem(res, 404, 'bench.not_found', 'Missing');++ });++}++++run([++ ['all JSON routes preserve Bench envelopes, paths, query and bodies', async () => {++ const srv = await bench();++ try {++ const b = createBenchClient(createClient({ baseUrls: { bench: srv.url }, retries: 0 }));++ assert.equal((await b.kinds.list()).count, 1);++ assert.equal((await b.kinds.get('vision')).kind.id, 'vision');++ assert.equal((await b.tasks.create('vision', { prompt: 'Compare', task_class: 'image', public: true })).task.id, 't1');++ assert.equal((await b.tasks.get('t1')).task.id, 't1');++ assert.equal((await b.rounds.create('t1', { offers: [{ provider: 'ai', model: 'one' }], seed: 7 })).round.id, 'r1');++ assert.equal((await b.rounds.get('r1')).round.id, 'r1');++ assert.equal((await b.entries.submit('r1', { provider: 'ai', model: 'one', payload: { text: 'hello' }, cost_usd: 0.01 })).entry.id, 'e1');++ assert.equal((await b.judgements.next({ kind: 'vision' })).pair, null);++ assert.equal((await b.judgements.cast('r1', { judge: 'human_pairwise', pair: { left: 'e1', right: 'e2' }, outcome: 'left' })).judgement.id, 'j1');++ assert.equal((await b.boards.get('vision', { type: 'pareto', category: 'all', period: 'month' })).board.type, 'pareto');++ assert.equal((await b.models.get('model/one')).model, 'model/one');++ assert.equal((await b.evals.create({ kind_id: 'vision', name: 'Private', test_suite: [{ prompt: 'x' }] })).eval.id, 'v1');++ assert.equal((await b.evals.get('v1')).eval.id, 'v1');++ assert.equal((await b.evals.run('v1', { offers: [{ provider: 'ai' }] })).run.id, 'run1');++ assert.deepEqual((await b.evals.results('v1')).runs, []);++ assert.deepEqual(srv.requests.map((r) => [r.method, r.url]), [++ ['GET', '/api/v1/kinds'], ['GET', '/api/v1/kinds/vision'],++ ['POST', '/api/v1/kinds/vision/tasks'], ['GET', '/api/v1/tasks/t1'],++ ['POST', '/api/v1/tasks/t1/rounds'], ['GET', '/api/v1/rounds/r1'],++ ['POST', '/api/v1/rounds/r1/entries'], ['GET', '/api/v1/judgements/next?kind=vision'],++ ['POST', '/api/v1/rounds/r1/judgements'], ['GET', '/api/v1/boards/vision?type=pareto&category=all&period=month'],++ ['GET', '/api/v1/models/model%2Fone'], ['POST', '/api/v1/evals'],++ ['GET', '/api/v1/evals/v1'], ['POST', '/api/v1/evals/v1/run'], ['GET', '/api/v1/evals/v1/results'],++ ]);++ assert.deepEqual(JSON.parse(srv.requests[2].body), { prompt: 'Compare', task_class: 'image', public: true });++ assert.deepEqual(JSON.parse(srv.requests[4].body), { offers: [{ provider: 'ai', model: 'one' }], seed: 7 });++ assert.deepEqual(JSON.parse(srv.requests[6].body), { provider: 'ai', model: 'one', payload: { text: 'hello' }, cost_usd: 0.01 });++ assert.deepEqual(JSON.parse(srv.requests[8].body), { judge: 'human_pairwise', pair: { left: 'e1', right: 'e2' }, outcome: 'left' });++ } finally { await srv.close(); }++ }],++ ['dataset text and streamed lines; MCP tools list; service audience and acting subject', async () => {++ const srv = await bench();++ try {++ const b = createBenchClient(createClient({ baseUrls: { bench: srv.url }, getToken: ({ audience }) => `svc-${audience}` }), { actingSubject: USR });++ assert.match(await b.datasets.get('vision'), /"type":"task"/);++ const lines = [];++ await b.datasets.iterate('vision', async (line) => { lines.push(line); });++ assert.deepEqual(lines, [{ type: 'task', id: 't1' }, { type: 'round', id: 'r1' }]);++ assert.equal((await b.mcpTools())[0].name, 'bench.list_kinds');++ assert.deepEqual(JSON.parse(srv.requests[2].body), { jsonrpc: '2.0', id: 1, method: 'tools/list', params: {} });++ assert.equal(srv.requests[0].headers.authorization, 'Bearer svc-openvibe.bench');++ assert.equal(srv.requests[0].headers['x-ov-subject'], USR);++ assert.equal(srv.requests[2].headers['x-ov-subject'], USR);++ } finally { await srv.close(); }++ }],++ ['problem errors are OpenVibeError; invalid acting subjects fail before the request', async () => {++ const srv = await bench();++ try {++ const b = createBenchClient(createClient({ baseUrls: { bench: srv.url }, retries: 0 }));++ await assert.rejects(b.kinds.get('missing'), (err) => err instanceof OpenVibeError && err.code === 'bench.kind_not_found' && err.status === 404);++ assert.throws(() => b.kinds.list({ actingSubject: 'bad' }), /actingSubject/);++ assert.equal(srv.requests.length, 1);++ } finally { await srv.close(); }++ }],++]);@@@ -68,7 +68,7 @@ run([})().catch((err) => { console.error(err); process.exit(1); });`;const res = execFileSync(process.execPath, ['-e', script], { cwd: app, encoding: 'utf8' });assert.match(res, new RegExp(`consumer ok: ${subpaths.length} subpaths \\+ the browser bundle`));-- assert.equal(subpaths.length, 68);++ assert.equal(subpaths.length, 69);assert.ok(!fs.existsSync(path.join(app, 'node_modules/openvibe-contracts')), 'the optional peer is not installed');} finally {fs.rmSync(tmp, { recursive: true, force: true });nnew file mode 100644@@@ -0,0 +1,68 @@++import type { OpenVibeClient } from './core';++++export interface BenchOptions { actingSubject?: string; signal?: AbortSignal; }++export interface BenchDefaults extends BenchOptions { baseUrl?: string; }++export interface BenchKind { id: string; room: string; entry: string; judge: string; name: string; schema: Record<string, unknown>; metrics: unknown[]; created_at: string | null; }++export interface BenchTask { id: string; kind_id: string; prompt: string; task_class: string | null; public: boolean; created_by?: string; created_at: string; }++export interface BenchOffer { provider: string; model?: string | null; }++export interface BenchEntry {++ id: string; round_id: string; provider?: string; model?: string;++ route?: Record<string, unknown>; payload: Record<string, unknown>;++ cost_usd?: number; ms?: number; tokens?: number; error: string | null;++ ui_check: Record<string, unknown> | null; run_id: string | null; created_at: string;++}++export interface BenchRound {++ id: string; task_id: string; kind_id: string; offers: BenchOffer[]; status: string;++ seed: number; params: Record<string, unknown>; pool_fallback: boolean; receipt: unknown | null;++ created_by?: string; created_at: string; finished_at: string | null;++ entries?: BenchEntry[]; votes?: number; blind?: boolean;++ [field: string]: unknown;++}++export interface BenchJudgementInput {++ kind_id?: string; judge?: string; judge_name?: string; judge_version?: string;++ pair?: { left: string; right: string }; outcome?: 'left' | 'right' | 'tie' | 'both_bad' | 'draw'; score?: number;++}++export interface BenchBoard { kind_id: string; type: 'elo' | 'pareto' | 'cost'; category: string; period: string; rows: Array<Record<string, unknown>>; }++export interface BenchEval { id: string; owner: string; kind_id: string; name: string; test_suite: unknown[]; created_at: string; }++export interface BenchEvalRun { id: string; eval_id: string; offers: BenchOffer[]; status: string; created_at: string; [field: string]: unknown; }++export type BenchDatasetLine = ({ type: 'task' } & BenchTask) | ({ type: 'round' } & BenchRound) | ({ type: 'entry' } & BenchEntry) | ({ type: 'judgement' | 'rating' } & Record<string, unknown>);++export interface BenchMcpTool { name: string; description?: string; inputSchema: Record<string, unknown>; [field: string]: unknown; }++++export interface BenchClient {++ kinds: {++ list(opts?: BenchOptions): Promise<{ count: number; kinds: BenchKind[] }>;++ get(kind: string, opts?: BenchOptions): Promise<{ kind: BenchKind }>;++ };++ tasks: {++ create(kind: string, task: { prompt: string; task_class?: string; public?: boolean }, opts?: BenchOptions): Promise<{ task: BenchTask }>;++ get(id: string, opts?: BenchOptions): Promise<{ task: BenchTask }>;++ };++ rounds: {++ create(taskId: string, round?: { offers?: BenchOffer[]; seed?: number }, opts?: BenchOptions): Promise<{ round: BenchRound }>;++ get(id: string, opts?: BenchOptions): Promise<{ round: BenchRound }>;++ };++ entries: {++ submit(roundId: string, entry: { provider?: string; model?: string; route?: Record<string, unknown>; payload?: Record<string, unknown>; cost_usd?: number; ms?: number; tokens?: number; error?: string | null; ui_check?: Record<string, unknown> | null; run_id?: string | null }, opts?: BenchOptions): Promise<{ entry: BenchEntry }>;++ };++ judgements: {++ next(query: { kind: string } & BenchOptions): Promise<{ round: { id: string; kind_id: string; prompt: string }; left: { id: string; payload: Record<string, unknown> }; right: { id: string; payload: Record<string, unknown> } } | { pair: null; detail: string }>;++ cast(roundId: string, judgement: BenchJudgementInput, opts?: BenchOptions): Promise<{ judgement: Record<string, unknown>; revealed: { left: BenchEntry; right: BenchEntry } | null }>;++ };++ boards: { get(kind: string, query?: { type?: 'elo' | 'pareto' | 'cost'; category?: string; period?: string } & BenchOptions): Promise<{ board: BenchBoard }> };++ models: { get(model: string, opts?: BenchOptions): Promise<{ model: string; ratings: Array<Record<string, unknown>>; entries: BenchEntry[]; total_cost_usd: number }> };++ datasets: {++ /** Raw application/x-ndjson text. */++ get(kind: string, opts?: BenchOptions): Promise<string>;++ /** Parses each JSONL object as bytes arrive and awaits onLine. */++ iterate(kind: string, onLine: (line: BenchDatasetLine) => void | Promise<void>, opts?: BenchOptions): Promise<void>;++ };++ evals: {++ create(evaluation: { kind_id: string; name: string; test_suite: unknown[] }, opts?: BenchOptions): Promise<{ eval: BenchEval }>;++ get(id: string, opts?: BenchOptions): Promise<{ eval: BenchEval }>;++ run(id: string, run?: { offers?: BenchOffer[] }, opts?: BenchOptions): Promise<{ run: BenchEvalRun }>;++ results(id: string, opts?: BenchOptions): Promise<{ eval: BenchEval; runs: BenchEvalRun[] }>;++ };++ mcpTools(opts?: BenchOptions): Promise<BenchMcpTool[]>;++}++++export function createBenchClient(client: OpenVibeClient, defaults?: BenchDefaults): BenchClient;@@@ -7,6 +7,7 @@ import * as media from './media';import * as community from './community';import * as bot from './bot';import * as search from './search';++import * as bench from './bench';import * as themes from './themes';import * as chat from './chat';import * as ai from './ai';@@@ -25,6 +26,7 @@ export * as media from './media';export * as community from './community';export * as bot from './bot';export * as search from './search';++export * as bench from './bench';export * as themes from './themes';export * as chat from './chat';export * as ai from './ai';@@@ -35,6 +37,6 @@ export * as geo from './geo';declare const sdk: typeof core & {auth: typeof auth; registry: typeof registry; modules: typeof modules; realtime: typeof realtime;-- media: typeof media; community: typeof community; bot: typeof bot; search: typeof search; themes: typeof themes; chat: typeof chat; ai: typeof ai; jobs: typeof jobs; tools: typeof tools; projects: typeof projects; geo: typeof geo;++ media: typeof media; community: typeof community; bot: typeof bot; search: typeof search; bench: typeof bench; themes: typeof themes; chat: typeof chat; ai: typeof ai; jobs: typeof jobs; tools: typeof tools; projects: typeof projects; geo: typeof geo;};export default sdk;@@@ -9,6 +9,7 @@ export * as media from './media';export * as community from './community';export * as bot from './bot';export * as search from './search';++export * as bench from './bench';export * as themes from './themes';export * as chat from './chat';export * as ai from './ai';
@@ -3,6 +3,10 @@
@@ -3,6 +3,10 @@
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.71.0 (2026-10-28)
+
+- **`openvibe-sdk/bench`** (BENCH-4, plan §4.12): browser-safe Bench client for kinds, tasks, pinned rounds, outside entries, judgements, boards, model summaries, streamed open JSONL datasets and private evaluations. `mcpTools()` lists the live Bench MCP tools through `POST /mcp`. Calls use the `bench` service origin and `openvibe.bench` audience; optional `actingSubject` sends `X-OV-Subject`. HTTP errors remain `OpenVibeError`.
+
## 0.70.0 (2026-10-28)
## 0.70.0 (2026-10-28)
- **`openvibe-sdk/search`: the Search Fabric** (Contracts 0.225.0 `search.fabric.query`; plan 2026-10-28 §4.5). One
- **`openvibe-sdk/search`: the Search Fabric** (Contracts 0.225.0 `search.fabric.query`; plan 2026-10-28 §4.5). One
@@ -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.70.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.71.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.70.0
+npm install https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.71.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).
@@ -174,6 +174,7 @@ Until OpenVibe.Tools serves the gateway facade (Tools S6), point the jobs client
@@ -174,6 +174,7 @@ Until OpenVibe.Tools serves the gateway facade (Tools S6), point the jobs client
| `openvibe-sdk/community` | both | `createCommunityClient(client, {actingSubject?, origin?, sourceRef?, staff?})`: `pastes.list/iterate/get/create/update/delete/fork/like/copy/versions/byUser/config`, `pastes.comments.list/create/delete`, `as(subject)` |
| `openvibe-sdk/community` | both | `createCommunityClient(client, {actingSubject?, origin?, sourceRef?, staff?})`: `pastes.list/iterate/get/create/update/delete/fork/like/copy/versions/byUser/config`, `pastes.comments.list/create/delete`, `as(subject)` |
| `openvibe-sdk/bot` | both | `createBotClient(client, {actingSubject?})`: OpenVibe.Bot robots. `robots.list/get/create/update/delete/pairingCode`, `robots.operators.list/add/remove`, `robots.devices`, `robots.streaming.get/set`, `robots.audit`/`iterateAudit`, `robots.estop`/`clearEstop`, `robots.command` (`bot.command@1`), `devices.rotate/revoke`, public `profiles.list/get` and `kits.list/get`, `pair` (the agent's one-time code, never retried), `as(subject)`; a service acts for a person with `X-OV-Subject` (capabilities `bot.robot.read`/`manage`/`control`, `bot.device.connect`) |
| `openvibe-sdk/bot` | both | `createBotClient(client, {actingSubject?})`: OpenVibe.Bot robots. `robots.list/get/create/update/delete/pairingCode`, `robots.operators.list/add/remove`, `robots.devices`, `robots.streaming.get/set`, `robots.audit`/`iterateAudit`, `robots.estop`/`clearEstop`, `robots.command` (`bot.command@1`), `devices.rotate/revoke`, public `profiles.list/get` and `kits.list/get`, `pair` (the agent's one-time code, never retried), `as(subject)`; a service acts for a person with `X-OV-Subject` (capabilities `bot.robot.read`/`manage`/`control`, `bot.device.connect`) |
| `openvibe-sdk/themes` | both | `createThemesClient(client)`: `catalog({mode, search, sort, limit})` (seasonal themes carry `season`), `get(idOrSlug)` (null when missing), `export(idOrSlug)`, `active()` (`{theme_id, custom_variables, display}`), `set(themeId, {custom})`, `setDisplay({motion, text, seasonal})`, `seasons({date})`, `submissions()`, `submit(theme)`, `import(file)`. In the browser: `followTheme(fn)` calls back now and on every theme, display or season change with `readTheme()` (`{id, mode, tokens, display: {motion, text, textScale, seasonal, calm}, season, seasonMark}`) and returns `stop()`, for a canvas or game to keep up; `applyTheme(vars, {root})`, `seasonOn(date, seasons)`, `onColor(hex)`, `DISPLAY_OPTIONS`, `TOKENS` |
| `openvibe-sdk/themes` | both | `createThemesClient(client)`: `catalog({mode, search, sort, limit})` (seasonal themes carry `season`), `get(idOrSlug)` (null when missing), `export(idOrSlug)`, `active()` (`{theme_id, custom_variables, display}`), `set(themeId, {custom})`, `setDisplay({motion, text, seasonal})`, `seasons({date})`, `submissions()`, `submit(theme)`, `import(file)`. In the browser: `followTheme(fn)` calls back now and on every theme, display or season change with `readTheme()` (`{id, mode, tokens, display: {motion, text, textScale, seasonal, calm}, season, seasonMark}`) and returns `stop()`, for a canvas or game to keep up; `applyTheme(vars, {root})`, `seasonOn(date, seasons)`, `onColor(hex)`, `DISPLAY_OPTIONS`, `TOKENS` |
+| `openvibe-sdk/bench` | both | `createBenchClient(client, {actingSubject?})`: `kinds.list/get`, `tasks.create/get`, `rounds.create/get`, `entries.submit`, `judgements.next/cast`, `boards.get`, `models.get`, `datasets.get/iterate`, `evals.create/get/run/results`, `mcpTools()`. Audience `openvibe.bench`; service id `bench`. |
| `openvibe-sdk/search` | both | `createSearchClient(client, {actingSubject?})`: `query(text, {owner, type, lang, filter, facets, limit, cursor})`, `iterate(text, {…, max})`, `suggest(text, {owner, type, limit})`, `document(owner, type, id)` (null when missing or not visible) |
| `openvibe-sdk/search` | both | `createSearchClient(client, {actingSubject?})`: `query(text, {owner, type, lang, filter, facets, limit, cursor})`, `iterate(text, {…, max})`, `suggest(text, {owner, type, limit})`, `document(owner, type, id)` (null when missing or not visible) |
| `openvibe-sdk/limits` | node | `createActorLimiter({limits: {minute, hour, day}, actor?, maxActors?, onLimited?})` → `limits(name, {minute?, hour?, day?})` Express middleware: per-actor fixed windows at a capability boundary (service principal, else the signed-in person, else `ip:`), 429 problem+json `rate_limited` with Retry-After before the handler; `defaultActor(req)`, `.stats()` |
| `openvibe-sdk/limits` | node | `createActorLimiter({limits: {minute, hour, day}, actor?, maxActors?, onLimited?})` → `limits(name, {minute?, hour?, day?})` Express middleware: per-actor fixed windows at a capability boundary (service principal, else the signed-in person, else `ip:`), 429 problem+json `rate_limited` with Retry-After before the handler; `defaultActor(req)`, `.stats()` |
| `openvibe-sdk/account-data` | node | `createAccountData({db, service, tables: [{table, subject, value?, file?, columns?, erase?}], extraExport?, extraErase?})` → `.apply(event, {send})`, `.consumer({secrets, send, onEvent?})` (a POST /internal/events handler), `.exportPart(subject)`, `.erase(subjects)`, `.ensureSchema()`; `createNetworkSender({networkInternalUrl, clientId, clientSecret})`; `startSubscriptions({eventsUrl, endpoint, secret, networkInternalUrl, clientId, clientSecret})` (the two Events subscriptions, created at boot when missing): a service's share of account export and deletion (ADR-033), idempotent per export/deletion id |
| `openvibe-sdk/account-data` | node | `createAccountData({db, service, tables: [{table, subject, value?, file?, columns?, erase?}], extraExport?, extraErase?})` → `.apply(event, {send})`, `.consumer({secrets, send, onEvent?})` (a POST /internal/events handler), `.exportPart(subject)`, `.erase(subjects)`, `.ensureSchema()`; `createNetworkSender({networkInternalUrl, clientId, clientSecret})`; `startSubscriptions({eventsUrl, endpoint, secret, networkInternalUrl, clientId, clientSecret})` (the two Events subscriptions, created at boot when missing): a service's share of account export and deletion (ADR-033), idempotent per export/deletion id |
@@ -3,7 +3,7 @@
@@ -3,7 +3,7 @@
"stage": "alpha",
"stage": "alpha",
"code": true,
"code": true,
"wave": "W2",
"wave": "W2",
- "version": "0.70.0",
+ "version": "0.71.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",
@@ -15,7 +15,7 @@
@@ -15,7 +15,7 @@
"events: publish, pull and checkpoints by opaque cursor only, app events, webhook verification (v2), outbox and inbox",
"events: publish, pull and checkpoints by opaque cursor only, app events, webhook verification (v2), outbox and inbox",
"realtime SSE (resumes from the opaque cursor, dedupes by event_id)",
"realtime SSE (resumes from the opaque cursor, dedupes by event_id)",
"media files and objects v2",
"media files and objects v2",
- "community, bot, search, chat, ai, jobs, tools, projects, modules, identity, openre, geo, vip, quest, play and download clients",
+ "community, bot, search, bench, chat, ai, jobs, tools, projects, modules, identity, openre, geo, vip, quest, play and download clients",
"bot (openvibe-sdk/bot): OpenVibe.Bot robots, operators, devices, streaming, commands and audit, the public kits and profiles, and pairing",
"bot (openvibe-sdk/bot): OpenVibe.Bot robots, operators, devices, streaming, commands and audit, the public kits and profiles, and pairing",
"per-actor limiter for services (openvibe-sdk/limits)",
"per-actor limiter for services (openvibe-sdk/limits)",
"no-looks (openvibe-sdk/no-looks): public AI text never describes a person, one filter and corpus for every service",
"no-looks (openvibe-sdk/no-looks): public AI text never describes a person, one filter and corpus for every service",
@@ -32,5 +32,5 @@
@@ -32,5 +32,5 @@
"the telemetry sink (openvibe-sdk/telemetry): telemetrySchema() DDL, createPgTelemetrySink() and pruneTelemetry(), a per-service PostgreSQL table every service stores platform.telemetry-sample@1 into",
"the telemetry sink (openvibe-sdk/telemetry): telemetrySchema() DDL, createPgTelemetrySink() and pruneTelemetry(), a per-service PostgreSQL table every service stores platform.telemetry-sample@1 into",
"resources (openvibe-sdk/resources): the ADR-048 resource index fan-out and the idempotent control client"
"resources (openvibe-sdk/resources): the ADR-048 resource index fan-out and the idempotent control client"
],
],
- "updated": "2026-10-27"
+ "updated": "2026-10-28"
}
}
@@ -15,6 +15,7 @@ module.exports = {
@@ -15,6 +15,7 @@ module.exports = {
community: require('./src/community'),
community: require('./src/community'),
bot: require('./src/bot'),
bot: require('./src/bot'),
search: require('./src/search'),
search: require('./src/search'),
+ bench: require('./src/bench'),
themes: require('./src/themes'),
themes: require('./src/themes'),
chat: require('./src/chat'),
chat: require('./src/chat'),
ai: require('./src/ai'),
ai: require('./src/ai'),
@@ -1,6 +1,6 @@
@@ -1,6 +1,6 @@
-// Generated by scripts/browser-bundle.js from browser.js (openvibe-sdk 0.70.0, MIT). Do not edit.
+// Generated by scripts/browser-bundle.js from browser.js (openvibe-sdk 0.71.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, bench, themes, chat, ai, jobs, tools, projects, geo). No server-only code: no service tokens, JWT verification, events, identity.
/* eslint-disable */
/* eslint-disable */
const __defs = [
const __defs = [
// ── 0: browser.js ──────────────────────────────────────────────────
// ── 0: browser.js ──────────────────────────────────────────────────
@@ -22,13 +22,14 @@ module.exports = {
@@ -22,13 +22,14 @@ module.exports = {
community: __load(14),
community: __load(14),
bot: __load(15),
bot: __load(15),
search: __load(16),
search: __load(16),
- themes: __load(17),
- chat: __load(18),
- ai: __load(19),
- jobs: __load(20),
- tools: __load(22),
- projects: __load(23),
- geo: __load(24),
+ bench: __load(17),
+ themes: __load(18),
+ chat: __load(19),
+ ai: __load(20),
+ jobs: __load(21),
+ tools: __load(23),
+ projects: __load(24),
+ geo: __load(25),
};
};
},
},
// ── 1: src/core/index.js ───────────────────────────────────────────
// ── 1: src/core/index.js ───────────────────────────────────────────
@@ -43,7 +44,7 @@ const { paginate, offsetPager } = __load(7);
@@ -43,7 +44,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.70.0';
+const SDK_VERSION = '0.71.0';
module.exports = {
module.exports = {
createClient,
createClient,
@@ -2090,7 +2091,108 @@ function createSearchClient(client, defaults = {}) {
@@ -2090,7 +2091,108 @@ function createSearchClient(client, defaults = {}) {
module.exports = { createSearchClient, queryOf, fabricOf };
module.exports = { createSearchClient, queryOf, fabricOf };
},
},
-// ── 17: src/themes.js ───────────────────────────────────────────────
+// ── 17: src/bench.js ────────────────────────────────────────────────
+function (module, exports) {
+'use strict';
+/** openvibe-sdk/bench: OpenVibe.Bench's JSON API and streamed open dataset. Browser-safe. */
+const { isActingSubjectId } = __load(5);
+const { OpenVibeError } = __load(3);
+
+const enc = encodeURIComponent;
+const path = (part, id) => `/api/v1/${part}/${enc(id)}`;
+
+function createBenchClient(client, defaults = {}) {
+ function options(opts = {}, perCall = {}) {
+ const acting = perCall.actingSubject !== undefined ? perCall.actingSubject : defaults.actingSubject;
+ const headers = { ...opts.headers };
+ if (acting) {
+ if (!isActingSubjectId(acting)) throw new TypeError('actingSubject must be a usr_… or gst_… subject id');
+ headers['X-OV-Subject'] = acting;
+ }
+ return { service: 'bench', baseUrl: defaults.baseUrl, audience: 'openvibe.bench', ...opts, headers, signal: perCall.signal };
+ }
+ const json = (opts, perCall) => client.json(options(opts, perCall));
+ const post = (route, body, o) => json({ method: 'POST', path: route, json: body }, o);
+ const datasetPath = (kind) => `${path('datasets', kind)}.jsonl`;
+
+ return {
+ kinds: {
+ list: (o = {}) => json({ path: '/api/v1/kinds' }, o),
+ get: (kind, o = {}) => json({ path: path('kinds', kind) }, o),
+ },
+ tasks: {
+ create: (kind, task, o = {}) => post(`${path('kinds', kind)}/tasks`, task, o),
+ get: (id, o = {}) => json({ path: path('tasks', id) }, o),
+ },
+ rounds: {
+ create: (taskId, round, o = {}) => post(`${path('tasks', taskId)}/rounds`, round, o),
+ get: (id, o = {}) => json({ path: path('rounds', id) }, o),
+ },
+ entries: {
+ submit: (roundId, entry, o = {}) => post(`${path('rounds', roundId)}/entries`, entry, o),
+ },
+ judgements: {
+ next: ({ kind, ...o } = {}) => json({ path: '/api/v1/judgements/next', query: { kind } }, o),
+ cast: (roundId, judgement, o = {}) => post(`${path('rounds', roundId)}/judgements`, judgement, o),
+ },
+ boards: {
+ get: (kind, { type, category, period, ...o } = {}) => json({ path: path('boards', kind), query: { type, category, period } }, o),
+ },
+ models: {
+ get: (model, o = {}) => json({ path: path('models', model) }, o),
+ },
+ datasets: {
+ get: (kind, o = {}) => client.json(options({ path: datasetPath(kind), responseType: 'text' }, o)),
+ async iterate(kind, onLine, o = {}) {
+ if (typeof onLine !== 'function') throw new TypeError('onLine must be a function');
+ const { response } = await client.request(options({ path: datasetPath(kind), responseType: 'response' }, o));
+ const reader = response.body.getReader();
+ const decoder = new TextDecoder();
+ let pending = '';
+ let line = 0;
+ const emit = async (s) => {
+ if (s.endsWith('\r')) s = s.slice(0, -1);
+ if (!s.trim()) return;
+ line++;
+ let value;
+ try { value = JSON.parse(s); }
+ catch (cause) { throw new OpenVibeError({ code: 'sdk.bad_response', message: `Bench dataset has invalid JSON at line ${line}`, cause }); }
+ await onLine(value);
+ };
+ try {
+ for (;;) {
+ const { value, done } = await reader.read();
+ if (done) break;
+ pending += decoder.decode(value, { stream: true });
+ let pos;
+ while ((pos = pending.indexOf('\n')) !== -1) {
+ await emit(pending.slice(0, pos));
+ pending = pending.slice(pos + 1);
+ }
+ }
+ pending += decoder.decode();
+ await emit(pending);
+ } finally { reader.releaseLock(); }
+ },
+ },
+ evals: {
+ create: (evaluation, o = {}) => post('/api/v1/evals', evaluation, o),
+ get: (id, o = {}) => json({ path: path('evals', id) }, o),
+ run: (id, run, o = {}) => post(`${path('evals', id)}/run`, run, o),
+ results: (id, o = {}) => json({ path: `${path('evals', id)}/results` }, o),
+ },
+ async mcpTools(o = {}) {
+ const data = await post('/mcp', { jsonrpc: '2.0', id: 1, method: 'tools/list', params: {} }, o);
+ if (data && data.error) throw new OpenVibeError({ code: 'sdk.bad_response', message: data.error.message || 'Bench MCP tools/list failed', detail: data.error.message });
+ if (!data || !data.result || !Array.isArray(data.result.tools)) throw new OpenVibeError({ code: 'sdk.bad_response', message: 'Bench MCP tools/list returned no tools' });
+ return data.result.tools;
+ },
+ };
+}
+
+module.exports = { createBenchClient };
+},
+// ── 18: src/themes.js ───────────────────────────────────────────────
function (module, exports) {
function (module, exports) {
'use strict';
'use strict';
/**
/**
@@ -2240,7 +2342,7 @@ function followTheme(fn, opts = {}) {
@@ -2240,7 +2342,7 @@ function followTheme(fn, opts = {}) {
module.exports = { createThemesClient, followTheme, readTheme, applyTheme, seasonOn, onColor, DISPLAY_OPTIONS, TOKENS };
module.exports = { createThemesClient, followTheme, readTheme, applyTheme, seasonOn, onColor, DISPLAY_OPTIONS, TOKENS };
},
},
-// ── 18: src/chat.js ─────────────────────────────────────────────────
+// ── 19: src/chat.js ─────────────────────────────────────────────────
function (module, exports) {
function (module, exports) {
'use strict';
'use strict';
/**
/**
@@ -2482,7 +2584,7 @@ function createFeedPublisher(client, { feed, key, baseUrl } = {}) {
@@ -2482,7 +2584,7 @@ function createFeedPublisher(client, { feed, key, baseUrl } = {}) {
module.exports = { createChatClient, createInboxVisitor, createFeedPublisher };
module.exports = { createChatClient, createInboxVisitor, createFeedPublisher };
},
},
-// ── 19: src/ai.js ───────────────────────────────────────────────────
+// ── 20: src/ai.js ───────────────────────────────────────────────────
function (module, exports) {
function (module, exports) {
'use strict';
'use strict';
/**
/**
@@ -2557,7 +2659,7 @@ function createAiClient(client, defaults = {}) {
@@ -2557,7 +2659,7 @@ function createAiClient(client, defaults = {}) {
module.exports = { createAiClient, TERMINAL, OPS };
module.exports = { createAiClient, TERMINAL, OPS };
},
},
-// ── 20: src/jobs.js ─────────────────────────────────────────────────
+// ── 21: src/jobs.js ─────────────────────────────────────────────────
function (module, exports) {
function (module, exports) {
'use strict';
'use strict';
/**
/**
@@ -2595,7 +2697,7 @@ function (module, exports) {
@@ -2595,7 +2697,7 @@ function (module, exports) {
*/
*/
const { OpenVibeError, isOpenVibeError } = __load(3);
const { OpenVibeError, isOpenVibeError } = __load(3);
const { newIdempotencyKey } = __load(5);
const { newIdempotencyKey } = __load(5);
-const { appendFiles } = __load(21);
+const { appendFiles } = __load(22);
const { parseSSE } = __load(11);
const { parseSSE } = __load(11);
const TERMINAL = new Set(['succeeded', 'failed', 'cancelled']);
const TERMINAL = new Set(['succeeded', 'failed', 'cancelled']);
@@ -2759,7 +2861,7 @@ function createJobsClient(client, { baseUrl, service = 'tools', audience = 'open
@@ -2759,7 +2861,7 @@ function createJobsClient(client, { baseUrl, service = 'tools', audience = 'open
module.exports = { createJobsClient, isTerminal, TERMINAL_STATES: [...TERMINAL] };
module.exports = { createJobsClient, isTerminal, TERMINAL_STATES: [...TERMINAL] };
},
},
-// ── 21: src/core/form.js ────────────────────────────────────────────
+// ── 22: src/core/form.js ────────────────────────────────────────────
function (module, exports) {
function (module, exports) {
'use strict';
'use strict';
/**
/**
@@ -2793,7 +2895,7 @@ function appendFiles(form, list, where) {
@@ -2793,7 +2895,7 @@ function appendFiles(form, list, where) {
module.exports = { isBlob, isUpload, appendFiles };
module.exports = { isBlob, isUpload, appendFiles };
},
},
-// ── 22: src/tools.js ────────────────────────────────────────────────
+// ── 23: src/tools.js ────────────────────────────────────────────────
function (module, exports) {
function (module, exports) {
'use strict';
'use strict';
/**
/**
@@ -2827,8 +2929,8 @@ function (module, exports) {
@@ -2827,8 +2929,8 @@ function (module, exports) {
*/
*/
const { OpenVibeError, isOpenVibeError } = __load(3);
const { OpenVibeError, isOpenVibeError } = __load(3);
const { newIdempotencyKey } = __load(5);
const { newIdempotencyKey } = __load(5);
-const { isUpload, appendFiles } = __load(21);
-const { createJobsClient } = __load(20);
+const { isUpload, appendFiles } = __load(22);
+const { createJobsClient } = __load(21);
const enc = encodeURIComponent;
const enc = encodeURIComponent;
const ID_RE = /^[a-z][a-z0-9-]{0,39}$/;
const ID_RE = /^[a-z][a-z0-9-]{0,39}$/;
@@ -3078,7 +3180,7 @@ function createToolsClient(client, { baseUrl, service = 'tools', audience = 'ope
@@ -3078,7 +3180,7 @@ function createToolsClient(client, { baseUrl, service = 'tools', audience = 'ope
module.exports = { createToolsClient, ToolRunError, isToolRunError };
module.exports = { createToolsClient, ToolRunError, isToolRunError };
},
},
-// ── 23: src/projects.js ─────────────────────────────────────────────
+// ── 24: src/projects.js ─────────────────────────────────────────────
function (module, exports) {
function (module, exports) {
'use strict';
'use strict';
/**
/**
@@ -3194,7 +3296,7 @@ function createProjectsClient(client, { baseUrl } = {}) {
@@ -3194,7 +3296,7 @@ function createProjectsClient(client, { baseUrl } = {}) {
module.exports = { createProjectsClient };
module.exports = { createProjectsClient };
},
},
-// ── 24: src/geo.js ──────────────────────────────────────────────────
+// ── 25: src/geo.js ──────────────────────────────────────────────────
function (module, exports) {
function (module, exports) {
'use strict';
'use strict';
/**
/**
@@ -3274,7 +3376,7 @@ export const {
@@ -3274,7 +3376,7 @@ export const {
startSpan, contextFromHeaders, ulid, newEventId, newIdempotencyKey, isActingSubjectId,
startSpan, contextFromHeaders, ulid, newEventId, newIdempotencyKey, isActingSubjectId,
satisfiesRange, compareVersions, CONTRACTS_RANGE, DEFAULT_NETWORK, SDK_VERSION, auth,
satisfiesRange, compareVersions, CONTRACTS_RANGE, DEFAULT_NETWORK, SDK_VERSION, auth,
registry, modules, realtime, media, community, bot,
registry, modules, realtime, media, community, bot,
- search, themes, chat, ai, jobs, tools,
- projects, geo,
+ search, bench, themes, chat, ai, jobs,
+ tools, projects, geo,
} = sdk;
} = sdk;
export default sdk;
export default sdk;
@@ -43,6 +43,11 @@
@@ -43,6 +43,11 @@
"verifyUserToken"
"verifyUserToken"
]
]
},
},
+ "bench": {
+ "object": [
+ "createBenchClient"
+ ]
+ },
"blog": {
"blog": {
"object": [
"object": [
"createBlogClient"
"createBlogClient"
@@ -374,6 +379,9 @@
@@ -374,6 +379,9 @@
"startAuthorization": "function",
"startAuthorization": "function",
"unverifiedClaims": "function"
"unverifiedClaims": "function"
},
},
+ "openvibe-sdk/bench": {
+ "createBenchClient": "function"
+ },
"openvibe-sdk/blog": {
"openvibe-sdk/blog": {
"createBlogClient": "function"
"createBlogClient": "function"
},
},
new file mode 100644
new file mode 100644
@@ -0,0 +1,7 @@
@@ -0,0 +1,7 @@
+// Generated by scripts/esm.js from src/bench.js. Do not edit.
+import cjs from '../src/bench.js';
+
+export const {
+ createBenchClient,
+} = cjs;
+export default cjs;
@@ -6,12 +6,12 @@ export const {
@@ -6,12 +6,12 @@ export const {
startSpan, contextFromHeaders, ulid, newEventId, newIdempotencyKey, isActingSubjectId,
startSpan, contextFromHeaders, ulid, newEventId, newIdempotencyKey, isActingSubjectId,
satisfiesRange, compareVersions, CONTRACTS_RANGE, DEFAULT_NETWORK, SDK_VERSION, auth,
satisfiesRange, compareVersions, CONTRACTS_RANGE, DEFAULT_NETWORK, SDK_VERSION, auth,
registry, identity, modules, events, realtime, media,
registry, identity, modules, events, realtime, media,
- community, bot, search, themes, chat, ai,
- jobs, tools, projects, resources, vip, quest,
- live, inventory, tips, blog, reviews, wiki,
- coupons, deals, trade, space, host, news,
- food, actor, run, video, work, zone,
- play, download, openre, geo, usage, commerce,
- telemetry,
+ community, bot, search, bench, themes, chat,
+ ai, jobs, tools, projects, resources, vip,
+ quest, live, inventory, tips, blog, reviews,
+ wiki, coupons, deals, trade, space, host,
+ news, food, actor, run, video, work,
+ zone, play, download, openre, geo, usage,
+ commerce, telemetry,
} = cjs;
} = cjs;
export default cjs;
export default cjs;
@@ -18,6 +18,7 @@ module.exports = {
@@ -18,6 +18,7 @@ module.exports = {
community: require('./src/community'),
community: require('./src/community'),
bot: require('./src/bot'),
bot: require('./src/bot'),
search: require('./src/search'),
search: require('./src/search'),
+ bench: require('./src/bench'),
themes: require('./src/themes'),
themes: require('./src/themes'),
chat: require('./src/chat'),
chat: require('./src/chat'),
ai: require('./src/ai'),
ai: require('./src/ai'),
@@ -1,12 +1,12 @@
@@ -1,12 +1,12 @@
{
{
"name": "openvibe-sdk",
"name": "openvibe-sdk",
- "version": "0.70.0",
+ "version": "0.71.0",
"lockfileVersion": 3,
"lockfileVersion": 3,
"requires": true,
"requires": true,
"packages": {
"packages": {
"": {
"": {
"name": "openvibe-sdk",
"name": "openvibe-sdk",
- "version": "0.70.0",
+ "version": "0.71.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.70.0",
+ "version": "0.71.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",
@@ -87,6 +87,11 @@
@@ -87,6 +87,11 @@
"import": "./esm/search.mjs",
"import": "./esm/search.mjs",
"default": "./src/search.js"
"default": "./src/search.js"
},
},
+ "./bench": {
+ "types": "./types/bench.d.ts",
+ "import": "./esm/bench.mjs",
+ "default": "./src/bench.js"
+ },
"./themes": {
"./themes": {
"types": "./types/themes.d.ts",
"types": "./types/themes.d.ts",
"import": "./esm/themes.mjs",
"import": "./esm/themes.mjs",
@@ -27,6 +27,7 @@ const ENTRIES = {
@@ -27,6 +27,7 @@ const ENTRIES = {
'community.mjs': '../src/community.js',
'community.mjs': '../src/community.js',
'bot.mjs': '../src/bot.js',
'bot.mjs': '../src/bot.js',
'search.mjs': '../src/search.js',
'search.mjs': '../src/search.js',
+ 'bench.mjs': '../src/bench.js',
'themes.mjs': '../src/themes.js',
'themes.mjs': '../src/themes.js',
'limits.mjs': '../src/limits.js',
'limits.mjs': '../src/limits.js',
'account-data.mjs': '../src/account-data.js',
'account-data.mjs': '../src/account-data.js',
new file mode 100644
new file mode 100644
@@ -0,0 +1,98 @@
@@ -0,0 +1,98 @@
+'use strict';
+/** openvibe-sdk/bench: OpenVibe.Bench's JSON API and streamed open dataset. Browser-safe. */
+const { isActingSubjectId } = require('./core/ids');
+const { OpenVibeError } = require('./core/errors');
+
+const enc = encodeURIComponent;
+const path = (part, id) => `/api/v1/${part}/${enc(id)}`;
+
+function createBenchClient(client, defaults = {}) {
+ function options(opts = {}, perCall = {}) {
+ const acting = perCall.actingSubject !== undefined ? perCall.actingSubject : defaults.actingSubject;
+ const headers = { ...opts.headers };
+ if (acting) {
+ if (!isActingSubjectId(acting)) throw new TypeError('actingSubject must be a usr_… or gst_… subject id');
+ headers['X-OV-Subject'] = acting;
+ }
+ return { service: 'bench', baseUrl: defaults.baseUrl, audience: 'openvibe.bench', ...opts, headers, signal: perCall.signal };
+ }
+ const json = (opts, perCall) => client.json(options(opts, perCall));
+ const post = (route, body, o) => json({ method: 'POST', path: route, json: body }, o);
+ const datasetPath = (kind) => `${path('datasets', kind)}.jsonl`;
+
+ return {
+ kinds: {
+ list: (o = {}) => json({ path: '/api/v1/kinds' }, o),
+ get: (kind, o = {}) => json({ path: path('kinds', kind) }, o),
+ },
+ tasks: {
+ create: (kind, task, o = {}) => post(`${path('kinds', kind)}/tasks`, task, o),
+ get: (id, o = {}) => json({ path: path('tasks', id) }, o),
+ },
+ rounds: {
+ create: (taskId, round, o = {}) => post(`${path('tasks', taskId)}/rounds`, round, o),
+ get: (id, o = {}) => json({ path: path('rounds', id) }, o),
+ },
+ entries: {
+ submit: (roundId, entry, o = {}) => post(`${path('rounds', roundId)}/entries`, entry, o),
+ },
+ judgements: {
+ next: ({ kind, ...o } = {}) => json({ path: '/api/v1/judgements/next', query: { kind } }, o),
+ cast: (roundId, judgement, o = {}) => post(`${path('rounds', roundId)}/judgements`, judgement, o),
+ },
+ boards: {
+ get: (kind, { type, category, period, ...o } = {}) => json({ path: path('boards', kind), query: { type, category, period } }, o),
+ },
+ models: {
+ get: (model, o = {}) => json({ path: path('models', model) }, o),
+ },
+ datasets: {
+ get: (kind, o = {}) => client.json(options({ path: datasetPath(kind), responseType: 'text' }, o)),
+ async iterate(kind, onLine, o = {}) {
+ if (typeof onLine !== 'function') throw new TypeError('onLine must be a function');
+ const { response } = await client.request(options({ path: datasetPath(kind), responseType: 'response' }, o));
+ const reader = response.body.getReader();
+ const decoder = new TextDecoder();
+ let pending = '';
+ let line = 0;
+ const emit = async (s) => {
+ if (s.endsWith('\r')) s = s.slice(0, -1);
+ if (!s.trim()) return;
+ line++;
+ let value;
+ try { value = JSON.parse(s); }
+ catch (cause) { throw new OpenVibeError({ code: 'sdk.bad_response', message: `Bench dataset has invalid JSON at line ${line}`, cause }); }
+ await onLine(value);
+ };
+ try {
+ for (;;) {
+ const { value, done } = await reader.read();
+ if (done) break;
+ pending += decoder.decode(value, { stream: true });
+ let pos;
+ while ((pos = pending.indexOf('\n')) !== -1) {
+ await emit(pending.slice(0, pos));
+ pending = pending.slice(pos + 1);
+ }
+ }
+ pending += decoder.decode();
+ await emit(pending);
+ } finally { reader.releaseLock(); }
+ },
+ },
+ evals: {
+ create: (evaluation, o = {}) => post('/api/v1/evals', evaluation, o),
+ get: (id, o = {}) => json({ path: path('evals', id) }, o),
+ run: (id, run, o = {}) => post(`${path('evals', id)}/run`, run, o),
+ results: (id, o = {}) => json({ path: `${path('evals', id)}/results` }, o),
+ },
+ async mcpTools(o = {}) {
+ const data = await post('/mcp', { jsonrpc: '2.0', id: 1, method: 'tools/list', params: {} }, o);
+ if (data && data.error) throw new OpenVibeError({ code: 'sdk.bad_response', message: data.error.message || 'Bench MCP tools/list failed', detail: data.error.message });
+ if (!data || !data.result || !Array.isArray(data.result.tools)) throw new OpenVibeError({ code: 'sdk.bad_response', message: 'Bench MCP tools/list returned no tools' });
+ return data.result.tools;
+ },
+ };
+}
+
+module.exports = { createBenchClient };
@@ -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.70.0';
+const SDK_VERSION = '0.71.0';
module.exports = {
module.exports = {
createClient,
createClient,
new file mode 100644
new file mode 100644
@@ -0,0 +1,102 @@
@@ -0,0 +1,102 @@
+'use strict';
+/** Bench API routes, envelopes, streaming JSONL, MCP, auth and problem errors. */
+const assert = require('node:assert/strict');
+const { stubServer, send, problem, run } = require('./helpers');
+const { createClient, OpenVibeError } = require('../src/core');
+const { createBenchClient } = require('../src/bench');
+
+const USR = 'usr_01JAB2C3D4E5F6G7H8J9K0MNPQ';
+
+async function bench() {
+ return stubServer((req, res, body) => {
+ const u = new URL(req.url, 'http://x');
+ if (u.pathname === '/api/v1/datasets/vision.jsonl') {
+ res.writeHead(200, { 'Content-Type': 'application/x-ndjson' });
+ res.write('{"type":"task","id":"t1"}\n{"type":"rou');
+ res.end('nd","id":"r1"}\n');
+ return;
+ }
+ if (u.pathname === '/api/v1/kinds/missing') return problem(res, 404, 'bench.kind_not_found', 'No such kind.');
+ if (u.pathname === '/mcp') return send(res, 200, { jsonrpc: '2.0', id: 1, result: { tools: [{ name: 'bench.list_kinds', inputSchema: { type: 'object' } }] } });
+ const responses = {
+ '/api/v1/kinds': { count: 1, kinds: [{ id: 'vision' }] },
+ '/api/v1/kinds/vision': { kind: { id: 'vision' } },
+ '/api/v1/kinds/vision/tasks': { task: { id: 't1' } },
+ '/api/v1/tasks/t1': { task: { id: 't1' } },
+ '/api/v1/tasks/t1/rounds': { round: { id: 'r1' } },
+ '/api/v1/rounds/r1': { round: { id: 'r1' } },
+ '/api/v1/rounds/r1/entries': { entry: { id: 'e1' } },
+ '/api/v1/judgements/next': { pair: null, detail: 'No round' },
+ '/api/v1/rounds/r1/judgements': { judgement: { id: 'j1' } },
+ '/api/v1/boards/vision': { board: { type: 'pareto', rows: [] } },
+ '/api/v1/models/model%2Fone': { model: 'model/one', ratings: [], entries: [], total_cost_usd: 0 },
+ '/api/v1/evals': { eval: { id: 'v1' } },
+ '/api/v1/evals/v1': { eval: { id: 'v1' } },
+ '/api/v1/evals/v1/run': { run: { id: 'run1' } },
+ '/api/v1/evals/v1/results': { eval: { id: 'v1' }, runs: [] },
+ };
+ if (Object.hasOwn(responses, u.pathname)) return send(res, req.method === 'POST' ? 201 : 200, responses[u.pathname]);
+ return problem(res, 404, 'bench.not_found', 'Missing');
+ });
+}
+
+run([
+ ['all JSON routes preserve Bench envelopes, paths, query and bodies', async () => {
+ const srv = await bench();
+ try {
+ const b = createBenchClient(createClient({ baseUrls: { bench: srv.url }, retries: 0 }));
+ assert.equal((await b.kinds.list()).count, 1);
+ assert.equal((await b.kinds.get('vision')).kind.id, 'vision');
+ assert.equal((await b.tasks.create('vision', { prompt: 'Compare', task_class: 'image', public: true })).task.id, 't1');
+ assert.equal((await b.tasks.get('t1')).task.id, 't1');
+ assert.equal((await b.rounds.create('t1', { offers: [{ provider: 'ai', model: 'one' }], seed: 7 })).round.id, 'r1');
+ assert.equal((await b.rounds.get('r1')).round.id, 'r1');
+ assert.equal((await b.entries.submit('r1', { provider: 'ai', model: 'one', payload: { text: 'hello' }, cost_usd: 0.01 })).entry.id, 'e1');
+ assert.equal((await b.judgements.next({ kind: 'vision' })).pair, null);
+ assert.equal((await b.judgements.cast('r1', { judge: 'human_pairwise', pair: { left: 'e1', right: 'e2' }, outcome: 'left' })).judgement.id, 'j1');
+ assert.equal((await b.boards.get('vision', { type: 'pareto', category: 'all', period: 'month' })).board.type, 'pareto');
+ assert.equal((await b.models.get('model/one')).model, 'model/one');
+ assert.equal((await b.evals.create({ kind_id: 'vision', name: 'Private', test_suite: [{ prompt: 'x' }] })).eval.id, 'v1');
+ assert.equal((await b.evals.get('v1')).eval.id, 'v1');
+ assert.equal((await b.evals.run('v1', { offers: [{ provider: 'ai' }] })).run.id, 'run1');
+ assert.deepEqual((await b.evals.results('v1')).runs, []);
+ assert.deepEqual(srv.requests.map((r) => [r.method, r.url]), [
+ ['GET', '/api/v1/kinds'], ['GET', '/api/v1/kinds/vision'],
+ ['POST', '/api/v1/kinds/vision/tasks'], ['GET', '/api/v1/tasks/t1'],
+ ['POST', '/api/v1/tasks/t1/rounds'], ['GET', '/api/v1/rounds/r1'],
+ ['POST', '/api/v1/rounds/r1/entries'], ['GET', '/api/v1/judgements/next?kind=vision'],
+ ['POST', '/api/v1/rounds/r1/judgements'], ['GET', '/api/v1/boards/vision?type=pareto&category=all&period=month'],
+ ['GET', '/api/v1/models/model%2Fone'], ['POST', '/api/v1/evals'],
+ ['GET', '/api/v1/evals/v1'], ['POST', '/api/v1/evals/v1/run'], ['GET', '/api/v1/evals/v1/results'],
+ ]);
+ assert.deepEqual(JSON.parse(srv.requests[2].body), { prompt: 'Compare', task_class: 'image', public: true });
+ assert.deepEqual(JSON.parse(srv.requests[4].body), { offers: [{ provider: 'ai', model: 'one' }], seed: 7 });
+ assert.deepEqual(JSON.parse(srv.requests[6].body), { provider: 'ai', model: 'one', payload: { text: 'hello' }, cost_usd: 0.01 });
+ assert.deepEqual(JSON.parse(srv.requests[8].body), { judge: 'human_pairwise', pair: { left: 'e1', right: 'e2' }, outcome: 'left' });
+ } finally { await srv.close(); }
+ }],
+ ['dataset text and streamed lines; MCP tools list; service audience and acting subject', async () => {
+ const srv = await bench();
+ try {
+ const b = createBenchClient(createClient({ baseUrls: { bench: srv.url }, getToken: ({ audience }) => `svc-${audience}` }), { actingSubject: USR });
+ assert.match(await b.datasets.get('vision'), /"type":"task"/);
+ const lines = [];
+ await b.datasets.iterate('vision', async (line) => { lines.push(line); });
+ assert.deepEqual(lines, [{ type: 'task', id: 't1' }, { type: 'round', id: 'r1' }]);
+ assert.equal((await b.mcpTools())[0].name, 'bench.list_kinds');
+ assert.deepEqual(JSON.parse(srv.requests[2].body), { jsonrpc: '2.0', id: 1, method: 'tools/list', params: {} });
+ assert.equal(srv.requests[0].headers.authorization, 'Bearer svc-openvibe.bench');
+ assert.equal(srv.requests[0].headers['x-ov-subject'], USR);
+ assert.equal(srv.requests[2].headers['x-ov-subject'], USR);
+ } finally { await srv.close(); }
+ }],
+ ['problem errors are OpenVibeError; invalid acting subjects fail before the request', async () => {
+ const srv = await bench();
+ try {
+ const b = createBenchClient(createClient({ baseUrls: { bench: srv.url }, retries: 0 }));
+ await assert.rejects(b.kinds.get('missing'), (err) => err instanceof OpenVibeError && err.code === 'bench.kind_not_found' && err.status === 404);
+ assert.throws(() => b.kinds.list({ actingSubject: 'bad' }), /actingSubject/);
+ assert.equal(srv.requests.length, 1);
+ } finally { await srv.close(); }
+ }],
+]);
@@ -68,7 +68,7 @@ run([
@@ -68,7 +68,7 @@ run([
})().catch((err) => { console.error(err); process.exit(1); });`;
})().catch((err) => { console.error(err); process.exit(1); });`;
const res = execFileSync(process.execPath, ['-e', script], { cwd: app, encoding: 'utf8' });
const res = execFileSync(process.execPath, ['-e', script], { cwd: app, encoding: 'utf8' });
assert.match(res, new RegExp(`consumer ok: ${subpaths.length} subpaths \\+ the browser bundle`));
assert.match(res, new RegExp(`consumer ok: ${subpaths.length} subpaths \\+ the browser bundle`));
- assert.equal(subpaths.length, 68);
+ assert.equal(subpaths.length, 69);
assert.ok(!fs.existsSync(path.join(app, 'node_modules/openvibe-contracts')), 'the optional peer is not installed');
assert.ok(!fs.existsSync(path.join(app, 'node_modules/openvibe-contracts')), 'the optional peer is not installed');
} finally {
} finally {
fs.rmSync(tmp, { recursive: true, force: true });
fs.rmSync(tmp, { recursive: true, force: true });
new file mode 100644
new file mode 100644
@@ -0,0 +1,68 @@
@@ -0,0 +1,68 @@
+import type { OpenVibeClient } from './core';
+
+export interface BenchOptions { actingSubject?: string; signal?: AbortSignal; }
+export interface BenchDefaults extends BenchOptions { baseUrl?: string; }
+export interface BenchKind { id: string; room: string; entry: string; judge: string; name: string; schema: Record<string, unknown>; metrics: unknown[]; created_at: string | null; }
+export interface BenchTask { id: string; kind_id: string; prompt: string; task_class: string | null; public: boolean; created_by?: string; created_at: string; }
+export interface BenchOffer { provider: string; model?: string | null; }
+export interface BenchEntry {
+ id: string; round_id: string; provider?: string; model?: string;
+ route?: Record<string, unknown>; payload: Record<string, unknown>;
+ cost_usd?: number; ms?: number; tokens?: number; error: string | null;
+ ui_check: Record<string, unknown> | null; run_id: string | null; created_at: string;
+}
+export interface BenchRound {
+ id: string; task_id: string; kind_id: string; offers: BenchOffer[]; status: string;
+ seed: number; params: Record<string, unknown>; pool_fallback: boolean; receipt: unknown | null;
+ created_by?: string; created_at: string; finished_at: string | null;
+ entries?: BenchEntry[]; votes?: number; blind?: boolean;
+ [field: string]: unknown;
+}
+export interface BenchJudgementInput {
+ kind_id?: string; judge?: string; judge_name?: string; judge_version?: string;
+ pair?: { left: string; right: string }; outcome?: 'left' | 'right' | 'tie' | 'both_bad' | 'draw'; score?: number;
+}
+export interface BenchBoard { kind_id: string; type: 'elo' | 'pareto' | 'cost'; category: string; period: string; rows: Array<Record<string, unknown>>; }
+export interface BenchEval { id: string; owner: string; kind_id: string; name: string; test_suite: unknown[]; created_at: string; }
+export interface BenchEvalRun { id: string; eval_id: string; offers: BenchOffer[]; status: string; created_at: string; [field: string]: unknown; }
+export type BenchDatasetLine = ({ type: 'task' } & BenchTask) | ({ type: 'round' } & BenchRound) | ({ type: 'entry' } & BenchEntry) | ({ type: 'judgement' | 'rating' } & Record<string, unknown>);
+export interface BenchMcpTool { name: string; description?: string; inputSchema: Record<string, unknown>; [field: string]: unknown; }
+
+export interface BenchClient {
+ kinds: {
+ list(opts?: BenchOptions): Promise<{ count: number; kinds: BenchKind[] }>;
+ get(kind: string, opts?: BenchOptions): Promise<{ kind: BenchKind }>;
+ };
+ tasks: {
+ create(kind: string, task: { prompt: string; task_class?: string; public?: boolean }, opts?: BenchOptions): Promise<{ task: BenchTask }>;
+ get(id: string, opts?: BenchOptions): Promise<{ task: BenchTask }>;
+ };
+ rounds: {
+ create(taskId: string, round?: { offers?: BenchOffer[]; seed?: number }, opts?: BenchOptions): Promise<{ round: BenchRound }>;
+ get(id: string, opts?: BenchOptions): Promise<{ round: BenchRound }>;
+ };
+ entries: {
+ submit(roundId: string, entry: { provider?: string; model?: string; route?: Record<string, unknown>; payload?: Record<string, unknown>; cost_usd?: number; ms?: number; tokens?: number; error?: string | null; ui_check?: Record<string, unknown> | null; run_id?: string | null }, opts?: BenchOptions): Promise<{ entry: BenchEntry }>;
+ };
+ judgements: {
+ next(query: { kind: string } & BenchOptions): Promise<{ round: { id: string; kind_id: string; prompt: string }; left: { id: string; payload: Record<string, unknown> }; right: { id: string; payload: Record<string, unknown> } } | { pair: null; detail: string }>;
+ cast(roundId: string, judgement: BenchJudgementInput, opts?: BenchOptions): Promise<{ judgement: Record<string, unknown>; revealed: { left: BenchEntry; right: BenchEntry } | null }>;
+ };
+ boards: { get(kind: string, query?: { type?: 'elo' | 'pareto' | 'cost'; category?: string; period?: string } & BenchOptions): Promise<{ board: BenchBoard }> };
+ models: { get(model: string, opts?: BenchOptions): Promise<{ model: string; ratings: Array<Record<string, unknown>>; entries: BenchEntry[]; total_cost_usd: number }> };
+ datasets: {
+ /** Raw application/x-ndjson text. */
+ get(kind: string, opts?: BenchOptions): Promise<string>;
+ /** Parses each JSONL object as bytes arrive and awaits onLine. */
+ iterate(kind: string, onLine: (line: BenchDatasetLine) => void | Promise<void>, opts?: BenchOptions): Promise<void>;
+ };
+ evals: {
+ create(evaluation: { kind_id: string; name: string; test_suite: unknown[] }, opts?: BenchOptions): Promise<{ eval: BenchEval }>;
+ get(id: string, opts?: BenchOptions): Promise<{ eval: BenchEval }>;
+ run(id: string, run?: { offers?: BenchOffer[] }, opts?: BenchOptions): Promise<{ run: BenchEvalRun }>;
+ results(id: string, opts?: BenchOptions): Promise<{ eval: BenchEval; runs: BenchEvalRun[] }>;
+ };
+ mcpTools(opts?: BenchOptions): Promise<BenchMcpTool[]>;
+}
+
+export function createBenchClient(client: OpenVibeClient, defaults?: BenchDefaults): BenchClient;
@@ -7,6 +7,7 @@ import * as media from './media';
@@ -7,6 +7,7 @@ import * as media from './media';
import * as community from './community';
import * as community from './community';
import * as bot from './bot';
import * as bot from './bot';
import * as search from './search';
import * as search from './search';
+import * as bench from './bench';
import * as themes from './themes';
import * as themes from './themes';
import * as chat from './chat';
import * as chat from './chat';
import * as ai from './ai';
import * as ai from './ai';
@@ -25,6 +26,7 @@ export * as media from './media';
@@ -25,6 +26,7 @@ export * as media from './media';
export * as community from './community';
export * as community from './community';
export * as bot from './bot';
export * as bot from './bot';
export * as search from './search';
export * as search from './search';
+export * as bench from './bench';
export * as themes from './themes';
export * as themes from './themes';
export * as chat from './chat';
export * as chat from './chat';
export * as ai from './ai';
export * as ai from './ai';
@@ -35,6 +37,6 @@ export * as geo from './geo';
@@ -35,6 +37,6 @@ export * as geo from './geo';
declare const sdk: typeof core & {
declare const sdk: typeof core & {
auth: typeof auth; registry: typeof registry; modules: typeof modules; realtime: typeof realtime;
auth: typeof auth; registry: typeof registry; modules: typeof modules; realtime: typeof realtime;
- media: typeof media; community: typeof community; bot: typeof bot; search: typeof search; themes: typeof themes; chat: typeof chat; ai: typeof ai; jobs: typeof jobs; tools: typeof tools; projects: typeof projects; geo: typeof geo;
+ media: typeof media; community: typeof community; bot: typeof bot; search: typeof search; bench: typeof bench; themes: typeof themes; chat: typeof chat; ai: typeof ai; jobs: typeof jobs; tools: typeof tools; projects: typeof projects; geo: typeof geo;
};
};
export default sdk;
export default sdk;
@@ -9,6 +9,7 @@ export * as media from './media';
@@ -9,6 +9,7 @@ export * as media from './media';
export * as community from './community';
export * as community from './community';
export * as bot from './bot';
export * as bot from './bot';
export * as search from './search';
export * as search from './search';
+export * as bench from './bench';
export * as themes from './themes';
export * as themes from './themes';
export * as chat from './chat';
export * as chat from './chat';
export * as ai from './ai';
export * as ai from './ai';