openvibers/openvibe.ai README.md

README.md

main 92.8 KB · 1116 lines · markdown

1# OpenVibe.AI
2
3> Providers, models, routing, prompt templates, workflows, runs, citations, cache and quotas for every product.
4
5**Live site:** https://ai.openvibe.services · **API docs:** https://openvibe.services · **For AI:** https://ai.openvibe.services/llms.txt
6**Run it:** `npm ci && npm run dev` (Node 20 or newer) · **Tests:** `npm test`
7
8**Status:** alpha (roadmap Wave 13). Deployed on the host since 2026-09-23 on `127.0.0.1:4700` and in
9production use: OpenVibe.Live runs with `AI_SERVICE=remote` and Network's footer copy runs
10`network.site_copy` here. Public since 2026-10-07 at [ai.openvibe.services](https://ai.openvibe.services):
11the API (`ai.app.run`, the OpenAI-compatible `/v1`) and a run's receipt (`/runs/:id`). The operator console and
12personal credential settings live on OpenVibe.Exchange.
13**The public site moved to [openvibe.exchange](https://openvibe.exchange) on 2026-11-07:** the home, prices
14and latency, the changelog, the legal pages and the discovery files answer 301 there on an exact page map
15(`server/app.js`), while every API route on this origin is unchanged. The content and the map are in
16`/home/workstation/openvibe/plan-state-2026-10-22/ai-site-inventory-2026-11-07.md`.
17**Domain:** `ai.openvibe.services` · **Port:** 4700 · **Unit:** `openvibe-ai.service`
18**Plan:** OpenVibe End-to-End Realignment & Implementation Plan, revision 3 (20 Sep 2026), §12.1, §12.13, §15.14, §33, §34.
19**License:** AGPL-3.0 (same as every OpenVibe service).
20
21## Purpose
22
23The reusable AI authority extracted from OpenVibe.Live. Products run registered, versioned
24workflows; every run returns a **draft/evidence package** (structured output + citations +
25provenance) attributed to a workflow, its version, a model and a run id — never to a person. AI never
26owns publication truth: Wiki, Blog, News, Live, … decide what to publish.
27
28## Owns
29
30- the eleven record groups ([below](#the-eleven-record-groups)): providers, models, routing profiles,
31 prompt templates, workflow definitions, runs, the request log (hashes, not prompts), citations, the
32 cache, quotas and usage, and the audit log, in AI's own PostgreSQL database (`ov_ai`, ADR-035; schema in
33 [migrations/](migrations/))
34- people's own provider keys (`ai.credential.manage`), encrypted at rest, one per provider (Codes C3: `anthropic`,
35 `openai`, `deepseek`, `minimax`, `openrouter` or a `custom` endpoint), and per-streamer
36 attribution quotas (`ai.quota.attribution.manage`)
37- **"Your AI" (L36 Y1/Y3/Y8):** the person's own keys under the reserved owner `self` for every supported provider
38 (Anthropic, OpenAI, Gemini, DeepSeek, Mistral, xAI, Groq, Cerebras, Together, Fireworks, OpenRouter, MiniMax and a
39 custom endpoint), tested on save (its models, or one token) with status, `models_count` and a redacted `last_error`;
40 their `only_my_keys`, `free_first`, price ceiling and default model per kind of work; a run `on_behalf_of` a person
41 tries their keys first, `explain.paid_by` and the attempts name who paid and why each failed, and a 401/403 marks the
42 key failing and publishes `ai.credential.failed` (`/api/v1/me/*`, `server/me.js`; only the person's own token or
43 AI-site session, never a service token or `on_behalf_of`). The person's settings UI is on
44 [OpenVibe.Exchange](https://openvibe.exchange/privacy/keys).
45- a person's own computer as their provider (credential provider `node`, contracts 0.220.0): a model on their own
46 machine (Ollama, LM Studio, llama.cpp through the OpenVibe Link), reached through OpenVibe.Bot with `bot.machine.ai`
47 for that person only, free and with no key (`server/machines.js`; Bot on this host by default, `OV_BOT_INTERNAL_URL`);
48 machine `any` (contracts 0.221.0) uses the fastest of their machines that is on with the model, then the next
49- the `ai.run.queued|succeeded|failed|cached` events and the `ai.preferences` / `ai.usage_summary`
50 user modules' AI side
51
52## Does not own
53
54- publication: every run is a draft/evidence package; Wiki, Blog, News, Live and the rest decide what is
55 published
56- the recordings it analyses (OpenVibe.Media), identity (OpenVibe.Network), the products' prompts'
57 meaning (each workflow's owner)
58
59## Depends on
60
61- PostgreSQL 18 (OpenVibe.Host `roles/data/`, ADR-035): every read and write is async through `openvibe-sdk/db`
62- OpenVibe.Network (JWKS, service tokens, SSO for the operator console, user modules)
63- OpenVibe.Events (the `ai.run.*` outbox relay, and the signed `bench.rating.changed` subscription; off without `EVENTS_URL`)
64- OpenVibe.Bench (`bench.rating.changed` and `GET /api/v1/boards/:kind?type=elo|cost`, for opted-in routes)
65- OpenVibe.Media (recordings `media.analyze` reads, over allow-listed https URLs or signed URLs)
66- provider APIs configured by the operator (OpenAI-compatible, OpenAI Responses, Anthropic, the HTTP seam), whisper.cpp
67 and the local model server `openvibe-llm.service` on this host
68- `openvibe-contracts` v0.421.0, `openvibe-sdk` v0.82.0 (service tokens, events outbox, redact),
69 `openvibe-shared` v4.109.2, pinned by release tarball
70
71## Capabilities
72
73Implemented here (the service manifest's `capabilities`, audience `openvibe.ai`; routes under
74[API](#api)): `ai.run.create`, `ai.run.read`, `ai.workflow.manage`, `ai.provider.manage`,
75`ai.usage.read`, `ai.usage.report` (`POST /api/v1/usage/calls`: another product reports the AI calls it made
76without AI, `ai.call-report@1`, and AI prices, dedupes and publishes each as `ai.call.recorded@1`),
77`ai.credential.manage` (`/api/v1/credentials/:subject[/:provider]`),
78`ai.quota.attribution.manage` (per-streamer quotas), `ai.memory.read` and `ai.memory.write` (Memory M2a,
79`/api/v1/memory/*`), `ai.me.read` and `ai.me.manage` ("Your AI", `/api/v1/me/*`: a person's own keys and
80routing preferences; only the person's own token or AI-site session) and `ai.app.run` (developer apps, ADR-014; public,
81registered since openvibe-contracts 0.109.0), and `ai.layout.suggest` (`POST /api/v1/ui/layout/suggest`: a surface catalog,
82the layout shown now and the person's words in, a validated `ui.layout@1` with a one-sentence summary or an honest
83refusal out; one retry with the validator's reasons, on the cheap structured-output route, as the person named in
84`X-OV-Subject`). A service token's `ns`
85claim limits which workflow namespaces it may run; an app token is fixed to its project's.
86
87A first-party service may also open a batch of the network's own with the `ai.batches.create` grant
88(see [Batches](#batches)): the batch is billed to the network's account, there is no Balance
89session for it, and the run's attribution names the service. The grant is checked locally by the
90batch service; the Contracts capability entry comes later (BA3).
91
92Called elsewhere, as the service principal `ai`: `events.event.publish` (Events, the outbox relay) and
93`events.subscription.manage` (Events, creating AI's own `bench.rating.changed` subscription), and
94`network.modules.read` / `network.modules.write` for `ai.preferences` and `ai.usage_summary`
95(Network). Other services grant `ai` their own draft capabilities (for example
96`blog.post.create`, `news.story.revise`, `reviews.summary.propose`) to receive drafts.
97
98## Run it
99
100```bash
101npm install
102cp .env.example .env # no key needed: without one every route falls back to the stub provider
103npm run dev # http://localhost:4700
104npm test # stub provider, temp databases, local servers only — no network
105```
106
107Link 22 (CI pins 22.22.1). Callers need an OpenVibe.Network service token with audience
108`openvibe.ai`; the Network public key is fetched from `/api/.well-known/jwks` (or pinned with
109`OV_NETWORK_PUBLIC_KEY`).
110
111## API
112
113```http
114POST /api/v1/runs?wait=10000
115Authorization: Bearer <service token, aud openvibe.ai, cap ai.run.create>
116Idempotency-Key: live-translate-8812
117
118{ "workflow": "live.translate",
119 "input": { "text": "みなさん、こんにちは", "from": "ja", "to": "en", "context": "chat" },
120 "target": { "service": "live", "type": "chat_message", "id": "8812" },
121 "attribution": { "service": "live", "type": "user", "id": "42" } }
122```
123
124answers `201` with the finished run (or `202` + `Location` while it is still running; `200` for an
125idempotent replay):
126
127```json
128{ "run": { "id": "run_01…", "status": "succeeded", "output": { "text": "Hello everyone", "unchanged": false },
129 "synthetic": false, "provenance": { "origin": "ai", "workflow": "live.translate", "workflow_version": 1,
130 "template_version": 1, "route": "live.chat", "route_version": 1, "provider": "shared", "model": "gpt-5-nano",
131 "fallback_used": false, "run_id": "run_01…" }, "citations_count": 0, "…": "…" } }
132```
133
134| Route | Capability |
135|---|---|
136| `POST /api/v1/runs` (`?wait=ms`, `Idempotency-Key`), `POST /runs/:id/cancel`, `POST /runs/:id/retry`, `POST /runs/:id/advice` (an advisor call on a run that asked for one), `POST /runs/:id/citations` | `ai.run.create` |
137| `POST /api/v1/{chat,generate,summarize,classify,extract,enrich,embed,image}` — each a run of workflow `ai.<op>` | `ai.run.create` |
138| `POST /api/v1/research` — a run of `research.gather` ([MegaSource](#megasource-research)) | `ai.run.create` (namespace `research.*`) or `ai.app.run` (free sources only) |
139| `POST /api/v1/{chat,generate,summarize,classify,extract,embed,image}` (no `enrich`), `GET /runs/:id` (the app's own runs) | `ai.app.run` for a developer-app token ([Developer apps](#developer-apps)) |
140| `GET /api/v1/runs`, `GET /runs/:id` (+ citations, request-log metadata), `GET /runs/:id/citations` | `ai.run.read` (own runs) |
141| `GET /api/v1/actions?subject=usr_…` — one person's Action Center items (`common.action-list-result@1`, runs whose `on_behalf_of` is that person) | `ai.actions.read` (first-party service) |
142| `POST /api/v1/memory/items:batch`, `GET/PATCH/DELETE /api/v1/memory/items[/:id]`, `DELETE /api/v1/memory/items?source=`, `GET /api/v1/memory/profile` | `ai.memory.read` / `ai.memory.write` (the person's own token, or a service acting `on_behalf_of` them) |
143| `GET /api/v1/memory/search?q=&scopes=` — active items only, at most 8, in the grant's scopes, audited | `ai.memory.read` plus `on_behalf_of` |
144| `GET/PUT/DELETE /api/v1/me/credentials[/:provider]`, `POST /api/v1/me/credentials/:provider/test`, `GET/PUT /api/v1/me/ai/preferences` | `ai.me.read` / `ai.me.manage` — the person's own token or AI-site session only; a service token or `on_behalf_of` is refused. A site session token (audience `openvibe.network`) may read these hints and settings but never write them: a write needs a token whose `aud` includes `openvibe.ai`, or AI's own same-origin session |
145| `GET /api/v1/workflows\|templates\|routes[/:key]` | `ai.run.create`, `ai.workflow.manage` or `ai.usage.read` |
146| `GET /api/v1/routes?operation=generate` | the same read: every route's `offers` (`{provider, model}`), plus the operation's deduplicated `offers` |
147| `POST /api/v1/{templates,workflows,routes}/:key/versions`, `…/versions/:v/status` | `ai.workflow.manage` |
148| `POST/PATCH /api/v1/providers…`, `…/disable\|enable\|reset`, `POST/PATCH /api/v1/models…`, `POST /api/v1/quotas`, `DELETE /api/v1/cache` | `ai.provider.manage` |
149| `GET /api/v1/status\|usage\|quotas\|requests\|audit\|cache\|providers\|models` | `ai.usage.read` |
150| `POST /api/v1/usage/calls` — up to 100 calls a product made outside AI (`ai.call-report@1`, 202 `{ accepted, duplicates, rejected }`) | `ai.usage.report` |
151| `GET /api/v1/usage/series?days=N` (N 1–90) — aggregate demand per UTC day and model: requests, successes and the four token totals, never a caller id or text (`AI_USAGE_SERIES=on`) | `ai.usage.read` |
152| `GET /api/health`, `GET /api/ready`, `GET /release.json` | public |
153| `GET /metrics` | direct loopback callers only (a request carrying X-Forwarded-For gets 404) |
154| `GET /console…` | 301 to the same path on [OpenVibe.Exchange](https://openvibe.exchange/console) |
155| `GET /runs/:id` — one run's receipt (status, workflow, model, cost, times, output or error; never the input) | the run's person, signed in (see [Run receipts](#run-receipts)) |
156| `GET /auth/person/login\|callback` | any Network person, for a run receipt |
157| `GET /robots.txt` | public: disallows `/console`, `/runs`, `/auth/`, `/api/` |
158| `GET /stats` | public: p50/p95 latency, success rate and rate-card price per registered provider and model over the last 7 days (server-rendered, `openvibe-shared/cache-policy` HTML headers, nothing per caller) |
159
160Browser reads of `GET /stats` and `GET /release.json` allow exact origins from `AI_CORS_ORIGINS`
161(comma-separated; default `https://ai.openvibe.services,https://openvibe.services`). Their `OPTIONS`
162preflight allows GET. Responses never allow credentials; `/api/v1/*`, `/console` and `/auth/*`
163have no CORS access. Service callers do not need CORS.
164
165`/api/ready` (openvibe-shared/ready) is 503 when the database, the active workflows or the Network
166key fail; provider configuration (an active provider without its credentials, or with an open
167circuit) degrades it. `/metrics` (openvibe-shared/metrics) carries golden signals by route template,
168`ai_runs{state="queued"|"running"}`, `ai_provider_circuit{provider,state}` and the measured
169`ai_provider_circuit_open{provider,model,class}` and `ai_provider_failures_total{provider,class}`.
170
171Each HTTP request is aggregated by route template, method and status class, never by raw URL; `/shared`
172files, static assets, health, readiness and `/metrics` are not counted. Once a minute one `http.request`
173sample per key, plus the active-requests, p95 latency and event-loop-lag gauges, is written to this
174service's own `telemetry_samples` table (openvibe-sdk 0.46 telemetry, the `platform.telemetry-sample@1`
175shape); rows older than 14 days are deleted daily. `server/app.js` mounts openvibe-sdk's
176`telemetryMiddleware` (a no-op until init) and `server/index.js` calls `telemetry.init()` at production
177start, runs the 14-day retention and flushes on stop. The table is created by migration
178`migrations/0009_telemetry.sql`.
179
180Errors are `application/problem+json` (openvibe-contracts `http.problem`): `input.invalid` (with the
181schema errors), `workflow.not_found`, `idempotency.conflict`, `quota.exceeded` (429 +
182`Retry-After`), `govern.exceeded` (429 + `Retry-After`: a tier budget) and `govern.unavailable` (503: Valkey
183unreachable for a paid or staff run), `queue.full` (429 + `Retry-After`: the run would wait behind `AI_MAX_QUEUED_RUNS`
184runs, or `AI_MAX_QUEUED_RUNS_PER_CALLER` of this caller's), `capability.denied`, `capability.namespace_denied`, `token.*`. A run that cannot
185produce a real answer ends `failed` with an explicit code — `ai.resting` (503: every offer is out of balance,
186paused or busy; the detail is the sentence products show, "AI is resting for a moment", with `Retry-After`),
187`provider.unavailable`, `route.unavailable`, `fetch.refused`, `source.unavailable`, `input.insufficient`,
188`output.empty`, `output.invalid`, `run.interrupted` — and never with filler content.
189
190Each active provider with a balance API (DeepSeek's `GET /user/balance`) is asked hourly; the last reading is
191`ai_provider_balance_usd{provider}` on `/metrics` and OpenVibe.Host's `OpenVibeAiProviderBalanceLow` alert turns a
192reading below $2 into an operator ticket. The key is only sent in the request's `Authorization` header: it is never
193logged, returned or stored by the probe. `AI_BALANCE_PROBE=0` (or a restore drill, `AI_DRILL=1`) turns it off.
194
195The capability ids and the `ai` service manifest are released in openvibe-contracts v0.76.0 (the
196drafts stay in `docs/capabilities-proposal/`); `server/auth.js` checks them with the contracts grant
197rule (exact id or `family.*`). A token's `ns` claim limits which
198workflow namespaces it may run (`live.*`, `wiki.*`, …), and namespaces fail closed: a token with
199no `ns` runs nothing. Every caller's Network grant names its namespaces (Live: `live.*`,
200`network.site_copy`, `media.analyze`). The fallback for service tokens without `ns` (`AI_NS_FALLBACK`)
201and its rollback lever (`AI_NS_REQUIRED`) were retired on 2026-09-28 (shims C-22 and C-23).
202
203## Batches
204
205Many prompts in, the answers later. A batch is half the retail price for the customer, and it runs
206where the money works out: on OpenVibe's own capacity and free providers, on a community offer that
207asks at most half, on a paid provider inside a price-book off-peak window, or on **a provider's own
208batch API** when that provider's real batch price is at most half the retail card. The customer
209batch therefore runs on a paid model at any hour, not only in an off-peak window.
210
211The providers whose own batch API this service speaks, with the discount, the window and the limits
212read on each provider's own documentation page (a provider whose batch API could not be read is not
213listed and is never called):
214
215| provider | adapter | batch price | window | limits |
216|---|---|---|---|---|
217| `shared` (OpenAI) | OpenAI-compatible | half the list price | 24 h | 50 000 requests, 200 MB, one model per file |
218| `qwen` (DashScope) | OpenAI-compatible | half the real-time price | 24 h | 50 000 requests, 500 MB, one model per file |
219| `kimi` (Moonshot) | OpenAI-compatible | 60% of the list price | 24 h | 10 000 requests, 100 MB, one model per file |
220| `deepinfra` | OpenAI-compatible | 80% of the list price | 24 h | 10 000 requests, 100 MB, one model per file |
221| `anthropic` | Message Batches | half the list price | 24 h | 100 000 requests, 256 MB (adapter written, not enabled) |
222| `gemini` | Gemini Batch API | half the list price | 24 h | 10 000 requests (adapter written, **not enabled**: the endpoint could not be read on the provider's own page) |
223
224Only the providers whose batch price is at most half the retail card (`shared`, `qwen`) may carry a
225**customer** batch: the customer is charged half, so the network's real cost has to fit inside that
226half. Kimi and DeepInfra are below half and are therefore not eligible for a customer batch, though
227the network's own batches may still use them.
228
229How a native batch runs: an item whose run has `latency_class = 'batch'` waits in the queue
230(`batch_queue`), grouped by provider and model. A group goes to the provider when it is full
231(`AI_BATCH_NATIVE_MAX_ITEMS`, default 1000) or old (`AI_BATCH_NATIVE_MAX_AGE_MS`, default 5
232minutes). One worker polls the batches the provider is working on (the claim is the database's and
233expires on its own, so several workers are safe), writes every answer onto its own run (the status,
234the output, the tokens and the cost **at the provider's batch price**) and finishes the run on the
235ordinary path: the same settle, the same receipt, the same events. A deadline the provider cannot
236meet re-routes the item to the ordinary (background or standard) path, and the item says so.
237
238The money: the cost written on a run is what the provider really charged for the tier it really ran
239on (its own list price times its own batch discount), computed in nano-USD. The customer is charged
240half the retail card through Billing like every other request; this queue is a queue and a status,
241never a second ledger.
242
243**The balance is checked when the batch is submitted, and the money is taken when the answer
244arrives.** A run whose estimate does not fit the customer's Balance (or the project's budget) is
245refused before the provider is given the work, so the network never owes a provider for a run nobody
246can pay for; the item carries the reason. The hold itself is taken and settled when the answer comes
247back, so a batch that waits hours does not hold the customer's Balance in the meantime.
248
249**A waiting batch run is durable.** It waits in the database, not in the service's memory: the
250service is deployed several times a day, and a restart leaves the waiting runs queued (the provider
251is still working on them) instead of failing them. The batcher finishes them when the answer is
252there, with the ordinary settle, the ordinary receipt and the ordinary events.
253
254**Service batches.** A first-party service principal whose token carries the `ai.batches.create`
255grant may open a batch of the network's own: it is billed to the network's account (there is no
256Balance session for it) and the run's attribution names the service. The grant is checked locally by
257the batch service (`server/batch/service.js`), because the pinned `openvibe-contracts` build does
258not know that capability id yet: the Contracts capability entry comes later (BA3). Until then the
259route's own guard still asks for `ai.run.create` (or an app's `ai.app.run`) as well.
260
261
262## Model catalog, exact ids and the disclosure
263
264Behind `AI_MODEL_CATALOG` (off by default; with it off both `GET /v1/models` endpoints answer the legacy virtual
265`openvibe` model and no disclosure is added; an exact id is still a pin, so it is refused with 400
266`ai.model_unavailable` rather than run on another model). It applies to both `POST /v1/chat/completions` and
267`POST /v1/messages`.
268
269- **`GET /v1/models` / `GET /v1/models/{id}`:** the registry's real models as `ai.model@1` catalog entries
270 (`provider_key`, `model_key`, `id`, family, variant, quant, revision, sha256, license, context, capabilities,
271 modalities, retail rate in integer nano-USD, availability) plus the dynamic policy aliases `openvibe/auto`,
272 `openvibe/economy`, `openvibe/balanced`, `openvibe/fast`, `openvibe/private` and legacy `openvibe` (= auto). An
273 alias carries `dynamic: true` and its `policy`. Every entry also carries the response shape of the protocol it was
274 asked in: OpenAI's `object`/`created`/`owned_by`, or Anthropic's `type`/`display_name`/`created_at`. An Anthropic
275 client identifies itself with its `anthropic-version` header (or `?format=anthropic`).
276- **Exact ids:** `model` may be `<org>/<model>[:<quant>]@<revision>` (for example
277 `qwen/qwen3-9b-abliterated:q4_k_m@3f2a1c9`). It runs only that artifact. An artifact that does not exist is 404
278 `ai.model_not_found`; one that exists but cannot run the request (no active deployment, unmet capability, down
279 provider, or an offer that may not be resold) is 400 `ai.model_unavailable`. It is never substituted with another
280 model, in fallback or otherwise. A row without artifact metadata has no exact id; it is still listed and reachable
281 by its legacy `provider/model` id.
282- **Aliases:** the dynamic names resolve through the same provider placement. `economy` ranks price first, `fast`
283 measured latency first, `balanced` weighs both, `private` never leaves local/own capacity, and `auto` keeps the
284 existing task-aware router. User preferences (`only_my_keys`, `free_first`, own keys and machines) still apply.
285- **The disclosure, inside `x_openvibe`:** `resolved_model`, `revision`, `deployment`, `provider_access`,
286 `rate_version`, `policy` and `fallback_reason` join the `x_openvibe` object responses already carry (run, provider,
287 cost). There is no second object.
288 - On a JSON response it is in the top-level `x_openvibe`.
289 - On SSE it is the first line, as an SSE comment (`: x_openvibe {...}`), and it is on the final chunk
290 (`message_stop` for Messages).
291- **Commercial access:** `provider_access` is a provider field (`POST/PATCH /api/v1/providers`, default `UNVERIFIED`).
292 Paid managed routing uses only `OWN_DEPLOYMENT` and `AUTHORIZED_WHOLESALE`; `RESTRICTED`, `UNVERIFIED` and
293 `CUSTOMER_BYOK` offers stay out of the shared pool. A person's own key or project provider is disclosed as
294 `CUSTOMER_BYOK` and is unaffected.
295- **Tokens:** developer apps call with the public `ai.app.run` they already hold (ADR-014). The separate
296 `ai.inference.create` once planned for this was retired (Contracts 0.285.0). `ai.run.create` remains the
297 first-party service capability.
298
299## OpenAI-compatible endpoint
300
301`POST /v1/chat/completions` and `GET /v1/models` (`server/api/openai.js`), so a client built for OpenAI's API (the SDKs,
302coding agents, Codes' harnesses) points its base URL at `https://ai.openvibe.services/v1`.
303
304- **Tokens:** the same as the direct operations: an app's Network token (`ai.app.run`; its project pays and owns the
305 run) or a first-party service's (`ai.run.create`). Network tokens last five minutes: a client refreshes it with the
306 client-credentials grant (the SDKs take a token function).
307- **Request:** Chat Completions, as written: `messages` (system, developer, user, assistant with `tool_calls`, and `tool`
308 results), `tools`, `tool_choice`, `parallel_tool_calls`, `response_format`, `stop`, `seed`, `temperature`, `top_p`,
309 `max_tokens` or `max_completion_tokens`, `stream`. `n` must be 1. `model` may be anything: AI's router picks the
310 upstream for `default.chat` by price and health, and a request with `tools` goes only to a provider that declares
311 `tools`. The answer's `model` names the model that ran.
312- **Answer:** `chat.completion` with the upstream's `choices` and `usage`, plus `x_openvibe` (`run_id`, `provider`,
313 `fallback_used`, `cost_usd`, the privacy label and the receipt id) and the `X-OpenVibe-Run-Id` header. The
314 answer's receipt is in `x-openvibe-receipt` (compact JSON) and `x-openvibe-receipt-id`.
315- **Streaming:** `stream: true` answers server-sent events in the chunk format **as the upstream streams them**.
316 - An OpenAI-format upstream (the router's, or the project's own) is asked with `stream: true` and
317 `stream_options.include_usage`, and each delta goes out when it arrives: text, and a tool call's id, name and
318 argument pieces.
319 - Then come the finish reason, `usage` (with `stream_options.include_usage`), `x_openvibe`, and `[DONE]`.
320 - The run itself ends with the whole answer put back together, the upstream's usage (estimated when it sends none)
321 and its cost, as without streaming.
322 - Once a delta has gone out, a failure is final: an `error` event, then `[DONE]`. It is never retried or handed to a
323 fallback whose words would follow the first one's.
324 - An answer that came back in one piece (a replayed idempotent run) is sent as one delta.
325- **Each request is a run** of workflow `ai.complete` (step kind `complete`): the run's quota, free allowance, cost,
326 audit and explain apply, and the prompt is not kept. Errors are OpenAI's shape (`{ error: { message, type, code } }`).
327- **Upstreams:** OpenAI-format providers (`kind: openai`) pass the request through (`complete()`; a reasoning model
328 takes `max_completion_tokens` and its default sampling). Anthropic is not used for it yet.
329- **An app's own provider** (`x_openvibe.provider`, ADR-056). An app may name its project's own OpenAI-format API
330 instead of the router: `{ "base_url": "https://api.openai.com/v1", "model": "gpt-5-mini", "secret": "openai-key" }`.
331 - `secret` is the name of a project secret (OpenVibe.Services → project → Secrets) that the project granted to `ai`
332 for reveal to that base URL's origin.
333 - OpenVibe.Network reveals the key for that one run, from the app's environment (sandbox or production). The key is
334 sent to that origin only, through the public-address guard, and is never stored, cached or logged. The run keeps
335 the secret's name.
336 - The project pays its provider directly, so no tier budget holds the run (the app's quotas still count it). Tools,
337 tool results and every other field pass through.
338 - Refusals: a secret that does not exist or is not granted answers 403 `project_secret.refused`, a base URL that is
339 not https answers 400, and a first-party service token naming a provider answers 403.
340 - Production app runs that use the router are limited to free and local capacity (see Developer apps). Today that is
341 a small local model without tools, so a coding agent's loop needs its project's own provider.
342
343## Presets, receipts, BYOK and the privacy label
344
345The Exchange's routing profiles and the receipt of every answer (the plan of 2026-11-06, "Product lines
346and features", item 1). `server/exchange/` holds the four parts: `presets.js`, `privacy.js`,
347`constraints.js` and `receipts.js`; `server/api/exchange.js` serves them and `migrations/0031_exchange_presets_receipts.sql`
348holds them.
349
350### Presets
351
352A preset is a named set of routing constraints. Five are built in — `cheapest`, `fastest`, `private`,
353`uncensored`, `eu-only` — and they are code, not rows: nobody can overwrite or delete one (409
354`ai.preset_builtin`). The others belong to an account (the person a token acts for) or to a Network
355project (an app's project).
356
357- `GET /api/v1/presets` lists the presets the caller may name: the built-in ones and their own.
358- `POST /api/v1/presets` stores (or replaces) one of their own: `{ "name": "…", "policy": { … } }`.
359- `DELETE /api/v1/presets/{name}` removes one of their own.
360
361The policy's knobs: `objective` (`cheapest`, `fastest`, `balanced`, `private`), `floor` (`fast`,
362`balanced`, `strong`, `frontier`), `ceiling` and `max_price` (US dollars per million tokens),
363`quantization`, `region`, `data_policy` (`private`, `anonymized`, `community`), `community` (may a
364community machine answer) and `adult` (the uncensored tier: the 18+ gate is the pool's own, never the
365preset's — the preset only names the tier the request asks for). A knob nobody knows is dropped, never
366stored, so a typo cannot come back as a rule nobody reads.
367
368A request names a preset as the model `"@preset/cheapest"` or beside it as `"preset": "cheapest"`; it
369may also write its profile inline (`"preset": { "policy": { … } }`) and cap the price on its own
370(`"max_price": 0.42`). A name nobody has is a 404 (`ai.preset_not_found`), never a silent default.
371
372The rules filter the pool **before** the call: an offer that breaks one is out, and the receipt says
373which rule removed it. A hard `max_price` that no offer fits is refused before anything is called, with
374a problem document (`ai.max_price_exceeded`) that carries the cap, the lowest price on offer and the
375offers that were excluded.
376
377### Receipts
378
379Every answered request carries a receipt: the offer that answered, the price it charged, the tokens in
380and out, the cache reads and writes, the latency, why the router chose it, the fallbacks it would have
381used, who paid and the privacy label. It never carries the prompt or the answer.
382
383- **In the answer:** the header `x-openvibe-receipt-id` and `x-openvibe-receipt` (the same facts, compact
384 JSON). A streamed answer names its receipt in the headers and carries the whole one in the last chunk.
385- **On its own:** `GET /v1/receipts/{id}` (and the documented `GET /api/v1/receipts/{id}`), readable by the
386 caller that made the run, by the person it was for, and by whoever may read the usage
387 (`ai.usage.read`). Anyone else gets a 404, not a refusal that says there is something there.
388- **Kept with the usage:** the receipt goes with its run (`runs.receipt_id` points at it) and is pruned
389 with the runs' retention window.
390
391### BYOK and the privacy label
392
393A run on the person's own key (BYOK) is metered at 0 %: the receipt says `paid_by: own_key` (or
394`own_machine`), `byok: true` and `fee_usd: 0`. The OpenVibe key pays the list price plus the fee.
395
396Every answer carries a privacy label — `private`, `anonymized` or `community` — read from the offer's
397declared data policy: the person's own machine is private, an offer that keeps nothing and trains on
398nothing is private, a community offer is community, and anything else is anonymized. The label is on the
399receipt and in the answer's `x_openvibe`.
400
401## Memory
402
403Memory M2a (spec `research-2026-10-30/memory-m2-spec.md`): a person's own notes, sealed with `AI_MEMORY_KEY`
404(AES-256-GCM, AAD `memory|<owner>|<id>|<scope>`, `m1:`), deduped by an HMAC of the normalized text (HKDF-SHA256, info
405`memory-mac`), and scoped by `user:<usr_…>` on every query. The server redacts every item again with
406`openvibe-sdk/redact` (the Link's shapes and corpus) and refuses an item that is only a secret; what an agent writes is
407a suggestion, and every item a service writes passes `noLooks`. An agent's search is active-only, at most 8 items, in
408the scopes its token's grant names (`user` always; `project:*` when the grant lists it), and leaves a `memory_reads`
409row with ids, never text. The profile cache rebuilds only when its `items_hash` moves, at most once an hour, in-process
410on the service's own chat route (no run row, no person allowance). The account export decrypts the person's items for
411their own export; a deletion removes the rows. Unset `AI_MEMORY_KEY` turns every memory route into 503
412`memory.unavailable`; no default key exists.
413
414## Anthropic-compatible endpoint
415
416`POST /v1/messages` and `GET /v1/models` (`server/api/messages.js`), so a client built for Anthropic's Messages API
417(Claude Code, an SDK) points `ANTHROPIC_BASE_URL` at `https://ai.openvibe.services` and works (Codes C3).
418
419- **Tokens:** the same Network tokens as `POST /v1/chat/completions`: an app's (`ai.app.run`; its project pays and owns the
420 run) or a first-party service's (`ai.run.create`).
421- **Request:** Anthropic's Messages API as written: `messages`, `system`, `tools`, `tool_choice`, `stop_sequences`,
422 `temperature`, `top_p`, `top_k`, `max_tokens`, `metadata`, `stream`.
423- **The person's own key:** `x-openvibe-credential: <provider>` uses that person's credential for that provider
424 (`anthropic`, `deepseek`, `minimax`, `openrouter` or a `custom` endpoint) instead of the router. The person is
425 `<provider>:<subject>`, or the token's `on_behalf_of` (an agent acting for them). Without the header the router picks an
426 upstream that speaks Anthropic's format (Anthropic, DeepSeek's and MiniMax's Anthropic-compatible endpoints); an
427 OpenAI-format provider is never converted.
428- **Answer:** a `message` (`id`, `role`, `model`, `content`, `stop_reason`, `usage`), and the run id in
429 `X-OpenVibe-Run-Id`. The answer's receipt is in `x-openvibe-receipt` (compact JSON) and
430 `x-openvibe-receipt-id`.
431- **Streaming:** `stream: true` answers Anthropic's own server-sent events as they arrive (`message_start`,
432 `content_block_start`, `content_block_delta`, `content_block_stop`, `message_delta`, `message_stop`, `ping`); the run
433 still ends with the whole message.
434- **Each request is a run** of workflow `ai.messages` (step kind `messages`): the run's quota, free allowance, cost,
435 audit and explain apply, and the prompt is not kept. Errors are Anthropic's shape
436 (`{ "type": "error", "error": { "type", "message" } }`).
437
438## Developer apps
439
440Roadmap Wave 20, ADR-014. A developer app gets a token from OpenVibe.Network (`POST /oauth/token`,
441`grant_type=client_credentials` with its `app_<ULID>` client, `audience=openvibe.ai`): `sub
442app:app_<ULID>`, `project_id prj_<ULID>`, `env sandbox|production`, ns `[project_id,
443app.<project_id>.*]`, five minutes. AI accepts one capability for it, `ai.app.run` (public). A
444sandbox app holds it without asking (Network's default sandbox allowance); a production app holds it
445once OpenVibe staff add it to the project's allowance. Create the project and its app at
446[openvibe.services/projects](https://openvibe.services/projects):
447
448| Route | What |
449|---|---|
450| `POST /api/v1/{chat,generate,summarize,classify,extract,embed,image}` | the direct operations; `enrich` is first-party only |
451| `GET /api/v1/runs/:id` | the app's own runs (404 for another project's) |
452
453Every other route refuses an app token (403 `capability.denied`), whatever its grant carries. An
454app's runs are its **project's**: the run's requester is `project:prj_<ULID>` and its attribution is
455the project EntityRef `{service: 'network', type: 'project', id: 'prj_<ULID>'}`, so two apps of one
456project share their runs, and the project is the budget unit — the free allowance's subject is the
457attribution (`network:project:prj_<ULID>`), the tier budgets ([Providers](#providers)) name the
458project (`project:prj_<ULID>`), and a quota can cap it on the service scope id `project:prj_<ULID>`.
459
460**Billing.** Every run's `platform.usage-sample@1` readings carry the app's `project`
461(`prj_<ULID>`) with subject `project:prj_<ULID>`, so Billing bills the project's use: the project's
462bounded free allowance is claimed first (`free_allowance_used` on the reading), the rest is priced
463under the project's tier budget (`AI_GOVERN_TIERS`, keyed `project:prj_<ULID>`). **Free and local
464capacity only** (the stub, the local model, a free server or a pooled subscription — never a metered
465provider) for every sandbox run, and for every app run while no tier budget governs its project
466(`AI_GOVERN_TIERS` off): metered capacity is only ever spent under a project budget.
467
468**Workflows.** An app runs the six direct operations (the fixed `ai.*` workflows) and nothing else:
469`POST /api/v1/runs`, which takes a workflow by key, stays first-party, and the workflow check refuses
470anything outside `ai.*` and the app's own `app.<project_key>.*` (`project_key` = `p` + the project's
471ULID in lowercase, Events' rule for `app.<project_key>.<name>` event types). An app never reads another
472project's runs and never uses a person's BYO provider key: the `byok` provider profile is never
473offered to an app run, and `credential` on its request is refused.
474
475```bash
476AI=https://ai.openvibe.services
477APP=app_01JAB… # the app's client id; SECRET is its client secret
478TOKEN=$(curl -s -X POST https://openvibe.network/oauth/token \
479 -d grant_type=client_credentials -d client_id=$APP -d client_secret=$SECRET \
480 -d audience=openvibe.ai | jq -r .access_token)
481curl -s -X POST "$AI/api/v1/chat" -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
482 -d '{ "messages": [ { "role": "user", "content": "hello" } ] }' | jq .run.provenance
483curl -s "$AI/api/v1/runs/$RUN" -H "Authorization: Bearer $TOKEN" | jq .run.status
484```
485
486A sandbox token is issued only for an audience that opted in (Network's `DEV_SANDBOX_AUDIENCES`);
487AI takes one on these routes alone, and every other route refuses `env: sandbox` with 401
488`token.sandbox_refused`, as before.
489
490## Operator console
491
492The owner-only AI operator console is on [OpenVibe.Exchange](https://openvibe.exchange/console). It uses Exchange sign-in and Exchange's page shell; Exchange reads and writes AI's JSON admin API with the `exchange` service grant. The old `ai.openvibe.services/console` paths return 301 to the matching Exchange path. AI keeps its JSON admin APIs and run receipts, not operator-console HTML.
493
494A person's provider keys and supported browser session cookies are managed at [Your keys and accounts](https://openvibe.exchange/privacy/keys). AI's `/api/v1/me/credentials` endpoints remain person-authenticated JSON APIs; list and balance responses contain metadata only, never secret values.
495
496## Run receipts
497
498`GET /runs/:id` is the person-facing page the Action Center's AI item links to (plan 2026-10-28
499§3.10). It shows one run's status, workflow, model, cost, created/started/finished times and the
500output or the error — never the stored input, the request log, the trace or another person's run.
501It is server-rendered with the console's classes and CSP (no script, no form, no external resource),
502`noindex`, `no-store`, `Referrer-Policy: no-referrer`.
503
504A person signs in through the same Network OAuth client and callback as the console, but
505`GET /auth/person/login` marks the flow as a person flow: the callback stores a read-only session
506(`__Host-ovai_person`, host-only, HttpOnly, Secure, SameSite=Lax, `AI_PERSON_SESSION_TTL_MIN` = 24 h;
507only its hash in `person_sessions`) and redirects only to the exact `/runs/run_<ULID>` page asked
508for. It grants no capability and never opens `/console`. The page shows a run only when the run's
509`on_behalf_of` is that session's `usr_…` subject; any other run is a 404. `GET /api/v1/actions` for
510the Action Center is separate: a first-party service token with `ai.actions.read` names the person.
511
512### Provider keys added from the console (`vault:`)
513
514A provider the owner adds from the console keeps its key in the `ai.provider_keys` namespace of the configuration
515store (D11, `server/settings.js`, `migrations/0028_config.sql`). For each provider `<p>.origin` is the origin of its
516base URL and `<p>.key` is a write-only secret sealed under `OV_CONFIG_KEKS` and x-bind'ed to `<p>.origin`; changing the
517base URL locks the key until it is entered again. The provider's `secret_ref` is `vault:<provider key>`, which
518`util.resolveSecret` reads from this process's cache (refreshed by the store's `onActivate`; providers resolve secrets
519synchronously). Only a four-character hint is ever shown, and audit rows say a key was replaced, never what it is.
520Without a KEK the store still works and reports `"sealed": false`. At boot, when `AI_CREDENTIALS_KEY` and
521`OV_CONFIG_KEKS` are both set, the old `provider_secrets` rows (migration `0010_provider_secrets.sql`) are opened,
522written as sealed `<p>.key` values bound to their current origin, read back and only then deleted; counts are logged,
523never a value.
524
525## The eleven record groups
526
527| Group | Table(s) |
528|---|---|
529| Providers | `providers`, `provider_circuits` (per provider+model breaker), `provider_failure_counts`, `provider_health` (roll-up) — secrets only as `env:NAME` references |
530| Models | `models` (type, limits, cost per Mtok, JSON/tools/streaming/vision support) |
531| Routing profiles | `routes` — versioned; primary + fallbacks, options, output limit, format, timeout, explicit aliases |
532| Prompt templates | `templates` — versioned; input/output schema, system/user prompt, default route, owner, visibility, `draft\|active\|deprecated\|archived` |
533| Workflow definitions | `workflows` — versioned; namespace, input/output schema, steps, default route, cache mode |
534| Workflow runs | `runs` — `queued\|running\|succeeded\|failed\|cancelled\|cached`, requester, on-behalf-of subject, attribution, source service, target EntityRef, idempotency key, trace id, pinned versions |
535| Request/completion log | `requests` — provider, model, route, status, skip reason, fallback flag, prompt/input/output **hashes**, tokens, cost, latency; raw prompts only with `AI_DEBUG_RAW_LOG` and a caller opt-in |
536| Sources/citations | `citations` |
537| Cache | `cache_entries` — scoped (below) |
538| Quotas/usage | `quotas`, `usage_counters`, `usage_daily` |
539| Audit | `audit_log` — every run create/cancel/retry, every provider/model/route/template/workflow/quota change, circuit changes, fallbacks |
540
541Edits to templates, workflows and routes always create a new version; old versions stay readable
542and every run records the versions it used. Boot seeding adds a new version when the code changes a
543definition, but never over an admin's version.
544
545## Providers
546
547| Kind | What | Configured by |
548|---|---|---|
549| `stub` | Deterministic, no key, realistically shaped output (schema-shaped JSON, sentences, unit-vector embeddings, timed transcript segments), clearly marked synthetic (`(synthetic)` text, `synthetic: true` on the run). Never cached. | always present |
550| `openai` | Any OpenAI-compatible API (OpenAI, OpenRouter, Groq, Together, Ollama, LM Studio, llama.cpp): chat with structured-output step-down, embeddings, Whisper-style transcription, image generation (`POST {base}/images/generations`). Ported from Live's `llm.js` / `ai-provider.js`. | `AI_PROVIDER`, `AI_BASE_URL`, `AI_API_KEY`, `AI_MODEL` — Live's setting names |
551| `openai`, key `deepseek` | DeepSeek's own API, registered when its key is configured: `deepseek-flash` and `deepseek-v4-pro`, priced from DeepSeek's price page (peak rates; off-peak is half). A missing key leaves the provider disabled, so none of its models is an offer. | `DEEPSEEK_API_KEY` (or `AI_DEEPSEEK_KEY_REF`), `AI_DEEPSEEK_BASE_URL` |
552| `responses` | The OpenAI Responses API (`POST {base}/responses`) for GPT-family models that only speak it, at any OpenAI-compatible `base_url` (default `https://api.openai.com/v1`): chat and every text operation, structured output through `text.format` with the same step-down, vision; no embeddings or transcription. | the admin API (`kind: "responses"`, `base_url`, `secret_ref`) |
553| `anthropic` | Messages API with cached system blocks and forced-tool JSON. Ported from Live's `llm.js`. | same, with `AI_PROVIDER=anthropic` |
554| `http` | The local HTTP seam: `POST {operation, …}` → `{text, json, usage}` | `AI_HTTP_SEAM_URL` or the admin API |
555| `whisper` | whisper.cpp on this host, with Live's VAD, hallucination filter, multilingual model and live/batch lanes; NVIDIA Parakeet TDT 0.6B v3 (`parakeet-cli`) for English and 24 European languages when `PARAKEET_MODEL` is set, falling back to whisper | `WHISPER_*`, `PARAKEET_*`, `ASR_ENGINE` — Live's names |
556| `openai`, key `local` | A model server on this host speaking the OpenAI API (llama.cpp's `llama-server`, Ollama's `/v1`): no key, never paid; route `media.local`; last offer for short text, one request at a time | `AI_LOCAL_LLM_URL`, `AI_LOCAL_LLM_MODEL`, `AI_LOCAL_LLM_TIMEOUT_MS`, `AI_LOCAL_LLM_MAX_TOKENS` (512), `AI_LOCAL_LLM_CONCURRENCY` (1) |
557| `openai`, key `local2` | A second local chat server, another llama.cpp process and model family: no key, never paid, no JSON or embeddings. Two local offers let a caller pin a match between them (Games' AI-vs-AI job) | `AI_LOCAL_LLM2_URL`, `AI_LOCAL_LLM2_MODEL`, `AI_LOCAL_LLM2_TIMEOUT_MS` |
558
559Routing: a route either names a `capability` and the router builds its pool from every provider that
560can serve it, ordered by `openvibe-sdk/placement` (objective, price, measured latency, health, with
561hysteresis through the route's current placement), or pins a `[primary, ...fallbacks]` list. A
562provider that is disabled, missing credentials, circuit-open or unable to do the operation is
563**skipped and logged**; every call has a strict timeout, cancellation and one retry on a transient
564failure. A failure is classified as `balance` (402, `insufficient_quota`, "credit balance is too low",
565OpenRouter's 402 credits), `auth` (401/403), `rate_limit` (429), `transient` (timeout, 5xx, broken
566stream) or `request` (400/413/422 and context too long). The circuit is per provider+model
567(`provider_circuits`, migrations/0060, survives a restart): balance and auth open it at once and are
568never retried; a run of transient failures opens it for 30 s doubling to 10 min; rate limits and
569malformed requests never open it. A balance/auth circuit may probe the next real request after
570`AI_BREAKER_EXHAUSTED_COOLDOWN_MS` (15 minutes) and closes on a success or when the provider's key
571changes. The catalogue reports `up`, `degraded` (over 5% of at least 10 requests in the last hour) or
572`down`, with `status_reason`, `status_since` and `status_class`. When nothing can serve, the run answers `ai.resting`; a fallback that answers is
573recorded on the run (`fallback_used`), in the request log
574(`fallback = 1`) and in the audit log. Every run response and `GET /api/v1/runs/:id` carry `explain`
575(objective, reasons, selected, and every candidate with its estimated cost, latency and excluded
576reason).
577
578### Bench evidence (BENCH-11, plan §4.12 and §4.7)
579
580AI consumes OpenVibe.Bench's `bench.rating.changed` (through the signed `/internal/events` webhook, subscribed
581at boot with `events.subscription.manage`) and keeps a small `bench_evidence` table: per kind and category, the
582model, provider, Elo rating, confidence interval, games and Bench's cost per solved. A sparse event (Bench's
583current outbox names only `subjects`) is filled from `GET /api/v1/boards/:kind?type=elo|cost`.
584
585A route learns from that only when it opts in — `learn_from_bench: true` on the route, default off; `bench_kind`
586names the board (when absent AI infers it from the route key or operation, e.g. `chat.reply`), and
587`bench_category` defaults to `all`. Among the route's eligible offers, placement then prefers the **cheapest**
588offer whose rating interval overlaps the leader's (the highest non-provisional rating): statistically tied with
589the best, so price decides. A provisional rating (under 10 games) never decides. With no evidence the order is
590exactly the one placement produced. `GET /api/v1/routes/:key` carries a `bench` object with the evidence, the
591leader, the tied offers, the selected offer and the reason; the operator console shows the same read-only.
592
593`GET /api/v1/routes?operation=generate` lists each route's eligible offers as `{ provider, model }`.
594A first-party service holding `ai.run.create` may pin one on `POST /api/v1/generate` (and on
595`POST /api/v1/runs` for `ai.generate`) with `"route": { "provider": …, "model": … }`. The run tries
596that offer alone, never a fallback; a provider/model the routing does not list for `generate` is
597`422 input.invalid` in words, and the caller's `ns` is still checked first (a workflow outside it is
598`403 capability.namespace_denied`). Developer-app tokens (`ai.app.run`) cannot pin. Quotas, tier budgets and Billing readings are exactly as for an unpinned
599run, and a pinned run is never served from the shared cache.
600
601`GET /api/v1/runs/:id` adds `explain.usage_readings` when the run has a Billing reading: per
602reading its `idempotency_key`, `state` (`queued` until Billing accepts it, `sent`, or `failed` when Billing
603refused it), `attempts`, `queued_at`, `sent_at`, `rejected_at`, `next_attempt_at`, `last_error` (the
604outbox's last send error, also while a queued reading is retried) and `free_allowance_used` when some
605**Balance (AI Exchange F; `AI_CREDITS`, off).** A request on a paid managed route — the model catalog's
606`provider_access` is `OWN_DEPLOYMENT` or `AUTHORIZED_WHOLESALE` and the offer is metered — reserves its
607worst case in OpenVibe.Billing before any provider is called: the input estimate plus `max_tokens` of output
608at the model's retail card (`ai.model@1` `retail_rate`). Who pays is the payer's own Balance (D12): an app's
609run holds against its project's Balance, with the hold's subject `{type: "app", id}` and its `project_id`, so
610no Network owner lookup is made; a person's own run holds against their personal Balance, subject
611`{type: "user", id}` and no `project_id`. A first-party service with no payer (no person and no app) is never
612served a paid offer that carries a versioned card (`payer_required`); offers without a card behave as before.
613An exact id whose only offer is skipped that way is `402 billing.payer_required`.
614
615`max_cost_usd` is parsed as an exact decimal (up to 9 fractional digits, finer digits round up; exponent forms
616are 422) into integer nano-USD. When it binds, the affordable output is computed in BigInt from nano-USD per
617million tokens and the provider receives that clamped `max_tokens`; fewer than one affordable token is 402
618before the provider. A pricier managed fallback extends the hold by the difference (clamped the same way); if
619Billing refuses the extension the offer is skipped as `hold_not_extended`. A stream extends the hold in steps
620when the output nears it.
621
622The hold's idempotency key is the request id; its fingerprint covers the resolved model, the token caps, the
623rate version, the payer and (for an app) the project. Billing's `402 billing.insufficient_funds` becomes AI's
624own 402 `Not enough Balance`; any other Billing failure refuses the run with `503 billing.unavailable` rather
625than serving unpaid capacity. At the end one settle carries the four token counts, the answering model's retail
626card version, the supply cost and every attempt in `billing.credit-hold-settle-request@1`'s exact shape
627(`deployment_id`, `provider_access`, start/end, `error_code`); a retried request replays the same settlement
628instead of charging twice. A settle Billing cannot take is kept in `credit_settle_queue` (migration 0025) and
629retried with backoff and the same key; a 409 or 422 marks it `review`, never a retry. A failure before the
630first token releases the hold, an abort settles the partial usage. BYOK — a person's key, their machine, a project's own provider — is never held and
631never charged; Billing is not called at all. A sandbox app token is never offered paid supply (D10), so it
632spends no Balance and never opens a hold. Requires `AI_MODEL_CATALOG` for the access labels; with either
633flag off nothing changes. Code: `server/credits.js`, `server/credit-settlements.js`; the fake-Billing tests are
634`test/credits.test.js` and `test/credits-run.test.js`.
635
636Speech and embeddings use placement pools of registered `stt` and `embedding` models; until
637a second upstream exists, an unavailable sole provider answers `503 provider.unavailable`. Media
638uses separate local and paid pools so paid work still requires its explicit budget. The synthetic
639stub can join general text routes as a last resort outside production only (`AI_STUB_FALLBACK`).
640
641Free allowance (§2.1.8): a rate card's `free_allowance` (tokens per metric, from a models row or
642`AI_PRICING_JSON`) is free provider capacity for **each subject** (the run's attribution, else its
643actor, else its requester) per provider, metric and `reset_period` (`day` and `month` reset at the UTC
644boundary; `none` never resets for the card's terms). An accounted run claims it first, atomically, in
645the same transaction as its usage (`free_allowance_usage`, `server/free-allowance.js`); the free tokens
646are the reading's `free_allowance_used` on its `platform.usage-sample@1`, and its `cost_estimate` leaves
647their price out. It is pricing, never admission: quotas, request counting and the run's own cost are
648unchanged, and no call is ever refused because the allowance is used up. Every card defaults to 0, so
649nothing is free and the field is absent until an operator configures an allowance.
650
651Free-use sources and price ceiling (`AI_FREE_PROVIDERS`, default `deepseek,local,community`;
652`AI_FREE_MAX_OUTPUT_PRICE_USD_PER_MTOK`, default 1.5): a run that nobody pays for — the free allowance
653(a first-party service naming a person, without that person's own token) or the Exchange's anonymous Try
654it — may use only offers from those sources and at or below this output price per million tokens. A
655source is a provider key; `local` covers every local model server (`local`, `local2`, whisper.cpp) and
656`community` the community machines. The design funds free use from the owner's DeepSeek key, local models
657and volunteers, never from Live's shared OpenAI key. The router skips the rest and answers
658`402 billing.payer_required` when every capable offer is outside those rules. A person's own token (and so
659their Balance, AI #311), their own provider key, an app's project and a service's own run-token budget are
660not capped this way. The check is admission, not pricing: it never changes a price or a Balance reading.
661The Exchange catalog marks each offer `free_tier` by the same two rules (distinct from `free`, which means
662prepaid capacity with no per-token price).
663
664Abuse prevention for the free tier (plan E5; `server/abuse/`, `docs/abuse-prevention.md`, privacy note
665`docs/privacy.md`): the daily free allowance is `AI_FREE_DAILY_TOKENS` (default 20,000) scaled by trust — ¼ for a
666new or unvouched account, ½ after 7 days, full when Network says a passkey, a linked account or 30 days with
667activity verifies it, plus up to one extra full for a volunteer's kudos. A **hosting/data-centre ASN** is always
668the new tier and a lower rate; the address is looked up in a local monthly iptoasn.com table and only the ASN and
669an HMAC of its `/24`–`/48` are kept, for 24 h. A signed `HttpOnly` device cookie counts accounts per device, and
670more than three accounts on one device share one allowance at the lowest tier. The anonymous Try it needs one
671in-house hashcash proof of work (`POST /api/v1/exchange/tryit/challenge`). Every finished free run feeds a
672per-minute velocity check per ASN, device and input bucket against the last week's baseline; above it the run is
673`429 ai.abuse_throttled` and an `abuse.throttled` row goes to `/console/abuse` and the
674`ai_abuse_throttles` gauge (OpenVibe.Host alerts on it). No raw IP, device id or prompt is stored.
675
676Tier budgets (T6 step 8, decision D5; `server/govern.js`, off unless `AI_GOVERN_TIERS=1`): the SaaS tiers
677`free`, `paid` and `staff` on `openvibe-sdk/govern`, counted in Valkey. Before a run touches a provider
678(and before `reserve()` counts it), it holds an estimate of the units `ai-token` and `ai-usd` against two
679budgets: its **subject** (attribution, else actor, else requester) inside its **project** (the calling
680product, e.g. `service:live`), and the **project** as a whole. Each hold is one atomic step, so two runs
681never both take the last unit; a hold that does not fit refuses the run with `govern.exceeded` (429 +
682`Retry-After`). When the run is accounted the hold becomes its real tokens and its priced cost (the free
683allowance's share left out, so a fully free run spends no `ai-usd`); a run that spent nothing gives it back.
684Like the quotas, a run that spends more than its hold bounds the next run, not itself. A subject's tier is
685its own `AI_GOVERN_TIER_MAP` entry, else its project's, else `AI_GOVERN_DEFAULT_TIER`. With the tiers on,
686the free allowance's counter lives in the same Valkey (same numbers, one atomic claim per metric);
687`free_allowance_usage` keeps its total for the console and is the floor a lost key restarts from. Each
688`platform.usage-sample@1` comes from the `onUsage` record of an in-process govern meter, with the same
689idempotency key (`ai:<run>:tokens`), whether the tiers are on or off. **Valkey unreachable:** a run with
690any budget on `paid` or `staff` is refused with `govern.unavailable` (503, fail closed); an all-`free` run
691is admitted on the PostgreSQL quotas alone (today's behaviour) and its tokens are priced in full (nothing is
692free while the counter cannot be read). Reads never touch the tiers. With the flag off nothing here runs and
693the service boots without Valkey; with it on, production refuses to boot without `VALKEY_URL`.
694
695| Setting | Default | Meaning |
696|---|---|---|
697| `AI_GOVERN_TIERS` | `0` | `1` turns the tier budgets on |
698| `AI_GOVERN_POLICY_JSON` | free: 200,000 `ai-token`/day and 2,000,000/month, $0.50 `ai-usd`/day and $5/month; paid: 5,000,000 `ai-token`/day, $50/day; staff: unlimited | govern policy `{ unit: { tier: { minute?, hour?, day?, month? } } }` for `ai-token` and `ai-usd`; a missing tier or window is unlimited, 0 refuses |
699| `AI_GOVERN_TIER_MAP` | empty | `subject=tier` pairs, comma separated (`service:live=paid,user:usr_1=staff`) |
700| `AI_GOVERN_DEFAULT_TIER` | `free` | the tier of a subject or project not in the map |
701| `AI_GOVERN_HOLD_TOKENS`, `AI_GOVERN_HOLD_USD` | `4000`, `0.02` | the estimate a run holds before its provider call |
702| `AI_GOVERN_TIMEOUT_MS` | `1000` | longest wait for Valkey before it counts as unreachable |
703| `VALKEY_URL`, `VALKEY_PREFIX` | empty | the shared Valkey (ADR-035) the counters live in |
704
705## The living price book
706
707The price the router charges and the Exchange shows is read from the provider that sells it, and from nowhere
708else. A price nobody publishes stays unknown: it is never guessed, never averaged, never taken from an
709aggregator.
710
711Once a week (plan E1, 2026-10-30 §8) a job reads each catalogued provider's own model and price listing —
712DeepSeek's price page, Alibaba Model Studio's price table, Kimi's pricing page, Z.ai's and MiniMax's price
713pages, DeepInfra's, Novita's, Featherless', Refuseless' and Abliteration.ai's listings — through the same egress
714guard the rest of the platform uses: https only, a public host, never an IP literal, a timeout and a size cap.
715OpenRouter's public model list is read as well, but only as a second opinion; no price ever comes from it.
716
717A price never changes silently. Every change lands in the `price_changes` log with the old price, the new
718price, the listing it was read from and the day it was read. A change of **more than 20%** waits there for the
719owner, who approves or rejects it on the operator console ([price book](https://openvibe.exchange/console/pricebook), `ai.provider.manage`); a
720smaller one the job applies at once, and the model row carries the new price with the day it was checked. A
721listing that cannot be read leaves every price alone: an unreadable page is not evidence of a change. A price a
722person typed by hand is never touched.
723
724A reviewed reading can also be put in front of the owner without waiting for the weekly job:
725`POST /api/v1/admin/console/pricebook/proposals` (owner-only, `ai.provider.manage`) takes the same readings
726`scripts/propose-pricebook.js` reads from a JSON file. Both write **pending** changes only — the source listing and
727the day it was read travel with each row — and a model the registry does not know is created switched off with no
728price, so there is a row to approve against. Nothing is applied until the owner approves it.
729
730The book is **due-based**, not timer-based: AI is deployed several times a day, so a timer in the process
731would be reset every time and the week would never come round. The last finished run is a row in the database
732(`price_book_state`, migration 0037) and the service asks once an hour whether the interval has passed since
733then; the first ask is a few minutes after a boot, so the boot path stays free. AI runs several workers, so a
734run is claimed before it starts: only one of them reads the listings at a time, and the claim expires on its own
735if a worker dies.
736
737The catalog says when a price was checked (`price_checked_at`, `price_checked`), and
738`GET /api/v1/exchange/price-changes` is the changelog Exchange renders: what a price was, what it became, the
739listing it was read from and the day it was read.
740
741| Setting | Default | Meaning |
742|---|---|---|
743| `AI_PRICE_BOOK` | on (off in test mode, off under `AI_DRILL=1`) | the weekly job |
744| `AI_PRICE_BOOK_INTERVAL_MS` | 604800000 (a week) | how long between two runs |
745| `AI_PRICE_BOOK_TICK_MS`, `AI_PRICE_BOOK_FIRST_TICK_MS` | 3600000, 300000 | how often the service asks whether the book is due, and how long after a boot the first ask is |
746| `AI_PRICE_BOOK_CLAIM_TTL_MS` | 900000 | how long a claimed run holds the claim |
747| `AI_PRICE_BOOK_THRESHOLD_PCT` | 20 | a change of more than this waits for the owner |
748| `AI_PRICE_BOOK_TIMEOUT_MS`, `AI_PRICE_BOOK_MAX_BYTES` | 20000, 8 MiB | the guard's timeout and size cap |
749
750The tests never touch the network: the listings are recorded (`test/fixtures/pricebook/`, written by
751`scripts/record-price-book.js`) and replayed through the reader's transport seam.
752
753## Webhooks and the MCP server
754
755The Exchange's developer tools (the plan of 2026-11-06, "For builders", item 7): `openvibe-sdk/exchange` has
756verified webhooks since 0.91.0; `server/exchange/webhooks.js` is the sender, and `server/mcp/exchange.js` the MCP
757server.
758
759### Webhooks
760
761A person's token or a developer app registers an https receiver on the Exchange API:
762
763| Route | What |
764|---|---|
765| `POST /api/v1/exchange/webhooks` | `{url, events?, description?}` → `201 {webhook}`; the signing `secret` (`whsec_…`) is in this answer only |
766| `GET /api/v1/exchange/webhooks` | the caller's receivers (never a secret) |
767| `DELETE /api/v1/exchange/webhooks/{id}` | remove one and its delivery history |
768| `GET /api/v1/exchange/webhooks/{id}/deliveries` | the delivery log: event, attempts, status, the receiver's answer and the payload |
769
770Three events: `usage` (an answered run: the tokens, the money and who paid), `earnings` (a community offer earned
771credits or kudos) and `offer.down` (an endpoint offer was removed). Every delivery carries
772`X-OpenVibe-Signature-V2: t=<unix seconds>, v2=<hex HMAC-SHA256 of "<t>.<raw body>">` — exactly what
773`require('openvibe-sdk/exchange').verifyWebhook(rawBody, header, secret)` checks — plus `X-OpenVibe-Event`,
774`X-OpenVibe-Delivery` and `X-OpenVibe-Webhook-Id`. A payload never carries a prompt or an answer.
775
776The receiver URL goes through the same egress guard as a community endpoint: https, a public host name, never an IP
777literal, port 443 or 8443, no query or fragment, and the DNS answer is judged inside the connection. A delivery is a
778row, not a call in the request that caused it: a 408/425/429/5xx or a network failure is retried with an exponential
779backoff (`AI_WEBHOOK_BASE_DELAY_MS`, up to `AI_WEBHOOK_MAX_DELAY_MS`, `AI_WEBHOOK_MAX_ATTEMPTS`); any other non-2xx
780fails it at once. The worker is off in test mode and in `NODE_ENV=test`.
781
782### The MCP server
783
784`bin/openvibe-exchange-mcp.js` speaks MCP over stdio in the repo that owns the Exchange API:
785
786```json
787{
788 "mcpServers": {
789 "openvibe-exchange": {
790 "command": "openvibe-exchange-mcp",
791 "env": { "OPENVIBE_EXCHANGE_TOKEN": "your Network developer key" }
792 }
793 }
794}
795```
796
797The tools are `exchange_models` (the live catalog: price in/out per million, measured p50/p95, uptime, health and the
798data policy), `exchange_chat` (one completion on the person's allowance, or their own stored provider key when
799`credential` names a provider) and `exchange_usage` (the person's own requests, tokens and spend by day and model,
800from `GET /api/v1/exchange/me/usage`). `exchange_models` is public; the other two refuse a call without a token
801before any request, and a 401 from the Exchange is refused back as the tool's error. The token is never printed,
802logged or stored.
803
804### The CLI
805
806`openvibe-link exchange serve --model qwen3:32b --public-url https://…/v1` (OpenVibe Link) serves the local model
807through an OpenAI-compatible listener and registers it on the existing endpoint-seller API with the person's
808`OPENVIBE_DEVELOPER_KEY`; Ctrl-C removes the offer. See that repo's README.
809
810## Workflows
811
812- **Seed product workflows** — `wiki.generate_space`, `wiki.generate_page` (both on route `wiki.write`, see [MegaSource](#megasource-research)), `blog.draft_post`,
813 `news.summarize_story`, `news.compare_perspectives`, `reviews.summarize_entity`,
814 `deals.enrich_deal`, `coupons.extract_coupon`, `trade.summarize_market_context`,
815 `codes.generate_docs`, `games.generate_lore`, `moderation.classify`, `food.suggest_recipes`,
816 `rent.screen_listing` (a rental listing's scam signs for Rent's staff: `allow` or `review`, quoting the words). They take numbered
817 `sources`, return `citations` (indices the engine bounds to the sources given) and `gaps`, and
818 never invent facts: Reviews has no rating field, a Deals price is null unless cited, a Coupons
819 expiry stays unknown unless stated, Trade carries its informational-only disclaimer. The
820 historical route keys (`wiki.generate`, `news.summarize`, …) are explicit aliases of `default.json`.
821 `blog.write_digest` writes an OpenVibe.Blog desk's digest from its sources' items, every section cited.
822- **Satire** — `blog.write_satire` (Blog's "Is This Real?" desk) is invented on purpose and takes no sources: absurd
823 mock-explainers that never name or quote a real person or put words in a real organisation's mouth, never joke about
824 tragedies, minors or groups of people, and never give steps for anything dangerous. Blog labels each post as satire
825 written by AI.
826- **Live's features** — `live.translate`, `live.paste.describe_image`, `live.paste.summarize_text`,
827 `live.stream.describe_frame`, `live.stream.summarize`, `live.streamer.overview`,
828 `live.media.overview`, `live.stream.recap`, `live.media.transcribe`, `network.site_copy`
829 (prompts moved verbatim), plus passthrough workflows for the features whose prompts Live still
830 renders (`live.viewers.*`, `live.chat.insight`, `live.moments.*`, `live.hero.slogans`,
831 `live.arena.*`, …). See `docs/migration.md`.
832- **Media** — `media.analyze` (roadmap WS-O task 5), local-first analysis of a VOD, clip or audio file by `media_url`
833 or MediaRef, fetched like any media input (allow-listed hosts) into `media-tmp` beside the database
834 (`AI_MEDIA_ANALYSIS_DIR`; never `/tmp`, which is memory on the production host). The file cap is
835 `AI_MEDIA_ANALYSIS_MAX_BYTES` (4 GB), also bounded by that disk's free space minus
836 `AI_MEDIA_ANALYSIS_DISK_RESERVE_BYTES` (3 GB). Otherwise the run is refused with `media.no_space` before downloading. Everything runs on this host, one analysis at a time, at nice 15:
837 1. **Signals**, one FFmpeg pass with video decoded at keyframes only: scene changes (`scdet`, threshold
838 `AI_MEDIA_SCENE_THRESHOLD`), black and frozen picture, silence, and loudness. Loudness has the integrated level
839 and range, the typical level (the median audible second) and loud moments at least 6 LU above it.
840 2. **Scenes** from the scene changes (at most 300).
841 3. **Speech**: whisper.cpp through `live.stt`, with another registered STT model eligible for fallback.
842 4. **Highlights**: windows scored by loud, scene changes and speech, each with its reasons and transcript excerpt.
843 5. **An overview** from a local model (`media.local`), else extractive from the measured facts. A paid provider
844 (`media.paid`, including the shared provider) writes it only with `allow_paid: true` **and** an active quota with
845 `workflow_prefix` `media.paid` and `max_cost_usd` > 0, while today's `media.*` spend is under it. That quota
846 gates only this call, never whole runs.
847
848- **Chat's AI members** ([server/workflows/chat.js](server/workflows/chat.js)): `chat.member.reply` (namespace `chat`, route
849 `chat.member`, an alias of `default.chat`, text out). A room on OpenVibe.Chat adds an AI member with a name and how
850 it should be; when someone names it, Chat sends the recent messages, the room's notes and any shared context a linked
851 service gave the room (Food: a household's pantry and plan), and posts the reply. The prompt forbids inventing facts
852 about the people or their things and claiming actions beyond writing in the chat.
853 `chat.member.turn` is the same member with tools: a Chat Completions request (Chat's system message, the transcript,
854 the tools Chat runs) passed through like `ai.complete` (the prompt is not kept) on route `chat.member`, so Chat's grant
855 and quota apply and a person's own key reaches only their provider; the choices come back with any tool calls, and
856 Chat sends the results in its next turn.
857 `chat.vibe.listen` is the Dock's voice on browsers without speech recognition: Chat sends a short recording
858 (audio/webm, audio/ogg or audio/mp4, at most 1 MB) inline as a data URL, the existing `transcribe` step runs it on
859 route `live.stt` (whisper.cpp/Parakeet), and the text comes back. The input is never retained (`retain_input:
860 false`); the engine writes it to a private temp file and deletes it in the same call.
861- **Space's spaces** ([server/workflows/space.js](server/workflows/space.js), namespace `space`): `space.app.build`
862 is the AI builder of OpenVibe.Space. From what a person asks for (and a space's current files when changing one),
863 it returns a working space: name, description, the instructions its AI follows, the SDK features it uses, and up to
864 six files with an `index.html`. Paths are relative and safe, and an answer without a real page is no answer. It runs
865 on route `space.build` (the creative model, 180 s, 16,000 output tokens), on behalf of the builder. `space.app.ask`
866 is a space's own AI (`OV.ai.ask` in the space SDK): the maker's instructions under the platform's rules, on route
867 `space.ask` (an alias of `default.chat`), on behalf of the visitor, so it counts against their daily allowance.
868
869 The builder's prompt can carry OpenVibe.Design's public `design.component@1` registry in a `components` input
870 (Space reads and caches it): the definitions are then the interface vocabulary and the answer marks each major
871 part with `data-ov-component`. `overview.source` says which (`local_model`, `paid`, `extractive`), and `gaps`
872 lists what is missing (no local model, no speech-to-text, no budget). The output is not cached: callers keep it.
873 Live's grant names `media.analyze`.
874- **Direct operations** — `ai.chat|generate|summarize|classify|extract|enrich|embed|image`.
875
876## MegaSource research
877
878`research.gather` (namespace `research`, `server/research/`, `server/workflows/research.js`) answers one question from
879many sources at once and returns one ranked, de-duplicated evidence pack the product workflows cite by number. No model
880is called; a run is recorded like a provider call (provider `research`, model `megasource`), so the web route's per-call
881price counts against the cost quotas and the daily usage.
882
883| Source | What it gives | Terms |
884|---|---|---|
885| Wikipedia | the best articles' full text, their pictures (each with its licence; an article's own non-free files are marked) and the Wikidata id | CC BY-SA 4.0 |
886| Fandom | the topic's own community wiki when it has one (from the web results' hosts, else the topic as a subdomain, confirmed by the wiki's name): pages read through its MediaWiki API, infobox rows kept as "Label: value" | CC BY-SA 3.0; pictures marked not free |
887| Wikidata | the top article's key statements as labelled facts, and its picture | CC0 |
888| Search Fabric | the web through OpenVibe.Search's web scope (`POST /api/v1/query`, capability `search.fabric.query`) first: the open index, then the providers Search routes to; every page keeps its provider, URL and dates | per provider Search used |
889| DeepSeek | the fallback web search when the fabric is off, refuses or answers nothing: page text (Search API) and full text for short results (Fetch API), fetched by DeepSeek, never from this host | $0.75 per 1,000 successful calls each |
890| Commons, Openverse | freely licensed pictures (CC0, public domain, CC BY, CC BY-SA; NonCommercial and NoDerivatives are not counted as free), kept only when their title or description names the subject | per picture |
891
892Input: `query` (required), `topic` (the subject it belongs to: finds the topic's wiki), `focus` (one subject inside the
893topic), `lang`, `max_sources` (≤ 20), `max_images` (≤ 24), `content_chars` (≤ 12,000), `providers` (a subset). Output:
894`sources` (each with `url`, `title`, `content`, `published_at`, `retrieved_at` and `provenance.provider|site|via|license`),
895`images` (each with `license`, `free`, `author`, `source_url`, `provider`), `facts`, `topic_wiki`, `providers` (per route:
896ok, ms, count, cost, or why it was skipped or failed) and `usage` (requests, DeepSeek calls, fabric calls and the fabric's
897`search_fabric_cost_usd`; `cost_usd` is this run's own DeepSeek calls, so a paid fabric step is not billed twice —
898Search and its nested AI run pay it). Cached for a day per calling service.
899
900Web search asks the Search Fabric first, as AI's own service principal (audience `openvibe.search`). Search's viewer
901requires `search.query.delegate` of a service token, so Network must grant `ai` that capability on `openvibe.search`
902(the route's `search.fabric.query` is public). The token is requested with exactly that scope; without
903`OV_OAUTH_CLIENT_SECRET` or with `AI_RESEARCH_SEARCH=0` the fabric is skipped. When Search's own web ladder calls this
904endpoint for DeepSeek or Wikipedia, research keeps DeepSeek direct, so Search is never asked back in a loop. DeepSeek is
905used only when its key resolves (`AI_RESEARCH_DEEPSEEK_KEY_REF`, default `env:DEEPSEEK_API_KEY`;
906`AI_RESEARCH_DEEPSEEK=0` turns it off), only for first-party callers (a developer app's run reads the free sources),
907within `AI_RESEARCH_DEEPSEEK_DAILY` calls a day (1500), and it stands down on its own answers: 402 (balance used up) for
90830 minutes, 401 for an hour, 429 for a minute. Tests read a canned web (`test/fixtures/research-fetch.js`); none reaches
909the internet.
910
911OpenVibe.Wiki's "vibe a wiki" uses it with `wiki.generate_space` (plans the pages: main page first, each with a kind,
912an outline and a search phrase) and `wiki.generate_page` (writes one page: lead, sections, infobox, `[[links]]` to the
913planned pages, and up to three of the numbered pictures by heading), both on route `wiki.write` (the creative model,
9147,000 output tokens, 170 s, past the provider's chat timeout).
915
916## Image generation
917
918Off unless `AI_IMAGE_MODEL` names the shared provider's image model (`dall-e-3`, `gpt-image-1`; OpenAI-shaped
919providers only). Then boot gives `shared` the `image` capability, registers the model as type `image` and seeds
920route `default.image` (a capability pool of registered `image` models, objective `correctness`,
921`provider_scope: paid`, timeout 120 s, above `AI_PROVIDER_TIMEOUT_MS`; a timed-out image is not retried, so it is never paid for twice). Without it no route is seeded and `POST /api/v1/image` answers
922`503 route.unavailable`, like any operation whose route is not configured.
923
924`POST /api/v1/image` (`ai.run.create`, or `ai.app.run` for an app) takes `{ prompt, size?, n? }` (`n` 1–4, 1 for `dall-e-3`;
925`size` defaults to `AI_IMAGE_SIZE`, `1024x1024`) and answers `output: { images: [{ b64, url }], count }`
926(`dall-e-*` is asked for `b64_json`; `gpt-image-1` answers base64 already). A missing prompt is
927`422 input.invalid`, a provider that returns no image `502 output.invalid`, no provider `503 provider.unavailable`.
928Outside production the stub answers a 1×1 PNG as the last resort (`AI_STUB_FALLBACK`).
929
930Images are metered per image, never per token: the request log's `operation` is `image` and `tokens_out` the
931image count, and each attempt is one `platform.usage-sample@1` reading (`resource` `output-images:<model>`,
932`unit` `image`, `quantity` the images returned), queued like every other reading. No rate card prices an image
933yet (an owner/T5 item, never AI-written), so an image costs 0 in AI's usage and cost quotas; Billing stores the
934reading unrated until a per-image card exists and its `unitFits()` accepts `image`.
935
936## Local model
937
938`deploy/systemd/openvibe-llm.service` runs llama.cpp's `llama-server` on `127.0.0.1:8090` (OpenAI API, no key),
939which AI reaches as the provider `local` and the route `media.local` (`AI_LOCAL_LLM_URL=http://127.0.0.1:8090/v1`,
940`AI_LOCAL_LLM_MODEL`). On the production host since 2026-09-27: llama.cpp b11222 (the CPU build, checked against
941the release's sha256) in `/opt/llama.cpp/b11222` with `/opt/llama.cpp/current`, and Qwen2.5-1.5B-Instruct Q4_K_M
942(Apache-2.0, checked against Hugging Face's sha256) in `/var/lib/openvibe-llm/models`. It is held back so live
943streams always win: 2 threads, at most 1.5 CPUs, nice 19, 3 GB. It uses about 0.9 GB and generates about
94428 tokens/s. Without it, `media.analyze` writes an extractive overview and says so. To swap the model, put the
945file beside it, change `--model`/`--alias` and `AI_LOCAL_LLM_MODEL`, and restart both units.
946Both servers run two slots (`--parallel 2`, 2026-11-07): llama.cpp batches the slots into each step, so a second
947request no longer waits for the first; the 8192-token context is split, 4096 a slot. Install a changed unit by hand:
948`sudo install -m 0644 deploy/systemd/openvibe-llm*.service /etc/systemd/system/ && sudo systemctl daemon-reload &&
949sudo systemctl restart openvibe-llm openvibe-llm2` (AI falls over to `shared` for the seconds they restart).
950
951`deploy/systemd/openvibe-llm2.service` is the optional second chat server: the same llama.cpp build and models
952directory on `127.0.0.1:8092`, a different model family, and the provider `local2`
953(`AI_LOCAL_LLM2_URL=http://127.0.0.1:8092/v1`, `AI_LOCAL_LLM2_MODEL`). It is held to 2 threads, at most 1 CPU,
954nice 19 and 2 GB, and it never advertises `json` or `embed`. Games' AI-vs-AI job pins one local offer per side,
955so the two models must be different families and both aliases must be the `model` in its `GAMES_AI_MATCHES_OFFERS`
956pair. On the production host since 2026-11-06: Llama-3.2-1B-Instruct Q4_K_M (Llama 3.2 Community License,
957Hugging Face sha256 `6f85a640a97cf2bf5b8e764087b1e83da0fdb51d7c9fab7d0fece9385611df83`, 807,694,464 bytes),
958checked against that hash before install; it uses about 0.8 GB.
959
960The local model is a **last offer for short text** (summaries, titles, moderation hints, a chat line), never
961for code or long reasoning: routes with `max_output_tokens` over 800 or `outlast_provider_timeout` (an app or a
962wiki page) never offer it. It answers with `AI_LOCAL_LLM_MAX_TOKENS` (512) at most, and one request at a time
963(`AI_LOCAL_LLM_CONCURRENCY`, 1) across `local` and `local2`, so a paid outage cannot starve the streams sharing
964the host. A run that it answered names `local`/`local2` as its provider and marks the receipt local.
965
966Local embeddings are off unless `AI_LOCAL_EMBED_MODEL` names an embedding model the server at `AI_LOCAL_LLM_URL`
967answers at `/embeddings`. Then `local` advertises `embed`, the model is registered as an `embedding` model, and
968`default.embedding` (a capability pool) offers it beside the shared provider's `AI_EMBEDDING_MODEL`; `local` is
969billing profile `free`, so its readings carry no cost. llama.cpp's `llama-server` answers `/embeddings` only when
970started with `--embedding` and a GGUF that supports embeddings (a chat-only model answers 400, and every embedding
971call then fails over to the shared provider with an error in the request log). That flag restricts llama-server
972to embeddings, so the Qwen chat unit above runs without it: embeddings need a server that serves both on one URL
973(Ollama's `/v1`, for example), or `AI_LOCAL_LLM_URL` given over to an embedding-only server. **Vector dimensions
974differ per upstream**: a run says which provider and model made its vectors, and a consumer must never compare or
975mix vectors from different providers (re-embed when the provider changes).
976
977## Acceptance: guarantees and where they are tested
978
979`npm test` runs every `test/*.test.js` with the stub provider, temp databases and local servers only.
980
981| Guarantee | Test |
982|---|---|
983| Every workflow has versioned, compiling input/output schemas, runs on the stub and validates | `test/workflows.test.js` |
984| Edits are new versions; runs pin versions; lifecycle; admin versions survive reseeding | `test/versioning.test.js` |
985| A degraded primary falls back and records it; breaker; skips; timeouts; explicit `provider.unavailable` | `test/fallback.test.js` |
986| 402/insufficient-quota opens the long exhausted circuit, never retries, fails over, then `ai.resting` | `test/provider-exhausted.test.js` |
987| Every provider error shape maps to balance/auth/rate_limit/transient/request; per-offer circuits, 30 s doubling backoff, persistence, key reset, catalogue status and the owner alert | `test/provider-circuits.test.js` |
988| Hourly DeepSeek balance gauge, no key in logs, off under a drill | `test/provider-balance.test.js` |
989| Quotas refuse with 429 + Retry-After **before** any provider call; per-service, attribution, cost caps | `test/quota.test.js` |
990| Free allowance: free up to the card's allowance then fully priced, reset per day/month (never for `none`), per subject, never past the cap under concurrent claims, absent at 0 | `test/free-allowance.test.js` |
991| Abuse prevention (plan E5): allowance by trust tier, kudos bonus, hosting override, `/24`–`/48` hashes, ASN table, signed device cookie, >3 accounts share one allowance, rate limit, per-minute velocity against the week's baseline, single-use proof of work, operator events and prune | `test/abuse-trust.test.js`, `test/abuse.test.js`, `test/abuse-pow.test.js` |
992| The run path refuses a spent daily allowance (402) and the anonymous Try it without/with a used proof of work (403) | `test/abuse-runs.test.js` |
993| Tier budgets: enforced per subject and per project and reset per window, two concurrent runs never both take the last unit, the free allowance's numbers unchanged on the Valkey store, one reading per accounted run from govern's `onUsage`, Valkey unreachable (paid 503, free admitted), off by default | `test/govern-tiers.test.js` |
994| The cache never crosses requester, actor, target or attribution scope | `test/cache.test.js` |
995| Queue caps: one caller cannot fill the run queue (per-caller and global 429 `queue.full` + Retry-After) | `test/queue.test.js` |
996| Idempotency, cancel (running and queued), retry, async polling, audit rows, restart recovery, no raw prompts or inline images kept | `test/runs.test.js` |
997| Media fetched only from allow-listed https OpenVibe hosts; DNS answers and every redirect hop re-checked (a public hop never reaches internal Media); size caps | `test/ssrf.test.js` |
998| The shared compiled-schema cache (caller-supplied schemas) is an LRU bounded by count and bytes, in Ajv too | `test/schemas.test.js` |
999| Token and capability denial, namespaces, no secret values in any response, runs private to the requester | `test/auth.test.js` |
1000| Developer apps: `ai.app.run` on the six direct operations and `GET /runs/:id`, 403 on every other route, project-scoped runs, readings and free allowance per project, govern tiers keyed by the project, sandbox and ungoverned runs free/local capacity only, a byok profile and `credential` never selected | `test/app-access.test.js` |
1001| Operator console: owner-only pages and controls on Exchange; AI keeps JSON admin APIs and redirects the old paths | Exchange `test/console-merge.test.js`, AI `test/home.test.js` |
1002| Public price/latency page: 200 without auth, rows from seeded stats and rate cards, the empty state, no caller ids, own-key providers, URLs or secrets | `test/stats-page.test.js` |
1003| The OpenAI Responses adapter (`kind: responses`) with a stubbed fetch: request shape, output and usage parsing, `text.format`, error mapping, the key never in an error | `test/provider-responses.test.js` |
1004| Ported adapters against fake OpenAI/Anthropic servers, stub determinism, whisper filter, templates | `test/providers.test.js` |
1005| OpenAI Chat Completions and Anthropic Messages pass the official-client fixtures (tool call + tool result, JSON response_format / structured tool, multi-turn system prompt, exact id, every alias) in JSON and SSE; usage is present and x_openvibe is top-level only | `test/protocol-compat.test.js` |
1006| Parakeet: engine by language, 30 s pieces past 45 s, fallback to whisper, `ASR_ENGINE=whisper` | `test/parakeet.test.js` |
1007| Word timing (`live.media.transcribe` with `words: true`): whisper's `-ojf` tokens and Parakeet's token lines become each segment's words with their times; punctuation joins a word without moving its end; one unit per token in Japanese; fillers and control tokens are not words | `test/word-timing.test.js` |
1008| `media.analyze` on a real 28 s fixture: signals, scenes and loud moments measured, highlights with evidence, the local model's overview from the facts only; the local model down → extractive and said; paid only with `allow_paid` and a `media.paid` budget, never past it; too little disk refused before downloading | `test/media-analysis.test.js` |
1009| Import from a Live snapshot: dry run, holds, idempotent re-run | `test/import.test.js` |
1010| Routing presets: the five built-in ones, a caller's own stored per account and per project, each knob applied (floor, ceiling, max_price, quantization, region, data policy, community), a hard max_price refused before the call with a problem document, every answer's receipt (offer, price, tokens, cache hits, latency, why, fallbacks, privacy label, BYOK at 0 %) in the headers and at `GET /v1/receipts/{id}`, and the Anthropic endpoint and the stream carrying the same receipt | `test/exchange-presets-receipts.test.js` |
1011| Capability proposals are valid contracts documents matching what is enforced | `test/proposals.test.js` |
1012| The living price book: every listing read in its own shape, the guard refusing an IP literal, a change of 20% or less applied at once, one of more than 20% waiting for the owner and applied only on approval, the change log, the catalog's "price checked" date, the public changelog, the job off under a drill, the book due-based (a restart before the week does not run it, a restart after it does) and two instances at the same moment never both running | `test/price-book.test.js` |
1013
1014## Security
1015
1016Reporting a vulnerability: [SECURITY.md](SECURITY.md). The rules the code keeps:
1017
1018- **Auth.** Every API route needs a Network service token for audience `openvibe.ai`, with the route's
1019 capability and, for runs, a namespace its `ns` claim allows. Runs are private to their requester.
1020 A developer-app token is accepted only on `ai.app.run`'s routes ([Developer apps](#developer-apps)),
1021where it acts as its project; every other route refuses it. The owner-only operator console is on OpenVibe.Exchange;
1022AI's admin JSON endpoints still require their named capabilities.
1023- **Secrets.** Shared provider keys are `env:NAME` references, never values; a person's own key is
1024 AES-256-GCM encrypted with `AI_CREDENTIALS_KEY`, bound to its owner and subject, and never returned
1025 (a four-character hint only). No response, log or console page carries a secret value.
1026- **Memory.** Item text is sealed with `AI_MEMORY_KEY` and bound to its owner, id and scope; plaintext exists only
1027 inside a request, and the profile builder calls the provider in-process so the notes are not written to a run.
1028- **Private data.** The request log keeps hashes, not prompts; raw prompts and inline images are not
1029 kept; the cache never crosses requester, actor, target or attribution; `history: false` keeps
1030 neither input nor cache entry.
1031- **Egress.** Provider calls go only to configured providers. Media is fetched only from allow-listed
1032 https OpenVibe hosts, with every DNS answer and redirect hop re-checked and size caps
1033 (`test/ssrf.test.js`); ffmpeg reads through a loopback proxy that applies the same rules.
1034- **Abuse.** Quotas refuse with 429 before any provider call; queue caps per caller and overall.
1035 `/metrics` answers direct loopback callers only.
1036
1037## Live and Network
1038
1039`docs/live-patch.diff` added `AI_SERVICE=remote` to Live (deployed as Live `fa22de5`; production runs
1040with `AI_SERVICE=remote` since 2026-09-23): Live's shared-key AI calls become runs here, authenticated
1041with `serviceHeaders('openvibe.ai')` from Live's `server/net/network-principal.js`. Without the flag
1042Live's behaviour is unchanged. The one-time `scripts/import-from-live.js` moved the AI records that belong here;
1043it ran on production on 2026-09-23 at 18:29 UTC (79,657 ledger rows, 11 holds recorded with their reasons, kept in
1044`import_ledger` and `import_holds`) and was retired with the move to PostgreSQL (it is in git history).
1045Network's footer copy calls the `network.site_copy` workflow (Network `422f8e9`), but the deployed Network still
1046falls back to Live's `/internal/ai/site-copy`, and Live still serves that route. The full plan, holds
1047and rollback are in `docs/migration.md`.
1048
1049## Deploy
1050
1051Production deploys with `sudo ovhost deploy ai` on the host (strategy `git-checkout`: fetch,
1052fast-forward `/opt/openvibe.ai`, install on a lockfile change, restart, wait for `/api/ready`).
1053The unit is `openvibe-ai.service` on `127.0.0.1:4700`, the env file `/etc/openvibe/ai.env`. The database is
1054`ov_ai` on the host's data role (`sudo /opt/openvibe.host/roles/data/add-service.sh ai` writes its settings); the
1055release migrates it at boot. The local model
1056runs as `openvibe-llm.service` on 127.0.0.1:8090 ([Local model](#local-model)).
1057Rollback: ovhost puts the previous sha back by itself when `/api/ready` does not answer 2xx after the
1058restart; afterwards `sudo ovhost rollback ai --to <sha>`. Migrations only add tables and columns.
1059
1060Backups and drills: `ov_ai` is in the host's nightly backup (pgBackRest to B2, plus a weekly logical dump), and a
1061restore drill (`sudo ovhost drill ai`) restored a fresh dump and passed on 2026-10-15: models, routes, templates,
1062workflows, quotas and citations counted equal, and `/api/health` matched production. `/metrics` is scraped by the
1063host's Prometheus on 127.0.0.1:4700 and refused on the public vhost.
1064
1065Deployed: `/opt/openvibe.ai`, env `/etc/openvibe/ai.env` (0600; `BASE_URL=https://ai.openvibe.services`), unit
1066`deploy/systemd/openvibe-ai.service` (`StateDirectory=openvibe-ai`; database `ov_ai` on the host's data role),
1067principal `ai`. The vhost `deploy/nginx/ai.openvibe.services.conf` (on the `*.openvibe.services` certificate)
1068serves the home, `/stats`, `/shared/*`, `/robots.txt`, `/sitemap.xml`, `/llms.txt`, health/ready, the operator
1069console `/console` with its sign-in `/auth/`, and exactly the developer-app routes (the six operations by POST,
1070`/api/v1/runs/:id` by GET, rate-limited per address); every other `/api/` path and `/metrics` stay host-local.
1071The old `ai.openvibe.network` address was retired on 2026-10-10. The console needs
1072`AI_CONSOLE_SESSION_SECRET` (32+ random characters) and `OV_OAUTH_CLIENT_SECRET` in the env file (without them it
1073answers 503 in production); Network client `ai` lists `https://ai.openvibe.services/auth/callback`.
1074
1075Sitemap entries use the release commit for home and the newest recorded stats day for `/stats` when available.
1076
1077## Not done yet
1078
1079- Live's VOD and clip overviews calling `media.analyze` instead of extracting frames and transcribing
1080 themselves; moving the remaining passthrough prompts into AI templates (most `live.*` workflows are
1081 versioned templates since 2026-09-27);
1082 removing Network's fallback to Live's `/internal/ai/site-copy`; a public server-rendered status page.
1083- Metered models for developer apps: production app runs use free and local capacity until a tier budget
1084 governs their project (`AI_GOVERN_TIERS` is off in production) and OpenVibe.Billing charges project subjects.
1085- Import holds from the 2026-09-23 run: a streamer's own provider key stays in Live, and 3,021 Live
1086 translations cannot become cache entries (they have no source text) and stay in Live.
1087- Text routes now run a capability pool (`shared` plus a local model when one is configured) instead
1088 of a declared fallback, but speech and embeddings still have a single upstream, so an outage there
1089 fails that feature until O9 adds a second provider. On 2026-09-23 `live.*` and `network.site_copy`
1090 outputs carried no citations or gaps
1091 (0 citation rows); runs record grounding (cited sources and named gaps) since 2026-09-25.
1092
1093## Launch
1094
1095Launched 2026-10-07 under the launch rule (plan §12.12): the runtime with health, readiness and metrics; Network
1096subjects and scoped service principals; server-rendered public routes useful without JavaScript (the home,
1097`/stats`); PostgreSQL persistence and end-to-end workflows; capabilities and events registered in
1098`OpenVibe.Contracts` (`ai.app.run` since 0.109.0, the service live since 0.110.0); migrations at boot,
1099robots/sitemap/llms.txt; acceptance tests ([above](#acceptance-guarantees-and-where-they-are-tested), and
1100`test/home.test.js` for the public routes and vhosts). The same release removed ai.openvibe.services and the
1101ai.openvibe.network notice from OpenVibe.Sites.
1102
1103---
1104
1105Part of the [OpenVibe network](https://openvibe.network). Built in the open by [OpenVibers](https://github.com/OpenVibers).
1106
1107<!-- versions:start -->
1108- openvibe-contracts: v0.421.0
1109- openvibe-sdk: v0.166.0
1110- openvibe-shared: v4.241.0
1111<!-- versions:end -->
1112
1113### Exchange search engines
1114
1115The web, image, icon, reverse-image, Unsplash download-event and fetch APIs, provider configuration, verified tariffs, personal keys and receipts are documented in [docs/search-engines.md](docs/search-engines.md). `GET /api/v1/exchange/search/engines` publishes the current adapter catalog; `/openapi.json` describes every search/fetch endpoint.
1116