134 lines
3.6 KiB
TypeScript
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: 
|
|
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;
|
|
}
|
|
}
|