From 80df783aa39f45a6e791265f113ea693b92e77ac Mon Sep 17 00:00:00 2001 From: multipleof4 Date: Fri, 20 Mar 2026 21:14:25 -0700 Subject: [PATCH] Feat: OpenRouter image-only generation helper --- assets/js/openrouter.js | 121 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 121 insertions(+) create mode 100644 assets/js/openrouter.js diff --git a/assets/js/openrouter.js b/assets/js/openrouter.js new file mode 100644 index 0000000..55e3c40 --- /dev/null +++ b/assets/js/openrouter.js @@ -0,0 +1,121 @@ +const OPENROUTER_URL = "https://openrouter.ai/api/v1/chat/completions"; + +function toDataUrlMaybe(item) { + if (!item) return null; + if (typeof item === "string") { + if (item.startsWith("http://") || item.startsWith("https://") || item.startsWith("data:image/")) return item; + return `data:image/png;base64,${item}`; + } + if (item.url) return item.url; + if (item.image_url?.url) return item.image_url.url; + if (item.b64_json) return `data:image/png;base64,${item.b64_json}`; + if (item.data) return `data:image/png;base64,${item.data}`; + return null; +} + +function extractImageFromResponse(json) { + const c = json?.choices?.[0]; + if (!c) return null; + + const msg = c.message || {}; + const content = msg.content; + + if (Array.isArray(content)) { + for (const part of content) { + if (part?.type === "image_url") { + const u = part?.image_url?.url || part?.url; + if (u) return u; + } + if (part?.type === "output_image" && part?.image_url?.url) return part.image_url.url; + if (part?.type === "text" && typeof part.text === "string") { + const m = part.text.match(/https?:\/\/\S+\.(?:png|jpg|jpeg|webp|gif)/i); + if (m) return m[0]; + } + } + } + + const fallbackPools = [ + json?.images, + json?.data, + msg?.images, + c?.images + ]; + + for (const pool of fallbackPools) { + if (Array.isArray(pool) && pool.length) { + const x = toDataUrlMaybe(pool[0]); + if (x) return x; + } + } + + if (typeof content === "string") { + const m = content.match(/https?:\/\/\S+\.(?:png|jpg|jpeg|webp|gif)/i); + if (m) return m[0]; + } + + return null; +} + +export async function generateImageFrame({ + apiKey, + model, + textPrompt, + previousFrames = [], + imageSize = "1K", + aspectRatio = "1:1" +}) { + const messages = []; + + // rolling window of 2 frames only + const recent = previousFrames.slice(-2); + + if (recent.length) { + for (const frame of recent) { + messages.push({ + role: "user", + content: [ + { type: "image_url", image_url: { url: frame } } + ] + }); + } + } + + messages.push({ + role: "user", + content: [ + { type: "text", text: textPrompt } + ] + }); + + const body = { + model, + modalities: ["image"], + messages, + image_config: { + image_size: imageSize, + aspect_ratio: aspectRatio + } + }; + + const res = await fetch(OPENROUTER_URL, { + method: "POST", + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${apiKey}`, + "HTTP-Referer": window.location.origin, + "X-Title": "vibegif.lol" + }, + body: JSON.stringify(body) + }); + + const json = await res.json().catch(() => ({})); + + if (!res.ok) { + const msg = json?.error?.message || `OpenRouter error (${res.status})`; + throw new Error(msg); + } + + const image = extractImageFromResponse(json); + if (!image) throw new Error("No image found in model response."); + return image; +}