agent(zcode): ZCode is an agent — its provider field puts every magpie model in ZCode's own model picker, as a provider named magpie in ~/.zcode/v2/config.json (kind anthropic, asked at the gateway's /v1/messages), kept in step with the catalog like OpenCode's list, and taken out again when cleared; its requests are known by the ZCode/ User-Agent (#72)

This commit is contained in:
yetone committed 2026-09-25 23:53:59 +08:00
1 parent 1dfbc10af8
commit c0cbf90eeb
10 files changed
+162 -4

No files matched your search

+1
View File
@@ -79,6 +79,7 @@ and there is a terminal version (`magpie tui`) and a plain CLI.
| Devin | `~/.config/devin/config.json` (`%APPDATA%\devin\config.json` on Windows) | model |
| Hermes Agent | `~/.hermes/config.yaml` (`$HERMES_HOME`) | model |
| Grok Build | `~/.grok/config.toml` (`$GROK_HOME`) | model, effort |
| ZCode | `~/.zcode/v2/config.json` | provider (magpie's models in ZCode's picker) |
Provider-scoped agents (OpenCode, Pi, Goose, Crush, omp, Hermes Agent) take `provider/model`.
Only agents that are installed or configured are shown.
+1
View File
@@ -64,6 +64,7 @@ func All() []*Agent {
devin(home, cfg),
hermes(home),
grok(home),
zcode(home),
}
}
+74
View File
@@ -0,0 +1,74 @@
package agent
import (
"path/filepath"
"github.com/yetone/magpie/internal/edit"
"github.com/yetone/magpie/internal/gateway"
)
// ZCode (Zhipu's desktop app) keeps its model providers in
// ~/.zcode/v2/config.json, OpenCode's provider shape with a kind of its own:
//
// {"provider":{"magpie":{"name":"magpie","kind":"anthropic",
// "options":{"apiKey":"magpie","baseURL":"http://127.0.0.1:3425"},
// "enabled":true,"source":"custom",
// "models":{"<id>":{"name":…,"limit":{"context":…},"modalities":{…}}}}}}
//
// An anthropic provider is asked at baseURL + /v1/messages. The model is
// picked per task in ZCode's own picker and kept in its window, not in a
// file, so what magpie sets is whether its models are in that picker.
func zcode(home string) *Agent {
dir := filepath.Join(home, ".zcode")
path := filepath.Join(dir, "v2", "config.json")
key := "provider." + magpieID
return &Agent{
ID: "zcode", Name: "ZCode", Icon: "zcode", Aliases: []string{"z-code"},
UA: []string{"zcode"},
Dir: dir, Path: path,
Notice: func() string {
if Running(`ZCode\.app/`, `(^|/)ZCode( |$)`) {
return "ZCode reads its providers at start-up — restart ZCode to see magpie's models in its picker."
}
return ""
},
Sync: func() error { return syncJSON(path, key, zcodeProviderJSON) },
Fields: []Field{{
Key: "provider", Label: "provider",
Get: func() string {
if _, ok := edit.GetJSON(path, key); ok {
return magpieID
}
return ""
},
Set: func(v string) error {
if v == "" {
return edit.DelJSON(path, key)
}
return edit.SetJSON(path, edit.KV{Path: key, Value: zcodeProviderJSON()})
},
Options: func(map[string]string) []Option {
return []Option{{Value: magpieID, Label: "magpie", Icon: "magpie", Note: "every magpie model in ZCode's picker"}}
},
}},
}
}
func zcodeProviderJSON() any {
ms := map[string]any{}
for _, m := range magpieModels() {
window := m.Context
if window == 0 {
window = 200000
}
in := []string{"text"}
if m.Images {
in = append(in, "image")
}
ms[m.ID] = map[string]any{"name": m.Name, "limit": map[string]any{"context": window},
"modalities": map[string]any{"input": in, "output": []string{"text"}}}
}
return map[string]any{"name": "magpie", "kind": "anthropic", "enabled": true, "source": "custom",
"options": map[string]any{"apiKey": gateway.Token, "baseURL": gateway.URL()}, "models": ms}
}
+81
View File
@@ -0,0 +1,81 @@
package agent
import (
"encoding/json"
"os"
"path/filepath"
"testing"
"github.com/yetone/magpie/internal/provider"
)
func TestZCode(t *testing.T) {
home := t.TempDir()
t.Setenv("HOME", home)
t.Setenv("XDG_CONFIG_HOME", filepath.Join(home, ".config"))
t.Setenv("XDG_CACHE_HOME", filepath.Join(home, ".cache"))
if err := provider.Save(provider.Provider{ID: "deepseek", Name: "DeepSeek", Chat: "https://api.deepseek.com/v1", Key: "k", Models: []string{"pro"}}); err != nil {
t.Fatal(err)
}
path := filepath.Join(home, ".zcode", "v2", "config.json")
os.MkdirAll(filepath.Dir(path), 0o755)
os.WriteFile(path, []byte(`{"provider":{"builtin:bigmodel":{"name":"Bigmodel","kind":"anthropic","enabled":true}}}`), 0o644)
read := func() map[string]any {
var c struct {
Provider map[string]map[string]any `json:"provider"`
}
b, _ := os.ReadFile(path)
if err := json.Unmarshal(b, &c); err != nil {
t.Fatalf("%v\n%s", err, b)
}
if c.Provider["builtin:bigmodel"] == nil {
t.Fatalf("ZCode's own provider went: %s", b)
}
return c.Provider["magpie"]
}
a := zcode(home)
if !a.Detected() {
t.Fatal("not detected")
}
f := a.Field("provider")
if f.Get() != "" {
t.Fatalf("get: %q", f.Get())
}
if err := f.Set("magpie"); err != nil {
t.Fatal(err)
}
m := read()
opts, _ := m["options"].(map[string]any)
models, _ := m["models"].(map[string]any)
pro, _ := models["deepseek/pro"].(map[string]any)
if m["kind"] != "anthropic" || m["enabled"] != true || m["source"] != "custom" || opts["apiKey"] != "magpie" ||
opts["baseURL"] == "" || pro == nil || pro["limit"] == nil || pro["modalities"] == nil {
t.Fatalf("magpie provider: %v", m)
}
if f.Get() != "magpie" {
t.Fatalf("get: %q", f.Get())
}
// a provider added later reaches ZCode's picker
if err := provider.Save(provider.Provider{ID: "kimi", Name: "Kimi", Chat: "https://api.moonshot.cn/v1", Key: "k", Models: []string{"k2"}}); err != nil {
t.Fatal(err)
}
if err := a.Sync(); err != nil {
t.Fatal(err)
}
if models, _ := read()["models"].(map[string]any); models["kimi/k2"] == nil {
t.Fatalf("not synced: %v", models)
}
if err := f.Set(""); err != nil {
t.Fatal(err)
}
if read() != nil || f.Get() != "" {
t.Fatal("magpie provider left behind")
}
// nothing to sync into a config that has no magpie provider
if err := a.Sync(); err != nil || read() != nil {
t.Fatal("sync added the provider")
}
}
+2 -2
View File
@@ -98,9 +98,9 @@ function icon(name) {
return e;
}
if (name) {
if (name.endsWith("-color") || name === "crush") {
if (name.endsWith("-color") || name === "crush" || name === "zcode") {
const img = el("img");
img.src = `icons/${name}.${name === "crush" ? "png" : "svg"}`;
img.src = `icons/${name}.${name === "crush" || name === "zcode" ? "png" : "svg"}`;
img.alt = "";
img.draggable = false;
e.append(img);
Binary file not shown.

After

Width:  |  Height:  |  Size: 8.0 KiB

+1 -1
View File
@@ -114,7 +114,7 @@
<p>Keys live in <code>~/.config/magpie/providers.json</code>, readable by you alone. magpie never reads keys from your shell environment: what you add is what it uses.</p>
<h2 id="agents"><a class="anchor" href="#agents">2 · Pick a model for each agent</a></h2>
<p>The <span class="ui">Agents</span> tab has a row for each agent installed or configured on this machine. magpie knows Claude Code, Codex, Gemini CLI, OpenCode, Pi, Goose, Cursor, Copilot CLI, Crush, DeepSeek Harness, Command Code, omp, Devin, Hermes Agent and Grok Build.</p>
<p>The <span class="ui">Agents</span> tab has a row for each agent installed or configured on this machine. magpie knows Claude Code, Codex, Gemini CLI, OpenCode, Pi, Goose, Cursor, Copilot CLI, Crush, DeepSeek Harness, Command Code, omp, Devin, Hermes Agent, Grok Build and ZCode.</p>
<figure class="figure">
<img class="for-light" src="/img/agents-light.png" width="2080" height="1058" loading="lazy" alt="The Agents tab: one row per agent with the model it uses, and saved profiles at the bottom">
+1 -1
View File
@@ -114,7 +114,7 @@
<p>密钥保存在 <code>~/.config/magpie/providers.json</code>,只有你自己可读。magpie 从不读取 shell 环境变量里的密钥:你添加什么,它就用什么。</p>
<h2 id="agents"><a class="anchor" href="#agents">2 · 给每个 Agent 选模型</a></h2>
<p><span class="ui">Agent</span> 标签页里,本机装了或配置过的每个 Agent 各占一行。magpie 支持 Claude Code、Codex、Gemini CLI、OpenCode、Pi、Goose、Cursor、Copilot CLI、Crush、DeepSeek Harness、Command Code、omp、Devin、Hermes Agent 和 Grok Build。</p>
<p><span class="ui">Agent</span> 标签页里,本机装了或配置过的每个 Agent 各占一行。magpie 支持 Claude Code、Codex、Gemini CLI、OpenCode、Pi、Goose、Cursor、Copilot CLI、Crush、DeepSeek Harness、Command Code、omp、Devin、Hermes Agent、Grok Build 和 ZCode。</p>
<figure class="figure">
<img class="for-light" src="/img/agents-zh-light.png" width="2080" height="1058" loading="lazy" alt="Agent 页:每个 Agent 一行,显示当前模型,底部是保存的方案">
Binary file not shown.

After

Width:  |  Height:  |  Size: 8.0 KiB

+1
View File
@@ -406,6 +406,7 @@ footer a:hover { color: var(--ink); }
<li><img class="bird" src="/icons/devin.svg" alt="">Devin</li>
<li><img class="bird" src="/icons/hermes.svg" alt="">Hermes Agent</li>
<li><img class="bird" src="/icons/xai.svg" alt="">Grok Build</li>
<li><img src="/icons/zcode.png" alt="">ZCode</li>
</ul>
<p style="margin-top:34px">Every model</p>
<ul>