@kaana.ai/sdk 0.0.0-stage → 1.0.0
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 +673 -0
- package/NOTICE +21 -0
- package/README.md +164 -2
- package/dist/audio.d.ts +39 -0
- package/dist/audio.js +250 -0
- package/dist/base64.d.ts +18 -0
- package/dist/base64.js +87 -0
- package/dist/chat.d.ts +11 -0
- package/dist/chat.js +149 -0
- package/dist/client.d.ts +52 -0
- package/dist/client.js +109 -0
- package/dist/errors.d.ts +82 -0
- package/dist/errors.js +150 -0
- package/dist/events.d.ts +11 -0
- package/dist/events.js +51 -0
- package/dist/http.d.ts +109 -0
- package/dist/http.js +338 -0
- package/dist/index.d.ts +14 -0
- package/dist/index.js +11 -0
- package/dist/realtime.d.ts +28 -0
- package/dist/realtime.js +322 -0
- package/dist/responses.d.ts +11 -0
- package/dist/responses.js +152 -0
- package/dist/sse.d.ts +14 -0
- package/dist/sse.js +94 -0
- package/dist/stream.d.ts +41 -0
- package/dist/stream.js +110 -0
- package/package.json +39 -4
package/dist/http.d.ts
ADDED
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The one place a request is sent: credentials, headers, the deadline, the
|
|
3
|
+
* caller's abort signal, typed error bodies and retries before the first byte.
|
|
4
|
+
*/
|
|
5
|
+
/**
|
|
6
|
+
* The value of `Authorization: Bearer`. A function is called on every request
|
|
7
|
+
* (every attempt); the SDK never caches it: whoever supplies the function
|
|
8
|
+
* already caches and renews.
|
|
9
|
+
*/
|
|
10
|
+
export type KaanaCredential = string | (() => string | Promise<string>);
|
|
11
|
+
/**
|
|
12
|
+
* The fetch the SDK calls. `globalThis.fetch` (or any WHATWG-compatible fetch)
|
|
13
|
+
* satisfies it; it is written structurally so a runtime's extra members are
|
|
14
|
+
* not required of a test double.
|
|
15
|
+
*/
|
|
16
|
+
export type KaanaFetch = (input: string, init: RequestInit) => Promise<Response>;
|
|
17
|
+
export interface KaanaOptions {
|
|
18
|
+
credential: KaanaCredential;
|
|
19
|
+
/** Only `https://kaana.ai/v1`, or a loopback origin in development. */
|
|
20
|
+
baseURL?: string;
|
|
21
|
+
fetch?: KaanaFetch;
|
|
22
|
+
/** For realtime where there is no global WebSocket (e.g. `ws` in Node < 22). */
|
|
23
|
+
WebSocket?: typeof globalThis.WebSocket;
|
|
24
|
+
/** Default 120 000. */
|
|
25
|
+
timeoutMs?: number;
|
|
26
|
+
/** Default 2: only before the first byte and only on retryable codes. */
|
|
27
|
+
maxRetries?: number;
|
|
28
|
+
}
|
|
29
|
+
export interface RequestOptions {
|
|
30
|
+
signal?: AbortSignal;
|
|
31
|
+
/** `Idempotency-Key`; reused unchanged by every retry. */
|
|
32
|
+
idempotencyKey?: string;
|
|
33
|
+
/** Delegated attribution (`X-Oxy-User-Id`); honoured only with a service token. */
|
|
34
|
+
onBehalfOfUserId?: string;
|
|
35
|
+
timeoutMs?: number;
|
|
36
|
+
}
|
|
37
|
+
export type ErrorDialect = 'public' | 'chat';
|
|
38
|
+
export interface SendInit {
|
|
39
|
+
method: 'GET' | 'POST';
|
|
40
|
+
path: string;
|
|
41
|
+
query?: Record<string, string>;
|
|
42
|
+
/** A JSON string or a multipart form. */
|
|
43
|
+
body?: string | FormData;
|
|
44
|
+
accept: string;
|
|
45
|
+
dialect?: ErrorDialect;
|
|
46
|
+
options?: RequestOptions;
|
|
47
|
+
/**
|
|
48
|
+
* A streamed response: the deadline covers only the wait for the response
|
|
49
|
+
* headers, never the stream itself.
|
|
50
|
+
*/
|
|
51
|
+
stream?: boolean;
|
|
52
|
+
}
|
|
53
|
+
/** An accepted (2xx) response and the handles that govern its body. */
|
|
54
|
+
export interface Sent {
|
|
55
|
+
readonly response: Response;
|
|
56
|
+
readonly requestId: string;
|
|
57
|
+
/** Aborts when the deadline passes or the caller aborts. */
|
|
58
|
+
readonly signal: AbortSignal;
|
|
59
|
+
/** True once the deadline has passed. */
|
|
60
|
+
timedOut(): boolean;
|
|
61
|
+
/** True once the caller's signal (or `abort()`) fired. */
|
|
62
|
+
aborted(): boolean;
|
|
63
|
+
/** Abort the body read. */
|
|
64
|
+
abort(reason?: unknown): void;
|
|
65
|
+
/** Clear the deadline and detach from the caller's signal. */
|
|
66
|
+
release(): void;
|
|
67
|
+
}
|
|
68
|
+
export declare class Transport {
|
|
69
|
+
#private;
|
|
70
|
+
readonly baseURL: string;
|
|
71
|
+
readonly WebSocket: typeof globalThis.WebSocket | undefined;
|
|
72
|
+
readonly timeoutMs: number;
|
|
73
|
+
constructor(options: KaanaOptions);
|
|
74
|
+
/** The credential, read now. Never stored. */
|
|
75
|
+
credential(): Promise<string>;
|
|
76
|
+
url(path: string, query?: Record<string, string>): string;
|
|
77
|
+
/** Send with retries before the first byte. Resolves only with a 2xx. */
|
|
78
|
+
send(init: SendInit): Promise<Sent>;
|
|
79
|
+
/** Send, read the whole JSON body and validate it. */
|
|
80
|
+
json<T>(init: SendInit, schema: {
|
|
81
|
+
safeParse(value: unknown): SafeParse<T>;
|
|
82
|
+
}): Promise<{
|
|
83
|
+
value: T;
|
|
84
|
+
requestId: string;
|
|
85
|
+
response: Response;
|
|
86
|
+
}>;
|
|
87
|
+
}
|
|
88
|
+
export type SafeParse<T> = {
|
|
89
|
+
success: true;
|
|
90
|
+
data: T;
|
|
91
|
+
} | {
|
|
92
|
+
success: false;
|
|
93
|
+
error: {
|
|
94
|
+
issues: readonly {
|
|
95
|
+
path: readonly PropertyKey[];
|
|
96
|
+
}[];
|
|
97
|
+
};
|
|
98
|
+
};
|
|
99
|
+
/** Parse and validate a JSON document, or fail as a protocol error. */
|
|
100
|
+
export declare function parseJson<T>(text: string, schema: {
|
|
101
|
+
safeParse(value: unknown): SafeParse<T>;
|
|
102
|
+
}, requestId: string | null): T;
|
|
103
|
+
export declare function validate<T>(payload: unknown, schema: {
|
|
104
|
+
safeParse(value: unknown): SafeParse<T>;
|
|
105
|
+
}, requestId: string | null, what: string): T;
|
|
106
|
+
/** A body read that failed: the caller's abort, the deadline, or the network. */
|
|
107
|
+
export declare function bodyReadFailure(sent: Sent, cause: unknown): Error;
|
|
108
|
+
/** The `Content-Type` essence: lowercase, without parameters. */
|
|
109
|
+
export declare function mediaTypeOf(response: Response): string | undefined;
|
package/dist/http.js
ADDED
|
@@ -0,0 +1,338 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The one place a request is sent: credentials, headers, the deadline, the
|
|
3
|
+
* caller's abort signal, typed error bodies and retries before the first byte.
|
|
4
|
+
*/
|
|
5
|
+
import { chatCompletionErrorSchema, KAANA_API_BASE_URL, KAANA_IDEMPOTENCY_KEY_HEADER, KAANA_ON_BEHALF_OF_HEADER, KAANA_REQUEST_ID_HEADER, publicErrorSchema, } from '@kaana.ai/contracts';
|
|
6
|
+
import { cancelledByCaller, fromPublicError, isRetryableCode, KaanaAuthenticationError, KaanaConnectionError, kaanaError, KaanaProtocolError, KaanaUnavailableError, } from './errors.js';
|
|
7
|
+
const DEFAULT_TIMEOUT_MS = 120_000;
|
|
8
|
+
const DEFAULT_MAX_RETRIES = 2;
|
|
9
|
+
const BASE_BACKOFF_MS = 500;
|
|
10
|
+
const MAX_BACKOFF_MS = 8_000;
|
|
11
|
+
const MAX_ERROR_BODY_CHARACTERS = 64 * 1024;
|
|
12
|
+
const LOOPBACK_HOSTS = new Set(['localhost', '127.0.0.1', '[::1]']);
|
|
13
|
+
export class Transport {
|
|
14
|
+
baseURL;
|
|
15
|
+
WebSocket;
|
|
16
|
+
timeoutMs;
|
|
17
|
+
#credential;
|
|
18
|
+
#fetch;
|
|
19
|
+
#maxRetries;
|
|
20
|
+
constructor(options) {
|
|
21
|
+
if (options === undefined || options === null || options.credential === undefined) {
|
|
22
|
+
throw new TypeError('Kaana: options.credential is required');
|
|
23
|
+
}
|
|
24
|
+
if (typeof options.credential !== 'string' && typeof options.credential !== 'function') {
|
|
25
|
+
throw new TypeError('Kaana: options.credential is a string or a function returning one');
|
|
26
|
+
}
|
|
27
|
+
this.#credential = options.credential;
|
|
28
|
+
this.baseURL = normalizeBaseURL(options.baseURL ?? KAANA_API_BASE_URL);
|
|
29
|
+
const fetchImpl = options.fetch ?? globalThis.fetch;
|
|
30
|
+
if (typeof fetchImpl !== 'function') {
|
|
31
|
+
throw new TypeError('Kaana: no fetch is available; pass options.fetch');
|
|
32
|
+
}
|
|
33
|
+
this.#fetch = (input, init) => fetchImpl(input, init);
|
|
34
|
+
this.WebSocket = options.WebSocket ?? globalThis.WebSocket;
|
|
35
|
+
this.timeoutMs = positiveInteger(options.timeoutMs, DEFAULT_TIMEOUT_MS, 'timeoutMs');
|
|
36
|
+
const retries = options.maxRetries ?? DEFAULT_MAX_RETRIES;
|
|
37
|
+
if (!Number.isInteger(retries) || retries < 0) {
|
|
38
|
+
throw new TypeError('Kaana: options.maxRetries is a non-negative integer');
|
|
39
|
+
}
|
|
40
|
+
this.#maxRetries = retries;
|
|
41
|
+
}
|
|
42
|
+
/** The credential, read now. Never stored. */
|
|
43
|
+
async credential() {
|
|
44
|
+
const value = typeof this.#credential === 'function' ? await this.#credential() : this.#credential;
|
|
45
|
+
if (typeof value !== 'string' || value.length === 0) {
|
|
46
|
+
throw new KaanaAuthenticationError({
|
|
47
|
+
code: 'authentication_failed',
|
|
48
|
+
message: 'The credential is empty. Create an API key in Oxy Console (https://console.oxy.so).',
|
|
49
|
+
status: null,
|
|
50
|
+
requestId: null,
|
|
51
|
+
retryable: false,
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
return value;
|
|
55
|
+
}
|
|
56
|
+
url(path, query) {
|
|
57
|
+
const search = query === undefined ? '' : `?${new URLSearchParams(query).toString()}`;
|
|
58
|
+
return `${this.baseURL}${path}${search}`;
|
|
59
|
+
}
|
|
60
|
+
/** Send with retries before the first byte. Resolves only with a 2xx. */
|
|
61
|
+
async send(init) {
|
|
62
|
+
const options = init.options ?? {};
|
|
63
|
+
const timeoutMs = positiveInteger(options.timeoutMs, this.timeoutMs, 'timeoutMs');
|
|
64
|
+
for (let attempt = 0;; attempt += 1) {
|
|
65
|
+
if (options.signal?.aborted)
|
|
66
|
+
throw cancelledByCaller(null);
|
|
67
|
+
const credential = await this.credential();
|
|
68
|
+
const headers = {
|
|
69
|
+
Authorization: `Bearer ${credential}`,
|
|
70
|
+
Accept: init.accept,
|
|
71
|
+
};
|
|
72
|
+
if (typeof init.body === 'string')
|
|
73
|
+
headers['Content-Type'] = 'application/json';
|
|
74
|
+
if (options.idempotencyKey !== undefined) {
|
|
75
|
+
headers[KAANA_IDEMPOTENCY_KEY_HEADER] = options.idempotencyKey;
|
|
76
|
+
}
|
|
77
|
+
if (options.onBehalfOfUserId !== undefined) {
|
|
78
|
+
headers[KAANA_ON_BEHALF_OF_HEADER] = options.onBehalfOfUserId;
|
|
79
|
+
}
|
|
80
|
+
const control = attemptControl(options.signal, timeoutMs);
|
|
81
|
+
let response;
|
|
82
|
+
try {
|
|
83
|
+
response = await this.#fetch(this.url(init.path, init.query), {
|
|
84
|
+
method: init.method,
|
|
85
|
+
headers,
|
|
86
|
+
...(init.body === undefined ? {} : { body: init.body }),
|
|
87
|
+
signal: control.signal,
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
catch (cause) {
|
|
91
|
+
control.release();
|
|
92
|
+
if (control.aborted())
|
|
93
|
+
throw cancelledByCaller(null);
|
|
94
|
+
if (control.timedOut()) {
|
|
95
|
+
throw new KaanaConnectionError(`No response within ${timeoutMs} ms.`, {
|
|
96
|
+
cause,
|
|
97
|
+
timedOut: true,
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
const failure = new KaanaConnectionError('The request could not reach Kaana.', { cause });
|
|
101
|
+
if (attempt < this.#maxRetries) {
|
|
102
|
+
await wait(backoff(attempt), options.signal);
|
|
103
|
+
continue;
|
|
104
|
+
}
|
|
105
|
+
throw failure;
|
|
106
|
+
}
|
|
107
|
+
if (!response.ok) {
|
|
108
|
+
let failure;
|
|
109
|
+
try {
|
|
110
|
+
failure = await errorFromResponse(response, init.dialect ?? 'public');
|
|
111
|
+
}
|
|
112
|
+
catch (cause) {
|
|
113
|
+
control.release();
|
|
114
|
+
if (control.aborted())
|
|
115
|
+
throw cancelledByCaller(null);
|
|
116
|
+
throw new KaanaConnectionError('The error response could not be read.', {
|
|
117
|
+
cause,
|
|
118
|
+
timedOut: control.timedOut(),
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
control.release();
|
|
122
|
+
if (failure instanceof KaanaProtocolError ||
|
|
123
|
+
!failure.retryable ||
|
|
124
|
+
attempt >= this.#maxRetries) {
|
|
125
|
+
throw failure;
|
|
126
|
+
}
|
|
127
|
+
await wait(failure.retryAfterMs ?? backoff(attempt), options.signal);
|
|
128
|
+
continue;
|
|
129
|
+
}
|
|
130
|
+
const requestId = response.headers.get(KAANA_REQUEST_ID_HEADER);
|
|
131
|
+
if (requestId === null || requestId.length === 0) {
|
|
132
|
+
control.abort();
|
|
133
|
+
control.release();
|
|
134
|
+
await response.body?.cancel().catch(() => undefined);
|
|
135
|
+
throw new KaanaProtocolError(`The response carries no ${KAANA_REQUEST_ID_HEADER}.`, null);
|
|
136
|
+
}
|
|
137
|
+
if (init.stream)
|
|
138
|
+
control.clearDeadline();
|
|
139
|
+
return { response, requestId, ...control };
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
/** Send, read the whole JSON body and validate it. */
|
|
143
|
+
async json(init, schema) {
|
|
144
|
+
const sent = await this.send(init);
|
|
145
|
+
let text;
|
|
146
|
+
try {
|
|
147
|
+
text = await sent.response.text();
|
|
148
|
+
}
|
|
149
|
+
catch (cause) {
|
|
150
|
+
throw bodyReadFailure(sent, cause);
|
|
151
|
+
}
|
|
152
|
+
finally {
|
|
153
|
+
sent.release();
|
|
154
|
+
}
|
|
155
|
+
return {
|
|
156
|
+
value: parseJson(text, schema, sent.requestId),
|
|
157
|
+
requestId: sent.requestId,
|
|
158
|
+
response: sent.response,
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
/** Parse and validate a JSON document, or fail as a protocol error. */
|
|
163
|
+
export function parseJson(text, schema, requestId) {
|
|
164
|
+
let payload;
|
|
165
|
+
try {
|
|
166
|
+
payload = JSON.parse(text);
|
|
167
|
+
}
|
|
168
|
+
catch (_error) {
|
|
169
|
+
throw new KaanaProtocolError('The response is not JSON.', requestId);
|
|
170
|
+
}
|
|
171
|
+
return validate(payload, schema, requestId, 'response');
|
|
172
|
+
}
|
|
173
|
+
export function validate(payload, schema, requestId, what) {
|
|
174
|
+
const parsed = schema.safeParse(payload);
|
|
175
|
+
if (!parsed.success) {
|
|
176
|
+
const path = parsed.error.issues[0]?.path ?? [];
|
|
177
|
+
throw new KaanaProtocolError(`The ${what} does not satisfy @kaana.ai/contracts at ${path.length === 0 ? 'its root' : path.map(String).join('.')}.`, requestId);
|
|
178
|
+
}
|
|
179
|
+
return parsed.data;
|
|
180
|
+
}
|
|
181
|
+
/** A body read that failed: the caller's abort, the deadline, or the network. */
|
|
182
|
+
export function bodyReadFailure(sent, cause) {
|
|
183
|
+
if (sent.aborted())
|
|
184
|
+
return cancelledByCaller(sent.requestId);
|
|
185
|
+
return new KaanaConnectionError(sent.timedOut() ? 'The response did not complete in time.' : 'The response was cut off.', { cause, timedOut: sent.timedOut() });
|
|
186
|
+
}
|
|
187
|
+
/** The `Content-Type` essence: lowercase, without parameters. */
|
|
188
|
+
export function mediaTypeOf(response) {
|
|
189
|
+
return response.headers.get('Content-Type')?.split(';', 1)[0]?.trim().toLowerCase();
|
|
190
|
+
}
|
|
191
|
+
/* -------------------------------------------------------------------------- */
|
|
192
|
+
function normalizeBaseURL(raw) {
|
|
193
|
+
let parsed;
|
|
194
|
+
try {
|
|
195
|
+
parsed = new URL(raw);
|
|
196
|
+
}
|
|
197
|
+
catch (_error) {
|
|
198
|
+
throw new TypeError(`Kaana: baseURL ${JSON.stringify(raw)} is not a URL`);
|
|
199
|
+
}
|
|
200
|
+
const normalized = `${parsed.origin}${parsed.pathname.replace(/\/+$/, '')}`;
|
|
201
|
+
if (normalized === KAANA_API_BASE_URL)
|
|
202
|
+
return normalized;
|
|
203
|
+
const loopback = (parsed.protocol === 'http:' || parsed.protocol === 'https:') &&
|
|
204
|
+
LOOPBACK_HOSTS.has(parsed.hostname);
|
|
205
|
+
if (!loopback || parsed.search !== '' || parsed.hash !== '' || parsed.username !== '') {
|
|
206
|
+
throw new TypeError(`Kaana: baseURL must be ${KAANA_API_BASE_URL} or a loopback origin in development`);
|
|
207
|
+
}
|
|
208
|
+
return normalized;
|
|
209
|
+
}
|
|
210
|
+
function positiveInteger(value, fallback, name) {
|
|
211
|
+
if (value === undefined)
|
|
212
|
+
return fallback;
|
|
213
|
+
if (!Number.isFinite(value) || value <= 0) {
|
|
214
|
+
throw new TypeError(`Kaana: ${name} is a positive number of milliseconds`);
|
|
215
|
+
}
|
|
216
|
+
return value;
|
|
217
|
+
}
|
|
218
|
+
function attemptControl(caller, timeoutMs) {
|
|
219
|
+
const controller = new AbortController();
|
|
220
|
+
let timedOut = false;
|
|
221
|
+
let aborted = false;
|
|
222
|
+
const timer = setTimeout(() => {
|
|
223
|
+
timedOut = true;
|
|
224
|
+
controller.abort(new Error('timeout'));
|
|
225
|
+
}, timeoutMs);
|
|
226
|
+
const onAbort = () => {
|
|
227
|
+
aborted = true;
|
|
228
|
+
controller.abort(caller?.reason);
|
|
229
|
+
};
|
|
230
|
+
caller?.addEventListener('abort', onAbort, { once: true });
|
|
231
|
+
return {
|
|
232
|
+
signal: controller.signal,
|
|
233
|
+
timedOut: () => timedOut,
|
|
234
|
+
aborted: () => aborted,
|
|
235
|
+
abort: (reason) => {
|
|
236
|
+
aborted = true;
|
|
237
|
+
controller.abort(reason);
|
|
238
|
+
},
|
|
239
|
+
clearDeadline: () => clearTimeout(timer),
|
|
240
|
+
release: () => {
|
|
241
|
+
clearTimeout(timer);
|
|
242
|
+
caller?.removeEventListener('abort', onAbort);
|
|
243
|
+
},
|
|
244
|
+
};
|
|
245
|
+
}
|
|
246
|
+
async function errorFromResponse(response, dialect) {
|
|
247
|
+
const status = response.status;
|
|
248
|
+
const headerRequestId = response.headers.get(KAANA_REQUEST_ID_HEADER);
|
|
249
|
+
const retryAfterHeader = retryAfterMs(response.headers.get('Retry-After'));
|
|
250
|
+
const text = (await response.text()).slice(0, MAX_ERROR_BODY_CHARACTERS);
|
|
251
|
+
let payload;
|
|
252
|
+
try {
|
|
253
|
+
payload = JSON.parse(text);
|
|
254
|
+
}
|
|
255
|
+
catch (_error) {
|
|
256
|
+
payload = undefined;
|
|
257
|
+
}
|
|
258
|
+
if (dialect === 'chat') {
|
|
259
|
+
const parsed = chatCompletionErrorSchema.safeParse(payload);
|
|
260
|
+
if (parsed.success) {
|
|
261
|
+
const { code, message, param } = parsed.data.error;
|
|
262
|
+
const retryable = isRetryableCode(code);
|
|
263
|
+
return kaanaError({
|
|
264
|
+
code,
|
|
265
|
+
message,
|
|
266
|
+
status,
|
|
267
|
+
requestId: headerRequestId,
|
|
268
|
+
retryable,
|
|
269
|
+
...(retryable && retryAfterHeader !== undefined ? { retryAfterMs: retryAfterHeader } : {}),
|
|
270
|
+
...(param === null ? {} : { param }),
|
|
271
|
+
});
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
else {
|
|
275
|
+
const parsed = publicErrorSchema.safeParse(payload);
|
|
276
|
+
if (parsed.success) {
|
|
277
|
+
const error = fromPublicError(parsed.data, status);
|
|
278
|
+
if (error.retryable && error.retryAfterMs === undefined && retryAfterHeader !== undefined) {
|
|
279
|
+
return kaanaError({
|
|
280
|
+
code: error.code,
|
|
281
|
+
message: error.message,
|
|
282
|
+
status,
|
|
283
|
+
requestId: error.requestId,
|
|
284
|
+
retryable: true,
|
|
285
|
+
retryAfterMs: retryAfterHeader,
|
|
286
|
+
...(error.param === undefined ? {} : { param: error.param }),
|
|
287
|
+
});
|
|
288
|
+
}
|
|
289
|
+
return error;
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
// A gateway in front of Kaana (or a load balancer with no healthy target)
|
|
293
|
+
// answers without the contract's body. That is an outage, not a protocol
|
|
294
|
+
// violation by Kaana, and it is as retryable as Kaana's own 503.
|
|
295
|
+
if (status === 502 || status === 503 || status === 504) {
|
|
296
|
+
return new KaanaUnavailableError({
|
|
297
|
+
code: 'service_unavailable',
|
|
298
|
+
message: `Kaana is unavailable (HTTP ${status}).`,
|
|
299
|
+
status,
|
|
300
|
+
requestId: headerRequestId,
|
|
301
|
+
retryable: true,
|
|
302
|
+
...(retryAfterHeader === undefined ? {} : { retryAfterMs: retryAfterHeader }),
|
|
303
|
+
});
|
|
304
|
+
}
|
|
305
|
+
return new KaanaProtocolError(`HTTP ${status} carries no error body of @kaana.ai/contracts.`, headerRequestId);
|
|
306
|
+
}
|
|
307
|
+
function retryAfterMs(header) {
|
|
308
|
+
if (header === null)
|
|
309
|
+
return undefined;
|
|
310
|
+
const trimmed = header.trim();
|
|
311
|
+
if (/^\d+$/.test(trimmed))
|
|
312
|
+
return Number(trimmed) * 1000;
|
|
313
|
+
const at = Date.parse(trimmed);
|
|
314
|
+
if (Number.isNaN(at))
|
|
315
|
+
return undefined;
|
|
316
|
+
return Math.max(0, at - Date.now());
|
|
317
|
+
}
|
|
318
|
+
function backoff(attempt) {
|
|
319
|
+
const ceiling = Math.min(MAX_BACKOFF_MS, BASE_BACKOFF_MS * 2 ** attempt);
|
|
320
|
+
return Math.round(ceiling / 2 + Math.random() * (ceiling / 2));
|
|
321
|
+
}
|
|
322
|
+
function wait(milliseconds, signal) {
|
|
323
|
+
return new Promise((resolve, reject) => {
|
|
324
|
+
if (signal?.aborted) {
|
|
325
|
+
reject(cancelledByCaller(null));
|
|
326
|
+
return;
|
|
327
|
+
}
|
|
328
|
+
const onAbort = () => {
|
|
329
|
+
clearTimeout(timer);
|
|
330
|
+
reject(cancelledByCaller(null));
|
|
331
|
+
};
|
|
332
|
+
const timer = setTimeout(() => {
|
|
333
|
+
signal?.removeEventListener('abort', onAbort);
|
|
334
|
+
resolve();
|
|
335
|
+
}, milliseconds);
|
|
336
|
+
signal?.addEventListener('abort', onAbort, { once: true });
|
|
337
|
+
});
|
|
338
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@kaana.ai/sdk` — the client of Kaana's public API, `https://kaana.ai/v1`.
|
|
3
|
+
*
|
|
4
|
+
* Applications, `oxy_sk_…` keys, limits, budgets and billing live in Oxy
|
|
5
|
+
* Console (https://console.oxy.so). The credential is a function or a string;
|
|
6
|
+
* the SDK never stores it and has no Oxy dependency.
|
|
7
|
+
*/
|
|
8
|
+
export type { SpeechResult, SpeechStream, TranscriptionRequest } from './audio.js';
|
|
9
|
+
export { encodeBase64, decodeBase64 } from './base64.js';
|
|
10
|
+
export { Kaana } from './client.js';
|
|
11
|
+
export { KaanaAuthenticationError, KaanaBillingError, KaanaCancelledError, KaanaConnectionError, KaanaContentFilteredError, KaanaError, type KaanaErrorInit, KaanaInternalError, KaanaInvalidRequestError, KaanaPermissionError, KaanaProtocolError, KaanaRateLimitError, KaanaUnavailableError, } from './errors.js';
|
|
12
|
+
export type { KaanaCredential, KaanaFetch, KaanaOptions, RequestOptions } from './http.js';
|
|
13
|
+
export { MAX_REALTIME_AUDIO_FRAME_BYTES, type RealtimeClientEventInput, type RealtimeConnectRequest, type RealtimeSession, } from './realtime.js';
|
|
14
|
+
export type { KaanaStream } from './stream.js';
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@kaana.ai/sdk` — the client of Kaana's public API, `https://kaana.ai/v1`.
|
|
3
|
+
*
|
|
4
|
+
* Applications, `oxy_sk_…` keys, limits, budgets and billing live in Oxy
|
|
5
|
+
* Console (https://console.oxy.so). The credential is a function or a string;
|
|
6
|
+
* the SDK never stores it and has no Oxy dependency.
|
|
7
|
+
*/
|
|
8
|
+
export { encodeBase64, decodeBase64 } from './base64.js';
|
|
9
|
+
export { Kaana } from './client.js';
|
|
10
|
+
export { KaanaAuthenticationError, KaanaBillingError, KaanaCancelledError, KaanaConnectionError, KaanaContentFilteredError, KaanaError, KaanaInternalError, KaanaInvalidRequestError, KaanaPermissionError, KaanaProtocolError, KaanaRateLimitError, KaanaUnavailableError, } from './errors.js';
|
|
11
|
+
export { MAX_REALTIME_AUDIO_FRAME_BYTES, } from './realtime.js';
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/** `GET /v1/realtime`: one WebSocket session, resumable, settled once on close. */
|
|
2
|
+
import { type ModelReference, type RealtimeClientCommand, type RealtimeServerEvent, type RealtimeSessionOpen, type Usage } from '@kaana.ai/contracts';
|
|
3
|
+
import type { RequestOptions, Transport } from './http.js';
|
|
4
|
+
/** What `realtime.connect` opens: the `session.open` frame and the model it is for. */
|
|
5
|
+
export type RealtimeConnectRequest = RealtimeSessionOpen & {
|
|
6
|
+
model: ModelReference;
|
|
7
|
+
};
|
|
8
|
+
type DistributiveOmit<T, K extends PropertyKey> = T extends unknown ? Omit<T, K> : never;
|
|
9
|
+
/**
|
|
10
|
+
* A client command. `schemaVersion`, `requestId` (the session's) and
|
|
11
|
+
* `commandId` (a fresh unique id) are filled in when omitted.
|
|
12
|
+
*/
|
|
13
|
+
export type RealtimeClientEventInput = DistributiveOmit<RealtimeClientCommand, 'schemaVersion' | 'requestId' | 'commandId'> & Partial<Pick<RealtimeClientCommand, 'schemaVersion' | 'requestId' | 'commandId'>>;
|
|
14
|
+
export interface RealtimeSession extends AsyncIterable<RealtimeServerEvent> {
|
|
15
|
+
/** The session's request id, from `session.created`. */
|
|
16
|
+
readonly sessionId: string;
|
|
17
|
+
send(event: RealtimeClientEventInput): void;
|
|
18
|
+
/** Raw PCM16 in, as `input_audio.append` frames of at most 49 152 bytes. */
|
|
19
|
+
appendAudio(pcm16: Uint8Array): void;
|
|
20
|
+
/** Reattach after a dropped connection, within the session's resume window. */
|
|
21
|
+
resume(): Promise<void>;
|
|
22
|
+
/** End the session; resolves with what it consumed. It settles exactly once. */
|
|
23
|
+
close(): Promise<Usage>;
|
|
24
|
+
}
|
|
25
|
+
/** The most audio one `input_audio.append` frame carries. */
|
|
26
|
+
export declare const MAX_REALTIME_AUDIO_FRAME_BYTES = 49152;
|
|
27
|
+
export declare function connectRealtime(transport: Transport, request: RealtimeConnectRequest, options: RequestOptions): Promise<RealtimeSession>;
|
|
28
|
+
export {};
|