openvibers/openvibe.ai Commits
Free allowance first only for a provider that can answer (#456)
Changes
@@@ -724,7 +724,9 @@ function createProviderPool({ db, registry, config, clock = { now: () => Date.noconst rest = [];for (const c of list) {const p = await registry.getProvider(c.provider);-- if (!p) { rest.push(c); continue; }++ // A provider that is off or has no key is never moved ahead: its free tokens cannot be spent yet.++ const usable = p && p.status === 'active' && (p.auth_mode === 'none' || credentialNotRequired(p) || Boolean(providerSecret(p, env)));++ if (!usable) { rest.push(c); continue; }if (privateRequest && traits.trainsOrRetains(p)) { rest.push(c); continue; }const row = c.model ? await registry.getModel(c.provider, c.model) : null;const allowed = traits.latencyAllows(p, row, ctx.latencyClass || 'interactive');@@@ -144,7 +144,17 @@ async function seedPoolProviders({ registry, actor = 'system', env = process.envout.models.push(`${p.key}/${m.model_key}`);continue;}-- if (row.status !== 'active') continue; // the owner turned it off: it stays off++ if (row.status !== 'active') {++ // The owner turned the model off: it stays off, and its price and status are the owner's. The++ // catalogue still records the offer's own routing and service tier (the OpenRouter Particle pin),++ // because those are facts about the offer, not a price or a switch.++ const next = modelOf(p, m);++ const facts = {};++ if (m.routing && JSON.stringify((row.metadata && row.metadata.routing) || null) !== JSON.stringify(m.routing)) facts.routing = m.routing;++ if (m.service_tier && !(row.metadata && row.metadata.service_tier)) facts.service_tier = m.service_tier;++ if (Object.keys(facts).length) await registry.upsertModel({ ...row, metadata: { ...(row.metadata || {}), ...facts } }, { actor });++ continue;++ }if (row.metadata && row.metadata.price_book) continue; // the price book owns this price: it refreshes itif (row.cost.in_per_mtok != null && !cataloguePrice(row)) continue; // a person's own price: it staysconst next = modelOf(p, m);@@@ -16,6 +16,7 @@ const { boot, request, token, suite, ALL } = require('./helpers');const { createPriceStore } = require('../server/pricebook/store');const { proposePriceChanges } = require('../server/pricebook/propose');const compute = require('../server/gpu/compute-providers');++const seed = require('../server/providers/pool-seed');const t = suite('provider-controls');const OWNER = token('owner', ['ai.provider.manage', 'ai.usage.read']);@@@ -119,6 +120,20 @@ t.test('the confirmed readings become pending changes; nothing is applied automaassert.strictEqual(live.cost.in_per_mtok, null, 'the live price is still unchanged');});++t.test('a disabled model keeps its price and status but gains the offer routing the catalogue states', async () => {++ const key = 'deepseek/deepseek-v4.1-flash-1017';++ const row = await h.registry.getModel('openrouter', key);++ // The production shape: an OpenRouter catalogue row already exists, off, with OpenRouter's own list price.++ await h.registry.upsertModel({ ...row, status: 'disabled', cost: { in_per_mtok: 0.006, out_per_mtok: 0.5, source: 'https://openrouter.ai/api/v1/models' },++ metadata: { ...(row.metadata || {}), routing: undefined, service_tier: undefined } });++ await seed.seedPoolProviders({ registry: h.registry, actor: 'test' });++ const after = await h.registry.getModel('openrouter', key);++ assert.strictEqual(after.status, 'disabled', 'the owner switch stays');++ assert.strictEqual(after.cost.in_per_mtok, 0.006, 'the live price is not overwritten');++ assert.deepStrictEqual(after.metadata.routing, { only: ['particle-ai'], allow_fallbacks: false, service_tier: 'flex', data_collection: 'deny' }, 'the offer routing is recorded');++ assert.strictEqual(after.metadata.service_tier, 'flex');++});++t.test('the rented capacity records name their prices and the startup credit note', () => {const runpod = compute.get('runpod');assert.strictEqual(compute.cheapest(runpod), 0.27);
@@ -724,7 +724,9 @@ function createProviderPool({ db, registry, config, clock = { now: () => Date.no
@@ -724,7 +724,9 @@ function createProviderPool({ db, registry, config, clock = { now: () => Date.no
const rest = [];
const rest = [];
for (const c of list) {
for (const c of list) {
const p = await registry.getProvider(c.provider);
const p = await registry.getProvider(c.provider);
- if (!p) { rest.push(c); continue; }
+ // A provider that is off or has no key is never moved ahead: its free tokens cannot be spent yet.
+ const usable = p && p.status === 'active' && (p.auth_mode === 'none' || credentialNotRequired(p) || Boolean(providerSecret(p, env)));
+ if (!usable) { rest.push(c); continue; }
if (privateRequest && traits.trainsOrRetains(p)) { rest.push(c); continue; }
if (privateRequest && traits.trainsOrRetains(p)) { rest.push(c); continue; }
const row = c.model ? await registry.getModel(c.provider, c.model) : null;
const row = c.model ? await registry.getModel(c.provider, c.model) : null;
const allowed = traits.latencyAllows(p, row, ctx.latencyClass || 'interactive');
const allowed = traits.latencyAllows(p, row, ctx.latencyClass || 'interactive');
@@ -144,7 +144,17 @@ async function seedPoolProviders({ registry, actor = 'system', env = process.env
@@ -144,7 +144,17 @@ async function seedPoolProviders({ registry, actor = 'system', env = process.env
out.models.push(`${p.key}/${m.model_key}`);
out.models.push(`${p.key}/${m.model_key}`);
continue;
continue;
}
}
- if (row.status !== 'active') continue; // the owner turned it off: it stays off
+ if (row.status !== 'active') {
+ // The owner turned the model off: it stays off, and its price and status are the owner's. The
+ // catalogue still records the offer's own routing and service tier (the OpenRouter Particle pin),
+ // because those are facts about the offer, not a price or a switch.
+ const next = modelOf(p, m);
+ const facts = {};
+ if (m.routing && JSON.stringify((row.metadata && row.metadata.routing) || null) !== JSON.stringify(m.routing)) facts.routing = m.routing;
+ if (m.service_tier && !(row.metadata && row.metadata.service_tier)) facts.service_tier = m.service_tier;
+ if (Object.keys(facts).length) await registry.upsertModel({ ...row, metadata: { ...(row.metadata || {}), ...facts } }, { actor });
+ continue;
+ }
if (row.metadata && row.metadata.price_book) continue; // the price book owns this price: it refreshes it
if (row.metadata && row.metadata.price_book) continue; // the price book owns this price: it refreshes it
if (row.cost.in_per_mtok != null && !cataloguePrice(row)) continue; // a person's own price: it stays
if (row.cost.in_per_mtok != null && !cataloguePrice(row)) continue; // a person's own price: it stays
const next = modelOf(p, m);
const next = modelOf(p, m);
@@ -16,6 +16,7 @@ const { boot, request, token, suite, ALL } = require('./helpers');
@@ -16,6 +16,7 @@ const { boot, request, token, suite, ALL } = require('./helpers');
const { createPriceStore } = require('../server/pricebook/store');
const { createPriceStore } = require('../server/pricebook/store');
const { proposePriceChanges } = require('../server/pricebook/propose');
const { proposePriceChanges } = require('../server/pricebook/propose');
const compute = require('../server/gpu/compute-providers');
const compute = require('../server/gpu/compute-providers');
+const seed = require('../server/providers/pool-seed');
const t = suite('provider-controls');
const t = suite('provider-controls');
const OWNER = token('owner', ['ai.provider.manage', 'ai.usage.read']);
const OWNER = token('owner', ['ai.provider.manage', 'ai.usage.read']);
@@ -119,6 +120,20 @@ t.test('the confirmed readings become pending changes; nothing is applied automa
@@ -119,6 +120,20 @@ t.test('the confirmed readings become pending changes; nothing is applied automa
assert.strictEqual(live.cost.in_per_mtok, null, 'the live price is still unchanged');
assert.strictEqual(live.cost.in_per_mtok, null, 'the live price is still unchanged');
});
});
+t.test('a disabled model keeps its price and status but gains the offer routing the catalogue states', async () => {
+ const key = 'deepseek/deepseek-v4.1-flash-1017';
+ const row = await h.registry.getModel('openrouter', key);
+ // The production shape: an OpenRouter catalogue row already exists, off, with OpenRouter's own list price.
+ await h.registry.upsertModel({ ...row, status: 'disabled', cost: { in_per_mtok: 0.006, out_per_mtok: 0.5, source: 'https://openrouter.ai/api/v1/models' },
+ metadata: { ...(row.metadata || {}), routing: undefined, service_tier: undefined } });
+ await seed.seedPoolProviders({ registry: h.registry, actor: 'test' });
+ const after = await h.registry.getModel('openrouter', key);
+ assert.strictEqual(after.status, 'disabled', 'the owner switch stays');
+ assert.strictEqual(after.cost.in_per_mtok, 0.006, 'the live price is not overwritten');
+ assert.deepStrictEqual(after.metadata.routing, { only: ['particle-ai'], allow_fallbacks: false, service_tier: 'flex', data_collection: 'deny' }, 'the offer routing is recorded');
+ assert.strictEqual(after.metadata.service_tier, 'flex');
+});
+
t.test('the rented capacity records name their prices and the startup credit note', () => {
t.test('the rented capacity records name their prices and the startup credit note', () => {
const runpod = compute.get('runpod');
const runpod = compute.get('runpod');
assert.strictEqual(compute.cheapest(runpod), 0.27);
assert.strictEqual(compute.cheapest(runpod), 0.27);