mirror of
https://github.com/lukaszraczylo/traefik-llmgateway.git
synced 2026-10-16 05:37:28 +00:00
fix(gateway): close billing, limit, routing and translation gaps
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
This commit is contained in:
@@ -1272,12 +1272,27 @@ type adminTargetCountersView struct {
|
||||
// group.allowsMCP/allowsAgent themselves delegate to (auth.go) against
|
||||
// each group's own cloned mcpServers/agents pattern list (groupSummary,
|
||||
// authStore.snapshot), so this view can never disagree with what
|
||||
// handleTargetProxy actually enforces. Access is omitted (nil) when every
|
||||
// configured group can reach this target — "empty meaning all", mirroring
|
||||
// groupSummary's own providers/models/mcpServers/agents omitempty
|
||||
// convention (adminUsageEntryView) — rather than always listing every
|
||||
// group name, which would grow with the group catalog for no reason once
|
||||
// nothing is actually restricted.
|
||||
// handleTargetProxy actually enforces.
|
||||
//
|
||||
// Access's JSON contract (review-auth finding F2, 2026-09 review — fixed
|
||||
// from a prior "omitempty" tag): "access":null means every configured
|
||||
// group can reach this target ("empty meaning all" — adminTargetAccess
|
||||
// returns a nil slice for that case); "access":[] (present, zero-length)
|
||||
// means NO configured group can reach it; "access":["g1",...] lists the
|
||||
// groups that can. The prior omitempty tag made the first two cases
|
||||
// serialize identically (both simply absent from the JSON body), so a
|
||||
// target restricted to zero groups rendered in the webui exactly like an
|
||||
// unrestricted one — the opposite of what its own access rule enforces.
|
||||
// No omitempty here specifically because that distinction depends on nil
|
||||
// vs. non-nil-but-empty, which omitempty collapses. adminTargetAccess's
|
||||
// own return value is unchanged by this fix (it already returned nil for
|
||||
// "all" and a non-nil, possibly zero-length, slice otherwise) — only this
|
||||
// struct's serialization of that value changes. The webui (webui/src/
|
||||
// types/api.ts's AdminTargetView.access, webui/src/lib/target-columns.ts)
|
||||
// must be updated to treat null/undefined and a zero-length array as
|
||||
// DIFFERENT states, distinct from today's "!access?.length means All
|
||||
// groups" check that treats both alike; report this contract to whoever
|
||||
// owns that file.
|
||||
//
|
||||
// Health is feat/target-health's own readout (targetHealthView, below).
|
||||
// Field order (Health first, the struct-typed field, then the strings/
|
||||
@@ -1288,7 +1303,7 @@ type adminTargetView struct {
|
||||
Health adminTargetHealthView `json:"health"`
|
||||
Name string `json:"name"`
|
||||
URL string `json:"url"`
|
||||
Access []string `json:"access,omitempty"`
|
||||
Access []string `json:"access"`
|
||||
Counters adminTargetCountersView `json:"counters"`
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user