[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
Nobody has claimed this yet.
- 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
- Add a custom provider in Settings with API type
OpenAI Chat Completionspointing to any strict OpenAI-compatible gateway (any proxy that validates the request schema — the issue reproduces with several popular ones). - Add a model that supports reasoning effort levels (any model the gateway exposes with
reasoning_effortsupport). - 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).
- Send any message. Effort toggle On or Off makes no difference.
- Observe
Provider rejected the model request. reason=invalid_request status=400 retryable=falseand 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
thinkingleaking into OpenAI requests - #233 — v3.7.3,
reasoning_effortsent 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:
- Emit
{"reasoning_effort": reasoningLevel}only (do not sendreasoningobject together with it). If areasoningobject is needed for some gateway, make it a separate model-specific rule rather than the default. - Do not hardcode
valuesto["disabled","enabled"]; allow the effort tiers configured on the model to pass through.
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- 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