fix(accounting): bound upstream-controlled usage and counter keys

Passthrough accounting read the model id, the usage figures and the
response size from the upstream response without limits.

- Mint a model counter scope only for ids the provider serves. An
  upstream that echoed new ids created unbounded counter keys.
- Drop a reported token or cost figure above a fixed ceiling. One huge
  figure locked a user out of every budget for the whole window.
- Bill an echoed unpriced model at the price of the model that passed
  the cost-budget gate.
- Cap a body-size prompt estimate at one million tokens.
- Bound the total bytes that concurrent passthrough accounting tees
  hold. A tee that cannot reserve room falls back to the estimate.
- Add cache.accountHits. When true, a cache hit charges the cached usage
  to the user. The default stays false.
This commit is contained in:
2026-10-15 22:19:57 +01:00
parent e5fe48a9a6
commit 8c4bd16a22
7 changed files with 306 additions and 8 deletions
+7
View File
@@ -447,6 +447,11 @@ type CacheConfig struct {
// value above maxCacheMaxBodyBytes is a construction error.
MaxBodyBytes int `json:"maxBodyBytes,omitempty"`
Enabled bool `json:"enabled,omitempty"`
// AccountHits charges the usage a cached response originally reported
// to the user and group that receive the hit, so token and cost
// budgets also bind repeated identical prompts. Defaults to false: a
// hit then adds only to the request counters.
AccountHits bool `json:"accountHits,omitempty"`
}
// BreakerConfig configures the per-provider discovery circuit breaker
@@ -761,6 +766,7 @@ type Gateway struct {
next http.Handler
events *eventLog
bodyAdmission chan struct{}
teeBudget *teeBudget
failoverHealth *requestHealthTracker
targetHealth *targetHealthTracker
limiter *limiter
@@ -1049,6 +1055,7 @@ func newGateway(ctx context.Context, next http.Handler, config *Config, name str
}
}
g.bodyAdmission = make(chan struct{}, bodyAdmissionCap)
g.teeBudget = newTeeBudget(maxAccountingTeeTotalBytes)
if config.Users != nil && config.Users.File != "" {
if err = attachUsersFile(auth, config.Users.File, g); err != nil {