Files
magpie/internal/agent/sync.go
T
yetone 8323b0ae87 agent: OpenCode and OpenChamber list a magpie model's reasoning levels weakest first, not alphabetically (#713, Hu9956 on v0.1.780: 思考 offered 默认, High, Low, Max, Medium, Ultra, Xhigh, where other providers in the same OpenCode go by strength and MiniMax Code had magpie's in order) — openCodeVariants returned a map[string]any, which encoding/json writes with its keys sorted, so each model's variants object in opencode.json came out high, low, max, medium, ultra, xhigh, and OpenCode builds its variant list from the object in key order (checked with OpenCode 1.18.34: opencode models --verbose under a sandbox HOME/XDG dirs keeps a config's variants in the file's order, alphabetical or by strength; it doesn't sort them itself). openCodeVariants now returns an orderedJSON (a key/value list whose MarshalJSON writes keys as given) of the model's efforts put weakest first by gateway.ByStrength, the gateway's effortRank order (none, minimal, low, medium, high, xhigh, max, ultra; a level it doesn't know after them, stably), so a level appended after max (ultra) lands last too; edit.SetJSON keeps the custom marshaller's order when it indents the block. An opencode.json an older magpie wrote would have stayed alphabetical, as Sync's sameJSON compares decoded values and took it for the same block: OpenCode's (and its WSL and mimocode forms', through openCodeLike) and OpenChamber's Sync now use syncJSONInOrder, which also rewrites the block when its tokens, keys included, don't read in the same order as magpie's (sameOrder), and leaves one in order alone; the other agents' syncs are as before. TestOpenCodeVariants now checks the marshalled bytes (weakest first, an out-of-order list sorted, none as {}), and TestOpenCodeVariantsInOrder writes opencode.json under a temp HOME and checks the raw file lists low, medium, high, xhigh, max, ultra, that a block in the old alphabetical order is put in order by Sync and that a second Sync leaves the file byte for byte; both fail on the old code, and the Sync half fails with plain syncJSON. go vet, builds (darwin, GOOS=linux, GOOS=windows) and go test -tags nogui -count=1 ./... under a temp HOME pass. Not tried in OpenCode's or OpenChamber's own picker with the reporter's setup; OpenCode reads its config at start, so one already open shows the new order after it is reopened.
2026-10-04 03:19:37 +08:00

152 lines
3.7 KiB
Go

package agent
import (
"bytes"
"encoding/json"
"io"
"reflect"
"strings"
"sync"
"github.com/yetone/magpie/internal/edit"
"gopkg.in/yaml.v3"
)
// Most agents can't ask the gateway which models it has: magpie writes the
// catalog into a file of the agent's own (Pi's models.json, OpenCode's
// provider block, Codex's model catalog …) when a model is picked through
// magpie. That list was the catalog as it was then, so a provider added
// later never reached the agent, and one removed stayed listed. SyncCatalog
// rewrites each such list whenever the catalog may have changed.
var syncing struct {
sync.Mutex
running, again bool
}
// SyncCatalog brings every model list magpie wrote into an agent's files up
// to the catalog as it is now. It is what catalog.Changed is set to, so a
// provider saved or removed, or a vendor's list fetched anew, reaches the
// agents; one already running asks for another round rather than waiting.
// Errors are left for the picker: a file magpie can't write now is one the
// next change, or a pick, writes.
func SyncCatalog() {
syncing.Lock()
if syncing.running {
syncing.again = true
syncing.Unlock()
return
}
syncing.running = true
syncing.Unlock()
for {
for _, a := range All() {
if a.Sync != nil {
_ = a.Sync()
}
}
syncing.Lock()
if !syncing.again {
syncing.running = false
syncing.Unlock()
return
}
syncing.again = false
syncing.Unlock()
}
}
// syncJSON rewrites the value at key of a JSON file, if the file has one
// there and it differs: an agent that watches its files isn't told of a
// change that isn't one.
func syncJSON(path, key string, value func() any) error {
cur, ok := edit.GetJSON(path, key)
if !ok {
return nil
}
v := value()
if sameJSON(cur, v) {
return nil
}
return edit.SetJSON(path, edit.KV{Path: key, Value: v})
}
// syncJSONInOrder is syncJSON for a block whose keys' order the agent reads
// (OpenCode lists a model's variants in theirs): one that says the same in
// another order is rewritten too, as an older magpie wrote the variants
// alphabetically (#713).
func syncJSONInOrder(path, key string, value func() any) error {
cur, ok := edit.GetJSON(path, key)
if !ok {
return nil
}
v := value()
if sameJSON(cur, v) && sameOrder(cur, v) {
return nil
}
return edit.SetJSON(path, edit.KV{Path: key, Value: v})
}
// sameOrder reports whether raw JSON and what v marshals to read alike token
// by token, keys in the same order; whitespace and escapes aside.
func sameOrder(raw string, v any) bool {
b, err := json.Marshal(v)
if err != nil {
return false
}
x, y := json.NewDecoder(strings.NewReader(raw)), json.NewDecoder(bytes.NewReader(b))
x.UseNumber()
y.UseNumber()
for {
a, errA := x.Token()
c, errC := y.Token()
if errA != nil || errC != nil {
return errA == io.EOF && errC == io.EOF
}
if a != c {
return false
}
}
}
// sameJSON reports whether raw JSON says what v marshals to.
func sameJSON(raw string, v any) bool {
b, err := json.Marshal(v)
if err != nil {
return false
}
var x, y any
if json.Unmarshal([]byte(raw), &x) != nil || json.Unmarshal(b, &y) != nil {
return false
}
return reflect.DeepEqual(x, y)
}
// syncYAML is syncJSON for a YAML file.
func syncYAML(path, key string, value func() any) error {
raw, err := edit.Read(path)
if err != nil || raw == nil {
return nil
}
var doc any
if yaml.Unmarshal(raw, &doc) != nil {
return nil
}
for _, k := range strings.Split(key, ".") {
m, _ := doc.(map[string]any)
if doc = m[k]; doc == nil {
return nil
}
}
v := value()
b, err := yaml.Marshal(v)
if err != nil {
return err
}
var want any
if yaml.Unmarshal(b, &want) == nil && reflect.DeepEqual(doc, want) {
return nil
}
return edit.SetYAML(path, edit.KV{Path: key, Value: v})
}