diff --git a/backend/internal/handler/ops_error_logger.go b/backend/internal/handler/ops_error_logger.go index cb2fad5d..ceb06f0e 100644 --- a/backend/internal/handler/ops_error_logger.go +++ b/backend/internal/handler/ops_error_logger.go @@ -26,6 +26,22 @@ 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" + opsErrInsufficientAccountBalance = "insufficient account 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 +1040,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 +1054,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 +1073,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 +1119,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 +1213,30 @@ 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, opsErrInsufficientAccountBalance) || + strings.Contains(bodyLower, opsErrInsufficientQuota) || + strings.Contains(msgLower, opsErrInsufficientBalance) || strings.Contains(msgLower, opsErrInsufficientAccountBalance) { 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 4ac03f3f..b64d8478 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, 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 7e1660ec..faa91d12 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') }} +

+
+ +