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
- 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.
- In Zoo Code, create a profile with API Provider = OpenAI Compatible, pointing at that endpoint. Configure the model under "Model Configuration".
- Run any task that takes several turns (e.g. a few tool calls in a row).
- 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:
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>
...
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 thereasoning_contentdelta 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: trueis hardcoded for DeepSeek, Z.ai, MiniMax, MiMo, Moonshot, Fireworks and Bedrock, and LiteLLM derives it heuristically insrc/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
llama-serverwith reasoning preserved in history (--reasoning-preserve, or the equivalent--chat-template-kwargs preserve_thinking:truefor Qwen3 templates). Enable prompt logging on the server.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):This gate is intentional and was introduced deliberately (Roo Code PR #9453). The mechanism behind it is complete and works:
extractReasoningFromDelta()readsreasoning_contentfrom the stream,prepareAssistantMessage()persists it as a{ type: "reasoning", text, summary: [] }content block, andconvertToOpenAiMessages()reads that block back viagetReasoningBlockText()and emits it asreasoning_contenton 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 asthis.options.openAiCustomModelInfo ?? openAiModelInfoSaneDefaults.openAiModelInfoSaneDefaults(packages/types/src/providers/openai.ts) does not includepreserveReasoning.webview-ui/src/components/settings/providers/OpenAICompatible.tsxexposesmaxTokens,contextWindow,supportsImages,supportsPromptCache, input/output/cache prices andreasoningEffort— but no control forpreserveReasoning.So the field is unreachable through normal use, even though it is part of
modelInfoSchemaand persists correctly once set.Workaround (confirms the diagnosis)
Export settings, add the field manually, re-import:
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
openAiCustomModelInfoalready exists —modelInfoSchemarequirescontextWindowandsupportsPromptCache, so creating the object from scratch with just this one field makes the import drop the whole profile.Suggested fix
Add a
preserveReasoningcheckbox to the "Model Configuration" section of the OpenAI Compatible settings, alongside the existingsupportsPromptCachetoggle — 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_contentgenerally 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:
Same setup after setting
preserveReasoning: true— 5 of 5 assistant turns carried real reasoning traces, none empty: