llm-retry-kit 0.1.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 llm-retry contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,152 @@
1
+ # llm-retry-kit
2
+
3
+ Smart retry wrapper for LLM APIs. It handles transient failures, rate limits,
4
+ fallback calls, exponential backoff, and simple token-cost tracking.
5
+
6
+ ## Install
7
+
8
+ ```bash
9
+ npm install llm-retry-kit
10
+ ```
11
+
12
+ ## Quick Start
13
+
14
+ ```ts
15
+ import { llmRetry } from 'llm-retry-kit'
16
+ import OpenAI from 'openai'
17
+
18
+ const openai = new OpenAI()
19
+
20
+ const result = await llmRetry({
21
+ fn: async () => {
22
+ const response = await openai.chat.completions.create({
23
+ model: 'gpt-4o-mini',
24
+ messages: [{ role: 'user', content: 'Hello!' }],
25
+ })
26
+
27
+ return {
28
+ data: response.choices[0]?.message.content ?? '',
29
+ usage: response.usage
30
+ ? {
31
+ promptTokens: response.usage.prompt_tokens,
32
+ completionTokens: response.usage.completion_tokens,
33
+ totalTokens: response.usage.total_tokens,
34
+ }
35
+ : undefined,
36
+ }
37
+ },
38
+ })
39
+
40
+ console.log(result.data)
41
+ console.log(result.attempts)
42
+ console.log(result.totalCostUSD)
43
+ ```
44
+
45
+ ## Fallback Example
46
+
47
+ ```ts
48
+ import { llmRetry } from 'llm-retry-kit'
49
+
50
+ const result = await llmRetry({
51
+ fn: async () => callPrimaryModel(),
52
+ fallback: async () => callFallbackModel(),
53
+ maxRetries: 3,
54
+ initialDelayMs: 1000,
55
+ maxDelayMs: 30000,
56
+ })
57
+
58
+ if (result.usedFallback) {
59
+ console.log('Fallback model was used')
60
+ }
61
+ ```
62
+
63
+ ## Budget Tracking
64
+
65
+ `llm-retry-kit` tracks cost from the `usage.totalTokens` value returned by your
66
+ function. It cannot know the exact cost of a future LLM call before that call
67
+ finishes, so the budget guard is best used to stop later retries or fallback
68
+ calls after tracked usage reaches the configured cap.
69
+
70
+ ```ts
71
+ const result = await llmRetry({
72
+ fn: myLLMCall,
73
+ maxCostUSD: 0.5,
74
+ costPer1kTokens: 0.002,
75
+ onBudgetExceeded: (spent, limit) => {
76
+ console.warn(`Budget exceeded: $${spent.toFixed(4)} / $${limit}`)
77
+ },
78
+ })
79
+ ```
80
+
81
+ ## Retry Logging
82
+
83
+ ```ts
84
+ const result = await llmRetry({
85
+ fn: myLLMCall,
86
+ maxRetries: 4,
87
+ initialDelayMs: 1000,
88
+ maxDelayMs: 60000,
89
+ onRetry: (attempt, error, delayMs) => {
90
+ console.log(`Attempt ${attempt} failed: ${error.message}`)
91
+ console.log(`Waiting ${(delayMs / 1000).toFixed(1)}s before retrying`)
92
+ },
93
+ })
94
+ ```
95
+
96
+ ## API
97
+
98
+ ### `llmRetry(options)`
99
+
100
+ | Option | Type | Default | Description |
101
+ | --- | --- | --- | --- |
102
+ | `fn` | `() => Promise<LLMResponse<T>>` | required | Primary async LLM call. |
103
+ | `fallback` | `() => Promise<LLMResponse<T>>` | optional | Backup async LLM call. |
104
+ | `maxRetries` | `number` | `3` | Number of retries after the first attempt. |
105
+ | `maxCostUSD` | `number` | optional | Maximum tracked cost in USD. |
106
+ | `costPer1kTokens` | `number` | `0.002` | Estimated cost per 1,000 tokens. |
107
+ | `initialDelayMs` | `number` | `1000` | Initial retry delay. |
108
+ | `maxDelayMs` | `number` | `30000` | Maximum retry delay. |
109
+ | `onRetry` | `(attempt, error, delayMs) => void` | optional | Called before each retry wait. |
110
+ | `onBudgetExceeded` | `(spentUSD, limitUSD) => void` | optional | Called when tracked budget is exhausted. |
111
+
112
+ ### `LLMResponse<T>`
113
+
114
+ ```ts
115
+ {
116
+ data: T
117
+ usage?: {
118
+ promptTokens: number
119
+ completionTokens: number
120
+ totalTokens: number
121
+ }
122
+ }
123
+ ```
124
+
125
+ ### `RetryResult<T>`
126
+
127
+ ```ts
128
+ {
129
+ data: T
130
+ attempts: number
131
+ usedFallback: boolean
132
+ totalCostUSD: number
133
+ totalTokens: number
134
+ }
135
+ ```
136
+
137
+ ## Retryable Errors
138
+
139
+ The package retries common transient failures:
140
+
141
+ - HTTP `429`
142
+ - HTTP `5xx`
143
+ - timeout errors
144
+ - network connection errors
145
+ - overloaded server errors
146
+
147
+ Authentication errors, invalid requests, and other non-transient failures are
148
+ not retried.
149
+
150
+ ## License
151
+
152
+ MIT
@@ -0,0 +1,5 @@
1
+ export declare function calculateBackoff(attempt: number, initialDelayMs: number, maxDelayMs: number): number;
2
+ export declare function sleep(ms: number): Promise<void>;
3
+ export declare function isRetryableError(error: unknown): boolean;
4
+ export declare function extractRetryAfter(error: unknown): number | null;
5
+ //# sourceMappingURL=backoff.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"backoff.d.ts","sourceRoot":"","sources":["../src/backoff.ts"],"names":[],"mappings":"AAAA,wBAAgB,gBAAgB,CAC9B,OAAO,EAAE,MAAM,EACf,cAAc,EAAE,MAAM,EACtB,UAAU,EAAE,MAAM,GACjB,MAAM,CAMR;AAED,wBAAgB,KAAK,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAE/C;AAED,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,OAAO,GAAG,OAAO,CAiDxD;AAED,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,GAAG,IAAI,CAyB/D"}
@@ -0,0 +1,96 @@
1
+ export function calculateBackoff(attempt, initialDelayMs, maxDelayMs) {
2
+ const exponential = initialDelayMs * Math.pow(2, attempt);
3
+ const jitter = exponential * 0.25 * (Math.random() * 2 - 1);
4
+ const delay = exponential + jitter;
5
+ return Math.min(Math.max(delay, initialDelayMs), maxDelayMs);
6
+ }
7
+ export function sleep(ms) {
8
+ return new Promise((resolve) => setTimeout(resolve, ms));
9
+ }
10
+ export function isRetryableError(error) {
11
+ if (!(error instanceof Error))
12
+ return false;
13
+ const err = error;
14
+ const status = toNumber(err.status ?? err.statusCode);
15
+ if (status === 429 || (status !== null && status >= 500 && status <= 599)) {
16
+ return true;
17
+ }
18
+ const code = typeof err.code === 'string' ? err.code.toLowerCase() : '';
19
+ if ([
20
+ 'etimedout',
21
+ 'econnreset',
22
+ 'econnrefused',
23
+ 'enotfound',
24
+ 'eai_again',
25
+ 'rate_limit_exceeded',
26
+ ].includes(code)) {
27
+ return true;
28
+ }
29
+ const message = error.message.toLowerCase();
30
+ const retryablePatterns = [
31
+ 'rate limit',
32
+ 'rate_limit',
33
+ 'too many requests',
34
+ '429',
35
+ 'server error',
36
+ '500',
37
+ '502',
38
+ '503',
39
+ '504',
40
+ 'timeout',
41
+ 'timed out',
42
+ 'econnreset',
43
+ 'econnrefused',
44
+ 'network',
45
+ 'socket',
46
+ 'overloaded',
47
+ ];
48
+ return retryablePatterns.some((pattern) => message.includes(pattern));
49
+ }
50
+ export function extractRetryAfter(error) {
51
+ if (!error || typeof error !== 'object')
52
+ return null;
53
+ const err = error;
54
+ const retryAfter = toNumber(err['retryAfter'] ?? err['retry-after']);
55
+ if (retryAfter !== null) {
56
+ return retryAfter * 1000;
57
+ }
58
+ const headerValue = getHeader(err['headers'], 'retry-after');
59
+ if (!headerValue) {
60
+ return null;
61
+ }
62
+ const seconds = Number(headerValue);
63
+ if (Number.isFinite(seconds)) {
64
+ return seconds * 1000;
65
+ }
66
+ const dateMs = Date.parse(headerValue);
67
+ if (Number.isFinite(dateMs)) {
68
+ return Math.max(dateMs - Date.now(), 0);
69
+ }
70
+ return null;
71
+ }
72
+ function toNumber(value) {
73
+ if (typeof value === 'number' && Number.isFinite(value)) {
74
+ return value;
75
+ }
76
+ if (typeof value === 'string' && value.trim() !== '') {
77
+ const parsed = Number(value);
78
+ return Number.isFinite(parsed) ? parsed : null;
79
+ }
80
+ return null;
81
+ }
82
+ function getHeader(headers, name) {
83
+ if (!headers || typeof headers !== 'object')
84
+ return null;
85
+ if ('get' in headers && typeof headers.get === 'function') {
86
+ const value = headers.get(name);
87
+ return typeof value === 'string' ? value : null;
88
+ }
89
+ const record = headers;
90
+ const matchedKey = Object.keys(record).find((key) => key.toLowerCase() === name.toLowerCase());
91
+ if (!matchedKey)
92
+ return null;
93
+ const value = record[matchedKey];
94
+ return typeof value === 'string' ? value : null;
95
+ }
96
+ //# sourceMappingURL=backoff.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"backoff.js","sourceRoot":"","sources":["../src/backoff.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,15 @@
1
+ import type { TokenUsage } from './types.js';
2
+ export declare class BudgetTracker {
3
+ private totalTokens;
4
+ private totalCostUSD;
5
+ private readonly costPer1kTokens;
6
+ private readonly maxCostUSD;
7
+ constructor(costPer1kTokens: number, maxCostUSD?: number);
8
+ add(usage: TokenUsage): void;
9
+ isExceeded(): boolean;
10
+ get spent(): number;
11
+ get tokens(): number;
12
+ get limit(): number | null;
13
+ summary(): string;
14
+ }
15
+ //# sourceMappingURL=budget.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"budget.d.ts","sourceRoot":"","sources":["../src/budget.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,YAAY,CAAA;AAE5C,qBAAa,aAAa;IACxB,OAAO,CAAC,WAAW,CAAI;IACvB,OAAO,CAAC,YAAY,CAAI;IACxB,OAAO,CAAC,QAAQ,CAAC,eAAe,CAAQ;IACxC,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAe;gBAE9B,eAAe,EAAE,MAAM,EAAE,UAAU,CAAC,EAAE,MAAM;IAaxD,GAAG,CAAC,KAAK,EAAE,UAAU,GAAG,IAAI;IAK5B,UAAU,IAAI,OAAO;IAKrB,IAAI,KAAK,IAAI,MAAM,CAElB;IAED,IAAI,MAAM,IAAI,MAAM,CAEnB;IAED,IAAI,KAAK,IAAI,MAAM,GAAG,IAAI,CAEzB;IAED,OAAO,IAAI,MAAM;CAKlB"}
package/dist/budget.js ADDED
@@ -0,0 +1,40 @@
1
+ export class BudgetTracker {
2
+ totalTokens = 0;
3
+ totalCostUSD = 0;
4
+ costPer1kTokens;
5
+ maxCostUSD;
6
+ constructor(costPer1kTokens, maxCostUSD) {
7
+ if (costPer1kTokens < 0) {
8
+ throw new Error('costPer1kTokens must be greater than or equal to 0');
9
+ }
10
+ if (maxCostUSD !== undefined && maxCostUSD < 0) {
11
+ throw new Error('maxCostUSD must be greater than or equal to 0');
12
+ }
13
+ this.costPer1kTokens = costPer1kTokens;
14
+ this.maxCostUSD = maxCostUSD ?? null;
15
+ }
16
+ add(usage) {
17
+ this.totalTokens += usage.totalTokens;
18
+ this.totalCostUSD += (usage.totalTokens / 1000) * this.costPer1kTokens;
19
+ }
20
+ isExceeded() {
21
+ if (this.maxCostUSD === null)
22
+ return false;
23
+ return this.totalCostUSD >= this.maxCostUSD;
24
+ }
25
+ get spent() {
26
+ return this.totalCostUSD;
27
+ }
28
+ get tokens() {
29
+ return this.totalTokens;
30
+ }
31
+ get limit() {
32
+ return this.maxCostUSD;
33
+ }
34
+ summary() {
35
+ const cost = this.totalCostUSD.toFixed(4);
36
+ const limit = this.maxCostUSD !== null ? `/ $${this.maxCostUSD}` : '';
37
+ return `$${cost}${limit} (${this.totalTokens.toLocaleString()} tokens)`;
38
+ }
39
+ }
40
+ //# sourceMappingURL=budget.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"budget.js","sourceRoot":"","sources":["../src/budget.ts"],"names":[],"mappings":"AAEA,MAAM,OAAO,aAAa;IAChB,WAAW,GAAG,CAAC,CAAA;IACf,YAAY,GAAG,CAAC,CAAA;IACP,eAAe,CAAQ;IACvB,UAAU,CAAe;IAE1C,YAAY,eAAuB,EAAE,UAAmB;QACtD,IAAI,eAAe,GAAG,CAAC,EAAE,CAAC;YACxB,MAAM,IAAI,KAAK,CAAC,oDAAoD,CAAC,CAAA;QACvE,CAAC;QAED,IAAI,UAAU,KAAK,SAAS,IAAI,UAAU,GAAG,CAAC,EAAE,CAAC;YAC/C,MAAM,IAAI,KAAK,CAAC,+CAA+C,CAAC,CAAA;QAClE,CAAC;QAED,IAAI,CAAC,eAAe,GAAG,eAAe,CAAA;QACtC,IAAI,CAAC,UAAU,GAAG,UAAU,IAAI,IAAI,CAAA;IACtC,CAAC;IAED,GAAG,CAAC,KAAiB;QACnB,IAAI,CAAC,WAAW,IAAI,KAAK,CAAC,WAAW,CAAA;QACrC,IAAI,CAAC,YAAY,IAAI,CAAC,KAAK,CAAC,WAAW,GAAG,IAAI,CAAC,GAAG,IAAI,CAAC,eAAe,CAAA;IACxE,CAAC;IAED,UAAU;QACR,IAAI,IAAI,CAAC,UAAU,KAAK,IAAI;YAAE,OAAO,KAAK,CAAA;QAC1C,OAAO,IAAI,CAAC,YAAY,IAAI,IAAI,CAAC,UAAU,CAAA;IAC7C,CAAC;IAED,IAAI,KAAK;QACP,OAAO,IAAI,CAAC,YAAY,CAAA;IAC1B,CAAC;IAED,IAAI,MAAM;QACR,OAAO,IAAI,CAAC,WAAW,CAAA;IACzB,CAAC;IAED,IAAI,KAAK;QACP,OAAO,IAAI,CAAC,UAAU,CAAA;IACxB,CAAC;IAED,OAAO;QACL,MAAM,IAAI,GAAG,IAAI,CAAC,YAAY,CAAC,OAAO,CAAC,CAAC,CAAC,CAAA;QACzC,MAAM,KAAK,GAAG,IAAI,CAAC,UAAU,KAAK,IAAI,CAAC,CAAC,CAAC,MAAM,IAAI,CAAC,UAAU,EAAE,CAAC,CAAC,CAAC,EAAE,CAAA;QACrE,OAAO,IAAI,IAAI,GAAG,KAAK,KAAK,IAAI,CAAC,WAAW,CAAC,cAAc,EAAE,UAAU,CAAA;IACzE,CAAC;CACF"}
@@ -0,0 +1,5 @@
1
+ export { llmRetry, LLMRetryError } from './retry.js';
2
+ export { BudgetTracker } from './budget.js';
3
+ export { calculateBackoff, isRetryableError } from './backoff.js';
4
+ export type { RetryOptions, RetryResult, TokenUsage, LLMResponse, RetryableError, } from './types.js';
5
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,aAAa,EAAE,MAAM,YAAY,CAAA;AACpD,OAAO,EAAE,aAAa,EAAE,MAAM,aAAa,CAAA;AAC3C,OAAO,EAAE,gBAAgB,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAA;AACjE,YAAY,EACV,YAAY,EACZ,WAAW,EACX,UAAU,EACV,WAAW,EACX,cAAc,GACf,MAAM,YAAY,CAAA"}
package/dist/index.js ADDED
@@ -0,0 +1,4 @@
1
+ export { llmRetry, LLMRetryError } from './retry.js';
2
+ export { BudgetTracker } from './budget.js';
3
+ export { calculateBackoff, isRetryableError } from './backoff.js';
4
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,aAAa,EAAE,MAAM,YAAY,CAAA;AACpD,OAAO,EAAE,aAAa,EAAE,MAAM,aAAa,CAAA;AAC3C,OAAO,EAAE,gBAAgB,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAA"}
@@ -0,0 +1,10 @@
1
+ import type { RetryOptions, RetryResult } from './types.js';
2
+ export declare function llmRetry<T>(options: RetryOptions<T>): Promise<RetryResult<T>>;
3
+ export declare class LLMRetryError extends Error {
4
+ readonly primaryError: Error | null;
5
+ readonly fallbackError: Error | null;
6
+ readonly totalCostUSD: number;
7
+ readonly totalTokens: number;
8
+ constructor(message: string, primaryError: Error | null, fallbackError: Error | null, totalCostUSD: number, totalTokens: number);
9
+ }
10
+ //# sourceMappingURL=retry.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"retry.d.ts","sourceRoot":"","sources":["../src/retry.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,YAAY,EAAE,WAAW,EAAE,MAAM,YAAY,CAAA;AAE3D,wBAAsB,QAAQ,CAAC,CAAC,EAAE,OAAO,EAAE,YAAY,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,WAAW,CAAC,CAAC,CAAC,CAAC,CAgHnF;AAeD,qBAAa,aAAc,SAAQ,KAAK;aAGpB,YAAY,EAAE,KAAK,GAAG,IAAI;aAC1B,aAAa,EAAE,KAAK,GAAG,IAAI;aAC3B,YAAY,EAAE,MAAM;aACpB,WAAW,EAAE,MAAM;gBAJnC,OAAO,EAAE,MAAM,EACC,YAAY,EAAE,KAAK,GAAG,IAAI,EAC1B,aAAa,EAAE,KAAK,GAAG,IAAI,EAC3B,YAAY,EAAE,MAAM,EACpB,WAAW,EAAE,MAAM;CAKtC"}
package/dist/retry.js ADDED
@@ -0,0 +1,103 @@
1
+ import { calculateBackoff, extractRetryAfter, isRetryableError, sleep } from './backoff.js';
2
+ import { BudgetTracker } from './budget.js';
3
+ export async function llmRetry(options) {
4
+ const { fn, fallback, maxRetries = 3, maxCostUSD, costPer1kTokens = 0.002, initialDelayMs = 1000, maxDelayMs = 30000, onRetry, onBudgetExceeded, } = options;
5
+ validateOptions({ maxRetries, initialDelayMs, maxDelayMs });
6
+ const budget = new BudgetTracker(costPer1kTokens, maxCostUSD);
7
+ let lastError = null;
8
+ let attempts = 0;
9
+ let budgetExceededNotified = false;
10
+ for (let retryIndex = 0; retryIndex <= maxRetries; retryIndex++) {
11
+ if (budget.isExceeded()) {
12
+ budgetExceededNotified = notifyBudgetExceeded(budget.spent, budget.limit, onBudgetExceeded, budgetExceededNotified);
13
+ break;
14
+ }
15
+ attempts += 1;
16
+ try {
17
+ const response = await fn();
18
+ if (response.usage) {
19
+ budget.add(response.usage);
20
+ }
21
+ return {
22
+ data: response.data,
23
+ attempts,
24
+ usedFallback: false,
25
+ totalCostUSD: budget.spent,
26
+ totalTokens: budget.tokens,
27
+ };
28
+ }
29
+ catch (error) {
30
+ const err = error instanceof Error ? error : new Error(String(error));
31
+ lastError = err;
32
+ const isLastAttempt = retryIndex === maxRetries;
33
+ if (isLastAttempt || !isRetryableError(err)) {
34
+ break;
35
+ }
36
+ const serverDelay = extractRetryAfter(error);
37
+ const delay = serverDelay ?? calculateBackoff(retryIndex, initialDelayMs, maxDelayMs);
38
+ onRetry?.(attempts, err, delay);
39
+ await sleep(delay);
40
+ }
41
+ }
42
+ if (fallback && !budget.isExceeded()) {
43
+ attempts += 1;
44
+ try {
45
+ const response = await fallback();
46
+ if (response.usage) {
47
+ budget.add(response.usage);
48
+ }
49
+ return {
50
+ data: response.data,
51
+ attempts,
52
+ usedFallback: true,
53
+ totalCostUSD: budget.spent,
54
+ totalTokens: budget.tokens,
55
+ };
56
+ }
57
+ catch (fallbackError) {
58
+ const err = fallbackError instanceof Error
59
+ ? fallbackError
60
+ : new Error(String(fallbackError));
61
+ throw new LLMRetryError(`Both primary and fallback failed. Primary: ${lastError?.message ?? 'unknown error'}. Fallback: ${err.message}`, lastError, err, budget.spent, budget.tokens);
62
+ }
63
+ }
64
+ if (budget.isExceeded()) {
65
+ notifyBudgetExceeded(budget.spent, budget.limit, onBudgetExceeded, budgetExceededNotified);
66
+ }
67
+ throw new LLMRetryError(`LLM call failed after ${attempts} attempt${attempts === 1 ? '' : 's'}: ${lastError?.message ?? 'budget exceeded'}`, lastError, null, budget.spent, budget.tokens);
68
+ }
69
+ function notifyBudgetExceeded(spentUSD, limitUSD, callback, alreadyNotified) {
70
+ if (!alreadyNotified && limitUSD !== null) {
71
+ callback?.(spentUSD, limitUSD);
72
+ }
73
+ return true;
74
+ }
75
+ export class LLMRetryError extends Error {
76
+ primaryError;
77
+ fallbackError;
78
+ totalCostUSD;
79
+ totalTokens;
80
+ constructor(message, primaryError, fallbackError, totalCostUSD, totalTokens) {
81
+ super(message);
82
+ this.primaryError = primaryError;
83
+ this.fallbackError = fallbackError;
84
+ this.totalCostUSD = totalCostUSD;
85
+ this.totalTokens = totalTokens;
86
+ this.name = 'LLMRetryError';
87
+ }
88
+ }
89
+ function validateOptions(options) {
90
+ if (!Number.isInteger(options.maxRetries) || options.maxRetries < 0) {
91
+ throw new Error('maxRetries must be a non-negative integer');
92
+ }
93
+ if (options.initialDelayMs < 0) {
94
+ throw new Error('initialDelayMs must be greater than or equal to 0');
95
+ }
96
+ if (options.maxDelayMs < 0) {
97
+ throw new Error('maxDelayMs must be greater than or equal to 0');
98
+ }
99
+ if (options.maxDelayMs < options.initialDelayMs) {
100
+ throw new Error('maxDelayMs must be greater than or equal to initialDelayMs');
101
+ }
102
+ }
103
+ //# sourceMappingURL=retry.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"retry.js","sourceRoot":"","sources":["../src/retry.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,29 @@
1
+ export interface TokenUsage {
2
+ promptTokens: number;
3
+ completionTokens: number;
4
+ totalTokens: number;
5
+ }
6
+ export interface LLMResponse<T = unknown> {
7
+ data: T;
8
+ usage?: TokenUsage;
9
+ }
10
+ export interface RetryOptions<T = unknown> {
11
+ fn: () => Promise<LLMResponse<T>>;
12
+ fallback?: () => Promise<LLMResponse<T>>;
13
+ maxRetries?: number;
14
+ maxCostUSD?: number;
15
+ costPer1kTokens?: number;
16
+ initialDelayMs?: number;
17
+ maxDelayMs?: number;
18
+ onRetry?: (attempt: number, error: Error, delayMs: number) => void;
19
+ onBudgetExceeded?: (spentUSD: number, limitUSD: number) => void;
20
+ }
21
+ export interface RetryResult<T = unknown> {
22
+ data: T;
23
+ attempts: number;
24
+ usedFallback: boolean;
25
+ totalCostUSD: number;
26
+ totalTokens: number;
27
+ }
28
+ export type RetryableError = 'rate_limit' | 'server_error' | 'timeout' | 'network_error';
29
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,UAAU;IACzB,YAAY,EAAE,MAAM,CAAA;IACpB,gBAAgB,EAAE,MAAM,CAAA;IACxB,WAAW,EAAE,MAAM,CAAA;CACpB;AAED,MAAM,WAAW,WAAW,CAAC,CAAC,GAAG,OAAO;IACtC,IAAI,EAAE,CAAC,CAAA;IACP,KAAK,CAAC,EAAE,UAAU,CAAA;CACnB;AAED,MAAM,WAAW,YAAY,CAAC,CAAC,GAAG,OAAO;IACvC,EAAE,EAAE,MAAM,OAAO,CAAC,WAAW,CAAC,CAAC,CAAC,CAAC,CAAA;IACjC,QAAQ,CAAC,EAAE,MAAM,OAAO,CAAC,WAAW,CAAC,CAAC,CAAC,CAAC,CAAA;IACxC,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,eAAe,CAAC,EAAE,MAAM,CAAA;IACxB,cAAc,CAAC,EAAE,MAAM,CAAA;IACvB,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,OAAO,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,KAAK,EAAE,OAAO,EAAE,MAAM,KAAK,IAAI,CAAA;IAClE,gBAAgB,CAAC,EAAE,CAAC,QAAQ,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,KAAK,IAAI,CAAA;CAChE;AAED,MAAM,WAAW,WAAW,CAAC,CAAC,GAAG,OAAO;IACtC,IAAI,EAAE,CAAC,CAAA;IACP,QAAQ,EAAE,MAAM,CAAA;IAChB,YAAY,EAAE,OAAO,CAAA;IACrB,YAAY,EAAE,MAAM,CAAA;IACpB,WAAW,EAAE,MAAM,CAAA;CACpB;AAED,MAAM,MAAM,cAAc,GACtB,YAAY,GACZ,cAAc,GACd,SAAS,GACT,eAAe,CAAA"}
package/dist/types.js ADDED
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":""}
package/package.json ADDED
@@ -0,0 +1,51 @@
1
+ {
2
+ "name": "llm-retry-kit",
3
+ "version": "0.1.0",
4
+ "description": "Smart retry wrapper for LLM APIs with fallback, budget tracking, and exponential backoff.",
5
+ "type": "module",
6
+ "main": "./dist/index.js",
7
+ "types": "./dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./dist/index.d.ts",
11
+ "import": "./dist/index.js"
12
+ },
13
+ "./package.json": "./package.json"
14
+ },
15
+ "files": [
16
+ "dist",
17
+ "README.md",
18
+ "LICENSE"
19
+ ],
20
+ "sideEffects": false,
21
+ "engines": {
22
+ "node": ">=18"
23
+ },
24
+ "scripts": {
25
+ "clean": "node -e \"fs.rmSync('dist', { recursive: true, force: true })\"",
26
+ "build": "npm run clean && tsc -p tsconfig.json",
27
+ "test": "vitest run",
28
+ "test:watch": "vitest",
29
+ "typecheck": "tsc -p tsconfig.json --noEmit",
30
+ "prepack": "npm run build",
31
+ "prepublishOnly": "npm test && npm run typecheck"
32
+ },
33
+ "keywords": [
34
+ "llm",
35
+ "openai",
36
+ "anthropic",
37
+ "retry",
38
+ "rate-limit",
39
+ "fallback",
40
+ "backoff",
41
+ "ai",
42
+ "gpt",
43
+ "claude"
44
+ ],
45
+ "author": "llm-retry contributors",
46
+ "license": "MIT",
47
+ "devDependencies": {
48
+ "typescript": "^5.4.0",
49
+ "vitest": "^4.1.8"
50
+ }
51
+ }