@tduncan / Cortana / commits / 54b85f8

Require loaded on-device model for chat target

kisloff committed Sep 21, 2026 at 13:28 UTC 54b85f87b9fb2b3e3d84e494b8c3e4e298141972
2 files changed +57 -20
lib/features/chat/providers/model_selection_providers.dart
+31 -20
@@ -52,39 +52,50 @@ final activeChatTargetProvider = Provider<ActiveChatTarget>((ref) {
52
53 final selected = ref.watch(selectedModelProvider);
54 final selectedForServer = selected?.serverId == server.id ? selected : null;
55 - if (selectedForServer != null) {
55 + if (server.type == ServerType.onDevice) {
56 + final engine = ref.watch(onDeviceEngineProvider);
57 + if (selectedForServer != null &&
58 + engine.status == OnDeviceEngineStatus.loaded &&
59 + engine.loadedModelId == selectedForServer.id) {
60 + return ActiveChatTarget(
61 + server: server,
62 + selectedModel: selectedForServer,
63 + effectiveModelId: selectedForServer.id,
64 + modelLabel: selectedForServer.displayName,
65 + );
66 + }
67 +
68 + final modelId = engine.status == OnDeviceEngineStatus.loaded
69 + ? engine.loadedModelId
70 + : null;
71 + final model = modelId == null
72 + ? null
73 + : OnDeviceModel.allCuratedModels
74 + .where((candidate) => candidate.id == modelId)
75 + .firstOrNull;
76 +
77 return ActiveChatTarget(
78 server: server,
58 - selectedModel: selectedForServer,
59 - effectiveModelId: selectedForServer.id,
60 - modelLabel: selectedForServer.displayName,
79 + selectedModel: null,
80 + effectiveModelId: modelId,
81 + modelLabel: model?.name ?? modelId ?? 'No model',
82 );
83 }
84
64 - if (server.type != ServerType.onDevice) {
85 + if (selectedForServer != null) {
86 return ActiveChatTarget(
87 server: server,
67 - selectedModel: null,
68 - effectiveModelId: 'default',
69 - modelLabel: 'Default model',
88 + selectedModel: selectedForServer,
89 + effectiveModelId: selectedForServer.id,
90 + modelLabel: selectedForServer.displayName,
91 );
92 }
93
73 - final engine = ref.watch(onDeviceEngineProvider);
74 - final modelId = engine.status == OnDeviceEngineStatus.loaded
75 - ? engine.loadedModelId
76 - : null;
77 - final model = modelId == null
78 - ? null
79 - : OnDeviceModel.allCuratedModels
80 - .where((candidate) => candidate.id == modelId)
81 - .firstOrNull;
82 -
94 return ActiveChatTarget(
95 server: server,
96 selectedModel: null,
86 - effectiveModelId: modelId,
87 - modelLabel: model?.name ?? modelId ?? 'No model',
97 + effectiveModelId: 'default',
98 + modelLabel: 'Default model',
99 );
100 });
101
test/features/chat/providers/active_chat_target_provider_test.dart
+26
@@ -2,6 +2,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
2 import 'package:flutter_test/flutter_test.dart';
3 import 'package:localmind/core/models/enums.dart';
4 import 'package:localmind/features/chat/providers/model_selection_providers.dart';
5 +import 'package:localmind/features/models/data/models/model_info.dart';
6 import 'package:localmind/features/on_device/providers/on_device_providers.dart';
7 import 'package:localmind/features/servers/data/models/server.dart';
8 import 'package:localmind/features/servers/providers/server_providers.dart';
@@ -39,6 +40,31 @@ void main() {
40 expect(target.modelLabel, 'No model');
41 });
42
43 + test('does not treat a selected on-device model as ready until loaded', () {
44 + final container = ProviderContainer(
45 + overrides: [
46 + activeServerProvider.overrideWith(_OnDeviceServerNotifier.new),
47 + onDeviceEngineProvider.overrideWith(_EmptyEngineNotifier.new),
48 + ],
49 + );
50 + addTearDown(container.dispose);
51 +
52 + container.read(selectedModelProvider.notifier).setModel(
53 + ModelInfo(
54 + id: 'apple-foundation-models',
55 + name: 'Apple Foundation Models',
56 + serverType: ServerType.onDevice,
57 + serverId: 'on-device',
58 + ),
59 + );
60 +
61 + final target = container.read(activeChatTargetProvider);
62 +
63 + expect(target.isReady, isFalse);
64 + expect(target.effectiveModelId, isNull);
65 + expect(target.modelLabel, 'No model');
66 + });
67 +
68 test('keeps default routing for a selected remote server', () {
69 final container = ProviderContainer(
70 overrides: [activeServerProvider.overrideWith(_RemoteServerNotifier.new)],