@skailar-ai/sdk 0.0.2 → 0.0.4
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 +102 -28
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +68 -19
- package/dist/index.d.ts +68 -19
- package/dist/index.js +102 -28
- package/dist/index.js.map +1 -1
- package/package.json +4 -4
package/dist/index.d.ts
CHANGED
|
@@ -309,11 +309,11 @@ interface ChatCompletionChunk {
|
|
|
309
309
|
* arrival order, excluding the terminal `[DONE]` sentinel.
|
|
310
310
|
*
|
|
311
311
|
* Implements just the slice of the SSE grammar the gateway emits: UTF-8 `data:`
|
|
312
|
-
* lines
|
|
313
|
-
* literal `data: [DONE]`. A partial
|
|
314
|
-
* preserved and completed by the next
|
|
315
|
-
* non-`data:` fields are ignored. Yielding
|
|
316
|
-
* end-of-stream without `[DONE]` simply ends the generator.
|
|
312
|
+
* lines terminated by any of `\n`, `\r\n` or `\r` (per the SSE spec), events
|
|
313
|
+
* delimited by blank lines, terminated by a literal `data: [DONE]`. A partial
|
|
314
|
+
* line left in the buffer across reads is preserved and completed by the next
|
|
315
|
+
* chunk. Comment lines (`:` prefix) and non-`data:` fields are ignored. Yielding
|
|
316
|
+
* stops at `[DONE]`; reaching end-of-stream without `[DONE]` simply ends the generator.
|
|
317
317
|
*
|
|
318
318
|
* On any exit — normal completion, `[DONE]`, an error, or the consumer
|
|
319
319
|
* abandoning the `for await` early — the reader is cancelled before its lock is
|
|
@@ -365,12 +365,15 @@ declare class ChatCompletionStream implements AsyncIterable<ChatCompletionChunk>
|
|
|
365
365
|
*
|
|
366
366
|
* Each `data:` payload is `JSON.parse`d. If a payload carries an `error` field
|
|
367
367
|
* (the gateway's in-band failure signal), iteration throws the corresponding
|
|
368
|
-
* {@link SkailarAPIError} instead of yielding.
|
|
369
|
-
*
|
|
368
|
+
* {@link SkailarAPIError} instead of yielding. A payload that is not valid JSON
|
|
369
|
+
* (e.g. a plain-text error injected by an intermediary) throws a
|
|
370
|
+
* {@link SkailarConnectionError} rather than being dropped, so a malformed
|
|
371
|
+
* stream cannot truncate the response silently.
|
|
370
372
|
*
|
|
371
373
|
* @returns An async generator over {@link ChatCompletionChunk} values.
|
|
372
374
|
* @throws {@link SkailarAPIError} When the stream delivers an in-band error event.
|
|
373
|
-
* @throws {@link SkailarConnectionError} When
|
|
375
|
+
* @throws {@link SkailarConnectionError} When a payload is malformed, or the
|
|
376
|
+
* stream is aborted or fails to read.
|
|
374
377
|
*/
|
|
375
378
|
private decode;
|
|
376
379
|
/**
|
|
@@ -890,7 +893,11 @@ interface SkailarOptions {
|
|
|
890
893
|
* The format is **not** validated locally — only emptiness is checked. A
|
|
891
894
|
* malformed or wrong-provider key (e.g. an OpenAI `sk-...` key passed by
|
|
892
895
|
* mistake) is accepted here and rejected at the first request with a
|
|
893
|
-
* {@link SkailarAuthError} (HTTP 401).
|
|
896
|
+
* {@link SkailarAuthError} (HTTP 401). Note this means such a key is **sent to
|
|
897
|
+
* {@link SkailarOptions.baseURL}** before it is rejected; if you point
|
|
898
|
+
* `baseURL` at a third-party host, take care not to forward a credential meant
|
|
899
|
+
* for a different provider. Keep the default `baseURL` unless you control the
|
|
900
|
+
* endpoint.
|
|
894
901
|
*/
|
|
895
902
|
apiKey?: string;
|
|
896
903
|
/**
|
|
@@ -900,9 +907,18 @@ interface SkailarOptions {
|
|
|
900
907
|
*/
|
|
901
908
|
baseURL?: string;
|
|
902
909
|
/**
|
|
903
|
-
*
|
|
904
|
-
*
|
|
905
|
-
*
|
|
910
|
+
* Timeout in milliseconds applied to **each attempt** (default `60000`). A
|
|
911
|
+
* timed-out attempt becomes a {@link SkailarConnectionError} and, for
|
|
912
|
+
* idempotent requests, is eligible for retry.
|
|
913
|
+
*
|
|
914
|
+
* @remarks
|
|
915
|
+
* This is **per attempt, not a total deadline**. With retries enabled the
|
|
916
|
+
* worst-case wall-clock for one call is roughly
|
|
917
|
+
* `(maxRetries + 1) × timeout + backoff` — e.g. the defaults (60s, 2 retries)
|
|
918
|
+
* can keep a call pending for ~3 minutes on a persistently slow endpoint. In
|
|
919
|
+
* environments with their own hard limits (serverless/Lambda, HTTP handlers),
|
|
920
|
+
* enforce an end-to-end bound by passing an `AbortSignal` (and/or a smaller
|
|
921
|
+
* per-call `timeout`) in the request options, or set `maxRetries: 0`.
|
|
906
922
|
*/
|
|
907
923
|
timeout?: number;
|
|
908
924
|
/**
|
|
@@ -918,8 +934,9 @@ interface SkailarOptions {
|
|
|
918
934
|
*/
|
|
919
935
|
fetch?: typeof fetch;
|
|
920
936
|
/**
|
|
921
|
-
* Headers merged into every request.
|
|
922
|
-
* `
|
|
937
|
+
* Headers merged into every request. Per-call `options.headers` override these.
|
|
938
|
+
* The SDK's `Authorization` always wins and cannot be overridden; `Accept` and
|
|
939
|
+
* `Content-Type` default to SDK values but may be overridden here or per call.
|
|
923
940
|
*/
|
|
924
941
|
defaultHeaders?: Record<string, string>;
|
|
925
942
|
}
|
|
@@ -942,7 +959,11 @@ interface RequestOptions {
|
|
|
942
959
|
* for this request only.
|
|
943
960
|
*/
|
|
944
961
|
timeout?: number;
|
|
945
|
-
/**
|
|
962
|
+
/**
|
|
963
|
+
* Extra headers merged into this request, overriding client `defaultHeaders`.
|
|
964
|
+
* Cannot override `Authorization` (always set by the SDK); may override `Accept`
|
|
965
|
+
* and `Content-Type`.
|
|
966
|
+
*/
|
|
946
967
|
headers?: Record<string, string>;
|
|
947
968
|
}
|
|
948
969
|
/** Internal description of a single HTTP request to dispatch. */
|
|
@@ -963,6 +984,15 @@ interface InternalRequest {
|
|
|
963
984
|
signal?: AbortSignal;
|
|
964
985
|
/** Per-call timeout override in ms; falls back to {@link Skailar.timeout}. */
|
|
965
986
|
timeout?: number;
|
|
987
|
+
/**
|
|
988
|
+
* Whether the request is safe to replay after a 5xx, timeout or connection
|
|
989
|
+
* failure — i.e. the server either did not execute it or executing it twice is
|
|
990
|
+
* harmless. `GET`s and text completions are idempotent; operations with billed
|
|
991
|
+
* side effects (image generation, speech, transcription, uploads) are not, and
|
|
992
|
+
* must not be retried once the request may have reached the server. A `429` is
|
|
993
|
+
* always retryable regardless, since it is rejected before execution.
|
|
994
|
+
*/
|
|
995
|
+
idempotent?: boolean;
|
|
966
996
|
}
|
|
967
997
|
/**
|
|
968
998
|
* The official Skailar API client. Construct once and reuse. Resource namespaces
|
|
@@ -1049,8 +1079,14 @@ declare class Skailar {
|
|
|
1049
1079
|
expect: "stream";
|
|
1050
1080
|
}): Promise<ChatCompletionStream>;
|
|
1051
1081
|
/**
|
|
1052
|
-
* Assemble the outgoing header set for a request
|
|
1053
|
-
*
|
|
1082
|
+
* Assemble the outgoing header set for a request.
|
|
1083
|
+
*
|
|
1084
|
+
* Precedence (later wins): client `defaultHeaders` → the SDK's default `Accept`
|
|
1085
|
+
* and `Content-Type` → per-call `options.headers` → `Authorization`.
|
|
1086
|
+
* `Authorization` is applied **last** so a caller-supplied header (including a
|
|
1087
|
+
* blindly proxied incoming one) can never override or drop the bearer token and
|
|
1088
|
+
* leak it elsewhere. `Accept`/`Content-Type` remain overridable on purpose —
|
|
1089
|
+
* e.g. audio speech sends `Accept: audio/mpeg`.
|
|
1054
1090
|
*
|
|
1055
1091
|
* @param options - The request description.
|
|
1056
1092
|
* @returns The header record to send.
|
|
@@ -1067,9 +1103,15 @@ declare class Skailar {
|
|
|
1067
1103
|
*/
|
|
1068
1104
|
private toApiError;
|
|
1069
1105
|
/**
|
|
1070
|
-
* Whether
|
|
1106
|
+
* Whether an HTTP error status is eligible for automatic retry.
|
|
1107
|
+
*
|
|
1108
|
+
* A `429` is always retryable: the gateway rejects it before executing the
|
|
1109
|
+
* request, so replaying it cannot duplicate side effects. A `5xx` may mean the
|
|
1110
|
+
* request already executed server-side, so it is retried only when the caller
|
|
1111
|
+
* marked the request idempotent.
|
|
1071
1112
|
*
|
|
1072
1113
|
* @param status - The HTTP status code.
|
|
1114
|
+
* @param idempotent - Whether replaying the request is safe.
|
|
1073
1115
|
* @returns `true` if retryable.
|
|
1074
1116
|
*/
|
|
1075
1117
|
private isRetryableStatus;
|
|
@@ -1140,7 +1182,14 @@ declare abstract class SkailarError extends Error {
|
|
|
1140
1182
|
readonly status: number | null;
|
|
1141
1183
|
/** Machine-readable error code from the body, if any. */
|
|
1142
1184
|
readonly code: string | undefined;
|
|
1143
|
-
/**
|
|
1185
|
+
/**
|
|
1186
|
+
* Correlation id from the `x-request-id` response header, if any, for support.
|
|
1187
|
+
*
|
|
1188
|
+
* @remarks
|
|
1189
|
+
* Populated on error responses. It is **not** currently surfaced for successful
|
|
1190
|
+
* streaming or audio responses (which return a stream rather than a wrapper),
|
|
1191
|
+
* so capture it from a thrown {@link SkailarError} when filing a ticket.
|
|
1192
|
+
*/
|
|
1144
1193
|
readonly requestId: string | undefined;
|
|
1145
1194
|
/** The raw response body captured for debugging. */
|
|
1146
1195
|
readonly raw: unknown;
|
package/dist/index.js
CHANGED
|
@@ -23,7 +23,14 @@ var SkailarError = class extends Error {
|
|
|
23
23
|
status;
|
|
24
24
|
/** Machine-readable error code from the body, if any. */
|
|
25
25
|
code;
|
|
26
|
-
/**
|
|
26
|
+
/**
|
|
27
|
+
* Correlation id from the `x-request-id` response header, if any, for support.
|
|
28
|
+
*
|
|
29
|
+
* @remarks
|
|
30
|
+
* Populated on error responses. It is **not** currently surfaced for successful
|
|
31
|
+
* streaming or audio responses (which return a stream rather than a wrapper),
|
|
32
|
+
* so capture it from a thrown {@link SkailarError} when filing a ticket.
|
|
33
|
+
*/
|
|
27
34
|
requestId;
|
|
28
35
|
/** The raw response body captured for debugging. */
|
|
29
36
|
raw;
|
|
@@ -134,6 +141,25 @@ async function* parseSSE(stream, signal) {
|
|
|
134
141
|
const reader = stream.getReader();
|
|
135
142
|
const decoder = new TextDecoder();
|
|
136
143
|
let buffer = "";
|
|
144
|
+
function nextLine() {
|
|
145
|
+
let i = -1;
|
|
146
|
+
for (let j = 0; j < buffer.length; j++) {
|
|
147
|
+
const c = buffer[j];
|
|
148
|
+
if (c === "\n" || c === "\r") {
|
|
149
|
+
i = j;
|
|
150
|
+
break;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
if (i === -1) return null;
|
|
154
|
+
const line = buffer.slice(0, i);
|
|
155
|
+
if (buffer[i] === "\r") {
|
|
156
|
+
if (i === buffer.length - 1) return null;
|
|
157
|
+
buffer = buffer.slice(i + (buffer[i + 1] === "\n" ? 2 : 1));
|
|
158
|
+
} else {
|
|
159
|
+
buffer = buffer.slice(i + 1);
|
|
160
|
+
}
|
|
161
|
+
return line;
|
|
162
|
+
}
|
|
137
163
|
try {
|
|
138
164
|
while (true) {
|
|
139
165
|
if (signal?.aborted) {
|
|
@@ -142,11 +168,8 @@ async function* parseSSE(stream, signal) {
|
|
|
142
168
|
const { done, value } = await reader.read();
|
|
143
169
|
if (done) break;
|
|
144
170
|
buffer += decoder.decode(value, { stream: true });
|
|
145
|
-
let
|
|
146
|
-
while ((
|
|
147
|
-
const rawLine = buffer.slice(0, newlineIndex);
|
|
148
|
-
buffer = buffer.slice(newlineIndex + 1);
|
|
149
|
-
const line = rawLine.endsWith("\r") ? rawLine.slice(0, -1) : rawLine;
|
|
171
|
+
let line;
|
|
172
|
+
while ((line = nextLine()) !== null) {
|
|
150
173
|
if (line === "" || line.startsWith(":")) continue;
|
|
151
174
|
if (!line.startsWith("data:")) continue;
|
|
152
175
|
const data = line.slice(5).trimStart();
|
|
@@ -154,7 +177,7 @@ async function* parseSSE(stream, signal) {
|
|
|
154
177
|
yield data;
|
|
155
178
|
}
|
|
156
179
|
}
|
|
157
|
-
const tail = buffer.
|
|
180
|
+
const tail = buffer.replace(/[\r\n]+$/, "");
|
|
158
181
|
if (tail.startsWith("data:")) {
|
|
159
182
|
const data = tail.slice(5).trimStart();
|
|
160
183
|
if (data !== "[DONE]" && data !== "") yield data;
|
|
@@ -190,12 +213,15 @@ var ChatCompletionStream = class {
|
|
|
190
213
|
*
|
|
191
214
|
* Each `data:` payload is `JSON.parse`d. If a payload carries an `error` field
|
|
192
215
|
* (the gateway's in-band failure signal), iteration throws the corresponding
|
|
193
|
-
* {@link SkailarAPIError} instead of yielding.
|
|
194
|
-
*
|
|
216
|
+
* {@link SkailarAPIError} instead of yielding. A payload that is not valid JSON
|
|
217
|
+
* (e.g. a plain-text error injected by an intermediary) throws a
|
|
218
|
+
* {@link SkailarConnectionError} rather than being dropped, so a malformed
|
|
219
|
+
* stream cannot truncate the response silently.
|
|
195
220
|
*
|
|
196
221
|
* @returns An async generator over {@link ChatCompletionChunk} values.
|
|
197
222
|
* @throws {@link SkailarAPIError} When the stream delivers an in-band error event.
|
|
198
|
-
* @throws {@link SkailarConnectionError} When
|
|
223
|
+
* @throws {@link SkailarConnectionError} When a payload is malformed, or the
|
|
224
|
+
* stream is aborted or fails to read.
|
|
199
225
|
*/
|
|
200
226
|
async *decode() {
|
|
201
227
|
for await (const data of parseSSE(this.body, this.controller.signal)) {
|
|
@@ -203,7 +229,9 @@ var ChatCompletionStream = class {
|
|
|
203
229
|
try {
|
|
204
230
|
parsed = JSON.parse(data);
|
|
205
231
|
} catch {
|
|
206
|
-
|
|
232
|
+
throw new SkailarConnectionError({
|
|
233
|
+
message: `Malformed streaming event (not JSON): ${data.slice(0, 200)}`
|
|
234
|
+
});
|
|
207
235
|
}
|
|
208
236
|
if (parsed !== null && typeof parsed === "object" && "error" in parsed) {
|
|
209
237
|
const { code, message } = parseErrorBody(parsed);
|
|
@@ -270,6 +298,8 @@ var ChatCompletions = class {
|
|
|
270
298
|
path: "/v1/chat/completions",
|
|
271
299
|
body,
|
|
272
300
|
expect: "stream",
|
|
301
|
+
// Text generation is safe to replay before the stream starts.
|
|
302
|
+
idempotent: true,
|
|
273
303
|
signal: options?.signal,
|
|
274
304
|
timeout: options?.timeout,
|
|
275
305
|
headers: options?.headers
|
|
@@ -280,6 +310,8 @@ var ChatCompletions = class {
|
|
|
280
310
|
path: "/v1/chat/completions",
|
|
281
311
|
body,
|
|
282
312
|
expect: "json",
|
|
313
|
+
// Text generation is safe to replay.
|
|
314
|
+
idempotent: true,
|
|
283
315
|
signal: options?.signal,
|
|
284
316
|
timeout: options?.timeout,
|
|
285
317
|
headers: options?.headers
|
|
@@ -315,6 +347,7 @@ var ModelsResource = class {
|
|
|
315
347
|
method: "GET",
|
|
316
348
|
path: "/v1/models",
|
|
317
349
|
expect: "json",
|
|
350
|
+
idempotent: true,
|
|
318
351
|
signal: options?.signal,
|
|
319
352
|
timeout: options?.timeout,
|
|
320
353
|
headers: options?.headers
|
|
@@ -336,6 +369,7 @@ var ModelsResource = class {
|
|
|
336
369
|
method: "GET",
|
|
337
370
|
path: `/v1/models/${encoded}`,
|
|
338
371
|
expect: "json",
|
|
372
|
+
idempotent: true,
|
|
339
373
|
signal: options?.signal,
|
|
340
374
|
timeout: options?.timeout,
|
|
341
375
|
headers: options?.headers
|
|
@@ -368,6 +402,8 @@ var ImagesResource = class {
|
|
|
368
402
|
path: "/v1/images/generations",
|
|
369
403
|
body,
|
|
370
404
|
expect: "json",
|
|
405
|
+
// Billed image generation: never replay on 5xx/timeout to avoid double charges.
|
|
406
|
+
idempotent: false,
|
|
371
407
|
signal: options?.signal,
|
|
372
408
|
timeout: options?.timeout,
|
|
373
409
|
headers: options?.headers
|
|
@@ -428,6 +464,8 @@ var AudioTranscriptions = class {
|
|
|
428
464
|
path: "/v1/audio/transcriptions",
|
|
429
465
|
body: { base64, mime },
|
|
430
466
|
expect: "json",
|
|
467
|
+
// Billed transcription: never replay on 5xx/timeout to avoid double charges.
|
|
468
|
+
idempotent: false,
|
|
431
469
|
signal: options?.signal,
|
|
432
470
|
timeout: options?.timeout,
|
|
433
471
|
headers: options?.headers
|
|
@@ -465,6 +503,8 @@ var AudioSpeech = class {
|
|
|
465
503
|
body: { input: params.input, voice: params.voice },
|
|
466
504
|
headers: { Accept: "audio/mpeg", ...options?.headers },
|
|
467
505
|
expect: "response",
|
|
506
|
+
// Billed speech synthesis: never replay on 5xx/timeout to avoid double charges.
|
|
507
|
+
idempotent: false,
|
|
468
508
|
signal: options?.signal,
|
|
469
509
|
timeout: options?.timeout
|
|
470
510
|
});
|
|
@@ -509,7 +549,9 @@ var ImageUploads = class {
|
|
|
509
549
|
method: "POST",
|
|
510
550
|
path: "/v1/uploads/images",
|
|
511
551
|
body: { base64, content_type: params.contentType },
|
|
512
|
-
expect: "json"
|
|
552
|
+
expect: "json",
|
|
553
|
+
// Upload creates a stored asset: never replay to avoid duplicate uploads.
|
|
554
|
+
idempotent: false
|
|
513
555
|
});
|
|
514
556
|
}
|
|
515
557
|
};
|
|
@@ -533,7 +575,9 @@ var FileUploads = class {
|
|
|
533
575
|
method: "POST",
|
|
534
576
|
path: "/v1/uploads/files",
|
|
535
577
|
body: { base64, content_type: params.contentType },
|
|
536
|
-
expect: "json"
|
|
578
|
+
expect: "json",
|
|
579
|
+
// Upload creates a stored asset: never replay to avoid duplicate uploads.
|
|
580
|
+
idempotent: false
|
|
537
581
|
});
|
|
538
582
|
}
|
|
539
583
|
};
|
|
@@ -550,6 +594,11 @@ var UploadsResource = class {
|
|
|
550
594
|
};
|
|
551
595
|
|
|
552
596
|
// src/client.ts
|
|
597
|
+
var MAX_RETRY_DELAY_MS = 6e4;
|
|
598
|
+
function capRetryDelay(retryAfterSeconds) {
|
|
599
|
+
if (retryAfterSeconds === void 0) return void 0;
|
|
600
|
+
return Math.min(Math.max(0, retryAfterSeconds) * 1e3, MAX_RETRY_DELAY_MS);
|
|
601
|
+
}
|
|
553
602
|
function delay(ms, signal) {
|
|
554
603
|
return new Promise((resolve, reject) => {
|
|
555
604
|
if (signal?.aborted) {
|
|
@@ -671,6 +720,7 @@ var Skailar = class {
|
|
|
671
720
|
method: "GET",
|
|
672
721
|
path: "/v1/ping-key",
|
|
673
722
|
expect: "json",
|
|
723
|
+
idempotent: true,
|
|
674
724
|
signal: options?.signal,
|
|
675
725
|
timeout: options?.timeout,
|
|
676
726
|
headers: options?.headers
|
|
@@ -681,15 +731,22 @@ var Skailar = class {
|
|
|
681
731
|
*
|
|
682
732
|
* Applies, per attempt: header assembly with bearer auth, a timeout-derived
|
|
683
733
|
* {@link AbortSignal} composed with any caller signal, execution of
|
|
684
|
-
* {@link SkailarOptions.fetch}, and error mapping.
|
|
685
|
-
*
|
|
686
|
-
*
|
|
687
|
-
*
|
|
734
|
+
* {@link SkailarOptions.fetch}, and error mapping. Backs off with full-jitter
|
|
735
|
+
* exponential delay, honoring a server `Retry-After` when present (capped at
|
|
736
|
+
* {@link MAX_RETRY_DELAY_MS} so an oversized value cannot stall the call), up
|
|
737
|
+
* to {@link Skailar.maxRetries}. Non-429 4xx responses fail fast.
|
|
738
|
+
*
|
|
739
|
+
* Retries are scoped to avoid duplicating side effects: an HTTP `429` is always
|
|
740
|
+
* retryable (rejected before execution), while a `5xx`, timeout or connection
|
|
741
|
+
* failure is retried **only** when `options.idempotent` is set — because the
|
|
742
|
+
* request may already have executed server-side. Requests with billed side
|
|
743
|
+
* effects (image generation, speech, transcription, uploads) leave it unset and
|
|
744
|
+
* are therefore never replayed once they may have reached the gateway.
|
|
688
745
|
*
|
|
689
746
|
* Transport failures are reported as {@link SkailarConnectionError} with a
|
|
690
747
|
* message distinguishing three causes: an external `signal` abort
|
|
691
|
-
* (non-retryable), an internal timeout once {@link Skailar.timeout} elapses
|
|
692
|
-
*
|
|
748
|
+
* (non-retryable), an internal timeout once {@link Skailar.timeout} elapses,
|
|
749
|
+
* and a generic network failure.
|
|
693
750
|
*
|
|
694
751
|
* @param options - The request description.
|
|
695
752
|
* @returns The parsed JSON, raw `Response`, or {@link ChatCompletionStream}
|
|
@@ -699,6 +756,7 @@ var Skailar = class {
|
|
|
699
756
|
const url = `${this.baseURL}${options.path}`;
|
|
700
757
|
const isStream = options.expect === "stream";
|
|
701
758
|
const timeoutMs = options.timeout ?? this.timeout;
|
|
759
|
+
const idempotent = options.idempotent ?? false;
|
|
702
760
|
let attempt = 0;
|
|
703
761
|
while (true) {
|
|
704
762
|
const controller = new AbortController();
|
|
@@ -729,7 +787,7 @@ var Skailar = class {
|
|
|
729
787
|
message: externallyAborted ? "Request aborted" : timedOut ? `Request timed out after ${timeoutMs}ms` : "Network request to the Skailar API failed",
|
|
730
788
|
cause: err
|
|
731
789
|
});
|
|
732
|
-
if (externallyAborted || !this.shouldRetry(attempt)) throw connErr;
|
|
790
|
+
if (externallyAborted || !idempotent || !this.shouldRetry(attempt)) throw connErr;
|
|
733
791
|
attempt += 1;
|
|
734
792
|
await delay(this.backoff(attempt), options.signal);
|
|
735
793
|
continue;
|
|
@@ -738,8 +796,8 @@ var Skailar = class {
|
|
|
738
796
|
if (!response.ok) {
|
|
739
797
|
detachExternal();
|
|
740
798
|
const apiError = await this.toApiError(response);
|
|
741
|
-
const retryAfterMs = apiError instanceof SkailarRateLimitError
|
|
742
|
-
if (this.isRetryableStatus(response.status) && this.shouldRetry(attempt)) {
|
|
799
|
+
const retryAfterMs = apiError instanceof SkailarRateLimitError ? capRetryDelay(apiError.retryAfter) : void 0;
|
|
800
|
+
if (this.isRetryableStatus(response.status, idempotent) && this.shouldRetry(attempt)) {
|
|
743
801
|
attempt += 1;
|
|
744
802
|
await delay(retryAfterMs ?? this.backoff(attempt), options.signal);
|
|
745
803
|
continue;
|
|
@@ -766,8 +824,14 @@ var Skailar = class {
|
|
|
766
824
|
}
|
|
767
825
|
}
|
|
768
826
|
/**
|
|
769
|
-
* Assemble the outgoing header set for a request
|
|
770
|
-
*
|
|
827
|
+
* Assemble the outgoing header set for a request.
|
|
828
|
+
*
|
|
829
|
+
* Precedence (later wins): client `defaultHeaders` → the SDK's default `Accept`
|
|
830
|
+
* and `Content-Type` → per-call `options.headers` → `Authorization`.
|
|
831
|
+
* `Authorization` is applied **last** so a caller-supplied header (including a
|
|
832
|
+
* blindly proxied incoming one) can never override or drop the bearer token and
|
|
833
|
+
* leak it elsewhere. `Accept`/`Content-Type` remain overridable on purpose —
|
|
834
|
+
* e.g. audio speech sends `Accept: audio/mpeg`.
|
|
771
835
|
*
|
|
772
836
|
* @param options - The request description.
|
|
773
837
|
* @returns The header record to send.
|
|
@@ -775,11 +839,14 @@ var Skailar = class {
|
|
|
775
839
|
buildHeaders(options) {
|
|
776
840
|
const headers = {
|
|
777
841
|
...this.defaultHeaders,
|
|
778
|
-
Authorization: `Bearer ${this.apiKey}`,
|
|
779
842
|
Accept: options.expect === "stream" ? "text/event-stream" : "application/json"
|
|
780
843
|
};
|
|
781
844
|
if (options.body !== void 0) headers["Content-Type"] = "application/json";
|
|
782
845
|
if (options.headers) Object.assign(headers, options.headers);
|
|
846
|
+
for (const key of Object.keys(headers)) {
|
|
847
|
+
if (key.toLowerCase() === "authorization") delete headers[key];
|
|
848
|
+
}
|
|
849
|
+
headers["Authorization"] = `Bearer ${this.apiKey}`;
|
|
783
850
|
return headers;
|
|
784
851
|
}
|
|
785
852
|
/**
|
|
@@ -805,13 +872,20 @@ var Skailar = class {
|
|
|
805
872
|
return SkailarAPIError.from(response.status, parsed, requestId, raw, retryAfter);
|
|
806
873
|
}
|
|
807
874
|
/**
|
|
808
|
-
* Whether
|
|
875
|
+
* Whether an HTTP error status is eligible for automatic retry.
|
|
876
|
+
*
|
|
877
|
+
* A `429` is always retryable: the gateway rejects it before executing the
|
|
878
|
+
* request, so replaying it cannot duplicate side effects. A `5xx` may mean the
|
|
879
|
+
* request already executed server-side, so it is retried only when the caller
|
|
880
|
+
* marked the request idempotent.
|
|
809
881
|
*
|
|
810
882
|
* @param status - The HTTP status code.
|
|
883
|
+
* @param idempotent - Whether replaying the request is safe.
|
|
811
884
|
* @returns `true` if retryable.
|
|
812
885
|
*/
|
|
813
|
-
isRetryableStatus(status) {
|
|
814
|
-
|
|
886
|
+
isRetryableStatus(status, idempotent) {
|
|
887
|
+
if (status === 429) return true;
|
|
888
|
+
return status >= 500 && idempotent;
|
|
815
889
|
}
|
|
816
890
|
/**
|
|
817
891
|
* Whether another attempt remains within the retry budget.
|