From e63fbb0e69b21ec89079f11e14dd1f64a3b360e9 Mon Sep 17 00:00:00 2001 From: yetone Date: Tue, 6 Oct 2026 01:28:58 +0800 Subject: [PATCH] =?UTF-8?q?gateway:=20a=20429=20that=20says=20the=20balanc?= =?UTF-8?q?e=20is=20spent=20is=20out=20of=20credit,=20not=20a=20rate=20lim?= =?UTF-8?q?it=20(kkgg=20on=20Discord:=20GLM=20=E7=9B=B4=E6=8E=A5429?= =?UTF-8?q?=E4=BA=86)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Zhipu answers a GLM Coding Plan's key sent to its pay-as-you-go endpoint with 429 and 1113 "余额不足或无可用资源包,请充值". failure() left every 429 out of the credit words (so "Add 10 credits to unlock 1000 free requests per day" stays a used-up plan), and the request list showed "429 · 限流", which sent the user looking for a rate limit. A 429 that names an insufficient balance (insufficient balance/credit/funds, 余额不足, 欠费, 请充值) is failCredit now: the list says "429 · 余额不足", and the key rests as one out of credit. TestRateLimitIsNotQuota has Zhipu's body as it comes and as magpie words it (rate_limit_error), in Chinese and English, and fails without the change. go vet, go build (GOOS=linux nogui, GOOS=windows) and go test -tags nogui ./... under a temp HOME pass. --- internal/gateway/ratewords_test.go | 5 +++++ internal/gateway/routing.go | 6 +++++- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/internal/gateway/ratewords_test.go b/internal/gateway/ratewords_test.go index 02e0f017..e9fd89bb 100644 --- a/internal/gateway/ratewords_test.go +++ b/internal/gateway/ratewords_test.go @@ -17,6 +17,11 @@ func TestRateLimitIsNotQuota(t *testing.T) { `{"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, + // Zhipu's pay-as-you-go endpoint answering a Coding Plan's key + // (kkgg on Discord: shown as "429 · rate limited") + `{"error":{"code":"1113","message":"余额不足或无可用资源包,请充值。"}}`: failCredit, + `{"error":{"message":"Zhipu GLM: 余额不足或无可用资源包,请充值。","type":"rate_limit_error"}}`: failCredit, + `{"error":{"code":"1113","message":"Insufficient balance or no resource package. Please recharge."}}`: failCredit, } { if got := failure(429, []byte(body)); got != want { t.Errorf("%s: %s, want %s", body, got, want) diff --git a/internal/gateway/routing.go b/internal/gateway/routing.go index 44ee919b..5e359361 100644 --- a/internal/gateway/routing.go +++ b/internal/gateway/routing.go @@ -108,6 +108,10 @@ const ( var ( // creditWords: the account or key has no money left. creditWords = regexp.MustCompile(`(?i)insufficient.?(balance|credit|fund)|balance|credit|billing|payment|arrear|overdue|suspended|余额|欠费|充值|账户.*(不足|停)`) + // brokeWords: a 429 that says the balance itself is spent, as Zhipu + // answers a GLM Coding Plan's key at the pay-as-you-go endpoint (1113 + // "余额不足或无可用资源包,请充值") — out of credit, not a rate limit + brokeWords = regexp.MustCompile(`(?i)insufficient.?(balance|credit|fund)|余额不足|欠费|请充值`) // quotaWords: it has used up what its plan allows for now. usedUpWords = regexp.MustCompile(`(?i)quota|usage.?limit|out of budget|budget (exceeded|exhausted)|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 @@ -188,7 +192,7 @@ func failure(status int, body []byte) string { // Kimi's "exceeded model token limit" is the conversation's // length, not the plan's return failOther - case status == 402, creditWords.Match(body) && status != 429 || strings.Contains(string(body), "insufficient_quota"): + case status == 402, creditWords.Match(body) && (status != 429 || brokeWords.Match(body)) || strings.Contains(string(body), "insufficient_quota"): return failCredit case status == 429 && rateWords.Match(body) && !plannedWords.Match(body): return failRate