mirror of
https://github.com/router-for-me/CLIProxyAPI.git
synced 2026-10-25 17:18:20 +08:00
- Introduce Devin quota exhaustion detection for daily and weekly usage windows with reset timestamp parsing. - Apply credential-scoped 429 status errors with reset-based Retry-After for Devin quota limits. - Support automatic quota revalidation and early credential cooldown release upon confirmed usage recovery. Closes: #6690
64 lines
2.4 KiB
Go
64 lines
2.4 KiB
Go
package helps
|
|
|
|
import (
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
// DevinQuotaPeriod identifies an account-wide Devin usage quota window.
|
|
type DevinQuotaPeriod int
|
|
|
|
const (
|
|
// DevinQuotaPeriodNone means the error is not an account-wide
|
|
// daily/weekly usage quota exhaustion.
|
|
DevinQuotaPeriodNone DevinQuotaPeriod = iota
|
|
// DevinQuotaPeriodDaily marks daily usage quota exhaustion.
|
|
DevinQuotaPeriodDaily
|
|
// DevinQuotaPeriodWeekly marks weekly usage quota exhaustion.
|
|
DevinQuotaPeriodWeekly
|
|
)
|
|
|
|
// devinQuotaResetMaxDelay bounds how far in the future an observed quota
|
|
// reset timestamp may lie before it is treated as unusable. A weekly window
|
|
// plus slack covers a just-rolled-over observation; anything further out is
|
|
// implausible for a daily/weekly quota and must not pin the credential.
|
|
const devinQuotaResetMaxDelay = 8 * 24 * time.Hour
|
|
|
|
// DevinUsageQuotaExhaustionPeriod reports whether the upstream error
|
|
// message explicitly says the account-wide daily or weekly usage quota has
|
|
// been exhausted (issue #6690). Matching is deliberately narrow: the period
|
|
// word, "quota", and "exhausted" must all be present, so monthly ACU
|
|
// messages, generic rate limits, and transient capacity errors never match
|
|
// and stay on the short per-model backoff path. If the message somehow
|
|
// names both periods, weekly wins because it is the longer window.
|
|
func DevinUsageQuotaExhaustionPeriod(message string) DevinQuotaPeriod {
|
|
lower := strings.ToLower(message)
|
|
if !strings.Contains(lower, "quota") || !strings.Contains(lower, "exhausted") {
|
|
return DevinQuotaPeriodNone
|
|
}
|
|
if strings.Contains(lower, "weekly") {
|
|
return DevinQuotaPeriodWeekly
|
|
}
|
|
if strings.Contains(lower, "daily") {
|
|
return DevinQuotaPeriodDaily
|
|
}
|
|
return DevinQuotaPeriodNone
|
|
}
|
|
|
|
// DevinQuotaRetryAfter converts an observed quota reset timestamp (stored
|
|
// in auth Quota.Signals by the executor's Refresh path) into a retry delay.
|
|
// It returns nil unless the signal parses and is strictly in the future
|
|
// within a plausible daily/weekly horizon, in which case the caller should
|
|
// leave the cooldown duration to the conductor's credential backoff.
|
|
func DevinQuotaRetryAfter(resetAtSignal string, now time.Time) *time.Duration {
|
|
resetAt, errParse := time.Parse(time.RFC3339, strings.TrimSpace(resetAtSignal))
|
|
if errParse != nil {
|
|
return nil
|
|
}
|
|
delay := resetAt.Sub(now)
|
|
if delay <= 0 || delay > devinQuotaResetMaxDelay {
|
|
return nil
|
|
}
|
|
return &delay
|
|
}
|