openvibers/openvibe.exchange Commits
Include live catalog models in public directory API (#206)
Changes
@@@ -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();@@@ -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 };@@@ -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'));
@@ -281,8 +281,7 @@ async function createApp(opts = {}) {
@@ -281,8 +281,7 @@ async function createApp(opts = {}) {
const directoryRows = async () => {
const directoryRows = async () => {
const [directory, catalog] = await Promise.all([ai.directory(), ai.catalog()]);
const [directory, catalog] = await Promise.all([ai.directory(), ai.catalog()]);
if (directory.unavailable) return null;
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) => {
app.get('/api/v1/models', async (req, res) => {
const rows = await directoryRows();
const rows = await directoryRows();
@@ -3,6 +3,30 @@ const { esc } = require('./render/html');
@@ -3,6 +3,30 @@ const { esc } = require('./render/html');
const href = id => `/models/${encodeURIComponent(id)}`;
const href = id => `/models/${encodeURIComponent(id)}`;
const allowedStatus = new Set(['available', 'preview', 'spotted', 'retired']);
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 = {}) {
function select(models, query = {}) {
const q = String(query.q || '').toLowerCase().slice(0, 120);
const q = String(query.q || '').toLowerCase().slice(0, 120);
const status = String(query.status || '').toLowerCase();
const status = String(query.status || '').toLowerCase();
@@ -34,4 +58,4 @@ function directoryDetail(m) {
@@ -34,4 +58,4 @@ function directoryDetail(m) {
${m.retired_at ? `<dt>Retired</dt><dd>${esc(m.retired_at)}</dd>` : ''}</dl>
${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>`;
${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 };
@@ -63,6 +63,11 @@ const { createFakeAi, downAi } = require('./helpers/ai');
@@ -63,6 +63,11 @@ const { createFakeAi, downAi } = require('./helpers/ai');
assert.strictEqual(one.status, 200, one.text);
assert.strictEqual(one.status, 200, one.text);
assert.strictEqual(one.json().model.slug, 'deepseek/deepseek-chat');
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');
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);
assert.strictEqual((await t.get('/api/v1/models/nope/nope')).status, 404);
const html = await t.get('/m/deepseek/deepseek-chat');
const html = await t.get('/m/deepseek/deepseek-chat');
assert.ok(html.text.includes('schema.org') && html.text.includes('SoftwareApplication'));
assert.ok(html.text.includes('schema.org') && html.text.includes('SoftwareApplication'));