Files
gateway: a gateway key can be held to some accounts (#905, LCorleone) — a key could burn any account a provider had: two coding plans of one vendor, and a key given to someone had them both. A key now keeps an optional list of accounts and keys (caller-keys.json "accounts", none = every account, so older files read as before): "<provider>/<account>", an account by its stable id — the login's, set once it is written and kept through renames (logins.json's savedLogin id, written by every write and stood in for by one made of the name until then, so an entry made before stands), a key by its fingerprint — every one a provider has when the list is set, and one gone later (a key rotated, an account signed out) kept as it is, matching nothing, holding the key closer, never wider. The command line and the menu name an account by who is signed in and keep its id. Routing holds a plan to the key's accounts beside its models: a candidate whose account the key may not use is left out — through a routing group the key names no less, for naming a group is not naming its accounts — the plan's order keeping step and its left-out list marking what the key held out (Weighed's held); magpie's own calls for the key — a web search, a picture described, a Codex title — are of the models the user picked, so the models don't hold them, but their spend lands on an account all the same, and the accounts hold them too. When every account or key a model has is one the key may not use, the request is refused with a 403 in the API's own error shape naming the model and the accounts the key may use, logged as a rejected call: after a usage cap's 429 and before an account barred for the model's, itself before the no-member 404. /v1/models and the Anthropic and Gemini lists show the key only the models an account or key it may use serves (a provider's account set not to serve one, #474, is none of the key's); a provider with no account and no key (a local ollama) is one the key names no account of — its models stay listed and its asks pass — while an entry kept after its provider lost its last account or key serves none, holding the key closer. The embeddings, images and videos endpoints hold their candidate to the key's accounts the same way, and counting tokens counts on those alone (the local estimate where it may use none). System One asks its decision model of an account or key the key may use — and sends on that key, of those in use, not the provider's first, which a key held to a later one was spent on; the videos and images keyed paths send on one the key may use too, held to a later key no longer refused for the first alone. Set it in the app's menu the models are picked in — the accounts after the models, an account by who is signed in with their plan in the note, a key by its fingerprint, what the CLI kept that no provider has now kept as "Not signed in now" — or
magpie gateway-key accounts <id> codex/me@example.com openai/<key id> (all takes it off); gateway-key list has an ACCOUNTS column. en/zh/ja/de, the German "{n} accounts" main already had left to it rather than written twice. The images API's fan-out over a subscription's accounts is held to the key's accounts as one: one of the key's whose plan refuses to draw no longer hands the pen to an account it may not use, and the second in the order, when it is the key's, draws what the first may not (drawOnAccounts filters the candidates, the account pinned to with the account header told plainly when the key may not use it). The Routing view's left-out list says an account the key held out for what it is, not that its plan lacks the model, in Chinese, Japanese and German too; the key's badge names its accounts by who is signed in, not by the stable ids it keeps. (#932)
gateway: a request a proxy or tunnel on this computer forwards to loopback is answered as one from another machine, and needs the gateway key (#1022, znjhahaha: asked for a Cloudflare Tunnel in magpie, with the gateway key checked for requests through it) — local() was the request's address alone, so cloudflared, ngrok, Tailscale serve or funnel, frp's HTTP proxy, or a Caddy or nginx here, all reaching 127.0.0.1:3425 for clients anywhere, were taken for this computer's agents: whoever had the tunnel's address used the user's subscriptions with any token, read /v1/magpie/quotas (the accounts and their balances) and the remote cards, and their usage was counted as local, all without sharing on. reference.md said a proxy here had to sign its clients in itself; a quick tunnel signs in nobody. A request on loopback carrying a forwarding header (Forwarded, X-Forwarded-For, X-Real-IP, Cf-Connecting-IP, True-Client-IP, Tailscale-User-Login), which no agent sends, is now proxied, and local() is false for it, so every check built on local() takes it as from the network: lanGuard refuses it while magpie isn't shared, saying it came through a proxy or tunnel and that Settings → Share on local network and a gateway key let it in; shared, it needs an enabled gateway key and is then sharedWith, as a LAN client is, for the model APIs, quotas, remote cards, concurrency, session route and MCP sign-ins; the Omarchy theme and the Codex title link stay this machine's only, and its usage is no longer marked local. A gateway MAGPIE_ADDR opens to the network stays open to a tunnel as to anyone; one MAGPIE_ADDR keeps on loopback is closed to it. MAGPIE_TRUST_PROXY=1 says a proxy here signs its clients in itself and keeps what it forwards local, as before (listed in agentenv_test's notAnAgent). A tunnel forwarding bare TCP (frp tcp, ssh -R, socat) adds no header and can't be told from an agent: reference.md says to point one at the LAN address with sharing on. Running cloudflared from magpie with a pasted Tunnel Token is not done here: magpie would have to download or run a program it doesn't ship; reference.md says how to serve magpie through a Cloudflare Tunnel today. TestProxiedLoopbackNeedsTheKey (from 127.0.0.1 with cloudflared's, ngrok's, nginx's, RFC 7239's, Akamai's and Tailscale serve's headers: refused unshared, 401 shared without a key, with magpie's token or a wrong key, let in with the key under its identity, a disabled key refused; an agent on loopback let in with any token; MAGPIE_TRUST_PROXY; MAGPIE_ADDR on loopback and open) and TestProxiedLoopbackRoutes (quotas, quotas history, models, Omarchy and /mcp through a tunnel, unshared and shared, with and without the key) fail on main (200 for every tunnel unshared) and pass -race -count=10 with TestLANGuard, TestQuotasOverLAN, TestManagedLANGuard and TestExplicitOpenGatewayAcceptsAllRemoteTokens; go test -race -tags nogui ./internal/gateway (579 s, under -timeout 30m) and go test -tags nogui ./internal/... under a temp HOME pass; go vet (darwin, windows), gofmt -l, linux and windows (nogui) builds pass. No GUI change. Not tried with a real cloudflared (not installed here); its Cf-Connecting-IP and X-Forwarded-For are what Cloudflare documents it sends.
appdir: a folder named data in /Applications isn't taken for magpie's portable data while ~/.config/magpie has its files (#989, Treyang: after updating 0.1.1077 → 0.1.1078 magpie opened with no providers, sign-ins or settings; ~/.config/magpie was intact and /Applications/data had appeared) — portable mode (#508) took any folder named data beside the folder holding Magpie.app, and on a Mac that folder is /Applications, which every app shares. The reporter's /Applications/data began with nodes/, providers/, tools/, skills/ and an initialized marker, none of which magpie makes (magpie keeps providers.json, not a providers folder); magpie's cli-identity.json, install-id and last-version came after, written once a magpie had taken the folder for its own. The updater didn't make it: Stage only unpacks and codesign-checks the new app in .magpie-update, Install renames, Relaunch opens the bundle, and nothing runs the staged copy; appdir never creates a data folder without a .portable marker either. So a folder another app left there turned an installed magpie portable and empty at its next start. Now, in /Applications or ~/Applications, a data folder is magpie's portable data only while the installed folder (~/.config/magpie, or $XDG_CONFIG_HOME/magpie) holds no providers.json, logins.json or settings.json — one who keeps magpie portable there never ran it installed, and keeps it — or with a .portable marker beside the app, which makes it portable as before. A data folder beside magpie anywhere else (a USB stick, a Windows or Linux folder of its own, an AppImage's) is taken as before. A user already hit is back on ~/.config/magpie at the next start, and the data folder is left as it is, never deleted or moved; the log says it was passed over. TestResolveSharedFolder (/Applications stood in by a temp folder: a data folder with nodes/ and install-id, installed state present/absent, .portable, a data folder on a folder of its own) fails without the change (Resolve and Config give the shared data folder) and passes; TestSharedFolderIsApplications pins which folders count. go test -race ./internal/appdir/ (-count=3), go test ./internal/settings/ ./internal/shortcut/ ./internal/update/, go vet (darwin, windows), darwin/linux(nogui)/windows builds pass. Not tried with the reporter's /Applications or the updater on a real install.
settings: keep awake can keep the display on too (#975, Hu9956: magpie's keep-awake held the system up, but the screen still went black and locked, and their agents record the screen to check their work, which a locked screen stops) — Settings › General's "Keep awake while agents work" row now has three segments, Off / On / Screen on too, saved as keepAwake and the new keepAwakeDisplay (default off, so On and every existing settings.json behave as before). awake.Keep takes a Level (Off, System, Display) in place of a bool; a level changed while held releases and takes the hold anew. On a Mac the display level runs
caffeinate -i -d -w <pid> (pmset -g assertions on a caffeinate of the same flags shows PreventUserIdleDisplaySleep beside PreventUserIdleSystemSleep); on Windows SetThreadExecutionState gets ES_DISPLAY_REQUIRED too; on Linux logind's idle inhibitor (systemd-inhibit --what=idle:sleep, unchanged) is all there is, and whether the screen stays on is up to the desktop. keepAwakeDisplay is this computer's own (KeepOwn) and goes in prefsKeep, so another setting's save keeps it. The row's line says the display stays on when it's chosen; it keeps its English in data-en, so a language change re-translates it. zh/ja/de strings added. No TUI or CLI has keep-awake, so none changed. TestKeepDisplay (taken once, taken anew with the display, not again at the same level, back to System, released at Off), TestCaffeinateArgs, TestExecutionState (GOOS=windows test build) and TestKeepAwakeKeptAndOwn cover it; go test ./internal/awake ./internal/settings ./internal/gui ./internal/gateway pass, -race -count=5 on awake. keep-awake.test.cjs now runs en/zh/ja/de at 900px and 440px in Chromium and WebKit (display posts keepAwake+keepAwakeDisplay true, the line changes, another setting's save keeps it, segments fit the row, On and Off clear it, no click moves the page): 32/32 twice; gui-ja, gui-de, i18n-keys and click-scroll pass. go vet (darwin, windows, linux), darwin, linux (nogui) and windows builds pass. Not tried with an agent recording the screen; the reporter is asked to confirm.
…
codexcat, gateway, gui: Settings › Codex auto-review model picks the model Codex's auto-review decides approvals on (#938, guitaoliu)
davsync, agent: sync turned off and on again to the same server keeps what changed here, and an agent shown only routing groups connects on the first (#939, aiirobyte)
…
…
gateway: a request a proxy or tunnel on this computer forwards to loopback is answered as one from another machine, and needs the gateway key (#1022, znjhahaha: asked for a Cloudflare Tunnel in magpie, with the gateway key checked for requests through it) — local() was the request's address alone, so cloudflared, ngrok, Tailscale serve or funnel, frp's HTTP proxy, or a Caddy or nginx here, all reaching 127.0.0.1:3425 for clients anywhere, were taken for this computer's agents: whoever had the tunnel's address used the user's subscriptions with any token, read /v1/magpie/quotas (the accounts and their balances) and the remote cards, and their usage was counted as local, all without sharing on. reference.md said a proxy here had to sign its clients in itself; a quick tunnel signs in nobody. A request on loopback carrying a forwarding header (Forwarded, X-Forwarded-For, X-Real-IP, Cf-Connecting-IP, True-Client-IP, Tailscale-User-Login), which no agent sends, is now proxied, and local() is false for it, so every check built on local() takes it as from the network: lanGuard refuses it while magpie isn't shared, saying it came through a proxy or tunnel and that Settings → Share on local network and a gateway key let it in; shared, it needs an enabled gateway key and is then sharedWith, as a LAN client is, for the model APIs, quotas, remote cards, concurrency, session route and MCP sign-ins; the Omarchy theme and the Codex title link stay this machine's only, and its usage is no longer marked local. A gateway MAGPIE_ADDR opens to the network stays open to a tunnel as to anyone; one MAGPIE_ADDR keeps on loopback is closed to it. MAGPIE_TRUST_PROXY=1 says a proxy here signs its clients in itself and keeps what it forwards local, as before (listed in agentenv_test's notAnAgent). A tunnel forwarding bare TCP (frp tcp, ssh -R, socat) adds no header and can't be told from an agent: reference.md says to point one at the LAN address with sharing on. Running cloudflared from magpie with a pasted Tunnel Token is not done here: magpie would have to download or run a program it doesn't ship; reference.md says how to serve magpie through a Cloudflare Tunnel today. TestProxiedLoopbackNeedsTheKey (from 127.0.0.1 with cloudflared's, ngrok's, nginx's, RFC 7239's, Akamai's and Tailscale serve's headers: refused unshared, 401 shared without a key, with magpie's token or a wrong key, let in with the key under its identity, a disabled key refused; an agent on loopback let in with any token; MAGPIE_TRUST_PROXY; MAGPIE_ADDR on loopback and open) and TestProxiedLoopbackRoutes (quotas, quotas history, models, Omarchy and /mcp through a tunnel, unshared and shared, with and without the key) fail on main (200 for every tunnel unshared) and pass -race -count=10 with TestLANGuard, TestQuotasOverLAN, TestManagedLANGuard and TestExplicitOpenGatewayAcceptsAllRemoteTokens; go test -race -tags nogui ./internal/gateway (579 s, under -timeout 30m) and go test -tags nogui ./internal/... under a temp HOME pass; go vet (darwin, windows), gofmt -l, linux and windows (nogui) builds pass. No GUI change. Not tried with a real cloudflared (not installed here); its Cf-Connecting-IP and X-Forwarded-For are what Cloudflare documents it sends.
gateway, provider: the Routing page says what a lone key or account has left, and a key bought as a plan (Kimi Code, GLM Coding Plan, MiniMax, OpenCode Go, Command Code) is weighed by its plan's windows (#1016, gphysign: Kimi Code (China) with one key on, Smart routing; the card said only "Kimi Code (China) 只启用了一个 Key,无需选择。" while /v1/magpie/quotas had its 5 hours 49 used, 51 left and its 7 days 45 used) — two gaps. weighRouted returned before reading anyone's allowance when there was one candidate, so a lone key or a lone subscription account was never known in the trace, and the Routing page, which shows a bar and "{n} used · renews in {d}" for a known row, had nothing to show; every single-account subscription said "what's left not known yet" the same way. And KeyAllowance read only a sub2api key's limits, so a key whose plan the Usage page reads (planQuotaSourceOf: Kimi Code, GLM Coding Plan and Z.ai, MiniMax Coding Plan, OpenCode Go, Command Code) was never known to routing, with one key or several. weighRouted now reads what each candidate has left before returning a lone one (one alone is still weighed against nobody). readsKeyWindows takes a plan key too; keyWindows reads it through planKeyWindows (planWindows, with Zhipu's team quota behind a key that has none of its own, as PlanQuotas does), and PlanQuotas' own read of a card is noted for routing at once (noteKeyAllowance), so the Usage page and routing never ask twice; the card kept on disk seeds it after a restart (keyCardTag keys a plan card as plan:, a sub2api one as balance:). A key at such a vendor that turns out to be pay-as-you-go, with no windows, is asked again every 30 minutes (keyNoWindowsAge), not every minute it is used. So, beyond the issue: several plan keys on one provider are now weighed by their windows under Smart, usage and pace, and a plan key the vendor refuses as out of quota rests until its window renews and comes back once read short of full, as a sub2api key does. affine no longer says a lone key or account all but used up "moved" its conversation: there is nobody to move to (at > 0). The GUI needed no change. TestLoneOneTellsWhatItHasLeft (a lone Kimi key known at 49% with both renews, the week first; a lone Codex account known; a plain DeepSeek key not), TestLoneSpentKeyKeepsItsConversation (fails with the at > 0 guard taken out), TestPlanKeyAllowance (the windows read behind the request, asked once; DeepSeek and Moonshot keys never asked), TestPlanKeyAllowanceFromItsCard (PlanQuotas' read known at once without a second ask; after a restart with the vendor down, the card on disk stands in and a failed read keeps it) and TestPayAsYouGoKeyAskedSeldom (a GLM pay-as-you-go key asked once, not two minutes on, again 31 minutes on) all fail without the change and pass -race -count=20 (provider) / -count=10 (gateway). routing-key-windows.test.cjs gains a lone Kimi Code key at 1100px and 440px (the one-key story, the row's bar at 49%, its provider row "49% used · renews in …" fitting, no sideways scroll) and passes in full on Chromium and WebKit in en, zh, ja and de; routing-quota-left and panel-routing pass. go test -tags nogui ./internal/..., go test -race on ./internal/provider and ./internal/gateway in full, go vet, gofmt -l, darwin, linux and windows (nogui) builds pass. Not tried with a real Kimi Code key. StepFun's Step Plan is read through a sign-in, not the key, so it isn't one of these. docs/subsystems/gateway-routing.md, providers-accounts.md and docs/reference.md say it.
library, imagemcp: magpie's own image server is given to agents in WSL, started through WSL's interop with the paths crossing over (#900, cjyrainbow) — on Windows the Library names magpie-image by this machine's binary ('D:\tools\Magpie\magpie-windows-amd64.exe' mcp image), and turning it on for "Codex · WSL Ubuntu" or "Claude Code · WSL Ubuntu" failed: a Windows program was refused for every agent in WSL. WSL's interop starts a Windows .exe from inside the distro, so magpie's own server (args mcp image, whatever its binary) is now written for an agent in WSL by the binary's path in the distro, asked of the distro itself (wslpath -u, so a custom automount root is followed; asked once in ten minutes; /mnt/<drive>/ when it can't be asked), with --wsl <distro>, --mount where the drives aren't under /mnt/, --home the distro's $HOME, and WSLENV naming the variables it is given (MAGPIE_ADDR), which a Windows program otherwise doesn't get. A distro whose interop is off (no WSLInterop in binfmt_misc) is told so: turn it on in /etc/wsl.conf. A user's own Windows program is still refused, as before: its paths and variables wouldn't cross over. A Linux magpie in the distro isn't looked for: the Windows one reaches the gateway at 127.0.0.1 and reads this machine's settings, the same version as the app, which a Linux one would need the gateway's address from inside WSL for. With --wsl, magpie mcp image opens the paths the agent gives (its roots, reference images, where to save, ~/) as the distro's — /home/me/x through \\wsl.localhost\<distro>\home\me\x (\\wsl$\ where that isn't), /mnt/c/x as C:\x — answers the saved paths as the distro's (/home/me/app/generated-images/…, /mnt/c/…), and with no project falls back to the distro's ~/Pictures/Magpie. Tested on the Windows 11 test box: in a WSL2 Ubuntu 24.04 distro the library's probe gave /mnt/c/…/magpie.exe and interop:on, and this build, started from the distro as the library writes it (the .exe's /mnt/c path, mcp image --wsl, MAGPIE_GATEWAY through WSLENV to a stand-in gateway in the distro), answered initialize and generate_image with a root of /home/yeton/m900proj: a relative path and an absolute reference image (sent as /v1/images/edits), ~/m900proj/home.png and /mnt/c/… were each saved there and answered as the distro's paths. TestWSLMagpieImage (the /mnt/d command, --wsl, WSLENV, nothing rewritten or asked again at the next sync, a custom mount, interop off as a problem) and TestLinuxHome in internal/library, TestWSLPaths in internal/imagemcp are new and fail without the change; TestWSLServers (npx.cmd still refused) passes. go vet, go build (darwin, GOOS=linux nogui, GOOS=windows) and go test -tags nogui ./... under a temp HOME pass (internal/provider failed once under load and passes alone).
…
netproxy: a host on the user's own network goes around the proxy (#982, jiz4oh: a Remote magpie's model list failed with "no model list: http://server…:3425/v1/models: 502 Bad Gateway", and Test with "no model to try", while curl with the key got the list) — the 502 had no body, which magpie's own errors always carry, and the other magpie logged nothing for the refresh: the request went to the proxy magpie takes from its settings, the environment or the system (Clash and the like), which couldn't reach the LAN name and answered 502. netproxy.Func now sends a request directly when the global proxy would take it and the host is on the user's network: a private, link-local, loopback or 100.64/10 (Tailscale) address, a .local name, or a name whose every address is one of those (looked up for at most 400ms, kept a minute; a name that doesn't resolve still goes through the proxy). A provider's own proxy is still taken as it is. Models, chat, count_tokens and the shared quota cards to a Remote magpie all go through Func (http.DefaultClient and the gateway's transport), so all are covered; so is any other provider on the LAN (an Ollama on a NAS). TestLANModelListNotThroughProxy (a proxy that answers 502 to everything, set in magpie, and a fake remote magpie on this computer's LAN address) fails without the change with the reporter's bare "502 Bad Gateway" and passes; TestLANGoesAroundTheProxy (IPs, LAN/Tailscale names, a vendor, a provider's own proxy) fails without it. Not tried on the reporter's network; they're asked to confirm. go test ./internal/netproxy/ ./internal/catalog/ ./internal/provider/ ./internal/gateway/ ./internal/update/ ./internal/usage/ pass; -race -count=5 on the new tests; go vet, darwin/linux(nogui)/windows builds pass.
…
…
gateway, provider: the Routing page says what a lone key or account has left, and a key bought as a plan (Kimi Code, GLM Coding Plan, MiniMax, OpenCode Go, Command Code) is weighed by its plan's windows (#1016, gphysign: Kimi Code (China) with one key on, Smart routing; the card said only "Kimi Code (China) 只启用了一个 Key,无需选择。" while /v1/magpie/quotas had its 5 hours 49 used, 51 left and its 7 days 45 used) — two gaps. weighRouted returned before reading anyone's allowance when there was one candidate, so a lone key or a lone subscription account was never known in the trace, and the Routing page, which shows a bar and "{n} used · renews in {d}" for a known row, had nothing to show; every single-account subscription said "what's left not known yet" the same way. And KeyAllowance read only a sub2api key's limits, so a key whose plan the Usage page reads (planQuotaSourceOf: Kimi Code, GLM Coding Plan and Z.ai, MiniMax Coding Plan, OpenCode Go, Command Code) was never known to routing, with one key or several. weighRouted now reads what each candidate has left before returning a lone one (one alone is still weighed against nobody). readsKeyWindows takes a plan key too; keyWindows reads it through planKeyWindows (planWindows, with Zhipu's team quota behind a key that has none of its own, as PlanQuotas does), and PlanQuotas' own read of a card is noted for routing at once (noteKeyAllowance), so the Usage page and routing never ask twice; the card kept on disk seeds it after a restart (keyCardTag keys a plan card as plan:, a sub2api one as balance:). A key at such a vendor that turns out to be pay-as-you-go, with no windows, is asked again every 30 minutes (keyNoWindowsAge), not every minute it is used. So, beyond the issue: several plan keys on one provider are now weighed by their windows under Smart, usage and pace, and a plan key the vendor refuses as out of quota rests until its window renews and comes back once read short of full, as a sub2api key does. affine no longer says a lone key or account all but used up "moved" its conversation: there is nobody to move to (at > 0). The GUI needed no change. TestLoneOneTellsWhatItHasLeft (a lone Kimi key known at 49% with both renews, the week first; a lone Codex account known; a plain DeepSeek key not), TestLoneSpentKeyKeepsItsConversation (fails with the at > 0 guard taken out), TestPlanKeyAllowance (the windows read behind the request, asked once; DeepSeek and Moonshot keys never asked), TestPlanKeyAllowanceFromItsCard (PlanQuotas' read known at once without a second ask; after a restart with the vendor down, the card on disk stands in and a failed read keeps it) and TestPayAsYouGoKeyAskedSeldom (a GLM pay-as-you-go key asked once, not two minutes on, again 31 minutes on) all fail without the change and pass -race -count=20 (provider) / -count=10 (gateway). routing-key-windows.test.cjs gains a lone Kimi Code key at 1100px and 440px (the one-key story, the row's bar at 49%, its provider row "49% used · renews in …" fitting, no sideways scroll) and passes in full on Chromium and WebKit in en, zh, ja and de; routing-quota-left and panel-routing pass. go test -tags nogui ./internal/..., go test -race on ./internal/provider and ./internal/gateway in full, go vet, gofmt -l, darwin, linux and windows (nogui) builds pass. Not tried with a real Kimi Code key. StepFun's Step Plan is read through a sign-in, not the key, so it isn't one of these. docs/subsystems/gateway-routing.md, providers-accounts.md and docs/reference.md say it.
…
…
…
…
…
…
update, gui: Add to PATH on Windows writes a magpie.cmd beside an app not named magpie.exe, so the site's magpie-windows-amd64.exe runs as magpie in a new terminal, and says so (#942, mintonight)
upstream: the vendors' own status pages say when their API is down (#971, emo172: an Anthropic or OpenAI outage looked like a sign-in or quota problem) — a provider that calls Anthropic's, OpenAI's or Moonshot's API (by its base URL's host: api.anthropic.com, which the Claude subscription calls too; api.openai.com and chatgpt.com, Codex's; api.moonshot.cn/.ai) carries "API outage" (red), "API degraded" or "API maintenance" beside its name on its Providers row and its Usage card while the vendor's page says so; its title names the parts and the open incidents, and a click opens the incident (or the page), not the row. internal/upstream reads each page's Statuspage-style summary.json (status.claude.com; status.openai.com, incident.io's; status.moonshot.cn), checked by hand on 2026-10-06, and only the API's own parts (Claude API and Claude Code, not claude.ai; Chat Completions, Responses, Codex, Codex CLI, not "Codex in ChatGPT Desktop", which was degraded that day); an incident naming no parts (OpenAI's) counts only while those parts are affected. Pages are read only when the Providers or Usage page asks (GET /api/upstream), at most once in five minutes, never on a request's way; a page that fails, isn't JSON or no longer lists the API's parts is unknown and marks nothing. A relay serving the same models is never marked. DeepSeek's page (Flashduty) has nothing readable and is only known for its link; OpenRouter, xAI, Mistral and z.ai have no page read. The gateway's failure messages, TUI, CLI and tray aren't changed. zh/ja/de added. upstream_test (fake pages from the real summaries: API down, claude.ai down isn't the API, Claude Code degraded, OpenAI's app isn't the API, 503/HTML/changed page unknown, read once in Fresh and Of never waits, hosts; -race -count=5) and gui's TestUpstreamRoute pass; upstream-status (Anthropic down: its provider and the Claude subscription marked, relay/OpenAI/unread Moonshot not, click opens the incident not the row, all well marks nothing; 900 and 440px, the badge whole, the name giving way) passes in Chromium and WebKit, en and zh, and fails 8/8 without the app.js change. i18n-keys, gui-ja, gui-de, click-scroll, account-return, account-models, usage-keys-fold, deprecated-subscriptions and add-sheet pass; go vet, go test ./internal/gui/, linux/windows/nogui builds pass.
usage, gateway, gui: a reply that reasoned is timed by its answer, from its first text, not by all its output from its first token (tony on Discord #general: Usage › Requests read "gpt-6.1-sol 163 token/秒 · 8,036 个请求 · 首字 14 秒" and asked whether subagents were added up) — speed was every output token over the time after the first content (text, reasoning or a tool call). Output counts the reasoning, and a model whose reasoning is hidden (OpenAI's encrypted reasoning, a summary sent when it is done, Claude's omitted thinking) writes it before the stream shows anything, inside the TTFT; so a 2,000-token think and a 200-token answer over the 1.5 s after the first token read 2,200 in 1.5 s, 1,467 tok/s. Concurrency was not the cause: the aggregate is Σ tokens over Σ per-request windows, so subagents running at once never add up. In the owner's own usage.jsonl codex/gpt-6-luna turns read up to 600 tok/s (out 496, reasoning 224, 11,055 ms, ttft 10,228) and its aggregate 133; it is 80 now. usage.DecodeOf is the one rule: a reply with reasoning counts out − reasoning over ms − firstText, one that reasoned and wrote only tool calls tells no speed, one without reasoning is timed from its TTFT as before; DecodeWindow's burst cut (#731) still applies. Record.Decode feeds summarize, the ledger's Totals and its timeline Parts; a route keeps its reply's reasoning (Route.Reasoning, from gateway.go and codex_backend.go), and routing.js's decodeOf/speedOf times its rows, the story and a session's average by it (an older route takes its served usage's reasoning); app.js's ledDecode does the Usage page's rows, whose tooltip says the reasoning left out, and the Output speed help says the rule, in en, zh, ja and de. History is read the same way, as it keeps the reasoning and the first text. Routing never ranks by speed. TestReasoningReplyTimedByItsAnswer (the 2200/2000 turn, a summary 10 ms before the text, a concurrent subagent, tool calls only, reasoning over output, no reasoning, not streamed: 1,200 tokens in 11 s through summarize, addRow, LedgerSeries and Decode) fails without the change and passes -race -count=10; TestUsageKeepsTimeToFirstToken now has its paced replies report reasoning and checks the route keeps it. reasoning-speed.test.cjs (Routing rows 133, 100, none and 100, the story never 1,467, the session 105; the Usage rows, tooltip and KPI the same; no sideways scroll) passes on Chromium and WebKit in en, zh, ja and de, and fails against the old assets (1467 tok/s); routing-request-metrics, routing-burst-speed, usage-speed, usage-ledger-detail, i18n-keys, gui-ja, gui-de, ledger-hscroll and click-scroll pass on both. go test -race -tags nogui ./internal/... under a temp HOME passes but internal/proc's TestUserPath ("the login shell's PATH never came"), which failed under the full run's load, passes 3/3 alone and is not touched here; go vet, gofmt -l, linux and windows (nogui) builds pass. Not tried with tony's data. docs/reference.md says how output speed is counted.
…