compressed-context 0.1.0 → 0.1.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/package.json +64 -64
- package/src/compression/active-summary-cache.ts +50 -50
- package/src/compression/adopt.ts +57 -57
- package/src/compression/auto.ts +372 -372
- package/src/compression/babel.ts +85 -85
- package/src/compression/block.ts +25 -25
- package/src/compression/cache-history.ts +75 -75
- package/src/compression/compaction.ts +189 -189
- package/src/compression/decompress-tool.ts +74 -74
- package/src/compression/decompress.ts +292 -292
- package/src/compression/deleted-sessions.ts +24 -24
- package/src/compression/distill.ts +220 -220
- package/src/compression/events.ts +275 -275
- package/src/compression/markdown.ts +53 -53
- package/src/compression/normalize.ts +114 -114
- package/src/compression/render.ts +70 -70
- package/src/compression/schema.ts +84 -84
- package/src/compression/select.ts +123 -123
- package/src/compression/session-message.ts +112 -112
- package/src/compression/settings-rpc.ts +111 -111
- package/src/compression/settings.ts +175 -175
- package/src/compression/state.ts +248 -248
- package/src/compression/summarize.ts +595 -595
- package/src/compression/summary.ts +73 -73
- package/src/compression/tool.ts +142 -142
- package/src/compression/transcript-store.ts +258 -258
- package/src/compression/transcript.ts +63 -63
- package/src/compression/validate.ts +81 -81
- package/src/i18n/en.ts +93 -93
- package/src/i18n/index.ts +28 -28
- package/src/i18n/types.ts +89 -89
- package/src/i18n/zh.ts +93 -93
- package/src/index.ts +108 -108
- package/src/shared/blocks.ts +17 -17
- package/src/shared/cache.ts +199 -199
- package/src/shared/format.ts +23 -23
- package/src/shared/media.ts +184 -184
- package/src/shared/parse.ts +25 -25
- package/src/shared/parts.ts +72 -72
- package/src/shared/rpc.ts +145 -145
- package/src/shared/scan.ts +32 -32
- package/src/shared/settings.ts +471 -471
- package/src/shared/stats.ts +142 -142
- package/src/shared/tokens.ts +259 -259
- package/src/tui/chart/cache-chart.ts +237 -237
- package/src/tui/chart/color.ts +58 -58
- package/src/tui/index.tsx +111 -111
- package/src/tui/panel/cache.tsx +203 -203
- package/src/tui/panel/data.ts +105 -105
- package/src/tui/panel/index.tsx +176 -176
- package/src/tui/panel/summaries.tsx +192 -192
- package/src/tui/settings/dialogs.tsx +215 -215
- package/src/tui/settings/options.ts +238 -238
- package/src/tui/settings.tsx +418 -418
- package/src/tui/types.ts +4 -4
package/src/shared/tokens.ts
CHANGED
|
@@ -1,259 +1,259 @@
|
|
|
1
|
-
import { attachmentTokens } from "./media.ts"
|
|
2
|
-
import { stringify, toolInput, toolResult, toolResultText } from "./parts.ts"
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* 上下文分类 token 估算。
|
|
6
|
-
*
|
|
7
|
-
* 输入:`context` 钩子里的 system / tools / messages 原始结构。
|
|
8
|
-
* 输出:各类别的 token 估算值(system、tools、tool、text、code、summaries、media)。
|
|
9
|
-
* 预期:只用于计算「各类占多少比例」;绝对总量以 provider 返回的 usage 为准,
|
|
10
|
-
* 展示层按真实总量缩放,所以这里的绝对值允许有偏差。
|
|
11
|
-
*/
|
|
12
|
-
|
|
13
|
-
/** 单条消息内容的最小结构,避免绑定 @opencode/ai 的具体类。 */
|
|
14
|
-
export interface FileAttachmentLike {
|
|
15
|
-
readonly mime?: string | undefined
|
|
16
|
-
readonly data?: string | Uint8Array | undefined
|
|
17
|
-
readonly name?: string | undefined
|
|
18
|
-
readonly uri?: string | undefined
|
|
19
|
-
readonly durationSeconds?: number | undefined
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
export interface ContentPartLike {
|
|
23
|
-
readonly type: string
|
|
24
|
-
readonly text?: string | null | undefined
|
|
25
|
-
readonly name?: string | undefined
|
|
26
|
-
readonly input?: unknown
|
|
27
|
-
readonly result?: unknown
|
|
28
|
-
readonly state?:
|
|
29
|
-
| {
|
|
30
|
-
readonly input?: unknown
|
|
31
|
-
readonly content?: unknown
|
|
32
|
-
readonly output?: unknown
|
|
33
|
-
}
|
|
34
|
-
| undefined
|
|
35
|
-
readonly file?: FileAttachmentLike | undefined
|
|
36
|
-
readonly media?:
|
|
37
|
-
| {
|
|
38
|
-
readonly source?:
|
|
39
|
-
| {
|
|
40
|
-
readonly type?: string | undefined
|
|
41
|
-
readonly data?: string | Uint8Array | undefined
|
|
42
|
-
readonly mediaType?: string | undefined
|
|
43
|
-
}
|
|
44
|
-
| undefined
|
|
45
|
-
readonly info?:
|
|
46
|
-
| {
|
|
47
|
-
readonly width?: number | undefined
|
|
48
|
-
readonly height?: number | undefined
|
|
49
|
-
readonly durationSeconds?: number | undefined
|
|
50
|
-
}
|
|
51
|
-
| undefined
|
|
52
|
-
}
|
|
53
|
-
| undefined
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
export interface MessageLike {
|
|
57
|
-
readonly role?: string
|
|
58
|
-
readonly content: readonly ContentPartLike[]
|
|
59
|
-
/** @opencode/ai 的 Message 用 content;保留 text 兼容旧的扁平静态消息。 */
|
|
60
|
-
readonly text?: string | null | undefined
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
export interface SystemPartLike {
|
|
64
|
-
readonly type: string
|
|
65
|
-
readonly text: string
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
export interface ToolDefinitionLike {
|
|
69
|
-
readonly description?: string
|
|
70
|
-
readonly input?: unknown
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
export interface TokenCategories {
|
|
74
|
-
readonly system: number
|
|
75
|
-
readonly tools: number
|
|
76
|
-
readonly tool: number
|
|
77
|
-
readonly text: number
|
|
78
|
-
readonly recent: number
|
|
79
|
-
readonly summaries: number
|
|
80
|
-
readonly media: number
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
/** CJK 字符按 1 token 计,其余按 4 字符 1 token 向上取整。 */
|
|
84
|
-
export function countText(text: string): number {
|
|
85
|
-
if (text.length === 0) return 0
|
|
86
|
-
const cjk = text.match(/[\u4e00-\u9fff\u3040-\u30ff\uac00-\ud7af]/gu)
|
|
87
|
-
const cjkCount = cjk?.length ?? 0
|
|
88
|
-
return cjkCount + Math.ceil((text.length - cjkCount) / 4)
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
/** 输入:工具结果里的单个 content 项;输出:token 估算。 */
|
|
92
|
-
function countToolResultItem(item: unknown): number {
|
|
93
|
-
if (typeof item === "object" && item !== null) {
|
|
94
|
-
const typed = item as {
|
|
95
|
-
readonly type?: unknown
|
|
96
|
-
readonly text?: unknown
|
|
97
|
-
readonly mime?: string
|
|
98
|
-
readonly uri?: string
|
|
99
|
-
readonly name?: string
|
|
100
|
-
readonly media?:
|
|
101
|
-
| {
|
|
102
|
-
readonly mediaType?: string
|
|
103
|
-
readonly durationSeconds?: number
|
|
104
|
-
readonly source?: { readonly data?: string | Uint8Array; readonly mediaType?: string }
|
|
105
|
-
}
|
|
106
|
-
| undefined
|
|
107
|
-
}
|
|
108
|
-
if (typed.type === "text" && typeof typed.text === "string") return countText(typed.text)
|
|
109
|
-
if (typed.type === "file") {
|
|
110
|
-
return attachmentTokens({ mime: typed.mime })
|
|
111
|
-
}
|
|
112
|
-
if (typed.type === "media") {
|
|
113
|
-
return attachmentTokens({
|
|
114
|
-
mime: typed.media?.mediaType ?? typed.media?.source?.mediaType,
|
|
115
|
-
data: typed.media?.source?.data,
|
|
116
|
-
durationSeconds: typed.media?.durationSeconds,
|
|
117
|
-
})
|
|
118
|
-
}
|
|
119
|
-
}
|
|
120
|
-
return countText(toolResultText(item))
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
/** 输入:工具结果;输出:token 估算;预期:v2 的 content 数组和旧 result 都能计数。 */
|
|
124
|
-
function countToolResult(result: unknown): number {
|
|
125
|
-
if (Array.isArray(result)) {
|
|
126
|
-
return result.reduce<number>((sum, item) => sum + countToolResultItem(item), 0)
|
|
127
|
-
}
|
|
128
|
-
if (typeof result === "object" && result !== null) {
|
|
129
|
-
const typed = result as { readonly content?: unknown; readonly value?: unknown; readonly output?: unknown }
|
|
130
|
-
if (Array.isArray(typed.content)) return countToolResult(typed.content)
|
|
131
|
-
if (Array.isArray(typed.value)) return countToolResult(typed.value)
|
|
132
|
-
if (typed.output !== undefined) return countToolResult(typed.output)
|
|
133
|
-
}
|
|
134
|
-
return countText(toolResultText(result))
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
const CHECKPOINT_PREFIX = "<conversation-checkpoint>"
|
|
138
|
-
const SUMMARY_TAG = /<summary>([\s\S]*?)<\/summary>/
|
|
139
|
-
const RECENT_TAG = /<recent-context>([\s\S]*?)<\/recent-context>/
|
|
140
|
-
|
|
141
|
-
/**
|
|
142
|
-
* 输入:checkpoint 文本。
|
|
143
|
-
* 输出:摘要与最近上下文两部分的 token 估算。
|
|
144
|
-
* 预期:只把 <summary> 计入摘要;<recent-context> 是逐字保留的最近对话,
|
|
145
|
-
* 单独计数,避免把最近上下文误当成摘要。
|
|
146
|
-
*/
|
|
147
|
-
function countCheckpoint(text: string): { readonly summary: number; readonly recent: number } {
|
|
148
|
-
const summary = SUMMARY_TAG.exec(text)?.[1]
|
|
149
|
-
const recent = RECENT_TAG.exec(text)?.[1]
|
|
150
|
-
if (summary === undefined && recent === undefined) {
|
|
151
|
-
return { summary: countText(text), recent: 0 }
|
|
152
|
-
}
|
|
153
|
-
return { summary: countText(summary ?? ""), recent: countText(recent ?? "") }
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
/**
|
|
157
|
-
* 输入:system 片段、工具定义表、消息列表。
|
|
158
|
-
* 输出:按类别汇总的 token 估算。
|
|
159
|
-
* 预期:所有 text / reasoning 都归入 text(代码块不再单列);tool-call 与 tool-result 归入 tool。
|
|
160
|
-
*/
|
|
161
|
-
export function estimateCategories(input: {
|
|
162
|
-
readonly system: readonly SystemPartLike[]
|
|
163
|
-
readonly tools: Readonly<Record<string, ToolDefinitionLike>>
|
|
164
|
-
readonly messages: readonly MessageLike[]
|
|
165
|
-
}): TokenCategories {
|
|
166
|
-
let system = 0
|
|
167
|
-
for (const part of input.system) {
|
|
168
|
-
if (part.type === "text") system += countText(part.text)
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
let tools = 0
|
|
172
|
-
for (const [name, definition] of Object.entries(input.tools)) {
|
|
173
|
-
tools += countText(`${name}${definition.description ?? ""}${stringify(definition.input)}`)
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
let tool = 0
|
|
177
|
-
let text = 0
|
|
178
|
-
let recent = 0
|
|
179
|
-
let summaries = 0
|
|
180
|
-
let media = 0
|
|
181
|
-
|
|
182
|
-
for (const message of input.messages) {
|
|
183
|
-
const content: readonly ContentPartLike[] = Array.isArray(message.content)
|
|
184
|
-
? message.content
|
|
185
|
-
: []
|
|
186
|
-
const contentHasText = content.some((part) => part.type === "text")
|
|
187
|
-
if (!contentHasText && typeof message.text === "string") text += countText(message.text)
|
|
188
|
-
for (const part of content) {
|
|
189
|
-
switch (part.type) {
|
|
190
|
-
case "text": {
|
|
191
|
-
const value = part.text ?? ""
|
|
192
|
-
if (value.trimStart().startsWith(CHECKPOINT_PREFIX)) {
|
|
193
|
-
const checkpoint = countCheckpoint(value)
|
|
194
|
-
summaries += checkpoint.summary
|
|
195
|
-
recent += checkpoint.recent
|
|
196
|
-
} else text += countText(value)
|
|
197
|
-
break
|
|
198
|
-
}
|
|
199
|
-
case "reasoning": {
|
|
200
|
-
text += countText(part.text ?? "")
|
|
201
|
-
break
|
|
202
|
-
}
|
|
203
|
-
case "tool-call": {
|
|
204
|
-
tool += countText(`${part.name ?? ""}${stringify(toolInput(part))}`)
|
|
205
|
-
break
|
|
206
|
-
}
|
|
207
|
-
case "tool-result": {
|
|
208
|
-
tool += countToolResult(toolResult(part))
|
|
209
|
-
break
|
|
210
|
-
}
|
|
211
|
-
case "tool": {
|
|
212
|
-
tool += countText(`${part.name ?? ""}${stringify(toolInput(part))}`)
|
|
213
|
-
tool += countToolResult(toolResult(part))
|
|
214
|
-
break
|
|
215
|
-
}
|
|
216
|
-
case "compaction": {
|
|
217
|
-
summaries += countText(part.text ?? "")
|
|
218
|
-
break
|
|
219
|
-
}
|
|
220
|
-
case "file": {
|
|
221
|
-
media += attachmentTokens({
|
|
222
|
-
mime: part.file?.mime,
|
|
223
|
-
data: part.file?.data,
|
|
224
|
-
durationSeconds: part.file?.durationSeconds,
|
|
225
|
-
})
|
|
226
|
-
break
|
|
227
|
-
}
|
|
228
|
-
case "media": {
|
|
229
|
-
media += attachmentTokens({
|
|
230
|
-
mime: part.media?.source?.mediaType,
|
|
231
|
-
data: part.media?.source?.data,
|
|
232
|
-
width: part.media?.info?.width,
|
|
233
|
-
height: part.media?.info?.height,
|
|
234
|
-
durationSeconds: part.media?.info?.durationSeconds,
|
|
235
|
-
})
|
|
236
|
-
break
|
|
237
|
-
}
|
|
238
|
-
default: {
|
|
239
|
-
break
|
|
240
|
-
}
|
|
241
|
-
}
|
|
242
|
-
}
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
return { system, tools, tool, text, recent, summaries, media }
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
/** 输入:分类估算;输出:各类之和。 */
|
|
249
|
-
export function totalCategories(categories: TokenCategories): number {
|
|
250
|
-
return (
|
|
251
|
-
categories.system +
|
|
252
|
-
categories.tools +
|
|
253
|
-
categories.tool +
|
|
254
|
-
categories.text +
|
|
255
|
-
categories.recent +
|
|
256
|
-
categories.summaries +
|
|
257
|
-
categories.media
|
|
258
|
-
)
|
|
259
|
-
}
|
|
1
|
+
import { attachmentTokens } from "./media.ts"
|
|
2
|
+
import { stringify, toolInput, toolResult, toolResultText } from "./parts.ts"
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* 上下文分类 token 估算。
|
|
6
|
+
*
|
|
7
|
+
* 输入:`context` 钩子里的 system / tools / messages 原始结构。
|
|
8
|
+
* 输出:各类别的 token 估算值(system、tools、tool、text、code、summaries、media)。
|
|
9
|
+
* 预期:只用于计算「各类占多少比例」;绝对总量以 provider 返回的 usage 为准,
|
|
10
|
+
* 展示层按真实总量缩放,所以这里的绝对值允许有偏差。
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
/** 单条消息内容的最小结构,避免绑定 @opencode/ai 的具体类。 */
|
|
14
|
+
export interface FileAttachmentLike {
|
|
15
|
+
readonly mime?: string | undefined
|
|
16
|
+
readonly data?: string | Uint8Array | undefined
|
|
17
|
+
readonly name?: string | undefined
|
|
18
|
+
readonly uri?: string | undefined
|
|
19
|
+
readonly durationSeconds?: number | undefined
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface ContentPartLike {
|
|
23
|
+
readonly type: string
|
|
24
|
+
readonly text?: string | null | undefined
|
|
25
|
+
readonly name?: string | undefined
|
|
26
|
+
readonly input?: unknown
|
|
27
|
+
readonly result?: unknown
|
|
28
|
+
readonly state?:
|
|
29
|
+
| {
|
|
30
|
+
readonly input?: unknown
|
|
31
|
+
readonly content?: unknown
|
|
32
|
+
readonly output?: unknown
|
|
33
|
+
}
|
|
34
|
+
| undefined
|
|
35
|
+
readonly file?: FileAttachmentLike | undefined
|
|
36
|
+
readonly media?:
|
|
37
|
+
| {
|
|
38
|
+
readonly source?:
|
|
39
|
+
| {
|
|
40
|
+
readonly type?: string | undefined
|
|
41
|
+
readonly data?: string | Uint8Array | undefined
|
|
42
|
+
readonly mediaType?: string | undefined
|
|
43
|
+
}
|
|
44
|
+
| undefined
|
|
45
|
+
readonly info?:
|
|
46
|
+
| {
|
|
47
|
+
readonly width?: number | undefined
|
|
48
|
+
readonly height?: number | undefined
|
|
49
|
+
readonly durationSeconds?: number | undefined
|
|
50
|
+
}
|
|
51
|
+
| undefined
|
|
52
|
+
}
|
|
53
|
+
| undefined
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export interface MessageLike {
|
|
57
|
+
readonly role?: string
|
|
58
|
+
readonly content: readonly ContentPartLike[]
|
|
59
|
+
/** @opencode/ai 的 Message 用 content;保留 text 兼容旧的扁平静态消息。 */
|
|
60
|
+
readonly text?: string | null | undefined
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export interface SystemPartLike {
|
|
64
|
+
readonly type: string
|
|
65
|
+
readonly text: string
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export interface ToolDefinitionLike {
|
|
69
|
+
readonly description?: string
|
|
70
|
+
readonly input?: unknown
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export interface TokenCategories {
|
|
74
|
+
readonly system: number
|
|
75
|
+
readonly tools: number
|
|
76
|
+
readonly tool: number
|
|
77
|
+
readonly text: number
|
|
78
|
+
readonly recent: number
|
|
79
|
+
readonly summaries: number
|
|
80
|
+
readonly media: number
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** CJK 字符按 1 token 计,其余按 4 字符 1 token 向上取整。 */
|
|
84
|
+
export function countText(text: string): number {
|
|
85
|
+
if (text.length === 0) return 0
|
|
86
|
+
const cjk = text.match(/[\u4e00-\u9fff\u3040-\u30ff\uac00-\ud7af]/gu)
|
|
87
|
+
const cjkCount = cjk?.length ?? 0
|
|
88
|
+
return cjkCount + Math.ceil((text.length - cjkCount) / 4)
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** 输入:工具结果里的单个 content 项;输出:token 估算。 */
|
|
92
|
+
function countToolResultItem(item: unknown): number {
|
|
93
|
+
if (typeof item === "object" && item !== null) {
|
|
94
|
+
const typed = item as {
|
|
95
|
+
readonly type?: unknown
|
|
96
|
+
readonly text?: unknown
|
|
97
|
+
readonly mime?: string
|
|
98
|
+
readonly uri?: string
|
|
99
|
+
readonly name?: string
|
|
100
|
+
readonly media?:
|
|
101
|
+
| {
|
|
102
|
+
readonly mediaType?: string
|
|
103
|
+
readonly durationSeconds?: number
|
|
104
|
+
readonly source?: { readonly data?: string | Uint8Array; readonly mediaType?: string }
|
|
105
|
+
}
|
|
106
|
+
| undefined
|
|
107
|
+
}
|
|
108
|
+
if (typed.type === "text" && typeof typed.text === "string") return countText(typed.text)
|
|
109
|
+
if (typed.type === "file") {
|
|
110
|
+
return attachmentTokens({ mime: typed.mime })
|
|
111
|
+
}
|
|
112
|
+
if (typed.type === "media") {
|
|
113
|
+
return attachmentTokens({
|
|
114
|
+
mime: typed.media?.mediaType ?? typed.media?.source?.mediaType,
|
|
115
|
+
data: typed.media?.source?.data,
|
|
116
|
+
durationSeconds: typed.media?.durationSeconds,
|
|
117
|
+
})
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
return countText(toolResultText(item))
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/** 输入:工具结果;输出:token 估算;预期:v2 的 content 数组和旧 result 都能计数。 */
|
|
124
|
+
function countToolResult(result: unknown): number {
|
|
125
|
+
if (Array.isArray(result)) {
|
|
126
|
+
return result.reduce<number>((sum, item) => sum + countToolResultItem(item), 0)
|
|
127
|
+
}
|
|
128
|
+
if (typeof result === "object" && result !== null) {
|
|
129
|
+
const typed = result as { readonly content?: unknown; readonly value?: unknown; readonly output?: unknown }
|
|
130
|
+
if (Array.isArray(typed.content)) return countToolResult(typed.content)
|
|
131
|
+
if (Array.isArray(typed.value)) return countToolResult(typed.value)
|
|
132
|
+
if (typed.output !== undefined) return countToolResult(typed.output)
|
|
133
|
+
}
|
|
134
|
+
return countText(toolResultText(result))
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const CHECKPOINT_PREFIX = "<conversation-checkpoint>"
|
|
138
|
+
const SUMMARY_TAG = /<summary>([\s\S]*?)<\/summary>/
|
|
139
|
+
const RECENT_TAG = /<recent-context>([\s\S]*?)<\/recent-context>/
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* 输入:checkpoint 文本。
|
|
143
|
+
* 输出:摘要与最近上下文两部分的 token 估算。
|
|
144
|
+
* 预期:只把 <summary> 计入摘要;<recent-context> 是逐字保留的最近对话,
|
|
145
|
+
* 单独计数,避免把最近上下文误当成摘要。
|
|
146
|
+
*/
|
|
147
|
+
function countCheckpoint(text: string): { readonly summary: number; readonly recent: number } {
|
|
148
|
+
const summary = SUMMARY_TAG.exec(text)?.[1]
|
|
149
|
+
const recent = RECENT_TAG.exec(text)?.[1]
|
|
150
|
+
if (summary === undefined && recent === undefined) {
|
|
151
|
+
return { summary: countText(text), recent: 0 }
|
|
152
|
+
}
|
|
153
|
+
return { summary: countText(summary ?? ""), recent: countText(recent ?? "") }
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* 输入:system 片段、工具定义表、消息列表。
|
|
158
|
+
* 输出:按类别汇总的 token 估算。
|
|
159
|
+
* 预期:所有 text / reasoning 都归入 text(代码块不再单列);tool-call 与 tool-result 归入 tool。
|
|
160
|
+
*/
|
|
161
|
+
export function estimateCategories(input: {
|
|
162
|
+
readonly system: readonly SystemPartLike[]
|
|
163
|
+
readonly tools: Readonly<Record<string, ToolDefinitionLike>>
|
|
164
|
+
readonly messages: readonly MessageLike[]
|
|
165
|
+
}): TokenCategories {
|
|
166
|
+
let system = 0
|
|
167
|
+
for (const part of input.system) {
|
|
168
|
+
if (part.type === "text") system += countText(part.text)
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
let tools = 0
|
|
172
|
+
for (const [name, definition] of Object.entries(input.tools)) {
|
|
173
|
+
tools += countText(`${name}${definition.description ?? ""}${stringify(definition.input)}`)
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
let tool = 0
|
|
177
|
+
let text = 0
|
|
178
|
+
let recent = 0
|
|
179
|
+
let summaries = 0
|
|
180
|
+
let media = 0
|
|
181
|
+
|
|
182
|
+
for (const message of input.messages) {
|
|
183
|
+
const content: readonly ContentPartLike[] = Array.isArray(message.content)
|
|
184
|
+
? message.content
|
|
185
|
+
: []
|
|
186
|
+
const contentHasText = content.some((part) => part.type === "text")
|
|
187
|
+
if (!contentHasText && typeof message.text === "string") text += countText(message.text)
|
|
188
|
+
for (const part of content) {
|
|
189
|
+
switch (part.type) {
|
|
190
|
+
case "text": {
|
|
191
|
+
const value = part.text ?? ""
|
|
192
|
+
if (value.trimStart().startsWith(CHECKPOINT_PREFIX)) {
|
|
193
|
+
const checkpoint = countCheckpoint(value)
|
|
194
|
+
summaries += checkpoint.summary
|
|
195
|
+
recent += checkpoint.recent
|
|
196
|
+
} else text += countText(value)
|
|
197
|
+
break
|
|
198
|
+
}
|
|
199
|
+
case "reasoning": {
|
|
200
|
+
text += countText(part.text ?? "")
|
|
201
|
+
break
|
|
202
|
+
}
|
|
203
|
+
case "tool-call": {
|
|
204
|
+
tool += countText(`${part.name ?? ""}${stringify(toolInput(part))}`)
|
|
205
|
+
break
|
|
206
|
+
}
|
|
207
|
+
case "tool-result": {
|
|
208
|
+
tool += countToolResult(toolResult(part))
|
|
209
|
+
break
|
|
210
|
+
}
|
|
211
|
+
case "tool": {
|
|
212
|
+
tool += countText(`${part.name ?? ""}${stringify(toolInput(part))}`)
|
|
213
|
+
tool += countToolResult(toolResult(part))
|
|
214
|
+
break
|
|
215
|
+
}
|
|
216
|
+
case "compaction": {
|
|
217
|
+
summaries += countText(part.text ?? "")
|
|
218
|
+
break
|
|
219
|
+
}
|
|
220
|
+
case "file": {
|
|
221
|
+
media += attachmentTokens({
|
|
222
|
+
mime: part.file?.mime,
|
|
223
|
+
data: part.file?.data,
|
|
224
|
+
durationSeconds: part.file?.durationSeconds,
|
|
225
|
+
})
|
|
226
|
+
break
|
|
227
|
+
}
|
|
228
|
+
case "media": {
|
|
229
|
+
media += attachmentTokens({
|
|
230
|
+
mime: part.media?.source?.mediaType,
|
|
231
|
+
data: part.media?.source?.data,
|
|
232
|
+
width: part.media?.info?.width,
|
|
233
|
+
height: part.media?.info?.height,
|
|
234
|
+
durationSeconds: part.media?.info?.durationSeconds,
|
|
235
|
+
})
|
|
236
|
+
break
|
|
237
|
+
}
|
|
238
|
+
default: {
|
|
239
|
+
break
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
return { system, tools, tool, text, recent, summaries, media }
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/** 输入:分类估算;输出:各类之和。 */
|
|
249
|
+
export function totalCategories(categories: TokenCategories): number {
|
|
250
|
+
return (
|
|
251
|
+
categories.system +
|
|
252
|
+
categories.tools +
|
|
253
|
+
categories.tool +
|
|
254
|
+
categories.text +
|
|
255
|
+
categories.recent +
|
|
256
|
+
categories.summaries +
|
|
257
|
+
categories.media
|
|
258
|
+
)
|
|
259
|
+
}
|