diff --git a/backend/internal/handler/ops_error_logger.go b/backend/internal/handler/ops_error_logger.go
index cb2fad5d..5fedc139 100644
--- a/backend/internal/handler/ops_error_logger.go
+++ b/backend/internal/handler/ops_error_logger.go
@@ -26,6 +26,21 @@ const (
opsStreamKey = "ops_stream"
opsRequestBodyKey = "ops_request_body"
opsAccountIDKey = "ops_account_id"
+
+ // 错误过滤匹配常量 — shouldSkipOpsErrorLog 和错误分类共用
+ opsErrContextCanceled = "context canceled"
+ opsErrNoAvailableAccounts = "no available accounts"
+ opsErrInvalidAPIKey = "invalid_api_key"
+ opsErrAPIKeyRequired = "api_key_required"
+ opsErrInsufficientBalance = "insufficient balance"
+ opsErrInsufficientQuota = "insufficient_quota"
+
+ // 上游错误码常量 — 错误分类 (normalizeOpsErrorType / classifyOpsPhase / classifyOpsIsBusinessLimited)
+ opsCodeInsufficientBalance = "INSUFFICIENT_BALANCE"
+ opsCodeUsageLimitExceeded = "USAGE_LIMIT_EXCEEDED"
+ opsCodeSubscriptionNotFound = "SUBSCRIPTION_NOT_FOUND"
+ opsCodeSubscriptionInvalid = "SUBSCRIPTION_INVALID"
+ opsCodeUserInactive = "USER_INACTIVE"
)
const (
@@ -1024,9 +1039,9 @@ func normalizeOpsErrorType(errType string, code string) string {
return errType
}
switch strings.TrimSpace(code) {
- case "INSUFFICIENT_BALANCE":
+ case opsCodeInsufficientBalance:
return "billing_error"
- case "USAGE_LIMIT_EXCEEDED", "SUBSCRIPTION_NOT_FOUND", "SUBSCRIPTION_INVALID":
+ case opsCodeUsageLimitExceeded, opsCodeSubscriptionNotFound, opsCodeSubscriptionInvalid:
return "subscription_error"
default:
return "api_error"
@@ -1038,7 +1053,7 @@ func classifyOpsPhase(errType, message, code string) string {
// Standardized phases: request|auth|routing|upstream|network|internal
// Map billing/concurrency/response => request; scheduling => routing.
switch strings.TrimSpace(code) {
- case "INSUFFICIENT_BALANCE", "USAGE_LIMIT_EXCEEDED", "SUBSCRIPTION_NOT_FOUND", "SUBSCRIPTION_INVALID":
+ case opsCodeInsufficientBalance, opsCodeUsageLimitExceeded, opsCodeSubscriptionNotFound, opsCodeSubscriptionInvalid:
return "request"
}
@@ -1057,7 +1072,7 @@ func classifyOpsPhase(errType, message, code string) string {
case "upstream_error", "overloaded_error":
return "upstream"
case "api_error":
- if strings.Contains(msg, "no available accounts") {
+ if strings.Contains(msg, opsErrNoAvailableAccounts) {
return "routing"
}
return "internal"
@@ -1103,7 +1118,7 @@ func classifyOpsIsRetryable(errType string, statusCode int) bool {
func classifyOpsIsBusinessLimited(errType, phase, code string, status int, message string) bool {
switch strings.TrimSpace(code) {
- case "INSUFFICIENT_BALANCE", "USAGE_LIMIT_EXCEEDED", "SUBSCRIPTION_NOT_FOUND", "SUBSCRIPTION_INVALID", "USER_INACTIVE":
+ case opsCodeInsufficientBalance, opsCodeUsageLimitExceeded, opsCodeSubscriptionNotFound, opsCodeSubscriptionInvalid, opsCodeUserInactive:
return true
}
if phase == "billing" || phase == "concurrency" {
@@ -1197,21 +1212,29 @@ func shouldSkipOpsErrorLog(ctx context.Context, ops *service.OpsService, message
// Check if context canceled errors should be ignored (client disconnects)
if settings.IgnoreContextCanceled {
- if strings.Contains(msgLower, "context canceled") || strings.Contains(bodyLower, "context canceled") {
+ if strings.Contains(msgLower, opsErrContextCanceled) || strings.Contains(bodyLower, opsErrContextCanceled) {
return true
}
}
// Check if "no available accounts" errors should be ignored
if settings.IgnoreNoAvailableAccounts {
- if strings.Contains(msgLower, "no available accounts") || strings.Contains(bodyLower, "no available accounts") {
+ if strings.Contains(msgLower, opsErrNoAvailableAccounts) || strings.Contains(bodyLower, opsErrNoAvailableAccounts) {
return true
}
}
// Check if invalid/missing API key errors should be ignored (user misconfiguration)
if settings.IgnoreInvalidApiKeyErrors {
- if strings.Contains(bodyLower, "invalid_api_key") || strings.Contains(bodyLower, "api_key_required") {
+ if strings.Contains(bodyLower, opsErrInvalidAPIKey) || strings.Contains(bodyLower, opsErrAPIKeyRequired) {
+ return true
+ }
+ }
+
+ // Check if insufficient balance errors should be ignored
+ if settings.IgnoreInsufficientBalanceErrors {
+ if strings.Contains(bodyLower, opsErrInsufficientBalance) || strings.Contains(bodyLower, opsErrInsufficientQuota) ||
+ strings.Contains(msgLower, opsErrInsufficientBalance) {
return true
}
}
diff --git a/backend/internal/service/gateway_multiplatform_test.go b/backend/internal/service/gateway_multiplatform_test.go
index f947a8ee..ea8fa784 100644
--- a/backend/internal/service/gateway_multiplatform_test.go
+++ b/backend/internal/service/gateway_multiplatform_test.go
@@ -440,7 +440,7 @@ func TestGatewayService_SelectAccountForModelWithPlatform_NoAvailableAccounts(t
acc, err := svc.selectAccountForModelWithPlatform(ctx, nil, "", "claude-3-5-sonnet-20241022", nil, PlatformAnthropic)
require.Error(t, err)
require.Nil(t, acc)
- require.Contains(t, err.Error(), "no available accounts")
+ require.ErrorIs(t, err, ErrNoAvailableAccounts)
}
// TestGatewayService_SelectAccountForModelWithPlatform_AllExcluded 测试所有账户被排除
@@ -1073,7 +1073,7 @@ func TestGatewayService_SelectAccountForModelWithPlatform_NoAccounts(t *testing.
acc, err := svc.selectAccountForModelWithPlatform(ctx, nil, "", "", nil, PlatformAnthropic)
require.Error(t, err)
require.Nil(t, acc)
- require.Contains(t, err.Error(), "no available accounts")
+ require.ErrorIs(t, err, ErrNoAvailableAccounts)
}
func TestGatewayService_isModelSupportedByAccount(t *testing.T) {
@@ -1734,7 +1734,7 @@ func TestGatewayService_selectAccountWithMixedScheduling(t *testing.T) {
acc, err := svc.selectAccountWithMixedScheduling(ctx, nil, "", "claude-3-5-sonnet-20241022", nil, PlatformAnthropic)
require.Error(t, err)
require.Nil(t, acc)
- require.Contains(t, err.Error(), "no available accounts")
+ require.ErrorIs(t, err, ErrNoAvailableAccounts)
})
t.Run("混合调度-不支持模型返回错误", func(t *testing.T) {
@@ -2290,7 +2290,7 @@ func TestGatewayService_SelectAccountWithLoadAwareness(t *testing.T) {
result, err := svc.SelectAccountWithLoadAwareness(ctx, nil, "", "claude-3-5-sonnet-20241022", nil, "")
require.Error(t, err)
require.Nil(t, result)
- require.Contains(t, err.Error(), "no available accounts")
+ require.ErrorIs(t, err, ErrNoAvailableAccounts)
})
t.Run("过滤不可调度账号-限流账号被跳过", func(t *testing.T) {
diff --git a/backend/internal/service/gateway_service.go b/backend/internal/service/gateway_service.go
index 70f64121..cff9e9bb 100644
--- a/backend/internal/service/gateway_service.go
+++ b/backend/internal/service/gateway_service.go
@@ -346,6 +346,9 @@ var systemBlockFilterPrefixes = []string{
"x-anthropic-billing-header",
}
+// ErrNoAvailableAccounts 表示没有可用的账号
+var ErrNoAvailableAccounts = errors.New("no available accounts")
+
// ErrClaudeCodeOnly 表示分组仅允许 Claude Code 客户端访问
var ErrClaudeCodeOnly = errors.New("this group only allows Claude Code clients")
@@ -1205,7 +1208,7 @@ func (s *GatewayService) SelectAccountWithLoadAwareness(ctx context.Context, gro
return nil, err
}
if len(accounts) == 0 {
- return nil, errors.New("no available accounts")
+ return nil, ErrNoAvailableAccounts
}
ctx = s.withWindowCostPrefetch(ctx, accounts)
ctx = s.withRPMPrefetch(ctx, accounts)
@@ -1553,7 +1556,7 @@ func (s *GatewayService) SelectAccountWithLoadAwareness(ctx context.Context, gro
}
if len(candidates) == 0 {
- return nil, errors.New("no available accounts")
+ return nil, ErrNoAvailableAccounts
}
accountLoads := make([]AccountWithConcurrency, 0, len(candidates))
@@ -1642,7 +1645,7 @@ func (s *GatewayService) SelectAccountWithLoadAwareness(ctx context.Context, gro
},
}, nil
}
- return nil, errors.New("no available accounts")
+ return nil, ErrNoAvailableAccounts
}
func (s *GatewayService) tryAcquireByLegacyOrder(ctx context.Context, candidates []*Account, groupID *int64, sessionHash string, preferOAuth bool) (*AccountSelectionResult, bool) {
@@ -2852,9 +2855,9 @@ func (s *GatewayService) selectAccountForModelWithPlatform(ctx context.Context,
if selected == nil {
stats := s.logDetailedSelectionFailure(ctx, groupID, sessionHash, requestedModel, platform, accounts, excludedIDs, false)
if requestedModel != "" {
- return nil, fmt.Errorf("no available accounts supporting model: %s (%s)", requestedModel, summarizeSelectionFailureStats(stats))
+ return nil, fmt.Errorf("%w supporting model: %s (%s)", ErrNoAvailableAccounts, requestedModel, summarizeSelectionFailureStats(stats))
}
- return nil, errors.New("no available accounts")
+ return nil, ErrNoAvailableAccounts
}
// 4. 建立粘性绑定
@@ -3090,9 +3093,9 @@ func (s *GatewayService) selectAccountWithMixedScheduling(ctx context.Context, g
if selected == nil {
stats := s.logDetailedSelectionFailure(ctx, groupID, sessionHash, requestedModel, nativePlatform, accounts, excludedIDs, true)
if requestedModel != "" {
- return nil, fmt.Errorf("no available accounts supporting model: %s (%s)", requestedModel, summarizeSelectionFailureStats(stats))
+ return nil, fmt.Errorf("%w supporting model: %s (%s)", ErrNoAvailableAccounts, requestedModel, summarizeSelectionFailureStats(stats))
}
- return nil, errors.New("no available accounts")
+ return nil, ErrNoAvailableAccounts
}
// 4. 建立粘性绑定
diff --git a/backend/internal/service/openai_account_scheduler.go b/backend/internal/service/openai_account_scheduler.go
index 0fcf450b..789888cb 100644
--- a/backend/internal/service/openai_account_scheduler.go
+++ b/backend/internal/service/openai_account_scheduler.go
@@ -725,7 +725,7 @@ func (s *defaultOpenAIAccountScheduler) selectByLoadBalance(
}, len(candidates), topK, loadSkew, nil
}
- return nil, len(candidates), topK, loadSkew, errors.New("no available accounts")
+ return nil, len(candidates), topK, loadSkew, ErrNoAvailableAccounts
}
func (s *defaultOpenAIAccountScheduler) isAccountTransportCompatible(account *Account, requiredTransport OpenAIUpstreamTransport) bool {
diff --git a/backend/internal/service/openai_gateway_service.go b/backend/internal/service/openai_gateway_service.go
index 406344e3..327ce916 100644
--- a/backend/internal/service/openai_gateway_service.go
+++ b/backend/internal/service/openai_gateway_service.go
@@ -1312,7 +1312,7 @@ func (s *OpenAIGatewayService) SelectAccountWithLoadAwareness(ctx context.Contex
return nil, err
}
if len(accounts) == 0 {
- return nil, errors.New("no available accounts")
+ return nil, ErrNoAvailableAccounts
}
isExcluded := func(accountID int64) bool {
@@ -1382,7 +1382,7 @@ func (s *OpenAIGatewayService) SelectAccountWithLoadAwareness(ctx context.Contex
}
if len(candidates) == 0 {
- return nil, errors.New("no available accounts")
+ return nil, ErrNoAvailableAccounts
}
accountLoads := make([]AccountWithConcurrency, 0, len(candidates))
@@ -1489,7 +1489,7 @@ func (s *OpenAIGatewayService) SelectAccountWithLoadAwareness(ctx context.Contex
}, nil
}
- return nil, errors.New("no available accounts")
+ return nil, ErrNoAvailableAccounts
}
func (s *OpenAIGatewayService) listSchedulableAccounts(ctx context.Context, groupID *int64) ([]Account, error) {
diff --git a/backend/internal/service/ops_retry.go b/backend/internal/service/ops_retry.go
index f0daa3e2..fdabbafd 100644
--- a/backend/internal/service/ops_retry.go
+++ b/backend/internal/service/ops_retry.go
@@ -467,7 +467,7 @@ func (s *OpsService) executeClientRetry(ctx context.Context, reqType opsRetryReq
return &opsRetryExecution{status: opsRetryStatusFailed, errorMessage: selErr.Error()}
}
if selection == nil || selection.Account == nil {
- return &opsRetryExecution{status: opsRetryStatusFailed, errorMessage: "no available accounts"}
+ return &opsRetryExecution{status: opsRetryStatusFailed, errorMessage: ErrNoAvailableAccounts.Error()}
}
account := selection.Account
diff --git a/backend/internal/service/ops_settings.go b/backend/internal/service/ops_settings.go
index 93815887..5871166c 100644
--- a/backend/internal/service/ops_settings.go
+++ b/backend/internal/service/ops_settings.go
@@ -368,13 +368,14 @@ func defaultOpsAdvancedSettings() *OpsAdvancedSettings {
Aggregation: OpsAggregationSettings{
AggregationEnabled: false,
},
- IgnoreCountTokensErrors: true, // count_tokens 404 是预期行为,默认忽略
- IgnoreContextCanceled: true, // Default to true - client disconnects are not errors
- IgnoreNoAvailableAccounts: false, // Default to false - this is a real routing issue
- DisplayOpenAITokenStats: false,
- DisplayAlertEvents: true,
- AutoRefreshEnabled: false,
- AutoRefreshIntervalSec: 30,
+ IgnoreCountTokensErrors: true, // count_tokens 404 是预期行为,默认忽略
+ IgnoreContextCanceled: true, // Default to true - client disconnects are not errors
+ IgnoreNoAvailableAccounts: false, // Default to false - this is a real routing issue
+ IgnoreInsufficientBalanceErrors: false, // 默认不忽略,余额不足可能需要关注
+ DisplayOpenAITokenStats: false,
+ DisplayAlertEvents: true,
+ AutoRefreshEnabled: false,
+ AutoRefreshIntervalSec: 30,
}
}
diff --git a/backend/internal/service/ops_settings_models.go b/backend/internal/service/ops_settings_models.go
index c8b9fcd1..fa18b05f 100644
--- a/backend/internal/service/ops_settings_models.go
+++ b/backend/internal/service/ops_settings_models.go
@@ -92,16 +92,17 @@ type OpsAlertRuntimeSettings struct {
// OpsAdvancedSettings stores advanced ops configuration (data retention, aggregation).
type OpsAdvancedSettings struct {
- DataRetention OpsDataRetentionSettings `json:"data_retention"`
- Aggregation OpsAggregationSettings `json:"aggregation"`
- IgnoreCountTokensErrors bool `json:"ignore_count_tokens_errors"`
- IgnoreContextCanceled bool `json:"ignore_context_canceled"`
- IgnoreNoAvailableAccounts bool `json:"ignore_no_available_accounts"`
- IgnoreInvalidApiKeyErrors bool `json:"ignore_invalid_api_key_errors"`
- DisplayOpenAITokenStats bool `json:"display_openai_token_stats"`
- DisplayAlertEvents bool `json:"display_alert_events"`
- AutoRefreshEnabled bool `json:"auto_refresh_enabled"`
- AutoRefreshIntervalSec int `json:"auto_refresh_interval_seconds"`
+ DataRetention OpsDataRetentionSettings `json:"data_retention"`
+ Aggregation OpsAggregationSettings `json:"aggregation"`
+ IgnoreCountTokensErrors bool `json:"ignore_count_tokens_errors"`
+ IgnoreContextCanceled bool `json:"ignore_context_canceled"`
+ IgnoreNoAvailableAccounts bool `json:"ignore_no_available_accounts"`
+ IgnoreInvalidApiKeyErrors bool `json:"ignore_invalid_api_key_errors"`
+ IgnoreInsufficientBalanceErrors bool `json:"ignore_insufficient_balance_errors"`
+ DisplayOpenAITokenStats bool `json:"display_openai_token_stats"`
+ DisplayAlertEvents bool `json:"display_alert_events"`
+ AutoRefreshEnabled bool `json:"auto_refresh_enabled"`
+ AutoRefreshIntervalSec int `json:"auto_refresh_interval_seconds"`
}
type OpsDataRetentionSettings struct {
diff --git a/frontend/src/api/admin/ops.ts b/frontend/src/api/admin/ops.ts
index 11699c79..64f6a6d0 100644
--- a/frontend/src/api/admin/ops.ts
+++ b/frontend/src/api/admin/ops.ts
@@ -841,6 +841,7 @@ export interface OpsAdvancedSettings {
ignore_context_canceled: boolean
ignore_no_available_accounts: boolean
ignore_invalid_api_key_errors: boolean
+ ignore_insufficient_balance_errors: boolean
display_openai_token_stats: boolean
display_alert_events: boolean
auto_refresh_enabled: boolean
diff --git a/frontend/src/i18n/locales/en.ts b/frontend/src/i18n/locales/en.ts
index 7f96f63c..a41bc89a 100644
--- a/frontend/src/i18n/locales/en.ts
+++ b/frontend/src/i18n/locales/en.ts
@@ -3842,6 +3842,8 @@ export default {
ignoreNoAvailableAccountsHint: 'When enabled, "No available accounts" errors will not be written to the error log (not recommended; usually a config issue).',
ignoreInvalidApiKeyErrors: 'Ignore invalid API key errors',
ignoreInvalidApiKeyErrorsHint: 'When enabled, invalid or missing API key errors (INVALID_API_KEY, API_KEY_REQUIRED) will not be written to the error log.',
+ ignoreInsufficientBalanceErrors: 'Ignore Insufficient Balance Errors',
+ ignoreInsufficientBalanceErrorsHint: 'When enabled, upstream insufficient account balance errors will not be written to the error log.',
autoRefresh: 'Auto Refresh',
enableAutoRefresh: 'Enable auto refresh',
enableAutoRefreshHint: 'Automatically refresh dashboard data at a fixed interval.',
diff --git a/frontend/src/i18n/locales/zh.ts b/frontend/src/i18n/locales/zh.ts
index c25ff211..4bf7a0b3 100644
--- a/frontend/src/i18n/locales/zh.ts
+++ b/frontend/src/i18n/locales/zh.ts
@@ -4016,6 +4016,8 @@ export default {
ignoreNoAvailableAccountsHint: '启用后,"No available accounts" 错误将不会写入错误日志(不推荐,这通常是配置问题)。',
ignoreInvalidApiKeyErrors: '忽略无效 API Key 错误',
ignoreInvalidApiKeyErrorsHint: '启用后,无效或缺失 API Key 的错误(INVALID_API_KEY、API_KEY_REQUIRED)将不会写入错误日志。',
+ ignoreInsufficientBalanceErrors: '忽略余额不足错误',
+ ignoreInsufficientBalanceErrorsHint: '启用后,上游账号余额不足(Insufficient balance)的错误将不会写入错误日志。',
autoRefresh: '自动刷新',
enableAutoRefresh: '启用自动刷新',
enableAutoRefreshHint: '自动刷新仪表板数据,启用后会定期拉取最新数据。',
diff --git a/frontend/src/views/admin/ops/components/OpsSettingsDialog.vue b/frontend/src/views/admin/ops/components/OpsSettingsDialog.vue
index 5dcd5c62..542f111d 100644
--- a/frontend/src/views/admin/ops/components/OpsSettingsDialog.vue
+++ b/frontend/src/views/admin/ops/components/OpsSettingsDialog.vue
@@ -516,6 +516,16 @@ async function saveAllSettings() {
+ {{ t('admin.ops.settings.ignoreInsufficientBalanceErrorsHint') }} +
+