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(); });