codex-copilot-dx 0.9.3 → 0.9.4

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: cached input tokens divided by 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. A rate is `—` when input tokens are zero or any included record lacks valid paired input/cache counts. 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.4",
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 = cached / input tokens. — = incomplete usage data or no input tokens.</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 * 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,13 @@ 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),
93
89
  output_tokens: finite(values.output_tokens),
94
90
  total_tokens: finite(values.total_tokens),
95
91
  };
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,12 @@ 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
+ }
273
280
  }
274
281
  return summary;
275
282
  }
@@ -298,13 +305,26 @@ function tableNumber(n) {
298
305
  return Number.isFinite(n) ? n.toLocaleString("en-US") : undefined;
299
306
  }
300
307
 
301
- function cacheReadTokens(usage = {}) {
308
+ export function cacheReadTokens(usage = {}) {
302
309
  const values = [usage.cache_read_input_tokens, usage.cached_input_tokens]
303
310
  .filter((value) => Number.isFinite(value));
304
311
  const value = values.length > 0 ? values.reduce((total, current) => total + current, 0) : undefined;
305
312
  return Number.isFinite(value) ? value : undefined;
306
313
  }
307
314
 
315
+ export function usageCacheHitRate(usage = {}) {
316
+ const input = usage.input_tokens;
317
+ const cached = cacheReadTokens(usage);
318
+ if (usage.cache_hit_unknown_requests > 0 || !Number.isFinite(input) || input <= 0
319
+ || !Number.isFinite(cached) || cached < 0 || cached > input) return null;
320
+ return cached / input;
321
+ }
322
+
323
+ function cacheHitPercent(usage) {
324
+ const rate = usageCacheHitRate(usage);
325
+ return rate === null ? "—" : `${(rate * 100).toFixed(1)}%`;
326
+ }
327
+
308
328
  function formatPlainUsageSummary(summary, filePath, { sanitize = false } = {}) {
309
329
  const lines = [`Usage log: ${filePath}`];
310
330
  if (summary.requests === 0) {
@@ -314,11 +334,12 @@ function formatPlainUsageSummary(summary, filePath, { sanitize = false } = {}) {
314
334
  lines.push(
315
335
  `Requests: ${fmt(summary.requests)}`,
316
336
  `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)}`,
337
+ `Cache hit: ${cacheHitPercent(summary.totals)} (cached/input tokens)`,
317
338
  "",
318
339
  "By model:",
319
340
  );
320
341
  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)}`);
342
+ 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
343
  }
323
344
  return (sanitize ? lines.map((line) => terminalCell(line, { fallback: "" })) : lines).join("\n");
324
345
  }
@@ -338,11 +359,12 @@ function usageTableRow(model, usage) {
338
359
  cacheRead: tableNumber(cacheReadTokens(usage)),
339
360
  output: tableNumber(usage.output_tokens),
340
361
  total: tableNumber(usage.total_tokens),
362
+ cacheHit: cacheHitPercent(usage),
341
363
  };
342
364
  }
343
365
 
344
366
  function usageDetailLine(model, usage) {
345
- return `${terminalCell(model)}: input=${fmt(usage.input_tokens)} cache_read=${fmt(cacheReadTokens(usage))} output=${fmt(usage.output_tokens)}`;
367
+ return `${terminalCell(model)}: input=${fmt(usage.input_tokens)} cache_read=${fmt(cacheReadTokens(usage))} output=${fmt(usage.output_tokens)} cache_hit=${cacheHitPercent(usage)}`;
346
368
  }
347
369
 
348
370
  function formatTableUsageSummary(summary, filePath, output) {
@@ -362,13 +384,19 @@ function formatTableUsageSummary(summary, filePath, output) {
362
384
  .sort(compareModelUsage);
363
385
  const totalUsage = { requests: summary.requests, ...summary.totals };
364
386
  const rows = [usageTableRow("TOTAL", totalUsage), ...modelUsage.map(({ model, row }) => usageTableRow(model, row))];
365
- const table = formatResponsiveCliTable({
366
- columns,
367
- compactColumns,
387
+ let table = formatResponsiveCliTable({
388
+ columns: [...columns, { key: "cacheHit", label: "HIT %", align: "right" }],
389
+ compactColumns: columns,
368
390
  rows,
369
391
  width: cliOutputWidth(output),
370
392
  gap: 1,
371
393
  });
394
+ const rateFits = !table.compact;
395
+ if (table.overflow) {
396
+ table = formatResponsiveCliTable({ columns, compactColumns, rows, width: cliOutputWidth(output), gap: 1 });
397
+ } else {
398
+ table.compact = false;
399
+ }
372
400
  const lines = [`Usage log: ${safePath}`, "", table.output];
373
401
  if (table.compact) {
374
402
  lines.push(
@@ -377,6 +405,10 @@ function formatTableUsageSummary(summary, filePath, output) {
377
405
  usageDetailLine("TOTAL", totalUsage),
378
406
  ...modelUsage.map(({ model, row }) => usageDetailLine(model, row)),
379
407
  );
408
+ } else if (!rateFits) {
409
+ lines.push("", "Cache hit (cached/input tokens):",
410
+ `TOTAL: ${cacheHitPercent(totalUsage)}`,
411
+ ...modelUsage.map(({ model, row }) => `${terminalCell(model)}: ${cacheHitPercent(row)}`));
380
412
  }
381
413
  return { output: lines.join("\n"), overflow: table.overflow };
382
414
  }