// Package proxy API 网关核心:代理 /v1/chat/completions、/v1/responses、/v1/models。 // M1:对 OpenAI 渠道直通(passthrough),不转格式;M3 起加入协议转换。 package proxy import ( "errors" "net/http" "strings" "time" "github.com/gin-gonic/gin" "github.com/openteam/server/internal/channel" "github.com/openteam/server/internal/pkg/apikey" "github.com/openteam/server/internal/pkg/crypto" "github.com/openteam/server/internal/store" "github.com/openteam/server/internal/usage" "gorm.io/gorm" ) const ( CtxUserID = "proxy_user_id" CtxKeyID = "proxy_key_id" CtxTrace = "proxy_trace_id" ) type Gateway struct { db *gorm.DB ch *channel.Service rec *usage.Recorder enc *crypto.Encryptor hc *http.Client } func NewGateway(db *gorm.DB, enc *crypto.Encryptor, rec *usage.Recorder) *Gateway { return &Gateway{ db: db, ch: channel.NewService(db, enc), rec: rec, enc: enc, hc: &http.Client{Timeout: 120 * time.Second}, } } // Auth 代理鉴权中间件:Bearer sk-xxx → 哈希查表 → 校验状态/过期/模型白名单。 func (g *Gateway) Auth(c *gin.Context) { auth := c.GetHeader("Authorization") key := strings.TrimPrefix(auth, "Bearer ") key = strings.TrimSpace(key) if !apikey.Valid(key) { openAIError(c, http.StatusUnauthorized, "invalid_api_key", "Invalid API key format. Expected: Bearer sk-...") c.Abort() return } hash := apikey.Hash(key) var k store.APIKey if err := g.db.Where("key_hash = ? AND status = ?", hash, store.KeyStatusActive).First(&k).Error; err != nil { openAIError(c, http.StatusUnauthorized, "invalid_api_key", "Invalid API key") c.Abort() return } var u store.User if err := g.db.First(&u, k.UserID).Error; err != nil || u.Status != store.UserStatusActive { openAIError(c, http.StatusForbidden, "user_disabled", "User account is disabled") c.Abort() return } if k.ExpiresAt != nil && time.Now().After(*k.ExpiresAt) { openAIError(c, http.StatusUnauthorized, "key_expired", "API key has expired") c.Abort() return } c.Set(CtxUserID, u.ID) c.Set(CtxKeyID, k.ID) c.Set(CtxTrace, newTraceID()) g.db.Model(&store.APIKey{}).Where("id = ?", k.ID).Update("last_used_at", time.Now()) c.Next() } // Handle 路由到对应协议处理器。 func (g *Gateway) Handle(c *gin.Context) { switch { case c.Request.URL.Path == "/v1/chat/completions": g.chatCompletions(c) case c.Request.URL.Path == "/v1/responses": g.responses(c) case c.Request.URL.Path == "/v1/models" && c.Request.Method == http.MethodGet: g.models(c) default: openAIError(c, http.StatusNotFound, "not_found", "Unknown endpoint: "+c.Request.URL.Path) } } // models GET /v1/models:返回启用的全局模型(OpenAI 风格)。 func (g *Gateway) models(c *gin.Context) { var ms []store.Model if err := g.db.Where("enabled = ?", true).Order("sort ASC, id ASC").Find(&ms).Error; err != nil { openAIError(c, http.StatusInternalServerError, "internal_error", "failed to load models") return } data := make([]gin.H, 0, len(ms)) for _, m := range ms { data = append(data, gin.H{ "id": m.Name, "object": "model", "created": m.CreatedAt.Unix(), "owned_by": "openteam", }) } c.JSON(http.StatusOK, gin.H{"object": "list", "data": data}) } // resolveUser 取当前用户(含余额)。 func (g *Gateway) resolveUser(c *gin.Context) (*store.User, bool) { uid, _ := c.Get(CtxUserID) var u store.User if err := g.db.First(&u, uid).Error; err != nil { openAIError(c, http.StatusUnauthorized, "invalid_api_key", "Invalid API key") return nil, false } return &u, true } // checkBalance 余额不足返回 402(PLANNING §4.4.3)。 func (g *Gateway) checkBalance(c *gin.Context, u *store.User) bool { if u.Balance <= 0 { openAIError(c, http.StatusPaymentRequired, "insufficient_balance", "Insufficient balance. Please recharge or contact admin.") return false } return true } var errNoChannel = errors.New("no available channel")