fix(ai): tool calls fail when an unsupported schema keyword is nested - #115741
Conversation
d6ac7ff to
51b7b42
Compare
The strip walked only properties, items, anyOf, oneOf and allOf, copying every other value through untouched. A keyword the model rejects therefore survived inside additionalProperties, prefixItems, patternProperties, contains, propertyNames, not, if/then/else, dependentSchemas and $defs, and the request was refused by the provider even though the strip reported success. Walk the same containers the caller already enumerates in agent-tools-parameter-schema.ts.
51b7b42 to
ef945d9
Compare
|
Codex review: needs maintainer review before merge. Reviewed July 29, 2026, 10:46 AM ET / 14:46 UTC. ClawSweeper reviewWhat this changesExtends unsupported JSON Schema keyword stripping to recurse through additional schema-bearing map, object, and array containers before tool definitions are sent to OpenAI-compatible providers. Merge readinessKeep this PR open for normal maintainer review. Its proposed traversal and regression coverage appear narrowly targeted, and the PR body supplies credible wire-level proof, but this review environment could not complete the required read-only inspection of current Priority: P2 Review scores
Verification
How this fits togetherProvider compatibility settings declare JSON Schema keywords that a target model cannot accept. OpenClaw cleans tool parameter schemas before the transport serializes them into a provider request, so incomplete traversal can leave rejected keywords on the wire and prevent tool calls. flowchart LR
A[Tool parameter schema] --> B[Provider compatibility settings]
B --> C[Schema keyword cleaner]
A --> C
C --> D[Cleaned tool definition]
D --> E[OpenAI-compatible transport]
E --> F[Provider tool-call request]
Before merge
Agent review detailsSecurityNone. PR surfaceSource +30, Tests +98. Total +128 across 2 files. View PR surface stats
Review metricsNone. Stored data modelPersistent data-model change detected: Merge-risk optionsMaintainer options:
Technical reviewBest possible solution: Confirm the helper’s full container set against the package’s canonical schema walkers on the current merge result, retain focused regression tests for each distinct container shape, and land only after the pending checks complete successfully. Do we have a high-confidence way to reproduce the issue? Unclear at high confidence: the PR provides a concrete local HTTP/SSE reproduction showing the unsupported keyword in the outbound payload, but this review could not independently execute or trace the current-main path. Is this the best way to solve the issue? Unclear: matching the cleaner to the package’s schema-container taxonomy appears maintainable, but a required comparison with the current caller, sibling walkers, and history could not be completed in this environment. AGENTS.md: unclear because the file could not be read completely. Codex review notes: model internal, reasoning high; reviewed against 17a8961a6a36. LabelsLabel justifications:
EvidenceWhat I checked:
Likely related people:
Rank-up movesOptional improvements that raise the rating; they are not merge blockers.
Rating scale
Overall follows the weaker of proof and patch quality. Workflow
History |
|
Merged via squash.
|
…openclaw#115741) * fix(ai): tool calls fail when an unsupported schema keyword is nested The strip walked only properties, items, anyOf, oneOf and allOf, copying every other value through untouched. A keyword the model rejects therefore survived inside additionalProperties, prefixItems, patternProperties, contains, propertyNames, not, if/then/else, dependentSchemas and $defs, and the request was refused by the provider even though the strip reported success. Walk the same containers the caller already enumerates in agent-tools-parameter-schema.ts. * fix(ai): cover all nested schema containers --------- Co-authored-by: Vincent Koc <vincentkoc@ieee.org>
What Problem This Solves
Fixes an issue where users on a model that declares
unsupportedToolSchemaKeywordswould still get their tool calls refused by the provider, because the keyword the model cannot accept was left in the request.The compatibility layer is meant to make these models usable by removing the offending keywords before the request goes out. It reports success, but the keyword is still there whenever it sits inside a schema container the strip does not walk. From the user's side the model simply fails to call the tool, and the compat setting looks like it does nothing.
Models that configure this today: xAI and Venice (
minContains,maxContains), Fireworks (not), and any LM Studio model configured with a keyword list.Why This Change Was Made
stripUnsupportedSchemaKeywordsrecursed through five containers —properties,items,anyOf,oneOf,allOf— and copied every other value through verbatim. JSON Schema has considerably more places a subschema can live, so a keyword nested under any of these survived:additionalProperties,prefixItems,patternProperties,contains,propertyNames,not,if/then/else,dependentSchemas,$defs,definitionsThese are not exotic shapes.
additionalPropertiesholding a schema is the ordinary way to describe a dictionary, and it is common in MCP tool definitions.The full container list was already written down in this function's own caller,
agent-tools-parameter-schema.ts, asSCHEMA_MAP_KEYS,SCHEMA_OBJECT_KEYSandSCHEMA_ARRAY_KEYS— and again as theARRAY_ITEMS_SCHEMA_*sets in the same file. This change mirrors those sets so the strip walks the same containers the rest of the pipeline already agrees on. The sets are declared locally to avoid a cycle, matching how the file already duplicates them.Traversal order is map, then array, then object, which preserves the previous dual handling of
items(array form maps over entries, object form recurses once). Non-schema values such asadditionalProperties: trueare still copied through untouched.User Impact
A keyword a model rejects is now removed wherever it appears in the schema, not only in the five containers previously covered, so tool calls that used to be refused by the provider now go out clean. Schemas that do not use the additional containers serialize exactly as before.
Evidence
Real runtime proof: what the provider actually receives on the wire
Requested by review — this goes past Vitest output to the bytes leaving the transport.
A local
node:httpserver stands in for the provider endpoint:model.baseUrlpoints athttp://127.0.0.1:<port>/v1, the server parses the real request body, recordstools, and answers with a real SSE stream. Nofetchstub and no mocked client — a real socket throughcreateOpenAICompletionsTransportStreamFn(), which is the stream function the runtime selects forapi: "openai-completions"(transports/provider-transport-stream.ts:85-86). It needs no provider account.The model declares
compat.unsupportedToolSchemaKeywords: ["maxLength", "minLength"], mirroring the live xAI / Venice records. The tool is an ordinary dictionary shape, with the value schema underadditionalProperties:Only
schema-keyword-strip.tsdiffers between the two runs.additionalPropertiesas received by the providermain{"maxLength":100,"type":"string"}— banned keyword present{"type":"string"}— strippedTerminal output, unfixed source first:
and with the patch applied:
The script exits on a boolean computed from the captured payload rather than printing a fixed "PASS", so a regression cannot pass silently.
Harness (run with
tsx, not committed)Regression tests
packages/ai/src/providers/schema-keyword-strip.test.tscovers three containers that were silently skipped:additionalProperties,prefixItems, andpatternProperties. All three fail against the unfixed source, each leaving the unsupported keyword in the output:The whole provider suite stays green, which covers the tool-schema projection paths that consume this helper:
oxfmt --checkis clean on both touched files. The exported surface is unchanged, so the public API assertion inpackages/ai/src/package.e2e.test.tsstill holds. The branch is rebased on currentmain.AI-assisted: written and verified with an AI coding agent; the runtime proof above, the failing-test-first measurement, and the regression run were reviewed by me.