refactor: Consolidate all AI calls to use OpenRouter provider switch

- bulk-scan.ts: Now uses OpenRouter for batch analysis
- scan-label.ts: Now uses OpenRouter with Gemini fallback
- analyze-bottle.ts: Now uses OpenRouter with Gemini fallback
- All AI calls now respect AI_PROVIDER env variable
- Uses Nebius/FP8 provider preferences consistently
- Unified logging: [FunctionName] Using provider: openrouter/gemini
This commit is contained in:
2025-12-26 21:21:56 +01:00
parent e978499b54
commit 9c5f538efb
4 changed files with 362 additions and 191 deletions

View File

@@ -6,6 +6,7 @@ import { createClient } from '@/lib/supabase/server';
import { createHash } from 'crypto'; import { createHash } from 'crypto';
import { trackApiUsage } from '@/services/track-api-usage'; import { trackApiUsage } from '@/services/track-api-usage';
import { checkCreditBalance, deductCredits } from '@/services/credit-service'; import { checkCreditBalance, deductCredits } from '@/services/credit-service';
import { getAIProvider, getOpenRouterClient, OPENROUTER_PROVIDER_PREFERENCES } from '@/lib/openrouter';
import sharp from 'sharp'; import sharp from 'sharp';
// Native Schema Definition for Gemini API // Native Schema Definition for Gemini API
@@ -30,10 +31,30 @@ const metadataSchema = {
required: ["name", "is_whisky", "confidence"], required: ["name", "is_whisky", "confidence"],
}; };
const SCAN_PROMPT = `Extract whisky label metadata. Return JSON with:
- name: Full product name
- distillery: Distillery name
- bottler: Independent bottler if applicable
- category: e.g. "Single Malt", "Bourbon"
- abv: Alcohol percentage
- age: Age statement in years
- vintage: Vintage year
- distilled_at: Distillation date
- bottled_at: Bottling date
- batch_info: Batch or cask info
- is_whisky: boolean
- confidence: 0-1`;
export async function scanLabel(input: any): Promise<AnalysisResponse> { export async function scanLabel(input: any): Promise<AnalysisResponse> {
if (!process.env.GEMINI_API_KEY) { const provider = getAIProvider();
// Check API key based on provider
if (provider === 'gemini' && !process.env.GEMINI_API_KEY) {
return { success: false, error: 'GEMINI_API_KEY is not configured.' }; return { success: false, error: 'GEMINI_API_KEY is not configured.' };
} }
if (provider === 'openrouter' && !process.env.OPENROUTER_API_KEY) {
return { success: false, error: 'OPENROUTER_API_KEY is not configured.' };
}
let supabase; let supabase;
try { try {
@@ -101,7 +122,7 @@ export async function scanLabel(input: any): Promise<AnalysisResponse> {
}; };
} }
// Step 3: AI-Specific Image Optimization (Grayscale, Normalize, Resize) // Step 3: AI-Specific Image Optimization
const startOptimization = performance.now(); const startOptimization = performance.now();
const optimizedBuffer = await sharp(buffer) const optimizedBuffer = await sharp(buffer)
.resize(1024, 1024, { fit: 'inside', withoutEnlargement: true }) .resize(1024, 1024, { fit: 'inside', withoutEnlargement: true })
@@ -117,14 +138,79 @@ export async function scanLabel(input: any): Promise<AnalysisResponse> {
const uploadSize = optimizedBuffer.length; const uploadSize = optimizedBuffer.length;
const endEncoding = performance.now(); const endEncoding = performance.now();
// Step 5: Model Initialization & Step 6: API Call // Step 5: AI Analysis
const startAiTotal = performance.now(); const startAiTotal = performance.now();
let jsonData; let jsonData;
let validatedData; let validatedData;
try { try {
console.log(`[ScanLabel] Using provider: ${provider}`);
if (provider === 'openrouter') {
// OpenRouter path
const client = getOpenRouterClient();
const startApi = performance.now();
const response = await client.chat.completions.create({
model: 'google/gemma-3-27b-it',
messages: [{
role: 'user',
content: [
{ type: 'image_url', image_url: { url: `data:${mimeType};base64,${base64Data}` } },
{ type: 'text', text: SCAN_PROMPT + '\n\nRespond ONLY with valid JSON.' },
],
}],
temperature: 0.1,
max_tokens: 1024,
// @ts-ignore
provider: OPENROUTER_PROVIDER_PREFERENCES,
});
const endApi = performance.now();
const content = response.choices[0]?.message?.content || '{}';
const startParse = performance.now();
let jsonStr = content;
const jsonMatch = content.match(/```(?:json)?\s*([\s\S]*?)```/);
if (jsonMatch) jsonStr = jsonMatch[1].trim();
jsonData = JSON.parse(jsonStr);
const endParse = performance.now();
const startValidation = performance.now();
validatedData = BottleMetadataSchema.parse(jsonData);
const endValidation = performance.now();
await supabase.from('vision_cache').insert({ hash: imageHash, result: validatedData });
await trackApiUsage({
userId: userId,
apiType: 'gemini_ai',
endpoint: 'scanLabel_openrouter',
success: true
});
await deductCredits(userId, 'gemini_ai', 'Bottle OCR scan (OpenRouter)');
return {
success: true,
data: validatedData,
perf: {
imagePrep: endImagePrep - startImagePrep,
optimization: endOptimization - startOptimization,
cacheCheck: endCacheCheck - startCacheCheck,
encoding: endEncoding - startEncoding,
apiCall: endApi - startApi,
parsing: endParse - startParse,
validation: endValidation - startValidation,
total: performance.now() - perfTotal,
cacheHit: false
},
raw: jsonData
} as any;
} else {
// Gemini path
const startModelInit = performance.now(); const startModelInit = performance.now();
const genAI = new GoogleGenerativeAI(process.env.GEMINI_API_KEY); const genAI = new GoogleGenerativeAI(process.env.GEMINI_API_KEY!);
const model = genAI.getGenerativeModel({ const model = genAI.getGenerativeModel({
model: 'gemini-2.5-flash', model: 'gemini-2.5-flash',
generationConfig: { generationConfig: {
@@ -141,11 +227,10 @@ export async function scanLabel(input: any): Promise<AnalysisResponse> {
}); });
const endModelInit = performance.now(); const endModelInit = performance.now();
const instruction = "Extract whisky label metadata.";
const startApi = performance.now(); const startApi = performance.now();
const result = await model.generateContent([ const result = await model.generateContent([
{ inlineData: { data: base64Data, mimeType: mimeType } }, { inlineData: { data: base64Data, mimeType: mimeType } },
{ text: instruction }, { text: 'Extract whisky label metadata.' },
]); ]);
const endApi = performance.now(); const endApi = performance.now();
@@ -157,18 +242,16 @@ export async function scanLabel(input: any): Promise<AnalysisResponse> {
validatedData = BottleMetadataSchema.parse(jsonData); validatedData = BottleMetadataSchema.parse(jsonData);
const endValidation = performance.now(); const endValidation = performance.now();
// Cache record
await supabase.from('vision_cache').insert({ hash: imageHash, result: validatedData }); await supabase.from('vision_cache').insert({ hash: imageHash, result: validatedData });
await trackApiUsage({ await trackApiUsage({
userId: userId, userId: userId,
apiType: 'gemini_ai', apiType: 'gemini_ai',
endpoint: 'scanLabel', endpoint: 'scanLabel_gemini',
success: true success: true
}); });
await deductCredits(userId, 'gemini_ai', 'Bottle OCR scan'); await deductCredits(userId, 'gemini_ai', 'Bottle OCR scan (Gemini)');
const totalTime = performance.now() - perfTotal;
return { return {
success: true, success: true,
data: validatedData, data: validatedData,
@@ -181,30 +264,29 @@ export async function scanLabel(input: any): Promise<AnalysisResponse> {
apiCall: endApi - startApi, apiCall: endApi - startApi,
parsing: endParse - startParse, parsing: endParse - startParse,
validation: endValidation - startValidation, validation: endValidation - startValidation,
total: totalTime, total: performance.now() - perfTotal,
cacheHit: false cacheHit: false
}, },
raw: jsonData raw: jsonData
} as any; } as any;
}
} catch (aiError: any) { } catch (aiError: any) {
console.warn('[ScanLabel] AI Analysis failed, providing fallback path:', aiError.message); console.warn(`[ScanLabel] ${provider} failed:`, aiError.message);
// Track failure
await trackApiUsage({ await trackApiUsage({
userId: userId, userId: userId,
apiType: 'gemini_ai', apiType: 'gemini_ai',
endpoint: 'scanLabel', endpoint: `scanLabel_${provider}`,
success: false, success: false,
errorMessage: aiError.message errorMessage: aiError.message
}); });
// Return a specific structure that ScanAndTasteFlow can use to fallback to placeholder
return { return {
success: false, success: false,
isAiError: true, isAiError: true,
error: aiError.message, error: aiError.message,
imageHash: imageHash // Useful for local tracking imageHash: imageHash
} as any; } as any;
} }

View File

@@ -1,29 +1,33 @@
'use server'; 'use server';
import { geminiModel } from '@/lib/gemini'; import { GoogleGenerativeAI, HarmCategory, HarmBlockThreshold } from '@google/generative-ai';
import { getSystemPrompt } from '@/lib/ai-prompts'; import { getSystemPrompt } from '@/lib/ai-prompts';
import { BottleMetadataSchema, AnalysisResponse } from '@/types/whisky'; import { BottleMetadataSchema, AnalysisResponse } from '@/types/whisky';
import { createClient } from '@/lib/supabase/server'; import { createClient } from '@/lib/supabase/server';
import { createHash } from 'crypto'; import { createHash } from 'crypto';
import { trackApiUsage } from './track-api-usage'; import { trackApiUsage } from './track-api-usage';
import { checkCreditBalance, deductCredits } from './credit-service'; import { checkCreditBalance, deductCredits } from './credit-service';
import { getAIProvider, getOpenRouterClient, OPENROUTER_PROVIDER_PREFERENCES } from '@/lib/openrouter';
// WICHTIG: Wir akzeptieren jetzt FormData statt Strings
export async function analyzeBottle(input: any): Promise<AnalysisResponse> { export async function analyzeBottle(input: any): Promise<AnalysisResponse> {
if (!process.env.GEMINI_API_KEY) { const provider = getAIProvider();
// Check API key based on provider
if (provider === 'gemini' && !process.env.GEMINI_API_KEY) {
return { success: false, error: 'GEMINI_API_KEY is not configured.' }; return { success: false, error: 'GEMINI_API_KEY is not configured.' };
} }
if (provider === 'openrouter' && !process.env.OPENROUTER_API_KEY) {
return { success: false, error: 'OPENROUTER_API_KEY is not configured.' };
}
let supabase; let supabase;
try { try {
// Helper to get value from either FormData or POJO
const getValue = (obj: any, key: string): any => { const getValue = (obj: any, key: string): any => {
if (obj && typeof obj.get === 'function') return obj.get(key); if (obj && typeof obj.get === 'function') return obj.get(key);
if (obj && typeof obj[key] !== 'undefined') return obj[key]; if (obj && typeof obj[key] !== 'undefined') return obj[key];
return null; return null;
}; };
// 1. Daten extrahieren (leichtgewichtig für den N100)
const file = getValue(input, 'file') as File; const file = getValue(input, 'file') as File;
const tagsString = getValue(input, 'tags') as string; const tagsString = getValue(input, 'tags') as string;
const locale = getValue(input, 'locale') || 'de'; const locale = getValue(input, 'locale') || 'de';
@@ -32,10 +36,8 @@ export async function analyzeBottle(input: any): Promise<AnalysisResponse> {
return { success: false, error: 'Kein Bild empfangen.' }; return { success: false, error: 'Kein Bild empfangen.' };
} }
// Tags müssen manuell geparst werden, da FormData alles flach macht
const tags = tagsString ? (typeof tagsString === 'string' ? JSON.parse(tagsString) : tagsString) : []; const tags = tagsString ? (typeof tagsString === 'string' ? JSON.parse(tagsString) : tagsString) : [];
// 2. Auth & Credits (bleibt gleich)
supabase = await createClient(); supabase = await createClient();
const { data: { user } } = await supabase.auth.getUser(); const { data: { user } } = await supabase.auth.getUser();
@@ -53,13 +55,10 @@ export async function analyzeBottle(input: any): Promise<AnalysisResponse> {
}; };
} }
// 3. Datei in Buffer umwandeln (Schneller als String-Manipulation)
// Der N100 mag ArrayBuffer lieber als riesige Base64 Strings im JSON
const arrayBuffer = await file.arrayBuffer(); const arrayBuffer = await file.arrayBuffer();
const buffer = Buffer.from(arrayBuffer); const buffer = Buffer.from(arrayBuffer);
// 4. Hash für Cache erstellen (direkt vom Buffer -> sehr schnell)
const imageHash = createHash('sha256').update(buffer).digest('hex'); const imageHash = createHash('sha256').update(buffer).digest('hex');
// Cache Check // Cache Check
const { data: cachedResult } = await supabase const { data: cachedResult } = await supabase
.from('vision_cache') .from('vision_cache')
@@ -79,40 +78,44 @@ export async function analyzeBottle(input: any): Promise<AnalysisResponse> {
}; };
} }
// 5. Für Gemini vorbereiten
const base64Data = buffer.toString('base64'); const base64Data = buffer.toString('base64');
const mimeType = file.type || 'image/webp'; const mimeType = file.type || 'image/webp';
const uploadSize = buffer.length; const uploadSize = buffer.length;
const instruction = getSystemPrompt(tags.length > 0 ? tags.join(', ') : 'No tags available', locale); const instruction = getSystemPrompt(tags.length > 0 ? tags.join(', ') : 'No tags available', locale);
try { try {
// API Call console.log(`[AnalyzeBottle] Using provider: ${provider}`);
if (provider === 'openrouter') {
// OpenRouter path
const client = getOpenRouterClient();
const startApi = performance.now(); const startApi = performance.now();
const result = await geminiModel.generateContent([
{ const response = await client.chat.completions.create({
inlineData: { model: 'google/gemma-3-27b-it',
data: base64Data, messages: [{
mimeType: mimeType, role: 'user',
}, content: [
}, { type: 'image_url', image_url: { url: `data:${mimeType};base64,${base64Data}` } },
{ text: instruction }, { type: 'text', text: instruction + '\n\nRespond ONLY with valid JSON.' },
]); ],
}],
temperature: 0.1,
max_tokens: 1024,
// @ts-ignore
provider: OPENROUTER_PROVIDER_PREFERENCES,
});
const endApi = performance.now(); const endApi = performance.now();
const content = response.choices[0]?.message?.content || '{}';
const startParse = performance.now(); const startParse = performance.now();
const responseText = result.response.text(); let jsonStr = content;
const jsonMatch = content.match(/```(?:json)?\s*([\s\S]*?)```/);
let jsonData; if (jsonMatch) jsonStr = jsonMatch[1].trim();
try { let jsonData = JSON.parse(jsonStr);
jsonData = JSON.parse(responseText);
} catch (e) {
const cleanedText = responseText.replace(/```json/g, '').replace(/```/g, '');
jsonData = JSON.parse(cleanedText);
}
if (Array.isArray(jsonData)) jsonData = jsonData[0]; if (Array.isArray(jsonData)) jsonData = jsonData[0];
console.log('[Gemini AI] JSON Response:', jsonData); const endParse = performance.now();
const searchString = jsonData.search_string; const searchString = jsonData.search_string;
delete jsonData.search_string; delete jsonData.search_string;
@@ -120,19 +123,15 @@ export async function analyzeBottle(input: any): Promise<AnalysisResponse> {
if (!jsonData) throw new Error('Keine Daten in der KI-Antwort gefunden.'); if (!jsonData) throw new Error('Keine Daten in der KI-Antwort gefunden.');
const validatedData = BottleMetadataSchema.parse(jsonData); const validatedData = BottleMetadataSchema.parse(jsonData);
const endParse = performance.now();
// 6. Tracking & Credits
await trackApiUsage({ await trackApiUsage({
userId: userId, userId: userId,
apiType: 'gemini_ai', apiType: 'gemini_ai',
endpoint: 'generateContent', endpoint: 'analyzeBottle_openrouter',
success: true success: true
}); });
await deductCredits(userId, 'gemini_ai', 'Bottle analysis (OpenRouter)');
await deductCredits(userId, 'gemini_ai', 'Bottle analysis');
// Cache speichern
await supabase await supabase
.from('vision_cache') .from('vision_cache')
.insert({ hash: imageHash, result: validatedData }); .insert({ hash: imageHash, result: validatedData });
@@ -149,13 +148,82 @@ export async function analyzeBottle(input: any): Promise<AnalysisResponse> {
raw: jsonData raw: jsonData
} as any; } as any;
} catch (aiError: any) { } else {
console.warn('[AnalyzeBottle] AI Analysis failed, providing fallback path:', aiError.message); // Gemini path
const genAI = new GoogleGenerativeAI(process.env.GEMINI_API_KEY!);
const model = genAI.getGenerativeModel({
model: 'gemini-2.5-flash',
generationConfig: {
responseMimeType: "application/json",
temperature: 0.1,
},
safetySettings: [
{ category: HarmCategory.HARM_CATEGORY_HARASSMENT, threshold: HarmBlockThreshold.BLOCK_NONE },
{ category: HarmCategory.HARM_CATEGORY_HATE_SPEECH, threshold: HarmBlockThreshold.BLOCK_NONE },
{ category: HarmCategory.HARM_CATEGORY_SEXUALLY_EXPLICIT, threshold: HarmBlockThreshold.BLOCK_NONE },
{ category: HarmCategory.HARM_CATEGORY_DANGEROUS_CONTENT, threshold: HarmBlockThreshold.BLOCK_NONE },
] as any,
});
const startApi = performance.now();
const result = await model.generateContent([
{ inlineData: { data: base64Data, mimeType: mimeType } },
{ text: instruction },
]);
const endApi = performance.now();
const startParse = performance.now();
const responseText = result.response.text();
let jsonData;
try {
jsonData = JSON.parse(responseText);
} catch (e) {
const cleanedText = responseText.replace(/```json/g, '').replace(/```/g, '');
jsonData = JSON.parse(cleanedText);
}
if (Array.isArray(jsonData)) jsonData = jsonData[0];
const endParse = performance.now();
const searchString = jsonData.search_string;
delete jsonData.search_string;
if (!jsonData) throw new Error('Keine Daten in der KI-Antwort gefunden.');
const validatedData = BottleMetadataSchema.parse(jsonData);
await trackApiUsage({ await trackApiUsage({
userId: userId, userId: userId,
apiType: 'gemini_ai', apiType: 'gemini_ai',
endpoint: 'generateContent', endpoint: 'analyzeBottle_gemini',
success: true
});
await deductCredits(userId, 'gemini_ai', 'Bottle analysis (Gemini)');
await supabase
.from('vision_cache')
.insert({ hash: imageHash, result: validatedData });
return {
success: true,
data: validatedData,
search_string: searchString,
perf: {
apiDuration: endApi - startApi,
parseDuration: endParse - startParse,
uploadSize: uploadSize
},
raw: jsonData
} as any;
}
} catch (aiError: any) {
console.warn(`[AnalyzeBottle] ${provider} failed:`, aiError.message);
await trackApiUsage({
userId: userId,
apiType: 'gemini_ai',
endpoint: `analyzeBottle_${provider}`,
success: false, success: false,
errorMessage: aiError.message errorMessage: aiError.message
}); });
@@ -169,7 +237,7 @@ export async function analyzeBottle(input: any): Promise<AnalysisResponse> {
} }
} catch (error) { } catch (error) {
console.error('Gemini Analysis Global Error:', error); console.error('Analyze Bottle Global Error:', error);
return { return {
success: false, success: false,
error: error instanceof Error ? error.message : 'An unknown error occurred during analysis.', error: error instanceof Error ? error.message : 'An unknown error occurred during analysis.',

View File

@@ -234,8 +234,8 @@ async function markBottleError(
} }
/** /**
* Call Gemini to analyze bottle image * Analyze bottle image using configured AI provider
* Uses existing Gemini integration * Uses OpenRouter by default, falls back to Gemini
*/ */
async function analyzeBottleImage(imageUrl: string): Promise<{ async function analyzeBottleImage(imageUrl: string): Promise<{
success: boolean; success: boolean;
@@ -250,6 +250,9 @@ async function analyzeBottleImage(imageUrl: string): Promise<{
}; };
error?: string; error?: string;
}> { }> {
const { getAIProvider, getOpenRouterClient, OPENROUTER_PROVIDER_PREFERENCES } = await import('@/lib/openrouter');
const provider = getAIProvider();
try { try {
// Fetch image and convert to base64 // Fetch image and convert to base64
const response = await fetch(imageUrl); const response = await fetch(imageUrl);
@@ -262,22 +265,7 @@ async function analyzeBottleImage(imageUrl: string): Promise<{
const base64 = Buffer.from(buffer).toString('base64'); const base64 = Buffer.from(buffer).toString('base64');
const mimeType = blob.type || 'image/webp'; const mimeType = blob.type || 'image/webp';
// Call Gemini const prompt = `Analyze this whisky bottle image. Extract:
const apiKey = process.env.GOOGLE_GENERATIVE_AI_API_KEY;
if (!apiKey) {
return { success: false, error: 'API Key nicht konfiguriert' };
}
const geminiResponse = await fetch(
`https://generativelanguage.googleapis.com/v1beta/models/gemini-2.0-flash:generateContent?key=${apiKey}`,
{
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
contents: [{
parts: [
{
text: `Analyze this whisky bottle image. Extract:
- name: Full product name - name: Full product name
- distillery: Distillery name - distillery: Distillery name
- category: e.g. "Single Malt", "Bourbon", "Blend" - category: e.g. "Single Malt", "Bourbon", "Blend"
@@ -286,20 +274,53 @@ async function analyzeBottleImage(imageUrl: string): Promise<{
- is_whisky: boolean, false if not a whisky - is_whisky: boolean, false if not a whisky
- confidence: 0-100 how confident you are - confidence: 0-100 how confident you are
Respond ONLY with valid JSON, no markdown.` Respond ONLY with valid JSON, no markdown.`;
},
if (provider === 'openrouter') {
// OpenRouter path
const client = getOpenRouterClient();
const openRouterResponse = await client.chat.completions.create({
model: 'google/gemma-3-27b-it',
messages: [{
role: 'user',
content: [
{ type: 'image_url', image_url: { url: `data:${mimeType};base64,${base64}` } },
{ type: 'text', text: prompt },
],
}],
temperature: 0.1,
max_tokens: 500,
// @ts-ignore
provider: OPENROUTER_PROVIDER_PREFERENCES,
});
const content = openRouterResponse.choices[0]?.message?.content || '{}';
let jsonStr = content;
const jsonMatch = content.match(/```(?:json)?\s*([\s\S]*?)```/);
if (jsonMatch) jsonStr = jsonMatch[1].trim();
const parsed = JSON.parse(jsonStr);
return { success: true, data: parsed };
} else {
// Gemini path
const apiKey = process.env.GEMINI_API_KEY;
if (!apiKey) {
return { success: false, error: 'GEMINI_API_KEY nicht konfiguriert' };
}
const geminiResponse = await fetch(
`https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash:generateContent?key=${apiKey}`,
{ {
inline_data: { method: 'POST',
mime_type: mimeType, headers: { 'Content-Type': 'application/json' },
data: base64 body: JSON.stringify({
} contents: [{
} parts: [
{ text: prompt },
{ inline_data: { mime_type: mimeType, data: base64 } }
] ]
}], }],
generationConfig: { generationConfig: { temperature: 0.1, maxOutputTokens: 500 }
temperature: 0.1,
maxOutputTokens: 500,
}
}) })
} }
); );
@@ -315,7 +336,6 @@ Respond ONLY with valid JSON, no markdown.`
return { success: false, error: 'Keine Antwort von Gemini' }; return { success: false, error: 'Keine Antwort von Gemini' };
} }
// Parse JSON response
const jsonMatch = textContent.match(/\{[\s\S]*\}/); const jsonMatch = textContent.match(/\{[\s\S]*\}/);
if (!jsonMatch) { if (!jsonMatch) {
return { success: false, error: 'Ungültige Gemini-Antwort' }; return { success: false, error: 'Ungültige Gemini-Antwort' };
@@ -323,9 +343,10 @@ Respond ONLY with valid JSON, no markdown.`
const parsed = JSON.parse(jsonMatch[0]); const parsed = JSON.parse(jsonMatch[0]);
return { success: true, data: parsed }; return { success: true, data: parsed };
}
} catch (error) { } catch (error) {
console.error('Gemini analysis error:', error); console.error(`[BulkScan] ${provider} analysis error:`, error);
return { success: false, error: 'Analysefehler' }; return { success: false, error: 'Analysefehler' };
} }
} }

File diff suppressed because one or more lines are too long