@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.
- package/dist/errors.d.ts +6 -0
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +18 -0
- package/dist/errors.js.map +1 -1
- package/dist/fallback.d.ts +73 -0
- package/dist/fallback.d.ts.map +1 -0
- package/dist/fallback.js +182 -0
- package/dist/fallback.js.map +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/providers/anthropic.d.ts +2 -1
- package/dist/providers/anthropic.d.ts.map +1 -1
- package/dist/providers/anthropic.js +3 -1
- package/dist/providers/anthropic.js.map +1 -1
- package/dist/providers/factory.d.ts +7 -1
- package/dist/providers/factory.d.ts.map +1 -1
- package/dist/providers/factory.js +18 -5
- package/dist/providers/factory.js.map +1 -1
- package/dist/providers/gemini.d.ts +2 -1
- package/dist/providers/gemini.d.ts.map +1 -1
- package/dist/providers/gemini.js +3 -1
- package/dist/providers/gemini.js.map +1 -1
- package/dist/providers/openai.d.ts +2 -1
- package/dist/providers/openai.d.ts.map +1 -1
- package/dist/providers/openai.js +3 -1
- package/dist/providers/openai.js.map +1 -1
- package/dist/providers/responses.d.ts +2 -1
- package/dist/providers/responses.d.ts.map +1 -1
- package/dist/providers/responses.js +3 -1
- package/dist/providers/responses.js.map +1 -1
- package/dist/rate-limit.d.ts +3 -0
- package/dist/rate-limit.d.ts.map +1 -1
- package/dist/rate-limit.js +16 -0
- package/dist/rate-limit.js.map +1 -1
- package/dist/retry.d.ts +2 -0
- package/dist/retry.d.ts.map +1 -1
- package/dist/retry.js +13 -5
- package/dist/retry.js.map +1 -1
- package/dist/transport.d.ts.map +1 -1
- package/dist/transport.js +5 -2
- package/dist/transport.js.map +1 -1
- package/dist/types.d.ts +8 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +5 -1
- package/dist/types.js.map +1 -1
- package/package.json +1 -1
- package/src/errors.ts +19 -0
- package/src/fallback.ts +228 -0
- package/src/index.ts +1 -0
- package/src/providers/anthropic.ts +4 -2
- package/src/providers/factory.ts +31 -6
- package/src/providers/gemini.ts +5 -2
- package/src/providers/openai.ts +5 -2
- package/src/providers/responses.ts +5 -2
- package/src/rate-limit.ts +22 -0
- package/src/retry.ts +14 -5
- package/src/transport.ts +5 -2
- 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
|
-
/**
|
|
71
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|