Skip to content

fix(xai): normalize nested tool schemas - #124

Merged
lidge-jun merged 2 commits into
lidge-jun:mainfrom
Wibias:codex/fix-xai-nested-tool-schema
Jul 14, 2026
Merged

fix(xai): normalize nested tool schemas#124
lidge-jun merged 2 commits into
lidge-jun:mainfrom
Wibias:codex/fix-xai-nested-tool-schema

Conversation

@Wibias

@Wibias Wibias commented Jul 13, 2026

Copy link
Copy Markdown
Collaborator

Summary

Fixes #122.

xAI/Grok rejects some function-tool parameter schemas after Codex tool_search loads a deferred app or MCP tool. The failing shape is a root oneOf/anyOf whose branches include another root union. xAI requires every root union branch to resolve directly to an object schema.

Root cause

Responses parsing correctly replays tool definitions from historical tool_search_output items into the active tool list. The openai-chat adapter previously forwarded every t.parameters schema unchanged. Once the incompatible schema entered the thread history, every later request resent it and received HTTP 400.

Changes

  • Scope normalization to xAI transports (api.x.ai and the Grok CLI proxy) so other OpenAI-compatible providers retain their original schemas.
  • Recursively expand nested root oneOf/anyOf object branches into direct object branches.
  • Preserve root metadata such as $defs while rebuilding the normalized union.
  • Reject and omit a tool when its root schema contains a non-object branch that cannot be normalized safely, avoiding a poisoned request.
  • Add regression coverage for direct adapter requests and the real tool_search_output history replay path.

Verification

  • bun test --isolate ./tests/xai-transport.test.ts ./tests/adapter-usage.test.ts ./tests/responses-parser.test.ts
  • Result: 54 passed, 0 failed.
  • bun x tsc --noEmit
  • git diff --check

Risk and compatibility

This changes only tool schemas sent to xAI-compatible endpoints. Providers using other base URLs are unchanged. The normalization intentionally does not rewrite allOf, and unsafe non-object union branches are omitted instead of being silently coerced. A remaining limitation is that a tool omitted for an unsafe schema will not be callable for that request; this is preferable to making the entire conversation fail repeatedly with HTTP 400.

Maintainer review focus

Please verify the xAI schema contract for nested root unions and, if possible, retest with Codex Desktop using tool_search followed by an automation-related operation on Grok.

@webmastertorch

Copy link
Copy Markdown

Tested locally per your request (Windows, on top of the installed 2.7.16-equivalent source, verified against the live xAI endpoint through the running proxy, which forwards parameters verbatim — so the probes exercise exactly what this PR would send).

What works

  • bun test tests/xai-transport.test.ts on the PR branch: 14/14 pass.
  • The recursive flattening is correct on the real captured automation_update schema: 6 flat object branches, no nested unions left, $defs preserved at the root.
  • Wire acceptance: posting the PR's normalized output to grok-4.5 returns HTTP 200 — the original tool parameter root must be an object type 400 from [Bug]: xAI/Grok 400 after tool_search loads automation_update's nested oneOf schema #122 is gone.
  • The unsafe-root drop path and the tool_search history replay path both behave as intended (buildTools already defaults parameters to {}, so ordinary and freeform tools are never dropped).

One functional regression: the normalized union tool can no longer emit arguments

normalizeXaiToolParameters emits { ...metadata, type: "object", oneOf: variants }. With type: "object" sitting next to the root oneOf, xAI accepts the request but compiles the argument grammar as a plain empty object (root properties is absent and additionalProperties defaults to false in xAI's strict tool grammar), so the union branches never contribute properties.

Forced-call probe against grok-4.5 (tool_choice: {type:"function", name:"codex_app__automation_update"}, prompt: "view the automation with id auto-123"), reproduced twice:

root schema shape status generated arguments
PR output: { $defs, type: "object", oneOf: [6 object branches] } 200 {} (empty, twice)
Same minus root type: { $defs, oneOf: [6 object branches] } 200 {"id":"auto-123","mode":"view"}
Simple object control 200 {"id":"auto-123","mode":"view"}

This matches the xAI docs: their accepted root-union example is a bare oneOf with object branches and no root type (Function Calling → Parameter Schema).

Suggested change

In the multi-variant branch of normalizeXaiToolParameters, drop the root type:

return { ...metadata, oneOf: variants };

and flip the test assertion accordingly (expect(xaiParameters.type).toBeUndefined() while keeping the all-branches-are-objects assertion). The single-variant and plain-object paths are fine as they are.

With that one-line change this looks like the right fix for #122 to me.

@Wibias

Wibias commented Jul 13, 2026

Copy link
Copy Markdown
Collaborator Author

Addressed review feedback in commit 7e449d3. Multi-variant xAI unions now omit the root ype wrapper, preserving xAI's argument grammar so forced tool calls can emit branch properties instead of {}. Updated regression assertion; focused suite remains 54 passed, with typecheck and diff check clean.

@Wibias
Wibias marked this pull request as ready for review July 13, 2026 19:49
@lidge-jun
lidge-jun merged commit d346be5 into lidge-jun:main Jul 14, 2026
6 checks passed
@lidge-jun

Copy link
Copy Markdown
Owner

Reviewed and merged. The recursive root oneOf/anyOf flattening, scoped to api.x.ai and cli-chat-proxy.grok.com, resolves #122, including the historical tool_search_output replay path where src/responses/parser.ts rebuilds historical tools each turn.

One follow-up remains: when normalization omits an unsafe tool, an explicit tool_choice naming that tool can still be forwarded. The tool list is normalized in src/adapters/openai-chat.ts around lines 180-193, while tool_choice is computed from the original list around lines 196-253. That can produce the same 400 response. A follow-up should reconcile tool_choice with the normalized tool list.

Thanks for the fix.

@Wibias
Wibias deleted the codex/fix-xai-nested-tool-schema branch July 15, 2026 17:59
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.

[Bug]: xAI/Grok 400 after tool_search loads automation_update's nested oneOf schema

3 participants