328 lines
11 KiB
Go
328 lines
11 KiB
Go
package provider
|
|
|
|
// PLUGIN-SERVED (see AGENTS.md): ZCode ("zcode") is a deprecated built-in
|
|
// subscription served by its plugin, @magpie-community/opencode-zcode-auth,
|
|
// once moved onto it (provider.Moved; the default for a new sign-in). A
|
|
// moved one's sign-ins, models, requests and usage are all the plugin's,
|
|
// never this code's (only the move, in migrate*.go, still reads its
|
|
// accounts). A fix here alone doesn't reach those users; fix the plugin
|
|
// (github.com/magpie-community/plugins, packages/zcode) and raise the
|
|
// mover's min in internal/provider/migrate_zcode.go.
|
|
|
|
// 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, 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 (
|
|
"bytes"
|
|
_ "embed"
|
|
"encoding/json"
|
|
"os"
|
|
"path/filepath"
|
|
"regexp"
|
|
"runtime"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
//go:embed zcode_prompt.json
|
|
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 {
|
|
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 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 "bigmodel-api"
|
|
}
|
|
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, cwd being where the agent runs.
|
|
func zcodeEnvironment(provider, model, cwd string) string {
|
|
e := zcodePrompt.Environment
|
|
lines := []string{
|
|
e.Heading,
|
|
e.InvokedLine,
|
|
"- " + e.CwdLabel + ": " + cwd,
|
|
"- " + e.GitLabel + ": " + e.GitNo,
|
|
"- " + e.PlatformLabel + ": " + zcodePlatform(),
|
|
"- " + e.ShellLabel + ": " + zcodeShell(),
|
|
"- " + e.OSVersionLabel + ": " + zcodeOSVersion(),
|
|
}
|
|
if model != "" {
|
|
lines = append(lines, strings.NewReplacer("{provider}", provider, "{model}", model).Replace(e.PoweredByLine))
|
|
}
|
|
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, 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, cwd), zcodePrompt.AfterEnvironment}, "\n\n")
|
|
return []any{cached(zcodePrompt.Prefix), cached(zcodeStable()), cached("\n\n" + dynamic)}
|
|
}
|
|
|
|
// 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{"role": "user", "content": []any{map[string]any{"type": "text", "text": "<system-reminder>" + text + "</system-reminder>"}}}
|
|
}
|
|
|
|
// 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 {
|
|
return body
|
|
}
|
|
var model string
|
|
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, &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
|
|
}
|
|
}
|
|
|
|
var msgs []map[string]any
|
|
if zcodeDecode(m["messages"], &msgs) != nil {
|
|
return body
|
|
}
|
|
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)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
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
|
|
}
|
|
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 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)
|
|
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)
|
|
}
|
|
}
|
|
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 {
|
|
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
|
|
}
|