@providerkit/core 0.21.0 → 0.22.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/README.md +7 -7
- package/dist/attribution.d.ts +2 -2
- package/dist/capability.d.ts +9 -9
- package/dist/capability.js +9 -9
- package/dist/capability.js.map +1 -1
- package/dist/context.d.ts +4 -4
- package/dist/context.js +4 -4
- package/dist/errors.d.ts +15 -15
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +14 -14
- package/dist/image.d.ts +1 -1
- package/dist/image.d.ts.map +1 -1
- package/dist/jev.d.ts +9 -9
- package/dist/jev.d.ts.map +1 -1
- package/dist/jev.js +1 -1
- package/dist/key-pool.d.ts +4 -4
- package/dist/key-pool.js +6 -6
- package/dist/key-pool.js.map +1 -1
- package/dist/presets.d.ts +26 -22
- package/dist/presets.d.ts.map +1 -1
- package/dist/presets.js +13 -12
- package/dist/presets.js.map +1 -1
- package/dist/providers/anthropic.d.ts +5 -5
- package/dist/providers/anthropic.d.ts.map +1 -1
- package/dist/providers/anthropic.js +9 -9
- package/dist/providers/factory.d.ts +2 -2
- package/dist/providers/factory.d.ts.map +1 -1
- package/dist/providers/factory.js +2 -1
- package/dist/providers/factory.js.map +1 -1
- package/dist/providers/gemini.d.ts +2 -2
- package/dist/providers/gemini.d.ts.map +1 -1
- package/dist/providers/gemini.js +15 -14
- package/dist/providers/gemini.js.map +1 -1
- package/dist/providers/openai.d.ts +12 -9
- package/dist/providers/openai.d.ts.map +1 -1
- package/dist/providers/openai.js +14 -13
- package/dist/providers/openai.js.map +1 -1
- package/dist/providers/responses.d.ts +3 -3
- package/dist/providers/responses.js +5 -5
- package/dist/providers/responses.js.map +1 -1
- package/dist/providers/zai.d.ts +2 -2
- package/dist/providers/zai.js +2 -2
- package/dist/rate-limit.d.ts +3 -3
- package/dist/rate-limit.js +3 -3
- package/dist/retry.d.ts +5 -5
- package/dist/retry.js +4 -4
- package/dist/schema.d.ts +6 -6
- package/dist/schema.js +8 -8
- package/dist/tool-args.d.ts +1 -1
- package/dist/tool-args.js +2 -2
- package/dist/tools.d.ts +3 -3
- package/dist/tools.js +2 -2
- package/dist/tools.js.map +1 -1
- package/dist/transport.d.ts +7 -7
- package/dist/transport.js +6 -6
- package/dist/types.d.ts +24 -23
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +3 -3
- package/dist/types.js.map +1 -1
- package/dist/usage.d.ts +4 -4
- package/dist/usage.d.ts.map +1 -1
- package/dist/usage.js +3 -3
- package/dist/usage.js.map +1 -1
- package/dist/watchdog.d.ts +12 -12
- package/dist/watchdog.js +10 -10
- package/dist/zod.d.ts +4 -4
- package/dist/zod.js +3 -3
- package/package.json +1 -1
- package/src/attribution.ts +2 -2
- package/src/capability.ts +12 -12
- package/src/context.ts +4 -4
- package/src/errors.ts +18 -18
- package/src/image.ts +1 -1
- package/src/jev.ts +11 -11
- package/src/key-pool.ts +7 -7
- package/src/presets.ts +26 -22
- package/src/providers/anthropic.ts +14 -14
- package/src/providers/factory.ts +4 -3
- package/src/providers/gemini.ts +19 -16
- package/src/providers/openai.ts +22 -17
- package/src/providers/responses.ts +11 -11
- package/src/providers/zai.ts +2 -2
- package/src/rate-limit.ts +4 -4
- package/src/retry.ts +5 -5
- package/src/schema.ts +8 -8
- package/src/tool-args.ts +2 -2
- package/src/tools.ts +4 -4
- package/src/transport.ts +7 -7
- package/src/types.ts +24 -23
- package/src/usage.ts +5 -5
- package/src/watchdog.ts +13 -13
- package/src/zod.ts +4 -4
package/src/providers/openai.ts
CHANGED
|
@@ -31,7 +31,7 @@ export interface OpenAIConfig extends ProviderFallbackConfig {
|
|
|
31
31
|
* /chat/completions when the base already includes the API version, such as
|
|
32
32
|
* a gateway using /v4 rather than /v1. */
|
|
33
33
|
path?: string;
|
|
34
|
-
/** Names the provider in errors and logs
|
|
34
|
+
/** Names the provider in errors and logs ("openrouter", "deepseek", …). It
|
|
35
35
|
* also picks the effort dialect below, unless `effortDialect` overrides. */
|
|
36
36
|
id?: string;
|
|
37
37
|
effort?: Effort;
|
|
@@ -50,7 +50,7 @@ export interface OpenAIConfig extends ProviderFallbackConfig {
|
|
|
50
50
|
/**
|
|
51
51
|
* How a json request rides when the SAME call also carries tools.
|
|
52
52
|
*
|
|
53
|
-
* `response_format`
|
|
53
|
+
* `response_format` (the default) sends both, which is what the shape
|
|
54
54
|
* documents and what most models honour. `prompt` drops the response format
|
|
55
55
|
* from those calls only and sends the schema as prompt instead: the shape the
|
|
56
56
|
* Anthropic adapter has always used, and the reason this failure cannot
|
|
@@ -58,7 +58,7 @@ export interface OpenAIConfig extends ProviderFallbackConfig {
|
|
|
58
58
|
*
|
|
59
59
|
* It needs a knob because a model that cannot serve both does not say so. Its
|
|
60
60
|
* decoder is pinned to the schema, the tool call has nowhere to go, and the
|
|
61
|
-
* model writes the announcement instead
|
|
61
|
+
* model writes the announcement instead ("let me look that up for you") and
|
|
62
62
|
* the turn ends looking like a model with no initiative rather than a request
|
|
63
63
|
* that made the call impossible. Nothing is logged, because nothing failed.
|
|
64
64
|
*
|
|
@@ -70,7 +70,7 @@ export interface OpenAIConfig extends ProviderFallbackConfig {
|
|
|
70
70
|
* No default is right for everyone, which is why this stays a setting rather
|
|
71
71
|
* than becoming a rule: the same run put `qwen3.8-flash` at 1/6 with the
|
|
72
72
|
* schema in the prompt against 6/6 with the response format. Don't guess it
|
|
73
|
-
* from the model card
|
|
73
|
+
* from the model card. `probeJsonWithTools` asks the model itself, in one
|
|
74
74
|
* call. `StreamOptions.jsonWithTools` overrides this per call.
|
|
75
75
|
*/
|
|
76
76
|
jsonWithTools?: JsonWithTools;
|
|
@@ -78,7 +78,7 @@ export interface OpenAIConfig extends ProviderFallbackConfig {
|
|
|
78
78
|
fetchImpl?: typeof fetch;
|
|
79
79
|
headers?: Record<string, string>;
|
|
80
80
|
/**
|
|
81
|
-
* App attribution for OpenRouter's rankings
|
|
81
|
+
* App attribution for OpenRouter's rankings. The site URL rides as
|
|
82
82
|
* `HTTP-Referer`, the app name as `X-Title`. Public, not secret. Sent
|
|
83
83
|
* whenever set (other vendors ignore unknown headers); an explicit entry
|
|
84
84
|
* in `headers` always wins.
|
|
@@ -91,7 +91,7 @@ export interface OpenAIConfig extends ProviderFallbackConfig {
|
|
|
91
91
|
/**
|
|
92
92
|
* Pin OpenRouter to preferred upstream hosts so the PROMPT CACHE stays warm
|
|
93
93
|
* across rounds. The cache lives on the upstream host's account and default
|
|
94
|
-
* routing hops between them
|
|
94
|
+
* routing hops between them. Every hop is a cold cache, with worse latency
|
|
95
95
|
* and higher effective input cost. Fallbacks stay on: this is a preference,
|
|
96
96
|
* not a lock.
|
|
97
97
|
*/
|
|
@@ -109,6 +109,9 @@ export interface OpenAIConfig extends ProviderFallbackConfig {
|
|
|
109
109
|
* `agent`. GitHub Copilot bills a premium request per `user` turn and lets
|
|
110
110
|
* the agent's own follow-ups (tool results) ride free. */
|
|
111
111
|
initiatorHeader?: boolean;
|
|
112
|
+
/** Never send `temperature` or `top_p`. For an endpoint that fixes its own
|
|
113
|
+
* sampling and rejects the fields (Gemini's OpenAI-compatible route). */
|
|
114
|
+
noSampling?: boolean;
|
|
112
115
|
/** Default `service_tier` for every call; `StreamOptions.serviceTier` wins. */
|
|
113
116
|
serviceTier?: ServiceTier;
|
|
114
117
|
}
|
|
@@ -215,14 +218,14 @@ const OPENROUTER_NONE: ReadonlyMap<string, "low" | "minimal" | "none"> = new Map
|
|
|
215
218
|
* MiMo v2.6 Flash takes one, and DeepSeek V4 Flash thinks at `low` and not
|
|
216
219
|
* with the field omitted. So `none` is said per model, from
|
|
217
220
|
* `OPENROUTER_NONE`, which also says why an unlisted model gets nothing.
|
|
218
|
-
* Naming a level raises it
|
|
221
|
+
* Naming a level raises it. The accepted values are `low`, `high` and
|
|
219
222
|
* `max` (`medium` and `xhigh` are accepted too, but our vocabulary has no
|
|
220
223
|
* use for them).
|
|
221
224
|
* - **DeepSeek V4 defaults thinking ON** on its own API, so `none` has to be an
|
|
222
225
|
* explicit refusal there. The same word is a different request on each
|
|
223
226
|
* dialect, and on OpenRouter on each model: the spelling belongs to the
|
|
224
227
|
* endpoint, never to the caller.
|
|
225
|
-
* - **OpenAI** takes `reasoning_effort`, and `none` is one of its values
|
|
228
|
+
* - **OpenAI** takes `reasoning_effort`, and `none` is one of its values, not
|
|
226
229
|
* the absence of the field. GPT-5.1 both accepted `none` and made it the
|
|
227
230
|
* default; everything from GPT-5 back still defaults to `medium`. So sending
|
|
228
231
|
* nothing is NOT a way to say "do not think": on every model released before
|
|
@@ -286,11 +289,12 @@ export function effortParams(
|
|
|
286
289
|
/**
|
|
287
290
|
* OpenCode Go's chat models each take a different set of `reasoning_effort`
|
|
288
291
|
* values (cc-proxy's table, measured on live traffic). A level a model can't
|
|
289
|
-
* take is
|
|
290
|
-
*
|
|
292
|
+
* take is raised to the least it offers (GLM, DeepSeek) or refused (MiMo
|
|
293
|
+
* `max`), rather than sent as a 400. A refusal is `invalid`, which stops the
|
|
294
|
+
* whole fallback chain, so refuse only where there is nothing sensible to send.
|
|
291
295
|
*
|
|
292
296
|
* - GLM 5.2 and 5.3 take only `high` and `max`, with no way to turn thinking
|
|
293
|
-
* off. `none`
|
|
297
|
+
* off. `none`, `low` and `medium` get `high`, the least they offer.
|
|
294
298
|
* - DeepSeek V4 takes `low` to `max`. `none` gets `low`, its least.
|
|
295
299
|
* - MiMo takes `low`, `medium` and `high`, and `none` (measured 2026-09-27 on
|
|
296
300
|
* `mimo-v2.6-flash`; cc-proxy maps it to `low` instead).
|
|
@@ -307,8 +311,7 @@ function goEffortParams(effort: Effort, model = ""): Record<string, unknown> {
|
|
|
307
311
|
};
|
|
308
312
|
const level = (value: string) => ({ reasoning_effort: value });
|
|
309
313
|
if (/glm-5[-.p]?[23]/.test(id)) {
|
|
310
|
-
|
|
311
|
-
return effort === "max" ? level("max") : refuse("high or max");
|
|
314
|
+
return level(effort === "max" ? "max" : "high");
|
|
312
315
|
}
|
|
313
316
|
if (id.includes("deepseek-v4")) return level(effort === "none" ? "low" : effort);
|
|
314
317
|
if (id.includes("mimo"))
|
|
@@ -351,7 +354,7 @@ function partsToOpenAI(content: string | ContentPart[], provider: string): unkno
|
|
|
351
354
|
}
|
|
352
355
|
|
|
353
356
|
/**
|
|
354
|
-
* Assistant turns carry `reasoning_content` when the history has it
|
|
357
|
+
* Assistant turns carry `reasoning_content` when the history has it. Thinking
|
|
355
358
|
* providers require the prior turn's chain-of-thought replayed on a turn that
|
|
356
359
|
* made a tool call. A caller running a turn with thinking OFF must strip it
|
|
357
360
|
* first (`stripReasoning`); the two cannot be mixed.
|
|
@@ -454,7 +457,7 @@ interface OpenAIChunk {
|
|
|
454
457
|
is_byok?: boolean;
|
|
455
458
|
cost_details?: { upstream_inference_cost?: unknown } | null;
|
|
456
459
|
} | null;
|
|
457
|
-
/** Present only on the in-band failure below
|
|
460
|
+
/** Present only on the in-band failure below. Never beside a choice.
|
|
458
461
|
* `code` is the numeric HTTP status on the gateways, a slug on OpenAI. */
|
|
459
462
|
error?: { message?: string; code?: string | number; type?: string };
|
|
460
463
|
}
|
|
@@ -527,8 +530,10 @@ export function createOpenAIProvider(config: OpenAIConfig): Provider {
|
|
|
527
530
|
};
|
|
528
531
|
const maxTokens = opts.maxTokens ?? config.maxTokens;
|
|
529
532
|
if (maxTokens !== undefined) request.max_tokens = maxTokens;
|
|
530
|
-
if (
|
|
531
|
-
|
|
533
|
+
if (!config.noSampling) {
|
|
534
|
+
if (opts.temperature !== undefined) request.temperature = opts.temperature;
|
|
535
|
+
if (opts.topP !== undefined) request.top_p = opts.topP;
|
|
536
|
+
}
|
|
532
537
|
if (opts.stopSequences?.length) request.stop = opts.stopSequences;
|
|
533
538
|
const serviceTier = opts.serviceTier ?? config.serviceTier;
|
|
534
539
|
if (serviceTier) request.service_tier = serviceTier;
|
|
@@ -39,7 +39,7 @@ export interface ResponsesConfig extends ProviderFallbackConfig {
|
|
|
39
39
|
effort?: Effort;
|
|
40
40
|
maxTokens?: number;
|
|
41
41
|
fetchImpl?: typeof fetch;
|
|
42
|
-
/** Extra request headers
|
|
42
|
+
/** Extra request headers, where a subscription backend's account id goes
|
|
43
43
|
* (`ChatGPT-Account-Id`), which those backends reject the request without. */
|
|
44
44
|
headers?: Record<string, string>;
|
|
45
45
|
/** Header that carries `StreamOptions.sessionId`. When set and a call has no
|
|
@@ -50,7 +50,7 @@ export interface ResponsesConfig extends ProviderFallbackConfig {
|
|
|
50
50
|
* The ChatGPT subscription surface serves it at `/backend-api/codex/responses`
|
|
51
51
|
* with no version segment, so that backend needs
|
|
52
52
|
* `{ baseUrl: "https://chatgpt.com/backend-api/codex", path: "/responses" }`.
|
|
53
|
-
* Without the override the POST 404s, and a 404 classifies as "model"
|
|
53
|
+
* Without the override the POST 404s, and a 404 classifies as "model". The
|
|
54
54
|
* user is told the model id does not exist when the path was the problem.
|
|
55
55
|
*/
|
|
56
56
|
path?: string;
|
|
@@ -141,8 +141,8 @@ function partsToResponses(
|
|
|
141
141
|
});
|
|
142
142
|
}
|
|
143
143
|
|
|
144
|
-
/** A tool result is a bare string unless it carried images
|
|
145
|
-
* array form, the only way to hand this shape a screenshot back. */
|
|
144
|
+
/** A tool result is a bare string unless it carried images. Then it uses the
|
|
145
|
+
* content-array form, the only way to hand this shape a screenshot back. */
|
|
146
146
|
function toolOutput(
|
|
147
147
|
content: string,
|
|
148
148
|
images: readonly ImagePart[],
|
|
@@ -157,7 +157,7 @@ function toolOutput(
|
|
|
157
157
|
/**
|
|
158
158
|
* Flatten a history into `instructions` plus the input item list.
|
|
159
159
|
*
|
|
160
|
-
* This shape has no system
|
|
160
|
+
* This shape has no system role. The system prompt is a top-level
|
|
161
161
|
* `instructions` string, and everything else is items. One assistant turn can
|
|
162
162
|
* become several items (its text, then one `function_call` per tool it asked
|
|
163
163
|
* for), which is why a message maps to a list rather than to one item.
|
|
@@ -244,7 +244,7 @@ interface ResponsesUsage {
|
|
|
244
244
|
|
|
245
245
|
interface ResponsesItem {
|
|
246
246
|
type?: string;
|
|
247
|
-
/** The output item's own id (`fc_…`)
|
|
247
|
+
/** The output item's own id (`fc_…`), which the argument deltas reference. */
|
|
248
248
|
id?: string;
|
|
249
249
|
/** The id a `function_call_output` must quote on the next turn. */
|
|
250
250
|
call_id?: string;
|
|
@@ -263,7 +263,7 @@ interface ResponsesEvent {
|
|
|
263
263
|
arguments?: string;
|
|
264
264
|
item?: ResponsesItem;
|
|
265
265
|
response?: {
|
|
266
|
-
/** `completed`, `incomplete` or `failed
|
|
266
|
+
/** `completed`, `incomplete` or `failed`, stated on the terminal event,
|
|
267
267
|
* and not always the one its `type` implies. */
|
|
268
268
|
status?: string;
|
|
269
269
|
usage?: ResponsesUsage;
|
|
@@ -297,8 +297,8 @@ interface CodexRateLimits {
|
|
|
297
297
|
/**
|
|
298
298
|
* The quota wall a `codex.rate_limits` snapshot reports, if it is one.
|
|
299
299
|
*
|
|
300
|
-
* The snapshot rides ahead of the first output on every turn and is telemetry
|
|
301
|
-
*
|
|
300
|
+
* The snapshot rides ahead of the first output on every turn and is telemetry.
|
|
301
|
+
* A full window the account's credits cover, or that the backend still
|
|
302
302
|
* `allowed`, lets the turn proceed. It is only the reason when the stream then
|
|
303
303
|
* closes without a response: that close is a spent window, not a dropped
|
|
304
304
|
* socket, and retrying it walks every backup into the same wall. The fuller
|
|
@@ -336,8 +336,8 @@ function usageChunk(usage: ResponsesUsage): ProviderChunk {
|
|
|
336
336
|
};
|
|
337
337
|
}
|
|
338
338
|
|
|
339
|
-
/** What has already gone out for one in-flight function call
|
|
340
|
-
* arguments
|
|
339
|
+
/** What has already gone out for one in-flight function call (identity and
|
|
340
|
+
* arguments), so the authoritative snapshot on `.done` can be diffed
|
|
341
341
|
* against it instead of duplicated. */
|
|
342
342
|
interface PendingCall {
|
|
343
343
|
index: number;
|
package/src/providers/zai.ts
CHANGED
|
@@ -13,8 +13,8 @@ export type ZaiCodingConfig = Omit<AnthropicConfig, "baseUrl" | "id" | "bearer">
|
|
|
13
13
|
* adapter.
|
|
14
14
|
*
|
|
15
15
|
* Dialect notes, measured 2026-09-13 against the live endpoint on
|
|
16
|
-
* `glm-5.3-flash`: an absent thinking field means the
|
|
17
|
-
* thinking
|
|
16
|
+
* `glm-5.3-flash`: an absent thinking field means the model's default
|
|
17
|
+
* (thinking on), so `effort: "none"` is sent as an explicit
|
|
18
18
|
* `thinking: { type: "disabled" }`; `low`–`max` ride the standard thinking
|
|
19
19
|
* budgets, which the endpoint accepts. Images force a thinking block even
|
|
20
20
|
* under `disabled` (the model's choice, billed as output). */
|
package/src/rate-limit.ts
CHANGED
|
@@ -32,7 +32,7 @@ export type RateLimitWindow = "5h" | "weekly" | "monthly";
|
|
|
32
32
|
export interface RateLimitReset {
|
|
33
33
|
/** Absolute time the binding window resets, when any header disclosed it. */
|
|
34
34
|
resetAtMs?: number;
|
|
35
|
-
/** Server-requested wait (`retry-after`)
|
|
35
|
+
/** Server-requested wait (`retry-after`), which a retry policy honours. */
|
|
36
36
|
retryAfterMs?: number;
|
|
37
37
|
/** Which subscription window bound, when it could be named at all. */
|
|
38
38
|
window?: RateLimitWindow;
|
|
@@ -58,7 +58,7 @@ const RESET_HEADERS = [
|
|
|
58
58
|
];
|
|
59
59
|
|
|
60
60
|
/**
|
|
61
|
-
* The ChatGPT backend's two windows. Both ride every response
|
|
61
|
+
* The ChatGPT backend's two windows. Both ride every response. The slots are
|
|
62
62
|
* "primary" and "secondary", NOT "5h" and "weekly", so the window is named by
|
|
63
63
|
* its own `-window-minutes` (300 and 10080 in every capture), and its
|
|
64
64
|
* `-used-percent` says which one is full. Recorded by cc-proxy off a live turn
|
|
@@ -154,7 +154,7 @@ function parseRetryAfter(value: string | null, now: number): number | undefined
|
|
|
154
154
|
* Anthropic's `anthropic-ratelimit-unified-*` pair rides on every OAuth
|
|
155
155
|
* response, so both windows are always reported and only one of them is the
|
|
156
156
|
* reason for this 429. API-key accounts get the `anthropic-ratelimit-*-reset`
|
|
157
|
-
* RFC 3339 timestamps instead. Everything is optional
|
|
157
|
+
* RFC 3339 timestamps instead. Everything is optional: an unrecognized shape
|
|
158
158
|
* yields an empty result, and the caller falls back to its generic message.
|
|
159
159
|
*/
|
|
160
160
|
export function parseRateLimitReset(headers: Headers, now = Date.now()): RateLimitReset {
|
|
@@ -270,7 +270,7 @@ function parseProseRetryMs(text: string): number | undefined {
|
|
|
270
270
|
* Other providers and gateways (LiteLLM, Cloudflare, Ollama) report countdowns in prose:
|
|
271
271
|
* `Rate limit exceeded. Try again in 20s.`
|
|
272
272
|
* The window name is inferred from the wait itself, and only past the floor
|
|
273
|
-
* above
|
|
273
|
+
* above. A sub-minute retry is a throttle, not a subscription window.
|
|
274
274
|
*/
|
|
275
275
|
export function parseUsageLimitBody(bodyText: string, now = Date.now()): RateLimitReset {
|
|
276
276
|
let body: unknown;
|
package/src/retry.ts
CHANGED
|
@@ -93,8 +93,8 @@ function delayFor(err: unknown, attempt: number, opts: RetryOptions): number | n
|
|
|
93
93
|
const defaultShouldRetry = (err: unknown, _attempt: number): boolean => isRetryable(err);
|
|
94
94
|
|
|
95
95
|
/**
|
|
96
|
-
* Run `fn`, retrying transient failures with backoff. For one-shot calls
|
|
97
|
-
* a title, a summary
|
|
96
|
+
* Run `fn`, retrying transient failures with backoff. For one-shot calls such
|
|
97
|
+
* as a title, a summary or a compaction pass.
|
|
98
98
|
*/
|
|
99
99
|
export async function withRetry<T>(
|
|
100
100
|
fn: (attempt: number) => Promise<T>,
|
|
@@ -124,7 +124,7 @@ export async function withRetry<T>(
|
|
|
124
124
|
}
|
|
125
125
|
|
|
126
126
|
/**
|
|
127
|
-
* The streaming twin
|
|
127
|
+
* The streaming twin of `withRetry`. A retry happens only
|
|
128
128
|
* while NOTHING has been yielded yet.
|
|
129
129
|
*
|
|
130
130
|
* `factory` is re-invoked per attempt and gets a fresh signal, so an abandoned
|
|
@@ -182,7 +182,7 @@ export interface BackupModelOptions {
|
|
|
182
182
|
models: string[];
|
|
183
183
|
/**
|
|
184
184
|
* Whether a failure may fall through to the remaining models. Default:
|
|
185
|
-
* overload and rate limits only
|
|
185
|
+
* overload and rate limits only. Those are per-model-endpoint, and nothing
|
|
186
186
|
* else on the list is. An auth failure or an invalid request would land
|
|
187
187
|
* identically on every backup.
|
|
188
188
|
*/
|
|
@@ -226,7 +226,7 @@ export async function withBackupModels<T>(
|
|
|
226
226
|
}
|
|
227
227
|
|
|
228
228
|
/**
|
|
229
|
-
* The streaming twin
|
|
229
|
+
* The streaming twin of `withBackupModels`, with the same commitment rule as `withStreamRetry`.
|
|
230
230
|
*
|
|
231
231
|
* This is the correction worth naming: walking to a backup model AFTER chunks
|
|
232
232
|
* have already reached the consumer replays the answer from the top, in a
|
package/src/schema.ts
CHANGED
|
@@ -70,19 +70,19 @@ export function clampToSchema(value: unknown, node: unknown): unknown {
|
|
|
70
70
|
* Whether OpenAI's `strict` schema mode will accept this schema.
|
|
71
71
|
*
|
|
72
72
|
* Strict is the only JSON mode that actually guarantees the shape, so it is
|
|
73
|
-
* worth having
|
|
73
|
+
* worth having, but it demands more than JSON Schema does: every property an
|
|
74
74
|
* object lists must ALSO be required, and every object must close itself with
|
|
75
75
|
* `additionalProperties: false`, all the way down. A schema with one optional
|
|
76
76
|
* field is not "mostly strict"; it is a flat 400 naming a nested path rather
|
|
77
77
|
* than the rule it broke.
|
|
78
78
|
*
|
|
79
79
|
* That trap is the reason this exists. An agent's response schema grows
|
|
80
|
-
* optional fields naturally
|
|
81
|
-
* one step collects
|
|
80
|
+
* optional fields naturally (a `data` block only some flows fill, the fields
|
|
81
|
+
* one step collects). A caller who adds one wants their answer, not a
|
|
82
82
|
* lecture about a mode they never asked for. So the OpenAI-shape adapters ask
|
|
83
83
|
* this and drop to plain (unenforced) schema mode instead of failing the turn.
|
|
84
84
|
*
|
|
85
|
-
* Anything it cannot verify
|
|
85
|
+
* Anything it cannot verify (a `$ref`, a composed `allOf`) answers false:
|
|
86
86
|
* the cost of guessing wrong that way is unenforced output, and the cost of
|
|
87
87
|
* guessing wrong the other way is a request that cannot succeed at all.
|
|
88
88
|
*/
|
|
@@ -97,7 +97,7 @@ export function clampToSchema(value: unknown, node: unknown): unknown {
|
|
|
97
97
|
*
|
|
98
98
|
* Two shapes need it, which is why the wording lives here rather than in one of
|
|
99
99
|
* them. Anthropic has never had a schema mode. The OpenAI dialect falls back to
|
|
100
|
-
* plain JSON mode on every gateway
|
|
100
|
+
* plain JSON mode on every gateway: `json_object` asks for valid JSON and says
|
|
101
101
|
* nothing whatsoever about its shape.
|
|
102
102
|
*/
|
|
103
103
|
export function schemaPrompt(schema: unknown): string {
|
|
@@ -134,8 +134,8 @@ export function isStrictSchema(node: unknown): boolean {
|
|
|
134
134
|
);
|
|
135
135
|
}
|
|
136
136
|
|
|
137
|
-
/** Keywords whose value is a schema, a list of schemas, or a map of them
|
|
138
|
-
* the only places a `pattern` can mean a regex. Everything else (`default`,
|
|
137
|
+
/** Keywords whose value is a schema, a list of schemas, or a map of them.
|
|
138
|
+
* These are the only places a `pattern` can mean a regex. Everything else (`default`,
|
|
139
139
|
* `examples`, `enum`, property NAMES) is data and is left alone. */
|
|
140
140
|
const SCHEMA_SLOTS = [
|
|
141
141
|
"items",
|
|
@@ -156,7 +156,7 @@ const SCHEMA_MAPS = ["properties", "patternProperties", "$defs", "definitions",
|
|
|
156
156
|
* A tool schema with every `pattern` constraint removed.
|
|
157
157
|
*
|
|
158
158
|
* The ChatGPT backend compiles tool schemas with OpenAI's own regex dialect and
|
|
159
|
-
* answers a pattern it cannot read with a 400 for the
|
|
159
|
+
* answers a pattern it cannot read with a 400 for the whole request, strict
|
|
160
160
|
* or not. Claude Code's Artifact tool carries one, and every request that
|
|
161
161
|
* offered it failed (cc-proxy #141/#142). Losing the constraint costs a
|
|
162
162
|
* validation the caller's tool runner repeats anyway; the 400 costs the turn.
|
package/src/tool-args.ts
CHANGED
|
@@ -58,7 +58,7 @@ function unescapeJson(text: string): string {
|
|
|
58
58
|
/**
|
|
59
59
|
* Best-effort recovery of `"key": "value"` string fields from truncated JSON.
|
|
60
60
|
*
|
|
61
|
-
* Only strings: they are what a summary field
|
|
61
|
+
* Only strings: they are what a summary field (the one worth rescuing) is
|
|
62
62
|
* made of, and a closing quote is the single reliable boundary in a partial
|
|
63
63
|
* stream. Numbers, booleans and objects are dropped, because a salvaged
|
|
64
64
|
* half-value is worse than none.
|
|
@@ -151,7 +151,7 @@ export function parseToolArgs(raw: string): Record<string, unknown> {
|
|
|
151
151
|
return healArgs(isRecord(parsed) ? parsed : {});
|
|
152
152
|
}
|
|
153
153
|
|
|
154
|
-
/** Whether `raw` parses at all
|
|
154
|
+
/** Whether `raw` parses at all. Lets a caller tell a truncated tool call from
|
|
155
155
|
* an intact one, since `parseToolArgs` deliberately never throws. */
|
|
156
156
|
export function isCompleteJson(raw: string): boolean {
|
|
157
157
|
if (!raw.trim()) return false;
|
package/src/tools.ts
CHANGED
|
@@ -13,7 +13,7 @@ import type { JsonObjectSchema, ToolDefinition } from "./types.ts";
|
|
|
13
13
|
import { messageOf } from "./errors.ts";
|
|
14
14
|
|
|
15
15
|
export interface ToolContext {
|
|
16
|
-
/** The model's own call id when there is one
|
|
16
|
+
/** The model's own call id when there is one. The event, any approval row
|
|
17
17
|
* and the tool message must all key on what the provider expects back. */
|
|
18
18
|
callId?: string;
|
|
19
19
|
signal?: AbortSignal;
|
|
@@ -29,7 +29,7 @@ export type ToolOutcome<O> =
|
|
|
29
29
|
ok: false;
|
|
30
30
|
callId: string;
|
|
31
31
|
kind: ToolFailure;
|
|
32
|
-
/** Fed back to the model verbatim
|
|
32
|
+
/** Fed back to the model verbatim, so it must read as an instruction to
|
|
33
33
|
* a reader who cannot see our stack trace. */
|
|
34
34
|
error: string;
|
|
35
35
|
durationMs: number;
|
|
@@ -83,7 +83,7 @@ export class ToolTimeoutError extends Error {
|
|
|
83
83
|
}
|
|
84
84
|
}
|
|
85
85
|
|
|
86
|
-
/** Rejects when `signal` aborts
|
|
86
|
+
/** Rejects when `signal` aborts. Races a `run` that ignores its own signal. */
|
|
87
87
|
function abortion(signal: AbortSignal): Promise<never> {
|
|
88
88
|
return new Promise((_resolve, reject) => {
|
|
89
89
|
if (signal.aborted) reject(signal.reason);
|
|
@@ -220,7 +220,7 @@ export class ToolRegistry {
|
|
|
220
220
|
}
|
|
221
221
|
|
|
222
222
|
/**
|
|
223
|
-
* Definitions for an allow-list, in the order given
|
|
223
|
+
* Definitions for an allow-list, in the order given. This is the order the
|
|
224
224
|
* model reads them in, and it is part of the cached prompt prefix. Reordering
|
|
225
225
|
* or appending mid-conversation invalidates that prefix, so a caller that
|
|
226
226
|
* cares should freeze the list when the session opens.
|
package/src/transport.ts
CHANGED
|
@@ -16,7 +16,7 @@ export interface RequestInit_ {
|
|
|
16
16
|
signal?: AbortSignal;
|
|
17
17
|
/** Swapped in tests, or to route through a proxy. Defaults to global fetch. */
|
|
18
18
|
fetchImpl?: typeof fetch;
|
|
19
|
-
/** Called when the response headers arrive and on every body read after
|
|
19
|
+
/** Called when the response headers arrive and on every body read after,
|
|
20
20
|
* keep-alives included. What the watchdog's idle clock runs on. */
|
|
21
21
|
onActivity?: () => void;
|
|
22
22
|
/** Called when the body states `[DONE]`. See `SseHooks.onDone`. */
|
|
@@ -41,8 +41,8 @@ export interface SseHooks {
|
|
|
41
41
|
|
|
42
42
|
/**
|
|
43
43
|
* One frame larger than this is not a frame. Without a cap a malformed or
|
|
44
|
-
* runaway upstream grows the buffer until the runtime dies
|
|
45
|
-
*
|
|
44
|
+
* runaway upstream grows the buffer until the runtime dies, starting with an
|
|
45
|
+
* MV3 worker. cc-proxy caps the ChatGPT backend at the same 8 MiB.
|
|
46
46
|
*/
|
|
47
47
|
export const MAX_SSE_FRAME_CHARS = 8 * 1024 * 1024;
|
|
48
48
|
|
|
@@ -53,7 +53,7 @@ export function apiUrl(baseUrl: string, path: string): string {
|
|
|
53
53
|
|
|
54
54
|
/**
|
|
55
55
|
* How long the provider says to wait, read from the RESPONSE rather than from
|
|
56
|
-
* a thrown error. Headers are authoritative
|
|
56
|
+
* a thrown error. Headers are authoritative: `Retry-After` first, then the
|
|
57
57
|
* vendor reset headers that name a subscription window rather than a
|
|
58
58
|
* per-minute throttle, because "try again in a moment" is a lie for those.
|
|
59
59
|
*/
|
|
@@ -168,21 +168,21 @@ function payloadOf(frame: string): string | null {
|
|
|
168
168
|
}
|
|
169
169
|
|
|
170
170
|
/**
|
|
171
|
-
* Yield each `data:` payload of an SSE body, trimmed
|
|
171
|
+
* Yield each `data:` payload of an SSE body, trimmed, plus the bare JSON
|
|
172
172
|
* object a vendor appends outside the framing, which is only ever an error
|
|
173
173
|
* (see `payloadOf`).
|
|
174
174
|
*
|
|
175
175
|
* Frames are split on the blank line the spec requires, so a payload containing
|
|
176
176
|
* a bare newline survives; `[DONE]` is swallowed here rather than in every
|
|
177
177
|
* adapter, and reported through `hooks.onDone`. CRLF and bare CR are
|
|
178
|
-
* normalized
|
|
178
|
+
* normalized: some gateways send them, and a `\r` left on the end of a JSON
|
|
179
179
|
* payload is a parse error nobody enjoys debugging.
|
|
180
180
|
*
|
|
181
181
|
* Exported apart from `streamSse` because the envelope above it is the half an
|
|
182
182
|
* adopting app most often cannot take: an app with its own translated error
|
|
183
183
|
* copy, its own log levels, or its own auth refresh has to keep building the
|
|
184
184
|
* request and reading the failure itself. Framing is the half nobody should
|
|
185
|
-
* write twice
|
|
185
|
+
* write twice. Hand it a `res.body` and keep your own envelope.
|
|
186
186
|
*/
|
|
187
187
|
export async function* parseSseStream(
|
|
188
188
|
body: ReadableStream<Uint8Array>,
|
package/src/types.ts
CHANGED
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
* `output_config.effort` for current Claude models, where `none` is the lowest
|
|
15
15
|
* effort on a model that cannot stop thinking; and to thinking budgets for
|
|
16
16
|
* Claude 4.5 and older and for every other vendor on that wire.
|
|
17
|
-
* Support varies per model
|
|
17
|
+
* Support varies per model. An unsupported level comes back as a clean 400.
|
|
18
18
|
*
|
|
19
19
|
* Ordered least → most; the type derives from the array so the runtime guard
|
|
20
20
|
* and the union can never drift apart.
|
|
@@ -34,7 +34,7 @@ export interface TextPart {
|
|
|
34
34
|
text: string;
|
|
35
35
|
}
|
|
36
36
|
|
|
37
|
-
/** An image the model looks at (vision)
|
|
37
|
+
/** An image the model looks at (vision), with bytes as base64, never a URL the
|
|
38
38
|
* provider would have to fetch on our behalf. */
|
|
39
39
|
export interface ImagePart {
|
|
40
40
|
type: "image";
|
|
@@ -48,7 +48,7 @@ export type FileMimeType =
|
|
|
48
48
|
`application/${string}` | `audio/${string}` | `video/${string}` | `text/${string}`;
|
|
49
49
|
|
|
50
50
|
/**
|
|
51
|
-
* A file the model reads
|
|
51
|
+
* A file the model reads (a PDF, a recording, a video) as base64 bytes,
|
|
52
52
|
* never a URL. Only the Gemini adapter sends one, as `inlineData`, the way it
|
|
53
53
|
* sends an image. Every other adapter refuses it with an `invalid`
|
|
54
54
|
* ProviderError before any request goes out, rather than drop it: a dropped
|
|
@@ -94,7 +94,7 @@ export type ChatMessage =
|
|
|
94
94
|
* OpenAI-shape serializes it as `reasoning_content`; Anthropic-shape
|
|
95
95
|
* drops it, since its thinking blocks carry signatures we never capture.
|
|
96
96
|
*
|
|
97
|
-
* A turn that
|
|
97
|
+
* A turn that disables thinking must not carry it. Mixing the two is
|
|
98
98
|
* unsupported. `stripReasoning` below is that rule, once.
|
|
99
99
|
*/
|
|
100
100
|
reasoning?: string;
|
|
@@ -102,7 +102,7 @@ export type ChatMessage =
|
|
|
102
102
|
* OpenRouter's normalized reasoning payload, arriving on the stream and
|
|
103
103
|
* riding back UNMODIFIED on the next turn's assistant message.
|
|
104
104
|
*
|
|
105
|
-
* Opaque on purpose
|
|
105
|
+
* Opaque on purpose, with the same contract as Gemini's `thoughtSignature`,
|
|
106
106
|
* and for the same reason: it is the provider's own record of how it got
|
|
107
107
|
* here, and reading, reshaping or dropping it costs the model its
|
|
108
108
|
* continuity across a tool round. Absent on every other dialect.
|
|
@@ -133,7 +133,7 @@ export interface ReasoningItems {
|
|
|
133
133
|
items: unknown[];
|
|
134
134
|
}
|
|
135
135
|
|
|
136
|
-
/** JSON Schema for an object
|
|
136
|
+
/** JSON Schema for an object, as every provider's tool contract requires. */
|
|
137
137
|
export interface JsonObjectSchema {
|
|
138
138
|
type: "object";
|
|
139
139
|
properties?: Record<string, unknown>;
|
|
@@ -161,19 +161,19 @@ export interface ToolCallDelta {
|
|
|
161
161
|
|
|
162
162
|
export interface TokenUsage {
|
|
163
163
|
inputTokens: number;
|
|
164
|
-
/** Cache-
|
|
164
|
+
/** Cache-hit subset of `inputTokens`. Providers auto-cache repeated
|
|
165
165
|
* prefixes and bill the hit portion far cheaper, so it must be tracked
|
|
166
166
|
* separately to cost a turn correctly. 0 when the provider reports none. */
|
|
167
167
|
cachedInputTokens: number;
|
|
168
|
-
/** Tokens
|
|
168
|
+
/** Tokens written to cache, also a subset of `inputTokens`, so the total
|
|
169
169
|
* stays the whole prompt the window has to hold. Anthropic bills these
|
|
170
170
|
* above the input rate; the OpenAI-shape auto-cachers bill them at it.
|
|
171
171
|
* 0 when not reported. */
|
|
172
172
|
cacheWriteTokens?: number;
|
|
173
173
|
outputTokens: number;
|
|
174
174
|
/**
|
|
175
|
-
* The part of `outputTokens` spent thinking, when the provider reports it
|
|
176
|
-
*
|
|
175
|
+
* The part of `outputTokens` spent thinking, when the provider reports it.
|
|
176
|
+
* Already inside `outputTokens`, so never add it again. It is the number
|
|
177
177
|
* that explains a turn that came back empty with a length finish: the
|
|
178
178
|
* reasoning ate the budget. Anthropic does not report it; absent there.
|
|
179
179
|
*/
|
|
@@ -195,7 +195,7 @@ export const EMPTY_USAGE: TokenUsage = {
|
|
|
195
195
|
outputTokens: 0,
|
|
196
196
|
};
|
|
197
197
|
|
|
198
|
-
/** Normalized streaming chunk
|
|
198
|
+
/** Normalized streaming chunk. Every provider's shape maps to this. */
|
|
199
199
|
export interface ProviderChunk {
|
|
200
200
|
type: "delta" | "usage" | "finish";
|
|
201
201
|
/** The selected endpoint and model when a fallback provider emits this chunk.
|
|
@@ -203,7 +203,7 @@ export interface ProviderChunk {
|
|
|
203
203
|
source?: { provider: string; model: string };
|
|
204
204
|
content?: string;
|
|
205
205
|
reasoning?: string;
|
|
206
|
-
/** OpenRouter's normalized reasoning payload
|
|
206
|
+
/** OpenRouter's normalized reasoning payload. Hand it back on the next
|
|
207
207
|
* turn's assistant message verbatim. See ChatMessage.reasoningDetails. */
|
|
208
208
|
reasoningDetails?: unknown[];
|
|
209
209
|
/** Once per turn, where a provider replays its reasoning. See ChatMessage.reasoningItems. */
|
|
@@ -213,22 +213,22 @@ export interface ProviderChunk {
|
|
|
213
213
|
finishReason?: FinishReason;
|
|
214
214
|
}
|
|
215
215
|
|
|
216
|
-
/** Pin or deny tool use. `{ name }` forces one specific tool
|
|
216
|
+
/** Pin or deny tool use. `{ name }` forces one specific tool, so a run is
|
|
217
217
|
* made to commit an answer at its step budget's edge. */
|
|
218
218
|
export type ToolChoice = "auto" | "none" | "required" | { name: string };
|
|
219
219
|
|
|
220
220
|
/**
|
|
221
221
|
* Ask for a JSON object matching `schema`. Providers that enforce schemas get
|
|
222
222
|
* it verbatim; the rest get JSON mode plus the schema in the prompt. Either
|
|
223
|
-
* way the
|
|
223
|
+
* way the caller validates: a provider's "guaranteed" JSON is not one.
|
|
224
224
|
*/
|
|
225
225
|
export interface JsonOutput {
|
|
226
226
|
name: string;
|
|
227
227
|
schema: JsonObjectSchema;
|
|
228
228
|
/**
|
|
229
229
|
* Force OpenAI's strict schema mode on or off. Left unset, the adapters ask
|
|
230
|
-
* `isStrictSchema` and enforce whenever the schema actually qualifies
|
|
231
|
-
*
|
|
230
|
+
* `isStrictSchema` and enforce whenever the schema actually qualifies.
|
|
231
|
+
* This keeps an optional field from turning a working call into a
|
|
232
232
|
* 400. Set it only to overrule that reading.
|
|
233
233
|
*/
|
|
234
234
|
strict?: boolean;
|
|
@@ -339,13 +339,13 @@ function bracketed(text: string): string {
|
|
|
339
339
|
/**
|
|
340
340
|
* How the schema rides on a call that ALSO carries tools.
|
|
341
341
|
*
|
|
342
|
-
* `"response_format"` sends both
|
|
342
|
+
* `"response_format"` sends both, as the shapes document and most models
|
|
343
343
|
* honour. `"prompt"` leaves the format off that call and sends the schema as
|
|
344
344
|
* prompt instead, which is what the Anthropic shape has always done.
|
|
345
345
|
*
|
|
346
346
|
* It is a setting because a model that cannot serve both never says so: its
|
|
347
347
|
* decoder is pinned to the schema, the tool call has nowhere to go, and it
|
|
348
|
-
* writes the announcement instead
|
|
348
|
+
* writes the announcement instead ("let me look that up") and stops. Nothing
|
|
349
349
|
* is logged, because nothing failed. `probeJsonWithTools` answers it in one
|
|
350
350
|
* call; there is no default that is right for every model.
|
|
351
351
|
*/
|
|
@@ -359,9 +359,10 @@ export interface StreamOptions {
|
|
|
359
359
|
* task emitting a large artifact must raise it or the tool-call JSON is
|
|
360
360
|
* silently truncated mid-argument. */
|
|
361
361
|
maxTokens?: number;
|
|
362
|
+
/** Not sent to Gemini, which fixes its own sampling. Same for `topP`. */
|
|
362
363
|
temperature?: number;
|
|
363
364
|
/**
|
|
364
|
-
* Nucleus sampling. Set this
|
|
365
|
+
* Nucleus sampling. Set this or `temperature`, not both. The vendors all
|
|
365
366
|
* document them as alternatives and some reject the pair outright.
|
|
366
367
|
*/
|
|
367
368
|
topP?: number;
|
|
@@ -378,7 +379,7 @@ export interface StreamOptions {
|
|
|
378
379
|
toolChoice?: ToolChoice;
|
|
379
380
|
json?: JsonOutput;
|
|
380
381
|
/**
|
|
381
|
-
* Overrides the provider's own setting for this call
|
|
382
|
+
* Overrides the provider's own setting for this call, so `probeJsonWithTools`
|
|
382
383
|
* asks the same model both ways. See {@link JsonWithTools}.
|
|
383
384
|
*/
|
|
384
385
|
jsonWithTools?: JsonWithTools;
|
|
@@ -397,7 +398,7 @@ export interface StreamOptions {
|
|
|
397
398
|
*/
|
|
398
399
|
onActivity?: () => void;
|
|
399
400
|
/**
|
|
400
|
-
* `false` asks for at most one tool call per turn
|
|
401
|
+
* `false` asks for at most one tool call per turn, for tools that must run
|
|
401
402
|
* in order, or a backend lane that refuses parallel calls. Absent or `true`
|
|
402
403
|
* is every vendor's default and sends nothing. Gemini has no such switch,
|
|
403
404
|
* so it refuses `false` rather than silently ignore it.
|
|
@@ -488,7 +489,7 @@ export async function drainStream(
|
|
|
488
489
|
*
|
|
489
490
|
* The chain-of-thought belongs only to thinking turns. A provider that
|
|
490
491
|
* requires it replayed while thinking is ON (DeepSeek) rejects it when
|
|
491
|
-
* thinking is
|
|
492
|
+
* thinking is off. This is exactly the shape of a forced-submit salvage
|
|
492
493
|
* turn, where a run reasons through its whole investigation and then drops
|
|
493
494
|
* thinking to serialize what it already found.
|
|
494
495
|
*
|
|
@@ -518,7 +519,7 @@ export function stripReasoning(messages: readonly ChatMessage[]): ChatMessage[]
|
|
|
518
519
|
});
|
|
519
520
|
}
|
|
520
521
|
|
|
521
|
-
/** `data:` URI for an image part
|
|
522
|
+
/** `data:` URI for an image part, as the OpenAI dialect requires inline. */
|
|
522
523
|
export function toDataUri(part: ImagePart): string {
|
|
523
524
|
return `data:${part.mimeType};base64,${part.data}`;
|
|
524
525
|
}
|