@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.
- package/dist/watchdog.d.ts +21 -11
- package/dist/watchdog.d.ts.map +1 -1
- package/dist/watchdog.js +38 -17
- package/dist/watchdog.js.map +1 -1
- package/package.json +1 -1
- package/src/watchdog.ts +42 -17
package/dist/watchdog.d.ts
CHANGED
|
@@ -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
|
|
44
|
+
* Reject a turn that completed without an answer.
|
|
45
45
|
*
|
|
46
|
-
* A stream that ends with no text
|
|
47
|
-
*
|
|
48
|
-
*
|
|
49
|
-
*
|
|
50
|
-
*
|
|
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
|
-
*
|
|
53
|
-
*
|
|
54
|
-
*
|
|
55
|
-
*
|
|
56
|
-
*
|
|
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;
|
package/dist/watchdog.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"watchdog.d.ts","sourceRoot":"","sources":["../src/watchdog.ts"],"names":[],"mappings":"AA+BA,OAAO,KAAK,EAEV,QAAQ,EACR,aAAa,
|
|
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
|
|
123
|
+
* Reject a turn that completed without an answer.
|
|
124
124
|
*
|
|
125
|
-
* A stream that ends with no text
|
|
126
|
-
*
|
|
127
|
-
*
|
|
128
|
-
*
|
|
129
|
-
*
|
|
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
|
-
*
|
|
132
|
-
*
|
|
133
|
-
*
|
|
134
|
-
*
|
|
135
|
-
*
|
|
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
|
|
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 (
|
|
145
|
-
|
|
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
|
|
163
|
+
// fire. Nothing has arrived worth showing yet, so there is nothing to
|
|
149
164
|
// hold back but the empty frames.
|
|
150
|
-
if (!
|
|
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 (
|
|
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
|
/**
|
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,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;
|
|
1
|
+
{"version":3,"file":"watchdog.js","sourceRoot":"","sources":["../src/watchdog.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
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
|
|
171
|
+
* Reject a turn that completed without an answer.
|
|
171
172
|
*
|
|
172
|
-
* A stream that ends with no text
|
|
173
|
-
*
|
|
174
|
-
*
|
|
175
|
-
*
|
|
176
|
-
*
|
|
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
|
-
*
|
|
179
|
-
*
|
|
180
|
-
*
|
|
181
|
-
*
|
|
182
|
-
*
|
|
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
|
|
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 (
|
|
196
|
-
|
|
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
|
|
214
|
+
// fire. Nothing has arrived worth showing yet, so there is nothing to
|
|
200
215
|
// hold back but the empty frames.
|
|
201
|
-
if (!
|
|
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 (
|
|
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
|
|