refactor: enhance model loading and error handling in providers

- Improved handling of asynchronous states in the model loading process.
- Added debug logging for better traceability of model loading failures.
- Ensured proper checks for mounted state to prevent updates after disposal.
- Cleaned up code formatting for better readability.
- Updated the `defaultModel` provider to include more detailed logging and error handling.
This commit is contained in:
cogwheel0
2025-09-30 15:20:08 +05:30
parent fd35ba3167
commit ff6d33abdf
2 changed files with 117 additions and 27 deletions

View File

@@ -924,7 +924,9 @@ Future<List<Conversation>> conversations(Ref ref) async {
); );
// Update cache timestamp // Update cache timestamp
ref.read(_conversationsCacheTimestampProvider.notifier).set(DateTime.now()); ref
.read(_conversationsCacheTimestampProvider.notifier)
.set(DateTime.now());
return sortedConversations; return sortedConversations;
} catch (e) { } catch (e) {
@@ -942,7 +944,9 @@ Future<List<Conversation>> conversations(Ref ref) async {
); );
// Update cache timestamp // Update cache timestamp
ref.read(_conversationsCacheTimestampProvider.notifier).set(DateTime.now()); ref
.read(_conversationsCacheTimestampProvider.notifier)
.set(DateTime.now());
return conversations; // Return original conversations if folder fetch fails return conversations; // Return original conversations if folder fetch fails
} }
@@ -1003,13 +1007,16 @@ Future<Conversation> loadConversation(Ref ref, String conversationId) async {
} }
// Provider to automatically load and set the default model from user settings or OpenWebUI // Provider to automatically load and set the default model from user settings or OpenWebUI
@riverpod @Riverpod(keepAlive: true)
Future<Model?> defaultModel(Ref ref) async { Future<Model?> defaultModel(Ref ref) async {
DebugLogger.log('provider-called', scope: 'models/default');
// Initialize the settings watcher (side-effect only) // Initialize the settings watcher (side-effect only)
ref.read(_settingsWatcherProvider); ref.read(_settingsWatcherProvider);
// Read settings without subscribing to rebuilds to avoid watch/await hazards // Read settings without subscribing to rebuilds to avoid watch/await hazards
final reviewerMode = ref.read(reviewerModeProvider); final reviewerMode = ref.read(reviewerModeProvider);
if (reviewerMode) { if (reviewerMode) {
DebugLogger.log('reviewer-mode', scope: 'models/default');
// Check if a model is manually selected // Check if a model is manually selected
final currentSelected = ref.read(selectedModelProvider); final currentSelected = ref.read(selectedModelProvider);
final isManualSelection = ref.read(isManualModelSelectionProvider); final isManualSelection = ref.read(isManualModelSelectionProvider);
@@ -1037,11 +1044,17 @@ Future<Model?> defaultModel(Ref ref) async {
} }
return defaultModel; return defaultModel;
} }
DebugLogger.warning('no-demo-models', scope: 'models/default');
return null; return null;
} }
final api = ref.watch(apiServiceProvider); final api = ref.watch(apiServiceProvider);
if (api == null) return null; if (api == null) {
DebugLogger.warning('no-api', scope: 'models/default');
return null;
}
DebugLogger.log('api-available', scope: 'models/default');
try { try {
// Respect manual selection if present // Respect manual selection if present
@@ -1065,7 +1078,10 @@ Future<Model?> defaultModel(Ref ref) async {
// Reconcile against real models in background // Reconcile against real models in background
Future.microtask(() async { Future.microtask(() async {
try { try {
if (!ref.mounted) return;
final models = await ref.read(modelsProvider.future); final models = await ref.read(modelsProvider.future);
if (!ref.mounted) return;
Model? resolved; Model? resolved;
try { try {
resolved = models.firstWhere((m) => m.id == storedDefaultId); resolved = models.firstWhere((m) => m.id == storedDefaultId);
@@ -1076,6 +1092,8 @@ Future<Model?> defaultModel(Ref ref) async {
if (byName.length == 1) resolved = byName.first; if (byName.length == 1) resolved = byName.first;
} }
resolved ??= models.isNotEmpty ? models.first : null; resolved ??= models.isNotEmpty ? models.first : null;
if (!ref.mounted) return;
if (resolved != null && !ref.read(isManualModelSelectionProvider)) { if (resolved != null && !ref.read(isManualModelSelectionProvider)) {
ref.read(selectedModelProvider.notifier).set(resolved); ref.read(selectedModelProvider.notifier).set(resolved);
DebugLogger.log( DebugLogger.log(
@@ -1084,7 +1102,13 @@ Future<Model?> defaultModel(Ref ref) async {
data: {'name': resolved.name, 'source': 'stored'}, data: {'name': resolved.name, 'source': 'stored'},
); );
} }
} catch (_) {} } catch (e) {
DebugLogger.error(
'reconcile-failed',
scope: 'models/default',
error: e,
);
}
}); });
return ref.read(selectedModelProvider); return ref.read(selectedModelProvider);
} }
@@ -1105,7 +1129,10 @@ Future<Model?> defaultModel(Ref ref) async {
// Reconcile against real models in background // Reconcile against real models in background
Future.microtask(() async { Future.microtask(() async {
try { try {
if (!ref.mounted) return;
final models = await ref.read(modelsProvider.future); final models = await ref.read(modelsProvider.future);
if (!ref.mounted) return;
Model? resolved; Model? resolved;
try { try {
resolved = models.firstWhere((m) => m.id == serverDefault); resolved = models.firstWhere((m) => m.id == serverDefault);
@@ -1116,6 +1143,8 @@ Future<Model?> defaultModel(Ref ref) async {
if (byName.length == 1) resolved = byName.first; if (byName.length == 1) resolved = byName.first;
} }
resolved ??= models.isNotEmpty ? models.first : null; resolved ??= models.isNotEmpty ? models.first : null;
if (!ref.mounted) return;
if (resolved != null && !ref.read(isManualModelSelectionProvider)) { if (resolved != null && !ref.read(isManualModelSelectionProvider)) {
ref.read(selectedModelProvider.notifier).set(resolved); ref.read(selectedModelProvider.notifier).set(resolved);
DebugLogger.log( DebugLogger.log(
@@ -1124,14 +1153,26 @@ Future<Model?> defaultModel(Ref ref) async {
data: {'name': resolved.name, 'source': 'server'}, data: {'name': resolved.name, 'source': 'server'},
); );
} }
} catch (_) {} } catch (e) {
DebugLogger.error(
'reconcile-failed',
scope: 'models/default',
error: e,
);
}
}); });
return ref.read(selectedModelProvider); return ref.read(selectedModelProvider);
} }
} catch (_) {} } catch (_) {}
// 3) Fallback: fetch models and pick first available // 3) Fallback: fetch models and pick first available
DebugLogger.log('fallback-path', scope: 'models/default');
final models = await ref.read(modelsProvider.future); final models = await ref.read(modelsProvider.future);
DebugLogger.log(
'models-loaded',
scope: 'models/default',
data: {'count': models.length},
);
if (models.isEmpty) { if (models.isEmpty) {
DebugLogger.warning('no-models', scope: 'models/default'); DebugLogger.warning('no-models', scope: 'models/default');
return null; return null;
@@ -1140,10 +1181,12 @@ Future<Model?> defaultModel(Ref ref) async {
if (!ref.read(isManualModelSelectionProvider)) { if (!ref.read(isManualModelSelectionProvider)) {
ref.read(selectedModelProvider.notifier).set(selectedModel); ref.read(selectedModelProvider.notifier).set(selectedModel);
DebugLogger.log( DebugLogger.log(
'fallback', 'fallback-selected',
scope: 'models/default', scope: 'models/default',
data: {'name': selectedModel.name}, data: {'name': selectedModel.name, 'id': selectedModel.id},
); );
} else {
DebugLogger.log('skip-manual-override', scope: 'models/default');
} }
return selectedModel; return selectedModel;
} catch (e) { } catch (e) {
@@ -1158,24 +1201,46 @@ final backgroundModelLoadProvider = Provider<void>((ref) {
// Ensure API token updater is initialized // Ensure API token updater is initialized
ref.watch(apiTokenUpdaterProvider); ref.watch(apiTokenUpdaterProvider);
// Only run when authenticated, and defer until after first frame // Watch auth state to trigger model loading when authenticated
final navState = ref.read(authNavigationStateProvider); final navState = ref.watch(authNavigationStateProvider);
if (navState != AuthNavigationState.authenticated) { if (navState != AuthNavigationState.authenticated) {
DebugLogger.log('skip-not-authed', scope: 'models/background');
return; return;
} }
// Use a flag to prevent multiple concurrent loads
var isLoading = false;
WidgetsBinding.instance.addPostFrameCallback((_) { WidgetsBinding.instance.addPostFrameCallback((_) {
if (isLoading) return;
isLoading = true;
// Schedule background loading without blocking startup frame // Schedule background loading without blocking startup frame
Future.microtask(() async { Future.microtask(() async {
// Small delay to allow initial build/layout to settle // Reduced delay for faster startup model selection
await Future.delayed(const Duration(milliseconds: 250)); await Future.delayed(const Duration(milliseconds: 100));
if (!ref.mounted) {
DebugLogger.log('cancelled-unmounted', scope: 'models/background');
return;
}
DebugLogger.log('bg-start', scope: 'models/background'); DebugLogger.log('bg-start', scope: 'models/background');
try { try {
await ref.read(defaultModelProvider.future); final model = await ref.read(defaultModelProvider.future);
DebugLogger.log('bg-complete', scope: 'models/background'); if (!ref.mounted) {
DebugLogger.log('complete-unmounted', scope: 'models/background');
return;
}
DebugLogger.log(
'bg-complete',
scope: 'models/background',
data: {'model': model?.name ?? 'null'},
);
} catch (e) { } catch (e) {
DebugLogger.error('bg-failed', scope: 'models/background', error: e); DebugLogger.error('bg-failed', scope: 'models/background', error: e);
} finally {
isLoading = false;
} }
}); });
}); });
@@ -1600,10 +1665,7 @@ Future<List<KnowledgeBase>> knowledgeBases(Ref ref) async {
} }
@riverpod @riverpod
Future<List<KnowledgeBaseItem>> knowledgeBaseItems( Future<List<KnowledgeBaseItem>> knowledgeBaseItems(Ref ref, String kbId) async {
Ref ref,
String kbId,
) async {
// Protected: require authentication // Protected: require authentication
if (!ref.read(isAuthenticatedProvider2)) { if (!ref.read(isAuthenticatedProvider2)) {
DebugLogger.log('skip-unauthed', scope: 'knowledge/items'); DebugLogger.log('skip-unauthed', scope: 'knowledge/items');
@@ -1616,11 +1678,7 @@ Future<List<KnowledgeBaseItem>> knowledgeBaseItems(
final itemsData = await api.getKnowledgeBaseItems(kbId); final itemsData = await api.getKnowledgeBaseItems(kbId);
return itemsData.map((data) => KnowledgeBaseItem.fromJson(data)).toList(); return itemsData.map((data) => KnowledgeBaseItem.fromJson(data)).toList();
} catch (e) { } catch (e) {
DebugLogger.error( DebugLogger.error('knowledge-items-failed', scope: 'knowledge', error: e);
'knowledge-items-failed',
scope: 'knowledge',
error: e,
);
return []; return [];
} }
} }

View File

@@ -1170,11 +1170,43 @@ class _ChatPageState extends ConsumerState<ChatPage> {
), ),
) )
: GestureDetector( : GestureDetector(
onTap: () { onTap: () async {
final modelsAsync = ref.read(modelsProvider); final modelsAsync = ref.read(modelsProvider);
modelsAsync.whenData(
(models) => _showModelDropdown(context, ref, models), // Handle all async states properly
if (modelsAsync.isLoading) {
// If still loading, wait for it to complete
try {
final models = await ref.read(modelsProvider.future);
if (mounted) {
_showModelDropdown(context, ref, models);
}
} catch (e) {
DebugLogger.error(
'model-load-failed',
scope: 'chat/model-selector',
error: e,
); );
}
} else if (modelsAsync.hasValue) {
// If we have data, show immediately
_showModelDropdown(context, ref, modelsAsync.value!);
} else if (modelsAsync.hasError) {
// If there's an error, try to refresh and load
try {
ref.invalidate(modelsProvider);
final models = await ref.read(modelsProvider.future);
if (mounted) {
_showModelDropdown(context, ref, models);
}
} catch (e) {
DebugLogger.error(
'model-refresh-failed',
scope: 'chat/model-selector',
error: e,
);
}
}
}, },
onLongPress: () { onLongPress: () {
final conversation = ref.read(activeConversationProvider); final conversation = ref.read(activeConversationProvider);