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`