Compare commits

...
Sign in to create a new pull request.

2 commits

Author SHA1 Message Date
Virgil
781e0ee3d6 fix(ratelimit): enforce negative token validation before quota lookup
Some checks failed
Security Scan / security (push) Has been cancelled
Test / test (push) Has been cancelled
2026-04-03 07:37:23 +00:00
Virgil
2ad4870bd0 fix(ratelimit): initialise state on invalid decides
Some checks failed
Security Scan / security (push) Successful in 8s
Test / test (push) Failing after 14m39s
Co-Authored-By: Virgil <virgil@lethean.io>
2026-04-01 04:47:30 +00:00
2 changed files with 29 additions and 11 deletions

View file

@ -249,7 +249,12 @@ func (rl *RateLimiter) Load() error {
return err
}
return yaml.Unmarshal([]byte(content), rl)
if err := yaml.Unmarshal([]byte(content), rl); err != nil {
return err
}
ensureMaps(rl)
return nil
}
// loadSQLite reads quotas and state from the SQLite backend.
@ -590,21 +595,20 @@ func (rl *RateLimiter) AllStats() map[string]ModelStats {
// Decide returns structured allow/deny information for an estimated request.
// It never records usage; call RecordUsage after a successful decision.
func (rl *RateLimiter) Decide(model string, estimatedTokens int) Decision {
if estimatedTokens < 0 {
return Decision{
Allowed: false,
Code: DecisionInvalidTokens,
Reason: "estimated tokens must be non-negative",
Stats: rl.Stats(model),
}
}
rl.mu.Lock()
defer rl.mu.Unlock()
now := time.Now()
decision := Decision{}
if estimatedTokens < 0 {
decision.Allowed = false
decision.Code = DecisionInvalidTokens
decision.Reason = "estimated tokens must be non-negative"
decision.Stats = rl.snapshotLocked(model)
return decision
}
quota, ok := rl.Quotas[model]
if !ok {
decision.Allowed = true
@ -830,6 +834,15 @@ func newConfiguredRateLimiter(cfg Config) *RateLimiter {
return rl
}
func ensureMaps(rl *RateLimiter) {
if rl.Quotas == nil {
rl.Quotas = make(map[string]ModelQuota)
}
if rl.State == nil {
rl.State = make(map[string]*UsageStats)
}
}
func applyConfig(rl *RateLimiter, cfg Config) {
profiles := DefaultProfiles()
providers := cfg.Providers

View file

@ -363,12 +363,17 @@ func TestRatelimit_Decide_Good(t *testing.T) {
t.Run("negative estimate returns invalid decision", func(t *testing.T) {
rl := newTestLimiter(t)
model := "neg"
rl.Quotas[model] = ModelQuota{MaxRPM: 5, MaxTPM: 50, MaxRPD: 5}
decision := rl.Decide("neg", -5)
decision := rl.Decide(model, -5)
assert.False(t, decision.Allowed)
assert.Equal(t, DecisionInvalidTokens, decision.Code)
assert.Zero(t, decision.RetryAfter)
require.Contains(t, rl.State, model)
require.NotNil(t, rl.State[model])
assert.Equal(t, 0, rl.State[model].DayCount)
})
}