openvibers/openvibe.chat Commits
Point Chat machine flows to OpenVibe Link (#424)
Changes
@@@ -356,7 +356,7 @@ API: `GET|PUT|DELETE /api/chat/me/ai-key` (own sign-in only, not API tokens); palinked from the channel on the server's settings page.**A model on their own computer** (AI on your own machines; contracts 0.220.0/0.221.0). The key may instead be provider--`node`: a `machine` (a `dev_` id from openvibe.bot/machines, or `any`, the fastest of theirs that is on) and a model,++`node`: a `machine` (a `dev_` id from openvibe.link/machines, or `any`, the fastest of theirs that is on) and a model,with no key and nothing spent. OpenVibe.AI reaches it through OpenVibe.Bot and the person's OpenVibe Link, only where theyturned on "Let my AI use this computer's models".- **The form** shows only the fields the chosen provider takes (`chat.css` `:has()`). It comes prefilled from Bot's@@@ -17,7 +17,7 @@"express-rate-limit": "^7.5.0","iovalkey": "^0.4.0","multer": "^2.0.2",-- "openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.338.0",++ "openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.372.0","openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.123.0","openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v4.173.0","pg": "^8.23.0",@@@ -1557,9 +1557,9 @@}},"node_modules/openvibe-contracts": {-- "version": "0.338.0",-- "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.338.0",-- "integrity": "sha512-75FD6IxMsXoud3NFXKDs0/D0rhJtF2D+WL8M92ArIyabkUnM/sZvw6E6L97EqwQ3O7M/kB+uwsNSxGudbil+ng==",++ "version": "0.372.0",++ "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.372.0",++ "integrity": "sha512-HnfBnkhfMXYNbW4NnpTAMqZc0uV9d4XvwA20VaZXZFbBTPZiClYfoZ0/TWWWh24BFANx9TSzC+4jE0WDHKuNhA==","license": "AGPL-3.0-only","dependencies": {"ajv": "^8.17.1",@@@ -29,7 +29,7 @@"express-rate-limit": "^7.5.0","iovalkey": "^0.4.0","multer": "^2.0.2",-- "openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.338.0",++ "openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.372.0","openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.123.0","openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v4.173.0","pg": "^8.23.0",@@@ -13,14 +13,14 @@* Needs Network's grant [chat, ai.credential.manage, openvibe.ai]. Refusals come back as CredentialError (status, code).** Provider node (contracts 0.220.0) is a model on the person's own computer instead: `machine` names it (a dev_ id on-- * openvibe.bot/machines), there is no key and nothing is spent; OpenVibe.AI reaches it through OpenVibe.Bot, and only++ * openvibe.link/machines), there is no key and nothing is spent; OpenVibe.AI reaches it through OpenVibe.Bot, and only* while the person lets their AI use that computer's models there.*/const config = require('../config');const aiClient = require('./client');const SUBJECT_RE = /^usr_[0-9A-HJKMNP-TV-Z]{26}$/;--const PROVIDERS = { openai: 'OpenAI (or any OpenAI-compatible endpoint)', anthropic: 'Anthropic', node: 'A model on your own computer' };++const PROVIDERS = { openai: 'OpenAI (or any OpenAI-compatible endpoint)', anthropic: 'Anthropic', link: 'A model on your own computer' };const MACHINE_RE = /^dev_[0-9A-HJKMNP-TV-Z]{26}$/;class CredentialError extends Error { constructor(status, code, message) { super(message); this.status = status; this.code = code; } }@@@ -53,13 +53,13 @@ async function put(subject, { provider, base_url, api_key, model, budget_usd_perif (!PROVIDERS[provider]) fail(422, 'credentials.provider', 'Choose OpenAI (or an OpenAI-compatible endpoint), Anthropic, or a model on your own computer.');const m = String(model || '').trim();if (!m || m.length > 120 || /\s/.test(m)) fail(422, 'credentials.model', 'Name the model, for example gpt-4o-mini or claude-haiku-4-5.');-- if (provider === 'node') {++ if (provider === 'link') {// A model on the person's own computer: the machine, no key, no budget (nothing is spent).const id = String(machine || '').trim();// `any` (contracts 0.221.0): whichever of their computers is on with the model, the fastest first.-- if (!MACHINE_RE.test(id) && id !== 'any') fail(422, 'credentials.machine', 'Name your computer (its id, dev_…, from openvibe.bot/machines), or choose any of your computers.');++ if (!MACHINE_RE.test(id) && id !== 'any') fail(422, 'credentials.machine', 'Name your computer (its id, dev_…, from openvibe.link/machines), or choose any of your computers.');try {-- return view(await call('PUT', subject, { provider: 'node', machine: id, models: { chat: m } }));++ return view(await call('PUT', subject, { provider: 'link', machine: id, models: { chat: m } }));} catch (err) {if (err instanceof CredentialError) throw err;if (err && err.status >= 400 && err.status < 500) throw new CredentialError(err.status, 'credentials.refused', (err.detail || err.message || 'OpenVibe.AI refused it.').slice(0, 200));@@@ -93,12 +93,12 @@ async function remove(subject) {}}--const label = (c) => (c && c.provider === 'node' ? `${c.machine === 'any' ? 'any of your own computers' : 'your own computer'} · ${c.model || 'its model'}` : c ? `${c.provider}${c.base_url ? ` (${(() => { try { return new URL(c.base_url).hostname; } catch { return 'custom'; } })()})` : ''} · ${c.model || 'default model'}` : null);++const label = (c) => (c && c.provider === 'link' ? `${c.machine === 'any' ? 'any of your own computers' : 'your own computer'} · ${c.model || 'its model'}` : c ? `${c.provider}${c.base_url ? ` (${(() => { try { return new URL(c.base_url).hostname; } catch { return 'custom'; } })()})` : ''} · ${c.model || 'default model'}` : null);/** What a room's members see an AI member think with: "<model> on Fran's computer", or "openai · gpt-4o-mini, Fran's key". */const memberLabel = (c, name) => {const who = String(name || 'a member').slice(0, 40);-- return c && c.provider === 'node' ? `${c.model || 'a model'} on ${who}'s computer${c.machine === 'any' ? 's' : ''}` : `${label(c)}, ${who}'s key`;++ return c && c.provider === 'link' ? `${c.model || 'a model'} on ${who}'s computer${c.machine === 'any' ? 's' : ''}` : `${label(c)}, ${who}'s key`;};module.exports = { get, put, remove, label, memberLabel, CredentialError, PROVIDERS };@@@ -487,7 +487,7 @@ async function afterPost(room, message, user, deps = {}) {return await say(resting ? 'AI is resting for a moment. Try again in a minute.' : 'I could not answer just now. Try again in a minute.');}if (onComputer) return await say(vibe-- ? 'I could not reach the model on your computer just now. Is it on, with its model server running (Ollama, LM Studio or llama.cpp), and "Let my AI use this computer\'s models" on at openvibe.bot/machines? You can switch me back to OpenVibe\'s model in Settings, under "Your AI key".'++ ? 'I could not reach the model on your computer just now. Is it on, with its model server running (Ollama, LM Studio or llama.cpp), and "Let my AI use this computer\'s models" on at openvibe.link/machines? You can switch me back to OpenVibe\'s model in Settings, under "Your AI key".': 'I could not reach the model on my manager\'s computer just now. Try again later, or the room\'s owner can switch me to OpenVibe\'s model.');return await say(vibe? 'I could not reach your model just now (your key may be out of today\'s budget, or the provider did not answer). Try again later, or switch me back to OpenVibe\'s model in Settings, under "Your AI key".'@@@ -12,7 +12,7 @@ const controlLease = require('./control-lease');const rooms = require('./rooms');const parties = require('./parties');--const NODE = /^node:(nod_[0-9A-HJKMNP-TV-Z]{26})$/;++const NODE = /^link:(lnk_[0-9A-HJKMNP-TV-Z]{26})$/;const SHARE = /^shr_[0-9A-HJKMNP-TV-Z]{26}$/;async function nodeOf(req) {@@@ -21,7 +21,7 @@ async function nodeOf(req) {const r = await verifyServiceToken(header.slice(7).trim(), {...keys().verifyOptions, issuer: config.networkUrl, audience: 'openvibe.chat', contracts,});-- if (!r.ok || r.claims.actor_type !== 'node') return null;++ if (!r.ok || r.claims.actor_type !== 'link') return null;const m = NODE.exec(String(r.claims.sub || ''));const owner = String(r.claims.on_behalf_of || '');return m && /^usr_[0-9A-HJKMNP-TV-Z]{26}$/.test(owner) ? { id: m[1], owner } : null;@@@ -64,7 +64,7 @@ function titleOf(text) {const newSlug = () => `v-${crypto.randomBytes(8).toString('hex').slice(0, 12)}`;/** What Vibe thinks with, said to its person: "llama3.1:8b on your computer", "your openai key (gpt-4o-mini)". */--const youOf = (cred) => (cred.provider === 'node'++const youOf = (cred) => (cred.provider === 'link'? `${cred.model || 'a model'} on your computer${cred.machine === 'any' ? 's' : ''}`: `your ${cred.provider} key (${cred.model || 'its default model'})`);@@@ -662,21 +662,21 @@ ${await aiKeySection(actor, req.query || {})}`,try { key = await credentials.get(actor.user.subject_id); } catch { unreachable = true; }const opt = (v, label, cur) => `<option value="${v}"${v === cur ? ' selected' : ''}>${label}</option>`;const spent = key ? `$${key.spent_usd_today.toFixed(2)} of ${key.budget_usd_per_day ? `$${key.budget_usd_per_day.toFixed(2)}` : 'no cap'} used today` : '';-- // From openvibe.bot/machines, "Use these models in OpenVibe.Chat": a model on your own computer, filled in.-- const offered = q.own === 'node' && /^(dev_[0-9A-HJKMNP-TV-Z]{26}|any)$/.test(String(q.machine || '')) ? { machine: String(q.machine), model: /^[A-Za-z0-9._:/-]{1,120}$/.test(String(q.model || '')) ? String(q.model) : '' } : null;-- const provider = offered ? 'node' : key ? key.provider : 'openai';++ // From openvibe.link/machines, "Use these models in OpenVibe.Chat": a model on your own computer, filled in.++ const offered = q.own === 'link' && /^(dev_[0-9A-HJKMNP-TV-Z]{26}|any)$/.test(String(q.machine || '')) ? { machine: String(q.machine), model: /^[A-Za-z0-9._:/-]{1,120}$/.test(String(q.model || '')) ? String(q.model) : '' } : null;++ const provider = offered ? 'link' : key ? key.provider : 'openai';const machineValue = offered ? offered.machine : (key && key.machine) || '';const modelValue = offered && offered.model ? offered.model : (key && key.model) || '';-- const keyLine = key && key.provider === 'node' ? `${key.machine === 'any' ? 'on whichever of your computers is on, the fastest first' : `on your computer <code>${esc(key.machine || '')}</code>`} · nothing spent` : key ? `key ending ${esc(key.key_hint || '····')} · ${esc(spent)}` : '';++ const keyLine = key && key.provider === 'link' ? `${key.machine === 'any' ? 'on whichever of your computers is on, the fastest first' : `on your computer <code>${esc(key.machine || '')}</code>`} · nothing spent` : key ? `key ending ${esc(key.key_hint || '····')} · ${esc(spent)}` : '';const vibeOwn = key ? (await require('../rooms/vibe').engineOf(actor.user)).own : false;const vibeBox = require('../rooms/vibe').enabled() ? `<label class="oc-check"><input type="checkbox" name="vibe" value="1"${vibeOwn || offered ? ' checked' : ''}> Vibe thinks with it too (in all your chats with Vibe; its answers then have no daily limit)</label>` : '';const open = offered || q.saved || q.error ? ' open' : '';return `<details class="oc-card oc-fold oc-aikey" id="oc-ai-key"${open}><summary><span>Your AI key${key ? ` <span class="oc-muted">· ${esc(credentials.label(key))}</span>` : ''}</span>${IC.chevron}</summary>--<p class="oc-muted">AI members you add to a room, your Vibe among them, can think with your own provider and model instead of OpenVibe's: OpenAI, Anthropic, any OpenAI-compatible endpoint (OpenRouter, Groq, Together, your own server), or <strong>a model on your own computer</strong> (Ollama, LM Studio or llama.cpp through the OpenVibe Link, free, with no port opened: turn on "Let my AI use this computer's models" for it on <a href="https://openvibe.bot/machines">openvibe.bot/machines</a>). A key is kept encrypted in OpenVibe.AI and never shown again; the room's messages go to your provider or your computer when one of those members answers, and a key's replies count against your daily budget, not the room's limit.</p>++<p class="oc-muted">AI members you add to a room, your Vibe among them, can think with your own provider and model instead of OpenVibe's: OpenAI, Anthropic, any OpenAI-compatible endpoint (OpenRouter, Groq, Together, your own server), or <strong>a model on your own computer</strong> (Ollama, LM Studio or llama.cpp through the OpenVibe Link, free, with no port opened: turn on "Let my AI use this computer's models" for it on <a href="https://openvibe.link/machines">openvibe.link/machines</a>). A key is kept encrypted in OpenVibe.AI and never shown again; the room's messages go to your provider or your computer when one of those members answers, and a key's replies count against your daily budget, not the room's limit.</p>${offered ? notice('ok', `Save to let your AI members think with ${offered.model || 'a model'} on ${offered.machine === 'any' ? 'whichever of your computers is on' : 'your own computer'}.`) : ''}${unreachable ? notice('error', 'OpenVibe.AI did not answer, so your key cannot be shown or changed right now.') : `${key ? `<p><strong>${esc(credentials.label(key))}</strong> <span class="oc-muted">· ${keyLine}${vibeOwn ? ' · Vibe thinks with it' : ''}</span></p>` : '<p class="oc-muted">No key yet.</p>'}<form class="oc-form oc-ai-form" method="post" action="/settings/ai-key">--<label>Provider <select name="provider">${opt('openai', 'OpenAI or OpenAI-compatible', provider)}${opt('anthropic', 'Anthropic', provider)}${opt('node', 'A model on my own computer', provider)}</select></label>++<label>Provider <select name="provider">${opt('openai', 'OpenAI or OpenAI-compatible', provider)}${opt('anthropic', 'Anthropic', provider)}${opt('link', 'A model on my own computer', provider)}</select></label><label class="oc-if-node">Computer <input name="machine" maxlength="40" pattern="dev_[0-9A-Z]{26}" placeholder="dev_… (a model on your own computer only)" value="${esc(machineValue === 'any' ? '' : machineValue)}" autocomplete="off" spellcheck="false"></label><label class="oc-check oc-if-node"><input type="checkbox" name="machine_any" value="1"${machineValue === 'any' ? ' checked' : ''}> Any of my computers that is on with this model (the fastest first, the next when one is busy)</label><label class="oc-if-openai">Endpoint <input name="base_url" type="url" maxlength="300" inputmode="url" placeholder="https://openrouter.ai/api/v1 (leave empty for OpenAI itself)" value="${esc((key && key.base_url) || '')}"></label>@@@ -1077,7 +1077,7 @@ ${composer}${work ? `</div>${queuePanel(room, can, me, work)}</div>` : ''}`,if (can.manage && room.kind !== 'system' && aiMembers.enabled()) { try { myKey = await credentials.get(actor.user.subject_id); } catch { myKey = null; } }const engineField = (a) => {const cur = a ? (a.engine || 'openvibe') : 'openvibe';-- const ownLabel = a && cur === 'own' ? aiMembers.modelOf(a) : myKey && myKey.provider === 'node' ? `Your own computer: ${myKey.model || 'its model'}` : myKey ? `Your own key: ${credentials.label(myKey)}` : 'Your own key (add one in your chat settings first)';++ const ownLabel = a && cur === 'own' ? aiMembers.modelOf(a) : myKey && myKey.provider === 'link' ? `Your own computer: ${myKey.model || 'its model'}` : myKey ? `Your own key: ${credentials.label(myKey)}` : 'Your own key (add one in your chat settings first)';return `<label>Thinks with <select name="engine"><option value="openvibe"${cur === 'openvibe' ? ' selected' : ''}>OpenVibe's model</option><option value="own"${cur === 'own' ? ' selected' : ''}${cur === 'own' || myKey ? '' : ' disabled'}>${esc(ownLabel)}</option></select></label>${myKey || cur === 'own' ? '' : '<p class="oc-muted">To give it a different model (yours, from OpenAI, Anthropic or an OpenAI-compatible provider, or a model on your own computer), <a href="/settings#oc-ai-key">add your AI key</a>.</p>'}`;};@@@ -101,7 +101,7 @@ function rail({ chats, current, signedIn, allowance, query = '', here = null })const allow = own ? `<p class="vb-allow" id="vb-allow" data-own="1">No daily limit: Vibe thinks with ${esc(own.you)}</p>`: left != null ? `<p class="vb-allow" id="vb-allow">${left} of ${allowance.per_day} answers left today</p>` : '';// Chat's other places, quietly at the foot (the network's navbar reaches them too).-- const more = [['https://openvibe.bot/machines', 'Computers & robots'], ['/work', 'Work together'], ['/messages', 'Messages'], ['/rooms', 'Rooms and servers'], ['/', 'Global chat']];++ const more = [['https://openvibe.link/machines', 'Computers & robots'], ['/work', 'Work together'], ['/messages', 'Messages'], ['/rooms', 'Rooms and servers'], ['/', 'Global chat']];const html = kit.sidebar({id: 'vb-rail', label: 'Your chats with Vibe',action: { href: '/vibe', label: 'New chat', icon: 'pencil' },@@@ -177,7 +177,7 @@ ${tasks.length ? `<ol class="oc-tasks">${tasks.map((t) => taskItem(t, room, can,`<a class="oc-pill" href="${back.href}">${ICONS.chat}<span>${vibeChat ? 'Chat' : 'Room'}</span></a>`,t.result_url ? `<a class="oc-pill oc-pill-strong" href="${esc(t.result_url)}" rel="noopener nofollow">${ICONS.open}<span>Open where it landed</span></a>` : '',info && info.actor_url ? `<a class="oc-pill" href="${esc(info.actor_url)}" title="Every step the agent took, on OpenVibe.Actor">${ICONS.steps}<span>Every step</span></a>` : '',-- t.kind === 'computer' || t.kind === 'code' ? `<a class="oc-pill" href="https://openvibe.bot/machines" title="Your computers: pair one, see its screen, set what agents may do on it">${ICONS.computer}<span>Your computers</span></a>` : '',++ t.kind === 'computer' || t.kind === 'code' ? `<a class="oc-pill" href="https://openvibe.link/machines" title="Your computers: pair one, see its screen, set what agents may do on it">${ICONS.computer}<span>Your computers</span></a>` : '',].filter(Boolean).join('');const checked = info ? info.lines.find((l) => l.startsWith('Checked by')) : null;const sources = t.sources.length ? `<section class="oc-tp-card" aria-labelledby="oc-src-h"><h2 id="oc-src-h">Sources <span class="oc-muted">${t.sources.length}</span></h2>nnew file mode 100644@@@ -0,0 +1,10 @@++'use strict';++const assert = require('node:assert/strict');++const { spawnSync } = require('node:child_process');++const path = require('node:path');++const root = path.resolve(__dirname, '..');++const oldName = ['OpenVibe[ .-]No' + 'de', 'openvibe-' + 'node', 'no' + 'd_', 'node[.]openvibe[.]network', 'node:lnk_', 'openvibe[.](bot|my|network)/mach' + 'ines'].join('|');++const result = spawnSync('git', ['grep', '-n', '-I', '-i', '-E', oldName, '--', '.', ':!migrations/**', ':!test/link-brand.test.js'], { cwd: root, encoding: 'utf8' });++assert.ok(result.status === 0 || result.status === 1, result.stderr || 'git grep failed');++assert.equal(result.status, 1, `Old Link brand or principal returned:\n${(result.stdout || '').split('\n').slice(0, 10).join('\n')}`);++console.log(' ok old Link brand and principal IDs are absent');@@@ -162,9 +162,9 @@ t('a desktop party gates members, claims one Link machine, and issues member ticconst seen = await api('GET', `/parties/${slug}`, bob);assert.strictEqual(seen.body.party.lease.holder.subject, bob.subject_id);assert.strictEqual((await api('GET', `/parties/${slug}/desktop/ticket`, ann)).status, 404);-- const nodeId = ids.newId('node');++ const nodeId = ids.newId('link');const now = Math.floor(Date.now() / 1000);-- const nodeToken = (owner) => serviceAuth.signServiceToken({ iss: h.ISS, sub: `node:${nodeId}`, actor_type: 'node', on_behalf_of: owner,++ const nodeToken = (owner) => serviceAuth.signServiceToken({ iss: h.ISS, sub: `link:${nodeId}`, actor_type: 'link', on_behalf_of: owner,aud: ['openvibe.chat'], cap: [], iat: now, exp: now + 300, jti: `party_${owner}` }, h.keys.privateKey);const wrong = await h.http('POST', `/api/chat/parties/${slug}/desktop/claim`, { token: nodeToken(ann.subject_id), body: { port: 18180 } });assert.strictEqual(wrong.status, 404);@@@ -40,7 +40,7 @@ t('reading needs a signed-in person; nothing stored yet is {} at revision 0', asassert.strictEqual(r.status, 401);r = await h.http('GET', '/api/chat/preferences', { token: ann.token });assert.strictEqual(r.status, 200, r.text);-- assert.deepStrictEqual(r.body, { namespace: NS, version: 2, revision: 0, preferences: {} });++ assert.deepStrictEqual(r.body, { namespace: NS, version: 3, revision: 0, preferences: {} });assert.strictEqual(r.headers.get('etag'), '"0"');assert.ok(r.headers.get('cache-control').includes('no-store'));const tok = h.tokenRequests.find((x) => x.audience === 'openvibe.network');@@@ -35,8 +35,8 @@ const fakeHttp = {if (method === 'GET') { if (!store.has(subject)) throw problem(404, 'credential.not_found'); return store.get(subject); }if (method === 'DELETE') { if (!store.delete(subject)) throw problem(404, 'credential.not_found'); return null; }const prev = store.get(subject);-- if (json.provider === 'node') { // a model on the person's own computer: no key (contracts 0.220.0)-- const row = { owner: 'chat', subject, provider: 'node', machine: json.machine, base_url: null, models: json.models, budget_usd_per_day: null, spent_usd_today: 0 };++ if (json.provider === 'link') { // a model on the person's own computer: no key (contracts 0.220.0)++ const row = { owner: 'chat', subject, provider: 'link', machine: json.machine, base_url: null, models: json.models, budget_usd_per_day: null, spent_usd_today: 0 };store.set(subject, row);return row;}@@@ -154,40 +154,40 @@ t('a new model relabels its members; removing the key sends them back to OpenVibassert.strictEqual((await api('POST', '/rooms/model-night/ai-members', ann, { name: 'Echo', engine: 'own' })).body.code, 'rooms.ai_no_key', 'no key, no own engine');});--t('a model on your own computer: the machine and no key; the settings form comes prefilled from openvibe.bot', async () => {++t('a model on your own computer: the machine and no key; the settings form comes prefilled from openvibe.link', async () => {const MACHINE = 'dev_01M5YGM39VC2RRGDQPRD6D4SHD';const cy = h.addUser('cy', { subject: ids.newId('user') });await h.ctx.upsertUser(h.live.users.get(cy.id));-- assert.strictEqual((await api('PUT', '/me/ai-key', cy, { provider: 'node', model: 'llama3.1:8b' })).status, 422, 'it names the computer');-- assert.strictEqual((await api('PUT', '/me/ai-key', cy, { provider: 'node', machine: 'dev_nope', model: 'llama3.1:8b' })).status, 422);-- const put = await api('PUT', '/me/ai-key', cy, { provider: 'node', machine: MACHINE, model: 'llama3.1:8b' });++ assert.strictEqual((await api('PUT', '/me/ai-key', cy, { provider: 'link', model: 'llama3.1:8b' })).status, 422, 'it names the computer');++ assert.strictEqual((await api('PUT', '/me/ai-key', cy, { provider: 'link', machine: 'dev_nope', model: 'llama3.1:8b' })).status, 422);++ const put = await api('PUT', '/me/ai-key', cy, { provider: 'link', machine: MACHINE, model: 'llama3.1:8b' });assert.strictEqual(put.status, 200, JSON.stringify(put.body));-- assert.deepStrictEqual([put.body.key.provider, put.body.key.machine, put.body.key.model], ['node', MACHINE, 'llama3.1:8b']);++ assert.deepStrictEqual([put.body.key.provider, put.body.key.machine, put.body.key.model], ['link', MACHINE, 'llama3.1:8b']);const { label, memberLabel } = require('../server/ai/credentials');assert.strictEqual(label(put.body.key), 'your own computer · llama3.1:8b');assert.strictEqual(memberLabel(put.body.key, 'Cy'), 'llama3.1:8b on Cy\'s computer', 'what a room sees: whose computer, never "your own"');assert.strictEqual(memberLabel({ provider: 'openai', model: 'gpt-4o-mini' }, 'Cy'), 'openai · gpt-4o-mini, Cy\'s key');-- // From openvibe.bot/machines ("Use these models in OpenVibe.Chat"): the form is filled in, nothing is saved yet.-- const page = await web('GET', `/settings?own=node&machine=${MACHINE}&model=qwen2.5:7b`, cy);++ // From openvibe.link/machines ("Use these models in OpenVibe.Chat"): the form is filled in, nothing is saved yet.++ const page = await web('GET', `/settings?own=link&machine=${MACHINE}&model=qwen2.5:7b`, cy);const sect = page.text.slice(page.text.indexOf('id="oc-ai-key"'));-- assert.ok(sect.includes('<option value="node" selected>A model on my own computer</option>'), 'own computer chosen');++ assert.ok(sect.includes('<option value="link" selected>A model on my own computer</option>'), 'own computer chosen');assert.ok(sect.includes(`name="machine" maxlength="40"`) && sect.includes(`value="${MACHINE}"`));assert.ok(sect.includes('class="oc-form oc-ai-form"') && sect.includes('<label class="oc-if-node">Computer') && sect.includes('<label class="oc-not-node">API key') && sect.includes('<label class="oc-if-openai">Endpoint'), 'each field says which providers take it (chat.css hides the rest)');assert.ok(sect.includes('value="qwen2.5:7b"'), 'the model from the link');assert.ok(sect.includes('Save to let your AI members think with qwen2.5:7b on your own computer.'));assert.ok(sect.includes('your own computer · llama3.1:8b') && sect.includes('nothing spent'), 'what is saved now');// Any of their computers (contracts 0.221.0): the box, the label, what a room sees.-- const any = await web('POST', '/settings/ai-key', cy, { provider: 'node', machine: '', machine_any: '1', model: 'llama3.1:8b' });++ const any = await web('POST', '/settings/ai-key', cy, { provider: 'link', machine: '', machine_any: '1', model: 'llama3.1:8b' });assert.strictEqual(any.status, 303);const anyKey = (await api('GET', '/me/ai-key', cy)).body.key;assert.deepStrictEqual([anyKey.machine, label(anyKey), memberLabel(anyKey, 'Cy')], ['any', 'any of your own computers · llama3.1:8b', 'llama3.1:8b on Cy\'s computers']);const anyPage = (await web('GET', '/settings', cy)).text;assert.ok(anyPage.includes('name="machine_any" value="1" checked') && anyPage.includes('on whichever of your computers is on, the fastest first'), 'the box, checked, and the saved line');-- assert.strictEqual((await api('PUT', '/me/ai-key', cy, { provider: 'node', machine: 'anyone', model: 'llama3.1:8b' })).status, 422);-- // From openvibe.bot when the person has several computers with models: any of them, the box checked.-- const anyLink = (await web('GET', '/settings?own=node&machine=any&model=qwen2.5:7b', cy)).text;++ assert.strictEqual((await api('PUT', '/me/ai-key', cy, { provider: 'link', machine: 'anyone', model: 'llama3.1:8b' })).status, 422);++ // From openvibe.link when the person has several computers with models: any of them, the box checked.++ const anyLink = (await web('GET', '/settings?own=link&machine=any&model=qwen2.5:7b', cy)).text;assert.ok(anyLink.includes('think with qwen2.5:7b on whichever of your computers is on') && anyLink.includes('name="machine_any" value="1" checked'), 'any, from the link');-- const bad = await web('GET', '/settings?own=node&machine=../x&model=<b>', cy);++ const bad = await web('GET', '/settings?own=link&machine=../x&model=<b>', cy);assert.ok(!bad.text.includes('Save to let your AI members'), 'a bad link fills nothing');assert.strictEqual((await api('DELETE', '/me/ai-key', cy)).status, 200);});@@@ -206,7 +206,7 @@ t('Vibe thinks with your own key or computer when you choose it: every chat of yconst first = await vibeApi('POST', '', { message: 'hello there' });assert.strictEqual(first.status, 201, first.text);assert.strictEqual((await memberOf(first.body.room.slug)).engine, 'openvibe', 'OpenVibe\'s model by default');-- assert.strictEqual((await api('PUT', '/me/ai-key', di, { provider: 'node', machine: MACHINE, model: 'llama3.1:8b' })).status, 200);++ assert.strictEqual((await api('PUT', '/me/ai-key', di, { provider: 'link', machine: MACHINE, model: 'llama3.1:8b' })).status, 200);const on = await vibeApi('PUT', '/engine', { own: true });assert.deepStrictEqual(on.body, { own: true, label: 'llama3.1:8b on Di\'s computer' }, on.text);assert.deepStrictEqual(await memberOf(first.body.room.slug), { engine: 'own', engine_subject: h.live.users.get(di.id).subject_id, engine_label: 'llama3.1:8b on Di\'s computer' }, 'the chats they have');@@@ -226,15 +226,15 @@ t('Vibe thinks with your own key or computer when you choose it: every chat of y// The settings form says so and keeps it; unchecked, Vibe goes back to OpenVibe's model.const page = await web('GET', '/settings', di);assert.ok(page.text.includes('name="vibe" value="1" checked') && page.text.includes('· Vibe thinks with it'), 'the box, checked');-- const saved = await web('POST', '/settings/ai-key', di, { provider: 'node', machine: MACHINE, model: 'qwen2.5:7b' });++ const saved = await web('POST', '/settings/ai-key', di, { provider: 'link', machine: MACHINE, model: 'qwen2.5:7b' });assert.strictEqual(saved.status, 303);assert.deepStrictEqual((await vibeApi('GET', '/engine')).body, { own: false, label: null }, 'unchecked: OpenVibe\'s model');assert.ok((await web('GET', '/vibe', di)).text.includes('answers left today'), 'the free answers left again');assert.strictEqual((await memberOf(first.body.room.slug)).engine, 'openvibe');-- await web('POST', '/settings/ai-key', di, { provider: 'node', machine: MACHINE, model: 'qwen2.5:7b', vibe: '1' });++ await web('POST', '/settings/ai-key', di, { provider: 'link', machine: MACHINE, model: 'qwen2.5:7b', vibe: '1' });assert.strictEqual((await memberOf(next.body.room.slug)).engine_label, 'qwen2.5:7b on Di\'s computer', 'checked: their computer, the new model');-- // From openvibe.bot's link the box comes checked.-- assert.ok((await web('GET', `/settings?own=node&machine=${MACHINE}&model=phi3`, di)).text.includes('name="vibe" value="1" checked'));++ // From openvibe.link's link the box comes checked.++ assert.ok((await web('GET', `/settings?own=link&machine=${MACHINE}&model=phi3`, di)).text.includes('name="vibe" value="1" checked'));// The key goes: so does Vibe's use of it.assert.strictEqual((await api('DELETE', '/me/ai-key', di)).status, 200);assert.strictEqual((await memberOf(first.body.room.slug)).engine, 'openvibe');@@@ -13,7 +13,7 @@ const { plain, took } = require('../server/web/workspace-pages');const { ICONS } = require('../server/web/icons');assert.strictEqual(-- plain("I couldn't do it: **no computer is open to me**.\n\n**What I did**\n1. Asked for `list_my_computers`\n- see [the machines page](https://openvibe.bot/machines)"),++ plain("I couldn't do it: **no computer is open to me**.\n\n**What I did**\n1. Asked for `list_my_computers`\n- see [the machines page](https://openvibe.link/machines)"),"I couldn't do it: no computer is open to me. What I did Asked for list_my_computers see the machines page");assert.strictEqual(plain('```\ncode\n```\nafter'), 'after');assert.strictEqual(plain('snake_case and 2*3*4 stay'), 'snake_case and 2*3*4 stay');@@@ -103,7 +103,7 @@ t('an agent that hands its person a step on its desk: Vibe asks them in the chatconst r = await vibeApi('POST', `/${slug}/ask`, ann, { message: 'Book the cheapest flight to Lisbon', mode: 'agent' });assert.strictEqual(r.status, 201, r.text);await new Promise((res) => setTimeout(res, 150));-- const desk = 'https://openvibe.bot/machines/dev_01M4W5JBFDG2TJ8C4FRDMCWBZ9/screen?desk=desk-0123456789';++ const desk = 'https://openvibe.link/machines/dev_01M4W5JBFDG2TJ8C4FRDMCWBZ9/screen?desk=desk-0123456789';actorTask = { id: 'tsk_h1', state: 'running', needs_you: { seq: 7, what: 'Sign in to the airline.', url: desk } };await workspace.poll();await workspace.poll();@@@ -128,7 +128,7 @@ t('an agent working on a desk: Vibe offers to watch it and take it over, once peconst r = await vibeApi('POST', `/${slug}/ask`, bob, { message: 'Compare three laptops on the store site', mode: 'agent' });assert.strictEqual(r.status, 201, r.text);await new Promise((res) => setTimeout(res, 150));-- const desk = 'https://openvibe.bot/machines/dev_01M4W5JBFDG2TJ8C4FRDMCWBZ9/screen?desk=desk-9876543210';++ const desk = 'https://openvibe.link/machines/dev_01M4W5JBFDG2TJ8C4FRDMCWBZ9/screen?desk=desk-9876543210';actorTask = { id: 'tsk_d1', state: 'running' };await workspace.poll();const lines = async () => (await messagesOf(slug, bob)).filter((m) => m.ai_member_id && /Your agent is working on/.test(m.message)).map((m) => m.message);@@@ -152,7 +152,7 @@ t('the app page: signed out it invites a sign-in, signed in it lists the chats a// The rail leads to the rest of the person's agent: their tasks, their computers and robots, team workspaces.assert.ok(/href="https:\/\/openvibe\.actor\/tasks"[^>]*>[\s\S]*?<span>Tasks<\/span>/.test(signed.text), 'Tasks');assert.ok(/href="https:\/\/openvibe\.actor\/me\/agents"[^>]*>[\s\S]*?<span>Your agents<\/span>/.test(signed.text), 'Your agents: recurring work');-- assert.ok(signed.text.includes('<a href="https://openvibe.bot/machines">Computers & robots</a>'), 'Computers & robots, at the foot');++ assert.ok(signed.text.includes('<a href="https://openvibe.link/machines">Computers & robots</a>'), 'Computers & robots, at the foot');assert.ok(signed.text.includes('<a href="/work">Work together</a>'), 'Work together, at the foot');const form = await web('POST', '/vibe', ann, { message: 'hello from a form' });assert.strictEqual(form.status, 303);@@@ -200,7 +200,7 @@ t('answers render as escaped Markdown with http(s) links only', () => {assert.ok(!md.render('[x](https://a.com" onmouseover="alert(1))').includes('onmouseover="'));assert.strictEqual(md.render('```\n<i>\n```'), '<pre><code><i></code></pre>');// <https://…> (Markdown's autolink, which agents write): the address is the link; anything else in brackets stays text.-- assert.strictEqual(md.render('Go to <https://openvibe.bot/machines>.'), '<p>Go to <a href="https://openvibe.bot/machines" rel="nofollow ugc noopener" target="_blank">https://openvibe.bot/machines</a>.</p>');++ assert.strictEqual(md.render('Go to <https://openvibe.link/machines>.'), '<p>Go to <a href="https://openvibe.link/machines" rel="nofollow ugc noopener" target="_blank">https://openvibe.link/machines</a>.</p>');assert.ok(!md.render('<javascript:alert(1)>').includes('href'));// Tables and nested lists (answers are model-written Markdown, which uses both): escaped like everything else.assert.strictEqual(md.render('| Plan | Cost |\n|---|--:|\n| Rice | $2 |\n| <b>x</b> | 1 \\| 2 |'),
@@ -356,7 +356,7 @@ API: `GET|PUT|DELETE /api/chat/me/ai-key` (own sign-in only, not API tokens); pa
@@ -356,7 +356,7 @@ API: `GET|PUT|DELETE /api/chat/me/ai-key` (own sign-in only, not API tokens); pa
linked from the channel on the server's settings page.
linked from the channel on the server's settings page.
**A model on their own computer** (AI on your own machines; contracts 0.220.0/0.221.0). The key may instead be provider
**A model on their own computer** (AI on your own machines; contracts 0.220.0/0.221.0). The key may instead be provider
-`node`: a `machine` (a `dev_` id from openvibe.bot/machines, or `any`, the fastest of theirs that is on) and a model,
+`node`: a `machine` (a `dev_` id from openvibe.link/machines, or `any`, the fastest of theirs that is on) and a model,
with no key and nothing spent. OpenVibe.AI reaches it through OpenVibe.Bot and the person's OpenVibe Link, only where they
with no key and nothing spent. OpenVibe.AI reaches it through OpenVibe.Bot and the person's OpenVibe Link, only where they
turned on "Let my AI use this computer's models".
turned on "Let my AI use this computer's models".
- **The form** shows only the fields the chosen provider takes (`chat.css` `:has()`). It comes prefilled from Bot's
- **The form** shows only the fields the chosen provider takes (`chat.css` `:has()`). It comes prefilled from Bot's
@@ -17,7 +17,7 @@
@@ -17,7 +17,7 @@
"express-rate-limit": "^7.5.0",
"express-rate-limit": "^7.5.0",
"iovalkey": "^0.4.0",
"iovalkey": "^0.4.0",
"multer": "^2.0.2",
"multer": "^2.0.2",
- "openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.338.0",
+ "openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.372.0",
"openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.123.0",
"openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.123.0",
"openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v4.173.0",
"openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v4.173.0",
"pg": "^8.23.0",
"pg": "^8.23.0",
@@ -1557,9 +1557,9 @@
@@ -1557,9 +1557,9 @@
}
}
},
},
"node_modules/openvibe-contracts": {
"node_modules/openvibe-contracts": {
- "version": "0.338.0",
- "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.338.0",
- "integrity": "sha512-75FD6IxMsXoud3NFXKDs0/D0rhJtF2D+WL8M92ArIyabkUnM/sZvw6E6L97EqwQ3O7M/kB+uwsNSxGudbil+ng==",
+ "version": "0.372.0",
+ "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.372.0",
+ "integrity": "sha512-HnfBnkhfMXYNbW4NnpTAMqZc0uV9d4XvwA20VaZXZFbBTPZiClYfoZ0/TWWWh24BFANx9TSzC+4jE0WDHKuNhA==",
"license": "AGPL-3.0-only",
"license": "AGPL-3.0-only",
"dependencies": {
"dependencies": {
"ajv": "^8.17.1",
"ajv": "^8.17.1",
@@ -29,7 +29,7 @@
@@ -29,7 +29,7 @@
"express-rate-limit": "^7.5.0",
"express-rate-limit": "^7.5.0",
"iovalkey": "^0.4.0",
"iovalkey": "^0.4.0",
"multer": "^2.0.2",
"multer": "^2.0.2",
- "openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.338.0",
+ "openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.372.0",
"openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.123.0",
"openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.123.0",
"openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v4.173.0",
"openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v4.173.0",
"pg": "^8.23.0",
"pg": "^8.23.0",
@@ -13,14 +13,14 @@
@@ -13,14 +13,14 @@
* Needs Network's grant [chat, ai.credential.manage, openvibe.ai]. Refusals come back as CredentialError (status, code).
* Needs Network's grant [chat, ai.credential.manage, openvibe.ai]. Refusals come back as CredentialError (status, code).
*
*
* Provider node (contracts 0.220.0) is a model on the person's own computer instead: `machine` names it (a dev_ id on
* Provider node (contracts 0.220.0) is a model on the person's own computer instead: `machine` names it (a dev_ id on
- * openvibe.bot/machines), there is no key and nothing is spent; OpenVibe.AI reaches it through OpenVibe.Bot, and only
+ * openvibe.link/machines), there is no key and nothing is spent; OpenVibe.AI reaches it through OpenVibe.Bot, and only
* while the person lets their AI use that computer's models there.
* while the person lets their AI use that computer's models there.
*/
*/
const config = require('../config');
const config = require('../config');
const aiClient = require('./client');
const aiClient = require('./client');
const SUBJECT_RE = /^usr_[0-9A-HJKMNP-TV-Z]{26}$/;
const SUBJECT_RE = /^usr_[0-9A-HJKMNP-TV-Z]{26}$/;
-const PROVIDERS = { openai: 'OpenAI (or any OpenAI-compatible endpoint)', anthropic: 'Anthropic', node: 'A model on your own computer' };
+const PROVIDERS = { openai: 'OpenAI (or any OpenAI-compatible endpoint)', anthropic: 'Anthropic', link: 'A model on your own computer' };
const MACHINE_RE = /^dev_[0-9A-HJKMNP-TV-Z]{26}$/;
const MACHINE_RE = /^dev_[0-9A-HJKMNP-TV-Z]{26}$/;
class CredentialError extends Error { constructor(status, code, message) { super(message); this.status = status; this.code = code; } }
class CredentialError extends Error { constructor(status, code, message) { super(message); this.status = status; this.code = code; } }
@@ -53,13 +53,13 @@ async function put(subject, { provider, base_url, api_key, model, budget_usd_per
@@ -53,13 +53,13 @@ async function put(subject, { provider, base_url, api_key, model, budget_usd_per
if (!PROVIDERS[provider]) fail(422, 'credentials.provider', 'Choose OpenAI (or an OpenAI-compatible endpoint), Anthropic, or a model on your own computer.');
if (!PROVIDERS[provider]) fail(422, 'credentials.provider', 'Choose OpenAI (or an OpenAI-compatible endpoint), Anthropic, or a model on your own computer.');
const m = String(model || '').trim();
const m = String(model || '').trim();
if (!m || m.length > 120 || /\s/.test(m)) fail(422, 'credentials.model', 'Name the model, for example gpt-4o-mini or claude-haiku-4-5.');
if (!m || m.length > 120 || /\s/.test(m)) fail(422, 'credentials.model', 'Name the model, for example gpt-4o-mini or claude-haiku-4-5.');
- if (provider === 'node') {
+ if (provider === 'link') {
// A model on the person's own computer: the machine, no key, no budget (nothing is spent).
// A model on the person's own computer: the machine, no key, no budget (nothing is spent).
const id = String(machine || '').trim();
const id = String(machine || '').trim();
// `any` (contracts 0.221.0): whichever of their computers is on with the model, the fastest first.
// `any` (contracts 0.221.0): whichever of their computers is on with the model, the fastest first.
- if (!MACHINE_RE.test(id) && id !== 'any') fail(422, 'credentials.machine', 'Name your computer (its id, dev_…, from openvibe.bot/machines), or choose any of your computers.');
+ if (!MACHINE_RE.test(id) && id !== 'any') fail(422, 'credentials.machine', 'Name your computer (its id, dev_…, from openvibe.link/machines), or choose any of your computers.');
try {
try {
- return view(await call('PUT', subject, { provider: 'node', machine: id, models: { chat: m } }));
+ return view(await call('PUT', subject, { provider: 'link', machine: id, models: { chat: m } }));
} catch (err) {
} catch (err) {
if (err instanceof CredentialError) throw err;
if (err instanceof CredentialError) throw err;
if (err && err.status >= 400 && err.status < 500) throw new CredentialError(err.status, 'credentials.refused', (err.detail || err.message || 'OpenVibe.AI refused it.').slice(0, 200));
if (err && err.status >= 400 && err.status < 500) throw new CredentialError(err.status, 'credentials.refused', (err.detail || err.message || 'OpenVibe.AI refused it.').slice(0, 200));
@@ -93,12 +93,12 @@ async function remove(subject) {
@@ -93,12 +93,12 @@ async function remove(subject) {
}
}
}
}
-const label = (c) => (c && c.provider === 'node' ? `${c.machine === 'any' ? 'any of your own computers' : 'your own computer'} · ${c.model || 'its model'}` : c ? `${c.provider}${c.base_url ? ` (${(() => { try { return new URL(c.base_url).hostname; } catch { return 'custom'; } })()})` : ''} · ${c.model || 'default model'}` : null);
+const label = (c) => (c && c.provider === 'link' ? `${c.machine === 'any' ? 'any of your own computers' : 'your own computer'} · ${c.model || 'its model'}` : c ? `${c.provider}${c.base_url ? ` (${(() => { try { return new URL(c.base_url).hostname; } catch { return 'custom'; } })()})` : ''} · ${c.model || 'default model'}` : null);
/** What a room's members see an AI member think with: "<model> on Fran's computer", or "openai · gpt-4o-mini, Fran's key". */
/** What a room's members see an AI member think with: "<model> on Fran's computer", or "openai · gpt-4o-mini, Fran's key". */
const memberLabel = (c, name) => {
const memberLabel = (c, name) => {
const who = String(name || 'a member').slice(0, 40);
const who = String(name || 'a member').slice(0, 40);
- return c && c.provider === 'node' ? `${c.model || 'a model'} on ${who}'s computer${c.machine === 'any' ? 's' : ''}` : `${label(c)}, ${who}'s key`;
+ return c && c.provider === 'link' ? `${c.model || 'a model'} on ${who}'s computer${c.machine === 'any' ? 's' : ''}` : `${label(c)}, ${who}'s key`;
};
};
module.exports = { get, put, remove, label, memberLabel, CredentialError, PROVIDERS };
module.exports = { get, put, remove, label, memberLabel, CredentialError, PROVIDERS };
@@ -487,7 +487,7 @@ async function afterPost(room, message, user, deps = {}) {
@@ -487,7 +487,7 @@ async function afterPost(room, message, user, deps = {}) {
return await say(resting ? 'AI is resting for a moment. Try again in a minute.' : 'I could not answer just now. Try again in a minute.');
return await say(resting ? 'AI is resting for a moment. Try again in a minute.' : 'I could not answer just now. Try again in a minute.');
}
}
if (onComputer) return await say(vibe
if (onComputer) return await say(vibe
- ? 'I could not reach the model on your computer just now. Is it on, with its model server running (Ollama, LM Studio or llama.cpp), and "Let my AI use this computer\'s models" on at openvibe.bot/machines? You can switch me back to OpenVibe\'s model in Settings, under "Your AI key".'
+ ? 'I could not reach the model on your computer just now. Is it on, with its model server running (Ollama, LM Studio or llama.cpp), and "Let my AI use this computer\'s models" on at openvibe.link/machines? You can switch me back to OpenVibe\'s model in Settings, under "Your AI key".'
: 'I could not reach the model on my manager\'s computer just now. Try again later, or the room\'s owner can switch me to OpenVibe\'s model.');
: 'I could not reach the model on my manager\'s computer just now. Try again later, or the room\'s owner can switch me to OpenVibe\'s model.');
return await say(vibe
return await say(vibe
? 'I could not reach your model just now (your key may be out of today\'s budget, or the provider did not answer). Try again later, or switch me back to OpenVibe\'s model in Settings, under "Your AI key".'
? 'I could not reach your model just now (your key may be out of today\'s budget, or the provider did not answer). Try again later, or switch me back to OpenVibe\'s model in Settings, under "Your AI key".'
@@ -12,7 +12,7 @@ const controlLease = require('./control-lease');
@@ -12,7 +12,7 @@ const controlLease = require('./control-lease');
const rooms = require('./rooms');
const rooms = require('./rooms');
const parties = require('./parties');
const parties = require('./parties');
-const NODE = /^node:(nod_[0-9A-HJKMNP-TV-Z]{26})$/;
+const NODE = /^link:(lnk_[0-9A-HJKMNP-TV-Z]{26})$/;
const SHARE = /^shr_[0-9A-HJKMNP-TV-Z]{26}$/;
const SHARE = /^shr_[0-9A-HJKMNP-TV-Z]{26}$/;
async function nodeOf(req) {
async function nodeOf(req) {
@@ -21,7 +21,7 @@ async function nodeOf(req) {
@@ -21,7 +21,7 @@ async function nodeOf(req) {
const r = await verifyServiceToken(header.slice(7).trim(), {
const r = await verifyServiceToken(header.slice(7).trim(), {
...keys().verifyOptions, issuer: config.networkUrl, audience: 'openvibe.chat', contracts,
...keys().verifyOptions, issuer: config.networkUrl, audience: 'openvibe.chat', contracts,
});
});
- if (!r.ok || r.claims.actor_type !== 'node') return null;
+ if (!r.ok || r.claims.actor_type !== 'link') return null;
const m = NODE.exec(String(r.claims.sub || ''));
const m = NODE.exec(String(r.claims.sub || ''));
const owner = String(r.claims.on_behalf_of || '');
const owner = String(r.claims.on_behalf_of || '');
return m && /^usr_[0-9A-HJKMNP-TV-Z]{26}$/.test(owner) ? { id: m[1], owner } : null;
return m && /^usr_[0-9A-HJKMNP-TV-Z]{26}$/.test(owner) ? { id: m[1], owner } : null;
@@ -64,7 +64,7 @@ function titleOf(text) {
@@ -64,7 +64,7 @@ function titleOf(text) {
const newSlug = () => `v-${crypto.randomBytes(8).toString('hex').slice(0, 12)}`;
const newSlug = () => `v-${crypto.randomBytes(8).toString('hex').slice(0, 12)}`;
/** What Vibe thinks with, said to its person: "llama3.1:8b on your computer", "your openai key (gpt-4o-mini)". */
/** What Vibe thinks with, said to its person: "llama3.1:8b on your computer", "your openai key (gpt-4o-mini)". */
-const youOf = (cred) => (cred.provider === 'node'
+const youOf = (cred) => (cred.provider === 'link'
? `${cred.model || 'a model'} on your computer${cred.machine === 'any' ? 's' : ''}`
? `${cred.model || 'a model'} on your computer${cred.machine === 'any' ? 's' : ''}`
: `your ${cred.provider} key (${cred.model || 'its default model'})`);
: `your ${cred.provider} key (${cred.model || 'its default model'})`);
@@ -662,21 +662,21 @@ ${await aiKeySection(actor, req.query || {})}`,
@@ -662,21 +662,21 @@ ${await aiKeySection(actor, req.query || {})}`,
try { key = await credentials.get(actor.user.subject_id); } catch { unreachable = true; }
try { key = await credentials.get(actor.user.subject_id); } catch { unreachable = true; }
const opt = (v, label, cur) => `<option value="${v}"${v === cur ? ' selected' : ''}>${label}</option>`;
const opt = (v, label, cur) => `<option value="${v}"${v === cur ? ' selected' : ''}>${label}</option>`;
const spent = key ? `$${key.spent_usd_today.toFixed(2)} of ${key.budget_usd_per_day ? `$${key.budget_usd_per_day.toFixed(2)}` : 'no cap'} used today` : '';
const spent = key ? `$${key.spent_usd_today.toFixed(2)} of ${key.budget_usd_per_day ? `$${key.budget_usd_per_day.toFixed(2)}` : 'no cap'} used today` : '';
- // From openvibe.bot/machines, "Use these models in OpenVibe.Chat": a model on your own computer, filled in.
- const offered = q.own === 'node' && /^(dev_[0-9A-HJKMNP-TV-Z]{26}|any)$/.test(String(q.machine || '')) ? { machine: String(q.machine), model: /^[A-Za-z0-9._:/-]{1,120}$/.test(String(q.model || '')) ? String(q.model) : '' } : null;
- const provider = offered ? 'node' : key ? key.provider : 'openai';
+ // From openvibe.link/machines, "Use these models in OpenVibe.Chat": a model on your own computer, filled in.
+ const offered = q.own === 'link' && /^(dev_[0-9A-HJKMNP-TV-Z]{26}|any)$/.test(String(q.machine || '')) ? { machine: String(q.machine), model: /^[A-Za-z0-9._:/-]{1,120}$/.test(String(q.model || '')) ? String(q.model) : '' } : null;
+ const provider = offered ? 'link' : key ? key.provider : 'openai';
const machineValue = offered ? offered.machine : (key && key.machine) || '';
const machineValue = offered ? offered.machine : (key && key.machine) || '';
const modelValue = offered && offered.model ? offered.model : (key && key.model) || '';
const modelValue = offered && offered.model ? offered.model : (key && key.model) || '';
- const keyLine = key && key.provider === 'node' ? `${key.machine === 'any' ? 'on whichever of your computers is on, the fastest first' : `on your computer <code>${esc(key.machine || '')}</code>`} · nothing spent` : key ? `key ending ${esc(key.key_hint || '····')} · ${esc(spent)}` : '';
+ const keyLine = key && key.provider === 'link' ? `${key.machine === 'any' ? 'on whichever of your computers is on, the fastest first' : `on your computer <code>${esc(key.machine || '')}</code>`} · nothing spent` : key ? `key ending ${esc(key.key_hint || '····')} · ${esc(spent)}` : '';
const vibeOwn = key ? (await require('../rooms/vibe').engineOf(actor.user)).own : false;
const vibeOwn = key ? (await require('../rooms/vibe').engineOf(actor.user)).own : false;
const vibeBox = require('../rooms/vibe').enabled() ? `<label class="oc-check"><input type="checkbox" name="vibe" value="1"${vibeOwn || offered ? ' checked' : ''}> Vibe thinks with it too (in all your chats with Vibe; its answers then have no daily limit)</label>` : '';
const vibeBox = require('../rooms/vibe').enabled() ? `<label class="oc-check"><input type="checkbox" name="vibe" value="1"${vibeOwn || offered ? ' checked' : ''}> Vibe thinks with it too (in all your chats with Vibe; its answers then have no daily limit)</label>` : '';
const open = offered || q.saved || q.error ? ' open' : '';
const open = offered || q.saved || q.error ? ' open' : '';
return `<details class="oc-card oc-fold oc-aikey" id="oc-ai-key"${open}><summary><span>Your AI key${key ? ` <span class="oc-muted">· ${esc(credentials.label(key))}</span>` : ''}</span>${IC.chevron}</summary>
return `<details class="oc-card oc-fold oc-aikey" id="oc-ai-key"${open}><summary><span>Your AI key${key ? ` <span class="oc-muted">· ${esc(credentials.label(key))}</span>` : ''}</span>${IC.chevron}</summary>
-<p class="oc-muted">AI members you add to a room, your Vibe among them, can think with your own provider and model instead of OpenVibe's: OpenAI, Anthropic, any OpenAI-compatible endpoint (OpenRouter, Groq, Together, your own server), or <strong>a model on your own computer</strong> (Ollama, LM Studio or llama.cpp through the OpenVibe Link, free, with no port opened: turn on "Let my AI use this computer's models" for it on <a href="https://openvibe.bot/machines">openvibe.bot/machines</a>). A key is kept encrypted in OpenVibe.AI and never shown again; the room's messages go to your provider or your computer when one of those members answers, and a key's replies count against your daily budget, not the room's limit.</p>
+<p class="oc-muted">AI members you add to a room, your Vibe among them, can think with your own provider and model instead of OpenVibe's: OpenAI, Anthropic, any OpenAI-compatible endpoint (OpenRouter, Groq, Together, your own server), or <strong>a model on your own computer</strong> (Ollama, LM Studio or llama.cpp through the OpenVibe Link, free, with no port opened: turn on "Let my AI use this computer's models" for it on <a href="https://openvibe.link/machines">openvibe.link/machines</a>). A key is kept encrypted in OpenVibe.AI and never shown again; the room's messages go to your provider or your computer when one of those members answers, and a key's replies count against your daily budget, not the room's limit.</p>
${offered ? notice('ok', `Save to let your AI members think with ${offered.model || 'a model'} on ${offered.machine === 'any' ? 'whichever of your computers is on' : 'your own computer'}.`) : ''}
${offered ? notice('ok', `Save to let your AI members think with ${offered.model || 'a model'} on ${offered.machine === 'any' ? 'whichever of your computers is on' : 'your own computer'}.`) : ''}
${unreachable ? notice('error', 'OpenVibe.AI did not answer, so your key cannot be shown or changed right now.') : `${key ? `<p><strong>${esc(credentials.label(key))}</strong> <span class="oc-muted">· ${keyLine}${vibeOwn ? ' · Vibe thinks with it' : ''}</span></p>` : '<p class="oc-muted">No key yet.</p>'}
${unreachable ? notice('error', 'OpenVibe.AI did not answer, so your key cannot be shown or changed right now.') : `${key ? `<p><strong>${esc(credentials.label(key))}</strong> <span class="oc-muted">· ${keyLine}${vibeOwn ? ' · Vibe thinks with it' : ''}</span></p>` : '<p class="oc-muted">No key yet.</p>'}
<form class="oc-form oc-ai-form" method="post" action="/settings/ai-key">
<form class="oc-form oc-ai-form" method="post" action="/settings/ai-key">
-<label>Provider <select name="provider">${opt('openai', 'OpenAI or OpenAI-compatible', provider)}${opt('anthropic', 'Anthropic', provider)}${opt('node', 'A model on my own computer', provider)}</select></label>
+<label>Provider <select name="provider">${opt('openai', 'OpenAI or OpenAI-compatible', provider)}${opt('anthropic', 'Anthropic', provider)}${opt('link', 'A model on my own computer', provider)}</select></label>
<label class="oc-if-node">Computer <input name="machine" maxlength="40" pattern="dev_[0-9A-Z]{26}" placeholder="dev_… (a model on your own computer only)" value="${esc(machineValue === 'any' ? '' : machineValue)}" autocomplete="off" spellcheck="false"></label>
<label class="oc-if-node">Computer <input name="machine" maxlength="40" pattern="dev_[0-9A-Z]{26}" placeholder="dev_… (a model on your own computer only)" value="${esc(machineValue === 'any' ? '' : machineValue)}" autocomplete="off" spellcheck="false"></label>
<label class="oc-check oc-if-node"><input type="checkbox" name="machine_any" value="1"${machineValue === 'any' ? ' checked' : ''}> Any of my computers that is on with this model (the fastest first, the next when one is busy)</label>
<label class="oc-check oc-if-node"><input type="checkbox" name="machine_any" value="1"${machineValue === 'any' ? ' checked' : ''}> Any of my computers that is on with this model (the fastest first, the next when one is busy)</label>
<label class="oc-if-openai">Endpoint <input name="base_url" type="url" maxlength="300" inputmode="url" placeholder="https://openrouter.ai/api/v1 (leave empty for OpenAI itself)" value="${esc((key && key.base_url) || '')}"></label>
<label class="oc-if-openai">Endpoint <input name="base_url" type="url" maxlength="300" inputmode="url" placeholder="https://openrouter.ai/api/v1 (leave empty for OpenAI itself)" value="${esc((key && key.base_url) || '')}"></label>
@@ -1077,7 +1077,7 @@ ${composer}${work ? `</div>${queuePanel(room, can, me, work)}</div>` : ''}`,
@@ -1077,7 +1077,7 @@ ${composer}${work ? `</div>${queuePanel(room, can, me, work)}</div>` : ''}`,
if (can.manage && room.kind !== 'system' && aiMembers.enabled()) { try { myKey = await credentials.get(actor.user.subject_id); } catch { myKey = null; } }
if (can.manage && room.kind !== 'system' && aiMembers.enabled()) { try { myKey = await credentials.get(actor.user.subject_id); } catch { myKey = null; } }
const engineField = (a) => {
const engineField = (a) => {
const cur = a ? (a.engine || 'openvibe') : 'openvibe';
const cur = a ? (a.engine || 'openvibe') : 'openvibe';
- const ownLabel = a && cur === 'own' ? aiMembers.modelOf(a) : myKey && myKey.provider === 'node' ? `Your own computer: ${myKey.model || 'its model'}` : myKey ? `Your own key: ${credentials.label(myKey)}` : 'Your own key (add one in your chat settings first)';
+ const ownLabel = a && cur === 'own' ? aiMembers.modelOf(a) : myKey && myKey.provider === 'link' ? `Your own computer: ${myKey.model || 'its model'}` : myKey ? `Your own key: ${credentials.label(myKey)}` : 'Your own key (add one in your chat settings first)';
return `<label>Thinks with <select name="engine"><option value="openvibe"${cur === 'openvibe' ? ' selected' : ''}>OpenVibe's model</option><option value="own"${cur === 'own' ? ' selected' : ''}${cur === 'own' || myKey ? '' : ' disabled'}>${esc(ownLabel)}</option></select></label>
return `<label>Thinks with <select name="engine"><option value="openvibe"${cur === 'openvibe' ? ' selected' : ''}>OpenVibe's model</option><option value="own"${cur === 'own' ? ' selected' : ''}${cur === 'own' || myKey ? '' : ' disabled'}>${esc(ownLabel)}</option></select></label>
${myKey || cur === 'own' ? '' : '<p class="oc-muted">To give it a different model (yours, from OpenAI, Anthropic or an OpenAI-compatible provider, or a model on your own computer), <a href="/settings#oc-ai-key">add your AI key</a>.</p>'}`;
${myKey || cur === 'own' ? '' : '<p class="oc-muted">To give it a different model (yours, from OpenAI, Anthropic or an OpenAI-compatible provider, or a model on your own computer), <a href="/settings#oc-ai-key">add your AI key</a>.</p>'}`;
};
};
@@ -101,7 +101,7 @@ function rail({ chats, current, signedIn, allowance, query = '', here = null })
@@ -101,7 +101,7 @@ function rail({ chats, current, signedIn, allowance, query = '', here = null })
const allow = own ? `<p class="vb-allow" id="vb-allow" data-own="1">No daily limit: Vibe thinks with ${esc(own.you)}</p>`
const allow = own ? `<p class="vb-allow" id="vb-allow" data-own="1">No daily limit: Vibe thinks with ${esc(own.you)}</p>`
: left != null ? `<p class="vb-allow" id="vb-allow">${left} of ${allowance.per_day} answers left today</p>` : '';
: left != null ? `<p class="vb-allow" id="vb-allow">${left} of ${allowance.per_day} answers left today</p>` : '';
// Chat's other places, quietly at the foot (the network's navbar reaches them too).
// Chat's other places, quietly at the foot (the network's navbar reaches them too).
- const more = [['https://openvibe.bot/machines', 'Computers & robots'], ['/work', 'Work together'], ['/messages', 'Messages'], ['/rooms', 'Rooms and servers'], ['/', 'Global chat']];
+ const more = [['https://openvibe.link/machines', 'Computers & robots'], ['/work', 'Work together'], ['/messages', 'Messages'], ['/rooms', 'Rooms and servers'], ['/', 'Global chat']];
const html = kit.sidebar({
const html = kit.sidebar({
id: 'vb-rail', label: 'Your chats with Vibe',
id: 'vb-rail', label: 'Your chats with Vibe',
action: { href: '/vibe', label: 'New chat', icon: 'pencil' },
action: { href: '/vibe', label: 'New chat', icon: 'pencil' },
@@ -177,7 +177,7 @@ ${tasks.length ? `<ol class="oc-tasks">${tasks.map((t) => taskItem(t, room, can,
@@ -177,7 +177,7 @@ ${tasks.length ? `<ol class="oc-tasks">${tasks.map((t) => taskItem(t, room, can,
`<a class="oc-pill" href="${back.href}">${ICONS.chat}<span>${vibeChat ? 'Chat' : 'Room'}</span></a>`,
`<a class="oc-pill" href="${back.href}">${ICONS.chat}<span>${vibeChat ? 'Chat' : 'Room'}</span></a>`,
t.result_url ? `<a class="oc-pill oc-pill-strong" href="${esc(t.result_url)}" rel="noopener nofollow">${ICONS.open}<span>Open where it landed</span></a>` : '',
t.result_url ? `<a class="oc-pill oc-pill-strong" href="${esc(t.result_url)}" rel="noopener nofollow">${ICONS.open}<span>Open where it landed</span></a>` : '',
info && info.actor_url ? `<a class="oc-pill" href="${esc(info.actor_url)}" title="Every step the agent took, on OpenVibe.Actor">${ICONS.steps}<span>Every step</span></a>` : '',
info && info.actor_url ? `<a class="oc-pill" href="${esc(info.actor_url)}" title="Every step the agent took, on OpenVibe.Actor">${ICONS.steps}<span>Every step</span></a>` : '',
- t.kind === 'computer' || t.kind === 'code' ? `<a class="oc-pill" href="https://openvibe.bot/machines" title="Your computers: pair one, see its screen, set what agents may do on it">${ICONS.computer}<span>Your computers</span></a>` : '',
+ t.kind === 'computer' || t.kind === 'code' ? `<a class="oc-pill" href="https://openvibe.link/machines" title="Your computers: pair one, see its screen, set what agents may do on it">${ICONS.computer}<span>Your computers</span></a>` : '',
].filter(Boolean).join('');
].filter(Boolean).join('');
const checked = info ? info.lines.find((l) => l.startsWith('Checked by')) : null;
const checked = info ? info.lines.find((l) => l.startsWith('Checked by')) : null;
const sources = t.sources.length ? `<section class="oc-tp-card" aria-labelledby="oc-src-h"><h2 id="oc-src-h">Sources <span class="oc-muted">${t.sources.length}</span></h2>
const sources = t.sources.length ? `<section class="oc-tp-card" aria-labelledby="oc-src-h"><h2 id="oc-src-h">Sources <span class="oc-muted">${t.sources.length}</span></h2>
new file mode 100644
new file mode 100644
@@ -0,0 +1,10 @@
@@ -0,0 +1,10 @@
+'use strict';
+const assert = require('node:assert/strict');
+const { spawnSync } = require('node:child_process');
+const path = require('node:path');
+const root = path.resolve(__dirname, '..');
+const oldName = ['OpenVibe[ .-]No' + 'de', 'openvibe-' + 'node', 'no' + 'd_', 'node[.]openvibe[.]network', 'node:lnk_', 'openvibe[.](bot|my|network)/mach' + 'ines'].join('|');
+const result = spawnSync('git', ['grep', '-n', '-I', '-i', '-E', oldName, '--', '.', ':!migrations/**', ':!test/link-brand.test.js'], { cwd: root, encoding: 'utf8' });
+assert.ok(result.status === 0 || result.status === 1, result.stderr || 'git grep failed');
+assert.equal(result.status, 1, `Old Link brand or principal returned:\n${(result.stdout || '').split('\n').slice(0, 10).join('\n')}`);
+console.log(' ok old Link brand and principal IDs are absent');
@@ -162,9 +162,9 @@ t('a desktop party gates members, claims one Link machine, and issues member tic
@@ -162,9 +162,9 @@ t('a desktop party gates members, claims one Link machine, and issues member tic
const seen = await api('GET', `/parties/${slug}`, bob);
const seen = await api('GET', `/parties/${slug}`, bob);
assert.strictEqual(seen.body.party.lease.holder.subject, bob.subject_id);
assert.strictEqual(seen.body.party.lease.holder.subject, bob.subject_id);
assert.strictEqual((await api('GET', `/parties/${slug}/desktop/ticket`, ann)).status, 404);
assert.strictEqual((await api('GET', `/parties/${slug}/desktop/ticket`, ann)).status, 404);
- const nodeId = ids.newId('node');
+ const nodeId = ids.newId('link');
const now = Math.floor(Date.now() / 1000);
const now = Math.floor(Date.now() / 1000);
- const nodeToken = (owner) => serviceAuth.signServiceToken({ iss: h.ISS, sub: `node:${nodeId}`, actor_type: 'node', on_behalf_of: owner,
+ const nodeToken = (owner) => serviceAuth.signServiceToken({ iss: h.ISS, sub: `link:${nodeId}`, actor_type: 'link', on_behalf_of: owner,
aud: ['openvibe.chat'], cap: [], iat: now, exp: now + 300, jti: `party_${owner}` }, h.keys.privateKey);
aud: ['openvibe.chat'], cap: [], iat: now, exp: now + 300, jti: `party_${owner}` }, h.keys.privateKey);
const wrong = await h.http('POST', `/api/chat/parties/${slug}/desktop/claim`, { token: nodeToken(ann.subject_id), body: { port: 18180 } });
const wrong = await h.http('POST', `/api/chat/parties/${slug}/desktop/claim`, { token: nodeToken(ann.subject_id), body: { port: 18180 } });
assert.strictEqual(wrong.status, 404);
assert.strictEqual(wrong.status, 404);
@@ -40,7 +40,7 @@ t('reading needs a signed-in person; nothing stored yet is {} at revision 0', as
@@ -40,7 +40,7 @@ t('reading needs a signed-in person; nothing stored yet is {} at revision 0', as
assert.strictEqual(r.status, 401);
assert.strictEqual(r.status, 401);
r = await h.http('GET', '/api/chat/preferences', { token: ann.token });
r = await h.http('GET', '/api/chat/preferences', { token: ann.token });
assert.strictEqual(r.status, 200, r.text);
assert.strictEqual(r.status, 200, r.text);
- assert.deepStrictEqual(r.body, { namespace: NS, version: 2, revision: 0, preferences: {} });
+ assert.deepStrictEqual(r.body, { namespace: NS, version: 3, revision: 0, preferences: {} });
assert.strictEqual(r.headers.get('etag'), '"0"');
assert.strictEqual(r.headers.get('etag'), '"0"');
assert.ok(r.headers.get('cache-control').includes('no-store'));
assert.ok(r.headers.get('cache-control').includes('no-store'));
const tok = h.tokenRequests.find((x) => x.audience === 'openvibe.network');
const tok = h.tokenRequests.find((x) => x.audience === 'openvibe.network');
@@ -35,8 +35,8 @@ const fakeHttp = {
@@ -35,8 +35,8 @@ const fakeHttp = {
if (method === 'GET') { if (!store.has(subject)) throw problem(404, 'credential.not_found'); return store.get(subject); }
if (method === 'GET') { if (!store.has(subject)) throw problem(404, 'credential.not_found'); return store.get(subject); }
if (method === 'DELETE') { if (!store.delete(subject)) throw problem(404, 'credential.not_found'); return null; }
if (method === 'DELETE') { if (!store.delete(subject)) throw problem(404, 'credential.not_found'); return null; }
const prev = store.get(subject);
const prev = store.get(subject);
- if (json.provider === 'node') { // a model on the person's own computer: no key (contracts 0.220.0)
- const row = { owner: 'chat', subject, provider: 'node', machine: json.machine, base_url: null, models: json.models, budget_usd_per_day: null, spent_usd_today: 0 };
+ if (json.provider === 'link') { // a model on the person's own computer: no key (contracts 0.220.0)
+ const row = { owner: 'chat', subject, provider: 'link', machine: json.machine, base_url: null, models: json.models, budget_usd_per_day: null, spent_usd_today: 0 };
store.set(subject, row);
store.set(subject, row);
return row;
return row;
}
}
@@ -154,40 +154,40 @@ t('a new model relabels its members; removing the key sends them back to OpenVib
@@ -154,40 +154,40 @@ t('a new model relabels its members; removing the key sends them back to OpenVib
assert.strictEqual((await api('POST', '/rooms/model-night/ai-members', ann, { name: 'Echo', engine: 'own' })).body.code, 'rooms.ai_no_key', 'no key, no own engine');
assert.strictEqual((await api('POST', '/rooms/model-night/ai-members', ann, { name: 'Echo', engine: 'own' })).body.code, 'rooms.ai_no_key', 'no key, no own engine');
});
});
-t('a model on your own computer: the machine and no key; the settings form comes prefilled from openvibe.bot', async () => {
+t('a model on your own computer: the machine and no key; the settings form comes prefilled from openvibe.link', async () => {
const MACHINE = 'dev_01M5YGM39VC2RRGDQPRD6D4SHD';
const MACHINE = 'dev_01M5YGM39VC2RRGDQPRD6D4SHD';
const cy = h.addUser('cy', { subject: ids.newId('user') });
const cy = h.addUser('cy', { subject: ids.newId('user') });
await h.ctx.upsertUser(h.live.users.get(cy.id));
await h.ctx.upsertUser(h.live.users.get(cy.id));
- assert.strictEqual((await api('PUT', '/me/ai-key', cy, { provider: 'node', model: 'llama3.1:8b' })).status, 422, 'it names the computer');
- assert.strictEqual((await api('PUT', '/me/ai-key', cy, { provider: 'node', machine: 'dev_nope', model: 'llama3.1:8b' })).status, 422);
- const put = await api('PUT', '/me/ai-key', cy, { provider: 'node', machine: MACHINE, model: 'llama3.1:8b' });
+ assert.strictEqual((await api('PUT', '/me/ai-key', cy, { provider: 'link', model: 'llama3.1:8b' })).status, 422, 'it names the computer');
+ assert.strictEqual((await api('PUT', '/me/ai-key', cy, { provider: 'link', machine: 'dev_nope', model: 'llama3.1:8b' })).status, 422);
+ const put = await api('PUT', '/me/ai-key', cy, { provider: 'link', machine: MACHINE, model: 'llama3.1:8b' });
assert.strictEqual(put.status, 200, JSON.stringify(put.body));
assert.strictEqual(put.status, 200, JSON.stringify(put.body));
- assert.deepStrictEqual([put.body.key.provider, put.body.key.machine, put.body.key.model], ['node', MACHINE, 'llama3.1:8b']);
+ assert.deepStrictEqual([put.body.key.provider, put.body.key.machine, put.body.key.model], ['link', MACHINE, 'llama3.1:8b']);
const { label, memberLabel } = require('../server/ai/credentials');
const { label, memberLabel } = require('../server/ai/credentials');
assert.strictEqual(label(put.body.key), 'your own computer · llama3.1:8b');
assert.strictEqual(label(put.body.key), 'your own computer · llama3.1:8b');
assert.strictEqual(memberLabel(put.body.key, 'Cy'), 'llama3.1:8b on Cy\'s computer', 'what a room sees: whose computer, never "your own"');
assert.strictEqual(memberLabel(put.body.key, 'Cy'), 'llama3.1:8b on Cy\'s computer', 'what a room sees: whose computer, never "your own"');
assert.strictEqual(memberLabel({ provider: 'openai', model: 'gpt-4o-mini' }, 'Cy'), 'openai · gpt-4o-mini, Cy\'s key');
assert.strictEqual(memberLabel({ provider: 'openai', model: 'gpt-4o-mini' }, 'Cy'), 'openai · gpt-4o-mini, Cy\'s key');
- // From openvibe.bot/machines ("Use these models in OpenVibe.Chat"): the form is filled in, nothing is saved yet.
- const page = await web('GET', `/settings?own=node&machine=${MACHINE}&model=qwen2.5:7b`, cy);
+ // From openvibe.link/machines ("Use these models in OpenVibe.Chat"): the form is filled in, nothing is saved yet.
+ const page = await web('GET', `/settings?own=link&machine=${MACHINE}&model=qwen2.5:7b`, cy);
const sect = page.text.slice(page.text.indexOf('id="oc-ai-key"'));
const sect = page.text.slice(page.text.indexOf('id="oc-ai-key"'));
- assert.ok(sect.includes('<option value="node" selected>A model on my own computer</option>'), 'own computer chosen');
+ assert.ok(sect.includes('<option value="link" selected>A model on my own computer</option>'), 'own computer chosen');
assert.ok(sect.includes(`name="machine" maxlength="40"`) && sect.includes(`value="${MACHINE}"`));
assert.ok(sect.includes(`name="machine" maxlength="40"`) && sect.includes(`value="${MACHINE}"`));
assert.ok(sect.includes('class="oc-form oc-ai-form"') && sect.includes('<label class="oc-if-node">Computer') && sect.includes('<label class="oc-not-node">API key') && sect.includes('<label class="oc-if-openai">Endpoint'), 'each field says which providers take it (chat.css hides the rest)');
assert.ok(sect.includes('class="oc-form oc-ai-form"') && sect.includes('<label class="oc-if-node">Computer') && sect.includes('<label class="oc-not-node">API key') && sect.includes('<label class="oc-if-openai">Endpoint'), 'each field says which providers take it (chat.css hides the rest)');
assert.ok(sect.includes('value="qwen2.5:7b"'), 'the model from the link');
assert.ok(sect.includes('value="qwen2.5:7b"'), 'the model from the link');
assert.ok(sect.includes('Save to let your AI members think with qwen2.5:7b on your own computer.'));
assert.ok(sect.includes('Save to let your AI members think with qwen2.5:7b on your own computer.'));
assert.ok(sect.includes('your own computer · llama3.1:8b') && sect.includes('nothing spent'), 'what is saved now');
assert.ok(sect.includes('your own computer · llama3.1:8b') && sect.includes('nothing spent'), 'what is saved now');
// Any of their computers (contracts 0.221.0): the box, the label, what a room sees.
// Any of their computers (contracts 0.221.0): the box, the label, what a room sees.
- const any = await web('POST', '/settings/ai-key', cy, { provider: 'node', machine: '', machine_any: '1', model: 'llama3.1:8b' });
+ const any = await web('POST', '/settings/ai-key', cy, { provider: 'link', machine: '', machine_any: '1', model: 'llama3.1:8b' });
assert.strictEqual(any.status, 303);
assert.strictEqual(any.status, 303);
const anyKey = (await api('GET', '/me/ai-key', cy)).body.key;
const anyKey = (await api('GET', '/me/ai-key', cy)).body.key;
assert.deepStrictEqual([anyKey.machine, label(anyKey), memberLabel(anyKey, 'Cy')], ['any', 'any of your own computers · llama3.1:8b', 'llama3.1:8b on Cy\'s computers']);
assert.deepStrictEqual([anyKey.machine, label(anyKey), memberLabel(anyKey, 'Cy')], ['any', 'any of your own computers · llama3.1:8b', 'llama3.1:8b on Cy\'s computers']);
const anyPage = (await web('GET', '/settings', cy)).text;
const anyPage = (await web('GET', '/settings', cy)).text;
assert.ok(anyPage.includes('name="machine_any" value="1" checked') && anyPage.includes('on whichever of your computers is on, the fastest first'), 'the box, checked, and the saved line');
assert.ok(anyPage.includes('name="machine_any" value="1" checked') && anyPage.includes('on whichever of your computers is on, the fastest first'), 'the box, checked, and the saved line');
- assert.strictEqual((await api('PUT', '/me/ai-key', cy, { provider: 'node', machine: 'anyone', model: 'llama3.1:8b' })).status, 422);
- // From openvibe.bot when the person has several computers with models: any of them, the box checked.
- const anyLink = (await web('GET', '/settings?own=node&machine=any&model=qwen2.5:7b', cy)).text;
+ assert.strictEqual((await api('PUT', '/me/ai-key', cy, { provider: 'link', machine: 'anyone', model: 'llama3.1:8b' })).status, 422);
+ // From openvibe.link when the person has several computers with models: any of them, the box checked.
+ const anyLink = (await web('GET', '/settings?own=link&machine=any&model=qwen2.5:7b', cy)).text;
assert.ok(anyLink.includes('think with qwen2.5:7b on whichever of your computers is on') && anyLink.includes('name="machine_any" value="1" checked'), 'any, from the link');
assert.ok(anyLink.includes('think with qwen2.5:7b on whichever of your computers is on') && anyLink.includes('name="machine_any" value="1" checked'), 'any, from the link');
- const bad = await web('GET', '/settings?own=node&machine=../x&model=<b>', cy);
+ const bad = await web('GET', '/settings?own=link&machine=../x&model=<b>', cy);
assert.ok(!bad.text.includes('Save to let your AI members'), 'a bad link fills nothing');
assert.ok(!bad.text.includes('Save to let your AI members'), 'a bad link fills nothing');
assert.strictEqual((await api('DELETE', '/me/ai-key', cy)).status, 200);
assert.strictEqual((await api('DELETE', '/me/ai-key', cy)).status, 200);
});
});
@@ -206,7 +206,7 @@ t('Vibe thinks with your own key or computer when you choose it: every chat of y
@@ -206,7 +206,7 @@ t('Vibe thinks with your own key or computer when you choose it: every chat of y
const first = await vibeApi('POST', '', { message: 'hello there' });
const first = await vibeApi('POST', '', { message: 'hello there' });
assert.strictEqual(first.status, 201, first.text);
assert.strictEqual(first.status, 201, first.text);
assert.strictEqual((await memberOf(first.body.room.slug)).engine, 'openvibe', 'OpenVibe\'s model by default');
assert.strictEqual((await memberOf(first.body.room.slug)).engine, 'openvibe', 'OpenVibe\'s model by default');
- assert.strictEqual((await api('PUT', '/me/ai-key', di, { provider: 'node', machine: MACHINE, model: 'llama3.1:8b' })).status, 200);
+ assert.strictEqual((await api('PUT', '/me/ai-key', di, { provider: 'link', machine: MACHINE, model: 'llama3.1:8b' })).status, 200);
const on = await vibeApi('PUT', '/engine', { own: true });
const on = await vibeApi('PUT', '/engine', { own: true });
assert.deepStrictEqual(on.body, { own: true, label: 'llama3.1:8b on Di\'s computer' }, on.text);
assert.deepStrictEqual(on.body, { own: true, label: 'llama3.1:8b on Di\'s computer' }, on.text);
assert.deepStrictEqual(await memberOf(first.body.room.slug), { engine: 'own', engine_subject: h.live.users.get(di.id).subject_id, engine_label: 'llama3.1:8b on Di\'s computer' }, 'the chats they have');
assert.deepStrictEqual(await memberOf(first.body.room.slug), { engine: 'own', engine_subject: h.live.users.get(di.id).subject_id, engine_label: 'llama3.1:8b on Di\'s computer' }, 'the chats they have');
@@ -226,15 +226,15 @@ t('Vibe thinks with your own key or computer when you choose it: every chat of y
@@ -226,15 +226,15 @@ t('Vibe thinks with your own key or computer when you choose it: every chat of y
// The settings form says so and keeps it; unchecked, Vibe goes back to OpenVibe's model.
// The settings form says so and keeps it; unchecked, Vibe goes back to OpenVibe's model.
const page = await web('GET', '/settings', di);
const page = await web('GET', '/settings', di);
assert.ok(page.text.includes('name="vibe" value="1" checked') && page.text.includes('· Vibe thinks with it'), 'the box, checked');
assert.ok(page.text.includes('name="vibe" value="1" checked') && page.text.includes('· Vibe thinks with it'), 'the box, checked');
- const saved = await web('POST', '/settings/ai-key', di, { provider: 'node', machine: MACHINE, model: 'qwen2.5:7b' });
+ const saved = await web('POST', '/settings/ai-key', di, { provider: 'link', machine: MACHINE, model: 'qwen2.5:7b' });
assert.strictEqual(saved.status, 303);
assert.strictEqual(saved.status, 303);
assert.deepStrictEqual((await vibeApi('GET', '/engine')).body, { own: false, label: null }, 'unchecked: OpenVibe\'s model');
assert.deepStrictEqual((await vibeApi('GET', '/engine')).body, { own: false, label: null }, 'unchecked: OpenVibe\'s model');
assert.ok((await web('GET', '/vibe', di)).text.includes('answers left today'), 'the free answers left again');
assert.ok((await web('GET', '/vibe', di)).text.includes('answers left today'), 'the free answers left again');
assert.strictEqual((await memberOf(first.body.room.slug)).engine, 'openvibe');
assert.strictEqual((await memberOf(first.body.room.slug)).engine, 'openvibe');
- await web('POST', '/settings/ai-key', di, { provider: 'node', machine: MACHINE, model: 'qwen2.5:7b', vibe: '1' });
+ await web('POST', '/settings/ai-key', di, { provider: 'link', machine: MACHINE, model: 'qwen2.5:7b', vibe: '1' });
assert.strictEqual((await memberOf(next.body.room.slug)).engine_label, 'qwen2.5:7b on Di\'s computer', 'checked: their computer, the new model');
assert.strictEqual((await memberOf(next.body.room.slug)).engine_label, 'qwen2.5:7b on Di\'s computer', 'checked: their computer, the new model');
- // From openvibe.bot's link the box comes checked.
- assert.ok((await web('GET', `/settings?own=node&machine=${MACHINE}&model=phi3`, di)).text.includes('name="vibe" value="1" checked'));
+ // From openvibe.link's link the box comes checked.
+ assert.ok((await web('GET', `/settings?own=link&machine=${MACHINE}&model=phi3`, di)).text.includes('name="vibe" value="1" checked'));
// The key goes: so does Vibe's use of it.
// The key goes: so does Vibe's use of it.
assert.strictEqual((await api('DELETE', '/me/ai-key', di)).status, 200);
assert.strictEqual((await api('DELETE', '/me/ai-key', di)).status, 200);
assert.strictEqual((await memberOf(first.body.room.slug)).engine, 'openvibe');
assert.strictEqual((await memberOf(first.body.room.slug)).engine, 'openvibe');
@@ -13,7 +13,7 @@ const { plain, took } = require('../server/web/workspace-pages');
@@ -13,7 +13,7 @@ const { plain, took } = require('../server/web/workspace-pages');
const { ICONS } = require('../server/web/icons');
const { ICONS } = require('../server/web/icons');
assert.strictEqual(
assert.strictEqual(
- plain("I couldn't do it: **no computer is open to me**.\n\n**What I did**\n1. Asked for `list_my_computers`\n- see [the machines page](https://openvibe.bot/machines)"),
+ plain("I couldn't do it: **no computer is open to me**.\n\n**What I did**\n1. Asked for `list_my_computers`\n- see [the machines page](https://openvibe.link/machines)"),
"I couldn't do it: no computer is open to me. What I did Asked for list_my_computers see the machines page");
"I couldn't do it: no computer is open to me. What I did Asked for list_my_computers see the machines page");
assert.strictEqual(plain('```\ncode\n```\nafter'), 'after');
assert.strictEqual(plain('```\ncode\n```\nafter'), 'after');
assert.strictEqual(plain('snake_case and 2*3*4 stay'), 'snake_case and 2*3*4 stay');
assert.strictEqual(plain('snake_case and 2*3*4 stay'), 'snake_case and 2*3*4 stay');
@@ -103,7 +103,7 @@ t('an agent that hands its person a step on its desk: Vibe asks them in the chat
@@ -103,7 +103,7 @@ t('an agent that hands its person a step on its desk: Vibe asks them in the chat
const r = await vibeApi('POST', `/${slug}/ask`, ann, { message: 'Book the cheapest flight to Lisbon', mode: 'agent' });
const r = await vibeApi('POST', `/${slug}/ask`, ann, { message: 'Book the cheapest flight to Lisbon', mode: 'agent' });
assert.strictEqual(r.status, 201, r.text);
assert.strictEqual(r.status, 201, r.text);
await new Promise((res) => setTimeout(res, 150));
await new Promise((res) => setTimeout(res, 150));
- const desk = 'https://openvibe.bot/machines/dev_01M4W5JBFDG2TJ8C4FRDMCWBZ9/screen?desk=desk-0123456789';
+ const desk = 'https://openvibe.link/machines/dev_01M4W5JBFDG2TJ8C4FRDMCWBZ9/screen?desk=desk-0123456789';
actorTask = { id: 'tsk_h1', state: 'running', needs_you: { seq: 7, what: 'Sign in to the airline.', url: desk } };
actorTask = { id: 'tsk_h1', state: 'running', needs_you: { seq: 7, what: 'Sign in to the airline.', url: desk } };
await workspace.poll();
await workspace.poll();
await workspace.poll();
await workspace.poll();
@@ -128,7 +128,7 @@ t('an agent working on a desk: Vibe offers to watch it and take it over, once pe
@@ -128,7 +128,7 @@ t('an agent working on a desk: Vibe offers to watch it and take it over, once pe
const r = await vibeApi('POST', `/${slug}/ask`, bob, { message: 'Compare three laptops on the store site', mode: 'agent' });
const r = await vibeApi('POST', `/${slug}/ask`, bob, { message: 'Compare three laptops on the store site', mode: 'agent' });
assert.strictEqual(r.status, 201, r.text);
assert.strictEqual(r.status, 201, r.text);
await new Promise((res) => setTimeout(res, 150));
await new Promise((res) => setTimeout(res, 150));
- const desk = 'https://openvibe.bot/machines/dev_01M4W5JBFDG2TJ8C4FRDMCWBZ9/screen?desk=desk-9876543210';
+ const desk = 'https://openvibe.link/machines/dev_01M4W5JBFDG2TJ8C4FRDMCWBZ9/screen?desk=desk-9876543210';
actorTask = { id: 'tsk_d1', state: 'running' };
actorTask = { id: 'tsk_d1', state: 'running' };
await workspace.poll();
await workspace.poll();
const lines = async () => (await messagesOf(slug, bob)).filter((m) => m.ai_member_id && /Your agent is working on/.test(m.message)).map((m) => m.message);
const lines = async () => (await messagesOf(slug, bob)).filter((m) => m.ai_member_id && /Your agent is working on/.test(m.message)).map((m) => m.message);
@@ -152,7 +152,7 @@ t('the app page: signed out it invites a sign-in, signed in it lists the chats a
@@ -152,7 +152,7 @@ t('the app page: signed out it invites a sign-in, signed in it lists the chats a
// The rail leads to the rest of the person's agent: their tasks, their computers and robots, team workspaces.
// The rail leads to the rest of the person's agent: their tasks, their computers and robots, team workspaces.
assert.ok(/href="https:\/\/openvibe\.actor\/tasks"[^>]*>[\s\S]*?<span>Tasks<\/span>/.test(signed.text), 'Tasks');
assert.ok(/href="https:\/\/openvibe\.actor\/tasks"[^>]*>[\s\S]*?<span>Tasks<\/span>/.test(signed.text), 'Tasks');
assert.ok(/href="https:\/\/openvibe\.actor\/me\/agents"[^>]*>[\s\S]*?<span>Your agents<\/span>/.test(signed.text), 'Your agents: recurring work');
assert.ok(/href="https:\/\/openvibe\.actor\/me\/agents"[^>]*>[\s\S]*?<span>Your agents<\/span>/.test(signed.text), 'Your agents: recurring work');
- assert.ok(signed.text.includes('<a href="https://openvibe.bot/machines">Computers & robots</a>'), 'Computers & robots, at the foot');
+ assert.ok(signed.text.includes('<a href="https://openvibe.link/machines">Computers & robots</a>'), 'Computers & robots, at the foot');
assert.ok(signed.text.includes('<a href="/work">Work together</a>'), 'Work together, at the foot');
assert.ok(signed.text.includes('<a href="/work">Work together</a>'), 'Work together, at the foot');
const form = await web('POST', '/vibe', ann, { message: 'hello from a form' });
const form = await web('POST', '/vibe', ann, { message: 'hello from a form' });
assert.strictEqual(form.status, 303);
assert.strictEqual(form.status, 303);
@@ -200,7 +200,7 @@ t('answers render as escaped Markdown with http(s) links only', () => {
@@ -200,7 +200,7 @@ t('answers render as escaped Markdown with http(s) links only', () => {
assert.ok(!md.render('[x](https://a.com" onmouseover="alert(1))').includes('onmouseover="'));
assert.ok(!md.render('[x](https://a.com" onmouseover="alert(1))').includes('onmouseover="'));
assert.strictEqual(md.render('```\n<i>\n```'), '<pre><code><i></code></pre>');
assert.strictEqual(md.render('```\n<i>\n```'), '<pre><code><i></code></pre>');
// <https://…> (Markdown's autolink, which agents write): the address is the link; anything else in brackets stays text.
// <https://…> (Markdown's autolink, which agents write): the address is the link; anything else in brackets stays text.
- assert.strictEqual(md.render('Go to <https://openvibe.bot/machines>.'), '<p>Go to <a href="https://openvibe.bot/machines" rel="nofollow ugc noopener" target="_blank">https://openvibe.bot/machines</a>.</p>');
+ assert.strictEqual(md.render('Go to <https://openvibe.link/machines>.'), '<p>Go to <a href="https://openvibe.link/machines" rel="nofollow ugc noopener" target="_blank">https://openvibe.link/machines</a>.</p>');
assert.ok(!md.render('<javascript:alert(1)>').includes('href'));
assert.ok(!md.render('<javascript:alert(1)>').includes('href'));
// Tables and nested lists (answers are model-written Markdown, which uses both): escaped like everything else.
// Tables and nested lists (answers are model-written Markdown, which uses both): escaped like everything else.
assert.strictEqual(md.render('| Plan | Cost |\n|---|--:|\n| Rice | $2 |\n| <b>x</b> | 1 \\| 2 |'),
assert.strictEqual(md.render('| Plan | Cost |\n|---|--:|\n| Rice | $2 |\n| <b>x</b> | 1 \\| 2 |'),