Files
ResolveSpec/pkg/aiproxy/ratelimit.go
T
warkanum 63cb0d22eb feat(aiproxy): add authenticated proxy for OpenAI-compatible APIs and MCP servers
Hides upstream URLs and keys behind the security layer. Per-upstream roles,
model/tool allowlists, per-user rate limits, Before/AfterProxy hooks, audit
sink, Prometheus metrics (metrics.AIProxyRecorder) and upstreams loaded from
the resolvespec_ai_proxies stored procedure.
2026-10-07 23:39:40 +02:00

81 lines
1.5 KiB
Go

package aiproxy
import (
"math"
"strings"
"sync"
"time"
"golang.org/x/time/rate"
)
const (
limiterSweepEvery = 5 * time.Minute
limiterIdleAfter = 10 * time.Minute
)
type limiterEntry struct {
lim *rate.Limiter
last time.Time
}
// limiters keeps one token bucket per key and drops idle ones.
type limiters struct {
mu sync.Mutex
entries map[string]*limiterEntry
lastSweep time.Time
}
func newLimiters() *limiters {
return &limiters{entries: make(map[string]*limiterEntry), lastSweep: time.Now()}
}
// allow takes a token. When denied it returns how long to wait.
func (l *limiters) allow(key string, rl *RateLimit) (bool, time.Duration) {
now := time.Now()
l.mu.Lock()
defer l.mu.Unlock()
if now.Sub(l.lastSweep) > limiterSweepEvery {
for k, e := range l.entries {
if now.Sub(e.last) > limiterIdleAfter {
delete(l.entries, k)
}
}
l.lastSweep = now
}
e, ok := l.entries[key]
if !ok {
burst := rl.Burst
if burst < 1 {
burst = int(math.Ceil(rl.PerSecond))
if burst < 1 {
burst = 1
}
}
e = &limiterEntry{lim: rate.NewLimiter(rate.Limit(rl.PerSecond), burst)}
l.entries[key] = e
}
e.last = now
res := e.lim.ReserveN(now, 1)
if d := res.DelayFrom(now); d > 0 {
res.CancelAt(now)
return false, d
}
return true, 0
}
// forget drops the buckets of an upstream (after it was replaced or removed).
func (l *limiters) forget(upstream string) {
prefix := upstream + "|"
l.mu.Lock()
defer l.mu.Unlock()
for k := range l.entries {
if strings.HasPrefix(k, prefix) {
delete(l.entries, k)
}
}
}