BYOK: client always sends enable_thinking, so strict upstreams (NVIDIA NIM) reject every request with 400

Issue Description

When a conversation runs on a custom (BYOK) OpenAI-compatible provider, Qoder unconditionally injects the non-standard top-level parameter enable_thinking into the chat-completion request body. Upstreams that strictly validate top-level parameters reject the whole request, so BYOK against those providers is completely unusable — every message fails within ~1 second.

Affected provider in my case: NVIDIA NIM (https://integrate.api.nvidia.com/v1). enable_thinking is not part of NVIDIA’s public API surface — it is an internal chat-template field, and NVIDIA’s hosted endpoints expose it as the standard reasoning_effort instead. Any unknown top-level key triggers a hard 400.

This is not an account, quota or network issue: the upstream answers the request in under a second with a parameter-validation error that names the offending key.

Steps to Reproduce

  1. Qoder desktop → add a custom provider: type = openai-compatible, protocol = openai, baseUrl = https://integrate.api.nvidia.com/v1, a valid NVIDIA API key.
  2. Add any model under it, e.g. nvidia/nemotron-3-super-120b-a12b.
  3. Start a new chat, select that model, send any one-line message.
  4. The turn fails after ~1s showing only a generic “系统发生异常 / Something went wrong” card, with no reason surfaced.
  5. To get the real error, open ~/.qoder/logs/sessions/<project>/<sessionId>/segments/*.jsonl and read model.request.attempt_failed.

Already ruled out — this is not fixable from configuration:

  • capabilities.thinking.modes: [] + supportsEffort: false + supportedEffortLevels: []
  • capabilities.thinking.modes: [] + supportsEffort: true + supportedEffortLevels: ["medium","high"]

Both were tested after fully quitting and restarting Qoder (confirmed the new settings.json was picked up; settings.json is the only store for provider config, there is no second cache). Both still produce the identical 400.

Expected Behavior

The client should not inject vendor-private parameters into a BYOK request. Concretely, either:

  1. Emit enable_thinking only when the model explicitly declares thinking support (capabilities.thinking.modes non-empty) — including not emitting it with value false when thinking is off; or
  2. Expose a per-provider omitParams / paramAllowlist field so users can drop parameters their upstream rejects; or
  3. Place such vendor-specific fields inside chat_template_kwargs / extra_body rather than at the top level.

Separately: when the upstream returns a 4xx with an explicit message, that message should be shown in the UI instead of the generic error card. This failure cost me a long debugging session that the error text alone would have resolved in seconds.

Actual Behavior

Every request fails immediately and the UI gives no diagnostic. Real error from the session log:

model.request.attempt_failed  attempt=1 max_attempts=11 status=400
  error_message="Custom provider request failed: 400 Validation: Unsupported parameter(s): `enable_thinking`"
  retryable=false will_retry=false
turn.finished  duration_ms=993
  error="The model service rejected this request. Please check the request and try again."

Cause, traced in @qoder-ai/qoder-agent-sdk → dist/_worker/qoder-worker-runtime.obf.mjs (minified names from build 0.4.3). The custom-openai parameter mapping writes the key whenever a reasoning object exists, including the disabled case:

switch (r.thinking?.type) {
  case "disabled": i.enable_thinking = !1; break;   // key still present, value false
  case "adaptive": t && (i.enable_thinking = !0); break;
  case "enabled":  i.enable_thinking = !0, i[e] = r.thinking.budgetTokens; break;
  default: void 0 !== r.enabled && (i.enable_thinking = r.enabled);
}

The layer above always supplies an effort value, whose default is "none" (visible in the run log as [session.runtime_config] … reasoning_effort=none). "none" is a truthy string, so this branch runs:

C.reasoning_effort = w;
"none" === w ? (C.enable_thinking = !1, delete C.reasoning_budget_tokens)
             : (C.enable_thinking = !0, …);

Result: enable_thinking is in the body on every BYOK request, with false when thinking is off and true when it is on. NVIDIA rejects the key regardless of its value, so no capability declaration can avoid the 400.

Scope: reproducible with every model declared under this provider (16 failures, 0 successes across nvidia/nemotron-3-ultra-550b-a55b, nvidia/nemotron-3-super-120b-a12b, deepseek-ai/deepseek-v4-flash, moonshotai/kimi-k3). vLLM / SGLang style deployments that also keep enable_thinking inside chat_template_kwargs are very likely hit by the same thing.

Workaround in use: a loopback-only Node shim on 127.0.0.1 that deletes enable_thinking / thinking_budget / reasoning_budget_tokens / preserve_thinking and keeps the standard reasoning_effort before forwarding. That is not something end users should have to build, which is why I’m reporting it.

Screenshots / Screen Recordings

The generic error card shown for a 400 that carries an explicit upstream message (attach screenshot here).

Operating System

Windows 11 Pro, build 22000, x64

Current Qoder Version (Menu → About Qoder → Copy)

0.4.3 (stable)

If the exact “Copy” string differs, please prefer the copied value; the running build is confirmed from the crash stack as:

productVersion 0.4.3, commit e921dfbf7e82e00ee61078e57540d2a614f8fa0f,
buildNumber 76166284, electron 43.1.1, win32-x64, buildTime 2026-09-26