|
1
|
import ExpoModulesCore |
|
2
|
import Foundation |
|
3
|
|
|
4
|
private let ondeAppGroupIdentifier = "group.com.ondeinference.apps" |
|
5
|
|
|
6
|
// MARK: - Rust C FFI declarations |
|
7
|
|
|
8
|
// Engine lifecycle |
|
9
|
@_silgen_name("onde_engine_create") |
|
10
|
func onde_engine_create() -> UnsafeMutableRawPointer? |
|
11
|
|
|
12
|
@_silgen_name("onde_engine_destroy") |
|
13
|
func onde_engine_destroy(_ engine: UnsafeMutableRawPointer) |
|
14
|
|
|
15
|
// Model loading / unloading |
|
16
|
@_silgen_name("onde_engine_load_default_model") |
|
17
|
func onde_engine_load_default_model( |
|
18
|
_ engine: UnsafeMutableRawPointer, |
|
19
|
_ systemPrompt: UnsafePointer<CChar>?, |
|
20
|
_ samplingJson: UnsafePointer<CChar>? |
|
21
|
) -> UnsafeMutablePointer<CChar>? |
|
22
|
|
|
23
|
@_silgen_name("onde_engine_load_model") |
|
24
|
func onde_engine_load_model( |
|
25
|
_ engine: UnsafeMutableRawPointer, |
|
26
|
_ configJson: UnsafePointer<CChar>, |
|
27
|
_ systemPrompt: UnsafePointer<CChar>?, |
|
28
|
_ samplingJson: UnsafePointer<CChar>? |
|
29
|
) -> UnsafeMutablePointer<CChar>? |
|
30
|
|
|
31
|
@_silgen_name("onde_engine_unload_model") |
|
32
|
func onde_engine_unload_model(_ engine: UnsafeMutableRawPointer) -> UnsafeMutablePointer<CChar>? |
|
33
|
|
|
34
|
@_silgen_name("onde_engine_is_loaded") |
|
35
|
func onde_engine_is_loaded(_ engine: UnsafeMutableRawPointer) -> Bool |
|
36
|
|
|
37
|
// Engine info |
|
38
|
@_silgen_name("onde_engine_info") |
|
39
|
func onde_engine_info(_ engine: UnsafeMutableRawPointer) -> UnsafeMutablePointer<CChar>? |
|
40
|
|
|
41
|
// System prompt |
|
42
|
@_silgen_name("onde_engine_set_system_prompt") |
|
43
|
func onde_engine_set_system_prompt( |
|
44
|
_ engine: UnsafeMutableRawPointer, _ prompt: UnsafePointer<CChar>) |
|
45
|
|
|
46
|
@_silgen_name("onde_engine_clear_system_prompt") |
|
47
|
func onde_engine_clear_system_prompt(_ engine: UnsafeMutableRawPointer) |
|
48
|
|
|
49
|
// Sampling |
|
50
|
@_silgen_name("onde_engine_set_sampling") |
|
51
|
func onde_engine_set_sampling( |
|
52
|
_ engine: UnsafeMutableRawPointer, _ samplingJson: UnsafePointer<CChar>) |
|
53
|
|
|
54
|
// History |
|
55
|
@_silgen_name("onde_engine_history") |
|
56
|
func onde_engine_history(_ engine: UnsafeMutableRawPointer) -> UnsafeMutablePointer<CChar>? |
|
57
|
|
|
58
|
@_silgen_name("onde_engine_clear_history") |
|
59
|
func onde_engine_clear_history(_ engine: UnsafeMutableRawPointer) -> UInt64 |
|
60
|
|
|
61
|
@_silgen_name("onde_engine_push_history") |
|
62
|
func onde_engine_push_history( |
|
63
|
_ engine: UnsafeMutableRawPointer, _ messageJson: UnsafePointer<CChar>) |
|
64
|
|
|
65
|
// Inference |
|
66
|
@_silgen_name("onde_engine_send_message") |
|
67
|
func onde_engine_send_message( |
|
68
|
_ engine: UnsafeMutableRawPointer, |
|
69
|
_ message: UnsafePointer<CChar> |
|
70
|
) -> UnsafeMutablePointer<CChar>? |
|
71
|
|
|
72
|
@_silgen_name("onde_engine_generate") |
|
73
|
func onde_engine_generate( |
|
74
|
_ engine: UnsafeMutableRawPointer, |
|
75
|
_ messagesJson: UnsafePointer<CChar>, |
|
76
|
_ samplingJson: UnsafePointer<CChar>? |
|
77
|
) -> UnsafeMutablePointer<CChar>? |
|
78
|
|
|
79
|
// Model config free functions |
|
80
|
@_silgen_name("onde_default_model_config") |
|
81
|
func onde_default_model_config() -> UnsafeMutablePointer<CChar>? |
|
82
|
|
|
83
|
@_silgen_name("onde_qwen25_1_5b_config") |
|
84
|
func onde_qwen25_1_5b_config() -> UnsafeMutablePointer<CChar>? |
|
85
|
|
|
86
|
@_silgen_name("onde_qwen25_3b_config") |
|
87
|
func onde_qwen25_3b_config() -> UnsafeMutablePointer<CChar>? |
|
88
|
|
|
89
|
// Sampling config free functions |
|
90
|
@_silgen_name("onde_default_sampling_config") |
|
91
|
func onde_default_sampling_config() -> UnsafeMutablePointer<CChar>? |
|
92
|
|
|
93
|
@_silgen_name("onde_deterministic_sampling_config") |
|
94
|
func onde_deterministic_sampling_config() -> UnsafeMutablePointer<CChar>? |
|
95
|
|
|
96
|
@_silgen_name("onde_mobile_sampling_config") |
|
97
|
func onde_mobile_sampling_config() -> UnsafeMutablePointer<CChar>? |
|
98
|
|
|
99
|
// Memory management |
|
100
|
@_silgen_name("onde_free_string") |
|
101
|
func onde_free_string(_ ptr: UnsafeMutablePointer<CChar>?) |
|
102
|
|
|
103
|
// MARK: - Error types |
|
104
|
|
|
105
|
enum OndeError: Error, CustomStringConvertible { |
|
106
|
case engineNotInitialized |
|
107
|
case invalidResponse |
|
108
|
case inferenceError(String) |
|
109
|
|
|
110
|
var description: String { |
|
111
|
switch self { |
|
112
|
case .engineNotInitialized: |
|
113
|
return "Onde engine is not initialized" |
|
114
|
case .invalidResponse: |
|
115
|
return "Invalid response from Onde engine" |
|
116
|
case .inferenceError(let message): |
|
117
|
return "Onde inference error: \(message)" |
|
118
|
} |
|
119
|
} |
|
120
|
} |
|
121
|
|
|
122
|
// MARK: - Expo Module |
|
123
|
|
|
124
|
public class OndeInferenceModule: Module { |
|
125
|
private var enginePtr: UnsafeMutableRawPointer? |
|
126
|
|
|
127
|
// MARK: Helpers |
|
128
|
|
|
129
|
private func configureApplicationFilesystem() throws { |
|
130
|
let fileManager = FileManager.default |
|
131
|
|
|
132
|
let containerDirectory: URL |
|
133
|
if let appGroupDirectory = fileManager.containerURL( |
|
134
|
forSecurityApplicationGroupIdentifier: ondeAppGroupIdentifier |
|
135
|
) { |
|
136
|
containerDirectory = appGroupDirectory |
|
137
|
} else { |
|
138
|
containerDirectory = try fileManager.url( |
|
139
|
for: .applicationSupportDirectory, |
|
140
|
in: .userDomainMask, |
|
141
|
appropriateFor: nil, |
|
142
|
create: true |
|
143
|
) |
|
144
|
} |
|
145
|
|
|
146
|
let hfHomeDirectory = containerDirectory.appendingPathComponent("models", isDirectory: true) |
|
147
|
let hfHubCacheDirectory = hfHomeDirectory.appendingPathComponent("hub", isDirectory: true) |
|
148
|
let tempDirectory = containerDirectory.appendingPathComponent("tmp", isDirectory: true) |
|
149
|
|
|
150
|
try fileManager.createDirectory(at: hfHomeDirectory, withIntermediateDirectories: true) |
|
151
|
try fileManager.createDirectory(at: hfHubCacheDirectory, withIntermediateDirectories: true) |
|
152
|
try fileManager.createDirectory(at: tempDirectory, withIntermediateDirectories: true) |
|
153
|
|
|
154
|
setenv("HF_HOME", hfHomeDirectory.path, 1) |
|
155
|
setenv("HF_HUB_CACHE", hfHubCacheDirectory.path, 1) |
|
156
|
setenv("HUGGINGFACE_HUB_CACHE", hfHubCacheDirectory.path, 1) |
|
157
|
setenv("TMPDIR", tempDirectory.path, 1) |
|
158
|
} |
|
159
|
|
|
160
|
/// Consume a Rust-allocated C string: copy it into a Swift `String`, then |
|
161
|
/// free the original allocation via `onde_free_string`. |
|
162
|
private func consumeRustString(_ ptr: UnsafeMutablePointer<CChar>?) -> String? { |
|
163
|
guard let ptr = ptr else { return nil } |
|
164
|
let str = String(cString: ptr) |
|
165
|
onde_free_string(ptr) |
|
166
|
return str |
|
167
|
} |
|
168
|
|
|
169
|
/// Validate a JSON string returned by the Rust FFI. |
|
170
|
/// If the JSON contains an `"error"` key, throw an `OndeError.inferenceError`. |
|
171
|
/// Otherwise return the original JSON string unchanged so the TypeScript |
|
172
|
/// wrapper can parse it consistently. |
|
173
|
private func validateJsonResult(_ json: String?) throws -> String { |
|
174
|
guard let json = json, |
|
175
|
let data = json.data(using: .utf8), |
|
176
|
let dict = try JSONSerialization.jsonObject(with: data) as? [String: Any] |
|
177
|
else { |
|
178
|
throw OndeError.invalidResponse |
|
179
|
} |
|
180
|
if let error = dict["error"] as? String { |
|
181
|
throw OndeError.inferenceError(error) |
|
182
|
} |
|
183
|
return json |
|
184
|
} |
|
185
|
|
|
186
|
/// Validate that a JSON string is an array payload and return it unchanged. |
|
187
|
private func validateJsonArray(_ json: String?) throws -> String { |
|
188
|
guard let json = json, |
|
189
|
let data = json.data(using: .utf8), |
|
190
|
(try JSONSerialization.jsonObject(with: data) as? [[String: Any]]) != nil |
|
191
|
else { |
|
192
|
throw OndeError.invalidResponse |
|
193
|
} |
|
194
|
return json |
|
195
|
} |
|
196
|
|
|
197
|
// MARK: Module definition |
|
198
|
|
|
199
|
public func definition() -> ModuleDefinition { |
|
200
|
Name("OndeInference") |
|
201
|
|
|
202
|
OnCreate { |
|
203
|
do { |
|
204
|
try self.configureApplicationFilesystem() |
|
205
|
self.enginePtr = onde_engine_create() |
|
206
|
} catch { |
|
207
|
self.enginePtr = nil |
|
208
|
} |
|
209
|
} |
|
210
|
|
|
211
|
OnDestroy { |
|
212
|
if let ptr = self.enginePtr { |
|
213
|
onde_engine_destroy(ptr) |
|
214
|
self.enginePtr = nil |
|
215
|
} |
|
216
|
} |
|
217
|
|
|
218
|
// MARK: Sync functions |
|
219
|
|
|
220
|
Function("isLoaded") { () -> Bool in |
|
221
|
guard let ptr = self.enginePtr else { return false } |
|
222
|
return onde_engine_is_loaded(ptr) |
|
223
|
} |
|
224
|
|
|
225
|
Function("setSystemPrompt") { (prompt: String) in |
|
226
|
guard let ptr = self.enginePtr else { return } |
|
227
|
prompt.withCString { cPrompt in |
|
228
|
onde_engine_set_system_prompt(ptr, cPrompt) |
|
229
|
} |
|
230
|
} |
|
231
|
|
|
232
|
Function("clearSystemPrompt") { () in |
|
233
|
guard let ptr = self.enginePtr else { return } |
|
234
|
onde_engine_clear_system_prompt(ptr) |
|
235
|
} |
|
236
|
|
|
237
|
Function("setSampling") { (samplingJson: String) in |
|
238
|
guard let ptr = self.enginePtr else { return } |
|
239
|
samplingJson.withCString { cJson in |
|
240
|
onde_engine_set_sampling(ptr, cJson) |
|
241
|
} |
|
242
|
} |
|
243
|
|
|
244
|
Function("clearHistory") { () -> Int in |
|
245
|
guard let ptr = self.enginePtr else { return 0 } |
|
246
|
return Int(onde_engine_clear_history(ptr)) |
|
247
|
} |
|
248
|
|
|
249
|
Function("pushHistory") { (messageJson: String) in |
|
250
|
guard let ptr = self.enginePtr else { return } |
|
251
|
messageJson.withCString { cJson in |
|
252
|
onde_engine_push_history(ptr, cJson) |
|
253
|
} |
|
254
|
} |
|
255
|
|
|
256
|
// MARK: Config free functions (no engine needed) |
|
257
|
|
|
258
|
Function("defaultModelConfig") { () -> String in |
|
259
|
let result = self.consumeRustString(onde_default_model_config()) |
|
260
|
return try self.validateJsonResult(result) |
|
261
|
} |
|
262
|
|
|
263
|
Function("qwen251_5bConfig") { () -> String in |
|
264
|
let result = self.consumeRustString(onde_qwen25_1_5b_config()) |
|
265
|
return try self.validateJsonResult(result) |
|
266
|
} |
|
267
|
|
|
268
|
Function("qwen253bConfig") { () -> String in |
|
269
|
let result = self.consumeRustString(onde_qwen25_3b_config()) |
|
270
|
return try self.validateJsonResult(result) |
|
271
|
} |
|
272
|
|
|
273
|
Function("defaultSamplingConfig") { () -> String in |
|
274
|
let result = self.consumeRustString(onde_default_sampling_config()) |
|
275
|
return try self.validateJsonResult(result) |
|
276
|
} |
|
277
|
|
|
278
|
Function("deterministicSamplingConfig") { () -> String in |
|
279
|
let result = self.consumeRustString(onde_deterministic_sampling_config()) |
|
280
|
return try self.validateJsonResult(result) |
|
281
|
} |
|
282
|
|
|
283
|
Function("mobileSamplingConfig") { () -> String in |
|
284
|
let result = self.consumeRustString(onde_mobile_sampling_config()) |
|
285
|
return try self.validateJsonResult(result) |
|
286
|
} |
|
287
|
|
|
288
|
// MARK: Async functions |
|
289
|
|
|
290
|
AsyncFunction("loadDefaultModel") { |
|
291
|
(systemPrompt: String?, samplingJson: String?) -> String in |
|
292
|
try self.configureApplicationFilesystem() |
|
293
|
guard let ptr = self.enginePtr else { |
|
294
|
throw OndeError.engineNotInitialized |
|
295
|
} |
|
296
|
let result: String? |
|
297
|
if let sp = systemPrompt, let sj = samplingJson { |
|
298
|
result = sp.withCString { cSp in |
|
299
|
sj.withCString { cSj in |
|
300
|
self.consumeRustString(onde_engine_load_default_model(ptr, cSp, cSj)) |
|
301
|
} |
|
302
|
} |
|
303
|
} else if let sp = systemPrompt { |
|
304
|
result = sp.withCString { cSp in |
|
305
|
self.consumeRustString(onde_engine_load_default_model(ptr, cSp, nil)) |
|
306
|
} |
|
307
|
} else if let sj = samplingJson { |
|
308
|
result = sj.withCString { cSj in |
|
309
|
self.consumeRustString(onde_engine_load_default_model(ptr, nil, cSj)) |
|
310
|
} |
|
311
|
} else { |
|
312
|
result = self.consumeRustString(onde_engine_load_default_model(ptr, nil, nil)) |
|
313
|
} |
|
314
|
return try self.validateJsonResult(result) |
|
315
|
} |
|
316
|
|
|
317
|
AsyncFunction("loadModel") { |
|
318
|
(configJson: String, systemPrompt: String?, samplingJson: String?) -> String in |
|
319
|
try self.configureApplicationFilesystem() |
|
320
|
guard let ptr = self.enginePtr else { |
|
321
|
throw OndeError.engineNotInitialized |
|
322
|
} |
|
323
|
let result: String? = configJson.withCString { cConfig in |
|
324
|
if let sp = systemPrompt, let sj = samplingJson { |
|
325
|
return sp.withCString { cSp in |
|
326
|
sj.withCString { cSj in |
|
327
|
self.consumeRustString(onde_engine_load_model(ptr, cConfig, cSp, cSj)) |
|
328
|
} |
|
329
|
} |
|
330
|
} else if let sp = systemPrompt { |
|
331
|
return sp.withCString { cSp in |
|
332
|
self.consumeRustString(onde_engine_load_model(ptr, cConfig, cSp, nil)) |
|
333
|
} |
|
334
|
} else if let sj = samplingJson { |
|
335
|
return sj.withCString { cSj in |
|
336
|
self.consumeRustString(onde_engine_load_model(ptr, cConfig, nil, cSj)) |
|
337
|
} |
|
338
|
} else { |
|
339
|
return self.consumeRustString(onde_engine_load_model(ptr, cConfig, nil, nil)) |
|
340
|
} |
|
341
|
} |
|
342
|
return try self.validateJsonResult(result) |
|
343
|
} |
|
344
|
|
|
345
|
AsyncFunction("unloadModel") { () -> String in |
|
346
|
guard let ptr = self.enginePtr else { |
|
347
|
throw OndeError.engineNotInitialized |
|
348
|
} |
|
349
|
let result = self.consumeRustString(onde_engine_unload_model(ptr)) |
|
350
|
return try self.validateJsonResult(result) |
|
351
|
} |
|
352
|
|
|
353
|
AsyncFunction("info") { () -> String in |
|
354
|
guard let ptr = self.enginePtr else { |
|
355
|
throw OndeError.engineNotInitialized |
|
356
|
} |
|
357
|
let result = self.consumeRustString(onde_engine_info(ptr)) |
|
358
|
return try self.validateJsonResult(result) |
|
359
|
} |
|
360
|
|
|
361
|
AsyncFunction("history") { () -> String in |
|
362
|
guard let ptr = self.enginePtr else { return "[]" } |
|
363
|
let result = self.consumeRustString(onde_engine_history(ptr)) |
|
364
|
return try self.validateJsonArray(result) |
|
365
|
} |
|
366
|
|
|
367
|
AsyncFunction("sendMessage") { (message: String) -> String in |
|
368
|
guard let ptr = self.enginePtr else { |
|
369
|
throw OndeError.engineNotInitialized |
|
370
|
} |
|
371
|
let result = message.withCString { cMessage in |
|
372
|
self.consumeRustString(onde_engine_send_message(ptr, cMessage)) |
|
373
|
} |
|
374
|
return try self.validateJsonResult(result) |
|
375
|
} |
|
376
|
|
|
377
|
AsyncFunction("generate") { |
|
378
|
(messagesJson: String, samplingJson: String?) -> String in |
|
379
|
guard let ptr = self.enginePtr else { |
|
380
|
throw OndeError.engineNotInitialized |
|
381
|
} |
|
382
|
let result: String? = messagesJson.withCString { cMessages in |
|
383
|
if let sj = samplingJson { |
|
384
|
return sj.withCString { cSj in |
|
385
|
self.consumeRustString(onde_engine_generate(ptr, cMessages, cSj)) |
|
386
|
} |
|
387
|
} else { |
|
388
|
return self.consumeRustString(onde_engine_generate(ptr, cMessages, nil)) |
|
389
|
} |
|
390
|
} |
|
391
|
return try self.validateJsonResult(result) |
|
392
|
} |
|
393
|
} |
|
394
|
} |