feat(api): implement retry logic for Mistral API calls in receipt import and AI services
This commit is contained in:
@@ -47,7 +47,9 @@ Regler:
|
||||
|
||||
const userPrompt = `Produkt: "${productName}"`;
|
||||
|
||||
let raw: string;
|
||||
let raw = '';
|
||||
const MAX_RETRIES = 3;
|
||||
for (let attempt = 1; attempt <= MAX_RETRIES; attempt++) {
|
||||
try {
|
||||
const response = await fetch(MISTRAL_API_URL, {
|
||||
method: 'POST',
|
||||
@@ -67,6 +69,16 @@ Regler:
|
||||
}),
|
||||
});
|
||||
|
||||
if (response.status === 503 || response.status === 429) {
|
||||
const err = await response.text();
|
||||
this.logger.warn(`Mistral API ${response.status} (försök ${attempt}/${MAX_RETRIES}): ${err}`);
|
||||
if (attempt < MAX_RETRIES) {
|
||||
await new Promise((r) => setTimeout(r, attempt * 1500));
|
||||
continue;
|
||||
}
|
||||
throw new ServiceUnavailableException('AI-tjänsten är tillfälligt otillgänglig, försök igen');
|
||||
}
|
||||
|
||||
if (!response.ok) {
|
||||
const err = await response.text();
|
||||
this.logger.error(`Mistral API-fel: ${response.status} ${err}`);
|
||||
@@ -75,11 +87,17 @@ Regler:
|
||||
|
||||
const data = await response.json() as { choices: { message: { content: string } }[] };
|
||||
raw = data.choices?.[0]?.message?.content ?? '';
|
||||
break;
|
||||
} catch (err) {
|
||||
if (err instanceof ServiceUnavailableException) throw err;
|
||||
this.logger.error(`Mistral fetch-fel: ${String(err)}`);
|
||||
this.logger.error(`Mistral fetch-fel (försök ${attempt}/${MAX_RETRIES}): ${String(err)}`);
|
||||
if (attempt < MAX_RETRIES) {
|
||||
await new Promise((r) => setTimeout(r, attempt * 1500));
|
||||
continue;
|
||||
}
|
||||
throw new ServiceUnavailableException('Kunde inte nå AI-tjänsten');
|
||||
}
|
||||
}
|
||||
|
||||
// Parsa och validera AI-svaret
|
||||
let parsed: { categoryId: number; confidence: string };
|
||||
|
||||
@@ -38,6 +38,33 @@ ${text}`;
|
||||
@Injectable()
|
||||
export class ReceiptImportService {
|
||||
private readonly logger = new Logger(ReceiptImportService.name);
|
||||
private readonly MAX_RETRIES = 3;
|
||||
|
||||
private async callMistralWithRetry(body: object, apiKey: string, source: string): Promise<Response> {
|
||||
for (let attempt = 1; attempt <= this.MAX_RETRIES; attempt++) {
|
||||
const response = await fetch(MISTRAL_API_URL, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${apiKey}`,
|
||||
},
|
||||
body: JSON.stringify(body),
|
||||
});
|
||||
|
||||
if (response.status === 503 || response.status === 429) {
|
||||
const err = await response.text();
|
||||
this.logger.warn(`Mistral ${response.status} (${source}, försök ${attempt}/${this.MAX_RETRIES}): ${err}`);
|
||||
if (attempt < this.MAX_RETRIES) {
|
||||
await new Promise((r) => setTimeout(r, attempt * 2000));
|
||||
continue;
|
||||
}
|
||||
throw new ServiceUnavailableException('Mistral API returnerade ett fel: Tjänsten tillfälligt otillgänglig (503)');
|
||||
}
|
||||
|
||||
return response;
|
||||
}
|
||||
throw new ServiceUnavailableException('Kunde inte nå Mistral API efter flera försök');
|
||||
}
|
||||
|
||||
constructor(
|
||||
private readonly prisma: PrismaService,
|
||||
@@ -72,13 +99,7 @@ export class ReceiptImportService {
|
||||
apiKey: string,
|
||||
): Promise<ParsedReceiptItem[]> {
|
||||
const base64 = buffer.toString('base64');
|
||||
const response = await fetch(MISTRAL_API_URL, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${apiKey}`,
|
||||
},
|
||||
body: JSON.stringify({
|
||||
const response = await this.callMistralWithRetry({
|
||||
model: RECEIPT_IMPORT_MODEL,
|
||||
messages: [
|
||||
{
|
||||
@@ -94,8 +115,7 @@ export class ReceiptImportService {
|
||||
],
|
||||
max_tokens: 2000,
|
||||
temperature: 0.1,
|
||||
}),
|
||||
});
|
||||
}, apiKey, 'bild');
|
||||
|
||||
return this.extractItemsFromMistralResponse(response, 'bild');
|
||||
}
|
||||
@@ -120,19 +140,12 @@ export class ReceiptImportService {
|
||||
|
||||
this.logger.log(`PDF-text extraherad (${pdfText.length} tecken)`);
|
||||
|
||||
const response = await fetch(MISTRAL_API_URL, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${apiKey}`,
|
||||
},
|
||||
body: JSON.stringify({
|
||||
const response = await this.callMistralWithRetry({
|
||||
model: RECEIPT_IMPORT_MODEL,
|
||||
messages: [{ role: 'user', content: TEXT_PROMPT(pdfText) }],
|
||||
max_tokens: 2000,
|
||||
temperature: 0.1,
|
||||
}),
|
||||
});
|
||||
}, apiKey, 'PDF');
|
||||
|
||||
return this.extractItemsFromMistralResponse(response, 'PDF');
|
||||
}
|
||||
|
||||
@@ -1,19 +1,21 @@
|
||||
import { NextResponse } from 'next/server';
|
||||
import { auth } from '../../../auth';
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { getAuthHeaders } from '../../../lib/auth-headers';
|
||||
|
||||
const API_BASE = process.env.NEXT_PUBLIC_API_URL_INTERNAL || 'http://recipe-api:8080';
|
||||
|
||||
export const GET = auth(async function GET(req) {
|
||||
const token = (req.auth as any)?.accessToken as string | undefined;
|
||||
if (!token) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
||||
export async function GET(req: NextRequest) {
|
||||
const authHeaders = await getAuthHeaders();
|
||||
if (!authHeaders.Authorization) {
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
||||
}
|
||||
|
||||
const url = new URL(req.url);
|
||||
const query = url.searchParams.toString();
|
||||
const res = await fetch(`${API_BASE}/api/products${query ? `?${query}` : ''}`, {
|
||||
headers: { Authorization: `Bearer ${token}` },
|
||||
headers: authHeaders,
|
||||
cache: 'no-store',
|
||||
});
|
||||
|
||||
const data = await res.json();
|
||||
return NextResponse.json(data, { status: res.status });
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user