@providerkit/core 0.16.0 → 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 +22 -12
- package/dist/watchdog.d.ts.map +1 -1
- package/dist/watchdog.js +43 -18
- package/dist/watchdog.js.map +1 -1
- package/package.json +1 -1
- package/src/watchdog.ts +48 -19
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;
|
|
@@ -63,7 +73,7 @@ export interface WatchdogOptions {
|
|
|
63
73
|
* Defaults to `STREAM_IDLE_MS`. */
|
|
64
74
|
idleMs?: number;
|
|
65
75
|
/** No chunk this long, from the POST on, and the stream is going nowhere.
|
|
66
|
-
* Defaults to `STREAM_PROGRESS_MS
|
|
76
|
+
* Defaults to `STREAM_PROGRESS_MS`, or to `idleMs` when that is longer. */
|
|
67
77
|
progressMs?: number;
|
|
68
78
|
/**
|
|
69
79
|
* Reject a turn that ends having said nothing, as `requireContent` does. On
|
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
|
@@ -38,7 +38,11 @@ export const STREAM_PROGRESS_MS = 300_000;
|
|
|
38
38
|
export function streamWatch(opts = {}) {
|
|
39
39
|
const provider = opts.provider ?? "provider";
|
|
40
40
|
const idleMs = opts.idleMs ?? STREAM_IDLE_MS;
|
|
41
|
-
|
|
41
|
+
// Never shorter than the idle clock unless the caller says so. Progress is
|
|
42
|
+
// the PATIENT clock; a caller who raised `idleMs` past five minutes (to
|
|
43
|
+
// keep an outer watch from cutting a model chain's own rotation) was still
|
|
44
|
+
// cut at five, by a clock they never set (@falai/agent, on 0.16.0).
|
|
45
|
+
const progressMs = opts.progressMs ?? Math.max(STREAM_PROGRESS_MS, idleMs);
|
|
42
46
|
const callerSignal = opts.signal;
|
|
43
47
|
const started = Date.now();
|
|
44
48
|
const timeout = new AbortController();
|
|
@@ -116,34 +120,49 @@ export async function* watchChunks(watch, chunks) {
|
|
|
116
120
|
}
|
|
117
121
|
}
|
|
118
122
|
/**
|
|
119
|
-
* Reject a turn that completed
|
|
123
|
+
* Reject a turn that completed without an answer.
|
|
120
124
|
*
|
|
121
|
-
* A stream that ends with no text
|
|
122
|
-
*
|
|
123
|
-
*
|
|
124
|
-
*
|
|
125
|
-
*
|
|
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.
|
|
126
130
|
*
|
|
127
|
-
*
|
|
128
|
-
*
|
|
129
|
-
*
|
|
130
|
-
*
|
|
131
|
-
*
|
|
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.
|
|
132
146
|
*/
|
|
133
147
|
export async function* requireContent(provider, chunks, opts = {}) {
|
|
134
148
|
const held = [];
|
|
135
|
-
let
|
|
149
|
+
let streaming = false;
|
|
150
|
+
let answered = false;
|
|
136
151
|
let finish;
|
|
152
|
+
let usage;
|
|
137
153
|
for await (const chunk of chunks) {
|
|
138
154
|
if (chunk.type === "finish")
|
|
139
155
|
finish = chunk.finishReason;
|
|
140
|
-
if (
|
|
141
|
-
|
|
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);
|
|
142
161
|
// Held rather than forwarded: once a chunk is out, the stream is
|
|
143
162
|
// committed and the retry this guard exists to trigger can no longer
|
|
144
|
-
// fire. Nothing
|
|
163
|
+
// fire. Nothing has arrived worth showing yet, so there is nothing to
|
|
145
164
|
// hold back but the empty frames.
|
|
146
|
-
if (!
|
|
165
|
+
if (!streaming) {
|
|
147
166
|
held.push(chunk);
|
|
148
167
|
continue;
|
|
149
168
|
}
|
|
@@ -152,7 +171,7 @@ export async function* requireContent(provider, chunks, opts = {}) {
|
|
|
152
171
|
}
|
|
153
172
|
yield chunk;
|
|
154
173
|
}
|
|
155
|
-
if (
|
|
174
|
+
if (answered)
|
|
156
175
|
return;
|
|
157
176
|
// The one empty turn that is an answer: the model read a tool result and
|
|
158
177
|
// chose to stop. An agent whose reply IS a tool (a chat bot's send_message)
|
|
@@ -163,6 +182,12 @@ export async function* requireContent(provider, chunks, opts = {}) {
|
|
|
163
182
|
yield* held;
|
|
164
183
|
return;
|
|
165
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
|
+
}
|
|
166
191
|
throw new ProviderError(provider, "overload", `${provider}: completed with no content`);
|
|
167
192
|
}
|
|
168
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":"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;AAU5C;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,2EAA2E;IAC3E,wEAAwE;IACxE,2EAA2E;IAC3E,oEAAoE;IACpE,MAAM,UAAU,GAAG,IAAI,CAAC,UAAU,IAAI,IAAI,CAAC,GAAG,CAAC,kBAAkB,EAAE,MAAM,CAAC,CAAC;IAC3E,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;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,MAAM,CAAC,KAAK,SAAS,CAAC,CAAC,cAAc,CACnC,QAAgB,EAChB,MAAwB,EACxB,OAAsC,EAAE;IAExC,MAAM,IAAI,GAAQ,EAAE,CAAC;IACrB,IAAI,SAAS,GAAG,KAAK,CAAC;IACtB,IAAI,QAAQ,GAAG,KAAK,CAAC;IACrB,IAAI,MAAqC,CAAC;IAC1C,IAAI,KAA6B,CAAC;IAElC,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,KAAK,CAAC,KAAK;YAAE,KAAK,GAAG,KAAK,CAAC,KAAK,CAAC;QACrC,QAAQ,KAAK,OAAO,CAAC,KAAK,CAAC,OAAO,IAAI,KAAK,CAAC,SAAS,EAAE,MAAM,CAAC,CAAC;QAC/D,IAAI,CAAC,SAAS,EAAE,CAAC;YACf,SAAS,GAAG,QAAQ,IAAI,OAAO,CAAC,KAAK,CAAC,SAAS,CAAC,CAAC;YACjD,iEAAiE;YACjE,qEAAqE;YACrE,sEAAsE;YACtE,kCAAkC;YAClC,IAAI,CAAC,SAAS,EAAE,CAAC;gBACf,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,QAAQ;QAAE,OAAO;IACrB,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,IAAI,MAAM,KAAK,QAAQ,EAAE,CAAC;QACxB,MAAM,KAAK,GAAG,KAAK,EAAE,eAAe;YAClC,CAAC,CAAC,KAAK,KAAK,CAAC,eAAe,OAAO,KAAK,CAAC,YAAY,mCAAmC;YACxF,CAAC,CAAC,EAAE,CAAC;QACP,MAAM,IAAI,aAAa,CACrB,QAAQ,EACR,SAAS,EACT,GAAG,QAAQ,6CAA6C,KAAK,oCAAoC,CAClG,CAAC;IACJ,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
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
|
|
|
@@ -78,7 +79,11 @@ export interface StreamWatchOptions {
|
|
|
78
79
|
export function streamWatch(opts: StreamWatchOptions = {}): StreamWatch {
|
|
79
80
|
const provider = opts.provider ?? "provider";
|
|
80
81
|
const idleMs = opts.idleMs ?? STREAM_IDLE_MS;
|
|
81
|
-
|
|
82
|
+
// Never shorter than the idle clock unless the caller says so. Progress is
|
|
83
|
+
// the PATIENT clock; a caller who raised `idleMs` past five minutes (to
|
|
84
|
+
// keep an outer watch from cutting a model chain's own rotation) was still
|
|
85
|
+
// cut at five, by a clock they never set (@falai/agent, on 0.16.0).
|
|
86
|
+
const progressMs = opts.progressMs ?? Math.max(STREAM_PROGRESS_MS, idleMs);
|
|
82
87
|
const callerSignal = opts.signal;
|
|
83
88
|
const started = Date.now();
|
|
84
89
|
const timeout = new AbortController();
|
|
@@ -163,19 +168,29 @@ export async function* watchChunks<T>(
|
|
|
163
168
|
}
|
|
164
169
|
|
|
165
170
|
/**
|
|
166
|
-
* Reject a turn that completed
|
|
171
|
+
* Reject a turn that completed without an answer.
|
|
167
172
|
*
|
|
168
|
-
* A stream that ends with no text
|
|
169
|
-
*
|
|
170
|
-
*
|
|
171
|
-
*
|
|
172
|
-
*
|
|
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.
|
|
173
178
|
*
|
|
174
|
-
*
|
|
175
|
-
*
|
|
176
|
-
*
|
|
177
|
-
*
|
|
178
|
-
*
|
|
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.
|
|
179
194
|
*/
|
|
180
195
|
export async function* requireContent<T extends ProviderChunk>(
|
|
181
196
|
provider: string,
|
|
@@ -183,18 +198,22 @@ export async function* requireContent<T extends ProviderChunk>(
|
|
|
183
198
|
opts: { afterToolResult?: boolean } = {},
|
|
184
199
|
): AsyncGenerator<T> {
|
|
185
200
|
const held: T[] = [];
|
|
186
|
-
let
|
|
201
|
+
let streaming = false;
|
|
202
|
+
let answered = false;
|
|
187
203
|
let finish: ProviderChunk["finishReason"];
|
|
204
|
+
let usage: TokenUsage | undefined;
|
|
188
205
|
|
|
189
206
|
for await (const chunk of chunks) {
|
|
190
207
|
if (chunk.type === "finish") finish = chunk.finishReason;
|
|
191
|
-
if (
|
|
192
|
-
|
|
208
|
+
if (chunk.usage) usage = chunk.usage;
|
|
209
|
+
answered ||= Boolean(chunk.content || chunk.toolCalls?.length);
|
|
210
|
+
if (!streaming) {
|
|
211
|
+
streaming = answered || Boolean(chunk.reasoning);
|
|
193
212
|
// Held rather than forwarded: once a chunk is out, the stream is
|
|
194
213
|
// committed and the retry this guard exists to trigger can no longer
|
|
195
|
-
// fire. Nothing
|
|
214
|
+
// fire. Nothing has arrived worth showing yet, so there is nothing to
|
|
196
215
|
// hold back but the empty frames.
|
|
197
|
-
if (!
|
|
216
|
+
if (!streaming) {
|
|
198
217
|
held.push(chunk);
|
|
199
218
|
continue;
|
|
200
219
|
}
|
|
@@ -204,7 +223,7 @@ export async function* requireContent<T extends ProviderChunk>(
|
|
|
204
223
|
yield chunk;
|
|
205
224
|
}
|
|
206
225
|
|
|
207
|
-
if (
|
|
226
|
+
if (answered) return;
|
|
208
227
|
// The one empty turn that is an answer: the model read a tool result and
|
|
209
228
|
// chose to stop. An agent whose reply IS a tool (a chat bot's send_message)
|
|
210
229
|
// ends every turn this way — retried, cc-proxy measured one such bot
|
|
@@ -214,6 +233,16 @@ export async function* requireContent<T extends ProviderChunk>(
|
|
|
214
233
|
yield* held;
|
|
215
234
|
return;
|
|
216
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
|
+
}
|
|
217
246
|
throw new ProviderError(provider, "overload", `${provider}: completed with no content`);
|
|
218
247
|
}
|
|
219
248
|
|
|
@@ -222,7 +251,7 @@ export interface WatchdogOptions {
|
|
|
222
251
|
* Defaults to `STREAM_IDLE_MS`. */
|
|
223
252
|
idleMs?: number;
|
|
224
253
|
/** No chunk this long, from the POST on, and the stream is going nowhere.
|
|
225
|
-
* Defaults to `STREAM_PROGRESS_MS
|
|
254
|
+
* Defaults to `STREAM_PROGRESS_MS`, or to `idleMs` when that is longer. */
|
|
226
255
|
progressMs?: number;
|
|
227
256
|
/**
|
|
228
257
|
* Reject a turn that ends having said nothing, as `requireContent` does. On
|