package util import ( "context" "errors" "fmt" "testing" ) func assertClassifyError(t *testing.T, err error, wantStatus int, wantLevel ErrorLevel, wantRetry bool, reason string) { t.Helper() statusCode, errorLevel, shouldRetry := ClassifyError(err) if statusCode != wantStatus { t.Errorf("状态码错误: 期望 %d, 实际 %d (%s)", wantStatus, statusCode, reason) } if errorLevel != wantLevel { t.Errorf("错误级别错误: 期望 %v, 实际 %v (%s)", wantLevel, errorLevel, reason) } if shouldRetry != wantRetry { t.Errorf("重试标志错误: 期望 %v, 实际 %v (%s)", wantRetry, shouldRetry, reason) } } func TestClassifyHTTPResponse(t *testing.T) { tests := []struct { name string statusCode int responseBody []byte expected ErrorLevel reason string }{ // 401错误 - Key级场景(新设计:额度用尽先尝试其他Key) { name: "401_quota_exhausted_chinese", statusCode: 401, responseBody: []byte(`{"error":{"message":"该令牌额度已用尽"}}`), expected: ErrorLevelKey, reason: "额度用尽可能只是单个Key问题,应先尝试其他Key", }, { name: "401_insufficient_quota", statusCode: 401, responseBody: []byte(`{"error":"insufficient_quota"}`), expected: ErrorLevelKey, reason: "insufficient_quota可能只是单个Key问题,应先尝试其他Key", }, { name: "401_balance_insufficient", statusCode: 401, responseBody: []byte(`{"error":"balance insufficient"}`), expected: ErrorLevelKey, reason: "余额不足可能只是单个Key问题,应先尝试其他Key", }, // 401错误 - 渠道级场景(仅限账户级不可逆错误) { name: "401_account_suspended", statusCode: 401, responseBody: []byte(`{"error":"account suspended"}`), expected: ErrorLevelChannel, reason: "账户暂停是账户级不可逆错误,应冷却整个渠道", }, { name: "401_account_disabled", statusCode: 401, responseBody: []byte(`{"error":"account disabled"}`), expected: ErrorLevelChannel, reason: "账户禁用是账户级不可逆错误,应冷却整个渠道", }, // 401错误 - Key级场景 { name: "401_invalid_authentication", statusCode: 401, responseBody: []byte(`{"error":"invalid_authentication"}`), expected: ErrorLevelKey, reason: "认证失败应该仅冷却当前Key", }, { name: "401_token_expired", statusCode: 401, responseBody: []byte(`{"error":"token expired"}`), expected: ErrorLevelKey, reason: "Token过期应该仅冷却当前Key(与account expired区分)", }, { name: "401_unauthorized", statusCode: 401, responseBody: []byte(`{"error":"unauthorized"}`), expected: ErrorLevelKey, reason: "未授权应该仅冷却当前Key", }, { name: "401_empty_body", statusCode: 401, responseBody: []byte{}, expected: ErrorLevelKey, reason: "无响应体默认为Key级错误", }, // 403错误 - Key级场景(新设计:额度/限额类错误先尝试其他Key) { name: "403_quota_exceeded_openai_style", statusCode: 403, responseBody: []byte(`{"error":{"type":"quota_exceeded","message":"Daily cost limit reached ($1)"},"details":{"dailyLimit":1,"currentUsage":1.09655195,"remaining":0}}`), expected: ErrorLevelKey, reason: "quota_exceeded可能只是单个Key问题,应先尝试其他Key", }, { name: "403_daily_limit_reached", statusCode: 403, responseBody: []byte(`{"error":"Daily cost limit reached ($10)"}`), expected: ErrorLevelKey, reason: "Daily limit可能只是单个Key问题,应先尝试其他Key", }, { name: "403_remaining_zero", statusCode: 403, responseBody: []byte(`{"details":{"remaining":0}}`), expected: ErrorLevelKey, reason: "remaining:0可能只是单个Key问题,应先尝试其他Key", }, { name: "403_quota_chinese", statusCode: 403, responseBody: []byte(`{"error":"余额不足,请充值"}`), expected: ErrorLevelKey, reason: "余额不足可能只是单个Key问题,应先尝试其他Key", }, // 403错误 - 渠道级场景(仅限账户级不可逆错误) { name: "403_service_disabled", statusCode: 403, responseBody: []byte(`{"error":"service disabled"}`), expected: ErrorLevelChannel, reason: "服务禁用是账户级不可逆错误,应冷却整个渠道", }, // 403错误 - Key级场景 { name: "403_permission_denied", statusCode: 403, responseBody: []byte(`{"error":"permission denied"}`), expected: ErrorLevelKey, reason: "403 + permission denied应该仅冷却当前Key", }, { name: "403_forbidden", statusCode: 403, responseBody: []byte(`{"error":"forbidden"}`), expected: ErrorLevelKey, reason: "403 + forbidden应该仅冷却当前Key", }, { name: "403_empty_body", statusCode: 403, responseBody: []byte{}, expected: ErrorLevelKey, reason: "403无响应体默认为Key级错误", }, // 其他状态码(确保不影响现有逻辑) { name: "404_not_found", statusCode: 404, responseBody: []byte(`{"error":"not found"}`), expected: ErrorLevelChannel, reason: "404非model_not_found应返回渠道级错误以触发切换", }, { name: "500_internal_error", statusCode: 500, responseBody: []byte(`{"error":"internal server error"}`), expected: ErrorLevelChannel, reason: "500应该冷却渠道", }, { name: "429_rate_limit", statusCode: 429, responseBody: []byte(`{"error":"rate limit exceeded"}`), expected: ErrorLevelKey, reason: "429应该冷却Key", }, // 边界情况 { name: "401_case_insensitive", statusCode: 401, responseBody: []byte(`{"error":"Account SUSPENDED"}`), expected: ErrorLevelChannel, reason: "大小写不敏感匹配(account suspended)", }, { name: "401_mixed_case", statusCode: 401, responseBody: []byte(`{"error":"Account Disabled"}`), expected: ErrorLevelChannel, reason: "混合大小写应该正确识别(account disabled)", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { result := ClassifyHTTPResponseWithMeta(tt.statusCode, nil, tt.responseBody).Level if result != tt.expected { t.Errorf("%s\n 期望: %v\n 实际: %v\n 原因: %s\n 响应体: %s", tt.name, tt.expected, result, tt.reason, string(tt.responseBody)) } }) } } func TestClassifyHTTPStatus(t *testing.T) { tests := []struct { statusCode int expected ErrorLevel reason string }{ {200, ErrorLevelNone, "2xx成功"}, {201, ErrorLevelNone, "2xx成功"}, {402, ErrorLevelKey, "402 应视为Key级(额度/余额/配额)"}, {401, ErrorLevelKey, "Key级错误(默认)"}, {403, ErrorLevelKey, "Key级错误"}, {429, ErrorLevelKey, "Key级限流"}, // 499 HTTP响应应触发渠道级重试 {499, ErrorLevelChannel, "499来自HTTP响应时,说明上游API返回,应重试其他渠道"}, // nginx 非标准状态码 {444, ErrorLevelChannel, "444 nginx No Response - 服务器主动关闭连接,渠道级错误"}, {500, ErrorLevelChannel, "渠道级错误"}, {502, ErrorLevelChannel, "渠道级错误"}, {503, ErrorLevelChannel, "渠道级错误"}, {504, ErrorLevelChannel, "渠道级错误"}, {520, ErrorLevelChannel, "520 Web Server Returned an Unknown Error - 渠道级错误"}, {521, ErrorLevelChannel, "521 Web Server Is Down - 渠道级错误"}, {524, ErrorLevelChannel, "524 A Timeout Occurred - 渠道级错误"}, // 兜底策略测试 {418, ErrorLevelKey, "418 未知4xx - 兜底策略应为Key级冷却"}, {451, ErrorLevelKey, "451 未知4xx - 兜底策略应为Key级冷却"}, {599, ErrorLevelChannel, "599 未知5xx - 兜底策略应为Channel级冷却"}, } for _, tt := range tests { t.Run(string(rune(tt.statusCode)), func(t *testing.T) { result := ClassifyHTTPStatus(tt.statusCode) if result != tt.expected { t.Errorf("状态码 %d: 期望 %v, 实际 %v (%s)", tt.statusCode, tt.expected, result, tt.reason) } }) } } // 测试context.Canceled与HTTP 499的区分 func TestClassifyError_ContextCanceled(t *testing.T) { tests := []struct { name string err error expectedStatus int expectedLevel ErrorLevel expectedRetry bool reason string }{ { name: "context_canceled_from_client", err: context.Canceled, expectedStatus: 499, expectedLevel: ErrorLevelClient, expectedRetry: false, reason: "下游客户端取消请求(context.Canceled)应返回499+ErrorLevelClient,不重试", }, { name: "context_deadline_exceeded", err: context.DeadlineExceeded, expectedStatus: 504, expectedLevel: ErrorLevelChannel, expectedRetry: true, reason: "上游超时(context.DeadlineExceeded)应返回504+ErrorLevelChannel,可重试其他渠道", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { assertClassifyError(t, tt.err, tt.expectedStatus, tt.expectedLevel, tt.expectedRetry, tt.reason) }) } } // 测试空响应错误分类 func TestClassifyError_EmptyResponse(t *testing.T) { tests := []struct { name string err error expectedStatus int expectedLevel ErrorLevel expectedRetry bool reason string }{ { name: "empty_response_200_with_content_length_0", err: errors.New("upstream returned empty response (200 OK with Content-Length: 0)"), expectedStatus: 502, expectedLevel: ErrorLevelChannel, expectedRetry: true, reason: "200状态码但Content-Length=0应视为上游故障,触发渠道级重试", }, { name: "empty_response_uppercase", err: errors.New("Upstream Returned Empty Response (200 OK with Content-Length: 0)"), expectedStatus: 502, expectedLevel: ErrorLevelChannel, expectedRetry: true, reason: "大小写不敏感匹配", }, { name: "empty_response_sentinel_without_content_length", err: fmt.Errorf("response probe failed: %w", ErrUpstreamEmptyResponse), expectedStatus: 502, expectedLevel: ErrorLevelChannel, expectedRetry: true, reason: "无Content-Length的200空体也应视为上游故障,不能依赖错误文案", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { assertClassifyError(t, tt.err, tt.expectedStatus, tt.expectedLevel, tt.expectedRetry, tt.reason) }) } } // 测试HTTP/2流错误分类 func TestClassifyError_HTTP2StreamErrors(t *testing.T) { tests := []struct { name string err error expectedStatus int expectedLevel ErrorLevel expectedRetry bool reason string }{ { name: "http2_response_body_closed", err: fmt.Errorf("http2: response body closed"), expectedStatus: 502, // Bad Gateway expectedLevel: ErrorLevelChannel, expectedRetry: true, reason: "上游服务器主动关闭HTTP/2流,应触发渠道级重试", }, { name: "http2_stream_error_internal", err: fmt.Errorf("stream error: stream ID 7; INTERNAL_ERROR"), expectedStatus: 502, // Bad Gateway expectedLevel: ErrorLevelChannel, expectedRetry: true, reason: "HTTP/2 RST_STREAM INTERNAL_ERROR,上游服务异常", }, { name: "http2_stream_error_protocol", err: fmt.Errorf("stream error: stream ID 3; PROTOCOL_ERROR"), expectedStatus: 502, // Bad Gateway expectedLevel: ErrorLevelChannel, expectedRetry: true, reason: "HTTP/2协议错误,应切换渠道重试", }, { name: "http2_error_wrapped", err: fmt.Errorf("failed to read response: http2: response body closed"), expectedStatus: 502, // Bad Gateway expectedLevel: ErrorLevelChannel, expectedRetry: true, reason: "包装后的HTTP/2错误也应正确识别", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { assertClassifyError(t, tt.err, tt.expectedStatus, tt.expectedLevel, tt.expectedRetry, tt.reason) }) } } func TestClassifyError_ConnectionResetAndBrokenPipe(t *testing.T) { tests := []struct { name string err error expectedStatus int expectedLevel ErrorLevel expectedRetry bool reason string }{ { name: "broken_pipe_client_closed", err: errors.New("write: broken pipe"), expectedStatus: 499, expectedLevel: ErrorLevelClient, expectedRetry: false, reason: "broken pipe 基本是客户端断开,不应重试", }, { name: "connection_reset_by_peer_upstream", err: errors.New("read: connection reset by peer"), expectedStatus: 502, expectedLevel: ErrorLevelChannel, expectedRetry: true, reason: "connection reset by peer 通常是上游断开,应按 502 进入健康度统计并允许切换渠道重试", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { assertClassifyError(t, tt.err, tt.expectedStatus, tt.expectedLevel, tt.expectedRetry, tt.reason) }) } } // 测试429错误的智能分类 func TestClassifyRateLimitError(t *testing.T) { tests := []struct { name string headers map[string][]string responseBody []byte expected ErrorLevel reason string }{ // Retry-After头测试 { name: "retry_after_120_seconds", headers: map[string][]string{ "Retry-After": {"120"}, }, responseBody: []byte(`{"error":"rate limit exceeded"}`), expected: ErrorLevelChannel, reason: "Retry-After > 60秒表示账户级或IP级限流,应冷却渠道", }, { name: "retry_after_30_seconds", headers: map[string][]string{ "Retry-After": {"30"}, }, responseBody: []byte(`{"error":"rate limit exceeded"}`), expected: ErrorLevelKey, reason: "Retry-After ≤ 60秒表示Key级限流,应冷却Key", }, { name: "retry_after_60_seconds_boundary", headers: map[string][]string{ "Retry-After": {"60"}, }, responseBody: []byte(`{"error":"rate limit exceeded"}`), expected: ErrorLevelKey, reason: "Retry-After = 60秒边界值,保守策略为Key级", }, { name: "retry_after_http_date", headers: map[string][]string{ "Retry-After": {"Wed, 29 Oct 2025 12:00:00 GMT"}, }, responseBody: []byte(`{"error":"rate limit exceeded"}`), expected: ErrorLevelChannel, reason: "HTTP日期格式通常表示长时间限流,应冷却渠道", }, { name: "retry_after_invalid_format", headers: map[string][]string{ "Retry-After": {"invalid"}, }, responseBody: []byte(`{"error":"rate limit exceeded"}`), expected: ErrorLevelKey, reason: "无效的Retry-After格式应默认为Key级", }, // X-RateLimit-Scope头测试 { name: "scope_global", headers: map[string][]string{ "X-Ratelimit-Scope": {"global"}, }, responseBody: []byte(`{"error":"rate limit exceeded"}`), expected: ErrorLevelChannel, reason: "global scope表示全局限流,应冷却渠道", }, { name: "scope_ip", headers: map[string][]string{ "X-Ratelimit-Scope": {"ip"}, }, responseBody: []byte(`{"error":"rate limit exceeded"}`), expected: ErrorLevelChannel, reason: "IP scope表示IP级限流,应冷却渠道", }, { name: "scope_account", headers: map[string][]string{ "X-Ratelimit-Scope": {"account"}, }, responseBody: []byte(`{"error":"rate limit exceeded"}`), expected: ErrorLevelChannel, reason: "account scope表示账户级限流,应冷却渠道", }, { name: "scope_user", headers: map[string][]string{ "X-Ratelimit-Scope": {"user"}, }, responseBody: []byte(`{"error":"rate limit exceeded"}`), expected: ErrorLevelKey, reason: "user scope(非global/ip/account)应默认为Key级", }, { name: "scope_case_insensitive", headers: map[string][]string{ "X-Ratelimit-Scope": {"GLOBAL"}, }, responseBody: []byte(`{"error":"rate limit exceeded"}`), expected: ErrorLevelChannel, reason: "scope匹配应不区分大小写", }, // 响应体错误描述测试 { name: "body_ip_rate_limit", headers: map[string][]string{}, responseBody: []byte(`{ "error": { "message": "IP rate limit exceeded", "type": "rate_limit_error" } }`), expected: ErrorLevelChannel, reason: "响应体包含'ip rate limit'应冷却渠道", }, { name: "body_account_rate_limit", headers: map[string][]string{}, responseBody: []byte(`{ "error": { "message": "Account rate limit exceeded for organization" } }`), expected: ErrorLevelChannel, reason: "响应体包含'account rate limit'应冷却渠道", }, { name: "body_global_rate_limit", headers: map[string][]string{}, responseBody: []byte(`{ "error": "Global rate limit has been exceeded" }`), expected: ErrorLevelChannel, reason: "响应体包含'global rate limit'应冷却渠道", }, { name: "body_organization_limit", headers: map[string][]string{}, responseBody: []byte(`{ "error": "Organization limit reached" }`), expected: ErrorLevelChannel, reason: "响应体包含'organization limit'应冷却渠道", }, { name: "body_case_insensitive", headers: map[string][]string{}, responseBody: []byte(`{ "error": "IP Rate Limit Exceeded" }`), expected: ErrorLevelChannel, reason: "响应体匹配应不区分大小写", }, // 默认Key级限流测试 { name: "default_key_level_no_special_indicators", headers: map[string][]string{}, responseBody: []byte(`{ "error": "Too many requests" }`), expected: ErrorLevelKey, reason: "无特殊指示器时应默认为Key级", }, { name: "nil_headers", headers: nil, responseBody: []byte(`{"error":"rate limit"}`), expected: ErrorLevelKey, reason: "nil headers应默认为Key级", }, { name: "empty_headers", headers: map[string][]string{}, responseBody: []byte(`{"error":"rate limit"}`), expected: ErrorLevelKey, reason: "空headers应默认为Key级", }, { name: "empty_response_body", headers: map[string][]string{}, responseBody: []byte{}, expected: ErrorLevelKey, reason: "空响应体应默认为Key级", }, { name: "nil_response_body", headers: map[string][]string{}, responseBody: nil, expected: ErrorLevelKey, reason: "nil响应体应默认为Key级", }, // 组合场景测试 { name: "combined_retry_after_and_scope", headers: map[string][]string{ "Retry-After": {"30"}, // Key级指示器 "X-Ratelimit-Scope": {"ip"}, // 渠道级指示器 }, responseBody: []byte(`{"error":"rate limit"}`), expected: ErrorLevelChannel, reason: "Retry-After检查优先于Scope,但>60秒判断优先", }, { name: "combined_scope_and_body", headers: map[string][]string{ "X-Ratelimit-Scope": {"user"}, // Key级指示器 }, responseBody: []byte(`{"error":"IP rate limit exceeded"}`), // 渠道级指示器 expected: ErrorLevelChannel, reason: "响应体中的渠道级指示器应被识别", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { result := ClassifyHTTPResponseWithMeta(429, tt.headers, tt.responseBody).Level if result != tt.expected { t.Errorf("%s\n 期望: %v\n 实际: %v\n 原因: %s", tt.name, tt.expected, result, tt.reason) } }) } } // TestClassifySSEError 测试SSE error事件分类 func TestClassifySSEError(t *testing.T) { tests := []struct { name string responseBody []byte expected ErrorLevel reason string }{ { name: "api_error_500", responseBody: []byte(`{"type":"error","error":{"type":"api_error","message":"上游API返回错误: 500"}}`), expected: ErrorLevelChannel, reason: "api_error表示上游服务错误,应触发渠道级冷却", }, { name: "overloaded_error", responseBody: []byte(`{"type":"error","error":{"type":"overloaded_error","message":"服务过载"}}`), expected: ErrorLevelChannel, reason: "overloaded_error表示上游过载,应触发渠道级冷却", }, { name: "service_unavailable_error_server_is_overloaded", responseBody: []byte(`{"type":"error","error":{"type":"service_unavailable_error","code":"server_is_overloaded","message":"Our servers are currently overloaded. Please try again later.","param":null},"sequence_number":2}`), expected: ErrorLevelChannel, reason: "service_unavailable_error/server_is_overloaded 表示上游服务过载,应触发渠道级冷却", }, { name: "rate_limit_error", responseBody: []byte(`{"type":"error","error":{"type":"rate_limit_error","message":"请求过于频繁"}}`), expected: ErrorLevelKey, reason: "rate_limit_error可能只是单个Key限流,应触发Key级冷却", }, { name: "authentication_error", responseBody: []byte(`{"type":"error","error":{"type":"authentication_error","message":"认证失败"}}`), expected: ErrorLevelKey, reason: "authentication_error是Key级问题,应触发Key级冷却", }, { name: "invalid_request_error", responseBody: []byte(`{"type":"error","error":{"type":"invalid_request_error","message":"请求无效"}}`), expected: ErrorLevelKey, reason: "invalid_request_error是Key级问题,应触发Key级冷却", }, { name: "1308_error", responseBody: []byte(`{"type":"error","error":{"type":"1308","message":"已达到使用上限"}}`), expected: ErrorLevelKey, reason: "1308错误是Key配额问题,应触发Key级冷却", }, { name: "1308_error_with_code_field", responseBody: []byte(`{"error":{"code":"1308","message":"已达到 5 小时的使用上限。您的限额将在 2025-12-21 15:00:05 重置。"},"request_id":"202512211335142b05cc4f9bbb4e6c"}`), expected: ErrorLevelKey, reason: "使用code字段的1308错误(非Anthropic格式)应触发Key级冷却", }, { name: "1305_overloaded", responseBody: []byte(`{"error":{"code":"1305","message":"The service may be temporarily overloaded"},"request_id":"..."}`), expected: ErrorLevelChannel, reason: "1305错误表示上游过载,应触发渠道级冷却", }, { name: "1310_limit_exhausted", responseBody: []byte(`{"error":{"code":"1310","message":"Weekly/Monthly Limit Exhausted. Your limit will reset at 2026-04-20 15:24:20"},"request_id":"..."}`), expected: ErrorLevelKey, reason: "1310错误是Key配额问题,应触发Key级冷却", }, { name: "unknown_error_type", responseBody: []byte(`{"type":"error","error":{"type":"unknown_type","message":"未知错误"}}`), expected: ErrorLevelKey, reason: "未知错误类型应保守处理为Key级", }, { name: "empty_body", responseBody: []byte{}, expected: ErrorLevelKey, reason: "空响应体应保守处理为Key级", }, { name: "invalid_json", responseBody: []byte(`not valid json`), expected: ErrorLevelKey, reason: "无效JSON应保守处理为Key级", }, { name: "nil_body", responseBody: nil, expected: ErrorLevelKey, reason: "nil响应体应保守处理为Key级", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { // 使用 ClassifyHTTPResponseWithMeta 测试 597 状态码 result := ClassifyHTTPResponseWithMeta(StatusSSEError, nil, tt.responseBody).Level if result != tt.expected { t.Errorf("%s\n 期望: %v\n 实际: %v\n 原因: %s", tt.name, tt.expected, result, tt.reason) } }) } } func TestClassify400Error(t *testing.T) { tests := []struct { name string responseBody []byte expected ErrorLevel reason string }{ { name: "empty_body", responseBody: []byte{}, expected: ErrorLevelChannel, reason: "空响应体应判定为上游异常", }, { name: "nil_body", responseBody: nil, expected: ErrorLevelChannel, reason: "nil响应体应判定为上游异常", }, { name: "invalid_api_key", responseBody: []byte(`{"error": {"message": "Invalid API Key provided"}}`), expected: ErrorLevelKey, reason: "包含 invalid_api_key 特征应判定为 Key 级错误", }, { name: "api_key_error", responseBody: []byte(`{"error": {"message": "The API key you provided is malformed"}}`), expected: ErrorLevelKey, reason: "包含 api key 特征应判定为 Key 级错误", }, { name: "bad_request_params", responseBody: []byte(`{"error": {"message": "Missing required parameter: 'model'"}}`), expected: ErrorLevelChannel, reason: "代理场景下400默认视为上游异常", }, { name: "invalid_json_format", responseBody: []byte(`{"error": {"message": "Invalid JSON format in request body"}}`), expected: ErrorLevelChannel, reason: "代理场景下400默认视为上游异常", }, { name: "validation_error", responseBody: []byte(`{"error": {"message": "Validation failed: max_tokens must be positive"}}`), expected: ErrorLevelChannel, reason: "代理场景下400默认视为上游异常", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { result := classify400Error(tt.responseBody) if result != tt.expected { t.Errorf("%s\n 期望: %v\n 实际: %v\n 原因: %s\n 响应体: %s", tt.name, tt.expected, result, tt.reason, string(tt.responseBody)) } }) } } func TestClassify404Error(t *testing.T) { tests := []struct { name string responseBody []byte expected ErrorLevel reason string }{ { name: "empty_body", responseBody: []byte{}, expected: ErrorLevelChannel, reason: "空响应体应判定为渠道配置错误(路径不存在)", }, { name: "nil_body", responseBody: nil, expected: ErrorLevelChannel, reason: "nil响应体应判定为渠道配置错误(路径不存在)", }, { name: "model_not_found", responseBody: []byte(`{"error": {"message": "The model 'gpt-5' could not be found", "type": "model_not_found"}}`), expected: ErrorLevelClient, reason: "模型不存在应判定为客户端级错误", }, { name: "resource_not_exist", responseBody: []byte(`{"error": {"message": "The requested resource does not exist"}}`), expected: ErrorLevelClient, reason: "资源不存在应判定为客户端级错误", }, { name: "html_error_page", responseBody: []byte(`