@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.
Files changed (56) hide show
  1. package/README.md +3 -0
  2. package/dist/errors.d.ts +17 -0
  3. package/dist/errors.d.ts.map +1 -1
  4. package/dist/errors.js +55 -6
  5. package/dist/errors.js.map +1 -1
  6. package/dist/presets.d.ts +37 -2
  7. package/dist/presets.d.ts.map +1 -1
  8. package/dist/presets.js +37 -2
  9. package/dist/presets.js.map +1 -1
  10. package/dist/providers/anthropic.d.ts.map +1 -1
  11. package/dist/providers/anthropic.js +84 -33
  12. package/dist/providers/anthropic.js.map +1 -1
  13. package/dist/providers/gemini.d.ts.map +1 -1
  14. package/dist/providers/gemini.js +25 -1
  15. package/dist/providers/gemini.js.map +1 -1
  16. package/dist/providers/openai.d.ts.map +1 -1
  17. package/dist/providers/openai.js +44 -16
  18. package/dist/providers/openai.js.map +1 -1
  19. package/dist/providers/responses.d.ts.map +1 -1
  20. package/dist/providers/responses.js +131 -24
  21. package/dist/providers/responses.js.map +1 -1
  22. package/dist/rate-limit.d.ts.map +1 -1
  23. package/dist/rate-limit.js +53 -22
  24. package/dist/rate-limit.js.map +1 -1
  25. package/dist/schema.d.ts +10 -0
  26. package/dist/schema.d.ts.map +1 -1
  27. package/dist/schema.js +49 -0
  28. package/dist/schema.js.map +1 -1
  29. package/dist/transport.d.ts +30 -3
  30. package/dist/transport.d.ts.map +1 -1
  31. package/dist/transport.js +67 -28
  32. package/dist/transport.js.map +1 -1
  33. package/dist/types.d.ts +26 -2
  34. package/dist/types.d.ts.map +1 -1
  35. package/dist/types.js.map +1 -1
  36. package/dist/usage.d.ts +5 -4
  37. package/dist/usage.d.ts.map +1 -1
  38. package/dist/usage.js +14 -5
  39. package/dist/usage.js.map +1 -1
  40. package/dist/watchdog.d.ts +22 -8
  41. package/dist/watchdog.d.ts.map +1 -1
  42. package/dist/watchdog.js +77 -27
  43. package/dist/watchdog.js.map +1 -1
  44. package/package.json +3 -2
  45. package/src/errors.ts +60 -5
  46. package/src/presets.ts +37 -2
  47. package/src/providers/anthropic.ts +94 -36
  48. package/src/providers/gemini.ts +30 -1
  49. package/src/providers/openai.ts +42 -16
  50. package/src/providers/responses.ts +158 -26
  51. package/src/rate-limit.ts +55 -24
  52. package/src/schema.ts +51 -0
  53. package/src/transport.ts +95 -24
  54. package/src/types.ts +26 -2
  55. package/src/usage.ts +14 -5
  56. 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-idle watchdog.
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 a deadline. Any byte of any kind re-arms it
12
- // (reasoning models emit thinking deltas continuously, so silence really is
13
- // silence). When it fires, the watchdog aborts ITS OWN controller and the
14
- // caller's signal is only bridged in — which is what keeps a person's Stop
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 and the stream is considered wedged. */
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 idle = false;
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
- const idleError = (cause) => new ProviderError(provider, "timeout", `stream went ${idleMs / 1000}s without a byte`, {
37
- cause,
38
- });
39
- function arm() {
54
+ function deadline(ms, what) {
40
55
  const timer = setTimeout(() => {
41
- idle = true;
42
- timeout.abort(idleError());
43
- }, idleMs);
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
- let timer = arm();
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
- clearTimeout(timer);
55
- if (!disposed && !signal.aborted)
56
- timer = arm();
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 (idle && !(callerSignal?.aborted ?? false))
62
- return idleError(err);
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(timer);
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 (!usable) {
125
- throw new ProviderError(provider, "overload", `${provider}: completed with no content`);
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
- // idleMs and signal both default inside streamWatch.
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
- // byte of any kind, not the first byte worth showing.
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 ? watched() : requireContent(provider.id, watched());
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
  }
@@ -1 +1 @@
1
- {"version":3,"file":"watchdog.js","sourceRoot":"","sources":["../src/watchdog.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"watchdog.js","sourceRoot":"","sources":["../src/watchdog.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@providerkit/core",
3
- "version": "0.15.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
- if (/^\d+$/.test(value)) return Number(value) * 1000;
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 = readNumber(error, "code") ?? readNumber(error, "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
- ...parseUsageLimitBody(body),
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
- /** The Codex backend behind a ChatGPT sign-in — Responses wire, no public
455
- * model list, so these ids are the list. */
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": {