openvibers/openvibe.design Commits

Draw an icon: guests on the cheapest model, signed in on Claude Opus 5.5 (#199)

OP OpenVibers committed 4 days ago · a71def9676 · 4 files, +84 −5

A guest's draft now runs on AI's built-in 'cheapest' preset, on OpenVibe's, inside the guest caps; a signed-in
person's asks for Claude Opus 5.5 as that person, so their own Anthropic key is tried first, then OpenVibe's
Anthropic offer (owner, 11-09: cheapest when signed out, the maximum tier, Anthropic, signed in). Until now every
draft went out with the same soft Claude preference and all came back from the shared pool's GPT-5 nano.

The name field's pattern escapes its dash, so Chrome's v-flag check keeps it instead of dropping it with a console
error. Test: test/glyph-draft-tiers.test.js.

Changes

ddiff --git a/server/arena/ai.js b/server/arena/ai.js
iindex 91dcd25..4fb76e1 100644
---- a/server/arena/ai.js
++++ b/server/arena/ai.js
@@@ -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);
ddiff --git a/server/glyphs/drafts.js b/server/glyphs/drafts.js
iindex 9858862..0aab77d 100644
---- a/server/glyphs/drafts.js
++++ b/server/glyphs/drafts.js
@@@ -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 {
ddiff --git a/server/views/icons.js b/server/views/icons.js
iindex c3e0bcc..232f99a 100644
---- a/server/views/icons.js
++++ b/server/views/icons.js
@@@ -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>
ddiff --git a/test/glyph-draft-tiers.test.js b/test/glyph-draft-tiers.test.js
nnew file mode 100644
iindex 0000000..c45a00f
---- /dev/null
++++ b/test/glyph-draft-tiers.test.js
@@@ -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); });