yetone 2719dd6ac3 tui, cli: adding a provider says how many models its list had, or why it had none, and m (magpie provider refresh <id>) fetches the list again (akic404 on Discord: 还是tui cli问题,没有获取上游模型功能,新建之后模型数是0)
The TUI's add fetched the new provider's list and dropped any error, so a remote magpie that couldn't be listed (wrong address, the other magpie not shared, a gateway key it refuses) showed just "added Remote magpie" and 0 models, and the Providers page had no way to ask again: t tests the APIs, the models picker said "t tests it, which asks the vendor for them", which it doesn't. Now the add says "added X · N models from its list · M for agents (↵ picks)", or "added X · no model list · <the first URL's answer> · m asks again" in red; m on the Providers page is the app editor's Refresh (Refetch: picks the new list no longer has are dropped and named), with the same note; the footer lists it and the empty picker points at it. The CLI already said a failed list on add and fetched with provider models <id>; provider refresh <id> (and fetch) is that by its GUI name, and models <id> now goes through Refetch too, naming dropped picks. The ids were never names: a remote magpie's are <its id>/<the other magpie's provider id>/<model> (remote-magpie/custom/qwen3.6-max); the Chinese is the label, "qwen3.6-max · 中转站", the other magpie's provider name it sends as magpie_label, which magpie models and the TUI's picker show beside the id. README says so.

TestTUIFetchesAProvidersModels (add over a fake magpie: 2 models; its list grows, m: 3; it refuses, m: the error and the 3 kept) and TestProviderRefreshCmd fail on the old code; TestTUIAddsARemoteMagpieWithItsAddress now expects the error at a dead address. End to end with two sandbox magpies (temp HOMEs, 3461/3462, a fake upstream on 3463): A with "中转站" over the upstream (id custom), B's TUI in tmux adding A at 127.0.0.1:3461: "added Remote magpie · 2 models from its list · 2 for agents", row 2 models, m again the same; a second at 127.0.0.1:3464: "added Remote magpie 2 · no model list · … connection refused · m asks again", 0 models; provider refresh remote-magpie lists remote-magpie/custom/qwen3.6-max "qwen3.6-max · 中转站". go vet, builds (darwin, GOOS=linux, GOOS=windows) and go test -tags nogui -count=1 ./... under a temp HOME pass.
2026-10-04 01:24:47 +08:00
providers: each account of a subscription, and each key, can be kept for some of the provider's models only (#474: 增加账号级别的模型选项, a low-quota but verified account kept for the cheap models) — every account or key a provider had served every model it did, so one couldn't be spared for some. An account's or key's row in its provider's editor now has a models badge: a click opens the provider's models as chips, Save keeps the account for those picked, All models gives it every one again; one kept for some says how many, always shown. magpie provider account-models <id> [account|key [models…|all]] does the same and lists them. It is kept in providers.json as accountModels, an account by its name in lower case, a key by its id; one not in it serves every model, as before. The gateway never sends a model to an account or key whose list leaves it out, in every routing mode, in drag order, in a group and as a fallback, and never falls back to it all the same; the trace says it was left out as set to serve other models. When every account or key there is leaves the model out, the request is refused with a 403 saying so and how to change it, not "none of X's models is ready". TestKeyModelsNarrowCandidates, TestAccountModelsNeverChosen, TestAccountModelsCmd and account-models.test.cjs fail without the change. go vet, go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui ./... pass; account-models, click-scroll, old-webkit, account-arrange, account-proxy, account-return, routing-account-order, panel-routing, unlisted-models pass.
2026-10-02 05:50:23 +08:00
plugins: a plugin's subscription does what a built-in's does, in seven places it didn't (the user: 要保证插件订阅 provider 跟内置的订阅 provider所有逻辑都要一模一样) — an installed plugin's provider id is kept for it signed in or not, as a built-in's is: a provider of the user's own isn't saved on it, renamed onto it or imported as it (freeID gives id-2). A plugin's account whose plan lacks the model is tried after the ones that list it, as a Codex Free account behind a Plus, by the list the plugin told for it. Cursor's plugin has fast mode where the built-in Cursor has it: a model with a -fast one is saved :fast in a group and sent service_tier "priority", one without is refused. Removing a plugin's provider hides it as a built-in's is, its accounts kept and shown again from Hidden or by signing in, rather than signing every account out ("stays signed in" said so already). magpie accounts lists, adds, switches and forgets a plugin's accounts by its provider's id or name. Grok's plugin beside the built-in Grok (grok-plugin) searches by itself as Grok does. A plugin's plan is named as its built-in's ("Cursor Pro", "SuperGrok"), and a plugin's resets are shown but have no Use button or Auto-use, which spent Codex's reset of the same user. TestPluginIDReservedSignedOut, TestPluginAccountLackingTheModelGoesLast, TestCursorPluginGroupFast, TestAccountsOfAPlugin, TestGrokPluginSearchesItself, plugin-plan and codex-auto-reset fail without the change; TestPluginSignIn and TestPluginAccounts now expect a removed plugin hidden. go vet, go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui ./... pass (TestClaudeSignedOut failed once under load, passes alone); the gateway, provider and plugin suites and TestAccountsOfAPlugin pass on Windows, the plugin tests on Linux.
2026-10-01 20:26:55 +08:00
gui: the Add sheet shows a new user only magpie's own subscriptions; a deprecated one is installed from Plugins (the owner: 对于新用户来说,这里应该只显示内置的 provider 啊,更多不是要点击那个链接跳转到插件页面让用户去主动安装吗?) — a fresh magpie listed all 18 subscription tiles, the deprecated built-ins (Cursor, Kiro, Zed, Qoder…) among them, each offering its plugin when clicked. A deprecated built-in with no account in magpie (unusedSub) is now left out of the Subscriptions section, which for a new user is Claude, ChatGPT, Copilot, Gemini CLI and Antigravity, then More in Plugins; a search naming a left-out one shows More in Plugins, which opens the Plugins page on that search, where its plugin is installed. One with an account stays, badged Deprecated; one moved to its plugin stays as before; one whose plugin is installed shows the plugin's tile (replacedSub). The tile's plugin offer (Install and sign in / Use the built-in), unreachable now, is removed with its CSS and zh strings. AGENTS.md says so. deprecated-subscriptions.test.cjs now checks Zed (no account) isn't in the sheet, a search for it shows More in Plugins and opens Plugins on "zed" with Zed's card, and nothing signs in or installs by itself; plugin-replaced.test.cjs expects no tile for an unused deprecated built-in and one for a signed-in one. Both pass in Chromium and WebKit (en/zh), as do add-sheet, provider-search, qoder-cn-tile, mobile-web, deprecation-notice, click-scroll, plugin-move-overflow, plugin-movable-review, plugin-move-unserved, plugin-progressive and plugin-check-updates; plugin-move's editor-scroll check in Chromium flakes the same on origin/main.
2026-10-03 00:52:41 +08:00
docs: every built-in subscription a plugin serves once moved says so in its code, and AGENTS.md says where a fix for it goes (user: 对内置的订阅 provider 在代码层面和文档层面加上注释,防止又有别的 agent 在改内置订阅 provider 的代码而不知道生效的其实是对应的插件代码) — a moved Cursor, Devin, Grok, Command Code plan, Kiro, Zed, Qoder, Factory, Xiaomi MiMo, ZCode or WorkBuddy (provider.Moved; since v0.1.642 also the default for a new sign-in, provider.Adopt) is signed in, listed, requested and metered by its @magpie-community plugin, and none of the built-in's code in internal/provider/<name>*.go, internal/gateway/<name>.go, internal/zed or internal/qoder serves it; a fix made there alone never reaches those users. Each of those files now opens with a PLUGIN-SERVED notice naming the subscription's id, its plugin package, where the plugin lives (magpie-community/plugins, packages/<name>) and the mover whose min to raise so moved users get the fixed plugin (keepMovedCurrent); Qoder's adds that Qoder CN isn't moved and still runs there. AGENTS.md (CLAUDE.md imports it) lists the moved built-ins with their packages and movers, how to tell whether a user's is moved (move.state "plugin", onPlugins), the order a fix goes in (the plugin, publish, raise min, the built-in only for those not moved), the magpie code that does run for a moved one (internal/plugin, plugins.go, plugin_usage.go, pluginsignin.go, migrate*.go, app.js's subOf/pluginSubs/startPluginSignIn), and the parity tests. movers in migrate.go and SUBS in app.js point there too. TestMovedBuiltinsSayTheirPlugin fails for a mover whose code carries no PLUGIN-SERVED notice naming its id and package (it failed while a wrapped package name was split). Comments and docs only, no behaviour changed. go vet ./internal/..., go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui -count=1 ./... under a temp HOME pass (TestProvidersAnswerWhileListsComeIn and TestClaudeSignedOut failed once on their timing under load, pass alone; untouched).
2026-10-02 19:37:02 +08:00
gateway: each gateway key can have its own limit, a token total and/or an estimated cost per day, week or month, and a spent key is refused before any provider is asked (margbug01 on #585, v0.1.671: 每个网关密钥都能设置自己的用量上限) — a gateway key named who called and the Usage page grouped its calls, but nothing held a key to an amount, so one client shared on the network could spend a subscription's whole window. A key now carries limit {period day|week|month, tokens, cost, cacheReads} in caller-keys.json, set with the new "limit-key" change. Windows are calendar ones in local time (from midnight, from Monday, from the 1st) and a call counts in the window it started in. Tokens are a call's uncached input, output and cache writes, plus cache reads when the limit says so; cost is an estimate at the Usage page's effective prices (usage.NewCoster), a call with no known price adding none and counted as unpriced. What a key used is summed from usage.jsonl (usage.Visit), so it survives a restart; the new internal/budget keeps the sums in memory per window and appendUsage writes a key's record through budget.Append, which appends and counts under one lock so a window read meanwhile never counts a call twice. Before a keyed POST reaches the mux, keyLimited reserves for it: its body's size in tokens (4 bytes a token) plus the key's mean output per call this window, at the window's cost per token or the named model's input price; it is let in while used + reserved is under the cap, so requests sent at once overshoot by about one call, and the reservation is released when the request ends, a streamed reply being settled then with the usage its vendor reported. A spent key gets a 429 in its API's error shape (Anthropic, OpenAI chat/responses, Gemini) naming the key, the cap and the reset time, with Retry-After, X-Should-Retry: false and X-Magpie-Limit-Reset, recorded as a rejected call (gateway_key_limit); other keys go on. Requests from this computer without a gateway key are not limited; a key used from this computer is. count_tokens and bridge callbacks are never held. GET /v1/magpie/limit tells a key its own status. Gateway keys rows show a Limit badge (what is used of it, per what, Spent in red) and a line under a limited key (used of limit, left, resets / refused until, in flight; unpriced calls in its title); Limit opens an editor under the row (window, tokens with k/m/万/亿, cost in US$, Count cache reads too, what is counted) staged until Save, marked unsaved, Cancel/Escape dropping it, a bad amount disabling Save, No limit removing it; en and zh; no left-border accent; no click scrolls. The CLI gains magpie gateway-key limit <id> [off | day|week|month --tokens N --cost USD --cache-reads] and a LIMIT column in list. TestKeyLimitRefusesBeforeUpstream (two calls within 700 tokens, the third a 429 the fake upstream never sees, message/type/headers, the rejected record, Anthropic's shape on /v1/messages, another key and a keyless loopback request let in, still refused after budget.Forget as after a restart, a raised and a removed limit let in, /v1/magpie/limit), TestKeyLimitHoldsParallelRequests (20 at once against 2000 tokens: some refused, at most one call over, no reservation left; without the reservation all 20 got in and 7350 were used), TestKeyLimitWindowResets, TestLimitWindow, TestLimitValid and TestGatewayKeyLimitCLI are new; gateway-key-limit.test.cjs (WebKit, en and zh) is new and passes, as do gateway-caller-keys, gateway-connect-keys, api-key-theme and click-scroll (whose request-pick case flaked once under a parallel run and passes alone, as on main). go vet ./internal/..., go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui -count=1 ./... under a temp HOME pass (TestFXInState's known TempDir-cleanup flake aside).
2026-10-03 00:18:16 +08:00
gateway: each gateway key can have its own limit, a token total and/or an estimated cost per day, week or month, and a spent key is refused before any provider is asked (margbug01 on #585, v0.1.671: 每个网关密钥都能设置自己的用量上限) — a gateway key named who called and the Usage page grouped its calls, but nothing held a key to an amount, so one client shared on the network could spend a subscription's whole window. A key now carries limit {period day|week|month, tokens, cost, cacheReads} in caller-keys.json, set with the new "limit-key" change. Windows are calendar ones in local time (from midnight, from Monday, from the 1st) and a call counts in the window it started in. Tokens are a call's uncached input, output and cache writes, plus cache reads when the limit says so; cost is an estimate at the Usage page's effective prices (usage.NewCoster), a call with no known price adding none and counted as unpriced. What a key used is summed from usage.jsonl (usage.Visit), so it survives a restart; the new internal/budget keeps the sums in memory per window and appendUsage writes a key's record through budget.Append, which appends and counts under one lock so a window read meanwhile never counts a call twice. Before a keyed POST reaches the mux, keyLimited reserves for it: its body's size in tokens (4 bytes a token) plus the key's mean output per call this window, at the window's cost per token or the named model's input price; it is let in while used + reserved is under the cap, so requests sent at once overshoot by about one call, and the reservation is released when the request ends, a streamed reply being settled then with the usage its vendor reported. A spent key gets a 429 in its API's error shape (Anthropic, OpenAI chat/responses, Gemini) naming the key, the cap and the reset time, with Retry-After, X-Should-Retry: false and X-Magpie-Limit-Reset, recorded as a rejected call (gateway_key_limit); other keys go on. Requests from this computer without a gateway key are not limited; a key used from this computer is. count_tokens and bridge callbacks are never held. GET /v1/magpie/limit tells a key its own status. Gateway keys rows show a Limit badge (what is used of it, per what, Spent in red) and a line under a limited key (used of limit, left, resets / refused until, in flight; unpriced calls in its title); Limit opens an editor under the row (window, tokens with k/m/万/亿, cost in US$, Count cache reads too, what is counted) staged until Save, marked unsaved, Cancel/Escape dropping it, a bad amount disabling Save, No limit removing it; en and zh; no left-border accent; no click scrolls. The CLI gains magpie gateway-key limit <id> [off | day|week|month --tokens N --cost USD --cache-reads] and a LIMIT column in list. TestKeyLimitRefusesBeforeUpstream (two calls within 700 tokens, the third a 429 the fake upstream never sees, message/type/headers, the rejected record, Anthropic's shape on /v1/messages, another key and a keyless loopback request let in, still refused after budget.Forget as after a restart, a raised and a removed limit let in, /v1/magpie/limit), TestKeyLimitHoldsParallelRequests (20 at once against 2000 tokens: some refused, at most one call over, no reservation left; without the reservation all 20 got in and 7350 were used), TestKeyLimitWindowResets, TestLimitWindow, TestLimitValid and TestGatewayKeyLimitCLI are new; gateway-key-limit.test.cjs (WebKit, en and zh) is new and passes, as do gateway-caller-keys, gateway-connect-keys, api-key-theme and click-scroll (whose request-pick case flaked once under a parallel run and passes alone, as on main). go vet ./internal/..., go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui -count=1 ./... under a temp HOME pass (TestFXInState's known TempDir-cleanup flake aside).
2026-10-03 00:18:16 +08:00
claude: adding a Claude account is Claude Code's own claude auth login, and magpie never refreshes a Claude sign-in nor asks Anthropic anything with one (the user: 把 magpie 自己的 Claude 登录改成直接用 claude /login,只要体验上没什么差别就改) — magpie traded the sign-in page's code for tokens with Claude Code's OAuth client, asked Anthropic's profile and model list with them, refreshed saved accounts itself (daily keepalive included) and handed a saved account to Claude Code as CLAUDE_CODE_OAUTH_TOKEN. Now Add account runs claude auth login --claudeai in a config directory of magpie's own: the page Claude Code would open comes to magpie's window (Claude Code is given magpie as its BROWSER, which writes the page down and exits), the browser comes back to Claude Code's own listener, and when the browser can't reach this machine the address it ended on, or the code Claude's page shows, is pasted as before and handed to Claude Code. Who signed in is what Claude Code kept (.claude.json, auth status). A saved account in use beside the signed-in one runs Claude Code with CLAUDE_CONFIG_DIR set to its own directory (claude-accounts/ beside magpie's config), its sign-in put there once from logins.json and read back as Claude Code refreshes it; switching to it moves it into Claude Code's own store and removes the directory, as forgetting it does. Claude's models are the catalog's (models.dev), not asked of Anthropic; an imported Claude login is kept as the file has it, and Claude Code refreshes it on first use. Claude Code is required to add a Claude account. TestClaudeSignInByPaste, TestClaudeSignInByPastedAddress, TestClaudeSavedAccountDir (a fake claude CLI, and a server standing in for Anthropic that fails the test if asked) and the reworked sign-in, import and gateway tests fail without the change. With the real Claude Code 2.1.286 in a sandbox directory, its sign-in page (localhost callback) reached magpie's binary and the sign-in waited for the browser; nothing was left behind once it was canceled. go vet, go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui ./... pass; claude-risk, plugin-signin, signin-callback, signin-link, signin-paste-codex, routing-signin, own-signin-remove, click-scroll, old-webkit pass.
2026-10-01 16:37:07 +08:00
routing: a group's member can be sent in its vendor's fast mode, beside its own reasoning (vs on Discord: a member "gpt-6.1 sol · high · fast") — a member's effort was already its own (#189, "provider/model:effort") but there was no way to ask for speed, so a group could not hold "gpt-6.1-sol at high, priority" next to slower members. A group now lists in fast the members it sends fast, as Members spells them, kept beside Members rather than in their ids so an older version still routes to them, only not fast (checked: HEAD's build on the same providers.json sends the same members at their effort, no tier, and leaves the file as it was). When the gateway sends to such a member it asks in the upstream's own words, only where the model has a fast mode magpie can ask for (provider.CanFast): service_tier "priority" for a ChatGPT account's GPT models and an OpenAI key's GPT/o-series models on its Responses API, Cursor's -fast variant, and speed "fast" with the fast-mode-2026-02-01 beta for Claude Opus 4.8/5/5.5 on an Anthropic key (api.anthropic.com); a relay, Bedrock, a Claude subscription, a model without one, or a provider since moved off those hosts is sent untouched. It is applied in the agent's own API and carried through translation (parseAnthropic now reads speed), so Chat, Responses and Anthropic agents all get it. A translated request for OpenAI's Chat API is still told no tier (TestCursorPluginFast), hence the Responses requirement. The trace marks the try, the order and the group (fast), shown as "fast" beside the reasoning in Requests and the order. Typed, a member is provider/model[:effort]:fast: magpie group add/set models=… takes it, fast=<m1>,… sets them, models-= takes either spelling, and the group lists and shows members as typed with "· fast"; a model with no fast mode, or a group in the group, is refused. The GUI group editor shows a "Standard speed"/"Fast" toggle (标准速度/快速) beside a member's reasoning only for a model that has one, the card reads "· fast" after the reasoning, and fast follows a member's reasoning change and leaves with a removed member; the TUI's group view takes E to step a member's effort and F to toggle fast. Provider renames carry it. TestFastMemberOpenAI, TestFastMemberAnthropic, TestWithFastWire (gateway, per wire format, a fake vendor recording body and anthropic-beta), TestCanFast, TestSaveGroupFast, TestGroupMembersFast, TestGroupMemberEffortAndFast, TestGroupSaveFast and group-member-fast.test.cjs fail without the change. A sandbox magpie (own HOME, gateway on 3966) with providers at http://api.openai.com and http://api.anthropic.com through a local recording proxy sent priority+high to OpenAI and speed fast + the beta to Anthropic from Chat, Responses and Anthropic clients, and nothing extra for Sonnet or a non-fast member. go vet, go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui ./... pass; group-member-fast, click-scroll, old-webkit, routing-levels, routing-manual, routing-effort, settings-groups pass (Chromium, WebKit, en/zh).
2026-10-01 17:13:34 +08:00
test: every package's tests pass on Windows, and none writes outside its sandbox (the user: 把 Windows 上那几个失败的测试修掉) — on a Windows box the suite failed in most packages, and the root package hung for 25 minutes. Tests set HOME, which os.UserHomeDir ignores on Windows (it reads USERPROFILE), so they read and wrote the runner's own home; each now sets USERPROFILE beside HOME, and Devin's, Goose's, Crush's and Claude Desktop's tests set APPDATA/LOCALAPPDATA, where those keep their files there. said() read the command's output only once it returned, and a pipe holds a few KB on Windows, so magpie model --help blocked forever; it reads while the command writes. Unix permission bits (the gateway key store's 0600 too), exec bits and symlinks Windows lacks are checked only off Windows; TOML strings hold Windows paths as literal strings, JSON as marshalled ones; a URL's path is compared as one. The provider and usage packages run in a home of their own (TestMain, as the gateway's already does), since a CLI's identity kept by a refresh still going when its test ended landed in the runner's ~/.config/magpie — and on a Mac in the user's own. Two product fixes came out of it: imagemcp took a file:///C:/… root or image for the path /C:/…, which doesn't exist on Windows, and now makes a Windows path of it (a host for a UNC share); and session discovery could keep a new Cowork session hidden on Linux, where a folder's mtime is a kernel tick and its size doesn't change, so an entry added within the tick of the last listing left both as they were — a listing made within 2s of its folder's last change is read again, as git does for racy files. TestFilePath and TestRacyDirectoryListedAgain fail without the change. On Windows 11 every package passes (32 of 32) and a run of each package in a fresh home leaves nothing in home, APPDATA or LOCALAPPDATA; on Linux (WSL) and macOS go test -tags nogui ./... passes. go vet, go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build pass.
2026-10-01 23:15:07 +08:00
library: RTK says when rtk isn't on the PATH the agents get, and puts it there when asked (#601, Moody-Sin: magpie好像没有正确安装RTK — Pi said "RTK disabled: rtk binary not found in PATH" while the Library showed RTK working) — rtk's hooks run it by name (Pi's extension probes rtk --version and turns itself off when that fails; Claude Code's and Codex's are rtk hook …; the commands they rewrite to are rtk git status and so on), but magpie looked for rtk in more places than an agent's PATH: ~/.local/bin and ~/.cargo/bin, which the app adds to its own PATH whether the user's shell has them or not (rtk's own install.sh puts it in ~/.local/bin and only warns when that isn't on PATH), and WinGet\Links on Windows, which winget doesn't always link into. So rtk was "installed", the agents were switched on with it by its whole path, and none of them could run it. Now the PATH a terminal opened now has is read — the login shell's (proc.LoginPath, the one UserPath already asks for, kept a minute) or on Windows the registry's (moved from provider's registryPath) — and when rtk isn't in it the RTK card says so, with what Put RTK on PATH will do, and each agent that has rtk is tagged Not on PATH (不在 PATH 中). Put RTK on PATH (magpie library rtk path) links rtk into the first of ~/.local/bin, ~/bin, /opt/homebrew/bin, /usr/local/bin, linuxbrew's bin, ~/.cargo/bin that is on that PATH and writable, or on Windows adds rtk's own folder to the user's PATH in the registry (a link needs Developer Mode, and a copy is left behind when winget upgrades rtk; winget's package folder doesn't change across upgrades) and tells Explorer; with no such folder it says which folder to add to PATH in the shell profile — shell profiles are never edited. rtk is also found in winget's package folder (WinGet\Packages\rtk-ai.rtk_*), and rtk's script upgrades a linked rtk where the link points, not over the link. TestRTKOffPath and library-rtk-path.test.cjs (en, zh) fail without the change. go vet ./internal/..., go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build and vet, go test -tags nogui -count=1 ./... under a temp HOME pass but the known TestFXInState flake. The Windows registry path is built, not run on Windows.
2026-10-03 02:38:17 +08:00
update: install.sh and magpie update take --proxy and --mirror, and magpie update mirror keeps one for the app's updates too (akic404 on Discord: 安装和update如果没有梯子也是麻烦事,能否在install.sh和magpie update后加子选项?--proxy等可自定义或默认ghproxy) — install.sh had no options and fetched with curl alone, and magpie update took none; without a way out to GitHub both failed. Now both take --proxy <url> (http, https, socks5, socks5h; install.sh also MAGPIE_PROXY=) for the feed and the download, and --mirror <prefix> (or MAGPIE_MIRROR=), a GitHub download mirror the release file is fetched from as <prefix>https://github.com/…; only github.com / githubusercontent.com URLs are put through it. No mirror is used unless one is given: magpie names none and has none by default. The feed with each file's SHA-256 is always fetched from usemagpie.ai, never through the mirror, so a file the mirror changed is refused and deleted ("… from the mirror X does not match its checksum from https://usemagpie.ai; not installed"); macOS still checks the app's signing team. magpie update mirror <prefix>|off saves Settings' updateMirror (http(s) only), which the app's own background update follows too; magpie update --mirror off skips it once; the Settings page's save keeps it. The app's update already followed Settings → Proxy (#294); magpie update --proxy overrides it for one run. The environment's ALL_PROXY / all_proxy is now honoured when HTTPS_PROXY / HTTP_PROXY are unset (Go's ProxyFromEnvironment ignores it), with NO_PROXY as curl reads it. magpie update takes --flag value or --flag=value, and an unknown word is an error instead of an update. The help, README and the docs' FAQ (en/zh/ja) say how. install.test.mjs's proxy, mirror and tampered-mirror tests, TestAllProxyFromTheEnvironment, TestUpdateThroughAMirror, TestUpdateThroughAProxyGiven and TestUpdateFlags fail on the old code and pass; TestSettingsSaveKeepsWhatItDoesNotSend covers updateMirror. A dry run of install.sh as Linux amd64 into a temp HOME installed the real 0.1.773 with its SHA-256 checked, refused a local mirror serving a wrong file, and with --proxy to a closed port failed reaching the site through it. go vet, builds (darwin, GOOS=linux, GOOS=windows), sh -n, node --test site/install.test.mjs and go test -tags nogui -count=1 ./... under a temp HOME pass but TestClaudeIdentityServedWhileAsked, a timing test that passes alone. Not tried through a real proxy server or a real public mirror; no install.ps1 exists for Windows, and the app has no field for the mirror.
2026-10-04 00:19:47 +08:00
test: every package's tests pass on Windows, and none writes outside its sandbox (the user: 把 Windows 上那几个失败的测试修掉) — on a Windows box the suite failed in most packages, and the root package hung for 25 minutes. Tests set HOME, which os.UserHomeDir ignores on Windows (it reads USERPROFILE), so they read and wrote the runner's own home; each now sets USERPROFILE beside HOME, and Devin's, Goose's, Crush's and Claude Desktop's tests set APPDATA/LOCALAPPDATA, where those keep their files there. said() read the command's output only once it returned, and a pipe holds a few KB on Windows, so magpie model --help blocked forever; it reads while the command writes. Unix permission bits (the gateway key store's 0600 too), exec bits and symlinks Windows lacks are checked only off Windows; TOML strings hold Windows paths as literal strings, JSON as marshalled ones; a URL's path is compared as one. The provider and usage packages run in a home of their own (TestMain, as the gateway's already does), since a CLI's identity kept by a refresh still going when its test ended landed in the runner's ~/.config/magpie — and on a Mac in the user's own. Two product fixes came out of it: imagemcp took a file:///C:/… root or image for the path /C:/…, which doesn't exist on Windows, and now makes a Windows path of it (a host for a UNC share); and session discovery could keep a new Cowork session hidden on Linux, where a folder's mtime is a kernel tick and its size doesn't change, so an entry added within the tick of the last listing left both as they were — a listing made within 2s of its folder's last change is read again, as git does for racy files. TestFilePath and TestRacyDirectoryListedAgain fail without the change. On Windows 11 every package passes (32 of 32) and a run of each package in a fresh home leaves nothing in home, APPDATA or LOCALAPPDATA; on Linux (WSL) and macOS go test -tags nogui ./... passes. go vet, go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build pass.
2026-10-01 23:15:07 +08:00
models: Provider in model names has a third way, a name the user gave used just as they wrote it (#92 marsxxl: v0.1.591 的 Codex 下拉里自定义名称又带上了 · WorkBuddy / · Claude Code) — 559ce021 (#203) had a custom name keep " · <provider>" after it like any other, so "Opus 5.5" read "Opus 5.5 · Claude Code" in every list Labels feeds (Codex's model_catalog_json and the gateway's Codex /models, Pi's, OpenCode's, ZCode's, OMP's, Muse's), while #92 had been promised the name as written. Both are kept: the setting is now Off / Not on names I set (自定义名称不带供应商) / On. On stays the default and keeps #203's behaviour, so nobody's list changes unasked; own leaves a name the user gave as it is and a vendor's name with its provider's; off is #335's plain names. Stored as plainOwnNames beside plainNames, so a settings.json of before reads as it did (plainNames: true is off). POST /api/settings/plain-names takes mode on|own|off (a body of on alone still works), magpie model suffix on|own|off, a Settings save keeps it, and /v1/models' magpie_label now follows the setting through Labels as the agents' lists do. The row's hint is one line that fits (en, zh), and its zh name says 供应商 as the rest of the app. Prompts and headers are unchanged. TestSuffixMode, TestSuffixModesReachAgentFiles (Codex model_catalog_json and Pi models.json), TestModelsListSuffixModes (/v1/models magpie_label) and model-suffix-own.test.cjs (Chromium and WebKit, en and zh) fail without the change; TestModelName and TestModelNameAndLevelsReachAgentFiles keep #203's assertions. go vet, go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui ./... pass (TestConfiguredModelsAdvertiseImageInput load-flaky, passes alone); plain-names, click-scroll, old-webkit pass.
2026-10-01 23:20:57 +08:00
models: Provider in model names has a third way, a name the user gave used just as they wrote it (#92 marsxxl: v0.1.591 的 Codex 下拉里自定义名称又带上了 · WorkBuddy / · Claude Code) — 559ce021 (#203) had a custom name keep " · <provider>" after it like any other, so "Opus 5.5" read "Opus 5.5 · Claude Code" in every list Labels feeds (Codex's model_catalog_json and the gateway's Codex /models, Pi's, OpenCode's, ZCode's, OMP's, Muse's), while #92 had been promised the name as written. Both are kept: the setting is now Off / Not on names I set (自定义名称不带供应商) / On. On stays the default and keeps #203's behaviour, so nobody's list changes unasked; own leaves a name the user gave as it is and a vendor's name with its provider's; off is #335's plain names. Stored as plainOwnNames beside plainNames, so a settings.json of before reads as it did (plainNames: true is off). POST /api/settings/plain-names takes mode on|own|off (a body of on alone still works), magpie model suffix on|own|off, a Settings save keeps it, and /v1/models' magpie_label now follows the setting through Labels as the agents' lists do. The row's hint is one line that fits (en, zh), and its zh name says 供应商 as the rest of the app. Prompts and headers are unchanged. TestSuffixMode, TestSuffixModesReachAgentFiles (Codex model_catalog_json and Pi models.json), TestModelsListSuffixModes (/v1/models magpie_label) and model-suffix-own.test.cjs (Chromium and WebKit, en and zh) fail without the change; TestModelName and TestModelNameAndLevelsReachAgentFiles keep #203's assertions. go vet, go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui ./... pass (TestConfiguredModelsAdvertiseImageInput load-flaky, passes alone); plain-names, click-scroll, old-webkit pass.
2026-10-01 23:20:57 +08:00
providers: a built-in subscription whose plugin the user installed is the plugin's, and the "+ provider" sheet lists it once (ARNO on Discord: the sheet had Qoder 国际版 and Qoder CN under 订阅, and Qoder ● 2 and Qoder CN ● under 来自插件, the same two subscriptions twice) — a deprecated built-in hands its id to its plugin only through an explicit Move (the editor's Move to plugin) or Adopt (clicking the built-in's tile before signing in, since v0.1.642); the automatic list, Retiring, is empty, and f099d07e (v0.1.659) gave Qoder CN its mover but moves nobody on its own. A user who installed the Qoder plugin from Plugins was never moved: the plugin's providers took the ids qoder-plugin and qoder-cn-plugin (provider.PluginID, as the built-in ids were not moved), the add sheet listed those under 来自插件, and the built-ins stayed under 订阅 as deprecated tiles. Qoder international (moved since 09-30) was listed twice the same way. Existing migration logic, unchanged: movers (built-in id → plugin package, min version, out/back of its accounts), Move (each account checked through the plugin, everything put back on any failure), Adopt (no accounts: install the plugin and mark it moved), MoveBack, the migrations.json states (moving, plugin, back, failed, a failed one retried after 6 h), and KeepRetiringMoved in the gateway (20 s after start, then hourly). Added: provider.HandOver, for each mover whose plugin is installed, enabled and at least its min, serving the built-in's id, not signed in under its own -plugin id, and not moved back or failed in the last 6 h: with no built-in accounts it Adopts at once; with accounts it Moves them, with Move's rollback, only from the hourly loop (HandOver(ctx, true) beside MoveRetiring). The GUI's plugin add/update/upgrade and the CLI's plugin add/update call HandOver(ctx, false) once they succeed. One the plugin is already signed in to under its -plugin id is left as it is: handing the id over would rename the models agents use (qoder-plugin/... to qoder/...). Until a hand-over happens, and whenever it can't, the add sheet hides a deprecated built-in whose plugin serves its id (replacedSub in app.js), keeping it only while it is signing in; a moved one was already one tile. With the plugin not installed the built-in tile stays, as clicking it is how a new user gets the plugin. A provider signed in through the built-in keeps working and stays in the provider list. AGENTS.md says so. The manual Check for updates button for plugins that was also asked for is not shipped: per the owner, official community plugins update on their own. TestHandOver (a fake plugin under a temp HOME: nothing without the plugin; no accounts: moved at once; moved back: left; built-in accounts: left by HandOver(false), moved by HandOver(true), the built-in's emptied and the plugin signed in; the plugin signed in on its own: left) is new. plugin-replaced.test.cjs (WebKit, en and zh: no plugin, both built-ins under Subscriptions; the plugin installed with Qoder signed in to two accounts, the plugin's two tiles alone; Qoder moved, its one tile under Subscriptions and Qoder CN the plugin's; a built-in Qoder CN signed in, its provider row still listed) fails without the app.js change (all four tiles) and passes with it. deprecated-subscriptions, plugin-move, click-scroll, add-button, mobile-web, signin-paste-key and every plugin-* test pass in WebKit. go vet ./internal/..., go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui -count=1 ./... under a temp HOME pass (TestFXInState failed on its TempDir cleanup, the known flake).
2026-10-02 20:31:19 +08:00
test: every package's tests pass on Windows, and none writes outside its sandbox (the user: 把 Windows 上那几个失败的测试修掉) — on a Windows box the suite failed in most packages, and the root package hung for 25 minutes. Tests set HOME, which os.UserHomeDir ignores on Windows (it reads USERPROFILE), so they read and wrote the runner's own home; each now sets USERPROFILE beside HOME, and Devin's, Goose's, Crush's and Claude Desktop's tests set APPDATA/LOCALAPPDATA, where those keep their files there. said() read the command's output only once it returned, and a pipe holds a few KB on Windows, so magpie model --help blocked forever; it reads while the command writes. Unix permission bits (the gateway key store's 0600 too), exec bits and symlinks Windows lacks are checked only off Windows; TOML strings hold Windows paths as literal strings, JSON as marshalled ones; a URL's path is compared as one. The provider and usage packages run in a home of their own (TestMain, as the gateway's already does), since a CLI's identity kept by a refresh still going when its test ended landed in the runner's ~/.config/magpie — and on a Mac in the user's own. Two product fixes came out of it: imagemcp took a file:///C:/… root or image for the path /C:/…, which doesn't exist on Windows, and now makes a Windows path of it (a host for a UNC share); and session discovery could keep a new Cowork session hidden on Linux, where a folder's mtime is a kernel tick and its size doesn't change, so an entry added within the tick of the last listing left both as they were — a listing made within 2s of its folder's last change is read again, as git does for racy files. TestFilePath and TestRacyDirectoryListedAgain fail without the change. On Windows 11 every package passes (32 of 32) and a run of each package in a fresh home leaves nothing in home, APPDATA or LOCALAPPDATA; on Linux (WSL) and macOS go test -tags nogui ./... passes. go vet, go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build pass.
2026-10-01 23:15:07 +08:00
provider: Bailian's decision model as a routing preset, and System One for custom providers (#647, Aimer779: Alibaba Cloud Bailian's decision-model-preview speaks TypeSafe's System One at POST <root>/systemone, and magpie only took it from TypeSafe's Jev) — a new preset, Bailian Decision Model (qwen icon, decide-only like TypeSafe Jev), asks decision-model-preview at its plan's root: Workspace · Beijing / Workspace · Singapore (https://{WorkspaceId}.<region>.maas.aliyuncs.com/compatible-mode/v1, the editor asks for the workspace ID and fills it in) or Token Plan · Beijing (token-plan.cn-beijing…, sk-sp- keys). Saving with the workspace still unfilled is refused, both in the editor and in provider.Add. A Bailian host has no /models for it, so its list is the one model, checked by a System One ask. A custom provider can now be a System One API: the CLI's decide=<root> (with models=… named anything, not only jev-*), workspace= and region=/plan= for a preset's plan; the editor's API seg has a fourth option, System One, whose Detect APIs posts a test question to <root>/systemone with the model typed, and a decide-only custom provider passes the "base URL is needed" check. A decide-only provider's first model is what it is asked as (Jev() no longer assumes jev-*), and its models stay routing groups' classifiers, never agents'. TestBailianDecisionPreset, TestCustomSystemOne, TestProviderSystemOne and TestProviderDecidePairs fail on the old code (undefined BailianDecision / DetectDecide, unknown decide=); internal/gui/tests/system-one.test.cjs (webkit, en/zh) covers the workspace, the Singapore and Token Plan plans, TypeSafe's unchanged editor and a custom System One detected and added. go vet, builds (darwin, GOOS=linux, GOOS=windows) and go test -tags nogui -count=1 ./... under a temp HOME pass but TestProvidersAnswerWhileListsComeIn, a timing test that passes alone. Not tried against a real Bailian key.
2026-10-03 15:21:15 +08:00
provider: a providers.json that can't be read is said where the providers are listed, never backed up or synced as none, and is replaced through a rename (PR #415's review) — #415 made edits refuse a file they couldn't read or decode, but every read for the user still took it as empty: magpie providers said "no providers yet", magpie models "no models yet", the Providers page opened the Add sheet as for a first use, and backup.Collect, so an export and a WebDAV/S3 sync, carried it as no providers, which another computer's Mirror would then take as every provider removed. magpie providers and magpie models now list what is left (the signed-in accounts) and end in the error, the TUI says it over its list, and the Providers page shows a "Your providers file can't be read" box (无法读取供应商配置文件) over the list with magpie's reason and no Add sheet; start-up, the gateway and the catalog still go on with it as empty. Stored and StoredGroups return the read error, so a backup and a sync stop on it with nothing pushed. The error, for reads and refused writes alike, names the file, says magpie left it unchanged and to fix it or move it aside (errors.Is ErrUnreadable). store writes through writePrivate, a 0600 temp file renamed over it (steady.Rename waits out a Windows reader holding it, a symlink keeps its target), instead of os.WriteFile's truncate-then-write, and the read goes through steady.ReadFile. TestFileErrorSaysUnreadable, TestWriteErrorsSayLeftUnchanged, TestStoredRefusesUnreadable, TestStoreReplacesAtomically, TestSyncStopsOnUnreadableProviders, TestProvidersCmdSaysUnreadable and provider-file-error.test.cjs fail without the change. go vet, go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui ./... pass; provider-file-error, click-scroll, old-webkit, providers-off-fold, add-sheet pass.
2026-10-01 19:45:24 +08:00
quota: each subscription account, plan and key says when it last answered, and the latest is marked last (#570: quota --json / GET /v1/magpie/quotas 增加每个订阅、plan、key 最近一次应答请求的时间) — under routing Smart nobody is first for good, so a menu-bar widget had nothing to tell the one in use by: a Quota's asOf is when its reading was taken, accounts --json covers signed-in Codex and Claude accounts only, and /v1/magpie/route wants a session. Each Quota in magpie quota --json and GET /v1/magpie/quotas now has lastServedAt (RFC3339, left out when it hasn't answered) and last: true on the latest (each of a key's plan and balance, which are one key). The gateway notes each answer where servedCandidate counts it (provider.NoteServed): an account by its agent and user, a key by its provider and key id, matched to a card by its user, or by the key's name or mask, or, for a card with no user, by any of its provider's. It is kept in served.json beside providers.json, entries older than 30 days dropped, since magpie quota runs in a process of its own and computes the report itself (provider.QuotaReport) rather than asking the gateway: both read the file, so they say the same, and a restart keeps it. TestQuotasLastServedAt (gateway: a request answered by one key, its balance card has lastServedAt and last, the other key's neither) and TestQuotaJSONLastServedAt (the CLI's --json, fake CLIs and keychain on PATH) fail without the change; TestQuotasLastServed covers the matching (account by user, key by name and by mask, a card with no user, the tie, read back from the file alone). README tells the fields. go vet ./internal/..., go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui -count=1 ./... under a temp HOME pass.
2026-10-02 22:16:17 +08:00
quota: each subscription account, plan and key says when it last answered, and the latest is marked last (#570: quota --json / GET /v1/magpie/quotas 增加每个订阅、plan、key 最近一次应答请求的时间) — under routing Smart nobody is first for good, so a menu-bar widget had nothing to tell the one in use by: a Quota's asOf is when its reading was taken, accounts --json covers signed-in Codex and Claude accounts only, and /v1/magpie/route wants a session. Each Quota in magpie quota --json and GET /v1/magpie/quotas now has lastServedAt (RFC3339, left out when it hasn't answered) and last: true on the latest (each of a key's plan and balance, which are one key). The gateway notes each answer where servedCandidate counts it (provider.NoteServed): an account by its agent and user, a key by its provider and key id, matched to a card by its user, or by the key's name or mask, or, for a card with no user, by any of its provider's. It is kept in served.json beside providers.json, entries older than 30 days dropped, since magpie quota runs in a process of its own and computes the report itself (provider.QuotaReport) rather than asking the gateway: both read the file, so they say the same, and a restart keeps it. TestQuotasLastServedAt (gateway: a request answered by one key, its balance card has lastServedAt and last, the other key's neither) and TestQuotaJSONLastServedAt (the CLI's --json, fake CLIs and keychain on PATH) fail without the change; TestQuotasLastServed covers the matching (account by user, key by name and by mask, a card with no user, the tie, read back from the file alone). README tells the fields. go vet ./internal/..., go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui -count=1 ./... under a temp HOME pass.
2026-10-02 22:16:17 +08:00
2026-10-01 19:44:29 +08:00
test: every package's tests pass on Windows, and none writes outside its sandbox (the user: 把 Windows 上那几个失败的测试修掉) — on a Windows box the suite failed in most packages, and the root package hung for 25 minutes. Tests set HOME, which os.UserHomeDir ignores on Windows (it reads USERPROFILE), so they read and wrote the runner's own home; each now sets USERPROFILE beside HOME, and Devin's, Goose's, Crush's and Claude Desktop's tests set APPDATA/LOCALAPPDATA, where those keep their files there. said() read the command's output only once it returned, and a pipe holds a few KB on Windows, so magpie model --help blocked forever; it reads while the command writes. Unix permission bits (the gateway key store's 0600 too), exec bits and symlinks Windows lacks are checked only off Windows; TOML strings hold Windows paths as literal strings, JSON as marshalled ones; a URL's path is compared as one. The provider and usage packages run in a home of their own (TestMain, as the gateway's already does), since a CLI's identity kept by a refresh still going when its test ended landed in the runner's ~/.config/magpie — and on a Mac in the user's own. Two product fixes came out of it: imagemcp took a file:///C:/… root or image for the path /C:/…, which doesn't exist on Windows, and now makes a Windows path of it (a host for a UNC share); and session discovery could keep a new Cowork session hidden on Linux, where a folder's mtime is a kernel tick and its size doesn't change, so an entry added within the tick of the last listing left both as they were — a listing made within 2s of its folder's last change is read again, as git does for racy files. TestFilePath and TestRacyDirectoryListedAgain fail without the change. On Windows 11 every package passes (32 of 32) and a run of each package in a fresh home leaves nothing in home, APPDATA or LOCALAPPDATA; on Linux (WSL) and macOS go test -tags nogui ./... passes. go vet, go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build pass.
2026-10-01 23:15:07 +08:00
gateway: a model's web search goes to a search API of the user's own when no provider can search (#419 by HwFee: let me set up Tavily, Brave or SearXNG with my key rather than lose the search tool) — with no provider that searches by itself and no provider magpie could search with, the gateway took the client's web search tool away. Tavily, Brave Search, Exa, Firecrawl and SearXNG (an address of one's own, a key only if it asks for one) can now be set up, in an order: the injected web_search tool is then offered, and a search goes to the searcher's model as before, or, with none or when it fails, to each search API in turn, called directly with no model, its results handed back as the same hits and text the searcher path gives, so clients see the same web_search_tool_result blocks. The order is provider searches itself, searcher provider, search APIs, strip. They are kept in providers.json (0600) like provider keys; a backup carries them, their keys only when it carries keys, and one restored without keys keeps the key here; sync does the same, and a backup or sync from a magpie before them leaves the ones here alone. magpie search lists them, magpie search add <api> [key] [url=…] adds or updates one, magpie search rm <api> removes one; Settings has a Web search section (联网搜索) to add and remove them. Prompts, identity lines and headers to model providers are unchanged. TestWebSearchBySearchAPI, TestNoSearchAPINoTool, TestBackupSearchAPIs, TestSyncSearchAPIs and search-api.test.cjs fail without the change; TestSearchAPIsEachAsked checks each vendor against a fake, TestSearchCLI the command. go vet, go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui ./... pass; search-api, provider-search, click-scroll, old-webkit pass.
2026-10-01 19:44:44 +08:00
update: install.sh and magpie update take --proxy and --mirror, and magpie update mirror keeps one for the app's updates too (akic404 on Discord: 安装和update如果没有梯子也是麻烦事,能否在install.sh和magpie update后加子选项?--proxy等可自定义或默认ghproxy) — install.sh had no options and fetched with curl alone, and magpie update took none; without a way out to GitHub both failed. Now both take --proxy <url> (http, https, socks5, socks5h; install.sh also MAGPIE_PROXY=) for the feed and the download, and --mirror <prefix> (or MAGPIE_MIRROR=), a GitHub download mirror the release file is fetched from as <prefix>https://github.com/…; only github.com / githubusercontent.com URLs are put through it. No mirror is used unless one is given: magpie names none and has none by default. The feed with each file's SHA-256 is always fetched from usemagpie.ai, never through the mirror, so a file the mirror changed is refused and deleted ("… from the mirror X does not match its checksum from https://usemagpie.ai; not installed"); macOS still checks the app's signing team. magpie update mirror <prefix>|off saves Settings' updateMirror (http(s) only), which the app's own background update follows too; magpie update --mirror off skips it once; the Settings page's save keeps it. The app's update already followed Settings → Proxy (#294); magpie update --proxy overrides it for one run. The environment's ALL_PROXY / all_proxy is now honoured when HTTPS_PROXY / HTTP_PROXY are unset (Go's ProxyFromEnvironment ignores it), with NO_PROXY as curl reads it. magpie update takes --flag value or --flag=value, and an unknown word is an error instead of an update. The help, README and the docs' FAQ (en/zh/ja) say how. install.test.mjs's proxy, mirror and tampered-mirror tests, TestAllProxyFromTheEnvironment, TestUpdateThroughAMirror, TestUpdateThroughAProxyGiven and TestUpdateFlags fail on the old code and pass; TestSettingsSaveKeepsWhatItDoesNotSend covers updateMirror. A dry run of install.sh as Linux amd64 into a temp HOME installed the real 0.1.773 with its SHA-256 checked, refused a local mirror serving a wrong file, and with --proxy to a closed port failed reaching the site through it. go vet, builds (darwin, GOOS=linux, GOOS=windows), sh -n, node --test site/install.test.mjs and go test -tags nogui -count=1 ./... under a temp HOME pass but TestClaudeIdentityServedWhileAsked, a timing test that passes alone. Not tried through a real proxy server or a real public mirror; no install.ps1 exists for Windows, and the app has no field for the mirror.
2026-10-04 00:19:47 +08:00
update: install.sh and magpie update take --proxy and --mirror, and magpie update mirror keeps one for the app's updates too (akic404 on Discord: 安装和update如果没有梯子也是麻烦事,能否在install.sh和magpie update后加子选项?--proxy等可自定义或默认ghproxy) — install.sh had no options and fetched with curl alone, and magpie update took none; without a way out to GitHub both failed. Now both take --proxy <url> (http, https, socks5, socks5h; install.sh also MAGPIE_PROXY=) for the feed and the download, and --mirror <prefix> (or MAGPIE_MIRROR=), a GitHub download mirror the release file is fetched from as <prefix>https://github.com/…; only github.com / githubusercontent.com URLs are put through it. No mirror is used unless one is given: magpie names none and has none by default. The feed with each file's SHA-256 is always fetched from usemagpie.ai, never through the mirror, so a file the mirror changed is refused and deleted ("… from the mirror X does not match its checksum from https://usemagpie.ai; not installed"); macOS still checks the app's signing team. magpie update mirror <prefix>|off saves Settings' updateMirror (http(s) only), which the app's own background update follows too; magpie update --mirror off skips it once; the Settings page's save keeps it. The app's update already followed Settings → Proxy (#294); magpie update --proxy overrides it for one run. The environment's ALL_PROXY / all_proxy is now honoured when HTTPS_PROXY / HTTP_PROXY are unset (Go's ProxyFromEnvironment ignores it), with NO_PROXY as curl reads it. magpie update takes --flag value or --flag=value, and an unknown word is an error instead of an update. The help, README and the docs' FAQ (en/zh/ja) say how. install.test.mjs's proxy, mirror and tampered-mirror tests, TestAllProxyFromTheEnvironment, TestUpdateThroughAMirror, TestUpdateThroughAProxyGiven and TestUpdateFlags fail on the old code and pass; TestSettingsSaveKeepsWhatItDoesNotSend covers updateMirror. A dry run of install.sh as Linux amd64 into a temp HOME installed the real 0.1.773 with its SHA-256 checked, refused a local mirror serving a wrong file, and with --proxy to a closed port failed reaching the site through it. go vet, builds (darwin, GOOS=linux, GOOS=windows), sh -n, node --test site/install.test.mjs and go test -tags nogui -count=1 ./... under a temp HOME pass but TestClaudeIdentityServedWhileAsked, a timing test that passes alone. Not tried through a real proxy server or a real public mirror; no install.ps1 exists for Windows, and the app has no field for the mirror.
2026-10-04 00:19:47 +08:00
usage: each subscription account's tokens and cost, by the account that answered (#557: 希望用量账本能按订阅账号统计,每个 Codex / Claude 账号各用了多少 token、多少钱) — a call's record named the upstream key it went out on (ProviderKeyID/Name, #500) but never the subscription account, so with two Codex accounts signed in, or a Claude account that took over after a failover, the ledger could say what Codex cost and not what each account did. A record now carries providerAccount, the account that answered as the Routing trace names it (Account.User: its email or login, never a token), set by the gateway at the try that went out (the candidate loop's last try, so a failover's answer is the account that took over and the refused try stays the first account's; an X-Magpie-Account pin is the pinned one), and on the decide, draw and video paths too. Record.Account() gives it, or for a record written before the field the account its host named (Where() has long written "chatgpt.com as dee@example.com" for an account), and otherwise "" — shown as "account not recorded", never inferred from today's sign-in. The summary groups calls by provider and account (Summary.Accounts, the same email on Codex and on Claude being two accounts), priced as every other group; the ledger's Filter has Account, the search finds a call by its account, the CSV has a provider_account column beside provider_key_name (the provider's identity columns together, the caller's still last), and the Requests page's packed rows keep it and offer the period's accounts. magpie usage prints an accounts table after the provider keys, and magpie usage --csv --account <name> keeps one account's calls. The app's Usage page lists Accounts on the Overview (tokens and cost per account, the provider under its name, an unrecorded one in the faint colour, no stripe down a row's side) and has an Account filter on Requests, each account noting the providers it answered for, which asks for and exports that account's calls without moving the page. OTLP export is an allowlist and does not carry the account. TestUsageNamesPinnedAccount (both pinned accounts on magpie's endpoint and on Codex's own, the trace's Who and the record agreeing, no token in usage.jsonl) and TestUsageNamesAccountThatTookOver (Ordered routing, the first account refusing: the answer recorded as the spare, the refusal as the first) fail without the gateway change; TestSummaryByAccount, TestLedgerByAccount and TestRequestPageByAccount cover the grouping, the host fallback, "not recorded", keyed providers kept out, the filter, the search, the CSV column and the packed rows; TestLedger and TestLedgerWithSessionLogCalls take the new column. usage-accounts.test.cjs (WebKit and Chromium, en and zh: the Overview's accounts and costs, the Account picker's notes, its request and export, All accounts clearing it, scrollTop unchanged) fails without the asset change; api-key-usage, click-scroll, panel-usage, request-archive-usage, tray-usages, claude-usage-asked and every usage-* test pass in WebKit. go vet ./internal/..., go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui -count=1 ./... under a temp HOME pass (TestFXInState failed once on its TempDir cleanup, passes alone).
2026-10-02 20:15:33 +08:00
test: one list of the agent folders a sandbox clears, so no sandbox misses an agent (yetone, on #522: A shared list of env vars for the other sandboxes would be a welcome follow-up) — every sandbox kept its own list of the variables that point at an agent's folder, and each had drifted: the gateway's cleared 5 of the 32 magpie reads, the provider package's 2, the tui's 10, the library's 20. A sandbox clearing 2 meets whatever Grok, Kimi, Hermes, MiniMax, Qoder or ZCode the developer has installed, which is how #522 came about — Qoder's two were in the library's list and in none of the four that failed. internal/agentenv is now the one list, and the 21 sandboxes that fake an environment with no agent in it clear it, each keeping beside it the variables that are not an agent's: APPDATA and LOCALAPPDATA, which a sandbox sets to a folder of its own rather than clearing, VISUAL and EDITOR. A name in the list is one magpie reads, and TestVarsAreRead walks the sources to say so, which is how two names a sandbox had been clearing turned out to be read by nothing at all — GEMINI_CLI_HOME and OPENCODE_CONFIG in internal/backup's, while OPENCODE_CONFIG_DIR, the one internal/agent reads (agents.go:545), was not in it. Where a sandbox set an agent's folder to a fixture of its own, the clearing now comes first, since a list of 32 wipes what a list of 5 left standing. Not converted: the omp tests' four variables, which are the ones omp's own lookup consults (a Pi fork) rather than every agent's, and the warm-up's Claude Code traffic flags, which are not folders — CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC stays out of the list for that reason too, its one test setting it on purpose. On Windows 11 the whole suite answers as it did before the change: 31 packages pass, and the same 15 tests in internal/library, internal/gateway and internal/agent fail for what they failed for before (symlinks this machine has no privilege for, and TestSuffixModesReachAgentFiles). go vet ./..., go build -tags nogui (windows, GOOS=linux, GOOS=darwin), and go vet -tags nogui for linux and darwin over every package touched pass. (#543)
2026-10-02 17:22:16 +08:00
sync: a backup on a slow line is no longer cut off at 2 minutes; a server that stops moving is (#657, irx999: a 43 MB backup with many skills to a WebDAV server on a 2.4 Mbps VPS, 305 KB/s, needs 2.4 minutes and every sync failed) — a sync had a fixed deadline in all (2 minutes from the gateway's loop and magpie webdav/s3 sync, 90 s from Settings › Sync now), so a large backup on a slow line never got through, however steadily it moved. WebDAV and S3 requests now go through a watchdog instead (stall.go): the backup's GET and PUT fail once nothing has moved either way for 60 s, each byte of the body sent or read putting it back, and after a PUT's body is all handed over the answer gets 60 s on top of as long as the write took (at least 4 minutes), for what still sits in the system's buffers to leave; the other requests (PROPFIND, MKCOL, HEAD, MOVE, DELETE) have 30 s in all. A stall says so ("the server sent and took nothing for 1m0s (PUT)"). Sync now and the CLI have no deadline; the gateway's loop keeps an hour as a backstop; a wait for another magpie's sync stays 30 s. TestSlowTransferIsNotCutShort (20 MB up and down at a steady trickle, over 5 times the watchdog each way), TestStalledTransferFailsPromptly (a server stopping mid-PUT and mid-GET fails within a second) and TestMetadataRequestHasALimit fail on the old code (no watchdog: the stalled ones hang) and pass. go vet, builds (darwin, GOOS=linux, GOOS=windows) and go test -tags nogui -count=1 ./... under a temp HOME pass. Not tried against a real slow WebDAV server.
2026-10-03 16:10:02 +08:00

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

Discord

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.jsonc or config.yaml survive 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/v1 and 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, PipeLLM, 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 /model included, 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
Zed ~/.config/zed/settings.json ($XDG_CONFIG_HOME on Linux, %APPDATA%\Zed on Windows) model (a magpie OpenAI-compatible provider; its catalog in Zed's picker)
Copilot CLI ~/.copilot/settings.json model
Crush ~/.config/crush/crush.json large, small
DeepSeek Harness (dsh) ~/.dsh/profiles/*/cordis.patch.yml ($DSH_HOME; a custom provider, Magpie), or ~/.dsh/config.yaml before dsh 0.1.5 model, effort
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)
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)
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)
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)
T3 Code ~/.t3/userdata/settings.json ($T3CODE_HOME/userdata) provider (a magpie provider instance on Claude Code, magpie's models as its custom models)
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 refresh deepseek        # re-fetch it, and drop picks it no longer has (the TUI: m)
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.

magpie usage also lists upstream provider keys to help check upstream bills. Each request records the fingerprint and saved name of the key that actually served it, including image calls and account/key failover. The CSV adds provider_key_id and provider_key_name. JSON uses providerKeyId and providerKeyName, distinct from gateway caller keys. System One calls use the same attribution. No raw credential is stored in usage records. Rotating the provider's first key does not move old usage to its replacement; deleted keys keep their historical identity. Older records appear as key not recorded, never inferred from today's configured key. These are upstream credentials, not keys clients use to call Magpie.

It lists accounts too: each Codex, Claude or other subscription account's tokens and cost, by the account that actually answered — the one that took over after a failover, the one X-Magpie-Account pinned. The account is named as the Routing view names it (its email or login, never a token); the CSV adds provider_account (JSON providerAccount), magpie usage --csv --account <name> keeps one account's calls, and the app's Usage page has an Accounts list and an Account filter on Requests. An older record names the account its call went out as when its host says so (chatgpt.com as dee@example.com); otherwise it appears as account not recorded, never inferred from today's sign-in. OTLP export never carries the account.

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, on the TUI's Providers page (a, then its address and key; w changes the address, m fetches its list again), 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. Its list is the models the shared magpie's agents are shown, each named with its provider there (Claude Sonnet 5 · Relay A · office) — the ids stay ids (office/relay-a/claude-sonnet-5), only these labels carry the names — and its image models are listed under Settings → Images and draw through it.

The gateway issues gateway keys for clients, separate from a provider's upstream API keys. Turn on Settings → Share on local network, then open Gateway → Gateway keys → Add gateway key. This block appears only while sharing is on. Create a named key for each client and copy it from its row. Rename, disable, rotate or remove keys independently; rotation and removal ask for confirmation. Rotation keeps the name, enabled state and usage history; other keys are unchanged. While sharing is on, Gateway → Connect offers loopback and shared addresses, plus enabled gateway keys, for all connection examples. With sharing off, Connect keeps the original API key field and local magpie token, without a gateway-key picker. Its arbitrary local option is This computer, distinct from the named Magpie key.

For a headless gateway, use the CLI before exposing the port:

magpie gateway-key add "Remote laptop" # prints the new credential once
magpie gateway-key list                # ids, names, enabled state and masked keys
magpie gateway-key rotate <id>         # prints the replacement; identity stays the same
magpie gateway-key remove <id>         # revokes remote access
magpie gateway-key limit <id> week --tokens 2m --cost 5   # its own limit
magpie gateway-key limit <id>          # limit, used, left and reset
magpie gateway-key limit <id> off      # no limit

Each gateway key can have its own limit: a token total, an estimated cost in US$, or both, per day, week or month (calendar windows in local time: from midnight, from Monday, from the 1st). Set it with Limit on the key's row (saved with Save) or magpie gateway-key limit; the row shows what the key has used, what is left and when it resets. Tokens counted are a call's uncached input, output and cache writes, plus cache reads when Count cache reads too is on. Cost is an estimate at the Usage page's prices; a call with no known price adds none. A call counts in the window it started in. Once a key is spent, its requests are refused before any provider is asked, with a 429 in the API's own error shape that names the key, the limit and the reset time, plus Retry-After; other keys are unaffected. The counts are read from the usage log, so they survive a restart. A request in flight holds a reservation (its body's size in tokens plus the key's mean output per call), so requests sent at once overshoot by about one call; a streamed reply is settled when it ends with the usage its vendor reported. A key can read its own status with GET /v1/magpie/limit. Requests from this computer that send no gateway key are not limited; a gateway key used from this computer is.

While LAN sharing is enabled, remote requests require an enabled gateway key sent as Bearer, x-api-key, x-goog-api-key or ?key=. Loopback remains permissive: any token works, including a stale or disabled gateway key. Only a valid, enabled key is attributed to its named identity. Without sharing, an explicitly exposed MAGPIE_ADDR keeps its original open access, including old sk-magpie-… tokens, without key authentication.

Usage → Overview → Gateway keys groups calls by the client's key, never the provider's credential. Usage → Requests offers the same filter; CSV includes caller_key_id and caller_key_name. Deleted keys keep their history. The usual local magpie token and older records stay unattributed. Caller attribution includes chat, images, video creation and System One. Provider attribution stays independent; CSV puts provider_key_* before caller_key_*. An existing LAN key becomes Magpie without changing the credential. lanKey remains in settings for older Magpie versions. Disabling or removing the default key replaces that mirror with a random non-empty revoked value; rotation does not re-enable it. The key store records migration completion, even if lanKeyId cannot be saved, so reads do not keep retrying that write. A migration write failure is logged without preventing gateway startup, CLI key management, or the Settings and key-list pages from opening. If settings are read-only, changing the default key fails without changing it: make settings.json writable and retry so older versions cannot keep accepting its old credential. Independent named keys remain manageable. Gateway credentials stay in ~/.config/magpie/caller-keys.json (XDG-aware, mode 0600), never in usage records or list responses.

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.

For plugin authors

A plugin is an OpenCode plugin; magpie reads a few more fields, which OpenCode ignores:

  • The provider's icon: icon on the auth hook, or "magpie": { "icon": "…" } in the plugin's package.json (for every provider it signs in to that names none). An https:// URL of a picture on a public host, which magpie fetches once and keeps, or a data:image/… URI; PNG, JPEG, GIF, WebP, ICO or SVG, at most 1 MB. Anything else is ignored, and the icon the plugin market lists for the plugin is shown instead.
  • An API key's field: a type: "api" method's label titles the key's field, as OpenCode's dialog does (one that only says "API key" reads " API key"), and its placeholder is the hint inside the field (and after the question in magpie plugin login). The method's prompts are asked first, as in OpenCode, and reach authorize(inputs).
export const LemonPlugin = async () => ({
  auth: {
    provider: "lemon",
    icon: "https://lemon.example/icon.png", // or "data:image/svg+xml;base64,…"
    methods: [
      { type: "api", label: "Lemon API key (lemon.example/keys)", placeholder: "sk-lemon-…" },
    ],
  },
})

In TypeScript, icon and placeholder aren't in OpenCode's types: build the hook as a variable (or cast it), or put the icon in package.json.

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. Records now identify upstream API keys, but a provider charging different tariffs per key still cannot be costed exactly from a single provider-wide price.

What a model takes

A provider that serves a model models.dev does not list, or lists at the wrong size, has a window and a reply limit magpie cannot know. Say what they are:

magpie model context relay-a/gpt-5.5 262144    # the window a request may hold, or 1m
magpie model output  relay-a/gpt-5.5 131072    # the most a reply may hold, or 128k
magpie model context "relay-a/*" 200000        # every model of that provider
magpie model output  relay-a/gpt-5.5 --reset   # take your limit off this model

The * is quoted because zsh treats a name it cannot expand as a command that failed, rather than passing the name on as bash does.

Both are looked for in this order: this model → this provider's * → the provider's own list → models.dev. --reset removes only the value this model has of its own.

A provider you keep unlisted, or switch off, takes them like any other: the numbers are kept, and are what its models take once it is serving again.

A window is a number agents are shown and a routing input: at 95% of the window a request held on a routing-group member moves to one that takes more, so overstating a window makes that move happen too late. A reply limit is advertised in /models and is what a group advertises the smallest of; the gateway does not itself cap a reply by it.

Saving a window or a reply limit writes that number into the model lists magpie keeps in the agents' own files — Pi's contextWindow and maxTokens, OpenCode's limit, and Crush's, droid's, Cline's, Qoder's and Zcode's — which an agent reads at start-up. A session already running therefore keeps the window it began with, while the gateway's own /models and every request from then on are right at once.

The name a vendor knows a model by

A relay often serves a model under an id of its own — a prefix it namespaces with, a dated name, a -latest that is not what models.dev calls it. Say which name to ask for:

magpie model wire relay-b/model-2                           # what the vendor is asked for
magpie model wire relay-b/model-2 vendor-c/model-2-preview  # ask for it by this
magpie model wire 'relay-b/*' 'vendor-c/*'                  # every model, * being the model
magpie model wire relay-b/model-2 --reset                   # ask for it by its own name again
magpie model wires                                          # every name your vendors are asked for models by

Quote the arguments with a * in them: zsh reads a bare * as a glob and answers no matches found. In the name, * stands for the model itself, so 'relay-b/*' 'vendor-c/*' sends vendor-c/model-3 for model-3 and vendor-c/model-2 for model-2 — one name for a relay that namespaces its models, each still asked for by its own. A name with no * in it sends every model of that key under that one name, which is the right answer only for a relay that does serve them all alike. The model's own key wins over the provider's, and --reset takes away only the one it is given, so resetting relay-b/model-2 while 'relay-b/*' is set leaves the provider's name in force — the CLI says which of the two is in force after every change, naming the models a name for the whole provider leaves to their own, and --reset over a name that was never given says so rather than ticking a removal that took nothing away.

Only the request that goes out carries that name, in the model field of a chat, Responses or Anthropic Messages request and of an Anthropic token count; image requests are left with the name magpie knows the model by. Gemini CLI and Antigravity sign-ins go on Code Assist, and there the model an effort picks is a variant of the model magpie knows — gemini-3.7-flash at high is sent as gemini-3.7-flash-high — so a * in the name is that variant: 'antigravity/*' 'vendor-c/*' asks for vendor-c/gemini-3.7-flash-high and vendor-c/gemini-3.7-flash-low each by its own, as it does everywhere else. A name with no * in it, or one given for antigravity/gemini-3.7-flash itself, is that one name at every level.

Everything else keeps the name magpie knows the model by: the catalog agents pick from, the routing groups' membership, GET /models, and what a call is recorded and priced as. What the vendor's own reply said answered is kept beside that, in the ledger's served_model, which is what it is for, and it is compared with the name the vendor was asked for — so a model answered under the relay's own id is not read as a swap. That is against the names in force when the ledger is read, since a record keeps what the vendor answered and not the name the request went out under: naming a model after the call re-judges that call, which is then left reading as a swap. What a provider supports — whether it takes a temperature, which reasoning levels fit — is still asked about the model magpie knows, so a rename upstream does not change how magpie behaves towards the model.

An upstream name is one provider's, not the model's: another provider serving the same id is asked for it under its own name, or this one. The model it is given for has to be one that provider serves — <provider>/* is the way to say one for every model — because a name for a model magpie would never ask the provider for is not a name of its own: the model is asked for by the name magpie knows it by, and nothing anywhere would say the name given for it is not the one in force. A relay that serves a model under an id of its own and lists it does get the name; what is refused is a model the provider's list does not have at all. The model test uses the name too, so a relay that only knows its own ids does not report a working model as broken. An image model is the exception that follows the rule above: it is tested by the name magpie knows it by, on the images API and on the chat it falls back to, because that is how a drawing is asked for.

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), usage (least used first) or pace (weekly pace: the account with the most of its week left per hour until it renews first, so less of a week is lost at its reset). 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 Routing page's Requests list defaults to the time-ordered By request view. Choose By session to group calls by the agent's session ID; the page remembers your choice across reloads. Codex title helpers with an explicit parent or fork source join their originating chat, retaining their title badge and contributing to its cost. Titles without ancestry and ordinary forked chats stay separate. Codex chat names come from its local name index and follow renames. Unknown or remote-only names fall back to the ID; the full ID remains in the heading tooltip. Expand a session to see each request. Each request and session shows its estimated cost at the effective model prices, including cache reads and writes. Session totals cover the listed requests only (the live trace or the selected day's retained history), and a + marks a partial estimate. Calls without a session ID are listed separately; old history without token tiers, or a model without a known price, shows —.

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. For reverse-proxied or container deployments, set MAGPIE_PUBLIC_URL=https://magpie.example.com to the base URL shown in the console and CLI, including connection examples. Local agent configs still use the local gateway address.

A reverse proxy must enforce authentication itself, or you must enable Settings → Share on local network and use an enabled gateway key (Gateway → Gateway keys) for external clients. A public URL with no port of its own — a reverse proxy's https://magpie.example.com — is the address magpie web prints for its own page too, so the proxy must forward /v1 and /v1beta to the gateway's port and the rest to the page's. When the proxy and magpie run on the same machine, requests forwarded over loopback are treated as local and need no key, so the proxy must authenticate those clients itself.

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 which 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.

A status bar can show where a turn went before its first token arrives: send X-Magpie-Session: <id> with the requests (an agent's own session header, such as Pi's or Claude Code's, works too) and read GET /v1/magpie/route?session=<id>. It answers the session's latest request as routing has it so far — asked (the model the agent named), group, rule (the group's rule that matched, and rule.pick, the effort its decision model picked), model and effort (the member being tried now, as provider/model, and the reasoning it was sent at), and tries, one per member tried, each failed one a fallback with its fail — then done, status and served once the reply is over; route is null before the session has one. The route appears once routing has decided, before the vendor is asked. after=<seq>&wait=<seconds> (up to 60) holds the answer until the route changes past the seq of the last one, so a UI can follow a turn with one request at a time. Only the session named is told; like /v1/magpie/quotas, it answers this machine, and another only with the key of a gateway shared on the local network.

To show the one in use without a session, GET /v1/magpie/quotas (and magpie quota --json, the same list) has lastServedAt on each subscription account, plan and key that answered a request through the gateway in the last 30 days, and last: true on the latest. It is kept in served.json beside providers.json, so a restart keeps it.

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.

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

Behind a firewall, or without access to GitHub, both the installer and magpie update take --proxy <url> (http, https, socks5, socks5h; or MAGPIE_PROXY, and HTTPS_PROXY / ALL_PROXY as usual) and --mirror <prefix>, a GitHub download mirror of your choosing put before the release's github.com URL (or MAGPIE_MIRROR; magpie update mirror <prefix>|off keeps one for every update, the app's too):

curl -fsSL https://usemagpie.ai/install.sh | sh -s -- --proxy http://127.0.0.1:7890
magpie update --mirror https://mirror.example/

No mirror is used unless you give one, and the SHA-256 every download is checked against still comes from usemagpie.ai, never from the mirror.

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.

Termux uses the Android terminal build: the installer puts it in $PREFIX/bin, and make build selects it automatically. With plain go build or go install, pass -tags nogui; use magpie web or magpie tui for the interface. magpie autostart on requires Termux:Boot.

Docker

docker build makes a server image: the terminal-only binary on distroless (cc, for the glibc the plugins' Bun needs), run as nonroot, with everything it keeps in a volume at /config: magpie's own files (/config/magpie), the sign-ins kept where their agent keeps them (HOME is /config/home, so ~/.codex, ~/.claude… are in it) and the cache with the Bun plugins run on (/config/cache, downloaded once). A volume made by an older image keeps working: magpie adds these folders to it on start, and only sign-ins made with that older image, which lived outside the volume, have to be made again.

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 in /config/magpie/settings.json). Turning it on in Settings creates a named Magpie key. With settings edited by hand, run magpie gateway-key add "Docker client" in the container to create a key without the browser UI. A request from outside the container must carry one of those keys as its API key. Only then publish the port beyond 127.0.0.1. Inside the container magpie only sees the container's own address (Docker's 172.17.x), so set -e MAGPIE_PUBLIC_URL=http://<the host's or NAS's address>:3425 (the port published on the host) for the address it shows and prints to be the one other machines use; behind a reverse proxy, set it to that external base URL and follow the authentication requirements above, especially when the proxy reaches magpie over loopback.

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, sign in to subscriptions and import sign-ins from a file. The vendor sends a sign-in's browser back to localhost (ChatGPT to http://localhost:1455/auth/callback?code=…), which is your own machine, not the container, so that page won't load: copy its whole address from the address bar and paste it into the sign-in's Callback URL field. docker exec -it magpie /magpie accounts add codex does the same in a terminal: open the link it prints, then paste the address the browser ended on. Keys, sign-ins and plugins live in the volume, so a restart, or a new container on the same volume, keeps them.

The image has a HEALTHCHECK: magpie healthcheck exits 0 while the gateway answers on MAGPIE_ADDR, under serve and web alike, so docker ps shows the container as healthy (Compose: depends_on: condition: service_healthy) with no curl in the image. Bind-mounting a folder at /config in place of a named volume works too; it has to be writable by uid 65532.

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.

In Usage → Requests and the tray's Usage tab, click a daily bar to see that day's totals and details. Other days turn gray; click the selected day again to return to the whole period. Changing the period clears the selection.

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. Gateway credentials, their names, ids and disabled state travel encrypted with Settings too. Restoring Settings replaces the gateway-key store with the backed-up one. --no-keys leaves gateway credentials and their legacy mirror out; restoring it preserves the destination's existing keys instead. 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:

  • endpoint is empty for AWS.
  • region defaults to us-east-1, or to auto on R2.
  • path-style=yes puts 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.

OTLP export

Settings → Observability can export gateway request metadata over OTLP/HTTP (JSON). Export is off by default. Set the collector's base URL and optional headers, then enable OTLP export. Export metrics is separately off by default; enable it for a collector that accepts duration and token histograms. No restart is needed for saved settings.

For magpie serve, environment variables override the saved preferences:

MAGPIE_OTEL_ENABLED=true MAGPIE_OTEL_ENDPOINT=http://localhost:4318 magpie serve
  • MAGPIE_OTEL_ENABLED: true or false; an endpoint alone does not enable export.
  • MAGPIE_OTEL_ENDPOINT: an HTTP(S) base URL; /v1/traces and /v1/metrics are appended.
  • MAGPIE_OTEL_HEADERS: comma-separated name=value pairs, for example Authorization=Bearer%20token. Percent-encode spaces and commas in values.
  • MAGPIE_OTEL_METRICS: true or false, off by default.
  • MAGPIE_OTEL_SESSIONS: true or false, off by default; trace supported local agent interactions from newly recorded session events.
  • MAGPIE_OTEL_BODIES: true or false, off by default; sends each call's request and reply as the trace's Langfuse input and output.
  • MAGPIE_OTEL_BODIES_WHOLE: true or false, off by default; with bodies on, keeps them entire rather than cut at 256 KB. A long reply is written to a temporary file; a body too large for the collector is still refused.

For Langfuse, use https://<your-langfuse-host>/api/public/otel as the base URL and Authorization=Basic%20<base64(public-key:secret-key)> as the header. Leave metrics off. This uses Langfuse's OTLP ingestion endpoint. For Langfuse v4, add the header x-langfuse-ingestion-version=4 for real-time ingestion.

Traces include agent, provider, model, token counts (including cache and reasoning), HTTP status, timing, and route ID. Attempts with the same route ID share a trace ID. Chat, Responses, Anthropic and Gemini gateway requests also export a parent request span and child spans for every routing attempt, including unbilled failures, retries and fallbacks, so Langfuse can render a waterfall. A valid incoming W3C version-00 traceparent connects these spans to the caller's trace. Attempt spans carry token usage; the parent does not duplicate it. Attempt timings include any wait for a concurrency slot, while routing and retry delays remain visible as gaps inside the parent span. Tool execution inside the caller is outside the gateway's trace.

Model-call spans also send estimated USD input, output and total costs to Langfuse via langfuse.observation.cost_details, using the same effective prices as Magpie's usage ledger (custom prices first, then catalog prices). Input cost includes cache reads and writes; reasoning tokens are already included in output cost. Explicit zero prices are exported too. Unknown prices are omitted so Langfuse can use its own model pricing. Parent and tool spans carry no costs, avoiding duplicate counting. Subscription costs at catalog prices are API-equivalent estimates, not subscription charges.

Enable Trace agent conversations to instead export one trace per user interaction, grouping model calls and tool executions under an agent root. Conversation IDs group those traces into Langfuse sessions. Gateway traces remain available until a visible local store covers that exact agent/session. For Codex and Pi, a readable session header establishes readiness without waiting for a model/tool span. The first request checks a header bounded to 256 KiB when needed (including Codex base instructions); subsequent polls keep idle sessions ready while their files remain among the reader's 200 newest main files. Claude children have a separate 200-file quota and share the same 8 MiB polling read budget. Header checks never upload history or bodies. Discovery is shared across request IDs for two seconds. The request that refreshes discovery waits for the scan; other unresolved concurrent requests retain gateway traces without waiting. Polling reuses headers by path, size and mtime. Other adapters still use recent observations (five minutes). Only loopback requests without a gateway key can be deduplicated. The native client session ID takes precedence over a Magpie routing override; unknown sessions retain gateway traces, including WSL mirrored and Docker Desktop clients whose sessions are not visible locally. Requests without a session ID fall back to recent observations for that agent. Claude Code/Cowork readers also follow <session>/subagents/*.jsonl. Child interactions share their parent session and use separate trace/span IDs, even when a user UUID is copied. Child final responses are combined across content blocks and exported when the transcript is unchanged for two seconds; no turn_duration is required. Their start times remain inferred; a parent tool link is not guessed. Main-thread calls can therefore be deduplicated. Claude tool-less small requests (max_tokens at most 4096), including title/haiku helpers, retain gateway traces because they may not enter a transcript. This conservative rule can retain a short tool-less main request as well. Explicitly classified auxiliary calls also retain gateway traces for other agents. This includes Codex calls with an explicit kind (subagent, review, memgen, title or compact): even if a child has a readable rollout, these can appear twice. Only its unclassified calls use local-session deduplication; a rollout alone does not prove every auxiliary request was recorded. Keyed and non-loopback clients always retain gateway traces. Magpie reads new events from local session stores every two seconds; it does not upload completed history when enabled. Restarting or changing the destination starts an observation window. Span IDs remain stable across repeated records.

Pi and Codex have been tested end to end with real clients. The other adapters are covered by format fixtures but have not been tested end to end.

Supported clients and formats:

Client Local store Timing
Codex JSONL rollouts (token_usage_record, item_completed, response_item) Recorded operations and paired native tools; inferred model/native-tool intervals
Pi Version-3 JSONL; optional timing-final Recorded model times; inferred tool intervals
Oh My Pi Pi-compatible JSONL, including model_usage Pi timing; auxiliary calls have inferred zero duration
Claude Code / Cowork projects/*/*.jsonl and <session>/subagents/*.jsonl, repeated assistant blocks and paired tool results Inferred model/tool starts; recorded transcript boundaries
OpenCode SQLite V1/V2 (message/part or session_message); legacy JSON storage Recorded model and tool timestamps
Gemini CLI ~/.gemini/tmp/*/chats/session-*.json[l], including patches and rewinds Inferred intervals from message/tool event boundaries

The adapters follow upstream schemas: OpenCode V1, OpenCode V2, Gemini CLI, Oh My Pi. Claude transcript records are checked against local Claude Code sessions; its hook documentation describes transcript locations. Separate subagent transcripts are not joined: the parent agent's task/delegation tool is included, but its children's internal work is not yet linked. No hooks or client changes are required.

Polling uses a soft 8 MiB reading budget and up to 200 recent files per store. JSONL files are read incrementally; incomplete lines wait for the next poll. OpenCode SQLite is opened read-only, including WAL changes, and unchanged stores are skipped. SQLite message/part updates are read incrementally in bounded batches on separate read-only connections; oversized conversations resume on the next poll. OpenCode retains up to 256 messages and an 8 MiB body window per session; individual rows above 8 MiB export metadata only. Legacy JSON and Gemini snapshot reads cap at 32 MiB; Gemini retains the current prompt plus 255 recent messages. Completed observations are deduplicated with bounded metadata. Claude's final response and Gemini's final token records may take one extra poll to settle.

Inferred timings carry magpie.timing.source=inferred. Tool arguments/results and user input/output follow Include request and response bodies, masking secrets and respecting the whole-body preference. Session directory/title metadata, credentials and account names are not exported.

Conversation tracing replaces standalone gateway traces for these local clients to avoid duplicate token usage. Unsupported clients and remote forwarded requests retain gateway tracing; the local usage ledger is unchanged. This mode shows agent activity rather than gateway retries or provider-routing details. It reads this computer's local sessions, including calls made without Magpie as the proxy.

Metrics group duration and input/output token histograms by agent, provider, model, operation and error status. Prompt/reply text is exported only when Include request and response bodies is enabled (MAGPIE_OTEL_BODIES=true); secrets are masked and each body is limited to 256 KiB unless Include the whole bodies is enabled. Conversation IDs are exported only with conversation tracing; provider account names/keys are never exported.

Whole bodies increase transient memory and allocation costs during read-back, secret scrubbing and JSON encoding; a 32 MiB request and reply can roughly double total allocations compared with truncated export. The export limits queued bodies to 128 MiB, but this does not bound in-flight processing memory. Recent calls retain only the first 256 KiB of each body.

Export runs in the background with a bounded queue (128 records) and batches of up to 32 records, flushed every five seconds. A full queue drops telemetry without delaying gateway requests. Network errors and HTTP 429/502/503/504 are retried up to two times, with retry delays capped at 60 seconds; other errors and partial rejection are logged without the collector's response body. Each HTTP attempt times out after three seconds. Graceful gateway shutdown allows at most three seconds to drain; magpie serve currently exits on SIGTERM without draining, so its last batch may be lost. This is best-effort export; local usage records remain available if it fails. Requests follow magpie's proxy setting, with loopback collectors going direct. Queued records are discarded if export is disabled or the destination or credentials change before sending. Redirects are not followed. Backups without keys omit OTLP headers; restoring one preserves this machine's headers only when the collector endpoint is unchanged.

Mirrors

The Plugins page's Mirrors in China (国内镜像) switch has what that page downloads asked of a mirror in China first and of its official address after: npm's registry (plugin packages, their versions and search) at registry.npmmirror.com, Bun's releases at npmmirror's binary mirror, and the plugin list at jsDelivr (cdn.jsdelivr.net/gh/magpie-community/plugins). Each is a copy of the very file — Bun's zip is still checked against Bun's SHA-256s, and a package against the integrity npm gave — and what the mirror hasn't copied yet is asked of the official address. npm's weekly download counts stay npmjs's own. bun add is pointed at npmmirror too, unless BUN_CONFIG_REGISTRY or NPM_CONFIG_REGISTRY names a registry already. The switch never sends anything through a proxy such as gh-proxy.

Mirror fallback is off by default. Set MAGPIE_MIRRORS=on to use it. When it is on, the official source is tried first; a mirror is only tried after a network error, HTTP 429, HTTP 5xx, or GitHub's rate-limit 403. The defaults are registry.npmmirror.com for npm and gh-proxy.com for GitHub. MAGPIE_NPM_REGISTRY and MAGPIE_GITHUB_MIRROR replace them.

Only two downloads are checked against a checksum that does not come from the mirror: Magpie's own update assets (their SHA-256 comes from the unmirrored update feed) and Bun's zip. Bun's default version carries its SHA-256s in the code, so it can be downloaded through a mirror even when Bun's official SHASUMS256.txt is unreachable; a newer Bun still needs that official file. Skill tarballs always use the official source, and the plugin-market registry does unless the Mirrors in China switch is on (jsDelivr, above). npm metadata, plugin README/search results and version checks are not checksummed; a mirror can report a different version.

The official source and a mirror share one request budget, so a hanging official source doesn't make the whole wait twice as long.

Requests that carry credentials, such as an Authorization, Cookie, API key, or URL userinfo/query token, never use a mirror; they go to the official source alone.

MAGPIE_MIRRORS=on \
MAGPIE_NPM_REGISTRY=https://registry.example.com \
MAGPIE_GITHUB_MIRROR=https://gh.example magpie serve

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.json is 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.

S
Description
Every agent's model. One place. Codex on DeepSeek, Claude Code on Kimi, from the menu bar.
https://usemagpie.ai
Readme MIT
103 MiB
0 Stars 1 Watchers 0 Forks
Languages
Go 63.1%
JavaScript 31.8%
HTML 3.2%
CSS 1.7%