openvibers/openvibe.ai Commits

Free allowance first only for a provider that can answer (#456)

OP OpenVibers committed 2 days ago · f2ebf912ad · 3 files, +29 −2

A switched-off or keyless provider is no longer moved to the front of the
order for its free allowance: it could not spend it, and the skip was logged
on every request. A disabled model row keeps its owner status and live price,
but the catalogue now records the routing and service tier the offer states
(the OpenRouter Particle Flex pin), so the offer is pinned when it is enabled.

Changes

ddiff --git a/server/providers/index.js b/server/providers/index.js
iindex 0d65ab4..042508f 100644
---- a/server/providers/index.js
++++ b/server/providers/index.js
@@@ -724,7 +724,9 @@ function createProviderPool({ db, registry, config, clock = { now: () => Date.no
const 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');
ddiff --git a/server/providers/pool-seed.js b/server/providers/pool-seed.js
iindex 8f29850..7e4bad8 100644
---- a/server/providers/pool-seed.js
++++ b/server/providers/pool-seed.js
@@@ -144,7 +144,17 @@ async function seedPoolProviders({ registry, actor = 'system', env = process.env
out.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 it
if (row.cost.in_per_mtok != null && !cataloguePrice(row)) continue; // a person's own price: it stays
const next = modelOf(p, m);
ddiff --git a/test/provider-controls.test.js b/test/provider-controls.test.js
iindex f10ca80..633e8b5 100644
---- a/test/provider-controls.test.js
++++ b/test/provider-controls.test.js
@@@ -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 automa
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', () => {
const runpod = compute.get('runpod');
assert.strictEqual(compute.cheapest(runpod), 0.27);