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 +2 -0
- package/package.json +1 -1
- package/src/dashboard/index.html +2 -1
- package/src/dashboard/ui.js +6 -4
- package/src/dashboard-api.mjs +4 -7
- package/src/usage.mjs +47 -8
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
package/src/dashboard/index.html
CHANGED
|
@@ -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="
|
|
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">
|
package/src/dashboard/ui.js
CHANGED
|
@@ -308,7 +308,9 @@ async function loadModels() {
|
|
|
308
308
|
}
|
|
309
309
|
|
|
310
310
|
function usageValues(row) {
|
|
311
|
-
|
|
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",
|
|
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",
|
|
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",
|
|
339
|
+
emptyTable("usage-body", 7, "No usage summary available.");
|
|
338
340
|
} finally {
|
|
339
341
|
button.disabled = false;
|
|
340
342
|
}
|
package/src/dashboard-api.mjs
CHANGED
|
@@ -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:
|
|
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
|
-
|
|
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
|
}
|