@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
package/src/wire.ts
ADDED
|
@@ -0,0 +1,408 @@
|
|
|
1
|
+
// Translation between the public types and the plain objects that cross the
|
|
2
|
+
// TurboModule boundary. Every convenience field (voiceId, speed, sampleRate,
|
|
3
|
+
// channels, role) is folded into / lifted out of envelope metadata HERE, once,
|
|
4
|
+
// so the native codecs stay generic and lossless: an envelope is a payload
|
|
5
|
+
// kind plus string metadata, exactly like bolt's `XybridEnvelope`.
|
|
6
|
+
//
|
|
7
|
+
// Pure functions only — no native calls — so tests/wire.test.mjs covers them
|
|
8
|
+
// on Node.
|
|
9
|
+
|
|
10
|
+
import type {
|
|
11
|
+
DownloadStatus,
|
|
12
|
+
Envelope,
|
|
13
|
+
EnvelopeMetadata,
|
|
14
|
+
GenerationConfig,
|
|
15
|
+
InferenceMetrics,
|
|
16
|
+
InferenceResult,
|
|
17
|
+
MessageRole,
|
|
18
|
+
OutputType,
|
|
19
|
+
PipelineResult,
|
|
20
|
+
StageResult,
|
|
21
|
+
StreamToken,
|
|
22
|
+
ToolCall,
|
|
23
|
+
ToolDefinition,
|
|
24
|
+
ToolResult,
|
|
25
|
+
} from './types';
|
|
26
|
+
|
|
27
|
+
// Metadata keys the Rust core reads; see the Swift/Kotlin envelope factories.
|
|
28
|
+
const KEY_VOICE_ID = 'voice_id';
|
|
29
|
+
const KEY_SPEED = 'speed';
|
|
30
|
+
const KEY_SAMPLE_RATE = 'sample_rate';
|
|
31
|
+
const KEY_CHANNELS = 'channels';
|
|
32
|
+
const KEY_ROLE = 'xybrid.role';
|
|
33
|
+
|
|
34
|
+
const ROLES: readonly MessageRole[] = ['system', 'user', 'assistant'];
|
|
35
|
+
|
|
36
|
+
// -- Wire shapes --------------------------------------------------------------
|
|
37
|
+
|
|
38
|
+
export type WireEnvelope =
|
|
39
|
+
| { kind: 'text'; text: string; metadata: EnvelopeMetadata }
|
|
40
|
+
| { kind: 'audio'; bytesBase64: string; metadata: EnvelopeMetadata }
|
|
41
|
+
| { kind: 'embedding'; data: number[]; metadata: EnvelopeMetadata }
|
|
42
|
+
| { kind: 'image'; bytesBase64: string; format: string; metadata: EnvelopeMetadata }
|
|
43
|
+
| { kind: 'multipart'; parts: WireEnvelope[]; metadata: EnvelopeMetadata };
|
|
44
|
+
|
|
45
|
+
export interface WireToolDefinition {
|
|
46
|
+
name: string;
|
|
47
|
+
description: string;
|
|
48
|
+
parametersJson: string;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export interface WireGenerationConfig {
|
|
52
|
+
maxTokens?: number;
|
|
53
|
+
temperature?: number;
|
|
54
|
+
topP?: number;
|
|
55
|
+
minP?: number;
|
|
56
|
+
topK?: number;
|
|
57
|
+
repetitionPenalty?: number;
|
|
58
|
+
stopSequences?: string[];
|
|
59
|
+
grammar?: string;
|
|
60
|
+
tools?: WireToolDefinition[];
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export interface WireRunOptions {
|
|
64
|
+
generationConfig?: WireGenerationConfig;
|
|
65
|
+
abortOn?: string[];
|
|
66
|
+
fallbackToCloud?: boolean;
|
|
67
|
+
maxGraceTokens?: number;
|
|
68
|
+
correlationId?: string;
|
|
69
|
+
/** Conversation-context handle. */
|
|
70
|
+
context?: string;
|
|
71
|
+
/** Cancellation-token handle. */
|
|
72
|
+
cancel?: string;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export interface WireResult {
|
|
76
|
+
envelope: WireEnvelope;
|
|
77
|
+
outputType: OutputType;
|
|
78
|
+
modelId: string;
|
|
79
|
+
latencyMs: number;
|
|
80
|
+
executionTarget: 'local' | 'cloud';
|
|
81
|
+
metrics: InferenceMetrics;
|
|
82
|
+
toolCalls: ToolCall[];
|
|
83
|
+
reasoningContent?: string;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export interface WireStreamToken {
|
|
87
|
+
token: string;
|
|
88
|
+
tokenId?: number;
|
|
89
|
+
index: number;
|
|
90
|
+
cumulativeText: string;
|
|
91
|
+
finishReason?: string;
|
|
92
|
+
toolCalls: ToolCall[];
|
|
93
|
+
rawText?: string;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export type WireStreamEvent =
|
|
97
|
+
| { kind: 'token'; token: WireStreamToken }
|
|
98
|
+
| { kind: 'complete'; result: WireResult };
|
|
99
|
+
|
|
100
|
+
export interface WireStageResult {
|
|
101
|
+
stageId: string;
|
|
102
|
+
envelope: WireEnvelope;
|
|
103
|
+
outputType: OutputType;
|
|
104
|
+
latencyMs: number;
|
|
105
|
+
executionTarget: 'local' | 'cloud';
|
|
106
|
+
metrics: InferenceMetrics;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export interface WirePipelineResult {
|
|
110
|
+
envelope: WireEnvelope;
|
|
111
|
+
outputType: OutputType;
|
|
112
|
+
latencyMs: number;
|
|
113
|
+
stages: WireStageResult[];
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// -- Envelopes ----------------------------------------------------------------
|
|
117
|
+
|
|
118
|
+
function setIfDefined(target: EnvelopeMetadata, key: string, value: unknown): void {
|
|
119
|
+
if (value !== undefined && value !== null) target[key] = String(value);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Fold an envelope's convenience fields into metadata. Typed fields win over
|
|
124
|
+
* raw `metadata` entries for the same key; audio input defaults to 16 kHz
|
|
125
|
+
* mono, like the Swift and Kotlin `Envelope.audio` factories.
|
|
126
|
+
*/
|
|
127
|
+
export function toWireEnvelope(envelope: Envelope): WireEnvelope {
|
|
128
|
+
if (envelope == null || typeof envelope !== 'object') {
|
|
129
|
+
throw new TypeError('An envelope must be an object with a `kind` field');
|
|
130
|
+
}
|
|
131
|
+
const metadata: EnvelopeMetadata = { ...(envelope.metadata ?? {}) };
|
|
132
|
+
setIfDefined(metadata, KEY_ROLE, envelope.role);
|
|
133
|
+
|
|
134
|
+
switch (envelope.kind) {
|
|
135
|
+
case 'text':
|
|
136
|
+
requireType(envelope.text, 'string', 'text envelope: `text`');
|
|
137
|
+
setIfDefined(metadata, KEY_VOICE_ID, envelope.voiceId);
|
|
138
|
+
setIfDefined(metadata, KEY_SPEED, envelope.speed);
|
|
139
|
+
return { kind: 'text', text: envelope.text, metadata };
|
|
140
|
+
case 'audio':
|
|
141
|
+
requireType(envelope.bytesBase64, 'string', 'audio envelope: `bytesBase64`');
|
|
142
|
+
setIfDefined(metadata, KEY_SAMPLE_RATE, envelope.sampleRate ?? metadata[KEY_SAMPLE_RATE] ?? 16000);
|
|
143
|
+
setIfDefined(metadata, KEY_CHANNELS, envelope.channels ?? metadata[KEY_CHANNELS] ?? 1);
|
|
144
|
+
return { kind: 'audio', bytesBase64: envelope.bytesBase64, metadata };
|
|
145
|
+
case 'embedding':
|
|
146
|
+
if (!Array.isArray(envelope.data)) {
|
|
147
|
+
throw new TypeError('embedding envelope: `data` must be a number array');
|
|
148
|
+
}
|
|
149
|
+
return { kind: 'embedding', data: envelope.data, metadata };
|
|
150
|
+
case 'image':
|
|
151
|
+
requireType(envelope.bytesBase64, 'string', 'image envelope: `bytesBase64`');
|
|
152
|
+
return {
|
|
153
|
+
kind: 'image',
|
|
154
|
+
bytesBase64: envelope.bytesBase64,
|
|
155
|
+
format: normalizeImageFormat(envelope.format),
|
|
156
|
+
metadata,
|
|
157
|
+
};
|
|
158
|
+
case 'multipart':
|
|
159
|
+
if (!Array.isArray(envelope.parts)) {
|
|
160
|
+
throw new TypeError('multipart envelope: `parts` must be an array');
|
|
161
|
+
}
|
|
162
|
+
return { kind: 'multipart', parts: envelope.parts.map(toWireEnvelope), metadata };
|
|
163
|
+
default:
|
|
164
|
+
throw new TypeError(
|
|
165
|
+
`Unknown envelope kind: ${String((envelope as { kind?: unknown }).kind)}`,
|
|
166
|
+
);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/** Lift well-known metadata keys back into typed fields. Lossless. */
|
|
171
|
+
export function fromWireEnvelope(wire: WireEnvelope): Envelope {
|
|
172
|
+
const metadata = { ...(wire.metadata ?? {}) };
|
|
173
|
+
const role = metadata[KEY_ROLE] as MessageRole | undefined;
|
|
174
|
+
const base = {
|
|
175
|
+
...(role && ROLES.includes(role) ? { role } : {}),
|
|
176
|
+
metadata,
|
|
177
|
+
};
|
|
178
|
+
switch (wire.kind) {
|
|
179
|
+
case 'text': {
|
|
180
|
+
const speed = parseNumber(metadata[KEY_SPEED]);
|
|
181
|
+
return {
|
|
182
|
+
kind: 'text',
|
|
183
|
+
text: wire.text,
|
|
184
|
+
...(metadata[KEY_VOICE_ID] !== undefined ? { voiceId: metadata[KEY_VOICE_ID] } : {}),
|
|
185
|
+
...(speed !== undefined ? { speed } : {}),
|
|
186
|
+
...base,
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
case 'audio': {
|
|
190
|
+
const sampleRate = parseNumber(metadata[KEY_SAMPLE_RATE]);
|
|
191
|
+
const channels = parseNumber(metadata[KEY_CHANNELS]);
|
|
192
|
+
return {
|
|
193
|
+
kind: 'audio',
|
|
194
|
+
bytesBase64: wire.bytesBase64,
|
|
195
|
+
...(sampleRate !== undefined ? { sampleRate } : {}),
|
|
196
|
+
...(channels !== undefined ? { channels } : {}),
|
|
197
|
+
...base,
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
case 'embedding':
|
|
201
|
+
return { kind: 'embedding', data: wire.data, ...base };
|
|
202
|
+
case 'image':
|
|
203
|
+
return {
|
|
204
|
+
kind: 'image',
|
|
205
|
+
bytesBase64: wire.bytesBase64,
|
|
206
|
+
format: normalizeImageFormat(wire.format),
|
|
207
|
+
...base,
|
|
208
|
+
};
|
|
209
|
+
case 'multipart':
|
|
210
|
+
return { kind: 'multipart', parts: wire.parts.map(fromWireEnvelope), ...base };
|
|
211
|
+
default:
|
|
212
|
+
throw new Error(
|
|
213
|
+
`Unexpected envelope kind from native: ${String((wire as { kind?: unknown }).kind)}`,
|
|
214
|
+
);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/** Accept `jpg` as an alias and reject anything the core cannot decode. */
|
|
219
|
+
export function normalizeImageFormat(format: string): 'png' | 'jpeg' | 'webp' {
|
|
220
|
+
const normalized = String(format ?? '').trim().toLowerCase();
|
|
221
|
+
if (normalized === 'jpg' || normalized === 'jpeg') return 'jpeg';
|
|
222
|
+
if (normalized === 'png' || normalized === 'webp') return normalized;
|
|
223
|
+
throw new TypeError(`Unsupported image format '${format}'. Supported: png, jpeg, jpg, webp`);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// -- Options ------------------------------------------------------------------
|
|
227
|
+
|
|
228
|
+
const GENERATION_KEYS = [
|
|
229
|
+
'maxTokens',
|
|
230
|
+
'temperature',
|
|
231
|
+
'topP',
|
|
232
|
+
'minP',
|
|
233
|
+
'topK',
|
|
234
|
+
'repetitionPenalty',
|
|
235
|
+
'stopSequences',
|
|
236
|
+
'grammar',
|
|
237
|
+
'tools',
|
|
238
|
+
] as const;
|
|
239
|
+
|
|
240
|
+
export function toWireToolDefinition(tool: ToolDefinition): WireToolDefinition {
|
|
241
|
+
const parameters = tool.parameters;
|
|
242
|
+
return {
|
|
243
|
+
name: tool.name,
|
|
244
|
+
description: tool.description,
|
|
245
|
+
parametersJson: typeof parameters === 'string' ? parameters : JSON.stringify(parameters),
|
|
246
|
+
};
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
export function toWireGenerationConfig(config: GenerationConfig): WireGenerationConfig {
|
|
250
|
+
const { tools, ...rest } = config;
|
|
251
|
+
return {
|
|
252
|
+
...rest,
|
|
253
|
+
...(tools ? { tools: tools.map(toWireToolDefinition) } : {}),
|
|
254
|
+
};
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
export function fromWireGenerationConfig(wire: WireGenerationConfig): GenerationConfig {
|
|
258
|
+
const { tools, ...rest } = wire;
|
|
259
|
+
return {
|
|
260
|
+
...rest,
|
|
261
|
+
tools: (tools ?? []).map((tool) => ({
|
|
262
|
+
name: tool.name,
|
|
263
|
+
description: tool.description,
|
|
264
|
+
parameters: tool.parametersJson,
|
|
265
|
+
})),
|
|
266
|
+
};
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/** Inputs to {@link toWireRunOptions}: the serializable `RunOptions` fields. */
|
|
270
|
+
export interface RunOptionsInput {
|
|
271
|
+
generationConfig?: GenerationConfig;
|
|
272
|
+
abortOn?: string[];
|
|
273
|
+
fallbackToCloud?: boolean;
|
|
274
|
+
maxGraceTokens?: number;
|
|
275
|
+
correlationId?: string;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/**
|
|
279
|
+
* Build the wire options. Sampling parameters must sit under
|
|
280
|
+
* `generationConfig`; passing them at the top level is a mistake that would
|
|
281
|
+
* otherwise be silently ignored, so it throws.
|
|
282
|
+
*/
|
|
283
|
+
export function toWireRunOptions(
|
|
284
|
+
options: RunOptionsInput | undefined,
|
|
285
|
+
handles: { context?: string | null; cancel?: string | null } = {},
|
|
286
|
+
): WireRunOptions | null {
|
|
287
|
+
if (options != null && typeof options !== 'object') {
|
|
288
|
+
throw new TypeError('Run options must be an object');
|
|
289
|
+
}
|
|
290
|
+
const misplaced = GENERATION_KEYS.filter((key) => options != null && key in options);
|
|
291
|
+
if (misplaced.length > 0) {
|
|
292
|
+
throw new TypeError(
|
|
293
|
+
`Sampling parameters belong under \`generationConfig\` (got ${misplaced.join(', ')} ` +
|
|
294
|
+
'at the top level of the run options)',
|
|
295
|
+
);
|
|
296
|
+
}
|
|
297
|
+
const wire: WireRunOptions = {};
|
|
298
|
+
if (options?.generationConfig) {
|
|
299
|
+
wire.generationConfig = toWireGenerationConfig(options.generationConfig);
|
|
300
|
+
}
|
|
301
|
+
if (options?.abortOn) wire.abortOn = [...options.abortOn];
|
|
302
|
+
if (options?.fallbackToCloud !== undefined) wire.fallbackToCloud = options.fallbackToCloud;
|
|
303
|
+
if (options?.maxGraceTokens !== undefined) wire.maxGraceTokens = options.maxGraceTokens;
|
|
304
|
+
if (options?.correlationId !== undefined) wire.correlationId = options.correlationId;
|
|
305
|
+
if (handles.context) wire.context = handles.context;
|
|
306
|
+
if (handles.cancel) wire.cancel = handles.cancel;
|
|
307
|
+
return Object.keys(wire).length > 0 ? wire : null;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
export function toWireToolResult(result: ToolResult): {
|
|
311
|
+
callId: string;
|
|
312
|
+
name: string;
|
|
313
|
+
contentJson: string;
|
|
314
|
+
} {
|
|
315
|
+
const { content } = result;
|
|
316
|
+
return {
|
|
317
|
+
callId: result.callId,
|
|
318
|
+
name: result.name,
|
|
319
|
+
contentJson: typeof content === 'string' ? content : JSON.stringify(content ?? null),
|
|
320
|
+
};
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
// -- Results ------------------------------------------------------------------
|
|
324
|
+
|
|
325
|
+
/** Convenience payload fields derived from an envelope (no copies). */
|
|
326
|
+
function payloadFields(envelope: WireEnvelope): {
|
|
327
|
+
text?: string;
|
|
328
|
+
audioBytesBase64?: string;
|
|
329
|
+
embedding?: number[];
|
|
330
|
+
} {
|
|
331
|
+
switch (envelope.kind) {
|
|
332
|
+
case 'text':
|
|
333
|
+
return { text: envelope.text };
|
|
334
|
+
case 'audio':
|
|
335
|
+
return { audioBytesBase64: envelope.bytesBase64 };
|
|
336
|
+
case 'embedding':
|
|
337
|
+
return { embedding: envelope.data };
|
|
338
|
+
default:
|
|
339
|
+
return {};
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
export function fromWireResult(wire: WireResult): InferenceResult {
|
|
344
|
+
return {
|
|
345
|
+
envelope: fromWireEnvelope(wire.envelope),
|
|
346
|
+
outputType: wire.outputType,
|
|
347
|
+
modelId: wire.modelId,
|
|
348
|
+
...payloadFields(wire.envelope),
|
|
349
|
+
...(wire.reasoningContent !== undefined ? { reasoningContent: wire.reasoningContent } : {}),
|
|
350
|
+
toolCalls: wire.toolCalls ?? [],
|
|
351
|
+
latencyMs: wire.latencyMs,
|
|
352
|
+
metrics: wire.metrics,
|
|
353
|
+
executionTarget: wire.executionTarget,
|
|
354
|
+
};
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
export function fromWireStreamToken(wire: WireStreamToken): StreamToken {
|
|
358
|
+
return { ...wire, toolCalls: wire.toolCalls ?? [] };
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
function fromWireStage(wire: WireStageResult): StageResult {
|
|
362
|
+
const payload = payloadFields(wire.envelope);
|
|
363
|
+
return {
|
|
364
|
+
stageId: wire.stageId,
|
|
365
|
+
envelope: fromWireEnvelope(wire.envelope),
|
|
366
|
+
outputType: wire.outputType,
|
|
367
|
+
...(payload.text !== undefined ? { text: payload.text } : {}),
|
|
368
|
+
...(payload.audioBytesBase64 !== undefined
|
|
369
|
+
? { audioBytesBase64: payload.audioBytesBase64 }
|
|
370
|
+
: {}),
|
|
371
|
+
latencyMs: wire.latencyMs,
|
|
372
|
+
executionTarget: wire.executionTarget,
|
|
373
|
+
metrics: wire.metrics,
|
|
374
|
+
};
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
export function fromWirePipelineResult(wire: WirePipelineResult): PipelineResult {
|
|
378
|
+
const payload = payloadFields(wire.envelope);
|
|
379
|
+
return {
|
|
380
|
+
envelope: fromWireEnvelope(wire.envelope),
|
|
381
|
+
outputType: wire.outputType,
|
|
382
|
+
...(payload.text !== undefined ? { text: payload.text } : {}),
|
|
383
|
+
...(payload.audioBytesBase64 !== undefined
|
|
384
|
+
? { audioBytesBase64: payload.audioBytesBase64 }
|
|
385
|
+
: {}),
|
|
386
|
+
latencyMs: wire.latencyMs,
|
|
387
|
+
stages: wire.stages.map(fromWireStage),
|
|
388
|
+
};
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
export function fromWireDownloadStatus(wire: DownloadStatus): DownloadStatus {
|
|
392
|
+
// Both shims omit `totalBytes` when unknown; drop an explicit null too so
|
|
393
|
+
// callers can rely on `status.totalBytes === undefined`.
|
|
394
|
+
const { totalBytes, ...rest } = wire;
|
|
395
|
+
return totalBytes == null ? rest : { ...rest, totalBytes };
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
// -- Helpers ------------------------------------------------------------------
|
|
399
|
+
|
|
400
|
+
function requireType(value: unknown, type: 'string', what: string): void {
|
|
401
|
+
if (typeof value !== type) throw new TypeError(`${what} must be a ${type}`);
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
function parseNumber(raw: string | undefined): number | undefined {
|
|
405
|
+
if (raw === undefined) return undefined;
|
|
406
|
+
const value = Number(raw);
|
|
407
|
+
return Number.isFinite(value) ? value : undefined;
|
|
408
|
+
}
|
package/src/xybrid.ts
ADDED
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
import { AppState } from 'react-native';
|
|
2
|
+
|
|
3
|
+
import NativeXybrid from './NativeXybrid';
|
|
4
|
+
import { ModelLoader } from './model';
|
|
5
|
+
import type { CacheEntry, CacheStatus, ThermalState } from './types';
|
|
6
|
+
|
|
7
|
+
/** Options for {@link Xybrid.initialize}. All optional. */
|
|
8
|
+
export interface XybridInitOptions {
|
|
9
|
+
/**
|
|
10
|
+
* Xybrid API key. Enables the platform features on top of local inference:
|
|
11
|
+
* telemetry to the dashboard, cloud fallback, speculative cloud serving.
|
|
12
|
+
* Get one at https://dashboard.xybrid.dev.
|
|
13
|
+
*/
|
|
14
|
+
apiKey?: string;
|
|
15
|
+
/** Override the LLM gateway URL (full URL including `/v1`). */
|
|
16
|
+
gatewayUrl?: string;
|
|
17
|
+
/** Override the telemetry ingest URL (self-hosted dashboards). */
|
|
18
|
+
ingestUrl?: string;
|
|
19
|
+
/**
|
|
20
|
+
* Where downloaded models live. Defaults to `<Caches>/xybrid/models` on
|
|
21
|
+
* iOS and `<filesDir>/xybrid/models` on Android. Only honoured when
|
|
22
|
+
* `initialize` is the first Xybrid call of the process.
|
|
23
|
+
*/
|
|
24
|
+
cacheDir?: string;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
let initialized = false;
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Process-wide SDK entry points. Local inference needs no setup at all —
|
|
31
|
+
* `initialize` only matters for the optional platform features.
|
|
32
|
+
*/
|
|
33
|
+
export const Xybrid = {
|
|
34
|
+
/**
|
|
35
|
+
* Apply process-wide configuration. The SDK otherwise configures itself
|
|
36
|
+
* anonymously on first use, so call this before anything else when you
|
|
37
|
+
* have options. Configuration is applied once per app process: repeating
|
|
38
|
+
* it with the same options resolves, with different options rejects
|
|
39
|
+
* (`xybrid_config_error`) — restart the app to change them.
|
|
40
|
+
*/
|
|
41
|
+
async initialize(options: XybridInitOptions = {}): Promise<void> {
|
|
42
|
+
await NativeXybrid.initialize(options);
|
|
43
|
+
initialized = true;
|
|
44
|
+
},
|
|
45
|
+
|
|
46
|
+
/** Whether {@link initialize} has resolved in this JS context. */
|
|
47
|
+
get isInitialized(): boolean {
|
|
48
|
+
return initialized;
|
|
49
|
+
},
|
|
50
|
+
|
|
51
|
+
/** Describe a registry model without loading it (same as `ModelLoader.fromRegistry`). */
|
|
52
|
+
model(id: string): ModelLoader {
|
|
53
|
+
return ModelLoader.fromRegistry(id);
|
|
54
|
+
},
|
|
55
|
+
|
|
56
|
+
/** The native SDK version, e.g. `0.9.0`. */
|
|
57
|
+
version(): Promise<string> {
|
|
58
|
+
return NativeXybrid.sdkVersion();
|
|
59
|
+
},
|
|
60
|
+
|
|
61
|
+
/** Whether an API key resolves, from {@link initialize} or the environment. */
|
|
62
|
+
hasApiKey(): Promise<boolean> {
|
|
63
|
+
return NativeXybrid.hasApiKey();
|
|
64
|
+
},
|
|
65
|
+
|
|
66
|
+
/** Set the key for a cloud provider the gateway forwards to (not the Xybrid key). */
|
|
67
|
+
setProviderApiKey(provider: string, apiKey: string): Promise<void> {
|
|
68
|
+
return NativeXybrid.setProviderApiKey(provider, apiKey);
|
|
69
|
+
},
|
|
70
|
+
|
|
71
|
+
/** Point the cloud gateway at a platform base URL; the `/v1` suffix is added for you. */
|
|
72
|
+
setPlatformUrl(url: string): Promise<void> {
|
|
73
|
+
return NativeXybrid.setPlatformUrl(url);
|
|
74
|
+
},
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Default speculative cloud serving on or off for loads that don't opt in
|
|
78
|
+
* per load (`ModelLoader.fromRegistrySpeculative` always speculates). Needs
|
|
79
|
+
* an API key. LLM/chat only — prefer the per-load form when the app also
|
|
80
|
+
* loads ASR/TTS models.
|
|
81
|
+
*/
|
|
82
|
+
setSpeculativeCloud(enabled: boolean): Promise<void> {
|
|
83
|
+
return NativeXybrid.setSpeculativeCloud(enabled);
|
|
84
|
+
},
|
|
85
|
+
|
|
86
|
+
isSpeculativeCloudEnabled(): Promise<boolean> {
|
|
87
|
+
return NativeXybrid.isSpeculativeCloudEnabled();
|
|
88
|
+
},
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Release every idle model's memory; resolves how many were released. Runs
|
|
92
|
+
* in flight are skipped, and a released model reloads itself on next use.
|
|
93
|
+
*/
|
|
94
|
+
releaseMemory(): Promise<number> {
|
|
95
|
+
return NativeXybrid.releaseMemory();
|
|
96
|
+
},
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Call {@link releaseMemory} on every OS memory warning (React Native
|
|
100
|
+
* `AppState` `memoryWarning`, iOS and Android). Returns the unsubscribe
|
|
101
|
+
* function.
|
|
102
|
+
*/
|
|
103
|
+
releaseMemoryOnWarning(): () => void {
|
|
104
|
+
const subscription = AppState.addEventListener('memoryWarning', () => {
|
|
105
|
+
NativeXybrid.releaseMemory().catch(() => {});
|
|
106
|
+
});
|
|
107
|
+
return () => subscription.remove();
|
|
108
|
+
},
|
|
109
|
+
|
|
110
|
+
/** Release least-recently-used idle models before loads under memory pressure. */
|
|
111
|
+
setAutoRelease(enabled: boolean): Promise<void> {
|
|
112
|
+
return NativeXybrid.setAutoRelease(enabled);
|
|
113
|
+
},
|
|
114
|
+
|
|
115
|
+
isAutoReleaseEnabled(): Promise<boolean> {
|
|
116
|
+
return NativeXybrid.isAutoReleaseEnabled();
|
|
117
|
+
},
|
|
118
|
+
|
|
119
|
+
// -- Device state. Both platforms already observe battery (and Android
|
|
120
|
+
// thermal) natively; these exist for tests and custom readings. --
|
|
121
|
+
|
|
122
|
+
/** Push a battery percentage (0–100) to the routing engine. */
|
|
123
|
+
setBatteryLevel(percent: number): Promise<void> {
|
|
124
|
+
return NativeXybrid.setBatteryLevel(percent);
|
|
125
|
+
},
|
|
126
|
+
|
|
127
|
+
clearBatteryLevel(): Promise<void> {
|
|
128
|
+
return NativeXybrid.clearBatteryLevel();
|
|
129
|
+
},
|
|
130
|
+
|
|
131
|
+
setThermalState(state: ThermalState): Promise<void> {
|
|
132
|
+
return NativeXybrid.setThermalState(state);
|
|
133
|
+
},
|
|
134
|
+
|
|
135
|
+
clearThermalState(): Promise<void> {
|
|
136
|
+
return NativeXybrid.clearThermalState();
|
|
137
|
+
},
|
|
138
|
+
|
|
139
|
+
// -- Model cache. Every call walks or deletes the cache on disk, off the
|
|
140
|
+
// JS thread. --
|
|
141
|
+
|
|
142
|
+
/** Aggregate storage usage across every managed cache location. */
|
|
143
|
+
async modelCacheStatus(): Promise<CacheStatus> {
|
|
144
|
+
return (await NativeXybrid.cacheStatus()) as CacheStatus;
|
|
145
|
+
},
|
|
146
|
+
|
|
147
|
+
/** Physical entries; a model can appear more than once. */
|
|
148
|
+
async modelCacheEntries(): Promise<CacheEntry[]> {
|
|
149
|
+
return (await NativeXybrid.cacheEntries()) as CacheEntry[];
|
|
150
|
+
},
|
|
151
|
+
|
|
152
|
+
/** Whether `modelId` occupies any cache entry. */
|
|
153
|
+
hasCachedModelData(modelId: string): Promise<boolean> {
|
|
154
|
+
return NativeXybrid.cacheIsModelCached(modelId);
|
|
155
|
+
},
|
|
156
|
+
|
|
157
|
+
/** A local path for `modelId`, or `null`. Presence does not mean extracted. */
|
|
158
|
+
cachedModelPath(modelId: string): Promise<string | null> {
|
|
159
|
+
return NativeXybrid.cacheModelPath(modelId);
|
|
160
|
+
},
|
|
161
|
+
|
|
162
|
+
/** Models extracted, validated and ready to run offline. */
|
|
163
|
+
extractedModelIds(): Promise<string[]> {
|
|
164
|
+
return NativeXybrid.cacheExtractedModelIds();
|
|
165
|
+
},
|
|
166
|
+
|
|
167
|
+
/** Remove every cache entry for `modelId`. Don't race a load of the same model. */
|
|
168
|
+
removeCachedModel(modelId: string): Promise<number> {
|
|
169
|
+
return NativeXybrid.cacheRemoveModel(modelId);
|
|
170
|
+
},
|
|
171
|
+
|
|
172
|
+
/** Clear all managed cache storage. Don't race any model load. */
|
|
173
|
+
clearModelCache(): Promise<number> {
|
|
174
|
+
return NativeXybrid.cacheClear();
|
|
175
|
+
},
|
|
176
|
+
};
|