mirror of
https://github.com/Wei-Shaw/sub2api.git
synced 2026-09-01 15:02:58 +08:00
Merge pull request #4616 from zhangxaochen/codex/openai-quota-error
fix(openai): return standard insufficient quota errors
This commit is contained in:
@@ -217,7 +217,7 @@ func apiKeyAuthWithSubscription(apiKeyService *service.APIKeyService, subscripti
|
||||
// Key 状态检查
|
||||
switch apiKey.Status {
|
||||
case service.StatusAPIKeyQuotaExhausted:
|
||||
AbortWithError(c, 429, "API_KEY_QUOTA_EXHAUSTED", "API key 额度已用完")
|
||||
abortWithAPIKeyQuotaError(c)
|
||||
return
|
||||
case service.StatusAPIKeyExpired:
|
||||
AbortWithError(c, 403, "API_KEY_EXPIRED", "API key 已过期")
|
||||
@@ -230,7 +230,7 @@ func apiKeyAuthWithSubscription(apiKeyService *service.APIKeyService, subscripti
|
||||
return
|
||||
}
|
||||
if apiKey.IsQuotaExhausted() {
|
||||
AbortWithError(c, 429, "API_KEY_QUOTA_EXHAUSTED", "API key 额度已用完")
|
||||
abortWithAPIKeyQuotaError(c)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -305,6 +305,34 @@ func hasAPIKeyCredentialInput(c *gin.Context) bool {
|
||||
c.GetHeader("x-goog-api-key") != ""
|
||||
}
|
||||
|
||||
func abortWithAPIKeyQuotaError(c *gin.Context) {
|
||||
const message = "API key 额度已用完"
|
||||
if isOpenAICompatibleAPIKeyRequest(c) {
|
||||
abortWithOpenAIQuotaError(c, http.StatusTooManyRequests, message)
|
||||
return
|
||||
}
|
||||
AbortWithError(c, http.StatusTooManyRequests, "API_KEY_QUOTA_EXHAUSTED", message)
|
||||
}
|
||||
|
||||
func isOpenAICompatibleAPIKeyRequest(c *gin.Context) bool {
|
||||
if c == nil || c.Request == nil || c.Request.URL == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
path := strings.TrimRight(c.Request.URL.Path, "/")
|
||||
for _, root := range []string{
|
||||
"/v1/responses",
|
||||
"/openai/v1/responses",
|
||||
"/responses",
|
||||
"/backend-api/codex/responses",
|
||||
} {
|
||||
if path == root || strings.HasPrefix(path, root+"/") {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func isAsyncImageTaskRead(method, path string) bool {
|
||||
if method != http.MethodGet {
|
||||
return false
|
||||
|
||||
@@ -1427,6 +1427,78 @@ func TestAPIKeyAuthRejectsExhaustedBalance(t *testing.T) {
|
||||
requireAPIKeyAuthError(t, w, "INSUFFICIENT_BALANCE", "Insufficient account balance")
|
||||
}
|
||||
|
||||
func TestAPIKeyAuthOpenAIQuotaErrorFormat(t *testing.T) {
|
||||
gin.SetMode(gin.TestMode)
|
||||
|
||||
user := &service.User{ID: 11, Role: service.RoleUser, Status: service.StatusActive, Balance: 10}
|
||||
group := &service.Group{ID: 8, Platform: service.PlatformOpenAI, Status: service.StatusActive}
|
||||
apiKey := &service.APIKey{
|
||||
ID: 105, UserID: user.ID, Key: "openai-quota-exhausted", Status: service.StatusAPIKeyQuotaExhausted,
|
||||
User: user, Group: group, GroupID: &group.ID,
|
||||
}
|
||||
apiKeyRepo := &stubApiKeyRepo{getByKey: func(ctx context.Context, key string) (*service.APIKey, error) {
|
||||
if key != apiKey.Key {
|
||||
return nil, service.ErrAPIKeyNotFound
|
||||
}
|
||||
clone := *apiKey
|
||||
userClone := *user
|
||||
clone.User = &userClone
|
||||
return &clone, nil
|
||||
}}
|
||||
|
||||
cfg := &config.Config{RunMode: config.RunModeStandard}
|
||||
router := newAuthTestRouter(service.NewAPIKeyService(apiKeyRepo, nil, nil, nil, nil, nil, cfg), nil, cfg)
|
||||
w := httptest.NewRecorder()
|
||||
req := httptest.NewRequest(http.MethodPost, "/v1/responses", nil)
|
||||
req.Header.Set("x-api-key", apiKey.Key)
|
||||
router.ServeHTTP(w, req)
|
||||
|
||||
require.Equal(t, http.StatusTooManyRequests, w.Code)
|
||||
var response struct {
|
||||
Error struct {
|
||||
Message string `json:"message"`
|
||||
Type string `json:"type"`
|
||||
Param *string `json:"param"`
|
||||
Code string `json:"code"`
|
||||
} `json:"error"`
|
||||
}
|
||||
require.NoError(t, json.Unmarshal(w.Body.Bytes(), &response))
|
||||
require.Equal(t, "API key 额度已用完", response.Error.Message)
|
||||
require.Equal(t, "insufficient_quota", response.Error.Type)
|
||||
require.Nil(t, response.Error.Param)
|
||||
require.Equal(t, "insufficient_quota", response.Error.Code)
|
||||
}
|
||||
|
||||
func TestAPIKeyAuthQuotaErrorKeepsLegacyFormatOutsideResponses(t *testing.T) {
|
||||
gin.SetMode(gin.TestMode)
|
||||
|
||||
user := &service.User{ID: 11, Role: service.RoleUser, Status: service.StatusActive, Balance: 10}
|
||||
group := &service.Group{ID: 8, Platform: service.PlatformOpenAI, Status: service.StatusActive}
|
||||
apiKey := &service.APIKey{
|
||||
ID: 105, UserID: user.ID, Key: "openai-quota-exhausted", Status: service.StatusAPIKeyQuotaExhausted,
|
||||
User: user, Group: group, GroupID: &group.ID,
|
||||
}
|
||||
apiKeyRepo := &stubApiKeyRepo{getByKey: func(ctx context.Context, key string) (*service.APIKey, error) {
|
||||
if key != apiKey.Key {
|
||||
return nil, service.ErrAPIKeyNotFound
|
||||
}
|
||||
clone := *apiKey
|
||||
userClone := *user
|
||||
clone.User = &userClone
|
||||
return &clone, nil
|
||||
}}
|
||||
|
||||
cfg := &config.Config{RunMode: config.RunModeStandard}
|
||||
router := newAuthTestRouter(service.NewAPIKeyService(apiKeyRepo, nil, nil, nil, nil, nil, cfg), nil, cfg)
|
||||
w := httptest.NewRecorder()
|
||||
req := httptest.NewRequest(http.MethodPost, "/v1/messages", nil)
|
||||
req.Header.Set("x-api-key", apiKey.Key)
|
||||
router.ServeHTTP(w, req)
|
||||
|
||||
require.Equal(t, http.StatusTooManyRequests, w.Code)
|
||||
requireAPIKeyAuthError(t, w, "API_KEY_QUOTA_EXHAUSTED", "API key 额度已用完")
|
||||
}
|
||||
|
||||
func newAuthTestRouter(apiKeyService *service.APIKeyService, subscriptionService *service.SubscriptionService, cfg *config.Config) *gin.Engine {
|
||||
router := gin.New()
|
||||
router.Use(gin.HandlerFunc(NewAPIKeyAuthMiddleware(apiKeyService, subscriptionService, cfg)))
|
||||
@@ -1434,6 +1506,8 @@ func newAuthTestRouter(apiKeyService *service.APIKeyService, subscriptionService
|
||||
c.JSON(http.StatusOK, gin.H{"ok": true})
|
||||
}
|
||||
router.GET("/t", ok)
|
||||
router.POST("/v1/responses", ok)
|
||||
router.POST("/v1/messages", ok)
|
||||
router.GET("/v1/usage", ok)
|
||||
router.GET("/v1/sub2api/billing", ok)
|
||||
return router
|
||||
|
||||
@@ -80,6 +80,19 @@ func AbortWithError(c *gin.Context, statusCode int, code, message string) {
|
||||
c.Abort()
|
||||
}
|
||||
|
||||
// abortWithOpenAIQuotaError writes the OpenAI-compatible insufficient quota response.
|
||||
func abortWithOpenAIQuotaError(c *gin.Context, statusCode int, message string) {
|
||||
c.JSON(statusCode, gin.H{
|
||||
"error": gin.H{
|
||||
"message": message,
|
||||
"type": "insufficient_quota",
|
||||
"param": nil,
|
||||
"code": "insufficient_quota",
|
||||
},
|
||||
})
|
||||
c.Abort()
|
||||
}
|
||||
|
||||
// ──────────────────────────────────────────────────────────
|
||||
// RequireGroupAssignment — 未分组 Key 拦截中间件
|
||||
// ──────────────────────────────────────────────────────────
|
||||
|
||||
Reference in New Issue
Block a user