Files
llmgw/internal/store/latency_test.go
T
Ilker ManapandClaude Opus 5.5 9f479b9a70 Show each team's first-token and latency percentiles, errors and refusals
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>
2026-10-03 19:50:16 +02:00

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)
}
})
}