Skip to content

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

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

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