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
+26
View File
@@ -1824,6 +1824,23 @@ func (l *limiter) accountWith(scopes []limitScope, u usage, costMicros int64, x
// misbehaving or hostile upstream is visible rather than silently
// rewriting a budget. Clamped per-component, so one bad field does not
// discard the other two, which may be perfectly valid.
// The positive direction needs a ceiling too: one absurd figure from an
// upstream (or a saturated cost) would otherwise put a counter past
// every budget until its window, up to 400 days on Redis, expires. No
// real request reports figures anywhere near these bounds, so an
// out-of-range figure is dropped like a negative one.
if u.prompt > maxUsageTokensPerRequest || u.completion > maxUsageTokensPerRequest || costMicros > maxCostMicrosPerRequest {
l.logf("%s", fmt.Sprintf("limits: dropping implausible usage reported by an upstream (prompt=%d completion=%d costMicros=%d)", u.prompt, u.completion, costMicros))
if u.prompt > maxUsageTokensPerRequest {
u.prompt = 0
}
if u.completion > maxUsageTokensPerRequest {
u.completion = 0
}
if costMicros > maxCostMicrosPerRequest {
costMicros = 0
}
}
if u.prompt < 0 || u.completion < 0 || costMicros < 0 {
l.logf("%s", fmt.Sprintf("limits: dropping negative usage reported by an upstream (prompt=%d completion=%d costMicros=%d); these counters are increment-only", u.prompt, u.completion, costMicros))
if u.prompt < 0 {
@@ -2250,6 +2267,15 @@ const (
// scope can never collide with a user, group, provider or target scope.
const kindModel = "model"
// maxUsageTokensPerRequest and maxCostMicrosPerRequest bound what one
// upstream response may add to a token or cost counter. 2^30 tokens is
// far above any model context window, and 2^40 micro-USD is about
// 1.1 million USD.
const (
maxUsageTokensPerRequest = 1 << 30
maxCostMicrosPerRequest = 1 << 40
)
// maxModelScopeIDLen bounds the "provider/model" id withModelScope will
// accept as a counter-scope id. Generous next to any real model id —
// the longest in the built-in table is well under 40 bytes — and far