@ondeinference / onde / commits / af44930

Onde SDK v1

keypair34 committed Apr 26, 2026 at 15:16 UTC af4493008184e4bae874ac2aaf6a2bd10d6d34a7
18 files changed +440 -111
CHANGELOG.md new
+85
@@ -0,0 +1,85 @@
1 +## 1.0.0
2 +
3 +Onde has been running in production across multiple Splitfire AB apps on the App Store for months. The `0.x` versioning wasn't reflecting reality anymore.
4 +
5 +### Assigned model loading (FFI)
6 +
7 +`load_assigned_model` is now exposed through the UniFFI layer. Swift and Kotlin consumers can call it directly — the method hardcodes `Environment::Production` so SDK consumers don't need to know about the internal environment enum. Pass your `app_id` and `app_secret` from [ondeinference.com](https://ondeinference.com), and the SDK fetches whatever model you've assigned in the dashboard. Falls back to the platform default if nothing's assigned.
8 +
9 +### New models
10 +
11 +* Qwen 3 1.7B, 4B, 8B, and 14B (GGUF Q4_K_M)
12 +* Qwen 2.5 Coder 1.5B, 3B, and 7B (GGUF Q4_K_M)
13 +* DeepSeek Coder 6.7B (GGUF Q4_K_M) — ships with a bundled chat template since the GGUF doesn't include one
14 +
15 +### Type changes
16 +
17 +* `GgufModelConfig` has a new `chat_template: Option<String>` field for models that need a custom template.
18 +* `InferenceResult` now carries `tool_calls: Vec<ToolCallInfo>`. Empty for most responses. When a model requests a tool call, you get structured data instead of raw markup in `text`.
19 +* New types: `ToolCallInfo`, `ToolDefinition`, `ToolResult`. The last two are Rust-only for now — they'll hit the FFI surface when `send_message_with_tools` is wired up.
20 +
21 +### Engine
22 +
23 +* Model weights are dropped outside the write lock during model swaps. Status queries no longer block while the old model's memory is being freed.
24 +* `platform_default()` now returns Coder variants (Qwen 2.5 Coder 1.5B on mobile, Coder 3B on desktop).
25 +
26 +### Tool calling (Rust-native only)
27 +
28 +* `send_message_with_tools`, `send_tool_results`, and `stream_tool_results` on `ChatEngine`. These are available to direct Rust consumers (like [siGit](https://github.com/smbcloud/sigit)) but not yet exposed through UniFFI. The types cross the FFI boundary on the output side (`InferenceResult.tool_calls`), just not the input side yet.
29 +
30 +### Dependencies
31 +
32 +* Switched from git-based `mistralrs` to published `onde-mistralrs 0.8.2` crates on crates.io. No more `[patch.crates-io]` gymnastics for `cargo publish`.
33 +
34 +### Cross-platform
35 +
36 +* Linux and Windows CPU builds work properly now — fixed `TokenSource` for non-Darwin platforms.
37 +* Added cross-compile CI for all supported targets.
38 +
39 +## 0.1.8
40 +
41 +* Published to crates.io with `onde-mistralrs 0.8.2` registry dependencies (replacing git refs that `cargo publish` strips).
42 +
43 +## 0.1.7
44 +
45 +* Qwen 3 4B GGUF model with OpenAI-compatible tool calling.
46 +* `GgufModelConfig::qwen3_4b()` constructor.
47 +
48 +## 0.1.6
49 +
50 +* License fix: canonical MIT text so crates.io recognizes the license correctly.
51 +
52 +## 0.1.5
53 +
54 +* `ChatEngine::load_assigned_model()` — Rust-native API for fetching operator-assigned models from the GresIQ backend.
55 +* GresIQ pulse telemetry client (build-time credential embedding via `dotenvy`).
56 +* watchOS and visionOS platform support.
57 +
58 +## 0.1.4
59 +
60 +* Switched all `mistralrs` dependencies to the `fix/all-platform-fixes` fork for cross-platform stability.
61 +* Dual-licensed MIT OR Apache-2.0.
62 +
63 +## 0.1.3
64 +
65 +* Added watchOS and visionOS targets.
66 +
67 +## 0.1.2
68 +
69 +* HF cache sandbox workaround for iOS and Android (`GLOBAL_HF_CACHE` seeding).
70 +* `configure_cache_dir` free function for sandboxed platforms.
71 +
72 +## 0.1.1
73 +
74 +* CI/CD: `release-sdk-swift.yml` builds the XCFramework and auto-updates `onde-swift` on tag push.
75 +
76 +## 0.1.0
77 +
78 +* Initial release.
79 +* Multi-turn chat with Qwen 2.5 1.5B and 3B GGUF models.
80 +* Metal on iOS/macOS, CPU on Android/Linux/Windows.
81 +* Streaming inference via `tokio::sync::mpsc`.
82 +* Conversation history management.
83 +* One-shot `generate()` API.
84 +* Configurable sampling with presets.
85 +* UniFFI bindings for Swift/Kotlin.
\ No newline at end of file
Cargo.lock
+1 -1
@@ -3214,7 +3214,7 @@ checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe"
3214
3215 [[package]]
3216 name = "onde"
3217 -version = "0.1.8"
3217 +version = "1.0.0"
3218 dependencies = [
3219 "anyhow",
3220 "cc",
Cargo.toml
+1 -1
@@ -1,6 +1,6 @@
1 [package]
2 name = "onde"
3 -version = "0.1.8"
3 +version = "1.0.0"
4 edition = "2021"
5 description = "On-device inference engine for Apple silicon."
6 license = "MIT OR Apache-2.0"
sdk/dart/CHANGELOG.md
+33
@@ -1,3 +1,36 @@
1 +## 1.0.0
2 +
3 +This is the first stable release. Onde has been running in production across multiple Splitfire AB apps on the App Store for months now — the `0.x` versioning stopped making sense a while ago.
4 +
5 +### New: assigned model loading
6 +
7 +`loadAssignedModel(appId:appSecret:)` fetches whatever model you've assigned to your app in the [ondeinference.com](https://ondeinference.com) dashboard. If you haven't assigned anything yet, it falls back to the platform default. This is the recommended path for production apps — `loadDefaultModel` is still there for prototyping.
8 +
9 +The example app picks up credentials via `--dart-define=ONDE_APP_ID=...` and `--dart-define=ONDE_APP_SECRET=...`. Without them, it falls back to the default model like before.
10 +
11 +### New models
12 +
13 +* Qwen 3 8B, 14B, and 1.7B (GGUF Q4_K_M)
14 +* Qwen 2.5 Coder 7B (GGUF Q4_K_M)
15 +* DeepSeek Coder 6.7B (GGUF Q4_K_M) with bundled chat template
16 +
17 +### Type changes
18 +
19 +* `GgufModelConfig` has a new optional `chatTemplate` field for models that need a custom chat template (DeepSeek Coder uses this).
20 +* `InferenceResult` now carries a `toolCalls` list (`List<ToolCallInfo>`). Most responses return an empty list — but if the model decides to request a tool call, the structured data is there instead of raw markup in `text`. The `InferenceResultToolsX` extension adds a `hasToolCalls` convenience getter.
21 +
22 +### Engine
23 +
24 +* Old model weights are now dropped outside the lock when loading a new model. Previously the drop happened while holding the write lock, which blocked status queries during the memory release.
25 +
26 +### Dependencies
27 +
28 +* Switched from git-based `mistralrs` to the published `onde-mistralrs 0.8.2` crates on crates.io. Builds are faster and `cargo publish` works without `[patch.crates-io]` gymnastics.
29 +
30 +### Cross-platform
31 +
32 +* Linux and Windows builds with CPU inference now work out of the box (TokenSource fix for non-Darwin platforms).
33 +
34 ## 0.1.7
35
36 * **Fix:** Removed `example/android/app/src/main/java/io/flutter/plugins/GeneratedPluginRegistrant.java` from git tracking and added it to `.gitignore`. This file is regenerated by `flutter pub get` on every CI run, causing a dirty working tree that blocked `pub publish`. Added a CI restore step as an additional safety net.
sdk/dart/README.md
+3 -3
@@ -265,6 +265,6 @@ Dual-licensed under **MIT** and **Apache 2.0**. Pick whichever works for you.
265
266 ---
267
268 -<p align="center">
269 - <sub>© 2026 <a href="https://ondeinference.com">Onde Inference</a></sub>
270 -</p>
\ No newline at end of file
268 +## Copyright
269 +
270 +© 2026 [Onde Inference](https://ondeinference.com) (Splitfire AB).
sdk/dart/example/lib/main.dart
+23 -5
@@ -18,7 +18,15 @@ import 'dart:io' show Platform;
18
19 import 'package:flutter/material.dart';
20 import 'package:onde_inference/onde_inference.dart';
21 -import 'package:path_provider/path_provider.dart' show getApplicationSupportDirectory;
21 +import 'package:path_provider/path_provider.dart'
22 + show getApplicationSupportDirectory;
23 +
24 +// ── Onde app credentials ──────────────────────────────────────────────────
25 +// Register your app at https://ondeinference.com to get these.
26 +// The dashboard lets you assign a model to your app — the SDK fetches
27 +// it automatically. If no model is assigned, the platform default is used.
28 +const String ondeAppId = String.fromEnvironment('ONDE_APP_ID');
29 +const String ondeAppSecret = String.fromEnvironment('ONDE_APP_SECRET');
30
31 // ---------------------------------------------------------------------------
32 // Entry point
@@ -193,10 +201,20 @@ class _ChatScreenState extends State<ChatScreen> {
201 _errorBanner = null;
202 });
203 try {
196 - final elapsed = await _engine.loadDefaultModel(
197 - systemPrompt: 'You are a helpful, concise assistant.',
198 - sampling: _samplingPreset.config,
199 - );
204 + final double elapsed;
205 + if (ondeAppId.isNotEmpty && ondeAppSecret.isNotEmpty) {
206 + elapsed = await _engine.loadAssignedModel(
207 + appId: ondeAppId,
208 + appSecret: ondeAppSecret,
209 + systemPrompt: 'You are a helpful, concise assistant.',
210 + sampling: _samplingPreset.config,
211 + );
212 + } else {
213 + elapsed = await _engine.loadDefaultModel(
214 + systemPrompt: 'You are a helpful, concise assistant.',
215 + sampling: _samplingPreset.config,
216 + );
217 + }
218 final info = await _engine.info();
219 setState(() {
220 _isModelLoading = false;
sdk/dart/lib/src/engine.dart
+15 -11
@@ -47,11 +47,18 @@ export 'frb_generated.dart/api.dart'
47 /// // 2. Create a native engine instance (sync — no Future).
48 /// final engine = OndeChatEngine();
49 ///
50 -/// // 3. Load the platform-appropriate default model.
51 -/// await engine.loadDefaultModel(
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);
@@ -93,12 +100,11 @@ extension OndeChatEngineX on api.OndeChatEngine {
100 Future<double> loadDefaultModel({
101 String? systemPrompt,
102 SamplingConfig? sampling,
96 - }) =>
97 - loadGgufModel(
98 - config: api.defaultModelConfig(),
99 - systemPrompt: systemPrompt,
100 - sampling: sampling,
101 - );
103 + }) => loadGgufModel(
104 + config: api.defaultModelConfig(),
105 + systemPrompt: systemPrompt,
106 + sampling: sampling,
107 + );
108
109 // -------------------------------------------------------------------------
110 // clearHistoryCount — int-typed convenience over clearHistory() → BigInt
@@ -156,9 +162,7 @@ abstract final class OndeInference {
162 if (Platform.isMacOS) {
163 // CocoaPods + use_frameworks! → pod is a dynamic framework.
164 // DynamicLibrary.process() can't resolve symbols inside it.
159 - lib = ExternalLibrary.open(
160 - 'onde_inference.framework/onde_inference',
161 - );
165 + lib = ExternalLibrary.open('onde_inference.framework/onde_inference');
166 } else if (Platform.isIOS) {
167 // CocoaPods on iOS statically links the pod into the Runner
168 // binary via -force_load. Symbols live in the main executable.
sdk/dart/lib/src/frb_generated.dart/api.dart
+79 -40
@@ -85,8 +85,10 @@ abstract class OndeChatEngine implements RustOpaqueInterface {
85 /// One-shot generation from an explicit message list.
86 ///
87 /// Does **not** modify the conversation history.
88 - Future<InferenceResult> generate(
89 - {required List<ChatMessage> messages, SamplingConfig? sampling});
88 + Future<InferenceResult> generate({
89 + required List<ChatMessage> messages,
90 + SamplingConfig? sampling,
91 + });
92
93 /// Returns a clone of the full conversation history.
94 Future<List<ChatMessage>> history();
@@ -97,13 +99,28 @@ abstract class OndeChatEngine implements RustOpaqueInterface {
99 /// Returns `true` if a model is currently loaded and ready.
100 Future<bool> isLoaded();
101
102 + /// Load the model assigned to this app via the Onde dashboard.
103 + ///
104 + /// The [appId] and [appSecret] identify your app on
105 + /// [ondeinference.com](https://ondeinference.com). If no model is
106 + /// assigned, the platform default is loaded instead.
107 + ///
108 + /// Returns the wall-clock seconds taken to load the model.
109 + Future<double> loadAssignedModel({
110 + required String appId,
111 + required String appSecret,
112 + String? systemPrompt,
113 + SamplingConfig? sampling,
114 + });
115 +
116 /// Load a GGUF model into the engine.
117 ///
118 /// Returns the load duration in seconds on success.
103 - Future<double> loadGgufModel(
104 - {required GgufModelConfig config,
105 - String? systemPrompt,
106 - SamplingConfig? sampling});
119 + Future<double> loadGgufModel({
120 + required GgufModelConfig config,
121 + String? systemPrompt,
122 + SamplingConfig? sampling,
123 + });
124
125 /// Create a new engine with no model loaded.
126 factory OndeChatEngine() => RustLib.instance.api.crateApiOndeChatEngineNew();
@@ -147,10 +164,7 @@ class ChatMessage {
164 final ChatRole role;
165 final String content;
166
150 - const ChatMessage({
151 - required this.role,
152 - required this.content,
153 - });
167 + const ChatMessage({required this.role, required this.content});
168
169 @override
170 int get hashCode => role.hashCode ^ content.hashCode;
@@ -165,12 +179,7 @@ class ChatMessage {
179 }
180
181 /// Role of a participant in a chat conversation.
168 -enum ChatRole {
169 - system,
170 - user,
171 - assistant,
172 - ;
173 -}
182 +enum ChatRole { system, user, assistant }
183
184 /// A point-in-time snapshot of the engine's state.
185 class EngineInfo {
@@ -211,14 +220,7 @@ class EngineInfo {
220 }
221
222 /// Lifecycle status of the inference engine.
214 -enum EngineStatus {
215 - unloaded,
216 - loading,
217 - ready,
218 - generating,
219 - error,
220 - ;
221 -}
223 +enum EngineStatus { unloaded, loading, ready, generating, error }
224
225 /// Configuration for loading a pre-quantised GGUF model.
226 class GgufModelConfig {
@@ -237,12 +239,16 @@ class GgufModelConfig {
239 /// Approximate memory footprint, e.g. `"~941 MB (GGUF Q4_K_M)"`.
240 final String approxMemory;
241
242 + /// Optional chat template override (Jinja format).
243 + final String? chatTemplate;
244 +
245 const GgufModelConfig({
246 required this.modelId,
247 required this.files,
248 this.tokModelId,
249 required this.displayName,
250 required this.approxMemory,
251 + this.chatTemplate,
252 });
253
254 @override
@@ -251,7 +257,8 @@ class GgufModelConfig {
257 files.hashCode ^
258 tokModelId.hashCode ^
259 displayName.hashCode ^
254 - approxMemory.hashCode;
260 + approxMemory.hashCode ^
261 + chatTemplate.hashCode;
262
263 @override
264 bool operator ==(Object other) =>
@@ -262,7 +269,8 @@ class GgufModelConfig {
269 files == other.files &&
270 tokModelId == other.tokModelId &&
271 displayName == other.displayName &&
265 - approxMemory == other.approxMemory;
272 + approxMemory == other.approxMemory &&
273 + chatTemplate == other.chatTemplate;
274 }
275
276 /// The result of a completed inference request.
@@ -279,11 +287,15 @@ class InferenceResult {
287 /// Finish reason reported by the model, e.g. `"stop"` or `"length"`.
288 final String finishReason;
289
290 + /// Tool calls requested by the model (empty if none).
291 + final List<ToolCallInfo> toolCalls;
292 +
293 const InferenceResult({
294 required this.text,
295 required this.durationSecs,
296 required this.durationDisplay,
297 required this.finishReason,
298 + required this.toolCalls,
299 });
300
301 @override
@@ -291,7 +303,8 @@ class InferenceResult {
303 text.hashCode ^
304 durationSecs.hashCode ^
305 durationDisplay.hashCode ^
294 - finishReason.hashCode;
306 + finishReason.hashCode ^
307 + toolCalls.hashCode;
308
309 @override
310 bool operator ==(Object other) =>
@@ -301,7 +314,8 @@ class InferenceResult {
314 text == other.text &&
315 durationSecs == other.durationSecs &&
316 durationDisplay == other.durationDisplay &&
304 - finishReason == other.finishReason;
317 + finishReason == other.finishReason &&
318 + toolCalls == other.toolCalls;
319 }
320
321 @freezed
@@ -309,19 +323,14 @@ sealed class OndeError with _$OndeError implements FrbException {
323 const OndeError._();
324
325 const factory OndeError.noModelLoaded() = OndeError_NoModelLoaded;
312 - const factory OndeError.alreadyLoaded({
313 - required String modelName,
314 - }) = OndeError_AlreadyLoaded;
315 - const factory OndeError.modelBuild({
316 - required String reason,
317 - }) = OndeError_ModelBuild;
318 - const factory OndeError.inference({
319 - required String reason,
320 - }) = OndeError_Inference;
326 + const factory OndeError.alreadyLoaded({required String modelName}) =
327 + OndeError_AlreadyLoaded;
328 + const factory OndeError.modelBuild({required String reason}) =
329 + OndeError_ModelBuild;
330 + const factory OndeError.inference({required String reason}) =
331 + OndeError_Inference;
332 const factory OndeError.cancelled() = OndeError_Cancelled;
322 - const factory OndeError.other({
323 - required String reason,
324 - }) = OndeError_Other;
333 + const factory OndeError.other({required String reason}) = OndeError_Other;
334 }
335
336 /// Sampling parameters for text generation. All fields are optional — `None`
@@ -398,3 +407,33 @@ class StreamChunk {
407 done == other.done &&
408 finishReason == other.finishReason;
409 }
410 +
411 +/// Metadata about a tool call requested by the model.
412 +class ToolCallInfo {
413 + /// Unique identifier for this tool call.
414 + final String id;
415 +
416 + /// Name of the function to call.
417 + final String functionName;
418 +
419 + /// JSON-encoded arguments for the function.
420 + final String arguments;
421 +
422 + const ToolCallInfo({
423 + required this.id,
424 + required this.functionName,
425 + required this.arguments,
426 + });
427 +
428 + @override
429 + int get hashCode => id.hashCode ^ functionName.hashCode ^ arguments.hashCode;
430 +
431 + @override
432 + bool operator ==(Object other) =>
433 + identical(this, other) ||
434 + other is ToolCallInfo &&
435 + runtimeType == other.runtimeType &&
436 + id == other.id &&
437 + functionName == other.functionName &&
438 + arguments == other.arguments;
439 +}
sdk/dart/lib/src/types.dart
+50 -40
@@ -22,6 +22,7 @@ export 'frb_generated.dart/api.dart'
22 GgufModelConfig,
23 InferenceResult,
24 StreamChunk,
25 + ToolCallInfo,
26 EngineStatus,
27 EngineInfo,
28 OndeError,
@@ -121,14 +122,14 @@ extension SamplingConfigX on SamplingConfig {
122 /// Balanced creative-chat defaults:
123 /// `temperature=0.7`, `topP=0.95`, `maxTokens=512`.
124 static SamplingConfig defaultConfig() => SamplingConfig(
124 - temperature: 0.7,
125 - topP: 0.95,
126 - topK: null,
127 - minP: null,
128 - maxTokens: BigInt.from(512),
129 - frequencyPenalty: null,
130 - presencePenalty: null,
131 - );
125 + temperature: 0.7,
126 + topP: 0.95,
127 + topK: null,
128 + minP: null,
129 + maxTokens: BigInt.from(512),
130 + frequencyPenalty: null,
131 + presencePenalty: null,
132 + );
133
134 /// Greedy / fully deterministic decoding:
135 /// `temperature=0.0`, `maxTokens=512`.
@@ -136,26 +137,26 @@ extension SamplingConfigX on SamplingConfig {
137 /// Use for coding, fact-retrieval, or any task where reproducibility
138 /// matters.
139 static SamplingConfig deterministic() => SamplingConfig(
139 - temperature: 0.0,
140 - topP: null,
141 - topK: null,
142 - minP: null,
143 - maxTokens: BigInt.from(512),
144 - frequencyPenalty: null,
145 - presencePenalty: null,
146 - );
140 + temperature: 0.0,
141 + topP: null,
142 + topK: null,
143 + minP: null,
144 + maxTokens: BigInt.from(512),
145 + frequencyPenalty: null,
146 + presencePenalty: null,
147 + );
148
149 /// Low-latency preset for memory-constrained mobile devices:
150 /// `temperature=0.7`, `topP=0.95`, `maxTokens=128`.
151 static SamplingConfig mobile() => SamplingConfig(
151 - temperature: 0.7,
152 - topP: 0.95,
153 - topK: null,
154 - minP: null,
155 - maxTokens: BigInt.from(128),
156 - frequencyPenalty: null,
157 - presencePenalty: null,
158 - );
152 + temperature: 0.7,
153 + topP: 0.95,
154 + topK: null,
155 + minP: null,
156 + maxTokens: BigInt.from(128),
157 + frequencyPenalty: null,
158 + presencePenalty: null,
159 + );
160
161 /// Returns a copy of this config with the given fields replaced.
162 SamplingConfig copyWith({
@@ -166,16 +167,15 @@ extension SamplingConfigX on SamplingConfig {
167 BigInt? maxTokens,
168 double? frequencyPenalty,
169 double? presencePenalty,
169 - }) =>
170 - SamplingConfig(
171 - temperature: temperature ?? this.temperature,
172 - topP: topP ?? this.topP,
173 - topK: topK ?? this.topK,
174 - minP: minP ?? this.minP,
175 - maxTokens: maxTokens ?? this.maxTokens,
176 - frequencyPenalty: frequencyPenalty ?? this.frequencyPenalty,
177 - presencePenalty: presencePenalty ?? this.presencePenalty,
178 - );
170 + }) => SamplingConfig(
171 + temperature: temperature ?? this.temperature,
172 + topP: topP ?? this.topP,
173 + topK: topK ?? this.topK,
174 + minP: minP ?? this.minP,
175 + maxTokens: maxTokens ?? this.maxTokens,
176 + frequencyPenalty: frequencyPenalty ?? this.frequencyPenalty,
177 + presencePenalty: presencePenalty ?? this.presencePenalty,
178 + );
179 }
180
181 // ---------------------------------------------------------------------------
@@ -199,12 +199,12 @@ extension EngineStatusX on EngineStatus {
199
200 /// A short human-readable label for display in UIs.
201 String get label => switch (this) {
202 - EngineStatus.unloaded => 'Unloaded',
203 - EngineStatus.loading => 'Loading…',
204 - EngineStatus.ready => 'Ready',
205 - EngineStatus.generating => 'Generating…',
206 - EngineStatus.error => 'Error',
207 - };
202 + EngineStatus.unloaded => 'Unloaded',
203 + EngineStatus.loading => 'Loading…',
204 + EngineStatus.ready => 'Ready',
205 + EngineStatus.generating => 'Generating…',
206 + EngineStatus.error => 'Error',
207 + };
208 }
209
210 // ---------------------------------------------------------------------------
@@ -234,3 +234,13 @@ extension InferenceResultX on InferenceResult {
234 /// Returns `true` if generation was truncated by the `maxTokens` limit.
235 bool get truncated => finishReason == 'length';
236 }
237 +
238 +// ---------------------------------------------------------------------------
239 +// InferenceResult — tool calling extension
240 +// ---------------------------------------------------------------------------
241 +
242 +/// Convenience helpers on [InferenceResult] for tool calling.
243 +extension InferenceResultToolsX on InferenceResult {
244 + /// Returns `true` if the model requested one or more tool calls.
245 + bool get hasToolCalls => toolCalls.isNotEmpty;
246 +}
sdk/dart/pubspec.yaml
+1 -1
@@ -2,7 +2,7 @@ name: onde_inference
2 description: >-
3 On-device LLM inference for Flutter & Dart. Run Qwen 2.5 models locally
4 with Metal on iOS and macOS, CPU on Android and desktop. No cloud, no API key.
5 -version: 0.1.7
5 +version: 1.0.0
6 repository: https://github.com/ondeinference/onde
7 homepage: https://ondeinference.com
8 issue_tracker: https://github.com/ondeinference/onde/issues
sdk/react-native/CHANGELOG.md
+34
@@ -1,3 +1,37 @@
1 +## 1.0.0
2 +
3 +First stable release. Onde has been in production across multiple App Store apps for a while now, so we're dropping the `0.x`.
4 +
5 +### Assigned model loading
6 +
7 +New `loadAssignedModel(appId, appSecret)` method on `OndeChatEngine`. Register your app at [ondeinference.com](https://ondeinference.com), assign a model in the dashboard, and the SDK fetches it at runtime. Falls back to the platform default if nothing's assigned yet. `loadDefaultModel` still works for quick prototyping.
8 +
9 +The example app has `ONDE_APP_ID` and `ONDE_APP_SECRET` constants at the top — fill them in and it switches to the assigned model path automatically.
10 +
11 +### New models
12 +
13 +* Qwen 3 8B, 14B, and 1.7B (GGUF Q4_K_M)
14 +* Qwen 2.5 Coder 7B (GGUF Q4_K_M)
15 +* DeepSeek Coder 6.7B (GGUF Q4_K_M) with bundled chat template
16 +
17 +### Type changes
18 +
19 +* `GgufModelConfig` has a new optional `chatTemplate` field. Models that ship without a built-in template (like DeepSeek Coder) use this.
20 +* `InferenceResult` now includes a `toolCalls` array (`ToolCallInfo[]`). Usually empty — but when a model decides to request a tool call, you get structured data instead of raw markup in `text`.
21 +* Added `ToolCallInfo` type export.
22 +
23 +### Engine
24 +
25 +* Old model weights are dropped outside the lock during model swaps, so status queries don't block while memory is being freed.
26 +
27 +### Dependencies
28 +
29 +* Switched from git-based `mistralrs` to published `onde-mistralrs 0.8.2` crates on crates.io.
30 +
31 +### Cross-platform
32 +
33 +* Linux and Windows CPU inference builds work properly now (TokenSource fix for non-Darwin platforms).
34 +
35 ## 0.1.7
36
37 * **CI:** No user-visible changes. Internal release to keep version numbers in sync across all SDK distributions.
sdk/react-native/README.md
+3 -3
@@ -148,6 +148,6 @@ Dual-licensed under **MIT** and **Apache 2.0**. Pick whichever works for you.
148
149 ---
150
151 -<p align="center">
152 - <sub>© 2026 <a href="https://ondeinference.com">Onde Inference</a></sub>
153 -</p>
151 +## Copyright
152 +
153 +© 2026 [Onde Inference](https://ondeinference.com) (Splitfire AB).
sdk/react-native/example/App.tsx
+19 -3
@@ -19,6 +19,13 @@ import {
19 type EngineStatus,
20 } from "@ondeinference/react-native";
21
22 +// ── Onde app credentials ─────────────────────────────────────────────────
23 +// Register your app at https://ondeinference.com to get these.
24 +// The dashboard lets you assign a model to your app — the SDK fetches
25 +// it automatically. If no model is assigned, the platform default is used.
26 +const ONDE_APP_ID = "";
27 +const ONDE_APP_SECRET = "";
28 +
29 interface DisplayMessage {
30 id: string;
31 role: "user" | "assistant";
@@ -74,9 +81,18 @@ const App: React.FC = () => {
81 setIsLoading(true);
82 setEngineStatus("Loading");
83 try {
77 - const seconds = await OndeChatEngine.loadDefaultModel(
78 - "You are a helpful, concise assistant.",
79 - );
84 + let seconds: number;
85 + if (ONDE_APP_ID && ONDE_APP_SECRET) {
86 + seconds = await OndeChatEngine.loadAssignedModel(
87 + ONDE_APP_ID,
88 + ONDE_APP_SECRET,
89 + "You are a helpful, concise assistant.",
90 + );
91 + } else {
92 + seconds = await OndeChatEngine.loadDefaultModel(
93 + "You are a helpful, concise assistant.",
94 + );
95 + }
96 await refreshStatus();
97 Alert.alert("Model Loaded", `Ready in ${seconds.toFixed(1)}s`);
98 } catch (error: unknown) {
sdk/react-native/package.json
+1 -1
@@ -1,6 +1,6 @@
1 {
2 "name": "@ondeinference/react-native",
3 - "version": "0.1.7",
3 + "version": "1.0.0",
4 "description": "On-device LLM inference for React Native. Run Qwen 2.5 models locally with Metal on iOS, CPU on Android. No cloud, no API key.",
5 "main": "build/index.js",
6 "types": "build/index.d.ts",
sdk/react-native/src/OndeInferenceModule.ts
+8 -2
@@ -19,12 +19,18 @@ export interface OndeInferenceNativeModule {
19 // Async (returns JSON strings)
20 loadDefaultModel(
21 systemPrompt: string | null,
22 - samplingJson: string | null
22 + samplingJson: string | null,
23 + ): Promise<string>;
24 + loadAssignedModel(
25 + appId: string,
26 + appSecret: string,
27 + systemPrompt: string | null,
28 + samplingJson: string | null,
29 ): Promise<string>;
30 loadModel(
31 configJson: string,
32 systemPrompt: string | null,
27 - samplingJson: string | null
33 + samplingJson: string | null,
34 ): Promise<string>;
35 unloadModel(): Promise<string>;
36 info(): Promise<string>;
sdk/react-native/src/index.ts
+42
@@ -7,6 +7,7 @@ import type {
7 InferenceResult,
8 SamplingConfig,
9 StreamChunk,
10 + ToolCallInfo,
11 } from "./types";
12 import { OndeError } from "./types";
13
@@ -20,6 +21,7 @@ export type {
21 InferenceResult,
22 SamplingConfig,
23 StreamChunk,
24 + ToolCallInfo,
25 } from "./types";
26 export { OndeError } from "./types";
27
@@ -46,6 +48,7 @@ function serializeModelConfig(config: GgufModelConfig): string {
48 tok_model_id: config.tokModelId,
49 display_name: config.displayName,
50 approx_memory: config.approxMemory,
51 + chat_template: config.chatTemplate,
52 });
53 }
54
@@ -65,12 +68,22 @@ function parseInferenceResult(json: string): InferenceResult {
68 duration_secs: number;
69 duration_display: string;
70 finish_reason?: string;
71 + tool_calls?: Array<{
72 + id: string;
73 + function_name: string;
74 + arguments: string;
75 + }>;
76 }>(json);
77 return {
78 text: raw.text,
79 durationSecs: raw.duration_secs,
80 durationDisplay: raw.duration_display,
81 finishReason: raw.finish_reason,
82 + toolCalls: (raw.tool_calls ?? []).map((tc) => ({
83 + id: tc.id,
84 + functionName: tc.function_name,
85 + arguments: tc.arguments,
86 + })),
87 };
88 }
89
@@ -97,6 +110,7 @@ function parseModelConfig(json: string): GgufModelConfig {
110 tokModelId: raw.tok_model_id,
111 displayName: raw.display_name,
112 approxMemory: raw.approx_memory,
113 + chatTemplate: raw.chat_template,
114 };
115 }
116
@@ -158,6 +172,34 @@ export const OndeChatEngine = {
172 return result.elapsed_secs;
173 },
174
175 + /**
176 + * Load the model assigned to this app via the Onde dashboard.
177 + *
178 + * Register your app at https://ondeinference.com to get an app ID
179 + * and secret. The dashboard lets you assign a model — the SDK
180 + * fetches it automatically. If no model is assigned, the platform
181 + * default is loaded instead.
182 + *
183 + * @param appId - Your Onde app ID from ondeinference.com
184 + * @param appSecret - Your Onde app secret from ondeinference.com
185 + * @returns Wall-clock loading time in seconds.
186 + */
187 + async loadAssignedModel(
188 + appId: string,
189 + appSecret: string,
190 + systemPrompt?: string,
191 + sampling?: SamplingConfig,
192 + ): Promise<number> {
193 + const json = await OndeInferenceModule.loadAssignedModel(
194 + appId,
195 + appSecret,
196 + systemPrompt ?? null,
197 + sampling ? serializeSampling(sampling) : null,
198 + );
199 + const result = parseResult<{ elapsed_secs: number }>(json);
200 + return result.elapsed_secs;
201 + },
202 +
203 /**
204 * Load a specific GGUF model.
205 * @returns Wall-clock loading time in seconds.
sdk/react-native/src/types.ts
+14
@@ -48,6 +48,8 @@ export interface GgufModelConfig {
48 displayName: string;
49 /** Approximate memory footprint description. */
50 approxMemory: string;
51 + /** Optional Jinja chat template override. */
52 + chatTemplate?: string;
53 }
54
55 // ── Inference result ─────────────────────────────────────────────────────────
@@ -62,6 +64,18 @@ export interface InferenceResult {
64 durationDisplay: string;
65 /** Finish reason, if available. */
66 finishReason?: string;
67 + /** Tool calls requested by the model, if any. */
68 + toolCalls: ToolCallInfo[];
69 +}
70 +
71 +/** Metadata about a tool call requested by the model. */
72 +export interface ToolCallInfo {
73 + /** Unique identifier for this tool call. */
74 + id: string;
75 + /** Name of the function to call. */
76 + functionName: string;
77 + /** JSON-encoded arguments for the function. */
78 + arguments: string;
79 }
80
81 // ── Streaming ────────────────────────────────────────────────────────────────
src/inference/ffi.rs
+28
@@ -138,6 +138,34 @@ impl OndeChatEngine {
138 Ok(elapsed.as_secs_f64())
139 }
140
141 + /// Load the model assigned to this app via the Onde dashboard.
142 + ///
143 + /// The `app_id` and `app_secret` identify your app on
144 + /// [ondeinference.com](https://ondeinference.com). The backend returns
145 + /// the GGUF model configuration assigned to your app; if no model is
146 + /// assigned (HTTP 404) the platform default is loaded instead.
147 + ///
148 + /// Returns the wall-clock loading time in seconds.
149 + pub async fn load_assigned_model(
150 + &self,
151 + app_id: String,
152 + app_secret: String,
153 + system_prompt: Option<String>,
154 + sampling: Option<SamplingConfig>,
155 + ) -> Result<f64, InferenceError> {
156 + let elapsed = self
157 + .inner
158 + .load_assigned_model(
159 + smbcloud_gresiq_sdk::Environment::Production,
160 + &app_id,
161 + &app_secret,
162 + system_prompt,
163 + sampling,
164 + )
165 + .await?;
166 + Ok(elapsed.as_secs_f64())
167 + }
168 +
169 /// Unload the current model, freeing all memory.
170 ///
171 /// Returns the display name of the model that was unloaded, or `nil`