Improve LLM provider configuration
This commit is contained in:
+215
-31
@@ -2,32 +2,119 @@ 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';
|
||||
|
||||
enum LlmProvider { openai, anthropic, google }
|
||||
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 {
|
||||
const LlmConfigState({
|
||||
this.apiKeyConfigured = false,
|
||||
this.provider = LlmProvider.openai,
|
||||
this.isConfigured = false,
|
||||
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 bool isConfigured;
|
||||
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,
|
||||
bool? isConfigured,
|
||||
String? baseUrl,
|
||||
String? model,
|
||||
}) {
|
||||
final LlmProvider nextProvider = provider ?? this.provider;
|
||||
return LlmConfigState(
|
||||
apiKeyConfigured: apiKeyConfigured ?? this.apiKeyConfigured,
|
||||
provider: provider ?? this.provider,
|
||||
isConfigured: isConfigured ?? this.isConfigured,
|
||||
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> {
|
||||
@@ -35,7 +122,7 @@ class LlmConfigNotifier extends Notifier<LlmConfigState> {
|
||||
|
||||
@override
|
||||
LlmConfigState build() {
|
||||
return const LlmConfigState();
|
||||
return LlmConfigState();
|
||||
}
|
||||
|
||||
Future<void> initialize() async {
|
||||
@@ -49,36 +136,53 @@ class LlmConfigNotifier extends Notifier<LlmConfigState> {
|
||||
|
||||
Future<void> _initializeFromStorage() async {
|
||||
const FlutterSecureStorage secureStorage = FlutterSecureStorage();
|
||||
final String? storedKey = await secureStorage.read(
|
||||
key: _llmApiKeyStorageKey,
|
||||
);
|
||||
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: storedProvider != null
|
||||
? LlmProvider.values.firstWhere(
|
||||
(LlmProvider p) => p.name == storedProvider,
|
||||
orElse: () => LlmProvider.openai,
|
||||
)
|
||||
: LlmProvider.openai,
|
||||
isConfigured: 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: _llmApiKeyStorageKey);
|
||||
await secureStorage.delete(key: key);
|
||||
} else {
|
||||
await secureStorage.write(key: _llmApiKeyStorageKey, value: apiKey);
|
||||
await secureStorage.write(key: key, value: apiKey);
|
||||
}
|
||||
state = state.copyWith(
|
||||
apiKeyConfigured: apiKey.isNotEmpty,
|
||||
isConfigured: apiKey.isNotEmpty,
|
||||
);
|
||||
state = state.copyWith(apiKeyConfigured: apiKey.isNotEmpty);
|
||||
}
|
||||
|
||||
Future<void> setProvider(LlmProvider provider) async {
|
||||
@@ -87,18 +191,98 @@ class LlmConfigNotifier extends Notifier<LlmConfigState> {
|
||||
key: _llmProviderStorageKey,
|
||||
value: provider.name,
|
||||
);
|
||||
state = state.copyWith(provider: provider);
|
||||
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<String?> getApiKey() async {
|
||||
Future<void> saveConfiguration({
|
||||
required LlmProvider provider,
|
||||
required String baseUrl,
|
||||
required String model,
|
||||
String? apiKey,
|
||||
bool clearApiKey = false,
|
||||
}) async {
|
||||
const FlutterSecureStorage secureStorage = FlutterSecureStorage();
|
||||
return secureStorage.read(key: _llmApiKeyStorageKey);
|
||||
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: _llmApiKeyStorageKey);
|
||||
state = state.copyWith(apiKeyConfigured: false, isConfigured: false);
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user