Merge pull request #2643 from Arron196/fix/ops-sla-local-client-errors
fix(ops): 排除本地客户端限制错误的 SLA 计数
This commit is contained in:
commit
f2d072ffc7
@ -41,13 +41,18 @@ const (
|
|||||||
opsErrInsufficientQuota = "insufficient_quota"
|
opsErrInsufficientQuota = "insufficient_quota"
|
||||||
|
|
||||||
// 上游错误码常量 — 错误分类 (normalizeOpsErrorType / classifyOpsPhase / classifyOpsIsBusinessLimited)
|
// 上游错误码常量 — 错误分类 (normalizeOpsErrorType / classifyOpsPhase / classifyOpsIsBusinessLimited)
|
||||||
opsCodeInsufficientBalance = "INSUFFICIENT_BALANCE"
|
opsCodeInsufficientBalance = "INSUFFICIENT_BALANCE"
|
||||||
opsCodeUsageLimitExceeded = "USAGE_LIMIT_EXCEEDED"
|
opsCodeUsageLimitExceeded = "USAGE_LIMIT_EXCEEDED"
|
||||||
opsCodeSubscriptionNotFound = "SUBSCRIPTION_NOT_FOUND"
|
opsCodeSubscriptionNotFound = "SUBSCRIPTION_NOT_FOUND"
|
||||||
opsCodeSubscriptionInvalid = "SUBSCRIPTION_INVALID"
|
opsCodeSubscriptionInvalid = "SUBSCRIPTION_INVALID"
|
||||||
opsCodeUserInactive = "USER_INACTIVE"
|
opsCodeUserInactive = "USER_INACTIVE"
|
||||||
opsCodeInvalidAPIKey = "INVALID_API_KEY"
|
opsCodeInvalidAPIKey = "INVALID_API_KEY"
|
||||||
opsCodeAPIKeyRequired = "API_KEY_REQUIRED"
|
opsCodeAPIKeyRequired = "API_KEY_REQUIRED"
|
||||||
|
opsCodeAPIKeyExpired = "API_KEY_EXPIRED"
|
||||||
|
opsCodeAPIKeyDisabled = "API_KEY_DISABLED"
|
||||||
|
opsCodeUserNotFound = "USER_NOT_FOUND"
|
||||||
|
opsCodeAPIKeyQuotaExhausted = "API_KEY_QUOTA_EXHAUSTED"
|
||||||
|
opsCodeAPIKeyQueryDeprecated = "api_key_in_query_deprecated"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@ -1089,8 +1094,7 @@ func classifyOpsPhase(errType, message, code string) string {
|
|||||||
if isOpsClientAuthError(code, msg) {
|
if isOpsClientAuthError(code, msg) {
|
||||||
return "auth"
|
return "auth"
|
||||||
}
|
}
|
||||||
switch strings.TrimSpace(code) {
|
if isOpsLocalBusinessLimitError(code, msg) {
|
||||||
case opsCodeInsufficientBalance, opsCodeUsageLimitExceeded, opsCodeSubscriptionNotFound, opsCodeSubscriptionInvalid:
|
|
||||||
return "request"
|
return "request"
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1149,8 +1153,10 @@ func classifyOpsErrorLog(c *gin.Context, errType, message, code string, status i
|
|||||||
if routingCapacityLimited {
|
if routingCapacityLimited {
|
||||||
phase = "routing"
|
phase = "routing"
|
||||||
}
|
}
|
||||||
localClientAuthError := !upstreamError && phase == "auth" && isOpsClientAuthError(code, strings.ToLower(message))
|
msg := strings.ToLower(message)
|
||||||
isBusinessLimited = routingCapacityLimited || clientBusinessLimited || classifyOpsIsBusinessLimited(errType, phase, code, status, message, localClientAuthError)
|
localClientAuthError := !upstreamError && phase == "auth" && isOpsClientAuthError(code, msg)
|
||||||
|
localBusinessLimited := !upstreamError && classifyOpsIsBusinessLimited(errType, phase, code, status, message, localClientAuthError)
|
||||||
|
isBusinessLimited = routingCapacityLimited || (clientBusinessLimited && !upstreamError) || localBusinessLimited
|
||||||
errorOwner = classifyOpsErrorOwner(phase, message)
|
errorOwner = classifyOpsErrorOwner(phase, message)
|
||||||
errorSource = classifyOpsErrorSource(phase, message)
|
errorSource = classifyOpsErrorSource(phase, message)
|
||||||
return phase, isBusinessLimited, errorOwner, errorSource
|
return phase, isBusinessLimited, errorOwner, errorSource
|
||||||
@ -1160,8 +1166,7 @@ func classifyOpsIsBusinessLimited(errType, phase, code string, status int, messa
|
|||||||
if len(localClientAuthError) > 0 && localClientAuthError[0] {
|
if len(localClientAuthError) > 0 && localClientAuthError[0] {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
switch strings.TrimSpace(code) {
|
if isOpsLocalBusinessLimitError(code, strings.ToLower(message)) {
|
||||||
case opsCodeInsufficientBalance, opsCodeUsageLimitExceeded, opsCodeSubscriptionNotFound, opsCodeSubscriptionInvalid, opsCodeUserInactive:
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
if phase == "billing" || phase == "concurrency" {
|
if phase == "billing" || phase == "concurrency" {
|
||||||
@ -1178,10 +1183,45 @@ func classifyOpsIsBusinessLimited(errType, phase, code string, status int, messa
|
|||||||
|
|
||||||
func isOpsClientAuthError(code string, msg string) bool {
|
func isOpsClientAuthError(code string, msg string) bool {
|
||||||
switch strings.TrimSpace(code) {
|
switch strings.TrimSpace(code) {
|
||||||
case opsCodeInvalidAPIKey, opsCodeAPIKeyRequired:
|
case opsCodeInvalidAPIKey,
|
||||||
|
opsCodeAPIKeyRequired,
|
||||||
|
opsCodeAPIKeyExpired,
|
||||||
|
opsCodeAPIKeyDisabled,
|
||||||
|
opsCodeUserNotFound,
|
||||||
|
opsCodeUserInactive:
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
return strings.Contains(msg, "invalid api key") || strings.Contains(msg, "api key is required")
|
return strings.Contains(msg, "invalid api key") ||
|
||||||
|
strings.Contains(msg, "api key is required") ||
|
||||||
|
strings.Contains(msg, "api key is disabled") ||
|
||||||
|
strings.Contains(msg, "user associated with api key not found") ||
|
||||||
|
strings.Contains(msg, "user account is not active")
|
||||||
|
}
|
||||||
|
|
||||||
|
func isOpsLocalBusinessLimitError(code string, msg string) bool {
|
||||||
|
switch strings.TrimSpace(code) {
|
||||||
|
case opsCodeInsufficientBalance,
|
||||||
|
opsCodeUsageLimitExceeded,
|
||||||
|
opsCodeSubscriptionNotFound,
|
||||||
|
opsCodeSubscriptionInvalid,
|
||||||
|
opsCodeAPIKeyQuotaExhausted,
|
||||||
|
opsCodeAPIKeyQueryDeprecated:
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return strings.Contains(msg, "api key in query parameter is deprecated") ||
|
||||||
|
strings.Contains(msg, "query parameter api_key is deprecated") ||
|
||||||
|
strings.Contains(msg, "no active subscription found for this group") ||
|
||||||
|
strings.Contains(msg, opsErrInsufficientBalance) ||
|
||||||
|
strings.Contains(msg, "insufficient account balance") ||
|
||||||
|
strings.Contains(msg, "api key group platform is not gemini") ||
|
||||||
|
strings.Contains(msg, "api key 额度已用完") ||
|
||||||
|
strings.Contains(msg, "api key 5小时限额已用完") ||
|
||||||
|
strings.Contains(msg, "api key 日限额已用完") ||
|
||||||
|
strings.Contains(msg, "api key 7天限额已用完") ||
|
||||||
|
strings.Contains(msg, "daily usage limit exceeded") ||
|
||||||
|
strings.Contains(msg, "weekly usage limit exceeded") ||
|
||||||
|
strings.Contains(msg, "monthly usage limit exceeded") ||
|
||||||
|
strings.Contains(msg, "requests-per-minute limit exceeded")
|
||||||
}
|
}
|
||||||
|
|
||||||
func hasOpsUpstreamErrorContext(c *gin.Context) bool {
|
func hasOpsUpstreamErrorContext(c *gin.Context) bool {
|
||||||
|
|||||||
@ -260,6 +260,34 @@ func TestClassifyOpsAuthClientErrorsExcludedFromSLA(t *testing.T) {
|
|||||||
code: "API_KEY_REQUIRED",
|
code: "API_KEY_REQUIRED",
|
||||||
status: http.StatusUnauthorized,
|
status: http.StatusUnauthorized,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "expired local API key",
|
||||||
|
errType: "api_error",
|
||||||
|
message: "API key 已过期",
|
||||||
|
code: "API_KEY_EXPIRED",
|
||||||
|
status: http.StatusForbidden,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "disabled local API key",
|
||||||
|
errType: "api_error",
|
||||||
|
message: "API key is disabled",
|
||||||
|
code: "API_KEY_DISABLED",
|
||||||
|
status: http.StatusUnauthorized,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "local API key user missing",
|
||||||
|
errType: "api_error",
|
||||||
|
message: "User associated with API key not found",
|
||||||
|
code: "USER_NOT_FOUND",
|
||||||
|
status: http.StatusUnauthorized,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "inactive local API key user",
|
||||||
|
errType: "api_error",
|
||||||
|
message: "User account is not active",
|
||||||
|
code: "USER_INACTIVE",
|
||||||
|
status: http.StatusUnauthorized,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: "google invalid API key",
|
name: "google invalid API key",
|
||||||
errType: "api_error",
|
errType: "api_error",
|
||||||
@ -274,6 +302,27 @@ func TestClassifyOpsAuthClientErrorsExcludedFromSLA(t *testing.T) {
|
|||||||
code: "401",
|
code: "401",
|
||||||
status: http.StatusUnauthorized,
|
status: http.StatusUnauthorized,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "google disabled API key",
|
||||||
|
errType: "api_error",
|
||||||
|
message: "API key is disabled",
|
||||||
|
code: "401",
|
||||||
|
status: http.StatusUnauthorized,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "google local API key user missing",
|
||||||
|
errType: "api_error",
|
||||||
|
message: "User associated with API key not found",
|
||||||
|
code: "401",
|
||||||
|
status: http.StatusUnauthorized,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "google inactive local API key user",
|
||||||
|
errType: "api_error",
|
||||||
|
message: "User account is not active",
|
||||||
|
code: "401",
|
||||||
|
status: http.StatusUnauthorized,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
@ -294,6 +343,126 @@ func TestClassifyOpsAuthClientErrorsExcludedFromSLA(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestClassifyOpsLocalBusinessLimitErrorsExcludedFromSLA(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
errType string
|
||||||
|
message string
|
||||||
|
code string
|
||||||
|
status int
|
||||||
|
wantErrType string
|
||||||
|
wantPhase string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "standard API key quota exhausted",
|
||||||
|
errType: "api_error",
|
||||||
|
message: "API key 额度已用完",
|
||||||
|
code: "API_KEY_QUOTA_EXHAUSTED",
|
||||||
|
status: http.StatusTooManyRequests,
|
||||||
|
wantErrType: "api_error",
|
||||||
|
wantPhase: "request",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "standard query API key deprecated",
|
||||||
|
errType: "api_error",
|
||||||
|
message: "API key in query parameter is deprecated. Please use Authorization header instead.",
|
||||||
|
code: "api_key_in_query_deprecated",
|
||||||
|
status: http.StatusBadRequest,
|
||||||
|
wantErrType: "api_error",
|
||||||
|
wantPhase: "request",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "google query API key deprecated",
|
||||||
|
errType: "api_error",
|
||||||
|
message: "Query parameter api_key is deprecated. Use Authorization header or key instead.",
|
||||||
|
code: "400",
|
||||||
|
status: http.StatusBadRequest,
|
||||||
|
wantErrType: "api_error",
|
||||||
|
wantPhase: "request",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "google no active subscription",
|
||||||
|
errType: "api_error",
|
||||||
|
message: "No active subscription found for this group",
|
||||||
|
code: "403",
|
||||||
|
status: http.StatusForbidden,
|
||||||
|
wantErrType: "api_error",
|
||||||
|
wantPhase: "request",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "google insufficient account balance",
|
||||||
|
errType: "api_error",
|
||||||
|
message: "Insufficient account balance",
|
||||||
|
code: "403",
|
||||||
|
status: http.StatusForbidden,
|
||||||
|
wantErrType: "api_error",
|
||||||
|
wantPhase: "request",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "gateway billing cache insufficient balance",
|
||||||
|
errType: "billing_error",
|
||||||
|
message: "insufficient balance",
|
||||||
|
code: "",
|
||||||
|
status: http.StatusForbidden,
|
||||||
|
wantErrType: "billing_error",
|
||||||
|
wantPhase: "request",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "gemini group platform mismatch",
|
||||||
|
errType: "api_error",
|
||||||
|
message: "API key group platform is not gemini",
|
||||||
|
code: "400",
|
||||||
|
status: http.StatusBadRequest,
|
||||||
|
wantErrType: "api_error",
|
||||||
|
wantPhase: "request",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "gateway API key 5h rate limit",
|
||||||
|
errType: "api_error",
|
||||||
|
message: "api key 5小时限额已用完",
|
||||||
|
code: "rate_limit_exceeded",
|
||||||
|
status: http.StatusTooManyRequests,
|
||||||
|
wantErrType: "api_error",
|
||||||
|
wantPhase: "request",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "gateway group RPM limit",
|
||||||
|
errType: "api_error",
|
||||||
|
message: "group requests-per-minute limit exceeded",
|
||||||
|
code: "rate_limit_exceeded",
|
||||||
|
status: http.StatusTooManyRequests,
|
||||||
|
wantErrType: "api_error",
|
||||||
|
wantPhase: "request",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "google subscription daily limit",
|
||||||
|
errType: "api_error",
|
||||||
|
message: "daily usage limit exceeded",
|
||||||
|
code: "429",
|
||||||
|
status: http.StatusTooManyRequests,
|
||||||
|
wantErrType: "api_error",
|
||||||
|
wantPhase: "request",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
gin.SetMode(gin.TestMode)
|
||||||
|
rec := httptest.NewRecorder()
|
||||||
|
c, _ := gin.CreateTestContext(rec)
|
||||||
|
|
||||||
|
errType := normalizeOpsErrorType(tt.errType, tt.code)
|
||||||
|
phase, isBusinessLimited, errorOwner, errorSource := classifyOpsErrorLog(c, errType, tt.message, tt.code, tt.status)
|
||||||
|
|
||||||
|
require.Equal(t, tt.wantErrType, errType)
|
||||||
|
require.Equal(t, tt.wantPhase, phase)
|
||||||
|
require.True(t, isBusinessLimited)
|
||||||
|
require.Equal(t, "client", errorOwner)
|
||||||
|
require.Equal(t, "client_request", errorSource)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestClassifyOpsIPRestrictionAccessDeniedExcludedFromSLA(t *testing.T) {
|
func TestClassifyOpsIPRestrictionAccessDeniedExcludedFromSLA(t *testing.T) {
|
||||||
gin.SetMode(gin.TestMode)
|
gin.SetMode(gin.TestMode)
|
||||||
rec := httptest.NewRecorder()
|
rec := httptest.NewRecorder()
|
||||||
@ -372,23 +541,71 @@ func TestClassifyOpsUnmarkedNoAvailableTextStillCountsForSLA(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestClassifyOpsUpstreamAuthTextStillCountsForSLA(t *testing.T) {
|
func TestClassifyOpsUpstreamAuthTextStillCountsForSLA(t *testing.T) {
|
||||||
gin.SetMode(gin.TestMode)
|
tests := []struct {
|
||||||
rec := httptest.NewRecorder()
|
name string
|
||||||
c, _ := gin.CreateTestContext(rec)
|
message string
|
||||||
service.SetOpsUpstreamError(c, http.StatusUnauthorized, "Invalid API key", "")
|
code string
|
||||||
|
status int
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "invalid API key",
|
||||||
|
message: "Invalid API key",
|
||||||
|
code: "401",
|
||||||
|
status: http.StatusUnauthorized,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "disabled API key",
|
||||||
|
message: "API key is disabled",
|
||||||
|
code: "API_KEY_DISABLED",
|
||||||
|
status: http.StatusUnauthorized,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "gemini group platform mismatch",
|
||||||
|
message: "API key group platform is not gemini",
|
||||||
|
code: "400",
|
||||||
|
status: http.StatusBadRequest,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "provider balance error",
|
||||||
|
message: "Insufficient account balance",
|
||||||
|
code: "INSUFFICIENT_BALANCE",
|
||||||
|
status: http.StatusForbidden,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "provider subscription error",
|
||||||
|
message: "No active subscription found for this group",
|
||||||
|
code: "SUBSCRIPTION_NOT_FOUND",
|
||||||
|
status: http.StatusForbidden,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "provider quota error",
|
||||||
|
message: "api key 额度已用完",
|
||||||
|
code: "API_KEY_QUOTA_EXHAUSTED",
|
||||||
|
status: http.StatusTooManyRequests,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
phase, isBusinessLimited, errorOwner, errorSource := classifyOpsErrorLog(
|
for _, tt := range tests {
|
||||||
c,
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
"api_error",
|
gin.SetMode(gin.TestMode)
|
||||||
"Invalid API key",
|
rec := httptest.NewRecorder()
|
||||||
"401",
|
c, _ := gin.CreateTestContext(rec)
|
||||||
http.StatusUnauthorized,
|
service.SetOpsUpstreamError(c, tt.status, tt.message, "")
|
||||||
)
|
|
||||||
|
|
||||||
require.Equal(t, "upstream", phase)
|
phase, isBusinessLimited, errorOwner, errorSource := classifyOpsErrorLog(
|
||||||
require.False(t, isBusinessLimited)
|
c,
|
||||||
require.Equal(t, "provider", errorOwner)
|
"api_error",
|
||||||
require.Equal(t, "upstream_http", errorSource)
|
tt.message,
|
||||||
|
tt.code,
|
||||||
|
tt.status,
|
||||||
|
)
|
||||||
|
|
||||||
|
require.Equal(t, "upstream", phase)
|
||||||
|
require.False(t, isBusinessLimited)
|
||||||
|
require.Equal(t, "provider", errorOwner)
|
||||||
|
require.Equal(t, "upstream_http", errorSource)
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestClassifyOpsUpstreamNoAvailableTextStillCountsForSLA(t *testing.T) {
|
func TestClassifyOpsUpstreamNoAvailableTextStillCountsForSLA(t *testing.T) {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user