openvibers/openvibe.exchange Commits

Free is said only when it is free now (#94)

OP OpenVibers committed 7 days ago · 618d19e413 · 3 files, +40 −9

AI's catalog now carries free_now per offer and free_available overall (AI #329). The Exchange showed Free to try on
DeepSeek V4.1 Flash while production DeepSeek was out of credit and every free chat failed.

Changes

ddiff --git a/server/http/pages.js b/server/http/pages.js
iindex 425e261..cd1b41b 100644
---- a/server/http/pages.js
++++ b/server/http/pages.js
@@@ -154,9 +154,12 @@ function createPageRoutes(ctx) {
async function chatCatalog() {
const catalog = await catalogOf();
const notChat = new Set(['embedding', 'embeddings', 'stt', 'asr', 'tts', 'speech', 'image', 'images', 'rerank', 'moderation']);
-- return { catalog, models: (catalog.models || []).filter((m) => !notChat.has(String(m.kind || m.type || 'chat').toLowerCase())) };
++ const models = (catalog.models || []).filter((m) => !notChat.has(String(m.kind || m.type || 'chat').toLowerCase()));
++ // AI says when no chat model can serve the free allowance right now (free_available false).
++ models.freePaused = catalog.free_available === false;
++ return { catalog, models };
}
-- const modelOptions = (models, selected) => models.slice(0, 40).map((m) => [m.slug, m.name + (m.uncensored ? ' · 18+' : '') + ((m.offers || []).some((o) => o.free_tier === true) ? ' · Free to try' : ''), (selected || []).includes(m.slug)]);
++ const modelOptions = (models, selected) => models.slice(0, 40).map((m) => [m.slug, m.name + (m.uncensored ? ' · 18+' : '') + ((m.offers || []).some((o) => (o.free_now !== undefined ? o.free_now === true : o.free_tier === true)) ? ' · Free to try' : ''), (selected || []).includes(m.slug)]);
function chatSidebar(chats, current) {
const items = chats.map((c) => ({ href: `/chat/${c.id}`, title: c.title, current: c.id === current, at: c.updated_at }));
@@@ -193,7 +196,9 @@ ${kit.chips({ name: 'models', label: 'Compare up to three models', options: mode
return thread.composer({
action, name: 'text', placeholder: 'Ask anything…', top: settings, maxlength: config.chat.maxChars,
hidden: { chat: chat ? chat.id : '' },
-- note: `Your free daily allowance. Models marked Free to try run on it; other models need your own balance or key. Each message may run up to ${config.chat.models} models.`,
++ note: models.freePaused
++ ? `Free messages are paused right now: the free model\u2019s provider is out of credit. Your own key or Balance still works. Each message may run up to ${config.chat.models} models.`
++ : `Your free daily allowance. Models marked Free to try run on it; other models need your own balance or key. Each message may run up to ${config.chat.models} models.`,
starters: chat ? [] : ['Explain the difference between a mutex and a semaphore', 'Write a Python function that reads a CSV and sums a column', 'Which of you is cheaper for this?'],
});
}
ddiff --git a/server/views/exchange.js b/server/views/exchange.js
iindex f1fef5d..da4e92f 100644
---- a/server/views/exchange.js
++++ b/server/views/exchange.js
@@@ -98,13 +98,17 @@ function answerPrivacy(result) {
return { label: name, kind, facts: facts.join(' · ') || (d.known === false ? 'the offer declared no policy' : '') };
}
--const freeTier = (o) => Boolean(o && o.free_tier === true);
++// Free to try now: AI's free_now (a free-tier offer whose provider can answer); free_tier alone on an older AI.
++const freeTier = (o) => Boolean(o && (o.free_now !== undefined ? o.free_now === true : o.free_tier === true));
++// A free-tier offer whose provider cannot answer right now (out of credit, or down): said, never promised.
++const freePaused = (o) => Boolean(o && o.free_tier === true && o.free_now === false);
++const FREE_PAUSED = 'Free messages are paused right now: the free model\u2019s provider is out of credit. Your own key or Balance still works.';
function badgesOf(m, best) {
const out = [
m.uncensored ? kit.badge('18+', 'bad') : '',
m.community ? kit.badge('Community', 'accent') : '',
-- (m.offers || []).some(freeTier) ? kit.badge('Free to try') : '',
++ (m.offers || []).some(freeTier) ? kit.badge('Free to try') : ((m.offers || []).some(freePaused) ? kit.badge('Free paused') : ''),
m.capabilities && m.capabilities.tools ? kit.badge('Tools') : '',
];
const p = privacyOf(best);
@@@ -158,7 +162,8 @@ function home({ catalog, usage, viewer, q = '', config = null }) {
if (models.length) stats.push([models.length, 'models']);
if (providerCount(models)) stats.push([providerCount(models), 'providers']);
if (bestPrice) stats.push([money(bestPrice.price.in), 'cheapest in, per million']);
-- if (config) stats.push([config.chat.perDay, 'messages free every day']);
++ // The free allowance is a stat only while some free model can answer it.
++ if (config && catalog.free_available !== false) stats.push([config.chat.perDay, 'messages free every day']);
if (usage.today && usage.today.requests >= 1000) stats.push([count(usage.today.requests), 'requests today']);
return `<div class="ex-wrap ex-home">
@@@ -280,7 +285,7 @@ ${panel({ title: '', body: modelList(models, { empty: 'No model matches that sea
function modelPage({ model, viewer, result = null, problem = null, text = '', adult = false }) {
const offers = model.offers || [];
const best = cheapest(model);
-- const badges = [model.uncensored ? kit.badge('18+', 'bad') : '', model.community ? kit.badge('Community', 'accent') : '', offers.some(freeTier) ? kit.badge('Free to try') : ''].join('');
++ const badges = [model.uncensored ? kit.badge('18+', 'bad') : '', model.community ? kit.badge('Community', 'accent') : '', offers.some(freeTier) ? kit.badge('Free to try') : (offers.some(freePaused) ? kit.badge('Free paused') : '')].join('');
const stats = [
best ? [money(best.price.in), 'cheapest in, per M'] : null,
best ? [money(best.price.out), 'cheapest out, per M'] : null,
@@@ -301,7 +306,7 @@ function modelPage({ model, viewer, result = null, problem = null, text = '', ad
const rows = offers.map((o) => {
const p = privacyOf(o);
return {
-- provider: `<a class="ex-offer-name" href="#try">${esc(providerLabel(o))}</a>${o.trust === 'community' ? ` ${kit.badge('Community', 'accent')}` : ''}${freeTier(o) ? ` ${kit.badge('Free to try')}` : ''}`,
++ provider: `<a class="ex-offer-name" href="#try">${esc(providerLabel(o))}</a>${o.trust === 'community' ? ` ${kit.badge('Community', 'accent')}` : ''}${freeTier(o) ? ` ${kit.badge('Free to try')}` : (freePaused(o) ? ` ${kit.badge('Free paused')}` : '')}`,
price: o.price ? `<b>${money(o.price.in)}</b> / ${money(o.price.out)}` : '<span class="ex-muted">free capacity</span>',
speed: `${ms(o.speed.p50_ms)} <small>p50</small> · ${ms(o.speed.p95_ms)} <small>p95</small>`,
uptime: o.uptime == null ? '–' : pct(o.uptime),
@@@ -385,8 +390,9 @@ function tryBox(model, viewer, { result, problem, text, adult }) {
if (!canTry) {
return `<div class="ex-signin">${kit.button({ href: `/auth/login?next=${encodeURIComponent(hrefOf(model))}`, label: 'Sign in to try it' })}<span class="ex-muted">Free daily allowance, no card.</span></div>`;
}
++ const paused = (model.offers || []).some(freePaused) && !(model.offers || []).some(freeTier);
return `<form class="ovk-form ex-try" method="post" action="${hrefOf(model)}/try">
--${problem ? kit.notice({ kind: 'error', text: problem }) : ''}
++${problem ? kit.notice({ kind: 'error', text: problem }) : ''}${paused ? kit.notice({ kind: 'info', text: FREE_PAUSED }) : ''}
<label class="ex-label" for="try-text">Try ${esc(model.name)}</label>
<textarea id="try-text" name="text" rows="4" maxlength="4000" required placeholder="Ask it anything. The answer is one run on your own allowance.${model.uncensored ? ' The floor applies.' : ''}">${esc(text)}</textarea>
<div class="ex-try-row">${kit.button({ label: 'Send', type: 'submit' })}<span class="ex-muted">Free daily allowance; the run’s cost is shown with the answer.</span></div>
ddiff --git a/test/pages.test.js b/test/pages.test.js
iindex 1d0f627..63ba800 100644
---- a/test/pages.test.js
++++ b/test/pages.test.js
@@@ -168,6 +168,26 @@ const { createFakeAi, downAi } = require('./helpers/ai');
});
} finally { await o.close(); }
++ // The free model's provider is out of credit: AI marks its free-tier offer not free now (free_now false) and
++ // says no chat model can serve the free allowance (free_available false). Nothing promises free capacity.
++ const pausedAi = createFakeAi();
++ pausedAi.catalogData.free_available = false;
++ for (const m of pausedAi.catalogData.models) for (const o of m.offers || []) if (o.free_tier) { o.free_now = false; o.status = 'exhausted'; }
++ const paused = await boot({ ai: pausedAi });
++ try {
++ await check('free paused: no Free to try, a plain note, and no free-messages stat', async () => {
++ const home = await paused.get('/');
++ assert.strictEqual(home.status, 200);
++ assert.ok(!home.text.includes('Free to try'), 'no offer is promised as free');
++ assert.ok(!home.text.includes('messages free every day'), 'the free allowance is not a stat while paused');
++ const models = await paused.get('/models');
++ assert.ok(models.text.includes('Free paused'), 'the paused offer says so');
++ const freeModel = pausedAi.catalogData.models.find((m) => (m.offers || []).some((o) => o.free_tier));
++ const page = await paused.get('/m/' + freeModel.slug);
++ assert.ok(page.text.includes('Free paused'));
++ });
++ } finally { await paused.close(); }
++
const broken = await boot({ ai: createFakeAi(), openapiFetch: async () => { throw new Error('down'); } });
try {
await check('/openapi.json still answers when the API host is down', async () => {