Commit Graph

  • f55747ce82 agents: Claude Desktop's tiers each take a model of their own (WilianWeng on Discord: desktop中调用子agent时,claude无法识别模型角色) — Desktop's Code tab runs Claude Code with every ANTHROPIC_DEFAULT_<TIER>_MODEL set to "" unless the gateway's /v1/models tags a model with anthropic_family_tier (shortnameIdentityOverrides in its app.asar, 2.7032), so a subagent on sonnet or haiku asked for Claude Code's own claude-sonnet-…, which magpie doesn't serve, and ran on the chat's model. Claude Desktop now has opus, sonnet, haiku and fable pickers (the tiers' square, as Claude Code's), kept in claude-desktop.tiers.json: the model picked is tagged with its tier and is_family_default, a Claude model magpie serves is tagged with its own tier while that tier has none picked, and a request for Claude Code's own id of a tier no row is tagged with (a model picked for two tiers carries the first) goes to the tier's pick. The tiers' square and picker work without a model field to follow (Not set: a Claude model of the tier, else the chat's model). Desktop reads /v1/models at start-up. TestClaudeDesktopTiers (gateway, agent) and desktop-tiers.test.cjs (fails without the app.js change); not tried against a real Claude Desktop. go vet ./internal/..., go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui -count=1 ./... under a temp HOME pass. yetone 2026-10-03 03:57:24 +08:00
  • 94cadbc34d fix(factory): open other agents' Factory requests as Droid (#519) Davy 2026-10-02 12:39:54 -07:00
  • f0e4a79c06 plugins: auth.refresh — magpie renews a plugin's sign-ins before they run out, once per account at a time (owner: 按你建议的优先级,先做 1 和 2, from the pi plugin study) — each community plugin renewed its own tokens inside fetch, deduping by hand (factory's "two refreshes would spend it twice", minimax's renewing, cline's REFRESH_LEAD, workbuddy's EARLY_MS). A plugin can now give auth.refresh(auth, provider), returning the fields to keep over the old sign-in, and optionally auth.refreshLead (ms, 5 minutes by default). Before an account's loader options, provider.models and auth.usage, the host renews an OAuth account whose expires is that close: requests that find it due share one renewal (vendors that spend a refresh token once never see it twice), waiting up to 15s, and a renewal that takes longer is still saved when it ends (pending keeps the host up). What it gives is merged over the account and saved through setAuth, which clears the loader caches here and in Go, and the account is said renewed; a sign-in replaced or removed meanwhile stands. A throw leaves the account as it was and the request goes with the old token; signIn: "expired" on it marks the account and it isn't tried again until signed in again, any other error is retried after 30s. OpenCode ignores the hook, so plugins keep their in-fetch check for it. Documented in /docs/plugins (#renew, en and zh). TestPluginRenewsSignIns fails without the host change (no renewal). go vet ./internal/..., go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui -count=1 ./... under a temp HOME pass. yetone 2026-10-03 03:08:11 +08:00
  • 90d9d40dcf gui: a menu-bar quota picker taller than the window is clamped and scrolls (#603) 尔立 2026-10-03 03:03:25 +08:00
  • 8c7791c071 fix(provider): use message-array input for Responses probes (#605) Snowyy 2026-10-03 03:02:46 +08:00
  • 20c5fa6fd9 agents: Grok Build's model picker folds magpie's rows for the account Grok Build is signed in to (Fate on Discord: grokbuild 在登录态下会加载重复的模型) — signed in, the picker listed Grok Build's own models and then the same models again as the Grok (SuperGrok) subscription via magpie. They are marked Same, as Claude Code's are (#496), so the picker folds them into one row a click opens. viaMagpie also counts as the agent's own a plugin's account for the agent's vendor (Grok moved onto its plugin) when it is the user the agent is signed in to (provider.AgentUser: Grok Build's auth.json). TestGrokFoldsItsOwnAccount fails without it (magpie/grok/grok-4.7 unfolded); the plugin path is not covered by a test. yetone 2026-10-03 02:58:44 +08:00
  • 1e80a84fe3 provider: a Codex warm-up asks the account's cheapest model (#604) — warmModel took a model with mini in its id or else the first listed, and a GPT-6 account lists no mini and starts with the flagship gpt-6.1-sol. warmPick takes the cheapest by list price (the model's own, else models.dev's openai price), and with no price known a mini, then a luna (GPT-6's cheap one), then the first. TestWarmPick. yetone 2026-10-03 02:53:20 +08:00
  • 5154df624f gui: the Routing page's accounts say how much is left when Settings' allowance display is Left (#602) — the stage's rows and the list of what each account did said "{n} used" whatever quotaLeft was, while the Usage page, panel and menu bar followed it. routing.js now words a window, and fills its bar, with quotaLeft as app.js's quotaText/quotaFill do ("{n} left · kept for last", "剩余 {n} · {d}后重置", …); the routing explanations ("the most of its allowance left — {n} used") are unchanged. routing-quota-left.test.cjs (en/zh, Left and Used) passes in WebKit. yetone 2026-10-03 02:53:20 +08:00
  • dfc074afdb agents: Disconnect from magpie takes out everything magpie wrote into an agent's config and puts back what it had before (Fate on Discord: 移除由Magpie写入的配置,也就是重置功能,这个好像不是很明显。初步体验好像是选择默认就会清除写入的配置吗?希望能有一个更直接的交互动作,来代表移除由Magpie写入的配置。) — there was no one action for it, and picking Default isn't it: Default is the agent as installed, and for Codex, Claude Code and Gemini CLI it also forgets the stash, so the user's own provider or relay (model_provider + its table, ANTHROPIC_BASE_URL/AUTH_TOKEN, GOOGLE_GEMINI_BASE_URL/GEMINI_API_KEY and sign-in) was lost rather than put back. agent.Disconnect reuses each agent's own restore path: a new Agent.Unwire, for those three, runs the unroute their own-model pick already used (codex.go and claude.go have it pulled out of set as unroute; gemini.go's was there) and then forgets the stash, and every other field magpie set — each one reading a magpie value, or the value applied.json recorded and Unwire left — is set to "" through its own Set, which for every other agent unstashes what it had. applied.json forgets the agent (Keep), so no drift says it was switched off magpie. Agent.Wired (magpie or a magpie model in any field) goes to the GUI as agentJSON.wired; an agent magpie isn't in is left byte for byte. POST /api/agents/disconnect/{id} runs it under edit.Atomically as Sync and the fields' Sets are. yetone 2026-10-03 02:40:15 +08:00
  • 9848a9062a library: RTK says when rtk isn't on the PATH the agents get, and puts it there when asked (#601, Moody-Sin: magpie好像没有正确安装RTK — Pi said "RTK disabled: rtk binary not found in PATH" while the Library showed RTK working) — rtk's hooks run it by name (Pi's extension probes rtk --version and turns itself off when that fails; Claude Code's and Codex's are rtk hook …; the commands they rewrite to are rtk git status and so on), but magpie looked for rtk in more places than an agent's PATH: ~/.local/bin and ~/.cargo/bin, which the app adds to its own PATH whether the user's shell has them or not (rtk's own install.sh puts it in ~/.local/bin and only warns when that isn't on PATH), and WinGet\Links on Windows, which winget doesn't always link into. So rtk was "installed", the agents were switched on with it by its whole path, and none of them could run it. Now the PATH a terminal opened now has is read — the login shell's (proc.LoginPath, the one UserPath already asks for, kept a minute) or on Windows the registry's (moved from provider's registryPath) — and when rtk isn't in it the RTK card says so, with what Put RTK on PATH will do, and each agent that has rtk is tagged Not on PATH (不在 PATH 中). Put RTK on PATH (magpie library rtk path) links rtk into the first of ~/.local/bin, ~/bin, /opt/homebrew/bin, /usr/local/bin, linuxbrew's bin, ~/.cargo/bin that is on that PATH and writable, or on Windows adds rtk's own folder to the user's PATH in the registry (a link needs Developer Mode, and a copy is left behind when winget upgrades rtk; winget's package folder doesn't change across upgrades) and tells Explorer; with no such folder it says which folder to add to PATH in the shell profile — shell profiles are never edited. rtk is also found in winget's package folder (WinGet\Packages\rtk-ai.rtk_*), and rtk's script upgrades a linked rtk where the link points, not over the link. TestRTKOffPath and library-rtk-path.test.cjs (en, zh) fail without the change. go vet ./internal/..., go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build and vet, go test -tags nogui -count=1 ./... under a temp HOME pass but the known TestFXInState flake. The Windows registry path is built, not run on Windows. yetone 2026-10-03 02:34:08 +08:00
  • 4b5d6d725f site: the home page in Japanese at /ja/ too, picked from a language menu (the owner: 官网的日文也加上) — i18n.js gains a ja dictionary for every marked string, LANGS saying which languages have docs of their own (zh; /ja/ links the English docs); / sends a browser whose Accept-Language prefers Japanese to /ja/ as it does Chinese to /zh/, until a language is picked. The nav's 中文/EN link is now a globe button opening a menu of English, 中文 and 日本語, the current one checked (Escape or a click outside closes it), with hreflang ja alternates in the head; the page's scripts take T(en, zh, ja), so the routing demo, copy button, download label and theme titles speak Japanese on it, and Japanese text is set in Japanese faces (Hiragino Sans, Yu Gothic, Noto Sans CJK JP) rather than the Chinese ones. Also fixes the routing demo's stat labels in every language: since 8efd7d8 wrapped each in its own span, .rt-stats span matched the inner ones too and the labels were written into the wrong places ("errors your agent saw" twice); it now takes .rt-stats > span. Checked in WebKit: every demo string has three languages, each tab's labels are right in en/zh/ja, the menu marks and switches the language and sets the cookie, and /, /zh/ and /ja/ don't scroll sideways at 320, 390 or 768. worker.test.mjs covers the ja dictionary and the ja redirects. yetone 2026-10-03 02:35:59 +08:00
  • 5422306fac usage: guard cache transitions and Windows long-path reads (#598) Snowyy 2026-10-03 02:19:11 +08:00
  • dab594714f sessions: preserve symlinks when moving across volumes (#600) Zhizhen He 2026-10-03 02:17:57 +08:00
  • 4160f464aa groups: a routing group whose member's reasoning levels nothing knows offers its other members' levels, and Pi's thinking field offers and shows what Pi runs the model at (Moody-Sin, #597: 路由组设了推理强度最高是否正确应用?) — Pi on group/deepseek (腾讯DeepSeek) at magpie's 最高 listed off alone in its /thinking, and sent no reasoning_effort: Pi clamps defaultThinkingLevel max to the off it offers, so the group's members were asked at the vendor's default, not max. yetone 2026-10-03 02:08:23 +08:00
  • 7547dfb3c9 library: a skill in the library's own folder is never copied onto itself, is shown and brought in where it is, and one kept in ~/.agents/skills isn't linked a second time into the agents that read that folder (#595) yetone 2026-10-03 01:55:16 +08:00
  • a8309d5ca4 site: a plugin developer guide at /docs/plugins and /docs/zh/plugins — how magpie runs OpenCode plugins, the hooks it calls (config, auth methods and prompts, loader and fetch, provider.models, chat.headers, magpie's auth.usage), the API each AI SDK package maps to, accounts and their scope, X-Magpie-Sign-In, provider ids, testing in a sandbox, publishing and the registry, and the host protocol. The Acme example on it was run against v0.1.676 with a stand-in API (plugin add, login, provider test, quota). Every docs page links it; inline code wraps on phones so no page scrolls sideways. yetone 2026-10-03 01:59:42 +08:00
  • 6e6338f8d6 agent: configure Zed models and gateway credentials automatically (#584) XYenonandAmp 2026-10-03 01:51:53 +08:00
  • 12cc2089e4 agents: each magpie model in T3 Code has a Reasoning pick of the levels it takes (KevinXC on Discord: 加进去的那些模型没有任何 reason 和 thinking 的设置,导致现在所有的模型都默认是 medium) — magpie's custom models in T3 Code's magpie instance were a slug and a name, and T3 shows a custom model with no capabilities with no options and sends Claude Code no effort. T3 Code's custom model entry takes capabilities.optionDescriptors (packages/contracts model.ts CustomModelEntry), which replace the driver's default in its picker, and its Claude adapter hands the effort chosen to the Agent SDK as it is for a custom model (no effortMap), which Claude Code sends to the gateway. Each model now gets an "effort" select labelled Reasoning, as T3's own Claude models have it, of the levels it takes that Claude Code can send (low, medium, high, xhigh, max, within claudeEffortsFor for Claude's own models), medium chosen first, else the lowest; a model with none (or only none/minimal) gets no pick. TestT3CodeEfforts is new, and t3Format now decodes a custom model's capabilities strictly as T3's schema has them (one default per select). Not run against a live T3 Code. go vet ./internal/..., go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui -count=1 ./... under a temp HOME pass. yetone 2026-10-03 01:47:42 +08:00
  • 38b4a9d984 feat: export gateway and agent conversation waterfalls to Langfuse (#581) mofelee 2026-10-03 01:43:32 +08:00
  • 24bd8aa1ae gateway: DashScope's glm-5.3 cannot think with it off either, in its own words, so the request magpie turned thinking off for is asked again with it left to the model (#596) Gabi 2026-10-03 01:40:40 +08:00
  • d49d889cd7 gui: a provider's Fallback picker says how many more models match past the dozen it draws, and that typing narrows them (01huadalang on Discord: 这个备用模型的来源是哪里,怎么只有这么几个,是否可以选择供应商里的别的模型,例如 workbuddy 的模型) — focusing the picker drew the first 12 of every on model of every ready provider and nothing else, so the rest (a WorkBuddy model among them) looked missing. Past 12 it now ends with "{n} more — type to narrow" (还有 {n} 项 — 输入以筛选), the string the other pickers use. fallback-more.test.cjs is new (a provider beside one with 20 models: 12 chips and "8 more", a typed filter finds the 17th, the note gone; en and zh): it fails without the change and passes with it in WebKit; click-scroll passes. yetone 2026-10-03 01:40:30 +08:00
  • 3d8c6f24c5 usage: capture Windows change time for cached logs (#594) Snowyy 2026-10-03 01:15:29 +08:00
  • 6d7bd11d07 edit: validate nested YAML input and output (#591) Zhizhen He 2026-10-03 01:15:14 +08:00
  • 8efd7d8027 site: the home page in Chinese at /zh/, and made for phones (the owner: magpie官网加上多语言支持,另外官网要对移动端友好) — index.html stays the one English source, each string marked data-i18n / data-i18n-attr; the worker serves /zh/ from it with i18n.js's dictionary put in by HTMLRewriter (<html lang>, docs links to /docs/zh/…, canonical and hreflang alternates), and / sends a browser whose Accept-Language prefers Chinese there until a language is picked with the nav's 中文/EN link (the lang cookie; Vary: Accept-Language, Cookie). The routing demo, copy button, download label and theme titles speak Chinese on it; the zh docs' home links go to /zh/. On phones the nav's links fold into a menu (≤860px; Escape, a click outside or on a link closes it, aria-expanded kept), Discord and GitHub move into it under 560px, the footer wraps, the routing tabs wrap, and nothing scrolls sideways at 320, 390, 768 or 1280 in either language (WebKit, light and dark). worker.test.mjs checks every marked key has its Chinese and the dictionary has none unused, and the / redirect by Accept-Language and cookie. yetone 2026-10-03 01:09:21 +08:00
  • b683e531d9 gui: a window lightweight mode made again isn't shown before its page has come, however it is asked for (found smoke-testing #580's lightweight mode on Windows 11) — on Windows a remade window is shown only once WebView2 says its page has come (whenLoaded), but a second ask in that time — two magpie app at once, a tray click while the panel was being made — went to openMain's or togglePanelNow's ordinary path and showed it at once: the main window was drawn black for about half a second, the early Show markReady's comment warns can crash WebView2 on a slow start. A window waiting on whenLoaded is now marked loading (h.loading, main thread only) until its page comes; meanwhile openMain only sets its URL and togglePanelNow does nothing, the waiting callback showing it, and the lightweight loop doesn't let it go. Other systems are unchanged (whenLoaded runs at once there). go vet, GOOS=windows go build and go test ./internal/gui pass; the race itself was not run again on the Windows box. yetone 2026-10-03 01:05:14 +08:00
  • eddc298e92 gui: a gateway key's limit window and the session terminal are picked from the app's own menu too, so no native select is left (the owner, on the account picker: 这里为啥是原生的 selector 而不是我们抽象的 ComboBox?) — after the keep-signed-in account picker, two <select>s remained: the Window of a gateway key's limit editor (#585) and Settings → Session terminal. Each is now a sess-pick button showing what is picked, with a chevron, opening the app's menu (openProtoMenu) headed Limit window / Session terminal (限额周期 / 会话终端) with the current one ticked. The limit window's pick is staged in the editor as before (nothing sent until Save) and fills its column with the chevron at its end; the terminal's is drawn at once and saved as before. gateway-key-limit.test.cjs and session-terminal.test.cjs now check there is no native select, the menu's head and items, the button's value and text, a pick closing the menu (and leaving the limit editor open), a second click closing it, and picking through the menu, posting as before; they, keep-login, click-scroll and settings-sections pass in WebKit, en and zh. yetone 2026-10-03 01:02:53 +08:00
  • 53b60764d4 gui: the account Codex or Claude Code is kept signed in to is picked from the app's own menu, not a native select (the owner: 这里为啥是原生的 selector 而不是我们抽象的 ComboBox?) — the Keep {agent} signed in to row (#524) used a <select>, so it drew and dropped the system's menu while every other pick in magpie is a pill opening its own (openProtoMenu). It is now a sess-pick button showing the account (or the first account) with a chevron, opening the app's menu headed Account to stay signed in to (始终登录的账号) with the first account and each account that hasn't lapsed, the current one ticked; picking one ticks Keep and saves it as the select did, a second click or Escape closes it. keep-login.test.cjs now checks there is no native select, the button's value and text, the menu's head and items in order, a second click closing it, and picking through the menu (spare, then back to the first) posting as before; it and click-scroll pass in WebKit, en and zh. yetone 2026-10-03 00:59:48 +08:00
  • ad93293885 gui: the Add sheet shows a new user only magpie's own subscriptions; a deprecated one is installed from Plugins (the owner: 对于新用户来说,这里应该只显示内置的 provider 啊,更多不是要点击那个链接跳转到插件页面让用户去主动安装吗?) — a fresh magpie listed all 18 subscription tiles, the deprecated built-ins (Cursor, Kiro, Zed, Qoder…) among them, each offering its plugin when clicked. A deprecated built-in with no account in magpie (unusedSub) is now left out of the Subscriptions section, which for a new user is Claude, ChatGPT, Copilot, Gemini CLI and Antigravity, then More in Plugins; a search naming a left-out one shows More in Plugins, which opens the Plugins page on that search, where its plugin is installed. One with an account stays, badged Deprecated; one moved to its plugin stays as before; one whose plugin is installed shows the plugin's tile (replacedSub). The tile's plugin offer (Install and sign in / Use the built-in), unreachable now, is removed with its CSS and zh strings. AGENTS.md says so. deprecated-subscriptions.test.cjs now checks Zed (no account) isn't in the sheet, a search for it shows More in Plugins and opens Plugins on "zed" with Zed's card, and nothing signs in or installs by itself; plugin-replaced.test.cjs expects no tile for an unused deprecated built-in and one for a signed-in one. Both pass in Chromium and WebKit (en/zh), as do add-sheet, provider-search, qoder-cn-tile, mobile-web, deprecation-notice, click-scroll, plugin-move-overflow, plugin-movable-review, plugin-move-unserved, plugin-progressive and plugin-check-updates; plugin-move's editor-scroll check in Chromium flakes the same on origin/main. yetone 2026-10-03 00:52:41 +08:00
  • f1ca6139b5 usage: a Codex call through the gateway that wrote to its upstream's cache is no longer listed a second time as a local session call (laowangba233 on #589: 用量中的统计是不是重复了 … 为啥会一个是提供商的一个是本地会话的) — the Usage page drops a Codex rollout's call when it matches a gateway record by session, end time within 2s and equal tokens. Codex reads input_tokens_details.cache_write_tokens from a Responses reply and keeps it as cache_write_input_tokens; the gateway's rUsage read only cached_tokens, so for a call whose upstream told a write the record had CacheWrite 0 and the rollout didn't, the tokens never matched, and the call stayed listed under Local session beside its provider's row. Calls without a write matched, so only some calls were counted twice (Maru Code 92 calls plus 22 "本地会话" in the report). Also, sessions' spent took only the cache read out of Codex's input_tokens, which holds the write too, so such a call's write was counted twice in its own input. Now: the gateway reads cache_write_tokens (input = input_tokens - cached - written) and tells it to Codex when it translates a reply; spent takes the write out of the input as well; and the matcher compares a call's input plus cache writes, its output and its cache reads, so a record and a rollout that split the write differently — an older Codex without the field, or records already in usage.jsonl from before this change — still match, and the duplicates already on a user's page go too. The session caches' versions go up (sessions.json 11, calls-v4) so kept parses are read again, and older calls-v* folders are removed. Under a provider filter the provider ranking still lists the other providers, as it is a picker; the duplicates were what made it read as double counting. TestCodexCacheWriteMatchesGatewayCall (a Codex rollout and gateway records for two calls, one with a 9000-token cache write; with the rollout naming the write and the record not, both naming it, and an older Codex without it: two rows, no Local session row, the provider ranking maru-code alone, unfiltered and filtered to the provider, by QueryPage and LedgerOf; fails without the change with a third row from the log), TestCodexCacheWriteNotInInput and TestResponsesUsageCacheWrite (parse and round trip through what Codex is told) are new and fail without the change; TestCallsCodex's call with a write now expects input 200 (2500 - 2000 read - 300 written). go vet ./internal/..., go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui -count=1 ./internal/... under a temp HOME pass (TestFXInState's known TempDir-cleanup flake aside, which passes alone). yetone 2026-10-03 00:43:57 +08:00
  • 93fc561f17 feat(gui): a provider switched off no longer lists its models (#588) 尔立andClaude Code 2026-10-03 00:44:49 +08:00
  • 2c81af6983 fix(provider): InUseLogin follows the gateway when the signed-in account is spent (#587) 尔立andClaude Code 2026-10-03 00:44:43 +08:00
  • 4bc7d6c00d provider: moved Factory users get opencode-factory-auth 0.1.10 (magpie-community/plugins#10) — a tool result quoting Claude Code's fixed phrases is sent so Factory's check takes it, and Claude Code's # Environment update carrying a model is adapted as its model-switch updates are. The mover's min is raised from 0.1.9, so keepMovedCurrent updates every moved user's plugin. go build -tags nogui and go test ./internal/provider/... under a temp HOME pass. yetone 2026-10-03 00:42:16 +08:00
  • 71365192d9 test: the agent folder variable check also scans gateway, so a new variable there is caught (#586) Lin Huazhong 2026-10-03 00:37:02 +08:00
  • 99b1865494 docs: add missing relative pronoun in README.md (#593) lunar-meandlunar-me 2026-10-03 05:31:25 +13:00
  • 4a2f538c61 gui: lightweight mode, off by default, lets a closed panel's or window's webview go and makes it again when it is opened (somls on #580: GUI版可否增加轻量模式,关闭面板退出webview,减少资源占用) — closing the tray panel or the main window only hid it, so each kept its page and, on a Mac, its WebKit WebContent process (about 65 MB each at start, 95–100 MB after use) for as long as magpie ran. Settings → General now has Lightweight mode (轻量模式), shown in the app only (not on a browser's page) and kept as this computer's own through a sync or a restored backup (KeepOwn): with it on, a window found closed on two looks in a row 30 s apart (lightStep) is let go 30–60 s after it was closed, so a panel opened again soon after is still the same one at once. Letting go happens on the main thread under a new h.winMu: the window field is set to nil, the panel is unhooked from the tray icon (AttachWindow(nil); left click goes through magpie's own OnClick), and the main window is marked gone so its closing hook, which otherwise cancels a close and hides, lets it close; on the Mac the WKWebView is taken out of the window first (stopLoading, delegates and script handlers cleared, removed from its superview), since Wails' NSWindow close alone left the WebContent process alive for minutes. Opening it again (the tray icon, ShowMain, an import link, the Dock) makes the window anew with the options it was started with (makeMain/makePanel, moved out of Run with their hooks), its text size and title bar or Hyprland name (madeAgain), on Windows shown only once its page has loaded; every other path that reached h.main or h.panel (fit, tint, zoom, hide, glide, the Windows title-bar tint, the Hyprland click watch, the folder picker) now goes through the main thread and does nothing to a window let go; Linux keeps running with no window left (DisableQuitOnLastWindowClosed). Measured in a sandbox build under a temp HOME on other ports: started in the tray with the setting on, two WebContent processes of ~65 MB each; about a minute later both windows were let go and both processes ended (GPU and Networking, ~56 MB, are shared and stay; magpie's own process is unchanged), so the saving is ~130 MB idle and up to ~200 MB after use; the main window and the panel were then opened again with their pages, tinted and at the tray icon, closed, let go a second time, and magpie quit cleanly, with fit/tint/zoom calls every 0.2 s through a release crashing nothing. Windows and Linux build but were not run. TestLightStep and TestLightweightKeptAndOwn are new; lightweight.test.cjs (WebKit, en and zh: the row, off by default, On/Off posted as lightweight true/false, another setting's save keeping it, no scroll, no left-border accent, no row on a browser's page) is new and passes, as do click-scroll, settings-sections and searcher-pick. go vet ./internal/..., go build (darwin GUI), go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui -count=1 ./... under a temp HOME pass. yetone 2026-10-03 00:24:43 +08:00
  • 2e340f7bb6 gui: an agent row's grip is drawn only with the pointer on the row, as ec10d3b had it (the owner: 这个为什么又加回来了啊?不是说 hover 的时候才出现吗?一直出现太丑了) — 71ba715 (#537) drew the grip in every row's left margin with the pointer away, so the Agents list showed a column of dots beside its logos at all times. The grip is again drawn only on the row under the pointer, the one dragged, or a handle with its menu open or the keyboard on it, as the Providers list's is; dragging and clicking it do what they did, and Hide stays in the handle's menu and the row's right-click menu. agent-grip now checks no row's grip is drawn with the pointer away, the hovered row's alone is, and it goes when the pointer leaves; it fails without the change ("alpha's grip is drawn with the pointer away") and passes with it in WebKit (en/zh, window/panel). click-scroll and agent-layout pass in WebKit. yetone 2026-10-03 00:19:49 +08:00
  • 0879894af3 gateway: each gateway key can have its own limit, a token total and/or an estimated cost per day, week or month, and a spent key is refused before any provider is asked (margbug01 on #585, v0.1.671: 每个网关密钥都能设置自己的用量上限) — a gateway key named who called and the Usage page grouped its calls, but nothing held a key to an amount, so one client shared on the network could spend a subscription's whole window. A key now carries limit {period day|week|month, tokens, cost, cacheReads} in caller-keys.json, set with the new "limit-key" change. Windows are calendar ones in local time (from midnight, from Monday, from the 1st) and a call counts in the window it started in. Tokens are a call's uncached input, output and cache writes, plus cache reads when the limit says so; cost is an estimate at the Usage page's effective prices (usage.NewCoster), a call with no known price adding none and counted as unpriced. What a key used is summed from usage.jsonl (usage.Visit), so it survives a restart; the new internal/budget keeps the sums in memory per window and appendUsage writes a key's record through budget.Append, which appends and counts under one lock so a window read meanwhile never counts a call twice. Before a keyed POST reaches the mux, keyLimited reserves for it: its body's size in tokens (4 bytes a token) plus the key's mean output per call this window, at the window's cost per token or the named model's input price; it is let in while used + reserved is under the cap, so requests sent at once overshoot by about one call, and the reservation is released when the request ends, a streamed reply being settled then with the usage its vendor reported. A spent key gets a 429 in its API's error shape (Anthropic, OpenAI chat/responses, Gemini) naming the key, the cap and the reset time, with Retry-After, X-Should-Retry: false and X-Magpie-Limit-Reset, recorded as a rejected call (gateway_key_limit); other keys go on. Requests from this computer without a gateway key are not limited; a key used from this computer is. count_tokens and bridge callbacks are never held. GET /v1/magpie/limit tells a key its own status. Gateway keys rows show a Limit badge (what is used of it, per what, Spent in red) and a line under a limited key (used of limit, left, resets / refused until, in flight; unpriced calls in its title); Limit opens an editor under the row (window, tokens with k/m/万/亿, cost in US$, Count cache reads too, what is counted) staged until Save, marked unsaved, Cancel/Escape dropping it, a bad amount disabling Save, No limit removing it; en and zh; no left-border accent; no click scrolls. The CLI gains magpie gateway-key limit <id> [off | day|week|month --tokens N --cost USD --cache-reads] and a LIMIT column in list. TestKeyLimitRefusesBeforeUpstream (two calls within 700 tokens, the third a 429 the fake upstream never sees, message/type/headers, the rejected record, Anthropic's shape on /v1/messages, another key and a keyless loopback request let in, still refused after budget.Forget as after a restart, a raised and a removed limit let in, /v1/magpie/limit), TestKeyLimitHoldsParallelRequests (20 at once against 2000 tokens: some refused, at most one call over, no reservation left; without the reservation all 20 got in and 7350 were used), TestKeyLimitWindowResets, TestLimitWindow, TestLimitValid and TestGatewayKeyLimitCLI are new; gateway-key-limit.test.cjs (WebKit, en and zh) is new and passes, as do gateway-caller-keys, gateway-connect-keys, api-key-theme and click-scroll (whose request-pick case flaked once under a parallel run and passes alone, as on main). go vet ./internal/..., go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui -count=1 ./... under a temp HOME pass (TestFXInState's known TempDir-cleanup flake aside). yetone 2026-10-03 00:18:13 +08:00
  • b2ac45b0bc agents: whether WSL is in mirrored networking is asked of WSL itself (wslinfo --networking-mode), and a UTF-16 .wslconfig is read (lgtm on Discord: told "WSL Ubuntu isn't in mirrored networking … pointed at Windows (http://192.168.16.1:3425)", while their WSL has always been in Mirrored) — magpie decided it by %UserProfile%\.wslconfig alone, read as UTF-8: a .wslconfig Notepad saved as "Unicode" (UTF-16, which WSL reads) said nothing, a "[wsl2] # comment" header ended the section, and mirrored set anywhere magpie didn't read was missed — so a really mirrored distro was pointed at its default route, which under mirrored is the LAN router, not Windows. The probe each running distro already gets now also prints wslinfo --networking-mode (WSL 2.0 on), kept as the distro's Net ("mirrored", "nat", …) and deciding Mirrored; .wslconfig decides only when there is no wslinfo, and is read as UTF-8, UTF-16LE (BOM or not) or UTF-16BE. A distro seen stopped is probed again once it runs, so WSL restarted in another mode is seen without restarting magpie. The notice says what it knows: "WSL Ubuntu is in nat networking, not mirrored, …", or, without wslinfo, that magpie couldn't ask and .wslconfig doesn't set it — never "isn't in mirrored" on .wslconfig's word alone. TestWSLConfigUTF16 (UTF-8, its BOM, UTF-16LE with and without BOM, UTF-16BE; fails without the change for the UTF-16 three), TestParseProbeNet, TestWSLNetworkingFromWslinfo (wslinfo mirrored with no .wslconfig is 127.0.0.1; wslinfo nat beats a .wslconfig saying mirrored; the fallback either way and its notice) and TestWSLReprobedAfterStop are new. The probe run through the real wsl.exe on a Windows 11 box (WSL 2.6.1, a NAT distro) read net:nat. go vet ./internal/..., go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui -count=1 ./... under a temp HOME pass. yetone 2026-10-03 00:17:02 +08:00
  • ff36a2994f agents: T3 Code gets magpie as a provider of its own, every magpie model in it (KevinXC on Discord: T3 Code's Settings → Providers lists Codex, Claude, Cursor… each with "Add custom model"; magpie's models should be there without adding them one by one) — T3 Code (pingdotgg/t3code) keeps its settings in ~/.t3/userdata/settings.json ($T3CODE_HOME/userdata), sparse JSON its server watches and reloads on an outside edit, and besides its built-in providers takes provider instances of one's own: {"providerInstances":{"<id>":{"driver","displayName","enabled","environment":[{name,value,sensitive}],"config":{"customModels":[{slug,name}]}}}}. A claudeAgent instance runs Claude Code (the Agent SDK, ~/.claude settings read as Claude Code reads them) with its environment added, and sends a custom model's slug as Claude Code's model. The new t3code agent (t3code.go) adds the instance "magpie": driver claudeAgent, displayName magpie, ANTHROPIC_BASE_URL the gateway and ANTHROPIC_AUTH_TOKEN the token Claude Code is routed with (so its requests are Claude Code's to the gateway, haiku tier and all), not sensitive so T3 keeps them in the file rather than its secret store, and every magpie model shown to T3 Code as a custom model, a 1M one marked [1m] as for Claude Code (claude1MFor). The binary path and Claude home the user gave T3's own Claude (its instance, else the legacy providers.claudeAgent) are carried over. It is magpie's alone: the user's instances, their custom models and T3's other keys stay, off takes only it out, and the catalog sync rewrites it only where it is. T3's Codex lists Codex's model/list, which is magpie's catalog once Codex is routed through magpie, so it needs nothing; T3's own Claude asks for Claude's models by their full ids, which the gateway already sends to Claude Code's tiers (claudeTierStandIn). Found by ~/.t3 or, on a Mac, T3 Code (Alpha).app / T3 Code.app; T3CODE_HOME is in agentenv.Vars. Its icon is T3's mark (assets/prod/logo.svg, MIT), its letters alone so it draws as a mask; the option's note has its Chinese. Checked against T3 Code's own schema (effect 4.0.0-rc.115, packages/contracts at 54084ae1): the file magpie writes decodes as ServerSettings, magpie's config as ClaudeSettings, and encodes back to the same instance. Not run against a live T3 Code. TestT3CodeInstance (the user's instance, legacy Claude binary path and T3's key kept through on, sync and off; magpie's instance decoded strictly; found only with ~/.t3; among All()), TestT3CodeHomeAndInstance ($T3CODE_HOME, a file made, the Claude home carried from the user's claudeAgent instance) and TestStateListsT3Code are new. go vet ./internal/..., go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui -count=1 ./... under a temp HOME pass. yetone 2026-10-03 00:15:53 +08:00
  • 283d76bf73 gui: the deprecation notice over the Providers list is one quiet line like the list's rows (the owner: 这个太丑了) yetone 2026-10-02 23:53:29 +08:00
  • 6ab31fbaeb gateway: every full id of a Claude family Claude Code names goes to its tier's model, served by a provider of the user's or not (KevinXC on Discord, v0.1.665, T3 Code 0.0.45 over Claude Code 2.1.287: claude-haiku-4-5, claude-opus-5, claude-fable-5 and claude-sonnet-4-6 went to the haiku/opus/fable/sonnet tiers, but claude-fable-5-1, claude-sonnet-5 and claude-sonnet-5-5 went to the Claude account, 429 "requires usage credits" / "weekly limit") — the stand-in (4cbd96a3) was only asked for a model magpie shows no entry for, so a full id a provider of the user's serves bare (the Claude account lists claude-sonnet-5, claude-fable-5-1, …; a found auto-<model> group has it too) went there, while one none listed went to the tier: which tier a model reached depended on what the user's providers happen to list, not on the model. T3 Code names every model by its full id rather than Claude Code's alias, so it hit both. Now an id of the opus, sonnet, haiku or fable family (claudeFamily: claude-<family>-<any version>[-date][1m], and claude-3-5-sonnet-… alike, so a release magpie has never heard of is one too) from Claude Code routed through magpie goes to the model its tier is set to before groups or providers are looked at, count_tokens counting it there too. Kept as asked: a provider's "anth/claude-…", another agent's request, Claude Code not routed through magpie (StandIn says nothing), a routing group of the user's own named by the id (one magpie found, auto-…, was never picked for it), and the main model when Claude Code's ANTHROPIC_MODEL is that very id. A tier at an effort of its own (#536, "fake/sol:high[1m]") now reaches its model at that level through a stand-in too, where it was looked up whole and refused. TestClaudeTierStandInByFamily (claude-haiku-4-5, -20251001, claude-haiku-5, claude-opus-5, -5-5, -5-5[1m], claude-opus-6, claude-fable-5, -5-1, -5-1[1m], claude-sonnet-4-6, -5, -5-5, -5-5-20261001, claude-3-5-sonnet-20241022 through /v1/messages, beside a provider serving seven of them bare) fails without the change for the nine served ones; TestClaudeStandIn gains the families and the main model. go vet ./internal/..., go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui -count=1 ./... under a temp HOME pass (TestDevinSignedInOnlyInMagpie, a known flake, aside). yetone 2026-10-02 23:50:39 +08:00
  • 0f5eb941e7 providers: a custom provider's models can be picked from its vendor's list while it is added, before its first Save (Sun1090 on #578: 添加供应商的时候,希望添加可以获取全模型的按钮或者下拉框?像cc-switch那样,可以选) — the vendor's list, its chips and Refresh were only in a saved provider's editor; the custom provider's add form had a comma-separated Models field and the hint "magpie asks the vendor for its list after saving", so a user adding one had to save it, open it again and pick there, which nothing in the form said could be done sooner. The add form now has Fetch models (获取模型) beside its Models field: it posts the new provider/list with the form as it stands (the URL and key typed, its headers, models URL and proxy; no id), which builds the draft as detect does (from its preset when it has one, then typed) and asks the vendor with the new Provider.List — Fetch's own path (a NoList preset's models, Cline's feed, Kilo's list, fetchOne) without SaveLive, so nothing is kept — answering the models agents chat with, a model's name where it has one. The list shows as chips (a long one with a filter), each ticked one in the Models field and an id typed there ticking its chip, with "{n} of {all} picked", Pick all (Pick those shown under a filter) and Pick none above the chips, so a pick that reflows them moves no button; one typed by hand stays through Pick none. A vendor refusing says why under the field; no URL asks nothing. The Add sends the picks as the provider's models, as the field did. TestProviderListBeforeSave (the list asked with the key typed, an embedding model left out, no provider saved and no list kept, a 401 told, no URL refused) is new; provider-add-models.test.cjs (WebKit, en and zh: nothing asked without a URL, the post carries URL and key and no id, the refusal shown, chips with names, tick and type in sync, filter and Pick those shown / none / all, the Add's models, no click moving the page, no left-border accent) is new and passes; add-sheet, provider-detect, provider-detect-models, provider-typed and click-scroll pass in WebKit. go vet ./internal/..., go build (darwin GUI), go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui -count=1 ./... under a temp HOME pass (TestFXInState's known TempDir-cleanup flake aside). yetone 2026-10-02 23:43:23 +08:00
  • a90b52f1b4 gateway: the model list comes as ids one a line with ?format=text, to paste into ZCode's custom provider from a magpie on another computer (悠悠哥 on Discord, v0.1.671: ZCode pointed at magpie on a NAS, http://192.168.0.200:3425, Anthropic Messages, never got the model list, while at 127.0.0.1 it did) — ZCode (3.14.3) asks a custom provider for no list at all: its settings take the models typed one a line ("模型列表 · 每行一个模型名称"), its "智能配置" matches built-in metadata by id and base URL, and neither its desktop app nor its CLI requests /models anywhere. What filled it at 127.0.0.1 was a magpie on that computer, which writes its models into ZCode's own provider_config.json (personalModelIds) and config.json under the provider "magpie" at its own 127.0.0.1 gateway; a magpie on a NAS can't write that computer's files, so the provider pointed at it is left with what was typed. The gateway itself answers another computer as it answers this one (no Host check, the key read wherever the client puts it, 1c047f36 in v0.1.664). GET /v1/models?format=text (and /models) now answers text/plain, the ids in the list's order one a line, so a browser on that computer opening http://<nas>:3425/v1/models?format=text&key=<gateway key> gets what to paste into ZCode's model list; without a key from the network it is refused as the JSON list is. TestLANModelsAsText (from 192.168.0.9 to Host 192.168.0.200:3425: text/plain, the JSON list's ids in its order, none empty or JSON; no key 401) fails without the change. go vet ./internal/..., go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui -count=1 ./... under a temp HOME pass. yetone 2026-10-02 23:47:10 +08:00
  • 88887cdfff providers: a provider's Routing is picked in its editor and made with its Save, and its Stays (会话保持) is beside it (01huadalang on Discord, a WorkBuddy AI subscription with several accounts: 在这里切换会自动保存,没有手动点击就给我保存了,这样切换来看会很卡; 没有看到会话选择) — each click of Smart / In order / In turn / Least used first / Weekly pace posted provider/route, saved, and redrew the providers page with the editor in it, so looking through what each option says lagged and saved a pick only looked at. Routing is now staged in the editor's draft as Names & levels is (b0dc82c4): a click shows the option and its description, says "unsaved" (未保存) beside it, sends and redraws nothing, Cancel drops it, and both editors' Save (a signed-in account's and a key's) carry it. How long a conversation stays with the account or key that answered it (Auto / Session / Within a turn / Off, provider.Affinity, which the gateway already honours for a provider's own accounts) could be set only on the Routing page's "Several accounts or keys" row; it is now in the editor under Routing, staged the same way. The save action takes routing and affinity when sent and keeps them when not: before, every Save of the editor dropped the Stays set on the Routing page, since the editor never sent it and in.Affinity came in empty. routing-save.test.cjs (WebKit and Chromium, en and zh, a subscription with three accounts and a provider with two keys: every option clicked shows its description and unsaved with nothing sent, redrawn or scrolled; Cancel drops them; one Save carries routing "rotate" and affinity "session") fails without the change and passes with it; account-arrange.test.cjs now saves the routing it picks. TestProviderSaveRouting (the Save's picks made, a Save without them keeps them, the Routing page's route and affinity survive the next Save, Smart and Auto sent as "") fails without the change (routing lost). keep-login, provider-levels-save, provider-levels-scroll, click-scroll, editor-short-window, provider-proxy, provider-concurrency, account-proxy, account-models, plugin-move, deprecated-subscriptions, routing-account-order pass in WebKit (redact-rule-row's 640px case fails as on main). go vet ./internal/..., go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui -count=1 ./... under a temp HOME pass. yetone 2026-10-02 23:40:59 +08:00
  • f65d02f080 providers: a model a vendor names its own way is merged with the same model from other providers — by its id as Volcengine Ark spells it, or by the model the user says it is the same as in Names & levels (kyzhouxu, #583: 火山引擎获取到的默认 ds 4.1 flash 模型的名称跟其他供应商的名称不一样,没有自动合并到同一个模型分组) — the groups magpie finds merged a model by its id lowercased, its vendor prefix and an eight-digit date taken off and a version's dot made a dash, so Ark's deepseek-v4-1-flash-260910 (a six-digit YYMMDD snapshot) was a group of its own beside ali-tp's deepseek-v4.1-flash. sameModel now also takes off Ark's YYMMDD (a year from 23 to 39, a month and a day that are one), Vertex's "@YYYYMMDD", reads Fireworks' "p" between digits (deepseek-v4p1) as the dot and "_" as "-"; -flash, -thinking, ":batch", a four-digit release (qwen's -2507, kimi-k2-0905) and a number that is no date (-128000, -261301) stay apart. For an id no rule matches (an Ark endpoint id, a relay's own name), each model in a provider's Names & levels now has a "Same as" (等同于) box, its placeholder the name it is merged by now: the model it is the same as, as any vendor spells it, staged in draft.modelPrefs and made with the provider's Save like the other prefs (Cancel drops it, Restore default empties it, nothing sent or redrawn or scrolled before the Save). It is kept in settings' ModelSameAs by "<provider>/<model>" (renamed with the provider like every Model* map), set by provider.SetModelSame (its own id however spelt keeps nothing; a model the provider doesn't serve, or a name of no model, is refused) and taken by autoGroups, AutoStandIn and the agents' reseat (AutoGroupOf); the agents are told once. TestSameModelVendorSpellings (Ark, Fireworks, Vertex, underscores, and pairs that must stay apart) and TestModelSameAsMergesFoundGroup (c/ep-sol-x joins auto-sol once said, GroupFor("sol") finds it, back out when cleared, refusals) are new; TestProviderSaveModelPrefs covers a Save's same; provider-same-as.test.cjs (WebKit, en and zh: placeholder, staging, unsaved, Restore default, Cancel, one Save carrying exactly the changes, no scroll, Chinese for each string) fails without the change and passes with it; provider-levels-save and provider-detect pass. go vet ./internal/..., go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui -count=1 ./... under a temp HOME pass. yetone 2026-10-02 23:39:37 +08:00
  • 52e7e4fc8d update: a restart to update waits until the gateway is idle instead of cutting off what the agents have in flight (#577: 正式版更新时保持网关请求与工具链不中断 — a Codex reply streaming through magpie, or a tool chain under way, was cut off by Restart to update) — the app serves the gateway in its own process and a restart to update is install, quit, relaunch, so whatever was in flight ended with it: a stream mid-reply, and a Claude Code turn of the subscription bridge waiting on its caller's tool results, whose Claude Code lives only in this process. The gateway now says what a restart would cut short (Server.Busy): its requests in flight (all but GETs and the bridge's own MCP callbacks), the bridge's runs parked on tool results, and when the last request ended. Restart to update (Settings → Version, the header's Update pill, the tray's item) restarts at once only when nothing is in flight, no turn waits on tools and nothing ended in the last 10 s (a tool chain's gap, the agent running a tool between two requests); otherwise it waits, saying what for: Settings shows "restarts once the gateway is idle · 2 in flight · 1 awaiting tool results" (网关空闲后自动重启 · 2 个请求进行中 · 1 个工具调用待返回) with Restart now and Cancel, the pill reads "Waiting to update" (等待更新) and its next click (not a double click's second half) restarts now, the tray item becomes "Restart Now to Update (3 in flight)" (立即重启以更新(3 个进行中)). The wait asks the feed again once idle, so versions out meanwhile collapse into the newest, downloaded before the restart; it gives up after an hour, saying so, the update then going in at the next restart or quit. A gateway served by another magpie (magpie serve, an older one) is not this one's to wait for. Handing the gateway over to the new version instead (the dev handover, e0c6892) is not used for releases: it rests on SO_REUSEPORT, which Windows lacks, and the new app would have to start beside the old one (single-instance, the Mac's bundle swapped under it). TestBusy (a stream counts until it ends, a GET never, a parked bridge run until it is answered or ends), TestRestartToUpdateWaitsForTheGateway (fails without the change: the click restarted with 2 requests in flight), TestRestartWhenIdle (not while streaming, parked or in a tool chain's gap; then once; an hour's wait gives up), TestRestartWhenIdleIntoTheNewest (0.1.11 staged, 0.1.12 and 0.1.13 out while waiting: restarted into 0.1.13) and TestTrayRestartNow are new; update-wait.test.cjs (WebKit, en and zh: what is in flight, waiting with Restart now and Cancel, counts following the gateway, the hour's give-up, the pill's wait and restart now, an idle click restarting at once, no click scrolling, no left border) fails without the change and passes with it. update-hide, update-check, sync-update, update-auto and click-scroll pass in WebKit. go vet ./internal/..., go build (darwin GUI), go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui -count=1 ./... under a temp HOME pass (TestFXInState's known TempDir cleanup flake aside; passes on rerun). yetone 2026-10-02 23:34:49 +08:00
  • 06a2e9cff5 search: which provider searches the web for a model that can't is picked in Settings → Web search, Automatic as before (01huadalang on Discord: 能不能在供应商联网搜索里加一个入口,能用自己能联网搜索的地方来替代不能联网搜索的供应商) — magpie picked it by itself (a Claude account, then a Codex account, then a provider that searches by itself on Anthropic's API, the Responses API, Chat, each by its small model) and Settings only said who. A "Searches for other models" row now comes first in the section: its picker offers Automatic (magpie's pick, named), and each provider able to search — on, and searching by itself or a Claude/Codex account, in the order magpie would pick them — by its small model or by any model of it. The pick is saved as settings' searcher ("<provider>" or "<provider>/<model>"), sent with every other setting saved (prefsKeep), and refused when the provider can't search. searcher() takes it while it can (chosenSearcher); one gone, turned off or no longer able to search gives way to magpie's pick, and the row says which and why, showing Automatic; a model it no longer lists gives way to its small model. A relay said to search is still never asked for other models (#359: it would spend the relay's quota, and one that serves only Claude Code refuses magpie's own request), so it isn't offered, and the row names such relays and says why. TestSearcherChosen (the automatic order, a provider, a model of it, a model gone, one that can't search, a relay said to search, one gone, one turned off and back on, Automatic again) and searcher-pick.test.cjs (WebKit and Chromium, en and zh: Automatic shown with magpie's pick, the relay not offered and named, a model and a provider picked and saved, another setting saved keeping the pick, one turned off said and Automatic shown, Automatic sent back, no click moving the page) are new; it passes in WebKit (this machine's Playwright Chromium fails to launch, icudtl.dat missing, for every test). search-api, settings-sections, provider-search, click-scroll pass in WebKit. go vet ./internal/..., go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui -count=1 ./... under a temp HOME pass (TestDevinSignedInOnlyInMagpie failed once, passes 3/3 alone). yetone 2026-10-02 23:32:53 +08:00
  • 5a4c427630 plugins: moving a subscription onto the Factory plugin installs factory 0.1.9 (Claude Code's standalone model-switch system updates in the form Factory takes, magpie-community/plugins#9) — on npm now. go test -tags nogui ./internal/provider ./internal/plugin under a temp HOME pass. yetone 2026-10-02 23:33:06 +08:00
  • 089f4f7d87 accounts: Codex or Claude Code can be kept signed in to any of its accounts, not only the first, while the gateway still tries them in their order (Koohoko on #524: 建议不只是支持始终登录"首选"账号,应该可以支持始终登录任一选定的账号。有时候首选只是为了按顺序先消耗它的额度,不一定是用户期望的常用登录账号) — making an account first did two things at once: the gateway tried it first (the account the agent is signed in to always stood ahead of the others, AlsoOn behind it), and Codex or Claude Code was signed in to it (SwitchLogin). "Keep {agent} signed in to the first account" (817a5f50) only stopped magpie moving the sign-in on, so a user who wanted one account's allowance spent first had to sign the agent in to it too. Now beside that tick is a pick, "Keep {agent} signed in to [the first account ▾ | each saved account]" ({agent} 始终登录 [首选账号 ▾]): picking one keeps Provider.KeepLoginAs (providers.json, kept across an editor Save like KeepLogin; POST provider/keeplogin with keepLoginAs) and signs the agent in to it at once, the order the gateway went by then — the one signed in to first — written as the accounts' own first, so the first stays first. While it is kept: the login switcher signs the agent back in to it whenever it is on another and never moves it off, however used up (NextLogin); a drag or Make first sets the order without a sign-in (arrangeLoginsAs), Make first posting provider/arrange; the gateway puts the account signed in to at its own place in that order, not first (perKeyBarred by Provider.LoginRanks), so In order spends the first's allowance first; InUseLogin (the tray's in-use account) is the first in use in the order. The rows keep their order, the first saying First and the one kept saying Signed in (已登录), an account not in use ticked on by its dot rather than a Use that would sign the agent in; the Routing note says the agent stays signed in to that account and requests go in the accounts' order. Picking "the first account", or unticking, lets it go and signs the agent in to the first again, so the one signed in to is the first once more. TestKeepLoginAsChosen (signed in to the chosen at once, the order kept, stays at 100%, moved off it goes back, a drag changes the order not the sign-in, the first's again when let go) and TestKeptLoginStandsAtItsPlace (through the gateway In order: kept on spare, the first, acct-1, is tried first; acct-2 without the change) are new; keep-login.test.cjs now also picks an account (the post, First and Signed in, Make first arranging, back to the first, every string's Chinese, the page not moving) and passes 8/8 in WebKit; account-return, routing-account-order, account-arrange and click-scroll pass in WebKit. No live agent config, keychain or vendor was touched. go vet ./internal/..., go build (darwin GUI), go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui -count=1 ./... under a temp HOME pass. yetone 2026-10-02 23:30:47 +08:00
  • e194f97731 plugins: moving a subscription onto its plugin installs the plugin's newest fixes — cursor 0.1.7 (a model Cursor runs only in Max Mode is asked again in Max Mode, glm-5.3 and glm-5.3-flash listed), qoder 0.2.2 and workbuddy 0.1.6 (each model's credit rate, which magpie shows by it since v0.1.667), zcode 0.1.7, factory 0.1.8 (Claude 5's system-role metadata in the form Factory takes) — all on npm now. go test -tags nogui ./internal/provider ./internal/plugin under a temp HOME pass. yetone 2026-10-02 23:21:58 +08:00
  • c9b7f95f90 providers: Detect APIs asks every picked model on each API and shows model by API, and a model's right-click Test says the API it answered on (01huadalang on Discord: 右键选择测试只能测试出是不是通的,不能说什么协议是通的。检测协议的结果看不出对应的模型支持协议情况…有的仅支持 response 有的双协议) — Detect APIs asked one model (typed, or one picked from the vendor's list for each API), so a relay serving some models on Responses only and others on both read as "chat works, responses doesn't", and the right-click Test said only that a model answered. Detect APIs now has "Each picked model" (逐个检测已选模型): provider/detect with detectModels asks each picked model (the first provider.DetectMax, 30; an image model left out, it is asked on the images API) on Chat, Responses and Anthropic at the URLs Detect asks, provider.DetectWide (8) requests at a time, each waiting 12 s, the whole within 2 minutes (a probe not started by then is "not asked: out of time" and sent nowhere), and answers model by model plus, by API, the first model it served. The editor shows a model × API grid, each cell a ✓ with the time or a ✗ with the status and why (the whole in its title). Its "Use these" sets the URLs of the APIs that answered and gives each model that answered on one API only (of two or more asked) that API, staged in Names & levels as draft.modelPrefs like the rest of b0dc82c4 — "unsaved", nothing sent until the Save, Cancel drops it — while a model answering on two stays Auto; the hint under the grid names them ("m-resp: Responses only"). Detect for one model is as it was (its body now Provider.detecting and detectOne, shared). A chip's right-click Test now says the API it was asked on: "gpt-5.5 answered via Responses in 123 ms", the dot's title "Answered via Responses in …", a failure "didn't answer via OpenAI: …". TestDetectModels (a fake relay serving gpt-both on Chat and Responses, gpt-resp on Responses, glm-chat on Chat, claude-only on Anthropic: each model's ✓/✗ on each API, 12 requests for 4 models, the image model not asked, by API the first served; 35 models asked as 30, never more than 8 at once) and TestDetectModelsOutOfTime are new; provider-detect-models.test.cjs (WebKit, en and zh: the models posted, the grid, Use these staging m-resp on Responses and m-chat on OpenAI with m-both left Auto and nothing sent, the Save's modelPrefs, the right-click Test's "via Responses", no click scrolling, no left-border accent, every string's Chinese). provider-detect, provider-levels-save, provider-levels-scroll, model-test-one, model-test-why and click-scroll pass in WebKit. go vet ./internal/..., go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui -count=1 ./... under a temp HOME pass (TestFXInState's known TempDir cleanup flake aside). yetone 2026-10-02 22:59:39 +08:00
  • a802d98bfc routing: a group in a group is routed by its own routing whatever the outer group's, and an account with five hours and no week is ranked by its five hours (#576: 只有五小时额度的账号在智能/重置前用完下是否合理;嵌套路由组时上层路由是否会干预下层) — a group routed other than In order (Smart, In turn, Least used, Weekly pace, Manual) put every account and key of every group in it into one pool and weighed them together, so A (Smart) holding A-1 and A-2 (each In order) ranked all five of their accounts and keys by Smart and A-1's and A-2's own In order did nothing; only an outer In order let each inner group route itself. planLevel now plans each group in the group by its own routing, whatever the outer's, and weighs it as one beside the outer group's own models' keys, by the first of its own order that isn't resting; where that lands, the inner group is tried whole in its own order, the trace in the same order. As for the five hours: Smart already ranked such an account (Claude Enterprise) by its five hours, the biggest window it has, so ahead of every week renewing later, but a five hours not started (no reset told) counted as a reset not known and went after every week; Allowance.Renewal now has a window not started that says its span renew that long from now, which Smart sorts by (the trace's renews unchanged). Weekly pace took an account with no week as a fresh week with the share used (100/168 per hour), so a week with 80% left and five days to go went ahead of an Enterprise account whose five hours were being lost; it now goes by what its five hours have left per hour until they renew, the whole span when not started, except where the reading may leave a week out — a Claude account only heard of from Claude Code's rate_limit_event, never read whole by /usage (QuotaWindow.partial, claudeUsageEntry.whole) — which keeps the fresh week. The group editor's Smart and Weekly pace hints and the trace's say how an account with five hours and no week is ranked, a hint under Routing says a group among the models keeps its own routing, and the docs (en, zh) say both. TestGroupInGroupKeepsItsOwnRouting (Outer rotating over two In order groups: b c d a without the change, c d a b with it), TestSmartFiveHoursAlone (a five hours not started went after a week in two days), TestFiveHoursAlone, TestClaudeHeardOnlyIsPartial; TestWeeklyPace and TestAllowancePace now pin the five hours alone. settings-groups, group-member-fast, group-found-off, routing-idle-frame pass in WebKit. go vet ./internal/..., go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui -count=1 ./... under a temp HOME pass (TestFXInState, TestProvidersAnswerWhileListsComeIn, TestUserPath, TestClaudeSignedOut flaked; the last passes 3/3 alone). yetone 2026-10-02 23:05:11 +08:00
  • 2e1fcb8a77 gateway: a Chat upstream gets a mended tool exchange — strict ones (Kimi) answered a broken one 400 "tool_call_id is not found" (#554) sundjly 2026-10-02 23:02:08 +08:00
  • 5a41046325 sessions: Hermes Agent's sessions are on the Sessions page, read from its SQLite stores read-only (#569) Vocllum 2026-10-02 23:02:03 +08:00
  • 01a7b55050 imagemcp: an image that takes longer than the agent gives a tool call is answered "still being made" before the agent gives up, and is saved all the same (#575: 图片生成的mcp经常遇到超时问题 — "Tool execution timed out after 30000ms" in one agent, "Error: Request timed out" in another; the agent then looked for the file, found none, and asked for the image again, and again timed out) — generate_image held its tools/call open until the gateway's image came back, and an image model (gpt-image at quality high, 1024x1536, through a subscription) often takes a minute or more, while agents give an MCP tool call as little as 30 s, the MCP SDK's default is 60 s, and only Codex and Goose have a timeout magpie writes for them (tool_timeout_sec, timeout). So the call was cut short, the generation went on unseen, and the agent's retry started another one that was cut short too. Now each generate_image and generate_video call runs as a job that goes on to its end whatever becomes of the call, and the call waits for it at most answerWithin (20 s, under the shortest of those limits): what is ready by then is answered as before (errors too); what is not is answered, not as an error, that it is still being made, where it will be saved, not to ask for it again, and an id for the new tool generation_result, which waits on the job the same 20 s at a time and answers its paths once it is saved. A call given a progressToken is sent notifications/progress every 5 s while it waits, for clients that wait longer on progress, and magpie mcp image waits for jobs still running when the agent closes it (up to ToolTimeout) so what is being made is saved. TestSlowImageAnswersBeforeTheAgentGivesUp (a gateway taking 700 ms against a 150 ms answerWithin: the call answers in time with an id and progress notifications, generation_result then answers the saved file, the gateway was asked once, an unknown id is a tool error) fails without the change (the call took 703 ms and answered only when the image was saved). go vet ./internal/..., go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui -count=1 ./... under a temp HOME pass (TestFXInState's known TempDir cleanup flake aside); go test -race ./internal/imagemcp passes. yetone 2026-10-02 22:55:03 +08:00
  • 33acb6bca0 providers: each model's credit rate shows by it in the provider's model list and an agent's model picker, Qoder's 0.5× and WorkBuddy's 0.03×, a discount's price before it struck through (01huadalang on Discord: qcoder 或者 workbuddy 能不能实时抓取或者按一段时间抓取能展示列表模型的积分消耗倍率,这样不用打开软件查看,能选择到便宜的模型) — magpie read each model's price only to tell free from not: Qoder's price_factor (06584e15) and WorkBuddy's "credits" ("x0.03", wbFreeCredits), so the lists said FREE or nothing, and picking a cheap model meant opening Qoder or WorkBuddy. catalog.Model now keeps the price, Rate (the credits a request costs, as a multiple; 0 is not listed, or Free) and RateWas (the price before a discount running now), and carries both through provider.Entry, agent.Option and the providers page's models. Qoder (and Qoder CN): Rate is the price_factor; RateWas an active promotion's before_promotion_price_factor, else an original_price_factor above the price (Qwen3.8-Flash: free, 0.1× struck through), and a promotion's 0 that isn't free is its price before times its discount_factor. WorkBuddy and WorkBuddy AI (the CodeBuddy plan; both builds read the same /v3/config): Rate is the credits (wbCredits; x0.00 stays FREE, an unreadable or infinite rate is none). A plugin's model gives it as it gives free: rate and rateWas on the model its provider.models hook lists, a number (0.5) or as the vendor writes it ("x0.03", "0.5×"), which host.js reads (rateOf) into plugin.Model.Rate/RateWas and pluginCatalog keeps; the moved Qoder and WorkBuddy users get it once their plugins send it. The badge is small and grey (the context badge's colours, no uppercase), beside FREE where that shows, its title saying what a request costs (每次请求消耗 0.5× 积分;折扣前 / 限时免费,原价). Rates are as of the provider's last list: a built-in's is fetched at start-up when it has none, on Refresh and when its key changes, a plugin's when its plugins are asked again (start-up, a sign-in, a plugin change); nothing asks on a timer. Kiro, Cursor, ZCode and Devin list no per-model rate magpie reads, so they show none. TestParseModelsRate (qoder) and TestWBCreditsRate (provider) are new and don't build without the change; TestFakePlugin now checks a plugin's rate given as a number and as "x0.03" (0 for both with host.js not reading them). model-rate.test.cjs (WebKit, en and zh: the chips' badges and titles, struck-through prices, grey not green, no stripe, a click picks and scrolls nothing, the picker's badges) fails without the change and passes with it; click-scroll, models-free-only, model-pick and favorite-star pass in WebKit. No request was made to Qoder, WorkBuddy or any vendor. go vet ./internal/..., go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui -count=1 ./... under a temp HOME, go test ./internal/gui pass. yetone 2026-10-02 22:48:41 +08:00
  • 4007beb9c0 usage: a Codex account's credits bought or given besides its plan show on the Usage page (#571: codex的套餐用完之后,可能会购买额外的信用点套餐…希望能做展示) — ChatGPT's /wham/usage answers, besides the 5-hour and weekly windows, the account's credits (credits: has_credits, unlimited, balance), which Codex spends once a window is used up; magpie read only the windows, so an account whose windows were spent showed spent though it still had credits to run on. codexWindows now reads them too, and a balance held (not unlimited, above 0) is the account's Balance, shown under its windows on the Usage page ("What is left on the account besides its windows") and in the panel card, for the account Codex is signed in to and for each saved one. TestCodexCreditsBalance (held, a few, none said, none held, spent, unlimited, unreadable). go vet, go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui ./... under a temp HOME pass. yetone 2026-10-02 22:44:03 +08:00
  • bb42b47663 fix(davsync): retry short shared usage uploads (#574) TryWorld 2026-10-02 22:42:47 +08:00
  • e41cacb42a fix(gateway): account for streamed failures (#573) TryWorld 2026-10-02 22:42:40 +08:00
  • b92513439f gui: a copy button copies in a browser tab with no clipboard API, as magpie web on a NAS opened from another computer is (悠悠哥 on Discord: 网关的apikey复制不生效) — copy asks magpie first (/api/copy), which magpie web refuses (it has no clipboard of the browser's computer to write to), then navigator.clipboard, which a page over plain http from another computer doesn't have (not a secure context): the button showed the key in the status line and copied nothing. The copy command is now the last try (copyByCommand: a readonly textarea out of sight, focused and selected without scrolling, removed after, the focus given back). copy-command.test.cjs (WebKit, en and zh: no navigator.clipboard, /api/copy 501; the sign-in link is what the copy command copied, the status says copied, nothing scrolls) fails without the change and passes with it; click-scroll and signin-link pass in WebKit. go test -tags nogui ./internal/gui passes. yetone 2026-10-02 22:36:29 +08:00
  • b0486663d2 providers: OpenCode Zen's free models answer with no key, as OpenCode signed out asks them (#572: opencode zen的free模型,只能在他自己的cli里面用,能否做插件反代出来?) — magpie has asked Zen's free models as OpenCode does since v0.1.615 (zenfree.go), but the OpenCode Zen preset wanted a key, which means an opencode.ai account. OpenCode itself, signed out, asks Zen with the key "public", and Zen lists and serves its -free models to it (checked: /zen/v1/models with it lists twelve). The preset's key is now optional ("optional: free models need no key"), and a Zen provider saved with none asks with OpenCode's (normalize, provider.OpenCodeAnonymousKey); a key of the user's own is kept, and the editor's blank key still keeps the one saved. TestOpenCodeZenNeedsNoKey (keyless Add refused without the change) is new. go vet ./internal/..., go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui -count=1 ./... under a temp HOME pass. yetone 2026-10-02 22:31:49 +08:00
  • d3c3059a19 usage: Hide accounts hides every account, not only an email address, and a WorkBuddy account's card says how many credits it used each day (#568: 默认情况下启动邮箱打码时,workbuddy的用户名那里不会被打码;用量统计能不能统计一下每天用了workbuddy多少积分) — the mask matched email addresses only, so a WorkBuddy account named by its nickname or phone number (and a Qoder, Kiro, Devin, Cursor, CodeBuddy, ZCode or plugin account named so) stayed in sight on a screenshot taken with 邮箱已打码. The page now learns the accounts' names from what magpie answers it (an allowance's user, a provider's account and its logins, a route's account: noteAccounts over every API answer) and hides them as it hides an address, in a row, a sentence and a tooltip, on Usage and Routing; a card's own name (WorkBuddy, ZCode signed in with no name of its own), a provider's or an agent's is never taken for an account, nor is a name inside a longer word. The tray panel hides them too, following the window's setting as it is turned (a storage event); the button is now Hide accounts (账号打码). WorkBuddy's meter tells only what the cycle has used against what it grants, the built-in's card and its plugin's alike, so magpie keeps each reading beside the one before in credits-daily.json (by provider and account: the two figures and the days' sums, nothing else) and counts what was used in between on the local day of the later reading; a reading that has used less (a cycle started again, a pack gone) counts nothing and counting goes on from it, credits granted count nothing, an error or a reading kept from before isn't one. The card, under its Credits meter, says the period's credits (今天 n / 7 天共 n) and over more than a day draws a bar a day, each day's figure in its tooltip, the days before magpie began counting an empty dashed slot saying so, and "Counted since <day>" while that falls in the period. TestDailyCredits (days, a reset, a check-in's grant, a gap, both builds, error and kept readings, another vendor, the file's size, a year on) is new; account-mask.test.cjs and credit-days.test.cjs (WebKit, en and zh) fail without the change and pass with it; mask-emails, click-scroll and balance-parts pass in WebKit. go vet ./internal/..., go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui -count=1 ./... under a temp HOME pass (TestFXInState, TestProvidersAnswerWhileListsComeIn, TestClaudeSignedOut and TestClaudeAccountNamedAsItsLogin timed out once under load and pass alone). yetone 2026-10-02 22:17:45 +08:00
  • 0887a9d7eb cursor: a model Cursor serves only in Max Mode is asked for again in Max Mode, and the models Cursor's picker offers beyond cursor-agent models (GLM-5.3, GLM-5.3 Flash) are listed (ARNO on Discord: 调用cursor模型报错:AI_APICallError: Cursor: Max Mode Required: The model "gpt-5.6-luna-low" requires Max Mode to be enabled; cursor provider里少了glm-5.3和glm-5.3-flash模型) — the built-in Cursor (deprecated, served by @magpie-community/opencode-cursor-auth once moved; that plugin's 0.1.7, magpie-community/plugins 3ae3e43, carries the same two fixes) asked every model with no max_mode in the Run's ModelDetails (7) or RequestedModel (2), and a model whose picker entry has supportsNonMaxMode false or a variant isMaxMode (gpt-5.6-luna-*) is refused MAX_MODE_REQUIRED, where cursor-agent turns Max Mode on itself. askCursor now asks such a model again with max_mode set in both when Cursor refuses it so, and keeps asking it in Max Mode from then on (cursorMaxOnly). The list came from cursor-agent models (GetUsableModels) alone; Cursor's picker, AiService/AvailableModels, also offers models that list leaves out, so the account's list now adds them (cursorPickerModels: one per variant slug, else the model's name; hidden, Tab-only, chat-only, non-agent ones and the four the CLI drops stay out, and so does a model the list has under any of its slugs), the picker best-effort. TestServeCursorMaxModeRequired (a fake Cursor refusing gpt-5.6-luna-low without Max Mode; it fails without the change) and TestCursorPickerModels (no GLM-5.3 without the change) are new, against fakes; no request was made to Cursor. The mover's min stays 0.1.5 until 0.1.7 is published. go vet ./internal/..., go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui -count=1 ./... under a temp HOME pass (TestProvidersAnswerWhileListsComeIn, TestUserPath and TestFXInState failed once each under load and pass on a rerun). yetone 2026-10-02 22:18:13 +08:00
  • 5d7c7d2808 quota: each subscription account, plan and key says when it last answered, and the latest is marked last (#570: quota --json / GET /v1/magpie/quotas 增加每个订阅、plan、key 最近一次应答请求的时间) — under routing Smart nobody is first for good, so a menu-bar widget had nothing to tell the one in use by: a Quota's asOf is when its reading was taken, accounts --json covers signed-in Codex and Claude accounts only, and /v1/magpie/route wants a session. Each Quota in magpie quota --json and GET /v1/magpie/quotas now has lastServedAt (RFC3339, left out when it hasn't answered) and last: true on the latest (each of a key's plan and balance, which are one key). The gateway notes each answer where servedCandidate counts it (provider.NoteServed): an account by its agent and user, a key by its provider and key id, matched to a card by its user, or by the key's name or mask, or, for a card with no user, by any of its provider's. It is kept in served.json beside providers.json, entries older than 30 days dropped, since magpie quota runs in a process of its own and computes the report itself (provider.QuotaReport) rather than asking the gateway: both read the file, so they say the same, and a restart keeps it. TestQuotasLastServedAt (gateway: a request answered by one key, its balance card has lastServedAt and last, the other key's neither) and TestQuotaJSONLastServedAt (the CLI's --json, fake CLIs and keychain on PATH) fail without the change; TestQuotasLastServed covers the matching (account by user, key by name and by mask, a card with no user, the tie, read back from the file alone). README tells the fields. go vet ./internal/..., go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui -count=1 ./... under a temp HOME pass. yetone 2026-10-02 22:16:14 +08:00
  • 1c047f3685 gateway: another computer is let in by the gateway key it sends wherever its client puts it, beside an empty, placeholder or stale token (悠悠哥 on Discord: magpie shared on a NAS, a client's model list from http://192.168.0.200:3425 failed, Anthropic Messages format, while from magpie on 127.0.0.1 it came) — loopback takes any token, so a request from this computer is answered whatever key it carries, but one from another computer is let in only by an enabled gateway key, and the key was read from one place: callerKey took Authorization whenever the header was there, and looked at x-api-key, x-goog-api-key or ?key= only when it wasn't. A client that sends the key in x-api-key with "Authorization: Bearer " beside it (no token set), "Bearer undefined", or a stale token there (ANTHROPIC_AUTH_TOKEN left at magpie) was refused from the network — "no API key was sent" or "not an enabled magpie gateway key" — while the same request on 127.0.0.1 listed the models; "bearer <key>" in lower case was read as the key "bearer <key>". Now callerKeys reads every place a key comes (Authorization's bearer token, any case, then x-api-key, x-goog-api-key, ?key=), passing over an empty one, and identifyCaller lets the request in by whichever is an enabled gateway key, the refusal still naming the first; a key-shaped value in any of them (managedKey) also has a request from this computer counted by its key. Nothing is let in that wasn't carrying an enabled key. Nothing else differed for the network: the gateway checks no Host header (Host 192.168.0.200:3425 is answered), lists the same catalog to a keyed LAN caller as to loopback (no per-key model filter), and answers /v1/models, /models and /v1/models?limit= alike, in a shape Anthropic and OpenAI clients both read. TestLANModelsWithKeyBesideAnotherToken (from 192.168.0.9 to Host 192.168.0.200:3425: the key in x-api-key beside an empty, bare, "undefined" or "magpie" bearer, lower-case bearer, x-goog-api-key beside a stale bearer, over /v1/models, /models and ?limit=1000, each listed and counted by the key; a wrong key or none still 401; loopback counted by the key beside a token) fails without the change (401 "no API key was sent" for the key beside "Bearer "). go vet ./internal/..., go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui -count=1 ./... under a temp HOME pass. yetone 2026-10-02 22:14:18 +08:00
  • 1a50db1a8a gateway: redact image and video prompts (#567) Zhizhen He 2026-10-02 22:05:27 +08:00
  • c02e49b788 gui: serialize usage refreshes and pause hidden routing frames (#551) Snowyy 2026-10-02 22:04:57 +08:00
  • 96446806bd zcode: a Start Plan account neither lists nor offers GLM-5.3, and the move to the plugin doesn't count it lost (悠悠哥 on Discord: "仍使用内置:插件不提供 GLM-5.3" moving ZCode onto its plugin) — GLM-5.3 is the Coding Plan's alone (ZCode's builtin config lists GLM-5.3-Flash, GLM-5.2 and GLM-5-Turbo for account:zai-start-plan), and the plugin serves a Start Plan account what ZCode does. But a Z.ai account with a key made and no Coding Plan bought, whose requests go to the Start Plan, was taken for a Coding Plan account until a request found its route (zcodeOnStart with no context said the Coding Plan), and afterwards was listed whatever list was fetched last (the Coding Plan's, another account's or its own before), so GLM-5.3 was listed, could be ticked, and the move found the plugin "short" of it. Now: before a route is found, the plan the account showed at sign-in says it (zcodeOnStartAs, zcodeStartNamed: "Start Plan"); on the Start Plan, a model the plan doesn't serve (zcodeStartServes: ZCode's config on the machine, else zcodeStartModels) is unusable, taken out of a fetched list and out of the picks, with a list fetched or not (Exposed); and a mover may say which models the built-in served on an account (mover.builtin), ZCode's saying a Start Plan account was never served GLM-5.3, so one the built-in didn't serve either is no loss. A model really lost is named with the account and its plan: "the plugin doesn't serve GLM-5.3 for a@x (GLM Coding Pro), though the built-in does" (插件没有为 … 提供 …(内置提供)), the why's args carrying user. TestZCodeStartPlanHasNoGLM53 (a Start Plan account before and after the Coding Plan's list was fetched, GLM-5.3 ticked or not; a Coding Plan account still has it; the mover's builtin) fails without the change (it listed GLM-5.3 GLM-5.3-Flash GLM-5.2 GLM-5-Turbo); TestMoveToPlugin's new case (a model the built-in didn't serve moves; one it did is named with "a@fake (Fake Start)"); plugin-move-unserved.test.cjs (WebKit, en and zh, with an account and without) fails without the app.js change and passes with it, plugin-move.test.cjs passes. No ZCode request or sign-in was made. go vet ./internal/..., go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui -count=1 ./... under a temp HOME, go test ./internal/gui pass. yetone 2026-10-02 22:00:31 +08:00
  • 06584e15fc qoder: a model is free only at a price of 0, as Qoder's own client shows it (01huadalang on Discord: Qoder shows Qwen3.8-Max at 0.5× credits with its off-peak 40% discount, magpie called it free) — Qoder's listing has is_free true on Qwen3.8-Max at a price_factor of 0.5, and the built-in took either is_free or a price of 0 for free. Now, as the plugin's freeOf does (magpie-community/plugins 6fd444c): a listed price_factor (priceFactor) says it, free only at 0; a price of 0 only while an active promotion lasts, with a price before it (before_promotion_price_factor > 0), is a discount, not free; a limited-time 0× (Qwen3.8-Flash, its 0.1× struck through) is free while it is; is_free counts only on a listing with no price. TestParseModelsFreeIsThePrice, with the listing's real entries (Qwen3.8-Max's off-peak promotion, Qwen3.8-Flash's original_price_factor), fails without the change (Qwen3.8-Max and the promotion-window models read free) and passes with it; TestParseModelsFree's is_free-at-price-1 now reads paid. No request was made to Qoder. Moved Qoder users get it from opencode-qoder-auth 0.2.1 (the mover's min stays 0.2.0 until that is on npm). yetone 2026-10-02 22:00:31 +08:00
  • 7464ee5075 usage: bound retained chunks and invalidate same-stat rewrites (#563) Snowyy 2026-10-02 22:00:10 +08:00
  • 94bb3e2ca6 plugins: Check for updates asks npm now for each installed plugin's newest version, and each row says what it found (the owner: 插件的检查更新按钮还是得做) — the community's plugins update by themselves a little after magpie starts and every 6 hours, and the page knew of anyone else's new version only from what npm said within the hour (Info's cache), so a reader who heard a plugin had a new version had no way to have magpie look. The foot of Installed (beside Update all) now has Check for updates, shown when a plugin is npm's (a folder's or a git one's has no versions there; a git one keeps its own Update, which fetches it again): it says Checking… with a spinner while POST /api/plugins/check asks npm, a few at a time, past the hour's cache (plugin.CheckNow, through npmAsk, which is npmInfo telling why npm said nothing), and nothing is installed. Each row then says it: "v0.0.9 out" titled "v0.0.7 installed, v0.0.9 on npm" with its Update (the existing upgrade path, Upgrade → Add), "Up to date", or an amber "Couldn't check" chip with a dot, titled why — npm not reached (check your connection or proxy), npm turning requests away (429, try again in a few minutes), npm answering with an error, or npm not having the package (fetchJSON's answers other than 200/404 are now a statusError, so a 429 is told apart). The status line says how many have updates, or that every plugin is up to date, and how many couldn't be checked and why; npm not reached at all is "Couldn't check for updates: npm couldn't be reached — check your connection or proxy". A community plugin's Update is titled that magpie would update it by itself within a few hours and Update does it now. What npm said is kept as Info keeps it, so the rows and the market read it, and an update found for the reader (someone else's, or pinned) waits as the background look's do, putting the dot on Plugins; one npm didn't answer for keeps what waited before. Every new string has its Chinese; the click never scrolls the page. TestCheckNow (an httptest npm registry: the community's and someone else's updates found past an hour-old cache saying otherwise, up to date, 404 missing, 429 limited, 500 registry, git and folder not asked, nothing installed, the cache and the waiting list updated) and TestCheckNowOffline (npm not reached: offline, what waited still waits) are new, as is TestPluginsCheckRoute; plugin-check-updates.test.cjs (WebKit and Chromium, en and zh: Checking… while npm is asked, the rows' out / up to date / couldn't check chips and titles, the status line, Update after the check, offline said plainly, the view's scroll unmoved) fails without the change and passes with it. plugin-updates, plugin-market, plugin-progressive, plugin-git, plugin-local and click-scroll pass in WebKit and Chromium. go vet ./internal/..., go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui -count=1 ./... under a temp HOME pass. yetone 2026-10-02 21:56:17 +08:00
  • 14b9928d33 providers: a relay's APIs found at a click, and a model asked on the one API its vendor serves it on (01huadalang on Discord: 能不能加个供应商检测协议功能,类似 cc 的功能一键检测支持什么协议,发一个最小请求看是否返回200,还有每个模型自定义协议,例如有的供应商一个 api 里有很多模型但是不同协议) — a custom provider was given one base URL and a protocol picked by hand, and which of Chat Completions, Responses and Anthropic Messages a relay answered was learned by trying, one Save and Test at a time. A model's API came only from the vendor's list or a built-in (Copilot, Factory, Bedrock, OpenCode), so a relay serving some models on chat completions and others only on /v1/messages behind one key had every model tried on chat first, and the ones it had no channel for there failed before the gateway got to Anthropic's. The editor's Base URL now has Detect APIs, with a model to try beside it: it posts provider/detect with the form as it stands (the key typed, or the saved one), and Provider.Detect sends each of the three APIs Test's smallest request (tiny: 16 tokens, not streamed) at the URL the provider has for it, else at the URL typed as that API takes it (DetectBase: an endpoint pasted whole cut to its base, …/v1 for OpenAI's two, the root for Anthropic's), in parallel, and says what each answered. With no model typed, one is picked from the provider's list, else from the vendor's /models (asked then and kept nowhere): a Claude model for Anthropic's, an OpenAI one for Responses. Only the URLs typed are asked, only at the click, and nothing is saved. The editor shows each API's URL with its time or its status and error; "Use these" sets the URLs of those that answered (one found missing there, 404 or 405, cleared) and moves the Base URL's protocol to one that did, with no redraw and no scroll. A model's API is new in Names & levels (settings.ModelAPIs, "<provider>/<model>"): for a key's provider with URLs for two or more APIs, each model can be set to Auto or to one of them. It is staged with the rest and made with the Save (ModelPref.API, SetModelAPI), which refuses an API the provider has no URL for and a sign-in's provider. Once set, APIs() returns that API alone, so the gateway, Native and a model's test all ask it there; it is ignored once its URL is gone. After a detection with a model typed where only some APIs answered, "Ask <model> on <API> only" stages the same thing. Gemini isn't probed, since magpie never asks a vendor on it. yetone 2026-10-02 21:54:04 +08:00
  • 74fa1ca51d gateway: bound individual body reads without limiting streams (#553) Snowyy 2026-10-02 21:53:23 +08:00
  • cac5023fb7 plugin: test automatic refresh after bare invalidation (#564) Snowyy 2026-10-02 21:42:27 +08:00
  • 7bd383855b fix(provider): match Antigravity quota to collapsed model families (#565) Vocllum 2026-10-02 21:37:47 +08:00
  • 4afb698638 library: checking skills for updates asks GitHub with a GitHub token, set in Settings → Network and sharing or taken from GITHUB_TOKEN / GH_TOKEN, and a check GitHub limited says until when and that a token raises it (Player on Discord: 资源库检查技能更新时提示 github is limiting request,希望能设置 GitHub token) — the check asks GitHub's API for the last commit to each skill's folder, which GitHub answers 60 times an hour for an address without a token, so a library of a few repositories, checked twice, or a network shared with others, used it up, and the check said only "GitHub is limiting requests from here until 15:04", with nothing to do about it. Now the API's requests carry Authorization: Bearer <token> (5,000 an hour) when there is one: the GitHub token set in Settings → Network and sharing (a new GitHub row: typed masked, saved on its own with POST /api/settings/github-token, then shown as ghp_…0123 with Remove; one with a space is refused in the row, what was typed kept), else GITHUB_TOKEN, else GH_TOKEN, the row saying when one of those is used. gh auth token is not run: it would borrow another app's credential without asking. The token goes to GitHub's API's host alone (withGitHubToken checks the request's host against githubAPI's), never to codeload, raw.githubusercontent or anything else, and Go's client drops it on a redirect to another host; rtk's latest-release lookup on api.github.com carries it too. It is kept in settings.json as the LAN key is: the page is told only the masked one and where it is from (githubTokenMask, githubTokenFrom), a save of the Settings page keeps it, a backup or sync without keys leaves it out, and restoring one without keys keeps this machine's. Each folder's answer is kept with its ETag and asked for again with If-None-Match, so an unchanged one is a 304, which GitHub doesn't count against the limit. A rate limit (403 with the limit spent, a secondary limit's Retry-After, or 429) now says it plainly: without a token, "GitHub's rate limit for requests without a token (60 an hour) is used up until 15:04. Add a GitHub token in Settings → Network and sharing to raise it to 5,000 an hour"; with one, that the token's limit is used up until then; and the check carries limited {until, token} for the page, which says the same in its own words, in English and Chinese, in the status and on each Not checked tag. A token GitHub refuses (401) says which: the one in Settings, or GITHUB_TOKEN / GH_TOKEN. The library's test sandbox clears GITHUB_TOKEN and GH_TOKEN, so no developer's token reaches a fake GitHub. TestGitHubTokenSent (no token: none sent; GH_TOKEN; GITHUB_TOKEN before it; Settings before both; codeload never gets it), TestGitHubTokenOnlyToTheAPI (api.github.com only, not codeload, raw, a look-alike host or the gateway; dropped on a redirect), TestGitHubRateLimitSaid (until when and the token advice without one, not with one, the token never in the error, a secondary limit, a refused token from Settings and from GH_TOKEN), TestGitHubConditionalRequests (If-None-Match, and the 304 answered as the 200 was), TestGitHubTokenBackup (left out without keys, fails without the change; kept on a keyless restore), TestGitHubTokenSetting (set, masked, kept by a Settings save, refused with a space, removed with the environment's then said) and github-token.test.cjs (WebKit, en and zh: the row, a refusal keeping what was typed, saved masked with the token nowhere on the page, removed with GH_TOKEN said, nothing scrolls; a limited check's status and tag say until when, and the token advice only without one) are new; TestCheckSkills, TestSettingsSaveKeepsWhatItDoesNotSend (now with the token) pass. GitHub was never called: httptest stands in for it. settings-sections, search-api, click-scroll, library-toggle-status, library-all, otel, lan-docker, every-skill pass in WebKit. go vet ./internal/..., go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui -count=1 ./... under a temp HOME pass (TestFXInState failed on its TempDir cleanup, the known flake). yetone 2026-10-02 21:16:45 +08:00
  • e60b721532 providers: Tencent Cloud TokenHub pay as you go is a preset, its global endpoint and China's one row in the add sheet (Jorben on Discord: magpie's Tencent Cloud is TokenHub's Token Plan subscription, whose endpoints pay-as-you-go keys can't use; TokenHub's pay as you go is at tokenhub.tencentmaas.com and tokenhub-intl.tencentmaas.com and speaks Chat Completions, Responses and Anthropic Messages) — the only Tencent preset was tencent-token-plan, at the plan's own api.lkeap.cloud.tencent.com/plan/v3 and /plan/anthropic, where a pay-as-you-go key is no good, so such a key could only go in a custom provider by hand. Now tencent-tokenhub ("Tencent Cloud TokenHub", Singapore, tokenhub-intl.tencentmaas.com) and tencent-tokenhub-cn ("Tencent Cloud TokenHub (China)", Guangzhou, tokenhub.tencentmaas.com) are presets, as TokenHub's language-model API page (1823/130079) gives them: chat completions and Responses at <host>/v1 (Responses converted from chat on TokenHub's side for the models that speak chat alone, 1823/133813), Anthropic messages at <host>/v1/messages with the key as x-api-key, the OpenAI APIs with it as a Bearer, and the model list at /v1/models. Being id and id-cn they pair into one row in the add sheet, its editor switching between them with the key kept, beside the plan's Tencent Cloud row, with the plan's icon (tencentcloud-color), the note "Pay as you go" (按量付费), and models.dev's tencent-tokenhub entry for names and prices; Hosts, as it serves DeepSeek, GLM, Kimi, MiniMax and Qwen besides Hy, so its list is no maker's word on theirs. Keys are made at console.tencentcloud.com/tokenhub/apikey and console.cloud.tencent.com/tokenhub/apikey. An entry imported from another app at either host, on any of the three APIs, is the preset. The plan's preset, its endpoints, models and saved providers are untouched; its note "TokenHub · subscription" now has its Chinese (TokenHub · 订阅). TestTencentTokenHub (endpoints, pair, import from each API, the plan unmoved), TestTencentTokenHubList (its /v1/models as its docs show it, asked with a Bearer, an embedding model left out) and TestTencentTokenHubRoutes (both presets at a fake TokenHub: chat, Responses and messages each reach /v1/chat/completions, /v1/responses and /v1/messages untranslated, Bearer on the first two, x-api-key on messages) are new and fail without the change (no such preset). No request was made to Tencent and no real key used. add-sheet.test.cjs passes in WebKit. go vet ./internal/..., go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui -count=1 ./... under a temp HOME pass (TestFXInState's known TempDir cleanup flake aside). yetone 2026-10-02 21:11:03 +08:00
  • 055857e183 test: observe stale DSH route before allowing background repair (#560) Snowyy 2026-10-02 20:59:59 +08:00
  • 66844ab0f9 usage: cover growing rewrites followed by local and external appends (#559) Snowyy 2026-10-02 20:59:51 +08:00
  • f1e265ea3d gateway: a request from another machine turned away says whether a key came at all, and names a wrong one by its last four characters (azir12345, #545: Codex on a second computer got "API key is disabled, removed or invalid" while the same key worked with curl) — the refusal was one message for a request with no key and one with a key that isn't an enabled gateway key, so a client that drops its key (a proxy stripping Authorization, a base_url pointing elsewhere, an env key never read) read as a wrong key, and the reporter had no way to tell which. lanGuard's refusal (identifyCaller, refusedKey) now says "no API key was sent: send a magpie gateway key as Authorization: Bearer <key> (or x-api-key)" when none came, and "the API key ending in XXXX is not an enabled magpie gateway key: it is disabled, removed or mistyped" when one did, its last four characters named only for a key of 12 or more so a short one isn't mostly given away; magpie's log says the same with the caller's address. Requests from this computer are still let through without a key. TestLANRefusalSaysWhichKey (no key, a long wrong key named by its end and nothing more, a short wrong key not echoed) fails without the change and passes with it; TestQuotasOverLAN passes. go vet ./internal/..., go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui -count=1 ./... under a temp HOME pass. yetone 2026-10-02 20:48:14 +08:00
  • 1e4174cada sessions: Cursor CLI's chats are on the Sessions page, each listed, opened, resumed and deleted to magpie's trash as another agent's sessions are (@ownsh1p on X: why doesn't the Sessions page show cursor-agent's sessions?) — magpie read the session files of Claude Code, Codex, OpenCode, Pi and the rest, and none of Cursor's. cursor-agent (2026.10.01) keeps a chat in its config folder ($CURSOR_CONFIG_DIR, else $XDG_CONFIG_HOME/cursor, else ~/.cursor) at chats/<md5 of the cwd>/<chat id>/store.db, a SQLite file of two tables as its run store (cursor-sdk-local-runtime's sqlite-blob-store) makes them: meta, whose row "0" is the chat's metadata as JSON in hex (agentId, name — "New Agent" until it has a title — createdAt, lastUsedModel, latestRootBlobId and, for a subagent's chat, subagentInfo with its parent's id), and blobs, content-addressed by the hex of their SHA-256. The root blob is a ConversationStateStructure (protobuf): field 1 the ids of the conversation's messages in order, field 13 the archives a compaction summed up (each one's field 1 the messages it summed up), field 26 when the conversation started. Each message is the AI SDK's JSON ({role, content: a string or parts — text, reasoning, tool-call, tool-result, image, file}). Newer builds also write meta.json beside store.db: title, createdAtMs, updatedAtMs, isSubagent and cwd, the one place the chat's folder is said in words (the folder's name is only its hash). This was read from the installed bundle and matches what other projects that read these stores found; no real chat was opened. yetone 2026-10-02 20:44:16 +08:00
  • a04967d1de gui: use adaptive duration units for recent calls and provider tests (#485) (#492) Luis Araya 2026-10-02 06:44:29 -06:00
  • f362d755af perf(gateway): normalize null escapes in DeepSeek tool patterns via gjson (#490) Luis Araya 2026-10-02 06:43:21 -06:00
  • c2222d17bc otel: bodies can go whole to the trace, not cut at 256 KB (#538) (#555) mofelee 2026-10-02 20:39:07 +08:00
  • 294cf1225d providers: a built-in subscription whose plugin the user installed is the plugin's, and the "+ provider" sheet lists it once (ARNO on Discord: the sheet had Qoder 国际版 and Qoder CN under 订阅, and Qoder ● 2 and Qoder CN ● under 来自插件, the same two subscriptions twice) — a deprecated built-in hands its id to its plugin only through an explicit Move (the editor's Move to plugin) or Adopt (clicking the built-in's tile before signing in, since v0.1.642); the automatic list, Retiring, is empty, and f099d07e (v0.1.659) gave Qoder CN its mover but moves nobody on its own. A user who installed the Qoder plugin from Plugins was never moved: the plugin's providers took the ids qoder-plugin and qoder-cn-plugin (provider.PluginID, as the built-in ids were not moved), the add sheet listed those under 来自插件, and the built-ins stayed under 订阅 as deprecated tiles. Qoder international (moved since 09-30) was listed twice the same way. Existing migration logic, unchanged: movers (built-in id → plugin package, min version, out/back of its accounts), Move (each account checked through the plugin, everything put back on any failure), Adopt (no accounts: install the plugin and mark it moved), MoveBack, the migrations.json states (moving, plugin, back, failed, a failed one retried after 6 h), and KeepRetiringMoved in the gateway (20 s after start, then hourly). Added: provider.HandOver, for each mover whose plugin is installed, enabled and at least its min, serving the built-in's id, not signed in under its own -plugin id, and not moved back or failed in the last 6 h: with no built-in accounts it Adopts at once; with accounts it Moves them, with Move's rollback, only from the hourly loop (HandOver(ctx, true) beside MoveRetiring). The GUI's plugin add/update/upgrade and the CLI's plugin add/update call HandOver(ctx, false) once they succeed. One the plugin is already signed in to under its -plugin id is left as it is: handing the id over would rename the models agents use (qoder-plugin/... to qoder/...). Until a hand-over happens, and whenever it can't, the add sheet hides a deprecated built-in whose plugin serves its id (replacedSub in app.js), keeping it only while it is signing in; a moved one was already one tile. With the plugin not installed the built-in tile stays, as clicking it is how a new user gets the plugin. A provider signed in through the built-in keeps working and stays in the provider list. AGENTS.md says so. The manual Check for updates button for plugins that was also asked for is not shipped: per the owner, official community plugins update on their own. TestHandOver (a fake plugin under a temp HOME: nothing without the plugin; no accounts: moved at once; moved back: left; built-in accounts: left by HandOver(false), moved by HandOver(true), the built-in's emptied and the plugin signed in; the plugin signed in on its own: left) is new. plugin-replaced.test.cjs (WebKit, en and zh: no plugin, both built-ins under Subscriptions; the plugin installed with Qoder signed in to two accounts, the plugin's two tiles alone; Qoder moved, its one tile under Subscriptions and Qoder CN the plugin's; a built-in Qoder CN signed in, its provider row still listed) fails without the app.js change (all four tiles) and passes with it. deprecated-subscriptions, plugin-move, click-scroll, add-button, mobile-web, signin-paste-key and every plugin-* test pass in WebKit. go vet ./internal/..., go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui -count=1 ./... under a temp HOME pass (TestFXInState failed on its TempDir cleanup, the known flake). yetone 2026-10-02 20:31:19 +08:00
  • 0c8c4f4cab sessions: each project folder's row starts with a box, so several folders are picked and deleted together with the bar's Delete, the count still in sessions (mintonight, on #527: 现在这样一次只能删一个项目…在项目文件夹那一行的字首加一个复选框…多选同时删除多个项目,并且还能复用已选 x 项那一行右侧的删除按钮,这个数量 x 的计数逻辑还是按会话数量来,不要把文件夹这个复选框当做一个数) — #527 gave each folder's row a Delete all, which deleted one folder at a time beside the box-and-bar way sessions are deleted. Each folder's row now starts with a box, as each session's row does, where magpie can delete the agent's sessions: ticked, it picks every session of the folder shown, folded or not, and the bar says "{n} selected" (已选 {n} 个) counting those sessions, the folder not one of them; unticked, it lets them go; a session's own box ticked or not part-ticks or ticks its folder's at once; under a filter it picks the ones the filter shows, as the bar's select-all does. Folders and single sessions picked together go with the bar's Delete, which asks in magpie's own dialog as before: a pick that is one whole folder is headed "Delete all {n} sessions in {folder}?" with its path, as Delete all's was; several whole folders "Delete all {n} sessions in {k} folders?" (删除 {k} 个文件夹下的全部 {n} 个会话?); anything else "Delete {n} sessions?". They go to magpie's trash through sessions/delete as before, and one written to in the last minute is left and said so. Delete all is gone from the folder's row: it did what the box and the bar's Delete do, two ways for one thing (the lesson of #526's Close). The box's label is "Select every session in {folder}" (选中 {folder} 下的全部会话); the box doesn't fold the folder or move the page; no left-border accent. sessions-folder-delete.test.cjs now covers the boxes (first in their row, none for OpenCode), a folded folder picked and asked about by name, Cancel posting nothing, two folders and a mixed pick, the part-tick, a filtered pick, the whole of app with its running session refused; it fails without the change (no folder box). sessions-manage, sessions-purge, click-scroll, sessions-toolbar, session-terminal, sessions-bar-names and sessions-overview pass in WebKit. yetone 2026-10-02 20:15:11 +08:00
  • eaf6a98465 gui: a waiting sign-in has one button that puts it away, Cancel, and no Close beside Open again doing the same (mintonight, on #526: 那你这样改「取消」和「关闭」功能不是重复了吗,只保留一个就行了) — the fix for #526 had two causes to answer: the box's Cancel was pushed out of the account list by Qoder's long device link, and the reporter asked for a Close beside Open again. The first was fixed in the layout (the list's column is minmax(0, 1fr), the link cut short), which brought Cancel back in sight; the Close added beside Open again then did exactly what Cancel does (cancelSignIn: signin/<id>/cancel posted, the polling stopped, Add another back), two buttons for one thing in the same box. Close is gone; Cancel stays, as it is the button every other state of the sign-in box has (failed, installing, paste a code, the risk notice), at the box's right, and now carries Close's title, "Stop waiting for this sign-in" (不再等待这次登录). Open again stands alone in its row, with the import and key ways beside it where a subscription has them. The editor's Cancel, Save and Remove still put a waiting sign-in away. signin-close.test.cjs now checks that Open again stands alone, that no Close is in the box and that the one Cancel, titled so, posts the cancel, stops the polling and brings Add another back without moving the page; it fails without the change (Close still beside Open again). signin-link, signin-again, plugin-signin, routing-signin and signin-closes-sheet pass in WebKit (signin-callback's one case launches Chromium alone, which can't start here without its ICU data). yetone 2026-10-02 20:07:16 +08:00
  • 7ae1c344b3 plugins: a plugin signed in to one of its subscriptions no longer reads as signed out (#556: 供应商这边显示登录了,插件那边掉登录了) — the Qoder plugin signs in to Qoder and Qoder CN, the WorkBuddy one to WorkBuddy AI and WorkBuddy; the reporter was signed in to Qoder and WorkBuddy AI, which Providers showed, and the Installed row showed "Qoder: signed in · Qoder CN: not signed in" beside a blue Sign in, the row's primary button, which read as the plugin having lost its sign-in. Nothing was lost: the second one is a subscription of its own that was never signed in to. Now, when one of a plugin's subscriptions is signed in, the row's button names the one it signs in to ("Sign in to Qoder CN", 登录 Qoder CN) and is a plain one, not the primary; the not-signed-in one's tooltip says it is a subscription of its own that the signed-in one works without; and Discover's card says Signed in (opening the signed-in one in Providers) rather than Sign in. A plugin signed in to nothing keeps its blue Sign in. plugin-two-subs.test.cjs (WebKit; en, zh) fails without the change and passes with it; plugin-market, plugin-move, plugin-moved-signin, plugin-local, plugin-updates and click-scroll (one flake of the trace's "picking request", then 2/2) pass. yetone 2026-10-02 20:05:25 +08:00
  • 5332c0c638 usage: each subscription account's tokens and cost, by the account that answered (#557: 希望用量账本能按订阅账号统计,每个 Codex / Claude 账号各用了多少 token、多少钱) — a call's record named the upstream key it went out on (ProviderKeyID/Name, #500) but never the subscription account, so with two Codex accounts signed in, or a Claude account that took over after a failover, the ledger could say what Codex cost and not what each account did. A record now carries providerAccount, the account that answered as the Routing trace names it (Account.User: its email or login, never a token), set by the gateway at the try that went out (the candidate loop's last try, so a failover's answer is the account that took over and the refused try stays the first account's; an X-Magpie-Account pin is the pinned one), and on the decide, draw and video paths too. Record.Account() gives it, or for a record written before the field the account its host named (Where() has long written "chatgpt.com as dee@example.com" for an account), and otherwise "" — shown as "account not recorded", never inferred from today's sign-in. The summary groups calls by provider and account (Summary.Accounts, the same email on Codex and on Claude being two accounts), priced as every other group; the ledger's Filter has Account, the search finds a call by its account, the CSV has a provider_account column beside provider_key_name (the provider's identity columns together, the caller's still last), and the Requests page's packed rows keep it and offer the period's accounts. magpie usage prints an accounts table after the provider keys, and magpie usage --csv --account <name> keeps one account's calls. The app's Usage page lists Accounts on the Overview (tokens and cost per account, the provider under its name, an unrecorded one in the faint colour, no stripe down a row's side) and has an Account filter on Requests, each account noting the providers it answered for, which asks for and exports that account's calls without moving the page. OTLP export is an allowlist and does not carry the account. TestUsageNamesPinnedAccount (both pinned accounts on magpie's endpoint and on Codex's own, the trace's Who and the record agreeing, no token in usage.jsonl) and TestUsageNamesAccountThatTookOver (Ordered routing, the first account refusing: the answer recorded as the spare, the refusal as the first) fail without the gateway change; TestSummaryByAccount, TestLedgerByAccount and TestRequestPageByAccount cover the grouping, the host fallback, "not recorded", keyed providers kept out, the filter, the search, the CSV column and the packed rows; TestLedger and TestLedgerWithSessionLogCalls take the new column. usage-accounts.test.cjs (WebKit and Chromium, en and zh: the Overview's accounts and costs, the Account picker's notes, its request and export, All accounts clearing it, scrollTop unchanged) fails without the asset change; api-key-usage, click-scroll, panel-usage, request-archive-usage, tray-usages, claude-usage-asked and every usage-* test pass in WebKit. go vet ./internal/..., go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui -count=1 ./... under a temp HOME pass (TestFXInState failed once on its TempDir cleanup, passes alone). yetone 2026-10-02 20:15:30 +08:00
  • 5b602b850a plugin: bound provider refreshes and wait for changes after failure (#552) Snowyy 2026-10-02 20:05:23 +08:00
  • f099d07e92 qoder: Qoder CN is moved onto its plugin too, @magpie-community/opencode-qoder-auth 0.2.0, which serves it as "qoder-cn" (the user: 为啥 Qoder CN 没有插件啊) — the plugin had Qoder alone when the move was made (09-30), Qoder CN coming to magpie a day later, so Qoder CN stayed a built-in that wasn't deprecated. 0.2.0 serves both sites (Qoder CN signing in on qoder.cn with its own client id and no redirect_uri, chatting on gateway.qoder.com.cn, and on its device token when qoder.cn refuses the job token), so movers now has "qoder-cn" beside "qoder", both from qoderMover(id): an account's pairs, machine id and, for one chatting on its device token, deviceChat go to the plugin and come back. Both movers' min is 0.2.0. The PLUGIN-SERVED notices in internal/qoder, internal/provider/qoder*.go and internal/gateway/qoder.go and AGENTS.md's table now name both ids rather than saying Qoder CN isn't moved. TestQoderCNMover (a device-chat account and a job-token one, round trip) fails with deviceChat not brought back. go vet, go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui -count=1 ./... under a temp HOME pass. yetone 2026-10-02 19:59:36 +08:00
  • 9845e3af51 dsh: magpie's route keeps the protocol picked for it in dsh's Models page, OpenAI Responses or Anthropic Messages, where each round put Chat Completions back (01huadalang on Discord: 这些协议怎么改 responses,在 dsh 改了一会就会被 magpie 接管) — the route was written whole each round, api: openai-completions included (dshRouteConfig), so a route switched to OpenAI Responses in dsh's Models page went back to Chat Completions within the 30 s the loop takes. Each round now reads the route's api from the profile's patch list and keeps it when it is one of the three llm-pi-ai protocols the gateway serves (dshAPI: openai-completions at /v1/chat/completions, openai-responses at /v1/responses, anthropic-messages at /v1/messages); none, or one the gateway has no endpoint for (google-generative-ai), goes back to openai-completions. The base goes with the protocol (dshBaseURL): pi-ai hands it to the vendor's SDK as it is, and Anthropic's adds /v1/messages itself, so anthropic-messages is at the gateway's root and the two OpenAI ones at its /v1 (as omp and Pi have it); dshCheck asks for the same base, so a route on Messages isn't reported as pointed elsewhere. On Messages a Claude that thinks only adaptively gets compat.forceAdaptiveThinking, which llm-pi-ai offers there, since pi-ai asks every other model for a thinking budget it turns away. reasoningEfforts are as before on all three: llm-pi-ai (0.2.0-rc.2) makes them pi-ai's thinkingLevelMap, sent as reasoning_effort on Chat, reasoning.effort on Responses (off as the map's none, a model without none having off hidden) and output_config.effort for an adaptive Claude on Messages. TestDshRouteKeepsTheAPIPickedInDsh (a loop round and a sync on a route set to openai-responses, to anthropic-messages at the /v1 base, to google-generative-ai and to none; dsh's check clean, a second round writing nothing) fails without the change (openai-responses written back as openai-completions). TestDshResponsesReachChatVendor sends pi-ai's Responses request, with dsh's User-Agent, to the gateway for a model served on Chat Completions alone: it is asked in Chat with reasoning_effort high, the image, tool call and output carried over, and the reply comes back as Responses events. Read from the installed dsh's llm-pi-ai and pi-ai only; no dsh profile, live agent config or running magpie was touched. go vet ./internal/..., go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui -count=1 ./... under a temp HOME pass (TestFXInState failed once on its TempDir cleanup, passes alone). yetone 2026-10-02 19:35:36 +08:00
  • 50b554c9ab gui: an account row's tooltip joins its windows past the first two with " · " again, not a line each (reviewing #510) — #510 made the windows the row draws a filtered copy, the unlimited ones left out, and the separator still asked whether familyWindows had returned q.windows itself, which a copy never is: every vendor's tooltip put each window on a line of its own, as only a family's should. It now asks of the filtered copy. plugin-plan.test.cjs (an account with four windows: the tooltip is one line, C then D) fails without the change and passes with it in WebKit, en and zh; quota-families, quota-pools and panel-usage pass in WebKit, go test ./internal/gui passes. yetone 2026-10-02 19:38:58 +08:00
  • fa9daa6945 docs: every built-in subscription a plugin serves once moved says so in its code, and AGENTS.md says where a fix for it goes (user: 对内置的订阅 provider 在代码层面和文档层面加上注释,防止又有别的 agent 在改内置订阅 provider 的代码而不知道生效的其实是对应的插件代码) — a moved Cursor, Devin, Grok, Command Code plan, Kiro, Zed, Qoder, Factory, Xiaomi MiMo, ZCode or WorkBuddy (provider.Moved; since v0.1.642 also the default for a new sign-in, provider.Adopt) is signed in, listed, requested and metered by its @magpie-community plugin, and none of the built-in's code in internal/provider/<name>*.go, internal/gateway/<name>.go, internal/zed or internal/qoder serves it; a fix made there alone never reaches those users. Each of those files now opens with a PLUGIN-SERVED notice naming the subscription's id, its plugin package, where the plugin lives (magpie-community/plugins, packages/<name>) and the mover whose min to raise so moved users get the fixed plugin (keepMovedCurrent); Qoder's adds that Qoder CN isn't moved and still runs there. AGENTS.md (CLAUDE.md imports it) lists the moved built-ins with their packages and movers, how to tell whether a user's is moved (move.state "plugin", onPlugins), the order a fix goes in (the plugin, publish, raise min, the built-in only for those not moved), the magpie code that does run for a moved one (internal/plugin, plugins.go, plugin_usage.go, pluginsignin.go, migrate*.go, app.js's subOf/pluginSubs/startPluginSignIn), and the parity tests. movers in migrate.go and SUBS in app.js point there too. TestMovedBuiltinsSayTheirPlugin fails for a mover whose code carries no PLUGIN-SERVED notice naming its id and package (it failed while a wrapped package name was split). Comments and docs only, no behaviour changed. go vet ./internal/..., go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui -count=1 ./... under a temp HOME pass (TestProvidersAnswerWhileListsComeIn and TestClaudeSignedOut failed once on their timing under load, pass alone; untouched). yetone 2026-10-02 19:36:59 +08:00
  • 5fd503c789 fix(copilot): unify plan and entitlement identification (#510) Zhaofeng 2026-10-02 19:34:33 +08:00
  • 9657d3e9d8 usage: reuse incremental log blocks and index CSV matches (#550) Snowyy 2026-10-02 19:15:56 +08:00
  • 7586df4400 test: usage's and provider's TestMains clear the agents' variables too, and a new agent's variable left out of the list now fails (yetone, reviewing #543: internal/usage/main_test.go's TestMain still clears no agent variables, so a CLAUDE_CONFIG_DIR left in the shell reaches the real ~/.claude there too; nothing fails yet when a new agent's variable is left out of Vars) — both TestMains gave their package a home of its own and cleared no agent variable, the provider one as much as the usage one that was named. A CLAUDE_CONFIG_DIR left in the shell reached the real ~/.claude through the ledger's sessions.Calls (usage/ledger.go:122): with a canary Claude folder holding three of the sessions package's fixtures, six usage tests fail — TestLedger, TestSubscriptionListPrice, TestLedgerUsesTheStatedPrice, TestLedgerConfiguredPricesAndRefresh and the two Antigravity ledger ones — and pass with the clearing; provider passes with the same canary either way, but its Accounts read what an agent keeps on the machine too. #543 missed these by how it looked for its sites: by the clearing loops a sandbox already had, which one with no loop at all has no way to answer. TestFolderVarsAreListed reads the sources of the four packages an agent's folder variable is read in — this one, sessions, provider and library, the rest of the repository reading only magpie's own variables, the process' and the desktop's — and fails on a name that is neither in agentenv.Vars nor excused here with a reason, so a new agent's variable no longer waits for a developer's shell to meet one. It reads them as files, since a test in sessions or provider cannot import this package, which imports them; it asks for one name each way its pattern matches, so a change in how a variable is read empties the scan loudly instead of passing it, and for every name excused, so no excuse outlives its variable. What it cannot see is a name the sources build at runtime, as omoDir reads its two out of a slice (omo.go:33); both are listed, and a third taken the same way would not be caught. The package doc's VarsTest is TestVarsAreRead. TestFolderVarsAreListed fails without the change when KIMI_CODE_HOME is taken out of Vars, or when a pretend NEWAGENT_HOME is read in agents.go, and names the file that reads it; the six usage tests fail without theirs. On Windows 11 the whole suite answers as it did before the change: 31 packages pass, and the same 15 tests in internal/library, internal/gateway and internal/agent fail for what they failed for before. go vet ./..., go build -tags nogui (windows, GOOS=linux, GOOS=darwin), go vet -tags nogui for linux and darwin over every package touched, and gofmt pass. (#549) Lin Huazhong 2026-10-02 18:41:27 +08:00
  • 34538b0a4b Preserve fresh Claude headers and shorten tray cache labels (#548) SadWood 2026-10-02 18:31:27 +08:00
  • 1a283b95fc fix(gateway): normalize standalone Responses tool outputs (#544) Zhaofeng 2026-10-02 18:26:23 +08:00
  • 723b0f34f5 zcode: a Start Plan request goes over HTTP/1.1, as ZCode, its OpenCode plugin and zcode2api-plus speak to zcode.z.ai (ARNO on Discord, v0.1.639: magpie's built-in ZCode answered 405 / code 3012, "network firewall blocked", while his ZCode plugin run in magpie's plugin host got through on the same machine, account and moment) — the two requests, captured side by side against a local TLS server, carry the same path, headers, auth and body (the prompts' hashes match the plugin's); what differs is the connection: the gateway's client speaks HTTP/2 wherever it is offered, and zcode.z.ai offers it, while the plugin (Bun's fetch), ZCode itself (Node's fetch) and zcode2api-plus (Go, HTTP/2 turned off) all speak HTTP/1.1, which changes the TLS hello's ALPN too, a field Alibaba Cloud's firewall fingerprint can take in. A Start Plan request (to zcodeStartBase) now goes through zcodeStartClient, HTTP/1.1 only and otherwise as the gateway's transport (netproxy's proxy, the same wait for the head); Account.clientFor lets an account pick the client for a request, Provider.Do honouring it; a Coding Plan account still goes through the gateway's client. TestZCodeStartHTTP1 (a TLS server offering HTTP/2, an HTTP/2 client given: the Start Plan request arrived as HTTP/2.0 without the change; now HTTP/1.1, the Coding Plan request still HTTP/2.0). No request was made to z.ai; whether this alone lets the Start Plan through is for ARNO's account to say. go vet ./internal/..., go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui -count=1 ./... under a temp HOME pass. yetone 2026-10-02 18:15:33 +08:00