@@ -218,14 +218,21 @@ pub(super) fn parse_jsonl_file(path: &Path) -> Result<Vec<GeminiUsageEvent>> {
218218 Ok ( events)
219219}
220220
221+ /// Parses an Antigravity SQLite conversation database file.
222+ ///
223+ /// Reads generation metadata records from the `gen_metadata` table ordered by `idx ASC`
224+ /// to guarantee deterministic chronological sequence and ensure model names propagate
225+ /// accurately to continuation rows.
221226pub ( super ) fn parse_sqlite_file ( path : & Path ) -> Result < Vec < GeminiUsageEvent > > {
222227 let fallback_timestamp = file_modified_timestamp ( path) ;
223228 let Ok ( connection) =
224229 sqlite:: Connection :: open_with_flags ( path, sqlite:: OpenFlags :: new ( ) . with_read_only ( ) )
225230 else {
226231 return Ok ( Vec :: new ( ) ) ;
227232 } ;
228- let Ok ( mut statement) = connection. prepare ( "SELECT idx, data FROM gen_metadata" ) else {
233+ let Ok ( mut statement) =
234+ connection. prepare ( "SELECT idx, data FROM gen_metadata ORDER BY idx ASC" )
235+ else {
229236 return Ok ( Vec :: new ( ) ) ;
230237 } ;
231238
@@ -237,83 +244,91 @@ pub(super) fn parse_sqlite_file(path: &Path) -> Result<Vec<GeminiUsageEvent>> {
237244
238245 let mut current_model = None :: < String > ;
239246 let mut events = Vec :: new ( ) ;
240- while let Ok ( sqlite:: State :: Row ) = statement. next ( ) {
241- let idx: i64 = statement. read ( 0 ) . unwrap_or_default ( ) ;
242- let blob: Vec < u8 > = statement. read ( 1 ) . unwrap_or_default ( ) ;
243- if blob. is_empty ( ) {
244- continue ;
245- }
246- let parsed = parse_antigravity_protobuf ( & blob) ;
247- let row_model = parsed
248- . strings
249- . get ( "1.19" )
250- . or_else ( || parsed. strings . get ( "1.21" ) )
251- . or_else ( || parsed. strings . get ( "1.3" ) )
252- . map ( |s| normalize_antigravity_model ( s) ) ;
253-
254- if let Some ( ref m) = row_model {
255- current_model = Some ( m. clone ( ) ) ;
256- }
247+ loop {
248+ match statement. next ( ) {
249+ Ok ( sqlite:: State :: Row ) => {
250+ let idx: i64 = statement. read ( 0 ) . unwrap_or_default ( ) ;
251+ let blob: Vec < u8 > = statement. read ( 1 ) . unwrap_or_default ( ) ;
252+ if blob. is_empty ( ) {
253+ continue ;
254+ }
255+ let parsed = parse_antigravity_protobuf ( & blob) ;
256+ let row_model = parsed
257+ . strings
258+ . get ( "1.19" )
259+ . or_else ( || parsed. strings . get ( "1.21" ) )
260+ . or_else ( || parsed. strings . get ( "1.3" ) )
261+ . map ( |s| normalize_antigravity_model ( s) ) ;
262+
263+ if let Some ( ref m) = row_model {
264+ current_model = Some ( m. clone ( ) ) ;
265+ }
257266
258- let model = row_model
259- . or_else ( || current_model. clone ( ) )
260- . unwrap_or_else ( || "gemini-internal-model" . to_string ( ) ) ;
261- let input_tokens = parsed. numbers . get ( "1.4.2" ) . copied ( ) . unwrap_or ( 0 ) ;
262- let output_tokens = parsed. numbers . get ( "1.4.3" ) . copied ( ) . unwrap_or ( 0 ) ;
263- let cache_read_tokens = parsed. numbers . get ( "1.4.5" ) . copied ( ) . unwrap_or ( 0 ) ;
264- let reasoning_tokens = parsed. numbers . get ( "1.4.9" ) . copied ( ) . unwrap_or ( 0 ) ;
265- let visible_tokens = parsed. numbers . get ( "1.4.10" ) . copied ( ) . unwrap_or ( 0 ) ;
266- let timestamp_seconds = parsed. numbers . get ( "1.9.4.1" ) . copied ( ) ;
267- let timestamp_nanos = parsed
268- . numbers
269- . get ( "1.9.4.2" )
270- . copied ( )
271- . unwrap_or ( 0 )
272- . min ( 999_999_999 ) ;
273- let timestamp = timestamp_seconds
274- . and_then ( |value| i64:: try_from ( value) . ok ( ) )
275- . and_then ( |secs| {
276- if secs > 0 {
277- let ms = secs
278- . saturating_mul ( 1000 )
279- . saturating_add ( ( timestamp_nanos / 1_000_000 ) as i64 ) ;
280- Some ( TimestampMs :: from_millis ( ms) )
281- } else {
282- None
267+ let model = row_model
268+ . or_else ( || current_model. clone ( ) )
269+ . unwrap_or_else ( || "gemini-internal-model" . to_string ( ) ) ;
270+ let input_tokens = parsed. numbers . get ( "1.4.2" ) . copied ( ) . unwrap_or ( 0 ) ;
271+ let output_tokens = parsed. numbers . get ( "1.4.3" ) . copied ( ) . unwrap_or ( 0 ) ;
272+ let cache_read_tokens = parsed. numbers . get ( "1.4.5" ) . copied ( ) . unwrap_or ( 0 ) ;
273+ let reasoning_tokens = parsed. numbers . get ( "1.4.9" ) . copied ( ) . unwrap_or ( 0 ) ;
274+ let visible_tokens = parsed. numbers . get ( "1.4.10" ) . copied ( ) . unwrap_or ( 0 ) ;
275+ let timestamp_seconds = parsed. numbers . get ( "1.9.4.1" ) . copied ( ) ;
276+ let timestamp_nanos = parsed
277+ . numbers
278+ . get ( "1.9.4.2" )
279+ . copied ( )
280+ . unwrap_or ( 0 )
281+ . min ( 999_999_999 ) ;
282+ let timestamp = timestamp_seconds
283+ . and_then ( |value| i64:: try_from ( value) . ok ( ) )
284+ . and_then ( |secs| {
285+ if secs > 0 {
286+ let ms = secs
287+ . saturating_mul ( 1000 )
288+ . saturating_add ( ( timestamp_nanos / 1_000_000 ) as i64 ) ;
289+ Some ( TimestampMs :: from_millis ( ms) )
290+ } else {
291+ None
292+ }
293+ } )
294+ . unwrap_or ( fallback_timestamp) ;
295+ if input_tokens == 0
296+ && output_tokens == 0
297+ && cache_read_tokens == 0
298+ && reasoning_tokens == 0
299+ && visible_tokens == 0
300+ {
301+ continue ;
283302 }
284- } )
285- . unwrap_or ( fallback_timestamp) ;
286- if input_tokens == 0
287- && output_tokens == 0
288- && cache_read_tokens == 0
289- && reasoning_tokens == 0
290- && visible_tokens == 0
291- {
292- continue ;
293- }
294- let event = build_event (
295- Some ( & model) ,
296- & session_id,
297- timestamp,
298- GeminiTokens {
299- input : input_tokens,
300- output : output_tokens,
301- cached : cache_read_tokens,
302- thoughts : reasoning_tokens. max ( visible_tokens. saturating_sub ( output_tokens) ) ,
303- tool : 0 ,
304- total : Some ( input_tokens + output_tokens + cache_read_tokens + reasoning_tokens) ,
305- } ,
306- normalize_session_input,
307- Some ( format ! ( "antigravity-gen-{idx}" ) ) ,
308- ) ;
309- if let Some ( event) = event {
310- events. push ( event) ;
303+ let thoughts = reasoning_tokens. max ( visible_tokens. saturating_sub ( output_tokens) ) ;
304+ let event = build_event (
305+ Some ( & model) ,
306+ & session_id,
307+ timestamp,
308+ GeminiTokens {
309+ input : input_tokens,
310+ output : output_tokens,
311+ cached : cache_read_tokens,
312+ thoughts,
313+ tool : 0 ,
314+ total : Some ( input_tokens + output_tokens + cache_read_tokens + thoughts) ,
315+ } ,
316+ normalize_session_input,
317+ Some ( format ! ( "antigravity-gen-{idx}" ) ) ,
318+ ) ;
319+ if let Some ( event) = event {
320+ events. push ( event) ;
321+ }
322+ }
323+ Ok ( sqlite:: State :: Done ) => break ,
324+ Err ( _) => break ,
311325 }
312326 }
313327 Ok ( events)
314328}
315329
316- fn normalize_antigravity_model ( raw : & str ) -> String {
330+ /// Normalizes Antigravity model identifiers and display strings into standard model IDs.
331+ pub ( super ) fn normalize_antigravity_model ( raw : & str ) -> String {
317332 let trimmed = raw. trim ( ) ;
318333 if trimmed. is_empty ( ) {
319334 return "gemini-internal-model" . to_string ( ) ;
@@ -327,11 +342,14 @@ fn normalize_antigravity_model(raw: &str) -> String {
327342 } ;
328343
329344 match base {
345+ "gemini 3.7 flash" | "gemini 3.7 flash thinking" => "gemini-3.7-flash" . to_string ( ) ,
346+ "gemini 3.7 pro" | "gemini 3.7 pro thinking" => "gemini-3.7-pro" . to_string ( ) ,
330347 "gemini 3.6 flash" | "gemini 3 flash" => "gemini-3.6-flash" . to_string ( ) ,
331348 "gemini 3.6 pro" | "gemini 3 pro" => "gemini-3.6-pro" . to_string ( ) ,
332349 "gemini 2.5 flash" => "gemini-2.5-flash" . to_string ( ) ,
333350 "gemini 2.5 pro" => "gemini-2.5-pro" . to_string ( ) ,
334351 "gemini 2.0 flash" | "gemini 2 flash" => "gemini-2.0-flash" . to_string ( ) ,
352+ "gemini 2.0 pro" => "gemini-2.0-pro" . to_string ( ) ,
335353 "gemini 1.5 flash" => "gemini-1.5-flash" . to_string ( ) ,
336354 "gemini 1.5 pro" => "gemini-1.5-pro" . to_string ( ) ,
337355 "claude 3.7 sonnet" | "claude 3.7 sonnet thinking" => "claude-3-7-sonnet" . to_string ( ) ,
@@ -352,20 +370,24 @@ fn normalize_antigravity_model(raw: &str) -> String {
352370 }
353371}
354372
373+ /// Maximum recursion depth allowed during nested protobuf message parsing.
355374const MAX_PROTOBUF_DEPTH : usize = 16 ;
356375
376+ /// Intermediate storage for decoded protobuf field numbers and values.
357377#[ derive( Default ) ]
358378struct AntigravityProtobuf {
359379 numbers : HashMap < String , u64 > ,
360380 strings : HashMap < String , String > ,
361381}
362382
383+ /// Decodes raw protobuf payload into numeric and string field maps.
363384fn parse_antigravity_protobuf ( blob : & [ u8 ] ) -> AntigravityProtobuf {
364385 let mut out = AntigravityProtobuf :: default ( ) ;
365386 parse_antigravity_protobuf_fields ( blob, & mut out, "" , 0 ) ;
366387 out
367388}
368389
390+ /// Recursively parses protobuf wire-format fields up to `MAX_PROTOBUF_DEPTH`.
369391fn parse_antigravity_protobuf_fields (
370392 blob : & [ u8 ] ,
371393 out : & mut AntigravityProtobuf ,
@@ -427,6 +449,7 @@ fn parse_antigravity_protobuf_fields(
427449 }
428450}
429451
452+ /// Reads a single varint from `blob` starting at `offset`, checking for 10th-byte overflow.
430453fn read_varint ( blob : & [ u8 ] , mut offset : usize ) -> Option < ( u64 , usize ) > {
431454 let mut value = 0u64 ;
432455 let mut shift = 0u32 ;
0 commit comments