Files

134 lines
3.6 KiB
TypeScript

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<OpenRouterResult> {
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;
}
}