codex-copilot-dx 0.9.3 → 0.9.5

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/README.md CHANGED
@@ -107,6 +107,8 @@ This performs a fresh, read-only GitHub Copilot model-directory lookup without s
107
107
 
108
108
  Interactive terminals use aligned tables for `models`, `auth status`, and `usage`. Redirected or piped output retains the plain-text layout for scripts; use `--format table` or `--format plain` when a command offers an explicit format override.
109
109
 
110
+ `ccdx usage` and the dashboard Usage table show token cache hit rate for each model and the aggregate: recorded cached input tokens divided by recorded input tokens, weighted by token count rather than averaging request percentages. This describes the upstream prompt cache; the local history and image-transform caches are separate. Incomplete historical records do not hide a computable ratio: `~` marks a rate derived from incomplete counts. A rate is `—` when input tokens are zero, usable totals are missing, or token counts are invalid. Older logs may omit explicit zero cache counts; new Responses usage records retain reported zeros. CLI tables keep existing token columns on narrow terminals and show rates below the table when the extra column does not fit.
111
+
110
112
  ### Auto-review model
111
113
 
112
114
  ```bash
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codex-copilot-dx",
3
- "version": "0.9.3",
3
+ "version": "0.9.5",
4
4
  "description": "Use Codex App with GitHub Copilot",
5
5
  "author": "Dale Xiao",
6
6
  "type": "module",
@@ -63,7 +63,8 @@
63
63
  <section class="panel dashboard-section" aria-label="Usage by model">
64
64
  <div class="panel-heading"><h2>USAGE</h2><div class="heading-actions"><span id="usage-state">READING LOG</span><button id="refresh-usage" type="button">Refresh</button></div></div>
65
65
  <p id="usage-note" class="table-note">Aggregating local usage metadata…</p>
66
- <div class="table-wrap"><table class="usage-table"><thead><tr><th scope="col">MODEL</th><th scope="col">CALLS</th><th scope="col">INPUT</th><th scope="col">CACHE READ</th><th scope="col">OUTPUT</th><th scope="col">TOTAL</th></tr></thead><tbody id="usage-body"><tr><td colspan="6">Loading…</td></tr></tbody></table></div>
66
+ <div class="table-wrap"><table class="usage-table"><thead><tr><th scope="col">MODEL</th><th scope="col">CALLS</th><th scope="col">INPUT</th><th scope="col">CACHE READ</th><th scope="col">OUTPUT</th><th scope="col">TOTAL</th><th scope="col">CACHE HIT</th></tr></thead><tbody id="usage-body"><tr><td colspan="7">Loading…</td></tr></tbody></table></div>
67
+ <p class="hint">Cache hit = recorded cached / input tokens. ~ = incomplete history. — = unavailable or invalid counts.</p>
67
68
  </section>
68
69
 
69
70
  <section class="lower" aria-label="Recent failures">
@@ -308,7 +308,9 @@ async function loadModels() {
308
308
  }
309
309
 
310
310
  function usageValues(row) {
311
- return [row.model, number(row.requests), number(row.input_tokens), number(row.cache_read_tokens), number(row.output_tokens), number(row.total_tokens)];
311
+ const rate = Number.isFinite(row.cache_hit_rate) && row.cache_hit_rate >= 0 && row.cache_hit_rate <= 1
312
+ ? `${row.cache_hit_rate_partial ? "~" : ""}${(row.cache_hit_rate * 100).toFixed(1)}%` : "—";
313
+ return [row.model, number(row.requests), number(row.input_tokens), number(row.cache_read_tokens), number(row.output_tokens), number(row.total_tokens), rate];
312
314
  }
313
315
 
314
316
  async function loadUsage() {
@@ -316,7 +318,7 @@ async function loadUsage() {
316
318
  button.disabled = true;
317
319
  text("usage-state", "READING LOG");
318
320
  text("usage-note", "Aggregating local usage metadata…");
319
- emptyTable("usage-body", 6, "Loading…");
321
+ emptyTable("usage-body", 7, "Loading…");
320
322
  try {
321
323
  const response = await fetch("/_ccdx/ui/usage", { cache: "no-store", headers: { "X-CCDX-Dashboard": "1" }, signal: AbortSignal.timeout(15000) });
322
324
  const data = await response.json();
@@ -327,14 +329,14 @@ async function loadUsage() {
327
329
  body.append(tableRow(usageValues(data.total), { total: true }));
328
330
  for (const row of data.rows) body.append(tableRow(usageValues(row)));
329
331
  } else {
330
- emptyTable("usage-body", 6, "No usage records.");
332
+ emptyTable("usage-body", 7, "No usage records.");
331
333
  }
332
334
  text("usage-state", "LOCAL LOG");
333
335
  text("usage-note", `${number(data.total.requests)} records / ${number(data.rows.length)} of ${number(data.model_count)} models shown`);
334
336
  } catch (error) {
335
337
  text("usage-state", "LOG UNAVAILABLE");
336
338
  text("usage-note", String(error.message || error).slice(0, 180));
337
- emptyTable("usage-body", 6, "No usage summary available.");
339
+ emptyTable("usage-body", 7, "No usage summary available.");
338
340
  } finally {
339
341
  button.disabled = false;
340
342
  }
@@ -9,7 +9,7 @@ import {
9
9
  renderTerminalAnimationFrame,
10
10
  TERMINAL_ANIMATION_THEMES,
11
11
  } from "./terminal-animation.mjs";
12
- import { summarizeUsageLogs } from "./usage.mjs";
12
+ import { cacheReadTokens, summarizeUsageLogs, usageCacheHitRate } from "./usage.mjs";
13
13
  import { readUserSettings, terminalAnimationPreference, writeTerminalAnimationTheme } from "./user-settings.mjs";
14
14
 
15
15
  const ANIMATION_PATH = "/_ccdx/ui/animation";
@@ -79,17 +79,14 @@ function finite(value) {
79
79
  return Number.isFinite(value) ? Math.max(0, value) : null;
80
80
  }
81
81
 
82
- function cacheReadTokens(usage = {}) {
83
- const values = [usage.cache_read_input_tokens, usage.cached_input_tokens].filter(Number.isFinite);
84
- return values.length ? values.reduce((sum, value) => sum + value, 0) : null;
85
- }
86
-
87
82
  function usageRow(model, values) {
88
83
  return {
89
84
  model: safeText(model, 100),
90
85
  requests: finite(values.requests),
91
86
  input_tokens: finite(values.input_tokens),
92
- cache_read_tokens: cacheReadTokens(values),
87
+ cache_read_tokens: cacheReadTokens(values) ?? null,
88
+ cache_hit_rate: usageCacheHitRate(values),
89
+ cache_hit_rate_partial: values.cache_hit_unknown_requests > 0,
93
90
  output_tokens: finite(values.output_tokens),
94
91
  total_tokens: finite(values.total_tokens),
95
92
  };
package/src/usage.mjs CHANGED
@@ -54,9 +54,10 @@ function hasPositiveTokenValue(usage) {
54
54
 
55
55
  function normalizeResponsesUsage(usage) {
56
56
  if (!usage || typeof usage !== "object") return undefined;
57
+ const cachedInput = numberOrUndefined(usage.cached_input_tokens ?? usage.input_tokens_details?.cached_tokens);
57
58
  const out = compactObject({
58
59
  input_tokens: positiveNumber(usage.input_tokens),
59
- cached_input_tokens: positiveNumber(usage.cached_input_tokens ?? usage.input_tokens_details?.cached_tokens),
60
+ cached_input_tokens: cachedInput >= 0 ? cachedInput : undefined,
60
61
  output_tokens: positiveNumber(usage.output_tokens),
61
62
  reasoning_output_tokens: positiveNumber(usage.reasoning_output_tokens ?? usage.output_tokens_details?.reasoning_tokens),
62
63
  total_tokens: positiveNumber(usage.total_tokens),
@@ -251,7 +252,7 @@ export async function readUsageRecords(filePath = usageLogPath(), { warn = conso
251
252
  }
252
253
 
253
254
  function addUsageTotals(target, usage = {}) {
254
- for (const [key, value] of Object.entries(usage)) {
255
+ for (const [key, value] of Object.entries(usage || {})) {
255
256
  if (Number.isFinite(value)) target[key] = (target[key] || 0) + value;
256
257
  }
257
258
  }
@@ -270,6 +271,19 @@ export function summarizeUsage(records) {
270
271
  const modelTotals = summary.byModel[model];
271
272
  modelTotals.requests += 1;
272
273
  addUsageTotals(modelTotals, record.usage);
274
+ const input = record.usage?.input_tokens;
275
+ const cached = cacheReadTokens(record.usage || {});
276
+ if (!Number.isFinite(input) || input < 0 || !Number.isFinite(cached) || cached < 0 || cached > input) {
277
+ summary.totals.cache_hit_unknown_requests = (summary.totals.cache_hit_unknown_requests || 0) + 1;
278
+ modelTotals.cache_hit_unknown_requests = (modelTotals.cache_hit_unknown_requests || 0) + 1;
279
+ }
280
+ const cacheCounts = [record.usage?.cached_input_tokens, record.usage?.cache_read_input_tokens];
281
+ if ((input != null && (!Number.isFinite(input) || input < 0))
282
+ || cacheCounts.some((value) => value != null && (!Number.isFinite(value) || value < 0))
283
+ || (Number.isFinite(input) && Number.isFinite(cached) && cached > input)) {
284
+ summary.totals.cache_hit_invalid_requests = (summary.totals.cache_hit_invalid_requests || 0) + 1;
285
+ modelTotals.cache_hit_invalid_requests = (modelTotals.cache_hit_invalid_requests || 0) + 1;
286
+ }
273
287
  }
274
288
  return summary;
275
289
  }
@@ -298,13 +312,26 @@ function tableNumber(n) {
298
312
  return Number.isFinite(n) ? n.toLocaleString("en-US") : undefined;
299
313
  }
300
314
 
301
- function cacheReadTokens(usage = {}) {
315
+ export function cacheReadTokens(usage = {}) {
302
316
  const values = [usage.cache_read_input_tokens, usage.cached_input_tokens]
303
317
  .filter((value) => Number.isFinite(value));
304
318
  const value = values.length > 0 ? values.reduce((total, current) => total + current, 0) : undefined;
305
319
  return Number.isFinite(value) ? value : undefined;
306
320
  }
307
321
 
322
+ export function usageCacheHitRate(usage = {}) {
323
+ const input = usage.input_tokens;
324
+ const cached = cacheReadTokens(usage);
325
+ if (usage.cache_hit_invalid_requests > 0 || !Number.isFinite(input) || input <= 0
326
+ || !Number.isFinite(cached) || cached < 0 || cached > input) return null;
327
+ return cached / input;
328
+ }
329
+
330
+ function cacheHitPercent(usage) {
331
+ const rate = usageCacheHitRate(usage);
332
+ return rate === null ? "—" : `${usage.cache_hit_unknown_requests > 0 ? "~" : ""}${(rate * 100).toFixed(1)}%`;
333
+ }
334
+
308
335
  function formatPlainUsageSummary(summary, filePath, { sanitize = false } = {}) {
309
336
  const lines = [`Usage log: ${filePath}`];
310
337
  if (summary.requests === 0) {
@@ -314,11 +341,12 @@ function formatPlainUsageSummary(summary, filePath, { sanitize = false } = {}) {
314
341
  lines.push(
315
342
  `Requests: ${fmt(summary.requests)}`,
316
343
  `Tokens: input=${fmt(summary.totals.input_tokens)} cache_read=${fmt(cacheReadTokens(summary.totals))} output=${fmt(summary.totals.output_tokens)} total=${fmt(summary.totals.total_tokens)}`,
344
+ `Cache hit: ${cacheHitPercent(summary.totals)} (cached/input tokens)`,
317
345
  "",
318
346
  "By model:",
319
347
  );
320
348
  for (const [model, row] of Object.entries(summary.byModel)) {
321
- lines.push(` ${model}: requests=${fmt(row.requests)} input=${fmt(row.input_tokens)} cache_read=${fmt(cacheReadTokens(row))} output=${fmt(row.output_tokens)} total=${fmt(row.total_tokens)}`);
349
+ lines.push(` ${model}: requests=${fmt(row.requests)} input=${fmt(row.input_tokens)} cache_read=${fmt(cacheReadTokens(row))} output=${fmt(row.output_tokens)} total=${fmt(row.total_tokens)} cache_hit=${cacheHitPercent(row)}`);
322
350
  }
323
351
  return (sanitize ? lines.map((line) => terminalCell(line, { fallback: "" })) : lines).join("\n");
324
352
  }
@@ -338,11 +366,12 @@ function usageTableRow(model, usage) {
338
366
  cacheRead: tableNumber(cacheReadTokens(usage)),
339
367
  output: tableNumber(usage.output_tokens),
340
368
  total: tableNumber(usage.total_tokens),
369
+ cacheHit: cacheHitPercent(usage),
341
370
  };
342
371
  }
343
372
 
344
373
  function usageDetailLine(model, usage) {
345
- return `${terminalCell(model)}: input=${fmt(usage.input_tokens)} cache_read=${fmt(cacheReadTokens(usage))} output=${fmt(usage.output_tokens)}`;
374
+ return `${terminalCell(model)}: input=${fmt(usage.input_tokens)} cache_read=${fmt(cacheReadTokens(usage))} output=${fmt(usage.output_tokens)} cache_hit=${cacheHitPercent(usage)}`;
346
375
  }
347
376
 
348
377
  function formatTableUsageSummary(summary, filePath, output) {
@@ -362,13 +391,19 @@ function formatTableUsageSummary(summary, filePath, output) {
362
391
  .sort(compareModelUsage);
363
392
  const totalUsage = { requests: summary.requests, ...summary.totals };
364
393
  const rows = [usageTableRow("TOTAL", totalUsage), ...modelUsage.map(({ model, row }) => usageTableRow(model, row))];
365
- const table = formatResponsiveCliTable({
366
- columns,
367
- compactColumns,
394
+ let table = formatResponsiveCliTable({
395
+ columns: [...columns, { key: "cacheHit", label: "HIT %", align: "right" }],
396
+ compactColumns: columns,
368
397
  rows,
369
398
  width: cliOutputWidth(output),
370
399
  gap: 1,
371
400
  });
401
+ const rateFits = !table.compact;
402
+ if (table.overflow) {
403
+ table = formatResponsiveCliTable({ columns, compactColumns, rows, width: cliOutputWidth(output), gap: 1 });
404
+ } else {
405
+ table.compact = false;
406
+ }
372
407
  const lines = [`Usage log: ${safePath}`, "", table.output];
373
408
  if (table.compact) {
374
409
  lines.push(
@@ -377,6 +412,10 @@ function formatTableUsageSummary(summary, filePath, output) {
377
412
  usageDetailLine("TOTAL", totalUsage),
378
413
  ...modelUsage.map(({ model, row }) => usageDetailLine(model, row)),
379
414
  );
415
+ } else if (!rateFits) {
416
+ lines.push("", "Cache hit (cached/input tokens):",
417
+ `TOTAL: ${cacheHitPercent(totalUsage)}`,
418
+ ...modelUsage.map(({ model, row }) => `${terminalCell(model)}: ${cacheHitPercent(row)}`));
380
419
  }
381
420
  return { output: lines.join("\n"), overflow: table.overflow };
382
421
  }