Skip to content

Commit 96825d2

Browse files
committed
fix(providers): skip unreadable Anthropic tool schemas
1 parent 6c11383 commit 96825d2

2 files changed

Lines changed: 99 additions & 12 deletions

File tree

src/llm/providers/anthropic.test.ts

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -218,6 +218,74 @@ describe("Anthropic provider", () => {
218218
expect((capturedPayload as { stop_sequences?: unknown }).stop_sequences).toEqual(["STOP"]);
219219
});
220220

221+
it("skips unreadable tool schemas while preserving healthy Anthropic tools", async () => {
222+
let capturedPayload: unknown;
223+
const unreadableTool = {
224+
name: "unreadable_schema",
225+
description: "bad getter",
226+
};
227+
Object.defineProperty(unreadableTool, "parameters", {
228+
get() {
229+
throw new Error("parameters getter exploded");
230+
},
231+
});
232+
const nestedSchema = { type: "object" };
233+
Object.defineProperty(nestedSchema, "properties", {
234+
get() {
235+
throw new Error("properties getter exploded");
236+
},
237+
enumerable: true,
238+
});
239+
240+
const stream = streamSimpleAnthropic(
241+
makeAnthropicModel(),
242+
{
243+
messages: [{ role: "user", content: "hello", timestamp: 0 }],
244+
tools: [
245+
unreadableTool,
246+
{
247+
name: "nested_schema",
248+
description: "nested getter",
249+
parameters: nestedSchema,
250+
},
251+
{
252+
name: "healthy_tool",
253+
description: "healthy",
254+
parameters: {
255+
type: "object",
256+
properties: { value: { type: "string" } },
257+
required: ["value"],
258+
},
259+
},
260+
] as never,
261+
},
262+
{
263+
apiKey: "sk-ant-provider",
264+
cacheRetention: "none",
265+
onPayload: (payload) => {
266+
capturedPayload = payload;
267+
throw new Error("stop before network");
268+
},
269+
},
270+
);
271+
272+
const result = await stream.result();
273+
274+
expect(result.stopReason).toBe("error");
275+
expect((capturedPayload as { tools?: unknown }).tools).toEqual([
276+
{
277+
name: "healthy_tool",
278+
description: "healthy",
279+
eager_input_streaming: true,
280+
input_schema: {
281+
type: "object",
282+
properties: { value: { type: "string" } },
283+
required: ["value"],
284+
},
285+
},
286+
]);
287+
});
288+
221289
it("splits the system prompt cache boundary into cached and uncached Anthropic blocks", async () => {
222290
let capturedPayload: unknown;
223291
const stream = streamSimpleAnthropic(

src/llm/providers/anthropic.ts

Lines changed: 31 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1268,21 +1268,40 @@ function convertTools(
12681268
return [];
12691269
}
12701270

1271-
return tools.map((tool, index) => {
1272-
const schema = tool.parameters as { properties?: unknown; required?: string[] };
1271+
const converted = tools.flatMap((tool): Anthropic.Messages.Tool[] => {
1272+
let schema: { properties?: unknown; required?: string[] };
1273+
let properties: unknown;
1274+
let required: string[];
1275+
try {
1276+
schema = tool.parameters as { properties?: unknown; required?: string[] };
1277+
properties = schema.properties ?? {};
1278+
required = schema.required ?? [];
1279+
} catch {
1280+
return [];
1281+
}
12731282

1274-
return {
1275-
name: isOAuthTokenLocal ? toClaudeCodeName(tool.name) : tool.name,
1276-
description: tool.description,
1277-
...(supportsEagerToolInputStreaming ? { eager_input_streaming: true } : {}),
1278-
input_schema: {
1279-
type: "object",
1280-
properties: schema.properties ?? {},
1281-
required: schema.required ?? [],
1283+
return [
1284+
{
1285+
name: isOAuthTokenLocal ? toClaudeCodeName(tool.name) : tool.name,
1286+
description: tool.description,
1287+
...(supportsEagerToolInputStreaming ? { eager_input_streaming: true } : {}),
1288+
input_schema: {
1289+
type: "object",
1290+
properties,
1291+
required,
1292+
},
12821293
},
1283-
...(cacheControl && index === tools.length - 1 ? { cache_control: cacheControl } : {}),
1284-
};
1294+
];
12851295
});
1296+
1297+
if (cacheControl && converted.length > 0) {
1298+
converted[converted.length - 1] = {
1299+
...converted[converted.length - 1],
1300+
cache_control: cacheControl,
1301+
};
1302+
}
1303+
1304+
return converted;
12861305
}
12871306

12881307
function mapStopReason(reason: string): StopReason {

0 commit comments

Comments
 (0)