@ondeinference / onde / commits / ab6cd5f

Add Qwen 3 SDK configs and desktop download support

Seto Elkahfi committed Jul 13, 2026 at 19:38 UTC ab6cd5f734ab7a7e08a80133c10a60954ed61bda
21 files changed +1122 -140
.agents/skills/sdk-regressions/SKILL.md
+62 -8
@@ -80,22 +80,76 @@ cargo +nightly check -Z build-std --target aarch64-apple-tvos-sim
80
81 ---
82
83 -## Tier 3 — Android, Windows, Linux (stable, may need cross-linker)
83 +## Tier 3 — Android, Windows, Linux (stable, needs cross toolchains)
84
85 -These check that CPU-only code paths compile. They may fail on linker resolution
86 -if cross-linkers aren't installed, but `cargo check` (no linking) should pass:
85 +These check that CPU-only code paths compile. For this crate, `cargo check`
86 +still runs C build scripts from dependencies such as `aws-lc-sys` and `ring`,
87 +so absent cross GCC toolchains are real blockers, not harmless link-only
88 +failures.
89
90 ```bash
91 # Android arm64 — CPU, requires hf-hub sandbox workaround
92 cargo check --target aarch64-linux-android
93
92 -# Windows — CPU
94 +# Windows MSVC — CPU, exact CI target; run on Windows or a Windows CI runner
95 cargo check --target x86_64-pc-windows-msvc
96
97 # Linux — CPU
98 cargo check --target x86_64-unknown-linux-gnu
99 ```
100
101 +On macOS hosts without cross GCC installed, expect Linux checks to fail looking
102 +for `x86_64-linux-gnu-gcc`, and Windows GNU checks to fail looking for
103 +`x86_64-w64-mingw32-gcc`. Use the Colima/Docker flow below instead of treating
104 +those as code regressions.
105 +
106 +### macOS host fallback: Colima/Docker
107 +
108 +Use this when the goal is to run the Linux and Windows cross-checks on a macOS
109 +host and the host does not have cross GCC toolchains. On Apple Silicon, run a
110 +native `linux/arm64` container. Avoid `linux/amd64` emulation for this crate;
111 +the dependency build can crash under QEMU before reaching `onde` code.
112 +
113 +Give Colima enough memory before running these checks:
114 +
115 +```bash
116 +colima status
117 +colima stop
118 +colima start --memory 8
119 +```
120 +
121 +Run Linux and the local Windows GNU proxy check in the container:
122 +
123 +```bash
124 +docker run --rm --platform linux/arm64 \
125 + -v "$PWD:/workspace" \
126 + -v onde-cargo-registry:/usr/local/cargo/registry \
127 + -v onde-cargo-git:/usr/local/cargo/git \
128 + -w /workspace \
129 + rust:1.92-bookworm \
130 + bash -lc '
131 + export PATH=/usr/local/cargo/bin:$PATH
132 + apt-get update >/dev/null
133 + DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \
134 + gcc-x86-64-linux-gnu \
135 + libc6-dev-amd64-cross \
136 + linux-libc-dev-amd64-cross \
137 + gcc-mingw-w64-x86-64 \
138 + binutils-mingw-w64-x86-64 >/dev/null
139 + rustup target add x86_64-unknown-linux-gnu x86_64-pc-windows-gnu >/dev/null
140 + CARGO_BUILD_JOBS=1 cargo check --target x86_64-unknown-linux-gnu
141 + CARGO_BUILD_JOBS=1 cargo check --target x86_64-pc-windows-gnu
142 + '
143 +```
144 +
145 +`x86_64-pc-windows-gnu` exercises `#[cfg(target_os = "windows")]` and the
146 +CPU-only Windows path locally, but it is not a substitute for the CI
147 +`x86_64-pc-windows-msvc` check. The MSVC ABI check needs a Windows/MSVC
148 +toolchain, normally GitHub Actions `windows-latest`.
149 +
150 +If the container reaches `Checking onde ...` and then reports Rust errors, the
151 +cross toolchain is working. Fix the crate error and rerun the check.
152 +
153 ---
154
155 ## Full Regression Suite (copy-paste)
@@ -136,6 +190,7 @@ rustup target add aarch64-apple-ios aarch64-apple-ios-sim
190 rustup target add aarch64-linux-android armv7-linux-androideabi
191 rustup target add x86_64-linux-android i686-linux-android
192 rustup target add x86_64-pc-windows-msvc
193 +rustup target add x86_64-pc-windows-gnu
194 rustup target add x86_64-unknown-linux-gnu
195
196 # Tier 2 — tvOS (nightly, no `rustup target add` — uses -Z build-std)
@@ -513,7 +568,6 @@ consumers either way — but leaving it active makes the dry-run confusing.
568
569 ---
570
516 -*Last updated: July 2025 — added hf-hub/mistralrs-core iOS/tvOS deps,
517 -cache path consistency checks, App Group convention, onde-mistralrs publish
518 -strategy, GresIQ/HF_TOKEN secrets documentation.*
519 -
571 +*Last updated: July 2026 — added Colima/Docker Linux and Windows GNU
572 +cross-check guidance for macOS hosts without cross GCC, plus the MSVC CI
573 +caveat.*
Cargo.lock
+4 -4
@@ -862,9 +862,9 @@ dependencies = [
862
863 [[package]]
864 name = "crossbeam-epoch"
865 -version = "0.9.18"
865 +version = "0.9.20"
866 source = "registry+https://github.com/rust-lang/crates.io-index"
867 -checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e"
867 +checksum = "2d6914041f254d6e9176c01941b21115dcfb7089e55135a35411081bd106ef3f"
868 dependencies = [
869 "crossbeam-utils",
870 ]
@@ -3820,9 +3820,9 @@ dependencies = [
3820
3821 [[package]]
3822 name = "quinn-proto"
3823 -version = "0.11.14"
3823 +version = "0.11.15"
3824 source = "registry+https://github.com/rust-lang/crates.io-index"
3825 -checksum = "434b42fec591c96ef50e21e886936e66d3cc3f737104fdb9b737c40ffb94c098"
3825 +checksum = "4fcb935c5bec503c2f0e306bdd3e58bb9029dcb14fa8d9ac76e3a5256ac0763e"
3826 dependencies = [
3827 "aws-lc-rs",
3828 "bytes",
sdk/dart/lib/src/engine.dart
+30
@@ -255,6 +255,36 @@ abstract final class OndeInference {
255 /// Optimised for code-generation tasks on macOS and Linux desktop.
256 static GgufModelConfig qwen25Coder3bConfig() => api.qwen25Coder3BConfig();
257
258 + /// Qwen 3 0.6B (GGUF Q4_K_M, ~0.5 GB).
259 + static GgufModelConfig qwen306bConfig() => api.qwen306BConfig();
260 +
261 + /// Qwen 3 1.7B (GGUF Q4_K_M, ~1.3 GB).
262 + static GgufModelConfig qwen317bConfig() => api.qwen317BConfig();
263 +
264 + /// Qwen 3 4B (GGUF Q4_K_M, ~2.5 GB).
265 + static GgufModelConfig qwen34bConfig() => api.qwen34BConfig();
266 +
267 + /// Qwen 3 8B (GGUF Q4_K_M, ~5 GB).
268 + static GgufModelConfig qwen38bConfig() => api.qwen38BConfig();
269 +
270 + /// Qwen 3 14B (GGUF Q4_K_M, ~8.4 GB).
271 + static GgufModelConfig qwen314bConfig() => api.qwen314BConfig();
272 +
273 + /// Qwen 3 32B (GGUF Q4_K_M, ~19.8 GB).
274 + static GgufModelConfig qwen332bConfig() => api.qwen332BConfig();
275 +
276 + /// Qwen 3 4B Instruct 2507, the non-thinking checkpoint (~2.5 GB).
277 + static GgufModelConfig qwen34bInstruct2507Config() =>
278 + api.qwen34BInstruct2507Config();
279 +
280 + /// Qwen 3 4B Thinking 2507 (~2.5 GB).
281 + static GgufModelConfig qwen34bThinking2507Config() =>
282 + api.qwen34BThinking2507Config();
283 +
284 + /// Qwen 3 30B-A3B Instruct 2507 mixture-of-experts (~18.6 GB).
285 + static GgufModelConfig qwen330bA3bInstruct2507Config() =>
286 + api.qwen330BA3BInstruct2507Config();
287 +
288 // -------------------------------------------------------------------------
289 // Sampling config factories
290 // -------------------------------------------------------------------------
sdk/dart/lib/src/frb_generated.dart/api.dart
+36 -2
@@ -3,14 +3,14 @@
3
4 // ignore_for_file: type=lint, unused_import, unnecessary_import
5
6 -// ignore_for_file: invalid_use_of_internal_member
6 +// ignore_for_file: invalid_use_of_internal_member, unused_import, unnecessary_import
7
8 import 'frb_generated.dart';
9 import 'package:flutter_rust_bridge/flutter_rust_bridge_for_generated.dart';
10 import 'package:freezed_annotation/freezed_annotation.dart' hide protected;
11 part 'api.freezed.dart';
12
13 -// These function are ignored because they are on traits that is not defined in current crate (put an empty `#[frb]` on it to unignore): `assert_receiver_is_total_eq`, `assert_receiver_is_total_eq`, `clone`, `clone`, `clone`, `clone`, `clone`, `clone`, `clone`, `clone`, `clone`, `eq`, `eq`, `fmt`, `fmt`, `fmt`, `fmt`, `fmt`, `fmt`, `fmt`, `fmt`, `fmt`, `fmt`, `fmt`, `from`, `from`, `from`, `from`, `from`, `from`, `from`, `from`, `from`, `from`, `from`, `from`, `from`, `from`
13 +// These function are ignored because they are on traits that is not defined in current crate (put an empty `#[frb]` on it to unignore): `assert_fields_are_eq`, `assert_fields_are_eq`, `clone`, `clone`, `clone`, `clone`, `clone`, `clone`, `clone`, `clone`, `clone`, `eq`, `eq`, `fmt`, `fmt`, `fmt`, `fmt`, `fmt`, `fmt`, `fmt`, `fmt`, `fmt`, `fmt`, `fmt`, `from`, `from`, `from`, `from`, `from`, `from`, `from`, `from`, `from`, `from`, `from`, `from`, `from`, `from`
14
15 /// Return the platform-appropriate default `GgufModelConfig`.
16 ///
@@ -35,6 +35,40 @@ GgufModelConfig qwen25Coder15BConfig() =>
35 GgufModelConfig qwen25Coder3BConfig() =>
36 RustLib.instance.api.crateApiQwen25Coder3BConfig();
37
38 +/// `GgufModelConfig` for Qwen 3 0.6B Q4_K_M (~0.5 GB).
39 +GgufModelConfig qwen306BConfig() =>
40 + RustLib.instance.api.crateApiQwen306BConfig();
41 +
42 +/// `GgufModelConfig` for Qwen 3 1.7B Q4_K_M (~1.3 GB).
43 +GgufModelConfig qwen317BConfig() =>
44 + RustLib.instance.api.crateApiQwen317BConfig();
45 +
46 +/// `GgufModelConfig` for Qwen 3 4B Q4_K_M (~2.7 GB).
47 +GgufModelConfig qwen34BConfig() => RustLib.instance.api.crateApiQwen34BConfig();
48 +
49 +/// `GgufModelConfig` for Qwen 3 8B Q4_K_M (~5 GB).
50 +GgufModelConfig qwen38BConfig() => RustLib.instance.api.crateApiQwen38BConfig();
51 +
52 +/// `GgufModelConfig` for Qwen 3 14B Q4_K_M (~8.4 GB).
53 +GgufModelConfig qwen314BConfig() =>
54 + RustLib.instance.api.crateApiQwen314BConfig();
55 +
56 +/// `GgufModelConfig` for Qwen 3 32B Q4_K_M (~19.8 GB).
57 +GgufModelConfig qwen332BConfig() =>
58 + RustLib.instance.api.crateApiQwen332BConfig();
59 +
60 +/// `GgufModelConfig` for Qwen 3 4B Instruct 2507 Q4_K_M (~2.5 GB).
61 +GgufModelConfig qwen34BInstruct2507Config() =>
62 + RustLib.instance.api.crateApiQwen34BInstruct2507Config();
63 +
64 +/// `GgufModelConfig` for Qwen 3 4B Thinking 2507 Q4_K_M (~2.5 GB).
65 +GgufModelConfig qwen34BThinking2507Config() =>
66 + RustLib.instance.api.crateApiQwen34BThinking2507Config();
67 +
68 +/// `GgufModelConfig` for Qwen 3 30B-A3B Instruct 2507 (MoE) Q4_K_M (~18.6 GB).
69 +GgufModelConfig qwen330BA3BInstruct2507Config() =>
70 + RustLib.instance.api.crateApiQwen330BA3BInstruct2507Config();
71 +
72 /// Default sampling config: `temperature=0.7`, `top_p=0.95`, `max_tokens=512`.
73 SamplingConfig defaultSamplingConfig() =>
74 RustLib.instance.api.crateApiDefaultSamplingConfig();
sdk/dart/lib/src/frb_generated.dart/frb_generated.dart
+227 -2
@@ -3,7 +3,7 @@
3
4 // ignore_for_file: type=lint, unused_import, unnecessary_import
5
6 -// ignore_for_file: unused_element, duplicate_ignore, invalid_use_of_internal_member, annotate_overrides, non_constant_identifier_names, curly_braces_in_flow_control_structures, prefer_const_literals_to_create_immutables, unused_field
6 +// ignore_for_file: unused_import, unused_element, unnecessary_import, duplicate_ignore, invalid_use_of_internal_member, annotate_overrides, non_constant_identifier_names, curly_braces_in_flow_control_structures, prefer_const_literals_to_create_immutables, unused_field
7
8 import 'api.dart';
9 import 'dart:async';
@@ -66,7 +66,7 @@ class RustLib extends BaseEntrypoint<RustLibApi, RustLibApiImpl, RustLibWire> {
66 String get codegenVersion => '2.12.0';
67
68 @override
69 - int get rustContentHash => -467241906;
69 + int get rustContentHash => -1719313298;
70
71 static const kDefaultExternalLibraryLoaderConfig =
72 ExternalLibraryLoaderConfig(
@@ -164,6 +164,24 @@ abstract class RustLibApi extends BaseApi {
164
165 GgufModelConfig crateApiQwen25Coder3BConfig();
166
167 + GgufModelConfig crateApiQwen306BConfig();
168 +
169 + GgufModelConfig crateApiQwen314BConfig();
170 +
171 + GgufModelConfig crateApiQwen317BConfig();
172 +
173 + GgufModelConfig crateApiQwen330BA3BInstruct2507Config();
174 +
175 + GgufModelConfig crateApiQwen332BConfig();
176 +
177 + GgufModelConfig crateApiQwen34BConfig();
178 +
179 + GgufModelConfig crateApiQwen34BInstruct2507Config();
180 +
181 + GgufModelConfig crateApiQwen34BThinking2507Config();
182 +
183 + GgufModelConfig crateApiQwen38BConfig();
184 +
185 RustArcIncrementStrongCountFnType
186 get rust_arc_increment_strong_count_OndeChatEngine;
187
@@ -941,6 +959,213 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
959 TaskConstMeta get kCrateApiQwen25Coder3BConfigConstMeta =>
960 const TaskConstMeta(debugName: "qwen25_coder_3b_config", argNames: []);
961
962 + @override
963 + GgufModelConfig crateApiQwen306BConfig() {
964 + return handler.executeSync(
965 + SyncTask(
966 + callFfi: () {
967 + final serializer = SseSerializer(generalizedFrbRustBinding);
968 + return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 25)!;
969 + },
970 + codec: SseCodec(
971 + decodeSuccessData: sse_decode_gguf_model_config,
972 + decodeErrorData: null,
973 + ),
974 + constMeta: kCrateApiQwen306BConfigConstMeta,
975 + argValues: [],
976 + apiImpl: this,
977 + ),
978 + );
979 + }
980 +
981 + TaskConstMeta get kCrateApiQwen306BConfigConstMeta =>
982 + const TaskConstMeta(debugName: "qwen3_0_6b_config", argNames: []);
983 +
984 + @override
985 + GgufModelConfig crateApiQwen314BConfig() {
986 + return handler.executeSync(
987 + SyncTask(
988 + callFfi: () {
989 + final serializer = SseSerializer(generalizedFrbRustBinding);
990 + return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 26)!;
991 + },
992 + codec: SseCodec(
993 + decodeSuccessData: sse_decode_gguf_model_config,
994 + decodeErrorData: null,
995 + ),
996 + constMeta: kCrateApiQwen314BConfigConstMeta,
997 + argValues: [],
998 + apiImpl: this,
999 + ),
1000 + );
1001 + }
1002 +
1003 + TaskConstMeta get kCrateApiQwen314BConfigConstMeta =>
1004 + const TaskConstMeta(debugName: "qwen3_14b_config", argNames: []);
1005 +
1006 + @override
1007 + GgufModelConfig crateApiQwen317BConfig() {
1008 + return handler.executeSync(
1009 + SyncTask(
1010 + callFfi: () {
1011 + final serializer = SseSerializer(generalizedFrbRustBinding);
1012 + return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 27)!;
1013 + },
1014 + codec: SseCodec(
1015 + decodeSuccessData: sse_decode_gguf_model_config,
1016 + decodeErrorData: null,
1017 + ),
1018 + constMeta: kCrateApiQwen317BConfigConstMeta,
1019 + argValues: [],
1020 + apiImpl: this,
1021 + ),
1022 + );
1023 + }
1024 +
1025 + TaskConstMeta get kCrateApiQwen317BConfigConstMeta =>
1026 + const TaskConstMeta(debugName: "qwen3_1_7b_config", argNames: []);
1027 +
1028 + @override
1029 + GgufModelConfig crateApiQwen330BA3BInstruct2507Config() {
1030 + return handler.executeSync(
1031 + SyncTask(
1032 + callFfi: () {
1033 + final serializer = SseSerializer(generalizedFrbRustBinding);
1034 + return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 28)!;
1035 + },
1036 + codec: SseCodec(
1037 + decodeSuccessData: sse_decode_gguf_model_config,
1038 + decodeErrorData: null,
1039 + ),
1040 + constMeta: kCrateApiQwen330Ba3BInstruct2507ConfigConstMeta,
1041 + argValues: [],
1042 + apiImpl: this,
1043 + ),
1044 + );
1045 + }
1046 +
1047 + TaskConstMeta get kCrateApiQwen330Ba3BInstruct2507ConfigConstMeta =>
1048 + const TaskConstMeta(
1049 + debugName: "qwen3_30b_a3b_instruct_2507_config",
1050 + argNames: [],
1051 + );
1052 +
1053 + @override
1054 + GgufModelConfig crateApiQwen332BConfig() {
1055 + return handler.executeSync(
1056 + SyncTask(
1057 + callFfi: () {
1058 + final serializer = SseSerializer(generalizedFrbRustBinding);
1059 + return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 29)!;
1060 + },
1061 + codec: SseCodec(
1062 + decodeSuccessData: sse_decode_gguf_model_config,
1063 + decodeErrorData: null,
1064 + ),
1065 + constMeta: kCrateApiQwen332BConfigConstMeta,
1066 + argValues: [],
1067 + apiImpl: this,
1068 + ),
1069 + );
1070 + }
1071 +
1072 + TaskConstMeta get kCrateApiQwen332BConfigConstMeta =>
1073 + const TaskConstMeta(debugName: "qwen3_32b_config", argNames: []);
1074 +
1075 + @override
1076 + GgufModelConfig crateApiQwen34BConfig() {
1077 + return handler.executeSync(
1078 + SyncTask(
1079 + callFfi: () {
1080 + final serializer = SseSerializer(generalizedFrbRustBinding);
1081 + return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 30)!;
1082 + },
1083 + codec: SseCodec(
1084 + decodeSuccessData: sse_decode_gguf_model_config,
1085 + decodeErrorData: null,
1086 + ),
1087 + constMeta: kCrateApiQwen34BConfigConstMeta,
1088 + argValues: [],
1089 + apiImpl: this,
1090 + ),
1091 + );
1092 + }
1093 +
1094 + TaskConstMeta get kCrateApiQwen34BConfigConstMeta =>
1095 + const TaskConstMeta(debugName: "qwen3_4b_config", argNames: []);
1096 +
1097 + @override
1098 + GgufModelConfig crateApiQwen34BInstruct2507Config() {
1099 + return handler.executeSync(
1100 + SyncTask(
1101 + callFfi: () {
1102 + final serializer = SseSerializer(generalizedFrbRustBinding);
1103 + return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 31)!;
1104 + },
1105 + codec: SseCodec(
1106 + decodeSuccessData: sse_decode_gguf_model_config,
1107 + decodeErrorData: null,
1108 + ),
1109 + constMeta: kCrateApiQwen34BInstruct2507ConfigConstMeta,
1110 + argValues: [],
1111 + apiImpl: this,
1112 + ),
1113 + );
1114 + }
1115 +
1116 + TaskConstMeta get kCrateApiQwen34BInstruct2507ConfigConstMeta =>
1117 + const TaskConstMeta(
1118 + debugName: "qwen3_4b_instruct_2507_config",
1119 + argNames: [],
1120 + );
1121 +
1122 + @override
1123 + GgufModelConfig crateApiQwen34BThinking2507Config() {
1124 + return handler.executeSync(
1125 + SyncTask(
1126 + callFfi: () {
1127 + final serializer = SseSerializer(generalizedFrbRustBinding);
1128 + return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 32)!;
1129 + },
1130 + codec: SseCodec(
1131 + decodeSuccessData: sse_decode_gguf_model_config,
1132 + decodeErrorData: null,
1133 + ),
1134 + constMeta: kCrateApiQwen34BThinking2507ConfigConstMeta,
1135 + argValues: [],
1136 + apiImpl: this,
1137 + ),
1138 + );
1139 + }
1140 +
1141 + TaskConstMeta get kCrateApiQwen34BThinking2507ConfigConstMeta =>
1142 + const TaskConstMeta(
1143 + debugName: "qwen3_4b_thinking_2507_config",
1144 + argNames: [],
1145 + );
1146 +
1147 + @override
1148 + GgufModelConfig crateApiQwen38BConfig() {
1149 + return handler.executeSync(
1150 + SyncTask(
1151 + callFfi: () {
1152 + final serializer = SseSerializer(generalizedFrbRustBinding);
1153 + return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 33)!;
1154 + },
1155 + codec: SseCodec(
1156 + decodeSuccessData: sse_decode_gguf_model_config,
1157 + decodeErrorData: null,
1158 + ),
1159 + constMeta: kCrateApiQwen38BConfigConstMeta,
1160 + argValues: [],
1161 + apiImpl: this,
1162 + ),
1163 + );
1164 + }
1165 +
1166 + TaskConstMeta get kCrateApiQwen38BConfigConstMeta =>
1167 + const TaskConstMeta(debugName: "qwen3_8b_config", argNames: []);
1168 +
1169 RustArcIncrementStrongCountFnType
1170 get rust_arc_increment_strong_count_OndeChatEngine => wire
1171 .rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerOndeChatEngine;
sdk/dart/lib/src/frb_generated.dart/frb_generated.io.dart
+1 -1
@@ -3,7 +3,7 @@
3
4 // ignore_for_file: type=lint, unused_import, unnecessary_import
5
6 -// ignore_for_file: unused_element, duplicate_ignore, invalid_use_of_internal_member, annotate_overrides, non_constant_identifier_names, curly_braces_in_flow_control_structures, prefer_const_literals_to_create_immutables, unused_field
6 +// ignore_for_file: unused_import, unused_element, unnecessary_import, duplicate_ignore, invalid_use_of_internal_member, annotate_overrides, non_constant_identifier_names, curly_braces_in_flow_control_structures, prefer_const_literals_to_create_immutables, unused_field
7
8 import 'api.dart';
9 import 'dart:async';
sdk/dart/pubspec.yaml
+1 -1
@@ -39,7 +39,7 @@ dev_dependencies:
39 # freezed + build_runner are required by FRB 2.x codegen for union/sealed types.
40 # After running `flutter_rust_bridge_codegen generate`, also run:
41 # dart run build_runner build --delete-conflicting-outputs
42 - freezed: ^3.2.5
42 + freezed: ^3.2.3
43 build_runner: ^2.4.9
44 # NOTE: The FFI code-generator is a Rust binary — NOT a Dart package.
45 # Install it once with:
sdk/dart/rust/src/api.rs
+3
@@ -1,3 +1,6 @@
1 +// Copyright 2026 Onde Inference (Splitfire AB). All rights reserved.
2 +// Use of this source code is governed by the MIT license.
3 +//
4 //! Flutter Rust Bridge v2 API for the Onde Inference Dart SDK.
5 //!
6 //! This module declares mirror types (re-declared plain Rust structs/enums
sdk/dart/rust/src/frb_generated.rs
+274 -1
@@ -39,7 +39,7 @@ flutter_rust_bridge::frb_generated_boilerplate!(
39 default_rust_auto_opaque = RustAutoOpaqueMoi,
40 );
41 pub(crate) const FLUTTER_RUST_BRIDGE_CODEGEN_VERSION: &str = "2.12.0";
42 -pub(crate) const FLUTTER_RUST_BRIDGE_CODEGEN_CONTENT_HASH: i32 = -467241906;
42 +pub(crate) const FLUTTER_RUST_BRIDGE_CODEGEN_CONTENT_HASH: i32 = -1719313298;
43
44 // Section: executor
45
@@ -1178,6 +1178,268 @@ fn wire__crate__api__qwen25_coder_3b_config_impl(
1178 },
1179 )
1180 }
1181 +fn wire__crate__api__qwen3_0_6b_config_impl(
1182 + ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr,
1183 + rust_vec_len_: i32,
1184 + data_len_: i32,
1185 +) -> flutter_rust_bridge::for_generated::WireSyncRust2DartSse {
1186 + FLUTTER_RUST_BRIDGE_HANDLER.wrap_sync::<flutter_rust_bridge::for_generated::SseCodec, _>(
1187 + flutter_rust_bridge::for_generated::TaskInfo {
1188 + debug_name: "qwen3_0_6b_config",
1189 + port: None,
1190 + mode: flutter_rust_bridge::for_generated::FfiCallMode::Sync,
1191 + },
1192 + move || {
1193 + let message = unsafe {
1194 + flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire(
1195 + ptr_,
1196 + rust_vec_len_,
1197 + data_len_,
1198 + )
1199 + };
1200 + let mut deserializer =
1201 + flutter_rust_bridge::for_generated::SseDeserializer::new(message);
1202 + deserializer.end();
1203 + transform_result_sse::<_, ()>((move || {
1204 + let output_ok = Result::<_, ()>::Ok(crate::api::qwen3_0_6b_config())?;
1205 + Ok(output_ok)
1206 + })())
1207 + },
1208 + )
1209 +}
1210 +fn wire__crate__api__qwen3_14b_config_impl(
1211 + ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr,
1212 + rust_vec_len_: i32,
1213 + data_len_: i32,
1214 +) -> flutter_rust_bridge::for_generated::WireSyncRust2DartSse {
1215 + FLUTTER_RUST_BRIDGE_HANDLER.wrap_sync::<flutter_rust_bridge::for_generated::SseCodec, _>(
1216 + flutter_rust_bridge::for_generated::TaskInfo {
1217 + debug_name: "qwen3_14b_config",
1218 + port: None,
1219 + mode: flutter_rust_bridge::for_generated::FfiCallMode::Sync,
1220 + },
1221 + move || {
1222 + let message = unsafe {
1223 + flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire(
1224 + ptr_,
1225 + rust_vec_len_,
1226 + data_len_,
1227 + )
1228 + };
1229 + let mut deserializer =
1230 + flutter_rust_bridge::for_generated::SseDeserializer::new(message);
1231 + deserializer.end();
1232 + transform_result_sse::<_, ()>((move || {
1233 + let output_ok = Result::<_, ()>::Ok(crate::api::qwen3_14b_config())?;
1234 + Ok(output_ok)
1235 + })())
1236 + },
1237 + )
1238 +}
1239 +fn wire__crate__api__qwen3_1_7b_config_impl(
1240 + ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr,
1241 + rust_vec_len_: i32,
1242 + data_len_: i32,
1243 +) -> flutter_rust_bridge::for_generated::WireSyncRust2DartSse {
1244 + FLUTTER_RUST_BRIDGE_HANDLER.wrap_sync::<flutter_rust_bridge::for_generated::SseCodec, _>(
1245 + flutter_rust_bridge::for_generated::TaskInfo {
1246 + debug_name: "qwen3_1_7b_config",
1247 + port: None,
1248 + mode: flutter_rust_bridge::for_generated::FfiCallMode::Sync,
1249 + },
1250 + move || {
1251 + let message = unsafe {
1252 + flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire(
1253 + ptr_,
1254 + rust_vec_len_,
1255 + data_len_,
1256 + )
1257 + };
1258 + let mut deserializer =
1259 + flutter_rust_bridge::for_generated::SseDeserializer::new(message);
1260 + deserializer.end();
1261 + transform_result_sse::<_, ()>((move || {
1262 + let output_ok = Result::<_, ()>::Ok(crate::api::qwen3_1_7b_config())?;
1263 + Ok(output_ok)
1264 + })())
1265 + },
1266 + )
1267 +}
1268 +fn wire__crate__api__qwen3_30b_a3b_instruct_2507_config_impl(
1269 + ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr,
1270 + rust_vec_len_: i32,
1271 + data_len_: i32,
1272 +) -> flutter_rust_bridge::for_generated::WireSyncRust2DartSse {
1273 + FLUTTER_RUST_BRIDGE_HANDLER.wrap_sync::<flutter_rust_bridge::for_generated::SseCodec, _>(
1274 + flutter_rust_bridge::for_generated::TaskInfo {
1275 + debug_name: "qwen3_30b_a3b_instruct_2507_config",
1276 + port: None,
1277 + mode: flutter_rust_bridge::for_generated::FfiCallMode::Sync,
1278 + },
1279 + move || {
1280 + let message = unsafe {
1281 + flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire(
1282 + ptr_,
1283 + rust_vec_len_,
1284 + data_len_,
1285 + )
1286 + };
1287 + let mut deserializer =
1288 + flutter_rust_bridge::for_generated::SseDeserializer::new(message);
1289 + deserializer.end();
1290 + transform_result_sse::<_, ()>((move || {
1291 + let output_ok =
1292 + Result::<_, ()>::Ok(crate::api::qwen3_30b_a3b_instruct_2507_config())?;
1293 + Ok(output_ok)
1294 + })())
1295 + },
1296 + )
1297 +}
1298 +fn wire__crate__api__qwen3_32b_config_impl(
1299 + ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr,
1300 + rust_vec_len_: i32,
1301 + data_len_: i32,
1302 +) -> flutter_rust_bridge::for_generated::WireSyncRust2DartSse {
1303 + FLUTTER_RUST_BRIDGE_HANDLER.wrap_sync::<flutter_rust_bridge::for_generated::SseCodec, _>(
1304 + flutter_rust_bridge::for_generated::TaskInfo {
1305 + debug_name: "qwen3_32b_config",
1306 + port: None,
1307 + mode: flutter_rust_bridge::for_generated::FfiCallMode::Sync,
1308 + },
1309 + move || {
1310 + let message = unsafe {
1311 + flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire(
1312 + ptr_,
1313 + rust_vec_len_,
1314 + data_len_,
1315 + )
1316 + };
1317 + let mut deserializer =
1318 + flutter_rust_bridge::for_generated::SseDeserializer::new(message);
1319 + deserializer.end();
1320 + transform_result_sse::<_, ()>((move || {
1321 + let output_ok = Result::<_, ()>::Ok(crate::api::qwen3_32b_config())?;
1322 + Ok(output_ok)
1323 + })())
1324 + },
1325 + )
1326 +}
1327 +fn wire__crate__api__qwen3_4b_config_impl(
1328 + ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr,
1329 + rust_vec_len_: i32,
1330 + data_len_: i32,
1331 +) -> flutter_rust_bridge::for_generated::WireSyncRust2DartSse {
1332 + FLUTTER_RUST_BRIDGE_HANDLER.wrap_sync::<flutter_rust_bridge::for_generated::SseCodec, _>(
1333 + flutter_rust_bridge::for_generated::TaskInfo {
1334 + debug_name: "qwen3_4b_config",
1335 + port: None,
1336 + mode: flutter_rust_bridge::for_generated::FfiCallMode::Sync,
1337 + },
1338 + move || {
1339 + let message = unsafe {
1340 + flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire(
1341 + ptr_,
1342 + rust_vec_len_,
1343 + data_len_,
1344 + )
1345 + };
1346 + let mut deserializer =
1347 + flutter_rust_bridge::for_generated::SseDeserializer::new(message);
1348 + deserializer.end();
1349 + transform_result_sse::<_, ()>((move || {
1350 + let output_ok = Result::<_, ()>::Ok(crate::api::qwen3_4b_config())?;
1351 + Ok(output_ok)
1352 + })())
1353 + },
1354 + )
1355 +}
1356 +fn wire__crate__api__qwen3_4b_instruct_2507_config_impl(
1357 + ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr,
1358 + rust_vec_len_: i32,
1359 + data_len_: i32,
1360 +) -> flutter_rust_bridge::for_generated::WireSyncRust2DartSse {
1361 + FLUTTER_RUST_BRIDGE_HANDLER.wrap_sync::<flutter_rust_bridge::for_generated::SseCodec, _>(
1362 + flutter_rust_bridge::for_generated::TaskInfo {
1363 + debug_name: "qwen3_4b_instruct_2507_config",
1364 + port: None,
1365 + mode: flutter_rust_bridge::for_generated::FfiCallMode::Sync,
1366 + },
1367 + move || {
1368 + let message = unsafe {
1369 + flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire(
1370 + ptr_,
1371 + rust_vec_len_,
1372 + data_len_,
1373 + )
1374 + };
1375 + let mut deserializer =
1376 + flutter_rust_bridge::for_generated::SseDeserializer::new(message);
1377 + deserializer.end();
1378 + transform_result_sse::<_, ()>((move || {
1379 + let output_ok = Result::<_, ()>::Ok(crate::api::qwen3_4b_instruct_2507_config())?;
1380 + Ok(output_ok)
1381 + })())
1382 + },
1383 + )
1384 +}
1385 +fn wire__crate__api__qwen3_4b_thinking_2507_config_impl(
1386 + ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr,
1387 + rust_vec_len_: i32,
1388 + data_len_: i32,
1389 +) -> flutter_rust_bridge::for_generated::WireSyncRust2DartSse {
1390 + FLUTTER_RUST_BRIDGE_HANDLER.wrap_sync::<flutter_rust_bridge::for_generated::SseCodec, _>(
1391 + flutter_rust_bridge::for_generated::TaskInfo {
1392 + debug_name: "qwen3_4b_thinking_2507_config",
1393 + port: None,
1394 + mode: flutter_rust_bridge::for_generated::FfiCallMode::Sync,
1395 + },
1396 + move || {
1397 + let message = unsafe {
1398 + flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire(
1399 + ptr_,
1400 + rust_vec_len_,
1401 + data_len_,
1402 + )
1403 + };
1404 + let mut deserializer =
1405 + flutter_rust_bridge::for_generated::SseDeserializer::new(message);
1406 + deserializer.end();
1407 + transform_result_sse::<_, ()>((move || {
1408 + let output_ok = Result::<_, ()>::Ok(crate::api::qwen3_4b_thinking_2507_config())?;
1409 + Ok(output_ok)
1410 + })())
1411 + },
1412 + )
1413 +}
1414 +fn wire__crate__api__qwen3_8b_config_impl(
1415 + ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr,
1416 + rust_vec_len_: i32,
1417 + data_len_: i32,
1418 +) -> flutter_rust_bridge::for_generated::WireSyncRust2DartSse {
1419 + FLUTTER_RUST_BRIDGE_HANDLER.wrap_sync::<flutter_rust_bridge::for_generated::SseCodec, _>(
1420 + flutter_rust_bridge::for_generated::TaskInfo {
1421 + debug_name: "qwen3_8b_config",
1422 + port: None,
1423 + mode: flutter_rust_bridge::for_generated::FfiCallMode::Sync,
1424 + },
1425 + move || {
1426 + let message = unsafe {
1427 + flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire(
1428 + ptr_,
1429 + rust_vec_len_,
1430 + data_len_,
1431 + )
1432 + };
1433 + let mut deserializer =
1434 + flutter_rust_bridge::for_generated::SseDeserializer::new(message);
1435 + deserializer.end();
1436 + transform_result_sse::<_, ()>((move || {
1437 + let output_ok = Result::<_, ()>::Ok(crate::api::qwen3_8b_config())?;
1438 + Ok(output_ok)
1439 + })())
1440 + },
1441 + )
1442 +}
1443
1444 // Section: related_funcs
1445
@@ -1632,6 +1894,17 @@ fn pde_ffi_dispatcher_sync_impl(
1894 22 => wire__crate__api__qwen25_3b_config_impl(ptr, rust_vec_len, data_len),
1895 23 => wire__crate__api__qwen25_coder_1_5b_config_impl(ptr, rust_vec_len, data_len),
1896 24 => wire__crate__api__qwen25_coder_3b_config_impl(ptr, rust_vec_len, data_len),
1897 + 25 => wire__crate__api__qwen3_0_6b_config_impl(ptr, rust_vec_len, data_len),
1898 + 26 => wire__crate__api__qwen3_14b_config_impl(ptr, rust_vec_len, data_len),
1899 + 27 => wire__crate__api__qwen3_1_7b_config_impl(ptr, rust_vec_len, data_len),
1900 + 28 => {
1901 + wire__crate__api__qwen3_30b_a3b_instruct_2507_config_impl(ptr, rust_vec_len, data_len)
1902 + }
1903 + 29 => wire__crate__api__qwen3_32b_config_impl(ptr, rust_vec_len, data_len),
1904 + 30 => wire__crate__api__qwen3_4b_config_impl(ptr, rust_vec_len, data_len),
1905 + 31 => wire__crate__api__qwen3_4b_instruct_2507_config_impl(ptr, rust_vec_len, data_len),
1906 + 32 => wire__crate__api__qwen3_4b_thinking_2507_config_impl(ptr, rust_vec_len, data_len),
1907 + 33 => wire__crate__api__qwen3_8b_config_impl(ptr, rust_vec_len, data_len),
1908 _ => unreachable!(),
1909 }
1910 }
sdk/dart/test/dart_test.dart
+77
@@ -14,6 +14,17 @@ class _FakeOndeChatEngine implements OndeChatEngine {
14 }
15
16 class _MockRustLibApi implements frb.RustLibApi {
17 + static GgufModelConfig _qwen3(
18 + String variant,
19 + String filename,
20 + String memory,
21 + ) => GgufModelConfig(
22 + modelId: 'bartowski/Qwen_Qwen3-$variant-GGUF',
23 + files: [filename],
24 + displayName: 'Qwen 3 $variant',
25 + approxMemory: memory,
26 + );
27 +
28 static const _qwen2515b = GgufModelConfig(
29 modelId: 'bartowski/Qwen2.5-1.5B-Instruct-GGUF',
30 files: ['Qwen2.5-1.5B-Instruct-Q4_K_M.gguf'],
@@ -87,6 +98,51 @@ class _MockRustLibApi implements frb.RustLibApi {
98 @override
99 GgufModelConfig crateApiQwen25Coder3BConfig() => _qwen25Coder3b;
100
101 + @override
102 + GgufModelConfig crateApiQwen306BConfig() =>
103 + _qwen3('0.6B', 'Qwen_Qwen3-0.6B-Q4_K_M.gguf', '~0.5 GB');
104 +
105 + @override
106 + GgufModelConfig crateApiQwen317BConfig() =>
107 + _qwen3('1.7B', 'Qwen_Qwen3-1.7B-Q4_K_M.gguf', '~1.3 GB');
108 +
109 + @override
110 + GgufModelConfig crateApiQwen34BConfig() =>
111 + _qwen3('4B', 'Qwen_Qwen3-4B-Q4_K_M.gguf', '~2.5 GB');
112 +
113 + @override
114 + GgufModelConfig crateApiQwen38BConfig() =>
115 + _qwen3('8B', 'Qwen_Qwen3-8B-Q4_K_M.gguf', '~5 GB');
116 +
117 + @override
118 + GgufModelConfig crateApiQwen314BConfig() =>
119 + _qwen3('14B', 'Qwen_Qwen3-14B-Q4_K_M.gguf', '~8.4 GB');
120 +
121 + @override
122 + GgufModelConfig crateApiQwen332BConfig() =>
123 + _qwen3('32B', 'Qwen_Qwen3-32B-Q4_K_M.gguf', '~19.8 GB');
124 +
125 + @override
126 + GgufModelConfig crateApiQwen34BInstruct2507Config() => _qwen3(
127 + '4B-Instruct-2507',
128 + 'Qwen_Qwen3-4B-Instruct-2507-Q4_K_M.gguf',
129 + '~2.5 GB',
130 + );
131 +
132 + @override
133 + GgufModelConfig crateApiQwen34BThinking2507Config() => _qwen3(
134 + '4B-Thinking-2507',
135 + 'Qwen_Qwen3-4B-Thinking-2507-Q4_K_M.gguf',
136 + '~2.5 GB',
137 + );
138 +
139 + @override
140 + GgufModelConfig crateApiQwen330BA3BInstruct2507Config() => _qwen3(
141 + '30B-A3B-Instruct-2507',
142 + 'Qwen_Qwen3-30B-A3B-Instruct-2507-Q4_K_M.gguf',
143 + '~18.6 GB',
144 + );
145 +
146 @override
147 dynamic noSuchMethod(Invocation invocation) =>
148 throw UnimplementedError('Unexpected RustLibApi member: $invocation');
@@ -201,6 +257,27 @@ void main() {
257
258 final defaultConfig = OndeInference.defaultModelConfig();
259 expect(defaultConfig.modelId, isNotEmpty);
260 +
261 + final qwen3Configs = [
262 + OndeInference.qwen306bConfig(),
263 + OndeInference.qwen317bConfig(),
264 + OndeInference.qwen34bConfig(),
265 + OndeInference.qwen38bConfig(),
266 + OndeInference.qwen314bConfig(),
267 + OndeInference.qwen332bConfig(),
268 + OndeInference.qwen34bInstruct2507Config(),
269 + OndeInference.qwen34bThinking2507Config(),
270 + OndeInference.qwen330bA3bInstruct2507Config(),
271 + ];
272 + expect(qwen3Configs, everyElement(isA<GgufModelConfig>()));
273 + expect(
274 + qwen3Configs,
275 + everyElement(
276 + predicate<GgufModelConfig>(
277 + (config) => config.modelId.contains('Qwen3'),
278 + ),
279 + ),
280 + );
281 });
282
283 test('equality', () {
sdk/kotlin/lib/src/shared/kotlin/com/ondeinference/onde/Convenience.kt
+4 -3
@@ -1,5 +1,6 @@
1 -// Copyright 2025 Onde Inference (Splitfire AB). All rights reserved.
2 -// SPDX-License-Identifier: MIT OR Apache-2.0
1 +// Copyright 2026 Onde Inference (Splitfire AB). All rights reserved.
2 +// Use of this source code is governed by the MIT license.
3 +//
4
5 package com.ondeinference.onde
6
@@ -41,7 +42,7 @@ object OndeModels {
42 fun qwen3_32b(): GgufModelConfig = uniffi.onde.qwen332bConfig()
43 /** Qwen 3 4B Instruct 2507 GGUF Q4_K_M (~2.5 GB) — latest non-thinking 4B. */
44 fun qwen3_4b_instruct_2507(): GgufModelConfig = uniffi.onde.qwen34bInstruct2507Config()
44 - /** Qwen 3 4B Thinking 2507 GGUF Q4_K_M (~2.5 GB) — latest reasoning 4B. */
45 + /** Qwen 3 4B Thinking 2507 (~2.5 GB); nil sampling automatically uses 4096 tokens. */
46 fun qwen3_4b_thinking_2507(): GgufModelConfig = uniffi.onde.qwen34bThinking2507Config()
47 /** Qwen 3 30B-A3B Instruct 2507 GGUF Q4_K_M (~18.6 GB) — flagship MoE. */
48 fun qwen3_30b_a3b_instruct_2507(): GgufModelConfig = uniffi.onde.qwen330bA3bInstruct2507Config()
sdk/react-native/android/src/main/java/com/ondeinference/OndeInferenceModule.kt
+21
@@ -1,3 +1,6 @@
1 +// Copyright 2026 Onde Inference (Splitfire AB). All rights reserved.
2 +// Use of this source code is governed by the MIT license.
3 +//
4 package com.ondeinference
5
6 import expo.modules.kotlin.modules.Module
@@ -48,6 +51,15 @@ class OndeInferenceModule : Module() {
51 private external fun nativeDefaultModelConfig(): String
52 private external fun nativeQwen251_5bConfig(): String
53 private external fun nativeQwen253bConfig(): String
54 + private external fun nativeQwen3_0_6bConfig(): String
55 + private external fun nativeQwen3_1_7bConfig(): String
56 + private external fun nativeQwen3_4bConfig(): String
57 + private external fun nativeQwen3_8bConfig(): String
58 + private external fun nativeQwen3_14bConfig(): String
59 + private external fun nativeQwen3_32bConfig(): String
60 + private external fun nativeQwen3_4bInstruct2507Config(): String
61 + private external fun nativeQwen3_4bThinking2507Config(): String
62 + private external fun nativeQwen3_30bA3bInstruct2507Config(): String
63 private external fun nativeDefaultSamplingConfig(): String
64 private external fun nativeDeterministicSamplingConfig(): String
65 private external fun nativeMobileSamplingConfig(): String
@@ -101,6 +113,15 @@ class OndeInferenceModule : Module() {
113 Function("defaultModelConfig") { nativeDefaultModelConfig() }
114 Function("qwen251_5bConfig") { nativeQwen251_5bConfig() }
115 Function("qwen253bConfig") { nativeQwen253bConfig() }
116 + Function("qwen3_0_6bConfig") { nativeQwen3_0_6bConfig() }
117 + Function("qwen3_1_7bConfig") { nativeQwen3_1_7bConfig() }
118 + Function("qwen3_4bConfig") { nativeQwen3_4bConfig() }
119 + Function("qwen3_8bConfig") { nativeQwen3_8bConfig() }
120 + Function("qwen3_14bConfig") { nativeQwen3_14bConfig() }
121 + Function("qwen3_32bConfig") { nativeQwen3_32bConfig() }
122 + Function("qwen3_4bInstruct2507Config") { nativeQwen3_4bInstruct2507Config() }
123 + Function("qwen3_4bThinking2507Config") { nativeQwen3_4bThinking2507Config() }
124 + Function("qwen3_30bA3bInstruct2507Config") { nativeQwen3_30bA3bInstruct2507Config() }
125 Function("defaultSamplingConfig") { nativeDefaultSamplingConfig() }
126 Function("deterministicSamplingConfig") { nativeDeterministicSamplingConfig() }
127 Function("mobileSamplingConfig") { nativeMobileSamplingConfig() }
sdk/react-native/ios/OndeInferenceModule.swift
+69
@@ -1,3 +1,6 @@
1 +// Copyright 2026 Onde Inference (Splitfire AB). All rights reserved.
2 +// Use of this source code is governed by the MIT license.
3 +//
4 import ExpoModulesCore
5 import Foundation
6
@@ -86,6 +89,33 @@ func onde_qwen25_1_5b_config() -> UnsafeMutablePointer<CChar>?
89 @_silgen_name("onde_qwen25_3b_config")
90 func onde_qwen25_3b_config() -> UnsafeMutablePointer<CChar>?
91
92 +@_silgen_name("onde_qwen3_0_6b_config")
93 +func onde_qwen3_0_6b_config() -> UnsafeMutablePointer<CChar>?
94 +
95 +@_silgen_name("onde_qwen3_1_7b_config")
96 +func onde_qwen3_1_7b_config() -> UnsafeMutablePointer<CChar>?
97 +
98 +@_silgen_name("onde_qwen3_4b_config")
99 +func onde_qwen3_4b_config() -> UnsafeMutablePointer<CChar>?
100 +
101 +@_silgen_name("onde_qwen3_8b_config")
102 +func onde_qwen3_8b_config() -> UnsafeMutablePointer<CChar>?
103 +
104 +@_silgen_name("onde_qwen3_14b_config")
105 +func onde_qwen3_14b_config() -> UnsafeMutablePointer<CChar>?
106 +
107 +@_silgen_name("onde_qwen3_32b_config")
108 +func onde_qwen3_32b_config() -> UnsafeMutablePointer<CChar>?
109 +
110 +@_silgen_name("onde_qwen3_4b_instruct_2507_config")
111 +func onde_qwen3_4b_instruct_2507_config() -> UnsafeMutablePointer<CChar>?
112 +
113 +@_silgen_name("onde_qwen3_4b_thinking_2507_config")
114 +func onde_qwen3_4b_thinking_2507_config() -> UnsafeMutablePointer<CChar>?
115 +
116 +@_silgen_name("onde_qwen3_30b_a3b_instruct_2507_config")
117 +func onde_qwen3_30b_a3b_instruct_2507_config() -> UnsafeMutablePointer<CChar>?
118 +
119 // Sampling config free functions
120 @_silgen_name("onde_default_sampling_config")
121 func onde_default_sampling_config() -> UnsafeMutablePointer<CChar>?
@@ -270,6 +300,45 @@ public class OndeInferenceModule: Module {
300 return try self.validateJsonResult(result)
301 }
302
303 + Function("qwen3_0_6bConfig") { () -> String in
304 + try self.validateJsonResult(self.consumeRustString(onde_qwen3_0_6b_config()))
305 + }
306 +
307 + Function("qwen3_1_7bConfig") { () -> String in
308 + try self.validateJsonResult(self.consumeRustString(onde_qwen3_1_7b_config()))
309 + }
310 +
311 + Function("qwen3_4bConfig") { () -> String in
312 + try self.validateJsonResult(self.consumeRustString(onde_qwen3_4b_config()))
313 + }
314 +
315 + Function("qwen3_8bConfig") { () -> String in
316 + try self.validateJsonResult(self.consumeRustString(onde_qwen3_8b_config()))
317 + }
318 +
319 + Function("qwen3_14bConfig") { () -> String in
320 + try self.validateJsonResult(self.consumeRustString(onde_qwen3_14b_config()))
321 + }
322 +
323 + Function("qwen3_32bConfig") { () -> String in
324 + try self.validateJsonResult(self.consumeRustString(onde_qwen3_32b_config()))
325 + }
326 +
327 + Function("qwen3_4bInstruct2507Config") { () -> String in
328 + try self.validateJsonResult(
329 + self.consumeRustString(onde_qwen3_4b_instruct_2507_config()))
330 + }
331 +
332 + Function("qwen3_4bThinking2507Config") { () -> String in
333 + try self.validateJsonResult(
334 + self.consumeRustString(onde_qwen3_4b_thinking_2507_config()))
335 + }
336 +
337 + Function("qwen3_30bA3bInstruct2507Config") { () -> String in
338 + try self.validateJsonResult(
339 + self.consumeRustString(onde_qwen3_30b_a3b_instruct_2507_config()))
340 + }
341 +
342 Function("defaultSamplingConfig") { () -> String in
343 let result = self.consumeRustString(onde_default_sampling_config())
344 return try self.validateJsonResult(result)
sdk/react-native/package-lock.json
+2 -2
@@ -1,12 +1,12 @@
1 {
2 "name": "@ondeinference/react-native",
3 - "version": "1.1.1",
3 + "version": "1.1.2",
4 "lockfileVersion": 3,
5 "requires": true,
6 "packages": {
7 "": {
8 "name": "@ondeinference/react-native",
9 - "version": "1.1.1",
9 + "version": "1.1.2",
10 "license": "MIT OR Apache-2.0",
11 "devDependencies": {
12 "expo-modules-core": "^2.0.0",
sdk/react-native/rust/src/lib.rs
+96 -21
@@ -1,3 +1,6 @@
1 +// Copyright 2026 Onde Inference (Splitfire AB). All rights reserved.
2 +// Use of this source code is governed by the MIT license.
3 +//
4 //! C FFI bridge for the Onde React Native npm SDK.
5 //!
6 //! This crate wraps [`onde::inference::engine::ChatEngine`] with `extern "C"`
@@ -615,7 +618,7 @@ mod android {
618 // ── Engine lifecycle ─────────────────────────────────────────────────
619
620 #[no_mangle]
618 - pub extern "system" fn Java_com_ondeinference_OndeInferenceModule_ondeEngineCreate(
621 + pub extern "system" fn Java_com_ondeinference_OndeInferenceModule_nativeEngineCreate(
622 _env: JNIEnv,
623 _class: JClass,
624 ) -> jlong {
@@ -623,7 +626,7 @@ mod android {
626 }
627
628 #[no_mangle]
626 - pub extern "system" fn Java_com_ondeinference_OndeInferenceModule_ondeEngineDestroy(
629 + pub extern "system" fn Java_com_ondeinference_OndeInferenceModule_nativeEngineDestroy(
630 _env: JNIEnv,
631 _class: JClass,
632 engine: jlong,
@@ -632,7 +635,7 @@ mod android {
635 }
636
637 #[no_mangle]
635 - pub extern "system" fn Java_com_ondeinference_OndeInferenceModule_ondeEngineLoadDefaultModel(
638 + pub extern "system" fn Java_com_ondeinference_OndeInferenceModule_nativeEngineLoadDefaultModel(
639 mut env: JNIEnv,
640 _class: JClass,
641 engine: jlong,
@@ -657,7 +660,7 @@ mod android {
660 }
661
662 #[no_mangle]
660 - pub extern "system" fn Java_com_ondeinference_OndeInferenceModule_ondeEngineLoadModel(
663 + pub extern "system" fn Java_com_ondeinference_OndeInferenceModule_nativeEngineLoadModel(
664 mut env: JNIEnv,
665 _class: JClass,
666 engine: jlong,
@@ -688,7 +691,7 @@ mod android {
691 }
692
693 #[no_mangle]
691 - pub extern "system" fn Java_com_ondeinference_OndeInferenceModule_ondeEngineUnloadModel(
694 + pub extern "system" fn Java_com_ondeinference_OndeInferenceModule_nativeEngineUnloadModel(
695 mut env: JNIEnv,
696 _class: JClass,
697 engine: jlong,
@@ -698,7 +701,7 @@ mod android {
701 }
702
703 #[no_mangle]
701 - pub extern "system" fn Java_com_ondeinference_OndeInferenceModule_ondeEngineIsLoaded(
704 + pub extern "system" fn Java_com_ondeinference_OndeInferenceModule_nativeEngineIsLoaded(
705 _env: JNIEnv,
706 _class: JClass,
707 engine: jlong,
@@ -713,7 +716,7 @@ mod android {
716 // ── Engine info ──────────────────────────────────────────────────────
717
718 #[no_mangle]
716 - pub extern "system" fn Java_com_ondeinference_OndeInferenceModule_ondeEngineInfo(
719 + pub extern "system" fn Java_com_ondeinference_OndeInferenceModule_nativeEngineInfo(
720 mut env: JNIEnv,
721 _class: JClass,
722 engine: jlong,
@@ -725,7 +728,7 @@ mod android {
728 // ── System prompt ────────────────────────────────────────────────────
729
730 #[no_mangle]
728 - pub extern "system" fn Java_com_ondeinference_OndeInferenceModule_ondeEngineSetSystemPrompt(
731 + pub extern "system" fn Java_com_ondeinference_OndeInferenceModule_nativeEngineSetSystemPrompt(
732 mut env: JNIEnv,
733 _class: JClass,
734 engine: jlong,
@@ -740,7 +743,7 @@ mod android {
743 }
744
745 #[no_mangle]
743 - pub extern "system" fn Java_com_ondeinference_OndeInferenceModule_ondeEngineClearSystemPrompt(
746 + pub extern "system" fn Java_com_ondeinference_OndeInferenceModule_nativeEngineClearSystemPrompt(
747 _env: JNIEnv,
748 _class: JClass,
749 engine: jlong,
@@ -751,7 +754,7 @@ mod android {
754 // ── Sampling ─────────────────────────────────────────────────────────
755
756 #[no_mangle]
754 - pub extern "system" fn Java_com_ondeinference_OndeInferenceModule_ondeEngineSetSampling(
757 + pub extern "system" fn Java_com_ondeinference_OndeInferenceModule_nativeEngineSetSampling(
758 mut env: JNIEnv,
759 _class: JClass,
760 engine: jlong,
@@ -768,7 +771,7 @@ mod android {
771 // ── History ──────────────────────────────────────────────────────────
772
773 #[no_mangle]
771 - pub extern "system" fn Java_com_ondeinference_OndeInferenceModule_ondeEngineHistory(
774 + pub extern "system" fn Java_com_ondeinference_OndeInferenceModule_nativeEngineHistory(
775 mut env: JNIEnv,
776 _class: JClass,
777 engine: jlong,
@@ -778,7 +781,7 @@ mod android {
781 }
782
783 #[no_mangle]
781 - pub extern "system" fn Java_com_ondeinference_OndeInferenceModule_ondeEngineClearHistory(
784 + pub extern "system" fn Java_com_ondeinference_OndeInferenceModule_nativeEngineClearHistory(
785 _env: JNIEnv,
786 _class: JClass,
787 engine: jlong,
@@ -787,7 +790,7 @@ mod android {
790 }
791
792 #[no_mangle]
790 - pub extern "system" fn Java_com_ondeinference_OndeInferenceModule_ondeEnginePushHistory(
793 + pub extern "system" fn Java_com_ondeinference_OndeInferenceModule_nativeEnginePushHistory(
794 mut env: JNIEnv,
795 _class: JClass,
796 engine: jlong,
@@ -804,7 +807,7 @@ mod android {
807 // ── Inference ────────────────────────────────────────────────────────
808
809 #[no_mangle]
807 - pub extern "system" fn Java_com_ondeinference_OndeInferenceModule_ondeEngineSendMessage(
810 + pub extern "system" fn Java_com_ondeinference_OndeInferenceModule_nativeEngineSendMessage(
811 mut env: JNIEnv,
812 _class: JClass,
813 engine: jlong,
@@ -820,7 +823,7 @@ mod android {
823 }
824
825 #[no_mangle]
823 - pub extern "system" fn Java_com_ondeinference_OndeInferenceModule_ondeEngineGenerate(
826 + pub extern "system" fn Java_com_ondeinference_OndeInferenceModule_nativeEngineGenerate(
827 mut env: JNIEnv,
828 _class: JClass,
829 engine: jlong,
@@ -846,7 +849,7 @@ mod android {
849 // ── Model config presets ─────────────────────────────────────────────
850
851 #[no_mangle]
849 - pub extern "system" fn Java_com_ondeinference_OndeInferenceModule_ondeDefaultModelConfig(
852 + pub extern "system" fn Java_com_ondeinference_OndeInferenceModule_nativeDefaultModelConfig(
853 mut env: JNIEnv,
854 _class: JClass,
855 ) -> jstring {
@@ -855,7 +858,7 @@ mod android {
858 }
859
860 #[no_mangle]
858 - pub extern "system" fn Java_com_ondeinference_OndeInferenceModule_ondeQwen251_5bConfig(
861 + pub extern "system" fn Java_com_ondeinference_OndeInferenceModule_nativeQwen251_15bConfig(
862 mut env: JNIEnv,
863 _class: JClass,
864 ) -> jstring {
@@ -864,7 +867,7 @@ mod android {
867 }
868
869 #[no_mangle]
867 - pub extern "system" fn Java_com_ondeinference_OndeInferenceModule_ondeQwen253bConfig(
870 + pub extern "system" fn Java_com_ondeinference_OndeInferenceModule_nativeQwen253bConfig(
871 mut env: JNIEnv,
872 _class: JClass,
873 ) -> jstring {
@@ -872,10 +875,82 @@ mod android {
875 cstring_ptr_to_jstring(&mut env, result)
876 }
877
878 + #[no_mangle]
879 + pub extern "system" fn Java_com_ondeinference_OndeInferenceModule_nativeQwen3_10_16bConfig(
880 + mut env: JNIEnv,
881 + _class: JClass,
882 + ) -> jstring {
883 + cstring_ptr_to_jstring(&mut env, onde_qwen3_0_6b_config())
884 + }
885 +
886 + #[no_mangle]
887 + pub extern "system" fn Java_com_ondeinference_OndeInferenceModule_nativeQwen3_11_17bConfig(
888 + mut env: JNIEnv,
889 + _class: JClass,
890 + ) -> jstring {
891 + cstring_ptr_to_jstring(&mut env, onde_qwen3_1_7b_config())
892 + }
893 +
894 + #[no_mangle]
895 + pub extern "system" fn Java_com_ondeinference_OndeInferenceModule_nativeQwen3_14bConfig(
896 + mut env: JNIEnv,
897 + _class: JClass,
898 + ) -> jstring {
899 + cstring_ptr_to_jstring(&mut env, onde_qwen3_4b_config())
900 + }
901 +
902 + #[no_mangle]
903 + pub extern "system" fn Java_com_ondeinference_OndeInferenceModule_nativeQwen3_18bConfig(
904 + mut env: JNIEnv,
905 + _class: JClass,
906 + ) -> jstring {
907 + cstring_ptr_to_jstring(&mut env, onde_qwen3_8b_config())
908 + }
909 +
910 + #[no_mangle]
911 + pub extern "system" fn Java_com_ondeinference_OndeInferenceModule_nativeQwen3_114bConfig(
912 + mut env: JNIEnv,
913 + _class: JClass,
914 + ) -> jstring {
915 + cstring_ptr_to_jstring(&mut env, onde_qwen3_14b_config())
916 + }
917 +
918 + #[no_mangle]
919 + pub extern "system" fn Java_com_ondeinference_OndeInferenceModule_nativeQwen3_132bConfig(
920 + mut env: JNIEnv,
921 + _class: JClass,
922 + ) -> jstring {
923 + cstring_ptr_to_jstring(&mut env, onde_qwen3_32b_config())
924 + }
925 +
926 + #[no_mangle]
927 + pub extern "system" fn Java_com_ondeinference_OndeInferenceModule_nativeQwen3_14bInstruct2507Config(
928 + mut env: JNIEnv,
929 + _class: JClass,
930 + ) -> jstring {
931 + cstring_ptr_to_jstring(&mut env, onde_qwen3_4b_instruct_2507_config())
932 + }
933 +
934 + #[no_mangle]
935 + pub extern "system" fn Java_com_ondeinference_OndeInferenceModule_nativeQwen3_14bThinking2507Config(
936 + mut env: JNIEnv,
937 + _class: JClass,
938 + ) -> jstring {
939 + cstring_ptr_to_jstring(&mut env, onde_qwen3_4b_thinking_2507_config())
940 + }
941 +
942 + #[no_mangle]
943 + pub extern "system" fn Java_com_ondeinference_OndeInferenceModule_nativeQwen3_130bA3bInstruct2507Config(
944 + mut env: JNIEnv,
945 + _class: JClass,
946 + ) -> jstring {
947 + cstring_ptr_to_jstring(&mut env, onde_qwen3_30b_a3b_instruct_2507_config())
948 + }
949 +
950 // ── Sampling presets ─────────────────────────────────────────────────
951
952 #[no_mangle]
878 - pub extern "system" fn Java_com_ondeinference_OndeInferenceModule_ondeDefaultSamplingConfig(
953 + pub extern "system" fn Java_com_ondeinference_OndeInferenceModule_nativeDefaultSamplingConfig(
954 mut env: JNIEnv,
955 _class: JClass,
956 ) -> jstring {
@@ -884,7 +959,7 @@ mod android {
959 }
960
961 #[no_mangle]
887 - pub extern "system" fn Java_com_ondeinference_OndeInferenceModule_ondeDeterministicSamplingConfig(
962 + pub extern "system" fn Java_com_ondeinference_OndeInferenceModule_nativeDeterministicSamplingConfig(
963 mut env: JNIEnv,
964 _class: JClass,
965 ) -> jstring {
@@ -893,7 +968,7 @@ mod android {
968 }
969
970 #[no_mangle]
896 - pub extern "system" fn Java_com_ondeinference_OndeInferenceModule_ondeMobileSamplingConfig(
971 + pub extern "system" fn Java_com_ondeinference_OndeInferenceModule_nativeMobileSamplingConfig(
972 mut env: JNIEnv,
973 _class: JClass,
974 ) -> jstring {
sdk/react-native/src/OndeInferenceModule.ts
+12
@@ -1,3 +1,6 @@
1 +// Copyright 2026 Onde Inference (Splitfire AB). All rights reserved.
2 +// Use of this source code is governed by the MIT license.
3 +//
4 import { requireNativeModule } from "expo-modules-core";
5
6 /**
@@ -42,6 +45,15 @@ export interface OndeInferenceNativeModule {
45 defaultModelConfig(): string;
46 qwen251_5bConfig(): string;
47 qwen253bConfig(): string;
48 + qwen3_0_6bConfig(): string;
49 + qwen3_1_7bConfig(): string;
50 + qwen3_4bConfig(): string;
51 + qwen3_8bConfig(): string;
52 + qwen3_14bConfig(): string;
53 + qwen3_32bConfig(): string;
54 + qwen3_4bInstruct2507Config(): string;
55 + qwen3_4bThinking2507Config(): string;
56 + qwen3_30bA3bInstruct2507Config(): string;
57 defaultSamplingConfig(): string;
58 deterministicSamplingConfig(): string;
59 mobileSamplingConfig(): string;
sdk/react-native/src/index.ts
+48
@@ -1,3 +1,6 @@
1 +// Copyright 2026 Onde Inference (Splitfire AB). All rights reserved.
2 +// Use of this source code is governed by the MIT license.
3 +//
4 import OndeInferenceModule from "./OndeInferenceModule";
5 import type {
6 ChatMessage,
@@ -318,6 +321,51 @@ export function qwen253bConfig(): GgufModelConfig {
321 return parseModelConfig(OndeInferenceModule.qwen253bConfig());
322 }
323
324 +/** Return the Qwen 3 0.6B config (~0.5 GB). */
325 +export function qwen3_0_6bConfig(): GgufModelConfig {
326 + return parseModelConfig(OndeInferenceModule.qwen3_0_6bConfig());
327 +}
328 +
329 +/** Return the Qwen 3 1.7B config (~1.3 GB). */
330 +export function qwen3_1_7bConfig(): GgufModelConfig {
331 + return parseModelConfig(OndeInferenceModule.qwen3_1_7bConfig());
332 +}
333 +
334 +/** Return the Qwen 3 4B config (~2.5 GB). */
335 +export function qwen3_4bConfig(): GgufModelConfig {
336 + return parseModelConfig(OndeInferenceModule.qwen3_4bConfig());
337 +}
338 +
339 +/** Return the Qwen 3 8B config (~5 GB). */
340 +export function qwen3_8bConfig(): GgufModelConfig {
341 + return parseModelConfig(OndeInferenceModule.qwen3_8bConfig());
342 +}
343 +
344 +/** Return the Qwen 3 14B config (~8.4 GB). */
345 +export function qwen3_14bConfig(): GgufModelConfig {
346 + return parseModelConfig(OndeInferenceModule.qwen3_14bConfig());
347 +}
348 +
349 +/** Return the Qwen 3 32B config (~19.8 GB). */
350 +export function qwen3_32bConfig(): GgufModelConfig {
351 + return parseModelConfig(OndeInferenceModule.qwen3_32bConfig());
352 +}
353 +
354 +/** Return the non-thinking Qwen 3 4B Instruct 2507 config (~2.5 GB). */
355 +export function qwen3_4bInstruct2507Config(): GgufModelConfig {
356 + return parseModelConfig(OndeInferenceModule.qwen3_4bInstruct2507Config());
357 +}
358 +
359 +/** Return the Qwen 3 4B Thinking 2507 config (~2.5 GB). */
360 +export function qwen3_4bThinking2507Config(): GgufModelConfig {
361 + return parseModelConfig(OndeInferenceModule.qwen3_4bThinking2507Config());
362 +}
363 +
364 +/** Return the Qwen 3 30B-A3B Instruct 2507 MoE config (~18.6 GB). */
365 +export function qwen3_30bA3bInstruct2507Config(): GgufModelConfig {
366 + return parseModelConfig(OndeInferenceModule.qwen3_30bA3bInstruct2507Config());
367 +}
368 +
369 /** Return default sampling parameters (temp=0.7, top_p=0.95, max_tokens=512). */
370 export function defaultSamplingConfig(): SamplingConfig {
371 return parseSamplingConfig(OndeInferenceModule.defaultSamplingConfig());
src/hf_cache.rs
+51 -63
@@ -1,3 +1,6 @@
1 +// Copyright 2026 Onde Inference (Splitfire AB). All rights reserved.
2 +// Use of this source code is governed by the MIT license.
3 +//
4 //! HuggingFace hub cache inspection, repair, and model download.
5 //!
6 //! This module owns all on-disk state management for locally cached
@@ -252,7 +255,9 @@ fn list_revisions(model_dir: &Path) -> Vec<String> {
255 target_os = "tvos",
256 target_os = "visionos",
257 target_os = "watchos",
255 - target_os = "android"
258 + target_os = "android",
259 + target_os = "windows",
260 + target_os = "linux"
261 ))]
262 fn make_progress(model_id: &str, downloaded: u64, total: u64, done: bool) -> ModelDownloadProgress {
263 let progress = if total > 0 {
@@ -990,7 +995,9 @@ pub fn list_supported_hf_models() -> SupportedHfModelsResponse {
995 target_os = "ios",
996 target_os = "tvos",
997 target_os = "visionos",
993 - target_os = "watchos"
998 + target_os = "watchos",
999 + target_os = "windows",
1000 + target_os = "linux"
1001 ))]
1002 pub async fn download_model<F>(
1003 model_id: String,
@@ -1000,11 +1007,8 @@ pub async fn download_model<F>(
1007 where
1008 F: Fn(ModelDownloadProgress) + Send + Sync + Clone + 'static,
1009 {
1003 - use crate::inference::models::{
1004 - BARTOWSKI_QWEN25_1_5B_INSTRUCT_GGUF, BARTOWSKI_QWEN25_3B_INSTRUCT_GGUF,
1005 - QWEN25_1_5B_GGUF_FILE, QWEN25_3B_GGUF_FILE,
1006 - };
1010 use crate::inference::token::hf_token_source;
1011 + use crate::inference::GgufModelConfig;
1012 use mistralrs::GgufModelBuilder;
1013 use std::sync::{
1014 atomic::{AtomicBool, Ordering},
@@ -1036,6 +1040,9 @@ where
1040 return Err(format!("Model {} is not supported.", model_id));
1041 }
1042
1043 + let config = GgufModelConfig::from_supported_model_id(&model_id)
1044 + .ok_or_else(|| format!("Model {model_id} has no download configuration."))?;
1045 +
1046 let expected_size = SUPPORTED_MODEL_INFO
1047 .iter()
1048 .find(|m| m.id == model_id)
@@ -1055,14 +1062,8 @@ where
1062 // Recover from interrupted downloads where the blob exists but the
1063 // snapshot entry was never created (process killed after download,
1064 // before hf-hub finalised the cache structure).
1058 - let expected_files: Vec<&str> = match model_id.as_str() {
1059 - BARTOWSKI_QWEN25_1_5B_INSTRUCT_GGUF => vec![QWEN25_1_5B_GGUF_FILE],
1060 - BARTOWSKI_QWEN25_3B_INSTRUCT_GGUF => vec![QWEN25_3B_GGUF_FILE],
1061 - _ => vec![],
1062 - };
1063 - if !expected_files.is_empty() {
1064 - ensure_snapshot_entries(&model_id, &expected_files);
1065 - }
1065 + let expected_files: Vec<&str> = config.files.iter().map(String::as_str).collect();
1066 + ensure_snapshot_entries(&model_id, &expected_files);
1067
1068 // Emit an initial progress event based on the actual cache size so that
1069 // resumed downloads don't visually restart from 0%.
@@ -1077,6 +1078,12 @@ where
1078 false,
1079 ));
1080
1081 + let mut builder =
1082 + GgufModelBuilder::new(&model_id, config.files.clone()).with_token_source(hf_token_source());
1083 + if let Some(chat_template) = config.chat_template {
1084 + builder = builder.with_chat_template(chat_template);
1085 + }
1086 +
1087 // Shared flag so the monitor task knows when to stop.
1088 let finished = Arc::new(AtomicBool::new(false));
1089 let finished_clone = Arc::clone(&finished);
@@ -1109,29 +1116,14 @@ where
1116 ));
1117 });
1118
1112 - // Build (and immediately drop) the model using the GGUF chat builder.
1113 - // Building triggers the HuggingFace hub download.
1114 - let result: Result<mistralrs::Model, anyhow::Error> = match model_id.as_str() {
1115 - BARTOWSKI_QWEN25_1_5B_INSTRUCT_GGUF => {
1116 - GgufModelBuilder::new(&model_id, vec![QWEN25_1_5B_GGUF_FILE])
1117 - .with_token_source(hf_token_source())
1118 - .build()
1119 - .await
1120 - }
1121 - BARTOWSKI_QWEN25_3B_INSTRUCT_GGUF => {
1122 - GgufModelBuilder::new(&model_id, vec![QWEN25_3B_GGUF_FILE])
1123 - .with_token_source(hf_token_source())
1124 - .build()
1125 - .await
1126 - }
1127 - _ => Err(anyhow::anyhow!(
1128 - "Model '{}' is not part of the chat-only release.",
1129 - model_id
1130 - )),
1131 - };
1119 + // Building triggers the HuggingFace download and also validates that the
1120 + // downloaded GGUF can be opened by the same backend used for inference.
1121 + let result = builder.build().await;
1122
1123 finished.store(true, Ordering::Relaxed);
1134 - let _ = monitor_handle.join();
1124 + if monitor_handle.join().is_err() {
1125 + warn!("Download progress monitor panicked for model {model_id}");
1126 + }
1127
1128 match result {
1129 Ok(_model) => {
@@ -1171,12 +1163,8 @@ pub async fn download_model<F>(
1163 where
1164 F: Fn(ModelDownloadProgress) + Send + Sync + Clone + 'static,
1165 {
1174 - use crate::inference::models::{
1175 - BARTOWSKI_QWEN25_1_5B_INSTRUCT_GGUF, BARTOWSKI_QWEN25_3B_INSTRUCT_GGUF,
1176 - QWEN25_1_5B_GGUF_FILE, QWEN25_1_5B_TOK_MODEL_ID, QWEN25_3B_GGUF_FILE,
1177 - QWEN25_3B_TOK_MODEL_ID,
1178 - };
1166 use crate::inference::token::hf_token_source;
1167 + use crate::inference::GgufModelConfig;
1168 use hf_hub::Cache;
1169 use mistralrs::GgufModelBuilder;
1170 use mistralrs_core::GLOBAL_HF_CACHE;
@@ -1186,21 +1174,12 @@ where
1174 };
1175 use std::time::Duration;
1176
1189 - let (gguf_file, tok_model_id) = match model_id.as_str() {
1190 - id if id == BARTOWSKI_QWEN25_1_5B_INSTRUCT_GGUF => {
1191 - (QWEN25_1_5B_GGUF_FILE, QWEN25_1_5B_TOK_MODEL_ID)
1192 - }
1193 - id if id == BARTOWSKI_QWEN25_3B_INSTRUCT_GGUF => {
1194 - (QWEN25_3B_GGUF_FILE, QWEN25_3B_TOK_MODEL_ID)
1195 - }
1196 - _ => {
1197 - return Err(format!(
1198 - "Model '{}' is not supported on Android. \
1199 - Only Qwen 2.5 1.5B or 3B (GGUF) are available.",
1200 - model_id
1201 - ));
1202 - }
1203 - };
1177 + let config = GgufModelConfig::from_supported_model_id(&model_id)
1178 + .ok_or_else(|| format!("Model {model_id} has no Android download configuration."))?;
1179 + let tok_model_id = config
1180 + .tok_model_id
1181 + .clone()
1182 + .ok_or_else(|| format!("Model {model_id} has no Android tokenizer configuration."))?;
1183
1184 // ── Resolve the HF cache path ─────────────────────────────────────────
1185 //
@@ -1240,6 +1219,8 @@ where
1219 );
1220
1221 clean_stale_lock_files(&model_id);
1222 + let expected_files: Vec<&str> = config.files.iter().map(String::as_str).collect();
1223 + ensure_snapshot_entries(&model_id, &expected_files);
1224 ensure_model_cache_dir(&model_id);
1225
1226 let initial_bytes = model_cache_path(&model_id)
@@ -1253,6 +1234,14 @@ where
1234 false,
1235 ));
1236
1237 + let mut builder = GgufModelBuilder::new(&model_id, config.files.clone())
1238 + .with_tok_model_id(&tok_model_id)
1239 + .with_token_source(hf_token_source())
1240 + .with_logging();
1241 + if let Some(chat_template) = config.chat_template {
1242 + builder = builder.with_chat_template(chat_template);
1243 + }
1244 +
1245 // Native monitor thread — same reasoning as the Apple implementation.
1246 let finished = Arc::new(AtomicBool::new(false));
1247 let finished_clone = Arc::clone(&finished);
@@ -1276,15 +1265,12 @@ where
1265 ));
1266 });
1267
1279 - let result = GgufModelBuilder::new(&model_id, vec![gguf_file])
1280 - .with_tok_model_id(tok_model_id)
1281 - .with_token_source(hf_token_source())
1282 - .with_logging()
1283 - .build()
1284 - .await;
1268 + let result = builder.build().await;
1269
1270 finished.store(true, Ordering::Relaxed);
1287 - let _ = monitor_handle.join();
1271 + if monitor_handle.join().is_err() {
1272 + warn!("Download progress monitor panicked for model {model_id}");
1273 + }
1274
1275 match result {
1276 Ok(_model) => {
@@ -1312,7 +1298,9 @@ where
1298 target_os = "tvos",
1299 target_os = "visionos",
1300 target_os = "watchos",
1315 - target_os = "android"
1301 + target_os = "android",
1302 + target_os = "windows",
1303 + target_os = "linux"
1304 )))]
1305 pub async fn download_model<F>(
1306 model_id: String,
src/inference/engine.rs
+71 -20
@@ -1,3 +1,6 @@
1 +// Copyright 2026 Onde Inference (Splitfire AB). All rights reserved.
2 +// Use of this source code is governed by the MIT license.
3 +//
4 //! On-device LLM chat inference engine powered by [mistral.rs](https://github.com/EricLBuehler/mistral.rs).
5 //!
6 //! `ChatEngine` provides a high-level, framework-agnostic API for:
@@ -429,25 +432,9 @@ impl ChatEngine {
432 }
433
434 // Some older GGUF files (e.g. TheBloke) do not embed a chat template.
432 - // When the config provides one, write it to a temporary .jinja file
433 - // and pass the path — mistral.rs only accepts file paths ending in
434 - // .json or .jinja, not literal template strings.
435 - let _chat_template_tempfile = if let Some(ref template) = config.chat_template {
436 - let tmp_dir = std::env::temp_dir().join("onde-chat-templates");
437 - std::fs::create_dir_all(&tmp_dir).ok();
438 - let tmp_path = tmp_dir.join("chat_template.jinja");
439 - std::fs::write(&tmp_path, template).map_err(|e| InferenceError::ModelBuild {
440 - reason: format!(
441 - "Failed to write chat template to {}: {}",
442 - tmp_path.display(),
443 - e
444 - ),
445 - })?;
446 - builder = builder.with_chat_template(tmp_path.to_string_lossy().to_string());
447 - Some(tmp_path)
448 - } else {
449 - None
450 - };
435 + if let Some(ref template) = config.chat_template {
436 + builder = builder.with_chat_template(template);
437 + }
438
439 let model = builder
440 .build()
@@ -459,7 +446,9 @@ impl ChatEngine {
446 let elapsed = start.elapsed();
447
448 let sampling = sampling.unwrap_or_else(|| {
462 - if cfg!(any(
449 + if config.uses_extended_thinking() {
450 + SamplingConfig::reasoning()
451 + } else if cfg!(any(
452 target_os = "ios",
453 target_os = "tvos",
454 target_os = "visionos",
@@ -1921,6 +1910,52 @@ const DEEPSEEK_CODER_CHAT_TEMPLATE: &str = include_str!("deepseek_coder_chat_tem
1910 /// These mirror the constants in [`super::models`] but return a ready-to-use
1911 /// [`GgufModelConfig`].
1912 impl GgufModelConfig {
1913 + /// Whether the model's default chat template emits an extended reasoning
1914 + /// block that needs a larger output budget than the general presets.
1915 + fn uses_extended_thinking(&self) -> bool {
1916 + matches!(
1917 + self.model_id.as_str(),
1918 + super::models::BARTOWSKI_QWEN3_0_6B_GGUF
1919 + | super::models::BARTOWSKI_QWEN3_1_7B_GGUF
1920 + | super::models::BARTOWSKI_QWEN3_4B_GGUF
1921 + | super::models::BARTOWSKI_QWEN3_8B_GGUF
1922 + | super::models::BARTOWSKI_QWEN3_14B_GGUF
1923 + | super::models::BARTOWSKI_QWEN3_32B_GGUF
1924 + | super::models::BARTOWSKI_QWEN3_4B_THINKING_2507_GGUF
1925 + )
1926 + }
1927 +
1928 + /// Resolve a model-management ID to the same configuration used by the
1929 + /// inference engine. Keeping this mapping in one place prevents download
1930 + /// and load support from drifting apart.
1931 + pub(crate) fn from_supported_model_id(model_id: &str) -> Option<Self> {
1932 + match model_id {
1933 + super::models::BARTOWSKI_QWEN25_0_5B_INSTRUCT_GGUF => Some(Self::qwen25_0_5b()),
1934 + super::models::BARTOWSKI_QWEN25_1_5B_INSTRUCT_GGUF => Some(Self::qwen25_1_5b()),
1935 + super::models::BARTOWSKI_QWEN25_3B_INSTRUCT_GGUF => Some(Self::qwen25_3b()),
1936 + super::models::BARTOWSKI_QWEN3_0_6B_GGUF => Some(Self::qwen3_0_6b()),
1937 + super::models::BARTOWSKI_QWEN3_1_7B_GGUF => Some(Self::qwen3_1_7b()),
1938 + super::models::BARTOWSKI_QWEN3_4B_GGUF => Some(Self::qwen3_4b()),
1939 + super::models::BARTOWSKI_QWEN3_8B_GGUF => Some(Self::qwen3_8b()),
1940 + super::models::BARTOWSKI_QWEN3_14B_GGUF => Some(Self::qwen3_14b()),
1941 + super::models::BARTOWSKI_QWEN3_32B_GGUF => Some(Self::qwen3_32b()),
1942 + super::models::BARTOWSKI_QWEN3_4B_INSTRUCT_2507_GGUF => {
1943 + Some(Self::qwen3_4b_instruct_2507())
1944 + }
1945 + super::models::BARTOWSKI_QWEN3_4B_THINKING_2507_GGUF => {
1946 + Some(Self::qwen3_4b_thinking_2507())
1947 + }
1948 + super::models::BARTOWSKI_QWEN3_30B_A3B_INSTRUCT_2507_GGUF => {
1949 + Some(Self::qwen3_30b_a3b_instruct_2507())
1950 + }
1951 + super::models::BARTOWSKI_QWEN25_CODER_7B_INSTRUCT_GGUF => Some(Self::qwen25_coder_7b()),
1952 + super::models::THEBLOKE_DEEPSEEK_CODER_6_7B_INSTRUCT_GGUF => {
1953 + Some(Self::deepseek_coder_6_7b())
1954 + }
1955 + _ => None,
1956 + }
1957 + }
1958 +
1959 /// Qwen 2.5 0.5B Instruct (GGUF Q4_K_M), ~380 MB.
1960 ///
1961 /// Smallest option. Used on tvOS (Apple TV) where the memory limit is 2 GB.
@@ -2317,6 +2352,22 @@ mod tests {
2352 }
2353 }
2354
2355 + #[test]
2356 + fn every_supported_model_resolves_to_download_config() {
2357 + for model_id in super::super::models::SUPPORTED_MODELS {
2358 + let config = GgufModelConfig::from_supported_model_id(model_id)
2359 + .unwrap_or_else(|| panic!("missing config for {model_id}"));
2360 + assert_eq!(config.model_id, *model_id);
2361 + assert_eq!(config.files.len(), 1);
2362 + }
2363 + }
2364 +
2365 + #[test]
2366 + fn qwen3_thinking_models_request_extended_sampling() {
2367 + assert!(GgufModelConfig::qwen3_4b_thinking_2507().uses_extended_thinking());
2368 + assert!(!GgufModelConfig::qwen3_4b_instruct_2507().uses_extended_thinking());
2369 + }
2370 +
2371 #[test]
2372 fn gguf_model_config_platform_default() {
2373 let cfg = GgufModelConfig::platform_default();
src/inference/models.rs
+15 -12
@@ -1,3 +1,6 @@
1 +// Copyright 2026 Onde Inference (Splitfire AB). All rights reserved.
2 +// Use of this source code is governed by the MIT license.
3 +//
4 /// Pre-quantized Qwen 2.5 1.5B Instruct (GGUF Q4_K_M) — lightest mobile option (~941 MB).
5 /// Fits comfortably on both iOS (iPhone 16e, 8 GB RAM) and Android memory-constrained devices.
6 /// Note: the 3B variant (~1.93 GB) caused OOM on iPhone 16e because iOS gives apps only ~2-3 GB;
@@ -287,8 +290,8 @@ pub const SUPPORTED_MODEL_INFO: &[SupportedModelInfo] = &[
290 org: "Qwen / Alibaba",
291 description: "Smallest Qwen 3 variant with tool calling (~0.5 GB). \
292 Suitable for tvOS and the most memory-constrained mobile devices.",
290 - // Approximate Q4_K_M size; HF API unreachable at authoring time.
291 - expected_size_bytes: 483_000_000,
293 + // Exact Q4_K_M file size from HuggingFace API siblings[].size.
294 + expected_size_bytes: 484_220_320,
295 },
296 SupportedModelInfo {
297 id: BARTOWSKI_QWEN3_1_7B_GGUF,
@@ -305,7 +308,7 @@ pub const SUPPORTED_MODEL_INFO: &[SupportedModelInfo] = &[
308 description: "Full tool-calling support with extended reasoning mode (~2.7 GB). \
309 Recommended for siGit Code on macOS, Linux, and Windows.",
310 // Qwen_Qwen3-4B-Q4_K_M.gguf from bartowski repo.
308 - expected_size_bytes: 2_596_306_912,
311 + expected_size_bytes: 2_497_280_960,
312 },
313 SupportedModelInfo {
314 id: BARTOWSKI_QWEN3_8B_GGUF,
@@ -313,7 +316,7 @@ pub const SUPPORTED_MODEL_INFO: &[SupportedModelInfo] = &[
316 org: "Qwen / Alibaba",
317 description: "Strong tool-calling model with extended thinking (~5 GB). \
318 Best balance of quality and memory for macOS with 24+ GB RAM.",
316 - expected_size_bytes: 5_131_567_104,
319 + expected_size_bytes: 5_027_784_224,
320 },
321 SupportedModelInfo {
322 id: BARTOWSKI_QWEN3_14B_GGUF,
@@ -331,8 +334,8 @@ pub const SUPPORTED_MODEL_INFO: &[SupportedModelInfo] = &[
334 org: "Qwen / Alibaba",
335 description: "Largest dense Qwen 3 model with extended thinking (~19.8 GB). \
336 Highest-quality dense variant; requires 32+ GB RAM.",
334 - // Approximate Q4_K_M size; HF API unreachable at authoring time.
335 - expected_size_bytes: 21_260_000_000,
337 + // Exact Q4_K_M file size from HuggingFace API siblings[].size.
338 + expected_size_bytes: 19_762_149_696,
339 },
340 SupportedModelInfo {
341 id: BARTOWSKI_QWEN3_4B_INSTRUCT_2507_GGUF,
@@ -340,8 +343,8 @@ pub const SUPPORTED_MODEL_INFO: &[SupportedModelInfo] = &[
343 org: "Qwen / Alibaba",
344 description: "Latest non-thinking 4B checkpoint — faster, predictable chat and \
345 tool use (~2.5 GB). Recommended general-purpose Qwen 3 model.",
343 - // Approximate Q4_K_M size; HF API unreachable at authoring time.
344 - expected_size_bytes: 2_500_000_000,
346 + // Exact Q4_K_M file size from HuggingFace API siblings[].size.
347 + expected_size_bytes: 2_497_280_736,
348 },
349 SupportedModelInfo {
350 id: BARTOWSKI_QWEN3_4B_THINKING_2507_GGUF,
@@ -349,8 +352,8 @@ pub const SUPPORTED_MODEL_INFO: &[SupportedModelInfo] = &[
352 org: "Qwen / Alibaba",
353 description: "Latest reasoning-focused 4B checkpoint with extended thinking (~2.5 GB). \
354 Stronger reasoning and tool-use accuracy; load with max_tokens ≥ 4096.",
352 - // Approximate Q4_K_M size; HF API unreachable at authoring time.
353 - expected_size_bytes: 2_500_000_000,
355 + // Exact Q4_K_M file size from HuggingFace API siblings[].size.
356 + expected_size_bytes: 2_497_280_736,
357 },
358 SupportedModelInfo {
359 id: BARTOWSKI_QWEN3_30B_A3B_INSTRUCT_2507_GGUF,
@@ -358,8 +361,8 @@ pub const SUPPORTED_MODEL_INFO: &[SupportedModelInfo] = &[
361 org: "Qwen / Alibaba",
362 description: "Flagship mixture-of-experts model: 30B total / ~3B active (~18.6 GB). \
363 Near-dense quality at far lower inference cost; requires 32+ GB RAM.",
361 - // Approximate Q4_K_M size; HF API unreachable at authoring time.
362 - expected_size_bytes: 19_971_600_000,
364 + // Exact Q4_K_M file size from HuggingFace API siblings[].size.
365 + expected_size_bytes: 18_632_183_808,
366 },
367 SupportedModelInfo {
368 id: BARTOWSKI_QWEN25_CODER_7B_INSTRUCT_GGUF,
src/inference/types.rs
+18
@@ -1,3 +1,6 @@
1 +// Copyright 2026 Onde Inference (Splitfire AB). All rights reserved.
2 +// Use of this source code is governed by the MIT license.
3 +//
4 //! Shared types for the on-device LLM inference engine.
5 //!
6 //! These types are intentionally framework-agnostic — they do **not** depend on
@@ -111,6 +114,15 @@ impl Default for SamplingConfig {
114 }
115
116 impl SamplingConfig {
117 + /// Defaults for models that emit an extended reasoning block before the
118 + /// user-visible answer.
119 + pub fn reasoning() -> Self {
120 + Self {
121 + max_tokens: Some(4096),
122 + ..Self::default()
123 + }
124 + }
125 +
126 /// Deterministic sampling (temperature = 0, greedy decoding).
127 pub fn deterministic() -> Self {
128 Self {
@@ -505,6 +517,12 @@ mod tests {
517 assert_eq!(cfg.max_tokens, Some(128));
518 }
519
520 + #[test]
521 + fn sampling_config_reasoning_has_extended_budget() {
522 + let config = SamplingConfig::reasoning();
523 + assert_eq!(config.max_tokens, Some(4096));
524 + }
525 +
526 #[test]
527 fn engine_status_display() {
528 assert_eq!(EngineStatus::Ready.to_string(), "ready");