支持 Ante:把 magpie 作为 provider 写进 ~/.ante/catalog.json (#1210)
* agent: Ante takes magpie as a provider in ~/.ante/catalog.json Ante (antigma.ai, AntigmaLabs/ante) is asked for in #1179. Its catalog is ~/.ante/catalog.json ($ANTE_HOME moves the folder), merged on top of the providers it ships: a providers map keyed by the id --provider takes, each with base_url, wire_style (AnthropicMessage, OpenAiCompatible, OpenAiResponse or Gemini), an optional auth, and preferred_models, the ids its model picker shows (docs.antigma.ai, reference/catalog-reference). magpie adds the entry "magpie" at the gateway's /v1 on OpenAiCompatible, which is the wire style Ante speaks Chat Completions with and what the gateway takes every model on, and lists magpie's models in its preferred_models. The user's own providers and the models section are left as they are, and magpie's entry goes again when magpie steps out; a file magpie made goes with it. No auth block is written. The gateway listens on loopback and lets any token in from this machine (gateway.Token's own comment, lan.go's identifyCaller: "Loopback takes any token"), and Ante counts a provider with no auth as authenticated. A bearer env_key would mean putting the gateway's key into Ante's environment, which the user would have to do themselves -- the same decision Empryo's entry makes, which sends no key of the user's either. A gateway shared on the local network is another matter, and the same one every agent magpie wires has. Two things are deliberately left off a model's entry. A routing group is not told what it reasons at: which member answers, and what that member takes, is the group's own to decide per turn, and magpieModels sets no APIs for a group for the same reason. And support_vision is never written: Ante takes a model as seeing images unless told otherwise, and what magpie knows is only as good as a provider's own list -- a relay's models are often not described at all, so writing false would have Ante drop the reader's images before the gateway, which describes them to a model that can't see them itself (provider.Described). No other agent magpie wires declares it either. Ante ships macOS and Linux builds alone and suggests WSL on Windows, so the Windows machine's Ante is usually the one in a distro: wslKinds gains it beside Empryo, which also means the gateway address Ante is pointed at is the one that distro reaches magpie on (place.base), and a distro not known to be in mirrored networking gets the notice it already gets for the others. TestAnte covers a user's own providers and models section kept as they are, magpie's entry added and taken away, Sync repointing it, and a file magpie made going again. TestAnteKeepsTheUsersFile covers a catalog magpie can't parse costing magpie's provider and not the user's. TestAnteGroupSaysNoCapab ilities covers a group declaring neither effort nor support_vision. TestAnteEfforts covers the ladder: Ante's has no "none" or "minimal", its two lowest, so both are its "min", and the rungs come out in Ante's order whatever order magpie lists them in. Tried with a real Ante 0.2.9 in WSL Ubuntu: magpie wrote the catalog, and `ante catalog` then read 16 providers with magpie's among them, 39 models, and Ante filled in support_vision: true for each itself. Ante then ran a real turn through the gateway — `ante --provider magpie --model group/auto-deepseek-v4-1-flash -p …` answered, and magpie's own usage log has the request (ep /v1/chat/completions, req the group's id, served by one of its members, out 10) — and so did a second model on another provider. The gateway was reached over the Windows host's address from the distro (172.17.128.1), this machine's own being on 127.0.0.1 alone, which is what notMirrored tells the user about. go vet, gofmt -l and go build (linux nogui, windows) pass. go test -tags nogui ./internal/agent: 12 failures here, the same 12 on unmodified main (the Windows ones AGENTS.md lists). ./internal/gui and ./internal/provider: TestGatewayFixQuitsOnlyAMagpie, TestPurposeAssetMatchesUsage, TestUpdateThatWontStart and TestLoginUsageClaudeAskedMeanwhile, all four on unmodified main here too. docs/reference.md's adapter table gains Ante. * agent: Ante keeps the user's own keys, reads ANTE_HOME, sends no effort it wasn't told to, and starts Ante on magpie (#1210) Seven points from the review, each with a test that fails without it. 1. ANTE_HOME was in the file's own comment and in docs/reference.md but never read: the path was fixed to $HOME/.ante, so a user who set it had magpie write a catalog Ante does not look at. anteDir reads it through place.getenv, the way MiniMax Code reads MINIMAX_DATA_DIR, so a distro's own ANTE_HOME - which magpie can't read - still means ~/.ante there. "ANTE_HOME" joins agentenv.Vars so a sandbox clears it. 2. providers.magpie was written whole, so every key of the user's that magpie doesn't write - the auth block a gateway shared on the LAN or reached from WSL needs to send a key, http_headers, extra_body, stream_idle_timeout_secs, thinking_display - went at the next sync, and an unchanged catalog was rewritten every round. It goes through theirsKept and syncJSON now, as #1103 did for the others: the user's keys in magpie's entry stay, preferred_models is magpie's list alone, and a file that says the same as it does is left as it is. 3. The two files were written in separate steps, so a change that failed halfway left Ante's catalog without magpie's entry and its settings still naming magpie. Each field's Set and its Sync are wrapped in atomic(), as empryo, minimax and kimi do. 4. The comment said Ante's "min" was the rung it sends no effort at, and folded both magpie's "none" and its "minimal" into it. Ante's reference says what min is: "OpenAI-compatible providers send the selected level as the same-named reasoning_effort (min is sent as minimal)" - so min is a sent level, and magpie's "none" has no rung of Ante's to fold into. Ante's "min" is magpie's "minimal"; a model magpie knows only "none" of comes out as the empty list, which is Ante's own way of saying it takes no effort setting at all. 5. The default was the highest rung the model takes, so every model magpie listed ran at Ante's slowest and dearest level. No effort is written: "Omit for models with no reasoning knob (Ante then sends no effort parameter and hides the effort slider)". 6. Having magpie in the catalog never meant Ante used it. What a session starts on is the provider and the model of ~/.ante/settings.json; with neither set Ante "uses the first authenticated provider in catalog order", and Ante's own presets come before magpie's entry - so while the user has any of those signed in, that is the one picked and magpie is not. (Measured: nothing named in settings, two keyless providers ordered aaa_other then magpie, and the request went to aaa_other's port; settings provider=magpie and it went to magpie's.) magpie sets that pair now and saves what it found there, so Disconnect hands it back; a provider the user picks in Ante's own /providers while magpie is wired is theirs and stands, and Check says so instead of keeping quiet. Ante's settings name the model apart from the provider, so magpie writes the catalog id it put in preferred_models, un-prefixed, and reads a bare id as magpie's only beside magpie's provider. Tests lean on the package's own testenv, not a real ~/.ante. TestAnteSyncKeepsTheUsersKeys is modelled on ad3b7fe5's TestAirProviderWritesKeepCustomFields and goes both ways - sync and pick - and wants an unchanged file to keep its mtime. Tried with a real Ante 0.2.9 in WSL Ubuntu on this machine (Ante ships macOS and Linux builds alone; there is no Windows Ante here). The strongest of these is the whole of it: magpie's own Go code - ante(home).Connect(), then the model field - wrote a catalog.json and settings.json into a temp folder, that folder was handed to the real Ante as ANTE_HOME, and Ante ran a turn through it with no --provider, no --model and nothing typed by hand. - `ante doctor` read `home ok /tmp/ante-e2e` and `16 providers (1 authed), 108 models` from ANTE_HOME, and magpie's entry came back with the base_url it was given: ANTE_HOME really moves the folder, and a base_url already ending in /v1 is not doubled (the request went to /v1/chat/completions). - What a session starts on is settings.json's provider, measured against two keyless providers each answering on its own port, so the logged port names the one Ante asked. Catalog ordered aaa_other then magpie, nothing named in settings: Ante asked 44331, aaa_other - the first authenticated in catalog order, exactly the review's "很可能选不到 magpie". settings provider=magpie: it asked 44220, magpie. settings provider=aaa_other: 44331 again, so a pick the user makes in Ante's own /providers overrules what magpie wrote, which is why stepping out leaves it rather than handing back the one magpie found. And with magpie the only authenticated provider, auto-detection does reach it - so the catalog entry is not useless, it is simply not enough while the user has any other provider signed in, which is every real install. - What Ante sends for effort, from the same files: no reasoning_effort at all for a model with no effort written (item 5, as its reference says omitting it sends none); `--effort min` on a model sent `reasoning_effort: "minimal"` - Ante's min is magpie's minimal, exactly item 4; and a model Ante was told `"supported_efforts": []` for sent no reasoning_effort even when asked `--effort low`, which is what a none-only model now gets instead of a level that refuses it. - The user's own keys inside magpie's entry are ones Ante reads, not decoration: `ante catalog` hands back the entry's auth, http_headers, extra_body, stream_idle_timeout_secs and thinking_display unchanged, and a bearer env_key there makes `ante doctor` count the provider as the one authed. That is item 2. - The no-auth decision holds on Ante's side, isolated by doctor's counts: Ante's own presets alone read `15 providers (0 authed)`; a magpie with no auth block reads `(1 authed)`; the same magpie given a bearer env_key that isn't set drops back to `(0 authed)`; with the key set, `(1 authed)`. So an entry with no auth is authenticated and one that asks for a key the user doesn't have is not - writing an env_key, which the first version's comment considered, would have made magpie the one provider Ante refuses to auto-detect. - Ante fills `support_vision: true` in for itself, as the PR's own note said, and a routing group declared with no capabilities at all still worked. Not covered by that run: the stand-in server answered inside the distro, so this proves Ante's config handling end to end, not a live magpie gateway relay (the first commit's message records a real turn through one). Every probe used ANTE_HOME into a temp folder; this machine's real ~/.ante gained no catalog.json or settings.json. What each fix failed without it, one test each: taking ANTE_HOME out -> TestAnteHome "$ANTE_HOME ignored"; theirsKept out -> "the user's auth lost or changed"; syncJSON out -> "an unchanged catalog was rewritten"; atomic out -> "settings.json kept half of a change that failed"; the old min mapping -> "anteEfforts([minimal high]) = [high], want [min high]"; a written default -> "deepseek/pro: effort written"; settings never pointed at magpie -> TestAnte; a model never written -> TestAnteModelField; Check left quiet -> "moved off magpie: \"\""; handing magpie back over a pick the user made while wired -> TestAnteKeepsWhatTheUserPickedWhileWired; never restoring the model -> TestAnteConnectDisconnect "not put back"; Connect picking the model field instead of magpie as provider -> "Connect left the user's model beside magpie's provider"; ANTE_HOME kept out of agentenv.Vars -> TestFolderVarsAreListed. Taking preferred_models out of the whole list is the one mutation no test turns red on: mergeTheirs cannot merge a JSON array (sync.go returns want for it), so the file behaves the same either way and the argument is there for intent and for the other keys, not for behaviour. go vet ./... and go build ./... pass, as do GOOS=linux go build -tags nogui ./... and GOOS=windows go build ./..., and gofmt is clean on LF content (this clone has core.autocrlf and no .gitattributes, so gofmt -l reports every file). go test -tags nogui ./internal/agent fails twelve tests, and an untouched worktree of origin/main run the same command on this machine fails the same twelve, test for test; ./internal/agentenv, ./internal/appdir and ./internal/edit pass, and every Ante test passes at -count=5. The whole ./... was run on both: the failing sets are alike, origin/main having one more, TestLoginUsageClaudeAskedMeanwhile, already filed as #1228. None of those twelve is this change's, and none is fixed here. * docs: agent-wiring records that Ante keeps the user's keys in magpie's entry and starts on settings.json's provider (#1210) The two rules the review asked for, written where the repo keeps them - the same linesad3b7fe5(#1239) added for Air and2d9db1b6(#1242) for malformed configs. The Verification block gains the package's own Ante run, as #1239 added Air's. --------- Co-authored-by: wizzy-yang <wizzy-yang@users.noreply.github.com>
This commit is contained in:
1 parent
1b29cb81a2
commit
e5e87d980a
7 files changed
+1166
No files matched your search
@@ -109,6 +109,7 @@ line; agents connected to magpie lose it when it quits.
|
||||
| Kimi Code | `~/.kimi/config.toml` (`$KIMI_SHARE_DIR`) | model (a `magpie` provider; magpie's models in Kimi's /model) |
|
||||
| Muse Code | `~/.config/muse/settings.json` (`$XDG_CONFIG_HOME`) | model (endpoint_transport to the gateway, auth none; magpie's models in Muse's list) |
|
||||
| Empryo | `~/.empryo/config.json` | defaultModel (a `magpie` provider at the gateway in `providers`) |
|
||||
| Ante | `~/.ante/catalog.json` and `settings.json` (`$ANTE_HOME`) | provider, model (a `magpie` provider on OpenAiCompatible, with no auth of magpie's: the gateway lets this machine in with any token and Ante counts a provider with none as authenticated; magpie's models in Ante's model picker, and Ante started on magpie, which is what its settings' `provider` decides) |
|
||||
| MiniMax Code (mcode) | `~/.minimax/config.yaml` (`$MINIMAX_DATA_DIR`) | model (a `magpie` custom provider; magpie's models in its /model) |
|
||||
| Droid (Factory) | `~/.factory/settings.json` (`$FACTORY_HOME_OVERRIDE`) | model (magpie's models as BYOK `customModels`, in Droid's /model) |
|
||||
| Cline (CLI) | `~/.cline/data/settings/providers.json` (`$CLINE_DIR`) | model, effort (magpie takes its openai-compatible provider) |
|
||||
|
||||
@@ -35,6 +35,7 @@ something else shows up as drift.
|
||||
|
||||
- magpie only removes what it wrote. Anything else in an agent's config stays as it was.
|
||||
- JetBrains Air's `magpie-opencode.json` keeps user-added fields in `provider.magpie`, including nested options, on sync and model picks (`air` in [`air.go`](../../internal/agent/air.go), using `theirsKept` in [`sync.go`](../../internal/agent/sync.go)). The gateway fields and complete model list are still replaced, so removed models leave the list.
|
||||
- Ante's `catalog.json` keeps user-added fields in `providers.magpie` the same way (`ante` in [`ante.go`](../../internal/agent/ante.go)); its `auth`, `http_headers`, `extra_body` and `stream_idle_timeout_secs` are what a gateway shared on the local network or reached from WSL needs to send a key. Ante starts a session on the `provider` and `model` of `settings.json`, not on what its catalog holds (#1210), so magpie sets that pair and puts back what it found there; a pick the user makes in Ante while magpie is wired is theirs and stands.
|
||||
- JSON/JSONC config edits and their previews reject malformed input rather than editing a partially parsed document. Object edits and VS Code's model-group array edits leave the original file untouched on a parse error; valid comments and trailing commas remain supported (`internal/edit`).
|
||||
- Codex signed in to ChatGPT stays beside its sign-in (`openai_base_url`) unless the user picks magpie API. magpie becomes its provider only while the Codex app holds the active account, which it sends nothing for: `/wham/usage` says it isn't allowed, and it has no credits and isn't a workspace still within its overage (`codexHeld`, read by `provider.CodexUsedUp`). Where the app goes by what usage can't show (an experiment's gate, a reserve, a Team plan's overage under the reserve experiment), it counts as held: a wrong move costs ChatGPT extras, a missed one every turn. A saved reading kept through a failed read stops saying held once its used-up window has started again; a hold with no window used up stays till a fresh read. The app then loses its ChatGPT state (durable threads, remote control), so a window at 100% alone, which credits get past, doesn't move it (`codex.out` marks the move, and Sync undoes it once the account has room).
|
||||
- Moving a field from one of magpie's models to another (the agent's own picker) is not drift. Moving it off magpie is.
|
||||
@@ -55,5 +56,6 @@ something else shows up as drift.
|
||||
go test -tags nogui ./internal/agent -run 'TestConnect|TestDisconnect|Drift|TestKeptAddress|TestWSLAgentAddress|TestReachProbe|TestDryRunKnowsTheAskersProviders|TestReseat|TestPort|TestWSL|TestClaudeKeepsItsClaudeAISignIn|TestClaudeSignIn|TestClaudeOwnEndpointWithAnEmptyKey|TestClaudeAtAnOlderGatewayAddress'
|
||||
go test -tags nogui ./internal/agent
|
||||
go test -tags nogui ./internal/agent -run '^TestAir'
|
||||
go test -tags nogui ./internal/agent -run '^TestAnte'
|
||||
node --test internal/gui/tests/agent-connect.test.cjs internal/gui/tests/agent-disconnect-preview.test.cjs internal/gui/tests/claude-signin.test.cjs
|
||||
```
|
||||
@@ -98,6 +98,7 @@ func All() []*Agent {
|
||||
kimi(home),
|
||||
muse(cfg),
|
||||
empryo(home),
|
||||
ante(home),
|
||||
miniMax(home),
|
||||
droid(home),
|
||||
cline(home),
|
||||
|
||||
@@ -0,0 +1,411 @@
|
||||
package agent
|
||||
|
||||
// Ante (antigma.ai) takes a provider of one's own in ~/.ante/catalog.json
|
||||
// (ANTE_HOME moves the folder; the catalog is always $ANTE_HOME/catalog.json),
|
||||
// merged on top of the providers it ships (docs.antigma.ai,
|
||||
// reference/catalog-reference):
|
||||
//
|
||||
// {"providers":{"<id>":{"display_name":…,"base_url":…,
|
||||
// "wire_style":"OpenAiCompatible","auth":{"bearer":{"env_key":…}},
|
||||
// "http_headers":{…},"extra_body":{…},
|
||||
// "preferred_models":[{"id":…,"display_name":…,"context_limit":…,
|
||||
// "max_tokens":…,"effort":…,"supported_efforts":[…],
|
||||
// "support_vision":…}]}},
|
||||
// "models":{…}}
|
||||
//
|
||||
// The providers map's key is what --provider takes; an id inside an entry is
|
||||
// ignored. magpie adds the entry "magpie" at the gateway's /v1, on
|
||||
// OpenAiCompatible — the wire style Ante speaks Chat Completions with, which
|
||||
// is what the gateway takes every model on — and lists magpie's models in its
|
||||
// preferred_models, Ante's model picker. The user's own providers and the
|
||||
// models section are left as they are, and so is everything the user added
|
||||
// inside magpie's own entry (an auth block, http_headers, extra_body,
|
||||
// stream_idle_timeout_secs), which is what a gateway shared over the local
|
||||
// network or reached from WSL needs to send a key (#1103, theirsKept).
|
||||
//
|
||||
// No auth of magpie's own: the gateway listens on loopback and lets any token
|
||||
// in from this machine (gateway.Token's comment, lan.go's identifyCaller), and
|
||||
// Ante counts a provider with no auth block as authenticated ("Providers with
|
||||
// no `auth` block are always authenticated", catalog-reference). Writing a
|
||||
// bearer env_key instead would mean magpie putting a key into Ante's
|
||||
// environment, which the user would have to do themselves — the same decision
|
||||
// Empryo's entry makes, which sends no key of the user's either.
|
||||
//
|
||||
// Being in the catalog is not enough for Ante to use it. Which provider a
|
||||
// session starts on is the `provider` and `model` of ~/.ante/settings.json:
|
||||
// "Explicit --provider and --model flags override the provider and model
|
||||
// values in ~/.ante/settings.json. When neither source specifies a choice,
|
||||
// Ante uses the auto-detection rules below… it uses the first authenticated
|
||||
// provider in catalog order" (catalog-reference). Providers it ships come
|
||||
// first, so magpie's is one auto-detection never picks while the user has any
|
||||
// other signed in, and a user who set nothing is on Ante's own provider. So
|
||||
// magpie sets those two as well and saves the pair it found there, which is
|
||||
// what steps out puts back — the same pair Empryo's defaultModel and MiniMax
|
||||
// Code's defaultModel are. A pick the user makes in Ante while magpie is
|
||||
// wired is theirs and stands; magpie hands them what they moved off, not the
|
||||
// one it read when it wired in.
|
||||
//
|
||||
// Ante reads the catalog once per process (restart to pick an edit up) and
|
||||
// parses it leniently: an entry that doesn't deserialize is skipped with a
|
||||
// notice and the rest still load, so a bad file magpie wrote costs the user
|
||||
// magpie's provider, not their own.
|
||||
|
||||
import (
|
||||
"path/filepath"
|
||||
"slices"
|
||||
"strings"
|
||||
|
||||
"github.com/tidwall/gjson"
|
||||
|
||||
"github.com/yetone/magpie/internal/catalog"
|
||||
"github.com/yetone/magpie/internal/edit"
|
||||
"github.com/yetone/magpie/internal/provider"
|
||||
)
|
||||
|
||||
const (
|
||||
anteProviders = "providers"
|
||||
anteProvider = anteProviders + "." + magpieID
|
||||
// anteProviderKey and anteModelKey are the two settings.json keys that
|
||||
// decide which provider a session starts on (catalog-reference, "How a
|
||||
// model and provider are chosen").
|
||||
anteProviderKey = "provider"
|
||||
anteModelKey = "model"
|
||||
)
|
||||
|
||||
// anteDir is the folder Ante keeps catalog.json and settings.json in:
|
||||
// ANTE_HOME, else ~/.ante (catalog-reference: "~/.ante can be relocated with
|
||||
// the ANTE_HOME environment variable; the catalog is always
|
||||
// $ANTE_HOME/catalog.json"). A distro's ANTE_HOME isn't one magpie can read,
|
||||
// so there it is ~/.ante, as MiniMax Code's mcode (place.getenv).
|
||||
func anteDir(at place) string {
|
||||
if d := strings.TrimSpace(at.getenv("ANTE_HOME")); d != "" {
|
||||
return d
|
||||
}
|
||||
return filepath.Join(at.home, ".ante")
|
||||
}
|
||||
|
||||
func ante(home string) *Agent { return anteAt(here(home)) }
|
||||
|
||||
// anteIn is Ante in a WSL distro (see wsl.go).
|
||||
func anteIn(at place) *Agent { return anteAt(at) }
|
||||
|
||||
// anteAt is Ante with its home at at.home, reaching the gateway as at does.
|
||||
func anteAt(at place) *Agent {
|
||||
gatewayV1 := at.v1
|
||||
dir := anteDir(at)
|
||||
path := filepath.Join(dir, "catalog.json")
|
||||
settingsPath := filepath.Join(dir, "settings.json")
|
||||
// keyNew marks a file magpie made, which goes again once magpie's own
|
||||
// keys are taken out of it and nothing is left
|
||||
keyNew := "ante:" + path + ":new"
|
||||
keyNewSettings := "ante:" + settingsPath + ":new"
|
||||
// the provider and model Ante starts on, stashed for stepping out
|
||||
keyProvider := at.key("ante.provider")
|
||||
keyModel := at.key("ante.model")
|
||||
// wired: magpie's provider is in the catalog
|
||||
wired := func() bool { _, ok := edit.GetJSON(path, anteProvider); return ok }
|
||||
get := func(k string) string { v, _ := edit.GetJSON(settingsPath, k); return v }
|
||||
set := func(k, v string) error {
|
||||
if v == "" {
|
||||
return edit.DelJSON(settingsPath, k)
|
||||
}
|
||||
return edit.SetJSON(settingsPath, edit.KV{Path: k, Value: v})
|
||||
}
|
||||
// entry is magpie's catalog entry, the user's own keys in it kept and
|
||||
// preferred_models — magpie's list alone — replaced
|
||||
entry := theirsKept(path, anteProvider, func() any { return anteProviderJSON(gatewayV1()) }, "preferred_models")
|
||||
// ours is whether Ante starts on magpie. Its settings name the provider
|
||||
// and the model apart, so magpie's provider is what makes the pair
|
||||
// magpie's; with no model named Ante takes the default of
|
||||
// preferred_models, one of magpie's.
|
||||
ours := func() bool { return get(anteProviderKey) == magpieID }
|
||||
// unwire takes Ante back off magpie: the provider and model it found
|
||||
// when it wired in — or nothing, which leaves Ante to its
|
||||
// auto-detection — and magpie's catalog entry out. What the user chose
|
||||
// in Ante since is theirs and stands, and the pair saved for it goes
|
||||
// rather than being written over their pick. Every step is a no-op when
|
||||
// there is nothing to undo, which is how this survives Disconnect
|
||||
// returning every field to its default: the first call takes the whole
|
||||
// wiring out.
|
||||
unwire := func() error {
|
||||
if get(anteProviderKey) == magpieID {
|
||||
was, mine := unstash(keyProvider), unstash(keyModel)
|
||||
if err := set(anteProviderKey, was); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := set(anteModelKey, mine); err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
forget(keyProvider, keyModel)
|
||||
}
|
||||
// settings.json holding neither of magpie's keys, and magpie having
|
||||
// made it, is magpie's file to take away
|
||||
if get(anteProviderKey) == "" && get(anteModelKey) == "" && unstash(keyNewSettings) != "" {
|
||||
if raw, err := edit.Read(settingsPath); err == nil && len(gjson.ParseBytes(raw).Map()) == 0 {
|
||||
if err := edit.Remove(settingsPath); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
if !isFile(path) {
|
||||
return nil
|
||||
}
|
||||
if err := edit.DelJSON(path, anteProvider); err != nil {
|
||||
return err
|
||||
}
|
||||
// a providers map left with nothing goes with it, as Ante reads a
|
||||
// missing catalog as the providers it ships
|
||||
if raw, err := edit.Read(path); err == nil {
|
||||
if m := gjson.GetBytes(raw, anteProviders); m.Exists() && len(m.Map()) == 0 {
|
||||
if err := edit.DelJSON(path, anteProviders); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
// a file magpie made goes with its last entry
|
||||
if unstash(keyNew) != "" {
|
||||
if raw, err := edit.Read(path); err == nil && len(gjson.ParseBytes(raw).Map()) == 0 {
|
||||
return edit.Remove(path)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
// put is magpie's whole wiring in one step: magpie's catalog entry, with
|
||||
// the user's own keys in it kept, and Ante started on magpie. What Ante
|
||||
// named as its own provider is saved before magpie writes over it, and
|
||||
// only then — a value the user chose in Ante while magpie was wired is
|
||||
// theirs and is what comes back. An empty one is no value to restore,
|
||||
// which leaves Ante to its auto-detection.
|
||||
put := func(model string) error {
|
||||
if get(anteProviderKey) != magpieID {
|
||||
stash(map[string]string{keyProvider: get(anteProviderKey), keyModel: get(anteModelKey)})
|
||||
} else if m := get(anteModelKey); model == "" && isMagpie(m) {
|
||||
// already on magpie: the model it was started on stays
|
||||
model = m
|
||||
}
|
||||
if !wired() && !isFile(path) {
|
||||
stash(map[string]string{keyNew: "1"})
|
||||
}
|
||||
if !isFile(settingsPath) {
|
||||
stash(map[string]string{keyNewSettings: "1"})
|
||||
}
|
||||
if err := edit.SetJSON(path, edit.KV{Path: anteProvider, Value: entry()}); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := set(anteProviderKey, magpieID); err != nil {
|
||||
return err
|
||||
}
|
||||
return set(anteModelKey, model)
|
||||
}
|
||||
return atomic(&Agent{
|
||||
ID: "ante", Name: "Ante", Icon: "generic",
|
||||
Bin: "ante", Dir: dir, Path: path,
|
||||
Notice: func() string {
|
||||
if Running(`(^|/)ante( |$)`) {
|
||||
return "Ante reads its catalog at start-up — restart open ante sessions to use this."
|
||||
}
|
||||
return ""
|
||||
},
|
||||
Check: func() string {
|
||||
// magpie's entry alone says nothing is off when Ante has been
|
||||
// moved to another provider in its own /providers: its settings
|
||||
// decide what a session asks (commandcode's modelProvider is the
|
||||
// same pair)
|
||||
if !wired() {
|
||||
return ""
|
||||
}
|
||||
if !ours() {
|
||||
return "Ante's provider (settings.json) is " + orDefault(get(anteProviderKey)) + ", so it no longer asks magpie"
|
||||
}
|
||||
raw, err := edit.Read(path)
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
e := gjson.GetBytes(raw, anteProvider)
|
||||
return wiringOff("Ante", path, func(k string) (string, bool) {
|
||||
r := e.Get(k)
|
||||
return r.String(), r.Exists()
|
||||
}, "base_url", gatewayV1())
|
||||
},
|
||||
Sync: func() error {
|
||||
if !wired() {
|
||||
return nil
|
||||
}
|
||||
// the user's own keys in magpie's entry stay, preferred_models
|
||||
// is magpie's list, and a file that says the same as it does is
|
||||
// not written again (syncJSON, theirsKept)
|
||||
return syncJSON(path, anteProvider, entry)
|
||||
},
|
||||
Fields: []Field{{
|
||||
// This field is magpie's models in Ante's picker: its entry in
|
||||
// the catalog, and Ante started on it (settings.json's
|
||||
// provider). Get reads the entry alone, as Pencil's does, so
|
||||
// Disconnect takes the entry out even when the user has since
|
||||
// picked another provider in Ante's own /providers.
|
||||
Key: "provider", Label: "provider",
|
||||
Get: func() string {
|
||||
if wired() {
|
||||
return magpieID
|
||||
}
|
||||
return ""
|
||||
},
|
||||
Set: func(v string) error {
|
||||
if v == "" {
|
||||
return unwire()
|
||||
}
|
||||
return put("")
|
||||
},
|
||||
Options: func(map[string]string) []Option {
|
||||
return []Option{{Value: magpieID, Label: "magpie", Icon: "magpie", Note: "every magpie model in Ante's model picker, and Ante starts on magpie"}}
|
||||
},
|
||||
}, {
|
||||
// Ante names the model on its own, without a provider in front
|
||||
// of it (its settings keep the two apart, and its picker lists
|
||||
// the ids of preferred_models), so magpie's catalog refs are
|
||||
// written as they are — the ids magpie put in preferred_models.
|
||||
// A bare id says nothing on its own: read beside magpie's
|
||||
// provider only, so an id the user's own provider happens to
|
||||
// serve the same way is not taken for one of magpie's.
|
||||
Key: "model", Label: "model",
|
||||
Get: func() string {
|
||||
if ours() {
|
||||
return get(anteModelKey)
|
||||
}
|
||||
return ""
|
||||
},
|
||||
Set: func(v string) error {
|
||||
if v == "" {
|
||||
// no model of its own: Ante starts on magpie and takes
|
||||
// the default of preferred_models, one of magpie's
|
||||
if ours() {
|
||||
return set(anteModelKey, "")
|
||||
}
|
||||
return unwire()
|
||||
}
|
||||
if !isMagpie(v) {
|
||||
// a model of Ante's own: magpie steps out and Ante is
|
||||
// left on it — with no provider named, which is how Ante
|
||||
// reaches the provider whose list holds that id
|
||||
if err := unwire(); err != nil {
|
||||
return err
|
||||
}
|
||||
return set(anteModelKey, v)
|
||||
}
|
||||
return put(v)
|
||||
},
|
||||
Options: func(map[string]string) []Option {
|
||||
return viaMagpie("ante", "")
|
||||
},
|
||||
}},
|
||||
}, path, settingsPath)
|
||||
}
|
||||
|
||||
// anteProviderJSON is magpie's entry in Ante's catalog: the gateway's /v1 on
|
||||
// OpenAiCompatible, every magpie model in preferred_models.
|
||||
//
|
||||
// A routing group is not told what it reasons at: which member answers, and
|
||||
// what that member takes, is the group's own to decide per turn, so magpie's
|
||||
// other agents leave an effort off for one too (magpieModels sets no APIs for
|
||||
// a group for the same reason). Ante would otherwise ask for an effort the
|
||||
// member the group picked doesn't have.
|
||||
//
|
||||
// support_vision is left alone for every model: Ante takes a model as seeing
|
||||
// images unless it is told otherwise, and what magpie knows is only as good
|
||||
// as a provider's own list — a relay's models are often not described at all,
|
||||
// so writing false would have Ante drop the reader's images before the
|
||||
// gateway, which describes them to a model that can't see them itself
|
||||
// (provider.Described). No other agent magpie wires declares this either.
|
||||
func anteProviderJSON(gw string) map[string]any {
|
||||
ms := []map[string]any{}
|
||||
for _, m := range magpieModels("ante") {
|
||||
ms = append(ms, anteModelJSON(m))
|
||||
}
|
||||
p := map[string]any{
|
||||
"display_name": "magpie",
|
||||
// gw is the gateway's /v1 already (place.v1), which is what Ante
|
||||
// joins its own path onto
|
||||
"base_url": gw,
|
||||
"wire_style": "OpenAiCompatible",
|
||||
}
|
||||
if len(ms) > 0 {
|
||||
p["preferred_models"] = ms
|
||||
}
|
||||
return p
|
||||
}
|
||||
|
||||
// anteModelJSON is one magpie model as Ante's catalog entry lists it: what
|
||||
// magpie knows of it, and nothing it doesn't. A routing group is not told
|
||||
// what it reasons at (anteProviderJSON says why), so its levels are left out
|
||||
// with the rest of its capabilities.
|
||||
func anteModelJSON(m catalog.Model) map[string]any {
|
||||
group := strings.HasPrefix(m.ID, provider.GroupPrefix)
|
||||
e := map[string]any{"id": m.ID}
|
||||
if m.Name != "" && m.Name != m.ID {
|
||||
e["display_name"] = m.Name
|
||||
}
|
||||
if m.Context > 0 {
|
||||
e["context_limit"] = m.Context
|
||||
}
|
||||
if m.Output > 0 {
|
||||
e["max_tokens"] = maxTokens(m)
|
||||
}
|
||||
// Ante's ladder is per provider and model family; a model magpie knows
|
||||
// the levels of says which of them Ante may send. No default "effort" is
|
||||
// written: a model then starts where Ante puts it, and written as the
|
||||
// top rung it would make every one of them Ante's slowest and dearest
|
||||
// (anteEfforts).
|
||||
if !group && len(m.Efforts) > 0 {
|
||||
e["supported_efforts"] = anteEfforts(m.Efforts)
|
||||
}
|
||||
return e
|
||||
}
|
||||
|
||||
// anteLadder is Ante's effort scale, ascending, and the magpie level each
|
||||
// of its rungs is. Ante's lowest rung, "min", is not a name magpie lists:
|
||||
// Ante's reference says what it sends it as (docs.antigma.ai,
|
||||
// reference/catalog-reference, the supported_efforts row):
|
||||
//
|
||||
// "OpenAI-compatible providers send the selected level as the same-named
|
||||
// reasoning_effort (min is sent as minimal)"
|
||||
//
|
||||
// so Ante's "min" is magpie's "minimal", and magpie's "none" — no thinking
|
||||
// asked for at all — has no rung of Ante's, which sends one whatever the
|
||||
// model was told it accepts. The PR's first version folded both into "min",
|
||||
// so a model that takes only "none" was asked for `reasoning_effort:
|
||||
// minimal` and refused.
|
||||
//
|
||||
// The other way Ante says a model takes no effort is an empty list:
|
||||
//
|
||||
// "An empty list (supported_efforts: []) declares that the model takes no
|
||||
// effort setting at all."
|
||||
//
|
||||
// which is what a model magpie knows only "none" comes out as: Ante then
|
||||
// shows no slider and sends none, rather than a level the model refuses.
|
||||
// The default is left off entirely — Ante's own words are
|
||||
//
|
||||
// "effort … Omit for models with no reasoning knob (Ante then sends no
|
||||
// effort parameter and hides the effort slider)"
|
||||
//
|
||||
// — so a written default is a sent effort, and setting it to the top rung
|
||||
// made every model Ante's slowest and dearest one.
|
||||
var anteLadder = []struct{ ante, magpie string }{
|
||||
{"min", "minimal"},
|
||||
{"low", "low"},
|
||||
{"medium", "medium"},
|
||||
{"high", "high"},
|
||||
{"xhigh", "xhigh"},
|
||||
{"max", "max"},
|
||||
}
|
||||
|
||||
// anteEfforts are the levels Ante is told a model accepts, on Ante's ladder,
|
||||
// in its order. Empty is the declaration that it takes no effort setting.
|
||||
func anteEfforts(levels []string) []string {
|
||||
out := []string{}
|
||||
for _, rung := range anteLadder {
|
||||
if slices.Contains(levels, rung.magpie) {
|
||||
out = append(out, rung.ante)
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
@@ -0,0 +1,745 @@
|
||||
package agent
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/yetone/magpie/internal/catalog"
|
||||
"github.com/yetone/magpie/internal/edit"
|
||||
"github.com/yetone/magpie/internal/provider"
|
||||
)
|
||||
|
||||
// anteHome is a HOME with an Ante of the user's: a catalog holding a provider
|
||||
// of their own and a model patch, and a settings.json naming the provider and
|
||||
// model Ante starts a session on.
|
||||
func anteHome(t *testing.T) (home, path, settings string) {
|
||||
t.Helper()
|
||||
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", Key: "k",
|
||||
Chat: "https://api.deepseek.com/v1", Models: []string{"pro", "flash"}}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
// one model of magpie's with levels of its own, so what Ante is told of
|
||||
// them — and what it is not — is said by a real catalog entry
|
||||
if err := catalog.SaveLive("deepseek", "https://api.deepseek.com/v1", []catalog.Model{
|
||||
{ID: "pro", Efforts: []string{"none", "minimal", "low", "high", "max"}, Context: 200000, Output: 8000},
|
||||
{ID: "flash"},
|
||||
}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
dir := filepath.Join(home, ".ante")
|
||||
writeFile(t, filepath.Join(dir, "catalog.json"), `{
|
||||
"providers": {
|
||||
"together": {
|
||||
"display_name": "Together AI",
|
||||
"base_url": "https://api.together.xyz/v1",
|
||||
"wire_style": "OpenAiCompatible",
|
||||
"auth": { "bearer": { "env_key": "TOGETHER_API_KEY" } }
|
||||
}
|
||||
},
|
||||
"models": { "claude-opus-5-5": { "effort": "max" } }
|
||||
}
|
||||
`)
|
||||
settings = filepath.Join(dir, "settings.json")
|
||||
writeFile(t, settings, `{
|
||||
"provider": "together",
|
||||
"model": "moonshotai/Kimi-K2-Instruct",
|
||||
"permission_mode": "auto",
|
||||
"theme": "default"
|
||||
}
|
||||
`)
|
||||
return home, filepath.Join(dir, "catalog.json"), settings
|
||||
}
|
||||
|
||||
func anteRead(t *testing.T, p string) map[string]any {
|
||||
t.Helper()
|
||||
var m map[string]any
|
||||
b, err := os.ReadFile(p)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
if err := json.Unmarshal(b, &m); err != nil {
|
||||
t.Fatalf("%s is not JSON: %v\n%s", p, err, b)
|
||||
}
|
||||
return m
|
||||
}
|
||||
|
||||
func anteProv(t *testing.T, path string) map[string]any {
|
||||
t.Helper()
|
||||
ps, _ := anteRead(t, path)["providers"].(map[string]any)
|
||||
return ps
|
||||
}
|
||||
|
||||
func anteEntry(t *testing.T, path string) map[string]any {
|
||||
t.Helper()
|
||||
e, _ := anteProv(t, path)[magpieID].(map[string]any)
|
||||
return e
|
||||
}
|
||||
|
||||
func anteModelList(t *testing.T, path string) []map[string]any {
|
||||
t.Helper()
|
||||
var out []map[string]any
|
||||
for _, x := range anteEntry(t, path)["preferred_models"].([]any) {
|
||||
e, _ := x.(map[string]any)
|
||||
out = append(out, e)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// Ante's catalog.json is merged on top of the providers it ships: magpie's
|
||||
// entry is added to the providers map under its own id, with the user's own
|
||||
// providers and the models section left as they are, and it goes again when
|
||||
// magpie steps out. A file magpie made goes with it. Being in the catalog is
|
||||
// not enough for Ante to use it — its settings say which provider a session
|
||||
// starts on — so magpie sets those too and puts back what the user had.
|
||||
func TestAnte(t *testing.T) {
|
||||
home, path, settings := anteHome(t)
|
||||
want := anteRead(t, path)
|
||||
wantSettings := anteRead(t, settings)
|
||||
|
||||
a := ante(home)
|
||||
if a.Path != path || a.Dir != filepath.Dir(path) || a.Bin != "ante" {
|
||||
t.Fatalf("path %q dir %q bin %q", a.Path, a.Dir, a.Bin)
|
||||
}
|
||||
f, m := a.Field("provider"), a.Field("model")
|
||||
// not wired yet: the user's provider is there, magpie's is not, and
|
||||
// nothing is said about wiring magpie never did
|
||||
if got := f.Get(); got != "" || a.Check() != "" {
|
||||
t.Fatalf("own: %q %q", got, a.Check())
|
||||
}
|
||||
var vals []string
|
||||
for _, o := range f.Options(a.Values()) {
|
||||
vals = append(vals, o.Value)
|
||||
}
|
||||
if !reflect.DeepEqual(vals, []string{"magpie"}) {
|
||||
t.Fatalf("options: %v", vals)
|
||||
}
|
||||
|
||||
for range 2 { // twice: one magpie provider, and the user's left alone
|
||||
if err := f.Set("magpie"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
ps := anteProv(t, path)
|
||||
if len(ps) != 2 || !reflect.DeepEqual(ps["together"], want["providers"].(map[string]any)["together"]) {
|
||||
t.Fatalf("providers: %v", ps)
|
||||
}
|
||||
if got := anteRead(t, path)["models"]; !reflect.DeepEqual(got, want["models"]) {
|
||||
t.Fatalf("models: %v", got)
|
||||
}
|
||||
mine := anteEntry(t, path)
|
||||
if mine["base_url"] != gatewayV1() || mine["wire_style"] != "OpenAiCompatible" || mine["display_name"] != "magpie" {
|
||||
t.Fatalf("magpie provider: %v", mine)
|
||||
}
|
||||
// no auth block of magpie's: the gateway lets this machine in with any
|
||||
// token, and Ante counts a provider with none as authenticated
|
||||
if _, ok := mine["auth"]; ok {
|
||||
t.Fatalf("auth written: %v", mine["auth"])
|
||||
}
|
||||
ms := anteModelList(t, path)
|
||||
if len(ms) == 0 {
|
||||
t.Fatalf("no models: %v", mine)
|
||||
}
|
||||
for _, e := range ms {
|
||||
id, _ := e["id"].(string)
|
||||
if id == "" {
|
||||
t.Fatalf("a model without an id: %v", e)
|
||||
}
|
||||
// support_vision is never written: Ante takes a model as seeing
|
||||
// images unless told otherwise, and magpie's own answer is only as
|
||||
// good as a provider's list — a relay's models are often not
|
||||
// described — so writing false would drop the reader's images
|
||||
if _, ok := e["support_vision"]; ok {
|
||||
t.Fatalf("%s: support_vision written: %v", id, e)
|
||||
}
|
||||
// and no default effort either: written, Ante sends it on every turn,
|
||||
// and the top rung is its slowest and dearest one
|
||||
if _, ok := e["effort"]; ok {
|
||||
t.Fatalf("%s: effort written: %v", id, e)
|
||||
}
|
||||
}
|
||||
if f.Get() != "magpie" || a.Check() != "" {
|
||||
t.Fatalf("on: %q %q", f.Get(), a.Check())
|
||||
}
|
||||
// Ante starts on magpie: its provider named, and the provider and model
|
||||
// the user had kept out of the way for when magpie steps out. The model
|
||||
// goes rather than staying: Ante's settings name the two apart, so
|
||||
// together's model id beside magpie's provider would have the gateway
|
||||
// asked for a model it may not serve.
|
||||
if got := anteRead(t, settings); got["provider"] != magpieID {
|
||||
t.Fatalf("settings: %v", got)
|
||||
}
|
||||
if _, ok := anteRead(t, settings)["model"]; ok {
|
||||
t.Fatalf("the user's model left beside magpie's provider: %v", anteRead(t, settings))
|
||||
}
|
||||
for _, k := range []string{"permission_mode", "theme"} {
|
||||
if anteRead(t, settings)[k] != wantSettings[k] {
|
||||
t.Fatalf("%s lost: %v", k, anteRead(t, settings))
|
||||
}
|
||||
}
|
||||
|
||||
// magpie's provider pointed elsewhere: that is said, and Sync brings it
|
||||
// back
|
||||
writeFile(t, path, strings.Replace(readFile(path), gatewayV1(), "http://elsewhere/v1", 1))
|
||||
if !strings.Contains(a.Check(), "http://elsewhere/v1") {
|
||||
t.Fatalf("check: %q", a.Check())
|
||||
}
|
||||
if err := a.Sync(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if got := anteEntry(t, path)["base_url"]; got != gatewayV1() {
|
||||
t.Fatalf("sync: %v", got)
|
||||
}
|
||||
// Ante moved to one of its own providers: magpie's entry is still in the
|
||||
// catalog, and that is said rather than kept quiet about (#1210)
|
||||
writeFile(t, settings, `{"provider":"together","model":"moonshotai/Kimi-K2-Instruct","permission_mode":"auto","theme":"default"}`)
|
||||
if got := a.Check(); !strings.Contains(got, "together") || !strings.Contains(got, "no longer asks magpie") {
|
||||
t.Fatalf("moved off magpie: %q", got)
|
||||
}
|
||||
|
||||
// stepping out takes magpie's provider away, leaves the user's file as it
|
||||
// was, and gives Ante back the provider the user had — the one moved off
|
||||
// to there being their own choice, which stands
|
||||
if err := f.Set(""); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if got := anteRead(t, path); !reflect.DeepEqual(got, want) {
|
||||
t.Fatalf("restore: %v", got)
|
||||
}
|
||||
if got := anteRead(t, settings); got["provider"] != "together" {
|
||||
t.Fatalf("restore settings: %v", got)
|
||||
}
|
||||
if f.Get() != "" || m.Get() != "" || a.Check() != "" {
|
||||
t.Fatalf("off: %q %q %q", f.Get(), m.Get(), a.Check())
|
||||
}
|
||||
|
||||
// a pair of files magpie made go again, and what Ante had before them was
|
||||
// nothing: its auto-detection, not a made-up choice
|
||||
os.Remove(path)
|
||||
os.Remove(settings)
|
||||
if err := f.Set("magpie"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, ok := anteProv(t, path)[magpieID]; !ok {
|
||||
t.Fatalf("new: %v", anteRead(t, path))
|
||||
}
|
||||
if err := f.Set(""); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if b, err := os.ReadFile(path); err == nil {
|
||||
t.Fatalf("made catalog left: %s", b)
|
||||
}
|
||||
if b, err := os.ReadFile(settings); err == nil {
|
||||
t.Fatalf("made settings left: %s", b)
|
||||
}
|
||||
}
|
||||
|
||||
// The model field is the same wiring spelled as a model: Ante names its
|
||||
// model on its own, with the provider kept apart from it, so magpie writes
|
||||
// the catalog id and its provider beside it. A model of the user's own takes
|
||||
// magpie out again, and Disconnect puts back what Ante had.
|
||||
func TestAnteModelField(t *testing.T) {
|
||||
home, path, settings := anteHome(t)
|
||||
a := ante(home)
|
||||
m := a.Field("model")
|
||||
if err := m.Set("deepseek/pro"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
got := anteRead(t, settings)
|
||||
if got["provider"] != magpieID || got["model"] != "deepseek/pro" {
|
||||
t.Fatalf("on: %v", got)
|
||||
}
|
||||
if !a.Wired() || a.Check() != "" || m.Get() != "deepseek/pro" {
|
||||
t.Fatalf("wired: %v %q %q", a.Wired(), a.Check(), m.Get())
|
||||
}
|
||||
if _, ok := anteProv(t, path)[magpieID]; !ok {
|
||||
t.Fatalf("no catalog entry: %v", anteProv(t, path))
|
||||
}
|
||||
// one of Ante's own models: magpie steps out, and Ante is left on it —
|
||||
// its provider back with it, since a bare id means nothing without one
|
||||
if err := m.Set("moonshotai/Kimi-K2-Instruct"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
got = anteRead(t, settings)
|
||||
if got["provider"] != "together" || got["model"] != "moonshotai/Kimi-K2-Instruct" {
|
||||
t.Fatalf("own: %v", got)
|
||||
}
|
||||
if _, ok := anteProv(t, path)[magpieID]; ok {
|
||||
t.Fatalf("magpie's entry left: %v", anteProv(t, path))
|
||||
}
|
||||
if a.Wired() || m.Get() != "" {
|
||||
t.Fatalf("still wired: %v %q", a.Wired(), m.Get())
|
||||
}
|
||||
// Disconnect finds nothing left to undo and writes nothing new
|
||||
was := readFile(settings)
|
||||
if err := a.Disconnect(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if got := readFile(settings); got != was {
|
||||
t.Fatalf("rewritten by Disconnect:\n%s\n---\n%s", was, got)
|
||||
}
|
||||
}
|
||||
|
||||
// Connect starts Ante on magpie and Disconnect gives Ante back what it had:
|
||||
// the save-and-restore the catalog entry alone never was (#1210).
|
||||
func TestAnteConnectDisconnect(t *testing.T) {
|
||||
home, path, settings := anteHome(t)
|
||||
a := ante(home)
|
||||
if err := a.Connect(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !a.Wired() {
|
||||
t.Fatal("not wired after Connect")
|
||||
}
|
||||
got := anteRead(t, settings)
|
||||
if got["provider"] != magpieID {
|
||||
t.Fatalf("Ante starts on %v, not magpie: %v", got["provider"], got)
|
||||
}
|
||||
if got["model"] == nil || got["model"] == "moonshotai/Kimi-K2-Instruct" {
|
||||
t.Fatalf("Connect left the user's model beside magpie's provider: %v", got)
|
||||
}
|
||||
if d := a.Drift(); d != nil {
|
||||
t.Fatalf("drift after Connect: %+v", d)
|
||||
}
|
||||
if err := a.Disconnect(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
got = anteRead(t, settings)
|
||||
if got["provider"] != "together" || got["model"] != "moonshotai/Kimi-K2-Instruct" {
|
||||
t.Fatalf("not put back: %v", got)
|
||||
}
|
||||
if _, ok := anteProv(t, path)[magpieID]; ok {
|
||||
t.Fatalf("magpie's entry left: %v", anteProv(t, path))
|
||||
}
|
||||
if d := a.Drift(); d != nil {
|
||||
t.Fatalf("drift after Disconnect: %+v", d)
|
||||
}
|
||||
}
|
||||
|
||||
// What the user set in Ante itself is what comes back: a provider picked in
|
||||
// Ante's /providers while magpie's entry stands is their choice, and magpie
|
||||
// does not hand over the one it found when it wired in.
|
||||
func TestAnteKeepsWhatTheUserPickedWhileWired(t *testing.T) {
|
||||
home, path, settings := anteHome(t)
|
||||
a := ante(home)
|
||||
f := a.Field("provider")
|
||||
if err := f.Set("magpie"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
// the user moves to their own provider, and to one of magpie's models
|
||||
// there, in Ante's picker
|
||||
writeFile(t, settings, `{"provider":"together","model":"x-ai/grok-4.7","theme":"default"}`+"\n")
|
||||
if err := f.Set(""); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
got := anteRead(t, settings)
|
||||
if got["provider"] != "together" || got["model"] != "x-ai/grok-4.7" {
|
||||
t.Fatalf("the user's pick was overwritten: %v", got)
|
||||
}
|
||||
if got["theme"] != "default" {
|
||||
t.Fatalf("the user's other keys: %v", got)
|
||||
}
|
||||
if _, ok := anteProv(t, path)[magpieID]; ok {
|
||||
t.Fatalf("magpie's entry left: %v", anteProv(t, path))
|
||||
}
|
||||
}
|
||||
|
||||
// sameText is whether two reads of the same key say the same thing.
|
||||
// edit.GetJSON hands back an object as JSON and a scalar unquoted (900,
|
||||
// concise), so a JSON compare alone misses the second: both spellings are
|
||||
// tried. Indentation is the file's own, not a change of what the user wrote.
|
||||
func sameText(a, b string) bool {
|
||||
if a == b {
|
||||
return true
|
||||
}
|
||||
if sameJSON(a, json.RawMessage(b)) || sameJSON(b, json.RawMessage(a)) {
|
||||
return true
|
||||
}
|
||||
return strings.TrimSpace(a) == strings.TrimSpace(b)
|
||||
}
|
||||
|
||||
// anteEffortModel is a provider of magpie's with one model at the levels c
|
||||
// names, and the catalog entry Ante is handed for it.
|
||||
func anteEffortModel(t *testing.T, id string, efforts []string) map[string]any {
|
||||
t.Helper()
|
||||
if err := catalog.SaveLive("eff", "https://example.test/v1", []catalog.Model{
|
||||
{ID: id, Efforts: efforts},
|
||||
}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := provider.Save(provider.Provider{ID: "eff", Name: "Eff", Key: "k",
|
||||
Chat: "https://example.test/v1", Models: []string{id}}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for _, m := range magpieModels("ante") {
|
||||
if m.ID == "eff/"+id {
|
||||
e, _ := json.Marshal(anteModelJSON(m))
|
||||
var out map[string]any
|
||||
if err := json.Unmarshal(e, &out); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return out
|
||||
}
|
||||
}
|
||||
t.Fatalf("eff/%s is not in magpie's catalog", id)
|
||||
return nil
|
||||
}
|
||||
|
||||
// The effort levels magpie knows are put on Ante's ladder, and none of them
|
||||
// is a default: Ante's lowest rung, min, is the level it sends as
|
||||
// `reasoning_effort: minimal` — magpie's "minimal", never its "none" — and a
|
||||
// model magpie knows only "none" of says so with an empty list, which is
|
||||
// Ante's own way of saying the model takes no effort setting (#1210).
|
||||
func TestAnteEfforts(t *testing.T) {
|
||||
for _, c := range []struct {
|
||||
name string
|
||||
levels []string
|
||||
want []string
|
||||
}{
|
||||
{"a ladder", []string{"low", "medium", "high"}, []string{"low", "medium", "high"}},
|
||||
{"minimal is Ante's min", []string{"minimal", "high"}, []string{"min", "high"}},
|
||||
{"none is no rung of Ante's", []string{"none", "high"}, []string{"high"}},
|
||||
{"none alone takes no effort", []string{"none"}, []string{}},
|
||||
{"magpie's order, Ante's ladder", []string{"max", "low", "none", "minimal"}, []string{"min", "low", "max"}},
|
||||
{"all of them", []string{"none", "minimal", "low", "medium", "high", "xhigh", "max"},
|
||||
[]string{"min", "low", "medium", "high", "xhigh", "max"}},
|
||||
{"nothing known", nil, []string{}},
|
||||
} {
|
||||
t.Run(c.name, func(t *testing.T) {
|
||||
got := anteEfforts(c.levels)
|
||||
if len(got) != len(c.want) {
|
||||
t.Fatalf("anteEfforts(%v) = %v, want %v", c.levels, got, c.want)
|
||||
}
|
||||
for i := range got {
|
||||
if got[i] != c.want[i] {
|
||||
t.Fatalf("anteEfforts(%v) = %v, want %v", c.levels, got, c.want)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
// and what magpie writes into the catalog for each of them
|
||||
for _, c := range []struct {
|
||||
efforts []string
|
||||
want string // supported_efforts as JSON; "" for the key not written
|
||||
}{
|
||||
{[]string{"low", "medium", "high"}, `["low","medium","high"]`},
|
||||
{[]string{"minimal", "low"}, `["min","low"]`},
|
||||
{[]string{"none"}, `[]`},
|
||||
{nil, ""},
|
||||
} {
|
||||
e := anteEffortModel(t, "one", c.efforts)
|
||||
got, written := "", false
|
||||
if v, ok := e["supported_efforts"]; ok {
|
||||
b, _ := json.Marshal(v)
|
||||
got, written = string(b), true
|
||||
}
|
||||
if written != (c.want != "") || got != c.want {
|
||||
t.Fatalf("efforts %v: supported_efforts %q (%v), want %q", c.efforts, got, written, c.want)
|
||||
}
|
||||
if _, ok := e["effort"]; ok {
|
||||
t.Fatalf("efforts %v: a default effort written: %v", c.efforts, e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Sync and a pick keep the keys the user added inside magpie's own entry —
|
||||
// the auth block a gateway shared on the local network or reached from WSL
|
||||
// needs to send a key, its headers, its extra body, a longer idle timeout —
|
||||
// while the endpoint and the whole model list still follow the catalog
|
||||
// (#1103, #1210).
|
||||
func TestAnteSyncKeepsTheUsersKeys(t *testing.T) {
|
||||
const theirs = `{
|
||||
"providers": {
|
||||
"together": { "base_url": "https://api.together.xyz/v1", "wire_style": "OpenAiCompatible" },
|
||||
"magpie": {
|
||||
"display_name": "old",
|
||||
"base_url": "http://127.0.0.1:1/v1",
|
||||
"wire_style": "OpenAiCompatible",
|
||||
"auth": { "bearer": { "env_key": "MAGPIE_KEY" } },
|
||||
"http_headers": { "X-Org": "mine" },
|
||||
"extra_body": { "service_tier": "priority" },
|
||||
"stream_idle_timeout_secs": 900,
|
||||
"thinking_display": "concise",
|
||||
"preferred_models": [
|
||||
{ "id": "deepseek/flash", "user_note": "keep me" }
|
||||
]
|
||||
}
|
||||
},
|
||||
"models": { "claude-opus-5-5": { "effort": "max" } }
|
||||
}
|
||||
`
|
||||
for _, action := range []string{"sync", "pick"} {
|
||||
t.Run(action, func(t *testing.T) {
|
||||
home, path, settings := anteHome(t)
|
||||
a := ante(home)
|
||||
f := a.Field("provider")
|
||||
if err := f.Set("magpie"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
writeFile(t, path, theirs)
|
||||
kept := map[string]string{}
|
||||
for _, k := range []string{"auth", "http_headers", "extra_body", "stream_idle_timeout_secs", "thinking_display"} {
|
||||
kept[k], _ = edit.GetJSON(path, anteProvider+"."+k)
|
||||
}
|
||||
check := func() {
|
||||
t.Helper()
|
||||
for k, was := range kept {
|
||||
if v, ok := edit.GetJSON(path, anteProvider+"."+k); !ok || !sameText(v, was) {
|
||||
t.Errorf("the user's %s lost or changed: got %q, want %q", k, v, was)
|
||||
}
|
||||
}
|
||||
if v, _ := edit.GetJSON(path, anteProvider+".preferred_models"); strings.Contains(v, "user_note") {
|
||||
t.Errorf("preferred_models merged instead of replaced: %s", v)
|
||||
}
|
||||
for k, want := range map[string]string{
|
||||
"providers.together.base_url": "https://api.together.xyz/v1",
|
||||
"models.claude-opus-5-5.effort": "max",
|
||||
} {
|
||||
if v, ok := edit.GetJSON(path, k); !ok || v != want {
|
||||
t.Errorf("%s = %q (%v), want %q", k, v, ok, want)
|
||||
}
|
||||
}
|
||||
// the fields magpie owns still follow the gateway
|
||||
for k, want := range map[string]string{"base_url": gatewayV1(), "display_name": "magpie", "wire_style": "OpenAiCompatible"} {
|
||||
if v, _ := edit.GetJSON(path, anteProvider+"."+k); v != want {
|
||||
t.Errorf("%s = %q, want %q", k, v, want)
|
||||
}
|
||||
}
|
||||
// and every model magpie lists is in Ante's picker, and only
|
||||
// those: flash, gone from the catalog, left the list
|
||||
got := anteModelList(t, path)
|
||||
want := magpieModels("ante")
|
||||
if len(got) != len(want) {
|
||||
t.Fatalf("preferred_models: %v, want %d models", got, len(want))
|
||||
}
|
||||
for i, e := range got {
|
||||
if e["id"] != want[i].ID {
|
||||
t.Fatalf("preferred_models[%d] = %v, want %s", i, e, want[i].ID)
|
||||
}
|
||||
}
|
||||
}
|
||||
if action == "sync" {
|
||||
if err := a.Sync(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
} else if err := f.Set("magpie"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
check()
|
||||
// Ante still starts on magpie, and names no model of
|
||||
// together's beside it — the default of preferred_models, one of
|
||||
// magpie's, is what it starts on
|
||||
if v, _ := edit.GetJSON(settings, "provider"); v != magpieID {
|
||||
t.Errorf("settings' provider = %q, want magpie", v)
|
||||
}
|
||||
if v, ok := edit.GetJSON(settings, "model"); ok && v != "" {
|
||||
t.Errorf("settings' model = %q, want none", v)
|
||||
}
|
||||
// a catalog that says the same as it does is not written again
|
||||
before := readFile(path)
|
||||
stamp := time.Unix(1000000000, 0)
|
||||
if err := os.Chtimes(path, stamp, stamp); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := a.Sync(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if st, err := os.Stat(path); err != nil {
|
||||
t.Fatal(err)
|
||||
} else if !st.ModTime().Equal(stamp) || readFile(path) != before {
|
||||
t.Error("an unchanged catalog was rewritten")
|
||||
}
|
||||
// a model gone from the catalog leaves Ante's list
|
||||
if err := provider.Save(provider.Provider{ID: "deepseek", Name: "DeepSeek", Key: "k",
|
||||
Chat: "https://api.deepseek.com/v1", Models: []string{"pro"}}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := a.Sync(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
check()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// ANTE_HOME moves the folder Ante reads its catalog and its settings from, as
|
||||
// its reference says of it: magpie writes where Ante looks, not beside it in
|
||||
// the home folder (#1210).
|
||||
func TestAnteHome(t *testing.T) {
|
||||
home, catalogPath, settingsPath := anteHome(t)
|
||||
elsewhere := filepath.Join(home, "elsewhere", "ante")
|
||||
t.Setenv("ANTE_HOME", elsewhere)
|
||||
a := ante(home)
|
||||
path := filepath.Join(elsewhere, "catalog.json")
|
||||
settings := filepath.Join(elsewhere, "settings.json")
|
||||
if a.Dir != elsewhere || a.Path != path {
|
||||
t.Fatalf("$ANTE_HOME ignored: dir %q path %q", a.Dir, a.Path)
|
||||
}
|
||||
if got := anteDir(here(home)); got != elsewhere {
|
||||
t.Fatalf("anteDir = %q, want %q", got, elsewhere)
|
||||
}
|
||||
// a relative ANTE_HOME names no folder to write under the working
|
||||
// directory (appdir's rule for every agent's path variable)
|
||||
t.Setenv("ANTE_HOME", "ante-relative")
|
||||
if got := anteDir(here(home)); got != filepath.Join(home, ".ante") {
|
||||
t.Fatalf("a relative $ANTE_HOME was taken: %q", got)
|
||||
}
|
||||
t.Setenv("ANTE_HOME", elsewhere)
|
||||
if err := a.Field("provider").Set("magpie"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, ok := anteProv(t, path)[magpieID]; !ok {
|
||||
t.Fatalf("nothing at $ANTE_HOME: %v", anteProv(t, path))
|
||||
}
|
||||
if v, _ := edit.GetJSON(settings, "provider"); v != magpieID {
|
||||
t.Fatalf("the settings at $ANTE_HOME: %q", v)
|
||||
}
|
||||
// the home folder's own .ante is left exactly as the user had it
|
||||
if b := readFile(catalogPath); strings.Contains(b, magpieID) {
|
||||
t.Fatalf("magpie written beside the home folder: %s", b)
|
||||
}
|
||||
if b := readFile(settingsPath); b != `{
|
||||
"provider": "together",
|
||||
"model": "moonshotai/Kimi-K2-Instruct",
|
||||
"permission_mode": "auto",
|
||||
"theme": "default"
|
||||
}
|
||||
` {
|
||||
t.Fatalf("the home folder's settings touched: %s", b)
|
||||
}
|
||||
// stepping out writes back to $ANTE_HOME, and nowhere else
|
||||
if err := a.Field("provider").Set(""); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, ok := anteProv(t, path)[magpieID]; ok {
|
||||
t.Fatalf("magpie left at $ANTE_HOME: %v", anteProv(t, path))
|
||||
}
|
||||
if b := readFile(catalogPath); strings.Contains(b, magpieID) {
|
||||
t.Fatalf("magpie written beside the home folder on the way out: %s", b)
|
||||
}
|
||||
// A distro's ANTE_HOME is one magpie can't read, so its Ante is ~/.ante
|
||||
// there, as MiniMax Code's mcode (see wsl.go).
|
||||
t.Setenv("ANTE_HOME", elsewhere)
|
||||
wslHome := filepath.Join(home, "wsl")
|
||||
// a distro's place spells its paths as the distro names them (wsl.go),
|
||||
// which is also how magpie knows it can't read the distro's variables
|
||||
in := anteIn(place{home: wslHome, id: "ante@wsl:Distro", spell: func(p string) string { return p }})
|
||||
if want := filepath.Join(wslHome, ".ante", "catalog.json"); in.Path != want {
|
||||
t.Fatalf("a distro's Ante: %q, want %q", in.Path, want)
|
||||
}
|
||||
}
|
||||
|
||||
// Stepping out writes Ante's settings and then its catalog. When the catalog
|
||||
// can't be written, the settings go back as they were: an agent left with
|
||||
// magpie's provider taken out of its catalog, or the other way round, is one
|
||||
// that asks a provider it no longer has. (A catalog that isn't JSON is left
|
||||
// untouched rather than edited by what gjson of it could read — internal/edit
|
||||
// since #1242 — which is how this fails one step in.)
|
||||
func TestAnteWriteFailureRollsBothFilesBack(t *testing.T) {
|
||||
home, path, settings := anteHome(t)
|
||||
a := ante(home)
|
||||
f := a.Field("provider")
|
||||
if err := f.Set("magpie"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
beforeSettings := readFile(settings)
|
||||
if v, _ := edit.GetJSON(settings, "provider"); v != magpieID {
|
||||
t.Fatalf("not wired: %q", v)
|
||||
}
|
||||
bad := `{"providers": {"magpie": {"base_url": "http://127.0.0.1:1/v1"`
|
||||
writeFile(t, path, bad)
|
||||
if err := f.Set(""); err == nil {
|
||||
t.Fatal("a catalog that can't be written was stepped over")
|
||||
}
|
||||
if got := readFile(path); got != bad {
|
||||
t.Fatalf("the broken catalog was edited:\n%s\n---\n%s", bad, got)
|
||||
}
|
||||
if got := readFile(settings); got != beforeSettings {
|
||||
t.Fatalf("settings.json kept half of a change that failed:\n%s\n---\n%s", beforeSettings, got)
|
||||
}
|
||||
}
|
||||
|
||||
// The same pair is written coming the other way: a settings.json that can't be
|
||||
// written leaves the catalog as it was, and Ante is not left started on a
|
||||
// provider magpie took back out.
|
||||
func TestAnteConnectWriteFailureRollsBack(t *testing.T) {
|
||||
home, path, settings := anteHome(t)
|
||||
before := readFile(path)
|
||||
// nothing of it is written over, so the first settings write fails
|
||||
bad := `{"provider": "together",`
|
||||
writeFile(t, settings, bad)
|
||||
if err := ante(home).Field("provider").Set("magpie"); err == nil {
|
||||
t.Fatal("a settings.json that can't be written was stepped over")
|
||||
}
|
||||
if got := readFile(path); got != before {
|
||||
t.Fatalf("the catalog kept half of a change that failed:\n%s\n---\n%s", before, got)
|
||||
}
|
||||
if _, ok := anteProv(t, path)[magpieID]; ok {
|
||||
t.Fatalf("magpie's entry stayed beside settings magpie never wrote: %v", anteProv(t, path))
|
||||
}
|
||||
if got := readFile(settings); got != bad {
|
||||
t.Fatalf("the broken settings.json was edited:\n%s\n---\n%s", bad, got)
|
||||
}
|
||||
}
|
||||
|
||||
// A catalog Ante can't parse costs magpie's provider, not the user's: Ante
|
||||
// skips an entry that doesn't deserialize and loads the rest, and magpie
|
||||
// writes only its own key.
|
||||
func TestAnteKeepsTheUsersFile(t *testing.T) {
|
||||
home, path, _ := anteHome(t)
|
||||
a := ante(home)
|
||||
if err := a.Field("provider").Set("magpie"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
ps := anteProv(t, path)
|
||||
if _, ok := ps["together"]; !ok {
|
||||
t.Fatalf("the user's provider went: %v", ps)
|
||||
}
|
||||
if _, ok := ps[magpieID]; !ok {
|
||||
t.Fatalf("magpie's provider went: %v", ps)
|
||||
}
|
||||
}
|
||||
|
||||
// A routing group says neither what it reasons at nor what it takes in: which
|
||||
// member answers, and what that member can do, is the group's own to decide
|
||||
// per turn. magpie's other agents leave both off for one, and Ante would
|
||||
// otherwise filter images out before a group whose member takes them, or ask
|
||||
// for an effort the member the group picked doesn't have.
|
||||
func TestAnteGroupSaysNoCapabilities(t *testing.T) {
|
||||
home, path, _ := anteHome(t)
|
||||
// a group of magpie's own, over a model of a provider of the user's
|
||||
provider.Save(provider.Provider{ID: "relay", Name: "Relay", Key: "k",
|
||||
Chat: "http://127.0.0.1:1/v1", Models: []string{"m1"}})
|
||||
if err := provider.SaveGroup(provider.Group{ID: "g", Name: "G", Members: []string{"relay/m1"}}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
a := ante(home)
|
||||
if err := a.Field("provider").Set("magpie"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
var sawGroup bool
|
||||
for _, e := range anteModelList(t, path) {
|
||||
id, _ := e["id"].(string)
|
||||
if !strings.HasPrefix(id, provider.GroupPrefix) {
|
||||
continue
|
||||
}
|
||||
sawGroup = true
|
||||
for _, k := range []string{"effort", "supported_efforts", "support_vision"} {
|
||||
if _, ok := e[k]; ok {
|
||||
t.Fatalf("the group %s declares %s: %v", id, k, e)
|
||||
}
|
||||
}
|
||||
}
|
||||
if !sawGroup {
|
||||
t.Fatalf("no group in the list: %v", anteModelList(t, path))
|
||||
}
|
||||
}
|
||||
@@ -392,6 +392,10 @@ var wslKinds = []wslKind{
|
||||
}},
|
||||
{id: "empryo", name: "Empryo", dir: ".empryo", bin: "empryo", in: empryoIn,
|
||||
restart: "reads its config at start-up — restart open empryo sessions to use this."},
|
||||
// Ante ships macOS and Linux builds alone, and suggests WSL on Windows,
|
||||
// so a Windows machine's Ante is usually this one
|
||||
{id: "ante", name: "Ante", dir: ".ante", bin: "ante", in: anteIn,
|
||||
restart: "reads its catalog at start-up — restart open ante sessions to use this."},
|
||||
{id: "muse", name: "Muse Code", dir: ".config/muse", bin: "muse", in: museIn,
|
||||
restart: "reads its settings at start-up — restart open muse sessions to use this."},
|
||||
{id: "qoder", name: "Qoder", dir: ".qoder", bin: "qodercli", in: qoderIn,
|
||||
|
||||
@@ -51,6 +51,8 @@ var Vars = []string{
|
||||
"GROK_HOME", "GROK_BIN_DIR",
|
||||
// Kimi Code and its shared folder
|
||||
"KIMI_CODE_HOME", "KIMI_SHARE_DIR",
|
||||
// Ante
|
||||
"ANTE_HOME",
|
||||
// MiMo Code, MiniMax Code, OpenHanako, Hermes, dsh, WorkBuddy, CodeBuddy Code
|
||||
"MIMOCODE_HOME", "MINIMAX_DATA_DIR", "HANA_HOME", "HERMES_HOME", "DSH_HOME",
|
||||
"WORKBUDDY_CONFIG_DIR", "CODEBUDDY_CONFIG_DIR",
|
||||
|
||||
Reference in new issue
Block a user