1 // Copyright 2026 Splitfire AB (Onde Inference). All rights reserved.
2 // SPDX-License-Identifier: MIT OR Apache-2.0
3
4 // ignore_for_file: prefer_const_constructors
5
6 import 'dart:async';
7 import 'dart:io' show Platform;
8
9 import 'package:flutter/services.dart';
10 import 'package:flutter_rust_bridge/flutter_rust_bridge_for_generated.dart'
11 show ExternalLibrary;
12
13 import 'frb_generated.dart/frb_generated.dart' as frb;
14 import 'frb_generated.dart/api.dart' as api;
15 import 'types.dart';
16
17 export 'frb_generated.dart/frb_generated.dart' show RustLib;
18 export 'frb_generated.dart/api.dart'
19 show
20 OndeChatEngine,
21 OndeError,
22 OndeError_NoModelLoaded,
23 OndeError_AlreadyLoaded,
24 OndeError_ModelBuild,
25 OndeError_Inference,
26 OndeError_Cancelled,
27 OndeError_Other,
28 configureCacheDir;
29
30 // ---------------------------------------------------------------------------
31 // OndeChatEngine convenience wrapper
32 // ---------------------------------------------------------------------------
33
34 /// High-level wrapper around the FRB-generated [api.OndeChatEngine] opaque type.
35 ///
36 /// Provides:
37 /// - [OndeChatEngineX.loadDefaultModel]: convenience method not present in generated code
38 /// - [OndeChatEngineX.clearHistoryCount]: returns `int` instead of `BigInt`
39 /// - [OndeInference]: static initialisation plus config and sampling helpers
40 ///
41 /// ## Lifecycle
42 ///
43 /// ```dart
44 /// // 1. Initialise once at app startup (idempotent).
45 /// await OndeInference.init();
46 ///
47 /// // 2. Create a native engine instance (sync, no Future).
48 /// final engine = OndeChatEngine();
49 ///
50 /// // 3a. Load your assigned model (recommended for production).
51 /// await engine.loadAssignedModel(
52 /// appId: 'your-app-id', // from ondeinference.com
53 /// appSecret: 'your-app-secret',
54 /// systemPrompt: 'You are a helpful assistant.',
55 /// );
56 ///
57 /// // 3b. Or load the platform default (for prototyping).
58 /// // await engine.loadDefaultModel(
59 /// // systemPrompt: 'You are a helpful assistant.',
60 /// // );
61 ///
62 /// // 4. Chat.
63 /// final result = await engine.sendMessage(message: 'Hello!');
64 /// print(result.text);
65 ///
66 /// // 5. Streaming.
67 /// await for (final chunk in engine.streamMessage(message: 'Tell me a story.')) {
68 /// stdout.write(chunk.delta);
69 /// if (chunk.done) break;
70 /// }
71 ///
72 /// // 6. Clean up.
73 /// await engine.unloadModel();
74 /// ```
75 ///
76 /// ## Thread safety
77 ///
78 /// [OndeChatEngine] is backed by an `Arc`-wrapped Rust object and is safe to
79 /// reference from multiple Dart isolates. Concurrent inference calls are
80 /// serialised internally.
81 extension OndeChatEngineX on api.OndeChatEngine {
82 // -------------------------------------------------------------------------
83 // loadDefaultModel: implemented in Dart, not in the generated bridge.
84 // -------------------------------------------------------------------------
85
86 /// Loads the platform-appropriate default model.
87 ///
88 /// On iOS, tvOS, and Android the Qwen 2.5 1.5B (Q4_K_M, ~941 MB) model is
89 /// used. On macOS, Windows, and Linux the Qwen 2.5 3B (Q4_K_M, ~1.93 GB)
90 /// model is used.
91 ///
92 /// Delegates to [loadGgufModel] with [OndeInference.defaultModelConfig].
93 ///
94 /// [systemPrompt] replaces the engine's system prompt before loading.
95 /// [sampling] overrides the default sampling parameters.
96 ///
97 /// Returns the wall-clock seconds taken to load the model.
98 ///
99 /// Throws [OndeError] if the model cannot be downloaded or loaded.
100 Future<double> loadDefaultModel({
101 String? systemPrompt,
102 SamplingConfig? sampling,
103 }) => loadGgufModel(
104 config: api.defaultModelConfig(),
105 systemPrompt: systemPrompt,
106 sampling: sampling,
107 );
108
109 // -------------------------------------------------------------------------
110 // clearHistoryCount: int-typed convenience over clearHistory() -> BigInt.
111 // -------------------------------------------------------------------------
112
113 /// Clears the conversation history and returns the count as a plain [int].
114 ///
115 /// The generated [clearHistory] returns `Future<BigInt>`; this helper
116 /// downcasts it so callers never need to handle [BigInt] directly.
117 Future<int> clearHistoryCount() async => (await clearHistory()).toInt();
118 }
119
120 // ---------------------------------------------------------------------------
121 // OndeInference static SDK helpers
122 // ---------------------------------------------------------------------------
123
124 /// Static helper namespace for Onde SDK initialisation and configuration.
125 ///
126 /// ```dart
127 /// // Initialise before creating any OndeChatEngine.
128 /// await OndeInference.init();
129 ///
130 /// // Obtain a model config.
131 /// final config = OndeInference.defaultModelConfig();
132 ///
133 /// // Obtain sampling parameters.
134 /// final sampling = OndeInference.deterministicSamplingConfig();
135 /// ```
136 abstract final class OndeInference {
137 // -------------------------------------------------------------------------
138 // Initialisation
139 // -------------------------------------------------------------------------
140
141 /// Initialises the Rust shared library.
142 ///
143 /// Must be called before creating any [OndeChatEngine]. Subsequent calls
144 /// are safe no-ops. The library is only initialised once per process.
145 ///
146 /// Call this in `main()` or in a Flutter `initState` override before any
147 /// user interaction that could trigger model loading.
148 ///
149 /// On macOS the Rust static library is force-loaded into the
150 /// `onde_inference.framework` CocoaPods dynamic framework, so we must
151 /// `dlopen` that framework by name.
152 ///
153 /// On iOS the pod is statically linked into the Runner executable
154 /// (no separate `.framework` file exists at runtime), so we use
155 /// `DynamicLibrary.process()` (`RTLD_DEFAULT`) which searches the
156 /// main executable and all loaded images.
157 ///
158 /// On Android, Linux, and Windows, the default FRB stem-based loader
159 /// finds the `.so` or `.dll` automatically. No override needed.
160 static Future<void> init() async {
161 ExternalLibrary? lib;
162 if (Platform.isMacOS) {
163 // With CocoaPods + use_frameworks!, the pod is a dynamic framework.
164 // DynamicLibrary.process() cannot resolve symbols inside it.
165 lib = ExternalLibrary.open('onde_inference.framework/onde_inference');
166 } else if (Platform.isIOS) {
167 // On iOS, CocoaPods statically links the pod into the Runner binary
168 // through -force_load, so the symbols live in the main executable.
169 lib = ExternalLibrary.process(iKnowHowToUseIt: true);
170 }
171 await frb.RustLib.init(externalLibrary: lib);
172 }
173
174 // -------------------------------------------------------------------------
175 // Sandbox / cache setup
176 // -------------------------------------------------------------------------
177
178 /// Method channel shared with the native iOS/macOS plugin.
179 static const _channel = MethodChannel('com.ondeinference.onde_inference');
180
181 /// Configures the HuggingFace model cache for sandboxed platforms.
182 ///
183 /// On **iOS and macOS** this first tries to resolve the App Group shared
184 /// container (`group.com.ondeinference.apps`) via a native method channel
185 /// so all Onde-powered apps share downloaded models. If the App Group is
186 /// unavailable (entitlement missing, group not registered), it falls back
187 /// to [fallbackDir] if provided.
188 ///
189 /// On **Android** there is no App Group equivalent. Pass [fallbackDir]
190 /// (e.g. from `getApplicationSupportDirectory().path`).
191 ///
192 /// On **desktop Linux / Windows** this is a no-op. The default
193 /// `~/.cache/huggingface` works without intervention.
194 ///
195 /// Call **once** at startup, after [init] and before any model load:
196 ///
197 /// ```dart
198 /// await OndeInference.init();
199 /// await OndeInference.setupCacheDir();
200 /// // or with explicit fallback:
201 /// final dir = await getApplicationSupportDirectory();
202 /// await OndeInference.setupCacheDir(fallbackDir: dir.path);
203 /// ```
204 static Future<void> setupCacheDir({String? fallbackDir}) async {
205 // 1. On Apple platforms, try the shared App Group container first.
206 if (Platform.isIOS || Platform.isMacOS) {
207 try {
208 final String? groupPath = await _channel.invokeMethod<String>(
209 'getAppGroupContainerPath',
210 );
211 if (groupPath != null && groupPath.isNotEmpty) {
212 api.configureCacheDir(appDataDir: groupPath);
213 return;
214 }
215 } catch (_) {
216 // Method channel not available (e.g. plugin not registered).
217 // Fall through to fallbackDir.
218 }
219 }
220
221 // 2. Otherwise use the caller-provided directory in the app sandbox.
222 if (fallbackDir != null && fallbackDir.isNotEmpty) {
223 api.configureCacheDir(appDataDir: fallbackDir);
224 }
225 }
226
227 // -------------------------------------------------------------------------
228 // Model config factories
229 // -------------------------------------------------------------------------
230
231 /// Platform-appropriate default model config.
232 ///
233 /// Selects Qwen 2.5 1.5B on iOS / tvOS / Android and Qwen 2.5 3B on
234 /// macOS / Windows / Linux. Delegates to the Rust `default_model_config`
235 /// free function so the platform check runs natively.
236 static GgufModelConfig defaultModelConfig() => api.defaultModelConfig();
237
238 /// Builds a configuration for a pre-quantised UQFF model.
239 ///
240 /// [modelId] identifies the base repository or local model directory used
241 /// for tokenizer and configuration resolution. For sharded models, pass the
242 /// first shard in [files]; mistral.rs discovers sibling shards.
243 static UqffModelConfig uqffModelConfig({
244 required String modelId,
245 required List<String> files,
246 required String displayName,
247 required String approxMemory,
248 String? chatTemplate,
249 }) => api.uqffModelConfig(
250 modelId: modelId,
251 files: files,
252 displayName: displayName,
253 approxMemory: approxMemory,
254 chatTemplate: chatTemplate,
255 );
256
257 /// Qwen 2.5 1.5B Instruct (GGUF Q4_K_M, ~941 MB).
258 ///
259 /// Suitable for iOS, tvOS, and Android where available memory is limited.
260 static GgufModelConfig qwen2515bConfig() => api.qwen2515BConfig();
261
262 /// Qwen 2.5 3B Instruct (GGUF Q4_K_M, ~1.93 GB).
263 ///
264 /// Suitable for macOS, Windows, and Linux desktop deployments.
265 static GgufModelConfig qwen253bConfig() => api.qwen253BConfig();
266
267 /// Qwen 2.5 Coder 1.5B Instruct (GGUF Q4_K_M, ~941 MB).
268 ///
269 /// Optimised for code-generation tasks on memory-constrained devices.
270 static GgufModelConfig qwen25Coder15bConfig() => api.qwen25Coder15BConfig();
271
272 /// Qwen 2.5 Coder 3B Instruct (GGUF Q4_K_M, ~1.93 GB).
273 ///
274 /// Optimised for code-generation tasks on macOS and Linux desktop.
275 static GgufModelConfig qwen25Coder3bConfig() => api.qwen25Coder3BConfig();
276
277 /// Qwen 3 0.6B (GGUF Q4_K_M, ~0.5 GB).
278 static GgufModelConfig qwen306bConfig() => api.qwen306BConfig();
279
280 /// Qwen 3 1.7B (GGUF Q4_K_M, ~1.3 GB).
281 static GgufModelConfig qwen317bConfig() => api.qwen317BConfig();
282
283 /// Qwen 3 4B (GGUF Q4_K_M, ~2.5 GB).
284 static GgufModelConfig qwen34bConfig() => api.qwen34BConfig();
285
286 /// Qwen 3 8B (GGUF Q4_K_M, ~5 GB).
287 static GgufModelConfig qwen38bConfig() => api.qwen38BConfig();
288
289 /// Qwen 3 14B (GGUF Q4_K_M, ~8.4 GB).
290 static GgufModelConfig qwen314bConfig() => api.qwen314BConfig();
291
292 /// Qwen 3 32B (GGUF Q4_K_M, ~19.8 GB).
293 static GgufModelConfig qwen332bConfig() => api.qwen332BConfig();
294
295 /// Qwen 3 4B Instruct 2507, the non-thinking checkpoint (~2.5 GB).
296 static GgufModelConfig qwen34bInstruct2507Config() =>
297 api.qwen34BInstruct2507Config();
298
299 /// Qwen 3 4B Thinking 2507 (~2.5 GB).
300 static GgufModelConfig qwen34bThinking2507Config() =>
301 api.qwen34BThinking2507Config();
302
303 /// Qwen 3 30B-A3B Instruct 2507 mixture-of-experts (~18.6 GB).
304 static GgufModelConfig qwen330bA3bInstruct2507Config() =>
305 api.qwen330BA3BInstruct2507Config();
306
307 // -------------------------------------------------------------------------
308 // Sampling config factories
309 // -------------------------------------------------------------------------
310
311 /// Balanced defaults: `temperature=0.7`, `topP=0.95`, `maxTokens=512`.
312 ///
313 /// Good for general-purpose conversational chat.
314 static SamplingConfig defaultSamplingConfig() => api.defaultSamplingConfig();
315
316 /// Greedy / fully deterministic decoding: `temperature=0.0`, `maxTokens=512`.
317 ///
318 /// Use for coding, fact-retrieval, or reproducibility-sensitive tasks.
319 static SamplingConfig deterministicSamplingConfig() =>
320 api.deterministicSamplingConfig();
321
322 /// Low-latency preset for memory-constrained mobile:
323 /// `temperature=0.7`, `topP=0.95`, `maxTokens=128`.
324 static SamplingConfig mobileSamplingConfig() => api.mobileSamplingConfig();
325 }