anomalyco / anomalyco/opencode

core: flatten reasoningEffort settings for custom OpenAI-compatible providers

Open
#48,183 2 comments 0 reactions 1 assignee View on GitHub

@nexxeln is already working on this.

Since Sep 9, 2026.

2.0
Dominant language
TypeScript
Stars
209k
Forks
27.5k
PR merge metrics
PR metrics pending

Description

Summary

With a custom OpenAI-compatible provider (local mlx-serve) and a user-defined variants entry that sets reasoningEffort, the variant is selected and sent (variant:"high" appears on every assistant message, reasoning_effort is in the request body), and the server returns reasoning — but no reasoning part ever reaches the session/TUI. Model: compatibility.reasoningField: "reasoning_content" from opencode.json appears not to be applied to the model definition, so reasoning streamed by the provider is silently dropped.

Environment

  • opencode version: 0.0.0-beta-19378 (opencode2 --version)
  • OS: Darwin 25.6.0 (macOS, arm64)
  • Terminal: iTerm.app (TERM=xterm-256color, COLORTERM=truecolor)
  • Shell: /bin/zsh
  • Install/channel: beta (@opencode/cli npm bundle, opencode2)
  • Active plugins: none found in config
  • Provider: custom mlx-serve (package: "@opencode/ai/providers/openai-compatible", baseURL http://127.0.0.1:11234/v1), model Qwen reasoning hybrid model, server exposes capabilities: ["chat","tool_use","streaming","vision","reasoning","json_schema"]

Reproduction

  1. Configure a custom OpenAI-compatible provider in ~/.config/opencode/opencode.json with a model that defines compatibility: { "reasoningField": "reasoning_content" } and variants: [{ "id": "high", "settings": { "reasoningEffort": "high" } }].
  2. Select the model in the TUI and set the variant to high (Ctrl+T).
  3. Ask a question that triggers reasoning, e.g. a simple multi-step arithmetic word problem.
  4. Observe: no thinking block is rendered in the TUI.
  5. Inspect the session DB (session_message table): every assistant message has model.variant: "high" but contains only text/tool content parts — zero reasoning parts (0 occurrences across 538 assistant messages in the session).
  6. Directly against the same server with the equivalent request ({"reasoning_effort":"high","stream":true}): SSE deltas contain delta.reasoning_content (≈500 chars streamed), and non-streaming responses return message.reasoning_content with usage.completion_tokens_details.reasoning_tokens populated (203/293/223 for low/medium/high; 0 for none/absent).

Expected Behavior

When the variant is applied, the request sends reasoning_effort, and the server streams reasoning_content deltas, OpenCode should render/emit a reasoning part on the assistant message (as it does for built-in providers).

Actual Behavior

Reasoning output is silently dropped: no thinking appears in the TUI and no reasoning parts are stored in the session. No error is logged.

Additional Context

Config snippet (redacted, structurally verbatim):

{
  "model": "mlx-serve/Qwen3.8-Flash-Next-Uncensored-MLX-Serve-4bit",
  "providers": {
    "mlx-serve": {
      "package": "@opencode/ai/providers/openai-compatible",
      "settings": { "baseURL": "http://127.0.0.1:11234/v1" },
      "models": {
        "Qwen3.8-Flash-Next-Uncensored-MLX-Serve-4bit": {
          "compatibility": { "reasoningField": "reasoning_content" },
          "variants": [
            { "id": "none",   "settings": { "reasoningEffort": "none" } },
            { "id": "low",    "settings": { "reasoningEffort": "low" } },
            { "id": "medium", "settings": { "reasoningEffort": "medium" } },
            { "id": "high",   "settings": { "reasoningEffort": "high" } }
          ]
        }
      }
    }
  }
}

Investigation notes from the bundle (opencode2.exe strings, openai-chat protocol): the stream step reads reasoning via a helper that checks a set of fields including the configured reasoningField ({[key]: val} — an object literal, so the value reasoning_content is the checked key, undefined is never in the set, and the loop takes the first match). The non-streaming path likewise falls back to message.reasoning_content ?? message.reasoning. That implies dropping compatibility from the config would not break the reasoning_content case, so something else in the beta-19378 pipeline loses either model.compatibility from user config or the reasoning events between provider stream and session storage — I could not pinpoint the exact line statically.

Related issues (same family, not identical): #35283 (delta schema strips unknown reasoning fields — but that is about the field name reasoning, not user config), #41294/#20815 (variant reasoningEffort dropped from request body — closed; here the request body is correct), #42876 (variant body-level fields dropped before the request).

Frequency: consistent, every message, across all sessions using this custom provider.

Workarounds tried: explicitly setting compatibility.reasoningField: "reasoning_content" (no effect observed); toggling variant via Ctrl+T (variant metadata confirms high on every message). Untested lead: the model card advertises "capabilities": { "tools": true, "input": [...], "output": [...] } without a reasoning capability — the built-in catalog's variant generator (DQ) keys off capabilities.supports.reasoning_effort, so it is plausible that user-config models with variants are handled by a different code path than catalog models.

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.

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.