Files
magpie group rule add <group> use=<model> compact, compact=yes|no; the GUI editor's 压缩上下文时 / compacting toggle; the TUI hint), matches a request that is an agent compacting, told from the agent's own prompt (internal/gateway/compacting.go, read from each one's release): the system prompt of Claude Code 2.1's /compact and auto-compact, OpenCode's compaction agent, Pi, Gemini CLI's state_snapshot and Qwen Code, or the last user message of Claude Code's, Codex's (and magpie's own for a Codex compaction_trigger) and Kimi Code's; a prompt only quoted earlier in a conversation doesn't count. Only in a group with such a rule is a request looked at this way, so nothing else changes. A compaction is decided on its own, never held by the turn's rule, its classifier never asked; it isn't remembered for the turn, and neither the conversation's affinity nor the turn's size is taken from its answer, so the requests after it go on on the model the turn was on and its cache (without that, the next request of a Claude Code turn followed the summary to the cheap model). A member whose context is less than the compaction is passed over for the next rule that matches, or the group's order (RuleHit.Small). The trace tells it ("The agent is compacting the conversation, so rule 1 sends the summary to …", the models passed over named), and the editor notes a compact rule's model taking less than another member. Docs: start#rules (en, zh). Tested: internal/gateway/compacting_test.go — each agent's prompt taken, a plain and a quoted one not; in a group with rules compact→a/small and tokens≥2000→b/big, a long Claude Code turn on b, its /compact to a (Compact, N=1), the turn's next request back on b held, Codex's compaction to a too; a 70k-token compaction skipping a (64k) for rule 2 with Small [a/small]; a group with no compact rule routing a compaction as before; ParseRule/Line/Conditions/Matches for compact. TestRuleCompact failed with the affinity kept from the summary. internal/gui/tests/routing-compact.test.cjs, Chromium and WebKit, en and zh: the compaction in the trace, the rule's words, the toggle on and off with nothing moved, the room note, a compacting-only rule saved as compact:true and not refused. routing-kind/served/signin/steady, click-scroll and old-webkit pass. go vet, go build -tags nogui (and GOOS=linux, GOOS=windows) and go test -tags nogui ./... pass. Not tried against real agents' compactions through the gateway (the prompts are matched as read from their releases) nor in the real Wails window.
copilot: Auto — Copilot's own pick, the only choice a Student plan has — is listed for every Copilot account and works through magpie, as Copilot's clients do it (#256; Discord, CakeJskSUMM: Copilot Student can only use Auto) — Copilot's /models doesn't list Auto; its clients add it themselves (Copilot CLI 1.0.79: {id:"auto",name:"Auto"}, AUTO_MODEL_ID) and ask POST {api}/models/session with {"auto_mode":{"model_hints":["auto"]}} for a session_token, the selected_model and expires_at, each request then naming that model with the token in Copilot-Session-Token. magpie listed no Auto, failed an account whose list left nothing to pick by hand ("Copilot lists no chat model for this account"), and sent "auto" as a model. Now copilot_auto.go keeps the session per GitHub account, asked again two minutes before it expires; the gateway resolves Auto before choosing the API, so a picked model served only on /responses goes there; a body still naming "auto" (the model test) is rewritten; the endpoints of every model Copilot lists, hidden ones too, are kept for the picked one. A reply to "auto" from another model isn't counted as swapped (Cursor's auto too); the Copilot CLI's picker, which offers auto itself, leaves out the listed one. Tested with fakes in a sandbox HOME: provider/copilot_auto_test.go (a Student-like account whose models are all disabled lists [auto], the session body sent as above, requests carry the picked gpt-5-mini and the token, one session reused, renewed near expiry, an object-shaped selected_model, a refusal reported) and gateway/copilot_auto_test.go (a chat request for copilot/auto goes to /responses as gpt-5-mini with the token, two turns one session; fails without the gateway change); served_test gains the auto cases. go vet ./internal/..., go build -tags nogui (and GOOS=linux, GOOS=windows) and go test -tags nogui ./... pass. Not tried with a real Student account: what its /models returns, whether the plain session request suffices (Auto v2's /models/session/intent is not done), and the body is from VS Code's Copilot Chat, not captured traffic.
catalog: fetched again each day magpie stays open, and sooner when a call's model has no price, so a model models.dev lists after magpie started is priced (#224 Koohoko: gpt-6.1-sol shows no cost) — the models.dev catalog, where list prices come from, was fetched only at the app's start (when older than a week), so a magpie left open, or
magpie serve, never learned a new model's price. KeepFresh (internal/catalog/fresh.go), run by the app's backend and by magpie serve, looks every hour and fetches when the cache is older than a day; a call whose model has no price asks for a fetch (catalog.Missing, from usage's pricer) when the cache is older than six hours and none was tried in that time, only in a process keeping it fresh, so CLI commands and tests never fetch on their own. Loading and Reset now share a mutex instead of a sync.Once reassigned under a running Do, which a background Sync could hit while a call was being priced. Tested with fresh_test.go against a fake models.dev in a temp XDG_CACHE_HOME: stale after 25h and not before, Missing fetching nothing when not keeping, fetching once when keeping and gpt-6.1-sol then priced, and not again; passes under -race (failed under -race with the Once). go vet, go build -tags nogui (and GOOS=linux, GOOS=windows) and go test -tags nogui ./... pass.
cli: 新增 magpie webdav,在终端里开启、修改和触发 WebDAV 同步 / add magpie webdav to turn on, change and run WebDAV sync from the terminal (#127)
toml: Codex's config.toml with a multi-line array (notify = [ … ]) is edited right, and an edit that fails leaves every file as it was (#253 kerwinliu00001: "edited TOML is invalid: line 2, column 1: incomplete number", config.toml left with model_provider gone and [model_providers.magpie] added but no model) — the top-level key edits (GetTOMLTop, SetTOMLTop, DelTOMLTop) went line by line with regexes: a new key went after the last line that looked like
key = …, which for notify = [ was inside the array; key = value lines inside a multi-line string were read as keys and a line starting with [ as a table; replacing or deleting a multi-line value touched only its first line. They now locate the top-level keys with the TOML parser, as the table edits already did, each with its value's whole range however many lines it takes (parseTOMLFile, the unnamed root table beside the tables); SetTOMLTop finds all its keys in the file as read and writes once. A file the parser refuses is still read line by line, so a broken config keeps showing its model, and writes to it are refused untouched. And a pick is several writes (Codex: drop model_provider, add magpie's provider table, set openai_base_url and model), each checked, so one failing left the earlier ones on disk: edit.Atomically(fn, paths…) puts each file back to its bytes and mode, or removes one it created, when fn fails, and every field set and Sync of Codex (config.toml and magpie-models.json, so Codex in WSL too), Kimi and Grok goes through it, as do Codex's MCP server writes (putCodex, delCodex). Tested in temp dirs and a temp HOME: the reporter's file read, set and deleted directly and through Codex's model field (signed in, with model_provider and model above the array, with magpie as the provider), the array's text kept and the TOML valid, and back to a Codex model; nested arrays, trailing commas, comments in arrays, [x] and a = b in strings, an inline table in an array; """ and ''' strings holding model = … and [table] lines not read as keys, new keys after their closing quotes, one deleted whole; a forced failure of Codex's last write leaving config.toml byte-identical; TestAtomically. They fail on main (the Codex ones with the reporter's error). go vet ./internal/..., go build -tags nogui (and GOOS=linux, GOOS=windows) and go test -tags nogui ./... pass. Not tried against a real ~/.codex, in WSL or in the GUI; multi-line inline tables (TOML 1.1) are refused by the parser and so left untouched with an error.
usage: costs show in RMB at a live rate, when Settings picks it (#212 L1cardo: 添加费用用人名币结算的选项,最好是能根据实时汇率来计算) — the Usage page, the tray panel, the TUI and the CLI all price a call in USD, magpie's list prices being dollars; Settings now has a Currency row (after Usage shows) offering $ USD, kept by default, and ¥ CNY, which converts every one of those at display time only — usage.Totals.Cost and the JSONL records it comes from stay in USD always, and a vendor's own balance (already in its own currency, a relay's ¥) is never touched.
providers: each account of a subscription holding several can go through its own proxy — the provider's, none, or its own address (Discord, gakki: 是否可以为不同的codex账号设置不用的代理,现在是同一个供应商下使用相同的代理) — the per-provider proxy (#237) was one for all of Codex's signed-in accounts. A subscription's entry in providers.json now keeps accountProxies, by the account's name in lower case, each checked by settings.CheckProxy on save (ftp:// refused), addresses trimmed, accounts following the provider's proxy left out. Provider.ProxyChoice picks an account's own proxy, else the provider's, else the global one, so everything that already went through Provider.Via follows the account: gateway turns and fallback across accounts, usage and quota, token refresh, Codex's warm-up, reset and saved accounts' model lists, Claude's warm-up and the *_PROXY of Claude Code runs (ViaLogin / ProxyOfLogin), and Codex's own sign-in relayed as it came (ViaSignedIn). Extra accounts of Copilot, ZCode, Kiro and the like, made afresh without the provider's settings, now read them from providers.json, so they honour the provider's proxy too. A save without accountProxies keeps them; {} clears them. The provider editor, for a subscription with two or more accounts, lists each under the Proxy row: Provider's proxy | Direct | Custom, the address field's room kept so a pick moves nothing; Custom with no address is refused before anything is posted, its field focused without scrolling. en and zh together; no border stripes. Tested with internal/gateway/account_proxy_test.go in a sandbox HOME against fake proxies and a fake ChatGPT backend: three Codex accounts (own proxy, direct, following the provider) each reach the backend their own way in one turn and for usage; clearing an account's proxy sends it through the provider's, clearing the provider's through the global one, direct staying direct; ftp:// refused; TestClaudeAccountProxyEnv covers each Claude account's HTTPS_PROXY. Both fail with the per-account lookup off. account-proxy.test.cjs, Chromium and WebKit, en and zh (new), provider-proxy, click-scroll and old-webkit pass. go vet ./internal/..., go build -tags nogui (and GOOS=linux, GOOS=windows) and go test -tags nogui ./... pass. Not tried against a real proxy, real accounts or the real Wails window; Grok's per-account usage and Kiro's usage on a key still use the provider's proxy.
redact: a gateway's key in "key": "oc_sk_…" is masked, and Settings → Privacy takes the user's own masking rules (#195) — an OpenCode Go key in providers.json went to the vendor as it was: the API key rules wanted sk- with a hyphen, and a field called key alone was none of the secret field's names. Now a key of a gateway or relay, two to four letters and _sk_ (oc_sk_, or_sk_) with digits in it, is an API_KEY wherever it is, and a field named key and nothing more (not sort_key, monkey or obj.key) is a SECRET when its value is 16 or more letters and digits both, so a map's "key": "value" and key = "someIdentifier" are left alone; \"key\": \"…\" in JSON inside a string is found too. The secret field's names and its markers come from one list, so they can't drift apart, and of two rules finding the same value the first keeps it, so its placeholder no longer changes between an API_KEY and a SECRET from one request to the next. Masking rules, next to 脱敏词: a name for the placeholders (upper case letters, digits and _, 24 at most) and a prefix (then 8 or more key characters) or an RE2 regular expression, group 1 the value if it has one, masked with the secrets while 脱敏密钥 is on and put back as the others are, the user's name winning where one of magpie's finds the same value. They are set on their own: a pattern that doesn't compile, matches nothing at all, or is too long (300), a prefix under 3 or over 64, both at once, or more than 32 rules, is refused with the reason under the add row and what was typed kept. Tested: every row of the issue's table and the false positives, the rules' tidying, errors, stable placeholders, restore, overlaps, and kinds with digits in a stream; the settings file and the gateway's count_tokens with a rule; the page in a stub magpie web (add, a bad regex, remove, zh dark)
usage: a Grok id named at a reasoning effort is priced as its model (#224 Aimer779: calls made with Grok Build show no cost on the Usage page) — the Usage page, the Requests ledger and the Sessions page price a model by its models.dev id, and xAI lists grok-4.7 and grok-4.6 ($2 / $6 / $0.5 cache read per million in the cached catalog) but not the ids Grok's models are spelled at an effort: grok-4.7-low, -medium, -high, -xhigh (Cursor's ids, and grok-4.7 at the levels Grok's own model list gives it). Such an id is the same model at another setting, so when its own id has no price it is now looked up as the model it is an effort of (provider.pricedNames: grok-<version>[-…]-minimal|low|medium|high|xhigh|extra-high, with or without a path, any case), in Provider.ListPrice and MakerPrice (the gateway's ledger and summary) and in the Sessions page's priceOf (through provider.PricedName). An id priced by its own name keeps that price; what is recorded is unchanged. Only Grok's ids are read this way, and -max is not stripped. Left unpriced, as no catalog in use lists them and no price is made up: fast ids (grok-4.7-fast, grok-4.7-low-fast, … which cursor_models.go treats as models of their own), grok-4.7-build and grok-4.7-build-fast (Grok Build's own models, from its model list and sessions) and grok-4.7-mini; they keep the + as before. Tested with a new TestGrokEffortListPrice (MakerPrice and the Grok account's ListPrice price grok-4.7 at each effort, in upper case and with x-ai/, and grok-4.6-minimal, at grok's price; the fast, build, mini and -max ids, gpt-6-astra-high and cursor-grok-4.7-high stay unpriced) and TestPriceOfGrokEffort in internal/sessions (the same for a session's models, grok-4.7-build, grok-4.7-mini and the fast ones unpriced); TestSubscriptionListPrice unchanged. go build -tags nogui (and GOOS=linux, GOOS=windows), go test -tags nogui ./... pass (TestUserPath, TestClaudeSignedOut, TestClaudeAccountNamedAsItsLogin, TestClaudeIdentityServedWhileAsked and TestCodexVersion hit their 5s waits in the full run and pass alone). Not tried against the reporter's usage log or in the GUI.
app: Magpie is in the Start menu on Windows (#158: Windows 便携版首次运行后,无法在开始菜单 / 系统搜索中找到 Magpie — @Aimer779) — the Windows build is one portable exe with no installer, so nothing put it anywhere Windows search looks. Each start of the desktop app (not the CLI, TUI or serve) now makes sure %APPDATA%\Microsoft\Windows\Start Menu\Programs\Magpie.lnk (the Programs known folder) opens the exe it runs as, symlinks resolved, in its folder, with its icon and "Magpie" as its description; one that already does is left be, one for where the exe was before it was moved is pointed here again. It is the shell's own link, made through WScript.Shell with go-ole (already in the build through Wails, now a direct require), in the background beside the magpie:// registration, and a failure is only logged. An update replaces the exe in place, so the shortcut stays good; a magpie left running from magpie.exe.old(-N) (or .new) points it at magpie.exe, never at what was moved aside. HKCU App Paths\magpie.exe is set to the exe (and its folder) so Win+R magpie opens it too. No uninstall entry, nothing copied. MAGPIE_NO_SHORTCUT=1 skips it (tests, sandboxes); other systems do nothing. Tested with the Windows test built here and run on Windows 11: a shortcut made, left as it was on a second start, pointed at the moved exe. Run on Windows 11 as the desktop app: the shortcut made with the exe's folder and icon, pointed anew after the exe moved, left as it was on a start with nothing changed; App Paths set
web: magpie web knows its version and updates itself as the app does — the badge's restart puts the new version in and runs it in the old one's place with the same arguments and key, so the open page reloads onto it (#111); routing: a gateway served by magpie serve, which has no window, is told apart from another magpie's window, and the routing view says to stop it so this magpie serves the gateway and shows routing live (#110)
copilot: Auto — Copilot's own pick, the only choice a Student plan has — is listed for every Copilot account and works through magpie, as Copilot's clients do it (#256; Discord, CakeJskSUMM: Copilot Student can only use Auto) — Copilot's /models doesn't list Auto; its clients add it themselves (Copilot CLI 1.0.79: {id:"auto",name:"Auto"}, AUTO_MODEL_ID) and ask POST {api}/models/session with {"auto_mode":{"model_hints":["auto"]}} for a session_token, the selected_model and expires_at, each request then naming that model with the token in Copilot-Session-Token. magpie listed no Auto, failed an account whose list left nothing to pick by hand ("Copilot lists no chat model for this account"), and sent "auto" as a model. Now copilot_auto.go keeps the session per GitHub account, asked again two minutes before it expires; the gateway resolves Auto before choosing the API, so a picked model served only on /responses goes there; a body still naming "auto" (the model test) is rewritten; the endpoints of every model Copilot lists, hidden ones too, are kept for the picked one. A reply to "auto" from another model isn't counted as swapped (Cursor's auto too); the Copilot CLI's picker, which offers auto itself, leaves out the listed one. Tested with fakes in a sandbox HOME: provider/copilot_auto_test.go (a Student-like account whose models are all disabled lists [auto], the session body sent as above, requests carry the picked gpt-5-mini and the token, one session reused, renewed near expiry, an object-shaped selected_model, a refusal reported) and gateway/copilot_auto_test.go (a chat request for copilot/auto goes to /responses as gpt-5-mini with the token, two turns one session; fails without the gateway change); served_test gains the auto cases. go vet ./internal/..., go build -tags nogui (and GOOS=linux, GOOS=windows) and go test -tags nogui ./... pass. Not tried with a real Student account: what its /models returns, whether the plain session request suffices (Auto v2's /models/session/intent is not done), and the body is from VS Code's Copilot Chat, not captured traffic.