openvibers/openvibe.sdk Commits

0.144.0: public measured model analytics in the AI client (#193)

OP OpenVibers committed 3 days ago · 4e1164eb38 · 13 files, +181 −26

* 0.143.0: add public measured model analytics to the AI client

* Merge main and reserve 0.144.0 for measured metrics

Changes

ddiff --git a/CHANGELOG.md b/CHANGELOG.md
iindex 38ef1d5..923d96b 100644
---- a/CHANGELOG.md
++++ b/CHANGELOG.md
@@@ -1,5 +1,9 @@
# Changelog
++## 0.144.0 — 2026-11-10
++
++- `ai.metrics`: public daily model series, up-to-six-model comparisons, task leaderboards, listed price history and hourly provider health. Typed sample counts and nullable measurements; available in browser and server clients without credentials.
++
## 0.143.0 (2026-11-10)
- **`sso`: a plain sign-in is quiet first.** `GET /auth/login` with no `silent`, `prompt` or `login_hint` now asks the Network with `prompt=none` before it shows a page: a browser signed in at openvibe.network comes back signed in, with no chooser. Before, a page that needs a person (and every link to one from another site, such as "Open" in the Dock) landed on the chooser although the Network knew the browser. When the Network knows nobody (`login_required`), or always asks for this app (`interaction_required`), the callback goes on to the chooser by itself (`/auth/login?ask=1`, the new way to ask for the chooser outright). A person who signed out on this site (`ov_sso_hint=guest`) gets the chooser at once, as do "switch" and "add an account" (`prompt`, `login_hint`). The navbar's own silent sign-in (`?silent=1`) is unchanged: with nobody at the Network it returns to the page with `?sso=none`.
ddiff --git a/README.md b/README.md
iindex 79412fb..7e52990 100644
---- a/README.md
++++ b/README.md
@@@ -13,7 +13,7 @@ npm install https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags
> Supported browser and server clients for the OpenVibe platform.
--**Status:** alpha, v0.143.0 (the five 1.0 core entries — `core`, `auth`, `events`, `db` and `media` — are frozen: their exported names change only with a major release; roadmap Wave 2; developer apps from Wave 20; Media objects v2 from Wave 4; the Tools platform API; Search, Chat, AI, Vision and consent clients; sign-in with recovery and a client health check). 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.144.0 (the five 1.0 core entries — `core`, `auth`, `events`, `db` and `media` — are frozen: their exported names change only with a major release; roadmap Wave 2; developer apps from Wave 20; Media objects v2 from Wave 4; the Tools platform API; Search, Chat, AI, Vision and consent clients; sign-in with recovery and a client health check). 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.
@@@ -22,7 +22,7 @@ npm install https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags
After v0.97.1 is tagged:
```bash
--npm install https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.143.0
++npm install https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.144.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 the temporary Contracts sharing branch, a devDependency the tests check them against).
ddiff --git a/STATUS.json b/STATUS.json
iindex 1468efe..087c9e2 100644
---- a/STATUS.json
++++ b/STATUS.json
@@@ -3,7 +3,7 @@
"stage": "alpha",
"code": true,
"wave": "W2",
-- "version": "0.143.0",
++ "version": "0.144.0",
"plan": "OpenVibe End-to-End Realignment & Implementation Plan, revision 3 (20 Sep 2026), \u00a73.2; roadmap \u00a730",
"deployed": false,
"deployment": "none: a package released as git tags; services and apps pin a tag",
ddiff --git a/browser/openvibe-sdk.mjs b/browser/openvibe-sdk.mjs
iindex 80cb143..7aa2356 100644
---- a/browser/openvibe-sdk.mjs
++++ b/browser/openvibe-sdk.mjs
@@@ -1,4 +1,4 @@
--// Generated by scripts/browser-bundle.js from browser.js (openvibe-sdk 0.143.0, MIT). Do not edit.
++// Generated by scripts/browser-bundle.js from browser.js (openvibe-sdk 0.144.0, MIT). Do not edit.
// One self-contained ES module: core at the top level plus the browser-safe subpaths as namespaces
// (auth, locker, registry, trust, levels, modules, realtime, media, cam, mediaUpload, ui, community, homeFeed, bot, search, bench, meme, vision, themes, design, redact, chat, ai, credentials, jobs, tools, projects, geo). No server-only code: no service tokens, JWT verification, events, identity.
/* eslint-disable */
@@@ -38,11 +38,11 @@ module.exports = {
redact: __load(40),
chat: __load(41),
ai: __load(43),
-- credentials: __load(44),
-- jobs: __load(45),
-- tools: __load(47),
-- projects: __load(48),
-- geo: __load(49),
++ credentials: __load(45),
++ jobs: __load(46),
++ tools: __load(48),
++ projects: __load(49),
++ geo: __load(50),
};
},
// ── 1: src/core/index.js ───────────────────────────────────────────
@@@ -57,7 +57,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.143.0';
++const SDK_VERSION = '0.144.0';
module.exports = {
createClient,
@@@ -5300,7 +5300,8 @@ function createAiClient(client, defaults = {}) {
remove(id) { requireFiles('remove'); return fileObjects.delete(id); },
};
-- const api = { tasks, creations, choices, run, admin, runs, batches, files, TERMINAL, CLASSES };
++ const metrics = __load(44).createMetricsClient(call);
++ const api = { tasks, creations, choices, run, admin, runs, batches, files, metrics, TERMINAL, CLASSES };
// Direct operations: a run of workflow ai.<op> with the input as the body, waited for by AI.
for (const op of OPS) {
api[op] = (input = {}, o = {}) => call({ method: 'POST', path: `/api/v1/${op}`, query: waitQuery(o),
@@@ -5312,7 +5313,36 @@ function createAiClient(client, defaults = {}) {
module.exports = { createAiClient, TERMINAL, OPS, CLASSES };
},
--// ── 44: src/credentials.js ──────────────────────────────────────────
++// ── 44: src/ai-metrics.js ───────────────────────────────────────────
++function (module, exports) {
++'use strict';
++// Public measured reads: no bearer token, prompt, person or site identifiers.
++function createMetricsClient(call) {
++ const root = '/api/v1/exchange/metrics';
++ const read = (path, query, opts) => call({ method: 'GET', path: root + path, query, auth: false }, opts);
++ const model = (value) => {
++ if (typeof value !== 'string' || !/^[a-z0-9][a-z0-9._/-]{0,199}$/.test(value)) throw new TypeError('metrics: use a catalogue model slug');
++ return encodeURIComponent(value);
++ };
++ return {
++ status: (opts = {}) => read('/status', undefined, opts),
++ model: (id, query = {}, opts = {}) => read(`/models/${model(id)}`, query, opts),
++ compare(query, opts = {}) {
++ if (!query || !Array.isArray(query.models) || query.models.length < 1 || query.models.length > 6) throw new TypeError('metrics.compare: choose one to six models');
++ query.models.forEach(model);
++ return read('/compare', { ...query, models: query.models.join(','), measures: query.measures?.join(',') }, opts);
++ },
++ leaderboard: (query = {}, opts = {}) => read('/leaderboard', query, opts),
++ prices: (id, query = {}, opts = {}) => read(`/prices/${model(id)}`, query, opts),
++ providerHealth(provider, opts = {}) {
++ if (typeof provider !== 'string' || !/^[a-z0-9][a-z0-9._-]{0,79}$/.test(provider)) throw new TypeError('metrics.providerHealth: invalid provider');
++ return read(`/providers/${encodeURIComponent(provider)}`, undefined, opts);
++ },
++ };
++}
++module.exports = { createMetricsClient };
++},
++// ── 45: src/credentials.js ──────────────────────────────────────────
function (module, exports) {
'use strict';
@@@ -5330,7 +5360,7 @@ function createCredentialsClient(client, defaults = {}) {
module.exports = { createCredentialsClient };
},
--// ── 45: src/jobs.js ─────────────────────────────────────────────────
++// ── 46: src/jobs.js ─────────────────────────────────────────────────
function (module, exports) {
'use strict';
/**
@@@ -5368,7 +5398,7 @@ function (module, exports) {
*/
const { OpenVibeError, isOpenVibeError } = __load(3);
const { newIdempotencyKey } = __load(5);
--const { appendFiles } = __load(46);
++const { appendFiles } = __load(47);
const { parseSSE } = __load(25);
const TERMINAL = new Set(['succeeded', 'failed', 'cancelled']);
@@@ -5532,7 +5562,7 @@ function createJobsClient(client, { baseUrl, service = 'tools', audience = 'open
module.exports = { createJobsClient, isTerminal, TERMINAL_STATES: [...TERMINAL] };
},
--// ── 46: src/core/form.js ────────────────────────────────────────────
++// ── 47: src/core/form.js ────────────────────────────────────────────
function (module, exports) {
'use strict';
/**
@@@ -5566,7 +5596,7 @@ function appendFiles(form, list, where) {
module.exports = { isBlob, isUpload, appendFiles };
},
--// ── 47: src/tools.js ────────────────────────────────────────────────
++// ── 48: src/tools.js ────────────────────────────────────────────────
function (module, exports) {
'use strict';
/**
@@@ -5600,8 +5630,8 @@ function (module, exports) {
*/
const { OpenVibeError, isOpenVibeError } = __load(3);
const { newIdempotencyKey } = __load(5);
--const { isUpload, appendFiles } = __load(46);
--const { createJobsClient } = __load(45);
++const { isUpload, appendFiles } = __load(47);
++const { createJobsClient } = __load(46);
const enc = encodeURIComponent;
const ID_RE = /^[a-z][a-z0-9-]{0,39}$/;
@@@ -5861,7 +5891,7 @@ function createToolsClient(client, { baseUrl, service = 'tools', audience = 'ope
module.exports = { createToolsClient, ToolRunError, isToolRunError };
},
--// ── 48: src/projects.js ─────────────────────────────────────────────
++// ── 49: src/projects.js ─────────────────────────────────────────────
function (module, exports) {
'use strict';
/**
@@@ -5977,7 +6007,7 @@ function createProjectsClient(client, { baseUrl } = {}) {
module.exports = { createProjectsClient };
},
--// ── 49: src/geo.js ──────────────────────────────────────────────────
++// ── 50: src/geo.js ──────────────────────────────────────────────────
function (module, exports) {
'use strict';
/**
ddiff --git a/docs/measured-metrics.md b/docs/measured-metrics.md
nnew file mode 100644
iindex 0000000..c3904f1
---- /dev/null
++++ b/docs/measured-metrics.md
@@@ -0,0 +1,27 @@
++# Measured model metrics
++
++`ai.metrics` works in browser and server clients and never requests credentials.
++
++```js
++const { createClient } = require('openvibe-sdk/core');
++const { createAiClient } = require('openvibe-sdk/ai');
++const ai = createAiClient(createClient(), { baseUrl: 'https://api.openvibe.exchange' });
++const comparison = await ai.metrics.compare({
++ models: ['openai/gpt-5-nano', 'openai/gpt-5-mini'],
++ kind: 'chat', measures: ['latency_p50_ms', 'cost_per_run_usd'],
++});
++const series = await ai.metrics.model('openai/gpt-5-mini', { days: 90 });
++const board = await ai.metrics.leaderboard({ kind: 'chat', measure: 'latency_p50_ms' });
++const prices = await ai.metrics.prices('openai/gpt-5-mini', { days: 30 });
++const health = await ai.metrics.providerHealth('openai');
++```
++
++Comparisons and leaderboards use the same completed UTC day for every model and
++keep provider and task kind separate. The response supplies that day, sample counts
++and methodology. Model series cover 30 or 90 days; historical gaps remain empty.
++Measured cells and individual timing measures need twenty observations. Null means
++unmeasured, not zero. Listed price history starts when daily catalogue capture was
++deployed. Hourly provider health spans seven days. These reads perform no model call.
++
++Pass `{ signal }` as the last argument to cancel a read. Results are cached at the
++service for sixty seconds. Browsers can read the public CORS-enabled endpoints.
ddiff --git a/package-lock.json b/package-lock.json
iindex 605cc0a..6a785f2 100644
---- a/package-lock.json
++++ b/package-lock.json
@@@ -1,12 +1,12 @@
{
"name": "openvibe-sdk",
-- "version": "0.143.0",
++ "version": "0.144.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "openvibe-sdk",
-- "version": "0.143.0",
++ "version": "0.144.0",
"license": "MIT",
"bin": {
"openvibe-download": "bin/openvibe-download.js"
ddiff --git a/package.json b/package.json
iindex 82d6a11..4561067 100644
---- a/package.json
++++ b/package.json
@@@ -1,6 +1,6 @@
{
"name": "openvibe-sdk",
-- "version": "0.143.0",
++ "version": "0.144.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",
ddiff --git a/src/ai-metrics.js b/src/ai-metrics.js
nnew file mode 100644
iindex 0000000..4be383a
---- /dev/null
++++ b/src/ai-metrics.js
@@@ -0,0 +1,26 @@
++'use strict';
++// Public measured reads: no bearer token, prompt, person or site identifiers.
++function createMetricsClient(call) {
++ const root = '/api/v1/exchange/metrics';
++ const read = (path, query, opts) => call({ method: 'GET', path: root + path, query, auth: false }, opts);
++ const model = (value) => {
++ if (typeof value !== 'string' || !/^[a-z0-9][a-z0-9._/-]{0,199}$/.test(value)) throw new TypeError('metrics: use a catalogue model slug');
++ return encodeURIComponent(value);
++ };
++ return {
++ status: (opts = {}) => read('/status', undefined, opts),
++ model: (id, query = {}, opts = {}) => read(`/models/${model(id)}`, query, opts),
++ compare(query, opts = {}) {
++ if (!query || !Array.isArray(query.models) || query.models.length < 1 || query.models.length > 6) throw new TypeError('metrics.compare: choose one to six models');
++ query.models.forEach(model);
++ return read('/compare', { ...query, models: query.models.join(','), measures: query.measures?.join(',') }, opts);
++ },
++ leaderboard: (query = {}, opts = {}) => read('/leaderboard', query, opts),
++ prices: (id, query = {}, opts = {}) => read(`/prices/${model(id)}`, query, opts),
++ providerHealth(provider, opts = {}) {
++ if (typeof provider !== 'string' || !/^[a-z0-9][a-z0-9._-]{0,79}$/.test(provider)) throw new TypeError('metrics.providerHealth: invalid provider');
++ return read(`/providers/${encodeURIComponent(provider)}`, undefined, opts);
++ },
++ };
++}
++module.exports = { createMetricsClient };
ddiff --git a/src/ai.js b/src/ai.js
iindex ee953b2..bb8e228 100644
---- a/src/ai.js
++++ b/src/ai.js
@@@ -174,7 +174,8 @@ function createAiClient(client, defaults = {}) {
remove(id) { requireFiles('remove'); return fileObjects.delete(id); },
};
-- const api = { tasks, creations, choices, run, admin, runs, batches, files, TERMINAL, CLASSES };
++ const metrics = require('./ai-metrics').createMetricsClient(call);
++ const api = { tasks, creations, choices, run, admin, runs, batches, files, metrics, TERMINAL, CLASSES };
// Direct operations: a run of workflow ai.<op> with the input as the body, waited for by AI.
for (const op of OPS) {
api[op] = (input = {}, o = {}) => call({ method: 'POST', path: `/api/v1/${op}`, query: waitQuery(o),
ddiff --git a/src/core/index.js b/src/core/index.js
iindex fc6a375..8bef2d2 100644
---- a/src/core/index.js
++++ b/src/core/index.js
@@@ -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.143.0';
++const SDK_VERSION = '0.144.0';
module.exports = {
createClient,
ddiff --git a/test/ai-metrics.test.js b/test/ai-metrics.test.js
nnew file mode 100644
iindex 0000000..9677b35
---- /dev/null
++++ b/test/ai-metrics.test.js
@@@ -0,0 +1,29 @@
++'use strict';
++const assert = require('node:assert/strict');
++const { createAiClient } = require('../src/ai');
++const { createClient } = require('../src/core');
++(async () => {
++ const calls = [];
++ const cell = { latency_p50_ms: 1000, ttft_p50_ms: null, runs: 20 };
++ const ai = createAiClient({ json: async (r) => { calls.push(r); return { series: [cell] }; } });
++ const signal = new AbortController().signal;
++ await ai.metrics.status();
++ assert.equal((await ai.metrics.model('openai/gpt-5-mini', { days: 90 }, { signal })).series[0].ttft_p50_ms, null);
++ await ai.metrics.compare({ models: ['openai/gpt-5-mini', 'openai/gpt-5-nano'], measures: ['latency_p50_ms'], kind: 'chat' });
++ await ai.metrics.leaderboard({ kind: 'code', measure: 'cost_per_run_usd' });
++ await ai.metrics.prices('openai/gpt-5-mini', { days: 30 });
++ await ai.metrics.providerHealth('openai');
++ assert(calls.every((r) => r.auth === false && r.method === 'GET'));
++ assert.equal(calls[1].path, '/api/v1/exchange/metrics/models/openai%2Fgpt-5-mini');
++ assert.equal(calls[1].signal, signal);
++ assert.equal(calls[2].query.models, 'openai/gpt-5-mini,openai/gpt-5-nano');
++ assert.equal(calls[2].query.measures, 'latency_p50_ms');
++ assert.throws(() => ai.metrics.compare({ models: Array(7).fill('a/b') }), /six models/);
++ assert.throws(() => ai.metrics.model('../bad?query'), /slug/);
++ const live = createAiClient(createClient({ fetch: async (_url, init) => {
++ assert(!new Headers(init.headers).has('authorization'));
++ return new Response(JSON.stringify({ series: [cell] }), { headers: { 'Content-Type': 'application/json' } });
++ }, auth: { getToken: async () => { throw new Error('public metrics must not ask for a token'); } } }), { baseUrl: 'https://ai.example.test' });
++ assert.equal((await live.metrics.model('openai/gpt-5-mini')).series[0].latency_p50_ms, 1000);
++ console.log('ai.metrics: typed public reads preserve nulls, encode paths, forward signals and never request credentials');
++})().catch((err) => { console.error(err); process.exitCode = 1; });
ddiff --git a/types/ai-metrics.d.ts b/types/ai-metrics.d.ts
nnew file mode 100644
iindex 0000000..b6f32f7
---- /dev/null
++++ b/types/ai-metrics.d.ts
@@@ -0,0 +1,36 @@
++export type MetricKind = 'chat' | 'code' | 'design' | 'search' | 'vision' | 'speech' | 'embedding' | 'image' | 'video' | 'other';
++export type MetricMeasure = 'success_rate' | 'latency_p50_ms' | 'latency_p95_ms' | 'ttft_p50_ms' | 'tokens_per_second_p50' | 'cost_per_run_usd' | 'cost_per_1k_output_tokens_usd';
++export type MetricCell = Partial<Record<MetricMeasure, number | null>> & {
++ day: string; model: string; provider: string; task_kind: MetricKind; runs: number;
++ samples: { latency: number | null; ttft: number | null; throughput: number | null; cost: number | null };
++ computed_at: string;
++};
++export interface MetricMetadata {
++ schema: 'exchange.metrics@1'; minimum_samples: 20; source: string; methodology: string;
++ measures: MetricMeasure[]; task_kinds: MetricKind[];
++}
++export interface ModelMetricSeries extends MetricMetadata {
++ model: string; days: 30 | 90; from: string; to: string; kind: MetricKind | null; series: MetricCell[]; missing: string;
++}
++export interface MetricComparison extends MetricMetadata {
++ day: string; kind: MetricKind | null; models: { model: string; cells: MetricCell[] }[];
++}
++export interface MetricLeaderboard extends MetricMetadata {
++ day: string; kind: MetricKind; measure: MetricMeasure; direction: 'higher' | 'lower'; rows: MetricCell[];
++}
++export interface ModelPriceHistory {
++ schema: 'exchange.prices@1'; model: string; days: 30 | 90; source: string; currency: 'USD'; per: '1M tokens';
++ series: { day: string; model: string; provider: string; offer: string; price_in: number | null; price_out: number | null; price_cached: number | null; source: string | null; captured_at: string }[];
++}
++export interface ProviderHealth extends MetricMetadata {
++ provider: string; days: 7; interval: 'hour'; from: string; to: string;
++ series: { hour: string; provider: string; runs: number; success_rate: number; latency_p50_ms: number | null; latency_samples: number | null; computed_at: string }[];
++}
++export interface AiMetrics {
++ status(opts?: { signal?: AbortSignal }): Promise<{ schema: 'exchange.metrics@1'; minimum_samples: 20; completed_through: string | null; completed_at: string | null; published_cells: number; first_day: string | null; last_day: string | null }>;
++ model(model: string, query?: { days?: 30 | 90; kind?: MetricKind }, opts?: { signal?: AbortSignal }): Promise<ModelMetricSeries>;
++ compare(query: { models: string[]; measures?: MetricMeasure[]; kind?: MetricKind; day?: string }, opts?: { signal?: AbortSignal }): Promise<MetricComparison>;
++ leaderboard(query?: { kind?: MetricKind; measure?: MetricMeasure; day?: string }, opts?: { signal?: AbortSignal }): Promise<MetricLeaderboard>;
++ prices(model: string, query?: { days?: 30 | 90 }, opts?: { signal?: AbortSignal }): Promise<ModelPriceHistory>;
++ providerHealth(provider: string, opts?: { signal?: AbortSignal }): Promise<ProviderHealth>;
++}
ddiff --git a/types/ai.d.ts b/types/ai.d.ts
iindex c68ea47..7df7ec5 100644
---- a/types/ai.d.ts
++++ b/types/ai.d.ts
@@@ -1,3 +1,5 @@
++import type { AiMetrics } from './ai-metrics';
++export type * from './ai-metrics';
import type { OpenVibeClient } from './core';
export interface AiRun { id: string; status: 'queued' | 'running' | 'succeeded' | 'failed' | 'cancelled' | string; workflow?: { key: string; version: number }; output?: unknown; error?: unknown; latency_class?: LatencyClass; service_tier?: ServiceTier | null; latency_clamped?: { asked: LatencyClass; used: LatencyClass } | null; [field: string]: unknown; }
@@@ -70,7 +72,7 @@ export interface AiBatches {
cancel(id: string, opts?: { signal?: AbortSignal }): Promise<{ batch: AiBatch }>;
}
type Op = (input?: Record<string, unknown>, opts?: Omit<RunOptions, 'version' | 'onBehalfOf'>) => Promise<RunResponse>;
--export interface AiClient { tasks(opts?: {signal?: AbortSignal}): Promise<{tasks: AiTask[]}>; creations(opts?: {signal?: AbortSignal}): Promise<AiCreationList>; run(task: string, input?: Record<string, unknown>, opts?: RunOptions): Promise<RunResponse>; choices: { get(task: string, opts?: {signal?: AbortSignal}): Promise<{task: AiTask; choice: AiChoice; why: string; options: Record<string, unknown[]>}>; set(task: string, choice: AiChoice, opts?: {signal?: AbortSignal}): Promise<{choice: AiChoice}> }; admin: { defaults(value?: {defaults: Array<{task: string; value: AiTaskDefaults}>}, opts?: {signal?: AbortSignal}): Promise<{tasks?: AiTask[]; defaults: Record<string, AiTaskDefaults>}> }; runs: AiRuns; batches: AiBatches; files: AiFiles; TERMINAL: Set<string>; CLASSES: string[]; chat: Op; generate: Op; summarize: Op; classify: Op; extract: Op; enrich: Op; embed: Op; }
++export interface AiClient { metrics: AiMetrics; tasks(opts?: {signal?: AbortSignal}): Promise<{tasks: AiTask[]}>; creations(opts?: {signal?: AbortSignal}): Promise<AiCreationList>; run(task: string, input?: Record<string, unknown>, opts?: RunOptions): Promise<RunResponse>; choices: { get(task: string, opts?: {signal?: AbortSignal}): Promise<{task: AiTask; choice: AiChoice; why: string; options: Record<string, unknown[]>}>; set(task: string, choice: AiChoice, opts?: {signal?: AbortSignal}): Promise<{choice: AiChoice}> }; admin: { defaults(value?: {defaults: Array<{task: string; value: AiTaskDefaults}>}, opts?: {signal?: AbortSignal}): Promise<{tasks?: AiTask[]; defaults: Record<string, AiTaskDefaults>}> }; runs: AiRuns; batches: AiBatches; files: AiFiles; TERMINAL: Set<string>; CLASSES: string[]; chat: Op; generate: Op; summarize: Op; classify: Op; extract: Op; enrich: Op; embed: Op; }
export function createAiClient(client: OpenVibeClient, defaults?: { baseUrl?: string; media?: Record<string, unknown> & { app: string; subject?: string }; objectsClient?: { upload: Function; list: Function; signedUrl: Function; get: Function; delete: Function }; fetch?: typeof fetch }): AiClient;
export const TERMINAL: Set<string>;
export const OPS: string[];