fix: add retry + fallback for Gemini 503 UNAVAILABLE errors

- Retry up to 3 times with exponential backoff on 503/UNAVAILABLE
- Fall back to gemini-2.0-flash on final attempt
- Replace raw JSON error with user-friendly Japanese message

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Ponshu Developer 2026-04-09 19:04:40 +09:00
parent d1a41b4d3b
commit 0d741c4cb0
1 changed files with 60 additions and 28 deletions

View File

@ -261,14 +261,9 @@ $extractedText
throw Exception('Gemini API Key is missing. Please set GEMINI_API_KEY.'); throw Exception('Gemini API Key is missing. Please set GEMINI_API_KEY.');
} }
final model = GenerativeModel( // : 503/UNAVAILABLE
model: 'gemini-2.5-flash', // FIXED MODEL NAME - DO NOT CHANGE without explicit user approval (confirmed working on 2026-01-17) const primaryModel = 'gemini-2.5-flash'; // FIXED - confirmed 2026-01-17
apiKey: apiKey, const fallbackModel = 'gemini-2.0-flash'; // 503
generationConfig: GenerationConfig(
responseMimeType: 'application/json',
temperature: 0.2, // 0.40.2 (2026-02-09)
),
);
// Prepare Prompt // Prepare Prompt
final promptText = customPrompt ?? ''' final promptText = customPrompt ?? '''
@ -296,15 +291,37 @@ $extractedText
null null
'''; ''';
// Prepare Content // Prepare Content parts ()
final contentParts = <Part>[TextPart(promptText)]; final contentParts = <Part>[TextPart(promptText)];
for (var path in imagePaths) { for (var path in imagePaths) {
//
final bytes = await File(path).readAsBytes(); final bytes = await File(path).readAsBytes();
contentParts.add(DataPart('image/jpeg', bytes)); contentParts.add(DataPart('image/jpeg', bytes));
} }
// 503 :
const maxRetries = 3;
final modelsToTry = [primaryModel, primaryModel, primaryModel, fallbackModel];
for (int attempt = 0; attempt <= maxRetries; attempt++) {
final modelName = modelsToTry[attempt];
final isLastAttempt = attempt == maxRetries;
try { try {
if (attempt > 0) {
final waitSec = attempt == maxRetries ? 2 : (attempt * 3);
debugPrint('Retry $attempt/$maxRetries (model: $modelName, wait: ${waitSec}s)...');
await Future.delayed(Duration(seconds: waitSec));
}
final model = GenerativeModel(
model: modelName,
apiKey: apiKey,
generationConfig: GenerationConfig(
responseMimeType: 'application/json',
temperature: 0.2,
),
);
final response = await model.generateContent([Content.multi(contentParts)]); final response = await model.generateContent([Content.multi(contentParts)]);
final jsonString = response.text; final jsonString = response.text;
@ -321,12 +338,27 @@ $extractedText
await AnalysisCacheService.registerBrandIndex(result.name, imageHash); await AnalysisCacheService.registerBrandIndex(result.name, imageHash);
} }
if (attempt > 0) debugPrint('Succeeded on attempt $attempt (model: $modelName)');
return result; return result;
} catch (e) { } catch (e) {
debugPrint('Direct API Error: $e'); final errStr = e.toString();
final is503 = errStr.contains('503') || errStr.contains('UNAVAILABLE') || errStr.contains('high demand');
debugPrint('Direct API Error (attempt $attempt, model: $modelName): $e');
if (isLastAttempt || !is503) {
// or 503
if (is503) {
throw Exception('AIサーバーが混雑しています。しばらく待ってから再試行してください。');
}
throw Exception('AI解析エラー(Direct): $e'); throw Exception('AI解析エラー(Direct): $e');
} }
// 503
}
}
//
throw Exception('AI解析に失敗しました。再試行してください。');
} }
} }