openvibers/openvibe.ai Commits

AI: a live.creative route (the chat pool with AI_CREATIVE_MODEL, gpt-5-mini on OpenAI's API, as authority) for live.arena.drama (#99)

OP OpenVibers committed 28 days ago · 814ebfe249 · 3 files, +11 −3

Changes

ddiff --git a/server/workflows/live.js b/server/workflows/live.js
iindex b7fe236..b1a04c2 100644
---- a/server/workflows/live.js
++++ b/server/workflows/live.js
@@@ -149,7 +149,7 @@ const templates = [
output_schema: {"type": "object", "additionalProperties": false, "required": ["headline", "blurb", "alert"], "properties": {"headline": {"type": "string", "description": "≤ 110 chars"}, "blurb": {"type": "string", "description": "1-3 sentences, ≤ 420 chars"}, "alert": {"type": "integer", "minimum": 1, "maximum": 5}}},
system_prompt: "You are the anchor of ARENA ALERT, the drama-news desk of a small live-streaming site, in the style of internet drama channels: breaking-news hype, an ALL-CAPS headline, a savage roast, lolcow commentary, short punchy jokes. Rules: (1) Write each name exactly as given in streamer and other: same spelling, same capitals. (2) Use only the facts given. Copy numbers exactly; never add numbers up or invent one (a sounds map is already the count per sound). (3) Never invent an event, motive, relationship, substance, crime or accusation; speculate only as an obvious joke question. (4) Roast what happened on stream, never who they are: no slurs or jokes about race, religion, sexuality, gender, disability or looks; no sexual content, threats, doxxing or self-harm jokes. Swearing is fine. (5) quote is their own words: you may quote it verbatim, in quotation marks. Write: headline (starts with 🚨, ALL CAPS except the names as given, at most 100 characters, one savage hook), blurb (two short sentences: the facts with their numbers, then one roast line), alert (1 minor, 3 notable, 5 the biggest thing on the site today). Examples. In: {\"kind\":\"rage_quit\",\"streamer\":\"goosely\",\"facts\":{\"minutes_live\":6,\"usual_minutes\":140}} Out: {\"headline\":\"🚨 goosely RAGE QUITS AFTER 6 MINUTES\",\"blurb\":\"goosely usually lasts 140 minutes. Today the stream died in 6, faster than a microwave burrito.\",\"alert\":3} In: {\"kind\":\"ghost_town\",\"streamer\":\"finditfixit\",\"facts\":{\"minutes\":94,\"peak\":0,\"messages\":0}} Out: {\"headline\":\"🚨 finditfixit STREAMS 94 MINUTES TO ABSOLUTELY NOBODY\",\"blurb\":\"Peak viewers: 0. Chat messages: 0. The only thing watching was the encoder.\",\"alert\":2} Output only the JSON.",
user_prompt: "{{json payload}}",
-- default_route: 'live.chat', owner: 'live', metadata: { json_name: 'arena_drama' },
++ default_route: 'live.creative', owner: 'live', metadata: { json_name: 'arena_drama' },
},
{
key: 'live.clips.confirm', name: "Live: is this chat spike clip-worthy",
ddiff --git a/server/workflows/seed.js b/server/workflows/seed.js
iindex 0cd4990..fd94a77 100644
---- a/server/workflows/seed.js
++++ b/server/workflows/seed.js
@@@ -6,7 +6,7 @@
* AI_MODEL), http-seam (AI_HTTP_SEAM_URL), whisper (WHISPER_*), local (AI_LOCAL_LLM_URL)
* — records whose origin is 'seed' follow the environment; admin-edited ones are left alone
* models the configured default model
-- * routes live.<role> for Live's roles, live.stt, default.chat|json|embedding and media.local|paid
++ * routes live.<role> for Live's roles, live.stt, live.creative (AI_CREATIVE_MODEL), default.chat|json|embedding and media.local|paid
* as capability pools ordered by openvibe-sdk/placement; and the historical
* product route keys as explicit aliases of default.json
* templates, workflows from server/workflows/{live,core,products,media,chat}.js — a code change becomes a
@@@ -126,6 +126,12 @@ async function seed({ registry, quotas, config, env = process.env, db }) {
await registry.seedVersioned('route', 'food.vision', { primary: { provider: 'shared', model: null }, fallbacks: [], options: {}, max_output_tokens: 1200, response_format: 'json', timeout_ms: 60000, alias_of: 'default.json' });
// OpenVibe.Pics' alt text (pics.describe_image): JSON from an image, on the JSON pool like food.vision.
await registry.seedVersioned('route', 'pics.vision', { primary: { provider: 'shared', model: null }, fallbacks: [], options: {}, max_output_tokens: 400, response_format: 'json', timeout_ms: 60000, alias_of: 'default.json' });
++ // Writing people read for fun (the Arena's drama desk, live.arena.drama): the chat pool with a stronger model as its
++ // authority (AI_CREATIVE_MODEL, gpt-5-mini on OpenAI's API by default; the shared default elsewhere). The nano default
++ // misspelled names and wrote flat copy (2026-10-16); the desk makes at most ~20 short calls an hour.
++ const creativeModel = env.AI_CREATIVE_MODEL || (shared && shared.kind === 'openai' && /api\.openai\.com/i.test(shared.base_url || '') ? 'gpt-5-mini' : null);
++ if (creativeModel && !await registry.getModel('shared', creativeModel)) await registry.upsertModel({ provider_key: 'shared', model_key: creativeModel, type: 'chat', supports: { json: true, vision: true } }, { actor: 'seed' });
++ await registry.seedVersioned('route', 'live.creative', pool(ROLE_TIMEOUT.chat, creativeModel ? { constraints: { objective: 'correctness', latency_class: 'interactive', authority: `shared:${creativeModel}` } } : {}));
// Chat's AI members (server/workflows/chat.js): conversation text, so an alias of the chat pool.
await registry.seedVersioned('route', 'chat.member', { primary: { provider: 'shared', model: null }, fallbacks: [], options: {}, max_output_tokens: 600, response_format: 'text', timeout_ms: null, alias_of: 'default.chat' });
ddiff --git a/test/provider-router.test.js b/test/provider-router.test.js
iindex 0fe89ec..1b8f9fa 100644
---- a/test/provider-router.test.js
++++ b/test/provider-router.test.js
@@@ -277,7 +277,9 @@ t.test('seeded text pools keep the shared provider as authority: a free local mo
if (!rr || rr.disabled || rr.capability !== 'chat' || rr.created_by !== 'seed' || !/^(live\.|default\.(chat|json)$)/.test(rr.key)) continue;
checked++;
assert.strictEqual(rr.constraints.objective, 'correctness', `${rr.key}: correctness keeps the authority`);
-- assert.strictEqual(rr.constraints.authority, authority, `${rr.key}: the shared provider is the authority`);
++ // live.creative names a stronger model of the same shared provider (AI_CREATIVE_MODEL).
++ if (rr.key === 'live.creative') assert.match(rr.constraints.authority, /^shared:/, 'live.creative: the shared provider is the authority');
++ else assert.strictEqual(rr.constraints.authority, authority, `${rr.key}: the shared provider is the authority`);
}
assert.ok(checked >= 5, `seeded text pools checked: ${checked}`);
// The planner under those constraints: the paid shared offer beats the free local one while it is up, and