const API_URL = 'https://openrouter.ai/api/v1/chat/completions'; function safeJsonStringify(value) { try { return JSON.stringify(value); } catch { return String(value); } } function extractApiErrorDetails(payload) { if (!payload) return ''; const err = payload.error || payload; const parts = []; if (typeof err === 'string') parts.push(err); if (err?.message && typeof err.message === 'string') parts.push(err.message); if (err?.code && typeof err.code === 'string') parts.push(`code: ${err.code}`); if (err?.type && typeof err.type === 'string') parts.push(`type: ${err.type}`); if (err?.metadata) { if (err.metadata.provider_name) parts.push(`provider: ${err.metadata.provider_name}`); if (typeof err.metadata.raw === 'string') parts.push(err.metadata.raw); if (err.metadata.reason) parts.push(err.metadata.reason); } if (err?.details) { if (typeof err.details === 'string') parts.push(err.details); else parts.push(safeJsonStringify(err.details)); } const deduped = []; const seen = new Set(); for (const p of parts.map((x) => (x || '').trim())) { if (!p || seen.has(p)) continue; seen.add(p); deduped.push(p); } return deduped.join(' | '); } async function parseErrorResponse(res) { const raw = await res.text(); let json = null; if (raw) { try { json = JSON.parse(raw); } catch {} } let detail = extractApiErrorDetails(json); if (!detail && raw) detail = raw.trim(); if (!detail) detail = 'Unknown API error'; const statusPart = `HTTP ${res.status}${res.statusText ? ` ${res.statusText}` : ''}`; return `${statusPart} — ${detail}`; } function normalizeImageUrl(value) { if (typeof value !== 'string') return ''; if ( value.startsWith('data:') || value.startsWith('http://') || value.startsWith('https://') ) return value; return `data:image/png;base64,${value}`; } function extractImage(choice) { let src = null; const images = choice?.message?.images; if (Array.isArray(images) && images.length) { const img = images[0]; src = img?.image_url?.url || img?.url || img; } if (!src && Array.isArray(choice?.message?.content)) { for (const part of choice.message.content) { if (part?.type === 'image_url' && part?.image_url?.url) { src = part.image_url.url; break; } } } return normalizeImageUrl(src); } export async function generateFrame({ model, messages, imageSize, aspectRatio, apiKey }) { if (!apiKey) throw new Error('Missing API key.'); const isGemini = model.startsWith('google/'); const body = { model, messages, modalities: ['image'], image_config: { aspect_ratio: aspectRatio, image_size: imageSize } }; if (!isGemini && imageSize === '0.5K') body.image_config.image_size = '1K'; const res = await fetch(API_URL, { method: 'POST', headers: { Authorization: `Bearer ${apiKey}`, 'Content-Type': 'application/json', 'HTTP-Referer': 'https://vibegif.lol', 'X-Title': 'vibegif.lol' }, body: JSON.stringify(body) }); if (!res.ok) throw new Error(await parseErrorResponse(res)); const data = await res.json(); const choice = data?.choices?.[0]; if (!choice) throw new Error('No response from model'); const imageUrl = extractImage(choice); if (!imageUrl) throw new Error('No image in response. Model may have refused or returned text only.'); return { base64: imageUrl, assistantMsg: { role: 'assistant', content: [{ type: 'image_url', image_url: { url: imageUrl } }] } }; }