JuliusBrussee/caveman · error
stats days must be between 0 and 3660
Error message
stats days must be between 0 and 3660
What it means
BuildStatsReport validates StatsReportOptions.Days before reading the request ledger, accepting only 0 (all time) through 3660 days (10 years). Values outside that window are rejected because an absurd window would produce misleading rollups and expensive scans. 0 is explicitly allowed and means no lower-bound cutoff.
Solutions
- Clamp or validate the days value to 0..3660 before constructing StatsReportOptions
- Fix the date computation that produced a negative day count (swap start/end or take absolute value)
- If you truly need > 3660 days, chunk the report into multiple windows and merge
Example fix
// before
report, err := store.BuildStatsReport(StatsReportOptions{Days: days}) // days = 5000
// after
if days < 0 || days > 3660 {
return fmt.Errorf("--days must be 0..3660")
}
report, err := store.BuildStatsReport(StatsReportOptions{Days: days}) Defensive patterns
Strategy: validation
Validate before calling
func normalizeDays(d int) (int, error) {
if d < 0 || d > 3660 { return 0, fmt.Errorf("days must be 0..3660, got %d", d) }
return d, nil
} Prevention
- Clamp user-supplied windows to the supported max before calling
- Compute day counts from dates carefully (handle start>end explicitly)
- Document that 0 means 'no lower bound / all time'
- Add range checks wherever StatsReportOptions is constructed from raw input
When it happens
Trigger: Calling BuildStatsReport with StatsReportOptions{Days: n} where n is negative or greater than 3660, e.g. --days 9999 or a negative value computed from dates.
Common situations: A CLI script computing days from two dates where start > end yields a negative number; a typo like 36060 instead of 360; forwarding a raw user-supplied value without clamping.
Understand the failure class
Background: "value must be between 0 and 1" / "out of range" / "must not be negative" errors: fixing range-validation failures across open-source libraries — this error's family across 42 libraries.
Related errors
- --days must be an integer from 0 to 3660
- at least one report path and the canonical span fixture…
- at least one sink id is required
- base and head must be full Git object IDs
- cave_live_eval_sandbox_profile_invalid
AI-assisted analysis of JuliusBrussee/caveman@3ee70a1026 (2026-09-20).
Data as JSON: /api/errors/dcc8ef05754e87c2.
Report an issue: GitHub.
Appendix: source
Thrown at proxy/internal/store/stats.go:197
value := float64(delta) / 1_000_000 * rate
if finiteStatsNumber(value) && math.Abs(value) <= math.MaxFloat64/10_000_000_000 {
value = cost.RoundUSD(value)
rec.RequestEstimatedInputDeltaUSD = &value
} else {
rec.RequestEstimatedInputDeltaUSD = nil
}
} else {
rec.RequestEstimatedInputDeltaUSD = nil
}
}
}
// BuildStatsReport reads the request ledger only. Transcript imports are not
// unioned into this report: without an exact request join that would double count
// routed traffic and attach savings to unrelated agent usage.
func (s *Store) BuildStatsReport(opts StatsReportOptions) (StatsReport, error) {
if opts.Days < 0 || opts.Days > 3660 {
return StatsReport{}, fmt.Errorf("stats days must be between 0 and 3660")
}
now := opts.Now.UTC()
if opts.Now.IsZero() {
now = time.Now().UTC()
}
out := StatsReport{
Schema: "caveman.stats.v1", GeneratedAt: now.Format(time.RFC3339),
Window: StatsWindow{To: now.Format(time.RFC3339), Days: opts.Days, Lifetime: opts.Days == 0},
Groups: []StatsGroup{}, Daily: []StatsGroup{}, Providers: []StatsGroup{}, Models: []StatsGroup{}, Agents: []StatsGroup{}, AuthModes: []StatsGroup{}, Receipts: []StatsReceipt{},
Evidence: StatsEvidence{Basis: "inferred", TokenBasis: "estimated_request_json_o200k_v1", SpendBasis: "provider_counted_x_snapshotted_published_rate", CatalogVersions: []string{}, MeasurementStatuses: map[string]int64{}, ReceiptsLimit: statsReceiptLimit},
}
// Older ChatGPT rows used RFC3339 while the generic route used storeTSLayout.
// Compare instants, not their differing separators or timezone offsets.
where := []string{"julianday(ts) <= julianday(?)"}
args := []any{now.Format(time.RFC3339Nano)}
if opts.Days > 0 {
from := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, time.UTC).AddDate(0, 0, 1-opts.Days)
out.Window.From = from.Format(time.RFC3339)View on GitHub (pinned to 3ee70a1026)