// LLM gateway config — localStorage for the secret, env only for non-secrets. // // The app is a static Vite site with no server. The API key is a secret and // must never be baked into the build — it lives only in the browser's // localStorage (→ ⚙️ dialog). Gateway URL and model are non-secret and may // come from VITE env as defaults with a localStorage override; the key never // does. const STORAGE_KEY = 'flow-state:llm'; const DEFAULTS = { gatewayUrl: 'https://ai-gateway.vercel.sh/v1', model: 'meta/muse-spark-1.3-contributor', apiKey: '', }; function readEnv() { try { const env = import.meta.env || {}; return { gatewayUrl: env.VITE_AI_GATEWAY_URL || null, model: env.VITE_AI_MODEL || null, // apiKey intentionally NOT read from env — see module comment. }; } catch { return { gatewayUrl: null, model: 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 — key from localStorage only (never env); URL/model from localStorage > env > 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 key = typeof stored.apiKey === 'string' ? stored.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;