mirror of
https://github.com/vibegif/vibegif.lol.git
synced 2026-04-07 18:22:13 +00:00
Refactor: Extract OpenRouter client
This commit is contained in:
135
src/services/openrouter.js
Normal file
135
src/services/openrouter.js
Normal file
@@ -0,0 +1,135 @@
|
||||
const API_URL = 'https://openrouter.ai/api/v1/chat/completions';
|
||||
|
||||
function safeJsonStringify(value) {
|
||||
try {
|
||||
return JSON.stringify(value);
|
||||
} catch {
|
||||
return String(value);
|
||||
}
|
||||
}
|
||||
|
||||
function extractApiErrorDetails(payload) {
|
||||
if (!payload) return '';
|
||||
const err = payload.error || payload;
|
||||
const parts = [];
|
||||
|
||||
if (typeof err === 'string') parts.push(err);
|
||||
if (err?.message && typeof err.message === 'string') parts.push(err.message);
|
||||
if (err?.code && typeof err.code === 'string') parts.push(`code: ${err.code}`);
|
||||
if (err?.type && typeof err.type === 'string') parts.push(`type: ${err.type}`);
|
||||
|
||||
if (err?.metadata) {
|
||||
if (err.metadata.provider_name) parts.push(`provider: ${err.metadata.provider_name}`);
|
||||
if (typeof err.metadata.raw === 'string') parts.push(err.metadata.raw);
|
||||
if (err.metadata.reason) parts.push(err.metadata.reason);
|
||||
}
|
||||
|
||||
if (err?.details) {
|
||||
if (typeof err.details === 'string') parts.push(err.details);
|
||||
else parts.push(safeJsonStringify(err.details));
|
||||
}
|
||||
|
||||
const deduped = [];
|
||||
const seen = new Set();
|
||||
for (const p of parts.map((x) => (x || '').trim())) {
|
||||
if (!p || seen.has(p)) continue;
|
||||
seen.add(p);
|
||||
deduped.push(p);
|
||||
}
|
||||
|
||||
return deduped.join(' | ');
|
||||
}
|
||||
|
||||
async function parseErrorResponse(res) {
|
||||
const raw = await res.text();
|
||||
let json = null;
|
||||
|
||||
if (raw) {
|
||||
try {
|
||||
json = JSON.parse(raw);
|
||||
} catch {}
|
||||
}
|
||||
|
||||
let detail = extractApiErrorDetails(json);
|
||||
if (!detail && raw) detail = raw.trim();
|
||||
if (!detail) detail = 'Unknown API error';
|
||||
|
||||
const statusPart = `HTTP ${res.status}${res.statusText ? ` ${res.statusText}` : ''}`;
|
||||
return `${statusPart} — ${detail}`;
|
||||
}
|
||||
|
||||
function normalizeImageUrl(value) {
|
||||
if (typeof value !== 'string') return '';
|
||||
if (
|
||||
value.startsWith('data:') ||
|
||||
value.startsWith('http://') ||
|
||||
value.startsWith('https://')
|
||||
) return value;
|
||||
return `data:image/png;base64,${value}`;
|
||||
}
|
||||
|
||||
function extractImage(choice) {
|
||||
let src = null;
|
||||
|
||||
const images = choice?.message?.images;
|
||||
if (Array.isArray(images) && images.length) {
|
||||
const img = images[0];
|
||||
src = img?.image_url?.url || img?.url || img;
|
||||
}
|
||||
|
||||
if (!src && Array.isArray(choice?.message?.content)) {
|
||||
for (const part of choice.message.content) {
|
||||
if (part?.type === 'image_url' && part?.image_url?.url) {
|
||||
src = part.image_url.url;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return normalizeImageUrl(src);
|
||||
}
|
||||
|
||||
export async function generateFrame({ model, messages, imageSize, aspectRatio, apiKey }) {
|
||||
if (!apiKey) throw new Error('Missing API key.');
|
||||
|
||||
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(API_URL, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Authorization: `Bearer ${apiKey}`,
|
||||
'Content-Type': 'application/json',
|
||||
'HTTP-Referer': 'https://vibegif.lol',
|
||||
'X-Title': 'vibegif.lol'
|
||||
},
|
||||
body: JSON.stringify(body)
|
||||
});
|
||||
|
||||
if (!res.ok) throw new Error(await parseErrorResponse(res));
|
||||
|
||||
const data = await res.json();
|
||||
const choice = data?.choices?.[0];
|
||||
if (!choice) throw new Error('No response from model');
|
||||
|
||||
const imageUrl = extractImage(choice);
|
||||
if (!imageUrl) throw new Error('No image in response. Model may have refused or returned text only.');
|
||||
|
||||
return {
|
||||
base64: imageUrl,
|
||||
assistantMsg: {
|
||||
role: 'assistant',
|
||||
content: [{ type: 'image_url', image_url: { url: imageUrl } }]
|
||||
}
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user