@rohan9024/retrykit 0.1.0 → 0.2.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,8 +1,24 @@
1
+
1
2
  # @rohan9024/retrykit ⚡
2
3
 
3
- > Zero-dependency, header-aware retry wrapper tailored for LLM APIs and fetch.
4
+ > A zero-dependency, header-aware retry engine and circuit breaker built for reliable LLM API integrations.
5
+
6
+ [![npm version](https://img.shields.io/npm/v/@rohan9024/retrykit.svg)](https://www.npmjs.com/package/@rohan9024/retrykit)
7
+ [![license](https://img.shields.io/npm/l/@rohan9024/retrykit.svg)](https://github.com/rohan9024/retrykit)
8
+ [![bundle size](https://img.shields.io/bundlephobia/min/@rohan9024/retrykit)](https://bundlephobia.com/package/@rohan9024/retrykit)
9
+
10
+ **RetryKit** helps developers build resilient applications powered by LLM APIs such as OpenAI, Anthropic, DeepSeek, Groq, and Mistral.
4
11
 
5
- Automatically handles `429 Too Many Requests` by parsing rate-limit headers from OpenAI, Anthropic, and Groq before falling back to exponential backoff with full jitter.
12
+ Unlike basic retry libraries such as `p-retry` or `tenacity`, RetryKit understands provider-specific rate-limit headers and uses that information to schedule smarter retries.
13
+
14
+ ### Why RetryKit?
15
+
16
+ - **Header-aware retries:** Parses provider-specific rate-limit reset headers.
17
+ - **Exponential backoff with jitter:** Prevents synchronized retry storms.
18
+ - **SSE stream resilience:** Handles mid-stream connection interruptions.
19
+ - **Circuit breaker:** Prevents cascading failures during provider outages.
20
+ - **Timeout protection:** Supports per-attempt request timeouts.
21
+ - **Zero dependencies:** Lightweight and dependency-free.
6
22
 
7
23
  ---
8
24
 
@@ -14,40 +30,47 @@ npm install @rohan9024/retrykit
14
30
 
15
31
  ---
16
32
 
17
- ## 🚀 Usage
33
+ ## 🚀 Quick Start
18
34
 
19
- ### 1. Drop-in Replacement for `fetch` (`retryFetch`)
35
+ ### 1. Drop-in Fetch Replacement (`retryFetch`)
20
36
 
21
- `retryFetch` works exactly like standard `fetch`, but automatically handles retries and honors rate-limit headers:
37
+ Use `retryFetch` just like standard `fetch`, with automatic retries and rate-limit awareness.
22
38
 
23
39
  ```typescript
24
40
  import { retryFetch } from '@rohan9024/retrykit';
25
41
 
26
- const response = await retryFetch('https://api.openai.com/v1/chat/completions', {
27
- method: 'POST',
28
- headers: {
29
- 'Authorization': `Bearer ${process.env.OPENAI_API_KEY}`,
30
- 'Content-Type': 'application/json',
31
- },
32
- body: JSON.stringify({
33
- model: 'gpt-4o',
34
- messages: [{ role: 'user', content: 'Say hello!' }],
35
- }),
36
- }, {
37
- retries: 3,
38
- onRetry: (error, attempt, delayMs) => {
39
- console.warn(`Retry attempt #${attempt} after ${delayMs}ms delay...`);
42
+ const response = await retryFetch(
43
+ 'https://api.openai.com/v1/chat/completions',
44
+ {
45
+ method: 'POST',
46
+ headers: {
47
+ Authorization: `Bearer ${process.env.OPENAI_API_KEY}`,
48
+ 'Content-Type': 'application/json',
49
+ },
50
+ body: JSON.stringify({
51
+ model: 'gpt-4o',
52
+ messages: [
53
+ { role: 'user', content: 'Say hello!' }
54
+ ],
55
+ }),
40
56
  },
41
- });
57
+ {
58
+ retries: 4,
59
+ timeoutMs: 15000,
60
+ onRetry: (error, attempt, delayMs) => {
61
+ console.warn(
62
+ `Retry #${attempt}: Waiting ${delayMs}ms...`
63
+ );
64
+ },
65
+ }
66
+ );
42
67
 
43
68
  const data = await response.json();
44
69
  ```
45
70
 
46
- ---
47
-
48
71
  ### 2. Wrap Official SDKs (`retry`)
49
72
 
50
- Wrap calls from the official OpenAI, Anthropic, or Mistral client libraries:
73
+ Use RetryKit with existing SDKs such as OpenAI, Anthropic, Mistral, or Google.
51
74
 
52
75
  ```typescript
53
76
  import OpenAI from 'openai';
@@ -55,43 +78,113 @@ import { retry } from '@rohan9024/retrykit';
55
78
 
56
79
  const openai = new OpenAI();
57
80
 
58
- const completion = await retry(() =>
59
- openai.chat.completions.create({
60
- model: 'gpt-4o',
61
- messages: [{ role: 'user', content: 'Write a haiku' }],
62
- }),
81
+ const completion = await retry(
82
+ () =>
83
+ openai.chat.completions.create({
84
+ model: 'gpt-4o',
85
+ messages: [
86
+ {
87
+ role: 'user',
88
+ content: 'Write a poem about resilience',
89
+ },
90
+ ],
91
+ }),
63
92
  {
64
- retries: 4,
93
+ retries: 3,
65
94
  baseDelayMs: 500,
66
95
  }
67
96
  );
68
97
  ```
69
98
 
99
+ ### 3. Resilient Token Streaming (`retryStream`)
100
+
101
+ LLM streaming connections can fail midway through generation. `retryStream` helps recover from interrupted streams.
102
+
103
+ ```typescript
104
+ import { retryStream } from '@rohan9024/retrykit';
105
+
106
+ async function* tokenGenerator() {
107
+ const stream = await fetchLLMStream();
108
+
109
+ for await (const chunk of stream) {
110
+ yield chunk;
111
+ }
112
+ }
113
+
114
+ for await (const chunk of retryStream(() => tokenGenerator())) {
115
+ process.stdout.write(chunk);
116
+ }
117
+ ```
118
+
119
+ ### 4. Circuit Breaker
120
+
121
+ Protect your application from cascading failures and unnecessary requests when an AI provider experiences an outage.
122
+
123
+ ```typescript
124
+ import { CircuitBreaker } from '@rohan9024/retrykit';
125
+
126
+ const breaker = new CircuitBreaker({
127
+ failureThreshold: 3,
128
+ resetTimeoutMs: 30000,
129
+
130
+ onStateChange: (from, to) => {
131
+ console.log(`Circuit changed: ${from} -> ${to}`);
132
+ },
133
+ });
134
+
135
+ // Fails fast when the circuit is open
136
+ const result = await breaker.execute(() => callUnstableAPI());
137
+ ```
138
+
70
139
  ---
71
140
 
72
- ## 🧠 Provider Header Awareness
141
+ ## 🧠 Supported Provider Headers
73
142
 
74
- `retrykit` inspects and obeys the following provider headers before falling back to exponential backoff:
143
+ RetryKit automatically extracts retry timing information from commonly used API rate-limit headers.
75
144
 
76
- - `retry-after` (RFC 1123 HTTP Dates or relative seconds)
77
- - `x-ratelimit-reset-requests` (OpenAI format, e.g., `"15s"`, `"600ms"`)
78
- - `x-ratelimit-reset-tokens` (Anthropic format)
145
+ | Provider | Supported Headers |
146
+ |---|---|
147
+ | OpenAI | `x-ratelimit-reset-requests`, `x-ratelimit-reset-tokens`, `retry-after` |
148
+ | Anthropic | `anthropic-ratelimit-requests-reset`, `anthropic-ratelimit-tokens-reset`, `retry-after` |
149
+ | Groq / DeepSeek | `x-ratelimit-reset-total`, `retry-after-ms`, `retry-after` |
150
+ | Standard HTTP | `Retry-After` (integer seconds or RFC 1123 HTTP dates) |
79
151
 
80
152
  ---
81
153
 
82
- ## ⚙️ Options Reference
154
+ ## ⚙️ Configuration Options
155
+
156
+ RetryKit provides configurable retry behavior.
83
157
 
84
158
  | Option | Type | Default | Description |
85
- | :--- | :--- | :--- | :--- |
86
- | `retries` | `number` | `3` | Max retry attempts before failing |
87
- | `baseDelayMs` | `number` | `500` | Starting backoff delay in ms |
88
- | `maxDelayMs` | `number` | `30000` | Ceiling delay cap in ms |
89
- | `retryOnStatus`| `number[]` | `[408, 429, 500, 502, 503, 504]` | HTTP status codes that trigger a retry |
90
- | `onRetry` | `(err, attempt, delay) => void` | `undefined` | Hook called before every sleep |
91
- | `signal` | `AbortSignal` | `undefined` | Immediate cancellation controller |
159
+ |---|---|---|---|
160
+ | `retries` | `number` | `3` | Maximum retry attempts before failing |
161
+ | `baseDelayMs` | `number` | `500` | Initial backoff delay in milliseconds |
162
+ | `maxDelayMs` | `number` | `30000` | Maximum backoff delay |
163
+ | `timeoutMs` | `number` | `undefined` | Per-attempt timeout in milliseconds |
164
+ | `retryOnStatus` | `number[]` | `[408, 429, 500, 502, 503, 504]` | HTTP status codes that trigger retries |
165
+ | `shouldRetry` | `(err, attempt) => boolean` | `undefined` | Custom retry decision logic |
166
+ | `onRetry` | `(err, attempt, delay) => void` | `undefined` | Callback executed before each retry |
167
+ | `signal` | `AbortSignal` | `undefined` | Abort signal for immediate cancellation |
168
+
169
+ ---
170
+
171
+ ## 🛡️ Designed For
172
+
173
+ - LLM-powered applications
174
+ - AI agents and autonomous workflows
175
+ - RAG pipelines
176
+ - High-volume API integrations
177
+ - Streaming chat applications
178
+ - Production AI infrastructure
92
179
 
93
180
  ---
94
181
 
95
182
  ## 📄 License
96
183
 
97
- MIT
184
+ MIT © [rohan9024](https://github.com/rohan9024)
185
+
186
+ ---
187
+
188
+ <p align="center">
189
+ Built for developers who want their AI applications to be resilient. ⚡
190
+ </p>
package/dist/index.d.mts CHANGED
@@ -5,19 +5,61 @@ interface RetryOptions {
5
5
  baseDelayMs?: number;
6
6
  /** Maximum backoff ceiling delay in milliseconds. Default: 30000 */
7
7
  maxDelayMs?: number;
8
+ /** Per-request timeout in milliseconds. Default: undefined */
9
+ timeoutMs?: number;
8
10
  /** HTTP status codes that trigger a retry. Default: [408, 429, 500, 502, 503, 504] */
9
11
  retryOnStatus?: number[];
10
- /** Optional callback fired on every retry attempt before sleep */
12
+ /** Custom condition to decide whether to retry */
13
+ shouldRetry?: (error: unknown, attempt: number) => boolean;
14
+ /** Optional callback fired before every retry sleep */
11
15
  onRetry?: (error: unknown, attempt: number, delayMs: number) => void;
12
16
  /** AbortSignal for immediate cancellation */
13
17
  signal?: AbortSignal;
14
18
  }
19
+ interface CircuitBreakerOptions {
20
+ /** Number of consecutive failures before opening the circuit. Default: 5 */
21
+ failureThreshold?: number;
22
+ /** Time in milliseconds to wait before attempting recovery (HALF_OPEN). Default: 30000 */
23
+ resetTimeoutMs?: number;
24
+ /** Optional callback triggered on circuit state changes */
25
+ onStateChange?: (from: CircuitState, to: CircuitState) => void;
26
+ }
27
+ type CircuitState = 'CLOSED' | 'OPEN' | 'HALF_OPEN';
15
28
  type HeaderSource = Headers | Record<string, string | string[] | undefined> | [string, string][] | undefined;
16
29
 
17
30
  declare function retryFetch(url: string | URL | Request, init?: RequestInit, options?: RetryOptions): Promise<Response>;
18
31
 
19
32
  declare function retry<T>(fn: (attempt: number) => Promise<T>, options?: RetryOptions): Promise<T>;
20
33
 
34
+ /**
35
+ * Resilient wrapper for Server-Sent Events / LLM chunk streams.
36
+ * If the streaming connection drops mid-generation, it retries and resumes.
37
+ */
38
+ declare function retryStream<T>(streamFactory: (attempt: number) => Promise<AsyncIterable<T>>, options?: RetryOptions): AsyncGenerator<T, void, unknown>;
39
+
40
+ declare class CircuitBreakerOpenError extends Error {
41
+ constructor(message?: string);
42
+ }
43
+ /**
44
+ * Zero-dependency Circuit Breaker implementation to stop cascading failure
45
+ * cascades when downstream LLM APIs experience total outages.
46
+ */
47
+ declare class CircuitBreaker {
48
+ private state;
49
+ private failureCount;
50
+ private lastFailureTime;
51
+ private readonly failureThreshold;
52
+ private readonly resetTimeoutMs;
53
+ private readonly onStateChange?;
54
+ constructor(options?: CircuitBreakerOptions);
55
+ getState(): CircuitState;
56
+ execute<T>(fn: () => Promise<T>): Promise<T>;
57
+ private recordSuccess;
58
+ private recordFailure;
59
+ private transitionTo;
60
+ reset(): void;
61
+ }
62
+
21
63
  declare const DEFAULT_RETRYABLE_STATUS_CODES: number[];
22
64
  /**
23
65
  * Calculates exponential backoff with full jitter:
@@ -29,19 +71,20 @@ declare function computeJitterDelay(attempt: number, baseDelayMs?: number, maxDe
29
71
  */
30
72
  declare function parseDurationToMs(value: string): number | null;
31
73
  /**
32
- * Inspects rate-limiting headers and extracts sleep delay in milliseconds:
33
- * - retry-after (seconds or HTTP date)
34
- * - x-ratelimit-reset-requests (OpenAI format)
35
- * - x-ratelimit-reset-tokens (Anthropic format)
74
+ * Exhaustively checks provider headers across:
75
+ * - RFC 7231 / RFC 6585 standard `retry-after`
76
+ * - OpenAI: `x-ratelimit-reset-requests`, `x-ratelimit-reset-tokens`
77
+ * - Anthropic: `anthropic-ratelimit-requests-reset`, `anthropic-ratelimit-tokens-reset`
78
+ * - Groq / DeepSeek / Mistral: `x-ratelimit-reset-total`, `retry-after-ms`
36
79
  */
37
80
  declare function parseRateLimitDelay(headers: HeaderSource): number | null;
38
81
  /**
39
- * Detects transient network drop errors.
82
+ * Detects transient network drop errors across Node, Bun, Deno, and browsers.
40
83
  */
41
84
  declare function isTransientNetworkError(error: unknown): boolean;
42
85
  /**
43
- * Sleep helper respecting AbortSignal cancellation.
86
+ * Promisified sleep that respects AbortSignal cancellation.
44
87
  */
45
88
  declare function sleep(ms: number, signal?: AbortSignal): Promise<void>;
46
89
 
47
- export { DEFAULT_RETRYABLE_STATUS_CODES, type HeaderSource, type RetryOptions, computeJitterDelay, isTransientNetworkError, parseDurationToMs, parseRateLimitDelay, retry, retryFetch, sleep };
90
+ export { CircuitBreaker, CircuitBreakerOpenError, type CircuitBreakerOptions, type CircuitState, DEFAULT_RETRYABLE_STATUS_CODES, type HeaderSource, type RetryOptions, computeJitterDelay, isTransientNetworkError, parseDurationToMs, parseRateLimitDelay, retry, retryFetch, retryStream, sleep };
package/dist/index.d.ts CHANGED
@@ -5,19 +5,61 @@ interface RetryOptions {
5
5
  baseDelayMs?: number;
6
6
  /** Maximum backoff ceiling delay in milliseconds. Default: 30000 */
7
7
  maxDelayMs?: number;
8
+ /** Per-request timeout in milliseconds. Default: undefined */
9
+ timeoutMs?: number;
8
10
  /** HTTP status codes that trigger a retry. Default: [408, 429, 500, 502, 503, 504] */
9
11
  retryOnStatus?: number[];
10
- /** Optional callback fired on every retry attempt before sleep */
12
+ /** Custom condition to decide whether to retry */
13
+ shouldRetry?: (error: unknown, attempt: number) => boolean;
14
+ /** Optional callback fired before every retry sleep */
11
15
  onRetry?: (error: unknown, attempt: number, delayMs: number) => void;
12
16
  /** AbortSignal for immediate cancellation */
13
17
  signal?: AbortSignal;
14
18
  }
19
+ interface CircuitBreakerOptions {
20
+ /** Number of consecutive failures before opening the circuit. Default: 5 */
21
+ failureThreshold?: number;
22
+ /** Time in milliseconds to wait before attempting recovery (HALF_OPEN). Default: 30000 */
23
+ resetTimeoutMs?: number;
24
+ /** Optional callback triggered on circuit state changes */
25
+ onStateChange?: (from: CircuitState, to: CircuitState) => void;
26
+ }
27
+ type CircuitState = 'CLOSED' | 'OPEN' | 'HALF_OPEN';
15
28
  type HeaderSource = Headers | Record<string, string | string[] | undefined> | [string, string][] | undefined;
16
29
 
17
30
  declare function retryFetch(url: string | URL | Request, init?: RequestInit, options?: RetryOptions): Promise<Response>;
18
31
 
19
32
  declare function retry<T>(fn: (attempt: number) => Promise<T>, options?: RetryOptions): Promise<T>;
20
33
 
34
+ /**
35
+ * Resilient wrapper for Server-Sent Events / LLM chunk streams.
36
+ * If the streaming connection drops mid-generation, it retries and resumes.
37
+ */
38
+ declare function retryStream<T>(streamFactory: (attempt: number) => Promise<AsyncIterable<T>>, options?: RetryOptions): AsyncGenerator<T, void, unknown>;
39
+
40
+ declare class CircuitBreakerOpenError extends Error {
41
+ constructor(message?: string);
42
+ }
43
+ /**
44
+ * Zero-dependency Circuit Breaker implementation to stop cascading failure
45
+ * cascades when downstream LLM APIs experience total outages.
46
+ */
47
+ declare class CircuitBreaker {
48
+ private state;
49
+ private failureCount;
50
+ private lastFailureTime;
51
+ private readonly failureThreshold;
52
+ private readonly resetTimeoutMs;
53
+ private readonly onStateChange?;
54
+ constructor(options?: CircuitBreakerOptions);
55
+ getState(): CircuitState;
56
+ execute<T>(fn: () => Promise<T>): Promise<T>;
57
+ private recordSuccess;
58
+ private recordFailure;
59
+ private transitionTo;
60
+ reset(): void;
61
+ }
62
+
21
63
  declare const DEFAULT_RETRYABLE_STATUS_CODES: number[];
22
64
  /**
23
65
  * Calculates exponential backoff with full jitter:
@@ -29,19 +71,20 @@ declare function computeJitterDelay(attempt: number, baseDelayMs?: number, maxDe
29
71
  */
30
72
  declare function parseDurationToMs(value: string): number | null;
31
73
  /**
32
- * Inspects rate-limiting headers and extracts sleep delay in milliseconds:
33
- * - retry-after (seconds or HTTP date)
34
- * - x-ratelimit-reset-requests (OpenAI format)
35
- * - x-ratelimit-reset-tokens (Anthropic format)
74
+ * Exhaustively checks provider headers across:
75
+ * - RFC 7231 / RFC 6585 standard `retry-after`
76
+ * - OpenAI: `x-ratelimit-reset-requests`, `x-ratelimit-reset-tokens`
77
+ * - Anthropic: `anthropic-ratelimit-requests-reset`, `anthropic-ratelimit-tokens-reset`
78
+ * - Groq / DeepSeek / Mistral: `x-ratelimit-reset-total`, `retry-after-ms`
36
79
  */
37
80
  declare function parseRateLimitDelay(headers: HeaderSource): number | null;
38
81
  /**
39
- * Detects transient network drop errors.
82
+ * Detects transient network drop errors across Node, Bun, Deno, and browsers.
40
83
  */
41
84
  declare function isTransientNetworkError(error: unknown): boolean;
42
85
  /**
43
- * Sleep helper respecting AbortSignal cancellation.
86
+ * Promisified sleep that respects AbortSignal cancellation.
44
87
  */
45
88
  declare function sleep(ms: number, signal?: AbortSignal): Promise<void>;
46
89
 
47
- export { DEFAULT_RETRYABLE_STATUS_CODES, type HeaderSource, type RetryOptions, computeJitterDelay, isTransientNetworkError, parseDurationToMs, parseRateLimitDelay, retry, retryFetch, sleep };
90
+ export { CircuitBreaker, CircuitBreakerOpenError, type CircuitBreakerOptions, type CircuitState, DEFAULT_RETRYABLE_STATUS_CODES, type HeaderSource, type RetryOptions, computeJitterDelay, isTransientNetworkError, parseDurationToMs, parseRateLimitDelay, retry, retryFetch, retryStream, sleep };
package/dist/index.js CHANGED
@@ -20,6 +20,8 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
20
20
  // src/index.ts
21
21
  var index_exports = {};
22
22
  __export(index_exports, {
23
+ CircuitBreaker: () => CircuitBreaker,
24
+ CircuitBreakerOpenError: () => CircuitBreakerOpenError,
23
25
  DEFAULT_RETRYABLE_STATUS_CODES: () => DEFAULT_RETRYABLE_STATUS_CODES,
24
26
  computeJitterDelay: () => computeJitterDelay,
25
27
  isTransientNetworkError: () => isTransientNetworkError,
@@ -27,6 +29,7 @@ __export(index_exports, {
27
29
  parseRateLimitDelay: () => parseRateLimitDelay,
28
30
  retry: () => retry,
29
31
  retryFetch: () => retryFetch,
32
+ retryStream: () => retryStream,
30
33
  sleep: () => sleep
31
34
  });
32
35
  module.exports = __toCommonJS(index_exports);
@@ -100,6 +103,11 @@ function parseDurationToMs(value) {
100
103
  }
101
104
  function parseRateLimitDelay(headers) {
102
105
  if (!headers) return null;
106
+ const retryAfterMs = getHeader(headers, "retry-after-ms");
107
+ if (retryAfterMs) {
108
+ const parsed = parseFloat(retryAfterMs);
109
+ if (!isNaN(parsed)) return Math.max(0, Math.floor(parsed));
110
+ }
103
111
  const retryAfter = getHeader(headers, "retry-after");
104
112
  if (retryAfter) {
105
113
  if (/^\d+(\.\d+)?$/.test(retryAfter.trim())) {
@@ -111,15 +119,23 @@ function parseRateLimitDelay(headers) {
111
119
  return Math.max(0, parsedDate - Date.now());
112
120
  }
113
121
  }
114
- const openaiReset = getHeader(headers, "x-ratelimit-reset-requests");
115
- if (openaiReset) {
116
- const durationMs = parseDurationToMs(openaiReset);
117
- if (durationMs !== null) return durationMs;
118
- }
119
- const anthropicReset = getHeader(headers, "x-ratelimit-reset-tokens");
120
- if (anthropicReset) {
121
- const durationMs = parseDurationToMs(anthropicReset);
122
- if (durationMs !== null) return durationMs;
122
+ const candidateKeys = [
123
+ "x-ratelimit-reset-requests",
124
+ "x-ratelimit-reset-tokens",
125
+ "anthropic-ratelimit-requests-reset",
126
+ "anthropic-ratelimit-tokens-reset",
127
+ "x-ratelimit-reset-total"
128
+ ];
129
+ for (const key of candidateKeys) {
130
+ const raw = getHeader(headers, key);
131
+ if (raw) {
132
+ const ms = parseDurationToMs(raw);
133
+ if (ms !== null) return ms;
134
+ const epochSec = parseFloat(raw);
135
+ if (!isNaN(epochSec) && epochSec > 1e9) {
136
+ return Math.max(0, Math.floor(epochSec * 1e3 - Date.now()));
137
+ }
138
+ }
123
139
  }
124
140
  return null;
125
141
  }
@@ -128,9 +144,17 @@ function isTransientNetworkError(error) {
128
144
  const msg = error.message.toLowerCase();
129
145
  const code = error.code;
130
146
  if (error.name === "TypeError" && msg.includes("fetch failed")) return true;
131
- const transientCodes = ["ECONNRESET", "ETIMEDOUT", "ENOTFOUND", "EAI_AGAIN", "ECONNREFUSED"];
147
+ const transientCodes = [
148
+ "ECONNRESET",
149
+ "ETIMEDOUT",
150
+ "ENOTFOUND",
151
+ "EAI_AGAIN",
152
+ "ECONNREFUSED",
153
+ "UND_ERR_CONNECT_TIMEOUT",
154
+ "UND_ERR_SOCKET"
155
+ ];
132
156
  if (code && transientCodes.includes(code)) return true;
133
- return msg.includes("network error") || msg.includes("socket hang up") || msg.includes("timeout");
157
+ return msg.includes("network error") || msg.includes("socket hang up") || msg.includes("terminated") || msg.includes("econnreset");
134
158
  }
135
159
  function sleep(ms, signal) {
136
160
  return new Promise((resolve, reject) => {
@@ -155,7 +179,9 @@ async function retryFetch(url, init, options = {}) {
155
179
  retries = 3,
156
180
  baseDelayMs = 500,
157
181
  maxDelayMs = 3e4,
182
+ timeoutMs,
158
183
  retryOnStatus = DEFAULT_RETRYABLE_STATUS_CODES,
184
+ shouldRetry,
159
185
  onRetry,
160
186
  signal
161
187
  } = options;
@@ -164,12 +190,25 @@ async function retryFetch(url, init, options = {}) {
164
190
  if (signal?.aborted) {
165
191
  throw signal.reason ?? new DOMException("The operation was aborted.", "AbortError");
166
192
  }
193
+ const attemptController = new AbortController();
194
+ let timeoutId;
195
+ if (timeoutMs && timeoutMs > 0) {
196
+ timeoutId = setTimeout(() => {
197
+ attemptController.abort(new DOMException(`Request timed out after ${timeoutMs}ms`, "TimeoutError"));
198
+ }, timeoutMs);
199
+ }
200
+ const abortHandler = () => attemptController.abort(signal?.reason);
201
+ signal?.addEventListener("abort", abortHandler, { once: true });
167
202
  try {
168
203
  const response = await fetch(url, {
169
204
  ...init,
170
- signal: signal ?? init?.signal
205
+ signal: attemptController.signal
171
206
  });
172
- if (retryOnStatus.includes(response.status) && attempt < retries) {
207
+ if (timeoutId) clearTimeout(timeoutId);
208
+ signal?.removeEventListener("abort", abortHandler);
209
+ const isStatusRetryable = retryOnStatus.includes(response.status);
210
+ const customRetry = shouldRetry ? shouldRetry(response, attempt) : isStatusRetryable;
211
+ if (customRetry && attempt < retries) {
173
212
  const headerDelay = parseRateLimitDelay(response.headers);
174
213
  const backoffDelay = computeJitterDelay(attempt, baseDelayMs, maxDelayMs);
175
214
  const chosenDelay = Math.min(maxDelayMs, headerDelay ?? backoffDelay);
@@ -180,10 +219,15 @@ async function retryFetch(url, init, options = {}) {
180
219
  }
181
220
  return response;
182
221
  } catch (error) {
183
- if (signal?.aborted || error instanceof Error && error.name === "AbortError") {
184
- throw error;
222
+ if (timeoutId) clearTimeout(timeoutId);
223
+ signal?.removeEventListener("abort", abortHandler);
224
+ if (signal?.aborted) {
225
+ throw signal.reason ?? error;
185
226
  }
186
- if (isTransientNetworkError(error) && attempt < retries) {
227
+ const isTimeout = error instanceof Error && error.name === "TimeoutError";
228
+ const isNetworkDrop = isTransientNetworkError(error);
229
+ const customRetry = shouldRetry ? shouldRetry(error, attempt) : isNetworkDrop || isTimeout;
230
+ if (customRetry && attempt < retries) {
187
231
  const chosenDelay = computeJitterDelay(attempt, baseDelayMs, maxDelayMs);
188
232
  onRetry?.(error, attempt + 1, chosenDelay);
189
233
  await sleep(chosenDelay, signal);
@@ -202,6 +246,7 @@ async function retry(fn, options = {}) {
202
246
  baseDelayMs = 500,
203
247
  maxDelayMs = 3e4,
204
248
  retryOnStatus = DEFAULT_RETRYABLE_STATUS_CODES,
249
+ shouldRetry,
205
250
  onRetry,
206
251
  signal
207
252
  } = options;
@@ -224,7 +269,8 @@ async function retry(fn, options = {}) {
224
269
  const headers = err?.headers ?? err?.response?.headers;
225
270
  const isStatusRetryable = statusCode !== void 0 && retryOnStatus.includes(statusCode);
226
271
  const isNetworkRetryable = isTransientNetworkError(error);
227
- if (!isStatusRetryable && !isNetworkRetryable) {
272
+ const customRetry = shouldRetry ? shouldRetry(error, attempt) : isStatusRetryable || isNetworkRetryable;
273
+ if (!customRetry) {
228
274
  throw error;
229
275
  }
230
276
  let chosenDelay;
@@ -240,8 +286,117 @@ async function retry(fn, options = {}) {
240
286
  }
241
287
  }
242
288
  }
289
+
290
+ // src/stream.ts
291
+ async function* retryStream(streamFactory, options = {}) {
292
+ const {
293
+ retries = 3,
294
+ baseDelayMs = 500,
295
+ maxDelayMs = 3e4,
296
+ onRetry,
297
+ signal
298
+ } = options;
299
+ let attempt = 0;
300
+ while (true) {
301
+ if (signal?.aborted) {
302
+ throw signal.reason ?? new DOMException("The operation was aborted.", "AbortError");
303
+ }
304
+ try {
305
+ const stream = await streamFactory(attempt);
306
+ for await (const chunk of stream) {
307
+ if (signal?.aborted) {
308
+ throw signal.reason ?? new DOMException("The operation was aborted.", "AbortError");
309
+ }
310
+ yield chunk;
311
+ }
312
+ return;
313
+ } catch (error) {
314
+ if (signal?.aborted || error instanceof Error && error.name === "AbortError") {
315
+ throw error;
316
+ }
317
+ if (attempt >= retries) {
318
+ throw error;
319
+ }
320
+ const delay = computeJitterDelay(attempt, baseDelayMs, maxDelayMs);
321
+ onRetry?.(error, attempt + 1, delay);
322
+ await sleep(delay, signal);
323
+ attempt++;
324
+ }
325
+ }
326
+ }
327
+
328
+ // src/circuit-breaker.ts
329
+ var CircuitBreakerOpenError = class extends Error {
330
+ constructor(message = "Circuit breaker is OPEN. Fast-failing requests.") {
331
+ super(message);
332
+ this.name = "CircuitBreakerOpenError";
333
+ }
334
+ };
335
+ var CircuitBreaker = class {
336
+ state = "CLOSED";
337
+ failureCount = 0;
338
+ lastFailureTime = 0;
339
+ failureThreshold;
340
+ resetTimeoutMs;
341
+ onStateChange;
342
+ constructor(options = {}) {
343
+ this.failureThreshold = options.failureThreshold ?? 5;
344
+ this.resetTimeoutMs = options.resetTimeoutMs ?? 3e4;
345
+ this.onStateChange = options.onStateChange;
346
+ }
347
+ getState() {
348
+ if (this.state === "OPEN") {
349
+ const elapsed = Date.now() - this.lastFailureTime;
350
+ if (elapsed >= this.resetTimeoutMs) {
351
+ this.transitionTo("HALF_OPEN");
352
+ }
353
+ }
354
+ return this.state;
355
+ }
356
+ async execute(fn) {
357
+ const currentState = this.getState();
358
+ if (currentState === "OPEN") {
359
+ throw new CircuitBreakerOpenError();
360
+ }
361
+ try {
362
+ const result = await fn();
363
+ this.recordSuccess();
364
+ return result;
365
+ } catch (error) {
366
+ this.recordFailure();
367
+ throw error;
368
+ }
369
+ }
370
+ recordSuccess() {
371
+ if (this.state === "HALF_OPEN") {
372
+ this.transitionTo("CLOSED");
373
+ }
374
+ this.failureCount = 0;
375
+ }
376
+ recordFailure() {
377
+ this.failureCount++;
378
+ this.lastFailureTime = Date.now();
379
+ if (this.state === "HALF_OPEN" || this.failureCount >= this.failureThreshold) {
380
+ this.transitionTo("OPEN");
381
+ }
382
+ }
383
+ transitionTo(nextState) {
384
+ const prevState = this.state;
385
+ if (prevState !== nextState) {
386
+ this.state = nextState;
387
+ this.onStateChange?.(prevState, nextState);
388
+ }
389
+ }
390
+ reset() {
391
+ this.state = "CLOSED";
392
+ this.failureCount = 0;
393
+ this.lastFailureTime = 0;
394
+ }
395
+ };
243
396
  // Annotate the CommonJS export names for ESM import in node:
244
397
  0 && (module.exports = {
398
+ CircuitBreaker,
399
+ CircuitBreakerOpenError,
245
400
  DEFAULT_RETRYABLE_STATUS_CODES,
246
401
  computeJitterDelay,
247
402
  isTransientNetworkError,
@@ -249,6 +404,7 @@ async function retry(fn, options = {}) {
249
404
  parseRateLimitDelay,
250
405
  retry,
251
406
  retryFetch,
407
+ retryStream,
252
408
  sleep
253
409
  });
254
410
  //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/index.ts","../src/engine.ts","../src/fetch.ts","../src/wrap.ts"],"sourcesContent":["export { retryFetch } from './fetch.js';\r\nexport { retry } from './wrap.js';\r\nexport {\r\n computeJitterDelay,\r\n parseDurationToMs,\r\n parseRateLimitDelay,\r\n isTransientNetworkError,\r\n sleep,\r\n DEFAULT_RETRYABLE_STATUS_CODES,\r\n} from './engine.js';\r\nexport type { RetryOptions, HeaderSource } from './types.js';","import { HeaderSource } from './types.js';\r\n\r\nexport const DEFAULT_RETRYABLE_STATUS_CODES = [408, 429, 500, 502, 503, 504];\r\n\r\n/**\r\n * Calculates exponential backoff with full jitter:\r\n * sleepMs = min(maxDelayMs, baseDelayMs * 2 ** attempt) * Math.random()\r\n */\r\nexport function computeJitterDelay(\r\n attempt: number,\r\n baseDelayMs: number = 500,\r\n maxDelayMs: number = 30000\r\n): number {\r\n const exponential = baseDelayMs * Math.pow(2, attempt);\r\n const ceiling = Math.min(maxDelayMs, exponential);\r\n return Math.floor(ceiling * Math.random());\r\n}\r\n\r\n/**\r\n * Safely extracts a header value case-insensitively across Headers, records, or tuples.\r\n */\r\nexport function getHeader(headers: HeaderSource, name: string): string | null {\r\n if (!headers) return null;\r\n\r\n const target = name.toLowerCase();\r\n\r\n // Native Web API Headers\r\n if (typeof (headers as Headers).get === 'function') {\r\n return (headers as Headers).get(target);\r\n }\r\n\r\n // Array of tuples\r\n if (Array.isArray(headers)) {\r\n for (const [key, value] of headers) {\r\n if (key.toLowerCase() === target) {\r\n return value;\r\n }\r\n }\r\n return null;\r\n }\r\n\r\n // Plain object\r\n if (typeof headers === 'object') {\r\n for (const key of Object.keys(headers)) {\r\n if (key.toLowerCase() === target) {\r\n const val = (headers as Record<string, unknown>)[key];\r\n if (Array.isArray(val)) return val[0] ?? null;\r\n return typeof val === 'string' ? val : String(val);\r\n }\r\n }\r\n }\r\n\r\n return null;\r\n}\r\n\r\n/**\r\n * Parses duration strings returned by LLM APIs (e.g. \"600ms\", \"10s\", \"2m\", \"1m30s\", \"1.5\").\r\n */\r\nexport function parseDurationToMs(value: string): number | null {\r\n const trimmed = value.trim();\r\n if (!trimmed) return null;\r\n\r\n // Pure numeric string represents seconds\r\n if (/^\\d+(\\.\\d+)?$/.test(trimmed)) {\r\n const sec = parseFloat(trimmed);\r\n return isNaN(sec) ? null : Math.max(0, Math.floor(sec * 1000));\r\n }\r\n\r\n // Units match\r\n const regex = /(\\d+(?:\\.\\d+)?)\\s*(ms|s|m|h|d)/gi;\r\n let match: RegExpExecArray | null;\r\n let totalMs = 0;\r\n let matched = false;\r\n\r\n while ((match = regex.exec(trimmed)) !== null) {\r\n matched = true;\r\n const amount = parseFloat(match[1]);\r\n const unit = match[2].toLowerCase();\r\n\r\n switch (unit) {\r\n case 'ms':\r\n totalMs += amount;\r\n break;\r\n case 's':\r\n totalMs += amount * 1000;\r\n break;\r\n case 'm':\r\n totalMs += amount * 60 * 1000;\r\n break;\r\n case 'h':\r\n totalMs += amount * 60 * 60 * 1000;\r\n break;\r\n case 'd':\r\n totalMs += amount * 24 * 60 * 60 * 1000;\r\n break;\r\n }\r\n }\r\n\r\n return matched ? Math.max(0, Math.floor(totalMs)) : null;\r\n}\r\n\r\n/**\r\n * Inspects rate-limiting headers and extracts sleep delay in milliseconds:\r\n * - retry-after (seconds or HTTP date)\r\n * - x-ratelimit-reset-requests (OpenAI format)\r\n * - x-ratelimit-reset-tokens (Anthropic format)\r\n */\r\nexport function parseRateLimitDelay(headers: HeaderSource): number | null {\r\n if (!headers) return null;\r\n\r\n const retryAfter = getHeader(headers, 'retry-after');\r\n if (retryAfter) {\r\n if (/^\\d+(\\.\\d+)?$/.test(retryAfter.trim())) {\r\n const sec = parseFloat(retryAfter.trim());\r\n if (!isNaN(sec)) return Math.max(0, Math.floor(sec * 1000));\r\n }\r\n\r\n const parsedDate = Date.parse(retryAfter);\r\n if (!isNaN(parsedDate)) {\r\n return Math.max(0, parsedDate - Date.now());\r\n }\r\n }\r\n\r\n const openaiReset = getHeader(headers, 'x-ratelimit-reset-requests');\r\n if (openaiReset) {\r\n const durationMs = parseDurationToMs(openaiReset);\r\n if (durationMs !== null) return durationMs;\r\n }\r\n\r\n const anthropicReset = getHeader(headers, 'x-ratelimit-reset-tokens');\r\n if (anthropicReset) {\r\n const durationMs = parseDurationToMs(anthropicReset);\r\n if (durationMs !== null) return durationMs;\r\n }\r\n\r\n return null;\r\n}\r\n\r\n/**\r\n * Detects transient network drop errors.\r\n */\r\nexport function isTransientNetworkError(error: unknown): boolean {\r\n if (!(error instanceof Error)) return false;\r\n\r\n const msg = error.message.toLowerCase();\r\n const code = (error as { code?: string }).code;\r\n\r\n if (error.name === 'TypeError' && msg.includes('fetch failed')) return true;\r\n\r\n const transientCodes = ['ECONNRESET', 'ETIMEDOUT', 'ENOTFOUND', 'EAI_AGAIN', 'ECONNREFUSED'];\r\n if (code && transientCodes.includes(code)) return true;\r\n\r\n return (\r\n msg.includes('network error') ||\r\n msg.includes('socket hang up') ||\r\n msg.includes('timeout')\r\n );\r\n}\r\n\r\n/**\r\n * Sleep helper respecting AbortSignal cancellation.\r\n */\r\nexport function sleep(ms: number, signal?: AbortSignal): Promise<void> {\r\n return new Promise((resolve, reject) => {\r\n if (signal?.aborted) {\r\n return reject(signal.reason ?? new DOMException('The operation was aborted.', 'AbortError'));\r\n }\r\n\r\n const timer = setTimeout(() => {\r\n signal?.removeEventListener('abort', onAbort);\r\n resolve();\r\n }, ms);\r\n\r\n function onAbort() {\r\n clearTimeout(timer);\r\n reject(signal?.reason ?? new DOMException('The operation was aborted.', 'AbortError'));\r\n }\r\n\r\n signal?.addEventListener('abort', onAbort, { once: true });\r\n });\r\n}","import {\r\n computeJitterDelay,\r\n DEFAULT_RETRYABLE_STATUS_CODES,\r\n isTransientNetworkError,\r\n parseRateLimitDelay,\r\n sleep,\r\n} from './engine.js';\r\nimport { RetryOptions } from './types.js';\r\n\r\nexport async function retryFetch(\r\n url: string | URL | Request,\r\n init?: RequestInit,\r\n options: RetryOptions = {}\r\n): Promise<Response> {\r\n const {\r\n retries = 3,\r\n baseDelayMs = 500,\r\n maxDelayMs = 30000,\r\n retryOnStatus = DEFAULT_RETRYABLE_STATUS_CODES,\r\n onRetry,\r\n signal,\r\n } = options;\r\n\r\n let attempt = 0;\r\n\r\n while (true) {\r\n if (signal?.aborted) {\r\n throw signal.reason ?? new DOMException('The operation was aborted.', 'AbortError');\r\n }\r\n\r\n try {\r\n const response = await fetch(url, {\r\n ...init,\r\n signal: signal ?? init?.signal,\r\n });\r\n\r\n if (retryOnStatus.includes(response.status) && attempt < retries) {\r\n const headerDelay = parseRateLimitDelay(response.headers);\r\n const backoffDelay = computeJitterDelay(attempt, baseDelayMs, maxDelayMs);\r\n const chosenDelay = Math.min(maxDelayMs, headerDelay ?? backoffDelay);\r\n\r\n onRetry?.(response, attempt + 1, chosenDelay);\r\n await sleep(chosenDelay, signal);\r\n attempt++;\r\n continue;\r\n }\r\n\r\n return response;\r\n } catch (error) {\r\n if (signal?.aborted || (error instanceof Error && error.name === 'AbortError')) {\r\n throw error;\r\n }\r\n\r\n if (isTransientNetworkError(error) && attempt < retries) {\r\n const chosenDelay = computeJitterDelay(attempt, baseDelayMs, maxDelayMs);\r\n onRetry?.(error, attempt + 1, chosenDelay);\r\n await sleep(chosenDelay, signal);\r\n attempt++;\r\n continue;\r\n }\r\n\r\n throw error;\r\n }\r\n }\r\n}","import {\r\n computeJitterDelay,\r\n DEFAULT_RETRYABLE_STATUS_CODES,\r\n isTransientNetworkError,\r\n parseRateLimitDelay,\r\n sleep,\r\n} from './engine.js';\r\nimport { HeaderSource, RetryOptions } from './types.js';\r\n\r\ninterface ErrorWithStatus extends Error {\r\n status?: number;\r\n statusCode?: number;\r\n headers?: HeaderSource;\r\n response?: {\r\n status?: number;\r\n headers?: HeaderSource;\r\n };\r\n}\r\n\r\nexport async function retry<T>(\r\n fn: (attempt: number) => Promise<T>,\r\n options: RetryOptions = {}\r\n): Promise<T> {\r\n const {\r\n retries = 3,\r\n baseDelayMs = 500,\r\n maxDelayMs = 30000,\r\n retryOnStatus = DEFAULT_RETRYABLE_STATUS_CODES,\r\n onRetry,\r\n signal,\r\n } = options;\r\n\r\n let attempt = 0;\r\n\r\n while (true) {\r\n if (signal?.aborted) {\r\n throw signal.reason ?? new DOMException('The operation was aborted.', 'AbortError');\r\n }\r\n\r\n try {\r\n return await fn(attempt);\r\n } catch (error) {\r\n if (signal?.aborted || (error instanceof Error && error.name === 'AbortError')) {\r\n throw error;\r\n }\r\n\r\n if (attempt >= retries) {\r\n throw error;\r\n }\r\n\r\n const err = error as ErrorWithStatus;\r\n const statusCode = err?.status ?? err?.statusCode ?? err?.response?.status;\r\n const headers = err?.headers ?? err?.response?.headers;\r\n\r\n const isStatusRetryable = statusCode !== undefined && retryOnStatus.includes(statusCode);\r\n const isNetworkRetryable = isTransientNetworkError(error);\r\n\r\n if (!isStatusRetryable && !isNetworkRetryable) {\r\n throw error;\r\n }\r\n\r\n let chosenDelay: number;\r\n const headerDelay = headers ? parseRateLimitDelay(headers) : null;\r\n\r\n if (headerDelay !== null) {\r\n chosenDelay = Math.min(maxDelayMs, headerDelay);\r\n } else {\r\n chosenDelay = computeJitterDelay(attempt, baseDelayMs, maxDelayMs);\r\n }\r\n\r\n onRetry?.(error, attempt + 1, chosenDelay);\r\n await sleep(chosenDelay, signal);\r\n attempt++;\r\n }\r\n }\r\n}"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../src/index.ts","../src/engine.ts","../src/fetch.ts","../src/wrap.ts","../src/stream.ts","../src/circuit-breaker.ts"],"sourcesContent":["export { retryFetch } from './fetch.js';\r\nexport { retry } from './wrap.js';\r\nexport { retryStream } from './stream.js';\r\nexport { CircuitBreaker, CircuitBreakerOpenError } from './circuit-breaker.js';\r\nexport {\r\n computeJitterDelay,\r\n parseDurationToMs,\r\n parseRateLimitDelay,\r\n isTransientNetworkError,\r\n sleep,\r\n DEFAULT_RETRYABLE_STATUS_CODES,\r\n} from './engine.js';\r\nexport type {\r\n RetryOptions,\r\n CircuitBreakerOptions,\r\n CircuitState,\r\n HeaderSource,\r\n} from './types.js';","import { HeaderSource } from './types.js';\r\n\r\nexport const DEFAULT_RETRYABLE_STATUS_CODES = [408, 429, 500, 502, 503, 504];\r\n\r\n/**\r\n * Calculates exponential backoff with full jitter:\r\n * sleepMs = min(maxDelayMs, baseDelayMs * 2 ** attempt) * Math.random()\r\n */\r\nexport function computeJitterDelay(\r\n attempt: number,\r\n baseDelayMs: number = 500,\r\n maxDelayMs: number = 30000\r\n): number {\r\n const exponential = baseDelayMs * Math.pow(2, attempt);\r\n const ceiling = Math.min(maxDelayMs, exponential);\r\n return Math.floor(ceiling * Math.random());\r\n}\r\n\r\n/**\r\n * Safely extracts a header value case-insensitively across Headers instances, records, or tuples.\r\n */\r\nexport function getHeader(headers: HeaderSource, name: string): string | null {\r\n if (!headers) return null;\r\n\r\n const target = name.toLowerCase();\r\n\r\n if (typeof (headers as Headers).get === 'function') {\r\n return (headers as Headers).get(target);\r\n }\r\n\r\n if (Array.isArray(headers)) {\r\n for (const [key, value] of headers) {\r\n if (key.toLowerCase() === target) {\r\n return value;\r\n }\r\n }\r\n return null;\r\n }\r\n\r\n if (typeof headers === 'object') {\r\n for (const key of Object.keys(headers)) {\r\n if (key.toLowerCase() === target) {\r\n const val = (headers as Record<string, unknown>)[key];\r\n if (Array.isArray(val)) return val[0] ?? null;\r\n return typeof val === 'string' ? val : String(val);\r\n }\r\n }\r\n }\r\n\r\n return null;\r\n}\r\n\r\n/**\r\n * Parses duration strings returned by LLM APIs (e.g. \"600ms\", \"10s\", \"2m\", \"1m30s\", \"1.5\").\r\n */\r\nexport function parseDurationToMs(value: string): number | null {\r\n const trimmed = value.trim();\r\n if (!trimmed) return null;\r\n\r\n if (/^\\d+(\\.\\d+)?$/.test(trimmed)) {\r\n const sec = parseFloat(trimmed);\r\n return isNaN(sec) ? null : Math.max(0, Math.floor(sec * 1000));\r\n }\r\n\r\n const regex = /(\\d+(?:\\.\\d+)?)\\s*(ms|s|m|h|d)/gi;\r\n let match: RegExpExecArray | null;\r\n let totalMs = 0;\r\n let matched = false;\r\n\r\n while ((match = regex.exec(trimmed)) !== null) {\r\n matched = true;\r\n const amount = parseFloat(match[1]);\r\n const unit = match[2].toLowerCase();\r\n\r\n switch (unit) {\r\n case 'ms':\r\n totalMs += amount;\r\n break;\r\n case 's':\r\n totalMs += amount * 1000;\r\n break;\r\n case 'm':\r\n totalMs += amount * 60 * 1000;\r\n break;\r\n case 'h':\r\n totalMs += amount * 60 * 60 * 1000;\r\n break;\r\n case 'd':\r\n totalMs += amount * 24 * 60 * 60 * 1000;\r\n break;\r\n }\r\n }\r\n\r\n return matched ? Math.max(0, Math.floor(totalMs)) : null;\r\n}\r\n\r\n/**\r\n * Exhaustively checks provider headers across:\r\n * - RFC 7231 / RFC 6585 standard `retry-after`\r\n * - OpenAI: `x-ratelimit-reset-requests`, `x-ratelimit-reset-tokens`\r\n * - Anthropic: `anthropic-ratelimit-requests-reset`, `anthropic-ratelimit-tokens-reset`\r\n * - Groq / DeepSeek / Mistral: `x-ratelimit-reset-total`, `retry-after-ms`\r\n */\r\nexport function parseRateLimitDelay(headers: HeaderSource): number | null {\r\n if (!headers) return null;\r\n\r\n // 1. Direct milliseconds header (Groq, custom proxies)\r\n const retryAfterMs = getHeader(headers, 'retry-after-ms');\r\n if (retryAfterMs) {\r\n const parsed = parseFloat(retryAfterMs);\r\n if (!isNaN(parsed)) return Math.max(0, Math.floor(parsed));\r\n }\r\n\r\n // 2. Standard Retry-After (seconds or RFC 1123 HTTP Date)\r\n const retryAfter = getHeader(headers, 'retry-after');\r\n if (retryAfter) {\r\n if (/^\\d+(\\.\\d+)?$/.test(retryAfter.trim())) {\r\n const sec = parseFloat(retryAfter.trim());\r\n if (!isNaN(sec)) return Math.max(0, Math.floor(sec * 1000));\r\n }\r\n\r\n const parsedDate = Date.parse(retryAfter);\r\n if (!isNaN(parsedDate)) {\r\n return Math.max(0, parsedDate - Date.now());\r\n }\r\n }\r\n\r\n // 3. Provider-specific reset headers\r\n const candidateKeys = [\r\n 'x-ratelimit-reset-requests',\r\n 'x-ratelimit-reset-tokens',\r\n 'anthropic-ratelimit-requests-reset',\r\n 'anthropic-ratelimit-tokens-reset',\r\n 'x-ratelimit-reset-total',\r\n ];\r\n\r\n for (const key of candidateKeys) {\r\n const raw = getHeader(headers, key);\r\n if (raw) {\r\n const ms = parseDurationToMs(raw);\r\n if (ms !== null) return ms;\r\n\r\n // Handle epoch timestamp formats\r\n const epochSec = parseFloat(raw);\r\n if (!isNaN(epochSec) && epochSec > 1000000000) {\r\n return Math.max(0, Math.floor(epochSec * 1000 - Date.now()));\r\n }\r\n }\r\n }\r\n\r\n return null;\r\n}\r\n\r\n/**\r\n * Detects transient network drop errors across Node, Bun, Deno, and browsers.\r\n */\r\nexport function isTransientNetworkError(error: unknown): boolean {\r\n if (!(error instanceof Error)) return false;\r\n\r\n const msg = error.message.toLowerCase();\r\n const code = (error as { code?: string }).code;\r\n\r\n if (error.name === 'TypeError' && msg.includes('fetch failed')) return true;\r\n\r\n const transientCodes = [\r\n 'ECONNRESET',\r\n 'ETIMEDOUT',\r\n 'ENOTFOUND',\r\n 'EAI_AGAIN',\r\n 'ECONNREFUSED',\r\n 'UND_ERR_CONNECT_TIMEOUT',\r\n 'UND_ERR_SOCKET',\r\n ];\r\n if (code && transientCodes.includes(code)) return true;\r\n\r\n return (\r\n msg.includes('network error') ||\r\n msg.includes('socket hang up') ||\r\n msg.includes('terminated') ||\r\n msg.includes('econnreset')\r\n );\r\n}\r\n\r\n/**\r\n * Promisified sleep that respects AbortSignal cancellation.\r\n */\r\nexport function sleep(ms: number, signal?: AbortSignal): Promise<void> {\r\n return new Promise((resolve, reject) => {\r\n if (signal?.aborted) {\r\n return reject(signal.reason ?? new DOMException('The operation was aborted.', 'AbortError'));\r\n }\r\n\r\n const timer = setTimeout(() => {\r\n signal?.removeEventListener('abort', onAbort);\r\n resolve();\r\n }, ms);\r\n\r\n function onAbort() {\r\n clearTimeout(timer);\r\n reject(signal?.reason ?? new DOMException('The operation was aborted.', 'AbortError'));\r\n }\r\n\r\n signal?.addEventListener('abort', onAbort, { once: true });\r\n });\r\n}","import {\r\n computeJitterDelay,\r\n DEFAULT_RETRYABLE_STATUS_CODES,\r\n isTransientNetworkError,\r\n parseRateLimitDelay,\r\n sleep,\r\n} from './engine.js';\r\nimport { RetryOptions } from './types.js';\r\n\r\nexport async function retryFetch(\r\n url: string | URL | Request,\r\n init?: RequestInit,\r\n options: RetryOptions = {}\r\n): Promise<Response> {\r\n const {\r\n retries = 3,\r\n baseDelayMs = 500,\r\n maxDelayMs = 30000,\r\n timeoutMs,\r\n retryOnStatus = DEFAULT_RETRYABLE_STATUS_CODES,\r\n shouldRetry,\r\n onRetry,\r\n signal,\r\n } = options;\r\n\r\n let attempt = 0;\r\n\r\n while (true) {\r\n if (signal?.aborted) {\r\n throw signal.reason ?? new DOMException('The operation was aborted.', 'AbortError');\r\n }\r\n\r\n // Set up per-attempt timeout controller\r\n const attemptController = new AbortController();\r\n let timeoutId: ReturnType<typeof setTimeout> | undefined;\r\n\r\n if (timeoutMs && timeoutMs > 0) {\r\n timeoutId = setTimeout(() => {\r\n attemptController.abort(new DOMException(`Request timed out after ${timeoutMs}ms`, 'TimeoutError'));\r\n }, timeoutMs);\r\n }\r\n\r\n const abortHandler = () => attemptController.abort(signal?.reason);\r\n signal?.addEventListener('abort', abortHandler, { once: true });\r\n\r\n try {\r\n const response = await fetch(url, {\r\n ...init,\r\n signal: attemptController.signal,\r\n });\r\n\r\n if (timeoutId) clearTimeout(timeoutId);\r\n signal?.removeEventListener('abort', abortHandler);\r\n\r\n const isStatusRetryable = retryOnStatus.includes(response.status);\r\n const customRetry = shouldRetry ? shouldRetry(response, attempt) : isStatusRetryable;\r\n\r\n if (customRetry && attempt < retries) {\r\n const headerDelay = parseRateLimitDelay(response.headers);\r\n const backoffDelay = computeJitterDelay(attempt, baseDelayMs, maxDelayMs);\r\n const chosenDelay = Math.min(maxDelayMs, headerDelay ?? backoffDelay);\r\n\r\n onRetry?.(response, attempt + 1, chosenDelay);\r\n await sleep(chosenDelay, signal);\r\n attempt++;\r\n continue;\r\n }\r\n\r\n return response;\r\n } catch (error) {\r\n if (timeoutId) clearTimeout(timeoutId);\r\n signal?.removeEventListener('abort', abortHandler);\r\n\r\n // Do not retry intentional outer aborts\r\n if (signal?.aborted) {\r\n throw signal.reason ?? error;\r\n }\r\n\r\n const isTimeout = error instanceof Error && error.name === 'TimeoutError';\r\n const isNetworkDrop = isTransientNetworkError(error);\r\n const customRetry = shouldRetry ? shouldRetry(error, attempt) : (isNetworkDrop || isTimeout);\r\n\r\n if (customRetry && attempt < retries) {\r\n const chosenDelay = computeJitterDelay(attempt, baseDelayMs, maxDelayMs);\r\n onRetry?.(error, attempt + 1, chosenDelay);\r\n await sleep(chosenDelay, signal);\r\n attempt++;\r\n continue;\r\n }\r\n\r\n throw error;\r\n }\r\n }\r\n}","import {\r\n computeJitterDelay,\r\n DEFAULT_RETRYABLE_STATUS_CODES,\r\n isTransientNetworkError,\r\n parseRateLimitDelay,\r\n sleep,\r\n} from './engine.js';\r\nimport { HeaderSource, RetryOptions } from './types.js';\r\n\r\ninterface ErrorWithStatus extends Error {\r\n status?: number;\r\n statusCode?: number;\r\n headers?: HeaderSource;\r\n response?: {\r\n status?: number;\r\n headers?: HeaderSource;\r\n };\r\n}\r\n\r\nexport async function retry<T>(\r\n fn: (attempt: number) => Promise<T>,\r\n options: RetryOptions = {}\r\n): Promise<T> {\r\n const {\r\n retries = 3,\r\n baseDelayMs = 500,\r\n maxDelayMs = 30000,\r\n retryOnStatus = DEFAULT_RETRYABLE_STATUS_CODES,\r\n shouldRetry,\r\n onRetry,\r\n signal,\r\n } = options;\r\n\r\n let attempt = 0;\r\n\r\n while (true) {\r\n if (signal?.aborted) {\r\n throw signal.reason ?? new DOMException('The operation was aborted.', 'AbortError');\r\n }\r\n\r\n try {\r\n return await fn(attempt);\r\n } catch (error) {\r\n if (signal?.aborted || (error instanceof Error && error.name === 'AbortError')) {\r\n throw error;\r\n }\r\n\r\n if (attempt >= retries) {\r\n throw error;\r\n }\r\n\r\n const err = error as ErrorWithStatus;\r\n const statusCode = err?.status ?? err?.statusCode ?? err?.response?.status;\r\n const headers = err?.headers ?? err?.response?.headers;\r\n\r\n const isStatusRetryable = statusCode !== undefined && retryOnStatus.includes(statusCode);\r\n const isNetworkRetryable = isTransientNetworkError(error);\r\n const customRetry = shouldRetry\r\n ? shouldRetry(error, attempt)\r\n : (isStatusRetryable || isNetworkRetryable);\r\n\r\n if (!customRetry) {\r\n throw error;\r\n }\r\n\r\n let chosenDelay: number;\r\n const headerDelay = headers ? parseRateLimitDelay(headers) : null;\r\n\r\n if (headerDelay !== null) {\r\n chosenDelay = Math.min(maxDelayMs, headerDelay);\r\n } else {\r\n chosenDelay = computeJitterDelay(attempt, baseDelayMs, maxDelayMs);\r\n }\r\n\r\n onRetry?.(error, attempt + 1, chosenDelay);\r\n await sleep(chosenDelay, signal);\r\n attempt++;\r\n }\r\n }\r\n}","import { computeJitterDelay, sleep } from './engine.js';\r\nimport { RetryOptions } from './types.js';\r\n\r\n/**\r\n * Resilient wrapper for Server-Sent Events / LLM chunk streams.\r\n * If the streaming connection drops mid-generation, it retries and resumes.\r\n */\r\nexport async function* retryStream<T>(\r\n streamFactory: (attempt: number) => Promise<AsyncIterable<T>>,\r\n options: RetryOptions = {}\r\n): AsyncGenerator<T, void, unknown> {\r\n const {\r\n retries = 3,\r\n baseDelayMs = 500,\r\n maxDelayMs = 30000,\r\n onRetry,\r\n signal,\r\n } = options;\r\n\r\n let attempt = 0;\r\n\r\n while (true) {\r\n if (signal?.aborted) {\r\n throw signal.reason ?? new DOMException('The operation was aborted.', 'AbortError');\r\n }\r\n\r\n try {\r\n const stream = await streamFactory(attempt);\r\n for await (const chunk of stream) {\r\n if (signal?.aborted) {\r\n throw signal.reason ?? new DOMException('The operation was aborted.', 'AbortError');\r\n }\r\n yield chunk;\r\n }\r\n return;\r\n } catch (error) {\r\n if (signal?.aborted || (error instanceof Error && error.name === 'AbortError')) {\r\n throw error;\r\n }\r\n\r\n if (attempt >= retries) {\r\n throw error;\r\n }\r\n\r\n const delay = computeJitterDelay(attempt, baseDelayMs, maxDelayMs);\r\n onRetry?.(error, attempt + 1, delay);\r\n await sleep(delay, signal);\r\n attempt++;\r\n }\r\n }\r\n}","import { CircuitBreakerOptions, CircuitState } from './types.js';\r\n\r\nexport class CircuitBreakerOpenError extends Error {\r\n constructor(message: string = 'Circuit breaker is OPEN. Fast-failing requests.') {\r\n super(message);\r\n this.name = 'CircuitBreakerOpenError';\r\n }\r\n}\r\n\r\n/**\r\n * Zero-dependency Circuit Breaker implementation to stop cascading failure\r\n * cascades when downstream LLM APIs experience total outages.\r\n */\r\nexport class CircuitBreaker {\r\n private state: CircuitState = 'CLOSED';\r\n private failureCount: number = 0;\r\n private lastFailureTime: number = 0;\r\n private readonly failureThreshold: number;\r\n private readonly resetTimeoutMs: number;\r\n private readonly onStateChange?: (from: CircuitState, to: CircuitState) => void;\r\n\r\n constructor(options: CircuitBreakerOptions = {}) {\r\n this.failureThreshold = options.failureThreshold ?? 5;\r\n this.resetTimeoutMs = options.resetTimeoutMs ?? 30000;\r\n this.onStateChange = options.onStateChange;\r\n }\r\n\r\n public getState(): CircuitState {\r\n if (this.state === 'OPEN') {\r\n const elapsed = Date.now() - this.lastFailureTime;\r\n if (elapsed >= this.resetTimeoutMs) {\r\n this.transitionTo('HALF_OPEN');\r\n }\r\n }\r\n return this.state;\r\n }\r\n\r\n public async execute<T>(fn: () => Promise<T>): Promise<T> {\r\n const currentState = this.getState();\r\n\r\n if (currentState === 'OPEN') {\r\n throw new CircuitBreakerOpenError();\r\n }\r\n\r\n try {\r\n const result = await fn();\r\n this.recordSuccess();\r\n return result;\r\n } catch (error) {\r\n this.recordFailure();\r\n throw error;\r\n }\r\n }\r\n\r\n private recordSuccess(): void {\r\n if (this.state === 'HALF_OPEN') {\r\n this.transitionTo('CLOSED');\r\n }\r\n this.failureCount = 0;\r\n }\r\n\r\n private recordFailure(): void {\r\n this.failureCount++;\r\n this.lastFailureTime = Date.now();\r\n\r\n if (this.state === 'HALF_OPEN' || this.failureCount >= this.failureThreshold) {\r\n this.transitionTo('OPEN');\r\n }\r\n }\r\n\r\n private transitionTo(nextState: CircuitState): void {\r\n const prevState = this.state;\r\n if (prevState !== nextState) {\r\n this.state = nextState;\r\n this.onStateChange?.(prevState, nextState);\r\n }\r\n }\r\n\r\n public reset(): void {\r\n this.state = 'CLOSED';\r\n this.failureCount = 0;\r\n this.lastFailureTime = 0;\r\n }\r\n}"],"mappings":"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","names":[]}
package/dist/index.mjs CHANGED
@@ -67,6 +67,11 @@ function parseDurationToMs(value) {
67
67
  }
68
68
  function parseRateLimitDelay(headers) {
69
69
  if (!headers) return null;
70
+ const retryAfterMs = getHeader(headers, "retry-after-ms");
71
+ if (retryAfterMs) {
72
+ const parsed = parseFloat(retryAfterMs);
73
+ if (!isNaN(parsed)) return Math.max(0, Math.floor(parsed));
74
+ }
70
75
  const retryAfter = getHeader(headers, "retry-after");
71
76
  if (retryAfter) {
72
77
  if (/^\d+(\.\d+)?$/.test(retryAfter.trim())) {
@@ -78,15 +83,23 @@ function parseRateLimitDelay(headers) {
78
83
  return Math.max(0, parsedDate - Date.now());
79
84
  }
80
85
  }
81
- const openaiReset = getHeader(headers, "x-ratelimit-reset-requests");
82
- if (openaiReset) {
83
- const durationMs = parseDurationToMs(openaiReset);
84
- if (durationMs !== null) return durationMs;
85
- }
86
- const anthropicReset = getHeader(headers, "x-ratelimit-reset-tokens");
87
- if (anthropicReset) {
88
- const durationMs = parseDurationToMs(anthropicReset);
89
- if (durationMs !== null) return durationMs;
86
+ const candidateKeys = [
87
+ "x-ratelimit-reset-requests",
88
+ "x-ratelimit-reset-tokens",
89
+ "anthropic-ratelimit-requests-reset",
90
+ "anthropic-ratelimit-tokens-reset",
91
+ "x-ratelimit-reset-total"
92
+ ];
93
+ for (const key of candidateKeys) {
94
+ const raw = getHeader(headers, key);
95
+ if (raw) {
96
+ const ms = parseDurationToMs(raw);
97
+ if (ms !== null) return ms;
98
+ const epochSec = parseFloat(raw);
99
+ if (!isNaN(epochSec) && epochSec > 1e9) {
100
+ return Math.max(0, Math.floor(epochSec * 1e3 - Date.now()));
101
+ }
102
+ }
90
103
  }
91
104
  return null;
92
105
  }
@@ -95,9 +108,17 @@ function isTransientNetworkError(error) {
95
108
  const msg = error.message.toLowerCase();
96
109
  const code = error.code;
97
110
  if (error.name === "TypeError" && msg.includes("fetch failed")) return true;
98
- const transientCodes = ["ECONNRESET", "ETIMEDOUT", "ENOTFOUND", "EAI_AGAIN", "ECONNREFUSED"];
111
+ const transientCodes = [
112
+ "ECONNRESET",
113
+ "ETIMEDOUT",
114
+ "ENOTFOUND",
115
+ "EAI_AGAIN",
116
+ "ECONNREFUSED",
117
+ "UND_ERR_CONNECT_TIMEOUT",
118
+ "UND_ERR_SOCKET"
119
+ ];
99
120
  if (code && transientCodes.includes(code)) return true;
100
- return msg.includes("network error") || msg.includes("socket hang up") || msg.includes("timeout");
121
+ return msg.includes("network error") || msg.includes("socket hang up") || msg.includes("terminated") || msg.includes("econnreset");
101
122
  }
102
123
  function sleep(ms, signal) {
103
124
  return new Promise((resolve, reject) => {
@@ -122,7 +143,9 @@ async function retryFetch(url, init, options = {}) {
122
143
  retries = 3,
123
144
  baseDelayMs = 500,
124
145
  maxDelayMs = 3e4,
146
+ timeoutMs,
125
147
  retryOnStatus = DEFAULT_RETRYABLE_STATUS_CODES,
148
+ shouldRetry,
126
149
  onRetry,
127
150
  signal
128
151
  } = options;
@@ -131,12 +154,25 @@ async function retryFetch(url, init, options = {}) {
131
154
  if (signal?.aborted) {
132
155
  throw signal.reason ?? new DOMException("The operation was aborted.", "AbortError");
133
156
  }
157
+ const attemptController = new AbortController();
158
+ let timeoutId;
159
+ if (timeoutMs && timeoutMs > 0) {
160
+ timeoutId = setTimeout(() => {
161
+ attemptController.abort(new DOMException(`Request timed out after ${timeoutMs}ms`, "TimeoutError"));
162
+ }, timeoutMs);
163
+ }
164
+ const abortHandler = () => attemptController.abort(signal?.reason);
165
+ signal?.addEventListener("abort", abortHandler, { once: true });
134
166
  try {
135
167
  const response = await fetch(url, {
136
168
  ...init,
137
- signal: signal ?? init?.signal
169
+ signal: attemptController.signal
138
170
  });
139
- if (retryOnStatus.includes(response.status) && attempt < retries) {
171
+ if (timeoutId) clearTimeout(timeoutId);
172
+ signal?.removeEventListener("abort", abortHandler);
173
+ const isStatusRetryable = retryOnStatus.includes(response.status);
174
+ const customRetry = shouldRetry ? shouldRetry(response, attempt) : isStatusRetryable;
175
+ if (customRetry && attempt < retries) {
140
176
  const headerDelay = parseRateLimitDelay(response.headers);
141
177
  const backoffDelay = computeJitterDelay(attempt, baseDelayMs, maxDelayMs);
142
178
  const chosenDelay = Math.min(maxDelayMs, headerDelay ?? backoffDelay);
@@ -147,10 +183,15 @@ async function retryFetch(url, init, options = {}) {
147
183
  }
148
184
  return response;
149
185
  } catch (error) {
150
- if (signal?.aborted || error instanceof Error && error.name === "AbortError") {
151
- throw error;
186
+ if (timeoutId) clearTimeout(timeoutId);
187
+ signal?.removeEventListener("abort", abortHandler);
188
+ if (signal?.aborted) {
189
+ throw signal.reason ?? error;
152
190
  }
153
- if (isTransientNetworkError(error) && attempt < retries) {
191
+ const isTimeout = error instanceof Error && error.name === "TimeoutError";
192
+ const isNetworkDrop = isTransientNetworkError(error);
193
+ const customRetry = shouldRetry ? shouldRetry(error, attempt) : isNetworkDrop || isTimeout;
194
+ if (customRetry && attempt < retries) {
154
195
  const chosenDelay = computeJitterDelay(attempt, baseDelayMs, maxDelayMs);
155
196
  onRetry?.(error, attempt + 1, chosenDelay);
156
197
  await sleep(chosenDelay, signal);
@@ -169,6 +210,7 @@ async function retry(fn, options = {}) {
169
210
  baseDelayMs = 500,
170
211
  maxDelayMs = 3e4,
171
212
  retryOnStatus = DEFAULT_RETRYABLE_STATUS_CODES,
213
+ shouldRetry,
172
214
  onRetry,
173
215
  signal
174
216
  } = options;
@@ -191,7 +233,8 @@ async function retry(fn, options = {}) {
191
233
  const headers = err?.headers ?? err?.response?.headers;
192
234
  const isStatusRetryable = statusCode !== void 0 && retryOnStatus.includes(statusCode);
193
235
  const isNetworkRetryable = isTransientNetworkError(error);
194
- if (!isStatusRetryable && !isNetworkRetryable) {
236
+ const customRetry = shouldRetry ? shouldRetry(error, attempt) : isStatusRetryable || isNetworkRetryable;
237
+ if (!customRetry) {
195
238
  throw error;
196
239
  }
197
240
  let chosenDelay;
@@ -207,7 +250,116 @@ async function retry(fn, options = {}) {
207
250
  }
208
251
  }
209
252
  }
253
+
254
+ // src/stream.ts
255
+ async function* retryStream(streamFactory, options = {}) {
256
+ const {
257
+ retries = 3,
258
+ baseDelayMs = 500,
259
+ maxDelayMs = 3e4,
260
+ onRetry,
261
+ signal
262
+ } = options;
263
+ let attempt = 0;
264
+ while (true) {
265
+ if (signal?.aborted) {
266
+ throw signal.reason ?? new DOMException("The operation was aborted.", "AbortError");
267
+ }
268
+ try {
269
+ const stream = await streamFactory(attempt);
270
+ for await (const chunk of stream) {
271
+ if (signal?.aborted) {
272
+ throw signal.reason ?? new DOMException("The operation was aborted.", "AbortError");
273
+ }
274
+ yield chunk;
275
+ }
276
+ return;
277
+ } catch (error) {
278
+ if (signal?.aborted || error instanceof Error && error.name === "AbortError") {
279
+ throw error;
280
+ }
281
+ if (attempt >= retries) {
282
+ throw error;
283
+ }
284
+ const delay = computeJitterDelay(attempt, baseDelayMs, maxDelayMs);
285
+ onRetry?.(error, attempt + 1, delay);
286
+ await sleep(delay, signal);
287
+ attempt++;
288
+ }
289
+ }
290
+ }
291
+
292
+ // src/circuit-breaker.ts
293
+ var CircuitBreakerOpenError = class extends Error {
294
+ constructor(message = "Circuit breaker is OPEN. Fast-failing requests.") {
295
+ super(message);
296
+ this.name = "CircuitBreakerOpenError";
297
+ }
298
+ };
299
+ var CircuitBreaker = class {
300
+ state = "CLOSED";
301
+ failureCount = 0;
302
+ lastFailureTime = 0;
303
+ failureThreshold;
304
+ resetTimeoutMs;
305
+ onStateChange;
306
+ constructor(options = {}) {
307
+ this.failureThreshold = options.failureThreshold ?? 5;
308
+ this.resetTimeoutMs = options.resetTimeoutMs ?? 3e4;
309
+ this.onStateChange = options.onStateChange;
310
+ }
311
+ getState() {
312
+ if (this.state === "OPEN") {
313
+ const elapsed = Date.now() - this.lastFailureTime;
314
+ if (elapsed >= this.resetTimeoutMs) {
315
+ this.transitionTo("HALF_OPEN");
316
+ }
317
+ }
318
+ return this.state;
319
+ }
320
+ async execute(fn) {
321
+ const currentState = this.getState();
322
+ if (currentState === "OPEN") {
323
+ throw new CircuitBreakerOpenError();
324
+ }
325
+ try {
326
+ const result = await fn();
327
+ this.recordSuccess();
328
+ return result;
329
+ } catch (error) {
330
+ this.recordFailure();
331
+ throw error;
332
+ }
333
+ }
334
+ recordSuccess() {
335
+ if (this.state === "HALF_OPEN") {
336
+ this.transitionTo("CLOSED");
337
+ }
338
+ this.failureCount = 0;
339
+ }
340
+ recordFailure() {
341
+ this.failureCount++;
342
+ this.lastFailureTime = Date.now();
343
+ if (this.state === "HALF_OPEN" || this.failureCount >= this.failureThreshold) {
344
+ this.transitionTo("OPEN");
345
+ }
346
+ }
347
+ transitionTo(nextState) {
348
+ const prevState = this.state;
349
+ if (prevState !== nextState) {
350
+ this.state = nextState;
351
+ this.onStateChange?.(prevState, nextState);
352
+ }
353
+ }
354
+ reset() {
355
+ this.state = "CLOSED";
356
+ this.failureCount = 0;
357
+ this.lastFailureTime = 0;
358
+ }
359
+ };
210
360
  export {
361
+ CircuitBreaker,
362
+ CircuitBreakerOpenError,
211
363
  DEFAULT_RETRYABLE_STATUS_CODES,
212
364
  computeJitterDelay,
213
365
  isTransientNetworkError,
@@ -215,6 +367,7 @@ export {
215
367
  parseRateLimitDelay,
216
368
  retry,
217
369
  retryFetch,
370
+ retryStream,
218
371
  sleep
219
372
  };
220
373
  //# sourceMappingURL=index.mjs.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/engine.ts","../src/fetch.ts","../src/wrap.ts"],"sourcesContent":["import { HeaderSource } from './types.js';\r\n\r\nexport const DEFAULT_RETRYABLE_STATUS_CODES = [408, 429, 500, 502, 503, 504];\r\n\r\n/**\r\n * Calculates exponential backoff with full jitter:\r\n * sleepMs = min(maxDelayMs, baseDelayMs * 2 ** attempt) * Math.random()\r\n */\r\nexport function computeJitterDelay(\r\n attempt: number,\r\n baseDelayMs: number = 500,\r\n maxDelayMs: number = 30000\r\n): number {\r\n const exponential = baseDelayMs * Math.pow(2, attempt);\r\n const ceiling = Math.min(maxDelayMs, exponential);\r\n return Math.floor(ceiling * Math.random());\r\n}\r\n\r\n/**\r\n * Safely extracts a header value case-insensitively across Headers, records, or tuples.\r\n */\r\nexport function getHeader(headers: HeaderSource, name: string): string | null {\r\n if (!headers) return null;\r\n\r\n const target = name.toLowerCase();\r\n\r\n // Native Web API Headers\r\n if (typeof (headers as Headers).get === 'function') {\r\n return (headers as Headers).get(target);\r\n }\r\n\r\n // Array of tuples\r\n if (Array.isArray(headers)) {\r\n for (const [key, value] of headers) {\r\n if (key.toLowerCase() === target) {\r\n return value;\r\n }\r\n }\r\n return null;\r\n }\r\n\r\n // Plain object\r\n if (typeof headers === 'object') {\r\n for (const key of Object.keys(headers)) {\r\n if (key.toLowerCase() === target) {\r\n const val = (headers as Record<string, unknown>)[key];\r\n if (Array.isArray(val)) return val[0] ?? null;\r\n return typeof val === 'string' ? val : String(val);\r\n }\r\n }\r\n }\r\n\r\n return null;\r\n}\r\n\r\n/**\r\n * Parses duration strings returned by LLM APIs (e.g. \"600ms\", \"10s\", \"2m\", \"1m30s\", \"1.5\").\r\n */\r\nexport function parseDurationToMs(value: string): number | null {\r\n const trimmed = value.trim();\r\n if (!trimmed) return null;\r\n\r\n // Pure numeric string represents seconds\r\n if (/^\\d+(\\.\\d+)?$/.test(trimmed)) {\r\n const sec = parseFloat(trimmed);\r\n return isNaN(sec) ? null : Math.max(0, Math.floor(sec * 1000));\r\n }\r\n\r\n // Units match\r\n const regex = /(\\d+(?:\\.\\d+)?)\\s*(ms|s|m|h|d)/gi;\r\n let match: RegExpExecArray | null;\r\n let totalMs = 0;\r\n let matched = false;\r\n\r\n while ((match = regex.exec(trimmed)) !== null) {\r\n matched = true;\r\n const amount = parseFloat(match[1]);\r\n const unit = match[2].toLowerCase();\r\n\r\n switch (unit) {\r\n case 'ms':\r\n totalMs += amount;\r\n break;\r\n case 's':\r\n totalMs += amount * 1000;\r\n break;\r\n case 'm':\r\n totalMs += amount * 60 * 1000;\r\n break;\r\n case 'h':\r\n totalMs += amount * 60 * 60 * 1000;\r\n break;\r\n case 'd':\r\n totalMs += amount * 24 * 60 * 60 * 1000;\r\n break;\r\n }\r\n }\r\n\r\n return matched ? Math.max(0, Math.floor(totalMs)) : null;\r\n}\r\n\r\n/**\r\n * Inspects rate-limiting headers and extracts sleep delay in milliseconds:\r\n * - retry-after (seconds or HTTP date)\r\n * - x-ratelimit-reset-requests (OpenAI format)\r\n * - x-ratelimit-reset-tokens (Anthropic format)\r\n */\r\nexport function parseRateLimitDelay(headers: HeaderSource): number | null {\r\n if (!headers) return null;\r\n\r\n const retryAfter = getHeader(headers, 'retry-after');\r\n if (retryAfter) {\r\n if (/^\\d+(\\.\\d+)?$/.test(retryAfter.trim())) {\r\n const sec = parseFloat(retryAfter.trim());\r\n if (!isNaN(sec)) return Math.max(0, Math.floor(sec * 1000));\r\n }\r\n\r\n const parsedDate = Date.parse(retryAfter);\r\n if (!isNaN(parsedDate)) {\r\n return Math.max(0, parsedDate - Date.now());\r\n }\r\n }\r\n\r\n const openaiReset = getHeader(headers, 'x-ratelimit-reset-requests');\r\n if (openaiReset) {\r\n const durationMs = parseDurationToMs(openaiReset);\r\n if (durationMs !== null) return durationMs;\r\n }\r\n\r\n const anthropicReset = getHeader(headers, 'x-ratelimit-reset-tokens');\r\n if (anthropicReset) {\r\n const durationMs = parseDurationToMs(anthropicReset);\r\n if (durationMs !== null) return durationMs;\r\n }\r\n\r\n return null;\r\n}\r\n\r\n/**\r\n * Detects transient network drop errors.\r\n */\r\nexport function isTransientNetworkError(error: unknown): boolean {\r\n if (!(error instanceof Error)) return false;\r\n\r\n const msg = error.message.toLowerCase();\r\n const code = (error as { code?: string }).code;\r\n\r\n if (error.name === 'TypeError' && msg.includes('fetch failed')) return true;\r\n\r\n const transientCodes = ['ECONNRESET', 'ETIMEDOUT', 'ENOTFOUND', 'EAI_AGAIN', 'ECONNREFUSED'];\r\n if (code && transientCodes.includes(code)) return true;\r\n\r\n return (\r\n msg.includes('network error') ||\r\n msg.includes('socket hang up') ||\r\n msg.includes('timeout')\r\n );\r\n}\r\n\r\n/**\r\n * Sleep helper respecting AbortSignal cancellation.\r\n */\r\nexport function sleep(ms: number, signal?: AbortSignal): Promise<void> {\r\n return new Promise((resolve, reject) => {\r\n if (signal?.aborted) {\r\n return reject(signal.reason ?? new DOMException('The operation was aborted.', 'AbortError'));\r\n }\r\n\r\n const timer = setTimeout(() => {\r\n signal?.removeEventListener('abort', onAbort);\r\n resolve();\r\n }, ms);\r\n\r\n function onAbort() {\r\n clearTimeout(timer);\r\n reject(signal?.reason ?? new DOMException('The operation was aborted.', 'AbortError'));\r\n }\r\n\r\n signal?.addEventListener('abort', onAbort, { once: true });\r\n });\r\n}","import {\r\n computeJitterDelay,\r\n DEFAULT_RETRYABLE_STATUS_CODES,\r\n isTransientNetworkError,\r\n parseRateLimitDelay,\r\n sleep,\r\n} from './engine.js';\r\nimport { RetryOptions } from './types.js';\r\n\r\nexport async function retryFetch(\r\n url: string | URL | Request,\r\n init?: RequestInit,\r\n options: RetryOptions = {}\r\n): Promise<Response> {\r\n const {\r\n retries = 3,\r\n baseDelayMs = 500,\r\n maxDelayMs = 30000,\r\n retryOnStatus = DEFAULT_RETRYABLE_STATUS_CODES,\r\n onRetry,\r\n signal,\r\n } = options;\r\n\r\n let attempt = 0;\r\n\r\n while (true) {\r\n if (signal?.aborted) {\r\n throw signal.reason ?? new DOMException('The operation was aborted.', 'AbortError');\r\n }\r\n\r\n try {\r\n const response = await fetch(url, {\r\n ...init,\r\n signal: signal ?? init?.signal,\r\n });\r\n\r\n if (retryOnStatus.includes(response.status) && attempt < retries) {\r\n const headerDelay = parseRateLimitDelay(response.headers);\r\n const backoffDelay = computeJitterDelay(attempt, baseDelayMs, maxDelayMs);\r\n const chosenDelay = Math.min(maxDelayMs, headerDelay ?? backoffDelay);\r\n\r\n onRetry?.(response, attempt + 1, chosenDelay);\r\n await sleep(chosenDelay, signal);\r\n attempt++;\r\n continue;\r\n }\r\n\r\n return response;\r\n } catch (error) {\r\n if (signal?.aborted || (error instanceof Error && error.name === 'AbortError')) {\r\n throw error;\r\n }\r\n\r\n if (isTransientNetworkError(error) && attempt < retries) {\r\n const chosenDelay = computeJitterDelay(attempt, baseDelayMs, maxDelayMs);\r\n onRetry?.(error, attempt + 1, chosenDelay);\r\n await sleep(chosenDelay, signal);\r\n attempt++;\r\n continue;\r\n }\r\n\r\n throw error;\r\n }\r\n }\r\n}","import {\r\n computeJitterDelay,\r\n DEFAULT_RETRYABLE_STATUS_CODES,\r\n isTransientNetworkError,\r\n parseRateLimitDelay,\r\n sleep,\r\n} from './engine.js';\r\nimport { HeaderSource, RetryOptions } from './types.js';\r\n\r\ninterface ErrorWithStatus extends Error {\r\n status?: number;\r\n statusCode?: number;\r\n headers?: HeaderSource;\r\n response?: {\r\n status?: number;\r\n headers?: HeaderSource;\r\n };\r\n}\r\n\r\nexport async function retry<T>(\r\n fn: (attempt: number) => Promise<T>,\r\n options: RetryOptions = {}\r\n): Promise<T> {\r\n const {\r\n retries = 3,\r\n baseDelayMs = 500,\r\n maxDelayMs = 30000,\r\n retryOnStatus = DEFAULT_RETRYABLE_STATUS_CODES,\r\n onRetry,\r\n signal,\r\n } = options;\r\n\r\n let attempt = 0;\r\n\r\n while (true) {\r\n if (signal?.aborted) {\r\n throw signal.reason ?? new DOMException('The operation was aborted.', 'AbortError');\r\n }\r\n\r\n try {\r\n return await fn(attempt);\r\n } catch (error) {\r\n if (signal?.aborted || (error instanceof Error && error.name === 'AbortError')) {\r\n throw error;\r\n }\r\n\r\n if (attempt >= retries) {\r\n throw error;\r\n }\r\n\r\n const err = error as ErrorWithStatus;\r\n const statusCode = err?.status ?? err?.statusCode ?? err?.response?.status;\r\n const headers = err?.headers ?? err?.response?.headers;\r\n\r\n const isStatusRetryable = statusCode !== undefined && retryOnStatus.includes(statusCode);\r\n const isNetworkRetryable = isTransientNetworkError(error);\r\n\r\n if (!isStatusRetryable && !isNetworkRetryable) {\r\n throw error;\r\n }\r\n\r\n let chosenDelay: number;\r\n const headerDelay = headers ? parseRateLimitDelay(headers) : null;\r\n\r\n if (headerDelay !== null) {\r\n chosenDelay = Math.min(maxDelayMs, headerDelay);\r\n } else {\r\n chosenDelay = computeJitterDelay(attempt, baseDelayMs, maxDelayMs);\r\n }\r\n\r\n onRetry?.(error, attempt + 1, chosenDelay);\r\n await sleep(chosenDelay, signal);\r\n attempt++;\r\n }\r\n }\r\n}"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../src/engine.ts","../src/fetch.ts","../src/wrap.ts","../src/stream.ts","../src/circuit-breaker.ts"],"sourcesContent":["import { HeaderSource } from './types.js';\r\n\r\nexport const DEFAULT_RETRYABLE_STATUS_CODES = [408, 429, 500, 502, 503, 504];\r\n\r\n/**\r\n * Calculates exponential backoff with full jitter:\r\n * sleepMs = min(maxDelayMs, baseDelayMs * 2 ** attempt) * Math.random()\r\n */\r\nexport function computeJitterDelay(\r\n attempt: number,\r\n baseDelayMs: number = 500,\r\n maxDelayMs: number = 30000\r\n): number {\r\n const exponential = baseDelayMs * Math.pow(2, attempt);\r\n const ceiling = Math.min(maxDelayMs, exponential);\r\n return Math.floor(ceiling * Math.random());\r\n}\r\n\r\n/**\r\n * Safely extracts a header value case-insensitively across Headers instances, records, or tuples.\r\n */\r\nexport function getHeader(headers: HeaderSource, name: string): string | null {\r\n if (!headers) return null;\r\n\r\n const target = name.toLowerCase();\r\n\r\n if (typeof (headers as Headers).get === 'function') {\r\n return (headers as Headers).get(target);\r\n }\r\n\r\n if (Array.isArray(headers)) {\r\n for (const [key, value] of headers) {\r\n if (key.toLowerCase() === target) {\r\n return value;\r\n }\r\n }\r\n return null;\r\n }\r\n\r\n if (typeof headers === 'object') {\r\n for (const key of Object.keys(headers)) {\r\n if (key.toLowerCase() === target) {\r\n const val = (headers as Record<string, unknown>)[key];\r\n if (Array.isArray(val)) return val[0] ?? null;\r\n return typeof val === 'string' ? val : String(val);\r\n }\r\n }\r\n }\r\n\r\n return null;\r\n}\r\n\r\n/**\r\n * Parses duration strings returned by LLM APIs (e.g. \"600ms\", \"10s\", \"2m\", \"1m30s\", \"1.5\").\r\n */\r\nexport function parseDurationToMs(value: string): number | null {\r\n const trimmed = value.trim();\r\n if (!trimmed) return null;\r\n\r\n if (/^\\d+(\\.\\d+)?$/.test(trimmed)) {\r\n const sec = parseFloat(trimmed);\r\n return isNaN(sec) ? null : Math.max(0, Math.floor(sec * 1000));\r\n }\r\n\r\n const regex = /(\\d+(?:\\.\\d+)?)\\s*(ms|s|m|h|d)/gi;\r\n let match: RegExpExecArray | null;\r\n let totalMs = 0;\r\n let matched = false;\r\n\r\n while ((match = regex.exec(trimmed)) !== null) {\r\n matched = true;\r\n const amount = parseFloat(match[1]);\r\n const unit = match[2].toLowerCase();\r\n\r\n switch (unit) {\r\n case 'ms':\r\n totalMs += amount;\r\n break;\r\n case 's':\r\n totalMs += amount * 1000;\r\n break;\r\n case 'm':\r\n totalMs += amount * 60 * 1000;\r\n break;\r\n case 'h':\r\n totalMs += amount * 60 * 60 * 1000;\r\n break;\r\n case 'd':\r\n totalMs += amount * 24 * 60 * 60 * 1000;\r\n break;\r\n }\r\n }\r\n\r\n return matched ? Math.max(0, Math.floor(totalMs)) : null;\r\n}\r\n\r\n/**\r\n * Exhaustively checks provider headers across:\r\n * - RFC 7231 / RFC 6585 standard `retry-after`\r\n * - OpenAI: `x-ratelimit-reset-requests`, `x-ratelimit-reset-tokens`\r\n * - Anthropic: `anthropic-ratelimit-requests-reset`, `anthropic-ratelimit-tokens-reset`\r\n * - Groq / DeepSeek / Mistral: `x-ratelimit-reset-total`, `retry-after-ms`\r\n */\r\nexport function parseRateLimitDelay(headers: HeaderSource): number | null {\r\n if (!headers) return null;\r\n\r\n // 1. Direct milliseconds header (Groq, custom proxies)\r\n const retryAfterMs = getHeader(headers, 'retry-after-ms');\r\n if (retryAfterMs) {\r\n const parsed = parseFloat(retryAfterMs);\r\n if (!isNaN(parsed)) return Math.max(0, Math.floor(parsed));\r\n }\r\n\r\n // 2. Standard Retry-After (seconds or RFC 1123 HTTP Date)\r\n const retryAfter = getHeader(headers, 'retry-after');\r\n if (retryAfter) {\r\n if (/^\\d+(\\.\\d+)?$/.test(retryAfter.trim())) {\r\n const sec = parseFloat(retryAfter.trim());\r\n if (!isNaN(sec)) return Math.max(0, Math.floor(sec * 1000));\r\n }\r\n\r\n const parsedDate = Date.parse(retryAfter);\r\n if (!isNaN(parsedDate)) {\r\n return Math.max(0, parsedDate - Date.now());\r\n }\r\n }\r\n\r\n // 3. Provider-specific reset headers\r\n const candidateKeys = [\r\n 'x-ratelimit-reset-requests',\r\n 'x-ratelimit-reset-tokens',\r\n 'anthropic-ratelimit-requests-reset',\r\n 'anthropic-ratelimit-tokens-reset',\r\n 'x-ratelimit-reset-total',\r\n ];\r\n\r\n for (const key of candidateKeys) {\r\n const raw = getHeader(headers, key);\r\n if (raw) {\r\n const ms = parseDurationToMs(raw);\r\n if (ms !== null) return ms;\r\n\r\n // Handle epoch timestamp formats\r\n const epochSec = parseFloat(raw);\r\n if (!isNaN(epochSec) && epochSec > 1000000000) {\r\n return Math.max(0, Math.floor(epochSec * 1000 - Date.now()));\r\n }\r\n }\r\n }\r\n\r\n return null;\r\n}\r\n\r\n/**\r\n * Detects transient network drop errors across Node, Bun, Deno, and browsers.\r\n */\r\nexport function isTransientNetworkError(error: unknown): boolean {\r\n if (!(error instanceof Error)) return false;\r\n\r\n const msg = error.message.toLowerCase();\r\n const code = (error as { code?: string }).code;\r\n\r\n if (error.name === 'TypeError' && msg.includes('fetch failed')) return true;\r\n\r\n const transientCodes = [\r\n 'ECONNRESET',\r\n 'ETIMEDOUT',\r\n 'ENOTFOUND',\r\n 'EAI_AGAIN',\r\n 'ECONNREFUSED',\r\n 'UND_ERR_CONNECT_TIMEOUT',\r\n 'UND_ERR_SOCKET',\r\n ];\r\n if (code && transientCodes.includes(code)) return true;\r\n\r\n return (\r\n msg.includes('network error') ||\r\n msg.includes('socket hang up') ||\r\n msg.includes('terminated') ||\r\n msg.includes('econnreset')\r\n );\r\n}\r\n\r\n/**\r\n * Promisified sleep that respects AbortSignal cancellation.\r\n */\r\nexport function sleep(ms: number, signal?: AbortSignal): Promise<void> {\r\n return new Promise((resolve, reject) => {\r\n if (signal?.aborted) {\r\n return reject(signal.reason ?? new DOMException('The operation was aborted.', 'AbortError'));\r\n }\r\n\r\n const timer = setTimeout(() => {\r\n signal?.removeEventListener('abort', onAbort);\r\n resolve();\r\n }, ms);\r\n\r\n function onAbort() {\r\n clearTimeout(timer);\r\n reject(signal?.reason ?? new DOMException('The operation was aborted.', 'AbortError'));\r\n }\r\n\r\n signal?.addEventListener('abort', onAbort, { once: true });\r\n });\r\n}","import {\r\n computeJitterDelay,\r\n DEFAULT_RETRYABLE_STATUS_CODES,\r\n isTransientNetworkError,\r\n parseRateLimitDelay,\r\n sleep,\r\n} from './engine.js';\r\nimport { RetryOptions } from './types.js';\r\n\r\nexport async function retryFetch(\r\n url: string | URL | Request,\r\n init?: RequestInit,\r\n options: RetryOptions = {}\r\n): Promise<Response> {\r\n const {\r\n retries = 3,\r\n baseDelayMs = 500,\r\n maxDelayMs = 30000,\r\n timeoutMs,\r\n retryOnStatus = DEFAULT_RETRYABLE_STATUS_CODES,\r\n shouldRetry,\r\n onRetry,\r\n signal,\r\n } = options;\r\n\r\n let attempt = 0;\r\n\r\n while (true) {\r\n if (signal?.aborted) {\r\n throw signal.reason ?? new DOMException('The operation was aborted.', 'AbortError');\r\n }\r\n\r\n // Set up per-attempt timeout controller\r\n const attemptController = new AbortController();\r\n let timeoutId: ReturnType<typeof setTimeout> | undefined;\r\n\r\n if (timeoutMs && timeoutMs > 0) {\r\n timeoutId = setTimeout(() => {\r\n attemptController.abort(new DOMException(`Request timed out after ${timeoutMs}ms`, 'TimeoutError'));\r\n }, timeoutMs);\r\n }\r\n\r\n const abortHandler = () => attemptController.abort(signal?.reason);\r\n signal?.addEventListener('abort', abortHandler, { once: true });\r\n\r\n try {\r\n const response = await fetch(url, {\r\n ...init,\r\n signal: attemptController.signal,\r\n });\r\n\r\n if (timeoutId) clearTimeout(timeoutId);\r\n signal?.removeEventListener('abort', abortHandler);\r\n\r\n const isStatusRetryable = retryOnStatus.includes(response.status);\r\n const customRetry = shouldRetry ? shouldRetry(response, attempt) : isStatusRetryable;\r\n\r\n if (customRetry && attempt < retries) {\r\n const headerDelay = parseRateLimitDelay(response.headers);\r\n const backoffDelay = computeJitterDelay(attempt, baseDelayMs, maxDelayMs);\r\n const chosenDelay = Math.min(maxDelayMs, headerDelay ?? backoffDelay);\r\n\r\n onRetry?.(response, attempt + 1, chosenDelay);\r\n await sleep(chosenDelay, signal);\r\n attempt++;\r\n continue;\r\n }\r\n\r\n return response;\r\n } catch (error) {\r\n if (timeoutId) clearTimeout(timeoutId);\r\n signal?.removeEventListener('abort', abortHandler);\r\n\r\n // Do not retry intentional outer aborts\r\n if (signal?.aborted) {\r\n throw signal.reason ?? error;\r\n }\r\n\r\n const isTimeout = error instanceof Error && error.name === 'TimeoutError';\r\n const isNetworkDrop = isTransientNetworkError(error);\r\n const customRetry = shouldRetry ? shouldRetry(error, attempt) : (isNetworkDrop || isTimeout);\r\n\r\n if (customRetry && attempt < retries) {\r\n const chosenDelay = computeJitterDelay(attempt, baseDelayMs, maxDelayMs);\r\n onRetry?.(error, attempt + 1, chosenDelay);\r\n await sleep(chosenDelay, signal);\r\n attempt++;\r\n continue;\r\n }\r\n\r\n throw error;\r\n }\r\n }\r\n}","import {\r\n computeJitterDelay,\r\n DEFAULT_RETRYABLE_STATUS_CODES,\r\n isTransientNetworkError,\r\n parseRateLimitDelay,\r\n sleep,\r\n} from './engine.js';\r\nimport { HeaderSource, RetryOptions } from './types.js';\r\n\r\ninterface ErrorWithStatus extends Error {\r\n status?: number;\r\n statusCode?: number;\r\n headers?: HeaderSource;\r\n response?: {\r\n status?: number;\r\n headers?: HeaderSource;\r\n };\r\n}\r\n\r\nexport async function retry<T>(\r\n fn: (attempt: number) => Promise<T>,\r\n options: RetryOptions = {}\r\n): Promise<T> {\r\n const {\r\n retries = 3,\r\n baseDelayMs = 500,\r\n maxDelayMs = 30000,\r\n retryOnStatus = DEFAULT_RETRYABLE_STATUS_CODES,\r\n shouldRetry,\r\n onRetry,\r\n signal,\r\n } = options;\r\n\r\n let attempt = 0;\r\n\r\n while (true) {\r\n if (signal?.aborted) {\r\n throw signal.reason ?? new DOMException('The operation was aborted.', 'AbortError');\r\n }\r\n\r\n try {\r\n return await fn(attempt);\r\n } catch (error) {\r\n if (signal?.aborted || (error instanceof Error && error.name === 'AbortError')) {\r\n throw error;\r\n }\r\n\r\n if (attempt >= retries) {\r\n throw error;\r\n }\r\n\r\n const err = error as ErrorWithStatus;\r\n const statusCode = err?.status ?? err?.statusCode ?? err?.response?.status;\r\n const headers = err?.headers ?? err?.response?.headers;\r\n\r\n const isStatusRetryable = statusCode !== undefined && retryOnStatus.includes(statusCode);\r\n const isNetworkRetryable = isTransientNetworkError(error);\r\n const customRetry = shouldRetry\r\n ? shouldRetry(error, attempt)\r\n : (isStatusRetryable || isNetworkRetryable);\r\n\r\n if (!customRetry) {\r\n throw error;\r\n }\r\n\r\n let chosenDelay: number;\r\n const headerDelay = headers ? parseRateLimitDelay(headers) : null;\r\n\r\n if (headerDelay !== null) {\r\n chosenDelay = Math.min(maxDelayMs, headerDelay);\r\n } else {\r\n chosenDelay = computeJitterDelay(attempt, baseDelayMs, maxDelayMs);\r\n }\r\n\r\n onRetry?.(error, attempt + 1, chosenDelay);\r\n await sleep(chosenDelay, signal);\r\n attempt++;\r\n }\r\n }\r\n}","import { computeJitterDelay, sleep } from './engine.js';\r\nimport { RetryOptions } from './types.js';\r\n\r\n/**\r\n * Resilient wrapper for Server-Sent Events / LLM chunk streams.\r\n * If the streaming connection drops mid-generation, it retries and resumes.\r\n */\r\nexport async function* retryStream<T>(\r\n streamFactory: (attempt: number) => Promise<AsyncIterable<T>>,\r\n options: RetryOptions = {}\r\n): AsyncGenerator<T, void, unknown> {\r\n const {\r\n retries = 3,\r\n baseDelayMs = 500,\r\n maxDelayMs = 30000,\r\n onRetry,\r\n signal,\r\n } = options;\r\n\r\n let attempt = 0;\r\n\r\n while (true) {\r\n if (signal?.aborted) {\r\n throw signal.reason ?? new DOMException('The operation was aborted.', 'AbortError');\r\n }\r\n\r\n try {\r\n const stream = await streamFactory(attempt);\r\n for await (const chunk of stream) {\r\n if (signal?.aborted) {\r\n throw signal.reason ?? new DOMException('The operation was aborted.', 'AbortError');\r\n }\r\n yield chunk;\r\n }\r\n return;\r\n } catch (error) {\r\n if (signal?.aborted || (error instanceof Error && error.name === 'AbortError')) {\r\n throw error;\r\n }\r\n\r\n if (attempt >= retries) {\r\n throw error;\r\n }\r\n\r\n const delay = computeJitterDelay(attempt, baseDelayMs, maxDelayMs);\r\n onRetry?.(error, attempt + 1, delay);\r\n await sleep(delay, signal);\r\n attempt++;\r\n }\r\n }\r\n}","import { CircuitBreakerOptions, CircuitState } from './types.js';\r\n\r\nexport class CircuitBreakerOpenError extends Error {\r\n constructor(message: string = 'Circuit breaker is OPEN. Fast-failing requests.') {\r\n super(message);\r\n this.name = 'CircuitBreakerOpenError';\r\n }\r\n}\r\n\r\n/**\r\n * Zero-dependency Circuit Breaker implementation to stop cascading failure\r\n * cascades when downstream LLM APIs experience total outages.\r\n */\r\nexport class CircuitBreaker {\r\n private state: CircuitState = 'CLOSED';\r\n private failureCount: number = 0;\r\n private lastFailureTime: number = 0;\r\n private readonly failureThreshold: number;\r\n private readonly resetTimeoutMs: number;\r\n private readonly onStateChange?: (from: CircuitState, to: CircuitState) => void;\r\n\r\n constructor(options: CircuitBreakerOptions = {}) {\r\n this.failureThreshold = options.failureThreshold ?? 5;\r\n this.resetTimeoutMs = options.resetTimeoutMs ?? 30000;\r\n this.onStateChange = options.onStateChange;\r\n }\r\n\r\n public getState(): CircuitState {\r\n if (this.state === 'OPEN') {\r\n const elapsed = Date.now() - this.lastFailureTime;\r\n if (elapsed >= this.resetTimeoutMs) {\r\n this.transitionTo('HALF_OPEN');\r\n }\r\n }\r\n return this.state;\r\n }\r\n\r\n public async execute<T>(fn: () => Promise<T>): Promise<T> {\r\n const currentState = this.getState();\r\n\r\n if (currentState === 'OPEN') {\r\n throw new CircuitBreakerOpenError();\r\n }\r\n\r\n try {\r\n const result = await fn();\r\n this.recordSuccess();\r\n return result;\r\n } catch (error) {\r\n this.recordFailure();\r\n throw error;\r\n }\r\n }\r\n\r\n private recordSuccess(): void {\r\n if (this.state === 'HALF_OPEN') {\r\n this.transitionTo('CLOSED');\r\n }\r\n this.failureCount = 0;\r\n }\r\n\r\n private recordFailure(): void {\r\n this.failureCount++;\r\n this.lastFailureTime = Date.now();\r\n\r\n if (this.state === 'HALF_OPEN' || this.failureCount >= this.failureThreshold) {\r\n this.transitionTo('OPEN');\r\n }\r\n }\r\n\r\n private transitionTo(nextState: CircuitState): void {\r\n const prevState = this.state;\r\n if (prevState !== nextState) {\r\n this.state = nextState;\r\n this.onStateChange?.(prevState, nextState);\r\n }\r\n }\r\n\r\n public reset(): void {\r\n this.state = 'CLOSED';\r\n this.failureCount = 0;\r\n this.lastFailureTime = 0;\r\n }\r\n}"],"mappings":";AAEO,IAAM,iCAAiC,CAAC,KAAK,KAAK,KAAK,KAAK,KAAK,GAAG;AAMpE,SAAS,mBACd,SACA,cAAsB,KACtB,aAAqB,KACb;AACR,QAAM,cAAc,cAAc,KAAK,IAAI,GAAG,OAAO;AACrD,QAAM,UAAU,KAAK,IAAI,YAAY,WAAW;AAChD,SAAO,KAAK,MAAM,UAAU,KAAK,OAAO,CAAC;AAC3C;AAKO,SAAS,UAAU,SAAuB,MAA6B;AAC5E,MAAI,CAAC,QAAS,QAAO;AAErB,QAAM,SAAS,KAAK,YAAY;AAEhC,MAAI,OAAQ,QAAoB,QAAQ,YAAY;AAClD,WAAQ,QAAoB,IAAI,MAAM;AAAA,EACxC;AAEA,MAAI,MAAM,QAAQ,OAAO,GAAG;AAC1B,eAAW,CAAC,KAAK,KAAK,KAAK,SAAS;AAClC,UAAI,IAAI,YAAY,MAAM,QAAQ;AAChC,eAAO;AAAA,MACT;AAAA,IACF;AACA,WAAO;AAAA,EACT;AAEA,MAAI,OAAO,YAAY,UAAU;AAC/B,eAAW,OAAO,OAAO,KAAK,OAAO,GAAG;AACtC,UAAI,IAAI,YAAY,MAAM,QAAQ;AAChC,cAAM,MAAO,QAAoC,GAAG;AACpD,YAAI,MAAM,QAAQ,GAAG,EAAG,QAAO,IAAI,CAAC,KAAK;AACzC,eAAO,OAAO,QAAQ,WAAW,MAAM,OAAO,GAAG;AAAA,MACnD;AAAA,IACF;AAAA,EACF;AAEA,SAAO;AACT;AAKO,SAAS,kBAAkB,OAA8B;AAC9D,QAAM,UAAU,MAAM,KAAK;AAC3B,MAAI,CAAC,QAAS,QAAO;AAErB,MAAI,gBAAgB,KAAK,OAAO,GAAG;AACjC,UAAM,MAAM,WAAW,OAAO;AAC9B,WAAO,MAAM,GAAG,IAAI,OAAO,KAAK,IAAI,GAAG,KAAK,MAAM,MAAM,GAAI,CAAC;AAAA,EAC/D;AAEA,QAAM,QAAQ;AACd,MAAI;AACJ,MAAI,UAAU;AACd,MAAI,UAAU;AAEd,UAAQ,QAAQ,MAAM,KAAK,OAAO,OAAO,MAAM;AAC7C,cAAU;AACV,UAAM,SAAS,WAAW,MAAM,CAAC,CAAC;AAClC,UAAM,OAAO,MAAM,CAAC,EAAE,YAAY;AAElC,YAAQ,MAAM;AAAA,MACZ,KAAK;AACH,mBAAW;AACX;AAAA,MACF,KAAK;AACH,mBAAW,SAAS;AACpB;AAAA,MACF,KAAK;AACH,mBAAW,SAAS,KAAK;AACzB;AAAA,MACF,KAAK;AACH,mBAAW,SAAS,KAAK,KAAK;AAC9B;AAAA,MACF,KAAK;AACH,mBAAW,SAAS,KAAK,KAAK,KAAK;AACnC;AAAA,IACJ;AAAA,EACF;AAEA,SAAO,UAAU,KAAK,IAAI,GAAG,KAAK,MAAM,OAAO,CAAC,IAAI;AACtD;AASO,SAAS,oBAAoB,SAAsC;AACxE,MAAI,CAAC,QAAS,QAAO;AAGrB,QAAM,eAAe,UAAU,SAAS,gBAAgB;AACxD,MAAI,cAAc;AAChB,UAAM,SAAS,WAAW,YAAY;AACtC,QAAI,CAAC,MAAM,MAAM,EAAG,QAAO,KAAK,IAAI,GAAG,KAAK,MAAM,MAAM,CAAC;AAAA,EAC3D;AAGA,QAAM,aAAa,UAAU,SAAS,aAAa;AACnD,MAAI,YAAY;AACd,QAAI,gBAAgB,KAAK,WAAW,KAAK,CAAC,GAAG;AAC3C,YAAM,MAAM,WAAW,WAAW,KAAK,CAAC;AACxC,UAAI,CAAC,MAAM,GAAG,EAAG,QAAO,KAAK,IAAI,GAAG,KAAK,MAAM,MAAM,GAAI,CAAC;AAAA,IAC5D;AAEA,UAAM,aAAa,KAAK,MAAM,UAAU;AACxC,QAAI,CAAC,MAAM,UAAU,GAAG;AACtB,aAAO,KAAK,IAAI,GAAG,aAAa,KAAK,IAAI,CAAC;AAAA,IAC5C;AAAA,EACF;AAGA,QAAM,gBAAgB;AAAA,IACpB;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,EACF;AAEA,aAAW,OAAO,eAAe;AAC/B,UAAM,MAAM,UAAU,SAAS,GAAG;AAClC,QAAI,KAAK;AACP,YAAM,KAAK,kBAAkB,GAAG;AAChC,UAAI,OAAO,KAAM,QAAO;AAGxB,YAAM,WAAW,WAAW,GAAG;AAC/B,UAAI,CAAC,MAAM,QAAQ,KAAK,WAAW,KAAY;AAC7C,eAAO,KAAK,IAAI,GAAG,KAAK,MAAM,WAAW,MAAO,KAAK,IAAI,CAAC,CAAC;AAAA,MAC7D;AAAA,IACF;AAAA,EACF;AAEA,SAAO;AACT;AAKO,SAAS,wBAAwB,OAAyB;AAC/D,MAAI,EAAE,iBAAiB,OAAQ,QAAO;AAEtC,QAAM,MAAM,MAAM,QAAQ,YAAY;AACtC,QAAM,OAAQ,MAA4B;AAE1C,MAAI,MAAM,SAAS,eAAe,IAAI,SAAS,cAAc,EAAG,QAAO;AAEvE,QAAM,iBAAiB;AAAA,IACrB;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,EACF;AACA,MAAI,QAAQ,eAAe,SAAS,IAAI,EAAG,QAAO;AAElD,SACE,IAAI,SAAS,eAAe,KAC5B,IAAI,SAAS,gBAAgB,KAC7B,IAAI,SAAS,YAAY,KACzB,IAAI,SAAS,YAAY;AAE7B;AAKO,SAAS,MAAM,IAAY,QAAqC;AACrE,SAAO,IAAI,QAAQ,CAAC,SAAS,WAAW;AACtC,QAAI,QAAQ,SAAS;AACnB,aAAO,OAAO,OAAO,UAAU,IAAI,aAAa,8BAA8B,YAAY,CAAC;AAAA,IAC7F;AAEA,UAAM,QAAQ,WAAW,MAAM;AAC7B,cAAQ,oBAAoB,SAAS,OAAO;AAC5C,cAAQ;AAAA,IACV,GAAG,EAAE;AAEL,aAAS,UAAU;AACjB,mBAAa,KAAK;AAClB,aAAO,QAAQ,UAAU,IAAI,aAAa,8BAA8B,YAAY,CAAC;AAAA,IACvF;AAEA,YAAQ,iBAAiB,SAAS,SAAS,EAAE,MAAM,KAAK,CAAC;AAAA,EAC3D,CAAC;AACH;;;ACnMA,eAAsB,WACpB,KACA,MACA,UAAwB,CAAC,GACN;AACnB,QAAM;AAAA,IACJ,UAAU;AAAA,IACV,cAAc;AAAA,IACd,aAAa;AAAA,IACb;AAAA,IACA,gBAAgB;AAAA,IAChB;AAAA,IACA;AAAA,IACA;AAAA,EACF,IAAI;AAEJ,MAAI,UAAU;AAEd,SAAO,MAAM;AACX,QAAI,QAAQ,SAAS;AACnB,YAAM,OAAO,UAAU,IAAI,aAAa,8BAA8B,YAAY;AAAA,IACpF;AAGA,UAAM,oBAAoB,IAAI,gBAAgB;AAC9C,QAAI;AAEJ,QAAI,aAAa,YAAY,GAAG;AAC9B,kBAAY,WAAW,MAAM;AAC3B,0BAAkB,MAAM,IAAI,aAAa,2BAA2B,SAAS,MAAM,cAAc,CAAC;AAAA,MACpG,GAAG,SAAS;AAAA,IACd;AAEA,UAAM,eAAe,MAAM,kBAAkB,MAAM,QAAQ,MAAM;AACjE,YAAQ,iBAAiB,SAAS,cAAc,EAAE,MAAM,KAAK,CAAC;AAE9D,QAAI;AACF,YAAM,WAAW,MAAM,MAAM,KAAK;AAAA,QAChC,GAAG;AAAA,QACH,QAAQ,kBAAkB;AAAA,MAC5B,CAAC;AAED,UAAI,UAAW,cAAa,SAAS;AACrC,cAAQ,oBAAoB,SAAS,YAAY;AAEjD,YAAM,oBAAoB,cAAc,SAAS,SAAS,MAAM;AAChE,YAAM,cAAc,cAAc,YAAY,UAAU,OAAO,IAAI;AAEnE,UAAI,eAAe,UAAU,SAAS;AACpC,cAAM,cAAc,oBAAoB,SAAS,OAAO;AACxD,cAAM,eAAe,mBAAmB,SAAS,aAAa,UAAU;AACxE,cAAM,cAAc,KAAK,IAAI,YAAY,eAAe,YAAY;AAEpE,kBAAU,UAAU,UAAU,GAAG,WAAW;AAC5C,cAAM,MAAM,aAAa,MAAM;AAC/B;AACA;AAAA,MACF;AAEA,aAAO;AAAA,IACT,SAAS,OAAO;AACd,UAAI,UAAW,cAAa,SAAS;AACrC,cAAQ,oBAAoB,SAAS,YAAY;AAGjD,UAAI,QAAQ,SAAS;AACnB,cAAM,OAAO,UAAU;AAAA,MACzB;AAEA,YAAM,YAAY,iBAAiB,SAAS,MAAM,SAAS;AAC3D,YAAM,gBAAgB,wBAAwB,KAAK;AACnD,YAAM,cAAc,cAAc,YAAY,OAAO,OAAO,IAAK,iBAAiB;AAElF,UAAI,eAAe,UAAU,SAAS;AACpC,cAAM,cAAc,mBAAmB,SAAS,aAAa,UAAU;AACvE,kBAAU,OAAO,UAAU,GAAG,WAAW;AACzC,cAAM,MAAM,aAAa,MAAM;AAC/B;AACA;AAAA,MACF;AAEA,YAAM;AAAA,IACR;AAAA,EACF;AACF;;;AC1EA,eAAsB,MACpB,IACA,UAAwB,CAAC,GACb;AACZ,QAAM;AAAA,IACJ,UAAU;AAAA,IACV,cAAc;AAAA,IACd,aAAa;AAAA,IACb,gBAAgB;AAAA,IAChB;AAAA,IACA;AAAA,IACA;AAAA,EACF,IAAI;AAEJ,MAAI,UAAU;AAEd,SAAO,MAAM;AACX,QAAI,QAAQ,SAAS;AACnB,YAAM,OAAO,UAAU,IAAI,aAAa,8BAA8B,YAAY;AAAA,IACpF;AAEA,QAAI;AACF,aAAO,MAAM,GAAG,OAAO;AAAA,IACzB,SAAS,OAAO;AACd,UAAI,QAAQ,WAAY,iBAAiB,SAAS,MAAM,SAAS,cAAe;AAC9E,cAAM;AAAA,MACR;AAEA,UAAI,WAAW,SAAS;AACtB,cAAM;AAAA,MACR;AAEA,YAAM,MAAM;AACZ,YAAM,aAAa,KAAK,UAAU,KAAK,cAAc,KAAK,UAAU;AACpE,YAAM,UAAU,KAAK,WAAW,KAAK,UAAU;AAE/C,YAAM,oBAAoB,eAAe,UAAa,cAAc,SAAS,UAAU;AACvF,YAAM,qBAAqB,wBAAwB,KAAK;AACxD,YAAM,cAAc,cAChB,YAAY,OAAO,OAAO,IACzB,qBAAqB;AAE1B,UAAI,CAAC,aAAa;AAChB,cAAM;AAAA,MACR;AAEA,UAAI;AACJ,YAAM,cAAc,UAAU,oBAAoB,OAAO,IAAI;AAE7D,UAAI,gBAAgB,MAAM;AACxB,sBAAc,KAAK,IAAI,YAAY,WAAW;AAAA,MAChD,OAAO;AACL,sBAAc,mBAAmB,SAAS,aAAa,UAAU;AAAA,MACnE;AAEA,gBAAU,OAAO,UAAU,GAAG,WAAW;AACzC,YAAM,MAAM,aAAa,MAAM;AAC/B;AAAA,IACF;AAAA,EACF;AACF;;;ACxEA,gBAAuB,YACrB,eACA,UAAwB,CAAC,GACS;AAClC,QAAM;AAAA,IACJ,UAAU;AAAA,IACV,cAAc;AAAA,IACd,aAAa;AAAA,IACb;AAAA,IACA;AAAA,EACF,IAAI;AAEJ,MAAI,UAAU;AAEd,SAAO,MAAM;AACX,QAAI,QAAQ,SAAS;AACnB,YAAM,OAAO,UAAU,IAAI,aAAa,8BAA8B,YAAY;AAAA,IACpF;AAEA,QAAI;AACF,YAAM,SAAS,MAAM,cAAc,OAAO;AAC1C,uBAAiB,SAAS,QAAQ;AAChC,YAAI,QAAQ,SAAS;AACnB,gBAAM,OAAO,UAAU,IAAI,aAAa,8BAA8B,YAAY;AAAA,QACpF;AACA,cAAM;AAAA,MACR;AACA;AAAA,IACF,SAAS,OAAO;AACd,UAAI,QAAQ,WAAY,iBAAiB,SAAS,MAAM,SAAS,cAAe;AAC9E,cAAM;AAAA,MACR;AAEA,UAAI,WAAW,SAAS;AACtB,cAAM;AAAA,MACR;AAEA,YAAM,QAAQ,mBAAmB,SAAS,aAAa,UAAU;AACjE,gBAAU,OAAO,UAAU,GAAG,KAAK;AACnC,YAAM,MAAM,OAAO,MAAM;AACzB;AAAA,IACF;AAAA,EACF;AACF;;;AChDO,IAAM,0BAAN,cAAsC,MAAM;AAAA,EACjD,YAAY,UAAkB,mDAAmD;AAC/E,UAAM,OAAO;AACb,SAAK,OAAO;AAAA,EACd;AACF;AAMO,IAAM,iBAAN,MAAqB;AAAA,EAClB,QAAsB;AAAA,EACtB,eAAuB;AAAA,EACvB,kBAA0B;AAAA,EACjB;AAAA,EACA;AAAA,EACA;AAAA,EAEjB,YAAY,UAAiC,CAAC,GAAG;AAC/C,SAAK,mBAAmB,QAAQ,oBAAoB;AACpD,SAAK,iBAAiB,QAAQ,kBAAkB;AAChD,SAAK,gBAAgB,QAAQ;AAAA,EAC/B;AAAA,EAEO,WAAyB;AAC9B,QAAI,KAAK,UAAU,QAAQ;AACzB,YAAM,UAAU,KAAK,IAAI,IAAI,KAAK;AAClC,UAAI,WAAW,KAAK,gBAAgB;AAClC,aAAK,aAAa,WAAW;AAAA,MAC/B;AAAA,IACF;AACA,WAAO,KAAK;AAAA,EACd;AAAA,EAEA,MAAa,QAAW,IAAkC;AACxD,UAAM,eAAe,KAAK,SAAS;AAEnC,QAAI,iBAAiB,QAAQ;AAC3B,YAAM,IAAI,wBAAwB;AAAA,IACpC;AAEA,QAAI;AACF,YAAM,SAAS,MAAM,GAAG;AACxB,WAAK,cAAc;AACnB,aAAO;AAAA,IACT,SAAS,OAAO;AACd,WAAK,cAAc;AACnB,YAAM;AAAA,IACR;AAAA,EACF;AAAA,EAEQ,gBAAsB;AAC5B,QAAI,KAAK,UAAU,aAAa;AAC9B,WAAK,aAAa,QAAQ;AAAA,IAC5B;AACA,SAAK,eAAe;AAAA,EACtB;AAAA,EAEQ,gBAAsB;AAC5B,SAAK;AACL,SAAK,kBAAkB,KAAK,IAAI;AAEhC,QAAI,KAAK,UAAU,eAAe,KAAK,gBAAgB,KAAK,kBAAkB;AAC5E,WAAK,aAAa,MAAM;AAAA,IAC1B;AAAA,EACF;AAAA,EAEQ,aAAa,WAA+B;AAClD,UAAM,YAAY,KAAK;AACvB,QAAI,cAAc,WAAW;AAC3B,WAAK,QAAQ;AACb,WAAK,gBAAgB,WAAW,SAAS;AAAA,IAC3C;AAAA,EACF;AAAA,EAEO,QAAc;AACnB,SAAK,QAAQ;AACb,SAAK,eAAe;AACpB,SAAK,kBAAkB;AAAA,EACzB;AACF;","names":[]}
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@rohan9024/retrykit",
3
- "version": "0.1.0",
4
- "description": "Zero-dependency, header-aware network retry library tailored for LLM APIs and fetch.",
3
+ "version": "0.2.0",
4
+ "description": "The ultimate zero-dependency, header-aware retry engine and circuit breaker for LLM APIs (OpenAI, Anthropic, DeepSeek, Groq).",
5
5
  "main": "./dist/index.js",
6
6
  "module": "./dist/index.mjs",
7
7
  "types": "./dist/index.d.ts",
@@ -27,28 +27,27 @@
27
27
  },
28
28
  "keywords": [
29
29
  "retry",
30
- "fetch",
31
30
  "llm",
32
31
  "openai",
33
32
  "anthropic",
34
- "backoff",
35
- "jitter",
36
- "exponential-backoff",
33
+ "deepseek",
34
+ "groq",
35
+ "fetch",
36
+ "circuit-breaker",
37
+ "streaming",
37
38
  "rate-limit",
38
- "retry-after"
39
+ "backoff",
40
+ "jitter"
39
41
  ],
40
- "author": "",
42
+ "author": "rohan9024",
41
43
  "license": "MIT",
42
44
  "devDependencies": {
43
- "@types/node": "^22.20.5",
44
- "tsup": "^8.5.1",
45
- "typescript": "^5.9.3",
46
- "vitest": "^2.1.9"
45
+ "@types/node": "^22.0.0",
46
+ "tsup": "^8.3.0",
47
+ "typescript": "^5.6.0",
48
+ "vitest": "^2.1.0"
47
49
  },
48
50
  "engines": {
49
51
  "node": ">=18.0.0"
50
- },
51
- "directories": {
52
- "test": "test"
53
52
  }
54
- }
53
+ }