llm-retry-kit 0.2.0 → 0.3.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/README.md CHANGED
@@ -1,15 +1,59 @@
1
1
  # llm-retry-kit
2
2
 
3
- Small resilience layer for production LLM calls. It gives you retries,
4
- provider fallback, jittered exponential backoff, `Retry-After` handling,
5
- budget tracking, cancellation, timeouts, and observability hooks.
6
-
7
- ## Install
3
+ [![npm](https://img.shields.io/npm/v/llm-retry-kit?label=npm)](https://www.npmjs.com/package/llm-retry-kit)
4
+ [![downloads](https://img.shields.io/npm/dm/llm-retry-kit?label=downloads)](https://www.npmjs.com/package/llm-retry-kit)
5
+ [![license](https://img.shields.io/npm/l/llm-retry-kit?label=license)](./LICENSE)
6
+ [![node](https://img.shields.io/badge/node-%3E%3D18-brightgreen)](./package.json)
7
+ [![TypeScript](https://img.shields.io/badge/TypeScript-strict-blue)](./tsconfig.json)
8
+ [![CI](https://img.shields.io/github/actions/workflow/status/JavadRostami3/llm-retry-kit/ci.yml?label=CI)](https://github.com/JavadRostami3/llm-retry-kit/actions)
9
+
10
+ Small resilience layer for production LLM calls. `llm-retry-kit` gives you
11
+ provider-aware retries, fallback chains, jittered exponential backoff,
12
+ `Retry-After` handling, streaming retries, circuit breakers, hedged requests,
13
+ budget tracking, cancellation, timeouts, and observability hooks without
14
+ runtime dependencies.
8
15
 
9
16
  ```bash
10
17
  npm install llm-retry-kit
11
18
  ```
12
19
 
20
+ ## Why llm-retry-kit?
21
+
22
+ LLM APIs fail in ways that normal API wrappers often do not model well:
23
+
24
+ - `429` rate limits need backoff, not immediate loops.
25
+ - `500`, `503`, `504`, and Anthropic `529 overloaded_error` are usually
26
+ transient and often worth retrying or failing over.
27
+ - `400`, `401`, `403`, and request-too-large errors are usually request or
28
+ credential problems and should not blindly retry or fallback.
29
+ - Failed retry attempts can still count toward provider rate limits.
30
+ - Production apps need cancellation, budget limits, and logs around every
31
+ attempt.
32
+
33
+ This package keeps the core primitive small: you provide the actual SDK call,
34
+ and `llm-retry-kit` manages the reliability policy around it.
35
+
36
+ ## Features
37
+
38
+ - Retry transient LLM failures with exponential backoff and jitter.
39
+ - Respect `Retry-After` headers from provider errors.
40
+ - Chain named providers or models with explicit fallback behavior.
41
+ - Avoid fallback on non-transient client errors by default.
42
+ - Customize retry and fallback decisions with `shouldRetry` and
43
+ `shouldFallback`.
44
+ - Track token usage and estimated cost.
45
+ - Use custom input/output token pricing through `costCalculator`.
46
+ - Wrap streaming responses with retry-before-first-chunk safety.
47
+ - Track partial stream token usage from provider events.
48
+ - Skip unhealthy providers with `CircuitBreaker`.
49
+ - Set timeout budgets per provider/model.
50
+ - Start hedged requests to reduce tail latency.
51
+ - Pass request `meta` and `payload` through every context for logging.
52
+ - Abort long calls and retry sleeps with `AbortSignal` or `timeoutMs`.
53
+ - Observe attempts, retries, success, failure, and budget events.
54
+ - Strict TypeScript types.
55
+ - ESM package with no runtime dependencies.
56
+
13
57
  ## Quick Start
14
58
 
15
59
  ```ts
@@ -40,64 +84,257 @@ const result = await llmRetry({
40
84
  }
41
85
  },
42
86
  maxRetries: 3,
87
+ initialDelayMs: 1000,
88
+ maxDelayMs: 30000,
43
89
  })
44
90
 
45
91
  console.log(result.data)
46
92
  console.log(result.provider)
93
+ console.log(result.attempts)
47
94
  console.log(result.totalCostUSD)
48
95
  ```
49
96
 
50
- ## Provider Fallback Chain
97
+ ## Complete Provider Fallback Example
51
98
 
52
- Use `providers` when you want explicit model or vendor failover. Each provider
53
- can have its own retry count and cost calculator.
99
+ This example tries OpenAI first, then falls back to Anthropic only for transient
100
+ failures. Client errors like invalid requests or bad credentials stop the chain
101
+ by default.
54
102
 
55
103
  ```ts
104
+ import Anthropic from '@anthropic-ai/sdk'
105
+ import OpenAI from 'openai'
106
+ import { llmRetry } from 'llm-retry-kit'
107
+
108
+ const openai = new OpenAI()
109
+ const anthropic = new Anthropic()
110
+
111
+ const prompt = 'Summarize the following support ticket...'
112
+
56
113
  const result = await llmRetry({
57
114
  providers: [
58
115
  {
59
- name: 'openai:gpt-4o',
60
- fn: async (context) => callOpenAI(context),
116
+ name: 'openai:gpt-4o-mini',
61
117
  maxRetries: 2,
118
+ fn: async ({ signal }) => {
119
+ const response = await openai.chat.completions.create(
120
+ {
121
+ model: 'gpt-4o-mini',
122
+ messages: [{ role: 'user', content: prompt }],
123
+ },
124
+ { signal }
125
+ )
126
+
127
+ return {
128
+ data: response.choices[0]?.message.content ?? '',
129
+ usage: response.usage
130
+ ? {
131
+ promptTokens: response.usage.prompt_tokens,
132
+ completionTokens: response.usage.completion_tokens,
133
+ totalTokens: response.usage.total_tokens,
134
+ }
135
+ : undefined,
136
+ }
137
+ },
62
138
  },
63
139
  {
64
- name: 'anthropic:sonnet',
65
- fn: async (context) => callAnthropic(context),
140
+ name: 'anthropic:claude-sonnet',
66
141
  maxRetries: 1,
142
+ fn: async ({ signal }) => {
143
+ const response = await anthropic.messages.create(
144
+ {
145
+ model: 'claude-sonnet-4-6',
146
+ max_tokens: 1024,
147
+ messages: [{ role: 'user', content: prompt }],
148
+ },
149
+ { signal }
150
+ )
151
+
152
+ const text = response.content
153
+ .filter((block) => block.type === 'text')
154
+ .map((block) => block.text)
155
+ .join('')
156
+
157
+ return {
158
+ data: text,
159
+ usage: {
160
+ promptTokens: response.usage.input_tokens,
161
+ completionTokens: response.usage.output_tokens,
162
+ totalTokens: response.usage.input_tokens + response.usage.output_tokens,
163
+ },
164
+ }
165
+ },
67
166
  },
68
167
  ],
168
+ timeoutMs: 45_000,
69
169
  })
70
170
 
71
- console.log(result.provider)
72
- console.log(result.usedFallback)
171
+ console.log({
172
+ provider: result.provider,
173
+ usedFallback: result.usedFallback,
174
+ attempts: result.attempts,
175
+ answer: result.data,
176
+ })
73
177
  ```
74
178
 
75
- The older `fn` + `fallback` API still works:
179
+ ## Streaming
180
+
181
+ OpenAI and Anthropic both expose streaming APIs, but their event formats and
182
+ resume behavior are provider-specific. `llm-retry-kit` therefore keeps the
183
+ stream wrapper provider-agnostic and conservative:
184
+
185
+ - By default, it retries only if the stream fails before the first chunk.
186
+ - After a chunk has been yielded, retrying could duplicate output, so it stops
187
+ unless you explicitly set `retryMode: 'always'`.
188
+ - Token usage can be tracked from stream events with `getChunkUsage`.
189
+ - Use `chunkUsageMode: 'cumulative'` for providers that send cumulative usage
190
+ snapshots during a stream.
191
+
192
+ ```ts
193
+ import { llmRetryStream } from 'llm-retry-kit'
194
+
195
+ const result = llmRetryStream({
196
+ stream: async ({ signal }) => {
197
+ const stream = await openai.responses.create({
198
+ model: 'gpt-4o-mini',
199
+ input: 'Write a short incident summary.',
200
+ stream: true,
201
+ }, { signal })
202
+
203
+ return stream
204
+ },
205
+ retryMode: 'before-first-chunk',
206
+ getChunkUsage: (event) => {
207
+ if (!('usage' in event) || !event.usage) return undefined
208
+
209
+ return {
210
+ promptTokens: event.usage.input_tokens ?? 0,
211
+ completionTokens: event.usage.output_tokens ?? 0,
212
+ totalTokens: event.usage.total_tokens ?? 0,
213
+ }
214
+ },
215
+ chunkUsageMode: 'cumulative',
216
+ })
217
+
218
+ for await (const event of result.stream) {
219
+ // Send provider events to your UI, parser, or SSE response.
220
+ }
221
+
222
+ console.log(result.getStats())
223
+ ```
224
+
225
+ ## Advanced Production Controls
226
+
227
+ ### Circuit Breaker
228
+
229
+ Keep one `CircuitBreaker` instance per provider/model at application scope. If
230
+ the failure threshold is reached inside the time window, later calls skip that
231
+ provider until the cooldown expires.
232
+
233
+ ```ts
234
+ import { CircuitBreaker, llmRetry } from 'llm-retry-kit'
235
+
236
+ const openaiBreaker = new CircuitBreaker({
237
+ failureThreshold: 5,
238
+ windowMs: 60_000,
239
+ cooldownMs: 120_000,
240
+ })
241
+
242
+ await llmRetry({
243
+ providers: [
244
+ {
245
+ name: 'openai:gpt-4o-mini',
246
+ fn: callOpenAI,
247
+ circuitBreaker: openaiBreaker,
248
+ },
249
+ {
250
+ name: 'anthropic:claude-sonnet',
251
+ fn: callAnthropic,
252
+ },
253
+ ],
254
+ })
255
+ ```
256
+
257
+ ### Per-Provider Timeout
258
+
259
+ Use global `timeoutMs` for the whole workflow and provider `timeoutMs` for a
260
+ single attempt.
261
+
262
+ ```ts
263
+ await llmRetry({
264
+ providers: [
265
+ { name: 'openai:fast', fn: callOpenAI, timeoutMs: 3_000, maxRetries: 1 },
266
+ { name: 'anthropic:steady', fn: callAnthropic, timeoutMs: 10_000 },
267
+ ],
268
+ timeoutMs: 30_000,
269
+ })
270
+ ```
271
+
272
+ ### Hedged Requests
273
+
274
+ Hedging starts the next provider in parallel if the current provider has not
275
+ answered after `hedgeDelayMs`. The first successful response wins and the
276
+ slower request is aborted through the context signal.
277
+
278
+ ```ts
279
+ await llmRetry({
280
+ providers: [
281
+ { name: 'primary', fn: callPrimary },
282
+ { name: 'hedge', fn: callBackup },
283
+ ],
284
+ hedgeDelayMs: 750,
285
+ })
286
+ ```
287
+
288
+ Hedging is best for latency-sensitive read paths. It can increase provider
289
+ traffic, so pair it with budget tracking and conservative delay values.
290
+
291
+ ### Metadata And Payload Tracking
292
+
293
+ Attach request metadata once and it flows into provider calls and hooks.
294
+
295
+ ```ts
296
+ await llmRetry({
297
+ fn: callModel,
298
+ meta: { requestId: 'req_123', tenant: 'acme' },
299
+ payload: { prompt: 'Classify this ticket', userId: 'user_42' },
300
+ onAttempt: (context) => {
301
+ console.log(context.meta, context.payload)
302
+ },
303
+ onFailure: (error, context) => {
304
+ console.error(context.meta, error)
305
+ },
306
+ })
307
+ ```
308
+
309
+ ## Simple Fallback API
310
+
311
+ For smaller apps, `fn` plus `fallback` is still supported.
76
312
 
77
313
  ```ts
78
314
  const result = await llmRetry({
79
315
  fn: async () => callPrimaryModel(),
80
316
  fallback: async () => callFallbackModel(),
317
+ maxRetries: 2,
81
318
  })
82
319
  ```
83
320
 
84
- ## Custom Retry Policy
321
+ ## Configuration
322
+
323
+ ### Retry Timing
85
324
 
86
325
  ```ts
87
- const result = await llmRetry({
326
+ await llmRetry({
88
327
  fn: myLLMCall,
89
- shouldRetry: (error, context) => {
90
- if (error.message.includes('quota exceeded')) return false
91
- return context.retryAttempt < context.maxRetries
92
- },
328
+ maxRetries: 4,
329
+ initialDelayMs: 500,
330
+ maxDelayMs: 60_000,
93
331
  })
94
332
  ```
95
333
 
96
- Without `shouldRetry`, the package retries common transient failures such as
97
- HTTP `408`, `409`, `429`, `5xx`, Anthropic `529`, timeout, network, and
98
- overload errors.
334
+ Retries use exponential backoff with jitter. If the provider exposes a
335
+ `Retry-After` header, that delay is preferred.
99
336
 
100
- ## Timeouts And Cancellation
337
+ ### Timeout And Cancellation
101
338
 
102
339
  ```ts
103
340
  const controller = new AbortController()
@@ -109,36 +346,81 @@ const result = await llmRetry({
109
346
  })
110
347
  ```
111
348
 
112
- `timeoutMs` aborts the wrapper and retry waits. Passing `signal` into your SDK
113
- call lets the underlying HTTP request stop too, when the SDK supports it.
349
+ `timeoutMs` aborts the wrapper and retry sleeps. Passing `signal` into your SDK
350
+ call also lets the underlying request stop when the SDK supports it.
114
351
 
115
- ## Budget Tracking
116
-
117
- Simple mode:
352
+ ### Budget Tracking
118
353
 
119
354
  ```ts
120
355
  const result = await llmRetry({
121
356
  fn: myLLMCall,
122
357
  maxCostUSD: 0.5,
123
358
  costPer1kTokens: 0.002,
359
+ onBudgetExceeded: (spent, limit) => {
360
+ console.warn(`Budget exceeded: $${spent.toFixed(4)} / $${limit}`)
361
+ },
124
362
  })
125
363
  ```
126
364
 
127
- Provider pricing is often more nuanced than one flat token price, so production
128
- apps should prefer `costCalculator`:
365
+ For real provider pricing, prefer `costCalculator`:
129
366
 
130
367
  ```ts
131
368
  const result = await llmRetry({
132
369
  fn: myLLMCall,
133
370
  costCalculator: (usage) => {
134
- const input = usage.promptTokens * 0.00000015
135
- const output = usage.completionTokens * 0.0000006
136
- return input + output
371
+ const inputCost = usage.promptTokens * 0.00000015
372
+ const outputCost = usage.completionTokens * 0.0000006
373
+ return inputCost + outputCost
374
+ },
375
+ })
376
+ ```
377
+
378
+ Budget tracking is based on the `usage` object returned by your function. A
379
+ wrapper cannot know the final cost of an in-flight LLM call before the provider
380
+ returns usage, so `maxCostUSD` is a guard for later attempts and fallback calls.
381
+
382
+ ### Custom Retry Policy
383
+
384
+ Use `context.defaultShouldRetry` to compose with the built-in transient error
385
+ detection.
386
+
387
+ ```ts
388
+ await llmRetry({
389
+ fn: myLLMCall,
390
+ shouldRetry: (error, context) => {
391
+ if (error.message.includes('insufficient quota')) return false
392
+ return context.defaultShouldRetry
393
+ },
394
+ })
395
+ ```
396
+
397
+ By default, `llm-retry-kit` retries common transient failures such as HTTP
398
+ `408`, `409`, `429`, `5xx`, Anthropic `529`, timeout, network, and overload
399
+ errors.
400
+
401
+ ### Custom Fallback Policy
402
+
403
+ Fallback is a separate decision from retry. By default, fallback is allowed only
404
+ after transient failures. If you intentionally want to fallback for a known
405
+ client-side case, opt in explicitly.
406
+
407
+ ```ts
408
+ await llmRetry({
409
+ providers: [
410
+ { name: 'small-context-model', fn: callSmallModel },
411
+ { name: 'large-context-model', fn: callLargeModel },
412
+ ],
413
+ shouldFallback: (error, context) => {
414
+ if (error.message.includes('context length')) {
415
+ return context.nextProvider === 'large-context-model'
416
+ }
417
+
418
+ return context.defaultShouldFallback
137
419
  },
138
420
  })
139
421
  ```
140
422
 
141
- ## Observability
423
+ ### Observability
142
424
 
143
425
  ```ts
144
426
  await llmRetry({
@@ -153,13 +435,13 @@ await llmRetry({
153
435
  onSuccess: (context) => {
154
436
  console.log(`Cost so far: $${context.totalCostUSD}`)
155
437
  },
156
- onFailure: (error) => {
157
- console.error(error)
438
+ onFailure: (error, context) => {
439
+ console.error(context.meta, error)
158
440
  },
159
441
  })
160
442
  ```
161
443
 
162
- ## API
444
+ ## API Reference
163
445
 
164
446
  ### `llmRetry(options)`
165
447
 
@@ -175,14 +457,64 @@ await llmRetry({
175
457
  | `initialDelayMs` | `number` | `1000` | Initial retry delay. |
176
458
  | `maxDelayMs` | `number` | `30000` | Maximum retry delay. |
177
459
  | `timeoutMs` | `number` | optional | Abort wrapper after this time. |
460
+ | `hedgeDelayMs` | `number` | optional | Start the next provider after this delay if the current provider is still pending. |
178
461
  | `signal` | `AbortSignal` | optional | External cancellation signal. |
462
+ | `meta` | `unknown` | optional | User metadata copied into attempt/failure contexts. |
463
+ | `payload` | `unknown` | optional | Request payload copied into attempt/failure contexts. |
179
464
  | `shouldRetry` | `(error, context) => boolean \| Promise<boolean>` | optional | Override retry decisions. |
465
+ | `shouldFallback` | `(error, context) => boolean \| Promise<boolean>` | optional | Override provider fallback decisions. |
180
466
  | `onAttempt` | `(context) => void` | optional | Called before each attempt. |
181
467
  | `onRetry` | `(attempt, error, delayMs, context) => void` | optional | Called before retry wait. |
182
468
  | `onSuccess` | `(context) => void` | optional | Called after a successful response. |
183
- | `onFailure` | `(error) => void` | optional | Called before final failure is thrown. |
469
+ | `onFailure` | `(error, context) => void` | optional | Called before final failure is thrown. |
184
470
  | `onBudgetExceeded` | `(spentUSD, limitUSD) => void` | optional | Called when budget is exhausted. |
185
471
 
472
+ ### `RetryProvider<T>`
473
+
474
+ ```ts
475
+ {
476
+ name: string
477
+ fn: (context: RetryAttemptContext) => Promise<LLMResponse<T>>
478
+ maxRetries?: number
479
+ timeoutMs?: number
480
+ hedgeDelayMs?: number
481
+ circuitBreaker?: CircuitBreaker | CircuitBreakerOptions
482
+ costPer1kTokens?: number
483
+ costCalculator?: (usage, context) => number
484
+ }
485
+ ```
486
+
487
+ ### `llmRetryStream(options)`
488
+
489
+ Returns `{ stream, getStats }`. The request begins when the returned async
490
+ iterable is consumed.
491
+
492
+ | Option | Type | Default | Description |
493
+ | --- | --- | --- | --- |
494
+ | `stream` | `(context) => AsyncIterable<TChunk> \| Promise<AsyncIterable<TChunk>>` | optional | Primary stream call for the simple API. |
495
+ | `fallbackStream` | `(context) => AsyncIterable<TChunk> \| Promise<AsyncIterable<TChunk>>` | optional | Backup stream call. |
496
+ | `providers` | `StreamRetryProvider<TChunk>[]` | optional | Explicit stream provider chain. |
497
+ | `retryMode` | `'before-first-chunk' \| 'always' \| 'never'` | `'before-first-chunk'` | Controls whether interrupted streams are retried. |
498
+ | `getChunkUsage` | `(chunk, context) => TokenUsage \| undefined` | optional | Extract token usage from stream chunks/events. |
499
+ | `chunkUsageMode` | `'delta' \| 'cumulative'` | `'delta'` | Interpret chunk usage as incremental or cumulative. |
500
+ | `maxRetries` | `number` | `3` | Retries after the first attempt. |
501
+ | `timeoutMs` | `number` | optional | Abort the whole stream workflow after this time. |
502
+ | `meta` | `unknown` | optional | User metadata copied into contexts. |
503
+ | `payload` | `unknown` | optional | Request payload copied into contexts. |
504
+
505
+ ### `CircuitBreaker`
506
+
507
+ ```ts
508
+ new CircuitBreaker({
509
+ failureThreshold: 5,
510
+ windowMs: 60_000,
511
+ cooldownMs: 120_000,
512
+ })
513
+ ```
514
+
515
+ `snapshot()` returns `{ state, failures, openedAt }`, where state is
516
+ `'closed'`, `'open'`, or `'half_open'`.
517
+
186
518
  ### `RetryResult<T>`
187
519
 
188
520
  ```ts
@@ -196,11 +528,43 @@ await llmRetry({
196
528
  }
197
529
  ```
198
530
 
199
- ## Notes
531
+ ### `LLMRetryError`
200
532
 
201
- Budget tracking is based on the `usage` object returned by your function. A
202
- wrapper cannot know the final cost of an in-flight LLM call before the provider
203
- returns usage, so `maxCostUSD` is a guard for later attempts and fallback calls.
533
+ ```ts
534
+ {
535
+ name: 'LLMRetryError'
536
+ primaryError: Error | null
537
+ fallbackError: Error | null
538
+ totalCostUSD: number
539
+ totalTokens: number
540
+ attempts: number
541
+ providers: string[]
542
+ reason: 'failure' | 'budget_exceeded' | 'aborted'
543
+ }
544
+ ```
545
+
546
+ ## Defaults
547
+
548
+ | Setting | Default |
549
+ | --- | --- |
550
+ | `maxRetries` | `3` |
551
+ | `initialDelayMs` | `1000` |
552
+ | `maxDelayMs` | `30000` |
553
+ | `costPer1kTokens` | `0.002` |
554
+ | stream retry mode | `before-first-chunk` |
555
+ | fallback on client errors | `false` |
556
+ | fallback on transient errors | `true` |
557
+ | runtime dependencies | none |
558
+
559
+ ## Development
560
+
561
+ ```bash
562
+ npm install
563
+ npm run typecheck
564
+ npm test
565
+ npm run build
566
+ npm pack --dry-run
567
+ ```
204
568
 
205
569
  ## License
206
570
 
@@ -1 +1 @@
1
- {"version":3,"file":"backoff.d.ts","sourceRoot":"","sources":["../src/backoff.ts"],"names":[],"mappings":"AAAA,wBAAgB,gBAAgB,CAC9B,OAAO,EAAE,MAAM,EACf,cAAc,EAAE,MAAM,EACtB,UAAU,EAAE,MAAM,GACjB,MAAM,CAMR;AAED,wBAAgB,KAAK,CAAC,EAAE,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,WAAW,GAAG,OAAO,CAAC,IAAI,CAAC,CAiBrE;AAED,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,OAAO,GAAG,OAAO,CA2DxD;AAED,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,GAAG,IAAI,CA4B/D;AAED,wBAAgB,gBAAgB,IAAI,KAAK,CAIxC"}
1
+ {"version":3,"file":"backoff.d.ts","sourceRoot":"","sources":["../src/backoff.ts"],"names":[],"mappings":"AAAA,wBAAgB,gBAAgB,CAC9B,OAAO,EAAE,MAAM,EACf,cAAc,EAAE,MAAM,EACtB,UAAU,EAAE,MAAM,GACjB,MAAM,CAMR;AAED,wBAAgB,KAAK,CAAC,EAAE,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,WAAW,GAAG,OAAO,CAAC,IAAI,CAAC,CAiBrE;AAED,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,OAAO,GAAG,OAAO,CA2DxD;AAED,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,GAAG,IAAI,CAkC/D;AAED,wBAAgB,gBAAgB,IAAI,KAAK,CAIxC"}
package/dist/backoff.js CHANGED
@@ -78,11 +78,16 @@ export function extractRetryAfter(error) {
78
78
  if (!headerValue) {
79
79
  return null;
80
80
  }
81
- const seconds = Number(headerValue);
81
+ const trimmedHeaderValue = headerValue.trim();
82
+ const isNumericDelay = /^\d+(\.\d+)?$/.test(trimmedHeaderValue);
83
+ const seconds = Number(trimmedHeaderValue);
82
84
  if (Number.isFinite(seconds)) {
83
85
  return seconds * 1000;
84
86
  }
85
- const dateMs = Date.parse(headerValue);
87
+ if (isNumericDelay) {
88
+ return null;
89
+ }
90
+ const dateMs = Date.parse(trimmedHeaderValue);
86
91
  if (Number.isFinite(dateMs)) {
87
92
  return Math.max(dateMs - Date.now(), 0);
88
93
  }
@@ -1 +1 @@
1
- {"version":3,"file":"backoff.js","sourceRoot":"","sources":["../src/backoff.ts"],"names":[],"mappings":"AAAA,MAAM,UAAU,gBAAgB,CAC9B,OAAe,EACf,cAAsB,EACtB,UAAkB;IAElB,MAAM,WAAW,GAAG,cAAc,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,OAAO,CAAC,CAAA;IACzD,MAAM,MAAM,GAAG,WAAW,GAAG,IAAI,GAAG,CAAC,IAAI,CAAC,MAAM,EAAE,GAAG,CAAC,GAAG,CAAC,CAAC,CAAA;IAC3D,MAAM,KAAK,GAAG,WAAW,GAAG,MAAM,CAAA;IAElC,OAAO,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,GAAG,CAAC,KAAK,EAAE,cAAc,CAAC,EAAE,UAAU,CAAC,CAAA;AAC9D,CAAC;AAED,MAAM,UAAU,KAAK,CAAC,EAAU,EAAE,MAAoB;IACpD,IAAI,MAAM,EAAE,OAAO,EAAE,CAAC;QACpB,OAAO,OAAO,CAAC,MAAM,CAAC,gBAAgB,EAAE,CAAC,CAAA;IAC3C,CAAC;IAED,OAAO,IAAI,OAAO,CAAC,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE;QACrC,MAAM,OAAO,GAAG,UAAU,CAAC,OAAO,EAAE,EAAE,CAAC,CAAA;QAEvC,MAAM,EAAE,gBAAgB,CACtB,OAAO,EACP,GAAG,EAAE;YACH,YAAY,CAAC,OAAO,CAAC,CAAA;YACrB,MAAM,CAAC,gBAAgB,EAAE,CAAC,CAAA;QAC5B,CAAC,EACD,EAAE,IAAI,EAAE,IAAI,EAAE,CACf,CAAA;IACH,CAAC,CAAC,CAAA;AACJ,CAAC;AAED,MAAM,UAAU,gBAAgB,CAAC,KAAc;IAC7C,IAAI,CAAC,CAAC,KAAK,YAAY,KAAK,CAAC;QAAE,OAAO,KAAK,CAAA;IAE3C,MAAM,GAAG,GAAG,KAIX,CAAA;IAED,MAAM,MAAM,GAAG,QAAQ,CAAC,GAAG,CAAC,MAAM,IAAI,GAAG,CAAC,UAAU,CAAC,CAAA;IACrD,IACE,MAAM,KAAK,GAAG;QACd,MAAM,KAAK,GAAG;QACd,MAAM,KAAK,GAAG;QACd,CAAC,MAAM,KAAK,IAAI,IAAI,MAAM,IAAI,GAAG,IAAI,MAAM,IAAI,GAAG,CAAC,EACnD,CAAC;QACD,OAAO,IAAI,CAAA;IACb,CAAC;IAED,MAAM,IAAI,GAAG,OAAO,GAAG,CAAC,IAAI,KAAK,QAAQ,CAAC,CAAC,CAAC,GAAG,CAAC,IAAI,CAAC,WAAW,EAAE,CAAC,CAAC,CAAC,EAAE,CAAA;IACvE,IACE;QACE,WAAW;QACX,YAAY;QACZ,cAAc;QACd,WAAW;QACX,WAAW;QACX,qBAAqB;QACrB,UAAU;QACV,kBAAkB;KACnB,CAAC,QAAQ,CAAC,IAAI,CAAC,EAChB,CAAC;QACD,OAAO,IAAI,CAAA;IACb,CAAC;IAED,MAAM,OAAO,GAAG,KAAK,CAAC,OAAO,CAAC,WAAW,EAAE,CAAA;IAC3C,MAAM,iBAAiB,GAAG;QACxB,YAAY;QACZ,YAAY;QACZ,mBAAmB;QACnB,KAAK;QACL,KAAK;QACL,KAAK;QACL,cAAc;QACd,KAAK;QACL,KAAK;QACL,KAAK;QACL,KAAK;QACL,SAAS;QACT,WAAW;QACX,YAAY;QACZ,cAAc;QACd,SAAS;QACT,QAAQ;QACR,YAAY;QACZ,kBAAkB;KACnB,CAAA;IAED,OAAO,iBAAiB,CAAC,IAAI,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAC,CAAC,CAAA;AACvE,CAAC;AAED,MAAM,UAAU,iBAAiB,CAAC,KAAc;IAC9C,IAAI,CAAC,KAAK,IAAI,OAAO,KAAK,KAAK,QAAQ;QAAE,OAAO,IAAI,CAAA;IAEpD,MAAM,GAAG,GAAG,KAAgC,CAAA;IAC5C,MAAM,UAAU,GAAG,QAAQ,CAAC,GAAG,CAAC,YAAY,CAAC,IAAI,GAAG,CAAC,aAAa,CAAC,CAAC,CAAA;IACpE,IAAI,UAAU,KAAK,IAAI,EAAE,CAAC;QACxB,OAAO,UAAU,GAAG,IAAI,CAAA;IAC1B,CAAC;IAED,MAAM,QAAQ,GAAG,GAAG,CAAC,UAAU,CAAwC,CAAA;IACvE,MAAM,WAAW,GACf,SAAS,CAAC,GAAG,CAAC,SAAS,CAAC,EAAE,aAAa,CAAC;QACxC,SAAS,CAAC,QAAQ,EAAE,CAAC,SAAS,CAAC,EAAE,aAAa,CAAC,CAAA;IACjD,IAAI,CAAC,WAAW,EAAE,CAAC;QACjB,OAAO,IAAI,CAAA;IACb,CAAC;IAED,MAAM,OAAO,GAAG,MAAM,CAAC,WAAW,CAAC,CAAA;IACnC,IAAI,MAAM,CAAC,QAAQ,CAAC,OAAO,CAAC,EAAE,CAAC;QAC7B,OAAO,OAAO,GAAG,IAAI,CAAA;IACvB,CAAC;IAED,MAAM,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,WAAW,CAAC,CAAA;IACtC,IAAI,MAAM,CAAC,QAAQ,CAAC,MAAM,CAAC,EAAE,CAAC;QAC5B,OAAO,IAAI,CAAC,GAAG,CAAC,MAAM,GAAG,IAAI,CAAC,GAAG,EAAE,EAAE,CAAC,CAAC,CAAA;IACzC,CAAC;IAED,OAAO,IAAI,CAAA;AACb,CAAC;AAED,MAAM,UAAU,gBAAgB;IAC9B,MAAM,KAAK,GAAG,IAAI,KAAK,CAAC,2BAA2B,CAAC,CAAA;IACpD,KAAK,CAAC,IAAI,GAAG,YAAY,CAAA;IACzB,OAAO,KAAK,CAAA;AACd,CAAC;AAED,SAAS,QAAQ,CAAC,KAAc;IAC9B,IAAI,OAAO,KAAK,KAAK,QAAQ,IAAI,MAAM,CAAC,QAAQ,CAAC,KAAK,CAAC,EAAE,CAAC;QACxD,OAAO,KAAK,CAAA;IACd,CAAC;IAED,IAAI,OAAO,KAAK,KAAK,QAAQ,IAAI,KAAK,CAAC,IAAI,EAAE,KAAK,EAAE,EAAE,CAAC;QACrD,MAAM,MAAM,GAAG,MAAM,CAAC,KAAK,CAAC,CAAA;QAC5B,OAAO,MAAM,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,IAAI,CAAA;IAChD,CAAC;IAED,OAAO,IAAI,CAAA;AACb,CAAC;AAED,SAAS,SAAS,CAAC,OAAgB,EAAE,IAAY;IAC/C,IAAI,CAAC,OAAO,IAAI,OAAO,OAAO,KAAK,QAAQ;QAAE,OAAO,IAAI,CAAA;IAExD,IAAI,KAAK,IAAI,OAAO,IAAI,OAAO,OAAO,CAAC,GAAG,KAAK,UAAU,EAAE,CAAC;QAC1D,MAAM,KAAK,GAAG,OAAO,CAAC,GAAG,CAAC,IAAI,CAAC,CAAA;QAC/B,OAAO,OAAO,KAAK,KAAK,QAAQ,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,IAAI,CAAA;IACjD,CAAC;IAED,MAAM,MAAM,GAAG,OAAkC,CAAA;IACjD,MAAM,UAAU,GAAG,MAAM,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,IAAI,CACzC,CAAC,GAAG,EAAE,EAAE,CAAC,GAAG,CAAC,WAAW,EAAE,KAAK,IAAI,CAAC,WAAW,EAAE,CAClD,CAAA;IAED,IAAI,CAAC,UAAU;QAAE,OAAO,IAAI,CAAA;IAE5B,MAAM,KAAK,GAAG,MAAM,CAAC,UAAU,CAAC,CAAA;IAChC,OAAO,OAAO,KAAK,KAAK,QAAQ,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,IAAI,CAAA;AACjD,CAAC"}
1
+ {"version":3,"file":"backoff.js","sourceRoot":"","sources":["../src/backoff.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,17 @@
1
+ import type { CircuitBreakerOptions, CircuitBreakerSnapshot } from './types.js';
2
+ export declare class CircuitBreaker {
3
+ private readonly options;
4
+ private failureTimestamps;
5
+ private state;
6
+ private openedAt;
7
+ constructor(options: CircuitBreakerOptions);
8
+ canRequest(): boolean;
9
+ recordSuccess(): void;
10
+ recordFailure(): void;
11
+ snapshot(): CircuitBreakerSnapshot;
12
+ }
13
+ export declare class CircuitBreakerOpenError extends Error {
14
+ readonly provider: string;
15
+ constructor(provider: string);
16
+ }
17
+ //# sourceMappingURL=circuit-breaker.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"circuit-breaker.d.ts","sourceRoot":"","sources":["../src/circuit-breaker.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,qBAAqB,EACrB,sBAAsB,EAEvB,MAAM,YAAY,CAAA;AAEnB,qBAAa,cAAc;IAKb,OAAO,CAAC,QAAQ,CAAC,OAAO;IAJpC,OAAO,CAAC,iBAAiB,CAAe;IACxC,OAAO,CAAC,KAAK,CAAgC;IAC7C,OAAO,CAAC,QAAQ,CAAsB;gBAET,OAAO,EAAE,qBAAqB;IAI3D,UAAU,IAAI,OAAO;IAcrB,aAAa,IAAI,IAAI;IAMrB,aAAa,IAAI,IAAI;IAiBrB,QAAQ,IAAI,sBAAsB;CAOnC;AAED,qBAAa,uBAAwB,SAAQ,KAAK;aACpB,QAAQ,EAAE,MAAM;gBAAhB,QAAQ,EAAE,MAAM;CAI7C"}