@providerprotocol/ai 0.0.21 → 0.0.23

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 (53) hide show
  1. package/README.md +188 -6
  2. package/dist/anthropic/index.d.ts +1 -1
  3. package/dist/anthropic/index.js +115 -39
  4. package/dist/anthropic/index.js.map +1 -1
  5. package/dist/{chunk-Y3GBJNA2.js → chunk-55X3W2MN.js} +4 -3
  6. package/dist/chunk-55X3W2MN.js.map +1 -0
  7. package/dist/chunk-73IIE3QT.js +120 -0
  8. package/dist/chunk-73IIE3QT.js.map +1 -0
  9. package/dist/{chunk-M4BMM5IB.js → chunk-MF5ETY5O.js} +13 -4
  10. package/dist/chunk-MF5ETY5O.js.map +1 -0
  11. package/dist/{chunk-SKY2JLA7.js → chunk-MKDLXV4O.js} +1 -1
  12. package/dist/chunk-MKDLXV4O.js.map +1 -0
  13. package/dist/{chunk-Z7RBRCRN.js → chunk-NWS5IKNR.js} +37 -11
  14. package/dist/chunk-NWS5IKNR.js.map +1 -0
  15. package/dist/{chunk-EDENPF3E.js → chunk-QNJO7DSD.js} +152 -53
  16. package/dist/chunk-QNJO7DSD.js.map +1 -0
  17. package/dist/{chunk-Z4ILICF5.js → chunk-SBCATNHA.js} +43 -14
  18. package/dist/chunk-SBCATNHA.js.map +1 -0
  19. package/dist/chunk-Z6DKC37J.js +50 -0
  20. package/dist/chunk-Z6DKC37J.js.map +1 -0
  21. package/dist/google/index.d.ts +22 -7
  22. package/dist/google/index.js +286 -85
  23. package/dist/google/index.js.map +1 -1
  24. package/dist/http/index.d.ts +3 -3
  25. package/dist/http/index.js +4 -4
  26. package/dist/index.d.ts +10 -6
  27. package/dist/index.js +331 -204
  28. package/dist/index.js.map +1 -1
  29. package/dist/ollama/index.d.ts +5 -2
  30. package/dist/ollama/index.js +87 -28
  31. package/dist/ollama/index.js.map +1 -1
  32. package/dist/openai/index.d.ts +1 -1
  33. package/dist/openai/index.js +226 -81
  34. package/dist/openai/index.js.map +1 -1
  35. package/dist/openrouter/index.d.ts +1 -1
  36. package/dist/openrouter/index.js +199 -64
  37. package/dist/openrouter/index.js.map +1 -1
  38. package/dist/{provider-DGQHYE6I.d.ts → provider-DR1yins0.d.ts} +159 -53
  39. package/dist/proxy/index.d.ts +2 -2
  40. package/dist/proxy/index.js +178 -17
  41. package/dist/proxy/index.js.map +1 -1
  42. package/dist/{retry-Pcs3hnbu.d.ts → retry-DJiqAslw.d.ts} +11 -2
  43. package/dist/{stream-Di9acos2.d.ts → stream-BuTrqt_j.d.ts} +103 -41
  44. package/dist/xai/index.d.ts +1 -1
  45. package/dist/xai/index.js +189 -75
  46. package/dist/xai/index.js.map +1 -1
  47. package/package.json +1 -1
  48. package/dist/chunk-EDENPF3E.js.map +0 -1
  49. package/dist/chunk-M4BMM5IB.js.map +0 -1
  50. package/dist/chunk-SKY2JLA7.js.map +0 -1
  51. package/dist/chunk-Y3GBJNA2.js.map +0 -1
  52. package/dist/chunk-Z4ILICF5.js.map +0 -1
  53. package/dist/chunk-Z7RBRCRN.js.map +0 -1
@@ -1,4 +1,42 @@
1
1
  // src/types/errors.ts
2
+ var ErrorCode = {
3
+ /** API key is invalid or expired */
4
+ AuthenticationFailed: "AUTHENTICATION_FAILED",
5
+ /** Rate limit exceeded, retry after delay */
6
+ RateLimited: "RATE_LIMITED",
7
+ /** Input exceeds model's context window */
8
+ ContextLengthExceeded: "CONTEXT_LENGTH_EXCEEDED",
9
+ /** Requested model does not exist */
10
+ ModelNotFound: "MODEL_NOT_FOUND",
11
+ /** Request parameters are malformed */
12
+ InvalidRequest: "INVALID_REQUEST",
13
+ /** Provider returned an unexpected response format */
14
+ InvalidResponse: "INVALID_RESPONSE",
15
+ /** Content was blocked by safety filters */
16
+ ContentFiltered: "CONTENT_FILTERED",
17
+ /** Account quota or credits exhausted */
18
+ QuotaExceeded: "QUOTA_EXCEEDED",
19
+ /** Provider-specific error not covered by other codes */
20
+ ProviderError: "PROVIDER_ERROR",
21
+ /** Network connectivity issue */
22
+ NetworkError: "NETWORK_ERROR",
23
+ /** Request exceeded timeout limit */
24
+ Timeout: "TIMEOUT",
25
+ /** Request was cancelled via AbortSignal */
26
+ Cancelled: "CANCELLED"
27
+ };
28
+ var ModalityType = {
29
+ /** Large language model for text generation */
30
+ LLM: "llm",
31
+ /** Text/image embedding model */
32
+ Embedding: "embedding",
33
+ /** Image generation model */
34
+ Image: "image",
35
+ /** Audio processing/generation model */
36
+ Audio: "audio",
37
+ /** Video processing/generation model */
38
+ Video: "video"
39
+ };
2
40
  var UPPError = class _UPPError extends Error {
3
41
  /** Normalized error code for programmatic handling */
4
42
  code;
@@ -65,34 +103,60 @@ var UPPError = class _UPPError extends Error {
65
103
  }
66
104
  };
67
105
 
106
+ // src/utils/error.ts
107
+ function toError(value) {
108
+ if (value instanceof Error) {
109
+ return value;
110
+ }
111
+ if (typeof value === "string") {
112
+ return new Error(value);
113
+ }
114
+ if (typeof value === "object" && value !== null && "message" in value) {
115
+ const message = value.message;
116
+ if (typeof message === "string") {
117
+ return new Error(message);
118
+ }
119
+ }
120
+ return new Error(String(value));
121
+ }
122
+
68
123
  // src/http/errors.ts
69
124
  function statusToErrorCode(status) {
70
125
  switch (status) {
71
126
  case 400:
72
- return "INVALID_REQUEST";
127
+ return ErrorCode.InvalidRequest;
128
+ case 402:
129
+ return ErrorCode.QuotaExceeded;
73
130
  case 401:
74
131
  case 403:
75
- return "AUTHENTICATION_FAILED";
132
+ return ErrorCode.AuthenticationFailed;
76
133
  case 404:
77
- return "MODEL_NOT_FOUND";
134
+ return ErrorCode.ModelNotFound;
78
135
  case 408:
79
- return "TIMEOUT";
136
+ return ErrorCode.Timeout;
137
+ case 409:
138
+ return ErrorCode.InvalidRequest;
139
+ case 422:
140
+ return ErrorCode.InvalidRequest;
80
141
  case 413:
81
- return "CONTEXT_LENGTH_EXCEEDED";
142
+ return ErrorCode.ContextLengthExceeded;
143
+ case 451:
144
+ return ErrorCode.ContentFiltered;
82
145
  case 429:
83
- return "RATE_LIMITED";
146
+ return ErrorCode.RateLimited;
84
147
  case 500:
85
148
  case 502:
86
149
  case 503:
87
150
  case 504:
88
- return "PROVIDER_ERROR";
151
+ return ErrorCode.ProviderError;
89
152
  default:
90
- return "PROVIDER_ERROR";
153
+ return ErrorCode.ProviderError;
91
154
  }
92
155
  }
93
156
  async function normalizeHttpError(response, provider, modality) {
94
157
  const code = statusToErrorCode(response.status);
95
158
  let message = `HTTP ${response.status}: ${response.statusText}`;
159
+ let bodyReadError;
96
160
  try {
97
161
  const body = await response.text();
98
162
  if (body) {
@@ -108,14 +172,15 @@ async function normalizeHttpError(response, provider, modality) {
108
172
  }
109
173
  }
110
174
  }
111
- } catch {
175
+ } catch (error) {
176
+ bodyReadError = toError(error);
112
177
  }
113
- return new UPPError(message, code, provider, modality, response.status);
178
+ return new UPPError(message, code, provider, modality, response.status, bodyReadError);
114
179
  }
115
180
  function networkError(error, provider, modality) {
116
181
  return new UPPError(
117
182
  `Network error: ${error.message}`,
118
- "NETWORK_ERROR",
183
+ ErrorCode.NetworkError,
119
184
  provider,
120
185
  modality,
121
186
  void 0,
@@ -125,33 +190,43 @@ function networkError(error, provider, modality) {
125
190
  function timeoutError(timeout, provider, modality) {
126
191
  return new UPPError(
127
192
  `Request timed out after ${timeout}ms`,
128
- "TIMEOUT",
193
+ ErrorCode.Timeout,
129
194
  provider,
130
195
  modality
131
196
  );
132
197
  }
133
198
  function cancelledError(provider, modality) {
134
- return new UPPError("Request was cancelled", "CANCELLED", provider, modality);
199
+ return new UPPError(
200
+ "Request was cancelled",
201
+ ErrorCode.Cancelled,
202
+ provider,
203
+ modality
204
+ );
135
205
  }
136
206
 
137
207
  // src/http/fetch.ts
138
208
  var DEFAULT_TIMEOUT = 12e4;
209
+ var MAX_RETRY_AFTER_SECONDS = 3600;
210
+ function hasFork(strategy) {
211
+ return !!strategy && typeof strategy.fork === "function";
212
+ }
139
213
  async function doFetch(url, init, config, provider, modality) {
140
214
  const fetchFn = config.fetch ?? fetch;
141
215
  const timeout = config.timeout ?? DEFAULT_TIMEOUT;
142
- const strategy = config.retryStrategy;
143
- if (strategy?.beforeRequest) {
144
- const delay = await strategy.beforeRequest();
145
- if (delay > 0) {
146
- await sleep(delay);
147
- }
148
- }
149
- let lastError;
216
+ const baseStrategy = config.retryStrategy;
217
+ const strategy = hasFork(baseStrategy) ? baseStrategy.fork() : baseStrategy;
150
218
  let attempt = 0;
151
219
  while (true) {
152
220
  attempt++;
221
+ if (strategy?.beforeRequest) {
222
+ const delay = await strategy.beforeRequest();
223
+ if (delay > 0) {
224
+ await sleep(delay);
225
+ }
226
+ }
227
+ let response;
153
228
  try {
154
- const response = await fetchWithTimeout(
229
+ response = await fetchWithTimeout(
155
230
  fetchFn,
156
231
  url,
157
232
  init,
@@ -159,52 +234,49 @@ async function doFetch(url, init, config, provider, modality) {
159
234
  provider,
160
235
  modality
161
236
  );
162
- if (!response.ok) {
163
- const error = await normalizeHttpError(response, provider, modality);
164
- const retryAfter = response.headers.get("Retry-After");
165
- if (retryAfter && strategy) {
166
- const seconds = parseInt(retryAfter, 10);
167
- if (!isNaN(seconds) && "setRetryAfter" in strategy) {
168
- strategy.setRetryAfter(
169
- seconds
170
- );
171
- }
172
- }
173
- if (strategy) {
174
- const delay = await strategy.onRetry(error, attempt);
175
- if (delay !== null) {
176
- await sleep(delay);
177
- lastError = error;
178
- continue;
179
- }
180
- }
181
- throw error;
182
- }
183
- strategy?.reset?.();
184
- return response;
185
237
  } catch (error) {
186
238
  if (error instanceof UPPError) {
187
239
  if (strategy) {
188
240
  const delay = await strategy.onRetry(error, attempt);
189
241
  if (delay !== null) {
190
242
  await sleep(delay);
191
- lastError = error;
192
243
  continue;
193
244
  }
194
245
  }
195
246
  throw error;
196
247
  }
197
- const uppError = networkError(error, provider, modality);
248
+ const uppError = networkError(toError(error), provider, modality);
198
249
  if (strategy) {
199
250
  const delay = await strategy.onRetry(uppError, attempt);
200
251
  if (delay !== null) {
201
252
  await sleep(delay);
202
- lastError = uppError;
203
253
  continue;
204
254
  }
205
255
  }
206
256
  throw uppError;
207
257
  }
258
+ if (!response.ok) {
259
+ const error = await normalizeHttpError(response, provider, modality);
260
+ const retryAfterSeconds = parseRetryAfter(
261
+ response.headers.get("Retry-After"),
262
+ config.retryAfterMaxSeconds ?? MAX_RETRY_AFTER_SECONDS
263
+ );
264
+ if (retryAfterSeconds !== null && strategy && "setRetryAfter" in strategy) {
265
+ strategy.setRetryAfter(
266
+ retryAfterSeconds
267
+ );
268
+ }
269
+ if (strategy) {
270
+ const delay = await strategy.onRetry(error, attempt);
271
+ if (delay !== null) {
272
+ await sleep(delay);
273
+ continue;
274
+ }
275
+ }
276
+ throw error;
277
+ }
278
+ strategy?.reset?.();
279
+ return response;
208
280
  }
209
281
  }
210
282
  async function fetchWithTimeout(fetchFn, url, init, timeout, provider, modality) {
@@ -214,8 +286,9 @@ async function fetchWithTimeout(fetchFn, url, init, timeout, provider, modality)
214
286
  }
215
287
  const controller = new AbortController();
216
288
  const timeoutId = setTimeout(() => controller.abort(), timeout);
289
+ const onAbort = () => controller.abort();
217
290
  if (existingSignal) {
218
- existingSignal.addEventListener("abort", () => controller.abort());
291
+ existingSignal.addEventListener("abort", onAbort, { once: true });
219
292
  }
220
293
  try {
221
294
  const response = await fetchFn(url, {
@@ -224,7 +297,7 @@ async function fetchWithTimeout(fetchFn, url, init, timeout, provider, modality)
224
297
  });
225
298
  return response;
226
299
  } catch (error) {
227
- if (error.name === "AbortError") {
300
+ if (toError(error).name === "AbortError") {
228
301
  if (existingSignal?.aborted) {
229
302
  throw cancelledError(provider, modality);
230
303
  }
@@ -233,6 +306,9 @@ async function fetchWithTimeout(fetchFn, url, init, timeout, provider, modality)
233
306
  throw error;
234
307
  } finally {
235
308
  clearTimeout(timeoutId);
309
+ if (existingSignal) {
310
+ existingSignal.removeEventListener("abort", onAbort);
311
+ }
236
312
  }
237
313
  }
238
314
  function sleep(ms) {
@@ -241,7 +317,8 @@ function sleep(ms) {
241
317
  async function doStreamFetch(url, init, config, provider, modality) {
242
318
  const fetchFn = config.fetch ?? fetch;
243
319
  const timeout = config.timeout ?? DEFAULT_TIMEOUT;
244
- const strategy = config.retryStrategy;
320
+ const baseStrategy = config.retryStrategy;
321
+ const strategy = hasFork(baseStrategy) ? baseStrategy.fork() : baseStrategy;
245
322
  if (strategy?.beforeRequest) {
246
323
  const delay = await strategy.beforeRequest();
247
324
  if (delay > 0) {
@@ -262,12 +339,34 @@ async function doStreamFetch(url, init, config, provider, modality) {
262
339
  if (error instanceof UPPError) {
263
340
  throw error;
264
341
  }
265
- throw networkError(error, provider, modality);
342
+ throw networkError(toError(error), provider, modality);
343
+ }
344
+ }
345
+ function parseRetryAfter(headerValue, maxSeconds) {
346
+ if (!headerValue) {
347
+ return null;
348
+ }
349
+ const seconds = parseInt(headerValue, 10);
350
+ if (!Number.isNaN(seconds)) {
351
+ return Math.min(maxSeconds, Math.max(0, seconds));
352
+ }
353
+ const dateMillis = Date.parse(headerValue);
354
+ if (Number.isNaN(dateMillis)) {
355
+ return null;
356
+ }
357
+ const deltaMs = dateMillis - Date.now();
358
+ if (deltaMs <= 0) {
359
+ return 0;
266
360
  }
361
+ const deltaSeconds = Math.ceil(deltaMs / 1e3);
362
+ return Math.min(maxSeconds, Math.max(0, deltaSeconds));
267
363
  }
268
364
 
269
365
  export {
366
+ ErrorCode,
367
+ ModalityType,
270
368
  UPPError,
369
+ toError,
271
370
  statusToErrorCode,
272
371
  normalizeHttpError,
273
372
  networkError,
@@ -276,4 +375,4 @@ export {
276
375
  doFetch,
277
376
  doStreamFetch
278
377
  };
279
- //# sourceMappingURL=chunk-EDENPF3E.js.map
378
+ //# sourceMappingURL=chunk-QNJO7DSD.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/types/errors.ts","../src/utils/error.ts","../src/http/errors.ts","../src/http/fetch.ts"],"sourcesContent":["/**\n * @fileoverview Error types for the Unified Provider Protocol.\n *\n * Provides normalized error codes and a unified error class for handling\n * errors across different AI providers in a consistent manner.\n *\n * @module types/errors\n */\n\n/**\n * Error code constants for cross-provider error handling.\n *\n * Use these constants instead of raw strings for type-safe error handling:\n *\n * @example\n * ```typescript\n * import { ErrorCode } from 'upp';\n *\n * try {\n * await llm.generate('Hello');\n * } catch (error) {\n * if (error instanceof UPPError) {\n * switch (error.code) {\n * case ErrorCode.RateLimited:\n * await delay(error.retryAfter);\n * break;\n * case ErrorCode.AuthenticationFailed:\n * throw new Error('Invalid API key');\n * }\n * }\n * }\n * ```\n */\nexport const ErrorCode = {\n /** API key is invalid or expired */\n AuthenticationFailed: 'AUTHENTICATION_FAILED',\n /** Rate limit exceeded, retry after delay */\n RateLimited: 'RATE_LIMITED',\n /** Input exceeds model's context window */\n ContextLengthExceeded: 'CONTEXT_LENGTH_EXCEEDED',\n /** Requested model does not exist */\n ModelNotFound: 'MODEL_NOT_FOUND',\n /** Request parameters are malformed */\n InvalidRequest: 'INVALID_REQUEST',\n /** Provider returned an unexpected response format */\n InvalidResponse: 'INVALID_RESPONSE',\n /** Content was blocked by safety filters */\n ContentFiltered: 'CONTENT_FILTERED',\n /** Account quota or credits exhausted */\n QuotaExceeded: 'QUOTA_EXCEEDED',\n /** Provider-specific error not covered by other codes */\n ProviderError: 'PROVIDER_ERROR',\n /** Network connectivity issue */\n NetworkError: 'NETWORK_ERROR',\n /** Request exceeded timeout limit */\n Timeout: 'TIMEOUT',\n /** Request was cancelled via AbortSignal */\n Cancelled: 'CANCELLED',\n} as const;\n\n/**\n * Error code discriminator union.\n *\n * This type is derived from {@link ErrorCode} constants. Use `ErrorCode.RateLimited`\n * for constants or `type MyCode = ErrorCode` for type annotations.\n */\nexport type ErrorCode = (typeof ErrorCode)[keyof typeof ErrorCode];\n\n/**\n * Modality type constants.\n *\n * Use these constants for type-safe modality handling:\n *\n * @example\n * ```typescript\n * import { ModalityType } from 'upp';\n *\n * if (provider.modality === ModalityType.LLM) {\n * // Handle LLM provider\n * }\n * ```\n */\nexport const ModalityType = {\n /** Large language model for text generation */\n LLM: 'llm',\n /** Text/image embedding model */\n Embedding: 'embedding',\n /** Image generation model */\n Image: 'image',\n /** Audio processing/generation model */\n Audio: 'audio',\n /** Video processing/generation model */\n Video: 'video',\n} as const;\n\n/**\n * Modality type discriminator union.\n *\n * This type is derived from {@link ModalityType} constants. The name `Modality`\n * is kept for backward compatibility; `ModalityType` works as both the const\n * object and this type.\n */\nexport type Modality = (typeof ModalityType)[keyof typeof ModalityType];\n\n/**\n * Type alias for Modality, allowing `ModalityType` to work as both const and type.\n */\nexport type ModalityType = Modality;\n\n/**\n * Unified Provider Protocol Error.\n *\n * All provider-specific errors are normalized to this type, providing\n * a consistent interface for error handling across different AI providers.\n *\n * @example\n * ```typescript\n * import { ErrorCode, ModalityType } from 'upp';\n *\n * throw new UPPError(\n * 'API key is invalid',\n * ErrorCode.AuthenticationFailed,\n * 'openai',\n * ModalityType.LLM,\n * 401\n * );\n * ```\n *\n * @example\n * ```typescript\n * import { ErrorCode, ModalityType } from 'upp';\n *\n * // Wrapping a provider error\n * try {\n * await openai.chat.completions.create({ ... });\n * } catch (err) {\n * throw new UPPError(\n * 'OpenAI request failed',\n * ErrorCode.ProviderError,\n * 'openai',\n * ModalityType.LLM,\n * err.status,\n * err\n * );\n * }\n * ```\n */\nexport class UPPError extends Error {\n /** Normalized error code for programmatic handling */\n readonly code: ErrorCode;\n\n /** Name of the provider that generated the error */\n readonly provider: string;\n\n /** The modality that was being used when the error occurred */\n readonly modality: Modality;\n\n /** HTTP status code from the provider's response, if available */\n readonly statusCode?: number;\n\n /** The original error that caused this UPPError, if wrapping another error */\n override readonly cause?: Error;\n\n /** Error class name, always 'UPPError' */\n override readonly name = 'UPPError';\n\n /**\n * Creates a new UPPError instance.\n *\n * @param message - Human-readable error description\n * @param code - Normalized error code for programmatic handling\n * @param provider - Name of the provider that generated the error\n * @param modality - The modality that was being used\n * @param statusCode - HTTP status code from the provider's response\n * @param cause - The original error being wrapped\n */\n constructor(\n message: string,\n code: ErrorCode,\n provider: string,\n modality: Modality,\n statusCode?: number,\n cause?: Error\n ) {\n super(message);\n this.code = code;\n this.provider = provider;\n this.modality = modality;\n this.statusCode = statusCode;\n this.cause = cause;\n\n if (Error.captureStackTrace) {\n Error.captureStackTrace(this, UPPError);\n }\n }\n\n /**\n * Creates a string representation of the error.\n *\n * @returns Formatted error string including code, message, provider, and modality\n */\n override toString(): string {\n let str = `UPPError [${this.code}]: ${this.message}`;\n str += ` (provider: ${this.provider}, modality: ${this.modality}`;\n if (this.statusCode) {\n str += `, status: ${this.statusCode}`;\n }\n str += ')';\n return str;\n }\n\n /**\n * Converts the error to a JSON-serializable object.\n *\n * @returns Plain object representation suitable for logging or transmission\n */\n toJSON(): Record<string, unknown> {\n return {\n name: this.name,\n message: this.message,\n code: this.code,\n provider: this.provider,\n modality: this.modality,\n statusCode: this.statusCode,\n cause: this.cause?.message,\n };\n }\n}\n","/**\n * @fileoverview Error normalization utilities.\n *\n * @module utils/error\n */\n\n/**\n * Converts an unknown thrown value into an Error instance.\n *\n * @param value - Unknown error value\n * @returns An Error instance\n */\nexport function toError(value: unknown): Error {\n if (value instanceof Error) {\n return value;\n }\n if (typeof value === 'string') {\n return new Error(value);\n }\n if (typeof value === 'object' && value !== null && 'message' in value) {\n const message = (value as { message?: unknown }).message;\n if (typeof message === 'string') {\n return new Error(message);\n }\n }\n return new Error(String(value));\n}\n","/**\n * HTTP error handling and normalization utilities.\n * @module http/errors\n */\n\nimport {\n UPPError,\n ErrorCode,\n type Modality,\n} from '../types/errors.ts';\nimport { toError } from '../utils/error.ts';\n\n/**\n * Maps HTTP status codes to standardized UPP error codes.\n *\n * This function provides consistent error categorization across all providers:\n * - 400 -> INVALID_REQUEST (bad request format or parameters)\n * - 401, 403 -> AUTHENTICATION_FAILED (invalid or missing credentials)\n * - 404 -> MODEL_NOT_FOUND (requested model does not exist)\n * - 408 -> TIMEOUT (request timed out)\n * - 413 -> CONTEXT_LENGTH_EXCEEDED (input too long)\n * - 429 -> RATE_LIMITED (too many requests)\n * - 5xx -> PROVIDER_ERROR (server-side issues)\n *\n * @param status - HTTP status code from the response\n * @returns The corresponding UPP ErrorCode\n *\n * @example\n * ```typescript\n * const errorCode = statusToErrorCode(429);\n * // Returns 'RATE_LIMITED'\n *\n * const serverError = statusToErrorCode(503);\n * // Returns 'PROVIDER_ERROR'\n * ```\n */\nexport function statusToErrorCode(status: number): ErrorCode {\n switch (status) {\n case 400:\n return ErrorCode.InvalidRequest;\n case 402:\n return ErrorCode.QuotaExceeded;\n case 401:\n case 403:\n return ErrorCode.AuthenticationFailed;\n case 404:\n return ErrorCode.ModelNotFound;\n case 408:\n return ErrorCode.Timeout;\n case 409:\n return ErrorCode.InvalidRequest;\n case 422:\n return ErrorCode.InvalidRequest;\n case 413:\n return ErrorCode.ContextLengthExceeded;\n case 451:\n return ErrorCode.ContentFiltered;\n case 429:\n return ErrorCode.RateLimited;\n case 500:\n case 502:\n case 503:\n case 504:\n return ErrorCode.ProviderError;\n default:\n return ErrorCode.ProviderError;\n }\n}\n\n/**\n * Normalizes HTTP error responses into standardized UPPError objects.\n *\n * This function performs several operations:\n * 1. Maps the HTTP status code to an appropriate ErrorCode\n * 2. Attempts to extract a meaningful error message from the response body\n * 3. Handles various provider-specific error response formats\n *\n * Supported error message formats:\n * - `{ error: { message: \"...\" } }` (OpenAI, Anthropic)\n * - `{ message: \"...\" }` (simple format)\n * - `{ error: { error: { message: \"...\" } } }` (nested format)\n * - `{ detail: \"...\" }` (FastAPI style)\n * - Plain text body (if under 200 characters)\n *\n * @param response - The HTTP Response object with non-2xx status\n * @param provider - Provider identifier for error context\n * @param modality - Request modality for error context\n * @returns A UPPError with normalized code and message\n *\n * @example\n * ```typescript\n * if (!response.ok) {\n * const error = await normalizeHttpError(response, 'openai', 'llm');\n * // error.code might be 'RATE_LIMITED' for 429\n * // error.message contains provider's error message\n * throw error;\n * }\n * ```\n */\nexport async function normalizeHttpError(\n response: Response,\n provider: string,\n modality: Modality\n): Promise<UPPError> {\n const code = statusToErrorCode(response.status);\n let message = `HTTP ${response.status}: ${response.statusText}`;\n let bodyReadError: Error | undefined;\n\n try {\n const body = await response.text();\n if (body) {\n try {\n const json = JSON.parse(body);\n const extractedMessage =\n json.error?.message ||\n json.message ||\n json.error?.error?.message ||\n json.detail;\n\n if (extractedMessage) {\n message = extractedMessage;\n }\n } catch {\n if (body.length < 200) {\n message = body;\n }\n }\n }\n } catch (error) {\n bodyReadError = toError(error);\n }\n\n return new UPPError(message, code, provider, modality, response.status, bodyReadError);\n}\n\n/**\n * Creates a UPPError for network failures (DNS, connection, etc.).\n *\n * Use this when the request fails before receiving any HTTP response,\n * such as DNS resolution failures, connection refused, or network unreachable.\n *\n * @param error - The underlying Error that caused the failure\n * @param provider - Provider identifier for error context\n * @param modality - Request modality for error context\n * @returns A UPPError with NETWORK_ERROR code and the original error attached\n */\nexport function networkError(\n error: Error,\n provider: string,\n modality: Modality\n): UPPError {\n return new UPPError(\n `Network error: ${error.message}`,\n ErrorCode.NetworkError,\n provider,\n modality,\n undefined,\n error\n );\n}\n\n/**\n * Creates a UPPError for request timeout.\n *\n * Use this when the request exceeds the configured timeout duration\n * and is aborted by the AbortController.\n *\n * @param timeout - The timeout duration in milliseconds that was exceeded\n * @param provider - Provider identifier for error context\n * @param modality - Request modality for error context\n * @returns A UPPError with TIMEOUT code\n */\nexport function timeoutError(\n timeout: number,\n provider: string,\n modality: Modality\n): UPPError {\n return new UPPError(\n `Request timed out after ${timeout}ms`,\n ErrorCode.Timeout,\n provider,\n modality\n );\n}\n\n/**\n * Creates a UPPError for user-initiated request cancellation.\n *\n * Use this when the request is aborted via a user-provided AbortSignal,\n * distinct from timeout-based cancellation.\n *\n * @param provider - Provider identifier for error context\n * @param modality - Request modality for error context\n * @returns A UPPError with CANCELLED code\n */\nexport function cancelledError(provider: string, modality: Modality): UPPError {\n return new UPPError(\n 'Request was cancelled',\n ErrorCode.Cancelled,\n provider,\n modality\n );\n}\n","/**\n * HTTP fetch utilities with retry, timeout, and error normalization.\n * @module http/fetch\n */\n\nimport type { ProviderConfig, RetryStrategy } from '../types/provider.ts';\nimport type { Modality } from '../types/errors.ts';\nimport { UPPError } from '../types/errors.ts';\nimport {\n normalizeHttpError,\n networkError,\n timeoutError,\n cancelledError,\n} from './errors.ts';\nimport { toError } from '../utils/error.ts';\n\n/** Default request timeout in milliseconds (2 minutes). */\nconst DEFAULT_TIMEOUT = 120000;\nconst MAX_RETRY_AFTER_SECONDS = 3600;\n\ntype ForkableRetryStrategy = RetryStrategy & {\n fork: () => RetryStrategy | undefined;\n};\n\nfunction hasFork(strategy: RetryStrategy | undefined): strategy is ForkableRetryStrategy {\n return !!strategy && typeof (strategy as { fork?: unknown }).fork === 'function';\n}\n\n/**\n * Executes an HTTP fetch request with automatic retry, timeout handling, and error normalization.\n *\n * This function wraps the standard fetch API with additional capabilities:\n * - Configurable timeout with automatic request cancellation (per attempt)\n * - Retry strategy support (exponential backoff, linear, token bucket, etc.)\n * - Pre-request delay support for rate limiting strategies\n * - Automatic Retry-After header parsing and handling\n * - Error normalization to UPPError format\n *\n * @param url - The URL to fetch\n * @param init - Standard fetch RequestInit options (method, headers, body, etc.)\n * @param config - Provider configuration containing fetch customization, timeout, and retry strategy\n * @param provider - Provider identifier for error context (e.g., 'openai', 'anthropic')\n * @param modality - Request modality for error context (e.g., 'llm', 'embedding', 'image')\n * @returns The successful Response object\n *\n * @throws {UPPError} RATE_LIMITED - When rate limited and retries exhausted\n * @throws {UPPError} NETWORK_ERROR - When a network failure occurs\n * @throws {UPPError} TIMEOUT - When the request times out\n * @throws {UPPError} CANCELLED - When the request is aborted via signal\n * @throws {UPPError} Various codes based on HTTP status (see statusToErrorCode)\n *\n * @example\n * ```typescript\n * const response = await doFetch(\n * 'https://api.openai.com/v1/chat/completions',\n * {\n * method: 'POST',\n * headers: { 'Authorization': 'Bearer sk-...' },\n * body: JSON.stringify({ model: 'gpt-4', messages: [] })\n * },\n * { timeout: 30000, retryStrategy: new ExponentialBackoff() },\n * 'openai',\n * 'llm'\n * );\n * ```\n */\nexport async function doFetch(\n url: string,\n init: RequestInit,\n config: ProviderConfig,\n provider: string,\n modality: Modality\n): Promise<Response> {\n const fetchFn = config.fetch ?? fetch;\n const timeout = config.timeout ?? DEFAULT_TIMEOUT;\n const baseStrategy = config.retryStrategy;\n const strategy = hasFork(baseStrategy) ? baseStrategy.fork() : baseStrategy;\n\n let attempt = 0;\n\n while (true) {\n attempt++;\n\n if (strategy?.beforeRequest) {\n const delay = await strategy.beforeRequest();\n if (delay > 0) {\n await sleep(delay);\n }\n }\n\n let response: Response;\n try {\n response = await fetchWithTimeout(\n fetchFn,\n url,\n init,\n timeout,\n provider,\n modality\n );\n } catch (error) {\n if (error instanceof UPPError) {\n if (strategy) {\n const delay = await strategy.onRetry(error, attempt);\n if (delay !== null) {\n await sleep(delay);\n continue;\n }\n }\n throw error;\n }\n\n const uppError = networkError(toError(error), provider, modality);\n\n if (strategy) {\n const delay = await strategy.onRetry(uppError, attempt);\n if (delay !== null) {\n await sleep(delay);\n continue;\n }\n }\n\n throw uppError;\n }\n\n if (!response.ok) {\n const error = await normalizeHttpError(response, provider, modality);\n\n const retryAfterSeconds = parseRetryAfter(\n response.headers.get('Retry-After'),\n config.retryAfterMaxSeconds ?? MAX_RETRY_AFTER_SECONDS\n );\n if (retryAfterSeconds !== null && strategy && 'setRetryAfter' in strategy) {\n (strategy as { setRetryAfter: (s: number) => void }).setRetryAfter(\n retryAfterSeconds\n );\n }\n\n if (strategy) {\n const delay = await strategy.onRetry(error, attempt);\n if (delay !== null) {\n await sleep(delay);\n continue;\n }\n }\n\n throw error;\n }\n\n strategy?.reset?.();\n\n return response;\n }\n}\n\n/**\n * Executes a fetch request with configurable timeout.\n *\n * Creates an AbortController to cancel the request if it exceeds the timeout.\n * Properly handles both user-provided abort signals and timeout-based cancellation,\n * throwing appropriate error types for each case.\n *\n * @param fetchFn - The fetch function to use (allows custom implementations)\n * @param url - The URL to fetch\n * @param init - Standard fetch RequestInit options\n * @param timeout - Maximum time in milliseconds before aborting\n * @param provider - Provider identifier for error context\n * @param modality - Request modality for error context\n * @returns The Response from the fetch call\n *\n * @throws {UPPError} TIMEOUT - When the timeout is exceeded\n * @throws {UPPError} CANCELLED - When cancelled via user-provided signal\n * @throws {Error} Network errors are passed through unchanged\n */\nasync function fetchWithTimeout(\n fetchFn: typeof fetch,\n url: string,\n init: RequestInit,\n timeout: number,\n provider: string,\n modality: Modality\n): Promise<Response> {\n const existingSignal = init.signal;\n\n // Check if already aborted before starting\n if (existingSignal?.aborted) {\n throw cancelledError(provider, modality);\n }\n\n const controller = new AbortController();\n const timeoutId = setTimeout(() => controller.abort(), timeout);\n\n const onAbort = () => controller.abort();\n if (existingSignal) {\n existingSignal.addEventListener('abort', onAbort, { once: true });\n }\n\n try {\n const response = await fetchFn(url, {\n ...init,\n signal: controller.signal,\n });\n return response;\n } catch (error) {\n if (toError(error).name === 'AbortError') {\n if (existingSignal?.aborted) {\n throw cancelledError(provider, modality);\n }\n throw timeoutError(timeout, provider, modality);\n }\n throw error;\n } finally {\n clearTimeout(timeoutId);\n if (existingSignal) {\n existingSignal.removeEventListener('abort', onAbort);\n }\n }\n}\n\n/**\n * Delays execution for a specified duration.\n *\n * @param ms - Duration to sleep in milliseconds\n * @returns Promise that resolves after the specified delay\n */\nfunction sleep(ms: number): Promise<void> {\n return new Promise((resolve) => setTimeout(resolve, ms));\n}\n\n/**\n * Executes an HTTP fetch request for streaming responses.\n *\n * Unlike {@link doFetch}, this function returns the response immediately without\n * checking the HTTP status. This is necessary for Server-Sent Events (SSE) and\n * other streaming protocols where error information may be embedded in the stream.\n *\n * The caller is responsible for:\n * - Checking response.ok and handling HTTP errors\n * - Parsing the response stream (e.g., using parseSSEStream)\n * - Handling stream-specific error conditions\n *\n * Retries are not performed for streaming requests since partial data may have\n * already been consumed by the caller.\n *\n * @param url - The URL to fetch\n * @param init - Standard fetch RequestInit options\n * @param config - Provider configuration containing fetch customization and timeout\n * @param provider - Provider identifier for error context\n * @param modality - Request modality for error context\n * @returns The Response object (may have non-2xx status)\n *\n * @throws {UPPError} NETWORK_ERROR - When a network failure occurs\n * @throws {UPPError} TIMEOUT - When the request times out\n * @throws {UPPError} CANCELLED - When the request is aborted via signal\n *\n * @example\n * ```typescript\n * const response = await doStreamFetch(\n * 'https://api.openai.com/v1/chat/completions',\n * {\n * method: 'POST',\n * headers: { 'Authorization': 'Bearer sk-...' },\n * body: JSON.stringify({ model: 'gpt-4', messages: [], stream: true })\n * },\n * { timeout: 120000 },\n * 'openai',\n * 'llm'\n * );\n *\n * if (!response.ok) {\n * throw await normalizeHttpError(response, 'openai', 'llm');\n * }\n *\n * for await (const event of parseSSEStream(response.body!)) {\n * console.log(event);\n * }\n * ```\n */\nexport async function doStreamFetch(\n url: string,\n init: RequestInit,\n config: ProviderConfig,\n provider: string,\n modality: Modality\n): Promise<Response> {\n const fetchFn = config.fetch ?? fetch;\n const timeout = config.timeout ?? DEFAULT_TIMEOUT;\n const baseStrategy = config.retryStrategy;\n const strategy = hasFork(baseStrategy) ? baseStrategy.fork() : baseStrategy;\n\n if (strategy?.beforeRequest) {\n const delay = await strategy.beforeRequest();\n if (delay > 0) {\n await sleep(delay);\n }\n }\n\n try {\n const response = await fetchWithTimeout(\n fetchFn,\n url,\n init,\n timeout,\n provider,\n modality\n );\n return response;\n } catch (error) {\n if (error instanceof UPPError) {\n throw error;\n }\n throw networkError(toError(error), provider, modality);\n }\n}\n\n/**\n * Parses Retry-After header values into seconds.\n *\n * Supports both delta-seconds and HTTP-date formats.\n */\nfunction parseRetryAfter(headerValue: string | null, maxSeconds: number): number | null {\n if (!headerValue) {\n return null;\n }\n\n const seconds = parseInt(headerValue, 10);\n if (!Number.isNaN(seconds)) {\n return Math.min(maxSeconds, Math.max(0, seconds));\n }\n\n const dateMillis = Date.parse(headerValue);\n if (Number.isNaN(dateMillis)) {\n return null;\n }\n\n const deltaMs = dateMillis - Date.now();\n if (deltaMs <= 0) {\n return 0;\n }\n\n const deltaSeconds = Math.ceil(deltaMs / 1000);\n return Math.min(maxSeconds, Math.max(0, deltaSeconds));\n}\n"],"mappings":"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","names":[]}
@@ -1,3 +1,7 @@
1
+ import {
2
+ ErrorCode
3
+ } from "./chunk-QNJO7DSD.js";
4
+
1
5
  // src/http/retry.ts
2
6
  var ExponentialBackoff = class {
3
7
  maxAttempts;
@@ -47,7 +51,7 @@ var ExponentialBackoff = class {
47
51
  * @returns True if the error is transient and retryable
48
52
  */
49
53
  isRetryable(error) {
50
- return error.code === "RATE_LIMITED" || error.code === "NETWORK_ERROR" || error.code === "TIMEOUT" || error.code === "PROVIDER_ERROR";
54
+ return error.code === ErrorCode.RateLimited || error.code === ErrorCode.NetworkError || error.code === ErrorCode.Timeout || error.code === ErrorCode.ProviderError;
51
55
  }
52
56
  };
53
57
  var LinearBackoff = class {
@@ -87,7 +91,7 @@ var LinearBackoff = class {
87
91
  * @returns True if the error is transient and retryable
88
92
  */
89
93
  isRetryable(error) {
90
- return error.code === "RATE_LIMITED" || error.code === "NETWORK_ERROR" || error.code === "TIMEOUT" || error.code === "PROVIDER_ERROR";
94
+ return error.code === ErrorCode.RateLimited || error.code === ErrorCode.NetworkError || error.code === ErrorCode.Timeout || error.code === ErrorCode.ProviderError;
91
95
  }
92
96
  };
93
97
  var NoRetry = class {
@@ -106,6 +110,7 @@ var TokenBucket = class {
106
110
  refillRate;
107
111
  lastRefill;
108
112
  maxAttempts;
113
+ lock;
109
114
  /**
110
115
  * Creates a new TokenBucket instance.
111
116
  *
@@ -120,6 +125,7 @@ var TokenBucket = class {
120
125
  this.maxAttempts = options.maxAttempts ?? 3;
121
126
  this.tokens = this.maxTokens;
122
127
  this.lastRefill = Date.now();
128
+ this.lock = Promise.resolve();
123
129
  }
124
130
  /**
125
131
  * Called before each request to consume a token or calculate wait time.
@@ -128,16 +134,23 @@ var TokenBucket = class {
128
134
  * - Returns 0 if a token is available (consumed immediately)
129
135
  * - Returns the wait time in milliseconds until the next token
130
136
  *
137
+ * This method may allow tokens to go negative to reserve future capacity
138
+ * and avoid concurrent callers oversubscribing the same refill.
139
+ *
131
140
  * @returns Delay in milliseconds before the request can proceed
132
141
  */
133
142
  beforeRequest() {
134
- this.refill();
135
- if (this.tokens >= 1) {
143
+ return this.withLock(() => {
144
+ this.refill();
145
+ if (this.tokens >= 1) {
146
+ this.tokens -= 1;
147
+ return 0;
148
+ }
149
+ const deficit = 1 - this.tokens;
150
+ const msPerToken = 1e3 / this.refillRate;
136
151
  this.tokens -= 1;
137
- return 0;
138
- }
139
- const msPerToken = 1e3 / this.refillRate;
140
- return Math.ceil(msPerToken);
152
+ return Math.ceil(deficit * msPerToken);
153
+ });
141
154
  }
142
155
  /**
143
156
  * Handles retry logic for rate-limited requests.
@@ -152,7 +165,7 @@ var TokenBucket = class {
152
165
  if (attempt > this.maxAttempts) {
153
166
  return null;
154
167
  }
155
- if (error.code !== "RATE_LIMITED") {
168
+ if (error.code !== ErrorCode.RateLimited) {
156
169
  return null;
157
170
  }
158
171
  const msPerToken = 1e3 / this.refillRate;
@@ -164,8 +177,10 @@ var TokenBucket = class {
164
177
  * Called automatically on successful requests to restore available tokens.
165
178
  */
166
179
  reset() {
167
- this.tokens = this.maxTokens;
168
- this.lastRefill = Date.now();
180
+ void this.withLock(() => {
181
+ this.tokens = this.maxTokens;
182
+ this.lastRefill = Date.now();
183
+ });
169
184
  }
170
185
  /**
171
186
  * Refills the bucket based on elapsed time since last refill.
@@ -177,8 +192,13 @@ var TokenBucket = class {
177
192
  this.tokens = Math.min(this.maxTokens, this.tokens + newTokens);
178
193
  this.lastRefill = now;
179
194
  }
195
+ async withLock(fn) {
196
+ const next = this.lock.then(fn, fn);
197
+ this.lock = next.then(() => void 0, () => void 0);
198
+ return next;
199
+ }
180
200
  };
181
- var RetryAfterStrategy = class {
201
+ var RetryAfterStrategy = class _RetryAfterStrategy {
182
202
  maxAttempts;
183
203
  fallbackDelay;
184
204
  lastRetryAfter;
@@ -193,6 +213,15 @@ var RetryAfterStrategy = class {
193
213
  this.maxAttempts = options.maxAttempts ?? 3;
194
214
  this.fallbackDelay = options.fallbackDelay ?? 5e3;
195
215
  }
216
+ /**
217
+ * Creates a request-scoped copy of this strategy.
218
+ */
219
+ fork() {
220
+ return new _RetryAfterStrategy({
221
+ maxAttempts: this.maxAttempts,
222
+ fallbackDelay: this.fallbackDelay
223
+ });
224
+ }
196
225
  /**
197
226
  * Sets the retry delay from a Retry-After header value.
198
227
  *
@@ -215,7 +244,7 @@ var RetryAfterStrategy = class {
215
244
  if (attempt > this.maxAttempts) {
216
245
  return null;
217
246
  }
218
- if (error.code !== "RATE_LIMITED") {
247
+ if (error.code !== ErrorCode.RateLimited) {
219
248
  return null;
220
249
  }
221
250
  const delay = this.lastRetryAfter ?? this.fallbackDelay;
@@ -231,4 +260,4 @@ export {
231
260
  TokenBucket,
232
261
  RetryAfterStrategy
233
262
  };
234
- //# sourceMappingURL=chunk-Z4ILICF5.js.map
263
+ //# sourceMappingURL=chunk-SBCATNHA.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/http/retry.ts"],"sourcesContent":["/**\n * Retry strategies for handling transient failures in HTTP requests.\n * @module http/retry\n */\n\nimport type { RetryStrategy } from '../types/provider.ts';\nimport { ErrorCode, type UPPError } from '../types/errors.ts';\n\n/**\n * Implements exponential backoff with optional jitter for retry delays.\n *\n * The delay between retries doubles with each attempt, helping to:\n * - Avoid overwhelming servers during outages\n * - Reduce thundering herd effects when many clients retry simultaneously\n * - Give transient issues time to resolve\n *\n * Delay formula: min(baseDelay * 2^(attempt-1), maxDelay)\n * With jitter: delay * random(0.5, 1.0)\n *\n * Only retries on transient errors: RATE_LIMITED, NETWORK_ERROR, TIMEOUT, PROVIDER_ERROR\n *\n * @implements {RetryStrategy}\n *\n * @example\n * ```typescript\n * // Default configuration (3 retries, 1s base, 30s max, jitter enabled)\n * const retry = new ExponentialBackoff();\n *\n * // Custom configuration\n * const customRetry = new ExponentialBackoff({\n * maxAttempts: 5, // Up to 5 retry attempts\n * baseDelay: 500, // Start with 500ms delay\n * maxDelay: 60000, // Cap at 60 seconds\n * jitter: false // Disable random jitter\n * });\n *\n * // Use with provider\n * const provider = createOpenAI({\n * retryStrategy: customRetry\n * });\n * ```\n */\nexport class ExponentialBackoff implements RetryStrategy {\n private maxAttempts: number;\n private baseDelay: number;\n private maxDelay: number;\n private jitter: boolean;\n\n /**\n * Creates a new ExponentialBackoff instance.\n *\n * @param options - Configuration options\n * @param options.maxAttempts - Maximum number of retry attempts (default: 3)\n * @param options.baseDelay - Initial delay in milliseconds (default: 1000)\n * @param options.maxDelay - Maximum delay cap in milliseconds (default: 30000)\n * @param options.jitter - Whether to add random jitter to delays (default: true)\n */\n constructor(options: {\n maxAttempts?: number;\n baseDelay?: number;\n maxDelay?: number;\n jitter?: boolean;\n } = {}) {\n this.maxAttempts = options.maxAttempts ?? 3;\n this.baseDelay = options.baseDelay ?? 1000;\n this.maxDelay = options.maxDelay ?? 30000;\n this.jitter = options.jitter ?? true;\n }\n\n /**\n * Determines whether to retry and calculates the delay.\n *\n * @param error - The error that triggered the retry\n * @param attempt - Current attempt number (1-indexed)\n * @returns Delay in milliseconds before next retry, or null to stop retrying\n */\n onRetry(error: UPPError, attempt: number): number | null {\n if (attempt > this.maxAttempts) {\n return null;\n }\n\n if (!this.isRetryable(error)) {\n return null;\n }\n\n let delay = this.baseDelay * Math.pow(2, attempt - 1);\n delay = Math.min(delay, this.maxDelay);\n\n if (this.jitter) {\n delay = delay * (0.5 + Math.random());\n }\n\n return Math.floor(delay);\n }\n\n /**\n * Checks if an error is eligible for retry.\n *\n * @param error - The error to evaluate\n * @returns True if the error is transient and retryable\n */\n private isRetryable(error: UPPError): boolean {\n return (\n error.code === ErrorCode.RateLimited ||\n error.code === ErrorCode.NetworkError ||\n error.code === ErrorCode.Timeout ||\n error.code === ErrorCode.ProviderError\n );\n }\n}\n\n/**\n * Implements linear backoff where delays increase proportionally with each attempt.\n *\n * Unlike exponential backoff, linear backoff increases delays at a constant rate:\n * - Attempt 1: delay * 1 (e.g., 1000ms)\n * - Attempt 2: delay * 2 (e.g., 2000ms)\n * - Attempt 3: delay * 3 (e.g., 3000ms)\n *\n * This strategy is simpler and more predictable than exponential backoff,\n * suitable for scenarios where gradual delay increase is preferred over\n * aggressive backoff.\n *\n * Only retries on transient errors: RATE_LIMITED, NETWORK_ERROR, TIMEOUT, PROVIDER_ERROR\n *\n * @implements {RetryStrategy}\n *\n * @example\n * ```typescript\n * // Default configuration (3 retries, 1s delay increment)\n * const retry = new LinearBackoff();\n *\n * // Custom configuration\n * const customRetry = new LinearBackoff({\n * maxAttempts: 4, // Up to 4 retry attempts\n * delay: 2000 // 2s, 4s, 6s, 8s delays\n * });\n *\n * // Use with provider\n * const provider = createAnthropic({\n * retryStrategy: customRetry\n * });\n * ```\n */\nexport class LinearBackoff implements RetryStrategy {\n private maxAttempts: number;\n private delay: number;\n\n /**\n * Creates a new LinearBackoff instance.\n *\n * @param options - Configuration options\n * @param options.maxAttempts - Maximum number of retry attempts (default: 3)\n * @param options.delay - Base delay multiplier in milliseconds (default: 1000)\n */\n constructor(options: {\n maxAttempts?: number;\n delay?: number;\n } = {}) {\n this.maxAttempts = options.maxAttempts ?? 3;\n this.delay = options.delay ?? 1000;\n }\n\n /**\n * Determines whether to retry and calculates the linear delay.\n *\n * @param error - The error that triggered the retry\n * @param attempt - Current attempt number (1-indexed)\n * @returns Delay in milliseconds (delay * attempt), or null to stop retrying\n */\n onRetry(error: UPPError, attempt: number): number | null {\n if (attempt > this.maxAttempts) {\n return null;\n }\n\n if (!this.isRetryable(error)) {\n return null;\n }\n\n return this.delay * attempt;\n }\n\n /**\n * Checks if an error is eligible for retry.\n *\n * @param error - The error to evaluate\n * @returns True if the error is transient and retryable\n */\n private isRetryable(error: UPPError): boolean {\n return (\n error.code === ErrorCode.RateLimited ||\n error.code === ErrorCode.NetworkError ||\n error.code === ErrorCode.Timeout ||\n error.code === ErrorCode.ProviderError\n );\n }\n}\n\n/**\n * Disables all retry behavior, failing immediately on any error.\n *\n * Use this strategy when:\n * - Retries are handled at a higher level in your application\n * - You want immediate failure feedback\n * - The operation is not idempotent\n * - Time sensitivity requires fast failure\n *\n * @implements {RetryStrategy}\n *\n * @example\n * ```typescript\n * // Disable retries for time-sensitive operations\n * const provider = createOpenAI({\n * retryStrategy: new NoRetry()\n * });\n * ```\n */\nexport class NoRetry implements RetryStrategy {\n /**\n * Always returns null to indicate no retry should be attempted.\n *\n * @returns Always returns null\n */\n onRetry(_error: UPPError, _attempt: number): null {\n return null;\n }\n}\n\n/**\n * Implements token bucket rate limiting with automatic refill.\n *\n * The token bucket algorithm provides smooth rate limiting by:\n * - Maintaining a bucket of tokens that replenish over time\n * - Consuming one token per request\n * - Delaying requests when the bucket is empty\n * - Allowing burst traffic up to the bucket capacity\n *\n * This is particularly useful for:\n * - Client-side rate limiting to avoid hitting API rate limits\n * - Smoothing request patterns to maintain consistent throughput\n * - Preventing accidental API abuse\n *\n * Unlike other retry strategies, TokenBucket implements {@link beforeRequest}\n * to proactively delay requests before they are made.\n *\n * @implements {RetryStrategy}\n *\n * @example\n * ```typescript\n * // Allow 10 requests burst, refill 1 token per second\n * const bucket = new TokenBucket({\n * maxTokens: 10, // Burst capacity\n * refillRate: 1, // Tokens per second\n * maxAttempts: 3 // Retry attempts on rate limit\n * });\n *\n * // Aggressive rate limiting: 5 req/s sustained\n * const strictBucket = new TokenBucket({\n * maxTokens: 5,\n * refillRate: 5\n * });\n *\n * // Use with provider\n * const provider = createOpenAI({\n * retryStrategy: bucket\n * });\n * ```\n */\nexport class TokenBucket implements RetryStrategy {\n private tokens: number;\n private maxTokens: number;\n private refillRate: number;\n private lastRefill: number;\n private maxAttempts: number;\n private lock: Promise<void>;\n\n /**\n * Creates a new TokenBucket instance.\n *\n * @param options - Configuration options\n * @param options.maxTokens - Maximum bucket capacity (default: 10)\n * @param options.refillRate - Tokens added per second (default: 1)\n * @param options.maxAttempts - Maximum retry attempts on rate limit (default: 3)\n */\n constructor(options: {\n maxTokens?: number;\n refillRate?: number;\n maxAttempts?: number;\n } = {}) {\n this.maxTokens = options.maxTokens ?? 10;\n this.refillRate = options.refillRate ?? 1;\n this.maxAttempts = options.maxAttempts ?? 3;\n this.tokens = this.maxTokens;\n this.lastRefill = Date.now();\n this.lock = Promise.resolve();\n }\n\n /**\n * Called before each request to consume a token or calculate wait time.\n *\n * Refills the bucket based on elapsed time, then either:\n * - Returns 0 if a token is available (consumed immediately)\n * - Returns the wait time in milliseconds until the next token\n *\n * This method may allow tokens to go negative to reserve future capacity\n * and avoid concurrent callers oversubscribing the same refill.\n *\n * @returns Delay in milliseconds before the request can proceed\n */\n beforeRequest(): Promise<number> {\n return this.withLock(() => {\n this.refill();\n\n if (this.tokens >= 1) {\n this.tokens -= 1;\n return 0;\n }\n\n const deficit = 1 - this.tokens;\n const msPerToken = 1000 / this.refillRate;\n this.tokens -= 1;\n return Math.ceil(deficit * msPerToken);\n });\n }\n\n /**\n * Handles retry logic for rate-limited requests.\n *\n * Only retries on RATE_LIMITED errors, waiting for bucket refill.\n *\n * @param error - The error that triggered the retry\n * @param attempt - Current attempt number (1-indexed)\n * @returns Delay in milliseconds (time for 2 tokens), or null to stop\n */\n onRetry(error: UPPError, attempt: number): number | null {\n if (attempt > this.maxAttempts) {\n return null;\n }\n\n if (error.code !== ErrorCode.RateLimited) {\n return null;\n }\n\n const msPerToken = 1000 / this.refillRate;\n return Math.ceil(msPerToken * 2);\n }\n\n /**\n * Resets the bucket to full capacity.\n *\n * Called automatically on successful requests to restore available tokens.\n */\n reset(): void {\n void this.withLock(() => {\n this.tokens = this.maxTokens;\n this.lastRefill = Date.now();\n });\n }\n\n /**\n * Refills the bucket based on elapsed time since last refill.\n */\n private refill(): void {\n const now = Date.now();\n const elapsed = (now - this.lastRefill) / 1000;\n const newTokens = elapsed * this.refillRate;\n\n this.tokens = Math.min(this.maxTokens, this.tokens + newTokens);\n this.lastRefill = now;\n }\n\n private async withLock<T>(fn: () => T | Promise<T>): Promise<T> {\n const next = this.lock.then(fn, fn);\n this.lock = next.then(() => undefined, () => undefined);\n return next;\n }\n}\n\n/**\n * Respects server-provided Retry-After headers for optimal retry timing.\n *\n * When servers return a 429 (Too Many Requests) response, they often include\n * a Retry-After header indicating when the client should retry. This strategy\n * uses that information for precise retry timing.\n *\n * Benefits over fixed backoff strategies:\n * - Follows server recommendations for optimal retry timing\n * - Avoids retrying too early and wasting requests\n * - Adapts to dynamic rate limit windows\n *\n * If no Retry-After header is provided, falls back to a configurable delay.\n * Only retries on RATE_LIMITED errors.\n *\n * @implements {RetryStrategy}\n *\n * @example\n * ```typescript\n * // Use server-recommended retry timing\n * const retryAfter = new RetryAfterStrategy({\n * maxAttempts: 5, // Retry up to 5 times\n * fallbackDelay: 10000 // 10s fallback if no header\n * });\n *\n * // The doFetch function automatically calls setRetryAfter\n * // when a Retry-After header is present in the response\n *\n * const provider = createOpenAI({\n * retryStrategy: retryAfter\n * });\n * ```\n */\nexport class RetryAfterStrategy implements RetryStrategy {\n private maxAttempts: number;\n private fallbackDelay: number;\n private lastRetryAfter?: number;\n\n /**\n * Creates a new RetryAfterStrategy instance.\n *\n * @param options - Configuration options\n * @param options.maxAttempts - Maximum number of retry attempts (default: 3)\n * @param options.fallbackDelay - Delay in ms when no Retry-After header (default: 5000)\n */\n constructor(options: {\n maxAttempts?: number;\n fallbackDelay?: number;\n } = {}) {\n this.maxAttempts = options.maxAttempts ?? 3;\n this.fallbackDelay = options.fallbackDelay ?? 5000;\n }\n\n /**\n * Creates a request-scoped copy of this strategy.\n */\n fork(): RetryAfterStrategy {\n return new RetryAfterStrategy({\n maxAttempts: this.maxAttempts,\n fallbackDelay: this.fallbackDelay,\n });\n }\n\n /**\n * Sets the retry delay from a Retry-After header value.\n *\n * Called by doFetch when a Retry-After header is present in the response.\n * The value is used for the next onRetry call and then cleared.\n *\n * @param seconds - The Retry-After value in seconds\n */\n setRetryAfter(seconds: number): void {\n this.lastRetryAfter = seconds * 1000;\n }\n\n /**\n * Determines retry delay using Retry-After header or fallback.\n *\n * @param error - The error that triggered the retry\n * @param attempt - Current attempt number (1-indexed)\n * @returns Delay from Retry-After header or fallback, null to stop\n */\n onRetry(error: UPPError, attempt: number): number | null {\n if (attempt > this.maxAttempts) {\n return null;\n }\n\n if (error.code !== ErrorCode.RateLimited) {\n return null;\n }\n\n const delay = this.lastRetryAfter ?? this.fallbackDelay;\n this.lastRetryAfter = undefined;\n return delay;\n }\n}\n"],"mappings":"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","names":[]}
@@ -0,0 +1,50 @@
1
+ import {
2
+ ErrorCode,
3
+ UPPError,
4
+ toError
5
+ } from "./chunk-QNJO7DSD.js";
6
+
7
+ // src/http/json.ts
8
+ async function parseJsonResponse(response, provider, modality) {
9
+ let bodyText;
10
+ try {
11
+ bodyText = await response.text();
12
+ } catch (error) {
13
+ const cause = toError(error);
14
+ throw new UPPError(
15
+ "Failed to read response body",
16
+ ErrorCode.InvalidResponse,
17
+ provider,
18
+ modality,
19
+ response.status,
20
+ cause
21
+ );
22
+ }
23
+ if (!bodyText) {
24
+ throw new UPPError(
25
+ "Empty response body",
26
+ ErrorCode.InvalidResponse,
27
+ provider,
28
+ modality,
29
+ response.status
30
+ );
31
+ }
32
+ try {
33
+ return JSON.parse(bodyText);
34
+ } catch (error) {
35
+ const cause = toError(error);
36
+ throw new UPPError(
37
+ "Failed to parse JSON response",
38
+ ErrorCode.InvalidResponse,
39
+ provider,
40
+ modality,
41
+ response.status,
42
+ cause
43
+ );
44
+ }
45
+ }
46
+
47
+ export {
48
+ parseJsonResponse
49
+ };
50
+ //# sourceMappingURL=chunk-Z6DKC37J.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/http/json.ts"],"sourcesContent":["/**\n * @fileoverview JSON response parsing utilities.\n *\n * @module http/json\n */\n\nimport { ErrorCode, UPPError, type Modality } from '../types/errors.ts';\nimport { toError } from '../utils/error.ts';\n\n/**\n * Parses a JSON response body with normalized error handling.\n *\n * @typeParam T - Expected JSON shape\n * @param response - Fetch response to parse\n * @param provider - Provider identifier for error context\n * @param modality - Modality for error context\n * @returns Parsed JSON object\n * @throws {UPPError} INVALID_RESPONSE when JSON parsing fails or body is empty\n */\nexport async function parseJsonResponse<T>(\n response: Response,\n provider: string,\n modality: Modality\n): Promise<T> {\n let bodyText: string;\n try {\n bodyText = await response.text();\n } catch (error) {\n const cause = toError(error);\n throw new UPPError(\n 'Failed to read response body',\n ErrorCode.InvalidResponse,\n provider,\n modality,\n response.status,\n cause\n );\n }\n\n if (!bodyText) {\n throw new UPPError(\n 'Empty response body',\n ErrorCode.InvalidResponse,\n provider,\n modality,\n response.status\n );\n }\n\n try {\n return JSON.parse(bodyText) as T;\n } catch (error) {\n const cause = toError(error);\n throw new UPPError(\n 'Failed to parse JSON response',\n ErrorCode.InvalidResponse,\n provider,\n modality,\n response.status,\n cause\n );\n }\n}\n"],"mappings":";;;;;;;AAmBA,eAAsB,kBACpB,UACA,UACA,UACY;AACZ,MAAI;AACJ,MAAI;AACF,eAAW,MAAM,SAAS,KAAK;AAAA,EACjC,SAAS,OAAO;AACd,UAAM,QAAQ,QAAQ,KAAK;AAC3B,UAAM,IAAI;AAAA,MACR;AAAA,MACA,UAAU;AAAA,MACV;AAAA,MACA;AAAA,MACA,SAAS;AAAA,MACT;AAAA,IACF;AAAA,EACF;AAEA,MAAI,CAAC,UAAU;AACb,UAAM,IAAI;AAAA,MACR;AAAA,MACA,UAAU;AAAA,MACV;AAAA,MACA;AAAA,MACA,SAAS;AAAA,IACX;AAAA,EACF;AAEA,MAAI;AACF,WAAO,KAAK,MAAM,QAAQ;AAAA,EAC5B,SAAS,OAAO;AACd,UAAM,QAAQ,QAAQ,KAAK;AAC3B,UAAM,IAAI;AAAA,MACR;AAAA,MACA,UAAU;AAAA,MACV;AAAA,MACA;AAAA,MACA,SAAS;AAAA,MACT;AAAA,IACF;AAAA,EACF;AACF;","names":[]}