@skailar-ai/sdk 0.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/dist/index.cjs ADDED
@@ -0,0 +1,861 @@
1
+ 'use strict';
2
+
3
+ Object.defineProperty(exports, '__esModule', { value: true });
4
+
5
+ // src/errors.ts
6
+ function parseErrorBody(body) {
7
+ if (typeof body !== "object" || body === null) {
8
+ return { code: void 0, message: void 0 };
9
+ }
10
+ const root = body;
11
+ const err = root["error"];
12
+ if (typeof err === "string") {
13
+ const message = typeof root["message"] === "string" ? root["message"] : err;
14
+ return { code: err, message };
15
+ }
16
+ if (typeof err === "object" && err !== null) {
17
+ const nested = err;
18
+ const code = typeof nested["type"] === "string" ? nested["type"] : typeof nested["code"] === "string" ? nested["code"] : void 0;
19
+ const message = typeof nested["message"] === "string" ? nested["message"] : void 0;
20
+ return { code, message };
21
+ }
22
+ const topMessage = typeof root["message"] === "string" ? root["message"] : void 0;
23
+ return { code: void 0, message: topMessage };
24
+ }
25
+ var SkailarError = class extends Error {
26
+ /** HTTP status code, or `null` for non-HTTP failures (e.g. network). */
27
+ status;
28
+ /** Machine-readable error code from the body, if any. */
29
+ code;
30
+ /** Correlation id from the `x-request-id` response header, if any. */
31
+ requestId;
32
+ /** The raw response body captured for debugging. */
33
+ raw;
34
+ /**
35
+ * @param status - HTTP status, or `null` when not applicable.
36
+ * @param options - Message, code, request id, raw body and cause.
37
+ */
38
+ constructor(status, options = {}) {
39
+ super(options.message ?? "Skailar SDK error", { cause: options.cause });
40
+ this.name = new.target.name;
41
+ this.status = status;
42
+ this.code = options.code;
43
+ this.requestId = options.requestId;
44
+ this.raw = options.raw;
45
+ Object.setPrototypeOf(this, new.target.prototype);
46
+ }
47
+ };
48
+ var SkailarConnectionError = class extends SkailarError {
49
+ /** @param options - Message and originating cause. */
50
+ constructor(options = {}) {
51
+ super(null, {
52
+ message: options.message ?? "Failed to connect to the Skailar API",
53
+ cause: options.cause
54
+ });
55
+ }
56
+ };
57
+ var SkailarAPIError = class _SkailarAPIError extends SkailarError {
58
+ /**
59
+ * @param status - The HTTP status code of the response.
60
+ * @param options - Message, code, request id and raw body.
61
+ */
62
+ constructor(status, options = {}) {
63
+ super(status, options);
64
+ }
65
+ /**
66
+ * Build the most specific {@link SkailarAPIError} subclass for a status code.
67
+ *
68
+ * @param status - The HTTP status code returned by the gateway.
69
+ * @param parsed - The normalized `{ code, message }` from {@link parseErrorBody}.
70
+ * @param requestId - The `x-request-id` header value, if present.
71
+ * @param raw - The raw response body for diagnostics.
72
+ * @param retryAfter - Seconds from a `Retry-After` header, for 429 responses.
73
+ * @returns A {@link SkailarAuthError}, {@link SkailarBadRequestError},
74
+ * {@link SkailarNotFoundError}, {@link SkailarRateLimitError},
75
+ * {@link SkailarUpstreamError}, or a plain {@link SkailarAPIError}.
76
+ */
77
+ static from(status, parsed, requestId, raw, retryAfter) {
78
+ const options = {
79
+ message: parsed.message,
80
+ code: parsed.code,
81
+ requestId,
82
+ raw
83
+ };
84
+ switch (status) {
85
+ case 401:
86
+ return new SkailarAuthError(options);
87
+ case 400:
88
+ return new SkailarBadRequestError(options);
89
+ case 404:
90
+ return new SkailarNotFoundError(options);
91
+ case 429:
92
+ return new SkailarRateLimitError({ ...options, retryAfter });
93
+ default:
94
+ if (status >= 500) return new SkailarUpstreamError(status, options);
95
+ return new _SkailarAPIError(status, options);
96
+ }
97
+ }
98
+ };
99
+ var SkailarAuthError = class extends SkailarAPIError {
100
+ /** @param options - Error details. */
101
+ constructor(options = {}) {
102
+ super(401, { message: "Invalid or missing API key", ...options });
103
+ }
104
+ };
105
+ var SkailarBadRequestError = class extends SkailarAPIError {
106
+ /** @param options - Error details. */
107
+ constructor(options = {}) {
108
+ super(400, { message: "Bad request", ...options });
109
+ }
110
+ };
111
+ var SkailarNotFoundError = class extends SkailarAPIError {
112
+ /** @param options - Error details. */
113
+ constructor(options = {}) {
114
+ super(404, { message: "Resource not found", ...options });
115
+ }
116
+ };
117
+ var SkailarRateLimitError = class extends SkailarAPIError {
118
+ /** Seconds to wait before retrying, parsed from `Retry-After`; `undefined` if absent/unparseable. */
119
+ retryAfter;
120
+ /** @param options - Error details plus the optional `retryAfter` seconds. */
121
+ constructor(options = {}) {
122
+ super(429, { message: "Rate limit exceeded", ...options });
123
+ this.retryAfter = options.retryAfter;
124
+ }
125
+ };
126
+ var SkailarUpstreamError = class extends SkailarAPIError {
127
+ /**
128
+ * @param status - The specific 5xx status code.
129
+ * @param options - Error details.
130
+ */
131
+ constructor(status, options = {}) {
132
+ super(status, { message: "Upstream provider error", ...options });
133
+ }
134
+ };
135
+
136
+ // src/streaming.ts
137
+ async function* parseSSE(stream, signal) {
138
+ const reader = stream.getReader();
139
+ const decoder = new TextDecoder();
140
+ let buffer = "";
141
+ try {
142
+ while (true) {
143
+ if (signal?.aborted) {
144
+ throw new SkailarConnectionError({ message: "Stream aborted", cause: signal.reason });
145
+ }
146
+ const { done, value } = await reader.read();
147
+ if (done) break;
148
+ buffer += decoder.decode(value, { stream: true });
149
+ let newlineIndex;
150
+ while ((newlineIndex = buffer.indexOf("\n")) !== -1) {
151
+ const rawLine = buffer.slice(0, newlineIndex);
152
+ buffer = buffer.slice(newlineIndex + 1);
153
+ const line = rawLine.endsWith("\r") ? rawLine.slice(0, -1) : rawLine;
154
+ if (line === "" || line.startsWith(":")) continue;
155
+ if (!line.startsWith("data:")) continue;
156
+ const data = line.slice(5).trimStart();
157
+ if (data === "[DONE]") return;
158
+ yield data;
159
+ }
160
+ }
161
+ const tail = buffer.trim();
162
+ if (tail.startsWith("data:")) {
163
+ const data = tail.slice(5).trimStart();
164
+ if (data !== "[DONE]" && data !== "") yield data;
165
+ }
166
+ } catch (err) {
167
+ if (err instanceof SkailarConnectionError) throw err;
168
+ throw new SkailarConnectionError({ message: "Stream read failed", cause: err });
169
+ } finally {
170
+ await reader.cancel().catch(() => {
171
+ });
172
+ reader.releaseLock();
173
+ }
174
+ }
175
+ var ChatCompletionStream = class {
176
+ /**
177
+ * The {@link AbortController} governing the underlying HTTP request. Call
178
+ * `stream.controller.abort()` to cancel an in-flight stream; the active
179
+ * `for await` loop then terminates promptly.
180
+ */
181
+ controller;
182
+ /** The raw SSE byte stream backing this iterator. */
183
+ body;
184
+ /**
185
+ * @param body - The response body stream of SSE bytes.
186
+ * @param controller - The abort controller tied to the originating request.
187
+ */
188
+ constructor(body, controller) {
189
+ this.body = body;
190
+ this.controller = controller;
191
+ }
192
+ /**
193
+ * Decode the SSE byte stream into typed chunks.
194
+ *
195
+ * Each `data:` payload is `JSON.parse`d. If a payload carries an `error` field
196
+ * (the gateway's in-band failure signal), iteration throws the corresponding
197
+ * {@link SkailarAPIError} instead of yielding. Malformed JSON payloads are
198
+ * skipped defensively.
199
+ *
200
+ * @returns An async generator over {@link ChatCompletionChunk} values.
201
+ * @throws {@link SkailarAPIError} When the stream delivers an in-band error event.
202
+ * @throws {@link SkailarConnectionError} When the stream is aborted or read fails.
203
+ */
204
+ async *decode() {
205
+ for await (const data of parseSSE(this.body, this.controller.signal)) {
206
+ let parsed;
207
+ try {
208
+ parsed = JSON.parse(data);
209
+ } catch {
210
+ continue;
211
+ }
212
+ if (parsed !== null && typeof parsed === "object" && "error" in parsed) {
213
+ const { code, message } = parseErrorBody(parsed);
214
+ throw SkailarAPIError.from(
215
+ 500,
216
+ { code, message: message ?? "Streaming error" },
217
+ void 0,
218
+ parsed
219
+ );
220
+ }
221
+ yield parsed;
222
+ }
223
+ }
224
+ /**
225
+ * Iterate the decoded chunks. Abandoning the iteration early (a `break` or
226
+ * `return` inside a `for await`) invokes the returned iterator's `return()`,
227
+ * which aborts {@link ChatCompletionStream.controller} so the underlying fetch
228
+ * request is cancelled — not merely the body reader — and then runs the
229
+ * generator's own cleanup ({@link parseSSE}'s `finally`: reader cancel +
230
+ * lock release). Normal completion and thrown errors are unaffected.
231
+ *
232
+ * @returns An async iterator over {@link ChatCompletionChunk} values.
233
+ */
234
+ [Symbol.asyncIterator]() {
235
+ const inner = this.decode();
236
+ const controller = this.controller;
237
+ return {
238
+ next: () => inner.next(),
239
+ async return(value) {
240
+ controller.abort();
241
+ if (inner.return) await inner.return(value);
242
+ return { done: true, value: void 0 };
243
+ },
244
+ throw: (err) => inner.throw ? inner.throw(err) : Promise.reject(err)
245
+ };
246
+ }
247
+ };
248
+
249
+ // src/resources/chat.ts
250
+ var ChatCompletions = class {
251
+ /** The owning client used to dispatch requests. */
252
+ client;
253
+ /** @param client - The owning {@link Skailar} client. */
254
+ constructor(client) {
255
+ this.client = client;
256
+ }
257
+ /**
258
+ * Implementation backing the public overloads.
259
+ *
260
+ * @param body - The chat completion request.
261
+ * @param options - Optional per-call signal, timeout and headers.
262
+ * @returns Either a {@link ChatCompletion} or a {@link ChatCompletionStream}
263
+ * depending on `body.stream`.
264
+ * @throws {@link SkailarBadRequestError} On HTTP 400 (malformed request).
265
+ * @throws {@link SkailarAuthError} On HTTP 401 (bad key).
266
+ * @throws {@link SkailarRateLimitError} On HTTP 429 (after exhausting retries).
267
+ * @throws {@link SkailarUpstreamError} On HTTP 5xx (after exhausting retries).
268
+ * @throws {@link SkailarConnectionError} On network failure, timeout or abort.
269
+ */
270
+ create(body, options) {
271
+ if (body.stream === true) {
272
+ return this.client.request({
273
+ method: "POST",
274
+ path: "/v1/chat/completions",
275
+ body,
276
+ expect: "stream",
277
+ signal: options?.signal,
278
+ timeout: options?.timeout,
279
+ headers: options?.headers
280
+ });
281
+ }
282
+ return this.client.request({
283
+ method: "POST",
284
+ path: "/v1/chat/completions",
285
+ body,
286
+ expect: "json",
287
+ signal: options?.signal,
288
+ timeout: options?.timeout,
289
+ headers: options?.headers
290
+ });
291
+ }
292
+ };
293
+ var ChatResource = class {
294
+ /** The chat completions namespace. */
295
+ completions;
296
+ /** @param client - The owning {@link Skailar} client. */
297
+ constructor(client) {
298
+ this.completions = new ChatCompletions(client);
299
+ }
300
+ };
301
+
302
+ // src/resources/models.ts
303
+ var ModelsResource = class {
304
+ /** The owning client used to dispatch requests. */
305
+ client;
306
+ /** @param client - The owning {@link Skailar} client. */
307
+ constructor(client) {
308
+ this.client = client;
309
+ }
310
+ /**
311
+ * List every model the gateway can route to. Unwraps the
312
+ * `{ object: "list", data }` envelope and returns just `data` as a plain array.
313
+ *
314
+ * @param options - Optional per-call signal, timeout and headers.
315
+ * @returns A promise resolving to the array of {@link ModelSummary} cards.
316
+ */
317
+ async list(options) {
318
+ const res = await this.client.request({
319
+ method: "GET",
320
+ path: "/v1/models",
321
+ expect: "json",
322
+ signal: options?.signal,
323
+ timeout: options?.timeout,
324
+ headers: options?.headers
325
+ });
326
+ return res.data;
327
+ }
328
+ /**
329
+ * Retrieve the full detail card for a single model.
330
+ *
331
+ * @param id - The model identifier; may contain slashes (e.g.
332
+ * `"google/gemini-2.5-pro"`), which are preserved in the path.
333
+ * @param options - Optional per-call signal, timeout and headers.
334
+ * @returns A promise resolving to the {@link Model} detail.
335
+ * @throws {@link SkailarNotFoundError} If no model matches the id.
336
+ */
337
+ retrieve(id, options) {
338
+ const encoded = id.split("/").map(encodeURIComponent).join("/");
339
+ return this.client.request({
340
+ method: "GET",
341
+ path: `/v1/models/${encoded}`,
342
+ expect: "json",
343
+ signal: options?.signal,
344
+ timeout: options?.timeout,
345
+ headers: options?.headers
346
+ });
347
+ }
348
+ };
349
+
350
+ // src/resources/images.ts
351
+ var ImagesResource = class {
352
+ /** The owning client used to dispatch requests. */
353
+ client;
354
+ /** @param client - The owning {@link Skailar} client. */
355
+ constructor(client) {
356
+ this.client = client;
357
+ }
358
+ /**
359
+ * Generate one or more images from a text prompt. Named `generate` to match
360
+ * `openai`'s `images.generate(...)`.
361
+ *
362
+ * @param body - The generation request; see {@link ImageGenerationRequest}.
363
+ * @param options - Optional per-call signal, timeout and headers.
364
+ * @returns A promise resolving to the {@link ImageGenerationResponse}, whose
365
+ * `data` entries carry either a `url` or inline `b64_json`.
366
+ * @throws {@link SkailarBadRequestError} On HTTP 400.
367
+ * @throws {@link SkailarRateLimitError} On HTTP 429 (after exhausting retries).
368
+ */
369
+ generate(body, options) {
370
+ return this.client.request({
371
+ method: "POST",
372
+ path: "/v1/images/generations",
373
+ body,
374
+ expect: "json",
375
+ signal: options?.signal,
376
+ timeout: options?.timeout,
377
+ headers: options?.headers
378
+ });
379
+ }
380
+ };
381
+
382
+ // src/internal/binary.ts
383
+ function bytesToBase64(bytes) {
384
+ const maybeBuffer = globalThis.Buffer;
385
+ if (maybeBuffer) {
386
+ return maybeBuffer.from(bytes).toString("base64");
387
+ }
388
+ let binary = "";
389
+ const chunkSize = 32768;
390
+ for (let i = 0; i < bytes.length; i += chunkSize) {
391
+ const chunk = bytes.subarray(i, i + chunkSize);
392
+ binary += String.fromCharCode(...chunk);
393
+ }
394
+ return btoa(binary);
395
+ }
396
+ async function toBase64(input) {
397
+ if (typeof input === "string") return input;
398
+ if (input instanceof Uint8Array) return bytesToBase64(input);
399
+ if (input instanceof ArrayBuffer) return bytesToBase64(new Uint8Array(input));
400
+ if (typeof Blob !== "undefined" && input instanceof Blob) {
401
+ const buffer = await input.arrayBuffer();
402
+ return bytesToBase64(new Uint8Array(buffer));
403
+ }
404
+ throw new TypeError("Unsupported binary input; expected Uint8Array, ArrayBuffer, Blob or base64 string");
405
+ }
406
+
407
+ // src/resources/audio.ts
408
+ var AudioTranscriptions = class {
409
+ /** The owning client used to dispatch requests. */
410
+ client;
411
+ /** @param client - The owning {@link Skailar} client. */
412
+ constructor(client) {
413
+ this.client = client;
414
+ }
415
+ /**
416
+ * Transcribe an audio clip to text. The supplied bytes are base64-encoded
417
+ * client-side into the gateway's `base64` field. When `mime` is omitted and
418
+ * `file` is a {@link Blob} carrying a `type`, that type is used; otherwise the
419
+ * gateway default (`audio/wav`) applies.
420
+ *
421
+ * @param params - The audio and its MIME type; see {@link TranscriptionCreateParams}.
422
+ * `file` may be a {@link Uint8Array}, {@link ArrayBuffer}, {@link Blob} or a
423
+ * pre-encoded base64 string.
424
+ * @param options - Optional per-call signal, timeout and headers.
425
+ * @returns A promise resolving to the {@link TranscriptionResponse}.
426
+ */
427
+ async create(params, options) {
428
+ const base64 = await toBase64(params.file);
429
+ const mime = params.mime ?? (typeof Blob !== "undefined" && params.file instanceof Blob && params.file.type ? params.file.type : void 0);
430
+ return this.client.request({
431
+ method: "POST",
432
+ path: "/v1/audio/transcriptions",
433
+ body: { base64, mime },
434
+ expect: "json",
435
+ signal: options?.signal,
436
+ timeout: options?.timeout,
437
+ headers: options?.headers
438
+ });
439
+ }
440
+ };
441
+ var AudioSpeech = class {
442
+ /** The owning client used to dispatch requests. */
443
+ client;
444
+ /** @param client - The owning {@link Skailar} client. */
445
+ constructor(client) {
446
+ this.client = client;
447
+ }
448
+ /**
449
+ * Synthesize speech and return the raw MP3 audio stream. Unlike the JSON
450
+ * endpoints, this returns the response body stream directly so large audio
451
+ * payloads need not be buffered in memory.
452
+ *
453
+ * Pass `options.signal` to cancel the request: aborting it before the response
454
+ * arrives rejects this call, and aborting it while the MP3 is still downloading
455
+ * tears down the underlying connection so the body stops mid-stream.
456
+ *
457
+ * @param params - The text and voice; see {@link SpeechCreateParams}.
458
+ * @param options - Optional per-call signal, timeout and headers.
459
+ * @returns A promise resolving to a `ReadableStream<Uint8Array>` of
460
+ * `audio/mpeg` bytes, suitable for piping to a file, an HTTP response, or an
461
+ * audio element.
462
+ * @throws {@link SkailarConnectionError} If the response unexpectedly lacks a body.
463
+ * @throws {@link SkailarBadRequestError} On HTTP 400 (e.g. text exceeding 4000 chars).
464
+ */
465
+ async create(params, options) {
466
+ const response = await this.client.request({
467
+ method: "POST",
468
+ path: "/v1/audio/speech",
469
+ body: { input: params.input, voice: params.voice },
470
+ headers: { Accept: "audio/mpeg", ...options?.headers },
471
+ expect: "response",
472
+ signal: options?.signal,
473
+ timeout: options?.timeout
474
+ });
475
+ if (!response.body) {
476
+ throw new SkailarConnectionError({ message: "Speech response had no audio body" });
477
+ }
478
+ return response.body;
479
+ }
480
+ };
481
+ var AudioResource = class {
482
+ /** Speech-to-text operations. */
483
+ transcriptions;
484
+ /** Text-to-speech operations. */
485
+ speech;
486
+ /** @param client - The owning {@link Skailar} client. */
487
+ constructor(client) {
488
+ this.transcriptions = new AudioTranscriptions(client);
489
+ this.speech = new AudioSpeech(client);
490
+ }
491
+ };
492
+
493
+ // src/resources/uploads.ts
494
+ var ImageUploads = class {
495
+ /** The owning client used to dispatch requests. */
496
+ client;
497
+ /** @param client - The owning {@link Skailar} client. */
498
+ constructor(client) {
499
+ this.client = client;
500
+ }
501
+ /**
502
+ * Upload an image and obtain a URL usable as vision input. `data` may be a
503
+ * {@link Uint8Array}, {@link ArrayBuffer}, {@link Blob} or a pre-encoded base64
504
+ * string; it is base64-encoded client-side into the gateway's `base64` field.
505
+ *
506
+ * @param params - The image bytes and content type; see {@link ImageUploadCreateParams}.
507
+ * @returns A promise resolving to the {@link UploadResponse} whose `url` can be
508
+ * embedded in a chat completion as an `image_url` content part.
509
+ */
510
+ async create(params) {
511
+ const base64 = await toBase64(params.data);
512
+ return this.client.request({
513
+ method: "POST",
514
+ path: "/v1/uploads/images",
515
+ body: { base64, content_type: params.contentType },
516
+ expect: "json"
517
+ });
518
+ }
519
+ };
520
+ var FileUploads = class {
521
+ /** The owning client used to dispatch requests. */
522
+ client;
523
+ /** @param client - The owning {@link Skailar} client. */
524
+ constructor(client) {
525
+ this.client = client;
526
+ }
527
+ /**
528
+ * Upload a document (`application/pdf` or `text/plain`). `data` accepts the
529
+ * same forms as image upload and is base64-encoded client-side.
530
+ *
531
+ * @param params - The document bytes and content type; see {@link FileUploadCreateParams}.
532
+ * @returns A promise resolving to the {@link UploadResponse} with the stored asset URL.
533
+ */
534
+ async create(params) {
535
+ const base64 = await toBase64(params.data);
536
+ return this.client.request({
537
+ method: "POST",
538
+ path: "/v1/uploads/files",
539
+ body: { base64, content_type: params.contentType },
540
+ expect: "json"
541
+ });
542
+ }
543
+ };
544
+ var UploadsResource = class {
545
+ /** Image upload operations. */
546
+ images;
547
+ /** File/document upload operations. */
548
+ files;
549
+ /** @param client - The owning {@link Skailar} client. */
550
+ constructor(client) {
551
+ this.images = new ImageUploads(client);
552
+ this.files = new FileUploads(client);
553
+ }
554
+ };
555
+
556
+ // src/client.ts
557
+ function delay(ms, signal) {
558
+ return new Promise((resolve, reject) => {
559
+ if (signal?.aborted) {
560
+ reject(new SkailarConnectionError({ message: "Aborted", cause: signal.reason }));
561
+ return;
562
+ }
563
+ const timer = setTimeout(() => {
564
+ signal?.removeEventListener("abort", onAbort);
565
+ resolve();
566
+ }, ms);
567
+ const onAbort = () => {
568
+ clearTimeout(timer);
569
+ reject(new SkailarConnectionError({ message: "Aborted", cause: signal?.reason }));
570
+ };
571
+ signal?.addEventListener("abort", onAbort, { once: true });
572
+ });
573
+ }
574
+ function parseRetryAfter(header) {
575
+ if (!header) return void 0;
576
+ const seconds = Number(header);
577
+ if (Number.isFinite(seconds)) return Math.max(0, seconds);
578
+ const date = Date.parse(header);
579
+ if (Number.isFinite(date)) return Math.max(0, Math.ceil((date - Date.now()) / 1e3));
580
+ return void 0;
581
+ }
582
+ function withCleanup(source, onDone) {
583
+ let done = false;
584
+ const finish = () => {
585
+ if (done) return;
586
+ done = true;
587
+ onDone();
588
+ };
589
+ const reader = source.getReader();
590
+ return new ReadableStream({
591
+ async pull(controller) {
592
+ try {
593
+ const { done: streamDone, value } = await reader.read();
594
+ if (streamDone) {
595
+ finish();
596
+ controller.close();
597
+ return;
598
+ }
599
+ controller.enqueue(value);
600
+ } catch (err) {
601
+ finish();
602
+ controller.error(err);
603
+ }
604
+ },
605
+ async cancel(reason) {
606
+ finish();
607
+ await reader.cancel(reason);
608
+ }
609
+ });
610
+ }
611
+ function extractRequestId(headers) {
612
+ return headers.get("x-request-id") ?? headers.get("x-skailar-request-id") ?? headers.get("request-id") ?? void 0;
613
+ }
614
+ var Skailar = class {
615
+ /** Resolved API key sent as the bearer token. */
616
+ apiKey;
617
+ /** Resolved base URL with any trailing slash removed. */
618
+ baseURL;
619
+ /** Resolved per-attempt timeout in milliseconds. */
620
+ timeout;
621
+ /** Resolved maximum retry count. */
622
+ maxRetries;
623
+ /** Default headers merged into every request. */
624
+ defaultHeaders;
625
+ /** The `fetch` implementation used for all requests. */
626
+ fetchImpl;
627
+ /** Chat completions (OpenAI-compatible). */
628
+ chat;
629
+ /** Model discovery. */
630
+ models;
631
+ /** Image generation. */
632
+ images;
633
+ /** Speech synthesis and transcription. */
634
+ audio;
635
+ /** Direct uploads to Skailar storage. */
636
+ uploads;
637
+ /**
638
+ * @param options - Client configuration; see {@link SkailarOptions}.
639
+ * @throws If no API key is resolvable (neither `options.apiKey` nor
640
+ * `SKAILAR_API_KEY`). A key that is present but malformed is **not** rejected
641
+ * here; it fails at the first request with a {@link SkailarAuthError}.
642
+ */
643
+ constructor(options = {}) {
644
+ const env = typeof process !== "undefined" && process.env ? process.env["SKAILAR_API_KEY"] : void 0;
645
+ const apiKey = options.apiKey ?? env;
646
+ if (!apiKey) {
647
+ throw new Error(
648
+ "Missing Skailar API key. Pass { apiKey } or set the SKAILAR_API_KEY environment variable."
649
+ );
650
+ }
651
+ this.apiKey = apiKey;
652
+ this.baseURL = (options.baseURL ?? "https://api.skailar.com").replace(/\/+$/, "");
653
+ this.timeout = options.timeout ?? 6e4;
654
+ this.maxRetries = options.maxRetries ?? 2;
655
+ this.defaultHeaders = options.defaultHeaders ?? {};
656
+ this.fetchImpl = options.fetch ?? globalThis.fetch;
657
+ if (typeof this.fetchImpl !== "function") {
658
+ throw new Error("No fetch implementation available; pass { fetch } explicitly.");
659
+ }
660
+ this.chat = new ChatResource(this);
661
+ this.models = new ModelsResource(this);
662
+ this.images = new ImagesResource(this);
663
+ this.audio = new AudioResource(this);
664
+ this.uploads = new UploadsResource(this);
665
+ }
666
+ /**
667
+ * Verify the configured API key against `GET /v1/ping-key`.
668
+ *
669
+ * @param options - Optional per-call signal, timeout and headers.
670
+ * @returns The `{ status, user_id }` payload when the key is valid.
671
+ * @throws {@link SkailarAuthError} If the key is missing, invalid or revoked.
672
+ */
673
+ ping(options) {
674
+ return this.request({
675
+ method: "GET",
676
+ path: "/v1/ping-key",
677
+ expect: "json",
678
+ signal: options?.signal,
679
+ timeout: options?.timeout,
680
+ headers: options?.headers
681
+ });
682
+ }
683
+ /**
684
+ * Core dispatch implementation shared by all resources.
685
+ *
686
+ * Applies, per attempt: header assembly with bearer auth, a timeout-derived
687
+ * {@link AbortSignal} composed with any caller signal, execution of
688
+ * {@link SkailarOptions.fetch}, and error mapping. Retries HTTP 429, HTTP 5xx
689
+ * and transient connection failures up to {@link Skailar.maxRetries}, backing
690
+ * off with full-jitter exponential delay and honoring a server `Retry-After`
691
+ * when present. Non-429 4xx responses fail fast.
692
+ *
693
+ * Transport failures are reported as {@link SkailarConnectionError} with a
694
+ * message distinguishing three causes: an external `signal` abort
695
+ * (non-retryable), an internal timeout once {@link Skailar.timeout} elapses
696
+ * (retryable), and a generic network failure (retryable).
697
+ *
698
+ * @param options - The request description.
699
+ * @returns The parsed JSON, raw `Response`, or {@link ChatCompletionStream}
700
+ * depending on `options.expect`.
701
+ */
702
+ async request(options) {
703
+ const url = `${this.baseURL}${options.path}`;
704
+ const isStream = options.expect === "stream";
705
+ const timeoutMs = options.timeout ?? this.timeout;
706
+ let attempt = 0;
707
+ while (true) {
708
+ const controller = new AbortController();
709
+ const onExternalAbort = () => controller.abort(options.signal?.reason);
710
+ if (options.signal) {
711
+ if (options.signal.aborted) controller.abort(options.signal.reason);
712
+ else options.signal.addEventListener("abort", onExternalAbort);
713
+ }
714
+ const detachExternal = () => options.signal?.removeEventListener("abort", onExternalAbort);
715
+ let timedOut = false;
716
+ const timer = setTimeout(() => {
717
+ timedOut = true;
718
+ controller.abort(new Error("Request timed out"));
719
+ }, timeoutMs);
720
+ let response;
721
+ try {
722
+ response = await this.fetchImpl(url, {
723
+ method: options.method,
724
+ headers: this.buildHeaders(options),
725
+ body: options.body === void 0 ? void 0 : JSON.stringify(options.body),
726
+ signal: controller.signal
727
+ });
728
+ } catch (err) {
729
+ clearTimeout(timer);
730
+ detachExternal();
731
+ const externallyAborted = options.signal?.aborted ?? false;
732
+ const connErr = new SkailarConnectionError({
733
+ message: externallyAborted ? "Request aborted" : timedOut ? `Request timed out after ${timeoutMs}ms` : "Network request to the Skailar API failed",
734
+ cause: err
735
+ });
736
+ if (externallyAborted || !this.shouldRetry(attempt)) throw connErr;
737
+ attempt += 1;
738
+ await delay(this.backoff(attempt), options.signal);
739
+ continue;
740
+ }
741
+ clearTimeout(timer);
742
+ if (!response.ok) {
743
+ detachExternal();
744
+ const apiError = await this.toApiError(response);
745
+ const retryAfterMs = apiError instanceof SkailarRateLimitError && apiError.retryAfter !== void 0 ? apiError.retryAfter * 1e3 : void 0;
746
+ if (this.isRetryableStatus(response.status) && this.shouldRetry(attempt)) {
747
+ attempt += 1;
748
+ await delay(retryAfterMs ?? this.backoff(attempt), options.signal);
749
+ continue;
750
+ }
751
+ throw apiError;
752
+ }
753
+ if (isStream || options.expect === "response") {
754
+ if (!response.body) {
755
+ detachExternal();
756
+ throw new SkailarConnectionError({
757
+ message: isStream ? "Streaming response had no body" : "Response had no body"
758
+ });
759
+ }
760
+ const body = withCleanup(response.body, detachExternal);
761
+ if (isStream) return new ChatCompletionStream(body, controller);
762
+ return new Response(body, {
763
+ status: response.status,
764
+ statusText: response.statusText,
765
+ headers: response.headers
766
+ });
767
+ }
768
+ detachExternal();
769
+ return await response.json();
770
+ }
771
+ }
772
+ /**
773
+ * Assemble the outgoing header set for a request, applying defaults, auth,
774
+ * content-type and accept in precedence order.
775
+ *
776
+ * @param options - The request description.
777
+ * @returns The header record to send.
778
+ */
779
+ buildHeaders(options) {
780
+ const headers = {
781
+ ...this.defaultHeaders,
782
+ Authorization: `Bearer ${this.apiKey}`,
783
+ Accept: options.expect === "stream" ? "text/event-stream" : "application/json"
784
+ };
785
+ if (options.body !== void 0) headers["Content-Type"] = "application/json";
786
+ if (options.headers) Object.assign(headers, options.headers);
787
+ return headers;
788
+ }
789
+ /**
790
+ * Convert a non-2xx {@link Response} into the most specific
791
+ * {@link SkailarAPIError} subclass. Reads the body once, attempting JSON first
792
+ * and falling back to raw text, then defers classification to
793
+ * {@link SkailarAPIError.from}.
794
+ *
795
+ * @param response - The failed HTTP response.
796
+ * @returns The mapped error.
797
+ */
798
+ async toApiError(response) {
799
+ const text = await response.text().catch(() => "");
800
+ let raw = text;
801
+ try {
802
+ raw = text ? JSON.parse(text) : void 0;
803
+ } catch {
804
+ raw = text;
805
+ }
806
+ const parsed = parseErrorBody(raw);
807
+ const requestId = extractRequestId(response.headers);
808
+ const retryAfter = response.status === 429 ? parseRetryAfter(response.headers.get("retry-after")) : void 0;
809
+ return SkailarAPIError.from(response.status, parsed, requestId, raw, retryAfter);
810
+ }
811
+ /**
812
+ * Whether a status code is eligible for automatic retry (429 and any 5xx).
813
+ *
814
+ * @param status - The HTTP status code.
815
+ * @returns `true` if retryable.
816
+ */
817
+ isRetryableStatus(status) {
818
+ return status === 429 || status >= 500;
819
+ }
820
+ /**
821
+ * Whether another attempt remains within the retry budget.
822
+ *
823
+ * @param attempt - The count of attempts already made (before increment).
824
+ * @returns `true` if a retry is permitted.
825
+ */
826
+ shouldRetry(attempt) {
827
+ return attempt < this.maxRetries;
828
+ }
829
+ /**
830
+ * Compute a full-jitter exponential backoff delay: a random value in
831
+ * `[0, min(cap, base * 2^attempt))`, capped at 8000ms. Jitter spreads retries
832
+ * from many clients to avoid synchronized thundering-herd load on the gateway.
833
+ *
834
+ * @param attempt - The retry number, starting at 1 for the first retry.
835
+ * @returns A randomized delay in milliseconds.
836
+ */
837
+ backoff(attempt) {
838
+ const base = 500;
839
+ const cap = 8e3;
840
+ const exponential = Math.min(cap, base * 2 ** attempt);
841
+ return Math.random() * exponential;
842
+ }
843
+ };
844
+
845
+ // src/index.ts
846
+ var index_default = Skailar;
847
+
848
+ exports.ChatCompletionStream = ChatCompletionStream;
849
+ exports.Skailar = Skailar;
850
+ exports.SkailarAPIError = SkailarAPIError;
851
+ exports.SkailarAuthError = SkailarAuthError;
852
+ exports.SkailarBadRequestError = SkailarBadRequestError;
853
+ exports.SkailarConnectionError = SkailarConnectionError;
854
+ exports.SkailarError = SkailarError;
855
+ exports.SkailarNotFoundError = SkailarNotFoundError;
856
+ exports.SkailarRateLimitError = SkailarRateLimitError;
857
+ exports.SkailarUpstreamError = SkailarUpstreamError;
858
+ exports.default = index_default;
859
+ exports.parseSSE = parseSSE;
860
+ //# sourceMappingURL=index.cjs.map
861
+ //# sourceMappingURL=index.cjs.map