Summary
When using GitHub Copilot provider + Claude Opus 4.6, toggling thinking OFF via Ctrl+T mid-session does not fully stop reasoning behavior. Claude continues to produce <thinking> blocks as plain text output (not structured reasoning parts) in subsequent messages, even though thinking_budget is correctly omitted from the API request.
This appears to be caused by reasoning_opaque metadata from earlier thinking-enabled messages being sent back in the conversation history, which signals to the Copilot API / Claude that reasoning context exists — even when the current request has no thinking_budget.
Environment
- OpenCode version: v1.2.16
- Provider:
github-copilot
- Model:
claude-opus-4.6
- OS: Windows 11 (pwsh 7)
Steps to Reproduce
- Start a new session with
claude-opus-4.6 via GitHub Copilot provider
- Press Ctrl+T to enable thinking — model produces structured reasoning (works correctly)
- Send a few messages with thinking ON
- Press Ctrl+T again to disable thinking (variant cycles back to
undefined)
- Send a new message
- Observe: Claude still produces reasoning — but now as literal
<thinking>...</thinking> in the text output, not as structured reasoning parts
Expected Behavior
After toggling thinking OFF, Claude should produce normal text output without any reasoning/thinking content.
Actual Behavior
Claude outputs <thinking> tags as plain text content. Example from session data:
Part type: "text" (NOT "reasoning")
Part text: "<thinking>\nThe Taobao seller sent completely irrelevant instructions..."
Message variant: "" (empty — thinking is OFF)
Message model: claude-opus-4.6
Workarounds That Confirm the Cause
| Workaround |
Why it works |
| Start a new session |
No reasoning history — no reasoning_opaque in messages |
| Switch to GPT-5.4 for one message, then switch back |
differentModel becomes true, which strips providerMetadata (including reasoningOpaque) from history. When switching back to Opus, the reasoning chain is broken. |
Root Cause Analysis
1. thinking_budget is correctly removed ✅
When variant is toggled OFF, thinking_budget is properly absent from the API request body. Verified in:
packages/opencode/src/session/llm.ts (~L75-77): variant=undefined → merges {} → no thinking options
packages/opencode/src/provider/sdk/copilot/chat/openai-compatible-chat-language-model.ts (L186): thinking_budget: compatibleOptions.thinking_budget — undefined is serialized away
2. But reasoning_opaque persists in message history ⚠️
In packages/opencode/src/session/message-v2.ts (~L440-445):
if (part.type === "reasoning") {
assistantMessage.parts.push({
type: "reasoning",
text: part.text,
...(differentModel ? {} : { providerMetadata: part.metadata }),
})
}
- Reasoning parts are unconditionally included in the message history
differentModel only compares providerID/modelID — same model with different variant returns false
- So
providerMetadata (containing reasoningOpaque) is preserved
3. reasoning_opaque gates reasoning_text in the Copilot request
In packages/opencode/src/provider/sdk/copilot/chat/convert-to-openai-compatible-chat-messages.ts (L120-121):
reasoning_text: reasoningOpaque ? reasoningText : undefined,
reasoning_opaque: reasoningOpaque,
Since history messages DO contain reasoningOpaque, both reasoning_text and reasoning_opaque are sent to the Copilot API for previous assistant messages.
4. The API request becomes contradictory
The final API request contains:
- Current request: No
thinking_budget (thinking is OFF)
- History messages: Contain
reasoning_text + reasoning_opaque (from when thinking was ON)
For Anthropic's native API, this is documented to be safe — they auto-strip old thinking blocks when a new user message appears. But the Copilot API is a proxy layer that uses its own reasoning_opaque mechanism, and it apparently does NOT auto-strip reasoning context when thinking_budget is absent. Instead, Claude sees the reasoning context and continues to reason, outputting it as plain <thinking> text since the structured reasoning channel is not available.
Evidence from Session Data
Analyzed SQLite session database for a long-running session with multiple thinking ON/OFF transitions:
- 5 thinking ON→OFF transitions found in the session
- Transition 4 is the clear evidence:
- Assistant message with variant="" (empty — thinking is OFF), model=claude-opus-4.6
- Part type is
text (not reasoning), but text content starts with <thinking>
- Previous messages in history have reasoning parts with
metadata.copilot.reasoningOpaque containing long encrypted strings
Related Issues
| Issue |
Relevance |
| #8185 |
Discusses whether reasoning blocks should be sent back in history (OPEN, assigned to thdxr) |
| #3035 |
Proposed stripping reasoning traces on model switch (CLOSED) |
| #5577 |
DeepSeek reasoning_content not cleared from history (CLOSED — maintainer said API handles it) |
| #6418 |
Invalid signature in thinking block on model switch |
| #11571 |
Error switching thinking→non-thinking model (CLOSED, fixed) |
Suggested Enhancement
The current design decision to keep reasoning in history is reasonable for native APIs that auto-filter old thinking blocks. But for the Copilot provider specifically, when the current request has no thinking_budget, the reasoning_opaque metadata in history messages appears to cause unintended behavior.
Possible approaches:
Option A (minimal, Copilot-specific): In convert-to-openai-compatible-chat-messages.ts, accept a flag indicating whether thinking is currently enabled. If not, skip reasoning_opaque and reasoning_text from history messages.
Option B (broader): Extend the differentModel logic in message-v2.ts (or add a parallel differentVariant check) to strip providerMetadata when the variant changes from "thinking" to undefined, even when the model name is the same.
Option C (provider-level capability): Add a capability flag like stripReasoningWhenDisabled that providers can opt into, and have the Copilot provider set it.
Notes
- This may ultimately be a Copilot API limitation rather than an OpenCode issue — the Copilot proxy layer doesn't necessarily follow Anthropic's native reasoning history behavior.
- The
differentModel workaround (switching models briefly) confirms the mechanism — it's specifically the presence of reasoning_opaque in history that triggers continued reasoning.
- This is a narrow edge case: Copilot + Claude + thinking toggle mid-session. Most users either keep thinking on or start new sessions, which explains why it hasn't been widely reported.
Summary
When using GitHub Copilot provider + Claude Opus 4.6, toggling thinking OFF via Ctrl+T mid-session does not fully stop reasoning behavior. Claude continues to produce
<thinking>blocks as plain text output (not structured reasoning parts) in subsequent messages, even thoughthinking_budgetis correctly omitted from the API request.This appears to be caused by
reasoning_opaquemetadata from earlier thinking-enabled messages being sent back in the conversation history, which signals to the Copilot API / Claude that reasoning context exists — even when the current request has nothinking_budget.Environment
github-copilotclaude-opus-4.6Steps to Reproduce
claude-opus-4.6via GitHub Copilot providerundefined)<thinking>...</thinking>in the text output, not as structuredreasoningpartsExpected Behavior
After toggling thinking OFF, Claude should produce normal text output without any reasoning/thinking content.
Actual Behavior
Claude outputs
<thinking>tags as plain text content. Example from session data:Workarounds That Confirm the Cause
reasoning_opaquein messagesdifferentModelbecomestrue, which stripsproviderMetadata(includingreasoningOpaque) from history. When switching back to Opus, the reasoning chain is broken.Root Cause Analysis
1.
thinking_budgetis correctly removed ✅When variant is toggled OFF,
thinking_budgetis properly absent from the API request body. Verified in:packages/opencode/src/session/llm.ts(~L75-77): variant=undefined → merges{}→ no thinking optionspackages/opencode/src/provider/sdk/copilot/chat/openai-compatible-chat-language-model.ts(L186):thinking_budget: compatibleOptions.thinking_budget— undefined is serialized away2. But⚠️
reasoning_opaquepersists in message historyIn
packages/opencode/src/session/message-v2.ts(~L440-445):differentModelonly comparesproviderID/modelID— same model with different variant returnsfalseproviderMetadata(containingreasoningOpaque) is preserved3.
reasoning_opaquegatesreasoning_textin the Copilot requestIn
packages/opencode/src/provider/sdk/copilot/chat/convert-to-openai-compatible-chat-messages.ts(L120-121):Since history messages DO contain
reasoningOpaque, bothreasoning_textandreasoning_opaqueare sent to the Copilot API for previous assistant messages.4. The API request becomes contradictory
The final API request contains:
thinking_budget(thinking is OFF)reasoning_text+reasoning_opaque(from when thinking was ON)For Anthropic's native API, this is documented to be safe — they auto-strip old thinking blocks when a new user message appears. But the Copilot API is a proxy layer that uses its own
reasoning_opaquemechanism, and it apparently does NOT auto-strip reasoning context whenthinking_budgetis absent. Instead, Claude sees the reasoning context and continues to reason, outputting it as plain<thinking>text since the structured reasoning channel is not available.Evidence from Session Data
Analyzed SQLite session database for a long-running session with multiple thinking ON/OFF transitions:
text(notreasoning), but text content starts with<thinking>metadata.copilot.reasoningOpaquecontaining long encrypted stringsRelated Issues
Suggested Enhancement
The current design decision to keep reasoning in history is reasonable for native APIs that auto-filter old thinking blocks. But for the Copilot provider specifically, when the current request has no
thinking_budget, thereasoning_opaquemetadata in history messages appears to cause unintended behavior.Possible approaches:
Option A (minimal, Copilot-specific): In
convert-to-openai-compatible-chat-messages.ts, accept a flag indicating whether thinking is currently enabled. If not, skipreasoning_opaqueandreasoning_textfrom history messages.Option B (broader): Extend the
differentModellogic inmessage-v2.ts(or add a paralleldifferentVariantcheck) to stripproviderMetadatawhen the variant changes from"thinking"toundefined, even when the model name is the same.Option C (provider-level capability): Add a capability flag like
stripReasoningWhenDisabledthat providers can opt into, and have the Copilot provider set it.Notes
differentModelworkaround (switching models briefly) confirms the mechanism — it's specifically the presence ofreasoning_opaquein history that triggers continued reasoning.