@kaana.ai/sdk 0.0.0-stage → 1.0.1

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/NOTICE ADDED
@@ -0,0 +1,21 @@
1
+ ============================================================================
2
+ NOTICE
3
+ @kaana.ai/sdk
4
+ Copyright (c) 2026-present The Oxy Collective, Inc.
5
+ ============================================================================
6
+
7
+ This product is licensed under the Breathe License 1.0, license identifier
8
+ LicenseRef-Breathe-1.0. See the LICENSE file in this directory. Commercial
9
+ terms are available at
10
+ https://github.com/OxyHQ/.github/blob/main/LICENSE-COMMERCIAL.md
11
+
12
+ Attribution under Section 3.1 of the Breathe License is required of
13
+ everyone, including paying commercial licensees, and cannot be waived.
14
+
15
+ ----------------------------------------------------------------------------
16
+ THIRD PARTY COMPONENTS
17
+ ----------------------------------------------------------------------------
18
+
19
+ This package contains no third party source. Its dependencies are resolved
20
+ and installed by the package manager and remain governed by their own
21
+ licenses.
package/README.md CHANGED
@@ -1,3 +1,165 @@
1
- # Temporary Holding Version
1
+ # @kaana.ai/sdk
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ The TypeScript client of Kaana's public API, `https://kaana.ai/v1`. It runs in
4
+ Node 20+, browsers and React Native, validates every response against
5
+ [`@kaana.ai/contracts`](https://www.npmjs.com/package/@kaana.ai/contracts) and
6
+ reports failures as typed errors.
7
+
8
+ Applications, API keys (`oxy_sk_…`), limits, budgets, usage and billing are
9
+ managed in [Oxy Console](https://console.oxy.so). Kaana has no console and
10
+ issues no keys of its own.
11
+
12
+ ```sh
13
+ npm install @kaana.ai/sdk
14
+ ```
15
+
16
+ ## Credentials
17
+
18
+ The credential is the value of `Authorization: Bearer`: a string, or a
19
+ function called on every request. The SDK never caches it; whoever supplies
20
+ the function already caches and renews.
21
+
22
+ ```ts
23
+ import { Kaana } from '@kaana.ai/sdk';
24
+
25
+ new Kaana({ credential: process.env.KAANA_API_KEY! }); // oxy_sk_ from Console
26
+ new Kaana({ credential: () => oxyServer.serviceToken({ audience: 'kaana.ai' }) }); // an Oxy service
27
+ new Kaana({ credential: async () => (await oxy.getAccessToken())! }); // an Oxy app session
28
+ ```
29
+
30
+ Options: `baseURL` (only `https://kaana.ai/v1`, or a loopback origin in
31
+ development), `fetch`, `WebSocket` (for realtime where there is no global
32
+ one, e.g. `ws` in older Node), `timeoutMs` (default 120 000) and `maxRetries`
33
+ (default 2: only before the first byte, only for retryable codes, honouring
34
+ `retryAfterMs`).
35
+
36
+ Every method takes `RequestOptions`: `signal`, `idempotencyKey`
37
+ (`Idempotency-Key`, reused by every retry), `onBehalfOfUserId`
38
+ (`X-Oxy-User-Id`, honoured only with a service token) and `timeoutMs`.
39
+
40
+ ## Methods
41
+
42
+ ```ts
43
+ const kaana = new Kaana({ credential: process.env.KAANA_API_KEY! });
44
+
45
+ await kaana.models.list();
46
+ await kaana.models.get('elevenlabs/eleven-multilingual@v2');
47
+ await kaana.powerLevels.list();
48
+
49
+ await kaana.responses.create({ model: 'auto', input: 'Hola' });
50
+ const stream = kaana.responses.stream({ model: 'high', input: 'Cuéntame un cuento' });
51
+ for await (const event of stream) if (event.type === 'delta') process.stdout.write(event.text);
52
+ const response = await stream.final; // the folded Response
53
+
54
+ await kaana.chat.completions.create({ model: 'instant', messages: [{ role: 'user', content: 'Hola' }] });
55
+ kaana.chat.completions.stream({ model: 'instant', messages: [{ role: 'user', content: 'Hola' }] });
56
+
57
+ await kaana.embeddings.create({ model: 'openai/text-embedding-3-small', input: ['hola'] });
58
+ await kaana.images.generate({ model: 'openai/gpt-image-1', prompt: 'A fox' });
59
+ await kaana.decisions.create({ model: 'openai/gpt-5@2026-06-01', state: '…', questions: [/* … */] });
60
+ await kaana.generations.get('gen_…');
61
+ await kaana.generations.getByIdempotencyKey('my-key');
62
+ ```
63
+
64
+ A stream is iterated once. Breaking out of the loop, or `stream.abort()`,
65
+ cancels the request upstream. Awaiting `final` alone drains the stream.
66
+
67
+ The API is also compatible with the OpenAI SDK for chat completions:
68
+ `new OpenAI({ baseURL: 'https://kaana.ai/v1', apiKey: 'oxy_sk_…' })`.
69
+
70
+ ## Audio
71
+
72
+ ```ts
73
+ const speech = await kaana.audio.speech({
74
+ model: 'elevenlabs/eleven-multilingual',
75
+ input: 'Hola',
76
+ voice: 'rachel',
77
+ });
78
+ speech.audio; // Uint8Array, speech.mediaType, speech.model (the served revision), speech.usage
79
+
80
+ const live = kaana.audio.speechStream({ model: 'elevenlabs/eleven-multilingual', input: 'Hola', voice: 'rachel' });
81
+ for await (const chunk of live) play(chunk); // mp3 or pcm, in order, as produced
82
+ const { model, usage } = await live.done;
83
+ live.toReadableStream(); // the same audio as a ReadableStream
84
+
85
+ await kaana.audio.transcriptions.create({
86
+ model: 'openai/whisper-1',
87
+ file: bytesOrBlob, // a Blob is sent as multipart; a Uint8Array as JSON with base64
88
+ mediaType: 'audio/wav',
89
+ language: 'es-ES',
90
+ });
91
+ ```
92
+
93
+ Voices, languages, formats, the `speed` range and the character limit of each
94
+ speech model are in `models.get(...).audio`; the server checks them before
95
+ anything is reserved.
96
+
97
+ ## Realtime
98
+
99
+ ```ts
100
+ const session = await kaana.realtime.connect({
101
+ model: 'openai/gpt-realtime',
102
+ type: 'session.open',
103
+ kind: 'conversation',
104
+ config: {
105
+ outputModalities: ['audio'],
106
+ voice: 'marin',
107
+ inputAudioFormat: 'pcm16_24khz',
108
+ outputAudioFormat: 'pcm16_24khz',
109
+ turnDetection: { type: 'server_vad', createResponse: true, interruptResponse: true },
110
+ tools: [{ type: 'function', name: 'lookup', parameters: { type: 'object' } }],
111
+ },
112
+ });
113
+
114
+ session.appendAudio(pcm16); // split into frames of at most 49 152 bytes
115
+ session.send({ type: 'response.create' }); // schemaVersion, requestId and commandId are filled in
116
+ for await (const event of session) {
117
+ if (event.type === 'tool_call' && event.complete) {
118
+ session.send({
119
+ type: 'conversation.item.create',
120
+ item: { type: 'function_call_output', callId: event.toolCallId, output: '{}' },
121
+ });
122
+ }
123
+ }
124
+ const usage = await session.close(); // settles exactly once
125
+ ```
126
+
127
+ The credential travels as a WebSocket subprotocol (`kaana.bearer.…`), so
128
+ realtime works where a WebSocket cannot set headers (browsers, React Native).
129
+ If the connection drops, iteration throws a `KaanaConnectionError`; call
130
+ `session.resume()` within the session's resume window and iterate again:
131
+ what was missed is replayed once, in order.
132
+
133
+ ## Errors
134
+
135
+ | Class | Codes |
136
+ |---|---|
137
+ | `KaanaAuthenticationError` | `authentication_failed` (keys are managed in [Oxy Console](https://console.oxy.so)) |
138
+ | `KaanaPermissionError` | `permission_denied`, `insufficient_scope`, `commercial_permission_denied`, `policy_violation` |
139
+ | `KaanaInvalidRequestError` | `invalid_request`, `model_not_found`, `unsupported_modality`, `context_length_exceeded`, `request_too_large`, `output_limit_exceeded`, `idempotency_conflict`, `byok_credential_invalid` |
140
+ | `KaanaBillingError` | `insufficient_balance`, `spending_limit_exceeded`, `quota_exceeded` |
141
+ | `KaanaRateLimitError` | `rate_limited` |
142
+ | `KaanaUnavailableError` | `no_route_available`, `service_unavailable`, `provider_error`, `provider_timeout`, `provider_overloaded` |
143
+ | `KaanaContentFilteredError` | `upstream_content_filtered` |
144
+ | `KaanaCancelledError` | `cancelled`, including a request you aborted |
145
+ | `KaanaInternalError` | `internal_error` |
146
+
147
+ Each `KaanaError` has `code`, `status` (`null` when it arrived as a terminal
148
+ stream event), `requestId`, `retryable`, `retryAfterMs`, `param` and
149
+ `afterOutput` (output reached you before the failure). `KaanaProtocolError` is a
150
+ response outside the contract and is never retried; `KaanaConnectionError` is
151
+ the network or a timeout (`timedOut`) before a response.
152
+
153
+ ## React Native
154
+
155
+ Non-streaming methods work with React Native's built-in `fetch`. The streaming
156
+ methods (`responses.stream`, `chat.completions.stream`, `audio.speechStream`)
157
+ need a `fetch` whose response body is a `ReadableStream`; install a streaming
158
+ fetch polyfill and pass it as `fetch`. Without one they fail with a clear
159
+ `KaanaConnectionError` instead of buffering silently. Transcription of bytes
160
+ uses JSON with base64, so no `Blob` support is required. React Native's
161
+ `WebSocket` carries realtime sessions as is.
162
+
163
+ ## License
164
+
165
+ Breathe License 1.0 — see `LICENSE` and `NOTICE`.
@@ -0,0 +1,39 @@
1
+ /** `POST /v1/audio/speech` (whole and streamed) and `POST /v1/audio/transcriptions`. */
2
+ import { type AudioInputMediaType, type AudioMediaType, type ModelReference, type SpeechRequest, type Transcription, type Usage } from '@kaana.ai/contracts';
3
+ import { type RequestOptions, type Transport } from './http.js';
4
+ export interface SpeechResult {
5
+ audio: Uint8Array;
6
+ /** 'audio/mpeg' | 'audio/wav' | 'audio/ogg' | 'audio/aac' | 'audio/flac' | 'audio/pcm' */
7
+ mediaType: AudioMediaType;
8
+ /** The revision that actually served the request. */
9
+ model: ModelReference;
10
+ requestId: string;
11
+ usage: Usage;
12
+ }
13
+ /**
14
+ * Audio in order, as soon as it is produced (`stream_format: 'sse'`, so a
15
+ * failure mid-stream arrives typed).
16
+ */
17
+ export interface SpeechStream extends AsyncIterable<Uint8Array> {
18
+ readonly mediaType: Promise<AudioMediaType>;
19
+ readonly requestId: Promise<string>;
20
+ toReadableStream(): ReadableStream<Uint8Array>;
21
+ /** Model, usage and requestId at the end; rejects with KaanaError. */
22
+ readonly done: Promise<Omit<SpeechResult, 'audio'>>;
23
+ abort(reason?: unknown): void;
24
+ }
25
+ export interface TranscriptionRequest {
26
+ model: ModelReference;
27
+ /** A Blob is uploaded as multipart; bytes are sent as JSON with inline base64. */
28
+ file: Blob | Uint8Array;
29
+ mediaType: AudioInputMediaType;
30
+ /** BCP 47, validated against the catalogue. */
31
+ language?: string;
32
+ prompt?: string;
33
+ timestamps?: 'none' | 'segment' | 'word';
34
+ }
35
+ export declare function speech(transport: Transport, request: SpeechRequest, options: RequestOptions): Promise<SpeechResult>;
36
+ export declare function speechStream(transport: Transport, request: SpeechRequest, options: RequestOptions): SpeechStream;
37
+ /** `unit=quantity,unit=quantity`, each unit once. */
38
+ export declare function parseUsageHeader(header: string | null, requestId: string): Usage;
39
+ export declare function transcribe(transport: Transport, request: TranscriptionRequest, options: RequestOptions): Promise<Transcription>;
package/dist/audio.js ADDED
@@ -0,0 +1,250 @@
1
+ /** `POST /v1/audio/speech` (whole and streamed) and `POST /v1/audio/transcriptions`. */
2
+ import { audioInputMediaTypeSchema, KAANA_MODEL_HEADER, KAANA_USAGE_HEADER, MAX_INFERENCE_AUDIO_BYTES, MAX_TRANSCRIPTION_FILE_BYTES, modelReferenceSchema, STREAMABLE_SPEECH_FORMATS, speechMediaType, speechRequestSchema, speechStreamEventSchema, transcriptionJsonRequestSchema, transcriptionRequestSchema, transcriptionSchema, usageSchema, } from '@kaana.ai/contracts';
3
+ import { concatBytes, decodeBase64, encodeBase64 } from './base64.js';
4
+ import { clientRefusal, fromPublicError, KaanaProtocolError } from './errors.js';
5
+ import { cutOff, frameJson, readFrames, streamingBody } from './events.js';
6
+ import { bodyReadFailure, mediaTypeOf, validate, } from './http.js';
7
+ import { deferred, StreamHandle } from './stream.js';
8
+ /* -------------------------------------------------------------------------- */
9
+ /* Speech */
10
+ /* -------------------------------------------------------------------------- */
11
+ export async function speech(transport, request, options) {
12
+ if (request.stream === true || request.stream_format !== undefined) {
13
+ throw clientRefusal('stream', 'audio.speech returns whole audio; use audio.speechStream');
14
+ }
15
+ const body = checked(request);
16
+ const sent = await transport.send({
17
+ method: 'POST',
18
+ path: '/audio/speech',
19
+ body,
20
+ accept: 'audio/*',
21
+ options,
22
+ });
23
+ const requestId = sent.requestId;
24
+ const mediaType = speechMediaType(request.response_format ?? 'mp3');
25
+ try {
26
+ if (mediaTypeOf(sent.response) !== mediaType) {
27
+ throw new KaanaProtocolError(`Speech in ${request.response_format ?? 'mp3'} was answered with ${mediaTypeOf(sent.response) ?? 'no Content-Type'}.`, requestId);
28
+ }
29
+ const model = validate(sent.response.headers.get(KAANA_MODEL_HEADER), modelReferenceSchema, requestId, `${KAANA_MODEL_HEADER} header`);
30
+ const usage = parseUsageHeader(sent.response.headers.get(KAANA_USAGE_HEADER), requestId);
31
+ const audio = await readAudio(sent);
32
+ if (audio.byteLength === 0) {
33
+ throw new KaanaProtocolError('The speech response carries no audio.', requestId);
34
+ }
35
+ return { audio, mediaType, model, requestId, usage };
36
+ }
37
+ catch (error) {
38
+ sent.abort();
39
+ throw error;
40
+ }
41
+ finally {
42
+ sent.release();
43
+ }
44
+ }
45
+ export function speechStream(transport, request, options) {
46
+ const format = request.response_format ?? 'mp3';
47
+ if (!STREAMABLE_SPEECH_FORMATS.includes(format)) {
48
+ throw clientRefusal('response_format', `streamed speech is ${STREAMABLE_SPEECH_FORMATS.join(' or ')}; ${format} exists only whole`);
49
+ }
50
+ const body = checked({ ...request, stream: true, stream_format: 'sse' });
51
+ const mediaType = speechMediaType(format);
52
+ const handle = new StreamHandle(async function* (context) {
53
+ const sent = await transport.send({
54
+ method: 'POST',
55
+ path: '/audio/speech',
56
+ body,
57
+ accept: 'text/event-stream',
58
+ options: { ...options, signal: context.signal },
59
+ stream: true,
60
+ });
61
+ const requestId = sent.requestId;
62
+ context.setRequestId(requestId);
63
+ let output = false;
64
+ for await (const frame of readFrames(sent, () => output)) {
65
+ const event = validate(frameJson(frame, requestId), speechStreamEventSchema, requestId, 'speech event');
66
+ if (frame.event !== event.type) {
67
+ throw new KaanaProtocolError(`The stream framed a ${event.type} event as ${frame.event || 'unnamed'}.`, requestId);
68
+ }
69
+ switch (event.type) {
70
+ case 'speech.audio.delta':
71
+ output = true;
72
+ context.markOutput();
73
+ yield decodeBase64(event.audio);
74
+ break;
75
+ case 'speech.audio.done':
76
+ if (event.requestId !== requestId) {
77
+ throw new KaanaProtocolError('The stream ended for another request.', requestId);
78
+ }
79
+ return { mediaType, model: event.model, requestId, usage: event.usage };
80
+ case 'error':
81
+ throw fromPublicError(event.error, null, output);
82
+ }
83
+ }
84
+ throw cutOff(requestId);
85
+ }, options.signal);
86
+ const mediaTypePromise = deferred();
87
+ mediaTypePromise.resolve(mediaType);
88
+ return {
89
+ mediaType: mediaTypePromise.promise,
90
+ get requestId() {
91
+ return handle.requestId;
92
+ },
93
+ get done() {
94
+ return handle.final;
95
+ },
96
+ abort: (reason) => handle.abort(reason),
97
+ [Symbol.asyncIterator]: () => handle[Symbol.asyncIterator](),
98
+ toReadableStream() {
99
+ const iterator = handle[Symbol.asyncIterator]();
100
+ return new ReadableStream({
101
+ async pull(controller) {
102
+ try {
103
+ const next = await iterator.next();
104
+ if (next.done)
105
+ controller.close();
106
+ else
107
+ controller.enqueue(next.value);
108
+ }
109
+ catch (error) {
110
+ controller.error(error);
111
+ }
112
+ },
113
+ async cancel() {
114
+ await iterator.return?.();
115
+ },
116
+ });
117
+ },
118
+ };
119
+ }
120
+ function checked(request) {
121
+ const parsed = speechRequestSchema.safeParse(request);
122
+ if (!parsed.success) {
123
+ const issue = parsed.error.issues[0];
124
+ throw clientRefusal(String(issue?.path[0] ?? 'request'), issue?.message ?? 'invalid request');
125
+ }
126
+ return JSON.stringify(request);
127
+ }
128
+ /** `unit=quantity,unit=quantity`, each unit once. */
129
+ export function parseUsageHeader(header, requestId) {
130
+ if (header === null) {
131
+ throw new KaanaProtocolError(`The response carries no ${KAANA_USAGE_HEADER}.`, requestId);
132
+ }
133
+ const units = header.trim().length === 0
134
+ ? []
135
+ : header.split(',').map((pair) => {
136
+ const [unit, quantity, extra] = pair.trim().split('=');
137
+ if (extra !== undefined || quantity === undefined || !/^\d+$/.test(quantity)) {
138
+ throw new KaanaProtocolError(`${KAANA_USAGE_HEADER} is malformed.`, requestId);
139
+ }
140
+ return { unit, quantity: Number(quantity) };
141
+ });
142
+ return validate(units, usageSchema, requestId, `${KAANA_USAGE_HEADER} header`);
143
+ }
144
+ async function readAudio(sent) {
145
+ const body = sent.response.body;
146
+ if (body === null || typeof body.getReader !== 'function') {
147
+ // Runtimes without a streaming body still give the whole buffer.
148
+ let buffer;
149
+ try {
150
+ buffer = await sent.response.arrayBuffer();
151
+ }
152
+ catch (cause) {
153
+ throw bodyReadFailure(sent, cause);
154
+ }
155
+ if (buffer.byteLength > MAX_INFERENCE_AUDIO_BYTES) {
156
+ throw new KaanaProtocolError('The speech audio exceeds its size limit.', sent.requestId);
157
+ }
158
+ return new Uint8Array(buffer);
159
+ }
160
+ const reader = streamingBody(sent).getReader();
161
+ const chunks = [];
162
+ let size = 0;
163
+ let completed = false;
164
+ try {
165
+ while (true) {
166
+ let next;
167
+ try {
168
+ next = await reader.read();
169
+ }
170
+ catch (cause) {
171
+ throw bodyReadFailure(sent, cause);
172
+ }
173
+ if (next.done) {
174
+ completed = true;
175
+ break;
176
+ }
177
+ size += next.value.byteLength;
178
+ if (size > MAX_INFERENCE_AUDIO_BYTES) {
179
+ throw new KaanaProtocolError('The speech audio exceeds its size limit.', sent.requestId);
180
+ }
181
+ chunks.push(next.value);
182
+ }
183
+ }
184
+ finally {
185
+ if (!completed)
186
+ await reader.cancel().catch(() => undefined);
187
+ reader.releaseLock();
188
+ }
189
+ return concatBytes(chunks);
190
+ }
191
+ /* -------------------------------------------------------------------------- */
192
+ /* Transcription */
193
+ /* -------------------------------------------------------------------------- */
194
+ const EXTENSIONS = {
195
+ 'audio/mpeg': 'mp3',
196
+ 'audio/mp4': 'm4a',
197
+ 'audio/wav': 'wav',
198
+ 'audio/ogg': 'ogg',
199
+ 'audio/webm': 'webm',
200
+ 'audio/flac': 'flac',
201
+ 'audio/aac': 'aac',
202
+ 'audio/pcm': 'pcm',
203
+ };
204
+ export async function transcribe(transport, request, options) {
205
+ const fields = {
206
+ model: request.model,
207
+ ...(request.language === undefined ? {} : { language: request.language }),
208
+ ...(request.prompt === undefined ? {} : { prompt: request.prompt }),
209
+ ...(request.timestamps === undefined ? {} : { timestamps: request.timestamps }),
210
+ };
211
+ const parsedFields = transcriptionRequestSchema.safeParse(fields);
212
+ if (!parsedFields.success) {
213
+ const issue = parsedFields.error.issues[0];
214
+ throw clientRefusal(String(issue?.path[0] ?? 'request'), issue?.message ?? 'invalid request');
215
+ }
216
+ if (!audioInputMediaTypeSchema.safeParse(request.mediaType).success) {
217
+ throw clientRefusal('mediaType', `${String(request.mediaType)} is not an accepted audio type`);
218
+ }
219
+ const isBlob = typeof Blob !== 'undefined' && request.file instanceof Blob;
220
+ const size = isBlob ? request.file.size : request.file.byteLength;
221
+ if (size === 0)
222
+ throw clientRefusal('file', 'the audio file is empty');
223
+ if (size > MAX_TRANSCRIPTION_FILE_BYTES) {
224
+ throw clientRefusal('file', `the audio file exceeds ${MAX_TRANSCRIPTION_FILE_BYTES} bytes`);
225
+ }
226
+ let body;
227
+ if (isBlob) {
228
+ const blob = request.file;
229
+ const part = blob.type === request.mediaType ? blob : new Blob([blob], { type: request.mediaType });
230
+ const form = new FormData();
231
+ form.append('file', part, `audio.${EXTENSIONS[request.mediaType]}`);
232
+ for (const [name, value] of Object.entries(fields))
233
+ form.append(name, value);
234
+ body = form;
235
+ }
236
+ else {
237
+ const json = {
238
+ ...fields,
239
+ audio: { mediaType: request.mediaType, data: encodeBase64(request.file) },
240
+ };
241
+ const parsedJson = transcriptionJsonRequestSchema.safeParse(json);
242
+ if (!parsedJson.success) {
243
+ const issue = parsedJson.error.issues[0];
244
+ throw clientRefusal(issue?.path.map(String).join('.') || 'file', issue?.message ?? 'invalid');
245
+ }
246
+ body = JSON.stringify(json);
247
+ }
248
+ const { value } = await transport.json({ method: 'POST', path: '/audio/transcriptions', body, accept: 'application/json', options }, transcriptionSchema);
249
+ return value;
250
+ }
@@ -0,0 +1,18 @@
1
+ /**
2
+ * Base64 without a platform module: no `Buffer`, no `btoa`/`atob` (which are
3
+ * Latin-1 string APIs and absent from some React Native runtimes). The SDK
4
+ * encodes audio frames and credentials with these and nothing else.
5
+ */
6
+ /** Standard padded base64 (RFC 4648 §4). */
7
+ export declare function encodeBase64(bytes: Uint8Array): string;
8
+ /** Unpadded base64url (RFC 4648 §5), the form a WebSocket subprotocol token can carry. */
9
+ export declare function encodeBase64Url(bytes: Uint8Array): string;
10
+ /**
11
+ * Decode standard or url-safe base64, padded or not. Throws on any character
12
+ * outside the alphabet or an impossible length, never silently skipping.
13
+ */
14
+ export declare function decodeBase64(text: string): Uint8Array;
15
+ /** UTF-8 bytes of a string. */
16
+ export declare function utf8(text: string): Uint8Array;
17
+ /** Concatenate byte chunks. */
18
+ export declare function concatBytes(chunks: readonly Uint8Array[]): Uint8Array;
package/dist/base64.js ADDED
@@ -0,0 +1,87 @@
1
+ /**
2
+ * Base64 without a platform module: no `Buffer`, no `btoa`/`atob` (which are
3
+ * Latin-1 string APIs and absent from some React Native runtimes). The SDK
4
+ * encodes audio frames and credentials with these and nothing else.
5
+ */
6
+ const ALPHABET = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/';
7
+ const DECODE = (() => {
8
+ const table = new Int16Array(128).fill(-1);
9
+ for (let index = 0; index < ALPHABET.length; index += 1) {
10
+ table[ALPHABET.charCodeAt(index)] = index;
11
+ }
12
+ // base64url spellings decode to the same values.
13
+ table['-'.charCodeAt(0)] = 62;
14
+ table['_'.charCodeAt(0)] = 63;
15
+ return table;
16
+ })();
17
+ /** Standard padded base64 (RFC 4648 §4). */
18
+ export function encodeBase64(bytes) {
19
+ let out = '';
20
+ let index = 0;
21
+ for (; index + 2 < bytes.length; index += 3) {
22
+ const chunk = (bytes[index] << 16) | (bytes[index + 1] << 8) | bytes[index + 2];
23
+ out +=
24
+ ALPHABET[(chunk >> 18) & 63] +
25
+ ALPHABET[(chunk >> 12) & 63] +
26
+ ALPHABET[(chunk >> 6) & 63] +
27
+ ALPHABET[chunk & 63];
28
+ }
29
+ const rest = bytes.length - index;
30
+ if (rest === 1) {
31
+ const chunk = bytes[index] << 16;
32
+ out += `${ALPHABET[(chunk >> 18) & 63]}${ALPHABET[(chunk >> 12) & 63]}==`;
33
+ }
34
+ else if (rest === 2) {
35
+ const chunk = (bytes[index] << 16) | (bytes[index + 1] << 8);
36
+ out += `${ALPHABET[(chunk >> 18) & 63]}${ALPHABET[(chunk >> 12) & 63]}${ALPHABET[(chunk >> 6) & 63]}=`;
37
+ }
38
+ return out;
39
+ }
40
+ /** Unpadded base64url (RFC 4648 §5), the form a WebSocket subprotocol token can carry. */
41
+ export function encodeBase64Url(bytes) {
42
+ return encodeBase64(bytes).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
43
+ }
44
+ /**
45
+ * Decode standard or url-safe base64, padded or not. Throws on any character
46
+ * outside the alphabet or an impossible length, never silently skipping.
47
+ */
48
+ export function decodeBase64(text) {
49
+ const trimmed = text.replace(/=+$/, '');
50
+ if (trimmed.length % 4 === 1)
51
+ throw new Error('base64 has an impossible length');
52
+ const out = new Uint8Array(Math.floor((trimmed.length * 3) / 4));
53
+ let buffer = 0;
54
+ let bits = 0;
55
+ let position = 0;
56
+ for (let index = 0; index < trimmed.length; index += 1) {
57
+ const code = trimmed.charCodeAt(index);
58
+ const value = code < 128 ? DECODE[code] : -1;
59
+ if (value < 0)
60
+ throw new Error('base64 contains a character outside its alphabet');
61
+ buffer = (buffer << 6) | value;
62
+ bits += 6;
63
+ if (bits >= 8) {
64
+ bits -= 8;
65
+ out[position] = (buffer >> bits) & 0xff;
66
+ position += 1;
67
+ }
68
+ }
69
+ return out;
70
+ }
71
+ /** UTF-8 bytes of a string. */
72
+ export function utf8(text) {
73
+ return new TextEncoder().encode(text);
74
+ }
75
+ /** Concatenate byte chunks. */
76
+ export function concatBytes(chunks) {
77
+ let size = 0;
78
+ for (const chunk of chunks)
79
+ size += chunk.byteLength;
80
+ const out = new Uint8Array(size);
81
+ let offset = 0;
82
+ for (const chunk of chunks) {
83
+ out.set(chunk, offset);
84
+ offset += chunk.byteLength;
85
+ }
86
+ return out;
87
+ }
package/dist/chat.d.ts ADDED
@@ -0,0 +1,11 @@
1
+ /** `POST /v1/chat/completions`, the OpenAI dialect: whole and streamed. */
2
+ import { type ChatCompletion, type ChatCompletionChunk } from '@kaana.ai/contracts';
3
+ import { type RequestOptions, type Transport } from './http.js';
4
+ import { type KaanaStream } from './stream.js';
5
+ export declare function chatStream(transport: Transport, body: string, options: RequestOptions): KaanaStream<ChatCompletionChunk, ChatCompletion>;
6
+ /** Folds the chunks of one completion into the `ChatCompletion` it describes. */
7
+ export declare class ChatFold {
8
+ #private;
9
+ add(chunk: ChatCompletionChunk): void;
10
+ completion(requestId: string): ChatCompletion;
11
+ }