Repository navigation
Description
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.6via 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 structuredreasoningparts
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 optionspackages/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
differentModelonly comparesproviderID/modelID— same model with different variant returnsfalse- So
providerMetadata(containingreasoningOpaque) 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(notreasoning), but text content starts with<thinking> - Previous messages in history have reasoning parts with
metadata.copilot.reasoningOpaquecontaining 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
differentModelworkaround (switching models briefly) confirms the mechanism — it's specifically the presence ofreasoning_opaquein 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.
Activity
github-actions commentedon Mar 18, 2026
This issue might be a duplicate of existing issues. Please check:
lazyalex616 commentedon Mar 18, 2026
Thanks for flagging. This is related to #8185 but covers a different scope:
easoning_opaque mechanism causing Claude to emit as plain text when hinking_budget is absent but
easoning_opaque persists in history. This behavior does not occur with Anthropic's native API (which auto-strips old thinking blocks).
Regarding #16246 — that's about Anthropic subagent thinking signature replay, which is a different provider and a different failure mode (API error vs. plain-text reasoning leakage).
This issue provides concrete session-level evidence and a Copilot-specific root cause that may warrant a targeted fix in
convert-to-openai-compatible-chat-messages.ts.github-actions commentedon Jul 3, 2026
To stay organized issues are automatically closed after 60 days of no activity. If the issue is still relevant please open a new one.