@providerkit/core 0.3.0 → 0.4.1
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 +14 -8
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +14 -1
- package/dist/errors.js.map +1 -1
- package/dist/providers/anthropic.d.ts.map +1 -1
- package/dist/providers/anthropic.js +28 -2
- package/dist/providers/anthropic.js.map +1 -1
- package/dist/providers/gemini.d.ts.map +1 -1
- package/dist/providers/gemini.js +4 -0
- package/dist/providers/gemini.js.map +1 -1
- package/dist/providers/openai.d.ts +9 -2
- package/dist/providers/openai.d.ts.map +1 -1
- package/dist/providers/openai.js +20 -4
- package/dist/providers/openai.js.map +1 -1
- package/dist/providers/responses.d.ts.map +1 -1
- package/dist/providers/responses.js +16 -4
- package/dist/providers/responses.js.map +1 -1
- package/dist/schema.d.ts +21 -0
- package/dist/schema.d.ts.map +1 -1
- package/dist/schema.js +40 -0
- package/dist/schema.js.map +1 -1
- package/dist/types.d.ts +23 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +15 -1
- package/dist/types.js.map +1 -1
- package/dist/watchdog.d.ts +46 -0
- package/dist/watchdog.d.ts.map +1 -1
- package/dist/watchdog.js +92 -1
- package/dist/watchdog.js.map +1 -1
- package/package.json +4 -1
- package/src/errors.ts +13 -1
- package/src/providers/anthropic.ts +28 -1
- package/src/providers/gemini.ts +2 -0
- package/src/providers/openai.ts +18 -4
- package/src/providers/responses.ts +15 -4
- package/src/schema.ts +41 -0
- package/src/types.ts +37 -1
- package/src/watchdog.ts +124 -1
package/dist/schema.js
CHANGED
|
@@ -45,4 +45,44 @@ export function clampToSchema(value, node) {
|
|
|
45
45
|
}
|
|
46
46
|
return value;
|
|
47
47
|
}
|
|
48
|
+
/**
|
|
49
|
+
* Whether OpenAI's `strict` schema mode will accept this schema.
|
|
50
|
+
*
|
|
51
|
+
* Strict is the only JSON mode that actually guarantees the shape, so it is
|
|
52
|
+
* worth having — but it demands more than JSON Schema does: every property an
|
|
53
|
+
* object lists must ALSO be required, and every object must close itself with
|
|
54
|
+
* `additionalProperties: false`, all the way down. A schema with one optional
|
|
55
|
+
* field is not "mostly strict"; it is a flat 400 naming a nested path rather
|
|
56
|
+
* than the rule it broke.
|
|
57
|
+
*
|
|
58
|
+
* That trap is the reason this exists. An agent's response schema grows
|
|
59
|
+
* optional fields naturally — a `data` block only some flows fill, the fields
|
|
60
|
+
* one step collects — and a caller who adds one wants their answer, not a
|
|
61
|
+
* lecture about a mode they never asked for. So the OpenAI-shape adapters ask
|
|
62
|
+
* this and drop to plain (unenforced) schema mode instead of failing the turn.
|
|
63
|
+
*
|
|
64
|
+
* Anything it cannot verify — a `$ref`, a composed `allOf` — answers false:
|
|
65
|
+
* the cost of guessing wrong that way is unenforced output, and the cost of
|
|
66
|
+
* guessing wrong the other way is a request that cannot succeed at all.
|
|
67
|
+
*/
|
|
68
|
+
export function isStrictSchema(node) {
|
|
69
|
+
if (!node || typeof node !== "object")
|
|
70
|
+
return false;
|
|
71
|
+
const schema = node;
|
|
72
|
+
if (schema.$ref !== undefined || schema.allOf !== undefined)
|
|
73
|
+
return false;
|
|
74
|
+
const union = schema.anyOf ?? schema.oneOf;
|
|
75
|
+
if (Array.isArray(union))
|
|
76
|
+
return union.every(isStrictSchema);
|
|
77
|
+
// A leaf (string, number, boolean, enum) carries no strict obligations.
|
|
78
|
+
if (schema.type === "array")
|
|
79
|
+
return schema.items === undefined || isStrictSchema(schema.items);
|
|
80
|
+
if (schema.properties === undefined)
|
|
81
|
+
return true;
|
|
82
|
+
if (schema.additionalProperties !== false)
|
|
83
|
+
return false;
|
|
84
|
+
const properties = schema.properties;
|
|
85
|
+
const required = new Set(Array.isArray(schema.required) ? schema.required : []);
|
|
86
|
+
return Object.entries(properties).every(([key, value]) => required.has(key) && isStrictSchema(value));
|
|
87
|
+
}
|
|
48
88
|
//# sourceMappingURL=schema.js.map
|
package/dist/schema.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"schema.js","sourceRoot":"","sources":["../src/schema.ts"],"names":[],"mappings":"AAgBA,MAAM,UAAU,aAAa,CAAC,KAAc,EAAE,IAAa;IACzD,IAAI,CAAC,IAAI,IAAI,OAAO,IAAI,KAAK,QAAQ;QAAE,OAAO,KAAK,CAAC;IACpD,MAAM,MAAM,GAAG,IAAkB,CAAC;IAElC,8EAA8E;IAC9E,4EAA4E;IAC5E,MAAM,KAAK,GAAG,MAAM,CAAC,KAAK,IAAI,MAAM,CAAC,KAAK,CAAC;IAC3C,IAAI,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,EAAE,CAAC;QACzB,OAAO,KAAK,CAAC,MAAM,CAAU,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE,CAAC,aAAa,CAAC,OAAO,EAAE,MAAM,CAAC,EAAE,KAAK,CAAC,CAAC;IAC3F,CAAC;IAED,IAAI,OAAO,KAAK,KAAK,QAAQ,EAAE,CAAC;QAC9B,MAAM,GAAG,GAAG,MAAM,CAAC,SAAS,CAAC;QAC7B,2EAA2E;QAC3E,yEAAyE;QACzE,oBAAoB;QACpB,IAAI,OAAO,GAAG,KAAK,QAAQ,IAAI,GAAG,IAAI,CAAC,IAAI,KAAK,CAAC,MAAM,GAAG,GAAG,EAAE,CAAC;YAC9D,OAAO,GAAG,KAAK,CAAC,KAAK,CAAC,CAAC,EAAE,GAAG,GAAG,CAAC,CAAC,GAAG,CAAC;QACvC,CAAC;QACD,OAAO,KAAK,CAAC;IACf,CAAC;IAED,IAAI,OAAO,KAAK,KAAK,QAAQ,EAAE,CAAC;QAC9B,IAAI,GAAG,GAAG,KAAK,CAAC;QAChB,IAAI,OAAO,MAAM,CAAC,OAAO,KAAK,QAAQ,IAAI,GAAG,GAAG,MAAM,CAAC,OAAO;YAAE,GAAG,GAAG,MAAM,CAAC,OAAO,CAAC;QACrF,IAAI,OAAO,MAAM,CAAC,OAAO,KAAK,QAAQ,IAAI,GAAG,GAAG,MAAM,CAAC,OAAO;YAAE,GAAG,GAAG,MAAM,CAAC,OAAO,CAAC;QACrF,OAAO,GAAG,CAAC;IACb,CAAC;IAED,IAAI,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,EAAE,CAAC;QACzB,MAAM,KAAK,GAAG,KAAK,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,aAAa,CAAC,IAAI,EAAE,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC;QACrE,MAAM,GAAG,GAAG,MAAM,CAAC,QAAQ,CAAC;QAC5B,OAAO,OAAO,GAAG,KAAK,QAAQ,IAAI,KAAK,CAAC,MAAM,GAAG,GAAG,CAAC,CAAC,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC,EAAE,GAAG,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC;IACrF,CAAC;IAED,IACE,KAAK;QACL,OAAO,KAAK,KAAK,QAAQ;QACzB,MAAM,CAAC,UAAU;QACjB,OAAO,MAAM,CAAC,UAAU,KAAK,QAAQ,EACrC,CAAC;QACD,MAAM,UAAU,GAAG,MAAM,CAAC,UAAqC,CAAC;QAChE,MAAM,GAAG,GAA4B,EAAE,GAAI,KAAiC,EAAE,CAAC;QAC/E,KAAK,MAAM,GAAG,IAAI,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC;YACnC,IAAI,GAAG,IAAI,UAAU;gBAAE,GAAG,CAAC,GAAG,CAAC,GAAG,aAAa,CAAC,GAAG,CAAC,GAAG,CAAC,EAAE,UAAU,CAAC,GAAG,CAAC,CAAC,CAAC;QAC7E,CAAC;QACD,OAAO,GAAG,CAAC;IACb,CAAC;IAED,OAAO,KAAK,CAAC;AACf,CAAC"}
|
|
1
|
+
{"version":3,"file":"schema.js","sourceRoot":"","sources":["../src/schema.ts"],"names":[],"mappings":"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"}
|
package/dist/types.d.ts
CHANGED
|
@@ -134,6 +134,13 @@ export type ToolChoice = "auto" | "none" | "required" | {
|
|
|
134
134
|
export interface JsonOutput {
|
|
135
135
|
name: string;
|
|
136
136
|
schema: JsonObjectSchema;
|
|
137
|
+
/**
|
|
138
|
+
* Force OpenAI's strict schema mode on or off. Left unset, the adapters ask
|
|
139
|
+
* `isStrictSchema` and enforce whenever the schema actually qualifies —
|
|
140
|
+
* which is what keeps an optional field from turning a working call into a
|
|
141
|
+
* 400. Set it only to overrule that reading.
|
|
142
|
+
*/
|
|
143
|
+
strict?: boolean;
|
|
137
144
|
}
|
|
138
145
|
export interface StreamOptions {
|
|
139
146
|
/** Override the provider's bound model for this call. */
|
|
@@ -144,6 +151,20 @@ export interface StreamOptions {
|
|
|
144
151
|
* silently truncated mid-argument. */
|
|
145
152
|
maxTokens?: number;
|
|
146
153
|
temperature?: number;
|
|
154
|
+
/**
|
|
155
|
+
* Nucleus sampling. Set this OR `temperature`, not both — the vendors all
|
|
156
|
+
* document them as alternatives and some reject the pair outright.
|
|
157
|
+
*/
|
|
158
|
+
topP?: number;
|
|
159
|
+
/**
|
|
160
|
+
* Strings that end the turn when generated. The only four-shape sampling
|
|
161
|
+
* field beyond these two; `top_k`, `metadata` and the rest are one vendor's
|
|
162
|
+
* each and stay off the seam, where a caller reaching for them is asking for
|
|
163
|
+
* that vendor rather than for a provider.
|
|
164
|
+
*
|
|
165
|
+
* Not sent on the Responses shape, which has no equivalent.
|
|
166
|
+
*/
|
|
167
|
+
stopSequences?: string[];
|
|
147
168
|
signal?: AbortSignal;
|
|
148
169
|
toolChoice?: ToolChoice;
|
|
149
170
|
json?: JsonOutput;
|
|
@@ -156,6 +177,8 @@ export interface Provider {
|
|
|
156
177
|
export interface Completion {
|
|
157
178
|
text: string;
|
|
158
179
|
reasoning: string;
|
|
180
|
+
/** Present only where the provider sent one. See ChatMessage.reasoningDetails. */
|
|
181
|
+
reasoningDetails?: unknown[];
|
|
159
182
|
usage: TokenUsage;
|
|
160
183
|
finishReason: FinishReason | null;
|
|
161
184
|
model: string;
|
package/dist/types.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AASA;;;;;;;;GAQG;AACH,eAAO,MAAM,OAAO,mDAAoD,CAAC;AACzE,MAAM,MAAM,MAAM,GAAG,CAAC,OAAO,OAAO,CAAC,CAAC,MAAM,CAAC,CAAC;AAE9C,2EAA2E;AAC3E,wBAAgB,QAAQ,CAAC,KAAK,EAAE,MAAM,GAAG,KAAK,IAAI,MAAM,CAEvD;AAED,MAAM,MAAM,aAAa,GAAG,YAAY,GAAG,WAAW,GAAG,YAAY,GAAG,WAAW,CAAC;AAEpF,MAAM,WAAW,QAAQ;IACvB,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;CACd;AAED;kDACkD;AAClD,MAAM,WAAW,SAAS;IACxB,IAAI,EAAE,OAAO,CAAC;IACd,QAAQ,EAAE,aAAa,CAAC;IACxB,IAAI,EAAE,MAAM,CAAC;CACd;AAED,MAAM,MAAM,WAAW,GAAG,QAAQ,GAAG,SAAS,CAAC;AAE/C;;;;GAIG;AACH,MAAM,WAAW,QAAQ;IACvB,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,EAAE,MAAM,CAAC;IAClB;gEAC4D;IAC5D,gBAAgB,CAAC,EAAE,MAAM,CAAC;CAC3B;AAED,MAAM,MAAM,WAAW,GACnB;IAAE,IAAI,EAAE,QAAQ,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,GACnC;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,GAAG,WAAW,EAAE,CAAA;CAAE,GACjD;IACE,IAAI,EAAE,WAAW,CAAC;IAClB,OAAO,EAAE,MAAM,CAAC;IAChB;;;;;;;;OAQG;IACH,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB;;;;;;;;OAQG;IACH,gBAAgB,CAAC,EAAE,OAAO,EAAE,CAAC;IAC7B,SAAS,CAAC,EAAE,QAAQ,EAAE,CAAC;CACxB,GACD;IACE,IAAI,EAAE,MAAM,CAAC;IACb,UAAU,EAAE,MAAM,CAAC;IACnB,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;IAChB,iEAAiE;IACjE,MAAM,CAAC,EAAE,SAAS,EAAE,CAAC;CACtB,CAAC;AAEN,6EAA6E;AAC7E,MAAM,WAAW,gBAAgB;IAC/B,IAAI,EAAE,QAAQ,CAAC;IACf,UAAU,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACrC,QAAQ,CAAC,EAAE,MAAM,EAAE,CAAC;IACpB,oBAAoB,CAAC,EAAE,OAAO,CAAC;IAC/B,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;CACxB;AAED,MAAM,WAAW,cAAc;IAC7B,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,MAAM,CAAC;IACpB,WAAW,EAAE,gBAAgB,CAAC;CAC/B;AAED,MAAM,MAAM,YAAY,GAAG,MAAM,GAAG,QAAQ,GAAG,YAAY,GAAG,gBAAgB,CAAC;AAE/E,4EAA4E;AAC5E,MAAM,WAAW,aAAa;IAC5B,KAAK,EAAE,MAAM,CAAC;IACd,EAAE,CAAC,EAAE,MAAM,CAAC;IACZ,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,gBAAgB,CAAC,EAAE,MAAM,CAAC;CAC3B;AAED,MAAM,WAAW,UAAU;IACzB,WAAW,EAAE,MAAM,CAAC;IACpB;;iFAE6E;IAC7E,iBAAiB,EAAE,MAAM,CAAC;IAC1B;8EAC0E;IAC1E,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,YAAY,EAAE,MAAM,CAAC;CACtB;AAED,eAAO,MAAM,WAAW,EAAE,UAIzB,CAAC;AAEF,+EAA+E;AAC/E,MAAM,WAAW,aAAa;IAC5B,IAAI,EAAE,OAAO,GAAG,OAAO,GAAG,QAAQ,CAAC;IACnC,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB;+EAC2E;IAC3E,gBAAgB,CAAC,EAAE,OAAO,EAAE,CAAC;IAC7B,SAAS,CAAC,EAAE,aAAa,EAAE,CAAC;IAC5B,KAAK,CAAC,EAAE,UAAU,CAAC;IACnB,YAAY,CAAC,EAAE,YAAY,CAAC;CAC7B;AAED;0DAC0D;AAC1D,MAAM,MAAM,UAAU,GAAG,MAAM,GAAG,MAAM,GAAG,UAAU,GAAG;IAAE,IAAI,EAAE,MAAM,CAAA;CAAE,CAAC;AAEzE;;;;GAIG;AACH,MAAM,WAAW,UAAU;IACzB,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,gBAAgB,CAAC;
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AASA;;;;;;;;GAQG;AACH,eAAO,MAAM,OAAO,mDAAoD,CAAC;AACzE,MAAM,MAAM,MAAM,GAAG,CAAC,OAAO,OAAO,CAAC,CAAC,MAAM,CAAC,CAAC;AAE9C,2EAA2E;AAC3E,wBAAgB,QAAQ,CAAC,KAAK,EAAE,MAAM,GAAG,KAAK,IAAI,MAAM,CAEvD;AAED,MAAM,MAAM,aAAa,GAAG,YAAY,GAAG,WAAW,GAAG,YAAY,GAAG,WAAW,CAAC;AAEpF,MAAM,WAAW,QAAQ;IACvB,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;CACd;AAED;kDACkD;AAClD,MAAM,WAAW,SAAS;IACxB,IAAI,EAAE,OAAO,CAAC;IACd,QAAQ,EAAE,aAAa,CAAC;IACxB,IAAI,EAAE,MAAM,CAAC;CACd;AAED,MAAM,MAAM,WAAW,GAAG,QAAQ,GAAG,SAAS,CAAC;AAE/C;;;;GAIG;AACH,MAAM,WAAW,QAAQ;IACvB,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,EAAE,MAAM,CAAC;IAClB;gEAC4D;IAC5D,gBAAgB,CAAC,EAAE,MAAM,CAAC;CAC3B;AAED,MAAM,MAAM,WAAW,GACnB;IAAE,IAAI,EAAE,QAAQ,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,GACnC;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,GAAG,WAAW,EAAE,CAAA;CAAE,GACjD;IACE,IAAI,EAAE,WAAW,CAAC;IAClB,OAAO,EAAE,MAAM,CAAC;IAChB;;;;;;;;OAQG;IACH,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB;;;;;;;;OAQG;IACH,gBAAgB,CAAC,EAAE,OAAO,EAAE,CAAC;IAC7B,SAAS,CAAC,EAAE,QAAQ,EAAE,CAAC;CACxB,GACD;IACE,IAAI,EAAE,MAAM,CAAC;IACb,UAAU,EAAE,MAAM,CAAC;IACnB,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;IAChB,iEAAiE;IACjE,MAAM,CAAC,EAAE,SAAS,EAAE,CAAC;CACtB,CAAC;AAEN,6EAA6E;AAC7E,MAAM,WAAW,gBAAgB;IAC/B,IAAI,EAAE,QAAQ,CAAC;IACf,UAAU,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACrC,QAAQ,CAAC,EAAE,MAAM,EAAE,CAAC;IACpB,oBAAoB,CAAC,EAAE,OAAO,CAAC;IAC/B,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;CACxB;AAED,MAAM,WAAW,cAAc;IAC7B,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,MAAM,CAAC;IACpB,WAAW,EAAE,gBAAgB,CAAC;CAC/B;AAED,MAAM,MAAM,YAAY,GAAG,MAAM,GAAG,QAAQ,GAAG,YAAY,GAAG,gBAAgB,CAAC;AAE/E,4EAA4E;AAC5E,MAAM,WAAW,aAAa;IAC5B,KAAK,EAAE,MAAM,CAAC;IACd,EAAE,CAAC,EAAE,MAAM,CAAC;IACZ,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,gBAAgB,CAAC,EAAE,MAAM,CAAC;CAC3B;AAED,MAAM,WAAW,UAAU;IACzB,WAAW,EAAE,MAAM,CAAC;IACpB;;iFAE6E;IAC7E,iBAAiB,EAAE,MAAM,CAAC;IAC1B;8EAC0E;IAC1E,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,YAAY,EAAE,MAAM,CAAC;CACtB;AAED,eAAO,MAAM,WAAW,EAAE,UAIzB,CAAC;AAEF,+EAA+E;AAC/E,MAAM,WAAW,aAAa;IAC5B,IAAI,EAAE,OAAO,GAAG,OAAO,GAAG,QAAQ,CAAC;IACnC,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB;+EAC2E;IAC3E,gBAAgB,CAAC,EAAE,OAAO,EAAE,CAAC;IAC7B,SAAS,CAAC,EAAE,aAAa,EAAE,CAAC;IAC5B,KAAK,CAAC,EAAE,UAAU,CAAC;IACnB,YAAY,CAAC,EAAE,YAAY,CAAC;CAC7B;AAED;0DAC0D;AAC1D,MAAM,MAAM,UAAU,GAAG,MAAM,GAAG,MAAM,GAAG,UAAU,GAAG;IAAE,IAAI,EAAE,MAAM,CAAA;CAAE,CAAC;AAEzE;;;;GAIG;AACH,MAAM,WAAW,UAAU;IACzB,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,gBAAgB,CAAC;IACzB;;;;;OAKG;IACH,MAAM,CAAC,EAAE,OAAO,CAAC;CAClB;AAED,MAAM,WAAW,aAAa;IAC5B,yDAAyD;IACzD,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB;;2CAEuC;IACvC,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB;;;OAGG;IACH,IAAI,CAAC,EAAE,MAAM,CAAC;IACd;;;;;;;OAOG;IACH,aAAa,CAAC,EAAE,MAAM,EAAE,CAAC;IACzB,MAAM,CAAC,EAAE,WAAW,CAAC;IACrB,UAAU,CAAC,EAAE,UAAU,CAAC;IACxB,IAAI,CAAC,EAAE,UAAU,CAAC;CACnB;AAED,MAAM,WAAW,QAAQ;IACvB,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;IACpB,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,YAAY,CACV,QAAQ,EAAE,WAAW,EAAE,EACvB,KAAK,EAAE,cAAc,EAAE,EACvB,IAAI,CAAC,EAAE,aAAa,GACnB,aAAa,CAAC,aAAa,CAAC,CAAC;CACjC;AAED,MAAM,WAAW,UAAU;IACzB,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,EAAE,MAAM,CAAC;IAClB,kFAAkF;IAClF,gBAAgB,CAAC,EAAE,OAAO,EAAE,CAAC;IAC7B,KAAK,EAAE,UAAU,CAAC;IAClB,YAAY,EAAE,YAAY,GAAG,IAAI,CAAC;IAClC,KAAK,EAAE,MAAM,CAAC;CACf;AAED;kEACkE;AAClE,wBAAsB,WAAW,CAC/B,MAAM,EAAE,aAAa,CAAC,aAAa,CAAC,EACpC,KAAK,EAAE,MAAM,GACZ,OAAO,CAAC,UAAU,CAAC,CA6BrB;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,cAAc,CAAC,QAAQ,EAAE,SAAS,WAAW,EAAE,GAAG,WAAW,EAAE,CAU9E;AAED,4EAA4E;AAC5E,wBAAgB,SAAS,CAAC,IAAI,EAAE,SAAS,GAAG,MAAM,CAEjD"}
|
package/dist/types.js
CHANGED
|
@@ -30,6 +30,11 @@ export const EMPTY_USAGE = {
|
|
|
30
30
|
export async function drainStream(stream, model) {
|
|
31
31
|
let text = "";
|
|
32
32
|
let reasoning = "";
|
|
33
|
+
// Not concatenated: this half of the record is a payload the provider owns,
|
|
34
|
+
// and it arrives whole on one delta rather than in fragments. Dropped here,
|
|
35
|
+
// a drained turn replays only half its own reasoning on the next round —
|
|
36
|
+
// which is the failure `reasoningDetails` exists to prevent.
|
|
37
|
+
let reasoningDetails;
|
|
33
38
|
let usage = EMPTY_USAGE;
|
|
34
39
|
let finishReason = null;
|
|
35
40
|
for await (const chunk of stream) {
|
|
@@ -38,6 +43,8 @@ export async function drainStream(stream, model) {
|
|
|
38
43
|
text += chunk.content;
|
|
39
44
|
if (chunk.reasoning)
|
|
40
45
|
reasoning += chunk.reasoning;
|
|
46
|
+
if (chunk.reasoningDetails?.length)
|
|
47
|
+
reasoningDetails = chunk.reasoningDetails;
|
|
41
48
|
}
|
|
42
49
|
else if (chunk.type === "usage" && chunk.usage) {
|
|
43
50
|
usage = chunk.usage;
|
|
@@ -46,7 +53,14 @@ export async function drainStream(stream, model) {
|
|
|
46
53
|
finishReason = chunk.finishReason;
|
|
47
54
|
}
|
|
48
55
|
}
|
|
49
|
-
return {
|
|
56
|
+
return {
|
|
57
|
+
text,
|
|
58
|
+
reasoning,
|
|
59
|
+
...(reasoningDetails ? { reasoningDetails } : {}),
|
|
60
|
+
usage,
|
|
61
|
+
finishReason,
|
|
62
|
+
model,
|
|
63
|
+
};
|
|
50
64
|
}
|
|
51
65
|
/**
|
|
52
66
|
* Prepare a history for a thinking-DISABLED turn: strip `reasoning` from every
|
package/dist/types.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,wEAAwE;AACxE,2EAA2E;AAC3E,mEAAmE;AACnE,EAAE;AACF,8EAA8E;AAC9E,8EAA8E;AAC9E,6EAA6E;AAC7E,0BAA0B;AAE1B;;;;;;;;GAQG;AACH,MAAM,CAAC,MAAM,OAAO,GAAG,CAAC,MAAM,EAAE,KAAK,EAAE,QAAQ,EAAE,MAAM,EAAE,KAAK,CAAU,CAAC;AAGzE,2EAA2E;AAC3E,MAAM,UAAU,QAAQ,CAAC,KAAa;IACpC,OAAO,OAAO,CAAC,IAAI,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,MAAM,KAAK,KAAK,CAAC,CAAC;AACpD,CAAC;AA4GD,MAAM,CAAC,MAAM,WAAW,GAAe;IACrC,WAAW,EAAE,CAAC;IACd,iBAAiB,EAAE,CAAC;IACpB,YAAY,EAAE,CAAC;CAChB,CAAC;
|
|
1
|
+
{"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,wEAAwE;AACxE,2EAA2E;AAC3E,mEAAmE;AACnE,EAAE;AACF,8EAA8E;AAC9E,8EAA8E;AAC9E,6EAA6E;AAC7E,0BAA0B;AAE1B;;;;;;;;GAQG;AACH,MAAM,CAAC,MAAM,OAAO,GAAG,CAAC,MAAM,EAAE,KAAK,EAAE,QAAQ,EAAE,MAAM,EAAE,KAAK,CAAU,CAAC;AAGzE,2EAA2E;AAC3E,MAAM,UAAU,QAAQ,CAAC,KAAa;IACpC,OAAO,OAAO,CAAC,IAAI,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,MAAM,KAAK,KAAK,CAAC,CAAC;AACpD,CAAC;AA4GD,MAAM,CAAC,MAAM,WAAW,GAAe;IACrC,WAAW,EAAE,CAAC;IACd,iBAAiB,EAAE,CAAC;IACpB,YAAY,EAAE,CAAC;CAChB,CAAC;AAoFF;kEACkE;AAClE,MAAM,CAAC,KAAK,UAAU,WAAW,CAC/B,MAAoC,EACpC,KAAa;IAEb,IAAI,IAAI,GAAG,EAAE,CAAC;IACd,IAAI,SAAS,GAAG,EAAE,CAAC;IACnB,4EAA4E;IAC5E,4EAA4E;IAC5E,yEAAyE;IACzE,6DAA6D;IAC7D,IAAI,gBAAuC,CAAC;IAC5C,IAAI,KAAK,GAAe,WAAW,CAAC;IACpC,IAAI,YAAY,GAAwB,IAAI,CAAC;IAC7C,IAAI,KAAK,EAAE,MAAM,KAAK,IAAI,MAAM,EAAE,CAAC;QACjC,IAAI,KAAK,CAAC,IAAI,KAAK,OAAO,EAAE,CAAC;YAC3B,IAAI,KAAK,CAAC,OAAO;gBAAE,IAAI,IAAI,KAAK,CAAC,OAAO,CAAC;YACzC,IAAI,KAAK,CAAC,SAAS;gBAAE,SAAS,IAAI,KAAK,CAAC,SAAS,CAAC;YAClD,IAAI,KAAK,CAAC,gBAAgB,EAAE,MAAM;gBAAE,gBAAgB,GAAG,KAAK,CAAC,gBAAgB,CAAC;QAChF,CAAC;aAAM,IAAI,KAAK,CAAC,IAAI,KAAK,OAAO,IAAI,KAAK,CAAC,KAAK,EAAE,CAAC;YACjD,KAAK,GAAG,KAAK,CAAC,KAAK,CAAC;QACtB,CAAC;aAAM,IAAI,KAAK,CAAC,IAAI,KAAK,QAAQ,IAAI,KAAK,CAAC,YAAY,EAAE,CAAC;YACzD,YAAY,GAAG,KAAK,CAAC,YAAY,CAAC;QACpC,CAAC;IACH,CAAC;IACD,OAAO;QACL,IAAI;QACJ,SAAS;QACT,GAAG,CAAC,gBAAgB,CAAC,CAAC,CAAC,EAAE,gBAAgB,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;QACjD,KAAK;QACL,YAAY;QACZ,KAAK;KACN,CAAC;AACJ,CAAC;AAED;;;;;;;;;;;GAWG;AACH,MAAM,UAAU,cAAc,CAAC,QAAgC;IAC7D,OAAO,QAAQ,CAAC,GAAG,CAAC,CAAC,OAAO,EAAE,EAAE;QAC9B,IAAI,OAAO,CAAC,IAAI,KAAK,WAAW;YAAE,OAAO,OAAO,CAAC;QACjD,IAAI,OAAO,CAAC,SAAS,KAAK,SAAS,IAAI,OAAO,CAAC,gBAAgB,KAAK,SAAS;YAAE,OAAO,OAAO,CAAC;QAC9F,yEAAyE;QACzE,yEAAyE;QACzE,iEAAiE;QACjE,MAAM,EAAE,SAAS,EAAE,KAAK,EAAE,gBAAgB,EAAE,QAAQ,EAAE,GAAG,IAAI,EAAE,GAAG,OAAO,CAAC;QAC1E,OAAO,IAAI,CAAC;IACd,CAAC,CAAC,CAAC;AACL,CAAC;AAED,4EAA4E;AAC5E,MAAM,UAAU,SAAS,CAAC,IAAe;IACvC,OAAO,QAAQ,IAAI,CAAC,QAAQ,WAAW,IAAI,CAAC,IAAI,EAAE,CAAC;AACrD,CAAC"}
|
package/dist/watchdog.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { Provider, ProviderChunk } from "./types.ts";
|
|
1
2
|
/** No byte at all for this long and the stream is considered wedged. */
|
|
2
3
|
export declare const STREAM_IDLE_MS = 60000;
|
|
3
4
|
export interface StreamWatch {
|
|
@@ -31,4 +32,49 @@ export declare function streamWatch(opts?: StreamWatchOptions): StreamWatch;
|
|
|
31
32
|
* breaking out of the loop.
|
|
32
33
|
*/
|
|
33
34
|
export declare function watchChunks<T>(watch: StreamWatch, chunks: AsyncIterable<T>): AsyncGenerator<T>;
|
|
35
|
+
/**
|
|
36
|
+
* Reject a turn that completed but produced nothing usable.
|
|
37
|
+
*
|
|
38
|
+
* A stream that ends with no text, no reasoning and no tool call is a failure
|
|
39
|
+
* wearing a success's clothes: `stop_reason: end_turn` with zero content
|
|
40
|
+
* blocks, which the vendors emit under load and after a thinking block eats
|
|
41
|
+
* the whole `max_tokens`. Nothing throws, so nothing retries — the caller
|
|
42
|
+
* simply shows a person an empty answer, and the only trace is a bill.
|
|
43
|
+
*
|
|
44
|
+
* Classified `overload` because that is both true and useful: it is theirs and
|
|
45
|
+
* temporary, so it is transient (the same model, retried, usually answers) and
|
|
46
|
+
* backup-eligible (a model that keeps doing it should be walked away from).
|
|
47
|
+
* The throw lands before any chunk is yielded downstream, so the retry rule
|
|
48
|
+
* that matters — retry only while nothing was emitted — still holds.
|
|
49
|
+
*/
|
|
50
|
+
export declare function requireContent<T extends ProviderChunk>(provider: string, chunks: AsyncIterable<T>): AsyncGenerator<T>;
|
|
51
|
+
export interface WatchdogOptions {
|
|
52
|
+
/** Silence this long and the stream is wedged. Defaults to `STREAM_IDLE_MS`. */
|
|
53
|
+
idleMs?: number;
|
|
54
|
+
/**
|
|
55
|
+
* Reject a turn that ends having said nothing, as `requireContent` does. On
|
|
56
|
+
* by default: an empty completion is a failure in every loop, and the one
|
|
57
|
+
* shaped like a success is the one nobody catches.
|
|
58
|
+
*/
|
|
59
|
+
requireContent?: boolean;
|
|
60
|
+
/** Time to first byte for this call, reported once the byte arrives. */
|
|
61
|
+
onFirstChunk?: (ms: number) => void;
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* A provider with both silent failures already handled.
|
|
65
|
+
*
|
|
66
|
+
* Every consumer of this package wrote the same three lines around every
|
|
67
|
+
* `createStream` — build a watch, hand the provider the WATCH's signal, wrap
|
|
68
|
+
* the chunks — and the middle one is the trap. Pass the caller's signal
|
|
69
|
+
* instead and everything still compiles, still streams, still passes the
|
|
70
|
+
* tests: the watchdog simply never aborts anything, because the request it was
|
|
71
|
+
* meant to cancel was never told about it. The failure has no symptom until
|
|
72
|
+
* production, where it is the exact hang the watchdog was added to end.
|
|
73
|
+
*
|
|
74
|
+
* So the composition belongs here rather than in a docs snippet each app
|
|
75
|
+
* copies. The result is still a `Provider`, so it composes unchanged with
|
|
76
|
+
* `withStreamRetry` and `streamWithBackupModels` — and both of the failures it
|
|
77
|
+
* catches are transient, which is what makes wrapping it in a retry correct.
|
|
78
|
+
*/
|
|
79
|
+
export declare function withWatchdog(provider: Provider, opts?: WatchdogOptions): Provider;
|
|
34
80
|
//# sourceMappingURL=watchdog.d.ts.map
|
package/dist/watchdog.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"watchdog.d.ts","sourceRoot":"","sources":["../src/watchdog.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"watchdog.d.ts","sourceRoot":"","sources":["../src/watchdog.ts"],"names":[],"mappings":"AAkBA,OAAO,KAAK,EAEV,QAAQ,EACR,aAAa,EAGd,MAAM,YAAY,CAAC;AAEpB,wEAAwE;AACxE,eAAO,MAAM,cAAc,QAAS,CAAC;AAErC,MAAM,WAAW,WAAW;IAC1B,iEAAiE;IACjE,QAAQ,CAAC,MAAM,EAAE,WAAW,CAAC;IAC7B,8EAA8E;IAC9E,OAAO,IAAI,IAAI,CAAC;IAChB;;;;OAIG;IACH,YAAY,IAAI,MAAM,GAAG,IAAI,CAAC;IAC9B;;;OAGG;IACH,QAAQ,CAAC,GAAG,EAAE,OAAO,GAAG,OAAO,CAAC;IAChC,6DAA6D;IAC7D,OAAO,IAAI,IAAI,CAAC;CACjB;AAED,MAAM,WAAW,kBAAkB;IACjC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,MAAM,CAAC,EAAE,WAAW,CAAC;CACtB;AAED,wBAAgB,WAAW,CAAC,IAAI,GAAE,kBAAuB,GAAG,WAAW,CAsDtE;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,GACvB,cAAc,CAAC,CAAC,CAAC,CAwBnB;AAED,MAAM,WAAW,eAAe;IAC9B,gFAAgF;IAChF,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB;;;;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,CAoCrF"}
|
package/dist/watchdog.js
CHANGED
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
// The two ways a stream fails without failing: it goes silent, or it ends
|
|
2
|
+
// having said nothing at all.
|
|
3
|
+
//
|
|
1
4
|
// The stream-idle watchdog.
|
|
2
5
|
//
|
|
3
6
|
// A provider that stops sending bytes is indistinguishable from a long prefill
|
|
@@ -26,7 +29,9 @@ export function streamWatch(opts = {}) {
|
|
|
26
29
|
let disposed = false;
|
|
27
30
|
// The bridge is structural rather than an event listener: AbortSignal.any
|
|
28
31
|
// aborts synchronously when an input is ALREADY aborted, which is the race
|
|
29
|
-
// no listener can catch (the event fired before we subscribed).
|
|
32
|
+
// no listener can catch (the event fired before we subscribed). It is also
|
|
33
|
+
// the package's runtime floor — see `engines` — rather than a polyfilled
|
|
34
|
+
// nicety: every runtime this package targets has had it for years.
|
|
30
35
|
const signal = callerSignal ? AbortSignal.any([callerSignal, timeout.signal]) : timeout.signal;
|
|
31
36
|
const idleError = (cause) => new ProviderError(provider, "timeout", `stream went ${idleMs / 1000}s without a byte`, {
|
|
32
37
|
cause,
|
|
@@ -82,4 +87,90 @@ export async function* watchChunks(watch, chunks) {
|
|
|
82
87
|
watch.dispose();
|
|
83
88
|
}
|
|
84
89
|
}
|
|
90
|
+
/**
|
|
91
|
+
* Reject a turn that completed but produced nothing usable.
|
|
92
|
+
*
|
|
93
|
+
* A stream that ends with no text, no reasoning and no tool call is a failure
|
|
94
|
+
* wearing a success's clothes: `stop_reason: end_turn` with zero content
|
|
95
|
+
* blocks, which the vendors emit under load and after a thinking block eats
|
|
96
|
+
* the whole `max_tokens`. Nothing throws, so nothing retries — the caller
|
|
97
|
+
* simply shows a person an empty answer, and the only trace is a bill.
|
|
98
|
+
*
|
|
99
|
+
* Classified `overload` because that is both true and useful: it is theirs and
|
|
100
|
+
* temporary, so it is transient (the same model, retried, usually answers) and
|
|
101
|
+
* backup-eligible (a model that keeps doing it should be walked away from).
|
|
102
|
+
* The throw lands before any chunk is yielded downstream, so the retry rule
|
|
103
|
+
* that matters — retry only while nothing was emitted — still holds.
|
|
104
|
+
*/
|
|
105
|
+
export async function* requireContent(provider, chunks) {
|
|
106
|
+
const held = [];
|
|
107
|
+
let usable = false;
|
|
108
|
+
for await (const chunk of chunks) {
|
|
109
|
+
if (!usable) {
|
|
110
|
+
usable = Boolean(chunk.content || chunk.reasoning || chunk.toolCalls?.length);
|
|
111
|
+
// Held rather than forwarded: once a chunk is out, the stream is
|
|
112
|
+
// committed and the retry this guard exists to trigger can no longer
|
|
113
|
+
// fire. Nothing content-bearing has arrived yet, so there is nothing to
|
|
114
|
+
// hold back but the empty frames.
|
|
115
|
+
if (!usable) {
|
|
116
|
+
held.push(chunk);
|
|
117
|
+
continue;
|
|
118
|
+
}
|
|
119
|
+
yield* held;
|
|
120
|
+
held.length = 0;
|
|
121
|
+
}
|
|
122
|
+
yield chunk;
|
|
123
|
+
}
|
|
124
|
+
if (!usable) {
|
|
125
|
+
throw new ProviderError(provider, "overload", `${provider}: completed with no content`);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* A provider with both silent failures already handled.
|
|
130
|
+
*
|
|
131
|
+
* Every consumer of this package wrote the same three lines around every
|
|
132
|
+
* `createStream` — build a watch, hand the provider the WATCH's signal, wrap
|
|
133
|
+
* the chunks — and the middle one is the trap. Pass the caller's signal
|
|
134
|
+
* instead and everything still compiles, still streams, still passes the
|
|
135
|
+
* tests: the watchdog simply never aborts anything, because the request it was
|
|
136
|
+
* meant to cancel was never told about it. The failure has no symptom until
|
|
137
|
+
* production, where it is the exact hang the watchdog was added to end.
|
|
138
|
+
*
|
|
139
|
+
* So the composition belongs here rather than in a docs snippet each app
|
|
140
|
+
* copies. The result is still a `Provider`, so it composes unchanged with
|
|
141
|
+
* `withStreamRetry` and `streamWithBackupModels` — and both of the failures it
|
|
142
|
+
* catches are transient, which is what makes wrapping it in a retry correct.
|
|
143
|
+
*/
|
|
144
|
+
export function withWatchdog(provider, opts = {}) {
|
|
145
|
+
return {
|
|
146
|
+
...provider,
|
|
147
|
+
createStream(messages, tools, streamOpts = {}) {
|
|
148
|
+
// Armed on first read, not here: a stream built now and iterated later
|
|
149
|
+
// must not spend its deadline sitting in a variable.
|
|
150
|
+
async function* watched() {
|
|
151
|
+
// idleMs and signal both default inside streamWatch.
|
|
152
|
+
const watch = streamWatch({
|
|
153
|
+
provider: provider.id,
|
|
154
|
+
idleMs: opts.idleMs,
|
|
155
|
+
signal: streamOpts.signal,
|
|
156
|
+
});
|
|
157
|
+
const source = provider.createStream(messages, tools, {
|
|
158
|
+
...streamOpts,
|
|
159
|
+
signal: watch.signal,
|
|
160
|
+
});
|
|
161
|
+
let reported = false;
|
|
162
|
+
for await (const chunk of watchChunks(watch, source)) {
|
|
163
|
+
// Before `requireContent` holds anything back — TTFT is the first
|
|
164
|
+
// byte of any kind, not the first byte worth showing.
|
|
165
|
+
if (!reported) {
|
|
166
|
+
reported = true;
|
|
167
|
+
opts.onFirstChunk?.(watch.firstChunkMs() ?? 0);
|
|
168
|
+
}
|
|
169
|
+
yield chunk;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
return opts.requireContent === false ? watched() : requireContent(provider.id, watched());
|
|
173
|
+
},
|
|
174
|
+
};
|
|
175
|
+
}
|
|
85
176
|
//# sourceMappingURL=watchdog.js.map
|
package/dist/watchdog.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"watchdog.js","sourceRoot":"","sources":["../src/watchdog.ts"],"names":[],"mappings":"AAAA,4BAA4B;AAC5B,EAAE;AACF,+EAA+E;AAC/E,6EAA6E;AAC7E,+EAA+E;AAC/E,wEAAwE;AACxE,EAAE;AACF,uEAAuE;AACvE,4EAA4E;AAC5E,0EAA0E;AAC1E,2EAA2E;AAC3E,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
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@providerkit/core",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.1",
|
|
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",
|
|
@@ -23,6 +23,9 @@
|
|
|
23
23
|
"LICENSE"
|
|
24
24
|
],
|
|
25
25
|
"sideEffects": false,
|
|
26
|
+
"engines": {
|
|
27
|
+
"node": ">=22"
|
|
28
|
+
},
|
|
26
29
|
"publishConfig": {
|
|
27
30
|
"access": "public"
|
|
28
31
|
},
|
package/src/errors.ts
CHANGED
|
@@ -325,7 +325,10 @@ const RATE_PATTERNS: readonly RegExp[] = [
|
|
|
325
325
|
const OVERLOAD_PATTERNS: readonly RegExp[] = [
|
|
326
326
|
/overloaded|overloaded_error/i,
|
|
327
327
|
/\bunavailable\b|UNAVAILABLE/,
|
|
328
|
-
/internal error
|
|
328
|
+
/internal error/i,
|
|
329
|
+
// Google's status enum, which is upper-case by contract — kept exact so it
|
|
330
|
+
// does not swallow the word in ordinary prose.
|
|
331
|
+
/\bINTERNAL\b/,
|
|
329
332
|
/\bcapacity\b/i,
|
|
330
333
|
/"code"\s*:\s*5\d\d/,
|
|
331
334
|
];
|
|
@@ -356,6 +359,15 @@ export function classifyHttp(status: number | undefined, body: string): ErrorKin
|
|
|
356
359
|
}
|
|
357
360
|
|
|
358
361
|
export function classify(err: unknown, status?: number, body?: string): ErrorKind {
|
|
362
|
+
// An error this package already classified knows its own kind, and nothing is
|
|
363
|
+
// learned by deriving it a second time from a status and a body it never had.
|
|
364
|
+
//
|
|
365
|
+
// This is not a shortcut, it is a correctness fix. The watchdog's own idle
|
|
366
|
+
// timeout is a ProviderError carrying kind "timeout" and no HTTP status, so
|
|
367
|
+
// re-deriving it landed on "unknown" — not transient, therefore never
|
|
368
|
+
// retried, which is the exact opposite of the reason the watchdog exists
|
|
369
|
+
// (invariant 2). A wedged stream aborted at 60s and then failed for good.
|
|
370
|
+
if (err instanceof ProviderError) return err.kind;
|
|
359
371
|
if (isAbort(err)) return "aborted";
|
|
360
372
|
if (isTransportFailure(err)) return "network";
|
|
361
373
|
|
|
@@ -3,6 +3,7 @@ import { streamError } from "../errors.ts";
|
|
|
3
3
|
import { parseToolArgs } from "../tool-args.ts";
|
|
4
4
|
import { streamSse, apiUrl } from "../transport.ts";
|
|
5
5
|
import type {
|
|
6
|
+
JsonOutput,
|
|
6
7
|
ChatMessage,
|
|
7
8
|
ContentPart,
|
|
8
9
|
Effort,
|
|
@@ -104,6 +105,28 @@ function systemBlocks(text: string): unknown[] | undefined {
|
|
|
104
105
|
return text ? [{ type: "text", text, cache_control: { type: "ephemeral" } }] : undefined;
|
|
105
106
|
}
|
|
106
107
|
|
|
108
|
+
/**
|
|
109
|
+
* The schema, as an extra system block — Anthropic has no native schema mode,
|
|
110
|
+
* and the seam promises that a provider without one gets the schema in the
|
|
111
|
+
* prompt instead. Without this an `opts.json` request went out carrying
|
|
112
|
+
* nothing at all: the model answered in prose, the caller's `JSON.parse` threw,
|
|
113
|
+
* and the turn failed on the happy path where no retry looks.
|
|
114
|
+
*
|
|
115
|
+
* It rides AFTER the cached block, and that order is load-bearing. A cache
|
|
116
|
+
* breakpoint caches everything before it, so folding a per-call schema into the
|
|
117
|
+
* cached block would change the cached prefix on every turn whose schema
|
|
118
|
+
* differs and throw the whole system prompt's cache away — paying for the
|
|
119
|
+
* schema with the most expensive thing in an agent loop.
|
|
120
|
+
*/
|
|
121
|
+
function jsonBlock(json: JsonOutput): unknown {
|
|
122
|
+
return {
|
|
123
|
+
type: "text",
|
|
124
|
+
text:
|
|
125
|
+
"Respond with a single JSON object matching this schema. No prose, no code fence:\n" +
|
|
126
|
+
JSON.stringify(json.schema),
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
|
|
107
130
|
export function toAnthropicMessages(messages: readonly ChatMessage[]): {
|
|
108
131
|
system?: unknown[];
|
|
109
132
|
messages: unknown[];
|
|
@@ -209,8 +232,11 @@ export function createAnthropicProvider(config: AnthropicConfig): Provider {
|
|
|
209
232
|
messages: body,
|
|
210
233
|
stream: true,
|
|
211
234
|
};
|
|
212
|
-
|
|
235
|
+
const systemBody = opts.json ? [...(system ?? []), jsonBlock(opts.json)] : system;
|
|
236
|
+
if (systemBody?.length) request.system = systemBody;
|
|
213
237
|
if (opts.temperature !== undefined) request.temperature = opts.temperature;
|
|
238
|
+
if (opts.topP !== undefined) request.top_p = opts.topP;
|
|
239
|
+
if (opts.stopSequences?.length) request.stop_sequences = opts.stopSequences;
|
|
214
240
|
if (tools.length > 0) {
|
|
215
241
|
request.tools = tools.map((tool) => ({
|
|
216
242
|
name: tool.name,
|
|
@@ -231,6 +257,7 @@ export function createAnthropicProvider(config: AnthropicConfig): Provider {
|
|
|
231
257
|
request.thinking = { type: "enabled", budget_tokens: budget };
|
|
232
258
|
// Thinking and sampling are mutually exclusive on this shape.
|
|
233
259
|
delete request.temperature;
|
|
260
|
+
delete request.top_p;
|
|
234
261
|
}
|
|
235
262
|
|
|
236
263
|
// Anthropic reports cache reads and writes as fields of their OWN,
|
package/src/providers/gemini.ts
CHANGED
|
@@ -252,6 +252,8 @@ export function createGeminiProvider(config: GeminiConfig): Provider {
|
|
|
252
252
|
const generationConfig: Record<string, unknown> = {};
|
|
253
253
|
if (maxTokens !== undefined) generationConfig.maxOutputTokens = maxTokens;
|
|
254
254
|
if (opts.temperature !== undefined) generationConfig.temperature = opts.temperature;
|
|
255
|
+
if (opts.topP !== undefined) generationConfig.topP = opts.topP;
|
|
256
|
+
if (opts.stopSequences?.length) generationConfig.stopSequences = opts.stopSequences;
|
|
255
257
|
// No effort means the model's own dynamic thinking. Sending MINIMAL here
|
|
256
258
|
// would switch that off for a caller who never asked, which is the whole
|
|
257
259
|
// reason the seam treats an absent effort as "never sent".
|
package/src/providers/openai.ts
CHANGED
|
@@ -16,6 +16,7 @@ import type {
|
|
|
16
16
|
ToolDefinition,
|
|
17
17
|
} from "../types.ts";
|
|
18
18
|
import { toDataUri } from "../types.ts";
|
|
19
|
+
import { isStrictSchema } from "../schema.ts";
|
|
19
20
|
|
|
20
21
|
export interface OpenAIConfig {
|
|
21
22
|
apiKey: string;
|
|
@@ -72,10 +73,17 @@ export type EffortDialect = "openai" | "openrouter" | "deepseek" | "off";
|
|
|
72
73
|
* - **DeepSeek V4 defaults thinking ON**, so `none` has to be an explicit
|
|
73
74
|
* refusal. That is the case proving OpenRouter's floor is a constraint and
|
|
74
75
|
* not a preference for always thinking.
|
|
75
|
-
* - **OpenAI** takes `reasoning_effort
|
|
76
|
+
* - **OpenAI** takes `reasoning_effort`, and `none` is one of its values — not
|
|
77
|
+
* the absence of the field. GPT-5.1 both accepted `none` and made it the
|
|
78
|
+
* default; everything from GPT-5 back still defaults to `medium`. So sending
|
|
79
|
+
* nothing is NOT a way to say "do not think": on every model released before
|
|
80
|
+
* 5.1 it means medium, and thinking tokens come out of the same budget as the
|
|
81
|
+
* answer. A capped turn then spends its whole allowance thinking and returns
|
|
82
|
+
* empty with `finish_reason: "length"`.
|
|
76
83
|
*
|
|
77
84
|
* An absent effort sends nothing on every dialect: the seam's rule is that a
|
|
78
|
-
* knob the caller never touched is a knob the provider still owns.
|
|
85
|
+
* knob the caller never touched is a knob the provider still owns. `none` is
|
|
86
|
+
* the caller touching it, and the two must not produce the same request.
|
|
79
87
|
*/
|
|
80
88
|
export function effortParams(
|
|
81
89
|
dialect: EffortDialect,
|
|
@@ -97,7 +105,7 @@ export function effortParams(
|
|
|
97
105
|
case "openrouter":
|
|
98
106
|
return { reasoning: { effort: level ?? "low" } };
|
|
99
107
|
case "openai":
|
|
100
|
-
return
|
|
108
|
+
return { reasoning_effort: level ?? "none" };
|
|
101
109
|
case "off":
|
|
102
110
|
return {};
|
|
103
111
|
}
|
|
@@ -256,6 +264,8 @@ export function createOpenAIProvider(config: OpenAIConfig): Provider {
|
|
|
256
264
|
const maxTokens = opts.maxTokens ?? config.maxTokens;
|
|
257
265
|
if (maxTokens !== undefined) request.max_tokens = maxTokens;
|
|
258
266
|
if (opts.temperature !== undefined) request.temperature = opts.temperature;
|
|
267
|
+
if (opts.topP !== undefined) request.top_p = opts.topP;
|
|
268
|
+
if (opts.stopSequences?.length) request.stop = opts.stopSequences;
|
|
259
269
|
Object.assign(request, effortParams(config.effortDialect ?? dialectFor(id), effort));
|
|
260
270
|
if (tools.length > 0) {
|
|
261
271
|
request.tools = tools.map((tool) => ({
|
|
@@ -278,7 +288,11 @@ export function createOpenAIProvider(config: OpenAIConfig): Provider {
|
|
|
278
288
|
(config.jsonMode ?? (id === "openai" ? "schema" : "object")) === "schema"
|
|
279
289
|
? {
|
|
280
290
|
type: "json_schema",
|
|
281
|
-
json_schema: {
|
|
291
|
+
json_schema: {
|
|
292
|
+
name: opts.json.name,
|
|
293
|
+
schema: opts.json.schema,
|
|
294
|
+
strict: opts.json.strict ?? isStrictSchema(opts.json.schema),
|
|
295
|
+
},
|
|
282
296
|
}
|
|
283
297
|
: // Everything else gets plain JSON mode. Schema ENFORCEMENT is
|
|
284
298
|
// OpenAI's; the gateways and the vendors behind them offer JSON
|
|
@@ -24,6 +24,7 @@ import type {
|
|
|
24
24
|
ToolDefinition,
|
|
25
25
|
} from "../types.ts";
|
|
26
26
|
import { toDataUri } from "../types.ts";
|
|
27
|
+
import { isStrictSchema } from "../schema.ts";
|
|
27
28
|
|
|
28
29
|
export interface ResponsesConfig {
|
|
29
30
|
apiKey: string;
|
|
@@ -250,12 +251,22 @@ export function createResponsesProvider(config: ResponsesConfig): Provider {
|
|
|
250
251
|
const maxTokens = opts.maxTokens ?? config.maxTokens;
|
|
251
252
|
if (maxTokens !== undefined) request.max_output_tokens = maxTokens;
|
|
252
253
|
if (opts.temperature !== undefined) request.temperature = opts.temperature;
|
|
253
|
-
if (
|
|
254
|
+
if (opts.topP !== undefined) request.top_p = opts.topP;
|
|
255
|
+
// No stop sequences on this shape — it has no equivalent field, and
|
|
256
|
+
// inventing one would 400 the request rather than shorten the answer.
|
|
257
|
+
if (effort) {
|
|
258
|
+
// `max` is this package's word, not OpenAI's: the enum here is
|
|
259
|
+
// none/low/medium/high. Every other shape clamps it — this one sent it
|
|
260
|
+
// through and earned a 400 on the top setting alone.
|
|
261
|
+
const level = effort === "max" ? "high" : effort;
|
|
254
262
|
// `summary` is what switches the reasoning stream ON. Without it this
|
|
255
263
|
// shape emits no reasoning_summary_text events at all, and a caller
|
|
256
264
|
// rendering a thinking pane silently gets nothing while the tokens are
|
|
257
|
-
// billed either way.
|
|
258
|
-
|
|
265
|
+
// billed either way. At `none` there is nothing to summarise, and the
|
|
266
|
+
// field must still ride: omitting it is the model's own default, which
|
|
267
|
+
// is `medium` on everything older than GPT-5.1.
|
|
268
|
+
request.reasoning =
|
|
269
|
+
level === "none" ? { effort: "none" } : { effort: level, summary: "auto" };
|
|
259
270
|
}
|
|
260
271
|
if (tools.length > 0) {
|
|
261
272
|
// Flat here — no nested `function` envelope, unlike chat/completions.
|
|
@@ -279,7 +290,7 @@ export function createResponsesProvider(config: ResponsesConfig): Provider {
|
|
|
279
290
|
type: "json_schema",
|
|
280
291
|
name: opts.json.name,
|
|
281
292
|
schema: opts.json.schema,
|
|
282
|
-
strict:
|
|
293
|
+
strict: opts.json.strict ?? isStrictSchema(opts.json.schema),
|
|
283
294
|
},
|
|
284
295
|
};
|
|
285
296
|
}
|
package/src/schema.ts
CHANGED
|
@@ -65,3 +65,44 @@ export function clampToSchema(value: unknown, node: unknown): unknown {
|
|
|
65
65
|
|
|
66
66
|
return value;
|
|
67
67
|
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Whether OpenAI's `strict` schema mode will accept this schema.
|
|
71
|
+
*
|
|
72
|
+
* Strict is the only JSON mode that actually guarantees the shape, so it is
|
|
73
|
+
* worth having — but it demands more than JSON Schema does: every property an
|
|
74
|
+
* object lists must ALSO be required, and every object must close itself with
|
|
75
|
+
* `additionalProperties: false`, all the way down. A schema with one optional
|
|
76
|
+
* field is not "mostly strict"; it is a flat 400 naming a nested path rather
|
|
77
|
+
* than the rule it broke.
|
|
78
|
+
*
|
|
79
|
+
* That trap is the reason this exists. An agent's response schema grows
|
|
80
|
+
* optional fields naturally — a `data` block only some flows fill, the fields
|
|
81
|
+
* one step collects — and a caller who adds one wants their answer, not a
|
|
82
|
+
* lecture about a mode they never asked for. So the OpenAI-shape adapters ask
|
|
83
|
+
* this and drop to plain (unenforced) schema mode instead of failing the turn.
|
|
84
|
+
*
|
|
85
|
+
* Anything it cannot verify — a `$ref`, a composed `allOf` — answers false:
|
|
86
|
+
* the cost of guessing wrong that way is unenforced output, and the cost of
|
|
87
|
+
* guessing wrong the other way is a request that cannot succeed at all.
|
|
88
|
+
*/
|
|
89
|
+
export function isStrictSchema(node: unknown): boolean {
|
|
90
|
+
if (!node || typeof node !== "object") return false;
|
|
91
|
+
const schema = node as SchemaNode;
|
|
92
|
+
|
|
93
|
+
if (schema.$ref !== undefined || schema.allOf !== undefined) return false;
|
|
94
|
+
|
|
95
|
+
const union = schema.anyOf ?? schema.oneOf;
|
|
96
|
+
if (Array.isArray(union)) return union.every(isStrictSchema);
|
|
97
|
+
|
|
98
|
+
// A leaf (string, number, boolean, enum) carries no strict obligations.
|
|
99
|
+
if (schema.type === "array") return schema.items === undefined || isStrictSchema(schema.items);
|
|
100
|
+
if (schema.properties === undefined) return true;
|
|
101
|
+
|
|
102
|
+
if (schema.additionalProperties !== false) return false;
|
|
103
|
+
const properties = schema.properties as Record<string, unknown>;
|
|
104
|
+
const required = new Set(Array.isArray(schema.required) ? (schema.required as string[]) : []);
|
|
105
|
+
return Object.entries(properties).every(
|
|
106
|
+
([key, value]) => required.has(key) && isStrictSchema(value),
|
|
107
|
+
);
|
|
108
|
+
}
|