openvibers/openvibe.ai Commits
Owner-configured inference: spend caps, routing, economy (#445)
Changes
nnew file mode 100644@@@ -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++);@@@ -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);nnew file mode 100644@@@ -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 };@@@ -232,6 +232,88 @@ function createProviderPool({ db, registry, config, clock = { now: () => Date.noconst 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.noorder.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) {nnew file mode 100644@@@ -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());
new file mode 100644
new file mode 100644
@@ -0,0 +1,36 @@
@@ -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
+);
@@ -14,6 +14,7 @@ const { tokenCosts } = require('../gpu/token-cost');
@@ -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 }) {
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 r = express.Router();
const supply = require('../platform/supply').createSupply({ db, registry, clock });
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:
// 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.
// Exchange renders it with its existing console classes.
const marketPayload = async () => {
const marketPayload = async () => {
@@ -98,6 +99,26 @@ function consoleDataRouter({ db, registry, pool, quotas, cache, runs, ops, auth,
@@ -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 === '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 === 'abuse') return { report: abuse ? await abuse.overview() : null };
if (section === 'glossary') return { providers: await registry.listProviders(), models: await registry.listModels() };
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 });
throw Object.assign(new Error('no such console section'), { status: 404 });
}));
}));
r.get('/api/v1/admin/console/gpus/:model/quote', read, wrap(async (req) => {
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,
@@ -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/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/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) => {
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');
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);
const provider = await registry.getProvider(req.params.key);
new file mode 100644
new file mode 100644
@@ -0,0 +1,193 @@
@@ -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 };
@@ -232,6 +232,88 @@ function createProviderPool({ db, registry, config, clock = { now: () => Date.no
@@ -232,6 +232,88 @@ function createProviderPool({ db, registry, config, clock = { now: () => Date.no
const exhaustedCooldownMs = () => Math.max(60000, Number(config.breaker.exhaustedCooldownMs) || 10 * 60000);
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).
// L36 Y7: the provider that last answered this prompt, for five minutes (server/providers/warm-cache.js).
const warm = createWarmCache({ now: () => clock.now() });
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
// 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
// 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.
// no metrics are written and no order changes.
@@ -983,6 +1065,9 @@ function createProviderPool({ db, registry, config, clock = { now: () => Date.no
@@ -983,6 +1065,9 @@ function createProviderPool({ db, registry, config, clock = { now: () => Date.no
order.push({ provider: 'stub', model: null });
order.push({ provider: 'stub', model: null });
explain.candidates.push({ provider: 'stub', model: null, eligible: true, excluded_reason: null, cost: 0, latency: 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)
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
@@ -1600,6 +1685,11 @@ function createProviderPool({ db, registry, config, clock = { now: () => Date.no
}
}
explain.preset = { name: ctx.presetName || null, policy: ctx.presetPolicy };
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 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
// 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
// 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
@@ -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.
// 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 reserve = Number(attemptRow && attemptRow.metadata && attemptRow.metadata.reasoning_reserve_tokens) || 0;
const requestedMax = Number(req.maxTokens);
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)
&& ['chat', 'generate', 'summarize', 'classify', 'extract', 'enrich', 'complete', 'messages'].includes(operation)
? requestedMax + reserve
? requestedMax + reserve
: req.maxTokens;
: 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 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;
const routed = typeof ctx.onRoute === 'function' ? await ctx.onRoute(attemptFacts, attemptMeta) : null;
if (routed && routed.skip) {
if (routed && routed.skip) {
new file mode 100644
new file mode 100644
@@ -0,0 +1,106 @@
@@ -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());