@elizaos/capacitor-bun-runtime 2.0.3-beta.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.
- package/ElizaosCapacitorBunRuntime.podspec +54 -0
- package/LICENSE +21 -0
- package/README.md +127 -0
- package/dist/esm/definitions.d.ts +136 -0
- package/dist/esm/definitions.d.ts.map +1 -0
- package/dist/esm/definitions.js +14 -0
- package/dist/esm/definitions.js.map +1 -0
- package/dist/esm/index.d.ts +9 -0
- package/dist/esm/index.d.ts.map +1 -0
- package/dist/esm/index.js +11 -0
- package/dist/esm/index.js.map +1 -0
- package/dist/esm/web.d.ts +19 -0
- package/dist/esm/web.d.ts.map +1 -0
- package/dist/esm/web.js +44 -0
- package/dist/esm/web.js.map +1 -0
- package/dist/plugin.cjs.js +63 -0
- package/dist/plugin.cjs.js.map +1 -0
- package/dist/plugin.js +66 -0
- package/dist/plugin.js.map +1 -0
- package/ios/Sources/ElizaBunRuntimePlugin/BridgeInstaller.swift +94 -0
- package/ios/Sources/ElizaBunRuntimePlugin/ElizaBunRuntime.swift +705 -0
- package/ios/Sources/ElizaBunRuntimePlugin/ElizaBunRuntimePlugin.swift +1109 -0
- package/ios/Sources/ElizaBunRuntimePlugin/FullBunEngineHost.swift +677 -0
- package/ios/Sources/ElizaBunRuntimePlugin/JSContextHelpers.swift +226 -0
- package/ios/Sources/ElizaBunRuntimePlugin/SandboxPaths.swift +46 -0
- package/ios/Sources/ElizaBunRuntimePlugin/bridge/CryptoBridge.swift +238 -0
- package/ios/Sources/ElizaBunRuntimePlugin/bridge/ElizaSqliteVecBridge.m +28 -0
- package/ios/Sources/ElizaBunRuntimePlugin/bridge/FSBridge.swift +270 -0
- package/ios/Sources/ElizaBunRuntimePlugin/bridge/HTTPBridge.swift +153 -0
- package/ios/Sources/ElizaBunRuntimePlugin/bridge/HTTPServerBridge.swift +32 -0
- package/ios/Sources/ElizaBunRuntimePlugin/bridge/LlamaBridge.swift +233 -0
- package/ios/Sources/ElizaBunRuntimePlugin/bridge/LlamaBridgeImpl.swift +1863 -0
- package/ios/Sources/ElizaBunRuntimePlugin/bridge/LogBridge.swift +36 -0
- package/ios/Sources/ElizaBunRuntimePlugin/bridge/PathsBridge.swift +41 -0
- package/ios/Sources/ElizaBunRuntimePlugin/bridge/ProcessBridge.swift +80 -0
- package/ios/Sources/ElizaBunRuntimePlugin/bridge/SqliteBridge.swift +406 -0
- package/ios/Sources/ElizaBunRuntimePlugin/bridge/SqliteBridgeInstaller.swift +17 -0
- package/ios/Sources/ElizaBunRuntimePlugin/bridge/SqliteVecLoader.swift +66 -0
- package/ios/Sources/ElizaBunRuntimePlugin/bridge/UIBridge.swift +72 -0
- package/ios/Sources/ElizaBunRuntimePlugin/kokoro/KokoroCoreMlChinesePhonemizer.swift +313 -0
- package/ios/Sources/ElizaBunRuntimePlugin/kokoro/KokoroCoreMlConfiguration.swift +28 -0
- package/ios/Sources/ElizaBunRuntimePlugin/kokoro/KokoroCoreMlEngine.swift +325 -0
- package/ios/Sources/ElizaBunRuntimePlugin/kokoro/KokoroCoreMlHindiPhonemizer.swift +150 -0
- package/ios/Sources/ElizaBunRuntimePlugin/kokoro/KokoroCoreMlJapanesePhonemizer.swift +209 -0
- package/ios/Sources/ElizaBunRuntimePlugin/kokoro/KokoroCoreMlLatinPhonemizer.swift +374 -0
- package/ios/Sources/ElizaBunRuntimePlugin/kokoro/KokoroCoreMlModel.swift +87 -0
- package/ios/Sources/ElizaBunRuntimePlugin/kokoro/KokoroCoreMlPhonemizer.swift +679 -0
- package/ios/Sources/ElizaBunRuntimePlugin/kokoro/KokoroCoreMlPronunciationDicts.swift +131 -0
- package/ios/Sources/ElizaBunRuntimePlugin/kokoro/KokoroCoreMlSupport.swift +24 -0
- package/ios/Tests/llama-bridge-smoke-main.swift +92 -0
- package/package.json +68 -0
- package/src/bridge-contract.test.ts +127 -0
- package/src/definitions.d.ts +136 -0
- package/src/definitions.d.ts.map +1 -0
- package/src/definitions.ts +152 -0
- package/src/index.d.ts +9 -0
- package/src/index.d.ts.map +1 -0
- package/src/index.ts +16 -0
- package/src/web.d.ts +19 -0
- package/src/web.d.ts.map +1 -0
- package/src/web.ts +80 -0
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
import JavaScriptCore
|
|
3
|
+
|
|
4
|
+
/// Implements the `fs_*` host functions from `BRIDGE_CONTRACT.md`.
|
|
5
|
+
///
|
|
6
|
+
/// All sync calls return `null` / `false` on failure and stash the error
|
|
7
|
+
/// string in a thread-local field readable via `fs_last_error()`.
|
|
8
|
+
public final class FSBridge {
|
|
9
|
+
private weak var context: JSContext?
|
|
10
|
+
private var lastError: String?
|
|
11
|
+
|
|
12
|
+
public init() {}
|
|
13
|
+
|
|
14
|
+
public func install(into ctx: JSContext) {
|
|
15
|
+
self.context = ctx
|
|
16
|
+
let fm = FileManager.default
|
|
17
|
+
|
|
18
|
+
ctx.installBridgeFunction(name: "fs_read_text") { args in
|
|
19
|
+
self.clearError()
|
|
20
|
+
guard let path = args.first?.toString() else {
|
|
21
|
+
self.setError("fs_read_text: missing path")
|
|
22
|
+
return NSNull()
|
|
23
|
+
}
|
|
24
|
+
do {
|
|
25
|
+
let s = try String(contentsOfFile: path, encoding: .utf8)
|
|
26
|
+
return s
|
|
27
|
+
} catch {
|
|
28
|
+
self.setError("fs_read_text: \(error.localizedDescription)")
|
|
29
|
+
return NSNull()
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
ctx.installBridgeFunction(name: "fs_read_bytes") { args in
|
|
34
|
+
self.clearError()
|
|
35
|
+
guard let path = args.first?.toString() else {
|
|
36
|
+
self.setError("fs_read_bytes: missing path")
|
|
37
|
+
return NSNull()
|
|
38
|
+
}
|
|
39
|
+
do {
|
|
40
|
+
let data = try Data(contentsOf: URL(fileURLWithPath: path))
|
|
41
|
+
return ctx.newUint8Array(data)
|
|
42
|
+
} catch {
|
|
43
|
+
self.setError("fs_read_bytes: \(error.localizedDescription)")
|
|
44
|
+
return NSNull()
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
ctx.installBridgeFunction(name: "fs_write_text") { args in
|
|
49
|
+
self.clearError()
|
|
50
|
+
guard args.count >= 2,
|
|
51
|
+
let path = args[0].toString(),
|
|
52
|
+
let data = args[1].toString() else {
|
|
53
|
+
self.setError("fs_write_text: missing args")
|
|
54
|
+
return false
|
|
55
|
+
}
|
|
56
|
+
do {
|
|
57
|
+
try data.write(toFile: path, atomically: true, encoding: .utf8)
|
|
58
|
+
return true
|
|
59
|
+
} catch {
|
|
60
|
+
self.setError("fs_write_text: \(error.localizedDescription)")
|
|
61
|
+
return false
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
ctx.installBridgeFunction(name: "fs_write_bytes") { args in
|
|
66
|
+
self.clearError()
|
|
67
|
+
guard args.count >= 2,
|
|
68
|
+
let path = args[0].toString(),
|
|
69
|
+
let bytes = args[1].toData() else {
|
|
70
|
+
self.setError("fs_write_bytes: missing args")
|
|
71
|
+
return false
|
|
72
|
+
}
|
|
73
|
+
do {
|
|
74
|
+
try bytes.write(to: URL(fileURLWithPath: path), options: .atomic)
|
|
75
|
+
return true
|
|
76
|
+
} catch {
|
|
77
|
+
self.setError("fs_write_bytes: \(error.localizedDescription)")
|
|
78
|
+
return false
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
ctx.installBridgeFunction(name: "fs_append_text") { args in
|
|
83
|
+
self.clearError()
|
|
84
|
+
guard args.count >= 2,
|
|
85
|
+
let path = args[0].toString(),
|
|
86
|
+
let data = args[1].toString() else {
|
|
87
|
+
self.setError("fs_append_text: missing args")
|
|
88
|
+
return false
|
|
89
|
+
}
|
|
90
|
+
return self.appendText(path: path, text: data)
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
ctx.installBridgeFunction(name: "fs_exists") { args in
|
|
94
|
+
self.clearError()
|
|
95
|
+
guard let path = args.first?.toString() else { return false }
|
|
96
|
+
return fm.fileExists(atPath: path)
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
ctx.installBridgeFunction(name: "fs_mkdir") { args in
|
|
100
|
+
self.clearError()
|
|
101
|
+
guard args.count >= 2,
|
|
102
|
+
let path = args[0].toString() else {
|
|
103
|
+
self.setError("fs_mkdir: missing args")
|
|
104
|
+
return false
|
|
105
|
+
}
|
|
106
|
+
let recursive = args[1].toBool()
|
|
107
|
+
do {
|
|
108
|
+
try fm.createDirectory(
|
|
109
|
+
atPath: path,
|
|
110
|
+
withIntermediateDirectories: recursive,
|
|
111
|
+
attributes: nil
|
|
112
|
+
)
|
|
113
|
+
return true
|
|
114
|
+
} catch {
|
|
115
|
+
self.setError("fs_mkdir: \(error.localizedDescription)")
|
|
116
|
+
return false
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
ctx.installBridgeFunction(name: "fs_readdir") { args in
|
|
121
|
+
self.clearError()
|
|
122
|
+
guard let path = args.first?.toString() else {
|
|
123
|
+
self.setError("fs_readdir: missing path")
|
|
124
|
+
return NSNull()
|
|
125
|
+
}
|
|
126
|
+
do {
|
|
127
|
+
let entries = try fm.contentsOfDirectory(atPath: path)
|
|
128
|
+
return entries
|
|
129
|
+
} catch {
|
|
130
|
+
self.setError("fs_readdir: \(error.localizedDescription)")
|
|
131
|
+
return NSNull()
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
ctx.installBridgeFunction(name: "fs_stat") { args in
|
|
136
|
+
self.clearError()
|
|
137
|
+
guard let path = args.first?.toString() else {
|
|
138
|
+
self.setError("fs_stat: missing path")
|
|
139
|
+
return NSNull()
|
|
140
|
+
}
|
|
141
|
+
do {
|
|
142
|
+
let attrs = try fm.attributesOfItem(atPath: path)
|
|
143
|
+
let size = (attrs[.size] as? NSNumber)?.intValue ?? 0
|
|
144
|
+
let mtimeDate = attrs[.modificationDate] as? Date
|
|
145
|
+
let mtimeMs = (mtimeDate?.timeIntervalSince1970 ?? 0) * 1000.0
|
|
146
|
+
let type = attrs[.type] as? FileAttributeType
|
|
147
|
+
let isDir = (type == .typeDirectory)
|
|
148
|
+
let isFile = (type == .typeRegular)
|
|
149
|
+
return [
|
|
150
|
+
"size": size,
|
|
151
|
+
"mtime_ms": mtimeMs,
|
|
152
|
+
"is_directory": isDir,
|
|
153
|
+
"is_file": isFile,
|
|
154
|
+
] as [String: Any]
|
|
155
|
+
} catch {
|
|
156
|
+
self.setError("fs_stat: \(error.localizedDescription)")
|
|
157
|
+
return NSNull()
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
ctx.installBridgeFunction(name: "fs_remove") { args in
|
|
162
|
+
self.clearError()
|
|
163
|
+
guard let path = args.first?.toString() else {
|
|
164
|
+
self.setError("fs_remove: missing path")
|
|
165
|
+
return false
|
|
166
|
+
}
|
|
167
|
+
// `recursive` is an opt-in flag in the contract. FileManager
|
|
168
|
+
// removeItem already recurses for directories, but if the caller
|
|
169
|
+
// explicitly passes `false` for a directory we refuse so the
|
|
170
|
+
// semantics match POSIX rm-vs-rm-rf.
|
|
171
|
+
let recursive: Bool = args.count >= 2 ? args[1].toBool() : true
|
|
172
|
+
var isDir: ObjCBool = false
|
|
173
|
+
let exists = fm.fileExists(atPath: path, isDirectory: &isDir)
|
|
174
|
+
if !exists {
|
|
175
|
+
return true
|
|
176
|
+
}
|
|
177
|
+
if isDir.boolValue && !recursive {
|
|
178
|
+
self.setError("fs_remove: path is a directory and recursive=false")
|
|
179
|
+
return false
|
|
180
|
+
}
|
|
181
|
+
do {
|
|
182
|
+
try fm.removeItem(atPath: path)
|
|
183
|
+
return true
|
|
184
|
+
} catch {
|
|
185
|
+
self.setError("fs_remove: \(error.localizedDescription)")
|
|
186
|
+
return false
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
ctx.installBridgeFunction(name: "fs_rename") { args in
|
|
191
|
+
self.clearError()
|
|
192
|
+
guard args.count >= 2,
|
|
193
|
+
let from = args[0].toString(),
|
|
194
|
+
let to = args[1].toString() else {
|
|
195
|
+
self.setError("fs_rename: missing args")
|
|
196
|
+
return false
|
|
197
|
+
}
|
|
198
|
+
do {
|
|
199
|
+
try fm.moveItem(atPath: from, toPath: to)
|
|
200
|
+
return true
|
|
201
|
+
} catch {
|
|
202
|
+
self.setError("fs_rename: \(error.localizedDescription)")
|
|
203
|
+
return false
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
ctx.installBridgeFunction(name: "fs_copy") { args in
|
|
208
|
+
self.clearError()
|
|
209
|
+
guard args.count >= 2,
|
|
210
|
+
let from = args[0].toString(),
|
|
211
|
+
let to = args[1].toString() else {
|
|
212
|
+
self.setError("fs_copy: missing args")
|
|
213
|
+
return false
|
|
214
|
+
}
|
|
215
|
+
do {
|
|
216
|
+
if fm.fileExists(atPath: to) {
|
|
217
|
+
try fm.removeItem(atPath: to)
|
|
218
|
+
}
|
|
219
|
+
try fm.copyItem(atPath: from, toPath: to)
|
|
220
|
+
return true
|
|
221
|
+
} catch {
|
|
222
|
+
self.setError("fs_copy: \(error.localizedDescription)")
|
|
223
|
+
return false
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
ctx.installBridgeFunction(name: "fs_last_error") { _ in
|
|
228
|
+
return self.lastError ?? NSNull()
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// MARK: - Internals
|
|
233
|
+
|
|
234
|
+
private func setError(_ message: String) {
|
|
235
|
+
self.lastError = message
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
private func clearError() {
|
|
239
|
+
self.lastError = nil
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/// Appends UTF-8 text to a file, creating it if missing. Returns false
|
|
243
|
+
/// on failure and stashes a message in `lastError`.
|
|
244
|
+
private func appendText(path: String, text: String) -> Bool {
|
|
245
|
+
let fm = FileManager.default
|
|
246
|
+
if !fm.fileExists(atPath: path) {
|
|
247
|
+
do {
|
|
248
|
+
try text.write(toFile: path, atomically: true, encoding: .utf8)
|
|
249
|
+
return true
|
|
250
|
+
} catch {
|
|
251
|
+
setError("fs_append_text: \(error.localizedDescription)")
|
|
252
|
+
return false
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
guard let handle = FileHandle(forWritingAtPath: path),
|
|
256
|
+
let bytes = text.data(using: .utf8) else {
|
|
257
|
+
setError("fs_append_text: could not open file for writing")
|
|
258
|
+
return false
|
|
259
|
+
}
|
|
260
|
+
defer { try? handle.close() }
|
|
261
|
+
do {
|
|
262
|
+
try handle.seekToEnd()
|
|
263
|
+
try handle.write(contentsOf: bytes)
|
|
264
|
+
return true
|
|
265
|
+
} catch {
|
|
266
|
+
setError("fs_append_text: \(error.localizedDescription)")
|
|
267
|
+
return false
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
}
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
import JavaScriptCore
|
|
3
|
+
|
|
4
|
+
/// Implements `http_fetch` from `BRIDGE_CONTRACT.md`.
|
|
5
|
+
///
|
|
6
|
+
/// Returns a JS Promise via the constructor pattern: the bridge captures the
|
|
7
|
+
/// resolve callback as a `ManagedCallback`, runs the URLSession request off
|
|
8
|
+
/// the JSContext queue, then re-enters the JSContext queue to fulfill the
|
|
9
|
+
/// promise.
|
|
10
|
+
public final class HTTPBridge {
|
|
11
|
+
private weak var context: JSContext?
|
|
12
|
+
private let urlSession: URLSession
|
|
13
|
+
|
|
14
|
+
public init(session: URLSession = .shared) {
|
|
15
|
+
self.urlSession = session
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
public func install(into ctx: JSContext) {
|
|
19
|
+
self.context = ctx
|
|
20
|
+
|
|
21
|
+
ctx.installBridgeFunction(name: "http_fetch") { args in
|
|
22
|
+
guard let ctx = self.context else { return NSNull() }
|
|
23
|
+
guard let opts = args.first, opts.isObject else {
|
|
24
|
+
return Self.rejectedPromise(in: ctx, error: "http_fetch: missing options")
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
let url = opts.objectForKeyedSubscript("url")?.toString() ?? ""
|
|
28
|
+
let method = (opts.objectForKeyedSubscript("method")?.toString() ?? "GET").uppercased()
|
|
29
|
+
let headers = opts.objectForKeyedSubscript("headers")?.toStringMap() ?? [:]
|
|
30
|
+
let bodyValue = opts.objectForKeyedSubscript("body")
|
|
31
|
+
let body: Data? = (bodyValue?.isNullish == false) ? bodyValue?.toData() : nil
|
|
32
|
+
let timeoutMs = opts.objectForKeyedSubscript("timeout_ms")?.toNumber()?.doubleValue
|
|
33
|
+
|
|
34
|
+
guard let target = URL(string: url) else {
|
|
35
|
+
return Self.rejectedPromise(in: ctx, error: "http_fetch: invalid url")
|
|
36
|
+
}
|
|
37
|
+
guard target.scheme == "http" || target.scheme == "https" else {
|
|
38
|
+
return Self.rejectedPromise(in: ctx, error: "http_fetch: unsupported URL scheme")
|
|
39
|
+
}
|
|
40
|
+
if Self.isLocalOrPrivateNetwork(target) {
|
|
41
|
+
return Self.rejectedPromise(in: ctx, error: "http_fetch: loopback/private URLs must use path-only http_request IPC")
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
var request = URLRequest(url: target)
|
|
45
|
+
request.httpMethod = method
|
|
46
|
+
for (k, v) in headers {
|
|
47
|
+
request.setValue(v, forHTTPHeaderField: k)
|
|
48
|
+
}
|
|
49
|
+
if let body = body, method != "GET", method != "HEAD" {
|
|
50
|
+
request.httpBody = body
|
|
51
|
+
}
|
|
52
|
+
if let ms = timeoutMs, ms > 0 {
|
|
53
|
+
request.timeoutInterval = ms / 1000.0
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return self.runFetch(ctx: ctx, request: request)
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// MARK: - Internals
|
|
61
|
+
|
|
62
|
+
private func runFetch(ctx: JSContext, request: URLRequest) -> Any? {
|
|
63
|
+
// Build the promise factory once.
|
|
64
|
+
let promiseScript = """
|
|
65
|
+
(function(){
|
|
66
|
+
let resolveFn;
|
|
67
|
+
const p = new Promise(function(res){ resolveFn = res; });
|
|
68
|
+
p.__eliza_resolve = resolveFn;
|
|
69
|
+
return p;
|
|
70
|
+
})
|
|
71
|
+
"""
|
|
72
|
+
guard let promise = ctx.evaluateScript(promiseScript)?.call(withArguments: []) else {
|
|
73
|
+
return Self.rejectedPromise(in: ctx, error: "http_fetch: failed to construct promise")
|
|
74
|
+
}
|
|
75
|
+
let resolveValue = promise.forProperty("__eliza_resolve")
|
|
76
|
+
let managedResolve = resolveValue.flatMap { ManagedCallback(value: $0) }
|
|
77
|
+
|
|
78
|
+
let task = urlSession.dataTask(with: request) { data, response, error in
|
|
79
|
+
let result = Self.buildResultDict(data: data, response: response, error: error, ctx: ctx)
|
|
80
|
+
// Hop back onto the JS queue.
|
|
81
|
+
RuntimeQueue.dispatchOnJS {
|
|
82
|
+
guard let resolve = managedResolve else { return }
|
|
83
|
+
resolve.callSync(args: [result])
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
task.resume()
|
|
87
|
+
|
|
88
|
+
return promise
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
private static func isLocalOrPrivateNetwork(_ url: URL) -> Bool {
|
|
92
|
+
guard let host = url.host?.lowercased() else { return false }
|
|
93
|
+
return host == "localhost" ||
|
|
94
|
+
host == "127.0.0.1" ||
|
|
95
|
+
host == "0.0.0.0" ||
|
|
96
|
+
host == "::1" ||
|
|
97
|
+
host.hasPrefix("127.") ||
|
|
98
|
+
host.hasPrefix("10.") ||
|
|
99
|
+
host.hasPrefix("192.168.") ||
|
|
100
|
+
host.range(of: #"^172\.(1[6-9]|2[0-9]|3[0-1])\."#, options: .regularExpression) != nil ||
|
|
101
|
+
host.range(of: #"^100\.(6[4-9]|[7-9][0-9]|1[0-1][0-9]|12[0-7])\."#, options: .regularExpression) != nil ||
|
|
102
|
+
host.hasPrefix("169.254.") ||
|
|
103
|
+
(host.contains(":") &&
|
|
104
|
+
(host.hasPrefix("fe80:") ||
|
|
105
|
+
host.hasPrefix("fc") ||
|
|
106
|
+
host.hasPrefix("fd"))) ||
|
|
107
|
+
host == "local" ||
|
|
108
|
+
host == "internal" ||
|
|
109
|
+
host == "lan" ||
|
|
110
|
+
host == "ts.net" ||
|
|
111
|
+
host.hasSuffix(".local") ||
|
|
112
|
+
host.hasSuffix(".internal") ||
|
|
113
|
+
host.hasSuffix(".lan") ||
|
|
114
|
+
host.hasSuffix(".ts.net")
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
private static func buildResultDict(data: Data?, response: URLResponse?, error: Error?, ctx: JSContext) -> [String: Any] {
|
|
118
|
+
if let error = error {
|
|
119
|
+
return [
|
|
120
|
+
"status": 0,
|
|
121
|
+
"headers": [:] as [String: String],
|
|
122
|
+
"body": ctx.newUint8Array(Data()),
|
|
123
|
+
"error": error.localizedDescription,
|
|
124
|
+
]
|
|
125
|
+
}
|
|
126
|
+
guard let http = response as? HTTPURLResponse else {
|
|
127
|
+
return [
|
|
128
|
+
"status": 0,
|
|
129
|
+
"headers": [:] as [String: String],
|
|
130
|
+
"body": ctx.newUint8Array(Data()),
|
|
131
|
+
"error": "Non-HTTP response",
|
|
132
|
+
]
|
|
133
|
+
}
|
|
134
|
+
var headerDict: [String: String] = [:]
|
|
135
|
+
for (k, v) in http.allHeaderFields {
|
|
136
|
+
guard let key = k as? String else { continue }
|
|
137
|
+
headerDict[key.lowercased()] = String(describing: v)
|
|
138
|
+
}
|
|
139
|
+
let payload = data ?? Data()
|
|
140
|
+
return [
|
|
141
|
+
"status": http.statusCode,
|
|
142
|
+
"headers": headerDict,
|
|
143
|
+
"body": ctx.newUint8Array(payload),
|
|
144
|
+
]
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
private static func rejectedPromise(in ctx: JSContext, error: String) -> Any? {
|
|
148
|
+
// Promise.resolve({ error }) per contract — async errors are returned,
|
|
149
|
+
// not thrown.
|
|
150
|
+
let script = "(function(msg){return Promise.resolve({status:0,headers:{},body:new Uint8Array(),error:msg});})"
|
|
151
|
+
return ctx.evaluateScript(script)?.call(withArguments: [error])
|
|
152
|
+
}
|
|
153
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
import JavaScriptCore
|
|
3
|
+
|
|
4
|
+
/// Compatibility registration for the old `http_serve_*` bridge surface.
|
|
5
|
+
///
|
|
6
|
+
/// iOS local mode must route foreground and backend calls through Capacitor /
|
|
7
|
+
/// engine IPC (`ElizaBunRuntime.call("http_request", ...)`) instead of opening
|
|
8
|
+
/// a localhost listener inside the app. Keeping these symbols registered gives
|
|
9
|
+
/// older JSContext bundles a deterministic error without ever binding a port.
|
|
10
|
+
public final class HTTPServerBridge {
|
|
11
|
+
public init() {}
|
|
12
|
+
|
|
13
|
+
public func install(into ctx: JSContext) {
|
|
14
|
+
ctx.installBridgeFunction(name: "http_serve_start") { _ in
|
|
15
|
+
[
|
|
16
|
+
"ok": false,
|
|
17
|
+
"port": 0,
|
|
18
|
+
"error": "http_serve_start is disabled on iOS; use ElizaBunRuntime.call(http_request) IPC",
|
|
19
|
+
]
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
ctx.installBridgeFunction(name: "http_serve_register_handler") { _ in
|
|
23
|
+
NSNull()
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
ctx.installBridgeFunction(name: "http_serve_stop") { _ in
|
|
27
|
+
NSNull()
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
public func shutdown() {}
|
|
32
|
+
}
|
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
import JavaScriptCore
|
|
3
|
+
|
|
4
|
+
#if canImport(LlamaCppCapacitor)
|
|
5
|
+
import LlamaCppCapacitor
|
|
6
|
+
#endif
|
|
7
|
+
|
|
8
|
+
/// Implements `llama_*` from `BRIDGE_CONTRACT.md`.
|
|
9
|
+
///
|
|
10
|
+
/// `LlamaBridgeImpl` is JSContext-agnostic by design: this file owns JSValue
|
|
11
|
+
/// parsing, promise wiring, and ManagedCallback streaming; the impl owns the
|
|
12
|
+
/// llama.cpp C API calls. Bridge failures resolve as `{ error }` values
|
|
13
|
+
/// because the JS polyfill layer treats bridge results as native response
|
|
14
|
+
/// payloads rather than exception channels.
|
|
15
|
+
public final class LlamaBridge {
|
|
16
|
+
private weak var context: JSContext?
|
|
17
|
+
private var nextContextId: Int = 1
|
|
18
|
+
private var contexts: [Int: LlamaContextState] = [:]
|
|
19
|
+
private var streamCallbacks: [String: ManagedCallback] = [:]
|
|
20
|
+
private let inferenceQueue = DispatchQueue(label: "ai.eliza.bun.runtime.llama", qos: .userInitiated)
|
|
21
|
+
|
|
22
|
+
public init() {}
|
|
23
|
+
|
|
24
|
+
public func install(into ctx: JSContext) {
|
|
25
|
+
self.context = ctx
|
|
26
|
+
|
|
27
|
+
ctx.installBridgeFunction(name: "llama_load_model") { args in
|
|
28
|
+
guard let ctx = self.context else { return NSNull() }
|
|
29
|
+
return self.loadModel(args: args, ctx: ctx)
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
ctx.installBridgeFunction(name: "llama_generate") { args in
|
|
33
|
+
guard let ctx = self.context else { return NSNull() }
|
|
34
|
+
return self.generate(args: args, ctx: ctx)
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
ctx.installBridgeFunction(name: "llama_register_stream_callback") { args in
|
|
38
|
+
guard args.count >= 2,
|
|
39
|
+
let token = args[0].toString() else { return NSNull() }
|
|
40
|
+
let handlerValue = args[1]
|
|
41
|
+
if let mc = ManagedCallback(value: handlerValue) {
|
|
42
|
+
self.streamCallbacks[token] = mc
|
|
43
|
+
}
|
|
44
|
+
return NSNull()
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
ctx.installBridgeFunction(name: "llama_cancel") { args in
|
|
48
|
+
guard let id = args.first?.toNumber()?.intValue else { return NSNull() }
|
|
49
|
+
if var state = self.contexts[id] {
|
|
50
|
+
state.cancelled = true
|
|
51
|
+
self.contexts[id] = state
|
|
52
|
+
}
|
|
53
|
+
LlamaBridgeImpl.shared.cancel(contextId: Int64(id))
|
|
54
|
+
return NSNull()
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
ctx.installBridgeFunction(name: "llama_free") { args in
|
|
58
|
+
guard let id = args.first?.toNumber()?.intValue else { return NSNull() }
|
|
59
|
+
self.contexts.removeValue(forKey: id)
|
|
60
|
+
LlamaBridgeImpl.shared.free(contextId: Int64(id))
|
|
61
|
+
return NSNull()
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
ctx.installBridgeFunction(name: "llama_hardware_info") { _ in
|
|
65
|
+
return self.hardwareInfo()
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// MARK: - Context state
|
|
70
|
+
|
|
71
|
+
private struct LlamaContextState {
|
|
72
|
+
let id: Int
|
|
73
|
+
let modelPath: String
|
|
74
|
+
var contextSize: Int
|
|
75
|
+
var useGpu: Bool
|
|
76
|
+
var threads: Int
|
|
77
|
+
var cancelled: Bool
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// MARK: - Implementations
|
|
81
|
+
|
|
82
|
+
private func loadModel(args: [JSValue], ctx: JSContext) -> Any? {
|
|
83
|
+
guard let opts = args.first, opts.isObject else {
|
|
84
|
+
return Self.rejectedAsync(in: ctx, error: "llama_load_model: missing options")
|
|
85
|
+
}
|
|
86
|
+
let path = opts.objectForKeyedSubscript("path")?.toString() ?? ""
|
|
87
|
+
if path.isEmpty {
|
|
88
|
+
return Self.rejectedAsync(in: ctx, error: "llama_load_model: missing path")
|
|
89
|
+
}
|
|
90
|
+
let contextSize = opts.objectForKeyedSubscript("context_size")?.toNumber()?.intValue ?? 4096
|
|
91
|
+
let useGpu = opts.objectForKeyedSubscript("use_gpu")?.toBool() ?? true
|
|
92
|
+
let threads = opts.objectForKeyedSubscript("threads")?.toNumber()?.intValue
|
|
93
|
+
?? min(4, ProcessInfo.processInfo.activeProcessorCount)
|
|
94
|
+
|
|
95
|
+
// Build the promise + resolver pair on the JS side.
|
|
96
|
+
let (promise, resolver) = Self.makeAsyncPromise(in: ctx)
|
|
97
|
+
let managedResolve = resolver.flatMap { ManagedCallback(value: $0) }
|
|
98
|
+
|
|
99
|
+
inferenceQueue.async { [weak self] in
|
|
100
|
+
guard let self = self else { return }
|
|
101
|
+
|
|
102
|
+
if !FileManager.default.fileExists(atPath: path) {
|
|
103
|
+
RuntimeQueue.dispatchOnJS {
|
|
104
|
+
managedResolve?.callSync(args: [["error": "model file not found: \(path)"]])
|
|
105
|
+
}
|
|
106
|
+
return
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
let result = LlamaBridgeImpl.shared.loadModel(
|
|
110
|
+
path: path,
|
|
111
|
+
contextSize: UInt32(max(1, contextSize)),
|
|
112
|
+
useGPU: useGpu,
|
|
113
|
+
threads: Int32(max(1, threads))
|
|
114
|
+
)
|
|
115
|
+
if let error = result.error {
|
|
116
|
+
RuntimeQueue.dispatchOnJS {
|
|
117
|
+
managedResolve?.callSync(args: [["error": error]])
|
|
118
|
+
}
|
|
119
|
+
return
|
|
120
|
+
}
|
|
121
|
+
guard let contextId = result.contextId else {
|
|
122
|
+
RuntimeQueue.dispatchOnJS {
|
|
123
|
+
managedResolve?.callSync(args: [["error": "llama_load_model: backend returned no context_id"]])
|
|
124
|
+
}
|
|
125
|
+
return
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
let id = Int(contextId)
|
|
129
|
+
self.nextContextId = max(self.nextContextId, id + 1)
|
|
130
|
+
self.contexts[id] = LlamaContextState(
|
|
131
|
+
id: id,
|
|
132
|
+
modelPath: path,
|
|
133
|
+
contextSize: contextSize,
|
|
134
|
+
useGpu: useGpu,
|
|
135
|
+
threads: threads,
|
|
136
|
+
cancelled: false
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
RuntimeQueue.dispatchOnJS {
|
|
140
|
+
managedResolve?.callSync(args: [["context_id": id]])
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
return promise
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
private func generate(args: [JSValue], ctx: JSContext) -> Any? {
|
|
148
|
+
guard let opts = args.first, opts.isObject else {
|
|
149
|
+
return Self.rejectedAsync(in: ctx, error: "llama_generate: missing options")
|
|
150
|
+
}
|
|
151
|
+
let contextId = opts.objectForKeyedSubscript("context_id")?.toNumber()?.intValue ?? -1
|
|
152
|
+
let prompt = opts.objectForKeyedSubscript("prompt")?.toString() ?? ""
|
|
153
|
+
let maxTokens = opts.objectForKeyedSubscript("max_tokens")?.toNumber()?.intValue ?? 256
|
|
154
|
+
let temperature = opts.objectForKeyedSubscript("temperature")?.toNumber()?.doubleValue ?? 0.7
|
|
155
|
+
let topP = opts.objectForKeyedSubscript("top_p")?.toNumber()?.doubleValue ?? 0.95
|
|
156
|
+
let stop = opts.objectForKeyedSubscript("stop")?.toStringArray() ?? []
|
|
157
|
+
let streamToken = opts.objectForKeyedSubscript("stream_callback_token")?.toString()
|
|
158
|
+
|
|
159
|
+
guard let state = contexts[contextId] else {
|
|
160
|
+
return Self.rejectedAsync(in: ctx, error: "llama_generate: unknown context_id \(contextId)")
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
let (promise, resolver) = Self.makeAsyncPromise(in: ctx)
|
|
164
|
+
let managedResolve = resolver.flatMap { ManagedCallback(value: $0) }
|
|
165
|
+
let streamCallback = streamToken.flatMap { self.streamCallbacks[$0] }
|
|
166
|
+
|
|
167
|
+
let queue = LlamaBridgeImpl.shared.workQueue(for: Int64(contextId)) ?? inferenceQueue
|
|
168
|
+
queue.async {
|
|
169
|
+
let started = Date()
|
|
170
|
+
let result = LlamaBridgeImpl.shared.generate(
|
|
171
|
+
contextId: Int64(state.id),
|
|
172
|
+
prompt: prompt,
|
|
173
|
+
maxTokens: Int32(max(1, maxTokens)),
|
|
174
|
+
temperature: Float(temperature),
|
|
175
|
+
topP: Float(topP),
|
|
176
|
+
stopSequences: stop,
|
|
177
|
+
onToken: { token, isLast in
|
|
178
|
+
guard let cb = streamCallback else { return }
|
|
179
|
+
RuntimeQueue.dispatchOnJS {
|
|
180
|
+
cb.callSync(args: [token, isLast])
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
)
|
|
184
|
+
|
|
185
|
+
let durationMs = Int(Date().timeIntervalSince(started) * 1000)
|
|
186
|
+
let promptTokens = max(1, result.promptTokens)
|
|
187
|
+
let outputTokens = max(1, result.outputTokens)
|
|
188
|
+
|
|
189
|
+
RuntimeQueue.dispatchOnJS {
|
|
190
|
+
if let error = result.error {
|
|
191
|
+
managedResolve?.callSync(args: [["error": error]])
|
|
192
|
+
return
|
|
193
|
+
}
|
|
194
|
+
managedResolve?.callSync(args: [[
|
|
195
|
+
"text": result.text,
|
|
196
|
+
"prompt_tokens": promptTokens,
|
|
197
|
+
"output_tokens": outputTokens,
|
|
198
|
+
"duration_ms": Int(result.durationMs > 0 ? result.durationMs : Double(durationMs)),
|
|
199
|
+
]])
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
return promise
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
private func hardwareInfo() -> [String: Any] {
|
|
207
|
+
return LlamaBridgeImpl.shared.hardwareInfo().asDict()
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// MARK: - Promise builders
|
|
211
|
+
|
|
212
|
+
/// Returns `(promise, resolver)`. The resolver is a JS function value.
|
|
213
|
+
static func makeAsyncPromise(in ctx: JSContext) -> (Any, JSValue?) {
|
|
214
|
+
let script = """
|
|
215
|
+
(function(){
|
|
216
|
+
let resolveFn;
|
|
217
|
+
const p = new Promise(function(res){ resolveFn = res; });
|
|
218
|
+
p.__eliza_resolve = resolveFn;
|
|
219
|
+
return p;
|
|
220
|
+
})
|
|
221
|
+
"""
|
|
222
|
+
guard let promise = ctx.evaluateScript(script)?.call(withArguments: []) else {
|
|
223
|
+
return (NSNull(), nil)
|
|
224
|
+
}
|
|
225
|
+
let resolver = promise.forProperty("__eliza_resolve")
|
|
226
|
+
return (promise, resolver)
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
static func rejectedAsync(in ctx: JSContext, error: String) -> Any? {
|
|
230
|
+
let script = "(function(msg){return Promise.resolve({error:msg});})"
|
|
231
|
+
return ctx.evaluateScript(script)?.call(withArguments: [error])
|
|
232
|
+
}
|
|
233
|
+
}
|