zcode: what you choose in ZCode for magpie stays — the provider turned off stays off, its rule stays where you moved it, and a rule you removed isn't put back on the next sync. gateway: a model that always thinks (GLM-5.3 answers 1210 to thinking turned off) is asked again with thinking left to it, rather than failing. Asked on Discord
This commit is contained in:
5 files changed
+150
-7
No files matched your search
+24
-7
@@ -4,6 +4,7 @@ import (
|
||||
"encoding/json"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"slices"
|
||||
|
||||
"github.com/yetone/magpie/internal/edit"
|
||||
"github.com/yetone/magpie/internal/gateway"
|
||||
@@ -64,10 +65,14 @@ func zcode(home string) *Agent {
|
||||
if !wired() {
|
||||
return nil
|
||||
}
|
||||
if err := zcodeRules(rules, true); err != nil {
|
||||
return err
|
||||
// a ZCode that keeps its providers as rules, with magpie's taken
|
||||
// out there, had it removed in ZCode: it isn't put back
|
||||
if _, err := os.Stat(rules); err != nil || zcodeRuled(rules) {
|
||||
if err := zcodeRules(rules, true); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return edit.SetJSON(path, edit.KV{Path: key, Value: zcodeProviderJSON()})
|
||||
return edit.SetJSON(path, edit.KV{Path: key, Value: zcodeProviderJSON(path)})
|
||||
},
|
||||
Fields: []Field{{
|
||||
Key: "provider", Label: "provider",
|
||||
@@ -84,7 +89,7 @@ func zcode(home string) *Agent {
|
||||
if v == "" {
|
||||
return edit.DelJSON(path, key)
|
||||
}
|
||||
return edit.SetJSON(path, edit.KV{Path: key, Value: zcodeProviderJSON()})
|
||||
return edit.SetJSON(path, edit.KV{Path: key, Value: zcodeProviderJSON(path)})
|
||||
},
|
||||
Options: func(map[string]string) []Option {
|
||||
return []Option{{Value: magpieID, Label: "magpie", Icon: "magpie", Note: "every magpie model in ZCode's picker"}}
|
||||
@@ -93,7 +98,9 @@ func zcode(home string) *Agent {
|
||||
}
|
||||
}
|
||||
|
||||
func zcodeProviderJSON() any {
|
||||
// zcodeProviderJSON is magpie's provider in config.json at path; one
|
||||
// turned off in ZCode stays off.
|
||||
func zcodeProviderJSON(path string) any {
|
||||
ms := map[string]any{}
|
||||
for _, m := range magpieModels("zcode") {
|
||||
window := m.Context
|
||||
@@ -107,7 +114,11 @@ func zcodeProviderJSON() any {
|
||||
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",
|
||||
on := true
|
||||
if v, ok := edit.GetJSON(path, "provider."+magpieID+".enabled"); ok && v == "false" {
|
||||
on = false
|
||||
}
|
||||
return map[string]any{"name": "magpie", "kind": "anthropic", "enabled": on, "source": "custom",
|
||||
"options": map[string]any{"apiKey": gateway.Token, "baseURL": gateway.URL()}, "models": ms}
|
||||
}
|
||||
|
||||
@@ -161,9 +172,11 @@ func zcodeRules(path string, on bool) error {
|
||||
|
||||
var old map[string]any
|
||||
var providers []any
|
||||
at := -1 // where magpie's was, which it keeps
|
||||
for _, r := range zcodeList(pcr, "providerRules") {
|
||||
if mine(r) {
|
||||
old, _ = r.(map[string]any)
|
||||
at = len(providers)
|
||||
continue
|
||||
}
|
||||
providers = append(providers, r)
|
||||
@@ -213,7 +226,11 @@ func zcodeRules(path string, on bool) error {
|
||||
if e, ok := old["enabled"].(bool); ok {
|
||||
rule["enabled"] = e
|
||||
}
|
||||
providers = append(providers, rule)
|
||||
if at >= 0 {
|
||||
providers = slices.Insert(providers, at, any(rule))
|
||||
} else {
|
||||
providers = append(providers, rule)
|
||||
}
|
||||
}
|
||||
pcr["providerRules"] = zcodeNonNil(providers)
|
||||
mcr["providerModelRules"] = zcodeNonNil(models)
|
||||
|
||||
@@ -132,3 +132,62 @@ func TestZCode(t *testing.T) {
|
||||
t.Fatal("sync added the provider")
|
||||
}
|
||||
}
|
||||
|
||||
// What the user did in ZCode stays: magpie turned off stays off, in both
|
||||
// files; its rule stays where it is among theirs; and a rule they removed
|
||||
// isn't put back when magpie syncs.
|
||||
func TestZCodeKeepsUserChoices(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")
|
||||
rules := filepath.Join(home, ".zcode", "v2", "provider_config.json")
|
||||
os.MkdirAll(filepath.Dir(path), 0o755)
|
||||
os.WriteFile(path, []byte(`{"provider":{"magpie":{"name":"magpie","kind":"anthropic","enabled":false}}}`), 0o644)
|
||||
os.WriteFile(rules, []byte(`{"schemaVersion":1,"config":{"providerConfigRules":{"providerRules":[
|
||||
{"providerId":"a","config":{}},{"providerId":"magpie","enabled":false,"config":{}},{"providerId":"b","config":{}}]}}}`), 0o600)
|
||||
a := zcode(home)
|
||||
if err := a.Sync(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
ids := func() (out []string, on []any) {
|
||||
var d struct {
|
||||
Config struct {
|
||||
ProviderConfigRules struct {
|
||||
ProviderRules []map[string]any `json:"providerRules"`
|
||||
} `json:"providerConfigRules"`
|
||||
} `json:"config"`
|
||||
}
|
||||
b, _ := os.ReadFile(rules)
|
||||
json.Unmarshal(b, &d)
|
||||
for _, r := range d.Config.ProviderConfigRules.ProviderRules {
|
||||
out = append(out, r["providerId"].(string))
|
||||
on = append(on, r["enabled"])
|
||||
}
|
||||
return out, on
|
||||
}
|
||||
if got, on := ids(); len(got) != 3 || got[1] != "magpie" || on[1] != false {
|
||||
t.Fatalf("rules %v %v", got, on)
|
||||
}
|
||||
var c struct {
|
||||
Provider map[string]map[string]any `json:"provider"`
|
||||
}
|
||||
b, _ := os.ReadFile(path)
|
||||
json.Unmarshal(b, &c)
|
||||
if c.Provider["magpie"]["enabled"] != false || c.Provider["magpie"]["models"] == nil {
|
||||
t.Fatalf("config.json: %s", b)
|
||||
}
|
||||
|
||||
// removed in ZCode: not put back
|
||||
os.WriteFile(rules, []byte(`{"schemaVersion":1,"config":{"providerConfigRules":{"providerRules":[{"providerId":"a","config":{}}]}}}`), 0o600)
|
||||
if err := a.Sync(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if got, _ := ids(); len(got) != 1 {
|
||||
t.Fatalf("magpie put back: %v", got)
|
||||
}
|
||||
}
|
||||
@@ -154,6 +154,25 @@ func thinkingOffUnlessAsked(body []byte) []byte {
|
||||
return withFields(body, map[string]any{"thinking": map[string]any{"type": "disabled"}})
|
||||
}
|
||||
|
||||
// alwaysThinks is a vendor refusing to turn a model's thinking off: Z.ai's
|
||||
// GLM-5.3 answers 1210, "…always engages in thinking…".
|
||||
var alwaysThinks = regexp.MustCompile(`(?i)always engages in thinking|thinking (?:can ?not|can't) be (?:disabled|turned off)`)
|
||||
|
||||
// withoutThinkingOff is body with its thinking left to the model, when it
|
||||
// says thinking is off; false when it doesn't.
|
||||
func withoutThinkingOff(body []byte) ([]byte, bool) {
|
||||
var q map[string]json.RawMessage
|
||||
var th struct {
|
||||
Type string `json:"type"`
|
||||
}
|
||||
if json.Unmarshal(body, &q) != nil || json.Unmarshal(q["thinking"], &th) != nil || th.Type != "disabled" {
|
||||
return nil, false
|
||||
}
|
||||
delete(q, "thinking")
|
||||
out, err := json.Marshal(q)
|
||||
return out, err == nil
|
||||
}
|
||||
|
||||
// buildAnthropic renders a request for an Anthropic-style upstream.
|
||||
// claudeVersion finds the family's version in a Claude model id however a
|
||||
// relay spells it: claude-opus-4-6, claude-opus-5, anthropic.claude-sonnet-4.6-v1.
|
||||
|
||||
@@ -755,6 +755,18 @@ func (s *Server) passthrough(w http.ResponseWriter, r *http.Request, p provider.
|
||||
if err != nil {
|
||||
return writeError(w, proto, 502, p.Name+": "+err.Error()), err.Error(), true
|
||||
}
|
||||
if proto == provider.Anthropic && res.StatusCode == http.StatusBadRequest {
|
||||
// a model that always thinks refuses thinking turned off: asked
|
||||
// again with it left to the model
|
||||
b, _ := io.ReadAll(io.LimitReader(res.Body, 1<<20))
|
||||
res.Body.Close()
|
||||
res.Body = io.NopCloser(bytes.NewReader(b))
|
||||
if nb, ok := withoutThinkingOff(body); ok && alwaysThinks.Match(b) {
|
||||
if res, err = s.forward(r.Context(), p, proto, pathOf(proto), p.Prepare(nb), r.Header); err != nil {
|
||||
return writeError(w, proto, 502, p.Name+": "+err.Error()), err.Error(), true
|
||||
}
|
||||
}
|
||||
}
|
||||
defer res.Body.Close()
|
||||
if res.StatusCode >= 400 {
|
||||
b, _ := io.ReadAll(io.LimitReader(res.Body, 1<<20))
|
||||
|
||||
@@ -29,11 +29,23 @@ type fake struct {
|
||||
reply string // SSE body
|
||||
ctype string // "" means text/event-stream; "none" sends no header at all
|
||||
code int
|
||||
// refuse, when set, answers a request it gives a code for with it
|
||||
refuse func(body []byte) (code int, reply string)
|
||||
calls int
|
||||
}
|
||||
|
||||
func (f *fake) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
f.got, _ = io.ReadAll(r.Body)
|
||||
f.path, f.head = r.URL.Path, r.Header
|
||||
f.calls++
|
||||
if f.refuse != nil {
|
||||
if code, reply := f.refuse(f.got); code != 0 {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(code)
|
||||
io.WriteString(w, reply)
|
||||
return
|
||||
}
|
||||
}
|
||||
ct := f.ctype
|
||||
if ct == "" {
|
||||
ct = "text/event-stream"
|
||||
@@ -479,6 +491,30 @@ func TestAnthropicPassthroughTurnsThinkingOffUnlessAsked(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// GLM-5.3 always thinks and refuses thinking turned off (Z.ai's 1210):
|
||||
// the request magpie turned it off for is asked again with it left to the
|
||||
// model, once
|
||||
func TestAnthropicPassthroughModelThatAlwaysThinks(t *testing.T) {
|
||||
f := &fake{t: t, ctype: "application/json", reply: `{"id":"msg","type":"message","content":[]}`}
|
||||
f.refuse = func(b []byte) (int, string) {
|
||||
if bytes.Contains(b, []byte(`"disabled"`)) {
|
||||
return 400, `{"type":"error","error":{"type":"1210","message":"GLM-5.3 always engages in thinking; use low, high, or max"}}`
|
||||
}
|
||||
return 0, ""
|
||||
}
|
||||
setup(t, provider.Anthropic, f)
|
||||
code, body := post(t, "/v1/messages", `{"model":"m1","max_tokens":5,"messages":[{"role":"user","content":"title?"}]}`)
|
||||
if code != 200 || f.calls != 2 || bytes.Contains(f.got, []byte("thinking")) || !bytes.Contains(f.got, []byte(`"title?"`)) {
|
||||
t.Fatalf("%d %s after %d calls, last sent %s", code, body, f.calls, f.got)
|
||||
}
|
||||
// another 400 is the agent's to see, not asked again
|
||||
f.calls = 0
|
||||
f.refuse = func([]byte) (int, string) { return 400, `{"type":"error","error":{"message":"bad request"}}` }
|
||||
if code, _ := post(t, "/v1/messages", `{"model":"m1","max_tokens":5,"messages":[]}`); code != 400 || f.calls != 1 {
|
||||
t.Errorf("%d after %d calls", code, f.calls)
|
||||
}
|
||||
}
|
||||
|
||||
// effort without thinking asks for no reasoning on a Chat upstream
|
||||
func TestAnthropicEffortWithoutThinking(t *testing.T) {
|
||||
f := &fake{t: t, reply: sse(`data: {"id":"c1","choices":[{"delta":{"content":"T"},"finish_reason":"stop"}]}`, `data: [DONE]`)}
|
||||
|
||||
Reference in new issue
Block a user