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.
This commit is contained in:
1 parent
7b4065b12a
commit
901fd87ec0
6 files changed
+337
-152
No files matched your search
@@ -3,17 +3,17 @@ package provider
|
||||
// ZCode's Start Plan serves only what looks like ZCode's own request: one
|
||||
// without ZCode's system prompt is turned away with 405 "request has been
|
||||
// blocked due to unusual activity", code 3012 (#425), and one dressed as
|
||||
// the desktop app still was for some, where what the ZCode CLI sends got
|
||||
// through. So a Start Plan request goes as the ZCode 3.14.3 CLI sends it:
|
||||
// its system prompt first, three blocks (its opening line; its identity,
|
||||
// without the desktop context only the app adds; "\n\n" and its dynamic
|
||||
// sections around the environment), each cached, the agent's own system
|
||||
// prompt after them; the day in a <system-reminder> before the first user
|
||||
// turn; and the CLI's headers (zcodeSourceHeaders). The text
|
||||
// (zcode_prompt.json) and how it is put together are ZCode's own source
|
||||
// (github.com/zai-org/ZCode, apps/zcode-cli/packages/core/src/context).
|
||||
// The GLM Coding Plan, and every other provider, get the agent's request
|
||||
// as it is.
|
||||
// the desktop app, or as ZCode 3.14.3's CLI source has it, still was for
|
||||
// some, while the OpenCode plugin ARNO sent ("Freeflow", provider
|
||||
// zcode-start) got through on the same account. So a Start Plan request
|
||||
// goes as that plugin sends it: three cached system blocks (its opening
|
||||
// line; its identity and harness, then the desktop context; "\n\n" and its
|
||||
// dynamic sections around the environment), the agent's own system text
|
||||
// after them uncached; the day in a <system-reminder> as a user turn of
|
||||
// its own before the agent's; one cache mark in the turns, on the last;
|
||||
// metadata.user_id naming the device; and its headers
|
||||
// (zcodeSourceHeaders). The text is zcode_prompt.json. The GLM Coding
|
||||
// Plan, and every other provider, get the agent's request as it is.
|
||||
|
||||
import (
|
||||
_ "embed"
|
||||
@@ -21,6 +21,7 @@ import (
|
||||
"encoding/json"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"runtime"
|
||||
"strings"
|
||||
"time"
|
||||
@@ -32,6 +33,7 @@ var zcodePromptJSON []byte
|
||||
var zcodePrompt = func() (p struct {
|
||||
Prefix string `json:"prefix"`
|
||||
Stable string `json:"stable"`
|
||||
Desktop string `json:"desktop"`
|
||||
BeforeEnvironment string `json:"beforeEnvironment"`
|
||||
AfterEnvironment string `json:"afterEnvironment"`
|
||||
Environment struct {
|
||||
@@ -87,33 +89,56 @@ func zcodeOSVersion() string {
|
||||
return strings.Join(append(parts, zcodeArch()), " ")
|
||||
}
|
||||
|
||||
// zcodeStartProvider is ZCode's id for the Start Plan of an account on
|
||||
// base, as the powered-by line names it.
|
||||
// zcodeStartProvider is the provider the powered-by line names for the
|
||||
// Start Plan of an account on base, as the plugin the Start Plan serves
|
||||
// names it: zai-api, or bigmodel-api on BigModel.
|
||||
func zcodeStartProvider(base string) string {
|
||||
if base == ZCodeBigModelBase {
|
||||
return "account:bigmodel-start-plan"
|
||||
return "bigmodel-api"
|
||||
}
|
||||
return "account:zai-start-plan"
|
||||
return "zai-api"
|
||||
}
|
||||
|
||||
// zcodeCwdRe finds the working directory an agent names in its own prompt
|
||||
// (Claude Code's "Primary working directory: …", Codex's <cwd>…</cwd>).
|
||||
var zcodeCwdRe = regexp.MustCompile(`(?m)(?:^\s*-?\s*(?:Primary working directory|Working directory):[ \t]*([^\r\n]+?)[ \t]*$|<cwd>([^<\r\n]+)</cwd>)`)
|
||||
|
||||
// zcodeCwd is the agent's working directory, as the environment section's
|
||||
// process.cwd(): the one its request names, else the user's home.
|
||||
func zcodeCwd(texts []string) string {
|
||||
for _, t := range texts {
|
||||
if m := zcodeCwdRe.FindStringSubmatch(t); m != nil {
|
||||
return strings.TrimSpace(m[1] + m[2])
|
||||
}
|
||||
}
|
||||
if h, err := os.UserHomeDir(); err == nil && h != "" {
|
||||
return h
|
||||
}
|
||||
return "unknown"
|
||||
}
|
||||
|
||||
// zcodeShell is the shell as the environment section names it: the
|
||||
// basename of $SHELL, or of %ComSpec% on Windows (cmd.exe).
|
||||
func zcodeShell() string {
|
||||
for _, k := range []string{"SHELL", "ComSpec"} {
|
||||
if s := os.Getenv(k); s != "" {
|
||||
return filepath.Base(s)
|
||||
}
|
||||
}
|
||||
return "unknown"
|
||||
}
|
||||
|
||||
// zcodeEnvironment is the prompt's environment section for model on
|
||||
// provider. Where the agent runs isn't magpie's to say, so that is left
|
||||
// unknown.
|
||||
func zcodeEnvironment(provider, model string) string {
|
||||
// provider, cwd being where the agent runs.
|
||||
func zcodeEnvironment(provider, model, cwd string) string {
|
||||
e := zcodePrompt.Environment
|
||||
shell := "unknown"
|
||||
if s := os.Getenv("SHELL"); s != "" {
|
||||
shell = filepath.Base(s)
|
||||
} else if runtime.GOOS == "windows" {
|
||||
shell = "cmd"
|
||||
}
|
||||
lines := []string{
|
||||
e.Heading,
|
||||
e.InvokedLine,
|
||||
"- " + e.CwdLabel + ": unknown",
|
||||
"- " + e.CwdLabel + ": " + cwd,
|
||||
"- " + e.GitLabel + ": " + e.GitNo,
|
||||
"- " + e.PlatformLabel + ": " + zcodePlatform(),
|
||||
"- " + e.ShellLabel + ": " + shell,
|
||||
"- " + e.ShellLabel + ": " + zcodeShell(),
|
||||
"- " + e.OSVersionLabel + ": " + zcodeOSVersion(),
|
||||
}
|
||||
if model != "" {
|
||||
@@ -122,30 +147,45 @@ func zcodeEnvironment(provider, model string) string {
|
||||
return strings.Join(lines, "\n")
|
||||
}
|
||||
|
||||
// zcodeStable is the stable system block: the identity and harness
|
||||
// section, then the desktop context, as the plugin joins them.
|
||||
func zcodeStable() string { return zcodePrompt.Stable + "\n\n" + zcodePrompt.Desktop }
|
||||
|
||||
// zcodeSystem is ZCode's three system blocks for model on provider.
|
||||
func zcodeSystem(provider, model string) []any {
|
||||
func zcodeSystem(provider, model, cwd string) []any {
|
||||
cached := func(text string) map[string]any {
|
||||
return map[string]any{"type": "text", "text": text, "cache_control": map[string]any{"type": "ephemeral"}}
|
||||
}
|
||||
dynamic := strings.Join([]string{zcodePrompt.BeforeEnvironment, zcodeEnvironment(provider, model), zcodePrompt.AfterEnvironment}, "\n\n")
|
||||
return []any{cached(zcodePrompt.Prefix), cached(zcodePrompt.Stable), cached("\n\n" + dynamic)}
|
||||
dynamic := strings.Join([]string{zcodePrompt.BeforeEnvironment, zcodeEnvironment(provider, model, cwd), zcodePrompt.AfterEnvironment}, "\n\n")
|
||||
return []any{cached(zcodePrompt.Prefix), cached(zcodeStable()), cached("\n\n" + dynamic)}
|
||||
}
|
||||
|
||||
// zcodeDateReminder is what ZCode puts before the first user turn, wrapped
|
||||
// as its context prefix is: each tag on a line of its own, a newline after.
|
||||
// zcodeDateReminder is the context prefix, a user turn of its own before
|
||||
// the agent's: the day in a <system-reminder>, the tags hugging the text.
|
||||
func zcodeDateReminder(now time.Time) map[string]any {
|
||||
c := zcodePrompt.Context
|
||||
text := strings.Join([]string{c.Intro, c.CurrentDateHeading + "\n" + strings.ReplaceAll(c.CurrentDateLine, "{date}", now.Format("2006-01-02")), "", c.Outro}, "\n")
|
||||
return map[string]any{"type": "text", "text": "<system-reminder>\n" + text + "\n</system-reminder>\n"}
|
||||
return map[string]any{"role": "user", "content": []any{map[string]any{"type": "text", "text": "<system-reminder>" + text + "</system-reminder>"}}}
|
||||
}
|
||||
|
||||
// zcodeStartBody is an Anthropic messages request as ZCode would send it
|
||||
// to the Start Plan, provider (zcodeStartProvider) serving it: ZCode's system blocks before the agent's, the date
|
||||
// before the first user turn when nothing is reminded there already. As
|
||||
// ZCode's blocks take three of the four cache breakpoints Anthropic's API
|
||||
// allows, the agent's are dropped from its system and tools, and only its
|
||||
// last in the messages is kept. A body that isn't one, or already starts
|
||||
// with ZCode's prompt, is left as it is.
|
||||
// zcodeUserID is metadata.user_id as the plugin sends it: this machine's
|
||||
// device id, no account or session, as JSON.
|
||||
func zcodeUserID() string {
|
||||
b, _ := json.Marshal(struct {
|
||||
DeviceID string `json:"device_id"`
|
||||
AccountUUID string `json:"account_uuid"`
|
||||
SessionID string `json:"session_id"`
|
||||
}{zcodeDeviceMid(), "", ""})
|
||||
return string(b)
|
||||
}
|
||||
|
||||
// zcodeStartBody is an Anthropic messages request as the plugin the Start
|
||||
// Plan serves sends it, provider (zcodeStartProvider) serving it: ZCode's
|
||||
// three cached system blocks, then the agent's own system text uncached;
|
||||
// the context prefix as the first turn; no cache mark in the turns but one
|
||||
// on the last block of the last; none on the tools; and metadata.user_id
|
||||
// naming the device. A body that isn't one, or already starts with
|
||||
// ZCode's prompt, is left as it is.
|
||||
func zcodeStartBody(body []byte, provider string, now time.Time) []byte {
|
||||
var m map[string]json.RawMessage
|
||||
if json.Unmarshal(body, &m) != nil || m["messages"] == nil {
|
||||
@@ -155,62 +195,77 @@ func zcodeStartBody(body []byte, provider string, now time.Time) []byte {
|
||||
json.Unmarshal(m["model"], &model)
|
||||
|
||||
var own []any
|
||||
var texts []string
|
||||
if raw := m["system"]; raw != nil {
|
||||
var s string
|
||||
var blocks []any
|
||||
if json.Unmarshal(raw, &s) == nil {
|
||||
if strings.TrimSpace(s) != "" {
|
||||
own = []any{map[string]any{"type": "text", "text": s}}
|
||||
texts = append(texts, s)
|
||||
}
|
||||
} else if zcodeDecode(raw, &own) != nil {
|
||||
} else if zcodeDecode(raw, &blocks) == nil {
|
||||
for _, b := range blocks {
|
||||
b, _ := b.(map[string]any)
|
||||
t, _ := b["text"].(string)
|
||||
if b["type"] == "text" && t != "" {
|
||||
if t == zcodePrompt.Prefix && len(own) == 0 {
|
||||
return body
|
||||
}
|
||||
own = append(own, map[string]any{"type": "text", "text": t})
|
||||
texts = append(texts, t)
|
||||
}
|
||||
}
|
||||
} else if string(raw) != "null" {
|
||||
return body
|
||||
}
|
||||
}
|
||||
if len(own) > 0 {
|
||||
if b, ok := own[0].(map[string]any); ok && b["text"] == zcodePrompt.Prefix {
|
||||
return body
|
||||
}
|
||||
}
|
||||
for _, b := range own {
|
||||
if b, ok := b.(map[string]any); ok {
|
||||
delete(b, "cache_control")
|
||||
}
|
||||
}
|
||||
system, _ := zcodeEncode(append(zcodeSystem(provider, model), own...))
|
||||
|
||||
var msgs []map[string]any
|
||||
if zcodeDecode(m["messages"], &msgs) != nil {
|
||||
return body
|
||||
}
|
||||
last := true
|
||||
for i := len(msgs) - 1; i >= 0; i-- {
|
||||
blocks, _ := msgs[i]["content"].([]any)
|
||||
for j := len(blocks) - 1; j >= 0; j-- {
|
||||
if b, ok := blocks[j].(map[string]any); ok && b["cache_control"] != nil {
|
||||
if !last {
|
||||
delete(b, "cache_control")
|
||||
if len(msgs) > 0 {
|
||||
switch c := msgs[0]["content"].(type) {
|
||||
case string:
|
||||
texts = append(texts, c)
|
||||
case []any:
|
||||
for _, b := range c {
|
||||
if b, ok := b.(map[string]any); ok {
|
||||
if t, ok := b["text"].(string); ok {
|
||||
texts = append(texts, t)
|
||||
}
|
||||
}
|
||||
last = false
|
||||
}
|
||||
}
|
||||
}
|
||||
if len(msgs) > 0 && msgs[0]["role"] == "user" {
|
||||
var blocks []any
|
||||
switch c := msgs[0]["content"].(type) {
|
||||
case string:
|
||||
blocks = []any{map[string]any{"type": "text", "text": c}}
|
||||
case []any:
|
||||
blocks = c
|
||||
system, _ := zcodeEncode(append(zcodeSystem(provider, model, zcodeCwd(texts)), own...))
|
||||
|
||||
msgs = append([]map[string]any{zcodeDateReminder(now)}, msgs...)
|
||||
lastAt := -1
|
||||
for i, msg := range msgs {
|
||||
if msg["role"] == "system" {
|
||||
continue
|
||||
}
|
||||
reminded := false
|
||||
for _, b := range blocks {
|
||||
if b, ok := b.(map[string]any); ok {
|
||||
if t, _ := b["text"].(string); b["type"] == "text" && strings.HasPrefix(t, "<system-reminder>") {
|
||||
reminded = true
|
||||
lastAt = i
|
||||
if blocks, ok := msg["content"].([]any); ok {
|
||||
for _, b := range blocks {
|
||||
if b, ok := b.(map[string]any); ok {
|
||||
delete(b, "cache_control")
|
||||
}
|
||||
}
|
||||
}
|
||||
if !reminded {
|
||||
msgs[0]["content"] = append([]any{zcodeDateReminder(now)}, blocks...)
|
||||
}
|
||||
if lastAt >= 0 {
|
||||
switch c := msgs[lastAt]["content"].(type) {
|
||||
case string:
|
||||
msgs[lastAt]["content"] = []any{map[string]any{"type": "text", "text": c, "cache_control": map[string]any{"type": "ephemeral"}}}
|
||||
case []any:
|
||||
if len(c) > 0 {
|
||||
if b, ok := c[len(c)-1].(map[string]any); ok {
|
||||
b["cache_control"] = map[string]any{"type": "ephemeral"}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
messages, err := zcodeEncode(msgs)
|
||||
@@ -227,6 +282,15 @@ func zcodeStartBody(body []byte, provider string, now time.Time) []byte {
|
||||
m["tools"], _ = zcodeEncode(tools)
|
||||
}
|
||||
}
|
||||
meta := map[string]any{}
|
||||
if raw := m["metadata"]; raw != nil {
|
||||
zcodeDecode(raw, &meta)
|
||||
if meta == nil {
|
||||
meta = map[string]any{}
|
||||
}
|
||||
}
|
||||
meta["user_id"] = zcodeUserID()
|
||||
m["metadata"], _ = zcodeEncode(meta)
|
||||
m["system"], m["messages"] = system, messages
|
||||
out, err := zcodeEncode(m)
|
||||
if err != nil {
|
||||
|
||||
@@ -3,22 +3,26 @@ 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, cached turns, and a number too long for a float.
|
||||
const zcodeAgentBody = `{"model":"GLM-5.3-Flash","max_tokens":32000,"stream":true,` +
|
||||
`"system":[{"type":"text","text":"You are Claude Code.","cache_control":{"type":"ephemeral"}},{"type":"text","text":"Be brief & <exact>.","cache_control":{"type":"ephemeral","ttl":"1h"}}],` +
|
||||
// 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","cache_control":{"type":"ephemeral"}}]}]}`
|
||||
`{"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 {
|
||||
@@ -39,13 +43,40 @@ func countCached(v any) int {
|
||||
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 some still were (miaopasi and ARNO on Discord). A Start Plan request
|
||||
// reaches zcode.z.ai as the ZCode CLI sends it: its three cached system
|
||||
// blocks (no desktop context) before the agent's own, the day reminded
|
||||
// before the first turn, at most four cache breakpoints, and the CLI's
|
||||
// headers, the token as a Bearer only and no X-Device-Mid; the rest of the
|
||||
// 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) {
|
||||
@@ -54,6 +85,7 @@ func TestZCodeStartSentAsTheApp(t *testing.T) {
|
||||
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")
|
||||
@@ -63,6 +95,7 @@ func TestZCodeStartSentAsTheApp(t *testing.T) {
|
||||
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)
|
||||
}
|
||||
@@ -76,22 +109,28 @@ func TestZCodeStartSentAsTheApp(t *testing.T) {
|
||||
}
|
||||
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/" + zcodeAppVersion + " ai-sdk/anthropic/3.0.81", "X-ZCode-App-Version": zcodeAppVersion,
|
||||
"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": zcodeAPI, "Authorization": "Bearer " + jwt,
|
||||
"X-Platform": zcodePlatform() + "-" + zcodeArch(), "X-Os-Version": zcodeOSRelease(),
|
||||
"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)
|
||||
}
|
||||
}
|
||||
for _, k := range []string{"X-Request-Id", "X-ZCode-Trace-Id", "X-Os-Category", "X-Os-Version"} {
|
||||
if h.Get(k) == "" {
|
||||
t.Errorf("no %s", k)
|
||||
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"} {
|
||||
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)
|
||||
}
|
||||
@@ -99,82 +138,107 @@ func TestZCodeStartSentAsTheApp(t *testing.T) {
|
||||
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"`
|
||||
System []struct {
|
||||
Text string `json:"text"`
|
||||
Cache map[string]any `json:"cache_control"`
|
||||
} `json:"system"`
|
||||
Messages []struct {
|
||||
Role string `json:"role"`
|
||||
Content []struct {
|
||||
Type string `json:"type"`
|
||||
Text string `json:"text"`
|
||||
} `json:"content"`
|
||||
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) != 3 {
|
||||
if got.Model != "GLM-5.3-Flash" || got.MaxTokens != 32000 || !got.Stream || len(got.Messages) != 4 {
|
||||
t.Fatalf("the request changed: %s", u.body)
|
||||
}
|
||||
if len(got.System) != 5 || got.System[0].Text != "You are ZCode, an interactive coding agent" ||
|
||||
!strings.HasPrefix(got.System[1].Text, "\nYou are an interactive ZCode agent that helps users with software engineering tasks.") ||
|
||||
!strings.HasSuffix(got.System[1].Text, "- Reference code as `file_path:line_number` — it's clickable.") ||
|
||||
strings.Contains(got.System[1].Text, "Desktop") ||
|
||||
!strings.HasPrefix(got.System[2].Text, "\n\n# Communicating with the user") ||
|
||||
!strings.Contains(got.System[2].Text, "\n# Environment\nYou have been invoked in the following environment:\n") ||
|
||||
!strings.Contains(got.System[2].Text, "\n- OS Version: "+zcodePlatform()+" "+zcodeOSRelease()+" "+zcodeArch()+"\n") ||
|
||||
!strings.Contains(got.System[2].Text, "- You are powered by the model named account:zai-start-plan/GLM-5.3-Flash.") ||
|
||||
!strings.HasSuffix(got.System[2].Text, "may have a different cause.") {
|
||||
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) {
|
||||
t.Errorf("system block %d cached: %v", i, cached)
|
||||
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." || got.System[4].Text != "Be brief & <exact>." {
|
||||
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:])
|
||||
}
|
||||
first := got.Messages[0].Content
|
||||
if len(first) != 2 || !strings.HasPrefix(first[0].Text, "<system-reminder>\nAs you answer the user's questions") ||
|
||||
!strings.HasSuffix(first[0].Text, "unless it is highly relevant to your task.\n</system-reminder>\n") ||
|
||||
!strings.Contains(first[0].Text, "# currentDate\nToday's date is "+time.Now().Format("2006-01-02")+".") || first[1].Text != "hello" {
|
||||
t.Fatalf("first turn: %+v", first)
|
||||
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)[2].(map[string]any)["content"].([]any)
|
||||
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)
|
||||
}
|
||||
|
||||
// a request already ZCode's, and one with a reminder of its own, keep it
|
||||
again := zcodeStartBody(u.body, "account:zai-start-plan", time.Now())
|
||||
// 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>"},{"type":"text","text":"hi"}]}]}`
|
||||
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 []map[string]any `json:"content"`
|
||||
Content any `json:"content"`
|
||||
} `json:"messages"`
|
||||
}
|
||||
json.Unmarshal(zcodeStartBody([]byte(own), "account:zai-start-plan", time.Now()), &o)
|
||||
if len(o.System) != 4 || o.System[3]["text"] != "mine" || len(o.Messages[0].Content) != 2 {
|
||||
t.Fatalf("a string system and its own reminder: %+v", o)
|
||||
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), "account:zai-start-plan", time.Now()); string(got) != b {
|
||||
if got := zcodeStartBody([]byte(b), "zai-api", time.Now()); string(got) != b {
|
||||
t.Fatalf("%s became %s", b, got)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,15 @@
|
||||
|
||||
package provider
|
||||
|
||||
import "golang.org/x/sys/unix"
|
||||
import (
|
||||
"runtime"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"golang.org/x/sys/unix"
|
||||
|
||||
"github.com/yetone/magpie/internal/proc"
|
||||
)
|
||||
|
||||
// zcodeOSRelease is the kernel's release, as Node's os.release() gives it
|
||||
// (25.2.0 on macOS 26), "" when it can't be told.
|
||||
@@ -13,3 +21,16 @@ func zcodeOSRelease() string {
|
||||
}
|
||||
return unix.ByteSliceToString(u.Release[:])
|
||||
}
|
||||
|
||||
// zcodeSystemLocale is the Mac's region (zh_CN), the locale a terminal is
|
||||
// given, for magpie opened from Finder with no LANG; "" elsewhere.
|
||||
var zcodeSystemLocale = sync.OnceValue(func() string {
|
||||
if runtime.GOOS != "darwin" {
|
||||
return ""
|
||||
}
|
||||
out, err := proc.Command("defaults", "read", "-g", "AppleLocale").Output()
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
return strings.TrimSpace(string(out))
|
||||
})
|
||||
@@ -2,6 +2,7 @@ package provider
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
"sync"
|
||||
|
||||
"golang.org/x/sys/windows"
|
||||
)
|
||||
@@ -12,3 +13,11 @@ func zcodeOSRelease() string {
|
||||
v := windows.RtlGetVersion()
|
||||
return strconv.Itoa(int(v.MajorVersion)) + "." + strconv.Itoa(int(v.MinorVersion)) + "." + strconv.Itoa(int(v.BuildNumber))
|
||||
}
|
||||
|
||||
// zcodeSystemLocale is the user's first display language (zh-CN).
|
||||
var zcodeSystemLocale = sync.OnceValue(func() string {
|
||||
if l, err := windows.GetUserPreferredUILanguages(windows.MUI_LANGUAGE_NAME); err == nil && len(l) > 0 {
|
||||
return l[0]
|
||||
}
|
||||
return ""
|
||||
})
|
||||
@@ -1,6 +1,7 @@
|
||||
{
|
||||
"prefix": "You are ZCode, an interactive coding agent",
|
||||
"stable": "\nYou are an interactive ZCode agent that helps users with software engineering tasks.\n\nIMPORTANT: Assist with authorized security testing, defensive security, CTF challenges, and educational contexts. Refuse requests for destructive techniques, DoS attacks, mass targeting, supply chain compromise, or detection evasion for malicious purposes. Dual-use security tools (C2 frameworks, credential testing, exploit development) require clear authorization context: pentesting engagements, CTF competitions, security research, or defensive use cases.\n\n# Harness\n- Text you output outside of tool use is displayed to the user as Github-flavored markdown in a terminal.\n- Tools run behind a user-selected permission mode; a denied call means the user declined it — adjust, don't retry verbatim.\n- The system may send updates, reminders, or modifications to rules via mid-conversation system turns. These are system-controlled, unlike function results. Hooks may intercept tool calls; treat hook output as user feedback.\n- Prefer the dedicated file/search tools over shell commands when one fits. Independent tool calls can run in parallel in one response.\n- Reference code as `file_path:line_number` — it's clickable.",
|
||||
"desktop": "# ZCode Desktop Context\n\n### Files & URLs\n- Return local web URLs as Markdown links (e.g., [label](http://127.0.0.1:8080)).\n- File should be an absolute path or include the workspace folder segment so it can be resolved relative to the workspace.\n- Unless otherwise specified, return local file references as Markdown links (e.g., [name.md](/absolute/path/to/name.md)).\n\n### Inline Code Comments\n- Use the ::code-comment{...} directive when you need to attach feedback directly to specific code lines.\n- Emit one directive per inline comment; emit none when there are no actionable inline comments.\n- Required attributes: title (short label), body (one-paragraph explanation), file (path to the file).\n- Optional attributes: start, end (1-based line numbers), priority (0-3).\n- file should be an absolute path or include the workspace folder segment so it can be resolved relative to the workspace.\n- Keep line ranges tight; end defaults to start.\n- Example: ::code-comment{title=\"[P2] Off-by-one\" body=\"Loop iterates past the end when length is 0.\" file=\"/path/to/foo.ts\" start=10 end=11 priority=2}",
|
||||
"beforeEnvironment": "# Communicating with the user\n\nYour text output is what the user reads; they usually can't see your thinking or the raw tool results. Write it for a teammate who stepped away and is catching up, not for a log file: they don't know the codenames or shorthand you created along the way, and they didn't watch your process unfold. Before your first tool call, say in a sentence what you're about to do; while working, give brief updates when you find something load-bearing or change direction.\n\nText you write between tool calls may not be shown to the user. Everything the user needs from this turn — answers, summaries, findings, conclusions, deliverables — must be in the final text message of your turn, with no tool calls after it. Keep text between tool calls to brief status notes. If something important appeared only mid-turn or in your thinking, restate it in that final message.\n\nLead with the outcome. Your first sentence after finishing should answer \"what happened\" or \"what did you find\" — the thing the user would ask for if they said \"just give me the TLDR.\" Supporting detail and reasoning come after, for readers who want them.\n\nBeing readable and being concise are different things, and readable matters more. If the user has to reread your summary or ask you to explain, any time saved by brevity is gone. The way to keep output short is to be selective about what you include (drop details that don't change what the reader would do next), not to compress the writing into fragments, abbreviations, arrow chains like `A → B → fails`, or jargon. What you do include, write in complete sentences with the technical terms spelled out. Don't make the reader cross-reference labels or numbering you invented earlier; say what you mean in place.\n\nMatch the response to the question: a simple question gets a direct answer in prose, not headers and sections. Use tables only for short enumerable facts, with explanations in the surrounding prose rather than the cells. Calibrate to the user — a bit tighter for an expert, more explanatory for someone newer.\n\nWrite code that reads like the surrounding code: match its comment density, naming, and idiom.\nOnly write a code comment to state a constraint the code itself can't show — never to say where it came from, what the next line does, or why your change is correct; that's you talking to the reviewer, not the next reader, and it's noise the moment the PR merges.\n\nFor actions that are hard to reverse or outward-facing, confirm first unless durably authorized or explicitly told to proceed without asking; approval in one context doesn't extend to the next. Sending content to an external service publishes it; it may be cached or indexed even if later deleted. Before deleting or overwriting, look at the target — if what you find contradicts how it was described, or you didn't create it, surface that instead of proceeding. Report outcomes faithfully: if tests fail, say so with the output; if a step was skipped, say that; when something is done and verified, state it plainly without hedging.",
|
||||
"afterEnvironment": "# Context management\nWhen the conversation grows long, some or all of the current context is summarized; the summary, along with any remaining unsummarized context, is provided in the next context window so work can continue — you don't need to wrap up early or hand off mid-task.\n\nWhen you have enough information to act, act. Do not re-derive facts already established in the conversation, re-litigate a decision the user has already made, or narrate options you will not pursue. If you are weighing a choice, give a recommendation, not an exhaustive survey\n\nYou are operating autonomously. The user is not watching in real time and cannot answer questions mid-task, so asking 'Want me to…?' or 'Shall I…?' will block the work. For reversible actions that follow from the original request, proceed without asking. Stop only for destructive actions or genuine scope changes the user must decide. Offering follow-ups after the task is done is fine; asking permission before doing the work is not.\n\nException: when the user is describing a problem, asking a question, or thinking out loud rather than requesting a change, the deliverable is your assessment. Report your findings and stop. Don't apply a fix until they ask for one.\n\nBefore ending your turn, check your last paragraph. If it is a plan, an analysis, a question, a list of next steps, or a promise about work you have not done ('I'll…', 'let me know when…'), do that work now with tool calls. That includes retrying after errors and gathering missing information yourself. Do not stop because the context or session is long. End your turn only when the task is complete or you are blocked on input only the user can provide.\n\nBefore running a command that changes system state — restarts, deletes, config edits — check that the evidence actually supports that specific action. A signal that pattern-matches to a known failure may have a different cause.",
|
||||
"environment": {
|
||||
|
||||
@@ -144,54 +144,66 @@ func zcodeRebase(req *http.Request, from, to string) {
|
||||
}
|
||||
}
|
||||
|
||||
// zcodeSourceHeaders are the headers the ZCode CLI names itself with on a
|
||||
// model request to the Start Plan (zcode_client.go), as its source has
|
||||
// them (apps/zcode-cli/packages/bootstrap/src/model-config.ts,
|
||||
// runtime-platform-headers.ts; adapters/src/model/runner-attribution.ts):
|
||||
// the CLI ("Z Code@cli", the app's own agent being "@electron") and its
|
||||
// SDK, its agent and release, the machine and its OS release, the
|
||||
// zcodeSourceHeaders are the headers the plugin the Start Plan serves sends
|
||||
// with a model request (zcode_client.go): ZCode's app version and SDK,
|
||||
// the CLI's title, its agent and release, the machine (X-Os-Version being
|
||||
// "<platform> <release> <arch>", as the environment section has it), the
|
||||
// language and time zone, a new request and trace id, and no query or
|
||||
// session id. No X-Device-Mid: ZCode sends that to its own APIs, never
|
||||
// with a model request.
|
||||
// session id, X-Device-Mid or anthropic-beta.
|
||||
func zcodeSourceHeaders(req *http.Request) {
|
||||
platform := zcodePlatform()
|
||||
category := map[string]string{"darwin": "macos", "win32": "windows"}[platform]
|
||||
if category == "" {
|
||||
category = "linux"
|
||||
}
|
||||
req.Header.Del("anthropic-beta")
|
||||
req.Header.Set("anthropic-version", "2023-06-01")
|
||||
req.Header.Set("User-Agent", "ZCode/"+zcodeAppVersion+" ai-sdk/anthropic/3.0.81")
|
||||
req.Header.Set("X-ZCode-App-Version", zcodeAppVersion)
|
||||
req.Header.Set("X-Title", "Z Code@cli")
|
||||
req.Header.Set("X-ZCode-Agent", "glm")
|
||||
req.Header.Set("HTTP-Referer", zcodeAPI)
|
||||
req.Header.Set("HTTP-Referer", "https://zcode.z.ai") // as the plugin has it, wherever zcodeAPI points
|
||||
req.Header.Set("X-Platform", platform+"-"+zcodeArch())
|
||||
req.Header.Set("X-Os-Category", category)
|
||||
if r := zcodeOSRelease(); r != "" {
|
||||
req.Header.Set("X-Os-Version", r)
|
||||
}
|
||||
req.Header.Set("X-Os-Version", zcodeOSVersion())
|
||||
req.Header.Set("X-Release-Channel", "production")
|
||||
req.Header.Set("X-Client-Language", zcodeLanguage())
|
||||
if tz := zcodeTimezone(); tz != "" {
|
||||
req.Header.Set("X-Client-Timezone", tz)
|
||||
}
|
||||
req.Header.Set("X-Client-Timezone", zcodeTimezone())
|
||||
req.Header.Set("X-Request-Id", randomUUID())
|
||||
req.Header.Set("X-ZCode-Session-Type", "main")
|
||||
req.Header.Set("X-ZCode-Trace-Id", randomUUID())
|
||||
}
|
||||
|
||||
// zcodeLanguage is the user's language as ZCode gives it (zh-CN, en-US).
|
||||
// zcodeLanguage is the user's locale as Node's Intl gives it (zh-CN,
|
||||
// en-US): the environment's, else the system's (zcodeSystemLocale), else
|
||||
// en-US, ICU's own default.
|
||||
func zcodeLanguage() string {
|
||||
for _, k := range []string{"LC_ALL", "LC_MESSAGES", "LANG"} {
|
||||
v, _, _ := strings.Cut(os.Getenv(k), ".")
|
||||
if v != "" && v != "C" && v != "POSIX" {
|
||||
return strings.ReplaceAll(v, "_", "-")
|
||||
if l := zcodeBCP47(os.Getenv(k)); l != "" {
|
||||
return l
|
||||
}
|
||||
}
|
||||
if l := zcodeBCP47(zcodeSystemLocale()); l != "" {
|
||||
return l
|
||||
}
|
||||
return "en-US"
|
||||
}
|
||||
|
||||
// zcodeTimezone is the IANA name of the local time zone, "" when it can't
|
||||
// be told.
|
||||
// zcodeBCP47 turns a POSIX locale (zh_CN.UTF-8, en_US@euro) into a BCP 47
|
||||
// tag (zh-CN), "" for none or C/POSIX.
|
||||
func zcodeBCP47(v string) string {
|
||||
v, _, _ = strings.Cut(v, ".")
|
||||
v, _, _ = strings.Cut(v, "@")
|
||||
v = strings.TrimSpace(v)
|
||||
if v == "" || v == "C" || v == "POSIX" {
|
||||
return ""
|
||||
}
|
||||
return strings.ReplaceAll(v, "_", "-")
|
||||
}
|
||||
|
||||
// zcodeTimezone is the IANA name of the local time zone, as Node's Intl
|
||||
// gives it: TZ, the zone /etc/localtime links to, else one at the local
|
||||
// offset (Asia/Shanghai at +8, Etc/GMT∓n otherwise, UTC at 0).
|
||||
func zcodeTimezone() string {
|
||||
if tz := os.Getenv("TZ"); tz != "" && !strings.HasPrefix(tz, ":") {
|
||||
return tz
|
||||
@@ -201,7 +213,21 @@ func zcodeTimezone() string {
|
||||
return name
|
||||
}
|
||||
}
|
||||
return ""
|
||||
if name := time.Local.String(); name != "Local" && name != "" && strings.Contains(name, "/") {
|
||||
return name
|
||||
}
|
||||
_, off := time.Now().Zone()
|
||||
switch {
|
||||
case off == 8*3600:
|
||||
return "Asia/Shanghai"
|
||||
case off == 0:
|
||||
return "UTC"
|
||||
case off%3600 == 0 && off > 0:
|
||||
return "Etc/GMT-" + strconv.Itoa(off/3600)
|
||||
case off%3600 == 0:
|
||||
return "Etc/GMT+" + strconv.Itoa(-off/3600)
|
||||
}
|
||||
return "UTC"
|
||||
}
|
||||
|
||||
// zcodeStartRequest makes req, a request to the Start Plan, ZCode's own:
|
||||
|
||||
Reference in new issue
Block a user