Skip to content

Commit 39bbe0b

Browse files
praxstackImgBotApp
authored andcommitted
Revert "šŸ› fix(opencode): preserve thinking block signatures + configurable strategy UI"
This reverts commit 5b6069e.
1 parent 80555c2 commit 39bbe0b

1 file changed

Lines changed: 92 additions & 33 deletions

File tree

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

33
**Date:** 2026-02-25
4-
**Status:** Implemented
4+
**Status:** Approved — Ready to implement
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,34 +11,93 @@ 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 (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
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

0 commit comments

Comments
Ā (0)