mirror of
https://github.com/lukaszraczylo/traefik-llmgateway.git
synced 2026-10-18 05:47:38 +00:00
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:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user