openvibers/openvibe.contracts Commits

BA3: the latency classes and the batches contract (#344)

OP OpenVibers committed 5 days ago · 3fae8833b7 · 81 files, +2333 −101

* BA3: the latency classes and the batches contract

A run's options carry the class the caller asks for (interactive, background, batch) and, for a batch, how long the answer may take (deadline_hours, 1 to 72). A run's answer carries the class it really ran in, the tier it was priced on and the clamp when the ask was faster than the workflow allows.

# Conflicts:
#	CHANGELOG.md

* BA3: keep the whole changelog history

The rebased entry replaced the file with a part of it; this puts the whole history back and keeps the 0.336.0 entry on top.

Changes

ddiff --git a/CHANGELOG.md b/CHANGELOG.md
iindex cad1a9a3..4ddfe49d 100644
---- a/CHANGELOG.md
++++ b/CHANGELOG.md
@@@ -1,4 +1,22 @@
# Changelog
++## 0.336.0 — 2026-11-07
++
++- The run's options carry the latency class the caller asks for: `interactive` (answer as soon as the provider can),
++ `background` (minutes; the run is priced on the cheapest tier that can wait) or `batch` (hours; the run is queued and
++ answered through a provider's own batch API). A batch run may also say `deadline_hours` (1 to 72): how long the answer may
++ take. The class is the workflow's own (the route's `constraints.latency_class`): a caller may ask for a slower class and
++ never for a faster one, and a faster ask is not refused — the run happens in the workflow's own class and says so.
++- The run's answer carries `latency_class` (the class the run really ran in), `service_tier` (`standard`, `flex`,
++ `batch`, `offpeak`, `local`: the tier it was priced on — the one the provider served or the one the router chose) and
++ `latency_clamped` (the class asked for and the class used, when the ask was clamped).
++- Add the batches contract: `ai.batch@1`, `ai.batch-item@1`, `ai.batch-create-request@1`, `ai.batch-create-result@1` and
++ `ai.batch-read-result@1` for `/api/v1/batches` on OpenVibe.AI. A batch is a queue of many prompts that are answered
++ later, at half the retail price; the batch says where it stands, and a prompt that reaches its deadline without cheap
++ capacity expires and is never charged.
++- Add the grants `ai.batches.create` (open a batch, cancel one) and `ai.batches.read` (read the caller's own batches).
++ A first-party service opens a batch on the network's own account with `ai.batches.create`; a person or a developer app
++ pays with its own account.
++
## 0.335.0 — 2026-11-07
ddiff --git a/contracts/ai/batch-create-request.v1.json b/contracts/ai/batch-create-request.v1.json
nnew file mode 100644
iindex 00000000..bf44962b
---- /dev/null
++++ b/contracts/ai/batch-create-request.v1.json
@@@ -0,0 +1,55 @@
++{
++ "$schema": "https://json-schema.org/draft/2020-12/schema",
++ "$id": "https://openvibe.network/contracts/ai/batch-create-request.v1.json",
++ "title": "AiBatchCreateRequest",
++ "description": "ai.batch-create-request@1: the body of POST /api/v1/batches on OpenVibe.AI (capability ai.batches.create). One batch of many prompts: the answers come later, and the batch is billed at half the retail price. Errors are problem+json: 422 input.invalid (no items, an empty prompt, a prompt over 20000 characters, a deadline outside 1 to 72 hours), 422 batch.too_large (more prompts than the service's cap), 403 batch.no_payer (the token names no account to pay), 403 batch.no_grant (a service without the ai.batches.create grant).",
++ "type": "object",
++ "required": [
++ "items"
++ ],
++ "additionalProperties": false,
++ "properties": {
++ "items": {
++ "type": "array",
++ "minItems": 1,
++ "description": "The prompts of the batch, in order. Each item is a prompt, or an object with a prompt and the model it wants.",
++ "items": {
++ "anyOf": [
++ {
++ "type": "string",
++ "minLength": 1,
++ "maxLength": 20000
++ },
++ {
++ "type": "object",
++ "required": [
++ "prompt"
++ ],
++ "additionalProperties": false,
++ "properties": {
++ "prompt": {
++ "type": "string",
++ "minLength": 1,
++ "maxLength": 20000
++ },
++ "model": {
++ "type": "string",
++ "description": "The model this item wants (a model key, or provider/model)."
++ }
++ }
++ }
++ ]
++ }
++ },
++ "model": {
++ "type": "string",
++ "description": "The model the whole batch asks for, when no item names one."
++ },
++ "deadline_hours": {
++ "type": "integer",
++ "minimum": 1,
++ "maximum": 72,
++ "description": "How long the batch may take, in hours (1 to 72). Omitted: the service's own default (24 hours). A prompt that reaches the deadline without cheap capacity expires and is never charged."
++ }
++ }
++}
ddiff --git a/contracts/ai/batch-create-result.v1.json b/contracts/ai/batch-create-result.v1.json
nnew file mode 100644
iindex 00000000..e50e4f7d
---- /dev/null
++++ b/contracts/ai/batch-create-result.v1.json
@@@ -0,0 +1,16 @@
++{
++ "$schema": "https://json-schema.org/draft/2020-12/schema",
++ "$id": "https://openvibe.network/contracts/ai/batch-create-result.v1.json",
++ "title": "AiBatchCreateResult",
++ "description": "ai.batch-create-result@1: the answers of ai.batches.create on OpenVibe.AI. POST /api/v1/batches → 201 { batch }; POST /api/v1/batches/:id/cancel → { batch } (the batch is cancelled: the items that had not run are cancelled and nothing is charged for them).",
++ "type": "object",
++ "required": [
++ "batch"
++ ],
++ "additionalProperties": false,
++ "properties": {
++ "batch": {
++ "$ref": "batch.v1.json"
++ }
++ }
++}
ddiff --git a/contracts/ai/batch-item.v1.json b/contracts/ai/batch-item.v1.json
nnew file mode 100644
iindex 00000000..0fcf2da5
---- /dev/null
++++ b/contracts/ai/batch-item.v1.json
@@@ -0,0 +1,136 @@
++{
++ "$schema": "https://json-schema.org/draft/2020-12/schema",
++ "$id": "https://openvibe.network/contracts/ai/batch-item.v1.json",
++ "title": "AiBatchItem",
++ "description": "ai.batch-item@1: one prompt of a batch on OpenVibe.AI (POST /api/v1/batches). The item is the queue and the status of that one prompt: it waits for capacity (or for a provider's own batch API), runs, and carries the answer, the run it made and the money. A prompt that reached its deadline without capacity expires and is never charged.",
++ "type": "object",
++ "required": [
++ "idx",
++ "status",
++ "model",
++ "run_id",
++ "answer",
++ "error_code",
++ "error_detail",
++ "reason",
++ "capacity",
++ "route_provider",
++ "route_model",
++ "deadline_at",
++ "created_at",
++ "updated_at"
++ ],
++ "additionalProperties": false,
++ "properties": {
++ "idx": {
++ "type": "integer",
++ "minimum": 0,
++ "description": "The item's place in the batch."
++ },
++ "status": {
++ "enum": [
++ "queued",
++ "waiting",
++ "running",
++ "succeeded",
++ "failed",
++ "expired",
++ "cancelled"
++ ],
++ "description": "queued (asked for, not started), waiting (no cheap capacity yet), running (a provider works on it), succeeded, failed, expired (the deadline passed; not charged), cancelled (the batch was cancelled)."
++ },
++ "model": {
++ "type": [
++ "string",
++ "null"
++ ],
++ "description": "The model the item asked for, or the batch's own."
++ },
++ "run_id": {
++ "type": [
++ "string",
++ "null"
++ ],
++ "pattern": "^run_[0-9A-HJKMNP-TV-Z]{26}$",
++ "description": "The run the item made, when it made one."
++ },
++ "answer": {
++ "type": [
++ "string",
++ "null"
++ ],
++ "description": "The answer the provider gave, as text."
++ },
++ "error_code": {
++ "type": [
++ "string",
++ "null"
++ ]
++ },
++ "error_detail": {
++ "type": [
++ "string",
++ "null"
++ ]
++ },
++ "reason": {
++ "type": [
++ "string",
++ "null"
++ ],
++ "description": "Why the item ended without an answer (it expired, it was cancelled)."
++ },
++ "capacity": {
++ "type": [
++ "string",
++ "null"
++ ],
++ "description": "Where the answer came from: a community offer, the regular capacity, or a provider's own batch API."
++ },
++ "route_provider": {
++ "type": [
++ "string",
++ "null"
++ ]
++ },
++ "route_model": {
++ "type": [
++ "string",
++ "null"
++ ]
++ },
++ "cost_nano_usd": {
++ "type": [
++ "string",
++ "null"
++ ],
++ "description": "What the answer really cost, in nano-USD (1 USD = 1e9 nano), as a string: the money is never a float."
++ },
++ "absorbed_nano_usd": {
++ "type": [
++ "string",
++ "null"
++ ],
++ "description": "What the network absorbed (the route cost more than the customer paid), in nano-USD, as a string."
++ },
++ "charged_nano_usd": {
++ "type": [
++ "string",
++ "null"
++ ],
++ "description": "What the customer was charged, in nano-USD, as a string: half the retail card."
++ },
++ "deadline_at": {
++ "type": "string",
++ "format": "date-time"
++ },
++ "created_at": {
++ "type": "string",
++ "format": "date-time"
++ },
++ "updated_at": {
++ "type": "string",
++ "format": "date-time"
++ }
++ }
++}
ddiff --git a/contracts/ai/batch-read-result.v1.json b/contracts/ai/batch-read-result.v1.json
nnew file mode 100644
iindex 00000000..33254da9
---- /dev/null
++++ b/contracts/ai/batch-read-result.v1.json
@@@ -0,0 +1,35 @@
++{
++ "$schema": "https://json-schema.org/draft/2020-12/schema",
++ "$id": "https://openvibe.network/contracts/ai/batch-read-result.v1.json",
++ "title": "AiBatchReadResult",
++ "description": "ai.batch-read-result@1: the answers of ai.batches.read on OpenVibe.AI (the caller's own batches). GET /api/v1/batches → { batches } (newest first, the items not included); GET /api/v1/batches/:id → { batch } (with its items). Somebody else's batch is 404 batch.not_found.",
++ "anyOf": [
++ {
++ "type": "object",
++ "required": [
++ "batches"
++ ],
++ "additionalProperties": false,
++ "properties": {
++ "batches": {
++ "type": "array",
++ "items": {
++ "$ref": "batch.v1.json"
++ }
++ }
++ }
++ },
++ {
++ "type": "object",
++ "required": [
++ "batch"
++ ],
++ "additionalProperties": false,
++ "properties": {
++ "batch": {
++ "$ref": "batch.v1.json"
++ }
++ }
++ }
++ ]
++}
ddiff --git a/contracts/ai/batch.v1.json b/contracts/ai/batch.v1.json
nnew file mode 100644
iindex 00000000..0ee7c0b0
---- /dev/null
++++ b/contracts/ai/batch.v1.json
@@@ -0,0 +1,103 @@
++{
++ "$schema": "https://json-schema.org/draft/2020-12/schema",
++ "$id": "https://openvibe.network/contracts/ai/batch.v1.json",
++ "title": "AiBatch",
++ "description": "ai.batch@1: a batch of many prompts on OpenVibe.AI (POST /api/v1/batches, GET /api/v1/batches, GET /api/v1/batches/:id). The answers are not waited for: the batch is queued, and the batch says where it stands. A batch is billed at half the retail price through Billing like every other request; the batch table is the queue and the status, never a second ledger. The owner of the batch is the account that pays for it: a person, a developer app's project, or a first-party service with the ai.batches.create grant (the network's own account).",
++ "type": "object",
++ "required": [
++ "id",
++ "status",
++ "owner_type",
++ "owner_id",
++ "project_id",
++ "item_count",
++ "items",
++ "discount",
++ "deadline_at",
++ "created_at",
++ "updated_at"
++ ],
++ "additionalProperties": false,
++ "properties": {
++ "id": {
++ "type": "string",
++ "pattern": "^bat_[0-9a-f]{24}$"
++ },
++ "status": {
++ "enum": [
++ "queued",
++ "running",
++ "succeeded",
++ "failed",
++ "expired",
++ "cancelled"
++ ],
++ "description": "queued (asked for), running (items are being answered), succeeded, failed (an item failed), expired (the deadline passed with nothing answered), cancelled (the caller cancelled it)."
++ },
++ "owner_type": {
++ "enum": [
++ "user",
++ "app",
++ "service"
++ ],
++ "description": "Who pays for the batch."
++ },
++ "owner_id": {
++ "type": "string"
++ },
++ "project_id": {
++ "type": [
++ "string",
++ "null"
++ ],
++ "description": "The developer app's project, when an app owns the batch."
++ },
++ "item_count": {
++ "type": "integer",
++ "minimum": 0,
++ "description": "How many prompts the batch carries."
++ },
++ "items": {
++ "type": "array",
++ "items": {
++ "$ref": "batch-item.v1.json"
++ },
++ "description": "The items, in order. The list of batches answers with the same objects and no items (an empty array): the items come from GET /api/v1/batches/:id."
++ },
++ "discount": {
++ "type": "string",
++ "description": "The share of the retail card the customer is charged, as a decimal fraction of a million (500000 = half)."
++ },
++ "deadline_at": {
++ "type": "string",
++ "format": "date-time",
++ "description": "When the batch stops waiting: the deadline the caller asked for (1 to 72 hours)."
++ },
++ "created_at": {
++ "type": "string",
++ "format": "date-time"
++ },
++ "updated_at": {
++ "type": "string",
++ "format": "date-time"
++ },
++ "finished_at": {
++ "type": [
++ "string",
++ "null"
++ ],
++ "format": "date-time"
++ },
++ "absorbed_nano_usd": {
++ "type": [
++ "string",
++ "null"
++ ],
++ "description": "What the network absorbed for this batch, in nano-USD, as a string."
++ },
++ "ran_on_regular_capacity": {
++ "type": "boolean",
++ "description": "The batch ran on the ordinary capacity (not on free or community capacity): it cost the network more than the customer paid, and the difference is the absorbed amount."
++ }
++ }
++}
ddiff --git a/contracts/ai/run-request.v1.json b/contracts/ai/run-request.v1.json
iindex c80b59f4..e6010003 100644
---- a/contracts/ai/run-request.v1.json
++++ b/contracts/ai/run-request.v1.json
@@@ -2,7 +2,7 @@
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://openvibe.network/contracts/ai/run-request.v1.json",
"title": "AiRunRequest",
-- "description": "Body of POST /api/v1/runs on OpenVibe.AI (capability ai.run.create; ADR-015). Runs the newest active version of a workflow (or the given active or deprecated version) on input that must match that workflow version's own input schema; per-workflow input and output schemas live in AI's registry (GET /api/v1/workflows/:key), not here. The token's ns claim limits which workflow namespaces may run (403 capability.namespace_denied). ?wait=ms waits for the result: 201 finished or served from cache, 202 still queued/running (poll GET /api/v1/runs/:id), 200 an idempotent replay. Errors are problem+json: 404 workflow.not_found, 409 workflow.inactive, 409 idempotency.conflict, 413 input.too_large, 422 input.invalid, 429 quota.exceeded or queue.full with Retry-After. The direct operations POST /api/v1/{chat,generate,summarize,classify,extract,enrich,embed} take the same fields (except workflow and version) with the input fields at the top level and run workflow ai.<op>. target (EntityRef): what the output is about, part of the cache scope and a run filter. attribution (EntityRef): what the spend is attributed to for quotas and usage. on_behalf_of (SubjectRef): the person or actor the caller acts for, part of the cache scope and per-actor quotas.",
++ "description": "Body of POST /api/v1/runs on OpenVibe.AI (capability ai.run.create; ADR-015). Runs the newest active version of a workflow (or the given active or deprecated version) on input that must match that workflow version's own input schema; per-workflow input and output schemas live in AI's registry (GET /api/v1/workflows/:key), not here. The token's ns claim limits which workflow namespaces may run (403 capability.namespace_denied). ?wait=ms waits for the result: 201 finished or served from cache, 202 still queued/running (poll GET /api/v1/runs/:id), 200 an idempotent replay. Errors are problem+json: 404 workflow.not_found, 409 workflow.inactive, 409 idempotency.conflict, 413 input.too_large, 422 input.invalid, 429 quota.exceeded or queue.full with Retry-After. The direct operations POST /api/v1/{chat,generate,summarize,classify,extract,enrich,embed} take the same fields (except workflow and version) with the input fields at the top level and run workflow ai.<op>. target (EntityRef): what the output is about, part of the cache scope and a run filter. attribution (EntityRef): what the spend is attributed to for quotas and usage. on_behalf_of (SubjectRef): the person or actor the caller acts for, part of the cache scope and per-actor quotas. options.latency asks for a slower class than the workflow allows (options.deadline_hours says how long a batch run may take); the run answers with the class it ran in (run.latency_class), the tier it was priced on (run.service_tier) and, when the ask was clamped, the class it really ran in (run.latency_clamped).",
"type": "object",
"required": [
"workflow"
@@@ -51,6 +51,16 @@
"type": "boolean",
"default": false,
"description": "Keep raw prompts and responses in the request log, only when AI runs with raw debug logging on."
++ },
++ "latency": {
++ "enum": ["interactive", "background", "batch"],
++ "description": "The class the caller asks for: how long the caller may wait, and what the router may do with the wait. interactive (the default) answers as soon as the provider can; background may take minutes, and the run is priced on the cheapest tier that can wait (a Flex tier, a provider's off-peak hours, the host's own idle model); batch may take hours, and the run is queued and answered through a provider's own batch API. The class is the workflow's own (the route's constraints.latency_class): a caller may ask for a SLOWER class and never for a faster one. A faster ask is not refused: the run happens in the workflow's own class and the answer says so (run.latency_clamped)."
++ },
++ "deadline_hours": {
++ "type": "integer",
++ "minimum": 1,
++ "maximum": 72,
++ "description": "For a batch run: how long the answer may take, in hours (1 to 72). The run is queued and answered within the deadline; a deadline the provider cannot meet sends the run back to the ordinary path. Omitted: the service's own default."
}
}
},
ddiff --git a/contracts/ai/run.v1.json b/contracts/ai/run.v1.json
iindex 855d565a..c65cc43f 100644
---- a/contracts/ai/run.v1.json
++++ b/contracts/ai/run.v1.json
@@@ -2,7 +2,7 @@
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://openvibe.network/contracts/ai/run.v1.json",
"title": "AiRun",
-- "description": "A workflow run on OpenVibe.AI (ADR-015; server/runs.js decode): the `run` member of every run response (POST /api/v1/runs and the direct operations, GET /api/v1/runs/:id, cancel, retry) and each item of GET /api/v1/runs { runs }. Output is attributable to workflow + version + template version + route version + provider/model + run id, never to a person. A draft stays a draft: products store it with ai/ai_assisted authorship and publish nothing until a person reviews it. input keeps no inline media (replaced by data_url_sha256 and data_url_bytes) and no passthrough prompt ({ not_retained }); output is null until the run succeeded or was served from cache and then matches the workflow version's output_schema. requester is the token principal (service, app or mod). A run interrupted by a restart or shutdown ends failed with error.code run.interrupted. explain says why the run answered as it did: the task it read, the candidates it ranked and every attempt.",
++ "description": "A workflow run on OpenVibe.AI (ADR-015; server/runs.js decode): the `run` member of every run response (POST /api/v1/runs and the direct operations, GET /api/v1/runs/:id, cancel, retry) and each item of GET /api/v1/runs { runs }. Output is attributable to workflow + version + template version + route version + provider/model + run id, never to a person. A draft stays a draft: products store it with ai/ai_assisted authorship and publish nothing until a person reviews it. input keeps no inline media (replaced by data_url_sha256 and data_url_bytes) and no passthrough prompt ({ not_retained }); output is null until the run succeeded or was served from cache and then matches the workflow version's output_schema. requester is the token principal (service, app or mod). A run interrupted by a restart or shutdown ends failed with error.code run.interrupted. latency_class is the class the run really ran in, service_tier the tier it was priced on, and latency_clamped set when the caller asked for a faster class than the workflow allows. explain says why the run answered as it did: the task it read, the candidates it ranked and every attempt.",
"type": "object",
"required": ["id", "status", "workflow", "template", "route", "requester", "on_behalf_of", "attribution", "source_service", "target", "input", "output", "error", "synthetic", "provenance", "usage", "citations_count", "retry_of", "idempotency_key", "trace_id", "created_at", "started_at", "finished_at"],
"additionalProperties": false,
@@@ -69,6 +69,25 @@
}
},
"citations_count": { "type": "integer", "minimum": 0, "description": "Citations stored for the run (GET /api/v1/runs/:id/citations): the sources the workflow was given, and any the requester attached later." },
++ "latency_class": {
++ "enum": ["interactive", "background", "batch"],
++ "description": "The class the run really ran in: how long the caller was willing to wait. It is the workflow's own class (the route's constraints.latency_class) or a slower one the caller asked for (options.latency); a caller can never make a run faster than its workflow allows."
++ },
++ "service_tier": {
++ "type": ["string", "null"],
++ "enum": ["standard", "flex", "batch", "offpeak", "local", null],
++ "description": "The tier the run was priced on: the tier the provider really served (standard, flex, batch) or the one the router chose (offpeak inside a provider's off-peak hours, local on the host's own model). Null while the run has not answered. A provider that does not say which tier it served is priced at the list price: a saving nobody confirmed is not a saving."
++ },
++ "latency_clamped": {
++ "type": ["object", "null"],
++ "required": ["asked", "used"],
++ "additionalProperties": false,
++ "properties": {
++ "asked": { "enum": ["interactive", "background", "batch"], "description": "The class the caller asked for." },
++ "used": { "enum": ["interactive", "background", "batch"], "description": "The class the run really ran in." }
++ },
++ "description": "Set when the caller asked for a faster class than the workflow allows: the class asked for and the class the run really ran in. The run is never refused for it."
++ },
"retry_of": { "type": ["string", "null"], "pattern": "^run_[0-9A-HJKMNP-TV-Z]{26}$" },
"idempotency_key": { "type": ["string", "null"] },
"trace_id": { "type": ["string", "null"] },
ddiff --git a/contracts/catalog.json b/contracts/catalog.json
iindex 633248cd..a5fccf40 100644
---- a/contracts/catalog.json
++++ b/contracts/catalog.json
@@@ -1630,6 +1630,51 @@
"compatibility": "backward",
"status": "active"
},
++ {
++ "id": "ai.batch",
++ "version": "1.0.0",
++ "owner": "ai",
++ "schema": "ai/batch.v1.json",
++ "visibility": "first-party",
++ "compatibility": "backward",
++ "status": "active"
++ },
++ {
++ "id": "ai.batch-item",
++ "version": "1.0.0",
++ "owner": "ai",
++ "schema": "ai/batch-item.v1.json",
++ "visibility": "first-party",
++ "compatibility": "backward",
++ "status": "active"
++ },
++ {
++ "id": "ai.batch-create-request",
++ "version": "1.0.0",
++ "owner": "ai",
++ "schema": "ai/batch-create-request.v1.json",
++ "visibility": "first-party",
++ "compatibility": "backward",
++ "status": "active"
++ },
++ {
++ "id": "ai.batch-create-result",
++ "version": "1.0.0",
++ "owner": "ai",
++ "schema": "ai/batch-create-result.v1.json",
++ "visibility": "first-party",
++ "compatibility": "backward",
++ "status": "active"
++ },
++ {
++ "id": "ai.batch-read-result",
++ "version": "1.0.0",
++ "owner": "ai",
++ "schema": "ai/batch-read-result.v1.json",
++ "visibility": "first-party",
++ "compatibility": "backward",
++ "status": "active"
++ },
{
"id": "ai.run-read-result",
"version": "1.0.0",
ddiff --git a/fixtures/ai.batch-create-request/invalid/a-deadline-out-of-range.json b/fixtures/ai.batch-create-request/invalid/a-deadline-out-of-range.json
nnew file mode 100644
iindex 00000000..6c1ae868
---- /dev/null
++++ b/fixtures/ai.batch-create-request/invalid/a-deadline-out-of-range.json
@@@ -0,0 +1,6 @@
++{
++ "items": [
++ "one prompt"
++ ],
++ "deadline_hours": 100
++}
ddiff --git a/fixtures/ai.batch-create-request/invalid/empty-prompt.json b/fixtures/ai.batch-create-request/invalid/empty-prompt.json
nnew file mode 100644
iindex 00000000..b7358290
---- /dev/null
++++ b/fixtures/ai.batch-create-request/invalid/empty-prompt.json
@@@ -0,0 +1,5 @@
++{
++ "items": [
++ ""
++ ]
++}
ddiff --git a/fixtures/ai.batch-create-request/invalid/no-items.json b/fixtures/ai.batch-create-request/invalid/no-items.json
nnew file mode 100644
iindex 00000000..2feb2101
---- /dev/null
++++ b/fixtures/ai.batch-create-request/invalid/no-items.json
@@@ -0,0 +1,3 @@
++{
++ "items": []
++}
ddiff --git a/fixtures/ai.batch-create-request/valid/prompts.json b/fixtures/ai.batch-create-request/valid/prompts.json
nnew file mode 100644
iindex 00000000..684e1195
---- /dev/null
++++ b/fixtures/ai.batch-create-request/valid/prompts.json
@@@ -0,0 +1,9 @@
++{
++ "items": [
++ "one prompt",
++ {
++ "prompt": "another prompt",
++ "model": "gpt-4o-mini"
++ }
++ ]
++}
ddiff --git a/fixtures/ai.batch-create-request/valid/with-a-deadline.json b/fixtures/ai.batch-create-request/valid/with-a-deadline.json
nnew file mode 100644
iindex 00000000..36631ea2
---- /dev/null
++++ b/fixtures/ai.batch-create-request/valid/with-a-deadline.json
@@@ -0,0 +1,7 @@
++{
++ "items": [
++ "one prompt"
++ ],
++ "model": "gpt-4o-mini",
++ "deadline_hours": 24
++}
ddiff --git a/fixtures/ai.batch-create-result/invalid/no-batch.json b/fixtures/ai.batch-create-result/invalid/no-batch.json
nnew file mode 100644
iindex 00000000..a19c5293
---- /dev/null
++++ b/fixtures/ai.batch-create-result/invalid/no-batch.json
@@@ -0,0 +1,3 @@
++{
++ "batch_id": "bat_0123456789abcdef01234567"
++}
ddiff --git a/fixtures/ai.batch-create-result/valid/the-batch.json b/fixtures/ai.batch-create-result/valid/the-batch.json
nnew file mode 100644
iindex 00000000..6dc7c2aa
---- /dev/null
++++ b/fixtures/ai.batch-create-result/valid/the-batch.json
@@@ -0,0 +1,38 @@
++{
++ "batch": {
++ "id": "bat_0123456789abcdef01234567",
++ "status": "succeeded",
++ "owner_type": "user",
++ "owner_id": "usr_01K6R3A1B2C3D4E5F6G7H8J9KF",
++ "project_id": null,
++ "item_count": 1,
++ "items": [
++ {
++ "idx": 0,
++ "status": "succeeded",
++ "model": "gpt-4o-mini",
++ "run_id": "run_01K6R3A1B2C3D4E5F6G7H8J9KF",
++ "answer": "A digest of the week.",
++ "error_code": null,
++ "error_detail": null,
++ "reason": null,
++ "capacity": "regular",
++ "route_provider": "openai",
++ "route_model": "gpt-4o-mini",
++ "cost_nano_usd": "1200",
++ "absorbed_nano_usd": "0",
++ "charged_nano_usd": "600",
++ "deadline_at": "2026-11-08T22:58:00.000Z",
++ "created_at": "2026-11-07T22:58:00.000Z",
++ "updated_at": "2026-11-08T00:10:00.000Z"
++ }
++ ],
++ "discount": "500000",
++ "deadline_at": "2026-11-08T22:58:00.000Z",
++ "created_at": "2026-11-07T22:58:00.000Z",
++ "updated_at": "2026-11-08T00:10:00.000Z",
++ "finished_at": "2026-11-08T00:10:00.000Z",
++ "absorbed_nano_usd": "0",
++ "ran_on_regular_capacity": false
++ }
++}
ddiff --git a/fixtures/ai.batch-item/invalid/no-idx.json b/fixtures/ai.batch-item/invalid/no-idx.json
nnew file mode 100644
iindex 00000000..cb04a627
---- /dev/null
++++ b/fixtures/ai.batch-item/invalid/no-idx.json
@@@ -0,0 +1,18 @@
++{
++ "status": "succeeded",
++ "model": "gpt-4o-mini",
++ "run_id": "run_01K6R3A1B2C3D4E5F6G7H8J9KF",
++ "answer": "A digest of the week.",
++ "error_code": null,
++ "error_detail": null,
++ "reason": null,
++ "capacity": "regular",
++ "route_provider": "openai",
++ "route_model": "gpt-4o-mini",
++ "cost_nano_usd": "1200",
++ "absorbed_nano_usd": "0",
++ "charged_nano_usd": "600",
++ "deadline_at": "2026-11-08T22:58:00.000Z",
++ "created_at": "2026-11-07T22:58:00.000Z",
++ "updated_at": "2026-11-08T00:10:00.000Z"
++}
ddiff --git a/fixtures/ai.batch-item/invalid/unknown-status.json b/fixtures/ai.batch-item/invalid/unknown-status.json
nnew file mode 100644
iindex 00000000..ec0b0132
---- /dev/null
++++ b/fixtures/ai.batch-item/invalid/unknown-status.json
@@@ -0,0 +1,19 @@
++{
++ "idx": 0,
++ "status": "later",
++ "model": "gpt-4o-mini",
++ "run_id": "run_01K6R3A1B2C3D4E5F6G7H8J9KF",
++ "answer": "A digest of the week.",
++ "error_code": null,
++ "error_detail": null,
++ "reason": null,
++ "capacity": "regular",
++ "route_provider": "openai",
++ "route_model": "gpt-4o-mini",
++ "cost_nano_usd": "1200",
++ "absorbed_nano_usd": "0",
++ "charged_nano_usd": "600",
++ "deadline_at": "2026-11-08T22:58:00.000Z",
++ "created_at": "2026-11-07T22:58:00.000Z",
++ "updated_at": "2026-11-08T00:10:00.000Z"
++}
ddiff --git a/fixtures/ai.batch-item/valid/one-item.json b/fixtures/ai.batch-item/valid/one-item.json
nnew file mode 100644
iindex 00000000..1769e8fe
---- /dev/null
++++ b/fixtures/ai.batch-item/valid/one-item.json
@@@ -0,0 +1,19 @@
++{
++ "idx": 0,
++ "status": "succeeded",
++ "model": "gpt-4o-mini",
++ "run_id": "run_01K6R3A1B2C3D4E5F6G7H8J9KF",
++ "answer": "A digest of the week.",
++ "error_code": null,
++ "error_detail": null,
++ "reason": null,
++ "capacity": "regular",
++ "route_provider": "openai",
++ "route_model": "gpt-4o-mini",
++ "cost_nano_usd": "1200",
++ "absorbed_nano_usd": "0",
++ "charged_nano_usd": "600",
++ "deadline_at": "2026-11-08T22:58:00.000Z",
++ "created_at": "2026-11-07T22:58:00.000Z",
++ "updated_at": "2026-11-08T00:10:00.000Z"
++}
ddiff --git a/fixtures/ai.batch-read-result/invalid/both-ways.json b/fixtures/ai.batch-read-result/invalid/both-ways.json
nnew file mode 100644
iindex 00000000..eb90ec03
---- /dev/null
++++ b/fixtures/ai.batch-read-result/invalid/both-ways.json
@@@ -0,0 +1,76 @@
++{
++ "batch": {
++ "id": "bat_0123456789abcdef01234567",
++ "status": "succeeded",
++ "owner_type": "user",
++ "owner_id": "usr_01K6R3A1B2C3D4E5F6G7H8J9KF",
++ "project_id": null,
++ "item_count": 1,
++ "items": [
++ {
++ "idx": 0,
++ "status": "succeeded",
++ "model": "gpt-4o-mini",
++ "run_id": "run_01K6R3A1B2C3D4E5F6G7H8J9KF",
++ "answer": "A digest of the week.",
++ "error_code": null,
++ "error_detail": null,
++ "reason": null,
++ "capacity": "regular",
++ "route_provider": "openai",
++ "route_model": "gpt-4o-mini",
++ "cost_nano_usd": "1200",
++ "absorbed_nano_usd": "0",
++ "charged_nano_usd": "600",
++ "deadline_at": "2026-11-08T22:58:00.000Z",
++ "created_at": "2026-11-07T22:58:00.000Z",
++ "updated_at": "2026-11-08T00:10:00.000Z"
++ }
++ ],
++ "discount": "500000",
++ "deadline_at": "2026-11-08T22:58:00.000Z",
++ "created_at": "2026-11-07T22:58:00.000Z",
++ "updated_at": "2026-11-08T00:10:00.000Z",
++ "finished_at": "2026-11-08T00:10:00.000Z",
++ "absorbed_nano_usd": "0",
++ "ran_on_regular_capacity": false
++ },
++ "batches": [
++ {
++ "id": "bat_0123456789abcdef01234567",
++ "status": "succeeded",
++ "owner_type": "user",
++ "owner_id": "usr_01K6R3A1B2C3D4E5F6G7H8J9KF",
++ "project_id": null,
++ "item_count": 1,
++ "items": [
++ {
++ "idx": 0,
++ "status": "succeeded",
++ "model": "gpt-4o-mini",
++ "run_id": "run_01K6R3A1B2C3D4E5F6G7H8J9KF",
++ "answer": "A digest of the week.",
++ "error_code": null,
++ "error_detail": null,
++ "reason": null,
++ "capacity": "regular",
++ "route_provider": "openai",
++ "route_model": "gpt-4o-mini",
++ "cost_nano_usd": "1200",
++ "absorbed_nano_usd": "0",
++ "charged_nano_usd": "600",
++ "deadline_at": "2026-11-08T22:58:00.000Z",
++ "created_at": "2026-11-07T22:58:00.000Z",
++ "updated_at": "2026-11-08T00:10:00.000Z"
++ }
++ ],
++ "discount": "500000",
++ "deadline_at": "2026-11-08T22:58:00.000Z",
++ "created_at": "2026-11-07T22:58:00.000Z",
++ "updated_at": "2026-11-08T00:10:00.000Z",
++ "finished_at": "2026-11-08T00:10:00.000Z",
++ "absorbed_nano_usd": "0",
++ "ran_on_regular_capacity": false
++ }
++ ]
++}
ddiff --git a/fixtures/ai.batch-read-result/invalid/nothing.json b/fixtures/ai.batch-read-result/invalid/nothing.json
nnew file mode 100644
iindex 00000000..0967ef42
---- /dev/null
++++ b/fixtures/ai.batch-read-result/invalid/nothing.json
@@@ -0,0 +1 @@
++{}
ddiff --git a/fixtures/ai.batch-read-result/valid/one-batch.json b/fixtures/ai.batch-read-result/valid/one-batch.json
nnew file mode 100644
iindex 00000000..6dc7c2aa
---- /dev/null
++++ b/fixtures/ai.batch-read-result/valid/one-batch.json
@@@ -0,0 +1,38 @@
++{
++ "batch": {
++ "id": "bat_0123456789abcdef01234567",
++ "status": "succeeded",
++ "owner_type": "user",
++ "owner_id": "usr_01K6R3A1B2C3D4E5F6G7H8J9KF",
++ "project_id": null,
++ "item_count": 1,
++ "items": [
++ {
++ "idx": 0,
++ "status": "succeeded",
++ "model": "gpt-4o-mini",
++ "run_id": "run_01K6R3A1B2C3D4E5F6G7H8J9KF",
++ "answer": "A digest of the week.",
++ "error_code": null,
++ "error_detail": null,
++ "reason": null,
++ "capacity": "regular",
++ "route_provider": "openai",
++ "route_model": "gpt-4o-mini",
++ "cost_nano_usd": "1200",
++ "absorbed_nano_usd": "0",
++ "charged_nano_usd": "600",
++ "deadline_at": "2026-11-08T22:58:00.000Z",
++ "created_at": "2026-11-07T22:58:00.000Z",
++ "updated_at": "2026-11-08T00:10:00.000Z"
++ }
++ ],
++ "discount": "500000",
++ "deadline_at": "2026-11-08T22:58:00.000Z",
++ "created_at": "2026-11-07T22:58:00.000Z",
++ "updated_at": "2026-11-08T00:10:00.000Z",
++ "finished_at": "2026-11-08T00:10:00.000Z",
++ "absorbed_nano_usd": "0",
++ "ran_on_regular_capacity": false
++ }
++}
ddiff --git a/fixtures/ai.batch-read-result/valid/the-batches.json b/fixtures/ai.batch-read-result/valid/the-batches.json
nnew file mode 100644
iindex 00000000..129e2cbe
---- /dev/null
++++ b/fixtures/ai.batch-read-result/valid/the-batches.json
@@@ -0,0 +1,40 @@
++{
++ "batches": [
++ {
++ "id": "bat_0123456789abcdef01234567",
++ "status": "succeeded",
++ "owner_type": "user",
++ "owner_id": "usr_01K6R3A1B2C3D4E5F6G7H8J9KF",
++ "project_id": null,
++ "item_count": 1,
++ "items": [
++ {
++ "idx": 0,
++ "status": "succeeded",
++ "model": "gpt-4o-mini",
++ "run_id": "run_01K6R3A1B2C3D4E5F6G7H8J9KF",
++ "answer": "A digest of the week.",
++ "error_code": null,
++ "error_detail": null,
++ "reason": null,
++ "capacity": "regular",
++ "route_provider": "openai",
++ "route_model": "gpt-4o-mini",
++ "cost_nano_usd": "1200",
++ "absorbed_nano_usd": "0",
++ "charged_nano_usd": "600",
++ "deadline_at": "2026-11-08T22:58:00.000Z",
++ "created_at": "2026-11-07T22:58:00.000Z",
++ "updated_at": "2026-11-08T00:10:00.000Z"
++ }
++ ],
++ "discount": "500000",
++ "deadline_at": "2026-11-08T22:58:00.000Z",
++ "created_at": "2026-11-07T22:58:00.000Z",
++ "updated_at": "2026-11-08T00:10:00.000Z",
++ "finished_at": "2026-11-08T00:10:00.000Z",
++ "absorbed_nano_usd": "0",
++ "ran_on_regular_capacity": false
++ }
++ ]
++}
ddiff --git a/fixtures/ai.batch/invalid/no-id.json b/fixtures/ai.batch/invalid/no-id.json
nnew file mode 100644
iindex 00000000..3c509082
---- /dev/null
++++ b/fixtures/ai.batch/invalid/no-id.json
@@@ -0,0 +1,35 @@
++{
++ "status": "succeeded",
++ "owner_type": "user",
++ "owner_id": "usr_01K6R3A1B2C3D4E5F6G7H8J9KF",
++ "project_id": null,
++ "item_count": 1,
++ "items": [
++ {
++ "idx": 0,
++ "status": "succeeded",
++ "model": "gpt-4o-mini",
++ "run_id": "run_01K6R3A1B2C3D4E5F6G7H8J9KF",
++ "answer": "A digest of the week.",
++ "error_code": null,
++ "error_detail": null,
++ "reason": null,
++ "capacity": "regular",
++ "route_provider": "openai",
++ "route_model": "gpt-4o-mini",
++ "cost_nano_usd": "1200",
++ "absorbed_nano_usd": "0",
++ "charged_nano_usd": "600",
++ "deadline_at": "2026-11-08T22:58:00.000Z",
++ "created_at": "2026-11-07T22:58:00.000Z",
++ "updated_at": "2026-11-08T00:10:00.000Z"
++ }
++ ],
++ "discount": "500000",
++ "deadline_at": "2026-11-08T22:58:00.000Z",
++ "created_at": "2026-11-07T22:58:00.000Z",
++ "updated_at": "2026-11-08T00:10:00.000Z",
++ "finished_at": "2026-11-08T00:10:00.000Z",
++ "absorbed_nano_usd": "0",
++ "ran_on_regular_capacity": false
++}
ddiff --git a/fixtures/ai.batch/invalid/unknown-status.json b/fixtures/ai.batch/invalid/unknown-status.json
nnew file mode 100644
iindex 00000000..f4119979
---- /dev/null
++++ b/fixtures/ai.batch/invalid/unknown-status.json
@@@ -0,0 +1,36 @@
++{
++ "id": "bat_0123456789abcdef01234567",
++ "status": "whenever",
++ "owner_type": "user",
++ "owner_id": "usr_01K6R3A1B2C3D4E5F6G7H8J9KF",
++ "project_id": null,
++ "item_count": 1,
++ "items": [
++ {
++ "idx": 0,
++ "status": "succeeded",
++ "model": "gpt-4o-mini",
++ "run_id": "run_01K6R3A1B2C3D4E5F6G7H8J9KF",
++ "answer": "A digest of the week.",
++ "error_code": null,
++ "error_detail": null,
++ "reason": null,
++ "capacity": "regular",
++ "route_provider": "openai",
++ "route_model": "gpt-4o-mini",
++ "cost_nano_usd": "1200",
++ "absorbed_nano_usd": "0",
++ "charged_nano_usd": "600",
++ "deadline_at": "2026-11-08T22:58:00.000Z",
++ "created_at": "2026-11-07T22:58:00.000Z",
++ "updated_at": "2026-11-08T00:10:00.000Z"
++ }
++ ],
++ "discount": "500000",
++ "deadline_at": "2026-11-08T22:58:00.000Z",
++ "created_at": "2026-11-07T22:58:00.000Z",
++ "updated_at": "2026-11-08T00:10:00.000Z",
++ "finished_at": "2026-11-08T00:10:00.000Z",
++ "absorbed_nano_usd": "0",
++ "ran_on_regular_capacity": false
++}
ddiff --git a/fixtures/ai.batch/valid/a-queued-batch.json b/fixtures/ai.batch/valid/a-queued-batch.json
nnew file mode 100644
iindex 00000000..952a4285
---- /dev/null
++++ b/fixtures/ai.batch/valid/a-queued-batch.json
@@@ -0,0 +1,16 @@
++{
++ "id": "bat_0123456789abcdef01234567",
++ "status": "queued",
++ "owner_type": "user",
++ "owner_id": "usr_01K6R3A1B2C3D4E5F6G7H8J9KF",
++ "project_id": null,
++ "item_count": 2,
++ "items": [],
++ "discount": "500000",
++ "deadline_at": "2026-11-08T22:58:00.000Z",
++ "created_at": "2026-11-07T22:58:00.000Z",
++ "updated_at": "2026-11-08T00:10:00.000Z",
++ "finished_at": null,
++ "absorbed_nano_usd": "0",
++ "ran_on_regular_capacity": false
++}
ddiff --git a/fixtures/ai.batch/valid/one-batch.json b/fixtures/ai.batch/valid/one-batch.json
nnew file mode 100644
iindex 00000000..a1e25387
---- /dev/null
++++ b/fixtures/ai.batch/valid/one-batch.json
@@@ -0,0 +1,36 @@
++{
++ "id": "bat_0123456789abcdef01234567",
++ "status": "succeeded",
++ "owner_type": "user",
++ "owner_id": "usr_01K6R3A1B2C3D4E5F6G7H8J9KF",
++ "project_id": null,
++ "item_count": 1,
++ "items": [
++ {
++ "idx": 0,
++ "status": "succeeded",
++ "model": "gpt-4o-mini",
++ "run_id": "run_01K6R3A1B2C3D4E5F6G7H8J9KF",
++ "answer": "A digest of the week.",
++ "error_code": null,
++ "error_detail": null,
++ "reason": null,
++ "capacity": "regular",
++ "route_provider": "openai",
++ "route_model": "gpt-4o-mini",
++ "cost_nano_usd": "1200",
++ "absorbed_nano_usd": "0",
++ "charged_nano_usd": "600",
++ "deadline_at": "2026-11-08T22:58:00.000Z",
++ "created_at": "2026-11-07T22:58:00.000Z",
++ "updated_at": "2026-11-08T00:10:00.000Z"
++ }
++ ],
++ "discount": "500000",
++ "deadline_at": "2026-11-08T22:58:00.000Z",
++ "created_at": "2026-11-07T22:58:00.000Z",
++ "updated_at": "2026-11-08T00:10:00.000Z",
++ "finished_at": "2026-11-08T00:10:00.000Z",
++ "absorbed_nano_usd": "0",
++ "ran_on_regular_capacity": false
++}
ddiff --git a/generated/json-schema/bundle.json b/generated/json-schema/bundle.json
iindex 9e63a717..1ec43b1d 100644
---- a/generated/json-schema/bundle.json
++++ b/generated/json-schema/bundle.json
@@@ -15271,7 +15271,7 @@
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://openvibe.network/contracts/ai/run-request.v1.json",
"title": "AiRunRequest",
-- "description": "Body of POST /api/v1/runs on OpenVibe.AI (capability ai.run.create; ADR-015). Runs the newest active version of a workflow (or the given active or deprecated version) on input that must match that workflow version's own input schema; per-workflow input and output schemas live in AI's registry (GET /api/v1/workflows/:key), not here. The token's ns claim limits which workflow namespaces may run (403 capability.namespace_denied). ?wait=ms waits for the result: 201 finished or served from cache, 202 still queued/running (poll GET /api/v1/runs/:id), 200 an idempotent replay. Errors are problem+json: 404 workflow.not_found, 409 workflow.inactive, 409 idempotency.conflict, 413 input.too_large, 422 input.invalid, 429 quota.exceeded or queue.full with Retry-After. The direct operations POST /api/v1/{chat,generate,summarize,classify,extract,enrich,embed} take the same fields (except workflow and version) with the input fields at the top level and run workflow ai.<op>. target (EntityRef): what the output is about, part of the cache scope and a run filter. attribution (EntityRef): what the spend is attributed to for quotas and usage. on_behalf_of (SubjectRef): the person or actor the caller acts for, part of the cache scope and per-actor quotas.",
++ "description": "Body of POST /api/v1/runs on OpenVibe.AI (capability ai.run.create; ADR-015). Runs the newest active version of a workflow (or the given active or deprecated version) on input that must match that workflow version's own input schema; per-workflow input and output schemas live in AI's registry (GET /api/v1/workflows/:key), not here. The token's ns claim limits which workflow namespaces may run (403 capability.namespace_denied). ?wait=ms waits for the result: 201 finished or served from cache, 202 still queued/running (poll GET /api/v1/runs/:id), 200 an idempotent replay. Errors are problem+json: 404 workflow.not_found, 409 workflow.inactive, 409 idempotency.conflict, 413 input.too_large, 422 input.invalid, 429 quota.exceeded or queue.full with Retry-After. The direct operations POST /api/v1/{chat,generate,summarize,classify,extract,enrich,embed} take the same fields (except workflow and version) with the input fields at the top level and run workflow ai.<op>. target (EntityRef): what the output is about, part of the cache scope and a run filter. attribution (EntityRef): what the spend is attributed to for quotas and usage. on_behalf_of (SubjectRef): the person or actor the caller acts for, part of the cache scope and per-actor quotas. options.latency asks for a slower class than the workflow allows (options.deadline_hours says how long a batch run may take); the run answers with the class it ran in (run.latency_class), the tier it was priced on (run.service_tier) and, when the ask was clamped, the class it really ran in (run.latency_clamped).",
"type": "object",
"required": [
"workflow"
@@@ -15320,6 +15320,20 @@
"type": "boolean",
"default": false,
"description": "Keep raw prompts and responses in the request log, only when AI runs with raw debug logging on."
++ },
++ "latency": {
++ "enum": [
++ "interactive",
++ "background",
++ "batch"
++ ],
++ "description": "The class the caller asks for: how long the caller may wait, and what the router may do with the wait. interactive (the default) answers as soon as the provider can; background may take minutes, and the run is priced on the cheapest tier that can wait (a Flex tier, a provider's off-peak hours, the host's own idle model); batch may take hours, and the run is queued and answered through a provider's own batch API. The class is the workflow's own (the route's constraints.latency_class): a caller may ask for a SLOWER class and never for a faster one. A faster ask is not refused: the run happens in the workflow's own class and the answer says so (run.latency_clamped)."
++ },
++ "deadline_hours": {
++ "type": "integer",
++ "minimum": 1,
++ "maximum": 72,
++ "description": "For a batch run: how long the answer may take, in hours (1 to 72). The run is queued and answered within the deadline; a deadline the provider cannot meet sends the run back to the ordinary path. Omitted: the service's own default."
}
}
},
@@@ -15348,7 +15362,7 @@
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://openvibe.network/contracts/ai/run.v1.json",
"title": "AiRun",
-- "description": "A workflow run on OpenVibe.AI (ADR-015; server/runs.js decode): the `run` member of every run response (POST /api/v1/runs and the direct operations, GET /api/v1/runs/:id, cancel, retry) and each item of GET /api/v1/runs { runs }. Output is attributable to workflow + version + template version + route version + provider/model + run id, never to a person. A draft stays a draft: products store it with ai/ai_assisted authorship and publish nothing until a person reviews it. input keeps no inline media (replaced by data_url_sha256 and data_url_bytes) and no passthrough prompt ({ not_retained }); output is null until the run succeeded or was served from cache and then matches the workflow version's output_schema. requester is the token principal (service, app or mod). A run interrupted by a restart or shutdown ends failed with error.code run.interrupted. explain says why the run answered as it did: the task it read, the candidates it ranked and every attempt.",
++ "description": "A workflow run on OpenVibe.AI (ADR-015; server/runs.js decode): the `run` member of every run response (POST /api/v1/runs and the direct operations, GET /api/v1/runs/:id, cancel, retry) and each item of GET /api/v1/runs { runs }. Output is attributable to workflow + version + template version + route version + provider/model + run id, never to a person. A draft stays a draft: products store it with ai/ai_assisted authorship and publish nothing until a person reviews it. input keeps no inline media (replaced by data_url_sha256 and data_url_bytes) and no passthrough prompt ({ not_retained }); output is null until the run succeeded or was served from cache and then matches the workflow version's output_schema. requester is the token principal (service, app or mod). A run interrupted by a restart or shutdown ends failed with error.code run.interrupted. latency_class is the class the run really ran in, service_tier the tier it was priced on, and latency_clamped set when the caller asked for a faster class than the workflow allows. explain says why the run answered as it did: the task it read, the candidates it ranked and every attempt.",
"type": "object",
"required": [
"id",
@@@ -15646,6 +15660,59 @@
"minimum": 0,
"description": "Citations stored for the run (GET /api/v1/runs/:id/citations): the sources the workflow was given, and any the requester attached later."
},
++ "latency_class": {
++ "enum": [
++ "interactive",
++ "background",
++ "batch"
++ ],
++ "description": "The class the run really ran in: how long the caller was willing to wait. It is the workflow's own class (the route's constraints.latency_class) or a slower one the caller asked for (options.latency); a caller can never make a run faster than its workflow allows."
++ },
++ "service_tier": {
++ "type": [
++ "string",
++ "null"
++ ],
++ "enum": [
++ "standard",
++ "flex",
++ "batch",
++ "offpeak",
++ "local",
++ null
++ ],
++ "description": "The tier the run was priced on: the tier the provider really served (standard, flex, batch) or the one the router chose (offpeak inside a provider's off-peak hours, local on the host's own model). Null while the run has not answered. A provider that does not say which tier it served is priced at the list price: a saving nobody confirmed is not a saving."
++ },
++ "latency_clamped": {
++ "type": [
++ "object",
++ "null"
++ ],
++ "required": [
++ "asked",
++ "used"
++ ],
++ "additionalProperties": false,
++ "properties": {
++ "asked": {
++ "enum": [
++ "interactive",
++ "background",
++ "batch"
++ ],
++ "description": "The class the caller asked for."
++ },
++ "used": {
++ "enum": [
++ "interactive",
++ "background",
++ "batch"
++ ],
++ "description": "The class the run really ran in."
++ }
++ },
++ "description": "Set when the caller asked for a faster class than the workflow allows: the class asked for and the class the run really ran in. The run is never refused for it."
++ },
"retry_of": {
"type": [
"string",
@@@ -18835,6 +18902,376 @@
]
}
},
++ "ai.batch": {
++ "version": "1.0.0",
++ "owner": "ai",
++ "status": "active",
++ "schema": {
++ "$schema": "https://json-schema.org/draft/2020-12/schema",
++ "$id": "https://openvibe.network/contracts/ai/batch.v1.json",
++ "title": "AiBatch",
++ "description": "ai.batch@1: a batch of many prompts on OpenVibe.AI (POST /api/v1/batches, GET /api/v1/batches, GET /api/v1/batches/:id). The answers are not waited for: the batch is queued, and the batch says where it stands. A batch is billed at half the retail price through Billing like every other request; the batch table is the queue and the status, never a second ledger. The owner of the batch is the account that pays for it: a person, a developer app's project, or a first-party service with the ai.batches.create grant (the network's own account).",
++ "type": "object",
++ "required": [
++ "id",
++ "status",
++ "owner_type",
++ "owner_id",
++ "project_id",
++ "item_count",
++ "items",
++ "discount",
++ "deadline_at",
++ "created_at",
++ "updated_at"
++ ],
++ "additionalProperties": false,
++ "properties": {
++ "id": {
++ "type": "string",
++ "pattern": "^bat_[0-9a-f]{24}$"
++ },
++ "status": {
++ "enum": [
++ "queued",
++ "running",
++ "succeeded",
++ "failed",
++ "expired",
++ "cancelled"
++ ],
++ "description": "queued (asked for), running (items are being answered), succeeded, failed (an item failed), expired (the deadline passed with nothing answered), cancelled (the caller cancelled it)."
++ },
++ "owner_type": {
++ "enum": [
++ "user",
++ "app",
++ "service"
++ ],
++ "description": "Who pays for the batch."
++ },
++ "owner_id": {
++ "type": "string"
++ },
++ "project_id": {
++ "type": [
++ "string",
++ "null"
++ ],
++ "description": "The developer app's project, when an app owns the batch."
++ },
++ "item_count": {
++ "type": "integer",
++ "minimum": 0,
++ "description": "How many prompts the batch carries."
++ },
++ "items": {
++ "type": "array",
++ "items": {
++ "$ref": "batch-item.v1.json"
++ },
++ "description": "The items, in order. The list of batches answers with the same objects and no items (an empty array): the items come from GET /api/v1/batches/:id."
++ },
++ "discount": {
++ "type": "string",
++ "description": "The share of the retail card the customer is charged, as a decimal fraction of a million (500000 = half)."
++ },
++ "deadline_at": {
++ "type": "string",
++ "format": "date-time",
++ "description": "When the batch stops waiting: the deadline the caller asked for (1 to 72 hours)."
++ },
++ "created_at": {
++ "type": "string",
++ "format": "date-time"
++ },
++ "updated_at": {
++ "type": "string",
++ "format": "date-time"
++ },
++ "finished_at": {
++ "type": [
++ "string",
++ "null"
++ ],
++ "format": "date-time"
++ },
++ "absorbed_nano_usd": {
++ "type": [
++ "string",
++ "null"
++ ],
++ "description": "What the network absorbed for this batch, in nano-USD, as a string."
++ },
++ "ran_on_regular_capacity": {
++ "type": "boolean",
++ "description": "The batch ran on the ordinary capacity (not on free or community capacity): it cost the network more than the customer paid, and the difference is the absorbed amount."
++ }
++ }
++ }
++ },
++ "ai.batch-item": {
++ "version": "1.0.0",
++ "owner": "ai",
++ "status": "active",
++ "schema": {
++ "$schema": "https://json-schema.org/draft/2020-12/schema",
++ "$id": "https://openvibe.network/contracts/ai/batch-item.v1.json",
++ "title": "AiBatchItem",
++ "description": "ai.batch-item@1: one prompt of a batch on OpenVibe.AI (POST /api/v1/batches). The item is the queue and the status of that one prompt: it waits for capacity (or for a provider's own batch API), runs, and carries the answer, the run it made and the money. A prompt that reached its deadline without capacity expires and is never charged.",
++ "type": "object",
++ "required": [
++ "idx",
++ "status",
++ "model",
++ "run_id",
++ "answer",
++ "error_code",
++ "error_detail",
++ "reason",
++ "capacity",
++ "route_provider",
++ "route_model",
++ "deadline_at",
++ "created_at",
++ "updated_at"
++ ],
++ "additionalProperties": false,
++ "properties": {
++ "idx": {
++ "type": "integer",
++ "minimum": 0,
++ "description": "The item's place in the batch."
++ },
++ "status": {
++ "enum": [
++ "queued",
++ "waiting",
++ "running",
++ "succeeded",
++ "failed",
++ "expired",
++ "cancelled"
++ ],
++ "description": "queued (asked for, not started), waiting (no cheap capacity yet), running (a provider works on it), succeeded, failed, expired (the deadline passed; not charged), cancelled (the batch was cancelled)."
++ },
++ "model": {
++ "type": [
++ "string",
++ "null"
++ ],
++ "description": "The model the item asked for, or the batch's own."
++ },
++ "run_id": {
++ "type": [
++ "string",
++ "null"
++ ],
++ "pattern": "^run_[0-9A-HJKMNP-TV-Z]{26}$",
++ "description": "The run the item made, when it made one."
++ },
++ "answer": {
++ "type": [
++ "string",
++ "null"
++ ],
++ "description": "The answer the provider gave, as text."
++ },
++ "error_code": {
++ "type": [
++ "string",
++ "null"
++ ]
++ },
++ "error_detail": {
++ "type": [
++ "string",
++ "null"
++ ]
++ },
++ "reason": {
++ "type": [
++ "string",
++ "null"
++ ],
++ "description": "Why the item ended without an answer (it expired, it was cancelled)."
++ },
++ "capacity": {
++ "type": [
++ "string",
++ "null"
++ ],
++ "description": "Where the answer came from: a community offer, the regular capacity, or a provider's own batch API."
++ },
++ "route_provider": {
++ "type": [
++ "string",
++ "null"
++ ]
++ },
++ "route_model": {
++ "type": [
++ "string",
++ "null"
++ ]
++ },
++ "cost_nano_usd": {
++ "type": [
++ "string",
++ "null"
++ ],
++ "description": "What the answer really cost, in nano-USD (1 USD = 1e9 nano), as a string: the money is never a float."
++ },
++ "absorbed_nano_usd": {
++ "type": [
++ "string",
++ "null"
++ ],
++ "description": "What the network absorbed (the route cost more than the customer paid), in nano-USD, as a string."
++ },
++ "charged_nano_usd": {
++ "type": [
++ "string",
++ "null"
++ ],
++ "description": "What the customer was charged, in nano-USD, as a string: half the retail card."
++ },
++ "deadline_at": {
++ "type": "string",
++ "format": "date-time"
++ },
++ "created_at": {
++ "type": "string",
++ "format": "date-time"
++ },
++ "updated_at": {
++ "type": "string",
++ "format": "date-time"
++ }
++ }
++ }
++ },
++ "ai.batch-create-request": {
++ "version": "1.0.0",
++ "owner": "ai",
++ "status": "active",
++ "schema": {
++ "$schema": "https://json-schema.org/draft/2020-12/schema",
++ "$id": "https://openvibe.network/contracts/ai/batch-create-request.v1.json",
++ "title": "AiBatchCreateRequest",
++ "description": "ai.batch-create-request@1: the body of POST /api/v1/batches on OpenVibe.AI (capability ai.batches.create). One batch of many prompts: the answers come later, and the batch is billed at half the retail price. Errors are problem+json: 422 input.invalid (no items, an empty prompt, a prompt over 20000 characters, a deadline outside 1 to 72 hours), 422 batch.too_large (more prompts than the service's cap), 403 batch.no_payer (the token names no account to pay), 403 batch.no_grant (a service without the ai.batches.create grant).",
++ "type": "object",
++ "required": [
++ "items"
++ ],
++ "additionalProperties": false,
++ "properties": {
++ "items": {
++ "type": "array",
++ "minItems": 1,
++ "description": "The prompts of the batch, in order. Each item is a prompt, or an object with a prompt and the model it wants.",
++ "items": {
++ "anyOf": [
++ {
++ "type": "string",
++ "minLength": 1,
++ "maxLength": 20000
++ },
++ {
++ "type": "object",
++ "required": [
++ "prompt"
++ ],
++ "additionalProperties": false,
++ "properties": {
++ "prompt": {
++ "type": "string",
++ "minLength": 1,
++ "maxLength": 20000
++ },
++ "model": {
++ "type": "string",
++ "description": "The model this item wants (a model key, or provider/model)."
++ }
++ }
++ }
++ ]
++ }
++ },
++ "model": {
++ "type": "string",
++ "description": "The model the whole batch asks for, when no item names one."
++ },
++ "deadline_hours": {
++ "type": "integer",
++ "minimum": 1,
++ "maximum": 72,
++ "description": "How long the batch may take, in hours (1 to 72). Omitted: the service's own default (24 hours). A prompt that reaches the deadline without cheap capacity expires and is never charged."
++ }
++ }
++ }
++ },
++ "ai.batch-create-result": {
++ "version": "1.0.0",
++ "owner": "ai",
++ "status": "active",
++ "schema": {
++ "$schema": "https://json-schema.org/draft/2020-12/schema",
++ "$id": "https://openvibe.network/contracts/ai/batch-create-result.v1.json",
++ "title": "AiBatchCreateResult",
++ "description": "ai.batch-create-result@1: the answers of ai.batches.create on OpenVibe.AI. POST /api/v1/batches → 201 { batch }; POST /api/v1/batches/:id/cancel → { batch } (the batch is cancelled: the items that had not run are cancelled and nothing is charged for them).",
++ "type": "object",
++ "required": [
++ "batch"
++ ],
++ "additionalProperties": false,
++ "properties": {
++ "batch": {
++ "$ref": "batch.v1.json"
++ }
++ }
++ }
++ },
++ "ai.batch-read-result": {
++ "version": "1.0.0",
++ "owner": "ai",
++ "status": "active",
++ "schema": {
++ "$schema": "https://json-schema.org/draft/2020-12/schema",
++ "$id": "https://openvibe.network/contracts/ai/batch-read-result.v1.json",
++ "title": "AiBatchReadResult",
++ "description": "ai.batch-read-result@1: the answers of ai.batches.read on OpenVibe.AI (the caller's own batches). GET /api/v1/batches → { batches } (newest first, the items not included); GET /api/v1/batches/:id → { batch } (with its items). Somebody else's batch is 404 batch.not_found.",
++ "anyOf": [
++ {
++ "type": "object",
++ "required": [
++ "batches"
++ ],
++ "additionalProperties": false,
++ "properties": {
++ "batches": {
++ "type": "array",
++ "items": {
++ "$ref": "batch.v1.json"
++ }
++ }
++ }
++ },
++ {
++ "type": "object",
++ "required": [
++ "batch"
++ ],
++ "additionalProperties": false,
++ "properties": {
++ "batch": {
++ "$ref": "batch.v1.json"
++ }
++ }
++ }
++ ]
++ }
++ },
"ai.run-read-result": {
"version": "1.0.0",
"owner": "ai",
ddiff --git a/generated/openapi/actor.json b/generated/openapi/actor.json
iindex 98887612..6913dd4b 100644
---- a/generated/openapi/actor.json
++++ b/generated/openapi/actor.json
@@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"title": "OpenVibe.Actor API",
-- "version": "0.335.0",
-- "description": "Routes of OpenVibe.Actor as the OpenVibe contracts describe them (openvibe-contracts 0.335.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
++ "version": "0.336.0",
++ "description": "Routes of OpenVibe.Actor as the OpenVibe contracts describe them (openvibe-contracts 0.336.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"name": "MIT",
"identifier": "MIT"
ddiff --git a/generated/openapi/ai.json b/generated/openapi/ai.json
iindex dd74ea49..9fabe41d 100644
---- a/generated/openapi/ai.json
++++ b/generated/openapi/ai.json
@@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"title": "OpenVibe.AI API",
-- "version": "0.335.0",
-- "description": "Routes of OpenVibe.AI as the OpenVibe contracts describe them (openvibe-contracts 0.335.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
++ "version": "0.336.0",
++ "description": "Routes of OpenVibe.AI as the OpenVibe contracts describe them (openvibe-contracts 0.336.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"name": "MIT",
"identifier": "MIT"
@@@ -288,6 +288,228 @@
}
}
},
++ "/api/v1/batches": {
++ "get": {
++ "operationId": "get_api_v1_batches",
++ "summary": "Read the caller's own batches and their items (GET /api/v1/batches, GET /api/v1/batches/:id).",
++ "description": "**ai.batches.read** (first-party): Read the caller's own batches and their items (GET /api/v1/batches, GET /api/v1/batches/:id). Somebody else's batch is not found.",
++ "tags": [
++ "ai.batches"
++ ],
++ "x-openvibe-capabilities": [
++ "ai.batches.read"
++ ],
++ "x-openvibe-visibility": [
++ "first-party"
++ ],
++ "x-openvibe-quota-class": "ai-read",
++ "security": [
++ {
++ "openvibeToken": [
++ "ai.batches.read"
++ ]
++ }
++ ],
++ "responses": {
++ "2XX": {
++ "description": "Success",
++ "content": {
++ "application/json": {
++ "schema": {
++ "$ref": "#/components/schemas/ai.batch-read-result.v1"
++ }
++ }
++ }
++ },
++ "default": {
++ "description": "An error (RFC 9457 problem details)",
++ "content": {
++ "application/problem+json": {
++ "schema": {
++ "$ref": "#/components/schemas/errors.problem.v1"
++ }
++ }
++ }
++ }
++ }
++ },
++ "post": {
++ "operationId": "post_api_v1_batches",
++ "summary": "Open a batch of many prompts on OpenVibe.AI (POST /api/v1/batches) and cancel one (POST /api/v1/batches/:id/cancel).",
++ "description": "**ai.batches.create** (first-party): Open a batch of many prompts on OpenVibe.AI (POST /api/v1/batches) and cancel one (POST /api/v1/batches/:id/cancel). A batch is answered later, through a provider's own batch API when there is one, and is billed at half the retail price. A first-party service needs this grant to open a batch on the network's own account; a person or a developer app pays with its own account.",
++ "tags": [
++ "ai.batches"
++ ],
++ "x-openvibe-capabilities": [
++ "ai.batches.create"
++ ],
++ "x-openvibe-visibility": [
++ "first-party"
++ ],
++ "x-openvibe-quota-class": "ai-batch",
++ "security": [
++ {
++ "openvibeToken": [
++ "ai.batches.create"
++ ]
++ }
++ ],
++ "requestBody": {
++ "required": false,
++ "content": {
++ "application/json": {
++ "schema": {
++ "$ref": "#/components/schemas/ai.batch-create-request.v1"
++ }
++ }
++ }
++ },
++ "responses": {
++ "2XX": {
++ "description": "Success",
++ "content": {
++ "application/json": {
++ "schema": {
++ "$ref": "#/components/schemas/ai.batch-create-result.v1"
++ }
++ }
++ }
++ },
++ "default": {
++ "description": "An error (RFC 9457 problem details)",
++ "content": {
++ "application/problem+json": {
++ "schema": {
++ "$ref": "#/components/schemas/errors.problem.v1"
++ }
++ }
++ }
++ }
++ }
++ }
++ },
++ "/api/v1/batches/{id}": {
++ "get": {
++ "operationId": "get_api_v1_batches_id",
++ "summary": "Read the caller's own batches and their items (GET /api/v1/batches, GET /api/v1/batches/:id).",
++ "description": "**ai.batches.read** (first-party): Read the caller's own batches and their items (GET /api/v1/batches, GET /api/v1/batches/:id). Somebody else's batch is not found.",
++ "tags": [
++ "ai.batches"
++ ],
++ "x-openvibe-capabilities": [
++ "ai.batches.read"
++ ],
++ "x-openvibe-visibility": [
++ "first-party"
++ ],
++ "x-openvibe-quota-class": "ai-read",
++ "security": [
++ {
++ "openvibeToken": [
++ "ai.batches.read"
++ ]
++ }
++ ],
++ "parameters": [
++ {
++ "name": "id",
++ "in": "path",
++ "required": true,
++ "schema": {
++ "type": "string"
++ }
++ }
++ ],
++ "responses": {
++ "2XX": {
++ "description": "Success",
++ "content": {
++ "application/json": {
++ "schema": {
++ "$ref": "#/components/schemas/ai.batch-read-result.v1"
++ }
++ }
++ }
++ },
++ "default": {
++ "description": "An error (RFC 9457 problem details)",
++ "content": {
++ "application/problem+json": {
++ "schema": {
++ "$ref": "#/components/schemas/errors.problem.v1"
++ }
++ }
++ }
++ }
++ }
++ }
++ },
++ "/api/v1/batches/{id}/cancel": {
++ "post": {
++ "operationId": "post_api_v1_batches_id_cancel",
++ "summary": "Open a batch of many prompts on OpenVibe.AI (POST /api/v1/batches) and cancel one (POST /api/v1/batches/:id/cancel).",
++ "description": "**ai.batches.create** (first-party): Open a batch of many prompts on OpenVibe.AI (POST /api/v1/batches) and cancel one (POST /api/v1/batches/:id/cancel). A batch is answered later, through a provider's own batch API when there is one, and is billed at half the retail price. A first-party service needs this grant to open a batch on the network's own account; a person or a developer app pays with its own account.",
++ "tags": [
++ "ai.batches"
++ ],
++ "x-openvibe-capabilities": [
++ "ai.batches.create"
++ ],
++ "x-openvibe-visibility": [
++ "first-party"
++ ],
++ "x-openvibe-quota-class": "ai-batch",
++ "security": [
++ {
++ "openvibeToken": [
++ "ai.batches.create"
++ ]
++ }
++ ],
++ "requestBody": {
++ "required": false,
++ "content": {
++ "application/json": {
++ "schema": {
++ "$ref": "#/components/schemas/ai.batch-create-request.v1"
++ }
++ }
++ }
++ },
++ "parameters": [
++ {
++ "name": "id",
++ "in": "path",
++ "required": true,
++ "schema": {
++ "type": "string"
++ }
++ }
++ ],
++ "responses": {
++ "2XX": {
++ "description": "Success",
++ "content": {
++ "application/json": {
++ "schema": {
++ "$ref": "#/components/schemas/ai.batch-create-result.v1"
++ }
++ }
++ }
++ },
++ "default": {
++ "description": "An error (RFC 9457 problem details)",
++ "content": {
++ "application/problem+json": {
++ "schema": {
++ "$ref": "#/components/schemas/errors.problem.v1"
++ }
++ }
++ }
++ }
++ }
++ }
++ },
"/api/v1/cache": {
"delete": {
"operationId": "delete_api_v1_cache",
@@@ -4726,6 +4948,341 @@
}
}
},
++ "ai.batch-create-request.v1": {
++ "title": "AiBatchCreateRequest",
++ "description": "ai.batch-create-request@1: the body of POST /api/v1/batches on OpenVibe.AI (capability ai.batches.create). One batch of many prompts: the answers come later, and the batch is billed at half the retail price. Errors are problem+json: 422 input.invalid (no items, an empty prompt, a prompt over 20000 characters, a deadline outside 1 to 72 hours), 422 batch.too_large (more prompts than the service's cap), 403 batch.no_payer (the token names no account to pay), 403 batch.no_grant (a service without the ai.batches.create grant).",
++ "type": "object",
++ "required": [
++ "items"
++ ],
++ "additionalProperties": false,
++ "properties": {
++ "items": {
++ "type": "array",
++ "minItems": 1,
++ "description": "The prompts of the batch, in order. Each item is a prompt, or an object with a prompt and the model it wants.",
++ "items": {
++ "anyOf": [
++ {
++ "type": "string",
++ "minLength": 1,
++ "maxLength": 20000
++ },
++ {
++ "type": "object",
++ "required": [
++ "prompt"
++ ],
++ "additionalProperties": false,
++ "properties": {
++ "prompt": {
++ "type": "string",
++ "minLength": 1,
++ "maxLength": 20000
++ },
++ "model": {
++ "type": "string",
++ "description": "The model this item wants (a model key, or provider/model)."
++ }
++ }
++ }
++ ]
++ }
++ },
++ "model": {
++ "type": "string",
++ "description": "The model the whole batch asks for, when no item names one."
++ },
++ "deadline_hours": {
++ "type": "integer",
++ "minimum": 1,
++ "maximum": 72,
++ "description": "How long the batch may take, in hours (1 to 72). Omitted: the service's own default (24 hours). A prompt that reaches the deadline without cheap capacity expires and is never charged."
++ }
++ }
++ },
++ "ai.batch-create-result.v1": {
++ "title": "AiBatchCreateResult",
++ "description": "ai.batch-create-result@1: the answers of ai.batches.create on OpenVibe.AI. POST /api/v1/batches → 201 { batch }; POST /api/v1/batches/:id/cancel → { batch } (the batch is cancelled: the items that had not run are cancelled and nothing is charged for them).",
++ "type": "object",
++ "required": [
++ "batch"
++ ],
++ "additionalProperties": false,
++ "properties": {
++ "batch": {
++ "$ref": "#/components/schemas/ai.batch.v1"
++ }
++ }
++ },
++ "ai.batch-item.v1": {
++ "title": "AiBatchItem",
++ "description": "ai.batch-item@1: one prompt of a batch on OpenVibe.AI (POST /api/v1/batches). The item is the queue and the status of that one prompt: it waits for capacity (or for a provider's own batch API), runs, and carries the answer, the run it made and the money. A prompt that reached its deadline without capacity expires and is never charged.",
++ "type": "object",
++ "required": [
++ "idx",
++ "status",
++ "model",
++ "run_id",
++ "answer",
++ "error_code",
++ "error_detail",
++ "reason",
++ "capacity",
++ "route_provider",
++ "route_model",
++ "deadline_at",
++ "created_at",
++ "updated_at"
++ ],
++ "additionalProperties": false,
++ "properties": {
++ "idx": {
++ "type": "integer",
++ "minimum": 0,
++ "description": "The item's place in the batch."
++ },
++ "status": {
++ "enum": [
++ "queued",
++ "waiting",
++ "running",
++ "succeeded",
++ "failed",
++ "expired",
++ "cancelled"
++ ],
++ "description": "queued (asked for, not started), waiting (no cheap capacity yet), running (a provider works on it), succeeded, failed, expired (the deadline passed; not charged), cancelled (the batch was cancelled)."
++ },
++ "model": {
++ "type": [
++ "string",
++ "null"
++ ],
++ "description": "The model the item asked for, or the batch's own."
++ },
++ "run_id": {
++ "type": [
++ "string",
++ "null"
++ ],
++ "pattern": "^run_[0-9A-HJKMNP-TV-Z]{26}$",
++ "description": "The run the item made, when it made one."
++ },
++ "answer": {
++ "type": [
++ "string",
++ "null"
++ ],
++ "description": "The answer the provider gave, as text."
++ },
++ "error_code": {
++ "type": [
++ "string",
++ "null"
++ ]
++ },
++ "error_detail": {
++ "type": [
++ "string",
++ "null"
++ ]
++ },
++ "reason": {
++ "type": [
++ "string",
++ "null"
++ ],
++ "description": "Why the item ended without an answer (it expired, it was cancelled)."
++ },
++ "capacity": {
++ "type": [
++ "string",
++ "null"
++ ],
++ "description": "Where the answer came from: a community offer, the regular capacity, or a provider's own batch API."
++ },
++ "route_provider": {
++ "type": [
++ "string",
++ "null"
++ ]
++ },
++ "route_model": {
++ "type": [
++ "string",
++ "null"
++ ]
++ },
++ "cost_nano_usd": {
++ "type": [
++ "string",
++ "null"
++ ],
++ "description": "What the answer really cost, in nano-USD (1 USD = 1e9 nano), as a string: the money is never a float."
++ },
++ "absorbed_nano_usd": {
++ "type": [
++ "string",
++ "null"
++ ],
++ "description": "What the network absorbed (the route cost more than the customer paid), in nano-USD, as a string."
++ },
++ "charged_nano_usd": {
++ "type": [
++ "string",
++ "null"
++ ],
++ "description": "What the customer was charged, in nano-USD, as a string: half the retail card."
++ },
++ "deadline_at": {
++ "type": "string",
++ "format": "date-time"
++ },
++ "created_at": {
++ "type": "string",
++ "format": "date-time"
++ },
++ "updated_at": {
++ "type": "string",
++ "format": "date-time"
++ }
++ }
++ },
++ "ai.batch-read-result.v1": {
++ "title": "AiBatchReadResult",
++ "description": "ai.batch-read-result@1: the answers of ai.batches.read on OpenVibe.AI (the caller's own batches). GET /api/v1/batches → { batches } (newest first, the items not included); GET /api/v1/batches/:id → { batch } (with its items). Somebody else's batch is 404 batch.not_found.",
++ "anyOf": [
++ {
++ "type": "object",
++ "required": [
++ "batches"
++ ],
++ "additionalProperties": false,
++ "properties": {
++ "batches": {
++ "type": "array",
++ "items": {
++ "$ref": "#/components/schemas/ai.batch.v1"
++ }
++ }
++ }
++ },
++ {
++ "type": "object",
++ "required": [
++ "batch"
++ ],
++ "additionalProperties": false,
++ "properties": {
++ "batch": {
++ "$ref": "#/components/schemas/ai.batch.v1"
++ }
++ }
++ }
++ ]
++ },
++ "ai.batch.v1": {
++ "title": "AiBatch",
++ "description": "ai.batch@1: a batch of many prompts on OpenVibe.AI (POST /api/v1/batches, GET /api/v1/batches, GET /api/v1/batches/:id). The answers are not waited for: the batch is queued, and the batch says where it stands. A batch is billed at half the retail price through Billing like every other request; the batch table is the queue and the status, never a second ledger. The owner of the batch is the account that pays for it: a person, a developer app's project, or a first-party service with the ai.batches.create grant (the network's own account).",
++ "type": "object",
++ "required": [
++ "id",
++ "status",
++ "owner_type",
++ "owner_id",
++ "project_id",
++ "item_count",
++ "items",
++ "discount",
++ "deadline_at",
++ "created_at",
++ "updated_at"
++ ],
++ "additionalProperties": false,
++ "properties": {
++ "id": {
++ "type": "string",
++ "pattern": "^bat_[0-9a-f]{24}$"
++ },
++ "status": {
++ "enum": [
++ "queued",
++ "running",
++ "succeeded",
++ "failed",
++ "expired",
++ "cancelled"
++ ],
++ "description": "queued (asked for), running (items are being answered), succeeded, failed (an item failed), expired (the deadline passed with nothing answered), cancelled (the caller cancelled it)."
++ },
++ "owner_type": {
++ "enum": [
++ "user",
++ "app",
++ "service"
++ ],
++ "description": "Who pays for the batch."
++ },
++ "owner_id": {
++ "type": "string"
++ },
++ "project_id": {
++ "type": [
++ "string",
++ "null"
++ ],
++ "description": "The developer app's project, when an app owns the batch."
++ },
++ "item_count": {
++ "type": "integer",
++ "minimum": 0,
++ "description": "How many prompts the batch carries."
++ },
++ "items": {
++ "type": "array",
++ "items": {
++ "$ref": "#/components/schemas/ai.batch-item.v1"
++ },
++ "description": "The items, in order. The list of batches answers with the same objects and no items (an empty array): the items come from GET /api/v1/batches/:id."
++ },
++ "discount": {
++ "type": "string",
++ "description": "The share of the retail card the customer is charged, as a decimal fraction of a million (500000 = half)."
++ },
++ "deadline_at": {
++ "type": "string",
++ "format": "date-time",
++ "description": "When the batch stops waiting: the deadline the caller asked for (1 to 72 hours)."
++ },
++ "created_at": {
++ "type": "string",
++ "format": "date-time"
++ },
++ "updated_at": {
++ "type": "string",
++ "format": "date-time"
++ },
++ "finished_at": {
++ "type": [
++ "string",
++ "null"
++ ],
++ "format": "date-time"
++ },
++ "absorbed_nano_usd": {
++ "type": [
++ "string",
++ "null"
++ ],
++ "description": "What the network absorbed for this batch, in nano-USD, as a string."
++ },
++ "ran_on_regular_capacity": {
++ "type": "boolean",
++ "description": "The batch ran on the ordinary capacity (not on free or community capacity): it cost the network more than the customer paid, and the difference is the absorbed amount."
++ }
++ }
++ },
"ai.credential-put.v1": {
"title": "AiCredentialPut",
"description": "ai.credential-put@1 (OpenVibe.AI; roadmap WS-O task 2): a person's own provider key, stored by the service that holds their consent (Live, for a streamer's AI viewers), PUT /api/v1/credentials/{subject} with capability ai.credential.manage. OpenVibe.AI encrypts the key at rest and never returns it; runs that name the credential call that provider with it, never a shared key, and are limited by `budget_usd_per_day`. `base_url` is any https endpoint (OpenAI-compatible or Anthropic); OpenVibe.AI connects only to public addresses. Provider `node` (contracts 0.220.0) is the person's own machine instead: `machine` names it, there is no key or base_url, and a run reaches it only through OpenVibe.Bot (capability bot.machine.ai), only for that person, where they turned on \"Let my AI use this computer's models\".",
@@@ -6895,7 +7452,7 @@
},
"ai.run-request.v1": {
"title": "AiRunRequest",
-- "description": "Body of POST /api/v1/runs on OpenVibe.AI (capability ai.run.create; ADR-015). Runs the newest active version of a workflow (or the given active or deprecated version) on input that must match that workflow version's own input schema; per-workflow input and output schemas live in AI's registry (GET /api/v1/workflows/:key), not here. The token's ns claim limits which workflow namespaces may run (403 capability.namespace_denied). ?wait=ms waits for the result: 201 finished or served from cache, 202 still queued/running (poll GET /api/v1/runs/:id), 200 an idempotent replay. Errors are problem+json: 404 workflow.not_found, 409 workflow.inactive, 409 idempotency.conflict, 413 input.too_large, 422 input.invalid, 429 quota.exceeded or queue.full with Retry-After. The direct operations POST /api/v1/{chat,generate,summarize,classify,extract,enrich,embed} take the same fields (except workflow and version) with the input fields at the top level and run workflow ai.<op>. target (EntityRef): what the output is about, part of the cache scope and a run filter. attribution (EntityRef): what the spend is attributed to for quotas and usage. on_behalf_of (SubjectRef): the person or actor the caller acts for, part of the cache scope and per-actor quotas.",
++ "description": "Body of POST /api/v1/runs on OpenVibe.AI (capability ai.run.create; ADR-015). Runs the newest active version of a workflow (or the given active or deprecated version) on input that must match that workflow version's own input schema; per-workflow input and output schemas live in AI's registry (GET /api/v1/workflows/:key), not here. The token's ns claim limits which workflow namespaces may run (403 capability.namespace_denied). ?wait=ms waits for the result: 201 finished or served from cache, 202 still queued/running (poll GET /api/v1/runs/:id), 200 an idempotent replay. Errors are problem+json: 404 workflow.not_found, 409 workflow.inactive, 409 idempotency.conflict, 413 input.too_large, 422 input.invalid, 429 quota.exceeded or queue.full with Retry-After. The direct operations POST /api/v1/{chat,generate,summarize,classify,extract,enrich,embed} take the same fields (except workflow and version) with the input fields at the top level and run workflow ai.<op>. target (EntityRef): what the output is about, part of the cache scope and a run filter. attribution (EntityRef): what the spend is attributed to for quotas and usage. on_behalf_of (SubjectRef): the person or actor the caller acts for, part of the cache scope and per-actor quotas. options.latency asks for a slower class than the workflow allows (options.deadline_hours says how long a batch run may take); the run answers with the class it ran in (run.latency_class), the tier it was priced on (run.service_tier) and, when the ask was clamped, the class it really ran in (run.latency_clamped).",
"type": "object",
"required": [
"workflow"
@@@ -6944,6 +7501,20 @@
"type": "boolean",
"default": false,
"description": "Keep raw prompts and responses in the request log, only when AI runs with raw debug logging on."
++ },
++ "latency": {
++ "enum": [
++ "interactive",
++ "background",
++ "batch"
++ ],
++ "description": "The class the caller asks for: how long the caller may wait, and what the router may do with the wait. interactive (the default) answers as soon as the provider can; background may take minutes, and the run is priced on the cheapest tier that can wait (a Flex tier, a provider's off-peak hours, the host's own idle model); batch may take hours, and the run is queued and answered through a provider's own batch API. The class is the workflow's own (the route's constraints.latency_class): a caller may ask for a SLOWER class and never for a faster one. A faster ask is not refused: the run happens in the workflow's own class and the answer says so (run.latency_clamped)."
++ },
++ "deadline_hours": {
++ "type": "integer",
++ "minimum": 1,
++ "maximum": 72,
++ "description": "For a batch run: how long the answer may take, in hours (1 to 72). The run is queued and answered within the deadline; a deadline the provider cannot meet sends the run back to the ordinary path. Omitted: the service's own default."
}
}
},
@@@ -6965,7 +7536,7 @@
},
"ai.run.v1": {
"title": "AiRun",
-- "description": "A workflow run on OpenVibe.AI (ADR-015; server/runs.js decode): the `run` member of every run response (POST /api/v1/runs and the direct operations, GET /api/v1/runs/:id, cancel, retry) and each item of GET /api/v1/runs { runs }. Output is attributable to workflow + version + template version + route version + provider/model + run id, never to a person. A draft stays a draft: products store it with ai/ai_assisted authorship and publish nothing until a person reviews it. input keeps no inline media (replaced by data_url_sha256 and data_url_bytes) and no passthrough prompt ({ not_retained }); output is null until the run succeeded or was served from cache and then matches the workflow version's output_schema. requester is the token principal (service, app or mod). A run interrupted by a restart or shutdown ends failed with error.code run.interrupted. explain says why the run answered as it did: the task it read, the candidates it ranked and every attempt.",
++ "description": "A workflow run on OpenVibe.AI (ADR-015; server/runs.js decode): the `run` member of every run response (POST /api/v1/runs and the direct operations, GET /api/v1/runs/:id, cancel, retry) and each item of GET /api/v1/runs { runs }. Output is attributable to workflow + version + template version + route version + provider/model + run id, never to a person. A draft stays a draft: products store it with ai/ai_assisted authorship and publish nothing until a person reviews it. input keeps no inline media (replaced by data_url_sha256 and data_url_bytes) and no passthrough prompt ({ not_retained }); output is null until the run succeeded or was served from cache and then matches the workflow version's output_schema. requester is the token principal (service, app or mod). A run interrupted by a restart or shutdown ends failed with error.code run.interrupted. latency_class is the class the run really ran in, service_tier the tier it was priced on, and latency_clamped set when the caller asked for a faster class than the workflow allows. explain says why the run answered as it did: the task it read, the candidates it ranked and every attempt.",
"type": "object",
"required": [
"id",
@@@ -7263,6 +7834,59 @@
"minimum": 0,
"description": "Citations stored for the run (GET /api/v1/runs/:id/citations): the sources the workflow was given, and any the requester attached later."
},
++ "latency_class": {
++ "enum": [
++ "interactive",
++ "background",
++ "batch"
++ ],
++ "description": "The class the run really ran in: how long the caller was willing to wait. It is the workflow's own class (the route's constraints.latency_class) or a slower one the caller asked for (options.latency); a caller can never make a run faster than its workflow allows."
++ },
++ "service_tier": {
++ "type": [
++ "string",
++ "null"
++ ],
++ "enum": [
++ "standard",
++ "flex",
++ "batch",
++ "offpeak",
++ "local",
++ null
++ ],
++ "description": "The tier the run was priced on: the tier the provider really served (standard, flex, batch) or the one the router chose (offpeak inside a provider's off-peak hours, local on the host's own model). Null while the run has not answered. A provider that does not say which tier it served is priced at the list price: a saving nobody confirmed is not a saving."
++ },
++ "latency_clamped": {
++ "type": [
++ "object",
++ "null"
++ ],
++ "required": [
++ "asked",
++ "used"
++ ],
++ "additionalProperties": false,
++ "properties": {
++ "asked": {
++ "enum": [
++ "interactive",
++ "background",
++ "batch"
++ ],
++ "description": "The class the caller asked for."
++ },
++ "used": {
++ "enum": [
++ "interactive",
++ "background",
++ "batch"
++ ],
++ "description": "The class the run really ran in."
++ }
++ },
++ "description": "Set when the caller asked for a faster class than the workflow allows: the class asked for and the class the run really ran in. The run is never refused for it."
++ },
"retry_of": {
"type": [
"string",
ddiff --git a/generated/openapi/bench.json b/generated/openapi/bench.json
iindex 8a4dd398..04ffb917 100644
---- a/generated/openapi/bench.json
++++ b/generated/openapi/bench.json
@@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"title": "OpenVibe.Bench API",
-- "version": "0.335.0",
-- "description": "Routes of OpenVibe.Bench as the OpenVibe contracts describe them (openvibe-contracts 0.335.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
++ "version": "0.336.0",
++ "description": "Routes of OpenVibe.Bench as the OpenVibe contracts describe them (openvibe-contracts 0.336.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"name": "MIT",
"identifier": "MIT"
ddiff --git a/generated/openapi/billing.json b/generated/openapi/billing.json
iindex ec55e6a1..2c3ff9e5 100644
---- a/generated/openapi/billing.json
++++ b/generated/openapi/billing.json
@@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"title": "OpenVibe.Billing API",
-- "version": "0.335.0",
-- "description": "Routes of OpenVibe.Billing as the OpenVibe contracts describe them (openvibe-contracts 0.335.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
++ "version": "0.336.0",
++ "description": "Routes of OpenVibe.Billing as the OpenVibe contracts describe them (openvibe-contracts 0.336.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"name": "MIT",
"identifier": "MIT"
ddiff --git a/generated/openapi/blog.json b/generated/openapi/blog.json
iindex 043c4830..699f4f35 100644
---- a/generated/openapi/blog.json
++++ b/generated/openapi/blog.json
@@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"title": "OpenVibe.Blog API",
-- "version": "0.335.0",
-- "description": "Routes of OpenVibe.Blog as the OpenVibe contracts describe them (openvibe-contracts 0.335.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
++ "version": "0.336.0",
++ "description": "Routes of OpenVibe.Blog as the OpenVibe contracts describe them (openvibe-contracts 0.336.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"name": "MIT",
"identifier": "MIT"
ddiff --git a/generated/openapi/bot.json b/generated/openapi/bot.json
iindex 24061f40..29242782 100644
---- a/generated/openapi/bot.json
++++ b/generated/openapi/bot.json
@@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"title": "OpenVibe.Bot API",
-- "version": "0.335.0",
-- "description": "Routes of OpenVibe.Bot as the OpenVibe contracts describe them (openvibe-contracts 0.335.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
++ "version": "0.336.0",
++ "description": "Routes of OpenVibe.Bot as the OpenVibe contracts describe them (openvibe-contracts 0.336.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"name": "MIT",
"identifier": "MIT"
ddiff --git a/generated/openapi/chat.json b/generated/openapi/chat.json
iindex 3067f9b2..cc98f25d 100644
---- a/generated/openapi/chat.json
++++ b/generated/openapi/chat.json
@@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"title": "OpenVibe.Chat API",
-- "version": "0.335.0",
-- "description": "Routes of OpenVibe.Chat as the OpenVibe contracts describe them (openvibe-contracts 0.335.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
++ "version": "0.336.0",
++ "description": "Routes of OpenVibe.Chat as the OpenVibe contracts describe them (openvibe-contracts 0.336.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"name": "MIT",
"identifier": "MIT"
ddiff --git a/generated/openapi/codes.json b/generated/openapi/codes.json
iindex 58453eab..29416e2c 100644
---- a/generated/openapi/codes.json
++++ b/generated/openapi/codes.json
@@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"title": "OpenVibe.Codes API",
-- "version": "0.335.0",
-- "description": "Routes of OpenVibe.Codes as the OpenVibe contracts describe them (openvibe-contracts 0.335.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
++ "version": "0.336.0",
++ "description": "Routes of OpenVibe.Codes as the OpenVibe contracts describe them (openvibe-contracts 0.336.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"name": "MIT",
"identifier": "MIT"
ddiff --git a/generated/openapi/community.json b/generated/openapi/community.json
iindex 7be4e332..7c29d784 100644
---- a/generated/openapi/community.json
++++ b/generated/openapi/community.json
@@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"title": "OpenVibe.Community API",
-- "version": "0.335.0",
-- "description": "Routes of OpenVibe.Community as the OpenVibe contracts describe them (openvibe-contracts 0.335.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
++ "version": "0.336.0",
++ "description": "Routes of OpenVibe.Community as the OpenVibe contracts describe them (openvibe-contracts 0.336.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"name": "MIT",
"identifier": "MIT"
ddiff --git a/generated/openapi/connect.json b/generated/openapi/connect.json
iindex 5455b8db..d6876791 100644
---- a/generated/openapi/connect.json
++++ b/generated/openapi/connect.json
@@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"title": "OpenVibe Connect API",
-- "version": "0.335.0",
-- "description": "Routes of OpenVibe Connect as the OpenVibe contracts describe them (openvibe-contracts 0.335.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
++ "version": "0.336.0",
++ "description": "Routes of OpenVibe Connect as the OpenVibe contracts describe them (openvibe-contracts 0.336.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"name": "MIT",
"identifier": "MIT"
ddiff --git a/generated/openapi/coupons.json b/generated/openapi/coupons.json
iindex 3dbf5735..48c9358c 100644
---- a/generated/openapi/coupons.json
++++ b/generated/openapi/coupons.json
@@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"title": "OpenVibe.Coupons API",
-- "version": "0.335.0",
-- "description": "Routes of OpenVibe.Coupons as the OpenVibe contracts describe them (openvibe-contracts 0.335.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
++ "version": "0.336.0",
++ "description": "Routes of OpenVibe.Coupons as the OpenVibe contracts describe them (openvibe-contracts 0.336.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"name": "MIT",
"identifier": "MIT"
ddiff --git a/generated/openapi/deals.json b/generated/openapi/deals.json
iindex 303ed65c..e9bd8275 100644
---- a/generated/openapi/deals.json
++++ b/generated/openapi/deals.json
@@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"title": "OpenVibe.Deals API",
-- "version": "0.335.0",
-- "description": "Routes of OpenVibe.Deals as the OpenVibe contracts describe them (openvibe-contracts 0.335.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
++ "version": "0.336.0",
++ "description": "Routes of OpenVibe.Deals as the OpenVibe contracts describe them (openvibe-contracts 0.336.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"name": "MIT",
"identifier": "MIT"
ddiff --git a/generated/openapi/design.json b/generated/openapi/design.json
iindex 376f7f0f..75f2ee50 100644
---- a/generated/openapi/design.json
++++ b/generated/openapi/design.json
@@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"title": "OpenVibe.Design API",
-- "version": "0.335.0",
-- "description": "Routes of OpenVibe.Design as the OpenVibe contracts describe them (openvibe-contracts 0.335.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
++ "version": "0.336.0",
++ "description": "Routes of OpenVibe.Design as the OpenVibe contracts describe them (openvibe-contracts 0.336.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"name": "MIT",
"identifier": "MIT"
ddiff --git a/generated/openapi/domains.json b/generated/openapi/domains.json
iindex 2e3fc9cc..dec3b1ad 100644
---- a/generated/openapi/domains.json
++++ b/generated/openapi/domains.json
@@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"title": "OpenVibe.Domains API",
-- "version": "0.335.0",
-- "description": "Routes of OpenVibe.Domains as the OpenVibe contracts describe them (openvibe-contracts 0.335.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
++ "version": "0.336.0",
++ "description": "Routes of OpenVibe.Domains as the OpenVibe contracts describe them (openvibe-contracts 0.336.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"name": "MIT",
"identifier": "MIT"
ddiff --git a/generated/openapi/events.json b/generated/openapi/events.json
iindex 9b1d2999..29fb6e9c 100644
---- a/generated/openapi/events.json
++++ b/generated/openapi/events.json
@@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"title": "OpenVibe.Events API",
-- "version": "0.335.0",
-- "description": "Routes of OpenVibe.Events as the OpenVibe contracts describe them (openvibe-contracts 0.335.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
++ "version": "0.336.0",
++ "description": "Routes of OpenVibe.Events as the OpenVibe contracts describe them (openvibe-contracts 0.336.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"name": "MIT",
"identifier": "MIT"
ddiff --git a/generated/openapi/food.json b/generated/openapi/food.json
iindex 3315718d..8fa0de02 100644
---- a/generated/openapi/food.json
++++ b/generated/openapi/food.json
@@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"title": "OpenVibe.Food API",
-- "version": "0.335.0",
-- "description": "Routes of OpenVibe.Food as the OpenVibe contracts describe them (openvibe-contracts 0.335.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
++ "version": "0.336.0",
++ "description": "Routes of OpenVibe.Food as the OpenVibe contracts describe them (openvibe-contracts 0.336.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"name": "MIT",
"identifier": "MIT"
ddiff --git a/generated/openapi/games.json b/generated/openapi/games.json
iindex 9283c2fd..7b8d935e 100644
---- a/generated/openapi/games.json
++++ b/generated/openapi/games.json
@@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"title": "OpenVibe.Games API",
-- "version": "0.335.0",
-- "description": "Routes of OpenVibe.Games as the OpenVibe contracts describe them (openvibe-contracts 0.335.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
++ "version": "0.336.0",
++ "description": "Routes of OpenVibe.Games as the OpenVibe contracts describe them (openvibe-contracts 0.336.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"name": "MIT",
"identifier": "MIT"
ddiff --git a/generated/openapi/host.json b/generated/openapi/host.json
iindex 3010b82a..229569fa 100644
---- a/generated/openapi/host.json
++++ b/generated/openapi/host.json
@@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"title": "OpenVibe.Host API",
-- "version": "0.335.0",
-- "description": "Routes of OpenVibe.Host as the OpenVibe contracts describe them (openvibe-contracts 0.335.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
++ "version": "0.336.0",
++ "description": "Routes of OpenVibe.Host as the OpenVibe contracts describe them (openvibe-contracts 0.336.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"name": "MIT",
"identifier": "MIT"
ddiff --git a/generated/openapi/index.json b/generated/openapi/index.json
iindex a66792c2..6f25c958 100644
---- a/generated/openapi/index.json
++++ b/generated/openapi/index.json
@@@ -12,8 +12,8 @@
"service": "ai",
"name": "OpenVibe.AI",
"origin": "https://ai.openvibe.services",
-- "operations": 66,
-- "capabilities": 15
++ "operations": 70,
++ "capabilities": 17
},
{
"service": "bench",
ddiff --git a/generated/openapi/inventory.json b/generated/openapi/inventory.json
iindex 2c2a65d3..7d2847d2 100644
---- a/generated/openapi/inventory.json
++++ b/generated/openapi/inventory.json
@@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"title": "OpenVibe.Inventory API",
-- "version": "0.335.0",
-- "description": "Routes of OpenVibe.Inventory as the OpenVibe contracts describe them (openvibe-contracts 0.335.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
++ "version": "0.336.0",
++ "description": "Routes of OpenVibe.Inventory as the OpenVibe contracts describe them (openvibe-contracts 0.336.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"name": "MIT",
"identifier": "MIT"
ddiff --git a/generated/openapi/live.json b/generated/openapi/live.json
iindex 75b8057f..aabcc671 100644
---- a/generated/openapi/live.json
++++ b/generated/openapi/live.json
@@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"title": "OpenVibe.Live API",
-- "version": "0.335.0",
-- "description": "Routes of OpenVibe.Live as the OpenVibe contracts describe them (openvibe-contracts 0.335.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
++ "version": "0.336.0",
++ "description": "Routes of OpenVibe.Live as the OpenVibe contracts describe them (openvibe-contracts 0.336.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"name": "MIT",
"identifier": "MIT"
ddiff --git a/generated/openapi/mail.json b/generated/openapi/mail.json
iindex 13740e59..ff193525 100644
---- a/generated/openapi/mail.json
++++ b/generated/openapi/mail.json
@@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"title": "OpenVibe.Mail API",
-- "version": "0.335.0",
-- "description": "Routes of OpenVibe.Mail as the OpenVibe contracts describe them (openvibe-contracts 0.335.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
++ "version": "0.336.0",
++ "description": "Routes of OpenVibe.Mail as the OpenVibe contracts describe them (openvibe-contracts 0.336.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"name": "MIT",
"identifier": "MIT"
ddiff --git a/generated/openapi/media-hub.json b/generated/openapi/media-hub.json
iindex cf8b3645..0b8eff68 100644
---- a/generated/openapi/media-hub.json
++++ b/generated/openapi/media-hub.json
@@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"title": "OpenVibe.MediaHub API",
-- "version": "0.335.0",
-- "description": "Routes of OpenVibe.MediaHub as the OpenVibe contracts describe them (openvibe-contracts 0.335.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
++ "version": "0.336.0",
++ "description": "Routes of OpenVibe.MediaHub as the OpenVibe contracts describe them (openvibe-contracts 0.336.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"name": "MIT",
"identifier": "MIT"
ddiff --git a/generated/openapi/media.json b/generated/openapi/media.json
iindex 79829dfd..9abf65f1 100644
---- a/generated/openapi/media.json
++++ b/generated/openapi/media.json
@@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"title": "OpenVibe.Media API",
-- "version": "0.335.0",
-- "description": "Routes of OpenVibe.Media as the OpenVibe contracts describe them (openvibe-contracts 0.335.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
++ "version": "0.336.0",
++ "description": "Routes of OpenVibe.Media as the OpenVibe contracts describe them (openvibe-contracts 0.336.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"name": "MIT",
"identifier": "MIT"
ddiff --git a/generated/openapi/network.json b/generated/openapi/network.json
iindex 2c2e2a5b..c82580cc 100644
---- a/generated/openapi/network.json
++++ b/generated/openapi/network.json
@@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"title": "OpenVibe.Network API",
-- "version": "0.335.0",
-- "description": "Routes of OpenVibe.Network as the OpenVibe contracts describe them (openvibe-contracts 0.335.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
++ "version": "0.336.0",
++ "description": "Routes of OpenVibe.Network as the OpenVibe contracts describe them (openvibe-contracts 0.336.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"name": "MIT",
"identifier": "MIT"
ddiff --git a/generated/openapi/news.json b/generated/openapi/news.json
iindex 93e58cb8..8ab72cc8 100644
---- a/generated/openapi/news.json
++++ b/generated/openapi/news.json
@@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"title": "OpenVibe.News API",
-- "version": "0.335.0",
-- "description": "Routes of OpenVibe.News as the OpenVibe contracts describe them (openvibe-contracts 0.335.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
++ "version": "0.336.0",
++ "description": "Routes of OpenVibe.News as the OpenVibe contracts describe them (openvibe-contracts 0.336.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"name": "MIT",
"identifier": "MIT"
ddiff --git a/generated/openapi/openre.json b/generated/openapi/openre.json
iindex b7785d72..1037d71b 100644
---- a/generated/openapi/openre.json
++++ b/generated/openapi/openre.json
@@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"title": "OpenRestream API",
-- "version": "0.335.0",
-- "description": "Routes of OpenRestream as the OpenVibe contracts describe them (openvibe-contracts 0.335.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
++ "version": "0.336.0",
++ "description": "Routes of OpenRestream as the OpenVibe contracts describe them (openvibe-contracts 0.336.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"name": "MIT",
"identifier": "MIT"
ddiff --git a/generated/openapi/play.json b/generated/openapi/play.json
iindex b3659a30..cced82d9 100644
---- a/generated/openapi/play.json
++++ b/generated/openapi/play.json
@@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"title": "OpenVibe.Play API",
-- "version": "0.335.0",
-- "description": "Routes of OpenVibe.Play as the OpenVibe contracts describe them (openvibe-contracts 0.335.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
++ "version": "0.336.0",
++ "description": "Routes of OpenVibe.Play as the OpenVibe contracts describe them (openvibe-contracts 0.336.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"name": "MIT",
"identifier": "MIT"
ddiff --git a/generated/openapi/quest.json b/generated/openapi/quest.json
iindex 98867cef..223abcb4 100644
---- a/generated/openapi/quest.json
++++ b/generated/openapi/quest.json
@@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"title": "OpenVibe.Quest API",
-- "version": "0.335.0",
-- "description": "Routes of OpenVibe.Quest as the OpenVibe contracts describe them (openvibe-contracts 0.335.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
++ "version": "0.336.0",
++ "description": "Routes of OpenVibe.Quest as the OpenVibe contracts describe them (openvibe-contracts 0.336.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"name": "MIT",
"identifier": "MIT"
ddiff --git a/generated/openapi/report.json b/generated/openapi/report.json
iindex e1d097fc..31c1fde3 100644
---- a/generated/openapi/report.json
++++ b/generated/openapi/report.json
@@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"title": "OpenVibe.Report API",
-- "version": "0.335.0",
-- "description": "Routes of OpenVibe.Report as the OpenVibe contracts describe them (openvibe-contracts 0.335.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
++ "version": "0.336.0",
++ "description": "Routes of OpenVibe.Report as the OpenVibe contracts describe them (openvibe-contracts 0.336.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"name": "MIT",
"identifier": "MIT"
ddiff --git a/generated/openapi/reviews.json b/generated/openapi/reviews.json
iindex 7ee4f9ce..a6b423de 100644
---- a/generated/openapi/reviews.json
++++ b/generated/openapi/reviews.json
@@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"title": "OpenVibe.Reviews API",
-- "version": "0.335.0",
-- "description": "Routes of OpenVibe.Reviews as the OpenVibe contracts describe them (openvibe-contracts 0.335.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
++ "version": "0.336.0",
++ "description": "Routes of OpenVibe.Reviews as the OpenVibe contracts describe them (openvibe-contracts 0.336.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"name": "MIT",
"identifier": "MIT"
ddiff --git a/generated/openapi/run.json b/generated/openapi/run.json
iindex 4d827da1..92984fe4 100644
---- a/generated/openapi/run.json
++++ b/generated/openapi/run.json
@@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"title": "OpenVibe.Run API",
-- "version": "0.335.0",
-- "description": "Routes of OpenVibe.Run as the OpenVibe contracts describe them (openvibe-contracts 0.335.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
++ "version": "0.336.0",
++ "description": "Routes of OpenVibe.Run as the OpenVibe contracts describe them (openvibe-contracts 0.336.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"name": "MIT",
"identifier": "MIT"
ddiff --git a/generated/openapi/search.json b/generated/openapi/search.json
iindex 0acdc8e5..8957fbbd 100644
---- a/generated/openapi/search.json
++++ b/generated/openapi/search.json
@@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"title": "OpenVibe.Search API",
-- "version": "0.335.0",
-- "description": "Routes of OpenVibe.Search as the OpenVibe contracts describe them (openvibe-contracts 0.335.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
++ "version": "0.336.0",
++ "description": "Routes of OpenVibe.Search as the OpenVibe contracts describe them (openvibe-contracts 0.336.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"name": "MIT",
"identifier": "MIT"
ddiff --git a/generated/openapi/services.json b/generated/openapi/services.json
iindex 320fc5cb..4f7da548 100644
---- a/generated/openapi/services.json
++++ b/generated/openapi/services.json
@@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"title": "OpenVibe.Services API",
-- "version": "0.335.0",
-- "description": "Routes of OpenVibe.Services as the OpenVibe contracts describe them (openvibe-contracts 0.335.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
++ "version": "0.336.0",
++ "description": "Routes of OpenVibe.Services as the OpenVibe contracts describe them (openvibe-contracts 0.336.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"name": "MIT",
"identifier": "MIT"
ddiff --git a/generated/openapi/sources.json b/generated/openapi/sources.json
iindex 20e26c1a..6fc65742 100644
---- a/generated/openapi/sources.json
++++ b/generated/openapi/sources.json
@@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"title": "OpenVibe.Sources API",
-- "version": "0.335.0",
-- "description": "Routes of OpenVibe.Sources as the OpenVibe contracts describe them (openvibe-contracts 0.335.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
++ "version": "0.336.0",
++ "description": "Routes of OpenVibe.Sources as the OpenVibe contracts describe them (openvibe-contracts 0.336.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"name": "MIT",
"identifier": "MIT"
ddiff --git a/generated/openapi/space.json b/generated/openapi/space.json
iindex 747423e4..5f9d2193 100644
---- a/generated/openapi/space.json
++++ b/generated/openapi/space.json
@@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"title": "OpenVibe.Space API",
-- "version": "0.335.0",
-- "description": "Routes of OpenVibe.Space as the OpenVibe contracts describe them (openvibe-contracts 0.335.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
++ "version": "0.336.0",
++ "description": "Routes of OpenVibe.Space as the OpenVibe contracts describe them (openvibe-contracts 0.336.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"name": "MIT",
"identifier": "MIT"
ddiff --git a/generated/openapi/tips.json b/generated/openapi/tips.json
iindex a9928453..bfc6f648 100644
---- a/generated/openapi/tips.json
++++ b/generated/openapi/tips.json
@@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"title": "OpenVibe.Tips API",
-- "version": "0.335.0",
-- "description": "Routes of OpenVibe.Tips as the OpenVibe contracts describe them (openvibe-contracts 0.335.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
++ "version": "0.336.0",
++ "description": "Routes of OpenVibe.Tips as the OpenVibe contracts describe them (openvibe-contracts 0.336.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"name": "MIT",
"identifier": "MIT"
ddiff --git a/generated/openapi/today.json b/generated/openapi/today.json
iindex 74d37ff2..44948ae3 100644
---- a/generated/openapi/today.json
++++ b/generated/openapi/today.json
@@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"title": "OpenVibe.Today API",
-- "version": "0.335.0",
-- "description": "Routes of OpenVibe.Today as the OpenVibe contracts describe them (openvibe-contracts 0.335.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
++ "version": "0.336.0",
++ "description": "Routes of OpenVibe.Today as the OpenVibe contracts describe them (openvibe-contracts 0.336.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"name": "MIT",
"identifier": "MIT"
ddiff --git a/generated/openapi/tools.json b/generated/openapi/tools.json
iindex a043abff..6cdcd4e9 100644
---- a/generated/openapi/tools.json
++++ b/generated/openapi/tools.json
@@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"title": "OpenVibe.Tools API",
-- "version": "0.335.0",
-- "description": "Routes of OpenVibe.Tools as the OpenVibe contracts describe them (openvibe-contracts 0.335.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
++ "version": "0.336.0",
++ "description": "Routes of OpenVibe.Tools as the OpenVibe contracts describe them (openvibe-contracts 0.336.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"name": "MIT",
"identifier": "MIT"
ddiff --git a/generated/openapi/trade.json b/generated/openapi/trade.json
iindex e0d5eef8..e9743b42 100644
---- a/generated/openapi/trade.json
++++ b/generated/openapi/trade.json
@@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"title": "OpenVibe.Trade API",
-- "version": "0.335.0",
-- "description": "Routes of OpenVibe.Trade as the OpenVibe contracts describe them (openvibe-contracts 0.335.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
++ "version": "0.336.0",
++ "description": "Routes of OpenVibe.Trade as the OpenVibe contracts describe them (openvibe-contracts 0.336.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"name": "MIT",
"identifier": "MIT"
ddiff --git a/generated/openapi/vip.json b/generated/openapi/vip.json
iindex 6417296b..8395ad19 100644
---- a/generated/openapi/vip.json
++++ b/generated/openapi/vip.json
@@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"title": "OpenVibe.VIP API",
-- "version": "0.335.0",
-- "description": "Routes of OpenVibe.VIP as the OpenVibe contracts describe them (openvibe-contracts 0.335.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
++ "version": "0.336.0",
++ "description": "Routes of OpenVibe.VIP as the OpenVibe contracts describe them (openvibe-contracts 0.336.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"name": "MIT",
"identifier": "MIT"
ddiff --git a/generated/openapi/vision.json b/generated/openapi/vision.json
iindex cc9ce330..fafaff3a 100644
---- a/generated/openapi/vision.json
++++ b/generated/openapi/vision.json
@@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"title": "OpenVibe.Vision API",
-- "version": "0.335.0",
-- "description": "Routes of OpenVibe.Vision as the OpenVibe contracts describe them (openvibe-contracts 0.335.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
++ "version": "0.336.0",
++ "description": "Routes of OpenVibe.Vision as the OpenVibe contracts describe them (openvibe-contracts 0.336.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"name": "MIT",
"identifier": "MIT"
ddiff --git a/generated/openapi/watch.json b/generated/openapi/watch.json
iindex b781fb39..d411f286 100644
---- a/generated/openapi/watch.json
++++ b/generated/openapi/watch.json
@@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"title": "OpenVibe.Watch API",
-- "version": "0.335.0",
-- "description": "Routes of OpenVibe.Watch as the OpenVibe contracts describe them (openvibe-contracts 0.335.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
++ "version": "0.336.0",
++ "description": "Routes of OpenVibe.Watch as the OpenVibe contracts describe them (openvibe-contracts 0.336.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"name": "MIT",
"identifier": "MIT"
ddiff --git a/generated/openapi/wiki.json b/generated/openapi/wiki.json
iindex d8f3557d..fa615d86 100644
---- a/generated/openapi/wiki.json
++++ b/generated/openapi/wiki.json
@@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"title": "OpenVibe.Wiki API",
-- "version": "0.335.0",
-- "description": "Routes of OpenVibe.Wiki as the OpenVibe contracts describe them (openvibe-contracts 0.335.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
++ "version": "0.336.0",
++ "description": "Routes of OpenVibe.Wiki as the OpenVibe contracts describe them (openvibe-contracts 0.336.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"name": "MIT",
"identifier": "MIT"
ddiff --git a/generated/openapi/work.json b/generated/openapi/work.json
iindex d1ce565f..a18cd704 100644
---- a/generated/openapi/work.json
++++ b/generated/openapi/work.json
@@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"title": "OpenVibe.Work API",
-- "version": "0.335.0",
-- "description": "Routes of OpenVibe.Work as the OpenVibe contracts describe them (openvibe-contracts 0.335.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
++ "version": "0.336.0",
++ "description": "Routes of OpenVibe.Work as the OpenVibe contracts describe them (openvibe-contracts 0.336.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"name": "MIT",
"identifier": "MIT"
ddiff --git a/generated/openapi/zone.json b/generated/openapi/zone.json
iindex 6f249477..1bdd1772 100644
---- a/generated/openapi/zone.json
++++ b/generated/openapi/zone.json
@@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"title": "OpenVibe.Zone API",
-- "version": "0.335.0",
-- "description": "Routes of OpenVibe.Zone as the OpenVibe contracts describe them (openvibe-contracts 0.335.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
++ "version": "0.336.0",
++ "description": "Routes of OpenVibe.Zone as the OpenVibe contracts describe them (openvibe-contracts 0.336.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"name": "MIT",
"identifier": "MIT"
ddiff --git a/generated/typescript/index.d.ts b/generated/typescript/index.d.ts
iindex fa53ebf9..77b86b0d 100644
---- a/generated/typescript/index.d.ts
++++ b/generated/typescript/index.d.ts
@@@ -158449,7 +158449,7 @@ export interface AiAttributionQuota {
/** [email protected] (owner: ai) */
/**
-- * Body of POST /api/v1/runs on OpenVibe.AI (capability ai.run.create; ADR-015). Runs the newest active version of a workflow (or the given active or deprecated version) on input that must match that workflow version's own input schema; per-workflow input and output schemas live in AI's registry (GET /api/v1/workflows/:key), not here. The token's ns claim limits which workflow namespaces may run (403 capability.namespace_denied). ?wait=ms waits for the result: 201 finished or served from cache, 202 still queued/running (poll GET /api/v1/runs/:id), 200 an idempotent replay. Errors are problem+json: 404 workflow.not_found, 409 workflow.inactive, 409 idempotency.conflict, 413 input.too_large, 422 input.invalid, 429 quota.exceeded or queue.full with Retry-After. The direct operations POST /api/v1/{chat,generate,summarize,classify,extract,enrich,embed} take the same fields (except workflow and version) with the input fields at the top level and run workflow ai.<op>. target (EntityRef): what the output is about, part of the cache scope and a run filter. attribution (EntityRef): what the spend is attributed to for quotas and usage. on_behalf_of (SubjectRef): the person or actor the caller acts for, part of the cache scope and per-actor quotas.
++ * Body of POST /api/v1/runs on OpenVibe.AI (capability ai.run.create; ADR-015). Runs the newest active version of a workflow (or the given active or deprecated version) on input that must match that workflow version's own input schema; per-workflow input and output schemas live in AI's registry (GET /api/v1/workflows/:key), not here. The token's ns claim limits which workflow namespaces may run (403 capability.namespace_denied). ?wait=ms waits for the result: 201 finished or served from cache, 202 still queued/running (poll GET /api/v1/runs/:id), 200 an idempotent replay. Errors are problem+json: 404 workflow.not_found, 409 workflow.inactive, 409 idempotency.conflict, 413 input.too_large, 422 input.invalid, 429 quota.exceeded or queue.full with Retry-After. The direct operations POST /api/v1/{chat,generate,summarize,classify,extract,enrich,embed} take the same fields (except workflow and version) with the input fields at the top level and run workflow ai.<op>. target (EntityRef): what the output is about, part of the cache scope and a run filter. attribution (EntityRef): what the spend is attributed to for quotas and usage. on_behalf_of (SubjectRef): the person or actor the caller acts for, part of the cache scope and per-actor quotas. options.latency asks for a slower class than the workflow allows (options.deadline_hours says how long a batch run may take); the run answers with the class it ran in (run.latency_class), the tier it was priced on (run.service_tier) and, when the ask was clamped, the class it really ran in (run.latency_clamped).
*/
export interface AiRunRequest {
/**
@@@ -158480,6 +158480,14 @@ export interface AiRunRequest {
* Keep raw prompts and responses in the request log, only when AI runs with raw debug logging on.
*/
debug?: boolean;
++ /**
++ * The class the caller asks for: how long the caller may wait, and what the router may do with the wait. interactive (the default) answers as soon as the provider can; background may take minutes, and the run is priced on the cheapest tier that can wait (a Flex tier, a provider's off-peak hours, the host's own idle model); batch may take hours, and the run is queued and answered through a provider's own batch API. The class is the workflow's own (the route's constraints.latency_class): a caller may ask for a SLOWER class and never for a faster one. A faster ask is not refused: the run happens in the workflow's own class and the answer says so (run.latency_clamped).
++ */
++ latency?: "interactive" | "background" | "batch";
++ /**
++ * For a batch run: how long the answer may take, in hours (1 to 72). The run is queued and answered within the deadline; a deadline the provider cannot meet sends the run back to the ordinary path. Omitted: the service's own default.
++ */
++ deadline_hours?: number;
};
/**
* Run with the person's own provider key (ai.credential@1) that this service stored, instead of a shared route: only that provider is called (no fallback), the result is never cached or taken from the cache, the shared paid budget is not spent, and the credential's own daily budget applies (429 quota.exceeded). Chat operations only (text, JSON, vision).
@@@ -158491,7 +158499,7 @@ export interface AiRunRequest {
/** [email protected] (owner: ai) */
/**
-- * A workflow run on OpenVibe.AI (ADR-015; server/runs.js decode): the `run` member of every run response (POST /api/v1/runs and the direct operations, GET /api/v1/runs/:id, cancel, retry) and each item of GET /api/v1/runs { runs }. Output is attributable to workflow + version + template version + route version + provider/model + run id, never to a person. A draft stays a draft: products store it with ai/ai_assisted authorship and publish nothing until a person reviews it. input keeps no inline media (replaced by data_url_sha256 and data_url_bytes) and no passthrough prompt ({ not_retained }); output is null until the run succeeded or was served from cache and then matches the workflow version's output_schema. requester is the token principal (service, app or mod). A run interrupted by a restart or shutdown ends failed with error.code run.interrupted. explain says why the run answered as it did: the task it read, the candidates it ranked and every attempt.
++ * A workflow run on OpenVibe.AI (ADR-015; server/runs.js decode): the `run` member of every run response (POST /api/v1/runs and the direct operations, GET /api/v1/runs/:id, cancel, retry) and each item of GET /api/v1/runs { runs }. Output is attributable to workflow + version + template version + route version + provider/model + run id, never to a person. A draft stays a draft: products store it with ai/ai_assisted authorship and publish nothing until a person reviews it. input keeps no inline media (replaced by data_url_sha256 and data_url_bytes) and no passthrough prompt ({ not_retained }); output is null until the run succeeded or was served from cache and then matches the workflow version's output_schema. requester is the token principal (service, app or mod). A run interrupted by a restart or shutdown ends failed with error.code run.interrupted. latency_class is the class the run really ran in, service_tier the tier it was priced on, and latency_clamped set when the caller asked for a faster class than the workflow allows. explain says why the run answered as it did: the task it read, the candidates it ranked and every attempt.
*/
export interface AiRun {
id: string;
@@@ -158574,6 +158582,27 @@ export interface AiRun {
* Citations stored for the run (GET /api/v1/runs/:id/citations): the sources the workflow was given, and any the requester attached later.
*/
citations_count: number;
++ /**
++ * The class the run really ran in: how long the caller was willing to wait. It is the workflow's own class (the route's constraints.latency_class) or a slower one the caller asked for (options.latency); a caller can never make a run faster than its workflow allows.
++ */
++ latency_class?: "interactive" | "background" | "batch";
++ /**
++ * The tier the run was priced on: the tier the provider really served (standard, flex, batch) or the one the router chose (offpeak inside a provider's off-peak hours, local on the host's own model). Null while the run has not answered. A provider that does not say which tier it served is priced at the list price: a saving nobody confirmed is not a saving.
++ */
++ service_tier?: "standard" | "flex" | "batch" | "offpeak" | "local" | null;
++ /**
++ * Set when the caller asked for a faster class than the workflow allows: the class asked for and the class the run really ran in. The run is never refused for it.
++ */
++ latency_clamped?: {
++ /**
++ * The class the caller asked for.
++ */
++ asked: "interactive" | "background" | "batch";
++ /**
++ * The class the run really ran in.
++ */
++ used: "interactive" | "background" | "batch";
++ } | null;
retry_of: string | null;
idempotency_key: string | null;
trace_id: string | null;
@@@ -159864,6 +159893,170 @@ export type AiRunCreateResult =
citations: AiRunCitation[];
};
++/** [email protected] (owner: ai) */
++/**
++ * ai.batch@1: a batch of many prompts on OpenVibe.AI (POST /api/v1/batches, GET /api/v1/batches, GET /api/v1/batches/:id). The answers are not waited for: the batch is queued, and the batch says where it stands. A batch is billed at half the retail price through Billing like every other request; the batch table is the queue and the status, never a second ledger. The owner of the batch is the account that pays for it: a person, a developer app's project, or a first-party service with the ai.batches.create grant (the network's own account).
++ */
++export interface AiBatch {
++ id: string;
++ /**
++ * queued (asked for), running (items are being answered), succeeded, failed (an item failed), expired (the deadline passed with nothing answered), cancelled (the caller cancelled it).
++ */
++ status: "queued" | "running" | "succeeded" | "failed" | "expired" | "cancelled";
++ /**
++ * Who pays for the batch.
++ */
++ owner_type: "user" | "app" | "service";
++ owner_id: string;
++ /**
++ * The developer app's project, when an app owns the batch.
++ */
++ project_id: string | null;
++ /**
++ * How many prompts the batch carries.
++ */
++ item_count: number;
++ /**
++ * The items, in order. The list of batches answers with the same objects and no items (an empty array): the items come from GET /api/v1/batches/:id.
++ */
++ items: AiBatchItem[];
++ /**
++ * The share of the retail card the customer is charged, as a decimal fraction of a million (500000 = half).
++ */
++ discount: string;
++ /**
++ * When the batch stops waiting: the deadline the caller asked for (1 to 72 hours).
++ */
++ deadline_at: string;
++ created_at: string;
++ updated_at: string;
++ finished_at?: string | null;
++ /**
++ * What the network absorbed for this batch, in nano-USD, as a string.
++ */
++ absorbed_nano_usd?: string | null;
++ /**
++ * The batch ran on the ordinary capacity (not on free or community capacity): it cost the network more than the customer paid, and the difference is the absorbed amount.
++ */
++ ran_on_regular_capacity?: boolean;
++}
++
++/** [email protected] (owner: ai) */
++/**
++ * ai.batch-item@1: one prompt of a batch on OpenVibe.AI (POST /api/v1/batches). The item is the queue and the status of that one prompt: it waits for capacity (or for a provider's own batch API), runs, and carries the answer, the run it made and the money. A prompt that reached its deadline without capacity expires and is never charged.
++ */
++export interface AiBatchItem {
++ /**
++ * The item's place in the batch.
++ */
++ idx: number;
++ /**
++ * queued (asked for, not started), waiting (no cheap capacity yet), running (a provider works on it), succeeded, failed, expired (the deadline passed; not charged), cancelled (the batch was cancelled).
++ */
++ status: "queued" | "waiting" | "running" | "succeeded" | "failed" | "expired" | "cancelled";
++ /**
++ * The model the item asked for, or the batch's own.
++ */
++ model: string | null;
++ /**
++ * The run the item made, when it made one.
++ */
++ run_id: string | null;
++ /**
++ * The answer the provider gave, as text.
++ */
++ answer: string | null;
++ error_code: string | null;
++ error_detail: string | null;
++ /**
++ * Why the item ended without an answer (it expired, it was cancelled).
++ */
++ reason: string | null;
++ /**
++ * Where the answer came from: a community offer, the regular capacity, or a provider's own batch API.
++ */
++ capacity: string | null;
++ route_provider: string | null;
++ route_model: string | null;
++ /**
++ * What the answer really cost, in nano-USD (1 USD = 1e9 nano), as a string: the money is never a float.
++ */
++ cost_nano_usd?: string | null;
++ /**
++ * What the network absorbed (the route cost more than the customer paid), in nano-USD, as a string.
++ */
++ absorbed_nano_usd?: string | null;
++ /**
++ * What the customer was charged, in nano-USD, as a string: half the retail card.
++ */
++ charged_nano_usd?: string | null;
++ deadline_at: string;
++ created_at: string;
++ updated_at: string;
++}
++
++/** [email protected] (owner: ai) */
++/**
++ * ai.batch-create-request@1: the body of POST /api/v1/batches on OpenVibe.AI (capability ai.batches.create). One batch of many prompts: the answers come later, and the batch is billed at half the retail price. Errors are problem+json: 422 input.invalid (no items, an empty prompt, a prompt over 20000 characters, a deadline outside 1 to 72 hours), 422 batch.too_large (more prompts than the service's cap), 403 batch.no_payer (the token names no account to pay), 403 batch.no_grant (a service without the ai.batches.create grant).
++ */
++export interface AiBatchCreateRequest {
++ /**
++ * The prompts of the batch, in order. Each item is a prompt, or an object with a prompt and the model it wants.
++ *
++ * @minItems 1
++ */
++ items: [
++ (
++ | string
++ | {
++ prompt: string;
++ /**
++ * The model this item wants (a model key, or provider/model).
++ */
++ model?: string;
++ }
++ ),
++ ...(
++ | string
++ | {
++ prompt: string;
++ /**
++ * The model this item wants (a model key, or provider/model).
++ */
++ model?: string;
++ }
++ )[]
++ ];
++ /**
++ * The model the whole batch asks for, when no item names one.
++ */
++ model?: string;
++ /**
++ * How long the batch may take, in hours (1 to 72). Omitted: the service's own default (24 hours). A prompt that reaches the deadline without cheap capacity expires and is never charged.
++ */
++ deadline_hours?: number;
++}
++
++/** [email protected] (owner: ai) */
++/**
++ * ai.batch-create-result@1: the answers of ai.batches.create on OpenVibe.AI. POST /api/v1/batches → 201 { batch }; POST /api/v1/batches/:id/cancel → { batch } (the batch is cancelled: the items that had not run are cancelled and nothing is charged for them).
++ */
++export interface AiBatchCreateResult {
++ batch: AiBatch;
++}
++
++/** [email protected] (owner: ai) */
++/**
++ * ai.batch-read-result@1: the answers of ai.batches.read on OpenVibe.AI (the caller's own batches). GET /api/v1/batches → { batches } (newest first, the items not included); GET /api/v1/batches/:id → { batch } (with its items). Somebody else's batch is 404 batch.not_found.
++ */
++export type AiBatchReadResult =
++ | {
++ batches: AiBatch[];
++ }
++ | {
++ batch: AiBatch;
++ };
++
/** [email protected] (owner: ai) */
/**
* ai.run-read-result@1: answers of ai.run.read on OpenVibe.AI (the caller's own runs; usage readers may ask for all). GET /api/v1/runs?status=&workflow=&target=&limit=&before= → { runs }; GET /api/v1/runs/:id → { run, citations, requests } (the request log is metadata only: hashes, tokens, cost, latency, never prompts); GET /api/v1/runs/:id/citations → { citations }. Someone else's run is 404 run.not_found.
ddiff --git a/manifests/capabilities/ai.batches.create.json b/manifests/capabilities/ai.batches.create.json
nnew file mode 100644
iindex 00000000..94b2ce96
---- /dev/null
++++ b/manifests/capabilities/ai.batches.create.json
@@@ -0,0 +1,22 @@
++{
++ "id": "ai.batches.create",
++ "version": "1.0",
++ "owner": "ai",
++ "status": "active",
++ "visibility": "first-party",
++ "description": "Open a batch of many prompts on OpenVibe.AI (POST /api/v1/batches) and cancel one (POST /api/v1/batches/:id/cancel). A batch is answered later, through a provider's own batch API when there is one, and is billed at half the retail price. A first-party service needs this grant to open a batch on the network's own account; a person or a developer app pays with its own account.",
++ "inputSchema": "ai.batch-create-request@1",
++ "outputSchema": "ai.batch-create-result@1",
++ "permissions": [
++ "ai:batch"
++ ],
++ "resourceConstraints": [
++ "owner"
++ ],
++ "quotaClass": "ai-batch",
++ "events": [],
++ "implementedBy": [
++ "POST /api/v1/batches",
++ "POST /api/v1/batches/:id/cancel"
++ ]
++}
ddiff --git a/manifests/capabilities/ai.batches.read.json b/manifests/capabilities/ai.batches.read.json
nnew file mode 100644
iindex 00000000..48b5bbbb
---- /dev/null
++++ b/manifests/capabilities/ai.batches.read.json
@@@ -0,0 +1,22 @@
++{
++ "id": "ai.batches.read",
++ "version": "1.0",
++ "owner": "ai",
++ "status": "active",
++ "visibility": "first-party",
++ "description": "Read the caller's own batches and their items (GET /api/v1/batches, GET /api/v1/batches/:id). Somebody else's batch is not found.",
++ "inputSchema": "common.no-body@1",
++ "outputSchema": "ai.batch-read-result@1",
++ "permissions": [
++ "ai:read"
++ ],
++ "resourceConstraints": [
++ "owner"
++ ],
++ "quotaClass": "ai-read",
++ "events": [],
++ "implementedBy": [
++ "GET /api/v1/batches",
++ "GET /api/v1/batches/:id"
++ ]
++}
ddiff --git a/manifests/services/ai.json b/manifests/services/ai.json
iindex 50ec0a97..84392ee7 100644
---- a/manifests/services/ai.json
++++ b/manifests/services/ai.json
@@@ -25,6 +25,8 @@
"ai.model.read",
"ai.provider.manage",
"ai.quota.attribution.manage",
++ "ai.batches.create",
++ "ai.batches.read",
"ai.run.create",
"ai.run.read",
"ai.usage.read",
ddiff --git a/package.json b/package.json
iindex 2e1bb0b3..66fd45aa 100644
---- a/package.json
++++ b/package.json
@@@ -1,6 +1,6 @@
{
"name": "openvibe-contracts",
-- "version": "0.335.0",
++ "version": "0.336.0",
"description": "Machine-readable contracts for the OpenVibe network: schemas, manifests, ids, error envelope, capability checks.",
"main": "index.js",
"types": "generated/typescript/index.d.ts",
ddiff --git a/test/batching-latency.test.js b/test/batching-latency.test.js
nnew file mode 100644
iindex 00000000..34fb746b
---- /dev/null
++++ b/test/batching-latency.test.js
@@@ -0,0 +1,89 @@
++'use strict';
++// BA3 (plan track BA): the latency classes and the batches contract. The run's options carry the class
++// the caller asks for and, for a batch, how long the answer may take; the run's answer carries the class
++// it really ran in, the tier it was priced on, and the clamp when the ask was too fast. The batches
++// contract is the queue of many prompts that are answered later.
++const assert = require('assert');
++const fs = require('fs');
++const path = require('path');
++const Ajv = require('ajv/dist/2020');
++const addFormats = require('ajv-formats');
++const c = require('..');
++
++const ROOT = path.join(__dirname, '..');
++const load = (rel) => JSON.parse(fs.readFileSync(path.join(ROOT, 'contracts', rel), 'utf8'));
++
++// ── The options: the class the caller asks for ────────────────────────────
++const req = load('ai/run-request.v1.json');
++assert.deepStrictEqual(req.properties.options.properties.latency.enum, ['interactive', 'background', 'batch'],
++ 'the classes are interactive, background and batch');
++for (const cls of ['interactive', 'background', 'batch']) {
++ assert.ok(c.validate('ai.run-request@1', { workflow: 'live.rundown.episode', input: { a: 1 }, options: { latency: cls } }).valid,
++ 'a run may ask for the ' + cls + ' class');
++}
++assert.ok(!c.validate('ai.run-request@1', { workflow: 'live.rundown.episode', input: { a: 1 }, options: { latency: 'later' } }).valid,
++ 'a class that does not exist is refused');
++assert.ok(c.validate('ai.run-request@1', { workflow: 'live.rundown.episode', input: { a: 1 }, options: { latency: 'batch', deadline_hours: 1 } }).valid,
++ 'a batch run may ask for one hour');
++assert.ok(c.validate('ai.run-request@1', { workflow: 'live.rundown.episode', input: { a: 1 }, options: { latency: 'batch', deadline_hours: 72 } }).valid,
++ 'a batch run may ask for seventy-two hours');
++assert.ok(!c.validate('ai.run-request@1', { workflow: 'live.rundown.episode', input: { a: 1 }, options: { deadline_hours: 0 } }).valid,
++ 'no hour is not a deadline');
++assert.ok(!c.validate('ai.run-request@1', { workflow: 'live.rundown.episode', input: { a: 1 }, options: { deadline_hours: 73 } }).valid,
++ 'more than seventy-two hours is not a deadline');
++assert.ok(!c.validate('ai.run-request@1', { workflow: 'live.rundown.episode', input: { a: 1 }, options: { deadline_hours: 1.5 } }).valid,
++ 'a deadline is a whole number of hours');
++
++// ── The answer: the class it ran in, the tier it was priced on, the clamp ──
++// The run's own document has many parts (a provenance, a requester, a usage), so the three new members are
++// checked against the run's schema with the required list dropped: what matters here is that the members
++// exist, that they hold the right values, and that a run that does not carry them is still a valid run.
++const run = load('ai/run.v1.json');
++for (const name of ['latency_class', 'service_tier', 'latency_clamped']) {
++ assert.ok(run.properties[name], 'the run answers with ' + name);
++ assert.ok(!run.required.includes(name), name + ' is optional: a run that does not carry it stays a valid run');
++}
++const ajv = new Ajv({ strict: false, allErrors: true });
++addFormats(ajv);
++const member = (name) => ajv.compile(run.properties[name]);
++const latencyClass = member('latency_class');
++const serviceTier = member('service_tier');
++const latencyClamped = member('latency_clamped');
++assert.ok(latencyClass('batch'), 'a run may answer with the batch class');
++assert.ok(!latencyClass('now'), 'a class that does not exist is refused');
++for (const tier of ['standard', 'flex', 'batch', 'offpeak', 'local']) {
++ assert.ok(serviceTier(tier), 'the tier ' + tier + ' is a tier');
++}
++assert.ok(serviceTier(null), 'the tier is unknown until the run answers');
++assert.ok(!serviceTier('cheapest'), 'a tier that does not exist is refused');
++assert.ok(latencyClamped({ asked: 'interactive', used: 'background' }),
++ 'a clamped run says which class was asked for and which was used');
++assert.ok(!latencyClamped({ asked: 'interactive' }), 'a clamp without the class used is refused');
++assert.ok(!latencyClamped({ asked: 'now', used: 'background' }), 'a clamp names a class that exists');
++
++// ── The batches: many prompts, answered later ──────────────────────────────
++const batchDoc = {
++ id: 'bat_0123456789abcdef01234567', status: 'queued', owner_type: 'user', owner_id: 'usr_1', project_id: null,
++ item_count: 2, items: [], discount: '500000', deadline_at: '2026-11-08T22:58:00.000Z',
++ created_at: '2026-11-07T22:58:00.000Z', updated_at: '2026-11-07T22:58:00.000Z',
++};
++assert.ok(c.validate('ai.batch@1', batchDoc).valid, 'a batch is a queue with a deadline');
++assert.ok(c.validate('ai.batch-read-result@1', { batches: [batchDoc] }).valid, 'the list of batches answers with batches');
++assert.ok(c.validate('ai.batch-read-result@1', { batch: batchDoc }).valid, 'one batch answers with a batch');
++assert.ok(!c.validate('ai.batch-read-result@1', { batch: batchDoc, batches: [] }).valid, 'the read answers one way or the other');
++assert.ok(c.validate('ai.batch-create-request@1', { items: ['one prompt', { prompt: 'another', model: 'gpt-4o-mini' }] }).valid,
++ 'a batch carries prompts');
++assert.ok(c.validate('ai.batch-create-request@1', { items: ['a prompt'], deadline_hours: 24 }).valid, 'a batch may ask for a day');
++assert.ok(!c.validate('ai.batch-create-request@1', { items: ['a prompt'], deadline_hours: 100 }).valid,
++ 'a batch deadline outside 1 to 72 hours is refused');
++assert.ok(!c.validate('ai.batch-create-request@1', { items: [] }).valid, 'a batch without prompts is refused');
++assert.ok(c.validate('ai.batch-create-result@1', { batch: batchDoc }).valid, 'opening a batch answers with the batch');
++
++// ── The catalog and the grants ─────────────────────────────────────────────
++for (const id of ['ai.batch', 'ai.batch-item', 'ai.batch-create-request', 'ai.batch-create-result', 'ai.batch-read-result']) {
++ assert.ok(c.catalog.some(e => e.id === id), id + ' is in the catalog');
++}
++assert.ok(c.capabilities.get('ai.batches.create'), 'the grant that opens a batch exists');
++assert.ok(c.capabilities.get('ai.batches.read'), 'the grant that reads batches exists');
++assert.ok(c.services.get('ai').capabilities.includes('ai.batches.create'), 'the AI service offers the grant that opens a batch');
++assert.ok(c.services.get('ai').capabilities.includes('ai.batches.read'), 'the AI service offers the grant that reads batches');
ddiff --git a/test/run.js b/test/run.js
iindex c3dd083b..b6fd8d57 100644
---- a/test/run.js
++++ b/test/run.js
@@@ -960,6 +960,7 @@ sub(path.join(__dirname, 'billing-grace.test.js'));
sub(path.join(__dirname, 'resources.test.js'));
// The wait record (plan A2): a wait is a record, not a running loop.
sub(path.join(__dirname, 'actor-wait.test.js'));
++sub(path.join(__dirname, 'batching-latency.test.js'));
sub(path.join(__dirname, 'network-consent.test.js'));
// The machines hub (plan L34): one catalogue of roles, declared by the products that own them.
sub(path.join(__dirname, 'machines.test.js'));