Files
gateway: a gateway key can be held to some models (#882, LCorleone) — a key could reach every model magpie serves. A key now keeps an optional list of models (caller-keys.json "models", none = every model, so older files read as before): <provider>/<model> or <provider>/*, matched against the provider that serves the call, so a bare model name is resolved first and OpenRouter's "anthropic/x" is not Anthropic's. Such a key sees only its models in /v1/models, /v1/models/{id} and the Anthropic (Claude Desktop) and Gemini lists, a routing group only when every member is its; asking for another, by id, bare name or through a group with a member it may not use, is refused before any provider is asked with a 403 in the API's own error shape (permission_error) that names the key and its models, logged as a rejected call like an over-limit key's 429; a fallback it may not use is skipped; embeddings, images and videos it names are held too. magpie's own calls for it (a web search, a picture described, Codex titles with the model Settings name) are marked in their context, not by a User-Agent a client could send, and go on. Set it with the new badge on the key's row in the Gateway page (the app's menu: All models, every model of a provider, each model; sent when the menu closes) or
magpie gateway-key models <id> openai/gpt-5 anthropic/* (all takes it off); gateway-key list has a MODELS column. The app's menu, fitting neither below nor above its button, now goes on the roomier side and scrolls instead of running off the window. en/zh/ja/de. TestGatewayKeyModelsHoldAKey (lists, 403s by id, bare name and group, Anthropic shape, fallback skipped, magpie's own call, a free key) fails without the change; TestModelPatterns, TestKeyModelsKept, TestGatewayKeyModelsCLI, TestCallerKeyModelsRoutes pass; gateway-key-models.test.cjs (Chromium and WebKit, en and zh: no native select, menu inside a 420px window, picks sent on close, All models, no scroll, no left border, 560px fits) passes with gateway-connect-keys, gateway-key-limit, gateway-caller-keys and click-scroll. go vet, go build (darwin, GOOS=linux nogui, GOOS=windows) and go test -tags nogui ./... under a temp HOME pass.
sessions, gui: Codex sessions made with another provider are moved to Codex's own from the Sessions page, and back (#887, cjyrainbow) — Codex's history lists only the sessions of the provider it uses now, so sessions made through CC Switch (model_provider "custom") vanished from it once Codex used magpie, though their files were still there and the Sessions page listed them. The Codex tab now reads each session's provider from its session_meta (cached with the rest) and the one Codex in that session's home uses (config.toml's model_provider, or its profile's; "openai" when none), says how many were made with another and which, and tags each. "Move to <provider>…" opens magpie's own dialog listing them, each ticked; Move rewrites only the model_provider string in each session's files (.jsonl and .jsonl.zst, in its own Codex home, so WSL's too, mtime kept) and in Codex's state_*.sqlite threads row: messages, id, title and archive are left as they are. A copy of the files goes to magpie's trash folder (trash/codex-provider, with a move.json) first, a failed rewrite is put back from it, and a session written to in the last minute is left alone and said so. Undo moves each back to the provider it had, byte for byte. Nothing is moved by itself. POST /api/sessions/codex-provider; en/zh/ja/de. TestMoveCodexProvider (listing, bytes, mtime, the DB row and nothing else, backup, byte-identical undo, an active session, a bad id), TestMoveCodexProviderPacked and TestCodexProviderIn fail without the change, as does sessions-codex-provider.test.cjs (WebKit, en and zh: magpie's dialog, Cancel posts nothing, an unticked one stays, Undo, no click moves the page, no left border, translations); sessions-manage, -toolbar, -purge, -folder-delete, -wsl-delete and click-scroll pass. Not tried against a real CC Switch history. go vet, go build (darwin, GOOS=linux nogui, GOOS=windows) and go test -tags nogui ./... under a temp HOME pass.
gateway, gui: a provider's keys can be routed by weight, a key weighing 3 taking three requests for every one a key weighing 1 takes (dengtongcai, #841) — Routing already spread requests over a provider's keys in order, in turn, least used first or by pace, but never in a set proportion: two team keys meant to split 75/25 could only go evenly or one after the other. Routing now has By weight (routing "weight") for a provider with keys: each key that's on shows its weight and its share beside it in the editor (Weight 3 · 75%), a click edits it in place and posts keys/weight (provider.SetKeyWeight, 1 to 1000; none or 0 counts as 1, kept out of the file), and providers.json takes it as "weight" on each of keys (keyWeight for the first). The gateway picks as NGINX's smooth weighted round robin does (weighted.go): 3, 1 and 4 go round in every 8 requests in that ratio, never bunched; the others follow in their order for failover. A key resting after a failure is left out of the round while it rests, the others taking its share by their weights, and goes last. A conversation still stays with its key as Stays says (cache-warm across turns by default, within a turn always), and a request kept so is counted against the key that took it, not the one the round picked, so new conversations make up the share; Stays Off gives the ratio request by request. The weight goes with its key through Make first, the editor's Save, backups and WebDAV sync. The Routing page offers By weight for key pools and explains a pick by it; sink (Goes to the back) is off for it as for In turn. zh, ja, de strings. TestWeightedKeys (3:1:4 exact in each of 50 rounds of 8 through perKey and route; with one resting the other two 75/25 and the resting one last; a kept request made up by the next picks) and TestSetKeyWeight fail on the old code (no Weighted); key-weight.test.cjs (WebKit, en and zh: By weight shows the weights and shares, a click sets one and the shares redraw, Smart hides them, no scroll) fails on the old code (no By weight). key-import, panel-routing, routing-save, rate-limited-sink, routing-reset-deadline, account-arrange, gui-ja, gui-de and click-scroll pass on WebKit. go vet, builds (darwin, GOOS=linux nogui, GOOS=windows) and go test -tags nogui -count=1 ./... under a temp HOME pass.
provider, gui: Compact at — where Codex and Claude Code compact a long conversation, set per provider or per model in its editor and to any size in Settings → Long conversations (#876, hisiling)
sync: it can sync by itself less often or only when asked, and the server's setup can be restored over this computer's, undoably (Sun1090, #847: 同步太快了,需要设置个开关控制自动同步,还有恢复按钮) — the gateway's magpie synced every 3 minutes with no way to change it, and a free 坚果云 account is allowed only so many WebDAV requests (600 per 30 minutes, 1 GB up and 3 GB down a month); and there was no way to take the server's setup back over a computer's own, which the next sync would otherwise push up. Settings › Sync now has Sync by itself, the app's segmented control (no <select>): Off, 3 min (the default, as before), 15 min, 30 min, 1 hour; Off syncs only on Sync now, and the sync row says only when asked. The loop reads the setting each minute (no request), so a change takes effect without a restart, and a server limiting requests is still waited for as backoff says. Restore from the server asks first in the app's own dialog, naming the parts replaced; it reads the server's file as it is now, seals this computer's setup (keys and all) into the sync folder as <time>-before-restore (newest 5 kept), brings in each part that differs as a sync brings one changed only there, and leaves the state as such a sync would, so the next sync pushes nothing back. Its notice says what was restored, with Undo, which brings the kept setup back (the next sync then takes it to the others); OK, or a later sync's own notice, drops the undo. The form's Save keeps the setting (SetAuto alone sets it). CLI: magpie webdav|s3 auto off|3|15|30|60, restore, undo; the status says how often it syncs by itself. zh, ja, de strings. TestAutoSync (off: no request by itself; back to 15 min: one sync, then none; Save keeps it; 7 refused) and TestRestore (b's own changes replaced by the server's with no push, the kept copy holds b's provider and key, Undo puts it back and a gets it, nothing differing restores nothing) pass -race; sync-auto-restore.test.cjs (WebKit, en and zh: Off posts -1 and the rows say so, Cancel posts nothing, Restore and Undo post, no click moves the page) fails on the old code. sync-other-kind, sync-spin, s3-sync, webdav-settings-refresh, usage-computers, click-scroll, gui-ja, gui-de and settings-sections pass on WebKit. go vet, builds (darwin, GOOS=linux nogui, GOOS=windows) and go test -tags nogui -count=1 ./... under a temp HOME pass.
update, gui: a new version Windows won't start is never left in place — tried before it goes in, and a restart into one that doesn't say it is up puts the running one back; that version isn't downloaded again by the clock, and Settings › About › Version says why (#894, hisiling) — on Windows 11 with Smart App Control on, the update put the new unsigned exe in place of the running one and restarted into it; CodeIntegrity (3033/3077) refused it, magpie never started again and the working version sat in .old. Now, on Windows, the downloaded exe is first started suspended with no window and ended at once (Windows checks a program when its process is created), and one refused stays out: the running exe isn't moved. At the restart the new process is given MAGPIE_STARTED, a file it writes first thing (AwaitPredecessor); the old one waits up to 20s for it and, if the start fails (ERROR_SYSTEM_INTEGRITY_POLICY_VIOLATION 4551, 1260 or any other), the new process quits before saying so, or it says nothing in time (then it is ended), puts the running exe back over the new one (the new one renamed .blocked if it can't go) and keeps running instead of quitting; nothing notes it as updated in the app. The version is noted in the config folder (update-blocked.json, this machine's only, never synced; gone once a version at least as new runs); the clock's checks leave it alone and show why, a Check clicked downloads and tries it once more. Settings › About › Version: for an application control policy, "0.1.x didn't start on this computer: Windows' Smart App Control (or another app control policy) blocked the new version, which isn't signed. magpie stays on 0.1.y. Download it and run it yourself, or turn Smart App Control off and check again", else the reason, with Download and Check; the error in full on hover. en/zh/ja/de. A user who double-clicks the refused exe is refused by Windows before magpie runs; only signing helps there. TestRelaunchRefusedPutsBack (4551 at the start), TestRelaunchSilentPutsBack (no word in time, killed; quit at once), TestRelaunchReportedStays, TestReportStarted, TestBlockedNote and TestUpdateThatWontStart (the restart doesn't quit, the note, the clock not downloading it again, a Check that does) are new and fail without the change; on Windows TestInstallBinaryRefusedStaysOut, TestTrialStart and TestStartCheckedPutsBackForReal (real processes: not a program, quits at once, runs on silent) pass on the Windows 11 test box with the whole internal/update package (Smart App Control itself isn't on there, so the refusal is simulated). update-blocked.test.cjs (Chromium and WebKit, en and zh: the reason and both versions, full error on hover, Download opens the release, Check downloads, the other reason, no select, no scroll, no left border) fails without the change; update-check, update-mirror, update-auto, update-wait and update-download-percent pass in WebKit; click-scroll's request-list case fails now and then the same on main. go vet, go build (darwin, GOOS=linux nogui, GOOS=windows) and go test -tags nogui ./... under a temp HOME pass.
library: Claude Desktop's skills list shows again, each skill's updatedAt an ISO time (mingxitang, #863) — magpie wrote a skill's updatedAt in the kind lastUpdated had, milliseconds where it is a number, but Desktop's own entries are ISO times or null and listLocalSkills checks the whole list at once: one number and the skills page stayed loading (failed to pass validation). Now updatedAt is always an ISO time and lastUpdated keeps its own kind; an entry written as milliseconds before is written as the same moment in ISO at the next sync, its other keys as they were. TestClaudeDesktopSkills: the entry of a manifest with millisecond lastUpdated, and an old numeric entry fixed at a sync; it fails on the old code. go vet, builds (darwin, GOOS=linux nogui, GOOS=windows) and go test -tags nogui -count=1 ./... under a temp HOME pass (five timing tests under load 20 passed when run again).
gateway, gui, cli: each account or key can have its own limit on requests at once, with a bounded queue that turns requests away with a 429 (#892, margbug01) — a provider's Concurrency applied to every key and account alike, the queue past it had no bound and no wait, and nothing showed what ran or waited. A key or account now may have its own limit over the provider's (Provider.AccountConcurrency, by lowercased account or KeyID; 0 none; unset takes the provider's, as before), counted in one lane across every model, routing group and agent. A provider's Queue size bounds how many wait for each of its lanes (one more is turned away at once) and Queue wait how many seconds one waits (then it leaves the queue); both 0 by default, as before. A request turned away goes on to the next key, account or group member without resting anything, else gets a 429 in its API's own shape (Anthropic rate_limit_error, OpenAI's, Gemini's) with Retry-After: 1, saying the limit and the queue. A slot is given back when the reply is read to the end, fails or the agent goes away; a waiter that goes away leaves the queue, and one granted as it gave up hands the slot on. In a routing group, or among a provider's keys and accounts, a full one gives way once to another with a free slot now, so a group spreads before anyone waits; a fallback provider is never asked for want of a slot, and with all full the request waits for the one routing chose. The Claude subscription path and plugin providers take their slot the same way, around the attempt; a parked Claude Code tool turn has already answered and holds none. GUI: each account and key row has a pill — "1/1 running · 2 queued" while requests are out (GET /api/lanes, read every two seconds and written into the pill in place), else its limit; none set shows on hover only — opening the app's menu (provider's, no limit, 1/2/3/5/10, Other… typed in place), posting provider/accountconcurrency; the editor has Queue size and Queue wait under Concurrency. CLI: magpie provider account-concurrency <id> [account|key [n|off|default]] and magpie provider queue <id> [size [seconds]]. en/zh/ja/de. TestAccountLimitSharedAcrossGroups, TestQueueFullAndWaitedOut, TestLaneReleasedOnFailure and TestFullMemberGivesWay (pass with -race, with TestMaxConcurrency and TestLanesLimitLifted), TestLaneLimitRoutes and TestAccountConcurrencyCmd are new and fail without the change; lane-limit.test.cjs (WebKit, en and zh: a key's busy/queued pill updated in place from /api/lanes, set from the menu, provider's posts null, Other… typed, an account's own limit, none on hover only, queue fields saved and refused out of range, labels on one line, every string in zh/ja/de, no select, no scroll, no left border) fails without the change. key-weight-fit, account-cap, provider-concurrency, click-scroll, account-models and api-key-usage pass in WebKit; the other WebKit failures fail the same on main. go vet, go build (darwin, GOOS=linux nogui, GOOS=windows) and go test -tags nogui ./... under a temp HOME pass.
sessions, gui: Codex sessions made with another provider are moved to Codex's own from the Sessions page, and back (#887, cjyrainbow) — Codex's history lists only the sessions of the provider it uses now, so sessions made through CC Switch (model_provider "custom") vanished from it once Codex used magpie, though their files were still there and the Sessions page listed them. The Codex tab now reads each session's provider from its session_meta (cached with the rest) and the one Codex in that session's home uses (config.toml's model_provider, or its profile's; "openai" when none), says how many were made with another and which, and tags each. "Move to <provider>…" opens magpie's own dialog listing them, each ticked; Move rewrites only the model_provider string in each session's files (.jsonl and .jsonl.zst, in its own Codex home, so WSL's too, mtime kept) and in Codex's state_*.sqlite threads row: messages, id, title and archive are left as they are. A copy of the files goes to magpie's trash folder (trash/codex-provider, with a move.json) first, a failed rewrite is put back from it, and a session written to in the last minute is left alone and said so. Undo moves each back to the provider it had, byte for byte. Nothing is moved by itself. POST /api/sessions/codex-provider; en/zh/ja/de. TestMoveCodexProvider (listing, bytes, mtime, the DB row and nothing else, backup, byte-identical undo, an active session, a bad id), TestMoveCodexProviderPacked and TestCodexProviderIn fail without the change, as does sessions-codex-provider.test.cjs (WebKit, en and zh: magpie's dialog, Cancel posts nothing, an unticked one stays, Undo, no click moves the page, no left border, translations); sessions-manage, -toolbar, -purge, -folder-delete, -wsl-delete and click-scroll pass. Not tried against a real CC Switch history. go vet, go build (darwin, GOOS=linux nogui, GOOS=windows) and go test -tags nogui ./... under a temp HOME pass.
update, gui: a new version Windows won't start is never left in place — tried before it goes in, and a restart into one that doesn't say it is up puts the running one back; that version isn't downloaded again by the clock, and Settings › About › Version says why (#894, hisiling) — on Windows 11 with Smart App Control on, the update put the new unsigned exe in place of the running one and restarted into it; CodeIntegrity (3033/3077) refused it, magpie never started again and the working version sat in .old. Now, on Windows, the downloaded exe is first started suspended with no window and ended at once (Windows checks a program when its process is created), and one refused stays out: the running exe isn't moved. At the restart the new process is given MAGPIE_STARTED, a file it writes first thing (AwaitPredecessor); the old one waits up to 20s for it and, if the start fails (ERROR_SYSTEM_INTEGRITY_POLICY_VIOLATION 4551, 1260 or any other), the new process quits before saying so, or it says nothing in time (then it is ended), puts the running exe back over the new one (the new one renamed .blocked if it can't go) and keeps running instead of quitting; nothing notes it as updated in the app. The version is noted in the config folder (update-blocked.json, this machine's only, never synced; gone once a version at least as new runs); the clock's checks leave it alone and show why, a Check clicked downloads and tries it once more. Settings › About › Version: for an application control policy, "0.1.x didn't start on this computer: Windows' Smart App Control (or another app control policy) blocked the new version, which isn't signed. magpie stays on 0.1.y. Download it and run it yourself, or turn Smart App Control off and check again", else the reason, with Download and Check; the error in full on hover. en/zh/ja/de. A user who double-clicks the refused exe is refused by Windows before magpie runs; only signing helps there. TestRelaunchRefusedPutsBack (4551 at the start), TestRelaunchSilentPutsBack (no word in time, killed; quit at once), TestRelaunchReportedStays, TestReportStarted, TestBlockedNote and TestUpdateThatWontStart (the restart doesn't quit, the note, the clock not downloading it again, a Check that does) are new and fail without the change; on Windows TestInstallBinaryRefusedStaysOut, TestTrialStart and TestStartCheckedPutsBackForReal (real processes: not a program, quits at once, runs on silent) pass on the Windows 11 test box with the whole internal/update package (Smart App Control itself isn't on there, so the refusal is simulated). update-blocked.test.cjs (Chromium and WebKit, en and zh: the reason and both versions, full error on hover, Download opens the release, Check downloads, the other reason, no select, no scroll, no left border) fails without the change; update-check, update-mirror, update-auto, update-wait and update-download-percent pass in WebKit; click-scroll's request-list case fails now and then the same on main. go vet, go build (darwin, GOOS=linux nogui, GOOS=windows) and go test -tags nogui ./... under a temp HOME pass.