codex-copilot-dx 0.9.2 → 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 +4 -2
- package/package.json +1 -1
- package/src/codex-model-catalog.mjs +22 -11
- package/src/dashboard/index.html +2 -1
- package/src/dashboard/ui.js +6 -4
- package/src/dashboard-api.mjs +3 -7
- package/src/usage.mjs +40 -8
package/README.md
CHANGED
|
@@ -26,9 +26,9 @@ Codex Auto-review uses the hidden `codex-auto-review` model ID. CCDX maps it to
|
|
|
26
26
|
|
|
27
27
|
Codex App requests a versioned, capability-rich model catalog that is different from Copilot's raw model list. CCDX reads the complete catalog bundled with the installed Codex App and preserves every other model entry and capability. It exposes each managed GPT-6 entry only when Copilot advertises that exact model as enabled, selectable, OpenAI-owned, and Responses-capable.
|
|
28
28
|
|
|
29
|
-
When an installed 0.155.x catalog does not yet contain `gpt-6-sol` or `gpt-6-luna`, CCDX derives their client-compatible entries from that catalog's GPT-6 Astra schema and the published OpenAI Codex model definitions at commit `0a2eb4696c26ac33204bcd255721ab30220a4774`.
|
|
29
|
+
When an installed 0.155.x catalog does not yet contain `gpt-6-sol` or `gpt-6-luna`, CCDX derives their client-compatible entries from that catalog's GPT-6 Astra schema and the published OpenAI Codex model definitions at commit `0a2eb4696c26ac33204bcd255721ab30220a4774`. When an installed 0.158+ catalog contains `gpt-6-sol` but not `gpt-6.1-sol`, CCDX derives only the missing 6.1 entry from that client's Sol schema and OpenAI Codex model identity/defaults at commit `ab84d71f5767e4a565ce81c2c426287cb48c7918`. Fallback reasoning controls are intersected with Copilot's advertised efforts; every other bundled entry retains its established behavior. A newer client-provided entry supplies its schema and future fields while CCDX still gates its visibility, speed tiers, and 6.1 Sol reasoning controls against Copilot.
|
|
30
30
|
|
|
31
|
-
CCDX exposes GPT-6 Standard paths and removes unverified speed-tier metadata.
|
|
31
|
+
CCDX exposes GPT-6 Standard paths and removes unverified speed-tier metadata. Existing GPT-6 bundled reasoning-effort options remain unchanged; 6.1 Sol is limited to efforts advertised by Copilot. Fast follows the exact upstream-variant rule above. Versioned model discovery matches the bundled catalog to the client's `client_version`, including the nested Codex CLI binary in current macOS App bundles, caches it by application binary identity, and reloads it after an App update. If a matching usable catalog cannot be loaded, the versioned models request returns `503`; an unversioned request returns the Copilot-style list.
|
|
32
32
|
|
|
33
33
|
`/v1/models` still attempts live discovery first. With a usable in-process catalog, it limits that upstream wait to 1.5 seconds (or a shorter configured upstream timeout) before falling back to last-known-good data. Without a catalog it retains the regular upstream timeout. Received HTTP `401`/`403` responses remain errors, not cached successes; fallback does not grant inference access. This does not change the explicitly live `ccdx models` command.
|
|
34
34
|
|
|
@@ -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
|
@@ -4,11 +4,17 @@ import os from "node:os";
|
|
|
4
4
|
import path from "node:path";
|
|
5
5
|
import { CODEX_GPT6_MODEL } from "./models.mjs";
|
|
6
6
|
|
|
7
|
-
//
|
|
8
|
-
// at commit 0a2eb4696c26ac33204bcd255721ab30220a4774.
|
|
9
|
-
//
|
|
10
|
-
//
|
|
7
|
+
// GPT-6 Sol/Luna compatibility metadata comes from OpenAI Codex's public models
|
|
8
|
+
// catalog at commit 0a2eb4696c26ac33204bcd255721ab30220a4774. GPT-6.1 Sol
|
|
9
|
+
// identity and defaults come from commit ab84d71f5767e4a565ce81c2c426287cb48c7918.
|
|
10
|
+
// The installed client's Astra or Sol entry supplies its schema and instructions.
|
|
11
11
|
const GPT6_COMPATIBILITY_SPECS = new Map([
|
|
12
|
+
["gpt-6.1-sol", Object.freeze({
|
|
13
|
+
display_name: "GPT-6.1-Sol",
|
|
14
|
+
description: "Latest workhorse model for coding and everyday work.",
|
|
15
|
+
default_reasoning_level: "low",
|
|
16
|
+
priority: 1,
|
|
17
|
+
})],
|
|
12
18
|
["gpt-6-sol", Object.freeze({
|
|
13
19
|
display_name: "GPT-6-Sol",
|
|
14
20
|
description: "Workhorse model for coding and everyday work.",
|
|
@@ -33,8 +39,12 @@ export { CODEX_GPT6_MODEL };
|
|
|
33
39
|
export const CODEX_APP_BINARY_PATHS = [
|
|
34
40
|
"/Applications/Codex.app/Contents/Resources/codex",
|
|
35
41
|
"/Applications/ChatGPT.app/Contents/Resources/codex",
|
|
42
|
+
"/Applications/Codex.app/Contents/Resources/codex-cli/CodexCLI.app/Contents/MacOS/codex",
|
|
43
|
+
"/Applications/ChatGPT.app/Contents/Resources/codex-cli/CodexCLI.app/Contents/MacOS/codex",
|
|
36
44
|
path.join(os.homedir(), "Applications", "Codex.app", "Contents", "Resources", "codex"),
|
|
37
45
|
path.join(os.homedir(), "Applications", "ChatGPT.app", "Contents", "Resources", "codex"),
|
|
46
|
+
path.join(os.homedir(), "Applications", "Codex.app", "Contents", "Resources", "codex-cli", "CodexCLI.app", "Contents", "MacOS", "codex"),
|
|
47
|
+
path.join(os.homedir(), "Applications", "ChatGPT.app", "Contents", "Resources", "codex-cli", "CodexCLI.app", "Contents", "MacOS", "codex"),
|
|
38
48
|
];
|
|
39
49
|
export const CODEX_MODEL_CATALOG_TIMEOUT_MS = 5_000;
|
|
40
50
|
export const CODEX_MODEL_CATALOG_MAX_BUFFER = 8 * 1024 * 1024;
|
|
@@ -96,7 +106,7 @@ function catalogModelForCopilot(model, copilotModels, { filterReasoning = false
|
|
|
96
106
|
const patched = {
|
|
97
107
|
...structuredClone(model),
|
|
98
108
|
visibility: "list",
|
|
99
|
-
supported_reasoning_levels: filterReasoning && reasoning.size
|
|
109
|
+
supported_reasoning_levels: filterReasoning && reasoning.size && Array.isArray(model.supported_reasoning_levels)
|
|
100
110
|
? model.supported_reasoning_levels.filter(({ effort }) => reasoning.has(effort))
|
|
101
111
|
: model.supported_reasoning_levels,
|
|
102
112
|
};
|
|
@@ -111,17 +121,18 @@ function catalogModelForCopilot(model, copilotModels, { filterReasoning = false
|
|
|
111
121
|
}
|
|
112
122
|
|
|
113
123
|
function compatibilityModel(codexCatalog, slug) {
|
|
114
|
-
const
|
|
124
|
+
const isGpt61Sol = slug === "gpt-6.1-sol";
|
|
125
|
+
const template = codexCatalog.models.find((model) => model.slug === (isGpt61Sol ? "gpt-6-sol" : CODEX_GPT6_MODEL));
|
|
115
126
|
const spec = GPT6_COMPATIBILITY_SPECS.get(slug);
|
|
116
|
-
if (!
|
|
127
|
+
if (!template || !spec || (isGpt61Sol && !Array.isArray(template.supported_reasoning_levels))) return null;
|
|
117
128
|
return {
|
|
118
|
-
...structuredClone(
|
|
129
|
+
...structuredClone(template),
|
|
119
130
|
...spec,
|
|
120
131
|
slug,
|
|
121
132
|
supports_parallel_tool_calls: true,
|
|
122
|
-
multi_agent_reasoning_effort: null,
|
|
133
|
+
multi_agent_reasoning_effort: isGpt61Sol ? "xhigh" : null,
|
|
123
134
|
supports_experimental_context: false,
|
|
124
|
-
minimal_client_version: "0.155.0",
|
|
135
|
+
minimal_client_version: isGpt61Sol ? "0.158.0" : "0.155.0",
|
|
125
136
|
};
|
|
126
137
|
}
|
|
127
138
|
|
|
@@ -141,7 +152,7 @@ export function buildCodexModelResponse({ copilotModels, codexCatalog } = {}) {
|
|
|
141
152
|
const models = codexCatalog.models.map((model) => {
|
|
142
153
|
if (!MANAGED_GPT6_MODELS.has(model.slug)) return model;
|
|
143
154
|
if (!isEligibleCopilotGpt6(copilotModels, model.slug)) return { ...model, visibility: "hide" };
|
|
144
|
-
return catalogModelForCopilot(model, copilotModels);
|
|
155
|
+
return catalogModelForCopilot(model, copilotModels, { filterReasoning: model.slug === "gpt-6.1-sol" });
|
|
145
156
|
});
|
|
146
157
|
for (const slug of GPT6_COMPATIBILITY_SPECS.keys()) {
|
|
147
158
|
if (bundledSlugs.has(slug) || !isEligibleCopilotGpt6(copilotModels, slug)) continue;
|
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 = 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">
|
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 * 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,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:
|
|
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
|
-
|
|
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
|
}
|