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:
yetone committed 2026-09-28 21:37:46 +08:00
1 parent 2de043dabf
commit 46454d9801
2 files changed
+41 -1

No files matched your search

+29
View File
@@ -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)
}
}
+12 -1
View File
@@ -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