mirror of
https://github.com/tiennm99/goclaw.git
synced 2026-07-12 13:04:57 +00:00
230 lines
7.2 KiB
Go
230 lines
7.2 KiB
Go
package providers
|
|
|
|
import (
|
|
"errors"
|
|
"net"
|
|
"strings"
|
|
)
|
|
|
|
// FailoverReason categorizes provider errors for failover decisions.
|
|
type FailoverReason string
|
|
|
|
const (
|
|
FailoverAuth FailoverReason = "auth"
|
|
FailoverAuthPermanent FailoverReason = "auth_permanent"
|
|
FailoverFormat FailoverReason = "format"
|
|
FailoverRateLimit FailoverReason = "rate_limit"
|
|
FailoverOverloaded FailoverReason = "overloaded"
|
|
FailoverServerError FailoverReason = "server_error"
|
|
FailoverBilling FailoverReason = "billing"
|
|
FailoverTimeout FailoverReason = "timeout"
|
|
FailoverModelNotFound FailoverReason = "model_not_found"
|
|
FailoverContentPolicy FailoverReason = "content_policy"
|
|
FailoverUnknown FailoverReason = "unknown"
|
|
)
|
|
|
|
// FailoverClassification is the result of classifying a provider error.
|
|
type FailoverClassification struct {
|
|
Kind string // "reason" or "context_overflow"
|
|
Reason FailoverReason // only when Kind == "reason"
|
|
}
|
|
|
|
// Convenience constructors
|
|
func classifyReason(r FailoverReason) FailoverClassification {
|
|
return FailoverClassification{Kind: "reason", Reason: r}
|
|
}
|
|
|
|
func classifyContextOverflow() FailoverClassification {
|
|
return FailoverClassification{Kind: "context_overflow"}
|
|
}
|
|
|
|
// ErrorClassifier classifies provider errors for failover routing.
|
|
type ErrorClassifier interface {
|
|
Classify(err error, statusCode int, body string) FailoverClassification
|
|
}
|
|
|
|
// DefaultClassifier handles common HTTP status + body pattern matching.
|
|
type DefaultClassifier struct {
|
|
providerPatterns map[string][]ErrorPattern
|
|
}
|
|
|
|
// ErrorPattern maps a body substring pattern to a FailoverReason.
|
|
type ErrorPattern struct {
|
|
Contains string
|
|
Reason FailoverReason
|
|
}
|
|
|
|
// NewDefaultClassifier returns a classifier with built-in patterns
|
|
// for OpenAI and Anthropic providers pre-registered.
|
|
func NewDefaultClassifier() *DefaultClassifier {
|
|
c := &DefaultClassifier{
|
|
providerPatterns: make(map[string][]ErrorPattern),
|
|
}
|
|
RegisterOpenAIPatterns(c)
|
|
RegisterAnthropicPatterns(c)
|
|
return c
|
|
}
|
|
|
|
// RegisterPatterns adds provider-specific error body patterns.
|
|
// Must be called during init only (not thread-safe for concurrent writes).
|
|
func (c *DefaultClassifier) RegisterPatterns(provider string, patterns []ErrorPattern) {
|
|
c.providerPatterns[provider] = append(c.providerPatterns[provider], patterns...)
|
|
}
|
|
|
|
// Classify determines the failover reason for an error.
|
|
func (c *DefaultClassifier) Classify(err error, statusCode int, body string) FailoverClassification {
|
|
lower := strings.ToLower(body)
|
|
|
|
// Context overflow — not a failover reason, triggers compaction
|
|
if isContextOverflow(lower) {
|
|
return classifyContextOverflow()
|
|
}
|
|
|
|
// HTTP status-based classification
|
|
switch {
|
|
case statusCode == 429:
|
|
return classifyReason(FailoverRateLimit)
|
|
case statusCode == 402:
|
|
return classifyReason(FailoverBilling)
|
|
case statusCode == 401 || statusCode == 403:
|
|
if containsAny(lower, "revoked", "deleted", "deactivated", "disabled", "expired") {
|
|
return classifyReason(FailoverAuthPermanent)
|
|
}
|
|
return classifyReason(FailoverAuth)
|
|
case statusCode == 404:
|
|
if containsAny(lower, "model", "not found", "does not exist") {
|
|
return classifyReason(FailoverModelNotFound)
|
|
}
|
|
case statusCode == 529:
|
|
// Anthropic-specific overloaded status
|
|
return classifyReason(FailoverOverloaded)
|
|
case statusCode >= 500:
|
|
if containsAny(lower, "overload", "capacity", "too many") {
|
|
return classifyReason(FailoverOverloaded)
|
|
}
|
|
return classifyReason(FailoverServerError)
|
|
}
|
|
|
|
// Body pattern matching for specific error types
|
|
if containsAny(lower, "credit balance", "insufficient_quota", "billing") && statusCode != 0 {
|
|
return classifyReason(FailoverBilling)
|
|
}
|
|
if containsAny(lower, "tool_call", "function_call", "invalid_request") && statusCode == 400 {
|
|
return classifyReason(FailoverFormat)
|
|
}
|
|
if isContentPolicyRefusal(lower, statusCode) {
|
|
return classifyReason(FailoverContentPolicy)
|
|
}
|
|
|
|
// Provider-specific patterns
|
|
for _, patterns := range c.providerPatterns {
|
|
for _, p := range patterns {
|
|
if strings.Contains(lower, strings.ToLower(p.Contains)) {
|
|
return classifyReason(p.Reason)
|
|
}
|
|
}
|
|
}
|
|
|
|
// Network errors → timeout
|
|
if isNetworkError(err) {
|
|
return classifyReason(FailoverTimeout)
|
|
}
|
|
|
|
return classifyReason(FailoverUnknown)
|
|
}
|
|
|
|
// ClassifyHTTPError is a convenience that extracts status + body from an HTTPError.
|
|
func ClassifyHTTPError(classifier ErrorClassifier, err error) FailoverClassification {
|
|
if err == nil {
|
|
return classifyReason(FailoverUnknown)
|
|
}
|
|
var httpErr *HTTPError
|
|
if errors.As(err, &httpErr) {
|
|
return classifier.Classify(err, httpErr.Status, httpErr.Body)
|
|
}
|
|
classification := classifier.Classify(err, 0, err.Error())
|
|
if classification.Kind == "context_overflow" || classification.Reason != FailoverUnknown {
|
|
return classification
|
|
}
|
|
// Non-HTTP error — check for network errors
|
|
if isNetworkError(err) {
|
|
return classifyReason(FailoverTimeout)
|
|
}
|
|
return classifyReason(FailoverUnknown)
|
|
}
|
|
|
|
// isContextOverflow detects context window exceeded errors across providers.
|
|
func isContextOverflow(lower string) bool {
|
|
return containsAny(lower,
|
|
"context length exceeded",
|
|
"context window",
|
|
"maximum context length",
|
|
"token limit",
|
|
"too many tokens",
|
|
"prompt is too long",
|
|
// Chinese patterns (Qwen/DashScope)
|
|
"超出最大长度限制",
|
|
"上下文长度",
|
|
// Issue 958: Additional patterns
|
|
"prompt exceeds max length", // ZAI/GLM-5
|
|
"request_too_large", // Generic
|
|
"input is too long", // DashScope
|
|
"请求输入过长", // Chinese generic
|
|
)
|
|
}
|
|
|
|
// IsContextOverflowMessage exports overflow detection for use by pipeline.
|
|
func IsContextOverflowMessage(lower string) bool {
|
|
return isContextOverflow(lower)
|
|
}
|
|
|
|
// isNetworkError checks if an error is a network-level failure.
|
|
func isNetworkError(err error) bool {
|
|
if err == nil {
|
|
return false
|
|
}
|
|
var netErr net.Error
|
|
if errors.As(err, &netErr) {
|
|
return true
|
|
}
|
|
s := err.Error()
|
|
return containsAny(s, "connection reset", "broken pipe", "EOF", "timeout", "ECONNREFUSED")
|
|
}
|
|
|
|
func isContentPolicyRefusal(lower string, statusCode int) bool {
|
|
if statusCode != 0 && containsAny(lower, "data_inspection_failed", "inappropriate content", "content_policy_violation") {
|
|
return true
|
|
}
|
|
if containsAny(lower, "limited access to this content for safety reasons", "content for safety reasons") {
|
|
return true
|
|
}
|
|
return strings.Contains(lower, "invalid prompt") && strings.Contains(lower, "safety")
|
|
}
|
|
|
|
// containsAny returns true if s contains any of the substrings.
|
|
func containsAny(s string, subs ...string) bool {
|
|
for _, sub := range subs {
|
|
if strings.Contains(s, sub) {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// RegisterOpenAIPatterns adds OpenAI-ecosystem specific error patterns.
|
|
func RegisterOpenAIPatterns(c *DefaultClassifier) {
|
|
c.RegisterPatterns("openai", []ErrorPattern{
|
|
{Contains: "model_is_deactivated", Reason: FailoverModelNotFound},
|
|
{Contains: "model not found", Reason: FailoverModelNotFound},
|
|
{Contains: "does not exist", Reason: FailoverModelNotFound},
|
|
})
|
|
}
|
|
|
|
// RegisterAnthropicPatterns adds Anthropic-specific error patterns.
|
|
func RegisterAnthropicPatterns(c *DefaultClassifier) {
|
|
c.RegisterPatterns("anthropic", []ErrorPattern{
|
|
{Contains: "overloaded", Reason: FailoverOverloaded},
|
|
{Contains: "credit balance", Reason: FailoverBilling},
|
|
})
|
|
}
|