@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/LICENSE +21 -0
- package/README.md +74 -0
- package/dist/index.cjs +861 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +1254 -0
- package/dist/index.d.ts +1254 -0
- package/dist/index.js +846 -0
- package/dist/index.js.map +1 -0
- package/package.json +58 -0
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
|