@providerkit/core 0.23.0 → 0.25.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/README.md +43 -7
- package/dist/auth/device.d.ts +3 -2
- package/dist/auth/device.d.ts.map +1 -1
- package/dist/auth/device.js +5 -2
- package/dist/auth/device.js.map +1 -1
- package/dist/auth/flows.d.ts +5 -1
- package/dist/auth/flows.d.ts.map +1 -1
- package/dist/auth/flows.js +237 -0
- package/dist/auth/flows.js.map +1 -1
- package/dist/auth/oauth.d.ts +4 -2
- package/dist/auth/oauth.d.ts.map +1 -1
- package/dist/auth/oauth.js +4 -3
- package/dist/auth/oauth.js.map +1 -1
- package/dist/auth/with-token.d.ts +4 -1
- package/dist/auth/with-token.d.ts.map +1 -1
- package/dist/auth/with-token.js +9 -4
- package/dist/auth/with-token.js.map +1 -1
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +3 -0
- package/dist/errors.js.map +1 -1
- package/dist/gate.d.ts +29 -0
- package/dist/gate.d.ts.map +1 -0
- package/dist/gate.js +96 -0
- package/dist/gate.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -1
- package/dist/key-pool.d.ts +11 -0
- package/dist/key-pool.d.ts.map +1 -1
- package/dist/key-pool.js +14 -0
- package/dist/key-pool.js.map +1 -1
- package/dist/presets.d.ts +53 -0
- package/dist/presets.d.ts.map +1 -1
- package/dist/presets.js +100 -0
- package/dist/presets.js.map +1 -1
- package/dist/providers/anthropic.d.ts +7 -0
- package/dist/providers/anthropic.d.ts.map +1 -1
- package/dist/providers/anthropic.js +3 -1
- package/dist/providers/anthropic.js.map +1 -1
- package/dist/providers/factory.d.ts +36 -0
- package/dist/providers/factory.d.ts.map +1 -1
- package/dist/providers/factory.js +208 -22
- package/dist/providers/factory.js.map +1 -1
- package/dist/providers/gemini.d.ts +17 -5
- package/dist/providers/gemini.d.ts.map +1 -1
- package/dist/providers/gemini.js +241 -119
- package/dist/providers/gemini.js.map +1 -1
- package/dist/providers/openai.d.ts +7 -0
- package/dist/providers/openai.d.ts.map +1 -1
- package/dist/providers/openai.js +7 -4
- package/dist/providers/openai.js.map +1 -1
- package/dist/providers/responses.d.ts +11 -0
- package/dist/providers/responses.d.ts.map +1 -1
- package/dist/providers/responses.js +3 -1
- package/dist/providers/responses.js.map +1 -1
- package/dist/registry.d.ts +105 -0
- package/dist/registry.d.ts.map +1 -0
- package/dist/registry.js +291 -0
- package/dist/registry.js.map +1 -0
- package/dist/schema.d.ts +27 -9
- package/dist/schema.d.ts.map +1 -1
- package/dist/schema.js +197 -62
- package/dist/schema.js.map +1 -1
- package/dist/tools.d.ts +8 -0
- package/dist/tools.d.ts.map +1 -1
- package/dist/tools.js +2 -2
- package/dist/tools.js.map +1 -1
- package/dist/turn.d.ts +55 -0
- package/dist/turn.d.ts.map +1 -0
- package/dist/turn.js +125 -0
- package/dist/turn.js.map +1 -0
- package/dist/types.d.ts +8 -22
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +0 -45
- package/dist/types.js.map +1 -1
- package/package.json +1 -1
- package/src/auth/device.ts +8 -4
- package/src/auth/flows.ts +288 -1
- package/src/auth/oauth.ts +8 -3
- package/src/auth/with-token.ts +9 -4
- package/src/errors.ts +3 -0
- package/src/gate.ts +152 -0
- package/src/index.ts +3 -0
- package/src/key-pool.ts +15 -0
- package/src/presets.ts +100 -0
- package/src/providers/anthropic.ts +10 -1
- package/src/providers/factory.ts +288 -34
- package/src/providers/gemini.ts +273 -124
- package/src/providers/openai.ts +15 -4
- package/src/providers/responses.ts +14 -1
- package/src/registry.ts +420 -0
- package/src/schema.ts +200 -72
- package/src/tools.ts +10 -2
- package/src/turn.ts +166 -0
- package/src/types.ts +8 -62
package/src/schema.ts
CHANGED
|
@@ -162,106 +162,234 @@ const SCHEMA_MAPS = ["properties", "patternProperties", "$defs", "definitions",
|
|
|
162
162
|
* validation the caller's tool runner repeats anyway; the 400 costs the turn.
|
|
163
163
|
*/
|
|
164
164
|
export function withoutPatterns(node: unknown): unknown {
|
|
165
|
-
if (!
|
|
166
|
-
const { pattern: _pattern, ...schema } = node
|
|
167
|
-
|
|
168
|
-
|
|
165
|
+
if (!isSchemaObject(node)) return node;
|
|
166
|
+
const { pattern: _pattern, ...schema } = node;
|
|
167
|
+
mapChildren(schema, withoutPatterns);
|
|
168
|
+
return schema;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function isSchemaObject(node: unknown): node is Record<string, unknown> {
|
|
172
|
+
return typeof node === "object" && node !== null && !Array.isArray(node);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/** Replaces every child schema of `schema`, in place, with what `visit` returns for it. `slot`
|
|
176
|
+
* is the keyword it sits under and `name` is its property name, for the keywords that map
|
|
177
|
+
* names to schemas. */
|
|
178
|
+
function mapChildren(
|
|
179
|
+
schema: Record<string, unknown>,
|
|
180
|
+
visit: (child: unknown, slot: string, name?: string) => unknown,
|
|
181
|
+
): void {
|
|
182
|
+
for (const slot of SCHEMA_SLOTS) {
|
|
183
|
+
const value = schema[slot];
|
|
169
184
|
if (value !== undefined) {
|
|
170
|
-
schema[
|
|
185
|
+
schema[slot] = Array.isArray(value)
|
|
186
|
+
? value.map((child) => visit(child, slot))
|
|
187
|
+
: visit(value, slot);
|
|
171
188
|
}
|
|
172
189
|
}
|
|
173
|
-
for (const
|
|
174
|
-
const value = schema[
|
|
175
|
-
if (Array.isArray(value)) schema[
|
|
190
|
+
for (const slot of SCHEMA_LISTS) {
|
|
191
|
+
const value = schema[slot];
|
|
192
|
+
if (Array.isArray(value)) schema[slot] = value.map((child) => visit(child, slot));
|
|
176
193
|
}
|
|
177
|
-
for (const
|
|
178
|
-
const value = schema[
|
|
179
|
-
if (
|
|
180
|
-
schema[
|
|
181
|
-
Object.entries(value).map(([name, child]) => [name,
|
|
194
|
+
for (const slot of SCHEMA_MAPS) {
|
|
195
|
+
const value = schema[slot];
|
|
196
|
+
if (isSchemaObject(value)) {
|
|
197
|
+
schema[slot] = Object.fromEntries(
|
|
198
|
+
Object.entries(value).map(([name, child]) => [name, visit(child, slot, name)]),
|
|
182
199
|
);
|
|
183
200
|
}
|
|
184
201
|
}
|
|
185
|
-
return schema;
|
|
186
202
|
}
|
|
187
203
|
|
|
188
204
|
/**
|
|
189
|
-
*
|
|
205
|
+
* A JSON Schema for Gemini's `parametersJsonSchema` and `responseJsonSchema`, which take
|
|
206
|
+
* standard JSON Schema. It is sent as written except for the four shapes the API refuses.
|
|
207
|
+
* Measured on `gemini-3.8-flash` on 2026-10-21, in a tool and as a response schema alike;
|
|
208
|
+
* every other keyword tried (`$ref`, `$defs`, `oneOf`, `allOf`, `not`, `if`, `const`, numeric
|
|
209
|
+
* `enum`, `type: [..., "null"]`, `patternProperties`, `additionalProperties`, arrays without
|
|
210
|
+
* `items`) came back accepted:
|
|
211
|
+
*
|
|
212
|
+
* - `required` names a property that is not declared, at any depth: 400 "requires unspecified
|
|
213
|
+
* property". The names with no property are dropped.
|
|
214
|
+
* - Draft-04 `exclusiveMinimum: true` (or `false`): 400 "must be a number". It becomes the
|
|
215
|
+
* number form, `exclusiveMinimum: <minimum>`, and `minimum` goes.
|
|
216
|
+
* - A tuple, `items: [a, b]`: 400 "must be a boolean or an object". It becomes `prefixItems`,
|
|
217
|
+
* and `additionalItems` becomes `items`, as in 2020-12.
|
|
218
|
+
* - A recursive `$ref` that is not behind an optional property or an array that may be empty:
|
|
219
|
+
* 400 "ref loops are only supported if ...". The self-reference becomes `{}`.
|
|
190
220
|
*
|
|
191
|
-
*
|
|
192
|
-
*
|
|
193
|
-
*
|
|
194
|
-
*
|
|
195
|
-
* - Numeric enums (`enum: [1, 2, 3]`), requiring string enums.
|
|
196
|
-
* - `const: "val"`, requiring `enum: ["val"]`.
|
|
197
|
-
* - Arrays without `items`, requiring an explicit items schema.
|
|
198
|
-
* - Unsupported keywords ($schema, definitions, $defs, patternProperties, additionalProperties: true).
|
|
221
|
+
* This replaced a sanitizer written for the older `parameters` field, which accepts a small
|
|
222
|
+
* OpenAPI subset. Kept on `parametersJsonSchema`, it turned a numeric enum into a string enum
|
|
223
|
+
* (the model then sent `"2"` for `2`), forced string items onto an array of anything, and
|
|
224
|
+
* dropped every `$ref`, while still sending the 400s above.
|
|
199
225
|
*/
|
|
200
226
|
export function toGeminiToolSchema(node: unknown): Record<string, unknown> {
|
|
201
|
-
if (!node
|
|
202
|
-
|
|
227
|
+
if (!isSchemaObject(node)) return { type: "object", properties: {} };
|
|
228
|
+
const root = geminiStandardForm(node);
|
|
229
|
+
for (const defs of ["$defs", "definitions"]) {
|
|
230
|
+
const entries = root[defs];
|
|
231
|
+
if (isSchemaObject(entries)) {
|
|
232
|
+
root[defs] = Object.fromEntries(
|
|
233
|
+
Object.entries(entries).map(([name, def]) => [
|
|
234
|
+
name,
|
|
235
|
+
cutRefLoops(def, `#/${defs}/${name}`, false),
|
|
236
|
+
]),
|
|
237
|
+
);
|
|
238
|
+
}
|
|
203
239
|
}
|
|
204
|
-
const
|
|
240
|
+
const cut = cutRefLoops(root, "#", false);
|
|
241
|
+
return isSchemaObject(cut) ? cut : root;
|
|
242
|
+
}
|
|
205
243
|
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
delete schema.definitions;
|
|
210
|
-
delete schema.$defs;
|
|
211
|
-
delete schema.patternProperties;
|
|
244
|
+
function geminiStandardForm(node: Record<string, unknown>): Record<string, unknown> {
|
|
245
|
+
const schema = { ...node };
|
|
246
|
+
mapChildren(schema, (child) => (isSchemaObject(child) ? geminiStandardForm(child) : child));
|
|
212
247
|
|
|
213
|
-
if (schema.
|
|
214
|
-
|
|
248
|
+
if (Array.isArray(schema.required)) {
|
|
249
|
+
const declared = isSchemaObject(schema.properties) ? schema.properties : {};
|
|
250
|
+
schema.required = schema.required.filter(
|
|
251
|
+
(name) => typeof name === "string" && Object.hasOwn(declared, name),
|
|
252
|
+
);
|
|
215
253
|
}
|
|
216
254
|
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
255
|
+
for (const [flag, bound] of [
|
|
256
|
+
["exclusiveMinimum", "minimum"],
|
|
257
|
+
["exclusiveMaximum", "maximum"],
|
|
258
|
+
] as const) {
|
|
259
|
+
if (typeof schema[flag] !== "boolean") continue;
|
|
260
|
+
if (schema[flag] && typeof schema[bound] === "number") {
|
|
261
|
+
schema[flag] = schema[bound];
|
|
262
|
+
delete schema[bound];
|
|
263
|
+
} else {
|
|
264
|
+
delete schema[flag];
|
|
265
|
+
}
|
|
220
266
|
}
|
|
221
267
|
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
);
|
|
230
|
-
if (nonNullBranches.length === 1 && typeof nonNullBranches[0] === "object") {
|
|
231
|
-
const merged = toGeminiToolSchema(nonNullBranches[0]);
|
|
232
|
-
if (hasNull) merged.nullable = true;
|
|
233
|
-
return merged;
|
|
268
|
+
if (Array.isArray(schema.items)) {
|
|
269
|
+
schema.prefixItems = schema.items;
|
|
270
|
+
delete schema.items;
|
|
271
|
+
if ("additionalItems" in schema) {
|
|
272
|
+
const extra = schema.additionalItems;
|
|
273
|
+
schema.items = isSchemaObject(extra) ? geminiStandardForm(extra) : extra;
|
|
274
|
+
delete schema.additionalItems;
|
|
234
275
|
}
|
|
235
276
|
}
|
|
277
|
+
return schema;
|
|
278
|
+
}
|
|
236
279
|
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
280
|
+
/**
|
|
281
|
+
* Gemini takes a recursive `$ref` only when the loop passes through a property that is not
|
|
282
|
+
* required or an `items` that may be empty. Any other reference to `target` becomes `{}`.
|
|
283
|
+
*
|
|
284
|
+
* ponytail: this cuts a definition's reference to ITSELF. Two definitions that refer to each
|
|
285
|
+
* other through required properties still get the 400; follow the ref graph if one shows up.
|
|
286
|
+
*/
|
|
287
|
+
function cutRefLoops(node: unknown, target: string, safe: boolean): unknown {
|
|
288
|
+
if (!isSchemaObject(node)) return node;
|
|
289
|
+
if (!safe && node.$ref === target) return {};
|
|
290
|
+
const schema = { ...node };
|
|
291
|
+
const required = Array.isArray(schema.required) ? schema.required : [];
|
|
292
|
+
const mayBeEmpty = !(typeof schema.minItems === "number" && schema.minItems > 0);
|
|
293
|
+
mapChildren(schema, (child, slot, name) =>
|
|
294
|
+
cutRefLoops(
|
|
295
|
+
child,
|
|
296
|
+
target,
|
|
297
|
+
safe ||
|
|
298
|
+
(slot === "items" && mayBeEmpty) ||
|
|
299
|
+
(slot === "properties" && !required.includes(name)),
|
|
300
|
+
),
|
|
301
|
+
);
|
|
302
|
+
return schema;
|
|
303
|
+
}
|
|
244
304
|
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
305
|
+
/**
|
|
306
|
+
* Sanitize a JSON Schema to the OpenAPI 3.0 subset Google's Cloud Code Assist
|
|
307
|
+
* (`antigravity`) accepts in its `parameters` field (`type`, `nullable`,
|
|
308
|
+
* `description`, `format`, `enum`, `properties`, `items`, `required`, `anyOf`).
|
|
309
|
+
* Mirrors cc-proxy's `clean_json_schema`, including the `{ type: "string" }`
|
|
310
|
+
* fallback when an array omits `items`.
|
|
311
|
+
*/
|
|
312
|
+
export function toAntigravityToolSchema(node: unknown): Record<string, unknown> {
|
|
313
|
+
if (!isSchemaObject(node)) return { type: "object" };
|
|
314
|
+
const out: Record<string, unknown> = {};
|
|
315
|
+
let nullable = node.nullable === true;
|
|
248
316
|
|
|
249
|
-
if (
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
317
|
+
if (typeof node.type === "string") {
|
|
318
|
+
if (node.type === "null") nullable = true;
|
|
319
|
+
else out.type = node.type;
|
|
320
|
+
} else if (Array.isArray(node.type)) {
|
|
321
|
+
let picked: string | undefined;
|
|
322
|
+
for (const item of node.type) {
|
|
323
|
+
if (typeof item !== "string") continue;
|
|
324
|
+
if (item === "null") nullable = true;
|
|
325
|
+
else if (picked === undefined) picked = item;
|
|
326
|
+
}
|
|
327
|
+
if (picked !== undefined) out.type = picked;
|
|
253
328
|
}
|
|
254
|
-
|
|
255
|
-
if (
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
329
|
+
if (nullable) out.nullable = true;
|
|
330
|
+
if (typeof node.description === "string" && node.description.length > 0) {
|
|
331
|
+
out.description = node.description;
|
|
332
|
+
}
|
|
333
|
+
if (node.format === "enum" || node.format === "date-time") {
|
|
334
|
+
out.format = node.format;
|
|
335
|
+
}
|
|
336
|
+
if (Array.isArray(node.enum)) {
|
|
337
|
+
const strings = node.enum.filter((v): v is string => typeof v === "string");
|
|
338
|
+
if (strings.length > 0) out.enum = strings;
|
|
339
|
+
} else if (typeof node.const === "string") {
|
|
340
|
+
out.enum = [node.const];
|
|
341
|
+
}
|
|
342
|
+
if (isSchemaObject(node.properties)) {
|
|
343
|
+
const cleanProps: Record<string, unknown> = {};
|
|
344
|
+
for (const [propName, propSchema] of Object.entries(node.properties)) {
|
|
345
|
+
cleanProps[propName] = toAntigravityToolSchema(propSchema);
|
|
260
346
|
}
|
|
261
|
-
|
|
347
|
+
if (Array.isArray(node.required)) {
|
|
348
|
+
const validReq = node.required.filter(
|
|
349
|
+
(k): k is string => typeof k === "string" && Object.hasOwn(cleanProps, k),
|
|
350
|
+
);
|
|
351
|
+
if (validReq.length > 0) out.required = validReq;
|
|
352
|
+
}
|
|
353
|
+
out.properties = cleanProps;
|
|
354
|
+
if (!("type" in out)) out.type = "object";
|
|
262
355
|
}
|
|
263
|
-
|
|
264
|
-
|
|
356
|
+
if ("items" in node && node.items !== undefined) {
|
|
357
|
+
out.items = Array.isArray(node.items)
|
|
358
|
+
? node.items.length > 0
|
|
359
|
+
? toAntigravityToolSchema(node.items[0])
|
|
360
|
+
: { type: "string" }
|
|
361
|
+
: toAntigravityToolSchema(node.items);
|
|
362
|
+
if (!("type" in out)) out.type = "array";
|
|
363
|
+
}
|
|
364
|
+
const union = Array.isArray(node.anyOf)
|
|
365
|
+
? node.anyOf
|
|
366
|
+
: Array.isArray(node.oneOf)
|
|
367
|
+
? node.oneOf
|
|
368
|
+
: undefined;
|
|
369
|
+
if (union) {
|
|
370
|
+
const branches: Record<string, unknown>[] = [];
|
|
371
|
+
for (const branch of union) {
|
|
372
|
+
if (isSchemaObject(branch) && branch.type === "null") {
|
|
373
|
+
out.nullable = true;
|
|
374
|
+
continue;
|
|
375
|
+
}
|
|
376
|
+
branches.push(toAntigravityToolSchema(branch));
|
|
377
|
+
}
|
|
378
|
+
if (branches.length === 1) {
|
|
379
|
+
for (const [k, v] of Object.entries(branches[0]!)) {
|
|
380
|
+
if (!(k in out)) out[k] = v;
|
|
381
|
+
}
|
|
382
|
+
} else if (branches.length > 0) {
|
|
383
|
+
out.anyOf = branches;
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
if (!("type" in out) && !("anyOf" in out)) {
|
|
387
|
+
out.type = "string";
|
|
388
|
+
}
|
|
389
|
+
if (out.type === "array" && !("items" in out)) {
|
|
390
|
+
out.items = { type: "string" };
|
|
391
|
+
}
|
|
392
|
+
return out;
|
|
265
393
|
}
|
|
266
394
|
|
|
267
395
|
/**
|
package/src/tools.ts
CHANGED
|
@@ -51,8 +51,16 @@ export interface ToolSpec<I, O> {
|
|
|
51
51
|
summarize?: (output: O) => string;
|
|
52
52
|
/** Default 60s. A tool with no ceiling can hang a whole run. */
|
|
53
53
|
timeoutMs?: number;
|
|
54
|
+
/**
|
|
55
|
+
* The tool changes nothing. Default false: a flag that promises safety is
|
|
56
|
+
* true only when the tool says so. `repairHistory` tells the model an
|
|
57
|
+
* interrupted read-only call "changed nothing, so it is safe to call again",
|
|
58
|
+
* and a write that left this out used to be told that too.
|
|
59
|
+
*/
|
|
54
60
|
isReadOnly?: boolean;
|
|
55
61
|
needsApproval?: boolean;
|
|
62
|
+
/** The tool may run in parallel with others. Default false, for the same
|
|
63
|
+
* reason as `isReadOnly`: two writes run at once by omission can race. */
|
|
56
64
|
isConcurrencySafe?: boolean;
|
|
57
65
|
/** A terminal tool ends the run; its validated input is the run's output. */
|
|
58
66
|
isTerminal?: boolean;
|
|
@@ -142,9 +150,9 @@ export function defineTool<I = unknown, O = unknown>(spec: ToolSpec<I, O>): Tool
|
|
|
142
150
|
description: spec.description,
|
|
143
151
|
inputSchema: spec.inputSchema,
|
|
144
152
|
timeoutMs,
|
|
145
|
-
isReadOnly: spec.isReadOnly ??
|
|
153
|
+
isReadOnly: spec.isReadOnly ?? false,
|
|
146
154
|
needsApproval: spec.needsApproval ?? false,
|
|
147
|
-
isConcurrencySafe: spec.isConcurrencySafe ??
|
|
155
|
+
isConcurrencySafe: spec.isConcurrencySafe ?? false,
|
|
148
156
|
isTerminal: spec.isTerminal ?? false,
|
|
149
157
|
|
|
150
158
|
definition() {
|
package/src/turn.ts
ADDED
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
// One turn, drained. Every agent loop reads a stream the same way: text and
|
|
2
|
+
// reasoning joined, tool-call fragments put back together by index, usage and
|
|
3
|
+
// finish taken from the last chunk that states them. The apps that use this
|
|
4
|
+
// package each wrote that by hand, a little differently each time, and the
|
|
5
|
+
// one in `drainStream` dropped the tool calls altogether.
|
|
6
|
+
//
|
|
7
|
+
// It assembles and nothing else. Arguments stay the raw string the seam
|
|
8
|
+
// promises, because tool-args.ts heals them and tools.ts validates them. And a
|
|
9
|
+
// turn that said nothing is not judged here: that is `requireContent`'s call
|
|
10
|
+
// (invariant 13), made where the retry that can fix it still can.
|
|
11
|
+
import { ProviderError } from "./errors.ts";
|
|
12
|
+
import {
|
|
13
|
+
EMPTY_USAGE,
|
|
14
|
+
type ChatMessage,
|
|
15
|
+
type FinishReason,
|
|
16
|
+
type ProviderChunk,
|
|
17
|
+
type ReasoningItems,
|
|
18
|
+
type TokenUsage,
|
|
19
|
+
type ToolCall,
|
|
20
|
+
} from "./types.ts";
|
|
21
|
+
|
|
22
|
+
/** One drained turn: the message a loop appends, whose `toolCalls` are the
|
|
23
|
+
* calls it runs, and what the turn cost. */
|
|
24
|
+
export interface Turn {
|
|
25
|
+
/** Ready to append to history as it is. `toolCalls` holds the calls to run,
|
|
26
|
+
* in the order the provider indexed them, and is absent when there are none. */
|
|
27
|
+
message: Extract<ChatMessage, { role: "assistant" }>;
|
|
28
|
+
/** The last usage the stream reported, or zeros when it reported none. */
|
|
29
|
+
usage: TokenUsage;
|
|
30
|
+
/** As the provider stated it, and null when it stated none. */
|
|
31
|
+
finishReason: FinishReason | null;
|
|
32
|
+
/** The endpoint and model that answered, when a fallback provider says. */
|
|
33
|
+
source?: { provider: string; model: string };
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Drain one turn into the assistant message it produced, plus its usage,
|
|
38
|
+
* finish reason and source.
|
|
39
|
+
*
|
|
40
|
+
* ```ts
|
|
41
|
+
* const turn = await collectTurn(provider.createStream(history, tools));
|
|
42
|
+
* history.push(turn.message);
|
|
43
|
+
* for (const call of turn.message.toolCalls ?? []) history.push(await run(call));
|
|
44
|
+
* ```
|
|
45
|
+
*
|
|
46
|
+
* @throws {ProviderError} `overload` when a tool call ends the stream with no
|
|
47
|
+
* name. Whatever the stream itself throws passes through unchanged.
|
|
48
|
+
*/
|
|
49
|
+
export async function collectTurn(stream: AsyncIterable<ProviderChunk>): Promise<Turn> {
|
|
50
|
+
let content = "";
|
|
51
|
+
let reasoning = "";
|
|
52
|
+
// Not concatenated: this half of the record is a payload the provider owns,
|
|
53
|
+
// and it arrives whole on one delta rather than in fragments. Dropped here,
|
|
54
|
+
// a drained turn replays only half its own reasoning on the next round,
|
|
55
|
+
// which is the failure `reasoningDetails` exists to prevent.
|
|
56
|
+
let reasoningDetails: unknown[] | undefined;
|
|
57
|
+
let reasoningItems: ReasoningItems | undefined;
|
|
58
|
+
const calls = new Map<number, ToolCall>();
|
|
59
|
+
// Cumulative on some providers and final-only on others: the LAST one wins.
|
|
60
|
+
let usage: TokenUsage = EMPTY_USAGE;
|
|
61
|
+
let finishReason: FinishReason | null = null;
|
|
62
|
+
let source: Turn["source"];
|
|
63
|
+
|
|
64
|
+
// Read to the end, past the finish chunk: the OpenAI shape sends its usage
|
|
65
|
+
// after it, and a loop that stopped at the finish would bill every turn at zero.
|
|
66
|
+
for await (const chunk of stream) {
|
|
67
|
+
if (chunk.source) source = chunk.source;
|
|
68
|
+
if (chunk.content) content += chunk.content;
|
|
69
|
+
if (chunk.reasoning) reasoning += chunk.reasoning;
|
|
70
|
+
if (chunk.reasoningDetails?.length) reasoningDetails = chunk.reasoningDetails;
|
|
71
|
+
if (chunk.reasoningItems?.items.length) reasoningItems = chunk.reasoningItems;
|
|
72
|
+
for (const delta of chunk.toolCalls ?? []) {
|
|
73
|
+
const call = calls.get(delta.index) ?? { id: "", name: "", arguments: "" };
|
|
74
|
+
// Identity is the last value STATED, so an empty one never erases a real
|
|
75
|
+
// one: Anthropic opens a call with `""` for what it lacks, and the
|
|
76
|
+
// ChatGPT backend states the real call_id and name only on the call's
|
|
77
|
+
// closing event. A call that never states an id keeps `""`. The four
|
|
78
|
+
// adapters here mint one before their finish (`settleToolCalls`); for any
|
|
79
|
+
// other Provider, one made up here would be ours and not the wire's
|
|
80
|
+
// (invariant 10), and `repairHistory` already names it the same way on
|
|
81
|
+
// every send. Arguments are fragments, joined in arrival order.
|
|
82
|
+
if (delta.id) call.id = delta.id;
|
|
83
|
+
if (delta.name) call.name = delta.name;
|
|
84
|
+
if (delta.thoughtSignature) call.thoughtSignature = delta.thoughtSignature;
|
|
85
|
+
call.arguments += delta.arguments ?? "";
|
|
86
|
+
calls.set(delta.index, call);
|
|
87
|
+
}
|
|
88
|
+
if (chunk.usage) usage = chunk.usage;
|
|
89
|
+
if (chunk.finishReason) finishReason = chunk.finishReason;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// By index, not by arrival: parallel calls interleave their fragments.
|
|
93
|
+
const indexed = [...calls].sort(([a], [b]) => a - b);
|
|
94
|
+
const nameless = indexed.find(([, call]) => !call.name);
|
|
95
|
+
if (nameless) {
|
|
96
|
+
const [index, call] = nameless;
|
|
97
|
+
// `overload`, the kind for a provider's own anomaly inside a 200 (an empty
|
|
98
|
+
// body, a turn that completed with nothing): our request was fine, the
|
|
99
|
+
// vendor's frame was not. A retry may well get a whole call, and another
|
|
100
|
+
// model is a fair next try if this one keeps doing it. `invalid` would
|
|
101
|
+
// blame our request and stop the run over a fault that is not ours. The
|
|
102
|
+
// four adapters here fail such a call themselves, with the same kind
|
|
103
|
+
// (`settleToolCalls`); this gives any other Provider the same answer.
|
|
104
|
+
const provider = source?.provider ?? "turn";
|
|
105
|
+
const which = call.id || `at index ${index}`;
|
|
106
|
+
throw new ProviderError(
|
|
107
|
+
provider,
|
|
108
|
+
"overload",
|
|
109
|
+
`${provider}: tool call ${which} ended with no name, so there is no tool to run`,
|
|
110
|
+
);
|
|
111
|
+
}
|
|
112
|
+
const toolCalls = indexed.map(([, call]) => call);
|
|
113
|
+
|
|
114
|
+
return {
|
|
115
|
+
message: {
|
|
116
|
+
role: "assistant",
|
|
117
|
+
content,
|
|
118
|
+
...(reasoning ? { reasoning } : {}),
|
|
119
|
+
...(reasoningDetails ? { reasoningDetails } : {}),
|
|
120
|
+
...(reasoningItems ? { reasoningItems } : {}),
|
|
121
|
+
...(toolCalls.length > 0 ? { toolCalls } : {}),
|
|
122
|
+
},
|
|
123
|
+
usage,
|
|
124
|
+
finishReason,
|
|
125
|
+
...(source ? { source } : {}),
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export interface Completion {
|
|
130
|
+
text: string;
|
|
131
|
+
/** Present when the stream identifies the endpoint that answered. */
|
|
132
|
+
provider?: string;
|
|
133
|
+
reasoning: string;
|
|
134
|
+
/** Present only where the provider sent one. See ChatMessage.reasoningDetails. */
|
|
135
|
+
reasoningDetails?: unknown[];
|
|
136
|
+
/** Present only where the provider sent them. See ChatMessage.reasoningItems. */
|
|
137
|
+
reasoningItems?: ReasoningItems;
|
|
138
|
+
/** The calls the model asked for, in index order, and empty when it asked
|
|
139
|
+
* for none. Before this field a drained turn that called a tool read as a
|
|
140
|
+
* turn that said nothing. */
|
|
141
|
+
toolCalls: ToolCall[];
|
|
142
|
+
usage: TokenUsage;
|
|
143
|
+
finishReason: FinishReason | null;
|
|
144
|
+
model: string;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/** Drain a stream into a Completion: `collectTurn`, flattened for a caller
|
|
148
|
+
* that wants the answer rather than a message to append. `model` names the
|
|
149
|
+
* model unless a fallback provider stamped the one that answered. */
|
|
150
|
+
export async function drainStream(
|
|
151
|
+
stream: AsyncIterable<ProviderChunk>,
|
|
152
|
+
model: string,
|
|
153
|
+
): Promise<Completion> {
|
|
154
|
+
const { message, usage, finishReason, source } = await collectTurn(stream);
|
|
155
|
+
return {
|
|
156
|
+
text: message.content,
|
|
157
|
+
reasoning: message.reasoning ?? "",
|
|
158
|
+
...(message.reasoningDetails ? { reasoningDetails: message.reasoningDetails } : {}),
|
|
159
|
+
...(message.reasoningItems ? { reasoningItems: message.reasoningItems } : {}),
|
|
160
|
+
toolCalls: message.toolCalls ?? [],
|
|
161
|
+
usage,
|
|
162
|
+
finishReason,
|
|
163
|
+
model: source?.model ?? model,
|
|
164
|
+
...(source ? { provider: source.provider } : {}),
|
|
165
|
+
};
|
|
166
|
+
}
|
package/src/types.ts
CHANGED
|
@@ -71,8 +71,9 @@ export type ContentPart = TextPart | ImagePart | FilePart;
|
|
|
71
71
|
|
|
72
72
|
/**
|
|
73
73
|
* A tool the model asked to run. `arguments` is the RAW JSON string, not a
|
|
74
|
-
* parsed object: it arrives in fragments and can be truncated mid-stream
|
|
75
|
-
*
|
|
74
|
+
* parsed object: it arrives in fragments and can be truncated mid-stream.
|
|
75
|
+
* `collectTurn` joins the fragments; healing and validating them is the
|
|
76
|
+
* consumer's job (see tools/define).
|
|
76
77
|
*/
|
|
77
78
|
export interface ToolCall {
|
|
78
79
|
id: string;
|
|
@@ -151,7 +152,7 @@ export interface ToolDefinition {
|
|
|
151
152
|
|
|
152
153
|
export type FinishReason = "stop" | "length" | "tool_calls" | "content_filter";
|
|
153
154
|
|
|
154
|
-
/** Incremental tool-call fragment, assembled by `index`
|
|
155
|
+
/** Incremental tool-call fragment, assembled by `index`. `collectTurn` does it. */
|
|
155
156
|
export interface ToolCallDelta {
|
|
156
157
|
index: number;
|
|
157
158
|
id?: string;
|
|
@@ -385,9 +386,10 @@ export interface StreamOptions {
|
|
|
385
386
|
*/
|
|
386
387
|
jsonWithTools?: JsonWithTools;
|
|
387
388
|
/**
|
|
388
|
-
* Stable id for the conversation or job this call belongs to. Sent
|
|
389
|
-
*
|
|
390
|
-
*
|
|
389
|
+
* Stable id for the conversation or job this call belongs to. Sent in the
|
|
390
|
+
* `sessionHeader` of endpoints that name one (OpenCode Go routes and caches
|
|
391
|
+
* by it), and as `prompt_cache_key` on the Responses shape. A call without
|
|
392
|
+
* one sends the provider's `instanceSessionId` in that header instead.
|
|
391
393
|
*/
|
|
392
394
|
sessionId?: string;
|
|
393
395
|
/**
|
|
@@ -440,62 +442,6 @@ export interface Provider {
|
|
|
440
442
|
): AsyncIterable<ProviderChunk>;
|
|
441
443
|
}
|
|
442
444
|
|
|
443
|
-
export interface Completion {
|
|
444
|
-
text: string;
|
|
445
|
-
/** Present when the stream identifies the endpoint that answered. */
|
|
446
|
-
provider?: string;
|
|
447
|
-
reasoning: string;
|
|
448
|
-
/** Present only where the provider sent one. See ChatMessage.reasoningDetails. */
|
|
449
|
-
reasoningDetails?: unknown[];
|
|
450
|
-
/** Present only where the provider sent them. See ChatMessage.reasoningItems. */
|
|
451
|
-
reasoningItems?: ReasoningItems;
|
|
452
|
-
usage: TokenUsage;
|
|
453
|
-
finishReason: FinishReason | null;
|
|
454
|
-
model: string;
|
|
455
|
-
}
|
|
456
|
-
|
|
457
|
-
/** Drain a no-tools stream into a Completion. Usage chunks are cumulative on
|
|
458
|
-
* some providers and final-only on others: the LAST one wins. */
|
|
459
|
-
export async function drainStream(
|
|
460
|
-
stream: AsyncIterable<ProviderChunk>,
|
|
461
|
-
model: string,
|
|
462
|
-
): Promise<Completion> {
|
|
463
|
-
let text = "";
|
|
464
|
-
let reasoning = "";
|
|
465
|
-
// Not concatenated: this half of the record is a payload the provider owns,
|
|
466
|
-
// and it arrives whole on one delta rather than in fragments. Dropped here,
|
|
467
|
-
// a drained turn replays only half its own reasoning on the next round —
|
|
468
|
-
// which is the failure `reasoningDetails` exists to prevent.
|
|
469
|
-
let reasoningDetails: unknown[] | undefined;
|
|
470
|
-
let reasoningItems: ReasoningItems | undefined;
|
|
471
|
-
let usage: TokenUsage = EMPTY_USAGE;
|
|
472
|
-
let finishReason: FinishReason | null = null;
|
|
473
|
-
let source: ProviderChunk["source"];
|
|
474
|
-
for await (const chunk of stream) {
|
|
475
|
-
if (chunk.source) source = chunk.source;
|
|
476
|
-
if (chunk.type === "delta") {
|
|
477
|
-
if (chunk.content) text += chunk.content;
|
|
478
|
-
if (chunk.reasoning) reasoning += chunk.reasoning;
|
|
479
|
-
if (chunk.reasoningDetails?.length) reasoningDetails = chunk.reasoningDetails;
|
|
480
|
-
if (chunk.reasoningItems?.items.length) reasoningItems = chunk.reasoningItems;
|
|
481
|
-
} else if (chunk.type === "usage" && chunk.usage) {
|
|
482
|
-
usage = chunk.usage;
|
|
483
|
-
} else if (chunk.type === "finish" && chunk.finishReason) {
|
|
484
|
-
finishReason = chunk.finishReason;
|
|
485
|
-
}
|
|
486
|
-
}
|
|
487
|
-
return {
|
|
488
|
-
text,
|
|
489
|
-
reasoning,
|
|
490
|
-
...(reasoningDetails ? { reasoningDetails } : {}),
|
|
491
|
-
...(reasoningItems ? { reasoningItems } : {}),
|
|
492
|
-
usage,
|
|
493
|
-
finishReason,
|
|
494
|
-
model: source?.model ?? model,
|
|
495
|
-
...(source ? { provider: source.provider } : {}),
|
|
496
|
-
};
|
|
497
|
-
}
|
|
498
|
-
|
|
499
445
|
/**
|
|
500
446
|
* Prepare a history for a thinking-DISABLED turn: strip `reasoning` from every
|
|
501
447
|
* assistant message.
|