@providerkit/core 0.10.0 → 0.11.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/attribution.d.ts +9 -0
- package/dist/attribution.d.ts.map +1 -0
- package/dist/attribution.js +23 -0
- package/dist/attribution.js.map +1 -0
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js.map +1 -1
- package/dist/fallback.d.ts.map +1 -1
- package/dist/fallback.js +5 -1
- package/dist/fallback.js.map +1 -1
- 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/presets.d.ts.map +1 -1
- package/dist/presets.js +10 -2
- package/dist/presets.js.map +1 -1
- package/dist/providers/anthropic.d.ts +11 -0
- package/dist/providers/anthropic.d.ts.map +1 -1
- package/dist/providers/anthropic.js +2 -0
- package/dist/providers/anthropic.js.map +1 -1
- package/dist/providers/factory.d.ts +10 -0
- package/dist/providers/factory.d.ts.map +1 -1
- package/dist/providers/factory.js.map +1 -1
- package/dist/providers/openai.d.ts +28 -18
- package/dist/providers/openai.d.ts.map +1 -1
- package/dist/providers/openai.js +35 -43
- package/dist/providers/openai.js.map +1 -1
- package/dist/retry.d.ts.map +1 -1
- package/dist/retry.js.map +1 -1
- package/package.json +1 -1
- package/src/attribution.ts +44 -0
- package/src/capability.ts +0 -1
- package/src/errors.ts +8 -2
- package/src/fallback.ts +50 -22
- package/src/index.ts +1 -0
- package/src/presets.ts +10 -2
- package/src/providers/anthropic.ts +13 -0
- package/src/providers/factory.ts +10 -0
- package/src/providers/openai.ts +46 -43
- package/src/retry.ts +4 -3
package/src/providers/openai.ts
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
// Their divergences are small and named where they appear.
|
|
6
6
|
import { streamError } from "../errors.ts";
|
|
7
7
|
import { streamSse, apiUrl } from "../transport.ts";
|
|
8
|
+
import { attributionHeaders } from "../attribution.ts";
|
|
8
9
|
import type {
|
|
9
10
|
ChatMessage,
|
|
10
11
|
ContentPart,
|
|
@@ -75,6 +76,17 @@ export interface OpenAIConfig extends ProviderFallbackConfig {
|
|
|
75
76
|
maxTokens?: number;
|
|
76
77
|
fetchImpl?: typeof fetch;
|
|
77
78
|
headers?: Record<string, string>;
|
|
79
|
+
/**
|
|
80
|
+
* App attribution for OpenRouter's rankings — the site URL rides as
|
|
81
|
+
* `HTTP-Referer`, the app name as `X-Title`. Public, not secret. Sent
|
|
82
|
+
* whenever set (other vendors ignore unknown headers); an explicit entry
|
|
83
|
+
* in `headers` always wins.
|
|
84
|
+
*/
|
|
85
|
+
siteUrl?: string;
|
|
86
|
+
/**
|
|
87
|
+
* App name for OpenRouter's rankings, sent as `X-Title`. See `siteUrl`.
|
|
88
|
+
*/
|
|
89
|
+
siteName?: string;
|
|
78
90
|
/**
|
|
79
91
|
* Pin OpenRouter to preferred upstream hosts so the PROMPT CACHE stays warm
|
|
80
92
|
* across rounds. The cache lives on the upstream host's account and default
|
|
@@ -142,23 +154,20 @@ export type EffortDialect = "openai" | "openrouter" | "deepseek" | "off";
|
|
|
142
154
|
*
|
|
143
155
|
* One knob, three incompatible spellings, and the differences are not cosmetic:
|
|
144
156
|
*
|
|
145
|
-
* - **On OpenRouter,
|
|
146
|
-
* `reasoning
|
|
147
|
-
*
|
|
148
|
-
*
|
|
149
|
-
*
|
|
150
|
-
*
|
|
151
|
-
*
|
|
152
|
-
*
|
|
153
|
-
*
|
|
154
|
-
*
|
|
155
|
-
*
|
|
156
|
-
* mandatory, `none` is refused outright and the whole call is lost.
|
|
157
|
-
* The honest way to ask for the least thinking on this dialect is to pass no
|
|
158
|
-
* effort at all — see `effortParams`, where the numbers live.
|
|
157
|
+
* - **On OpenRouter, GLM's off switch is the ABSENT field.** Not setting
|
|
158
|
+
* `reasoning` at all disables thinking for GLM 5.3 Flash (measured live
|
|
159
|
+
* 2026-09-14). Naming a level raises it — the accepted values are
|
|
160
|
+
* `low`, `high` and `max` (`medium` and `xhigh` are accepted too, but our
|
|
161
|
+
* vocabulary has no use for them). `none` is NOT sendable there: the model
|
|
162
|
+
* answers
|
|
163
|
+
* `400 "Reasoning is mandatory for this endpoint and cannot be disabled."`
|
|
164
|
+
* to `reasoning.effort: "none"` exactly as it does to
|
|
165
|
+
* `reasoning.enabled: false` — so "do not think" on this dialect is said by
|
|
166
|
+
* omitting the field, never by naming a level. See `effortParams`, where the
|
|
167
|
+
* numbers live.
|
|
159
168
|
* - **DeepSeek V4 defaults thinking ON**, so `none` has to be an explicit
|
|
160
|
-
* refusal. That is the case proving OpenRouter's
|
|
161
|
-
* not a preference for always thinking.
|
|
169
|
+
* refusal. That is the case proving OpenRouter's absent-field off switch is
|
|
170
|
+
* a real dialect fact and not a preference for always thinking.
|
|
162
171
|
* - **OpenAI** takes `reasoning_effort`, and `none` is one of its values — not
|
|
163
172
|
* the absence of the field. GPT-5.1 both accepted `none` and made it the
|
|
164
173
|
* default; everything from GPT-5 back still defaults to `medium`. So sending
|
|
@@ -168,8 +177,10 @@ export type EffortDialect = "openai" | "openrouter" | "deepseek" | "off";
|
|
|
168
177
|
* empty with `finish_reason: "length"`.
|
|
169
178
|
*
|
|
170
179
|
* An absent effort sends nothing on every dialect: the seam's rule is that a
|
|
171
|
-
* knob the caller never touched is a knob the provider still owns.
|
|
172
|
-
*
|
|
180
|
+
* knob the caller never touched is a knob the provider still owns. On
|
|
181
|
+
* OpenRouter that makes `none` and "never asked" the same request — on this
|
|
182
|
+
* dialect, not thinking IS the default state, so there is no third spelling
|
|
183
|
+
* to invent.
|
|
173
184
|
*/
|
|
174
185
|
export function effortParams(
|
|
175
186
|
dialect: EffortDialect,
|
|
@@ -189,31 +200,19 @@ export function effortParams(
|
|
|
189
200
|
? { thinking: { type: "enabled" } }
|
|
190
201
|
: { thinking: { type: "enabled" }, reasoning_effort: level };
|
|
191
202
|
case "openrouter":
|
|
192
|
-
//
|
|
193
|
-
//
|
|
194
|
-
//
|
|
195
|
-
//
|
|
196
|
-
//
|
|
197
|
-
//
|
|
198
|
-
//
|
|
199
|
-
//
|
|
200
|
-
// to `reasoning.effort: "none"` exactly as it does to
|
|
201
|
-
// `reasoning.enabled: false` — so on a model that must think, `none` is a
|
|
202
|
-
// refusal and not a floor. Callers that mean "as little as possible"
|
|
203
|
-
// should pass no effort at all rather than `none`, which is why the
|
|
204
|
-
// absent-effort branch above stays load-bearing.
|
|
205
|
-
//
|
|
206
|
-
// Same measurement, same model, one structured-output call three ways:
|
|
207
|
-
//
|
|
208
|
-
// no reasoning field 4,1s · 1 reasoning token
|
|
209
|
-
// effort "minimal" 46,6s · 474 reasoning tokens
|
|
210
|
-
// max_tokens 2000 30,3s · 484 reasoning tokens
|
|
203
|
+
// GLM's off switch here is the ABSENT field (measured live 2026-09-14:
|
|
204
|
+
// omit `reasoning` → thinking disabled for glm-5.3-flash), so `none`
|
|
205
|
+
// sends nothing — the same request as a caller who never asked, which on
|
|
206
|
+
// this dialect is the same state. Naming a level RAISES it: the accepted
|
|
207
|
+
// values are low, high and max (`medium` and `xhigh` exist on the wire
|
|
208
|
+
// The refusal that once looked like "OpenRouter cannot be told not to
|
|
209
|
+
// think" was `reasoning.enabled: false` — a different field, never sent
|
|
210
|
+
// (pinned by the test below).
|
|
211
211
|
//
|
|
212
|
-
//
|
|
213
|
-
//
|
|
214
|
-
//
|
|
215
|
-
|
|
216
|
-
return { reasoning: { effort: effort === "max" ? "xhigh" : (level ?? "none") } };
|
|
212
|
+
// `max` rides verbatim here — the enum clamp below is DeepSeek/OpenAI's
|
|
213
|
+
// shape (DeepSeek auto-bumps past its own top, OpenAI's top IS high);
|
|
214
|
+
// OpenRouter takes `max` as named (measured live 2026-09-14).
|
|
215
|
+
return effort === "none" ? {} : { reasoning: { effort } };
|
|
217
216
|
case "openai":
|
|
218
217
|
return { reasoning_effort: level ?? "none" };
|
|
219
218
|
case "off":
|
|
@@ -464,7 +463,11 @@ export function createOpenAIProvider(config: OpenAIConfig): Provider {
|
|
|
464
463
|
|
|
465
464
|
for await (const data of streamSse({
|
|
466
465
|
url: apiUrl(baseUrl, config.path ?? defaultPath),
|
|
467
|
-
headers: {
|
|
466
|
+
headers: {
|
|
467
|
+
authorization: `Bearer ${config.apiKey}`,
|
|
468
|
+
...attributionHeaders(config, config.headers),
|
|
469
|
+
...config.headers,
|
|
470
|
+
},
|
|
468
471
|
body: request,
|
|
469
472
|
provider: id,
|
|
470
473
|
...(opts.signal ? { signal: opts.signal } : {}),
|
package/src/retry.ts
CHANGED
|
@@ -79,9 +79,10 @@ export function sleep(ms: number, signal?: AbortSignal): Promise<void> {
|
|
|
79
79
|
function delayFor(err: unknown, attempt: number, opts: RetryOptions): number | null {
|
|
80
80
|
const asked = parseRetryAfterMs(err);
|
|
81
81
|
const cap = opts.maxDelayMs ?? DEFAULT_MAX_DELAY_MS;
|
|
82
|
-
const resetWait =
|
|
83
|
-
|
|
84
|
-
|
|
82
|
+
const resetWait =
|
|
83
|
+
err instanceof ProviderError && err.resetAtMs !== undefined
|
|
84
|
+
? Math.max(0, err.resetAtMs - Date.now())
|
|
85
|
+
: undefined;
|
|
85
86
|
if (asked !== undefined || resetWait !== undefined) {
|
|
86
87
|
const wait = Math.max(asked ?? 0, resetWait ?? 0);
|
|
87
88
|
return wait > cap ? null : wait;
|