@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/dist/chat.js ADDED
@@ -0,0 +1,149 @@
1
+ /** `POST /v1/chat/completions`, the OpenAI dialect: whole and streamed. */
2
+ import { chatCompletionChunkSchema, chatCompletionErrorSchema, chatCompletionSchema, } from '@kaana.ai/contracts';
3
+ import { concatBytes, decodeBase64, encodeBase64 } from './base64.js';
4
+ import { isRetryableCode, kaanaError, KaanaProtocolError } from './errors.js';
5
+ import { cutOff, readFrames } from './events.js';
6
+ import { validate } from './http.js';
7
+ import { StreamHandle } from './stream.js';
8
+ const DONE = '[DONE]';
9
+ export function chatStream(transport, body, options) {
10
+ return new StreamHandle(async function* (context) {
11
+ const sent = await transport.send({
12
+ method: 'POST',
13
+ path: '/chat/completions',
14
+ body,
15
+ accept: 'text/event-stream',
16
+ dialect: 'chat',
17
+ options: { ...options, signal: context.signal },
18
+ stream: true,
19
+ });
20
+ const requestId = sent.requestId;
21
+ context.setRequestId(requestId);
22
+ const fold = new ChatFold();
23
+ let output = false;
24
+ for await (const frame of readFrames(sent, () => output)) {
25
+ if (frame.data === DONE)
26
+ return fold.completion(requestId);
27
+ let payload;
28
+ try {
29
+ payload = JSON.parse(frame.data);
30
+ }
31
+ catch (_error) {
32
+ throw new KaanaProtocolError('The stream sent a frame that is not JSON.', requestId);
33
+ }
34
+ if (typeof payload === 'object' && payload !== null && 'error' in payload) {
35
+ const { error } = validate(payload, chatCompletionErrorSchema, requestId, 'stream error');
36
+ throw kaanaError({
37
+ code: error.code,
38
+ message: error.message,
39
+ status: null,
40
+ requestId,
41
+ retryable: isRetryableCode(error.code),
42
+ ...(error.param === null ? {} : { param: error.param }),
43
+ afterOutput: output,
44
+ });
45
+ }
46
+ const chunk = validate(payload, chatCompletionChunkSchema, requestId, 'stream chunk');
47
+ if (chunk.choices.some((choice) => hasOutput(choice.delta))) {
48
+ output = true;
49
+ context.markOutput();
50
+ }
51
+ fold.add(chunk);
52
+ yield chunk;
53
+ }
54
+ throw cutOff(requestId);
55
+ }, options.signal);
56
+ }
57
+ function hasOutput(delta) {
58
+ return ((delta.content !== undefined && delta.content.length > 0) ||
59
+ delta.refusal !== undefined ||
60
+ delta.audio !== undefined ||
61
+ (delta.tool_calls !== undefined && delta.tool_calls.length > 0));
62
+ }
63
+ const ZERO_USAGE = {
64
+ prompt_tokens: 0,
65
+ completion_tokens: 0,
66
+ total_tokens: 0,
67
+ prompt_tokens_details: { cached_tokens: 0 },
68
+ completion_tokens_details: { reasoning_tokens: 0 },
69
+ };
70
+ /** Folds the chunks of one completion into the `ChatCompletion` it describes. */
71
+ export class ChatFold {
72
+ #first;
73
+ #usage;
74
+ #choices = new Map();
75
+ add(chunk) {
76
+ this.#first ??= chunk;
77
+ if (chunk.usage !== undefined)
78
+ this.#usage = chunk.usage;
79
+ for (const choice of chunk.choices) {
80
+ const fold = this.#choices.get(choice.index) ?? {
81
+ content: null,
82
+ finishReason: null,
83
+ toolCalls: new Map(),
84
+ };
85
+ const delta = choice.delta;
86
+ if (delta.content !== undefined)
87
+ fold.content = (fold.content ?? '') + delta.content;
88
+ if (delta.audio !== undefined) {
89
+ fold.audio ??= { id: delta.audio.id, data: [], transcript: '', expiresAt: 0 };
90
+ if (delta.audio.data !== undefined)
91
+ fold.audio.data.push(decodeBase64(delta.audio.data));
92
+ if (delta.audio.transcript !== undefined)
93
+ fold.audio.transcript += delta.audio.transcript;
94
+ if (delta.audio.expires_at !== undefined)
95
+ fold.audio.expiresAt = delta.audio.expires_at;
96
+ }
97
+ for (const call of delta.tool_calls ?? []) {
98
+ const known = fold.toolCalls.get(call.index) ?? { id: '', name: '', arguments: '' };
99
+ if (call.id !== undefined)
100
+ known.id = call.id;
101
+ if (call.function.name !== undefined)
102
+ known.name += call.function.name;
103
+ if (call.function.arguments !== undefined)
104
+ known.arguments += call.function.arguments;
105
+ fold.toolCalls.set(call.index, known);
106
+ }
107
+ if (choice.finish_reason !== null)
108
+ fold.finishReason = choice.finish_reason;
109
+ this.#choices.set(choice.index, fold);
110
+ }
111
+ }
112
+ completion(requestId) {
113
+ const first = this.#first;
114
+ if (first === undefined) {
115
+ throw new KaanaProtocolError('The stream ended without a chunk.', requestId);
116
+ }
117
+ const choices = [...this.#choices]
118
+ .sort(([a], [b]) => a - b)
119
+ .map(([index, fold]) => {
120
+ const message = { role: 'assistant', content: fold.content };
121
+ if (fold.audio !== undefined) {
122
+ message.audio = {
123
+ id: fold.audio.id,
124
+ data: encodeBase64(concatBytes(fold.audio.data)),
125
+ transcript: fold.audio.transcript,
126
+ expires_at: fold.audio.expiresAt,
127
+ };
128
+ }
129
+ if (fold.toolCalls.size > 0) {
130
+ message.tool_calls = [...fold.toolCalls]
131
+ .sort(([a], [b]) => a - b)
132
+ .map(([, call]) => ({
133
+ id: call.id,
134
+ type: 'function',
135
+ function: { name: call.name, arguments: call.arguments },
136
+ }));
137
+ }
138
+ return { index, message, finish_reason: fold.finishReason ?? 'stop' };
139
+ });
140
+ return validate({
141
+ id: first.id,
142
+ object: 'chat.completion',
143
+ created: first.created,
144
+ model: first.model,
145
+ choices,
146
+ usage: this.#usage ?? ZERO_USAGE,
147
+ }, chatCompletionSchema, requestId, 'folded completion');
148
+ }
149
+ }
@@ -0,0 +1,52 @@
1
+ /** The `Kaana` client (Kaana ADR 0002, "Superficie del SDK"). */
2
+ import { type ChatCompletion, type ChatCompletionChunk, type ChatCompletionsRequest, type DecisionRequest, type DecisionSuccess, type EmbeddingsRequest, type EmbeddingsResponse, type GenerationRecord, type ImageGenerations, type ImageGenerationsRequest, type Model, type ModelReference, type PowerLevelEntry, type Response, type ResponseStreamEvent, type ResponsesRequest, type SpeechRequest, type Transcription } from '@kaana.ai/contracts';
3
+ import { type SpeechResult, type SpeechStream, type TranscriptionRequest } from './audio.js';
4
+ import { type KaanaOptions, type RequestOptions } from './http.js';
5
+ import { type RealtimeConnectRequest, type RealtimeSession } from './realtime.js';
6
+ import type { KaanaStream } from './stream.js';
7
+ export declare class Kaana {
8
+ #private;
9
+ readonly models: {
10
+ list(options?: RequestOptions): Promise<Model[]>;
11
+ get(id: ModelReference, options?: RequestOptions): Promise<Model>;
12
+ };
13
+ readonly powerLevels: {
14
+ list(options?: RequestOptions): Promise<PowerLevelEntry[]>;
15
+ };
16
+ readonly responses: {
17
+ create(request: ResponsesRequest, options?: RequestOptions): Promise<Response>;
18
+ stream(request: ResponsesRequest, options?: RequestOptions): KaanaStream<ResponseStreamEvent, Response>;
19
+ };
20
+ readonly chat: {
21
+ completions: {
22
+ create(request: ChatCompletionsRequest, options?: RequestOptions): Promise<ChatCompletion>;
23
+ stream(request: ChatCompletionsRequest, options?: RequestOptions): KaanaStream<ChatCompletionChunk, ChatCompletion>;
24
+ };
25
+ };
26
+ readonly embeddings: {
27
+ create(request: EmbeddingsRequest, options?: RequestOptions): Promise<EmbeddingsResponse>;
28
+ };
29
+ readonly audio: {
30
+ speech(request: SpeechRequest, options?: RequestOptions): Promise<SpeechResult>;
31
+ speechStream(request: SpeechRequest, options?: RequestOptions): SpeechStream;
32
+ transcriptions: {
33
+ create(request: TranscriptionRequest, options?: RequestOptions): Promise<Transcription>;
34
+ };
35
+ };
36
+ readonly images: {
37
+ generate(request: ImageGenerationsRequest, options?: RequestOptions): Promise<ImageGenerations>;
38
+ };
39
+ readonly decisions: {
40
+ create(request: DecisionRequest, options?: RequestOptions): Promise<DecisionSuccess>;
41
+ };
42
+ readonly generations: {
43
+ get(id: string, options?: RequestOptions): Promise<GenerationRecord>;
44
+ getByIdempotencyKey(key: string, options?: RequestOptions): Promise<GenerationRecord>;
45
+ };
46
+ readonly realtime: {
47
+ connect(request: RealtimeConnectRequest, options?: RequestOptions): Promise<RealtimeSession>;
48
+ };
49
+ constructor(options: KaanaOptions);
50
+ /** The origin and version this client speaks to. */
51
+ get baseURL(): string;
52
+ }
package/dist/client.js ADDED
@@ -0,0 +1,109 @@
1
+ /** The `Kaana` client (Kaana ADR 0002, "Superficie del SDK"). */
2
+ import { chatCompletionSchema, chatCompletionsRequestSchema, decisionRequestSchema, decisionSuccessSchema, embeddingsRequestSchema, embeddingsResponseSchema, generationRecordSchema, imageGenerationsRequestSchema, imageGenerationsSchema, modelListSchema, modelReferenceSchema, modelSchema, powerLevelListSchema, responseSchema, responsesRequestSchema, } from '@kaana.ai/contracts';
3
+ import { speech, speechStream, transcribe, } from './audio.js';
4
+ import { chatStream } from './chat.js';
5
+ import { clientRefusal } from './errors.js';
6
+ import { Transport } from './http.js';
7
+ import { connectRealtime } from './realtime.js';
8
+ import { responsesStream } from './responses.js';
9
+ /** Validate a request before sending it; the original object is what is sent. */
10
+ function body(request, schema) {
11
+ const parsed = schema.safeParse(request);
12
+ if (!parsed.success) {
13
+ const issue = parsed.error.issues[0];
14
+ throw clientRefusal(issue?.path.map(String).join('.') || 'request', issue?.message ?? 'the request does not satisfy @kaana.ai/contracts');
15
+ }
16
+ return JSON.stringify(request);
17
+ }
18
+ function modelPath(id) {
19
+ if (!modelReferenceSchema.safeParse(id).success) {
20
+ throw clientRefusal('id', 'a model reference is publisher/model[@revision]');
21
+ }
22
+ return id
23
+ .split('/')
24
+ .map((segment) => encodeURIComponent(segment))
25
+ .join('/');
26
+ }
27
+ export class Kaana {
28
+ #transport;
29
+ models;
30
+ powerLevels;
31
+ responses;
32
+ chat;
33
+ embeddings;
34
+ audio;
35
+ images;
36
+ decisions;
37
+ generations;
38
+ realtime;
39
+ constructor(options) {
40
+ const transport = new Transport(options);
41
+ this.#transport = transport;
42
+ const get = async (path, schema, options = {}, query) => (await transport.json({ method: 'GET', path, accept: 'application/json', options, ...(query ? { query } : {}) }, schema)).value;
43
+ const post = async (path, json, schema, options = {}, dialect = 'public') => (await transport.json({ method: 'POST', path, body: json, accept: 'application/json', options, dialect }, schema)).value;
44
+ this.models = {
45
+ list: async (options) => (await get('/models', modelListSchema, options)).data,
46
+ get: async (id, options) => get(`/models/${modelPath(id)}`, modelSchema, options),
47
+ };
48
+ this.powerLevels = {
49
+ list: async (options) => (await get('/power-levels', powerLevelListSchema, options)).data,
50
+ };
51
+ this.responses = {
52
+ create: async (request, options) => {
53
+ if (request.stream === true) {
54
+ throw clientRefusal('stream', 'responses.create returns whole; use responses.stream');
55
+ }
56
+ return post('/responses', body(request, responsesRequestSchema), responseSchema, options);
57
+ },
58
+ stream: (request, options = {}) => responsesStream(transport, body({ ...request, stream: true }, responsesRequestSchema), options),
59
+ };
60
+ this.chat = {
61
+ completions: {
62
+ create: async (request, options) => {
63
+ if (request.stream === true) {
64
+ throw clientRefusal('stream', 'chat.completions.create returns whole; use chat.completions.stream');
65
+ }
66
+ return post('/chat/completions', body(request, chatCompletionsRequestSchema), chatCompletionSchema, options, 'chat');
67
+ },
68
+ stream: (request, options = {}) => chatStream(transport, body({ ...request, stream: true }, chatCompletionsRequestSchema), options),
69
+ },
70
+ };
71
+ this.embeddings = {
72
+ create: async (request, options) => post('/embeddings', body(request, embeddingsRequestSchema), embeddingsResponseSchema, options),
73
+ };
74
+ this.audio = {
75
+ speech: (request, options = {}) => speech(transport, request, options),
76
+ speechStream: (request, options = {}) => speechStream(transport, request, options),
77
+ transcriptions: {
78
+ create: (request, options = {}) => transcribe(transport, request, options),
79
+ },
80
+ };
81
+ this.images = {
82
+ generate: async (request, options) => post('/images/generations', body(request, imageGenerationsRequestSchema), imageGenerationsSchema, options),
83
+ };
84
+ this.decisions = {
85
+ create: async (request, options) => post('/decisions', body(request, decisionRequestSchema), decisionSuccessSchema, options),
86
+ };
87
+ this.generations = {
88
+ get: async (id, options) => {
89
+ if (typeof id !== 'string' || id.length === 0 || id.length > 128) {
90
+ throw clientRefusal('id', 'a generation id is 1 to 128 characters');
91
+ }
92
+ return get(`/generations/${encodeURIComponent(id)}`, generationRecordSchema, options);
93
+ },
94
+ getByIdempotencyKey: async (key, options) => {
95
+ if (typeof key !== 'string' || key.length === 0 || key.length > 255) {
96
+ throw clientRefusal('idempotency_key', 'an idempotency key is 1 to 255 characters');
97
+ }
98
+ return get('/generations', generationRecordSchema, options, { idempotency_key: key });
99
+ },
100
+ };
101
+ this.realtime = {
102
+ connect: (request, options = {}) => connectRealtime(transport, request, options),
103
+ };
104
+ }
105
+ /** The origin and version this client speaks to. */
106
+ get baseURL() {
107
+ return this.#transport.baseURL;
108
+ }
109
+ }
@@ -0,0 +1,82 @@
1
+ /**
2
+ * Typed errors (Kaana ADR 0002, "Superficie del SDK").
3
+ *
4
+ * `code` is the PUBLIC vocabulary of `@kaana.ai/contracts`: it says what went
5
+ * wrong and whether to retry, never which provider served or failed. Accounts,
6
+ * keys, limits and balances are managed in Oxy Console (https://console.oxy.so).
7
+ */
8
+ import { type PublicError, type PublicErrorCode } from '@kaana.ai/contracts';
9
+ /** Whether an identical retry of a request that failed with `code` can succeed. */
10
+ export declare function isRetryableCode(code: PublicErrorCode): boolean;
11
+ export interface KaanaErrorInit {
12
+ code: PublicErrorCode;
13
+ message: string;
14
+ /** HTTP status, or null when the error arrived as a terminal stream event. */
15
+ status: number | null;
16
+ requestId: string | null;
17
+ retryable: boolean;
18
+ retryAfterMs?: number;
19
+ param?: string;
20
+ /** Whether output reached the caller before the failure. */
21
+ afterOutput?: boolean;
22
+ }
23
+ /** A failure the API reported in its public vocabulary. */
24
+ export declare class KaanaError extends Error {
25
+ readonly code: PublicErrorCode;
26
+ readonly status: number | null;
27
+ readonly requestId: string | null;
28
+ readonly retryable: boolean;
29
+ readonly retryAfterMs?: number;
30
+ readonly param?: string;
31
+ readonly afterOutput: boolean;
32
+ constructor(init: KaanaErrorInit);
33
+ }
34
+ /** `authentication_failed`: the credential is missing, malformed, expired or revoked. */
35
+ export declare class KaanaAuthenticationError extends KaanaError {
36
+ }
37
+ /** `permission_denied`, `insufficient_scope`, `commercial_permission_denied`, `policy_violation`. */
38
+ export declare class KaanaPermissionError extends KaanaError {
39
+ }
40
+ /** The request itself cannot be served as written. */
41
+ export declare class KaanaInvalidRequestError extends KaanaError {
42
+ }
43
+ /** `insufficient_balance`, `spending_limit_exceeded`, `quota_exceeded`. */
44
+ export declare class KaanaBillingError extends KaanaError {
45
+ }
46
+ /** `rate_limited`. */
47
+ export declare class KaanaRateLimitError extends KaanaError {
48
+ }
49
+ /** No route could serve the request right now, or the upstream failed. */
50
+ export declare class KaanaUnavailableError extends KaanaError {
51
+ }
52
+ /** `upstream_content_filtered`. */
53
+ export declare class KaanaContentFilteredError extends KaanaError {
54
+ }
55
+ /** `cancelled`, including a request the caller aborted. */
56
+ export declare class KaanaCancelledError extends KaanaError {
57
+ }
58
+ /** `internal_error`. */
59
+ export declare class KaanaInternalError extends KaanaError {
60
+ }
61
+ /** A response that does not satisfy `@kaana.ai/contracts`. Never retryable. */
62
+ export declare class KaanaProtocolError extends Error {
63
+ readonly requestId: string | null;
64
+ constructor(message: string, requestId: string | null);
65
+ }
66
+ /** The network failed, or the request timed out, before a response arrived. */
67
+ export declare class KaanaConnectionError extends Error {
68
+ readonly cause: unknown;
69
+ readonly timedOut: boolean;
70
+ constructor(message: string, options?: {
71
+ cause?: unknown;
72
+ timedOut?: boolean;
73
+ });
74
+ }
75
+ /** The typed error for a public code. */
76
+ export declare function kaanaError(init: KaanaErrorInit): KaanaError;
77
+ /** The typed error for a `PublicError` body or terminal event. */
78
+ export declare function fromPublicError(error: PublicError, status: number | null, afterOutput?: boolean): KaanaError;
79
+ /** A request the SDK refuses before sending anything. */
80
+ export declare function clientRefusal(param: string, message: string): KaanaInvalidRequestError;
81
+ /** The caller withdrew the request. */
82
+ export declare function cancelledByCaller(requestId: string | null, afterOutput?: boolean): KaanaError;
package/dist/errors.js ADDED
@@ -0,0 +1,150 @@
1
+ /**
2
+ * Typed errors (Kaana ADR 0002, "Superficie del SDK").
3
+ *
4
+ * `code` is the PUBLIC vocabulary of `@kaana.ai/contracts`: it says what went
5
+ * wrong and whether to retry, never which provider served or failed. Accounts,
6
+ * keys, limits and balances are managed in Oxy Console (https://console.oxy.so).
7
+ */
8
+ import { NON_RETRYABLE_PUBLIC_ERROR_CODES, } from '@kaana.ai/contracts';
9
+ const NON_RETRYABLE = new Set(NON_RETRYABLE_PUBLIC_ERROR_CODES);
10
+ /** Whether an identical retry of a request that failed with `code` can succeed. */
11
+ export function isRetryableCode(code) {
12
+ return !NON_RETRYABLE.has(code);
13
+ }
14
+ /** A failure the API reported in its public vocabulary. */
15
+ export class KaanaError extends Error {
16
+ code;
17
+ status;
18
+ requestId;
19
+ retryable;
20
+ retryAfterMs;
21
+ param;
22
+ afterOutput;
23
+ constructor(init) {
24
+ super(init.message);
25
+ this.name = new.target.name;
26
+ this.code = init.code;
27
+ this.status = init.status;
28
+ this.requestId = init.requestId;
29
+ this.retryable = init.retryable;
30
+ if (init.retryAfterMs !== undefined)
31
+ this.retryAfterMs = init.retryAfterMs;
32
+ if (init.param !== undefined)
33
+ this.param = init.param;
34
+ this.afterOutput = init.afterOutput ?? false;
35
+ }
36
+ }
37
+ /** `authentication_failed`: the credential is missing, malformed, expired or revoked. */
38
+ export class KaanaAuthenticationError extends KaanaError {
39
+ }
40
+ /** `permission_denied`, `insufficient_scope`, `commercial_permission_denied`, `policy_violation`. */
41
+ export class KaanaPermissionError extends KaanaError {
42
+ }
43
+ /** The request itself cannot be served as written. */
44
+ export class KaanaInvalidRequestError extends KaanaError {
45
+ }
46
+ /** `insufficient_balance`, `spending_limit_exceeded`, `quota_exceeded`. */
47
+ export class KaanaBillingError extends KaanaError {
48
+ }
49
+ /** `rate_limited`. */
50
+ export class KaanaRateLimitError extends KaanaError {
51
+ }
52
+ /** No route could serve the request right now, or the upstream failed. */
53
+ export class KaanaUnavailableError extends KaanaError {
54
+ }
55
+ /** `upstream_content_filtered`. */
56
+ export class KaanaContentFilteredError extends KaanaError {
57
+ }
58
+ /** `cancelled`, including a request the caller aborted. */
59
+ export class KaanaCancelledError extends KaanaError {
60
+ }
61
+ /** `internal_error`. */
62
+ export class KaanaInternalError extends KaanaError {
63
+ }
64
+ /** A response that does not satisfy `@kaana.ai/contracts`. Never retryable. */
65
+ export class KaanaProtocolError extends Error {
66
+ requestId;
67
+ constructor(message, requestId) {
68
+ super(message);
69
+ this.name = 'KaanaProtocolError';
70
+ this.requestId = requestId;
71
+ }
72
+ }
73
+ /** The network failed, or the request timed out, before a response arrived. */
74
+ export class KaanaConnectionError extends Error {
75
+ cause;
76
+ timedOut;
77
+ constructor(message, options = {}) {
78
+ super(message);
79
+ this.name = 'KaanaConnectionError';
80
+ this.cause = options.cause;
81
+ this.timedOut = options.timedOut ?? false;
82
+ }
83
+ }
84
+ const CLASS_BY_CODE = {
85
+ authentication_failed: KaanaAuthenticationError,
86
+ permission_denied: KaanaPermissionError,
87
+ insufficient_scope: KaanaPermissionError,
88
+ commercial_permission_denied: KaanaPermissionError,
89
+ policy_violation: KaanaPermissionError,
90
+ invalid_request: KaanaInvalidRequestError,
91
+ model_not_found: KaanaInvalidRequestError,
92
+ unsupported_modality: KaanaInvalidRequestError,
93
+ context_length_exceeded: KaanaInvalidRequestError,
94
+ request_too_large: KaanaInvalidRequestError,
95
+ output_limit_exceeded: KaanaInvalidRequestError,
96
+ idempotency_conflict: KaanaInvalidRequestError,
97
+ byok_credential_invalid: KaanaInvalidRequestError,
98
+ insufficient_balance: KaanaBillingError,
99
+ spending_limit_exceeded: KaanaBillingError,
100
+ quota_exceeded: KaanaBillingError,
101
+ rate_limited: KaanaRateLimitError,
102
+ no_route_available: KaanaUnavailableError,
103
+ service_unavailable: KaanaUnavailableError,
104
+ provider_error: KaanaUnavailableError,
105
+ provider_timeout: KaanaUnavailableError,
106
+ provider_overloaded: KaanaUnavailableError,
107
+ upstream_content_filtered: KaanaContentFilteredError,
108
+ cancelled: KaanaCancelledError,
109
+ internal_error: KaanaInternalError,
110
+ };
111
+ /** The typed error for a public code. */
112
+ export function kaanaError(init) {
113
+ const ErrorType = CLASS_BY_CODE[init.code] ?? KaanaError;
114
+ return new ErrorType(init);
115
+ }
116
+ /** The typed error for a `PublicError` body or terminal event. */
117
+ export function fromPublicError(error, status, afterOutput = false) {
118
+ return kaanaError({
119
+ code: error.code,
120
+ message: error.message,
121
+ status,
122
+ requestId: error.requestId,
123
+ retryable: error.retryable,
124
+ ...(error.retryAfterMs === undefined ? {} : { retryAfterMs: error.retryAfterMs }),
125
+ ...(error.param === undefined ? {} : { param: error.param }),
126
+ afterOutput,
127
+ });
128
+ }
129
+ /** A request the SDK refuses before sending anything. */
130
+ export function clientRefusal(param, message) {
131
+ return new KaanaInvalidRequestError({
132
+ code: 'invalid_request',
133
+ message,
134
+ status: null,
135
+ requestId: null,
136
+ retryable: false,
137
+ param,
138
+ });
139
+ }
140
+ /** The caller withdrew the request. */
141
+ export function cancelledByCaller(requestId, afterOutput = false) {
142
+ return new KaanaCancelledError({
143
+ code: 'cancelled',
144
+ message: 'the request was aborted by the caller',
145
+ status: null,
146
+ requestId,
147
+ retryable: false,
148
+ afterOutput,
149
+ });
150
+ }
@@ -0,0 +1,11 @@
1
+ /** Reading a `text/event-stream` response into validated frames. */
2
+ import { KaanaConnectionError } from './errors.js';
3
+ import { type Sent } from './http.js';
4
+ import { type SseFrame } from './sse.js';
5
+ /** The response body as a stream, or a clear failure where the runtime has none. */
6
+ export declare function streamingBody(sent: Sent): ReadableStream<Uint8Array>;
7
+ export declare function readFrames(sent: Sent, output: () => boolean): AsyncGenerator<SseFrame>;
8
+ /** Parse one frame's JSON. */
9
+ export declare function frameJson(frame: SseFrame, requestId: string): unknown;
10
+ /** A stream that ended without its terminal event was cut off. */
11
+ export declare function cutOff(requestId: string): KaanaConnectionError;
package/dist/events.js ADDED
@@ -0,0 +1,51 @@
1
+ /** Reading a `text/event-stream` response into validated frames. */
2
+ import { cancelledByCaller, KaanaConnectionError, KaanaProtocolError } from './errors.js';
3
+ import { bodyReadFailure, mediaTypeOf } from './http.js';
4
+ import { decodeEventStream } from './sse.js';
5
+ /** The response body as a stream, or a clear failure where the runtime has none. */
6
+ export function streamingBody(sent) {
7
+ const body = sent.response.body;
8
+ if (body === null || typeof body.getReader !== 'function') {
9
+ sent.abort();
10
+ sent.release();
11
+ throw new KaanaConnectionError("This runtime's fetch exposes no streaming response body, which streaming methods need. " +
12
+ 'In React Native, install a fetch that streams (for example a streaming fetch polyfill); ' +
13
+ 'the non-streaming methods work without one.');
14
+ }
15
+ return body;
16
+ }
17
+ export async function* readFrames(sent, output) {
18
+ if (mediaTypeOf(sent.response) !== 'text/event-stream') {
19
+ sent.abort();
20
+ sent.release();
21
+ throw new KaanaProtocolError(`A streaming request was answered with ${mediaTypeOf(sent.response) ?? 'no Content-Type'} instead of text/event-stream.`, sent.requestId);
22
+ }
23
+ const body = streamingBody(sent);
24
+ try {
25
+ for await (const frame of decodeEventStream(body, sent.requestId))
26
+ yield frame;
27
+ }
28
+ catch (error) {
29
+ if (error instanceof KaanaProtocolError)
30
+ throw error;
31
+ if (sent.aborted())
32
+ throw cancelledByCaller(sent.requestId, output());
33
+ throw bodyReadFailure(sent, error);
34
+ }
35
+ finally {
36
+ sent.release();
37
+ }
38
+ }
39
+ /** Parse one frame's JSON. */
40
+ export function frameJson(frame, requestId) {
41
+ try {
42
+ return JSON.parse(frame.data);
43
+ }
44
+ catch (_error) {
45
+ throw new KaanaProtocolError(`The stream sent a ${frame.event || 'unnamed'} frame that is not JSON.`, requestId);
46
+ }
47
+ }
48
+ /** A stream that ended without its terminal event was cut off. */
49
+ export function cutOff(requestId) {
50
+ return new KaanaConnectionError(`The stream of request ${requestId} ended before its terminal event.`);
51
+ }