@xybrid/react-native 0.10.0-rc1
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/LICENSE +201 -0
- package/README.md +298 -0
- package/android/build.gradle +61 -0
- package/android/src/main/AndroidManifest.xml +1 -0
- package/android/src/main/java/ai/xybrid/reactnative/XybridCodec.kt +481 -0
- package/android/src/main/java/ai/xybrid/reactnative/XybridHandles.kt +139 -0
- package/android/src/main/java/ai/xybrid/reactnative/XybridModule.kt +665 -0
- package/android/src/main/java/ai/xybrid/reactnative/XybridPackage.kt +27 -0
- package/ios/XybridCodec.swift +508 -0
- package/ios/XybridHandles.swift +191 -0
- package/ios/XybridModule.mm +494 -0
- package/ios/XybridModuleImpl.swift +887 -0
- package/ios/XybridSwift/Xybrid.swift +1401 -0
- package/ios/XybridSwift/xybrid_bolt.swift +3198 -0
- package/ios/xybrid_natives.rb +178 -0
- package/lib/commonjs/NativeXybrid.js +24 -0
- package/lib/commonjs/base64.js +45 -0
- package/lib/commonjs/cancel.js +35 -0
- package/lib/commonjs/context.js +67 -0
- package/lib/commonjs/envelope.js +79 -0
- package/lib/commonjs/errors.js +61 -0
- package/lib/commonjs/index.js +59 -0
- package/lib/commonjs/model.js +307 -0
- package/lib/commonjs/pipeline.js +55 -0
- package/lib/commonjs/presets.js +32 -0
- package/lib/commonjs/session.js +76 -0
- package/lib/commonjs/types.js +10 -0
- package/lib/commonjs/wire.js +285 -0
- package/lib/commonjs/xybrid.js +133 -0
- package/lib/module/NativeXybrid.js +22 -0
- package/lib/module/base64.js +41 -0
- package/lib/module/cancel.js +29 -0
- package/lib/module/context.js +60 -0
- package/lib/module/envelope.js +73 -0
- package/lib/module/errors.js +56 -0
- package/lib/module/index.js +36 -0
- package/lib/module/model.js +298 -0
- package/lib/module/pipeline.js +48 -0
- package/lib/module/presets.js +27 -0
- package/lib/module/session.js +69 -0
- package/lib/module/types.js +9 -0
- package/lib/module/wire.js +271 -0
- package/lib/module/xybrid.js +127 -0
- package/lib/typescript/NativeXybrid.d.ts +68 -0
- package/lib/typescript/NativeXybrid.d.ts.map +1 -0
- package/lib/typescript/base64.d.ts +5 -0
- package/lib/typescript/base64.d.ts.map +1 -0
- package/lib/typescript/cancel.d.ts +24 -0
- package/lib/typescript/cancel.d.ts.map +1 -0
- package/lib/typescript/context.d.ts +40 -0
- package/lib/typescript/context.d.ts.map +1 -0
- package/lib/typescript/envelope.d.ts +51 -0
- package/lib/typescript/envelope.d.ts.map +1 -0
- package/lib/typescript/errors.d.ts +12 -0
- package/lib/typescript/errors.d.ts.map +1 -0
- package/lib/typescript/index.d.ts +28 -0
- package/lib/typescript/index.d.ts.map +1 -0
- package/lib/typescript/model.d.ts +211 -0
- package/lib/typescript/model.d.ts.map +1 -0
- package/lib/typescript/pipeline.d.ts +35 -0
- package/lib/typescript/pipeline.d.ts.map +1 -0
- package/lib/typescript/presets.d.ts +21 -0
- package/lib/typescript/presets.d.ts.map +1 -0
- package/lib/typescript/session.d.ts +50 -0
- package/lib/typescript/session.d.ts.map +1 -0
- package/lib/typescript/types.d.ts +313 -0
- package/lib/typescript/types.d.ts.map +1 -0
- package/lib/typescript/wire.d.ts +130 -0
- package/lib/typescript/wire.d.ts.map +1 -0
- package/lib/typescript/xybrid.d.ts +89 -0
- package/lib/typescript/xybrid.d.ts.map +1 -0
- package/package.json +88 -0
- package/react-native-xybrid.podspec +50 -0
- package/src/NativeXybrid.ts +131 -0
- package/src/base64.ts +43 -0
- package/src/cancel.ts +47 -0
- package/src/context.ts +70 -0
- package/src/envelope.ts +115 -0
- package/src/errors.ts +67 -0
- package/src/index.ts +91 -0
- package/src/model.ts +408 -0
- package/src/pipeline.ts +66 -0
- package/src/presets.ts +36 -0
- package/src/session.ts +76 -0
- package/src/types.ts +362 -0
- package/src/wire.ts +408 -0
- package/src/xybrid.ts +176 -0
|
@@ -0,0 +1,887 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
import React
|
|
3
|
+
|
|
4
|
+
// The Swift half of the TurboModule. XybridModule.mm conforms to the
|
|
5
|
+
// Codegen protocol and forwards every selector here unchanged; each method
|
|
6
|
+
// decodes its arguments (XybridCodec), calls the bolt Swift SDK compiled into
|
|
7
|
+
// this pod (Xybrid.swift + xybrid_bolt.swift), and settles the promise with a
|
|
8
|
+
// typed `xybrid_*` code on failure.
|
|
9
|
+
//
|
|
10
|
+
// Blocking SDK calls (load, run, stream pulls, downloads, disk walks) run on
|
|
11
|
+
// a detached task so the module's method queue stays free; cheap getters
|
|
12
|
+
// settle inline.
|
|
13
|
+
|
|
14
|
+
// File-scope aliases for the generated bolt free functions. Inside the class,
|
|
15
|
+
// a member named like a global (`setBatteryLevel`, `cacheStatus`, …) shadows
|
|
16
|
+
// it — "use of 'X' refers to instance method rather than global function" —
|
|
17
|
+
// and the module name can't qualify it portably. At file scope no member is
|
|
18
|
+
// in scope, so these bind to the globals.
|
|
19
|
+
private let ffiSetBinding = setBinding
|
|
20
|
+
private let ffiInitSdkCacheDir = initSdkCacheDir
|
|
21
|
+
private let ffiConfigureRuntime = configureRuntime
|
|
22
|
+
private let ffiVersion = version
|
|
23
|
+
private let ffiHasApiKey = hasApiKey
|
|
24
|
+
private let ffiSetProviderApiKey = setProviderApiKey
|
|
25
|
+
private let ffiSetPlatformUrl = setPlatformUrl
|
|
26
|
+
private let ffiSetSpeculativeCloud = setSpeculativeCloud
|
|
27
|
+
private let ffiIsSpeculativeCloudEnabled = isSpeculativeCloudEnabled
|
|
28
|
+
private let ffiWillSpeculateForModel = willSpeculateForModel
|
|
29
|
+
private let ffiReleaseMemory = releaseMemory
|
|
30
|
+
private let ffiSetAutoRelease = setAutoRelease
|
|
31
|
+
private let ffiIsAutoReleaseEnabled = isAutoReleaseEnabled
|
|
32
|
+
private let ffiSetBatteryLevel = setBatteryLevel
|
|
33
|
+
private let ffiClearBatteryLevel = clearBatteryLevel
|
|
34
|
+
private let ffiSetThermalState = setThermalState
|
|
35
|
+
private let ffiClearThermalState = clearThermalState
|
|
36
|
+
private let ffiCacheStatus = cacheStatus
|
|
37
|
+
private let ffiCacheEntries = cacheEntries
|
|
38
|
+
private let ffiCacheIsModelCached = cacheIsModelCached
|
|
39
|
+
private let ffiCacheModelPath = cacheModelPath
|
|
40
|
+
private let ffiCacheListExtractedModelIds = cacheListExtractedModelIds
|
|
41
|
+
private let ffiCacheRemoveModel = cacheRemoveModel
|
|
42
|
+
private let ffiCacheClear = cacheClear
|
|
43
|
+
private let ffiJsonSchemaToGbnf = jsonSchemaToGbnf
|
|
44
|
+
private let ffiToolResultsEnvelope = toolResultsEnvelope
|
|
45
|
+
|
|
46
|
+
/// Process-wide SDK setup. The Rust SDK's configuration is process-global
|
|
47
|
+
/// (first-set-wins), and it outlives this module: a JS reload builds a new
|
|
48
|
+
/// module instance but not a new process. So what has been applied is
|
|
49
|
+
/// tracked process-wide too, which is what keeps a reload from starting a
|
|
50
|
+
/// second telemetry exporter.
|
|
51
|
+
private enum SdkSetup {
|
|
52
|
+
struct Runtime: Equatable {
|
|
53
|
+
var apiKey: String?
|
|
54
|
+
var gatewayUrl: String?
|
|
55
|
+
var ingestUrl: String?
|
|
56
|
+
|
|
57
|
+
var isEmpty: Bool { apiKey == nil && gatewayUrl == nil && ingestUrl == nil }
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
private static let lock = NSLock()
|
|
61
|
+
nonisolated(unsafe) private static var appliedCacheDir: String?
|
|
62
|
+
nonisolated(unsafe) private static var appliedRuntime: Runtime?
|
|
63
|
+
|
|
64
|
+
/// Register the binding, battery observers and the cache directory, once.
|
|
65
|
+
/// Every bridged call runs this first, so local inference needs no
|
|
66
|
+
/// `initialize()` at all.
|
|
67
|
+
static func ensureBase(cacheDir requested: String? = nil) throws {
|
|
68
|
+
lock.lock()
|
|
69
|
+
defer { lock.unlock() }
|
|
70
|
+
if let applied = appliedCacheDir {
|
|
71
|
+
if let requested, requested != applied {
|
|
72
|
+
throw BridgeError.config(
|
|
73
|
+
"the model cache is already at \(applied); cacheDir only applies to the first Xybrid call")
|
|
74
|
+
}
|
|
75
|
+
return
|
|
76
|
+
}
|
|
77
|
+
// First-set-wins in the SDK: claim the binding before Xybrid.initialize()
|
|
78
|
+
// registers "swift". Its own configureRuntime(nil, nil, nil) is a no-op;
|
|
79
|
+
// what it adds is the UIDevice battery observer.
|
|
80
|
+
ffiSetBinding("react-native")
|
|
81
|
+
Xybrid.initialize()
|
|
82
|
+
let directory = try requested ?? defaultCacheDirectory()
|
|
83
|
+
try FileManager.default.createDirectory(atPath: directory, withIntermediateDirectories: true)
|
|
84
|
+
ffiInitSdkCacheDir(directory)
|
|
85
|
+
appliedCacheDir = directory
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/// Apply the API key and URL overrides — once per process.
|
|
89
|
+
static func configure(_ runtime: Runtime) throws {
|
|
90
|
+
guard !runtime.isEmpty else { return }
|
|
91
|
+
lock.lock()
|
|
92
|
+
defer { lock.unlock() }
|
|
93
|
+
if let applied = appliedRuntime {
|
|
94
|
+
if applied == runtime { return }
|
|
95
|
+
throw BridgeError.config(
|
|
96
|
+
"Xybrid is already initialized with different options; they apply once per app process")
|
|
97
|
+
}
|
|
98
|
+
ffiConfigureRuntime(runtime.apiKey, runtime.gatewayUrl, runtime.ingestUrl)
|
|
99
|
+
appliedRuntime = runtime
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
private static func defaultCacheDirectory() throws -> String {
|
|
103
|
+
guard let caches = FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask).first else {
|
|
104
|
+
throw BridgeError.config("could not resolve the caches directory")
|
|
105
|
+
}
|
|
106
|
+
return caches.appendingPathComponent("xybrid/models", isDirectory: true).path
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/// Clamp a JS millisecond timeout into `UInt64` without trapping on NaN,
|
|
111
|
+
/// infinity, negatives, or values at or above 2^63 (`UInt64.max` itself is
|
|
112
|
+
/// not exactly representable as a `Double`).
|
|
113
|
+
private func clampTimeoutMs(_ raw: Double) -> UInt64 {
|
|
114
|
+
guard raw.isFinite, raw > 0 else { return 0 }
|
|
115
|
+
let ceiling = Double(UInt64(1) << 63)
|
|
116
|
+
return raw >= ceiling ? UInt64(1) << 63 : UInt64(raw)
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/// Blank means absent for configuration strings.
|
|
120
|
+
private func nonBlank(_ value: String?) -> String? {
|
|
121
|
+
guard let trimmed = value?.trimmingCharacters(in: .whitespacesAndNewlines), !trimmed.isEmpty else {
|
|
122
|
+
return nil
|
|
123
|
+
}
|
|
124
|
+
return trimmed
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
@objc(XybridModuleImpl)
|
|
128
|
+
public final class XybridModuleImpl: NSObject {
|
|
129
|
+
private let handles = XybridHandles()
|
|
130
|
+
|
|
131
|
+
// MARK: - Settling promises
|
|
132
|
+
|
|
133
|
+
/// Settle inline — for calls that return immediately.
|
|
134
|
+
private func now(_ resolve: RCTPromiseResolveBlock,
|
|
135
|
+
_ reject: RCTPromiseRejectBlock,
|
|
136
|
+
_ work: () throws -> Any?) {
|
|
137
|
+
do {
|
|
138
|
+
try SdkSetup.ensureBase()
|
|
139
|
+
resolve(try work())
|
|
140
|
+
} catch {
|
|
141
|
+
Self.reject(reject, error)
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/// Settle from a detached task — for calls that block or suspend.
|
|
146
|
+
private func background(_ resolve: @escaping RCTPromiseResolveBlock,
|
|
147
|
+
_ reject: @escaping RCTPromiseRejectBlock,
|
|
148
|
+
_ work: @escaping () async throws -> Any?) {
|
|
149
|
+
Task.detached {
|
|
150
|
+
do {
|
|
151
|
+
try SdkSetup.ensureBase()
|
|
152
|
+
resolve(try await work())
|
|
153
|
+
} catch {
|
|
154
|
+
Self.reject(reject, error)
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/// The SDK reports a run stopped by its token as an inference error
|
|
160
|
+
/// ("user_cancelled"); JS gets the dedicated `xybrid_cancelled` code.
|
|
161
|
+
private static func cancellationAware(_ error: Error, _ token: XybridCancellationToken) -> Error {
|
|
162
|
+
guard token.isCancelled(), !(error is BridgeError) else { return error }
|
|
163
|
+
return XybridError.cancelled(message: "The run was cancelled")
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
private static func reject(_ reject: RCTPromiseRejectBlock, _ error: Error) {
|
|
167
|
+
let (code, message) = XybridCodec.rejection(for: error)
|
|
168
|
+
reject(code, message, NSError(domain: "Xybrid", code: 0,
|
|
169
|
+
userInfo: [NSLocalizedDescriptionKey: message]))
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// MARK: - Lifecycle
|
|
173
|
+
|
|
174
|
+
/// Module teardown (reload, host shutdown): stop in-flight work, free all.
|
|
175
|
+
@objc public func invalidate() {
|
|
176
|
+
handles.all(XybridCancellationToken.self).forEach { $0.cancel() }
|
|
177
|
+
handles.disposeAll()
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// MARK: - SDK configuration
|
|
181
|
+
|
|
182
|
+
@objc(initialize:resolve:reject:)
|
|
183
|
+
public func initialize(_ options: NSDictionary?,
|
|
184
|
+
resolve: @escaping RCTPromiseResolveBlock,
|
|
185
|
+
reject: @escaping RCTPromiseRejectBlock) {
|
|
186
|
+
do {
|
|
187
|
+
let object = (options as? JSObject) ?? [:]
|
|
188
|
+
let what = "initialize options"
|
|
189
|
+
let cacheDir = try nonBlank(XybridCodec.optionalString(object, "cacheDir", what))
|
|
190
|
+
try SdkSetup.ensureBase(cacheDir: cacheDir.map(XybridCodec.filePath))
|
|
191
|
+
try SdkSetup.configure(SdkSetup.Runtime(
|
|
192
|
+
apiKey: nonBlank(try XybridCodec.optionalString(object, "apiKey", what)),
|
|
193
|
+
gatewayUrl: nonBlank(try XybridCodec.optionalString(object, "gatewayUrl", what)),
|
|
194
|
+
ingestUrl: nonBlank(try XybridCodec.optionalString(object, "ingestUrl", what))
|
|
195
|
+
))
|
|
196
|
+
resolve(nil)
|
|
197
|
+
} catch {
|
|
198
|
+
Self.reject(reject, error)
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
@objc(sdkVersion:reject:)
|
|
203
|
+
public func sdkVersion(_ resolve: @escaping RCTPromiseResolveBlock,
|
|
204
|
+
reject: @escaping RCTPromiseRejectBlock) {
|
|
205
|
+
now(resolve, reject) { ffiVersion() }
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
@objc(hasApiKey:reject:)
|
|
209
|
+
public func hasApiKey(_ resolve: @escaping RCTPromiseResolveBlock,
|
|
210
|
+
reject: @escaping RCTPromiseRejectBlock) {
|
|
211
|
+
now(resolve, reject) { ffiHasApiKey() }
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
@objc(setProviderApiKey:apiKey:resolve:reject:)
|
|
215
|
+
public func setProviderApiKey(_ provider: String,
|
|
216
|
+
apiKey: String,
|
|
217
|
+
resolve: @escaping RCTPromiseResolveBlock,
|
|
218
|
+
reject: @escaping RCTPromiseRejectBlock) {
|
|
219
|
+
now(resolve, reject) { ffiSetProviderApiKey(provider, apiKey); return nil }
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
@objc(setPlatformUrl:resolve:reject:)
|
|
223
|
+
public func setPlatformUrl(_ url: String,
|
|
224
|
+
resolve: @escaping RCTPromiseResolveBlock,
|
|
225
|
+
reject: @escaping RCTPromiseRejectBlock) {
|
|
226
|
+
now(resolve, reject) { ffiSetPlatformUrl(url); return nil }
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
@objc(setSpeculativeCloud:resolve:reject:)
|
|
230
|
+
public func setSpeculativeCloud(_ enabled: Bool,
|
|
231
|
+
resolve: @escaping RCTPromiseResolveBlock,
|
|
232
|
+
reject: @escaping RCTPromiseRejectBlock) {
|
|
233
|
+
now(resolve, reject) { ffiSetSpeculativeCloud(enabled); return nil }
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
@objc(isSpeculativeCloudEnabled:reject:)
|
|
237
|
+
public func isSpeculativeCloudEnabled(_ resolve: @escaping RCTPromiseResolveBlock,
|
|
238
|
+
reject: @escaping RCTPromiseRejectBlock) {
|
|
239
|
+
now(resolve, reject) { ffiIsSpeculativeCloudEnabled() }
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
@objc(willSpeculate:resolve:reject:)
|
|
243
|
+
public func willSpeculate(_ modelId: String,
|
|
244
|
+
resolve: @escaping RCTPromiseResolveBlock,
|
|
245
|
+
reject: @escaping RCTPromiseRejectBlock) {
|
|
246
|
+
background(resolve, reject) { ffiWillSpeculateForModel(modelId) }
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
@objc(releaseMemory:reject:)
|
|
250
|
+
public func releaseMemory(_ resolve: @escaping RCTPromiseResolveBlock,
|
|
251
|
+
reject: @escaping RCTPromiseRejectBlock) {
|
|
252
|
+
background(resolve, reject) { ffiReleaseMemory() }
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
@objc(setAutoRelease:resolve:reject:)
|
|
256
|
+
public func setAutoRelease(_ enabled: Bool,
|
|
257
|
+
resolve: @escaping RCTPromiseResolveBlock,
|
|
258
|
+
reject: @escaping RCTPromiseRejectBlock) {
|
|
259
|
+
now(resolve, reject) { ffiSetAutoRelease(enabled); return nil }
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
@objc(isAutoReleaseEnabled:reject:)
|
|
263
|
+
public func isAutoReleaseEnabled(_ resolve: @escaping RCTPromiseResolveBlock,
|
|
264
|
+
reject: @escaping RCTPromiseRejectBlock) {
|
|
265
|
+
now(resolve, reject) { ffiIsAutoReleaseEnabled() }
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// MARK: - Device state push
|
|
269
|
+
|
|
270
|
+
@objc(setBatteryLevel:resolve:reject:)
|
|
271
|
+
public func setBatteryLevel(_ percent: Double,
|
|
272
|
+
resolve: @escaping RCTPromiseResolveBlock,
|
|
273
|
+
reject: @escaping RCTPromiseRejectBlock) {
|
|
274
|
+
now(resolve, reject) {
|
|
275
|
+
guard percent.isFinite else {
|
|
276
|
+
throw BridgeError.invalidArgument("battery level must be a finite number")
|
|
277
|
+
}
|
|
278
|
+
ffiSetBatteryLevel(UInt8(max(0, min(100, percent.rounded()))))
|
|
279
|
+
return nil
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
@objc(clearBatteryLevel:reject:)
|
|
284
|
+
public func clearBatteryLevel(_ resolve: @escaping RCTPromiseResolveBlock,
|
|
285
|
+
reject: @escaping RCTPromiseRejectBlock) {
|
|
286
|
+
now(resolve, reject) { ffiClearBatteryLevel(); return nil }
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
@objc(setThermalState:resolve:reject:)
|
|
290
|
+
public func setThermalState(_ state: String,
|
|
291
|
+
resolve: @escaping RCTPromiseResolveBlock,
|
|
292
|
+
reject: @escaping RCTPromiseRejectBlock) {
|
|
293
|
+
now(resolve, reject) {
|
|
294
|
+
let mapped: XybridThermalState
|
|
295
|
+
switch state {
|
|
296
|
+
case "normal": mapped = .normal
|
|
297
|
+
case "warm": mapped = .warm
|
|
298
|
+
case "hot": mapped = .hot
|
|
299
|
+
case "critical": mapped = .critical
|
|
300
|
+
default: throw BridgeError.invalidArgument("unknown thermal state '\(state)'")
|
|
301
|
+
}
|
|
302
|
+
ffiSetThermalState(mapped)
|
|
303
|
+
return nil
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
@objc(clearThermalState:reject:)
|
|
308
|
+
public func clearThermalState(_ resolve: @escaping RCTPromiseResolveBlock,
|
|
309
|
+
reject: @escaping RCTPromiseRejectBlock) {
|
|
310
|
+
now(resolve, reject) { ffiClearThermalState(); return nil }
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
// MARK: - Model cache (disk walks: off the method queue)
|
|
314
|
+
|
|
315
|
+
@objc(cacheStatus:reject:)
|
|
316
|
+
public func cacheStatus(_ resolve: @escaping RCTPromiseResolveBlock,
|
|
317
|
+
reject: @escaping RCTPromiseRejectBlock) {
|
|
318
|
+
background(resolve, reject) { XybridCodec.encodeCacheStatus(try ffiCacheStatus()) }
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
@objc(cacheEntries:reject:)
|
|
322
|
+
public func cacheEntries(_ resolve: @escaping RCTPromiseResolveBlock,
|
|
323
|
+
reject: @escaping RCTPromiseRejectBlock) {
|
|
324
|
+
background(resolve, reject) { try ffiCacheEntries().map(XybridCodec.encodeCacheEntry) }
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
@objc(cacheIsModelCached:resolve:reject:)
|
|
328
|
+
public func cacheIsModelCached(_ modelId: String,
|
|
329
|
+
resolve: @escaping RCTPromiseResolveBlock,
|
|
330
|
+
reject: @escaping RCTPromiseRejectBlock) {
|
|
331
|
+
background(resolve, reject) { try ffiCacheIsModelCached(modelId) }
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
@objc(cacheModelPath:resolve:reject:)
|
|
335
|
+
public func cacheModelPath(_ modelId: String,
|
|
336
|
+
resolve: @escaping RCTPromiseResolveBlock,
|
|
337
|
+
reject: @escaping RCTPromiseRejectBlock) {
|
|
338
|
+
background(resolve, reject) { try ffiCacheModelPath(modelId) }
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
@objc(cacheExtractedModelIds:reject:)
|
|
342
|
+
public func cacheExtractedModelIds(_ resolve: @escaping RCTPromiseResolveBlock,
|
|
343
|
+
reject: @escaping RCTPromiseRejectBlock) {
|
|
344
|
+
background(resolve, reject) { try ffiCacheListExtractedModelIds() }
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
@objc(cacheRemoveModel:resolve:reject:)
|
|
348
|
+
public func cacheRemoveModel(_ modelId: String,
|
|
349
|
+
resolve: @escaping RCTPromiseResolveBlock,
|
|
350
|
+
reject: @escaping RCTPromiseRejectBlock) {
|
|
351
|
+
background(resolve, reject) { try ffiCacheRemoveModel(modelId) }
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
@objc(cacheClear:reject:)
|
|
355
|
+
public func cacheClear(_ resolve: @escaping RCTPromiseResolveBlock,
|
|
356
|
+
reject: @escaping RCTPromiseRejectBlock) {
|
|
357
|
+
background(resolve, reject) { try ffiCacheClear() }
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
// MARK: - Stateless helpers
|
|
361
|
+
|
|
362
|
+
@objc(jsonSchemaToGbnf:resolve:reject:)
|
|
363
|
+
public func jsonSchemaToGbnf(_ schemaJson: String,
|
|
364
|
+
resolve: @escaping RCTPromiseResolveBlock,
|
|
365
|
+
reject: @escaping RCTPromiseRejectBlock) {
|
|
366
|
+
now(resolve, reject) { try ffiJsonSchemaToGbnf(schemaJson) }
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
@objc(toolResultsEnvelope:priorAssistantText:results:resolve:reject:)
|
|
370
|
+
public func toolResultsEnvelope(_ userText: String,
|
|
371
|
+
priorAssistantText: String,
|
|
372
|
+
results: NSArray,
|
|
373
|
+
resolve: @escaping RCTPromiseResolveBlock,
|
|
374
|
+
reject: @escaping RCTPromiseRejectBlock) {
|
|
375
|
+
now(resolve, reject) {
|
|
376
|
+
let decoded = try (results as? [Any] ?? []).map(XybridCodec.decodeToolResult)
|
|
377
|
+
return XybridCodec.encodeEnvelope(try ffiToolResultsEnvelope(userText, priorAssistantText, decoded))
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
// MARK: - Handles
|
|
382
|
+
|
|
383
|
+
@objc(dispose:resolve:reject:)
|
|
384
|
+
public func dispose(_ handle: String,
|
|
385
|
+
resolve: @escaping RCTPromiseResolveBlock,
|
|
386
|
+
reject: @escaping RCTPromiseRejectBlock) {
|
|
387
|
+
handles.dispose(handle)
|
|
388
|
+
resolve(nil)
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
// MARK: - Models
|
|
392
|
+
|
|
393
|
+
@objc(loadModel:resolve:reject:)
|
|
394
|
+
public func loadModel(_ source: NSDictionary,
|
|
395
|
+
resolve: @escaping RCTPromiseResolveBlock,
|
|
396
|
+
reject: @escaping RCTPromiseRejectBlock) {
|
|
397
|
+
background(resolve, reject) { [handles] in
|
|
398
|
+
let object = try XybridCodec.object(source, "model source")
|
|
399
|
+
let what = "model source"
|
|
400
|
+
let kind = try XybridCodec.string(object, "kind", what)
|
|
401
|
+
let value = try XybridCodec.string(object, "value", what)
|
|
402
|
+
let model: XybridModel
|
|
403
|
+
switch kind {
|
|
404
|
+
case "registry":
|
|
405
|
+
model = try XybridModel(fromRegistry: value)
|
|
406
|
+
case "registrySpeculative":
|
|
407
|
+
model = try XybridModel(fromRegistrySpeculative: value)
|
|
408
|
+
case "bundle":
|
|
409
|
+
model = try XybridModel(fromBundle: XybridCodec.filePath(value))
|
|
410
|
+
case "directory":
|
|
411
|
+
model = try XybridModel(fromDirectory: XybridCodec.filePath(value))
|
|
412
|
+
case "huggingFace":
|
|
413
|
+
if let revision = try XybridCodec.optionalString(object, "revision", what) {
|
|
414
|
+
model = try XybridModel(fromHuggingfaceWithRevision: value, revision: revision)
|
|
415
|
+
} else {
|
|
416
|
+
model = try XybridModel(fromHuggingface: value)
|
|
417
|
+
}
|
|
418
|
+
case "modelFile":
|
|
419
|
+
model = try XybridModel(fromModelFile: XybridCodec.filePath(value))
|
|
420
|
+
default:
|
|
421
|
+
throw BridgeError.invalidArgument("unknown model source kind '\(kind)'")
|
|
422
|
+
}
|
|
423
|
+
return handles.insert(model, kind: "model")
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
@objc(modelInfo:resolve:reject:)
|
|
428
|
+
public func modelInfo(_ model: String,
|
|
429
|
+
resolve: @escaping RCTPromiseResolveBlock,
|
|
430
|
+
reject: @escaping RCTPromiseRejectBlock) {
|
|
431
|
+
now(resolve, reject) {
|
|
432
|
+
let m = try handles.get(model, as: XybridModel.self)
|
|
433
|
+
let info: JSObject = [
|
|
434
|
+
"modelId": m.modelId(),
|
|
435
|
+
"version": m.version(),
|
|
436
|
+
"outputType": XybridCodec.encodeOutputType(m.outputType()),
|
|
437
|
+
"isLlm": m.isLlm(),
|
|
438
|
+
"supportsStreaming": m.supportsStreaming(),
|
|
439
|
+
"supportsTokenStreaming": m.supportsTokenStreaming(),
|
|
440
|
+
"supportsToolCalling": m.supportsToolCalling().map { $0 as Any } ?? NSNull(),
|
|
441
|
+
"hasVoices": m.hasVoices(),
|
|
442
|
+
"defaultGenerationConfig": XybridCodec.encodeGenerationConfig(m.defaultGenerationConfig()),
|
|
443
|
+
]
|
|
444
|
+
return info
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
@objc(isLoaded:resolve:reject:)
|
|
449
|
+
public func isLoaded(_ model: String,
|
|
450
|
+
resolve: @escaping RCTPromiseResolveBlock,
|
|
451
|
+
reject: @escaping RCTPromiseRejectBlock) {
|
|
452
|
+
now(resolve, reject) { try handles.get(model, as: XybridModel.self).isLoaded() }
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
@objc(warmup:resolve:reject:)
|
|
456
|
+
public func warmup(_ model: String,
|
|
457
|
+
resolve: @escaping RCTPromiseResolveBlock,
|
|
458
|
+
reject: @escaping RCTPromiseRejectBlock) {
|
|
459
|
+
background(resolve, reject) { [handles] in
|
|
460
|
+
try handles.get(model, as: XybridModel.self).warmup()
|
|
461
|
+
return nil
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
@objc(unload:resolve:reject:)
|
|
466
|
+
public func unload(_ model: String,
|
|
467
|
+
resolve: @escaping RCTPromiseResolveBlock,
|
|
468
|
+
reject: @escaping RCTPromiseRejectBlock) {
|
|
469
|
+
background(resolve, reject) { [handles] in
|
|
470
|
+
try handles.get(model, as: XybridModel.self).unload()
|
|
471
|
+
return nil
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
@objc(isCloudServing:resolve:reject:)
|
|
476
|
+
public func isCloudServing(_ model: String,
|
|
477
|
+
resolve: @escaping RCTPromiseResolveBlock,
|
|
478
|
+
reject: @escaping RCTPromiseRejectBlock) {
|
|
479
|
+
now(resolve, reject) { try handles.get(model, as: XybridModel.self).isCloudServing() }
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
@objc(downloadStatus:resolve:reject:)
|
|
483
|
+
public func downloadStatus(_ model: String,
|
|
484
|
+
resolve: @escaping RCTPromiseResolveBlock,
|
|
485
|
+
reject: @escaping RCTPromiseRejectBlock) {
|
|
486
|
+
now(resolve, reject) {
|
|
487
|
+
XybridCodec.encodeDownloadStatus(try handles.get(model, as: XybridModel.self).downloadStatus())
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
@objc(awaitDownload:timeoutMs:resolve:reject:)
|
|
492
|
+
public func awaitDownload(_ model: String,
|
|
493
|
+
timeoutMs: Double,
|
|
494
|
+
resolve: @escaping RCTPromiseResolveBlock,
|
|
495
|
+
reject: @escaping RCTPromiseRejectBlock) {
|
|
496
|
+
background(resolve, reject) { [handles] in
|
|
497
|
+
let m = try handles.get(model, as: XybridModel.self)
|
|
498
|
+
return XybridCodec.encodeDownloadStatus(m.awaitDownload(timeoutMs: clampTimeoutMs(timeoutMs)))
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
@objc(voices:resolve:reject:)
|
|
503
|
+
public func voices(_ model: String,
|
|
504
|
+
resolve: @escaping RCTPromiseResolveBlock,
|
|
505
|
+
reject: @escaping RCTPromiseRejectBlock) {
|
|
506
|
+
now(resolve, reject) {
|
|
507
|
+
try handles.get(model, as: XybridModel.self).voices().map(XybridCodec.encodeVoice)
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
@objc(defaultVoice:resolve:reject:)
|
|
512
|
+
public func defaultVoice(_ model: String,
|
|
513
|
+
resolve: @escaping RCTPromiseResolveBlock,
|
|
514
|
+
reject: @escaping RCTPromiseRejectBlock) {
|
|
515
|
+
now(resolve, reject) {
|
|
516
|
+
try handles.get(model, as: XybridModel.self).defaultVoice().map(XybridCodec.encodeVoice)
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
@objc(voice:voiceId:resolve:reject:)
|
|
521
|
+
public func voice(_ model: String,
|
|
522
|
+
voiceId: String,
|
|
523
|
+
resolve: @escaping RCTPromiseResolveBlock,
|
|
524
|
+
reject: @escaping RCTPromiseRejectBlock) {
|
|
525
|
+
now(resolve, reject) {
|
|
526
|
+
try handles.get(model, as: XybridModel.self).voice(voiceId: voiceId).map(XybridCodec.encodeVoice)
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
// MARK: - Inference
|
|
531
|
+
|
|
532
|
+
/// Resolve the handles a run request carries. Without a caller token a
|
|
533
|
+
/// batch run still gets one, registered under the model so disposing the
|
|
534
|
+
/// model (or tearing the module down) stops it; a stream's own entry plays
|
|
535
|
+
/// that role instead, so streams skip the registration.
|
|
536
|
+
private func prepareRun(_ modelHandle: String, _ options: NSDictionary?, registerToken: Bool) throws
|
|
537
|
+
-> (model: XybridModel, request: XybridCodec.RunRequest,
|
|
538
|
+
context: XybridConversationContext?, cancel: XybridCancellationToken, ownToken: String?)
|
|
539
|
+
{
|
|
540
|
+
let model = try handles.get(modelHandle, as: XybridModel.self)
|
|
541
|
+
let request = try XybridCodec.decodeRunRequest(options)
|
|
542
|
+
let context = try request.context.map { try handles.get($0, as: XybridConversationContext.self) }
|
|
543
|
+
if let token = request.cancel {
|
|
544
|
+
return (model, request, context, try handles.get(token, as: XybridCancellationToken.self), nil)
|
|
545
|
+
}
|
|
546
|
+
let token = XybridCancellationToken()
|
|
547
|
+
let handle = registerToken
|
|
548
|
+
? handles.insert(token, kind: "cancel", owner: modelHandle, onDispose: { token.cancel() })
|
|
549
|
+
: nil
|
|
550
|
+
return (model, request, context, token, handle)
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
@objc(run:envelope:options:resolve:reject:)
|
|
554
|
+
public func run(_ model: String,
|
|
555
|
+
envelope: NSDictionary,
|
|
556
|
+
options: NSDictionary?,
|
|
557
|
+
resolve: @escaping RCTPromiseResolveBlock,
|
|
558
|
+
reject: @escaping RCTPromiseRejectBlock) {
|
|
559
|
+
background(resolve, reject) { [handles] in
|
|
560
|
+
let input = try XybridCodec.decodeEnvelope(envelope)
|
|
561
|
+
let run = try self.prepareRun(model, options, registerToken: true)
|
|
562
|
+
defer { if let own = run.ownToken { handles.dispose(own) } }
|
|
563
|
+
do {
|
|
564
|
+
let result: XybridResult
|
|
565
|
+
if let context = run.context {
|
|
566
|
+
result = try run.model.runWithContext(envelope: input, context: context,
|
|
567
|
+
options: run.request.options, cancel: run.cancel)
|
|
568
|
+
} else {
|
|
569
|
+
result = try run.model.run(envelope: input, options: run.request.options, cancel: run.cancel)
|
|
570
|
+
}
|
|
571
|
+
return XybridCodec.encodeResult(result)
|
|
572
|
+
} catch {
|
|
573
|
+
throw Self.cancellationAware(error, run.cancel)
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
@objc(streamStart:envelope:options:resolve:reject:)
|
|
579
|
+
public func streamStart(_ model: String,
|
|
580
|
+
envelope: NSDictionary,
|
|
581
|
+
options: NSDictionary?,
|
|
582
|
+
resolve: @escaping RCTPromiseResolveBlock,
|
|
583
|
+
reject: @escaping RCTPromiseRejectBlock) {
|
|
584
|
+
background(resolve, reject) { [handles] in
|
|
585
|
+
let input = try XybridCodec.decodeEnvelope(envelope)
|
|
586
|
+
let run = try self.prepareRun(model, options, registerToken: false)
|
|
587
|
+
let streamId: UInt64
|
|
588
|
+
if let context = run.context {
|
|
589
|
+
streamId = try run.model.runStreamWithContext(envelope: input, context: context,
|
|
590
|
+
options: run.request.options, cancel: run.cancel)
|
|
591
|
+
} else {
|
|
592
|
+
streamId = try run.model.runStream(envelope: input, options: run.request.options, cancel: run.cancel)
|
|
593
|
+
}
|
|
594
|
+
let entry = XybridTokenStreamEntry(model: run.model, streamId: streamId, cancel: run.cancel)
|
|
595
|
+
return handles.insert(entry, kind: "stream", owner: model, onDispose: { entry.abort() })
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
@objc(streamNext:resolve:reject:)
|
|
600
|
+
public func streamNext(_ stream: String,
|
|
601
|
+
resolve: @escaping RCTPromiseResolveBlock,
|
|
602
|
+
reject: @escaping RCTPromiseRejectBlock) {
|
|
603
|
+
background(resolve, reject) { [handles] in
|
|
604
|
+
// A disposed stream reads as exhausted, not as an error.
|
|
605
|
+
guard let entry = handles.find(stream, as: XybridTokenStreamEntry.self) else { return nil }
|
|
606
|
+
do {
|
|
607
|
+
let event = try entry.model.streamNext(streamId: entry.streamId)
|
|
608
|
+
switch event.kind {
|
|
609
|
+
case .token:
|
|
610
|
+
guard let token = event.token else {
|
|
611
|
+
throw XybridError.inferenceError(message: "stream returned a token event without a token")
|
|
612
|
+
}
|
|
613
|
+
return ["kind": "token", "token": XybridCodec.encodeStreamToken(token)] as JSObject
|
|
614
|
+
case .complete:
|
|
615
|
+
// `streamResult` closes the bolt session; drop our entry with it.
|
|
616
|
+
let result = try entry.model.streamResult(streamId: entry.streamId)
|
|
617
|
+
handles.dispose(stream)
|
|
618
|
+
return ["kind": "complete", "result": XybridCodec.encodeResult(result)] as JSObject
|
|
619
|
+
}
|
|
620
|
+
} catch {
|
|
621
|
+
// A failed pull already closed the bolt session.
|
|
622
|
+
handles.dispose(stream)
|
|
623
|
+
throw Self.cancellationAware(error, entry.cancel)
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
// MARK: - Cancellation tokens
|
|
629
|
+
|
|
630
|
+
@objc(createCancelToken:reject:)
|
|
631
|
+
public func createCancelToken(_ resolve: @escaping RCTPromiseResolveBlock,
|
|
632
|
+
reject: @escaping RCTPromiseRejectBlock) {
|
|
633
|
+
now(resolve, reject) { handles.insert(XybridCancellationToken(), kind: "cancel") }
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
@objc(cancel:resolve:reject:)
|
|
637
|
+
public func cancel(_ token: String,
|
|
638
|
+
resolve: @escaping RCTPromiseResolveBlock,
|
|
639
|
+
reject: @escaping RCTPromiseRejectBlock) {
|
|
640
|
+
now(resolve, reject) {
|
|
641
|
+
try handles.get(token, as: XybridCancellationToken.self).cancel()
|
|
642
|
+
return nil
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
// MARK: - Conversation contexts
|
|
647
|
+
|
|
648
|
+
@objc(createContext:resolve:reject:)
|
|
649
|
+
public func createContext(_ contextId: String?,
|
|
650
|
+
resolve: @escaping RCTPromiseResolveBlock,
|
|
651
|
+
reject: @escaping RCTPromiseRejectBlock) {
|
|
652
|
+
now(resolve, reject) {
|
|
653
|
+
let context = contextId.map { XybridConversationContext(withId: $0) } ?? XybridConversationContext()
|
|
654
|
+
return handles.insert(context, kind: "context")
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
@objc(contextPush:envelope:resolve:reject:)
|
|
659
|
+
public func contextPush(_ context: String,
|
|
660
|
+
envelope: NSDictionary,
|
|
661
|
+
resolve: @escaping RCTPromiseResolveBlock,
|
|
662
|
+
reject: @escaping RCTPromiseRejectBlock) {
|
|
663
|
+
// Image turns are decode-validated natively, so this can take a moment.
|
|
664
|
+
background(resolve, reject) { [handles] in
|
|
665
|
+
try handles.get(context, as: XybridConversationContext.self)
|
|
666
|
+
.push(envelope: try XybridCodec.decodeEnvelope(envelope))
|
|
667
|
+
return nil
|
|
668
|
+
}
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
@objc(contextSetSystem:envelope:resolve:reject:)
|
|
672
|
+
public func contextSetSystem(_ context: String,
|
|
673
|
+
envelope: NSDictionary,
|
|
674
|
+
resolve: @escaping RCTPromiseResolveBlock,
|
|
675
|
+
reject: @escaping RCTPromiseRejectBlock) {
|
|
676
|
+
background(resolve, reject) { [handles] in
|
|
677
|
+
try handles.get(context, as: XybridConversationContext.self)
|
|
678
|
+
.setSystem(envelope: try XybridCodec.decodeEnvelope(envelope))
|
|
679
|
+
return nil
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
@objc(contextClear:resolve:reject:)
|
|
684
|
+
public func contextClear(_ context: String,
|
|
685
|
+
resolve: @escaping RCTPromiseResolveBlock,
|
|
686
|
+
reject: @escaping RCTPromiseRejectBlock) {
|
|
687
|
+
now(resolve, reject) {
|
|
688
|
+
try handles.get(context, as: XybridConversationContext.self).clear()
|
|
689
|
+
return nil
|
|
690
|
+
}
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
@objc(contextInfo:resolve:reject:)
|
|
694
|
+
public func contextInfo(_ context: String,
|
|
695
|
+
resolve: @escaping RCTPromiseResolveBlock,
|
|
696
|
+
reject: @escaping RCTPromiseRejectBlock) {
|
|
697
|
+
now(resolve, reject) {
|
|
698
|
+
let c = try handles.get(context, as: XybridConversationContext.self)
|
|
699
|
+
return ["id": c.id(), "historyLength": c.historyLen(), "hasSystem": c.hasSystem()] as JSObject
|
|
700
|
+
}
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
@objc(contextHistory:resolve:reject:)
|
|
704
|
+
public func contextHistory(_ context: String,
|
|
705
|
+
resolve: @escaping RCTPromiseResolveBlock,
|
|
706
|
+
reject: @escaping RCTPromiseRejectBlock) {
|
|
707
|
+
now(resolve, reject) {
|
|
708
|
+
try handles.get(context, as: XybridConversationContext.self).history().map(XybridCodec.encodeEnvelope)
|
|
709
|
+
}
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
@objc(contextSetMaxHistoryLength:length:resolve:reject:)
|
|
713
|
+
public func contextSetMaxHistoryLength(_ context: String,
|
|
714
|
+
length: Double,
|
|
715
|
+
resolve: @escaping RCTPromiseResolveBlock,
|
|
716
|
+
reject: @escaping RCTPromiseRejectBlock) {
|
|
717
|
+
now(resolve, reject) {
|
|
718
|
+
let c = try handles.get(context, as: XybridConversationContext.self)
|
|
719
|
+
c.setMaxHistoryLen(len: try XybridCodec.uint32(length, "max history length"))
|
|
720
|
+
return nil
|
|
721
|
+
}
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
// MARK: - Standalone downloads
|
|
725
|
+
|
|
726
|
+
@objc(startDownload:platform:resolve:reject:)
|
|
727
|
+
public func startDownload(_ modelId: String,
|
|
728
|
+
platform: String?,
|
|
729
|
+
resolve: @escaping RCTPromiseResolveBlock,
|
|
730
|
+
reject: @escaping RCTPromiseRejectBlock) {
|
|
731
|
+
background(resolve, reject) { [handles] in
|
|
732
|
+
let download = platform.map { XybridDownload(fromRegistryWithPlatform: modelId, platform: $0) }
|
|
733
|
+
?? XybridDownload(fromRegistry: modelId)
|
|
734
|
+
return handles.insert(download, kind: "download")
|
|
735
|
+
}
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
@objc(downloadHandleStatus:resolve:reject:)
|
|
739
|
+
public func downloadHandleStatus(_ download: String,
|
|
740
|
+
resolve: @escaping RCTPromiseResolveBlock,
|
|
741
|
+
reject: @escaping RCTPromiseRejectBlock) {
|
|
742
|
+
now(resolve, reject) {
|
|
743
|
+
XybridCodec.encodeDownloadStatus(try handles.get(download, as: XybridDownload.self).status())
|
|
744
|
+
}
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
@objc(downloadHandleError:resolve:reject:)
|
|
748
|
+
public func downloadHandleError(_ download: String,
|
|
749
|
+
resolve: @escaping RCTPromiseResolveBlock,
|
|
750
|
+
reject: @escaping RCTPromiseRejectBlock) {
|
|
751
|
+
now(resolve, reject) { try handles.get(download, as: XybridDownload.self).error() }
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
@objc(cancelDownload:resolve:reject:)
|
|
755
|
+
public func cancelDownload(_ download: String,
|
|
756
|
+
resolve: @escaping RCTPromiseResolveBlock,
|
|
757
|
+
reject: @escaping RCTPromiseRejectBlock) {
|
|
758
|
+
now(resolve, reject) {
|
|
759
|
+
try handles.get(download, as: XybridDownload.self).cancel()
|
|
760
|
+
return nil
|
|
761
|
+
}
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
// MARK: - Pipelines
|
|
765
|
+
|
|
766
|
+
@objc(loadPipeline:resolve:reject:)
|
|
767
|
+
public func loadPipeline(_ source: NSDictionary,
|
|
768
|
+
resolve: @escaping RCTPromiseResolveBlock,
|
|
769
|
+
reject: @escaping RCTPromiseRejectBlock) {
|
|
770
|
+
background(resolve, reject) { [handles] in
|
|
771
|
+
let object = try XybridCodec.object(source, "pipeline source")
|
|
772
|
+
let kind = try XybridCodec.string(object, "kind", "pipeline source")
|
|
773
|
+
let value = try XybridCodec.string(object, "value", "pipeline source")
|
|
774
|
+
let pipeline: XybridPipeline
|
|
775
|
+
switch kind {
|
|
776
|
+
case "yaml": pipeline = try XybridPipeline(fromYaml: value)
|
|
777
|
+
case "file": pipeline = try XybridPipeline(fromFile: XybridCodec.filePath(value))
|
|
778
|
+
case "bundle": pipeline = try XybridPipeline(fromBundle: XybridCodec.filePath(value))
|
|
779
|
+
default: throw BridgeError.invalidArgument("unknown pipeline source kind '\(kind)'")
|
|
780
|
+
}
|
|
781
|
+
return handles.insert(pipeline, kind: "pipeline")
|
|
782
|
+
}
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
@objc(pipelineInfo:resolve:reject:)
|
|
786
|
+
public func pipelineInfo(_ pipeline: String,
|
|
787
|
+
resolve: @escaping RCTPromiseResolveBlock,
|
|
788
|
+
reject: @escaping RCTPromiseRejectBlock) {
|
|
789
|
+
now(resolve, reject) {
|
|
790
|
+
let p = try handles.get(pipeline, as: XybridPipeline.self)
|
|
791
|
+
return [
|
|
792
|
+
"name": p.name().map { $0 as Any } ?? NSNull(),
|
|
793
|
+
"stageNames": p.stageNames(),
|
|
794
|
+
"stageCount": p.stageCount(),
|
|
795
|
+
] as JSObject
|
|
796
|
+
}
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
@objc(runPipeline:envelope:options:resolve:reject:)
|
|
800
|
+
public func runPipeline(_ pipeline: String,
|
|
801
|
+
envelope: NSDictionary,
|
|
802
|
+
options: NSDictionary?,
|
|
803
|
+
resolve: @escaping RCTPromiseResolveBlock,
|
|
804
|
+
reject: @escaping RCTPromiseRejectBlock) {
|
|
805
|
+
background(resolve, reject) { [handles] in
|
|
806
|
+
let p = try handles.get(pipeline, as: XybridPipeline.self)
|
|
807
|
+
let result = try p.run(envelope: try XybridCodec.decodeEnvelope(envelope),
|
|
808
|
+
options: try XybridCodec.decodeRunRequest(options).options)
|
|
809
|
+
return XybridCodec.encodePipelineResult(result)
|
|
810
|
+
}
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
// MARK: - Live ASR sessions
|
|
814
|
+
|
|
815
|
+
@objc(openStreamingSession:config:resolve:reject:)
|
|
816
|
+
public func openStreamingSession(_ model: String,
|
|
817
|
+
config: NSDictionary?,
|
|
818
|
+
resolve: @escaping RCTPromiseResolveBlock,
|
|
819
|
+
reject: @escaping RCTPromiseRejectBlock) {
|
|
820
|
+
// Opening warms the weights, so it runs off the method queue.
|
|
821
|
+
background(resolve, reject) { [handles] in
|
|
822
|
+
let m = try handles.get(model, as: XybridModel.self)
|
|
823
|
+
let session = try XybridStreamingSession(forModel: m, config: try XybridCodec.decodeStreamingConfig(config))
|
|
824
|
+
let entry = XybridSessionEntry(session: session)
|
|
825
|
+
return handles.insert(entry, kind: "session", onDispose: { session.cancel() })
|
|
826
|
+
}
|
|
827
|
+
}
|
|
828
|
+
|
|
829
|
+
@objc(sessionFeed:samplesBase64:resolve:reject:)
|
|
830
|
+
public func sessionFeed(_ session: String,
|
|
831
|
+
samplesBase64: String,
|
|
832
|
+
resolve: @escaping RCTPromiseResolveBlock,
|
|
833
|
+
reject: @escaping RCTPromiseRejectBlock) {
|
|
834
|
+
// `feed` blocks only when the worker's queue is full (back-pressure).
|
|
835
|
+
background(resolve, reject) { [handles] in
|
|
836
|
+
let entry = try handles.get(session, as: XybridSessionEntry.self)
|
|
837
|
+
try entry.session.feed(samples: try XybridCodec.float32Samples(samplesBase64))
|
|
838
|
+
return nil
|
|
839
|
+
}
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
@objc(sessionNextPartial:resolve:reject:)
|
|
843
|
+
public func sessionNextPartial(_ session: String,
|
|
844
|
+
resolve: @escaping RCTPromiseResolveBlock,
|
|
845
|
+
reject: @escaping RCTPromiseRejectBlock) {
|
|
846
|
+
background(resolve, reject) { [handles] in
|
|
847
|
+
guard let entry = handles.find(session, as: XybridSessionEntry.self) else { return nil }
|
|
848
|
+
return await entry.nextPartial().map(XybridCodec.encodePartial)
|
|
849
|
+
}
|
|
850
|
+
}
|
|
851
|
+
|
|
852
|
+
@objc(sessionFlush:resolve:reject:)
|
|
853
|
+
public func sessionFlush(_ session: String,
|
|
854
|
+
resolve: @escaping RCTPromiseResolveBlock,
|
|
855
|
+
reject: @escaping RCTPromiseRejectBlock) {
|
|
856
|
+
background(resolve, reject) { [handles] in
|
|
857
|
+
try handles.get(session, as: XybridSessionEntry.self).session.flush()
|
|
858
|
+
}
|
|
859
|
+
}
|
|
860
|
+
|
|
861
|
+
@objc(sessionReset:resolve:reject:)
|
|
862
|
+
public func sessionReset(_ session: String,
|
|
863
|
+
resolve: @escaping RCTPromiseResolveBlock,
|
|
864
|
+
reject: @escaping RCTPromiseRejectBlock) {
|
|
865
|
+
background(resolve, reject) { [handles] in
|
|
866
|
+
try handles.get(session, as: XybridSessionEntry.self).session.reset()
|
|
867
|
+
return nil
|
|
868
|
+
}
|
|
869
|
+
}
|
|
870
|
+
|
|
871
|
+
@objc(sessionCancel:resolve:reject:)
|
|
872
|
+
public func sessionCancel(_ session: String,
|
|
873
|
+
resolve: @escaping RCTPromiseResolveBlock,
|
|
874
|
+
reject: @escaping RCTPromiseRejectBlock) {
|
|
875
|
+
now(resolve, reject) {
|
|
876
|
+
try handles.get(session, as: XybridSessionEntry.self).session.cancel()
|
|
877
|
+
return nil
|
|
878
|
+
}
|
|
879
|
+
}
|
|
880
|
+
|
|
881
|
+
@objc(sessionIsRunning:resolve:reject:)
|
|
882
|
+
public func sessionIsRunning(_ session: String,
|
|
883
|
+
resolve: @escaping RCTPromiseResolveBlock,
|
|
884
|
+
reject: @escaping RCTPromiseRejectBlock) {
|
|
885
|
+
now(resolve, reject) { try handles.get(session, as: XybridSessionEntry.self).session.isRunning() }
|
|
886
|
+
}
|
|
887
|
+
}
|