Skip to content

Commit 6bc350c

Browse files
author
Test
committed
fix: monitor uses rolling window quota - count calls since last reset, not all day
1 parent d98a2da commit 6bc350c

File tree

1 file changed

+121
-31
lines changed

1 file changed

+121
-31
lines changed

internal/live/monitor.go

Lines changed: 121 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)