@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/_contracts/submission.d.ts +58 -23
- package/dist/_contracts/submission.js +54 -10
- package/dist/cli.mjs +79 -0
- package/dist/cli.mjs.sha256 +1 -1
- package/dist/client.d.ts +19 -0
- package/dist/client.js +92 -6
- package/dist/client.js.map +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.js +6 -0
- package/dist/index.js.map +1 -1
- package/dist/retry.d.ts +162 -0
- package/dist/retry.js +320 -0
- package/dist/retry.js.map +1 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/docs/retries.md +129 -0
- package/examples/feature-tour.ts +301 -0
- package/package.json +1 -1
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
package/dist/version.js
CHANGED
package/docs/retries.md
ADDED
|
@@ -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.
|