@providerprotocol/ai 0.0.1

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.
Files changed (67) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +84 -0
  3. package/dist/anthropic/index.d.ts +41 -0
  4. package/dist/anthropic/index.js +500 -0
  5. package/dist/anthropic/index.js.map +1 -0
  6. package/dist/chunk-CUCRF5W6.js +136 -0
  7. package/dist/chunk-CUCRF5W6.js.map +1 -0
  8. package/dist/chunk-FTFX2VET.js +424 -0
  9. package/dist/chunk-FTFX2VET.js.map +1 -0
  10. package/dist/chunk-QUUX4G7U.js +117 -0
  11. package/dist/chunk-QUUX4G7U.js.map +1 -0
  12. package/dist/chunk-Y6Q7JCNP.js +39 -0
  13. package/dist/chunk-Y6Q7JCNP.js.map +1 -0
  14. package/dist/google/index.d.ts +69 -0
  15. package/dist/google/index.js +517 -0
  16. package/dist/google/index.js.map +1 -0
  17. package/dist/http/index.d.ts +61 -0
  18. package/dist/http/index.js +43 -0
  19. package/dist/http/index.js.map +1 -0
  20. package/dist/index.d.ts +792 -0
  21. package/dist/index.js +898 -0
  22. package/dist/index.js.map +1 -0
  23. package/dist/openai/index.d.ts +204 -0
  24. package/dist/openai/index.js +1340 -0
  25. package/dist/openai/index.js.map +1 -0
  26. package/dist/provider-CUJWjgNl.d.ts +192 -0
  27. package/dist/retry-I2661_rv.d.ts +118 -0
  28. package/package.json +88 -0
  29. package/src/anthropic/index.ts +3 -0
  30. package/src/core/image.ts +188 -0
  31. package/src/core/llm.ts +619 -0
  32. package/src/core/provider.ts +92 -0
  33. package/src/google/index.ts +3 -0
  34. package/src/http/errors.ts +112 -0
  35. package/src/http/fetch.ts +210 -0
  36. package/src/http/index.ts +31 -0
  37. package/src/http/keys.ts +136 -0
  38. package/src/http/retry.ts +205 -0
  39. package/src/http/sse.ts +136 -0
  40. package/src/index.ts +32 -0
  41. package/src/openai/index.ts +9 -0
  42. package/src/providers/anthropic/index.ts +17 -0
  43. package/src/providers/anthropic/llm.ts +196 -0
  44. package/src/providers/anthropic/transform.ts +452 -0
  45. package/src/providers/anthropic/types.ts +213 -0
  46. package/src/providers/google/index.ts +17 -0
  47. package/src/providers/google/llm.ts +203 -0
  48. package/src/providers/google/transform.ts +487 -0
  49. package/src/providers/google/types.ts +214 -0
  50. package/src/providers/openai/index.ts +151 -0
  51. package/src/providers/openai/llm.completions.ts +201 -0
  52. package/src/providers/openai/llm.responses.ts +211 -0
  53. package/src/providers/openai/transform.completions.ts +628 -0
  54. package/src/providers/openai/transform.responses.ts +718 -0
  55. package/src/providers/openai/types.ts +711 -0
  56. package/src/types/content.ts +133 -0
  57. package/src/types/errors.ts +85 -0
  58. package/src/types/index.ts +105 -0
  59. package/src/types/llm.ts +211 -0
  60. package/src/types/messages.ts +182 -0
  61. package/src/types/provider.ts +195 -0
  62. package/src/types/schema.ts +58 -0
  63. package/src/types/stream.ts +146 -0
  64. package/src/types/thread.ts +226 -0
  65. package/src/types/tool.ts +88 -0
  66. package/src/types/turn.ts +118 -0
  67. package/src/utils/id.ts +28 -0
@@ -0,0 +1,112 @@
1
+ import { UPPError, type ErrorCode, type Modality } from '../types/errors.ts';
2
+
3
+ /**
4
+ * Map HTTP status codes to UPP error codes
5
+ */
6
+ export function statusToErrorCode(status: number): ErrorCode {
7
+ switch (status) {
8
+ case 400:
9
+ return 'INVALID_REQUEST';
10
+ case 401:
11
+ case 403:
12
+ return 'AUTHENTICATION_FAILED';
13
+ case 404:
14
+ return 'MODEL_NOT_FOUND';
15
+ case 408:
16
+ return 'TIMEOUT';
17
+ case 413:
18
+ return 'CONTEXT_LENGTH_EXCEEDED';
19
+ case 429:
20
+ return 'RATE_LIMITED';
21
+ case 500:
22
+ case 502:
23
+ case 503:
24
+ case 504:
25
+ return 'PROVIDER_ERROR';
26
+ default:
27
+ return 'PROVIDER_ERROR';
28
+ }
29
+ }
30
+
31
+ /**
32
+ * Normalize HTTP error responses to UPPError
33
+ * Maps HTTP status codes to appropriate ErrorCode values
34
+ * Extracts error message from response body when available
35
+ */
36
+ export async function normalizeHttpError(
37
+ response: Response,
38
+ provider: string,
39
+ modality: Modality
40
+ ): Promise<UPPError> {
41
+ const code = statusToErrorCode(response.status);
42
+ let message = `HTTP ${response.status}: ${response.statusText}`;
43
+
44
+ try {
45
+ const body = await response.text();
46
+ if (body) {
47
+ try {
48
+ const json = JSON.parse(body);
49
+ // Common error message locations across providers
50
+ const extractedMessage =
51
+ json.error?.message ||
52
+ json.message ||
53
+ json.error?.error?.message ||
54
+ json.detail;
55
+
56
+ if (extractedMessage) {
57
+ message = extractedMessage;
58
+ }
59
+ } catch {
60
+ // Body is not JSON, use raw text if short
61
+ if (body.length < 200) {
62
+ message = body;
63
+ }
64
+ }
65
+ }
66
+ } catch {
67
+ // Failed to read body, use default message
68
+ }
69
+
70
+ return new UPPError(message, code, provider, modality, response.status);
71
+ }
72
+
73
+ /**
74
+ * Create a network error
75
+ */
76
+ export function networkError(
77
+ error: Error,
78
+ provider: string,
79
+ modality: Modality
80
+ ): UPPError {
81
+ return new UPPError(
82
+ `Network error: ${error.message}`,
83
+ 'NETWORK_ERROR',
84
+ provider,
85
+ modality,
86
+ undefined,
87
+ error
88
+ );
89
+ }
90
+
91
+ /**
92
+ * Create a timeout error
93
+ */
94
+ export function timeoutError(
95
+ timeout: number,
96
+ provider: string,
97
+ modality: Modality
98
+ ): UPPError {
99
+ return new UPPError(
100
+ `Request timed out after ${timeout}ms`,
101
+ 'TIMEOUT',
102
+ provider,
103
+ modality
104
+ );
105
+ }
106
+
107
+ /**
108
+ * Create a cancelled error
109
+ */
110
+ export function cancelledError(provider: string, modality: Modality): UPPError {
111
+ return new UPPError('Request was cancelled', 'CANCELLED', provider, modality);
112
+ }
@@ -0,0 +1,210 @@
1
+ import type { ProviderConfig } from '../types/provider.ts';
2
+ import type { Modality } from '../types/errors.ts';
3
+ import { UPPError } from '../types/errors.ts';
4
+ import {
5
+ normalizeHttpError,
6
+ networkError,
7
+ timeoutError,
8
+ cancelledError,
9
+ } from './errors.ts';
10
+
11
+ /**
12
+ * Default timeout in milliseconds
13
+ */
14
+ const DEFAULT_TIMEOUT = 120000; // 2 minutes
15
+
16
+ /**
17
+ * Execute fetch with retry, timeout, and error normalization
18
+ *
19
+ * @param url - Request URL
20
+ * @param init - Fetch init options
21
+ * @param config - Provider config
22
+ * @param provider - Provider name for error messages
23
+ * @param modality - Modality for error messages
24
+ */
25
+ export async function doFetch(
26
+ url: string,
27
+ init: RequestInit,
28
+ config: ProviderConfig,
29
+ provider: string,
30
+ modality: Modality
31
+ ): Promise<Response> {
32
+ const fetchFn = config.fetch ?? fetch;
33
+ const timeout = config.timeout ?? DEFAULT_TIMEOUT;
34
+ const strategy = config.retryStrategy;
35
+
36
+ // Pre-request delay (e.g., token bucket)
37
+ if (strategy?.beforeRequest) {
38
+ const delay = await strategy.beforeRequest();
39
+ if (delay > 0) {
40
+ await sleep(delay);
41
+ }
42
+ }
43
+
44
+ let lastError: UPPError | undefined;
45
+ let attempt = 0;
46
+
47
+ while (true) {
48
+ attempt++;
49
+
50
+ try {
51
+ const response = await fetchWithTimeout(
52
+ fetchFn,
53
+ url,
54
+ init,
55
+ timeout,
56
+ provider,
57
+ modality
58
+ );
59
+
60
+ // Check for HTTP errors
61
+ if (!response.ok) {
62
+ const error = await normalizeHttpError(response, provider, modality);
63
+
64
+ // Check for Retry-After header
65
+ const retryAfter = response.headers.get('Retry-After');
66
+ if (retryAfter && strategy) {
67
+ const seconds = parseInt(retryAfter, 10);
68
+ if (!isNaN(seconds) && 'setRetryAfter' in strategy) {
69
+ (strategy as { setRetryAfter: (s: number) => void }).setRetryAfter(
70
+ seconds
71
+ );
72
+ }
73
+ }
74
+
75
+ // Try to retry
76
+ if (strategy) {
77
+ const delay = await strategy.onRetry(error, attempt);
78
+ if (delay !== null) {
79
+ await sleep(delay);
80
+ lastError = error;
81
+ continue;
82
+ }
83
+ }
84
+
85
+ throw error;
86
+ }
87
+
88
+ // Success - reset strategy state
89
+ strategy?.reset?.();
90
+
91
+ return response;
92
+ } catch (error) {
93
+ // Already a UPPError, handle retry
94
+ if (error instanceof UPPError) {
95
+ if (strategy) {
96
+ const delay = await strategy.onRetry(error, attempt);
97
+ if (delay !== null) {
98
+ await sleep(delay);
99
+ lastError = error;
100
+ continue;
101
+ }
102
+ }
103
+ throw error;
104
+ }
105
+
106
+ // Network error
107
+ const uppError = networkError(error as Error, provider, modality);
108
+
109
+ if (strategy) {
110
+ const delay = await strategy.onRetry(uppError, attempt);
111
+ if (delay !== null) {
112
+ await sleep(delay);
113
+ lastError = uppError;
114
+ continue;
115
+ }
116
+ }
117
+
118
+ throw uppError;
119
+ }
120
+ }
121
+ }
122
+
123
+ /**
124
+ * Fetch with timeout
125
+ */
126
+ async function fetchWithTimeout(
127
+ fetchFn: typeof fetch,
128
+ url: string,
129
+ init: RequestInit,
130
+ timeout: number,
131
+ provider: string,
132
+ modality: Modality
133
+ ): Promise<Response> {
134
+ const controller = new AbortController();
135
+ const timeoutId = setTimeout(() => controller.abort(), timeout);
136
+
137
+ // Merge abort signals if one was provided
138
+ const existingSignal = init.signal;
139
+ if (existingSignal) {
140
+ existingSignal.addEventListener('abort', () => controller.abort());
141
+ }
142
+
143
+ try {
144
+ const response = await fetchFn(url, {
145
+ ...init,
146
+ signal: controller.signal,
147
+ });
148
+ return response;
149
+ } catch (error) {
150
+ if ((error as Error).name === 'AbortError') {
151
+ // Check if it was the user's signal or our timeout
152
+ if (existingSignal?.aborted) {
153
+ throw cancelledError(provider, modality);
154
+ }
155
+ throw timeoutError(timeout, provider, modality);
156
+ }
157
+ throw error;
158
+ } finally {
159
+ clearTimeout(timeoutId);
160
+ }
161
+ }
162
+
163
+ /**
164
+ * Sleep for a given number of milliseconds
165
+ */
166
+ function sleep(ms: number): Promise<void> {
167
+ return new Promise((resolve) => setTimeout(resolve, ms));
168
+ }
169
+
170
+ /**
171
+ * Streaming fetch - returns response without checking ok status
172
+ * Used when we need to read the stream for SSE
173
+ */
174
+ export async function doStreamFetch(
175
+ url: string,
176
+ init: RequestInit,
177
+ config: ProviderConfig,
178
+ provider: string,
179
+ modality: Modality
180
+ ): Promise<Response> {
181
+ const fetchFn = config.fetch ?? fetch;
182
+ const timeout = config.timeout ?? DEFAULT_TIMEOUT;
183
+ const strategy = config.retryStrategy;
184
+
185
+ // Pre-request delay
186
+ if (strategy?.beforeRequest) {
187
+ const delay = await strategy.beforeRequest();
188
+ if (delay > 0) {
189
+ await sleep(delay);
190
+ }
191
+ }
192
+
193
+ // For streaming, we don't retry - the consumer handles errors
194
+ try {
195
+ const response = await fetchWithTimeout(
196
+ fetchFn,
197
+ url,
198
+ init,
199
+ timeout,
200
+ provider,
201
+ modality
202
+ );
203
+ return response;
204
+ } catch (error) {
205
+ if (error instanceof UPPError) {
206
+ throw error;
207
+ }
208
+ throw networkError(error as Error, provider, modality);
209
+ }
210
+ }
@@ -0,0 +1,31 @@
1
+ // Key management
2
+ export {
3
+ resolveApiKey,
4
+ RoundRobinKeys,
5
+ WeightedKeys,
6
+ DynamicKey,
7
+ } from './keys.ts';
8
+
9
+ // Retry strategies
10
+ export {
11
+ ExponentialBackoff,
12
+ LinearBackoff,
13
+ NoRetry,
14
+ TokenBucket,
15
+ RetryAfterStrategy,
16
+ } from './retry.ts';
17
+
18
+ // HTTP fetch
19
+ export { doFetch, doStreamFetch } from './fetch.ts';
20
+
21
+ // SSE parsing
22
+ export { parseSSEStream, parseSimpleTextStream } from './sse.ts';
23
+
24
+ // Error utilities
25
+ export {
26
+ normalizeHttpError,
27
+ networkError,
28
+ timeoutError,
29
+ cancelledError,
30
+ statusToErrorCode,
31
+ } from './errors.ts';
@@ -0,0 +1,136 @@
1
+ import type { ProviderConfig, KeyStrategy } from '../types/provider.ts';
2
+ import { UPPError, type Modality } from '../types/errors.ts';
3
+
4
+ /**
5
+ * Round-robin through a list of API keys
6
+ */
7
+ export class RoundRobinKeys implements KeyStrategy {
8
+ private keys: string[];
9
+ private index = 0;
10
+
11
+ constructor(keys: string[]) {
12
+ if (keys.length === 0) {
13
+ throw new Error('RoundRobinKeys requires at least one key');
14
+ }
15
+ this.keys = keys;
16
+ }
17
+
18
+ getKey(): string {
19
+ const key = this.keys[this.index]!;
20
+ this.index = (this.index + 1) % this.keys.length;
21
+ return key;
22
+ }
23
+ }
24
+
25
+ /**
26
+ * Weighted random selection of API keys
27
+ */
28
+ export class WeightedKeys implements KeyStrategy {
29
+ private entries: Array<{ key: string; weight: number }>;
30
+ private totalWeight: number;
31
+
32
+ constructor(keys: Array<{ key: string; weight: number }>) {
33
+ if (keys.length === 0) {
34
+ throw new Error('WeightedKeys requires at least one key');
35
+ }
36
+ this.entries = keys;
37
+ this.totalWeight = keys.reduce((sum, k) => sum + k.weight, 0);
38
+ }
39
+
40
+ getKey(): string {
41
+ const random = Math.random() * this.totalWeight;
42
+ let cumulative = 0;
43
+
44
+ for (const entry of this.entries) {
45
+ cumulative += entry.weight;
46
+ if (random <= cumulative) {
47
+ return entry.key;
48
+ }
49
+ }
50
+
51
+ // Fallback to last key (shouldn't happen)
52
+ return this.entries[this.entries.length - 1]!.key;
53
+ }
54
+ }
55
+
56
+ /**
57
+ * Dynamic key selection based on custom logic
58
+ */
59
+ export class DynamicKey implements KeyStrategy {
60
+ private selector: () => string | Promise<string>;
61
+
62
+ constructor(selector: () => string | Promise<string>) {
63
+ this.selector = selector;
64
+ }
65
+
66
+ async getKey(): Promise<string> {
67
+ return this.selector();
68
+ }
69
+ }
70
+
71
+ /**
72
+ * Check if a value is a KeyStrategy
73
+ */
74
+ function isKeyStrategy(value: unknown): value is KeyStrategy {
75
+ return (
76
+ typeof value === 'object' &&
77
+ value !== null &&
78
+ 'getKey' in value &&
79
+ typeof (value as KeyStrategy).getKey === 'function'
80
+ );
81
+ }
82
+
83
+ /**
84
+ * Resolve API key from ProviderConfig
85
+ * Falls back to environment variable if provided and config.apiKey is not set
86
+ * Throws UPPError with AUTHENTICATION_FAILED if no key is available
87
+ *
88
+ * @param config - Provider configuration
89
+ * @param envVar - Environment variable name to check as fallback
90
+ * @param provider - Provider name for error messages
91
+ * @param modality - Modality for error messages
92
+ */
93
+ export async function resolveApiKey(
94
+ config: ProviderConfig,
95
+ envVar?: string,
96
+ provider = 'unknown',
97
+ modality: Modality = 'llm'
98
+ ): Promise<string> {
99
+ const { apiKey } = config;
100
+
101
+ // If apiKey is provided in config
102
+ if (apiKey !== undefined) {
103
+ // String
104
+ if (typeof apiKey === 'string') {
105
+ return apiKey;
106
+ }
107
+
108
+ // Function
109
+ if (typeof apiKey === 'function') {
110
+ return apiKey();
111
+ }
112
+
113
+ // KeyStrategy
114
+ if (isKeyStrategy(apiKey)) {
115
+ return apiKey.getKey();
116
+ }
117
+ }
118
+
119
+ // Try environment variable
120
+ if (envVar) {
121
+ const envValue = process.env[envVar];
122
+ if (envValue) {
123
+ return envValue;
124
+ }
125
+ }
126
+
127
+ // No key found
128
+ throw new UPPError(
129
+ envVar
130
+ ? `API key not found. Set ${envVar} environment variable or provide apiKey in config.`
131
+ : 'API key not found. Provide apiKey in config.',
132
+ 'AUTHENTICATION_FAILED',
133
+ provider,
134
+ modality
135
+ );
136
+ }
@@ -0,0 +1,205 @@
1
+ import type { RetryStrategy } from '../types/provider.ts';
2
+ import type { UPPError } from '../types/errors.ts';
3
+
4
+ /**
5
+ * Exponential backoff retry strategy
6
+ */
7
+ export class ExponentialBackoff implements RetryStrategy {
8
+ private maxAttempts: number;
9
+ private baseDelay: number;
10
+ private maxDelay: number;
11
+ private jitter: boolean;
12
+
13
+ constructor(options: {
14
+ maxAttempts?: number;
15
+ baseDelay?: number;
16
+ maxDelay?: number;
17
+ jitter?: boolean;
18
+ } = {}) {
19
+ this.maxAttempts = options.maxAttempts ?? 3;
20
+ this.baseDelay = options.baseDelay ?? 1000;
21
+ this.maxDelay = options.maxDelay ?? 30000;
22
+ this.jitter = options.jitter ?? true;
23
+ }
24
+
25
+ onRetry(error: UPPError, attempt: number): number | null {
26
+ if (attempt > this.maxAttempts) {
27
+ return null;
28
+ }
29
+
30
+ // Only retry on retryable errors
31
+ if (!this.isRetryable(error)) {
32
+ return null;
33
+ }
34
+
35
+ // Calculate delay with exponential backoff
36
+ let delay = this.baseDelay * Math.pow(2, attempt - 1);
37
+ delay = Math.min(delay, this.maxDelay);
38
+
39
+ // Add jitter
40
+ if (this.jitter) {
41
+ delay = delay * (0.5 + Math.random());
42
+ }
43
+
44
+ return Math.floor(delay);
45
+ }
46
+
47
+ private isRetryable(error: UPPError): boolean {
48
+ return (
49
+ error.code === 'RATE_LIMITED' ||
50
+ error.code === 'NETWORK_ERROR' ||
51
+ error.code === 'TIMEOUT' ||
52
+ error.code === 'PROVIDER_ERROR'
53
+ );
54
+ }
55
+ }
56
+
57
+ /**
58
+ * Linear backoff retry strategy
59
+ */
60
+ export class LinearBackoff implements RetryStrategy {
61
+ private maxAttempts: number;
62
+ private delay: number;
63
+
64
+ constructor(options: {
65
+ maxAttempts?: number;
66
+ delay?: number;
67
+ } = {}) {
68
+ this.maxAttempts = options.maxAttempts ?? 3;
69
+ this.delay = options.delay ?? 1000;
70
+ }
71
+
72
+ onRetry(error: UPPError, attempt: number): number | null {
73
+ if (attempt > this.maxAttempts) {
74
+ return null;
75
+ }
76
+
77
+ // Only retry on retryable errors
78
+ if (!this.isRetryable(error)) {
79
+ return null;
80
+ }
81
+
82
+ return this.delay * attempt;
83
+ }
84
+
85
+ private isRetryable(error: UPPError): boolean {
86
+ return (
87
+ error.code === 'RATE_LIMITED' ||
88
+ error.code === 'NETWORK_ERROR' ||
89
+ error.code === 'TIMEOUT' ||
90
+ error.code === 'PROVIDER_ERROR'
91
+ );
92
+ }
93
+ }
94
+
95
+ /**
96
+ * No retry strategy - fail immediately
97
+ */
98
+ export class NoRetry implements RetryStrategy {
99
+ onRetry(): null {
100
+ return null;
101
+ }
102
+ }
103
+
104
+ /**
105
+ * Token bucket rate limiter with retry
106
+ */
107
+ export class TokenBucket implements RetryStrategy {
108
+ private tokens: number;
109
+ private maxTokens: number;
110
+ private refillRate: number; // tokens per second
111
+ private lastRefill: number;
112
+ private maxAttempts: number;
113
+
114
+ constructor(options: {
115
+ maxTokens?: number;
116
+ refillRate?: number;
117
+ maxAttempts?: number;
118
+ } = {}) {
119
+ this.maxTokens = options.maxTokens ?? 10;
120
+ this.refillRate = options.refillRate ?? 1;
121
+ this.maxAttempts = options.maxAttempts ?? 3;
122
+ this.tokens = this.maxTokens;
123
+ this.lastRefill = Date.now();
124
+ }
125
+
126
+ beforeRequest(): number {
127
+ this.refill();
128
+
129
+ if (this.tokens >= 1) {
130
+ this.tokens -= 1;
131
+ return 0;
132
+ }
133
+
134
+ // Calculate time until next token
135
+ const msPerToken = 1000 / this.refillRate;
136
+ return Math.ceil(msPerToken);
137
+ }
138
+
139
+ onRetry(error: UPPError, attempt: number): number | null {
140
+ if (attempt > this.maxAttempts) {
141
+ return null;
142
+ }
143
+
144
+ if (error.code !== 'RATE_LIMITED') {
145
+ return null;
146
+ }
147
+
148
+ // Wait for token bucket to refill
149
+ const msPerToken = 1000 / this.refillRate;
150
+ return Math.ceil(msPerToken * 2); // Wait for 2 tokens
151
+ }
152
+
153
+ reset(): void {
154
+ this.tokens = this.maxTokens;
155
+ this.lastRefill = Date.now();
156
+ }
157
+
158
+ private refill(): void {
159
+ const now = Date.now();
160
+ const elapsed = (now - this.lastRefill) / 1000;
161
+ const newTokens = elapsed * this.refillRate;
162
+
163
+ this.tokens = Math.min(this.maxTokens, this.tokens + newTokens);
164
+ this.lastRefill = now;
165
+ }
166
+ }
167
+
168
+ /**
169
+ * Retry strategy that respects Retry-After headers
170
+ */
171
+ export class RetryAfterStrategy implements RetryStrategy {
172
+ private maxAttempts: number;
173
+ private fallbackDelay: number;
174
+ private lastRetryAfter?: number;
175
+
176
+ constructor(options: {
177
+ maxAttempts?: number;
178
+ fallbackDelay?: number;
179
+ } = {}) {
180
+ this.maxAttempts = options.maxAttempts ?? 3;
181
+ this.fallbackDelay = options.fallbackDelay ?? 5000;
182
+ }
183
+
184
+ /**
185
+ * Set the Retry-After value from response headers
186
+ * Call this before onRetry when you have a Retry-After header
187
+ */
188
+ setRetryAfter(seconds: number): void {
189
+ this.lastRetryAfter = seconds * 1000;
190
+ }
191
+
192
+ onRetry(error: UPPError, attempt: number): number | null {
193
+ if (attempt > this.maxAttempts) {
194
+ return null;
195
+ }
196
+
197
+ if (error.code !== 'RATE_LIMITED') {
198
+ return null;
199
+ }
200
+
201
+ const delay = this.lastRetryAfter ?? this.fallbackDelay;
202
+ this.lastRetryAfter = undefined;
203
+ return delay;
204
+ }
205
+ }