// 错误分类器 -- Provider Adapter 模式实现. // // 升华改进(ELEVATED): 早期方案把 Anthropic 特定的 header 解析,消息匹配硬编码在通用错误处理中, // 导致无法支持其他供应商(OpenAI / Bedrock / 本地模型). // // 本设计:ErrorClassifier 接口定义分类契约,每个供应商实现自己的 Classifier. // 引擎只依赖 ErrorCategory 枚举,不依赖任何供应商特定的字符串. // // 替代方案:<原方案所有供应商的逻辑混在一个 500 行 if-else 中> package api import ( "net/http" "strconv" "strings" "time" ) // ============================================================ // ErrorClassifier 接口 // ============================================================ // ErrorClassifier 将原始 HTTP 响应或连接错误分类为结构化的 APIError. // // 精妙之处(CLEVER): 接口只有一个方法--简单就是最好的抽象. // 不同供应商实现不同的 Classify 逻辑,但返回统一的 *APIError. // 支持 CompositeClassifier 叠加(宪法第8条:叠加而非替换). type ErrorClassifier interface { // Classify 分类一个 API 错误. // statusCode: HTTP 状态码(连接错误时为 0) // headers: 响应头(连接错误时为 nil) // body: 响应体(连接错误时为 nil) // cause: 底层错误(网络错误时非 nil) Classify(statusCode int, headers http.Header, body []byte, cause error) *APIError } // ============================================================ // DefaultClassifier - 纯 HTTP 状态码分类 // ============================================================ // DefaultClassifier 是基于 HTTP 状态码的默认分类器. // 不解析任何供应商特定的 header 或 body 内容,作为兜底分类器使用. type DefaultClassifier struct{} // Classify 基于 HTTP 状态码分类. func (c *DefaultClassifier) Classify(statusCode int, headers http.Header, body []byte, cause error) *APIError { // 连接错误(statusCode == 0 表示未收到 HTTP 响应) if cause != nil && statusCode == 0 { return classifyConnectionError(cause) } errType, message := ParseAPIErrorBody(body) if message == "" { message = string(body) } apiErr := &APIError{ StatusCode: statusCode, Msg: message, RespHeaders: headers, Body: string(body), Cause: cause, } // 状态码分类 switch { case statusCode == 400: apiErr.ErrCategory = classifyBadRequest(errType, message) case statusCode == 401 || statusCode == 403: apiErr.ErrCategory = ErrAuthentication case statusCode == 404: apiErr.ErrCategory = ErrModelNotFound case statusCode == 408: apiErr.ErrCategory = ErrTimeout apiErr.Retry = &RetryInfo{Retryable: true} case statusCode == 413: apiErr.ErrCategory = ErrRequestTooLarge case statusCode == 429: apiErr.ErrCategory = ErrRateLimit apiErr.Retry = parseRetryInfoFromHeaders(headers) case statusCode == 529: apiErr.ErrCategory = ErrOverloaded apiErr.Retry = &RetryInfo{Retryable: true} case statusCode >= 500: apiErr.ErrCategory = ErrServerError apiErr.Retry = &RetryInfo{Retryable: true} default: apiErr.ErrCategory = ErrUnknown } return apiErr } // classifyBadRequest 细分 400 错误的子类型. func classifyBadRequest(errType, message string) ErrorCategory { lower := strings.ToLower(message) // 过载错误(SDK 流式时有时把 529 报为 400) if errType == "overloaded_error" || strings.Contains(message, `"type":"overloaded_error"`) { return ErrOverloaded } // Prompt 太长 if strings.Contains(lower, "prompt is too long") { return ErrPromptTooLong } // 媒体大小限制 if (strings.Contains(message, "image exceeds") && strings.Contains(message, "maximum")) || (strings.Contains(message, "image dimensions exceed") && strings.Contains(message, "many-image")) { return ErrMediaTooLarge } if matchesPDFPageLimit(message) { return ErrMediaTooLarge } // Tool 相关错误 if strings.Contains(message, "tool_use` ids were found without `tool_result`") { return ErrToolMismatch } if strings.Contains(message, "unexpected `tool_use_id` found in `tool_result`") { return ErrUnexpectedTool } if strings.Contains(message, "`tool_use` ids must be unique") { return ErrDuplicateToolID } // 模型名称无效 if strings.Contains(lower, "invalid model name") { return ErrInvalidModel } // 余额不足 if strings.Contains(lower, "credit balance is too low") { return ErrBilling } return ErrInvalidRequest } // matchesPDFPageLimit 检查是否是 PDF 页数超限错误. func matchesPDFPageLimit(message string) bool { return strings.Contains(message, "maximum of") && strings.Contains(message, "PDF pages") } // ============================================================ // AnthropicClassifier - Anthropic 供应商特定分类 // ============================================================ // AnthropicClassifier 在 DefaultClassifier 基础上增加 Anthropic 特定的分类逻辑: // - 解析 anthropic-ratelimit-* header // - 检测 overloaded_error(SDK 有时丢失 529 状态码) // - 解析 x-should-retry header // - SSL 证书错误诊断 // // 精妙之处(CLEVER): 先调 DefaultClassifier 做基础分类,再用 Anthropic 特定信息增强. // 这样即使 Anthropic header 格式变了,基础分类仍然正确(降级而非崩溃). type AnthropicClassifier struct { // Hinter 提供连接诊断提示(可选) Hinter DiagnosticHinter } // Classify 分类 API 错误,增强 Anthropic 特定信息. func (c *AnthropicClassifier) Classify(statusCode int, headers http.Header, body []byte, cause error) *APIError { // 先用默认分类器做基础分类 base := (&DefaultClassifier{}).Classify(statusCode, headers, body, cause) // 增强:确保 overloaded 有重试信息 // DefaultClassifier.classifyBadRequest 已经能识别 errType == "overloaded_error", // 这里补充确保 Retry 被设置. if base.ErrCategory == ErrOverloaded && base.Retry == nil { base.Retry = &RetryInfo{Retryable: true} } // 增强:429 细分 - 解析 Anthropic 统一限流 header if base.ErrCategory == ErrRateLimit && headers != nil { base.Retry = c.parseAnthropicRateLimit(headers, base.Retry) } // 增强:x-should-retry header(Anthropic 特有) if headers != nil { if shouldRetry := ParseShouldRetry(headers.Get("x-should-retry")); shouldRetry != nil { if base.Retry == nil { base.Retry = &RetryInfo{} } base.Retry.ServerSaid = shouldRetry base.Retry.Retryable = *shouldRetry } } // 增强:prompt_too_long 时提取 token 差值 if base.ErrCategory == ErrPromptTooLong { base.TokenGap = ParseTokenGap(base.Msg) } // 增强:连接错误诊断提示 if base.ErrCategory == ErrSSL || base.ErrCategory == ErrConnection { if c.Hinter != nil { base.Hint = c.Hinter.Hint(base) } } return base } // parseAnthropicRateLimit 解析 Anthropic 统一限流 header. // // Header 字段: // - anthropic-ratelimit-unified-representative-claim: "five_hour" | "seven_day" | ... // - anthropic-ratelimit-unified-overage-status: "allowed" | "rejected" // - anthropic-ratelimit-unified-reset: Unix timestamp (seconds) // - retry-after: seconds func (c *AnthropicClassifier) parseAnthropicRateLimit(headers http.Header, existing *RetryInfo) *RetryInfo { info := existing if info == nil { info = &RetryInfo{} } // retry-after(标准 HTTP header) if ra := headers.Get("Retry-After"); ra != "" { info.After = ParseRetryAfter(ra) } // anthropic-ratelimit-unified-reset(Anthropic 特有) if reset := headers.Get("anthropic-ratelimit-unified-reset"); reset != "" { // 值是 Unix 秒时间戳 // 精妙之处(CLEVER): 用 reset 时间戳计算精确等待时间, // 比 retry-after 更准确(retry-after 可能是近似值). // 但需要客户端时钟大致准确. // 如果已经有 retry-after 且更长,保留更长的那个(保守策略). resetDuration := parseResetTimestamp(reset) if resetDuration > info.After { info.After = resetDuration } } // overage-status(判断是否可以通过 overage 解决) overageStatus := headers.Get("anthropic-ratelimit-unified-overage-status") if overageStatus == "rejected" { // 被明确拒绝,不建议自动重试 info.Retryable = false } else { info.Retryable = info.After > 0 // 有 retry-after 才重试 } return info } // parseResetTimestamp 将 Unix 秒时间戳转换为从现在起的等待时间. func parseResetTimestamp(value string) time.Duration { // 使用 time 包的当前时间--不可测试性的代价换取简洁性. // 如果需要可测试性,可注入时间源. timestamp := parseInt64(value) if timestamp <= 0 { return 0 } delay := time.Until(time.Unix(timestamp, 0)) if delay < 0 { return 0 // 已经过了 reset 时间 } return delay } // parseInt64 安全地解析 int64 字符串. func parseInt64(s string) int64 { s = strings.TrimSpace(s) v, err := parseInt(s) if err != nil { return 0 } return v } // parseInt 包装 strconv.ParseInt(避免重复写错误处理). func parseInt(s string) (int64, error) { return parseIntFunc(s, 10, 64) } // parseIntFunc 是 strconv.ParseInt 的别名(同包可直接用,此处为语义清晰). var parseIntFunc = strconv.ParseInt // ============================================================ // CompositeClassifier - 叠加分类器(宪法第8条) // ============================================================ // CompositeClassifier 按优先级组合多个分类器. // 第一个返回非 ErrUnknown 分类的 Classifier 胜出. // // 升华改进(ELEVATED): 支持运行时动态添加分类器-- // 例如仓储场景可以 Add 一个识别 PLC 连接错误的分类器, // 而不需要修改核心代码. type CompositeClassifier struct { classifiers []ErrorClassifier } // NewCompositeClassifier 创建组合分类器. func NewCompositeClassifier(classifiers ...ErrorClassifier) *CompositeClassifier { return &CompositeClassifier{classifiers: classifiers} } // Add 添加分类器(追加到末尾,优先级最低). func (c *CompositeClassifier) Add(classifier ErrorClassifier) { c.classifiers = append(c.classifiers, classifier) } // Classify 按顺序调用分类器,第一个给出明确分类的胜出. func (c *CompositeClassifier) Classify(statusCode int, headers http.Header, body []byte, cause error) *APIError { var last *APIError for _, classifier := range c.classifiers { result := classifier.Classify(statusCode, headers, body, cause) if result == nil { continue } if result.ErrCategory != ErrUnknown { return result } last = result // 保留最后一个结果作为 fallback } if last != nil { return last } // 所有分类器都没结果,返回兜底 return &APIError{ ErrCategory: ErrUnknown, StatusCode: statusCode, Msg: string(body), RespHeaders: headers, Body: string(body), Cause: cause, } } // ============================================================ // 辅助:从 HTTP 响应头解析重试信息 // ============================================================ // parseRetryInfoFromHeaders 从标准 HTTP header 解析重试信息. func parseRetryInfoFromHeaders(headers http.Header) *RetryInfo { if headers == nil { return &RetryInfo{Retryable: true} // 429 默认可重试 } info := &RetryInfo{Retryable: true} // 标准 retry-after header if ra := headers.Get("Retry-After"); ra != "" { info.After = ParseRetryAfter(ra) } // x-should-retry header if shouldRetry := ParseShouldRetry(headers.Get("x-should-retry")); shouldRetry != nil { info.ServerSaid = shouldRetry info.Retryable = *shouldRetry } return info }