Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
77 changes: 74 additions & 3 deletions actions/setup/js/parse_token_usage.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -14,21 +14,87 @@ const { parseTokenUsageJsonl, generateTokenUsageSummary } = require("./parse_mcp
* is bundled in the agent artifact and accessible to third-party tools.
*/

const TOKEN_USAGE_AUDIT_PATH = "/tmp/gh-aw/sandbox/firewall-audit-logs/api-proxy-logs/token-usage.jsonl";
const TOKEN_USAGE_PATH = "/tmp/gh-aw/sandbox/firewall/logs/api-proxy-logs/token-usage.jsonl";
const TOKEN_USAGE_PATHS = [TOKEN_USAGE_AUDIT_PATH, TOKEN_USAGE_PATH];
const AGENT_USAGE_PATH = "/tmp/gh-aw/agent_usage.json";

/**
* Returns readable, non-empty token usage files, skipping paths that error.
* @param {string[]} paths
* @returns {string[]}
*/
function getReadableTokenUsagePaths(paths) {
const readablePaths = [];
for (const path of paths) {
try {
if (!fs.existsSync(path)) continue;
const stat = fs.statSync(path);
if (!stat || stat.size <= 0) continue;
readablePaths.push(path);
} catch (error) {
core.warning(`Skipping token usage path ${path}: ${getErrorMessage(error)}`);
}
}
return readablePaths;
}

/**
* Extracts request_id with lightweight matching (no full JSON parse).
* @param {string} line
* @returns {string}
*/
function extractRequestId(line) {
const match = line.match(/"request_id"\s*:\s*"((?:\\.|[^"\\])*)"/);
return match ? match[1] : "";
}

/**
* Reads token usage files and deduplicates overlapping lines by request_id.
* Falls back to raw line dedupe when request_id is absent.
* @param {string[]} paths
* @returns {string}
*/
function readDedupedTokenUsage(paths) {
const uniqueLineKeys = new Set();
const dedupedLines = [];

for (const path of paths) {
let fileContent = "";
try {
fileContent = fs.readFileSync(path, "utf8");
} catch (error) {
core.warning(`Skipping unreadable token usage file ${path}: ${getErrorMessage(error)}`);
continue;
}

for (const line of fileContent.split("\n")) {
const trimmed = line.trim();
if (!trimmed) continue;
const requestId = extractRequestId(trimmed);
const dedupeKey = requestId ? `request_id:${requestId}` : trimmed;
if (uniqueLineKeys.has(dedupeKey)) continue;
uniqueLineKeys.add(dedupeKey);
dedupedLines.push(trimmed);
}
}

return dedupedLines.join("\n");
}

/**
* Main function to parse token usage and write the step summary.
*/
async function main() {
try {
if (!fs.existsSync(TOKEN_USAGE_PATH) || fs.statSync(TOKEN_USAGE_PATH).size === 0) {
const tokenUsagePaths = getReadableTokenUsagePaths(TOKEN_USAGE_PATHS);
if (tokenUsagePaths.length === 0) {
core.info("No token usage data found, skipping summary");
return;
}

const content = fs.readFileSync(TOKEN_USAGE_PATH, "utf8");
core.info(`Parsing token usage from: ${TOKEN_USAGE_PATH} (${content.length} bytes)`);
const content = readDedupedTokenUsage(tokenUsagePaths);
core.info(`Parsing token usage from ${tokenUsagePaths.length} file(s): ${tokenUsagePaths.join(", ")} (${content.length} bytes)`);

const summary = parseTokenUsageJsonl(content);
if (!summary || summary.totalRequests === 0) {
Expand Down Expand Up @@ -72,7 +138,12 @@ async function main() {
if (typeof module !== "undefined" && module.exports) {
module.exports = {
main,
getReadableTokenUsagePaths,
extractRequestId,
readDedupedTokenUsage,
TOKEN_USAGE_AUDIT_PATH,
TOKEN_USAGE_PATH,
TOKEN_USAGE_PATHS,
AGENT_USAGE_PATH,
};
}
Expand Down
Loading