openvibers/openvibe.design Commits
Draw an icon: guests on the cheapest model, signed in on Claude Opus 5.5 (#199)
Changes
@@@ -377,7 +377,7 @@ function createArenaAi({ config, fetchImpl = globalThis.fetch, log = console })* Plain text from OpenVibe.AI's routing (the glyph drafts, L32): the same token and run as the Arena, one answer.* → { text, model, provider, run_id, paid_by, prefer }*/-- async function text({ prompt, system, maxTokens = 2000, target = null, prefer = null, person = null }) {++ async function text({ prompt, system, maxTokens = 2000, target = null, prefer = null, preset = null, person = null }) {const startedAt = Date.now();const who = onBehalfOf(person);const wait = Math.min(ai.waitMs, 60_000);@@@ -388,6 +388,7 @@ function createArenaAi({ config, fetchImpl = globalThis.fetch, log = console })input: { ...(system ? { system } : {}), messages: [{ role: 'user', content: String(prompt == null ? '' : prompt) }], max_tokens: maxTokens },options: { cache: false },...(prefer ? { prefer } : {}),++ ...(preset ? { preset } : {}), // a built-in policy by name, e.g. { name: 'cheapest' }...(who ? { on_behalf_of: who } : {}),...(target ? { target } : {}),}, legacy, wait);@@@ -19,6 +19,7 @@ const crypto = require('crypto');const { clean, cleanSolid } = require('./validate');const PER_DAY = 12;++const CLAUDE = Object.freeze({ provider: 'anthropic', model: 'claude-opus-5-5' });const CANDIDATES = 4;const NAME_RE = /^[a-z][a-z0-9-]{1,31}$/;@@@ -80,9 +81,11 @@ function createGlyphDrafts({ db, ai, clock = { now: () => Date.now() }, guests =prompt: `The icon: "${n}".${what ? ` What it stands for: ${what}` : ''}${extra}`,maxTokens: 1800,target: { service: 'design', type: 'glyph_draft', id: n },-- prefer: { provider: 'anthropic', model: 'claude-opus-5-5' }, // Claude first, soft, on the person's key (L37)-- // A person's own key when they have one (L37); a guest runs on OpenVibe's, inside the caps above.-- ...(subject ? { person: subject } : {}),++ // Two tiers (owner, 11-09: "the free one should use the cheapest or most cost-effective model if people++ // aren't logged in… log in for the maximum tier, Anthropic"). Signed in: Claude Opus 5.5 first, on the++ // person's own key when they have one, else OpenVibe's Anthropic offer. A guest: the cheapest offer that++ // can do the job, on OpenVibe's, inside the guest caps above.++ ...(subject ? { person: subject, prefer: CLAUDE } : { preset: { name: 'cheapest' } }),});let got = [];try {@@@ -97,7 +97,7 @@ function iconsPage({ base = 'https://openvibe.design', drafts = [], mine = [], r<div class="ic-sec-head"><h2 id="ic-draw">Draw an icon</h2><p>Name it and say what it stands for. OpenVibe.AI draws a few candidates in this hand; each is checked (strokes only, on the grid, a few shapes) before it is kept, and an admin reviews it before it joins the set.</p></div>${notice ? kit.notice({ kind: 'ok', text: notice }) : ''}${error ? kit.notice({ kind: 'error', text: error }) : ''}<form class="ic-form" method="post" action="/icons/draft">-- <label><span>Name</span><input name="name" required pattern="[a-z][a-z0-9-]{1,31}" maxlength="32" placeholder="hot-tub" value="${esc(draw)}" autocomplete="off" spellcheck="false"></label>++ <label><span>Name</span><input name="name" required pattern="[a-z][a-z0-9\\-]{1,31}" maxlength="32" placeholder="hot-tub" value="${esc(draw)}" autocomplete="off" spellcheck="false"></label><label class="ic-grow"><span>What it stands for</span><input name="sentence" maxlength="200" placeholder="A tub with steam over it: a hot tub at a rental"></label>${guest && guest.turnstile ? `<div class="cf-turnstile ic-human" data-sitekey="${esc(guest.turnstile)}" data-theme="auto" data-size="flexible"></div><script src="https://challenges.cloudflare.com/turnstile/v0/api.js" async defer></script>` : ''}<button type="submit" class="ic-btn is-go">${kit.icon('spark')} Draw it</button>nnew file mode 100644@@@ -0,0 +1,75 @@++'use strict';++// Who draws an icon (owner, 11-09): a guest's draft runs on the cheapest offer that can do the job ("the free one should++// use the cheapest or most cost-effective model if people aren't logged in"); a signed-in person's asks for Claude++// Opus 5.5 ("log in for the maximum tier… Anthropic, which has the best design ability"), on their own key when they++// have one. Before this, every draft (guest or not) went out with the same soft Claude preference and no key behind++// it, and all of them came back from the shared pool's smallest model. And the form's name pattern is a valid++// expression for the browser's v-flag check (an unescaped "-" in a class made Chrome drop it with a console error).++// node test/glyph-draft-tiers.test.js++const assert = require('assert');++const fs = require('fs');++const path = require('path');++const { createGlyphDrafts } = require('../server/glyphs/drafts');++++const LINE = '<path d="M5 12h14"/>';++const SOLID = '<path d="M5 11h14v2H5z"/>';++function fakeDb() {++ const rows = [];++ return {++ rows,++ async maybe(sql) { if (/count\(\*\)/.test(sql)) return { n: 0 }; return null; },++ async exec(sql, args) { if (/INSERT INTO glyph_drafts/.test(sql)) rows.push(args); },++ async many() { return []; },++ };++}++function fakeAi(calls) {++ return {++ enabled: true,++ async text(args) {++ calls.push(args);++ const claude = args.prefer && args.prefer.provider === 'anthropic';++ return { text: JSON.stringify({ candidates: [{ line: LINE, solid: SOLID }, { line: '<path d="M12 5v14"/>', solid: SOLID }] }),++ provider: claude ? 'anthropic' : 'shared', model: claude ? 'claude-opus-5-5' : 'gpt-5-nano' };++ },++ };++}++++(async () => {++ // A signed-in person: Claude Opus 5.5, as that person (their own key first), no cost preset.++ {++ const calls = [];++ const db = fakeDb();++ const d = createGlyphDrafts({ db, ai: fakeAi(calls) });++ const out = await d.draft({ subject: 'usr_1', name: 'hot-tub', sentence: 'a round tub with steam' });++ assert.ok(out.kept.length >= 1, 'drafts are kept');++ assert.strictEqual(calls.length, 1);++ assert.deepStrictEqual(calls[0].prefer, { provider: 'anthropic', model: 'claude-opus-5-5' }, 'signed in asks for Claude Opus 5.5');++ assert.strictEqual(calls[0].person, 'usr_1', 'as the person, so their own key is tried first');++ assert.strictEqual(calls[0].preset, undefined, 'no cheapest preset for a signed-in person');++ assert.strictEqual(out.kept[0].model, 'claude-opus-5-5', 'the draft records the model that drew it');++ }++ // A guest: the cheapest offer, on OpenVibe's, never a Claude preference and never as a person.++ {++ const calls = [];++ const db = fakeDb();++ const d = createGlyphDrafts({ db, ai: fakeAi(calls) });++ const out = await d.draft({ subject: null, guest: { id: 'a'.repeat(32), ipHash: 'h1' }, name: 'hot-tub' });++ assert.ok(out.kept.length >= 1, 'a guest gets drafts too');++ assert.deepStrictEqual(calls[0].preset, { name: 'cheapest' }, 'a guest runs on the cheapest offer');++ assert.strictEqual(calls[0].prefer, undefined, 'a guest never asks for Claude');++ assert.strictEqual(calls[0].person, undefined, 'a guest is nobody\'s key');++ }++ // The AI client carries the preset into the run it creates.++ const aiSrc = fs.readFileSync(path.join(__dirname, '..', 'server', 'arena', 'ai.js'), 'utf8');++ assert.match(aiSrc, /async function text\(\{[^}]*preset = null/, 'text() takes a preset');++ assert.match(aiSrc, /\.\.\.\(preset \? \{ preset \} : \{\}\)/, 'and sends it with the run');++++ // The name pattern renders as a valid v-flag expression.++ const views = fs.readFileSync(path.join(__dirname, '..', 'server', 'views', 'icons.js'), 'utf8');++ const m = views.match(/<input name="name" required pattern="([^"]*)"/);++ assert.ok(m, 'the name field has a pattern');++ const rendered = m[1].replace(/\\\\/g, '\\'); // the template literal's "\\-" is "\-" in the page++ assert.doesNotThrow(() => new RegExp(`^(?:${rendered})$`, 'v'), `the pattern ${rendered} is valid for the browser`);++ assert.ok(new RegExp(`^(?:${rendered})$`, 'v').test('hot-tub') && !new RegExp(`^(?:${rendered})$`, 'v').test('Hot tub'), 'and still means a lowercase name with dashes');++ console.log('glyph draft tiers: guests on the cheapest offer, signed in on Claude Opus 5.5; the name pattern is valid');++})().catch((e) => { console.error(e); process.exit(1); });
@@ -377,7 +377,7 @@ function createArenaAi({ config, fetchImpl = globalThis.fetch, log = console })
@@ -377,7 +377,7 @@ function createArenaAi({ config, fetchImpl = globalThis.fetch, log = console })
* Plain text from OpenVibe.AI's routing (the glyph drafts, L32): the same token and run as the Arena, one answer.
* Plain text from OpenVibe.AI's routing (the glyph drafts, L32): the same token and run as the Arena, one answer.
* → { text, model, provider, run_id, paid_by, prefer }
* → { text, model, provider, run_id, paid_by, prefer }
*/
*/
- async function text({ prompt, system, maxTokens = 2000, target = null, prefer = null, person = null }) {
+ async function text({ prompt, system, maxTokens = 2000, target = null, prefer = null, preset = null, person = null }) {
const startedAt = Date.now();
const startedAt = Date.now();
const who = onBehalfOf(person);
const who = onBehalfOf(person);
const wait = Math.min(ai.waitMs, 60_000);
const wait = Math.min(ai.waitMs, 60_000);
@@ -388,6 +388,7 @@ function createArenaAi({ config, fetchImpl = globalThis.fetch, log = console })
@@ -388,6 +388,7 @@ function createArenaAi({ config, fetchImpl = globalThis.fetch, log = console })
input: { ...(system ? { system } : {}), messages: [{ role: 'user', content: String(prompt == null ? '' : prompt) }], max_tokens: maxTokens },
input: { ...(system ? { system } : {}), messages: [{ role: 'user', content: String(prompt == null ? '' : prompt) }], max_tokens: maxTokens },
options: { cache: false },
options: { cache: false },
...(prefer ? { prefer } : {}),
...(prefer ? { prefer } : {}),
+ ...(preset ? { preset } : {}), // a built-in policy by name, e.g. { name: 'cheapest' }
...(who ? { on_behalf_of: who } : {}),
...(who ? { on_behalf_of: who } : {}),
...(target ? { target } : {}),
...(target ? { target } : {}),
}, legacy, wait);
}, legacy, wait);
@@ -19,6 +19,7 @@ const crypto = require('crypto');
@@ -19,6 +19,7 @@ const crypto = require('crypto');
const { clean, cleanSolid } = require('./validate');
const { clean, cleanSolid } = require('./validate');
const PER_DAY = 12;
const PER_DAY = 12;
+const CLAUDE = Object.freeze({ provider: 'anthropic', model: 'claude-opus-5-5' });
const CANDIDATES = 4;
const CANDIDATES = 4;
const NAME_RE = /^[a-z][a-z0-9-]{1,31}$/;
const NAME_RE = /^[a-z][a-z0-9-]{1,31}$/;
@@ -80,9 +81,11 @@ function createGlyphDrafts({ db, ai, clock = { now: () => Date.now() }, guests =
@@ -80,9 +81,11 @@ function createGlyphDrafts({ db, ai, clock = { now: () => Date.now() }, guests =
prompt: `The icon: "${n}".${what ? ` What it stands for: ${what}` : ''}${extra}`,
prompt: `The icon: "${n}".${what ? ` What it stands for: ${what}` : ''}${extra}`,
maxTokens: 1800,
maxTokens: 1800,
target: { service: 'design', type: 'glyph_draft', id: n },
target: { service: 'design', type: 'glyph_draft', id: n },
- prefer: { provider: 'anthropic', model: 'claude-opus-5-5' }, // Claude first, soft, on the person's key (L37)
- // A person's own key when they have one (L37); a guest runs on OpenVibe's, inside the caps above.
- ...(subject ? { person: subject } : {}),
+ // Two tiers (owner, 11-09: "the free one should use the cheapest or most cost-effective model if people
+ // aren't logged in… log in for the maximum tier, Anthropic"). Signed in: Claude Opus 5.5 first, on the
+ // person's own key when they have one, else OpenVibe's Anthropic offer. A guest: the cheapest offer that
+ // can do the job, on OpenVibe's, inside the guest caps above.
+ ...(subject ? { person: subject, prefer: CLAUDE } : { preset: { name: 'cheapest' } }),
});
});
let got = [];
let got = [];
try {
try {
@@ -97,7 +97,7 @@ function iconsPage({ base = 'https://openvibe.design', drafts = [], mine = [], r
@@ -97,7 +97,7 @@ function iconsPage({ base = 'https://openvibe.design', drafts = [], mine = [], r
<div class="ic-sec-head"><h2 id="ic-draw">Draw an icon</h2><p>Name it and say what it stands for. OpenVibe.AI draws a few candidates in this hand; each is checked (strokes only, on the grid, a few shapes) before it is kept, and an admin reviews it before it joins the set.</p></div>
<div class="ic-sec-head"><h2 id="ic-draw">Draw an icon</h2><p>Name it and say what it stands for. OpenVibe.AI draws a few candidates in this hand; each is checked (strokes only, on the grid, a few shapes) before it is kept, and an admin reviews it before it joins the set.</p></div>
${notice ? kit.notice({ kind: 'ok', text: notice }) : ''}${error ? kit.notice({ kind: 'error', text: error }) : ''}
${notice ? kit.notice({ kind: 'ok', text: notice }) : ''}${error ? kit.notice({ kind: 'error', text: error }) : ''}
<form class="ic-form" method="post" action="/icons/draft">
<form class="ic-form" method="post" action="/icons/draft">
- <label><span>Name</span><input name="name" required pattern="[a-z][a-z0-9-]{1,31}" maxlength="32" placeholder="hot-tub" value="${esc(draw)}" autocomplete="off" spellcheck="false"></label>
+ <label><span>Name</span><input name="name" required pattern="[a-z][a-z0-9\\-]{1,31}" maxlength="32" placeholder="hot-tub" value="${esc(draw)}" autocomplete="off" spellcheck="false"></label>
<label class="ic-grow"><span>What it stands for</span><input name="sentence" maxlength="200" placeholder="A tub with steam over it: a hot tub at a rental"></label>
<label class="ic-grow"><span>What it stands for</span><input name="sentence" maxlength="200" placeholder="A tub with steam over it: a hot tub at a rental"></label>
${guest && guest.turnstile ? `<div class="cf-turnstile ic-human" data-sitekey="${esc(guest.turnstile)}" data-theme="auto" data-size="flexible"></div><script src="https://challenges.cloudflare.com/turnstile/v0/api.js" async defer></script>` : ''}
${guest && guest.turnstile ? `<div class="cf-turnstile ic-human" data-sitekey="${esc(guest.turnstile)}" data-theme="auto" data-size="flexible"></div><script src="https://challenges.cloudflare.com/turnstile/v0/api.js" async defer></script>` : ''}
<button type="submit" class="ic-btn is-go">${kit.icon('spark')} Draw it</button>
<button type="submit" class="ic-btn is-go">${kit.icon('spark')} Draw it</button>
new file mode 100644
new file mode 100644
@@ -0,0 +1,75 @@
@@ -0,0 +1,75 @@
+'use strict';
+// Who draws an icon (owner, 11-09): a guest's draft runs on the cheapest offer that can do the job ("the free one should
+// use the cheapest or most cost-effective model if people aren't logged in"); a signed-in person's asks for Claude
+// Opus 5.5 ("log in for the maximum tier… Anthropic, which has the best design ability"), on their own key when they
+// have one. Before this, every draft (guest or not) went out with the same soft Claude preference and no key behind
+// it, and all of them came back from the shared pool's smallest model. And the form's name pattern is a valid
+// expression for the browser's v-flag check (an unescaped "-" in a class made Chrome drop it with a console error).
+// node test/glyph-draft-tiers.test.js
+const assert = require('assert');
+const fs = require('fs');
+const path = require('path');
+const { createGlyphDrafts } = require('../server/glyphs/drafts');
+
+const LINE = '<path d="M5 12h14"/>';
+const SOLID = '<path d="M5 11h14v2H5z"/>';
+function fakeDb() {
+ const rows = [];
+ return {
+ rows,
+ async maybe(sql) { if (/count\(\*\)/.test(sql)) return { n: 0 }; return null; },
+ async exec(sql, args) { if (/INSERT INTO glyph_drafts/.test(sql)) rows.push(args); },
+ async many() { return []; },
+ };
+}
+function fakeAi(calls) {
+ return {
+ enabled: true,
+ async text(args) {
+ calls.push(args);
+ const claude = args.prefer && args.prefer.provider === 'anthropic';
+ return { text: JSON.stringify({ candidates: [{ line: LINE, solid: SOLID }, { line: '<path d="M12 5v14"/>', solid: SOLID }] }),
+ provider: claude ? 'anthropic' : 'shared', model: claude ? 'claude-opus-5-5' : 'gpt-5-nano' };
+ },
+ };
+}
+
+(async () => {
+ // A signed-in person: Claude Opus 5.5, as that person (their own key first), no cost preset.
+ {
+ const calls = [];
+ const db = fakeDb();
+ const d = createGlyphDrafts({ db, ai: fakeAi(calls) });
+ const out = await d.draft({ subject: 'usr_1', name: 'hot-tub', sentence: 'a round tub with steam' });
+ assert.ok(out.kept.length >= 1, 'drafts are kept');
+ assert.strictEqual(calls.length, 1);
+ assert.deepStrictEqual(calls[0].prefer, { provider: 'anthropic', model: 'claude-opus-5-5' }, 'signed in asks for Claude Opus 5.5');
+ assert.strictEqual(calls[0].person, 'usr_1', 'as the person, so their own key is tried first');
+ assert.strictEqual(calls[0].preset, undefined, 'no cheapest preset for a signed-in person');
+ assert.strictEqual(out.kept[0].model, 'claude-opus-5-5', 'the draft records the model that drew it');
+ }
+ // A guest: the cheapest offer, on OpenVibe's, never a Claude preference and never as a person.
+ {
+ const calls = [];
+ const db = fakeDb();
+ const d = createGlyphDrafts({ db, ai: fakeAi(calls) });
+ const out = await d.draft({ subject: null, guest: { id: 'a'.repeat(32), ipHash: 'h1' }, name: 'hot-tub' });
+ assert.ok(out.kept.length >= 1, 'a guest gets drafts too');
+ assert.deepStrictEqual(calls[0].preset, { name: 'cheapest' }, 'a guest runs on the cheapest offer');
+ assert.strictEqual(calls[0].prefer, undefined, 'a guest never asks for Claude');
+ assert.strictEqual(calls[0].person, undefined, 'a guest is nobody\'s key');
+ }
+ // The AI client carries the preset into the run it creates.
+ const aiSrc = fs.readFileSync(path.join(__dirname, '..', 'server', 'arena', 'ai.js'), 'utf8');
+ assert.match(aiSrc, /async function text\(\{[^}]*preset = null/, 'text() takes a preset');
+ assert.match(aiSrc, /\.\.\.\(preset \? \{ preset \} : \{\}\)/, 'and sends it with the run');
+
+ // The name pattern renders as a valid v-flag expression.
+ const views = fs.readFileSync(path.join(__dirname, '..', 'server', 'views', 'icons.js'), 'utf8');
+ const m = views.match(/<input name="name" required pattern="([^"]*)"/);
+ assert.ok(m, 'the name field has a pattern');
+ const rendered = m[1].replace(/\\\\/g, '\\'); // the template literal's "\\-" is "\-" in the page
+ assert.doesNotThrow(() => new RegExp(`^(?:${rendered})$`, 'v'), `the pattern ${rendered} is valid for the browser`);
+ assert.ok(new RegExp(`^(?:${rendered})$`, 'v').test('hot-tub') && !new RegExp(`^(?:${rendered})$`, 'v').test('Hot tub'), 'and still means a lowercase name with dashes');
+ console.log('glyph draft tiers: guests on the cheapest offer, signed in on Claude Opus 5.5; the name pattern is valid');
+})().catch((e) => { console.error(e); process.exit(1); });