Skip to content

fix(ai): tool calls fail when an unsupported schema keyword is nested - #115741

Merged
vincentkoc merged 2 commits into
openclaw:mainfrom
Yigtwxx:fix/schema-keyword-strip-containers
Jul 29, 2026
Merged

fix(ai): tool calls fail when an unsupported schema keyword is nested#115741
vincentkoc merged 2 commits into
openclaw:mainfrom
Yigtwxx:fix/schema-keyword-strip-containers

Conversation

@Yigtwxx

@Yigtwxx Yigtwxx commented Jul 29, 2026

Copy link
Copy Markdown
Contributor

What Problem This Solves

Fixes an issue where users on a model that declares unsupportedToolSchemaKeywords would 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

stripUnsupportedSchemaKeywords recursed 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, definitions

These are not exotic shapes. additionalProperties holding 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, as SCHEMA_MAP_KEYS, SCHEMA_OBJECT_KEYS and SCHEMA_ARRAY_KEYS — and again as the ARRAY_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 as additionalProperties: true are 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:http server stands in for the provider endpoint: model.baseUrl points at http://127.0.0.1:<port>/v1, the server parses the real request body, records tools, and answers with a real SSE stream. No fetch stub and no mocked client — a real socket through createOpenAICompletionsTransportStreamFn(), which is the stream function the runtime selects for api: "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 under additionalProperties:

parameters: {
  type: "object",
  properties: {
    labels: { type: "object", additionalProperties: { type: "string", maxLength: 100 } },
  },
}

Only schema-keyword-strip.ts differs between the two runs.

source additionalProperties as received by the provider
current main {"maxLength":100,"type":"string"} — banned keyword present
this PR {"type":"string"} — stripped

Terminal output, unfixed source first:

=== main (unfixed) ===
=== tools payload the provider actually received ===
[{"type":"function","function":{"name":"store_labels","description":"Store a dictionary of labels",
  "parameters":{"properties":{"labels":{"additionalProperties":{"maxLength":100,"type":"string"},
  "type":"object"}},"type":"object"},"strict":false}}]
unsupported keyword present on the wire: true
RESULT: leaked (provider would 400)

and with the patch applied:

=== HEAD (fixed) ===
=== tools payload the provider actually received ===
[{"type":"function","function":{"name":"store_labels","description":"Store a dictionary of labels",
  "parameters":{"properties":{"labels":{"additionalProperties":{"type":"string"},
  "type":"object"}},"type":"object"},"strict":false}}]
unsupported keyword present on the wire: false
RESULT: stripped

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)
const server = createServer((req, res) => {
  const chunks: Buffer[] = [];
  req.on("data", (c: Buffer) => chunks.push(c));
  req.on("end", () => {
    capturedTools = JSON.parse(Buffer.concat(chunks).toString("utf8")).tools;
    res.writeHead(200, { "content-type": "text/event-stream" });
    // ... real SSE chunks + [DONE]
  });
});
await new Promise<void>((r) => server.listen(0, "127.0.0.1", r));

const model = {
  api: "openai-completions",
  baseUrl: `http://127.0.0.1:${port}/v1`,
  compat: { unsupportedToolSchemaKeywords: ["maxLength", "minLength"] },
  // ...
};

const streamFn = createOpenAICompletionsTransportStreamFn();
await streamFn(model, context, { apiKey: "proof-key" }).result();

const leaked = JSON.stringify(capturedTools).includes("maxLength");
process.exit(leaked ? 1 : 0);

Regression tests

packages/ai/src/providers/schema-keyword-strip.test.ts covers three containers that were silently skipped: additionalProperties, prefixItems, and patternProperties. All three fail against the unfixed source, each leaving the unsupported keyword in the output:

Test Files  1 failed (1)
     Tests  3 failed (3)

The whole provider suite stays green, which covers the tool-schema projection paths that consume this helper:

node scripts/run-vitest.mjs run packages/ai/src/providers/

Test Files  29 passed (29)
     Tests  522 passed (522)

oxfmt --check is clean on both touched files. The exported surface is unchanged, so the public API assertion in packages/ai/src/package.e2e.test.ts still holds. The branch is rebased on current main.

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.

@Yigtwxx
Yigtwxx force-pushed the fix/schema-keyword-strip-containers branch 2 times, most recently from d6ac7ff to 51b7b42 Compare July 29, 2026 09:42
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.
@Yigtwxx
Yigtwxx force-pushed the fix/schema-keyword-strip-containers branch from 51b7b42 to ef945d9 Compare July 29, 2026 12:43
@clawsweeper clawsweeper Bot added rating: 🦪 silver shellfish Thin PR readiness signal; proof, validation, or implementation needs work. status: 📣 needs proof The PR needs real behavior proof before ClawSweeper can clear the contributor ask. P2 Normal backlog priority with limited blast radius. labels Jul 29, 2026
@clawsweeper

clawsweeper Bot commented Jul 29, 2026

Copy link
Copy Markdown
Contributor

Codex review: needs maintainer review before merge. Reviewed July 29, 2026, 10:46 AM ET / 14:46 UTC.

ClawSweeper review

What this changes

Extends 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 readiness

⚠️ Needs maintainer review before merge - 3 items remain

Keep 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 main, scoped policy, callers, tests, and history to make a high-confidence merge or correctness verdict.

Priority: P2
Reviewed head: 839a5bf1911593820049cbb676be1d88664b1c59

Review scores

Measure Result What it means
Overall readiness 🦐 gold shrimp (3/6) The runtime proof is strong, while final patch confidence is limited because the required current-main and dependency-path inspection could not be completed.
Proof confidence 🦞 diamond lobster (5/6) Sufficient (terminal): The PR body includes an after-fix real local HTTP/SSE transport capture that shows the provider-bound tool schema no longer contains the rejected nested keyword.
Patch quality 🦐 gold shrimp (3/6) No actionable review findings were identified.

Verification

Check Result Evidence
Real behavior Verified Sufficient (terminal): The PR body includes an after-fix real local HTTP/SSE transport capture that shows the provider-bound tool schema no longer contains the rejected nested keyword.
Evidence reviewed 4 items Proposed implementation: The PR modifies the schema-keyword cleaner to classify schema-bearing values as maps, single nested schemas, or arrays of nested schemas, including draft-specific containers such as additionalProperties, dependentSchemas, and prefixItems.
Regression coverage: The added focused tests cover previously skipped nested shapes, including additionalProperties, prefixItems, patternProperties, and mixed Draft-07 dependencies values.
After-fix behavior proof: The PR body records a real local HTTP/SSE transport run that captured the outbound tools payload and showed maxLength removed from an additionalProperties child schema; this is stronger than a mock-only assertion.
Findings None None.
Security None None.

How this fits together

Provider 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]
Loading

Before merge

  • Resolve merge risk (P1) - GitHub reports the branch as cleanly mergeable but behind its base; review the actual merge result and pending required checks against the current head before landing.
  • Resolve merge risk (P1) - The required current-main, caller/callee, scoped-policy, and history inspection could not be completed here, so any assertion that the local duplicated container taxonomy is the best long-term owner boundary remains unverified.
  • Complete next step (P2) - No discrete mechanical repair is supported by this review; the remaining work is normal maintainer verification of the current merge result and pending checks.
Agent review details

Security

None.

PR surface

Source +30, Tests +98. Total +128 across 2 files.

View PR surface stats
Area Files Added Removed Net
Source 1 38 8 +30
Tests 1 98 0 +98
Docs 0 0 0 0
Config 0 0 0 0
Generated 0 0 0 0
Other 0 0 0 0
Total 2 136 8 +128

Review metrics

None.

Stored data model

Persistent data-model change detected: unknown-data-model-change: packages/ai/src/providers/schema-keyword-strip.test.ts, unknown-data-model-change: packages/ai/src/providers/schema-keyword-strip.ts. Confirm migration or upgrade compatibility proof before merge.

Merge-risk options

Maintainer options:

  1. Decide the mitigation before merge
    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.
  2. Pause or close
    Do not merge this PR until maintainers decide whether the risk is worth taking.

Technical review

Best 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.

Labels

Label justifications:

  • P2: This is a bounded provider-compatibility bug that can block tool calls for models configured with unsupported schema keywords, without evidence of broad default-path outage.
  • rating: 🦐 gold shrimp: Overall readiness is 🦐 gold shrimp; proof is 🦞 diamond lobster and patch quality is 🦐 gold shrimp.
  • status: 👀 ready for maintainer look: ClawSweeper has no concrete contributor-facing blocker left for this PR. Sufficient (terminal): The PR body includes an after-fix real local HTTP/SSE transport capture that shows the provider-bound tool schema no longer contains the rejected nested keyword.
  • proof: sufficient: Contributor real behavior proof is sufficient. The PR body includes an after-fix real local HTTP/SSE transport capture that shows the provider-bound tool schema no longer contains the rejected nested keyword.

Evidence

What I checked:

  • Proposed implementation: The PR modifies the schema-keyword cleaner to classify schema-bearing values as maps, single nested schemas, or arrays of nested schemas, including draft-specific containers such as additionalProperties, dependentSchemas, and prefixItems. (packages/ai/src/providers/schema-keyword-strip.ts:1, 839a5bf19115)
  • Regression coverage: The added focused tests cover previously skipped nested shapes, including additionalProperties, prefixItems, patternProperties, and mixed Draft-07 dependencies values. (packages/ai/src/providers/schema-keyword-strip.test.ts:1, 839a5bf19115)
  • After-fix behavior proof: The PR body records a real local HTTP/SSE transport run that captured the outbound tools payload and showed maxLength removed from an additionalProperties child schema; this is stronger than a mock-only assertion. (839a5bf19115)
  • Review limitation: The required local read-only commands failed before source, history, scoped policy, and current-main comparison could be inspected; no final merge verdict is supported from the supplied PR context alone.

Likely related people:

  • vincentkoc: Assigned reviewer and author of the current PR head commit that expands nested schema-container coverage. (role: reviewer and recent contributor on this PR surface; confidence: medium; commits: 839a5bf19115; files: packages/ai/src/providers/schema-keyword-strip.ts, packages/ai/src/providers/schema-keyword-strip.test.ts)
  • Yigtwxx: Authored the original traversal change and accompanying regression-test direction; current-main feature ownership was not available to verify. (role: proposed implementation author; confidence: low; commits: ef945d9d6971; files: packages/ai/src/providers/schema-keyword-strip.ts, packages/ai/src/providers/schema-keyword-strip.test.ts)

Rank-up moves

Optional improvements that raise the rating; they are not merge blockers.

  • Review the clean three-way merge result against current main and let the pending required checks finish.
  • Compare the local container sets with the canonical schema walkers before merge to confirm there is no missing or divergent dialect handling.

Rating scale

Score Internal tier Crab rank Meaning
6/6 S 🦀 challenger crab Exceptional readiness
5/6 A 🦞 diamond lobster Very strong readiness
4/6 B 🐚 platinum hermit Good normal PR; ordinary maintainer review
3/6 C 🦐 gold shrimp Useful, but confidence is limited
2/6 D 🦪 silver shellfish Proof or implementation needs work
1/6 F 🧂 unranked krab Not merge-ready
N/A NA 🌊 off-meta tidepool Rating does not apply

Overall follows the weaker of proof and patch quality.
Shiny media proof means a screenshot, video, or linked artifact directly shows the changed behavior. Runtime, network, CSP, and security claims still need visible diagnostics.

Workflow

  • ClawSweeper keeps one durable marker-backed review comment per issue or PR.
  • Re-runs edit this comment so the latest verdict, findings, and automation markers stay together instead of adding duplicate bot comments.
  • A fresh review can be triggered by eligible @clawsweeper re-review comments, exact-item GitHub events, scheduled/background review runs, or manual workflow dispatch.
  • PR/issue authors and users with repository write access can comment @clawsweeper re-review or @clawsweeper re-run on an open PR or issue to request a fresh review only.
  • Maintainers can also comment @clawsweeper review to request a fresh review only.
  • Fresh-review commands do not start repair, autofix, rebase, CI repair, or automerge.
  • Maintainer-only repair and merge flows require explicit commands such as @clawsweeper autofix, @clawsweeper automerge, @clawsweeper fix ci, or @clawsweeper address review.
  • Maintainers can comment @clawsweeper explain to ask for more context, or @clawsweeper stop to stop active automation.

History

Review history (2 earlier review cycles)
  • reviewed 2026-07-29T12:59:49.395Z sha ef945d9 :: needs real behavior proof before merge. :: none
  • reviewed 2026-07-29T13:55:29.807Z sha ef945d9 :: needs maintainer review before merge. :: none

@clawsweeper clawsweeper Bot added proof: sufficient ClawSweeper judged the real behavior proof convincing. rating: 🐚 platinum hermit Good normal PR readiness with ordinary maintainer review expected. status: 👀 ready for maintainer look ClawSweeper has no concrete contributor-facing blocker left for this PR. and removed rating: 🦪 silver shellfish Thin PR readiness signal; proof, validation, or implementation needs work. status: 📣 needs proof The PR needs real behavior proof before ClawSweeper can clear the contributor ask. labels Jul 29, 2026
@vincentkoc vincentkoc self-assigned this Jul 29, 2026
@clawsweeper clawsweeper Bot added rating: 🦐 gold shrimp Decent PR readiness signal, but merge confidence is limited. and removed rating: 🐚 platinum hermit Good normal PR readiness with ordinary maintainer review expected. labels Jul 29, 2026
@vincentkoc
vincentkoc merged commit 6ec3dbd into openclaw:main Jul 29, 2026
108 of 110 checks passed
@vincentkoc

Copy link
Copy Markdown
Member

Merged via squash.

@Yigtwxx
Yigtwxx deleted the fix/schema-keyword-strip-containers branch July 29, 2026 16:42
github-actions Bot pushed a commit to Desicool/openclaw that referenced this pull request Jul 30, 2026
…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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

P2 Normal backlog priority with limited blast radius. proof: sufficient ClawSweeper judged the real behavior proof convincing. rating: 🦐 gold shrimp Decent PR readiness signal, but merge confidence is limited. size: S status: 👀 ready for maintainer look ClawSweeper has no concrete contributor-facing blocker left for this PR.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants