Files
magpie/internal/gateway/zcode_blocked_test.go
T
yetone 15fa17f1c4 gateway: ZCode's count_tokens is estimated, and a vendor's firewall block is said in plain words (ZCode Start Plan: HTTP 405 "request has been blocked due to unusual activity") — Claude Code asks count_tokens before every turn, and magpie sent each to zcode.z.ai/api/v1/zcode-plan/anthropic/v1/messages/count_tokens, which answers it with an error every time and which ZCode itself never asks; a ZCode account's count, built-in or on its plugin, is now estimated as Cursor's, Grok's and Kiro's are, and zcode.z.ai is not asked. The 405 is Alibaba Cloud's edge firewall (ESA) in front of zcode.z.ai blocking the address: its HTML page was shown as "405 Method Not Allowed" and, a 405 not passing as a provider's failure, no other member of a group was asked. A block page (request has been blocked, errors.aliyun.com) now reads "the provider's network firewall blocked requests from this IP; wait a while, or switch to another network or proxy" to the agent and in the trace, where the Routing page gives it on its own line in English or Chinese, and the group goes on to its next member, the blocked one resting as for any other failure. No request header, user agent or device id changes. TestZCodeCountTokensEstimated, TestMovedZCodeCountTokensEstimated, TestEdgeBlockedSaidPlainly and routing-blocked.test.cjs fail without the change. go vet, go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui ./... pass; routing-blocked, routing-wb-refused, routing-proxy, routing-shape, click-scroll, old-webkit pass.
2026-10-01 15:04:16 +08:00

134 lines
5.0 KiB
Go

package gateway
import (
"encoding/json"
"io"
"net/http"
"net/http/httptest"
"strings"
"sync"
"testing"
"github.com/yetone/magpie/internal/provider"
)
// countAsked is an upstream that counts tokens, noting each time it's asked.
type countAsked struct {
mu sync.Mutex
paths []string
}
func (c *countAsked) ServeHTTP(w http.ResponseWriter, r *http.Request) {
c.mu.Lock()
c.paths = append(c.paths, r.URL.Path)
c.mu.Unlock()
w.Header().Set("Content-Type", "application/json")
io.WriteString(w, `{"input_tokens":999}`)
}
func (c *countAsked) asked() []string {
c.mu.Lock()
defer c.mu.Unlock()
return append([]string(nil), c.paths...)
}
func estimated(t *testing.T, rec *httptest.ResponseRecorder) int {
t.Helper()
var got struct {
InputTokens int `json:"input_tokens"`
}
if err := json.Unmarshal(rec.Body.Bytes(), &got); rec.Code != 200 || err != nil || got.InputTokens == 0 {
t.Fatalf("count_tokens: %d %s", rec.Code, rec.Body)
}
return got.InputTokens
}
// Claude Code asks count_tokens before each turn; ZCode itself never does,
// and zcode.z.ai answers it with an error every time. A ZCode account's
// count is estimated, the vendor not asked.
func TestZCodeCountTokensEstimated(t *testing.T) {
fresh(t)
up := &countAsked{}
srv := httptest.NewServer(up)
t.Cleanup(srv.Close)
p := provider.Provider{ID: "zcode", Name: "ZCode", Anthropic: srv.URL + "/api/v1/zcode-plan/anthropic", Models: []string{"glm-5.1"},
Account: &provider.Account{Agent: "zcode", User: "trial@example.com", Plan: "Start Plan"}}
body := []byte(`{"model":"glm-5.1","messages":[{"role":"user","content":"hello there"}]}`)
rec := httptest.NewRecorder()
New().countOn(rec, httptest.NewRequest("POST", "/v1/messages/count_tokens", nil), p, "glm-5.1", true, body)
if n := estimated(t, rec); n == 999 || len(up.asked()) > 0 {
t.Fatalf("zcode.z.ai was asked to count: %v, %d", up.asked(), n)
}
}
// ZCode moved onto its plugin estimates as the built-in does.
func TestMovedZCodeCountTokensEstimated(t *testing.T) {
up := &countAsked{}
movedFake(t, "zcode", up)
rec := httptest.NewRecorder()
New().Handler().ServeHTTP(rec, httptest.NewRequest("POST", "/v1/messages/count_tokens",
strings.NewReader(`{"model":"zcode/fake-claude","messages":[{"role":"user","content":"hello there"}]}`)))
if n := estimated(t, rec); n == 999 || len(up.asked()) > 0 {
t.Fatalf("the vendor was asked to count: %v, %d", up.asked(), n)
}
}
// edgeBlockPage is Alibaba Cloud's ESA block page, as zcode.z.ai served it.
const edgeBlockPage = `<!DOCTYPE html>
<html><head><meta charset="utf-8"><title>405</title></head>
<body><div class="main"><h1>Sorry, your request has been blocked due to unusual activity.</h1>
<p>Request ID: 0bd6a2b417592xxxx</p>
<a href="https://errors.aliyun.com/error/405?code=blocked">errors.aliyun.com</a></div></body></html>`
// A vendor's edge firewall blocking this address answers an HTML page: the
// agent and the trace are told so in plain words, not the page, and the
// group's next member is asked, as for another provider's failure.
func TestEdgeBlockedSaidPlainly(t *testing.T) {
fresh(t)
blocked := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "text/html")
w.WriteHeader(http.StatusMethodNotAllowed)
io.WriteString(w, edgeBlockPage)
}))
t.Cleanup(blocked.Close)
var other int
fine := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
other++
w.Header().Set("Content-Type", "application/json")
io.WriteString(w, `{"id":"ok","choices":[{"message":{"role":"assistant","content":"from b"}}]}`)
}))
t.Cleanup(fine.Close)
for _, p := range []provider.Provider{
{ID: "za", Name: "ZA", Key: "k", Models: []string{"m"}, Chat: blocked.URL + "/v1"},
{ID: "zb", Name: "ZB", Key: "k", Models: []string{"m"}, Chat: fine.URL + "/v1"},
} {
if err := provider.Save(p); err != nil {
t.Fatal(err)
}
}
// alone: the agent is told what happened
s := New()
code, reply := postAs(t, s, "", `{"model":"za/m","messages":[{"role":"user","content":"hi"}]}`)
if code != http.StatusMethodNotAllowed || !strings.Contains(reply, provider.BlockedHint) || strings.Contains(reply, "<") {
t.Fatalf("blocked reply: %d %s", code, reply)
}
if tr := s.trace.routes[len(s.trace.routes)-1].Tries[0]; !strings.HasSuffix(tr.Error, " — "+provider.BlockedHint) || strings.Contains(tr.Error, "<") {
t.Fatalf("trace: %+v", tr)
}
// in a group: the next member answers
if err := provider.SaveGroup(provider.Group{Name: "Z", Members: []string{"za/m", "zb/m"}, Routing: provider.Ordered}); err != nil {
t.Fatal(err)
}
s = New()
code, reply = postAs(t, s, "", `{"model":"group/z","messages":[{"role":"user","content":"hi"}]}`)
if code != http.StatusOK || !strings.Contains(reply, "from b") || other != 1 {
t.Fatalf("group reply: %d %s (next asked %d)", code, reply, other)
}
tries := s.trace.routes[len(s.trace.routes)-1].Tries
if len(tries) != 2 || tries[0].Rest == nil || !strings.Contains(tries[0].Error, provider.BlockedHint) {
t.Fatalf("tries: %+v", tries)
}
}