openvibers/openvibe.directory Commits
Read Exchange model directory for AI listings (#5)
Changes
@@@ -3,7 +3,7 @@ const {XMLParser} = require('fast-xml-parser');const URLS = Object.freeze({sites: 'https://openvibe.network/api/frame?host=openvibe.network',tools: 'https://openvibe.tools/api/v1/tools',-- models: 'https://openvibe.compare/api/v1/comparisons/ai-models',++ models: 'https://openvibe.exchange/api/v1/models',projects: 'https://api.github.com/search/repositories?q=org%3AOpenVibers+archived%3Afalse&per_page=100',businesses: 'https://api.openstreetmap.org/api/0.6/map?bbox=-122.425,37.774,-122.415,37.780'});@@@ -23,10 +23,11 @@ function tools(data, checked) {}function models(data, checked) {const out=[], makers=new Map();-- for (const m of data.things||[]) {-- const id=m.id, maker=m.maker||String(id).split('/')[0];++ for (const m of data.models||data.things||[]) {++ const id=m.slug||m.id, maker=m.maker||String(id).split('/')[0];const source=safeUrl(m.url)||URLS.models;-- const e=entry('models',id,m.name,`AI model from ${maker}. See the dated comparison for prices and measured results.`,`https://openvibe.compare/things/${encodeURIComponent(id)}`,source,iso(data.updated_at)||checked,{provider:maker});++ const website=m.slug ? `https://openvibe.exchange/m/${m.slug.split('/').map(encodeURIComponent).join('/')}` : `https://openvibe.compare/things/${encodeURIComponent(id)}`;++ const e=entry('models',id,m.name,`AI model from ${maker}. See current provider offers, prices and measured results.`,website,source,iso(data.generated_at||data.updated_at)||checked,{provider:maker,context:m.context||null,modalities:m.modalities||[]});if(e) out.push(e);if(maker && !makers.has(maker)) makers.set(maker,entry('providers',maker,maker,'AI model provider.',source,URLS.models,iso(data.updated_at)||checked));}@@@ -1,13 +1,20 @@'use strict';const {test}=require('node:test');const assert=require('node:assert/strict');--const {createStore,businesses}=require('../server/sources');++const {createStore,businesses,models,URLS}=require('../server/sources');const {createApp}=require('../server/app');test('unverified OSM phone tags are not published and business source is attributed',()=>{const xml='<osm><node id="1"><tag k="shop" v="books"/><tag k="name" v="Bookshop"/><tag k="phone" v="+1 415 555 0100"/></node><node id="2"><tag k="name" v="Jane Smith"/><tag k="phone" v="+1 415 555 0101"/></node></osm>';const rows=businesses(xml,'2026-11-10T00:00:00.000Z');assert.equal(rows.length,1);assert.equal(rows[0].phone,undefined);assert.match(rows[0].sources[0].url,/openstreetmap/);assert.match(rows[0].osm_attribution,/ODbL/);});++test('AI directory reads Exchange model ids and links to their model pages',()=>{++ const rows=models({generated_at:'2026-11-11T00:00:00Z',models:[{slug:'google/gemini-3',name:'Gemini 3',context:1000000,modalities:['text','image']}]},'2026-11-11T01:00:00Z');++ assert.equal(URLS.models,'https://openvibe.exchange/api/v1/models');++ assert.equal(rows[0].website,'https://openvibe.exchange/m/google/gemini-3');++ assert.equal(rows[0].context,1000000);++ assert.equal(rows[1].kind,'providers');++});test('failed refresh retains the last successful source and reports stale status',async()=>{let fail=false;const fetchImpl=async(url)=>{if(fail)throw Error('source down');return {ok:true,json:async()=>url.includes('frame?')?{nav:[{id:'live',name:'Live',url:'https://openvibe.live/'}]}:{tools:[{id:'yaml',name:'YAML',docs:'https://openvibe.tools/tool/yaml'}],things:[{id:'a/b',name:'B',maker:'A',url:'https://example.com'}],items:[{full_name:'OpenVibers/Test',name:'Test',html_url:'https://github.com/OpenVibers/Test'}]},text:async()=>'<osm><node id="1"><tag k="shop" v="books"/><tag k="name" v="Books"/></node></osm>'};};const store=createStore({fetchImpl});await store.refresh();const before=store.list('sites');fail=true;await store.refresh();assert.deepEqual(store.list('sites'),before);assert.equal(store.state.status.sites.ok,false);
@@ -3,7 +3,7 @@ const {XMLParser} = require('fast-xml-parser');
@@ -3,7 +3,7 @@ const {XMLParser} = require('fast-xml-parser');
const URLS = Object.freeze({
const URLS = Object.freeze({
sites: 'https://openvibe.network/api/frame?host=openvibe.network',
sites: 'https://openvibe.network/api/frame?host=openvibe.network',
tools: 'https://openvibe.tools/api/v1/tools',
tools: 'https://openvibe.tools/api/v1/tools',
- models: 'https://openvibe.compare/api/v1/comparisons/ai-models',
+ models: 'https://openvibe.exchange/api/v1/models',
projects: 'https://api.github.com/search/repositories?q=org%3AOpenVibers+archived%3Afalse&per_page=100',
projects: 'https://api.github.com/search/repositories?q=org%3AOpenVibers+archived%3Afalse&per_page=100',
businesses: 'https://api.openstreetmap.org/api/0.6/map?bbox=-122.425,37.774,-122.415,37.780'
businesses: 'https://api.openstreetmap.org/api/0.6/map?bbox=-122.425,37.774,-122.415,37.780'
});
});
@@ -23,10 +23,11 @@ function tools(data, checked) {
@@ -23,10 +23,11 @@ function tools(data, checked) {
}
}
function models(data, checked) {
function models(data, checked) {
const out=[], makers=new Map();
const out=[], makers=new Map();
- for (const m of data.things||[]) {
- const id=m.id, maker=m.maker||String(id).split('/')[0];
+ for (const m of data.models||data.things||[]) {
+ const id=m.slug||m.id, maker=m.maker||String(id).split('/')[0];
const source=safeUrl(m.url)||URLS.models;
const source=safeUrl(m.url)||URLS.models;
- const e=entry('models',id,m.name,`AI model from ${maker}. See the dated comparison for prices and measured results.`,`https://openvibe.compare/things/${encodeURIComponent(id)}`,source,iso(data.updated_at)||checked,{provider:maker});
+ const website=m.slug ? `https://openvibe.exchange/m/${m.slug.split('/').map(encodeURIComponent).join('/')}` : `https://openvibe.compare/things/${encodeURIComponent(id)}`;
+ const e=entry('models',id,m.name,`AI model from ${maker}. See current provider offers, prices and measured results.`,website,source,iso(data.generated_at||data.updated_at)||checked,{provider:maker,context:m.context||null,modalities:m.modalities||[]});
if(e) out.push(e);
if(e) out.push(e);
if(maker && !makers.has(maker)) makers.set(maker,entry('providers',maker,maker,'AI model provider.',source,URLS.models,iso(data.updated_at)||checked));
if(maker && !makers.has(maker)) makers.set(maker,entry('providers',maker,maker,'AI model provider.',source,URLS.models,iso(data.updated_at)||checked));
}
}
@@ -1,13 +1,20 @@
@@ -1,13 +1,20 @@
'use strict';
'use strict';
const {test}=require('node:test');
const {test}=require('node:test');
const assert=require('node:assert/strict');
const assert=require('node:assert/strict');
-const {createStore,businesses}=require('../server/sources');
+const {createStore,businesses,models,URLS}=require('../server/sources');
const {createApp}=require('../server/app');
const {createApp}=require('../server/app');
test('unverified OSM phone tags are not published and business source is attributed',()=>{
test('unverified OSM phone tags are not published and business source is attributed',()=>{
const xml='<osm><node id="1"><tag k="shop" v="books"/><tag k="name" v="Bookshop"/><tag k="phone" v="+1 415 555 0100"/></node><node id="2"><tag k="name" v="Jane Smith"/><tag k="phone" v="+1 415 555 0101"/></node></osm>';
const xml='<osm><node id="1"><tag k="shop" v="books"/><tag k="name" v="Bookshop"/><tag k="phone" v="+1 415 555 0100"/></node><node id="2"><tag k="name" v="Jane Smith"/><tag k="phone" v="+1 415 555 0101"/></node></osm>';
const rows=businesses(xml,'2026-11-10T00:00:00.000Z');
const rows=businesses(xml,'2026-11-10T00:00:00.000Z');
assert.equal(rows.length,1);assert.equal(rows[0].phone,undefined);assert.match(rows[0].sources[0].url,/openstreetmap/);assert.match(rows[0].osm_attribution,/ODbL/);
assert.equal(rows.length,1);assert.equal(rows[0].phone,undefined);assert.match(rows[0].sources[0].url,/openstreetmap/);assert.match(rows[0].osm_attribution,/ODbL/);
});
});
+test('AI directory reads Exchange model ids and links to their model pages',()=>{
+ const rows=models({generated_at:'2026-11-11T00:00:00Z',models:[{slug:'google/gemini-3',name:'Gemini 3',context:1000000,modalities:['text','image']}]},'2026-11-11T01:00:00Z');
+ assert.equal(URLS.models,'https://openvibe.exchange/api/v1/models');
+ assert.equal(rows[0].website,'https://openvibe.exchange/m/google/gemini-3');
+ assert.equal(rows[0].context,1000000);
+ assert.equal(rows[1].kind,'providers');
+});
test('failed refresh retains the last successful source and reports stale status',async()=>{
test('failed refresh retains the last successful source and reports stale status',async()=>{
let fail=false;const fetchImpl=async(url)=>{if(fail)throw Error('source down');return {ok:true,json:async()=>url.includes('frame?')?{nav:[{id:'live',name:'Live',url:'https://openvibe.live/'}]}:{tools:[{id:'yaml',name:'YAML',docs:'https://openvibe.tools/tool/yaml'}],things:[{id:'a/b',name:'B',maker:'A',url:'https://example.com'}],items:[{full_name:'OpenVibers/Test',name:'Test',html_url:'https://github.com/OpenVibers/Test'}]},text:async()=>'<osm><node id="1"><tag k="shop" v="books"/><tag k="name" v="Books"/></node></osm>'};};
let fail=false;const fetchImpl=async(url)=>{if(fail)throw Error('source down');return {ok:true,json:async()=>url.includes('frame?')?{nav:[{id:'live',name:'Live',url:'https://openvibe.live/'}]}:{tools:[{id:'yaml',name:'YAML',docs:'https://openvibe.tools/tool/yaml'}],things:[{id:'a/b',name:'B',maker:'A',url:'https://example.com'}],items:[{full_name:'OpenVibers/Test',name:'Test',html_url:'https://github.com/OpenVibers/Test'}]},text:async()=>'<osm><node id="1"><tag k="shop" v="books"/><tag k="name" v="Books"/></node></osm>'};};
const store=createStore({fetchImpl});await store.refresh();const before=store.list('sites');fail=true;await store.refresh();assert.deepEqual(store.list('sites'),before);assert.equal(store.state.status.sites.ok,false);
const store=createStore({fetchImpl});await store.refresh();const before=store.list('sites');fail=true;await store.refresh();assert.deepEqual(store.list('sites'),before);assert.equal(store.state.status.sites.ok,false);