Skip to content

[BUG] Reasoning is never sent back to the API for the OpenAI Compatible provider — preserveReasoning cannot be enabled from the UI #1096

Description

@1N4148

Problem (one or two sentences)

For the OpenAI Compatible provider, the assistant's reasoning is captured and displayed, but it is always stripped before the next request, so the model re-derives its reasoning on every turn. The flag that controls this, preserveReasoning, exists and works, but there is no way to turn it on for this provider.

Context (who is affected and when)

Anyone running a reasoning model behind a self-hosted OpenAI-compatible endpoint (llama.cpp / llama-server, Lemonade, vLLM, SGLang, LM Studio via the OpenAI Compatible provider) where the server returns chain-of-thought in the reasoning_content delta field.

This is silent — no error, no warning. The reasoning shows up correctly in the Zoo Code UI, so nothing looks wrong from the outside. The only visible symptom is degraded multi-turn behavior, plus wasted tokens re-thinking the same thing every turn.

Built-in providers are unaffected: preserveReasoning: true is hardcoded for DeepSeek, Z.ai, MiniMax, MiMo, Moonshot, Fireworks and Bedrock, and LiteLLM derives it heuristically in src/api/providers/fetchers/litellm.ts. OpenAI Compatible is the one provider class where the user is expected to supply the model metadata themselves — and this particular field isn't offered.

Reproduction steps

  1. Run any reasoning model on llama-server with reasoning preserved in history (--reasoning-preserve, or the equivalent --chat-template-kwargs preserve_thinking:true for Qwen3 templates). Enable prompt logging on the server.
  2. In Zoo Code, create a profile with API Provider = OpenAI Compatible, pointing at that endpoint. Configure the model under "Model Configuration".
  3. Run any task that takes several turns (e.g. a few tool calls in a row).
  4. Inspect the prompt the server actually receives.

Expected result: historical assistant turns in the prompt carry their reasoning trace.

Actual result: every historical assistant turn carries an empty <think></think> block — the reasoning was dropped client-side before the request was built.

Where it happens

src/core/task/Task.ts, buildCleanConversationHistory() (~line 4627):

const shouldPreserveForApi = this.api.getModel().info.preserveReasoning === true
...
} else {
    // Strip reasoning out - stored for history only, not sent back to API

This gate is intentional and was introduced deliberately (Roo Code PR #9453). The mechanism behind it is complete and works: extractReasoningFromDelta() reads reasoning_content from the stream, prepareAssistantMessage() persists it as a { type: "reasoning", text, summary: [] } content block, and convertToOpenAiMessages() reads that block back via getReasoningBlockText() and emits it as reasoning_content on the outgoing message. The gate is the only thing standing between the two halves.

The reason it never opens for this provider:

  • OpenAiHandler.getModel() (src/api/providers/openai.ts) resolves model info as this.options.openAiCustomModelInfo ?? openAiModelInfoSaneDefaults.
  • openAiModelInfoSaneDefaults (packages/types/src/providers/openai.ts) does not include preserveReasoning.
  • webview-ui/src/components/settings/providers/OpenAICompatible.tsx exposes maxTokens, contextWindow, supportsImages, supportsPromptCache, input/output/cache prices and reasoningEffort — but no control for preserveReasoning.

So the field is unreachable through normal use, even though it is part of modelInfoSchema and persists correctly once set.

Workaround (confirms the diagnosis)

Export settings, add the field manually, re-import:

"openAiCustomModelInfo": {
  "contextWindow": 262144,
  "supportsPromptCache": true,
  "preserveReasoning": true   // <-- add this
}

After the import, the same task produces prompts where every historical assistant turn carries its full reasoning trace instead of an empty <think></think> block. Nothing else changed. The field survives both import validation and subsequent edits in the settings UI (the handlers spread the existing object).

Note the JSON edit only works for profiles where openAiCustomModelInfo already exists — modelInfoSchema requires contextWindow and supportsPromptCache, so creating the object from scratch with just this one field makes the import drop the whole profile.

Suggested fix

Add a preserveReasoning checkbox to the "Model Configuration" section of the OpenAI Compatible settings, alongside the existing supportsPromptCache toggle — same wiring, one more field. Something like "Send reasoning back to the model" with a hint that it's needed for local reasoning models that expect the thinking trace in history.

Defaulting it on for this provider would also be defensible, since a backend that doesn't understand reasoning_content generally ignores the field rather than failing — but a checkbox is the smaller and safer change.

App Version

3.74.0

API Provider

OpenAI Compatible

Model Used

Qwen3.6-35B-A3B (GGUF, served via llama.cpp)

Relevant logs or errors

Server-side prompt log, historical assistant turns, before the workaround:

<|im_start|>assistant
<think>

</think>

I'll check if the documentation matches the code. Let me first gather information...

Same setup after setting preserveReasoning: true — 5 of 5 assistant turns carried real reasoning traces, none empty:

<|im_start|>assistant
<think>
I need to examine the MCP server code to understand what exists in the project, starting with the main entry point file.
</think>
...

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions