Percentiles are read from the histograms kept with the usage sums and shown per group on the usage tab and in the people page's panels. Requests refused by a limit are recorded as 429s against their owner, free of charge, so a team hitting its limits shows as such. Usage can be grouped by model. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
62 lines
2.2 KiB
Go
62 lines
2.2 KiB
Go
package store
|
|
|
|
import (
|
|
"context"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
func TestQuantilesAreReadFromTheHistogram(t *testing.T) {
|
|
// 100 durations: 90 at <=100 ms, 8 in (1000, 2500], 2 above two minutes.
|
|
counts := make([]int64, len(LatencyBounds)+1)
|
|
counts[0], counts[4], counts[len(counts)-1] = 90, 8, 2
|
|
q := quantiles(counts)
|
|
if q.N != 100 || q.P50 > 100 || q.P95 <= 1000 || q.P95 > 2500 || !q.Over {
|
|
t.Errorf("%+v: want p50 within 100 ms, p95 in (1000, 2500], p99 past the last edge", q)
|
|
}
|
|
if (quantiles(make([]int64, len(LatencyBounds)+1)) != Quantiles{}) {
|
|
t.Error("no durations read as some")
|
|
}
|
|
}
|
|
|
|
func TestStatsSayHowEachGroupFaredAndHowLongItTook(t *testing.T) {
|
|
onBoth(t, func(t *testing.T, s *DB) {
|
|
ctx := context.Background()
|
|
at := slice()
|
|
var batch []Usage
|
|
for i := range 20 {
|
|
batch = append(batch, Usage{At: at, KeyID: "k", UserID: "ayse", ProjectID: "p", Model: "big",
|
|
ServedModel: "big", Status: 200, LatencyMs: 4000, TTFTMs: int64(200 + i)})
|
|
}
|
|
batch = append(batch,
|
|
Usage{At: at, KeyID: "k", UserID: "ayse", ProjectID: "p", Model: "big", ServedModel: "big", Status: 502, LatencyMs: 50},
|
|
Usage{At: at, KeyID: "k2", UserID: "bora", ProjectID: "p", Model: "small", ServedModel: "small", Status: 429},
|
|
)
|
|
if err := s.WriteUsage(ctx, batch); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
byUser, err := s.StatsBy(ctx, GroupByUser, at.Add(-time.Hour))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
a, b := byUser["ayse"], byUser["bora"]
|
|
if a.Requests != 21 || a.Errors != 1 || a.Limited != 0 || b.Limited != 1 || b.Errors != 1 {
|
|
t.Errorf("outcomes: ayse %+v, bora %+v", a, b)
|
|
}
|
|
if a.TTFT.N != 20 || a.TTFT.P95 <= 100 || a.TTFT.P95 > 250 || a.Latency.P95 <= 2500 || a.Latency.P95 > 5000 {
|
|
t.Errorf("ayse's first token %+v and latency %+v", a.TTFT, a.Latency)
|
|
}
|
|
byModel, err := s.StatsBy(ctx, GroupByModel, at.Add(-time.Hour))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if byModel["big"].Requests != 21 || byModel["small"].Requests != 1 {
|
|
t.Errorf("by model: %+v", byModel)
|
|
}
|
|
series, err := s.UsageBucketsBy(ctx, GroupByModel, at.Add(-time.Hour), time.Hour)
|
|
if err != nil || len(series) != 2 {
|
|
t.Fatalf("usage over time by model: %v, %+v", err, series)
|
|
}
|
|
})
|
|
}
|