Files
rely/lib/core/llm/llm_config.dart
T
2026-05-19 11:14:23 +02:00

292 lines
8.5 KiB
Dart

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<LlmConfigState> {
Future<void>? _initializeFuture;
@override
LlmConfigState build() {
return LlmConfigState();
}
Future<void> initialize() async {
final Future<void>? inFlight = _initializeFuture;
if (inFlight != null) {
return inFlight;
}
_initializeFuture = _initializeFromStorage();
return _initializeFuture;
}
Future<void> _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<void> 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<void> 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<void> 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<String?> getApiKey({LlmProvider? provider}) async {
const FlutterSecureStorage secureStorage = FlutterSecureStorage();
return _readApiKey(secureStorage, provider: provider ?? state.provider);
}
Future<void> clearApiKey() async {
const FlutterSecureStorage secureStorage = FlutterSecureStorage();
await secureStorage.delete(key: _providerApiKeyStorageKey(state.provider));
state = state.copyWith(apiKeyConfigured: false);
}
Future<String?> _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, LlmConfigState>(
LlmConfigNotifier.new,
);