1 // Copyright 2026 Splitfire AB (Onde Inference). All rights reserved.
2 // SPDX-License-Identifier: MIT OR Apache-2.0
3
4 import ExpoModulesCore
5 import Foundation
6
7 private let ondeAppGroupIdentifier = "group.com.ondeinference.apps"
8
9 // MARK: - Rust C FFI declarations
10
11 // Engine lifecycle
12 @_silgen_name("onde_engine_create")
13 func onde_engine_create() -> UnsafeMutableRawPointer?
14
15 @_silgen_name("onde_engine_destroy")
16 func onde_engine_destroy(_ engine: UnsafeMutableRawPointer)
17
18 // Model loading / unloading
19 @_silgen_name("onde_engine_load_default_model")
20 func onde_engine_load_default_model(
21 _ engine: UnsafeMutableRawPointer,
22 _ systemPrompt: UnsafePointer<CChar>?,
23 _ samplingJson: UnsafePointer<CChar>?
24 ) -> UnsafeMutablePointer<CChar>?
25
26 @_silgen_name("onde_engine_load_model")
27 func onde_engine_load_model(
28 _ engine: UnsafeMutableRawPointer,
29 _ configJson: UnsafePointer<CChar>,
30 _ systemPrompt: UnsafePointer<CChar>?,
31 _ samplingJson: UnsafePointer<CChar>?
32 ) -> UnsafeMutablePointer<CChar>?
33
34 @_silgen_name("onde_engine_unload_model")
35 func onde_engine_unload_model(_ engine: UnsafeMutableRawPointer) -> UnsafeMutablePointer<CChar>?
36
37 @_silgen_name("onde_engine_is_loaded")
38 func onde_engine_is_loaded(_ engine: UnsafeMutableRawPointer) -> Bool
39
40 // Engine info
41 @_silgen_name("onde_engine_info")
42 func onde_engine_info(_ engine: UnsafeMutableRawPointer) -> UnsafeMutablePointer<CChar>?
43
44 // System prompt
45 @_silgen_name("onde_engine_set_system_prompt")
46 func onde_engine_set_system_prompt(
47 _ engine: UnsafeMutableRawPointer, _ prompt: UnsafePointer<CChar>)
48
49 @_silgen_name("onde_engine_clear_system_prompt")
50 func onde_engine_clear_system_prompt(_ engine: UnsafeMutableRawPointer)
51
52 // Sampling
53 @_silgen_name("onde_engine_set_sampling")
54 func onde_engine_set_sampling(
55 _ engine: UnsafeMutableRawPointer, _ samplingJson: UnsafePointer<CChar>)
56
57 // History
58 @_silgen_name("onde_engine_history")
59 func onde_engine_history(_ engine: UnsafeMutableRawPointer) -> UnsafeMutablePointer<CChar>?
60
61 @_silgen_name("onde_engine_clear_history")
62 func onde_engine_clear_history(_ engine: UnsafeMutableRawPointer) -> UInt64
63
64 @_silgen_name("onde_engine_push_history")
65 func onde_engine_push_history(
66 _ engine: UnsafeMutableRawPointer, _ messageJson: UnsafePointer<CChar>)
67
68 // Inference
69 @_silgen_name("onde_engine_send_message")
70 func onde_engine_send_message(
71 _ engine: UnsafeMutableRawPointer,
72 _ message: UnsafePointer<CChar>
73 ) -> UnsafeMutablePointer<CChar>?
74
75 @_silgen_name("onde_engine_generate")
76 func onde_engine_generate(
77 _ engine: UnsafeMutableRawPointer,
78 _ messagesJson: UnsafePointer<CChar>,
79 _ samplingJson: UnsafePointer<CChar>?
80 ) -> UnsafeMutablePointer<CChar>?
81
82 // Model config free functions
83 @_silgen_name("onde_default_model_config")
84 func onde_default_model_config() -> UnsafeMutablePointer<CChar>?
85
86 @_silgen_name("onde_qwen25_1_5b_config")
87 func onde_qwen25_1_5b_config() -> UnsafeMutablePointer<CChar>?
88
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>?
122
123 @_silgen_name("onde_deterministic_sampling_config")
124 func onde_deterministic_sampling_config() -> UnsafeMutablePointer<CChar>?
125
126 @_silgen_name("onde_mobile_sampling_config")
127 func onde_mobile_sampling_config() -> UnsafeMutablePointer<CChar>?
128
129 // Memory management
130 @_silgen_name("onde_free_string")
131 func onde_free_string(_ ptr: UnsafeMutablePointer<CChar>?)
132
133 // MARK: - Error types
134
135 enum OndeError: Error, CustomStringConvertible {
136 case engineNotInitialized
137 case invalidResponse
138 case inferenceError(String)
139
140 var description: String {
141 switch self {
142 case .engineNotInitialized:
143 return "Onde engine is not initialized"
144 case .invalidResponse:
145 return "Invalid response from Onde engine"
146 case .inferenceError(let message):
147 return "Onde inference error: \(message)"
148 }
149 }
150 }
151
152 // MARK: - Expo Module
153
154 public class OndeInferenceModule: Module {
155 private var enginePtr: UnsafeMutableRawPointer?
156
157 // MARK: Helpers
158
159 private func configureApplicationFilesystem() throws {
160 let fileManager = FileManager.default
161
162 let containerDirectory: URL
163 if let appGroupDirectory = fileManager.containerURL(
164 forSecurityApplicationGroupIdentifier: ondeAppGroupIdentifier
165 ) {
166 containerDirectory = appGroupDirectory
167 } else {
168 containerDirectory = try fileManager.url(
169 for: .applicationSupportDirectory,
170 in: .userDomainMask,
171 appropriateFor: nil,
172 create: true
173 )
174 }
175
176 let hfHomeDirectory = containerDirectory.appendingPathComponent("models", isDirectory: true)
177 let hfHubCacheDirectory = hfHomeDirectory.appendingPathComponent("hub", isDirectory: true)
178 let tempDirectory = containerDirectory.appendingPathComponent("tmp", isDirectory: true)
179
180 try fileManager.createDirectory(at: hfHomeDirectory, withIntermediateDirectories: true)
181 try fileManager.createDirectory(at: hfHubCacheDirectory, withIntermediateDirectories: true)
182 try fileManager.createDirectory(at: tempDirectory, withIntermediateDirectories: true)
183
184 setenv("HF_HOME", hfHomeDirectory.path, 1)
185 setenv("HF_HUB_CACHE", hfHubCacheDirectory.path, 1)
186 setenv("HUGGINGFACE_HUB_CACHE", hfHubCacheDirectory.path, 1)
187 setenv("TMPDIR", tempDirectory.path, 1)
188 }
189
190 /// Consume a Rust-allocated C string: copy it into a Swift `String`, then
191 /// free the original allocation via `onde_free_string`.
192 private func consumeRustString(_ ptr: UnsafeMutablePointer<CChar>?) -> String? {
193 guard let ptr = ptr else { return nil }
194 let str = String(cString: ptr)
195 onde_free_string(ptr)
196 return str
197 }
198
199 /// Validate a JSON string returned by the Rust FFI.
200 /// If the JSON contains an `"error"` key, throw an `OndeError.inferenceError`.
201 /// Otherwise return the original JSON string unchanged so the TypeScript
202 /// wrapper can parse it consistently.
203 private func validateJsonResult(_ json: String?) throws -> String {
204 guard let json = json,
205 let data = json.data(using: .utf8),
206 let dict = try JSONSerialization.jsonObject(with: data) as? [String: Any]
207 else {
208 throw OndeError.invalidResponse
209 }
210 if let error = dict["error"] as? String {
211 throw OndeError.inferenceError(error)
212 }
213 return json
214 }
215
216 /// Validate that a JSON string is an array payload and return it unchanged.
217 private func validateJsonArray(_ json: String?) throws -> String {
218 guard let json = json,
219 let data = json.data(using: .utf8),
220 (try JSONSerialization.jsonObject(with: data) as? [[String: Any]]) != nil
221 else {
222 throw OndeError.invalidResponse
223 }
224 return json
225 }
226
227 // MARK: Module definition
228
229 public func definition() -> ModuleDefinition {
230 Name("OndeInference")
231
232 OnCreate {
233 do {
234 try self.configureApplicationFilesystem()
235 self.enginePtr = onde_engine_create()
236 } catch {
237 self.enginePtr = nil
238 }
239 }
240
241 OnDestroy {
242 if let ptr = self.enginePtr {
243 onde_engine_destroy(ptr)
244 self.enginePtr = nil
245 }
246 }
247
248 // MARK: Sync functions
249
250 Function("isLoaded") { () -> Bool in
251 guard let ptr = self.enginePtr else { return false }
252 return onde_engine_is_loaded(ptr)
253 }
254
255 Function("setSystemPrompt") { (prompt: String) in
256 guard let ptr = self.enginePtr else { return }
257 prompt.withCString { cPrompt in
258 onde_engine_set_system_prompt(ptr, cPrompt)
259 }
260 }
261
262 Function("clearSystemPrompt") { () in
263 guard let ptr = self.enginePtr else { return }
264 onde_engine_clear_system_prompt(ptr)
265 }
266
267 Function("setSampling") { (samplingJson: String) in
268 guard let ptr = self.enginePtr else { return }
269 samplingJson.withCString { cJson in
270 onde_engine_set_sampling(ptr, cJson)
271 }
272 }
273
274 Function("clearHistory") { () -> Int in
275 guard let ptr = self.enginePtr else { return 0 }
276 return Int(onde_engine_clear_history(ptr))
277 }
278
279 Function("pushHistory") { (messageJson: String) in
280 guard let ptr = self.enginePtr else { return }
281 messageJson.withCString { cJson in
282 onde_engine_push_history(ptr, cJson)
283 }
284 }
285
286 // MARK: Config free functions (no engine needed)
287
288 Function("defaultModelConfig") { () -> String in
289 let result = self.consumeRustString(onde_default_model_config())
290 return try self.validateJsonResult(result)
291 }
292
293 Function("qwen251_5bConfig") { () -> String in
294 let result = self.consumeRustString(onde_qwen25_1_5b_config())
295 return try self.validateJsonResult(result)
296 }
297
298 Function("qwen253bConfig") { () -> String in
299 let result = self.consumeRustString(onde_qwen25_3b_config())
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)
345 }
346
347 Function("deterministicSamplingConfig") { () -> String in
348 let result = self.consumeRustString(onde_deterministic_sampling_config())
349 return try self.validateJsonResult(result)
350 }
351
352 Function("mobileSamplingConfig") { () -> String in
353 let result = self.consumeRustString(onde_mobile_sampling_config())
354 return try self.validateJsonResult(result)
355 }
356
357 // MARK: Async functions
358
359 AsyncFunction("loadDefaultModel") {
360 (systemPrompt: String?, samplingJson: String?) -> String in
361 try self.configureApplicationFilesystem()
362 guard let ptr = self.enginePtr else {
363 throw OndeError.engineNotInitialized
364 }
365 let result: String?
366 if let sp = systemPrompt, let sj = samplingJson {
367 result = sp.withCString { cSp in
368 sj.withCString { cSj in
369 self.consumeRustString(onde_engine_load_default_model(ptr, cSp, cSj))
370 }
371 }
372 } else if let sp = systemPrompt {
373 result = sp.withCString { cSp in
374 self.consumeRustString(onde_engine_load_default_model(ptr, cSp, nil))
375 }
376 } else if let sj = samplingJson {
377 result = sj.withCString { cSj in
378 self.consumeRustString(onde_engine_load_default_model(ptr, nil, cSj))
379 }
380 } else {
381 result = self.consumeRustString(onde_engine_load_default_model(ptr, nil, nil))
382 }
383 return try self.validateJsonResult(result)
384 }
385
386 AsyncFunction("loadModel") {
387 (configJson: String, systemPrompt: String?, samplingJson: String?) -> String in
388 try self.configureApplicationFilesystem()
389 guard let ptr = self.enginePtr else {
390 throw OndeError.engineNotInitialized
391 }
392 let result: String? = configJson.withCString { cConfig in
393 if let sp = systemPrompt, let sj = samplingJson {
394 return sp.withCString { cSp in
395 sj.withCString { cSj in
396 self.consumeRustString(onde_engine_load_model(ptr, cConfig, cSp, cSj))
397 }
398 }
399 } else if let sp = systemPrompt {
400 return sp.withCString { cSp in
401 self.consumeRustString(onde_engine_load_model(ptr, cConfig, cSp, nil))
402 }
403 } else if let sj = samplingJson {
404 return sj.withCString { cSj in
405 self.consumeRustString(onde_engine_load_model(ptr, cConfig, nil, cSj))
406 }
407 } else {
408 return self.consumeRustString(onde_engine_load_model(ptr, cConfig, nil, nil))
409 }
410 }
411 return try self.validateJsonResult(result)
412 }
413
414 AsyncFunction("unloadModel") { () -> String in
415 guard let ptr = self.enginePtr else {
416 throw OndeError.engineNotInitialized
417 }
418 let result = self.consumeRustString(onde_engine_unload_model(ptr))
419 return try self.validateJsonResult(result)
420 }
421
422 AsyncFunction("info") { () -> String in
423 guard let ptr = self.enginePtr else {
424 throw OndeError.engineNotInitialized
425 }
426 let result = self.consumeRustString(onde_engine_info(ptr))
427 return try self.validateJsonResult(result)
428 }
429
430 AsyncFunction("history") { () -> String in
431 guard let ptr = self.enginePtr else { return "[]" }
432 let result = self.consumeRustString(onde_engine_history(ptr))
433 return try self.validateJsonArray(result)
434 }
435
436 AsyncFunction("sendMessage") { (message: String) -> String in
437 guard let ptr = self.enginePtr else {
438 throw OndeError.engineNotInitialized
439 }
440 let result = message.withCString { cMessage in
441 self.consumeRustString(onde_engine_send_message(ptr, cMessage))
442 }
443 return try self.validateJsonResult(result)
444 }
445
446 AsyncFunction("generate") {
447 (messagesJson: String, samplingJson: String?) -> String in
448 guard let ptr = self.enginePtr else {
449 throw OndeError.engineNotInitialized
450 }
451 let result: String? = messagesJson.withCString { cMessages in
452 if let sj = samplingJson {
453 return sj.withCString { cSj in
454 self.consumeRustString(onde_engine_generate(ptr, cMessages, cSj))
455 }
456 } else {
457 return self.consumeRustString(onde_engine_generate(ptr, cMessages, nil))
458 }
459 }
460 return try self.validateJsonResult(result)
461 }
462 }
463 }