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
Billing:
- Passthrough SSE streams now bill the usage the upstream reports.
The gateway falls back to a request-size estimate only on a 2xx
stream or a truncated JSON body.
- A unified stream that fails after it starts is estimated, not
billed at zero.
- Billing uses the built-in LiteLLM price table and honours
modelMeta free: true.
- Pricing entries are validated. Cost arithmetic rounds the total and
saturates on overflow.
- Gemini thinking tokens count as completion tokens.
Limits and storage:
- A rejected request rolls back its counts on the other scopes, so one
user cannot lock out a group.
- Redis EXPIRE replies are checked. Keys recreated without a TTL are
repaired at once.
- A mutating pipeline is not retried after reply bytes arrive.
- History, model totals and target usage report a store outage.
Routing and translation:
- Discovered ids with a provider-like prefix are listed only in
owner/id form, so listing and routing agree.
- Anthropic and Gemini discovery follows pagination.
- The developer role maps to system.
- stream_options.include_usage is honoured on translated streams.
- tool_choice none keeps tools when history holds tool blocks.
- refusal maps to content_filter. Data URI parameters are stripped.
Streaming, retry and failover:
- SSE accepts CR, LF and CRLF line ends. This closes a CR injection
path.
- Retry-After is never jittered. A cancelled wait returns the
context error.
- The watchdog timer stops at end of body.
- Oversized bodies return 413.
- A failed stream ends with an error event.
- Failover restores pre-existing headers. Client cancels do not count
as provider successes.
Also: "admin" is a reserved provider name. metrics.path must start
with "/". Pricing warnings go to the right gateway instance. The admin
targets view sends access as null or [] so the two states differ.
Upstream organisation headers are stripped. The users-file reload
error is logged once per error. Dead code is removed, and test-only
helpers move to test files.
Claude-Session: https://claude.ai/code/session_01V5jYJNxpDFdJkyyExpADrv
The gateway refuses a request for an unpriced model when the caller has a
cost budget. An unpriced model cost 0, so the budget counter was never
written and the budget could never fire. The new
allowUnpricedWithCostBudget setting restores the old behaviour.
The retry loop honours an upstream Retry-After of up to 30s and jitters
its own computed backoff. SSE parsing bounds the total size of one event.
The Redis client can use TLS. Provider base URLs and errors are scrubbed
of credentials before they reach logs or the admin view. The gateway
warns when failover and retry settings multiply into a large number of
outbound calls.
Claude-Session: https://claude.ai/code/session_01V5jYJNxpDFdJkyyExpADrv
I2 (controller ruling): docs/superpowers/specs/2026-08-19-traefik-llmgateway-design.md
described limits as "user overrides group" / "user limit if set, else
group limit, else unlimited" — stale relative to the actual, already-correct
implementation (buildLimitScopes/checkAndCount evaluate every configured
scope and return the first violation). Amended §2 and §7 to state both
scopes are enforced together and the tighter bound wins.
I4 (manifest hedge): .traefik.yml's groups.default was a bare "{}",
the shape empirically reproduced as a decode failure outside this
repo's own (necessarily simplified) yaegi-check harness. Changed to
"providers: []" — same zero semantics, no longer a bare empty object.
manifest_test.go's JSON mirror and drift-check token updated to match;
make yaegi-check still passes (its YAML-subset parser already handles
an inline "[]" under a nested map key).
M9 (pricing provenance): pricing.go's built-in table comment replaced
"verify before release" with a real source note (each provider's own
published list prices, recorded 2026-08, approximate). README's limits
section gained the matching caveat and points to the "pricing" config
override for billing-grade accuracy.
Doc nits: README's Kubernetes section now says the example manifest
applies cleanly once Traefik's CRDs (Middleware, IngressRoute) are
installed, rather than claiming kubectl apply -f works unconditionally.
README's Security notes section no longer claims "no auth-event logging
at all" (see the paired fix: commit) — it now describes what is logged
and that key material never appears.
This commit and its sibling fix: commit are the final wave closing out
the whole-branch review findings for docs/superpowers/sdd/2026-08-20-
traefik-llmgateway.
Gates: go test ./... -count=1 (-race) green, gofmt -l . empty, go vet
clean, make yaegi-check OK, golangci-lint 0 issues.