llama-cpp-pro 0.2.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (310) hide show
  1. package/CHANGELOG.md +295 -0
  2. package/LICENSE +21 -0
  3. package/LlamaCpp.podspec +33 -0
  4. package/LlamaCppCapacitor.podspec +33 -0
  5. package/Package.swift +29 -0
  6. package/README.md +93 -0
  7. package/android/build.gradle +88 -0
  8. package/android/src/main/AndroidManifest.xml +4 -0
  9. package/android/src/main/CMakeLists-arm64.txt +138 -0
  10. package/android/src/main/CMakeLists-x86_64.txt +141 -0
  11. package/android/src/main/CMakeLists.txt +138 -0
  12. package/android/src/main/java/ai/annadata/plugin/capacitor/LlamaCpp.java +1340 -0
  13. package/android/src/main/java/ai/annadata/plugin/capacitor/LlamaCppPlugin.java +814 -0
  14. package/android/src/main/java/ai/annadata/plugin/capacitor/ModelAdmissionController.java +214 -0
  15. package/android/src/main/jni-chat-session.cpp +261 -0
  16. package/android/src/main/jni-lora.cpp +197 -0
  17. package/android/src/main/jni-multimodal.cpp +167 -0
  18. package/android/src/main/jni-tts.cpp +290 -0
  19. package/android/src/main/jni-utils.h +148 -0
  20. package/android/src/main/jni.cpp +1808 -0
  21. package/android/src/main/jniLibs/arm64-v8a/libllama-cpp-arm64.so +0 -0
  22. package/android/src/main/res/.gitkeep +0 -0
  23. package/build-native.sh +280 -0
  24. package/cmake/desktop-metal-embed.cmake +30 -0
  25. package/cmake/desktop-sources.cmake +100 -0
  26. package/cmake/ggml-backends.cmake +44 -0
  27. package/cpp/LICENSE +21 -0
  28. package/cpp/README.md +15 -0
  29. package/cpp/anyascii.c +22223 -0
  30. package/cpp/anyascii.h +42 -0
  31. package/cpp/cap-completion.cpp +942 -0
  32. package/cpp/cap-completion.h +127 -0
  33. package/cpp/cap-embedding.cpp +193 -0
  34. package/cpp/cap-embedding.h +35 -0
  35. package/cpp/cap-ios-bridge.cpp +1810 -0
  36. package/cpp/cap-ios-bridge.h +61 -0
  37. package/cpp/cap-llama.cpp +412 -0
  38. package/cpp/cap-llama.h +161 -0
  39. package/cpp/cap-mtmd.hpp +602 -0
  40. package/cpp/cap-native-server.cpp +1095 -0
  41. package/cpp/cap-native-server.h +40 -0
  42. package/cpp/cap-tts.cpp +591 -0
  43. package/cpp/cap-tts.h +59 -0
  44. package/cpp/cap-wasm-fs.cpp +156 -0
  45. package/cpp/cap-wasm-jspi.cpp +19 -0
  46. package/cpp/cap-wasm-jspi.h +30 -0
  47. package/cpp/cap-wasm-vfs.cpp +22 -0
  48. package/cpp/chat-parser.cpp +393 -0
  49. package/cpp/chat-parser.h +120 -0
  50. package/cpp/chat.cpp +2315 -0
  51. package/cpp/chat.h +221 -0
  52. package/cpp/common.cpp +1664 -0
  53. package/cpp/common.h +744 -0
  54. package/cpp/ggml-alloc.c +1028 -0
  55. package/cpp/ggml-alloc.h +76 -0
  56. package/cpp/ggml-backend-impl.h +255 -0
  57. package/cpp/ggml-backend-reg.cpp +600 -0
  58. package/cpp/ggml-backend.cpp +2121 -0
  59. package/cpp/ggml-backend.h +354 -0
  60. package/cpp/ggml-common.h +1878 -0
  61. package/cpp/ggml-cpp.h +39 -0
  62. package/cpp/ggml-cpu/amx/amx.cpp +221 -0
  63. package/cpp/ggml-cpu/amx/amx.h +8 -0
  64. package/cpp/ggml-cpu/amx/common.h +91 -0
  65. package/cpp/ggml-cpu/amx/mmq.cpp +2512 -0
  66. package/cpp/ggml-cpu/amx/mmq.h +10 -0
  67. package/cpp/ggml-cpu/arch/arm/cpu-feats.cpp +94 -0
  68. package/cpp/ggml-cpu/arch/arm/quants.c +3650 -0
  69. package/cpp/ggml-cpu/arch/arm/repack.cpp +1891 -0
  70. package/cpp/ggml-cpu/arch/x86/cpu-feats.cpp +327 -0
  71. package/cpp/ggml-cpu/arch/x86/quants.c +3820 -0
  72. package/cpp/ggml-cpu/arch/x86/repack.cpp +6307 -0
  73. package/cpp/ggml-cpu/arch-fallback.h +215 -0
  74. package/cpp/ggml-cpu/binary-ops.cpp +158 -0
  75. package/cpp/ggml-cpu/binary-ops.h +16 -0
  76. package/cpp/ggml-cpu/common.h +73 -0
  77. package/cpp/ggml-cpu/ggml-cpu-impl.h +559 -0
  78. package/cpp/ggml-cpu/ggml-cpu.c +3578 -0
  79. package/cpp/ggml-cpu/ggml-cpu.cpp +672 -0
  80. package/cpp/ggml-cpu/ops.cpp +10587 -0
  81. package/cpp/ggml-cpu/ops.h +114 -0
  82. package/cpp/ggml-cpu/quants.c +1193 -0
  83. package/cpp/ggml-cpu/quants.h +97 -0
  84. package/cpp/ggml-cpu/repack.cpp +1982 -0
  85. package/cpp/ggml-cpu/repack.h +120 -0
  86. package/cpp/ggml-cpu/simd-mappings.h +1184 -0
  87. package/cpp/ggml-cpu/traits.cpp +36 -0
  88. package/cpp/ggml-cpu/traits.h +38 -0
  89. package/cpp/ggml-cpu/unary-ops.cpp +186 -0
  90. package/cpp/ggml-cpu/unary-ops.h +28 -0
  91. package/cpp/ggml-cpu/vec.cpp +348 -0
  92. package/cpp/ggml-cpu/vec.h +1121 -0
  93. package/cpp/ggml-cpu.h +145 -0
  94. package/cpp/ggml-impl.h +622 -0
  95. package/cpp/ggml-metal-impl.h +688 -0
  96. package/cpp/ggml-metal.h +66 -0
  97. package/cpp/ggml-metal.m +6833 -0
  98. package/cpp/ggml-metal.metal +10754 -0
  99. package/cpp/ggml-opt.cpp +1093 -0
  100. package/cpp/ggml-opt.h +256 -0
  101. package/cpp/ggml-quants.c +5324 -0
  102. package/cpp/ggml-quants.h +106 -0
  103. package/cpp/ggml-threading.cpp +12 -0
  104. package/cpp/ggml-threading.h +14 -0
  105. package/cpp/ggml.c +7108 -0
  106. package/cpp/ggml.h +2492 -0
  107. package/cpp/gguf.cpp +1358 -0
  108. package/cpp/gguf.h +202 -0
  109. package/cpp/json-partial.cpp +256 -0
  110. package/cpp/json-partial.h +38 -0
  111. package/cpp/json-schema-to-grammar.cpp +985 -0
  112. package/cpp/json-schema-to-grammar.h +21 -0
  113. package/cpp/llama-adapter.cpp +388 -0
  114. package/cpp/llama-adapter.h +76 -0
  115. package/cpp/llama-arch.cpp +2355 -0
  116. package/cpp/llama-arch.h +499 -0
  117. package/cpp/llama-batch.cpp +875 -0
  118. package/cpp/llama-batch.h +160 -0
  119. package/cpp/llama-chat.cpp +783 -0
  120. package/cpp/llama-chat.h +65 -0
  121. package/cpp/llama-context.cpp +2788 -0
  122. package/cpp/llama-context.h +306 -0
  123. package/cpp/llama-cparams.cpp +5 -0
  124. package/cpp/llama-cparams.h +41 -0
  125. package/cpp/llama-cpp.h +30 -0
  126. package/cpp/llama-grammar.cpp +1229 -0
  127. package/cpp/llama-grammar.h +173 -0
  128. package/cpp/llama-graph.cpp +1891 -0
  129. package/cpp/llama-graph.h +810 -0
  130. package/cpp/llama-hparams.cpp +180 -0
  131. package/cpp/llama-hparams.h +233 -0
  132. package/cpp/llama-impl.cpp +167 -0
  133. package/cpp/llama-impl.h +61 -0
  134. package/cpp/llama-io.cpp +15 -0
  135. package/cpp/llama-io.h +35 -0
  136. package/cpp/llama-kv-cache-iswa.cpp +318 -0
  137. package/cpp/llama-kv-cache-iswa.h +135 -0
  138. package/cpp/llama-kv-cache.cpp +2059 -0
  139. package/cpp/llama-kv-cache.h +374 -0
  140. package/cpp/llama-kv-cells.h +491 -0
  141. package/cpp/llama-memory-hybrid.cpp +258 -0
  142. package/cpp/llama-memory-hybrid.h +137 -0
  143. package/cpp/llama-memory-recurrent.cpp +1146 -0
  144. package/cpp/llama-memory-recurrent.h +179 -0
  145. package/cpp/llama-memory.cpp +59 -0
  146. package/cpp/llama-memory.h +119 -0
  147. package/cpp/llama-mmap.cpp +609 -0
  148. package/cpp/llama-mmap.h +68 -0
  149. package/cpp/llama-model-loader.cpp +1166 -0
  150. package/cpp/llama-model-loader.h +170 -0
  151. package/cpp/llama-model-saver.cpp +282 -0
  152. package/cpp/llama-model-saver.h +37 -0
  153. package/cpp/llama-model.cpp +19061 -0
  154. package/cpp/llama-model.h +491 -0
  155. package/cpp/llama-sampling.cpp +2575 -0
  156. package/cpp/llama-sampling.h +32 -0
  157. package/cpp/llama-vocab.cpp +3792 -0
  158. package/cpp/llama-vocab.h +176 -0
  159. package/cpp/llama.cpp +358 -0
  160. package/cpp/llama.h +1373 -0
  161. package/cpp/log.cpp +428 -0
  162. package/cpp/log.h +103 -0
  163. package/cpp/minja/chat-template.hpp +550 -0
  164. package/cpp/minja/minja.hpp +3009 -0
  165. package/cpp/nlohmann/json.hpp +25526 -0
  166. package/cpp/nlohmann/json_fwd.hpp +187 -0
  167. package/cpp/regex-partial.cpp +204 -0
  168. package/cpp/regex-partial.h +56 -0
  169. package/cpp/sampling.cpp +579 -0
  170. package/cpp/sampling.h +107 -0
  171. package/cpp/tools/mtmd/clip-impl.h +473 -0
  172. package/cpp/tools/mtmd/clip.cpp +4322 -0
  173. package/cpp/tools/mtmd/clip.h +106 -0
  174. package/cpp/tools/mtmd/miniaudio/miniaudio.h +93468 -0
  175. package/cpp/tools/mtmd/mtmd-audio.cpp +769 -0
  176. package/cpp/tools/mtmd/mtmd-audio.h +47 -0
  177. package/cpp/tools/mtmd/mtmd-helper.cpp +460 -0
  178. package/cpp/tools/mtmd/mtmd-helper.h +95 -0
  179. package/cpp/tools/mtmd/mtmd.cpp +1066 -0
  180. package/cpp/tools/mtmd/mtmd.h +302 -0
  181. package/cpp/tools/mtmd/stb/stb_image.h +7988 -0
  182. package/cpp/unicode-data.cpp +7034 -0
  183. package/cpp/unicode-data.h +20 -0
  184. package/cpp/unicode.cpp +1061 -0
  185. package/cpp/unicode.h +68 -0
  186. package/cpp/vendor/cpp-httplib/httplib.cpp +16509 -0
  187. package/cpp/vendor/cpp-httplib/httplib.h +3883 -0
  188. package/desktop/electron-builder.config.cjs +157 -0
  189. package/desktop/entitlements.mac.plist +12 -0
  190. package/desktop/package.json +11 -0
  191. package/desktop/resolve-package-root.cjs +88 -0
  192. package/desktop/src/main/backend-selector.cjs +148 -0
  193. package/desktop/src/main/gpu-probe.cjs +142 -0
  194. package/desktop/src/main/index.cjs +58 -0
  195. package/desktop/src/main/ipc-handlers.cjs +212 -0
  196. package/desktop/src/main/model-store.cjs +50 -0
  197. package/desktop/src/main/preload.cjs +39 -0
  198. package/desktop/src/main/sidecar-client.cjs +76 -0
  199. package/desktop/src/main/sidecar-manager.cjs +364 -0
  200. package/dist/docs.json +14357 -0
  201. package/dist/esm/definitions.d.ts +731 -0
  202. package/dist/esm/definitions.js +2 -0
  203. package/dist/esm/definitions.js.map +1 -0
  204. package/dist/esm/desktop.d.ts +37 -0
  205. package/dist/esm/desktop.js +133 -0
  206. package/dist/esm/desktop.js.map +1 -0
  207. package/dist/esm/index.d.ts +200 -0
  208. package/dist/esm/index.js +612 -0
  209. package/dist/esm/index.js.map +1 -0
  210. package/dist/esm/isomorphic/desktop.runtime.d.ts +37 -0
  211. package/dist/esm/isomorphic/desktop.runtime.js +36 -0
  212. package/dist/esm/isomorphic/desktop.runtime.js.map +1 -0
  213. package/dist/esm/isomorphic/errors.d.ts +6 -0
  214. package/dist/esm/isomorphic/errors.js +9 -0
  215. package/dist/esm/isomorphic/errors.js.map +1 -0
  216. package/dist/esm/isomorphic/model.admission.d.ts +17 -0
  217. package/dist/esm/isomorphic/model.admission.js +27 -0
  218. package/dist/esm/isomorphic/model.admission.js.map +1 -0
  219. package/dist/esm/isomorphic/model.scheduler.d.ts +31 -0
  220. package/dist/esm/isomorphic/model.scheduler.js +95 -0
  221. package/dist/esm/isomorphic/model.scheduler.js.map +1 -0
  222. package/dist/esm/isomorphic/provider.desktop.d.ts +40 -0
  223. package/dist/esm/isomorphic/provider.desktop.js +411 -0
  224. package/dist/esm/isomorphic/provider.desktop.js.map +1 -0
  225. package/dist/esm/isomorphic/provider.factory.d.ts +2 -0
  226. package/dist/esm/isomorphic/provider.factory.js +16 -0
  227. package/dist/esm/isomorphic/provider.factory.js.map +1 -0
  228. package/dist/esm/isomorphic/provider.interface.d.ts +76 -0
  229. package/dist/esm/isomorphic/provider.interface.js +2 -0
  230. package/dist/esm/isomorphic/provider.interface.js.map +1 -0
  231. package/dist/esm/isomorphic/provider.native.d.ts +18 -0
  232. package/dist/esm/isomorphic/provider.native.js +173 -0
  233. package/dist/esm/isomorphic/provider.native.js.map +1 -0
  234. package/dist/esm/isomorphic/provider.web.d.ts +88 -0
  235. package/dist/esm/isomorphic/provider.web.js +573 -0
  236. package/dist/esm/isomorphic/provider.web.js.map +1 -0
  237. package/dist/esm/isomorphic/sidecar-sse.d.ts +14 -0
  238. package/dist/esm/isomorphic/sidecar-sse.js +76 -0
  239. package/dist/esm/isomorphic/sidecar-sse.js.map +1 -0
  240. package/dist/esm/isomorphic/wasmMemoryCalibration.d.ts +26 -0
  241. package/dist/esm/isomorphic/wasmMemoryCalibration.js +60 -0
  242. package/dist/esm/isomorphic/wasmMemoryCalibration.js.map +1 -0
  243. package/dist/esm/isomorphic/wasmMemoryPolicy.d.ts +55 -0
  244. package/dist/esm/isomorphic/wasmMemoryPolicy.js +93 -0
  245. package/dist/esm/isomorphic/wasmMemoryPolicy.js.map +1 -0
  246. package/dist/esm/storage/manifest.d.ts +13 -0
  247. package/dist/esm/storage/manifest.js +87 -0
  248. package/dist/esm/storage/manifest.js.map +1 -0
  249. package/dist/esm/storage/opfs.store.d.ts +31 -0
  250. package/dist/esm/storage/opfs.store.js +241 -0
  251. package/dist/esm/storage/opfs.store.js.map +1 -0
  252. package/dist/esm/web.d.ts +206 -0
  253. package/dist/esm/web.js +521 -0
  254. package/dist/esm/web.js.map +1 -0
  255. package/dist/esm/workers/async-file.d.ts +13 -0
  256. package/dist/esm/workers/async-file.js +12 -0
  257. package/dist/esm/workers/async-file.js.map +1 -0
  258. package/dist/esm/workers/heapfs.d.ts +55 -0
  259. package/dist/esm/workers/heapfs.js +115 -0
  260. package/dist/esm/workers/heapfs.js.map +1 -0
  261. package/dist/esm/workers/wasm.engine.d.ts +86 -0
  262. package/dist/esm/workers/wasm.engine.js +504 -0
  263. package/dist/esm/workers/wasm.engine.js.map +1 -0
  264. package/dist/esm/workers/worker.protocol.d.ts +160 -0
  265. package/dist/esm/workers/worker.protocol.js +2 -0
  266. package/dist/esm/workers/worker.protocol.js.map +1 -0
  267. package/dist/plugin.cjs +3179 -0
  268. package/dist/plugin.cjs.map +1 -0
  269. package/dist/plugin.js +3181 -0
  270. package/dist/plugin.js.map +1 -0
  271. package/dist/wasm/llama_engine.d.ts +74 -0
  272. package/dist/wasm/llama_engine.js +1829 -0
  273. package/dist/wasm/llama_engine.wasm +0 -0
  274. package/dist/wasm/llama_engine_emscripten.mjs +2 -0
  275. package/dist/wasm/package.json +16 -0
  276. package/dist/workers/llm.worker.js +1226 -0
  277. package/dist/workers/llm.worker.js.map +7 -0
  278. package/extraResources/llama-wasm/llama_engine.d.ts +74 -0
  279. package/extraResources/llama-wasm/llama_engine.js +1829 -0
  280. package/extraResources/llama-wasm/llama_engine.wasm +0 -0
  281. package/extraResources/llama-wasm/llama_engine_emscripten.mjs +2 -0
  282. package/extraResources/llama-wasm/package.json +16 -0
  283. package/extraResources/sidecar/README.md +11 -0
  284. package/extraResources/sidecar/darwin-arm64 +0 -0
  285. package/extraResources/sidecar/darwin-x64 +0 -0
  286. package/ios/CMakeLists-arm64.txt +161 -0
  287. package/ios/CMakeLists-x86_64.txt +188 -0
  288. package/ios/CMakeLists.txt +161 -0
  289. package/ios/Frameworks/llama-cpp.framework/Info.plist +28 -0
  290. package/ios/Frameworks/llama-cpp.framework/llama-cpp +0 -0
  291. package/ios/Sources/LlamaCppCapacitor/LlamaCpp.swift +1365 -0
  292. package/ios/Sources/LlamaCppCapacitor/LlamaCppPlugin.swift +694 -0
  293. package/ios/Sources/LlamaCppCapacitor/LlamaNativeBridge.swift +349 -0
  294. package/ios/Sources/LlamaCppCapacitor/ModelAdmissionController.swift +177 -0
  295. package/ios/embed-metal-shaders.sh +24 -0
  296. package/ios/metal-embed.cmake +29 -0
  297. package/package.json +281 -0
  298. package/scripts/build-js-preserve-wasm.cjs +53 -0
  299. package/scripts/build-sidecar-linux.sh +6 -0
  300. package/scripts/build-sidecar-win.bat +19 -0
  301. package/scripts/build-sidecar.sh +184 -0
  302. package/scripts/embed-llama-ios-app-framework.sh +63 -0
  303. package/scripts/ensure-desktop-sidecar-bundle.cjs +103 -0
  304. package/scripts/ensure-llama-ios-xcframework.sh +108 -0
  305. package/scripts/fix-esm-extensions.cjs +45 -0
  306. package/scripts/prepare-js-dist.cjs +28 -0
  307. package/scripts/stage-desktop-resources.cjs +116 -0
  308. package/sidecar/CMakeLists.txt +192 -0
  309. package/sidecar/cap-sidecar-main.cpp +68 -0
  310. package/types/llama-cpp-pro.d.ts +441 -0
@@ -0,0 +1,1365 @@
1
+ import Foundation
2
+ import Capacitor
3
+
4
+ // MARK: - Numeric clamping helper (used by decodeAudioTokens)
5
+ private extension Comparable {
6
+ func clamped(to range: ClosedRange<Self>) -> Self {
7
+ return min(max(self, range.lowerBound), range.upperBound)
8
+ }
9
+ }
10
+
11
+ // MARK: - Native Library Integration
12
+
13
+ private enum LibraryLoader {
14
+ /// dlopen must target the framework Mach-O (`llama-cpp.framework/llama-cpp`), not the `.framework` directory.
15
+ static var llamaLibrary: UnsafeMutableRawPointer? = {
16
+ let fm = FileManager.default
17
+ var candidates: [String] = []
18
+ if let fw = Bundle.main.path(forResource: "llama-cpp", ofType: "framework") {
19
+ candidates.append((fw as NSString).appendingPathComponent("llama-cpp"))
20
+ }
21
+ if let exec = Bundle.main.executablePath {
22
+ let frameworks = (exec as NSString).deletingLastPathComponent + "/Frameworks/llama-cpp.framework/llama-cpp"
23
+ candidates.append(frameworks)
24
+ }
25
+ for path in candidates where fm.fileExists(atPath: path) {
26
+ guard let handle = dlopen(path, RTLD_NOW) else {
27
+ print("[LlamaCpp] dlopen failed for \(path): \(String(cString: dlerror()))")
28
+ continue
29
+ }
30
+ return handle
31
+ }
32
+ print("[LlamaCpp] llama-cpp framework binary not found; tried: \(candidates)")
33
+ return nil
34
+ }()
35
+ }
36
+
37
+ private var llamaLibrary: UnsafeMutableRawPointer? { LibraryLoader.llamaLibrary }
38
+
39
+ private typealias NativeInitContext = @convention(c) (UnsafePointer<CChar>?, UnsafePointer<CChar>?) -> Int64
40
+ private typealias NativeReleaseContext = @convention(c) (Int64) -> Void
41
+ private typealias NativeCompletion = @convention(c) (Int64, UnsafePointer<CChar>?) -> UnsafePointer<CChar>?
42
+ private typealias NativeGetContextModelJson = @convention(c) (Int64) -> UnsafePointer<CChar>?
43
+ private typealias NativeContextNEmbd = @convention(c) (Int64) -> Int32
44
+ private typealias NativeGetFormattedChat = @convention(c) (Int64, UnsafePointer<CChar>?, UnsafePointer<CChar>?, UnsafePointer<CChar>?) -> UnsafePointer<CChar>?
45
+ private typealias NativeToggleLog = @convention(c) (Bool) -> Bool
46
+ private typealias NativeEmbedding = @convention(c) (Int64, UnsafePointer<CChar>?, UnsafePointer<CChar>?) -> UnsafePointer<Float>?
47
+ private typealias NativeRegisterEmb = @convention(c) (Int64, UnsafeMutableRawPointer?) -> Void
48
+ private typealias NativeUnregisterEmb = @convention(c) (Int64) -> Void
49
+ private typealias NativeModelInfo = @convention(c) (UnsafePointer<CChar>?, UnsafePointer<CChar>?) -> UnsafePointer<CChar>?
50
+ private typealias NativeTokenize = @convention(c) (Int64, UnsafePointer<CChar>?, UnsafePointer<CChar>?) -> UnsafePointer<CChar>?
51
+ private typealias NativeDetokenize = @convention(c) (Int64, UnsafePointer<CChar>?) -> UnsafePointer<CChar>?
52
+ private typealias NativeGrammar = @convention(c) (UnsafePointer<CChar>?) -> UnsafePointer<CChar>?
53
+ private typealias NativeCapServerStart = @convention(c) (UnsafePointer<CChar>?, UnsafePointer<CChar>?, Int32, UnsafePointer<CChar>?) -> Int32
54
+ private typealias NativeCapServerStop = @convention(c) () -> Void
55
+ private typealias NativeCapServerIsRunning = @convention(c) () -> Int32
56
+
57
+ private var initContextFunc: NativeInitContext?
58
+ private var releaseContextFunc: NativeReleaseContext?
59
+ private var completionFunc: NativeCompletion?
60
+ private var getContextModelJsonFunc: NativeGetContextModelJson?
61
+ private var contextNEmbdFunc: NativeContextNEmbd?
62
+ private var stopCompletionFunc: NativeReleaseContext?
63
+ private var getFormattedChatFunc: NativeGetFormattedChat?
64
+ private var toggleNativeLogFunc: NativeToggleLog?
65
+ private var embeddingFunc: NativeEmbedding?
66
+ private var registerEmbeddingContextFunc: NativeRegisterEmb?
67
+ private var unregisterEmbeddingContextFunc: NativeUnregisterEmb?
68
+ private var modelInfoFunc: NativeModelInfo?
69
+ private var tokenizeFunc: NativeTokenize?
70
+ private var detokenizeFunc: NativeDetokenize?
71
+ private var grammarFunc: NativeGrammar?
72
+ private var capServerStartFunc: NativeCapServerStart?
73
+ private var capServerStopFunc: NativeCapServerStop?
74
+ private var capServerIsRunningFunc: NativeCapServerIsRunning?
75
+
76
+ private func loadFunctionPointers() {
77
+ guard let library = llamaLibrary else { return }
78
+ func sym<T>(_ name: String, _ type: T.Type) -> T? {
79
+ guard let p = dlsym(library, name) else { return nil }
80
+ return unsafeBitCast(p, to: T.self)
81
+ }
82
+ initContextFunc = sym("llama_init_context", NativeInitContext.self)
83
+ releaseContextFunc = sym("llama_release_context", NativeReleaseContext.self)
84
+ completionFunc = sym("llama_completion", NativeCompletion.self)
85
+ getContextModelJsonFunc = sym("llama_get_context_model_json", NativeGetContextModelJson.self)
86
+ contextNEmbdFunc = sym("llama_context_n_embd", NativeContextNEmbd.self)
87
+ stopCompletionFunc = sym("llama_stop_completion", NativeReleaseContext.self)
88
+ getFormattedChatFunc = sym("llama_get_formatted_chat", NativeGetFormattedChat.self)
89
+ toggleNativeLogFunc = sym("llama_toggle_native_log", NativeToggleLog.self)
90
+ embeddingFunc = sym("llama_embedding", NativeEmbedding.self)
91
+ registerEmbeddingContextFunc = sym("llama_embedding_register_context", NativeRegisterEmb.self)
92
+ unregisterEmbeddingContextFunc = sym("llama_embedding_unregister_context", NativeUnregisterEmb.self)
93
+ modelInfoFunc = sym("llama_model_info", NativeModelInfo.self)
94
+ tokenizeFunc = sym("llama_cap_tokenize", NativeTokenize.self)
95
+ detokenizeFunc = sym("llama_cap_detokenize", NativeDetokenize.self)
96
+ grammarFunc = sym("llama_convert_json_schema_to_grammar", NativeGrammar.self)
97
+ capServerStartFunc = sym("cap_llama_server_start", NativeCapServerStart.self)
98
+ capServerStopFunc = sym("cap_llama_server_stop", NativeCapServerStop.self)
99
+ capServerIsRunningFunc = sym("cap_llama_server_is_running", NativeCapServerIsRunning.self)
100
+ }
101
+
102
+ private func jsonObject(fromCString p: UnsafePointer<CChar>?) -> [String: Any]? {
103
+ guard let p = p else { return nil }
104
+ let s = String(cString: p)
105
+ guard let d = s.data(using: .utf8),
106
+ let obj = try? JSONSerialization.jsonObject(with: d) as? [String: Any] else { return nil }
107
+ return obj
108
+ }
109
+
110
+ // MARK: - Result Types
111
+ typealias LlamaResult<T> = Result<T, LlamaError>
112
+
113
+ enum LlamaError: Error, LocalizedError {
114
+ case contextNotFound
115
+ case modelNotFound
116
+ case invalidParameters
117
+ case operationFailed(String)
118
+ case notImplemented
119
+
120
+ var errorDescription: String? {
121
+ switch self {
122
+ case .contextNotFound:
123
+ return "Context not found"
124
+ case .modelNotFound:
125
+ return "Model not found"
126
+ case .invalidParameters:
127
+ return "Invalid parameters"
128
+ case .operationFailed(let message):
129
+ return "Operation failed: \(message)"
130
+ case .notImplemented:
131
+ return "Operation not implemented"
132
+ }
133
+ }
134
+ }
135
+
136
+ // MARK: - Context Management
137
+ class LlamaContext {
138
+ let id: Int
139
+ var model: LlamaModel?
140
+ var isMultimodalEnabled: Bool = false
141
+ var isVocoderEnabled: Bool = false
142
+
143
+ init(id: Int) {
144
+ self.id = id
145
+ }
146
+ }
147
+
148
+ class LlamaModel {
149
+ let path: String
150
+ var desc: String
151
+ var size: Int
152
+ var nEmbd: Int
153
+ var nParams: Int
154
+ var chatTemplates: ChatTemplates
155
+ var metadata: [String: Any]
156
+
157
+ init(path: String, desc: String, size: Int, nEmbd: Int, nParams: Int, chatTemplates: ChatTemplates, metadata: [String: Any]) {
158
+ self.path = path
159
+ self.desc = desc
160
+ self.size = size
161
+ self.nEmbd = nEmbd
162
+ self.nParams = nParams
163
+ self.chatTemplates = chatTemplates
164
+ self.metadata = metadata
165
+ }
166
+ }
167
+
168
+ struct ChatTemplates {
169
+ let llamaChat: Bool
170
+ let minja: MinjaTemplates
171
+
172
+ init(llamaChat: Bool, minja: MinjaTemplates) {
173
+ self.llamaChat = llamaChat
174
+ self.minja = minja
175
+ }
176
+ }
177
+
178
+ struct MinjaTemplates {
179
+ let `default`: Bool
180
+ let defaultCaps: MinjaCaps
181
+ let toolUse: Bool
182
+ let toolUseCaps: MinjaCaps
183
+
184
+ init(default: Bool, defaultCaps: MinjaCaps, toolUse: Bool, toolUseCaps: MinjaCaps) {
185
+ self.default = `default`
186
+ self.defaultCaps = defaultCaps
187
+ self.toolUse = toolUse
188
+ self.toolUseCaps = toolUseCaps
189
+ }
190
+ }
191
+
192
+ struct MinjaCaps {
193
+ let tools: Bool
194
+ let toolCalls: Bool
195
+ let toolResponses: Bool
196
+ let systemRole: Bool
197
+ let parallelToolCalls: Bool
198
+ let toolCallId: Bool
199
+
200
+ init(tools: Bool, toolCalls: Bool, toolResponses: Bool, systemRole: Bool, parallelToolCalls: Bool, toolCallId: Bool) {
201
+ self.tools = tools
202
+ self.toolCalls = toolCalls
203
+ self.toolResponses = toolResponses
204
+ self.systemRole = systemRole
205
+ self.parallelToolCalls = parallelToolCalls
206
+ self.toolCallId = toolCallId
207
+ }
208
+ }
209
+
210
+ // MARK: - Main Implementation
211
+ @objc public class LlamaCpp: NSObject {
212
+ private var contexts: [Int: LlamaContext] = [:]
213
+ private var nativeContexts: [Int64: UnsafeMutableRawPointer] = [:]
214
+ private var contextIdToNative: [Int: Int64] = [:]
215
+ private var contextCounter: Int = 0
216
+ // Default aligned with the isomorphic limit (parity with WASM_MAX_CONCURRENT_MODELS = 5).
217
+ private var contextLimit: Int = ModelAdmissionController.maxConcurrentModels
218
+ private var nativeLogEnabled: Bool = false
219
+ // Memory admission control (parity with the Web DefaultModelScheduler).
220
+ private let admissionController = ModelAdmissionController()
221
+
222
+ private func defaultMinjaCaps() -> MinjaCaps {
223
+ MinjaCaps(
224
+ tools: true,
225
+ toolCalls: true,
226
+ toolResponses: true,
227
+ systemRole: true,
228
+ parallelToolCalls: true,
229
+ toolCallId: true
230
+ )
231
+ }
232
+
233
+ private func applyModelMetadata(_ model: inout LlamaModel, from m: [String: Any], defaultCaps: MinjaCaps) {
234
+ model.desc = (m["desc"] as? String) ?? model.desc
235
+ if let n = m["size"] as? NSNumber { model.size = n.intValue }
236
+ if let n = m["nEmbd"] as? NSNumber { model.nEmbd = n.intValue }
237
+ if let n = m["nParams"] as? NSNumber { model.nParams = n.intValue }
238
+ if let meta = m["metadata"] as? [String: Any] { model.metadata = meta }
239
+ if let ct = m["chatTemplates"] as? [String: Any] {
240
+ let llamaChat = (ct["llamaChat"] as? NSNumber)?.boolValue ?? true
241
+ if let minja = ct["minja"] as? [String: Any] {
242
+ func caps(_ d: [String: Any]?) -> MinjaCaps {
243
+ guard let d = d else { return defaultCaps }
244
+ return MinjaCaps(
245
+ tools: (d["tools"] as? NSNumber)?.boolValue ?? true,
246
+ toolCalls: (d["toolCalls"] as? NSNumber)?.boolValue ?? true,
247
+ toolResponses: (d["toolResponses"] as? NSNumber)?.boolValue ?? true,
248
+ systemRole: (d["systemRole"] as? NSNumber)?.boolValue ?? true,
249
+ parallelToolCalls: (d["parallelToolCalls"] as? NSNumber)?.boolValue ?? true,
250
+ toolCallId: (d["toolCallId"] as? NSNumber)?.boolValue ?? true
251
+ )
252
+ }
253
+ let defCaps = caps(minja["defaultCaps"] as? [String: Any])
254
+ let tuCaps = caps(minja["toolUseCaps"] as? [String: Any])
255
+ model.chatTemplates = ChatTemplates(
256
+ llamaChat: llamaChat,
257
+ minja: MinjaTemplates(
258
+ default: (minja["default"] as? NSNumber)?.boolValue ?? true,
259
+ defaultCaps: defCaps,
260
+ toolUse: (minja["toolUse"] as? NSNumber)?.boolValue ?? true,
261
+ toolUseCaps: tuCaps
262
+ )
263
+ )
264
+ }
265
+ }
266
+ }
267
+
268
+ /// Query n_embd from native at init or embed time (Android JNI does this inline).
269
+ private func resolveEmbeddingDimension(context: LlamaContext, nativeId: Int64) -> Int {
270
+ if let cached = context.model?.nEmbd, cached > 0 {
271
+ return cached
272
+ }
273
+ if getContextModelJsonFunc == nil || contextNEmbdFunc == nil {
274
+ loadFunctionPointers()
275
+ }
276
+ if let jsonFn = getContextModelJsonFunc, let c = jsonFn(nativeId), let m = jsonObject(fromCString: c) {
277
+ if var model = context.model {
278
+ applyModelMetadata(&model, from: m, defaultCaps: defaultMinjaCaps())
279
+ context.model = model
280
+ if model.nEmbd > 0 {
281
+ return model.nEmbd
282
+ }
283
+ }
284
+ }
285
+ if let nEmbdFn = contextNEmbdFunc {
286
+ let n = Int(nEmbdFn(nativeId))
287
+ if n > 0 {
288
+ if var model = context.model {
289
+ model.nEmbd = n
290
+ context.model = model
291
+ }
292
+ return n
293
+ }
294
+ }
295
+ return 0
296
+ }
297
+
298
+ // MARK: - Core initialization and management
299
+
300
+ func toggleNativeLog(enabled: Bool, completion: @escaping (LlamaResult<Void>) -> Void) {
301
+ nativeLogEnabled = enabled
302
+ if initContextFunc == nil { loadFunctionPointers() }
303
+ if let fn = toggleNativeLogFunc {
304
+ _ = fn(enabled)
305
+ }
306
+ if enabled {
307
+ print("[LlamaCpp] Native logging enabled")
308
+ } else {
309
+ print("[LlamaCpp] Native logging disabled")
310
+ }
311
+ completion(.success(()))
312
+ }
313
+
314
+ func setContextLimit(limit: Int, completion: @escaping (LlamaResult<Void>) -> Void) {
315
+ let maxAllowed = admissionController.maxModelCount
316
+ guard limit >= 1 else {
317
+ completion(.failure(.operationFailed("Context limit must be at least 1")))
318
+ return
319
+ }
320
+ if limit > maxAllowed {
321
+ // Clamp to the isomorphic hard cap rather than silently over-committing memory.
322
+ print("[LlamaCpp] Requested context limit \(limit) exceeds max \(maxAllowed); clamping to \(maxAllowed)")
323
+ contextLimit = maxAllowed
324
+ } else {
325
+ contextLimit = limit
326
+ }
327
+ print("[LlamaCpp] Context limit set to \(contextLimit)")
328
+ completion(.success(()))
329
+ }
330
+
331
+ func modelInfo(path: String, skip: [String], completion: @escaping (LlamaResult<[String: Any]>) -> Void) {
332
+ if modelInfoFunc == nil { loadFunctionPointers() }
333
+ guard let fn = modelInfoFunc else {
334
+ completion(.failure(.operationFailed("llama_model_info not found")))
335
+ return
336
+ }
337
+ var skipJson = "[]"
338
+ if !skip.isEmpty, let d = try? JSONSerialization.data(withJSONObject: skip), let s = String(data: d, encoding: .utf8) {
339
+ skipJson = s
340
+ }
341
+ let result: LlamaResult<[String: Any]> = path.withCString { pathPtr in
342
+ skipJson.withCString { skipPtr in
343
+ guard let c = fn(pathPtr, skipPtr), let dict = jsonObject(fromCString: c) else {
344
+ return .failure(.operationFailed("model info failed"))
345
+ }
346
+ if let err = dict["error"] as? String {
347
+ return .failure(.operationFailed(err))
348
+ }
349
+ return .success(dict)
350
+ }
351
+ }
352
+ completion(result)
353
+ }
354
+
355
+ func initContext(contextId: Int, params: [String: Any], completion: @escaping (LlamaResult<[String: Any]>) -> Void) {
356
+ // Extract parameters
357
+ guard let modelPath = params["model"] as? String else {
358
+ completion(.failure(.invalidParameters))
359
+ return
360
+ }
361
+
362
+ // Memory admission control: enforce the concurrent-model slot limit AND a
363
+ // process-memory guard before touching native code (parity with the Web path).
364
+ let embedding = (params["embedding"] as? Bool) ?? false
365
+ let decision = admissionController.canAdmit(
366
+ currentlyLoaded: contexts.count,
367
+ modelPath: modelPath,
368
+ embedding: embedding
369
+ )
370
+ if !decision.allow {
371
+ let prefix = decision.deniedBy == .limit ? "MODEL_LIMIT_REACHED: " : "INSUFFICIENT_MEMORY: "
372
+ let reason = decision.reason ?? "Model admission rejected"
373
+ print("[LlamaCpp] Admission rejected for context \(contextId) — \(reason)")
374
+ completion(.failure(.operationFailed(prefix + reason)))
375
+ return
376
+ }
377
+
378
+ // Create context
379
+ let context = LlamaContext(id: contextId)
380
+
381
+ let defaultCaps = defaultMinjaCaps()
382
+ let defaultTemplates = ChatTemplates(
383
+ llamaChat: true,
384
+ minja: MinjaTemplates(
385
+ default: true,
386
+ defaultCaps: defaultCaps,
387
+ toolUse: true,
388
+ toolUseCaps: defaultCaps
389
+ )
390
+ )
391
+
392
+ var model = LlamaModel(
393
+ path: modelPath,
394
+ desc: "model",
395
+ size: 0,
396
+ nEmbd: 0,
397
+ nParams: 0,
398
+ chatTemplates: defaultTemplates,
399
+ metadata: [:]
400
+ )
401
+
402
+ context.model = model
403
+
404
+ var paramsJson = "{}"
405
+ do {
406
+ let paramsData = try JSONSerialization.data(withJSONObject: params)
407
+ paramsJson = String(data: paramsData, encoding: .utf8) ?? "{}"
408
+ } catch {
409
+ completion(.failure(.operationFailed("Failed to serialize params: \(error.localizedDescription)")))
410
+ return
411
+ }
412
+
413
+ if initContextFunc == nil {
414
+ loadFunctionPointers()
415
+ }
416
+
417
+ let nativeContextId: Int64
418
+ do {
419
+ nativeContextId = try LlamaNativeBridge.initContext(modelPath: modelPath, paramsJson: paramsJson)
420
+ } catch let error as LlamaNativeBridge.Failure {
421
+ completion(.failure(.operationFailed(error.localizedDescription)))
422
+ return
423
+ } catch {
424
+ completion(.failure(.operationFailed("Native initContext failed: \(error.localizedDescription)")))
425
+ return
426
+ }
427
+
428
+ guard nativeContextId > 0 else {
429
+ completion(.failure(.operationFailed("Failed to initialize native context")))
430
+ return
431
+ }
432
+
433
+ contexts[contextId] = context
434
+ nativeContexts[nativeContextId] = UnsafeMutableRawPointer(bitPattern: Int(truncatingIfNeeded: nativeContextId))
435
+ contextIdToNative[contextId] = nativeContextId
436
+
437
+ if let jsonFn = getContextModelJsonFunc, let c = jsonFn(nativeContextId), let m = jsonObject(fromCString: c) {
438
+ applyModelMetadata(&model, from: m, defaultCaps: defaultCaps)
439
+ context.model = model
440
+ }
441
+ if model.nEmbd <= 0 {
442
+ _ = resolveEmbeddingDimension(context: context, nativeId: nativeContextId)
443
+ if let refreshed = context.model {
444
+ model = refreshed
445
+ }
446
+ }
447
+
448
+ // Query GPU info from native layer
449
+ let gpuInfo = LlamaNativeBridge.queryGpuInfo(nativeContextId)
450
+ let gpuEnabled = gpuInfo.gpu
451
+ let reasonNoGPU = gpuEnabled ? "" : (gpuInfo.reason.isEmpty ? "Metal not used" : gpuInfo.reason)
452
+
453
+ // Return context info — reflect actual GPU state
454
+ let contextInfo: [String: Any] = [
455
+ "contextId": contextId,
456
+ "gpu": gpuEnabled,
457
+ "reasonNoGPU": reasonNoGPU,
458
+ "model": [
459
+ "desc": model.desc,
460
+ "size": model.size,
461
+ "nEmbd": model.nEmbd,
462
+ "nParams": model.nParams,
463
+ "chatTemplates": [
464
+ "llamaChat": model.chatTemplates.llamaChat,
465
+ "minja": [
466
+ "default": model.chatTemplates.minja.default,
467
+ "defaultCaps": [
468
+ "tools": model.chatTemplates.minja.defaultCaps.tools,
469
+ "toolCalls": model.chatTemplates.minja.defaultCaps.toolCalls,
470
+ "toolResponses": model.chatTemplates.minja.defaultCaps.toolResponses,
471
+ "systemRole": model.chatTemplates.minja.defaultCaps.systemRole,
472
+ "parallelToolCalls": model.chatTemplates.minja.defaultCaps.parallelToolCalls,
473
+ "toolCallId": model.chatTemplates.minja.defaultCaps.toolCallId
474
+ ],
475
+ "toolUse": model.chatTemplates.minja.toolUse,
476
+ "toolUseCaps": [
477
+ "tools": model.chatTemplates.minja.toolUseCaps.tools,
478
+ "toolCalls": model.chatTemplates.minja.toolUseCaps.toolCalls,
479
+ "toolResponses": model.chatTemplates.minja.toolUseCaps.toolResponses,
480
+ "systemRole": model.chatTemplates.minja.toolUseCaps.systemRole,
481
+ "parallelToolCalls": model.chatTemplates.minja.toolUseCaps.parallelToolCalls,
482
+ "toolCallId": model.chatTemplates.minja.toolUseCaps.toolCallId
483
+ ]
484
+ ]
485
+ ],
486
+ "metadata": model.metadata,
487
+ "isChatTemplateSupported": true
488
+ ]
489
+ ]
490
+
491
+ completion(.success(contextInfo))
492
+ }
493
+
494
+ func releaseContext(contextId: Int, completion: @escaping (LlamaResult<Void>) -> Void) {
495
+ guard contexts[contextId] != nil else {
496
+ completion(.failure(.contextNotFound))
497
+ return
498
+ }
499
+
500
+ let nativeId = contextIdToNative[contextId] ?? Int64(contextId)
501
+
502
+ LlamaNativeBridge.releaseContext(nativeId)
503
+
504
+ contexts.removeValue(forKey: contextId)
505
+ nativeContexts.removeValue(forKey: nativeId)
506
+ contextIdToNative.removeValue(forKey: contextId)
507
+ completion(.success(()))
508
+ }
509
+
510
+ func releaseAllContexts(completion: @escaping (LlamaResult<Void>) -> Void) {
511
+ let pairs = Array(contextIdToNative)
512
+ for (_, nativeId) in pairs {
513
+ LlamaNativeBridge.releaseContext(nativeId)
514
+ }
515
+ contexts.removeAll()
516
+ nativeContexts.removeAll()
517
+ contextIdToNative.removeAll()
518
+ completion(.success(()))
519
+ }
520
+
521
+ // MARK: - Chat and completion
522
+
523
+ func getFormattedChat(contextId: Int, messages: String, chatTemplate: String?, params: [String: Any]?, completion: @escaping (LlamaResult<[String: Any]>) -> Void) {
524
+ guard contexts[contextId] != nil else {
525
+ completion(.failure(.contextNotFound))
526
+ return
527
+ }
528
+ guard let nativeId = contextIdToNative[contextId] else {
529
+ completion(.failure(.contextNotFound))
530
+ return
531
+ }
532
+ if getFormattedChatFunc == nil { loadFunctionPointers() }
533
+ guard let fn = getFormattedChatFunc else {
534
+ completion(.failure(.operationFailed("llama_get_formatted_chat not found")))
535
+ return
536
+ }
537
+ var paramsJson = "{}"
538
+ if let p = params, let d = try? JSONSerialization.data(withJSONObject: p), let s = String(data: d, encoding: .utf8) {
539
+ paramsJson = s
540
+ }
541
+ let template = chatTemplate ?? ""
542
+ let result: LlamaResult<[String: Any]> = messages.withCString { msgPtr in
543
+ template.withCString { tplPtr in
544
+ paramsJson.withCString { parPtr in
545
+ guard let c = fn(nativeId, msgPtr, tplPtr, parPtr), let dict = jsonObject(fromCString: c) else {
546
+ return .failure(.operationFailed("formatted chat failed"))
547
+ }
548
+ if let err = dict["error"] as? String {
549
+ return .failure(.operationFailed(err))
550
+ }
551
+ return .success(dict)
552
+ }
553
+ }
554
+ }
555
+ completion(result)
556
+ }
557
+
558
+ func completion(contextId: Int, params: [String: Any], completion: @escaping (LlamaResult<[String: Any]>) -> Void) {
559
+ guard contexts[contextId] != nil else {
560
+ completion(.failure(.contextNotFound))
561
+ return
562
+ }
563
+
564
+ let nativeId = contextIdToNative[contextId] ?? Int64(contextId)
565
+ var paramsJson = "{}"
566
+ do {
567
+ let paramsData = try JSONSerialization.data(withJSONObject: params)
568
+ paramsJson = String(data: paramsData, encoding: .utf8) ?? "{}"
569
+ } catch {
570
+ completion(.failure(.operationFailed("Failed to serialize params: \(error.localizedDescription)")))
571
+ return
572
+ }
573
+
574
+ DispatchQueue.global(qos: .userInitiated).async {
575
+ do {
576
+ let completionResult = try LlamaNativeBridge.runCompletion(contextId: nativeId, paramsJson: paramsJson)
577
+ completion(.success(completionResult))
578
+ } catch let error as LlamaNativeBridge.Failure {
579
+ completion(.failure(.operationFailed(error.localizedDescription)))
580
+ } catch {
581
+ completion(.failure(.operationFailed("Native completion failed: \(error.localizedDescription)")))
582
+ }
583
+ }
584
+ }
585
+
586
+ func stopCompletion(contextId: Int, completion: @escaping (LlamaResult<Void>) -> Void) {
587
+ guard contexts[contextId] != nil else {
588
+ completion(.failure(.contextNotFound))
589
+ return
590
+ }
591
+
592
+ let nativeId = contextIdToNative[contextId] ?? Int64(contextId)
593
+ LlamaNativeBridge.stopCompletion(nativeId)
594
+ completion(.success(()))
595
+ }
596
+
597
+ // MARK: - Chat-first methods (like llama-cli -sys)
598
+
599
+ func chat(contextId: Int, messages: [JSObject], system: String?, chatTemplate: String?, params: [String: Any]?, completion: @escaping (LlamaResult<[String: Any]>) -> Void) {
600
+ guard contexts[contextId] != nil else {
601
+ completion(.failure(.contextNotFound))
602
+ return
603
+ }
604
+
605
+ do {
606
+ // Convert JSObject messages to JSON string
607
+ let messagesData = try JSONSerialization.data(withJSONObject: messages)
608
+ let messagesJson = String(data: messagesData, encoding: .utf8) ?? "[]"
609
+
610
+ // Add system message if provided
611
+ var allMessages = messages
612
+ if let system = system, !system.isEmpty {
613
+ let systemMessage: [String: Any] = [
614
+ "role": "system",
615
+ "content": system
616
+ ]
617
+ let jsSystem = JSTypes.coerceDictionaryToJSObject(systemMessage) ?? [:]
618
+ allMessages.insert(jsSystem, at: 0)
619
+ }
620
+
621
+ // Convert to JSON string for getFormattedChat
622
+ let allMessagesData = try JSONSerialization.data(withJSONObject: allMessages)
623
+ let allMessagesJson = String(data: allMessagesData, encoding: .utf8) ?? "[]"
624
+
625
+ // First, format the chat
626
+ getFormattedChat(contextId: contextId, messages: allMessagesJson, chatTemplate: chatTemplate, params: nil) { [weak self] result in
627
+ switch result {
628
+ case .success(let formattedResult):
629
+ // Extract the formatted prompt
630
+ let formattedPrompt = formattedResult["prompt"] as? String ?? ""
631
+
632
+ // Create completion parameters
633
+ var completionParams = params ?? [:]
634
+ completionParams["prompt"] = formattedPrompt
635
+
636
+ // Call completion with formatted prompt
637
+ self?.completion(contextId: contextId, params: completionParams, completion: completion)
638
+
639
+ case .failure(let error):
640
+ completion(.failure(error))
641
+ }
642
+ }
643
+
644
+ } catch {
645
+ completion(.failure(.contextNotFound)) // Use a more appropriate error
646
+ }
647
+ }
648
+
649
+ func chatWithSystem(contextId: Int, system: String, message: String, params: [String: Any]?, completion: @escaping (LlamaResult<[String: Any]>) -> Void) {
650
+ // Create a simple message array
651
+ let userMessage: [String: Any] = [
652
+ "role": "user",
653
+ "content": message
654
+ ]
655
+ let jsUser = JSTypes.coerceDictionaryToJSObject(userMessage) ?? [:]
656
+ let messages: [JSObject] = [jsUser]
657
+
658
+ // Call the main chat method
659
+ chat(contextId: contextId, messages: messages, system: system, chatTemplate: nil, params: params, completion: completion)
660
+ }
661
+
662
+ func generateText(contextId: Int, prompt: String, params: [String: Any]?, completion: @escaping (LlamaResult<[String: Any]>) -> Void) {
663
+ guard contexts[contextId] != nil else {
664
+ completion(.failure(.contextNotFound))
665
+ return
666
+ }
667
+
668
+ // Create completion parameters
669
+ var completionParams = params ?? [:]
670
+ completionParams["prompt"] = prompt
671
+
672
+ // Call completion method directly
673
+ self.completion(contextId: contextId, params: completionParams, completion: completion)
674
+ }
675
+
676
+ // MARK: - Session management
677
+
678
+ func loadSession(contextId: Int, filepath: String, completion: @escaping (LlamaResult<[String: Any]>) -> Void) {
679
+ guard contexts[contextId] != nil else {
680
+ completion(.failure(.contextNotFound))
681
+ return
682
+ }
683
+ guard let nativeId = contextIdToNative[contextId] else {
684
+ completion(.failure(.contextNotFound))
685
+ return
686
+ }
687
+ DispatchQueue.global(qos: .userInitiated).async {
688
+ do {
689
+ let result = try LlamaNativeBridge.loadSession(contextId: nativeId, filepath: filepath)
690
+ completion(.success(result))
691
+ } catch let err as LlamaNativeBridge.Failure {
692
+ completion(.failure(.operationFailed(err.localizedDescription)))
693
+ } catch {
694
+ completion(.failure(.operationFailed("loadSession failed: \(error.localizedDescription)")))
695
+ }
696
+ }
697
+ }
698
+
699
+ func saveSession(contextId: Int, filepath: String, size: Int, completion: @escaping (LlamaResult<Int>) -> Void) {
700
+ guard contexts[contextId] != nil else {
701
+ completion(.failure(.contextNotFound))
702
+ return
703
+ }
704
+ guard let nativeId = contextIdToNative[contextId] else {
705
+ completion(.failure(.contextNotFound))
706
+ return
707
+ }
708
+ DispatchQueue.global(qos: .userInitiated).async {
709
+ do {
710
+ let saved = try LlamaNativeBridge.saveSession(contextId: nativeId, filepath: filepath, size: size)
711
+ completion(.success(saved))
712
+ } catch let err as LlamaNativeBridge.Failure {
713
+ completion(.failure(.operationFailed(err.localizedDescription)))
714
+ } catch {
715
+ completion(.failure(.operationFailed("saveSession failed: \(error.localizedDescription)")))
716
+ }
717
+ }
718
+ }
719
+
720
+ // MARK: - Tokenization
721
+
722
+ func tokenize(contextId: Int, text: String, imagePaths: [String], completion: @escaping (LlamaResult<[String: Any]>) -> Void) {
723
+ guard contexts[contextId] != nil else {
724
+ completion(.failure(.contextNotFound))
725
+ return
726
+ }
727
+ guard let nativeId = contextIdToNative[contextId] else {
728
+ completion(.failure(.contextNotFound))
729
+ return
730
+ }
731
+ if tokenizeFunc == nil { loadFunctionPointers() }
732
+ guard let fn = tokenizeFunc else {
733
+ completion(.failure(.operationFailed("llama_cap_tokenize not found")))
734
+ return
735
+ }
736
+ var pathsJson = "[]"
737
+ if !imagePaths.isEmpty, let d = try? JSONSerialization.data(withJSONObject: imagePaths), let s = String(data: d, encoding: .utf8) {
738
+ pathsJson = s
739
+ }
740
+ let result: LlamaResult<[String: Any]> = text.withCString { txtPtr in
741
+ pathsJson.withCString { pj in
742
+ guard let c = fn(nativeId, txtPtr, pj), let dict = jsonObject(fromCString: c) else {
743
+ return .failure(.operationFailed("tokenize failed"))
744
+ }
745
+ if let err = dict["error"] as? String {
746
+ return .failure(.operationFailed(err))
747
+ }
748
+ return .success(dict)
749
+ }
750
+ }
751
+ completion(result)
752
+ }
753
+
754
+ func detokenize(contextId: Int, tokens: [Int], completion: @escaping (LlamaResult<String>) -> Void) {
755
+ guard contexts[contextId] != nil else {
756
+ completion(.failure(.contextNotFound))
757
+ return
758
+ }
759
+ guard let nativeId = contextIdToNative[contextId] else {
760
+ completion(.failure(.contextNotFound))
761
+ return
762
+ }
763
+ if detokenizeFunc == nil { loadFunctionPointers() }
764
+ guard let fn = detokenizeFunc else {
765
+ completion(.failure(.operationFailed("llama_cap_detokenize not found")))
766
+ return
767
+ }
768
+ guard let d = try? JSONSerialization.data(withJSONObject: tokens), let tokensJson = String(data: d, encoding: .utf8) else {
769
+ completion(.failure(.invalidParameters))
770
+ return
771
+ }
772
+ let s: LlamaResult<String> = tokensJson.withCString { ptr in
773
+ guard let c = fn(nativeId, ptr) else {
774
+ return .success("")
775
+ }
776
+ return .success(String(cString: c))
777
+ }
778
+ completion(s)
779
+ }
780
+
781
+ // MARK: - Embeddings and reranking
782
+
783
+ func embedding(contextId: Int, text: String, params: [String: Any], completion: @escaping (LlamaResult<[String: Any]>) -> Void) {
784
+ guard contexts[contextId] != nil else {
785
+ completion(.failure(.contextNotFound))
786
+ return
787
+ }
788
+
789
+ let nativeId = contextIdToNative[contextId] ?? Int64(contextId)
790
+ var paramsJson = "{}"
791
+ if !params.isEmpty {
792
+ do {
793
+ let paramsData = try JSONSerialization.data(withJSONObject: params)
794
+ paramsJson = String(data: paramsData, encoding: .utf8) ?? "{}"
795
+ } catch {
796
+ completion(.failure(.operationFailed("Failed to serialize params: \(error.localizedDescription)")))
797
+ return
798
+ }
799
+ }
800
+
801
+ DispatchQueue.global(qos: .userInitiated).async {
802
+ do {
803
+ let embeddingResult = try LlamaNativeBridge.runEmbedding(contextId: nativeId, text: text, paramsJson: paramsJson)
804
+ completion(.success(embeddingResult))
805
+ } catch let error as LlamaNativeBridge.Failure {
806
+ completion(.failure(.operationFailed(error.localizedDescription)))
807
+ } catch {
808
+ completion(.failure(.operationFailed("Native embedding failed: \(error.localizedDescription)")))
809
+ }
810
+ }
811
+ }
812
+
813
+ func rerank(contextId: Int, query: String, documents: [String], params: [String: Any]?, completion: @escaping (LlamaResult<[[String: Any]]>) -> Void) {
814
+ guard contexts[contextId] != nil else {
815
+ completion(.failure(.contextNotFound))
816
+ return
817
+ }
818
+ guard let nativeId = contextIdToNative[contextId] else {
819
+ completion(.failure(.contextNotFound))
820
+ return
821
+ }
822
+ DispatchQueue.global(qos: .userInitiated).async {
823
+ do {
824
+ guard let docsData = try? JSONSerialization.data(withJSONObject: documents),
825
+ let docsJson = String(data: docsData, encoding: .utf8) else {
826
+ completion(.failure(.operationFailed("Failed to serialize documents")))
827
+ return
828
+ }
829
+ let results = try LlamaNativeBridge.runRerank(contextId: nativeId, query: query, documentsJson: docsJson)
830
+ completion(.success(results))
831
+ } catch let err as LlamaNativeBridge.Failure {
832
+ completion(.failure(.operationFailed(err.localizedDescription)))
833
+ } catch {
834
+ completion(.failure(.operationFailed("rerank failed: \(error.localizedDescription)")))
835
+ }
836
+ }
837
+ }
838
+
839
+ func bench(contextId: Int, pp: Int, tg: Int, pl: Int, nr: Int, completion: @escaping (LlamaResult<String>) -> Void) {
840
+ guard contexts[contextId] != nil else {
841
+ completion(.failure(.contextNotFound))
842
+ return
843
+ }
844
+ guard let nativeId = contextIdToNative[contextId] else {
845
+ completion(.failure(.contextNotFound))
846
+ return
847
+ }
848
+ DispatchQueue.global(qos: .userInitiated).async {
849
+ do {
850
+ let result = try LlamaNativeBridge.runBench(contextId: nativeId, pp: pp, tg: tg, pl: pl, nr: nr)
851
+ completion(.success(result))
852
+ } catch let err as LlamaNativeBridge.Failure {
853
+ completion(.failure(.operationFailed(err.localizedDescription)))
854
+ } catch {
855
+ completion(.failure(.operationFailed("bench failed: \(error.localizedDescription)")))
856
+ }
857
+ }
858
+ }
859
+
860
+ // MARK: - LoRA adapters
861
+
862
+ func applyLoraAdapters(contextId: Int, loraAdapters: [[String: Any]], completion: @escaping (LlamaResult<Void>) -> Void) {
863
+ guard contexts[contextId] != nil else {
864
+ completion(.failure(.contextNotFound))
865
+ return
866
+ }
867
+ guard let nativeId = contextIdToNative[contextId] else {
868
+ completion(.failure(.contextNotFound))
869
+ return
870
+ }
871
+ do {
872
+ let data = try JSONSerialization.data(withJSONObject: loraAdapters)
873
+ let json = String(data: data, encoding: .utf8) ?? "[]"
874
+ DispatchQueue.global(qos: .userInitiated).async {
875
+ do {
876
+ _ = try LlamaNativeBridge.applyLoraAdapters(contextId: nativeId, loraAdaptersJson: json)
877
+ completion(.success(()))
878
+ } catch let err as LlamaNativeBridge.Failure {
879
+ completion(.failure(.operationFailed(err.localizedDescription)))
880
+ } catch {
881
+ completion(.failure(.operationFailed("applyLoraAdapters failed: \(error.localizedDescription)")))
882
+ }
883
+ }
884
+ } catch {
885
+ completion(.failure(.operationFailed("Failed to serialize LoRA adapters: \(error.localizedDescription)")))
886
+ }
887
+ }
888
+
889
+ func removeLoraAdapters(contextId: Int, completion: @escaping (LlamaResult<Void>) -> Void) {
890
+ guard contexts[contextId] != nil else {
891
+ completion(.failure(.contextNotFound))
892
+ return
893
+ }
894
+ guard let nativeId = contextIdToNative[contextId] else {
895
+ completion(.failure(.contextNotFound))
896
+ return
897
+ }
898
+ DispatchQueue.global(qos: .userInitiated).async {
899
+ do {
900
+ try LlamaNativeBridge.removeLoraAdapters(contextId: nativeId)
901
+ completion(.success(()))
902
+ } catch let err as LlamaNativeBridge.Failure {
903
+ completion(.failure(.operationFailed(err.localizedDescription)))
904
+ } catch {
905
+ completion(.failure(.operationFailed("removeLoraAdapters failed: \(error.localizedDescription)")))
906
+ }
907
+ }
908
+ }
909
+
910
+ func getLoadedLoraAdapters(contextId: Int, completion: @escaping (LlamaResult<[[String: Any]]>) -> Void) {
911
+ guard contexts[contextId] != nil else {
912
+ completion(.failure(.contextNotFound))
913
+ return
914
+ }
915
+ guard let nativeId = contextIdToNative[contextId] else {
916
+ completion(.failure(.contextNotFound))
917
+ return
918
+ }
919
+ DispatchQueue.global(qos: .userInitiated).async {
920
+ do {
921
+ let adapters = try LlamaNativeBridge.getLoadedLoraAdapters(contextId: nativeId)
922
+ completion(.success(adapters))
923
+ } catch let err as LlamaNativeBridge.Failure {
924
+ completion(.failure(.operationFailed(err.localizedDescription)))
925
+ } catch {
926
+ completion(.failure(.operationFailed("getLoadedLoraAdapters failed: \(error.localizedDescription)")))
927
+ }
928
+ }
929
+ }
930
+
931
+ // MARK: - Multimodal methods
932
+
933
+ func initMultimodal(contextId: Int, path: String, useGpu: Bool, completion: @escaping (LlamaResult<Bool>) -> Void) {
934
+ guard let context = contexts[contextId] else {
935
+ completion(.failure(.contextNotFound))
936
+ return
937
+ }
938
+ guard let nativeId = contextIdToNative[contextId] else {
939
+ completion(.failure(.contextNotFound))
940
+ return
941
+ }
942
+ DispatchQueue.global(qos: .userInitiated).async {
943
+ do {
944
+ let ok = try LlamaNativeBridge.initMultimodal(contextId: nativeId, mmProjPath: path, useGpu: useGpu)
945
+ if ok { context.isMultimodalEnabled = true }
946
+ completion(.success(ok))
947
+ } catch let err as LlamaNativeBridge.Failure {
948
+ completion(.failure(.operationFailed(err.localizedDescription)))
949
+ } catch {
950
+ completion(.failure(.operationFailed("initMultimodal failed: \(error.localizedDescription)")))
951
+ }
952
+ }
953
+ }
954
+
955
+ func isMultimodalEnabled(contextId: Int, completion: @escaping (LlamaResult<Bool>) -> Void) {
956
+ guard let context = contexts[contextId] else {
957
+ completion(.failure(.contextNotFound))
958
+ return
959
+ }
960
+ guard let nativeId = contextIdToNative[contextId] else {
961
+ completion(.success(context.isMultimodalEnabled))
962
+ return
963
+ }
964
+ let enabled = LlamaNativeBridge.isMultimodalEnabled(contextId: nativeId)
965
+ context.isMultimodalEnabled = enabled
966
+ completion(.success(enabled))
967
+ }
968
+
969
+ func getMultimodalSupport(contextId: Int, completion: @escaping (LlamaResult<[String: Any]>) -> Void) {
970
+ guard contexts[contextId] != nil else {
971
+ completion(.failure(.contextNotFound))
972
+ return
973
+ }
974
+ guard let nativeId = contextIdToNative[contextId] else {
975
+ completion(.success(["vision": false, "audio": false]))
976
+ return
977
+ }
978
+ DispatchQueue.global(qos: .userInitiated).async {
979
+ do {
980
+ let support = try LlamaNativeBridge.getMultimodalSupport(contextId: nativeId)
981
+ completion(.success(support))
982
+ } catch {
983
+ completion(.success(["vision": false, "audio": false]))
984
+ }
985
+ }
986
+ }
987
+
988
+ func releaseMultimodal(contextId: Int, completion: @escaping (LlamaResult<Void>) -> Void) {
989
+ guard let context = contexts[contextId] else {
990
+ completion(.failure(.contextNotFound))
991
+ return
992
+ }
993
+ guard let nativeId = contextIdToNative[contextId] else {
994
+ context.isMultimodalEnabled = false
995
+ completion(.success(()))
996
+ return
997
+ }
998
+ LlamaNativeBridge.releaseMultimodal(contextId: nativeId)
999
+ context.isMultimodalEnabled = false
1000
+ completion(.success(()))
1001
+ }
1002
+
1003
+ // MARK: - TTS methods
1004
+
1005
+ func initVocoder(contextId: Int, path: String, nBatch: Int?, completion: @escaping (LlamaResult<Bool>) -> Void) {
1006
+ guard let context = contexts[contextId] else {
1007
+ completion(.failure(.contextNotFound))
1008
+ return
1009
+ }
1010
+ guard let nativeId = contextIdToNative[contextId] else {
1011
+ completion(.failure(.contextNotFound))
1012
+ return
1013
+ }
1014
+ let batchSize = nBatch ?? 512
1015
+ DispatchQueue.global(qos: .userInitiated).async {
1016
+ do {
1017
+ let ok = try LlamaNativeBridge.initVocoder(contextId: nativeId, path: path, nBatch: batchSize)
1018
+ if ok { context.isVocoderEnabled = true }
1019
+ completion(.success(ok))
1020
+ } catch let err as LlamaNativeBridge.Failure {
1021
+ completion(.failure(.operationFailed(err.localizedDescription)))
1022
+ } catch {
1023
+ completion(.failure(.operationFailed("initVocoder failed: \(error.localizedDescription)")))
1024
+ }
1025
+ }
1026
+ }
1027
+
1028
+ func isVocoderEnabled(contextId: Int, completion: @escaping (LlamaResult<Bool>) -> Void) {
1029
+ guard let context = contexts[contextId] else {
1030
+ completion(.failure(.contextNotFound))
1031
+ return
1032
+ }
1033
+ guard let nativeId = contextIdToNative[contextId] else {
1034
+ completion(.success(context.isVocoderEnabled))
1035
+ return
1036
+ }
1037
+ let enabled = LlamaNativeBridge.isVocoderEnabled(contextId: nativeId)
1038
+ context.isVocoderEnabled = enabled
1039
+ completion(.success(enabled))
1040
+ }
1041
+
1042
+ func getFormattedAudioCompletion(contextId: Int, speakerJsonStr: String, textToSpeak: String, completion: @escaping (LlamaResult<[String: Any]>) -> Void) {
1043
+ guard contexts[contextId] != nil else {
1044
+ completion(.failure(.contextNotFound))
1045
+ return
1046
+ }
1047
+ guard let nativeId = contextIdToNative[contextId] else {
1048
+ completion(.failure(.contextNotFound))
1049
+ return
1050
+ }
1051
+ DispatchQueue.global(qos: .userInitiated).async {
1052
+ do {
1053
+ let result = try LlamaNativeBridge.getFormattedAudioCompletion(
1054
+ contextId: nativeId, speakerJson: speakerJsonStr, text: textToSpeak)
1055
+ completion(.success(result))
1056
+ } catch let err as LlamaNativeBridge.Failure {
1057
+ completion(.failure(.operationFailed(err.localizedDescription)))
1058
+ } catch {
1059
+ completion(.failure(.operationFailed("getFormattedAudioCompletion failed: \(error.localizedDescription)")))
1060
+ }
1061
+ }
1062
+ }
1063
+
1064
+ func getAudioCompletionGuideTokens(contextId: Int, textToSpeak: String, completion: @escaping (LlamaResult<[Int]>) -> Void) {
1065
+ guard contexts[contextId] != nil else {
1066
+ completion(.failure(.contextNotFound))
1067
+ return
1068
+ }
1069
+ guard let nativeId = contextIdToNative[contextId] else {
1070
+ completion(.failure(.contextNotFound))
1071
+ return
1072
+ }
1073
+ DispatchQueue.global(qos: .userInitiated).async {
1074
+ do {
1075
+ let tokens = try LlamaNativeBridge.getAudioCompletionGuideTokens(contextId: nativeId, text: textToSpeak)
1076
+ completion(.success(tokens))
1077
+ } catch let err as LlamaNativeBridge.Failure {
1078
+ completion(.failure(.operationFailed(err.localizedDescription)))
1079
+ } catch {
1080
+ completion(.failure(.operationFailed("getAudioCompletionGuideTokens failed: \(error.localizedDescription)")))
1081
+ }
1082
+ }
1083
+ }
1084
+
1085
+ func decodeAudioTokens(contextId: Int, tokens: [Int], completion: @escaping (LlamaResult<[Int]>) -> Void) {
1086
+ guard contexts[contextId] != nil else {
1087
+ completion(.failure(.contextNotFound))
1088
+ return
1089
+ }
1090
+ guard let nativeId = contextIdToNative[contextId] else {
1091
+ completion(.failure(.contextNotFound))
1092
+ return
1093
+ }
1094
+ do {
1095
+ let data = try JSONSerialization.data(withJSONObject: tokens)
1096
+ let json = String(data: data, encoding: .utf8) ?? "[]"
1097
+ DispatchQueue.global(qos: .userInitiated).async {
1098
+ do {
1099
+ let floatSamples = try LlamaNativeBridge.decodeAudioTokens(contextId: nativeId, tokensJson: json)
1100
+ // Convert float PCM [-1,1] to Int16 range for interop
1101
+ let intSamples = floatSamples.map { Int(($0 * 32767.0).clamped(to: -32768...32767)) }
1102
+ completion(.success(intSamples))
1103
+ } catch let err as LlamaNativeBridge.Failure {
1104
+ completion(.failure(.operationFailed(err.localizedDescription)))
1105
+ } catch {
1106
+ completion(.failure(.operationFailed("decodeAudioTokens failed: \(error.localizedDescription)")))
1107
+ }
1108
+ }
1109
+ } catch {
1110
+ completion(.failure(.operationFailed("Failed to serialize tokens: \(error.localizedDescription)")))
1111
+ }
1112
+ }
1113
+
1114
+ func releaseVocoder(contextId: Int, completion: @escaping (LlamaResult<Void>) -> Void) {
1115
+ guard let context = contexts[contextId] else {
1116
+ completion(.failure(.contextNotFound))
1117
+ return
1118
+ }
1119
+ guard let nativeId = contextIdToNative[contextId] else {
1120
+ context.isVocoderEnabled = false
1121
+ completion(.success(()))
1122
+ return
1123
+ }
1124
+ LlamaNativeBridge.releaseVocoder(contextId: nativeId)
1125
+ context.isVocoderEnabled = false
1126
+ completion(.success(()))
1127
+ }
1128
+
1129
+ // MARK: - Model download and management
1130
+ //
1131
+ // Active downloads keyed by URL string. URLSession-based to support real progress
1132
+ // and cancellation — replaces the old blocking Data(contentsOf:) implementation.
1133
+
1134
+ private var activeDownloads: [String: ActiveDownload] = [:]
1135
+
1136
+ private class ActiveDownload: NSObject, URLSessionDownloadDelegate {
1137
+ let url: String
1138
+ let localPath: String
1139
+ var downloadedBytes: Int64 = 0
1140
+ var totalBytes: Int64 = 0
1141
+ var completed: Bool = false
1142
+ var failed: Bool = false
1143
+ var errorMessage: String?
1144
+ var session: URLSession?
1145
+ var task: URLSessionDownloadTask?
1146
+ var onProgress: ((Int64, Int64) -> Void)?
1147
+ var onDone: ((String?) -> Void)?
1148
+
1149
+ init(url: String, localPath: String) {
1150
+ self.url = url
1151
+ self.localPath = localPath
1152
+ }
1153
+
1154
+ func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask,
1155
+ didWriteData bytesWritten: Int64,
1156
+ totalBytesWritten: Int64, totalBytesExpectedToWrite: Int64) {
1157
+ downloadedBytes = totalBytesWritten
1158
+ totalBytes = totalBytesExpectedToWrite
1159
+ onProgress?(totalBytesWritten, totalBytesExpectedToWrite)
1160
+ }
1161
+
1162
+ func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask,
1163
+ didFinishDownloadingTo location: URL) {
1164
+ do {
1165
+ let dest = URL(fileURLWithPath: localPath)
1166
+ if FileManager.default.fileExists(atPath: localPath) {
1167
+ try FileManager.default.removeItem(at: dest)
1168
+ }
1169
+ try FileManager.default.moveItem(at: location, to: dest)
1170
+ completed = true
1171
+ onDone?(nil)
1172
+ } catch {
1173
+ failed = true
1174
+ errorMessage = error.localizedDescription
1175
+ onDone?(error.localizedDescription)
1176
+ }
1177
+ }
1178
+
1179
+ func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) {
1180
+ if let error = error, !completed {
1181
+ failed = true
1182
+ errorMessage = error.localizedDescription
1183
+ onDone?(error.localizedDescription)
1184
+ }
1185
+ }
1186
+ }
1187
+
1188
+ func downloadModel(url: String, filename: String, completion: @escaping (LlamaResult<String>) -> Void) {
1189
+ guard let documentsDir = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first else {
1190
+ completion(.failure(.operationFailed("Cannot access documents directory")))
1191
+ return
1192
+ }
1193
+ let localPath = documentsDir.appendingPathComponent(filename).path
1194
+ // Return cached file immediately
1195
+ if FileManager.default.fileExists(atPath: localPath) {
1196
+ completion(.success(localPath))
1197
+ return
1198
+ }
1199
+ guard let downloadURL = URL(string: url) else {
1200
+ completion(.failure(.operationFailed("Invalid URL: \(url)")))
1201
+ return
1202
+ }
1203
+ let dl = ActiveDownload(url: url, localPath: localPath)
1204
+ let config = URLSessionConfiguration.default
1205
+ let session = URLSession(configuration: config, delegate: dl, delegateQueue: nil)
1206
+ dl.session = session
1207
+ dl.onDone = { [weak self] errorMsg in
1208
+ self?.activeDownloads.removeValue(forKey: url)
1209
+ if let msg = errorMsg {
1210
+ // Don't call completion again; the path was already returned
1211
+ print("[LlamaCpp] Download failed for \(url): \(msg)")
1212
+ }
1213
+ }
1214
+ activeDownloads[url] = dl
1215
+ let task = session.downloadTask(with: downloadURL)
1216
+ dl.task = task
1217
+ task.resume()
1218
+ // Return the path immediately; caller polls getDownloadProgress
1219
+ completion(.success(localPath))
1220
+ }
1221
+
1222
+ func getDownloadProgress(url: String, completion: @escaping (LlamaResult<[String: Any]>) -> Void) {
1223
+ guard let dl = activeDownloads[url] else {
1224
+ // Check if the file exists (completed in a previous session)
1225
+ let result: [String: Any] = [
1226
+ "progress": 0, "completed": false, "failed": false,
1227
+ "downloadedBytes": 0, "totalBytes": 0
1228
+ ]
1229
+ completion(.success(result))
1230
+ return
1231
+ }
1232
+ let progress: Double = dl.totalBytes > 0 ? Double(dl.downloadedBytes) / Double(dl.totalBytes) : 0
1233
+ let result: [String: Any] = [
1234
+ "progress": progress,
1235
+ "completed": dl.completed,
1236
+ "failed": dl.failed,
1237
+ "errorMessage": dl.errorMessage ?? "",
1238
+ "localPath": dl.localPath,
1239
+ "downloadedBytes": dl.downloadedBytes,
1240
+ "totalBytes": dl.totalBytes
1241
+ ]
1242
+ completion(.success(result))
1243
+ }
1244
+
1245
+ func cancelDownload(url: String, completion: @escaping (LlamaResult<Bool>) -> Void) {
1246
+ guard let dl = activeDownloads[url] else {
1247
+ completion(.success(false))
1248
+ return
1249
+ }
1250
+ dl.task?.cancel()
1251
+ dl.session?.invalidateAndCancel()
1252
+ activeDownloads.removeValue(forKey: url)
1253
+ completion(.success(true))
1254
+ }
1255
+
1256
+ func getAvailableModels(completion: @escaping (LlamaResult<[[String: Any]]>) -> Void) {
1257
+ let fileManager = FileManager.default
1258
+ var models: [[String: Any]] = []
1259
+
1260
+ // Search common model directories
1261
+ let searchPaths = [
1262
+ fileManager.urls(for: .documentDirectory, in: .userDomainMask).first,
1263
+ fileManager.urls(for: .downloadsDirectory, in: .userDomainMask).first
1264
+ ].compactMap { $0 }
1265
+
1266
+ for searchPath in searchPaths {
1267
+ do {
1268
+ let files = try fileManager.contentsOfDirectory(at: searchPath, includingPropertiesForKeys: [.fileSizeKey, .contentModificationDateKey])
1269
+
1270
+ for file in files {
1271
+ let pathExtension = file.pathExtension.lowercased()
1272
+ // Check for common model file extensions
1273
+ if pathExtension == "gguf" || pathExtension == "ggml" || pathExtension == "bin" {
1274
+ let attributes = try fileManager.attributesOfItem(atPath: file.path)
1275
+ let fileSize = attributes[.size] as? Int64 ?? 0
1276
+
1277
+ let model: [String: Any] = [
1278
+ "id": file.lastPathComponent,
1279
+ "name": file.lastPathComponent,
1280
+ "path": file.path,
1281
+ "size": fileSize,
1282
+ "sizeMB": Double(fileSize) / (1024 * 1024),
1283
+ "status": "available"
1284
+ ]
1285
+ models.append(model)
1286
+ }
1287
+ }
1288
+ } catch {
1289
+ // Continue searching other paths
1290
+ continue
1291
+ }
1292
+ }
1293
+
1294
+ completion(.success(models))
1295
+ }
1296
+
1297
+ // MARK: - Native in-process HTTP server (127.0.0.1 — use App Transport Security allowances for localhost in the host app)
1298
+
1299
+ func startNativeLlamaServer(modelPath: String, host: String?, port: Int, params: [String: Any], completion: @escaping (LlamaResult<[String: Any]>) -> Void) {
1300
+ if capServerStartFunc == nil { loadFunctionPointers() }
1301
+ guard let start = capServerStartFunc else {
1302
+ completion(.failure(.operationFailed("cap_llama_server_start not available")))
1303
+ return
1304
+ }
1305
+ var paramsJson = "{}"
1306
+ do {
1307
+ let data = try JSONSerialization.data(withJSONObject: params)
1308
+ paramsJson = String(data: data, encoding: .utf8) ?? "{}"
1309
+ } catch {
1310
+ completion(.failure(.operationFailed("params JSON: \(error.localizedDescription)")))
1311
+ return
1312
+ }
1313
+ let h = (host != nil && !host!.isEmpty) ? host! : "127.0.0.1"
1314
+ let rc = modelPath.withCString { mp in
1315
+ h.withCString { hp in
1316
+ paramsJson.withCString { pj in
1317
+ start(mp, hp, Int32(port), pj)
1318
+ }
1319
+ }
1320
+ }
1321
+ if rc != 0 {
1322
+ completion(.success(["running": true]))
1323
+ } else {
1324
+ completion(.failure(.operationFailed("startNativeLlamaServer failed")))
1325
+ }
1326
+ }
1327
+
1328
+ func stopNativeLlamaServer(completion: @escaping (LlamaResult<Void>) -> Void) {
1329
+ if capServerStopFunc == nil { loadFunctionPointers() }
1330
+ guard let stop = capServerStopFunc else {
1331
+ completion(.failure(.operationFailed("cap_llama_server_stop not available")))
1332
+ return
1333
+ }
1334
+ stop()
1335
+ completion(.success(()))
1336
+ }
1337
+
1338
+ func isNativeLlamaServerRunning(completion: @escaping (LlamaResult<[String: Any]>) -> Void) {
1339
+ if capServerIsRunningFunc == nil { loadFunctionPointers() }
1340
+ guard let fn = capServerIsRunningFunc else {
1341
+ completion(.failure(.operationFailed("cap_llama_server_is_running not available")))
1342
+ return
1343
+ }
1344
+ let running = fn() != 0
1345
+ completion(.success(["running": running]))
1346
+ }
1347
+
1348
+ // MARK: - Grammar utilities
1349
+
1350
+ func convertJsonSchemaToGrammar(schema: String, completion: @escaping (LlamaResult<String>) -> Void) {
1351
+ if grammarFunc == nil { loadFunctionPointers() }
1352
+ guard let fn = grammarFunc else {
1353
+ completion(.success(schema))
1354
+ return
1355
+ }
1356
+ let s: LlamaResult<String> = schema.withCString { ptr in
1357
+ guard let c = fn(ptr) else {
1358
+ return .success("")
1359
+ }
1360
+ let g = String(cString: c)
1361
+ return g.isEmpty ? .success(schema) : .success(g)
1362
+ }
1363
+ completion(s)
1364
+ }
1365
+ }