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 }