mirror of
https://github.com/lukaszraczylo/traefik-llmgateway.git
synced 2026-10-18 05:47:38 +00:00
feat(admin): surface usage-provenance summary in the Providers overview
Add adminProviderView.Provenance to GET /admin/api/overview: a compact, per-provider map keyed by provenance kind, populated only for "estimated" and "unbilled" observations. "reported" never appears here, since it is the healthy default and AttemptsDay/FailuresDay already describe it; the full reported/estimated/unbilled breakdown, by request and by token, stays on GET /metrics. buildAdminProvenanceViews mirrors buildAdminLatencyViews' own shape: one batched g.provenance.snapshot() read per response, grouped by provider, so this adds no additional store round trip and keeps the already-large overview response small, matching the existing latency summary's own size budget. A test proves the reported/estimated/unbilled filtering, verified against a real mutation of the exclusion guard before being kept.
This commit is contained in:
@@ -303,9 +303,20 @@ type adminProviderView struct {
|
|||||||
// declared after the scalar block as costing extra GC pointer-scan
|
// declared after the scalar block as costing extra GC pointer-scan
|
||||||
// bytes. See timeout.go's watchdogBody for the identical convention.
|
// bytes. See timeout.go's watchdogBody for the identical convention.
|
||||||
Latency map[string]adminLatencyView `json:"latency,omitempty"`
|
Latency map[string]adminLatencyView `json:"latency,omitempty"`
|
||||||
Type string `json:"type"`
|
// Provenance summarizes non-reported usage accounting for this
|
||||||
BaseURL string `json:"baseUrl"`
|
// provider (feat: expose token-accounting provenance) — keyed by
|
||||||
LastErr string `json:"lastErr,omitempty"`
|
// provenance kind, "estimated" or "unbilled" only (see
|
||||||
|
// adminProvenanceView's own doc comment for why "reported" is never a
|
||||||
|
// key here). Sourced from the SAME in-process g.provenance
|
||||||
|
// accumulator /metrics reads (metrics.go). nil (omitted) for a
|
||||||
|
// provider with no estimated/unbilled outcome yet, or for a
|
||||||
|
// deployment with metrics collection gated off entirely — mirroring
|
||||||
|
// Latency's identical "nil means no observations yet" convention
|
||||||
|
// immediately above.
|
||||||
|
Provenance map[string]adminProvenanceView `json:"provenance,omitempty"`
|
||||||
|
Type string `json:"type"`
|
||||||
|
BaseURL string `json:"baseUrl"`
|
||||||
|
LastErr string `json:"lastErr,omitempty"`
|
||||||
// HealthState is this provider's discovery circuit breaker state
|
// HealthState is this provider's discovery circuit breaker state
|
||||||
// (feat/provider-health): "closed" (normal), "open" (backing off
|
// (feat/provider-health): "closed" (normal), "open" (backing off
|
||||||
// after repeated discovery failures — maybeRefresh skips it until
|
// after repeated discovery failures — maybeRefresh skips it until
|
||||||
@@ -417,6 +428,48 @@ func buildAdminLatencyViews(snaps []latencySnapshot) map[string]map[string]admin
|
|||||||
return out
|
return out
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// adminProvenanceView is one provider's one non-reported provenance
|
||||||
|
// kind's compact usage-accounting summary (feat: expose token-accounting
|
||||||
|
// provenance) — adminProviderView.Provenance's value type, keyed by
|
||||||
|
// provenance kind ("estimated" or "unbilled" — provenanceEstimated/
|
||||||
|
// provenanceUnbilled, metrics.go) in adminProviderView.Provenance's outer
|
||||||
|
// map. "reported" is deliberately never a key here: it is the default,
|
||||||
|
// healthy case AttemptsDay/FailuresDay above already partially describe,
|
||||||
|
// and duplicating its own count would not answer any operator question
|
||||||
|
// this compact surface (buildAdminLatencyViews' own "compact, no
|
||||||
|
// per-model breakdown" ruling, GET /admin/api/overview's ~113KB/5s-poll
|
||||||
|
// budget) is not already better placed to answer — the full reported/
|
||||||
|
// estimated/unbilled breakdown, by request AND by token, lives on
|
||||||
|
// /metrics (llmgateway_usage_provenance_requests_total/-_tokens_total)
|
||||||
|
// for an operator who wants the precise fraction.
|
||||||
|
type adminProvenanceView struct {
|
||||||
|
Requests int64 `json:"requests"`
|
||||||
|
Tokens int64 `json:"tokens"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// buildAdminProvenanceViews groups snaps (g.provenance.snapshot(),
|
||||||
|
// metrics.go) by provider, discarding the "reported" provenance entirely
|
||||||
|
// (adminProvenanceView's own doc comment) — mirroring
|
||||||
|
// buildAdminLatencyViews' identical "compact, provider-keyed outer map"
|
||||||
|
// shape immediately above. A provider with no estimated/unbilled
|
||||||
|
// observations at all is simply absent from the outer map, so
|
||||||
|
// adminOverviewResponse's own lookup (buildAdminOverview, below) yields
|
||||||
|
// nil for it — adminProviderView.Provenance's own documented "nil means
|
||||||
|
// no estimated/unbilled accounting yet".
|
||||||
|
func buildAdminProvenanceViews(snaps []provenanceSnapshot) map[string]map[string]adminProvenanceView {
|
||||||
|
out := make(map[string]map[string]adminProvenanceView)
|
||||||
|
for _, s := range snaps {
|
||||||
|
if s.key.provenance == provenanceReported {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if out[s.key.provider] == nil {
|
||||||
|
out[s.key.provider] = make(map[string]adminProvenanceView)
|
||||||
|
}
|
||||||
|
out[s.key.provider][s.key.provenance] = adminProvenanceView{Requests: s.requests, Tokens: s.tokens}
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
// adminModelMetaView is one upstream model's resolved metadata (feature
|
// adminModelMetaView is one upstream model's resolved metadata (feature
|
||||||
// v0.23: context window, per-token cost) within its provider —
|
// v0.23: context window, per-token cost) within its provider —
|
||||||
// adminProviderView.ModelMeta's value type. Every field is a pointer,
|
// adminProviderView.ModelMeta's value type. Every field is a pointer,
|
||||||
@@ -559,6 +612,10 @@ func (g *Gateway) buildAdminOverview() adminOverviewResponse {
|
|||||||
// for the whole response, mirroring providerUsage's own one-batched-
|
// for the whole response, mirroring providerUsage's own one-batched-
|
||||||
// read-for-every-provider shape above, not a per-provider read.
|
// read-for-every-provider shape above, not a per-provider read.
|
||||||
latencyViews := buildAdminLatencyViews(g.latency.snapshot())
|
latencyViews := buildAdminLatencyViews(g.latency.snapshot())
|
||||||
|
// feat: expose token-accounting provenance — one g.provenance.
|
||||||
|
// snapshot() call for the whole response, mirroring g.latency.
|
||||||
|
// snapshot()'s own one-batched-read-for-every-provider shape above.
|
||||||
|
provenanceViews := buildAdminProvenanceViews(g.provenance.snapshot())
|
||||||
|
|
||||||
providers := make([]adminProviderView, len(snaps))
|
providers := make([]adminProviderView, len(snaps))
|
||||||
mi := 0
|
mi := 0
|
||||||
@@ -593,6 +650,7 @@ func (g *Gateway) buildAdminOverview() adminOverviewResponse {
|
|||||||
ModelRates: modelRates,
|
ModelRates: modelRates,
|
||||||
ModelMeta: modelMeta,
|
ModelMeta: modelMeta,
|
||||||
Latency: latencyViews[s.name],
|
Latency: latencyViews[s.name],
|
||||||
|
Provenance: provenanceViews[s.name],
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -631,6 +631,78 @@ func TestAdminOverview_LatencySummary_AvgReflectsSeededObservations(t *testing.T
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TestAdminOverview_ProvenanceSummary_ExcludesReportedIncludesEstimatedAndUnbilled
|
||||||
|
// proves buildAdminProvenanceViews' own filtering (admin.go, feat:
|
||||||
|
// expose token-accounting provenance): "reported" observations never
|
||||||
|
// surface on this compact admin summary (the operator already sees
|
||||||
|
// AttemptsDay/FailuresDay for the healthy default), while "estimated"
|
||||||
|
// and "unbilled" both do, as DISTINCT keys — never merged.
|
||||||
|
//
|
||||||
|
// MUTATION VERIFIED: removing the `if s.key.provenance ==
|
||||||
|
// provenanceReported { continue }` guard from buildAdminProvenanceViews
|
||||||
|
// (admin.go) made this test fail — zeta.Provenance gained a leaked
|
||||||
|
// "reported" entry (Requests=1, Tokens=15) that must never appear here.
|
||||||
|
// Reverted before committing.
|
||||||
|
func TestAdminOverview_ProvenanceSummary_ExcludesReportedIncludesEstimatedAndUnbilled(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
cfg := newAdminTestConfig()
|
||||||
|
h, gw := newAdminGatewayHandle(t, cfg)
|
||||||
|
|
||||||
|
// zeta: one reported observation (must never surface below) and one
|
||||||
|
// estimated observation.
|
||||||
|
gw.provenance.record("zeta", provenanceReported, 15)
|
||||||
|
gw.provenance.record("zeta", provenanceEstimated, 7)
|
||||||
|
|
||||||
|
// alpha: two unbilled observations, seeded separately to prove
|
||||||
|
// requests accumulates across calls rather than overwriting.
|
||||||
|
gw.provenance.record("alpha", provenanceUnbilled, 0)
|
||||||
|
gw.provenance.record("alpha", provenanceUnbilled, 0)
|
||||||
|
|
||||||
|
rec := httptest.NewRecorder()
|
||||||
|
h.ServeHTTP(rec, adminRequest(http.MethodGet, adminOverviewPath, "sk-admin1"))
|
||||||
|
if rec.Code != http.StatusOK {
|
||||||
|
t.Fatalf("status = %d, want 200, body=%s", rec.Code, rec.Body.String())
|
||||||
|
}
|
||||||
|
var got adminOverviewResponse
|
||||||
|
if err := json.Unmarshal(rec.Body.Bytes(), &got); err != nil {
|
||||||
|
t.Fatalf("decode: %v", err)
|
||||||
|
}
|
||||||
|
var zeta, alpha *adminProviderView
|
||||||
|
for i := range got.Providers {
|
||||||
|
switch got.Providers[i].Name {
|
||||||
|
case "zeta":
|
||||||
|
zeta = &got.Providers[i]
|
||||||
|
case "alpha":
|
||||||
|
alpha = &got.Providers[i]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if zeta == nil || alpha == nil {
|
||||||
|
t.Fatalf("providers = %+v, want entries named zeta and alpha", got.Providers)
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, ok := zeta.Provenance[provenanceReported]; ok {
|
||||||
|
t.Errorf(`zeta.Provenance has a %q entry: %+v — "reported" must never surface on this compact admin summary`, provenanceReported, zeta.Provenance)
|
||||||
|
}
|
||||||
|
est, ok := zeta.Provenance[provenanceEstimated]
|
||||||
|
if !ok {
|
||||||
|
t.Fatalf("zeta.Provenance missing %q entry: %+v", provenanceEstimated, zeta.Provenance)
|
||||||
|
}
|
||||||
|
if est.Requests != 1 || est.Tokens != 7 {
|
||||||
|
t.Errorf("zeta estimated = %+v, want {Requests:1 Tokens:7}", est)
|
||||||
|
}
|
||||||
|
|
||||||
|
unb, ok := alpha.Provenance[provenanceUnbilled]
|
||||||
|
if !ok {
|
||||||
|
t.Fatalf("alpha.Provenance missing %q entry: %+v", provenanceUnbilled, alpha.Provenance)
|
||||||
|
}
|
||||||
|
if unb.Requests != 2 || unb.Tokens != 0 {
|
||||||
|
t.Errorf("alpha unbilled = %+v, want {Requests:2 Tokens:0}", unb)
|
||||||
|
}
|
||||||
|
if _, ok := alpha.Provenance[provenanceEstimated]; ok {
|
||||||
|
t.Errorf(`alpha.Provenance has a %q entry despite only unbilled observations being seeded: %+v — estimated and unbilled must never conflate`, provenanceEstimated, alpha.Provenance)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// --- usage: math against seeded counters ---
|
// --- usage: math against seeded counters ---
|
||||||
|
|
||||||
func TestAdminUsage_MathAgainstSeededCounters(t *testing.T) {
|
func TestAdminUsage_MathAgainstSeededCounters(t *testing.T) {
|
||||||
|
|||||||
Reference in New Issue
Block a user