@providerkit/core 0.7.0 → 0.8.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.
Files changed (60) hide show
  1. package/dist/errors.d.ts +6 -0
  2. package/dist/errors.d.ts.map +1 -1
  3. package/dist/errors.js +18 -0
  4. package/dist/errors.js.map +1 -1
  5. package/dist/fallback.d.ts +73 -0
  6. package/dist/fallback.d.ts.map +1 -0
  7. package/dist/fallback.js +182 -0
  8. package/dist/fallback.js.map +1 -0
  9. package/dist/index.d.ts +1 -0
  10. package/dist/index.d.ts.map +1 -1
  11. package/dist/index.js +1 -0
  12. package/dist/index.js.map +1 -1
  13. package/dist/providers/anthropic.d.ts +2 -1
  14. package/dist/providers/anthropic.d.ts.map +1 -1
  15. package/dist/providers/anthropic.js +3 -1
  16. package/dist/providers/anthropic.js.map +1 -1
  17. package/dist/providers/factory.d.ts +7 -1
  18. package/dist/providers/factory.d.ts.map +1 -1
  19. package/dist/providers/factory.js +18 -5
  20. package/dist/providers/factory.js.map +1 -1
  21. package/dist/providers/gemini.d.ts +2 -1
  22. package/dist/providers/gemini.d.ts.map +1 -1
  23. package/dist/providers/gemini.js +3 -1
  24. package/dist/providers/gemini.js.map +1 -1
  25. package/dist/providers/openai.d.ts +2 -1
  26. package/dist/providers/openai.d.ts.map +1 -1
  27. package/dist/providers/openai.js +3 -1
  28. package/dist/providers/openai.js.map +1 -1
  29. package/dist/providers/responses.d.ts +2 -1
  30. package/dist/providers/responses.d.ts.map +1 -1
  31. package/dist/providers/responses.js +3 -1
  32. package/dist/providers/responses.js.map +1 -1
  33. package/dist/rate-limit.d.ts +3 -0
  34. package/dist/rate-limit.d.ts.map +1 -1
  35. package/dist/rate-limit.js +16 -0
  36. package/dist/rate-limit.js.map +1 -1
  37. package/dist/retry.d.ts +2 -0
  38. package/dist/retry.d.ts.map +1 -1
  39. package/dist/retry.js +13 -5
  40. package/dist/retry.js.map +1 -1
  41. package/dist/transport.d.ts.map +1 -1
  42. package/dist/transport.js +5 -2
  43. package/dist/transport.js.map +1 -1
  44. package/dist/types.d.ts +8 -0
  45. package/dist/types.d.ts.map +1 -1
  46. package/dist/types.js +5 -1
  47. package/dist/types.js.map +1 -1
  48. package/package.json +1 -1
  49. package/src/errors.ts +19 -0
  50. package/src/fallback.ts +228 -0
  51. package/src/index.ts +1 -0
  52. package/src/providers/anthropic.ts +4 -2
  53. package/src/providers/factory.ts +31 -6
  54. package/src/providers/gemini.ts +5 -2
  55. package/src/providers/openai.ts +5 -2
  56. package/src/providers/responses.ts +5 -2
  57. package/src/rate-limit.ts +22 -0
  58. package/src/retry.ts +14 -5
  59. package/src/transport.ts +5 -2
  60. package/src/types.ts +9 -1
package/src/retry.ts CHANGED
@@ -13,12 +13,14 @@
13
13
  //
14
14
  // 3. Honour the provider's own number. When it says `Retry-After: 30`, a
15
15
  // one-second backoff is three wasted attempts before the same wait.
16
- import { classify, isBackupEligible, isTransient, parseRetryAfterMs } from "./errors.ts";
16
+ import { classify, isBackupEligible, isTransient, parseRetryAfterMs, ProviderError } from "./errors.ts";
17
17
 
18
18
  export interface RetryOptions {
19
19
  /** Total attempts including the first. Default 3. */
20
20
  maxAttempts?: number;
21
21
  baseDelayMs?: number;
22
+ /** Maximum acceptable wait. A longer server deadline ends retry so an outer
23
+ * fallback can switch provider; it is never shortened to retry too early. */
22
24
  maxDelayMs?: number;
23
25
  /** Aborts the wait as well as the work, so Stop lands promptly. */
24
26
  signal?: AbortSignal;
@@ -67,12 +69,17 @@ export function sleep(ms: number, signal?: AbortSignal): Promise<void> {
67
69
  });
68
70
  }
69
71
 
70
- /** The delay before the next attempt: the provider's own figure when it gave
71
- * one, capped, else full-jitter backoff. */
72
- function delayFor(err: unknown, attempt: number, opts: RetryOptions): number {
72
+ /** null means the server deadline exceeds this call's wait budget. */
73
+ function delayFor(err: unknown, attempt: number, opts: RetryOptions): number | null {
73
74
  const asked = parseRetryAfterMs(err);
74
75
  const cap = opts.maxDelayMs ?? DEFAULT_MAX_DELAY_MS;
75
- if (asked !== undefined) return Math.min(asked, cap);
76
+ const resetWait = err instanceof ProviderError && err.resetAtMs !== undefined
77
+ ? Math.max(0, err.resetAtMs - Date.now())
78
+ : undefined;
79
+ if (asked !== undefined || resetWait !== undefined) {
80
+ const wait = Math.max(asked ?? 0, resetWait ?? 0);
81
+ return wait > cap ? null : wait;
82
+ }
76
83
  return backoffMs(attempt, opts.baseDelayMs ?? DEFAULT_BASE_DELAY_MS, cap);
77
84
  }
78
85
 
@@ -101,6 +108,7 @@ export async function withRetry<T>(
101
108
  if (opts.signal?.aborted) throw err;
102
109
  if (attempt >= maxAttempts || !shouldRetry(err, attempt)) throw err;
103
110
  const delayMs = delayFor(err, attempt, opts);
111
+ if (delayMs === null) throw err;
104
112
  opts.onRetry?.({ error: err, attempt, delayMs });
105
113
  await nap(delayMs, opts.signal);
106
114
  }
@@ -149,6 +157,7 @@ export async function* withStreamRetry<T>(
149
157
  // Rule 2: past the first chunk there is no going back.
150
158
  if (emitted || attempt >= maxAttempts || !shouldRetry(err, attempt)) throw err;
151
159
  const delayMs = delayFor(err, attempt, opts);
160
+ if (delayMs === null) throw err;
152
161
  opts.onRetry?.({ error: err, attempt, delayMs });
153
162
  await nap(delayMs, opts.signal);
154
163
  } finally {
package/src/transport.ts CHANGED
@@ -4,7 +4,8 @@
4
4
  //
5
5
  // Adapters keep only their per-event mapping; everything about being an HTTP
6
6
  // client lives here once.
7
- import { classifyHttp, isTransportFailure, ProviderError } from "./errors.ts";
7
+ import { classifyHttp, isTransportFailure, parseRetryAfterMs, ProviderError } from "./errors.ts";
8
+ import { parseRateLimitResponse } from "./rate-limit.ts";
8
9
 
9
10
  export interface RequestInit_ {
10
11
  url: string;
@@ -63,12 +64,14 @@ export function retryAfterFromHeaders(headers: Headers, now = Date.now()): numbe
63
64
  async function errorFor(provider: string, res: Response): Promise<ProviderError> {
64
65
  const text = await res.text().catch(() => "");
65
66
  const kind = classifyHttp(res.status, text);
67
+ const reset = parseRateLimitResponse(res.headers, text);
66
68
  const message = text
67
69
  ? `${provider} ${res.status}: ${text.slice(0, 500)}`
68
70
  : `${provider} ${res.status} ${res.statusText}`;
69
71
  return new ProviderError(provider, kind, message, {
70
72
  status: res.status,
71
- retryAfterMs: retryAfterFromHeaders(res.headers),
73
+ ...reset,
74
+ retryAfterMs: retryAfterFromHeaders(res.headers) ?? parseRetryAfterMs({}, text),
72
75
  body: text.slice(0, 2_000) || undefined,
73
76
  });
74
77
  }
package/src/types.ts CHANGED
@@ -139,6 +139,9 @@ export const EMPTY_USAGE: TokenUsage = {
139
139
  /** Normalized streaming chunk — every provider's shape collapses into this. */
140
140
  export interface ProviderChunk {
141
141
  type: "delta" | "usage" | "finish";
142
+ /** The selected endpoint and model when a fallback provider emits this chunk.
143
+ * Per-call metadata, never shared mutable state on the provider object. */
144
+ source?: { provider: string; model: string };
142
145
  content?: string;
143
146
  reasoning?: string;
144
147
  /** OpenRouter's normalized reasoning payload — hand it back on the next
@@ -230,6 +233,8 @@ export interface Provider {
230
233
 
231
234
  export interface Completion {
232
235
  text: string;
236
+ /** Present when the stream identifies the endpoint that answered. */
237
+ provider?: string;
233
238
  reasoning: string;
234
239
  /** Present only where the provider sent one. See ChatMessage.reasoningDetails. */
235
240
  reasoningDetails?: unknown[];
@@ -253,7 +258,9 @@ export async function drainStream(
253
258
  let reasoningDetails: unknown[] | undefined;
254
259
  let usage: TokenUsage = EMPTY_USAGE;
255
260
  let finishReason: FinishReason | null = null;
261
+ let source: ProviderChunk["source"];
256
262
  for await (const chunk of stream) {
263
+ if (chunk.source) source = chunk.source;
257
264
  if (chunk.type === "delta") {
258
265
  if (chunk.content) text += chunk.content;
259
266
  if (chunk.reasoning) reasoning += chunk.reasoning;
@@ -270,7 +277,8 @@ export async function drainStream(
270
277
  ...(reasoningDetails ? { reasoningDetails } : {}),
271
278
  usage,
272
279
  finishReason,
273
- model,
280
+ model: source?.model ?? model,
281
+ ...(source ? { provider: source.provider } : {}),
274
282
  };
275
283
  }
276
284