Files
vendoo/lib/ai-openrouter.mjs
T
Masterluke77andGitHub 6f48827f4d Vendoo 1.41.2 – Git Ignore Boundary & Module Tracking Hotfix (#18)
* docs: prepare Vendoo 1.41.2 git-ignore boundary hotfix

* fix: track native source modules with root-anchored runtime ignores
2026-07-09 17:09:00 +02:00

153 lines
5.3 KiB
JavaScript

const OPENROUTER_BASE = 'https://openrouter.ai/api/v1';
export const MODELS = [
{ id: 'google/gemini-3.1-flash-lite-image-20260630', name: 'Gemini 3.1 Flash Lite (~0)', free: false, priority: 1 },
{ id: 'google/gemini-3.1-flash-image-20260528', name: 'Gemini 3.1 Flash Image (~0)', free: false, priority: 2 },
{ id: 'google/gemini-3-pro-image-20260528', name: 'Gemini 3 Pro Image', free: false, priority: 3 },
{ id: 'nvidia/nemotron-3-nano-omni-30b-a3b-reasoning:free', name: 'Nemotron 3 Nano Omni (free)', free: true, priority: 4 },
{ id: 'google/gemma-3-27b-it:free', name: 'Gemma 3 27B (free)', free: true, priority: 5 },
{ id: 'meta-llama/llama-4-scout:free', name: 'Llama 4 Scout (free)', free: true, priority: 6 },
{ id: 'deepseek/deepseek-r1:free', name: 'DeepSeek R1 (free)', free: true, priority: 7 },
{ id: 'qwen/qwen3-235b-a22b:free', name: 'Qwen 3 235B (free)', free: true, priority: 8 },
];
function extractJSON(text) {
if (!text) return null;
// Try direct parse first
try { return JSON.parse(text); } catch {}
// Find outermost { ... }
let depth = 0, start = -1;
for (let i = 0; i < text.length; i++) {
if (text[i] === '{') { if (depth === 0) start = i; depth++; }
else if (text[i] === '}') { depth--; if (depth === 0 && start >= 0) {
try { return JSON.parse(text.substring(start, i + 1)); } catch {}
}}
}
// Try fixing common issues: trailing commas, unescaped newlines
const match = text.match(/\{[\s\S]*\}/);
if (match) {
let cleaned = match[0]
.replace(/,\s*([}\]])/g, '$1') // trailing commas
.replace(/[\x00-\x1f]/g, m => // control chars in strings
m === '\n' ? '\\n' : m === '\r' ? '\\r' : m === '\t' ? '\\t' : ''
);
try { return JSON.parse(cleaned); } catch {}
}
return null;
}
async function callModel(modelId, images, systemPrompt) {
const apiKey = process.env.OPENROUTER_API_KEY;
if (!apiKey) throw new Error('OPENROUTER_API_KEY nicht gesetzt');
const content = [];
for (const img of images) {
content.push({
type: 'image_url',
image_url: { url: `data:${img.mediaType};base64,${img.base64}` },
});
}
content.push({
type: 'text',
text: 'Analysiere die Fotos und erstelle das Listing gemäß den Anweisungen. Antworte ausschließlich mit validem JSON.',
});
const res = await fetch(`${OPENROUTER_BASE}/chat/completions`, {
method: 'POST',
headers: {
'Authorization': `Bearer ${apiKey}`,
'Content-Type': 'application/json',
'HTTP-Referer': 'http://localhost:8124',
'X-Title': 'Vendoo',
},
body: JSON.stringify({
model: modelId,
max_tokens: 1500,
messages: [
{ role: 'system', content: systemPrompt },
{ role: 'user', content },
],
}),
});
if (!res.ok) {
const err = await res.json().catch(() => ({}));
throw new Error(err.error?.message || `HTTP ${res.status}`);
}
const data = await res.json();
const text = data.choices?.[0]?.message?.content;
if (!text) throw new Error('Leere Antwort');
const parsed = extractJSON(text);
if (!parsed) throw new Error('Kein valides JSON in Antwort');
return parsed;
}
async function callTextModel(modelId, systemPrompt, userPrompt) {
const apiKey = process.env.OPENROUTER_API_KEY;
if (!apiKey) throw new Error('OPENROUTER_API_KEY nicht gesetzt');
const res = await fetch(`${OPENROUTER_BASE}/chat/completions`, {
method: 'POST',
headers: {
'Authorization': `Bearer ${apiKey}`,
'Content-Type': 'application/json',
'HTTP-Referer': 'http://localhost:8124',
'X-Title': 'Vendoo',
},
body: JSON.stringify({
model: modelId,
max_tokens: 1400,
messages: [
{ role: 'system', content: String(systemPrompt || '') },
{ role: 'user', content: String(userPrompt || '') },
],
}),
});
if (!res.ok) {
const err = await res.json().catch(() => ({}));
throw new Error(err.error?.message || `HTTP ${res.status}`);
}
const data = await res.json();
return String(data.choices?.[0]?.message?.content || '').trim();
}
export async function generateText(systemPrompt, userPrompt, model) {
if (model) return callTextModel(model, systemPrompt, userPrompt);
const errors = [];
for (const entry of [...MODELS].sort((a, b) => a.priority - b.priority)) {
try { return await callTextModel(entry.id, systemPrompt, userPrompt); }
catch (error) {
errors.push(`${entry.name}: ${error.message}`);
if (/API_KEY|nicht gesetzt/i.test(error.message)) break;
}
}
throw new Error(`Kein OpenRouter-Modell konnte den Prompt verbessern: ${errors.join(' | ')}`);
}
export async function generate(images, systemPrompt, model) {
if (model) {
return callModel(model, images, systemPrompt);
}
const ordered = [...MODELS].sort((a, b) => a.priority - b.priority);
const errors = [];
for (const m of ordered) {
try {
console.log(`OpenRouter: Versuche ${m.name}...`);
const result = await callModel(m.id, images, systemPrompt);
console.log(`OpenRouter: Erfolg mit ${m.name}`);
return result;
} catch (err) {
const msg = err.message;
console.log(`OpenRouter: ${m.name} fehlgeschlagen: ${msg}`);
errors.push(`${m.name}: ${msg}`);
if (msg.includes('API_KEY') || msg.includes('nicht gesetzt')) break;
}
}
throw new Error(`Alle Modelle fehlgeschlagen:\n${errors.join('\n')}`);
}