@@ -123,21 +123,32 @@ func parseAntigravityLog(logFile, today string, usage map[string]*AgentUsage) {
123123 defer f .Close ()
124124
125125 scanner := bufio .NewScanner (f )
126- // Increase buffer size for long lines
127126 buf := make ([]byte , 0 , 256 * 1024 )
128127 scanner .Buffer (buf , 1024 * 1024 )
129128
130129 currentModel := ""
130+ // Track exhaustion events with timestamps to compute rolling window
131+ type exhaustionEvent struct {
132+ model string
133+ timestamp string // when the 429 happened
134+ resetStr string // e.g. "2h49m19s"
135+ }
136+ var exhaustions []exhaustionEvent
137+
138+ // First pass: collect all lines, detect models, and track exhaustion events
139+ type apiCall struct {
140+ model string
141+ timestamp string
142+ }
143+ var calls []apiCall
131144
132145 for scanner .Scan () {
133146 line := scanner .Text ()
134-
135- // Only process today's entries
136147 if ! strings .HasPrefix (line , today ) {
137148 continue
138149 }
139150
140- // Detect model from RESOURCE_EXHAUSTED or capacity errors
151+ // Detect model from log lines (error messages, planner requests, etc.)
141152 if strings .Contains (line , "claude-opus" ) {
142153 currentModel = "claude-opus-4-6-thinking"
143154 } else if strings .Contains (line , "claude-sonnet" ) {
@@ -146,58 +157,137 @@ func parseAntigravityLog(logFile, today string, usage map[string]*AgentUsage) {
146157 currentModel = "gemini-2.5-pro"
147158 } else if strings .Contains (line , "gemini-2.5-flash" ) || strings .Contains (line , "gemini_2.5_flash" ) {
148159 currentModel = "gemini-2.5-flash"
160+ } else if strings .Contains (line , "gemini-2.0-flash" ) || strings .Contains (line , "gemini_2.0_flash" ) {
161+ currentModel = "gemini-2.0-flash"
149162 }
150163
151- // Count API calls: streamGenerateContent is the Gemini/Claude proxy call
164+ // Count API calls
152165 if strings .Contains (line , "streamGenerateContent" ) {
153- // Default model if not detected
166+ ts := ""
167+ if len (line ) >= 23 {
168+ ts = line [:23 ]
169+ }
154170 model := currentModel
155171 if model == "" {
156172 model = "unknown"
157173 }
174+ calls = append (calls , apiCall {model : model , timestamp : ts })
175+ }
158176
159- // Extract timestamp (format: 2026-03-20 19:48:46.310)
177+ // Track exhaustion events
178+ if strings .Contains (line , "RESOURCE_EXHAUSTED" ) || strings .Contains (line , "code 429" ) {
160179 ts := ""
161180 if len (line ) >= 23 {
162181 ts = line [:23 ]
163182 }
183+ resetStr := ""
184+ if idx := strings .Index (line , "reset after " ); idx > 0 {
185+ rest := line [idx + len ("reset after " ):]
186+ // Extract duration like "2h49m19s" or "0s"
187+ endIdx := strings .Index (rest , ":" )
188+ if endIdx > 0 {
189+ resetStr = strings .TrimRight (rest [:endIdx ], "." )
190+ }
191+ }
192+ exhaustions = append (exhaustions , exhaustionEvent {
193+ model : currentModel ,
194+ timestamp : ts ,
195+ resetStr : resetStr ,
196+ })
197+ }
198+ }
199+
200+ // Find the last reset time per model
201+ lastResetTime := make (map [string ]string ) // model -> timestamp when quota reset
202+ for _ , evt := range exhaustions {
203+ model := evt .model
204+ if model == "" {
205+ model = "unknown"
206+ }
207+ // Parse the reset duration and calculate when quota reset
208+ if evt .resetStr != "" && evt .resetStr != "0s" {
209+ dur , err := time .ParseDuration (evt .resetStr )
210+ if err == nil {
211+ // Parse the exhaustion timestamp
212+ t , err := time .Parse ("2006-01-02 15:04:05" , evt .timestamp [:19 ])
213+ if err == nil {
214+ resetAt := t .Add (dur )
215+ resetTs := resetAt .Format ("2006-01-02 15:04:05.000" )
216+ // Keep the latest reset time
217+ if existing , ok := lastResetTime [model ]; ! ok || resetTs > existing {
218+ lastResetTime [model ] = resetTs
219+ }
220+ }
221+ }
222+ }
223+ }
164224
225+ // If unknown model has calls but we detected a model from exhaustion, merge them
226+ // The Antigravity log doesn't repeat the model name on normal API calls
227+ detectedModel := currentModel
228+ if detectedModel == "" && len (exhaustions ) > 0 {
229+ detectedModel = exhaustions [len (exhaustions )- 1 ].model
230+ }
231+
232+ // Count calls per model, only SINCE the last reset
233+ for _ , call := range calls {
234+ model := call .model
235+ // Merge "unknown" into the detected model if we have one
236+ if model == "unknown" && detectedModel != "" {
237+ model = detectedModel
238+ }
239+
240+ // Check if this call is after the last reset for this model
241+ resetTs , hasReset := lastResetTime [model ]
242+ if hasReset && call .timestamp <= resetTs {
243+ // This call was before the quota reset — skip it for current window
244+ // But still track total daily calls
165245 if _ , ok := usage [model ]; ! ok {
166246 usage [model ] = & AgentUsage {
167247 Agent : "Antigravity" ,
168248 Model : model ,
169249 Provider : detectProvider (model ),
170- FirstCall : ts ,
171250 }
172251 }
173-
174- usage [model ].APICalls ++
175- usage [model ].LastCall = ts
252+ // Don't count these in APICalls (which represents current window)
253+ continue
176254 }
177255
178- // Detect exhaustion
179- if strings .Contains (line , "RESOURCE_EXHAUSTED" ) || strings .Contains (line , "code 429" ) {
180- model := currentModel
181- if model == "" {
182- model = "unknown"
183- }
184- if u , ok := usage [model ]; ok {
185- u .Exhausted = true
256+ if _ , ok := usage [model ]; ! ok {
257+ usage [model ] = & AgentUsage {
258+ Agent : "Antigravity" ,
259+ Model : model ,
260+ Provider : detectProvider (model ),
261+ FirstCall : call .timestamp ,
186262 }
263+ }
264+ usage [model ].APICalls ++
265+ usage [model ].LastCall = call .timestamp
266+ if usage [model ].FirstCall == "" {
267+ usage [model ].FirstCall = call .timestamp
268+ }
269+ }
187270
188- // Extract reset time
189- if idx := strings .Index (line , "reset after " ); idx > 0 {
190- rest := line [idx + len ("reset after " ):]
191- if end := strings .Index (rest , ":" ); end > 0 {
192- // Find the full reset time (e.g. "2h49m19s")
193- resetEnd := strings .Index (rest , ".: " )
194- if resetEnd < 0 {
195- resetEnd = len (rest )
196- }
197- if u , ok := usage [model ]; ok {
198- u .ResetIn = rest [:resetEnd ]
199- }
271+ // Mark models that had exhaustion today (for informational display)
272+ for _ , evt := range exhaustions {
273+ model := evt .model
274+ if model == "" {
275+ model = detectedModel
276+ }
277+ if model == "" {
278+ continue
279+ }
280+ if u , ok := usage [model ]; ok {
281+ // Only mark as currently exhausted if it hasn't reset yet
282+ if resetTs , hasReset := lastResetTime [model ]; hasReset {
283+ now := time .Now ().Format ("2006-01-02 15:04:05.000" )
284+ if now < resetTs {
285+ u .Exhausted = true
286+ u .ResetIn = evt .resetStr
200287 }
288+ // If reset is in the past, quota recovered — don't mark exhausted
289+ } else {
290+ u .Exhausted = true
201291 }
202292 }
203293 }
0 commit comments