Skip to content

Copilot Claude: reasoning persists as plain-text <thinking> after Ctrl+T toggles thinking OFF mid-session #18099

@doomsday616

Description

@doomsday616

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

  1. Start a new session with claude-opus-4.6 via GitHub Copilot provider
  2. Press Ctrl+T to enable thinking — model produces structured reasoning (works correctly)
  3. Send a few messages with thinking ON
  4. Press Ctrl+T again to disable thinking (variant cycles back to undefined)
  5. Send a new message
  6. 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.

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type
No fields configured for issues without a type.

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions