Skip to content

Commit fbff21b

Browse files
committed
fix(agents): guard OpenAI strict tool diagnostics
1 parent 4e57526 commit fbff21b

2 files changed

Lines changed: 88 additions & 6 deletions

File tree

src/agents/openai-tool-schema.test.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { beforeEach, describe, expect, it } from "vitest";
22
import {
33
clearOpenAIToolSchemaCacheForTest,
4+
findOpenAIStrictToolSchemaDiagnostics,
45
isStrictOpenAIJsonSchemaCompatible,
56
normalizeStrictOpenAIJsonSchema,
67
resolveOpenAIStrictToolFlagForInventory,
@@ -91,4 +92,26 @@ describe("OpenAI strict tool schema normalization", () => {
9192
}),
9293
).toBe(third);
9394
});
95+
96+
it("reports unreadable strict tool parameters without throwing", () => {
97+
const unreadable = {
98+
name: "fuzzplugin_unreadable",
99+
parameters: { type: "object", properties: {} },
100+
};
101+
Object.defineProperty(unreadable, "parameters", {
102+
enumerable: true,
103+
get() {
104+
throw new Error("fuzzplugin parameters getter exploded");
105+
},
106+
});
107+
108+
expect(resolveOpenAIStrictToolFlagForInventory([unreadable], true)).toBe(false);
109+
expect(findOpenAIStrictToolSchemaDiagnostics([unreadable])).toEqual([
110+
{
111+
toolIndex: 0,
112+
toolName: "fuzzplugin_unreadable",
113+
violations: ["fuzzplugin_unreadable.parameters is unreadable"],
114+
},
115+
]);
116+
});
94117
});

src/agents/openai-tool-schema.ts

Lines changed: 65 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -18,9 +18,50 @@ type ToolWithParameters = {
1818
parameters: unknown;
1919
};
2020

21+
type ToolFieldRead<TField extends keyof ToolWithParameters> =
22+
| { readable: true; value: ToolWithParameters[TField] }
23+
| { readable: false };
24+
2125
const MAX_STRICT_SCHEMA_CACHE_ENTRIES_PER_SCHEMA = 8;
2226
let strictOpenAISchemaCache = new WeakMap<object, Array<{ key: string; value: unknown }>>();
2327

28+
function readToolField<TField extends keyof ToolWithParameters>(
29+
tool: ToolWithParameters,
30+
field: TField,
31+
): ToolFieldRead<TField> {
32+
try {
33+
return { readable: true, value: tool[field] };
34+
} catch {
35+
return { readable: false };
36+
}
37+
}
38+
39+
function readToolName(tool: ToolWithParameters, toolIndex: number): {
40+
toolName: string;
41+
diagnosticToolName?: string;
42+
violations: string[];
43+
} {
44+
const nameRead = readToolField(tool, "name");
45+
if (!nameRead.readable) {
46+
const toolName = `tool[${toolIndex}]`;
47+
return {
48+
toolName,
49+
violations: [`${toolName}.name is unreadable`],
50+
};
51+
}
52+
if (typeof nameRead.value === "string" && nameRead.value) {
53+
return {
54+
toolName: nameRead.value,
55+
diagnosticToolName: nameRead.value,
56+
violations: [],
57+
};
58+
}
59+
return {
60+
toolName: `tool[${toolIndex}]`,
61+
violations: [],
62+
};
63+
}
64+
2465
function resolveToolSchemaModelCompat(
2566
compat: ToolSchemaCompatInput | null | undefined,
2667
): ModelCompatConfig | undefined {
@@ -179,18 +220,33 @@ export function findOpenAIStrictToolSchemaDiagnostics(
179220
tools: readonly ToolWithParameters[],
180221
): OpenAIStrictToolSchemaDiagnostic[] {
181222
return tools.flatMap((tool, toolIndex) => {
223+
const nameRead = readToolName(tool, toolIndex);
224+
const parametersRead = readToolField(tool, "parameters");
225+
if (!parametersRead.readable) {
226+
return [
227+
{
228+
toolIndex,
229+
...(nameRead.diagnosticToolName ? { toolName: nameRead.diagnosticToolName } : {}),
230+
violations: [
231+
...nameRead.violations,
232+
`${nameRead.toolName}.parameters is unreadable`,
233+
],
234+
},
235+
];
236+
}
182237
const violations = findStrictOpenAIJsonSchemaViolations(
183-
normalizeStrictOpenAIJsonSchema(tool.parameters),
184-
`${typeof tool.name === "string" && tool.name ? tool.name : `tool[${toolIndex}]`}.parameters`,
238+
normalizeStrictOpenAIJsonSchema(parametersRead.value),
239+
`${nameRead.toolName}.parameters`,
185240
);
186-
if (violations.length === 0) {
241+
const allViolations = [...nameRead.violations, ...violations];
242+
if (allViolations.length === 0) {
187243
return [];
188244
}
189245
return [
190246
{
191247
toolIndex,
192-
...(typeof tool.name === "string" && tool.name ? { toolName: tool.name } : {}),
193-
violations,
248+
...(nameRead.diagnosticToolName ? { toolName: nameRead.diagnosticToolName } : {}),
249+
violations: allViolations,
194250
},
195251
];
196252
});
@@ -317,5 +373,8 @@ export function resolveOpenAIStrictToolFlagForInventory(
317373
if (strict !== true) {
318374
return strict === false ? false : undefined;
319375
}
320-
return tools.every((tool) => isStrictOpenAIJsonSchemaCompatible(tool.parameters));
376+
return tools.every((tool) => {
377+
const parametersRead = readToolField(tool, "parameters");
378+
return parametersRead.readable && isStrictOpenAIJsonSchemaCompatible(parametersRead.value);
379+
});
321380
}

0 commit comments

Comments
 (0)