@@ -66,84 +66,81 @@ type GeminiSession = {
6666function parseSession ( data : GeminiSession , seenKeys : Set < string > ) : ParsedProviderCall [ ] {
6767 const results : ParsedProviderCall [ ] = [ ]
6868
69- const geminiMessages = data . messages . filter ( m => m . type === 'gemini' && m . tokens && m . model )
70- if ( geminiMessages . length === 0 ) return results
71-
72- const dedupKey = `gemini:${ data . sessionId } `
73- if ( seenKeys . has ( dedupKey ) ) return results
74- seenKeys . add ( dedupKey )
75-
76- let totalInput = 0
77- let totalOutput = 0
78- let totalCached = 0
79- let totalThoughts = 0
80- const allTools : string [ ] = [ ]
81- const bashCommands : string [ ] = [ ]
82- let model = ''
83-
84- for ( const msg of geminiMessages ) {
85- const t = msg . tokens !
86- totalInput += t . input ?? 0
87- totalOutput += t . output ?? 0
88- totalCached += t . cached ?? 0
89- totalThoughts += t . thoughts ?? 0
90- if ( msg . model && ! model ) model = msg . model
69+ let lastUserMessage = ''
70+ let geminiOrdinal = 0
71+
72+ for ( const msg of data . messages ) {
73+ if ( msg . type === 'user' ) {
74+ if ( Array . isArray ( msg . content ) ) {
75+ lastUserMessage = msg . content . map ( c => c . text ) . join ( ' ' ) . slice ( 0 , 500 )
76+ } else if ( typeof msg . content === 'string' ) {
77+ lastUserMessage = msg . content . slice ( 0 , 500 )
78+ }
79+ continue
80+ }
81+
82+ if ( msg . type !== 'gemini' || ! msg . tokens || ! msg . model ) continue
83+
84+ const t = msg . tokens
85+ const totalInput = t . input ?? 0
86+ const totalOutput = t . output ?? 0
87+ const totalCached = t . cached ?? 0
88+ const totalThoughts = t . thoughts ?? 0
89+ if ( totalInput === 0 && totalOutput === 0 && totalCached === 0 && totalThoughts === 0 ) continue
90+
91+ const messageKey = msg . id || `idx-${ geminiOrdinal } `
92+ geminiOrdinal ++
93+ const dedupKey = `gemini:${ data . sessionId } :${ messageKey } `
94+ if ( seenKeys . has ( dedupKey ) ) continue
95+
96+ const tools : string [ ] = [ ]
97+ const bashCommands : string [ ] = [ ]
9198
9299 if ( msg . toolCalls ) {
93100 for ( const tc of msg . toolCalls ) {
94101 const mapped = toolNameMap [ tc . displayName ?? '' ] ?? toolNameMap [ tc . name ] ?? tc . displayName ?? tc . name
95- allTools . push ( mapped )
102+ tools . push ( mapped )
96103 if ( mapped === 'Bash' && tc . args && typeof tc . args . command === 'string' ) {
97104 bashCommands . push ( ...extractBashCommands ( tc . args . command ) )
98105 }
99106 }
100107 }
101- }
102-
103- if ( totalInput === 0 && totalOutput === 0 ) return results
104108
105- // Gemini's `input` count includes `cached` tokens as a subset, so fresh input
106- // must subtract cached to avoid double-charging at both rates.
107- const freshInput = totalInput - totalCached
108-
109- let userMessage = ''
110- const firstUser = data . messages . find ( m => m . type === 'user' )
111- if ( firstUser ) {
112- if ( Array . isArray ( firstUser . content ) ) {
113- userMessage = firstUser . content . map ( c => c . text ) . join ( ' ' ) . slice ( 0 , 500 )
114- } else if ( typeof firstUser . content === 'string' ) {
115- userMessage = firstUser . content . slice ( 0 , 500 )
116- }
109+ // Gemini's `input` count includes `cached` tokens as a subset, so fresh
110+ // input must subtract cached to avoid double-charging at both rates.
111+ const freshInput = Math . max ( 0 , totalInput - totalCached )
112+
113+ const tsDate = new Date ( msg . timestamp || data . startTime )
114+ if ( isNaN ( tsDate . getTime ( ) ) || tsDate . getTime ( ) < 1_000_000_000_000 ) continue
115+
116+ seenKeys . add ( dedupKey )
117+
118+ // Gemini bills thoughts at the output token rate; calculateCost does not
119+ // accept a reasoning parameter, so fold thoughts into the output count for
120+ // pricing while keeping outputTokens / reasoningTokens reported separately.
121+ const costUSD = calculateCost ( msg . model , freshInput , totalOutput + totalThoughts , 0 , totalCached , 0 )
122+
123+ results . push ( {
124+ provider : 'gemini' ,
125+ model : msg . model ,
126+ inputTokens : freshInput ,
127+ outputTokens : totalOutput ,
128+ cacheCreationInputTokens : 0 ,
129+ cacheReadInputTokens : totalCached ,
130+ cachedInputTokens : totalCached ,
131+ reasoningTokens : totalThoughts ,
132+ webSearchRequests : 0 ,
133+ costUSD,
134+ tools : [ ...new Set ( tools ) ] ,
135+ bashCommands : [ ...new Set ( bashCommands ) ] ,
136+ timestamp : tsDate . toISOString ( ) ,
137+ speed : 'standard' ,
138+ deduplicationKey : dedupKey ,
139+ userMessage : lastUserMessage ,
140+ sessionId : data . sessionId ,
141+ } )
117142 }
118143
119- const tsDate = new Date ( data . startTime )
120- if ( isNaN ( tsDate . getTime ( ) ) || tsDate . getTime ( ) < 1_000_000_000_000 ) return results
121-
122- // Gemini bills thoughts at the output token rate; calculateCost does not
123- // accept a reasoning parameter, so fold thoughts into the output count for
124- // pricing while keeping outputTokens / reasoningTokens reported separately.
125- const costUSD = calculateCost ( model , freshInput , totalOutput + totalThoughts , 0 , totalCached , 0 )
126-
127- results . push ( {
128- provider : 'gemini' ,
129- model,
130- inputTokens : freshInput ,
131- outputTokens : totalOutput ,
132- cacheCreationInputTokens : 0 ,
133- cacheReadInputTokens : totalCached ,
134- cachedInputTokens : totalCached ,
135- reasoningTokens : totalThoughts ,
136- webSearchRequests : 0 ,
137- costUSD,
138- tools : [ ...new Set ( allTools ) ] ,
139- bashCommands : [ ...new Set ( bashCommands ) ] ,
140- timestamp : tsDate . toISOString ( ) ,
141- speed : 'standard' ,
142- deduplicationKey : dedupKey ,
143- userMessage,
144- sessionId : data . sessionId ,
145- } )
146-
147144 return results
148145}
149146
0 commit comments