@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
@@ -0,0 +1,50 @@
1
+ require "json"
2
+ require_relative "ios/xybrid_natives"
3
+
4
+ package = JSON.parse(File.read(File.join(__dir__, "package.json")))
5
+
6
+ Pod::Spec.new do |s|
7
+ # The npm package is @xybrid/react-native; pod names cannot contain `@` or
8
+ # `/`, so the pod keeps this name (it only shows up in Podfile.lock).
9
+ s.name = "react-native-xybrid"
10
+ s.version = package["version"]
11
+ s.summary = package["description"]
12
+ s.homepage = package["homepage"]
13
+ s.license = package["license"]
14
+ s.authors = package["author"]
15
+
16
+ # Matches the XCFramework's minimum (bindings/apple/BUILD.bazel) and
17
+ # Package.swift; React Native 0.76+ already needs iOS 15.1.
18
+ s.platforms = { :ios => "16.0" }
19
+ s.source = { :git => "https://github.com/xybrid-ai/xybrid.git", :tag => "v#{s.version}" }
20
+
21
+ # The TurboModule (XybridModule.mm + the Swift implementation) and the
22
+ # Swift SDK it calls (ios/XybridSwift: Xybrid.swift + xybrid_bolt.swift, the
23
+ # same sources the standalone Apple SDK ships) compile into this one pod.
24
+ s.source_files = "ios/*.{mm,swift}", "ios/XybridSwift/*.swift"
25
+ s.swift_version = "5.0"
26
+
27
+ # The Rust core, resolved at `pod install` time — downloaded from the
28
+ # GitHub Release and checked against package.json's pinned SHA-256, or a
29
+ # local build. See ios/xybrid_natives.rb for the order and overrides.
30
+ s.vendored_frameworks = XybridNatives.prepare!(__dir__)
31
+
32
+ # System frameworks the Rust core links against (mirrors Package.swift).
33
+ s.frameworks = "Metal", "MetalPerformanceShaders", "MetalPerformanceShadersGraph",
34
+ "CoreML", "Accelerate", "Security"
35
+ s.libraries = "c++"
36
+
37
+ s.pod_target_xcconfig = {
38
+ "DEFINES_MODULE" => "YES",
39
+ # A method missing from the Codegen protocol only warns — and then
40
+ # crashes at the first JS call, because the TurboModule looks methods up
41
+ # by the protocol's selectors. Make it a build error instead.
42
+ "WARNING_CFLAGS" => "$(inherited) -Werror=protocol",
43
+ # The XCFramework has no x86_64 simulator slice (no prebuilt ONNX Runtime
44
+ # for Intel simulators), so build simulator targets for arm64 only.
45
+ "EXCLUDED_ARCHS[sdk=iphonesimulator*]" => "i386 x86_64",
46
+ }
47
+
48
+ # React Native's New Architecture wiring (TurboModule headers + Codegen).
49
+ install_modules_dependencies(s)
50
+ end
@@ -0,0 +1,131 @@
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
+
22
+ import type { TurboModule } from 'react-native';
23
+ import { TurboModuleRegistry } from 'react-native';
24
+
25
+ export interface Spec extends TurboModule {
26
+ // -- SDK configuration --
27
+ // `options` is `{ apiKey?, gatewayUrl?, ingestUrl?, cacheDir? }`. The SDK
28
+ // configures itself anonymously on first use; the first `initialize` that
29
+ // carries options applies them, and later ones reject if they differ.
30
+ initialize(options: Object | null): Promise<void>;
31
+ sdkVersion(): Promise<string>;
32
+ hasApiKey(): Promise<boolean>;
33
+ setProviderApiKey(provider: string, apiKey: string): Promise<void>;
34
+ setPlatformUrl(url: string): Promise<void>;
35
+ setSpeculativeCloud(enabled: boolean): Promise<void>;
36
+ isSpeculativeCloudEnabled(): Promise<boolean>;
37
+ willSpeculate(modelId: string): Promise<boolean>;
38
+ releaseMemory(): Promise<number>;
39
+ setAutoRelease(enabled: boolean): Promise<void>;
40
+ isAutoReleaseEnabled(): Promise<boolean>;
41
+
42
+ // -- Device state push (both platforms also observe it natively) --
43
+ setBatteryLevel(percent: number): Promise<void>;
44
+ clearBatteryLevel(): Promise<void>;
45
+ setThermalState(state: string): Promise<void>;
46
+ clearThermalState(): Promise<void>;
47
+
48
+ // -- Model cache --
49
+ cacheStatus(): Promise<Object>;
50
+ cacheEntries(): Promise<Object[]>;
51
+ cacheIsModelCached(modelId: string): Promise<boolean>;
52
+ cacheModelPath(modelId: string): Promise<string | null>;
53
+ cacheExtractedModelIds(): Promise<string[]>;
54
+ cacheRemoveModel(modelId: string): Promise<number>;
55
+ cacheClear(): Promise<number>;
56
+
57
+ // -- Stateless helpers --
58
+ jsonSchemaToGbnf(schemaJson: string): Promise<string>;
59
+ toolResultsEnvelope(
60
+ userText: string,
61
+ priorAssistantText: string,
62
+ results: Object[],
63
+ ): Promise<Object>;
64
+
65
+ // -- Handles --
66
+ dispose(handle: string): Promise<void>;
67
+
68
+ // -- Models --
69
+ // `source` is `{ kind, value, revision? }`, kind one of `registry`,
70
+ // `registrySpeculative`, `bundle`, `directory`, `huggingFace`, `modelFile`.
71
+ loadModel(source: Object): Promise<string>;
72
+ modelInfo(model: string): Promise<Object>;
73
+ isLoaded(model: string): Promise<boolean>;
74
+ warmup(model: string): Promise<void>;
75
+ unload(model: string): Promise<void>;
76
+ isCloudServing(model: string): Promise<boolean>;
77
+ downloadStatus(model: string): Promise<Object>;
78
+ awaitDownload(model: string, timeoutMs: number): Promise<Object>;
79
+ voices(model: string): Promise<Object[]>;
80
+ defaultVoice(model: string): Promise<Object | null>;
81
+ voice(model: string, voiceId: string): Promise<Object | null>;
82
+
83
+ // -- Inference --
84
+ // `options` is the wire form of `RunOptions`: sampling config, platform
85
+ // knobs, plus `context` (a conversation handle) and `cancel` (a
86
+ // cancellation-token handle) resolved natively.
87
+ run(model: string, envelope: Object, options: Object | null): Promise<Object>;
88
+ // Pull-based token streaming. `streamNext` resolves each event
89
+ // (`{ kind: 'token', token }` or `{ kind: 'complete', result }`) and `null`
90
+ // once the stream is gone; failures reject with the same codes as `run`.
91
+ // Disposing a stream aborts its generation.
92
+ streamStart(model: string, envelope: Object, options: Object | null): Promise<string>;
93
+ streamNext(stream: string): Promise<Object | null>;
94
+
95
+ // -- Cancellation tokens (the stop button behind `RunOptions.signal`) --
96
+ createCancelToken(): Promise<string>;
97
+ cancel(token: string): Promise<void>;
98
+
99
+ // -- Conversation contexts --
100
+ createContext(contextId: string | null): Promise<string>;
101
+ contextPush(context: string, envelope: Object): Promise<void>;
102
+ contextSetSystem(context: string, envelope: Object): Promise<void>;
103
+ contextClear(context: string): Promise<void>;
104
+ contextInfo(context: string): Promise<Object>;
105
+ contextHistory(context: string): Promise<Object[]>;
106
+ contextSetMaxHistoryLength(context: string, length: number): Promise<void>;
107
+
108
+ // -- Standalone downloads (fill the cache without loading) --
109
+ startDownload(modelId: string, platform: string | null): Promise<string>;
110
+ downloadHandleStatus(download: string): Promise<Object>;
111
+ downloadHandleError(download: string): Promise<string | null>;
112
+ cancelDownload(download: string): Promise<void>;
113
+
114
+ // -- Pipelines --
115
+ // `source` is `{ kind: 'yaml' | 'file' | 'bundle', value }`.
116
+ loadPipeline(source: Object): Promise<string>;
117
+ pipelineInfo(pipeline: string): Promise<Object>;
118
+ runPipeline(pipeline: string, envelope: Object, options: Object | null): Promise<Object>;
119
+
120
+ // -- Live ASR sessions --
121
+ // `samplesBase64` is little-endian Float32 PCM, mono, 16 kHz.
122
+ openStreamingSession(model: string, config: Object | null): Promise<string>;
123
+ sessionFeed(session: string, samplesBase64: string): Promise<void>;
124
+ sessionNextPartial(session: string): Promise<Object | null>;
125
+ sessionFlush(session: string): Promise<string>;
126
+ sessionReset(session: string): Promise<void>;
127
+ sessionCancel(session: string): Promise<void>;
128
+ sessionIsRunning(session: string): Promise<boolean>;
129
+ }
130
+
131
+ export default TurboModuleRegistry.getEnforcing<Spec>('RNXybrid');
package/src/base64.ts ADDED
@@ -0,0 +1,43 @@
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
+
6
+ const ALPHABET = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/';
7
+
8
+ /** Standard (RFC 4648, padded) base64 of `bytes`. */
9
+ export function bytesToBase64(bytes: Uint8Array): string {
10
+ const chunks: string[] = [];
11
+ let chunk = '';
12
+ let i = 0;
13
+ for (; i + 2 < bytes.length; i += 3) {
14
+ const n = (bytes[i] << 16) | (bytes[i + 1] << 8) | bytes[i + 2];
15
+ chunk +=
16
+ ALPHABET[(n >> 18) & 63] +
17
+ ALPHABET[(n >> 12) & 63] +
18
+ ALPHABET[(n >> 6) & 63] +
19
+ ALPHABET[n & 63];
20
+ // Flush periodically so long inputs don't build one enormous rope.
21
+ if (chunk.length >= 8192) {
22
+ chunks.push(chunk);
23
+ chunk = '';
24
+ }
25
+ }
26
+ const rest = bytes.length - i;
27
+ if (rest === 1) {
28
+ const n = bytes[i] << 16;
29
+ chunk += ALPHABET[(n >> 18) & 63] + ALPHABET[(n >> 12) & 63] + '==';
30
+ } else if (rest === 2) {
31
+ const n = (bytes[i] << 16) | (bytes[i + 1] << 8);
32
+ chunk +=
33
+ ALPHABET[(n >> 18) & 63] + ALPHABET[(n >> 12) & 63] + ALPHABET[(n >> 6) & 63] + '=';
34
+ }
35
+ chunks.push(chunk);
36
+ return chunks.join('');
37
+ }
38
+
39
+ /** Base64 of Float32 PCM samples as little-endian bytes. */
40
+ export function float32ToBase64(samples: Float32Array | ArrayLike<number>): string {
41
+ const floats = samples instanceof Float32Array ? samples : Float32Array.from(samples);
42
+ return bytesToBase64(new Uint8Array(floats.buffer, floats.byteOffset, floats.byteLength));
43
+ }
package/src/cancel.ts ADDED
@@ -0,0 +1,47 @@
1
+ import NativeXybrid from './NativeXybrid';
2
+
3
+ /**
4
+ * The part of the standard `AbortSignal` this SDK needs, so any
5
+ * implementation (React Native's `AbortController`, a polyfill) works.
6
+ */
7
+ export interface AbortSignalLike {
8
+ readonly aborted: boolean;
9
+ addEventListener(type: 'abort', listener: () => void): void;
10
+ removeEventListener(type: 'abort', listener: () => void): void;
11
+ }
12
+
13
+ /** A native cancellation token bound to an `AbortSignal` for one call. */
14
+ export interface CancelBinding {
15
+ /** Native token handle, or `null` when the call has no signal. */
16
+ readonly token: string | null;
17
+ /** Detach from the signal and free the native token. Never rejects. */
18
+ release(): Promise<void>;
19
+ }
20
+
21
+ const NO_SIGNAL: CancelBinding = { token: null, release: () => Promise.resolve() };
22
+
23
+ /**
24
+ * Create a native stop button wired to `signal`. Aborting the signal cancels
25
+ * the token, which stops generation at the next token boundary. A signal
26
+ * that is already aborted cancels the token before the call starts, so the
27
+ * call rejects with `xybrid_cancelled` instead of running.
28
+ */
29
+ export async function bindCancel(signal: AbortSignalLike | undefined): Promise<CancelBinding> {
30
+ if (!signal) return NO_SIGNAL;
31
+ const token = await NativeXybrid.createCancelToken();
32
+ const onAbort = () => {
33
+ NativeXybrid.cancel(token).catch(() => {});
34
+ };
35
+ if (signal.aborted) {
36
+ await NativeXybrid.cancel(token);
37
+ } else {
38
+ signal.addEventListener('abort', onAbort);
39
+ }
40
+ return {
41
+ token,
42
+ release: async () => {
43
+ signal.removeEventListener('abort', onAbort);
44
+ await NativeXybrid.dispose(token).catch(() => {});
45
+ },
46
+ };
47
+ }
package/src/context.ts ADDED
@@ -0,0 +1,70 @@
1
+ import NativeXybrid from './NativeXybrid';
2
+ import type { ConversationInfo, Envelope } from './types';
3
+ import { fromWireEnvelope, toWireEnvelope, type WireEnvelope } from './wire';
4
+
5
+ /**
6
+ * Multi-turn conversation history, held natively. Pass it to
7
+ * `model.run(envelope, { context })` or `runStreaming`; inference reads the
8
+ * history but never changes it. The prompt you run is appended to the
9
+ * history for that run only, so push both turns *after* the run — pushing the
10
+ * prompt first would send it twice:
11
+ *
12
+ * ```ts
13
+ * const chat = await ConversationContext.create();
14
+ * await chat.setSystem('You are terse.');
15
+ *
16
+ * const question = Envelope.user('What is the capital of France?');
17
+ * const reply = await model.run(question, { context: chat });
18
+ * await chat.push(question);
19
+ * await chat.push(reply.envelope); // already tagged `assistant`
20
+ * ```
21
+ */
22
+ export class ConversationContext {
23
+ private constructor(
24
+ /** Opaque native handle. */
25
+ readonly handle: string,
26
+ ) {}
27
+
28
+ /** A new, empty conversation; `id` defaults to a fresh one (telemetry correlation). */
29
+ static async create(id?: string): Promise<ConversationContext> {
30
+ return new ConversationContext(await NativeXybrid.createContext(id ?? null));
31
+ }
32
+
33
+ /** Append a turn. Give it a `role` (see `Envelope.user` / `Envelope.assistant`). */
34
+ push(envelope: Envelope): Promise<void> {
35
+ return NativeXybrid.contextPush(this.handle, toWireEnvelope(envelope));
36
+ }
37
+
38
+ /** Set the persistent system prompt; it survives {@link clear}. */
39
+ setSystem(system: Envelope | string): Promise<void> {
40
+ const envelope: Envelope =
41
+ typeof system === 'string' ? { kind: 'text', text: system, role: 'system' } : system;
42
+ return NativeXybrid.contextSetSystem(this.handle, toWireEnvelope(envelope));
43
+ }
44
+
45
+ /** Drop the history; the system prompt is kept. */
46
+ clear(): Promise<void> {
47
+ return NativeXybrid.contextClear(this.handle);
48
+ }
49
+
50
+ /** Id, history length and whether a system prompt is set. */
51
+ async info(): Promise<ConversationInfo> {
52
+ return (await NativeXybrid.contextInfo(this.handle)) as ConversationInfo;
53
+ }
54
+
55
+ /** History turns in order, excluding the system prompt. */
56
+ async history(): Promise<Envelope[]> {
57
+ const wire = (await NativeXybrid.contextHistory(this.handle)) as WireEnvelope[];
58
+ return wire.map(fromWireEnvelope);
59
+ }
60
+
61
+ /** Cap the history; older turns are pruned first, immediately. */
62
+ setMaxHistoryLength(length: number): Promise<void> {
63
+ return NativeXybrid.contextSetMaxHistoryLength(this.handle, length);
64
+ }
65
+
66
+ /** Free the native context. Runs already started keep their snapshot. */
67
+ release(): Promise<void> {
68
+ return NativeXybrid.dispose(this.handle);
69
+ }
70
+ }
@@ -0,0 +1,115 @@
1
+ import NativeXybrid from './NativeXybrid';
2
+ import type {
3
+ AudioEnvelope,
4
+ EmbeddingEnvelope,
5
+ Envelope as EnvelopeType,
6
+ ImageEnvelope,
7
+ ImageFormat,
8
+ MessageRole,
9
+ MultiPartEnvelope,
10
+ TextEnvelope,
11
+ ToolResult,
12
+ } from './types';
13
+ import {
14
+ fromWireEnvelope,
15
+ normalizeImageFormat,
16
+ toWireToolResult,
17
+ type WireEnvelope,
18
+ } from './wire';
19
+
20
+ /** Input to (and output of) a model. Narrow on `kind`. */
21
+ export type Envelope = EnvelopeType;
22
+
23
+ /**
24
+ * Envelope factories, named like the Swift and Kotlin `Envelope` helpers.
25
+ * Object literals (`{ kind: 'text', text }`) work just as well; these only
26
+ * save typing and apply the same defaults.
27
+ */
28
+ export const Envelope = {
29
+ /** A text prompt, or text to speak with an optional TTS voice and speed. */
30
+ text(
31
+ text: string,
32
+ options: { voiceId?: string; speed?: number; role?: MessageRole } = {},
33
+ ): TextEnvelope {
34
+ return { kind: 'text', text, ...options };
35
+ },
36
+
37
+ /** A user turn for a conversation. */
38
+ user(text: string): TextEnvelope {
39
+ return { kind: 'text', text, role: 'user' };
40
+ },
41
+
42
+ /** An assistant turn for a conversation (e.g. a model reply you keep). */
43
+ assistant(text: string): TextEnvelope {
44
+ return { kind: 'text', text, role: 'assistant' };
45
+ },
46
+
47
+ /** A system prompt. */
48
+ system(text: string): TextEnvelope {
49
+ return { kind: 'text', text, role: 'system' };
50
+ },
51
+
52
+ /** PCM/WAV audio, base64-encoded. Defaults to 16 kHz mono. */
53
+ audio(
54
+ bytesBase64: string,
55
+ options: { sampleRate?: number; channels?: number } = {},
56
+ ): AudioEnvelope {
57
+ return {
58
+ kind: 'audio',
59
+ bytesBase64,
60
+ sampleRate: options.sampleRate ?? 16000,
61
+ channels: options.channels ?? 1,
62
+ };
63
+ },
64
+
65
+ /** An embedding vector. */
66
+ embedding(data: number[]): EmbeddingEnvelope {
67
+ return { kind: 'embedding', data };
68
+ },
69
+
70
+ /**
71
+ * An encoded image for vision-language models. The bytes are validated
72
+ * natively at run time (`xybrid_invalid_image` on bad input).
73
+ */
74
+ image(bytesBase64: string, format: ImageFormat | 'jpg'): ImageEnvelope {
75
+ return { kind: 'image', bytesBase64, format: normalizeImageFormat(format) };
76
+ },
77
+
78
+ /** A user message with image attachments, for vision-language models. */
79
+ userMessage(text: string, images: ImageEnvelope[] = []): MultiPartEnvelope {
80
+ if (!images.every((image) => image?.kind === 'image')) {
81
+ throw new TypeError('Envelope.userMessage accepts only image envelopes');
82
+ }
83
+ return {
84
+ kind: 'multipart',
85
+ parts: [{ kind: 'text', text }, ...images],
86
+ role: 'user',
87
+ };
88
+ },
89
+
90
+ /**
91
+ * The continuation envelope for the turn after the model asked for tools.
92
+ *
93
+ * One `run` is one model turn, so the tool loop lives in your code: run a
94
+ * request carrying `generationConfig.tools`, execute every
95
+ * `result.toolCalls` entry, then run this envelope (with the same tools) to
96
+ * feed the outcomes back.
97
+ *
98
+ * @param userText the original user message of the turn being continued.
99
+ * @param priorAssistantText that turn's raw output — `result.text` from a
100
+ * batch run, or the terminal token's `rawText` when streaming.
101
+ * @param results tool outcomes, in call order.
102
+ */
103
+ async toolResults(
104
+ userText: string,
105
+ priorAssistantText: string,
106
+ results: ToolResult[],
107
+ ): Promise<Envelope> {
108
+ const wire = (await NativeXybrid.toolResultsEnvelope(
109
+ userText,
110
+ priorAssistantText,
111
+ results.map(toWireToolResult),
112
+ )) as WireEnvelope;
113
+ return fromWireEnvelope(wire);
114
+ },
115
+ };
package/src/errors.ts ADDED
@@ -0,0 +1,67 @@
1
+ // Every native rejection carries one of these codes as `error.code`. The
2
+ // SDK codes map 1:1 onto bolt's `XybridError` variants (same order as
3
+ // crates/xybrid-bolt/src/lib.rs); the last three are raised by the React
4
+ // Native layer itself.
5
+
6
+ /** Stable error codes for `XybridError.code`. */
7
+ export const XybridErrorCodes = [
8
+ 'xybrid_model_not_found',
9
+ 'xybrid_directory_not_found',
10
+ 'xybrid_metadata_not_found',
11
+ 'xybrid_metadata_invalid',
12
+ 'xybrid_load_error',
13
+ 'xybrid_inference_error',
14
+ 'xybrid_aborted_cloud_fallback',
15
+ 'xybrid_streaming_unsupported',
16
+ 'xybrid_not_loaded',
17
+ 'xybrid_config_error',
18
+ 'xybrid_network_error',
19
+ 'xybrid_offline',
20
+ 'xybrid_io_error',
21
+ 'xybrid_cache_error',
22
+ 'xybrid_pipeline_error',
23
+ 'xybrid_circuit_open',
24
+ 'xybrid_rate_limited',
25
+ 'xybrid_timeout',
26
+ 'xybrid_missing_artifact',
27
+ 'xybrid_unsupported_model_capability',
28
+ 'xybrid_unsupported_backend_capability',
29
+ 'xybrid_invalid_image',
30
+ 'xybrid_cancelled',
31
+ // React Native layer:
32
+ /** The handle was never issued, was released, or names another kind of object. */
33
+ 'xybrid_handle',
34
+ /** A JS argument could not be decoded (bad envelope, options, source, base64…). */
35
+ 'xybrid_invalid_argument',
36
+ /** Anything unexpected; the message carries the detail. */
37
+ 'xybrid_unknown',
38
+ ] as const;
39
+
40
+ export type XybridErrorCode = (typeof XybridErrorCodes)[number];
41
+
42
+ /** A rejection raised by the Xybrid native module. */
43
+ export interface XybridError extends Error {
44
+ code: XybridErrorCode;
45
+ }
46
+
47
+ const CODES: ReadonlySet<string> = new Set(XybridErrorCodes);
48
+
49
+ /** Narrow an unknown rejection to a {@link XybridError}. */
50
+ export function isXybridError(error: unknown): error is XybridError {
51
+ if (!(error instanceof Error)) return false;
52
+ const { code } = error as Error & { code?: unknown };
53
+ return typeof code === 'string' && CODES.has(code);
54
+ }
55
+
56
+ // Mirrors `RetryableError::is_retryable` on the Rust `SdkError`.
57
+ const RETRYABLE: ReadonlySet<XybridErrorCode> = new Set<XybridErrorCode>([
58
+ 'xybrid_network_error',
59
+ 'xybrid_rate_limited',
60
+ 'xybrid_timeout',
61
+ 'xybrid_offline',
62
+ ]);
63
+
64
+ /** Whether retrying the same call later can succeed (network, rate limit, timeout, offline). */
65
+ export function isRetryable(error: unknown): boolean {
66
+ return isXybridError(error) && RETRYABLE.has(error.code);
67
+ }
package/src/index.ts ADDED
@@ -0,0 +1,91 @@
1
+ // @xybrid/react-native — on-device (and optionally cloud) inference for React
2
+ // Native, backed by the same Rust SDK as the Swift, Kotlin, Flutter and Unity
3
+ // bindings.
4
+ //
5
+ // await Xybrid.initialize({ apiKey }); // optional
6
+ // const model = await ModelLoader.fromRegistry('qwen3.5-0.8b').load();
7
+ // const result = await model.run(Envelope.text('Hello'));
8
+ // await model.release();
9
+
10
+ import NativeXybrid from './NativeXybrid';
11
+
12
+ export { Xybrid, type XybridInitOptions } from './xybrid';
13
+ export {
14
+ Model,
15
+ ModelDownload,
16
+ ModelLoader,
17
+ type ModelSource,
18
+ type RunOptions,
19
+ } from './model';
20
+ export { ConversationContext } from './context';
21
+ export { Pipeline } from './pipeline';
22
+ export { StreamingSession } from './session';
23
+ export { Envelope } from './envelope';
24
+ export { GenerationConfigs, creative, greedy } from './presets';
25
+ export {
26
+ XybridErrorCodes,
27
+ isRetryable,
28
+ isXybridError,
29
+ type XybridError,
30
+ type XybridErrorCode,
31
+ } from './errors';
32
+ export type { AbortSignalLike } from './cancel';
33
+ export { bytesToBase64, float32ToBase64 } from './base64';
34
+
35
+ export type {
36
+ AbortSignalKind,
37
+ AudioEnvelope,
38
+ CacheEntry,
39
+ CacheEntryLocation,
40
+ CacheStatus,
41
+ ConversationInfo,
42
+ DownloadState,
43
+ DownloadStatus,
44
+ EmbeddingEnvelope,
45
+ EnvelopeKind,
46
+ EnvelopeMetadata,
47
+ ExecutionTarget,
48
+ GenerationConfig,
49
+ ImageEnvelope,
50
+ ImageFormat,
51
+ InferenceMetrics,
52
+ InferenceResult,
53
+ MessageRole,
54
+ ModelInfo,
55
+ MultiPartEnvelope,
56
+ OutputType,
57
+ PartialResult,
58
+ PipelineInfo,
59
+ PipelineResult,
60
+ StageLatency,
61
+ StageResult,
62
+ StreamToken,
63
+ StreamingConfig,
64
+ TextEnvelope,
65
+ ThermalState,
66
+ ToolCall,
67
+ ToolDefinition,
68
+ ToolResult,
69
+ VoiceInfo,
70
+ } from './types';
71
+
72
+ /**
73
+ * Convert a JSON Schema (object or JSON string) into a GBNF grammar for
74
+ * `generationConfig.grammar`, so a local LLM emits schema-valid JSON. Runs the
75
+ * same native converter every binding uses; rejects with
76
+ * `xybrid_config_error` on invalid JSON or an unsupported construct.
77
+ *
78
+ * ```ts
79
+ * const grammar = await jsonSchemaToGbnf({
80
+ * type: 'object',
81
+ * properties: { name: { type: 'string' } },
82
+ * required: ['name'],
83
+ * });
84
+ * const result = await model.run(prompt, { generationConfig: { grammar } });
85
+ * ```
86
+ */
87
+ export function jsonSchemaToGbnf(schema: object | string): Promise<string> {
88
+ return NativeXybrid.jsonSchemaToGbnf(
89
+ typeof schema === 'string' ? schema : JSON.stringify(schema),
90
+ );
91
+ }