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_load_uqff_model")
35 func onde_engine_load_uqff_model(
36 _ engine: UnsafeMutableRawPointer,
37 _ configJson: UnsafePointer<CChar>,
38 _ systemPrompt: UnsafePointer<CChar>?,
39 _ samplingJson: UnsafePointer<CChar>?
40 ) -> UnsafeMutablePointer<CChar>?
41
42 @_silgen_name("onde_engine_unload_model")
43 func onde_engine_unload_model(_ engine: UnsafeMutableRawPointer) -> UnsafeMutablePointer<CChar>?
44
45 @_silgen_name("onde_engine_is_loaded")
46 func onde_engine_is_loaded(_ engine: UnsafeMutableRawPointer) -> Bool
47
48 // Engine info
49 @_silgen_name("onde_engine_info")
50 func onde_engine_info(_ engine: UnsafeMutableRawPointer) -> UnsafeMutablePointer<CChar>?
51
52 // System prompt
53 @_silgen_name("onde_engine_set_system_prompt")
54 func onde_engine_set_system_prompt(
55 _ engine: UnsafeMutableRawPointer, _ prompt: UnsafePointer<CChar>)
56
57 @_silgen_name("onde_engine_clear_system_prompt")
58 func onde_engine_clear_system_prompt(_ engine: UnsafeMutableRawPointer)
59
60 // Sampling
61 @_silgen_name("onde_engine_set_sampling")
62 func onde_engine_set_sampling(
63 _ engine: UnsafeMutableRawPointer, _ samplingJson: UnsafePointer<CChar>)
64
65 // History
66 @_silgen_name("onde_engine_history")
67 func onde_engine_history(_ engine: UnsafeMutableRawPointer) -> UnsafeMutablePointer<CChar>?
68
69 @_silgen_name("onde_engine_clear_history")
70 func onde_engine_clear_history(_ engine: UnsafeMutableRawPointer) -> UInt64
71
72 @_silgen_name("onde_engine_push_history")
73 func onde_engine_push_history(
74 _ engine: UnsafeMutableRawPointer, _ messageJson: UnsafePointer<CChar>)
75
76 // Inference
77 @_silgen_name("onde_engine_send_message")
78 func onde_engine_send_message(
79 _ engine: UnsafeMutableRawPointer,
80 _ message: UnsafePointer<CChar>
81 ) -> UnsafeMutablePointer<CChar>?
82
83 @_silgen_name("onde_engine_generate")
84 func onde_engine_generate(
85 _ engine: UnsafeMutableRawPointer,
86 _ messagesJson: UnsafePointer<CChar>,
87 _ samplingJson: UnsafePointer<CChar>?
88 ) -> UnsafeMutablePointer<CChar>?
89
90 // Model config free functions
91 @_silgen_name("onde_default_model_config")
92 func onde_default_model_config() -> UnsafeMutablePointer<CChar>?
93
94 @_silgen_name("onde_uqff_model_config")
95 func onde_uqff_model_config(
96 _ modelId: UnsafePointer<CChar>,
97 _ filesJson: UnsafePointer<CChar>,
98 _ displayName: UnsafePointer<CChar>,
99 _ approxMemory: UnsafePointer<CChar>,
100 _ chatTemplate: UnsafePointer<CChar>?
101 ) -> UnsafeMutablePointer<CChar>?
102
103 @_silgen_name("onde_qwen25_1_5b_config")
104 func onde_qwen25_1_5b_config() -> UnsafeMutablePointer<CChar>?
105
106 @_silgen_name("onde_qwen25_3b_config")
107 func onde_qwen25_3b_config() -> UnsafeMutablePointer<CChar>?
108
109 @_silgen_name("onde_qwen3_0_6b_config")
110 func onde_qwen3_0_6b_config() -> UnsafeMutablePointer<CChar>?
111
112 @_silgen_name("onde_qwen3_1_7b_config")
113 func onde_qwen3_1_7b_config() -> UnsafeMutablePointer<CChar>?
114
115 @_silgen_name("onde_qwen3_4b_config")
116 func onde_qwen3_4b_config() -> UnsafeMutablePointer<CChar>?
117
118 @_silgen_name("onde_qwen3_8b_config")
119 func onde_qwen3_8b_config() -> UnsafeMutablePointer<CChar>?
120
121 @_silgen_name("onde_qwen3_14b_config")
122 func onde_qwen3_14b_config() -> UnsafeMutablePointer<CChar>?
123
124 @_silgen_name("onde_qwen3_32b_config")
125 func onde_qwen3_32b_config() -> UnsafeMutablePointer<CChar>?
126
127 @_silgen_name("onde_qwen3_4b_instruct_2507_config")
128 func onde_qwen3_4b_instruct_2507_config() -> UnsafeMutablePointer<CChar>?
129
130 @_silgen_name("onde_qwen3_4b_thinking_2507_config")
131 func onde_qwen3_4b_thinking_2507_config() -> UnsafeMutablePointer<CChar>?
132
133 @_silgen_name("onde_qwen3_30b_a3b_instruct_2507_config")
134 func onde_qwen3_30b_a3b_instruct_2507_config() -> UnsafeMutablePointer<CChar>?
135
136 // Sampling config free functions
137 @_silgen_name("onde_default_sampling_config")
138 func onde_default_sampling_config() -> UnsafeMutablePointer<CChar>?
139
140 @_silgen_name("onde_deterministic_sampling_config")
141 func onde_deterministic_sampling_config() -> UnsafeMutablePointer<CChar>?
142
143 @_silgen_name("onde_mobile_sampling_config")
144 func onde_mobile_sampling_config() -> UnsafeMutablePointer<CChar>?
145
146 // Memory management
147 @_silgen_name("onde_free_string")
148 func onde_free_string(_ ptr: UnsafeMutablePointer<CChar>?)
149
150 // MARK: - Error types
151
152 enum OndeError: Error, CustomStringConvertible {
153 case engineNotInitialized
154 case invalidResponse
155 case inferenceError(String)
156
157 var description: String {
158 switch self {
159 case .engineNotInitialized:
160 return "Onde engine is not initialized"
161 case .invalidResponse:
162 return "Invalid response from Onde engine"
163 case .inferenceError(let message):
164 return "Onde inference error: \(message)"
165 }
166 }
167 }
168
169 // MARK: - Expo Module
170
171 public class OndeInferenceModule: Module {
172 private var enginePtr: UnsafeMutableRawPointer?
173
174 // MARK: Helpers
175
176 private func configureApplicationFilesystem() throws {
177 let fileManager = FileManager.default
178
179 let containerDirectory: URL
180 if let appGroupDirectory = fileManager.containerURL(
181 forSecurityApplicationGroupIdentifier: ondeAppGroupIdentifier
182 ) {
183 containerDirectory = appGroupDirectory
184 } else {
185 containerDirectory = try fileManager.url(
186 for: .applicationSupportDirectory,
187 in: .userDomainMask,
188 appropriateFor: nil,
189 create: true
190 )
191 }
192
193 let hfHomeDirectory = containerDirectory.appendingPathComponent("models", isDirectory: true)
194 let hfHubCacheDirectory = hfHomeDirectory.appendingPathComponent("hub", isDirectory: true)
195 let tempDirectory = containerDirectory.appendingPathComponent("tmp", isDirectory: true)
196
197 try fileManager.createDirectory(at: hfHomeDirectory, withIntermediateDirectories: true)
198 try fileManager.createDirectory(at: hfHubCacheDirectory, withIntermediateDirectories: true)
199 try fileManager.createDirectory(at: tempDirectory, withIntermediateDirectories: true)
200
201 setenv("HF_HOME", hfHomeDirectory.path, 1)
202 setenv("HF_HUB_CACHE", hfHubCacheDirectory.path, 1)
203 setenv("HUGGINGFACE_HUB_CACHE", hfHubCacheDirectory.path, 1)
204 setenv("TMPDIR", tempDirectory.path, 1)
205 }
206
207 /// Consume a Rust-allocated C string: copy it into a Swift `String`, then
208 /// free the original allocation via `onde_free_string`.
209 private func consumeRustString(_ ptr: UnsafeMutablePointer<CChar>?) -> String? {
210 guard let ptr = ptr else { return nil }
211 let str = String(cString: ptr)
212 onde_free_string(ptr)
213 return str
214 }
215
216 /// Validate a JSON string returned by the Rust FFI.
217 /// If the JSON contains an `"error"` key, throw an `OndeError.inferenceError`.
218 /// Otherwise return the original JSON string unchanged so the TypeScript
219 /// wrapper can parse it consistently.
220 private func validateJsonResult(_ json: String?) throws -> String {
221 guard let json = json,
222 let data = json.data(using: .utf8),
223 let dict = try JSONSerialization.jsonObject(with: data) as? [String: Any]
224 else {
225 throw OndeError.invalidResponse
226 }
227 if let error = dict["error"] as? String {
228 throw OndeError.inferenceError(error)
229 }
230 return json
231 }
232
233 /// Validate that a JSON string is an array payload and return it unchanged.
234 private func validateJsonArray(_ json: String?) throws -> String {
235 guard let json = json,
236 let data = json.data(using: .utf8),
237 (try JSONSerialization.jsonObject(with: data) as? [[String: Any]]) != nil
238 else {
239 throw OndeError.invalidResponse
240 }
241 return json
242 }
243
244 // MARK: Module definition
245
246 public func definition() -> ModuleDefinition {
247 Name("OndeInference")
248
249 OnCreate {
250 do {
251 try self.configureApplicationFilesystem()
252 self.enginePtr = onde_engine_create()
253 } catch {
254 self.enginePtr = nil
255 }
256 }
257
258 OnDestroy {
259 if let ptr = self.enginePtr {
260 onde_engine_destroy(ptr)
261 self.enginePtr = nil
262 }
263 }
264
265 // MARK: Sync functions
266
267 Function("isLoaded") { () -> Bool in
268 guard let ptr = self.enginePtr else { return false }
269 return onde_engine_is_loaded(ptr)
270 }
271
272 Function("setSystemPrompt") { (prompt: String) in
273 guard let ptr = self.enginePtr else { return }
274 prompt.withCString { cPrompt in
275 onde_engine_set_system_prompt(ptr, cPrompt)
276 }
277 }
278
279 Function("clearSystemPrompt") { () in
280 guard let ptr = self.enginePtr else { return }
281 onde_engine_clear_system_prompt(ptr)
282 }
283
284 Function("setSampling") { (samplingJson: String) in
285 guard let ptr = self.enginePtr else { return }
286 samplingJson.withCString { cJson in
287 onde_engine_set_sampling(ptr, cJson)
288 }
289 }
290
291 Function("clearHistory") { () -> Int in
292 guard let ptr = self.enginePtr else { return 0 }
293 return Int(onde_engine_clear_history(ptr))
294 }
295
296 Function("pushHistory") { (messageJson: String) in
297 guard let ptr = self.enginePtr else { return }
298 messageJson.withCString { cJson in
299 onde_engine_push_history(ptr, cJson)
300 }
301 }
302
303 // MARK: Config free functions (no engine needed)
304
305 Function("defaultModelConfig") { () -> String in
306 let result = self.consumeRustString(onde_default_model_config())
307 return try self.validateJsonResult(result)
308 }
309
310 Function("uqffModelConfig") {
311 (
312 modelId: String,
313 filesJson: String,
314 displayName: String,
315 approxMemory: String,
316 chatTemplate: String?
317 ) -> String in
318 let result = modelId.withCString { cModelId in
319 filesJson.withCString { cFilesJson in
320 displayName.withCString { cDisplayName in
321 approxMemory.withCString { cApproxMemory in
322 if let chatTemplate = chatTemplate {
323 return chatTemplate.withCString { cChatTemplate in
324 self.consumeRustString(
325 onde_uqff_model_config(
326 cModelId,
327 cFilesJson,
328 cDisplayName,
329 cApproxMemory,
330 cChatTemplate
331 ))
332 }
333 } else {
334 return self.consumeRustString(
335 onde_uqff_model_config(
336 cModelId,
337 cFilesJson,
338 cDisplayName,
339 cApproxMemory,
340 nil
341 ))
342 }
343 }
344 }
345 }
346 }
347 return try self.validateJsonResult(result)
348 }
349
350 Function("qwen251_5bConfig") { () -> String in
351 let result = self.consumeRustString(onde_qwen25_1_5b_config())
352 return try self.validateJsonResult(result)
353 }
354
355 Function("qwen253bConfig") { () -> String in
356 let result = self.consumeRustString(onde_qwen25_3b_config())
357 return try self.validateJsonResult(result)
358 }
359
360 Function("qwen3_0_6bConfig") { () -> String in
361 try self.validateJsonResult(self.consumeRustString(onde_qwen3_0_6b_config()))
362 }
363
364 Function("qwen3_1_7bConfig") { () -> String in
365 try self.validateJsonResult(self.consumeRustString(onde_qwen3_1_7b_config()))
366 }
367
368 Function("qwen3_4bConfig") { () -> String in
369 try self.validateJsonResult(self.consumeRustString(onde_qwen3_4b_config()))
370 }
371
372 Function("qwen3_8bConfig") { () -> String in
373 try self.validateJsonResult(self.consumeRustString(onde_qwen3_8b_config()))
374 }
375
376 Function("qwen3_14bConfig") { () -> String in
377 try self.validateJsonResult(self.consumeRustString(onde_qwen3_14b_config()))
378 }
379
380 Function("qwen3_32bConfig") { () -> String in
381 try self.validateJsonResult(self.consumeRustString(onde_qwen3_32b_config()))
382 }
383
384 Function("qwen3_4bInstruct2507Config") { () -> String in
385 try self.validateJsonResult(
386 self.consumeRustString(onde_qwen3_4b_instruct_2507_config()))
387 }
388
389 Function("qwen3_4bThinking2507Config") { () -> String in
390 try self.validateJsonResult(
391 self.consumeRustString(onde_qwen3_4b_thinking_2507_config()))
392 }
393
394 Function("qwen3_30bA3bInstruct2507Config") { () -> String in
395 try self.validateJsonResult(
396 self.consumeRustString(onde_qwen3_30b_a3b_instruct_2507_config()))
397 }
398
399 Function("defaultSamplingConfig") { () -> String in
400 let result = self.consumeRustString(onde_default_sampling_config())
401 return try self.validateJsonResult(result)
402 }
403
404 Function("deterministicSamplingConfig") { () -> String in
405 let result = self.consumeRustString(onde_deterministic_sampling_config())
406 return try self.validateJsonResult(result)
407 }
408
409 Function("mobileSamplingConfig") { () -> String in
410 let result = self.consumeRustString(onde_mobile_sampling_config())
411 return try self.validateJsonResult(result)
412 }
413
414 // MARK: Async functions
415
416 AsyncFunction("loadDefaultModel") {
417 (systemPrompt: String?, samplingJson: String?) -> String in
418 try self.configureApplicationFilesystem()
419 guard let ptr = self.enginePtr else {
420 throw OndeError.engineNotInitialized
421 }
422 let result: String?
423 if let sp = systemPrompt, let sj = samplingJson {
424 result = sp.withCString { cSp in
425 sj.withCString { cSj in
426 self.consumeRustString(onde_engine_load_default_model(ptr, cSp, cSj))
427 }
428 }
429 } else if let sp = systemPrompt {
430 result = sp.withCString { cSp in
431 self.consumeRustString(onde_engine_load_default_model(ptr, cSp, nil))
432 }
433 } else if let sj = samplingJson {
434 result = sj.withCString { cSj in
435 self.consumeRustString(onde_engine_load_default_model(ptr, nil, cSj))
436 }
437 } else {
438 result = self.consumeRustString(onde_engine_load_default_model(ptr, nil, nil))
439 }
440 return try self.validateJsonResult(result)
441 }
442
443 AsyncFunction("loadModel") {
444 (configJson: String, systemPrompt: String?, samplingJson: String?) -> String in
445 try self.configureApplicationFilesystem()
446 guard let ptr = self.enginePtr else {
447 throw OndeError.engineNotInitialized
448 }
449 let result: String? = configJson.withCString { cConfig in
450 if let sp = systemPrompt, let sj = samplingJson {
451 return sp.withCString { cSp in
452 sj.withCString { cSj in
453 self.consumeRustString(onde_engine_load_model(ptr, cConfig, cSp, cSj))
454 }
455 }
456 } else if let sp = systemPrompt {
457 return sp.withCString { cSp in
458 self.consumeRustString(onde_engine_load_model(ptr, cConfig, cSp, nil))
459 }
460 } else if let sj = samplingJson {
461 return sj.withCString { cSj in
462 self.consumeRustString(onde_engine_load_model(ptr, cConfig, nil, cSj))
463 }
464 } else {
465 return self.consumeRustString(onde_engine_load_model(ptr, cConfig, nil, nil))
466 }
467 }
468 return try self.validateJsonResult(result)
469 }
470
471 AsyncFunction("loadUqffModel") {
472 (configJson: String, systemPrompt: String?, samplingJson: String?) -> String in
473 try self.configureApplicationFilesystem()
474 guard let ptr = self.enginePtr else {
475 throw OndeError.engineNotInitialized
476 }
477 let result: String? = configJson.withCString { cConfig in
478 if let sp = systemPrompt, let sj = samplingJson {
479 return sp.withCString { cSp in
480 sj.withCString { cSj in
481 self.consumeRustString(
482 onde_engine_load_uqff_model(ptr, cConfig, cSp, cSj))
483 }
484 }
485 } else if let sp = systemPrompt {
486 return sp.withCString { cSp in
487 self.consumeRustString(onde_engine_load_uqff_model(ptr, cConfig, cSp, nil))
488 }
489 } else if let sj = samplingJson {
490 return sj.withCString { cSj in
491 self.consumeRustString(onde_engine_load_uqff_model(ptr, cConfig, nil, cSj))
492 }
493 } else {
494 return self.consumeRustString(
495 onde_engine_load_uqff_model(ptr, cConfig, nil, nil))
496 }
497 }
498 return try self.validateJsonResult(result)
499 }
500
501 AsyncFunction("unloadModel") { () -> String in
502 guard let ptr = self.enginePtr else {
503 throw OndeError.engineNotInitialized
504 }
505 let result = self.consumeRustString(onde_engine_unload_model(ptr))
506 return try self.validateJsonResult(result)
507 }
508
509 AsyncFunction("info") { () -> String in
510 guard let ptr = self.enginePtr else {
511 throw OndeError.engineNotInitialized
512 }
513 let result = self.consumeRustString(onde_engine_info(ptr))
514 return try self.validateJsonResult(result)
515 }
516
517 AsyncFunction("history") { () -> String in
518 guard let ptr = self.enginePtr else { return "[]" }
519 let result = self.consumeRustString(onde_engine_history(ptr))
520 return try self.validateJsonArray(result)
521 }
522
523 AsyncFunction("sendMessage") { (message: String) -> String in
524 guard let ptr = self.enginePtr else {
525 throw OndeError.engineNotInitialized
526 }
527 let result = message.withCString { cMessage in
528 self.consumeRustString(onde_engine_send_message(ptr, cMessage))
529 }
530 return try self.validateJsonResult(result)
531 }
532
533 AsyncFunction("generate") {
534 (messagesJson: String, samplingJson: String?) -> String in
535 guard let ptr = self.enginePtr else {
536 throw OndeError.engineNotInitialized
537 }
538 let result: String? = messagesJson.withCString { cMessages in
539 if let sj = samplingJson {
540 return sj.withCString { cSj in
541 self.consumeRustString(onde_engine_generate(ptr, cMessages, cSj))
542 }
543 } else {
544 return self.consumeRustString(onde_engine_generate(ptr, cMessages, nil))
545 }
546 }
547 return try self.validateJsonResult(result)
548 }
549 }
550 }