@providerkit/core 0.15.0 → 0.16.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 +3 -0
- package/dist/errors.d.ts +17 -0
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +55 -6
- package/dist/errors.js.map +1 -1
- package/dist/presets.d.ts +37 -2
- package/dist/presets.d.ts.map +1 -1
- package/dist/presets.js +37 -2
- package/dist/presets.js.map +1 -1
- package/dist/providers/anthropic.d.ts.map +1 -1
- package/dist/providers/anthropic.js +84 -33
- package/dist/providers/anthropic.js.map +1 -1
- package/dist/providers/gemini.d.ts.map +1 -1
- package/dist/providers/gemini.js +25 -1
- package/dist/providers/gemini.js.map +1 -1
- package/dist/providers/openai.d.ts.map +1 -1
- package/dist/providers/openai.js +44 -16
- package/dist/providers/openai.js.map +1 -1
- package/dist/providers/responses.d.ts.map +1 -1
- package/dist/providers/responses.js +131 -24
- package/dist/providers/responses.js.map +1 -1
- package/dist/rate-limit.d.ts.map +1 -1
- package/dist/rate-limit.js +53 -22
- package/dist/rate-limit.js.map +1 -1
- package/dist/schema.d.ts +10 -0
- package/dist/schema.d.ts.map +1 -1
- package/dist/schema.js +49 -0
- package/dist/schema.js.map +1 -1
- package/dist/transport.d.ts +30 -3
- package/dist/transport.d.ts.map +1 -1
- package/dist/transport.js +67 -28
- package/dist/transport.js.map +1 -1
- package/dist/types.d.ts +26 -2
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/dist/usage.d.ts +5 -4
- package/dist/usage.d.ts.map +1 -1
- package/dist/usage.js +14 -5
- package/dist/usage.js.map +1 -1
- package/dist/watchdog.d.ts +22 -8
- package/dist/watchdog.d.ts.map +1 -1
- package/dist/watchdog.js +77 -27
- package/dist/watchdog.js.map +1 -1
- package/package.json +3 -2
- package/src/errors.ts +60 -5
- package/src/presets.ts +37 -2
- package/src/providers/anthropic.ts +94 -36
- package/src/providers/gemini.ts +30 -1
- package/src/providers/openai.ts +42 -16
- package/src/providers/responses.ts +158 -26
- package/src/rate-limit.ts +55 -24
- package/src/schema.ts +51 -0
- package/src/transport.ts +95 -24
- package/src/types.ts +26 -2
- package/src/usage.ts +14 -5
- package/src/watchdog.ts +89 -32
package/dist/watchdog.js
CHANGED
|
@@ -1,31 +1,49 @@
|
|
|
1
1
|
// The two ways a stream fails without failing: it goes silent, or it ends
|
|
2
2
|
// having said nothing at all.
|
|
3
3
|
//
|
|
4
|
-
// The stream
|
|
4
|
+
// The stream watchdog.
|
|
5
5
|
//
|
|
6
6
|
// A provider that stops sending bytes is indistinguishable from a long prefill
|
|
7
7
|
// — except that it never ends, and every SDK's default is to wait forever. A
|
|
8
8
|
// queued route or a wedged prefill upstream hangs the caller indefinitely, and
|
|
9
9
|
// the symptom is the worst kind: nothing. No error, no log, no timeout.
|
|
10
10
|
//
|
|
11
|
-
// So the seam gives itself
|
|
12
|
-
//
|
|
13
|
-
//
|
|
14
|
-
//
|
|
11
|
+
// So the seam gives itself two deadlines, because "silent" means two things:
|
|
12
|
+
//
|
|
13
|
+
// - IDLE: no byte at all, keep-alives included, for a minute after the
|
|
14
|
+
// response started. A dead socket. Any read re-arms it — a keep-alive is
|
|
15
|
+
// proof of life no chunk can carry (cc-proxy pings every 15s while it
|
|
16
|
+
// buffers a long tool call, and the old chunk-only clock killed those turns).
|
|
17
|
+
// - PROGRESS: no chunk for five minutes, counted from the POST. A stream that
|
|
18
|
+
// is alive and going nowhere — a route parked behind keep-alives — and the
|
|
19
|
+
// wait for the response to START, which is the one some backends take
|
|
20
|
+
// minutes over: the ChatGPT backend withholds its headers until the model's
|
|
21
|
+
// first output, so a high-effort turn on a large prompt sends nothing at all
|
|
22
|
+
// for minutes and is healthy the whole time (cc-proxy defaults that wait to
|
|
23
|
+
// 300s, measured). On a one-minute clock it was aborted and retried from
|
|
24
|
+
// scratch, which re-ran the reasoning into the same deadline.
|
|
25
|
+
//
|
|
26
|
+
// When either fires, the watchdog aborts ITS OWN controller and the caller's
|
|
27
|
+
// signal is only bridged in — which is what keeps a person's Stop
|
|
15
28
|
// distinguishable from our timeout. One is their cancel and is never retried;
|
|
16
29
|
// the other is ours, is transient, and fires while nothing has streamed yet,
|
|
17
30
|
// so the retry is always safe.
|
|
18
31
|
import { ProviderError } from "./errors.js";
|
|
19
|
-
/** No byte at all for this long
|
|
32
|
+
/** No byte at all for this long, once the response has started, and the
|
|
33
|
+
* stream is dead. */
|
|
20
34
|
export const STREAM_IDLE_MS = 60_000;
|
|
35
|
+
/** No chunk for this long — the wait for the response to start included —
|
|
36
|
+
* and the stream is going nowhere, keep-alives or not. */
|
|
37
|
+
export const STREAM_PROGRESS_MS = 300_000;
|
|
21
38
|
export function streamWatch(opts = {}) {
|
|
22
39
|
const provider = opts.provider ?? "provider";
|
|
23
40
|
const idleMs = opts.idleMs ?? STREAM_IDLE_MS;
|
|
41
|
+
const progressMs = opts.progressMs ?? STREAM_PROGRESS_MS;
|
|
24
42
|
const callerSignal = opts.signal;
|
|
25
43
|
const started = Date.now();
|
|
26
44
|
const timeout = new AbortController();
|
|
27
45
|
let firstChunk = null;
|
|
28
|
-
let
|
|
46
|
+
let fired = null;
|
|
29
47
|
let disposed = false;
|
|
30
48
|
// The bridge is structural rather than an event listener: AbortSignal.any
|
|
31
49
|
// aborts synchronously when an input is ALREADY aborted, which is the race
|
|
@@ -33,38 +51,48 @@ export function streamWatch(opts = {}) {
|
|
|
33
51
|
// the package's runtime floor — see `engines` — rather than a polyfilled
|
|
34
52
|
// nicety: every runtime this package targets has had it for years.
|
|
35
53
|
const signal = callerSignal ? AbortSignal.any([callerSignal, timeout.signal]) : timeout.signal;
|
|
36
|
-
|
|
37
|
-
cause,
|
|
38
|
-
});
|
|
39
|
-
function arm() {
|
|
54
|
+
function deadline(ms, what) {
|
|
40
55
|
const timer = setTimeout(() => {
|
|
41
|
-
|
|
42
|
-
timeout.abort(
|
|
43
|
-
},
|
|
56
|
+
fired = new ProviderError(provider, "timeout", `stream went ${ms / 1000}s without ${what}`);
|
|
57
|
+
timeout.abort(fired);
|
|
58
|
+
}, ms);
|
|
44
59
|
// An orphaned watch — its consumer gone, dispose never called — must not
|
|
45
60
|
// hold a Node event loop open for a full deadline.
|
|
46
61
|
timer.unref?.();
|
|
47
62
|
return timer;
|
|
48
63
|
}
|
|
49
|
-
|
|
64
|
+
// The idle clock waits for the response to start; until then only the
|
|
65
|
+
// progress clock runs (see the header comment).
|
|
66
|
+
let idle;
|
|
67
|
+
let progress = deadline(progressMs, "a chunk");
|
|
68
|
+
const live = () => !disposed && !signal.aborted;
|
|
69
|
+
function sawActivity() {
|
|
70
|
+
clearTimeout(idle);
|
|
71
|
+
if (live())
|
|
72
|
+
idle = deadline(idleMs, "a byte");
|
|
73
|
+
}
|
|
50
74
|
return {
|
|
51
75
|
signal,
|
|
52
76
|
sawByte() {
|
|
53
77
|
firstChunk ??= Date.now() - started;
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
78
|
+
sawActivity();
|
|
79
|
+
clearTimeout(progress);
|
|
80
|
+
if (live())
|
|
81
|
+
progress = deadline(progressMs, "a chunk");
|
|
57
82
|
},
|
|
83
|
+
sawActivity,
|
|
58
84
|
firstChunkMs: () => firstChunk,
|
|
59
85
|
classify(err) {
|
|
60
86
|
// Our deadline, not theirs — and not the caller's Stop.
|
|
61
|
-
if (
|
|
62
|
-
return
|
|
87
|
+
if (fired && !(callerSignal?.aborted ?? false)) {
|
|
88
|
+
return new ProviderError(fired.provider, "timeout", fired.message, { cause: err });
|
|
89
|
+
}
|
|
63
90
|
return err;
|
|
64
91
|
},
|
|
65
92
|
dispose() {
|
|
66
93
|
disposed = true;
|
|
67
|
-
clearTimeout(
|
|
94
|
+
clearTimeout(idle);
|
|
95
|
+
clearTimeout(progress);
|
|
68
96
|
},
|
|
69
97
|
};
|
|
70
98
|
}
|
|
@@ -102,10 +130,13 @@ export async function* watchChunks(watch, chunks) {
|
|
|
102
130
|
* The throw lands before any chunk is yielded downstream, so the retry rule
|
|
103
131
|
* that matters — retry only while nothing was emitted — still holds.
|
|
104
132
|
*/
|
|
105
|
-
export async function* requireContent(provider, chunks) {
|
|
133
|
+
export async function* requireContent(provider, chunks, opts = {}) {
|
|
106
134
|
const held = [];
|
|
107
135
|
let usable = false;
|
|
136
|
+
let finish;
|
|
108
137
|
for await (const chunk of chunks) {
|
|
138
|
+
if (chunk.type === "finish")
|
|
139
|
+
finish = chunk.finishReason;
|
|
109
140
|
if (!usable) {
|
|
110
141
|
usable = Boolean(chunk.content || chunk.reasoning || chunk.toolCalls?.length);
|
|
111
142
|
// Held rather than forwarded: once a chunk is out, the stream is
|
|
@@ -121,9 +152,18 @@ export async function* requireContent(provider, chunks) {
|
|
|
121
152
|
}
|
|
122
153
|
yield chunk;
|
|
123
154
|
}
|
|
124
|
-
if (
|
|
125
|
-
|
|
155
|
+
if (usable)
|
|
156
|
+
return;
|
|
157
|
+
// The one empty turn that is an answer: the model read a tool result and
|
|
158
|
+
// chose to stop. An agent whose reply IS a tool (a chat bot's send_message)
|
|
159
|
+
// ends every turn this way — retried, cc-proxy measured one such bot
|
|
160
|
+
// failing ~68% of real turns (11 retries, ~3 minutes, then an error). A
|
|
161
|
+
// `length` stop or no finish at all is still the failure this guard is for.
|
|
162
|
+
if (opts.afterToolResult && finish === "stop") {
|
|
163
|
+
yield* held;
|
|
164
|
+
return;
|
|
126
165
|
}
|
|
166
|
+
throw new ProviderError(provider, "overload", `${provider}: completed with no content`);
|
|
127
167
|
}
|
|
128
168
|
/**
|
|
129
169
|
* A provider with both silent failures already handled.
|
|
@@ -148,20 +188,26 @@ export function withWatchdog(provider, opts = {}) {
|
|
|
148
188
|
// Armed on first read, not here: a stream built now and iterated later
|
|
149
189
|
// must not spend its deadline sitting in a variable.
|
|
150
190
|
async function* watched() {
|
|
151
|
-
//
|
|
191
|
+
// The deadlines and signal all default inside streamWatch.
|
|
152
192
|
const watch = streamWatch({
|
|
153
193
|
provider: provider.id,
|
|
154
194
|
idleMs: opts.idleMs,
|
|
195
|
+
progressMs: opts.progressMs,
|
|
155
196
|
signal: streamOpts.signal,
|
|
156
197
|
});
|
|
198
|
+
const callerActivity = streamOpts.onActivity;
|
|
157
199
|
const source = provider.createStream(messages, tools, {
|
|
158
200
|
...streamOpts,
|
|
159
201
|
signal: watch.signal,
|
|
202
|
+
onActivity: () => {
|
|
203
|
+
watch.sawActivity();
|
|
204
|
+
callerActivity?.();
|
|
205
|
+
},
|
|
160
206
|
});
|
|
161
207
|
let reported = false;
|
|
162
208
|
for await (const chunk of watchChunks(watch, source)) {
|
|
163
209
|
// Before `requireContent` holds anything back — TTFT is the first
|
|
164
|
-
//
|
|
210
|
+
// chunk of any kind, not the first one worth showing.
|
|
165
211
|
if (!reported) {
|
|
166
212
|
reported = true;
|
|
167
213
|
opts.onFirstChunk?.(watch.firstChunkMs() ?? 0);
|
|
@@ -169,7 +215,11 @@ export function withWatchdog(provider, opts = {}) {
|
|
|
169
215
|
yield chunk;
|
|
170
216
|
}
|
|
171
217
|
}
|
|
172
|
-
return opts.requireContent === false
|
|
218
|
+
return opts.requireContent === false
|
|
219
|
+
? watched()
|
|
220
|
+
: requireContent(provider.id, watched(), {
|
|
221
|
+
afterToolResult: messages.at(-1)?.role === "tool",
|
|
222
|
+
});
|
|
173
223
|
},
|
|
174
224
|
};
|
|
175
225
|
}
|
package/dist/watchdog.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"watchdog.js","sourceRoot":"","sources":["../src/watchdog.ts"],"names":[],"mappings":"AAAA,0EAA0E;AAC1E,8BAA8B;AAC9B,EAAE;AACF,
|
|
1
|
+
{"version":3,"file":"watchdog.js","sourceRoot":"","sources":["../src/watchdog.ts"],"names":[],"mappings":"AAAA,0EAA0E;AAC1E,8BAA8B;AAC9B,EAAE;AACF,uBAAuB;AACvB,EAAE;AACF,+EAA+E;AAC/E,6EAA6E;AAC7E,+EAA+E;AAC/E,wEAAwE;AACxE,EAAE;AACF,6EAA6E;AAC7E,EAAE;AACF,uEAAuE;AACvE,2EAA2E;AAC3E,wEAAwE;AACxE,gFAAgF;AAChF,8EAA8E;AAC9E,6EAA6E;AAC7E,wEAAwE;AACxE,8EAA8E;AAC9E,+EAA+E;AAC/E,8EAA8E;AAC9E,2EAA2E;AAC3E,gEAAgE;AAChE,EAAE;AACF,6EAA6E;AAC7E,kEAAkE;AAClE,8EAA8E;AAC9E,6EAA6E;AAC7E,+BAA+B;AAC/B,OAAO,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAS5C;sBACsB;AACtB,MAAM,CAAC,MAAM,cAAc,GAAG,MAAM,CAAC;AAErC;2DAC2D;AAC3D,MAAM,CAAC,MAAM,kBAAkB,GAAG,OAAO,CAAC;AAgC1C,MAAM,UAAU,WAAW,CAAC,OAA2B,EAAE;IACvD,MAAM,QAAQ,GAAG,IAAI,CAAC,QAAQ,IAAI,UAAU,CAAC;IAC7C,MAAM,MAAM,GAAG,IAAI,CAAC,MAAM,IAAI,cAAc,CAAC;IAC7C,MAAM,UAAU,GAAG,IAAI,CAAC,UAAU,IAAI,kBAAkB,CAAC;IACzD,MAAM,YAAY,GAAG,IAAI,CAAC,MAAM,CAAC;IACjC,MAAM,OAAO,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC;IAC3B,MAAM,OAAO,GAAG,IAAI,eAAe,EAAE,CAAC;IAEtC,IAAI,UAAU,GAAkB,IAAI,CAAC;IACrC,IAAI,KAAK,GAAyB,IAAI,CAAC;IACvC,IAAI,QAAQ,GAAG,KAAK,CAAC;IAErB,0EAA0E;IAC1E,2EAA2E;IAC3E,2EAA2E;IAC3E,yEAAyE;IACzE,mEAAmE;IACnE,MAAM,MAAM,GAAG,YAAY,CAAC,CAAC,CAAC,WAAW,CAAC,GAAG,CAAC,CAAC,YAAY,EAAE,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,MAAM,CAAC;IAE/F,SAAS,QAAQ,CAAC,EAAU,EAAE,IAAY;QACxC,MAAM,KAAK,GAAG,UAAU,CAAC,GAAG,EAAE;YAC5B,KAAK,GAAG,IAAI,aAAa,CAAC,QAAQ,EAAE,SAAS,EAAE,eAAe,EAAE,GAAG,IAAI,aAAa,IAAI,EAAE,CAAC,CAAC;YAC5F,OAAO,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC;QACvB,CAAC,EAAE,EAAE,CAAC,CAAC;QACP,yEAAyE;QACzE,mDAAmD;QAClD,KAAgC,CAAC,KAAK,EAAE,EAAE,CAAC;QAC5C,OAAO,KAAK,CAAC;IACf,CAAC;IAED,sEAAsE;IACtE,gDAAgD;IAChD,IAAI,IAA+C,CAAC;IACpD,IAAI,QAAQ,GAAG,QAAQ,CAAC,UAAU,EAAE,SAAS,CAAC,CAAC;IAC/C,MAAM,IAAI,GAAG,GAAG,EAAE,CAAC,CAAC,QAAQ,IAAI,CAAC,MAAM,CAAC,OAAO,CAAC;IAEhD,SAAS,WAAW;QAClB,YAAY,CAAC,IAAI,CAAC,CAAC;QACnB,IAAI,IAAI,EAAE;YAAE,IAAI,GAAG,QAAQ,CAAC,MAAM,EAAE,QAAQ,CAAC,CAAC;IAChD,CAAC;IAED,OAAO;QACL,MAAM;QACN,OAAO;YACL,UAAU,KAAK,IAAI,CAAC,GAAG,EAAE,GAAG,OAAO,CAAC;YACpC,WAAW,EAAE,CAAC;YACd,YAAY,CAAC,QAAQ,CAAC,CAAC;YACvB,IAAI,IAAI,EAAE;gBAAE,QAAQ,GAAG,QAAQ,CAAC,UAAU,EAAE,SAAS,CAAC,CAAC;QACzD,CAAC;QACD,WAAW;QACX,YAAY,EAAE,GAAG,EAAE,CAAC,UAAU;QAC9B,QAAQ,CAAC,GAAY;YACnB,wDAAwD;YACxD,IAAI,KAAK,IAAI,CAAC,CAAC,YAAY,EAAE,OAAO,IAAI,KAAK,CAAC,EAAE,CAAC;gBAC/C,OAAO,IAAI,aAAa,CAAC,KAAK,CAAC,QAAQ,EAAE,SAAS,EAAE,KAAK,CAAC,OAAO,EAAE,EAAE,KAAK,EAAE,GAAG,EAAE,CAAC,CAAC;YACrF,CAAC;YACD,OAAO,GAAG,CAAC;QACb,CAAC;QACD,OAAO;YACL,QAAQ,GAAG,IAAI,CAAC;YAChB,YAAY,CAAC,IAAI,CAAC,CAAC;YACnB,YAAY,CAAC,QAAQ,CAAC,CAAC;QACzB,CAAC;KACF,CAAC;AACJ,CAAC;AAED;;;;GAIG;AACH,MAAM,CAAC,KAAK,SAAS,CAAC,CAAC,WAAW,CAChC,KAAkB,EAClB,MAAwB;IAExB,IAAI,CAAC;QACH,IAAI,KAAK,EAAE,MAAM,KAAK,IAAI,MAAM,EAAE,CAAC;YACjC,KAAK,CAAC,OAAO,EAAE,CAAC;YAChB,MAAM,KAAK,CAAC;QACd,CAAC;IACH,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,MAAM,KAAK,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC;IAC5B,CAAC;YAAS,CAAC;QACT,KAAK,CAAC,OAAO,EAAE,CAAC;IAClB,CAAC;AACH,CAAC;AAED;;;;;;;;;;;;;;GAcG;AACH,MAAM,CAAC,KAAK,SAAS,CAAC,CAAC,cAAc,CACnC,QAAgB,EAChB,MAAwB,EACxB,OAAsC,EAAE;IAExC,MAAM,IAAI,GAAQ,EAAE,CAAC;IACrB,IAAI,MAAM,GAAG,KAAK,CAAC;IACnB,IAAI,MAAqC,CAAC;IAE1C,IAAI,KAAK,EAAE,MAAM,KAAK,IAAI,MAAM,EAAE,CAAC;QACjC,IAAI,KAAK,CAAC,IAAI,KAAK,QAAQ;YAAE,MAAM,GAAG,KAAK,CAAC,YAAY,CAAC;QACzD,IAAI,CAAC,MAAM,EAAE,CAAC;YACZ,MAAM,GAAG,OAAO,CAAC,KAAK,CAAC,OAAO,IAAI,KAAK,CAAC,SAAS,IAAI,KAAK,CAAC,SAAS,EAAE,MAAM,CAAC,CAAC;YAC9E,iEAAiE;YACjE,qEAAqE;YACrE,wEAAwE;YACxE,kCAAkC;YAClC,IAAI,CAAC,MAAM,EAAE,CAAC;gBACZ,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;gBACjB,SAAS;YACX,CAAC;YACD,KAAK,CAAC,CAAC,IAAI,CAAC;YACZ,IAAI,CAAC,MAAM,GAAG,CAAC,CAAC;QAClB,CAAC;QACD,MAAM,KAAK,CAAC;IACd,CAAC;IAED,IAAI,MAAM;QAAE,OAAO;IACnB,yEAAyE;IACzE,4EAA4E;IAC5E,qEAAqE;IACrE,wEAAwE;IACxE,4EAA4E;IAC5E,IAAI,IAAI,CAAC,eAAe,IAAI,MAAM,KAAK,MAAM,EAAE,CAAC;QAC9C,KAAK,CAAC,CAAC,IAAI,CAAC;QACZ,OAAO;IACT,CAAC;IACD,MAAM,IAAI,aAAa,CAAC,QAAQ,EAAE,UAAU,EAAE,GAAG,QAAQ,6BAA6B,CAAC,CAAC;AAC1F,CAAC;AAmBD;;;;;;;;;;;;;;;GAeG;AACH,MAAM,UAAU,YAAY,CAAC,QAAkB,EAAE,OAAwB,EAAE;IACzE,OAAO;QACL,GAAG,QAAQ;QACX,YAAY,CACV,QAAuB,EACvB,KAAuB,EACvB,aAA4B,EAAE;YAE9B,uEAAuE;YACvE,qDAAqD;YACrD,KAAK,SAAS,CAAC,CAAC,OAAO;gBACrB,2DAA2D;gBAC3D,MAAM,KAAK,GAAG,WAAW,CAAC;oBACxB,QAAQ,EAAE,QAAQ,CAAC,EAAE;oBACrB,MAAM,EAAE,IAAI,CAAC,MAAM;oBACnB,UAAU,EAAE,IAAI,CAAC,UAAU;oBAC3B,MAAM,EAAE,UAAU,CAAC,MAAM;iBAC1B,CAAC,CAAC;gBACH,MAAM,cAAc,GAAG,UAAU,CAAC,UAAU,CAAC;gBAC7C,MAAM,MAAM,GAAG,QAAQ,CAAC,YAAY,CAAC,QAAQ,EAAE,KAAK,EAAE;oBACpD,GAAG,UAAU;oBACb,MAAM,EAAE,KAAK,CAAC,MAAM;oBACpB,UAAU,EAAE,GAAG,EAAE;wBACf,KAAK,CAAC,WAAW,EAAE,CAAC;wBACpB,cAAc,EAAE,EAAE,CAAC;oBACrB,CAAC;iBACF,CAAC,CAAC;gBACH,IAAI,QAAQ,GAAG,KAAK,CAAC;gBACrB,IAAI,KAAK,EAAE,MAAM,KAAK,IAAI,WAAW,CAAC,KAAK,EAAE,MAAM,CAAC,EAAE,CAAC;oBACrD,kEAAkE;oBAClE,sDAAsD;oBACtD,IAAI,CAAC,QAAQ,EAAE,CAAC;wBACd,QAAQ,GAAG,IAAI,CAAC;wBAChB,IAAI,CAAC,YAAY,EAAE,CAAC,KAAK,CAAC,YAAY,EAAE,IAAI,CAAC,CAAC,CAAC;oBACjD,CAAC;oBACD,MAAM,KAAK,CAAC;gBACd,CAAC;YACH,CAAC;YAED,OAAO,IAAI,CAAC,cAAc,KAAK,KAAK;gBAClC,CAAC,CAAC,OAAO,EAAE;gBACX,CAAC,CAAC,cAAc,CAAC,QAAQ,CAAC,EAAE,EAAE,OAAO,EAAE,EAAE;oBACrC,eAAe,EAAE,QAAQ,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,EAAE,IAAI,KAAK,MAAM;iBAClD,CAAC,CAAC;QACT,CAAC;KACF,CAAC;AACJ,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@providerkit/core",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.16.0",
|
|
4
4
|
"description": "The layer under your agent loop: one seam for every LLM provider, plus the failure handling you only learn in production.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -46,7 +46,7 @@
|
|
|
46
46
|
],
|
|
47
47
|
"scripts": {
|
|
48
48
|
"build": "tsc",
|
|
49
|
-
"typecheck": "tsc --noEmit",
|
|
49
|
+
"typecheck": "tsc --noEmit && tsc -p test && tsc -p scripts",
|
|
50
50
|
"test": "vitest run",
|
|
51
51
|
"test:watch": "vitest",
|
|
52
52
|
"lint": "eslint src test",
|
|
@@ -70,6 +70,7 @@
|
|
|
70
70
|
}
|
|
71
71
|
},
|
|
72
72
|
"devDependencies": {
|
|
73
|
+
"@types/node": "^24.13.3",
|
|
73
74
|
"typescript": "^5.9.3",
|
|
74
75
|
"vitest": "^4.1.2",
|
|
75
76
|
"zod": "^4.3.6"
|
package/src/errors.ts
CHANGED
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
// read before the status — status alone gives the wrong advice ("retry" for an
|
|
11
11
|
// empty balance, "check your key" for a plan that never included the API).
|
|
12
12
|
|
|
13
|
-
import { parseUsageLimitBody, type RateLimitWindow } from "./rate-limit.ts";
|
|
13
|
+
import { parseRateLimitResponse, parseUsageLimitBody, type RateLimitWindow } from "./rate-limit.ts";
|
|
14
14
|
import type { FilePart } from "./types.ts";
|
|
15
15
|
|
|
16
16
|
/** What kind of failure this is, named by what actually fixes it. */
|
|
@@ -379,6 +379,12 @@ const QUOTA_PATTERNS: readonly RegExp[] = [
|
|
|
379
379
|
/usage limits? (?:reached|exceeded|hit)/i,
|
|
380
380
|
/reached your (?:usage|weekly|monthly|daily) limit/i,
|
|
381
381
|
/(?:weekly|monthly|daily|plan) usage limit/i,
|
|
382
|
+
// The ChatGPT backend's spent-window error: `usage_limit_reached`, worded
|
|
383
|
+
// "The usage limit has been reached". As a 429 it read as a throttle and in
|
|
384
|
+
// a stream as an overload, so the backup walker spent every model on the
|
|
385
|
+
// same account-wide wall (cc-proxy: "a 502 retry storm").
|
|
386
|
+
/usage[_\s]limit[_\s]reached/i,
|
|
387
|
+
/usage limit has been reached/i,
|
|
382
388
|
/purchase extra usage/i,
|
|
383
389
|
/upgrade your plan/i,
|
|
384
390
|
/quota\b[^.]{0,40}\b(?:exhausted|exceeded|will be refreshed)/i,
|
|
@@ -429,6 +435,12 @@ const CONTENT_PATTERNS: readonly RegExp[] = [
|
|
|
429
435
|
/content policy/i,
|
|
430
436
|
/cyber[_\s]policy/i,
|
|
431
437
|
/safety|PROHIBITED_CONTENT|blocked|refusal|policy violation|misalignment/i,
|
|
438
|
+
// OpenAI's flagged-prompt codes. They arrive with no status inside a stream,
|
|
439
|
+
// where an unnamed failure is floored to overload and retried — three more
|
|
440
|
+
// copies of a prompt that will be flagged every time.
|
|
441
|
+
/\binvalid_prompt\b/,
|
|
442
|
+
/\bbio[_\s]policy\b/i,
|
|
443
|
+
/violat\w* (?:our |the )?usage polic/i,
|
|
432
444
|
// Document and media boundary violations
|
|
433
445
|
/maximum of \d+ PDF pages/i,
|
|
434
446
|
/PDF specified is password protected/i,
|
|
@@ -576,8 +588,10 @@ export function parseRetryAfterMs(err: unknown, body?: string): number | undefin
|
|
|
576
588
|
? (headers as Record<string, unknown>)["retry-after"]
|
|
577
589
|
: undefined;
|
|
578
590
|
if (typeof value !== "string") return undefined;
|
|
579
|
-
// Seconds, or an HTTP-date — both are legal per RFC 9110.
|
|
580
|
-
|
|
591
|
+
// Seconds, or an HTTP-date — both are legal per RFC 9110. Seconds are read
|
|
592
|
+
// as a NUMBER first: `Date.parse("1.5")` is a day in 2001, a wait of zero.
|
|
593
|
+
const seconds = Number(value.trim());
|
|
594
|
+
if (value.trim() !== "" && Number.isFinite(seconds) && seconds >= 0) return seconds * 1000;
|
|
581
595
|
const date = Date.parse(value);
|
|
582
596
|
return Number.isNaN(date) ? undefined : Math.max(0, date - Date.now());
|
|
583
597
|
}
|
|
@@ -660,7 +674,8 @@ export function parseContextOverflow(err: unknown, body?: string): ContextOverfl
|
|
|
660
674
|
*/
|
|
661
675
|
export function streamError(provider: string, error: unknown): ProviderError {
|
|
662
676
|
const body = JSON.stringify(error ?? {}) ?? "";
|
|
663
|
-
const status =
|
|
677
|
+
const status =
|
|
678
|
+
readNumber(error, "code") ?? readNumber(error, "status") ?? readNumber(error, "status_code");
|
|
664
679
|
const code =
|
|
665
680
|
readString(error, "code") ?? readString(error, "status") ?? readString(error, "type");
|
|
666
681
|
const kind = classifyHttp(status, body);
|
|
@@ -672,12 +687,52 @@ export function streamError(provider: string, error: unknown): ProviderError {
|
|
|
672
687
|
...(status !== undefined ? { status } : {}),
|
|
673
688
|
...(code ? { code } : {}),
|
|
674
689
|
retryAfterMs: parseRetryAfterMs(error, body),
|
|
675
|
-
...
|
|
690
|
+
...readReset(error, body),
|
|
676
691
|
body: body.slice(0, 2_000),
|
|
677
692
|
},
|
|
678
693
|
);
|
|
679
694
|
}
|
|
680
695
|
|
|
696
|
+
/** The reset a stream error carries: the body's clock, plus the rate-limit
|
|
697
|
+
* headers the ChatGPT backend mirrors INTO its error frame — the only place a
|
|
698
|
+
* failure after the 200 can say which window ran out. */
|
|
699
|
+
function readReset(error: unknown, body: string) {
|
|
700
|
+
const raw =
|
|
701
|
+
typeof error === "object" && error !== null
|
|
702
|
+
? (error as { headers?: unknown }).headers
|
|
703
|
+
: undefined;
|
|
704
|
+
if (typeof raw !== "object" || raw === null) return parseUsageLimitBody(body);
|
|
705
|
+
const headers = new Headers();
|
|
706
|
+
for (const [name, value] of Object.entries(raw)) {
|
|
707
|
+
if (typeof value === "string" || typeof value === "number") headers.set(name, String(value));
|
|
708
|
+
}
|
|
709
|
+
return parseRateLimitResponse(headers, body);
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
/**
|
|
713
|
+
* A stream that closed before its turn did: no finish stated, no terminal
|
|
714
|
+
* event, no `[DONE]`.
|
|
715
|
+
*
|
|
716
|
+
* It used to end quietly, with a null finish and no usage. Honest, and nobody
|
|
717
|
+
* read it: a caller that never checks the finish keeps half an answer as if it
|
|
718
|
+
* were the whole one, the ledger records a billed turn as free, and no pool
|
|
719
|
+
* learns the endpoint is dropping streams. cc-proxy shipped the same fix
|
|
720
|
+
* (0.1.39) for the same reason — "instead of marking affected streamed
|
|
721
|
+
* responses as successful".
|
|
722
|
+
*
|
|
723
|
+
* `network`, because that is what it is: the same failure as a socket that
|
|
724
|
+
* dies mid-read, seen from the other side of a clean close. Transient, so a
|
|
725
|
+
* cut that lands before anything was emitted is retried; one that lands after
|
|
726
|
+
* is not (invariant 1), and reaches the caller after everything it delivered.
|
|
727
|
+
*/
|
|
728
|
+
export function streamCut(provider: string): ProviderError {
|
|
729
|
+
return new ProviderError(
|
|
730
|
+
provider,
|
|
731
|
+
"network",
|
|
732
|
+
`${provider}: the stream ended before the turn did`,
|
|
733
|
+
);
|
|
734
|
+
}
|
|
735
|
+
|
|
681
736
|
/** The loggable surface of a failure — so a dead run never reads
|
|
682
737
|
* "400 status code (no body)". */
|
|
683
738
|
export function describeProviderError(err: unknown): Record<string, unknown> {
|
package/src/presets.ts
CHANGED
|
@@ -451,15 +451,50 @@ export const PROVIDER_PRESETS = {
|
|
|
451
451
|
// by tabrunner's Claude plan sign-in).
|
|
452
452
|
headers: { "anthropic-beta": "claude-code-20250219,oauth-2025-04-20" },
|
|
453
453
|
},
|
|
454
|
-
/**
|
|
455
|
-
*
|
|
454
|
+
/**
|
|
455
|
+
* The Codex backend behind a ChatGPT sign-in — Responses wire, no public
|
|
456
|
+
* model list, so these ids are the list.
|
|
457
|
+
*
|
|
458
|
+
* The endpoint is `/backend-api/codex/responses`, with no version segment.
|
|
459
|
+
* Without the `path` the adapter appends `/v1/responses`, the POST 404s, and
|
|
460
|
+
* a 404 classifies as "model": every call failed telling the user their
|
|
461
|
+
* model id was wrong. `session-id` is the header the official Codex client
|
|
462
|
+
* sends; the backend keys its prompt cache on it (cc-proxy, in daily use
|
|
463
|
+
* against this backend, 2026-09).
|
|
464
|
+
*/
|
|
456
465
|
chatgpt: {
|
|
457
466
|
shape: "responses",
|
|
458
467
|
baseUrl: "https://chatgpt.com/backend-api/codex",
|
|
468
|
+
path: "/responses",
|
|
469
|
+
sessionHeader: "session-id",
|
|
459
470
|
auth: "oauth",
|
|
460
471
|
defaultModel: "gpt-5.3-codex",
|
|
461
472
|
models: ["gpt-5.3-codex", "gpt-5.3-codex-spark", "gpt-5.5", "gpt-5.4-mini"],
|
|
462
473
|
},
|
|
474
|
+
/**
|
|
475
|
+
* A Grok subscription (SuperGrok, X Premium) — the backend the Grok CLI
|
|
476
|
+
* talks to, Responses wire. Distinct from `xai`, which is the keyed API and
|
|
477
|
+
* bills per token.
|
|
478
|
+
*
|
|
479
|
+
* The two `x-…` headers are what switch the backend into CLI-token mode;
|
|
480
|
+
* without them the subscription token is refused. The version header is the
|
|
481
|
+
* Grok CLI release cc-proxy sends by default (2026-09); pass your own in
|
|
482
|
+
* `headers` when the backend starts asking for a newer one. Tool calls on
|
|
483
|
+
* this backend are keyed by `call_id` alone, which the adapter follows.
|
|
484
|
+
*/
|
|
485
|
+
grok: {
|
|
486
|
+
shape: "responses",
|
|
487
|
+
baseUrl: "https://cli-chat-proxy.grok.com/v1",
|
|
488
|
+
path: "/responses",
|
|
489
|
+
auth: "oauth",
|
|
490
|
+
defaultModel: "grok-4.7",
|
|
491
|
+
models: ["grok-4.7", "grok-4.6", "grok-4.5", "grok-composer-2.5-fast"],
|
|
492
|
+
headers: {
|
|
493
|
+
"x-xai-token-auth": "xai-grok-cli",
|
|
494
|
+
"x-grok-client-identifier": "grok-shell",
|
|
495
|
+
"x-grok-client-version": "0.2.93",
|
|
496
|
+
},
|
|
497
|
+
},
|
|
463
498
|
/** Kimi's coding endpoint reached with a subscription token instead of a
|
|
464
499
|
* key — Kimi bills the two separately. */
|
|
465
500
|
"kimi-plan": {
|