import 'dart:convert'; import 'package:flutter_dotenv/flutter_dotenv.dart'; import 'package:google_generative_ai/google_generative_ai.dart'; import 'package:http/http.dart' as http; class GeminiService { static final GeminiService _instance = GeminiService._internal(); late final String _apiKey; /// Cache of valid model IDs (flash/lite, supporting generateContent). List _validModels = []; factory GeminiService() => _instance; GeminiService._internal() { final apiKey = dotenv.env['GEMINI_API_KEY']; if (apiKey == null || apiKey.isEmpty) { throw Exception('GEMINI_API_KEY not found in .env file'); } _apiKey = apiKey; } // --------------------------------------------------------------------------- // Model discovery // --------------------------------------------------------------------------- /// Queries the Gemini REST API for available models and caches those that /// - support `generateContent`, AND /// - contain "flash" or "lite" in their name (free-tier / fast models). /// /// Returns a stable cached list on subsequent calls. Future> _getValidModels() async { if (_validModels.isNotEmpty) return _validModels; try { final uri = Uri.parse( 'https://generativelanguage.googleapis.com/v1beta/models?key=$_apiKey', ); final response = await http.get(uri); if (response.statusCode == 200) { final data = jsonDecode(response.body) as Map; final rawModels = (data['models'] as List?) ?? []; final discovered = []; for (final m in rawModels) { final fullName = (m['name'] as String? ?? ''); final lower = fullName.toLowerCase(); final methods = (m['supportedGenerationMethods'] as List?) ?? []; if (methods.contains('generateContent') && (lower.contains('flash') || lower.contains('lite'))) { // Strip the "models/" prefix so it can be passed directly to // GenerativeModel(model: ...). final id = fullName.startsWith('models/') ? fullName.substring('models/'.length) : fullName; discovered.add(id); } } // Sort descending so newer/more-capable models are tried first. discovered.sort((a, b) => b.compareTo(a)); _validModels = discovered; } } catch (_) { // Fall back to hard-coded list of known free-tier models below. } // If discovery failed or returned nothing, use safe known fallbacks. if (_validModels.isEmpty) { _validModels = [ 'gemini-2.5-flash-lite', 'gemini-2.5-flash', 'gemini-2.0-flash', 'gemini-1.5-flash', ]; } return _validModels; } // --------------------------------------------------------------------------- // Public API // --------------------------------------------------------------------------- /// Fixes spelling/grammar, improves clarity, and translates [text] to /// professional English. /// /// Provide a custom [promptInstruction] to give Gemini field-specific /// context (subject, description, action-taken). If omitted a sensible /// default is used. /// /// Automatically retries with the next available model on 429 / quota /// errors to minimise wasted quota calls. Future enhanceText(String text, {String? promptInstruction}) async { if (text.trim().isEmpty) return text; final instruction = promptInstruction ?? 'Fix spelling and grammar, improve clarity, and translate to ' 'professional English. Return ONLY the improved text, no explanations:'; final prompt = '$instruction\n\n"$text"'; return _generateWithRetry(prompt, fallback: text); } // --------------------------------------------------------------------------- // Internal helpers // --------------------------------------------------------------------------- /// Sends [prompt] to Gemini, retrying across all valid models on 429 errors. /// Returns the model response on success, or throws if all models fail. Future _generateWithRetry( String prompt, { required String fallback, }) async { final models = await _getValidModels(); for (int i = 0; i < models.length; i++) { try { final model = GenerativeModel(model: models[i], apiKey: _apiKey); final response = await model.generateContent([Content.text(prompt)]); return response.text ?? fallback; } catch (e) { final msg = e.toString().toLowerCase(); final is429 = msg.contains('429') || msg.contains('quota') || msg.contains('resource_exhausted'); if (!is429 || i == models.length - 1) { // Non-quota error or last model — give up. throw Exception('Gemini request failed: $e'); } // Quota hit — try the next model. } } // Should not reach here, but safety fallback. throw Exception('All Gemini models exhausted'); } }