@polpo-ai/server 0.15.38 → 0.15.39
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/routes/completions/agent-step-runner.d.ts.map +1 -1
- package/dist/routes/completions/agent-step-runner.js +10 -2
- package/dist/routes/completions/agent-step-runner.js.map +1 -1
- package/dist/routes/completions/agent-step-runner.test.js +84 -1
- package/dist/routes/completions/agent-step-runner.test.js.map +1 -1
- package/dist/routes/completions/chat-handler.d.ts.map +1 -1
- package/dist/routes/completions/chat-handler.js +25 -7
- package/dist/routes/completions/chat-handler.js.map +1 -1
- package/dist/routes/completions/tool-mapping.d.ts +3 -0
- package/dist/routes/completions/tool-mapping.d.ts.map +1 -1
- package/dist/routes/completions/tool-mapping.gateway.test.d.ts +2 -0
- package/dist/routes/completions/tool-mapping.gateway.test.d.ts.map +1 -0
- package/dist/routes/completions/tool-mapping.gateway.test.js +78 -0
- package/dist/routes/completions/tool-mapping.gateway.test.js.map +1 -0
- package/dist/routes/completions/tool-mapping.js +23 -1
- package/dist/routes/completions/tool-mapping.js.map +1 -1
- package/dist/routes/completions/tool-mapping.test.js +642 -1
- package/dist/routes/completions/tool-mapping.test.js.map +1 -1
- package/package.json +6 -6
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { describe, expect, it, vi } from "vitest";
|
|
2
|
-
import {
|
|
2
|
+
import { generateText } from "ai";
|
|
3
|
+
import { MockLanguageModelV3 } from "ai/test";
|
|
4
|
+
import { persistAssistantMessage, toAITools, toPortableToolInputSchema, } from "./tool-mapping.js";
|
|
3
5
|
function fakeStore() {
|
|
4
6
|
return { updateMessage: vi.fn().mockResolvedValue(true) };
|
|
5
7
|
}
|
|
@@ -45,4 +47,643 @@ describe("persistAssistantMessage", () => {
|
|
|
45
47
|
expect(toolCalls[0].arguments.credentials.password).toBe("s3cret");
|
|
46
48
|
});
|
|
47
49
|
});
|
|
50
|
+
describe("toPortableToolInputSchema", () => {
|
|
51
|
+
it("moves minimum into descriptions without mutating the source", () => {
|
|
52
|
+
const schema = {
|
|
53
|
+
type: "object",
|
|
54
|
+
additionalProperties: false,
|
|
55
|
+
required: ["targetCalories", "missing"],
|
|
56
|
+
properties: {
|
|
57
|
+
targetCalories: {
|
|
58
|
+
type: "number",
|
|
59
|
+
minimum: 1000,
|
|
60
|
+
maximum: 5000,
|
|
61
|
+
multipleOf: 5,
|
|
62
|
+
description: "Daily calorie target.",
|
|
63
|
+
},
|
|
64
|
+
label: {
|
|
65
|
+
type: "string",
|
|
66
|
+
minLength: 2,
|
|
67
|
+
maxLength: 40,
|
|
68
|
+
pattern: "^[A-Za-z ]+$",
|
|
69
|
+
},
|
|
70
|
+
},
|
|
71
|
+
};
|
|
72
|
+
const original = structuredClone(schema);
|
|
73
|
+
expect(toPortableToolInputSchema(schema)).toEqual({
|
|
74
|
+
type: "object",
|
|
75
|
+
additionalProperties: false,
|
|
76
|
+
required: ["targetCalories", "missing"],
|
|
77
|
+
properties: {
|
|
78
|
+
targetCalories: {
|
|
79
|
+
type: "number",
|
|
80
|
+
maximum: 5000,
|
|
81
|
+
multipleOf: 5,
|
|
82
|
+
description: "Daily calorie target. Minimum: 1000.",
|
|
83
|
+
},
|
|
84
|
+
label: {
|
|
85
|
+
type: "string",
|
|
86
|
+
minLength: 2,
|
|
87
|
+
maxLength: 40,
|
|
88
|
+
pattern: "^[A-Za-z ]+$",
|
|
89
|
+
},
|
|
90
|
+
},
|
|
91
|
+
});
|
|
92
|
+
expect(schema).toEqual(original);
|
|
93
|
+
});
|
|
94
|
+
it("preserves unions, references, dictionaries, tuples, and nullability", () => {
|
|
95
|
+
const schema = {
|
|
96
|
+
type: "object",
|
|
97
|
+
properties: {
|
|
98
|
+
choice: {
|
|
99
|
+
oneOf: [
|
|
100
|
+
{ $ref: "#/$defs/named" },
|
|
101
|
+
{ type: ["number", "null"], minimum: 10 },
|
|
102
|
+
],
|
|
103
|
+
},
|
|
104
|
+
metadata: {
|
|
105
|
+
type: "object",
|
|
106
|
+
additionalProperties: { type: "string" },
|
|
107
|
+
},
|
|
108
|
+
tuple: {
|
|
109
|
+
type: "array",
|
|
110
|
+
items: [{ type: "string" }, { type: "number", minimum: 2 }],
|
|
111
|
+
additionalItems: { type: "number", minimum: 3 },
|
|
112
|
+
},
|
|
113
|
+
},
|
|
114
|
+
$defs: {
|
|
115
|
+
named: {
|
|
116
|
+
type: "object",
|
|
117
|
+
properties: {
|
|
118
|
+
name: { type: "string" },
|
|
119
|
+
},
|
|
120
|
+
required: ["name"],
|
|
121
|
+
},
|
|
122
|
+
},
|
|
123
|
+
};
|
|
124
|
+
expect(toPortableToolInputSchema(schema)).toEqual({
|
|
125
|
+
type: "object",
|
|
126
|
+
properties: {
|
|
127
|
+
choice: {
|
|
128
|
+
oneOf: [
|
|
129
|
+
{ $ref: "#/$defs/named" },
|
|
130
|
+
{
|
|
131
|
+
type: ["number", "null"],
|
|
132
|
+
description: "Minimum: 10.",
|
|
133
|
+
},
|
|
134
|
+
],
|
|
135
|
+
},
|
|
136
|
+
metadata: {
|
|
137
|
+
type: "object",
|
|
138
|
+
additionalProperties: { type: "string" },
|
|
139
|
+
},
|
|
140
|
+
tuple: {
|
|
141
|
+
type: "array",
|
|
142
|
+
items: [
|
|
143
|
+
{ type: "string" },
|
|
144
|
+
{ type: "number", description: "Minimum: 2." },
|
|
145
|
+
],
|
|
146
|
+
additionalItems: {
|
|
147
|
+
type: "number",
|
|
148
|
+
description: "Minimum: 3.",
|
|
149
|
+
},
|
|
150
|
+
},
|
|
151
|
+
},
|
|
152
|
+
$defs: {
|
|
153
|
+
named: {
|
|
154
|
+
type: "object",
|
|
155
|
+
properties: {
|
|
156
|
+
name: { type: "string" },
|
|
157
|
+
},
|
|
158
|
+
required: ["name"],
|
|
159
|
+
},
|
|
160
|
+
},
|
|
161
|
+
});
|
|
162
|
+
});
|
|
163
|
+
it("preserves allOf branches instead of inventing merged semantics", () => {
|
|
164
|
+
const schema = {
|
|
165
|
+
type: "object",
|
|
166
|
+
allOf: [
|
|
167
|
+
{
|
|
168
|
+
properties: {
|
|
169
|
+
value: { type: "number", minimum: 10 },
|
|
170
|
+
},
|
|
171
|
+
required: ["value"],
|
|
172
|
+
},
|
|
173
|
+
{
|
|
174
|
+
properties: {
|
|
175
|
+
value: { type: "number", maximum: 5 },
|
|
176
|
+
},
|
|
177
|
+
},
|
|
178
|
+
],
|
|
179
|
+
};
|
|
180
|
+
expect(toPortableToolInputSchema(schema)).toEqual({
|
|
181
|
+
type: "object",
|
|
182
|
+
allOf: [
|
|
183
|
+
{
|
|
184
|
+
properties: {
|
|
185
|
+
value: { type: "number", description: "Minimum: 10." },
|
|
186
|
+
},
|
|
187
|
+
required: ["value"],
|
|
188
|
+
},
|
|
189
|
+
{
|
|
190
|
+
properties: {
|
|
191
|
+
value: { type: "number", maximum: 5 },
|
|
192
|
+
},
|
|
193
|
+
},
|
|
194
|
+
],
|
|
195
|
+
});
|
|
196
|
+
});
|
|
197
|
+
it("falls back only for non-object schemas", () => {
|
|
198
|
+
expect(toPortableToolInputSchema(null)).toEqual({
|
|
199
|
+
type: "object",
|
|
200
|
+
properties: {},
|
|
201
|
+
});
|
|
202
|
+
expect(toPortableToolInputSchema(true)).toEqual({
|
|
203
|
+
type: "object",
|
|
204
|
+
properties: {},
|
|
205
|
+
});
|
|
206
|
+
});
|
|
207
|
+
it("does not rewrite object-valued enum, const, default, or examples", () => {
|
|
208
|
+
const literal = {
|
|
209
|
+
minimum: 1,
|
|
210
|
+
nested: { minimum: 2 },
|
|
211
|
+
};
|
|
212
|
+
const schema = {
|
|
213
|
+
type: "object",
|
|
214
|
+
properties: {
|
|
215
|
+
payload: {
|
|
216
|
+
enum: [literal],
|
|
217
|
+
const: literal,
|
|
218
|
+
default: literal,
|
|
219
|
+
examples: [literal],
|
|
220
|
+
},
|
|
221
|
+
},
|
|
222
|
+
};
|
|
223
|
+
expect(toPortableToolInputSchema(schema)).toEqual(schema);
|
|
224
|
+
expect(toPortableToolInputSchema(schema)).not.toBe(schema);
|
|
225
|
+
});
|
|
226
|
+
it("uses the portable schema for providers and the original for validation", async () => {
|
|
227
|
+
const tools = toAITools([
|
|
228
|
+
{
|
|
229
|
+
name: "calculate",
|
|
230
|
+
description: "Calculate a target.",
|
|
231
|
+
parameters: {
|
|
232
|
+
type: "object",
|
|
233
|
+
additionalProperties: false,
|
|
234
|
+
properties: {
|
|
235
|
+
target: {
|
|
236
|
+
type: "number",
|
|
237
|
+
minimum: 1000,
|
|
238
|
+
maximum: 5000,
|
|
239
|
+
},
|
|
240
|
+
},
|
|
241
|
+
required: ["target"],
|
|
242
|
+
},
|
|
243
|
+
},
|
|
244
|
+
]);
|
|
245
|
+
expect(tools.calculate.inputSchema.jsonSchema).toEqual({
|
|
246
|
+
type: "object",
|
|
247
|
+
additionalProperties: false,
|
|
248
|
+
properties: {
|
|
249
|
+
target: {
|
|
250
|
+
type: "number",
|
|
251
|
+
maximum: 5000,
|
|
252
|
+
description: "Minimum: 1000.",
|
|
253
|
+
},
|
|
254
|
+
},
|
|
255
|
+
required: ["target"],
|
|
256
|
+
});
|
|
257
|
+
expect(await tools.calculate.inputSchema.validate({ target: 999 })).toEqual(expect.objectContaining({
|
|
258
|
+
success: false,
|
|
259
|
+
error: expect.any(Error),
|
|
260
|
+
}));
|
|
261
|
+
expect(await tools.calculate.inputSchema.validate({ target: 1000 })).toEqual({
|
|
262
|
+
success: true,
|
|
263
|
+
value: { target: 1000 },
|
|
264
|
+
});
|
|
265
|
+
expect(await tools.calculate.inputSchema.validate({
|
|
266
|
+
target: 1000,
|
|
267
|
+
unexpected: true,
|
|
268
|
+
})).toEqual(expect.objectContaining({
|
|
269
|
+
success: false,
|
|
270
|
+
error: expect.any(Error),
|
|
271
|
+
}));
|
|
272
|
+
});
|
|
273
|
+
it.each([
|
|
274
|
+
"http://json-schema.org/draft-07/schema#",
|
|
275
|
+
"https://json-schema.org/draft/2019-09/schema",
|
|
276
|
+
"https://json-schema.org/draft/2020-12/schema",
|
|
277
|
+
])("validates numeric constraints declared with %s", async ($schema) => {
|
|
278
|
+
const tools = toAITools([
|
|
279
|
+
{
|
|
280
|
+
name: "draft_check",
|
|
281
|
+
parameters: {
|
|
282
|
+
$schema,
|
|
283
|
+
type: "object",
|
|
284
|
+
properties: {
|
|
285
|
+
value: { type: "number", minimum: 10 },
|
|
286
|
+
},
|
|
287
|
+
required: ["value"],
|
|
288
|
+
},
|
|
289
|
+
},
|
|
290
|
+
]);
|
|
291
|
+
const validate = tools.draft_check.inputSchema.validate;
|
|
292
|
+
expect(await validate({ value: 9 })).toEqual(expect.objectContaining({
|
|
293
|
+
success: false,
|
|
294
|
+
error: expect.any(Error),
|
|
295
|
+
}));
|
|
296
|
+
expect(await validate({ value: 10 })).toEqual({
|
|
297
|
+
success: true,
|
|
298
|
+
value: { value: 10 },
|
|
299
|
+
});
|
|
300
|
+
});
|
|
301
|
+
it("validates registered string formats", async () => {
|
|
302
|
+
const tools = toAITools([
|
|
303
|
+
{
|
|
304
|
+
name: "contact",
|
|
305
|
+
parameters: {
|
|
306
|
+
$schema: "https://json-schema.org/draft/2020-12/schema",
|
|
307
|
+
type: "object",
|
|
308
|
+
additionalProperties: false,
|
|
309
|
+
properties: {
|
|
310
|
+
email: { type: "string", format: "email" },
|
|
311
|
+
},
|
|
312
|
+
required: ["email"],
|
|
313
|
+
},
|
|
314
|
+
},
|
|
315
|
+
]);
|
|
316
|
+
const validate = tools.contact.inputSchema.validate;
|
|
317
|
+
expect(await validate({ email: "not-an-email" })).toEqual(expect.objectContaining({
|
|
318
|
+
success: false,
|
|
319
|
+
error: expect.any(Error),
|
|
320
|
+
}));
|
|
321
|
+
expect(await validate({ email: "person@example.com" })).toEqual({
|
|
322
|
+
success: true,
|
|
323
|
+
value: { email: "person@example.com" },
|
|
324
|
+
});
|
|
325
|
+
});
|
|
326
|
+
it("fails closed for nonstandard async schemas in edge runtimes", async () => {
|
|
327
|
+
const tools = toAITools([
|
|
328
|
+
{
|
|
329
|
+
name: "async_check",
|
|
330
|
+
parameters: {
|
|
331
|
+
$async: true,
|
|
332
|
+
type: "object",
|
|
333
|
+
properties: {
|
|
334
|
+
value: { type: "number", minimum: 10 },
|
|
335
|
+
},
|
|
336
|
+
required: ["value"],
|
|
337
|
+
},
|
|
338
|
+
},
|
|
339
|
+
]);
|
|
340
|
+
const validate = tools.async_check.inputSchema.validate;
|
|
341
|
+
expect(tools.async_check.inputSchema.jsonSchema).not.toHaveProperty("$async");
|
|
342
|
+
expect(await validate({ value: 9 })).toEqual(expect.objectContaining({
|
|
343
|
+
success: false,
|
|
344
|
+
error: expect.objectContaining({
|
|
345
|
+
message: expect.stringContaining("$async is not supported"),
|
|
346
|
+
}),
|
|
347
|
+
}));
|
|
348
|
+
expect(await validate({ value: 10 })).toEqual(expect.objectContaining({
|
|
349
|
+
success: false,
|
|
350
|
+
error: expect.objectContaining({
|
|
351
|
+
message: expect.stringContaining("$async is not supported"),
|
|
352
|
+
}),
|
|
353
|
+
}));
|
|
354
|
+
});
|
|
355
|
+
it("fails closed when an original schema cannot be compiled", async () => {
|
|
356
|
+
const tools = toAITools([
|
|
357
|
+
{
|
|
358
|
+
name: "broken",
|
|
359
|
+
parameters: {
|
|
360
|
+
type: "object",
|
|
361
|
+
properties: {
|
|
362
|
+
payload: { $ref: "#/$defs/missing" },
|
|
363
|
+
},
|
|
364
|
+
},
|
|
365
|
+
},
|
|
366
|
+
]);
|
|
367
|
+
const result = await tools.broken.inputSchema.validate({
|
|
368
|
+
payload: "unsafe",
|
|
369
|
+
});
|
|
370
|
+
expect(result).toEqual(expect.objectContaining({
|
|
371
|
+
success: false,
|
|
372
|
+
error: expect.objectContaining({
|
|
373
|
+
message: expect.stringContaining("Invalid tool input schema"),
|
|
374
|
+
}),
|
|
375
|
+
}));
|
|
376
|
+
});
|
|
377
|
+
it("isolates separately decoded schemas that reuse the same $id", async () => {
|
|
378
|
+
const first = toAITools([{
|
|
379
|
+
name: "first",
|
|
380
|
+
parameters: {
|
|
381
|
+
$id: "https://example.com/reused-tool-schema",
|
|
382
|
+
type: "object",
|
|
383
|
+
properties: { first: { type: "string" } },
|
|
384
|
+
required: ["first"],
|
|
385
|
+
},
|
|
386
|
+
}]);
|
|
387
|
+
const second = toAITools([{
|
|
388
|
+
name: "second",
|
|
389
|
+
parameters: {
|
|
390
|
+
$id: "https://example.com/reused-tool-schema",
|
|
391
|
+
type: "object",
|
|
392
|
+
properties: { second: { type: "number" } },
|
|
393
|
+
required: ["second"],
|
|
394
|
+
},
|
|
395
|
+
}]);
|
|
396
|
+
const [firstResult, secondResult] = await Promise.all([
|
|
397
|
+
first.first.inputSchema.validate({ first: "ok" }),
|
|
398
|
+
second.second.inputSchema.validate({ second: 2 }),
|
|
399
|
+
]);
|
|
400
|
+
expect(firstResult).toEqual({
|
|
401
|
+
success: true,
|
|
402
|
+
value: { first: "ok" },
|
|
403
|
+
});
|
|
404
|
+
expect(secondResult).toEqual({
|
|
405
|
+
success: true,
|
|
406
|
+
value: { second: 2 },
|
|
407
|
+
});
|
|
408
|
+
});
|
|
409
|
+
it("validates ordinary local references without mutating the source schema", async () => {
|
|
410
|
+
const schema = {
|
|
411
|
+
$schema: "https://json-schema.org/draft/2020-12/schema",
|
|
412
|
+
type: "object",
|
|
413
|
+
properties: {
|
|
414
|
+
payload: { $ref: "#/$defs/payload" },
|
|
415
|
+
},
|
|
416
|
+
required: ["payload"],
|
|
417
|
+
$defs: {
|
|
418
|
+
payload: {
|
|
419
|
+
type: "object",
|
|
420
|
+
properties: { name: { type: "string" } },
|
|
421
|
+
required: ["name"],
|
|
422
|
+
},
|
|
423
|
+
},
|
|
424
|
+
};
|
|
425
|
+
const original = structuredClone(schema);
|
|
426
|
+
const tools = toAITools([{ name: "local_ref", parameters: schema }]);
|
|
427
|
+
const validate = tools.local_ref.inputSchema.validate;
|
|
428
|
+
expect(await validate({ payload: { name: "safe" } })).toEqual({
|
|
429
|
+
success: true,
|
|
430
|
+
value: { payload: { name: "safe" } },
|
|
431
|
+
});
|
|
432
|
+
expect(await validate({ payload: { name: 42 } })).toEqual(expect.objectContaining({ success: false, error: expect.any(Error) }));
|
|
433
|
+
expect(schema).toEqual(original);
|
|
434
|
+
});
|
|
435
|
+
it("enforces recursive Draft 2020-12 dynamic references", async () => {
|
|
436
|
+
const tools = toAITools([{
|
|
437
|
+
name: "recursive_tree",
|
|
438
|
+
parameters: {
|
|
439
|
+
$schema: "https://json-schema.org/draft/2020-12/schema",
|
|
440
|
+
$dynamicAnchor: "node",
|
|
441
|
+
type: "object",
|
|
442
|
+
properties: {
|
|
443
|
+
data: { type: "string" },
|
|
444
|
+
children: {
|
|
445
|
+
type: "array",
|
|
446
|
+
items: { $dynamicRef: "#node" },
|
|
447
|
+
},
|
|
448
|
+
},
|
|
449
|
+
required: ["data"],
|
|
450
|
+
},
|
|
451
|
+
}]);
|
|
452
|
+
const validate = tools.recursive_tree.inputSchema.validate;
|
|
453
|
+
expect(await validate({
|
|
454
|
+
data: "root",
|
|
455
|
+
children: [{ data: "child" }],
|
|
456
|
+
})).toEqual({
|
|
457
|
+
success: true,
|
|
458
|
+
value: { data: "root", children: [{ data: "child" }] },
|
|
459
|
+
});
|
|
460
|
+
expect(await validate({
|
|
461
|
+
data: "root",
|
|
462
|
+
children: [{ data: 42 }],
|
|
463
|
+
})).toEqual(expect.objectContaining({ success: false, error: expect.any(Error) }));
|
|
464
|
+
});
|
|
465
|
+
it("fails closed for malformed schemas instead of treating them as parameterless", async () => {
|
|
466
|
+
const tools = toAITools([{
|
|
467
|
+
name: "malformed",
|
|
468
|
+
parameters: {
|
|
469
|
+
$schema: "https://json-schema.org/draft/2020-12/schema",
|
|
470
|
+
type: "object",
|
|
471
|
+
properties: {
|
|
472
|
+
value: { type: "number", minimum: "not-a-number" },
|
|
473
|
+
},
|
|
474
|
+
},
|
|
475
|
+
}]);
|
|
476
|
+
expect(await tools.malformed.inputSchema.validate({ value: 100 })).toEqual(expect.objectContaining({
|
|
477
|
+
success: false,
|
|
478
|
+
error: expect.objectContaining({
|
|
479
|
+
message: expect.stringContaining("Invalid tool input schema"),
|
|
480
|
+
}),
|
|
481
|
+
}));
|
|
482
|
+
expect(tools.malformed.inputSchema.jsonSchema).toEqual({
|
|
483
|
+
type: "object",
|
|
484
|
+
properties: {
|
|
485
|
+
value: {
|
|
486
|
+
type: "number",
|
|
487
|
+
description: "Minimum: not-a-number.",
|
|
488
|
+
},
|
|
489
|
+
},
|
|
490
|
+
$schema: "https://json-schema.org/draft/2020-12/schema",
|
|
491
|
+
});
|
|
492
|
+
});
|
|
493
|
+
it("fails closed for unknown schema dialects", async () => {
|
|
494
|
+
const tools = toAITools([{
|
|
495
|
+
name: "unknown_draft",
|
|
496
|
+
parameters: {
|
|
497
|
+
$schema: "https://example.com/unknown-schema-dialect",
|
|
498
|
+
type: "object",
|
|
499
|
+
},
|
|
500
|
+
}]);
|
|
501
|
+
expect(await tools.unknown_draft.inputSchema.validate({})).toEqual(expect.objectContaining({
|
|
502
|
+
success: false,
|
|
503
|
+
error: expect.objectContaining({
|
|
504
|
+
message: expect.stringContaining("Unsupported JSON Schema dialect"),
|
|
505
|
+
}),
|
|
506
|
+
}));
|
|
507
|
+
expect(tools.unknown_draft.inputSchema.jsonSchema).toEqual({
|
|
508
|
+
type: "object",
|
|
509
|
+
properties: {},
|
|
510
|
+
additionalProperties: false,
|
|
511
|
+
});
|
|
512
|
+
});
|
|
513
|
+
it("rejects external references before the validator can perform network I/O", async () => {
|
|
514
|
+
const tools = toAITools([{
|
|
515
|
+
name: "external_ref",
|
|
516
|
+
parameters: {
|
|
517
|
+
$schema: "https://json-schema.org/draft/2020-12/schema",
|
|
518
|
+
type: "object",
|
|
519
|
+
properties: {
|
|
520
|
+
payload: { $ref: "https://attacker.example/schema.json" },
|
|
521
|
+
},
|
|
522
|
+
},
|
|
523
|
+
}]);
|
|
524
|
+
expect(await tools.external_ref.inputSchema.validate({ payload: {} })).toEqual(expect.objectContaining({
|
|
525
|
+
success: false,
|
|
526
|
+
error: expect.objectContaining({
|
|
527
|
+
message: expect.stringContaining("External tool schema references are not supported"),
|
|
528
|
+
}),
|
|
529
|
+
}));
|
|
530
|
+
expect(tools.external_ref.inputSchema.jsonSchema).toEqual({
|
|
531
|
+
type: "object",
|
|
532
|
+
properties: {},
|
|
533
|
+
additionalProperties: false,
|
|
534
|
+
});
|
|
535
|
+
});
|
|
536
|
+
it("does not interpret references embedded in literal instance values", async () => {
|
|
537
|
+
const literal = { $ref: "https://example.com/this-is-data" };
|
|
538
|
+
const tools = toAITools([{
|
|
539
|
+
name: "literal_reference",
|
|
540
|
+
parameters: {
|
|
541
|
+
$schema: "https://json-schema.org/draft/2020-12/schema",
|
|
542
|
+
type: "object",
|
|
543
|
+
properties: {
|
|
544
|
+
payload: { const: literal },
|
|
545
|
+
},
|
|
546
|
+
required: ["payload"],
|
|
547
|
+
},
|
|
548
|
+
}]);
|
|
549
|
+
const validate = tools.literal_reference.inputSchema.validate;
|
|
550
|
+
expect(await validate({ payload: literal })).toEqual({
|
|
551
|
+
success: true,
|
|
552
|
+
value: { payload: literal },
|
|
553
|
+
});
|
|
554
|
+
});
|
|
555
|
+
it("fails closed for non-JSON schema values without poisoning structural cache entries", async () => {
|
|
556
|
+
const malformed = toAITools([{
|
|
557
|
+
name: "malformed_runtime_value",
|
|
558
|
+
parameters: {
|
|
559
|
+
type: "object",
|
|
560
|
+
properties: {
|
|
561
|
+
value: {
|
|
562
|
+
type: "number",
|
|
563
|
+
custom: () => "not JSON",
|
|
564
|
+
},
|
|
565
|
+
},
|
|
566
|
+
},
|
|
567
|
+
}]);
|
|
568
|
+
const valid = toAITools([{
|
|
569
|
+
name: "valid_runtime_value",
|
|
570
|
+
parameters: {
|
|
571
|
+
type: "object",
|
|
572
|
+
properties: {
|
|
573
|
+
value: { type: "number" },
|
|
574
|
+
},
|
|
575
|
+
},
|
|
576
|
+
}]);
|
|
577
|
+
expect(await malformed.malformed_runtime_value.inputSchema.validate({
|
|
578
|
+
value: 1,
|
|
579
|
+
})).toEqual(expect.objectContaining({
|
|
580
|
+
success: false,
|
|
581
|
+
error: expect.objectContaining({
|
|
582
|
+
message: expect.stringContaining("non-JSON function"),
|
|
583
|
+
}),
|
|
584
|
+
}));
|
|
585
|
+
expect(await valid.valid_runtime_value.inputSchema.validate({
|
|
586
|
+
value: 1,
|
|
587
|
+
})).toEqual({ success: true, value: { value: 1 } });
|
|
588
|
+
});
|
|
589
|
+
it("accepts TypeBox-style symbol metadata that is absent from serialized JSON Schema", async () => {
|
|
590
|
+
const valueSchema = { type: "number" };
|
|
591
|
+
const schema = {
|
|
592
|
+
type: "object",
|
|
593
|
+
properties: { value: valueSchema },
|
|
594
|
+
required: ["value"],
|
|
595
|
+
};
|
|
596
|
+
Object.defineProperty(schema, Symbol.for("TypeBox.Kind"), {
|
|
597
|
+
value: "Object",
|
|
598
|
+
enumerable: true,
|
|
599
|
+
});
|
|
600
|
+
Object.defineProperty(valueSchema, Symbol.for("TypeBox.Kind"), {
|
|
601
|
+
value: "Number",
|
|
602
|
+
enumerable: true,
|
|
603
|
+
});
|
|
604
|
+
const tools = toAITools([{
|
|
605
|
+
name: "typebox_metadata",
|
|
606
|
+
parameters: schema,
|
|
607
|
+
}]);
|
|
608
|
+
const validate = tools.typebox_metadata.inputSchema.validate;
|
|
609
|
+
expect(await validate({ value: 1 })).toEqual({
|
|
610
|
+
success: true,
|
|
611
|
+
value: { value: 1 },
|
|
612
|
+
});
|
|
613
|
+
expect(await validate({ value: "wrong" })).toEqual(expect.objectContaining({ success: false, error: expect.any(Error) }));
|
|
614
|
+
});
|
|
615
|
+
it("ignores validator-internal-looking fields instead of redirecting references", async () => {
|
|
616
|
+
const schema = {
|
|
617
|
+
$schema: "https://json-schema.org/draft/2020-12/schema",
|
|
618
|
+
type: "object",
|
|
619
|
+
properties: {
|
|
620
|
+
value: {
|
|
621
|
+
$ref: "#/$defs/strict",
|
|
622
|
+
__absolute_ref__: "#/$defs/permissive",
|
|
623
|
+
},
|
|
624
|
+
},
|
|
625
|
+
required: ["value"],
|
|
626
|
+
$defs: {
|
|
627
|
+
strict: { type: "string" },
|
|
628
|
+
permissive: {},
|
|
629
|
+
},
|
|
630
|
+
};
|
|
631
|
+
const original = structuredClone(schema);
|
|
632
|
+
const tools = toAITools([{ name: "reserved_field", parameters: schema }]);
|
|
633
|
+
const validate = tools.reserved_field.inputSchema.validate;
|
|
634
|
+
expect(await validate({ value: 42 })).toEqual(expect.objectContaining({ success: false, error: expect.any(Error) }));
|
|
635
|
+
expect(await validate({ value: "safe" })).toEqual({
|
|
636
|
+
success: true,
|
|
637
|
+
value: { value: "safe" },
|
|
638
|
+
});
|
|
639
|
+
expect(schema).toEqual(original);
|
|
640
|
+
});
|
|
641
|
+
it("marks provider tool calls invalid when they violate the original schema", async () => {
|
|
642
|
+
const model = new MockLanguageModelV3({
|
|
643
|
+
doGenerate: {
|
|
644
|
+
content: [
|
|
645
|
+
{
|
|
646
|
+
type: "tool-call",
|
|
647
|
+
toolCallId: "call_1",
|
|
648
|
+
toolName: "calculate",
|
|
649
|
+
input: JSON.stringify({ target: 999 }),
|
|
650
|
+
},
|
|
651
|
+
],
|
|
652
|
+
finishReason: { unified: "tool-calls", raw: undefined },
|
|
653
|
+
usage: {
|
|
654
|
+
inputTokens: { total: 1 },
|
|
655
|
+
outputTokens: { total: 1 },
|
|
656
|
+
},
|
|
657
|
+
warnings: [],
|
|
658
|
+
},
|
|
659
|
+
});
|
|
660
|
+
const tools = toAITools([
|
|
661
|
+
{
|
|
662
|
+
name: "calculate",
|
|
663
|
+
parameters: {
|
|
664
|
+
type: "object",
|
|
665
|
+
properties: {
|
|
666
|
+
target: { type: "number", minimum: 1000 },
|
|
667
|
+
},
|
|
668
|
+
required: ["target"],
|
|
669
|
+
},
|
|
670
|
+
},
|
|
671
|
+
]);
|
|
672
|
+
const result = await generateText({
|
|
673
|
+
model,
|
|
674
|
+
prompt: "Calculate a target.",
|
|
675
|
+
tools,
|
|
676
|
+
});
|
|
677
|
+
expect(result.toolCalls).toEqual([
|
|
678
|
+
expect.objectContaining({
|
|
679
|
+
toolName: "calculate",
|
|
680
|
+
input: { target: 999 },
|
|
681
|
+
invalid: true,
|
|
682
|
+
error: expect.objectContaining({
|
|
683
|
+
message: expect.stringContaining("minimum"),
|
|
684
|
+
}),
|
|
685
|
+
}),
|
|
686
|
+
]);
|
|
687
|
+
});
|
|
688
|
+
});
|
|
48
689
|
//# sourceMappingURL=tool-mapping.test.js.map
|