gateway: a 429 that is a per-minute rate limit rests a minute, not a quarter of an hour (found reviewing #153) — usedUpWords took "Rate limit exceeded" and "Rate limit reached … tokens per min" for a used-up plan, and as magpie words an upstream error ("rate_limit_error" its type) exceeded.*limit matched every such 429 whatever the vendor said, so a provider rate-limited for a minute sat out 15 as out of quota (an OpenRouter free model's free-models-per-min, OpenAI's TPM, Anthropic's per-minute limit). Now a 429 in rate words (rate limit, too many requests, per second/minute, RPM/TPM) is a rate limit unless it also says the plan's allowance is used (quota, usage limit, per day/week/month, daily…), and a 429 saying that is out of quota, as a day's free requests are. Tested: those bodies as vendors and magpie word them, and a group through the gateway whose first member answers OpenRouter's free-models-per-min — it rests rate for 1m where main rested quota for 15m
This commit is contained in:
2 files changed
+41
-1
No files matched your search
@@ -0,0 +1,29 @@
|
||||
package gateway
|
||||
|
||||
import "testing"
|
||||
|
||||
// A 429 that is a per-minute rate limit rests a minute, not the quarter
|
||||
// hour of a used-up plan — however magpie or the vendor words it (#153).
|
||||
func TestRateLimitIsNotQuota(t *testing.T) {
|
||||
for body, want := range map[string]string{
|
||||
// as magpie words an upstream 429 for the agent
|
||||
`{"error":{"code":null,"message":"ORF: Rate limit exceeded: free-models-per-min","param":null,"type":"rate_limit_error"}}`: failRate,
|
||||
`{"error":{"message":"Rate limit reached for gpt-4o in organization org-x on tokens per min (TPM): Limit 30000, Used 29000"}}`: failRate,
|
||||
`{"type":"error","error":{"type":"rate_limit_error","message":"This request would exceed your organization's rate limit of 50,000 input tokens per minute"}}`: failRate,
|
||||
`{"error":{"message":"Too Many Requests"}}`: failRate,
|
||||
// still a used-up plan
|
||||
`{"error":{"message":"Rate limit exceeded: free-models-per-day. Add 10 credits to unlock 1000 free model requests per day"}}`: failQuota,
|
||||
`{"type":"error","error":{"type":"rate_limit_error","message":"Claude AI usage limit reached|1790000000"}}`: failQuota,
|
||||
`{"error":{"type":"usage_limit_reached","message":"You've hit your usage limit."}}`: failQuota,
|
||||
`{"error":{"message":"Resource has been exhausted (e.g. check quota).","status":"RESOURCE_EXHAUSTED"}}`: failQuota,
|
||||
`{"error":{"message":"You exceeded your current quota","type":"insufficient_quota"}}`: failCredit,
|
||||
} {
|
||||
if got := failure(429, []byte(body)); got != want {
|
||||
t.Errorf("%s: %s, want %s", body, got, want)
|
||||
}
|
||||
}
|
||||
// not a 429: words alone don't make it a rate limit
|
||||
if got := failure(403, []byte(`{"error":{"message":"rate limit exceeded for plan"}}`)); got != failQuota {
|
||||
t.Errorf("403: %s, want %s", got, failQuota)
|
||||
}
|
||||
}
|
||||
@@ -86,6 +86,15 @@ var (
|
||||
creditWords = regexp.MustCompile(`(?i)insufficient.?(balance|credit|fund)|balance|credit|billing|payment|arrear|overdue|suspended|余额|欠费|充值|账户.*(不足|停)`)
|
||||
// quotaWords: it has used up what its plan allows for now.
|
||||
usedUpWords = regexp.MustCompile(`(?i)quota|usage.?limit|limit.?reached|hit your .*limit|limit.{0,24}resets|exceeded.*(plan|limit)|额度|用量|套餐|上限`)
|
||||
// rateWords: a 429 that is a short rate limit — requests or tokens per
|
||||
// minute — which usedUpWords took for a used-up plan ("Rate limit
|
||||
// exceeded", "reached"): as magpie words an upstream error, with
|
||||
// "rate_limit_error" for its type, every such 429 was, and rested a
|
||||
// quarter of an hour rather than a minute (#153).
|
||||
rateWords = regexp.MustCompile(`(?i)rate.?limit|too many requests|per.?(second|sec|minute|min)\b|\b[rt]pm\b|频率|太频繁`)
|
||||
// plannedWords: a 429 that says the plan's own allowance is used, rate
|
||||
// words or not — a day's free requests, say.
|
||||
plannedWords = regexp.MustCompile(`(?i)quota|usage.?limit|hit your .*limit|limit.{0,24}resets|per.?(day|week|month)|daily|weekly|monthly|额度|用量|套餐`)
|
||||
// resetsWords: Claude Code's "usage limit reached|<when it resets>".
|
||||
resetsWords = regexp.MustCompile(`(?i)limit reached\|(\d{10})\b`)
|
||||
)
|
||||
@@ -117,7 +126,9 @@ func failure(status int, body []byte) string {
|
||||
switch {
|
||||
case status == 402, creditWords.Match(body) && status != 429 || strings.Contains(string(body), "insufficient_quota"):
|
||||
return failCredit
|
||||
case usedUpWords.Match(body):
|
||||
case status == 429 && rateWords.Match(body) && !plannedWords.Match(body):
|
||||
return failRate
|
||||
case usedUpWords.Match(body), status == 429 && plannedWords.Match(body):
|
||||
return failQuota
|
||||
case status == 429:
|
||||
return failRate
|
||||
|
||||
Reference in new issue
Block a user