@royalcat/opencode-dcp-rc 0.0.0-stage → 4.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +619 -0
- package/README.md +348 -2
- package/dist/index.d.ts +7 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +8129 -0
- package/dist/index.js.map +1 -0
- package/dist/lib/commands/compression-targets.d.ts +13 -0
- package/dist/lib/commands/compression-targets.d.ts.map +1 -0
- package/dist/lib/commands/context.d.ts +67 -0
- package/dist/lib/commands/context.d.ts.map +1 -0
- package/dist/lib/commands/decompress.d.ts +12 -0
- package/dist/lib/commands/decompress.d.ts.map +1 -0
- package/dist/lib/commands/help.d.ts +18 -0
- package/dist/lib/commands/help.d.ts.map +1 -0
- package/dist/lib/commands/index.d.ts +8 -0
- package/dist/lib/commands/index.d.ts.map +1 -0
- package/dist/lib/commands/manual.d.ts +23 -0
- package/dist/lib/commands/manual.d.ts.map +1 -0
- package/dist/lib/commands/recompress.d.ts +12 -0
- package/dist/lib/commands/recompress.d.ts.map +1 -0
- package/dist/lib/commands/stats.d.ts +27 -0
- package/dist/lib/commands/stats.d.ts.map +1 -0
- package/dist/lib/commands/sweep.d.ts +23 -0
- package/dist/lib/commands/sweep.d.ts.map +1 -0
- package/dist/lib/compress/index.d.ts +3 -0
- package/dist/lib/compress/index.d.ts.map +1 -0
- package/dist/lib/compress/pipeline.d.ts +29 -0
- package/dist/lib/compress/pipeline.d.ts.map +1 -0
- package/dist/lib/compress/protected-content.d.ts +6 -0
- package/dist/lib/compress/protected-content.d.ts.map +1 -0
- package/dist/lib/compress/rc.d.ts +15 -0
- package/dist/lib/compress/rc.d.ts.map +1 -0
- package/dist/lib/compress/search.d.ts +12 -0
- package/dist/lib/compress/search.d.ts.map +1 -0
- package/dist/lib/compress/state.d.ts +10 -0
- package/dist/lib/compress/state.d.ts.map +1 -0
- package/dist/lib/compress/summary.d.ts +68 -0
- package/dist/lib/compress/summary.d.ts.map +1 -0
- package/dist/lib/compress/timing.d.ts +18 -0
- package/dist/lib/compress/timing.d.ts.map +1 -0
- package/dist/lib/compress/types.d.ts +64 -0
- package/dist/lib/compress/types.d.ts.map +1 -0
- package/dist/lib/compress/usage.d.ts +45 -0
- package/dist/lib/compress/usage.d.ts.map +1 -0
- package/dist/lib/compress-permission.d.ts +4 -0
- package/dist/lib/compress-permission.d.ts.map +1 -0
- package/dist/lib/config.d.ts +83 -0
- package/dist/lib/config.d.ts.map +1 -0
- package/dist/lib/hooks.d.ts +22 -0
- package/dist/lib/hooks.d.ts.map +1 -0
- package/dist/lib/logger.d.ts +31 -0
- package/dist/lib/logger.d.ts.map +1 -0
- package/dist/lib/message-ids.d.ts +20 -0
- package/dist/lib/message-ids.d.ts.map +1 -0
- package/dist/lib/messages/index.d.ts +8 -0
- package/dist/lib/messages/index.d.ts.map +1 -0
- package/dist/lib/messages/inject/inject.d.ts +7 -0
- package/dist/lib/messages/inject/inject.d.ts.map +1 -0
- package/dist/lib/messages/inject/subagent-results.d.ts +4 -0
- package/dist/lib/messages/inject/subagent-results.d.ts.map +1 -0
- package/dist/lib/messages/inject/utils.d.ts +23 -0
- package/dist/lib/messages/inject/utils.d.ts.map +1 -0
- package/dist/lib/messages/prune.d.ts +4 -0
- package/dist/lib/messages/prune.d.ts.map +1 -0
- package/dist/lib/messages/query.d.ts +7 -0
- package/dist/lib/messages/query.d.ts.map +1 -0
- package/dist/lib/messages/reasoning-strip.d.ts +8 -0
- package/dist/lib/messages/reasoning-strip.d.ts.map +1 -0
- package/dist/lib/messages/shape.d.ts +5 -0
- package/dist/lib/messages/shape.d.ts.map +1 -0
- package/dist/lib/messages/sync.d.ts +4 -0
- package/dist/lib/messages/sync.d.ts.map +1 -0
- package/dist/lib/messages/utils.d.ts +27 -0
- package/dist/lib/messages/utils.d.ts.map +1 -0
- package/dist/lib/prompts/compress-rc.d.ts +4 -0
- package/dist/lib/prompts/compress-rc.d.ts.map +1 -0
- package/dist/lib/prompts/context-limit-nudge.d.ts +2 -0
- package/dist/lib/prompts/context-limit-nudge.d.ts.map +1 -0
- package/dist/lib/prompts/extensions/nudge.d.ts +4 -0
- package/dist/lib/prompts/extensions/nudge.d.ts.map +1 -0
- package/dist/lib/prompts/extensions/system.d.ts +4 -0
- package/dist/lib/prompts/extensions/system.d.ts.map +1 -0
- package/dist/lib/prompts/extensions/tool.d.ts +3 -0
- package/dist/lib/prompts/extensions/tool.d.ts.map +1 -0
- package/dist/lib/prompts/index.d.ts +4 -0
- package/dist/lib/prompts/index.d.ts.map +1 -0
- package/dist/lib/prompts/iteration-nudge.d.ts +2 -0
- package/dist/lib/prompts/iteration-nudge.d.ts.map +1 -0
- package/dist/lib/prompts/store.d.ts +26 -0
- package/dist/lib/prompts/store.d.ts.map +1 -0
- package/dist/lib/prompts/system.d.ts +4 -0
- package/dist/lib/prompts/system.d.ts.map +1 -0
- package/dist/lib/prompts/turn-nudge.d.ts +2 -0
- package/dist/lib/prompts/turn-nudge.d.ts.map +1 -0
- package/dist/lib/protected-patterns.d.ts +6 -0
- package/dist/lib/protected-patterns.d.ts.map +1 -0
- package/dist/lib/state/index.d.ts +5 -0
- package/dist/lib/state/index.d.ts.map +1 -0
- package/dist/lib/state/persistence.d.ts +47 -0
- package/dist/lib/state/persistence.d.ts.map +1 -0
- package/dist/lib/state/state.d.ts +9 -0
- package/dist/lib/state/state.d.ts.map +1 -0
- package/dist/lib/state/tool-cache.d.ts +13 -0
- package/dist/lib/state/tool-cache.d.ts.map +1 -0
- package/dist/lib/state/types.d.ts +105 -0
- package/dist/lib/state/types.d.ts.map +1 -0
- package/dist/lib/state/utils.d.ts +22 -0
- package/dist/lib/state/utils.d.ts.map +1 -0
- package/dist/lib/strategies/deduplication.d.ts +10 -0
- package/dist/lib/strategies/deduplication.d.ts.map +1 -0
- package/dist/lib/strategies/index.d.ts +3 -0
- package/dist/lib/strategies/index.d.ts.map +1 -0
- package/dist/lib/strategies/purge-errors.d.ts +13 -0
- package/dist/lib/strategies/purge-errors.d.ts.map +1 -0
- package/dist/lib/subagents/subagent-results.d.ts +5 -0
- package/dist/lib/subagents/subagent-results.d.ts.map +1 -0
- package/dist/lib/token-utils.d.ts +19 -0
- package/dist/lib/token-utils.d.ts.map +1 -0
- package/dist/lib/tui/dialogs.d.ts +29 -0
- package/dist/lib/tui/dialogs.d.ts.map +1 -0
- package/dist/lib/tui/format.d.ts +4 -0
- package/dist/lib/tui/format.d.ts.map +1 -0
- package/dist/lib/tui/types.d.ts +18 -0
- package/dist/lib/tui/types.d.ts.map +1 -0
- package/dist/lib/tui/ui.d.ts +48 -0
- package/dist/lib/tui/ui.d.ts.map +1 -0
- package/dist/lib/ui/notification.d.ts +17 -0
- package/dist/lib/ui/notification.d.ts.map +1 -0
- package/dist/lib/ui/utils.d.ts +10 -0
- package/dist/lib/ui/utils.d.ts.map +1 -0
- package/dist/lib/v2/index.d.ts +5 -0
- package/dist/lib/v2/index.d.ts.map +1 -0
- package/dist/lib/v2/messages.d.ts +13 -0
- package/dist/lib/v2/messages.d.ts.map +1 -0
- package/dist/lib/v2/rpc.d.ts +75 -0
- package/dist/lib/v2/rpc.d.ts.map +1 -0
- package/dist/lib/v2/theme.d.ts +55 -0
- package/dist/lib/v2/theme.d.ts.map +1 -0
- package/dist/lib/v2/tui.d.ts +4 -0
- package/dist/lib/v2/tui.d.ts.map +1 -0
- package/dist/lib/v2/usage.d.ts +44 -0
- package/dist/lib/v2/usage.d.ts.map +1 -0
- package/dist/tui.d.ts +7 -0
- package/dist/tui.d.ts.map +1 -0
- package/lib/commands/compression-targets.ts +54 -0
- package/lib/commands/context.ts +305 -0
- package/lib/commands/decompress.ts +273 -0
- package/lib/commands/help.ts +76 -0
- package/lib/commands/index.ts +11 -0
- package/lib/commands/manual.ts +126 -0
- package/lib/commands/recompress.ts +222 -0
- package/lib/commands/stats.ts +180 -0
- package/lib/commands/sweep.ts +266 -0
- package/lib/compress/index.ts +2 -0
- package/lib/compress/pipeline.ts +116 -0
- package/lib/compress/protected-content.ts +163 -0
- package/lib/compress/rc.ts +494 -0
- package/lib/compress/search.ts +277 -0
- package/lib/compress/state.ts +272 -0
- package/lib/compress/summary.ts +320 -0
- package/lib/compress/timing.ts +77 -0
- package/lib/compress/types.ts +70 -0
- package/lib/compress/usage.ts +168 -0
- package/lib/compress-permission.ts +9 -0
- package/lib/config.ts +995 -0
- package/lib/hooks.ts +205 -0
- package/lib/logger.ts +226 -0
- package/lib/message-ids.ts +181 -0
- package/lib/messages/index.ts +7 -0
- package/lib/messages/inject/inject.ts +215 -0
- package/lib/messages/inject/subagent-results.ts +84 -0
- package/lib/messages/inject/utils.ts +304 -0
- package/lib/messages/prune.ts +238 -0
- package/lib/messages/query.ts +71 -0
- package/lib/messages/reasoning-strip.ts +40 -0
- package/lib/messages/shape.ts +50 -0
- package/lib/messages/sync.ts +124 -0
- package/lib/messages/utils.ts +190 -0
- package/lib/prompts/compress-rc.ts +25 -0
- package/lib/prompts/context-limit-nudge.ts +18 -0
- package/lib/prompts/extensions/nudge.ts +34 -0
- package/lib/prompts/extensions/system.ts +32 -0
- package/lib/prompts/extensions/tool.ts +21 -0
- package/lib/prompts/index.ts +29 -0
- package/lib/prompts/iteration-nudge.ts +6 -0
- package/lib/prompts/store.ts +452 -0
- package/lib/prompts/system.ts +41 -0
- package/lib/prompts/turn-nudge.ts +10 -0
- package/lib/protected-patterns.ts +159 -0
- package/lib/state/index.ts +4 -0
- package/lib/state/persistence.ts +321 -0
- package/lib/state/state.ts +214 -0
- package/lib/state/tool-cache.ts +99 -0
- package/lib/state/types.ts +117 -0
- package/lib/state/utils.ts +345 -0
- package/lib/strategies/deduplication.ts +125 -0
- package/lib/strategies/index.ts +2 -0
- package/lib/strategies/purge-errors.ts +86 -0
- package/lib/subagents/subagent-results.ts +75 -0
- package/lib/token-utils.ts +164 -0
- package/lib/tui/dialogs.tsx +261 -0
- package/lib/tui/format.ts +25 -0
- package/lib/tui/types.ts +25 -0
- package/lib/tui/ui.tsx +245 -0
- package/lib/ui/notification.ts +347 -0
- package/lib/ui/utils.ts +304 -0
- package/lib/v2/index.ts +448 -0
- package/lib/v2/messages.ts +252 -0
- package/lib/v2/rpc.ts +60 -0
- package/lib/v2/theme.ts +67 -0
- package/lib/v2/tui.tsx +115 -0
- package/lib/v2/usage.ts +423 -0
- package/package.json +91 -5
- package/server.js +1 -0
- package/tui.tsx +7 -0
package/lib/v2/usage.ts
ADDED
|
@@ -0,0 +1,423 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Captures token usage of the hidden compression (summary) requests.
|
|
3
|
+
*
|
|
4
|
+
* OpenCode V2 `session.generate` returns only the reply text, so provider
|
|
5
|
+
* usage is observed in two complementary ways: wrapping the resolved language
|
|
6
|
+
* model through the `aisdk.language` hook (AI SDK packages) and reading the
|
|
7
|
+
* raw response body through the session `http.response` hook (native
|
|
8
|
+
* `@opencode/ai` packages, whose model never passes through the core AISDK
|
|
9
|
+
* service). The session `generate` hook records a local input estimate as a
|
|
10
|
+
* fallback for runtimes where neither capture path is available. All state is
|
|
11
|
+
* kept in memory and keyed by the per-request call id embedded in the hidden
|
|
12
|
+
* prompt marker.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import type { Plugin } from "@opencode/plugin"
|
|
16
|
+
import type { Logger } from "../logger"
|
|
17
|
+
import { countTokens } from "../token-utils"
|
|
18
|
+
import { extractSummaryCallId } from "../compress/summary"
|
|
19
|
+
import {
|
|
20
|
+
estimatedCompressionUsage,
|
|
21
|
+
normalizeProviderUsage,
|
|
22
|
+
type CompressionUsage,
|
|
23
|
+
} from "../compress/usage"
|
|
24
|
+
|
|
25
|
+
interface PendingUsage {
|
|
26
|
+
provider?: CompressionUsage
|
|
27
|
+
estimatedInput?: number
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface CompressionUsageTracker {
|
|
31
|
+
recordProvider(callId: string, usage: CompressionUsage): void
|
|
32
|
+
recordEstimatedInput(callId: string, inputTokens: number): void
|
|
33
|
+
/** Provider usage when captured, otherwise a tokenizer estimate, otherwise undefined. */
|
|
34
|
+
resolve(callId: string, outputText: string): CompressionUsage | undefined
|
|
35
|
+
discard(callId: string): void
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function createCompressionUsageTracker(): CompressionUsageTracker {
|
|
39
|
+
const pending = new Map<string, PendingUsage>()
|
|
40
|
+
|
|
41
|
+
const entry = (callId: string): PendingUsage => {
|
|
42
|
+
let current = pending.get(callId)
|
|
43
|
+
if (!current) {
|
|
44
|
+
current = {}
|
|
45
|
+
pending.set(callId, current)
|
|
46
|
+
}
|
|
47
|
+
return current
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return {
|
|
51
|
+
recordProvider(callId, usage) {
|
|
52
|
+
entry(callId).provider = usage
|
|
53
|
+
},
|
|
54
|
+
recordEstimatedInput(callId, inputTokens) {
|
|
55
|
+
if (Number.isFinite(inputTokens) && inputTokens > 0) {
|
|
56
|
+
entry(callId).estimatedInput = Math.round(inputTokens)
|
|
57
|
+
}
|
|
58
|
+
},
|
|
59
|
+
resolve(callId, outputText) {
|
|
60
|
+
const current = pending.get(callId)
|
|
61
|
+
if (!current) {
|
|
62
|
+
return undefined
|
|
63
|
+
}
|
|
64
|
+
if (current.provider) {
|
|
65
|
+
return current.provider
|
|
66
|
+
}
|
|
67
|
+
if (current.estimatedInput === undefined) {
|
|
68
|
+
return undefined
|
|
69
|
+
}
|
|
70
|
+
return estimatedCompressionUsage(current.estimatedInput, countTokens(outputText))
|
|
71
|
+
},
|
|
72
|
+
discard(callId) {
|
|
73
|
+
pending.delete(callId)
|
|
74
|
+
},
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** Depth-first search of the AI SDK call options for a summary marker. */
|
|
79
|
+
function findSummaryCallId(options: unknown): string | undefined {
|
|
80
|
+
const texts: string[] = []
|
|
81
|
+
collectTexts((options as { prompt?: unknown } | undefined)?.prompt, texts)
|
|
82
|
+
for (const text of texts) {
|
|
83
|
+
const callId = extractSummaryCallId(text)
|
|
84
|
+
if (callId) {
|
|
85
|
+
return callId
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
return undefined
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function collectTexts(value: unknown, out: string[], depth = 0): void {
|
|
92
|
+
if (depth > 8 || out.length > 200) {
|
|
93
|
+
return
|
|
94
|
+
}
|
|
95
|
+
if (typeof value === "string") {
|
|
96
|
+
out.push(value)
|
|
97
|
+
return
|
|
98
|
+
}
|
|
99
|
+
if (Array.isArray(value)) {
|
|
100
|
+
for (const item of value) {
|
|
101
|
+
collectTexts(item, out, depth + 1)
|
|
102
|
+
}
|
|
103
|
+
return
|
|
104
|
+
}
|
|
105
|
+
if (value && typeof value === "object") {
|
|
106
|
+
const record = value as Record<string, unknown>
|
|
107
|
+
if (typeof record.text === "string") {
|
|
108
|
+
out.push(record.text)
|
|
109
|
+
}
|
|
110
|
+
if (typeof record.content === "string") {
|
|
111
|
+
out.push(record.content)
|
|
112
|
+
} else if (Array.isArray(record.content)) {
|
|
113
|
+
collectTexts(record.content, out, depth + 1)
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function wrapLanguageModel(model: any, tracker: CompressionUsageTracker, logger: Logger): any {
|
|
119
|
+
return new Proxy(model, {
|
|
120
|
+
get(target, property, receiver) {
|
|
121
|
+
if (property === "doStream") {
|
|
122
|
+
return (options: unknown) => trackedStream(target, options, tracker, logger)
|
|
123
|
+
}
|
|
124
|
+
if (property === "doGenerate") {
|
|
125
|
+
return (options: unknown) => trackedGenerate(target, options, tracker, logger)
|
|
126
|
+
}
|
|
127
|
+
return Reflect.get(target, property, receiver)
|
|
128
|
+
},
|
|
129
|
+
})
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
async function trackedStream(
|
|
133
|
+
target: any,
|
|
134
|
+
options: unknown,
|
|
135
|
+
tracker: CompressionUsageTracker,
|
|
136
|
+
logger: Logger,
|
|
137
|
+
): Promise<any> {
|
|
138
|
+
const result = await target.doStream(options)
|
|
139
|
+
const callId = findSummaryCallId(options)
|
|
140
|
+
logger.debug("Compression usage stream call", { callId: callId ?? null })
|
|
141
|
+
const TransformStreamImpl = (globalThis as any).TransformStream
|
|
142
|
+
if (!callId || typeof result?.stream?.pipeThrough !== "function") {
|
|
143
|
+
return result
|
|
144
|
+
}
|
|
145
|
+
if (typeof TransformStreamImpl !== "function") {
|
|
146
|
+
return result
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
let recorded = false
|
|
150
|
+
const transform = new TransformStreamImpl({
|
|
151
|
+
transform(part: any, controller: any) {
|
|
152
|
+
try {
|
|
153
|
+
if (!recorded && part?.type === "finish" && part.usage) {
|
|
154
|
+
const usage = normalizeProviderUsage(part.usage)
|
|
155
|
+
if (usage) {
|
|
156
|
+
recorded = true
|
|
157
|
+
tracker.recordProvider(callId, usage)
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
} catch {
|
|
161
|
+
// Never disturb the model stream because of usage accounting.
|
|
162
|
+
}
|
|
163
|
+
controller.enqueue(part)
|
|
164
|
+
},
|
|
165
|
+
})
|
|
166
|
+
return { ...result, stream: result.stream.pipeThrough(transform) }
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
async function trackedGenerate(
|
|
170
|
+
target: any,
|
|
171
|
+
options: unknown,
|
|
172
|
+
tracker: CompressionUsageTracker,
|
|
173
|
+
logger: Logger,
|
|
174
|
+
): Promise<any> {
|
|
175
|
+
const result = await target.doGenerate(options)
|
|
176
|
+
const callId = findSummaryCallId(options)
|
|
177
|
+
logger.debug("Compression usage generate call", { callId: callId ?? null })
|
|
178
|
+
if (!callId) {
|
|
179
|
+
return result
|
|
180
|
+
}
|
|
181
|
+
const usage = normalizeProviderUsage(result?.usage)
|
|
182
|
+
if (usage) {
|
|
183
|
+
tracker.recordProvider(callId, usage)
|
|
184
|
+
}
|
|
185
|
+
return result
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Best-effort registration of the language-model wrapper. Runtimes without the
|
|
190
|
+
* `aisdk` hook simply rely on the local estimate fallback.
|
|
191
|
+
*/
|
|
192
|
+
export async function installCompressionUsageHook(
|
|
193
|
+
ctx: Plugin.Context,
|
|
194
|
+
tracker: CompressionUsageTracker,
|
|
195
|
+
logger: Logger,
|
|
196
|
+
): Promise<void> {
|
|
197
|
+
const aisdk = (ctx as any)?.aisdk
|
|
198
|
+
if (!aisdk || typeof aisdk.hook !== "function") {
|
|
199
|
+
logger.debug("aisdk hook unavailable; compression usage will be estimated")
|
|
200
|
+
return
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
try {
|
|
204
|
+
await aisdk.hook("language", (event: any) => {
|
|
205
|
+
try {
|
|
206
|
+
const isModel = (candidate: unknown) =>
|
|
207
|
+
!!candidate && typeof (candidate as any).doStream === "function"
|
|
208
|
+
logger.debug("Compression usage hook event", {
|
|
209
|
+
modelID: event?.model?.modelID ?? event?.model?.id ?? null,
|
|
210
|
+
hasLanguage: isModel(event?.language),
|
|
211
|
+
hasFactory: typeof event?.sdk?.languageModel === "function",
|
|
212
|
+
})
|
|
213
|
+
// Another hook may already have resolved a model; otherwise fall
|
|
214
|
+
// back to the provider SDK factory the core would use itself.
|
|
215
|
+
let model: unknown = isModel(event?.language) ? event.language : undefined
|
|
216
|
+
if (!model && typeof event?.sdk?.languageModel === "function") {
|
|
217
|
+
model = event.sdk.languageModel(
|
|
218
|
+
event?.model?.modelID ?? event?.model?.id,
|
|
219
|
+
) as unknown
|
|
220
|
+
}
|
|
221
|
+
if (!isModel(model)) {
|
|
222
|
+
return
|
|
223
|
+
}
|
|
224
|
+
event.language = wrapLanguageModel(model, tracker, logger)
|
|
225
|
+
} catch (error: any) {
|
|
226
|
+
logger.debug("Compression usage hook could not wrap language model", {
|
|
227
|
+
error: error?.message,
|
|
228
|
+
})
|
|
229
|
+
}
|
|
230
|
+
})
|
|
231
|
+
logger.debug("Compression usage hook installed")
|
|
232
|
+
} catch (error: any) {
|
|
233
|
+
logger.warn("Compression usage hook registration failed", {
|
|
234
|
+
error: error?.message,
|
|
235
|
+
})
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
interface UsageAccumulator {
|
|
240
|
+
input: number
|
|
241
|
+
output: number
|
|
242
|
+
cacheRead: number
|
|
243
|
+
cacheWrite: number
|
|
244
|
+
reasoning: number
|
|
245
|
+
/** Anthropic reports cache tokens in addition to `input_tokens`. */
|
|
246
|
+
exclusiveInput: boolean
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
function emptyUsageAccumulator(): UsageAccumulator {
|
|
250
|
+
return {
|
|
251
|
+
input: 0,
|
|
252
|
+
output: 0,
|
|
253
|
+
cacheRead: 0,
|
|
254
|
+
cacheWrite: 0,
|
|
255
|
+
reasoning: 0,
|
|
256
|
+
exclusiveInput: false,
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
type UsageField = "input" | "output" | "cacheRead" | "cacheWrite" | "reasoning"
|
|
261
|
+
|
|
262
|
+
function usageField(key: string): UsageField | undefined {
|
|
263
|
+
switch (key) {
|
|
264
|
+
case "input_tokens":
|
|
265
|
+
case "prompt_tokens":
|
|
266
|
+
case "promptTokenCount":
|
|
267
|
+
return "input"
|
|
268
|
+
case "output_tokens":
|
|
269
|
+
case "completion_tokens":
|
|
270
|
+
case "candidatesTokenCount":
|
|
271
|
+
return "output"
|
|
272
|
+
case "cache_read_input_tokens":
|
|
273
|
+
case "cache_creation_input_tokens":
|
|
274
|
+
case "cached_tokens":
|
|
275
|
+
case "cachedContentTokenCount":
|
|
276
|
+
return key === "cache_creation_input_tokens" ? "cacheWrite" : "cacheRead"
|
|
277
|
+
case "reasoning_tokens":
|
|
278
|
+
case "thoughtsTokenCount":
|
|
279
|
+
return "reasoning"
|
|
280
|
+
default:
|
|
281
|
+
return undefined
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
/** Merge every usage-like object found in a provider payload. */
|
|
286
|
+
function scanUsage(value: unknown, acc: UsageAccumulator, depth: number): void {
|
|
287
|
+
if (depth > 8 || !value || typeof value !== "object") {
|
|
288
|
+
return
|
|
289
|
+
}
|
|
290
|
+
if (Array.isArray(value)) {
|
|
291
|
+
for (const item of value) {
|
|
292
|
+
scanUsage(item, acc, depth + 1)
|
|
293
|
+
}
|
|
294
|
+
return
|
|
295
|
+
}
|
|
296
|
+
for (const [key, raw] of Object.entries(value as Record<string, unknown>)) {
|
|
297
|
+
const field = usageField(key)
|
|
298
|
+
if (field && typeof raw === "number" && Number.isFinite(raw)) {
|
|
299
|
+
acc[field] = Math.max(acc[field], raw)
|
|
300
|
+
if (key === "cache_read_input_tokens" || key === "cache_creation_input_tokens") {
|
|
301
|
+
acc.exclusiveInput = true
|
|
302
|
+
}
|
|
303
|
+
continue
|
|
304
|
+
}
|
|
305
|
+
if (raw && typeof raw === "object") {
|
|
306
|
+
scanUsage(raw, acc, depth + 1)
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
function usageFromAccumulator(acc: UsageAccumulator): CompressionUsage | undefined {
|
|
312
|
+
const input = acc.exclusiveInput ? acc.input + acc.cacheRead + acc.cacheWrite : acc.input
|
|
313
|
+
const usage: CompressionUsage = {
|
|
314
|
+
inputTokens: Math.max(0, Math.round(input)),
|
|
315
|
+
outputTokens: Math.max(0, Math.round(acc.output)),
|
|
316
|
+
cacheReadTokens: Math.max(0, Math.round(acc.cacheRead)),
|
|
317
|
+
cacheWriteTokens: Math.max(0, Math.round(acc.cacheWrite)),
|
|
318
|
+
reasoningTokens: Math.max(0, Math.round(acc.reasoning)),
|
|
319
|
+
source: "provider",
|
|
320
|
+
}
|
|
321
|
+
const total =
|
|
322
|
+
usage.inputTokens +
|
|
323
|
+
usage.outputTokens +
|
|
324
|
+
usage.cacheReadTokens +
|
|
325
|
+
usage.cacheWriteTokens +
|
|
326
|
+
usage.reasoningTokens
|
|
327
|
+
return total > 0 ? usage : undefined
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
/**
|
|
331
|
+
* Parse provider usage from a raw model response body. Handles plain JSON
|
|
332
|
+
* bodies and SSE streams from the common provider protocols (OpenAI
|
|
333
|
+
* responses/chat, Anthropic, Google) by merging every usage-like object.
|
|
334
|
+
*/
|
|
335
|
+
export function parseProviderUsageFromBody(body: string): CompressionUsage | undefined {
|
|
336
|
+
const acc = emptyUsageAccumulator()
|
|
337
|
+
const trimmed = body.trim()
|
|
338
|
+
let parsedJson = false
|
|
339
|
+
if (trimmed.startsWith("{") || trimmed.startsWith("[")) {
|
|
340
|
+
try {
|
|
341
|
+
scanUsage(JSON.parse(trimmed), acc, 0)
|
|
342
|
+
parsedJson = true
|
|
343
|
+
} catch {
|
|
344
|
+
// Not a single JSON document; treat the body as an SSE stream.
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
if (!parsedJson) {
|
|
348
|
+
for (const line of body.split(/\r?\n/)) {
|
|
349
|
+
const match = /^\s*data:\s*(.*)$/.exec(line)
|
|
350
|
+
if (!match) {
|
|
351
|
+
continue
|
|
352
|
+
}
|
|
353
|
+
const payload = match[1].trim()
|
|
354
|
+
if (!payload || payload === "[DONE]") {
|
|
355
|
+
continue
|
|
356
|
+
}
|
|
357
|
+
try {
|
|
358
|
+
scanUsage(JSON.parse(payload), acc, 0)
|
|
359
|
+
} catch {
|
|
360
|
+
// Ignore malformed events.
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
return usageFromAccumulator(acc)
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
/**
|
|
368
|
+
* Best-effort capture of provider usage from the raw HTTP response of the
|
|
369
|
+
* hidden summary request. Needed for native `@opencode/ai` provider packages,
|
|
370
|
+
* which bypass the core AISDK service entirely. The hook reads clones of the
|
|
371
|
+
* request/response, so the real traffic is never disturbed, and any failure
|
|
372
|
+
* falls back to the local estimate recorded by the `generate` hook.
|
|
373
|
+
*/
|
|
374
|
+
export async function installHttpUsageHook(
|
|
375
|
+
ctx: Plugin.Context,
|
|
376
|
+
tracker: CompressionUsageTracker,
|
|
377
|
+
logger: Logger,
|
|
378
|
+
): Promise<void> {
|
|
379
|
+
const session = (ctx as any)?.session
|
|
380
|
+
if (!session || typeof session.hook !== "function") {
|
|
381
|
+
return
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
try {
|
|
385
|
+
await session.hook("http.response", async (event: any) => {
|
|
386
|
+
if (event?.kind !== "generate") {
|
|
387
|
+
return
|
|
388
|
+
}
|
|
389
|
+
try {
|
|
390
|
+
const request = event.request
|
|
391
|
+
const response = event.response
|
|
392
|
+
if (!request || typeof request.clone !== "function") {
|
|
393
|
+
return
|
|
394
|
+
}
|
|
395
|
+
const callId = extractSummaryCallId(await request.clone().text())
|
|
396
|
+
if (!callId) {
|
|
397
|
+
return
|
|
398
|
+
}
|
|
399
|
+
if (!response || typeof response.clone !== "function") {
|
|
400
|
+
return
|
|
401
|
+
}
|
|
402
|
+
const usage = parseProviderUsageFromBody(await response.clone().text())
|
|
403
|
+
if (usage) {
|
|
404
|
+
tracker.recordProvider(callId, usage)
|
|
405
|
+
logger.debug("Compression usage captured from http response", {
|
|
406
|
+
callId,
|
|
407
|
+
inputTokens: usage.inputTokens,
|
|
408
|
+
outputTokens: usage.outputTokens,
|
|
409
|
+
})
|
|
410
|
+
}
|
|
411
|
+
} catch (error: any) {
|
|
412
|
+
logger.debug("Compression usage http capture failed", {
|
|
413
|
+
error: error?.message,
|
|
414
|
+
})
|
|
415
|
+
}
|
|
416
|
+
})
|
|
417
|
+
logger.debug("Compression usage http hook installed")
|
|
418
|
+
} catch (error: any) {
|
|
419
|
+
logger.warn("Compression usage http hook registration failed", {
|
|
420
|
+
error: error?.message,
|
|
421
|
+
})
|
|
422
|
+
}
|
|
423
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,92 @@
|
|
|
1
1
|
{
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
2
|
+
"$schema": "https://json.schemastore.org/package.json",
|
|
3
|
+
"name": "@royalcat/opencode-dcp-rc",
|
|
4
|
+
"version": "4.0.1",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"workspaces": [
|
|
7
|
+
"tests/logger"
|
|
8
|
+
],
|
|
9
|
+
"description": "OpenCode DCP fork: hidden context compression with a selection-only compress tool (Sleev-style)",
|
|
10
|
+
"main": "./dist/index.js",
|
|
11
|
+
"types": "./dist/index.d.ts",
|
|
12
|
+
"exports": {
|
|
13
|
+
".": {
|
|
14
|
+
"types": "./dist/index.d.ts",
|
|
15
|
+
"import": "./dist/index.js"
|
|
16
|
+
},
|
|
17
|
+
"./server": {
|
|
18
|
+
"types": "./dist/index.d.ts",
|
|
19
|
+
"import": "./dist/index.js"
|
|
20
|
+
},
|
|
21
|
+
"./tui": {
|
|
22
|
+
"types": "./dist/tui.d.ts",
|
|
23
|
+
"import": "./tui.tsx"
|
|
24
|
+
}
|
|
25
|
+
},
|
|
26
|
+
"scripts": {
|
|
27
|
+
"clean": "rm -rf dist",
|
|
28
|
+
"build": "npm run clean && tsup && tsc --emitDeclarationOnly",
|
|
29
|
+
"verify:package": "node scripts/verify-package.mjs",
|
|
30
|
+
"check:package": "npm run build && npm run verify:package",
|
|
31
|
+
"prepublishOnly": "npm run check:package",
|
|
32
|
+
"dev": "opencode plugin dev",
|
|
33
|
+
"typecheck": "tsc --noEmit && npm run typecheck --workspace=tests/logger",
|
|
34
|
+
"test": "node --import tsx --test tests/*.test.ts && npm test --workspace=tests/logger",
|
|
35
|
+
"format": "prettier --write .",
|
|
36
|
+
"format:check": "prettier --check .",
|
|
37
|
+
"dcp": "tsx scripts/print.ts",
|
|
38
|
+
"lab:rc": "node scripts/lab-rc.mjs"
|
|
39
|
+
},
|
|
40
|
+
"keywords": [
|
|
41
|
+
"opencode",
|
|
42
|
+
"opencode-plugin",
|
|
43
|
+
"plugin",
|
|
44
|
+
"context",
|
|
45
|
+
"pruning",
|
|
46
|
+
"optimization",
|
|
47
|
+
"tokens"
|
|
48
|
+
],
|
|
49
|
+
"repository": {
|
|
50
|
+
"type": "git",
|
|
51
|
+
"url": "git+https://github.com/royalcat/opencode-dcp-rc.git"
|
|
52
|
+
},
|
|
53
|
+
"bugs": {
|
|
54
|
+
"url": "https://github.com/royalcat/opencode-dcp-rc/issues"
|
|
55
|
+
},
|
|
56
|
+
"homepage": "https://github.com/royalcat/opencode-dcp-rc#readme",
|
|
57
|
+
"author": "royalcat",
|
|
58
|
+
"license": "AGPL-3.0-or-later",
|
|
59
|
+
"peerDependencies": {
|
|
60
|
+
"@opencode-ai/plugin": ">=1.18.29"
|
|
61
|
+
},
|
|
62
|
+
"dependencies": {
|
|
63
|
+
"@anthropic-ai/tokenizer": "^0.0.4",
|
|
64
|
+
"@opencode-ai/sdk": "^1.4.3",
|
|
65
|
+
"@opentui/core": "^0.5.14",
|
|
66
|
+
"@opentui/solid": "^0.5.14",
|
|
67
|
+
"jsonc-parser": "^3.3.1",
|
|
68
|
+
"solid-js": "^1.9.12"
|
|
69
|
+
},
|
|
70
|
+
"devDependencies": {
|
|
71
|
+
"@opencode-ai/plugin": "^1.18.34",
|
|
72
|
+
"@opencode/plugin": "^2.0.22",
|
|
73
|
+
"@opencode/theme": "^2.0.4",
|
|
74
|
+
"@types/node": "^25.5.0",
|
|
75
|
+
"prettier": "^3.8.1",
|
|
76
|
+
"tsup": "^8.5.1",
|
|
77
|
+
"tsx": "^4.21.0",
|
|
78
|
+
"typescript": "^6.0.2"
|
|
79
|
+
},
|
|
80
|
+
"overrides": {
|
|
81
|
+
"@babel/core": "7.29.7",
|
|
82
|
+
"esbuild": "0.28.1"
|
|
83
|
+
},
|
|
84
|
+
"files": [
|
|
85
|
+
"dist/",
|
|
86
|
+
"lib/",
|
|
87
|
+
"server.js",
|
|
88
|
+
"tui.tsx",
|
|
89
|
+
"README.md",
|
|
90
|
+
"LICENSE"
|
|
91
|
+
]
|
|
92
|
+
}
|
package/server.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { default } from "./dist/index.js"
|