package providers import ( "errors" "net" "testing" "time" ) func TestClassifyHTTP429RateLimit(t *testing.T) { classifier := NewDefaultClassifier() result := classifier.Classify(errors.New("wrapped"), 429, "Rate limit exceeded") if result.Reason != FailoverRateLimit { t.Errorf("expected FailoverRateLimit, got %s", result.Reason) } } func TestClassifyHTTP402Billing(t *testing.T) { classifier := NewDefaultClassifier() result := classifier.Classify(nil, 402, "Payment required") if result.Reason != FailoverBilling { t.Errorf("expected FailoverBilling, got %s", result.Reason) } } func TestClassifyHTTP401RevokedAuthPermanent(t *testing.T) { classifier := NewDefaultClassifier() result := classifier.Classify(nil, 401, "API key has been revoked") if result.Reason != FailoverAuthPermanent { t.Errorf("expected FailoverAuthPermanent, got %s", result.Reason) } } func TestClassifyHTTP401WithoutRevokedAuth(t *testing.T) { classifier := NewDefaultClassifier() result := classifier.Classify(nil, 401, "Invalid API key") if result.Reason != FailoverAuth { t.Errorf("expected FailoverAuth, got %s", result.Reason) } } func TestClassifyHTTP403DeletedAuthPermanent(t *testing.T) { classifier := NewDefaultClassifier() result := classifier.Classify(nil, 403, "Account has been deleted") if result.Reason != FailoverAuthPermanent { t.Errorf("expected FailoverAuthPermanent, got %s", result.Reason) } } func TestClassifyHTTP403DisabledAuthPermanent(t *testing.T) { classifier := NewDefaultClassifier() result := classifier.Classify(nil, 403, "API key is disabled") if result.Reason != FailoverAuthPermanent { t.Errorf("expected FailoverAuthPermanent, got %s", result.Reason) } } func TestClassifyHTTP404ModelNotFound(t *testing.T) { classifier := NewDefaultClassifier() result := classifier.Classify(nil, 404, "Model not found") if result.Reason != FailoverModelNotFound { t.Errorf("expected FailoverModelNotFound, got %s", result.Reason) } } func TestClassifyHTTP404WithModelKeyword(t *testing.T) { classifier := NewDefaultClassifier() result := classifier.Classify(nil, 404, "Requested model does not exist") if result.Reason != FailoverModelNotFound { t.Errorf("expected FailoverModelNotFound, got %s", result.Reason) } } func TestClassifyHTTP529Overloaded(t *testing.T) { classifier := NewDefaultClassifier() result := classifier.Classify(nil, 529, "Service overloaded") if result.Reason != FailoverOverloaded { t.Errorf("expected FailoverOverloaded, got %s", result.Reason) } } func TestClassifyHTTP500WithOverload(t *testing.T) { classifier := NewDefaultClassifier() result := classifier.Classify(nil, 500, "Server overload detected") if result.Reason != FailoverOverloaded { t.Errorf("expected FailoverOverloaded, got %s", result.Reason) } } func TestClassifyHTTP500WithCapacity(t *testing.T) { classifier := NewDefaultClassifier() result := classifier.Classify(nil, 500, "Insufficient capacity") if result.Reason != FailoverOverloaded { t.Errorf("expected FailoverOverloaded, got %s", result.Reason) } } func TestClassifyHTTP502BadGateway(t *testing.T) { classifier := NewDefaultClassifier() result := classifier.Classify(nil, 502, "Bad gateway") if result.Reason != FailoverServerError { t.Errorf("expected FailoverServerError for 502, got %s", result.Reason) } } func TestClassifyContextWindowExceeded(t *testing.T) { classifier := NewDefaultClassifier() result := classifier.Classify(nil, 400, "Context length exceeded") if result.Kind != "context_overflow" { t.Errorf("expected context_overflow kind, got %s", result.Kind) } } func TestClassifyContextWindowEnglish(t *testing.T) { classifier := NewDefaultClassifier() result := classifier.Classify(nil, 400, "error: maximum context length reached") if result.Kind != "context_overflow" { t.Errorf("expected context_overflow, got %s", result.Kind) } } func TestClassifyContextWindowChinese(t *testing.T) { classifier := NewDefaultClassifier() result := classifier.Classify(nil, 400, "错误: 超出最大长度限制") if result.Kind != "context_overflow" { t.Errorf("expected context_overflow, got %s", result.Kind) } } func TestClassifyPromptTooLong(t *testing.T) { classifier := NewDefaultClassifier() result := classifier.Classify(nil, 400, "Prompt is too long") if result.Kind != "context_overflow" { t.Errorf("expected context_overflow, got %s", result.Kind) } } func TestClassifyTooManyTokens(t *testing.T) { classifier := NewDefaultClassifier() result := classifier.Classify(nil, 400, "Too many tokens in request") if result.Kind != "context_overflow" { t.Errorf("expected context_overflow, got %s", result.Kind) } } func TestClassifyNetworkTimeoutError(t *testing.T) { classifier := NewDefaultClassifier() timeoutErr := &net.DNSError{ Err: "timeout", Name: "example.com", IsTimeout: true, } result := classifier.Classify(timeoutErr, 0, "") if result.Reason != FailoverTimeout { t.Errorf("expected FailoverTimeout, got %s", result.Reason) } } func TestClassifyNetworkConnectionReset(t *testing.T) { classifier := NewDefaultClassifier() err := errors.New("connection reset by peer") result := classifier.Classify(err, 0, "") if result.Reason != FailoverTimeout { t.Errorf("expected FailoverTimeout, got %s", result.Reason) } } func TestClassifyNetworkBrokenPipe(t *testing.T) { classifier := NewDefaultClassifier() err := errors.New("broken pipe") result := classifier.Classify(err, 0, "") if result.Reason != FailoverTimeout { t.Errorf("expected FailoverTimeout, got %s", result.Reason) } } func TestClassifyNetworkEOF(t *testing.T) { classifier := NewDefaultClassifier() err := errors.New("EOF") result := classifier.Classify(err, 0, "") if result.Reason != FailoverTimeout { t.Errorf("expected FailoverTimeout, got %s", result.Reason) } } func TestRegisterOpenAIPatterns(t *testing.T) { classifier := NewDefaultClassifier() RegisterOpenAIPatterns(classifier) result := classifier.Classify(nil, 0, "model_is_deactivated") if result.Reason != FailoverModelNotFound { t.Errorf("expected FailoverModelNotFound for deactivated model, got %s", result.Reason) } } func TestRegisterOpenAIPatternsModelNotFound(t *testing.T) { classifier := NewDefaultClassifier() RegisterOpenAIPatterns(classifier) result := classifier.Classify(nil, 0, "The model 'gpt-999' does not exist") if result.Reason != FailoverModelNotFound { t.Errorf("expected FailoverModelNotFound, got %s", result.Reason) } } func TestRegisterAnthropicPatterns(t *testing.T) { classifier := NewDefaultClassifier() RegisterAnthropicPatterns(classifier) result := classifier.Classify(nil, 0, "API is overloaded") if result.Reason != FailoverOverloaded { t.Errorf("expected FailoverOverloaded, got %s", result.Reason) } } func TestRegisterAnthropicPatternsBilling(t *testing.T) { classifier := NewDefaultClassifier() RegisterAnthropicPatterns(classifier) result := classifier.Classify(nil, 0, "Insufficient credit balance") if result.Reason != FailoverBilling { t.Errorf("expected FailoverBilling, got %s", result.Reason) } } func TestClassifyHTTPErrorConvenience(t *testing.T) { classifier := NewDefaultClassifier() httpErr := &HTTPError{ Status: 429, Body: "Rate limit exceeded", RetryAfter: 60 * time.Second, } result := ClassifyHTTPError(classifier, httpErr) if result.Reason != FailoverRateLimit { t.Errorf("expected FailoverRateLimit, got %s", result.Reason) } } func TestClassifyHTTPErrorNonHTTPError(t *testing.T) { classifier := NewDefaultClassifier() err := errors.New("connection reset by peer") result := ClassifyHTTPError(classifier, err) if result.Reason != FailoverTimeout { t.Errorf("expected FailoverTimeout, got %s", result.Reason) } } func TestClassifyHTTPErrorCodexSafetyRefusalString(t *testing.T) { classifier := NewDefaultClassifier() err := errors.New("codex: response failed: Invalid prompt: we've limited access to this content for safety reasons") result := ClassifyHTTPError(classifier, err) if result.Reason != FailoverContentPolicy { t.Errorf("expected FailoverContentPolicy, got %s", result.Reason) } } func TestClassifyHTTPErrorNil(t *testing.T) { classifier := NewDefaultClassifier() result := ClassifyHTTPError(classifier, nil) if result.Reason != FailoverUnknown { t.Errorf("expected FailoverUnknown for nil error, got %s", result.Reason) } } func TestClassifyBillingInsufficientQuota(t *testing.T) { classifier := NewDefaultClassifier() result := classifier.Classify(nil, 400, "insufficient_quota") if result.Reason != FailoverBilling { t.Errorf("expected FailoverBilling, got %s", result.Reason) } } func TestClassifyFormatToolCall(t *testing.T) { classifier := NewDefaultClassifier() result := classifier.Classify(nil, 400, "Invalid tool_call format") if result.Reason != FailoverFormat { t.Errorf("expected FailoverFormat, got %s", result.Reason) } } func TestClassifyFormatFunctionCall(t *testing.T) { classifier := NewDefaultClassifier() result := classifier.Classify(nil, 400, "Invalid function_call in request") if result.Reason != FailoverFormat { t.Errorf("expected FailoverFormat, got %s", result.Reason) } } func TestClassifyFormatInvalidRequest(t *testing.T) { classifier := NewDefaultClassifier() result := classifier.Classify(nil, 400, "invalid_request_error") if result.Reason != FailoverFormat { t.Errorf("expected FailoverFormat, got %s", result.Reason) } } func TestClassifyContentPolicyDataInspectionFailed(t *testing.T) { classifier := NewDefaultClassifier() result := classifier.Classify(nil, 400, `{"error":{"code":"data_inspection_failed","message":"Input text data may contain inappropriate content."}}`) if result.Reason != FailoverContentPolicy { t.Errorf("expected FailoverContentPolicy, got %s", result.Reason) } } func TestClassifyHTTP401WithExpired(t *testing.T) { classifier := NewDefaultClassifier() result := classifier.Classify(nil, 401, "Token has expired") if result.Reason != FailoverAuthPermanent { t.Errorf("expected FailoverAuthPermanent for expired token, got %s", result.Reason) } } func TestClassifyContextOverflowTokenLimit(t *testing.T) { classifier := NewDefaultClassifier() result := classifier.Classify(nil, 400, "token limit exceeded") if result.Kind != "context_overflow" { t.Errorf("expected context_overflow, got %s", result.Kind) } } func TestClassifyContextOverflowChineseContext(t *testing.T) { classifier := NewDefaultClassifier() result := classifier.Classify(nil, 400, "请求的上下文长度超出限制") if result.Kind != "context_overflow" { t.Errorf("expected context_overflow, got %s", result.Kind) } } func TestClassifyLowercaseInsensitive(t *testing.T) { classifier := NewDefaultClassifier() result := classifier.Classify(nil, 401, "API KEY HAS BEEN REVOKED") if result.Reason != FailoverAuthPermanent { t.Errorf("expected FailoverAuthPermanent (case-insensitive), got %s", result.Reason) } } func TestClassifyUnknownError(t *testing.T) { classifier := NewDefaultClassifier() result := classifier.Classify(nil, 500, "Unknown server error") if result.Reason != FailoverServerError { t.Errorf("expected FailoverServerError, got %s", result.Reason) } } // Issue 958: New context overflow patterns for ZAI/GLM, DashScope, generic func TestClassifyPromptExceedsMaxLength(t *testing.T) { classifier := NewDefaultClassifier() result := classifier.Classify(nil, 400, `{"error":{"code":"1261","message":"Prompt exceeds max length"}}`) if result.Kind != "context_overflow" { t.Errorf("expected context_overflow, got %s (reason: %s)", result.Kind, result.Reason) } } func TestClassifyInputTooLong(t *testing.T) { classifier := NewDefaultClassifier() result := classifier.Classify(nil, 400, "Input is too long for this model") if result.Kind != "context_overflow" { t.Errorf("expected context_overflow, got %s (reason: %s)", result.Kind, result.Reason) } } func TestClassifyRequestTooLarge(t *testing.T) { classifier := NewDefaultClassifier() result := classifier.Classify(nil, 400, "request_too_large: payload exceeds limit") if result.Kind != "context_overflow" { t.Errorf("expected context_overflow, got %s (reason: %s)", result.Kind, result.Reason) } } func TestClassifyChineseInputTooLong(t *testing.T) { classifier := NewDefaultClassifier() result := classifier.Classify(nil, 400, "请求输入过长") if result.Kind != "context_overflow" { t.Errorf("expected context_overflow, got %s (reason: %s)", result.Kind, result.Reason) } }