ccus-cli 0.2.10 → 0.2.12

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.
@@ -8,6 +8,7 @@ exports.summarizeCodexSessionUsage = summarizeCodexSessionUsage;
8
8
  exports.summarizeCodexSessionUsageByDay = summarizeCodexSessionUsageByDay;
9
9
  const promises_1 = __importDefault(require("node:fs/promises"));
10
10
  const node_path_1 = __importDefault(require("node:path"));
11
+ const api_equivalent_cost_1 = require("./api-equivalent-cost");
11
12
  const paths_1 = require("./paths");
12
13
  const time_1 = require("./time");
13
14
  function isRecord(value) {
@@ -19,6 +20,13 @@ function getNumber(value) {
19
20
  function getString(value) {
20
21
  return typeof value === "string" && value.length > 0 ? value : null;
21
22
  }
23
+ function splitInputTokens(usage) {
24
+ const cacheReadInputTokens = getNumber(usage.cached_input_tokens);
25
+ return {
26
+ inputTokens: Math.max(0, getNumber(usage.input_tokens) - cacheReadInputTokens),
27
+ cacheReadInputTokens,
28
+ };
29
+ }
22
30
  /** Guardian 是 Codex Desktop 的动作安全审查子代理,不代表用户请求或业务模型调用。 */
23
31
  function isGuardianRollout(content) {
24
32
  let offset = 0;
@@ -178,8 +186,7 @@ async function findActiveCodexSessionFiles(start, end) {
178
186
  /**
179
187
  * 从 `token_count` 事件的 `info.last_token_usage` 取 token 用量对象(缺失返回 null)。
180
188
  * 必须用 last_token_usage(本次增量),不能用 total_token_usage(会话累计,会重复计)。
181
- * 注意:Codex `input_tokens` 含缓存命中(`cached_input_tokens` 是其子集),
182
- * 累加 inputTokens 时要减去 cached 得到净输入,对齐 Claude 的 `input_tokens` 口径。
189
+ * `cached_input_tokens` `input_tokens` 的子集;普通输入需扣除缓存输入并钳制为非负数。
183
190
  */
184
191
  function extractLastTokenUsage(payload) {
185
192
  if (!isRecord(payload.info)) {
@@ -204,19 +211,28 @@ function summarizeRollout(content, start, end) {
204
211
  inputTokens: 0,
205
212
  outputTokens: 0,
206
213
  cacheReadInputTokens: 0,
214
+ apiEquivalentCost: (0, api_equivalent_cost_1.emptyApiEquivalentCost)(),
207
215
  };
216
+ let currentModel = null;
208
217
  for (const line of lines) {
209
218
  try {
210
219
  const record = JSON.parse(line);
211
- if (!isRecord(record) || !timestampInRange(getString(record.timestamp), start, end)) {
220
+ if (!isRecord(record) || !isRecord(record.payload)) {
212
221
  continue;
213
222
  }
214
- if (record.type !== "event_msg" || !isRecord(record.payload)) {
223
+ const payload = record.payload;
224
+ if (record.type === "turn_context") {
225
+ if (typeof payload.model === "string") {
226
+ currentModel = payload.model;
227
+ }
228
+ continue;
229
+ }
230
+ const timestamp = getString(record.timestamp);
231
+ if (!timestampInRange(timestamp, start, end) || record.type !== "event_msg") {
215
232
  continue;
216
233
  }
217
- const payload = record.payload;
218
234
  if (payload.type === "task_started" && typeof payload.turn_id === "string") {
219
- const ms = new Date(getString(record.timestamp) ?? "").getTime();
235
+ const ms = new Date(timestamp ?? "").getTime();
220
236
  if (Number.isFinite(ms)) {
221
237
  result.turns.push({ turnId: payload.turn_id, ms });
222
238
  }
@@ -224,12 +240,23 @@ function summarizeRollout(content, start, end) {
224
240
  if (payload.type === "token_count") {
225
241
  const usage = extractLastTokenUsage(payload);
226
242
  if (usage) {
227
- // 净输入 = input_tokens - cached_input_tokens(input_tokens 含缓存命中,减去对齐 Claude 口径)。
228
- const netInputTokens = Math.max(0, getNumber(usage.input_tokens) - getNumber(usage.cached_input_tokens));
243
+ const { inputTokens, cacheReadInputTokens } = splitInputTokens(usage);
244
+ const outputTokens = getNumber(usage.output_tokens);
229
245
  result.apiRequestCount += 1;
230
- result.inputTokens += netInputTokens;
231
- result.outputTokens += getNumber(usage.output_tokens);
232
- result.cacheReadInputTokens += getNumber(usage.cached_input_tokens);
246
+ result.inputTokens += inputTokens;
247
+ result.outputTokens += outputTokens;
248
+ result.cacheReadInputTokens += cacheReadInputTokens;
249
+ result.apiEquivalentCost = (0, api_equivalent_cost_1.mergeApiEquivalentCosts)([
250
+ result.apiEquivalentCost,
251
+ (0, api_equivalent_cost_1.priceApiRequest)({
252
+ provider: "codex",
253
+ timestamp: timestamp,
254
+ model: currentModel,
255
+ inputTokens,
256
+ outputTokens,
257
+ cacheReadInputTokens,
258
+ }),
259
+ ]);
233
260
  }
234
261
  }
235
262
  }
@@ -240,7 +267,7 @@ function summarizeRollout(content, start, end) {
240
267
  return result;
241
268
  }
242
269
  /**
243
- * 从标准 Codex 与 Orca 本地 session rollout 统计消息数、请求数和 token 用量。
270
+ * 从标准 Codex 与 Orca 本地 session rollout 统计消息数、请求数、token 用量和标准 API 等效成本。
244
271
  * Codex Desktop 的 guardian 安全审查 rollout 整体排除,避免把内部审批轮次计为用户使用量。
245
272
  *
246
273
  * 消息数 = task_started 的 distinct turn_id(跨文件去重)。重放副本会让同一 turn_id 出现在多个文件,
@@ -254,6 +281,7 @@ async function summarizeCodexSessionUsage(start, end) {
254
281
  let inputTokens = 0;
255
282
  let outputTokens = 0;
256
283
  let cacheReadInputTokens = 0;
284
+ let apiEquivalentCost = (0, api_equivalent_cost_1.emptyApiEquivalentCost)();
257
285
  for (const group of groups) {
258
286
  try {
259
287
  const content = await readMergedRollout(group);
@@ -271,6 +299,7 @@ async function summarizeCodexSessionUsage(start, end) {
271
299
  inputTokens += parsed.inputTokens;
272
300
  outputTokens += parsed.outputTokens;
273
301
  cacheReadInputTokens += parsed.cacheReadInputTokens;
302
+ apiEquivalentCost = (0, api_equivalent_cost_1.mergeApiEquivalentCosts)([apiEquivalentCost, parsed.apiEquivalentCost]);
274
303
  }
275
304
  catch {
276
305
  continue;
@@ -282,12 +311,13 @@ async function summarizeCodexSessionUsage(start, end) {
282
311
  inputTokens,
283
312
  outputTokens,
284
313
  cacheReadInputTokens,
314
+ apiEquivalentCost,
285
315
  matchedFileCount: groups.length,
286
316
  codexDataDir,
287
317
  };
288
318
  }
289
319
  /**
290
- * 按天汇总 Codex session rollout 中的消息数、请求数和 token 用量。
320
+ * 按天汇总 Codex session rollout 中的消息数、请求数、token 用量和标准 API 等效成本。
291
321
  * 与周汇总一致,排除 Codex Desktop 的 guardian 安全审查 rollout。
292
322
  *
293
323
  * 消息数同 weekly:先全局 Map<turn_id, minMs> 去重,再按 minMs 的本地日归桶(保证 weekly = Σ daily、
@@ -307,6 +337,7 @@ async function summarizeCodexSessionUsageByDay(start, end) {
307
337
  inputTokens: 0,
308
338
  outputTokens: 0,
309
339
  cacheReadInputTokens: 0,
340
+ apiEquivalentCost: (0, api_equivalent_cost_1.emptyApiEquivalentCost)(),
310
341
  };
311
342
  daily.set(date, current);
312
343
  }
@@ -318,20 +349,27 @@ async function summarizeCodexSessionUsageByDay(start, end) {
318
349
  continue;
319
350
  }
320
351
  const lines = content.split(/\r?\n/).map((line) => line.trim()).filter((line) => line.length > 0);
352
+ let currentModel = null;
321
353
  for (const line of lines) {
322
354
  try {
323
355
  const record = JSON.parse(line);
324
- if (!isRecord(record)) {
356
+ if (!isRecord(record) || !isRecord(record.payload)) {
357
+ continue;
358
+ }
359
+ const payload = record.payload;
360
+ if (record.type === "turn_context") {
361
+ if (typeof payload.model === "string") {
362
+ currentModel = payload.model;
363
+ }
325
364
  continue;
326
365
  }
327
366
  const timestamp = getString(record.timestamp);
328
367
  if (!timestampInRange(timestamp, start, end)) {
329
368
  continue;
330
369
  }
331
- if (record.type !== "event_msg" || !isRecord(record.payload)) {
370
+ if (record.type !== "event_msg") {
332
371
  continue;
333
372
  }
334
- const payload = record.payload;
335
373
  const ms = new Date(timestamp).getTime();
336
374
  if (payload.type === "task_started" && typeof payload.turn_id === "string") {
337
375
  if (Number.isFinite(ms)) {
@@ -345,12 +383,23 @@ async function summarizeCodexSessionUsageByDay(start, end) {
345
383
  const usage = extractLastTokenUsage(payload);
346
384
  if (usage) {
347
385
  const current = ensureDay((0, time_1.localDateKey)(new Date(ms)));
348
- // summarizeRollout:累加净输入(input - cached),对齐 Claude 口径。
349
- const netInputTokens = Math.max(0, getNumber(usage.input_tokens) - getNumber(usage.cached_input_tokens));
386
+ const { inputTokens, cacheReadInputTokens } = splitInputTokens(usage);
387
+ const outputTokens = getNumber(usage.output_tokens);
350
388
  current.apiRequestCount += 1;
351
- current.inputTokens += netInputTokens;
352
- current.outputTokens += getNumber(usage.output_tokens);
353
- current.cacheReadInputTokens += getNumber(usage.cached_input_tokens);
389
+ current.inputTokens += inputTokens;
390
+ current.outputTokens += outputTokens;
391
+ current.cacheReadInputTokens += cacheReadInputTokens;
392
+ current.apiEquivalentCost = (0, api_equivalent_cost_1.mergeApiEquivalentCosts)([
393
+ current.apiEquivalentCost,
394
+ (0, api_equivalent_cost_1.priceApiRequest)({
395
+ provider: "codex",
396
+ timestamp: timestamp,
397
+ model: currentModel,
398
+ inputTokens,
399
+ outputTokens,
400
+ cacheReadInputTokens,
401
+ }),
402
+ ]);
354
403
  }
355
404
  }
356
405
  }
@@ -0,0 +1,15 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.renderDashboardPage = renderDashboardPage;
4
+ const api_pricing_table_1 = require("./api-pricing-table");
5
+ /** 两类 dashboard serve 共用的页面路由;null 表示 404。 */
6
+ async function renderDashboardPage(pathname, renderDashboard, generatedAt = new Date()) {
7
+ if (pathname === "/") {
8
+ return renderDashboard();
9
+ }
10
+ if (pathname === api_pricing_table_1.API_PRICING_PAGE_PATH) {
11
+ return (0, api_pricing_table_1.buildApiPricingPage)(generatedAt);
12
+ }
13
+ return null;
14
+ }
15
+ //# sourceMappingURL=dashboard-pages.js.map
@@ -5,6 +5,7 @@ exports.bucketizeEvents = bucketizeEvents;
5
5
  exports.buildDashboardHtml = buildDashboardHtml;
6
6
  const aggregate_1 = require("./aggregate");
7
7
  const chart_assets_1 = require("./chart-assets");
8
+ const api_pricing_table_1 = require("./api-pricing-table");
8
9
  const payload_1 = require("./payload");
9
10
  const time_1 = require("./time");
10
11
  /** 所有插入到 HTML/SVG 的动态文本都先转义,避免本地页面被注入内容。 */
@@ -216,6 +217,36 @@ function renderDailyMessages(points) {
216
217
  function statValue(value, suffix = "%") {
217
218
  return value === null ? "--" : `${value.toFixed(1)}${suffix}`;
218
219
  }
220
+ function formatCostUsd(value) {
221
+ return `$${value < 0.01 ? value.toFixed(4) : value.toFixed(2)}`;
222
+ }
223
+ function costDisplay(result) {
224
+ if (result.pricedApiRequestCount === 0 && result.unpricedApiRequestCount > 0) {
225
+ return { value: "不可用", note: `${result.unpricedApiRequestCount} 个请求未定价` };
226
+ }
227
+ const amount = formatCostUsd(result.estimatedUsd ?? 0);
228
+ if (result.unpricedApiRequestCount > 0) {
229
+ return { value: `≥ ${amount}`, note: `${result.unpricedApiRequestCount} 个请求未定价,结果不完整` };
230
+ }
231
+ return { value: amount, note: `${result.pricedApiRequestCount} 个请求已定价` };
232
+ }
233
+ function renderApiEquivalentCostCard(costs) {
234
+ if (!costs) {
235
+ return "";
236
+ }
237
+ const display = costDisplay(costs.total);
238
+ return `<article class="panel stat-card">
239
+ <h2>合计等效 API 成本</h2>
240
+ <p class="stat-value">${escapeHtml(display.value)}</p>
241
+ <p class="stat-note">${escapeHtml(display.note)}</p>
242
+ <a class="pricing-link" href="${api_pricing_table_1.API_PRICING_PAGE_FILE}" target="_blank" rel="noopener noreferrer">查看当前价格表</a>
243
+ </article>`;
244
+ }
245
+ function renderApiEquivalentCostNote(costs, pricingCatalogVersion) {
246
+ return costs
247
+ ? `<p class="muted">标准同步 API 等效成本,不是订阅或实际账单 · 事件时间计价 · 目录 ${escapeHtml(pricingCatalogVersion ?? "不可用")}</p>`
248
+ : "";
249
+ }
219
250
  /** 最近事件表帮助回看 statusline 在某一时刻到底输出了什么。 */
220
251
  function renderRecentEvents(events) {
221
252
  const rows = [...events]
@@ -263,7 +294,7 @@ function renderRecentEvents(events) {
263
294
  *
264
295
  * 整个 dashboard 保持“单文件可打开”,方便导出和直接分享本地结果。
265
296
  */
266
- function buildDashboardHtml(events, rangeLabel, start, end, dailyUserMessages = []) {
297
+ function buildDashboardHtml(events, rangeLabel, start, end, dailyUserMessages = [], apiEquivalentCost = null, pricingCatalogVersion = null) {
267
298
  const summary = summarizeEvents(events);
268
299
  const buckets = bucketizeEvents(events, start, end, pickBucketMinutes(start, end));
269
300
  const totalUserMessages = dailyUserMessages.reduce((sum, point) => sum + point.userMessageCount, 0);
@@ -373,6 +404,7 @@ function buildDashboardHtml(events, rangeLabel, start, end, dailyUserMessages =
373
404
  color: var(--muted);
374
405
  font-size: 14px;
375
406
  }
407
+ .pricing-link { display: inline-block; margin-top: 12px; color: var(--accent); font-size: 13px; }
376
408
  .panel-header {
377
409
  display: flex;
378
410
  align-items: end;
@@ -464,7 +496,9 @@ function buildDashboardHtml(events, rangeLabel, start, end, dailyUserMessages =
464
496
  <p class="stat-value">${combinedUserMessages}</p>
465
497
  <p class="stat-note">窗口内每日真实用户请求数合计(Claude ${totalUserMessages} · Codex ${codexTotalUserMessages})</p>
466
498
  </article>
499
+ ${renderApiEquivalentCostCard(apiEquivalentCost)}
467
500
  </section>
501
+ ${renderApiEquivalentCostNote(apiEquivalentCost, pricingCatalogVersion)}
468
502
  ${renderChart(buckets)}
469
503
  ${renderDailyMessages(dailyUserMessages)}
470
504
  ${renderRecentEvents(events)}
@@ -28,6 +28,10 @@ function quoteCsv(value) {
28
28
  function toMillions(value) {
29
29
  return value === null ? null : (0, time_1.roundNumber)(value / 1_000_000, 6);
30
30
  }
31
+ /** 等效 API 成本以美元输出,最多保留 6 位小数;不可用时留空。 */
32
+ function formatApiEquivalentCost(value) {
33
+ return value === null ? null : (0, time_1.roundNumber)(value, 6);
34
+ }
31
35
  /**
32
36
  * 清理控制字符,并防止 Excel/Sheets 把值解释成公式。
33
37
  *
@@ -219,6 +223,8 @@ function buildAggregatedDailyCsv(rows) {
219
223
  "sevenDayCumulativeUsagePct",
220
224
  "uniqueSessions",
221
225
  "uniqueWorkspaces",
226
+ "estimatedApiEquivalentCostUsd",
227
+ "pricingCatalogVersion",
222
228
  ];
223
229
  const lines = rows.map((row) => toCsvLine([
224
230
  row.personKey,
@@ -236,6 +242,8 @@ function buildAggregatedDailyCsv(rows) {
236
242
  row.sevenDayCumulativeUsagePct,
237
243
  row.uniqueSessions,
238
244
  row.uniqueWorkspaces,
245
+ formatApiEquivalentCost(row.estimatedApiEquivalentCostUsd),
246
+ row.pricingCatalogVersion,
239
247
  ]));
240
248
  return [header.join(","), ...lines].join("\n");
241
249
  }
@@ -257,6 +265,8 @@ function buildAggregatedWeeklyCsv(rows) {
257
265
  "sevenDayCumulativeUsagePct",
258
266
  "uniqueSessions",
259
267
  "uniqueWorkspaces",
268
+ "estimatedApiEquivalentCostUsd",
269
+ "pricingCatalogVersion",
260
270
  ];
261
271
  const lines = rows.map((row) => toCsvLine([
262
272
  row.personKey,
@@ -274,6 +284,8 @@ function buildAggregatedWeeklyCsv(rows) {
274
284
  row.sevenDayCumulativeUsagePct,
275
285
  row.uniqueSessions,
276
286
  row.uniqueWorkspaces,
287
+ formatApiEquivalentCost(row.estimatedApiEquivalentCostUsd),
288
+ row.pricingCatalogVersion,
277
289
  ]));
278
290
  return [header.join(","), ...lines].join("\n");
279
291
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ccus-cli",
3
- "version": "0.2.10",
3
+ "version": "0.2.12",
4
4
  "description": "Claude Code statusline usage logger and dashboard CLI",
5
5
  "type": "commonjs",
6
6
  "bin": {
@@ -9,6 +9,7 @@
9
9
  "files": [
10
10
  "dist/cli.js",
11
11
  "dist/lib/**/*.js",
12
+ "dist/lib/api-pricing-catalog.json",
12
13
  "dist/types.js",
13
14
  "dist/vendor/**"
14
15
  ],
@@ -16,7 +17,7 @@
16
17
  "build": "tsc -p tsconfig.json && node scripts/copy-vendor.js",
17
18
  "prepublishOnly": "npm run build",
18
19
  "test": "node --test --test-concurrency 1 \"dist/test/**/*.test.js\"",
19
- "test:src": "node --import tsx --test --test-concurrency 1 src/test/payload.test.ts src/test/dashboard.test.ts src/test/export.test.ts src/test/storage.test.ts src/test/claude.test.ts src/test/aggregate.test.ts src/test/aggregate-dashboard.test.ts src/test/install.test.ts src/test/debug.test.ts src/test/time.test.ts src/test/update-check.test.ts src/test/sync.test.ts src/test/scheduler.test.ts src/test/api-mode.test.ts src/test/codex-fetcher.test.ts src/test/codex-sessions.test.ts src/test/sessions.test.ts src/test/codex-install.test.ts src/test/cli-codex-notify.test.ts src/test/cli-codex-hook.test.ts src/test/cli-install-codex.test.ts",
20
+ "test:src": "node --import tsx --test --test-concurrency 1 src/test/payload.test.ts src/test/dashboard.test.ts src/test/export.test.ts src/test/storage.test.ts src/test/api-equivalent-cost.test.ts src/test/claude.test.ts src/test/aggregate.test.ts src/test/aggregate-dashboard.test.ts src/test/install.test.ts src/test/debug.test.ts src/test/time.test.ts src/test/update-check.test.ts src/test/sync.test.ts src/test/scheduler.test.ts src/test/api-mode.test.ts src/test/codex-fetcher.test.ts src/test/codex-sessions.test.ts src/test/sessions.test.ts src/test/codex-install.test.ts src/test/cli-codex-notify.test.ts src/test/cli-codex-hook.test.ts src/test/cli-install-codex.test.ts",
20
21
  "smoke:api": "node scripts/smoke-api.mjs"
21
22
  },
22
23
  "keywords": [