@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.
Files changed (87) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +298 -0
  3. package/android/build.gradle +61 -0
  4. package/android/src/main/AndroidManifest.xml +1 -0
  5. package/android/src/main/java/ai/xybrid/reactnative/XybridCodec.kt +481 -0
  6. package/android/src/main/java/ai/xybrid/reactnative/XybridHandles.kt +139 -0
  7. package/android/src/main/java/ai/xybrid/reactnative/XybridModule.kt +665 -0
  8. package/android/src/main/java/ai/xybrid/reactnative/XybridPackage.kt +27 -0
  9. package/ios/XybridCodec.swift +508 -0
  10. package/ios/XybridHandles.swift +191 -0
  11. package/ios/XybridModule.mm +494 -0
  12. package/ios/XybridModuleImpl.swift +887 -0
  13. package/ios/XybridSwift/Xybrid.swift +1401 -0
  14. package/ios/XybridSwift/xybrid_bolt.swift +3198 -0
  15. package/ios/xybrid_natives.rb +178 -0
  16. package/lib/commonjs/NativeXybrid.js +24 -0
  17. package/lib/commonjs/base64.js +45 -0
  18. package/lib/commonjs/cancel.js +35 -0
  19. package/lib/commonjs/context.js +67 -0
  20. package/lib/commonjs/envelope.js +79 -0
  21. package/lib/commonjs/errors.js +61 -0
  22. package/lib/commonjs/index.js +59 -0
  23. package/lib/commonjs/model.js +307 -0
  24. package/lib/commonjs/pipeline.js +55 -0
  25. package/lib/commonjs/presets.js +32 -0
  26. package/lib/commonjs/session.js +76 -0
  27. package/lib/commonjs/types.js +10 -0
  28. package/lib/commonjs/wire.js +285 -0
  29. package/lib/commonjs/xybrid.js +133 -0
  30. package/lib/module/NativeXybrid.js +22 -0
  31. package/lib/module/base64.js +41 -0
  32. package/lib/module/cancel.js +29 -0
  33. package/lib/module/context.js +60 -0
  34. package/lib/module/envelope.js +73 -0
  35. package/lib/module/errors.js +56 -0
  36. package/lib/module/index.js +36 -0
  37. package/lib/module/model.js +298 -0
  38. package/lib/module/pipeline.js +48 -0
  39. package/lib/module/presets.js +27 -0
  40. package/lib/module/session.js +69 -0
  41. package/lib/module/types.js +9 -0
  42. package/lib/module/wire.js +271 -0
  43. package/lib/module/xybrid.js +127 -0
  44. package/lib/typescript/NativeXybrid.d.ts +68 -0
  45. package/lib/typescript/NativeXybrid.d.ts.map +1 -0
  46. package/lib/typescript/base64.d.ts +5 -0
  47. package/lib/typescript/base64.d.ts.map +1 -0
  48. package/lib/typescript/cancel.d.ts +24 -0
  49. package/lib/typescript/cancel.d.ts.map +1 -0
  50. package/lib/typescript/context.d.ts +40 -0
  51. package/lib/typescript/context.d.ts.map +1 -0
  52. package/lib/typescript/envelope.d.ts +51 -0
  53. package/lib/typescript/envelope.d.ts.map +1 -0
  54. package/lib/typescript/errors.d.ts +12 -0
  55. package/lib/typescript/errors.d.ts.map +1 -0
  56. package/lib/typescript/index.d.ts +28 -0
  57. package/lib/typescript/index.d.ts.map +1 -0
  58. package/lib/typescript/model.d.ts +211 -0
  59. package/lib/typescript/model.d.ts.map +1 -0
  60. package/lib/typescript/pipeline.d.ts +35 -0
  61. package/lib/typescript/pipeline.d.ts.map +1 -0
  62. package/lib/typescript/presets.d.ts +21 -0
  63. package/lib/typescript/presets.d.ts.map +1 -0
  64. package/lib/typescript/session.d.ts +50 -0
  65. package/lib/typescript/session.d.ts.map +1 -0
  66. package/lib/typescript/types.d.ts +313 -0
  67. package/lib/typescript/types.d.ts.map +1 -0
  68. package/lib/typescript/wire.d.ts +130 -0
  69. package/lib/typescript/wire.d.ts.map +1 -0
  70. package/lib/typescript/xybrid.d.ts +89 -0
  71. package/lib/typescript/xybrid.d.ts.map +1 -0
  72. package/package.json +88 -0
  73. package/react-native-xybrid.podspec +50 -0
  74. package/src/NativeXybrid.ts +131 -0
  75. package/src/base64.ts +43 -0
  76. package/src/cancel.ts +47 -0
  77. package/src/context.ts +70 -0
  78. package/src/envelope.ts +115 -0
  79. package/src/errors.ts +67 -0
  80. package/src/index.ts +91 -0
  81. package/src/model.ts +408 -0
  82. package/src/pipeline.ts +66 -0
  83. package/src/presets.ts +36 -0
  84. package/src/session.ts +76 -0
  85. package/src/types.ts +362 -0
  86. package/src/wire.ts +408 -0
  87. 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
+ };