Skip to content

core: flatten reasoningEffort settings for custom OpenAI-compatible providers #48183

Closed

Description

@gevezex

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.

Activity

  1. opencode-agent commented on Sep 9, 2026

    @opencode-agent
    Contributor

    Hey — we reproduced this on beta-19378. The reasoning stream parser is handling reasoning_content correctly; the problem is the settings shape for the native OpenAI-compatible package. It currently requires an unnecessarily nested providerOptions object, and we’re going to flatten/fix that structure.

    For now, this variant works as a workaround:

    {
      "id": "high",
      "settings": {
        "providerOptions": {
          "reasoningEffort": "high"
        }
      }
    }

    With that shape, OpenCode sends reasoning_effort: "high" and persists the streamed reasoning_content as a reasoning part. After the fix, the original settings.reasoningEffort shape should work directly.

  2. changed the title [-]core: reasoning from custom OpenAI-compatible providers is silently dropped despite variant reasoningEffort being sent[/-] [+]core: flatten reasoningEffort settings for custom OpenAI-compatible providers[/+] on Sep 9, 2026
  3. gevezex commented on Sep 9, 2026

    @gevezex
    Author

    Confirmed — the workaround works. After nesting reasoningEffort under settings.providerOptions for each variant (and restarting the service), streamed reasoning_content is now persisted as reasoning parts on assistant messages; before the change the session had 0 reasoning parts across ~500 assistant messages.

    One suggestion alongside the flatten fix: the hardest part of debugging this was that settings.reasoningEffort was silently ignored — the request body was correct, the server streamed reasoning_content, yet nothing surfaced and no warning was logged. A debug-level log (or config validation error) for unrecognized keys under variants[].settings would have shortened this to minutes. Until the flattened shape lands, it may also be worth a note in the variants docs that the @opencode/ai/providers/openai-compatible package currently requires the providerOptions nesting.

  4. neriousy commented on Oct 20, 2026

    @neriousy
    Member

    Thanks for the report! Version 2.0.0 fixed the settings shape for custom OpenAI-compatible providers, so variants can set settings.reasoningEffort directly without nesting it under providerOptions. Update with opencode upgrade or npm i -g @opencode/cli@latest (desktop: update the app). Please reopen or comment if reasoning still disappears on that version or newer.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions