@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,285 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// Translation between the public types and the plain objects that cross the
|
|
3
|
+
// TurboModule boundary. Every convenience field (voiceId, speed, sampleRate,
|
|
4
|
+
// channels, role) is folded into / lifted out of envelope metadata HERE, once,
|
|
5
|
+
// so the native codecs stay generic and lossless: an envelope is a payload
|
|
6
|
+
// kind plus string metadata, exactly like bolt's `XybridEnvelope`.
|
|
7
|
+
//
|
|
8
|
+
// Pure functions only — no native calls — so tests/wire.test.mjs covers them
|
|
9
|
+
// on Node.
|
|
10
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
11
|
+
exports.toWireEnvelope = toWireEnvelope;
|
|
12
|
+
exports.fromWireEnvelope = fromWireEnvelope;
|
|
13
|
+
exports.normalizeImageFormat = normalizeImageFormat;
|
|
14
|
+
exports.toWireToolDefinition = toWireToolDefinition;
|
|
15
|
+
exports.toWireGenerationConfig = toWireGenerationConfig;
|
|
16
|
+
exports.fromWireGenerationConfig = fromWireGenerationConfig;
|
|
17
|
+
exports.toWireRunOptions = toWireRunOptions;
|
|
18
|
+
exports.toWireToolResult = toWireToolResult;
|
|
19
|
+
exports.fromWireResult = fromWireResult;
|
|
20
|
+
exports.fromWireStreamToken = fromWireStreamToken;
|
|
21
|
+
exports.fromWirePipelineResult = fromWirePipelineResult;
|
|
22
|
+
exports.fromWireDownloadStatus = fromWireDownloadStatus;
|
|
23
|
+
// Metadata keys the Rust core reads; see the Swift/Kotlin envelope factories.
|
|
24
|
+
const KEY_VOICE_ID = 'voice_id';
|
|
25
|
+
const KEY_SPEED = 'speed';
|
|
26
|
+
const KEY_SAMPLE_RATE = 'sample_rate';
|
|
27
|
+
const KEY_CHANNELS = 'channels';
|
|
28
|
+
const KEY_ROLE = 'xybrid.role';
|
|
29
|
+
const ROLES = ['system', 'user', 'assistant'];
|
|
30
|
+
// -- Envelopes ----------------------------------------------------------------
|
|
31
|
+
function setIfDefined(target, key, value) {
|
|
32
|
+
if (value !== undefined && value !== null)
|
|
33
|
+
target[key] = String(value);
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Fold an envelope's convenience fields into metadata. Typed fields win over
|
|
37
|
+
* raw `metadata` entries for the same key; audio input defaults to 16 kHz
|
|
38
|
+
* mono, like the Swift and Kotlin `Envelope.audio` factories.
|
|
39
|
+
*/
|
|
40
|
+
function toWireEnvelope(envelope) {
|
|
41
|
+
if (envelope == null || typeof envelope !== 'object') {
|
|
42
|
+
throw new TypeError('An envelope must be an object with a `kind` field');
|
|
43
|
+
}
|
|
44
|
+
const metadata = { ...(envelope.metadata ?? {}) };
|
|
45
|
+
setIfDefined(metadata, KEY_ROLE, envelope.role);
|
|
46
|
+
switch (envelope.kind) {
|
|
47
|
+
case 'text':
|
|
48
|
+
requireType(envelope.text, 'string', 'text envelope: `text`');
|
|
49
|
+
setIfDefined(metadata, KEY_VOICE_ID, envelope.voiceId);
|
|
50
|
+
setIfDefined(metadata, KEY_SPEED, envelope.speed);
|
|
51
|
+
return { kind: 'text', text: envelope.text, metadata };
|
|
52
|
+
case 'audio':
|
|
53
|
+
requireType(envelope.bytesBase64, 'string', 'audio envelope: `bytesBase64`');
|
|
54
|
+
setIfDefined(metadata, KEY_SAMPLE_RATE, envelope.sampleRate ?? metadata[KEY_SAMPLE_RATE] ?? 16000);
|
|
55
|
+
setIfDefined(metadata, KEY_CHANNELS, envelope.channels ?? metadata[KEY_CHANNELS] ?? 1);
|
|
56
|
+
return { kind: 'audio', bytesBase64: envelope.bytesBase64, metadata };
|
|
57
|
+
case 'embedding':
|
|
58
|
+
if (!Array.isArray(envelope.data)) {
|
|
59
|
+
throw new TypeError('embedding envelope: `data` must be a number array');
|
|
60
|
+
}
|
|
61
|
+
return { kind: 'embedding', data: envelope.data, metadata };
|
|
62
|
+
case 'image':
|
|
63
|
+
requireType(envelope.bytesBase64, 'string', 'image envelope: `bytesBase64`');
|
|
64
|
+
return {
|
|
65
|
+
kind: 'image',
|
|
66
|
+
bytesBase64: envelope.bytesBase64,
|
|
67
|
+
format: normalizeImageFormat(envelope.format),
|
|
68
|
+
metadata,
|
|
69
|
+
};
|
|
70
|
+
case 'multipart':
|
|
71
|
+
if (!Array.isArray(envelope.parts)) {
|
|
72
|
+
throw new TypeError('multipart envelope: `parts` must be an array');
|
|
73
|
+
}
|
|
74
|
+
return { kind: 'multipart', parts: envelope.parts.map(toWireEnvelope), metadata };
|
|
75
|
+
default:
|
|
76
|
+
throw new TypeError(`Unknown envelope kind: ${String(envelope.kind)}`);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
/** Lift well-known metadata keys back into typed fields. Lossless. */
|
|
80
|
+
function fromWireEnvelope(wire) {
|
|
81
|
+
const metadata = { ...(wire.metadata ?? {}) };
|
|
82
|
+
const role = metadata[KEY_ROLE];
|
|
83
|
+
const base = {
|
|
84
|
+
...(role && ROLES.includes(role) ? { role } : {}),
|
|
85
|
+
metadata,
|
|
86
|
+
};
|
|
87
|
+
switch (wire.kind) {
|
|
88
|
+
case 'text': {
|
|
89
|
+
const speed = parseNumber(metadata[KEY_SPEED]);
|
|
90
|
+
return {
|
|
91
|
+
kind: 'text',
|
|
92
|
+
text: wire.text,
|
|
93
|
+
...(metadata[KEY_VOICE_ID] !== undefined ? { voiceId: metadata[KEY_VOICE_ID] } : {}),
|
|
94
|
+
...(speed !== undefined ? { speed } : {}),
|
|
95
|
+
...base,
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
case 'audio': {
|
|
99
|
+
const sampleRate = parseNumber(metadata[KEY_SAMPLE_RATE]);
|
|
100
|
+
const channels = parseNumber(metadata[KEY_CHANNELS]);
|
|
101
|
+
return {
|
|
102
|
+
kind: 'audio',
|
|
103
|
+
bytesBase64: wire.bytesBase64,
|
|
104
|
+
...(sampleRate !== undefined ? { sampleRate } : {}),
|
|
105
|
+
...(channels !== undefined ? { channels } : {}),
|
|
106
|
+
...base,
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
case 'embedding':
|
|
110
|
+
return { kind: 'embedding', data: wire.data, ...base };
|
|
111
|
+
case 'image':
|
|
112
|
+
return {
|
|
113
|
+
kind: 'image',
|
|
114
|
+
bytesBase64: wire.bytesBase64,
|
|
115
|
+
format: normalizeImageFormat(wire.format),
|
|
116
|
+
...base,
|
|
117
|
+
};
|
|
118
|
+
case 'multipart':
|
|
119
|
+
return { kind: 'multipart', parts: wire.parts.map(fromWireEnvelope), ...base };
|
|
120
|
+
default:
|
|
121
|
+
throw new Error(`Unexpected envelope kind from native: ${String(wire.kind)}`);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
/** Accept `jpg` as an alias and reject anything the core cannot decode. */
|
|
125
|
+
function normalizeImageFormat(format) {
|
|
126
|
+
const normalized = String(format ?? '').trim().toLowerCase();
|
|
127
|
+
if (normalized === 'jpg' || normalized === 'jpeg')
|
|
128
|
+
return 'jpeg';
|
|
129
|
+
if (normalized === 'png' || normalized === 'webp')
|
|
130
|
+
return normalized;
|
|
131
|
+
throw new TypeError(`Unsupported image format '${format}'. Supported: png, jpeg, jpg, webp`);
|
|
132
|
+
}
|
|
133
|
+
// -- Options ------------------------------------------------------------------
|
|
134
|
+
const GENERATION_KEYS = [
|
|
135
|
+
'maxTokens',
|
|
136
|
+
'temperature',
|
|
137
|
+
'topP',
|
|
138
|
+
'minP',
|
|
139
|
+
'topK',
|
|
140
|
+
'repetitionPenalty',
|
|
141
|
+
'stopSequences',
|
|
142
|
+
'grammar',
|
|
143
|
+
'tools',
|
|
144
|
+
];
|
|
145
|
+
function toWireToolDefinition(tool) {
|
|
146
|
+
const parameters = tool.parameters;
|
|
147
|
+
return {
|
|
148
|
+
name: tool.name,
|
|
149
|
+
description: tool.description,
|
|
150
|
+
parametersJson: typeof parameters === 'string' ? parameters : JSON.stringify(parameters),
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
function toWireGenerationConfig(config) {
|
|
154
|
+
const { tools, ...rest } = config;
|
|
155
|
+
return {
|
|
156
|
+
...rest,
|
|
157
|
+
...(tools ? { tools: tools.map(toWireToolDefinition) } : {}),
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
function fromWireGenerationConfig(wire) {
|
|
161
|
+
const { tools, ...rest } = wire;
|
|
162
|
+
return {
|
|
163
|
+
...rest,
|
|
164
|
+
tools: (tools ?? []).map((tool) => ({
|
|
165
|
+
name: tool.name,
|
|
166
|
+
description: tool.description,
|
|
167
|
+
parameters: tool.parametersJson,
|
|
168
|
+
})),
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
/**
|
|
172
|
+
* Build the wire options. Sampling parameters must sit under
|
|
173
|
+
* `generationConfig`; passing them at the top level is a mistake that would
|
|
174
|
+
* otherwise be silently ignored, so it throws.
|
|
175
|
+
*/
|
|
176
|
+
function toWireRunOptions(options, handles = {}) {
|
|
177
|
+
if (options != null && typeof options !== 'object') {
|
|
178
|
+
throw new TypeError('Run options must be an object');
|
|
179
|
+
}
|
|
180
|
+
const misplaced = GENERATION_KEYS.filter((key) => options != null && key in options);
|
|
181
|
+
if (misplaced.length > 0) {
|
|
182
|
+
throw new TypeError(`Sampling parameters belong under \`generationConfig\` (got ${misplaced.join(', ')} ` +
|
|
183
|
+
'at the top level of the run options)');
|
|
184
|
+
}
|
|
185
|
+
const wire = {};
|
|
186
|
+
if (options?.generationConfig) {
|
|
187
|
+
wire.generationConfig = toWireGenerationConfig(options.generationConfig);
|
|
188
|
+
}
|
|
189
|
+
if (options?.abortOn)
|
|
190
|
+
wire.abortOn = [...options.abortOn];
|
|
191
|
+
if (options?.fallbackToCloud !== undefined)
|
|
192
|
+
wire.fallbackToCloud = options.fallbackToCloud;
|
|
193
|
+
if (options?.maxGraceTokens !== undefined)
|
|
194
|
+
wire.maxGraceTokens = options.maxGraceTokens;
|
|
195
|
+
if (options?.correlationId !== undefined)
|
|
196
|
+
wire.correlationId = options.correlationId;
|
|
197
|
+
if (handles.context)
|
|
198
|
+
wire.context = handles.context;
|
|
199
|
+
if (handles.cancel)
|
|
200
|
+
wire.cancel = handles.cancel;
|
|
201
|
+
return Object.keys(wire).length > 0 ? wire : null;
|
|
202
|
+
}
|
|
203
|
+
function toWireToolResult(result) {
|
|
204
|
+
const { content } = result;
|
|
205
|
+
return {
|
|
206
|
+
callId: result.callId,
|
|
207
|
+
name: result.name,
|
|
208
|
+
contentJson: typeof content === 'string' ? content : JSON.stringify(content ?? null),
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
// -- Results ------------------------------------------------------------------
|
|
212
|
+
/** Convenience payload fields derived from an envelope (no copies). */
|
|
213
|
+
function payloadFields(envelope) {
|
|
214
|
+
switch (envelope.kind) {
|
|
215
|
+
case 'text':
|
|
216
|
+
return { text: envelope.text };
|
|
217
|
+
case 'audio':
|
|
218
|
+
return { audioBytesBase64: envelope.bytesBase64 };
|
|
219
|
+
case 'embedding':
|
|
220
|
+
return { embedding: envelope.data };
|
|
221
|
+
default:
|
|
222
|
+
return {};
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
function fromWireResult(wire) {
|
|
226
|
+
return {
|
|
227
|
+
envelope: fromWireEnvelope(wire.envelope),
|
|
228
|
+
outputType: wire.outputType,
|
|
229
|
+
modelId: wire.modelId,
|
|
230
|
+
...payloadFields(wire.envelope),
|
|
231
|
+
...(wire.reasoningContent !== undefined ? { reasoningContent: wire.reasoningContent } : {}),
|
|
232
|
+
toolCalls: wire.toolCalls ?? [],
|
|
233
|
+
latencyMs: wire.latencyMs,
|
|
234
|
+
metrics: wire.metrics,
|
|
235
|
+
executionTarget: wire.executionTarget,
|
|
236
|
+
};
|
|
237
|
+
}
|
|
238
|
+
function fromWireStreamToken(wire) {
|
|
239
|
+
return { ...wire, toolCalls: wire.toolCalls ?? [] };
|
|
240
|
+
}
|
|
241
|
+
function fromWireStage(wire) {
|
|
242
|
+
const payload = payloadFields(wire.envelope);
|
|
243
|
+
return {
|
|
244
|
+
stageId: wire.stageId,
|
|
245
|
+
envelope: fromWireEnvelope(wire.envelope),
|
|
246
|
+
outputType: wire.outputType,
|
|
247
|
+
...(payload.text !== undefined ? { text: payload.text } : {}),
|
|
248
|
+
...(payload.audioBytesBase64 !== undefined
|
|
249
|
+
? { audioBytesBase64: payload.audioBytesBase64 }
|
|
250
|
+
: {}),
|
|
251
|
+
latencyMs: wire.latencyMs,
|
|
252
|
+
executionTarget: wire.executionTarget,
|
|
253
|
+
metrics: wire.metrics,
|
|
254
|
+
};
|
|
255
|
+
}
|
|
256
|
+
function fromWirePipelineResult(wire) {
|
|
257
|
+
const payload = payloadFields(wire.envelope);
|
|
258
|
+
return {
|
|
259
|
+
envelope: fromWireEnvelope(wire.envelope),
|
|
260
|
+
outputType: wire.outputType,
|
|
261
|
+
...(payload.text !== undefined ? { text: payload.text } : {}),
|
|
262
|
+
...(payload.audioBytesBase64 !== undefined
|
|
263
|
+
? { audioBytesBase64: payload.audioBytesBase64 }
|
|
264
|
+
: {}),
|
|
265
|
+
latencyMs: wire.latencyMs,
|
|
266
|
+
stages: wire.stages.map(fromWireStage),
|
|
267
|
+
};
|
|
268
|
+
}
|
|
269
|
+
function fromWireDownloadStatus(wire) {
|
|
270
|
+
// Both shims omit `totalBytes` when unknown; drop an explicit null too so
|
|
271
|
+
// callers can rely on `status.totalBytes === undefined`.
|
|
272
|
+
const { totalBytes, ...rest } = wire;
|
|
273
|
+
return totalBytes == null ? rest : { ...rest, totalBytes };
|
|
274
|
+
}
|
|
275
|
+
// -- Helpers ------------------------------------------------------------------
|
|
276
|
+
function requireType(value, type, what) {
|
|
277
|
+
if (typeof value !== type)
|
|
278
|
+
throw new TypeError(`${what} must be a ${type}`);
|
|
279
|
+
}
|
|
280
|
+
function parseNumber(raw) {
|
|
281
|
+
if (raw === undefined)
|
|
282
|
+
return undefined;
|
|
283
|
+
const value = Number(raw);
|
|
284
|
+
return Number.isFinite(value) ? value : undefined;
|
|
285
|
+
}
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.Xybrid = void 0;
|
|
7
|
+
const react_native_1 = require("react-native");
|
|
8
|
+
const NativeXybrid_1 = __importDefault(require("./NativeXybrid"));
|
|
9
|
+
const model_1 = require("./model");
|
|
10
|
+
let initialized = false;
|
|
11
|
+
/**
|
|
12
|
+
* Process-wide SDK entry points. Local inference needs no setup at all —
|
|
13
|
+
* `initialize` only matters for the optional platform features.
|
|
14
|
+
*/
|
|
15
|
+
exports.Xybrid = {
|
|
16
|
+
/**
|
|
17
|
+
* Apply process-wide configuration. The SDK otherwise configures itself
|
|
18
|
+
* anonymously on first use, so call this before anything else when you
|
|
19
|
+
* have options. Configuration is applied once per app process: repeating
|
|
20
|
+
* it with the same options resolves, with different options rejects
|
|
21
|
+
* (`xybrid_config_error`) — restart the app to change them.
|
|
22
|
+
*/
|
|
23
|
+
async initialize(options = {}) {
|
|
24
|
+
await NativeXybrid_1.default.initialize(options);
|
|
25
|
+
initialized = true;
|
|
26
|
+
},
|
|
27
|
+
/** Whether {@link initialize} has resolved in this JS context. */
|
|
28
|
+
get isInitialized() {
|
|
29
|
+
return initialized;
|
|
30
|
+
},
|
|
31
|
+
/** Describe a registry model without loading it (same as `ModelLoader.fromRegistry`). */
|
|
32
|
+
model(id) {
|
|
33
|
+
return model_1.ModelLoader.fromRegistry(id);
|
|
34
|
+
},
|
|
35
|
+
/** The native SDK version, e.g. `0.9.0`. */
|
|
36
|
+
version() {
|
|
37
|
+
return NativeXybrid_1.default.sdkVersion();
|
|
38
|
+
},
|
|
39
|
+
/** Whether an API key resolves, from {@link initialize} or the environment. */
|
|
40
|
+
hasApiKey() {
|
|
41
|
+
return NativeXybrid_1.default.hasApiKey();
|
|
42
|
+
},
|
|
43
|
+
/** Set the key for a cloud provider the gateway forwards to (not the Xybrid key). */
|
|
44
|
+
setProviderApiKey(provider, apiKey) {
|
|
45
|
+
return NativeXybrid_1.default.setProviderApiKey(provider, apiKey);
|
|
46
|
+
},
|
|
47
|
+
/** Point the cloud gateway at a platform base URL; the `/v1` suffix is added for you. */
|
|
48
|
+
setPlatformUrl(url) {
|
|
49
|
+
return NativeXybrid_1.default.setPlatformUrl(url);
|
|
50
|
+
},
|
|
51
|
+
/**
|
|
52
|
+
* Default speculative cloud serving on or off for loads that don't opt in
|
|
53
|
+
* per load (`ModelLoader.fromRegistrySpeculative` always speculates). Needs
|
|
54
|
+
* an API key. LLM/chat only — prefer the per-load form when the app also
|
|
55
|
+
* loads ASR/TTS models.
|
|
56
|
+
*/
|
|
57
|
+
setSpeculativeCloud(enabled) {
|
|
58
|
+
return NativeXybrid_1.default.setSpeculativeCloud(enabled);
|
|
59
|
+
},
|
|
60
|
+
isSpeculativeCloudEnabled() {
|
|
61
|
+
return NativeXybrid_1.default.isSpeculativeCloudEnabled();
|
|
62
|
+
},
|
|
63
|
+
/**
|
|
64
|
+
* Release every idle model's memory; resolves how many were released. Runs
|
|
65
|
+
* in flight are skipped, and a released model reloads itself on next use.
|
|
66
|
+
*/
|
|
67
|
+
releaseMemory() {
|
|
68
|
+
return NativeXybrid_1.default.releaseMemory();
|
|
69
|
+
},
|
|
70
|
+
/**
|
|
71
|
+
* Call {@link releaseMemory} on every OS memory warning (React Native
|
|
72
|
+
* `AppState` `memoryWarning`, iOS and Android). Returns the unsubscribe
|
|
73
|
+
* function.
|
|
74
|
+
*/
|
|
75
|
+
releaseMemoryOnWarning() {
|
|
76
|
+
const subscription = react_native_1.AppState.addEventListener('memoryWarning', () => {
|
|
77
|
+
NativeXybrid_1.default.releaseMemory().catch(() => { });
|
|
78
|
+
});
|
|
79
|
+
return () => subscription.remove();
|
|
80
|
+
},
|
|
81
|
+
/** Release least-recently-used idle models before loads under memory pressure. */
|
|
82
|
+
setAutoRelease(enabled) {
|
|
83
|
+
return NativeXybrid_1.default.setAutoRelease(enabled);
|
|
84
|
+
},
|
|
85
|
+
isAutoReleaseEnabled() {
|
|
86
|
+
return NativeXybrid_1.default.isAutoReleaseEnabled();
|
|
87
|
+
},
|
|
88
|
+
// -- Device state. Both platforms already observe battery (and Android
|
|
89
|
+
// thermal) natively; these exist for tests and custom readings. --
|
|
90
|
+
/** Push a battery percentage (0–100) to the routing engine. */
|
|
91
|
+
setBatteryLevel(percent) {
|
|
92
|
+
return NativeXybrid_1.default.setBatteryLevel(percent);
|
|
93
|
+
},
|
|
94
|
+
clearBatteryLevel() {
|
|
95
|
+
return NativeXybrid_1.default.clearBatteryLevel();
|
|
96
|
+
},
|
|
97
|
+
setThermalState(state) {
|
|
98
|
+
return NativeXybrid_1.default.setThermalState(state);
|
|
99
|
+
},
|
|
100
|
+
clearThermalState() {
|
|
101
|
+
return NativeXybrid_1.default.clearThermalState();
|
|
102
|
+
},
|
|
103
|
+
// -- Model cache. Every call walks or deletes the cache on disk, off the
|
|
104
|
+
// JS thread. --
|
|
105
|
+
/** Aggregate storage usage across every managed cache location. */
|
|
106
|
+
async modelCacheStatus() {
|
|
107
|
+
return (await NativeXybrid_1.default.cacheStatus());
|
|
108
|
+
},
|
|
109
|
+
/** Physical entries; a model can appear more than once. */
|
|
110
|
+
async modelCacheEntries() {
|
|
111
|
+
return (await NativeXybrid_1.default.cacheEntries());
|
|
112
|
+
},
|
|
113
|
+
/** Whether `modelId` occupies any cache entry. */
|
|
114
|
+
hasCachedModelData(modelId) {
|
|
115
|
+
return NativeXybrid_1.default.cacheIsModelCached(modelId);
|
|
116
|
+
},
|
|
117
|
+
/** A local path for `modelId`, or `null`. Presence does not mean extracted. */
|
|
118
|
+
cachedModelPath(modelId) {
|
|
119
|
+
return NativeXybrid_1.default.cacheModelPath(modelId);
|
|
120
|
+
},
|
|
121
|
+
/** Models extracted, validated and ready to run offline. */
|
|
122
|
+
extractedModelIds() {
|
|
123
|
+
return NativeXybrid_1.default.cacheExtractedModelIds();
|
|
124
|
+
},
|
|
125
|
+
/** Remove every cache entry for `modelId`. Don't race a load of the same model. */
|
|
126
|
+
removeCachedModel(modelId) {
|
|
127
|
+
return NativeXybrid_1.default.cacheRemoveModel(modelId);
|
|
128
|
+
},
|
|
129
|
+
/** Clear all managed cache storage. Don't race any model load. */
|
|
130
|
+
clearModelCache() {
|
|
131
|
+
return NativeXybrid_1.default.cacheClear();
|
|
132
|
+
},
|
|
133
|
+
};
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
// TurboModule spec consumed by React Native Codegen — the one contract both
|
|
2
|
+
// native shims are compiled against: Android extends the generated
|
|
3
|
+
// `NativeXybridSpec` class and iOS conforms to the generated
|
|
4
|
+
// `NativeXybridSpec` protocol (tests/spec-conformance.test.mjs checks the
|
|
5
|
+
// selectors without a Mac).
|
|
6
|
+
//
|
|
7
|
+
// Codegen accepts only `string`, `number`, `boolean`, `Object`, arrays and
|
|
8
|
+
// `Promise`s of those, and `| null`. Structured values therefore cross as
|
|
9
|
+
// plain `Object`s: the TS facade (src/index.ts) builds and narrows them, and
|
|
10
|
+
// the native codecs (ios/XybridCodec.swift, android/.../XybridCodec.kt)
|
|
11
|
+
// translate them to and from the bolt types.
|
|
12
|
+
//
|
|
13
|
+
// Native objects (models, conversation contexts, cancellation tokens, token
|
|
14
|
+
// streams, downloads, pipelines, live-ASR sessions) are opaque string handles
|
|
15
|
+
// held in one registry per platform. `dispose` frees any of them, and freeing
|
|
16
|
+
// a model also disposes the token streams started from it.
|
|
17
|
+
//
|
|
18
|
+
// Nothing here is public API. Keep method names distinct from NSObject /
|
|
19
|
+
// Java `Object` members (hence `dispose`, not `release`; `sdkVersion`, not
|
|
20
|
+
// `version`).
|
|
21
|
+
import { TurboModuleRegistry } from 'react-native';
|
|
22
|
+
export default TurboModuleRegistry.getEnforcing('RNXybrid');
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
// Minimal base64 for the one binary payload JS produces itself: live-ASR PCM.
|
|
2
|
+
// Hermes has no Buffer, and `btoa` only takes binary strings, so encode the
|
|
3
|
+
// bytes directly. Every platform React Native runs on is little-endian, which
|
|
4
|
+
// is the byte order the native shims decode Float32 samples in.
|
|
5
|
+
const ALPHABET = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/';
|
|
6
|
+
/** Standard (RFC 4648, padded) base64 of `bytes`. */
|
|
7
|
+
export function bytesToBase64(bytes) {
|
|
8
|
+
const chunks = [];
|
|
9
|
+
let chunk = '';
|
|
10
|
+
let i = 0;
|
|
11
|
+
for (; i + 2 < bytes.length; i += 3) {
|
|
12
|
+
const n = (bytes[i] << 16) | (bytes[i + 1] << 8) | bytes[i + 2];
|
|
13
|
+
chunk +=
|
|
14
|
+
ALPHABET[(n >> 18) & 63] +
|
|
15
|
+
ALPHABET[(n >> 12) & 63] +
|
|
16
|
+
ALPHABET[(n >> 6) & 63] +
|
|
17
|
+
ALPHABET[n & 63];
|
|
18
|
+
// Flush periodically so long inputs don't build one enormous rope.
|
|
19
|
+
if (chunk.length >= 8192) {
|
|
20
|
+
chunks.push(chunk);
|
|
21
|
+
chunk = '';
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
const rest = bytes.length - i;
|
|
25
|
+
if (rest === 1) {
|
|
26
|
+
const n = bytes[i] << 16;
|
|
27
|
+
chunk += ALPHABET[(n >> 18) & 63] + ALPHABET[(n >> 12) & 63] + '==';
|
|
28
|
+
}
|
|
29
|
+
else if (rest === 2) {
|
|
30
|
+
const n = (bytes[i] << 16) | (bytes[i + 1] << 8);
|
|
31
|
+
chunk +=
|
|
32
|
+
ALPHABET[(n >> 18) & 63] + ALPHABET[(n >> 12) & 63] + ALPHABET[(n >> 6) & 63] + '=';
|
|
33
|
+
}
|
|
34
|
+
chunks.push(chunk);
|
|
35
|
+
return chunks.join('');
|
|
36
|
+
}
|
|
37
|
+
/** Base64 of Float32 PCM samples as little-endian bytes. */
|
|
38
|
+
export function float32ToBase64(samples) {
|
|
39
|
+
const floats = samples instanceof Float32Array ? samples : Float32Array.from(samples);
|
|
40
|
+
return bytesToBase64(new Uint8Array(floats.buffer, floats.byteOffset, floats.byteLength));
|
|
41
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import NativeXybrid from './NativeXybrid';
|
|
2
|
+
const NO_SIGNAL = { token: null, release: () => Promise.resolve() };
|
|
3
|
+
/**
|
|
4
|
+
* Create a native stop button wired to `signal`. Aborting the signal cancels
|
|
5
|
+
* the token, which stops generation at the next token boundary. A signal
|
|
6
|
+
* that is already aborted cancels the token before the call starts, so the
|
|
7
|
+
* call rejects with `xybrid_cancelled` instead of running.
|
|
8
|
+
*/
|
|
9
|
+
export async function bindCancel(signal) {
|
|
10
|
+
if (!signal)
|
|
11
|
+
return NO_SIGNAL;
|
|
12
|
+
const token = await NativeXybrid.createCancelToken();
|
|
13
|
+
const onAbort = () => {
|
|
14
|
+
NativeXybrid.cancel(token).catch(() => { });
|
|
15
|
+
};
|
|
16
|
+
if (signal.aborted) {
|
|
17
|
+
await NativeXybrid.cancel(token);
|
|
18
|
+
}
|
|
19
|
+
else {
|
|
20
|
+
signal.addEventListener('abort', onAbort);
|
|
21
|
+
}
|
|
22
|
+
return {
|
|
23
|
+
token,
|
|
24
|
+
release: async () => {
|
|
25
|
+
signal.removeEventListener('abort', onAbort);
|
|
26
|
+
await NativeXybrid.dispose(token).catch(() => { });
|
|
27
|
+
},
|
|
28
|
+
};
|
|
29
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import NativeXybrid from './NativeXybrid';
|
|
2
|
+
import { fromWireEnvelope, toWireEnvelope } from './wire';
|
|
3
|
+
/**
|
|
4
|
+
* Multi-turn conversation history, held natively. Pass it to
|
|
5
|
+
* `model.run(envelope, { context })` or `runStreaming`; inference reads the
|
|
6
|
+
* history but never changes it. The prompt you run is appended to the
|
|
7
|
+
* history for that run only, so push both turns *after* the run — pushing the
|
|
8
|
+
* prompt first would send it twice:
|
|
9
|
+
*
|
|
10
|
+
* ```ts
|
|
11
|
+
* const chat = await ConversationContext.create();
|
|
12
|
+
* await chat.setSystem('You are terse.');
|
|
13
|
+
*
|
|
14
|
+
* const question = Envelope.user('What is the capital of France?');
|
|
15
|
+
* const reply = await model.run(question, { context: chat });
|
|
16
|
+
* await chat.push(question);
|
|
17
|
+
* await chat.push(reply.envelope); // already tagged `assistant`
|
|
18
|
+
* ```
|
|
19
|
+
*/
|
|
20
|
+
export class ConversationContext {
|
|
21
|
+
constructor(
|
|
22
|
+
/** Opaque native handle. */
|
|
23
|
+
handle) {
|
|
24
|
+
this.handle = handle;
|
|
25
|
+
}
|
|
26
|
+
/** A new, empty conversation; `id` defaults to a fresh one (telemetry correlation). */
|
|
27
|
+
static async create(id) {
|
|
28
|
+
return new ConversationContext(await NativeXybrid.createContext(id ?? null));
|
|
29
|
+
}
|
|
30
|
+
/** Append a turn. Give it a `role` (see `Envelope.user` / `Envelope.assistant`). */
|
|
31
|
+
push(envelope) {
|
|
32
|
+
return NativeXybrid.contextPush(this.handle, toWireEnvelope(envelope));
|
|
33
|
+
}
|
|
34
|
+
/** Set the persistent system prompt; it survives {@link clear}. */
|
|
35
|
+
setSystem(system) {
|
|
36
|
+
const envelope = typeof system === 'string' ? { kind: 'text', text: system, role: 'system' } : system;
|
|
37
|
+
return NativeXybrid.contextSetSystem(this.handle, toWireEnvelope(envelope));
|
|
38
|
+
}
|
|
39
|
+
/** Drop the history; the system prompt is kept. */
|
|
40
|
+
clear() {
|
|
41
|
+
return NativeXybrid.contextClear(this.handle);
|
|
42
|
+
}
|
|
43
|
+
/** Id, history length and whether a system prompt is set. */
|
|
44
|
+
async info() {
|
|
45
|
+
return (await NativeXybrid.contextInfo(this.handle));
|
|
46
|
+
}
|
|
47
|
+
/** History turns in order, excluding the system prompt. */
|
|
48
|
+
async history() {
|
|
49
|
+
const wire = (await NativeXybrid.contextHistory(this.handle));
|
|
50
|
+
return wire.map(fromWireEnvelope);
|
|
51
|
+
}
|
|
52
|
+
/** Cap the history; older turns are pruned first, immediately. */
|
|
53
|
+
setMaxHistoryLength(length) {
|
|
54
|
+
return NativeXybrid.contextSetMaxHistoryLength(this.handle, length);
|
|
55
|
+
}
|
|
56
|
+
/** Free the native context. Runs already started keep their snapshot. */
|
|
57
|
+
release() {
|
|
58
|
+
return NativeXybrid.dispose(this.handle);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import NativeXybrid from './NativeXybrid';
|
|
2
|
+
import { fromWireEnvelope, normalizeImageFormat, toWireToolResult, } from './wire';
|
|
3
|
+
/**
|
|
4
|
+
* Envelope factories, named like the Swift and Kotlin `Envelope` helpers.
|
|
5
|
+
* Object literals (`{ kind: 'text', text }`) work just as well; these only
|
|
6
|
+
* save typing and apply the same defaults.
|
|
7
|
+
*/
|
|
8
|
+
export const Envelope = {
|
|
9
|
+
/** A text prompt, or text to speak with an optional TTS voice and speed. */
|
|
10
|
+
text(text, options = {}) {
|
|
11
|
+
return { kind: 'text', text, ...options };
|
|
12
|
+
},
|
|
13
|
+
/** A user turn for a conversation. */
|
|
14
|
+
user(text) {
|
|
15
|
+
return { kind: 'text', text, role: 'user' };
|
|
16
|
+
},
|
|
17
|
+
/** An assistant turn for a conversation (e.g. a model reply you keep). */
|
|
18
|
+
assistant(text) {
|
|
19
|
+
return { kind: 'text', text, role: 'assistant' };
|
|
20
|
+
},
|
|
21
|
+
/** A system prompt. */
|
|
22
|
+
system(text) {
|
|
23
|
+
return { kind: 'text', text, role: 'system' };
|
|
24
|
+
},
|
|
25
|
+
/** PCM/WAV audio, base64-encoded. Defaults to 16 kHz mono. */
|
|
26
|
+
audio(bytesBase64, options = {}) {
|
|
27
|
+
return {
|
|
28
|
+
kind: 'audio',
|
|
29
|
+
bytesBase64,
|
|
30
|
+
sampleRate: options.sampleRate ?? 16000,
|
|
31
|
+
channels: options.channels ?? 1,
|
|
32
|
+
};
|
|
33
|
+
},
|
|
34
|
+
/** An embedding vector. */
|
|
35
|
+
embedding(data) {
|
|
36
|
+
return { kind: 'embedding', data };
|
|
37
|
+
},
|
|
38
|
+
/**
|
|
39
|
+
* An encoded image for vision-language models. The bytes are validated
|
|
40
|
+
* natively at run time (`xybrid_invalid_image` on bad input).
|
|
41
|
+
*/
|
|
42
|
+
image(bytesBase64, format) {
|
|
43
|
+
return { kind: 'image', bytesBase64, format: normalizeImageFormat(format) };
|
|
44
|
+
},
|
|
45
|
+
/** A user message with image attachments, for vision-language models. */
|
|
46
|
+
userMessage(text, images = []) {
|
|
47
|
+
if (!images.every((image) => image?.kind === 'image')) {
|
|
48
|
+
throw new TypeError('Envelope.userMessage accepts only image envelopes');
|
|
49
|
+
}
|
|
50
|
+
return {
|
|
51
|
+
kind: 'multipart',
|
|
52
|
+
parts: [{ kind: 'text', text }, ...images],
|
|
53
|
+
role: 'user',
|
|
54
|
+
};
|
|
55
|
+
},
|
|
56
|
+
/**
|
|
57
|
+
* The continuation envelope for the turn after the model asked for tools.
|
|
58
|
+
*
|
|
59
|
+
* One `run` is one model turn, so the tool loop lives in your code: run a
|
|
60
|
+
* request carrying `generationConfig.tools`, execute every
|
|
61
|
+
* `result.toolCalls` entry, then run this envelope (with the same tools) to
|
|
62
|
+
* feed the outcomes back.
|
|
63
|
+
*
|
|
64
|
+
* @param userText the original user message of the turn being continued.
|
|
65
|
+
* @param priorAssistantText that turn's raw output — `result.text` from a
|
|
66
|
+
* batch run, or the terminal token's `rawText` when streaming.
|
|
67
|
+
* @param results tool outcomes, in call order.
|
|
68
|
+
*/
|
|
69
|
+
async toolResults(userText, priorAssistantText, results) {
|
|
70
|
+
const wire = (await NativeXybrid.toolResultsEnvelope(userText, priorAssistantText, results.map(toWireToolResult)));
|
|
71
|
+
return fromWireEnvelope(wire);
|
|
72
|
+
},
|
|
73
|
+
};
|