From 4a6a58eb5978ebc46df5a2590e65959d7fe1cbf5 Mon Sep 17 00:00:00 2001 From: David Brailovsky Date: Mon, 2 Nov 2026 13:12:50 +0100 Subject: [PATCH] fix(ai): keep root $defs in Anthropic non-strict tool schemas The non-strict input_schema kept only type/properties/required, so property $refs into root $defs or definitions pointed at nothing. Claude then produced wrong arguments (a JSON string instead of an object), and some Anthropic-compatible gateways rejected the request. Root anyOf/oneOf/allOf are still left out: Anthropic rejects them with a 400. That part of #9557 and #9134 stays open. --- packages/ai/CHANGELOG.md | 1 + packages/ai/src/api/anthropic-messages.ts | 11 +++++++- .../test/anthropic-strict-tool-schema.test.ts | 25 +++++++++++++++++++ 3 files changed, 36 insertions(+), 1 deletion(-) diff --git a/packages/ai/CHANGELOG.md b/packages/ai/CHANGELOG.md index 6830dcbe8..1823bff41 100644 --- a/packages/ai/CHANGELOG.md +++ b/packages/ai/CHANGELOG.md @@ -5,6 +5,7 @@ ### Fixed - Fixed OpenCode Go Claude Haiku 5.5 failing with a 400 (`Invalid signature in thinking block ... bound to a different conversation`) after the tool list, system prompt, or earlier history changed, which left the session failing on every retry. OpenCode Go and Zen Claude 5.5 models, Opus 5, and Fable 5.1 now use per-message effort and send `thinking.block_binding: drop_block`, like the Anthropic provider ([#10954](https://github.com/earendil-works/pi/issues/10954)) +- Fixed Anthropic non-strict tool schemas dropping root `$defs` and `definitions`, which left property `$ref`s pointing at nothing. Claude then produced wrong arguments for such tools (common in MCP servers), for example a JSON string instead of an object, and some Anthropic-compatible gateways rejected the request outright ([#9557](https://github.com/earendil-works/pi/issues/9557)) ## [1.5.0] - 2026-11-02 diff --git a/packages/ai/src/api/anthropic-messages.ts b/packages/ai/src/api/anthropic-messages.ts index 9d6a8bdb7..4a81dc44e 100644 --- a/packages/ai/src/api/anthropic-messages.ts +++ b/packages/ai/src/api/anthropic-messages.ts @@ -1632,11 +1632,20 @@ function convertTools( ? undefined : resolveJsonSchemaStrictSampling(tool, supportsStrictTools, isAnthropicStrictUnsupportedKeyword); const parameters = getJsonSchemaToolParameters(tool, strict); - const schema = parameters as { properties?: unknown; required?: string[] }; + const schema = parameters as { + properties?: unknown; + required?: string[]; + $defs?: unknown; + definitions?: unknown; + }; + // Keep shared definitions so property $refs still resolve. Other root keywords stay out: + // Anthropic rejects root anyOf/oneOf/allOf with a 400. const legacyInputSchema = { type: "object" as const, properties: schema.properties ?? {}, required: schema.required ?? [], + ...(schema.$defs === undefined ? {} : { $defs: schema.$defs }), + ...(schema.definitions === undefined ? {} : { definitions: schema.definitions }), }; const inputSchema = strict === true diff --git a/packages/ai/test/anthropic-strict-tool-schema.test.ts b/packages/ai/test/anthropic-strict-tool-schema.test.ts index 587cde17f..0e7ec623d 100644 --- a/packages/ai/test/anthropic-strict-tool-schema.test.ts +++ b/packages/ai/test/anthropic-strict-tool-schema.test.ts @@ -98,6 +98,31 @@ describe("Anthropic strict tool schemas", () => { }); }); + // https://github.com/earendil-works/pi/issues/9557 + it("keeps root definitions in non-strict schemas so property refs resolve", async () => { + const settings = { type: "object", properties: { verbose: { type: "boolean" } } }; + const cases = [ + { ref: "#/$defs/Settings", definitions: { $defs: { Settings: settings } } }, + { ref: "#/definitions/Settings", definitions: { definitions: { Settings: settings } } }, + ]; + for (const { ref, definitions } of cases) { + const parameters = { + type: "object", + properties: { settings: { $ref: ref } }, + required: ["settings"], + ...definitions, + } as unknown as Tool["parameters"]; + const tool = await captureFirstTool(createTool(parameters)); + expect(tool.strict).toBeUndefined(); + expect(tool.input_schema).toEqual({ + type: "object", + properties: { settings: { $ref: ref } }, + required: ["settings"], + ...definitions, + }); + } + }); + // https://github.com/earendil-works/pi/issues/9953 it("sends prefer tools non-strict when they use keywords Anthropic strict mode rejects", async () => { const unsupportedParameters: Tool["parameters"][] = [