Files
magpie/internal/provider/zcode_client.go
T
yetone 11be3f31b0 zcode: a Start Plan request goes as the ZCode CLI sends it, not the desktop app (miaopasi and ARNO on Discord: Start plan还是405 感觉不是IP的问题; an OpenCode plugin sending the CLI's request works on the same plan) — dressed as the desktop app, some Start Plan requests were still turned away with 405 "request has been blocked due to unusual activity", code 3012. Following ZCode 3.14.3's own source (github.com/zai-org/ZCode, apps/zcode-cli/packages/core/src/context/builder.ts and sections/, bootstrap/src/model-config.ts and runtime-platform-headers.ts, adapters/src/model/runner-attribution.ts), the request is now the CLI's: X-Title "Z Code@cli" (was "Z Code@electron", which only the desktop app's agent server sends); the stable system block is the identity section alone, the "ZCode Desktop Context" section being added only for presentationSurface zcode_desktop; X-Platform is <platform>-<arch> and the new X-Os-Version is os.release(), as runtime-platform-headers sets them (Darwin's uname release, Windows' Major.Minor.Build from RtlGetVersion); the environment block's OS Version is "<platform> <release> <arch>" as env-info.ts writes it (was "unknown") and its powered-by line is the provider id and model as ZCode names them (account:zai-start-plan/GLM-5.3-Flash, account:bigmodel-start-plan on BigModel; was lowercased without the provider); the currentDate reminder is wrapped as system-reminder/source.ts wraps a context prefix (a newline after the opening tag, one after the closing). Model requests no longer carry X-Device-Mid (ZCode sends it only to its own API, where the balance still needs it, #282) nor x-api-key: the JWT goes as a Bearer only, as the plugin that works sends it (the official client passes it as the SDK's apiKey too, so this follows the working plugin over the source). The prompt text itself was checked byte for byte against the 3.14.3 sections (rendered with node). The plugin's own source wasn't found (pi-freeflow has no zcode code), so where it and the source might differ (X-Os-Version as release alone, not "<platform> <release> <arch>") the source wins. No request was made to zcode.z.ai. TestZCodeStartSentAsTheApp, TestZCodeStartPlanOwnAccount and TestZCodeSignInStartPlanDeviceMid are updated; with the old prompt, X-Title and x-api-key they fail (X-Title "Z Code@electron", X-Api-Key sent, the desktop section in the system prompt). go vet ./internal/..., go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui ./internal/provider/ ./internal/gateway/ pass.
2026-10-02 13:17:26 +08:00

255 lines
7.9 KiB
Go

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.
import (
_ "embed"
"bytes"
"encoding/json"
"os"
"path/filepath"
"runtime"
"strings"
"time"
)
//go:embed zcode_prompt.json
var zcodePromptJSON []byte
var zcodePrompt = func() (p struct {
Prefix string `json:"prefix"`
Stable string `json:"stable"`
BeforeEnvironment string `json:"beforeEnvironment"`
AfterEnvironment string `json:"afterEnvironment"`
Environment struct {
Heading string `json:"heading"`
InvokedLine string `json:"invokedLine"`
CwdLabel string `json:"cwdLabel"`
GitLabel string `json:"gitLabel"`
GitNo string `json:"gitNo"`
PlatformLabel string `json:"platformLabel"`
ShellLabel string `json:"shellLabel"`
OSVersionLabel string `json:"osVersionLabel"`
PoweredByLine string `json:"poweredByLine"`
} `json:"environment"`
Context struct {
Intro string `json:"intro"`
Outro string `json:"outro"`
CurrentDateHeading string `json:"currentDateHeading"`
CurrentDateLine string `json:"currentDateLine"`
} `json:"context"`
}) {
if err := json.Unmarshal(zcodePromptJSON, &p); err != nil {
panic("zcode_prompt.json: " + err.Error())
}
return p
}()
// zcodePlatform is the platform as ZCode (Node's process.platform) names it.
func zcodePlatform() string {
if runtime.GOOS == "windows" {
return "win32"
}
return runtime.GOOS
}
// zcodeArch is the architecture as ZCode (Node's os.arch()) names it.
func zcodeArch() string {
switch runtime.GOARCH {
case "amd64":
return "x64"
case "386":
return "ia32"
}
return runtime.GOARCH
}
// zcodeOSVersion is the prompt's OS Version as ZCode gives it: "darwin
// 25.2.0 arm64".
func zcodeOSVersion() string {
parts := []string{zcodePlatform()}
if r := zcodeOSRelease(); r != "" {
parts = append(parts, r)
}
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.
func zcodeStartProvider(base string) string {
if base == ZCodeBigModelBase {
return "account:bigmodel-start-plan"
}
return "account:zai-start-plan"
}
// 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 {
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.GitLabel + ": " + e.GitNo,
"- " + e.PlatformLabel + ": " + zcodePlatform(),
"- " + e.ShellLabel + ": " + shell,
"- " + e.OSVersionLabel + ": " + zcodeOSVersion(),
}
if model != "" {
lines = append(lines, strings.NewReplacer("{provider}", provider, "{model}", model).Replace(e.PoweredByLine))
}
return strings.Join(lines, "\n")
}
// zcodeSystem is ZCode's three system blocks for model on provider.
func zcodeSystem(provider, model 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)}
}
// 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.
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"}
}
// 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.
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 {
return body
}
var model string
json.Unmarshal(m["model"], &model)
var own []any
if raw := m["system"]; raw != nil {
var s string
if json.Unmarshal(raw, &s) == nil {
if strings.TrimSpace(s) != "" {
own = []any{map[string]any{"type": "text", "text": s}}
}
} else if zcodeDecode(raw, &own) != nil {
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")
}
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
}
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
}
}
}
if !reminded {
msgs[0]["content"] = append([]any{zcodeDateReminder(now)}, blocks...)
}
}
messages, err := zcodeEncode(msgs)
if err != nil {
return body
}
if raw := m["tools"]; raw != nil {
var tools []map[string]any
if zcodeDecode(raw, &tools) == nil {
for _, t := range tools {
delete(t, "cache_control")
}
m["tools"], _ = zcodeEncode(tools)
}
}
m["system"], m["messages"] = system, messages
out, err := zcodeEncode(m)
if err != nil {
return body
}
return out
}
// zcodeDecode reads JSON keeping its numbers as they are written.
func zcodeDecode(raw []byte, v any) error {
d := json.NewDecoder(bytes.NewReader(raw))
d.UseNumber()
return d.Decode(v)
}
// zcodeEncode writes JSON leaving <, > and & as they are.
func zcodeEncode(v any) ([]byte, error) {
var b bytes.Buffer
e := json.NewEncoder(&b)
e.SetEscapeHTML(false)
if err := e.Encode(v); err != nil {
return nil, err
}
return bytes.TrimSuffix(b.Bytes(), []byte("\n")), nil
}