openvibers/openvibe.chat Commits
"Your AI key" shows only the fields the chosen provider takes (a computer: no endpoint, key or budget), and the computer field holds a whole dev_ id at 320 px (#210)
Changes
@@@ -74,6 +74,12 @@ button[disabled] { opacity: .6; cursor: default; }.oc-form h2 { font-size: 1rem; margin: 18px 0 4px; }.oc-form select { margin-left: 8px; max-width: calc(100% - 8px); } /* its margin counts too: a full-width select stuck 8 px out on phones */.oc-form input[type=url], .oc-form textarea { display: block; width: 100%; margin-top: 4px; } /* an address or a paragraph needs the width to be read */++/* "Your AI key": only the fields the chosen provider takes (a model on your own computer has no endpoint, key or budget;++ only OpenAI-compatible providers take an endpoint). Without :has() every field shows, as before. */++.oc-ai-form:has(select[name="provider"] option[value="node"]:checked) .oc-not-node,++.oc-ai-form:not(:has(select[name="provider"] option[value="node"]:checked)) .oc-if-node,++.oc-ai-form:not(:has(select[name="provider"] option[value="openai"]:checked)) .oc-if-openai { display: none; }++.oc-ai-form input[name="machine"] { display: block; width: 100%; max-width: 36ch; box-sizing: border-box; margin-top: 4px; font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 0.92em; } /* a whole dev_ id, even at 320 px */@media (max-width: 560px) {.oc-main { padding: 12px 12px 40px; }.oc-feed { max-height: 60vh; }@@@ -513,13 +513,13 @@ ${await aiKeySection(actor, req.query || {})}`,<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 Node, 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>${offered ? notice('ok', `Save to let your AI members think with ${offered.model || 'a model'} 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" 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>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>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-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-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>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>--<label>Daily budget, US dollars <input name="budget_usd_per_day" type="number" min="0" max="1000" step="0.5" value="${key && key.budget_usd_per_day != null ? key.budget_usd_per_day : 1}"> <span class="oc-muted">0 means no cap</span></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>++<label class="oc-not-node">Daily budget, US dollars <input name="budget_usd_per_day" type="number" min="0" max="1000" step="0.5" value="${key && key.budget_usd_per_day != null ? key.budget_usd_per_day : 1}"> <span class="oc-muted">0 means no cap</span></label>${vibeBox}<p><button type="submit">${key ? 'Save' : 'Add key'}</button></p></form>@@@ -529,7 +529,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.base_url, 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 });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');@@@ -172,6 +172,7 @@ t('a model on your own computer: the machine and no key; the settings form comesconst 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(`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');
@@ -74,6 +74,12 @@ button[disabled] { opacity: .6; cursor: default; }
@@ -74,6 +74,12 @@ button[disabled] { opacity: .6; cursor: default; }
.oc-form h2 { font-size: 1rem; margin: 18px 0 4px; }
.oc-form h2 { font-size: 1rem; margin: 18px 0 4px; }
.oc-form select { margin-left: 8px; max-width: calc(100% - 8px); } /* its margin counts too: a full-width select stuck 8 px out on phones */
.oc-form select { margin-left: 8px; max-width: calc(100% - 8px); } /* its margin counts too: a full-width select stuck 8 px out on phones */
.oc-form input[type=url], .oc-form textarea { display: block; width: 100%; margin-top: 4px; } /* an address or a paragraph needs the width to be read */
.oc-form input[type=url], .oc-form textarea { display: block; width: 100%; margin-top: 4px; } /* an address or a paragraph needs the width to be read */
+/* "Your AI key": only the fields the chosen provider takes (a model on your own computer has no endpoint, key or budget;
+ only OpenAI-compatible providers take an endpoint). Without :has() every field shows, as before. */
+.oc-ai-form:has(select[name="provider"] option[value="node"]:checked) .oc-not-node,
+.oc-ai-form:not(:has(select[name="provider"] option[value="node"]:checked)) .oc-if-node,
+.oc-ai-form:not(:has(select[name="provider"] option[value="openai"]:checked)) .oc-if-openai { display: none; }
+.oc-ai-form input[name="machine"] { display: block; width: 100%; max-width: 36ch; box-sizing: border-box; margin-top: 4px; font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 0.92em; } /* a whole dev_ id, even at 320 px */
@media (max-width: 560px) {
@media (max-width: 560px) {
.oc-main { padding: 12px 12px 40px; }
.oc-main { padding: 12px 12px 40px; }
.oc-feed { max-height: 60vh; }
.oc-feed { max-height: 60vh; }
@@ -513,13 +513,13 @@ ${await aiKeySection(actor, req.query || {})}`,
@@ -513,13 +513,13 @@ ${await aiKeySection(actor, req.query || {})}`,
<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 Node, 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 Node, 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>
${offered ? notice('ok', `Save to let your AI members think with ${offered.model || 'a model'} on your own computer.`) : ''}
${offered ? notice('ok', `Save to let your AI members think with ${offered.model || 'a model'} 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" 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('node', 'A model on my own computer', provider)}</select></label>
-<label>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>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-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-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>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>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>
-<label>Daily budget, US dollars <input name="budget_usd_per_day" type="number" min="0" max="1000" step="0.5" value="${key && key.budget_usd_per_day != null ? key.budget_usd_per_day : 1}"> <span class="oc-muted">0 means no cap</span></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>
+<label class="oc-not-node">Daily budget, US dollars <input name="budget_usd_per_day" type="number" min="0" max="1000" step="0.5" value="${key && key.budget_usd_per_day != null ? key.budget_usd_per_day : 1}"> <span class="oc-muted">0 means no cap</span></label>
${vibeBox}
${vibeBox}
<p><button type="submit">${key ? 'Save' : 'Add key'}</button></p>
<p><button type="submit">${key ? 'Save' : 'Add key'}</button></p>
</form>
</form>
@@ -529,7 +529,7 @@ ${key ? `<form class="oc-inline" method="post" action="/settings/ai-key/delete">
@@ -529,7 +529,7 @@ ${key ? `<form class="oc-inline" method="post" action="/settings/ai-key/delete">
const actor = await needUser(req, res); if (!actor) return;
const actor = await needUser(req, res); if (!actor) return;
const b = req.body || {};
const b = req.body || {};
try {
try {
- const key = await credentials.put(actor.user.subject_id, { provider: b.provider, base_url: b.base_url, 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 });
await require('../rooms/ai-members').relabelKey(actor.user.subject_id, key, actor.user);
await require('../rooms/ai-members').relabelKey(actor.user.subject_id, key, actor.user);
const vibe = require('../rooms/vibe');
const vibe = require('../rooms/vibe');
if (vibe.enabled()) await vibe.setEngine(actor.user, b.vibe === '1');
if (vibe.enabled()) await vibe.setEngine(actor.user, b.vibe === '1');
@@ -172,6 +172,7 @@ t('a model on your own computer: the machine and no key; the settings form comes
@@ -172,6 +172,7 @@ t('a model on your own computer: the machine and no key; the settings form comes
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="node" 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('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');