zcode: the API format the user picks for magpie's provider in ZCode's settings stays theirs; a sync no longer puts Anthropic Messages back on every start (杰多夫 on Discord)
What changed: zcodeRules wrote api.type "anthropic-messages" into provider_config.json, and zcodeProviderJSON wrote kind "anthropic" into config.json, on every sync (magpie's start, any catalog change). A user who switched the provider to OpenAI Chat Completions or Responses in ZCode had it switched back the next time magpie started. zcodeFormat now reads the format already there and keeps it, in both files: provider_config.json's api.type (ZCode 3.14 on) or config.json's kind (older ZCode), the one written last where they differ, mapped as ZCode maps them (anthropic-messages/anthropic, openai-chat-completions/openai-compatible, openai-responses/openai). A new provider, or a format ZCode doesn't have, is Anthropic Messages as before. Why it's safe: magpie's gateway answers all three at the same base address. Checked with ZCode 0.16.9's own agent CLI (glm/zcode.cjs -p, run from a copy under a sandbox HOME, never ~/.zcode) against a sandbox gateway with the DeepSeek provider: anthropic-messages asks /v1/messages, openai-chat-completions /chat/completions, openai-responses /responses, and a tool-using turn (Read a.txt) answered 200 on each, to deepseek-flash. What was run: - TestZCodeKeepsUserFormat (new): a format picked in provider_config.json, for each of the three, survives two syncs and is carried to config.json's kind; one picked in config.json is carried to provider_config.json; an unknown one isn't kept. On the old zcode.go it fails: "picked openai-chat-completions: kind "anthropic", api "anthropic-messages"". - Sandbox, real binaries: provider_config.json set to openai-responses, then `magpie zcode magpie`: the old build wrote anthropic-messages back, this one kept openai-responses (config.json kind openai). Then openai-chat-completions, resynced, and the real ZCode CLI ran a turn on the file magpie wrote, served over /chat/completions. - gofmt -l; GOOS=linux and GOOS=windows go vet -tags nogui ./internal/agent; go test -tags nogui ./internal/agent under a temp HOME: ok. The "bcww: Field required" 400 on Anthropic Messages is not part of this commit: ZCode's real requests (captured from its CLI, plain and with web search) to deepseek/deepseek-flash through magpie returned 200, and DeepSeek answers schema errors in serde's words, not "Field required".
This commit is contained in:
1 parent
eb035e29e1
commit
d9bf7c1ccf
2 files changed
+176
-2
No files matched your search
+75
-2
@@ -45,6 +45,12 @@ import (
|
||||
//
|
||||
// magpie writes both files, so an older ZCode sees its models too. A model
|
||||
// the user set by hand in ZCode (a manual rule) keeps what they set.
|
||||
//
|
||||
// magpie's gateway answers all three of ZCode's API formats at the same
|
||||
// address (/v1/messages, /chat/completions, /responses), so the format the
|
||||
// user picked in ZCode's provider settings is theirs: a sync keeps it in both
|
||||
// files (zcodeFormat), and only a provider magpie adds new starts on
|
||||
// Anthropic Messages.
|
||||
|
||||
func zcode(home string) *Agent {
|
||||
dir := filepath.Join(home, ".zcode")
|
||||
@@ -137,7 +143,7 @@ func zcodeProviderJSON(path string, keep bool) any {
|
||||
if keep {
|
||||
base, token = zcodeKept(filepath.Dir(path))
|
||||
}
|
||||
return map[string]any{"name": "magpie", "kind": "anthropic", "enabled": on, "source": "custom",
|
||||
return map[string]any{"name": "magpie", "kind": zcodeKinds[zcodeFormat(filepath.Dir(path))], "enabled": on, "source": "custom",
|
||||
"options": map[string]any{"apiKey": token, "baseURL": base}, "models": ms}
|
||||
}
|
||||
|
||||
@@ -215,6 +221,73 @@ func zcodeKept(dir string) (string, string) {
|
||||
return zcodeAddress(was, wasKey)
|
||||
}
|
||||
|
||||
// zcodeKinds are ZCode's API formats in provider_config.json (api.type) and
|
||||
// the kind each is in config.json, as ZCode maps one to the other.
|
||||
var zcodeKinds = map[string]string{
|
||||
"anthropic-messages": "anthropic",
|
||||
"openai-chat-completions": "openai-compatible",
|
||||
"openai-responses": "openai",
|
||||
}
|
||||
|
||||
// zcodeFormat is the API format magpie's provider has in ZCode's files in
|
||||
// dir: the one the user picked in ZCode's settings, else Anthropic Messages.
|
||||
// magpie writes both files together, so where they differ the user changed
|
||||
// one of them in ZCode (provider_config.json from 3.14 on, config.json
|
||||
// before): the one written last has their choice, which is carried to the
|
||||
// other.
|
||||
func zcodeFormat(dir string) string {
|
||||
rules, kind := zcodeRuleFormat(dir), ""
|
||||
path := filepath.Join(dir, "config.json")
|
||||
if k, _ := edit.GetJSON(path, "provider."+magpieID+".kind"); k != "" {
|
||||
for f, fk := range zcodeKinds {
|
||||
if fk == k {
|
||||
kind = f
|
||||
}
|
||||
}
|
||||
}
|
||||
switch {
|
||||
case kind == "" && rules == "":
|
||||
return "anthropic-messages"
|
||||
case kind == "":
|
||||
return rules
|
||||
case rules == "" || rules == kind:
|
||||
return kind
|
||||
}
|
||||
a, errA := os.Stat(filepath.Join(dir, "provider_config.json"))
|
||||
b, errB := os.Stat(path)
|
||||
if errA == nil && errB == nil && b.ModTime().After(a.ModTime()) {
|
||||
return kind
|
||||
}
|
||||
return rules
|
||||
}
|
||||
|
||||
// zcodeRuleFormat is magpie's provider's API format in provider_config.json
|
||||
// in dir, or "" when it has none ZCode knows.
|
||||
func zcodeRuleFormat(dir string) string {
|
||||
var r struct {
|
||||
Config struct {
|
||||
ProviderConfigRules struct {
|
||||
ProviderRules []struct {
|
||||
ProviderID string `json:"providerId"`
|
||||
Config struct {
|
||||
API struct {
|
||||
Type string `json:"type"`
|
||||
} `json:"api"`
|
||||
} `json:"config"`
|
||||
} `json:"providerRules"`
|
||||
} `json:"providerConfigRules"`
|
||||
} `json:"config"`
|
||||
}
|
||||
b, _ := os.ReadFile(filepath.Join(dir, "provider_config.json"))
|
||||
json.Unmarshal(b, &r)
|
||||
for _, p := range r.Config.ProviderConfigRules.ProviderRules {
|
||||
if _, ok := zcodeKinds[p.Config.API.Type]; ok && p.ProviderID == magpieID {
|
||||
return p.Config.API.Type
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// onAnotherMachine says base is an http(s) address whose host isn't this
|
||||
// machine's loopback.
|
||||
func onAnotherMachine(base string) bool {
|
||||
@@ -378,7 +451,7 @@ func zcodeRules(path string, on, keep bool) error {
|
||||
"config": map[string]any{
|
||||
"group": "standard-personal",
|
||||
"access": map[string]any{"type": "api-key", "apiKey": token},
|
||||
"api": map[string]any{"type": "anthropic-messages", "baseUrl": base},
|
||||
"api": map[string]any{"type": zcodeFormat(filepath.Dir(path)), "baseUrl": base},
|
||||
"personalModelIds": ids, "modelOrder": ids,
|
||||
}}
|
||||
// turned off in ZCode, it stays off
|
||||
|
||||
@@ -6,8 +6,10 @@ import (
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/yetone/magpie/internal/catalog"
|
||||
"github.com/yetone/magpie/internal/edit"
|
||||
"github.com/yetone/magpie/internal/gateway"
|
||||
"github.com/yetone/magpie/internal/provider"
|
||||
)
|
||||
@@ -390,3 +392,102 @@ func TestZCodeKeepsRemoteAddress(t *testing.T) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// The API format the user picks for magpie's provider in ZCode's settings
|
||||
// stays theirs: magpie answers all three, and a sync put Anthropic Messages
|
||||
// back on every start (杰多夫 on Discord). A change made in either file is
|
||||
// carried to the other; a provider magpie adds new starts on Anthropic.
|
||||
func TestZCodeKeepsUserFormat(t *testing.T) {
|
||||
home := t.TempDir()
|
||||
t.Setenv("HOME", home)
|
||||
t.Setenv("USERPROFILE", 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)
|
||||
}
|
||||
dir := filepath.Join(home, ".zcode", "v2")
|
||||
path, rules := filepath.Join(dir, "config.json"), filepath.Join(dir, "provider_config.json")
|
||||
os.MkdirAll(dir, 0o755)
|
||||
// what ZCode's own settings do: write the file, after magpie's write
|
||||
edited := func(p string, change func(doc map[string]any)) {
|
||||
b, _ := os.ReadFile(p)
|
||||
doc := map[string]any{}
|
||||
if err := json.Unmarshal(b, &doc); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
change(doc)
|
||||
b, _ = json.Marshal(doc)
|
||||
os.WriteFile(p, b, 0o600)
|
||||
later := time.Now().Add(time.Minute)
|
||||
os.Chtimes(p, later, later)
|
||||
}
|
||||
rule := func(doc map[string]any) map[string]any {
|
||||
for _, r := range doc["config"].(map[string]any)["providerConfigRules"].(map[string]any)["providerRules"].([]any) {
|
||||
if r := r.(map[string]any); r["providerId"] == "magpie" {
|
||||
return r["config"].(map[string]any)["api"].(map[string]any)
|
||||
}
|
||||
}
|
||||
t.Fatal("no magpie rule")
|
||||
return nil
|
||||
}
|
||||
formats := func() (kind, api string) {
|
||||
k, _ := edit.GetJSON(path, "provider.magpie.kind")
|
||||
b, _ := os.ReadFile(rules)
|
||||
doc := map[string]any{}
|
||||
json.Unmarshal(b, &doc)
|
||||
r := rule(doc)
|
||||
if r["baseUrl"] == "" {
|
||||
t.Fatalf("no address: %s", b)
|
||||
}
|
||||
api, _ = r["type"].(string)
|
||||
return k, api
|
||||
}
|
||||
a := zcode(home)
|
||||
if err := a.Field("provider").Set("magpie"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if k, api := formats(); k != "anthropic" || api != "anthropic-messages" {
|
||||
t.Fatalf("new provider: %q %q", k, api)
|
||||
}
|
||||
|
||||
// ZCode 3.14 on writes provider_config.json
|
||||
for _, c := range []struct{ api, kind string }{
|
||||
{"openai-chat-completions", "openai-compatible"},
|
||||
{"openai-responses", "openai"},
|
||||
{"anthropic-messages", "anthropic"},
|
||||
} {
|
||||
edited(rules, func(doc map[string]any) { rule(doc)["type"] = c.api })
|
||||
for range 2 { // and on the next start
|
||||
if err := a.Sync(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if k, api := formats(); k != c.kind || api != c.api {
|
||||
t.Fatalf("picked %s: kind %q, api %q", c.api, k, api)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// an older ZCode writes config.json
|
||||
edited(path, func(doc map[string]any) {
|
||||
doc["provider"].(map[string]any)["magpie"].(map[string]any)["kind"] = "openai-compatible"
|
||||
})
|
||||
later := time.Now().Add(2 * time.Minute) // past rules' edit above
|
||||
os.Chtimes(path, later, later)
|
||||
if err := a.Sync(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if k, api := formats(); k != "openai-compatible" || api != "openai-chat-completions" {
|
||||
t.Fatalf("config.json's kind: %q %q", k, api)
|
||||
}
|
||||
|
||||
// a format ZCode doesn't have isn't kept
|
||||
edited(rules, func(doc map[string]any) { rule(doc)["type"] = "grpc" })
|
||||
edited(path, func(doc map[string]any) { doc["provider"].(map[string]any)["magpie"].(map[string]any)["kind"] = "grpc" })
|
||||
if err := a.Sync(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if k, api := formats(); k != "anthropic" || api != "anthropic-messages" {
|
||||
t.Fatalf("unknown format: %q %q", k, api)
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user