Skip to content

Commit 21012de

Browse files
mudlerlocalai-bot
authored andcommitted
feat: add quota system (mudler#9090)
* feat: add quota system Signed-off-by: Ettore Di Giacinto <mudler@localai.io> * Fix tests Signed-off-by: Ettore Di Giacinto <mudler@localai.io> --------- Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
1 parent 9f8b9f0 commit 21012de

9 files changed

Lines changed: 993 additions & 15 deletions

File tree

core/http/app.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -221,6 +221,7 @@ func API(application *application.Application) (*echo.Echo, error) {
221221
if application.AuthDB() != nil {
222222
e.Use(auth.RequireRouteFeature(application.AuthDB()))
223223
e.Use(auth.RequireModelAccess(application.AuthDB()))
224+
e.Use(auth.RequireQuota(application.AuthDB()))
224225
}
225226

226227
// CORS middleware

core/http/auth/db.go

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,10 +33,14 @@ func InitDB(databaseURL string) (*gorm.DB, error) {
3333
return nil, fmt.Errorf("failed to open auth database: %w", err)
3434
}
3535

36-
if err := db.AutoMigrate(&User{}, &Session{}, &UserAPIKey{}, &UsageRecord{}, &UserPermission{}, &InviteCode{}); err != nil {
36+
if err := db.AutoMigrate(&User{}, &Session{}, &UserAPIKey{}, &UsageRecord{}, &UserPermission{}, &InviteCode{}, &QuotaRule{}); err != nil {
3737
return nil, fmt.Errorf("failed to migrate auth tables: %w", err)
3838
}
3939

40+
// Backfill: users created before the provider column existed have an empty
41+
// provider — treat them as local accounts so the UI can identify them.
42+
db.Exec("UPDATE users SET provider = ? WHERE provider = '' OR provider IS NULL", ProviderLocal)
43+
4044
// Create composite index on users(provider, subject) for fast OAuth lookups
4145
if err := db.Exec("CREATE INDEX IF NOT EXISTS idx_users_provider_subject ON users(provider, subject)").Error; err != nil {
4246
// Ignore error on postgres if index already exists

core/http/auth/middleware.go

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"bytes"
55
"crypto/subtle"
66
"encoding/json"
7+
"fmt"
78
"io"
89
"net/http"
910
"strings"
@@ -370,6 +371,55 @@ func extractModelFromRequest(c echo.Context) string {
370371
return ""
371372
}
372373

374+
// RequireQuota returns a global middleware that enforces per-user quota rules.
375+
// If no auth DB is provided, it's a no-op. Admin users always bypass quotas.
376+
// Only inference routes (those listed in RouteFeatureRegistry) count toward quota.
377+
func RequireQuota(db *gorm.DB) echo.MiddlewareFunc {
378+
if db == nil {
379+
return NoopMiddleware()
380+
}
381+
// Pre-build lookup set from RouteFeatureRegistry — only these routes
382+
// should count toward quota. Mirrors RequireRouteFeature's approach.
383+
inferenceRoutes := map[string]bool{}
384+
for _, rf := range RouteFeatureRegistry {
385+
inferenceRoutes[rf.Method+":"+rf.Pattern] = true
386+
}
387+
return func(next echo.HandlerFunc) echo.HandlerFunc {
388+
return func(c echo.Context) error {
389+
// Only enforce quotas on inference routes
390+
path := c.Path()
391+
method := c.Request().Method
392+
if !inferenceRoutes[method+":"+path] && !inferenceRoutes["*:"+path] {
393+
return next(c)
394+
}
395+
396+
user := GetUser(c)
397+
if user == nil {
398+
return next(c)
399+
}
400+
if user.Role == RoleAdmin {
401+
return next(c)
402+
}
403+
404+
model := extractModelFromRequest(c)
405+
406+
exceeded, retryAfter, msg := QuotaExceeded(db, user.ID, model)
407+
if exceeded {
408+
c.Response().Header().Set("Retry-After", fmt.Sprintf("%d", retryAfter))
409+
return c.JSON(http.StatusTooManyRequests, schema.ErrorResponse{
410+
Error: &schema.APIError{
411+
Message: msg,
412+
Code: http.StatusTooManyRequests,
413+
Type: "quota_exceeded",
414+
},
415+
})
416+
}
417+
418+
return next(c)
419+
}
420+
}
421+
}
422+
373423
// tryAuthenticate attempts to authenticate the request using the database.
374424
func tryAuthenticate(c echo.Context, db *gorm.DB, appConfig *config.ApplicationConfig) *User {
375425
hmacSecret := appConfig.Auth.APIKeyHMACSecret

0 commit comments

Comments
 (0)