The Context tab now replays the period's calls of each agent against the
settings that decide what they cost, and shows, per agent and per model,
the value that would have used the fewest tokens for how this user works:
Claude Code's auto-compact window per model (modelSettings.<model>.
autoCompactWindow), its main and subagent prompt cache lifetimes
(promptCacheTtl, subagentPromptCacheTtl: 5m or 1h), and Codex's
model_auto_compact_token_limit. Each row says where the value comes from
now (the agent's default, the user's own setting, one magpie set, or a
variable that comes first and locks it), why (how fast conversations grow,
what a compaction leaves, how long the user pauses), the cost curve, and
the saving. Nothing is written until "Use …" is clicked; what was there is
stashed and Undo puts it back, unless the user changed it since. A window
the user set past the model's own is shown capped, as Claude Code caps it
(min(setting, model window)), so no saving is claimed that wouldn't happen.
Advice under a 3% saving is "already the leanest".
On the owner's 7 days (88K Claude Code calls, 956 Codex): opus keeps 200K
(their 500K is capped to it), fable 5.1 200K → 160K saves 5% (2.56M
tokens), both cache lifetimes and Codex's 272K are already the leanest.
Run: go vet ./... and go test ./... pass (internal/tune, internal/agent
TestTune*: user-set values kept, env locks, undo after a user edit, TOML
key order). context-tune.test.cjs passes in Chromium and WebKit, en/zh/
zh-TW/ja/de, at 360 and 1100px (22); context-window, context-scroll-end
pass. Full GUI suite: library-servers-bulk and market-have failed once
under load and pass alone; market-have fails on origin/main 494dedcf too
(1 of 8 runs). Claude Code 2.1.296 in a sandbox config dir shows
"Auto-compact window for Fable 5.1: 160k tokens (from settings)" for the
written modelSettings key. The real-data advice was read from the owner's
own ~/.claude and ~/.codex, nothing written.
22 KiB
22 KiB
Agent wiring
magpie connects a coding agent to its gateway by editing the agent's own
config. That covers the endpoint, the key magpie-<agent> and the model,
in the agent's own format. When the agent is disconnected, magpie puts back
what the agent had before. Package internal/agent describes every
supported agent: where its config lives, which fields matter and which
values to offer. It also records what magpie set, so a later change made by
something else shows up as drift.
Responsibilities and sources of truth
| Part | Responsibility | Source |
|---|---|---|
| Agent description | Agent has an id, name, icon, Bin and Dir for detection, Path, Fields and Native. It also has optional hooks: Notice, UA, ListsModels, Sync, Unwire, Join/Joined, Routed, Follow, RenameRefs, Check, LastUsed, Reached, Import/Added, Launch, SplitSuffix and Spelled. Each hook's comment says which agents need it. |
agent.go |
| Fields and options | A Field has Get, Set and Options. Set("") returns the field to the agent's default and takes magpie's wiring out. Quiet fields follow another field until set (Follows, such as Claude Code's tiers). An Option carries the picker's facts: Direct, Via, Same, Alias, Rate and Context. |
agent.go, route.go |
| The list of agents | All lists every agent magpie knows, then the WSL agents (wslAgents). Clients lists every agent whose requests the gateway knows by name, which usage.Agents reads. One file per agent: claude.go, codex.go, opencode in agents.go, and so on. |
agents.go |
| Connect | ConnectHow picks the model an agent starts on and reports how. How is one of: kept, again, joined, magpie, same, alike, default or first (#726). With no models to give, it returns NoModelsError (code no_models). |
applied.go |
| What magpie set | Apply sets a field and records it in applied.json. Wired says whether magpie is in the config. Drift reports unwired, replaced, unreachable or bypassed. Reapply sets the record again. Keep accepts the config as it is now. |
applied.go |
| What came before | The stash (stash.json) remembers what the config said before magpie pointed it at the gateway, so Disconnect restores it rather than guessing. |
stash.go |
| Disconnect and preview | Disconnect runs Unwire, then returns each field still on magpie to its default. DisconnectPreview runs the real Disconnect on a copy under a temporary home, in a subprocess (DryRunArg), and diffs the files. The preview is never a second account of what Disconnect does. |
applied.go, preview.go |
| Keeping configs current | Reseat moves agents off a model magpie no longer serves (#200). OnGateway and Rewire write the new gateway address into every connected agent when the port changes. Source and Stale say what an unconnected agent runs on, and whether running copies still use the old list. |
reseat.go, rewire.go, connectinfo.go |
| Named omp profiles | omp keeps a profile's settings under ~/.omp/profiles/<name> (or under PI_CONFIG_DIR), each with its own config.yml and models.yml. Every such profile is an agent of its own — omp#<name> on Windows, omp#<name>@wsl:<distro> in a distro — so each can be connected, drifted and disconnected on its own. They share omp's model lists (ListsFor), and the default omp row is the folder ompDir/ompIn already pick, so an OMP_PROFILE that row is already pointed at is not listed twice. A name omp refuses is not a row: default, a trailing dot, or a Windows reserved device name (aux, nul, com1, lpt9.x, con.work), on every platform (ompProfileOK). A distro's profile names come from its probe and are kept in wsl.json; a stopped distro is never started to list them, its profile rows showing what was last seen (asleep). |
omp.go, wsl.go |
| WSL twins | On Windows each running distro is probed once, without starting a stopped one. The wslKinds agents found there become agents of their own, <id>@wsl:<distro>, edited through \\wsl.localhost\<distro>. A kind is found by its folder under the distro's home or by its command there (not one on a Windows drive). Where another tool shares one of them, the kind leaves it out: Gemini CLI is found by its command alone, because Antigravity keeps ~/.gemini too. Goose is found by its folder alone, because a goose command can be pressly's migration tool. Reasonix also needs the version its command gives (accept), because the old npm client is named reasonix too. The gateway address and key follow the distro's networking: mirrored or NAT (place, keyAt). ListsFor keeps a twin's model picks under the Windows twin's id (#927). |
wsl.go, gatewaykey.go |
| Settings tuned from use | Usage › Context's "Tuned for you" advises, per agent, the settings that would have spent the fewest tokens on the user's own calls of the range, and puts one in on a click. Package tune replays the calls sessions.Calls reads from the agents' session files under each candidate value (Compact, CacheTTL); tuneOf picks the agents and models (Claude Code per model, Codex pooled). TuneRead, TuneApply and TuneUndo know where each setting lives: Claude Code's modelSettings.<model>.autoCompactWindow (where its /autocompact saves), promptCacheTtl and subagentPromptCacheTtl in ~/.claude/settings.json; Codex's top-level model_auto_compact_token_limit. |
internal/tune, tune.go, internal/gui/tune.go |
| Environment-only agents | Agents that read the gateway only from the environment get a Launch command. Cursor Private Inference instead gets user-level variables (cursorLocalOn, KeepCursorLocalEnv), and magpie writes none of Cursor's own files. Its effort field is kept in magpie (provider.AgentEffort) and asked by the gateway, its levels those of the models it lists (cursorLocalEfforts, #1003). |
cursorlocal.go |
Runtime path
- Connect. The Agents page's switch calls
ConnectHow(POST /api/agents/{action}/{id}ininternal/gui/api.go). It stashes the current values (<id>.connect.was). It brings back the models used before a switch-off (reconnect), or callsJoinwhere the agent keeps its own model (Codex signed in with ChatGPT). Otherwise it picks a model by the orderConnectHowdocuments and sets it throughApply. - Pick.
Picksets a field from the model picker. Picking one of magpie's models on an agent that isn't connected connects it first. - Notice. After a change the page shows
Noticewhen the agent reads its config only at start-up and needs a restart. - Drift. As the Agents page is drawn,
Driftcompares the config withapplied.json. It runsCheckfirst, then each field. When the config is right and points at an address off loopback (a WSL twin under NAT, or this machine's Codex at an address the user kept, #816), it tries that address (reach.go): in the background, at most every 20 s, never waited for. When nothing answers there while the gateway answers on loopback, the drift isunreachable(#1013), with what has to listen there: Share on local network, the user's portproxy, Windows' firewall. Then it checks whether the agent's latest use (LastUsed,Reached) reached the gateway. - Disconnect.
Disconnectputs back what the stash kept and forgets the record.
Constraints and failure behavior
- magpie only removes what it wrote. Anything else in an agent's config stays as it was.
- JetBrains Air's
magpie-opencode.jsonkeeps user-added fields inprovider.magpie, including nested options, on sync and model picks (airinair.go, usingtheirsKeptinsync.go). The gateway fields and complete model list are still replaced, so removed models leave the list. - Copilot in JetBrains IDEs keeps its Bring Your Own Key providers in the Copilot language server's
byok.json, shared with Xcode's and Eclipse's Copilot (copilotJetBrainsincopilotjetbrains.go). magpie writes its own three keys there,magpie-provider-config,magpie-api-keyandmagpie-models-config, in the bytes the server itself writes, and only when they differ; the user's providers and keys stay. A magpie model the user hid in Manage Models (isRegistered: false) stays hidden through a sync. A new file is made 0600 in a 0700 folder, as the server makes it. - Ante's
catalog.jsonkeeps user-added fields inproviders.magpiethe same way (anteinante.go); itsauth,http_headers,extra_bodyandstream_idle_timeout_secsare what a gateway shared on the local network or reached from WSL needs to send a key. Ante starts a session on theproviderandmodelofsettings.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/usagesays it isn't allowed, and it has no credits and isn't a workspace still within its overage (codexHeld, read byprovider.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.outmarks 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.
- A field that follows another while empty (
Follows: Claude Code's tiers and subagents on its main model) is read byDriftas on that field's model. The main model moved, in Claude Code's own/model, onto the model magpie had set a tier to is not drift (#1050). - A Claude Code tier or its subagents given a model of their own keep it when the main model moves onto that model and on again (
ownKeyinclaudeIn), whether the main model is picked in magpie or in Claude Code's/model. Same as model, picked for them, has them follow again. - Claude Code's tier and subagent models the user had set for their own endpoint (
claudeOwnEnv) are stashed with that endpoint and token when magpie is wired in, and come back with them when magpie steps out, by Disconnect or by Claude Code's own model (#1050). - Claude Code's sign-in (
login) is magpie's key ("", the default) or claude.ai (claudeai). claude.ai writesANTHROPIC_AUTH_TOKENempty, which Claude Code reads as no key: it keeps its claude.ai login and sends that to magpie, which never passes it on (claudeSignInin the gateway). Every later write keeps the token empty (wiredKey), and a key put there by hand is reported by Check, not taken for magpie's. It's offered only where the gateway takes any key (gwKey() == gateway.Token: loopback, a WSL distro mirrored), not from a distro under NAT while magpie is shared on the network, where only the sharing key gets in. The choice is stashed, so it survives Disconnect and a port move. An empty token counts as magpie's only beside the address magpie last wrote (claude.wired_atin the stash, which Rewire carries to the new port,keyOurs): an endpoint of the user's own with an empty token stays theirs when the model goes back to Claude Code's own. - MiniMax Code takes a model ref with one slash in the whole (a SubAgent's
modelmust match^[^/\s]+\/[^/\s]+$, #1387), and magpie's refs there arecustom_provider:magpie/<model>. So a model undercustom_provider.magpieis written with~for each slash of its catalog id (bb-codex~gpt-6.1-sol,provider.FlatID), only when that spelling stands for one served id (provider.Flattened); the gateway takes it back. A config an older magpie wrote moves over on the next sync: the user's keys on a model go to its new key,defaultModel,defaultLightModelandmemory.captureModel/consolidationModelfollow (mcodeRefs), and the slashed key stays, switched off (enabled: false), so a session begun on it still resumes. magpie never edits the user's ownagents/<name>/agent.md. No other agent was found with this limit (minimax.go). - Codex's "subagents" field is
[agents] default_subagent_model: only the model a subagent starts on when its lead names none inspawn_agent(codex-rscore/src/agent/child_config.rs). Unset, it isn't "same as model": the lead may still name another, so the GUI calls it the lead's pick, else the model (leadPicksin app.js), and its title points at the "subagent model" field. - Codex's "subagent model" field writes nothing to Codex's config: its lead names each subagent's model in
spawn_agent, which[agents]can't overrule. It is magpie's setting (settings.CodexSubagentModel,codex_subagent.go), and the gateway moves the subagent's request onto it (see gateway routing). Its options are only ChatGPT accounts' models, as a subagent's task is sealed for them. Its setter keeps a ChatGPT account's model no account serves now, so a profile or backup put back on another computer still applies (ApplyFieldsstops at the first failed Set), and refuses any other provider's model. - Snow CLI runs on one profile at a time (
profiles/<name>.jsonin~/.snowor$SNOW_CONFIG_DIR, named inactive-profile.json) and copies the active one overconfig.jsonat every start,snow --asktoo (snow.go). magpie writes amagpieprofile, makes it the active one, and copies it toconfig.jsonitself, so a Snow already running and Snow App's "Sync Snow CLI API config" see the same. It is made from the profile the user was on, their settings kept and its OAuth sign-in left out. The profile they were on (or that there was noactive-profile.json) and amagpie.jsonof their own are stashed and put back by Disconnect. A Snow from before profiles (config.jsonalone) gets thedefault.jsonSnow itself would have made of it, so its next start doesn't take magpie's for the user's. Picking a model of another of the user's profiles switches Snow to that profile. - A value an agent spells as its own provider's model is never taken for one of magpie's, even when magpie has a provider of the same name (
Spelled, #835). unreachablenever sets anything: the config is right, and the page's pill only says what has to listen. The exception is an address a WSL distro reached Windows at before (distro.Was, kept inwsl.json) while it reaches Windows at another now: the pill offers that one, andReapplymoves the config there (Agent.move, Codex'scodexMoveGateway). An address no distro was seen at is the user's own and is never moved. An unknown answer (no probe yet) and a gateway down on loopback too are not drift. Tests stubreachProbeinTestMain.- Listing never starts a stopped WSL distro. A stopped distro's agent shows what was last seen there (
asleep,wsl.json); setting one of its fields writes its files, which starts the distro. - Codex's sign-in (
login) is ChatGPT ("", the default), Always ChatGPT (chatgpt) or magpie API (api). With ChatGPT, magpie's models join Codex's own beside the sign-in (openai_base_url), and while the Codex app holds the ChatGPT account Codex is signed in to (codexUsedUp,provider.CodexUsedUp: OpenAI no longer allows the account and it has no credits left, a workspace still within its overage excepted), magpie is made Codex's provider (codex.out): the Codex app sends nothing at all on an account it holds, a magpie model's turn included (#540), and as a provider of its own magpie is past that. A window at 100% with credits left doesn't count, and Codex stays signed in. Sync puts it back beside the sign-in once the account has room. Always ChatGPT never does that: Codex stays in its ChatGPT state, and the Codex app may send nothing till the account has room, while Codex CLI's turns still reach magpie by the base URL, and its routing and other accounts serve them. - Codex signed in to ChatGPT and not connected still gets
openai_base_urlwhile more of its ChatGPT accounts are on in magpie (failover, stashcodex.failover; with a CC Switch mirror as its provider, the mirror'sbase_urlandcodex.mirror_failover), so a turn its own account has no allowance for goes on to the next. That Codex isFailingOver: notWired, yet its requests go through magpie. The Agents row says so and what turns it off (FailoverSaidin the CLI and TUI,failoverin/api/state, a button to Providers › Codex in the GUI), and the gateway hands it only its own models from/backend-api/codex/models(CodexOwnOnly, set ongateway.CodexOwnOnlyby main; its ETag carriesprovider.CodexOwnListTag, and Sync ages amodels_cache.jsonwithout it). A connected WSL Codex shares the gateway path, so while one is connected every Codex gets magpie's models (#1385). A base URL the user wrote by hand (#856) isn't failover's and stays a plain not-connected row. - An agent's config is written in the form the installed version reads, taken from what that version declares, not the latest one. VSCodium has no Copilot Chat of its own, and the Marketplace's last one (0.48.1) has no Custom Endpoint provider, only OpenAI Compatible (
customoai, #1014). magpie reads the provider list in the package.json of thegithub.copilot-chatinstalled under~/.vscode-oss/extensions(vscodeChatOf). It writes its group forcustomendpointwhen that is declared, forcustomoaiwhen only that is, with each model's window asmaxInputTokens+maxOutputTokensand the token asx-api-key(that version drops an Authorization inrequestHeaders). A missing or unreadable extension keepscustomendpoint. Sync rewrites a group written for the other provider, and Check reports it, as it does a Copilot Chat declaring neither. That Copilot Chat registers its providers only while signed in to GitHub with a personal Copilot plan, which the Notice says. - Tuned settings are written only on the user's click, never by Sync or Connect. The value there before is stashed once (
tune.<agent>.<knob>[.<model>]instash.json), so Undo puts it back, or takes the key out (and amodelSettingsentry left empty). A value the user changed after the click is theirs: Undo then only forgets the stash. A variable that comes before the setting (CLAUDE_CODE_AUTO_COMPACT_WINDOW,FORCE_PROMPT_CACHING_5M,CLAUDE_CODE_PROMPT_CACHE_TTL,CLAUDE_CODE_SUBAGENT_PROMPT_CACHE_TTL, in settings.json'senvor magpie's own) is shown aslockedand Apply refuses, since the setting would change nothing. Claude Code compacts at the smaller of the window set and the model's own (its/autocompactsays "capped to 200k by model"), so advice starts from that, and a model that isn't Claude's has no window here (magpie's own compact wiring serves it). Advice changes a value only when the replay saves 3% or more, and of the windows within 1% of the cheapest takes the largest. Tokens are weighed as billed: cache reads 0.1, 5-minute writes 1.25, hour writes 2, output 5 for Claude; reads 0.1, writes 1, output 8 otherwise. - Tests never touch the real machine.
TestMainusestestenv, which points HOME, USERPROFILE, the XDG folders and APPDATA into a temporary folder, clearsagentenv.Varsand puts failing stand-ins for keychain tools and agent CLIs on PATH. Agent-specific side effects (Aside, the Zed keychain, Codex's usage check) are also stubbed inTestMain.
Verification
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'
go test -tags nogui ./internal/agent -run 'TestCodexFailoverOnlyKeepsOwnModels|TestCodexOwnModelThroughMagpieWhileAccountsOn|TestCodexHandWrittenBaseURLStays'
go test -tags nogui ./internal/tune ./internal/agent -run 'TestCompact|TestCacheTTL|TestTune'
node --test internal/gui/tests/context-tune.test.cjs
node --test internal/gui/tests/agent-connect.test.cjs internal/gui/tests/agent-disconnect-preview.test.cjs internal/gui/tests/claude-signin.test.cjs internal/gui/tests/agent-failover.test.cjs