openvibers/openvibe.chat Commits

Your AI key may be any of your computers that is on (machine any, contracts 0.221.0): the fastest first, the next when one is busy; "llama3.1:8b on Fran's computers" (#211)

OP OpenVibers committed 18 days ago · 988bedb032 · 4 files, +18 −8

Changes

ddiff --git a/server/ai/credentials.js b/server/ai/credentials.js
iindex 623d0ca..db344c0 100644
---- a/server/ai/credentials.js
++++ b/server/ai/credentials.js
@@@ -56,7 +56,8 @@ async function put(subject, { provider, base_url, api_key, model, budget_usd_per
if (provider === 'node') {
// A model on the person's own computer: the machine, no key, no budget (nothing is spent).
const id = String(machine || '').trim();
-- if (!MACHINE_RE.test(id)) fail(422, 'credentials.machine', 'Name your computer: its id (dev_…) from openvibe.bot/machines.');
++ // `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.');
try {
return view(await call('PUT', subject, { provider: 'node', machine: id, models: { chat: m } }));
} catch (err) {
@@@ -92,12 +93,12 @@ async function remove(subject) {
}
}
--const label = (c) => (c && c.provider === 'node' ? `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 === '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);
/** 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` : `${label(c)}, ${who}'s key`;
++ return c && c.provider === 'node' ? `${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 };
ddiff --git a/server/rooms/ai-members.js b/server/rooms/ai-members.js
iindex a9631ff..47b749b 100644
---- a/server/rooms/ai-members.js
++++ b/server/rooms/ai-members.js
@@@ -398,8 +398,8 @@ async function afterPost(room, message, user, deps = {}) {
}, opts);
text = out && typeof out.text === 'string' ? clean(out.text, 2000) : '';
}
-- // A model on someone's own computer (credential provider node): its label reads "<model> on Fran's computer".
-- const onComputer = own && /'s computer$/.test(String(member.engine_label || ''));
++ // A model on someone's own computer (credential provider node): its label reads "<model> on Fran's computer(s)".
++ const onComputer = own && /'s computers?$/.test(String(member.engine_label || ''));
if (!text) {
if (!own) return await say('I could not answer just now. Try again in a minute.');
if (onComputer) return await say(vibe
ddiff --git a/server/web/pages.js b/server/web/pages.js
iindex 6eaf56d..1a8c0e4 100644
---- a/server/web/pages.js
++++ b/server/web/pages.js
@@@ -506,7 +506,7 @@ ${await aiKeySection(actor, req.query || {})}`,
const provider = offered ? 'node' : 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' ? `on your computer <code>${esc(key.machine || '')}</code> · nothing spent` : key ? `key ending ${esc(key.key_hint || '····')} · ${esc(spent)}` : '';
++ 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 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>` : '';
return `<h2 id="oc-ai-key">Your AI key</h2>
@@@ -515,7 +515,8 @@ ${offered ? notice('ok', `Save to let your AI members think with ${offered.model
${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 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)}" 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-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>Model <input name="model" required maxlength="120" placeholder="gpt-4o-mini, or llama3.1:8b on your computer" value="${esc(modelValue)}" autocomplete="off" spellcheck="false"></label>
<label class="oc-not-node">API key <input name="api_key" type="password" maxlength="400" autocomplete="off" spellcheck="false" placeholder="${key ? 'Leave empty to keep the current key' : 'sk-… (none for your own computer)'}"></label>
@@@ -529,7 +530,7 @@ ${key ? `<form class="oc-inline" method="post" action="/settings/ai-key/delete">
const actor = await needUser(req, res); if (!actor) return;
const b = req.body || {};
try {
-- const key = await credentials.put(actor.user.subject_id, { provider: b.provider, base_url: b.provider === 'openai' ? b.base_url : undefined, api_key: b.api_key, model: b.model, budget_usd_per_day: b.budget_usd_per_day, machine: b.machine });
++ const key = await credentials.put(actor.user.subject_id, { provider: b.provider, base_url: b.provider === 'openai' ? b.base_url : undefined, api_key: b.api_key, model: b.model, budget_usd_per_day: b.budget_usd_per_day, machine: b.machine_any === '1' ? 'any' : b.machine });
await require('../rooms/ai-members').relabelKey(actor.user.subject_id, key, actor.user);
const vibe = require('../rooms/vibe');
if (vibe.enabled()) await vibe.setEngine(actor.user, b.vibe === '1');
ddiff --git a/test/room-ai-models.test.js b/test/room-ai-models.test.js
iindex b84f471..b1f17d8 100644
---- a/test/room-ai-models.test.js
++++ b/test/room-ai-models.test.js
@@@ -176,6 +176,14 @@ t('a model on your own computer: the machine and no key; the settings form comes
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' });
++ 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);
const bad = await web('GET', '/settings?own=node&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);