openvibers/openvibe.chat Commits
Vibe's page says what Vibe thinks with when it is your own model ("Vibe thinks with llama3.1:8b on your computer (change)"), and the rail says there is no daily limit then (#212)
Changes
@@@ -296,7 +296,7 @@fetch('/api/chat/vibe?limit=100', { credentials: 'same-origin' }).then(function (r) { return r.ok ? r.json() : null; }).then(function (d) {if (!d) return;var allow = document.getElementById('vb-allow');-- if (allow && d.allowance) allow.textContent = Math.max(0, d.allowance.per_day - d.allowance.used_today) + ' of ' + d.allowance.per_day + ' answers left today';++ if (allow && d.allowance && !allow.hasAttribute('data-own')) allow.textContent = Math.max(0, d.allowance.per_day - d.allowance.used_today) + ' of ' + d.allowance.per_day + ' answers left today';var list = document.querySelector('.vb-rail-list'); if (!list || !d.chats) return;// Rebuilt from data with text nodes (the server's grouping, kept simple here: Today and Earlier).var start = new Date(); start.setUTCHours(0, 0, 0, 0);@@@ -63,6 +63,11 @@ 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'++ ? `${cred.model || 'a model'} on your computer${cred.machine === 'any' ? 's' : ''}`++ : `your ${cred.provider} key (${cred.model || 'its default model'})`);++/** The person's choice of model for Vibe: their own credential when they chose it and it is still stored. */async function engineOf(user) {if (!user || !user.subject_id || !(await db.get('SELECT 1 AS own FROM vibe_own_engine WHERE user_id = ?', [user.id]))) return { own: false, label: null };@@@ -253,4 +258,4 @@ async function countReply(user) {ON CONFLICT (user_id, day) DO UPDATE SET replies = vibe_usage.replies + 1`, [user.id, today()]);}--module.exports = { VIBE, MAX_CHATS, TITLE_MAX, SEARCH_MAX, DEFAULT_TITLE, MODES, VibeError, enabled, isVibeRoom, titleOf, start, startWork, work, createRoom, erase: (roomId) => db.tx(() => erase(roomId)), list, search, ownChat, rename, remove, retry, lastExchange, repliesToday, countReply, engineOf: async (user) => { const e = await engineOf(user); return { own: e.own, label: e.label }; }, setEngine };++module.exports = { VIBE, MAX_CHATS, TITLE_MAX, SEARCH_MAX, DEFAULT_TITLE, MODES, VibeError, enabled, isVibeRoom, titleOf, start, startWork, work, createRoom, erase: (roomId) => db.tx(() => erase(roomId)), list, search, ownChat, rename, remove, retry, lastExchange, repliesToday, countReply, engineOf: async (user) => { const e = await engineOf(user); return { own: e.own, label: e.label, ...(e.own ? { you: youOf(e.cred) } : {}) }; }, setEngine };@@@ -93,6 +93,7 @@ function rail({ chats, current, signedIn, allowance, query = '', here = null })? groups.map((g) => `<p class="vb-group">${esc(g.label)}</p><ul class="vb-chats">${g.chats.map((c) => `<li><a class="vb-chat" href="/vibe/${esc(c.slug)}"${c.slug === current ? ' aria-current="page"' : ''}>${esc(c.name)}</a></li>`).join('')}</ul>`).join(''): '<p class="vb-rail-note">Your chats with Vibe show up here.</p>');const left = allowance ? Math.max(0, allowance.per_day - allowance.used_today) : null;++ const own = allowance && allowance.engine && allowance.engine.own ? allowance.engine : null; // Vibe on their own modelreturn `<aside class="vb-rail" id="vb-rail" aria-label="Your chats with Vibe"><div class="vb-rail-top"><a class="vb-new" href="/vibe">${ICON.new}<span>New chat</span></a>@@@ -110,7 +111,7 @@ ${signedIn ? `<a href="/vibe/memory" title="What Vibe remembers about you across<a href="/rooms">${ICON.rooms}<span>Rooms and servers</span></a><a href="/">${ICON.globe}<span>Global chat</span></a></nav>--${left != null ? `<p class="vb-allow" id="vb-allow">${left} of ${allowance.per_day} answers left today</p>` : ''}++${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>` : ''}</aside>`;}@@@ -140,7 +141,7 @@ const MODES = [// The text files the attach button takes (read in the browser, vibe.js readText).const TEXT_FILES = 'text/plain,text/markdown,text/csv,application/json,.txt,.md,.markdown,.csv,.tsv,.json,.log,.xml,.yaml,.yml,.toml,.ini,.html,.css,.js,.mjs,.ts,.py,.go,.rs,.java,.c,.h,.cpp,.rb,.php,.sh,.sql';--function composer({ slug, signedIn, enabled, prefill = '' }) {++function composer({ slug, signedIn, enabled, prefill = '', engine = null }) {const action = !signedIn ? `/auth/login` : slug ? `/vibe/${esc(slug)}` : '/vibe';// A picture (public/web/vibe.js): it goes to OpenVibe.Pics as the person, and Vibe sees it with the question. A text// file (notes, a CSV, code) is read in the browser and goes into the message itself.@@@ -158,11 +159,16 @@ ${attach}<button class="vb-send" type="submit" aria-label="Send"${enabled ? '' : ' disabled'}>${ICON.send}</button><fieldset class="vb-modes" aria-label="What Vibe does with it">${MODES.map(([value, label, hint, placeholder], i) => `<label title="${esc(hint)}"><input type="radio" name="mode" value="${value}" data-placeholder="${esc(placeholder)}"${i ? '' : ' checked'}><span>${label}</span></label>`).join('')}</fieldset></form>--<p class="vb-fine">${enabled ? 'Vibe can get things wrong. Check what matters.' : 'Vibe is resting right now. Try again later.'}</p>`;++<p class="vb-fine">${!enabled ? 'Vibe is resting right now. Try again later.' : engine && engine.own ? `Vibe thinks with ${esc(engine.you)} (<a href="/settings#oc-ai-key">change</a>). It can get things wrong: check what matters.` : 'Vibe can get things wrong. Check what matters.'}</p>`;}function mountVibePages(router, { page, viewerOf, needUser, sameSite, formLimit, notice }) {-- const allowanceOf = async (user) => ({ per_day: require('../config').ai.vibeRepliesPerDay, used_today: await vibe.repliesToday(user) });++ const allowanceOf = async (user) => {++ // Which model Vibe thinks with: the person's own key or computer when they chose it (no daily limit then).++ let engine = null;++ try { engine = await vibe.engineOf(user); } catch { engine = null; }++ return { per_day: require('../config').ai.vibeRepliesPerDay, used_today: await vibe.repliesToday(user), engine };++ };/*** "Ask Vibe" from any OpenVibe site (the shared navbar): ?ask=<the text they selected> and ?from=<the page's https@@@ -220,7 +226,7 @@ ${head}${error ? notice('error', error) : ''}${room ? thread : hello}</div>--<div class="vb-dock">${composer({ slug: room && room.slug, signedIn: !!user, enabled, prefill: room ? '' : prefillOf(req.query || {}) })}</div>++<div class="vb-dock">${composer({ slug: room && room.slug, signedIn: !!user, enabled, prefill: room ? '' : prefillOf(req.query || {}), engine: allowance && allowance.engine })}</div></section></div>`;await page(req, res, status, {@@@ -216,12 +216,17 @@ t('Vibe thinks with your own key or computer when you choose it: every chat of yassert.ok(mine.length >= 1, `Vibe answered with their computer (${runs.length - before} runs)`);const answer = await h.db.get('SELECT m.how FROM room_messages m JOIN rooms r ON r.id = m.room_id WHERE r.slug = ? AND m.user_id = 0 ORDER BY m.id DESC LIMIT 1', [next.body.room.slug]);assert.ok(answer && String(answer.how).includes('Answered with llama3.1:8b on Di\'s computer'), `"How Vibe did this" names it (${answer && answer.how})`);++ // Vibe's page says so, with a way to change it, and there is no daily limit then.++ const vibePage = (await web('GET', `/vibe/${next.body.room.slug}`, di)).text;++ assert.ok(vibePage.includes('Vibe thinks with llama3.1:8b on your computer (<a href="/settings#oc-ai-key">change</a>)'), 'the composer says what Vibe thinks with');++ assert.ok(vibePage.includes('data-own="1">No daily limit: Vibe thinks with llama3.1:8b on your computer'), 'the rail, instead of the free answers left');// 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' });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' });assert.strictEqual((await memberOf(next.body.room.slug)).engine_label, 'qwen2.5:7b on Di\'s computer', 'checked: their computer, the new model');
@@ -296,7 +296,7 @@
@@ -296,7 +296,7 @@
fetch('/api/chat/vibe?limit=100', { credentials: 'same-origin' }).then(function (r) { return r.ok ? r.json() : null; }).then(function (d) {
fetch('/api/chat/vibe?limit=100', { credentials: 'same-origin' }).then(function (r) { return r.ok ? r.json() : null; }).then(function (d) {
if (!d) return;
if (!d) return;
var allow = document.getElementById('vb-allow');
var allow = document.getElementById('vb-allow');
- if (allow && d.allowance) allow.textContent = Math.max(0, d.allowance.per_day - d.allowance.used_today) + ' of ' + d.allowance.per_day + ' answers left today';
+ if (allow && d.allowance && !allow.hasAttribute('data-own')) allow.textContent = Math.max(0, d.allowance.per_day - d.allowance.used_today) + ' of ' + d.allowance.per_day + ' answers left today';
var list = document.querySelector('.vb-rail-list'); if (!list || !d.chats) return;
var list = document.querySelector('.vb-rail-list'); if (!list || !d.chats) return;
// Rebuilt from data with text nodes (the server's grouping, kept simple here: Today and Earlier).
// Rebuilt from data with text nodes (the server's grouping, kept simple here: Today and Earlier).
var start = new Date(); start.setUTCHours(0, 0, 0, 0);
var start = new Date(); start.setUTCHours(0, 0, 0, 0);
@@ -63,6 +63,11 @@ function titleOf(text) {
@@ -63,6 +63,11 @@ 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)". */
+const youOf = (cred) => (cred.provider === 'node'
+ ? `${cred.model || 'a model'} on your computer${cred.machine === 'any' ? 's' : ''}`
+ : `your ${cred.provider} key (${cred.model || 'its default model'})`);
+
/** The person's choice of model for Vibe: their own credential when they chose it and it is still stored. */
/** The person's choice of model for Vibe: their own credential when they chose it and it is still stored. */
async function engineOf(user) {
async function engineOf(user) {
if (!user || !user.subject_id || !(await db.get('SELECT 1 AS own FROM vibe_own_engine WHERE user_id = ?', [user.id]))) return { own: false, label: null };
if (!user || !user.subject_id || !(await db.get('SELECT 1 AS own FROM vibe_own_engine WHERE user_id = ?', [user.id]))) return { own: false, label: null };
@@ -253,4 +258,4 @@ async function countReply(user) {
@@ -253,4 +258,4 @@ async function countReply(user) {
ON CONFLICT (user_id, day) DO UPDATE SET replies = vibe_usage.replies + 1`, [user.id, today()]);
ON CONFLICT (user_id, day) DO UPDATE SET replies = vibe_usage.replies + 1`, [user.id, today()]);
}
}
-module.exports = { VIBE, MAX_CHATS, TITLE_MAX, SEARCH_MAX, DEFAULT_TITLE, MODES, VibeError, enabled, isVibeRoom, titleOf, start, startWork, work, createRoom, erase: (roomId) => db.tx(() => erase(roomId)), list, search, ownChat, rename, remove, retry, lastExchange, repliesToday, countReply, engineOf: async (user) => { const e = await engineOf(user); return { own: e.own, label: e.label }; }, setEngine };
+module.exports = { VIBE, MAX_CHATS, TITLE_MAX, SEARCH_MAX, DEFAULT_TITLE, MODES, VibeError, enabled, isVibeRoom, titleOf, start, startWork, work, createRoom, erase: (roomId) => db.tx(() => erase(roomId)), list, search, ownChat, rename, remove, retry, lastExchange, repliesToday, countReply, engineOf: async (user) => { const e = await engineOf(user); return { own: e.own, label: e.label, ...(e.own ? { you: youOf(e.cred) } : {}) }; }, setEngine };
@@ -93,6 +93,7 @@ function rail({ chats, current, signedIn, allowance, query = '', here = null })
@@ -93,6 +93,7 @@ function rail({ chats, current, signedIn, allowance, query = '', here = null })
? groups.map((g) => `<p class="vb-group">${esc(g.label)}</p><ul class="vb-chats">${g.chats.map((c) => `<li><a class="vb-chat" href="/vibe/${esc(c.slug)}"${c.slug === current ? ' aria-current="page"' : ''}>${esc(c.name)}</a></li>`).join('')}</ul>`).join('')
? groups.map((g) => `<p class="vb-group">${esc(g.label)}</p><ul class="vb-chats">${g.chats.map((c) => `<li><a class="vb-chat" href="/vibe/${esc(c.slug)}"${c.slug === current ? ' aria-current="page"' : ''}>${esc(c.name)}</a></li>`).join('')}</ul>`).join('')
: '<p class="vb-rail-note">Your chats with Vibe show up here.</p>');
: '<p class="vb-rail-note">Your chats with Vibe show up here.</p>');
const left = allowance ? Math.max(0, allowance.per_day - allowance.used_today) : null;
const left = allowance ? Math.max(0, allowance.per_day - allowance.used_today) : null;
+ const own = allowance && allowance.engine && allowance.engine.own ? allowance.engine : null; // Vibe on their own model
return `<aside class="vb-rail" id="vb-rail" aria-label="Your chats with Vibe">
return `<aside class="vb-rail" id="vb-rail" aria-label="Your chats with Vibe">
<div class="vb-rail-top">
<div class="vb-rail-top">
<a class="vb-new" href="/vibe">${ICON.new}<span>New chat</span></a>
<a class="vb-new" href="/vibe">${ICON.new}<span>New chat</span></a>
@@ -110,7 +111,7 @@ ${signedIn ? `<a href="/vibe/memory" title="What Vibe remembers about you across
@@ -110,7 +111,7 @@ ${signedIn ? `<a href="/vibe/memory" title="What Vibe remembers about you across
<a href="/rooms">${ICON.rooms}<span>Rooms and servers</span></a>
<a href="/rooms">${ICON.rooms}<span>Rooms and servers</span></a>
<a href="/">${ICON.globe}<span>Global chat</span></a>
<a href="/">${ICON.globe}<span>Global chat</span></a>
</nav>
</nav>
-${left != null ? `<p class="vb-allow" id="vb-allow">${left} of ${allowance.per_day} answers left today</p>` : ''}
+${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>` : ''}
</aside>`;
</aside>`;
}
}
@@ -140,7 +141,7 @@ const MODES = [
@@ -140,7 +141,7 @@ const MODES = [
// The text files the attach button takes (read in the browser, vibe.js readText).
// The text files the attach button takes (read in the browser, vibe.js readText).
const TEXT_FILES = 'text/plain,text/markdown,text/csv,application/json,.txt,.md,.markdown,.csv,.tsv,.json,.log,.xml,.yaml,.yml,.toml,.ini,.html,.css,.js,.mjs,.ts,.py,.go,.rs,.java,.c,.h,.cpp,.rb,.php,.sh,.sql';
const TEXT_FILES = 'text/plain,text/markdown,text/csv,application/json,.txt,.md,.markdown,.csv,.tsv,.json,.log,.xml,.yaml,.yml,.toml,.ini,.html,.css,.js,.mjs,.ts,.py,.go,.rs,.java,.c,.h,.cpp,.rb,.php,.sh,.sql';
-function composer({ slug, signedIn, enabled, prefill = '' }) {
+function composer({ slug, signedIn, enabled, prefill = '', engine = null }) {
const action = !signedIn ? `/auth/login` : slug ? `/vibe/${esc(slug)}` : '/vibe';
const action = !signedIn ? `/auth/login` : slug ? `/vibe/${esc(slug)}` : '/vibe';
// A picture (public/web/vibe.js): it goes to OpenVibe.Pics as the person, and Vibe sees it with the question. A text
// A picture (public/web/vibe.js): it goes to OpenVibe.Pics as the person, and Vibe sees it with the question. A text
// file (notes, a CSV, code) is read in the browser and goes into the message itself.
// file (notes, a CSV, code) is read in the browser and goes into the message itself.
@@ -158,11 +159,16 @@ ${attach}
@@ -158,11 +159,16 @@ ${attach}
<button class="vb-send" type="submit" aria-label="Send"${enabled ? '' : ' disabled'}>${ICON.send}</button>
<button class="vb-send" type="submit" aria-label="Send"${enabled ? '' : ' disabled'}>${ICON.send}</button>
<fieldset class="vb-modes" aria-label="What Vibe does with it">${MODES.map(([value, label, hint, placeholder], i) => `<label title="${esc(hint)}"><input type="radio" name="mode" value="${value}" data-placeholder="${esc(placeholder)}"${i ? '' : ' checked'}><span>${label}</span></label>`).join('')}</fieldset>
<fieldset class="vb-modes" aria-label="What Vibe does with it">${MODES.map(([value, label, hint, placeholder], i) => `<label title="${esc(hint)}"><input type="radio" name="mode" value="${value}" data-placeholder="${esc(placeholder)}"${i ? '' : ' checked'}><span>${label}</span></label>`).join('')}</fieldset>
</form>
</form>
-<p class="vb-fine">${enabled ? 'Vibe can get things wrong. Check what matters.' : 'Vibe is resting right now. Try again later.'}</p>`;
+<p class="vb-fine">${!enabled ? 'Vibe is resting right now. Try again later.' : engine && engine.own ? `Vibe thinks with ${esc(engine.you)} (<a href="/settings#oc-ai-key">change</a>). It can get things wrong: check what matters.` : 'Vibe can get things wrong. Check what matters.'}</p>`;
}
}
function mountVibePages(router, { page, viewerOf, needUser, sameSite, formLimit, notice }) {
function mountVibePages(router, { page, viewerOf, needUser, sameSite, formLimit, notice }) {
- const allowanceOf = async (user) => ({ per_day: require('../config').ai.vibeRepliesPerDay, used_today: await vibe.repliesToday(user) });
+ const allowanceOf = async (user) => {
+ // Which model Vibe thinks with: the person's own key or computer when they chose it (no daily limit then).
+ let engine = null;
+ try { engine = await vibe.engineOf(user); } catch { engine = null; }
+ return { per_day: require('../config').ai.vibeRepliesPerDay, used_today: await vibe.repliesToday(user), engine };
+ };
/**
/**
* "Ask Vibe" from any OpenVibe site (the shared navbar): ?ask=<the text they selected> and ?from=<the page's https
* "Ask Vibe" from any OpenVibe site (the shared navbar): ?ask=<the text they selected> and ?from=<the page's https
@@ -220,7 +226,7 @@ ${head}
@@ -220,7 +226,7 @@ ${head}
${error ? notice('error', error) : ''}
${error ? notice('error', error) : ''}
${room ? thread : hello}
${room ? thread : hello}
</div>
</div>
-<div class="vb-dock">${composer({ slug: room && room.slug, signedIn: !!user, enabled, prefill: room ? '' : prefillOf(req.query || {}) })}</div>
+<div class="vb-dock">${composer({ slug: room && room.slug, signedIn: !!user, enabled, prefill: room ? '' : prefillOf(req.query || {}), engine: allowance && allowance.engine })}</div>
</section>
</section>
</div>`;
</div>`;
await page(req, res, status, {
await page(req, res, status, {
@@ -216,12 +216,17 @@ t('Vibe thinks with your own key or computer when you choose it: every chat of y
@@ -216,12 +216,17 @@ t('Vibe thinks with your own key or computer when you choose it: every chat of y
assert.ok(mine.length >= 1, `Vibe answered with their computer (${runs.length - before} runs)`);
assert.ok(mine.length >= 1, `Vibe answered with their computer (${runs.length - before} runs)`);
const answer = await h.db.get('SELECT m.how FROM room_messages m JOIN rooms r ON r.id = m.room_id WHERE r.slug = ? AND m.user_id = 0 ORDER BY m.id DESC LIMIT 1', [next.body.room.slug]);
const answer = await h.db.get('SELECT m.how FROM room_messages m JOIN rooms r ON r.id = m.room_id WHERE r.slug = ? AND m.user_id = 0 ORDER BY m.id DESC LIMIT 1', [next.body.room.slug]);
assert.ok(answer && String(answer.how).includes('Answered with llama3.1:8b on Di\'s computer'), `"How Vibe did this" names it (${answer && answer.how})`);
assert.ok(answer && String(answer.how).includes('Answered with llama3.1:8b on Di\'s computer'), `"How Vibe did this" names it (${answer && answer.how})`);
+ // Vibe's page says so, with a way to change it, and there is no daily limit then.
+ const vibePage = (await web('GET', `/vibe/${next.body.room.slug}`, di)).text;
+ assert.ok(vibePage.includes('Vibe thinks with llama3.1:8b on your computer (<a href="/settings#oc-ai-key">change</a>)'), 'the composer says what Vibe thinks with');
+ assert.ok(vibePage.includes('data-own="1">No daily limit: Vibe thinks with llama3.1:8b on your computer'), 'the rail, instead of the free answers left');
// 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: 'node', 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.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: 'node', 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');