openvibers/openvibe.design Commits
Design's icon generator asks AI for the design.icon task (#218)
Changes
@@@ -380,7 +380,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, preset = null, person = null }) {++ async function text({ prompt, system, maxTokens = 2000, target = null, prefer = null, preset = null, person = null, task = null, choice = null }) {const startedAt = Date.now();const who = onBehalfOf(person);const wait = Math.min(ai.waitMs, 60_000);@@@ -388,6 +388,9 @@ function createArenaAi({ config, fetchImpl = globalThis.fetch, log = console })...(system ? { system } : {}), ...(who ? { on_behalf_of: who } : {}), ...(target ? { target } : {}) };const created = await createRun({workflow: 'ai.chat',++ // A task lets AI resolve the model (the owner's default for it, the person's saved choice, their kind++ // default): Design no longer names a model for its icon drafts. A per-run choice overrides it.++ ...(task ? { task } : {}), ...(choice ? { choice } : {}),input: { ...(system ? { system } : {}), messages: [{ role: 'user', content: String(prompt == null ? '' : prompt) }], max_tokens: maxTokens },options: { cache: false },...(prefer ? { prefer } : {}),@@@ -21,7 +21,6 @@ const { fitDrawing } = require('./fit');const { searchReferences } = require('./references');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}$/;@@@ -64,7 +63,7 @@ function createGlyphDrafts({ db, ai, clock = { now: () => Date.now() }, guests =db.exec("DELETE FROM glyph_guest_draws WHERE created_at < now() - interval '2 days'").catch(() => {});}-- async function draft({ subject, name, sentence = '', guest = null, service = false, styles = null, sourceSvg = '' }) {++ async function draft({ subject, name, sentence = '', guest = null, service = false, styles = null, sourceSvg = '', choice = null }) {if (!subject && !guest) throw Object.assign(new Error('sign in to draw an icon'), { status: 401 });if (guest && !/^[0-9a-f]{16,64}$/.test(String(guest.id || ''))) throw Object.assign(new Error('this browser cannot draw without signing in'), { status: 400 });const who = subject || `guest:${guest.id}`;@@@ -92,11 +91,8 @@ function createGlyphDrafts({ db, ai, clock = { now: () => Date.now() }, guests =prompt: `${sourceLine ? `Make a solid twin of this existing line drawing for "${n}": ${sourceLine.svg}` : `The icon: "${n}".`}${what ? ` What it stands for: ${what}` : ''}${referenceWords ? `\nRelated open icon concepts for vocabulary only: ${referenceWords}. Draw an original OpenVibe glyph; do not copy a reference.` : ''}${extra}`,maxTokens: 1800,target: { service: 'design', type: 'glyph_draft', id: n },-- // 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 && !service ? { person: subject, prefer: CLAUDE } : { preset: { name: 'cheapest' } }),++ task: 'design.icon', choice,++ ...(subject && !service ? { person: subject } : {}),});let got = [];try {@@@ -273,7 +273,7 @@ function createIconRoutes(ctx) {guest = guestOf(req, res, { make: true });}try {-- const out = await drafts.draft({ subject, guest, name: (req.body || {}).name, sentence: (req.body || {}).sentence });++ const out = await drafts.draft({ subject, guest, name: (req.body || {}).name, sentence: (req.body || {}).sentence, choice: req.body.choice ? JSON.parse(req.body.choice) : null });back(res, { drafted: String(out.kept.length), draw: String((req.body || {}).name || '') });} catch (err) {if (!err.status || err.status >= 500) log.warn('[OpenVibe.Design] glyph draft failed:', err && err.message);@@@ -289,7 +289,7 @@ function createIconRoutes(ctx) {if (!writeAllowed(req, config)) return problem(res, req, 403, 'design.same_origin_required', 'Open this request from an authorized OpenVibe client.');try {const body = req.body || {};-- const out = await drafts.draft({ subject, name: body.name, sentence: body.sentence, styles: body.styles, sourceSvg: body.sourceSvg });++ const out = await drafts.draft({ subject, name: body.name, sentence: body.sentence, styles: body.styles, sourceSvg: body.sourceSvg, choice: body.choice });return res.status(201).set('Cache-Control', 'private, no-store').json({ kept: out.kept, refused: out.refused });} catch (err) {if (!err.status || err.status >= 500) log.warn('[OpenVibe.Design] glyph API draft failed:', err && err.message);@@@ -27,7 +27,7 @@ function fakeAi(calls) {enabled: true,async text(args) {calls.push(args);-- const claude = args.prefer && args.prefer.provider === 'anthropic';++ const claude = !!args.person;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' };},@@@ -43,7 +43,8 @@ function 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.equal(calls[0].task, 'design.icon', 'AI owns the signed-in default');++ assert.equal(calls[0].prefer, undefined);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');@@@ -55,7 +56,8 @@ function fakeAi(calls) {const d = createGlyphDrafts({ db, ai: fakeAi(calls), referenceSearch: async () => [] });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.equal(calls[0].task, 'design.icon', 'AI owns the guest default');++ assert.equal(calls[0].preset, undefined);assert.strictEqual(calls[0].prefer, undefined, 'a guest never asks for Claude');assert.strictEqual(calls[0].person, undefined, 'a guest is nobody\'s key');}@@@ -64,7 +66,9 @@ function fakeAi(calls) {const calls = [], db = fakeDb();const d = createGlyphDrafts({ db, ai: fakeAi(calls), referenceSearch: async () => [] });const sourceSvg = '<path d="M5 12h14"/>';-- const out = await d.draft({ subject: 'usr_2', name: 'hot-tub', sourceSvg, styles: ['solid'] });++ const choice = {model:'qwen/qwen3',source:'keys'};++ const out = await d.draft({ subject: 'usr_2', name: 'hot-tub', sourceSvg, styles: ['solid'], choice });++ assert.deepEqual(calls[0].choice, choice, 'per-run choice reaches AI');assert.ok(out.kept.length && out.kept[0].solid_svg, 'the model solid is fitted and kept');assert.ok(calls[0].system.includes('filled twins for existing'), 'solid work uses the solid-specific instructions');assert.ok(calls[0].prompt.includes(sourceSvg), 'the existing line drawing is supplied to match');@@@ -34,13 +34,14 @@ function token(t, cap = ['design.glyph.draft'], sub = 'svc:quest') {assert.equal((await post(token(t, ['design.glyph.draft']), { ...body, svg: '<svg/>' })).status, 422);assert.equal(calls.length, 0);});-- await check('a service request creates review drafts on the cheapest tier and deduplicates', async () => {++ await check('a service request creates review drafts on the design.icon task and deduplicates', async () => {const first = await post(token(t));assert.equal(first.status, 201, first.text.slice(0, 180));assert.equal(first.json().status, 'pending');assert.ok(first.json().draft_ids.length > 0);assert.equal(calls.length, 1);-- assert.deepEqual(calls[0].preset, { name: 'cheapest' });++ assert.equal(calls[0].task, 'design.icon', 'AI resolves the model for the task');++ assert.equal(calls[0].preset, undefined, 'Design no longer names a routing preset');assert.equal(calls[0].person, undefined, 'a service request never spends a person’s key');const again = await post(token(t));assert.equal(again.status, 200);
@@ -380,7 +380,7 @@ function createArenaAi({ config, fetchImpl = globalThis.fetch, log = console })
@@ -380,7 +380,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, preset = null, person = null }) {
+ async function text({ prompt, system, maxTokens = 2000, target = null, prefer = null, preset = null, person = null, task = null, choice = 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,9 @@ function createArenaAi({ config, fetchImpl = globalThis.fetch, log = console })
@@ -388,6 +388,9 @@ function createArenaAi({ config, fetchImpl = globalThis.fetch, log = console })
...(system ? { system } : {}), ...(who ? { on_behalf_of: who } : {}), ...(target ? { target } : {}) };
...(system ? { system } : {}), ...(who ? { on_behalf_of: who } : {}), ...(target ? { target } : {}) };
const created = await createRun({
const created = await createRun({
workflow: 'ai.chat',
workflow: 'ai.chat',
+ // A task lets AI resolve the model (the owner's default for it, the person's saved choice, their kind
+ // default): Design no longer names a model for its icon drafts. A per-run choice overrides it.
+ ...(task ? { task } : {}), ...(choice ? { choice } : {}),
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 } : {}),
@@ -21,7 +21,6 @@ const { fitDrawing } = require('./fit');
@@ -21,7 +21,6 @@ const { fitDrawing } = require('./fit');
const { searchReferences } = require('./references');
const { searchReferences } = require('./references');
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}$/;
@@ -64,7 +63,7 @@ function createGlyphDrafts({ db, ai, clock = { now: () => Date.now() }, guests =
@@ -64,7 +63,7 @@ function createGlyphDrafts({ db, ai, clock = { now: () => Date.now() }, guests =
db.exec("DELETE FROM glyph_guest_draws WHERE created_at < now() - interval '2 days'").catch(() => {});
db.exec("DELETE FROM glyph_guest_draws WHERE created_at < now() - interval '2 days'").catch(() => {});
}
}
- async function draft({ subject, name, sentence = '', guest = null, service = false, styles = null, sourceSvg = '' }) {
+ async function draft({ subject, name, sentence = '', guest = null, service = false, styles = null, sourceSvg = '', choice = null }) {
if (!subject && !guest) throw Object.assign(new Error('sign in to draw an icon'), { status: 401 });
if (!subject && !guest) throw Object.assign(new Error('sign in to draw an icon'), { status: 401 });
if (guest && !/^[0-9a-f]{16,64}$/.test(String(guest.id || ''))) throw Object.assign(new Error('this browser cannot draw without signing in'), { status: 400 });
if (guest && !/^[0-9a-f]{16,64}$/.test(String(guest.id || ''))) throw Object.assign(new Error('this browser cannot draw without signing in'), { status: 400 });
const who = subject || `guest:${guest.id}`;
const who = subject || `guest:${guest.id}`;
@@ -92,11 +91,8 @@ function createGlyphDrafts({ db, ai, clock = { now: () => Date.now() }, guests =
@@ -92,11 +91,8 @@ function createGlyphDrafts({ db, ai, clock = { now: () => Date.now() }, guests =
prompt: `${sourceLine ? `Make a solid twin of this existing line drawing for "${n}": ${sourceLine.svg}` : `The icon: "${n}".`}${what ? ` What it stands for: ${what}` : ''}${referenceWords ? `\nRelated open icon concepts for vocabulary only: ${referenceWords}. Draw an original OpenVibe glyph; do not copy a reference.` : ''}${extra}`,
prompt: `${sourceLine ? `Make a solid twin of this existing line drawing for "${n}": ${sourceLine.svg}` : `The icon: "${n}".`}${what ? ` What it stands for: ${what}` : ''}${referenceWords ? `\nRelated open icon concepts for vocabulary only: ${referenceWords}. Draw an original OpenVibe glyph; do not copy a reference.` : ''}${extra}`,
maxTokens: 1800,
maxTokens: 1800,
target: { service: 'design', type: 'glyph_draft', id: n },
target: { service: 'design', type: 'glyph_draft', id: n },
- // 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 && !service ? { person: subject, prefer: CLAUDE } : { preset: { name: 'cheapest' } }),
+ task: 'design.icon', choice,
+ ...(subject && !service ? { person: subject } : {}),
});
});
let got = [];
let got = [];
try {
try {
@@ -273,7 +273,7 @@ function createIconRoutes(ctx) {
@@ -273,7 +273,7 @@ function createIconRoutes(ctx) {
guest = guestOf(req, res, { make: true });
guest = guestOf(req, res, { make: true });
}
}
try {
try {
- const out = await drafts.draft({ subject, guest, name: (req.body || {}).name, sentence: (req.body || {}).sentence });
+ const out = await drafts.draft({ subject, guest, name: (req.body || {}).name, sentence: (req.body || {}).sentence, choice: req.body.choice ? JSON.parse(req.body.choice) : null });
back(res, { drafted: String(out.kept.length), draw: String((req.body || {}).name || '') });
back(res, { drafted: String(out.kept.length), draw: String((req.body || {}).name || '') });
} catch (err) {
} catch (err) {
if (!err.status || err.status >= 500) log.warn('[OpenVibe.Design] glyph draft failed:', err && err.message);
if (!err.status || err.status >= 500) log.warn('[OpenVibe.Design] glyph draft failed:', err && err.message);
@@ -289,7 +289,7 @@ function createIconRoutes(ctx) {
@@ -289,7 +289,7 @@ function createIconRoutes(ctx) {
if (!writeAllowed(req, config)) return problem(res, req, 403, 'design.same_origin_required', 'Open this request from an authorized OpenVibe client.');
if (!writeAllowed(req, config)) return problem(res, req, 403, 'design.same_origin_required', 'Open this request from an authorized OpenVibe client.');
try {
try {
const body = req.body || {};
const body = req.body || {};
- const out = await drafts.draft({ subject, name: body.name, sentence: body.sentence, styles: body.styles, sourceSvg: body.sourceSvg });
+ const out = await drafts.draft({ subject, name: body.name, sentence: body.sentence, styles: body.styles, sourceSvg: body.sourceSvg, choice: body.choice });
return res.status(201).set('Cache-Control', 'private, no-store').json({ kept: out.kept, refused: out.refused });
return res.status(201).set('Cache-Control', 'private, no-store').json({ kept: out.kept, refused: out.refused });
} catch (err) {
} catch (err) {
if (!err.status || err.status >= 500) log.warn('[OpenVibe.Design] glyph API draft failed:', err && err.message);
if (!err.status || err.status >= 500) log.warn('[OpenVibe.Design] glyph API draft failed:', err && err.message);
@@ -27,7 +27,7 @@ function fakeAi(calls) {
@@ -27,7 +27,7 @@ function fakeAi(calls) {
enabled: true,
enabled: true,
async text(args) {
async text(args) {
calls.push(args);
calls.push(args);
- const claude = args.prefer && args.prefer.provider === 'anthropic';
+ const claude = !!args.person;
return { text: JSON.stringify({ candidates: [{ line: LINE, solid: SOLID }, { line: '<path d="M12 5v14"/>', solid: SOLID }] }),
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' };
provider: claude ? 'anthropic' : 'shared', model: claude ? 'claude-opus-5-5' : 'gpt-5-nano' };
},
},
@@ -43,7 +43,8 @@ function fakeAi(calls) {
@@ -43,7 +43,8 @@ function fakeAi(calls) {
const out = await d.draft({ subject: 'usr_1', name: 'hot-tub', sentence: 'a round tub with steam' });
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.ok(out.kept.length >= 1, 'drafts are kept');
assert.strictEqual(calls.length, 1);
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.equal(calls[0].task, 'design.icon', 'AI owns the signed-in default');
+ assert.equal(calls[0].prefer, undefined);
assert.strictEqual(calls[0].person, 'usr_1', 'as the person, so their own key is tried first');
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(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');
assert.strictEqual(out.kept[0].model, 'claude-opus-5-5', 'the draft records the model that drew it');
@@ -55,7 +56,8 @@ function fakeAi(calls) {
@@ -55,7 +56,8 @@ function fakeAi(calls) {
const d = createGlyphDrafts({ db, ai: fakeAi(calls), referenceSearch: async () => [] });
const d = createGlyphDrafts({ db, ai: fakeAi(calls), referenceSearch: async () => [] });
const out = await d.draft({ subject: null, guest: { id: 'a'.repeat(32), ipHash: 'h1' }, name: 'hot-tub' });
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.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.equal(calls[0].task, 'design.icon', 'AI owns the guest default');
+ assert.equal(calls[0].preset, undefined);
assert.strictEqual(calls[0].prefer, undefined, 'a guest never asks for Claude');
assert.strictEqual(calls[0].prefer, undefined, 'a guest never asks for Claude');
assert.strictEqual(calls[0].person, undefined, 'a guest is nobody\'s key');
assert.strictEqual(calls[0].person, undefined, 'a guest is nobody\'s key');
}
}
@@ -64,7 +66,9 @@ function fakeAi(calls) {
@@ -64,7 +66,9 @@ function fakeAi(calls) {
const calls = [], db = fakeDb();
const calls = [], db = fakeDb();
const d = createGlyphDrafts({ db, ai: fakeAi(calls), referenceSearch: async () => [] });
const d = createGlyphDrafts({ db, ai: fakeAi(calls), referenceSearch: async () => [] });
const sourceSvg = '<path d="M5 12h14"/>';
const sourceSvg = '<path d="M5 12h14"/>';
- const out = await d.draft({ subject: 'usr_2', name: 'hot-tub', sourceSvg, styles: ['solid'] });
+ const choice = {model:'qwen/qwen3',source:'keys'};
+ const out = await d.draft({ subject: 'usr_2', name: 'hot-tub', sourceSvg, styles: ['solid'], choice });
+ assert.deepEqual(calls[0].choice, choice, 'per-run choice reaches AI');
assert.ok(out.kept.length && out.kept[0].solid_svg, 'the model solid is fitted and kept');
assert.ok(out.kept.length && out.kept[0].solid_svg, 'the model solid is fitted and kept');
assert.ok(calls[0].system.includes('filled twins for existing'), 'solid work uses the solid-specific instructions');
assert.ok(calls[0].system.includes('filled twins for existing'), 'solid work uses the solid-specific instructions');
assert.ok(calls[0].prompt.includes(sourceSvg), 'the existing line drawing is supplied to match');
assert.ok(calls[0].prompt.includes(sourceSvg), 'the existing line drawing is supplied to match');
@@ -34,13 +34,14 @@ function token(t, cap = ['design.glyph.draft'], sub = 'svc:quest') {
@@ -34,13 +34,14 @@ function token(t, cap = ['design.glyph.draft'], sub = 'svc:quest') {
assert.equal((await post(token(t, ['design.glyph.draft']), { ...body, svg: '<svg/>' })).status, 422);
assert.equal((await post(token(t, ['design.glyph.draft']), { ...body, svg: '<svg/>' })).status, 422);
assert.equal(calls.length, 0);
assert.equal(calls.length, 0);
});
});
- await check('a service request creates review drafts on the cheapest tier and deduplicates', async () => {
+ await check('a service request creates review drafts on the design.icon task and deduplicates', async () => {
const first = await post(token(t));
const first = await post(token(t));
assert.equal(first.status, 201, first.text.slice(0, 180));
assert.equal(first.status, 201, first.text.slice(0, 180));
assert.equal(first.json().status, 'pending');
assert.equal(first.json().status, 'pending');
assert.ok(first.json().draft_ids.length > 0);
assert.ok(first.json().draft_ids.length > 0);
assert.equal(calls.length, 1);
assert.equal(calls.length, 1);
- assert.deepEqual(calls[0].preset, { name: 'cheapest' });
+ assert.equal(calls[0].task, 'design.icon', 'AI resolves the model for the task');
+ assert.equal(calls[0].preset, undefined, 'Design no longer names a routing preset');
assert.equal(calls[0].person, undefined, 'a service request never spends a person’s key');
assert.equal(calls[0].person, undefined, 'a service request never spends a person’s key');
const again = await post(token(t));
const again = await post(token(t));
assert.equal(again.status, 200);
assert.equal(again.status, 200);