fix: Add retry logic for OpenRouter 429 rate limit errors

- Retries up to 3 times with exponential backoff (2s, 4s, 8s)
- Also handles 503 service unavailable errors
- Logs retry attempts for debugging
- Only retries rate limit errors, other errors fail immediately
This commit is contained in:
2025-12-26 00:06:21 +01:00
parent 8ccd600dcb
commit 8cf51d4aea
3 changed files with 64 additions and 32 deletions

View File

@@ -71,50 +71,82 @@ Respond ONLY with valid JSON in this format:
"confidence": 0.85 "confidence": 0.85
}`; }`;
/**
* Sleep helper for retry delays
*/
function sleep(ms: number): Promise<void> {
return new Promise(resolve => setTimeout(resolve, ms));
}
/** /**
* Analyze whisky label with OpenRouter (Gemma 3 27B) * Analyze whisky label with OpenRouter (Gemma 3 27B)
* Includes retry logic for 429 rate limit errors
*/ */
async function analyzeWithOpenRouter(base64Data: string, mimeType: string): Promise<{ data: any; apiTime: number }> { async function analyzeWithOpenRouter(base64Data: string, mimeType: string): Promise<{ data: any; apiTime: number }> {
const client = getOpenRouterClient(); const client = getOpenRouterClient();
const startApi = performance.now(); const startApi = performance.now();
const maxRetries = 3;
let lastError: any = null;
const response = await client.chat.completions.create({ for (let attempt = 1; attempt <= maxRetries; attempt++) {
model: OPENROUTER_VISION_MODEL, try {
messages: [ const response = await client.chat.completions.create({
{ model: OPENROUTER_VISION_MODEL,
role: 'user', messages: [
content: [
{ {
type: 'image_url', role: 'user',
image_url: { content: [
url: `data:${mimeType};base64,${base64Data}`, {
}, type: 'image_url',
}, image_url: {
{ url: `data:${mimeType};base64,${base64Data}`,
type: 'text', },
text: VISION_PROMPT, },
{
type: 'text',
text: VISION_PROMPT,
},
],
}, },
], ],
}, temperature: 0.1,
], max_tokens: 1024,
temperature: 0.1, });
max_tokens: 1024,
});
const endApi = performance.now(); const endApi = performance.now();
const content = response.choices[0]?.message?.content || '{}'; const content = response.choices[0]?.message?.content || '{}';
// Extract JSON from response (may have markdown code blocks) // Extract JSON from response (may have markdown code blocks)
let jsonStr = content; let jsonStr = content;
const jsonMatch = content.match(/```(?:json)?\s*([\s\S]*?)```/); const jsonMatch = content.match(/```(?:json)?\s*([\s\S]*?)```/);
if (jsonMatch) { if (jsonMatch) {
jsonStr = jsonMatch[1].trim(); jsonStr = jsonMatch[1].trim();
}
return {
data: JSON.parse(jsonStr),
apiTime: endApi - startApi,
};
} catch (error: any) {
lastError = error;
const status = error?.status || error?.response?.status;
// Only retry on 429 (rate limit) or 503 (service unavailable)
if (status === 429 || status === 503) {
const delay = Math.pow(2, attempt) * 1000; // 2s, 4s, 8s
console.log(`[OpenRouter] Rate limited (${status}), retry ${attempt}/${maxRetries} in ${delay}ms...`);
await sleep(delay);
continue;
}
// Other errors - don't retry
throw error;
}
} }
return { // All retries exhausted
data: JSON.parse(jsonStr), throw lastError || new Error('OpenRouter request failed after retries');
apiTime: endApi - startApi,
};
} }
/** /**

View File

@@ -37,4 +37,4 @@ export function getOpenRouterClient(): OpenAI {
} }
// Default OpenRouter model for vision tasks // Default OpenRouter model for vision tasks
export const OPENROUTER_VISION_MODEL = 'google/gemma-3-27b-it'; export const OPENROUTER_VISION_MODEL = 'google/gemma-3-27b-it:free';

File diff suppressed because one or more lines are too long