@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,481 @@
|
|
|
1
|
+
package ai.xybrid.reactnative
|
|
2
|
+
|
|
3
|
+
import ai.xybrid.XybridAbortSignal
|
|
4
|
+
import ai.xybrid.XybridCacheEntry
|
|
5
|
+
import ai.xybrid.XybridCacheEntryLocation
|
|
6
|
+
import ai.xybrid.XybridCacheStatus
|
|
7
|
+
import ai.xybrid.XybridDownloadState
|
|
8
|
+
import ai.xybrid.XybridDownloadStatus
|
|
9
|
+
import ai.xybrid.XybridEnvelope
|
|
10
|
+
import ai.xybrid.XybridEnvelopeKind
|
|
11
|
+
import ai.xybrid.XybridError
|
|
12
|
+
import ai.xybrid.XybridExecutionTarget
|
|
13
|
+
import ai.xybrid.XybridGenerationConfig
|
|
14
|
+
import ai.xybrid.XybridInferenceMetrics
|
|
15
|
+
import ai.xybrid.XybridMetadataEntry
|
|
16
|
+
import ai.xybrid.XybridOutputType
|
|
17
|
+
import ai.xybrid.XybridPartialResult
|
|
18
|
+
import ai.xybrid.XybridPipelineResult
|
|
19
|
+
import ai.xybrid.XybridResult
|
|
20
|
+
import ai.xybrid.XybridRunOptions
|
|
21
|
+
import ai.xybrid.XybridStageResult
|
|
22
|
+
import ai.xybrid.XybridStreamToken
|
|
23
|
+
import ai.xybrid.XybridStreamingConfig
|
|
24
|
+
import ai.xybrid.XybridToolCall
|
|
25
|
+
import ai.xybrid.XybridToolDefinition
|
|
26
|
+
import ai.xybrid.XybridToolResult
|
|
27
|
+
import ai.xybrid.XybridVadMode
|
|
28
|
+
import ai.xybrid.XybridVoiceInfo
|
|
29
|
+
import android.util.Base64
|
|
30
|
+
import java.nio.ByteBuffer
|
|
31
|
+
import java.nio.ByteOrder
|
|
32
|
+
|
|
33
|
+
// Translation between the bolt Kotlin types and plain Kotlin maps and lists.
|
|
34
|
+
//
|
|
35
|
+
// Pure functions only — no SDK calls, no React Native types — so
|
|
36
|
+
// tests/kotlin runs them on a plain JVM against the real bolt records.
|
|
37
|
+
// XybridModule converts at the edge: `ReadableMap.toHashMap()` in,
|
|
38
|
+
// `Arguments.makeNativeMap()` out. The wire shapes are defined once, in
|
|
39
|
+
// src/wire.ts; envelopes are a payload kind plus string metadata, exactly
|
|
40
|
+
// like `XybridEnvelope`, because every convenience field was already folded
|
|
41
|
+
// into metadata on the JS side.
|
|
42
|
+
//
|
|
43
|
+
// Every number crosses as a Double (JS numbers), and unsigned values are
|
|
44
|
+
// converted explicitly: `Arguments` cannot marshal Kotlin's UInt/ULong.
|
|
45
|
+
|
|
46
|
+
/** A JS object as `ReadableMap.toHashMap()` hands it over. */
|
|
47
|
+
internal typealias JsObject = Map<String, Any?>
|
|
48
|
+
|
|
49
|
+
/** A failure raised by the bridge itself rather than the SDK. */
|
|
50
|
+
internal sealed class BridgeException(message: String) : RuntimeException(message) {
|
|
51
|
+
/** A JS argument could not be decoded. → `xybrid_invalid_argument` */
|
|
52
|
+
class InvalidArgument(message: String) : BridgeException(message)
|
|
53
|
+
|
|
54
|
+
/** Unknown, released, or wrong-kind handle. → `xybrid_handle` */
|
|
55
|
+
class Handle(message: String) : BridgeException(message)
|
|
56
|
+
|
|
57
|
+
/** A configuration conflict. → `xybrid_config_error` */
|
|
58
|
+
class Config(message: String) : BridgeException(message)
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
internal object XybridCodec {
|
|
62
|
+
// -- Reading JS values --------------------------------------------------------
|
|
63
|
+
|
|
64
|
+
fun obj(any: Any?, what: String): JsObject {
|
|
65
|
+
@Suppress("UNCHECKED_CAST")
|
|
66
|
+
return any as? Map<String, Any?> ?: throw BridgeException.InvalidArgument("$what must be an object")
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
fun optionalObj(o: JsObject, key: String, what: String): JsObject? = o[key]?.let { obj(it, "$what: '$key'") }
|
|
70
|
+
|
|
71
|
+
fun string(o: JsObject, key: String, what: String): String =
|
|
72
|
+
o[key] as? String ?: throw BridgeException.InvalidArgument("$what: '$key' must be a string")
|
|
73
|
+
|
|
74
|
+
fun optionalString(o: JsObject, key: String, what: String): String? {
|
|
75
|
+
val raw = o[key] ?: return null
|
|
76
|
+
return raw as? String ?: throw BridgeException.InvalidArgument("$what: '$key' must be a string")
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
fun optionalBoolean(o: JsObject, key: String, what: String): Boolean? {
|
|
80
|
+
val raw = o[key] ?: return null
|
|
81
|
+
return raw as? Boolean ?: throw BridgeException.InvalidArgument("$what: '$key' must be a boolean")
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
fun optionalDouble(o: JsObject, key: String, what: String): Double? {
|
|
85
|
+
val raw = o[key] ?: return null
|
|
86
|
+
val number = (raw as? Number)?.toDouble()
|
|
87
|
+
if (number == null || !number.isFinite()) {
|
|
88
|
+
throw BridgeException.InvalidArgument("$what: '$key' must be a finite number")
|
|
89
|
+
}
|
|
90
|
+
return number
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
fun optionalFloat(o: JsObject, key: String, what: String): Float? = optionalDouble(o, key, what)?.toFloat()
|
|
94
|
+
|
|
95
|
+
/** A non-negative integer that fits in `UInt`; fractions truncate. */
|
|
96
|
+
fun optionalUInt(o: JsObject, key: String, what: String): UInt? =
|
|
97
|
+
optionalDouble(o, key, what)?.let { uint(it, "$what: '$key'") }
|
|
98
|
+
|
|
99
|
+
fun uint(value: Double, what: String): UInt {
|
|
100
|
+
if (!value.isFinite() || value < 0 || value > UInt.MAX_VALUE.toDouble()) {
|
|
101
|
+
throw BridgeException.InvalidArgument("$what must be between 0 and ${UInt.MAX_VALUE}")
|
|
102
|
+
}
|
|
103
|
+
return value.toLong().toUInt()
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
fun stringList(o: JsObject, key: String, what: String): List<String> {
|
|
107
|
+
val raw = o[key] ?: return emptyList()
|
|
108
|
+
val list = raw as? List<*> ?: throw BridgeException.InvalidArgument("$what: '$key' must be an array of strings")
|
|
109
|
+
return list.map { it as? String ?: throw BridgeException.InvalidArgument("$what: '$key' must be an array of strings") }
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
fun base64(o: JsObject, key: String, what: String): ByteArray =
|
|
113
|
+
decodeBase64(string(o, key, what)) ?: throw BridgeException.InvalidArgument("$what: '$key' is not valid base64")
|
|
114
|
+
|
|
115
|
+
private val base64Shape = Regex("^[A-Za-z0-9+/]*={0,2}$")
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Strict standard base64 (whitespace ignored), or `null`. Android's own
|
|
119
|
+
* decoder silently skips characters outside the alphabet, which would turn
|
|
120
|
+
* garbage into an empty payload instead of an error — and disagree with iOS.
|
|
121
|
+
*/
|
|
122
|
+
fun decodeBase64(encoded: String): ByteArray? {
|
|
123
|
+
val compact = encoded.filterNot(Char::isWhitespace)
|
|
124
|
+
if (compact.length % 4 != 0 || !base64Shape.matches(compact)) return null
|
|
125
|
+
return try {
|
|
126
|
+
Base64.decode(compact, Base64.NO_WRAP)
|
|
127
|
+
} catch (e: IllegalArgumentException) {
|
|
128
|
+
null
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/** Accept plain paths and `file://` URLs (what Expo's file APIs hand out). */
|
|
133
|
+
fun filePath(raw: String): String =
|
|
134
|
+
if (raw.startsWith("file://")) java.net.URI(raw).path ?: raw else raw
|
|
135
|
+
|
|
136
|
+
/** Little-endian Float32 PCM from base64. */
|
|
137
|
+
fun float32Samples(encoded: String): FloatArray {
|
|
138
|
+
val bytes = decodeBase64(encoded) ?: throw BridgeException.InvalidArgument("samples are not valid base64")
|
|
139
|
+
if (bytes.size % 4 != 0) {
|
|
140
|
+
throw BridgeException.InvalidArgument("samples must be whole Float32 values (${bytes.size} bytes)")
|
|
141
|
+
}
|
|
142
|
+
val floats = FloatArray(bytes.size / 4)
|
|
143
|
+
ByteBuffer.wrap(bytes).order(ByteOrder.LITTLE_ENDIAN).asFloatBuffer().get(floats)
|
|
144
|
+
return floats
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// -- Envelopes ------------------------------------------------------------------
|
|
148
|
+
|
|
149
|
+
fun decodeEnvelope(any: Any?): XybridEnvelope {
|
|
150
|
+
val o = obj(any, "envelope")
|
|
151
|
+
val kind = string(o, "kind", "envelope")
|
|
152
|
+
val what = "$kind envelope"
|
|
153
|
+
val payload = when (kind) {
|
|
154
|
+
"text" -> XybridEnvelopeKind.Text(string(o, "text", what))
|
|
155
|
+
"audio" -> XybridEnvelopeKind.Audio(base64(o, "bytesBase64", what))
|
|
156
|
+
"embedding" -> {
|
|
157
|
+
val numbers = o["data"] as? List<*>
|
|
158
|
+
?: throw BridgeException.InvalidArgument("$what: 'data' must be an array of numbers")
|
|
159
|
+
XybridEnvelopeKind.Embedding(
|
|
160
|
+
FloatArray(numbers.size) { i ->
|
|
161
|
+
(numbers[i] as? Number)?.toFloat()
|
|
162
|
+
?: throw BridgeException.InvalidArgument("$what: 'data' must be an array of numbers")
|
|
163
|
+
},
|
|
164
|
+
)
|
|
165
|
+
}
|
|
166
|
+
"image" -> XybridEnvelopeKind.Image(base64(o, "bytesBase64", what), string(o, "format", what))
|
|
167
|
+
"multipart" -> {
|
|
168
|
+
val parts = o["parts"] as? List<*>
|
|
169
|
+
?: throw BridgeException.InvalidArgument("$what: 'parts' must be an array")
|
|
170
|
+
XybridEnvelopeKind.MultiPart(parts.map(::decodeEnvelope))
|
|
171
|
+
}
|
|
172
|
+
else -> throw BridgeException.InvalidArgument("unknown envelope kind '$kind'")
|
|
173
|
+
}
|
|
174
|
+
return XybridEnvelope(payload, decodeMetadata(o["metadata"]))
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
fun decodeMetadata(any: Any?): List<XybridMetadataEntry> {
|
|
178
|
+
if (any == null) return emptyList()
|
|
179
|
+
val o = any as? Map<*, *>
|
|
180
|
+
?: throw BridgeException.InvalidArgument("envelope metadata must be an object of strings")
|
|
181
|
+
return o.keys.map { it.toString() }.sorted().map { key ->
|
|
182
|
+
val value = o[key] as? String
|
|
183
|
+
?: throw BridgeException.InvalidArgument("envelope metadata '$key' must be a string")
|
|
184
|
+
XybridMetadataEntry(key, value)
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
fun encodeEnvelope(envelope: XybridEnvelope): Map<String, Any?> {
|
|
189
|
+
val out = linkedMapOf<String, Any?>()
|
|
190
|
+
when (val kind = envelope.kind) {
|
|
191
|
+
is XybridEnvelopeKind.Text -> {
|
|
192
|
+
out["kind"] = "text"
|
|
193
|
+
out["text"] = kind.text
|
|
194
|
+
}
|
|
195
|
+
is XybridEnvelopeKind.Audio -> {
|
|
196
|
+
out["kind"] = "audio"
|
|
197
|
+
out["bytesBase64"] = Base64.encodeToString(kind.bytes, Base64.NO_WRAP)
|
|
198
|
+
}
|
|
199
|
+
is XybridEnvelopeKind.Embedding -> {
|
|
200
|
+
out["kind"] = "embedding"
|
|
201
|
+
out["data"] = kind.values.map { it.toDouble() }
|
|
202
|
+
}
|
|
203
|
+
is XybridEnvelopeKind.Image -> {
|
|
204
|
+
out["kind"] = "image"
|
|
205
|
+
out["bytesBase64"] = Base64.encodeToString(kind.bytes, Base64.NO_WRAP)
|
|
206
|
+
out["format"] = kind.format
|
|
207
|
+
}
|
|
208
|
+
is XybridEnvelopeKind.MultiPart -> {
|
|
209
|
+
out["kind"] = "multipart"
|
|
210
|
+
out["parts"] = kind.parts.map(::encodeEnvelope)
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
out["metadata"] = envelope.metadata.associate { it.key to it.value }
|
|
214
|
+
return out
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// -- Generation and run options -----------------------------------------------
|
|
218
|
+
|
|
219
|
+
fun decodeToolDefinition(any: Any?): XybridToolDefinition {
|
|
220
|
+
val o = obj(any, "tool")
|
|
221
|
+
return XybridToolDefinition(
|
|
222
|
+
name = string(o, "name", "tool"),
|
|
223
|
+
description = string(o, "description", "tool"),
|
|
224
|
+
parametersJson = string(o, "parametersJson", "tool"),
|
|
225
|
+
)
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
fun decodeGenerationConfig(o: JsObject): XybridGenerationConfig {
|
|
229
|
+
val what = "generationConfig"
|
|
230
|
+
val tools = (o["tools"] as? List<*>).orEmpty().map(::decodeToolDefinition)
|
|
231
|
+
return XybridGenerationConfig(
|
|
232
|
+
maxTokens = optionalUInt(o, "maxTokens", what),
|
|
233
|
+
temperature = optionalFloat(o, "temperature", what),
|
|
234
|
+
topP = optionalFloat(o, "topP", what),
|
|
235
|
+
minP = optionalFloat(o, "minP", what),
|
|
236
|
+
topK = optionalUInt(o, "topK", what),
|
|
237
|
+
repetitionPenalty = optionalFloat(o, "repetitionPenalty", what),
|
|
238
|
+
stopSequences = stringList(o, "stopSequences", what),
|
|
239
|
+
grammar = optionalString(o, "grammar", what),
|
|
240
|
+
tools = tools,
|
|
241
|
+
)
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
fun encodeGenerationConfig(config: XybridGenerationConfig): Map<String, Any?> {
|
|
245
|
+
val out = linkedMapOf<String, Any?>(
|
|
246
|
+
"stopSequences" to config.stopSequences,
|
|
247
|
+
"tools" to config.tools.map {
|
|
248
|
+
mapOf("name" to it.name, "description" to it.description, "parametersJson" to it.parametersJson)
|
|
249
|
+
},
|
|
250
|
+
)
|
|
251
|
+
config.maxTokens?.let { out["maxTokens"] = it.toDouble() }
|
|
252
|
+
config.temperature?.let { out["temperature"] = it.toDouble() }
|
|
253
|
+
config.topP?.let { out["topP"] = it.toDouble() }
|
|
254
|
+
config.minP?.let { out["minP"] = it.toDouble() }
|
|
255
|
+
config.topK?.let { out["topK"] = it.toDouble() }
|
|
256
|
+
config.repetitionPenalty?.let { out["repetitionPenalty"] = it.toDouble() }
|
|
257
|
+
config.grammar?.let { out["grammar"] = it }
|
|
258
|
+
return out
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
fun decodeAbortSignal(raw: String): XybridAbortSignal = when (raw) {
|
|
262
|
+
"memoryPressureWarn" -> XybridAbortSignal.MEMORY_PRESSURE_WARN
|
|
263
|
+
"memoryPressureCritical" -> XybridAbortSignal.MEMORY_PRESSURE_CRITICAL
|
|
264
|
+
"thermalHot" -> XybridAbortSignal.THERMAL_HOT
|
|
265
|
+
"thermalCritical" -> XybridAbortSignal.THERMAL_CRITICAL
|
|
266
|
+
else -> throw BridgeException.InvalidArgument("unknown abort signal '$raw'")
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/** Run options plus the two handles that ride along with them. */
|
|
270
|
+
data class RunRequest(
|
|
271
|
+
val options: XybridRunOptions? = null,
|
|
272
|
+
val context: String? = null,
|
|
273
|
+
val cancel: String? = null,
|
|
274
|
+
)
|
|
275
|
+
|
|
276
|
+
fun decodeRunRequest(any: Any?): RunRequest {
|
|
277
|
+
if (any == null) return RunRequest()
|
|
278
|
+
val what = "run options"
|
|
279
|
+
val o = obj(any, what)
|
|
280
|
+
val options = XybridRunOptions(
|
|
281
|
+
generationConfig = optionalObj(o, "generationConfig", what)?.let(::decodeGenerationConfig),
|
|
282
|
+
abortOn = stringList(o, "abortOn", what).map(::decodeAbortSignal),
|
|
283
|
+
fallbackToCloud = optionalBoolean(o, "fallbackToCloud", what) ?: false,
|
|
284
|
+
maxGraceTokens = optionalUInt(o, "maxGraceTokens", what) ?: 0u,
|
|
285
|
+
correlationId = optionalString(o, "correlationId", what),
|
|
286
|
+
)
|
|
287
|
+
return RunRequest(
|
|
288
|
+
options = options,
|
|
289
|
+
context = optionalString(o, "context", what),
|
|
290
|
+
cancel = optionalString(o, "cancel", what),
|
|
291
|
+
)
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
fun decodeToolResult(any: Any?): XybridToolResult {
|
|
295
|
+
val o = obj(any, "tool result")
|
|
296
|
+
return XybridToolResult(
|
|
297
|
+
callId = string(o, "callId", "tool result"),
|
|
298
|
+
name = string(o, "name", "tool result"),
|
|
299
|
+
contentJson = string(o, "contentJson", "tool result"),
|
|
300
|
+
)
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
// -- Results --------------------------------------------------------------------
|
|
304
|
+
|
|
305
|
+
fun encodeOutputType(type: XybridOutputType): String = when (type) {
|
|
306
|
+
XybridOutputType.TEXT -> "text"
|
|
307
|
+
XybridOutputType.AUDIO -> "audio"
|
|
308
|
+
XybridOutputType.EMBEDDING -> "embedding"
|
|
309
|
+
XybridOutputType.UNKNOWN -> "unknown"
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
fun encodeExecutionTarget(target: XybridExecutionTarget): String =
|
|
313
|
+
if (target == XybridExecutionTarget.CLOUD) "cloud" else "local"
|
|
314
|
+
|
|
315
|
+
/** Metrics in native stage order; LLM-only values only when reported. */
|
|
316
|
+
fun encodeMetrics(metrics: XybridInferenceMetrics): Map<String, Any?> {
|
|
317
|
+
val out = linkedMapOf<String, Any?>("totalMs" to metrics.totalMs.toDouble())
|
|
318
|
+
metrics.ttftMs?.let { out["ttftMs"] = it.toDouble() }
|
|
319
|
+
metrics.tokensPerSecond?.let { out["tokensPerSecond"] = it.toDouble() }
|
|
320
|
+
metrics.prefillTps?.let { out["prefillTps"] = it.toDouble() }
|
|
321
|
+
metrics.decodeTps?.let { out["decodeTps"] = it.toDouble() }
|
|
322
|
+
metrics.tokensOut?.let { out["tokensOut"] = it.toDouble() }
|
|
323
|
+
out["stageLatenciesMs"] = metrics.stageLatenciesMs.map {
|
|
324
|
+
mapOf("stageId" to it.stageId, "latencyMs" to it.latencyMs.toDouble())
|
|
325
|
+
}
|
|
326
|
+
return out
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
fun encodeToolCall(call: XybridToolCall): Map<String, Any?> =
|
|
330
|
+
mapOf("id" to call.id, "name" to call.name, "argumentsJson" to call.argumentsJson)
|
|
331
|
+
|
|
332
|
+
fun encodeResult(result: XybridResult): Map<String, Any?> {
|
|
333
|
+
val out = linkedMapOf<String, Any?>(
|
|
334
|
+
"envelope" to encodeEnvelope(result.envelope),
|
|
335
|
+
"outputType" to encodeOutputType(result.outputType),
|
|
336
|
+
"modelId" to result.modelId,
|
|
337
|
+
"latencyMs" to result.latencyMs.toDouble(),
|
|
338
|
+
"executionTarget" to encodeExecutionTarget(result.executionTarget),
|
|
339
|
+
"metrics" to encodeMetrics(result.metrics),
|
|
340
|
+
"toolCalls" to result.toolCalls.map(::encodeToolCall),
|
|
341
|
+
)
|
|
342
|
+
result.reasoningContent?.let { out["reasoningContent"] = it }
|
|
343
|
+
return out
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
fun encodeStreamToken(token: XybridStreamToken): Map<String, Any?> {
|
|
347
|
+
val out = linkedMapOf<String, Any?>(
|
|
348
|
+
"token" to token.token,
|
|
349
|
+
// u64 → Double: JS numbers are exact to 2^53.
|
|
350
|
+
"index" to token.index.toDouble(),
|
|
351
|
+
"cumulativeText" to token.cumulativeText,
|
|
352
|
+
"toolCalls" to token.toolCalls.map(::encodeToolCall),
|
|
353
|
+
)
|
|
354
|
+
token.tokenId?.let { out["tokenId"] = it.toDouble() }
|
|
355
|
+
token.finishReason?.let { out["finishReason"] = it }
|
|
356
|
+
token.rawText?.let { out["rawText"] = it }
|
|
357
|
+
return out
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
fun encodeDownloadStatus(status: XybridDownloadStatus): Map<String, Any?> {
|
|
361
|
+
val out = linkedMapOf<String, Any?>(
|
|
362
|
+
"state" to when (status.state) {
|
|
363
|
+
XybridDownloadState.DOWNLOADING -> "downloading"
|
|
364
|
+
XybridDownloadState.READY -> "ready"
|
|
365
|
+
XybridDownloadState.FAILED -> "failed"
|
|
366
|
+
XybridDownloadState.CANCELLED -> "cancelled"
|
|
367
|
+
},
|
|
368
|
+
"progress" to status.progress.toDouble(),
|
|
369
|
+
"downloadedBytes" to status.downloadedBytes.toDouble(),
|
|
370
|
+
)
|
|
371
|
+
// Absent, not 0, when the source declares no size.
|
|
372
|
+
status.totalBytes?.let { out["totalBytes"] = it.toDouble() }
|
|
373
|
+
return out
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
fun encodeVoice(voice: XybridVoiceInfo): Map<String, Any?> {
|
|
377
|
+
val out = linkedMapOf<String, Any?>("id" to voice.id, "name" to voice.name)
|
|
378
|
+
voice.gender?.let { out["gender"] = it }
|
|
379
|
+
voice.language?.let { out["language"] = it }
|
|
380
|
+
voice.style?.let { out["style"] = it }
|
|
381
|
+
return out
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
fun encodeCacheStatus(status: XybridCacheStatus): Map<String, Any?> = mapOf(
|
|
385
|
+
"totalSizeBytes" to status.totalSizeBytes.toDouble(),
|
|
386
|
+
"entryCount" to status.entryCount.toDouble(),
|
|
387
|
+
"modelCount" to status.modelCount.toDouble(),
|
|
388
|
+
"extractedModelCount" to status.extractedModelCount.toDouble(),
|
|
389
|
+
"cacheRoot" to status.cacheRoot,
|
|
390
|
+
)
|
|
391
|
+
|
|
392
|
+
fun encodeCacheEntry(entry: XybridCacheEntry): Map<String, Any?> = mapOf(
|
|
393
|
+
"modelId" to entry.modelId,
|
|
394
|
+
"location" to when (entry.location) {
|
|
395
|
+
XybridCacheEntryLocation.REGISTRY -> "registry"
|
|
396
|
+
XybridCacheEntryLocation.EXTRACTED -> "extracted"
|
|
397
|
+
XybridCacheEntryLocation.HUGGING_FACE -> "huggingFace"
|
|
398
|
+
XybridCacheEntryLocation.HUGGING_FACE_HUB -> "huggingFaceHub"
|
|
399
|
+
},
|
|
400
|
+
"path" to entry.path,
|
|
401
|
+
"sizeBytes" to entry.sizeBytes.toDouble(),
|
|
402
|
+
)
|
|
403
|
+
|
|
404
|
+
fun encodeStageResult(stage: XybridStageResult): Map<String, Any?> = mapOf(
|
|
405
|
+
"stageId" to stage.stageId,
|
|
406
|
+
"envelope" to encodeEnvelope(stage.envelope),
|
|
407
|
+
"outputType" to encodeOutputType(stage.outputType),
|
|
408
|
+
"latencyMs" to stage.latencyMs.toDouble(),
|
|
409
|
+
"executionTarget" to encodeExecutionTarget(stage.executionTarget),
|
|
410
|
+
"metrics" to encodeMetrics(stage.metrics),
|
|
411
|
+
)
|
|
412
|
+
|
|
413
|
+
fun encodePipelineResult(result: XybridPipelineResult): Map<String, Any?> = mapOf(
|
|
414
|
+
"envelope" to encodeEnvelope(result.envelope),
|
|
415
|
+
"outputType" to encodeOutputType(result.outputType),
|
|
416
|
+
"latencyMs" to result.latencyMs.toDouble(),
|
|
417
|
+
"stages" to result.stages.map(::encodeStageResult),
|
|
418
|
+
)
|
|
419
|
+
|
|
420
|
+
fun encodePartial(partial: XybridPartialResult): Map<String, Any?> = mapOf(
|
|
421
|
+
"text" to partial.text,
|
|
422
|
+
"isStable" to partial.isStable,
|
|
423
|
+
"chunkSequence" to partial.chunkSequence.toDouble(),
|
|
424
|
+
"audioDurationMs" to partial.audioDurationMs.toDouble(),
|
|
425
|
+
)
|
|
426
|
+
|
|
427
|
+
// -- Live ASR -------------------------------------------------------------------
|
|
428
|
+
|
|
429
|
+
fun decodeStreamingConfig(any: Any?): XybridStreamingConfig {
|
|
430
|
+
val what = "streaming config"
|
|
431
|
+
val o = if (any == null) emptyMap() else obj(any, what)
|
|
432
|
+
val vad = optionalObj(o, "vad", what)
|
|
433
|
+
?.let { XybridVadMode.Enabled(filePath(string(it, "modelDir", "streaming config vad"))) }
|
|
434
|
+
?: XybridVadMode.Off
|
|
435
|
+
return XybridStreamingConfig(
|
|
436
|
+
sampleRate = optionalUInt(o, "sampleRate", what) ?: 16_000u,
|
|
437
|
+
vad = vad,
|
|
438
|
+
vadThreshold = optionalFloat(o, "vadThreshold", what) ?: 0.5f,
|
|
439
|
+
language = optionalString(o, "language", what),
|
|
440
|
+
audioCtx = optionalUInt(o, "audioCtx", what),
|
|
441
|
+
)
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
// -- Errors ---------------------------------------------------------------------
|
|
445
|
+
|
|
446
|
+
/** The stable `xybrid_*` code for an SDK error (see src/errors.ts). */
|
|
447
|
+
fun code(error: XybridError): String = when (error) {
|
|
448
|
+
is XybridError.ModelNotFound -> "xybrid_model_not_found"
|
|
449
|
+
is XybridError.DirectoryNotFound -> "xybrid_directory_not_found"
|
|
450
|
+
is XybridError.MetadataNotFound -> "xybrid_metadata_not_found"
|
|
451
|
+
is XybridError.MetadataInvalid -> "xybrid_metadata_invalid"
|
|
452
|
+
is XybridError.LoadError -> "xybrid_load_error"
|
|
453
|
+
is XybridError.InferenceError -> "xybrid_inference_error"
|
|
454
|
+
is XybridError.AbortedForCloudFallback -> "xybrid_aborted_cloud_fallback"
|
|
455
|
+
is XybridError.StreamingNotSupported -> "xybrid_streaming_unsupported"
|
|
456
|
+
is XybridError.NotLoaded -> "xybrid_not_loaded"
|
|
457
|
+
is XybridError.ConfigError -> "xybrid_config_error"
|
|
458
|
+
is XybridError.NetworkError -> "xybrid_network_error"
|
|
459
|
+
is XybridError.Offline -> "xybrid_offline"
|
|
460
|
+
is XybridError.IoError -> "xybrid_io_error"
|
|
461
|
+
is XybridError.CacheError -> "xybrid_cache_error"
|
|
462
|
+
is XybridError.PipelineError -> "xybrid_pipeline_error"
|
|
463
|
+
is XybridError.CircuitOpen -> "xybrid_circuit_open"
|
|
464
|
+
is XybridError.RateLimited -> "xybrid_rate_limited"
|
|
465
|
+
is XybridError.Timeout -> "xybrid_timeout"
|
|
466
|
+
is XybridError.MissingArtifact -> "xybrid_missing_artifact"
|
|
467
|
+
is XybridError.UnsupportedModelCapability -> "xybrid_unsupported_model_capability"
|
|
468
|
+
is XybridError.UnsupportedBackendCapability -> "xybrid_unsupported_backend_capability"
|
|
469
|
+
is XybridError.InvalidImage -> "xybrid_invalid_image"
|
|
470
|
+
is XybridError.Cancelled -> "xybrid_cancelled"
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
/** `(code, message)` for anything a bridged call can throw. */
|
|
474
|
+
fun rejection(error: Throwable): Pair<String, String> = when (error) {
|
|
475
|
+
is XybridError -> code(error) to (error.message ?: "Xybrid error")
|
|
476
|
+
is BridgeException.InvalidArgument -> "xybrid_invalid_argument" to (error.message ?: "invalid argument")
|
|
477
|
+
is BridgeException.Handle -> "xybrid_handle" to (error.message ?: "unknown handle")
|
|
478
|
+
is BridgeException.Config -> "xybrid_config_error" to (error.message ?: "configuration conflict")
|
|
479
|
+
else -> "xybrid_unknown" to (error.message ?: error.toString())
|
|
480
|
+
}
|
|
481
|
+
}
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
package ai.xybrid.reactnative
|
|
2
|
+
|
|
3
|
+
import java.util.UUID
|
|
4
|
+
import java.util.concurrent.ConcurrentHashMap
|
|
5
|
+
import java.util.concurrent.atomic.AtomicBoolean
|
|
6
|
+
import java.util.concurrent.atomic.AtomicInteger
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Every native object JS holds — models, conversation contexts, cancellation
|
|
10
|
+
* tokens, token streams, downloads, pipelines, live-ASR sessions — keyed by an
|
|
11
|
+
* opaque `"<kind>:<uuid>"` string.
|
|
12
|
+
*
|
|
13
|
+
* A BoltFFI handle is a raw owned pointer with no reference count: closing it
|
|
14
|
+
* while another thread is inside a call on it frees memory that call is still
|
|
15
|
+
* using. So every call holds a [Lease] on the entries it touches, and
|
|
16
|
+
* disposing a handle defers `onClose` until the last lease ends. `onDispose`
|
|
17
|
+
* runs at once, for side effects that must not wait (aborting a stream,
|
|
18
|
+
* stopping a session); entries owned by a disposed handle go with it.
|
|
19
|
+
*/
|
|
20
|
+
internal class XybridHandles {
|
|
21
|
+
private val entries = ConcurrentHashMap<String, Entry>()
|
|
22
|
+
|
|
23
|
+
private class Entry(
|
|
24
|
+
val value: Any,
|
|
25
|
+
val owner: String?,
|
|
26
|
+
private val onDispose: (() -> Unit)?,
|
|
27
|
+
private val onClose: (() -> Unit)?,
|
|
28
|
+
) {
|
|
29
|
+
private val leases = AtomicInteger(0)
|
|
30
|
+
private val closed = AtomicBoolean(false)
|
|
31
|
+
|
|
32
|
+
@Volatile
|
|
33
|
+
private var disposed = false
|
|
34
|
+
|
|
35
|
+
/** Take a lease; `false` once the entry is disposed. */
|
|
36
|
+
fun acquire(): Boolean {
|
|
37
|
+
leases.incrementAndGet()
|
|
38
|
+
if (disposed) {
|
|
39
|
+
release()
|
|
40
|
+
return false
|
|
41
|
+
}
|
|
42
|
+
return true
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
fun release() {
|
|
46
|
+
if (leases.decrementAndGet() == 0 && disposed) close()
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
fun dispose() {
|
|
50
|
+
disposed = true
|
|
51
|
+
onDispose?.invoke()
|
|
52
|
+
if (leases.get() == 0) close()
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
private fun close() {
|
|
56
|
+
if (closed.compareAndSet(false, true)) onClose?.invoke()
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** A claim on a live entry. Close it exactly once; extra closes are ignored. */
|
|
61
|
+
class Lease<T : Any> internal constructor(val value: T, private val onRelease: () -> Unit) : AutoCloseable {
|
|
62
|
+
private val released = AtomicBoolean(false)
|
|
63
|
+
|
|
64
|
+
override fun close() {
|
|
65
|
+
if (released.compareAndSet(false, true)) onRelease()
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Register [value] and return its handle. [onClose] defaults to closing an
|
|
71
|
+
* [AutoCloseable] (every BoltFFI handle is one).
|
|
72
|
+
*/
|
|
73
|
+
fun insert(
|
|
74
|
+
value: Any,
|
|
75
|
+
kind: String,
|
|
76
|
+
owner: String? = null,
|
|
77
|
+
onDispose: (() -> Unit)? = null,
|
|
78
|
+
onClose: (() -> Unit)? = (value as? AutoCloseable)?.let { closeable -> { closeable.close() } },
|
|
79
|
+
): String {
|
|
80
|
+
val handle = "$kind:${UUID.randomUUID()}"
|
|
81
|
+
entries[handle] = Entry(value, owner, onDispose, onClose)
|
|
82
|
+
return handle
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Lease the object behind [handle]; a `xybrid_handle` error when it is
|
|
87
|
+
* unknown, disposed, or another kind of object.
|
|
88
|
+
*/
|
|
89
|
+
fun <T : Any> lease(handle: String, type: Class<T>): Lease<T> {
|
|
90
|
+
val entry = entries[handle]
|
|
91
|
+
val value = entry?.value
|
|
92
|
+
if (entry == null || !type.isInstance(value) || !entry.acquire()) {
|
|
93
|
+
throw BridgeException.Handle("unknown or released handle: $handle")
|
|
94
|
+
}
|
|
95
|
+
return Lease(type.cast(value)!!, entry::release)
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/** Like [lease], but `null` instead of an error. */
|
|
99
|
+
fun <T : Any> leaseOrNull(handle: String, type: Class<T>): Lease<T>? =
|
|
100
|
+
try {
|
|
101
|
+
lease(handle, type)
|
|
102
|
+
} catch (e: BridgeException.Handle) {
|
|
103
|
+
null
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/** Run [block] on the object behind [handle] under a lease. */
|
|
107
|
+
inline fun <T : Any, R> use(handle: String, type: Class<T>, block: (T) -> R): R =
|
|
108
|
+
lease(handle, type).use { block(it.value) }
|
|
109
|
+
|
|
110
|
+
/** Dispose [handle] and every entry it owns. Idempotent. */
|
|
111
|
+
fun dispose(handle: String) {
|
|
112
|
+
val removed = mutableListOf<Entry>()
|
|
113
|
+
entries.remove(handle)?.let(removed::add)
|
|
114
|
+
val owned = entries.entries.filter { it.value.owner == handle }
|
|
115
|
+
for ((key, entry) in owned) {
|
|
116
|
+
if (entries.remove(key, entry)) removed.add(entry)
|
|
117
|
+
}
|
|
118
|
+
removed.forEach(Entry::dispose)
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/** Dispose everything (module invalidation: reload, teardown). */
|
|
122
|
+
fun disposeAll() {
|
|
123
|
+
val removed = entries.keys.toList().mapNotNull { entries.remove(it) }
|
|
124
|
+
removed.forEach(Entry::dispose)
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/** Run [action] on every live object of type [T], each under a lease. */
|
|
128
|
+
fun <T : Any> forEachLive(type: Class<T>, action: (T) -> Unit) {
|
|
129
|
+
for (entry in entries.values) {
|
|
130
|
+
val value = entry.value
|
|
131
|
+
if (!type.isInstance(value) || !entry.acquire()) continue
|
|
132
|
+
try {
|
|
133
|
+
action(type.cast(value)!!)
|
|
134
|
+
} finally {
|
|
135
|
+
entry.release()
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
}
|