Skip to content

Commit 9c67b74

Browse files
praxstackImgBotApp
authored andcommitted
🐛 fix(opencode): preserve thinking block signatures + configurable strategy UI
Root cause fix (from PR anomalyco#14393): - Always pass providerMetadata for reasoning parts (removed differentModel guard) - Always pass callProviderMetadata for tool parts - Fix asymmetric compaction buffer (use maxOutputTokens consistently) Configurable thinking strategy (none/strip/compact): - Settings > General: Thinking Strategy dropdown - Context tab: Always-visible strategy selector - Error card: Retry buttons for thinking block errors - Processor: Auto-compact on thinking error with compact strategy Default 'none' preserves original behavior.
1 parent c918d90 commit 9c67b74

8 files changed

Lines changed: 71 additions & 4655 deletions

File tree

Lines changed: 33 additions & 92 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
1-
# Design: Fix Thinking Block Error (Option D)
1+
# Design: Fix Thinking Block Error
22

33
**Date:** 2026-02-25
4-
**Status:** Approved — Ready to implement
4+
**Status:** Implemented
55

66
## Problem
77
When using Claude models with extended thinking, the API returns `thinking`/`redacted_thinking` blocks. When OpenCode replays these back (on next message or compaction), if they're modified during storage/retrieval, Claude rejects them:
@@ -11,93 +11,34 @@ messages.3.content.1: `thinking` or `redacted_thinking` blocks in the latest ass
1111

1212
Session becomes stuck — even compaction triggers the same error.
1313

14-
## Root Cause
15-
`MessageV2.toModelMessages()` stores reasoning parts as `{type: "reasoning", text: part.text}` but the original API response had `{type: "thinking", thinking: "..."}`. The reconstruction is not byte-identical. Claude's constraint only applies to the LAST assistant message.
16-
17-
## Approach: Strip reasoning from last assistant message (user-controlled)
18-
19-
### Component 1: Backend Strip Logic
20-
**File:** `packages/opencode/src/session/message-v2.ts`
21-
22-
In `toModelMessages()`, add optional `stripLastReasoning` parameter:
23-
```typescript
24-
export function toModelMessages(input: WithParts[], model: Provider.Model, opts?: { stripLastReasoning?: boolean }): ModelMessage[] {
25-
// ... existing code ...
26-
27-
// Before return, if stripLastReasoning:
28-
if (opts?.stripLastReasoning) {
29-
const lastAssistantIdx = result.findLastIndex((msg) => msg.role === "assistant")
30-
if (lastAssistantIdx !== -1) {
31-
result[lastAssistantIdx].parts = result[lastAssistantIdx].parts.filter((p) => p.type !== "reasoning")
32-
if (result[lastAssistantIdx].parts.length === 0 || result[lastAssistantIdx].parts.every((p) => p.type === "step-start")) {
33-
result.splice(lastAssistantIdx, 1)
34-
}
35-
}
36-
}
37-
38-
return convertToModelMessages(...)
39-
}
40-
```
41-
42-
### Component 2: Config Setting
43-
**File:** `packages/opencode/src/config/config.ts`
44-
45-
Add to appearance/compaction config:
46-
```typescript
47-
strip_thinking_on_error: z.boolean().optional().default(false).describe("Automatically strip thinking blocks when API error occurs")
48-
```
49-
50-
### Component 3: Auto-Retry in Processor
51-
**File:** `packages/opencode/src/session/processor.ts`
52-
53-
In the catch block (~line 350), detect the specific error:
54-
```typescript
55-
const isThinkingError = e?.message?.includes("thinking") && e?.message?.includes("cannot be modified")
56-
if (isThinkingError) {
57-
const config = await Config.get()
58-
if (config.strip_thinking_on_error) {
59-
// Auto-retry with stripped thinking
60-
// Set a flag that toModelMessages should strip
61-
continue // retry the loop
62-
}
63-
// Otherwise, throw the error (UI will show "Retry without thinking" button)
64-
}
65-
```
66-
67-
### Component 4: Error Card Button
68-
**File:** `packages/ui/src/components/message-part.tsx`
69-
70-
In the error rendering section (~line 1040), detect thinking error:
71-
```tsx
72-
<Match when={cleaned.includes("thinking") && cleaned.includes("cannot be modified")}>
73-
<Card variant="error">
74-
<div>{cleaned}</div>
75-
<Button onClick={() => retryWithoutThinking()} variant="secondary">
76-
Retry without thinking blocks
77-
</Button>
78-
</Card>
79-
</Match>
80-
```
81-
82-
### Component 5: Settings Toggle
83-
**File:** `packages/app/src/components/settings-general.tsx`
84-
85-
Add toggle in Appearance section:
86-
```
87-
Strip Thinking on Error: [Toggle]
88-
Description: "Automatically retry without thinking blocks when API rejects modified thinking content"
89-
```
90-
91-
## Implementation Order
92-
1. Backend strip logic (message-v2.ts)
93-
2. Config setting (config.ts)
94-
3. Auto-retry logic (processor.ts)
95-
4. Error card button (message-part.tsx)
96-
5. Settings toggle (settings-general.tsx)
97-
98-
## Testing
99-
- Reproduce with Claude Opus in long conversation
100-
- Verify error → button appears
101-
- Click button → retries successfully
102-
- Enable auto-mode → errors auto-recover
103-
- Compaction still works after fix
14+
## Root Cause (verified via PR #14393)
15+
1. **Bug 1:** `toModelMessages()` strips `providerMetadata` (including Bedrock thinking signatures) when `differentModel` is true — which always happens during compaction due to model ID format mismatch.
16+
2. **Bug 2:** Asymmetric compaction buffer (20K vs 32K) causes compaction to trigger too late for some models.
17+
18+
## Solution: Root Fix + Configurable Strategy
19+
20+
### Root Fix (from PR #14393)
21+
- Always pass `providerMetadata` for reasoning parts and `callProviderMetadata` for tool parts (removed `differentModel` guard)
22+
- Symmetric compaction buffer using `maxOutputTokens()` consistently
23+
24+
### Configurable Thinking Strategy
25+
Three options available in Settings and Context tab:
26+
- **"none" (default):** Original behavior — send thinking blocks as-is. With the root fix, signatures are now preserved correctly.
27+
- **"strip":** Proactively remove thinking from last assistant message before sending. Prevents errors but loses thinking context.
28+
- **"compact":** Preserve thinking but auto-compact on error. First message may fail, then auto-recovers.
29+
30+
### Error Recovery UI
31+
- Chat error card shows "Retry (strip thinking)" and "Retry (compact session)" buttons
32+
- Context tab shows error alert with recovery buttons when thinking error detected
33+
34+
## Files Modified
35+
1. `message-v2.ts` — Root fix: always pass providerMetadata/callProviderMetadata + conditional strip logic
36+
2. `compaction.ts` — Root fix: symmetric buffer calculation
37+
3. `config.ts``thinking_strategy: "none" | "strip" | "compact"` config option
38+
4. `prompt.ts` — Reads config, passes stripLastReasoning flag
39+
5. `processor.ts` — Detects thinking errors, auto-compacts with "compact" strategy
40+
6. `session-turn.tsx` — Error card with retry buttons
41+
7. `session-turn.css` — Error button styles
42+
8. `message-timeline.tsx` — Retry handler wiring
43+
9. `settings-general.tsx` — Thinking Strategy dropdown
44+
10. `session-context-tab.tsx` — Always-visible strategy selector + error recovery

0 commit comments

Comments
 (0)