From 390a1c969331ae31ad54ef3666b2bb244f3c17e4 Mon Sep 17 00:00:00 2001 From: multipleof4 Date: Fri, 20 Mar 2026 22:07:41 -0700 Subject: [PATCH] Feat: OpenRouter image generation API client --- api.js | 88 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 88 insertions(+) create mode 100644 api.js diff --git a/api.js b/api.js new file mode 100644 index 0000000..ff4fefe --- /dev/null +++ b/api.js @@ -0,0 +1,88 @@ +import { getApiKey } from './storage.js'; + +const BASE = 'https://openrouter.ai/api/v1/chat/completions'; +const MASTER_PROMPT = 'minimal black and white line doodle, single stroke, white background, kawaii style'; + +/** + * Generates a single image frame via OpenRouter. + * @param {Object} opts + * @param {string} opts.model + * @param {Array} opts.messages - chat history messages + * @param {string} opts.imageSize - "1K" or "0.5K" + * @param {string} opts.aspectRatio - e.g. "1:1" + * @returns {Promise<{base64: string, assistantMsg: Object}>} + */ +export async function generateFrame({ model, messages, imageSize, aspectRatio }) { + 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(BASE, { + method: 'POST', + headers: { + 'Authorization': `Bearer ${getApiKey()}`, + 'Content-Type': 'application/json', + 'HTTP-Referer': 'https://vibegif.lol', + 'X-Title': 'vibegif.lol', + }, + body: JSON.stringify(body), + }); + + if (!res.ok) { + const err = await res.json().catch(() => ({})); + throw new Error(err?.error?.message || `API error ${res.status}`); + } + + const data = await res.json(); + const choice = data.choices?.[0]; + if (!choice) throw new Error('No response from model'); + + const images = choice.message?.images; + if (!images?.length) throw new Error('No image in response. The model may have refused or returned text only.'); + + const url = images[0].image_url?.url || images[0].url; + if (!url) throw new Error('Could not parse image from response'); + + const base64 = url.startsWith('data:') ? url : `data:image/png;base64,${url}`; + + const assistantMsg = { + role: 'assistant', + content: [ + { type: 'image_url', image_url: { url: base64 } } + ] + }; + + return { base64, assistantMsg }; +} + +/** + * Build the first user message (frame 1). + */ +export function buildFirstMessage(userPrompt) { + return { + role: 'user', + content: `${MASTER_PROMPT}, ${userPrompt}` + }; +} + +/** + * Build follow-up user message for subsequent frames. + */ +export function buildNextMessage(frameIndex, frameCount) { + return { + role: 'user', + content: `imagine we are trying to create a ${frameCount} frame gif. generate the next meaningful frame (frame ${frameIndex} of ${frameCount})` + }; +}