Skip to content

fix: normalize tool function schemas to always have root type: "object" - #745

Closed
white-source wants to merge 1 commit into
lidge-jun:devfrom
white-source:fix/deepseek-tool-schema-type-null
Closed

fix: normalize tool function schemas to always have root type: "object"#745
white-source wants to merge 1 commit into
lidge-jun:devfrom
white-source:fix/deepseek-tool-schema-type-null

Conversation

@white-source

@white-source white-source commented Jul 30, 2026

Copy link
Copy Markdown

Problem

Some provider APIs (notably DeepSeek) strictly validate JSON Schema in function tool definitions and reject requests where the root type is null or missing with HTTP 400:

Invalid schema for function 'codex_app__automation_update':
schema must be a JSON Schema of 'type: "object"', got 'type: null'.

This occurs because Codex injects tools like codex_app__automation_update without a parameters field. When opencodex forwards the request, the serialized schema has type: null (or no type at all) at its root.

OpenAI and most other providers tolerate this, but stricter validators reject it. The same issue has been reported across multiple proxy projects:

Root Cause

Two forwarding paths in opencodex can pass through a broken tool schema:

  1. Adapter translation path: buildTools() in parser.ts uses (t.parameters ?? {}) — when parameters is null/absent, the resulting object has no root type.
  2. Passthrough path (used by wire_api=responses providers like DeepSeek): createResponsesPassthroughAdapter serializes _rawBody verbatim. The raw body contains the same broken tool schemas directly.

Fix

1. src/responses/parser.tsbuildTools()

Added normalizeParameters() helper that ensures every function tool's parameters is a valid JSON Schema object with type: "object" and a properties key. Covers the adapter translation path (Anthropic, Gemini, Cursor, etc.).

2. src/adapters/openai-responses.ts — passthrough adapter

Added normalizeToolSchemas() function that walks both top-level body.tools and Responses Lite additional_tools[].tools, fixing any function tool whose parameters schema is invalid. Inserted into the sanitization chain at the outermost position so it runs regardless of which strip functions are active.

Both changes are idempotent — already-valid schemas pass through unchanged. Tools that are not type: "function" are not touched.

Testing

  • bun build syntax check on both files
  • bun x tsc --noEmit typecheck passes
  • Unit-tested with edge cases: no parameters, parameters: null, type: null, already-valid schemas, nested additional_tools, non-function tool types
  • Verified live with DeepSeek (deepseek-v4-pro via wire_api=responses) — the 400 error is resolved

Notes

This is a defensive normalization — the output is a valid JSON Schema regardless of what the upstream tool definition provides. The change is minimal (70 lines added across 2 files) and does not alter any existing behavior for already-valid schemas.

Summary by CodeRabbit

  • Bug Fixes
    • Improved compatibility with strict JSON schema validation for function tools.
    • Automatically fills in missing or invalid tool parameter definitions with a valid object schema.
    • Prevents requests from being rejected when tool parameters are missing, null, or incorrectly structured.

Some tool definitions (e.g. Codex `codex_app__automation_update`) arrive
without a `parameters` field or with `parameters: null`. When serialized,
that produces a JSON Schema whose root `type` is `null` (or missing).
Strict validators — such as DeepSeek — reject such schemas with HTTP 400.

Changes in two places to cover both forwarding paths:

1. `src/responses/parser.ts` (`buildTools`): Coerce every function tool's
   `parameters` to a valid object-schema root. Covers the adapter
   translation path (Anthropic, Gemini, Cursor, etc.).

2. `src/adapters/openai-responses.ts` (`createResponsesPassthroughAdapter`):
   Add `normalizeToolSchemas()` that fixes top-level `tools` and Responses
   Lite `additional_tools[].tools` before serialization. Required because the
   passthrough path (e.g. DeepSeek with `wire_api=responses`) forwards
   `_rawBody` verbatim and bypasses the parsed `context.tools`.

Fixes the 400 error: "Invalid schema for function: schema must be a JSON
Schema of 'type: \"object\"', got 'type: null'."
@github-actions github-actions Bot added the bug Something isn't working label Jul 30, 2026
@coderabbitai

coderabbitai Bot commented Jul 30, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 8d59ba27-ebbf-442d-8fc6-4972703802ea

📥 Commits

Reviewing files that changed from the base of the PR and between e449521 and 4386da7.

📒 Files selected for processing (2)
  • src/adapters/openai-responses.ts
  • src/responses/parser.ts

📝 Walkthrough

Walkthrough

Function-tool parameter schemas are normalized to object schemas with object properties during parsing and Responses passthrough request sanitization, including nested additional_tools definitions.

Changes

Function tool schema normalization

Layer / File(s) Summary
Normalize parsed and forwarded function-tool schemas
src/responses/parser.ts, src/adapters/openai-responses.ts
Function-tool parameters now default to an object schema with object properties; passthrough sanitization applies the same normalization to top-level tools and nested additional_tools tools.

Estimated code review effort: 2 (Simple) | ~10 minutes

Possibly related PRs

Suggested labels: bug

Suggested reviewers: ingwannu, lidge-jun, wibias

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly matches the main change: normalizing function tool schemas to ensure an object root type.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Comment @coderabbitai help to get the list of available commands.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 4386da79d3

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread src/responses/parser.ts
name: t.name as string,
description: (t.description as string) ?? "",
parameters: (t.parameters ?? {}) as Record<string, unknown>,
parameters: normalizeParameters(t.parameters),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Add focused tests for schema normalization

This changes the request/tool schema behavior in src/ but the commit does not add any focused regression coverage, so cases like missing/null parameters on top-level tools and Responses Lite additional_tools can regress without a failing test. Please add targeted coverage near the existing responses parser/passthrough tests before merging.

AGENTS.md reference: AGENTS.md:L149-L151

Useful? React with 👍 / 👎.

if (!isPlainObject(body)) return body;

const fixFunctionParams = (tool: unknown): unknown => {
if (!isPlainObject(tool) || tool.type !== "function") return tool;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Normalize namespace-contained function schemas too

When a Responses passthrough request carries MCP tools grouped as {type: "namespace", tools: [{type: "function", parameters: null}]} and the target is not the Spark path that flattens namespaces first, this guard returns the namespace unchanged, so the inner function still reaches strict Responses gateways with the same missing/null root schema that this patch is meant to prevent. Please recurse into namespace tools for both top-level tools and additional_tools containers, or flatten before normalizing.

AGENTS.md reference: src/AGENTS.md:L19-L19

Useful? React with 👍 / 👎.

Comment thread src/responses/parser.ts
// so all downstream adapters pass through cleanly.
const normalizeParameters = (raw: unknown): Record<string, unknown> => {
const p = isObj(raw) ? { ...raw } : {};
if (p.type !== "object") p.type = "object";

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Do not mask schemas xAI is supposed to reject

For routed xAI requests where the client supplies an unsafe function schema such as {type: "string"}, this parser-level coercion rewrites it to an empty object schema before normalizeXaiToolParameters() can reject it, bypassing the existing safeguard that omits unnormalizable tools. That makes the model see and call a tool with an argument contract the client did not declare; limit this normalization to missing/null roots or let provider-specific sanitizers handle non-object schemas.

Useful? React with 👍 / 👎.

@lidge-jun

Copy link
Copy Markdown
Owner

NEEDS-CHANGES — the change looks right, but it ships with no tests.

The only files here are src/adapters/openai-responses.ts and src/responses/parser.ts. AGENTS.md asks for a focused regression next to the existing tests for any behaviour change in src/, and this one has none.

That matters more than usual because the normalization lands on two independent paths. src/responses/parser.ts:140-163 handles parsed tool translation; src/adapters/openai-responses.ts:357-414 handles raw Responses passthrough including nested additional_tools. A regression in one is invisible from the other, and both can fail in either direction — sending an invalid schema upstream, or rewriting a schema that was already fine.

The cases worth pinning:

  • parameters missing, and parameters: null
  • root type missing, null, and set to something other than "object"
  • top-level passthrough tools and nested additional_tools
  • an already-valid object schema passes through untouched
  • non-function tools are left alone
  • running normalization twice produces the same result

Please assert against the final serialized upstream request body rather than a helper's return value. That is what proves both production paths actually apply the normalization, which is the claim the PR is making.

What happens next: add that coverage and push. The change itself reads correctly; it just needs something holding it in place.


CI context (shared across the current review round). dev is now at 62e937614. The baseline breakage that made every required check red is fixed: run 30545575865 has macos-latest, ubuntu-latest, and all three npm-global jobs green. The one remaining red is windows-latest, and it is not a test failure — it is a Bun 1.3.14 runtime panic (heap.zig:deleteMin reached through spawnSync with a timeout, triggered by our Windows ACL hardening path). That is a runtime defect being tracked separately, so please do not treat a red windows-latest as a signal about your change. Rebase onto current dev before your next push so your checks run against the repaired baseline.

@lidge-jun lidge-jun left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Formalizing the disposition so it shows on your dashboard.

Blocking: no regression tests. The only changed files are src/adapters/openai-responses.ts and src/responses/parser.ts, and AGENTS.md asks for focused coverage on any behaviour change in src/.

That matters more than usual here because the normalization lands on two independent paths — parsed tool translation and raw Responses passthrough including nested additional_tools. A regression in one is invisible from the other, and both can fail in either direction: sending an invalid schema, or rewriting a valid one.

Please assert against the final serialized upstream request body rather than a helper return value, so the tests prove both production paths actually apply it. The case list is in my earlier comment.

The change itself reads correctly — it just needs something holding it in place.

lidge-jun added a commit that referenced this pull request Jul 31, 2026
Providers that validate strictly, DeepSeek among them, reject a function
schema whose root has no type. The parser forwarded (t.parameters ?? {})
untouched, so a tool declared without parameters — or with properties
but no root type — went out as an invalid schema.

Both routes are covered. The parser normalizes at build time, and the
raw Responses passthrough normalizes top-level tools and
input[].additional_tools.tools before serializing, because that path
copies _rawBody and never reaches the parser. Fixing only the parser
would have left every passthrough request still broken.

Valid schemas are returned unchanged rather than rebuilt, so existing
property sets cannot be reordered or dropped on the way through.

PR #745 by @white-source proposed the right normalizers but carried no
tests, which is why it could not land as-is. Ablating both normalizers
fails three tests: the parser root-type case and the two serialized
passthrough cases.

Co-authored-by: Mr.Hat <white-source@users.noreply.github.com>
@lidge-jun

Copy link
Copy Markdown
Owner

Landed on dev in d52aa6846, with you credited as co-author.

Your diagnosis and both normalizers were right. What held this back was that the PR carried no regression test, and this repo has shipped tests twice recently that passed against both the fixed and unfixed behavior — so "the tests are green" was not evidence the fix worked.

What landed:

  • src/responses/parser.ts — normalizes at build time, returning already-valid schemas unchanged rather than rebuilding them, so existing property sets cannot be reordered or dropped.
  • src/adapters/openai-responses.ts — the same normalization for top-level tools and input[].additional_tools.tools, because the passthrough copies _rawBody and never reaches the parser. Fixing only the parser would have left every passthrough request still broken.
  • Tests covering: missing parameters, properties without a root type, an already-valid schema staying intact, and both guarantees asserted on the serialized outgoing body rather than the parsed context.

Ablating both normalizers fails three tests, so they are not vacuous.

Closing this PR since the change is on dev. Thanks — the two-path insight was the part that mattered.

@lidge-jun lidge-jun closed this Jul 31, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants