mirror of
https://github.com/lukaszraczylo/traefik-llmgateway.git
synced 2026-10-14 05:18:54 +00:00
feat(admin): add fleet statistics and admin read endpoints
Counters (always on when admin is enabled): - last seen per user and group, written asynchronously after admission, throttled to one write per scope per minute per replica - hourly provider and model attempts, failures and timeouts - failovers per provider, rejections per hour, and unpriced (402) refusals - cache hits, misses and cost saved - request counts per target and caller Opt-in counters under admin.stats (both default off): - userModel: usage per user and model - latency: latency histograms per provider and model All counters ride the existing Redis batches. A test measures three round trips per request with defaults and with every statistic on. A cache hit takes two. A model request is no longer counted for failed failover candidates. New admin endpoints: - usage/series and usage/totals - performance, with percentiles from the histograms - catalog, with the price billing actually uses and its source - consumers, with key source, grants and model access - config, with secrets redacted The overview gains features and a pricing summary. Usage gains lastSeen. usage/models gains offset and more detail fields. Reads are chunked at 1600 keys per round trip and capped at 64000 keys per request. Claude-Session: https://claude.ai/code/session_01V5jYJNxpDFdJkyyExpADrv
This commit is contained in:
@@ -820,3 +820,42 @@ func TestResponseCache_Store_EffectiveTTLDiffersPerGroup_SETCarriesGroupTTL(t *t
|
||||
c.store("k-override", 200, "application/json", []byte(`{"a":1}`), effectiveTTL(c, overrideGrp))
|
||||
c.store("k-inherit", 200, "application/json", []byte(`{"b":2}`), effectiveTTL(c, inheritGrp))
|
||||
}
|
||||
|
||||
// --- admin-redesign WP-A step 6: cachedUsage ---
|
||||
|
||||
// TestCachedUsage_ParsesOpenAIShapedUsageObject proves cachedUsage extracts
|
||||
// prompt_tokens/completion_tokens from a cached response body's top-level
|
||||
// "usage" object — recordCacheHit's own source for a cache HIT's avoided
|
||||
// cost (routes_unified.go).
|
||||
func TestCachedUsage_ParsesOpenAIShapedUsageObject(t *testing.T) {
|
||||
body := []byte(`{"id":"chatcmpl-1","choices":[],"usage":{"prompt_tokens":40,"completion_tokens":10}}`)
|
||||
got := cachedUsage(body)
|
||||
if got.prompt != 40 || got.completion != 10 {
|
||||
t.Errorf("cachedUsage = %+v, want prompt=40 completion=10", got)
|
||||
}
|
||||
}
|
||||
|
||||
// TestCachedUsage_MissingOrMalformed_ReturnsZeroUsage proves a body with
|
||||
// no "usage" object, or one that fails to unmarshal at all, reports a
|
||||
// zero usage{} — a cache hit must still count (chit), it simply has
|
||||
// nothing to value (csave skipped, matching account's own "skip a zero
|
||||
// direction" rule).
|
||||
func TestCachedUsage_MissingOrMalformed_ReturnsZeroUsage(t *testing.T) {
|
||||
cases := []struct {
|
||||
name string
|
||||
body []byte
|
||||
}{
|
||||
{name: "no usage field", body: []byte(`{"id":"chatcmpl-1","choices":[]}`)},
|
||||
{name: "usage is null", body: []byte(`{"usage":null}`)},
|
||||
{name: "malformed JSON", body: []byte(`not json`)},
|
||||
{name: "empty body", body: []byte(``)},
|
||||
}
|
||||
for _, c := range cases {
|
||||
t.Run(c.name, func(t *testing.T) {
|
||||
got := cachedUsage(c.body)
|
||||
if got != (usage{}) {
|
||||
t.Errorf("cachedUsage(%q) = %+v, want the zero value", c.body, got)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user