Files
magpie/internal/provider/zcode_client_test.go
T
yetone 901fd87ec0 zcode: a Start Plan request goes as the OpenCode plugin ARNO sent sends it, the one zcode.z.ai still serves (ARNO on Discord: "0.1.633版本还是不行,我给的插件还是成功的") — 11be3f31 followed ZCode 3.14.3's CLI source and some Start Plan requests were still turned away with 405 / code 3012 "request has been blocked due to unusual activity", while ARNO's plugin ("Freeflow for OpenCode and magpie", provider zcode-start), which ARNO sent us, works on the same machine and account. Where the plugin and 11be3f31 differ, the plugin now wins: the stable system block is the identity section and then the "# ZCode Desktop Context" section joined by "\n\n" (11be3f31 had dropped the desktop section; it is back from 31568cb0's text as zcode_prompt.json's desktop); the powered-by line names zai-api / bigmodel-api (was account:zai-start-plan / account:bigmodel-start-plan); the environment's working directory is the one the agent's own prompt names (Claude Code's "Primary working directory:", Codex's <cwd>), else the home directory (was "unknown"), and its shell is the basename of $SHELL or %ComSpec% (cmd.exe on Windows, was "cmd"); the date reminder is a user turn of its own before the agent's turns, always, its tags hugging the text ("<system-reminder>As you answer…task.</system-reminder>", no newlines; was put into the first user turn with newlines, and left out when that turn already had a <system-reminder>, as Claude Code's always does); every cache mark in the turns goes and the last block of the last turn gets one (a string turn becomes a text block), where the agent's last mark used to be kept wherever it was; the agent's system goes as plain text blocks (non-text blocks and empty text dropped); metadata.user_id is {"device_id":<this machine's device id>,"account_uuid":"","session_id":""} as JSON, the rest of metadata kept; X-Os-Version is "<platform> <release> <arch>" (was the release alone); X-Client-Timezone is always sent (TZ, /etc/localtime, else a zone at the local offset, Asia/Shanghai at +8); X-Client-Language falls back to the Mac's AppleLocale or Windows' display language, then en-US as Node's Intl does, so magpie opened from Finder with no LANG no longer says en-US for a zh-CN user; HTTP-Referer is https://zcode.z.ai as written; anthropic-version is 2023-06-01 and the agent's anthropic-beta (claude-code-20250219…) is no longer passed on. Unchanged and already the plugin's: the opening line block, the three cached blocks, User-Agent ZCode/3.14.3 ai-sdk/anthropic/3.0.81, X-ZCode-App-Version, X-Title Z Code@cli, X-ZCode-Agent glm, X-Release-Channel production, X-Platform, X-Os-Category, fresh x-request-id and x-zcode-trace-id, x-zcode-session-type main, the JWT as a Bearer, no x-api-key or X-Device-Mid, tools without cache marks. The prompt texts' SHA-256 (first 8 bytes) all match the plugin's: cliPrefix 46dd360a22c87a92, stableSections[0] 3f21ff9a88a03a76, [1] 39a1e86c93452c01, joined 49bda31511fc9670, beforeEnvironment bbdc66b399d297ff, afterEnvironment 1732b7f098a925d7. The agent's thinking and other body fields go as sent. No request was made to zcode.z.ai. TestZCodeStartPromptIsThePlugins (the hashes) and TestZCodeStartSentAsTheApp (headers, the exact environment section, the context prefix turn, the cache marks, metadata.user_id, BigModel's provider, a string last turn) fail without the change. go vet ./internal/..., go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui ./... pass.
2026-10-02 15:11:32 +08:00

268 lines
11 KiB
Go

package provider
import (
"bytes"
"context"
"crypto/sha256"
"encoding/hex"
"encoding/json"
"io"
"net/http"
"regexp"
"strings"
"testing"
"time"
)
// A request as Claude Code sends it: its own system blocks and tools, each
// cached, its working directory named, cached turns, metadata of its own,
// and a number too long for a float.
const zcodeAgentBody = `{"model":"GLM-5.3-Flash","max_tokens":32000,"stream":true,"metadata":{"user_id":"claude-user","x":1},` +
`"system":[{"type":"text","text":"You are Claude Code.","cache_control":{"type":"ephemeral"}},{"type":"text","text":"Be brief & <exact>.\n - Primary working directory: /work/proj\n","cache_control":{"type":"ephemeral","ttl":"1h"}}],` +
`"tools":[{"name":"Read","input_schema":{"type":"object"},"cache_control":{"type":"ephemeral"}}],` +
`"messages":[{"role":"user","content":"hello"},` +
`{"role":"assistant","content":[{"type":"tool_use","id":"t1","name":"Read","input":{"n":12345678901234567890}}]},` +
`{"role":"user","content":[{"type":"tool_result","tool_use_id":"t1","content":"ok","cache_control":{"type":"ephemeral"}},{"type":"text","text":"go on"}]}]}`
// countCached counts the cache breakpoints anywhere in v.
func countCached(v any) int {
n := 0
switch x := v.(type) {
case map[string]any:
if x["cache_control"] != nil {
n++
}
for _, y := range x {
n += countCached(y)
}
case []any:
for _, y := range x {
n += countCached(y)
}
}
return n
}
// zcodeHash is the spec's hash of a text: SHA-256 over its UTF-8, the
// first 8 bytes in hex.
func zcodeHash(s string) string {
h := sha256.Sum256([]byte(s))
return hex.EncodeToString(h[:8])
}
// ZCode's prompt is the text of the plugin the Start Plan serves, byte for
// byte: the hashes of its strings, computed over the plugin's own.
func TestZCodeStartPromptIsThePlugins(t *testing.T) {
for name, c := range map[string]struct{ text, want string }{
"cliPrefix": {zcodePrompt.Prefix, "46dd360a22c87a92"},
"stableSections[0]": {zcodePrompt.Stable, "3f21ff9a88a03a76"},
"stableSections[1]": {zcodePrompt.Desktop, "39a1e86c93452c01"},
"stableSections.join": {zcodeStable(), "49bda31511fc9670"},
"dynamicSections.before": {zcodePrompt.BeforeEnvironment, "bbdc66b399d297ff"},
"dynamicSections.afterEnviron": {zcodePrompt.AfterEnvironment, "1732b7f098a925d7"},
} {
if got := zcodeHash(c.text); got != c.want {
t.Errorf("%s: %s, want %s", name, got, c.want)
}
}
}
// ZCode's Start Plan turns away a request without ZCode's own system
// prompt as unusual activity (#425, 405 / 3012), and dressed as the desktop
// app or as ZCode's CLI source has it, some still were (miaopasi and ARNO
// on Discord: "0.1.633版本还是不行,我给的插件还是成功的"). A Start Plan
// request reaches zcode.z.ai as the plugin ARNO sent sends it: its three
// cached system blocks (the desktop context in the second) before the
// agent's own uncached text, the context prefix as a turn of its own, one
// cache mark in the turns on the last block of the last, none on tools,
// metadata.user_id naming the device, and the plugin's headers, the token
// as a Bearer only, no X-Device-Mid or anthropic-beta; the rest of the
// request as the agent sent it. A Coding Plan account's request goes as it
// is.
func TestZCodeStartSentAsTheApp(t *testing.T) {
signIn(t)
t.Setenv("LANG", "zh_CN.UTF-8")
t.Setenv("LC_ALL", "")
t.Setenv("LC_MESSAGES", "")
t.Setenv("TZ", "Asia/Shanghai")
t.Setenv("SHELL", "/bin/zsh")
jwt := zcodeTestJWT(time.Now().Add(24 * time.Hour))
u := newZCodeStartUpstream(t, jwt)
u.balance = zcodeActiveStart(time.Now(), "active")
send := func(k zcodeKey, body string) *http.Request {
t.Helper()
p := zcodeProvider("a@example.com", "", k)
req, _ := http.NewRequest("POST", p.Anthropic+"/v1/messages?beta=true", strings.NewReader(body))
req.Header.Set("User-Agent", "magpie/test")
req.Header.Set("anthropic-beta", "claude-code-20250219,interleaved-thinking-2025-05-14")
if err := p.Sign(context.Background(), req, Anthropic, []byte(body)); err != nil {
t.Fatal(err)
}
return req
}
req := send(zcodeKey{Base: ZCodeZaiBase, JWT: jwt}, zcodeAgentBody)
res, err := http.DefaultClient.Do(req)
if err != nil || res.StatusCode != 200 {
t.Fatalf("sent: %v %v", err, res)
}
res.Body.Close()
h := u.model.Header
osVersion := zcodePlatform() + " " + zcodeOSRelease() + " " + zcodeArch()
category := map[string]string{"darwin": "macos", "win32": "windows"}[zcodePlatform()]
if category == "" {
category = "linux"
}
for k, want := range map[string]string{
"User-Agent": "ZCode/3.14.3 ai-sdk/anthropic/3.0.81", "X-ZCode-App-Version": "3.14.3", "anthropic-version": "2023-06-01",
"X-Title": "Z Code@cli", "X-ZCode-Agent": "glm", "X-ZCode-Session-Type": "main", "X-Release-Channel": "production",
"X-Client-Language": "zh-CN", "X-Client-Timezone": "Asia/Shanghai", "HTTP-Referer": "https://zcode.z.ai", "Authorization": "Bearer " + jwt,
"X-Platform": zcodePlatform() + "-" + zcodeArch(), "X-Os-Category": category, "X-Os-Version": osVersion,
} {
if got := h.Get(k); got != want {
t.Errorf("%s: %q, want %q", k, got, want)
}
}
uuidRe := regexp.MustCompile(`^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$`)
for _, k := range []string{"X-Request-Id", "X-ZCode-Trace-Id"} {
if !uuidRe.MatchString(h.Get(k)) {
t.Errorf("%s: %q", k, h.Get(k))
}
}
for _, k := range []string{"X-Session-Id", "X-Query-Id", "X-Device-Mid", "X-Api-Key", "anthropic-beta"} {
if h.Get(k) != "" {
t.Errorf("%s sent", k)
}
}
if bytes.Contains(u.body, []byte("\\u00"+"3c")) || !bytes.Contains(u.body, []byte(`12345678901234567890`)) {
t.Fatalf("body not kept as written: %s", u.body)
}
type block struct {
Type string `json:"type"`
Text string `json:"text"`
Cache map[string]any `json:"cache_control"`
}
var got struct {
Model string `json:"model"`
MaxTokens float64 `json:"max_tokens"`
Stream bool `json:"stream"`
Metadata map[string]any `json:"metadata"`
System []block `json:"system"`
Messages []struct {
Role string `json:"role"`
Content json.RawMessage `json:"content"`
} `json:"messages"`
}
if err := json.Unmarshal(u.body, &got); err != nil {
t.Fatal(err)
}
if got.Model != "GLM-5.3-Flash" || got.MaxTokens != 32000 || !got.Stream || len(got.Messages) != 4 {
t.Fatalf("the request changed: %s", u.body)
}
env := strings.Join([]string{
"# Environment",
"You have been invoked in the following environment:",
"- Primary working directory: /work/proj",
"- Is a git repository: no",
"- Platform: " + zcodePlatform(),
"- Shell: zsh",
"- OS Version: " + osVersion,
"- You are powered by the model named zai-api/GLM-5.3-Flash.",
}, "\n")
if len(got.System) != 5 ||
got.System[0].Text != "You are ZCode, an interactive coding agent" ||
zcodeHash(got.System[1].Text) != "49bda31511fc9670" ||
got.System[2].Text != "\n\n"+zcodePrompt.BeforeEnvironment+"\n\n"+env+"\n\n"+zcodePrompt.AfterEnvironment ||
zcodeHash(zcodePrompt.BeforeEnvironment) != "bbdc66b399d297ff" || zcodeHash(zcodePrompt.AfterEnvironment) != "1732b7f098a925d7" {
t.Fatalf("ZCode's system prompt: %s", u.body)
}
for i, s := range got.System {
if cached := s.Cache != nil; cached != (i < 3) || cached && (len(s.Cache) != 1 || s.Cache["type"] != "ephemeral") {
t.Errorf("system block %d cache: %v", i, s.Cache)
}
}
if got.System[3].Text != "You are Claude Code." || !strings.HasPrefix(got.System[4].Text, "Be brief & <exact>.") {
t.Fatalf("the agent's system prompt: %+v", got.System[3:])
}
var prefix []block
json.Unmarshal(got.Messages[0].Content, &prefix)
want := "<system-reminder>As you answer the user's questions, you can use the following context:\n# currentDate\nToday's date is " +
time.Now().Format("2006-01-02") + ".\n\n IMPORTANT: this context may or may not be relevant to your tasks. You should not respond to this context unless it is highly relevant to your task.</system-reminder>"
if got.Messages[0].Role != "user" || len(prefix) != 1 || prefix[0].Type != "text" || prefix[0].Text != want || prefix[0].Cache != nil {
t.Fatalf("the context prefix: %s", got.Messages[0].Content)
}
if got.Messages[1].Role != "user" || string(got.Messages[1].Content) != `"hello"` {
t.Fatalf("the first turn: %s", got.Messages[1].Content)
}
var all any
json.Unmarshal(u.body, &all)
if n := countCached(all); n != 4 {
t.Fatalf("%d cache breakpoints: %s", n, u.body)
}
last := all.(map[string]any)["messages"].([]any)[3].(map[string]any)["content"].([]any)
if last[0].(map[string]any)["cache_control"] != nil || last[1].(map[string]any)["cache_control"] == nil {
t.Fatalf("the last turn's breakpoint: %v", last)
}
if tools := all.(map[string]any)["tools"].([]any); tools[0].(map[string]any)["cache_control"] != nil {
t.Fatalf("a tool's cache mark kept: %v", tools)
}
var uid map[string]any
if got.Metadata["x"] != float64(1) || json.Unmarshal([]byte(got.Metadata["user_id"].(string)), &uid) != nil ||
got.Metadata["user_id"] != `{"device_id":"`+zcodeDeviceMid()+`","account_uuid":"","session_id":""}` {
t.Fatalf("metadata: %v", got.Metadata)
}
// shaped once only; a string system, a string last turn, BigModel
again := zcodeStartBody(u.body, "zai-api", time.Now())
if !bytes.Equal(again, u.body) {
t.Fatalf("shaped twice:\n%s\n%s", u.body, again)
}
own := `{"model":"GLM-5.2","system":"mine","messages":[{"role":"user","content":[{"type":"text","text":"<system-reminder>x</system-reminder>","cache_control":{"type":"ephemeral"}}]},{"role":"assistant","content":"ok"},{"role":"user","content":"hi"}]}`
var o struct {
System []map[string]any `json:"system"`
Messages []struct {
Content any `json:"content"`
} `json:"messages"`
}
json.Unmarshal(zcodeStartBody([]byte(own), zcodeStartProvider(ZCodeBigModelBase), time.Now()), &o)
if len(o.System) != 4 || o.System[3]["text"] != "mine" || o.System[3]["cache_control"] != nil || len(o.Messages) != 4 ||
!strings.Contains(o.System[2]["text"].(string), "- You are powered by the model named bigmodel-api/GLM-5.2.") {
t.Fatalf("a string system: %+v", o)
}
if b, _ := json.Marshal(o.Messages[3].Content); string(b) != `[{"cache_control":{"type":"ephemeral"},"text":"hi","type":"text"}]` {
t.Fatalf("a string last turn: %s", b)
}
if b, _ := json.Marshal(o.Messages[1].Content); strings.Contains(string(b), "cache_control") {
t.Fatalf("an earlier turn's cache mark kept: %s", b)
}
// what isn't a messages request is left alone
for _, b := range []string{`not json`, `{"model":"GLM-5.2"}`} {
if got := zcodeStartBody([]byte(b), "zai-api", time.Now()); string(got) != b {
t.Fatalf("%s became %s", b, got)
}
}
// the GLM Coding Plan: the agent's request as it was, and none of the app's headers
u.plans = []any{map[string]any{"productName": "GLM Coding Lite", "status": "VALID"}}
zcodeRoutes.Lock()
zcodeRoutes.m = map[string]zcodeRoute{}
zcodeRoutes.Unlock()
req = send(zcodeKey{Key: "key.secret", Base: ZCodeZaiBase, JWT: jwt}, zcodeAgentBody)
if !strings.HasPrefix(req.URL.Path, "/api/anthropic/") {
t.Fatalf("coding plan went to %s", req.URL)
}
b, _ := io.ReadAll(req.Body)
if string(b) != zcodeAgentBody {
t.Fatalf("coding plan body changed: %s", b)
}
for _, k := range []string{"X-ZCode-Agent", "X-ZCode-Trace-Id", "X-Title", "X-Client-Language"} {
if req.Header.Get(k) != "" {
t.Errorf("coding plan sent %s", k)
}
}
if req.Header.Get("User-Agent") != "magpie/test" {
t.Errorf("coding plan User-Agent: %q", req.Header.Get("User-Agent"))
}
}