@@ -15,18 +15,28 @@ export interface ContextUsage {
1515 used : number ;
1616 size : number ;
1717 percentage : number ;
18+ /** Cumulative estimated session cost, summed across turns; `null` if none reported (e.g. codex). */
1819 cost : { amount : number ; currency : string } | null ;
1920 breakdown : ContextBreakdown | null ;
2021}
2122
22- type ContextUsageAggregate = Omit < ContextUsage , "breakdown" > ;
23+ type ContextUsageAggregate = Omit < ContextUsage , "breakdown" | "cost" > ;
2324
2425export function extractContextUsage ( events : AcpMessage [ ] ) : ContextUsage | null {
2526 let aggregate : ContextUsageAggregate | null = null ;
2627 let breakdown : ContextBreakdown | null = null ;
28+ let costAmount : number | null = null ;
29+ let costCurrency = "USD" ;
2730
31+ // Cost sums over every turn, so this can't early-break once the newest
32+ // aggregate/breakdown is found — it walks the full log.
2833 for ( let i = events . length - 1 ; i >= 0 ; i -- ) {
2934 const msg = events [ i ] . message ;
35+ const cost = extractCost ( msg ) ;
36+ if ( cost ) {
37+ costAmount = ( costAmount ?? 0 ) + cost . amount ;
38+ costCurrency = cost . currency ;
39+ }
3040 if ( ! aggregate ) {
3141 aggregate = extractAggregate ( msg ) ;
3242 } else if ( aggregate . size <= 0 ) {
@@ -37,36 +47,55 @@ export function extractContextUsage(events: AcpMessage[]): ContextUsage | null {
3747 if ( ! breakdown ) {
3848 breakdown = extractBreakdown ( msg ) ;
3949 }
40- if ( aggregate && aggregate . size > 0 && breakdown ) break ;
4150 }
4251
4352 if ( ! aggregate ) return null ;
44- return { ...aggregate , breakdown } ;
53+ return { ...aggregate , cost : toCost ( costAmount , costCurrency ) , breakdown } ;
4554}
4655
4756interface ContextUsageState {
4857 aggregate : ContextUsageAggregate | null ;
58+ costAmount : number | null ;
59+ costCurrency : string ;
4960 breakdown : ContextBreakdown | null ;
5061}
5162
5263export function createContextUsageTracker ( ) {
5364 return createAppendOnlyTracker < ContextUsageState , ContextUsage | null > ( {
54- init : ( ) => ( { aggregate : null , breakdown : null } ) ,
65+ init : ( ) => ( {
66+ aggregate : null ,
67+ costAmount : null ,
68+ costCurrency : "USD" ,
69+ breakdown : null ,
70+ } ) ,
5571 processEvent : ( state , event ) => {
5672 const msg = event . message ;
5773 const next = extractAggregate ( msg ) ;
5874 if ( next ) {
5975 state . aggregate = withCarriedSize ( next , state . aggregate ) ;
6076 }
77+ const cost = extractCost ( msg ) ;
78+ if ( cost ) {
79+ state . costAmount = ( state . costAmount ?? 0 ) + cost . amount ;
80+ state . costCurrency = cost . currency ;
81+ }
6182 state . breakdown = extractBreakdown ( msg ) ?? state . breakdown ;
6283 } ,
6384 getResult : ( state ) =>
6485 state . aggregate
65- ? { ...state . aggregate , breakdown : state . breakdown }
86+ ? {
87+ ...state . aggregate ,
88+ cost : toCost ( state . costAmount , state . costCurrency ) ,
89+ breakdown : state . breakdown ,
90+ }
6691 : null ,
6792 } ) ;
6893}
6994
95+ function toCost ( amount : number | null , currency : string ) : ContextUsage [ "cost" ] {
96+ return amount != null ? { amount, currency } : null ;
97+ }
98+
7099/**
71100 * An update that omits `size` must not wipe a previously known context window
72101 * (codex reports `modelContextWindow` intermittently), so keep the last known
@@ -116,11 +145,38 @@ function extractAggregate(
116145 const size = typeof update . size === "number" ? update . size : 0 ;
117146 const percentage =
118147 size > 0 ? Math . min ( 100 , Math . round ( ( update . used / size ) * 100 ) ) : 0 ;
148+ return { used : update . used , size, percentage } ;
149+ }
150+ }
151+ return null ;
152+ }
153+
154+ function extractCost (
155+ msg : AcpMessage [ "message" ] ,
156+ ) : { amount : number ; currency : string } | null {
157+ if (
158+ "method" in msg &&
159+ msg . method === "session/update" &&
160+ ! ( "id" in msg ) &&
161+ "params" in msg
162+ ) {
163+ const params = msg . params as
164+ | {
165+ update ?: {
166+ sessionUpdate ?: string ;
167+ cost ?: { amount : number ; currency : string } | null ;
168+ } ;
169+ }
170+ | undefined ;
171+ const update = params ?. update ;
172+ if (
173+ update ?. sessionUpdate === "usage_update" &&
174+ update . cost &&
175+ typeof update . cost . amount === "number"
176+ ) {
119177 return {
120- used : update . used ,
121- size,
122- percentage,
123- cost : update . cost ?? null ,
178+ amount : update . cost . amount ,
179+ currency : update . cost . currency ?? "USD" ,
124180 } ;
125181 }
126182 }
0 commit comments