@aexhq/sdk 0.34.0 → 0.35.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/retry.js ADDED
@@ -0,0 +1,320 @@
1
+ /**
2
+ * Built-in transport resilience for the aex SDK.
3
+ *
4
+ * Every BFF-bound request the SDK makes goes through one {@link FetchLike}. This
5
+ * module wraps that fetch so a transient failure — an HTTP 429 (rate limited),
6
+ * a 500/502/503/504 (server hiccup), a 529 (upstream overloaded), or a network
7
+ * error — is retried with BOUNDED exponential backoff + full jitter, honoring
8
+ * the server's `Retry-After` header when present. Non-retryable 4xx responses
9
+ * (400/401/403/404/…) fail fast — retrying them only wastes the caller's time.
10
+ *
11
+ * Retries are SAFE to enable by default because the billable submits
12
+ * (`createSession` / `sendSessionMessage`) carry a stable `Idempotency-Key`
13
+ * header: re-sending the identical request de-duplicates server-side, so a retry
14
+ * never creates a duplicate billable turn.
15
+ *
16
+ * When retries are exhausted on a rate-limit / overloaded status the wrapper
17
+ * surfaces an {@link AexRateLimitError} — a structured, non-leaky throttle error
18
+ * carrying the parsed `retryAfterMs`, the attempt count, and (when the runtime
19
+ * supplies it) an upstream {@link ProviderFault}. All other exhausted retries
20
+ * fall through to the transport's usual `AexApiError` / network rejection.
21
+ */
22
+ import { AexApiError } from "./_contracts/index.js";
23
+ /**
24
+ * HTTP statuses that are transient and worth retrying. The billable submits
25
+ * carry an idempotency key, so re-issuing them is safe. Everything not in this
26
+ * set (400/401/403/404/409/422/…) is a definitive client error and fails fast.
27
+ */
28
+ export const RETRYABLE_STATUS = [429, 500, 502, 503, 504, 529];
29
+ /**
30
+ * The subset of {@link RETRYABLE_STATUS} the platform / upstream provider uses to
31
+ * say "slow down": 429 rate-limit, 503 unavailable, 529 overloaded. When retries
32
+ * for one of these run out, the wrapper raises an {@link AexRateLimitError}.
33
+ */
34
+ export const RATE_LIMIT_STATUS = [429, 503, 529];
35
+ const DEFAULT_RETRY = {
36
+ maxAttempts: 4,
37
+ initialDelayMs: 500,
38
+ maxDelayMs: 20_000,
39
+ maxElapsedMs: 120_000
40
+ };
41
+ /** Resolve caller options over the defaults, clamping to sane bounds. */
42
+ export function resolveRetryConfig(options) {
43
+ const maxAttempts = Math.max(1, Math.floor(options?.maxAttempts ?? DEFAULT_RETRY.maxAttempts));
44
+ const initialDelayMs = Math.max(0, options?.initialDelayMs ?? DEFAULT_RETRY.initialDelayMs);
45
+ const maxDelayMs = Math.max(initialDelayMs, options?.maxDelayMs ?? DEFAULT_RETRY.maxDelayMs);
46
+ const maxElapsedMs = Math.max(0, options?.maxElapsedMs ?? DEFAULT_RETRY.maxElapsedMs);
47
+ return { maxAttempts, initialDelayMs, maxDelayMs, maxElapsedMs };
48
+ }
49
+ export function isRetryableStatus(status) {
50
+ return RETRYABLE_STATUS.includes(status);
51
+ }
52
+ export function isRateLimitStatus(status) {
53
+ return RATE_LIMIT_STATUS.includes(status);
54
+ }
55
+ /**
56
+ * Parse an HTTP `Retry-After` header into milliseconds. Per RFC 7231 the value
57
+ * is either a non-negative integer number of seconds or an HTTP-date; both are
58
+ * handled. Returns `undefined` for a missing or unparseable value.
59
+ */
60
+ export function parseRetryAfterMs(headerValue, now = Date.now()) {
61
+ if (headerValue === null || headerValue === undefined)
62
+ return undefined;
63
+ const trimmed = headerValue.trim();
64
+ if (trimmed.length === 0)
65
+ return undefined;
66
+ if (/^\d+$/.test(trimmed)) {
67
+ return Number(trimmed) * 1000;
68
+ }
69
+ const dateMs = Date.parse(trimmed);
70
+ if (!Number.isNaN(dateMs)) {
71
+ return Math.max(0, dateMs - now);
72
+ }
73
+ return undefined;
74
+ }
75
+ /**
76
+ * Full-jitter exponential backoff (AWS-style): the nominal wait doubles per
77
+ * retry up to `maxDelayMs`, and the actual wait is a uniform sample in
78
+ * `[0, nominal]` to de-correlate concurrent clients. `attemptNumber` is the
79
+ * 1-based number of the attempt that just failed.
80
+ */
81
+ export function computeBackoffDelayMs(config, attemptNumber, random) {
82
+ const exponent = Math.max(0, attemptNumber - 1);
83
+ const nominal = Math.min(config.maxDelayMs, config.initialDelayMs * 2 ** exponent);
84
+ return Math.round(random() * nominal);
85
+ }
86
+ /** Combine the server's `Retry-After` (a floor) with our jittered backoff. */
87
+ function nextDelayMs(config, attemptNumber, random, retryAfterMs) {
88
+ const backoff = computeBackoffDelayMs(config, attemptNumber, random);
89
+ return retryAfterMs === undefined ? backoff : Math.max(retryAfterMs, backoff);
90
+ }
91
+ const THROTTLE_KINDS = new Set(["rate_limit", "overloaded", "quota_exceeded"]);
92
+ /** True when a {@link ProviderFault} represents a "back off and retry" signal. */
93
+ export function isThrottleFault(fault) {
94
+ return THROTTLE_KINDS.has(fault.kind);
95
+ }
96
+ /**
97
+ * Structured throttle error. Extends {@link AexApiError} so existing
98
+ * `catch (err instanceof AexApiError)` sites keep working, while callers that
99
+ * want the details narrow with {@link isRateLimited} and read `retryAfterMs`,
100
+ * `attempts`, `source`, and `providerFault`. The `message` is a fixed,
101
+ * non-leaky summary — it never echoes the raw error body (which is still
102
+ * available, redacted, on `.body`).
103
+ */
104
+ export class AexRateLimitError extends AexApiError {
105
+ /** Milliseconds the server/provider asked us to wait, when known. */
106
+ retryAfterMs;
107
+ /** How many attempts were made before giving up. */
108
+ attempts;
109
+ /** Whether the throttle came from the aex API plane or the upstream provider. */
110
+ source;
111
+ /** The upstream provider fault, when the throttle originated there. */
112
+ providerFault;
113
+ constructor(args) {
114
+ super(args.status, args.message ?? defaultThrottleMessage(args), args.body);
115
+ this.attempts = args.attempts;
116
+ this.source = args.source ?? "api";
117
+ if (args.retryAfterMs !== undefined)
118
+ this.retryAfterMs = args.retryAfterMs;
119
+ if (args.providerFault !== undefined)
120
+ this.providerFault = args.providerFault;
121
+ }
122
+ }
123
+ /** Type guard for {@link AexRateLimitError}. */
124
+ export function isRateLimited(err) {
125
+ return err instanceof AexRateLimitError;
126
+ }
127
+ function defaultThrottleMessage(args) {
128
+ const who = args.source === "provider" ? "upstream provider" : "aex API";
129
+ const label = args.status === 529 ? "overloaded" : "rate limit reached";
130
+ const attempts = `${args.attempts} attempt${args.attempts === 1 ? "" : "s"}`;
131
+ const wait = args.retryAfterMs !== undefined
132
+ ? `; retry after ~${Math.ceil(args.retryAfterMs / 1000)}s`
133
+ : "";
134
+ return `${who} ${label} (HTTP ${args.status}) after ${attempts}${wait}`;
135
+ }
136
+ /**
137
+ * Best-effort parse of an unknown value into a {@link ProviderFault}. Tolerant
138
+ * of two shapes so the SDK consumes the runtime fault the moment it starts
139
+ * emitting one, without a contracts change:
140
+ *
141
+ * 1. The canonical `{ provider?, kind, status?, retryAfterMs?, message? }`
142
+ * (optionally nested under a `providerFault` key), OR
143
+ * 2. A raw upstream error `{ type: "rate_limit_error" | "overloaded_error"
144
+ * | ..., message?, retry_after? | retryAfter? }` — `type` maps to `kind`
145
+ * and `retry_after` (seconds) maps to `retryAfterMs`.
146
+ *
147
+ * Returns `undefined` when the value carries no recognizable fault.
148
+ */
149
+ export function parseProviderFault(value) {
150
+ if (value === null || typeof value !== "object")
151
+ return undefined;
152
+ const record = value;
153
+ const nested = record.providerFault ?? record.provider_fault;
154
+ if (nested !== undefined && nested !== value) {
155
+ const fromNested = parseProviderFault(nested);
156
+ if (fromNested)
157
+ return fromNested;
158
+ }
159
+ const kind = coerceFaultKind(record.kind ?? record.type ?? record.code);
160
+ if (kind === undefined)
161
+ return undefined;
162
+ const provider = typeof record.provider === "string" ? record.provider : undefined;
163
+ const status = coerceStatus(record.status ?? record.statusCode ?? record.httpStatus);
164
+ const retryAfterMs = coerceRetryAfterMs(record.retryAfterMs ?? record.retry_after_ms ?? record.retryAfter ?? record.retry_after);
165
+ const message = typeof record.message === "string" ? record.message : undefined;
166
+ return {
167
+ kind,
168
+ ...(provider !== undefined ? { provider } : {}),
169
+ ...(status !== undefined ? { status } : {}),
170
+ ...(retryAfterMs !== undefined ? { retryAfterMs } : {}),
171
+ ...(message !== undefined ? { message } : {})
172
+ };
173
+ }
174
+ function coerceFaultKind(raw) {
175
+ if (typeof raw !== "string")
176
+ return undefined;
177
+ const value = raw.toLowerCase();
178
+ if (value.includes("rate_limit") || value.includes("rate limit") || value === "429")
179
+ return "rate_limit";
180
+ if (value.includes("overload") || value === "529")
181
+ return "overloaded";
182
+ if (value.includes("quota") || value.includes("insufficient"))
183
+ return "quota_exceeded";
184
+ if (value.includes("provider_error") || value.includes("provider error") || value.includes("api_error")) {
185
+ return "provider_error";
186
+ }
187
+ return undefined;
188
+ }
189
+ function coerceStatus(raw) {
190
+ if (typeof raw === "number" && Number.isFinite(raw))
191
+ return raw;
192
+ if (typeof raw === "string" && /^\d+$/.test(raw.trim()))
193
+ return Number(raw.trim());
194
+ return undefined;
195
+ }
196
+ /** Accept a ms number, a `<digits>` string, or seconds under a `retry_after` alias. */
197
+ function coerceRetryAfterMs(raw) {
198
+ if (typeof raw === "number" && Number.isFinite(raw)) {
199
+ // Heuristic: small integers are seconds (the upstream convention), large
200
+ // ones are already milliseconds.
201
+ return raw > 0 && raw < 1000 ? raw * 1000 : raw;
202
+ }
203
+ if (typeof raw === "string" && /^\d+$/.test(raw.trim())) {
204
+ const n = Number(raw.trim());
205
+ return n > 0 && n < 1000 ? n * 1000 : n;
206
+ }
207
+ return undefined;
208
+ }
209
+ const defaultSleep = (ms, signal) => new Promise((resolve, reject) => {
210
+ if (signal?.aborted) {
211
+ reject(signal.reason instanceof Error ? signal.reason : new DOMException("Aborted", "AbortError"));
212
+ return;
213
+ }
214
+ const timer = setTimeout(() => {
215
+ signal?.removeEventListener("abort", onAbort);
216
+ resolve();
217
+ }, ms);
218
+ const onAbort = () => {
219
+ clearTimeout(timer);
220
+ reject(signal?.reason instanceof Error ? signal.reason : new DOMException("Aborted", "AbortError"));
221
+ };
222
+ signal?.addEventListener("abort", onAbort, { once: true });
223
+ });
224
+ function isAbortError(err) {
225
+ return err instanceof Error && err.name === "AbortError";
226
+ }
227
+ async function drain(response) {
228
+ try {
229
+ if (response.body && typeof response.body.cancel === "function") {
230
+ await response.body.cancel();
231
+ return;
232
+ }
233
+ await response.text();
234
+ }
235
+ catch {
236
+ // Draining is best-effort; a discarded retryable response never surfaces.
237
+ }
238
+ }
239
+ async function readBodyForError(response) {
240
+ try {
241
+ const text = await response.text();
242
+ if (text.length === 0)
243
+ return {};
244
+ try {
245
+ return JSON.parse(text);
246
+ }
247
+ catch {
248
+ return { raw: text };
249
+ }
250
+ }
251
+ catch {
252
+ return {};
253
+ }
254
+ }
255
+ /**
256
+ * Wrap a {@link FetchLike} with the bounded-retry loop. `retry === false`
257
+ * disables the layer entirely (the input fetch is returned unchanged). Otherwise
258
+ * the returned fetch retries transient failures per {@link RetryOptions} and, on
259
+ * an exhausted rate-limit/overloaded status, throws {@link AexRateLimitError}.
260
+ */
261
+ export function withRetry(fetchImpl, retry, deps = {}) {
262
+ if (retry === false)
263
+ return fetchImpl;
264
+ const config = resolveRetryConfig(retry);
265
+ const sleep = deps.sleep ?? defaultSleep;
266
+ const random = deps.random ?? Math.random;
267
+ const now = deps.now ?? Date.now;
268
+ return async (input, init) => {
269
+ const startedAt = now();
270
+ const signal = init?.signal ?? undefined;
271
+ let attempt = 0;
272
+ for (;;) {
273
+ attempt += 1;
274
+ let response;
275
+ try {
276
+ response = await fetchImpl(input, init);
277
+ }
278
+ catch (err) {
279
+ // A caller-initiated abort is terminal, never transient.
280
+ if (isAbortError(err))
281
+ throw err;
282
+ if (attempt >= config.maxAttempts)
283
+ throw err;
284
+ const delay = computeBackoffDelayMs(config, attempt, random);
285
+ if (now() - startedAt + delay > config.maxElapsedMs)
286
+ throw err;
287
+ await sleep(delay, signal ?? undefined);
288
+ continue;
289
+ }
290
+ // Success or a definitive (non-retryable) response — hand straight back so
291
+ // the transport reads/throws exactly as it does without the retry layer.
292
+ if (!isRetryableStatus(response.status)) {
293
+ return response;
294
+ }
295
+ const retryAfterMs = parseRetryAfterMs(response.headers.get("retry-after"), now());
296
+ const willRetry = attempt < config.maxAttempts &&
297
+ now() - startedAt + nextDelayMs(config, attempt, random, retryAfterMs) <= config.maxElapsedMs;
298
+ if (willRetry) {
299
+ await drain(response);
300
+ await sleep(nextDelayMs(config, attempt, random, retryAfterMs), signal ?? undefined);
301
+ continue;
302
+ }
303
+ // Retries exhausted (or budget spent). A rate-limit/overloaded status
304
+ // becomes a structured throttle error; any other transient status falls
305
+ // through to the transport's normal AexApiError.
306
+ if (isRateLimitStatus(response.status)) {
307
+ const body = await readBodyForError(response);
308
+ throw new AexRateLimitError({
309
+ status: response.status,
310
+ attempts: attempt,
311
+ source: "api",
312
+ ...(retryAfterMs !== undefined ? { retryAfterMs } : {}),
313
+ body
314
+ });
315
+ }
316
+ return response;
317
+ }
318
+ };
319
+ }
320
+ //# sourceMappingURL=retry.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"retry.js","sourceRoot":"","sources":["../src/retry.ts"],"names":[],"mappings":"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"}
package/dist/version.d.ts CHANGED
@@ -6,4 +6,4 @@
6
6
  *
7
7
  * Used by the (future) User-Agent header on outbound SDK requests.
8
8
  */
9
- export declare const SDK_VERSION = "0.34.0";
9
+ export declare const SDK_VERSION = "0.35.0";
package/dist/version.js CHANGED
@@ -6,5 +6,5 @@
6
6
  *
7
7
  * Used by the (future) User-Agent header on outbound SDK requests.
8
8
  */
9
- export const SDK_VERSION = "0.34.0";
9
+ export const SDK_VERSION = "0.35.0";
10
10
  //# sourceMappingURL=version.js.map
@@ -0,0 +1,129 @@
1
+ ---
2
+ title: Retries and throttling
3
+ ---
4
+
5
+ # Retries and throttling
6
+
7
+ The SDK ships with built-in transport resilience. Every request it makes to the
8
+ aex API is automatically retried on **transient** failures with bounded
9
+ exponential backoff and jitter, honoring the server's `Retry-After` header. You
10
+ get this by default — no wrapper code — and it is safe to leave on because the
11
+ billable submits carry a stable idempotency key, so a retry never creates a
12
+ duplicate run.
13
+
14
+ ## What gets retried
15
+
16
+ Retried automatically:
17
+
18
+ - HTTP `429` (rate limited)
19
+ - HTTP `500`, `502`, `503`, `504` (server hiccups)
20
+ - HTTP `529` (upstream provider overloaded)
21
+ - Network errors (connection reset, DNS failure, timeout)
22
+
23
+ Never retried — these fail fast so you see the real problem immediately:
24
+
25
+ - `400` / `422` (bad request), `401` / `403` (auth), `404` (not found),
26
+ `409` (conflict), and every other non-transient `4xx`.
27
+ - A request you aborted yourself (via an `AbortSignal`).
28
+
29
+ ## Tuning or disabling
30
+
31
+ Pass a `retry` option when you construct the client:
32
+
33
+ ```ts
34
+ import { Aex } from "@aexhq/sdk";
35
+
36
+ const aex = new Aex({
37
+ apiToken: process.env.AEX_API_TOKEN!,
38
+ retry: {
39
+ maxAttempts: 4, // total tries incl. the first (default 4)
40
+ initialDelayMs: 500, // base backoff, doubles per retry (default 500)
41
+ maxDelayMs: 20_000, // cap on any single wait (default 20s)
42
+ maxElapsedMs: 120_000 // overall wall-clock budget (default 2m)
43
+ }
44
+ });
45
+ ```
46
+
47
+ Turn it off entirely with `retry: false`, or make a single attempt with
48
+ `retry: { maxAttempts: 1 }`.
49
+
50
+ ## Idempotent by construction
51
+
52
+ Retries — whether the built-in transport retry or your own re-invocation of
53
+ `run(...)` — never double-bill. The one-shot `run(...)` and `sessions.run(...)`
54
+ derive the turn's idempotency key from the session-create key, so re-invoking
55
+ either with the same `idempotencyKey` de-duplicates **both** the session create
56
+ and the billable turn server-side:
57
+
58
+ ```ts
59
+ // A retried call with the same idempotencyKey resolves to the same run,
60
+ // not a second billable one.
61
+ const result = await aex.run({
62
+ model: "claude-haiku-4-5",
63
+ message: "Write a short report and save it as a file.",
64
+ apiKeys: { anthropic: process.env.ANTHROPIC_API_KEY! },
65
+ idempotencyKey: "report-2026-07-01"
66
+ });
67
+ ```
68
+
69
+ ## Replaying a throttled turn
70
+
71
+ When a turn on a live session is interrupted by a throttle, replay the last
72
+ message with `session.replayLast()`. It reuses the previous message's idempotency
73
+ key by default, so if the original turn actually landed it de-duplicates instead
74
+ of billing twice:
75
+
76
+ ```ts
77
+ const session = await aex.openSession({
78
+ model: "claude-haiku-4-5",
79
+ apiKeys: { anthropic: process.env.ANTHROPIC_API_KEY! }
80
+ });
81
+
82
+ try {
83
+ await session.send("Summarize the attached dataset.").done();
84
+ } catch (err) {
85
+ const { isRateLimited } = await import("@aexhq/sdk");
86
+ if (isRateLimited(err)) {
87
+ // Wait out the throttle, then replay the same message.
88
+ await new Promise((r) => setTimeout(r, err.retryAfterMs ?? 2_000));
89
+ await session.replayLast().done();
90
+ } else {
91
+ throw err;
92
+ }
93
+ }
94
+ ```
95
+
96
+ Pass a fresh key (`session.replayLast({ idempotencyKey: "..." })`) when you
97
+ deliberately want a brand-new turn instead of a de-duplicated replay.
98
+
99
+ ## The throttle error
100
+
101
+ When retries are exhausted on a rate-limit / overloaded status, the SDK throws an
102
+ `AexRateLimitError`. It extends `AexApiError`, so existing `catch` sites keep
103
+ working, and it carries structured, non-leaky detail:
104
+
105
+ ```ts
106
+ import { isRateLimited } from "@aexhq/sdk";
107
+
108
+ try {
109
+ await aex.run({ /* … */ });
110
+ } catch (err) {
111
+ if (isRateLimited(err)) {
112
+ err.status; // 429 | 503 | 529
113
+ err.attempts; // how many tries were made
114
+ err.retryAfterMs; // suggested wait, when the server supplied one
115
+ err.source; // "api" (aex plane) or "provider" (upstream model)
116
+ err.providerFault; // upstream fault detail, when the model provider throttled
117
+ }
118
+ }
119
+ ```
120
+
121
+ The `message` is a fixed summary (e.g. `aex API rate limit reached (HTTP 429)
122
+ after 4 attempts; retry after ~2s`) — it never echoes the raw response body,
123
+ which stays available, redacted, on `err.body`.
124
+
125
+ When the throttle originated at the upstream model provider (rather than the aex
126
+ API plane), `err.source` is `"provider"` and `err.providerFault` describes it:
127
+ its `kind` (`rate_limit` / `overloaded` / `quota_exceeded` / `provider_error`),
128
+ the upstream `status`, and a suggested `retryAfterMs`. Use `parseProviderFault`
129
+ to read the same shape off a raw fault value yourself.