3 files changed
+203
-12
lib/features/chat/providers/chat_notifier.dart
+31
-11
@@ -704,8 +704,8 @@ class ChatNotifier extends Notifier<ChatState> {
704
return;
705
}
706
707
- final server = ref.read(activeServerProvider);
708
- final selectedModel = ref.read(selectedModelProvider);
707
+ final target = ref.read(activeChatTargetProvider);
708
+ final server = target.server;
709
final settings = ref.read(settingsProvider);
710
if (server == null) return;
711
@@ -730,7 +730,7 @@ class ChatNotifier extends Notifier<ChatState> {
730
.createConversation(
731
title: initialTitle,
732
serverId: server.id,
733
- modelId: selectedModel?.id,
733
+ modelId: target.effectiveModelId,
734
personaId: preselected.isEmpty
735
? null
736
: PersonaPromptUtils.joinPersonaIds(
@@ -1927,6 +1927,10 @@ class ChatNotifier extends Notifier<ChatState> {
1927
if (assistant.role != MessageRole.assistant) return;
1928
if (state.isStreaming) return;
1929
1930
+ final target = ref.read(activeChatTargetProvider);
1931
+ final effectiveModelId = target.effectiveModelId;
1932
+ if (target.server == null || effectiveModelId == null) return;
1933
+
1934
final streamingAssistant = assistant.copyWith(
1935
status: MessageStatus.streaming,
1936
isProcessing: true,
@@ -1949,7 +1953,8 @@ class ChatNotifier extends Notifier<ChatState> {
1953
1954
await _runAssistantStream(
1955
streamingAssistant,
1952
- ref.read(selectedModelProvider),
1956
+ target.selectedModel,
1957
+ effectiveModelId: effectiveModelId,
1958
continueGeneration: true,
1959
);
1960
}
@@ -2042,9 +2047,15 @@ class ChatNotifier extends Notifier<ChatState> {
2047
required int threadOrder,
2048
required int variantIndex,
2049
}) async {
2045
- final selectedModel = ref.read(selectedModelProvider);
2046
- final server = ref.read(activeServerProvider);
2047
- if (server == null || _activeConversationId == null) return;
2050
+ final target = ref.read(activeChatTargetProvider);
2051
+ final selectedModel = target.selectedModel;
2052
+ final server = target.server;
2053
+ final effectiveModelId = target.effectiveModelId;
2054
+ if (server == null ||
2055
+ effectiveModelId == null ||
2056
+ _activeConversationId == null) {
2057
+ return;
2058
+ }
2059
2060
await _abortStreamImmediately();
2061
if (!ref.mounted) return;
@@ -2056,7 +2067,7 @@ class ChatNotifier extends Notifier<ChatState> {
2067
content: '',
2068
createdAt: DateTime.now(),
2069
status: MessageStatus.streaming,
2059
- modelId: selectedModel?.id,
2070
+ modelId: effectiveModelId,
2071
variantGroupId: variantGroupId,
2072
variantIndex: variantIndex,
2073
threadOrder: threadOrder,
@@ -2079,7 +2090,11 @@ class ChatNotifier extends Notifier<ChatState> {
2090
_resetCheckpointMetrics();
2091
_updateSavedMetrics(assistantMessage);
2092
2082
- await _runAssistantStream(assistantMessage, selectedModel);
2093
+ await _runAssistantStream(
2094
+ assistantMessage,
2095
+ selectedModel,
2096
+ effectiveModelId: effectiveModelId,
2097
+ );
2098
}
2099
2100
/// After successful tool execution (issue #77), build `tool` role messages
@@ -2216,12 +2231,17 @@ class ChatNotifier extends Notifier<ChatState> {
2231
// call, the same onDone flow will execute it again until the model
2232
// yields a final answer or `maxIterations` (enforced inside
2233
// `ToolExecutionLoop`) is reached.
2219
- await _runAssistantStream(continuationMessage, selectedModel);
2234
+ await _runAssistantStream(
2235
+ continuationMessage,
2236
+ selectedModel,
2237
+ effectiveModelId: effectiveModelId,
2238
+ );
2239
}
2240
2241
Future<void> _runAssistantStream(
2242
Message assistantMessage,
2243
ModelInfo? selectedModel, {
2244
+ required String effectiveModelId,
2245
bool continueGeneration = false,
2246
}) async {
2247
final server = ref.read(activeServerProvider);
@@ -2287,7 +2307,7 @@ class ChatNotifier extends Notifier<ChatState> {
2307
_streamSubscription = chatService
2308
.sendMessage(
2309
server: server,
2290
- modelId: selectedModel?.id ?? 'default',
2310
+ modelId: effectiveModelId,
2311
messages: messagesForApi,
2312
params: chatParams,
2313
integrations: integrations,
pubspec.yaml
+1
-1
@@ -16,7 +16,7 @@ publish_to: "none" # Remove this line if you wish to publish to pub.dev
16
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
17
# In Windows, build-name is used as the major, minor, and patch parts
18
# of the product and file versions while build-number is used as the build suffix.
19
-version: 1.8.2+88
19
+version: 1.8.3+89
20
21
environment:
22
sdk: ">=3.12.0 <4.0.0"
test/features/chat/providers/chat_notifier_effective_model_test.dart
new
+171
@@ -0,0 +1,171 @@
1
+import 'dart:async';
2
+
3
+import 'package:flutter_riverpod/flutter_riverpod.dart';
4
+import 'package:flutter_test/flutter_test.dart';
5
+import 'package:localmind/core/models/enums.dart';
6
+import 'package:localmind/core/providers/app_providers.dart';
7
+import 'package:localmind/core/providers/chat_background_service_provider.dart';
8
+import 'package:localmind/core/services/chat_background_service.dart';
9
+import 'package:localmind/features/chat/data/chat_service.dart';
10
+import 'package:localmind/features/chat/data/models/chat_parameters.dart';
11
+import 'package:localmind/features/chat/data/models/message.dart';
12
+import 'package:localmind/features/chat/data/tools/tool_definition.dart';
13
+import 'package:localmind/features/chat/data/tools/tool_registry.dart';
14
+import 'package:localmind/features/chat/providers/chat_mcp_providers.dart';
15
+import 'package:localmind/features/chat/providers/chat_notifier.dart';
16
+import 'package:localmind/features/chat/providers/chat_params_providers.dart';
17
+import 'package:localmind/features/chat/providers/chat_service_providers.dart';
18
+import 'package:localmind/features/chat/providers/model_selection_providers.dart';
19
+import 'package:localmind/features/chat/providers/tooling_providers.dart';
20
+import 'package:localmind/features/models/data/models/model_info.dart';
21
+import 'package:localmind/features/on_device/data/models/on_device_model.dart';
22
+import 'package:localmind/features/on_device/providers/on_device_providers.dart';
23
+import 'package:localmind/features/servers/data/models/server.dart';
24
+import 'package:localmind/features/servers/providers/server_providers.dart';
25
+import 'package:localmind/features/settings/data/models/app_settings.dart';
26
+import 'package:localmind/features/voice_mode/providers/voice_mode_provider.dart';
27
+
28
+void main() {
29
+ test(
30
+ 'continues with the loaded model when selection metadata is unavailable',
31
+ () async {
32
+ final chatService = _RecordingChatService();
33
+ final container = ProviderContainer(
34
+ overrides: [
35
+ activeServerProvider.overrideWith(_OnDeviceServerNotifier.new),
36
+ onDeviceEngineProvider.overrideWith(_LoadedLlamaEngineNotifier.new),
37
+ selectedModelProvider.overrideWith(_UnselectedModelNotifier.new),
38
+ chatProvider.overrideWith(_FallbackChatNotifier.new),
39
+ chatServiceProvider.overrideWithValue(chatService),
40
+ chatParamsProvider.overrideWithValue(ChatParameters.defaults()),
41
+ chatMcpConfigProvider.overrideWith(
42
+ _DisabledChatMcpConfigNotifier.new,
43
+ ),
44
+ toolRegistryProvider.overrideWithValue(
45
+ ToolRegistry(providers: const []),
46
+ ),
47
+ chatBackgroundServiceProvider.overrideWithValue(
48
+ _TestChatBackgroundService(),
49
+ ),
50
+ settingsProvider.overrideWith(_TestSettingsNotifier.new),
51
+ voiceModeProvider.overrideWith(_IdleVoiceModeNotifier.new),
52
+ ],
53
+ );
54
+ addTearDown(container.dispose);
55
+
56
+ await container
57
+ .read(chatProvider.notifier)
58
+ .continueFromMessage('assistant-1');
59
+
60
+ expect(await chatService.requestedModelId.future, 'imported-gguf-model');
61
+ },
62
+ );
63
+}
64
+
65
+class _FallbackChatNotifier extends ChatNotifier {
66
+ @override
67
+ ChatState build() {
68
+ final messages = [
69
+ Message(
70
+ id: 'user-1',
71
+ conversationId: 'conversation-1',
72
+ role: MessageRole.user,
73
+ content: 'Hello',
74
+ createdAt: DateTime.utc(2026, 9, 22),
75
+ ),
76
+ Message(
77
+ id: 'assistant-1',
78
+ conversationId: 'conversation-1',
79
+ role: MessageRole.assistant,
80
+ content: 'Hello there.',
81
+ createdAt: DateTime.utc(2026, 9, 22),
82
+ ),
83
+ ];
84
+ return ChatState(
85
+ messages: messages,
86
+ allMessages: messages,
87
+ isTemporary: true,
88
+ );
89
+ }
90
+}
91
+
92
+class _RecordingChatService implements ChatService {
93
+ final requestedModelId = Completer<String>();
94
+
95
+ @override
96
+ Stream<ChatResponse> sendMessage({
97
+ required Server server,
98
+ required String modelId,
99
+ required List<Message> messages,
100
+ required ChatParameters params,
101
+ List<McpIntegration>? integrations,
102
+ List<ToolDefinition>? tools,
103
+ bool continueGeneration = false,
104
+ }) async* {
105
+ if (!requestedModelId.isCompleted) {
106
+ requestedModelId.complete(modelId);
107
+ }
108
+ yield const ChatResponse(
109
+ type: ChatResponseType.error,
110
+ content: 'Expected test failure',
111
+ );
112
+ }
113
+
114
+ @override
115
+ void cancelStream() {}
116
+}
117
+
118
+class _DisabledChatMcpConfigNotifier extends ChatMcpConfigNotifier {
119
+ @override
120
+ ChatMcpConfig build() => const ChatMcpConfig(enabled: false);
121
+}
122
+
123
+class _TestChatBackgroundService extends ChatBackgroundService {
124
+ @override
125
+ Future<void> start() async {}
126
+
127
+ @override
128
+ Future<void> stop() async {}
129
+}
130
+
131
+class _OnDeviceServerNotifier extends ActiveServerNotifier {
132
+ @override
133
+ Server? build() => Server(
134
+ id: 'on-device',
135
+ name: 'On-Device',
136
+ type: ServerType.onDevice,
137
+ host: '',
138
+ port: 0,
139
+ createdAt: DateTime.utc(2026, 9, 22),
140
+ lastConnectedAt: DateTime.utc(2026, 9, 22),
141
+ status: ConnectionStatus.connected,
142
+ );
143
+}
144
+
145
+class _LoadedLlamaEngineNotifier extends OnDeviceEngineNotifier {
146
+ @override
147
+ OnDeviceEngineState build() => const OnDeviceEngineState(
148
+ status: OnDeviceEngineStatus.loaded,
149
+ loadedModelId: 'imported-gguf-model',
150
+ loadedRuntime: OnDeviceModelRuntime.llamaCpp,
151
+ );
152
+}
153
+
154
+class _UnselectedModelNotifier extends SelectedModelNotifier {
155
+ @override
156
+ ModelInfo? build() => null;
157
+}
158
+
159
+class _TestSettingsNotifier extends SettingsNotifier {
160
+ @override
161
+ AppSettings build() => AppSettings(
162
+ mcpEnabled: false,
163
+ newChatMcpEnabled: false,
164
+ resumeLastChat: false,
165
+ );
166
+}
167
+
168
+class _IdleVoiceModeNotifier extends VoiceModeNotifier {
169
+ @override
170
+ VoiceModeState build() => const VoiceModeState();
171
+}