@providerkit/core 0.16.1 → 0.17.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.
@@ -41,19 +41,29 @@ export declare function streamWatch(opts?: StreamWatchOptions): StreamWatch;
41
41
  */
42
42
  export declare function watchChunks<T>(watch: StreamWatch, chunks: AsyncIterable<T>): AsyncGenerator<T>;
43
43
  /**
44
- * Reject a turn that completed but produced nothing usable.
44
+ * Reject a turn that completed without an answer.
45
45
  *
46
- * A stream that ends with no text, no reasoning and no tool call is a failure
47
- * wearing a success's clothes: `stop_reason: end_turn` with zero content
48
- * blocks, which the vendors emit under load and after a thinking block eats
49
- * the whole `max_tokens`. Nothing throws, so nothing retries — the caller
50
- * simply shows a person an empty answer, and the only trace is a bill.
46
+ * A stream that ends with no text and no tool call is a failure wearing a
47
+ * success's clothes: `stop_reason: end_turn` with zero content blocks, which
48
+ * the vendors emit under load, or a thinking block that ate the whole
49
+ * `max_tokens`. Nothing throws, so nothing retries — the caller simply shows a
50
+ * person an empty answer, and the only trace is a bill.
51
51
  *
52
- * Classified `overload` because that is both true and useful: it is theirs and
53
- * temporary, so it is transient (the same model, retried, usually answers) and
54
- * backup-eligible (a model that keeps doing it should be walked away from).
55
- * The throw lands before any chunk is yielded downstream, so the retry rule
56
- * that matters — retry only while nothing was emitted — still holds.
52
+ * Reasoning is not an answer. It still streams the moment it arrives, so a
53
+ * reader can watch the model think, but a turn that only thought has said
54
+ * nothing. Counting it as content is how a review capped at 2,048 tokens on a
55
+ * high-effort model passed this guard 22 times in a row in production: every
56
+ * token went to thinking, the answer was an empty string, and the app's JSON
57
+ * parse failed on it with nothing to say why.
58
+ *
59
+ * Classified by what fixes it. A `length` finish is the caller's own cap:
60
+ * `invalid`, so nothing retries it into the same cap and no backup model is
61
+ * cooled down for it — the fix is a larger `maxTokens` or a lower effort. Any
62
+ * other empty turn is `overload`, because that is both true and useful: it is
63
+ * theirs and temporary, so it is transient (the same model, retried, usually
64
+ * answers) and backup-eligible (a model that keeps doing it should be walked
65
+ * away from). Until something streams, the empty frames are held back, so the
66
+ * retry rule that matters — retry only while nothing was emitted — still holds.
57
67
  */
58
68
  export declare function requireContent<T extends ProviderChunk>(provider: string, chunks: AsyncIterable<T>, opts?: {
59
69
  afterToolResult?: boolean;
@@ -1 +1 @@
1
- {"version":3,"file":"watchdog.d.ts","sourceRoot":"","sources":["../src/watchdog.ts"],"names":[],"mappings":"AA+BA,OAAO,KAAK,EAEV,QAAQ,EACR,aAAa,EAGd,MAAM,YAAY,CAAC;AAEpB;sBACsB;AACtB,eAAO,MAAM,cAAc,QAAS,CAAC;AAErC;2DAC2D;AAC3D,eAAO,MAAM,kBAAkB,SAAU,CAAC;AAE1C,MAAM,WAAW,WAAW;IAC1B,iEAAiE;IACjE,QAAQ,CAAC,MAAM,EAAE,WAAW,CAAC;IAC7B,iFAAiF;IACjF,OAAO,IAAI,IAAI,CAAC;IAChB;mEAC+D;IAC/D,WAAW,IAAI,IAAI,CAAC;IACpB;;;;OAIG;IACH,YAAY,IAAI,MAAM,GAAG,IAAI,CAAC;IAC9B;;;OAGG;IACH,QAAQ,CAAC,GAAG,EAAE,OAAO,GAAG,OAAO,CAAC;IAChC,8DAA8D;IAC9D,OAAO,IAAI,IAAI,CAAC;CACjB;AAED,MAAM,WAAW,kBAAkB;IACjC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,MAAM,CAAC,EAAE,WAAW,CAAC;CACtB;AAED,wBAAgB,WAAW,CAAC,IAAI,GAAE,kBAAuB,GAAG,WAAW,CAoEtE;AAED;;;;GAIG;AACH,wBAAuB,WAAW,CAAC,CAAC,EAClC,KAAK,EAAE,WAAW,EAClB,MAAM,EAAE,aAAa,CAAC,CAAC,CAAC,GACvB,cAAc,CAAC,CAAC,CAAC,CAWnB;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAuB,cAAc,CAAC,CAAC,SAAS,aAAa,EAC3D,QAAQ,EAAE,MAAM,EAChB,MAAM,EAAE,aAAa,CAAC,CAAC,CAAC,EACxB,IAAI,GAAE;IAAE,eAAe,CAAC,EAAE,OAAO,CAAA;CAAO,GACvC,cAAc,CAAC,CAAC,CAAC,CAkCnB;AAED,MAAM,WAAW,eAAe;IAC9B;wCACoC;IACpC,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB;gFAC4E;IAC5E,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB;;;;OAIG;IACH,cAAc,CAAC,EAAE,OAAO,CAAC;IACzB,wEAAwE;IACxE,YAAY,CAAC,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,IAAI,CAAC;CACrC;AAED;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,YAAY,CAAC,QAAQ,EAAE,QAAQ,EAAE,IAAI,GAAE,eAAoB,GAAG,QAAQ,CA8CrF"}
1
+ {"version":3,"file":"watchdog.d.ts","sourceRoot":"","sources":["../src/watchdog.ts"],"names":[],"mappings":"AA+BA,OAAO,KAAK,EAEV,QAAQ,EACR,aAAa,EAId,MAAM,YAAY,CAAC;AAEpB;sBACsB;AACtB,eAAO,MAAM,cAAc,QAAS,CAAC;AAErC;2DAC2D;AAC3D,eAAO,MAAM,kBAAkB,SAAU,CAAC;AAE1C,MAAM,WAAW,WAAW;IAC1B,iEAAiE;IACjE,QAAQ,CAAC,MAAM,EAAE,WAAW,CAAC;IAC7B,iFAAiF;IACjF,OAAO,IAAI,IAAI,CAAC;IAChB;mEAC+D;IAC/D,WAAW,IAAI,IAAI,CAAC;IACpB;;;;OAIG;IACH,YAAY,IAAI,MAAM,GAAG,IAAI,CAAC;IAC9B;;;OAGG;IACH,QAAQ,CAAC,GAAG,EAAE,OAAO,GAAG,OAAO,CAAC;IAChC,8DAA8D;IAC9D,OAAO,IAAI,IAAI,CAAC;CACjB;AAED,MAAM,WAAW,kBAAkB;IACjC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,MAAM,CAAC,EAAE,WAAW,CAAC;CACtB;AAED,wBAAgB,WAAW,CAAC,IAAI,GAAE,kBAAuB,GAAG,WAAW,CAoEtE;AAED;;;;GAIG;AACH,wBAAuB,WAAW,CAAC,CAAC,EAClC,KAAK,EAAE,WAAW,EAClB,MAAM,EAAE,aAAa,CAAC,CAAC,CAAC,GACvB,cAAc,CAAC,CAAC,CAAC,CAWnB;AAED;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,wBAAuB,cAAc,CAAC,CAAC,SAAS,aAAa,EAC3D,QAAQ,EAAE,MAAM,EAChB,MAAM,EAAE,aAAa,CAAC,CAAC,CAAC,EACxB,IAAI,GAAE;IAAE,eAAe,CAAC,EAAE,OAAO,CAAA;CAAO,GACvC,cAAc,CAAC,CAAC,CAAC,CAgDnB;AAED,MAAM,WAAW,eAAe;IAC9B;wCACoC;IACpC,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB;gFAC4E;IAC5E,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB;;;;OAIG;IACH,cAAc,CAAC,EAAE,OAAO,CAAC;IACzB,wEAAwE;IACxE,YAAY,CAAC,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,IAAI,CAAC;CACrC;AAED;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,YAAY,CAAC,QAAQ,EAAE,QAAQ,EAAE,IAAI,GAAE,eAAoB,GAAG,QAAQ,CA8CrF"}
package/dist/watchdog.js CHANGED
@@ -120,34 +120,49 @@ export async function* watchChunks(watch, chunks) {
120
120
  }
121
121
  }
122
122
  /**
123
- * Reject a turn that completed but produced nothing usable.
123
+ * Reject a turn that completed without an answer.
124
124
  *
125
- * A stream that ends with no text, no reasoning and no tool call is a failure
126
- * wearing a success's clothes: `stop_reason: end_turn` with zero content
127
- * blocks, which the vendors emit under load and after a thinking block eats
128
- * the whole `max_tokens`. Nothing throws, so nothing retries — the caller
129
- * simply shows a person an empty answer, and the only trace is a bill.
125
+ * A stream that ends with no text and no tool call is a failure wearing a
126
+ * success's clothes: `stop_reason: end_turn` with zero content blocks, which
127
+ * the vendors emit under load, or a thinking block that ate the whole
128
+ * `max_tokens`. Nothing throws, so nothing retries — the caller simply shows a
129
+ * person an empty answer, and the only trace is a bill.
130
130
  *
131
- * Classified `overload` because that is both true and useful: it is theirs and
132
- * temporary, so it is transient (the same model, retried, usually answers) and
133
- * backup-eligible (a model that keeps doing it should be walked away from).
134
- * The throw lands before any chunk is yielded downstream, so the retry rule
135
- * that matters — retry only while nothing was emitted — still holds.
131
+ * Reasoning is not an answer. It still streams the moment it arrives, so a
132
+ * reader can watch the model think, but a turn that only thought has said
133
+ * nothing. Counting it as content is how a review capped at 2,048 tokens on a
134
+ * high-effort model passed this guard 22 times in a row in production: every
135
+ * token went to thinking, the answer was an empty string, and the app's JSON
136
+ * parse failed on it with nothing to say why.
137
+ *
138
+ * Classified by what fixes it. A `length` finish is the caller's own cap:
139
+ * `invalid`, so nothing retries it into the same cap and no backup model is
140
+ * cooled down for it — the fix is a larger `maxTokens` or a lower effort. Any
141
+ * other empty turn is `overload`, because that is both true and useful: it is
142
+ * theirs and temporary, so it is transient (the same model, retried, usually
143
+ * answers) and backup-eligible (a model that keeps doing it should be walked
144
+ * away from). Until something streams, the empty frames are held back, so the
145
+ * retry rule that matters — retry only while nothing was emitted — still holds.
136
146
  */
137
147
  export async function* requireContent(provider, chunks, opts = {}) {
138
148
  const held = [];
139
- let usable = false;
149
+ let streaming = false;
150
+ let answered = false;
140
151
  let finish;
152
+ let usage;
141
153
  for await (const chunk of chunks) {
142
154
  if (chunk.type === "finish")
143
155
  finish = chunk.finishReason;
144
- if (!usable) {
145
- usable = Boolean(chunk.content || chunk.reasoning || chunk.toolCalls?.length);
156
+ if (chunk.usage)
157
+ usage = chunk.usage;
158
+ answered ||= Boolean(chunk.content || chunk.toolCalls?.length);
159
+ if (!streaming) {
160
+ streaming = answered || Boolean(chunk.reasoning);
146
161
  // Held rather than forwarded: once a chunk is out, the stream is
147
162
  // committed and the retry this guard exists to trigger can no longer
148
- // fire. Nothing content-bearing has arrived yet, so there is nothing to
163
+ // fire. Nothing has arrived worth showing yet, so there is nothing to
149
164
  // hold back but the empty frames.
150
- if (!usable) {
165
+ if (!streaming) {
151
166
  held.push(chunk);
152
167
  continue;
153
168
  }
@@ -156,7 +171,7 @@ export async function* requireContent(provider, chunks, opts = {}) {
156
171
  }
157
172
  yield chunk;
158
173
  }
159
- if (usable)
174
+ if (answered)
160
175
  return;
161
176
  // The one empty turn that is an answer: the model read a tool result and
162
177
  // chose to stop. An agent whose reply IS a tool (a chat bot's send_message)
@@ -167,6 +182,12 @@ export async function* requireContent(provider, chunks, opts = {}) {
167
182
  yield* held;
168
183
  return;
169
184
  }
185
+ if (finish === "length") {
186
+ const spent = usage?.reasoningTokens
187
+ ? ` (${usage.reasoningTokens} of ${usage.outputTokens} output tokens went to reasoning)`
188
+ : "";
189
+ throw new ProviderError(provider, "invalid", `${provider}: the output cap ran out before any answer${spent} — raise maxTokens or lower effort`);
190
+ }
170
191
  throw new ProviderError(provider, "overload", `${provider}: completed with no content`);
171
192
  }
172
193
  /**
@@ -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.16.1",
3
+ "version": "0.17.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",
package/src/watchdog.ts CHANGED
@@ -34,6 +34,7 @@ import type {
34
34
  Provider,
35
35
  ProviderChunk,
36
36
  StreamOptions,
37
+ TokenUsage,
37
38
  ToolDefinition,
38
39
  } from "./types.ts";
39
40
 
@@ -167,19 +168,29 @@ export async function* watchChunks<T>(
167
168
  }
168
169
 
169
170
  /**
170
- * Reject a turn that completed but produced nothing usable.
171
+ * Reject a turn that completed without an answer.
171
172
  *
172
- * A stream that ends with no text, no reasoning and no tool call is a failure
173
- * wearing a success's clothes: `stop_reason: end_turn` with zero content
174
- * blocks, which the vendors emit under load and after a thinking block eats
175
- * the whole `max_tokens`. Nothing throws, so nothing retries — the caller
176
- * simply shows a person an empty answer, and the only trace is a bill.
173
+ * A stream that ends with no text and no tool call is a failure wearing a
174
+ * success's clothes: `stop_reason: end_turn` with zero content blocks, which
175
+ * the vendors emit under load, or a thinking block that ate the whole
176
+ * `max_tokens`. Nothing throws, so nothing retries — the caller simply shows a
177
+ * person an empty answer, and the only trace is a bill.
177
178
  *
178
- * Classified `overload` because that is both true and useful: it is theirs and
179
- * temporary, so it is transient (the same model, retried, usually answers) and
180
- * backup-eligible (a model that keeps doing it should be walked away from).
181
- * The throw lands before any chunk is yielded downstream, so the retry rule
182
- * that matters — retry only while nothing was emitted — still holds.
179
+ * Reasoning is not an answer. It still streams the moment it arrives, so a
180
+ * reader can watch the model think, but a turn that only thought has said
181
+ * nothing. Counting it as content is how a review capped at 2,048 tokens on a
182
+ * high-effort model passed this guard 22 times in a row in production: every
183
+ * token went to thinking, the answer was an empty string, and the app's JSON
184
+ * parse failed on it with nothing to say why.
185
+ *
186
+ * Classified by what fixes it. A `length` finish is the caller's own cap:
187
+ * `invalid`, so nothing retries it into the same cap and no backup model is
188
+ * cooled down for it — the fix is a larger `maxTokens` or a lower effort. Any
189
+ * other empty turn is `overload`, because that is both true and useful: it is
190
+ * theirs and temporary, so it is transient (the same model, retried, usually
191
+ * answers) and backup-eligible (a model that keeps doing it should be walked
192
+ * away from). Until something streams, the empty frames are held back, so the
193
+ * retry rule that matters — retry only while nothing was emitted — still holds.
183
194
  */
184
195
  export async function* requireContent<T extends ProviderChunk>(
185
196
  provider: string,
@@ -187,18 +198,22 @@ export async function* requireContent<T extends ProviderChunk>(
187
198
  opts: { afterToolResult?: boolean } = {},
188
199
  ): AsyncGenerator<T> {
189
200
  const held: T[] = [];
190
- let usable = false;
201
+ let streaming = false;
202
+ let answered = false;
191
203
  let finish: ProviderChunk["finishReason"];
204
+ let usage: TokenUsage | undefined;
192
205
 
193
206
  for await (const chunk of chunks) {
194
207
  if (chunk.type === "finish") finish = chunk.finishReason;
195
- if (!usable) {
196
- usable = Boolean(chunk.content || chunk.reasoning || chunk.toolCalls?.length);
208
+ if (chunk.usage) usage = chunk.usage;
209
+ answered ||= Boolean(chunk.content || chunk.toolCalls?.length);
210
+ if (!streaming) {
211
+ streaming = answered || Boolean(chunk.reasoning);
197
212
  // Held rather than forwarded: once a chunk is out, the stream is
198
213
  // committed and the retry this guard exists to trigger can no longer
199
- // fire. Nothing content-bearing has arrived yet, so there is nothing to
214
+ // fire. Nothing has arrived worth showing yet, so there is nothing to
200
215
  // hold back but the empty frames.
201
- if (!usable) {
216
+ if (!streaming) {
202
217
  held.push(chunk);
203
218
  continue;
204
219
  }
@@ -208,7 +223,7 @@ export async function* requireContent<T extends ProviderChunk>(
208
223
  yield chunk;
209
224
  }
210
225
 
211
- if (usable) return;
226
+ if (answered) return;
212
227
  // The one empty turn that is an answer: the model read a tool result and
213
228
  // chose to stop. An agent whose reply IS a tool (a chat bot's send_message)
214
229
  // ends every turn this way — retried, cc-proxy measured one such bot
@@ -218,6 +233,16 @@ export async function* requireContent<T extends ProviderChunk>(
218
233
  yield* held;
219
234
  return;
220
235
  }
236
+ if (finish === "length") {
237
+ const spent = usage?.reasoningTokens
238
+ ? ` (${usage.reasoningTokens} of ${usage.outputTokens} output tokens went to reasoning)`
239
+ : "";
240
+ throw new ProviderError(
241
+ provider,
242
+ "invalid",
243
+ `${provider}: the output cap ran out before any answer${spent} — raise maxTokens or lower effort`,
244
+ );
245
+ }
221
246
  throw new ProviderError(provider, "overload", `${provider}: completed with no content`);
222
247
  }
223
248