|
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
|
} |