* docs: prepare Vendoo 1.41.2 git-ignore boundary hotfix * fix: track native source modules with root-anchored runtime ignores
153 lines
5.3 KiB
JavaScript
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')}`);
|
|
}
|