AI Gateway Go 0.10.0 源码快照 + 旗舰版需求规划报告

M0-M7 已完成:核心网关(身份/RBAC/TOTP/OIDC/SAML/Provider/配额/路由/内容策略/审计/定价)+ 资源市场(MCP/Skills/数字员工)。
含 22 个 PostgreSQL 迁移、管理端/门户端前端源码、OpenAPI 契约、部署 compose。

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
ben
2026-08-12 11:45:54 +08:00
commit 5759c1862e
807 changed files with 114727 additions and 0 deletions
+129
View File
@@ -0,0 +1,129 @@
package gateway
import (
"context"
"errors"
"fmt"
"strconv"
"time"
"aigateway.local/core/internal/apikey"
"github.com/redis/go-redis/v9"
)
var ErrTokenQuotaUnavailable = errors.New("token quota unavailable")
type TokenReservation struct {
Allowed bool
APIKeyID string
CounterKey string
Reserved int64
Limit int64
Remaining int64
ResetAt time.Time
}
type TokenQuotaController interface {
Reserve(context.Context, apikey.Principal, int64, time.Time) (TokenReservation, error)
Commit(context.Context, TokenReservation, int64) error
}
type RedisTokenQuotaController struct {
client *redis.Client
reserveScript *redis.Script
commitScript *redis.Script
}
func NewRedisTokenQuotaController(client *redis.Client) *RedisTokenQuotaController {
return &RedisTokenQuotaController{
client: client, reserveScript: redis.NewScript(tokenReserveScript), commitScript: redis.NewScript(tokenCommitScript),
}
}
func (c *RedisTokenQuotaController) Reserve(ctx context.Context, principal apikey.Principal, estimate int64, now time.Time) (TokenReservation, error) {
if principal.APIKeyID == "" {
return TokenReservation{Allowed: true}, nil
}
if estimate < 0 {
estimate = 0
}
// Accounts without a monthly token quota never need a reservation and must
// not create a pointless monthly counter key in Redis.
if principal.MonthlyTokenQuota == 0 {
return TokenReservation{Allowed: true}, nil
}
if c == nil || c.client == nil {
return TokenReservation{}, ErrTokenQuotaUnavailable
}
now = now.UTC()
reset := time.Date(now.Year(), now.Month()+1, 1, 0, 0, 0, 0, time.UTC)
key := apikey.MonthlyTokenUsageKey(principal.APIKeyID, now)
result, err := c.reserveScript.Run(ctx, c.client, []string{key}, estimate, principal.MonthlyTokenQuota, int64(reset.Sub(now).Seconds())+86400).Slice()
if err != nil || len(result) != 2 {
if principal.MonthlyTokenQuota == 0 {
return TokenReservation{Allowed: true}, nil
}
return TokenReservation{}, fmt.Errorf("%w: %v", ErrTokenQuotaUnavailable, err)
}
allowed, err := redisInteger(result[0])
if err != nil {
return TokenReservation{}, ErrTokenQuotaUnavailable
}
current, err := redisInteger(result[1])
if err != nil {
return TokenReservation{}, ErrTokenQuotaUnavailable
}
return TokenReservation{
Allowed: allowed == 1, APIKeyID: principal.APIKeyID, CounterKey: key, Reserved: estimate,
Limit: principal.MonthlyTokenQuota, Remaining: max(principal.MonthlyTokenQuota-current, 0), ResetAt: reset,
}, nil
}
func (c *RedisTokenQuotaController) Commit(ctx context.Context, reservation TokenReservation, actual int64) error {
if c == nil || c.client == nil || reservation.CounterKey == "" || !reservation.Allowed {
return nil
}
if actual < 0 {
actual = 0
}
if _, err := c.commitScript.Run(ctx, c.client, []string{reservation.CounterKey}, actual-reservation.Reserved).Result(); err != nil {
return fmt.Errorf("%w: %v", ErrTokenQuotaUnavailable, err)
}
return nil
}
const tokenReserveScript = `
local estimate = tonumber(ARGV[1])
local quota = tonumber(ARGV[2])
local current = tonumber(redis.call('GET', KEYS[1]) or '0')
if quota > 0 and current + estimate > quota then
return {0, current}
end
if estimate > 0 then
current = redis.call('INCRBY', KEYS[1], estimate)
if current == estimate then redis.call('EXPIRE', KEYS[1], tonumber(ARGV[3])) end
elseif redis.call('EXISTS', KEYS[1]) == 0 then
redis.call('SET', KEYS[1], 0, 'EX', tonumber(ARGV[3]))
end
return {1, current}
`
const tokenCommitScript = `
local delta = tonumber(ARGV[1])
local current = tonumber(redis.call('GET', KEYS[1]) or '0')
local updated = current + delta
if updated < 0 then updated = 0 end
redis.call('SET', KEYS[1], updated, 'KEEPTTL')
return updated
`
func writeTokenQuotaHeaders(header mapHeader, reservation TokenReservation) {
if reservation.Limit <= 0 || reservation.ResetAt.IsZero() {
return
}
header.Set("X-TokenLimit-Limit", strconv.FormatInt(reservation.Limit, 10))
header.Set("X-TokenLimit-Remaining", strconv.FormatInt(max(reservation.Remaining, 0), 10))
header.Set("X-TokenLimit-Reset", strconv.FormatInt(reservation.ResetAt.Unix(), 10))
}
type mapHeader interface{ Set(string, string) }