openvibers/openvibe.ai Commits
Publish weekly real-traffic share and attach provider fidelity to offers (B8/B5) (#421)
Changes
@@@ -69,3 +69,18 @@ per minute. Every known metadata field carries its own source URL. Provider-pagefacts take precedence over models.dev. Tests disable live refresh and injectrecorded responses. The market projects public fields explicitly; credentials,account ids, internal provider metadata and price-change actors are never returned.++++## Fidelity and real-traffic share++++Each offer in the market feed and the catalog carries a `fidelity` field when OpenVibe.Bench has measured that exact++endpoint on its fixed objective set: `{score, n, interval, compared_to, measured_at, prompt_set}`. AI reads Bench's++public `GET /api/v1/fidelity` (fail-soft, five-minute cache) and never measures or invents the number itself; when++Bench has no measurement, or is unreachable, the field is null and the page says "not measured", never zero.++++`GET /api/v1/exchange/share` publishes the week's real-traffic share by model and app category, from the Exchange++receipts of a completed UTC week Monday to Monday. It is aggregate only: a cell needs at least `k` requests++(`AI_TRAFFIC_SHARE_K`, floor 20) to be published; smaller cells are withheld and counted in `withheld`. Runs whose++person turned `history` off are excluded, as are failed, cached and synthetic runs; no prompt, answer, person or++developer app is named (every app folds into the category `apps`). The first read stores the week in++`exchange_traffic_share` and later reads return the stored copy, so the number survives receipt retention. Tests++replay receipts into a real test database; no live call is made. Migration 0063 is expand-only.nnew file mode 100644@@@ -0,0 +1,24 @@++-- phase: expand++-- Real-traffic share (plan 2026-11-09, stage B8).++--++-- One row per completed UTC week: the share of requests and tokens each model and each app category++-- carried, read from the Exchange receipts. Only cells with at least k requests are published; a cell++-- below k is withheld and only counted in the withheld totals. Receipts are pruned with their runs, so++-- the weekly row is the durable copy: it stays after the receipts it was computed from are gone.++--++-- payload is the exact public answer (aggregate counts and shares, never a person, an app's name or a++-- prompt). recomputed_at is the last time the same week was recomputed (before the receipts aged out).++-- The weekly aggregate scans the last seven days of receipts; that read gets its own index.++CREATE INDEX IF NOT EXISTS ai_receipts_created_idx ON ai_receipts (created_at);++++CREATE TABLE IF NOT EXISTS exchange_traffic_share (++ week_start date PRIMARY KEY, -- Monday 00:00 UTC++ week_end date NOT NULL, -- the next Monday, exclusive++ k integer NOT NULL CHECK (k >= 1), -- the minimum requests per published cell++ payload jsonb NOT NULL,++ cells integer NOT NULL DEFAULT 0, -- cells before the k rule++ published_cells integer NOT NULL DEFAULT 0,++ withheld_cells integer NOT NULL DEFAULT 0,++ computed_at timestamptz NOT NULL DEFAULT now(),++ recomputed_at timestamptz++);@@@ -35,8 +35,16 @@ function memo(fn, ttlMs, clock) {};}--function exchangeRouter({ db, registry, pool, quotas = null, env = process.env, clock = { now: () => Date.now() }, config = null, presets, receipts, auth, log = console, ownEndpoints = null }) {-- const catalog = createCatalog({ db, registry, pool, env, clock, config });++function exchangeRouter({ db, registry, pool, quotas = null, env = process.env, clock = { now: () => Date.now() }, config = null, presets, receipts, auth, log = console, ownEndpoints = null, fetchImpl = globalThis.fetch, fidelity = null, share = null }) {++ // Bench's provider-fidelity read (stage B5): optional and fail-soft. Off in tests, where no Bench runs.++ const fidelitySource = fidelity || require('../exchange/fidelity').createFidelitySource({++ url: config && config.bench ? config.bench.url : null, fetchImpl, clock, log,++ timeoutMs: 2500, enabled: !config || config.nodeEnv !== 'test',++ });++ const catalog = createCatalog({ db, registry, pool, env, clock, config, fidelity: fidelitySource });++ const trafficShare = share || require('../exchange/share').createShare({++ db, clock, log, k: config && config.trafficShare ? config.trafficShare.k : 20,++ });const usage = createUsage({ db, clock });// The price book's log: what a price was, what it became and when it was read.const priceStore = require('../pricebook/store').createPriceStore({ db, clock });@@@ -48,7 +56,7 @@ function exchangeRouter({ db, registry, pool, quotas = null, env = process.env,return hits.get(key)();};const all = () => allFor(null);-- const market = require('../exchange/market').createMarket({ db, registry, catalog: all, config, clock });++ const market = require('../exchange/market').createMarket({ db, registry, catalog: all, config, clock, fidelity: fidelitySource });const usageByWindow = new Map();const r = express.Router();@@@ -244,6 +252,20 @@ function exchangeRouter({ db, registry, pool, quotas = null, env = process.env,} catch (err) { return next(err); }});++ // Stage B8: the share of requests and tokens each model and each app category carried in a completed++ // week. Aggregated only: cells below k requests are withheld, opt-outs are excluded, and no person, app++ // or prompt is ever named.++ r.get('/api/v1/exchange/share', publicCache(300), async (req, res, next) => {++ try {++ const week = req.query.week == null || req.query.week === '' ? null : String(req.query.week);++ const out = await trafficShare.read({ week });++ return res.json(out);++ } catch (err) {++ if (err && err.name === 'ShareError') return http.sendProblem(res, err.status, err.code, { detail: err.detail, ctx: req.ov });++ return next(err);++ }++ });++r.get('/api/v1/exchange/usage', publicCache(60), async (req, res, next) => {try {const days = Math.max(1, Math.min(366, Number.parseInt(req.query.days, 10) || 30));@@@ -22,7 +22,7 @@ const pkg = require('../package.json');const { adminRoutes } = require('openvibe-shared/config');const ownerInputs = require('./owner-inputs');--function createApp({ choices = null, searchFetch = null, config, db, registry, pool, quotas, cache, runs, auth, keys, env = process.env, clock = { now: () => Date.now() }, fetchImpl = globalThis.fetch, log = console, credentials = null, preferences = null, grants = null, vault = null, settings = null, benchEvents = null, runBudgets = null, memory = null, memoryImport = null, memoryServices = null, memorySharing = null, community = null, endpoints = null, webhooks = null, presets = null, receipts = null, balance = null, creditGrants = null, sessionCookies = null, gpus = null, gpuMarket = null, abuse = null, priceBook = null, ownEndpoints = null, refusal = null, batches = null }) {++function createApp({ choices = null, searchFetch = null, config, db, registry, pool, quotas, cache, runs, auth, keys, env = process.env, clock = { now: () => Date.now() }, fetchImpl = globalThis.fetch, log = console, credentials = null, preferences = null, grants = null, vault = null, settings = null, benchEvents = null, runBudgets = null, memory = null, memoryImport = null, memoryServices = null, memorySharing = null, community = null, endpoints = null, webhooks = null, presets = null, receipts = null, balance = null, creditGrants = null, sessionCookies = null, gpus = null, gpuMarket = null, abuse = null, priceBook = null, ownEndpoints = null, refusal = null, batches = null, share = null }) {const app = express();// /.well-known/agent.json (openvibe-shared/agent-layer, SEO-2): what this site serves, for agents.require('openvibe-shared/agent-layer').mount(app, { descriptor: require('./agent-descriptor') });@@@ -109,7 +109,7 @@ function createApp({ choices = null, searchFetch = null, config, db, registry, papp.use(exchangePlatform.router);// OpenVibe.Exchange's read API (server/api/exchange.js): the public catalog and aggregate usage. Public on// purpose — the site and anyone comparing prices reads the same numbers the router ranks on.-- app.use(require('./api/exchange').exchangeRouter({ config, db, registry, pool, quotas, env, clock, presets, receipts, auth, log, ownEndpoints }));++ app.use(require('./api/exchange').exchangeRouter({ config, db, registry, pool, quotas, env, clock, presets, receipts, auth, log, ownEndpoints, fetchImpl, share }));// A person's own endpoint (the Exchange remake, product line 6): the private rent's person API and its// OpenAI-compatible proxy. Only the owner's own Network user token reaches it; a service token never does.app.use((req,_res,next) => { if (/^\/api\/v1\/exchange\/deployments(?:\/|$)/.test(req.path)) req.url = req.url.replace('/api/v1/exchange/deployments','/api/v1/own-endpoints'); next(); });@@@ -102,6 +102,11 @@ function load(env = process.env) {url: trimUrl(env.AI_BENCH_URL || env.OV_BENCH_INTERNAL_URL || env.OV_BENCH_URL || env.BENCH_URL || 'http://127.0.0.1:5230'),timeoutMs: int(env.AI_BENCH_TIMEOUT_MS, 15000),},++ // Real-traffic share by model and app category (stage B8). The k rule is a floor: a cell below it++ // is never published, whatever the environment says.++ trafficShare: {++ k: Math.max(20, int(env.AI_TRAFFIC_SHARE_K, 20)),++ },// PostgreSQL (ADR-035): DATABASE_URL serves (PgBouncer), DATABASE_DIRECT_URL migrates (owner role).db: { url: env.DATABASE_URL || '', directUrl: env.DATABASE_DIRECT_URL || '' },@@@ -116,7 +116,7 @@ const laterOf = (a, b) => {return x > y ? x : y;};--function createCatalog({ db, registry, pool, env = process.env, clock = { now: () => Date.now() }, config = null }) {++function createCatalog({ db, registry, pool, env = process.env, clock = { now: () => Date.now() }, config = null, fidelity = null }) {// The free-tier admission rules are the router's (server/free-tier.js): the catalog marks an offer with what// the free allowance and the anonymous Try it may actually use. config.freeUse is fixed at boot, like the// provider keys and upstreams the rest of this read shows.@@@ -146,6 +146,7 @@ function createCatalog({ db, registry, pool, env = process.env, clock = { now: (const now = clock.now();const providers = await registry.listProviders();const byKey = new Map(providers.map((p) => [p.key, p]));++ const fidelityMap = fidelity ? await fidelity.read() : new Map();// The router's breaker state per provider (server/providers/index.js): an offer whose provider answered// "insufficient balance" is exhausted until a call succeeds again, and the catalog says so rather than// promising free capacity that cannot answer.@@@ -229,6 +230,9 @@ function createCatalog({ db, registry, pool, env = process.env, clock = { now: (refusal_rate: refusal && refusal.refusal_rate != null ? Number(refusal.refusal_rate) : null,refusal_measured_on: refusal ? (String(refusal.measured_on || '').slice(0, 10) || null) : null,refusal_prompt_set: refusal ? (refusal.prompt_set || null) : null,++ // Bench's measured provider fidelity for this exact endpoint: score, n, interval, the model++ // it was compared on and when. Null when nobody measured it (missing is not zero).++ fidelity: fidelityMap.get(`${String(up.key).toLowerCase()}\0${String(row.model_key).toLowerCase()}`) || null,updated_at: row.updated_at || provider.updated_at || null,};if (!groups.has(slug)) {nnew file mode 100644@@@ -0,0 +1,80 @@++'use strict';++++/**++ * Provider fidelity on an Exchange offer (stage B5).++ *++ * OpenVibe.Bench measures the same fixed objective set through every provider that serves an open-weight++ * model and publishes the latest score at GET /api/v1/fidelity. This is a read-through of that public++ * answer, keyed by the provider and exact endpoint: AI never measures fidelity itself and never invents++ * a score. Bench being down (or not configured) leaves the field null, which the catalog renders as "not++ * measured", never as zero.++ *++ * The read is cached in this process for five minutes; a failed read keeps the last good answer for the++ * same window, and a first failure is retried after a minute rather than on every page view.++ */++const CACHE_MS = 5 * 60 * 1000;++const FAIL_MS = 60 * 1000;++++const keyOf = (provider, model) => `${String(provider || '').toLowerCase()}\0${String(model || '').toLowerCase()}`;++const num = (v) => (v == null || v === '' || !Number.isFinite(Number(v)) ? null : Number(v));++++function createFidelitySource({ url = null, fetchImpl = globalThis.fetch, clock = { now: () => Date.now() }, log = console, timeoutMs = 2500, enabled = true } = {}) {++ const base = String(url || '').replace(/\/+$/, '');++ const live = enabled && Boolean(base) && typeof fetchImpl === 'function';++ let hit = null;++ let pending = null;++++ async function load() {++ if (!live) return new Map();++ let body;++ try {++ const res = await fetchImpl(`${base}/api/v1/fidelity`, {++ headers: { Accept: 'application/json' },++ signal: AbortSignal.timeout(Math.max(250, Math.min(5000, Number(timeoutMs) || 2500))),++ });++ if (!res.ok) throw new Error(`Bench answered ${res.status}`);++ body = await res.json();++ } catch (err) {++ log.warn('[exchange] fidelity read failed: ' + String((err && err.message) || err).slice(0, 200));++ return null;++ }++ const map = new Map();++ for (const offer of (Array.isArray(body && body.offers) ? body.offers : [])) {++ const provider = String((offer && offer.provider) || '').trim();++ const model = String((offer && (offer.provider_model || offer.model)) || '').trim();++ const score = num(offer && offer.score);++ const n = num(offer && offer.n);++ if (!provider || !model || score == null || n == null || n <= 0) continue;++ const interval = offer.interval && num(offer.interval.lower) != null && num(offer.interval.upper) != null++ ? { lower: num(offer.interval.lower), upper: num(offer.interval.upper) }++ : null;++ map.set(keyOf(provider, model), {++ score,++ n,++ interval,++ compared_to: (offer && (offer.compared_to || offer.model)) || null,++ measured_at: (offer && offer.measured_at) || null,++ prompt_set: (offer && offer.prompt_set) || (body && body.objective_set) || null,++ });++ }++ return map;++ }++++ /** The fidelity map for this snapshot. Never throws: the market/catalog answer without the field. */++ async function read() {++ if (!live) return new Map();++ if (hit && clock.now() - hit.at < (hit.ok ? CACHE_MS : FAIL_MS)) return hit.value;++ if (pending) return pending;++ pending = load()++ .then((map) => {++ hit = { at: clock.now(), value: map || hit?.value || new Map(), ok: Boolean(map) };++ return hit.value;++ })++ .finally(() => { pending = null; });++ return pending;++ }++++ return { read, enabled: live, url: base || null, keyOf };++}++++module.exports = { createFidelitySource, keyOf, CACHE_MS, FAIL_MS };@@@ -8,7 +8,7 @@ const recordKey = (provider, model) => `${provider}\0${String(model).toLowerCaseconst num = (v) => v == null || v === '' || !Number.isFinite(Number(v)) || Number(v) < 0 ? null : Number(v);const webUrl = (v) => typeof v === 'string' && /^https:\/\//.test(v) ? v : null;--function buildOffers({ providers = [], rows = [], changes = [], catalogue = {}, metadata = {}, observations = [], presets = PROVIDERS }) {++function buildOffers({ providers = [], rows = [], changes = [], catalogue = {}, metadata = {}, observations = [], presets = PROVIDERS, fidelity = null }) {const publicKeys = new Set([...presets.map(p => p.key), ...SOURCES.filter(s => s.role === 'own').map(s => s.provider)]);const byProvider = new Map(providers.filter(p => publicKeys.has(p.key)).map((p) => [p.key, p]));const seeds = new Map();@@@ -58,6 +58,9 @@ function buildOffers({ providers = [], rows = [], changes = [], catalogue = {},routed: routed.some(o => o.provider === p.key && String(o.model).toLowerCase() === String(row.model_key).toLowerCase()),uncensored: seed?.m.uncensored === true || presets.find(preset => preset.key === p.key)?.uncensored === true,provider_access: p.provider_access || 'UNVERIFIED', field_sources: {},++ // The measured provider fidelity from Bench, when Bench has measured this exact endpoint;++ // absent (null) when nobody measured it, never a zero.++ fidelity: fidelity && typeof fidelity.get === 'function' ? (fidelity.get(recordKey(p.key, row.model_key)) || null) : null,};// Pending price changes are the latest observed own-page price, even before the router// approves charging it. Never publish the change log's actor, note or account data.@@@ -107,20 +110,21 @@ function pageMarket(snapshot, query = {}) {offers: offers.slice(offset, offset + limit), next_cursor: offset + limit < offers.length ? String(offset + limit) : null };}--function createMarket({ db, registry, catalog, config, clock = { now: Date.now }, metadata = null }) {++function createMarket({ db, registry, catalog, config, clock = { now: Date.now }, metadata = null, fidelity = null }) {const source = metadata || createModelMetadata({ db, now: () => clock.now(), enabled: config?.nodeEnv !== 'test' && process.env.NODE_ENV !== 'test' });let hit = null; let pending = null;async function snapshot() {if (hit && clock.now() < hit.until) return hit.value;if (!pending) pending = (async () => {-- const [providers, rows, changes, catalogue, meta, observations] = await Promise.all([++ const [providers, rows, changes, catalogue, meta, observations, fidelityMap] = await Promise.all([registry.listProviders(), registry.listModels(),db.prepare('SELECT provider_key, model_key, field, new_price, source, checked_at FROM price_changes ORDER BY checked_at DESC').all(),catalog(), source.read(),db.prepare('SELECT provider_key, model_key, price, source, checked_at FROM exchange_market_prices').all(),++ fidelity ? fidelity.read() : Promise.resolve(new Map()),]);const value = { generated_at: new Date(clock.now()).toISOString(), metadata: { source: URL, checked_at: meta.checked_at, stale: meta.stale },-- offers: buildOffers({ providers, rows, changes, catalogue, metadata: meta, observations }) };++ offers: buildOffers({ providers, rows, changes, catalogue, metadata: meta, observations, fidelity: fidelityMap }) };hit = { value, until: clock.now() + TTL_MS }; return value;})().finally(() => { pending = null; });return pending;nnew file mode 100644@@@ -0,0 +1,239 @@++'use strict';++++/**++ * Real-traffic share (plan 2026-11-09, stage B8).++ *++ * Which models real requests and tokens actually carried, per model and per app category, from the++ * Exchange receipts. It is an aggregate or it is nothing: no prompt, no answer, no person, no app's++ * name. Every cell needs at least k requests before it is published; a smaller cell is withheld and++ * counted only in the withheld total, so a category with one user cannot be read back from the shares.++ *++ * The week is a completed UTC week (Monday 00:00 to the next Monday). The answer is stored once per++ * week: receipts are pruned with their runs, and the stored row is the durable public record. The++ * last completed week is computed on first read; a past week is only recomputed while its receipts are++ * still in the retention window (never a silent empty week after they aged out).++ *++ * "Category" is the first-party service whose run answered (a product), or `apps` for every developer++ * app together. Named developer apps would be per-app data, so they are never named; the network's++ * own moderation requests are not Exchange traffic and are not counted.++ */++const DAY_MS = 86400000;++const WEEK_MS = 7 * DAY_MS;++const K_DEFAULT = 20;++const RETENTION_DAYS = 30;++const SCHEMA = 'exchange.traffic-share@1';++++class ShareError extends Error {++ constructor(status, code, detail) {++ super(detail);++ this.name = 'ShareError';++ this.status = status;++ this.code = code;++ this.detail = detail;++ }++}++++const round = (v) => Math.round(Number(v) * 1e6) / 1e6;++const int = (v) => Math.max(0, Math.round(Number(v) || 0));++const isoDay = (ms) => new Date(ms).toISOString().slice(0, 10);++const dayString = (v) => (v instanceof Date ? isoDay(v.getTime()) : String(v || '').slice(0, 10));++++/** The Monday 00:00 UTC of the week a moment is in. */++function mondayOf(ms) {++ const d = new Date(ms);++ const offset = (d.getUTCDay() + 6) % 7;++ return Date.UTC(d.getUTCFullYear(), d.getUTCMonth(), d.getUTCDate() - offset);++}++++/** The last completed week: this week's Monday minus seven days. */++function lastCompleteWeek(ms) {++ return mondayOf(ms) - WEEK_MS;++}++++function parseWeek(day) {++ const s = String(day || '');++ if (!/^\d{4}-\d{2}-\d{2}$/.test(s)) throw new ShareError(422, 'traffic_share.week_invalid', 'week must be a YYYY-MM-DD Monday');++ const ms = Date.parse(`${s}T00:00:00.000Z`);++ if (!Number.isFinite(ms) || new Date(ms).getUTCDay() !== 1) throw new ShareError(422, 'traffic_share.week_invalid', 'week must be a Monday 00:00 UTC');++ return ms;++}++++/** A first-party service keeps its product name; every developer app folds into one public category. */++function categoryOf(row) {++ if (row.requester_type === 'app') return 'apps';++ if (row.requester_type === 'service' && /^[a-z0-9][a-z0-9._-]{0,63}$/i.test(String(row.requester_id || ''))) return String(row.requester_id);++ return 'other';++}++++const shareOf = (n, total) => (total > 0 ? round(n / total) : null);++++function aggregate(cells, keyFields, keyOf) {++ const map = new Map();++ for (const cell of cells) {++ const key = keyOf(cell);++ const row = map.get(key) || { ...Object.fromEntries(keyFields.map((f) => [f, cell[f]])), requests: 0, tokens_in: 0, tokens_out: 0 };++ row.requests += cell.requests;++ row.tokens_in += cell.tokens_in;++ row.tokens_out += cell.tokens_out;++ map.set(key, row);++ }++ return [...map.values()];++}++++/**++ * Read the receipts of one completed week and publish what meets the k rule. Pure over the rows passed++ * to it, so a test can replay a small fixture without a database or a paid call.++ */++function aggregateWeek({ rows = [], weekStart, k = K_DEFAULT, generatedAt = new Date().toISOString() } = {}) {++ const start = typeof weekStart === 'number' ? weekStart : parseWeek(weekStart);++ const cells = new Map();++ let allRequests = 0;++ let allTokens = 0;++ for (const row of rows) {++ const provider = String(row.provider || '').trim();++ const model = String(row.model || '').trim();++ if (!provider || !model) continue;++ const category = categoryOf(row);++ const key = `${category}\0${provider}\0${model}`;++ const cell = cells.get(key) || { category, provider, model, requests: 0, tokens_in: 0, tokens_out: 0 };++ cell.requests += int(row.requests);++ cell.tokens_in += int(row.tokens_in);++ cell.tokens_out += int(row.tokens_out);++ cells.set(key, cell);++ allRequests += int(row.requests);++ allTokens += int(row.tokens_in) + int(row.tokens_out);++ }++ const all = [...cells.values()];++ const published = all.filter((c) => c.requests >= Math.max(1, Number(k) || K_DEFAULT));++ const withheld = all.filter((c) => c.requests < Math.max(1, Number(k) || K_DEFAULT));++ const requests = published.reduce((n, c) => n + c.requests, 0);++ const tokens = published.reduce((n, c) => n + c.tokens_in + c.tokens_out, 0);++ const tokensIn = published.reduce((n, c) => n + c.tokens_in, 0);++ for (const cell of published) cell.tokens = cell.tokens_in + cell.tokens_out;++ const offers = aggregate(published, ['provider', 'model'], (c) => `${c.provider}\0${c.model}`)++ .map((r) => ({++ provider: r.provider, model: r.model, requests: r.requests,++ tokens_in: r.tokens_in, tokens_out: r.tokens_out, tokens: r.tokens_in + r.tokens_out,++ request_share: shareOf(r.requests, requests), token_share: shareOf(r.tokens_in + r.tokens_out, tokens),++ }))++ .sort((a, b) => b.requests - a.requests || b.tokens - a.tokens || a.model.localeCompare(b.model) || a.provider.localeCompare(b.provider));++ const models = aggregate(published, ['model'], (c) => c.model)++ .map((r) => {++ const providers = new Set(published.filter((c) => c.model === r.model).map((c) => c.provider)).size;++ return { model: r.model, providers, requests: r.requests, tokens_in: r.tokens_in, tokens_out: r.tokens_out,++ tokens: r.tokens_in + r.tokens_out, request_share: shareOf(r.requests, requests), token_share: shareOf(r.tokens_in + r.tokens_out, tokens) };++ })++ .sort((a, b) => b.requests - a.requests || b.tokens - a.tokens || a.model.localeCompare(b.model));++ const categories = aggregate(published, ['category'], (c) => c.category)++ .map((r) => ({ category: r.category, requests: r.requests, tokens_in: r.tokens_in, tokens_out: r.tokens_out,++ tokens: r.tokens_in + r.tokens_out, request_share: shareOf(r.requests, requests), token_share: shareOf(r.tokens_in + r.tokens_out, tokens) }))++ .sort((a, b) => b.requests - a.requests || b.tokens - a.tokens || a.category.localeCompare(b.category));++ const withheldRequests = withheld.reduce((n, c) => n + c.requests, 0);++ const withheldTokens = withheld.reduce((n, c) => n + c.tokens_in + c.tokens_out, 0);++ return {++ $schema: SCHEMA,++ week_start: isoDay(start),++ week_end: isoDay(start + WEEK_MS),++ generated_at: generatedAt,++ k: Math.max(1, Number(k) || K_DEFAULT),++ totals: { requests, tokens_in: tokensIn, tokens_out: tokens - tokensIn, tokens },++ offers,++ models,++ categories,++ withheld: { cells: withheld.length, requests: withheldRequests, tokens: withheldTokens, share_of_requests: shareOf(withheldRequests, allRequests) },++ source: 'Exchange receipts: succeeded, non-synthetic requests that a provider answered.',++ note: published.length++ ? 'Only cells with at least k requests are published. Every developer app is the single category apps.'++ : 'No model and category reached k requests this week, so nothing is published.',++ };++}++++function createShare({ db, clock = { now: () => Date.now() }, k = K_DEFAULT, log = console } = {}) {++ const minimum = Math.max(1, Number(k) || K_DEFAULT);++ const byCell = db.prepare(`SELECT r.requester_type, r.requester_id,++ COALESCE(r.body -> 'offer' ->> 'provider', '') AS provider,++ COALESCE(r.body -> 'offer' ->> 'model', '') AS model,++ COUNT(*)::bigint AS requests,++ COALESCE(SUM(NULLIF(r.body -> 'usage' ->> 'tokens_in', '')::bigint), 0) AS tokens_in,++ COALESCE(SUM(NULLIF(r.body -> 'usage' ->> 'tokens_out', '')::bigint), 0) AS tokens_out++ FROM ai_receipts r++ JOIN runs u ON u.id = r.run_id++ WHERE r.created_at >= ?::timestamptz AND r.created_at < ?::timestamptz++ AND r.requester_type IN ('service', 'app')++ AND u.status = 'succeeded' AND u.synthetic = 0 AND u.provider_key IS NOT NULL++ AND COALESCE(u.options::jsonb ->> 'history', 'true') <> 'false'++ AND NULLIF(r.body -> 'offer' ->> 'provider', '') IS NOT NULL++ AND NULLIF(r.body -> 'offer' ->> 'model', '') IS NOT NULL++ GROUP BY 1, 2, 3, 4`);++ const insert = db.prepare(`INSERT INTO exchange_traffic_share++ (week_start, week_end, k, payload, cells, published_cells, withheld_cells, computed_at)++ VALUES (?::date, ?::date, ?, ?::jsonb, ?, ?, ?, now())++ ON CONFLICT (week_start) DO UPDATE SET++ k = EXCLUDED.k, payload = EXCLUDED.payload, cells = EXCLUDED.cells,++ published_cells = EXCLUDED.published_cells, withheld_cells = EXCLUDED.withheld_cells,++ recomputed_at = now()`);++ const stored = db.prepare('SELECT week_start, payload, computed_at, recomputed_at FROM exchange_traffic_share WHERE week_start = ?::date');++ const latest = db.prepare('SELECT week_start, payload, computed_at, recomputed_at FROM exchange_traffic_share ORDER BY week_start DESC LIMIT 1');++++ const decode = (row) => ({++ ...(row.payload && typeof row.payload === 'object' ? row.payload : JSON.parse(row.payload || '{}')),++ week_start: dayString(row.week_start),++ computed_at: row.computed_at instanceof Date ? row.computed_at.toISOString() : String(row.computed_at || ''),++ recomputed_at: row.recomputed_at ? (row.recomputed_at instanceof Date ? row.recomputed_at.toISOString() : String(row.recomputed_at)) : null,++ stored: true,++ });++++ async function compute(weekStart) {++ const start = isoDay(weekStart);++ const end = isoDay(weekStart + WEEK_MS);++ const rows = await byCell.all(`${start}T00:00:00.000Z`, `${end}T00:00:00.000Z`);++ return aggregateWeek({ rows, weekStart, k: minimum, generatedAt: new Date(clock.now()).toISOString() });++ }++++ async function store(payload) {++ const withheld = payload.withheld ? payload.withheld.cells : 0;++ const published = payload.offers.length;++ const allCells = published + withheld;++ await insert.run(payload.week_start, payload.week_end, payload.k, JSON.stringify(payload), allCells, published, withheld);++ }++++ async function storedWeek(weekStart) {++ const row = await stored.get(isoDay(weekStart));++ return row ? decode(row) : null;++ }++++ /**++ * The public answer. With no week: the last completed week (computed and stored on first read, so the++ * publication exists without a timer). With a week: that week, while its receipts are still kept.++ */++ async function read({ week = null } = {}) {++ const now = clock.now();++ const current = mondayOf(now);++ if (week) {++ const start = parseWeek(week);++ if (start >= current) throw new ShareError(422, 'traffic_share.week_future', 'that week has not finished yet');++ const known = await storedWeek(start);++ if (known) return known;++ if (now - start > RETENTION_DAYS * DAY_MS) throw new ShareError(404, 'traffic_share.expired', 'the receipts for that week are no longer kept; only the weeks already stored are available');++ const payload = await compute(start);++ await store(payload);++ return { ...payload, computed_at: new Date(clock.now()).toISOString(), recomputed_at: null, stored: true };++ }++ const target = lastCompleteWeek(now);++ const row = await latest.get();++ if (row && dayString(row.week_start) === isoDay(target)) return decode(row);++ const payload = await compute(target);++ await store(payload);++ return { ...payload, computed_at: new Date(clock.now()).toISOString(), recomputed_at: null, stored: true };++ }++++ /** The hourly housekeeping pass: publish the last completed week even when nobody has read it. */++ async function ensure() {++ const out = await read();++ return { week_start: out.week_start, stored: Boolean(out.stored) };++ }++++ return { read, ensure, compute, store, aggregateWeek };++}++++module.exports = { createShare, aggregateWeek, categoryOf, mondayOf, lastCompleteWeek, parseWeek, ShareError, SCHEMA, K_DEFAULT, RETENTION_DAYS };@@@ -122,6 +122,9 @@ async function start({ searchFetch = null, config, db: givenDb = null, clock = {// The Exchange's routing presets and the receipts of the answered requests (server/exchange/).const presets = require('./exchange/presets').createPresets({ db, clock, log });const receipts = require('./exchange/receipts').createReceipts({ db, clock, log });++ // The weekly real-traffic share (stage B8): the public read stores the week; the housekeeping pass++ // publishes it even when nobody reads it, before the receipts it came from are pruned.++ const share = require('./exchange/share').createShare({ db, clock, log, k: config.trafficShare.k });const choices = require('./choices').createChoices({ db, registry, preferences, credentials, machines, adultStatus, ratings: require('./choices/ratings').createRatings({ fetchImpl, clock }), clock, catalog: require('./exchange/catalog').createCatalog({ db, registry, pool, config, env, clock }) });await choices.seed();const projectSecrets = require('./project-secrets').createProjectSecrets({ config, env, fetchImpl });@@@ -208,7 +211,7 @@ async function start({ searchFetch = null, config, db: givenDb = null, clock = {clientSecret: env.OV_OAUTH_CLIENT_SECRET, fetch: fetchImpl,}) : null;const benchEvents = createBenchEvents({ bench, accountData, accountSend, config, log, fetchImpl, clientSecret: env.OV_OAUTH_CLIENT_SECRET });-- const app = createApp({ choices, searchFetch, config, db, registry, pool, quotas, cache, runs, auth, keys, env, clock, fetchImpl, log, credentials, preferences, grants, vault, settings, benchEvents, runBudgets, memory, memoryImport, memoryServices, memorySharing, community, endpoints, webhooks, presets, receipts, balance, creditGrants, sessionCookies, gpus, abuse, priceBook, ownEndpoints, refusal, batches, gpuMarket });++ const app = createApp({ choices, searchFetch, config, db, registry, pool, quotas, cache, runs, auth, keys, env, clock, fetchImpl, log, credentials, preferences, grants, vault, settings, benchEvents, runBudgets, memory, memoryImport, memoryServices, memorySharing, community, endpoints, webhooks, presets, receipts, balance, creditGrants, sessionCookies, gpus, abuse, priceBook, ownEndpoints, refusal, batches, gpuMarket, share });let jwks = null;let keyLoaded = Promise.resolve(null);if (!keys.pinned && config.nodeEnv !== 'test') {@@@ -228,6 +231,7 @@ async function start({ searchFetch = null, config, db: givenDb = null, clock = {if (pruned || expired || lapsed) log.log(`[ai] retention: ${pruned} old run(s), ${expired} expired cache entr${expired === 1 ? 'y' : 'ies'}, ${lapsed} unsent usage reading(s)`);if (Object.values(abusePruned).some((n) => n)) log.log(`[ai] abuse retention: ${Object.entries(abusePruned).filter(([, n]) => n).map(([k, n]) => `${n} ${k}`).join(', ')}`);} catch (err) { log.error(`[ai] retention failed: ${err.message}`); }++ try { await share.ensure(); } catch (err) { log.warn(`[ai] traffic share failed: ${err.message}`); }}, 60 * 60 * 1000);housekeeping.unref?.();@@@ -454,6 +454,7 @@ const exchangeOffer = {refusal_rate: nullable('number', { minimum: 0, maximum: 1, description: 'The share of the published probe set this offer refused (0.75 is three probes out of four). null when nobody measured this offer, never 0.' }),refusal_measured_on: nullable('string', { description: 'The day the refusal rate was measured' }),refusal_prompt_set: nullable('string', { description: 'The version of the published probe set the rate was measured with' }),++ fidelity: nullable('object', { description: 'Provider fidelity measured by OpenVibe.Bench: the same fixed objective set run through every provider that serves this open-weight model. score is the pass rate, n the answered items, interval its 95% Wilson band. Null when Bench has not measured this exact endpoint.', properties: { score: { type: 'number', minimum: 0, maximum: 1 }, n: integer({ minimum: 0 }), interval: { type: ['object', 'null'], properties: { lower: nullable('number'), upper: nullable('number') } }, compared_to: nullable('string'), measured_at: nullable('string'), prompt_set: nullable('string') } }),updated_at: nullable('string'),},};@@@ -491,6 +492,7 @@ route('get', '/exchange/market', {maker: nullable('string'), context: nullable('number'), max_output: nullable('number'),open_weights: nullable('boolean'), licence: nullable('string'), release_date: nullable('string'), knowledge_cutoff: nullable('string'),modalities: { type: ['object', 'null'], properties: { input: arrayOf(str()), output: arrayOf(str()) } },++ fidelity: nullable('object', { properties: { score: nullable('number'), n: integer(), interval: { type: ['object', 'null'], properties: { lower: nullable('number'), upper: nullable('number') } }, compared_to: nullable('string'), measured_at: nullable('string'), prompt_set: nullable('string') } }),field_sources: { type: 'object', additionalProperties: str({ format: 'uri' }) },} }),},@@@ -626,6 +628,21 @@ route('get', '/exchange/usage', {} }) },});++route('get', '/exchange/share', {++ operationId: 'exchangeTrafficShare', summary: 'Real-traffic share by model and app category, published weekly', tags: ['exchange'], security: [], servers: [{ url: 'https://api.openvibe.exchange' }],++ description: 'Public and aggregate only: never a prompt, an answer, a person or an app\'s name. Read from the Exchange receipts of a completed UTC week (succeeded, non-synthetic requests that a provider answered; runs whose person turned history off are excluded). Every cell needs at least k requests before it is published; smaller cells are withheld and counted only in withheld. Developer apps fold into one category, apps, so no app is named. The weekly answer is stored once and kept after the receipts it came from are pruned.',++ parameters: [query('week', str(), 'A Monday, YYYY-MM-DD. Default: the last completed week')],++ responses: { 200: json('The share', { type: 'object', required: ['$schema', 'week_start', 'week_end', 'k', 'totals', 'offers', 'models', 'categories', 'withheld'], properties: {++ $schema: { const: 'exchange.traffic-share@1' }, week_start: str({ format: 'date' }), week_end: str({ format: 'date' }), generated_at: str({ format: 'date-time' }),++ k: integer({ minimum: 20 }), source: str(), note: str(),++ totals: { type: 'object', properties: { requests: integer(), tokens_in: integer(), tokens_out: integer(), tokens: integer() } },++ offers: arrayOf({ type: 'object', properties: { provider: str(), model: str(), requests: integer(), tokens_in: integer(), tokens_out: integer(), tokens: integer(), request_share: nullable('number'), token_share: nullable('number') } }),++ models: arrayOf({ type: 'object', properties: { model: str(), providers: integer(), requests: integer(), tokens: integer(), request_share: nullable('number'), token_share: nullable('number') } }),++ categories: arrayOf({ type: 'object', properties: { category: str(), requests: integer(), tokens: integer(), request_share: nullable('number'), token_share: nullable('number') } }),++ withheld: { type: 'object', properties: { cells: integer(), requests: integer(), tokens: integer(), share_of_requests: nullable('number') } },++ } }), ...errors([404, 422]) },++});++route('get', '/exchange/price-changes', {operationId: 'exchangePriceChanges', summary: 'What a price was and what it became', tags: ['exchange'], security: [],description: "Public. The price changelog: every change the weekly price book made, with the old price, the new price, the provider's own listing the price was read from and the day it was read. A change that waits for the owner's approval is not a price yet and is not here.",nnew file mode 100644@@@ -0,0 +1,76 @@++'use strict';++++// Stage B5, AI's side: Bench's public fidelity score is read through once, cached, and put on the exact++// provider offer in the market feed. Bench down or unconfigured leaves the field null (not measured), never++// a zero. The test fetches a fixture; no Bench and no provider is called.++const assert = require('assert');++const { createFidelitySource, keyOf } = require('../server/exchange/fidelity');++const { buildOffers } = require('../server/exchange/market');++++const BENCH = {++ $schema: 'exchange.fidelity@1',++ objective_set: 'fidelity-2026-11-09.v1',++ items: 10,++ offers: [++ { model: 'acme/alpha-1', provider: 'acme', provider_model: 'ACME/Alpha-1', score: 0.9, n: 10, passed: 9, interval: { lower: 0.596, upper: 0.982 }, compared_to: 'acme/alpha-1', prompt_set: 'fidelity-2026-11-09.v1', measured_at: '2026-11-09T08:00:00.000Z', measured_on: '2026-11-09' },++ { model: 'acme/alpha-1', provider: 'beta', provider_model: 'beta/alpha-1', score: null, n: 0, interval: { lower: null, upper: null }, compared_to: 'acme/alpha-1', measured_at: '2026-11-09T08:00:00.000Z' },++ ],++};++++const presets = [{++ key: 'acme', display_name: 'Acme', models: [{++ model_key: 'acme/alpha-1', display_name: 'Alpha 1', type: 'chat',++ cost: { in: 1, out: 2, cached: null }, source: 'https://acme.test/prices', checked: '2026-11-01',++ }],++}];++++(async () => {++ let time = Date.parse('2026-11-09T09:00:00Z');++ let calls = 0;++ let fail = false;++ const source = createFidelitySource({++ url: 'https://bench.openvibe.services/',++ fetchImpl: async (url) => {++ calls++;++ assert.strictEqual(url, 'https://bench.openvibe.services/api/v1/fidelity');++ if (fail) throw new Error('offline');++ return new Response(JSON.stringify(BENCH), { status: 200, headers: { 'content-type': 'application/json' } });++ },++ clock: { now: () => time },++ log: { warn() {} },++ });++++ const first = await source.read();++ const score = first.get(keyOf('acme', 'ACME/Alpha-1'));++ assert(score, 'the exact endpoint is keyed case-insensitively');++ assert.strictEqual(score.score, 0.9);++ assert.strictEqual(score.n, 10);++ assert.deepStrictEqual(score.interval, { lower: 0.596, upper: 0.982 });++ assert.strictEqual(score.compared_to, 'acme/alpha-1');++ assert(!first.has(keyOf('beta', 'beta/alpha-1')), 'a row with no score is not published as a score');++++ await source.read();++ assert.strictEqual(calls, 1, 'the read is cached');++ time += 6 * 60 * 1000;++ await source.read();++ assert.strictEqual(calls, 2, 'the cache expires after five minutes');++ fail = true;++ time += 6 * 60 * 1000;++ const afterFailure = await source.read();++ assert.strictEqual(afterFailure.get(keyOf('acme', 'acme/alpha-1')).score, 0.9, 'a failed refresh keeps the last good answer');++ assert.strictEqual(calls, 3);++++ const disabled = createFidelitySource({ url: 'https://bench.openvibe.services', fetchImpl: async () => { throw new Error('must not be called'); }, enabled: false });++ assert.strictEqual((await disabled.read()).size, 0);++++ const offers = buildOffers({ presets, fidelity: first });++ const offer = offers.find((o) => o.provider === 'acme' && o.provider_model === 'acme/alpha-1');++ assert(offer, 'the preset offer is listed');++ assert.strictEqual(offer.fidelity.score, 0.9);++ assert.strictEqual(offer.fidelity.compared_to, 'acme/alpha-1');++ assert.strictEqual(offer.fidelity.n, 10);++ const unmeasured = buildOffers({ presets });++ assert.strictEqual(unmeasured[0].fidelity, null, 'no measurement is null, never zero');++++ console.log('exchange fidelity: Bench score read once, attached to the exact offer, null when unmeasured');++})().catch((e) => { console.error(e); process.exit(1); });nnew file mode 100644@@@ -0,0 +1,125 @@++'use strict';++++// Stage B8: the weekly real-traffic share from Exchange receipts. Aggregate only, k >= 20 per cell,++// opt-outs respected, stored once per completed week. The tests replay a small week into a real++// database; no test calls a provider.++const assert = require('assert');++const { boot, request, suite } = require('./helpers');++const { aggregateWeek, mondayOf, lastCompleteWeek, categoryOf } = require('../server/exchange/share');++++const DAY_MS = 86400000;++const t = suite('exchange-traffic-share');++let h;++++let seq = 0;++const uid = (p) => `${p}_${Date.now()}_${seq++}`;++const iso = (ms) => new Date(ms).toISOString();++++async function insertRun({ id, type, requester, provider, model, created, history = true, status = 'succeeded', synthetic = 0 }) {++ await h.db.prepare(`INSERT INTO runs (id, workflow_key, workflow_version, status, requester_type, requester_id, input, input_hash,++ provider_key, model_key, synthetic, options, created_at, started_at, finished_at)++ VALUES (?, 'ai.chat', 1, ?, ?, ?, '{}', 'input-hash', ?, ?, ?, ?, ?, ?, ?)`)++ .run(id, status, type, requester, provider, model, synthetic, JSON.stringify({ history }), created, created, created);++}++async function insertReceipt({ runId, type, requester, provider, model, created, tokensIn = 100, tokensOut = 10 }) {++ const body = {++ id: `rcpt_${runId}`,++ run_id: runId,++ created_at: created,++ offer: { provider, model },++ usage: { tokens_in: tokensIn, tokens_out: tokensOut },++ };++ await h.db.prepare(`INSERT INTO ai_receipts (id, run_id, requester_type, requester_id, body, created_at)++ VALUES (?, ?, ?, ?, ?::jsonb, ?)`)++ .run(body.id, runId, type, requester, JSON.stringify(body), created);++}++++let weekStart;++async function addTraffic({ type, requester, provider, model, count, history = true, status = 'succeeded', tokensIn = 100, tokensOut = 10 }) {++ for (let i = 0; i < count; i++) {++ const id = uid('run');++ const created = iso(weekStart + (i % 4) * 6 * 3600000);++ await insertRun({ id, type, requester, provider, model, created, history, status });++ await insertReceipt({ runId: id, type, requester, provider, model, created, tokensIn, tokensOut });++ }++}++++t.test('aggregateWeek publishes only cells with k requests and folds apps into one category', () => {++ const rows = [++ { requester_type: 'service', requester_id: 'live', provider: 'deepseek', model: 'deepseek-chat', requests: 25, tokens_in: 2500, tokens_out: 250 },++ { requester_type: 'service', requester_id: 'live', provider: 'qwen', model: 'qwen2.5', requests: 5, tokens_in: 500, tokens_out: 50 },++ { requester_type: 'app', requester_id: 'prj_PRIVATE', provider: 'deepseek', model: 'deepseek-chat', requests: 20, tokens_in: 2000, tokens_out: 200 },++ { requester_type: 'service', requester_id: 'codes', provider: 'acme', model: 'acme-1', requests: 2, tokens_in: 200, tokens_out: 20 },++ ];++ const out = aggregateWeek({ rows, weekStart: Date.parse('2026-11-09T00:00:00Z'), k: 20 });++ assert.strictEqual(out.week_start, '2026-11-09');++ assert.strictEqual(out.week_end, '2026-11-16');++ assert.strictEqual(out.totals.requests, 45, 'only the published cells count');++ assert.strictEqual(out.categories.length, 2);++ assert.strictEqual(out.categories[0].category, 'live');++ assert.strictEqual(out.categories.find((c) => c.category === 'apps').requests, 20, 'every app is one category');++ assert.strictEqual(out.offers.length, 1, 'only the published provider/model cell remains');++ assert.strictEqual(out.withheld.cells, 2, 'the small service cells are withheld');++ assert.strictEqual(out.withheld.requests, 7);++ const share = out.categories.reduce((n, c) => n + c.request_share, 0);++ assert(Math.abs(share - 1) < 1e-9, 'published category shares add to one');++ assert(!JSON.stringify(out).includes('prj_PRIVATE'), 'no project or app id is published');++ assert.strictEqual(categoryOf({ requester_type: 'service', requester_id: 'live' }), 'live');++});++++t.test('boot', async () => {++ h = await boot();++ const now = Date.now();++ weekStart = lastCompleteWeek(now);++ assert.strictEqual(mondayOf(weekStart), weekStart);++});++++t.test('a completed week is stored once and the read is public and cached', async () => {++ await addTraffic({ type: 'service', requester: 'live', provider: 'deepseek', model: 'deepseek-chat', count: 25 });++ await addTraffic({ type: 'app', requester: 'prj_SECRET_APP', provider: 'deepseek', model: 'deepseek-chat', count: 20 });++ await addTraffic({ type: 'service', requester: 'codes', provider: 'acme', model: 'acme-1', count: 10 });++ // Opted out and failed traffic must not be in any number, not even the withheld total.++ await addTraffic({ type: 'service', requester: 'live', provider: 'deepseek', model: 'deepseek-chat', count: 7, history: false });++ await addTraffic({ type: 'service', requester: 'live', provider: 'deepseek', model: 'deepseek-chat', count: 4, status: 'failed' });++++ const first = await request(h.base, 'GET', '/api/v1/exchange/share');++ assert.strictEqual(first.status, 200, first.text);++ assert.strictEqual(first.headers.get('cache-control'), 'public, max-age=300');++ const body = first.body;++ assert.strictEqual(body.$schema, 'exchange.traffic-share@1');++ assert.strictEqual(body.week_start, iso(weekStart).slice(0, 10));++ assert(body.k >= 20);++ assert.strictEqual(body.totals.requests, 45, 'opt-outs and failures are excluded');++ assert.strictEqual(body.withheld.cells, 1);++ assert.strictEqual(body.withheld.requests, 10);++ assert.deepStrictEqual(body.categories.map((c) => c.category).sort(), ['apps', 'live']);++ assert.strictEqual(body.offers.find((o) => o.provider === 'deepseek').requests, 45);++ assert(!/prj_SECRET_APP/.test(first.text), 'no developer app is named');++ assert(!/run_|rcpt_|usr_/.test(first.text), 'no run, receipt or person id leaves the read');++ assert.strictEqual(body.offers.every((o) => o.request_share > 0 && o.request_share <= 1), true);++++ const second = await request(h.base, 'GET', '/api/v1/exchange/share');++ assert(second.body.recomputed_at === null);++ assert.strictEqual((await h.db.prepare('SELECT COUNT(*)::int AS n FROM exchange_traffic_share').get()).n, 1, 'the week is stored once, not recomputed on every read');++});++++t.test('a named week is validated and old receipts are not a silent empty week', async () => {++ const day = iso(weekStart).slice(0, 10);++ const stored = await request(h.base, 'GET', `/api/v1/exchange/share?week=${day}`);++ assert.strictEqual(stored.status, 200, stored.text);++ assert.strictEqual(stored.body.week_start, day);++ const tuesday = iso(weekStart + DAY_MS).slice(0, 10);++ assert.strictEqual((await request(h.base, 'GET', `/api/v1/exchange/share?week=${tuesday}`)).status, 422);++ const nextMonday = iso(mondayOf(Date.now())).slice(0, 10);++ assert.strictEqual((await request(h.base, 'GET', `/api/v1/exchange/share?week=${nextMonday}`)).status, 422);++ const old = iso(weekStart - 42 * DAY_MS).slice(0, 10);++ assert.strictEqual((await request(h.base, 'GET', `/api/v1/exchange/share?week=${old}`)).status, 404);++});++++(async () => {++ try {++ await t.run();++ } finally {++ if (h) await h.stop();++ }++})().catch((e) => { console.error(e); process.exit(1); });
@@ -69,3 +69,18 @@ per minute. Every known metadata field carries its own source URL. Provider-page
@@ -69,3 +69,18 @@ per minute. Every known metadata field carries its own source URL. Provider-page
facts take precedence over models.dev. Tests disable live refresh and inject
facts take precedence over models.dev. Tests disable live refresh and inject
recorded responses. The market projects public fields explicitly; credentials,
recorded responses. The market projects public fields explicitly; credentials,
account ids, internal provider metadata and price-change actors are never returned.
account ids, internal provider metadata and price-change actors are never returned.
+
+## Fidelity and real-traffic share
+
+Each offer in the market feed and the catalog carries a `fidelity` field when OpenVibe.Bench has measured that exact
+endpoint on its fixed objective set: `{score, n, interval, compared_to, measured_at, prompt_set}`. AI reads Bench's
+public `GET /api/v1/fidelity` (fail-soft, five-minute cache) and never measures or invents the number itself; when
+Bench has no measurement, or is unreachable, the field is null and the page says "not measured", never zero.
+
+`GET /api/v1/exchange/share` publishes the week's real-traffic share by model and app category, from the Exchange
+receipts of a completed UTC week Monday to Monday. It is aggregate only: a cell needs at least `k` requests
+(`AI_TRAFFIC_SHARE_K`, floor 20) to be published; smaller cells are withheld and counted in `withheld`. Runs whose
+person turned `history` off are excluded, as are failed, cached and synthetic runs; no prompt, answer, person or
+developer app is named (every app folds into the category `apps`). The first read stores the week in
+`exchange_traffic_share` and later reads return the stored copy, so the number survives receipt retention. Tests
+replay receipts into a real test database; no live call is made. Migration 0063 is expand-only.
new file mode 100644
new file mode 100644
@@ -0,0 +1,24 @@
@@ -0,0 +1,24 @@
+-- phase: expand
+-- Real-traffic share (plan 2026-11-09, stage B8).
+--
+-- One row per completed UTC week: the share of requests and tokens each model and each app category
+-- carried, read from the Exchange receipts. Only cells with at least k requests are published; a cell
+-- below k is withheld and only counted in the withheld totals. Receipts are pruned with their runs, so
+-- the weekly row is the durable copy: it stays after the receipts it was computed from are gone.
+--
+-- payload is the exact public answer (aggregate counts and shares, never a person, an app's name or a
+-- prompt). recomputed_at is the last time the same week was recomputed (before the receipts aged out).
+-- The weekly aggregate scans the last seven days of receipts; that read gets its own index.
+CREATE INDEX IF NOT EXISTS ai_receipts_created_idx ON ai_receipts (created_at);
+
+CREATE TABLE IF NOT EXISTS exchange_traffic_share (
+ week_start date PRIMARY KEY, -- Monday 00:00 UTC
+ week_end date NOT NULL, -- the next Monday, exclusive
+ k integer NOT NULL CHECK (k >= 1), -- the minimum requests per published cell
+ payload jsonb NOT NULL,
+ cells integer NOT NULL DEFAULT 0, -- cells before the k rule
+ published_cells integer NOT NULL DEFAULT 0,
+ withheld_cells integer NOT NULL DEFAULT 0,
+ computed_at timestamptz NOT NULL DEFAULT now(),
+ recomputed_at timestamptz
+);
@@ -35,8 +35,16 @@ function memo(fn, ttlMs, clock) {
@@ -35,8 +35,16 @@ function memo(fn, ttlMs, clock) {
};
};
}
}
-function exchangeRouter({ db, registry, pool, quotas = null, env = process.env, clock = { now: () => Date.now() }, config = null, presets, receipts, auth, log = console, ownEndpoints = null }) {
- const catalog = createCatalog({ db, registry, pool, env, clock, config });
+function exchangeRouter({ db, registry, pool, quotas = null, env = process.env, clock = { now: () => Date.now() }, config = null, presets, receipts, auth, log = console, ownEndpoints = null, fetchImpl = globalThis.fetch, fidelity = null, share = null }) {
+ // Bench's provider-fidelity read (stage B5): optional and fail-soft. Off in tests, where no Bench runs.
+ const fidelitySource = fidelity || require('../exchange/fidelity').createFidelitySource({
+ url: config && config.bench ? config.bench.url : null, fetchImpl, clock, log,
+ timeoutMs: 2500, enabled: !config || config.nodeEnv !== 'test',
+ });
+ const catalog = createCatalog({ db, registry, pool, env, clock, config, fidelity: fidelitySource });
+ const trafficShare = share || require('../exchange/share').createShare({
+ db, clock, log, k: config && config.trafficShare ? config.trafficShare.k : 20,
+ });
const usage = createUsage({ db, clock });
const usage = createUsage({ db, clock });
// The price book's log: what a price was, what it became and when it was read.
// The price book's log: what a price was, what it became and when it was read.
const priceStore = require('../pricebook/store').createPriceStore({ db, clock });
const priceStore = require('../pricebook/store').createPriceStore({ db, clock });
@@ -48,7 +56,7 @@ function exchangeRouter({ db, registry, pool, quotas = null, env = process.env,
@@ -48,7 +56,7 @@ function exchangeRouter({ db, registry, pool, quotas = null, env = process.env,
return hits.get(key)();
return hits.get(key)();
};
};
const all = () => allFor(null);
const all = () => allFor(null);
- const market = require('../exchange/market').createMarket({ db, registry, catalog: all, config, clock });
+ const market = require('../exchange/market').createMarket({ db, registry, catalog: all, config, clock, fidelity: fidelitySource });
const usageByWindow = new Map();
const usageByWindow = new Map();
const r = express.Router();
const r = express.Router();
@@ -244,6 +252,20 @@ function exchangeRouter({ db, registry, pool, quotas = null, env = process.env,
@@ -244,6 +252,20 @@ function exchangeRouter({ db, registry, pool, quotas = null, env = process.env,
} catch (err) { return next(err); }
} catch (err) { return next(err); }
});
});
+ // Stage B8: the share of requests and tokens each model and each app category carried in a completed
+ // week. Aggregated only: cells below k requests are withheld, opt-outs are excluded, and no person, app
+ // or prompt is ever named.
+ r.get('/api/v1/exchange/share', publicCache(300), async (req, res, next) => {
+ try {
+ const week = req.query.week == null || req.query.week === '' ? null : String(req.query.week);
+ const out = await trafficShare.read({ week });
+ return res.json(out);
+ } catch (err) {
+ if (err && err.name === 'ShareError') return http.sendProblem(res, err.status, err.code, { detail: err.detail, ctx: req.ov });
+ return next(err);
+ }
+ });
+
r.get('/api/v1/exchange/usage', publicCache(60), async (req, res, next) => {
r.get('/api/v1/exchange/usage', publicCache(60), async (req, res, next) => {
try {
try {
const days = Math.max(1, Math.min(366, Number.parseInt(req.query.days, 10) || 30));
const days = Math.max(1, Math.min(366, Number.parseInt(req.query.days, 10) || 30));
@@ -22,7 +22,7 @@ const pkg = require('../package.json');
@@ -22,7 +22,7 @@ const pkg = require('../package.json');
const { adminRoutes } = require('openvibe-shared/config');
const { adminRoutes } = require('openvibe-shared/config');
const ownerInputs = require('./owner-inputs');
const ownerInputs = require('./owner-inputs');
-function createApp({ choices = null, searchFetch = null, config, db, registry, pool, quotas, cache, runs, auth, keys, env = process.env, clock = { now: () => Date.now() }, fetchImpl = globalThis.fetch, log = console, credentials = null, preferences = null, grants = null, vault = null, settings = null, benchEvents = null, runBudgets = null, memory = null, memoryImport = null, memoryServices = null, memorySharing = null, community = null, endpoints = null, webhooks = null, presets = null, receipts = null, balance = null, creditGrants = null, sessionCookies = null, gpus = null, gpuMarket = null, abuse = null, priceBook = null, ownEndpoints = null, refusal = null, batches = null }) {
+function createApp({ choices = null, searchFetch = null, config, db, registry, pool, quotas, cache, runs, auth, keys, env = process.env, clock = { now: () => Date.now() }, fetchImpl = globalThis.fetch, log = console, credentials = null, preferences = null, grants = null, vault = null, settings = null, benchEvents = null, runBudgets = null, memory = null, memoryImport = null, memoryServices = null, memorySharing = null, community = null, endpoints = null, webhooks = null, presets = null, receipts = null, balance = null, creditGrants = null, sessionCookies = null, gpus = null, gpuMarket = null, abuse = null, priceBook = null, ownEndpoints = null, refusal = null, batches = null, share = null }) {
const app = express();
const app = express();
// /.well-known/agent.json (openvibe-shared/agent-layer, SEO-2): what this site serves, for agents.
// /.well-known/agent.json (openvibe-shared/agent-layer, SEO-2): what this site serves, for agents.
require('openvibe-shared/agent-layer').mount(app, { descriptor: require('./agent-descriptor') });
require('openvibe-shared/agent-layer').mount(app, { descriptor: require('./agent-descriptor') });
@@ -109,7 +109,7 @@ function createApp({ choices = null, searchFetch = null, config, db, registry, p
@@ -109,7 +109,7 @@ function createApp({ choices = null, searchFetch = null, config, db, registry, p
app.use(exchangePlatform.router);
app.use(exchangePlatform.router);
// OpenVibe.Exchange's read API (server/api/exchange.js): the public catalog and aggregate usage. Public on
// OpenVibe.Exchange's read API (server/api/exchange.js): the public catalog and aggregate usage. Public on
// purpose — the site and anyone comparing prices reads the same numbers the router ranks on.
// purpose — the site and anyone comparing prices reads the same numbers the router ranks on.
- app.use(require('./api/exchange').exchangeRouter({ config, db, registry, pool, quotas, env, clock, presets, receipts, auth, log, ownEndpoints }));
+ app.use(require('./api/exchange').exchangeRouter({ config, db, registry, pool, quotas, env, clock, presets, receipts, auth, log, ownEndpoints, fetchImpl, share }));
// A person's own endpoint (the Exchange remake, product line 6): the private rent's person API and its
// A person's own endpoint (the Exchange remake, product line 6): the private rent's person API and its
// OpenAI-compatible proxy. Only the owner's own Network user token reaches it; a service token never does.
// OpenAI-compatible proxy. Only the owner's own Network user token reaches it; a service token never does.
app.use((req,_res,next) => { if (/^\/api\/v1\/exchange\/deployments(?:\/|$)/.test(req.path)) req.url = req.url.replace('/api/v1/exchange/deployments','/api/v1/own-endpoints'); next(); });
app.use((req,_res,next) => { if (/^\/api\/v1\/exchange\/deployments(?:\/|$)/.test(req.path)) req.url = req.url.replace('/api/v1/exchange/deployments','/api/v1/own-endpoints'); next(); });
@@ -102,6 +102,11 @@ function load(env = process.env) {
@@ -102,6 +102,11 @@ function load(env = process.env) {
url: trimUrl(env.AI_BENCH_URL || env.OV_BENCH_INTERNAL_URL || env.OV_BENCH_URL || env.BENCH_URL || 'http://127.0.0.1:5230'),
url: trimUrl(env.AI_BENCH_URL || env.OV_BENCH_INTERNAL_URL || env.OV_BENCH_URL || env.BENCH_URL || 'http://127.0.0.1:5230'),
timeoutMs: int(env.AI_BENCH_TIMEOUT_MS, 15000),
timeoutMs: int(env.AI_BENCH_TIMEOUT_MS, 15000),
},
},
+ // Real-traffic share by model and app category (stage B8). The k rule is a floor: a cell below it
+ // is never published, whatever the environment says.
+ trafficShare: {
+ k: Math.max(20, int(env.AI_TRAFFIC_SHARE_K, 20)),
+ },
// PostgreSQL (ADR-035): DATABASE_URL serves (PgBouncer), DATABASE_DIRECT_URL migrates (owner role).
// PostgreSQL (ADR-035): DATABASE_URL serves (PgBouncer), DATABASE_DIRECT_URL migrates (owner role).
db: { url: env.DATABASE_URL || '', directUrl: env.DATABASE_DIRECT_URL || '' },
db: { url: env.DATABASE_URL || '', directUrl: env.DATABASE_DIRECT_URL || '' },
@@ -116,7 +116,7 @@ const laterOf = (a, b) => {
@@ -116,7 +116,7 @@ const laterOf = (a, b) => {
return x > y ? x : y;
return x > y ? x : y;
};
};
-function createCatalog({ db, registry, pool, env = process.env, clock = { now: () => Date.now() }, config = null }) {
+function createCatalog({ db, registry, pool, env = process.env, clock = { now: () => Date.now() }, config = null, fidelity = null }) {
// The free-tier admission rules are the router's (server/free-tier.js): the catalog marks an offer with what
// The free-tier admission rules are the router's (server/free-tier.js): the catalog marks an offer with what
// the free allowance and the anonymous Try it may actually use. config.freeUse is fixed at boot, like the
// the free allowance and the anonymous Try it may actually use. config.freeUse is fixed at boot, like the
// provider keys and upstreams the rest of this read shows.
// provider keys and upstreams the rest of this read shows.
@@ -146,6 +146,7 @@ function createCatalog({ db, registry, pool, env = process.env, clock = { now: (
@@ -146,6 +146,7 @@ function createCatalog({ db, registry, pool, env = process.env, clock = { now: (
const now = clock.now();
const now = clock.now();
const providers = await registry.listProviders();
const providers = await registry.listProviders();
const byKey = new Map(providers.map((p) => [p.key, p]));
const byKey = new Map(providers.map((p) => [p.key, p]));
+ const fidelityMap = fidelity ? await fidelity.read() : new Map();
// The router's breaker state per provider (server/providers/index.js): an offer whose provider answered
// The router's breaker state per provider (server/providers/index.js): an offer whose provider answered
// "insufficient balance" is exhausted until a call succeeds again, and the catalog says so rather than
// "insufficient balance" is exhausted until a call succeeds again, and the catalog says so rather than
// promising free capacity that cannot answer.
// promising free capacity that cannot answer.
@@ -229,6 +230,9 @@ function createCatalog({ db, registry, pool, env = process.env, clock = { now: (
@@ -229,6 +230,9 @@ function createCatalog({ db, registry, pool, env = process.env, clock = { now: (
refusal_rate: refusal && refusal.refusal_rate != null ? Number(refusal.refusal_rate) : null,
refusal_rate: refusal && refusal.refusal_rate != null ? Number(refusal.refusal_rate) : null,
refusal_measured_on: refusal ? (String(refusal.measured_on || '').slice(0, 10) || null) : null,
refusal_measured_on: refusal ? (String(refusal.measured_on || '').slice(0, 10) || null) : null,
refusal_prompt_set: refusal ? (refusal.prompt_set || null) : null,
refusal_prompt_set: refusal ? (refusal.prompt_set || null) : null,
+ // Bench's measured provider fidelity for this exact endpoint: score, n, interval, the model
+ // it was compared on and when. Null when nobody measured it (missing is not zero).
+ fidelity: fidelityMap.get(`${String(up.key).toLowerCase()}\0${String(row.model_key).toLowerCase()}`) || null,
updated_at: row.updated_at || provider.updated_at || null,
updated_at: row.updated_at || provider.updated_at || null,
};
};
if (!groups.has(slug)) {
if (!groups.has(slug)) {
new file mode 100644
new file mode 100644
@@ -0,0 +1,80 @@
@@ -0,0 +1,80 @@
+'use strict';
+
+/**
+ * Provider fidelity on an Exchange offer (stage B5).
+ *
+ * OpenVibe.Bench measures the same fixed objective set through every provider that serves an open-weight
+ * model and publishes the latest score at GET /api/v1/fidelity. This is a read-through of that public
+ * answer, keyed by the provider and exact endpoint: AI never measures fidelity itself and never invents
+ * a score. Bench being down (or not configured) leaves the field null, which the catalog renders as "not
+ * measured", never as zero.
+ *
+ * The read is cached in this process for five minutes; a failed read keeps the last good answer for the
+ * same window, and a first failure is retried after a minute rather than on every page view.
+ */
+const CACHE_MS = 5 * 60 * 1000;
+const FAIL_MS = 60 * 1000;
+
+const keyOf = (provider, model) => `${String(provider || '').toLowerCase()}\0${String(model || '').toLowerCase()}`;
+const num = (v) => (v == null || v === '' || !Number.isFinite(Number(v)) ? null : Number(v));
+
+function createFidelitySource({ url = null, fetchImpl = globalThis.fetch, clock = { now: () => Date.now() }, log = console, timeoutMs = 2500, enabled = true } = {}) {
+ const base = String(url || '').replace(/\/+$/, '');
+ const live = enabled && Boolean(base) && typeof fetchImpl === 'function';
+ let hit = null;
+ let pending = null;
+
+ async function load() {
+ if (!live) return new Map();
+ let body;
+ try {
+ const res = await fetchImpl(`${base}/api/v1/fidelity`, {
+ headers: { Accept: 'application/json' },
+ signal: AbortSignal.timeout(Math.max(250, Math.min(5000, Number(timeoutMs) || 2500))),
+ });
+ if (!res.ok) throw new Error(`Bench answered ${res.status}`);
+ body = await res.json();
+ } catch (err) {
+ log.warn('[exchange] fidelity read failed: ' + String((err && err.message) || err).slice(0, 200));
+ return null;
+ }
+ const map = new Map();
+ for (const offer of (Array.isArray(body && body.offers) ? body.offers : [])) {
+ const provider = String((offer && offer.provider) || '').trim();
+ const model = String((offer && (offer.provider_model || offer.model)) || '').trim();
+ const score = num(offer && offer.score);
+ const n = num(offer && offer.n);
+ if (!provider || !model || score == null || n == null || n <= 0) continue;
+ const interval = offer.interval && num(offer.interval.lower) != null && num(offer.interval.upper) != null
+ ? { lower: num(offer.interval.lower), upper: num(offer.interval.upper) }
+ : null;
+ map.set(keyOf(provider, model), {
+ score,
+ n,
+ interval,
+ compared_to: (offer && (offer.compared_to || offer.model)) || null,
+ measured_at: (offer && offer.measured_at) || null,
+ prompt_set: (offer && offer.prompt_set) || (body && body.objective_set) || null,
+ });
+ }
+ return map;
+ }
+
+ /** The fidelity map for this snapshot. Never throws: the market/catalog answer without the field. */
+ async function read() {
+ if (!live) return new Map();
+ if (hit && clock.now() - hit.at < (hit.ok ? CACHE_MS : FAIL_MS)) return hit.value;
+ if (pending) return pending;
+ pending = load()
+ .then((map) => {
+ hit = { at: clock.now(), value: map || hit?.value || new Map(), ok: Boolean(map) };
+ return hit.value;
+ })
+ .finally(() => { pending = null; });
+ return pending;
+ }
+
+ return { read, enabled: live, url: base || null, keyOf };
+}
+
+module.exports = { createFidelitySource, keyOf, CACHE_MS, FAIL_MS };
@@ -8,7 +8,7 @@ const recordKey = (provider, model) => `${provider}\0${String(model).toLowerCase
@@ -8,7 +8,7 @@ const recordKey = (provider, model) => `${provider}\0${String(model).toLowerCase
const num = (v) => v == null || v === '' || !Number.isFinite(Number(v)) || Number(v) < 0 ? null : Number(v);
const num = (v) => v == null || v === '' || !Number.isFinite(Number(v)) || Number(v) < 0 ? null : Number(v);
const webUrl = (v) => typeof v === 'string' && /^https:\/\//.test(v) ? v : null;
const webUrl = (v) => typeof v === 'string' && /^https:\/\//.test(v) ? v : null;
-function buildOffers({ providers = [], rows = [], changes = [], catalogue = {}, metadata = {}, observations = [], presets = PROVIDERS }) {
+function buildOffers({ providers = [], rows = [], changes = [], catalogue = {}, metadata = {}, observations = [], presets = PROVIDERS, fidelity = null }) {
const publicKeys = new Set([...presets.map(p => p.key), ...SOURCES.filter(s => s.role === 'own').map(s => s.provider)]);
const publicKeys = new Set([...presets.map(p => p.key), ...SOURCES.filter(s => s.role === 'own').map(s => s.provider)]);
const byProvider = new Map(providers.filter(p => publicKeys.has(p.key)).map((p) => [p.key, p]));
const byProvider = new Map(providers.filter(p => publicKeys.has(p.key)).map((p) => [p.key, p]));
const seeds = new Map();
const seeds = new Map();
@@ -58,6 +58,9 @@ function buildOffers({ providers = [], rows = [], changes = [], catalogue = {},
@@ -58,6 +58,9 @@ function buildOffers({ providers = [], rows = [], changes = [], catalogue = {},
routed: routed.some(o => o.provider === p.key && String(o.model).toLowerCase() === String(row.model_key).toLowerCase()),
routed: routed.some(o => o.provider === p.key && String(o.model).toLowerCase() === String(row.model_key).toLowerCase()),
uncensored: seed?.m.uncensored === true || presets.find(preset => preset.key === p.key)?.uncensored === true,
uncensored: seed?.m.uncensored === true || presets.find(preset => preset.key === p.key)?.uncensored === true,
provider_access: p.provider_access || 'UNVERIFIED', field_sources: {},
provider_access: p.provider_access || 'UNVERIFIED', field_sources: {},
+ // The measured provider fidelity from Bench, when Bench has measured this exact endpoint;
+ // absent (null) when nobody measured it, never a zero.
+ fidelity: fidelity && typeof fidelity.get === 'function' ? (fidelity.get(recordKey(p.key, row.model_key)) || null) : null,
};
};
// Pending price changes are the latest observed own-page price, even before the router
// Pending price changes are the latest observed own-page price, even before the router
// approves charging it. Never publish the change log's actor, note or account data.
// approves charging it. Never publish the change log's actor, note or account data.
@@ -107,20 +110,21 @@ function pageMarket(snapshot, query = {}) {
@@ -107,20 +110,21 @@ function pageMarket(snapshot, query = {}) {
offers: offers.slice(offset, offset + limit), next_cursor: offset + limit < offers.length ? String(offset + limit) : null };
offers: offers.slice(offset, offset + limit), next_cursor: offset + limit < offers.length ? String(offset + limit) : null };
}
}
-function createMarket({ db, registry, catalog, config, clock = { now: Date.now }, metadata = null }) {
+function createMarket({ db, registry, catalog, config, clock = { now: Date.now }, metadata = null, fidelity = null }) {
const source = metadata || createModelMetadata({ db, now: () => clock.now(), enabled: config?.nodeEnv !== 'test' && process.env.NODE_ENV !== 'test' });
const source = metadata || createModelMetadata({ db, now: () => clock.now(), enabled: config?.nodeEnv !== 'test' && process.env.NODE_ENV !== 'test' });
let hit = null; let pending = null;
let hit = null; let pending = null;
async function snapshot() {
async function snapshot() {
if (hit && clock.now() < hit.until) return hit.value;
if (hit && clock.now() < hit.until) return hit.value;
if (!pending) pending = (async () => {
if (!pending) pending = (async () => {
- const [providers, rows, changes, catalogue, meta, observations] = await Promise.all([
+ const [providers, rows, changes, catalogue, meta, observations, fidelityMap] = await Promise.all([
registry.listProviders(), registry.listModels(),
registry.listProviders(), registry.listModels(),
db.prepare('SELECT provider_key, model_key, field, new_price, source, checked_at FROM price_changes ORDER BY checked_at DESC').all(),
db.prepare('SELECT provider_key, model_key, field, new_price, source, checked_at FROM price_changes ORDER BY checked_at DESC').all(),
catalog(), source.read(),
catalog(), source.read(),
db.prepare('SELECT provider_key, model_key, price, source, checked_at FROM exchange_market_prices').all(),
db.prepare('SELECT provider_key, model_key, price, source, checked_at FROM exchange_market_prices').all(),
+ fidelity ? fidelity.read() : Promise.resolve(new Map()),
]);
]);
const value = { generated_at: new Date(clock.now()).toISOString(), metadata: { source: URL, checked_at: meta.checked_at, stale: meta.stale },
const value = { generated_at: new Date(clock.now()).toISOString(), metadata: { source: URL, checked_at: meta.checked_at, stale: meta.stale },
- offers: buildOffers({ providers, rows, changes, catalogue, metadata: meta, observations }) };
+ offers: buildOffers({ providers, rows, changes, catalogue, metadata: meta, observations, fidelity: fidelityMap }) };
hit = { value, until: clock.now() + TTL_MS }; return value;
hit = { value, until: clock.now() + TTL_MS }; return value;
})().finally(() => { pending = null; });
})().finally(() => { pending = null; });
return pending;
return pending;
new file mode 100644
new file mode 100644
@@ -0,0 +1,239 @@
@@ -0,0 +1,239 @@
+'use strict';
+
+/**
+ * Real-traffic share (plan 2026-11-09, stage B8).
+ *
+ * Which models real requests and tokens actually carried, per model and per app category, from the
+ * Exchange receipts. It is an aggregate or it is nothing: no prompt, no answer, no person, no app's
+ * name. Every cell needs at least k requests before it is published; a smaller cell is withheld and
+ * counted only in the withheld total, so a category with one user cannot be read back from the shares.
+ *
+ * The week is a completed UTC week (Monday 00:00 to the next Monday). The answer is stored once per
+ * week: receipts are pruned with their runs, and the stored row is the durable public record. The
+ * last completed week is computed on first read; a past week is only recomputed while its receipts are
+ * still in the retention window (never a silent empty week after they aged out).
+ *
+ * "Category" is the first-party service whose run answered (a product), or `apps` for every developer
+ * app together. Named developer apps would be per-app data, so they are never named; the network's
+ * own moderation requests are not Exchange traffic and are not counted.
+ */
+const DAY_MS = 86400000;
+const WEEK_MS = 7 * DAY_MS;
+const K_DEFAULT = 20;
+const RETENTION_DAYS = 30;
+const SCHEMA = 'exchange.traffic-share@1';
+
+class ShareError extends Error {
+ constructor(status, code, detail) {
+ super(detail);
+ this.name = 'ShareError';
+ this.status = status;
+ this.code = code;
+ this.detail = detail;
+ }
+}
+
+const round = (v) => Math.round(Number(v) * 1e6) / 1e6;
+const int = (v) => Math.max(0, Math.round(Number(v) || 0));
+const isoDay = (ms) => new Date(ms).toISOString().slice(0, 10);
+const dayString = (v) => (v instanceof Date ? isoDay(v.getTime()) : String(v || '').slice(0, 10));
+
+/** The Monday 00:00 UTC of the week a moment is in. */
+function mondayOf(ms) {
+ const d = new Date(ms);
+ const offset = (d.getUTCDay() + 6) % 7;
+ return Date.UTC(d.getUTCFullYear(), d.getUTCMonth(), d.getUTCDate() - offset);
+}
+
+/** The last completed week: this week's Monday minus seven days. */
+function lastCompleteWeek(ms) {
+ return mondayOf(ms) - WEEK_MS;
+}
+
+function parseWeek(day) {
+ const s = String(day || '');
+ if (!/^\d{4}-\d{2}-\d{2}$/.test(s)) throw new ShareError(422, 'traffic_share.week_invalid', 'week must be a YYYY-MM-DD Monday');
+ const ms = Date.parse(`${s}T00:00:00.000Z`);
+ if (!Number.isFinite(ms) || new Date(ms).getUTCDay() !== 1) throw new ShareError(422, 'traffic_share.week_invalid', 'week must be a Monday 00:00 UTC');
+ return ms;
+}
+
+/** A first-party service keeps its product name; every developer app folds into one public category. */
+function categoryOf(row) {
+ if (row.requester_type === 'app') return 'apps';
+ if (row.requester_type === 'service' && /^[a-z0-9][a-z0-9._-]{0,63}$/i.test(String(row.requester_id || ''))) return String(row.requester_id);
+ return 'other';
+}
+
+const shareOf = (n, total) => (total > 0 ? round(n / total) : null);
+
+function aggregate(cells, keyFields, keyOf) {
+ const map = new Map();
+ for (const cell of cells) {
+ const key = keyOf(cell);
+ const row = map.get(key) || { ...Object.fromEntries(keyFields.map((f) => [f, cell[f]])), requests: 0, tokens_in: 0, tokens_out: 0 };
+ row.requests += cell.requests;
+ row.tokens_in += cell.tokens_in;
+ row.tokens_out += cell.tokens_out;
+ map.set(key, row);
+ }
+ return [...map.values()];
+}
+
+/**
+ * Read the receipts of one completed week and publish what meets the k rule. Pure over the rows passed
+ * to it, so a test can replay a small fixture without a database or a paid call.
+ */
+function aggregateWeek({ rows = [], weekStart, k = K_DEFAULT, generatedAt = new Date().toISOString() } = {}) {
+ const start = typeof weekStart === 'number' ? weekStart : parseWeek(weekStart);
+ const cells = new Map();
+ let allRequests = 0;
+ let allTokens = 0;
+ for (const row of rows) {
+ const provider = String(row.provider || '').trim();
+ const model = String(row.model || '').trim();
+ if (!provider || !model) continue;
+ const category = categoryOf(row);
+ const key = `${category}\0${provider}\0${model}`;
+ const cell = cells.get(key) || { category, provider, model, requests: 0, tokens_in: 0, tokens_out: 0 };
+ cell.requests += int(row.requests);
+ cell.tokens_in += int(row.tokens_in);
+ cell.tokens_out += int(row.tokens_out);
+ cells.set(key, cell);
+ allRequests += int(row.requests);
+ allTokens += int(row.tokens_in) + int(row.tokens_out);
+ }
+ const all = [...cells.values()];
+ const published = all.filter((c) => c.requests >= Math.max(1, Number(k) || K_DEFAULT));
+ const withheld = all.filter((c) => c.requests < Math.max(1, Number(k) || K_DEFAULT));
+ const requests = published.reduce((n, c) => n + c.requests, 0);
+ const tokens = published.reduce((n, c) => n + c.tokens_in + c.tokens_out, 0);
+ const tokensIn = published.reduce((n, c) => n + c.tokens_in, 0);
+ for (const cell of published) cell.tokens = cell.tokens_in + cell.tokens_out;
+ const offers = aggregate(published, ['provider', 'model'], (c) => `${c.provider}\0${c.model}`)
+ .map((r) => ({
+ provider: r.provider, model: r.model, requests: r.requests,
+ tokens_in: r.tokens_in, tokens_out: r.tokens_out, tokens: r.tokens_in + r.tokens_out,
+ request_share: shareOf(r.requests, requests), token_share: shareOf(r.tokens_in + r.tokens_out, tokens),
+ }))
+ .sort((a, b) => b.requests - a.requests || b.tokens - a.tokens || a.model.localeCompare(b.model) || a.provider.localeCompare(b.provider));
+ const models = aggregate(published, ['model'], (c) => c.model)
+ .map((r) => {
+ const providers = new Set(published.filter((c) => c.model === r.model).map((c) => c.provider)).size;
+ return { model: r.model, providers, requests: r.requests, tokens_in: r.tokens_in, tokens_out: r.tokens_out,
+ tokens: r.tokens_in + r.tokens_out, request_share: shareOf(r.requests, requests), token_share: shareOf(r.tokens_in + r.tokens_out, tokens) };
+ })
+ .sort((a, b) => b.requests - a.requests || b.tokens - a.tokens || a.model.localeCompare(b.model));
+ const categories = aggregate(published, ['category'], (c) => c.category)
+ .map((r) => ({ category: r.category, requests: r.requests, tokens_in: r.tokens_in, tokens_out: r.tokens_out,
+ tokens: r.tokens_in + r.tokens_out, request_share: shareOf(r.requests, requests), token_share: shareOf(r.tokens_in + r.tokens_out, tokens) }))
+ .sort((a, b) => b.requests - a.requests || b.tokens - a.tokens || a.category.localeCompare(b.category));
+ const withheldRequests = withheld.reduce((n, c) => n + c.requests, 0);
+ const withheldTokens = withheld.reduce((n, c) => n + c.tokens_in + c.tokens_out, 0);
+ return {
+ $schema: SCHEMA,
+ week_start: isoDay(start),
+ week_end: isoDay(start + WEEK_MS),
+ generated_at: generatedAt,
+ k: Math.max(1, Number(k) || K_DEFAULT),
+ totals: { requests, tokens_in: tokensIn, tokens_out: tokens - tokensIn, tokens },
+ offers,
+ models,
+ categories,
+ withheld: { cells: withheld.length, requests: withheldRequests, tokens: withheldTokens, share_of_requests: shareOf(withheldRequests, allRequests) },
+ source: 'Exchange receipts: succeeded, non-synthetic requests that a provider answered.',
+ note: published.length
+ ? 'Only cells with at least k requests are published. Every developer app is the single category apps.'
+ : 'No model and category reached k requests this week, so nothing is published.',
+ };
+}
+
+function createShare({ db, clock = { now: () => Date.now() }, k = K_DEFAULT, log = console } = {}) {
+ const minimum = Math.max(1, Number(k) || K_DEFAULT);
+ const byCell = db.prepare(`SELECT r.requester_type, r.requester_id,
+ COALESCE(r.body -> 'offer' ->> 'provider', '') AS provider,
+ COALESCE(r.body -> 'offer' ->> 'model', '') AS model,
+ COUNT(*)::bigint AS requests,
+ COALESCE(SUM(NULLIF(r.body -> 'usage' ->> 'tokens_in', '')::bigint), 0) AS tokens_in,
+ COALESCE(SUM(NULLIF(r.body -> 'usage' ->> 'tokens_out', '')::bigint), 0) AS tokens_out
+ FROM ai_receipts r
+ JOIN runs u ON u.id = r.run_id
+ WHERE r.created_at >= ?::timestamptz AND r.created_at < ?::timestamptz
+ AND r.requester_type IN ('service', 'app')
+ AND u.status = 'succeeded' AND u.synthetic = 0 AND u.provider_key IS NOT NULL
+ AND COALESCE(u.options::jsonb ->> 'history', 'true') <> 'false'
+ AND NULLIF(r.body -> 'offer' ->> 'provider', '') IS NOT NULL
+ AND NULLIF(r.body -> 'offer' ->> 'model', '') IS NOT NULL
+ GROUP BY 1, 2, 3, 4`);
+ const insert = db.prepare(`INSERT INTO exchange_traffic_share
+ (week_start, week_end, k, payload, cells, published_cells, withheld_cells, computed_at)
+ VALUES (?::date, ?::date, ?, ?::jsonb, ?, ?, ?, now())
+ ON CONFLICT (week_start) DO UPDATE SET
+ k = EXCLUDED.k, payload = EXCLUDED.payload, cells = EXCLUDED.cells,
+ published_cells = EXCLUDED.published_cells, withheld_cells = EXCLUDED.withheld_cells,
+ recomputed_at = now()`);
+ const stored = db.prepare('SELECT week_start, payload, computed_at, recomputed_at FROM exchange_traffic_share WHERE week_start = ?::date');
+ const latest = db.prepare('SELECT week_start, payload, computed_at, recomputed_at FROM exchange_traffic_share ORDER BY week_start DESC LIMIT 1');
+
+ const decode = (row) => ({
+ ...(row.payload && typeof row.payload === 'object' ? row.payload : JSON.parse(row.payload || '{}')),
+ week_start: dayString(row.week_start),
+ computed_at: row.computed_at instanceof Date ? row.computed_at.toISOString() : String(row.computed_at || ''),
+ recomputed_at: row.recomputed_at ? (row.recomputed_at instanceof Date ? row.recomputed_at.toISOString() : String(row.recomputed_at)) : null,
+ stored: true,
+ });
+
+ async function compute(weekStart) {
+ const start = isoDay(weekStart);
+ const end = isoDay(weekStart + WEEK_MS);
+ const rows = await byCell.all(`${start}T00:00:00.000Z`, `${end}T00:00:00.000Z`);
+ return aggregateWeek({ rows, weekStart, k: minimum, generatedAt: new Date(clock.now()).toISOString() });
+ }
+
+ async function store(payload) {
+ const withheld = payload.withheld ? payload.withheld.cells : 0;
+ const published = payload.offers.length;
+ const allCells = published + withheld;
+ await insert.run(payload.week_start, payload.week_end, payload.k, JSON.stringify(payload), allCells, published, withheld);
+ }
+
+ async function storedWeek(weekStart) {
+ const row = await stored.get(isoDay(weekStart));
+ return row ? decode(row) : null;
+ }
+
+ /**
+ * The public answer. With no week: the last completed week (computed and stored on first read, so the
+ * publication exists without a timer). With a week: that week, while its receipts are still kept.
+ */
+ async function read({ week = null } = {}) {
+ const now = clock.now();
+ const current = mondayOf(now);
+ if (week) {
+ const start = parseWeek(week);
+ if (start >= current) throw new ShareError(422, 'traffic_share.week_future', 'that week has not finished yet');
+ const known = await storedWeek(start);
+ if (known) return known;
+ if (now - start > RETENTION_DAYS * DAY_MS) throw new ShareError(404, 'traffic_share.expired', 'the receipts for that week are no longer kept; only the weeks already stored are available');
+ const payload = await compute(start);
+ await store(payload);
+ return { ...payload, computed_at: new Date(clock.now()).toISOString(), recomputed_at: null, stored: true };
+ }
+ const target = lastCompleteWeek(now);
+ const row = await latest.get();
+ if (row && dayString(row.week_start) === isoDay(target)) return decode(row);
+ const payload = await compute(target);
+ await store(payload);
+ return { ...payload, computed_at: new Date(clock.now()).toISOString(), recomputed_at: null, stored: true };
+ }
+
+ /** The hourly housekeeping pass: publish the last completed week even when nobody has read it. */
+ async function ensure() {
+ const out = await read();
+ return { week_start: out.week_start, stored: Boolean(out.stored) };
+ }
+
+ return { read, ensure, compute, store, aggregateWeek };
+}
+
+module.exports = { createShare, aggregateWeek, categoryOf, mondayOf, lastCompleteWeek, parseWeek, ShareError, SCHEMA, K_DEFAULT, RETENTION_DAYS };
@@ -122,6 +122,9 @@ async function start({ searchFetch = null, config, db: givenDb = null, clock = {
@@ -122,6 +122,9 @@ async function start({ searchFetch = null, config, db: givenDb = null, clock = {
// The Exchange's routing presets and the receipts of the answered requests (server/exchange/).
// The Exchange's routing presets and the receipts of the answered requests (server/exchange/).
const presets = require('./exchange/presets').createPresets({ db, clock, log });
const presets = require('./exchange/presets').createPresets({ db, clock, log });
const receipts = require('./exchange/receipts').createReceipts({ db, clock, log });
const receipts = require('./exchange/receipts').createReceipts({ db, clock, log });
+ // The weekly real-traffic share (stage B8): the public read stores the week; the housekeeping pass
+ // publishes it even when nobody reads it, before the receipts it came from are pruned.
+ const share = require('./exchange/share').createShare({ db, clock, log, k: config.trafficShare.k });
const choices = require('./choices').createChoices({ db, registry, preferences, credentials, machines, adultStatus, ratings: require('./choices/ratings').createRatings({ fetchImpl, clock }), clock, catalog: require('./exchange/catalog').createCatalog({ db, registry, pool, config, env, clock }) });
const choices = require('./choices').createChoices({ db, registry, preferences, credentials, machines, adultStatus, ratings: require('./choices/ratings').createRatings({ fetchImpl, clock }), clock, catalog: require('./exchange/catalog').createCatalog({ db, registry, pool, config, env, clock }) });
await choices.seed();
await choices.seed();
const projectSecrets = require('./project-secrets').createProjectSecrets({ config, env, fetchImpl });
const projectSecrets = require('./project-secrets').createProjectSecrets({ config, env, fetchImpl });
@@ -208,7 +211,7 @@ async function start({ searchFetch = null, config, db: givenDb = null, clock = {
@@ -208,7 +211,7 @@ async function start({ searchFetch = null, config, db: givenDb = null, clock = {
clientSecret: env.OV_OAUTH_CLIENT_SECRET, fetch: fetchImpl,
clientSecret: env.OV_OAUTH_CLIENT_SECRET, fetch: fetchImpl,
}) : null;
}) : null;
const benchEvents = createBenchEvents({ bench, accountData, accountSend, config, log, fetchImpl, clientSecret: env.OV_OAUTH_CLIENT_SECRET });
const benchEvents = createBenchEvents({ bench, accountData, accountSend, config, log, fetchImpl, clientSecret: env.OV_OAUTH_CLIENT_SECRET });
- const app = createApp({ choices, searchFetch, config, db, registry, pool, quotas, cache, runs, auth, keys, env, clock, fetchImpl, log, credentials, preferences, grants, vault, settings, benchEvents, runBudgets, memory, memoryImport, memoryServices, memorySharing, community, endpoints, webhooks, presets, receipts, balance, creditGrants, sessionCookies, gpus, abuse, priceBook, ownEndpoints, refusal, batches, gpuMarket });
+ const app = createApp({ choices, searchFetch, config, db, registry, pool, quotas, cache, runs, auth, keys, env, clock, fetchImpl, log, credentials, preferences, grants, vault, settings, benchEvents, runBudgets, memory, memoryImport, memoryServices, memorySharing, community, endpoints, webhooks, presets, receipts, balance, creditGrants, sessionCookies, gpus, abuse, priceBook, ownEndpoints, refusal, batches, gpuMarket, share });
let jwks = null;
let jwks = null;
let keyLoaded = Promise.resolve(null);
let keyLoaded = Promise.resolve(null);
if (!keys.pinned && config.nodeEnv !== 'test') {
if (!keys.pinned && config.nodeEnv !== 'test') {
@@ -228,6 +231,7 @@ async function start({ searchFetch = null, config, db: givenDb = null, clock = {
@@ -228,6 +231,7 @@ async function start({ searchFetch = null, config, db: givenDb = null, clock = {
if (pruned || expired || lapsed) log.log(`[ai] retention: ${pruned} old run(s), ${expired} expired cache entr${expired === 1 ? 'y' : 'ies'}, ${lapsed} unsent usage reading(s)`);
if (pruned || expired || lapsed) log.log(`[ai] retention: ${pruned} old run(s), ${expired} expired cache entr${expired === 1 ? 'y' : 'ies'}, ${lapsed} unsent usage reading(s)`);
if (Object.values(abusePruned).some((n) => n)) log.log(`[ai] abuse retention: ${Object.entries(abusePruned).filter(([, n]) => n).map(([k, n]) => `${n} ${k}`).join(', ')}`);
if (Object.values(abusePruned).some((n) => n)) log.log(`[ai] abuse retention: ${Object.entries(abusePruned).filter(([, n]) => n).map(([k, n]) => `${n} ${k}`).join(', ')}`);
} catch (err) { log.error(`[ai] retention failed: ${err.message}`); }
} catch (err) { log.error(`[ai] retention failed: ${err.message}`); }
+ try { await share.ensure(); } catch (err) { log.warn(`[ai] traffic share failed: ${err.message}`); }
}, 60 * 60 * 1000);
}, 60 * 60 * 1000);
housekeeping.unref?.();
housekeeping.unref?.();
@@ -454,6 +454,7 @@ const exchangeOffer = {
@@ -454,6 +454,7 @@ const exchangeOffer = {
refusal_rate: nullable('number', { minimum: 0, maximum: 1, description: 'The share of the published probe set this offer refused (0.75 is three probes out of four). null when nobody measured this offer, never 0.' }),
refusal_rate: nullable('number', { minimum: 0, maximum: 1, description: 'The share of the published probe set this offer refused (0.75 is three probes out of four). null when nobody measured this offer, never 0.' }),
refusal_measured_on: nullable('string', { description: 'The day the refusal rate was measured' }),
refusal_measured_on: nullable('string', { description: 'The day the refusal rate was measured' }),
refusal_prompt_set: nullable('string', { description: 'The version of the published probe set the rate was measured with' }),
refusal_prompt_set: nullable('string', { description: 'The version of the published probe set the rate was measured with' }),
+ fidelity: nullable('object', { description: 'Provider fidelity measured by OpenVibe.Bench: the same fixed objective set run through every provider that serves this open-weight model. score is the pass rate, n the answered items, interval its 95% Wilson band. Null when Bench has not measured this exact endpoint.', properties: { score: { type: 'number', minimum: 0, maximum: 1 }, n: integer({ minimum: 0 }), interval: { type: ['object', 'null'], properties: { lower: nullable('number'), upper: nullable('number') } }, compared_to: nullable('string'), measured_at: nullable('string'), prompt_set: nullable('string') } }),
updated_at: nullable('string'),
updated_at: nullable('string'),
},
},
};
};
@@ -491,6 +492,7 @@ route('get', '/exchange/market', {
@@ -491,6 +492,7 @@ route('get', '/exchange/market', {
maker: nullable('string'), context: nullable('number'), max_output: nullable('number'),
maker: nullable('string'), context: nullable('number'), max_output: nullable('number'),
open_weights: nullable('boolean'), licence: nullable('string'), release_date: nullable('string'), knowledge_cutoff: nullable('string'),
open_weights: nullable('boolean'), licence: nullable('string'), release_date: nullable('string'), knowledge_cutoff: nullable('string'),
modalities: { type: ['object', 'null'], properties: { input: arrayOf(str()), output: arrayOf(str()) } },
modalities: { type: ['object', 'null'], properties: { input: arrayOf(str()), output: arrayOf(str()) } },
+ fidelity: nullable('object', { properties: { score: nullable('number'), n: integer(), interval: { type: ['object', 'null'], properties: { lower: nullable('number'), upper: nullable('number') } }, compared_to: nullable('string'), measured_at: nullable('string'), prompt_set: nullable('string') } }),
field_sources: { type: 'object', additionalProperties: str({ format: 'uri' }) },
field_sources: { type: 'object', additionalProperties: str({ format: 'uri' }) },
} }),
} }),
},
},
@@ -626,6 +628,21 @@ route('get', '/exchange/usage', {
@@ -626,6 +628,21 @@ route('get', '/exchange/usage', {
} }) },
} }) },
});
});
+route('get', '/exchange/share', {
+ operationId: 'exchangeTrafficShare', summary: 'Real-traffic share by model and app category, published weekly', tags: ['exchange'], security: [], servers: [{ url: 'https://api.openvibe.exchange' }],
+ description: 'Public and aggregate only: never a prompt, an answer, a person or an app\'s name. Read from the Exchange receipts of a completed UTC week (succeeded, non-synthetic requests that a provider answered; runs whose person turned history off are excluded). Every cell needs at least k requests before it is published; smaller cells are withheld and counted only in withheld. Developer apps fold into one category, apps, so no app is named. The weekly answer is stored once and kept after the receipts it came from are pruned.',
+ parameters: [query('week', str(), 'A Monday, YYYY-MM-DD. Default: the last completed week')],
+ responses: { 200: json('The share', { type: 'object', required: ['$schema', 'week_start', 'week_end', 'k', 'totals', 'offers', 'models', 'categories', 'withheld'], properties: {
+ $schema: { const: 'exchange.traffic-share@1' }, week_start: str({ format: 'date' }), week_end: str({ format: 'date' }), generated_at: str({ format: 'date-time' }),
+ k: integer({ minimum: 20 }), source: str(), note: str(),
+ totals: { type: 'object', properties: { requests: integer(), tokens_in: integer(), tokens_out: integer(), tokens: integer() } },
+ offers: arrayOf({ type: 'object', properties: { provider: str(), model: str(), requests: integer(), tokens_in: integer(), tokens_out: integer(), tokens: integer(), request_share: nullable('number'), token_share: nullable('number') } }),
+ models: arrayOf({ type: 'object', properties: { model: str(), providers: integer(), requests: integer(), tokens: integer(), request_share: nullable('number'), token_share: nullable('number') } }),
+ categories: arrayOf({ type: 'object', properties: { category: str(), requests: integer(), tokens: integer(), request_share: nullable('number'), token_share: nullable('number') } }),
+ withheld: { type: 'object', properties: { cells: integer(), requests: integer(), tokens: integer(), share_of_requests: nullable('number') } },
+ } }), ...errors([404, 422]) },
+});
+
route('get', '/exchange/price-changes', {
route('get', '/exchange/price-changes', {
operationId: 'exchangePriceChanges', summary: 'What a price was and what it became', tags: ['exchange'], security: [],
operationId: 'exchangePriceChanges', summary: 'What a price was and what it became', tags: ['exchange'], security: [],
description: "Public. The price changelog: every change the weekly price book made, with the old price, the new price, the provider's own listing the price was read from and the day it was read. A change that waits for the owner's approval is not a price yet and is not here.",
description: "Public. The price changelog: every change the weekly price book made, with the old price, the new price, the provider's own listing the price was read from and the day it was read. A change that waits for the owner's approval is not a price yet and is not here.",
new file mode 100644
new file mode 100644
@@ -0,0 +1,76 @@
@@ -0,0 +1,76 @@
+'use strict';
+
+// Stage B5, AI's side: Bench's public fidelity score is read through once, cached, and put on the exact
+// provider offer in the market feed. Bench down or unconfigured leaves the field null (not measured), never
+// a zero. The test fetches a fixture; no Bench and no provider is called.
+const assert = require('assert');
+const { createFidelitySource, keyOf } = require('../server/exchange/fidelity');
+const { buildOffers } = require('../server/exchange/market');
+
+const BENCH = {
+ $schema: 'exchange.fidelity@1',
+ objective_set: 'fidelity-2026-11-09.v1',
+ items: 10,
+ offers: [
+ { model: 'acme/alpha-1', provider: 'acme', provider_model: 'ACME/Alpha-1', score: 0.9, n: 10, passed: 9, interval: { lower: 0.596, upper: 0.982 }, compared_to: 'acme/alpha-1', prompt_set: 'fidelity-2026-11-09.v1', measured_at: '2026-11-09T08:00:00.000Z', measured_on: '2026-11-09' },
+ { model: 'acme/alpha-1', provider: 'beta', provider_model: 'beta/alpha-1', score: null, n: 0, interval: { lower: null, upper: null }, compared_to: 'acme/alpha-1', measured_at: '2026-11-09T08:00:00.000Z' },
+ ],
+};
+
+const presets = [{
+ key: 'acme', display_name: 'Acme', models: [{
+ model_key: 'acme/alpha-1', display_name: 'Alpha 1', type: 'chat',
+ cost: { in: 1, out: 2, cached: null }, source: 'https://acme.test/prices', checked: '2026-11-01',
+ }],
+}];
+
+(async () => {
+ let time = Date.parse('2026-11-09T09:00:00Z');
+ let calls = 0;
+ let fail = false;
+ const source = createFidelitySource({
+ url: 'https://bench.openvibe.services/',
+ fetchImpl: async (url) => {
+ calls++;
+ assert.strictEqual(url, 'https://bench.openvibe.services/api/v1/fidelity');
+ if (fail) throw new Error('offline');
+ return new Response(JSON.stringify(BENCH), { status: 200, headers: { 'content-type': 'application/json' } });
+ },
+ clock: { now: () => time },
+ log: { warn() {} },
+ });
+
+ const first = await source.read();
+ const score = first.get(keyOf('acme', 'ACME/Alpha-1'));
+ assert(score, 'the exact endpoint is keyed case-insensitively');
+ assert.strictEqual(score.score, 0.9);
+ assert.strictEqual(score.n, 10);
+ assert.deepStrictEqual(score.interval, { lower: 0.596, upper: 0.982 });
+ assert.strictEqual(score.compared_to, 'acme/alpha-1');
+ assert(!first.has(keyOf('beta', 'beta/alpha-1')), 'a row with no score is not published as a score');
+
+ await source.read();
+ assert.strictEqual(calls, 1, 'the read is cached');
+ time += 6 * 60 * 1000;
+ await source.read();
+ assert.strictEqual(calls, 2, 'the cache expires after five minutes');
+ fail = true;
+ time += 6 * 60 * 1000;
+ const afterFailure = await source.read();
+ assert.strictEqual(afterFailure.get(keyOf('acme', 'acme/alpha-1')).score, 0.9, 'a failed refresh keeps the last good answer');
+ assert.strictEqual(calls, 3);
+
+ const disabled = createFidelitySource({ url: 'https://bench.openvibe.services', fetchImpl: async () => { throw new Error('must not be called'); }, enabled: false });
+ assert.strictEqual((await disabled.read()).size, 0);
+
+ const offers = buildOffers({ presets, fidelity: first });
+ const offer = offers.find((o) => o.provider === 'acme' && o.provider_model === 'acme/alpha-1');
+ assert(offer, 'the preset offer is listed');
+ assert.strictEqual(offer.fidelity.score, 0.9);
+ assert.strictEqual(offer.fidelity.compared_to, 'acme/alpha-1');
+ assert.strictEqual(offer.fidelity.n, 10);
+ const unmeasured = buildOffers({ presets });
+ assert.strictEqual(unmeasured[0].fidelity, null, 'no measurement is null, never zero');
+
+ console.log('exchange fidelity: Bench score read once, attached to the exact offer, null when unmeasured');
+})().catch((e) => { console.error(e); process.exit(1); });
new file mode 100644
new file mode 100644
@@ -0,0 +1,125 @@
@@ -0,0 +1,125 @@
+'use strict';
+
+// Stage B8: the weekly real-traffic share from Exchange receipts. Aggregate only, k >= 20 per cell,
+// opt-outs respected, stored once per completed week. The tests replay a small week into a real
+// database; no test calls a provider.
+const assert = require('assert');
+const { boot, request, suite } = require('./helpers');
+const { aggregateWeek, mondayOf, lastCompleteWeek, categoryOf } = require('../server/exchange/share');
+
+const DAY_MS = 86400000;
+const t = suite('exchange-traffic-share');
+let h;
+
+let seq = 0;
+const uid = (p) => `${p}_${Date.now()}_${seq++}`;
+const iso = (ms) => new Date(ms).toISOString();
+
+async function insertRun({ id, type, requester, provider, model, created, history = true, status = 'succeeded', synthetic = 0 }) {
+ await h.db.prepare(`INSERT INTO runs (id, workflow_key, workflow_version, status, requester_type, requester_id, input, input_hash,
+ provider_key, model_key, synthetic, options, created_at, started_at, finished_at)
+ VALUES (?, 'ai.chat', 1, ?, ?, ?, '{}', 'input-hash', ?, ?, ?, ?, ?, ?, ?)`)
+ .run(id, status, type, requester, provider, model, synthetic, JSON.stringify({ history }), created, created, created);
+}
+async function insertReceipt({ runId, type, requester, provider, model, created, tokensIn = 100, tokensOut = 10 }) {
+ const body = {
+ id: `rcpt_${runId}`,
+ run_id: runId,
+ created_at: created,
+ offer: { provider, model },
+ usage: { tokens_in: tokensIn, tokens_out: tokensOut },
+ };
+ await h.db.prepare(`INSERT INTO ai_receipts (id, run_id, requester_type, requester_id, body, created_at)
+ VALUES (?, ?, ?, ?, ?::jsonb, ?)`)
+ .run(body.id, runId, type, requester, JSON.stringify(body), created);
+}
+
+let weekStart;
+async function addTraffic({ type, requester, provider, model, count, history = true, status = 'succeeded', tokensIn = 100, tokensOut = 10 }) {
+ for (let i = 0; i < count; i++) {
+ const id = uid('run');
+ const created = iso(weekStart + (i % 4) * 6 * 3600000);
+ await insertRun({ id, type, requester, provider, model, created, history, status });
+ await insertReceipt({ runId: id, type, requester, provider, model, created, tokensIn, tokensOut });
+ }
+}
+
+t.test('aggregateWeek publishes only cells with k requests and folds apps into one category', () => {
+ const rows = [
+ { requester_type: 'service', requester_id: 'live', provider: 'deepseek', model: 'deepseek-chat', requests: 25, tokens_in: 2500, tokens_out: 250 },
+ { requester_type: 'service', requester_id: 'live', provider: 'qwen', model: 'qwen2.5', requests: 5, tokens_in: 500, tokens_out: 50 },
+ { requester_type: 'app', requester_id: 'prj_PRIVATE', provider: 'deepseek', model: 'deepseek-chat', requests: 20, tokens_in: 2000, tokens_out: 200 },
+ { requester_type: 'service', requester_id: 'codes', provider: 'acme', model: 'acme-1', requests: 2, tokens_in: 200, tokens_out: 20 },
+ ];
+ const out = aggregateWeek({ rows, weekStart: Date.parse('2026-11-09T00:00:00Z'), k: 20 });
+ assert.strictEqual(out.week_start, '2026-11-09');
+ assert.strictEqual(out.week_end, '2026-11-16');
+ assert.strictEqual(out.totals.requests, 45, 'only the published cells count');
+ assert.strictEqual(out.categories.length, 2);
+ assert.strictEqual(out.categories[0].category, 'live');
+ assert.strictEqual(out.categories.find((c) => c.category === 'apps').requests, 20, 'every app is one category');
+ assert.strictEqual(out.offers.length, 1, 'only the published provider/model cell remains');
+ assert.strictEqual(out.withheld.cells, 2, 'the small service cells are withheld');
+ assert.strictEqual(out.withheld.requests, 7);
+ const share = out.categories.reduce((n, c) => n + c.request_share, 0);
+ assert(Math.abs(share - 1) < 1e-9, 'published category shares add to one');
+ assert(!JSON.stringify(out).includes('prj_PRIVATE'), 'no project or app id is published');
+ assert.strictEqual(categoryOf({ requester_type: 'service', requester_id: 'live' }), 'live');
+});
+
+t.test('boot', async () => {
+ h = await boot();
+ const now = Date.now();
+ weekStart = lastCompleteWeek(now);
+ assert.strictEqual(mondayOf(weekStart), weekStart);
+});
+
+t.test('a completed week is stored once and the read is public and cached', async () => {
+ await addTraffic({ type: 'service', requester: 'live', provider: 'deepseek', model: 'deepseek-chat', count: 25 });
+ await addTraffic({ type: 'app', requester: 'prj_SECRET_APP', provider: 'deepseek', model: 'deepseek-chat', count: 20 });
+ await addTraffic({ type: 'service', requester: 'codes', provider: 'acme', model: 'acme-1', count: 10 });
+ // Opted out and failed traffic must not be in any number, not even the withheld total.
+ await addTraffic({ type: 'service', requester: 'live', provider: 'deepseek', model: 'deepseek-chat', count: 7, history: false });
+ await addTraffic({ type: 'service', requester: 'live', provider: 'deepseek', model: 'deepseek-chat', count: 4, status: 'failed' });
+
+ const first = await request(h.base, 'GET', '/api/v1/exchange/share');
+ assert.strictEqual(first.status, 200, first.text);
+ assert.strictEqual(first.headers.get('cache-control'), 'public, max-age=300');
+ const body = first.body;
+ assert.strictEqual(body.$schema, 'exchange.traffic-share@1');
+ assert.strictEqual(body.week_start, iso(weekStart).slice(0, 10));
+ assert(body.k >= 20);
+ assert.strictEqual(body.totals.requests, 45, 'opt-outs and failures are excluded');
+ assert.strictEqual(body.withheld.cells, 1);
+ assert.strictEqual(body.withheld.requests, 10);
+ assert.deepStrictEqual(body.categories.map((c) => c.category).sort(), ['apps', 'live']);
+ assert.strictEqual(body.offers.find((o) => o.provider === 'deepseek').requests, 45);
+ assert(!/prj_SECRET_APP/.test(first.text), 'no developer app is named');
+ assert(!/run_|rcpt_|usr_/.test(first.text), 'no run, receipt or person id leaves the read');
+ assert.strictEqual(body.offers.every((o) => o.request_share > 0 && o.request_share <= 1), true);
+
+ const second = await request(h.base, 'GET', '/api/v1/exchange/share');
+ assert(second.body.recomputed_at === null);
+ assert.strictEqual((await h.db.prepare('SELECT COUNT(*)::int AS n FROM exchange_traffic_share').get()).n, 1, 'the week is stored once, not recomputed on every read');
+});
+
+t.test('a named week is validated and old receipts are not a silent empty week', async () => {
+ const day = iso(weekStart).slice(0, 10);
+ const stored = await request(h.base, 'GET', `/api/v1/exchange/share?week=${day}`);
+ assert.strictEqual(stored.status, 200, stored.text);
+ assert.strictEqual(stored.body.week_start, day);
+ const tuesday = iso(weekStart + DAY_MS).slice(0, 10);
+ assert.strictEqual((await request(h.base, 'GET', `/api/v1/exchange/share?week=${tuesday}`)).status, 422);
+ const nextMonday = iso(mondayOf(Date.now())).slice(0, 10);
+ assert.strictEqual((await request(h.base, 'GET', `/api/v1/exchange/share?week=${nextMonday}`)).status, 422);
+ const old = iso(weekStart - 42 * DAY_MS).slice(0, 10);
+ assert.strictEqual((await request(h.base, 'GET', `/api/v1/exchange/share?week=${old}`)).status, 404);
+});
+
+(async () => {
+ try {
+ await t.run();
+ } finally {
+ if (h) await h.stop();
+ }
+})().catch((e) => { console.error(e); process.exit(1); });