Skip to content

fix(llm): keep the chat working when a tool schema uses true or false - #1148

Draft
sosidudku1 wants to merge 2 commits into
mainfrom
fix/ato-1399-tool-schema-bool
Draft

sosidudku1 wants to merge 2 commits into
mainfrom
fix/ato-1399-tool-schema-bool

Conversation

@sosidudku1

@sosidudku1 sosidudku1 commented Nov 10, 2026 •

Copy link
Copy Markdown
Collaborator

What

A user connected LM Studio as an OpenAI-compatible provider, and every turn failed with:

JSON schema error at #/$defs/RecordSource/oneOf/1/properties/evidence: schema must be an object

One of their MCP or plugin tools uses a boolean subschema ("evidence": true). That is valid JSON Schema, but LM Studio and llama.cpp reject it, so the whole request failed, not just that tool.

Now:

  • Normalize. Boolean subschemas in every OpenAI-compatible tools array are rewritten. true becomes {} or is dropped where the keyword is optional. false drops the property (and its required entry) or the anyOf/oneOf branch, caps maxItems for items (at the prefixItems length when there is a tuple, 0 otherwise), and becomes { not: {} } elsewhere. additionalProperties and similar keywords keep their boolean values. The input is never mutated, and a tools array with nothing to rewrite is sent byte-identical to before.
  • Drop and resend. If the server still answers with a JSON schema error and the error's pointer names exactly one tool, the request is sent once more without that tool (unary and stream, only before any output). A warning names the tool, the pointer and the status. reply and finish are never dropped. If no single tool matches, the original error goes back. If the resend fails too before any output, the first (schema) error is thrown, not the resend's.
  • No blind retries. A schema error is no longer retried three times as a generic 5xx, and it gets its own user-facing sentence.

Why

One badly-shaped third-party tool should not break the whole chat.

Known gaps

  • The dropped-tool notice goes to the structured log only. The provider layer has no channel that reaches the chat today, so showing it in the TUI or desktop is a separate change.
  • { not: {} } (only for a false that cannot be dropped, which is rare) may not be understood by every local server.
  • With strict tools on, a pointer computed against the rewritten schema may not match, and the original error is surfaced.
  • openai-provider.ts grew from 1023 to 1096 lines.

Merge note

Conflicts with #1123 in openai-provider.ts. #1123 is expected to merge first; when this branch is updated after it, keep learnTemperature(request, err, body), activeRequest, and #1123's sentBody in the unary wrapper.

Tests

  • tool-schema-booleans.test.ts: the exact $defs/RecordSource/oneOf/1/properties/evidence: true case, the other rewrite rules, no mutation, same reference when nothing changes, body builder applies it.
  • tool-schema-rejection.test.ts: pointer matching, including ambiguous, missing and reply/finish cases.
  • openai-tool-schema-retry.test.ts: resend without the tool (unary and stream, exactly two fetches), an unmatched error surfacing after one fetch, at most one drop, the new user-facing sentence, and a failed resend surfacing the first schema error (500 then 400).
  • tool-schema-booleans.test.ts also covers prefixItems + items: false keeping the tuple satisfiable.

Fixes ATO-1399.

Not yet checked on the second Mac.

… server rejects

LM Studio and llama.cpp refuse the whole chat request when one tool's
JSON Schema holds a boolean subschema ("evidence": true), answering
'JSON schema error at #/...: schema must be an object' (ATO-1399).

- normalize boolean subschemas in every OpenAI-compatible tools array
  (true -> {}, false -> dropped property/branch or { not: {} });
  additionalProperties and friends keep their boolean values, and tools
  with nothing to rewrite stay byte-identical
- treat a schema error as non-retryable instead of three blind 5xx tries
- when the error's JSON pointer names exactly one tool, resend once
  without it (unary and stream) and log a warning naming the tool
- give the leftover case its own user-facing sentence
…l schema error

- items: false next to prefixItems now caps maxItems at the tuple length
  (or a tighter existing maxItems) instead of 0
- when the resend without the rejected tool also fails before output,
  the first (schema) error is thrown in both unary and stream paths

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant