* fix(gui): keep provider model list position after level change * gateway: a Claude Code run whose subagent ended on StructuredOutput is let go, and runs left waiting on tool calls are bounded (#345) — a workflow's subagents end on StructuredOutput, which the client answers itself, so the bridge's claude -p waited 30 minutes for a result that never came, one process per subagent. Such a run now waits 30s (for a schema error the client sends at once) and ends; of other runs waiting 5 minutes or more on their caller's tools, 16 are kept, the longest waiting let go first (one that then gets its results starts anew with the whole conversation). TestClaudeRunEndsAfterStructuredOutput and TestClaudeParkedRunsAreBounded fail without the change. go vet, go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui ./... pass. * plugins: a built-in moved onto its plugin looks and works as it did (你确保插件版的订阅 provider 和其对应的内置的…都保持一模一样了吗?) — moved onto its plugin, a built-in lost pieces: its tile's name, icon, plans and risk note went to the plugin's own, "Add account" still opened the built-in's sign-in (in the app, on the server and in `magpie accounts add`), an agent's own sign-in the built-in still found showed a second usage card of the same account, the account's plan badge went, WorkBuddy's daily check-in stopped, the provider's icon and site were the plugin's, WorkBuddy AI's card was named WorkBuddy, and Devin, which had no card of its own, lost the plugin's. Now a moved built-in keeps its tile and risk sheet and signs in through its plugin; its cards take the built-in's place and name, and it has none of its own; the plan the plugin tells stays with the account; WorkBuddy checks in through the plugin, which signs the call; a card is left out only where its built-in's slot showed it. TestMovedBuiltinQuiet, TestWorkBuddyCheckinThroughPlugin, TestMoveToPlugin (Devin-like card) and TestPluginUsage (plan kept) fail without the change. Command Code and Devin moved and back in a sandbox HOME: one card each, plan, name. go vet, go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui ./... pass; plugin-move, plugin-signin, commandcode-risk, old-webkit pass; click-scroll's WebKit fails the same on origin/main under this load. * provider: the Providers and Gateway pages no longer wait on `claude --version` (deluxe on Discord: on Omarchy the two pages never loaded) — on Omarchy `claude` is a mise wrapper that installs or updates Claude Code before it answers, which on a slow network takes minutes or never ends; magpie asked it with no limit and with a lock held, so /api/providers (fetching the Claude account's models) and every Claude request waited with it. The CLI is now asked in the background, given a minute, and a caller waits at most 3s before the version it has stands in; a late answer is taken once it comes. Reproduced on an Omarchy VM (/api/providers unanswered after 100s on v0.1.516, 3.7s with the change). TestClaudeVersionDoesNotWaitForAStalledCLI and TestClaudeVersionTakenWhenItComes fail without the change. go vet, go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui ./... pass. * plugins: a plugin's account behaves as its built-in's (the user: 插件版的订阅 provider 和内置的…保持一模一样了吗) — a plugin's failed sign-in now says why ("the sign-in failed: the sign-in page expired") instead of a bare failure; a plugin account the provider refuses with 401 is marked lapsed, "<user>'s <provider> sign-in has expired — sign in again", beside the account and on its usage card as a built-in's is, and the mark goes when it answers again or is signed in anew; a Devin or Command Code account taken from the agent's CLI moves to the plugin marked as the CLI's and back as the agent's own, so the one it follows stays the one the CLI is signed in to. TestPluginProvider (lapsed), TestMoveSideState (cli) and TestPluginSignIn (reason) fail without the change. go vet, go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui ./... pass. * agents: "Hide all" in an agent's model list keeps its line, and an uninstalled Gemini CLI goes from the Agents page (#356, #230) — the line under an agent ("Showing 0 / N models") came only when a picker still listed one of magpie's models, so hiding them all took away the one way back; it now stays while any is hidden. Gemini CLI was taken as present from what it leaves in ~/.gemini (settings.json, .env, installation_id…), which stays after it is uninstalled, so its row stayed, offering to set up a CLI that can't run; it is now its gemini binary. TestAgentModelLineAllHidden and TestGeminiNotAntigravity fail without the change; agent-models-all-hidden.test.cjs (Chromium, WebKit, en/zh) added. go vet, go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui ./internal/agent ./internal/gui ./internal/library ./internal/provider pass; agent-models, click-scroll, old-webkit pass. * gateway: Codex's Responses Lite tools reach SuperGrok and every other backend, and a group moves on when a member can't read a request (#350) — Codex sends a Lite model's tools as an additional_tools input item, its own functions in a "functions" namespace; magpie relayed the item to xAI, which 422s on it (unknown item type), and translated routes (Cursor, Anthropic, Chat…) dropped it, so the model had no tools. Outside OpenAI the item's tools now go in tools, the functions namespace opened into plain tools and earlier calls un-namespaced; OpenAI and the ChatGPT account still get Codex's own form. A 400/422 over the request's shape (failed to deserialize, unknown item type/parameter…) now moves to the group's next member, each asked once, the member not resting; the Routing page says so. TestAdditionalToolsRelayed, TestAdditionalToolsTranslated and TestGroupFailsOverARequestShape fail without the change; routing-shape.test.cjs (Chromium, WebKit, en/zh) added. go vet, go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui ./... pass; click-scroll, old-webkit, routing-wb-refused pass. * agents: omp's roles pick their own models (subagents, smol, slow), keep their thinking level, and get back what they had before magpie (#325) * agents: omp's subagents and small role pick their own models, through magpie too * agents: omp keeps a role's thinking level and the model it had before magpie, and offers the slow and designer roles A role set to "anthropic/claude-opus-5:max" and switched to a magpie model lost its ":max", and resetting it deleted the key rather than putting the user's model back. The level now goes with the new model (omp clamps one the model lacks, never fails), the model the role had before magpie is stashed and restored, and the reviewer's @slow and the designer's @designer roles are fields beside subagents and small. * gui: omp's slow and designer roles follow the model, as its subagents do Unset, both roles take the session's model, yet the picker offered them omp's Default and no typed model. They are now squares beside subagents, with Same as model first and a model typed in taken as it is. * agents: omp's thinking level no longer hides a role's model from magpie, and the model alone clears it — magpie omp magpie/x/y:high is taken, a role moved back to omp's own is drift, a list of models comes back on reset, and designer (gone in omp 18.1.5) goes A role on "magpie/deepseek/pro:max" matched the picker only through the option offered for its level beside the model, and only for the value in the file now. So the CLI refused a model typed at a level, a recorded value at a level the current one didn't share wasn't known for magpie's (no "replaced" drift), and a level could never be taken away: a model picked without one took the old one on. An agent may now say what it keeps after the model (Agent.SplitSuffix; omp's is its level). Drift, Reseat, RenameProvider, Spell and the CLI's field-for-value match the model and put the suffix back after the one they move to; agents without it are unchanged. The option at the level stays: it keeps the level. Another model keeps it too; the same model picked alone clears it. A role omp falls back through (a YAML list, or the comma list omp joins it into, settings.ts modelRoleValueFromUnknown) read as nothing, so it wasn't stashed and a reset deleted it. It reads as that comma list now, is stashed and comes back; a list has no level of its own. omp 18.1.5 removed the designer role (CHANGELOG), so its field goes. The smol role, like task and slow, gives its agents the session's model when unset; its field is labelled smol, as omp names it, so the GUI can show it as the others without touching other agents' "small". * gui: omp's smol role follows the model as its subagents and slow role do, and designer goes smol unset gives omp's scout and sonic the session's model, so it is a square after the pickers with "Same as model" first, as subagents and slow are (a feather). Its label is smol, not small: OpenCode's, Crush's and OpenChamber's small model stays a picker. designer, a role omp 18.1.5 removed, leaves FOLLOWS_MODEL, the glyphs and i18n. * agents: the note on omp's fallback chains says their keys are roles, and from omp 18 models too ompRefKeys' comment had retry.fallbackChains keyed by model. omp 16.3.5 takes only a role's name there (session/agent-session.ts: "a mapping of role names to selector arrays"); omp 18.4.4 takes a role's name or a model selector (session/retry-fallback-chains.ts). Walking the keys stays: a role's name is no magpie model and is passed over. Follow-up to the review of #340. * agents: omp reads whether it is on magpie one way, and a role at a level that a fallback chain names too is renamed once in each Rebased on #340, one scan over omp's config (ompRefs) answers both questions: magpie's provider stays in models.yml while anything under ompRefKeys is on magpie, as #340 has it, and Check looks at the wiring while any role under modelRoles is, as this PR had it; a fallback chain or enabledModels alone keeps the provider without being checked. A provider renamed moves a role at a thinking level through SplitSuffix first, then RenameRefs the rest, which finds no old name left in the roles: each place moves once, the level kept. * agents: a fallback chain alone on magpie has omp's wiring checked, and a role set off magpie mends it Check now reads the same keys dropMagpie does (ompRefKeys: every role, retry.fallbackChains, enabledModels, task.agentModelOverrides): whatever keeps magpie's provider in models.yml has its baseUrl checked, so a chain on magpie whose wiring something else moved no longer fails its fallback silently. Applying again sets the model role, which may be omp's own; a role set off magpie while something else still names it now writes magpie's provider afresh instead of leaving it as it is, so that mends the wiring. Review of #325. * agents: an omp role that is a list of models stays the user's own, even starting on one of magpie's, and comes back as it was written A role omp falls back through ([magpie/deepseek/flash, openai/gpt-6], or the same as a comma-separated string) was taken for one of magpie's models because it started with magpie/: magpie taking the role over stashed nothing, and a reset deleted it. Only one model, no comma, is magpie's now: SplitSuffix answers whether a value is one model, and Spell, drift (magpieValue), Reseat, RenameProvider and the CLI's fieldForValue leave a list alone. The stash keeps a YAML list as the YAML it was written in (edit.GetYAMLText), and a reset puts it back so, flow or block; a comma-separated string comes back a string. A provider renamed moves the names inside a list through RenameRefs, once, and in the lists the stash keeps too. Review of #325. * gui: a test holds omp's subagents, smol and slow roles as squares that follow the model, and OpenCode's small model a picker omp-roles.test.cjs, in Chromium and WebKit, English and Chinese, API faked: the three roles are squares after the model's picker, unset named "same as model" (「同主模型」) and set naming their model; smol's picker opens on "Same as model" with the model beside it, and a pick sends it and shows it. OpenCode's small model stays a picker. With smol and slow taken out of FOLLOWS_MODEL it fails at "small is a square". Review of #325. * gateway: a Claude Code run whose subagent ended on StructuredOutput is let go, and runs left waiting on tool calls are bounded (#345) — a workflow's subagents end on StructuredOutput, which the client answers itself, so the bridge's claude -p waited 30 minutes for a result that never came, one process per subagent. Such a run now waits 30s (for a schema error the client sends at once) and ends; of other runs waiting 5 minutes or more on their caller's tools, 16 are kept, the longest waiting let go first (one that then gets its results starts anew with the whole conversation). TestClaudeRunEndsAfterStructuredOutput and TestClaudeParkedRunsAreBounded fail without the change. go vet, go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui ./... pass. * plugins: a built-in moved onto its plugin looks and works as it did (你确保插件版的订阅 provider 和其对应的内置的…都保持一模一样了吗?) — moved onto its plugin, a built-in lost pieces: its tile's name, icon, plans and risk note went to the plugin's own, "Add account" still opened the built-in's sign-in (in the app, on the server and in `magpie accounts add`), an agent's own sign-in the built-in still found showed a second usage card of the same account, the account's plan badge went, WorkBuddy's daily check-in stopped, the provider's icon and site were the plugin's, WorkBuddy AI's card was named WorkBuddy, and Devin, which had no card of its own, lost the plugin's. Now a moved built-in keeps its tile and risk sheet and signs in through its plugin; its cards take the built-in's place and name, and it has none of its own; the plan the plugin tells stays with the account; WorkBuddy checks in through the plugin, which signs the call; a card is left out only where its built-in's slot showed it. TestMovedBuiltinQuiet, TestWorkBuddyCheckinThroughPlugin, TestMoveToPlugin (Devin-like card) and TestPluginUsage (plan kept) fail without the change. Command Code and Devin moved and back in a sandbox HOME: one card each, plan, name. go vet, go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui ./... pass; plugin-move, plugin-signin, commandcode-risk, old-webkit pass; click-scroll's WebKit fails the same on origin/main under this load. * provider: the Providers and Gateway pages no longer wait on `claude --version` (deluxe on Discord: on Omarchy the two pages never loaded) — on Omarchy `claude` is a mise wrapper that installs or updates Claude Code before it answers, which on a slow network takes minutes or never ends; magpie asked it with no limit and with a lock held, so /api/providers (fetching the Claude account's models) and every Claude request waited with it. The CLI is now asked in the background, given a minute, and a caller waits at most 3s before the version it has stands in; a late answer is taken once it comes. Reproduced on an Omarchy VM (/api/providers unanswered after 100s on v0.1.516, 3.7s with the change). TestClaudeVersionDoesNotWaitForAStalledCLI and TestClaudeVersionTakenWhenItComes fail without the change. go vet, go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui ./... pass. * plugins: a plugin's account behaves as its built-in's (the user: 插件版的订阅 provider 和内置的…保持一模一样了吗) — a plugin's failed sign-in now says why ("the sign-in failed: the sign-in page expired") instead of a bare failure; a plugin account the provider refuses with 401 is marked lapsed, "<user>'s <provider> sign-in has expired — sign in again", beside the account and on its usage card as a built-in's is, and the mark goes when it answers again or is signed in anew; a Devin or Command Code account taken from the agent's CLI moves to the plugin marked as the CLI's and back as the agent's own, so the one it follows stays the one the CLI is signed in to. TestPluginProvider (lapsed), TestMoveSideState (cli) and TestPluginSignIn (reason) fail without the change. go vet, go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui ./... pass. * agents: "Hide all" in an agent's model list keeps its line, and an uninstalled Gemini CLI goes from the Agents page (#356, #230) — the line under an agent ("Showing 0 / N models") came only when a picker still listed one of magpie's models, so hiding them all took away the one way back; it now stays while any is hidden. Gemini CLI was taken as present from what it leaves in ~/.gemini (settings.json, .env, installation_id…), which stays after it is uninstalled, so its row stayed, offering to set up a CLI that can't run; it is now its gemini binary. TestAgentModelLineAllHidden and TestGeminiNotAntigravity fail without the change; agent-models-all-hidden.test.cjs (Chromium, WebKit, en/zh) added. go vet, go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui ./internal/agent ./internal/gui ./internal/library ./internal/provider pass; agent-models, click-scroll, old-webkit pass. * gateway: Codex's Responses Lite tools reach SuperGrok and every other backend, and a group moves on when a member can't read a request (#350) — Codex sends a Lite model's tools as an additional_tools input item, its own functions in a "functions" namespace; magpie relayed the item to xAI, which 422s on it (unknown item type), and translated routes (Cursor, Anthropic, Chat…) dropped it, so the model had no tools. Outside OpenAI the item's tools now go in tools, the functions namespace opened into plain tools and earlier calls un-namespaced; OpenAI and the ChatGPT account still get Codex's own form. A 400/422 over the request's shape (failed to deserialize, unknown item type/parameter…) now moves to the group's next member, each asked once, the member not resting; the Routing page says so. TestAdditionalToolsRelayed, TestAdditionalToolsTranslated and TestGroupFailsOverARequestShape fail without the change; routing-shape.test.cjs (Chromium, WebKit, en/zh) added. go vet, go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui ./... pass; click-scroll, old-webkit, routing-wb-refused pass. --------- Co-authored-by: yetone <yetoneful@gmail.com> * plugins: a plugin's models show as the built-in's did (插件版…保持一模一样) — a model a plugin says its plan serves at no cost (WorkBuddy's x0.00 credits) is marked free, as the built-in marked it; Cursor's plugin models not named 1M hold what the catalog knows their base to (GPT-4o 128K, Kimi K2 131K), not Cursor's 200K, as the built-in's list; ZCode's own account moves marked as ZCode's, for the plugin (0.1.2) to read ZCode's own sign-in for each request and follow its switches, and comes back as ZCode's own with nothing saved. TestPluginUsage (free), TestCursorPluginContext and TestZCodeMover fail without the change. go vet, go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui ./... pass. * agents: Claude Code's effort reaches Opus 5.5 and Sonnet 5.5, follows each model's levels, and gets an ultracode switch (#351, #352, #353, #354) — Claude Code 2.1.251 on reads Opus 5.5's, Sonnet 5.5's and any newer Claude's effort only from modelSettings.<model>.effortLevel, so the top-level effortLevel magpie wrote left them at medium; it now writes modelSettings for the model (and the top key for the models that still read it), and a reset takes out only its own level. The levels offered follow what Claude Code 2.1.285 sends each model: Opus 4.5 low–high, Opus/Sonnet 4.6 without xhigh, Haiku 4.5, Sonnet 4.5 and older none; a level a model doesn't take is refused. A new ultracode square writes "ultracode": true, on models taking xhigh, and magpie claude ultra no longer writes "ultra" as the model. An open session keeps the effort it started with, so a change says to restart it while Claude Code runs. Checked against real Claude Code 2.1.285 in a sandbox HOME with a fake API recording the effort sent. TestClaudeEffortModelSettings, TestClaudeEffortPerModel, TestClaudeUltracode and TestClaudeEffortNotice fail without the change; claude-effort.test.cjs (Chromium, WebKit, en/zh) added. go vet, go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui ./... pass; omp-roles, panel-effort, click-scroll, old-webkit pass. * providers: a Remote magpie provider uses another computer's shared magpie, each request translated at most once (Jorben on Discord: 远程 magpie / magpie 接力) — a custom provider pointed at another magpie has one API per address and couldn't know which API each model's own provider serves, so a request was translated on both computers (Chat → Messages here, then again there). magpie's /v1/models now tells each model's native_endpoints (the APIs its provider serves it on; none for a routing group) and its image input, and reads them, with max_output_tokens and supported_reasoning_levels, from a list; the new remote-magpie preset takes the other magpie's address however typed (host:port, /v1, /v1/messages) for all three APIs and lists all its models, its user's choice already. Add sheet tile with the address field and a hint to Settings → Share on local network; CLI: magpie provider add remote-magpie <key> url=http://<ip>:3425. Checked with two sandbox magpies over the LAN address with the key check on and a Messages-only fake upstream: Chat, Messages (streamed), count_tokens, Responses, a routing group and Gemini all reached it as /v1/messages, translated once. TestRemoteMagpieNativeAPI, TestFetchMagpieFields and TestRemoteMagpie fail without the change; remote-magpie.test.cjs (Chromium, WebKit, en/zh) added. go vet, go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui ./... pass but for TestCursorPluginContext, which fails on main (1ce05f0) as well; click-scroll, old-webkit pass. * subscriptions: Qoder CN, for accounts on qoder.cn (#349) — Qoder's China site (qoder.cn, Alibaba Cloud or phone sign-in) has its own accounts, sign-in page, account API (openapi.qoder.com.cn) and gateway (gateway.qoder.com.cn), so a qoder.cn account couldn't sign in through magpie's Qoder, which only knows qoder.com. A new Qoder CN subscription signs in there with the client id Qoder CN's own CLI sends, and everything after — polling, job token and refresh, user info, models, usage and chat — follows the site saved with the account; if qoder.cn refuses the job token, the account chats with its device token and renews it, as that CLI does. Qoder's tile is now Qoder (international), each tile saying which accounts it is for. Accounts saved before keep working on qoder.com unchanged. Not tried with a real qoder.cn account. TestSites, TestCNDeviceChatRefresh, TestQoderCNSignIn, TestQoderCNDeviceChat, TestQoderGlobalUnchanged and TestQoderCNGateway fail without the change; qoder-cn-tile.test.cjs (Chromium, WebKit, en/zh) added. go vet, go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui ./... pass but for TestCursorPluginContext, which fails on main (1ce05f0) as well; click-scroll, old-webkit pass. --------- Co-authored-by: heyallencao <258265534+heyallencao@users.noreply.github.com> Co-authored-by: yetone <yetoneful@gmail.com> Co-authored-by: cyxer000 <48980829+cyxer000@users.noreply.github.com>
omo update updates it (#264 rhyme227). Pi's own ~/.pi is left alone. piIn becomes piLike(at, id, name, dir) so both share one wiring. Tested with omo_test.go in agent and library (fail without the change); go vet, go build -tags nogui (darwin, GOOS=linux, GOOS=windows), go test -tags nogui ./... pass.
magpie group pick <id> <model> (or group set <id> pick=<model>, a bare model id when it names one member) picks from the CLI; magpie groups and group show mark the pick, the TUI shows "manual → model". A renamed provider or model renames the pick. TestManualGroup, TestGroupPick and routing-manual.test.cjs (Chromium, WebKit, en/zh) fail without the change. go vet, go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui ./... pass; click-scroll, old-webkit, routing-levels, routing-steady, panel-routing, settings-groups pass.
magpie group set <id> levels=low,medium,high,xhigh, levels= clears; TUI l; GUI group editor's Levels: its models' shared / named) is offered instead in /v1/models, Codex's catalog, Pi and the rest; only levels magpie knows are taken, stored lowest first; a group in a group that names its own offers them for its models; unset is as before. A member without the level asked is sent its nearest, as it already was. The GUI's group save also keeps a family set from the CLI. TestGroupLevelsOverride, TestGroupLevelsReachAgents, TestGroupLevelsNamed (passthrough and translated: medium→high, xhigh→max on the low/high/max member, as asked on the one with all), TestGroupSetLevels and routing-levels.test.cjs (Chromium, WebKit, en/zh) fail without the change. go vet, go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui ./... pass (TestUserPath, a known flake under load, passes alone); click-scroll, old-webkit, routing-effort(-row), routing-levels, settings-groups GUI tests pass.
magpie group pick <id> <model> (or group set <id> pick=<model>, a bare model id when it names one member) picks from the CLI; magpie groups and group show mark the pick, the TUI shows "manual → model". A renamed provider or model renames the pick. TestManualGroup, TestGroupPick and routing-manual.test.cjs (Chromium, WebKit, en/zh) fail without the change. go vet, go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui ./... pass; click-scroll, old-webkit, routing-levels, routing-steady, panel-routing, settings-groups pass.
magpie tray, autostart's argument, so the new version came up as a tray icon however the update was asked for; the Mac reopens its .app and was fine. The window's Update pill and Settings' "Restart to update" send the tab they're on, the new process starts as magpie gui <tab>; the tray menu's Restart to Update brings the window back if it was open and keeps to the tray if not; autostart stays tray. magpie gui <tab> / magpie app <tab> open the window on that tab, and hand it to a magpie already running. The MAGPIE_REPLACES handover is unchanged: the new process waits for the old one before its single-instance lock. Tested with TestRelaunchArgs and TestRelaunchBinaryArgs (a script in magpie's place is started with gui settings <pid> / tray <pid>); go vet, go build (GUI and -tags nogui, GOOS=linux, GOOS=windows), go test -tags nogui ./... pass. Not tried: a real restart to update on Windows or Linux.
magpie tray, autostart's argument, so the new version came up as a tray icon however the update was asked for; the Mac reopens its .app and was fine. The window's Update pill and Settings' "Restart to update" send the tab they're on, the new process starts as magpie gui <tab>; the tray menu's Restart to Update brings the window back if it was open and keeps to the tray if not; autostart stays tray. magpie gui <tab> / magpie app <tab> open the window on that tab, and hand it to a magpie already running. The MAGPIE_REPLACES handover is unchanged: the new process waits for the old one before its single-instance lock. Tested with TestRelaunchArgs and TestRelaunchBinaryArgs (a script in magpie's place is started with gui settings <pid> / tray <pid>); go vet, go build (GUI and -tags nogui, GOOS=linux, GOOS=windows), go test -tags nogui ./... pass. Not tried: a real restart to update on Windows or Linux.
magpie omp auto sets omp's thinking to auto, and the panel's effort slider shows a level it doesn't list as it is (#339)
1ce05f0) as well; click-scroll, old-webkit pass.
1ce05f0) as well; click-scroll, old-webkit pass.
magpie s3 on s3://bucket/prefix access-key-id=… endpoint=… region=… path-style. The bucket holds the same passphrase-sealed file at <prefix>/magpie/magpie.magpie-backup, signed with SigV4 (no SDK). It is read with its ETag and written with If-Match (If-None-Match: * for the first), so two computers syncing at once don't overwrite each other; a server that refuses conditional writes (501, or 400 naming the header) is remembered and gets a HEAD-then-PUT instead. An IP, localhost or a dotted bucket on https goes path-style on its own. Errors say what to do: wrong key/secret, clock skew, bucket in another region (names it), no such bucket, 403 on the first read on AWS without s3:ListBucket. The secret is kept as the WebDAV password is, sent only to the endpoint and key it was typed for, and may not be the passphrase. sigv4_test (AWS's published vectors), s3_test (fake server: conditions, conflict, fallback, regions, errors), s3_cli_test and s3-sync.test.cjs (Chromium, WebKit, en/zh) fail without the change; tested against MinIO. go vet, go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui ./... pass (TestGroupAddSetRemove, a TempDir-cleanup flake under load, passes alone); click-scroll, old-webkit pass.
magpie s3 on s3://bucket/prefix access-key-id=… endpoint=… region=… path-style. The bucket holds the same passphrase-sealed file at <prefix>/magpie/magpie.magpie-backup, signed with SigV4 (no SDK). It is read with its ETag and written with If-Match (If-None-Match: * for the first), so two computers syncing at once don't overwrite each other; a server that refuses conditional writes (501, or 400 naming the header) is remembered and gets a HEAD-then-PUT instead. An IP, localhost or a dotted bucket on https goes path-style on its own. Errors say what to do: wrong key/secret, clock skew, bucket in another region (names it), no such bucket, 403 on the first read on AWS without s3:ListBucket. The secret is kept as the WebDAV password is, sent only to the endpoint and key it was typed for, and may not be the passphrase. sigv4_test (AWS's published vectors), s3_test (fake server: conditions, conflict, fallback, regions, errors), s3_cli_test and s3-sync.test.cjs (Chromium, WebKit, en/zh) fail without the change; tested against MinIO. go vet, go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui ./... pass (TestGroupAddSetRemove, a TempDir-cleanup flake under load, passes alone); click-scroll, old-webkit pass.
magpie
One place to pick every agent's model: Codex on DeepSeek, Claude Code on Kimi, Gemini CLI on GLM, from the menu bar. usemagpie.ai
magpie is a single screen that lists each AI agent on your machine and
the model it is set to. Click a value, pick a model. That is the whole app.
It lives in the menu bar: click the icon and a panel drops down; the same
screen also opens as a normal window (magpie, or Open magpie in the tray menu),
and there is a terminal version (magpie tui) and a plain CLI.
◉ magpie
▸ Claude Code claude-fable-5-1[1m] ~/.claude/settings.json
Codex gpt-6-astra effort medium
Gemini CLI gemini-3.1-pro
OpenCode anthropic/claude-sonnet-5 small anthropic/claude-haiku-4-5
MiMo Code anthropic/claude-sonnet-5
Pi openrouter/z-ai/glm-5.2:batch
Goose anthropic/claude-sonnet-5
Cursor auto
Copilot CLI claude-fable-5
↑↓ agent · ←→ field · ↵ change · s save profile · p profiles · q quit
- One small binary. Under 15 MB with the desktop app (it uses the system webview through Wails, nothing bundled), 7 MB for the terminal-only build. macOS, Linux and Windows.
- Edits config files surgically. Only the one key you change is touched;
comments, ordering and indentation in your
settings.json,config.toml,opencode.jsoncorconfig.yamlsurvive intact. Writes are atomic. - One endpoint for every agent. magpie runs a local gateway that speaks
OpenAI chat completions, OpenAI Responses and the Anthropic Messages API,
and forwards to whichever vendor serves the model. Codex, Claude Code,
OpenCode and the rest all point at
http://127.0.0.1:3425/v1and pick from one catalog; the translation between APIs happens in magpie, streaming and tool calls included. - Your subscriptions, shared. Sign in to Claude Code, Codex (ChatGPT) or Copilot and that login shows up as a provider: every other agent can use its models through the gateway, with nothing copied and no key to paste.
- Providers with one field. Pick a preset (Anthropic, OpenAI, Gemini, DeepSeek, Kimi, GLM, MiniMax, StepFun, Qwen, Baidu Qianfan, Tencent Cloud Token Plan, Huawei Cloud MaaS, Volcengine Ark, Mistral, Groq, xAI, OpenRouter, Together, Fireworks, SiliconFlow, NVIDIA NIM, ModelScope, AiHubMix, 302.AI, Ollama, LM Studio…), paste a key, done. Custom vendors need a name and a base URL. magpie never reads keys from your shell environment.
- Real model lists, nothing compiled in. With a key in hand magpie asks the vendor which models it serves and offers exactly those; the models.dev catalog fills in names, reasoning efforts and the list for vendors that have none, and refreshes itself in the background once it goes stale. Choose which models each provider exposes, or expose them all — a model released this morning is in the picker on the next refresh.
- Each agent's own model list. Under an agent's name on the Agents page,
"Showing 5 / 32 models" opens its list: click a model to take it out of
that agent's picker (Codex's
/modelincluded, its ChatGPT models too) or put it back; other agents still use it, and a new model is shown. - Profiles. Snapshot every agent's settings under a name and switch all of them back in one move.
- Real logos, no framework. Plain HTML over the system webview; brand icons from lobehub/icons.
Agents
| Agent | File | Fields |
|---|---|---|
| Claude Code | ~/.claude/settings.json |
provider, model, opus/sonnet/haiku/fable (through magpie) |
| Claude Desktop | Claude/ + Claude-3p/configLibrary/ in ~/Library/Application Support (%LOCALAPPDATA% on Windows, ~/.config on Linux) |
provider (its third-party gateway mode: Code and Cowork on magpie, no Anthropic sign-in; restart Desktop) |
| Codex | ~/.codex/config.toml |
provider, model, effort |
| Gemini CLI | ~/.gemini/settings.json, ~/.gemini/.env |
auth, model |
| OpenCode | ~/.config/opencode/opencode.json(c) ($OPENCODE_CONFIG_DIR) |
model, small |
| OpenChamber | ~/.config/openchamber/preferences.json ($OPENCHAMBER_DATA_DIR; magpie's provider in OpenCode's config) |
model, small (its own defaults, over OpenCode's) |
| MiMo Code | ~/.config/mimocode/mimocode.json(c) |
model, small |
| Pi | ~/.pi/agent/settings.json |
model |
| OmO (omo-ai) | ~/.omo/agent/settings.json (+ models.json; $OMO_CODING_AGENT_DIR, $SENPI_CODING_AGENT_DIR) |
model |
| Goose | ~/.config/goose/config.yaml |
model |
| Cursor CLI | ~/.cursor/cli-config.json |
model |
| Copilot CLI | ~/.copilot/settings.json |
model |
| Crush | ~/.config/crush/crush.json |
large, small |
| DeepSeek Harness (dsh) | ~/.dsh/config.yaml ($DSH_HOME) |
model |
| Command Code | ~/.commandcode/settings.json (+ providers.json) |
model |
| fx | ~/.fx/settings.json |
model (a keyless magpie provider) |
| omp (oh-my-pi) | ~/.omp/agent/config.yml (+ models.yml) |
model |
| Devin | ~/.config/devin/config.json (%APPDATA%\devin\config.json on Windows) |
model |
| Hermes Agent | ~/.hermes/config.yaml ($HERMES_HOME) |
model |
| Kimi Code | ~/.kimi/config.toml ($KIMI_SHARE_DIR) |
model (a magpie provider; magpie's models in Kimi's /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) |
| Qoder (CLI) | ~/.qoder/settings.json ($QODER_CONFIG_DIR) |
model, effort (a magpie custom provider; needs a Qoder plan with BYOK) |
| Qoder CN (CLI) | ~/.qoder-cn/settings.json ($QODERCN_CONFIG_DIR) |
model, effort (as Qoder; its own accounts, a Qoder CN plan with BYOK) |
| Grok Build | ~/.grok/config.toml ($GROK_HOME) |
model, effort |
| ZCode | ~/.zcode/v2/config.json |
provider (magpie's models in ZCode's picker) |
| WorkBuddy | ~/.workbuddy/models.json ($WORKBUDDY_CONFIG_DIR) |
provider (magpie's models in WorkBuddy's picker) |
| OpenHanako | ~/.hanako/provider-catalog.json + agents/<id>/config.yaml ($HANA_HOME; its local API while it runs) |
model (the primary agent's; magpie's models as a provider) |
| Alma | Alma's local API (localhost:23001, while Alma runs) |
model (Alma's default; magpie's models as a provider) |
Provider-scoped agents (OpenCode, MiMo Code, Pi, OmO, Goose, Crush, omp, Hermes Agent) take provider/model.
Only agents that are installed or configured are shown.
Providers and the gateway
Every model an agent can pick is spelled provider/model and served by
magpie's gateway, so agents never hold vendor keys or vendor URLs. Add a
provider, and its models appear in every agent's picker:
magpie presets # the vendors magpie knows, grouped: vendors, relays, local
magpie provider add deepseek sk-… # a preset needs only the key
magpie provider add ollama # local servers need none
magpie provider add "My Relay" url=https://relay.example.com/v1 key=sk-… models=gpt-5.5,claude-sonnet-5
magpie providers # host, key, exposed models, who uses what
magpie provider deepseek # one provider in detail
magpie provider models deepseek # re-fetch the vendor's list (add ids to choose which to expose)
magpie provider test deepseek # one tiny request per API, with latency
magpie provider key deepseek sk-… # replace the key
magpie provider rm deepseek
magpie models # the catalog agents see
magpie claude deepseek/deepseek-chat # use it
Custom providers take url= (an OpenAI-compatible base), anthropic= (an
Anthropic-compatible base), or both, plus responses= when the vendor has a
separate Responses endpoint, catalog= to borrow a models.dev list, and
models= to name the models to expose. Anything a preset does not know can
be overridden the same way.
One magpie can serve several computers (an office one, a personal one):
share it on the network (Settings → Share on local network), and on each
other computer add it as a Remote magpie — in the app's Add sheet, or
magpie provider add remote-magpie sk-magpie-… url=http://192.168.1.20:3425 id=office.
Each computer's own magpie still wires its agents, while the providers,
routing groups (office/group/…) and usage are the shared one's. A request
goes on in the API the agent spoke — Anthropic Messages, Responses, Chat
Completions, token counting — and a model the shared magpie's provider serves
on another API only is turned into that API once, never on both computers.
Baidu Qianfan's Token Plans
are available as baidu-qianfan: a personal (个人版) and an enterprise (企业版)
plan and pay as you go, each with its own Chat Completions, Responses and
Anthropic Messages endpoints, and a key that works only on its own plan. Add
it with magpie provider add baidu-qianfan <api-key> — the id it carried its
first day, qianfan-token-plan, is taken too. The plans serve no model list,
so the preset carries their documented models; pay as you go serves its own
at /v2/models.
Plugins
A subscription magpie doesn't sign in to itself can come from an
OpenCode provider plugin: the npm packages OpenCode
users install to sign in to a plan (their auth hook) work in magpie as they
do there. magpie runs them on Bun, downloaded the first
time a plugin needs it, and the plugin signs in, lists the models and makes
each request; magpie serves them to agents like any provider's.
magpie plugin add opencode-gemini-auth # an npm package, or a path to a plugin of your own
magpie plugin # the plugins, what each signs in to, and whether you are
magpie plugin login google-plugin # its sign-in: the method, its questions, the browser or a key
magpie plugin logout google-plugin
magpie plugin off opencode-gemini-auth # on brings it back; rm removes it; update updates them all
A provider id magpie already has (google, openai, anthropic) is
<id>-plugin. In the app, Settings → Plugins adds and removes them, and
the providers they sign in to are in Add provider → From plugins.
What a model costs
A call is counted at its effective price: what you set for that provider and model if you did, otherwise what the provider's own catalogue lists, and otherwise what models.dev lists for the model's maker. Out of the box that last one is the whole story, and it is the wrong number for any provider that does not charge list price — a relay reselling at a discount or a multiplier is counted at whatever the model's maker charges. Say what a provider actually charges, and the usage ledger and the session totals use that:
magpie model price relay-a/gpt-5.5 # what it is counted at, and where that came from
magpie model price relay-a/gpt-5.5 0.12,0.60,0.01,0.15 # input,output,cache read,cache write
magpie model price relay-a/gpt-5.5 --reset # take your price off this model
magpie model prices # every model you priced
The four numbers are USD per million tokens. All four are asked for, because
a price missing one would understate the rest of every call; 0 is a model
served at no cost, which is a price, not the absence of one.
The order a price is looked for in is: the price for this model → the price
for <provider id>/*, which covers every model of that provider → what the
provider's own catalogue lists → what models.dev lists for the maker.
--reset removes the first, and says so when a <provider id>/* price is
still in force; reset that one by name to take it away too.
A price is one provider's tariff for one model, not the model's own: the same model through two providers is two prices, and each keeps its own. Nothing an agent can see changes. The model list, the agents' own settings and the pickers that choose a model for a background task — an image, a web search, a description — all still work from the catalogue; only the cost reports read the effective price.
Two things worth knowing. The ledger and the session totals re-price when they are read, so adding or changing a price restates earlier figures: they are estimates at the effective price, not settled charges. And a price is per provider and model — usage records do not retain which key or account served a call, so a provider charging different tariffs per account cannot be costed exactly from a single provider-wide price.
Routing groups
A routing group is several models, from one provider or many, that an agent
picks as one: group/<id>. The gateway routes each request over every
member's keys and accounts together. A model two of your providers serve
under the same name becomes a group on its own; the Routing view in the app
and magpie group make any other:
magpie groups # yours, then those magpie found
magpie group add "Opus anywhere" models=claude/claude-opus-5-5,copilot/claude-opus-5.5 routing=order stays=session
magpie group opus-anywhere # one group, its models in order
magpie group set opus-anywhere models+=openrouter/anthropic/claude-opus-5.5 routing=usage
magpie group set opus-anywhere models-=copilot/claude-opus-5.5
magpie group rm opus-anywhere # one magpie found is hidden; magpie group restore <id> brings it back
magpie claude group/opus-anywhere # use it
routing= is smart (the default: of the subscriptions with quota to
spare, the one whose allowance renews soonest first), order (the first
model until it can't answer, then the next), rotate (each turn to the next
member) or usage (least used first). stays= is how long a conversation
stays with the key or account that answered it: auto (the default, while
the vendor's cache of it is worth keeping), session, turn or off.
models= replaces the whole list, in order; a bare model id works when only
one provider serves it.
The app's Import from other apps dialog can copy providers from Claude Code's
settings.json (CLAUDE_CONFIG_DIR when set) and Codex's config.toml
(CODEX_HOME when set) into magpie. Codex imports custom
[model_providers.*] entries with an inline experimental_bearer_token,
including fixed headers for custom providers in
[model_providers.*.http_headers] and models from
[profiles.*] or model_catalog_json. Review the entries before importing;
subsequent changes to agent settings are not automatically copied to magpie.
Entries that point back to magpie or only name an env_key are skipped.
Signed-in agents as providers
An agent you have signed in to is a subscription with models behind it, so
magpie offers it as a provider too. Claude Code (an OAuth login in the macOS
Keychain or ~/.claude/.credentials.json), Codex (a ChatGPT login in
~/.codex/auth.json), Copilot (a GitHub login in
~/.config/github-copilot/apps.json), Devin (devin auth login, kept in
~/.local/share/devin/credentials.toml) and Qoder (signed in from magpie with
its OAuth device flow, kept in magpie's own config; Qoder CN is its own
subscription beside it, for accounts on qoder.cn made with an Alibaba Cloud
account or a phone number, which can't sign in on qoder.com) appear in magpie providers and in
the Providers tab as signed in as …, with their models spelled
claude/claude-sonnet-5, codex/gpt-5.5, copilot/claude-sonnet-4.5 or
devin/swe-2-max in every other agent's picker. magpie reads the agent's own credentials each
time, refreshes tokens the way the agent does — writing a rotated token
back where the agent will find it — and stores nothing but your model
picks; sign out of the agent and the provider is gone. The model list is
the vendor's own too: magpie asks Anthropic's, Copilot's or Codex's API with
that same sign-in, so a model added upstream appears on the next refresh.
The ChatGPT backend only streams and rejects a few parameters, so magpie
translates non-streaming requests and drops what it would refuse.
Claude subscriptions are different: Anthropic classifies another agent's
system prompt as third-party traffic even when the OAuth request otherwise
looks like Claude Code. magpie therefore drives the genuine local claude
binary for every Claude subscription generation. The caller's tools are
bridged into that live turn over MCP, and tool results resume the same Claude
Code process; Pi, OpenCode and every other agent use this path automatically.
The generated harness stays out of Anthropic's system-prompt classifier while
its instructions remain part of the user context. This requires Claude Code
to be installed and signed in.
A Grok subscription (SuperGrok, signed in with Grok Build) talks straight
to the Responses API the grok CLI uses, a Devin subscription to the API the
devin CLI uses, and a Cursor subscription to the agent API cursor-agent
uses, each with the CLI's sign-in and the caller's tools passed through
(Cursor's model calls them as MCP tools; none of Cursor's own tools run).
Google sign-ins — Gemini CLI's and Antigravity's — talk to Google's Code
Assist API directly: magpie reads Gemini CLI's own login from ~/.gemini or
signs one in itself, and refreshes the token in memory. Google no longer
serves Gemini CLI's sign-in to individual accounts, only to Gemini Code
Assist Standard and Enterprise, which need a Google Cloud project named
(magpie accounts project gemini <email> <project-id>, or
GOOGLE_CLOUD_PROJECT in ~/.gemini/.env). Google may suspend an
Antigravity account it sees used outside Antigravity, so magpie asks before
adding one; use an account you can afford to lose.
Connecting anything else
The gateway listens on 127.0.0.1:3425 (MAGPIE_ADDR changes it) and starts
with the app; magpie serve runs it alone. It exposes:
| Path | API |
|---|---|
/v1/chat/completions |
OpenAI chat completions |
/v1/responses |
OpenAI Responses |
/v1/messages |
Anthropic Messages |
/v1/messages/count_tokens |
Anthropic token counting |
/v1beta/models/{model}:generateContent |
Google Gemini (also :streamGenerateContent, :countTokens) |
/v1/models, /v1beta/models |
the catalog |
Each /v1/models entry includes reasoning and supported_reasoning_levels
([{"effort":"low"}, ...]). A routing group lists only the levels every
member supports. native_endpoints (["/v1/messages"]) names the APIs a
request for the model is passed straight through on; it is left out of a
routing group, and of a model every request to is translated anyway.
Requests pass straight through when the vendor speaks the agent's API and
are translated otherwise, streaming, tool calls and reasoning included. The
key is magpie (any value works; the gateway only listens on loopback), and
models are named provider/model. Anything with a base-URL setting can use
it:
| Tool speaks | Base URL | Environment |
|---|---|---|
| OpenAI | http://127.0.0.1:3425/v1 |
OPENAI_BASE_URL, OPENAI_API_KEY=magpie |
| Anthropic | http://127.0.0.1:3425 |
ANTHROPIC_BASE_URL, ANTHROPIC_API_KEY=magpie |
| Gemini | http://127.0.0.1:3425 |
GOOGLE_GEMINI_BASE_URL, GEMINI_API_KEY=magpie |
An optional X-Magpie-Account: <account> header (the account's email
or login, or its id on the Routing page) pins a request to one account of a
subscription with several: only it is tried, and an unknown account, one
whose plan lacks the model, or one resting is an error rather than another
account's reply. The header is not sent on to the vendor.
The Gateway tab in the app has this as copy buttons and ready-made
snippets (shell, curl, Python, Node) for each API, the list of model ids,
and the recent calls; MAGPIE_DEBUG=1 logs every call to the terminal.
Claude Code gets ANTHROPIC_BASE_URL, ANTHROPIC_AUTH_TOKEN and the
model variables in the env block of settings.json; picking a native
model (opus, sonnet…) removes them and restores whatever was there.
Codex gets a [model_providers.magpie] table, model_catalog_json
pointing at ~/.codex/magpie-models.json (written from the catalog, so the
models show in Codex's own list) and a valid model/effort; picking a
native model removes all of that. Your ChatGPT sign-in is never touched.
Codex reads its model list at start-up, so restart it after a switch.
OpenCode, Pi, Crush get a magpie provider entry and magpie/provider/model.
Gemini CLI switches auth between API key, Google account and Vertex;
the API key goes to ~/.gemini/.env. Picking a catalog model points
GOOGLE_GEMINI_BASE_URL at the gateway (which speaks the Gemini API), sets
auth to API key with the gateway token, and names the model in
settings.json; a native model puts the previous auth back.
Import links
A vendor or relay can hand its users a ready-made provider as a link:
magpie://import?preset=deepseek&key=sk-…
magpie://import?name=Acme%20Relay&chat=https://api.acme.example/v1&anthropic=https://api.acme.example&key=sk-…&models=gpt-5.5,claude-sonnet-5
Opening one brings up magpie with what the link would add: the name, the
hosts your prompts and key would go to, the models. Nothing is saved until
you press Add. magpie import <link> does the same in a terminal.
| Parameter | Meaning |
|---|---|
preset |
a preset id (magpie presets); its endpoints are used |
region |
with a preset that has regions, which one |
name |
the provider's name; required without a preset |
id |
its id; derived from the name when absent |
key |
the API key; the user pastes one when absent |
chat |
OpenAI Chat Completions base URL (…/v1) |
responses |
OpenAI Responses base URL (…/v1) |
anthropic |
Anthropic Messages base URL (the root, without /v1) |
models |
model ids to expose, comma separated |
catalog |
models.dev provider id, for model names and reasoning levels |
website, keys |
the vendor's site and its API-key page (https) |
icon |
an https picture of the vendor's own (PNG, JPEG, GIF, WebP, ICO, SVG, at most 1 MB). magpie downloads it once, after you confirm the import, into its icons folder; without one it falls back to the catalog's logo or a plain mark |
Base URLs must be https (plain http only to this machine or the local
network). Web pages and GitHub don't link custom schemes reliably, so link
to https://usemagpie.ai/import#<same parameters> instead: it opens
magpie, and offers the download when it is not installed. The parameters
stay in the fragment, which browsers never send to a server. The full guide,
with a link builder: https://usemagpie.ai/docs/import.
Install
Download the app for macOS, Windows or Linux from usemagpie.ai, or install it from a terminal (on Linux, the desktop app when WebKitGTK 4.1 is installed, the command otherwise):
curl -fsSL https://usemagpie.ai/install.sh | sh
Mac releases are signed and notarised; the Windows and Linux builds are not
signed yet (Windows SmartScreen may ask before the first run). Every build
keeps itself current: the app
downloads a new version in the background and installs it when you restart
(Restart to Update in the menu) or quit; magpie update does the same from
a terminal. Every release is on
yetone/magpie-releases.
From source:
go install github.com/yetone/magpie@latest
or build locally:
make build # ./magpie with the desktop app (needs cgo + the platform webview)
make app # macOS: magpie.app, a menu bar app with no Dock icon
make cli # terminal-only build, no cgo, cross-compiles anywhere
make release # dist/: native app build + cli builds for every platform
make release-windows # dist/: the Windows app, amd64 and arm64 (cross-compiles)
make release-linux # dist/: the Linux app for this machine's arch
Linux needs libgtk-3-dev and libwebkit2gtk-4.1-dev for the app build
(the Makefile adds the gtk3 tag; with plain go build, pass -tags gtk3);
Windows uses the WebView2 runtime that ships with the OS.
Docker
docker build makes a server image: the terminal-only binary on
distroless, run as nonroot, with magpie's files in a volume at /config.
docker build -t magpie .
docker run -d --name magpie -p 127.0.0.1:3425:3425 -p 127.0.0.1:3430:3430 -v magpie-config:/config magpie
3425 is the gateway for agents. Until it is shared (below) it takes any key,
Bearer magpie included, from anyone who reaches it, so the ports above are
published on the host's loopback only; Docker's -p 3425:3425 would put it
on every interface of the host, past its firewall. To reach it from other
machines, turn on Settings → Share on local network in the browser UI (or
put "lan": true, "lanKey": "sk-magpie-…" in /config/magpie/settings.json):
from then on a request from outside the container must carry that key as its
API key, and only then publish the port beyond 127.0.0.1.
For the browser UI run the image with magpie web --addr 0.0.0.0:3430 --no-open
in place of the default serve, and open
http://localhost:3430/?k=<key from docker logs magpie> (set MAGPIE_WEB_KEY
to keep one key across restarts). There you add providers and import
sign-ins from a file; a subscription sign-in started in the container cannot
finish, because the vendor sends the browser back to the container's own
loopback, so sign in on a machine where magpie runs with a browser. Keys and
sign-ins live in the volume, so a restart keeps them.
Developing
make dev
builds with -tags dev and opens the app with the UI served straight from
internal/gui/assets: save app.css, app.js or index.html and the window
reloads itself. With fswatch installed (brew install fswatch), a change to a
Go file rebuilds and relaunches the app too. The dev build uses its own gateway
port (DEV_ADDR, default 127.0.0.1:3426), so a magpie you already run keeps
serving your agents. Point it at a scratch home to keep your real agent
configs out of it:
HOME=/tmp/magpie-home XDG_CONFIG_HOME=/tmp/magpie-home/.config make dev
MAGPIE_THEME=light|dark forces the palette and MAGPIE_DEBUG=1 prints what the
gateway translates.
Use
magpie # open the app: a window plus the menu bar icon
magpie tray # menu bar icon only (use this in your login items)
magpie tui # the same thing, in the terminal
magpie web # the app's window in a browser (WSL, a server over SSH); --lan, --addr, --no-open
# (a new key each run; MAGPIE_WEB_KEY keeps one, for a page run as a service)
magpie ls # list every agent and its current settings
magpie claude opus # set a model (agent names accept prefixes: cc, oc, gem …)
magpie codex gpt-5.6-sol
magpie codex effort high # other fields
magpie codex xhigh # bare effort levels are recognised too
magpie codex deepseek/deepseek-chat # any catalog model, through the gateway
magpie claude moonshot/kimi-k2.5
magpie claude haiku deepseek/deepseek-v4-flash # one tier on its own model
magpie claude haiku "" # back to the main model
magpie gemini auth api-key
magpie opencode anthropic/claude-sonnet-5
magpie oc small anthropic/claude-haiku-4-5
magpie mimo anthropic/claude-sonnet-5
magpie save work # snapshot everything as a profile
magpie use work # switch back
magpie profiles
magpie rm work
magpie sync # refresh the models.dev catalog and every live model list
In the app, click any value to open a filtered list; type to search or to
enter something that is not listed; esc closes the panel. Profiles are the
chips at the bottom: click to apply, × to delete, + save current to add.
The Providers tab of the window lists your providers with the agents on
each; click a row to change the key or the exposed models, Test it, or
click an agent icon to point that agent at one of its models. Add
provider shows the presets as tiles: pick one, paste the key.
On macOS, Settings → Preferences → Session terminal chooses which installed
app opens a session from the terminal button in Usage → Sessions. The list
contains apps registered to open .command files, with the current system
default listed once. With no saved choice, magpie follows that default, or
Terminal when the default is not a terminal. The Resume button still copies
the command.
Keys in the terminal version:
| Key | Action |
|---|---|
↑ ↓ |
choose agent |
← → |
choose field (model, effort, small …) |
↵ |
open the picker |
| type | filter; enter accepts custom values |
s |
save current setup as a profile |
p |
apply or delete (ctrl+d) a profile |
S |
sync the model catalog |
q |
quit |
Agents read their config at startup, so a running session keeps its model until you start a new one.
Moving to another machine
magpie backup # writes magpie.magpie-backup, asks for a passphrase twice
magpie backup --no-keys ~/b.magpie-backup # the same with no API keys in it
magpie restore magpie.magpie-backup # on the other machine
magpie restore --no-agents b.magpie-backup # providers, settings, profiles; agents left as they are
magpie restore --no-library b.magpie-backup # the library here left as it is
A backup holds your providers (with their keys, unless --no-keys), the
pictures picked for them, the settings, the profiles, every agent's model and
the library (unless --no-library): the instruction sets, the MCP servers and
the skills with their files (a file over 2 MB is left out). Without keys, a
server's environment variables and headers that look like a key go empty.
Restoring the library replaces the one there — what it replaces is kept with
the library's backups — and writes it into the agents on that machine.
It is encrypted on your machine (AES-256-GCM, the key derived from the
passphrase with PBKDF2-SHA256); nothing in it can be read without the
passphrase. Restoring replaces providers with the same id and adds the rest;
one that came without a key keeps the key already there. Agent models are set
only for agents installed on that machine. Subscriptions are not in it: sign
in to them on each machine. Piped in, the passphrase is the first line of
stdin.
Keeping machines in sync
Settings → Sync and backup → WebDAV or S3 sync keeps the same backup on a server and brings every machine up to date with it, every 3 minutes while the gateway runs. Choose one of these:
- WebDAV: a folder on a WebDAV server such as 坚果云, Nextcloud or a Synology.
- S3: a bucket on AWS S3, Cloudflare R2, Backblaze B2, MinIO, Garage, a NAS or any other S3-compatible server.
The file is sealed on your machine with the passphrase, so the server only ever stores ciphertext. Each machine writes only over the version it read (a conditional write), so an update that another machine made in between is merged rather than lost.
magpie webdav on https://dav.jianguoyun.com/dav/ user=me@example.com
magpie s3 on s3://my-bucket/magpie endpoint=https://<account>.r2.cloudflarestorage.com access-key-id=…
magpie s3 on s3://backups endpoint=http://nas.local:9000 path-style=yes access-key-id=…
magpie s3 # where it syncs to and how the last sync went; magpie s3 now, off
For S3:
endpointis empty for AWS.regiondefaults tous-east-1, or toautoon R2.path-style=yesputs the bucket in the path, which MinIO and most servers you run yourself need.- The secret is asked for and saved like the WebDAV password. It is used only with the endpoint and access key it was given for.
- The bucket must already exist.
- The access key needs to read and write
<prefix>/magpie/. On AWS it also needs to list the bucket. - A server without conditional writes is supported. There magpie checks the object's ETag just before each write.
Files
~/.config/magpie/profiles.json— saved profiles~/.config/magpie/providers.json— your providers, keys included (0600)~/.config/magpie/stash.json— values magpie replaced, restored on switch-back~/.config/magpie/plugins.json,plugins/— the plugins added, and their packages~/.config/magpie/plugin-auth.json— the plugins' sign-ins (0600)~/.cache/magpie/bun/— the Bun plugins run on~/.cache/magpie/models.json— models.dev catalog (OpenCode's cache at~/.cache/opencode/models.jsonis used when present)~/.cache/magpie/models/<provider>.json— model lists fetched from vendors
XDG_CONFIG_HOME and XDG_CACHE_HOME are respected.
Counting users
Once a day, a running magpie (the app, or magpie serve) sends one event to
PostHog so we know how many people use it: a random id made up on your
computer (~/.config/magpie/install-id), magpie's version, and your system
and architecture. Nothing else goes: no accounts, keys, providers, models,
prompts or usage. Turn it off in Settings → Privacy → Count me as a user, or
with DO_NOT_TRACK=1 or MAGPIE_NO_STATS=1. Builds from source never send
it. The code is internal/stats.
Community
Questions, setups worth sharing, ideas, bugs: come talk to us and other magpie users on Discord. Issues and pull requests are welcome here too.
License
MIT. See LICENSE.