@chenlongapps/opencode-token-usage 0.3.3 → 0.4.0

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/dist/usage.d.ts CHANGED
@@ -91,16 +91,48 @@ export declare function summarize(messages: Iterable<UsageMessage>, catalog?: Pr
91
91
  total: number;
92
92
  cacheRate: number;
93
93
  steps: number;
94
+ calls: number;
94
95
  cost: number;
95
96
  costStatus: CostStatus;
96
97
  };
97
98
  export type Summary = ReturnType<typeof summarize>;
99
+ export interface ModelCost {
100
+ model: string;
101
+ calls: number;
102
+ tokens: number;
103
+ cost: number;
104
+ costStatus: CostStatus;
105
+ }
106
+ /** Groups the same per-message estimates used by the tree total; unlabelled calls stay visible. */
107
+ export declare function summarizeModels(messages: Iterable<UsageMessage>, catalog?: PriceCatalog): ModelCost[];
98
108
  export interface ContextUsage {
99
109
  used: number;
100
110
  limit: number;
101
111
  percent: number;
102
112
  }
113
+ export interface ContextDetails {
114
+ usage: ContextUsage;
115
+ tokens: Tokens;
116
+ }
117
+ export declare function contextDetails(messages: readonly UsageMessage[], limit?: number): ContextDetails | undefined;
103
118
  export declare function contextUsage(messages: readonly UsageMessage[], limit?: number): ContextUsage | undefined;
104
119
  export declare function formatTokens(value: number): string;
105
120
  export declare const formatCost: (value: number) => string;
121
+ export declare function formatEstimatedCost(cost: number, status: CostStatus): string | undefined;
122
+ /** Compact display for scan-first views: 812, 139.4K, 3.70M. Detailed views keep formatTokens. */
123
+ export declare function formatCompact(value: number): string;
124
+ /** Splits a percentage into filled and empty cell counts for a plain-text bar. */
125
+ export declare function bar(percent: number, width: number): {
126
+ filled: number;
127
+ empty: number;
128
+ };
129
+ /**
130
+ * One model request: the five categories of the last assistant call plus its cache rate.
131
+ * Compact mode hides zero rows and drops percentages; detailed mode keeps both.
132
+ */
133
+ export declare function requestRows(details: ContextDetails, compact: boolean): readonly (readonly [string, string])[];
134
+ /** Tree summary rows for the dialog's detailed Session view, calls included. */
135
+ export declare function summaryRows(summary: Summary): readonly (readonly [string, string])[];
136
+ /** `1 step` reads better than `1 steps` in the dialog's single-line summaries. */
137
+ export declare const countLabel: (value: number, noun: string) => string;
106
138
  export declare function usageRows(summary?: Summary, context?: ContextUsage, performance?: PerformanceSummary): readonly (readonly [string, string])[];
package/dist/usage.js CHANGED
@@ -22,6 +22,22 @@ export function estimate(tokens, prices = []) {
22
22
  && (rate === undefined || !Number.isFinite(rate) || rate < 0))),
23
23
  };
24
24
  }
25
+ function priceMessage(tokens, model, catalog) {
26
+ if (!model)
27
+ return undefined;
28
+ let value = estimate(tokens, catalog.get(modelKey(model)));
29
+ if (value.defaultPrice || value.cost === 0) {
30
+ const snapshot = estimate(tokens, officialPrice(model)?.prices);
31
+ if (!snapshot.defaultPrice)
32
+ value = snapshot;
33
+ }
34
+ return value.defaultPrice ? undefined : value.cost;
35
+ }
36
+ function costStatus(known, missing) {
37
+ if (missing > 0)
38
+ return known > 0 ? "partial" : "unavailable";
39
+ return known > 0 ? "complete" : "empty";
40
+ }
25
41
  /** Prices every call with its recorded model. A complete non-zero OpenCode
26
42
  * price wins; a complete snapshot price can replace a complete OpenCode zero.
27
43
  * Incomplete OpenCode prices fall back to the snapshot as a whole. */
@@ -29,6 +45,7 @@ export function summarize(messages, catalog = new Map()) {
29
45
  const tokens = normalize();
30
46
  let cost = 0;
31
47
  let steps = 0;
48
+ let calls = 0;
32
49
  let knownCosts = 0;
33
50
  let missingCosts = 0;
34
51
  for (const message of messages) {
@@ -46,37 +63,55 @@ export function summarize(messages, catalog = new Map()) {
46
63
  tokens.cache.write += t.cache.write;
47
64
  if (total(t) === 0)
48
65
  continue;
49
- if (!message.model) {
50
- missingCosts++;
51
- continue;
52
- }
53
- let value = estimate(t, catalog.get(modelKey(message.model)));
54
- if (value.defaultPrice || value.cost === 0) {
55
- const snapshot = estimate(t, officialPrice(message.model)?.prices);
56
- if (!snapshot.defaultPrice)
57
- value = snapshot;
58
- }
59
- if (value.defaultPrice)
66
+ // Same population the per-model rows price, so the dialog's call count reconciles with them.
67
+ calls++;
68
+ const value = priceMessage(t, message.model, catalog);
69
+ if (value === undefined)
60
70
  missingCosts++;
61
71
  else {
62
- cost += value.cost;
72
+ cost += value;
63
73
  knownCosts++;
64
74
  }
65
75
  }
66
- const costStatus = missingCosts > 0
67
- ? knownCosts > 0 ? "partial" : "unavailable"
68
- : knownCosts > 0 ? "complete" : "empty";
69
76
  return {
70
77
  tokens, total: total(tokens), cacheRate: incoming(tokens) ? tokens.cache.read / incoming(tokens) : 0,
71
- steps, cost, costStatus,
78
+ steps, calls, cost, costStatus: costStatus(knownCosts, missingCosts),
72
79
  };
73
80
  }
81
+ /** Groups the same per-message estimates used by the tree total; unlabelled calls stay visible. */
82
+ export function summarizeModels(messages, catalog = new Map()) {
83
+ const groups = new Map();
84
+ for (const message of messages) {
85
+ if (message.type !== "assistant" && message.type !== "compaction")
86
+ continue;
87
+ const tokens = normalize(message.tokens);
88
+ const count = total(tokens);
89
+ if (count === 0)
90
+ continue;
91
+ const key = message.model && modelKey(message.model);
92
+ const group = groups.get(key) ?? { calls: 0, tokens: 0, cost: 0, known: 0, missing: 0 };
93
+ group.calls++;
94
+ group.tokens += count;
95
+ const value = priceMessage(tokens, message.model, catalog);
96
+ if (value === undefined)
97
+ group.missing++;
98
+ else {
99
+ group.cost += value;
100
+ group.known++;
101
+ }
102
+ groups.set(key, group);
103
+ }
104
+ return [...groups].map(([model, group]) => ({
105
+ model: model ?? "Unknown model", calls: group.calls, tokens: group.tokens,
106
+ cost: group.cost, costStatus: costStatus(group.known, group.missing),
107
+ })).sort((a, b) => b.cost - a.cost || a.model.localeCompare(b.model));
108
+ }
74
109
  const usableLimit = (value) => value !== undefined && Number.isFinite(value) && value > 0;
75
110
  // Matches the OpenCode 2.0.10 sidebar: the last assistant message with tokens that follows
76
111
  // the last completed compaction carries the current context size. Running or failed
77
112
  // compactions do not reset it, and a last message without usage hides the row entirely
78
113
  // instead of falling back to pre-compaction history.
79
- export function contextUsage(messages, limit) {
114
+ export function contextDetails(messages, limit) {
80
115
  if (!usableLimit(limit))
81
116
  return undefined;
82
117
  let boundary = -1;
@@ -91,15 +126,68 @@ export function contextUsage(messages, limit) {
91
126
  const message = messages[index];
92
127
  if (message.type !== "assistant" || !message.tokens)
93
128
  continue;
94
- const used = total(normalize(message.tokens));
95
- return used > 0 ? { used, limit, percent: used / limit * 100 } : undefined;
129
+ const tokens = normalize(message.tokens);
130
+ const used = total(tokens);
131
+ return used > 0 ? { usage: { used, limit, percent: used / limit * 100 }, tokens } : undefined;
96
132
  }
97
133
  return undefined;
98
134
  }
135
+ export function contextUsage(messages, limit) {
136
+ return contextDetails(messages, limit)?.usage;
137
+ }
99
138
  export function formatTokens(value) {
100
139
  return Math.round(safe(value)).toLocaleString("en-US");
101
140
  }
102
141
  export const formatCost = (value) => value > 0 && value < 0.01 ? "<$0.01" : `$${safe(value).toFixed(2)}`;
142
+ export function formatEstimatedCost(cost, status) {
143
+ if (status === "empty")
144
+ return undefined;
145
+ if (status === "unavailable")
146
+ return "—";
147
+ return `${formatCost(cost)}${status === "partial" ? " · partial" : ""}`;
148
+ }
149
+ /** Compact display for scan-first views: 812, 139.4K, 3.70M. Detailed views keep formatTokens. */
150
+ export function formatCompact(value) {
151
+ const n = safe(value);
152
+ if (n < 1_000)
153
+ return String(Math.round(n));
154
+ const k = n / 1_000;
155
+ return k >= 999.95 ? `${(n / 1_000_000).toFixed(2)}M` : `${k.toFixed(1)}K`;
156
+ }
157
+ /** Splits a percentage into filled and empty cell counts for a plain-text bar. */
158
+ export function bar(percent, width) {
159
+ const size = Math.max(0, Math.floor(width));
160
+ const filled = Math.max(0, Math.min(size, Math.round(percent / 100 * size)));
161
+ return { filled, empty: size - filled };
162
+ }
163
+ /**
164
+ * One model request: the five categories of the last assistant call plus its cache rate.
165
+ * Compact mode hides zero rows and drops percentages; detailed mode keeps both.
166
+ */
167
+ export function requestRows(details, compact) {
168
+ const t = details.tokens;
169
+ const rows = [
170
+ ["Input", t.input], ["Output", t.output], ["Reasoning", t.reasoning],
171
+ ["Cache Read", t.cache.read], ["Cache Write", t.cache.write],
172
+ ].filter(([, count]) => !compact || count > 0);
173
+ const incomingTokens = incoming(t);
174
+ return [
175
+ ...rows.map(([label, count]) => [
176
+ label,
177
+ compact ? formatCompact(count) : `${formatTokens(count)} (${(count / details.usage.used * 100).toFixed(1)}%)`,
178
+ ]),
179
+ ["Cache Rate", `${(incomingTokens ? t.cache.read / incomingTokens * 100 : 0).toFixed(1)}%`],
180
+ ];
181
+ }
182
+ /** Tree summary rows for the dialog's detailed Session view, calls included. */
183
+ export function summaryRows(summary) {
184
+ return [
185
+ ["Steps", formatTokens(summary.steps)], ["Calls", formatTokens(summary.calls)],
186
+ ...usageRows(summary).filter(([label]) => label !== "Steps"),
187
+ ];
188
+ }
189
+ /** `1 step` reads better than `1 steps` in the dialog's single-line summaries. */
190
+ export const countLabel = (value, noun) => `${formatTokens(value)} ${noun}${value === 1 ? "" : "s"}`;
103
191
  export function usageRows(summary, context, performance) {
104
192
  const t = summary?.tokens;
105
193
  const number = (value) => value === undefined ? "—" : formatTokens(value);
@@ -110,12 +198,9 @@ export function usageRows(summary, context, performance) {
110
198
  if (t && t.cache.write > 0)
111
199
  rows.push(["Cache Write", number(t.cache.write)]);
112
200
  rows.push(["Cache Rate", summary ? `${(summary.cacheRate * 100).toFixed(1)}%` : "—"], ["Total", number(summary?.total)]);
113
- if (summary?.costStatus === "complete")
114
- rows.push(["Cost", formatCost(summary.cost)]);
115
- if (summary?.costStatus === "partial")
116
- rows.push(["Cost", `${formatCost(summary.cost)} · partial`]);
117
- if (summary?.costStatus === "unavailable")
118
- rows.push(["Cost", "—"]);
201
+ const cost = summary && formatEstimatedCost(summary.cost, summary.costStatus);
202
+ if (cost !== undefined)
203
+ rows.push(["Cost", cost]);
119
204
  if (performance?.tps !== undefined && Number.isFinite(performance.tps)) {
120
205
  rows.push(["TPS", `${performance.tpsEstimated ? "~" : ""}${Math.max(0, performance.tps).toFixed(1)} tok/s`]);
121
206
  }
package/docs/pricing.md CHANGED
@@ -1,14 +1,14 @@
1
1
  # 内置官方价格快照
2
2
 
3
- 插件自 v0.3.0 起,在 OpenCode 当前模型目录没有可用于某条消息的完整价格时,使用本页所列的公开 API 价格补全。若 OpenCode 给出完整零价,而快照能完整计价,则使用快照价格。快照核验于 **2026-09-23**,共收录 79 个模型,金额均为美元 / 100 万 token。
3
+ 插件自 v0.3.0 起,在 OpenCode 当前模型目录没有可用于某条消息的完整价格时,使用本页所列的公开 API 价格补全。若 OpenCode 给出完整零价,而快照能完整计价,则使用快照价格。快照核验于 **2026-09-23**,共收录 86 个模型,金额均为美元 / 100 万 token。
4
4
 
5
5
  ## 规则
6
6
 
7
7
  - 每条 assistant 与 compaction 消息按自身记录的 `providerID`、模型 ID 和 token 用量分别计算。
8
8
  - OpenCode 当前解析出的完整适用非零价格优先。若完整适用价格为零,则尝试使用内置价格;只有快照能完整覆盖该消息实际使用的 token 类别时才覆盖零价,否则保留 OpenCode 的零价。OpenCode 价格不完整时,整条消息回退到内置价格,不混用两套费率。
9
- - 网关模型只通过精确厂商 ID、精确别名及已知包装格式匹配,例如 `openai/gpt-5.6-luna`、`us.anthropic.claude-opus-5` 和 `claude-opus-5@default`。匹配这些现有形式后,也会尝试仅移除模型 ID 的末尾 `-free` 或 `:free` 再精确匹配基础 ID,例如 `mimo-v2.6-flash-free`、`meta/muse-spark-1.3:free` 和 `muse-spark-1.3-contributor-free`;`-pro`、`-fast`、`-free-preview` 等其他后缀不会剥离。不按相似名称猜测。
9
+ - 网关模型只通过精确厂商 ID、精确别名及已知包装格式匹配,例如 `openai/gpt-5.6-luna`、`us.anthropic.claude-opus-5` 和 `claude-opus-5@default`。匹配这些现有形式后,也会尝试仅移除模型 ID 的末尾 `-free` 或 `:free` 再精确匹配基础 ID,例如 `mimo-v2.6-flash-free`、`meta/muse-spark-1.3:free` 和 `muse-spark-1.3-contributor-free`;`-pro`、`-fast`、`-free-preview` 等其他后缀不会剥离。单独列出的 Fast 型号仅按完整 ID 和厂商支持的 provider 精确匹配,不会回退到标准费率。不按相似名称猜测。
10
10
  - 目录首版只新增 2026-03-22 至 2026-09-22 发布、具有明确 token 费率的模型。后续版本只限制新增窗口,已经收录的条目不会因超过半年而自动删除。
11
- - 采用标准同步 API 的公开价;Meta Contributor 条件价按单独标注的来源记录。不含 Batch、Flex、Fast/Priority、区域溢价、网关加价、企业折扣、工具费和税费,因此 Cost 仍是估算,不代表账单。
11
+ - 采用同步 API 的公开价;Meta Contributor 条件价按单独标注的来源记录。仅收录下表明确列出的 Fast 型号,不含 Batch、Flex、其他 Fast/Priority、区域溢价、网关加价、企业折扣、工具费和税费,因此 Cost 仍是估算,不代表账单。
12
12
  - 任一有用量消息仍无法定价时,已知小计标记 `partial`;全部无法定价时显示 `—`。明确零价才显示 `$0.00`。
13
13
  - 促销价按核验日官网显示的费率收录(如 `gpt-5.6-sol`、MiniMax-M3 的"长期 5 折"标价),不用划线原价。
14
14
 
@@ -19,16 +19,23 @@
19
19
  | 模型 | 发布日期 | Input | Output | Cache Read | Cache Write | 长上下文档位 |
20
20
  | --- | --- | ---: | ---: | ---: | ---: | --- |
21
21
  | `gpt-5.5` | 2026-04-23 | 5 | 30 | 0.5 | — | >272K:10 / 45 / 1 / — |
22
+ | `gpt-5.5-fast`(Fast) | 2026-04-23 | 12.5 | 75 | 1.25 | — | >272K:无官方 Fast 费率 |
22
23
  | `gpt-5.5-pro` | 2026-04-23 | 30 | 180 | — | — | >272K:60 / 270 / — / — |
23
24
  | `gpt-5.6-sol` | 2026-07-09 | 4 | 20 | 0.4 | 5 | >272K:8 / 30 / 0.8 / 10 |
25
+ | `gpt-5.6-sol-fast`(Fast) | 2026-07-09 | 8 | 40 | 0.8 | 10 | >272K:16 / 60 / 1.6 / 20 |
24
26
  | `gpt-5.6-terra` | 2026-07-09 | 2 | 12 | 0.2 | 2.5 | >272K:4 / 18 / 0.4 / 5 |
27
+ | `gpt-5.6-terra-fast`(Fast) | 2026-07-09 | 4 | 24 | 0.4 | 5 | >272K:8 / 36 / 0.8 / 10 |
25
28
  | `gpt-5.6-luna` | 2026-07-09 | 0.2 | 1.2 | 0.02 | 0.25 | >272K:0.4 / 1.8 / 0.04 / 0.5 |
29
+ | `gpt-5.6-luna-fast`(Fast) | 2026-07-09 | 0.4 | 2.4 | 0.04 | 0.5 | >272K:0.8 / 3.6 / 0.08 / 1 |
26
30
  | `gpt-6-astra` | 2026-09-04 | 10 | 50 | 1 | 12.5 | >272K:20 / 75 / 2 / 25 |
31
+ | `gpt-6-astra-fast`(Fast) | 2026-09-04 | 20 | 100 | 2 | 25 | >272K:40 / 150 / 4 / 50 |
27
32
  | `gpt-6-sol` | 2026-09-22 | 2 | 10 | 0.2 | 2.5 | >272K:4 / 15 / 0.4 / 5 |
33
+ | `gpt-6-sol-fast`(Fast) | 2026-09-22 | 4 | 20 | 0.4 | 5 | >272K:8 / 30 / 0.8 / 10 |
28
34
  | `gpt-6-luna` | 2026-09-22 | 0.1 | 0.5 | 0.01 | 0.125 | >272K:0.2 / 0.75 / 0.02 / 0.25 |
35
+ | `gpt-6-luna-fast`(Fast) | 2026-09-22 | 0.2 | 1 | 0.02 | 0.25 | >272K:0.4 / 1.5 / 0.04 / 0.5 |
29
36
  | `chat-latest`(别名 `gpt-chat-latest`) | 2026-05-05 | 5 | 30 | 0.5 | — | — |
30
37
 
31
- `gpt-6-sol` 和 `gpt-6-luna` 的发布日期见 [OpenAI API 更新日志](https://developers.openai.com/api/docs/changelog)。表内 OpenAI 长上下文价格在传入 token 超过 272,000 时对整条请求生效。`gpt-5.6-sol` 的当前标准价是限期促销价;快照采用核验日官网显示的费率。`chat-latest` 是官方 ChatGPT SKU,OpenRouter 上的 `gpt-chat-latest` 作为精确别名指向同一条目。
38
+ `gpt-6-sol` 和 `gpt-6-luna` 的发布日期见 [OpenAI API 更新日志](https://developers.openai.com/api/docs/changelog)。表内 OpenAI 长上下文价格在传入 token 超过 272,000 时对整条请求生效。Fast 条目对应官方 Fast processing 费率;API 通过基础模型配合 `service_tier: "fast"` 选择该档,因此只有以对应 `-fast` ID 记录的 OpenAI 消息使用 Fast 价格,其他 provider 不匹配这些条目。官方未公布 `gpt-5.5` Fast 长上下文费率,故超过 272,000 个传入 token 时标记为不可估价,不沿用短上下文价。`gpt-5.6-sol` 的当前标准价是限期促销价;快照采用核验日官网显示的费率。`chat-latest` 是官方 ChatGPT SKU,OpenRouter 上的 `gpt-chat-latest` 作为精确别名指向同一条目。
32
39
 
33
40
  ## Anthropic
34
41
 
@@ -38,16 +45,21 @@
38
45
  | --- | --- | ---: | ---: | ---: | ---: |
39
46
  | `claude-opus-4-7` | 2026-04-14 | 5 | 25 | 0.5 | 6.25 |
40
47
  | `claude-opus-4-8` | 2026-05-28 | 5 | 25 | 0.5 | 6.25 |
48
+ | `claude-opus-4-8-fast`(Fast) | 2026-05-28 | 10 | 50 | 1 | 12.5 |
41
49
  | `claude-fable-5` | 2026-06-07 | 10 | 50 | 1 | 12.5 |
42
50
  | `claude-sonnet-5` | 2026-06-29 | 2 | 10 | 0.2 | 2.5 |
43
51
  | `claude-opus-5` | 2026-07-24 | 5 | 25 | 0.5 | 6.25 |
52
+ | `claude-opus-5-fast`(Fast) | 2026-07-24 | 10 | 50 | 1 | 12.5 |
44
53
  | `claude-fable-5-1` | 2026-09-01 | 10 | 50 | 0.25 | 12.5 |
45
54
  | `claude-opus-5-5` | 2026-09-22 | 4 | 20 | 0.2 | 5 |
55
+ | `claude-opus-5-5-fast`(Fast) | 2026-09-22 | 8 | 40 | 0.4 | 10 |
46
56
 
47
57
  Cache Write 采用官方 5 分钟写入价。OpenCode 的聚合字段不区分 5 分钟与 1 小时 TTL,无法从历史消息可靠应用 1 小时写入价。OpenRouter 的 `claude-opus-4.7`、`claude-opus-4.8`、`claude-opus-5.5` 圆点写法注册为对应条目的精确别名。
48
58
 
49
59
  `claude-opus-5-5` 的 Cache Read 是官方 0.05 倍基础输入价的特殊乘率(多数 Claude 模型为 0.1 倍),按官方标价 $0.20 / M 收录。
50
60
 
61
+ 表中 Claude Fast 条目仅适用于 Claude API 一方。Fast mode 的 Cache Read 和 5 分钟 Cache Write 分别按 Fast 输入价乘以官方缓存倍率计算;OpenCode 聚合的 Cache Write 无法区分 TTL,因此按 5 分钟写入价估算。Opus 5.5 的 Cache Read 为 Fast 输入价的 0.05 倍,其余表列 Fast 型号为 0.1 倍。
62
+
51
63
  ## Google
52
64
 
53
65
  来源:[Gemini Developer API Pricing](https://ai.google.dev/gemini-api/docs/pricing)
@@ -284,7 +296,7 @@ Fugu Ultra 的编排 token 已包含在 API 返回的 input/output 用量中,
284
296
 
285
297
  - Realtime、音频、图片、视频、embedding、按页、按分钟和按工具调用收费的模型无法仅由当前五类 token 用量准确计算。
286
298
  - `grok-4.20`、`grok-4.20-multi-agent` 发布于 2026-03-09,早于目录收录窗口起点 2026-03-22。
287
- - Fast/Priority 等加速档(`claude-opus-*-fast`、`gpt-5.6-*-pro`、`gpt-6-astra-pro`、MiniMax priority 档)不是标准同步价。
299
+ - Fast/Priority 等其他未列出的加速档(例如 `claude-opus-4-7-fast`、`gpt-5.5-pro-fast`、`gpt-6-astra-pro` 和 MiniMax priority 档)不收录;上表列出当前快照中官方定价页明确支持的 GPT 与 Claude Fast 型号。
288
300
  - 其他免费模型(Gemma 系列、`cohere/north-mini-code` 等开源或网关零价款)不设价格条目。
289
301
  - 无法从厂商官方页面确认标准 token 价格的模型不猜价,包括 OpenRouter stealth/alpha 匿名模型、`z-ai/glm-5v-turbo`、`qwen3.6-plus-preview`、腾讯 `hy3-preview` 与 `hy-mt2-*`、`kwaipilot/kat-coder-pro-v2.5` 及其 Air 版本、除上表 OpenCode Zen SKU 外的 `inclusionai/ling-3.0-*`、`bytedance-seed/seed-2.0-code` 等。这些模型目前只能查到 OpenRouter、AtlasCloud、DeepInfra 等网关或第三方报价,不能据此推断厂商标准价。
290
302
  - 浮动别名(`-latest` 指针与 OpenRouter `~` 前缀条目)随底层模型漂移,不收录固定价。
@@ -1,5 +1,53 @@
1
1
  # 验证记录
2
2
 
3
+ ## v0.4.0(`/usage` 原生弹窗)
4
+
5
+ 验证日期:2026-09-23。Node.js v22.23.2;`@opencode/plugin`、`@opencode/client` 等 SDK 精确锁定 2.0.11。弹窗布局参考本地 OpenCode 2.0.15 源码中的 `dialog-status.tsx`、`dialog-debug.tsx` 和 `dialog-shell-output.tsx`,通过插件公开的斜杠命令、弹窗、`session.hook("context")` 和 RPC 接口实现,并在真实 OpenCode 2.0.11 宿主中验证。
6
+
7
+ | 检查 | 结果 |
8
+ | --- | --- |
9
+ | `npm run typecheck` | 通过 |
10
+ | `npm test` | 62 项通过 |
11
+ | `npm run build` | 通过,输出 ESM 与类型声明 |
12
+ | `npm run test:smoke` | 通过,使用 `.tgz` 安装产物、真实 OpenCode 2.0.11 与本地模拟提供商 |
13
+
14
+ `/usage` 仅在会话页面注册为 CLI 斜杠/命令面板命令,打开原生弹窗而不向模型发送新消息。弹窗按当前查看会话展示最后一次已完成压缩后的上下文用量;五类 token 的占比以该条 assistant 消息的已用 context 为分母,窗口占用率以活动模型上下文上限为分母。会话树明细沿用侧边栏的完整分页、子代理汇总和分叉继承去重口径;按模型汇总的费用与全树 Cost 共用逐消息计价规则,分别处理免费、缺价、`partial` 及内置官方价格回退。
15
+
16
+ 自动化测试新增:五类 context 分类百分比、压缩边界与未知上限;混合模型和 compaction、价格回退、免费与缺价时逐模型费用对全树费用的核对;弹窗详细快照在子会话、刷新失败恢复和切换会话时的生命周期。打包集成测试确认空会话不会伪造上下文占用,真实 `/usage` 可打开与关闭、显示 1,270 token 当前上下文分类和模型费用;100 × 28 终端内滚动可到达模型费用;真实子代理合并后显示全树 5,080 token;切换活动模型后弹窗上限变为 32,000,历史消息费用仍列在原模型下。2.0.11 子代理视图最初处于宿主的子代理覆盖层,不提供斜杠输入;其全树面板验证继续由 `session.composer.top` 覆盖。
17
+
18
+ 新增 **Context Sources · estimated**:服务端只在 `context` 钩子中观察最近一次已组装的模型请求,按可识别的消息文本、系统工具定义/Code Mode 目录、系统提示词、技能文本/目录、MCP 指令/工具以及其他内容分别估算,持久化的仅是六类数值、模型和采样时间;CLI 通过位置限定的 RPC 读取。估算使用 UTF-8 字节数近似,不是提供商真实 token 归因,媒体负载及提供商包装无法准确计量,也不会与含输出/推理的实测 Context 相加一致。未捕获过请求的会话显示不可用;服务端插件或 RPC 不可用时仍显示实测统计。测试覆盖六类独立归因、占比分母、无技能/MCP 时的零值、Code Mode 后续项目指令、工具结果媒体忽略、无效 RPC 数据,以及 RPC 缓慢/失败时不阻塞实测用量或串会话。真实 2.0.11 打包集成验证了请求钩子记录与 CLI 弹窗读取均生效。
19
+
20
+ ## v0.4.0(`/usage` 弹窗信息架构重构)
21
+
22
+ 验证日期:2026-09-23。Node.js v22.23.2;SDK 仍精确锁定 2.0.11。本轮只改弹窗的数据映射与呈现,统计与定价逻辑不变。
23
+
24
+ | 检查 | 结果 |
25
+ | --- | --- |
26
+ | `npm run typecheck` | 通过 |
27
+ | `npm test` | 62 项通过 |
28
+ | `npm run build` | 通过 |
29
+ | `npm run test:smoke` | 通过,真实 OpenCode 2.0.11 + 本地模拟提供商 |
30
+
31
+ 信息架构由 `Context / Context Sources / Session Tree / Cost by Model` 改为 **Context Window / Last Request / Context Breakdown / Session / By Model**:Context Window 只呈现上下文占用(分母为活动模型上限)并带条形图;原 Context 内的五类 token 迁入 Last Request 并补充该次请求的缓存率;`Context Sources · estimated` 改名 Context Breakdown;`Session Tree` 改名 Session,因为面板本来就是全树汇总而非树结构;`Cost by Model` 改名 By Model。标题下方新增一行显示会话名与活动模型(多模型时显示模型数量),正文不再重复模型名,弹窗改用宿主 `centered` 选项垂直居中。
32
+
33
+ 默认紧凑模式使用 K/M 缩写(< 1,000 精确、< 1,000,000 一位小数 K、≥ 1,000,000 两位小数 M,四舍五入不会出现 `1000.0K`),隐藏零值行,并把 Context Breakdown 的两类工具合并为 `Tools` 行按占比降序;按 `d` 切换详细模式,恢复精确数字、零值行、`System Tools` / `MCP Tools` 拆分以及 Session 逐类明细(含 Calls)。Session 单行汇总为 `steps · calls · tokens · cached% · cost`,其中 calls 与 By Model 各行同源,可与逐模型行核对。条形图宽度、Last Request 双列与内容宽度随宿主 `large` 档(88 列)收敛,窄终端自动改为单列并收窄条长。
34
+
35
+ 自动化测试新增:`formatCompact` 边界、`bar` 填充与越界、`requestRows` 的紧凑/详细两态、`summarize().calls` 与 `summaryRows`、`countLabel` 单复数、`breakdownRows` 的工具合并、降序与零值策略。打包集成测试确认紧凑与详细两种模式可切换、`Used / Limit` 显示 `1.3K / 128.0K (1.0%)`、Context Breakdown 显示 `Tools` 合并行与条形图、Session 单行汇总、By Model 逐模型费用,以及 100 × 28 终端内滚动仍可到达模型费用。
36
+
37
+ 尚未实现(属于后续评估):真实的分叉/分支树统计、进度条以外的窗口告警色阶、公开价格目录。
38
+
39
+ ## v0.3.3(GPT 与 Claude Fast 价格)
40
+
41
+ 验证日期:2026-09-23。根据 [OpenAI API Pricing](https://developers.openai.com/api/docs/pricing) 与 [Claude Platform Pricing](https://platform.claude.com/docs/en/about-claude/pricing),为当前快照中官方支持 Fast mode 的 GPT-5.5、GPT-5.6 Sol/Terra/Luna、GPT-6 Astra/Sol/Luna,以及 Claude Opus 4.8/5/5.5 分别加入独立 Fast 条目。Fast 费率按官方长上下文档位与缓存倍率记录;GPT-5.5 Fast 未公布长上下文费率,超过 272,000 个传入 token 时显示不可估价。条目分别限制到官方支持 Fast mode 的 OpenAI 与 Anthropic provider;快照由 76 个模型增至 86 个。
42
+
43
+ | 检查 | 结果 |
44
+ | --- | --- |
45
+ | `npm run typecheck` | 通过 |
46
+ | `npm test` | 51 项通过,覆盖 Fast 费率、缓存倍率、长上下文边界及普通版价格隔离 |
47
+ | `npm run build` | 通过 |
48
+
49
+ 本轮只改动价格目录、对应测试及价格说明,未重跑打包集成 smoke 测试。
50
+
3
51
  ## v0.3.2(GPT-6 Sol / Luna 价格更新,SDK 2.0.11)
4
52
 
5
53
  验证日期:2026-09-23。根据 [OpenAI API 更新日志](https://developers.openai.com/api/docs/changelog),`gpt-6-sol` 和 `gpt-6-luna` 于 2026-09-22 发布;[官方定价页](https://developers.openai.com/api/docs/pricing)列出两者的 Standard 短、长上下文四类 token 费率。内置快照由 72 个模型增至 74 个。根据 [Sol](https://developers.openai.com/api/docs/models/gpt-6-sol) 与 [Luna](https://developers.openai.com/api/docs/models/gpt-6-luna) 模型说明,传入 token 超过 272,000 时才使用长上下文价格;同轮也将已有 OpenAI 条目的边界从 271,999 校正为 272,000。
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@chenlongapps/opencode-token-usage",
3
- "version": "0.3.3",
3
+ "version": "0.4.0",
4
4
  "description": "Session-tree token usage, live performance, context window and estimated cost for OpenCode 2",
5
5
  "keywords": [
6
6
  "opencode",