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