Skip to content

Commit 7f13654

Browse files
Copilotpelikhan
andauthored
Fix AIC extraction from token usage JSONL files
Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
1 parent 2ad991b commit 7f13654

5 files changed

Lines changed: 294 additions & 45 deletions

File tree

actions/setup/js/daily_aic_workflow_helpers.cjs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -214,6 +214,12 @@ function sumAICFromUsageJSONLFiles(filePaths) {
214214
total += explicitAIC;
215215
continue;
216216
}
217+
// Prefer proxy-emitted per-request AIC over locally computed when present.
218+
const explicitPerRequest = getNumericAliasField(usage, parsed, ["ai_credits_this_response"]);
219+
if (explicitPerRequest > 0) {
220+
total += explicitPerRequest;
221+
continue;
222+
}
217223

218224
const computed = computeInferenceAIC({
219225
provider: getStringField(usage, parsed, "provider", "provider"),

actions/setup/js/parse_mcp_gateway_log.cjs

Lines changed: 64 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -18,9 +18,11 @@ const { parseUnknownModelAICreditsFromAuditLog } = require("./ai_credits_context
1818
* - /tmp/gh-aw/mcp-logs/gateway.log (main gateway log, fallback)
1919
* - /tmp/gh-aw/mcp-logs/stderr.log (stderr output, fallback)
2020
* - /tmp/gh-aw/sandbox/firewall/logs/api-proxy-logs/token-usage.jsonl (token usage from firewall proxy)
21+
* - /tmp/gh-aw/sandbox/firewall-audit-logs/api-proxy-logs/token-usage.jsonl (audit copy, checked as fallback)
2122
*/
2223

2324
const TOKEN_USAGE_PATH = "/tmp/gh-aw/sandbox/firewall/logs/api-proxy-logs/token-usage.jsonl";
25+
const TOKEN_USAGE_AUDIT_PATH = "/tmp/gh-aw/sandbox/firewall-audit-logs/api-proxy-logs/token-usage.jsonl";
2426
const MAX_RPC_SUMMARY_DETAILS_LENGTH = 120;
2527
const MAX_RPC_SUMMARY_GENERIC_LENGTH = 160;
2628
const MAX_RPC_MESSAGE_LABEL_LENGTH = 80;
@@ -66,7 +68,7 @@ function parseTokenUsageJsonl(jsonlContent) {
6668
totalAIC: 0,
6769
ambientContextTokens: undefined,
6870
byModel: {},
69-
/** @type {{ model: string, provider: string, inputTokens: number, outputTokens: number, cacheReadTokens: number, cacheWriteTokens: number, reasoningTokens: number, durationMs: number, deltaAIC: number }[]} */
71+
/** @type {{ model: string, provider: string, inputTokens: number, outputTokens: number, cacheReadTokens: number, cacheWriteTokens: number, reasoningTokens: number, durationMs: number, deltaAIC: number, explicitDeltaAIC: number | null }[]} */
7072
entries: [],
7173
};
7274

@@ -84,6 +86,9 @@ function parseTokenUsageJsonl(jsonlContent) {
8486
const cacheWriteTokens = entry.cache_write_tokens || 0;
8587
const reasoningTokens = entry.reasoning_tokens || 0;
8688
const durationMs = entry.duration_ms || 0;
89+
// When the proxy emits an explicit per-request AIC value, prefer it over
90+
// the locally-computed value so that proxy-side pricing updates take effect.
91+
const explicitDeltaAIC = typeof entry.ai_credits_this_response === "number" && entry.ai_credits_this_response > 0 ? entry.ai_credits_this_response : null;
8792

8893
summary.totalInputTokens += inputTokens;
8994
summary.totalOutputTokens += outputTokens;
@@ -117,33 +122,19 @@ function parseTokenUsageJsonl(jsonlContent) {
117122
m.requests++;
118123
m.durationMs += durationMs;
119124

120-
summary.entries.push({ model, provider: m.provider, inputTokens, outputTokens, cacheReadTokens, cacheWriteTokens, reasoningTokens, durationMs, deltaAIC: 0 });
125+
summary.entries.push({ model, provider: m.provider, inputTokens, outputTokens, cacheReadTokens, cacheWriteTokens, reasoningTokens, durationMs, deltaAIC: 0, explicitDeltaAIC });
121126
} catch {
122127
// skip malformed lines
123128
}
124129
}
125130

126131
if (summary.totalRequests === 0) return null;
127132

128-
let totalAIC = 0;
129-
for (const [model, usage] of Object.entries(summary.byModel)) {
130-
const aic = computeInferenceAIC({
131-
provider: usage.provider || "",
132-
model,
133-
inputTokens: usage.inputTokens,
134-
outputTokens: usage.outputTokens,
135-
cacheReadTokens: usage.cacheReadTokens,
136-
cacheWriteTokens: usage.cacheWriteTokens,
137-
reasoningTokens: usage.reasoningTokens || 0,
138-
});
139-
usage.aic = aic;
140-
totalAIC += aic;
141-
}
142-
summary.totalAIC = totalAIC;
143-
144133
// Compute per-request AI credits.
134+
// Prefer the proxy-emitted explicit value when available; fall back to
135+
// computing from token counts and the local pricing catalog.
145136
for (const entry of summary.entries) {
146-
entry.deltaAIC = computeInferenceAIC({
137+
const computed = computeInferenceAIC({
147138
provider: entry.provider || "",
148139
model: entry.model,
149140
inputTokens: entry.inputTokens,
@@ -152,7 +143,18 @@ function parseTokenUsageJsonl(jsonlContent) {
152143
cacheWriteTokens: entry.cacheWriteTokens,
153144
reasoningTokens: entry.reasoningTokens || 0,
154145
});
146+
entry.deltaAIC = entry.explicitDeltaAIC ?? computed;
147+
}
148+
149+
// Aggregate per-model AIC and overall total by summing per-entry deltaAIC.
150+
// This keeps model totals consistent with the per-entry view regardless of
151+
// whether explicit or computed AIC is used.
152+
let totalAIC = 0;
153+
for (const entry of summary.entries) {
154+
summary.byModel[entry.model].aic += entry.deltaAIC;
155+
totalAIC += entry.deltaAIC;
155156
}
157+
summary.totalAIC = totalAIC;
156158

157159
return summary;
158160
}
@@ -202,25 +204,47 @@ function generateTokenUsageSummary(summary) {
202204
* @param {typeof import('@actions/core')} coreObj - The GitHub Actions core object
203205
*/
204206
function writeStepSummaryWithTokenUsage(coreObj) {
205-
if (!fs.existsSync(TOKEN_USAGE_PATH)) {
206-
coreObj.debug(`No token-usage.jsonl found at: ${TOKEN_USAGE_PATH}`);
207-
} else {
208-
const content = fs.readFileSync(TOKEN_USAGE_PATH, "utf8");
209-
if (content?.trim()) {
210-
coreObj.info(`Found token-usage.jsonl (${content.length} bytes)`);
211-
const parsedSummary = parseTokenUsageJsonl(content);
212-
if (parsedSummary && parsedSummary.totalAIC > 0) {
213-
const roundedAIC = parsedSummary.totalAIC.toFixed(3);
214-
coreObj.exportVariable("GH_AW_AIC", roundedAIC);
215-
coreObj.setOutput("aic", roundedAIC);
216-
coreObj.info(`AI Credits: ${roundedAIC}`);
217-
}
218-
if (parsedSummary && typeof parsedSummary.ambientContextTokens === "number" && parsedSummary.ambientContextTokens > 0) {
219-
const roundedAmbientContext = String(Math.round(parsedSummary.ambientContextTokens));
220-
coreObj.exportVariable("GH_AW_AMBIENT_CONTEXT", roundedAmbientContext);
221-
coreObj.setOutput("ambient_context", roundedAmbientContext);
222-
coreObj.info(`Ambient context: ${roundedAmbientContext}`);
223-
}
207+
// Read from both the primary path and the audit path, deduplicating by request_id.
208+
// The audit path may contain additional entries when the primary path is absent or
209+
// partially written (e.g. the proxy was restarted mid-run).
210+
const paths = [TOKEN_USAGE_AUDIT_PATH, TOKEN_USAGE_PATH];
211+
const seenRequestIds = new Set();
212+
const dedupedLines = [];
213+
214+
for (const filePath of paths) {
215+
if (!fs.existsSync(filePath)) {
216+
coreObj.debug(`No token-usage.jsonl found at: ${filePath}`);
217+
continue;
218+
}
219+
const raw = fs.readFileSync(filePath, "utf8");
220+
if (!raw?.trim()) continue;
221+
coreObj.info(`Found token-usage.jsonl at ${filePath} (${raw.length} bytes)`);
222+
for (const rawLine of raw.split("\n")) {
223+
const line = rawLine.trim();
224+
if (!line) continue;
225+
// Lightweight request_id extraction for deduplication.
226+
const idMatch = line.match(/"request_id"\s*:\s*"((?:\\.|[^"\\])*)"/);
227+
const dedupeKey = idMatch ? `request_id:${idMatch[1]}` : line;
228+
if (seenRequestIds.has(dedupeKey)) continue;
229+
seenRequestIds.add(dedupeKey);
230+
dedupedLines.push(line);
231+
}
232+
}
233+
234+
if (dedupedLines.length > 0) {
235+
const content = dedupedLines.join("\n");
236+
const parsedSummary = parseTokenUsageJsonl(content);
237+
if (parsedSummary && parsedSummary.totalAIC > 0) {
238+
const roundedAIC = parsedSummary.totalAIC.toFixed(3);
239+
coreObj.exportVariable("GH_AW_AIC", roundedAIC);
240+
coreObj.setOutput("aic", roundedAIC);
241+
coreObj.info(`AI Credits: ${roundedAIC}`);
242+
}
243+
if (parsedSummary && typeof parsedSummary.ambientContextTokens === "number" && parsedSummary.ambientContextTokens > 0) {
244+
const roundedAmbientContext = String(Math.round(parsedSummary.ambientContextTokens));
245+
coreObj.exportVariable("GH_AW_AMBIENT_CONTEXT", roundedAmbientContext);
246+
coreObj.setOutput("ambient_context", roundedAmbientContext);
247+
coreObj.info(`Ambient context: ${roundedAmbientContext}`);
224248
}
225249
}
226250

@@ -1126,6 +1150,8 @@ if (typeof module !== "undefined" && module.exports) {
11261150
hasAICreditsRateLimitError,
11271151
hasUnknownModelAICreditsError,
11281152
setUnknownModelAICreditsOutput,
1153+
TOKEN_USAGE_PATH,
1154+
TOKEN_USAGE_AUDIT_PATH,
11291155
};
11301156
}
11311157

0 commit comments

Comments
 (0)