feat(api): implement retry logic for Mistral API calls in receipt import and AI services

This commit is contained in:
Nils-Johan Gynther
2026-04-19 11:31:05 +02:00
parent 15c24df1a7
commit 045f160655
3 changed files with 107 additions and 74 deletions
@@ -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,30 +99,23 @@ 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({
model: RECEIPT_IMPORT_MODEL,
messages: [
{
role: 'user',
content: [
{
type: 'image_url',
image_url: { url: `data:${mimeType};base64,${base64}` },
},
{ type: 'text', text: IMAGE_PROMPT },
],
},
],
max_tokens: 2000,
temperature: 0.1,
}),
});
const response = await this.callMistralWithRetry({
model: RECEIPT_IMPORT_MODEL,
messages: [
{
role: 'user',
content: [
{
type: 'image_url',
image_url: { url: `data:${mimeType};base64,${base64}` },
},
{ type: 'text', text: IMAGE_PROMPT },
],
},
],
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({
model: RECEIPT_IMPORT_MODEL,
messages: [{ role: 'user', content: TEXT_PROMPT(pdfText) }],
max_tokens: 2000,
temperature: 0.1,
}),
});
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');
}