gateway: every vendor's "too long" reaches the agent as context_length_exceeded and fails over to nobody, and a spent budget isn't asked again in place (owner: 继续做第 4 项,错误分类接入 failover) — pi's two tables (packages/ai/src/utils/overflow.ts, retry.ts) against magpie's classifier: 15 of pi's 25 overflow texts weren't known, so the agent got the vendor's words and didn't compact, and four were worse: Kimi's "exceeded model token limit" rested the account as out of quota, and z.ai's model_context_window_exceeded, Ollama's "prompt too long; exceeded max context length" and "token limit exceeded" went to every other account in turn (quotaWords' "exceeded"). tooLongRe takes pi's patterns (request_too_large, Gemini, xAI, Groq, OpenRouter, Together, llama.cpp, LM Studio, Copilot, MiniMax, Kimi, DS4, z.ai, Ollama, DashScope), and overflowed (a 400/413/422 saying so) is neither retryable nor a quota or credit failure, so nobody rests and it is answered at once. "out of budget" / "budget exceeded|exhausted" are a used-up plan: a 429 was a rate limit asked three times in place, a 400 was answered without trying the next account. TestEveryVendorsOverflow and TestOutOfBudgetIsntRetriedInPlace fail on the old code (26 failures). Real accounts, sandbox magpie, a 2.2M-token request: Qoder's "invalid params, context window exceeds limit" came back from the old build as 413 api_error with no code (Claude Code and Codex wouldn't compact) and is now 400 context_length_exceeded / Anthropic "prompt is too long: …"; WorkBuddy, WorkBuddy AI, Kiro and Grok (SuperGrok) answer context_length_exceeded too, and ordinary requests through each still answer. go vet, GOOS=linux/windows builds and go test -tags nogui -count=1 ./... under a temp HOME pass. Silent overflow (usage past the window on a 200) isn't detected: the reply is already the agent's by then.

This commit is contained in:
yetone committed 2026-10-03 14:55:37 +08:00
1 parent eb7c45ac9f
commit 11a09d418c
4 files changed
+105 -4

No files matched your search

+5 -1
View File
@@ -549,7 +549,7 @@ func restOf(id string) (Rest, bool) {
// quotaWords are how vendors say "out of quota" or "slow down" when their
// status code doesn't: some answer 400 or 403 with it.
var quotaWords = regexp.MustCompile(`(?i)quota|insufficient|balance|credit|billing|exceeded|rate.?limit|usage.?limit|limit.?reached|hit your .*limit|limit.{0,24}resets|too many requests|overloaded|余额|额度|欠费|限流|频率|套餐|用量|上限`)
var quotaWords = regexp.MustCompile(`(?i)quota|insufficient|balance|credit|billing|out of budget|budget (exceeded|exhausted)|exceeded|rate.?limit|usage.?limit|limit.?reached|hit your .*limit|limit.{0,24}resets|too many requests|overloaded|余额|额度|欠费|限流|频率|套餐|用量|上限`)
// unservedWords are how a vendor says the model isn't one it serves this
// key, or this way — words another provider, or key, may not answer with.
@@ -580,6 +580,10 @@ func shapeRefused(status int, body []byte) bool {
// this key or provider can't serve it — not the request itself at fault.
func retryable(status int, body []byte) bool {
switch {
case overflowed(status, body):
// a conversation too long for the model is as long on every
// account of it: the agent is told, and compacts
return false
case status == 401, status == 402, status == 403, status == 404, status == 408, status == 429, status >= 500:
return true
case status >= 400 && provider.EdgeBlocked(body):
+20 -2
View File
@@ -3535,8 +3535,26 @@ func writeError(w http.ResponseWriter, proto provider.Protocol, status int, msg
// tooLongRe matches how vendors say a request is more than the model's
// context holds: OpenAI's context_length_exceeded, Anthropic's "prompt is
// too long", Volcengine's "Input exceeds the context limit", "maximum
// context length", "context window"…
var tooLongRe = regexp.MustCompile(`(?i)context_length_exceeded|prompt is too long|input is too long|(exceeds?|exceeded|over|beyond)( the)?( model'?s?)?( maximum)? context|context (length|limit|window) (exceeded|is exceeded)|maximum context length|too many (input |prompt )?tokens|上下文(长度)?(超|过长)|超(过|出)(了)?(模型)?(的)?(最大)?上下文`)
// context length", "context window"… and the second line, as pi
// (packages/ai/src/utils/overflow.ts) collected them: Anthropic's 413
// request_too_large, Gemini's "input token count … exceeds the maximum",
// xAI's "maximum prompt length is", Groq's "reduce the length of the
// messages", OpenRouter's "maximum allowed input length", Together's
// "longer than the model's context length", llama.cpp's "available
// context size", LM Studio's "greater than the context length", Copilot's
// "prompt token count of", MiniMax's "context window exceeds limit",
// Kimi's "exceeded model token limit", DS4's "configured context size",
// z.ai's "Prompt too long" / "Prompt exceeds max length" and
// model_context_window_exceeded, Ollama's "exceeded max context length",
// DashScope's "Range of input length should be", "token limit exceeded".
var tooLongRe = regexp.MustCompile(`(?i)context_length_exceeded|prompt is too long|input is too long|(exceeds?|exceeded|over|beyond)( the)?( model'?s?)?( maximum)? context|context (length|limit|window) (exceeded|is exceeded)|maximum context length|too many (input |prompt )?tokens|上下文(长度)?(超|过长)|超(过|出)(了)?(模型)?(的)?(最大)?上下文` +
`|request_too_large|input token count.*exceeds the maximum|maximum prompt length is \d|reduce the length of the messages|maximum allowed input length|longer than the model'?s context length|available context size|greater than the context length|prompt token count of [\d,]+ exceeds the limit|context window exceeds limit|exceeded model token limit|configured context size|prompt too long|prompt exceeds max length|context_window_exceeded|exceeded (max |maximum )?context length|range of input length should be|token limit exceeded`)
// overflowed says a refused request was too long for the model — a
// request error (400, 413, 422) whose words say so.
func overflowed(status int, body []byte) bool {
return (status == 400 || status == 413 || status == 422) && tooLong(status, string(body))
}
// tooLong is whether a vendor's error says the conversation no longer
// fits. One about max_tokens is left alone: the reply's allowance, not
+5 -1
View File
@@ -102,7 +102,7 @@ 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|余额|欠费|充值|账户.*(不足|停)`)
// 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)|额度|用量|套餐|上限`)
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
// minute — which usedUpWords took for a used-up plan ("Rate limit
// exceeded", "reached"): as magpie words an upstream error, with
@@ -165,6 +165,10 @@ func failure(status int, body []byte) string {
return failVerify
}
switch {
case overflowed(status, body):
// 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"):
return failCredit
case status == 429 && rateWords.Match(body) && !plannedWords.Match(body):
+75
View File
@@ -53,3 +53,78 @@ func TestContextOverflowSaidTheClientsWay(t *testing.T) {
}
}
}
// Each vendor's way of saying the conversation is too long, as pi
// collected them (packages/ai/src/utils/overflow.ts), reaches the agent as
// context_length_exceeded, so it compacts; and none is taken for a used-up
// plan or asked of another account, which holds the conversation no
// better: Kimi's "exceeded model token limit" rested the account as out of
// quota, z.ai's model_context_window_exceeded went to every account in
// turn.
func TestEveryVendorsOverflow(t *testing.T) {
for _, tc := range []struct {
status int
msg string
}{
{413, `{"type":"error","error":{"type":"request_too_large","message":"Request exceeds the maximum size"}}`},
{400, "The input token count (1196265) exceeds the maximum number of tokens allowed (1048575)"},
{400, "This model's maximum prompt length is 131072 but the request contains 537812 tokens"},
{400, "Please reduce the length of the messages or completion"},
{400, "Input length 2000 exceeds the maximum allowed input length of 1000 tokens."},
{400, "The input (2000 tokens) is longer than the model's context length (1000 tokens)."},
{400, "the request exceeds the available context size, try increasing it"},
{400, "tokens to keep from the initial prompt is greater than the context length"},
{400, "prompt token count of 2000 exceeds the limit of 1000"},
{400, "invalid params, context window exceeds limit"},
{400, "Your request exceeded model token limit: 1000 (requested: 2000)"},
{400, "Prompt has 2000 tokens, but the configured context size is 1000 tokens"},
{400, `{"code":"1261","message":"Prompt too long"}`},
{400, `{"code":"1261","message":"Prompt exceeds max length"}`},
{400, "model_context_window_exceeded"},
{400, "prompt too long; exceeded max context length by 100 tokens"},
{400, "Range of input length should be [1, 98304]"},
{400, "token limit exceeded"},
} {
if !tooLong(tc.status, tc.msg) {
t.Errorf("%q isn't taken for a conversation too long", tc.msg)
}
if retryable(tc.status, []byte(tc.msg)) {
t.Errorf("%q is asked of another account", tc.msg)
}
if f := failure(tc.status, []byte(tc.msg)); f != failOther {
t.Errorf("%q is a %s failure", tc.msg, f)
}
}
// a rate limit worded with tokens is still one
if tooLong(429, "Too many tokens, please wait before trying again.") {
t.Error("a rate limit taken for a conversation too long")
}
}
// A spent budget, like a used-up plan (pi's retry.ts lists both as not to
// be retried), goes to the next account at once rather than being asked
// again in place: "out of budget" was a rate limit, asked three times.
func TestOutOfBudgetIsntRetriedInPlace(t *testing.T) {
for _, tc := range []struct {
status int
msg string
}{
{429, "out of budget"},
{400, "Budget exceeded for this key"},
{429, `{"error":{"type":"GoUsageLimitError","message":"Monthly usage limit reached"}}`},
{429, `{"error":{"code":"subscription_sharing_usage_limit_exceeded"}}`},
} {
if !retryable(tc.status, []byte(tc.msg)) {
t.Errorf("%d %q isn't asked of the next account", tc.status, tc.msg)
}
if f := failure(tc.status, []byte(tc.msg)); f != failQuota {
t.Errorf("%d %q is a %s failure", tc.status, tc.msg, f)
}
if _, again := passing(tc.status, nil, []byte(tc.msg), 0); again {
t.Errorf("%d %q is asked again in place", tc.status, tc.msg)
}
}
if _, again := passing(429, nil, []byte("Rate limit reached for requests"), 0); !again {
t.Error("a rate limit isn't asked again")
}
}