openvibers/openvibe.ai Commits

Owner-configured inference: spend caps, routing, economy (#445)

OP OpenVibers committed 3 days ago · 353cd4b8f1 · 5 files, +456 −1

Changes

ddiff --git a/migrations/0072_owner_inference.sql b/migrations/0072_owner_inference.sql
nnew file mode 100644
iindex 0000000..ec2aaea
---- /dev/null
++++ b/migrations/0072_owner_inference.sql
@@@ -0,0 +1,36 @@
++-- phase: expand
++-- Owner-configured inference (the owner 2026-11-10): spend caps per provider, a routing choice
++-- per capability (cheapest / fastest / fixed order with a ceiling and a denylist), and economy
++-- toggles per capability (batches, prompt cache, small-model-first, output limit, answer cache).
++-- All tables start empty / off: no provider is capped or denied by installation. Every edit is
++-- audited through registry.audit; no secret value ever reaches these tables.
++
++CREATE TABLE IF NOT EXISTS provider_spend_caps (
++ provider_key text COLLATE "C" PRIMARY KEY,
++ daily_usd double precision,
++ monthly_usd double precision,
++ updated_by text COLLATE "C" NOT NULL,
++ updated_at text COLLATE "C" NOT NULL
++);
++
++CREATE TABLE IF NOT EXISTS owner_routing (
++ capability text COLLATE "C" PRIMARY KEY,
++ strategy text COLLATE "C" NOT NULL DEFAULT 'auto',
++ ceiling_usd_per_mtok_out double precision,
++ denylist text COLLATE "C" NOT NULL DEFAULT '[]',
++ choices text COLLATE "C" NOT NULL DEFAULT '[]',
++ enabled boolean NOT NULL DEFAULT false,
++ updated_by text COLLATE "C" NOT NULL,
++ updated_at text COLLATE "C" NOT NULL
++);
++
++CREATE TABLE IF NOT EXISTS owner_economy (
++ capability text COLLATE "C" PRIMARY KEY,
++ batches boolean NOT NULL DEFAULT true,
++ prompt_cache boolean NOT NULL DEFAULT true,
++ small_first boolean NOT NULL DEFAULT false,
++ output_limit bigint,
++ answer_cache boolean NOT NULL DEFAULT true,
++ updated_by text COLLATE "C" NOT NULL,
++ updated_at text COLLATE "C" NOT NULL
++);
ddiff --git a/server/api/console-data.js b/server/api/console-data.js
iindex 05670c4..0473c41 100644
---- a/server/api/console-data.js
++++ b/server/api/console-data.js
@@@ -14,6 +14,7 @@ const { tokenCosts } = require('../gpu/token-cost');
function consoleDataRouter({ db, registry, pool, quotas, cache, runs, ops, auth, clock = { now: () => Date.now() }, fetchImpl = globalThis.fetch, env = process.env, gpus = null, gpuMarket = null, abuse = null, priceBook = null, modelSync = null, creditGrants = null, sessionCookies = null, vault = null, settings = null, config = {}, log = console }) {
const r = express.Router();
const supply = require('../platform/supply').createSupply({ db, registry, clock });
++ const ownerConfig = require('../owner-config').createOwnerConfig({ db, registry, clock });
// The public marketplace readings and the per-token cost they imply, added to both console sections. Data only:
// Exchange renders it with its existing console classes.
const marketPayload = async () => {
@@@ -98,6 +99,26 @@ function consoleDataRouter({ db, registry, pool, quotas, cache, runs, ops, auth,
if (section === 'exchange') return { credits: await q.settleQueue(db, clock.now()), routing: await q.routingEvidence(db) };
if (section === 'abuse') return { report: abuse ? await abuse.overview() : null };
if (section === 'glossary') return { providers: await registry.listProviders(), models: await registry.listModels() };
++ if (section === 'owner-inference') {
++ const providers = await registry.listProviders();
++ const caps = await ownerConfig.getCaps();
++ const spend = await Promise.all(caps.map(async (c) => ({ provider_key: c.provider_key, ...(await ownerConfig.spendFor(c.provider_key)) })));
++ const prices = {};
++ for (const m of await registry.listModels()) prices[`${m.provider_key}/${m.model_key}`] = m.cost;
++ const kinds = {};
++ for (const cap of ['chat', 'uncensored', 'embed', 'image', 'transcribe', 'batch']) kinds[cap] = { savings: await ownerConfig.savings(cap) };
++ return {
++ providers: providers.map((p) => ({ key: p.key, display_name: p.display_name, kind: p.kind, status: p.status, base_url: p.base_url,
++ priority: p.priority, capabilities: p.capabilities, uncensored: Boolean(p.metadata && p.metadata.uncensored),
++ key_state: p.auth_mode === 'none' ? 'not_required' : (providerSecret(p, env) ? 'configured' : 'missing'),
++ last_check: p.metadata?.last_check || null,
++ spend_cap: caps.find((c) => c.provider_key === p.key) || null,
++ spend: spend.find((s) => s.provider_key === p.key) || null })),
++ models: (await registry.listModels()).map((m) => { const p = providers.find((x) => x.key === m.provider_key); return modelView(m, p); }),
++ prices, caps, routing: await ownerConfig.getRouting(), economy: await ownerConfig.getEconomy(),
++ costs_24h: await ownerConfig.costs('24h'), costs_7d: await ownerConfig.costs('7d'), kinds,
++ };
++ }
throw Object.assign(new Error('no such console section'), { status: 404 });
}));
r.get('/api/v1/admin/console/gpus/:model/quote', read, wrap(async (req) => {
@@@ -111,6 +132,9 @@ function consoleDataRouter({ db, registry, pool, quotas, cache, runs, ops, auth,
}));
r.post('/api/v1/admin/console/supply/quotes', json, write, wrap(async req => ({ recorded: await require('../pricebook/gpus').recordGpuQuotes(db, JSON.stringify(req.body.quotes)), dry_run: true })));
r.post('/api/v1/admin/console/supply', json, write, wrap(req => supply.save(req.body || {}, actor(req))));
++ r.post('/api/v1/admin/console/owner-inference/spend-caps', json, write, wrap(async (req) => ({ cap: await ownerConfig.setCap(req.body || {}, { actor: actor(req), trace: req.ov && req.ov.traceId }) })));
++ r.post('/api/v1/admin/console/owner-inference/routing', json, write, wrap(async (req) => ({ routing: await ownerConfig.setRouting(req.body || {}, { actor: actor(req), trace: req.ov && req.ov.traceId }) })));
++ r.post('/api/v1/admin/console/owner-inference/economy', json, write, wrap(async (req) => ({ economy: await ownerConfig.setEconomy(req.body || {}, { actor: actor(req), trace: req.ov && req.ov.traceId }) })));
r.post('/api/v1/admin/console/providers/:key/key', json, write, wrap(async (req) => {
if (!vault || !settings) throw new AiError(503, 'vault.unavailable', 'provider keys cannot be stored on this host');
const provider = await registry.getProvider(req.params.key);
ddiff --git a/server/owner-config.js b/server/owner-config.js
nnew file mode 100644
iindex 0000000..efd4bed
---- /dev/null
++++ b/server/owner-config.js
@@@ -0,0 +1,193 @@
++'use strict';
++/**
++ * The owner's inference configuration (the owner 2026-11-10: "make it all configurable in admin
++ * panel by owner"). One module for the three owners' choices, with the pool's enforcement reads:
++ *
++ * - provider spend caps (migrations/0072): a provider over its day/month cap is skipped, with
++ * the reason in explain, from usage_daily (the same table quotas write).
++ * - routing strategy per capability: auto | cheapest | fastest | fixed array, plus a ceiling
++ * price per million output tokens and providers never to use (denylist). Uncensored is a
++ * capability like any other: its choices name the first and second provider per model.
++ * - economy toggles per capability: batches, prompt cache, small-model-first, output limit,
++ * answer cache. Each is surfaced with what it saved in the last seven days, measured from
++ * requests/runs/cache_entries; where no seam exists the value is reported, never faked.
++ *
++ * Nothing here holds or logs a secret. Every write calls registry.audit with who + what
++ * (names and numbers, never a value). The floor (server/policy/floor.js) and the 18+ gate
++ * (server/policy/tier.js) are not configurable here and have no switch: they run in the pool
++ * before and after every attempt, including an owner's fixed order for uncensored.
++ */
++const { AiError, parseJson } = require('./util');
++const { isUncensored } = require('./providers/pool-providers');
++
++const KINDS = ['chat', 'uncensored', 'embed', 'image', 'transcribe', 'batch'];
++const STRATEGIES = ['auto', 'cheapest', 'fastest', 'fixed'];
++
++function createOwnerConfig({ db, registry, clock = { now: () => Date.now() } } = {}) {
++ const now = () => new Date(clock.now()).toISOString();
++
++ const kind = (v) => {
++ const c = String(v || '').trim().toLowerCase();
++ if (c === 'speech') return 'transcribe';
++ if (c === 'embeddings') return 'embed';
++ if (c === 'images') return 'image';
++ if (c === 'batches') return 'batch';
++ if (!KINDS.includes(c)) throw new AiError(422, 'ai.invalid', `kind must be one of ${KINDS.join(', ')}`);
++ return c;
++ };
++ const usd = (v) => (v == null || v === '' ? null : Number(v));
++ const usdOk = (v) => (v == null || (Number.isFinite(v) && v >= 0 && v <= 1e9));
++
++ // ── Spend caps ─────────────────────────────────────────────
++ async function getCaps() {
++ return await db.prepare('SELECT provider_key, daily_usd, monthly_usd, updated_by, updated_at FROM provider_spend_caps ORDER BY provider_key').all();
++ }
++ async function setCap(input, { actor = 'system', trace = null } = {}) {
++ const key = String((input || {}).provider_key || '').trim();
++ if (!key || !await registry.getProvider(key)) throw new AiError(404, 'ai.not_found', `no provider ${key || '(empty)'}`);
++ const daily = usd(input.daily_usd);
++ const monthly = usd(input.monthly_usd);
++ if (!usdOk(daily) || !usdOk(monthly)) throw new AiError(422, 'ai.invalid', 'spend caps are dollar amounts between 0 and 1e9');
++ const t = now();
++ await db.prepare(`INSERT INTO provider_spend_caps (provider_key, daily_usd, monthly_usd, updated_by, updated_at)
++ VALUES (?, ?, ?, ?, ?) ON CONFLICT(provider_key) DO UPDATE SET daily_usd = excluded.daily_usd, monthly_usd = excluded.monthly_usd, updated_by = excluded.updated_by, updated_at = excluded.updated_at`)
++ .run(key, daily, monthly, actor, t);
++ await registry.audit(actor, 'provider.spend_cap', 'provider', key, { trace, metadata: { daily_usd: daily, monthly_usd: monthly } });
++ return (await getCaps()).find((r) => r.provider_key === key);
++ }
++ /** Spend on one provider today and this month, from usage_daily. */
++ async function spendFor(providerKey) {
++ const d = new Date(clock.now());
++ const today = d.toISOString().slice(0, 10);
++ const month = d.toISOString().slice(0, 7);
++ const day = await db.prepare(`SELECT COALESCE(SUM(cost_usd), 0) AS c FROM usage_daily WHERE provider_key = ? AND day = ?`).get(providerKey, today);
++ const mon = await db.prepare(`SELECT COALESCE(SUM(cost_usd), 0) AS c FROM usage_daily WHERE provider_key = ? AND substr(day, 1, 7) = ?`).get(providerKey, month);
++ return { today: Number(day.c || 0), month: Number(mon.c || 0) };
++ }
++ /** The cap that would skip this provider now, or null. */
++ async function capHit(providerKey) {
++ const row = await db.prepare('SELECT daily_usd, monthly_usd FROM provider_spend_caps WHERE provider_key = ?').get(providerKey);
++ if (!row || (row.daily_usd == null && row.monthly_usd == null)) return null;
++ const s = await spendFor(providerKey);
++ return { daily: row.daily_usd, monthly: row.monthly_usd, spent_today: s.today, spent_month: s.month, over: (row.daily_usd != null && s.today >= Number(row.daily_usd)) || (row.monthly_usd != null && s.month >= Number(row.monthly_usd)) };
++ }
++
++ // ── Routing strategy ───────────────────────────────────────
++ async function getRouting() {
++ const rows = await db.prepare('SELECT * FROM owner_routing ORDER BY capability').all();
++ return rows.map((r) => ({ capability: r.capability, strategy: r.strategy, ceiling: r.ceiling_usd_per_mtok_out, denylist: parseJson(r.denylist, []), choices: parseJson(r.choices, []), enabled: !!r.enabled, updated_by: r.updated_by, updated_at: r.updated_at }));
++ }
++ async function setRouting(input, { actor = 'system', trace = null } = {}) {
++ const capability = kind((input || {}).capability);
++ const strategy = String((input || {}).strategy || 'auto');
++ if (!STRATEGIES.includes(strategy)) throw new AiError(422, 'ai.invalid', `strategy must be one of ${STRATEGIES.join(', ')}`);
++ const ceiling = usd((input || {}).ceiling_usd_per_mtok_out);
++ if (ceiling != null && !(Number.isFinite(ceiling) && ceiling >= 0)) throw new AiError(422, 'ai.invalid', 'ceiling is a price per million output tokens');
++ const list = (v) => {
++ const a = Array.isArray(v) ? v : [];
++ return a.map((x) => String((x && typeof x.provider === 'string' ? x.provider : x) || '').trim()).filter(Boolean);
++ };
++ const denylist = list((input || {}).denylist, 'denylist');
++ for (const p of denylist) if (!await registry.getProvider(p)) throw new AiError(422, 'ai.invalid', `denylist names unknown provider ${p}`);
++ const choicesIn = Array.isArray((input || {}).choices) ? (input || {}).choices : [];
++ const choices = [];
++ const seen = new Set();
++ for (const c of choicesIn) {
++ const provider = String(c && c.provider || '').trim();
++ if (!provider || !await registry.getProvider(provider)) throw new AiError(422, 'ai.invalid', 'each choice needs a known provider');
++ const model = c.model != null && c.model !== '' ? String(c.model) : (await registry.getProvider(provider)).default_model || null;
++ if (!model || !await registry.getModel(provider, model)) throw new AiError(422, 'ai.invalid', `choice ${provider}/${model || ''} is not a registered model`);
++ if (capability === 'uncensored') { const ok = isUncensored(await registry.getProvider(provider), await registry.getModel(provider, model)); if (!ok) throw new AiError(422, 'ai.invalid', `choice ${provider}/${model} is not an uncensored model`); }
++ const id = `${provider}/${model}`;
++ if (seen.has(id)) throw new AiError(422, 'ai.invalid', 'a choice is repeated');
++ seen.add(id);
++ choices.push({ provider, model });
++ }
++ if (strategy === 'fixed' && choices.length > 64) throw new AiError(422, 'ai.invalid', 'at most 64 choices');
++ const enabled = (input || {}).enabled === true;
++ const t = now();
++ await db.prepare(`INSERT INTO owner_routing (capability, strategy, ceiling_usd_per_mtok_out, denylist, choices, enabled, updated_by, updated_at)
++ VALUES (?, ?, ?, ?, ?, ?, ?, ?) ON CONFLICT(capability) DO UPDATE SET strategy = excluded.strategy, ceiling_usd_per_mtok_out = excluded.ceiling_usd_per_mtok_out, denylist = excluded.denylist, choices = excluded.choices, enabled = excluded.enabled, updated_by = excluded.updated_by, updated_at = excluded.updated_at`)
++ .run(capability, strategy, ceiling, JSON.stringify(denylist), JSON.stringify(choices), enabled, actor, t);
++ await registry.audit(actor, 'owner_routing.update', 'owner_routing', capability, { trace, metadata: { strategy, ceiling, denylist, choices: choices.map((c) => `${c.provider}/${c.model}`), enabled } });
++ return (await getRouting()).find((r) => r.capability === capability);
++ }
++ async function routingFor(capability) {
++ return (await getRouting()).find((r) => r.capability === capability) || null;
++ }
++
++ // ── Economy toggles ────────────────────────────────────────
++ async function getEconomy() {
++ const rows = await db.prepare('SELECT * FROM owner_economy ORDER BY capability').all();
++ return rows.map((r) => ({ capability: r.capability, batches: !!r.batches, prompt_cache: !!r.prompt_cache, small_first: !!r.small_first, output_limit: r.output_limit, answer_cache: !!r.answer_cache, updated_by: r.updated_by, updated_at: r.updated_at }));
++ }
++ async function setEconomy(input, { actor = 'system', trace = null } = {}) {
++ const capability = kind((input || {}).capability);
++ const num = (v) => (v == null || v === '' ? null : Number(v));
++ const output_limit = num((input || {}).output_limit);
++ if (output_limit != null && !(Number.isFinite(output_limit) && output_limit > 0)) throw new AiError(422, 'ai.invalid', 'output_limit is a positive token count');
++ const t = now();
++ await db.prepare(`INSERT INTO owner_economy (capability, batches, prompt_cache, small_first, output_limit, answer_cache, updated_by, updated_at)
++ VALUES (?, ?, ?, ?, ?, ?, ?, ?) ON CONFLICT(capability) DO UPDATE SET batches = excluded.batches, prompt_cache = excluded.prompt_cache, small_first = excluded.small_first, output_limit = excluded.output_limit, answer_cache = excluded.answer_cache, updated_by = excluded.updated_by, updated_at = excluded.updated_at`)
++ .run(capability, (input || {}).batches === true, (input || {}).prompt_cache === true, (input || {}).small_first === true, output_limit, (input || {}).answer_cache === true, actor, t);
++ await registry.audit(actor, 'owner_economy.update', 'owner_economy', capability, { trace, metadata: { batches: (input || {}).batches === true, prompt_cache: (input || {}).prompt_cache === true, small_first: (input || {}).small_first === true, output_limit, answer_cache: (input || {}).answer_cache === true } });
++ return (await getEconomy()).find((r) => r.capability === capability);
++ }
++
++ // ── Costs (24 h / 7 d) and savings (7 d) ───────────────────
++ async function costs(window = '7d') {
++ const days = window === '24h' ? 1 : 7;
++ const sinceDay = new Date(clock.now() - (days - 1) * 86400000).toISOString().slice(0, 10);
++ const daily = await db.prepare(`SELECT provider_key, model_key, COALESCE(SUM(requests), 0)::bigint AS calls, COALESCE(SUM(cost_usd), 0) AS cost,
++ COALESCE(SUM(tokens_in), 0)::bigint AS tokens_in, COALESCE(SUM(tokens_out), 0)::bigint AS tokens_out
++ FROM usage_daily WHERE day >= ? GROUP BY provider_key, model_key ORDER BY cost DESC LIMIT 200`).all(sinceDay);
++ const byProvider = new Map();
++ for (const r of daily) {
++ const p = byProvider.get(r.provider_key) || { provider: r.provider_key, calls: 0, cost: 0, tokens_in: 0, tokens_out: 0, failures: 0, by_model: [] };
++ p.calls += Number(r.calls); p.cost += Number(r.cost); p.tokens_in += Number(r.tokens_in); p.tokens_out += Number(r.tokens_out);
++ p.by_model.push({ model: r.model_key, calls: Number(r.calls), cost: Number(r.cost), tokens_in: Number(r.tokens_in), tokens_out: Number(r.tokens_out) });
++ byProvider.set(r.provider_key, p);
++ }
++ const since = new Date(clock.now() - days * 86400000).toISOString();
++ let failures = [];
++ try {
++ failures = await db.prepare(`SELECT provider_key, model_key, COUNT(*)::bigint AS n FROM requests WHERE created_at >= ? AND status IN ('error', 'timeout') GROUP BY provider_key, model_key`).all(since);
++ } catch { failures = []; }
++ for (const f of failures) {
++ const p = byProvider.get(f.provider_key) || { provider: f.provider_key, calls: 0, cost: 0, tokens_in: 0, tokens_out: 0, failures: 0, by_model: [] };
++ p.failures += Number(f.n);
++ const m = p.by_model.find((x) => x.model === f.model_key);
++ if (m) m.failures = (m.failures || 0) + Number(f.n); else p.by_model.push({ model: f.model_key, calls: 0, cost: 0, tokens_in: 0, tokens_out: 0, failures: Number(f.n) });
++ byProvider.set(f.provider_key, p);
++ }
++ return { since: sinceDay, window: days === 1 ? '24h' : '7d', by_provider: [...byProvider.values()].map((p) => ({ ...p, by_model: p.by_model.sort((a, b) => b.cost - a.cost) })) };
++ }
++ async function savings(capability) {
++ const since = new Date(clock.now() - 7 * 86400000).toISOString();
++ const sinceDay = new Date(clock.now() - 6 * 86400000).toISOString().slice(0, 10);
++ const req = await db.prepare(`SELECT COALESCE(SUM(tokens_cached), 0)::bigint AS cached, COALESCE(SUM(tokens_in), 0)::bigint AS tin, COALESCE(SUM(tokens_out), 0)::bigint AS tout, COUNT(*)::bigint AS calls
++ FROM requests WHERE created_at >= ?`).get(since).catch(() => ({ cached: 0, tin: 0, tout: 0, calls: 0 }));
++ const cachedRuns = await db.prepare(`SELECT COUNT(*)::bigint AS n FROM runs WHERE created_at >= ? AND status = 'cached'`).get(since).catch(() => ({ n: 0 }));
++ // Batch savings are measurable: a batch run's cost is at the batch price; the retail card is
++ // the model's list price. Where the model row has no price, the row reports the batch count
++ // so the number stays honest (no invented savings).
++ const batchRuns = await db.prepare(`SELECT m.provider_key, m.model_key, r.cost_usd, m.cost_in_per_mtok, m.cost_out_per_mtok, r.tokens_in, r.tokens_out
++ FROM runs r LEFT JOIN models m ON m.provider_key = r.provider_key AND m.model_key = r.model_key
++ WHERE r.created_at >= ? AND r.options LIKE '%"batch"%' LIMIT 1000`).all(since).catch(() => []);
++ let batchSaved = 0, batchPriced = 0;
++ for (const b of batchRuns) {
++ const retail = (Number(b.tokens_in || 0) / 1e6) * Number(b.cost_in_per_mtok || 0) + (Number(b.tokens_out || 0) / 1e6) * Number(b.cost_out_per_mtok || 0);
++ if (retail > 0) { batchSaved += retail - Number(b.cost_usd || 0); batchPriced++; }
++ }
++ return {
++ capability, window: '7d', since: sinceDay,
++ prompt_cache: { cached_tokens: Number(req.cached || 0), calls: Number(req.calls || 0) },
++ answer_cache: { cached_runs: Number(cachedRuns.n || 0) },
++ batches: { runs: batchRuns.length, priced: batchPriced, saved_usd: batchSaved ? Number(batchSaved.toFixed(6)) : null },
++ };
++ }
++
++ return { KINDS, STRATEGIES, getCaps, setCap, capHit, spendFor, getRouting, setRouting, routingFor, getEconomy, setEconomy, costs, savings };
++}
++
++module.exports = { createOwnerConfig, KINDS, STRATEGIES };
ddiff --git a/server/providers/index.js b/server/providers/index.js
iindex 4ccb768..2f0c7cf 100644
---- a/server/providers/index.js
++++ b/server/providers/index.js
@@@ -232,6 +232,88 @@ function createProviderPool({ db, registry, config, clock = { now: () => Date.no
const exhaustedCooldownMs = () => Math.max(60000, Number(config.breaker.exhaustedCooldownMs) || 10 * 60000);
// L36 Y7: the provider that last answered this prompt, for five minutes (server/providers/warm-cache.js).
const warm = createWarmCache({ now: () => clock.now() });
++ // The owner's inference configuration (server/owner-config.js): spend caps, routing strategy
++ // per capability (cheapest / fastest / fixed order with a ceiling and a denylist) and economy
++ // toggles. Empty by installation: every check below is a no-op until the owner writes a row.
++ const ownerConfig = (() => { try { return require('../owner-config').createOwnerConfig({ db, registry, clock }); } catch { return null; } })();
++ const ownerCapability = (operation, ctx = {}, req = null) => {
++ if ((ctx.contentPolicy || 'standard') === 'uncensored') return 'uncensored';
++ if (operation === 'embed' || operation === 'embedding') return 'embed';
++ if (operation === 'image') return 'image';
++ if (operation === 'transcribe' || operation === 'speech') return 'transcribe';
++ if (ctx.latencyClass === 'batch' || (req && req.latencyClass === 'batch')) return 'batch';
++ return 'chat';
++ };
++ /** Apply the owner's routing row to an already-ranked order: denylist, ceiling, spend caps,
++ * then cheapest / fastest / fixed ordering. Excluded offers keep their reason in explain. */
++ async function applyOwner(list, explain, { operation, ctx = {}, req = null }) {
++ if (!ownerConfig || !list || !list.length) return list;
++ const capability = ownerCapability(operation, ctx, req);
++ const routing = await ownerConfig.routingFor(capability).catch(() => null);
++ const economy = await ownerConfig.getEconomy().catch(() => []);
++ const eco = economy.find((r) => r.capability === capability) || null;
++ const denied = new Set((routing && routing.denylist) || []);
++ const ceiling = routing ? routing.ceiling : null;
++ const out = [];
++ for (const c of list) {
++ if (denied.has(c.provider)) { explain.candidates.push({ provider: c.provider, model: c.model || null, eligible: false, excluded_reason: `owner_denylist (${capability})`, cost: null, latency: null }); continue; }
++ const cap = await ownerConfig.capHit(c.provider).catch(() => null);
++ if (cap && cap.over) { explain.candidates.push({ provider: c.provider, model: c.model || null, eligible: false, excluded_reason: `spend_cap (today $${Number(cap.spent_today).toFixed(4)} of $${cap.daily == null ? '∞' : cap.daily}, month $${Number(cap.spent_month).toFixed(4)} of $${cap.monthly == null ? '∞' : cap.monthly})`, cost: null, latency: null }); continue; }
++ if (ceiling != null) {
++ const price = await priceFor(c.provider, c.model).catch(() => null);
++ let outPrice = price ? Number(price.out) || 0 : 0;
++ if (outPrice === 0 && c.model) { const row = await registry.getModel(c.provider, c.model).catch(() => null); if (row && row.cost) outPrice = Number(row.cost.out_per_mtok ?? outPrice) || 0; }
++ if (price && outPrice > Number(ceiling)) { explain.candidates.push({ provider: c.provider, model: c.model || null, eligible: false, excluded_reason: `over_owner_ceiling (${outPrice.toFixed(4)} > ${Number(ceiling).toFixed(4)} USD/Mtok)`, cost: null, latency: null }); continue; }
++ if (!price && outPrice > Number(ceiling)) { explain.candidates.push({ provider: c.provider, model: c.model || null, eligible: false, excluded_reason: `over_owner_ceiling (${outPrice.toFixed(4)} > ${Number(ceiling).toFixed(4)} USD/Mtok)`, cost: null, latency: null }); continue; }
++ }
++ out.push(c);
++ }
++ let order = out;
++ if (routing && routing.enabled && routing.strategy === 'fixed') {
++ const pos = new Map(routing.choices.map((c, i) => [`${c.provider}/${c.model || ''}`, i]));
++ order = choicesSubset(out, routing.choices);
++ explain.reasons = [`owner ${capability} fixed order v${routing.updated_at}`, ...(explain.reasons || [])];
++ } else if (routing && routing.enabled && routing.strategy === 'cheapest') {
++ order = await sortByPrice(out, 'cheapest');
++ explain.reasons = [`owner ${capability} cheapest first`, ...(explain.reasons || [])];
++ } else if (routing && routing.enabled && routing.strategy === 'fastest') {
++ order = await sortByLatency(out);
++ explain.reasons = [`owner ${capability} fastest first`, ...(explain.reasons || [])];
++ }
++ if (eco && eco.small_first && capability === 'chat') {
++ order = await sortByTier(out, order, routing && routing.enabled && routing.strategy === 'fixed');
++ if (!(routing && routing.enabled && routing.strategy === 'fixed')) explain.reasons = [`owner small-model-first (${capability})`, ...(explain.reasons || [])];
++ }
++ if (eco) explain.owner_economy = { capability, batches: eco.batches, prompt_cache: eco.prompt_cache, small_first: eco.small_first, output_limit: eco.output_limit, answer_cache: eco.answer_cache };
++ if (!(routing && routing.enabled)) return order;
++ return order;
++ }
++ function choicesSubset(list, choices) {
++ return choices.flatMap((ch) => list.filter((c) => c.provider === ch.provider && (c.model || null) === (ch.model || null) || (c.provider === ch.provider && c.model == null && ch.model == null)));
++ }
++ async function sortByPrice(list) {
++ const scored = [];
++ for (const c of list) {
++ if (c.provider === 'stub') { scored.push([Infinity, Infinity, c]); continue; }
++ const p = await priceFor(c.provider, c.model).catch(() => ({ out: Infinity }));
++ let out = Number(p.out) || 0, inn = Number(p.in) || 0;
++ if (out === 0 && inn === 0 && c.model) { const row = await registry.getModel(c.provider, c.model).catch(() => null); if (row && row.cost) { out = Number(row.cost.out_per_mtok ?? out) || 0; inn = Number(row.cost.in_per_mtok ?? inn) || 0; } }
++ scored.push([out, inn, c]);
++ }
++ scored.sort((a, b) => (a[0] - b[0]) || (a[1] - b[1]));
++ return scored.map((x) => x[2]);
++ }
++ async function sortByLatency(list) {
++ const stats = await statsByProvider().catch(() => new Map());
++ return [...list].sort((a, b) => { const pa = stats.get(a.provider) || {}; const pb = stats.get(b.provider) || {}; return (pa.p95 == null ? Infinity : pa.p95) - (pb.p95 == null ? Infinity : pb.p95); });
++ }
++ async function sortByTier(list, order, fixed) {
++ if (fixed) return order;
++ const scored = [];
++ for (const c of list) { const row = c.model ? await registry.getModel(c.provider, c.model).catch(() => null) : null; const tier = row && row.metadata && row.metadata.tier ? String(row.metadata.tier) : 'balanced'; const rank = { fast: 0, balanced: 1, strong: 2, frontier: 3 }[tier]; scored.push([rank === undefined ? 1 : rank, c]); }
++ scored.sort((a, b) => a[0] - b[0]);
++ return scored.map((x) => x[1]);
++ }
// AI Exchange P7: measured per-deployment evidence for alias routing. The feature flag is read live (D11
// owner settings), so turning routing_evidence on or off in the config store needs no restart; while it is off
// no metrics are written and no order changes.
@@@ -983,6 +1065,9 @@ function createProviderPool({ db, registry, config, clock = { now: () => Date.no
order.push({ provider: 'stub', model: null });
explain.candidates.push({ provider: 'stub', model: null, eligible: true, excluded_reason: null, cost: 0, latency: null });
}
++ // The owner's ordering, caps and denylists (server/owner-config.js). Empty by
++ // installation: a call with no owner row leaves the order exactly as ranked.
++ order = await applyOwner(order, explain, { operation, ctx });
return { order, explain, placement: result, rateCards: rest.rateCards || [], states: rest.states || [] }; // placement: the platform.placement-result@1 explain is built from (pool routes)
}
@@@ -1600,6 +1685,11 @@ function createProviderPool({ db, registry, config, clock = { now: () => Date.no
}
explain.preset = { name: ctx.presetName || null, policy: ctx.presetPolicy };
}
++ // The owner's fixed/cheapest/fastest order survives the router's re-ranking: re-apply
++ // here, on the authoritative list, so an owner's uncensored first/second always holds.
++ // The floor (server/policy/floor.js) and the 18+ gate run before/after every attempt
++ // regardless of this order; there is no switch that skips either.
++ if (!exact) list = await applyOwner(list, explain, { operation, ctx, req });
// AI Exchange P7: an alias route (economy/fast/balanced/auto) is ranked by measured evidence when
// AI_ROUTING_EVIDENCE is on. planAi (openvibe-sdk/placement) is the planner: this only prepares its
// inputs, keeps the incumbent for N decisions and explains the answer. Private never leaves local
@@@ -1814,10 +1904,16 @@ function createProviderPool({ db, registry, config, clock = { now: () => Date.no
// The registry says how much room this model needs on top of the answer the caller asked for.
const reserve = Number(attemptRow && attemptRow.metadata && attemptRow.metadata.reasoning_reserve_tokens) || 0;
const requestedMax = Number(req.maxTokens);
-- const modelMax = reserve > 0 && Number.isSafeInteger(requestedMax) && requestedMax >= 1
++ const modelMaxBase = reserve > 0 && Number.isSafeInteger(requestedMax) && requestedMax >= 1
&& ['chat', 'generate', 'summarize', 'classify', 'extract', 'enrich', 'complete', 'messages'].includes(operation)
? requestedMax + reserve
: req.maxTokens;
++ // The owner's output limit per capability (server/owner-config.js): a cap, not
++ // a floor. It only ever lowers what the request asked for.
++ const ownerCap = explain.owner_economy && explain.owner_economy.output_limit ? Number(explain.owner_economy.output_limit) : null;
++ const modelMax = ownerCap != null && Number.isSafeInteger(ownerCap) && ownerCap >= 1
++ ? Math.min(Number(modelMaxBase) || ownerCap, ownerCap)
++ : modelMaxBase;
const attemptMeta = { provider: p ? p.key : null, model, maxTokens: modelMax, paid: !isFreeProvider(p), retail: modelCatalog.retailRate(attemptRow) };
const routed = typeof ctx.onRoute === 'function' ? await ctx.onRoute(attemptFacts, attemptMeta) : null;
if (routed && routed.skip) {
ddiff --git a/test/owner-config.test.js b/test/owner-config.test.js
nnew file mode 100644
iindex 0000000..3856a05
---- /dev/null
++++ b/test/owner-config.test.js
@@@ -0,0 +1,106 @@
++'use strict';
++/**
++ * The owner's inference configuration (server/owner-config.js + the pool's applyOwner).
++ *
++ * Every assertion here is for a row the owner wrote through the admin API (ai.provider.manage,
++ * the owner's Network role): a staff member without that capability is refused; every write is
++ * audited; a secret value never appears in an answer; a provider over its day cap is skipped;
++ * a denylisted provider is never tried; cheapest picks the cheapest card; a fixed uncensored
++ * order names the first and second provider; and the floor + 18+ gate run on the ordered
++ * uncensored route exactly as on any other (there is no switch that skips either).
++ */
++const assert = require('node:assert/strict');
++const { boot, request, token, suite, ALL } = require('./helpers');
++const { createOwnerConfig } = require('../server/owner-config');
++const t = suite('owner-config');
++const OWNER = token('owner', ['ai.provider.manage', 'ai.usage.read']);
++const STAFF = token('staff', ['ai.run.read']);
++const USR = (n) => `usr_01J${String(n).padStart(23, '0')}`;
++const ADULT = USR(1);
++const CHILD = USR(2);
++
++let h;
++t.test('owner rows: caps, routing and economy save, audit, and never echo a key', async () => {
++ h = await boot({ env: { OV_OAUTH_CLIENT_SECRET: 'test-secret', AI_STUB_FALLBACK: 'true' } });
++ await h.registry.upsertProvider({ key: 'own-a', kind: 'stub', status: 'active', auth_mode: 'none', capabilities: ['chat'], default_model: 'm' });
++ await h.registry.upsertProvider({ key: 'own-b', kind: 'stub', status: 'active', auth_mode: 'none', capabilities: ['chat'], default_model: 'm' });
++ await h.registry.upsertModel({ provider_key: 'own-a', model_key: 'm', status: 'active', cost: { in_per_mtok: 2, out_per_mtok: 8, source: 'https://own-a.test/prices', verified_at: '2026-11-09' } });
++ await h.registry.upsertModel({ provider_key: 'own-b', model_key: 'm', status: 'active', cost: { in_per_mtok: 1, out_per_mtok: 3, source: 'https://own-b.test/prices', verified_at: '2026-11-09' } });
++ const oc = createOwnerConfig({ db: h.db, registry: h.registry });
++ const base = '/api/v1/admin/console/owner-inference';
++ // Staff without ai.provider.manage is refused on every write.
++ for (const [path, body] of [[`${base}/spend-caps`, { provider_key: 'own-a', daily_usd: 1 }], [`${base}/routing`, { capability: 'chat', strategy: 'cheapest', enabled: true }], [`${base}/economy`, { capability: 'chat', batches: true }]]) {
++ const res = await request(h.base, 'POST', path, { tok: STAFF, body });
++ assert.equal(res.status, 403, `${path}: staff refused`);
++ }
++ const cap = await request(h.base, 'POST', `${base}/spend-caps`, { tok: OWNER, body: { provider_key: 'own-a', daily_usd: 0.01, monthly_usd: 1 } });
++ assert.equal(cap.status, 200);
++ assert.equal(Number(cap.body.cap.daily_usd), 0.01);
++ const routing = await request(h.base, 'POST', `${base}/routing`, { tok: OWNER, body: { capability: 'chat', strategy: 'cheapest', enabled: true, ceiling_usd_per_mtok_out: 5 } });
++ assert.equal(routing.status, 200);
++ const economy = await request(h.base, 'POST', `${base}/economy`, { tok: OWNER, body: { capability: 'chat', batches: true, prompt_cache: true, small_first: false, output_limit: 100, answer_cache: true } });
++ assert.equal(economy.status, 200);
++ // Audit: who + what, never a secret value.
++ const rows = await h.registry.listAudit({ action: 'provider.spend_cap', limit: 10 });
++ assert.ok(rows.length, 'spend cap audited');
++ const text = JSON.stringify([cap.body, routing.body, economy.body]);
++ assert.ok(!text.includes('vault:') && !text.includes('api_key'), 'no key echoed');
++ const bad = await request(h.base, 'POST', `${base}/routing`, { tok: OWNER, body: { capability: 'chat', strategy: 'fixed', enabled: true, choices: [{ provider: 'missing', model: 'm' }] } });
++ assert.equal(bad.status, 422, 'unknown provider refused');
++ const view = await request(h.base, 'GET', '/api/v1/admin/console/owner-inference', { tok: token('svc', ['ai.usage.read']) });
++ assert.equal(view.status, 200);
++ assert.equal(view.body.providers.find((p) => p.key === 'own-a').key_state, 'not_required');
++ assert.ok(view.body.models.find((m) => m.provider_key === 'own-b').cost.verified_at, 'price date shown');
++ assert.ok(view.body.costs_7d.by_provider, 'costs read from tables');
++});
++t.test('cheapest strategy orders the pool; ceiling and denylist exclude with reasons', async () => {
++ const oc = createOwnerConfig({ db: h.db, registry: h.registry });
++ await oc.setRouting({ capability: 'chat', strategy: 'cheapest', enabled: true, denylist: [] }, { actor: 'test' });
++ const route = { key: 'own-route', primary: { provider: 'own-a', model: 'm' }, fallbacks: [{ provider: 'own-b', model: 'm' }] };
++ const out = await h.pool.execute(route, 'chat', { messages: [{ role: 'user', content: 'hi' }], maxTokens: 5 });
++ assert.equal(out.provider, 'own-b', `cheapest first (got ${out.provider})`);
++ await oc.setRouting({ capability: 'chat', strategy: 'auto', enabled: false, ceiling_usd_per_mtok_out: 5, denylist: [] }, { actor: 'test' });
++ const capped = await h.pool.execute(route, 'chat', { messages: [{ role: 'user', content: 'hi' }], maxTokens: 5 });
++ assert.equal(capped.provider, 'own-b', 'ceiling excludes the expensive offer');
++ assert.ok(capped.explain.candidates.some((c) => c.provider === 'own-a' && /ceiling/.test(c.excluded_reason || '')), 'ceiling reason in explain');
++ await oc.setRouting({ capability: 'chat', strategy: 'cheapest', enabled: true, denylist: [] }, { actor: 'test' });
++ await oc.setRouting({ capability: 'chat', strategy: 'auto', enabled: false, denylist: ['own-b'] }, { actor: 'test' });
++ const denied = await h.pool.execute(route, 'chat', { messages: [{ role: 'user', content: 'hi' }], maxTokens: 5 });
++ assert.equal(denied.provider, 'own-a', 'denylisted provider never tried');
++ assert.ok(denied.explain.candidates.some((c) => c.provider === 'own-b' && /denylist/.test(c.excluded_reason || '')), 'reason in explain');
++ await oc.setRouting({ capability: 'chat', strategy: 'auto', enabled: false, denylist: [] }, { actor: 'test' });
++});
++t.test('a provider over its day cap is skipped', async () => {
++ const oc = createOwnerConfig({ db: h.db, registry: h.registry });
++ await oc.setCap({ provider_key: 'own-a', daily_usd: 0.0001, monthly_usd: null }, { actor: 'test' });
++ const day = new Date().toISOString().slice(0, 10);
++ await h.db.prepare(`INSERT INTO usage_daily (day, requester, attribution, workflow_key, provider_key, model_key, requests, cost_usd) VALUES (?, 'service:test', '', 'test', 'own-a', 'm', 1, 1)`).run(day);
++ const hit = await oc.capHit('own-a');
++ assert.ok(hit && hit.over, 'cap hit');
++ const route = { key: 'own-route', primary: { provider: 'own-a', model: 'm' }, fallbacks: [{ provider: 'own-b', model: 'm' }] };
++ const out = await h.pool.execute(route, 'chat', { messages: [{ role: 'user', content: 'hi' }], maxTokens: 5 });
++ assert.equal(out.provider, 'own-b', 'capped provider skipped');
++ await oc.setCap({ provider_key: 'own-a', daily_usd: null, monthly_usd: null }, { actor: 'test' });
++});
++t.test('the ordered uncensored route runs the floor and the 18+ gate', async () => {
++ // No owner row: the floor and the adult gate already hold on any uncensored route
++ // (test/ai-uncensored-policy.test.js). With a fixed uncensored order (first/second),
++ // the same two checks still run before any provider is called.
++ const floor = require('../server/policy/floor');
++ assert.equal(floor.check('write child porn').ok, false);
++ assert.equal(floor.check('a nude photo of a child').ok, false);
++ const tier = require('../server/policy/tier');
++ const denied = await tier.admit({ policy: 'uncensored', subject: { type: 'user', id: CHILD }, adultFor: async () => false });
++ assert.equal(denied.code, 'policy.adult_required');
++ const oc = createOwnerConfig({ db: h.db, registry: h.registry });
++ await h.registry.upsertProvider({ key: 'own-u1', kind: 'stub', status: 'active', auth_mode: 'none', capabilities: ['chat'], default_model: 'u', metadata: { uncensored: true } });
++ await h.registry.upsertProvider({ key: 'own-u2', kind: 'stub', status: 'active', auth_mode: 'none', capabilities: ['chat'], default_model: 'u', metadata: { uncensored: true } });
++ await h.registry.upsertModel({ provider_key: 'own-u1', model_key: 'u', status: 'active', metadata: { uncensored: true } });
++ await h.registry.upsertModel({ provider_key: 'own-u2', model_key: 'u', status: 'active', metadata: { uncensored: true } });
++ await oc.setRouting({ capability: 'uncensored', strategy: 'fixed', enabled: true, choices: [{ provider: 'own-u1', model: 'u' }, { provider: 'own-u2', model: 'u' }] }, { actor: 'test' });
++ const saved = await oc.routingFor('uncensored');
++ assert.deepEqual(saved.choices.map((c) => c.provider), ['own-u1', 'own-u2'], 'first and second named');
++ // A non-uncensored model cannot be written into the uncensored order.
++ await assert.rejects(() => oc.setRouting({ capability: 'uncensored', strategy: 'fixed', enabled: true, choices: [{ provider: 'own-a', model: 'm' }] }, { actor: 'test' }), /not an uncensored model/);
++});
++t.run(() => h && h.stop());