openvibers/openvibe.exchange Commits

Include live catalog models in public directory API (#206)

OP OpenVibers committed 2 days ago · 7d6883b12b · 3 files, +31 −3

* Include live catalog models in public directory API

* Use offer update time for catalog-only prices

Changes

ddiff --git a/server/app.js b/server/app.js
iindex eea7eea..688eb06 100644
---- a/server/app.js
++++ b/server/app.js
@@@ -281,8 +281,7 @@ async function createApp(opts = {}) {
const directoryRows = async () => {
const [directory, catalog] = await Promise.all([ai.directory(), ai.catalog()]);
if (directory.unavailable) return null;
-- const live = new Map((catalog.models || []).map(m => [m.slug, m]));
-- return directory.models.map(m => ({ ...m, slug: m.id, offers: live.get(m.id)?.offers || [] }));
++ return require('./model-directory').withLiveCatalog(directory.models, catalog.models || []);
};
app.get('/api/v1/models', async (req, res) => {
const rows = await directoryRows();
ddiff --git a/server/model-directory.js b/server/model-directory.js
iindex 18efeb6..0eecf45 100644
---- a/server/model-directory.js
++++ b/server/model-directory.js
@@@ -3,6 +3,30 @@ const { esc } = require('./render/html');
const href = id => `/models/${encodeURIComponent(id)}`;
const allowedStatus = new Set(['available', 'preview', 'spotted', 'retired']);
++function withLiveCatalog(directoryModels = [], catalogModels = []) {
++ const live = new Map(catalogModels.map(m => [m.slug, m]));
++ const rows = directoryModels.map(m => ({ ...m, slug: m.id, offers: live.get(m.id)?.offers || [] }));
++ const seen = new Set(rows.map(m => m.id));
++ for (const m of catalogModels) {
++ if (!m.slug || seen.has(m.slug)) continue;
++ const offers = Array.isArray(m.offers) ? m.offers : [];
++ const priced = offers.filter(o => o.price && Number.isFinite(Number(o.price.out)));
++ priced.sort((a, b) => Number(a.price.out) - Number(b.price.out));
++ const cheapest = priced[0];
++ const [provider, ...modelParts] = m.slug.split('/');
++ rows.push({
++ id: m.slug, slug: m.slug, provider, model: modelParts.join('/'), name: m.name || m.slug,
++ family: m.family || null, status: 'available', routing_enabled: offers.length > 0,
++ context_window: m.context ?? null, max_output: m.max_output ?? null, modalities: m.modalities || [],
++ price: { input_per_million: cheapest?.price?.in ?? null, output_per_million: cheapest?.price?.out ?? null,
++ cached_per_million: cheapest?.price?.cached ?? null, checked_at: cheapest?.updated_at || null,
++ source: null },
++ source: null, offers,
++ });
++ seen.add(m.slug);
++ }
++ return rows;
++}
function select(models, query = {}) {
const q = String(query.q || '').toLowerCase().slice(0, 120);
const status = String(query.status || '').toLowerCase();
@@@ -34,4 +58,4 @@ function directoryDetail(m) {
${m.retired_at ? `<dt>Retired</dt><dd>${esc(m.retired_at)}</dd>` : ''}</dl>
${m.sources?.length ? `<h2>Sources</h2><ul>${m.sources.map(s => `<li>${link(s.url)} · seen ${esc(s.observed_at || 'date not listed')}</li>`).join('')}</ul>` : ''}</div>`;
}
--module.exports = { href, select, directoryList, directoryDetail };
++module.exports = { href, select, withLiveCatalog, directoryList, directoryDetail };
ddiff --git a/test/pages.test.js b/test/pages.test.js
iindex b20e1a9..7cab080 100644
---- a/test/pages.test.js
++++ b/test/pages.test.js
@@@ -63,6 +63,11 @@ const { createFakeAi, downAi } = require('./helpers/ai');
assert.strictEqual(one.status, 200, one.text);
assert.strictEqual(one.json().model.slug, 'deepseek/deepseek-chat');
assert.strictEqual((await t.get('/api/v1/models/deepseek%2Fdeepseek-chat')).json().model.slug, 'deepseek/deepseek-chat');
++ const liveOnly = await t.get('/api/v1/models/meta-llama/llama-3-8b-abliterated');
++ assert.strictEqual(liveOnly.status, 200, liveOnly.text);
++ assert.strictEqual(liveOnly.json().model.status, 'available');
++ assert.ok(liveOnly.json().model.offers.length > 0);
++ assert.ok(list.json().models.some(m => m.id === 'deepseek/deepseek-chat'));
assert.strictEqual((await t.get('/api/v1/models/nope/nope')).status, 404);
const html = await t.get('/m/deepseek/deepseek-chat');
assert.ok(html.text.includes('schema.org') && html.text.includes('SoftwareApplication'));