Skip to content

Commit 260afd1

Browse files
authored
Merge pull request #34 from Tarquinen/strip-reasoning
fix fetch wrapping for reasoning anthropic models
2 parents 7034350 + 155901b commit 260afd1

File tree

5 files changed

+111
-4
lines changed

5 files changed

+111
-4
lines changed

.claude/settings.local.json

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
{
2+
"permissions": {
3+
"allow": [
4+
"Bash(cat:*)",
5+
"Bash(for f in ~/.local/share/opencode/storage/part/*/*)",
6+
"Bash(do grep -l \"\"type\"\":\"\"reasoning\"\" $f)",
7+
"Bash(done)",
8+
"WebSearch",
9+
"WebFetch(domain:ai-sdk.dev)",
10+
"Bash(npm run typecheck:*)"
11+
],
12+
"deny": [],
13+
"ask": []
14+
}
15+
}

CLAUDE.md

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
# CLAUDE.md
2+
3+
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
4+
5+
## Build Commands
6+
7+
```bash
8+
npm run build # Clean and compile TypeScript
9+
npm run typecheck # Type check without emitting
10+
npm run dev # Run in OpenCode plugin dev mode
11+
npm run test # Run tests (node --import tsx --test tests/*.test.ts)
12+
```
13+
14+
## Architecture
15+
16+
This is an OpenCode plugin that optimizes token usage by pruning obsolete tool outputs from conversation context. The plugin is non-destructive—pruning state is kept in memory only, with original session data remaining intact.
17+
18+
### Core Components
19+
20+
**index.ts** - Plugin entry point. Registers:
21+
- Global fetch wrapper that intercepts LLM requests and replaces pruned tool outputs with placeholder text
22+
- Event handler for `session.status` idle events triggering automatic pruning
23+
- `chat.params` hook to cache session model info
24+
- `context_pruning` tool for AI-initiated pruning
25+
26+
**lib/janitor.ts** - Orchestrates the two-phase pruning process:
27+
1. Deduplication phase: Fast, zero-cost detection of repeated tool calls (keeps most recent)
28+
2. AI analysis phase: Uses LLM to semantically identify obsolete outputs
29+
30+
**lib/deduplicator.ts** - Implements duplicate detection by creating normalized signatures from tool name + parameters
31+
32+
**lib/model-selector.ts** - Model selection cascade: config model → session model → fallback models (with provider priority order)
33+
34+
**lib/config.ts** - Config loading with precedence: defaults → global (~/.config/opencode/dcp.jsonc) → project (.opencode/dcp.jsonc)
35+
36+
**lib/prompt.ts** - Builds the analysis prompt with minimized message history for LLM evaluation
37+
38+
### Key Concepts
39+
40+
- **Tool call IDs**: Normalized to lowercase for consistent matching
41+
- **Protected tools**: Never pruned (default: task, todowrite, todoread, context_pruning)
42+
- **Batch tool expansion**: When a batch tool is pruned, its child tool calls are also pruned
43+
- **Strategies**: `deduplication` (fast) and `ai-analysis` (thorough), configurable per trigger (`onIdle`, `onTool`)
44+
45+
### State Management
46+
47+
Plugin maintains in-memory state per session:
48+
- `prunedIdsState`: Map of session ID → array of pruned tool call IDs
49+
- `statsState`: Map of session ID → cumulative pruning statistics
50+
- `toolParametersCache`: Cached tool parameters extracted from LLM request bodies
51+
- `modelCache`: Cached provider/model info from chat.params hook

index.ts

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,10 +63,22 @@ const plugin: Plugin = (async (ctx) => {
6363
if (init?.body && typeof init.body === 'string') {
6464
try {
6565
const body = JSON.parse(init.body)
66+
6667
if (body.messages && Array.isArray(body.messages)) {
6768
cacheToolParameters(body.messages)
6869

69-
const toolMessages = body.messages.filter((m: any) => m.role === 'tool')
70+
// Check for tool messages in both formats:
71+
// 1. OpenAI style: role === 'tool'
72+
// 2. Anthropic style: role === 'user' with content containing tool_result
73+
const toolMessages = body.messages.filter((m: any) => {
74+
if (m.role === 'tool') return true
75+
if (m.role === 'user' && Array.isArray(m.content)) {
76+
for (const part of m.content) {
77+
if (part.type === 'tool_result') return true
78+
}
79+
}
80+
return false
81+
})
7082

7183
const allSessions = await ctx.client.session.list()
7284
const allPrunedIds = new Set<string>()
@@ -83,13 +95,34 @@ const plugin: Plugin = (async (ctx) => {
8395
let replacedCount = 0
8496

8597
body.messages = body.messages.map((m: any) => {
98+
// OpenAI style: role === 'tool' with tool_call_id
8699
if (m.role === 'tool' && allPrunedIds.has(m.tool_call_id?.toLowerCase())) {
87100
replacedCount++
88101
return {
89102
...m,
90103
content: '[Output removed to save context - information superseded or no longer needed]'
91104
}
92105
}
106+
107+
// Anthropic style: role === 'user' with content array containing tool_result
108+
if (m.role === 'user' && Array.isArray(m.content)) {
109+
let messageModified = false
110+
const newContent = m.content.map((part: any) => {
111+
if (part.type === 'tool_result' && allPrunedIds.has(part.tool_use_id?.toLowerCase())) {
112+
messageModified = true
113+
replacedCount++
114+
return {
115+
...part,
116+
content: '[Output removed to save context - information superseded or no longer needed]'
117+
}
118+
}
119+
return part
120+
})
121+
if (messageModified) {
122+
return { ...m, content: newContent }
123+
}
124+
}
125+
93126
return m
94127
})
95128

lib/config.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import { parse } from 'jsonc-parser'
55
import { Logger } from './logger'
66
import type { PluginInput } from '@opencode-ai/plugin'
77

8-
export type PruningStrategy = "deduplication" | "ai-analysis"
8+
export type PruningStrategy = "deduplication" | "ai-analysis" | "strip-reasoning"
99

1010
export interface PluginConfig {
1111
enabled: boolean
@@ -34,8 +34,8 @@ const defaultConfig: PluginConfig = {
3434
strictModelSelection: false,
3535
pruning_summary: 'detailed',
3636
strategies: {
37-
onIdle: ['deduplication', 'ai-analysis'],
38-
onTool: ['deduplication', 'ai-analysis']
37+
onIdle: ['deduplication', 'ai-analysis', "strip-reasoning"],
38+
onTool: ['deduplication', 'ai-analysis', "strip-reasoning"]
3939
}
4040
}
4141

lib/janitor.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ export interface SessionStats {
1515
export interface PruningResult {
1616
prunedCount: number
1717
tokensSaved: number
18+
thinkingIds: string[]
1819
deduplicatedIds: string[]
1920
llmPrunedIds: string[]
2021
deduplicationDetails: Map<string, any>
@@ -155,6 +156,12 @@ export class Janitor {
155156
return !metadata || !this.protectedTools.includes(metadata.tool)
156157
}).length
157158

159+
// PHASE 1.5: STRIP-REASONING
160+
let reasoningPrunedIds: string[] = []
161+
162+
if (strategies.includes('strip-reasoning')) {
163+
}
164+
158165
// PHASE 2: LLM ANALYSIS
159166
let llmPrunedIds: string[] = []
160167

@@ -329,6 +336,7 @@ export class Janitor {
329336
return {
330337
prunedCount: finalNewlyPrunedIds.length,
331338
tokensSaved,
339+
thinkingIds: [],
332340
deduplicatedIds,
333341
llmPrunedIds: expandedLlmPrunedIds,
334342
deduplicationDetails,

0 commit comments

Comments
 (0)