feat: robust FormData handling for AI services to improve N100 performance and stability
This commit is contained in:
@@ -8,13 +8,33 @@ import { createHash } from 'crypto';
|
||||
import { trackApiUsage } from './track-api-usage';
|
||||
import { checkCreditBalance, deductCredits } from './credit-service';
|
||||
|
||||
export async function analyzeBottleMistral(base64Image: string, tags?: string[], locale: string = 'de'): Promise<AnalysisResponse & { search_string?: string }> {
|
||||
// WICHTIG: Wir akzeptieren jetzt FormData statt Strings
|
||||
export async function analyzeBottleMistral(input: any): Promise<AnalysisResponse & { search_string?: string }> {
|
||||
if (!process.env.MISTRAL_API_KEY) {
|
||||
return { success: false, error: 'MISTRAL_API_KEY is not configured.' };
|
||||
}
|
||||
|
||||
let supabase;
|
||||
try {
|
||||
// Helper to get value from either FormData or POJO
|
||||
const getValue = (obj: any, key: string): any => {
|
||||
if (obj && typeof obj.get === 'function') return obj.get(key);
|
||||
if (obj && typeof obj[key] !== 'undefined') return obj[key];
|
||||
return null;
|
||||
};
|
||||
|
||||
// 1. Daten extrahieren
|
||||
const file = getValue(input, 'file') as File;
|
||||
const tagsString = getValue(input, 'tags') as string;
|
||||
const locale = getValue(input, 'locale') || 'de';
|
||||
|
||||
if (!file) {
|
||||
return { success: false, error: 'Kein Bild empfangen.' };
|
||||
}
|
||||
|
||||
const tags = tagsString ? (typeof tagsString === 'string' ? JSON.parse(tagsString) : tagsString) : [];
|
||||
|
||||
// 2. Auth & Credits
|
||||
supabase = await createClient();
|
||||
const { data: { session } } = await supabase.auth.getSession();
|
||||
if (!session || !session.user) {
|
||||
@@ -22,18 +42,22 @@ export async function analyzeBottleMistral(base64Image: string, tags?: string[],
|
||||
}
|
||||
|
||||
const userId = session.user.id;
|
||||
|
||||
const creditCheck = await checkCreditBalance(userId, 'gemini_ai');
|
||||
if (!creditCheck.allowed) {
|
||||
return {
|
||||
success: false,
|
||||
error: `Nicht genügend Credits. Du benötigst ${creditCheck.cost} Credits, hast aber nur ${creditCheck.balance}.`
|
||||
error: `Nicht genügend Credits. Benötigt: ${creditCheck.cost}, Verfügbar: ${creditCheck.balance}.`
|
||||
};
|
||||
}
|
||||
|
||||
const base64Data = base64Image.split(',')[1] || base64Image;
|
||||
const imageHash = createHash('sha256').update(base64Data).digest('hex');
|
||||
// 3. Datei in Buffer umwandeln
|
||||
const arrayBuffer = await file.arrayBuffer();
|
||||
const buffer = Buffer.from(arrayBuffer);
|
||||
|
||||
// 4. Hash für Cache erstellen
|
||||
const imageHash = createHash('sha256').update(buffer).digest('hex');
|
||||
|
||||
// Cache Check
|
||||
const { data: cachedResult } = await supabase
|
||||
.from('vision_cache')
|
||||
.select('result')
|
||||
@@ -47,10 +71,13 @@ export async function analyzeBottleMistral(base64Image: string, tags?: string[],
|
||||
};
|
||||
}
|
||||
|
||||
// 5. Für Mistral vorbereiten
|
||||
const client = new Mistral({ apiKey: process.env.MISTRAL_API_KEY });
|
||||
const dataUrl = `data:image/webp;base64,${base64Data}`;
|
||||
const base64Data = buffer.toString('base64');
|
||||
const mimeType = file.type || 'image/webp';
|
||||
const dataUrl = `data:${mimeType};base64,${base64Data}`;
|
||||
|
||||
const prompt = getSystemPrompt(tags ? tags.join(', ') : 'Keine Tags verfügbar', locale);
|
||||
const prompt = getSystemPrompt(tags.length > 0 ? tags.join(', ') : 'Keine Tags verfügbar', locale);
|
||||
|
||||
const chatResponse = await client.chat.complete({
|
||||
model: 'mistral-large-latest',
|
||||
@@ -70,7 +97,15 @@ export async function analyzeBottleMistral(base64Image: string, tags?: string[],
|
||||
const rawContent = chatResponse.choices?.[0].message.content;
|
||||
if (!rawContent) throw new Error("Keine Antwort von Mistral");
|
||||
|
||||
const jsonData = JSON.parse(rawContent as string);
|
||||
let jsonData;
|
||||
try {
|
||||
jsonData = JSON.parse(rawContent as string);
|
||||
} catch (e) {
|
||||
const cleanedText = (rawContent as string).replace(/```json/g, '').replace(/```/g, '');
|
||||
jsonData = JSON.parse(cleanedText);
|
||||
}
|
||||
|
||||
if (Array.isArray(jsonData)) jsonData = jsonData[0];
|
||||
|
||||
// Extract search_string before validation
|
||||
const searchString = jsonData.search_string;
|
||||
@@ -90,7 +125,7 @@ export async function analyzeBottleMistral(base64Image: string, tags?: string[],
|
||||
// Track usage
|
||||
await trackApiUsage({
|
||||
userId: userId,
|
||||
apiType: 'gemini_ai', // Keep as generic 'gemini_ai' for now or update schema later
|
||||
apiType: 'gemini_ai',
|
||||
endpoint: 'mistral/mistral-large',
|
||||
success: true
|
||||
});
|
||||
@@ -112,22 +147,17 @@ export async function analyzeBottleMistral(base64Image: string, tags?: string[],
|
||||
} catch (error) {
|
||||
console.error('Mistral Analysis Error:', error);
|
||||
|
||||
// Track failed API call
|
||||
try {
|
||||
if (supabase) {
|
||||
const { data: { session } } = await supabase.auth.getSession();
|
||||
if (session?.user) {
|
||||
await trackApiUsage({
|
||||
userId: session.user.id,
|
||||
apiType: 'gemini_ai',
|
||||
endpoint: 'mistral/mistral-large',
|
||||
success: false,
|
||||
errorMessage: error instanceof Error ? error.message : 'Unknown error'
|
||||
});
|
||||
}
|
||||
if (supabase) {
|
||||
const { data: { session } } = await supabase.auth.getSession();
|
||||
if (session?.user) {
|
||||
await trackApiUsage({
|
||||
userId: session.user.id,
|
||||
apiType: 'gemini_ai',
|
||||
endpoint: 'mistral/mistral-large',
|
||||
success: false,
|
||||
errorMessage: error instanceof Error ? error.message : 'Unknown error'
|
||||
});
|
||||
}
|
||||
} catch (trackError) {
|
||||
console.error('Failed to track error:', trackError);
|
||||
}
|
||||
|
||||
return {
|
||||
|
||||
Reference in New Issue
Block a user