Something went wrong. Try again.
sloproxy
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254package helps
import ( cryptorand "crypto/rand" "math/big" "net/http" "strconv" "strings" "time"
log "github.com/sirupsen/logrus")
const ( defaultClaudeRateLimitFuzzMinSeconds = 1 defaultClaudeRateLimitFuzzMaxSeconds = 30)
// ClaudeHeadersIndicateUnifiedRateLimitRejection reports whether response headers explicitly// declare an Anthropic shared 5h or 7d rate-limit rejection. A Fable-only 7d_oi rejection// remains model-scoped when both shared windows are explicitly allowed (status "allowed" or "allowed_warning").func ClaudeHeadersIndicateUnifiedRateLimitRejection(headers http.Header) bool { if headers == nil { return false } unifiedStatus := strings.ToLower(strings.TrimSpace(getHeaderCaseInsensitive(headers, "Anthropic-Ratelimit-Unified-Status"))) status5h := strings.ToLower(strings.TrimSpace(getHeaderCaseInsensitive(headers, "Anthropic-Ratelimit-Unified-5h-Status"))) if status5h == "rejected" { return true } status7d := strings.ToLower(strings.TrimSpace(getHeaderCaseInsensitive(headers, "Anthropic-Ratelimit-Unified-7d-Status"))) if status7d == "rejected" { return true } if unifiedStatus != "rejected" { return false } status7dOI := strings.ToLower(strings.TrimSpace(getHeaderCaseInsensitive(headers, "Anthropic-Ratelimit-Unified-7d_oi-Status"))) return !isFableOnlyRejection(status5h, status7d, status7dOI)}
func isClaudeWindowAllowed(status string) bool { return status == "allowed" || status == "allowed_warning"}
func isFableOnlyRejection(status5h, status7d, status7dOI string) bool { return isClaudeWindowAllowed(status5h) && isClaudeWindowAllowed(status7d) && status7dOI == "rejected"}
// ParseClaudeRateLimitReset inspects Anthropic response headers for shared and Fable-specific// unified rate-limit and standard Retry-After reset information, returning the conservative cooldown// duration including a bounded non-negative random grace period.// If no valid future reset information is present, it returns nil.func ParseClaudeRateLimitReset(headers http.Header, now time.Time) *time.Duration { return parseClaudeRateLimitResetWithFuzz(headers, now, defaultClaudeRateLimitFuzzMinSeconds, defaultClaudeRateLimitFuzzMaxSeconds)}
func parseClaudeRateLimitResetWithFuzz(headers http.Header, now time.Time, minFuzzSec, maxFuzzSec int) *time.Duration { if headers == nil { return nil }
unifiedStatus := strings.ToLower(strings.TrimSpace(getHeaderCaseInsensitive(headers, "Anthropic-Ratelimit-Unified-Status"))) status5h := strings.ToLower(strings.TrimSpace(getHeaderCaseInsensitive(headers, "Anthropic-Ratelimit-Unified-5h-Status"))) status7d := strings.ToLower(strings.TrimSpace(getHeaderCaseInsensitive(headers, "Anthropic-Ratelimit-Unified-7d-Status"))) status7dOI := strings.ToLower(strings.TrimSpace(getHeaderCaseInsensitive(headers, "Anthropic-Ratelimit-Unified-7d_oi-Status"))) fableOnlyRejection := isFableOnlyRejection(status5h, status7d, status7dOI)
var candidateDeadlines []time.Time var rejectedWindows []string
if unifiedStatus == "rejected" { rejectedWindows = append(rejectedWindows, "unified") } if status5h == "rejected" { rejectedWindows = append(rejectedWindows, "5h") } if status7d == "rejected" { rejectedWindows = append(rejectedWindows, "7d") } if status7dOI == "rejected" { rejectedWindows = append(rejectedWindows, "7d_oi") }
// 1. Retry-After header if rawRetryAfter := getHeaderCaseInsensitive(headers, "Retry-After"); rawRetryAfter != "" { if !containsString(rejectedWindows, "retry-after") { rejectedWindows = append(rejectedWindows, "retry-after") } if t, ok := parseRetryAfterHeader(rawRetryAfter, now); ok && t.After(now) { candidateDeadlines = append(candidateDeadlines, t) } }
// 2. 5-hour window reset (only when rejected) if status5h == "rejected" { if raw := getHeaderCaseInsensitive(headers, "Anthropic-Ratelimit-Unified-5h-Reset"); raw != "" { if t, ok := parseUnixOrTimestamp(raw); ok && t.After(now) { candidateDeadlines = append(candidateDeadlines, t) } } }
// 3. 7-day window reset (only when rejected) if status7d == "rejected" { if raw := getHeaderCaseInsensitive(headers, "Anthropic-Ratelimit-Unified-7d-Reset"); raw != "" { if t, ok := parseUnixOrTimestamp(raw); ok && t.After(now) { candidateDeadlines = append(candidateDeadlines, t) } } }
// 4. Fable-specific 7-day window reset (only when rejected and not a Fable-only rejection) if status7dOI == "rejected" && !fableOnlyRejection { if raw := getHeaderCaseInsensitive(headers, "Anthropic-Ratelimit-Unified-7d_oi-Reset"); raw != "" { if t, ok := parseUnixOrTimestamp(raw); ok && t.After(now) { candidateDeadlines = append(candidateDeadlines, t) } } }
// 5. Unified reset header: unifiedRejected := !fableOnlyRejection && (unifiedStatus == "rejected" || status5h == "rejected" || status7d == "rejected" || status7dOI == "rejected" || (unifiedStatus == "" && !isClaudeWindowAllowed(status5h) && !isClaudeWindowAllowed(status7d)))
if unifiedRejected { if raw := getHeaderCaseInsensitive(headers, "Anthropic-Ratelimit-Unified-Reset"); raw != "" { if !containsString(rejectedWindows, "unified") { rejectedWindows = append(rejectedWindows, "unified") } if t, ok := parseUnixOrTimestamp(raw); ok && t.After(now) { candidateDeadlines = append(candidateDeadlines, t) } } }
if len(candidateDeadlines) == 0 { if len(rejectedWindows) > 0 { log.WithFields(log.Fields{ "rejected_windows": strings.Join(rejectedWindows, ","), "status": "fallback_exponential_backoff", }).Info("Anthropic rate limit window rejected; falling back to generic exponential backoff") } return nil }
// Pick the latest applicable deadline across rejected windows var latestDeadline time.Time for _, deadline := range candidateDeadlines { if deadline.After(latestDeadline) { latestDeadline = deadline } }
if latestDeadline.IsZero() || !latestDeadline.After(now) { if len(rejectedWindows) > 0 { log.WithFields(log.Fields{ "rejected_windows": strings.Join(rejectedWindows, ","), "status": "fallback_exponential_backoff", }).Info("Anthropic rate limit window rejected; falling back to generic exponential backoff") } return nil }
baseDuration := latestDeadline.Sub(now) fuzz := randomClaudeFuzzDuration(minFuzzSec, maxFuzzSec) effectiveDuration := baseDuration + fuzz
log.WithFields(log.Fields{ "rejected_windows": strings.Join(rejectedWindows, ","), "effective_cooldown": effectiveDuration.String(), "base_cooldown": baseDuration.String(), "fuzz": fuzz.String(), "deadline": latestDeadline.Format(time.RFC3339), }).Info("parsed Anthropic rate limit reset headers")
return &effectiveDuration}
func containsString(list []string, target string) bool { for _, item := range list { if item == target { return true } } return false}
func getHeaderCaseInsensitive(h http.Header, target string) string { if h == nil { return "" } if val := h.Get(target); val != "" { return val } for k, v := range h { if strings.EqualFold(k, target) && len(v) > 0 { return v[0] } } return ""}
func parseUnixOrTimestamp(raw string) (time.Time, bool) { raw = strings.TrimSpace(raw) if raw == "" { return time.Time{}, false } if sec, err := strconv.ParseFloat(raw, 64); err == nil && sec > 0 { secInt := int64(sec) nsec := int64((sec - float64(secInt)) * 1e9) return time.Unix(secInt, nsec), true } if t, err := time.Parse(time.RFC3339, raw); err == nil { return t, true } if t, err := http.ParseTime(raw); err == nil { return t, true } return time.Time{}, false}
func parseRetryAfterHeader(raw string, now time.Time) (time.Time, bool) { raw = strings.TrimSpace(raw) if raw == "" { return time.Time{}, false } if sec, err := strconv.ParseFloat(raw, 64); err == nil && sec > 0 { d := time.Duration(sec * float64(time.Second)) return now.Add(d), true } if t, err := http.ParseTime(raw); err == nil { return t, true } if t, err := time.Parse(time.RFC3339, raw); err == nil { return t, true } return time.Time{}, false}
func randomClaudeFuzzDuration(minSec, maxSec int) time.Duration { if maxSec <= minSec { if minSec < 0 { return 0 } return time.Duration(minSec) * time.Second } nBig, err := cryptorand.Int(cryptorand.Reader, big.NewInt(int64(maxSec-minSec+1))) if err != nil { return time.Duration(minSec) * time.Second } return time.Duration(minSec+int(nBig.Int64())) * time.Second}