Files
With Codex signed in to ChatGPT through magpie (openai_base_url at /backend-api/codex), Codex reads the backend's model entries from magpie's /backend-api/codex/models, which passed them on as the backend gave them: a window the user set on the codex provider (magpie model context codex/gpt-6.1-sol 600k, or "*" for all) was in /v1/models but Codex still got context_window 272000. The model_catalog_json path did the same: an own model's entry, taken from Codex's models_cache.json, kept the cache's window instead of the one magpie resolved for it. Now each native entry takes the window the user set (Provider.ContextOf, as /v1/models says it) — context_window, and max_context_window raised to it where the window is past it, so the entry never says less than its window — and nothing else in it changes; a model with none set keeps the backend's. The ETag carries the windows set, so setting or taking one away has Codex ask again. A list made from Codex's cache (the backend not answering), which keeps what magpie last handed Codex, takes the account's own list's window back for a model with none set, so a window taken away doesn't stay. The model_catalog_json entry takes the window magpie resolved. base_instructions and the rest of each entry are untouched. TestCodexNativeModelsTakeSetWindows (set windows on, other models as the backend says, max raised, ETag changing on reset, the cache restored) and TestCodexCatalogOwnEntryTakesResolvedWindow fail on the old code and pass. Against the owner's real ChatGPT account in a sandbox gateway under a temp HOME (codex provider only, gpt-6-sol 600000 and gpt-5.5 400000 set): /backend-api/codex/models gave gpt-6-sol 600000/872000, gpt-5.5 400000/400000 (max raised from 272000), the other five 272000/872000 as the backend lists them, all 52 fields and prompts kept; /v1/models agrees for the two set; no token rotated. go vet, builds (darwin, GOOS=linux, GOOS=windows) and go test -tags nogui -count=1 ./... under a temp HOME pass. A Codex session's usable window wasn't checked.
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.
provider: a plugin provider's accounts can be dragged into a new order too (#216) — dragging them failed with "accounts changed; reopen the provider and try again": the account a plugin's provider holds names its agent "plugin", which no list goes by, while the page lists and switches its accounts by the provider's id and logins.json keeps them as plugin:<id>. SetAccountOrder now lists and switches a plugin's accounts by the provider's id and writes their ranks on its plugin:<id> rows, so the order dragged is the order shown and tried. TestAccountOrderPluginAccounts fails without the change. go vet, go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui ./... pass (TestAccountProxy flaked once under load, passes alone).
accounts: Codex or Claude Code can be kept signed in to any of its accounts, not only the first, while the gateway still tries them in their order (Koohoko on #524: 建议不只是支持始终登录"首选"账号,应该可以支持始终登录任一选定的账号。有时候首选只是为了按顺序先消耗它的额度,不一定是用户期望的常用登录账号) — making an account first did two things at once: the gateway tried it first (the account the agent is signed in to always stood ahead of the others, AlsoOn behind it), and Codex or Claude Code was signed in to it (SwitchLogin). "Keep {agent} signed in to the first account" (
817a5f50) only stopped magpie moving the sign-in on, so a user who wanted one account's allowance spent first had to sign the agent in to it too. Now beside that tick is a pick, "Keep {agent} signed in to [the first account ▾ | each saved account]" ({agent} 始终登录 [首选账号 ▾]): picking one keeps Provider.KeepLoginAs (providers.json, kept across an editor Save like KeepLogin; POST provider/keeplogin with keepLoginAs) and signs the agent in to it at once, the order the gateway went by then — the one signed in to first — written as the accounts' own first, so the first stays first. While it is kept: the login switcher signs the agent back in to it whenever it is on another and never moves it off, however used up (NextLogin); a drag or Make first sets the order without a sign-in (arrangeLoginsAs), Make first posting provider/arrange; the gateway puts the account signed in to at its own place in that order, not first (perKeyBarred by Provider.LoginRanks), so In order spends the first's allowance first; InUseLogin (the tray's in-use account) is the first in use in the order. The rows keep their order, the first saying First and the one kept saying Signed in (已登录), an account not in use ticked on by its dot rather than a Use that would sign the agent in; the Routing note says the agent stays signed in to that account and requests go in the accounts' order. Picking "the first account", or unticking, lets it go and signs the agent in to the first again, so the one signed in to is the first once more. TestKeepLoginAsChosen (signed in to the chosen at once, the order kept, stays at 100%, moved off it goes back, a drag changes the order not the sign-in, the first's again when let go) and TestKeptLoginStandsAtItsPlace (through the gateway In order: kept on spare, the first, acct-1, is tried first; acct-2 without the change) are new; keep-login.test.cjs now also picks an account (the post, First and Signed in, Make first arranging, back to the first, every string's Chinese, the page not moving) and passes 8/8 in WebKit; account-return, routing-account-order, account-arrange and click-scroll pass in WebKit. No live agent config, keychain or vendor was touched. go vet ./internal/..., go build (darwin GUI), go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui -count=1 ./... under a temp HOME pass.
provider: OpenCode Zen's Jev routes groups on System One, and a preset's or a subscription's model can be asked on the API picked (Discord, 01huadalang: Zen's "Jev free" was asked as a chat model and failed with no way to switch; OpenCode Go's DeepSeek V4.1 Flash answers on Responses too, and magpie asked it on chat alone, while a custom provider could pick) — Jev isn't an Anthropic model: OpenCode's docs (anomalyco/opencode packages/web/src/content/docs/zen.mdx) list jev-1.13 and jev-1.13-free at https://opencode.ai/zen/v1/systemone alone, and models.dev has no entry for it; asked keyless, Zen answers it there and errors on chat, messages and responses. The opencode-zen preset now has Decide (https://opencode.ai/zen/v1), making Zen a mixed decision provider (#609): Jev is left out of agents' models and is a routing groups' classifier, the free one when Zen is used without a key; a Zen provider saved before gets Decide back in Normalize. Zen's and Go's other models already take their API from models.dev's npm package (catalog.APIOf), which matches the docs. The per-model API custom providers have (settings ModelAPIs, the gateway's Provider.APIs) now reaches presets and subscriptions with more than one API: ModelAPI no longer refuses an account, APIs() is the pick or ListedAPIs (what the vendor's list says, sent to the editor as a model's auto); the editor's model chip has "Asked on: Auto…" on its right-click, opening the app's menu (Auto says what the vendor's list says, then each API the provider has), tagging the chip with the pick, staged and sent with Save as modelPrefs, the page not moved; Names & levels' API row shows for subscriptions too. English and Chinese. TestOpenCodeZenJevDecides and TestPresetAndSubscriptionModelAPI fail on the old code and pass; internal/gui/tests/model-api-preset.test.cjs (WebKit, en/zh) covers the menu, no <select>, no left-border accent, no scroll, the Save's modelPrefs, a subscription's tag and a single-API provider with nothing to pick, and fails on the old assets. A sandbox magpie (keyless Zen) answered POST /v1/systemone with opencode-zen/jev-1.13-free (noul 0.44) and chat on mimo-v2.6-flash-free, Jev not in /v1/models. go vet, builds (darwin, GOOS=linux, GOOS=windows) and go test -tags nogui -count=1 ./... under a temp HOME pass but TestUserPath, a timing test that passes alone; provider-levels-scroll.test.cjs fails the same on origin/main. Not tried with a paid Zen/Go key.
usage: an Antigravity call at a level answered under its model's own name is no swap (#462: gemini-3.8-flash at medium marked Swapped, answered by gemini-3.8-flash) — on an Antigravity account a call at an effort goes out as that level's id (AntigravitySentID: gemini-3.8-flash-medium), and Cloud Code's reply names the family in modelVersion (gemini-3.8-flash), so Swapped compared gemini-3.8-flash-medium with gemini-3.8-flash and the request list and the Routing view tagged the model as swapped for itself. A sent id whose last word is an effort word (provider.EffortFamily: low, medium, high, xhigh, minimal, extra-low, extra-high) now matches a reply naming the id without it; a reply naming another level, or another model, is still a swap. TestLedgerAntigravityReplyNamingTheFamilyIsNoSwap fails without the change. go vet, go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui ./... pass (TestClaudeRunEndsWhenTheClientLeaves, a load flake, passes alone).
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)
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.
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.
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)
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)
usage: a custom balance's card says when it was read, shows each of its amounts on a line of its own and a percent as a meter (#420 by emo172: 余额卡片显示读取时间、百分比进度条) — a balance read from a provider's own Balance URL showed one line, "Balance $2.00 · Today $2.50 · Used 90%", and nothing said how old it was. magpie now hands the card the amounts the user's expression names, each with its label, and a "%" one's share; the Usage page shows each on a line of its own, the user's label quiet and the first amount the larger, "Balance" only where no label was given, and a percent as the quota meter, amber from 90%. Every card that was read says so, "Updated 3 minutes ago" (3分钟前更新), and one standing in for a reading that failed just now says "As of … — couldn't be read just now" (截至 …,暂时无法获取最新用量), allowance cards too. The menu bar panel's Balances show the same amounts and meter and an amber "As of" (截至), and the tray's tooltip adds "as of …, couldn't be read just now". One amount alone reads as it did, and the balance line, the CLI and the expression syntax are unchanged. No balance URL, path or site is added: /api/usage/token and unlimited_quota are one kind of relay's and stay the user's to write. TestBalanceParts, TestTrayUsageText and balance-parts.test.cjs fail without the change. go vet, go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui ./... pass; balance-parts, click-scroll, old-webkit, quota-families, panel-usage, tray-usages, balance-fix, usage-refresh, currency, claude-usage-asked pass.
claude: a Claude Code /logout takes only the account it was signed in to, and that one is never served in its place (StringKe on Discord: Claude-Code -> Magpie -> Claude Subscription 的这个流程现在顺畅吗,昨天 /logout 之后导致六个账号全部都要重新登录) — before
41d50e15 (v0.1.588) a logout left magpie no Claude account at all and told the user to sign in again; since then the account seen last stands in. That one is the account Claude Code was signed in to, and Claude Code's /logout revokes the refresh token it holds (2.1.287's performLogout: POST <TOKEN_URL>/revoke with claudeAiOauth.refreshToken, best effort, before it deletes its keychain item): magpie's copy of it in logins.json, bookmarked every 30s, is that same sign-in, so the stand-in served first was a revoked one and every request paid a failed Claude Code run before moving on. The saved accounts in config directories of their own are sign-ins of their own and outlive the logout. rememberLogins now marks the account Claude Code holds (savedLogin.Held, cleared on the others); when Claude Code is next seen signed out, that one is marked lapsed ("Claude Code's /logout signed it out (it revokes the sign-in it holds); sign in again") and the stand-in is picked among the others — never the held or logged-out one, one with a saved sign-in, on before off, then the one seen last. A switch in magpie isn't taken for a logout (the account switched off is saved as it was), and a /login in Claude Code to the same account clears the lapse. TestClaudeAccountsOutliveLogout (now: the revoked account lapsed and never put in a config directory, the one on standing in, another on behind it) and TestClaudeSignedOut fail without the change; TestClaudeSwitchIsNoLogout guards the switch. No sign-in was made and nothing was asked of Anthropic. go vet ./internal/..., go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui -count=1 ./internal/... (under a temp HOME) pass.
provider: Claude usage reads the reset times Claude Code 2.1.285 on prints, "Oct 9, 2:59pm (UTC)" with a comma where "at" was (#631) — claudeResetTime took only "Oct 9 at 2:59pm", so the weekly windows lost their reset time. It now takes "Jan 2, 3:04pm", "Jan 2, 3pm" and the same with a year; TestParseClaudeUsage covers them (fails without the change). go vet, go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build and go test -tags nogui -count=1 ./... under a temp HOME pass.
provider: Claude usage reads the reset times Claude Code 2.1.285 on prints, "Oct 9, 2:59pm (UTC)" with a comma where "at" was (#631) — claudeResetTime took only "Oct 9 at 2:59pm", so the weekly windows lost their reset time. It now takes "Jan 2, 3:04pm", "Jan 2, 3pm" and the same with a year; TestParseClaudeUsage covers them (fails without the change). go vet, go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build and go test -tags nogui -count=1 ./... under a temp HOME pass.
warmup: a window that hasn't started is started, and started again after a try that didn't take (#178) — the warm-up only acted on a window with a reset time, so a Codex or Claude account whose 5-hour window had never started (no reset time, nothing used) was never warmed. Now a window the usage reads as not started, or one the agent's windows should have but the reading lacks, is due at once when idle; a try that leaves it unstarted is tried again after 15 minutes, doubling to 4 hours; reading the usage and seeing a window not started kicks the warm-up rather than waiting its 10-minute round, and a warm-up forgets the cached usage so the new window shows. Tested with the repro of the issue, the backoff, and the kick
providers: ClinePass lists Cline's real free models and asks for them as Cline's desktop app (lml and ARNO on Discord: the free models didn't match Cline's) —
a3962602 listed the Cline API's OpenRouter-style ":free" ids (qwen/qwen3.8-27b:free…), which are not what Cline offers free: Cline's clients take their picker from GET /api/v1/ai/cline/recommended-models (cline/cline sdk/packages/llms/src/catalog/catalog-cline-recommended.ts), whose free bucket is the desktop's FREE section ("Try with limited usage, separate from ClinePass quota": cline-free/deepseek-v4.1-flash, stealth/space-bunny-alpha, cline-free/mimo-v2.6-flash, cline-free/muse-spark-1.3-contributor) and whose clinePass bucket is the plan's. ClinePass now fetches that feed, asked as Cline's desktop app and with no key, as Cline asks it (pi-freeflow measured that X-CLIENT-TYPE cline-desktop is what the feed keys its free list on); the plan's models are its clinePass ones, the free ones are marked Free (the FREE badge) and priced at nothing, and its recommended paid models are left out, as before. When the feed can't be had the API's list is asked as before, and the free models are the desktop's list of today (from Cline's generated fallback, cline-recommended.generated.ts); the ":free" ids are dropped with FreeTag, as Cline's own clients never offer them as free and their cost on a Cline key was never shown. Requests to the Cline API (the gateway's and the Test button's) carry the desktop's headers — HTTP-Referer https://cline.bot, X-Title Cline, X-IS-MULTIROOT false, X-CLIENT-TYPE cline-desktop, X-CLIENT-VERSION 3.5.54, User-Agent Cline/3.5.54 — since the cline-free/ models answer 403 "only available via Cline product surfaces" without them (pi-freeflow, measured live); the key goes as before. ListPrice prices a model free by its cline-free/ id, by the fetched list or by the fallback at zero. No request was made to Cline and nothing was tried with a real Cline key. TestClineFeed (the feed asked as the desktop with no key; plan, free and no paid models listed; a free model the feed alone names priced at zero), TestClineClientHeaders (the headers on a cline-free request to api.cline.bot and on no other provider's) and the updated TestPlanModels fail without the change. go vet ./internal/..., go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui ./internal/provider/ ./internal/gateway/ pass (TestClaudeSignInOverStaleCredentials failed once under the full run, passes alone and on the rerun; Cline untouched by it).
providers: ClinePass lists Cline's real free models and asks for them as Cline's desktop app (lml and ARNO on Discord: the free models didn't match Cline's) —
a3962602 listed the Cline API's OpenRouter-style ":free" ids (qwen/qwen3.8-27b:free…), which are not what Cline offers free: Cline's clients take their picker from GET /api/v1/ai/cline/recommended-models (cline/cline sdk/packages/llms/src/catalog/catalog-cline-recommended.ts), whose free bucket is the desktop's FREE section ("Try with limited usage, separate from ClinePass quota": cline-free/deepseek-v4.1-flash, stealth/space-bunny-alpha, cline-free/mimo-v2.6-flash, cline-free/muse-spark-1.3-contributor) and whose clinePass bucket is the plan's. ClinePass now fetches that feed, asked as Cline's desktop app and with no key, as Cline asks it (pi-freeflow measured that X-CLIENT-TYPE cline-desktop is what the feed keys its free list on); the plan's models are its clinePass ones, the free ones are marked Free (the FREE badge) and priced at nothing, and its recommended paid models are left out, as before. When the feed can't be had the API's list is asked as before, and the free models are the desktop's list of today (from Cline's generated fallback, cline-recommended.generated.ts); the ":free" ids are dropped with FreeTag, as Cline's own clients never offer them as free and their cost on a Cline key was never shown. Requests to the Cline API (the gateway's and the Test button's) carry the desktop's headers — HTTP-Referer https://cline.bot, X-Title Cline, X-IS-MULTIROOT false, X-CLIENT-TYPE cline-desktop, X-CLIENT-VERSION 3.5.54, User-Agent Cline/3.5.54 — since the cline-free/ models answer 403 "only available via Cline product surfaces" without them (pi-freeflow, measured live); the key goes as before. ListPrice prices a model free by its cline-free/ id, by the fetched list or by the fallback at zero. No request was made to Cline and nothing was tried with a real Cline key. TestClineFeed (the feed asked as the desktop with no key; plan, free and no paid models listed; a free model the feed alone names priced at zero), TestClineClientHeaders (the headers on a cline-free request to api.cline.bot and on no other provider's) and the updated TestPlanModels fail without the change. go vet ./internal/..., go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui ./internal/provider/ ./internal/gateway/ pass (TestClaudeSignInOverStaleCredentials failed once under the full run, passes alone and on the rerun; Cline untouched by it).
usage: a Codex account's credits bought or given besides its plan show on the Usage page (#571: codex的套餐用完之后,可能会购买额外的信用点套餐…希望能做展示) — ChatGPT's /wham/usage answers, besides the 5-hour and weekly windows, the account's credits (credits: has_credits, unlimited, balance), which Codex spends once a window is used up; magpie read only the windows, so an account whose windows were spent showed spent though it still had credits to run on. codexWindows now reads them too, and a balance held (not unlimited, above 0) is the account's Balance, shown under its windows on the Usage page ("What is left on the account besides its windows") and in the panel card, for the account Codex is signed in to and for each saved one. TestCodexCreditsBalance (held, a few, none said, none held, spent, unlimited, unreadable). go vet, go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui ./... under a temp HOME pass.
usage: a Codex account's credits bought or given besides its plan show on the Usage page (#571: codex的套餐用完之后,可能会购买额外的信用点套餐…希望能做展示) — ChatGPT's /wham/usage answers, besides the 5-hour and weekly windows, the account's credits (credits: has_credits, unlimited, balance), which Codex spends once a window is used up; magpie read only the windows, so an account whose windows were spent showed spent though it still had credits to run on. codexWindows now reads them too, and a balance held (not unlimited, above 0) is the account's Balance, shown under its windows on the Usage page ("What is left on the account besides its windows") and in the panel card, for the account Codex is signed in to and for each saved one. TestCodexCreditsBalance (held, a few, none said, none held, spent, unlimited, unreadable). go vet, go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui ./... under a temp HOME pass.
provider: a Codex reset about to run out unused is spent shortly before it does (#624, jyuuroku16: 快到重置卡到期时间前,自动使用重置卡) — Auto-use only spent a reset once the account's week was used up and no other account could answer, so a reset the account never got that far for ran out unused. For the accounts with Auto-use on, the gateway now goes over them every 10 minutes (KeepResetsFromRunningOut): within 3 hours of the soonest reset running out (the credits' expires_at, already read for the card), if any of the account's windows has been used, it spends that reset — UseCodexReset spends the one that runs out first, so a longer-lasting one is kept. Nothing used means nothing to start again, and it is left to run out. An account is read only when there may be something to do: at most every 6 hours, at 3 hours before a reset runs out, every 10 minutes inside those 3 hours, 30 minutes after a failure. It doesn't count against the week's one reset (a reset lost anyway takes none of it) and spends under the same lock. Auto-use's tooltips (en, zh) say so. TestResetSpentBeforeItRunsOut, TestResetExpiryRetriesLater, TestSpendExpiringCodexResets (a fake ChatGPT: off → never read; on → the soon one spent, not the later one; the next pass doesn't read again); dropping the windows-used or the lead check fails them. 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 but for the known flakes (each passes alone).
provider: a Codex reset about to run out unused is spent shortly before it does (#624, jyuuroku16: 快到重置卡到期时间前,自动使用重置卡) — Auto-use only spent a reset once the account's week was used up and no other account could answer, so a reset the account never got that far for ran out unused. For the accounts with Auto-use on, the gateway now goes over them every 10 minutes (KeepResetsFromRunningOut): within 3 hours of the soonest reset running out (the credits' expires_at, already read for the card), if any of the account's windows has been used, it spends that reset — UseCodexReset spends the one that runs out first, so a longer-lasting one is kept. Nothing used means nothing to start again, and it is left to run out. An account is read only when there may be something to do: at most every 6 hours, at 3 hours before a reset runs out, every 10 minutes inside those 3 hours, 30 minutes after a failure. It doesn't count against the week's one reset (a reset lost anyway takes none of it) and spends under the same lock. Auto-use's tooltips (en, zh) say so. TestResetSpentBeforeItRunsOut, TestResetExpiryRetriesLater, TestSpendExpiringCodexResets (a fake ChatGPT: off → never read; on → the soon one spent, not the later one; the next pass doesn't read again); dropping the windows-used or the lead check fails them. 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 but for the known flakes (each passes alone).
accounts: the account made first stays first — magpie signs Codex or Claude Code back in to it once it has room again (#408: 智能路由会自动把所使用账号设为首选) — since #208/#209 magpie signs the agent in to the next ticked account once the one it is on is 98% used, and left it there for good, so the account the user made first lost its place after its first spent window. magpie now keeps the account it moved off (login-returns.json beside logins.json) and signs the agent back in to it once that is below 90% (Smart's low share, so it doesn't flap) and still ticked; a switch the user makes meanwhile, in magpie or the agent, ends that. The Accounts list says it: the stand-in reads "First for now" (暂为首选), its title naming the one it goes back to, and that one "First again once it has room" (额度恢复后切回首选); the Routing note says magpie moves back. TestCodexSwitchedBackWhenItHasRoom and account-return.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-return, click-scroll, old-webkit, routing-signin pass.
routing: In order keeps sending to the first account until it is used up or refused, and a tick keeps Codex or Claude Code signed in to the first account (#530: an account In order at 98% of its week, its five hours untouched, stopped getting requests; #524: "不允许切换首选账号的登录状态", the agent's own sign-in must stay on the account made first while the gateway still spreads requests) — since #209 magpie signs Codex and Claude Code in to the next ticked account with room once the one they are on reaches 98% (provider.SpentShare, the share Smart counts an account spent at), and since #408 back once the first is under 90%. The account the agent is signed in to is the one every routing tries first (the others stand behind it, AlsoOn), so In order the 98% switch put the second account ahead of the first, which is what #530 saw as the first being dropped; and the gateway counted an account at 98% as full whatever the routing, so a passing failure on it (a 502) rested it until its week renewed (39 hours there), not for the usual backoff. Now SpentShareOf gives 100 In order and 98 otherwise: the login switch moves the agent off an account In order only once a window of it is used up, the gateway's full (the rest after a failure, the quota rest's window) and affinity's "spent" go by the same share, and a vendor's 429 or usage-limit refusal still rests the account until it says it's back, as Smart does; a window used up is out, as before. Smart, In turn and Least used are unchanged. For #524, the subscription keeps KeepLogin (providers.json, beside Routing, kept across an editor Save; POST provider/keeplogin, only for Codex and Claude Code): with it on, magpie never moves the agent off the account it is signed in to, however used up, while the gateway still routes requests through magpie to the other ticked accounts; a pending return to the first (#408) still happens, that being the first. The tick sits at the foot of the Accounts list, under the rows where Make first is, shown only for Codex and Claude Code with more than one account on: "Keep {agent} signed in to the first account" / "{agent} 始终登录首选账号", its tooltip saying requests through magpie still go to the other ticked accounts. The Routing note follows it: kept, it says the agent stays signed in to the first whatever it has left; In order, that magpie moves it on once it is used up; otherwise the 98% wording as before. TestCodexLoginOrderedAndKept (In order stays at 98% and moves at 100%, back once renewed; Smart moves at 98%; kept, it stays at 100% and the setting survives a SetRouting; let go, it moves) and TestOrderedKeepsAnAccountAt98 (In order an account at 98% isn't full and a 502 rests it by backoff, Smart rests it to its week, a refusal rests it either way) fail without the change. keep-login.test.cjs (en and zh, Chromium and WebKit: the tick, its tooltip and Chinese strings, the post, the note when kept and In order, no tick with one account, the page doesn't move) fails without the UI and passes with it (12/12, Playwright 1.61); account-return, routing-account-order and account-arrange pass; click-scroll fails in Chromium here on origin/main too and passes in WebKit. No request was made to any vendor and no live agent config was touched. go vet ./internal/..., go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui ./... pass (TestClaudeRunEndsWhenTheClientLeaves, TestClaudeAccountNamedAsItsLogin and TestClaudeIdentityServedWhileAsked failed once under the full run and pass alone). Turned on after magpie has moved the agent off the first, it goes back to it at once rather than once that has room again.
accounts: Codex or Claude Code can be kept signed in to any of its accounts, not only the first, while the gateway still tries them in their order (Koohoko on #524: 建议不只是支持始终登录"首选"账号,应该可以支持始终登录任一选定的账号。有时候首选只是为了按顺序先消耗它的额度,不一定是用户期望的常用登录账号) — making an account first did two things at once: the gateway tried it first (the account the agent is signed in to always stood ahead of the others, AlsoOn behind it), and Codex or Claude Code was signed in to it (SwitchLogin). "Keep {agent} signed in to the first account" (
817a5f50) only stopped magpie moving the sign-in on, so a user who wanted one account's allowance spent first had to sign the agent in to it too. Now beside that tick is a pick, "Keep {agent} signed in to [the first account ▾ | each saved account]" ({agent} 始终登录 [首选账号 ▾]): picking one keeps Provider.KeepLoginAs (providers.json, kept across an editor Save like KeepLogin; POST provider/keeplogin with keepLoginAs) and signs the agent in to it at once, the order the gateway went by then — the one signed in to first — written as the accounts' own first, so the first stays first. While it is kept: the login switcher signs the agent back in to it whenever it is on another and never moves it off, however used up (NextLogin); a drag or Make first sets the order without a sign-in (arrangeLoginsAs), Make first posting provider/arrange; the gateway puts the account signed in to at its own place in that order, not first (perKeyBarred by Provider.LoginRanks), so In order spends the first's allowance first; InUseLogin (the tray's in-use account) is the first in use in the order. The rows keep their order, the first saying First and the one kept saying Signed in (已登录), an account not in use ticked on by its dot rather than a Use that would sign the agent in; the Routing note says the agent stays signed in to that account and requests go in the accounts' order. Picking "the first account", or unticking, lets it go and signs the agent in to the first again, so the one signed in to is the first once more. TestKeepLoginAsChosen (signed in to the chosen at once, the order kept, stays at 100%, moved off it goes back, a drag changes the order not the sign-in, the first's again when let go) and TestKeptLoginStandsAtItsPlace (through the gateway In order: kept on spare, the first, acct-1, is tried first; acct-2 without the change) are new; keep-login.test.cjs now also picks an account (the post, First and Signed in, Make first arranging, back to the first, every string's Chinese, the page not moving) and passes 8/8 in WebKit; account-return, routing-account-order, account-arrange and click-scroll pass in WebKit. No live agent config, keychain or vendor was touched. go vet ./internal/..., go build (darwin GUI), go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui -count=1 ./... under a temp HOME pass.
codex: when the ChatGPT account runs out of its allowance after a magpie model was picked, magpie becomes Codex's provider, and goes back beside the sign-in when the allowance is back (#540: once the Codex 5-hour/weekly quota is used up, a third-party model through magpie sends nothing in new or old sessions; Dazzle-sys: the session was on the magpie model all along, the quota went elsewhere) — the Codex desktop app refuses to send for a ChatGPT account that is out of its allowance before any request is made, whichever model the turn is on, so a magpie model beside the sign-in (openai_base_url, ChatGPT mode) is blocked with it; Codex's open-source core and TUI only report a usage limit after a response, the block is the app's, keyed on the account.
bd5bf422 made magpie the provider (model_provider = "magpie", model_catalog_json; the app-server's getAuthStatus then answers requires_openai_auth false, the app's API state) only when a model was picked while the account was already out, so an allowance running out later left Codex as it was. KeepOnAnAccountWithRoom now also looks, every 5 minutes, whether the active Codex account is out (CodexUsedUp, now read through LoginUsage's cache, fetched at most once a minute, as the agents' sync asks it on every catalog sync) and tells the agents' sync when that changes (noteCodexUsedUp → catalog.Touched); Codex's Sync then sets the magpie model again, which wires it as the provider while the account is out, marked codex.out in the stash, and puts it back beside the sign-in once the allowance is back, only where it was moved for that (a provider asked for with sign-in "api" stays). Threads started on Codex's own provider and reopened may still be held by the app; the Codex app may need a restart to take the new config. agent's TestMain stubs codexUsedUp so no test asks OpenAI. TestCodexRunsOutAfterPick (ChatGPT mode, then out: provider; out again: stays; back: beside the sign-in; api stays) fails without the change; TestNoteCodexUsedUp checks only a change is told. No request was made to OpenAI. 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.
provider: a Codex warm-up asks the account's cheapest model (#604) — warmModel took a model with mini in its id or else the first listed, and a GPT-6 account lists no mini and starts with the flagship gpt-6.1-sol. warmPick takes the cheapest by list price (the model's own, else models.dev's openai price), and with no price known a mini, then a luna (GPT-6's cheap one), then the first. TestWarmPick.
provider: a Codex warm-up asks the account's cheapest model (#604) — warmModel took a model with mini in its id or else the first listed, and a GPT-6 account lists no mini and starts with the flagship gpt-6.1-sol. warmPick takes the cheapest by list price (the model's own, else models.dev's openai price), and with no price known a mini, then a luna (GPT-6's cheap one), then the first. TestWarmPick.
copilot: Auto asks its session with the API version VS Code sends, and an account Copilot has no Auto for is told so (#256, CakewthinSUm: "Copilot Auto: 404 Not Found") — signed in from magpie (the editors' sign-in), the POST {api}/models/session went out with no X-GitHub-Api-Version, which VS Code's Copilot Chat puts on every CAPI request (@vscode/copilot-api 0.2.19 _mixinHeaders: 2025-10-01, AutoModels among the types); it now carries 2025-10-01 (the Copilot CLI's sign-in keeps its own 2026-07-01). VS Code's session answer names no selected_model, only available_models, and it takes the first it knows: magpie does the same when none is named. A 404 however asked falls back to the first model the account may pick by hand, sent without a session, for 10 minutes; with none, the error says Copilot doesn't offer Auto to this account. TestCopilotAutoSessionAsVSCode and TestCopilotAutoUnavailable fail without the change. go vet, go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui ./... pass.
copilot: a model the plan refuses is remembered and Auto falls back (#371, #256) — Copilot answers a model the account's plan doesn't carry (Student, Free) with 400 model_not_supported; magpie kept offering and picking it. Such a model is now marked refused for that token for 6h, left out of the models listed and of Auto's picks, and the request is retried on the next model (fallback, then default, then non-premium such as gpt-4.1). TestCopilotRefused* fail without the change.
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.
copilot: a model the plan refuses is remembered and Auto falls back (#371, #256) — Copilot answers a model the account's plan doesn't carry (Student, Free) with 400 model_not_supported; magpie kept offering and picking it. Such a model is now marked refused for that token for 6h, left out of the models listed and of Auto's picks, and the request is retried on the next model (fallback, then default, then non-premium such as gpt-4.1). TestCopilotRefused* fail without the change.
copilot: a model the plan refuses is remembered and Auto falls back (#371, #256) — Copilot answers a model the account's plan doesn't carry (Student, Free) with 400 model_not_supported; magpie kept offering and picking it. Such a model is now marked refused for that token for 6h, left out of the models listed and of Auto's picks, and the request is retried on the next model (fallback, then default, then non-premium such as gpt-4.1). TestCopilotRefused* fail without the change.
usage: Hide accounts hides every account, not only an email address, and a WorkBuddy account's card says how many credits it used each day (#568: 默认情况下启动邮箱打码时,workbuddy的用户名那里不会被打码;用量统计能不能统计一下每天用了workbuddy多少积分) — the mask matched email addresses only, so a WorkBuddy account named by its nickname or phone number (and a Qoder, Kiro, Devin, Cursor, CodeBuddy, ZCode or plugin account named so) stayed in sight on a screenshot taken with 邮箱已打码. The page now learns the accounts' names from what magpie answers it (an allowance's user, a provider's account and its logins, a route's account: noteAccounts over every API answer) and hides them as it hides an address, in a row, a sentence and a tooltip, on Usage and Routing; a card's own name (WorkBuddy, ZCode signed in with no name of its own), a provider's or an agent's is never taken for an account, nor is a name inside a longer word. The tray panel hides them too, following the window's setting as it is turned (a storage event); the button is now Hide accounts (账号打码). WorkBuddy's meter tells only what the cycle has used against what it grants, the built-in's card and its plugin's alike, so magpie keeps each reading beside the one before in credits-daily.json (by provider and account: the two figures and the days' sums, nothing else) and counts what was used in between on the local day of the later reading; a reading that has used less (a cycle started again, a pack gone) counts nothing and counting goes on from it, credits granted count nothing, an error or a reading kept from before isn't one. The card, under its Credits meter, says the period's credits (今天 n / 7 天共 n) and over more than a day draws a bar a day, each day's figure in its tooltip, the days before magpie began counting an empty dashed slot saying so, and "Counted since <day>" while that falls in the period. TestDailyCredits (days, a reset, a check-in's grant, a gap, both builds, error and kept readings, another vendor, the file's size, a year on) is new; account-mask.test.cjs and credit-days.test.cjs (WebKit, en and zh) fail without the change and pass with it; mask-emails, click-scroll and balance-parts 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, TestProvidersAnswerWhileListsComeIn, TestClaudeSignedOut and TestClaudeAccountNamedAsItsLogin timed out once under load and pass alone).
usage: Hide accounts hides every account, not only an email address, and a WorkBuddy account's card says how many credits it used each day (#568: 默认情况下启动邮箱打码时,workbuddy的用户名那里不会被打码;用量统计能不能统计一下每天用了workbuddy多少积分) — the mask matched email addresses only, so a WorkBuddy account named by its nickname or phone number (and a Qoder, Kiro, Devin, Cursor, CodeBuddy, ZCode or plugin account named so) stayed in sight on a screenshot taken with 邮箱已打码. The page now learns the accounts' names from what magpie answers it (an allowance's user, a provider's account and its logins, a route's account: noteAccounts over every API answer) and hides them as it hides an address, in a row, a sentence and a tooltip, on Usage and Routing; a card's own name (WorkBuddy, ZCode signed in with no name of its own), a provider's or an agent's is never taken for an account, nor is a name inside a longer word. The tray panel hides them too, following the window's setting as it is turned (a storage event); the button is now Hide accounts (账号打码). WorkBuddy's meter tells only what the cycle has used against what it grants, the built-in's card and its plugin's alike, so magpie keeps each reading beside the one before in credits-daily.json (by provider and account: the two figures and the days' sums, nothing else) and counts what was used in between on the local day of the later reading; a reading that has used less (a cycle started again, a pack gone) counts nothing and counting goes on from it, credits granted count nothing, an error or a reading kept from before isn't one. The card, under its Credits meter, says the period's credits (今天 n / 7 天共 n) and over more than a day draws a bar a day, each day's figure in its tooltip, the days before magpie began counting an empty dashed slot saying so, and "Counted since <day>" while that falls in the period. TestDailyCredits (days, a reset, a check-in's grant, a gap, both builds, error and kept readings, another vendor, the file's size, a year on) is new; account-mask.test.cjs and credit-days.test.cjs (WebKit, en and zh) fail without the change and pass with it; mask-emails, click-scroll and balance-parts 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, TestProvidersAnswerWhileListsComeIn, TestClaudeSignedOut and TestClaudeAccountNamedAsItsLogin timed out once under load and pass alone).
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.
provider: OpenCode Zen's Jev routes groups on System One, and a preset's or a subscription's model can be asked on the API picked (Discord, 01huadalang: Zen's "Jev free" was asked as a chat model and failed with no way to switch; OpenCode Go's DeepSeek V4.1 Flash answers on Responses too, and magpie asked it on chat alone, while a custom provider could pick) — Jev isn't an Anthropic model: OpenCode's docs (anomalyco/opencode packages/web/src/content/docs/zen.mdx) list jev-1.13 and jev-1.13-free at https://opencode.ai/zen/v1/systemone alone, and models.dev has no entry for it; asked keyless, Zen answers it there and errors on chat, messages and responses. The opencode-zen preset now has Decide (https://opencode.ai/zen/v1), making Zen a mixed decision provider (#609): Jev is left out of agents' models and is a routing groups' classifier, the free one when Zen is used without a key; a Zen provider saved before gets Decide back in Normalize. Zen's and Go's other models already take their API from models.dev's npm package (catalog.APIOf), which matches the docs. The per-model API custom providers have (settings ModelAPIs, the gateway's Provider.APIs) now reaches presets and subscriptions with more than one API: ModelAPI no longer refuses an account, APIs() is the pick or ListedAPIs (what the vendor's list says, sent to the editor as a model's auto); the editor's model chip has "Asked on: Auto…" on its right-click, opening the app's menu (Auto says what the vendor's list says, then each API the provider has), tagging the chip with the pick, staged and sent with Save as modelPrefs, the page not moved; Names & levels' API row shows for subscriptions too. English and Chinese. TestOpenCodeZenJevDecides and TestPresetAndSubscriptionModelAPI fail on the old code and pass; internal/gui/tests/model-api-preset.test.cjs (WebKit, en/zh) covers the menu, no <select>, no left-border accent, no scroll, the Save's modelPrefs, a subscription's tag and a single-API provider with nothing to pick, and fails on the old assets. A sandbox magpie (keyless Zen) answered POST /v1/systemone with opencode-zen/jev-1.13-free (noul 0.44) and chat on mimo-v2.6-flash-free, Jev not in /v1/models. go vet, builds (darwin, GOOS=linux, GOOS=windows) and go test -tags nogui -count=1 ./... under a temp HOME pass but TestUserPath, a timing test that passes alone; provider-levels-scroll.test.cjs fails the same on origin/main. Not tried with a paid Zen/Go key.
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.
devin: whether a Devin model takes images is what Devin tells its CLI, for models models.dev doesn't know (#417, HwFee: swe-2 and gpt-6-1-sol were said to take no images) —
devin models list says nothing of images and models.dev has no entry for Devin's own ids, so swe-2, gpt-6-1-sol and the like were served as text-only and agents dropped the images. When it reads the list, magpie now also asks Devin's GetCliModelConfigs, the read-only call the CLI's own model picker makes, with the same metadata magpie's chats already send. Each ClientModelConfig's supports_images is put on its variant, and on the family and its tiers when all their variants agree: swe-2, gpt-6-1-sol, claude-sonnet-5-5 and kimi-k3 take images, GLM and DeepSeek don't. A model Devin doesn't name, or a failed call, is left to models.dev as before; a list saved earlier takes what Devin said, and a user's own image setting still wins. No prompt, header, user agent or existing request changes. TestDevinImagesFromModelConfigs fails without the change. go vet, go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui ./... pass (TestUserPath and the Claude account tests flaked and pass alone).
providers: DimAgent's subscription is taken out of magpie (a DimAgent team member on X: dim doesn't allow its subscription used through a proxy, and users doing so get banned or lose their discount) — #226 signed in to DimAgent as its desktop client does and served its models through the gateway. The sign-in, the account provider and its dimagent/<model> models, the quota card,
magpie accounts … dimagent and the icon are gone, with internal/dimagent. An account signed in before is left out when logins.json is read and dropped from the file at its next write, so its tokens aren't kept. The pasted-callback field and route stay, generic, with no sign-in using them. Tested with TestDimAgentLoginDropped (fails without the filter); in a sandbox HOME with a DimAgent login left in logins.json, magpie accounts --json lists nothing and magpie accounts add dimagent is refused. signin-callback (fixture renamed), old-webkit and click-scroll pass; go vet, go build (GUI, -tags nogui, GOOS=linux, GOOS=windows) and go test -tags nogui ./... pass (TestUserPath and the Claude/Devin sign-in tests flaked under the full run and pass alone).
factory: the Gateway page says when an older magpie serves the gateway, and a Factory account can be added by its API key (#506, 黄瑞 and aohun: Factory 403 "Factory refused this account the request; check that
droid… sign in to it again" on v0.1.630; aohun: support FACTORY_API_KEY as droid2api does) — the 403's wording quoted in #506 was removed in 5523eb17 (first in v0.1.551; git grep finds it in v0.1.550, not in v0.1.630), so those requests were served by a magpie at v0.1.550 or older still holding :3425 (a magpie serve, a second copy, one that didn't quit): serveGateway leaves the port to any magpie answering GET / and the page said only "running · served by another magpie", so none of the later Factory fixes (droid's opening line on /api/llm/o in v0.1.630 among them) reached their requests. gateway.ServedBy reads the serving magpie's version from its GET / answer (Serving wraps it); the providers state carries it with older when it is below this one's, the Gateway card then says "running · served by magpie <v>, older than this one" with an amber dot and that agents' requests go through it without this version's fixes until it quits, when this one takes over within 15 s (zh too, wrapped, never cut off), and the log says so. A Factory API key (fk-…) is added from the import box (Factory's accounts list "Add an account by API key…", and "Use an API key instead…" on the warning before a sign-in): as droid's om() takes FACTORY_API_KEY, the key is the bearer with droid's headers, never renewed, and sends no X-Factory-Org-Id (droid's active org comes from a sign-in, st() is null for a key); whoami asked with the key says whose it is and its region/host, as droid's yP checks a key ("invalid-api-key" when refused), and a refused key isn't added. factoryFresh hands a key account on as it is (no WorkOS, no whoami for an org) and factoryMendOrg leaves it alone; when Factory has moved to its plugin the key isn't offered, the built-in being what keeps it. No request was made to Factory, no real key or account was tried, and whether Factory serves a key's requests from agents other than droid is not known. TestGatewayServedByAnOlderMagpie (fake magpie answering 0.1.550/0.1.630/0.1.640/no version), TestFactoryAPIKey (fake Factory: import, bearer and headers, no org, no WorkOS, refused key, "exists" again; fails with a 403 without the key handling in factoryFresh) and gateway-older.test.cjs, factory-key.test.cjs (Chromium and WebKit, en and zh) pass; login-import, factory-sub, plugin-moved-signin, gateway-fold pass in WebKit (Chromium's channel launch lacks ICU here). 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.
providers: a custom provider's icon can come from its website (#12) — From the website, beside Choose a picture, finds the site behind the base URL (its host, then the domains it sits under: api.deepseek.com, then deepseek.com) and keeps the icon its home page names (an SVG first, then the apple-touch-icon, data: URIs too) or its /favicon.ico, like a picture picked by hand; only public https hosts are reached. Icons are fetched as magpie, since deepseek.com turns Go's own User-Agent away
providers: an account signed in while magpie runs is listed with its vendor's models (#204 uclort: 首次添加 WorkBuddy 模型显示不全,点击刷新后模型全部显示) — a signed-in account's own list was fetched only at start-up, for the accounts there then (and once: a fetch that failed was not tried again). WorkBuddy AI signed in afterwards, in magpie or in WorkBuddy itself, showed magpie's built-in list ("magpie's list · Refresh asks the vendor", 14 models) until Refresh fetched /v3/config's 20-odd. Now provider.FetchNew, which start-up uses in place of its loop, also runs before the providers are listed (providersState, 8s each): each ready account with no fetched list is asked for it, so the first listing after a sign-in is the vendor's. One that fails is asked again after 10 minutes, not on each listing; one fetched is not asked again. Tested with a new TestFetchNewAccount (a fake WorkBuddy /v3/config: nothing asked with no account; a WorkBuddy auth file written while running lists the built-in models, then the vendor's three after FetchNew; not asked again; a 502 asked once over two listings, then again after the retry time). go build -tags nogui (and GOOS=linux, GOOS=windows), go test -tags nogui ./... pass (TestUserPath and three Claude sign-in tests timed out once under load, pass on their own and on a rerun of the package). Not tried in the GUI with a real WorkBuddy account
routing: a group in a group is routed by its own routing whatever the outer group's, and an account with five hours and no week is ranked by its five hours (#576: 只有五小时额度的账号在智能/重置前用完下是否合理;嵌套路由组时上层路由是否会干预下层) — a group routed other than In order (Smart, In turn, Least used, Weekly pace, Manual) put every account and key of every group in it into one pool and weighed them together, so A (Smart) holding A-1 and A-2 (each In order) ranked all five of their accounts and keys by Smart and A-1's and A-2's own In order did nothing; only an outer In order let each inner group route itself. planLevel now plans each group in the group by its own routing, whatever the outer's, and weighs it as one beside the outer group's own models' keys, by the first of its own order that isn't resting; where that lands, the inner group is tried whole in its own order, the trace in the same order. As for the five hours: Smart already ranked such an account (Claude Enterprise) by its five hours, the biggest window it has, so ahead of every week renewing later, but a five hours not started (no reset told) counted as a reset not known and went after every week; Allowance.Renewal now has a window not started that says its span renew that long from now, which Smart sorts by (the trace's renews unchanged). Weekly pace took an account with no week as a fresh week with the share used (100/168 per hour), so a week with 80% left and five days to go went ahead of an Enterprise account whose five hours were being lost; it now goes by what its five hours have left per hour until they renew, the whole span when not started, except where the reading may leave a week out — a Claude account only heard of from Claude Code's rate_limit_event, never read whole by /usage (QuotaWindow.partial, claudeUsageEntry.whole) — which keeps the fresh week. The group editor's Smart and Weekly pace hints and the trace's say how an account with five hours and no week is ranked, a hint under Routing says a group among the models keeps its own routing, and the docs (en, zh) say both. TestGroupInGroupKeepsItsOwnRouting (Outer rotating over two In order groups: b c d a without the change, c d a b with it), TestSmartFiveHoursAlone (a five hours not started went after a week in two days), TestFiveHoursAlone, TestClaudeHeardOnlyIsPartial; TestWeeklyPace and TestAllowancePace now pin the five hours alone. settings-groups, group-member-fast, group-found-off, routing-idle-frame 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, TestProvidersAnswerWhileListsComeIn, TestUserPath, TestClaudeSignedOut flaked; the last passes 3/3 alone).
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)
subscription: Antigravity accounts can be imported from a JSON file (莫: https://github.com/jlcodes99/vscode-antigravity-cockpit 这个开源的里面有 你可以看看) — beside Add account (and as "Import instead…" on the risk note) the Antigravity card takes files or pasted text exported by Antigravity Cockpit (a JSON array of {email, refresh_token}, or refresh tokens one a line), Antigravity Manager (the same array, {accounts:[…]}, or an account file with token{refresh_token, email, project_id}) and CLIProxyAPI (an antigravity auth file, type/email/refresh_token/project_id; another type's is refused). All three sign in with the Antigravity app's own OAuth client, as magpie does, so their refresh tokens are good here. Each account is checked as signing in does: the token refreshed at Google, the email read from userinfo (the file's if that's refused), the project and plan from loadCodeAssist, then kept in logins.json like a signed-in account. A token magpie already has is not sent again (exists); a known email with a new token replaces the old (updated); duplicates in the file, entries with no token and tokens Google refuses are reported per account, Google's bare "Bad Request" explained. POST /api/signin/import; tokens are never logged, shown or returned, and the paste box is masked. Tested against an httptest token/userinfo/Code Assist server in a sandbox HOME
subscription: Antigravity accounts can be imported from a JSON file (莫: https://github.com/jlcodes99/vscode-antigravity-cockpit 这个开源的里面有 你可以看看) — beside Add account (and as "Import instead…" on the risk note) the Antigravity card takes files or pasted text exported by Antigravity Cockpit (a JSON array of {email, refresh_token}, or refresh tokens one a line), Antigravity Manager (the same array, {accounts:[…]}, or an account file with token{refresh_token, email, project_id}) and CLIProxyAPI (an antigravity auth file, type/email/refresh_token/project_id; another type's is refused). All three sign in with the Antigravity app's own OAuth client, as magpie does, so their refresh tokens are good here. Each account is checked as signing in does: the token refreshed at Google, the email read from userinfo (the file's if that's refused), the project and plan from loadCodeAssist, then kept in logins.json like a signed-in account. A token magpie already has is not sent again (exists); a known email with a new token replaces the old (updated); duplicates in the file, entries with no token and tokens Google refuses are reported per account, Google's bare "Bad Request" explained. POST /api/signin/import; tokens are never logged, shown or returned, and the paste box is masked. Tested against an httptest token/userinfo/Code Assist server in a sandbox HOME
gateway: a tool whose input_schema has anyOf/oneOf/allOf at its root reaches Anthropic-protocol vendors as an object, and moved Factory users get the plugin that keeps its constraints (#646, tedestar; magpie-community/plugins#12, SadWood)
providers: a model a vendor names its own way is merged with the same model from other providers — by its id as Volcengine Ark spells it, or by the model the user says it is the same as in Names & levels (kyzhouxu, #583: 火山引擎获取到的默认 ds 4.1 flash 模型的名称跟其他供应商的名称不一样,没有自动合并到同一个模型分组) — the groups magpie finds merged a model by its id lowercased, its vendor prefix and an eight-digit date taken off and a version's dot made a dash, so Ark's deepseek-v4-1-flash-260910 (a six-digit YYMMDD snapshot) was a group of its own beside ali-tp's deepseek-v4.1-flash. sameModel now also takes off Ark's YYMMDD (a year from 23 to 39, a month and a day that are one), Vertex's "@YYYYMMDD", reads Fireworks' "p" between digits (deepseek-v4p1) as the dot and "_" as "-"; -flash, -thinking, ":batch", a four-digit release (qwen's -2507, kimi-k2-0905) and a number that is no date (-128000, -261301) stay apart. For an id no rule matches (an Ark endpoint id, a relay's own name), each model in a provider's Names & levels now has a "Same as" (等同于) box, its placeholder the name it is merged by now: the model it is the same as, as any vendor spells it, staged in draft.modelPrefs and made with the provider's Save like the other prefs (Cancel drops it, Restore default empties it, nothing sent or redrawn or scrolled before the Save). It is kept in settings' ModelSameAs by "<provider>/<model>" (renamed with the provider like every Model* map), set by provider.SetModelSame (its own id however spelt keeps nothing; a model the provider doesn't serve, or a name of no model, is refused) and taken by autoGroups, AutoStandIn and the agents' reseat (AutoGroupOf); the agents are told once. TestSameModelVendorSpellings (Ark, Fireworks, Vertex, underscores, and pairs that must stay apart) and TestModelSameAsMergesFoundGroup (c/ep-sol-x joins auto-sol once said, GroupFor("sol") finds it, back out when cleared, refusals) are new; TestProviderSaveModelPrefs covers a Save's same; provider-same-as.test.cjs (WebKit, en and zh: placeholder, staging, unsaved, Restore default, Cancel, one Save carrying exactly the changes, no scroll, Chinese for each string) fails without the change and passes with it; provider-levels-save and provider-detect pass. 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.
models: reasoning levels are borrowed again for models Azure also hosts, and a routing group keeps its members' levels when one thinks with none to pick from (ARNO on Discord: routing group在接入harness的时候,没有设置reasoning_efforts。我发现是之前很多能自动推导reasoning_efforts列表的模型现在都推导失败了) — the Azure OpenAI preset (
c35471cf, Catalog "azure, openai") was taken for a maker, so models.dev's azure list was the maker's word on every model it hosts: Kimi K2.5, DeepSeek V3.2 and R1, Grok 4.20/4.1 Fast Reasoning, listed there with no levels, took none on a relay or custom provider (and the gateway capped them at high as levelless), where the providers listing any give low…high, none/high and the rest. Azure is now marked Hosts, as Groq and Ollama Cloud are; its own provider still takes its own list's word. And groupEntries narrowed a group to the levels every member has, so one member that thinks with no levels (a thinking switch, a Claude 4.5 with a budget alone, OpenRouter's qwen3.8-omni-flash) left the group none and dsh's route wrote it without reasoningEfforts; such a member is now skipped as an unknown one is, since the gateway sends it the effort asked, no more than high (fitFor). The same Entry.Efforts feeds dsh, Codex's catalog, Pi and OpenCode. On a copy of the owner's config and models.dev cache: no plain model loses levels; group/auto-claude-haiku-4-5 and -sonnet-4-5 get low/medium/high/max again (they had them on 09-30), group/auto-qwen3-8-omni-flash none/low/medium/xhigh; across models.dev, the 34 ids Azure emptied (kimi-k2.5, deepseek-v3.2, deepseek-r1, grok-4-20-reasoning …) borrow again. TestHostListIsNoMakersWord (provider) and TestDshGroupHasItsMembersLevels (agent: the group's dsh entry has reasoningEfforts and the thinking field offers low/high/max) fail on the old code. go vet, builds (darwin, GOOS=linux, GOOS=windows) and go test -tags nogui -count=1 ./... under a temp HOME pass.
routing: a group can name the reasoning levels agents are offered, so one member with few doesn't take the rest from the others (#295) — a group offered only the levels every member has, so deepseek-flash (low/high/max) beside models with medium and xhigh left the group without them. Group.Levels (CLI
magpie group set <id> levels=low,medium,high,xhigh, levels= clears; TUI l; GUI group editor's Levels: its models' shared / named) is offered instead in /v1/models, Codex's catalog, Pi and the rest; only levels magpie knows are taken, stored lowest first; a group in a group that names its own offers them for its models; unset is as before. A member without the level asked is sent its nearest, as it already was. The GUI's group save also keeps a family set from the CLI. TestGroupLevelsOverride, TestGroupLevelsReachAgents, TestGroupLevelsNamed (passthrough and translated: medium→high, xhigh→max on the low/high/max member, as asked on the one with all), TestGroupSetLevels and routing-levels.test.cjs (Chromium, WebKit, en/zh) fail without the change. go vet, go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui ./... pass (TestUserPath, a known flake under load, passes alone); click-scroll, old-webkit, routing-effort(-row), routing-levels, settings-groups GUI tests pass.
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).
routing: a group can name the reasoning levels agents are offered, so one member with few doesn't take the rest from the others (#295) — a group offered only the levels every member has, so deepseek-flash (low/high/max) beside models with medium and xhigh left the group without them. Group.Levels (CLI
magpie group set <id> levels=low,medium,high,xhigh, levels= clears; TUI l; GUI group editor's Levels: its models' shared / named) is offered instead in /v1/models, Codex's catalog, Pi and the rest; only levels magpie knows are taken, stored lowest first; a group in a group that names its own offers them for its models; unset is as before. A member without the level asked is sent its nearest, as it already was. The GUI's group save also keeps a family set from the CLI. TestGroupLevelsOverride, TestGroupLevelsReachAgents, TestGroupLevelsNamed (passthrough and translated: medium→high, xhigh→max on the low/high/max member, as asked on the one with all), TestGroupSetLevels and routing-levels.test.cjs (Chromium, WebKit, en/zh) fail without the change. go vet, go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui ./... pass (TestUserPath, a known flake under load, passes alone); click-scroll, old-webkit, routing-effort(-row), routing-levels, settings-groups GUI tests pass.
groups: a routing group whose member's reasoning levels nothing knows offers its other members' levels, and Pi's thinking field offers and shows what Pi runs the model at (Moody-Sin, #597: 路由组设了推理强度最高是否正确应用?) — Pi on group/deepseek (腾讯DeepSeek) at magpie's 最高 listed off alone in its /thinking, and sent no reasoning_effort: Pi clamps defaultThinkingLevel max to the off it offers, so the group's members were asked at the vendor's default, not max.
models: reasoning levels are borrowed again for models Azure also hosts, and a routing group keeps its members' levels when one thinks with none to pick from (ARNO on Discord: routing group在接入harness的时候,没有设置reasoning_efforts。我发现是之前很多能自动推导reasoning_efforts列表的模型现在都推导失败了) — the Azure OpenAI preset (
c35471cf, Catalog "azure, openai") was taken for a maker, so models.dev's azure list was the maker's word on every model it hosts: Kimi K2.5, DeepSeek V3.2 and R1, Grok 4.20/4.1 Fast Reasoning, listed there with no levels, took none on a relay or custom provider (and the gateway capped them at high as levelless), where the providers listing any give low…high, none/high and the rest. Azure is now marked Hosts, as Groq and Ollama Cloud are; its own provider still takes its own list's word. And groupEntries narrowed a group to the levels every member has, so one member that thinks with no levels (a thinking switch, a Claude 4.5 with a budget alone, OpenRouter's qwen3.8-omni-flash) left the group none and dsh's route wrote it without reasoningEfforts; such a member is now skipped as an unknown one is, since the gateway sends it the effort asked, no more than high (fitFor). The same Entry.Efforts feeds dsh, Codex's catalog, Pi and OpenCode. On a copy of the owner's config and models.dev cache: no plain model loses levels; group/auto-claude-haiku-4-5 and -sonnet-4-5 get low/medium/high/max again (they had them on 09-30), group/auto-qwen3-8-omni-flash none/low/medium/xhigh; across models.dev, the 34 ids Azure emptied (kimi-k2.5, deepseek-v3.2, deepseek-r1, grok-4-20-reasoning …) borrow again. TestHostListIsNoMakersWord (provider) and TestDshGroupHasItsMembersLevels (agent: the group's dsh entry has reasoningEfforts and the thinking field offers low/high/max) fail on the old code. go vet, builds (darwin, GOOS=linux, GOOS=windows) and go test -tags nogui -count=1 ./... under a temp HOME pass.
provider: Huawei Cloud's MaaS Token Plan is a preset (Lemon: 能否支持华为 codearts 订阅啊) — huaweicloud, Huawei Cloud MaaS: the Token Plan (personal accounts, 西南-贵阳一 only) is spent only at its own endpoints, https://api.modelarts-maas.com/plan/v2 for chat completions (its OpenClaw, Cherry Studio and CodeArts pages, tokenplan-maas-0004/0009/0014) and /plan/anthropic for messages (its Claude Code page, tokenplan-maas-0003); pay-as-you-go MaaS is the other Plan, /openai/v1 (qs-maas-0001, OpenCode page maas-modelarts-0909) and /anthropic (model-call-060). No Responses on either. Its models (glm-5.3, glm-5.1, kimi-k2.6, deepseek-v4.1-flash, deepseek-v4-flash, lowercase as its OpenClaw page has them) are given until a list is fetched; the key is a MaaS API key from console.huaweicloud.com/modelarts/?#/model-studio/authmanage; no usage API is documented (quota is shown in the console's 订阅管理), so none is read. CodeArts (码道) itself isn't a provider: it has no official API, its private endpoints are reverse-engineered, and its own doc has it reach the Token Plan through these same endpoints. Icon is lobehub's huaweicloud-color, as the others are.
grok: Grok Build is found on Windows where its installer puts it (#180: the Grok Build installer finished, but magpie can't find it) — magpie looked for ~/.grok/bin/grok, without .exe, and then only at the first grok on PATH, which on Windows may be another (an npm grok.cmd). Now it looks at GROK_BIN_DIR, the grok home's bin and ~/.grok/bin (grok.exe on Windows), then every folder on PATH and on the registry's PATH, which has what was installed since magpie started, taking a grok there only under a .grok folder; the install-first error gives the PowerShell command on Windows. Tested the order and names of the places looked at, and which grok is taken, in a fake home
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.
accounts: Codex or Claude Code can be kept signed in to any of its accounts, not only the first, while the gateway still tries them in their order (Koohoko on #524: 建议不只是支持始终登录"首选"账号,应该可以支持始终登录任一选定的账号。有时候首选只是为了按顺序先消耗它的额度,不一定是用户期望的常用登录账号) — making an account first did two things at once: the gateway tried it first (the account the agent is signed in to always stood ahead of the others, AlsoOn behind it), and Codex or Claude Code was signed in to it (SwitchLogin). "Keep {agent} signed in to the first account" (
817a5f50) only stopped magpie moving the sign-in on, so a user who wanted one account's allowance spent first had to sign the agent in to it too. Now beside that tick is a pick, "Keep {agent} signed in to [the first account ▾ | each saved account]" ({agent} 始终登录 [首选账号 ▾]): picking one keeps Provider.KeepLoginAs (providers.json, kept across an editor Save like KeepLogin; POST provider/keeplogin with keepLoginAs) and signs the agent in to it at once, the order the gateway went by then — the one signed in to first — written as the accounts' own first, so the first stays first. While it is kept: the login switcher signs the agent back in to it whenever it is on another and never moves it off, however used up (NextLogin); a drag or Make first sets the order without a sign-in (arrangeLoginsAs), Make first posting provider/arrange; the gateway puts the account signed in to at its own place in that order, not first (perKeyBarred by Provider.LoginRanks), so In order spends the first's allowance first; InUseLogin (the tray's in-use account) is the first in use in the order. The rows keep their order, the first saying First and the one kept saying Signed in (已登录), an account not in use ticked on by its dot rather than a Use that would sign the agent in; the Routing note says the agent stays signed in to that account and requests go in the accounts' order. Picking "the first account", or unticking, lets it go and signs the agent in to the first again, so the one signed in to is the first once more. TestKeepLoginAsChosen (signed in to the chosen at once, the order kept, stays at 100%, moved off it goes back, a drag changes the order not the sign-in, the first's again when let go) and TestKeptLoginStandsAtItsPlace (through the gateway In order: kept on spare, the first, acct-1, is tried first; acct-2 without the change) are new; keep-login.test.cjs now also picks an account (the post, First and Signed in, Make first arranging, back to the first, every string's Chinese, the page not moving) and passes 8/8 in WebKit; account-return, routing-account-order, account-arrange and click-scroll pass in WebKit. No live agent config, keychain or vendor was touched. go vet ./internal/..., go build (darwin GUI), go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui -count=1 ./... under a temp HOME pass.
accounts: Codex or Claude Code can be kept signed in to any of its accounts, not only the first, while the gateway still tries them in their order (Koohoko on #524: 建议不只是支持始终登录"首选"账号,应该可以支持始终登录任一选定的账号。有时候首选只是为了按顺序先消耗它的额度,不一定是用户期望的常用登录账号) — making an account first did two things at once: the gateway tried it first (the account the agent is signed in to always stood ahead of the others, AlsoOn behind it), and Codex or Claude Code was signed in to it (SwitchLogin). "Keep {agent} signed in to the first account" (
817a5f50) only stopped magpie moving the sign-in on, so a user who wanted one account's allowance spent first had to sign the agent in to it too. Now beside that tick is a pick, "Keep {agent} signed in to [the first account ▾ | each saved account]" ({agent} 始终登录 [首选账号 ▾]): picking one keeps Provider.KeepLoginAs (providers.json, kept across an editor Save like KeepLogin; POST provider/keeplogin with keepLoginAs) and signs the agent in to it at once, the order the gateway went by then — the one signed in to first — written as the accounts' own first, so the first stays first. While it is kept: the login switcher signs the agent back in to it whenever it is on another and never moves it off, however used up (NextLogin); a drag or Make first sets the order without a sign-in (arrangeLoginsAs), Make first posting provider/arrange; the gateway puts the account signed in to at its own place in that order, not first (perKeyBarred by Provider.LoginRanks), so In order spends the first's allowance first; InUseLogin (the tray's in-use account) is the first in use in the order. The rows keep their order, the first saying First and the one kept saying Signed in (已登录), an account not in use ticked on by its dot rather than a Use that would sign the agent in; the Routing note says the agent stays signed in to that account and requests go in the accounts' order. Picking "the first account", or unticking, lets it go and signs the agent in to the first again, so the one signed in to is the first once more. TestKeepLoginAsChosen (signed in to the chosen at once, the order kept, stays at 100%, moved off it goes back, a drag changes the order not the sign-in, the first's again when let go) and TestKeptLoginStandsAtItsPlace (through the gateway In order: kept on spare, the first, acct-1, is tried first; acct-2 without the change) are new; keep-login.test.cjs now also picks an account (the post, First and Signed in, Make first arranging, back to the first, every string's Chinese, the page not moving) and passes 8/8 in WebKit; account-return, routing-account-order, account-arrange and click-scroll pass in WebKit. No live agent config, keychain or vendor was touched. go vet ./internal/..., go build (darwin GUI), go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui -count=1 ./... under a temp HOME pass.
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)
…
providers: a Kilo Gateway preset serves Kilo Code's free models with no key (lml on Discord: the community plugin's free models come from OpenCode Zen, KiloCode Gateway and Cline) — Kilo's gateway is OpenRouter's API at https://api.kilo.ai/api/openrouter (KILO_OPENROUTER_BASE in Kilo-Org/kilocode packages/kilo-gateway; /api/gateway is the same), its list GET {base}/models in OpenRouter's shape with an isFree flag, readable with no key, and its docs (kilo-docs gateway/authentication.md) and backend (Kilo-Org/cloud web-shared ai-gateway/handlers/llm-proxy.ts, is-free-model.ts: kilo-auto/free, ":free", openrouter/free, stealth/*-alpha, Kilo's own free models) serve a free model to a caller with no credential, by IP, 200 requests an hour. The new "kilo" preset (Kilo Gateway, NoKey) is Ready with no key: its list is the gateway's own, asked as the Kilo CLI asks it, the free models alone without a key and every model with one, the free ones (isFree, or free by id) marked Free and priced at nothing in ListPrice; a model that says it takes no tools, or that makes no text (Lyria), is left out. Requests to Kilo's host (the gateway's and the Test button's) carry the Kilo CLI 7.8.3's headers (packages/opencode session/llm/request.ts, kilocode/const.ts, kilo-gateway headers.ts): HTTP-Referer https://kilocode.ai, X-Title Kilo Code, User-Agent Kilo-Code/7.8.3, X-KILOCODE-EDITORNAME "Kilo CLI 7.8.3", and on a chat x-kilocode-mode code with the conversation as its task (X-KILOCODE-TASKID, x-session-affinity, X-Session-Id, a ses_ id as for OpenCode), which the gateway keys its prompt cache on; no other provider gets them. Without a key no Authorization is sent: Kilo's clients send "Bearer anonymous", which the gateway reads as no credential only until no released client sends it (tokens.ts ANONYMOUS_BEARER_SENTINEL, "Delete this constant once no released client sends it"), and any other bearer it can't verify is refused with 401 rather than served as anonymous; pi-freeflow measured that a placeholder bearer gets 401 INVALID_TOKEN and no Authorization gets 200 on every free model. A key, when given, goes as a Bearer and reaches the paid models. The key field can now say more than "optional for local servers" (PresetDef.KeyHint: "optional: free models need no key", 可不填:免费模型无需密钥); Kilo's icon is its own (kilocode packages/ui provider/kilo.svg, MIT). Only Chat is set; Kilo's Responses and Anthropic routes are not used. One GET of the public /models was made while looking (399 models, 18 free); no chat request was made to Kilo. TestKiloModels (a fake gateway: asked as the CLI with no Authorization, the free models alone without a key and all with one, a model free by its flag priced at nothing), TestKiloClient and TestKiloClientHeaders (the CLI's headers and no Authorization on a keyless request through the gateway, the key on a keyed one, none of them on another provider) fail without the change. go vet ./internal/..., go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui ./internal/... pass (TestClaudeRunEndsWhenTheClientLeaves, TestUserPath and TestClaudeSignedOut failed once under the full run's load, pass on the rerun; none touches Kilo).
providers: a Kilo Gateway preset serves Kilo Code's free models with no key (lml on Discord: the community plugin's free models come from OpenCode Zen, KiloCode Gateway and Cline) — Kilo's gateway is OpenRouter's API at https://api.kilo.ai/api/openrouter (KILO_OPENROUTER_BASE in Kilo-Org/kilocode packages/kilo-gateway; /api/gateway is the same), its list GET {base}/models in OpenRouter's shape with an isFree flag, readable with no key, and its docs (kilo-docs gateway/authentication.md) and backend (Kilo-Org/cloud web-shared ai-gateway/handlers/llm-proxy.ts, is-free-model.ts: kilo-auto/free, ":free", openrouter/free, stealth/*-alpha, Kilo's own free models) serve a free model to a caller with no credential, by IP, 200 requests an hour. The new "kilo" preset (Kilo Gateway, NoKey) is Ready with no key: its list is the gateway's own, asked as the Kilo CLI asks it, the free models alone without a key and every model with one, the free ones (isFree, or free by id) marked Free and priced at nothing in ListPrice; a model that says it takes no tools, or that makes no text (Lyria), is left out. Requests to Kilo's host (the gateway's and the Test button's) carry the Kilo CLI 7.8.3's headers (packages/opencode session/llm/request.ts, kilocode/const.ts, kilo-gateway headers.ts): HTTP-Referer https://kilocode.ai, X-Title Kilo Code, User-Agent Kilo-Code/7.8.3, X-KILOCODE-EDITORNAME "Kilo CLI 7.8.3", and on a chat x-kilocode-mode code with the conversation as its task (X-KILOCODE-TASKID, x-session-affinity, X-Session-Id, a ses_ id as for OpenCode), which the gateway keys its prompt cache on; no other provider gets them. Without a key no Authorization is sent: Kilo's clients send "Bearer anonymous", which the gateway reads as no credential only until no released client sends it (tokens.ts ANONYMOUS_BEARER_SENTINEL, "Delete this constant once no released client sends it"), and any other bearer it can't verify is refused with 401 rather than served as anonymous; pi-freeflow measured that a placeholder bearer gets 401 INVALID_TOKEN and no Authorization gets 200 on every free model. A key, when given, goes as a Bearer and reaches the paid models. The key field can now say more than "optional for local servers" (PresetDef.KeyHint: "optional: free models need no key", 可不填:免费模型无需密钥); Kilo's icon is its own (kilocode packages/ui provider/kilo.svg, MIT). Only Chat is set; Kilo's Responses and Anthropic routes are not used. One GET of the public /models was made while looking (399 models, 18 free); no chat request was made to Kilo. TestKiloModels (a fake gateway: asked as the CLI with no Authorization, the free models alone without a key and all with one, a model free by its flag priced at nothing), TestKiloClient and TestKiloClientHeaders (the CLI's headers and no Authorization on a keyless request through the gateway, the key on a keyed one, none of them on another provider) fail without the change. go vet ./internal/..., go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui ./internal/... pass (TestClaudeRunEndsWhenTheClientLeaves, TestUserPath and TestClaudeSignedOut failed once under the full run's load, pass on the rerun; none touches Kilo).
kiro: a Builder ID sign-in is asked with Builder ID's service profile, so its models are listed (#422, Q1CHENL: only Auto offered while kiro-cli lists Opus 5.5, Sonnet 5 …) — a Builder ID sign-in (kiro-cli's, the Kiro IDE's or magpie's own) names no profile, and Kiro won't list one for it: List-Available-Profiles answers 403 "AWS Builder ID is not supported for this operation" in both regions, so every call needing the account's auth failed — List-Available-Models (magpie fell back to Auto alone), Get-Usage-Limits (the account stayed "Kiro account") and chat. A sign-in kiro-cli keeps with Builder ID's start URL (or none, as kiro-cli takes it), or one saved with provider BuilderId, now takes the service profile the Kiro IDE and kiro-cli use for Builder ID (arn:aws:codewhisperer:us-east-1:638616132270:profile/AAAACCCCXXXX) without asking, and an AWS sign-in whose profile listing is that 403 takes it too. Identity Center sign-ins still have their profile listed. TestKiroBuilderIDListsItsModels and TestKiroBuilderIDFoundByKirosAnswer fail without the change (fake management API). go vet, go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui ./... pass (TestFXInState's temp-dir cleanup flaked once under load, passes alone and with its package).
test: usage's and provider's TestMains clear the agents' variables too, and a new agent's variable left out of the list now fails (yetone, reviewing #543: internal/usage/main_test.go's TestMain still clears no agent variables, so a CLAUDE_CONFIG_DIR left in the shell reaches the real ~/.claude there too; nothing fails yet when a new agent's variable is left out of Vars) — both TestMains gave their package a home of its own and cleared no agent variable, the provider one as much as the usage one that was named. A CLAUDE_CONFIG_DIR left in the shell reached the real ~/.claude through the ledger's sessions.Calls (usage/ledger.go:122): with a canary Claude folder holding three of the sessions package's fixtures, six usage tests fail — TestLedger, TestSubscriptionListPrice, TestLedgerUsesTheStatedPrice, TestLedgerConfiguredPricesAndRefresh and the two Antigravity ledger ones — and pass with the clearing; provider passes with the same canary either way, but its Accounts read what an agent keeps on the machine too. #543 missed these by how it looked for its sites: by the clearing loops a sandbox already had, which one with no loop at all has no way to answer. TestFolderVarsAreListed reads the sources of the four packages an agent's folder variable is read in — this one, sessions, provider and library, the rest of the repository reading only magpie's own variables, the process' and the desktop's — and fails on a name that is neither in agentenv.Vars nor excused here with a reason, so a new agent's variable no longer waits for a developer's shell to meet one. It reads them as files, since a test in sessions or provider cannot import this package, which imports them; it asks for one name each way its pattern matches, so a change in how a variable is read empties the scan loudly instead of passing it, and for every name excused, so no excuse outlives its variable. What it cannot see is a name the sources build at runtime, as omoDir reads its two out of a slice (omo.go:33); both are listed, and a third taken the same way would not be caught. The package doc's VarsTest is TestVarsAreRead. TestFolderVarsAreListed fails without the change when KIMI_CODE_HOME is taken out of Vars, or when a pretend NEWAGENT_HOME is read in agents.go, and names the file that reads it; the six usage tests fail without theirs. 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. go vet ./..., go build -tags nogui (windows, GOOS=linux, GOOS=darwin), go vet -tags nogui for linux and darwin over every package touched, and gofmt pass. (#549)
usage: a Codex account's credits bought or given besides its plan show on the Usage page (#571: codex的套餐用完之后,可能会购买额外的信用点套餐…希望能做展示) — ChatGPT's /wham/usage answers, besides the 5-hour and weekly windows, the account's credits (credits: has_credits, unlimited, balance), which Codex spends once a window is used up; magpie read only the windows, so an account whose windows were spent showed spent though it still had credits to run on. codexWindows now reads them too, and a balance held (not unlimited, above 0) is the account's Balance, shown under its windows on the Usage page ("What is left on the account besides its windows") and in the panel card, for the account Codex is signed in to and for each saved one. TestCodexCreditsBalance (held, a few, none said, none held, spent, unlimited, unreadable). go vet, go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui ./... under a temp HOME pass.
providers: account usage no longer writes its map from two goroutines at once — an account fetched less than a minute ago went into the map the other accounts' fetches were still writing to, which Go answers with a fatal concurrent map writes, not a panic anything recovers from; LoginUsage and Grok's copy of it now write that hit under the lock their fetches use (#119)
factory: the mover takes plugin 0.1.14, which adapts Claude Code 2.1.288's runtime context sent as a system message of text blocks (#658)
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).
usage: WorkBuddy's credits are said beside their share — on the Routing page, the Usage page and the menu bar, used or left as the share is (#659)
mimo: a Xiaomi MiMo account (the one MiMo's app signs in to) is signed in inside magpie and its models served through the gateway to any agent, and MiMo Code is an agent magpie configures (#249 thedavidweng) — magpie had neither. The subscription (id mimo-app, shown as Xiaomi MiMo) signs in with Xiaomi's long-poll login (account.xiaomi.com/longPolling/loginUrl, sid mimosgp) and keeps the MiMo server's cookie session (serviceToken, userId) in logins.json at 0600; the passToken is replayed to renew the session after a day or on a 401. Requests go to POST <base>/route/chat/completions on mimo-server-{sgp,ru,in}.xiaomimimo.com/api with X-Mimo-Source mimocode-cli-free and X-Client-Version; mimo-auto is sent as mimo-pro; the models are mimo-pro and mimo-flash. Usage from /user/usage and /user/xiaomi/subscription/self. Several accounts, switched, on/off and forgotten as the other subscriptions. The add list has a Xiaomi MiMo tile (Free · Starter · Plus · Pro · Ultra) that says Xiaomi may treat this as third-party use before anything is posted; en and zh.
magpie accounts takes mimo. MiMo Code (agent mimocode, OpenCode-like) is configured in its config dir (MIMOCODE_HOME/config when set), reading mimocode.jsonc, mimocode.json or config.json, whichever is there. Tested with internal/provider/mimo_test.go against fake Xiaomi SSO and MiMo servers in a temp HOME, internal/agent tests for MiMo Code's config, and internal/gui/tests/mimo-sub.test.cjs, Chromium and WebKit, en and zh; they fail without the change. factory-sub, click-scroll, old-webkit, zed-sub and add-sheet pass; go vet, go build -tags nogui (and GOOS=linux, GOOS=windows) and go test -tags nogui ./... pass. Not tried with a real Xiaomi account: the long-poll's success payload, the SSO cookie chain, whether /route accepts magpie's requests, the UTC+8 times, and whether MiMo Code picks up the config; not tried in the real Wails window.
provider: OpenCode Zen's Jev routes groups on System One, and a preset's or a subscription's model can be asked on the API picked (Discord, 01huadalang: Zen's "Jev free" was asked as a chat model and failed with no way to switch; OpenCode Go's DeepSeek V4.1 Flash answers on Responses too, and magpie asked it on chat alone, while a custom provider could pick) — Jev isn't an Anthropic model: OpenCode's docs (anomalyco/opencode packages/web/src/content/docs/zen.mdx) list jev-1.13 and jev-1.13-free at https://opencode.ai/zen/v1/systemone alone, and models.dev has no entry for it; asked keyless, Zen answers it there and errors on chat, messages and responses. The opencode-zen preset now has Decide (https://opencode.ai/zen/v1), making Zen a mixed decision provider (#609): Jev is left out of agents' models and is a routing groups' classifier, the free one when Zen is used without a key; a Zen provider saved before gets Decide back in Normalize. Zen's and Go's other models already take their API from models.dev's npm package (catalog.APIOf), which matches the docs. The per-model API custom providers have (settings ModelAPIs, the gateway's Provider.APIs) now reaches presets and subscriptions with more than one API: ModelAPI no longer refuses an account, APIs() is the pick or ListedAPIs (what the vendor's list says, sent to the editor as a model's auto); the editor's model chip has "Asked on: Auto…" on its right-click, opening the app's menu (Auto says what the vendor's list says, then each API the provider has), tagging the chip with the pick, staged and sent with Save as modelPrefs, the page not moved; Names & levels' API row shows for subscriptions too. English and Chinese. TestOpenCodeZenJevDecides and TestPresetAndSubscriptionModelAPI fail on the old code and pass; internal/gui/tests/model-api-preset.test.cjs (WebKit, en/zh) covers the menu, no <select>, no left-border accent, no scroll, the Save's modelPrefs, a subscription's tag and a single-API provider with nothing to pick, and fails on the old assets. A sandbox magpie (keyless Zen) answered POST /v1/systemone with opencode-zen/jev-1.13-free (noul 0.44) and chat on mimo-v2.6-flash-free, Jev not in /v1/models. go vet, builds (darwin, GOOS=linux, GOOS=windows) and go test -tags nogui -count=1 ./... under a temp HOME pass but TestUserPath, a timing test that passes alone; provider-levels-scroll.test.cjs fails the same on origin/main. Not tried with a paid Zen/Go key.
provider: OpenCode Zen's Jev routes groups on System One, and a preset's or a subscription's model can be asked on the API picked (Discord, 01huadalang: Zen's "Jev free" was asked as a chat model and failed with no way to switch; OpenCode Go's DeepSeek V4.1 Flash answers on Responses too, and magpie asked it on chat alone, while a custom provider could pick) — Jev isn't an Anthropic model: OpenCode's docs (anomalyco/opencode packages/web/src/content/docs/zen.mdx) list jev-1.13 and jev-1.13-free at https://opencode.ai/zen/v1/systemone alone, and models.dev has no entry for it; asked keyless, Zen answers it there and errors on chat, messages and responses. The opencode-zen preset now has Decide (https://opencode.ai/zen/v1), making Zen a mixed decision provider (#609): Jev is left out of agents' models and is a routing groups' classifier, the free one when Zen is used without a key; a Zen provider saved before gets Decide back in Normalize. Zen's and Go's other models already take their API from models.dev's npm package (catalog.APIOf), which matches the docs. The per-model API custom providers have (settings ModelAPIs, the gateway's Provider.APIs) now reaches presets and subscriptions with more than one API: ModelAPI no longer refuses an account, APIs() is the pick or ListedAPIs (what the vendor's list says, sent to the editor as a model's auto); the editor's model chip has "Asked on: Auto…" on its right-click, opening the app's menu (Auto says what the vendor's list says, then each API the provider has), tagging the chip with the pick, staged and sent with Save as modelPrefs, the page not moved; Names & levels' API row shows for subscriptions too. English and Chinese. TestOpenCodeZenJevDecides and TestPresetAndSubscriptionModelAPI fail on the old code and pass; internal/gui/tests/model-api-preset.test.cjs (WebKit, en/zh) covers the menu, no <select>, no left-border accent, no scroll, the Save's modelPrefs, a subscription's tag and a single-API provider with nothing to pick, and fails on the old assets. A sandbox magpie (keyless Zen) answered POST /v1/systemone with opencode-zen/jev-1.13-free (noul 0.44) and chat on mimo-v2.6-flash-free, Jev not in /v1/models. go vet, builds (darwin, GOOS=linux, GOOS=windows) and go test -tags nogui -count=1 ./... under a temp HOME pass but TestUserPath, a timing test that passes alone; provider-levels-scroll.test.cjs fails the same on origin/main. Not tried with a paid Zen/Go key.
codex: Ultra on GPT-6.1 Sol wherever it is served, Copilot's too, with Codex's agents taking parts of the task (#656) — Codex offers Ultra on a model whose entry lists "ultra", and on a thread at multi-agent V2 hands parts of the task to agents of its own; its catalog (models-manager/models.json) lists it for gpt-6.1-sol, gpt-6-sol, gpt-6-astra, gpt-5.6-sol and gpt-5.6-terra, not the Lunas, which stop at max. magpie listed Copilot's gpt-6.1-sol only up to max, so Codex had no Ultra for it. Now an entry for one of those models (by the id the vendor serves: openai/…, dated snapshots) that reaches max lists ultra too, in Codex's catalog and /v1/models, and its Codex entry says multi_agent_version v2, as Codex's own does; GPT-6 Luna still stops at max. A ChatGPT account's entry stays as the backend lists it, and a group with a ChatGPT account in it offers ultra but isn't told V2 (its lead's subagent tasks come sealed, #141); the "subagents on any model" setting still stamps v1 on OpenAI's entries. Ultra is no API's level: the gateway now always sends max for it (fitEffort), even to a model whose list says ultra. Subagent tasks with web_search kept (the translated path) were fixed in v0.1.689 (#613); TestCopilotUltraSubagentWithWebSearch now covers it against a stub Copilot: the lead's spawn_agent comes back with plain arguments and encrypted_function_args [], no "encrypted" goes upstream, and the subagent's agent_message reaches Copilot as text. TestCopilotSolOffersUltra (sol has ultra in /v1/models and Codex's catalog with v2, luna not; ultra goes to Copilot as max, with and without web_search) and TestUltraIsSentAsMax fail on the old code; TestUltraOnModelsThatOfferIt and TestUltraEntriesSayV2 cover entries, groups and the catalog. go vet, builds (darwin, GOOS=linux, GOOS=windows) and go test -tags nogui -count=1 ./... under a temp HOME pass. Not tried in the Codex App with a real Copilot account.
providers: a model a vendor names its own way is merged with the same model from other providers — by its id as Volcengine Ark spells it, or by the model the user says it is the same as in Names & levels (kyzhouxu, #583: 火山引擎获取到的默认 ds 4.1 flash 模型的名称跟其他供应商的名称不一样,没有自动合并到同一个模型分组) — the groups magpie finds merged a model by its id lowercased, its vendor prefix and an eight-digit date taken off and a version's dot made a dash, so Ark's deepseek-v4-1-flash-260910 (a six-digit YYMMDD snapshot) was a group of its own beside ali-tp's deepseek-v4.1-flash. sameModel now also takes off Ark's YYMMDD (a year from 23 to 39, a month and a day that are one), Vertex's "@YYYYMMDD", reads Fireworks' "p" between digits (deepseek-v4p1) as the dot and "_" as "-"; -flash, -thinking, ":batch", a four-digit release (qwen's -2507, kimi-k2-0905) and a number that is no date (-128000, -261301) stay apart. For an id no rule matches (an Ark endpoint id, a relay's own name), each model in a provider's Names & levels now has a "Same as" (等同于) box, its placeholder the name it is merged by now: the model it is the same as, as any vendor spells it, staged in draft.modelPrefs and made with the provider's Save like the other prefs (Cancel drops it, Restore default empties it, nothing sent or redrawn or scrolled before the Save). It is kept in settings' ModelSameAs by "<provider>/<model>" (renamed with the provider like every Model* map), set by provider.SetModelSame (its own id however spelt keeps nothing; a model the provider doesn't serve, or a name of no model, is refused) and taken by autoGroups, AutoStandIn and the agents' reseat (AutoGroupOf); the agents are told once. TestSameModelVendorSpellings (Ark, Fireworks, Vertex, underscores, and pairs that must stay apart) and TestModelSameAsMergesFoundGroup (c/ep-sol-x joins auto-sol once said, GroupFor("sol") finds it, back out when cleared, refusals) are new; TestProviderSaveModelPrefs covers a Save's same; provider-same-as.test.cjs (WebKit, en and zh: placeholder, staging, unsaved, Restore default, Cancel, one Save carrying exactly the changes, no scroll, Chinese for each string) fails without the change and passes with it; provider-levels-save and provider-detect pass. 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.
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)
providers: NVIDIA and ModelScope are presets (#197: 增加 NVIDIA NIM 和魔搭 ModelScope 作为供应商预设) — NVIDIA NIM at integrate.api.nvidia.com/v1 with a key from build.nvidia.com, and 魔搭's API-Inference at api-inference.modelscope.cn/v1 with a ModelScope access token (not DashScope, which the Qwen presets are), each with its models.dev catalog (nvidia, modelscope) and its logo. NVIDIA's /v1/responses answers 404 for its models but a few, so it has chat completions only; ModelScope's /v1/responses is served (401 without a token, 404 for a path it doesn't know), so it has both. Neither has an Anthropic endpoint here. An entry imported at either base URL is the preset, and the import link pages and README list them. Tested: the presets' endpoints, catalogs and import, a /v1/models answer shaped as each sends it (NVIDIA's embedding models left out, ModelScope's object "" rows kept), and catalog.FetchAt against the live integrate.api.nvidia.com (74 models) and api-inference.modelscope.cn (35) lists
providers: oMLX is a local-server preset — its :8000 server speaks Chat, Responses and Anthropic Messages and asks no key on a loopback bind, so it joins Ollama and LM Studio in the add sheet's On this machine section instead of being typed as a custom provider (no issue: the gap was simply that oMLX, an OpenAI-compatible MLX server for Apple Silicon, was missing a preset). TestOmlxPreset fails without the change (no omlx preset). 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 but for TestBunThroughBridge, which fails on a clean tree too (a local bun, unrelated to this change). (#635)
providers: OpenCode Zen's free models answer with no key, as OpenCode signed out asks them (#572: opencode zen的free模型,只能在他自己的cli里面用,能否做插件反代出来?) — magpie has asked Zen's free models as OpenCode does since v0.1.615 (zenfree.go), but the OpenCode Zen preset wanted a key, which means an opencode.ai account. OpenCode itself, signed out, asks Zen with the key "public", and Zen lists and serves its -free models to it (checked: /zen/v1/models with it lists twelve). The preset's key is now optional ("optional: free models need no key"), and a Zen provider saved with none asks with OpenCode's (normalize, provider.OpenCodeAnonymousKey); a key of the user's own is kept, and the editor's blank key still keeps the one saved. TestOpenCodeZenNeedsNoKey (keyless Add refused without the change) is new. 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.
providers: OpenCode Zen's free models answer with no key, as OpenCode signed out asks them (#572: opencode zen的free模型,只能在他自己的cli里面用,能否做插件反代出来?) — magpie has asked Zen's free models as OpenCode does since v0.1.615 (zenfree.go), but the OpenCode Zen preset wanted a key, which means an opencode.ai account. OpenCode itself, signed out, asks Zen with the key "public", and Zen lists and serves its -free models to it (checked: /zen/v1/models with it lists twelve). The preset's key is now optional ("optional: free models need no key"), and a Zen provider saved with none asks with OpenCode's (normalize, provider.OpenCodeAnonymousKey); a key of the user's own is kept, and the editor's blank key still keeps the one saved. TestOpenCodeZenNeedsNoKey (keyless Add refused without the change) is new. 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.
routing, models: a rerouted request's tooltip names who answered it (#337), and model names can go without their provider (#335) — the Routing page's and the tray panel's rows named the member that answered a rerouted request, but their tooltip still said the path first resolved; it now says the provider · account · model that answered, and on a second line what it was rerouted from. A setting, Settings → Preferences "Provider in model names" or magpie model suffix off (on by default, as before), writes the models listed to Codex and the other agents (OpenCode, Pi, omp…) by their name alone, except two in an agent's list that would read the same, which keep theirs. routing-reroute-title.test.cjs and plain-names.test.cjs (Chromium, WebKit, en/zh), TestPlainNames, TestPlainNamesReachAgents and TestModelSuffixCmd fail without the change; click-scroll, old-webkit, currency, text-size, panel-routing, routing-served, routing-steady pass. go vet, go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui ./... pass.
subscription: WorkBuddy AI, the international build, is its own subscription (Workbuddy国际版的Subscription是否也要支持呀?… 你应该新加一个Subscription Provider叫做Workbuddy国际版) — workbuddy-ai, "WorkBuddy 国际版 (WorkBuddy AI)" under 订阅. From WorkBuddy AI.app 5.6.2's product config: the same API as the Chinese build at its own endpoint, https://www.workbuddy.ai (isOversea, externalDomain www.codebuddy.ai / www.workbuddy.ai), signed in with platform=workbuddy-ai; its own account is read, never changed, from workbuddy-desktop-ai.info beside workbuddy-desktop.info in CodeBuddyExtension's auth folder, others kept in logins.json apart from the Chinese build's. Its models are its own (the Fast/Balanced/Primary/Deep tiers, GPT-5.5/5.4/5.3-Codex, Gemini 3.1 Pro/3.5 Flash, GLM-5.3/5.2, Hy3, Kimi-K3/K2.6, MiniMax-M3) until /v3/config is read; credits from the same resource summary. workbuddy.go is now parametrised by a wbSite (CN, AI); token cache keyed by site and uid. Checked live: auth/state at www.workbuddy.ai answers and magpie opens https://www.workbuddy.ai/login?platform=workbuddy-ai (200); chat and usage not tried against a real international account.
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)
usage: quota cards draw what was left over time, against an even burn, and the tray card its cycle's line (#651, songlairui) — each reading magpie makes anyway of a subscription's windows (and of a plan bought with a key) is kept as a point in quota-history.json beside the config: provider, account, window, when read, percent left, the window's start and reset — no token, no prompt. A failed reading, one kept from before (As of), an unlimited window and a model family's aren't kept; Antigravity's pools are kept by pool. Within a cycle points are 5 minutes apart at least and a run of one figure is its two ends; a cycle ends where the window starts again (its reset passed, or moved further than the time between two readings, so a rolling window's isn't one). 45 days and 6000 points a window are kept. The Usage page's card has, under its meters, a line a window (several tiers of one account on one chart), broken at each reset, a dashed even burn from each window's start to its reset and an upright now line, with 2 days / Cycle (the account's longest current window, start to reset) in the app's segmented control, remembered, turning every card without moving the page. The tray's card has a thin line of its first window's current cycle. GET /v1/magpie/quotas/history (?days=, ?provider=, ?user=; local or LAN with the key, as /v1/magpie/quotas) and /api/usage/quotas/history serve it; /v1/magpie/quotas and
magpie quota stay snapshots. Computers sharing usage (WebDAV/S3, Settings › Sync › usage) put their history beside their days, sealed, as <id>.magpie-quotas, and merge the others': the union by account, window and time, two points within 30 s the newer, nothing twice however often they sync; an older magpie never reads the file. TestQuotaHistoryRecords, TestQuotaHistoryCycles, TestMergeQuotaHistory, TestQuotasHistory and TestQuotaHistorySharedBetweenComputers cover recording, cycles, pruning, merge, the API and two computers on one WebDAV folder; quota-curve.test.cjs (WebKit, en/zh, light and dark) the curves, the toggle, no curve without readings, and the tray's line (Chromium doesn't start in this sandbox). go vet, builds (darwin, GOOS=linux, GOOS=windows) and go test -tags nogui -count=1 ./... under a temp HOME pass. Not tried with real accounts; pay-as-you-go balances aren't drawn.
usage: WorkBuddy's credits are said beside their share — on the Routing page, the Usage page and the menu bar, used or left as the share is (#659)
usage: WorkBuddy's credits are said beside their share — on the Routing page, the Usage page and the menu bar, used or left as the share is (#659)
models: reasoning levels are borrowed again for models Azure also hosts, and a routing group keeps its members' levels when one thinks with none to pick from (ARNO on Discord: routing group在接入harness的时候,没有设置reasoning_efforts。我发现是之前很多能自动推导reasoning_efforts列表的模型现在都推导失败了) — the Azure OpenAI preset (
c35471cf, Catalog "azure, openai") was taken for a maker, so models.dev's azure list was the maker's word on every model it hosts: Kimi K2.5, DeepSeek V3.2 and R1, Grok 4.20/4.1 Fast Reasoning, listed there with no levels, took none on a relay or custom provider (and the gateway capped them at high as levelless), where the providers listing any give low…high, none/high and the rest. Azure is now marked Hosts, as Groq and Ollama Cloud are; its own provider still takes its own list's word. And groupEntries narrowed a group to the levels every member has, so one member that thinks with no levels (a thinking switch, a Claude 4.5 with a budget alone, OpenRouter's qwen3.8-omni-flash) left the group none and dsh's route wrote it without reasoningEfforts; such a member is now skipped as an unknown one is, since the gateway sends it the effort asked, no more than high (fitFor). The same Entry.Efforts feeds dsh, Codex's catalog, Pi and OpenCode. On a copy of the owner's config and models.dev cache: no plain model loses levels; group/auto-claude-haiku-4-5 and -sonnet-4-5 get low/medium/high/max again (they had them on 09-30), group/auto-qwen3-8-omni-flash none/low/medium/xhigh; across models.dev, the 34 ids Azure emptied (kimi-k2.5, deepseek-v3.2, deepseek-r1, grok-4-20-reasoning …) borrow again. TestHostListIsNoMakersWord (provider) and TestDshGroupHasItsMembersLevels (agent: the group's dsh entry has reasoningEfforts and the thinking field offers low/high/max) fail on the old code. go vet, builds (darwin, GOOS=linux, GOOS=windows) and go test -tags nogui -count=1 ./... under a temp HOME pass.
providers: a model a vendor names its own way is merged with the same model from other providers — by its id as Volcengine Ark spells it, or by the model the user says it is the same as in Names & levels (kyzhouxu, #583: 火山引擎获取到的默认 ds 4.1 flash 模型的名称跟其他供应商的名称不一样,没有自动合并到同一个模型分组) — the groups magpie finds merged a model by its id lowercased, its vendor prefix and an eight-digit date taken off and a version's dot made a dash, so Ark's deepseek-v4-1-flash-260910 (a six-digit YYMMDD snapshot) was a group of its own beside ali-tp's deepseek-v4.1-flash. sameModel now also takes off Ark's YYMMDD (a year from 23 to 39, a month and a day that are one), Vertex's "@YYYYMMDD", reads Fireworks' "p" between digits (deepseek-v4p1) as the dot and "_" as "-"; -flash, -thinking, ":batch", a four-digit release (qwen's -2507, kimi-k2-0905) and a number that is no date (-128000, -261301) stay apart. For an id no rule matches (an Ark endpoint id, a relay's own name), each model in a provider's Names & levels now has a "Same as" (等同于) box, its placeholder the name it is merged by now: the model it is the same as, as any vendor spells it, staged in draft.modelPrefs and made with the provider's Save like the other prefs (Cancel drops it, Restore default empties it, nothing sent or redrawn or scrolled before the Save). It is kept in settings' ModelSameAs by "<provider>/<model>" (renamed with the provider like every Model* map), set by provider.SetModelSame (its own id however spelt keeps nothing; a model the provider doesn't serve, or a name of no model, is refused) and taken by autoGroups, AutoStandIn and the agents' reseat (AutoGroupOf); the agents are told once. TestSameModelVendorSpellings (Ark, Fireworks, Vertex, underscores, and pairs that must stay apart) and TestModelSameAsMergesFoundGroup (c/ep-sol-x joins auto-sol once said, GroupFor("sol") finds it, back out when cleared, refusals) are new; TestProviderSaveModelPrefs covers a Save's same; provider-same-as.test.cjs (WebKit, en and zh: placeholder, staging, unsaved, Restore default, Cancel, one Save carrying exactly the changes, no scroll, Chinese for each string) fails without the change and passes with it; provider-levels-save and provider-detect pass. 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.
sync: the order the providers were put in on the Providers tab goes with them, so a computer that syncs lists and tries them as the other does (ARNO on Discord: 发现webdav同步的时候没有同步provider的顺序) — since #499 the order is kept apart from the providers, as providers.json's "order" (ids, the built-in signed-in accounts among them), and the list is sorted by it when read; the providers themselves stay in the file in the order they were added. The sync's bundle (backup.Bundle) carried the providers, icons, groups and search APIs but not that order, and the providers part was hashed without it, so arranging the list was not even a change to sync, and a computer bringing the providers in (provider.Mirror) kept its own order, or none. The bundle now has Order (omitempty, from provider.StoredOrder), hashed with the providers part only when there is one, so the hashes of a setup never arranged are as before and an older magpie's file still compares the same; take carries it with the providers, and bring applies it after Mirror with the new provider.MirrorOrder: the other computer's ids first as it has them, then the ones named here alone (an account only this computer has) in the order they have here; none, from a magpie before this or a setup never arranged, leaves the order here as it is. This goes for WebDAV and S3 alike (one path). A backup restored by hand still merges providers without changing the order. TestSyncProviderOrder (a arranges three providers, b joins and lists them in a's order and pushes nothing back, b arranging only is pushed and a follows; against an httptest WebDAV server) fails without the change ("b after joining: [one two three]"); TestMirrorOrder covers the merge. go vet ./internal/..., go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui ./... (temp HOME) pass.
provider: OpenCode Zen's Jev routes groups on System One, and a preset's or a subscription's model can be asked on the API picked (Discord, 01huadalang: Zen's "Jev free" was asked as a chat model and failed with no way to switch; OpenCode Go's DeepSeek V4.1 Flash answers on Responses too, and magpie asked it on chat alone, while a custom provider could pick) — Jev isn't an Anthropic model: OpenCode's docs (anomalyco/opencode packages/web/src/content/docs/zen.mdx) list jev-1.13 and jev-1.13-free at https://opencode.ai/zen/v1/systemone alone, and models.dev has no entry for it; asked keyless, Zen answers it there and errors on chat, messages and responses. The opencode-zen preset now has Decide (https://opencode.ai/zen/v1), making Zen a mixed decision provider (#609): Jev is left out of agents' models and is a routing groups' classifier, the free one when Zen is used without a key; a Zen provider saved before gets Decide back in Normalize. Zen's and Go's other models already take their API from models.dev's npm package (catalog.APIOf), which matches the docs. The per-model API custom providers have (settings ModelAPIs, the gateway's Provider.APIs) now reaches presets and subscriptions with more than one API: ModelAPI no longer refuses an account, APIs() is the pick or ListedAPIs (what the vendor's list says, sent to the editor as a model's auto); the editor's model chip has "Asked on: Auto…" on its right-click, opening the app's menu (Auto says what the vendor's list says, then each API the provider has), tagging the chip with the pick, staged and sent with Save as modelPrefs, the page not moved; Names & levels' API row shows for subscriptions too. English and Chinese. TestOpenCodeZenJevDecides and TestPresetAndSubscriptionModelAPI fail on the old code and pass; internal/gui/tests/model-api-preset.test.cjs (WebKit, en/zh) covers the menu, no <select>, no left-border accent, no scroll, the Save's modelPrefs, a subscription's tag and a single-API provider with nothing to pick, and fails on the old assets. A sandbox magpie (keyless Zen) answered POST /v1/systemone with opencode-zen/jev-1.13-free (noul 0.44) and chat on mimo-v2.6-flash-free, Jev not in /v1/models. go vet, builds (darwin, GOOS=linux, GOOS=windows) and go test -tags nogui -count=1 ./... under a temp HOME pass but TestUserPath, a timing test that passes alone; provider-levels-scroll.test.cjs fails the same on origin/main. Not tried with a paid Zen/Go key.
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.
subscriptions: Qoder CN, for accounts on qoder.cn (#349) — Qoder's China site (qoder.cn, Alibaba Cloud or phone sign-in) has its own accounts, sign-in page, account API (openapi.qoder.com.cn) and gateway (gateway.qoder.com.cn), so a qoder.cn account couldn't sign in through magpie's Qoder, which only knows qoder.com. A new Qoder CN subscription signs in there with the client id Qoder CN's own CLI sends, and everything after — polling, job token and refresh, user info, models, usage and chat — follows the site saved with the account; if qoder.cn refuses the job token, the account chats with its device token and renews it, as that CLI does. Qoder's tile is now Qoder (international), each tile saying which accounts it is for. Accounts saved before keep working on qoder.com unchanged. Not tried with a real qoder.cn account. TestSites, TestCNDeviceChatRefresh, TestQoderCNSignIn, TestQoderCNDeviceChat, TestQoderGlobalUnchanged and TestQoderCNGateway fail without the change; qoder-cn-tile.test.cjs (Chromium, WebKit, en/zh) added. go vet, go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui ./... pass but for TestCursorPluginContext, which fails on main (
1ce05f0) as well; click-scroll, old-webkit pass.
subscriptions: Qoder CN, for accounts on qoder.cn (#349) — Qoder's China site (qoder.cn, Alibaba Cloud or phone sign-in) has its own accounts, sign-in page, account API (openapi.qoder.com.cn) and gateway (gateway.qoder.com.cn), so a qoder.cn account couldn't sign in through magpie's Qoder, which only knows qoder.com. A new Qoder CN subscription signs in there with the client id Qoder CN's own CLI sends, and everything after — polling, job token and refresh, user info, models, usage and chat — follows the site saved with the account; if qoder.cn refuses the job token, the account chats with its device token and renews it, as that CLI does. Qoder's tile is now Qoder (international), each tile saying which accounts it is for. Accounts saved before keep working on qoder.com unchanged. Not tried with a real qoder.cn account. TestSites, TestCNDeviceChatRefresh, TestQoderCNSignIn, TestQoderCNDeviceChat, TestQoderGlobalUnchanged and TestQoderCNGateway fail without the change; qoder-cn-tile.test.cjs (Chromium, WebKit, en/zh) added. go vet, go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui ./... pass but for TestCursorPluginContext, which fails on main (
1ce05f0) as well; click-scroll, old-webkit pass.
usage: a notification when a window is nearly used up or a balance runs low (#368) — Settings has Usage alert (off, or a share of 1–100%, 80 by default) and Low balance alert (off, or an amount, 5 by default), and magpie quota alert sets them from the command line. Each subscription or plan window, other than one set aside, is told once a run when it reaches the share, and a balance once until topped up past the amount again; what was said is kept, so a restart doesn't say it again, and a failed or stale reading neither alerts nor clears. The system's notifications are started only when first needed, so a Mac build that can't have them or a Linux desktop without a daemon still starts, and Settings says when they are refused or unavailable; a click opens the Usage page. TestDueAlerts and friends, TestAlertText and usage-alert.test.cjs fail without the change.
usage: a notification when a window is nearly used up or a balance runs low (#368) — Settings has Usage alert (off, or a share of 1–100%, 80 by default) and Low balance alert (off, or an amount, 5 by default), and magpie quota alert sets them from the command line. Each subscription or plan window, other than one set aside, is told once a run when it reaches the share, and a balance once until topped up past the amount again; what was said is kept, so a restart doesn't say it again, and a failed or stale reading neither alerts nor clears. The system's notifications are started only when first needed, so a Mac build that can't have them or a Linux desktop without a daemon still starts, and Settings says when they are refused or unavailable; a click opens the Usage page. TestDueAlerts and friends, TestAlertText and usage-alert.test.cjs fail without the change.
usage: WorkBuddy's credits are said beside their share — on the Routing page, the Usage page and the menu bar, used or left as the share is (#659)
usage: quota cards draw what was left over time, against an even burn, and the tray card its cycle's line (#651, songlairui) — each reading magpie makes anyway of a subscription's windows (and of a plan bought with a key) is kept as a point in quota-history.json beside the config: provider, account, window, when read, percent left, the window's start and reset — no token, no prompt. A failed reading, one kept from before (As of), an unlimited window and a model family's aren't kept; Antigravity's pools are kept by pool. Within a cycle points are 5 minutes apart at least and a run of one figure is its two ends; a cycle ends where the window starts again (its reset passed, or moved further than the time between two readings, so a rolling window's isn't one). 45 days and 6000 points a window are kept. The Usage page's card has, under its meters, a line a window (several tiers of one account on one chart), broken at each reset, a dashed even burn from each window's start to its reset and an upright now line, with 2 days / Cycle (the account's longest current window, start to reset) in the app's segmented control, remembered, turning every card without moving the page. The tray's card has a thin line of its first window's current cycle. GET /v1/magpie/quotas/history (?days=, ?provider=, ?user=; local or LAN with the key, as /v1/magpie/quotas) and /api/usage/quotas/history serve it; /v1/magpie/quotas and
magpie quota stay snapshots. Computers sharing usage (WebDAV/S3, Settings › Sync › usage) put their history beside their days, sealed, as <id>.magpie-quotas, and merge the others': the union by account, window and time, two points within 30 s the newer, nothing twice however often they sync; an older magpie never reads the file. TestQuotaHistoryRecords, TestQuotaHistoryCycles, TestMergeQuotaHistory, TestQuotasHistory and TestQuotaHistorySharedBetweenComputers cover recording, cycles, pruning, merge, the API and two computers on one WebDAV folder; quota-curve.test.cjs (WebKit, en/zh, light and dark) the curves, the toggle, no curve without readings, and the tray's line (Chromium doesn't start in this sandbox). go vet, builds (darwin, GOOS=linux, GOOS=windows) and go test -tags nogui -count=1 ./... under a temp HOME pass. Not tried with real accounts; pay-as-you-go balances aren't drawn.
usage: quota cards draw what was left over time, against an even burn, and the tray card its cycle's line (#651, songlairui) — each reading magpie makes anyway of a subscription's windows (and of a plan bought with a key) is kept as a point in quota-history.json beside the config: provider, account, window, when read, percent left, the window's start and reset — no token, no prompt. A failed reading, one kept from before (As of), an unlimited window and a model family's aren't kept; Antigravity's pools are kept by pool. Within a cycle points are 5 minutes apart at least and a run of one figure is its two ends; a cycle ends where the window starts again (its reset passed, or moved further than the time between two readings, so a rolling window's isn't one). 45 days and 6000 points a window are kept. The Usage page's card has, under its meters, a line a window (several tiers of one account on one chart), broken at each reset, a dashed even burn from each window's start to its reset and an upright now line, with 2 days / Cycle (the account's longest current window, start to reset) in the app's segmented control, remembered, turning every card without moving the page. The tray's card has a thin line of its first window's current cycle. GET /v1/magpie/quotas/history (?days=, ?provider=, ?user=; local or LAN with the key, as /v1/magpie/quotas) and /api/usage/quotas/history serve it; /v1/magpie/quotas and
magpie quota stay snapshots. Computers sharing usage (WebDAV/S3, Settings › Sync › usage) put their history beside their days, sealed, as <id>.magpie-quotas, and merge the others': the union by account, window and time, two points within 30 s the newer, nothing twice however often they sync; an older magpie never reads the file. TestQuotaHistoryRecords, TestQuotaHistoryCycles, TestMergeQuotaHistory, TestQuotasHistory and TestQuotaHistorySharedBetweenComputers cover recording, cycles, pruning, merge, the API and two computers on one WebDAV folder; quota-curve.test.cjs (WebKit, en/zh, light and dark) the curves, the toggle, no curve without readings, and the tray's line (Chromium doesn't start in this sandbox). go vet, builds (darwin, GOOS=linux, GOOS=windows) and go test -tags nogui -count=1 ./... under a temp HOME pass. Not tried with real accounts; pay-as-you-go balances aren't drawn.
usage: WorkBuddy's credits are said beside their share — on the Routing page, the Usage page and the menu bar, used or left as the share is (#659)
providers: a Remote magpie provider uses another computer's shared magpie, each request translated at most once (Jorben on Discord: 远程 magpie / magpie 接力) — a custom provider pointed at another magpie has one API per address and couldn't know which API each model's own provider serves, so a request was translated on both computers (Chat → Messages here, then again there). magpie's /v1/models now tells each model's native_endpoints (the APIs its provider serves it on; none for a routing group) and its image input, and reads them, with max_output_tokens and supported_reasoning_levels, from a list; the new remote-magpie preset takes the other magpie's address however typed (host:port, /v1, /v1/messages) for all three APIs and lists all its models, its user's choice already. Add sheet tile with the address field and a hint to Settings → Share on local network; CLI: magpie provider add remote-magpie <key> url=http://<ip>:3425. Checked with two sandbox magpies over the LAN address with the key check on and a Messages-only fake upstream: Chat, Messages (streamed), count_tokens, Responses, a routing group and Gemini all reached it as /v1/messages, translated once. TestRemoteMagpieNativeAPI, TestFetchMagpieFields and TestRemoteMagpie fail without the change; remote-magpie.test.cjs (Chromium, WebKit, en/zh) added. go vet, go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui ./... pass but for TestCursorPluginContext, which fails on main (
1ce05f0) as well; click-scroll, old-webkit pass.
remote magpie: another computer makes videos with the video models a shared magpie has, as it draws with its image models (#545: 图像生成能否也提供给局域网中其他终端) — drawing from another computer already worked both ways, a plain client with the gateway's key at its /v1/images/generations and a magpie with it as a Remote magpie (since v0.1.555), but a Remote magpie's videos did not: its list never gave them, and /v1/videos turned away any provider but a Grok subscription. A magpie now asks its Remote magpie's list for them too (X-Magpie-Videomakers), the other lists them marked kind:video (a magpie that doesn't ask isn't given them, as it would take grok-imagine-video for an image model), and they are kept apart from the models to chat or draw with (catalog.Model.Films). A video with one is started at the other's /v1/videos, its id there carried, base64, in this magpie's video id, and asked after and fetched at its /v1/videos/{id} and /content. TestRemoteMagpieVideos fails without the change (no video models listed); TestLANDraws covers images from another computer both ways through lanGuard with a caller key. go vet ./internal/..., go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui of gateway, provider, catalog and imagemcp pass.
routing: a group in a group is routed by its own routing whatever the outer group's, and an account with five hours and no week is ranked by its five hours (#576: 只有五小时额度的账号在智能/重置前用完下是否合理;嵌套路由组时上层路由是否会干预下层) — a group routed other than In order (Smart, In turn, Least used, Weekly pace, Manual) put every account and key of every group in it into one pool and weighed them together, so A (Smart) holding A-1 and A-2 (each In order) ranked all five of their accounts and keys by Smart and A-1's and A-2's own In order did nothing; only an outer In order let each inner group route itself. planLevel now plans each group in the group by its own routing, whatever the outer's, and weighs it as one beside the outer group's own models' keys, by the first of its own order that isn't resting; where that lands, the inner group is tried whole in its own order, the trace in the same order. As for the five hours: Smart already ranked such an account (Claude Enterprise) by its five hours, the biggest window it has, so ahead of every week renewing later, but a five hours not started (no reset told) counted as a reset not known and went after every week; Allowance.Renewal now has a window not started that says its span renew that long from now, which Smart sorts by (the trace's renews unchanged). Weekly pace took an account with no week as a fresh week with the share used (100/168 per hour), so a week with 80% left and five days to go went ahead of an Enterprise account whose five hours were being lost; it now goes by what its five hours have left per hour until they renew, the whole span when not started, except where the reading may leave a week out — a Claude account only heard of from Claude Code's rate_limit_event, never read whole by /usage (QuotaWindow.partial, claudeUsageEntry.whole) — which keeps the fresh week. The group editor's Smart and Weekly pace hints and the trace's say how an account with five hours and no week is ranked, a hint under Routing says a group among the models keeps its own routing, and the docs (en, zh) say both. TestGroupInGroupKeepsItsOwnRouting (Outer rotating over two In order groups: b c d a without the change, c d a b with it), TestSmartFiveHoursAlone (a five hours not started went after a week in two days), TestFiveHoursAlone, TestClaudeHeardOnlyIsPartial; TestWeeklyPace and TestAllowancePace now pin the five hours alone. settings-groups, group-member-fast, group-found-off, routing-idle-frame 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, TestProvidersAnswerWhileListsComeIn, TestUserPath, TestClaudeSignedOut flaked; the last passes 3/3 alone).
usage: WorkBuddy's credits are said beside their share — on the Routing page, the Usage page and the menu bar, used or left as the share is (#659)
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.
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.
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.
usage: WorkBuddy's credits are said beside their share — on the Routing page, the Usage page and the menu bar, used or left as the share is (#659)
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.
codex: Ultra on GPT-6.1 Sol wherever it is served, Copilot's too, with Codex's agents taking parts of the task (#656) — Codex offers Ultra on a model whose entry lists "ultra", and on a thread at multi-agent V2 hands parts of the task to agents of its own; its catalog (models-manager/models.json) lists it for gpt-6.1-sol, gpt-6-sol, gpt-6-astra, gpt-5.6-sol and gpt-5.6-terra, not the Lunas, which stop at max. magpie listed Copilot's gpt-6.1-sol only up to max, so Codex had no Ultra for it. Now an entry for one of those models (by the id the vendor serves: openai/…, dated snapshots) that reaches max lists ultra too, in Codex's catalog and /v1/models, and its Codex entry says multi_agent_version v2, as Codex's own does; GPT-6 Luna still stops at max. A ChatGPT account's entry stays as the backend lists it, and a group with a ChatGPT account in it offers ultra but isn't told V2 (its lead's subagent tasks come sealed, #141); the "subagents on any model" setting still stamps v1 on OpenAI's entries. Ultra is no API's level: the gateway now always sends max for it (fitEffort), even to a model whose list says ultra. Subagent tasks with web_search kept (the translated path) were fixed in v0.1.689 (#613); TestCopilotUltraSubagentWithWebSearch now covers it against a stub Copilot: the lead's spawn_agent comes back with plain arguments and encrypted_function_args [], no "encrypted" goes upstream, and the subagent's agent_message reaches Copilot as text. TestCopilotSolOffersUltra (sol has ultra in /v1/models and Codex's catalog with v2, luna not; ultra goes to Copilot as max, with and without web_search) and TestUltraIsSentAsMax fail on the old code; TestUltraOnModelsThatOfferIt and TestUltraEntriesSayV2 cover entries, groups and the catalog. go vet, builds (darwin, GOOS=linux, GOOS=windows) and go test -tags nogui -count=1 ./... under a temp HOME pass. Not tried in the Codex App with a real Copilot account.
codex: Ultra on GPT-6.1 Sol wherever it is served, Copilot's too, with Codex's agents taking parts of the task (#656) — Codex offers Ultra on a model whose entry lists "ultra", and on a thread at multi-agent V2 hands parts of the task to agents of its own; its catalog (models-manager/models.json) lists it for gpt-6.1-sol, gpt-6-sol, gpt-6-astra, gpt-5.6-sol and gpt-5.6-terra, not the Lunas, which stop at max. magpie listed Copilot's gpt-6.1-sol only up to max, so Codex had no Ultra for it. Now an entry for one of those models (by the id the vendor serves: openai/…, dated snapshots) that reaches max lists ultra too, in Codex's catalog and /v1/models, and its Codex entry says multi_agent_version v2, as Codex's own does; GPT-6 Luna still stops at max. A ChatGPT account's entry stays as the backend lists it, and a group with a ChatGPT account in it offers ultra but isn't told V2 (its lead's subagent tasks come sealed, #141); the "subagents on any model" setting still stamps v1 on OpenAI's entries. Ultra is no API's level: the gateway now always sends max for it (fitEffort), even to a model whose list says ultra. Subagent tasks with web_search kept (the translated path) were fixed in v0.1.689 (#613); TestCopilotUltraSubagentWithWebSearch now covers it against a stub Copilot: the lead's spawn_agent comes back with plain arguments and encrypted_function_args [], no "encrypted" goes upstream, and the subagent's agent_message reaches Copilot as text. TestCopilotSolOffersUltra (sol has ultra in /v1/models and Codex's catalog with v2, luna not; ultra goes to Copilot as max, with and without web_search) and TestUltraIsSentAsMax fail on the old code; TestUltraOnModelsThatOfferIt and TestUltraEntriesSayV2 cover entries, groups and the catalog. go vet, builds (darwin, GOOS=linux, GOOS=windows) and go test -tags nogui -count=1 ./... under a temp HOME pass. Not tried in the Codex App with a real Copilot account.
provider: Volcengine Ark (火山方舟) is a preset, its Coding Plan, Agent Plan and pay-as-you-go (tcdw: 提供一个选项能够取消自动给 LLM 提供商地址补全 /v1/ 的默认行为) — volcengine, a first-party vendor with a Plan picker: Coding Plan (default) at https://ark.cn-beijing.volces.com/api/coding/v3 for chat completions and Responses and /api/coding for messages (套餐概览 docs/82379/1925114, 快速开始 1928261; its Codex page 2556056 sets wire_api "responses"); Agent Plan, which tcdw is on, at /api/plan/v3 (chat completions and Responses, its Hermes Agent page 2373743) and /api/plan (其他工具 2373746, Claude Code 2373740), with a key of its own; pay-as-you-go at /api/v3 (兼容 OpenAI SDK 1330626, 创建 Response 1569618), which both plans' pages warn bills apart from the plan — no Anthropic endpoint is documented for it. Its models (ark-code-latest, whichever the console picked, then doubao-seed-evolving, doubao-seed-2.1-pro/-lite, doubao-seed-2.0-mini, minimax-m3, glm-5.3, glm-5.3-flash, deepseek-v4.1-flash, deepseek-v4-flash, deepseek-v4-pro, kimi-k2.7-code, kimi-k2.8-preview, kimi-k3, lowercase as the quick-start page gives them) are shown until a list is fetched; no models.dev catalog is tied to it, as its volcengine one has pay-as-you-go's dated ids and volcengine-coding-plan misses the Agent Plan's. Key from ark.volcengine.com/region:cn-beijing/apikey. Their versioned paths go out as written (previous commit). Icon is lobehub's volcengine-color, as the others are; the import page offers its three plans
warmup: a test for the weekly warm-up waiting for the daily time (#246): held at 08:32 before 09:30, both windows in one request at 09:30, at once with no daily time or with the 5-hour window running; fails on main
workbuddy: an added account is told from the others by its uid, and the one WorkBuddy is signed in to stays once WorkBuddy moves on (#413: 添加已提前登录的 WorkBuddy 账号会顶掉默认第一个账号) — magpie matched a new sign-in to a listed account by its name, the nickname: another account of the same nickname replaced the one listed, or was taken for WorkBuddy's own and let go with "signed in". And WorkBuddy's own account is only read from WorkBuddy's file, so signing WorkBuddy in to the next account (to pick it on WorkBuddy's page from magpie) renamed that row, the last account gone, and the one picked, being WorkBuddy's own, was let go: one account at a time. A WorkBuddy or WorkBuddy AI sign-in is now matched by its uid (one of the same name whose uid isn't known is still taken to be it, #155); another account of the same name is added as "name (end of uid)"; the same account renews its sign-in where it is listed, and the one WorkBuddy is signed in to becomes magpie's own sign-in of it, first as it was, so it stays when WorkBuddy signs in to another. The sign-in says so: "该账号已在列表中,已更新登录" in the app, "is already listed — its sign-in was renewed" from magpie accounts add. On the plugin, the host no longer takes two sign-ins of one account id for one account when their uids differ. No request header or user agent changes. TestWorkBuddyAddSameNicknameAsOwn, TestWorkBuddyAddSameNicknameTwice, TestWorkBuddyAddWhatWorkBuddyIsSignedInTo, TestPluginSameNameOtherUID and signin-again.test.cjs fail without the change. go vet, go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui ./... pass; signin-again, click-scroll, old-webkit pass.
workbuddy: a chat with no system prompt is sent with one (Jerry lee on Discord: 没有 system 消息时报 first message is not system prompt) — WorkBuddy refuses a chat whose first message isn't the system prompt, which scripts and plain chat clients often send; such a chat now starts with a neutral "You are a helpful assistant.", the rest of the body kept as it was, and one that already starts with a system prompt goes as it is. The comment on WorkBuddy AI's client headers no longer says they fixed "unapproved channel": that answer comes from Claude Code's and Codex's own system prompts (#182). Tested on the account's request body, through the provider as signed in
workbuddy: a chat with no system prompt is sent with one (Jerry lee on Discord: 没有 system 消息时报 first message is not system prompt) — WorkBuddy refuses a chat whose first message isn't the system prompt, which scripts and plain chat clients often send; such a chat now starts with a neutral "You are a helpful assistant.", the rest of the body kept as it was, and one that already starts with a system prompt goes as it is. The comment on WorkBuddy AI's client headers no longer says they fixed "unapproved channel": that answer comes from Claude Code's and Codex's own system prompts (#182). Tested on the account's request body, through the provider as signed in
providers: each model's credit rate shows by it in the provider's model list and an agent's model picker, Qoder's 0.5× and WorkBuddy's 0.03×, a discount's price before it struck through (01huadalang on Discord: qcoder 或者 workbuddy 能不能实时抓取或者按一段时间抓取能展示列表模型的积分消耗倍率,这样不用打开软件查看,能选择到便宜的模型) — magpie read each model's price only to tell free from not: Qoder's price_factor (
06584e15) and WorkBuddy's "credits" ("x0.03", wbFreeCredits), so the lists said FREE or nothing, and picking a cheap model meant opening Qoder or WorkBuddy. catalog.Model now keeps the price, Rate (the credits a request costs, as a multiple; 0 is not listed, or Free) and RateWas (the price before a discount running now), and carries both through provider.Entry, agent.Option and the providers page's models. Qoder (and Qoder CN): Rate is the price_factor; RateWas an active promotion's before_promotion_price_factor, else an original_price_factor above the price (Qwen3.8-Flash: free, 0.1× struck through), and a promotion's 0 that isn't free is its price before times its discount_factor. WorkBuddy and WorkBuddy AI (the CodeBuddy plan; both builds read the same /v3/config): Rate is the credits (wbCredits; x0.00 stays FREE, an unreadable or infinite rate is none). A plugin's model gives it as it gives free: rate and rateWas on the model its provider.models hook lists, a number (0.5) or as the vendor writes it ("x0.03", "0.5×"), which host.js reads (rateOf) into plugin.Model.Rate/RateWas and pluginCatalog keeps; the moved Qoder and WorkBuddy users get it once their plugins send it. The badge is small and grey (the context badge's colours, no uppercase), beside FREE where that shows, its title saying what a request costs (每次请求消耗 0.5× 积分;折扣前 / 限时免费,原价). Rates are as of the provider's last list: a built-in's is fetched at start-up when it has none, on Refresh and when its key changes, a plugin's when its plugins are asked again (start-up, a sign-in, a plugin change); nothing asks on a timer. Kiro, Cursor, ZCode and Devin list no per-model rate magpie reads, so they show none. TestParseModelsRate (qoder) and TestWBCreditsRate (provider) are new and don't build without the change; TestFakePlugin now checks a plugin's rate given as a number and as "x0.03" (0 for both with host.js not reading them). model-rate.test.cjs (WebKit, en and zh: the chips' badges and titles, struck-through prices, grey not green, no stripe, a click picks and scrolls nothing, the picker's badges) fails without the change and passes with it; click-scroll, models-free-only, model-pick and favorite-star pass in WebKit. No request was made to Qoder, WorkBuddy or any vendor. go vet ./internal/..., go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui -count=1 ./... under a temp HOME, go test ./internal/gui pass.
providers: each model's credit rate shows by it in the provider's model list and an agent's model picker, Qoder's 0.5× and WorkBuddy's 0.03×, a discount's price before it struck through (01huadalang on Discord: qcoder 或者 workbuddy 能不能实时抓取或者按一段时间抓取能展示列表模型的积分消耗倍率,这样不用打开软件查看,能选择到便宜的模型) — magpie read each model's price only to tell free from not: Qoder's price_factor (
06584e15) and WorkBuddy's "credits" ("x0.03", wbFreeCredits), so the lists said FREE or nothing, and picking a cheap model meant opening Qoder or WorkBuddy. catalog.Model now keeps the price, Rate (the credits a request costs, as a multiple; 0 is not listed, or Free) and RateWas (the price before a discount running now), and carries both through provider.Entry, agent.Option and the providers page's models. Qoder (and Qoder CN): Rate is the price_factor; RateWas an active promotion's before_promotion_price_factor, else an original_price_factor above the price (Qwen3.8-Flash: free, 0.1× struck through), and a promotion's 0 that isn't free is its price before times its discount_factor. WorkBuddy and WorkBuddy AI (the CodeBuddy plan; both builds read the same /v3/config): Rate is the credits (wbCredits; x0.00 stays FREE, an unreadable or infinite rate is none). A plugin's model gives it as it gives free: rate and rateWas on the model its provider.models hook lists, a number (0.5) or as the vendor writes it ("x0.03", "0.5×"), which host.js reads (rateOf) into plugin.Model.Rate/RateWas and pluginCatalog keeps; the moved Qoder and WorkBuddy users get it once their plugins send it. The badge is small and grey (the context badge's colours, no uppercase), beside FREE where that shows, its title saying what a request costs (每次请求消耗 0.5× 积分;折扣前 / 限时免费,原价). Rates are as of the provider's last list: a built-in's is fetched at start-up when it has none, on Refresh and when its key changes, a plugin's when its plugins are asked again (start-up, a sign-in, a plugin change); nothing asks on a timer. Kiro, Cursor, ZCode and Devin list no per-model rate magpie reads, so they show none. TestParseModelsRate (qoder) and TestWBCreditsRate (provider) are new and don't build without the change; TestFakePlugin now checks a plugin's rate given as a number and as "x0.03" (0 for both with host.js not reading them). model-rate.test.cjs (WebKit, en and zh: the chips' badges and titles, struck-through prices, grey not green, no stripe, a click picks and scrolls nothing, the picker's badges) fails without the change and passes with it; click-scroll, models-free-only, model-pick and favorite-star pass in WebKit. No request was made to Qoder, WorkBuddy or any vendor. go vet ./internal/..., go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui -count=1 ./... under a temp HOME, go test ./internal/gui pass.
workbuddy: its "Illegal API invocation from an unapproved channel" says what to do (QuentinHsu on Discord, deepseek-v4-flash on WorkBuddy from Codex) — WorkBuddy, both builds, refuses a chat whose system prompt is Codex's or Claude Code's (#182), whatever the headers, and magpie does not rewrite that prompt. The refusal now carries "WorkBuddy refuses chats from Codex and Claude Code (their system prompt); use it from Hermes, OpenCode or Pi, or add another provider to this group", to the agent and on the Routing page (apart from the vendor's words, in the page's language); a group still passes the request on to its next member. workbuddy_refused_test (provider, gateway) and routing-wb-refused.test.cjs (Chromium, WebKit, en/zh) fail without the change. go vet, go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui ./... pass; click-scroll, old-webkit and the routing tests pass.
usage: WorkBuddy's credits are said beside their share — on the Routing page, the Usage page and the menu bar, used or left as the share is (#659)
zcode: a Start Plan request goes as the ZCode app sends it, its system prompt and headers (#425; the user allowed mimicking ZCode's client, for this vendor only) — zcode.z.ai turns a Start Plan (体验套餐) request without ZCode's own system prompt away with 405 "request has been blocked due to unusual activity", code 3012, as zcode2api found (a137460387/zcode2api src/upstream/system-prompt.js, zcode-plan.js; dengyie/zcode2api app/body_transform.py), and magpie sent the agent's request as it was. A Start Plan /v1/messages request now carries ZCode 3.11's three system blocks, each cached (its opening line; its identity, harness and desktop context; "\n\n" and its dynamic sections around an environment section that leaves the working directory and OS version unknown), the agent's own system after them, and the day in a <system-reminder> before the first user turn unless one is there; ZCode's blocks take three of the four cache breakpoints, so the agent's system and tools give up theirs and only its last in the messages is kept. Model, max_tokens and the rest of the body go as sent (numbers as written). Its headers are ZCode's as zcode2api's headers.js has them: User-Agent ZCode/3.14.3 ai-sdk/anthropic/3.0.81, X-ZCode-Agent glm, X-Release-Channel production, X-Os-Category, X-Client-Language and X-Client-Timezone from the machine, a fresh X-Request-Id and X-ZCode-Trace-Id, X-ZCode-Session-Type main, no X-Session-Id or X-Query-Id, beside the X-Title, X-Platform, HTTP-Referer and X-Device-Mid sent before. The GLM Coding Plan and team seats are unchanged. The 405 hint no longer says magpie won't pretend to be ZCode: a block that still happens is the network blocking the IP or ZCode checking something new. No ZCode account was signed in; TestZCodeStartSentAsTheApp (httptest) fails without the change. go vet, go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui ./... pass; routing-zcode-blocked, click-scroll, old-webkit pass.
zcode: a Start Plan request goes as the OpenCode plugin ARNO sent sends it, the one zcode.z.ai still serves (ARNO on Discord: "0.1.633版本还是不行,我给的插件还是成功的") —
11be3f31 followed ZCode 3.14.3's CLI source and some Start Plan requests were still turned away with 405 / code 3012 "request has been blocked due to unusual activity", while ARNO's plugin ("Freeflow for OpenCode and magpie", provider zcode-start), which ARNO sent us, works on the same machine and account. Where the plugin and 11be3f31 differ, the plugin now wins: the stable system block is the identity section and then the "# ZCode Desktop Context" section joined by "\n\n" (11be3f31 had dropped the desktop section; it is back from 31568cb0's text as zcode_prompt.json's desktop); the powered-by line names zai-api / bigmodel-api (was account:zai-start-plan / account:bigmodel-start-plan); the environment's working directory is the one the agent's own prompt names (Claude Code's "Primary working directory:", Codex's <cwd>), else the home directory (was "unknown"), and its shell is the basename of $SHELL or %ComSpec% (cmd.exe on Windows, was "cmd"); the date reminder is a user turn of its own before the agent's turns, always, its tags hugging the text ("<system-reminder>As you answer…task.</system-reminder>", no newlines; was put into the first user turn with newlines, and left out when that turn already had a <system-reminder>, as Claude Code's always does); every cache mark in the turns goes and the last block of the last turn gets one (a string turn becomes a text block), where the agent's last mark used to be kept wherever it was; the agent's system goes as plain text blocks (non-text blocks and empty text dropped); metadata.user_id is {"device_id":<this machine's device id>,"account_uuid":"","session_id":""} as JSON, the rest of metadata kept; X-Os-Version is "<platform> <release> <arch>" (was the release alone); X-Client-Timezone is always sent (TZ, /etc/localtime, else a zone at the local offset, Asia/Shanghai at +8); X-Client-Language falls back to the Mac's AppleLocale or Windows' display language, then en-US as Node's Intl does, so magpie opened from Finder with no LANG no longer says en-US for a zh-CN user; HTTP-Referer is https://zcode.z.ai as written; anthropic-version is 2023-06-01 and the agent's anthropic-beta (claude-code-20250219…) is no longer passed on. Unchanged and already the plugin's: the opening line block, the three cached blocks, User-Agent ZCode/3.14.3 ai-sdk/anthropic/3.0.81, X-ZCode-App-Version, X-Title Z Code@cli, X-ZCode-Agent glm, X-Release-Channel production, X-Platform, X-Os-Category, fresh x-request-id and x-zcode-trace-id, x-zcode-session-type main, the JWT as a Bearer, no x-api-key or X-Device-Mid, tools without cache marks. The prompt texts' SHA-256 (first 8 bytes) all match the plugin's: cliPrefix 46dd360a22c87a92, stableSections[0] 3f21ff9a88a03a76, [1] 39a1e86c93452c01, joined 49bda31511fc9670, beforeEnvironment bbdc66b399d297ff, afterEnvironment 1732b7f098a925d7. The agent's thinking and other body fields go as sent. No request was made to zcode.z.ai. TestZCodeStartPromptIsThePlugins (the hashes) and TestZCodeStartSentAsTheApp (headers, the exact environment section, the context prefix turn, the cache marks, metadata.user_id, BigModel's provider, a string last turn) fail without the change. go vet ./internal/..., go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui ./... pass.
zcode: a Start Plan request goes as the ZCode CLI sends it, not the desktop app (miaopasi and ARNO on Discord: Start plan还是405 感觉不是IP的问题; an OpenCode plugin sending the CLI's request works on the same plan) — dressed as the desktop app, some Start Plan requests were still turned away with 405 "request has been blocked due to unusual activity", code 3012. Following ZCode 3.14.3's own source (github.com/zai-org/ZCode, apps/zcode-cli/packages/core/src/context/builder.ts and sections/, bootstrap/src/model-config.ts and runtime-platform-headers.ts, adapters/src/model/runner-attribution.ts), the request is now the CLI's: X-Title "Z Code@cli" (was "Z Code@electron", which only the desktop app's agent server sends); the stable system block is the identity section alone, the "ZCode Desktop Context" section being added only for presentationSurface zcode_desktop; X-Platform is <platform>-<arch> and the new X-Os-Version is os.release(), as runtime-platform-headers sets them (Darwin's uname release, Windows' Major.Minor.Build from RtlGetVersion); the environment block's OS Version is "<platform> <release> <arch>" as env-info.ts writes it (was "unknown") and its powered-by line is the provider id and model as ZCode names them (account:zai-start-plan/GLM-5.3-Flash, account:bigmodel-start-plan on BigModel; was lowercased without the provider); the currentDate reminder is wrapped as system-reminder/source.ts wraps a context prefix (a newline after the opening tag, one after the closing). Model requests no longer carry X-Device-Mid (ZCode sends it only to its own API, where the balance still needs it, #282) nor x-api-key: the JWT goes as a Bearer only, as the plugin that works sends it (the official client passes it as the SDK's apiKey too, so this follows the working plugin over the source). The prompt text itself was checked byte for byte against the 3.14.3 sections (rendered with node). The plugin's own source wasn't found (pi-freeflow has no zcode code), so where it and the source might differ (X-Os-Version as release alone, not "<platform> <release> <arch>") the source wins. No request was made to zcode.z.ai. TestZCodeStartSentAsTheApp, TestZCodeStartPlanOwnAccount and TestZCodeSignInStartPlanDeviceMid are updated; with the old prompt, X-Title and x-api-key they fail (X-Title "Z Code@electron", X-Api-Key sent, the desktop section in the system prompt). go vet ./internal/..., go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui ./internal/provider/ ./internal/gateway/ pass.
zcode: a Start Plan request goes as the OpenCode plugin ARNO sent sends it, the one zcode.z.ai still serves (ARNO on Discord: "0.1.633版本还是不行,我给的插件还是成功的") —
11be3f31 followed ZCode 3.14.3's CLI source and some Start Plan requests were still turned away with 405 / code 3012 "request has been blocked due to unusual activity", while ARNO's plugin ("Freeflow for OpenCode and magpie", provider zcode-start), which ARNO sent us, works on the same machine and account. Where the plugin and 11be3f31 differ, the plugin now wins: the stable system block is the identity section and then the "# ZCode Desktop Context" section joined by "\n\n" (11be3f31 had dropped the desktop section; it is back from 31568cb0's text as zcode_prompt.json's desktop); the powered-by line names zai-api / bigmodel-api (was account:zai-start-plan / account:bigmodel-start-plan); the environment's working directory is the one the agent's own prompt names (Claude Code's "Primary working directory:", Codex's <cwd>), else the home directory (was "unknown"), and its shell is the basename of $SHELL or %ComSpec% (cmd.exe on Windows, was "cmd"); the date reminder is a user turn of its own before the agent's turns, always, its tags hugging the text ("<system-reminder>As you answer…task.</system-reminder>", no newlines; was put into the first user turn with newlines, and left out when that turn already had a <system-reminder>, as Claude Code's always does); every cache mark in the turns goes and the last block of the last turn gets one (a string turn becomes a text block), where the agent's last mark used to be kept wherever it was; the agent's system goes as plain text blocks (non-text blocks and empty text dropped); metadata.user_id is {"device_id":<this machine's device id>,"account_uuid":"","session_id":""} as JSON, the rest of metadata kept; X-Os-Version is "<platform> <release> <arch>" (was the release alone); X-Client-Timezone is always sent (TZ, /etc/localtime, else a zone at the local offset, Asia/Shanghai at +8); X-Client-Language falls back to the Mac's AppleLocale or Windows' display language, then en-US as Node's Intl does, so magpie opened from Finder with no LANG no longer says en-US for a zh-CN user; HTTP-Referer is https://zcode.z.ai as written; anthropic-version is 2023-06-01 and the agent's anthropic-beta (claude-code-20250219…) is no longer passed on. Unchanged and already the plugin's: the opening line block, the three cached blocks, User-Agent ZCode/3.14.3 ai-sdk/anthropic/3.0.81, X-ZCode-App-Version, X-Title Z Code@cli, X-ZCode-Agent glm, X-Release-Channel production, X-Platform, X-Os-Category, fresh x-request-id and x-zcode-trace-id, x-zcode-session-type main, the JWT as a Bearer, no x-api-key or X-Device-Mid, tools without cache marks. The prompt texts' SHA-256 (first 8 bytes) all match the plugin's: cliPrefix 46dd360a22c87a92, stableSections[0] 3f21ff9a88a03a76, [1] 39a1e86c93452c01, joined 49bda31511fc9670, beforeEnvironment bbdc66b399d297ff, afterEnvironment 1732b7f098a925d7. The agent's thinking and other body fields go as sent. No request was made to zcode.z.ai. TestZCodeStartPromptIsThePlugins (the hashes) and TestZCodeStartSentAsTheApp (headers, the exact environment section, the context prefix turn, the cache marks, metadata.user_id, BigModel's provider, a string last turn) fail without the change. go vet ./internal/..., go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui ./... pass.
zcode: an old plan with no monthly cap is no longer taken as used up (#366) — the quota reply's TIME_LIMIT is the month's MCP tool calls, which ZCode only shows and never stops the models on, but magpie read it as the account's monthly allowance, so an old plan reporting it at 100% had its ZCode models skipped with the 5 hours full. It is now shown as MCP · Month and set aside, as a pasted Zhipu key's already was, and a limit whose whole is 0 (no cap) counts as unused. TestZCodeNoMonthlyCap fails without the change. go vet, go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui ./... pass (the Claude account tests pass alone).
providers: a Zhipu or Z.ai key on a team's GLM Coding Plan can be given the team's organization and project IDs, and the Usage page then shows the team's 5-hour and weekly windows (#236 EZN7L2C3: the IDs are in the BigModel console; CC Switch asks the team quota with them) — a key pasted alone was asked for the team's quota (type=2) without the Bigmodel-Organization / Bigmodel-Project headers, which magpie can't find from a key, so a team key showed nothing. providers.json keeps zhipuTeam {org, project} for those key providers (trimmed; both empty removes it; a save without it keeps it, {} clears it; carried through from the Zhipu and Z.ai presets). The team query sends the typed IDs, else those of a ZCode team login holding the key, and is now asked at the key's own host (open.bigmodel.cn, as CC Switch asks it) first and bigmodel.cn (where ZCode asks it) only when that gives no windows. It still runs only when the key's personal quota fails or has no windows. The provider editor shows Team org ID / Team project ID (团队组织 ID / 团队项目 ID), optional, for Zhipu and Z.ai key providers alone, adding and editing; one without the other is refused before anything is posted. Tested with internal/provider/zhipu_team_key_test.go against a fake open.bigmodel.cn / bigmodel.cn in a temp HOME: the IDs kept trimmed, the team asked with type=2, the key and both headers at open.bigmodel.cn and read as 5 hours 42% and weekly 10%, a key without them asked with no team headers at both hosts and showing nothing, a pay-as-you-go key with blank IDs no card, Z.ai taking the fields and DeepSeek not. internal/gui/tests/zhipu-team.test.cjs, Chromium and WebKit, en and zh: the fields for a saved Zhipu provider and from the add sheet for Z.ai, none for DeepSeek or a relay, trimmed values posted, one alone refused, both empty posting {}, nothing scrolled on a click. Both fail without the change. click-scroll, old-webkit, provider-proxy and account-proxy pass; go vet, go build -tags nogui (and GOOS=linux, GOOS=windows) and go test -tags nogui ./... pass. Not tried with a real team key or in the real Wails window; the team's reset counts are not read for a key (the reset list is asked only with a ZCode sign-in token).
providers: DimAgent's subscription is taken out of magpie (a DimAgent team member on X: dim doesn't allow its subscription used through a proxy, and users doing so get banned or lose their discount) — #226 signed in to DimAgent as its desktop client does and served its models through the gateway. The sign-in, the account provider and its dimagent/<model> models, the quota card,
magpie accounts … dimagent and the icon are gone, with internal/dimagent. An account signed in before is left out when logins.json is read and dropped from the file at its next write, so its tokens aren't kept. The pasted-callback field and route stay, generic, with no sign-in using them. Tested with TestDimAgentLoginDropped (fails without the filter); in a sandbox HOME with a DimAgent login left in logins.json, magpie accounts --json lists nothing and magpie accounts add dimagent is refused. signin-callback (fixture renamed), old-webkit and click-scroll pass; go vet, go build (GUI, -tags nogui, GOOS=linux, GOOS=windows) and go test -tags nogui ./... pass (TestUserPath and the Claude/Devin sign-in tests flaked under the full run and pass alone).
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)