zai-org / zai-org/feedback

[Bug] v3.12.3: openai-chat-completions default rule sends both "reasoning" (object) and "reasoning_effort" (string) simultaneously → 400 Invalid request payload on custom OpenAI-compatible providers

Open Beginner friendly
#703 1 comment 1 reaction 0 assignees View on GitHub

Nobody has claimed this yet.

priority: P2
Dominant language
No language data
Stars
22
Forks
1
PR merge metrics
No merged PRs in 30d

Description

Pre-submission checklist

  • Searched existing issues (closest: #629, #233, #607 — different root causes)
  • Read CONTRIBUTING.md

Category

Model config

Agent framework

ZCode Agent

Severity

Blocking — every reasoning-capable model on a custom OpenAI-compatible provider fails with 400 on every turn, regardless of the effort toggle

Reproducibility

Always

Description

After updating to v3.12.3, every request to a custom openai-compatible provider fails with 400 Invalid request payload as soon as the model has a reasoning/thought level (even when the level is set to Off, since v3.12.3 still sends "none" values).

Root cause (verified by replaying the exact request body captured in ~/.zcode/cli/rollout/model-io-<session>.jsonl):

The new rule engine in Resources/config/provider/zcode-builtin.json contains a generic fallback rule for apiTypeMatch: "openai-chat-completions", modelMatch: ".*" whose optionSpecs.reasoningLevel.map emits four fields at once:

{
  "thinking": { "type": "enabled" },
  "enable_thinking": true,
  "reasoning_effort": "high",
  "reasoning": { "effort": "high" }
}

Sending reasoning_effort (the standard OpenAI field) together with reasoning (a non-standard object) in the same payload violates the request schema of strict OpenAI-compatible gateways, which reject it with HTTP 400. Isolation testing:

Payload variant Result
Full v3.12.3 body (4 fields) 400 Invalid request payload
Same body, 4 reasoning fields removed 200 OK
thinking only / enable_thinking only / reasoning only / reasoning_effort only 200 OK each
thinking + enable_thinking 200 OK
reasoning + reasoning_effort together 400
Off state (thinking.type:"disabled", enable_thinking:false, reasoning_effort:"none", reasoning.effort:"none") 400 (still both fields present)

So the conflict is specifically the pair reasoning (object) + reasoning_effort (string). Tools count (221) and message count (327) were unchanged across all variants and are not a factor.

Note that ZCode's own model-specific rules for officially supported OpenAI-compatible models (qwen / deepseek / kimi / gpt-5.4 etc.) already emit only {"reasoning_effort": reasoningLevel} — the generic fallback is the only rule that sends both.

Secondary effect of the same fallback rule: reasoningLevel.values is hardcoded to ["disabled","enabled"], so the effort menu for every custom OpenAI-compatible model collapses to a binary On/Off toggle (On is mapped to "high"). Models that previously exposed low/medium/high/xhigh etc. lose those choices.

Steps to reproduce

  1. Add a custom provider in Settings with API type OpenAI Chat Completions pointing to any strict OpenAI-compatible gateway (any proxy that validates the request schema — the issue reproduces with several popular ones).
  2. Add a model that supports reasoning effort levels (any model the gateway exposes with reasoning_effort support).
  3. Update ZCode to v3.12.3 (issue does not occur on v3.12.2: same session, same model, requests before the update succeed with no reasoning fields; the first request after the app relaunch carries the 4 fields and fails).
  4. Send any message. Effort toggle On or Off makes no difference.
  5. Observe Provider rejected the model request. reason=invalid_request status=400 retryable=false and the turn aborts.

To confirm the payload, open ~/.zcode/cli/rollout/model-io-<sessionId>.jsonl, take .request.body of the failed entry and check that thinking, enable_thinking, reasoning_effort and reasoning are all present.

Expected behavior

For openai-chat-completions providers, effort should be sent using the standard OpenAI field only:

{ "reasoning_effort": "high" }

and reasoning (object) should not be sent alongside it. Effort level choices should not be flattened to a binary toggle for models that support multiple tiers.

Actual behavior

All four fields are sent simultaneously. Strict OpenAI-compatible gateways reject the request with 400 Invalid request payload and the turn fails immediately (retryable=false). The effort menu shows only On/Off.

ZCode version

v3.12.3 (macOS, auto-updated 2026-09-17 17:43 JST via ShipIt; last successful request on previous version 17:38, first failure 17:47 — same session, same model)

Device / OS / Browser

MacBook (Apple Silicon) / macOS 27.0.0 (darwin arm64) / ZCode desktop app

Screenshots / Recordings / Logs

Core log entry (~/.zcode/cli/log/zcode-2026-09-17.jsonl):

{"event":"model.request.failed","context":{"providerKind":"openai-compatible","transport":"sse","reason":"invalid_request","retryable":false,"statusCode":400,"statusMessage":"Provider rejected the model request.","maxAttempts":11,"attempt":1}}

Gateway response body:

{"status_code":400,"error":{"message":"Invalid request payload"}}

Request body top-level keys captured in rollout (failed request):

model, messages, tools, tool_choice, stream, stream_options,
thinking, enable_thinking, reasoning_effort, reasoning, max_completion_tokens

with values thinking:{"type":"enabled"}, enable_thinking:true, reasoning_effort:"high", reasoning:{"effort":"high"}.

Workaround that confirms the diagnosis

Adding a per-model rule in ~/.zcode/v2/provider_config.json under config.modelConfigRules.providerModelRules[<providerId>/<modelId>].config:

"optionSpecs": {
  "reasoningLevel": {
    "values": ["low", "high"],
    "map": "{\"reasoning_effort\": reasoningLevel}"
  }
}

makes ZCode send reasoning_effort only. After restart: 200 OK on every request, and the effort menu shows the listed values again. (I understand this file is not a documented interface; sharing it only as evidence that the conflicting map is the cause.)

Related issues (different root cause but same problem area)
  • #629 — v3.12.1, 400 caused by Anthropic-style thinking leaking into OpenAI requests
  • #233 — v3.7.3, reasoning_effort sent to non-reasoning models
  • #607 — v3.11.2, thought level collapsed to a binary toggle for deepseek due to model-name regex mismatch
Suggested fix

In the openai-chat-completions / .* fallback rule of zcode-builtin.json:

  1. Emit {"reasoning_effort": reasoningLevel} only (do not send reasoning object together with it). If a reasoning object is needed for some gateway, make it a separate model-specific rule rather than the default.
  2. Do not hardcode values to ["disabled","enabled"]; allow the effort tiers configured on the model to pass through.

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start with the generic openai-chat-completions fallback rule in Resources/config/provider/zcode-builtin.json and compare its reasoningLevel mapping with the model-specific rules described in the issue. Reproduce the request using the captured .request.body in ~/.zcode/cli/rollout/model-io-.jsonl or the listed log entry. Done means strict OpenAI-compatible requests succeed with only reasoning_effort and the available effort tiers are preserved.

Written by the indexing model from the issue text.

Assessment

Domain
api
Issue type
Bug
Difficulty
2/5
Estimated time
1-3 hours
Activity status
Active
Clarity
Clearly specified
Newbie friendliness
78/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.