import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_secure_storage/flutter_secure_storage.dart'; const String _llmApiKeyStorageKey = 'llm_api_key'; const String _llmBaseUrlStorageKey = 'llm_base_url'; const String _llmModelStorageKey = 'llm_model'; const String _llmProviderStorageKey = 'llm_provider'; String _providerApiKeyStorageKey(LlmProvider provider) { return 'llm_api_key_${provider.name}'; } enum LlmProvider { openai, anthropic, google, ollama, openaiCompatible } extension LlmProviderMetadata on LlmProvider { String get label { return switch (this) { LlmProvider.openai => 'OpenAI', LlmProvider.anthropic => 'Anthropic', LlmProvider.google => 'Google AI', LlmProvider.ollama => 'Ollama', LlmProvider.openaiCompatible => 'OpenAI-compatible', }; } String get defaultBaseUrl { return switch (this) { LlmProvider.openai => 'https://api.openai.com/v1', LlmProvider.anthropic => 'https://api.anthropic.com/v1', LlmProvider.google => 'https://generativelanguage.googleapis.com/v1beta', LlmProvider.ollama => 'http://localhost:11434', LlmProvider.openaiCompatible => '', }; } String get defaultModel { return switch (this) { LlmProvider.openai => 'gpt-4o-mini', LlmProvider.anthropic => 'claude-3-haiku-20240307', LlmProvider.google => 'gemini-2.0-flash', LlmProvider.ollama => '', LlmProvider.openaiCompatible => '', }; } bool get requiresApiKey { return switch (this) { LlmProvider.openai || LlmProvider.anthropic || LlmProvider.google => true, LlmProvider.ollama || LlmProvider.openaiCompatible => false, }; } bool get hasConfigurableBaseUrl { return this == LlmProvider.ollama || this == LlmProvider.openaiCompatible; } } class LlmConfigState { LlmConfigState({ bool apiKeyConfigured = false, LlmProvider provider = LlmProvider.openai, String? baseUrl, String? model, }) : this._( apiKeyConfigured: apiKeyConfigured, provider: provider, baseUrl: _resolveBaseUrl(provider, baseUrl), model: _resolveModel(provider, model), ); const LlmConfigState._({ required this.apiKeyConfigured, required this.provider, required this.baseUrl, required this.model, }); final bool apiKeyConfigured; final LlmProvider provider; final String baseUrl; final String model; bool get isConfigured { final bool hasKey = apiKeyConfigured || !provider.requiresApiKey; return hasKey && baseUrl.trim().isNotEmpty && model.trim().isNotEmpty; } LlmConfigState copyWith({ bool? apiKeyConfigured, LlmProvider? provider, String? baseUrl, String? model, }) { final LlmProvider nextProvider = provider ?? this.provider; return LlmConfigState( apiKeyConfigured: apiKeyConfigured ?? this.apiKeyConfigured, provider: nextProvider, baseUrl: baseUrl ?? this.baseUrl, model: model ?? this.model, ); } static String _resolveBaseUrl(LlmProvider provider, String? baseUrl) { final String? trimmed = baseUrl?.trim(); if (trimmed != null && trimmed.isNotEmpty) { return trimmed; } return provider.defaultBaseUrl; } static String _resolveModel(LlmProvider provider, String? model) { final String? trimmed = model?.trim(); if (trimmed != null && trimmed.isNotEmpty) { return trimmed; } return provider.defaultModel; } } class LlmConfigNotifier extends Notifier { Future? _initializeFuture; @override LlmConfigState build() { return LlmConfigState(); } Future initialize() async { final Future? inFlight = _initializeFuture; if (inFlight != null) { return inFlight; } _initializeFuture = _initializeFromStorage(); return _initializeFuture; } Future _initializeFromStorage() async { const FlutterSecureStorage secureStorage = FlutterSecureStorage(); final String? storedProvider = await secureStorage.read( key: _llmProviderStorageKey, ); final String? storedBaseUrl = await secureStorage.read( key: _llmBaseUrlStorageKey, ); final String? storedModel = await secureStorage.read( key: _llmModelStorageKey, ); final LlmProvider provider = storedProvider != null ? LlmProvider.values.firstWhere( (LlmProvider p) => p.name == storedProvider, orElse: () => LlmProvider.openai, ) : LlmProvider.openai; String? storedKey = await _readApiKey(secureStorage, provider: provider); if (storedKey == null || storedKey.isEmpty) { final String? legacyKey = await secureStorage.read( key: _llmApiKeyStorageKey, ); if (legacyKey != null && legacyKey.isNotEmpty) { await secureStorage.write( key: _providerApiKeyStorageKey(provider), value: legacyKey, ); storedKey = legacyKey; } } state = LlmConfigState( apiKeyConfigured: storedKey != null && storedKey.isNotEmpty, provider: provider, baseUrl: storedBaseUrl, model: storedModel, ); } Future setApiKey(String apiKey) async { const FlutterSecureStorage secureStorage = FlutterSecureStorage(); final String key = _providerApiKeyStorageKey(state.provider); if (apiKey.isEmpty) { await secureStorage.delete(key: key); } else { await secureStorage.write(key: key, value: apiKey); } state = state.copyWith(apiKeyConfigured: apiKey.isNotEmpty); } Future setProvider(LlmProvider provider) async { const FlutterSecureStorage secureStorage = FlutterSecureStorage(); await secureStorage.write( key: _llmProviderStorageKey, value: provider.name, ); await secureStorage.write( key: _llmBaseUrlStorageKey, value: provider.defaultBaseUrl, ); await secureStorage.write( key: _llmModelStorageKey, value: provider.defaultModel, ); final String? storedKey = await _readApiKey( secureStorage, provider: provider, ); state = state.copyWith( apiKeyConfigured: storedKey != null && storedKey.isNotEmpty, provider: provider, baseUrl: provider.defaultBaseUrl, model: provider.defaultModel, ); } Future saveConfiguration({ required LlmProvider provider, required String baseUrl, required String model, String? apiKey, bool clearApiKey = false, }) async { const FlutterSecureStorage secureStorage = FlutterSecureStorage(); await secureStorage.write( key: _llmProviderStorageKey, value: provider.name, ); await secureStorage.write( key: _llmBaseUrlStorageKey, value: baseUrl.trim(), ); await secureStorage.write(key: _llmModelStorageKey, value: model.trim()); final String? storedProviderKey = await _readApiKey( secureStorage, provider: provider, ); bool apiKeyConfigured = storedProviderKey != null && storedProviderKey.isNotEmpty; if (clearApiKey) { await secureStorage.delete(key: _providerApiKeyStorageKey(provider)); apiKeyConfigured = false; } else if (apiKey != null) { final String trimmedApiKey = apiKey.trim(); if (trimmedApiKey.isEmpty) { await secureStorage.delete(key: _providerApiKeyStorageKey(provider)); apiKeyConfigured = false; } else { await secureStorage.write( key: _providerApiKeyStorageKey(provider), value: trimmedApiKey, ); apiKeyConfigured = true; } } state = LlmConfigState( apiKeyConfigured: apiKeyConfigured, provider: provider, baseUrl: baseUrl, model: model, ); } Future getApiKey({LlmProvider? provider}) async { const FlutterSecureStorage secureStorage = FlutterSecureStorage(); return _readApiKey(secureStorage, provider: provider ?? state.provider); } Future clearApiKey() async { const FlutterSecureStorage secureStorage = FlutterSecureStorage(); await secureStorage.delete(key: _providerApiKeyStorageKey(state.provider)); state = state.copyWith(apiKeyConfigured: false); } Future _readApiKey( FlutterSecureStorage secureStorage, { required LlmProvider provider, }) async { final String? providerKey = await secureStorage.read( key: _providerApiKeyStorageKey(provider), ); if (providerKey != null && providerKey.isNotEmpty) { return providerKey; } return null; } } final llmConfigProvider = NotifierProvider( LlmConfigNotifier.new, );