From a087ecd2d9f4b393f6ade9281ffeaab97589bd0f Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Sat, 6 Jun 2026 18:02:37 +0000
Subject: [PATCH 1/2] Update token usage summary wording
Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
---
actions/setup/js/parse_mcp_gateway_log.cjs | 21 +++--
.../setup/js/parse_mcp_gateway_log.test.cjs | 25 +++---
actions/setup/js/parse_token_usage.cjs | 36 ++++++++-
actions/setup/js/parse_token_usage.test.cjs | 81 +++++++++++++++----
4 files changed, 122 insertions(+), 41 deletions(-)
diff --git a/actions/setup/js/parse_mcp_gateway_log.cjs b/actions/setup/js/parse_mcp_gateway_log.cjs
index f2d3258c02a..bdf6145abde 100644
--- a/actions/setup/js/parse_mcp_gateway_log.cjs
+++ b/actions/setup/js/parse_mcp_gateway_log.cjs
@@ -5,7 +5,7 @@ const fs = require("fs");
const { getErrorMessage } = require("./error_helpers.cjs");
const { displayDirectories } = require("./display_file_helpers.cjs");
const { ERR_PARSE, ERR_SYSTEM } = require("./error_codes.cjs");
-const { computeEffectiveTokens, formatET, formatModelEmojiAlias } = require("./effective_tokens.cjs");
+const { computeEffectiveTokens, formatModelEmojiAlias } = require("./effective_tokens.cjs");
const { computeInferenceAIC, formatAIC } = require("./model_costs.cjs");
const { generateUnifiedTimelineSummary } = require("./unified_timeline.cjs");
@@ -162,9 +162,8 @@ function parseTokenUsageJsonl(jsonlContent) {
/**
* Generates a markdown summary section for token usage data.
- * Renders one row per turn in chronological order with per-turn delta ET (ΔET),
- * a running compounded ET total (ET), per-turn delta AIC (ΔAIC), a running
- * compounded AIC total (AIC), followed by an aggregate totals row.
+ * Renders one row per request in chronological order with per-request AI credits,
+ * a running AI credits total, followed by an aggregate totals row and legend.
* @param {{totalInputTokens: number, totalOutputTokens: number, totalCacheReadTokens: number, totalCacheWriteTokens: number, totalRequests: number, totalDurationMs: number, totalEffectiveTokens: number, totalAIC: number, byModel: Object, entries: Array} | null} summary
* @returns {string} Markdown section, or empty string if no data
*/
@@ -172,29 +171,27 @@ function generateTokenUsageSummary(summary) {
if (!summary || summary.totalRequests === 0) return "";
const lines = [];
- lines.push("| # | Alias | Input | Output | Cache Read | Cache Write | ΔET | ET | ΔAIC | AIC | Duration |");
- lines.push("|--:|-------|------:|-------:|-----------:|------------:|----:|---:|-----:|----:|---------:|");
+ lines.push("| # | Alias | Input | Output | Cache Read | Cache Write | ΔAI Credits | AI Credits | Duration |");
+ lines.push("|--:|-------|------:|-------:|-----------:|------------:|-------------:|-----------:|---------:|");
const entries = summary.entries || [];
- let compoundedET = 0;
let compoundedAIC = 0;
for (let i = 0; i < entries.length; i++) {
const entry = entries[i];
- const deltaET = Math.round(entry.deltaET || 0);
- compoundedET += deltaET;
const deltaAIC = entry.deltaAIC || 0;
compoundedAIC += deltaAIC;
lines.push(
- `| ${i + 1} | ${formatModelEmojiAlias(entry.model) || entry.model} | ${entry.inputTokens.toLocaleString()} | ${entry.outputTokens.toLocaleString()} | ${entry.cacheReadTokens.toLocaleString()} | ${entry.cacheWriteTokens.toLocaleString()} | ${formatET(deltaET)} | ${formatET(compoundedET)} | ${formatAIC(deltaAIC)} | ${formatAIC(compoundedAIC)} | ${formatDurationMs(entry.durationMs)} |`
+ `| ${i + 1} | ${formatModelEmojiAlias(entry.model) || entry.model} | ${entry.inputTokens.toLocaleString()} | ${entry.outputTokens.toLocaleString()} | ${entry.cacheReadTokens.toLocaleString()} | ${entry.cacheWriteTokens.toLocaleString()} | ${formatAIC(deltaAIC)} | ${formatAIC(compoundedAIC)} | ${formatDurationMs(entry.durationMs)} |`
);
}
- const totalET = formatET(Math.round(summary.totalEffectiveTokens || 0));
const totalAIC = formatAIC(summary.totalAIC || 0);
lines.push(
- `| **Total** | | **${summary.totalInputTokens.toLocaleString()}** | **${summary.totalOutputTokens.toLocaleString()}** | **${summary.totalCacheReadTokens.toLocaleString()}** | **${summary.totalCacheWriteTokens.toLocaleString()}** | | **${totalET}** | | **${totalAIC}** | **${formatDurationMs(summary.totalDurationMs)}** |`
+ `| **Total** | | **${summary.totalInputTokens.toLocaleString()}** | **${summary.totalOutputTokens.toLocaleString()}** | **${summary.totalCacheReadTokens.toLocaleString()}** | **${summary.totalCacheWriteTokens.toLocaleString()}** | | **${totalAIC}** | **${formatDurationMs(summary.totalDurationMs)}** |`
);
+ lines.push("");
+ lines.push("Legend: `Alias` shows the model shorthand used in the table. `ΔAI Credits` is the per-request cost, and `AI Credits` is the running total computed with the current AI credits pricing model.");
lines.push("");
return lines.join("\n") + "\n";
diff --git a/actions/setup/js/parse_mcp_gateway_log.test.cjs b/actions/setup/js/parse_mcp_gateway_log.test.cjs
index ebe7d771ded..750b866b805 100644
--- a/actions/setup/js/parse_mcp_gateway_log.test.cjs
+++ b/actions/setup/js/parse_mcp_gateway_log.test.cjs
@@ -1597,7 +1597,7 @@ not-json
test("renders header and table columns", () => {
const summary = parseTokenUsageJsonl(JSON.stringify({ model: "claude-sonnet-4-6", provider: "anthropic", input_tokens: 100, output_tokens: 200, cache_read_tokens: 5000, cache_write_tokens: 3000, duration_ms: 2500 }));
const md = generateTokenUsageSummary(summary);
- expect(md).toContain("| # | Alias | Input | Output | Cache Read | Cache Write | ΔET | ET | ΔAIC | AIC | Duration |");
+ expect(md).toContain("| # | Alias | Input | Output | Cache Read | Cache Write | ΔAI Credits | AI Credits | Duration |");
expect(md).toContain("◉ sonnet46");
});
@@ -1627,20 +1627,21 @@ not-json
expect(firstIdx).toBeLessThan(secondIdx);
});
- test("includes ΔET and ET columns in table", () => {
+ test("does not include effective token columns in table", () => {
const content = JSON.stringify({ model: "m", input_tokens: 100, output_tokens: 200, cache_read_tokens: 0, cache_write_tokens: 0, duration_ms: 1000 });
const summary = parseTokenUsageJsonl(content);
const md = generateTokenUsageSummary(summary);
- expect(md).toContain("| ΔET |");
- expect(md).toContain("| ET |");
+ expect(md).not.toContain("| ΔET |");
+ expect(md).not.toContain("| ET |");
});
- test("includes ΔAIC and AIC columns in table header", () => {
+ test("includes AI credits columns in table header", () => {
const content = JSON.stringify({ model: "m", input_tokens: 100, output_tokens: 200, cache_read_tokens: 0, cache_write_tokens: 0, duration_ms: 1000 });
const summary = parseTokenUsageJsonl(content);
const md = generateTokenUsageSummary(summary);
- expect(md).toContain("| ΔAIC |");
- expect(md).toContain("| AIC |");
+ expect(md).toContain("| ΔAI Credits |");
+ expect(md).toContain("| AI Credits |");
+ expect(md).not.toContain("effective token");
});
test("renders AIC value in totals row for known model with pricing", () => {
@@ -1665,21 +1666,23 @@ not-json
expect(Math.abs(totalAIC - summary.totalAIC)).toBeLessThan(0.0001);
});
- test("does not render a dangling ET footer line", () => {
+ test("includes an AI credits legend", () => {
const content = JSON.stringify({ model: "m", input_tokens: 100, output_tokens: 200, cache_read_tokens: 0, cache_write_tokens: 0, duration_ms: 1000 });
const summary = parseTokenUsageJsonl(content);
expect(summary.totalEffectiveTokens).toBeGreaterThan(0);
const md = generateTokenUsageSummary(summary);
- expect(md).toContain("| ET |");
- expect(md).not.toContain("●");
+ expect(md).toContain("Legend:");
+ expect(md).toContain("current AI credits pricing model");
+ expect(md).not.toContain("effective token");
});
- test("does not include cache efficiency or an ET footer line", () => {
+ test("does not include cache efficiency or effective token wording", () => {
const content = JSON.stringify({ model: "m", input_tokens: 100, output_tokens: 10, cache_read_tokens: 900, cache_write_tokens: 0, duration_ms: 100 });
const summary = parseTokenUsageJsonl(content);
const md = generateTokenUsageSummary(summary);
expect(md).not.toContain("●");
expect(md).not.toContain("Cache efficiency");
+ expect(md).not.toContain("effective token");
});
test("compounded ET equals sum of per-turn delta ET values", () => {
diff --git a/actions/setup/js/parse_token_usage.cjs b/actions/setup/js/parse_token_usage.cjs
index b98e6798598..e79bf4d6808 100644
--- a/actions/setup/js/parse_token_usage.cjs
+++ b/actions/setup/js/parse_token_usage.cjs
@@ -92,6 +92,35 @@ function getSummaryTitle() {
return title && title.trim() ? title.trim() : DEFAULT_SUMMARY_TITLE;
}
+/**
+ * Builds the token usage section for the GitHub step summary.
+ * @param {string} title
+ * @param {string} markdown
+ * @returns {string}
+ */
+function buildStepSummarySection(title, markdown) {
+ return `### ${title}\n\n\nPer-request AI credits and token totals
\n\n${markdown} \n\n`;
+}
+
+/**
+ * Appends the token usage section to GITHUB_STEP_SUMMARY when available.
+ * Falls back to the Actions summary API when the summary path is unavailable.
+ * @param {string} title
+ * @param {string} markdown
+ * @returns {Promise}
+ */
+async function appendStepSummarySection(title, markdown) {
+ const section = buildStepSummarySection(title, markdown);
+ const summaryPath = process.env.GITHUB_STEP_SUMMARY;
+ if (summaryPath) {
+ fs.appendFileSync(summaryPath, section, "utf8");
+ return;
+ }
+
+ core.summary.addRaw(section, true);
+ await core.summary.write();
+}
+
/**
* Main function to parse token usage and write the step summary.
*/
@@ -111,13 +140,12 @@ async function main() {
core.info("Token usage file contained no valid entries");
return;
}
-
const markdown = generateTokenUsageSummary(summary);
if (markdown.length > 0) {
- core.summary.addDetails(getSummaryTitle(), "\n\n" + markdown);
+ if (markdown.length > 0) {
+ }
}
- await core.summary.write();
core.info("Token usage summary appended to step summary");
// Write agent_usage.json so the aggregated totals are bundled in the agent
@@ -173,6 +201,8 @@ if (typeof module !== "undefined" && module.exports) {
extractRequestId,
readDedupedTokenUsage,
getSummaryTitle,
+ buildStepSummarySection,
+ appendStepSummarySection,
TOKEN_USAGE_AUDIT_PATH,
TOKEN_USAGE_PATH,
TOKEN_USAGE_PATHS,
diff --git a/actions/setup/js/parse_token_usage.test.cjs b/actions/setup/js/parse_token_usage.test.cjs
index 9809171c855..02ae0edf38e 100644
--- a/actions/setup/js/parse_token_usage.test.cjs
+++ b/actions/setup/js/parse_token_usage.test.cjs
@@ -5,7 +5,19 @@ const fs = require("fs");
const path = require("path");
const os = require("os");
-const { main, getReadableTokenUsagePaths, extractRequestId, readDedupedTokenUsage, getSummaryTitle, TOKEN_USAGE_AUDIT_PATH, TOKEN_USAGE_PATH, TOKEN_USAGE_PATHS, AGENT_USAGE_PATH, DEFAULT_SUMMARY_TITLE } = require("./parse_token_usage.cjs");
+const {
+ main,
+ getReadableTokenUsagePaths,
+ extractRequestId,
+ readDedupedTokenUsage,
+ getSummaryTitle,
+ buildStepSummarySection,
+ TOKEN_USAGE_AUDIT_PATH,
+ TOKEN_USAGE_PATH,
+ TOKEN_USAGE_PATHS,
+ AGENT_USAGE_PATH,
+ DEFAULT_SUMMARY_TITLE,
+} = require("./parse_token_usage.cjs");
describe("parse_token_usage", () => {
const singleEntry = JSON.stringify({
@@ -56,6 +68,7 @@ describe("parse_token_usage", () => {
beforeEach(() => {
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "parse-token-usage-test-"));
delete process.env.GH_AW_TOKEN_USAGE_SUMMARY_TITLE;
+ delete process.env.GITHUB_STEP_SUMMARY;
mockCore = {
info: vi.fn(),
@@ -159,7 +172,8 @@ describe("parse_token_usage", () => {
await main();
- expect(mockCore.summary.addDetails).toHaveBeenCalledWith("Token Usage", expect.stringContaining("| Alias |"));
+ expect(mockCore.summary.addRaw).toHaveBeenCalledWith(expect.stringContaining("### Token Usage"), true);
+ expect(mockCore.summary.addRaw).toHaveBeenCalledWith(expect.stringContaining("| Alias |"), true);
expect(mockCore.summary.write).toHaveBeenCalled();
expect(mockCore.info).toHaveBeenCalledWith(expect.stringContaining("Token usage summary appended"));
});
@@ -185,7 +199,37 @@ describe("parse_token_usage", () => {
await main();
- expect(mockCore.summary.addDetails).toHaveBeenCalledWith("Threat Detection Token Usage", expect.stringContaining("| Alias |"));
+ expect(mockCore.summary.addRaw).toHaveBeenCalledWith(expect.stringContaining("### Threat Detection Token Usage"), true);
+ });
+
+ test("appends token usage section to GITHUB_STEP_SUMMARY when configured", async () => {
+ const stepSummaryPath = path.join(tmpDir, "step-summary.md");
+ process.env.GITHUB_STEP_SUMMARY = stepSummaryPath;
+
+ fs.existsSync = vi.fn(p => {
+ if (p === TOKEN_USAGE_PATH) return true;
+ if (p === TOKEN_USAGE_AUDIT_PATH) return false;
+ return originalExistsSync(p);
+ });
+ fs.statSync = vi.fn(p => {
+ if (p === TOKEN_USAGE_PATH) return { size: singleEntry.length };
+ if (p === TOKEN_USAGE_AUDIT_PATH) return { size: 0 };
+ return originalStatSync(p);
+ });
+ fs.readFileSync = vi.fn((p, enc) => {
+ if (p === TOKEN_USAGE_PATH) return singleEntry;
+ if (p === TOKEN_USAGE_AUDIT_PATH) return "";
+ return originalReadFileSync(p, enc);
+ });
+
+ await main();
+
+ const stepSummary = originalReadFileSync(stepSummaryPath, "utf8");
+ expect(stepSummary).toContain("### Token Usage");
+ expect(stepSummary).toContain("Per-request AI credits and token totals");
+ expect(stepSummary).toContain("| ΔAI Credits | AI Credits |");
+ expect(mockCore.summary.addRaw).not.toHaveBeenCalled();
+ expect(mockCore.summary.write).not.toHaveBeenCalled();
});
test("writes agent_usage.json with aggregated token totals including effective_tokens and primary_model", async () => {
@@ -292,11 +336,11 @@ describe("parse_token_usage", () => {
await main();
- const detailsCall = mockCore.summary.addDetails.mock.calls[0];
- expect(detailsCall[0]).toBe("Token Usage");
- expect(detailsCall[1]).toContain("◉ sonnet46");
- expect(detailsCall[1]).toContain("■ gpt40");
- expect(detailsCall[1]).toContain("**Total**");
+ const summaryCall = mockCore.summary.addRaw.mock.calls[0];
+ expect(summaryCall[0]).toContain("### Token Usage");
+ expect(summaryCall[0]).toContain("◉ sonnet46");
+ expect(summaryCall[0]).toContain("■ gpt40");
+ expect(summaryCall[0]).toContain("**Total**");
const agentUsage = JSON.parse(fs.readFileSync(agentUsageFile, "utf8"));
expect(agentUsage.input_tokens).toBe(150);
@@ -331,9 +375,9 @@ describe("parse_token_usage", () => {
await main();
- const detailsCall = mockCore.summary.addDetails.mock.calls[0];
- expect(detailsCall[1]).toContain("◉ sonnet46");
- expect(detailsCall[1]).toContain("■ gpt40");
+ const summaryCall = mockCore.summary.addRaw.mock.calls[0];
+ expect(summaryCall[0]).toContain("◉ sonnet46");
+ expect(summaryCall[0]).toContain("■ gpt40");
const agentUsage = JSON.parse(fs.readFileSync(agentUsageFile, "utf8"));
expect(agentUsage.input_tokens).toBe(150);
@@ -397,10 +441,10 @@ describe("parse_token_usage", () => {
await main();
- const detailsCall = mockCore.summary.addDetails.mock.calls[0];
- expect(detailsCall[1]).toContain("◉ sonnet46");
- expect(detailsCall[1]).toContain("▲ haiku45");
- expect(detailsCall[1]).toContain("■ gpt40");
+ const summaryCall = mockCore.summary.addRaw.mock.calls[0];
+ expect(summaryCall[0]).toContain("◉ sonnet46");
+ expect(summaryCall[0]).toContain("▲ haiku45");
+ expect(summaryCall[0]).toContain("■ gpt40");
const agentUsage = JSON.parse(fs.readFileSync(agentUsageFile, "utf8"));
expect(agentUsage.input_tokens).toBe(170);
@@ -495,5 +539,12 @@ describe("parse_token_usage", () => {
delete process.env.GH_AW_TOKEN_USAGE_SUMMARY_TITLE;
expect(getSummaryTitle()).toBe("Token Usage");
});
+
+ test("buildStepSummarySection wraps markdown in a heading and details block", () => {
+ const section = buildStepSummarySection("Token Usage", "| Alias |\n| --- |");
+ expect(section).toContain("### Token Usage");
+ expect(section).toContain("");
+ expect(section).toContain("Per-request AI credits and token totals
");
+ });
});
});
From 217bc03955dab23ec97e911ebc6d4d5d438caad5 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Sat, 6 Jun 2026 18:04:31 +0000
Subject: [PATCH 2/2] Ensure token usage summary reaches step summary
Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
---
actions/setup/js/parse_token_usage.cjs | 3 +--
actions/setup/js/parse_token_usage.test.cjs | 7 ++++++-
2 files changed, 7 insertions(+), 3 deletions(-)
diff --git a/actions/setup/js/parse_token_usage.cjs b/actions/setup/js/parse_token_usage.cjs
index e79bf4d6808..6bdd18cac00 100644
--- a/actions/setup/js/parse_token_usage.cjs
+++ b/actions/setup/js/parse_token_usage.cjs
@@ -142,8 +142,7 @@ async function main() {
}
const markdown = generateTokenUsageSummary(summary);
if (markdown.length > 0) {
- if (markdown.length > 0) {
- }
+ await appendStepSummarySection(getSummaryTitle(), markdown);
}
core.info("Token usage summary appended to step summary");
diff --git a/actions/setup/js/parse_token_usage.test.cjs b/actions/setup/js/parse_token_usage.test.cjs
index 02ae0edf38e..8f8cff8509c 100644
--- a/actions/setup/js/parse_token_usage.test.cjs
+++ b/actions/setup/js/parse_token_usage.test.cjs
@@ -60,6 +60,7 @@ describe("parse_token_usage", () => {
describe("main function", () => {
let tmpDir;
let mockCore;
+ let originalAppendFileSync;
let originalExistsSync;
let originalStatSync;
let originalReadFileSync;
@@ -68,7 +69,7 @@ describe("parse_token_usage", () => {
beforeEach(() => {
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "parse-token-usage-test-"));
delete process.env.GH_AW_TOKEN_USAGE_SUMMARY_TITLE;
- delete process.env.GITHUB_STEP_SUMMARY;
+ process.env.GITHUB_STEP_SUMMARY = "";
mockCore = {
info: vi.fn(),
@@ -87,6 +88,7 @@ describe("parse_token_usage", () => {
global.core = mockCore;
+ originalAppendFileSync = fs.appendFileSync;
originalExistsSync = fs.existsSync;
originalStatSync = fs.statSync;
originalReadFileSync = fs.readFileSync;
@@ -107,6 +109,7 @@ describe("parse_token_usage", () => {
});
afterEach(() => {
+ fs.appendFileSync = originalAppendFileSync;
fs.existsSync = originalExistsSync;
fs.statSync = originalStatSync;
fs.readFileSync = originalReadFileSync;
@@ -205,6 +208,7 @@ describe("parse_token_usage", () => {
test("appends token usage section to GITHUB_STEP_SUMMARY when configured", async () => {
const stepSummaryPath = path.join(tmpDir, "step-summary.md");
process.env.GITHUB_STEP_SUMMARY = stepSummaryPath;
+ fs.appendFileSync = vi.fn((...args) => originalAppendFileSync(...args));
fs.existsSync = vi.fn(p => {
if (p === TOKEN_USAGE_PATH) return true;
@@ -228,6 +232,7 @@ describe("parse_token_usage", () => {
expect(stepSummary).toContain("### Token Usage");
expect(stepSummary).toContain("Per-request AI credits and token totals
");
expect(stepSummary).toContain("| ΔAI Credits | AI Credits |");
+ expect(fs.appendFileSync).toHaveBeenCalledWith(stepSummaryPath, expect.any(String), "utf8");
expect(mockCore.summary.addRaw).not.toHaveBeenCalled();
expect(mockCore.summary.write).not.toHaveBeenCalled();
});