gateway: a vendor refusing the client outright (WorkBuddy's 400 "Illegal API invocation from an unapproved channel") hands a group's request to its next member (PR #232 Y-C-Fan: a routing group stopped at a WorkBuddy member refusing Claude Code's chats) — retryable let a 400 fall over only on quota or unserved-model words, so this refusal, which is of the provider and not the request, went back to the agent. A new refusedWords (unapproved channel, illegal api invocation) joins them. The request body is not changed: a chat opening with Claude Code's own system prompt is still sent as it came. Tested with a new case in TestGroupFailsOverAnUnservedModel's retryable table (false before) and the gateway package.

This commit is contained in:
yetone committed 2026-09-30 02:15:47 +08:00
1 parent ac1f522154
commit 5353ed54fe
2 files changed
+8 -1

No files matched your search

+7 -1
View File
@@ -411,6 +411,12 @@ var quotaWords = regexp.MustCompile(`(?i)quota|insufficient|balance|credit|billi
// key, or this way — words another provider, or key, may not answer with.
var unservedWords = regexp.MustCompile(`(?i)model.{0,80}(not (supported|accessible|available|found|enabled|allowed)|unsupported|does ?n[o']t exist|unknown|invalid)|(no such|unknown|invalid|unsupported) model|model_not_found|模型.{0,12}(不存在|不支持|无权|未开通)`)
// refusedWords are how a vendor says it won't take requests from this
// client at all — WorkBuddy's "Illegal API invocation from an unapproved
// channel" to a chat opening with another agent's own system prompt — a
// refusal of the provider, not of the request, another member may serve.
var refusedWords = regexp.MustCompile(`(?i)unapproved channel|illegal api invocation`)
// retryable says whether another provider may do better with a request
// that failed this way: the vendor was busy, out of quota or failing, or
// this key or provider can't serve it — not the request itself at fault.
@@ -419,7 +425,7 @@ func retryable(status int, body []byte) bool {
case status == 401, status == 402, status == 403, status == 404, status == 408, status == 429, status >= 500:
return true
case status == 400, status == 422:
return quotaWords.Match(body) || unservedWords.Match(body)
return quotaWords.Match(body) || unservedWords.Match(body) || refusedWords.Match(body)
}
return false
}
+1
View File
@@ -90,6 +90,7 @@ func TestGroupFailsOverAnUnservedModel(t *testing.T) {
{400, `{"error":{"message":"The model gpt-x does not exist"}}`, true},
{400, `{"error":{"message":"messages: field required"}}`, false},
{400, `{"error":{"message":"This model's maximum context length is 128000 tokens"}}`, false},
{400, `{"error":{"message":"Illegal API invocation from an unapproved channel"}}`, true},
} {
if got := retryable(x.status, []byte(x.body)); got != x.want {
t.Errorf("retryable(%d, %s) = %v", x.status, x.body, got)