@providerkit/core 0.21.1 → 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 +9 -7
- 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 +17 -12
- 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
|
@@ -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
|
}
|
package/src/usage.ts
CHANGED
|
@@ -15,14 +15,14 @@
|
|
|
15
15
|
import type { TokenUsage } from "./types.ts";
|
|
16
16
|
import { EMPTY_USAGE } from "./types.ts";
|
|
17
17
|
|
|
18
|
-
/** USD per
|
|
18
|
+
/** USD per million tokens, as every vendor prints its price sheet, so a row
|
|
19
19
|
* can be checked against one without arithmetic. */
|
|
20
20
|
export interface ModelRate {
|
|
21
21
|
/** Cache-MISS input: fresh tokens the provider had to read. */
|
|
22
22
|
input: number;
|
|
23
23
|
output: number;
|
|
24
24
|
/** Cache-HIT input. Providers auto-cache repeated prefixes and bill the hit
|
|
25
|
-
* portion far cheaper
|
|
25
|
+
* portion far cheaper: 0.1× on Anthropic, ~0.1× on Gemini, 0.5× on some
|
|
26
26
|
* OpenAI models. In an agent loop the re-sent context is overwhelmingly
|
|
27
27
|
* hits, so billing it at the miss rate overcounts by up to 10×. */
|
|
28
28
|
cacheRead: number;
|
|
@@ -95,14 +95,14 @@ export function costUsd(usage: TokenUsage, rate: ModelRate): number {
|
|
|
95
95
|
}
|
|
96
96
|
|
|
97
97
|
/** What one call adds to a run: its reported cost, else its rate, else
|
|
98
|
-
* nothing
|
|
98
|
+
* nothing. An unpriced call still counts its tokens. */
|
|
99
99
|
function callCost(usage: TokenUsage, rate: ModelRate | undefined): number {
|
|
100
100
|
return rate ? costUsd(usage, rate) : (usage.reportedCostUsd ?? 0);
|
|
101
101
|
}
|
|
102
102
|
|
|
103
103
|
/**
|
|
104
104
|
* Accumulates usage and cost across the calls of one run, pricing each with
|
|
105
|
-
* the rate in effect for the model that served it
|
|
105
|
+
* the rate in effect for the model that served it, so a run that switches to
|
|
106
106
|
* a backup model, or spans a time-of-day price boundary, still bills correctly.
|
|
107
107
|
* A call that carries a reported cost is billed that, with or without a rate.
|
|
108
108
|
*/
|
|
@@ -110,7 +110,7 @@ export class UsageTracker {
|
|
|
110
110
|
private usage: TokenUsage = { ...EMPTY_USAGE, cacheWriteTokens: 0 };
|
|
111
111
|
private cost = 0;
|
|
112
112
|
/** USD the cache hits saved, versus billing them all as misses.
|
|
113
|
-
* Observability only
|
|
113
|
+
* Observability only. Never billed. */
|
|
114
114
|
private saved = 0;
|
|
115
115
|
|
|
116
116
|
add(usage: TokenUsage, rate?: ModelRate): void {
|
package/src/watchdog.ts
CHANGED
|
@@ -42,7 +42,7 @@ import type {
|
|
|
42
42
|
* stream is dead. */
|
|
43
43
|
export const STREAM_IDLE_MS = 60_000;
|
|
44
44
|
|
|
45
|
-
/** No chunk for this long
|
|
45
|
+
/** No chunk for this long, including the wait for the response to start,
|
|
46
46
|
* and the stream is going nowhere, keep-alives or not. */
|
|
47
47
|
export const STREAM_PROGRESS_MS = 300_000;
|
|
48
48
|
|
|
@@ -51,17 +51,17 @@ export interface StreamWatch {
|
|
|
51
51
|
readonly signal: AbortSignal;
|
|
52
52
|
/** A chunk arrived: re-arm both deadlines, and mark TTFT if it was the first. */
|
|
53
53
|
sawByte(): void;
|
|
54
|
-
/** The response showed life without a chunk
|
|
54
|
+
/** The response showed life without a chunk: its headers, a keep-alive, a
|
|
55
55
|
* partial frame. Starts or re-arms the idle deadline only. */
|
|
56
56
|
sawActivity(): void;
|
|
57
57
|
/**
|
|
58
|
-
* Milliseconds from the call opening to its first chunk of any kind
|
|
58
|
+
* Milliseconds from the call opening to its first chunk of any kind. The
|
|
59
59
|
* wait a person actually experiences, and the number a prompt-cache pin
|
|
60
60
|
* exists to shrink. Null until something arrives.
|
|
61
61
|
*/
|
|
62
62
|
firstChunkMs(): number | null;
|
|
63
63
|
/**
|
|
64
|
-
* Re-issue a provider failure as the timeout
|
|
64
|
+
* Re-issue a provider failure as the timeout only when it was
|
|
65
65
|
* one of our deadlines that aborted. A caller's Stop passes through untouched.
|
|
66
66
|
*/
|
|
67
67
|
classify(err: unknown): unknown;
|
|
@@ -148,7 +148,7 @@ export function streamWatch(opts: StreamWatchOptions = {}): StreamWatch {
|
|
|
148
148
|
|
|
149
149
|
/**
|
|
150
150
|
* Wrap a stream so every chunk re-arms `watch`, and a failure is re-classified
|
|
151
|
-
* through it. Disposes on any exit
|
|
151
|
+
* through it. Disposes on any exit: completion, throw, or the consumer
|
|
152
152
|
* breaking out of the loop.
|
|
153
153
|
*/
|
|
154
154
|
export async function* watchChunks<T>(
|
|
@@ -173,7 +173,7 @@ export async function* watchChunks<T>(
|
|
|
173
173
|
* A stream that ends with no text and no tool call is a failure wearing a
|
|
174
174
|
* success's clothes: `stop_reason: end_turn` with zero content blocks, which
|
|
175
175
|
* the vendors emit under load, or a thinking block that ate the whole
|
|
176
|
-
* `max_tokens`. Nothing throws, so nothing retries
|
|
176
|
+
* `max_tokens`. Nothing throws, so nothing retries. The caller simply shows a
|
|
177
177
|
* person an empty answer, and the only trace is a bill.
|
|
178
178
|
*
|
|
179
179
|
* Reasoning is not an answer. It still streams the moment it arrives, so a
|
|
@@ -185,12 +185,12 @@ export async function* watchChunks<T>(
|
|
|
185
185
|
*
|
|
186
186
|
* Classified by what fixes it. A `length` finish is the caller's own cap:
|
|
187
187
|
* `invalid`, so nothing retries it into the same cap and no backup model is
|
|
188
|
-
* cooled down for it
|
|
188
|
+
* cooled down for it. The fix is a larger `maxTokens` or a lower effort. Any
|
|
189
189
|
* other empty turn is `overload`, because that is both true and useful: it is
|
|
190
190
|
* theirs and temporary, so it is transient (the same model, retried, usually
|
|
191
191
|
* answers) and backup-eligible (a model that keeps doing it should be walked
|
|
192
192
|
* away from). Until something streams, the empty frames are held back, so the
|
|
193
|
-
* retry rule
|
|
193
|
+
* retry rule still holds: retry only while nothing was emitted.
|
|
194
194
|
*/
|
|
195
195
|
export async function* requireContent<T extends ProviderChunk>(
|
|
196
196
|
provider: string,
|
|
@@ -240,7 +240,7 @@ export async function* requireContent<T extends ProviderChunk>(
|
|
|
240
240
|
throw new ProviderError(
|
|
241
241
|
provider,
|
|
242
242
|
"invalid",
|
|
243
|
-
`${provider}: the output cap ran out before any answer${spent}
|
|
243
|
+
`${provider}: the output cap ran out before any answer${spent}. Raise maxTokens or lower effort.`,
|
|
244
244
|
);
|
|
245
245
|
}
|
|
246
246
|
throw new ProviderError(provider, "overload", `${provider}: completed with no content`);
|
|
@@ -267,8 +267,8 @@ export interface WatchdogOptions {
|
|
|
267
267
|
* A provider with both silent failures already handled.
|
|
268
268
|
*
|
|
269
269
|
* Every consumer of this package wrote the same three lines around every
|
|
270
|
-
* `createStream
|
|
271
|
-
* the chunks
|
|
270
|
+
* `createStream`: build a watch, hand the provider the watch's signal, wrap
|
|
271
|
+
* the chunks. The middle one is the trap. Pass the caller's signal
|
|
272
272
|
* instead and everything still compiles, still streams, still passes the
|
|
273
273
|
* tests: the watchdog simply never aborts anything, because the request it was
|
|
274
274
|
* meant to cancel was never told about it. The failure has no symptom until
|
|
@@ -276,8 +276,8 @@ export interface WatchdogOptions {
|
|
|
276
276
|
*
|
|
277
277
|
* So the composition belongs here rather than in a docs snippet each app
|
|
278
278
|
* copies. The result is still a `Provider`, so it composes unchanged with
|
|
279
|
-
* `withStreamRetry` and `streamWithBackupModels
|
|
280
|
-
*
|
|
279
|
+
* `withStreamRetry` and `streamWithBackupModels`. Timeouts and empty turns
|
|
280
|
+
* without a `length` finish are transient, so wrapping it in a retry is safe.
|
|
281
281
|
*/
|
|
282
282
|
export function withWatchdog(provider: Provider, opts: WatchdogOptions = {}): Provider {
|
|
283
283
|
return {
|