LLM song-tweak: AI Gateway via Vercel AI SDK, budgeted tune flow
Add a ✨ tune button that lets an LLM inspect the proposed visualization and tweak the proc-song knobs so the next generation looks better. Edits only the 7 songbank params (bpm, brightness, noise, noiseColour, dynamics, transition, arrangement) — never the Look — then re-synthesizes and re-generates locally for preview. - ai + @ai-sdk/openai + zod (library per plan) against the Vercel AI Gateway (OpenAI-compatible, configurable via VITE_AI_GATEWAY_URL/MODEL/API_KEY, localStorage override, .env.example). - Structured output (TweakResultSchema) + clampEdits enforces ranges; prompt ≤ ~2.5k tokens, max 2-3 knobs ±0.15 / bpm ±8. - Budget caps: 3/load + 10/session (sessionStorage), checked before network, NO_EDITS refunded, offline/auth/rate-limit mapped. - Prompt split into src/llm/spec.js (pure, node-testable) + llmTweak.js (gateway + digest) to keep `node --test` off the Vite JSON path. - UI: ⚙️ gateway/model/key dialog (localStorage flow-state:llm), confirm(prompt) with hint, diff modal with Apply/Dismiss; Apply synthesizes → FeatureTrack → generateLook → Show.useTrack. - Tests: 7 new LLM cases (clamp, schema, prompt, budget), 18/18 pass. Build + lint:scenes green. Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,87 @@
|
||||
// LLM gateway config — localStorage overrides > VITE env > defaults.
|
||||
//
|
||||
// The app is a static Vite site with no server; the gateway credential lives
|
||||
// in the browser. Never commit a key — paste it in the panel (→ localStorage)
|
||||
// or in an untracked .env.local.
|
||||
|
||||
const STORAGE_KEY = 'flow-state:llm';
|
||||
|
||||
const DEFAULTS = {
|
||||
gatewayUrl: 'https://ai-gateway.vercel.sh/v1',
|
||||
model: 'openai/gpt-4o-mini',
|
||||
apiKey: '',
|
||||
};
|
||||
|
||||
function readEnv() {
|
||||
try {
|
||||
const env = import.meta.env || {};
|
||||
return {
|
||||
gatewayUrl: env.VITE_AI_GATEWAY_URL || null,
|
||||
model: env.VITE_AI_MODEL || null,
|
||||
apiKey: env.VITE_AI_API_KEY || null,
|
||||
};
|
||||
} catch {
|
||||
return { gatewayUrl: null, model: null, apiKey: null };
|
||||
}
|
||||
}
|
||||
|
||||
function readStorage() {
|
||||
try {
|
||||
const raw = localStorage.getItem(STORAGE_KEY);
|
||||
if (!raw) return {};
|
||||
const parsed = JSON.parse(raw);
|
||||
return parsed && typeof parsed === 'object' ? parsed : {};
|
||||
} catch {
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
/** Resolved config — localStorage wins, then env, then defaults. */
|
||||
export function getLlmConfig() {
|
||||
const env = readEnv();
|
||||
const stored = readStorage();
|
||||
const gatewayUrl = stored.gatewayUrl || env.gatewayUrl || DEFAULTS.gatewayUrl;
|
||||
const model = stored.model || env.model || DEFAULTS.model;
|
||||
const apiKey = stored.apiKey != null ? stored.apiKey : (env.apiKey != null ? env.apiKey : DEFAULTS.apiKey);
|
||||
const key = typeof apiKey === 'string' ? apiKey.trim() : '';
|
||||
return {
|
||||
gatewayUrl: String(gatewayUrl || DEFAULTS.gatewayUrl).trim() || DEFAULTS.gatewayUrl,
|
||||
model: String(model || DEFAULTS.model).trim() || DEFAULTS.model,
|
||||
apiKey: key,
|
||||
isConfigured: key.length > 0,
|
||||
defaults: { ...DEFAULTS },
|
||||
env: { ...env },
|
||||
stored: { ...stored },
|
||||
};
|
||||
}
|
||||
|
||||
/** Merge patch into localStorage. Pass null/'' to clear a field. */
|
||||
export function setLlmConfig(patch) {
|
||||
const current = readStorage();
|
||||
const next = { ...current };
|
||||
for (const k of ['gatewayUrl', 'model', 'apiKey']) {
|
||||
if (patch && Object.prototype.hasOwnProperty.call(patch, k)) {
|
||||
const v = patch[k];
|
||||
if (v == null || v === '') {
|
||||
delete next[k];
|
||||
} else {
|
||||
next[k] = String(v).trim();
|
||||
}
|
||||
}
|
||||
}
|
||||
try {
|
||||
if (!Object.keys(next).length) localStorage.removeItem(STORAGE_KEY);
|
||||
else localStorage.setItem(STORAGE_KEY, JSON.stringify(next));
|
||||
} catch {
|
||||
// storage full / blocked — ignore, caller sees isConfigured false
|
||||
}
|
||||
return getLlmConfig();
|
||||
}
|
||||
|
||||
export function clearLlmConfig() {
|
||||
try { localStorage.removeItem(STORAGE_KEY); } catch { /* ignore */ }
|
||||
return getLlmConfig();
|
||||
}
|
||||
|
||||
export const LLM_STORAGE_KEY = STORAGE_KEY;
|
||||
export const LLM_DEFAULTS = DEFAULTS;
|
||||
Reference in New Issue
Block a user