@@ -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
2324const 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" ;
2426const MAX_RPC_SUMMARY_DETAILS_LENGTH = 120 ;
2527const MAX_RPC_SUMMARY_GENERIC_LENGTH = 160 ;
2628const 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 */
204206function 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 ( / " r e q u e s t _ i d " \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