Files
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.
provider: Antigravity's models at several levels are one model with those levels (#150: 统一 Antigravity 模型变体与客户端推理档位的映射) — Antigravity lists gemini-3.7-flash-low, -medium and -high as three models, "Gemini 3.7 Flash (Low)" …, so a client picked a level twice: once in the model, once as its reasoning effort, with no telling which won. Now ids that are one id with an effort word after it (-low, -medium, -high, -extra-low, -minimal, -xhigh) are one model, gemini-3.7-flash "Gemini 3.7 Flash", declaring only the levels there are (low,medium,high; Gemini 3.1 Pro low,high), so Codex, Pi and the rest get reasoning levels for it. The level a variant is at is what Antigravity's name says where each name says a different one (gemini-3.5-flash-extra-low is "(Low)", gemini-3.5-flash-low "(Medium)"), else its id's word; a model only named like another (gemini-3-flash-agent, "Gemini 3.5 Flash (High)"; gemini-pro-agent) or with no sibling (gpt-oss-120b-medium) stays as listed. A request asks for the variant the effort picks, an effort it lacks at the nearest level (xhigh is high; medium on low/high is high); with none asked, the id without a level if Antigravity has one, else high — what Gemini 3 does unasked — else the highest. A variant's request carries no thinkingLevel, so the id and the level can't disagree. An old variant id still works: as named with no effort, and moved to the effort asked for. Saved picks and agents set to a variant (antigravity/gemini-3.7-flash-high) are moved to the family, at that level where the agent's own is unset, as Cursor's are. For Gemini 3 models without variants, medium now goes as medium to Flash (which takes minimal/low/medium/high) and still as high to Pro (low/high). Tested against the list an account's fetchAvailableModels gave (sandbox cache)
provider: Antigravity's models at several levels are one model with those levels (#150: 统一 Antigravity 模型变体与客户端推理档位的映射) — Antigravity lists gemini-3.7-flash-low, -medium and -high as three models, "Gemini 3.7 Flash (Low)" …, so a client picked a level twice: once in the model, once as its reasoning effort, with no telling which won. Now ids that are one id with an effort word after it (-low, -medium, -high, -extra-low, -minimal, -xhigh) are one model, gemini-3.7-flash "Gemini 3.7 Flash", declaring only the levels there are (low,medium,high; Gemini 3.1 Pro low,high), so Codex, Pi and the rest get reasoning levels for it. The level a variant is at is what Antigravity's name says where each name says a different one (gemini-3.5-flash-extra-low is "(Low)", gemini-3.5-flash-low "(Medium)"), else its id's word; a model only named like another (gemini-3-flash-agent, "Gemini 3.5 Flash (High)"; gemini-pro-agent) or with no sibling (gpt-oss-120b-medium) stays as listed. A request asks for the variant the effort picks, an effort it lacks at the nearest level (xhigh is high; medium on low/high is high); with none asked, the id without a level if Antigravity has one, else high — what Gemini 3 does unasked — else the highest. A variant's request carries no thinkingLevel, so the id and the level can't disagree. An old variant id still works: as named with no effort, and moved to the effort asked for. Saved picks and agents set to a variant (antigravity/gemini-3.7-flash-high) are moved to the family, at that level where the agent's own is unset, as Cursor's are. For Gemini 3 models without variants, medium now goes as medium to Flash (which takes minimal/low/medium/high) and still as high to Pro (low/high). Tested against the list an account's fetchAvailableModels gave (sandbox cache)
sync: magpie's setup kept the same on every computer through WebDAV (坚果云, Nextcloud, a NAS…) — Settings has Sync and backup: the providers with their keys, groups and icons, the settings, the profiles and each agent's models go to the server as one file sealed with a passphrase, so the server never sees them; the magpie serving the gateway looks every three minutes, sends what changed here and brings in what changed elsewhere, a removal too; a part changed on both sides goes to the newer one, and the copy it replaced is kept in the sync folder with a note saying what was replaced; the proxy and the window size stay each computer's own, and a computer set not to send keys never strips them from the server; Export and Import carry the same file by hand (#28)
providers: a provider can be duplicated, the signed-in account paused, and a Balance URL names each key with {key} (#268, #263, #266)
providers: Codex on Amazon Bedrock's GPT models goes to the runtime's Responses API (#176: Amazon Bedrock: Function tools with reasoning_effort are not supported for global.openai.gpt-6-luna in /v1/chat/completions. To use function tools, use /v1/responses or set reasoning_effort to 'none'.) — the preset gave Bedrock chat completions alone for every model not Claude, so Codex's Responses, tools and reasoning in it, were translated to chat, which the runtime refuses for its GPT models. bedrock-runtime serves the Responses API at the same /openai/v1 with the same Bedrock API key as a Bearer, for OpenAI's closed GPT models (not gpt-oss, which stays on chat): the preset and each region now have it, a Bedrock provider saved before is given it where its chat is, and a GPT model is asked there first (Codex's request relayed as it came; a chat client's taken there once chat refuses it). A chat endpoint with no Responses to go to that refuses tools with reasoning so is asked again with reasoning_effort "none". Tested with a fake runtime returning that 400 on chat and serving /openai/v1/responses (Codex and a chat client with tools, a provider saved without Responses, gpt-oss still on chat) and a chat-only relay refusing the same (retried at none); both fail on main. Not tested against a real Bedrock account
windows: the desktop app no longer opens a console window for every command it runs (#54) — it is linked as a GUI program and has no console, so each console CLI it started (devin auth status and models list, devin acp for every Devin turn, claude, codex, cursor-agent and grok probes and runs, grok login) was given a new console by Windows, and a Windows Terminal window with it, some every few seconds while the app sat idle. Every command magpie runs is now made through internal/proc, which starts it with CREATE_NO_WINDOW when magpie has no console of its own: the child gets a console without a window, and its output, which magpie always pipes back, arrives as before. A magpie run in a terminal (magpie serve, the TUI, the desktop app started from one with a command) has one, and its children share it as they did, so Ctrl+C there still reaches them. The relaunch after an update keeps its DETACHED_PROCESS, added to rather than replacing the flags it was made with, and a test fails on any command made with os/exec directly, so a new one can't bring the windows back
provider: a Claude account's Fable week is read and routed on (#139) — /api/oauth/usage gives a week's allowance per model only in its limits[] array, which readClaudeWindows didn't parse, so magpie accounts, magpie quota and the app never showed Fable's, often the first to run out on Max, and routing went on sending Fable requests to an account whose Fable week was used up. Each weekly_scoped item with a scope.model.display_name is now a window "7 days · <name>", counted in percent, renewing at resets_at, a week long and counting only its model (the name lowercased, spaces and dots as dashes, so "Fable" counts every claude-fable-*), which Allowance.For and Full already weigh against that model alone, as they do the Opus and Sonnet weeks; one without a scope (seven_day again) or one for a model seven_day_opus/seven_day_sonnet already told is left out. Claude Code's rate_limit_event names the same window seven_day_overage_included, its "Fable limit" (in 2.1.283's bundle, for its allowlist Fable, Fable 5, Fable 5.1); claudeKinds takes it too, so the Fable week stays known while the usage endpoint turns magpie away. Reported by Cyb3rN8
provider: a Claude account's Fable week is read and routed on (#139) — /api/oauth/usage gives a week's allowance per model only in its limits[] array, which readClaudeWindows didn't parse, so magpie accounts, magpie quota and the app never showed Fable's, often the first to run out on Max, and routing went on sending Fable requests to an account whose Fable week was used up. Each weekly_scoped item with a scope.model.display_name is now a window "7 days · <name>", counted in percent, renewing at resets_at, a week long and counting only its model (the name lowercased, spaces and dots as dashes, so "Fable" counts every claude-fable-*), which Allowance.For and Full already weigh against that model alone, as they do the Opus and Sonnet weeks; one without a scope (seven_day again) or one for a model seven_day_opus/seven_day_sonnet already told is left out. Claude Code's rate_limit_event names the same window seven_day_overage_included, its "Fable limit" (in 2.1.283's bundle, for its allowlist Fable, Fable 5, Fable 5.1); claudeKinds takes it too, so the Fable week stays known while the usage endpoint turns magpie away. Reported by Cyb3rN8
claude: the signed-in account is one account, and one out of quota hands over to the next (#177) — the account Claude Code is signed in to was named one way as a provider and another as a saved login, so it was served twice and its limits weren't found under its name; and
claude auth status was asked with magpie's own ANTHROPIC_BASE_URL/token in the environment, telling of those rather than the sign-in. Now both go by ~/.claude.json's email (with the org), the CLI's email when that has none, and the status is asked without magpie's wiring. A turn the CLI ends with an error result (out of quota, rate limited) ends the reply there instead of waiting on, and a non-streamed one answers 429 for quota words, so another account takes over as in a stream. Tested: the account named as its login, the quota result ending the reply, another account taking over
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: 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.
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.
providers: Claude Code is signed in to its next account once the one it is on is spent, as Codex is, and an account keeps its own affinity, backoff and usage across a switch (#208 Cyb3rN8: Claude Code 用自己的模型时,登录账号额度用满不会自动切换 · #209: Smart 选中的账号和 Claude Code 登录账号互不同步…切换登录账号还会把按 id 记的状态串到另一个账号上) — Claude Code on its own model asks Anthropic as the account it is signed in to, never through magpie, so with A spent its sessions got "You've hit your session limit" while the gateway had moved on to B. The Codex watcher (KeepCodexOnAnAccountWithRoom, now KeepOnAnAccountWithRoom, same place: started by the magpie serving the gateway, a minute in and every 5 minutes) now looks at Claude Code too: when the account it is signed in to is spent and another ticked account's allowance is known and has room, it is signed in to that one through SwitchLogin (credentials and .claude.json's oauthAccount), as Use on the Accounts page does; not when the Claude account was removed from magpie. The rule for #209: the sign-in follows Smart, not the other way round — it moves at the share Smart counts an account spent (provider.SpentShare = 98, which the gateway's usedShare now is), for Codex too (it moved at 100%). The Routing field of a Codex or Claude subscription with several accounts says so: routing picks the account for what comes through magpie; the agent on its own uses the one it is signed in to, moved on at 98%. Sessions already running keep the account they started with until restarted, as SwitchLogin says; routing Claude Code's own model through the gateway (the codexFailover way) is not done here. #209's second part was a real bug: the account the agent is on is the candidate "claude" and the others "claude@user", so a switch handed "claude" from A to B — a conversation A answered was kept on B and traced kept=true, and the Least used tokens and the backoff count followed the id. who(), routed.used and routed.failures are now keyed by restKey (provider@user, as rests already were); rest itself (group members, the trace) is unchanged. Tested with new TestClaudeSwitchedWhenSpent (sandbox HOME, keychain off, a fake usage endpoint: 97% stays, 98% signs Claude Code in to b — credentials and .claude.json — a stays ticked; on b it stays; both spent it stays), TestCodexSwitchedWhenUsedUp still passing, new TestLoginSwitchKeepsEachAccountItself (fails on main: the conversation went to b, kept) and new internal/gui/tests/routing-signin.test.cjs in Chromium and WebKit, en and zh; login-import and routing-kind GUI tests pass. go build -tags nogui (and GOOS=linux, GOOS=windows), go test -tags nogui ./... pass (TestUserPath, TestClaudeAccountNamedAsItsLogin, TestClaudeIdentityServedWhileAsked and TestCodexVersion timed out at 5s under the full run's load and pass on their own). Not tested against real Claude accounts or a running Claude Code.
providers: Claude Code is signed in to its next account once the one it is on is spent, as Codex is, and an account keeps its own affinity, backoff and usage across a switch (#208 Cyb3rN8: Claude Code 用自己的模型时,登录账号额度用满不会自动切换 · #209: Smart 选中的账号和 Claude Code 登录账号互不同步…切换登录账号还会把按 id 记的状态串到另一个账号上) — Claude Code on its own model asks Anthropic as the account it is signed in to, never through magpie, so with A spent its sessions got "You've hit your session limit" while the gateway had moved on to B. The Codex watcher (KeepCodexOnAnAccountWithRoom, now KeepOnAnAccountWithRoom, same place: started by the magpie serving the gateway, a minute in and every 5 minutes) now looks at Claude Code too: when the account it is signed in to is spent and another ticked account's allowance is known and has room, it is signed in to that one through SwitchLogin (credentials and .claude.json's oauthAccount), as Use on the Accounts page does; not when the Claude account was removed from magpie. The rule for #209: the sign-in follows Smart, not the other way round — it moves at the share Smart counts an account spent (provider.SpentShare = 98, which the gateway's usedShare now is), for Codex too (it moved at 100%). The Routing field of a Codex or Claude subscription with several accounts says so: routing picks the account for what comes through magpie; the agent on its own uses the one it is signed in to, moved on at 98%. Sessions already running keep the account they started with until restarted, as SwitchLogin says; routing Claude Code's own model through the gateway (the codexFailover way) is not done here. #209's second part was a real bug: the account the agent is on is the candidate "claude" and the others "claude@user", so a switch handed "claude" from A to B — a conversation A answered was kept on B and traced kept=true, and the Least used tokens and the backoff count followed the id. who(), routed.used and routed.failures are now keyed by restKey (provider@user, as rests already were); rest itself (group members, the trace) is unchanged. Tested with new TestClaudeSwitchedWhenSpent (sandbox HOME, keychain off, a fake usage endpoint: 97% stays, 98% signs Claude Code in to b — credentials and .claude.json — a stays ticked; on b it stays; both spent it stays), TestCodexSwitchedWhenUsedUp still passing, new TestLoginSwitchKeepsEachAccountItself (fails on main: the conversation went to b, kept) and new internal/gui/tests/routing-signin.test.cjs in Chromium and WebKit, en and zh; login-import and routing-kind GUI tests pass. go build -tags nogui (and GOOS=linux, GOOS=windows), go test -tags nogui ./... pass (TestUserPath, TestClaudeAccountNamedAsItsLogin, TestClaudeIdentityServedWhileAsked and TestCodexVersion timed out at 5s under the full run's load and pass on their own). Not tested against real Claude accounts or a running Claude Code.
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.
providers: test that a subscription's context window set in its editor reaches the models agents are told of (#120) — Codex's 272K from the ChatGPT backend gives way to the 872K a user sets on the Codex account, through the account's save
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: 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: 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.
windows: the desktop app no longer opens a console window for every command it runs (#54) — it is linked as a GUI program and has no console, so each console CLI it started (devin auth status and models list, devin acp for every Devin turn, claude, codex, cursor-agent and grok probes and runs, grok login) was given a new console by Windows, and a Windows Terminal window with it, some every few seconds while the app sat idle. Every command magpie runs is now made through internal/proc, which starts it with CREATE_NO_WINDOW when magpie has no console of its own: the child gets a console without a window, and its output, which magpie always pipes back, arrives as before. A magpie run in a terminal (magpie serve, the TUI, the desktop app started from one with a command) has one, and its children share it as they did, so Ctrl+C there still reaches them. The relaunch after an update keeps its DETACHED_PROCESS, added to rather than replacing the flags it was made with, and a test fails on any command made with os/exec directly, so a new one can't bring the windows back
providers: a provider can be duplicated, the signed-in account paused, and a Balance URL names each key with {key} (#268, #263, #266)
cursor: a sign-in link cursor-agent wrapped over lines is joined whole before it is opened (#261: the page said "This sign-in link is incomplete or has expired" for https://cursor.com/loginDeepControl?, the link cut after its "?") — runCLISignIn took the first https:// run of a line, so a link broken where the terminal wrapped lost everything after the break. A Cursor link without challenge and uuid now takes the lines after it that are only URL characters; one that has them takes only more of its query read with it, not a word printed after it; a link never made whole is used as it was once the wait is over. Grok's link is handed on as printed. Not reproduced with cursor-agent 2025.08.08, 2026.09.23 or 2026.09.28, which print it on one line. Tested with TestCursorSignInJoinsAWrappedLink (a break after ?, over three lines, in and after the uuid, the rest 0.3s later, whole then "Waiting...", never whole; fails without the change) and TestCLISignInFirstLinkAsPrinted.
cursor: a sign-in link cursor-agent wrapped over lines is joined whole before it is opened (#261: the page said "This sign-in link is incomplete or has expired" for https://cursor.com/loginDeepControl?, the link cut after its "?") — runCLISignIn took the first https:// run of a line, so a link broken where the terminal wrapped lost everything after the break. A Cursor link without challenge and uuid now takes the lines after it that are only URL characters; one that has them takes only more of its query read with it, not a word printed after it; a link never made whole is used as it was once the wait is over. Grok's link is handed on as printed. Not reproduced with cursor-agent 2025.08.08, 2026.09.23 or 2026.09.28, which print it on one line. Tested with TestCursorSignInJoinsAWrappedLink (a break after ?, over three lines, in and after the uuid, the rest 0.3s later, whole then "Waiting...", never whole; fails without the change) and TestCLISignInFirstLinkAsPrinted.
providers: each provider, a signed-in account like Codex's among them, goes through its own proxy — the one in Settings, none, or its own address (#237 xosi: 目前似乎是全局设置代理的,codex和国模供应商无法单独区分设置代理) — magpie had one proxy, Settings' (or the environment's, or the system's), for every request it made, so Codex, which needs a proxy from China, and a vendor at home, which is slower or refused through one, couldn't both be right. A provider now has a proxy, "" (following the global one, as before and the default), "direct" (none, whatever Settings says) or an http://, https:// or socks5:// address (host:port meaning http), kept as proxy in providers.json only when set, and for an account provider in its picks entry beside its models. It is checked on save by the same rule Settings' proxy is (settings.CheckProxy, now shared), so ftp:// and the like are refused. netproxy.With puts a provider's choice on a request's context and netproxy.Func honours it; netproxy.Dispatch sends such a request through a clone of the transport kept for that proxy alone (at most 32, cleared past that), since a transport shares an HTTP/2 connection by host whatever proxy dialled it; loopback is never proxied. The gateway's client and http.DefaultClient go through Dispatch, and every request made on a provider's behalf carries its choice: the gateway's turns (attempt, forward, draw, decide, the Codex backend), and the model lists, balances, plan and subscription usage, tests, sign-in, Codex's warm-up and reset, while the Claude subscription's and warm-up's CLI runs get the account's proxy in their *_PROXY (netproxy.EnvWith). The provider editor, a key provider's and a signed-in account's alike, has a Proxy row after Fallback: Global proxy | Direct | Custom (全局代理 | 直连 | 自定义), the address field beside the options only for Custom, its room kept otherwise so a pick never changes the centred dialog's height and moves nothing, and a line under it saying what the pick does; Save posts proxy ("" , "direct" or the trimmed address), a save without it keeps the old one, and Custom with no address is refused before anything is posted ("Proxy: type its address, like http://127.0.0.1:7890"). No border stripes; en and zh strings together. Tested with a new internal/gateway/proxy_test.go in a sandbox HOME: with a global proxy set, provider A's turns and model list go through its own fake proxy, B's (direct) straight to the vendor, C's (following) through the global one, none through another, and A set back to following goes through the global one; ftp:// is refused (fails with With neutered). netproxy_test.go's TestWith covers Func, Dispatch's per-proxy transports and EnvWith. A new internal/gui/tests/provider-proxy.test.cjs in Chromium and WebKit, en and zh: a key provider opens on Global proxy with no address, Custom unfilled is refused and nothing posted, a padded socks5 address saved trimmed, Direct saved as "direct" and Global as ""; Codex's account opens on Custom with its address and saves Direct with its models; every pick leaves the option within 1px; no left border. go build -tags nogui (and GOOS=linux, GOOS=windows), go test -tags nogui ./..., provider-proxy, click-scroll, old-webkit, balance-fix and add-sheet pass (click-scroll's WebKit case failed once alongside the others and passes alone, twice). Not tried against a real proxy or the real Wails window.
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: an account with no organization asks Factory's whoami for it before it sends, as droid does, and every new token asks again, so its model requests carry X-Factory-Org-Id (#242 tasselx: "Factory: Forbidden" on GLM-5.2)
factory: an account with no organization asks Factory's whoami for it before it sends, as droid does, and every new token asks again, so its model requests carry X-Factory-Org-Id (#242 tasselx: "Factory: Forbidden" on GLM-5.2)
factory: X-Factory-Org-Id carries Factory's own id for the org, as droid sends it, not WorkOS's, and a refused org is mended once (#242 tasselx: "403 permission_error: Factory: Requested active organization is not accessible by this user") — read again from droid 0.229.0 (still the latest @factory/cli-darwin-arm64): its header builder (Ad) sends X-Factory-Org-Id only from the stored active_organization_id, left off when null; that field is only ever filled with Factory's (Firestore) org ids, whoami's orgId or /api/app/auth/me's memberships, never WorkOS's org_… ids, which go only to WorkOS's refresh; moving a token that names no org into the first of /api/cli/org's workosOrgIds (gt) saves active_organization_id null; routine refresh names no org. magpie sent the WorkOS org id as the header, which Factory looks up as one of its own and refuses. Now factoryCreds keeps Active (activeOrganizationId) from whoami's orgId, asked without an org header, and only it is sent; Org, the WorkOS id, is kept but never sent; logins saved by v0.1.432 have no Active and so send none; refresh names no org. The sign-in checks the JWT's external_org_id then org_id, carries on as droid does when /api/cli/org fails or lists none, and when whoami fails but the token says whose it is. On that 403 an account mends itself once (Account.retry, Provider.Retries/Retry, asked by gateway.forward and by the usage read): an active org sent is dropped and the request goes again without the header; with none sent, the token is put in the first org /api/cli/org lists (droid does this at sign-in, not on a 403). Other 403s and 401s are passed on as they were. Tested in factory_test.go against fake WorkOS and Factory servers that answer droid's way: TestFactoryActiveOrg (a token WorkOS put in org_A keeps fac_A and gets a 200; a v0.1.432-format login sends no header and gets a 200) and TestFactoryOrgRefused (a left active org dropped and retried with no refresh; a left token org refreshed into org_A and retried; the usage read the same; other 403s and 401s not retried) fail on main with the reporter's 403; the existing tests now check whoami and /api/cli/org get no org header and refresh sends no organization_id. go vet ./internal/..., go build -tags nogui (and GOOS=linux, GOOS=windows) and go test -tags nogui ./... pass. Not tried against a real Factory account: that the server reads the header as a Factory org id is from droid's code, not seen on the wire; the gateway's retry is covered only by a provider-level test; accounts with several orgs take the first, with no picker.
factory: an account with no organization asks Factory's whoami for it before it sends, as droid does, and every new token asks again, so its model requests carry X-Factory-Org-Id (#242 tasselx: "Factory: Forbidden" on GLM-5.2)
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: 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
provider: a base URL with a version in its path is used as written, never given a /v1 (tcdw: 提供一个选项能够取消自动给 LLM 提供商地址补全 /v1/ 的默认行为) — a custom provider at Volcengine Ark's Agent Plan, https://ark.cn-beijing.volces.com/api/plan/v3 (its 其他工具 page, docs/82379/2373746), was asked for its list at …/v3/models and then …/v3/v1/models, and only the last failure was shown, so the error named a URL the user never gave. Ark answers both 401 without a valid key (checked unauthenticated: auth comes before routing), so which path it serves can't be told from outside; and had …/v3/v1/models answered, fixV1 would have rewritten the chat base to …/v3/v1 for good. Now an OpenAI-style base with a version segment anywhere in its path (v1, v3, v4, v1beta: Ark's /api/plan/v3 and /api/coding/v3, Zhipu's /api/paas/v4, Gemini's /v1beta/openai) is asked only at base/models and fixV1 leaves it alone; a bare host or an unversioned path (a relay's …/api) is still looked around for the /v1 it may lack, and an Anthropic base still has its /v1/models. A failed list names every URL asked with its status and the vendor's own message ("401 Unauthorized (The API key doesn't exist.)") and says the model ids can be typed in by hand or a models URL given — both already work, the base stays as typed; magpie provider add now prints it instead of saying nothing. No "don't touch my URL" toggle: a versioned URL is never touched, and an unversioned one is only fixed when its /v1 alone answered, when requests at the base as written would miss too. Tested against an httptest server shaped like Ark (/api/plan/v3/chat/completions served, /models 404, a lenient /v3/v1/models) in a sandbox HOME: before, …/v3/v1/models: 404 and, lenient, chat moved to …/v3/v1; after, only …/api/plan/v3/models is asked and the base is kept
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
usage: a Grok id named at a reasoning effort is priced as its model (#224 Aimer779: calls made with Grok Build show no cost on the Usage page) — the Usage page, the Requests ledger and the Sessions page price a model by its models.dev id, and xAI lists grok-4.7 and grok-4.6 ($2 / $6 / $0.5 cache read per million in the cached catalog) but not the ids Grok's models are spelled at an effort: grok-4.7-low, -medium, -high, -xhigh (Cursor's ids, and grok-4.7 at the levels Grok's own model list gives it). Such an id is the same model at another setting, so when its own id has no price it is now looked up as the model it is an effort of (provider.pricedNames: grok-<version>[-…]-minimal|low|medium|high|xhigh|extra-high, with or without a path, any case), in Provider.ListPrice and MakerPrice (the gateway's ledger and summary) and in the Sessions page's priceOf (through provider.PricedName). An id priced by its own name keeps that price; what is recorded is unchanged. Only Grok's ids are read this way, and -max is not stripped. Left unpriced, as no catalog in use lists them and no price is made up: fast ids (grok-4.7-fast, grok-4.7-low-fast, … which cursor_models.go treats as models of their own), grok-4.7-build and grok-4.7-build-fast (Grok Build's own models, from its model list and sessions) and grok-4.7-mini; they keep the + as before. Tested with a new TestGrokEffortListPrice (MakerPrice and the Grok account's ListPrice price grok-4.7 at each effort, in upper case and with x-ai/, and grok-4.6-minimal, at grok's price; the fast, build, mini and -max ids, gpt-6-astra-high and cursor-grok-4.7-high stay unpriced) and TestPriceOfGrokEffort in internal/sessions (the same for a session's models, grok-4.7-build, grok-4.7-mini and the fast ones unpriced); TestSubscriptionListPrice unchanged. go build -tags nogui (and GOOS=linux, GOOS=windows), go test -tags nogui ./... pass (TestUserPath, TestClaudeSignedOut, TestClaudeAccountNamedAsItsLogin, TestClaudeIdentityServedWhileAsked and TestCodexVersion hit their 5s waits in the full run and pass alone). Not tried against the reporter's usage log or in the GUI.
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
cursor: a sign-in link cursor-agent wrapped over lines is joined whole before it is opened (#261: the page said "This sign-in link is incomplete or has expired" for https://cursor.com/loginDeepControl?, the link cut after its "?") — runCLISignIn took the first https:// run of a line, so a link broken where the terminal wrapped lost everything after the break. A Cursor link without challenge and uuid now takes the lines after it that are only URL characters; one that has them takes only more of its query read with it, not a word printed after it; a link never made whole is used as it was once the wait is over. Grok's link is handed on as printed. Not reproduced with cursor-agent 2025.08.08, 2026.09.23 or 2026.09.28, which print it on one line. Tested with TestCursorSignInJoinsAWrappedLink (a break after ?, over three lines, in and after the uuid, the rest 0.3s later, whole then "Waiting...", never whole; fails without the change) and TestCLISignInFirstLinkAsPrinted.
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 may be a model at an effort of its own, provider/model:low (#189, @luw2007: model + effort 才是完整的组合项) — a member was only a model, reasoning as the agent asked or as the group's classifier picked, so "the same model, cheap and quick" and "the same model, thinking hard" could not both be in a group, nor could a rule send to one of them. A member now may carry an effort (none … max): a request routed to it is asked for that much reasoning whatever the agent asked or effort=auto picked, at the level the model has nearest, while the vendor is asked for the model's own id. The same model at two efforts is two members that rules (use=m:high), stickiness and the trace tell apart; an id whose own last part is a colon (a :free, :7b, :0) stays the model's. The group's reasoning levels are those of its members that follow the agent, and the editor has a reasoning button on each member. Tested with a fake provider through a sandbox magpie: the upstream bodies carry the fixed effort in Chat, Responses and Anthropic shapes, and the routing view shows m1:low / m1:high as two seats, light and dark
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 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.
group: a group's id can change — a group magpie found is group/auto-<model>, and agents could only pick it by that; now its id (any group's) is changed in the editor or with magpie group set auto-gpt-6-astra id=gpt-6-astra: the found one stays removed under the old id, the groups that have it in them and their rules follow, and an agent set to the old id is told it needs setting again (#83). magpie groups no longer says a group in a group is not served
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.
icon: a site's icon is fetched with Clash's TUN fake-ip or Surge's enhanced mode on, and through a proxy on this computer (#252 loosheng: "the icon host … resolves to 198.18.0.x, which is not public") — the guard on icon fetches ("From the website", an import link's icon) refused 198.18.0.0/15, RFC 2544's benchmarking range, where fake-ip proxies answer every name and carry the connection to the real host; it is now let through, since no machine on the user's network sits there. Loopback, private, link-local, CGNAT and the documentation ranges are still refused. And with a proxy set (Clash's system proxy at 127.0.0.1:7890), the transport dialed the proxy through the same guard and refused it as loopback: the proxy's own address is now dialed as it is, and the site's name is judged before the proxy is asked, since the proxy resolves it where magpie can't see (a name that doesn't resolve here is left to the proxy). Tested: TestPublicIP and TestPublicDialFakeIP (198.18.0.5 and 198.19.0.200 dialed, 127.0.0.1, 10.1.2.3, 192.168.1.10, 169.254.169.254, 100.64.0.1 refused before any dial); TestGuardClientThroughLocalProxy, a fake proxy on 127.0.0.1 fetching a public site's icon while 10.x and 192.168.x sites are refused without asking the proxy; both fail on main. go vet ./internal/..., go build -tags nogui (and GOOS=linux, GOOS=windows) and go test -tags nogui ./... pass. Not tried with a real Clash or Surge, nor in the real Wails window.
icon: a site's icon is fetched with Clash's TUN fake-ip or Surge's enhanced mode on, and through a proxy on this computer (#252 loosheng: "the icon host … resolves to 198.18.0.x, which is not public") — the guard on icon fetches ("From the website", an import link's icon) refused 198.18.0.0/15, RFC 2544's benchmarking range, where fake-ip proxies answer every name and carry the connection to the real host; it is now let through, since no machine on the user's network sits there. Loopback, private, link-local, CGNAT and the documentation ranges are still refused. And with a proxy set (Clash's system proxy at 127.0.0.1:7890), the transport dialed the proxy through the same guard and refused it as loopback: the proxy's own address is now dialed as it is, and the site's name is judged before the proxy is asked, since the proxy resolves it where magpie can't see (a name that doesn't resolve here is left to the proxy). Tested: TestPublicIP and TestPublicDialFakeIP (198.18.0.5 and 198.19.0.200 dialed, 127.0.0.1, 10.1.2.3, 192.168.1.10, 169.254.169.254, 100.64.0.1 refused before any dial); TestGuardClientThroughLocalProxy, a fake proxy on 127.0.0.1 fetching a public site's icon while 10.x and 192.168.x sites are refused without asking the proxy; both fail on main. go vet ./internal/..., go build -tags nogui (and GOOS=linux, GOOS=windows) and go test -tags nogui ./... pass. Not tried with a real Clash or Surge, nor in the real Wails window.
provider: a preset's provider takes request headers of the user's own, and a preset can be added more than once (#50) — which workspace a key that reaches several is for (anthropic-workspace-id), or who the app is to OpenRouter, could only be said by a custom provider, so it meant giving up the preset's endpoints and catalog; the editor now shows headers for presets as well, offering the ones the vendor documents with their values left to fill in, and "Add another" makes one more provider of a preset under an id of its own (anthropic-2) where adding again replaced the first; the same key, host and headers twice is refused as the one already there, and importers keep a preset's headers and its preset when they split an entry off to a free id
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
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
provider: saved accounts are kept signed in — a Claude or ChatGPT account saved in magpie but not the one the agent is on was refreshed only when used or its usage looked at, and smart routing leaves the ones waiting their turn idle for days; the magpie serving the gateway now renews each such sign-in about once a day (hourly check, so a machine that slept catches up), never the agent's own; a refresh the vendor refuses (400/401) marks the account signed out — the account list says to add it again, magpie accounts too — and one that succeeds clears it; magpie accounts refresh renews them all now, for cron or launchd; a switch waits for a refresh in flight, so the agent is never handed a refresh token being spent (#53)
accounts: a Claude or Codex account removed from magpie is left alone — its sign-ins are no longer saved from the agent nor kept renewed in the background, until it's brought back; before, removing it only kept it out of the providers while magpie went on copying and refreshing the sign-in (#121)
providers: ChatGPT and Claude accounts come in from CLIProxyAPI's auth files (#124 Mrhe525: 要是能把那什么cpa的json文件可以导入直接使用就无敌了) — the ChatGPT (Codex) and Claude subscriptions now take the import Antigravity had: Import accounts from a file… under the accounts, and Import from a file instead… while a browser sign-in waits. It reads CLIProxyAPI's files ({type:"codex"|"claude", access_token, refresh_token, id_token, account_id, email, …}), one or a list or {accounts:[…]}, Codex's own auth.json ({tokens:{…}}), Claude Code's .credentials.json ({claudeAiOauth:{…}}) and bare refresh tokens one a line. Each account is checked as a sign-in in magpie is finished: its refresh token traded with the agent's own OAuth client (codexRefresh / claudeRefresh), then the ChatGPT claims or Claude's profile read (plan, tier, account and organization), and it is kept as that sign-in would keep it: Codex's or Claude Code's own if the agent has none, else added and ticked. The refresh rotates the token, so the file's copy stops working; the box says so. A refresh token magpie holds already (saved or the agent's own) is "Already in magpie" and never refreshed; another app's file ("a claude sign-in, not Codex's"), an API key, a token in the files twice and one the vendor turns down ("used since … expired or signed out") are listed as not added with the reason; tokens are never in what comes back. codexExchange and claudeExchange now build their sign-in through the same codexLogin / claudeLogin. Also: a Claude sign-in with no ~/.claude yet failed writing .credentials.json (the directory is made now), and the magpie://import dialog was drawn by the subscription import's renderImport, declared later under the same name (renamed renderLoginImport). zh strings added. Tested with new TestImportCodexLogins (a fake token endpoint: a CPA file and a native auth.json added with the new tokens and the file's account id, the live account untouched and reported as there, a Claude file, a spent token and a duplicate refused, a second import "exists", garbage an error) and TestImportClaudeLogins (with no sign-in the CPA file becomes Claude Code's credentials, plan max, active; a spent .credentials.json and a Codex file refused), and a new internal/gui/tests/login-import.test.cjs in Chromium and WebKit, en and zh (both ways in, the texts, what is posted, the outcomes, the account listed). go build -tags nogui (and GOOS=windows, GOOS=linux), go test -tags nogui ./... and the other GUI tests pass on
fe22692 (click-scroll's WebKit case fails after f1a8caf, with or without this change)
providers: ChatGPT and Claude accounts come in from CLIProxyAPI's auth files (#124 Mrhe525: 要是能把那什么cpa的json文件可以导入直接使用就无敌了) — the ChatGPT (Codex) and Claude subscriptions now take the import Antigravity had: Import accounts from a file… under the accounts, and Import from a file instead… while a browser sign-in waits. It reads CLIProxyAPI's files ({type:"codex"|"claude", access_token, refresh_token, id_token, account_id, email, …}), one or a list or {accounts:[…]}, Codex's own auth.json ({tokens:{…}}), Claude Code's .credentials.json ({claudeAiOauth:{…}}) and bare refresh tokens one a line. Each account is checked as a sign-in in magpie is finished: its refresh token traded with the agent's own OAuth client (codexRefresh / claudeRefresh), then the ChatGPT claims or Claude's profile read (plan, tier, account and organization), and it is kept as that sign-in would keep it: Codex's or Claude Code's own if the agent has none, else added and ticked. The refresh rotates the token, so the file's copy stops working; the box says so. A refresh token magpie holds already (saved or the agent's own) is "Already in magpie" and never refreshed; another app's file ("a claude sign-in, not Codex's"), an API key, a token in the files twice and one the vendor turns down ("used since … expired or signed out") are listed as not added with the reason; tokens are never in what comes back. codexExchange and claudeExchange now build their sign-in through the same codexLogin / claudeLogin. Also: a Claude sign-in with no ~/.claude yet failed writing .credentials.json (the directory is made now), and the magpie://import dialog was drawn by the subscription import's renderImport, declared later under the same name (renamed renderLoginImport). zh strings added. Tested with new TestImportCodexLogins (a fake token endpoint: a CPA file and a native auth.json added with the new tokens and the file's account id, the live account untouched and reported as there, a Claude file, a spent token and a duplicate refused, a second import "exists", garbage an error) and TestImportClaudeLogins (with no sign-in the CPA file becomes Claude Code's credentials, plan max, active; a spent .credentials.json and a Codex file refused), and a new internal/gui/tests/login-import.test.cjs in Chromium and WebKit, en and zh (both ways in, the texts, what is posted, the outcomes, the account listed). go build -tags nogui (and GOOS=windows, GOOS=linux), go test -tags nogui ./... and the other GUI tests pass on
fe22692 (click-scroll's WebKit case fails after f1a8caf, with or without this change)
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).
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)
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).
models: a model its maker lists with a thinking switch alone takes no borrowed levels, and the gateway sends it no more than high (#214 Moody-Max: opencode 的 mimo-v2.6 Flash 你这边设置的默认推理强度是 max 导致400报错)
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.
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.
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.
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.
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.
provider: a custom provider can list its models from a URL of its own — Xiaomi MiMo's plans are served at their own hosts while the list is at api.xiaomimimo.com/v1/models, so fetching models from the base URL found nothing; More endpoints has a Models URL (模型列表地址), asked with the key and nowhere else, and magpie provider set <id> models.url=… sets it (#67)
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
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.
zcode: a seat on a team's GLM Coding Plan (团队套餐) is signed in, routed and metered as ZCode does it, on BigModel (智谱) as well as Z.ai, and a Zhipu key on a team plan shows its 5-hour and weekly windows and resets (#236 EZN7L2C3: a BigModel team GLM Coding Plan, the sign-in refused it and the usage was never read) — magpie's ZCode sign-in only went to Z.ai and only looked for the account's own coding-plan key, so a member of a team's plan, whose seat lives in a team project, was refused, and a key on a team plan read nothing from /api/monitor/usage/quota/limit. Read from ZCode.app 3.14.3 (host/index.js and config/provider/zcode-builtin.json): the team rules account:{zai|bigmodel}-team-coding-plan use the individual plans' base URLs with no extra inference headers; getCustomerInfo lists the projects, projectType "2" being a team's (isBigModelTeamCodingPlanProject); fetchTeamCodingPlanEntitlement asks /api/biz/team/subscribe/product/querySubscribeDetail (EFFECTIVE + memberGrantStatus VALID usable, UNASSIGNED no seat, EXPIRED run out); ensureBigModelTeamPlanProjectApiKeyWithStatus / copyBigModelTeamPlanProjectApiKeySecret find or create "zcode-team-api-key" (keyType 2) in the team project and copy its secret; createBigModelBizHeaders sends the access token bare in Authorization with bigmodel-organization / bigmodel-project; resolveAccountTeamPlanRuntimeApiKey takes the stored oauth:{family}:access_token (skipping BigModel's when it equals zcodejwttoken); buildQuotaLimitUrl adds type=2 and createBigModelUsageHeaders sends the key with the org/project headers; ZCode keeps the chosen team in ~/.zcode/v2/setting.json providerFamilyConnectionSelections (legacy modelProviderFamilySelectedKeys "team-plan:builtin:…", migrateLegacyAccountConnectionSettings); pollPendingOAuth/startOAuthWithPolling init with {provider:"zai"|"bigmodel"}, BigModel's redirect param being
redirect, its token the poll's bigmodel.access_token. Now: the sign-in first asks where the account is — Z.ai or BigModel (智谱) (POST /api/signin takes a site, StartSignInAt, "zcode:bigmodel" too) — and on its site tries the account's own plan, then a team project with a usable seat (a new zcode_team.go: zcodeTeamSignIn keeps the team key, org, project and the biz token, the plan named by its product, "GLM Coding Team" otherwise), then the Start Plan; a team seat not yet granted or a team plan expired is refused saying so ("…has no seat on it yet — ask the team's admin…"), and an account with none of them names both plans and the site's subscribe page. ZCode's own team login is read from its credentials and setting.json (zcodeOwnTeam), its key minted lazily and kept (zcodeTeamKeyOf, retried after a minute, "sign in to ZCode again" when refused). Team requests go to the family's Anthropic base signed with the team key, no ZCode headers, never the Start Plan. Team usage asks bigmodel.cn (or api.z.ai) /api/monitor/usage/quota/limit?type=2 with the key and Authorization, Bigmodel-Organization, Bigmodel-Project, Set-Language, Accept-Language: CREDIT_LIMIT windows, unit 3 "5 hours" and unit 6 weekly, "current / total" when there is no remaining; /api/biz/customer-package-reset/list?targetType=TEAM gives the resets, fiveHourResets and weekResets counted where available is true, the soonest expireTime as until; the plan's end from querySubscribeDetail, Renew off. ResetCredits gains byWindow/fiveHour/weekly and Words(), "2 five-hour resets · 1 weekly reset" in the CLI, TUI and GUI (en and zh), with no "Use a reset" button for them since they are spent on the vendor's page. The Zhipu GLM key provider (planquota.go) falls back to the team query when the personal one fails or has no windows, adding the team headers when a ZCode team login holds that key. Personal accounts, Coding Plan and Start Plan, are routed, signed and metered as before. Tested with a new internal/provider/zcode_team_test.go against a fake bigmodel.cn/Z.ai in a sandbox HOME, no real tokens: a BigModel team sign-in through StartSignIn("zcode:bigmodel") is added (redirect param, plan "GLM Coding Team Pro", key made with keyType 2, org/project kept), its requests at the BigModel base signed with the team key and no ZCode headers, usage 5 hours 42% "420 / 1000" with a 5h span and weekly 10%, the quota headers sent, resets 2 five-hour and 1 weekly until 2026-10-05, plan until 2026-12-31; an unassigned seat is refused with no key made; ZCode's own team login from credentials + setting.json is found and metered while an individual plan stays as it was; the key provider's team fallback. TestPlanQuotas gains the type=2 case; the zcode Start Plan and account tests pass. A new internal/gui/tests/zcode-site.test.cjs, Chromium and WebKit, en and zh: starting ZCode's sign-in asks the site and posts nothing, BigModel posts {agent:"zcode",site:"bigmodel"} and keeps it, and the by-window resets read in both languages; signin-callback, click-scroll and login-import pass. go vet ./internal/..., go build -tags nogui (and GOOS=linux, GOOS=windows) and go test -tags nogui ./... pass (TestUserPath timed out under load and passes alone). Not tried against a real account: BigModel's OAuth sign-in and poll shape, the Set-Language values, customer-package-reset's field formats, querySubscribeDetail on Z.ai, type=2 for a pasted key with no org/project, and the time formats are from ZCode's code, not seen on the wire; not tried in the real Wails window.
sessions: omp's local sessions are in Usage › Sessions (#303), and the GLM Coding Plans take Codex's Responses as they are (#306) — omp (oh-my-pi) keeps Pi's session jsonl (v3) under ~/.omp/agent/sessions (and profiles' and $XDG_DATA_HOME/omp's) with a title line, title changes, model_usage for calls outside the chat, provider/model changes and subagent/advisor transcripts beside the file; they are read through Pi's parser, subagents counted in their session, a fork's copied entries in the original only, resumed by omp --resume <id>. The zhipu and zai Coding Plan regions get their Responses base (open.bigmodel.cn/api/v1, api.z.ai/api/v1, per their Codex docs), so Codex's requests go there as they are instead of rebuilt for Chat; Chat and Claude Code routes are unchanged. TestOmp, TestDirsMore and TestGLMCodingPlanResponses fail without the change. go vet, go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui ./... pass.
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.
qoder: thinking turned off reaches Qoder as off, a refused usage sign-in no longer lapses the account, only a gone sign-in is a 401, and a tool's images are sent (#162 review follow-ups)
qoder: thinking turned off reaches Qoder as off, a refused usage sign-in no longer lapses the account, only a gone sign-in is a 401, and a tool's images are sent (#162 review follow-ups)
qoder: thinking turned off reaches Qoder as off, a refused usage sign-in no longer lapses the account, only a gone sign-in is a 401, and a tool's images are sent (#162 review follow-ups)
routing: an account out of quota rests until it says it's back, not an hour at a time (关于路由组优先级高的模型异常的展示文案 #147) — ChatGPT's usage-limit refusal reaches the failover put in words, its resets_at only as the Retry-After magpie made of it, and a Retry-After is trusted for an hour at most, so a Plus account out until 21:34 read "59 分钟后恢复" and was tried, refused and benched again each hour; the reset now goes on beside the error for the rest to take as the vendor's own, the window it filled renews next, a bare Retry-After alone keeps the hour, a rate limit and a failure keep their short rests, and a Codex reset spent in magpie brings the account back at once. The Routing page says when as well as how long: "quota used up · back at 10:29 PM · in 3 h 41 min" (额度用尽 · 22:29 恢复 · 还有 3 小时 41 分钟)
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).
subscriptions: signing in to Claude over credentials a logout left behind makes the new account the one Claude Code uses — magpie took the stale keychain item and profile for a live sign-in, so the new account was kept beside it unused while Claude Code said it was signed out, and no Claude provider showed up (#31)
provider: add a Claude or ChatGPT subscription from magpie itself — it opens the vendor's sign-in, takes the account when it comes back and lists it with the others, one click from being in use; magpie accounts add <agent> on the command line (#15)
provider: Antigravity's usage shows only the models enabled again (antigravity 为什么又显示了没有启用的模型的用量?) — since #150 (
2baad53) magpie offers a family of Antigravity's levels as one model (gemini-3.8-flash), but its quota still names each level's id (gemini-3.8-flash-high), so none of the windows matched what's enabled and chosenWindows, finding none, kept all 27. A window's id is now taken to the model magpie offers for it (AntigravityBase) before it's matched; on this machine, with Gemini 3.8 Flash enabled, 3 of 27 are kept: its high, low and medium
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).
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: 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
workbuddy: a check-in that never reached WorkBuddy is tried again a minute later, then 2, 4, 8, 16 and 30, not after half an hour (#265 WhyWhatHow: the check-in went out the second Windows woke, before its network was back, "no such host", then 签到失败 for 30 minutes)
workbuddy: a check-in that never reached WorkBuddy is tried again a minute later, then 2, 4, 8, 16 and 30, not after half an hour (#265 WhyWhatHow: the check-in went out the second Windows woke, before its network was back, "no such host", then 签到失败 for 30 minutes)
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.
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.
zcode: requests to zcode.z.ai name the machine with X-Device-Mid, so a Z.ai account with only ZCode's Start Plan signs in again (#282 volcanicll: /api/v1/zcode-plan/billing/balance now refuses a request without it, 400 code 3001 "parameter error", and sign-in read that as the Start Plan having ended) — ZCode's apiClient sends it on every request; the server asks only that it is there. magpie makes one v4 UUID for this machine, kept as zcode-device-mid beside providers.json (never ZCode's deviceMid, never ~/.zcode), and sends it on zcode.z.ai calls and Start Plan model requests; Z.ai's and BigModel's business APIs don't get it. A Start Plan balance that can't be read is now said as such in the sign-in error, with the server's code ("parameter error (400, code 3001)"), instead of "has ended or was never started", which stays for an account whose Start Plan really has. Tested with a fake zcode.z.ai refusing a request without the header like the real one (TestZCodeSignInStartPlan, TestZCodeStartPlanOwnAccount, TestZCodeStartPlanRouting and a new TestZCodeSignInStartPlanDeviceMid fail without it): the id is a v4 UUID, kept across a restart, sent to zcode.z.ai only; with it stripped sign-in says the server's reason; an ended Start Plan still says ended. go vet, go build (GUI, -tags nogui, GOOS=linux, GOOS=windows), go test -tags nogui ./... pass. Not tried against the real zcode.z.ai.
zcode: requests to zcode.z.ai name the machine with X-Device-Mid, so a Z.ai account with only ZCode's Start Plan signs in again (#282 volcanicll: /api/v1/zcode-plan/billing/balance now refuses a request without it, 400 code 3001 "parameter error", and sign-in read that as the Start Plan having ended) — ZCode's apiClient sends it on every request; the server asks only that it is there. magpie makes one v4 UUID for this machine, kept as zcode-device-mid beside providers.json (never ZCode's deviceMid, never ~/.zcode), and sends it on zcode.z.ai calls and Start Plan model requests; Z.ai's and BigModel's business APIs don't get it. A Start Plan balance that can't be read is now said as such in the sign-in error, with the server's code ("parameter error (400, code 3001)"), instead of "has ended or was never started", which stays for an account whose Start Plan really has. Tested with a fake zcode.z.ai refusing a request without the header like the real one (TestZCodeSignInStartPlan, TestZCodeStartPlanOwnAccount, TestZCodeStartPlanRouting and a new TestZCodeSignInStartPlanDeviceMid fail without it): the id is a v4 UUID, kept across a restart, sent to zcode.z.ai only; with it stripped sign-in says the server's reason; an ended Start Plan still says ended. go vet, go build (GUI, -tags nogui, GOOS=linux, GOOS=windows), go test -tags nogui ./... pass. Not tried against the real zcode.z.ai.
zcode: an account with no GLM Coding Plan uses ZCode's Start Plan (体验套餐) where ZCode does, with ZCode's own token, and shows its allowance (#236 tasselx: ZCode 3.14.4, magpie 0.1.410, a trial-only account, the free quota not read and every request refused with rate_limit_error "ZCode: [1113][Insufficient balance or no resource package. Please recharge.]"; #225 xiaozhu1337: ZCode ships a built-in trial package) — magpie sent every ZCode account to the Coding Plan's endpoint, api.z.ai/api/anthropic, with the account's zcode-api-key, and read its allowance only from /api/monitor/usage/quota/limit. That key is billed to a Coding Plan, or else to the account's API balance, so an account with only the Start Plan got 1113. ZCode itself (ZCode.app 3.14.3's zcode-builtin.json providerRules and host/index.js: resolveCurrent, buildZaiStartPlanBalanceUrl, hasActiveStartPlan, isZaiStartPlanIdentity, normalizeStartPlanExpiry, normalizeZaiStartPlanBalanceLimits, pollPendingOAuth) serves the Start Plan (account:zai-start-plan and account:bigmodel-start-plan) from zcode.z.ai/api/v1/zcode-plan/anthropic, keyed by its own session token (the credential store's zcodejwttoken, the
token its sign-in's poll hands back, which magpie dropped), and reads what is left from /api/v1/zcode-plan/billing/balance?app_version= with that token. Now, in a new zcode_start.go: an account keeps ZCode's token beside its key (zcodeKey.JWT; zcodeOwn reads zcodejwttoken, and user_info from oauth:bigmodel:user_info when there is no Z.ai one; an account with the token and no coding plan key is an account). Whether it is on the Start Plan is asked of /api/biz/subscription/list (a Coding Plan: as before; none: the Start Plan; can't tell: the Start Plan if its balance has an active one), kept 10 minutes (1 when unsure); with no token it is the Coding Plan as before, asking nothing, and with no key it is the Start Plan. On the Start Plan a request is moved to zcode-plan/anthropic, signed with the token (x-api-key and Bearer) and ZCode's own headers (User-Agent ZCode/<ver>, X-ZCode-App-Version, X-Title, HTTP-Referer, X-Platform), refused with "sign in to ZCode again" once the token has run out; its models are the Start Plan's (zcodePlanID account:zai-start-plan for ZCode's config, zcodeModels less GLM-5.3 before it is read). Its allowance is the balance as ZCode reads it: a plan still "active" past ends_at (by server_time) is expired and the buckets of expired plans left out; the plan's name (Start Plan) with Until its end and Renew off; a window per bucket, named by its show_name (or its models, or Credits), Used and "used / total" from total/used/remaining units (numbers or strings), ResetsAt its expires_at, Span from its entitlement's period (daily/weekly/monthly) or period_start..end, and matched to the bucket's models. Signing in from magpie no longer refuses an account with no Coding Plan: with an active Start Plan it is added on that (its key kept too, so a Coding Plan bought later is used), and refused only when the Start Plan is over as well ("this Z.ai account has no GLM Coding Plan, and ZCode's Start Plan has ended or was never started — subscribe at z.ai/subscribe, then add it again"). Accounts with a Coding Plan are routed, signed and metered as before. The provider keeps the name ZCode (#225 asked for "GLM Coding Plan"; ZCode names these plans "Z.AI Individual Coding Plan" and "Start Plan", and the plan line already shows "GLM Coding <Level>"). Tested with a new internal/provider/zcode_start_test.go against a fake Z.ai/zcode.z.ai in a sandbox HOME, no real tokens: ZCode signed in with only its token (BigModel user_info) is an account, its provider served at zcode-plan/anthropic, a request signed with the token and ZCode's headers, the Start Plan's models and plan id, its allowance (Start Plan, Until, Renew off, GLM-5.1's window 25%, "250000 / 1000000", reset, a day's span, matching GLM-5.1 only); an expired plan and one active past its end give none; with a key and the token, no Coding Plan moves the request (query kept) to the Start Plan and it reaches it with the token, a Coding Plan keeps it on api.z.ai with the key and no ZCode headers, no token asks nothing, an expired token is refused; a sign-in with only the Start Plan is added on it, with a Coding Plan on that plan, with neither refused naming both. TestZCodeAccounts passes unchanged. go build -tags nogui (and GOOS=linux, GOOS=windows), go vet and go test -tags nogui ./... pass (TestUpdateCLI, TestClaudeSignedOut, TestClaudeAccountNamedAsItsLogin and TestUserPath timed out once each under load beside other builds and pass alone). Not tried with a real Start Plan account: the endpoint's acceptance of the token as x-api-key/Bearer and whether it needs ZCode's headers, the balance's real field values and period strings, what subscription/list returns for a trial-only account, and ZCode 3.14.4 (read from 3.14.3) are from ZCode's code, not seen on the wire.
zcode: requests to zcode.z.ai name the machine with X-Device-Mid, so a Z.ai account with only ZCode's Start Plan signs in again (#282 volcanicll: /api/v1/zcode-plan/billing/balance now refuses a request without it, 400 code 3001 "parameter error", and sign-in read that as the Start Plan having ended) — ZCode's apiClient sends it on every request; the server asks only that it is there. magpie makes one v4 UUID for this machine, kept as zcode-device-mid beside providers.json (never ZCode's deviceMid, never ~/.zcode), and sends it on zcode.z.ai calls and Start Plan model requests; Z.ai's and BigModel's business APIs don't get it. A Start Plan balance that can't be read is now said as such in the sign-in error, with the server's code ("parameter error (400, code 3001)"), instead of "has ended or was never started", which stays for an account whose Start Plan really has. Tested with a fake zcode.z.ai refusing a request without the header like the real one (TestZCodeSignInStartPlan, TestZCodeStartPlanOwnAccount, TestZCodeStartPlanRouting and a new TestZCodeSignInStartPlanDeviceMid fail without it): the id is a v4 UUID, kept across a restart, sent to zcode.z.ai only; with it stripped sign-in says the server's reason; an ended Start Plan still says ended. go vet, go build (GUI, -tags nogui, GOOS=linux, GOOS=windows), go test -tags nogui ./... pass. Not tried against the real zcode.z.ai.
zcode: requests to zcode.z.ai name the machine with X-Device-Mid, so a Z.ai account with only ZCode's Start Plan signs in again (#282 volcanicll: /api/v1/zcode-plan/billing/balance now refuses a request without it, 400 code 3001 "parameter error", and sign-in read that as the Start Plan having ended) — ZCode's apiClient sends it on every request; the server asks only that it is there. magpie makes one v4 UUID for this machine, kept as zcode-device-mid beside providers.json (never ZCode's deviceMid, never ~/.zcode), and sends it on zcode.z.ai calls and Start Plan model requests; Z.ai's and BigModel's business APIs don't get it. A Start Plan balance that can't be read is now said as such in the sign-in error, with the server's code ("parameter error (400, code 3001)"), instead of "has ended or was never started", which stays for an account whose Start Plan really has. Tested with a fake zcode.z.ai refusing a request without the header like the real one (TestZCodeSignInStartPlan, TestZCodeStartPlanOwnAccount, TestZCodeStartPlanRouting and a new TestZCodeSignInStartPlanDeviceMid fail without it): the id is a v4 UUID, kept across a restart, sent to zcode.z.ai only; with it stripped sign-in says the server's reason; an ended Start Plan still says ended. go vet, go build (GUI, -tags nogui, GOOS=linux, GOOS=windows), go test -tags nogui ./... pass. Not tried against the real zcode.z.ai.
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).
zcode: a seat on a team's GLM Coding Plan (团队套餐) is signed in, routed and metered as ZCode does it, on BigModel (智谱) as well as Z.ai, and a Zhipu key on a team plan shows its 5-hour and weekly windows and resets (#236 EZN7L2C3: a BigModel team GLM Coding Plan, the sign-in refused it and the usage was never read) — magpie's ZCode sign-in only went to Z.ai and only looked for the account's own coding-plan key, so a member of a team's plan, whose seat lives in a team project, was refused, and a key on a team plan read nothing from /api/monitor/usage/quota/limit. Read from ZCode.app 3.14.3 (host/index.js and config/provider/zcode-builtin.json): the team rules account:{zai|bigmodel}-team-coding-plan use the individual plans' base URLs with no extra inference headers; getCustomerInfo lists the projects, projectType "2" being a team's (isBigModelTeamCodingPlanProject); fetchTeamCodingPlanEntitlement asks /api/biz/team/subscribe/product/querySubscribeDetail (EFFECTIVE + memberGrantStatus VALID usable, UNASSIGNED no seat, EXPIRED run out); ensureBigModelTeamPlanProjectApiKeyWithStatus / copyBigModelTeamPlanProjectApiKeySecret find or create "zcode-team-api-key" (keyType 2) in the team project and copy its secret; createBigModelBizHeaders sends the access token bare in Authorization with bigmodel-organization / bigmodel-project; resolveAccountTeamPlanRuntimeApiKey takes the stored oauth:{family}:access_token (skipping BigModel's when it equals zcodejwttoken); buildQuotaLimitUrl adds type=2 and createBigModelUsageHeaders sends the key with the org/project headers; ZCode keeps the chosen team in ~/.zcode/v2/setting.json providerFamilyConnectionSelections (legacy modelProviderFamilySelectedKeys "team-plan:builtin:…", migrateLegacyAccountConnectionSettings); pollPendingOAuth/startOAuthWithPolling init with {provider:"zai"|"bigmodel"}, BigModel's redirect param being
redirect, its token the poll's bigmodel.access_token. Now: the sign-in first asks where the account is — Z.ai or BigModel (智谱) (POST /api/signin takes a site, StartSignInAt, "zcode:bigmodel" too) — and on its site tries the account's own plan, then a team project with a usable seat (a new zcode_team.go: zcodeTeamSignIn keeps the team key, org, project and the biz token, the plan named by its product, "GLM Coding Team" otherwise), then the Start Plan; a team seat not yet granted or a team plan expired is refused saying so ("…has no seat on it yet — ask the team's admin…"), and an account with none of them names both plans and the site's subscribe page. ZCode's own team login is read from its credentials and setting.json (zcodeOwnTeam), its key minted lazily and kept (zcodeTeamKeyOf, retried after a minute, "sign in to ZCode again" when refused). Team requests go to the family's Anthropic base signed with the team key, no ZCode headers, never the Start Plan. Team usage asks bigmodel.cn (or api.z.ai) /api/monitor/usage/quota/limit?type=2 with the key and Authorization, Bigmodel-Organization, Bigmodel-Project, Set-Language, Accept-Language: CREDIT_LIMIT windows, unit 3 "5 hours" and unit 6 weekly, "current / total" when there is no remaining; /api/biz/customer-package-reset/list?targetType=TEAM gives the resets, fiveHourResets and weekResets counted where available is true, the soonest expireTime as until; the plan's end from querySubscribeDetail, Renew off. ResetCredits gains byWindow/fiveHour/weekly and Words(), "2 five-hour resets · 1 weekly reset" in the CLI, TUI and GUI (en and zh), with no "Use a reset" button for them since they are spent on the vendor's page. The Zhipu GLM key provider (planquota.go) falls back to the team query when the personal one fails or has no windows, adding the team headers when a ZCode team login holds that key. Personal accounts, Coding Plan and Start Plan, are routed, signed and metered as before. Tested with a new internal/provider/zcode_team_test.go against a fake bigmodel.cn/Z.ai in a sandbox HOME, no real tokens: a BigModel team sign-in through StartSignIn("zcode:bigmodel") is added (redirect param, plan "GLM Coding Team Pro", key made with keyType 2, org/project kept), its requests at the BigModel base signed with the team key and no ZCode headers, usage 5 hours 42% "420 / 1000" with a 5h span and weekly 10%, the quota headers sent, resets 2 five-hour and 1 weekly until 2026-10-05, plan until 2026-12-31; an unassigned seat is refused with no key made; ZCode's own team login from credentials + setting.json is found and metered while an individual plan stays as it was; the key provider's team fallback. TestPlanQuotas gains the type=2 case; the zcode Start Plan and account tests pass. A new internal/gui/tests/zcode-site.test.cjs, Chromium and WebKit, en and zh: starting ZCode's sign-in asks the site and posts nothing, BigModel posts {agent:"zcode",site:"bigmodel"} and keeps it, and the by-window resets read in both languages; signin-callback, click-scroll and login-import pass. go vet ./internal/..., go build -tags nogui (and GOOS=linux, GOOS=windows) and go test -tags nogui ./... pass (TestUserPath timed out under load and passes alone). Not tried against a real account: BigModel's OAuth sign-in and poll shape, the Set-Language values, customer-package-reset's field formats, querySubscribeDetail on Z.ai, type=2 for a pasted key with no org/project, and the time formats are from ZCode's code, not seen on the wire; not tried in the real Wails window.
zcode: requests to zcode.z.ai name the machine with X-Device-Mid, so a Z.ai account with only ZCode's Start Plan signs in again (#282 volcanicll: /api/v1/zcode-plan/billing/balance now refuses a request without it, 400 code 3001 "parameter error", and sign-in read that as the Start Plan having ended) — ZCode's apiClient sends it on every request; the server asks only that it is there. magpie makes one v4 UUID for this machine, kept as zcode-device-mid beside providers.json (never ZCode's deviceMid, never ~/.zcode), and sends it on zcode.z.ai calls and Start Plan model requests; Z.ai's and BigModel's business APIs don't get it. A Start Plan balance that can't be read is now said as such in the sign-in error, with the server's code ("parameter error (400, code 3001)"), instead of "has ended or was never started", which stays for an account whose Start Plan really has. Tested with a fake zcode.z.ai refusing a request without the header like the real one (TestZCodeSignInStartPlan, TestZCodeStartPlanOwnAccount, TestZCodeStartPlanRouting and a new TestZCodeSignInStartPlanDeviceMid fail without it): the id is a v4 UUID, kept across a restart, sent to zcode.z.ai only; with it stripped sign-in says the server's reason; an ended Start Plan still says ended. go vet, go build (GUI, -tags nogui, GOOS=linux, GOOS=windows), go test -tags nogui ./... pass. Not tried against the real zcode.z.ai.
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).
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).