@providerkit/core 0.8.0 → 0.9.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/capability.d.ts +41 -0
- package/dist/capability.d.ts.map +1 -1
- package/dist/capability.js +94 -0
- package/dist/capability.js.map +1 -1
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +77 -13
- package/dist/errors.js.map +1 -1
- package/dist/fallback.d.ts +15 -2
- package/dist/fallback.d.ts.map +1 -1
- package/dist/fallback.js +9 -1
- package/dist/fallback.js.map +1 -1
- package/dist/presets.d.ts +127 -28
- package/dist/presets.d.ts.map +1 -1
- package/dist/presets.js +126 -26
- package/dist/presets.js.map +1 -1
- package/dist/providers/factory.d.ts.map +1 -1
- package/dist/providers/factory.js +1 -0
- package/dist/providers/factory.js.map +1 -1
- package/dist/providers/openai.d.ts.map +1 -1
- package/dist/providers/openai.js +4 -1
- package/dist/providers/openai.js.map +1 -1
- package/dist/rate-limit.d.ts +2 -0
- package/dist/rate-limit.d.ts.map +1 -1
- package/dist/rate-limit.js +91 -34
- package/dist/rate-limit.js.map +1 -1
- package/package.json +2 -1
- package/src/capability.ts +157 -0
- package/src/errors.ts +78 -13
- package/src/fallback.ts +29 -3
- package/src/presets.ts +131 -28
- package/src/providers/factory.ts +1 -0
- package/src/providers/openai.ts +5 -1
- package/src/rate-limit.ts +98 -36
package/src/fallback.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { classify, parseRetryAfterMs, ProviderError, type ErrorKind } from "./errors.ts";
|
|
2
2
|
import type { RateLimitWindow } from "./rate-limit.ts";
|
|
3
|
+
import type { ProviderPresetId } from "./presets.ts";
|
|
3
4
|
import type { Provider, ProviderChunk } from "./types.ts";
|
|
5
|
+
import { createPresetProvider, type PresetProviderConfig } from "./providers/factory.ts";
|
|
4
6
|
|
|
5
7
|
/** Retry intervals when the server supplies no deadline, not estimates of when
|
|
6
8
|
* it will recover. A server deadline always wins over these defaults. */
|
|
@@ -30,10 +32,31 @@ export interface FallbackOptions<T> {
|
|
|
30
32
|
now?: () => number;
|
|
31
33
|
}
|
|
32
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
|
+
|
|
33
55
|
/** Standard options that any provider config can accept to configure fallbacks inline. */
|
|
34
56
|
export interface ProviderFallbackConfig {
|
|
35
|
-
/** Secondary and tertiary providers to try if this provider fails/exhausts
|
|
36
|
-
|
|
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[];
|
|
37
60
|
/** Custom cooldown or telemetry options for the fallback pool. */
|
|
38
61
|
fallbackOptions?: FallbackOptions<Provider>;
|
|
39
62
|
}
|
|
@@ -47,7 +70,10 @@ export function withConfiguredFallbacks(
|
|
|
47
70
|
config?: ProviderFallbackConfig,
|
|
48
71
|
): Provider {
|
|
49
72
|
if (!config?.fallbacks || config.fallbacks.length === 0) return primary;
|
|
50
|
-
return withFallbackProviders(
|
|
73
|
+
return withFallbackProviders(
|
|
74
|
+
[primary, ...config.fallbacks.map(resolveFallback)],
|
|
75
|
+
config.fallbackOptions,
|
|
76
|
+
);
|
|
51
77
|
}
|
|
52
78
|
|
|
53
79
|
interface Cooldown {
|
package/src/presets.ts
CHANGED
|
@@ -37,9 +37,11 @@ export type PresetAuth = "key" | "bearer" | "oauth";
|
|
|
37
37
|
export interface ProviderPreset {
|
|
38
38
|
/** Adapter wire format. */
|
|
39
39
|
shape: PresetShape;
|
|
40
|
-
/** API root —
|
|
41
|
-
*
|
|
40
|
+
/** API root — if it ends with /v1, /v4 or another version segment, the
|
|
41
|
+
* adapter automatically routes to /chat/completions instead of /v1/chat/completions. */
|
|
42
42
|
baseUrl: string;
|
|
43
|
+
/** Explicit path override when a gateway has custom routing. */
|
|
44
|
+
path?: string;
|
|
43
45
|
auth: PresetAuth;
|
|
44
46
|
/** Fallback when the caller passes no model. */
|
|
45
47
|
defaultModel?: string;
|
|
@@ -65,15 +67,15 @@ export const PROVIDER_PRESETS = {
|
|
|
65
67
|
shape: "anthropic",
|
|
66
68
|
baseUrl: "https://api.anthropic.com",
|
|
67
69
|
auth: "key",
|
|
68
|
-
defaultModel: "claude-sonnet-
|
|
69
|
-
models: ["claude-sonnet-
|
|
70
|
+
defaultModel: "claude-sonnet-5",
|
|
71
|
+
models: ["claude-sonnet-5", "claude-opus-5", "claude-fable-5-1", "claude-haiku-4-5"],
|
|
70
72
|
},
|
|
71
73
|
openai: {
|
|
72
74
|
shape: "openai",
|
|
73
75
|
baseUrl: "https://api.openai.com",
|
|
74
76
|
auth: "key",
|
|
75
77
|
defaultModel: "gpt-5.6-sol",
|
|
76
|
-
models: ["gpt-5.6-sol", "gpt-5-pro", "gpt-5-
|
|
78
|
+
models: ["gpt-5.6-sol", "gpt-5.5-pro", "gpt-5.4-mini"],
|
|
77
79
|
},
|
|
78
80
|
google: {
|
|
79
81
|
// Native Generative Language REST — the adapter authenticates with
|
|
@@ -81,8 +83,8 @@ export const PROVIDER_PRESETS = {
|
|
|
81
83
|
shape: "gemini",
|
|
82
84
|
baseUrl: "https://generativelanguage.googleapis.com",
|
|
83
85
|
auth: "key",
|
|
84
|
-
defaultModel: "gemini-3.1-pro",
|
|
85
|
-
models: ["gemini-3.1-pro", "gemini-
|
|
86
|
+
defaultModel: "gemini-3.1-pro-preview",
|
|
87
|
+
models: ["gemini-3.1-pro-preview", "gemini-3.8-flash", "gemini-flash-latest"],
|
|
86
88
|
},
|
|
87
89
|
/** Gemini behind its OpenAI-compatible route — the drop-in for codebases
|
|
88
90
|
* that only speak the openai shape. */
|
|
@@ -90,15 +92,17 @@ export const PROVIDER_PRESETS = {
|
|
|
90
92
|
shape: "openai",
|
|
91
93
|
baseUrl: "https://generativelanguage.googleapis.com/v1beta/openai",
|
|
92
94
|
auth: "key",
|
|
93
|
-
defaultModel: "gemini-3.1-pro",
|
|
94
|
-
models: ["gemini-3.1-pro", "gemini-
|
|
95
|
+
defaultModel: "gemini-3.1-pro-preview",
|
|
96
|
+
models: ["gemini-3.1-pro-preview", "gemini-3.8-flash", "gemini-flash-latest"],
|
|
95
97
|
},
|
|
96
98
|
deepseek: {
|
|
97
99
|
shape: "openai",
|
|
98
100
|
baseUrl: "https://api.deepseek.com",
|
|
99
101
|
auth: "key",
|
|
100
|
-
|
|
101
|
-
|
|
102
|
+
// Measured 2026-09-13 via /models on live key: first-party serves only
|
|
103
|
+
// deepseek-flash and deepseek-v4-pro. v4.1-flash is OpenRouter/Baseten.
|
|
104
|
+
defaultModel: "deepseek-flash",
|
|
105
|
+
models: ["deepseek-flash", "deepseek-v4-pro"],
|
|
102
106
|
// Text-only API — an image part in the body is a hard 400 (measured).
|
|
103
107
|
textOnly: true,
|
|
104
108
|
},
|
|
@@ -107,7 +111,7 @@ export const PROVIDER_PRESETS = {
|
|
|
107
111
|
baseUrl: "https://openrouter.ai/api",
|
|
108
112
|
auth: "key",
|
|
109
113
|
defaultModel: "deepseek/deepseek-v4.1-flash",
|
|
110
|
-
models: ["deepseek/deepseek-v4.1-flash", "z-ai/glm-5.3-flash", "
|
|
114
|
+
models: ["deepseek/deepseek-v4.1-flash", "z-ai/glm-5.3-flash", "x-ai/grok-4.6", "openai/gpt-5.6-luna-pro"],
|
|
111
115
|
},
|
|
112
116
|
groq: {
|
|
113
117
|
shape: "openai",
|
|
@@ -121,25 +125,28 @@ export const PROVIDER_PRESETS = {
|
|
|
121
125
|
baseUrl: "https://api.mistral.ai/v1",
|
|
122
126
|
auth: "key",
|
|
123
127
|
defaultModel: "devstral-2512",
|
|
124
|
-
models: ["devstral-2512", "
|
|
128
|
+
models: ["devstral-2512", "mistral-large-latest", "magistral-small"],
|
|
125
129
|
},
|
|
126
130
|
xai: {
|
|
127
131
|
shape: "openai",
|
|
128
132
|
baseUrl: "https://api.x.ai/v1",
|
|
129
133
|
auth: "key",
|
|
130
|
-
defaultModel: "grok-4.
|
|
131
|
-
models: ["grok-4.
|
|
134
|
+
defaultModel: "grok-4.6",
|
|
135
|
+
models: ["grok-4.6", "grok-4.5", "grok-4.3"],
|
|
132
136
|
},
|
|
133
137
|
together: {
|
|
134
138
|
shape: "openai",
|
|
135
139
|
baseUrl: "https://api.together.xyz/v1",
|
|
136
140
|
auth: "key",
|
|
137
|
-
defaultModel: "
|
|
141
|
+
defaultModel: "MiniMaxAI/MiniMax-M3",
|
|
142
|
+
models: ["MiniMaxAI/MiniMax-M3", "Qwen/Qwen3-Coder-Next-FP8"],
|
|
138
143
|
},
|
|
139
144
|
fireworks: {
|
|
140
145
|
shape: "openai",
|
|
141
146
|
baseUrl: "https://api.fireworks.ai/inference/v1",
|
|
142
147
|
auth: "key",
|
|
148
|
+
defaultModel: "accounts/fireworks/models/deepseek-v4p1-flash",
|
|
149
|
+
models: ["accounts/fireworks/models/deepseek-v4p1-flash", "accounts/fireworks/models/glm-5p3-flash"],
|
|
143
150
|
},
|
|
144
151
|
cerebras: {
|
|
145
152
|
shape: "openai",
|
|
@@ -158,16 +165,112 @@ export const PROVIDER_PRESETS = {
|
|
|
158
165
|
zhipu: {
|
|
159
166
|
shape: "openai",
|
|
160
167
|
baseUrl: "https://api.z.ai/api/paas/v4",
|
|
168
|
+
path: "/chat/completions",
|
|
161
169
|
auth: "key",
|
|
162
170
|
defaultModel: "glm-5.3-flash",
|
|
163
|
-
models: ["glm-5.3-flash", "glm-5.
|
|
171
|
+
models: ["glm-5.3-flash", "glm-5.3", "glm-5.2"],
|
|
164
172
|
},
|
|
165
173
|
minimax: {
|
|
166
174
|
shape: "anthropic",
|
|
167
175
|
baseUrl: "https://api.minimax.io/anthropic",
|
|
168
176
|
auth: "key",
|
|
169
|
-
defaultModel: "MiniMax-
|
|
170
|
-
models: ["MiniMax-M3", "MiniMax-M2.
|
|
177
|
+
defaultModel: "MiniMax-M3",
|
|
178
|
+
models: ["MiniMax-M3", "MiniMax-M2.7", "MiniMax-M2.5"],
|
|
179
|
+
},
|
|
180
|
+
|
|
181
|
+
// ── Popular gateways & cloud providers ──────────────────────────────────
|
|
182
|
+
nvidia: {
|
|
183
|
+
shape: "openai",
|
|
184
|
+
baseUrl: "https://integrate.api.nvidia.com/v1",
|
|
185
|
+
auth: "key",
|
|
186
|
+
defaultModel: "deepseek-ai/deepseek-v4-flash",
|
|
187
|
+
models: ["deepseek-ai/deepseek-v4-flash", "deepseek-ai/deepseek-v4-pro"],
|
|
188
|
+
},
|
|
189
|
+
perplexity: {
|
|
190
|
+
shape: "openai",
|
|
191
|
+
baseUrl: "https://api.perplexity.ai",
|
|
192
|
+
auth: "key",
|
|
193
|
+
defaultModel: "sonar",
|
|
194
|
+
models: ["sonar", "sonar-pro", "sonar-reasoning-pro", "sonar-deep-research"],
|
|
195
|
+
},
|
|
196
|
+
deepinfra: {
|
|
197
|
+
shape: "openai",
|
|
198
|
+
baseUrl: "https://api.deepinfra.com/v1/openai",
|
|
199
|
+
path: "/chat/completions",
|
|
200
|
+
auth: "key",
|
|
201
|
+
defaultModel: "MiniMaxAI/MiniMax-M3",
|
|
202
|
+
models: ["MiniMaxAI/MiniMax-M3", "ByteDance/Seed-2.0-pro"],
|
|
203
|
+
},
|
|
204
|
+
nebius: {
|
|
205
|
+
shape: "openai",
|
|
206
|
+
baseUrl: "https://api.tokenfactory.nebius.com/v1",
|
|
207
|
+
auth: "key",
|
|
208
|
+
defaultModel: "Qwen/Qwen3.5-397B-A17B",
|
|
209
|
+
models: ["Qwen/Qwen3.5-397B-A17B", "deepseek-ai/DeepSeek-V4-Flash-0731"],
|
|
210
|
+
},
|
|
211
|
+
novita: {
|
|
212
|
+
shape: "openai",
|
|
213
|
+
baseUrl: "https://api.novita.ai/openai",
|
|
214
|
+
path: "/v1/chat/completions",
|
|
215
|
+
auth: "key",
|
|
216
|
+
defaultModel: "baichuan/baichuan-m2-32b",
|
|
217
|
+
models: ["baichuan/baichuan-m2-32b", "baidu/ernie-4.5-300b-a47b-paddle"],
|
|
218
|
+
},
|
|
219
|
+
chutes: {
|
|
220
|
+
shape: "openai",
|
|
221
|
+
baseUrl: "https://llm.chutes.ai/v1",
|
|
222
|
+
auth: "key",
|
|
223
|
+
defaultModel: "Qwen/Qwen3.8-27B-TEE",
|
|
224
|
+
models: ["Qwen/Qwen3.8-27B-TEE", "Qwen/Qwen3.5-397B-A17B-TEE"],
|
|
225
|
+
},
|
|
226
|
+
siliconflow: {
|
|
227
|
+
shape: "openai",
|
|
228
|
+
baseUrl: "https://api.siliconflow.com/v1",
|
|
229
|
+
auth: "key",
|
|
230
|
+
defaultModel: "Qwen/Qwen3-235B-A22B-Thinking-2507",
|
|
231
|
+
models: ["Qwen/Qwen3-235B-A22B-Thinking-2507", "MiniMaxAI/MiniMax-M2.5"],
|
|
232
|
+
},
|
|
233
|
+
volcengine: {
|
|
234
|
+
shape: "openai",
|
|
235
|
+
baseUrl: "https://ark.cn-beijing.volces.com/api/v3",
|
|
236
|
+
auth: "key",
|
|
237
|
+
defaultModel: "doubao-seed-1-8-251228",
|
|
238
|
+
models: ["doubao-seed-1-8-251228", "deepseek-v4-flash-ga-260731"],
|
|
239
|
+
},
|
|
240
|
+
stepfun: {
|
|
241
|
+
shape: "openai",
|
|
242
|
+
baseUrl: "https://api.stepfun.com/v1",
|
|
243
|
+
auth: "key",
|
|
244
|
+
defaultModel: "step-3.7-flash",
|
|
245
|
+
models: ["step-3.7-flash", "step-3.5-flash"],
|
|
246
|
+
},
|
|
247
|
+
digitalocean: {
|
|
248
|
+
shape: "openai",
|
|
249
|
+
baseUrl: "https://inference.do-ai.run/v1",
|
|
250
|
+
auth: "key",
|
|
251
|
+
defaultModel: "anthropic-claude-3.7-sonnet",
|
|
252
|
+
models: ["anthropic-claude-3.7-sonnet", "alibaba-qwen3-32b"],
|
|
253
|
+
},
|
|
254
|
+
crusoe: {
|
|
255
|
+
shape: "openai",
|
|
256
|
+
baseUrl: "https://api.inference.crusoecloud.com/v1",
|
|
257
|
+
auth: "key",
|
|
258
|
+
defaultModel: "Qwen/Qwen3-235B-A22B-Instruct-2507",
|
|
259
|
+
models: ["Qwen/Qwen3-235B-A22B-Instruct-2507", "nvidia/NVIDIA-Nemotron-3-Super-120B-A12B"],
|
|
260
|
+
},
|
|
261
|
+
baseten: {
|
|
262
|
+
shape: "openai",
|
|
263
|
+
baseUrl: "https://inference.baseten.co/v1",
|
|
264
|
+
auth: "key",
|
|
265
|
+
defaultModel: "deepseek-ai/DeepSeek-V4.1-Flash",
|
|
266
|
+
models: ["deepseek-ai/DeepSeek-V4.1-Flash", "MiniMaxAI/MiniMax-M2.5"],
|
|
267
|
+
},
|
|
268
|
+
huggingface: {
|
|
269
|
+
shape: "openai",
|
|
270
|
+
baseUrl: "https://router.huggingface.co/v1",
|
|
271
|
+
auth: "key",
|
|
272
|
+
defaultModel: "MiniMaxAI/MiniMax-M3",
|
|
273
|
+
models: ["MiniMaxAI/MiniMax-M3", "Qwen/Qwen2.5-Coder-32B-Instruct"],
|
|
171
274
|
},
|
|
172
275
|
|
|
173
276
|
// ── Coding plans — Anthropic wire + Bearer (measured family trait) ─────
|
|
@@ -176,7 +279,7 @@ export const PROVIDER_PRESETS = {
|
|
|
176
279
|
baseUrl: "https://api.z.ai/api/anthropic",
|
|
177
280
|
auth: "bearer",
|
|
178
281
|
defaultModel: "glm-5.3-flash",
|
|
179
|
-
models: ["glm-5.3-flash", "glm-5.3-highspeed", "glm-5.
|
|
282
|
+
models: ["glm-5.3-flash", "glm-5.3-highspeed", "glm-5.3", "glm-5.2"],
|
|
180
283
|
// Model ids are BARE here — the gateway-prefixed spelling
|
|
181
284
|
// (`z-ai/glm-5.3-flash`) answers 400 [1211] Unknown Model.
|
|
182
285
|
explicitNone: true,
|
|
@@ -186,15 +289,15 @@ export const PROVIDER_PRESETS = {
|
|
|
186
289
|
baseUrl: "https://api.kimi.ai/coding",
|
|
187
290
|
auth: "bearer",
|
|
188
291
|
defaultModel: "kimi-for-coding",
|
|
189
|
-
models: ["kimi-for-coding", "kimi-for-coding-highspeed", "k3"],
|
|
292
|
+
models: ["kimi-for-coding", "kimi-for-coding-highspeed", "k3", "k3-256k"],
|
|
190
293
|
},
|
|
191
294
|
/** Alibaba's dashscope coding plan — OpenAI-compatible per its registry. */
|
|
192
295
|
"alibaba-coding-plan": {
|
|
193
296
|
shape: "openai",
|
|
194
297
|
baseUrl: "https://coding-intl.dashscope.aliyuncs.com/v1",
|
|
195
298
|
auth: "bearer",
|
|
196
|
-
defaultModel: "qwen3.
|
|
197
|
-
models: ["qwen3.
|
|
299
|
+
defaultModel: "qwen3.5-plus",
|
|
300
|
+
models: ["qwen3.5-plus", "qwen3-coder-next", "glm-5"],
|
|
198
301
|
},
|
|
199
302
|
/** QwenCloud's token plan — Anthropic wire (measured by tabrunner). */
|
|
200
303
|
qwen: {
|
|
@@ -202,7 +305,7 @@ export const PROVIDER_PRESETS = {
|
|
|
202
305
|
baseUrl: "https://token-plan.ap-southeast-1.maas.aliyuncs.com/apps/anthropic",
|
|
203
306
|
auth: "bearer",
|
|
204
307
|
defaultModel: "qwen3.8-max",
|
|
205
|
-
models: ["qwen3.8-max", "qwen3.6-flash"],
|
|
308
|
+
models: ["qwen3.8-max", "qwen3.8-flash", "qwen3.6-flash"],
|
|
206
309
|
},
|
|
207
310
|
|
|
208
311
|
// ── Local runtimes ──────────────────────────────────────────────────────
|
|
@@ -227,8 +330,8 @@ export const PROVIDER_PRESETS = {
|
|
|
227
330
|
shape: "anthropic",
|
|
228
331
|
baseUrl: "https://api.anthropic.com",
|
|
229
332
|
auth: "oauth",
|
|
230
|
-
defaultModel: "claude-sonnet-
|
|
231
|
-
models: ["claude-sonnet-
|
|
333
|
+
defaultModel: "claude-sonnet-5",
|
|
334
|
+
models: ["claude-sonnet-5", "claude-opus-5", "claude-fable-5"],
|
|
232
335
|
// The beta flag is what switches the API into OAuth-token mode (measured
|
|
233
336
|
// by tabrunner's Claude plan sign-in).
|
|
234
337
|
headers: { "anthropic-beta": "claude-code-20250219,oauth-2025-04-20" },
|
|
@@ -240,7 +343,7 @@ export const PROVIDER_PRESETS = {
|
|
|
240
343
|
baseUrl: "https://chatgpt.com/backend-api/codex",
|
|
241
344
|
auth: "oauth",
|
|
242
345
|
defaultModel: "gpt-5.3-codex",
|
|
243
|
-
models: ["gpt-5.3-codex", "gpt-5.5", "gpt-5.4-mini"],
|
|
346
|
+
models: ["gpt-5.3-codex", "gpt-5.3-codex-spark", "gpt-5.5", "gpt-5.4-mini"],
|
|
244
347
|
},
|
|
245
348
|
/** Kimi's coding endpoint reached with a subscription token instead of a
|
|
246
349
|
* key — Kimi bills the two separately. */
|
|
@@ -255,7 +358,7 @@ export const PROVIDER_PRESETS = {
|
|
|
255
358
|
baseUrl: "https://api.githubcopilot.com",
|
|
256
359
|
auth: "oauth",
|
|
257
360
|
defaultModel: "gpt-5.6-sol",
|
|
258
|
-
models: ["gpt-5.6-sol", "claude-opus-5", "claude-
|
|
361
|
+
models: ["gpt-5.6-sol", "claude-opus-5", "claude-sonnet-5", "gpt-6-astra"],
|
|
259
362
|
},
|
|
260
363
|
} as const satisfies Record<string, ProviderPreset>;
|
|
261
364
|
|
package/src/providers/factory.ts
CHANGED
|
@@ -91,6 +91,7 @@ export function createPresetProvider(id: ProviderPresetId, config: PresetProvide
|
|
|
91
91
|
effort: config.effort,
|
|
92
92
|
maxTokens: config.maxTokens,
|
|
93
93
|
baseUrl: preset.baseUrl,
|
|
94
|
+
...(preset.path ? { path: preset.path } : {}),
|
|
94
95
|
headers,
|
|
95
96
|
...(config.providerOrder ? { providerOrder: config.providerOrder } : {}),
|
|
96
97
|
fetchImpl: config.fetchImpl,
|
package/src/providers/openai.ts
CHANGED
|
@@ -402,8 +402,12 @@ export function createOpenAIProvider(config: OpenAIConfig): Provider {
|
|
|
402
402
|
request.provider = { order: config.providerOrder, allow_fallbacks: true };
|
|
403
403
|
}
|
|
404
404
|
|
|
405
|
+
const defaultPath = /\/v\d+[^/]*$/i.test(baseUrl.replace(/\/+$/, ""))
|
|
406
|
+
? "/chat/completions"
|
|
407
|
+
: "/v1/chat/completions";
|
|
408
|
+
|
|
405
409
|
for await (const data of streamSse({
|
|
406
|
-
url: apiUrl(baseUrl, config.path ??
|
|
410
|
+
url: apiUrl(baseUrl, config.path ?? defaultPath),
|
|
407
411
|
headers: { authorization: `Bearer ${config.apiKey}`, ...config.headers },
|
|
408
412
|
body: request,
|
|
409
413
|
provider: id,
|
package/src/rate-limit.ts
CHANGED
|
@@ -39,6 +39,7 @@ export interface RateLimitReset {
|
|
|
39
39
|
}
|
|
40
40
|
|
|
41
41
|
const RETRY_AFTER = "retry-after";
|
|
42
|
+
const RETRY_AFTER_MS = "retry-after-ms";
|
|
42
43
|
const UNIFIED_5H = "anthropic-ratelimit-unified-5h-";
|
|
43
44
|
const UNIFIED_7D = "anthropic-ratelimit-unified-7d-";
|
|
44
45
|
|
|
@@ -74,27 +75,50 @@ function unixSecondsToMs(value: string | null): number | undefined {
|
|
|
74
75
|
return n === undefined ? undefined : n * 1000;
|
|
75
76
|
}
|
|
76
77
|
|
|
78
|
+
/** Matches Go-style duration strings like "2m59.56s", "7.66s", "500ms", "1h30m". */
|
|
79
|
+
const DURATION_PATTERN =
|
|
80
|
+
/^(?:(\d+(?:\.\d+)?)h)?(?:(\d+(?:\.\d+)?)m(?!s))?(?:(\d+(?:\.\d+)?)m?s)?$/i;
|
|
81
|
+
|
|
82
|
+
function parseDurationMs(text: string): number | undefined {
|
|
83
|
+
const match = DURATION_PATTERN.exec(text.trim());
|
|
84
|
+
if (!match || !match.slice(1).some(Boolean)) return undefined;
|
|
85
|
+
const hours = Number(match[1]) || 0;
|
|
86
|
+
const minutes = Number(match[2]) || 0;
|
|
87
|
+
const seconds = Number(match[3]) || 0;
|
|
88
|
+
const isMs = text.toLowerCase().endsWith("ms");
|
|
89
|
+
const secMs = isMs ? seconds : seconds * 1000;
|
|
90
|
+
const total = Math.round(hours * 3_600_000 + minutes * 60_000 + secMs);
|
|
91
|
+
return total >= 0 ? total : undefined;
|
|
92
|
+
}
|
|
93
|
+
|
|
77
94
|
/** Absolute reset time from a header that spells it either way: epoch seconds
|
|
78
|
-
* (the unified reset, most gateways)
|
|
79
|
-
* API-key reset headers).
|
|
80
|
-
* is not a rejection — it is the year 120, two millennia in the past. */
|
|
95
|
+
* (the unified reset, most gateways), Go-style duration ("2m59s" from OpenAI),
|
|
96
|
+
* or an RFC 3339 / HTTP-date string (the API-key reset headers). */
|
|
81
97
|
function resetHeaderMs(value: string | null, now: number): number | undefined {
|
|
82
98
|
if (!value) return undefined;
|
|
83
|
-
|
|
84
|
-
|
|
99
|
+
const trimmed = value.trim();
|
|
100
|
+
if (/^\d+$/.test(trimmed)) {
|
|
101
|
+
const seconds = Number(trimmed);
|
|
85
102
|
return seconds > EPOCH_SECONDS_FLOOR ? seconds * 1000 : now + seconds * 1000;
|
|
86
103
|
}
|
|
87
|
-
const
|
|
104
|
+
const duration = parseDurationMs(trimmed);
|
|
105
|
+
if (duration !== undefined) {
|
|
106
|
+
return now + duration;
|
|
107
|
+
}
|
|
108
|
+
const at = Date.parse(trimmed);
|
|
88
109
|
return Number.isNaN(at) ? undefined : at;
|
|
89
110
|
}
|
|
90
111
|
|
|
91
|
-
/** `retry-after` is delay-seconds,
|
|
92
|
-
* and
|
|
112
|
+
/** `retry-after` is delay-seconds, Go-style duration ("60s"), or an absolute
|
|
113
|
+
* HTTP date, and all three occur across providers this package speaks to. */
|
|
93
114
|
function parseRetryAfter(value: string | null, now: number): number | undefined {
|
|
94
115
|
if (!value) return undefined;
|
|
95
|
-
const
|
|
116
|
+
const trimmed = value.trim();
|
|
117
|
+
const n = Number(trimmed);
|
|
96
118
|
if (Number.isFinite(n) && n >= 0) return n * 1000;
|
|
97
|
-
const
|
|
119
|
+
const duration = parseDurationMs(trimmed);
|
|
120
|
+
if (duration !== undefined) return duration;
|
|
121
|
+
const at = Date.parse(trimmed);
|
|
98
122
|
return Number.isNaN(at) ? undefined : Math.max(0, at - now);
|
|
99
123
|
}
|
|
100
124
|
|
|
@@ -110,7 +134,10 @@ function parseRetryAfter(value: string | null, now: number): number | undefined
|
|
|
110
134
|
export function parseRateLimitReset(headers: Headers, now = Date.now()): RateLimitReset {
|
|
111
135
|
const result: RateLimitReset = {};
|
|
112
136
|
|
|
113
|
-
|
|
137
|
+
// `retry-after-ms` is Anthropic's millisecond spelling of the same header;
|
|
138
|
+
// when both ride a response the coarser one is the same deadline rounded.
|
|
139
|
+
const afterMs = numeric(headers.get(RETRY_AFTER_MS));
|
|
140
|
+
const after = afterMs ?? parseRetryAfter(headers.get(RETRY_AFTER), now);
|
|
114
141
|
if (after !== undefined) {
|
|
115
142
|
result.retryAfterMs = after;
|
|
116
143
|
result.resetAtMs = now + after;
|
|
@@ -176,9 +203,31 @@ function jsonSeconds(value: unknown): number | undefined {
|
|
|
176
203
|
return Number.isFinite(n) ? n : undefined;
|
|
177
204
|
}
|
|
178
205
|
|
|
206
|
+
const PROSE_RETRY_PATTERNS = [
|
|
207
|
+
/try again in (\d+(?:\.\d+)?)\s*(s(?:ec(?:ond)?s?)?|m(?:in(?:ute)?s?)?|h(?:(?:ou)?rs?)?)/i,
|
|
208
|
+
/retry after (\d+(?:\.\d+)?)\s*(s(?:ec(?:ond)?s?)?|m(?:in(?:ute)?s?)?|h(?:(?:ou)?rs?)?)/i,
|
|
209
|
+
/resets? (?:in|after) (\d+(?:\.\d+)?)\s*(s(?:ec(?:ond)?s?)?|m(?:in(?:ute)?s?)?|h(?:(?:ou)?rs?)?)/i,
|
|
210
|
+
/retry[- ]after[:\s]+(\d+(?:\.\d+)?)\s*(s(?:ec(?:ond)?s?)?|m(?:in(?:ute)?s?)?|h(?:(?:ou)?rs?)?)?/i,
|
|
211
|
+
];
|
|
212
|
+
|
|
213
|
+
function parseProseRetryMs(text: string): number | undefined {
|
|
214
|
+
for (const pattern of PROSE_RETRY_PATTERNS) {
|
|
215
|
+
const match = text.match(pattern);
|
|
216
|
+
if (!match?.[1]) continue;
|
|
217
|
+
const val = Number.parseFloat(match[1]);
|
|
218
|
+
if (!Number.isFinite(val) || val <= 0) continue;
|
|
219
|
+
const unit = (match[2] ?? "s").toLowerCase();
|
|
220
|
+
const multiplier = unit.startsWith("h") ? 3_600_000 : unit.startsWith("m") ? 60_000 : 1_000;
|
|
221
|
+
return Math.ceil(val * multiplier);
|
|
222
|
+
}
|
|
223
|
+
return undefined;
|
|
224
|
+
}
|
|
225
|
+
|
|
179
226
|
/**
|
|
180
227
|
* ChatGPT's codex backend puts the reset IN THE 429 BODY, not the headers:
|
|
181
228
|
* `{"error":{"type":"usage_limit_reached","resets_at":1788801754,"resets_in_seconds":2501465}}`.
|
|
229
|
+
* Other providers and gateways (LiteLLM, Cloudflare, Ollama) report countdowns in prose:
|
|
230
|
+
* `Rate limit exceeded. Try again in 20s.`
|
|
182
231
|
* The window name is inferred from the wait itself, and only past the floor
|
|
183
232
|
* above — a sub-minute retry is a throttle, not a subscription window.
|
|
184
233
|
*/
|
|
@@ -187,33 +236,46 @@ export function parseUsageLimitBody(bodyText: string, now = Date.now()): RateLim
|
|
|
187
236
|
try {
|
|
188
237
|
body = JSON.parse(bodyText);
|
|
189
238
|
} catch {
|
|
190
|
-
|
|
239
|
+
body = undefined;
|
|
191
240
|
}
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
241
|
+
|
|
242
|
+
if (typeof body === "object" && body !== null) {
|
|
243
|
+
const source = (body as Record<string, unknown>).error;
|
|
244
|
+
const error =
|
|
245
|
+
typeof source === "object" && source !== null
|
|
246
|
+
? (source as Record<string, unknown>)
|
|
247
|
+
: (body as Record<string, unknown>);
|
|
248
|
+
|
|
249
|
+
const inSeconds = jsonSeconds(error.resets_in_seconds);
|
|
250
|
+
const atSeconds = jsonSeconds(error.resets_at);
|
|
251
|
+
const waitMs =
|
|
252
|
+
inSeconds !== undefined && inSeconds >= 0
|
|
253
|
+
? inSeconds * 1000
|
|
254
|
+
: atSeconds !== undefined && atSeconds > 0
|
|
255
|
+
? Math.max(0, atSeconds * 1000 - now)
|
|
256
|
+
: undefined;
|
|
257
|
+
if (waitMs !== undefined) {
|
|
258
|
+
const result: RateLimitReset = { retryAfterMs: waitMs, resetAtMs: now + waitMs };
|
|
259
|
+
if (waitMs > WINDOW_FLOOR_MS) {
|
|
260
|
+
result.window =
|
|
261
|
+
waitMs <= FIVE_HOUR_MAX_MS ? "5h" : waitMs <= WEEKLY_MAX_MS ? "weekly" : "monthly";
|
|
262
|
+
}
|
|
263
|
+
return result;
|
|
264
|
+
}
|
|
215
265
|
}
|
|
216
|
-
|
|
266
|
+
|
|
267
|
+
// If structured JSON did not yield a reset, check prose wording
|
|
268
|
+
const waitMs = parseProseRetryMs(bodyText);
|
|
269
|
+
if (waitMs !== undefined) {
|
|
270
|
+
const result: RateLimitReset = { retryAfterMs: waitMs, resetAtMs: now + waitMs };
|
|
271
|
+
if (waitMs > WINDOW_FLOOR_MS) {
|
|
272
|
+
result.window =
|
|
273
|
+
waitMs <= FIVE_HOUR_MAX_MS ? "5h" : waitMs <= WEEKLY_MAX_MS ? "weekly" : "monthly";
|
|
274
|
+
}
|
|
275
|
+
return result;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
return {};
|
|
217
279
|
}
|
|
218
280
|
|
|
219
281
|
/** Read both places an endpoint can report its reset. A short Retry-After is
|