openvibers/openvibe.ai Commits
C2 first slice: expiring keys, idempotency and routing previews (#382)
Changes
nnew file mode 100644@@@ -0,0 +1,16 @@++# C2 implementation status++++This is an independently reviewable first slice, not a completed parity release. It builds on current main, including the merged AI #379 platform and its bounded holds.++++Implemented:++++- Optional expires_at in the private key provisioning API and metadata. Expiry is checked at resolution and again at a spend reservation; an already admitted request may finish.++- Idempotency-Key forwarding for OpenAI Chat Completions and Anthropic Messages. The existing run store enforces ownership and rejects different inputs under one key. Replayed responses carry x-openvibe-replayed: 1. Opaque requests claim (owner, Idempotency-Key) in the database before reserving; replays reuse the original ceiling and stored run without another hold or usage row. A concurrent request still being admitted gets 409 idempotency.in_progress and can retry. Claims with a stored run survive restarts; an interrupted admission without a stored run remains refused rather than risking another hold.++- Authenticated, no-inference POST /v1/routing/preview: validates OpenRouter-shaped provider constraints, lists excluded offers, fails closed on unknown price/privacy/evidence, and never returns secrets or hidden/restricted/BYOK providers. It is explicitly not an execution policy.++- Authenticated POST /v1/presets/import/preview: validates an explicit exported JSON subset without fetching an account, storing a preset or executing a request. Success and error responses never echo submitted values; the importer returns only a validation summary. Model-level hidden/restricted/BYOK metadata is filtered as well as provider-level access.++++Outstanding: compatible legacy Completions/Embeddings/Images endpoints, full routing execution and model variants/fallbacks, persistent presets, generation lookup, web grounding/citations, live Report ratings, per-provider BYOK policy, tasks/choices and agent workflow step streams. The existing engine's features do not establish parity for those public endpoints.++++Contracts and SDK release changes are outside this first slice. Task-choice OpenAPI work remains in its separate job; this change only edits Exchange key schema fields.++++Migration 0054 is expand-only, after the applied 0052 platform and 0053 Link credential migration; neither applied migration is changed. No production data, provider execution, accounts or money were involved. Full-suite and focused results are recorded in the PR.nnew file mode 100644@@@ -0,0 +1,11 @@++-- phase: expand++-- Optional expiry; existing keys retain their current lifetime.++ALTER TABLE exchange_api_keys ADD COLUMN IF NOT EXISTS expires_at text;++++-- Claim a logical request before taking its hold. Store only a fingerprint and the++-- admitted ceiling; the durable runs table remains the authority for the result.++CREATE TABLE IF NOT EXISTS exchange_key_requests (++ owner text NOT NULL, idempotency_key text NOT NULL, request_hash text NOT NULL,++ max_cost_usd double precision,++ PRIMARY KEY(owner, idempotency_key)++);@@@ -26,7 +26,8 @@ const PERSON_RUN = `on_behalf_of IS NOT NULL AND on_behalf_of::jsonb ->> 'type'const RUN_COLUMNS = 'id, workflow_key, workflow_version, template_key, template_version, route_key, status, model_key, provider_key, input, output, citations_count, error_code, error_detail, tokens_in, tokens_out, cost_usd, created_at, started_at, finished_at';const TABLES = [-- { table: 'exchange_api_keys', subject: 'owner', file: 'exchange-keys.json', columns: ['id','name','prefix','monthly_cap_usd','allowed_models','created_at','revoked_at'], erase: { keep: 'extraErase deletes key usage and cap rows before deleting keys' } },++ { table: 'exchange_api_keys', subject: 'owner', file: 'exchange-keys.json', columns: ['id','name','prefix','monthly_cap_usd','allowed_models','created_at','revoked_at','expires_at'], erase: { keep: 'extraErase deletes key usage and cap rows before deleting keys' } },++ { table: 'exchange_key_requests', subject: 'owner', file: 'exchange-key-requests.json', columns: ['idempotency_key','request_hash','max_cost_usd'], erase: 'delete' },{ table: 'exchange_supply_settings', subject: 'actor', file: null, erase: { keep: 'owner pricing decisions are the operator audit trail' } },{ table: 'console_sessions', subject: 'subject', file: 'console-sessions.json', columns: ['username', 'created_at', 'expires_at', 'revoked_at'], order: 'created_at' },{ table: 'person_sessions', subject: 'subject', file: 'person-sessions.json', columns: ['username', 'created_at', 'expires_at', 'revoked_at'], order: 'created_at' },@@@ -212,7 +212,7 @@ function messagesRouter({ runs, auth, config, registry, presets = null, receipts}}}-- let body = { workflow: 'ai.messages', input };++ let body = { workflow: 'ai.messages', input, idempotency_key: req.get('idempotency-key') || undefined };// The preset's rules ride the run into the router.if (preset) body = { ...body, preset: { name: preset.name, scope: preset.scope, policy: preset.policy } };if (preset && preset.policy.adult && b.policy == null) body = { ...body, policy: 'uncensored' };@@@ -235,6 +235,7 @@ function messagesRouter({ runs, auth, config, registry, presets = null, receiptsconst created = await runs.create(body, req.principal, { trace: req.ov && req.ov.traceId, requestId: req.ov && req.ov.requestId, abuse: req.abuse,...(live ? { onDelta: live.event } : {}),...(live && config.modelCatalog && config.modelCatalog.enabled ? { onRoute: (info) => live.route(info) } : {}) });++ if (created.created === false) res.setHeader('x-openvibe-replayed', '1');if (live) live.open(created.run);run = await runs.explained(await runs.wait(created, Math.max(config.runs.maxWaitMs, 120000)));} catch (err) {@@@ -201,7 +201,7 @@ function openaiRouter({ runs, auth, config, registry, presets = null, receipts =if (maxTokens != null) input.max_tokens = Math.max(1, Math.min(Number(maxTokens) || 1, 16000));// AI Exchange F: the caller's liability cap for the Balance hold (an OpenVibe extension, like x_openvibe).if (b.max_cost_usd != null) input.max_cost_usd = b.max_cost_usd;-- let body = { workflow: 'ai.complete', input };++ let body = { workflow: 'ai.complete', input, idempotency_key: req.get('idempotency-key') || undefined };// The preset's rules ride the run into the router.if (preset) body = { ...body, preset: { name: preset.name, scope: preset.scope, policy: preset.policy } };if (preset && preset.policy.adult && b.policy == null) body = { ...body, policy: 'uncensored' };@@@ -238,6 +238,7 @@ function openaiRouter({ runs, auth, config, registry, presets = null, receipts =const created = await runs.create(body, req.principal, { trace: req.ov && req.ov.traceId, requestId: req.ov && req.ov.requestId, abuse: req.abuse,...(live ? { onDelta: live.delta } : {}),...(live && config.modelCatalog && config.modelCatalog.enabled ? { onRoute: (info) => live.route(info) } : {}) });++ if (created.created === false) res.setHeader('x-openvibe-replayed', '1');if (live) live.open(created.run);run = await runs.explained(await runs.wait(created, Math.max(config.runs.maxWaitMs, 120000)));} catch (err) {nnew file mode 100644@@@ -0,0 +1,107 @@++'use strict';++const express = require('express');++const { CAPS } = require('../auth');++const { policyFor, select } = require('../exchange/compat-routing');++const { canonicalOf } = require('../exchange/catalog');++const { sendError } = require('../util');++function routingPreview({ auth, registry, log }) {++ const r = express.Router();++ r.post(++ '/v1/routing/preview',++ auth.appOrCap(CAPS.appRun, CAPS.runCreate),++ async (req, res) => {++ try {++ const { model, variant, policy } = policyFor(req.body || {});++ if (variant === 'online')++ return res++ .status(400)++ .json({++ error: {++ code: 'input.invalid',++ message:++ 'Grounding is not part of a routing preview',++ },++ });++ const providers = new Map(++ (await registry.listProviders())++ .filter(++ (p) =>++ p.status === 'active' &&++ p.kind !== 'stub' &&++ p.metadata?.hidden !== true &&++ p.metadata?.synthetic !== true &&++ !['CUSTOMER_BYOK', 'RESTRICTED'].includes(++ p.provider_access,++ ),++ )++ .map((p) => [p.key, p]),++ );++ const rows = (await registry.listModels()).filter(++ (m) =>++ m.status === 'active' && providers.has(m.provider_key),++ ).filter(++ (m) => m.metadata?.hidden !== true && m.metadata?.synthetic !== true &&++ m.metadata?.restricted !== true && m.metadata?.byok !== true &&++ !['CUSTOMER_BYOK', 'RESTRICTED'].includes(m.provider_access || m.metadata?.provider_access),++ );++ const candidates = rows++ .filter(++ (m) =>++ model === 'openvibe/auto' ||++ canonicalOf(m, providers.get(m.provider_key)) ===++ model ||++ m.metadata?.id === model,++ )++ .map((m) => ({++ provider: m.provider_key,++ model: m.model_key,++ row: m,++ }));++ const answer = await select(candidates, policy, async (c) => {++ const p = providers.get(c.provider),++ m = c.row;++ const privacy = p.metadata?.data_policy || {};++ return {++ priceIn: m.cost?.in_per_mtok,++ priceOut: m.cost?.out_per_mtok,++ training: privacy.training,++ retention: privacy.retention,++ };++ });++ // No prompt, credential, balance hold or upstream request is needed for this diagnostic.++ return res++ .set('Cache-Control', 'no-store')++ .json({++ executable: false,++ notice: 'Catalogue-only preview; live availability, account gates and budgets are checked at execution. Quality and speed are excluded when no measured evidence is supplied.',++ candidates: answer.allowed.map((c) => ({++ provider: c.provider,++ model: c.model,++ })),++ excluded: answer.excluded,++ });++ } catch (err) {++ return sendError(res, err, req.ov, log);++ }++ },++ );++ r.post(++ '/v1/presets/import/preview',++ auth.appOrCap(CAPS.appRun, CAPS.runCreate),++ (req, res) => {++ try {++ return res++ .set('Cache-Control', 'no-store')++ .json(++ require('../exchange/import-preset').importPreset(++ req.body,++ ),++ );++ } catch (err) {++ return sendError(res, err, req.ov, log);++ }++ },++ );++ return r;++}++module.exports = { routingPreview };@@@ -217,6 +217,7 @@ function createApp({ config, db, registry, pool, quotas, cache, runs, auth, keysapp.use(runsRouter({ runs, registry, auth, config, log }));app.use(actionsRouter({ runs, registry, auth, log }));// The OpenAI-compatible endpoint (POST /v1/chat/completions, GET /v1/models): runs of ai.complete.++ app.use(require('./api/routing-preview').routingPreview({ auth, registry, log }));app.use(require('./api/openai').openaiRouter({ runs, auth, config, registry, presets, receipts, log }));// The Anthropic-compatible endpoint (POST /v1/messages, GET /v1/models): runs of ai.messages (Codes C3).app.use(require('./api/messages').messagesRouter({ runs, auth, config, registry, presets, receipts, log }));nnew file mode 100644@@@ -0,0 +1,158 @@++'use strict';++const { AiError } = require('../util');++const sorts = ['price', 'throughput', 'latency', 'quality', 'value'];++const fields = [++ 'order',++ 'only',++ 'ignore',++ 'allow_fallbacks',++ 'sort',++ 'max_price',++ 'data_collection',++ 'zdr',++];++const bad = (message) => {++ throw new AiError(400, 'input.invalid', message);++};++function normalize(provider = {}) {++ if (!provider || typeof provider !== 'object' || Array.isArray(provider))++ bad('provider must be an object');++ for (const key of Object.keys(provider))++ if (!fields.includes(key))++ bad('Unsupported provider parameter');++ const out = {};++ for (const key of ['order', 'only', 'ignore'])++ if (provider[key] !== undefined) {++ if (++ !Array.isArray(provider[key]) ||++ provider[key].length > 64 ||++ provider[key].some(++ (x) =>++ typeof x !== 'string' ||++ !/^[a-zA-Z0-9._-]{1,100}$/.test(x),++ )++ )++ bad(`provider.${key} must be a list of provider ids`);++ out[key] = [...new Set(provider[key])];++ }++ for (const key of ['allow_fallbacks', 'zdr'])++ if (provider[key] !== undefined) {++ if (typeof provider[key] !== 'boolean')++ bad(`provider.${key} must be boolean`);++ out[key] = provider[key];++ }++ if (provider.sort !== undefined) {++ if (!sorts.includes(provider.sort)) bad('Unknown provider.sort');++ out.sort = provider.sort;++ }++ if (provider.data_collection !== undefined) {++ if (!['allow', 'deny'].includes(provider.data_collection))++ bad('Unknown data_collection policy');++ out.data_collection = provider.data_collection;++ }++ if (provider.max_price !== undefined) {++ const p = provider.max_price;++ if (++ !p ||++ typeof p !== 'object' ||++ Array.isArray(p) ||++ Object.keys(p).some((k) => !['prompt', 'completion'].includes(k))++ )++ bad(++ 'max_price accepts prompt and completion in USD per million tokens',++ );++ out.max_price = {};++ for (const [k, v] of Object.entries(p)) {++ if (typeof v !== 'number' || !Number.isFinite(v) || v < 0)++ bad('Prices must be finite nonnegative numbers');++ out.max_price[k] = v;++ }++ }++ return out;++}++function modelName(value) {++ if (typeof value !== 'string' || !value || value.length > 256)++ bad('model must be a model id');++ const m = /:(free|floor|nitro|online)$/.exec(value);++ return {++ model: m ? value.slice(0, -m[0].length) : value,++ variant: m?.[1] || null,++ };++}++function policyFor(body) {++ const { model, variant } = modelName(body.model || 'openvibe/auto');++ const policy = normalize(body.provider);++ if (variant === 'free') policy.max_price = { prompt: 0, completion: 0 };++ if (variant === 'floor') policy.sort = 'price';++ if (variant === 'nitro') policy.sort = 'throughput';++ return { model, variant, policy };++}++const finite = (x) => typeof x === 'number' && Number.isFinite(x);++async function select(list, policy, facts) {++ const ranked = [],++ excluded = [];++ for (const [index, candidate] of list.entries()) {++ const f = await facts(candidate),++ id = candidate.provider;++ let reason = null;++ if (policy.only && !policy.only.includes(id)) reason = 'provider.only';++ if (policy.ignore?.includes(id)) reason = 'provider.ignore';++ if (policy.data_collection === 'deny' && f.training !== false)++ reason = 'provider.data_collection';++ if (policy.zdr && f.retention !== 'none') reason = 'provider.zdr';++ for (const [key, fact] of [++ ['prompt', 'priceIn'],++ ['completion', 'priceOut'],++ ])++ if (++ policy.max_price?.[key] !== undefined &&++ (!finite(f[fact]) || f[fact] > policy.max_price[key])++ )++ reason = 'provider.max_price';++ const cost =++ finite(f.priceIn) && finite(f.priceOut)++ ? f.priceIn + f.priceOut++ : null;++ let score = 0;++ if (policy.sort === 'price') score = cost ?? Infinity;++ if (policy.sort === 'latency') {++ if (!finite(f.latency)) reason = 'provider.latency_unavailable';++ score = finite(f.latency) ? f.latency : Infinity;++ }++ if (policy.sort === 'throughput') {++ if (!finite(f.throughput))++ reason = 'provider.throughput_unavailable';++ score = finite(f.throughput) ? -f.throughput : Infinity;++ }++ if (policy.sort === 'quality' || policy.sort === 'value') {++ if (!finite(f.quality)) reason = 'provider.rating_unavailable';++ score =++ policy.sort === 'quality'++ ? -f.quality++ : cost === null++ ? Infinity++ : -(f.quality / Math.max(cost, 0.000001));++ }++ if (reason)++ excluded.push({ provider: id, model: candidate.model, reason });++ else++ ranked.push({++ candidate,++ score,++ index,++ order: policy.order?.includes(id)++ ? policy.order.indexOf(id)++ : Infinity,++ });++ }++ ranked.sort(++ (a, b) => a.order - b.order || a.score - b.score || a.index - b.index,++ );++ const allowed = ranked.map((x) => x.candidate);++ return {++ allowed:++ policy.allow_fallbacks === false ? allowed.slice(0, 1) : allowed,++ excluded,++ };++}++module.exports = { normalize, policyFor, modelName, select };nnew file mode 100644@@@ -0,0 +1,104 @@++'use strict';++const { normalize, modelName } = require('./compat-routing');++const { AiError } = require('../util');++const accepted = [++ 'name',++ 'model',++ 'models',++ 'system',++ 'provider',++ 'temperature',++ 'top_p',++ 'max_tokens',++ 'seed',++ 'response_format',++];++// Import preview is intentionally pure: no keys, URLs, provider calls or writes.++function importPreset(source) {++ const bad = (detail) => {++ throw new AiError(400, 'input.invalid', detail);++ };++ if (!source || typeof source !== 'object' || Array.isArray(source))++ bad('A preset must be a JSON object');++ const unsupported = Object.keys(source).filter(++ (k) => !accepted.includes(k),++ );++ if (unsupported.length)++ bad('Preset contains unsupported fields; remove them before importing');++ if (++ typeof source.name !== 'string' ||++ !/^[a-z0-9][a-z0-9_-]{0,63}$/.test(source.name)++ )++ bad('Preset name must be a lowercase slug');++ const out = { name: source.name };++ if (source.model !== undefined) {++ modelName(source.model);++ if (source.model.startsWith('@preset/'))++ bad('Nested presets are not supported');++ out.model = source.model;++ }++ if (source.models !== undefined) {++ if (++ !Array.isArray(source.models) ||++ source.models.length < 1 ||++ source.models.length > 8++ )++ bad('models must contain one to eight model ids');++ source.models.forEach((x) => {++ modelName(x);++ if (x.startsWith('@preset/'))++ bad('Nested presets are not supported');++ });++ out.models = [...new Set(source.models)];++ }++ if (!out.model && !out.models)++ bad('A preset must name a model or fallbacks');++ if (source.provider !== undefined)++ out.provider = normalize(source.provider);++ if (source.system !== undefined) {++ if (typeof source.system !== 'string' || source.system.length > 32000)++ bad('system must be at most 32000 characters');++ out.system = source.system;++ }++ for (const [k, max] of [++ ['temperature', 2],++ ['top_p', 1],++ ['max_tokens', 64000],++ ])++ if (source[k] !== undefined) {++ if (++ !Number.isFinite(source[k]) ||++ source[k] < 0 ||++ source[k] > max ||++ (k === 'max_tokens' &&++ (!Number.isInteger(source[k]) || source[k] < 1))++ )++ bad('Invalid sampling parameter');++ out[k] = source[k];++ }++ if (source.seed !== undefined) {++ if (!Number.isSafeInteger(source.seed))++ bad('seed must be a safe integer');++ out.seed = source.seed;++ }++ if (source.response_format !== undefined) {++ if (++ !source.response_format ||++ typeof source.response_format !== 'object' ||++ Array.isArray(source.response_format)++ )++ bad('response_format must be an object');++ out.response_format = JSON.parse(++ JSON.stringify(source.response_format),++ );++ }++ return {++ valid: true,++ fields: Object.keys(out),++ model_count: out.models ? out.models.length : 1,++ warnings: [++ 'Preview only: this does not save a preset or make a provider request.',++ ],++ };++}++module.exports = { importPreset };@@@ -475,6 +475,7 @@ const exchangeKey = {id: str({ pattern: '^xkey_' }), name: str({ maxLength: 80 }), prefix: str({ description: 'The first ten characters of the secret (ovx_…), to tell keys apart' }),monthly_cap_usd: { type: 'number', minimum: 0 }, allowed_models: arrayOf(str()), created_at: str({ format: 'date-time' }),revoked_at: nullable('string', { format: 'date-time' }),++ expires_at: nullable('string', { format: 'date-time' }),},};route('get', '/exchange/keys', {@@@ -487,6 +488,7 @@ route('post', '/exchange/keys', {description: 'Only a person. The secret (ovx_…) is in this answer only; it is stored as a digest. A run with the key is refused once the month\'s cap is spent, and for a model not in allowed_models.',requestBody: body({ type: 'object', required: ['name', 'monthly_cap_usd', 'allowed_models'], properties: {name: str({ minLength: 1, maxLength: 80 }), monthly_cap_usd: { type: 'number', minimum: 0 }, allowed_models: arrayOf(str({ maxLength: 200 })),++ expires_at: nullable('string', { format: 'date-time', description: 'Optional future expiry; checked at authentication and reservation, before a hold' }),} }),responses: { 201: json('The key and its secret, shown once', { type: 'object', required: ['key', 'secret'], properties: { key: exchangeKey, secret: str({ pattern: '^ovx_' }) } }), 422: problem, ...errors([401, 403]) },});nnew file mode 100644@@@ -0,0 +1,43 @@++'use strict';++const { AiError, sha256 } = require('../util');++++// A database uniqueness constraint, not a process-local lock: concurrent callers and++// restarts cannot take two key holds for the same owner's logical request.++async function claimRequest(db, owner, idempotencyKey, body) {++ if (!/^[A-Za-z0-9._:-]{8,128}$/.test(idempotencyKey))++ throw new AiError(422, 'input.invalid', 'Idempotency-Key must be 8-128 of [A-Za-z0-9._:-]');++ const fingerprint = sha256(body);++ const claimed = await db.prepare(++ 'INSERT INTO exchange_key_requests(owner,idempotency_key,request_hash) VALUES(?,?,?) ON CONFLICT DO NOTHING RETURNING owner',++ ).get(owner, idempotencyKey, fingerprint);++ const existingRun = () => db.prepare(++ "SELECT id FROM runs WHERE requester_type = 'user' AND requester_id = ? AND idempotency_key = ?",++ ).get(owner, idempotencyKey);++ if (!claimed) {++ const previous = await db.prepare(++ 'SELECT request_hash,max_cost_usd FROM exchange_key_requests WHERE owner = ? AND idempotency_key = ?',++ ).get(owner, idempotencyKey);++ if (!previous || previous.request_hash !== fingerprint)++ throw new AiError(409, 'idempotency.conflict', 'Idempotency-Key was used with different input');++ const run = await existingRun();++ if (!run)++ throw new AiError(409, 'idempotency.in_progress', 'The original request is still being admitted; retry shortly');++ // runs.create verifies the input again and returns this owner's stored run.++ // Retain the original ceiling even if the key's remaining budget changed.++ return { replay: true, maxCost: previous.max_cost_usd === null ? null : Number(previous.max_cost_usd) };++ }++ // The same person may first have used their person token rather than an opaque++ // key. Their existing run is still a replay, with its original request ceiling.++ if (await existingRun()) return { replay: true, maxCost: null };++ return {++ replay: false,++ admitted: (amount) => db.prepare(++ 'UPDATE exchange_key_requests SET max_cost_usd = ? WHERE owner = ? AND idempotency_key = ?',++ ).run(amount, owner, idempotencyKey),++ abandon: () => db.prepare(++ `DELETE FROM exchange_key_requests WHERE owner = ? AND idempotency_key = ?++ AND NOT EXISTS (SELECT 1 FROM runs WHERE requester_type = 'user' AND requester_id = ? AND idempotency_key = ?)`,++ ).run(owner, idempotencyKey, owner, idempotencyKey),++ };++}++module.exports = { claimRequest };@@@ -14,6 +14,7 @@ function createKeys({ db, clock = { now: () => Date.now() } }) {allowed_models: JSON.parse(r.allowed_models),created_at: r.created_at,revoked_at: r.revoked_at,++ expires_at: r.expires_at || null,});async function list(owner) {return (@@@ -42,11 +43,13 @@ function createKeys({ db, clock = { now: () => Date.now() } }) {'ai.invalid','A name, nonnegative monthly cap and allowed model list are required',);++ const expires = b.expires_at == null ? null : Date.parse(b.expires_at);++ if (expires !== null && (!Number.isFinite(expires) || expires <= clock.now())) throw new AiError(422, 'ai.invalid', 'expires_at must be a future timestamp');const secret = 'ovx_' + randomBytes(32).toString('base64url');const id = 'xkey_' + randomUUID();await db.prepare(-- 'INSERT INTO exchange_api_keys(id,owner,name,digest,prefix,monthly_cap_usd,allowed_models,created_at) VALUES(?,?,?,?,?,?,?,?)',++ 'INSERT INTO exchange_api_keys(id,owner,name,digest,prefix,monthly_cap_usd,allowed_models,created_at,expires_at) VALUES(?,?,?,?,?,?,?,?,?)',).run(id,@@@ -57,6 +60,7 @@ function createKeys({ db, clock = { now: () => Date.now() } }) {b.monthly_cap_usd,JSON.stringify(b.allowed_models),iso(),++ expires === null ? null : new Date(expires).toISOString(),);return { key: (await list(owner)).find((k) => k.id === id), secret };}@@@ -73,9 +77,9 @@ function createKeys({ db, clock = { now: () => Date.now() } }) {if (!/^ovx_[A-Za-z0-9_-]{43}$/.test(secret)) return null;return db.prepare(-- 'SELECT * FROM exchange_api_keys WHERE digest = ? AND revoked_at IS NULL',++ 'SELECT * FROM exchange_api_keys WHERE digest = ? AND revoked_at IS NULL AND (expires_at IS NULL OR expires_at > ?)',)-- .get(hash(secret));++ .get(hash(secret), iso());}/*** A hold for one call against the key's monthly cap: the call's own ceiling (`limit`, its max_cost_usd) or@@@ -84,6 +88,11 @@ function createKeys({ db, clock = { now: () => Date.now() } }) {* hung up) and gives the rest of the hold back. Search calls (`free`) take no hold and skip the model list.*/async function reserve(key, model, free = false, limit = null) {++ // Re-read the credential: a previously resolved key may have expired or been revoked.++ const active = await db.prepare(++ 'SELECT id FROM exchange_api_keys WHERE id = ? AND revoked_at IS NULL AND (expires_at IS NULL OR expires_at > ?)',++ ).get(key.id, iso());++ if (!active) throw new AiError(401, 'token.invalid', 'Invalid, expired or revoked Exchange key');const allowed = JSON.parse(key.allowed_models);if (!free && allowed.length && !allowed.includes(model))throw new AiError(403, 'exchange.model_denied', 'This model is not allowed by this key');@@@ -108,10 +117,11 @@ function createKeys({ db, clock = { now: () => Date.now() } }) {)UPDATE exchange_key_months x SET reserved = x.reserved + m.take FROM mWHERE x.key_id = m.key_id AND x.month = m.month-- AND EXISTS (SELECT 1 FROM exchange_api_keys k WHERE k.id = x.key_id AND k.revoked_at IS NULL)++ AND EXISTS (SELECT 1 FROM exchange_api_keys k WHERE k.id = x.key_id AND k.revoked_at IS NULL++ AND (k.expires_at IS NULL OR k.expires_at::timestamptz > GREATEST(?::timestamptz, clock_timestamp())))RETURNING m.take AS take`,)-- .get(want, key.monthly_cap_usd, key.id, month);++ .get(want, key.monthly_cap_usd, key.id, month, iso());if (!row)throw new AiError(429, 'exchange.key_cap', "This key's monthly cap is spent");amount = Number(row.take);@@@ -3,6 +3,7 @@ const express = require('express');const { CAPS } = require('../auth');const { AiError, sendError } = require('../util');const { createKeys } = require('./keys');++const { claimRequest } = require('./idempotency');const { createSearch } = require('../search');function platform({ db, auth, clock, log, searchFetch }) {const keys = createKeys({ db, clock });@@@ -92,6 +93,7 @@ function platform({ db, auth, clock, log, searchFetch }) {'',);if (!token.startsWith('ovx_')) return next();++ let claim = null;try {const isSearch =req.method === 'POST' &&@@@ -115,9 +117,14 @@ function platform({ db, auth, clock, log, searchFetch }) {throw new AiError(401,'token.invalid',-- 'Invalid or revoked Exchange key',++ 'Invalid, expired or revoked Exchange key',);-- const hold = models++ const allowed = JSON.parse(key.allowed_models);++ if (inference && allowed.length && !allowed.includes(req.body?.model))++ throw new AiError(403, 'exchange.model_denied', 'This model is not allowed by this key');++ if (inference && req.get('idempotency-key'))++ claim = await claimRequest(db, key.owner, req.get('idempotency-key'), req.body);++ const hold = models || claim?.replay? null: await keys.reserve(key,@@@ -127,13 +134,17 @@ function platform({ db, auth, clock, log, searchFetch }) {isSearch,Number.isFinite(req.body?.max_cost_usd) ? req.body.max_cost_usd : null,);-- if (inference) {-- req.body.max_cost_usd = Math.min(++ if (inference && !(claim?.replay && claim.maxCost === null)) {++ req.body.max_cost_usd = claim?.replay ? claim.maxCost : Math.min(hold.amount,Number.isFinite(req.body.max_cost_usd)? req.body.max_cost_usd: hold.amount,);++ if (claim && !claim.replay) {++ try { await claim.admitted(req.body.max_cost_usd); }++ catch (err) { await hold.settle({ cost_usd: 0 }); throw err; }++ }}req.exchangePrincipal = {kind: 'user',@@@ -172,14 +183,16 @@ function platform({ db, auth, clock, log, searchFetch }) {}const usage = result?.usage || {};const cost =-- isSearch && res.statusCode < 400++ (isSearch || res.getHeader('x-openvibe-replayed') === '1') && res.statusCode < 400? 0: result?.x_openvibe?.cost_usd;++ // An early disconnect can precede run insertion. Keep its claim++ // until a response finishes so a retry cannot take another hold.hold.settle({cost_usd: cost,tokens_in: usage.prompt_tokens || 0,tokens_out: usage.completion_tokens || 0,-- }).catch(() =>++ }).then(() => res.writableFinished ? claim?.abandon?.() : undefined).catch(() =>log.error('Exchange usage settlement failed'),);};@@@ -188,6 +201,7 @@ function platform({ db, auth, clock, log, searchFetch }) {}next();} catch (e) {++ if (claim && !claim.replay) await claim.abandon().catch(() => log.error('Exchange request cleanup failed'));sendError(res, e, req.ov, log);}}@@@ -51,6 +51,7 @@ t.test('an export carries the person\'s sessions and credential hints, and a delconst h = await boot({ env: { AI_EVENTS_SECRET: SECRET, AI_EVENTS_SUBSCRIBE: '0', OV_NETWORK_INTERNAL_URL: stub.url, OV_OAUTH_CLIENT_SECRET: 'ai-secret' } });try {for (const [subject, name] of [[A, 'ada'], [B, 'bo']]) {++ await h.db.exec('INSERT INTO exchange_key_requests(owner,idempotency_key,request_hash,max_cost_usd) VALUES ($1,$2,$3,1)', [subject, `request-${name}`, `hash-${name}`]);await h.db.exec("INSERT INTO console_sessions (id_hash, subject, username, staff, csrf, created_at, expires_at) VALUES ($1, $2, $3, '{}', 'c', '2026-10-01', '2027-10-01')", [`h_${name}`, subject, name]);await h.db.exec("INSERT INTO person_sessions (id_hash, subject, username, created_at, expires_at) VALUES ($1, $2, $3, '2026-10-01', '2027-10-01')", [`p_${name}`, subject, name]);await h.db.exec("INSERT INTO subject_credentials (owner, subject, provider, key_enc, key_hint, models, created_at, updated_at) VALUES ('proj_1', $1, 'openai', 'v1:secret', $2, '{}', '2026-10-01', '2026-10-02')", [subject, `hint-${name}`]);@@@ -82,10 +83,13 @@ t.test('an export carries the person\'s sessions and credential hints, and a delassert.ok(!JSON.stringify(part.body).includes('v1:cookie-secret'), 'the encrypted session cookie is never exported');assert.ok(!JSON.stringify(part.body).includes('v1:secret'), 'the encrypted key is never exported');assert.ok(!JSON.stringify(part.body).includes(B), 'nobody else\'s rows');++ assert.deepStrictEqual(part.body.files.find((f) => f.name === 'exchange-key-requests.json').content, [{ idempotency_key: 'request-ada', request_hash: 'hash-ada', max_cost_usd: 1 }]);const d = await deliver(ev('evt_01JZ0000000000000000000D01', 'network.account.deleted', { deletion_id: DEL, subject: A, aliases: [] }));assert.deepStrictEqual([d.status, d.json.outcome], [200, 'erased']);assert.strictEqual(Number(await h.db.value('SELECT count(*)::int FROM console_sessions WHERE subject = $1', [A])), 0);++ assert.strictEqual(Number(await h.db.value('SELECT count(*)::int FROM exchange_key_requests WHERE owner = $1', [A])), 0, 'request claims are erased with their owner');++ assert.strictEqual(Number(await h.db.value('SELECT count(*)::int FROM exchange_key_requests WHERE owner = $1', [B])), 1, 'another owner\'s claims remain');assert.strictEqual(Number(await h.db.value('SELECT count(*)::int FROM abuse_devices WHERE subject = $1', [A])), 0, 'the device row goes');assert.strictEqual(Number(await h.db.value('SELECT count(*)::int FROM abuse_daily_usage WHERE subject = $1', [`user:${A}`])), 0, 'the daily counter goes');assert.strictEqual(Number(await h.db.value('SELECT count(*)::int FROM abuse_devices WHERE subject = $1', [B])), 1, "nobody else's device row");nnew file mode 100644@@@ -0,0 +1,107 @@++'use strict';++const assert = require('node:assert/strict');++const {++ normalize,++ policyFor,++ select,++} = require('../server/exchange/compat-routing');++(async () => {++ assert.throws(() => normalize({ unrecognized: true }), /Unsupported/);++ assert.throws(() => normalize({ allow_fallbacks: 'false' }), /boolean/);++ assert.throws(++ () => normalize({ max_price: { prompt: -1 } }),++ /nonnegative/,++ );++ assert.deepEqual(++ policyFor({ model: 'maker/model:free' }).policy.max_price,++ { prompt: 0, completion: 0 },++ );++ const list = [++ { provider: 'a', model: 'm' },++ { provider: 'b', model: 'm' },++ { provider: 'c', model: 'm' },++ ];++ const facts = async (c) =>++ ({++ a: { priceIn: null, priceOut: null },++ b: {++ priceIn: 1,++ priceOut: 2,++ training: false,++ retention: 'none',++ quality: 80,++ },++ c: {++ priceIn: 2,++ priceOut: 4,++ training: true,++ retention: 'standard',++ quality: 90,++ },++ })[c.provider];++ assert.deepEqual(++ (++ await select(++ list,++ normalize({++ max_price: { prompt: 1 },++ data_collection: 'deny',++ zdr: true,++ }),++ facts,++ )++ ).allowed,++ [list[1]],++ );++ assert.deepEqual(++ (++ await select(++ list,++ normalize({++ only: ['b', 'c'],++ order: ['c'],++ allow_fallbacks: false,++ }),++ facts,++ )++ ).allowed,++ [list[2]],++ );++ assert.deepEqual(++ (await select(list, normalize({ sort: 'value' }), facts)).allowed,++ [list[1], list[2]],++ );++ assert.equal(++ (await select(list, normalize({ sort: 'quality' }), async () => ({})))++ .allowed.length,++ 0,++ );++ console.log(++ 'Strict routing fields, missing prices, privacy, provider order and evidence requirements passed',++ );++})().catch((e) => {++ console.error(e);++ process.exitCode = 1;++});++const { importPreset } = require('../server/exchange/import-preset');++assert.equal(++ importPreset({++ name: 'writer',++ model: 'maker/model',++ provider: { only: ['a'] },++ }).valid,++ true,++);++assert.throws(++ () =>++ importPreset({ name: 'writer', model: 'm', api_key: 'not-a-real-key' }),++ /unsupported/,++);++assert.throws(++ () => importPreset({ name: 'writer', model: '@preset/recursive' }),++ /Nested/,++);++const sensitive = 'private-content-do-not-reflect';++const summary = importPreset({ name: sensitive, model: sensitive, system: sensitive, provider: { only: [sensitive] }, response_format: { [sensitive]: sensitive } });++assert.equal(JSON.stringify(summary).includes(sensitive), false, 'validated values are never returned');++assert.throws(() => normalize({ [sensitive]: sensitive }), (e) => !e.message.includes(sensitive));nnew file mode 100644@@@ -0,0 +1,203 @@++'use strict';++const assert = require('node:assert/strict');++const { boot, token, ALL } = require('./helpers');++(async () => {++ const h = await boot();++ try {++ const auth = 'Bearer ' + token('live', ALL);++ const call = async (path, body, extra = {}) =>++ fetch(h.base + path, {++ method: 'POST',++ headers: {++ authorization: auth,++ 'content-type': 'application/json',++ ...extra,++ },++ body: JSON.stringify(body),++ });++ for (const path of ['/v1/routing/preview', '/v1/presets/import/preview']) assert.equal(++ (++ await fetch(h.base + path, {++ method: 'POST',++ headers: { 'content-type': 'application/json' },++ body: '{}',++ })++ ).status,++ 401,++ );++ const listProviders = h.registry.listProviders;++ const listModels = h.registry.listModels;++ const realFetch = global.fetch;++ const createRun = h.runs.create;++ let upstreamCalls = 0;++ let modelCalls = 0;++ global.fetch = (url, ...args) => {++ if (String(url).startsWith(h.base + '/')) return realFetch(url, ...args);++ upstreamCalls++;++ throw new Error('Preview attempted an account or provider fetch');++ };++ h.runs.create = () => { modelCalls++; throw new Error('Preview attempted inference'); };++ h.registry.listProviders = async () => [++ { key: 'public-fixture', kind: 'openai', status: 'active', provider_access: 'OWN_DEPLOYMENT', metadata: {} },++ { key: 'hidden-fixture', kind: 'openai', status: 'active', metadata: { hidden: true } },++ { key: 'restricted-fixture', kind: 'openai', status: 'active', provider_access: 'RESTRICTED' },++ { key: 'byok-fixture', kind: 'openai', status: 'active', provider_access: 'CUSTOMER_BYOK' },++ ];++ h.registry.listModels = async () => [++ ...['public-fixture', 'hidden-fixture', 'restricted-fixture', 'byok-fixture'].map((provider_key) => ({ provider_key, model_key: provider_key + '-model', status: 'active', metadata: {}, cost: {} })),++ ...[{ hidden: true }, { provider_access: 'RESTRICTED' }, { provider_access: 'CUSTOMER_BYOK' }].map((metadata, i) => ({ provider_key: 'public-fixture', model_key: 'private-model-' + i, status: 'active', metadata, cost: {} })),++ ];++ try {++ const preview = await call('/v1/routing/preview', {++ model: 'openvibe/auto',++ provider: {},++ });++ assert.equal(preview.status, 200);++ const visible = await preview.json();++ assert.equal(visible.executable, false);++ assert.deepEqual(visible.candidates, [{ provider: 'public-fixture', model: 'public-fixture-model' }]);++ assert.deepEqual(visible.excluded, []);++ const sensitive = 'submitted-private-marker';++ for (const [path, input] of [++ ['/v1/routing/preview', { model: sensitive, provider: { only: [sensitive] } }],++ ['/v1/routing/preview', { provider: { [sensitive]: sensitive } }],++ ['/v1/presets/import/preview', { name: sensitive, model: sensitive, system: sensitive, provider: { only: [sensitive] }, response_format: { [sensitive]: sensitive } }],++ ['/v1/presets/import/preview', { name: 'test', model: sensitive, [sensitive]: sensitive }],++ ]) {++ const response = await call(path, input);++ assert.ok([200, 400].includes(response.status));++ assert.equal((await response.text()).includes(sensitive), false);++ }++ assert.equal(++ (await call('/v1/routing/preview', { provider: { unknown: 'x' } }))++ .status,++ 400,++ );++ const imported = await call('/v1/presets/import/preview', {++ name: 'writer',++ model: 'openvibe/auto',++ });++ assert.equal(imported.status, 200);++ assert.equal((await imported.json()).valid, true);++ assert.equal(upstreamCalls, 0, 'previews never fetch an account or provider');++ assert.equal(modelCalls, 0, 'previews never create a run');++ assert.equal((await h.db.prepare('SELECT id FROM runs').all()).length, 0);++ assert.equal((await h.db.prepare('SELECT id FROM exchange_key_usage').all()).length, 0);++ } finally {++ global.fetch = realFetch;++ h.runs.create = createRun;++ h.registry.listProviders = listProviders;++ h.registry.listModels = listModels;++ }++ const body = {++ model: 'openvibe',++ messages: [{ role: 'user', content: 'Hello' }],++ };++ const first = await call('/v1/chat/completions', body, {++ 'idempotency-key': 'c2-repeat-request-001',++ });++ assert.equal(first.status, 200);++ const a = await first.json();++ const again = await call('/v1/chat/completions', body, {++ 'idempotency-key': 'c2-repeat-request-001',++ });++ assert.equal(again.status, 200);++ assert.equal(again.headers.get('x-openvibe-replayed'), '1');++ assert.equal((await again.json()).id, a.id);++ const changed = await call(++ '/v1/chat/completions',++ { ...body, messages: [{ role: 'user', content: 'Different' }] },++ { 'idempotency-key': 'c2-repeat-request-001' },++ );++ assert.equal(changed.status, 409);++ const messagesBody = { model: 'openvibe', max_tokens: 20, messages: body.messages };++ const messagesFirst = await call('/v1/messages', messagesBody, { 'idempotency-key': 'messages-repeat-001' });++ assert.equal(messagesFirst.status, 200);++ const messagesResult = await messagesFirst.json();++ const messagesAgain = await call('/v1/messages', messagesBody, { 'idempotency-key': 'messages-repeat-001' });++ assert.equal(messagesAgain.status, 200);++ assert.equal(messagesAgain.headers.get('x-openvibe-replayed'), '1');++ assert.deepEqual(await messagesAgain.json(), messagesResult);++ const { createKeys } = require('../server/platform/keys');++ const keys = createKeys({ db: h.db });++ const key = await keys.create('usr_01JAB2C3D4E5F6G7H8J9K0MNPQ', {++ name: 'replay',++ monthly_cap_usd: 1,++ allowed_models: [],++ });++ const keyHeaders = {++ authorization: 'Bearer ' + key.secret,++ 'idempotency-key': 'key-repeat-request-001',++ };++ const prepare = h.db.prepare.bind(h.db);++ let holdStatements = 0;++ h.db.prepare = (sql) => {++ if (sql.includes('SET reserved = x.reserved + m.take')) holdStatements++;++ return prepare(sql);++ };++ const keyFirst = await call('/v1/chat/completions', body, keyHeaders);++ assert.equal(keyFirst.status, 200);++ const keyResult = await keyFirst.json();++ await new Promise((r) => setTimeout(r, 100));++ const before = await h.db.prepare('SELECT * FROM exchange_key_usage WHERE key_id = ?').all(key.key.id);++ assert.equal(before.length, 1);++ assert.equal(before[0].status, 'settled');++ // A replay must succeed even when no additional hold would fit.++ await h.db.prepare('UPDATE exchange_key_months SET spent = 1 WHERE key_id = ?').run(key.key.id);++ const keyAgain = await call('/v1/chat/completions', body, keyHeaders);++ assert.equal(keyAgain.status, 200);++ assert.equal(keyAgain.headers.get('x-openvibe-replayed'), '1');++ assert.deepEqual(await keyAgain.json(), keyResult);++ assert.equal(holdStatements, 1, 'a replay never takes a second hold');++ assert.deepEqual(await h.db.prepare('SELECT * FROM exchange_key_usage WHERE key_id = ?').all(key.key.id), before);++ assert.deepEqual(await h.db.prepare('SELECT spent,reserved FROM exchange_key_months WHERE key_id = ?').get(key.key.id), { spent: 1, reserved: 0 });++ const conflict = await call('/v1/chat/completions', { ...body, messages: [{ role: 'user', content: 'Changed' }] }, keyHeaders);++ assert.equal(conflict.status, 409);++ assert.equal(holdStatements, 1);++++ const sameOwner = await keys.create('usr_01JAB2C3D4E5F6G7H8J9K0MNPQ', { name: 'same owner', monthly_cap_usd: 0, allowed_models: [] });++ const sameReplay = await call('/v1/chat/completions', body, { ...keyHeaders, authorization: 'Bearer ' + sameOwner.secret });++ assert.equal(sameReplay.status, 200, 'idempotency belongs to the owner across their keys');++ assert.equal((await sameReplay.json()).id, keyResult.id);++ assert.equal(holdStatements, 1);++++ // Lookup rejects an expired key before the replay shortcut or any new hold.++ await h.db.prepare('UPDATE exchange_api_keys SET expires_at = ? WHERE id = ?').run('2000-01-01T00:00:00.000Z', sameOwner.key.id);++ const expired = await call('/v1/chat/completions', body, { ...keyHeaders, authorization: 'Bearer ' + sameOwner.secret });++ assert.equal(expired.status, 401);++ assert.equal(holdStatements, 1);++++ const other = await keys.create('usr_01JAB2C3D4E5F6G7H8J9K0MNPR', { name: 'other', monthly_cap_usd: 5, allowed_models: [] });++ const otherHeaders = { ...keyHeaders, authorization: 'Bearer ' + other.secret };++ const own = await call('/v1/chat/completions', body, otherHeaders);++ assert.equal(own.status, 200);++ assert.notEqual((await own.json()).id, keyResult.id, 'one owner cannot replay another owner\'s run');++ assert.equal(own.headers.get('x-openvibe-replayed'), null);++++ const holdCount = holdStatements;++ const concurrentHeaders = { ...otherHeaders, 'idempotency-key': 'concurrent-request-001' };++ const concurrent = await Promise.all(Array.from({ length: 4 }, () => call('/v1/chat/completions', body, concurrentHeaders)));++ const answers = await Promise.all(concurrent.map(async (r) => ({ status: r.status, body: await r.json() })));++ assert.ok(answers.some((r) => r.status === 200));++ assert.ok(answers.every((r) => r.status === 200 || r.status === 409));++ assert.equal(holdStatements - holdCount, 1, 'concurrent duplicates claim one hold');++ const concurrentReplay = await call('/v1/chat/completions', body, concurrentHeaders);++ assert.equal(concurrentReplay.status, 200);++ assert.equal((await concurrentReplay.json()).id, answers.find((r) => r.status === 200).body.id);++ assert.equal(holdStatements - holdCount, 1);++ assert.equal((await h.db.prepare("SELECT id FROM runs WHERE requester_type = 'user' AND requester_id = ? AND idempotency_key = ?").all('usr_01JAB2C3D4E5F6G7H8J9K0MNPR', 'concurrent-request-001')).length, 1);++ const { claimRequest } = require('../server/platform/idempotency');++ await claimRequest(h.db, 'pending-owner', 'pending-request-001', body);++ await assert.rejects(claimRequest(h.db, 'pending-owner', 'pending-request-001', body), (e) => e.code === 'idempotency.in_progress');++ assert.equal(holdStatements - holdCount, 1, 'an interrupted admission stays closed without a second hold');++ h.db.prepare = prepare;++ console.log(++ 'Authenticated preview, import validation and idempotent chat replay passed',++ );++ } finally {++ await h.stop();++ }++})().catch((e) => {++ console.error(e);++ process.exitCode = 1;++});nnew file mode 100644@@@ -0,0 +1,77 @@++'use strict';++const assert = require('node:assert/strict');++const { boot } = require('./helpers');++const { createKeys } = require('../server/platform/keys');++(async () => {++ const h = await boot();++ try {++ let now = Date.now();++ const keys = createKeys({ db: h.db, clock: { now: () => now } });++ await assert.rejects(++ keys.create('alice', {++ name: 'bad',++ monthly_cap_usd: 1,++ allowed_models: [],++ expires_at: 'not-a-date',++ }),++ (e) => e.status === 422,++ );++ const made = await keys.create('alice', {++ name: 'expiring',++ monthly_cap_usd: 1,++ allowed_models: [],++ expires_at: new Date(now + 1000).toISOString(),++ });++ const resolved = await keys.resolve(made.secret);++ assert.ok(resolved);++ assert.ok(made.key.expires_at);++ now += 2000;++ assert.equal(await keys.resolve(made.secret), undefined);++ for (const free of [false, true])++ await assert.rejects(keys.reserve(resolved, 'model', free), (e) => e.status === 401);++ assert.equal((await h.db.prepare('SELECT * FROM exchange_key_months WHERE key_id = ?').all(resolved.id)).length, 0);++ assert.equal((await h.db.prepare('SELECT * FROM exchange_key_usage WHERE key_id = ?').all(resolved.id)).length, 0);++++ const boundary = await keys.create('alice', {++ name: 'boundary', monthly_cap_usd: 2, allowed_models: [],++ expires_at: new Date(now + 1000).toISOString(),++ });++ const stale = await keys.resolve(boundary.secret);++ const firstHold = await keys.reserve(stale, 'model', false, 0.5);++ assert.equal(firstHold.amount, 0.5, 'main retains bounded holds');++ await firstHold.settle({ cost_usd: 0.1 });++ now += 1000;++ assert.equal(await keys.resolve(boundary.secret), undefined, 'expiry is inclusive');++ await assert.rejects(keys.reserve(stale, 'model'), (e) => e.status === 401);++ const ledger = await h.db.prepare('SELECT spent,reserved FROM exchange_key_months WHERE key_id = ?').get(stale.id);++ assert.deepEqual(ledger, { spent: 0.1, reserved: 0 });++++ // Expiry after the initial check is still refused by the atomic hold statement.++ const racing = await keys.create('alice', {++ name: 'race', monthly_cap_usd: 2, allowed_models: [],++ expires_at: new Date(now + 1000).toISOString(),++ });++ const raceKey = await keys.resolve(racing.secret);++ const raceDb = { prepare(sql) {++ const statement = h.db.prepare(sql);++ if (sql.startsWith('INSERT INTO exchange_key_months')) return { async run(...args) {++ const result = await statement.run(...args);++ now += 1000;++ return result;++ } };++ return statement;++ } };++ await assert.rejects(createKeys({ db: raceDb, clock: { now: () => now } }).reserve(raceKey, 'model'), (e) => e.status === 429);++ assert.equal((await h.db.prepare('SELECT reserved FROM exchange_key_months WHERE key_id = ?').get(raceKey.id)).reserved, 0);++ assert.equal((await h.db.prepare('SELECT * FROM exchange_key_usage WHERE key_id = ?').all(raceKey.id)).length, 0);++ assert.equal((await keys.list('alice')).length, 3);++ console.log(++ 'Expired keys fail authentication and stale-key reservations',++ );++ } finally {++ await h.stop();++ }++})().catch((e) => {++ console.error(e);++ process.exitCode = 1;++});
new file mode 100644
new file mode 100644
@@ -0,0 +1,16 @@
@@ -0,0 +1,16 @@
+# C2 implementation status
+
+This is an independently reviewable first slice, not a completed parity release. It builds on current main, including the merged AI #379 platform and its bounded holds.
+
+Implemented:
+
+- Optional expires_at in the private key provisioning API and metadata. Expiry is checked at resolution and again at a spend reservation; an already admitted request may finish.
+- Idempotency-Key forwarding for OpenAI Chat Completions and Anthropic Messages. The existing run store enforces ownership and rejects different inputs under one key. Replayed responses carry x-openvibe-replayed: 1. Opaque requests claim (owner, Idempotency-Key) in the database before reserving; replays reuse the original ceiling and stored run without another hold or usage row. A concurrent request still being admitted gets 409 idempotency.in_progress and can retry. Claims with a stored run survive restarts; an interrupted admission without a stored run remains refused rather than risking another hold.
+- Authenticated, no-inference POST /v1/routing/preview: validates OpenRouter-shaped provider constraints, lists excluded offers, fails closed on unknown price/privacy/evidence, and never returns secrets or hidden/restricted/BYOK providers. It is explicitly not an execution policy.
+- Authenticated POST /v1/presets/import/preview: validates an explicit exported JSON subset without fetching an account, storing a preset or executing a request. Success and error responses never echo submitted values; the importer returns only a validation summary. Model-level hidden/restricted/BYOK metadata is filtered as well as provider-level access.
+
+Outstanding: compatible legacy Completions/Embeddings/Images endpoints, full routing execution and model variants/fallbacks, persistent presets, generation lookup, web grounding/citations, live Report ratings, per-provider BYOK policy, tasks/choices and agent workflow step streams. The existing engine's features do not establish parity for those public endpoints.
+
+Contracts and SDK release changes are outside this first slice. Task-choice OpenAPI work remains in its separate job; this change only edits Exchange key schema fields.
+
+Migration 0054 is expand-only, after the applied 0052 platform and 0053 Link credential migration; neither applied migration is changed. No production data, provider execution, accounts or money were involved. Full-suite and focused results are recorded in the PR.
new file mode 100644
new file mode 100644
@@ -0,0 +1,11 @@
@@ -0,0 +1,11 @@
+-- phase: expand
+-- Optional expiry; existing keys retain their current lifetime.
+ALTER TABLE exchange_api_keys ADD COLUMN IF NOT EXISTS expires_at text;
+
+-- Claim a logical request before taking its hold. Store only a fingerprint and the
+-- admitted ceiling; the durable runs table remains the authority for the result.
+CREATE TABLE IF NOT EXISTS exchange_key_requests (
+ owner text NOT NULL, idempotency_key text NOT NULL, request_hash text NOT NULL,
+ max_cost_usd double precision,
+ PRIMARY KEY(owner, idempotency_key)
+);
@@ -26,7 +26,8 @@ const PERSON_RUN = `on_behalf_of IS NOT NULL AND on_behalf_of::jsonb ->> 'type'
@@ -26,7 +26,8 @@ const PERSON_RUN = `on_behalf_of IS NOT NULL AND on_behalf_of::jsonb ->> 'type'
const RUN_COLUMNS = 'id, workflow_key, workflow_version, template_key, template_version, route_key, status, model_key, provider_key, input, output, citations_count, error_code, error_detail, tokens_in, tokens_out, cost_usd, created_at, started_at, finished_at';
const RUN_COLUMNS = 'id, workflow_key, workflow_version, template_key, template_version, route_key, status, model_key, provider_key, input, output, citations_count, error_code, error_detail, tokens_in, tokens_out, cost_usd, created_at, started_at, finished_at';
const TABLES = [
const TABLES = [
- { table: 'exchange_api_keys', subject: 'owner', file: 'exchange-keys.json', columns: ['id','name','prefix','monthly_cap_usd','allowed_models','created_at','revoked_at'], erase: { keep: 'extraErase deletes key usage and cap rows before deleting keys' } },
+ { table: 'exchange_api_keys', subject: 'owner', file: 'exchange-keys.json', columns: ['id','name','prefix','monthly_cap_usd','allowed_models','created_at','revoked_at','expires_at'], erase: { keep: 'extraErase deletes key usage and cap rows before deleting keys' } },
+ { table: 'exchange_key_requests', subject: 'owner', file: 'exchange-key-requests.json', columns: ['idempotency_key','request_hash','max_cost_usd'], erase: 'delete' },
{ table: 'exchange_supply_settings', subject: 'actor', file: null, erase: { keep: 'owner pricing decisions are the operator audit trail' } },
{ table: 'exchange_supply_settings', subject: 'actor', file: null, erase: { keep: 'owner pricing decisions are the operator audit trail' } },
{ table: 'console_sessions', subject: 'subject', file: 'console-sessions.json', columns: ['username', 'created_at', 'expires_at', 'revoked_at'], order: 'created_at' },
{ table: 'console_sessions', subject: 'subject', file: 'console-sessions.json', columns: ['username', 'created_at', 'expires_at', 'revoked_at'], order: 'created_at' },
{ table: 'person_sessions', subject: 'subject', file: 'person-sessions.json', columns: ['username', 'created_at', 'expires_at', 'revoked_at'], order: 'created_at' },
{ table: 'person_sessions', subject: 'subject', file: 'person-sessions.json', columns: ['username', 'created_at', 'expires_at', 'revoked_at'], order: 'created_at' },
@@ -212,7 +212,7 @@ function messagesRouter({ runs, auth, config, registry, presets = null, receipts
@@ -212,7 +212,7 @@ function messagesRouter({ runs, auth, config, registry, presets = null, receipts
}
}
}
}
}
}
- let body = { workflow: 'ai.messages', input };
+ let body = { workflow: 'ai.messages', input, idempotency_key: req.get('idempotency-key') || undefined };
// The preset's rules ride the run into the router.
// The preset's rules ride the run into the router.
if (preset) body = { ...body, preset: { name: preset.name, scope: preset.scope, policy: preset.policy } };
if (preset) body = { ...body, preset: { name: preset.name, scope: preset.scope, policy: preset.policy } };
if (preset && preset.policy.adult && b.policy == null) body = { ...body, policy: 'uncensored' };
if (preset && preset.policy.adult && b.policy == null) body = { ...body, policy: 'uncensored' };
@@ -235,6 +235,7 @@ function messagesRouter({ runs, auth, config, registry, presets = null, receipts
@@ -235,6 +235,7 @@ function messagesRouter({ runs, auth, config, registry, presets = null, receipts
const created = await runs.create(body, req.principal, { trace: req.ov && req.ov.traceId, requestId: req.ov && req.ov.requestId, abuse: req.abuse,
const created = await runs.create(body, req.principal, { trace: req.ov && req.ov.traceId, requestId: req.ov && req.ov.requestId, abuse: req.abuse,
...(live ? { onDelta: live.event } : {}),
...(live ? { onDelta: live.event } : {}),
...(live && config.modelCatalog && config.modelCatalog.enabled ? { onRoute: (info) => live.route(info) } : {}) });
...(live && config.modelCatalog && config.modelCatalog.enabled ? { onRoute: (info) => live.route(info) } : {}) });
+ if (created.created === false) res.setHeader('x-openvibe-replayed', '1');
if (live) live.open(created.run);
if (live) live.open(created.run);
run = await runs.explained(await runs.wait(created, Math.max(config.runs.maxWaitMs, 120000)));
run = await runs.explained(await runs.wait(created, Math.max(config.runs.maxWaitMs, 120000)));
} catch (err) {
} catch (err) {
@@ -201,7 +201,7 @@ function openaiRouter({ runs, auth, config, registry, presets = null, receipts =
@@ -201,7 +201,7 @@ function openaiRouter({ runs, auth, config, registry, presets = null, receipts =
if (maxTokens != null) input.max_tokens = Math.max(1, Math.min(Number(maxTokens) || 1, 16000));
if (maxTokens != null) input.max_tokens = Math.max(1, Math.min(Number(maxTokens) || 1, 16000));
// AI Exchange F: the caller's liability cap for the Balance hold (an OpenVibe extension, like x_openvibe).
// AI Exchange F: the caller's liability cap for the Balance hold (an OpenVibe extension, like x_openvibe).
if (b.max_cost_usd != null) input.max_cost_usd = b.max_cost_usd;
if (b.max_cost_usd != null) input.max_cost_usd = b.max_cost_usd;
- let body = { workflow: 'ai.complete', input };
+ let body = { workflow: 'ai.complete', input, idempotency_key: req.get('idempotency-key') || undefined };
// The preset's rules ride the run into the router.
// The preset's rules ride the run into the router.
if (preset) body = { ...body, preset: { name: preset.name, scope: preset.scope, policy: preset.policy } };
if (preset) body = { ...body, preset: { name: preset.name, scope: preset.scope, policy: preset.policy } };
if (preset && preset.policy.adult && b.policy == null) body = { ...body, policy: 'uncensored' };
if (preset && preset.policy.adult && b.policy == null) body = { ...body, policy: 'uncensored' };
@@ -238,6 +238,7 @@ function openaiRouter({ runs, auth, config, registry, presets = null, receipts =
@@ -238,6 +238,7 @@ function openaiRouter({ runs, auth, config, registry, presets = null, receipts =
const created = await runs.create(body, req.principal, { trace: req.ov && req.ov.traceId, requestId: req.ov && req.ov.requestId, abuse: req.abuse,
const created = await runs.create(body, req.principal, { trace: req.ov && req.ov.traceId, requestId: req.ov && req.ov.requestId, abuse: req.abuse,
...(live ? { onDelta: live.delta } : {}),
...(live ? { onDelta: live.delta } : {}),
...(live && config.modelCatalog && config.modelCatalog.enabled ? { onRoute: (info) => live.route(info) } : {}) });
...(live && config.modelCatalog && config.modelCatalog.enabled ? { onRoute: (info) => live.route(info) } : {}) });
+ if (created.created === false) res.setHeader('x-openvibe-replayed', '1');
if (live) live.open(created.run);
if (live) live.open(created.run);
run = await runs.explained(await runs.wait(created, Math.max(config.runs.maxWaitMs, 120000)));
run = await runs.explained(await runs.wait(created, Math.max(config.runs.maxWaitMs, 120000)));
} catch (err) {
} catch (err) {
new file mode 100644
new file mode 100644
@@ -0,0 +1,107 @@
@@ -0,0 +1,107 @@
+'use strict';
+const express = require('express');
+const { CAPS } = require('../auth');
+const { policyFor, select } = require('../exchange/compat-routing');
+const { canonicalOf } = require('../exchange/catalog');
+const { sendError } = require('../util');
+function routingPreview({ auth, registry, log }) {
+ const r = express.Router();
+ r.post(
+ '/v1/routing/preview',
+ auth.appOrCap(CAPS.appRun, CAPS.runCreate),
+ async (req, res) => {
+ try {
+ const { model, variant, policy } = policyFor(req.body || {});
+ if (variant === 'online')
+ return res
+ .status(400)
+ .json({
+ error: {
+ code: 'input.invalid',
+ message:
+ 'Grounding is not part of a routing preview',
+ },
+ });
+ const providers = new Map(
+ (await registry.listProviders())
+ .filter(
+ (p) =>
+ p.status === 'active' &&
+ p.kind !== 'stub' &&
+ p.metadata?.hidden !== true &&
+ p.metadata?.synthetic !== true &&
+ !['CUSTOMER_BYOK', 'RESTRICTED'].includes(
+ p.provider_access,
+ ),
+ )
+ .map((p) => [p.key, p]),
+ );
+ const rows = (await registry.listModels()).filter(
+ (m) =>
+ m.status === 'active' && providers.has(m.provider_key),
+ ).filter(
+ (m) => m.metadata?.hidden !== true && m.metadata?.synthetic !== true &&
+ m.metadata?.restricted !== true && m.metadata?.byok !== true &&
+ !['CUSTOMER_BYOK', 'RESTRICTED'].includes(m.provider_access || m.metadata?.provider_access),
+ );
+ const candidates = rows
+ .filter(
+ (m) =>
+ model === 'openvibe/auto' ||
+ canonicalOf(m, providers.get(m.provider_key)) ===
+ model ||
+ m.metadata?.id === model,
+ )
+ .map((m) => ({
+ provider: m.provider_key,
+ model: m.model_key,
+ row: m,
+ }));
+ const answer = await select(candidates, policy, async (c) => {
+ const p = providers.get(c.provider),
+ m = c.row;
+ const privacy = p.metadata?.data_policy || {};
+ return {
+ priceIn: m.cost?.in_per_mtok,
+ priceOut: m.cost?.out_per_mtok,
+ training: privacy.training,
+ retention: privacy.retention,
+ };
+ });
+ // No prompt, credential, balance hold or upstream request is needed for this diagnostic.
+ return res
+ .set('Cache-Control', 'no-store')
+ .json({
+ executable: false,
+ notice: 'Catalogue-only preview; live availability, account gates and budgets are checked at execution. Quality and speed are excluded when no measured evidence is supplied.',
+ candidates: answer.allowed.map((c) => ({
+ provider: c.provider,
+ model: c.model,
+ })),
+ excluded: answer.excluded,
+ });
+ } catch (err) {
+ return sendError(res, err, req.ov, log);
+ }
+ },
+ );
+ r.post(
+ '/v1/presets/import/preview',
+ auth.appOrCap(CAPS.appRun, CAPS.runCreate),
+ (req, res) => {
+ try {
+ return res
+ .set('Cache-Control', 'no-store')
+ .json(
+ require('../exchange/import-preset').importPreset(
+ req.body,
+ ),
+ );
+ } catch (err) {
+ return sendError(res, err, req.ov, log);
+ }
+ },
+ );
+ return r;
+}
+module.exports = { routingPreview };
@@ -217,6 +217,7 @@ function createApp({ config, db, registry, pool, quotas, cache, runs, auth, keys
@@ -217,6 +217,7 @@ function createApp({ config, db, registry, pool, quotas, cache, runs, auth, keys
app.use(runsRouter({ runs, registry, auth, config, log }));
app.use(runsRouter({ runs, registry, auth, config, log }));
app.use(actionsRouter({ runs, registry, auth, log }));
app.use(actionsRouter({ runs, registry, auth, log }));
// The OpenAI-compatible endpoint (POST /v1/chat/completions, GET /v1/models): runs of ai.complete.
// The OpenAI-compatible endpoint (POST /v1/chat/completions, GET /v1/models): runs of ai.complete.
+ app.use(require('./api/routing-preview').routingPreview({ auth, registry, log }));
app.use(require('./api/openai').openaiRouter({ runs, auth, config, registry, presets, receipts, log }));
app.use(require('./api/openai').openaiRouter({ runs, auth, config, registry, presets, receipts, log }));
// The Anthropic-compatible endpoint (POST /v1/messages, GET /v1/models): runs of ai.messages (Codes C3).
// The Anthropic-compatible endpoint (POST /v1/messages, GET /v1/models): runs of ai.messages (Codes C3).
app.use(require('./api/messages').messagesRouter({ runs, auth, config, registry, presets, receipts, log }));
app.use(require('./api/messages').messagesRouter({ runs, auth, config, registry, presets, receipts, log }));
new file mode 100644
new file mode 100644
@@ -0,0 +1,158 @@
@@ -0,0 +1,158 @@
+'use strict';
+const { AiError } = require('../util');
+const sorts = ['price', 'throughput', 'latency', 'quality', 'value'];
+const fields = [
+ 'order',
+ 'only',
+ 'ignore',
+ 'allow_fallbacks',
+ 'sort',
+ 'max_price',
+ 'data_collection',
+ 'zdr',
+];
+const bad = (message) => {
+ throw new AiError(400, 'input.invalid', message);
+};
+function normalize(provider = {}) {
+ if (!provider || typeof provider !== 'object' || Array.isArray(provider))
+ bad('provider must be an object');
+ for (const key of Object.keys(provider))
+ if (!fields.includes(key))
+ bad('Unsupported provider parameter');
+ const out = {};
+ for (const key of ['order', 'only', 'ignore'])
+ if (provider[key] !== undefined) {
+ if (
+ !Array.isArray(provider[key]) ||
+ provider[key].length > 64 ||
+ provider[key].some(
+ (x) =>
+ typeof x !== 'string' ||
+ !/^[a-zA-Z0-9._-]{1,100}$/.test(x),
+ )
+ )
+ bad(`provider.${key} must be a list of provider ids`);
+ out[key] = [...new Set(provider[key])];
+ }
+ for (const key of ['allow_fallbacks', 'zdr'])
+ if (provider[key] !== undefined) {
+ if (typeof provider[key] !== 'boolean')
+ bad(`provider.${key} must be boolean`);
+ out[key] = provider[key];
+ }
+ if (provider.sort !== undefined) {
+ if (!sorts.includes(provider.sort)) bad('Unknown provider.sort');
+ out.sort = provider.sort;
+ }
+ if (provider.data_collection !== undefined) {
+ if (!['allow', 'deny'].includes(provider.data_collection))
+ bad('Unknown data_collection policy');
+ out.data_collection = provider.data_collection;
+ }
+ if (provider.max_price !== undefined) {
+ const p = provider.max_price;
+ if (
+ !p ||
+ typeof p !== 'object' ||
+ Array.isArray(p) ||
+ Object.keys(p).some((k) => !['prompt', 'completion'].includes(k))
+ )
+ bad(
+ 'max_price accepts prompt and completion in USD per million tokens',
+ );
+ out.max_price = {};
+ for (const [k, v] of Object.entries(p)) {
+ if (typeof v !== 'number' || !Number.isFinite(v) || v < 0)
+ bad('Prices must be finite nonnegative numbers');
+ out.max_price[k] = v;
+ }
+ }
+ return out;
+}
+function modelName(value) {
+ if (typeof value !== 'string' || !value || value.length > 256)
+ bad('model must be a model id');
+ const m = /:(free|floor|nitro|online)$/.exec(value);
+ return {
+ model: m ? value.slice(0, -m[0].length) : value,
+ variant: m?.[1] || null,
+ };
+}
+function policyFor(body) {
+ const { model, variant } = modelName(body.model || 'openvibe/auto');
+ const policy = normalize(body.provider);
+ if (variant === 'free') policy.max_price = { prompt: 0, completion: 0 };
+ if (variant === 'floor') policy.sort = 'price';
+ if (variant === 'nitro') policy.sort = 'throughput';
+ return { model, variant, policy };
+}
+const finite = (x) => typeof x === 'number' && Number.isFinite(x);
+async function select(list, policy, facts) {
+ const ranked = [],
+ excluded = [];
+ for (const [index, candidate] of list.entries()) {
+ const f = await facts(candidate),
+ id = candidate.provider;
+ let reason = null;
+ if (policy.only && !policy.only.includes(id)) reason = 'provider.only';
+ if (policy.ignore?.includes(id)) reason = 'provider.ignore';
+ if (policy.data_collection === 'deny' && f.training !== false)
+ reason = 'provider.data_collection';
+ if (policy.zdr && f.retention !== 'none') reason = 'provider.zdr';
+ for (const [key, fact] of [
+ ['prompt', 'priceIn'],
+ ['completion', 'priceOut'],
+ ])
+ if (
+ policy.max_price?.[key] !== undefined &&
+ (!finite(f[fact]) || f[fact] > policy.max_price[key])
+ )
+ reason = 'provider.max_price';
+ const cost =
+ finite(f.priceIn) && finite(f.priceOut)
+ ? f.priceIn + f.priceOut
+ : null;
+ let score = 0;
+ if (policy.sort === 'price') score = cost ?? Infinity;
+ if (policy.sort === 'latency') {
+ if (!finite(f.latency)) reason = 'provider.latency_unavailable';
+ score = finite(f.latency) ? f.latency : Infinity;
+ }
+ if (policy.sort === 'throughput') {
+ if (!finite(f.throughput))
+ reason = 'provider.throughput_unavailable';
+ score = finite(f.throughput) ? -f.throughput : Infinity;
+ }
+ if (policy.sort === 'quality' || policy.sort === 'value') {
+ if (!finite(f.quality)) reason = 'provider.rating_unavailable';
+ score =
+ policy.sort === 'quality'
+ ? -f.quality
+ : cost === null
+ ? Infinity
+ : -(f.quality / Math.max(cost, 0.000001));
+ }
+ if (reason)
+ excluded.push({ provider: id, model: candidate.model, reason });
+ else
+ ranked.push({
+ candidate,
+ score,
+ index,
+ order: policy.order?.includes(id)
+ ? policy.order.indexOf(id)
+ : Infinity,
+ });
+ }
+ ranked.sort(
+ (a, b) => a.order - b.order || a.score - b.score || a.index - b.index,
+ );
+ const allowed = ranked.map((x) => x.candidate);
+ return {
+ allowed:
+ policy.allow_fallbacks === false ? allowed.slice(0, 1) : allowed,
+ excluded,
+ };
+}
+module.exports = { normalize, policyFor, modelName, select };
new file mode 100644
new file mode 100644
@@ -0,0 +1,104 @@
@@ -0,0 +1,104 @@
+'use strict';
+const { normalize, modelName } = require('./compat-routing');
+const { AiError } = require('../util');
+const accepted = [
+ 'name',
+ 'model',
+ 'models',
+ 'system',
+ 'provider',
+ 'temperature',
+ 'top_p',
+ 'max_tokens',
+ 'seed',
+ 'response_format',
+];
+// Import preview is intentionally pure: no keys, URLs, provider calls or writes.
+function importPreset(source) {
+ const bad = (detail) => {
+ throw new AiError(400, 'input.invalid', detail);
+ };
+ if (!source || typeof source !== 'object' || Array.isArray(source))
+ bad('A preset must be a JSON object');
+ const unsupported = Object.keys(source).filter(
+ (k) => !accepted.includes(k),
+ );
+ if (unsupported.length)
+ bad('Preset contains unsupported fields; remove them before importing');
+ if (
+ typeof source.name !== 'string' ||
+ !/^[a-z0-9][a-z0-9_-]{0,63}$/.test(source.name)
+ )
+ bad('Preset name must be a lowercase slug');
+ const out = { name: source.name };
+ if (source.model !== undefined) {
+ modelName(source.model);
+ if (source.model.startsWith('@preset/'))
+ bad('Nested presets are not supported');
+ out.model = source.model;
+ }
+ if (source.models !== undefined) {
+ if (
+ !Array.isArray(source.models) ||
+ source.models.length < 1 ||
+ source.models.length > 8
+ )
+ bad('models must contain one to eight model ids');
+ source.models.forEach((x) => {
+ modelName(x);
+ if (x.startsWith('@preset/'))
+ bad('Nested presets are not supported');
+ });
+ out.models = [...new Set(source.models)];
+ }
+ if (!out.model && !out.models)
+ bad('A preset must name a model or fallbacks');
+ if (source.provider !== undefined)
+ out.provider = normalize(source.provider);
+ if (source.system !== undefined) {
+ if (typeof source.system !== 'string' || source.system.length > 32000)
+ bad('system must be at most 32000 characters');
+ out.system = source.system;
+ }
+ for (const [k, max] of [
+ ['temperature', 2],
+ ['top_p', 1],
+ ['max_tokens', 64000],
+ ])
+ if (source[k] !== undefined) {
+ if (
+ !Number.isFinite(source[k]) ||
+ source[k] < 0 ||
+ source[k] > max ||
+ (k === 'max_tokens' &&
+ (!Number.isInteger(source[k]) || source[k] < 1))
+ )
+ bad('Invalid sampling parameter');
+ out[k] = source[k];
+ }
+ if (source.seed !== undefined) {
+ if (!Number.isSafeInteger(source.seed))
+ bad('seed must be a safe integer');
+ out.seed = source.seed;
+ }
+ if (source.response_format !== undefined) {
+ if (
+ !source.response_format ||
+ typeof source.response_format !== 'object' ||
+ Array.isArray(source.response_format)
+ )
+ bad('response_format must be an object');
+ out.response_format = JSON.parse(
+ JSON.stringify(source.response_format),
+ );
+ }
+ return {
+ valid: true,
+ fields: Object.keys(out),
+ model_count: out.models ? out.models.length : 1,
+ warnings: [
+ 'Preview only: this does not save a preset or make a provider request.',
+ ],
+ };
+}
+module.exports = { importPreset };
@@ -475,6 +475,7 @@ const exchangeKey = {
@@ -475,6 +475,7 @@ const exchangeKey = {
id: str({ pattern: '^xkey_' }), name: str({ maxLength: 80 }), prefix: str({ description: 'The first ten characters of the secret (ovx_…), to tell keys apart' }),
id: str({ pattern: '^xkey_' }), name: str({ maxLength: 80 }), prefix: str({ description: 'The first ten characters of the secret (ovx_…), to tell keys apart' }),
monthly_cap_usd: { type: 'number', minimum: 0 }, allowed_models: arrayOf(str()), created_at: str({ format: 'date-time' }),
monthly_cap_usd: { type: 'number', minimum: 0 }, allowed_models: arrayOf(str()), created_at: str({ format: 'date-time' }),
revoked_at: nullable('string', { format: 'date-time' }),
revoked_at: nullable('string', { format: 'date-time' }),
+ expires_at: nullable('string', { format: 'date-time' }),
},
},
};
};
route('get', '/exchange/keys', {
route('get', '/exchange/keys', {
@@ -487,6 +488,7 @@ route('post', '/exchange/keys', {
@@ -487,6 +488,7 @@ route('post', '/exchange/keys', {
description: 'Only a person. The secret (ovx_…) is in this answer only; it is stored as a digest. A run with the key is refused once the month\'s cap is spent, and for a model not in allowed_models.',
description: 'Only a person. The secret (ovx_…) is in this answer only; it is stored as a digest. A run with the key is refused once the month\'s cap is spent, and for a model not in allowed_models.',
requestBody: body({ type: 'object', required: ['name', 'monthly_cap_usd', 'allowed_models'], properties: {
requestBody: body({ type: 'object', required: ['name', 'monthly_cap_usd', 'allowed_models'], properties: {
name: str({ minLength: 1, maxLength: 80 }), monthly_cap_usd: { type: 'number', minimum: 0 }, allowed_models: arrayOf(str({ maxLength: 200 })),
name: str({ minLength: 1, maxLength: 80 }), monthly_cap_usd: { type: 'number', minimum: 0 }, allowed_models: arrayOf(str({ maxLength: 200 })),
+ expires_at: nullable('string', { format: 'date-time', description: 'Optional future expiry; checked at authentication and reservation, before a hold' }),
} }),
} }),
responses: { 201: json('The key and its secret, shown once', { type: 'object', required: ['key', 'secret'], properties: { key: exchangeKey, secret: str({ pattern: '^ovx_' }) } }), 422: problem, ...errors([401, 403]) },
responses: { 201: json('The key and its secret, shown once', { type: 'object', required: ['key', 'secret'], properties: { key: exchangeKey, secret: str({ pattern: '^ovx_' }) } }), 422: problem, ...errors([401, 403]) },
});
});
new file mode 100644
new file mode 100644
@@ -0,0 +1,43 @@
@@ -0,0 +1,43 @@
+'use strict';
+const { AiError, sha256 } = require('../util');
+
+// A database uniqueness constraint, not a process-local lock: concurrent callers and
+// restarts cannot take two key holds for the same owner's logical request.
+async function claimRequest(db, owner, idempotencyKey, body) {
+ if (!/^[A-Za-z0-9._:-]{8,128}$/.test(idempotencyKey))
+ throw new AiError(422, 'input.invalid', 'Idempotency-Key must be 8-128 of [A-Za-z0-9._:-]');
+ const fingerprint = sha256(body);
+ const claimed = await db.prepare(
+ 'INSERT INTO exchange_key_requests(owner,idempotency_key,request_hash) VALUES(?,?,?) ON CONFLICT DO NOTHING RETURNING owner',
+ ).get(owner, idempotencyKey, fingerprint);
+ const existingRun = () => db.prepare(
+ "SELECT id FROM runs WHERE requester_type = 'user' AND requester_id = ? AND idempotency_key = ?",
+ ).get(owner, idempotencyKey);
+ if (!claimed) {
+ const previous = await db.prepare(
+ 'SELECT request_hash,max_cost_usd FROM exchange_key_requests WHERE owner = ? AND idempotency_key = ?',
+ ).get(owner, idempotencyKey);
+ if (!previous || previous.request_hash !== fingerprint)
+ throw new AiError(409, 'idempotency.conflict', 'Idempotency-Key was used with different input');
+ const run = await existingRun();
+ if (!run)
+ throw new AiError(409, 'idempotency.in_progress', 'The original request is still being admitted; retry shortly');
+ // runs.create verifies the input again and returns this owner's stored run.
+ // Retain the original ceiling even if the key's remaining budget changed.
+ return { replay: true, maxCost: previous.max_cost_usd === null ? null : Number(previous.max_cost_usd) };
+ }
+ // The same person may first have used their person token rather than an opaque
+ // key. Their existing run is still a replay, with its original request ceiling.
+ if (await existingRun()) return { replay: true, maxCost: null };
+ return {
+ replay: false,
+ admitted: (amount) => db.prepare(
+ 'UPDATE exchange_key_requests SET max_cost_usd = ? WHERE owner = ? AND idempotency_key = ?',
+ ).run(amount, owner, idempotencyKey),
+ abandon: () => db.prepare(
+ `DELETE FROM exchange_key_requests WHERE owner = ? AND idempotency_key = ?
+ AND NOT EXISTS (SELECT 1 FROM runs WHERE requester_type = 'user' AND requester_id = ? AND idempotency_key = ?)`,
+ ).run(owner, idempotencyKey, owner, idempotencyKey),
+ };
+}
+module.exports = { claimRequest };
@@ -14,6 +14,7 @@ function createKeys({ db, clock = { now: () => Date.now() } }) {
@@ -14,6 +14,7 @@ function createKeys({ db, clock = { now: () => Date.now() } }) {
allowed_models: JSON.parse(r.allowed_models),
allowed_models: JSON.parse(r.allowed_models),
created_at: r.created_at,
created_at: r.created_at,
revoked_at: r.revoked_at,
revoked_at: r.revoked_at,
+ expires_at: r.expires_at || null,
});
});
async function list(owner) {
async function list(owner) {
return (
return (
@@ -42,11 +43,13 @@ function createKeys({ db, clock = { now: () => Date.now() } }) {
@@ -42,11 +43,13 @@ function createKeys({ db, clock = { now: () => Date.now() } }) {
'ai.invalid',
'ai.invalid',
'A name, nonnegative monthly cap and allowed model list are required',
'A name, nonnegative monthly cap and allowed model list are required',
);
);
+ const expires = b.expires_at == null ? null : Date.parse(b.expires_at);
+ if (expires !== null && (!Number.isFinite(expires) || expires <= clock.now())) throw new AiError(422, 'ai.invalid', 'expires_at must be a future timestamp');
const secret = 'ovx_' + randomBytes(32).toString('base64url');
const secret = 'ovx_' + randomBytes(32).toString('base64url');
const id = 'xkey_' + randomUUID();
const id = 'xkey_' + randomUUID();
await db
await db
.prepare(
.prepare(
- 'INSERT INTO exchange_api_keys(id,owner,name,digest,prefix,monthly_cap_usd,allowed_models,created_at) VALUES(?,?,?,?,?,?,?,?)',
+ 'INSERT INTO exchange_api_keys(id,owner,name,digest,prefix,monthly_cap_usd,allowed_models,created_at,expires_at) VALUES(?,?,?,?,?,?,?,?,?)',
)
)
.run(
.run(
id,
id,
@@ -57,6 +60,7 @@ function createKeys({ db, clock = { now: () => Date.now() } }) {
@@ -57,6 +60,7 @@ function createKeys({ db, clock = { now: () => Date.now() } }) {
b.monthly_cap_usd,
b.monthly_cap_usd,
JSON.stringify(b.allowed_models),
JSON.stringify(b.allowed_models),
iso(),
iso(),
+ expires === null ? null : new Date(expires).toISOString(),
);
);
return { key: (await list(owner)).find((k) => k.id === id), secret };
return { key: (await list(owner)).find((k) => k.id === id), secret };
}
}
@@ -73,9 +77,9 @@ function createKeys({ db, clock = { now: () => Date.now() } }) {
@@ -73,9 +77,9 @@ function createKeys({ db, clock = { now: () => Date.now() } }) {
if (!/^ovx_[A-Za-z0-9_-]{43}$/.test(secret)) return null;
if (!/^ovx_[A-Za-z0-9_-]{43}$/.test(secret)) return null;
return db
return db
.prepare(
.prepare(
- 'SELECT * FROM exchange_api_keys WHERE digest = ? AND revoked_at IS NULL',
+ 'SELECT * FROM exchange_api_keys WHERE digest = ? AND revoked_at IS NULL AND (expires_at IS NULL OR expires_at > ?)',
)
)
- .get(hash(secret));
+ .get(hash(secret), iso());
}
}
/**
/**
* A hold for one call against the key's monthly cap: the call's own ceiling (`limit`, its max_cost_usd) or
* A hold for one call against the key's monthly cap: the call's own ceiling (`limit`, its max_cost_usd) or
@@ -84,6 +88,11 @@ function createKeys({ db, clock = { now: () => Date.now() } }) {
@@ -84,6 +88,11 @@ function createKeys({ db, clock = { now: () => Date.now() } }) {
* hung up) and gives the rest of the hold back. Search calls (`free`) take no hold and skip the model list.
* hung up) and gives the rest of the hold back. Search calls (`free`) take no hold and skip the model list.
*/
*/
async function reserve(key, model, free = false, limit = null) {
async function reserve(key, model, free = false, limit = null) {
+ // Re-read the credential: a previously resolved key may have expired or been revoked.
+ const active = await db.prepare(
+ 'SELECT id FROM exchange_api_keys WHERE id = ? AND revoked_at IS NULL AND (expires_at IS NULL OR expires_at > ?)',
+ ).get(key.id, iso());
+ if (!active) throw new AiError(401, 'token.invalid', 'Invalid, expired or revoked Exchange key');
const allowed = JSON.parse(key.allowed_models);
const allowed = JSON.parse(key.allowed_models);
if (!free && allowed.length && !allowed.includes(model))
if (!free && allowed.length && !allowed.includes(model))
throw new AiError(403, 'exchange.model_denied', 'This model is not allowed by this key');
throw new AiError(403, 'exchange.model_denied', 'This model is not allowed by this key');
@@ -108,10 +117,11 @@ function createKeys({ db, clock = { now: () => Date.now() } }) {
@@ -108,10 +117,11 @@ function createKeys({ db, clock = { now: () => Date.now() } }) {
)
)
UPDATE exchange_key_months x SET reserved = x.reserved + m.take FROM m
UPDATE exchange_key_months x SET reserved = x.reserved + m.take FROM m
WHERE x.key_id = m.key_id AND x.month = m.month
WHERE x.key_id = m.key_id AND x.month = m.month
- AND EXISTS (SELECT 1 FROM exchange_api_keys k WHERE k.id = x.key_id AND k.revoked_at IS NULL)
+ AND EXISTS (SELECT 1 FROM exchange_api_keys k WHERE k.id = x.key_id AND k.revoked_at IS NULL
+ AND (k.expires_at IS NULL OR k.expires_at::timestamptz > GREATEST(?::timestamptz, clock_timestamp())))
RETURNING m.take AS take`,
RETURNING m.take AS take`,
)
)
- .get(want, key.monthly_cap_usd, key.id, month);
+ .get(want, key.monthly_cap_usd, key.id, month, iso());
if (!row)
if (!row)
throw new AiError(429, 'exchange.key_cap', "This key's monthly cap is spent");
throw new AiError(429, 'exchange.key_cap', "This key's monthly cap is spent");
amount = Number(row.take);
amount = Number(row.take);
@@ -3,6 +3,7 @@ const express = require('express');
@@ -3,6 +3,7 @@ const express = require('express');
const { CAPS } = require('../auth');
const { CAPS } = require('../auth');
const { AiError, sendError } = require('../util');
const { AiError, sendError } = require('../util');
const { createKeys } = require('./keys');
const { createKeys } = require('./keys');
+const { claimRequest } = require('./idempotency');
const { createSearch } = require('../search');
const { createSearch } = require('../search');
function platform({ db, auth, clock, log, searchFetch }) {
function platform({ db, auth, clock, log, searchFetch }) {
const keys = createKeys({ db, clock });
const keys = createKeys({ db, clock });
@@ -92,6 +93,7 @@ function platform({ db, auth, clock, log, searchFetch }) {
@@ -92,6 +93,7 @@ function platform({ db, auth, clock, log, searchFetch }) {
'',
'',
);
);
if (!token.startsWith('ovx_')) return next();
if (!token.startsWith('ovx_')) return next();
+ let claim = null;
try {
try {
const isSearch =
const isSearch =
req.method === 'POST' &&
req.method === 'POST' &&
@@ -115,9 +117,14 @@ function platform({ db, auth, clock, log, searchFetch }) {
@@ -115,9 +117,14 @@ function platform({ db, auth, clock, log, searchFetch }) {
throw new AiError(
throw new AiError(
401,
401,
'token.invalid',
'token.invalid',
- 'Invalid or revoked Exchange key',
+ 'Invalid, expired or revoked Exchange key',
);
);
- const hold = models
+ const allowed = JSON.parse(key.allowed_models);
+ if (inference && allowed.length && !allowed.includes(req.body?.model))
+ throw new AiError(403, 'exchange.model_denied', 'This model is not allowed by this key');
+ if (inference && req.get('idempotency-key'))
+ claim = await claimRequest(db, key.owner, req.get('idempotency-key'), req.body);
+ const hold = models || claim?.replay
? null
? null
: await keys.reserve(
: await keys.reserve(
key,
key,
@@ -127,13 +134,17 @@ function platform({ db, auth, clock, log, searchFetch }) {
@@ -127,13 +134,17 @@ function platform({ db, auth, clock, log, searchFetch }) {
isSearch,
isSearch,
Number.isFinite(req.body?.max_cost_usd) ? req.body.max_cost_usd : null,
Number.isFinite(req.body?.max_cost_usd) ? req.body.max_cost_usd : null,
);
);
- if (inference) {
- req.body.max_cost_usd = Math.min(
+ if (inference && !(claim?.replay && claim.maxCost === null)) {
+ req.body.max_cost_usd = claim?.replay ? claim.maxCost : Math.min(
hold.amount,
hold.amount,
Number.isFinite(req.body.max_cost_usd)
Number.isFinite(req.body.max_cost_usd)
? req.body.max_cost_usd
? req.body.max_cost_usd
: hold.amount,
: hold.amount,
);
);
+ if (claim && !claim.replay) {
+ try { await claim.admitted(req.body.max_cost_usd); }
+ catch (err) { await hold.settle({ cost_usd: 0 }); throw err; }
+ }
}
}
req.exchangePrincipal = {
req.exchangePrincipal = {
kind: 'user',
kind: 'user',
@@ -172,14 +183,16 @@ function platform({ db, auth, clock, log, searchFetch }) {
@@ -172,14 +183,16 @@ function platform({ db, auth, clock, log, searchFetch }) {
}
}
const usage = result?.usage || {};
const usage = result?.usage || {};
const cost =
const cost =
- isSearch && res.statusCode < 400
+ (isSearch || res.getHeader('x-openvibe-replayed') === '1') && res.statusCode < 400
? 0
? 0
: result?.x_openvibe?.cost_usd;
: result?.x_openvibe?.cost_usd;
+ // An early disconnect can precede run insertion. Keep its claim
+ // until a response finishes so a retry cannot take another hold.
hold.settle({
hold.settle({
cost_usd: cost,
cost_usd: cost,
tokens_in: usage.prompt_tokens || 0,
tokens_in: usage.prompt_tokens || 0,
tokens_out: usage.completion_tokens || 0,
tokens_out: usage.completion_tokens || 0,
- }).catch(() =>
+ }).then(() => res.writableFinished ? claim?.abandon?.() : undefined).catch(() =>
log.error('Exchange usage settlement failed'),
log.error('Exchange usage settlement failed'),
);
);
};
};
@@ -188,6 +201,7 @@ function platform({ db, auth, clock, log, searchFetch }) {
@@ -188,6 +201,7 @@ function platform({ db, auth, clock, log, searchFetch }) {
}
}
next();
next();
} catch (e) {
} catch (e) {
+ if (claim && !claim.replay) await claim.abandon().catch(() => log.error('Exchange request cleanup failed'));
sendError(res, e, req.ov, log);
sendError(res, e, req.ov, log);
}
}
}
}
@@ -51,6 +51,7 @@ t.test('an export carries the person\'s sessions and credential hints, and a del
@@ -51,6 +51,7 @@ t.test('an export carries the person\'s sessions and credential hints, and a del
const h = await boot({ env: { AI_EVENTS_SECRET: SECRET, AI_EVENTS_SUBSCRIBE: '0', OV_NETWORK_INTERNAL_URL: stub.url, OV_OAUTH_CLIENT_SECRET: 'ai-secret' } });
const h = await boot({ env: { AI_EVENTS_SECRET: SECRET, AI_EVENTS_SUBSCRIBE: '0', OV_NETWORK_INTERNAL_URL: stub.url, OV_OAUTH_CLIENT_SECRET: 'ai-secret' } });
try {
try {
for (const [subject, name] of [[A, 'ada'], [B, 'bo']]) {
for (const [subject, name] of [[A, 'ada'], [B, 'bo']]) {
+ await h.db.exec('INSERT INTO exchange_key_requests(owner,idempotency_key,request_hash,max_cost_usd) VALUES ($1,$2,$3,1)', [subject, `request-${name}`, `hash-${name}`]);
await h.db.exec("INSERT INTO console_sessions (id_hash, subject, username, staff, csrf, created_at, expires_at) VALUES ($1, $2, $3, '{}', 'c', '2026-10-01', '2027-10-01')", [`h_${name}`, subject, name]);
await h.db.exec("INSERT INTO console_sessions (id_hash, subject, username, staff, csrf, created_at, expires_at) VALUES ($1, $2, $3, '{}', 'c', '2026-10-01', '2027-10-01')", [`h_${name}`, subject, name]);
await h.db.exec("INSERT INTO person_sessions (id_hash, subject, username, created_at, expires_at) VALUES ($1, $2, $3, '2026-10-01', '2027-10-01')", [`p_${name}`, subject, name]);
await h.db.exec("INSERT INTO person_sessions (id_hash, subject, username, created_at, expires_at) VALUES ($1, $2, $3, '2026-10-01', '2027-10-01')", [`p_${name}`, subject, name]);
await h.db.exec("INSERT INTO subject_credentials (owner, subject, provider, key_enc, key_hint, models, created_at, updated_at) VALUES ('proj_1', $1, 'openai', 'v1:secret', $2, '{}', '2026-10-01', '2026-10-02')", [subject, `hint-${name}`]);
await h.db.exec("INSERT INTO subject_credentials (owner, subject, provider, key_enc, key_hint, models, created_at, updated_at) VALUES ('proj_1', $1, 'openai', 'v1:secret', $2, '{}', '2026-10-01', '2026-10-02')", [subject, `hint-${name}`]);
@@ -82,10 +83,13 @@ t.test('an export carries the person\'s sessions and credential hints, and a del
@@ -82,10 +83,13 @@ t.test('an export carries the person\'s sessions and credential hints, and a del
assert.ok(!JSON.stringify(part.body).includes('v1:cookie-secret'), 'the encrypted session cookie is never exported');
assert.ok(!JSON.stringify(part.body).includes('v1:cookie-secret'), 'the encrypted session cookie is never exported');
assert.ok(!JSON.stringify(part.body).includes('v1:secret'), 'the encrypted key is never exported');
assert.ok(!JSON.stringify(part.body).includes('v1:secret'), 'the encrypted key is never exported');
assert.ok(!JSON.stringify(part.body).includes(B), 'nobody else\'s rows');
assert.ok(!JSON.stringify(part.body).includes(B), 'nobody else\'s rows');
+ assert.deepStrictEqual(part.body.files.find((f) => f.name === 'exchange-key-requests.json').content, [{ idempotency_key: 'request-ada', request_hash: 'hash-ada', max_cost_usd: 1 }]);
const d = await deliver(ev('evt_01JZ0000000000000000000D01', 'network.account.deleted', { deletion_id: DEL, subject: A, aliases: [] }));
const d = await deliver(ev('evt_01JZ0000000000000000000D01', 'network.account.deleted', { deletion_id: DEL, subject: A, aliases: [] }));
assert.deepStrictEqual([d.status, d.json.outcome], [200, 'erased']);
assert.deepStrictEqual([d.status, d.json.outcome], [200, 'erased']);
assert.strictEqual(Number(await h.db.value('SELECT count(*)::int FROM console_sessions WHERE subject = $1', [A])), 0);
assert.strictEqual(Number(await h.db.value('SELECT count(*)::int FROM console_sessions WHERE subject = $1', [A])), 0);
+ assert.strictEqual(Number(await h.db.value('SELECT count(*)::int FROM exchange_key_requests WHERE owner = $1', [A])), 0, 'request claims are erased with their owner');
+ assert.strictEqual(Number(await h.db.value('SELECT count(*)::int FROM exchange_key_requests WHERE owner = $1', [B])), 1, 'another owner\'s claims remain');
assert.strictEqual(Number(await h.db.value('SELECT count(*)::int FROM abuse_devices WHERE subject = $1', [A])), 0, 'the device row goes');
assert.strictEqual(Number(await h.db.value('SELECT count(*)::int FROM abuse_devices WHERE subject = $1', [A])), 0, 'the device row goes');
assert.strictEqual(Number(await h.db.value('SELECT count(*)::int FROM abuse_daily_usage WHERE subject = $1', [`user:${A}`])), 0, 'the daily counter goes');
assert.strictEqual(Number(await h.db.value('SELECT count(*)::int FROM abuse_daily_usage WHERE subject = $1', [`user:${A}`])), 0, 'the daily counter goes');
assert.strictEqual(Number(await h.db.value('SELECT count(*)::int FROM abuse_devices WHERE subject = $1', [B])), 1, "nobody else's device row");
assert.strictEqual(Number(await h.db.value('SELECT count(*)::int FROM abuse_devices WHERE subject = $1', [B])), 1, "nobody else's device row");
new file mode 100644
new file mode 100644
@@ -0,0 +1,107 @@
@@ -0,0 +1,107 @@
+'use strict';
+const assert = require('node:assert/strict');
+const {
+ normalize,
+ policyFor,
+ select,
+} = require('../server/exchange/compat-routing');
+(async () => {
+ assert.throws(() => normalize({ unrecognized: true }), /Unsupported/);
+ assert.throws(() => normalize({ allow_fallbacks: 'false' }), /boolean/);
+ assert.throws(
+ () => normalize({ max_price: { prompt: -1 } }),
+ /nonnegative/,
+ );
+ assert.deepEqual(
+ policyFor({ model: 'maker/model:free' }).policy.max_price,
+ { prompt: 0, completion: 0 },
+ );
+ const list = [
+ { provider: 'a', model: 'm' },
+ { provider: 'b', model: 'm' },
+ { provider: 'c', model: 'm' },
+ ];
+ const facts = async (c) =>
+ ({
+ a: { priceIn: null, priceOut: null },
+ b: {
+ priceIn: 1,
+ priceOut: 2,
+ training: false,
+ retention: 'none',
+ quality: 80,
+ },
+ c: {
+ priceIn: 2,
+ priceOut: 4,
+ training: true,
+ retention: 'standard',
+ quality: 90,
+ },
+ })[c.provider];
+ assert.deepEqual(
+ (
+ await select(
+ list,
+ normalize({
+ max_price: { prompt: 1 },
+ data_collection: 'deny',
+ zdr: true,
+ }),
+ facts,
+ )
+ ).allowed,
+ [list[1]],
+ );
+ assert.deepEqual(
+ (
+ await select(
+ list,
+ normalize({
+ only: ['b', 'c'],
+ order: ['c'],
+ allow_fallbacks: false,
+ }),
+ facts,
+ )
+ ).allowed,
+ [list[2]],
+ );
+ assert.deepEqual(
+ (await select(list, normalize({ sort: 'value' }), facts)).allowed,
+ [list[1], list[2]],
+ );
+ assert.equal(
+ (await select(list, normalize({ sort: 'quality' }), async () => ({})))
+ .allowed.length,
+ 0,
+ );
+ console.log(
+ 'Strict routing fields, missing prices, privacy, provider order and evidence requirements passed',
+ );
+})().catch((e) => {
+ console.error(e);
+ process.exitCode = 1;
+});
+const { importPreset } = require('../server/exchange/import-preset');
+assert.equal(
+ importPreset({
+ name: 'writer',
+ model: 'maker/model',
+ provider: { only: ['a'] },
+ }).valid,
+ true,
+);
+assert.throws(
+ () =>
+ importPreset({ name: 'writer', model: 'm', api_key: 'not-a-real-key' }),
+ /unsupported/,
+);
+assert.throws(
+ () => importPreset({ name: 'writer', model: '@preset/recursive' }),
+ /Nested/,
+);
+const sensitive = 'private-content-do-not-reflect';
+const summary = importPreset({ name: sensitive, model: sensitive, system: sensitive, provider: { only: [sensitive] }, response_format: { [sensitive]: sensitive } });
+assert.equal(JSON.stringify(summary).includes(sensitive), false, 'validated values are never returned');
+assert.throws(() => normalize({ [sensitive]: sensitive }), (e) => !e.message.includes(sensitive));
new file mode 100644
new file mode 100644
@@ -0,0 +1,203 @@
@@ -0,0 +1,203 @@
+'use strict';
+const assert = require('node:assert/strict');
+const { boot, token, ALL } = require('./helpers');
+(async () => {
+ const h = await boot();
+ try {
+ const auth = 'Bearer ' + token('live', ALL);
+ const call = async (path, body, extra = {}) =>
+ fetch(h.base + path, {
+ method: 'POST',
+ headers: {
+ authorization: auth,
+ 'content-type': 'application/json',
+ ...extra,
+ },
+ body: JSON.stringify(body),
+ });
+ for (const path of ['/v1/routing/preview', '/v1/presets/import/preview']) assert.equal(
+ (
+ await fetch(h.base + path, {
+ method: 'POST',
+ headers: { 'content-type': 'application/json' },
+ body: '{}',
+ })
+ ).status,
+ 401,
+ );
+ const listProviders = h.registry.listProviders;
+ const listModels = h.registry.listModels;
+ const realFetch = global.fetch;
+ const createRun = h.runs.create;
+ let upstreamCalls = 0;
+ let modelCalls = 0;
+ global.fetch = (url, ...args) => {
+ if (String(url).startsWith(h.base + '/')) return realFetch(url, ...args);
+ upstreamCalls++;
+ throw new Error('Preview attempted an account or provider fetch');
+ };
+ h.runs.create = () => { modelCalls++; throw new Error('Preview attempted inference'); };
+ h.registry.listProviders = async () => [
+ { key: 'public-fixture', kind: 'openai', status: 'active', provider_access: 'OWN_DEPLOYMENT', metadata: {} },
+ { key: 'hidden-fixture', kind: 'openai', status: 'active', metadata: { hidden: true } },
+ { key: 'restricted-fixture', kind: 'openai', status: 'active', provider_access: 'RESTRICTED' },
+ { key: 'byok-fixture', kind: 'openai', status: 'active', provider_access: 'CUSTOMER_BYOK' },
+ ];
+ h.registry.listModels = async () => [
+ ...['public-fixture', 'hidden-fixture', 'restricted-fixture', 'byok-fixture'].map((provider_key) => ({ provider_key, model_key: provider_key + '-model', status: 'active', metadata: {}, cost: {} })),
+ ...[{ hidden: true }, { provider_access: 'RESTRICTED' }, { provider_access: 'CUSTOMER_BYOK' }].map((metadata, i) => ({ provider_key: 'public-fixture', model_key: 'private-model-' + i, status: 'active', metadata, cost: {} })),
+ ];
+ try {
+ const preview = await call('/v1/routing/preview', {
+ model: 'openvibe/auto',
+ provider: {},
+ });
+ assert.equal(preview.status, 200);
+ const visible = await preview.json();
+ assert.equal(visible.executable, false);
+ assert.deepEqual(visible.candidates, [{ provider: 'public-fixture', model: 'public-fixture-model' }]);
+ assert.deepEqual(visible.excluded, []);
+ const sensitive = 'submitted-private-marker';
+ for (const [path, input] of [
+ ['/v1/routing/preview', { model: sensitive, provider: { only: [sensitive] } }],
+ ['/v1/routing/preview', { provider: { [sensitive]: sensitive } }],
+ ['/v1/presets/import/preview', { name: sensitive, model: sensitive, system: sensitive, provider: { only: [sensitive] }, response_format: { [sensitive]: sensitive } }],
+ ['/v1/presets/import/preview', { name: 'test', model: sensitive, [sensitive]: sensitive }],
+ ]) {
+ const response = await call(path, input);
+ assert.ok([200, 400].includes(response.status));
+ assert.equal((await response.text()).includes(sensitive), false);
+ }
+ assert.equal(
+ (await call('/v1/routing/preview', { provider: { unknown: 'x' } }))
+ .status,
+ 400,
+ );
+ const imported = await call('/v1/presets/import/preview', {
+ name: 'writer',
+ model: 'openvibe/auto',
+ });
+ assert.equal(imported.status, 200);
+ assert.equal((await imported.json()).valid, true);
+ assert.equal(upstreamCalls, 0, 'previews never fetch an account or provider');
+ assert.equal(modelCalls, 0, 'previews never create a run');
+ assert.equal((await h.db.prepare('SELECT id FROM runs').all()).length, 0);
+ assert.equal((await h.db.prepare('SELECT id FROM exchange_key_usage').all()).length, 0);
+ } finally {
+ global.fetch = realFetch;
+ h.runs.create = createRun;
+ h.registry.listProviders = listProviders;
+ h.registry.listModels = listModels;
+ }
+ const body = {
+ model: 'openvibe',
+ messages: [{ role: 'user', content: 'Hello' }],
+ };
+ const first = await call('/v1/chat/completions', body, {
+ 'idempotency-key': 'c2-repeat-request-001',
+ });
+ assert.equal(first.status, 200);
+ const a = await first.json();
+ const again = await call('/v1/chat/completions', body, {
+ 'idempotency-key': 'c2-repeat-request-001',
+ });
+ assert.equal(again.status, 200);
+ assert.equal(again.headers.get('x-openvibe-replayed'), '1');
+ assert.equal((await again.json()).id, a.id);
+ const changed = await call(
+ '/v1/chat/completions',
+ { ...body, messages: [{ role: 'user', content: 'Different' }] },
+ { 'idempotency-key': 'c2-repeat-request-001' },
+ );
+ assert.equal(changed.status, 409);
+ const messagesBody = { model: 'openvibe', max_tokens: 20, messages: body.messages };
+ const messagesFirst = await call('/v1/messages', messagesBody, { 'idempotency-key': 'messages-repeat-001' });
+ assert.equal(messagesFirst.status, 200);
+ const messagesResult = await messagesFirst.json();
+ const messagesAgain = await call('/v1/messages', messagesBody, { 'idempotency-key': 'messages-repeat-001' });
+ assert.equal(messagesAgain.status, 200);
+ assert.equal(messagesAgain.headers.get('x-openvibe-replayed'), '1');
+ assert.deepEqual(await messagesAgain.json(), messagesResult);
+ const { createKeys } = require('../server/platform/keys');
+ const keys = createKeys({ db: h.db });
+ const key = await keys.create('usr_01JAB2C3D4E5F6G7H8J9K0MNPQ', {
+ name: 'replay',
+ monthly_cap_usd: 1,
+ allowed_models: [],
+ });
+ const keyHeaders = {
+ authorization: 'Bearer ' + key.secret,
+ 'idempotency-key': 'key-repeat-request-001',
+ };
+ const prepare = h.db.prepare.bind(h.db);
+ let holdStatements = 0;
+ h.db.prepare = (sql) => {
+ if (sql.includes('SET reserved = x.reserved + m.take')) holdStatements++;
+ return prepare(sql);
+ };
+ const keyFirst = await call('/v1/chat/completions', body, keyHeaders);
+ assert.equal(keyFirst.status, 200);
+ const keyResult = await keyFirst.json();
+ await new Promise((r) => setTimeout(r, 100));
+ const before = await h.db.prepare('SELECT * FROM exchange_key_usage WHERE key_id = ?').all(key.key.id);
+ assert.equal(before.length, 1);
+ assert.equal(before[0].status, 'settled');
+ // A replay must succeed even when no additional hold would fit.
+ await h.db.prepare('UPDATE exchange_key_months SET spent = 1 WHERE key_id = ?').run(key.key.id);
+ const keyAgain = await call('/v1/chat/completions', body, keyHeaders);
+ assert.equal(keyAgain.status, 200);
+ assert.equal(keyAgain.headers.get('x-openvibe-replayed'), '1');
+ assert.deepEqual(await keyAgain.json(), keyResult);
+ assert.equal(holdStatements, 1, 'a replay never takes a second hold');
+ assert.deepEqual(await h.db.prepare('SELECT * FROM exchange_key_usage WHERE key_id = ?').all(key.key.id), before);
+ assert.deepEqual(await h.db.prepare('SELECT spent,reserved FROM exchange_key_months WHERE key_id = ?').get(key.key.id), { spent: 1, reserved: 0 });
+ const conflict = await call('/v1/chat/completions', { ...body, messages: [{ role: 'user', content: 'Changed' }] }, keyHeaders);
+ assert.equal(conflict.status, 409);
+ assert.equal(holdStatements, 1);
+
+ const sameOwner = await keys.create('usr_01JAB2C3D4E5F6G7H8J9K0MNPQ', { name: 'same owner', monthly_cap_usd: 0, allowed_models: [] });
+ const sameReplay = await call('/v1/chat/completions', body, { ...keyHeaders, authorization: 'Bearer ' + sameOwner.secret });
+ assert.equal(sameReplay.status, 200, 'idempotency belongs to the owner across their keys');
+ assert.equal((await sameReplay.json()).id, keyResult.id);
+ assert.equal(holdStatements, 1);
+
+ // Lookup rejects an expired key before the replay shortcut or any new hold.
+ await h.db.prepare('UPDATE exchange_api_keys SET expires_at = ? WHERE id = ?').run('2000-01-01T00:00:00.000Z', sameOwner.key.id);
+ const expired = await call('/v1/chat/completions', body, { ...keyHeaders, authorization: 'Bearer ' + sameOwner.secret });
+ assert.equal(expired.status, 401);
+ assert.equal(holdStatements, 1);
+
+ const other = await keys.create('usr_01JAB2C3D4E5F6G7H8J9K0MNPR', { name: 'other', monthly_cap_usd: 5, allowed_models: [] });
+ const otherHeaders = { ...keyHeaders, authorization: 'Bearer ' + other.secret };
+ const own = await call('/v1/chat/completions', body, otherHeaders);
+ assert.equal(own.status, 200);
+ assert.notEqual((await own.json()).id, keyResult.id, 'one owner cannot replay another owner\'s run');
+ assert.equal(own.headers.get('x-openvibe-replayed'), null);
+
+ const holdCount = holdStatements;
+ const concurrentHeaders = { ...otherHeaders, 'idempotency-key': 'concurrent-request-001' };
+ const concurrent = await Promise.all(Array.from({ length: 4 }, () => call('/v1/chat/completions', body, concurrentHeaders)));
+ const answers = await Promise.all(concurrent.map(async (r) => ({ status: r.status, body: await r.json() })));
+ assert.ok(answers.some((r) => r.status === 200));
+ assert.ok(answers.every((r) => r.status === 200 || r.status === 409));
+ assert.equal(holdStatements - holdCount, 1, 'concurrent duplicates claim one hold');
+ const concurrentReplay = await call('/v1/chat/completions', body, concurrentHeaders);
+ assert.equal(concurrentReplay.status, 200);
+ assert.equal((await concurrentReplay.json()).id, answers.find((r) => r.status === 200).body.id);
+ assert.equal(holdStatements - holdCount, 1);
+ assert.equal((await h.db.prepare("SELECT id FROM runs WHERE requester_type = 'user' AND requester_id = ? AND idempotency_key = ?").all('usr_01JAB2C3D4E5F6G7H8J9K0MNPR', 'concurrent-request-001')).length, 1);
+ const { claimRequest } = require('../server/platform/idempotency');
+ await claimRequest(h.db, 'pending-owner', 'pending-request-001', body);
+ await assert.rejects(claimRequest(h.db, 'pending-owner', 'pending-request-001', body), (e) => e.code === 'idempotency.in_progress');
+ assert.equal(holdStatements - holdCount, 1, 'an interrupted admission stays closed without a second hold');
+ h.db.prepare = prepare;
+ console.log(
+ 'Authenticated preview, import validation and idempotent chat replay passed',
+ );
+ } finally {
+ await h.stop();
+ }
+})().catch((e) => {
+ console.error(e);
+ process.exitCode = 1;
+});
new file mode 100644
new file mode 100644
@@ -0,0 +1,77 @@
@@ -0,0 +1,77 @@
+'use strict';
+const assert = require('node:assert/strict');
+const { boot } = require('./helpers');
+const { createKeys } = require('../server/platform/keys');
+(async () => {
+ const h = await boot();
+ try {
+ let now = Date.now();
+ const keys = createKeys({ db: h.db, clock: { now: () => now } });
+ await assert.rejects(
+ keys.create('alice', {
+ name: 'bad',
+ monthly_cap_usd: 1,
+ allowed_models: [],
+ expires_at: 'not-a-date',
+ }),
+ (e) => e.status === 422,
+ );
+ const made = await keys.create('alice', {
+ name: 'expiring',
+ monthly_cap_usd: 1,
+ allowed_models: [],
+ expires_at: new Date(now + 1000).toISOString(),
+ });
+ const resolved = await keys.resolve(made.secret);
+ assert.ok(resolved);
+ assert.ok(made.key.expires_at);
+ now += 2000;
+ assert.equal(await keys.resolve(made.secret), undefined);
+ for (const free of [false, true])
+ await assert.rejects(keys.reserve(resolved, 'model', free), (e) => e.status === 401);
+ assert.equal((await h.db.prepare('SELECT * FROM exchange_key_months WHERE key_id = ?').all(resolved.id)).length, 0);
+ assert.equal((await h.db.prepare('SELECT * FROM exchange_key_usage WHERE key_id = ?').all(resolved.id)).length, 0);
+
+ const boundary = await keys.create('alice', {
+ name: 'boundary', monthly_cap_usd: 2, allowed_models: [],
+ expires_at: new Date(now + 1000).toISOString(),
+ });
+ const stale = await keys.resolve(boundary.secret);
+ const firstHold = await keys.reserve(stale, 'model', false, 0.5);
+ assert.equal(firstHold.amount, 0.5, 'main retains bounded holds');
+ await firstHold.settle({ cost_usd: 0.1 });
+ now += 1000;
+ assert.equal(await keys.resolve(boundary.secret), undefined, 'expiry is inclusive');
+ await assert.rejects(keys.reserve(stale, 'model'), (e) => e.status === 401);
+ const ledger = await h.db.prepare('SELECT spent,reserved FROM exchange_key_months WHERE key_id = ?').get(stale.id);
+ assert.deepEqual(ledger, { spent: 0.1, reserved: 0 });
+
+ // Expiry after the initial check is still refused by the atomic hold statement.
+ const racing = await keys.create('alice', {
+ name: 'race', monthly_cap_usd: 2, allowed_models: [],
+ expires_at: new Date(now + 1000).toISOString(),
+ });
+ const raceKey = await keys.resolve(racing.secret);
+ const raceDb = { prepare(sql) {
+ const statement = h.db.prepare(sql);
+ if (sql.startsWith('INSERT INTO exchange_key_months')) return { async run(...args) {
+ const result = await statement.run(...args);
+ now += 1000;
+ return result;
+ } };
+ return statement;
+ } };
+ await assert.rejects(createKeys({ db: raceDb, clock: { now: () => now } }).reserve(raceKey, 'model'), (e) => e.status === 429);
+ assert.equal((await h.db.prepare('SELECT reserved FROM exchange_key_months WHERE key_id = ?').get(raceKey.id)).reserved, 0);
+ assert.equal((await h.db.prepare('SELECT * FROM exchange_key_usage WHERE key_id = ?').all(raceKey.id)).length, 0);
+ assert.equal((await keys.list('alice')).length, 3);
+ console.log(
+ 'Expired keys fail authentication and stale-key reservations',
+ );
+ } finally {
+ await h.stop();
+ }
+})().catch((e) => {
+ console.error(e);
+ process.exitCode = 1;
+});