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.
This commit is contained in:
David Brailovsky 2026-11-02 13:12:50 +01:00
parent 9ee7586a95
commit 4a6a58eb59
3 changed files with 36 additions and 1 deletions

View file

@ -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

View file

@ -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

View file

@ -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"][] = [