@providerkit/core 0.7.1 → 0.8.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 (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 +86 -0
  6. package/dist/fallback.d.ts.map +1 -0
  7. package/dist/fallback.js +190 -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 +2 -1
  18. package/dist/providers/factory.d.ts.map +1 -1
  19. package/dist/providers/factory.js +16 -4
  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 +254 -0
  51. package/src/index.ts +1 -0
  52. package/src/providers/anthropic.ts +4 -2
  53. package/src/providers/factory.ts +19 -5
  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/dist/types.d.ts CHANGED
@@ -112,6 +112,12 @@ export declare const EMPTY_USAGE: TokenUsage;
112
112
  /** Normalized streaming chunk — every provider's shape collapses into this. */
113
113
  export interface ProviderChunk {
114
114
  type: "delta" | "usage" | "finish";
115
+ /** The selected endpoint and model when a fallback provider emits this chunk.
116
+ * Per-call metadata, never shared mutable state on the provider object. */
117
+ source?: {
118
+ provider: string;
119
+ model: string;
120
+ };
115
121
  content?: string;
116
122
  reasoning?: string;
117
123
  /** OpenRouter's normalized reasoning payload — hand it back on the next
@@ -195,6 +201,8 @@ export interface Provider {
195
201
  }
196
202
  export interface Completion {
197
203
  text: string;
204
+ /** Present when the stream identifies the endpoint that answered. */
205
+ provider?: string;
198
206
  reasoning: string;
199
207
  /** Present only where the provider sent one. See ChatMessage.reasoningDetails. */
200
208
  reasoningDetails?: unknown[];
@@ -1 +1 @@
1
- {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"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"}
package/dist/types.js CHANGED
@@ -37,7 +37,10 @@ export async function drainStream(stream, model) {
37
37
  let reasoningDetails;
38
38
  let usage = EMPTY_USAGE;
39
39
  let finishReason = null;
40
+ let source;
40
41
  for await (const chunk of stream) {
42
+ if (chunk.source)
43
+ source = chunk.source;
41
44
  if (chunk.type === "delta") {
42
45
  if (chunk.content)
43
46
  text += chunk.content;
@@ -59,7 +62,8 @@ export async function drainStream(stream, model) {
59
62
  ...(reasoningDetails ? { reasoningDetails } : {}),
60
63
  usage,
61
64
  finishReason,
62
- model,
65
+ model: source?.model ?? model,
66
+ ...(source ? { provider: source.provider } : {}),
63
67
  };
64
68
  }
65
69
  /**
package/dist/types.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,wEAAwE;AACxE,2EAA2E;AAC3E,mEAAmE;AACnE,EAAE;AACF,8EAA8E;AAC9E,8EAA8E;AAC9E,6EAA6E;AAC7E,0BAA0B;AAE1B;;;;;;;;GAQG;AACH,MAAM,CAAC,MAAM,OAAO,GAAG,CAAC,MAAM,EAAE,KAAK,EAAE,QAAQ,EAAE,MAAM,EAAE,KAAK,CAAU,CAAC;AAGzE,2EAA2E;AAC3E,MAAM,UAAU,QAAQ,CAAC,KAAa;IACpC,OAAO,OAAO,CAAC,IAAI,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,MAAM,KAAK,KAAK,CAAC,CAAC;AACpD,CAAC;AA4GD,MAAM,CAAC,MAAM,WAAW,GAAe;IACrC,WAAW,EAAE,CAAC;IACd,iBAAiB,EAAE,CAAC;IACpB,YAAY,EAAE,CAAC;CAChB,CAAC;AAwGF;kEACkE;AAClE,MAAM,CAAC,KAAK,UAAU,WAAW,CAC/B,MAAoC,EACpC,KAAa;IAEb,IAAI,IAAI,GAAG,EAAE,CAAC;IACd,IAAI,SAAS,GAAG,EAAE,CAAC;IACnB,4EAA4E;IAC5E,4EAA4E;IAC5E,yEAAyE;IACzE,6DAA6D;IAC7D,IAAI,gBAAuC,CAAC;IAC5C,IAAI,KAAK,GAAe,WAAW,CAAC;IACpC,IAAI,YAAY,GAAwB,IAAI,CAAC;IAC7C,IAAI,KAAK,EAAE,MAAM,KAAK,IAAI,MAAM,EAAE,CAAC;QACjC,IAAI,KAAK,CAAC,IAAI,KAAK,OAAO,EAAE,CAAC;YAC3B,IAAI,KAAK,CAAC,OAAO;gBAAE,IAAI,IAAI,KAAK,CAAC,OAAO,CAAC;YACzC,IAAI,KAAK,CAAC,SAAS;gBAAE,SAAS,IAAI,KAAK,CAAC,SAAS,CAAC;YAClD,IAAI,KAAK,CAAC,gBAAgB,EAAE,MAAM;gBAAE,gBAAgB,GAAG,KAAK,CAAC,gBAAgB,CAAC;QAChF,CAAC;aAAM,IAAI,KAAK,CAAC,IAAI,KAAK,OAAO,IAAI,KAAK,CAAC,KAAK,EAAE,CAAC;YACjD,KAAK,GAAG,KAAK,CAAC,KAAK,CAAC;QACtB,CAAC;aAAM,IAAI,KAAK,CAAC,IAAI,KAAK,QAAQ,IAAI,KAAK,CAAC,YAAY,EAAE,CAAC;YACzD,YAAY,GAAG,KAAK,CAAC,YAAY,CAAC;QACpC,CAAC;IACH,CAAC;IACD,OAAO;QACL,IAAI;QACJ,SAAS;QACT,GAAG,CAAC,gBAAgB,CAAC,CAAC,CAAC,EAAE,gBAAgB,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;QACjD,KAAK;QACL,YAAY;QACZ,KAAK;KACN,CAAC;AACJ,CAAC;AAED;;;;;;;;;;;GAWG;AACH,MAAM,UAAU,cAAc,CAAC,QAAgC;IAC7D,OAAO,QAAQ,CAAC,GAAG,CAAC,CAAC,OAAO,EAAE,EAAE;QAC9B,IAAI,OAAO,CAAC,IAAI,KAAK,WAAW;YAAE,OAAO,OAAO,CAAC;QACjD,IAAI,OAAO,CAAC,SAAS,KAAK,SAAS,IAAI,OAAO,CAAC,gBAAgB,KAAK,SAAS;YAAE,OAAO,OAAO,CAAC;QAC9F,yEAAyE;QACzE,yEAAyE;QACzE,iEAAiE;QACjE,MAAM,EAAE,SAAS,EAAE,KAAK,EAAE,gBAAgB,EAAE,QAAQ,EAAE,GAAG,IAAI,EAAE,GAAG,OAAO,CAAC;QAC1E,OAAO,IAAI,CAAC;IACd,CAAC,CAAC,CAAC;AACL,CAAC;AAED,4EAA4E;AAC5E,MAAM,UAAU,SAAS,CAAC,IAAe;IACvC,OAAO,QAAQ,IAAI,CAAC,QAAQ,WAAW,IAAI,CAAC,IAAI,EAAE,CAAC;AACrD,CAAC"}
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@providerkit/core",
3
- "version": "0.7.1",
3
+ "version": "0.8.1",
4
4
  "description": "The layer under your agent loop: one seam for every LLM provider, plus the failure handling you only learn in production.",
5
5
  "type": "module",
6
6
  "license": "MIT",
package/src/errors.ts CHANGED
@@ -10,6 +10,8 @@
10
10
  // read before the status — status alone gives the wrong advice ("retry" for an
11
11
  // empty balance, "check your key" for a plan that never included the API).
12
12
 
13
+ import { parseUsageLimitBody, type RateLimitWindow } from "./rate-limit.ts";
14
+
13
15
  /** What kind of failure this is, named by what actually fixes it. */
14
16
  export type ErrorKind =
15
17
  /** Our own cancel — the caller pressed Stop. Never retried. */
@@ -67,6 +69,9 @@ export class ProviderError extends Error {
67
69
  /** Honoured when the provider said how long to wait (Retry-After, or
68
70
  * Gemini's RetryInfo.retryDelay). */
69
71
  readonly retryAfterMs?: number;
72
+ /** Absolute reset of the binding limit. Can be days beyond Retry-After. */
73
+ readonly resetAtMs?: number;
74
+ readonly window?: RateLimitWindow;
70
75
  /** The provider's own response body, truncated — the actual reason, which is
71
76
  * otherwise lost behind "400 status code (no body)". */
72
77
  readonly body?: string;
@@ -79,6 +84,8 @@ export class ProviderError extends Error {
79
84
  status?: number;
80
85
  code?: string;
81
86
  retryAfterMs?: number;
87
+ resetAtMs?: number;
88
+ window?: RateLimitWindow;
82
89
  body?: string;
83
90
  cause?: unknown;
84
91
  } = {},
@@ -90,6 +97,8 @@ export class ProviderError extends Error {
90
97
  this.status = opts.status;
91
98
  this.code = opts.code;
92
99
  this.retryAfterMs = opts.retryAfterMs;
100
+ this.resetAtMs = opts.resetAtMs;
101
+ this.window = opts.window;
93
102
  this.body = opts.body;
94
103
  }
95
104
 
@@ -113,6 +122,7 @@ export class ProviderError extends Error {
113
122
  status,
114
123
  code,
115
124
  retryAfterMs: parseRetryAfterMs(err, body),
125
+ ...parseUsageLimitBody(body),
116
126
  body: body.slice(0, 2_000) || undefined,
117
127
  cause: err,
118
128
  });
@@ -411,7 +421,13 @@ export function classify(err: unknown, status?: number, body?: string): ErrorKin
411
421
  * backoff shorter than the window just burns an attempt.
412
422
  */
413
423
  export function parseRetryAfterMs(err: unknown, body?: string): number | undefined {
424
+ // Adapters already normalized the response headers. Re-parsing only the body
425
+ // discarded this property and made retries ignore their own Retry-After.
426
+ const normalized = readNumber(err, "retryAfterMs");
427
+ if (normalized !== undefined && Number.isFinite(normalized) && normalized >= 0) return normalized;
414
428
  const text = body ?? bodyTextOf(err);
429
+ const usageLimit = parseUsageLimitBody(text);
430
+ if (usageLimit.retryAfterMs !== undefined) return usageLimit.retryAfterMs;
415
431
  const delay = text.match(/"?retryDelay"?\s*[:=]\s*"?(\d+(?:\.\d+)?)s"?/i);
416
432
  if (delay?.[1]) return Math.round(parseFloat(delay[1]) * 1000);
417
433
 
@@ -467,6 +483,7 @@ export function streamError(provider: string, error: unknown): ProviderError {
467
483
  ...(status !== undefined ? { status } : {}),
468
484
  ...(code ? { code } : {}),
469
485
  retryAfterMs: parseRetryAfterMs(error, body),
486
+ ...parseUsageLimitBody(body),
470
487
  body: body.slice(0, 2_000),
471
488
  },
472
489
  );
@@ -482,6 +499,8 @@ export function describeProviderError(err: unknown): Record<string, unknown> {
482
499
  status: err.status,
483
500
  code: err.code,
484
501
  retryAfterMs: err.retryAfterMs,
502
+ resetAtMs: err.resetAtMs,
503
+ window: err.window,
485
504
  error: err.message,
486
505
  body: err.body,
487
506
  };
@@ -0,0 +1,254 @@
1
+ import { classify, parseRetryAfterMs, ProviderError, type ErrorKind } from "./errors.ts";
2
+ import type { RateLimitWindow } from "./rate-limit.ts";
3
+ import type { ProviderPresetId } from "./presets.ts";
4
+ import type { Provider, ProviderChunk } from "./types.ts";
5
+ import { createPresetProvider, type PresetProviderConfig } from "./providers/factory.ts";
6
+
7
+ /** Retry intervals when the server supplies no deadline, not estimates of when
8
+ * it will recover. A server deadline always wins over these defaults. */
9
+ export const DEFAULT_FALLBACK_COOLDOWNS: Readonly<Partial<Record<ErrorKind, number>>> = Object.freeze({
10
+ rate: 60_000,
11
+ quota: 3_600_000,
12
+ timeout: 30_000,
13
+ network: 30_000,
14
+ overload: 30_000,
15
+ auth: 3_600_000,
16
+ entitlement: 3_600_000,
17
+ model: 3_600_000,
18
+ });
19
+
20
+ export interface FallbackOptions<T> {
21
+ /** Per-kind intervals in ms; null stops fallback for that kind. Adding a kind
22
+ * opts it into fallback. Cancellation is never overridable. */
23
+ cooldownMs?: Partial<Record<ErrorKind, number | null>>;
24
+ onCooldown?: (info: {
25
+ candidate: T;
26
+ error: unknown;
27
+ kind: ErrorKind;
28
+ retryAtMs: number;
29
+ window?: RateLimitWindow;
30
+ }) => void;
31
+ /** Inject a clock for deterministic tests. */
32
+ now?: () => number;
33
+ }
34
+
35
+ /**
36
+ * A fallback named by PRESET id rather than hand-built: the endpoint, auth
37
+ * style, effort dialect and every measured quirk come from the presets table,
38
+ * so a consumer writes neither a baseUrl nor a model-spelling rule. A
39
+ * hand-built `Provider` is still accepted wherever a spec is.
40
+ */
41
+ export interface FallbackSpec extends PresetProviderConfig {
42
+ preset: ProviderPresetId;
43
+ }
44
+
45
+ export type FallbackCandidate = Provider | FallbackSpec;
46
+
47
+ function resolveFallback(candidate: FallbackCandidate): Provider {
48
+ if (typeof candidate === "object" && candidate !== null && "createStream" in candidate) {
49
+ return candidate;
50
+ }
51
+ const { preset, ...config } = candidate;
52
+ return createPresetProvider(preset, config);
53
+ }
54
+
55
+ /** Standard options that any provider config can accept to configure fallbacks inline. */
56
+ export interface ProviderFallbackConfig {
57
+ /** Secondary and tertiary providers to try if this provider fails/exhausts:
58
+ * hand-built `Provider`s, preset-id `FallbackSpec`s, or both. */
59
+ fallbacks?: readonly FallbackCandidate[];
60
+ /** Custom cooldown or telemetry options for the fallback pool. */
61
+ fallbackOptions?: FallbackOptions<Provider>;
62
+ }
63
+
64
+ /**
65
+ * Wrap a primary provider with its fallbacks if any are specified.
66
+ * Returns the primary provider untouched when fallbacks is empty or omitted.
67
+ */
68
+ export function withConfiguredFallbacks(
69
+ primary: Provider,
70
+ config?: ProviderFallbackConfig,
71
+ ): Provider {
72
+ if (!config?.fallbacks || config.fallbacks.length === 0) return primary;
73
+ return withFallbackProviders(
74
+ [primary, ...config.fallbacks.map(resolveFallback)],
75
+ config.fallbackOptions,
76
+ );
77
+ }
78
+
79
+ interface Cooldown {
80
+ retryAtMs: number;
81
+ kind: ErrorKind;
82
+ window?: RateLimitWindow;
83
+ probing: boolean;
84
+ }
85
+
86
+ /** The whole chain is unavailable. Fail promptly instead of holding a request
87
+ * open for hours. `retryAtMs` names its next opportunity, not a sleep to cap. */
88
+ export class NoAvailableProviderError extends ProviderError {
89
+ readonly retryAtMs: number;
90
+
91
+ constructor(retryAtMs: number, now: number) {
92
+ super("fallback", "rate", "All configured providers are unavailable. Retry after the cooldown.", {
93
+ resetAtMs: retryAtMs,
94
+ retryAfterMs: Math.max(0, retryAtMs - now),
95
+ });
96
+ this.name = "NoAvailableProviderError";
97
+ this.retryAtMs = retryAtMs;
98
+ }
99
+ }
100
+
101
+ /** Ordered candidates with cooldowns shared across calls. Keep one pool alive
102
+ * per credential set: rebuilding it per request forgets a weekly lockout.
103
+ * State is in-memory and process-local; `status` and `reset` expose it without
104
+ * a background timer, vendor SDK, database or global singleton. */
105
+ export class FallbackPool<T> {
106
+ private readonly candidates: readonly T[];
107
+ private readonly cooldowns = new Map<T, Cooldown>();
108
+ private readonly now: () => number;
109
+
110
+ constructor(candidates: readonly T[], private readonly options: FallbackOptions<T> = {}) {
111
+ if (candidates.length === 0) throw new Error("providerkit: supply a primary before its backups");
112
+ if (new Set(candidates).size !== candidates.length) {
113
+ throw new Error("providerkit: each fallback candidate must appear only once");
114
+ }
115
+ for (const value of Object.values(options.cooldownMs ?? {})) {
116
+ if (value !== null && value !== undefined && (!Number.isFinite(value) || value < 0)) {
117
+ throw new Error("providerkit: fallback cooldowns must be finite, non-negative milliseconds");
118
+ }
119
+ }
120
+ this.candidates = [...candidates];
121
+ this.now = options.now ?? Date.now;
122
+ }
123
+
124
+ /** Clear a cooldown after replacing a key or fixing configuration. Omit the
125
+ * candidate to clear all. Does not interrupt calls already in flight. */
126
+ reset(...selection: [] | [T]): void {
127
+ if (selection.length === 0) this.cooldowns.clear();
128
+ else this.cooldowns.delete(selection[0]);
129
+ }
130
+
131
+ status(): Array<{ candidate: T; retryAtMs: number; probing: boolean; kind?: ErrorKind; window?: RateLimitWindow }> {
132
+ return this.candidates.map((candidate) => ({
133
+ candidate,
134
+ retryAtMs: 0,
135
+ probing: false,
136
+ ...this.cooldowns.get(candidate),
137
+ }));
138
+ }
139
+
140
+ /** Buffered calls only: do not wrap an agent turn that has run side effects. */
141
+ async with<R>(attempt: (candidate: T, signal: AbortSignal) => Promise<R>, signal?: AbortSignal): Promise<R> {
142
+ for await (const result of this.stream(async function* (candidate, inner) {
143
+ yield await attempt(candidate, inner);
144
+ }, signal)) return result;
145
+ throw new Error("providerkit: fallback attempt ended without a result");
146
+ }
147
+
148
+ /** Only a pre-output failure can switch provider. A later failure still
149
+ * cools the endpoint for FUTURE calls but never restarts the current answer. */
150
+ async *stream<R>(
151
+ attempt: (candidate: T, signal: AbortSignal) => AsyncIterable<R>,
152
+ signal?: AbortSignal,
153
+ ): AsyncGenerator<R> {
154
+ signal?.throwIfAborted();
155
+ let hasFailed = false;
156
+ let lastError: unknown;
157
+ for (const candidate of this.candidates) {
158
+ signal?.throwIfAborted();
159
+ const previous = this.cooldowns.get(candidate);
160
+ if (previous && (previous.retryAtMs > this.now() || previous.probing)) continue;
161
+ // One recovery probe after expiry. Healthy endpoints still allow normal
162
+ // concurrency; only a known failed endpoint gets a single probe.
163
+ if (previous) previous.probing = true;
164
+ const controller = new AbortController();
165
+ const inner = signal ? AbortSignal.any([signal, controller.signal]) : controller.signal;
166
+ let emitted = false;
167
+ let attemptFailed = false;
168
+ const startedWithCooldown = previous !== undefined;
169
+ try {
170
+ for await (const chunk of attempt(candidate, inner)) {
171
+ inner.throwIfAborted();
172
+ emitted = true;
173
+ // Only a call that was admitted as a recovery probe (started while on cooldown)
174
+ // is authorized to clear that cooldown upon succeeding.
175
+ if (startedWithCooldown && this.cooldowns.get(candidate) === previous) {
176
+ this.cooldowns.delete(candidate);
177
+ }
178
+ yield chunk;
179
+ }
180
+ inner.throwIfAborted();
181
+ if (startedWithCooldown && this.cooldowns.get(candidate) === previous) {
182
+ this.cooldowns.delete(candidate);
183
+ }
184
+ return;
185
+ } catch (error) {
186
+ signal?.throwIfAborted();
187
+ const kind = classify(error);
188
+ if (kind === "aborted") throw error;
189
+ const configured = this.options.cooldownMs?.[kind];
190
+ const interval = configured === undefined ? DEFAULT_FALLBACK_COOLDOWNS[kind] : configured;
191
+ if (interval === undefined || interval === null) throw error;
192
+ const now = this.now();
193
+ const after = parseRetryAfterMs(error);
194
+ const reset = error instanceof ProviderError ? error.resetAtMs : undefined;
195
+ const hints = [reset, after === undefined ? undefined : now + after].filter(
196
+ (value): value is number => value !== undefined && Number.isFinite(value),
197
+ );
198
+ const retryAtMs = Math.max(
199
+ now,
200
+ ...(hints.length ? hints : [now + interval]),
201
+ this.cooldowns.get(candidate)?.retryAtMs ?? 0,
202
+ );
203
+ const window = error instanceof ProviderError ? error.window : undefined;
204
+ this.cooldowns.set(candidate, { retryAtMs, kind, window, probing: false });
205
+ this.options.onCooldown?.({ candidate, error, kind, retryAtMs, window });
206
+ attemptFailed = true;
207
+ hasFailed = true;
208
+ lastError = error;
209
+ if (emitted) throw error;
210
+ } finally {
211
+ controller.abort();
212
+ const current = this.cooldowns.get(candidate);
213
+ if (current && current.probing && !attemptFailed) {
214
+ current.probing = false;
215
+ }
216
+ }
217
+ }
218
+ if (hasFailed) throw lastError;
219
+ // If all expired candidates are already being probed, the next opportunity
220
+ // is now. Other callers need not wait for those probes or start duplicates.
221
+ const retryAtMs = Math.min(...this.status().map((entry) => Math.max(this.now(), entry.retryAtMs)));
222
+ throw new NoAvailableProviderError(retryAtMs, this.now());
223
+ }
224
+ }
225
+
226
+ export interface FallbackProvider extends Provider {
227
+ readonly fallbacks: FallbackPool<Provider>;
228
+ }
229
+
230
+ /** Compose bound provider/model pairs. Identity names the configured primary;
231
+ * chunk.source names the endpoint that answered. Per-call model overrides
232
+ * apply ONLY to the primary because model ids are endpoint-specific.
233
+ * Keep this provider alive across calls so its cooldowns survive. */
234
+ export function withFallbackProviders(
235
+ providers: readonly Provider[],
236
+ options: FallbackOptions<Provider> = {},
237
+ ): FallbackProvider {
238
+ const fallbacks = new FallbackPool(providers, options);
239
+ const primary = providers[0]!;
240
+ return {
241
+ id: primary.id,
242
+ model: primary.model,
243
+ fallbacks,
244
+ createStream(messages, tools, opts = {}) {
245
+ return fallbacks.stream(async function* (provider, signal): AsyncGenerator<ProviderChunk> {
246
+ const { model: override, ...rest } = opts;
247
+ const model = provider === primary ? (override ?? provider.model) : provider.model;
248
+ for await (const chunk of provider.createStream(messages, tools, { ...rest, model, signal })) {
249
+ yield { ...chunk, source: chunk.source ?? { provider: provider.id, model } };
250
+ }
251
+ }, opts.signal);
252
+ },
253
+ };
254
+ }
package/src/index.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  export * from "./types.ts";
2
2
  export * from "./errors.ts";
3
3
  export * from "./retry.ts";
4
+ export * from "./fallback.ts";
4
5
  export * from "./key-pool.ts";
5
6
  export * from "./watchdog.ts";
6
7
  export * from "./usage.ts";
@@ -3,6 +3,7 @@ import { streamError } from "../errors.ts";
3
3
  import { schemaPrompt } from "../schema.ts";
4
4
  import { parseToolArgs } from "../tool-args.ts";
5
5
  import { streamSse, apiUrl } from "../transport.ts";
6
+ import { withConfiguredFallbacks, type ProviderFallbackConfig } from "../fallback.ts";
6
7
  import type {
7
8
  JsonOutput,
8
9
  ChatMessage,
@@ -15,7 +16,7 @@ import type {
15
16
  ToolDefinition,
16
17
  } from "../types.ts";
17
18
 
18
- export interface AnthropicConfig {
19
+ export interface AnthropicConfig extends ProviderFallbackConfig {
19
20
  apiKey: string;
20
21
  model: string;
21
22
  /** Any endpoint speaking the Anthropic Messages dialect — a proxy or gateway.
@@ -219,7 +220,7 @@ export function createAnthropicProvider(config: AnthropicConfig): Provider {
219
220
  const baseUrl = config.baseUrl ?? DEFAULT_BASE_URL;
220
221
  const id = config.id ?? "anthropic";
221
222
 
222
- return {
223
+ const provider: Provider = {
223
224
  id,
224
225
  model: config.model,
225
226
 
@@ -382,4 +383,5 @@ export function createAnthropicProvider(config: AnthropicConfig): Provider {
382
383
  }
383
384
  },
384
385
  };
386
+ return withConfiguredFallbacks(provider, config);
385
387
  }
@@ -21,8 +21,9 @@ import {
21
21
  type ProviderPresetId,
22
22
  } from "../presets.ts";
23
23
  import type { Effort, Provider } from "../types.ts";
24
+ import { withConfiguredFallbacks, type ProviderFallbackConfig } from "../fallback.ts";
24
25
 
25
- export interface PresetProviderConfig {
26
+ export interface PresetProviderConfig extends ProviderFallbackConfig {
26
27
  /** The credential: an API key for `key`/`bearer` presets, an ACCESS TOKEN
27
28
  * for `oauth` ones. Acquiring the token is the caller's job. */
28
29
  apiKey: string;
@@ -58,11 +59,15 @@ export function createPresetProvider(id: ProviderPresetId, config: PresetProvide
58
59
  }
59
60
  const headers = { ...preset.headers, ...config.headers };
60
61
 
62
+ const { fallbacks: _fallbacks, fallbackOptions: _fallbackOptions, ...baseConfig } = config;
63
+
64
+ let provider: Provider;
61
65
  switch (preset.shape) {
62
66
  case "anthropic":
63
67
  // `key` rides Anthropic's native x-api-key; coding plans and OAuth
64
68
  // tokens read Bearer (measured across the family's coding endpoints).
65
- return createAnthropicProvider({
69
+ provider = createAnthropicProvider({
70
+ ...baseConfig,
66
71
  apiKey: config.apiKey,
67
72
  model,
68
73
  effort: config.effort,
@@ -76,8 +81,10 @@ export function createPresetProvider(id: ProviderPresetId, config: PresetProvide
76
81
  ...(preset.explicitNone ? { explicitNone: true } : {}),
77
82
  fetchImpl: config.fetchImpl,
78
83
  });
84
+ break;
79
85
  case "openai":
80
- return createOpenAIProvider({
86
+ provider = createOpenAIProvider({
87
+ ...baseConfig,
81
88
  id,
82
89
  apiKey: config.apiKey,
83
90
  model,
@@ -88,8 +95,10 @@ export function createPresetProvider(id: ProviderPresetId, config: PresetProvide
88
95
  ...(config.providerOrder ? { providerOrder: config.providerOrder } : {}),
89
96
  fetchImpl: config.fetchImpl,
90
97
  });
98
+ break;
91
99
  case "responses":
92
- return createResponsesProvider({
100
+ provider = createResponsesProvider({
101
+ ...baseConfig,
93
102
  apiKey: config.apiKey,
94
103
  model,
95
104
  effort: config.effort,
@@ -99,8 +108,10 @@ export function createPresetProvider(id: ProviderPresetId, config: PresetProvide
99
108
  headers,
100
109
  fetchImpl: config.fetchImpl,
101
110
  });
111
+ break;
102
112
  case "gemini":
103
- return createGeminiProvider({
113
+ provider = createGeminiProvider({
114
+ ...baseConfig,
104
115
  apiKey: config.apiKey,
105
116
  model,
106
117
  effort: config.effort,
@@ -109,5 +120,8 @@ export function createPresetProvider(id: ProviderPresetId, config: PresetProvide
109
120
  headers,
110
121
  fetchImpl: config.fetchImpl,
111
122
  });
123
+ break;
112
124
  }
125
+
126
+ return withConfiguredFallbacks(provider, config);
113
127
  }
@@ -9,6 +9,7 @@ import { streamError } from "../errors.ts";
9
9
  import { schemaPrompt } from "../schema.ts";
10
10
  import { parseToolArgs } from "../tool-args.ts";
11
11
  import { streamSse, apiUrl } from "../transport.ts";
12
+ import { withConfiguredFallbacks, type ProviderFallbackConfig } from "../fallback.ts";
12
13
  import type {
13
14
  ChatMessage,
14
15
  Effort,
@@ -22,7 +23,7 @@ import type {
22
23
  ToolDefinition,
23
24
  } from "../types.ts";
24
25
 
25
- export interface GeminiConfig {
26
+ export interface GeminiConfig extends ProviderFallbackConfig {
26
27
  apiKey: string;
27
28
  model: string;
28
29
  /** Any endpoint speaking the Generative Language REST dialect — a proxy or
@@ -247,7 +248,7 @@ export function createGeminiProvider(config: GeminiConfig): Provider {
247
248
  const baseUrl = config.baseUrl ?? DEFAULT_BASE_URL;
248
249
  const id = config.id ?? "gemini";
249
250
 
250
- return {
251
+ const provider: Provider = {
251
252
  id,
252
253
  model: config.model,
253
254
 
@@ -409,4 +410,6 @@ export function createGeminiProvider(config: GeminiConfig): Provider {
409
410
  }
410
411
  },
411
412
  };
413
+
414
+ return withConfiguredFallbacks(provider, config);
412
415
  }
@@ -18,8 +18,9 @@ import type {
18
18
  } from "../types.ts";
19
19
  import { toDataUri } from "../types.ts";
20
20
  import { isStrictSchema, schemaPrompt } from "../schema.ts";
21
+ import { withConfiguredFallbacks, type ProviderFallbackConfig } from "../fallback.ts";
21
22
 
22
- export interface OpenAIConfig {
23
+ export interface OpenAIConfig extends ProviderFallbackConfig {
23
24
  apiKey: string;
24
25
  model: string;
25
26
  /** Any OpenAI-compatible endpoint. Defaults to OpenAI itself. */
@@ -305,7 +306,7 @@ export function createOpenAIProvider(config: OpenAIConfig): Provider {
305
306
  const baseUrl = config.baseUrl ?? DEFAULT_BASE_URL;
306
307
  const id = config.id ?? "openai";
307
308
 
308
- return {
309
+ const provider: Provider = {
309
310
  id,
310
311
  model: config.model,
311
312
 
@@ -489,4 +490,6 @@ export function createOpenAIProvider(config: OpenAIConfig): Provider {
489
490
  }
490
491
  },
491
492
  };
493
+
494
+ return withConfiguredFallbacks(provider, config);
492
495
  }
@@ -25,8 +25,9 @@ import type {
25
25
  } from "../types.ts";
26
26
  import { toDataUri } from "../types.ts";
27
27
  import { isStrictSchema } from "../schema.ts";
28
+ import { withConfiguredFallbacks, type ProviderFallbackConfig } from "../fallback.ts";
28
29
 
29
- export interface ResponsesConfig {
30
+ export interface ResponsesConfig extends ProviderFallbackConfig {
30
31
  apiKey: string;
31
32
  model: string;
32
33
  /** Any endpoint speaking the Responses format. Defaults to OpenAI itself. */
@@ -226,7 +227,7 @@ export function createResponsesProvider(config: ResponsesConfig): Provider {
226
227
  const baseUrl = config.baseUrl ?? DEFAULT_BASE_URL;
227
228
  const id = config.id ?? "openai-responses";
228
229
 
229
- return {
230
+ const provider: Provider = {
230
231
  id,
231
232
  model: config.model,
232
233
 
@@ -463,4 +464,6 @@ export function createResponsesProvider(config: ResponsesConfig): Provider {
463
464
  // included, was already yielded as it arrived.
464
465
  },
465
466
  };
467
+
468
+ return withConfiguredFallbacks(provider, config);
466
469
  }
package/src/rate-limit.ts CHANGED
@@ -215,3 +215,25 @@ export function parseUsageLimitBody(bodyText: string, now = Date.now()): RateLim
215
215
  }
216
216
  return result;
217
217
  }
218
+
219
+ /** Read both places an endpoint can report its reset. A short Retry-After is
220
+ * not evidence that a longer subscription window has lifted. */
221
+ export function parseRateLimitResponse(
222
+ headers: Headers,
223
+ bodyText: string,
224
+ now = Date.now(),
225
+ ): RateLimitReset {
226
+ const header = parseRateLimitReset(headers, now);
227
+ const body = parseUsageLimitBody(bodyText, now);
228
+ const resetAtMs = Math.max(header.resetAtMs ?? 0, body.resetAtMs ?? 0);
229
+ const retryAfterMs = header.retryAfterMs ?? body.retryAfterMs;
230
+ const window =
231
+ body.resetAtMs !== undefined && body.resetAtMs > (header.resetAtMs ?? 0)
232
+ ? body.window
233
+ : (header.window ?? body.window);
234
+ return {
235
+ ...(resetAtMs > 0 ? { resetAtMs } : {}),
236
+ ...(retryAfterMs !== undefined ? { retryAfterMs } : {}),
237
+ ...(window ? { window } : {}),
238
+ };
239
+ }