export interface OpenRouterGenerateOptions { model: string; prompt: string; negativePrompt?: string; imageBase64: string; mimeType: string; timeoutMs: number; } export interface OpenRouterResult { imageUrl: string; provider: string; model: string; } export async function generateWithOpenRouter( options: OpenRouterGenerateOptions ): Promise { const apiKey = process.env.OPENROUTER_API_KEY; if (!apiKey) { throw new Error("OPENROUTER_API_KEY is not configured in environment variables"); } const cleanBase64 = options.imageBase64.startsWith("data:") ? options.imageBase64 : `data:${options.mimeType || "image/jpeg"};base64,${options.imageBase64}`; const controller = new AbortController(); const timeoutId = setTimeout(() => controller.abort(), options.timeoutMs); try { const response = await fetch("https://openrouter.ai/api/v1/chat/completions", { method: "POST", headers: { Authorization: `Bearer ${apiKey}`, "Content-Type": "application/json", "HTTP-Referer": "https://photobooth.local", "X-Title": "AI PhotoBooth", }, body: JSON.stringify({ model: options.model, messages: [ { role: "user", content: [ { type: "text", text: `${options.prompt}\n\nNegative prompt: ${options.negativePrompt || "none"}`, }, { type: "image_url", image_url: { url: cleanBase64, }, }, ], }, ], }), signal: controller.signal, }); clearTimeout(timeoutId); if (!response.ok) { const errorText = await response.text(); throw new Error(`OpenRouter API error [${response.status}]: ${errorText}`); } const data = await response.json(); const choice = data.choices?.[0]; const message = choice?.message; // 1. Check OpenRouter image output attachments (message.images: [{ image_url: { url: ... } }]) if (Array.isArray(message?.images) && message.images.length > 0) { for (const img of message.images) { const url = img?.image_url?.url || img?.url; if (url) { return { imageUrl: url, provider: "openrouter", model: options.model, }; } } } const content = message?.content; if (!content) { throw new Error("OpenRouter returned an empty completion"); } // Check if content is or contains an image URL or base64 data const base64Match = content.match(/data:image\/[a-zA-Z]+;base64,[A-Za-z0-9+/=]+/); if (base64Match) { return { imageUrl: base64Match[0], provider: "openrouter", model: options.model, }; } const urlMatch = content.match(/https?:\/\/[^\s"']+\.(?:png|jpg|jpeg|webp)/i); if (urlMatch) { return { imageUrl: urlMatch[0], provider: "openrouter", model: options.model, }; } // Some models return markdown image: ![alt](url) const mdMatch = content.match(/!\[.*?\]\((https?:\/\/.*?)\)/); if (mdMatch) { return { imageUrl: mdMatch[1], provider: "openrouter", model: options.model, }; } throw new Error( `OpenRouter response did not contain an image. Output received: ${content.slice(0, 120)}...` ); } catch (error: any) { clearTimeout(timeoutId); if (error.name === "AbortError") { throw new Error(`OpenRouter request timed out after ${options.timeoutMs}ms`); } throw error; } }