Commit Graph

  • 6168bd1a18 workbuddy: its "Illegal API invocation from an unapproved channel" says what to do (QuentinHsu on Discord, deepseek-v4-flash on WorkBuddy from Codex) — WorkBuddy, both builds, refuses a chat whose system prompt is Codex's or Claude Code's (#182), whatever the headers, and magpie does not rewrite that prompt. The refusal now carries "WorkBuddy refuses chats from Codex and Claude Code (their system prompt); use it from Hermes, OpenCode or Pi, or add another provider to this group", to the agent and on the Routing page (apart from the vendor's words, in the page's language); a group still passes the request on to its next member. workbuddy_refused_test (provider, gateway) and routing-wb-refused.test.cjs (Chromium, WebKit, en/zh) fail without the change. go vet, go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui ./... pass; click-scroll, old-webkit and the routing tests pass. yetone 2026-09-30 17:41:14 +08:00
  • 79a7ba8864 gateway: Codex's own headers go on to a keyed provider, as Claude Code's do (#179) (lx_78654 on Discord: "This account only allows Codex official clients") — magpie sent Codex's requests as magpie/<version> with none of Codex's headers, so a provider that serves only Codex's clients (by User-Agent or originator, a version and an x-codex- header) refused them. A request from Codex (User-Agent or originator starting codex: codex_cli_rs, codex_exec, codex_vscode, Codex Desktop) to a key+URL provider over Responses or Chat, relayed or rebuilt, now carries its User-Agent, originator, session-id, thread-id and the x-codex- headers codexHeader passes; the provider's own key is the one sent, never magpie's, nor Codex's attestation; other agents go as magpie/<version>, and a provider's own User-Agent is still set when it signs. Headers as codex-cli 0.159.2 sent them to a fake upstream in a sandbox HOME. TestCodexHeadersReachKeyedRelay fails without the change. go vet, go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui ./... pass. yetone 2026-09-30 17:37:27 +08:00
  • 947521194f qoder: a model Qoder's listing marks free is shown free (lml on Discord asked which Qoder models cost no credits) — a model with is_free (isFree) true or a price_factor (priceFactor) of 0, as Qoder's own client reads its listing, gets catalog.Model.Free, so the model picker's free badge shows on it; a saved account's models, re-read from its stored listing, carry it too. TestParseModelsFree and TestQoderSavedModelsFree fail without the change. go vet, go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui ./... pass (TestClaudeSignedOut, a load flake, passes alone). yetone 2026-09-30 17:29:15 +08:00
  • 782380a7f5 gui: a Claude account saved in magpie can be removed while Claude Code is signed out (𝕏 user on Discord: a banned account logged out, and its line in Providers had nothing to remove it with) — saved accounts are listed only beside the one the agent is signed in to, so with none signed in the line saying they are saved was all there was. It now names them and has Remove, which asks first and forgets magpie's own copies only (login/forget, one each): the agent's files, keychain and the account itself are left as they are. TestSavedButSignedOutCanBeRemoved and saved-signedout-remove.test.cjs (Chromium, WebKit, en/zh) fail without the change. go vet, go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui ./... pass (TestClaudeSignedOut, a load flake, passes alone); old-webkit, own-signin-remove pass. yetone 2026-09-30 17:29:34 +08:00
  • 51e7b51713 gui: an agent's model list closes whatever the clicks while it loads (有时候这个关不掉), and its rows sit indented under their group (左侧有缩进才对) — the list is fetched before its box is up, and a second click on the line then (Codex's 266 models are slow to put together) was taken as a fresh open: each fetch that came back drew a box, and the one magpie no longer held stayed on the screen with nothing to close it. Now a click on the line while it loads takes it back, as it closes the box, a click elsewhere too, and another open or a close supersedes the fetch; one box at most. A model row starts in its group icon's column, past the twisty, not flush with the group's header. New agent-models-loading.test.cjs (Chromium, WebKit: a slow list clicked twice, three times, clicked away from; row icon under the group icon) fails without the change; agent-models, agent-model-groups, click-scroll, old-webkit, model-test-one pass. go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test ./internal/gui pass. yetone 2026-09-30 17:27:56 +08:00
  • 4fc980347c agents: Codex's OpenAI models are its own, not magpie's routing groups read back (group/semantic, group/test … listed under OPENAI in Codex's model picker) — Codex keeps the list it was last handed in models_cache.json, and handed through the gateway it has magpie's models in it too; magpie read the file back as Codex's own models, so its groups (one since removed among them) were listed under OpenAI, and stayed after Codex was routed elsewhere. An entry magpie added (a magpie id — provider/model, group/<id>, codex/<slug> — in a list whose ETag magpie tagged, or described "… via magpie") is left out of Codex's own models and of the entries the gateway falls back to. TestCodexCacheLeavesMagpieOut fails without the change. go vet, go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui ./... pass. yetone 2026-09-30 17:19:39 +08:00
  • 865ad6b03f gui: one magpie in Omarchy's tray, not two (songlairui on X: 为什么我的 omarchy 上会有两只喜鹊,一起来,一起退) — Wails' tray takes the name org.kde.StatusNotifierItem-<pid>-1 and registers with the watcher by its path, which Quickshell's watcher files under the connection's own name (:1.x); something on their system also registered the well-known name, and Quickshell tells items apart by how they were named, not by who owns them, so the one icon was two items (another app of theirs was doubled the same way). magpie now gives the name up once the tray has taken it: a registration by that name is refused, and one made already is dropped with it. In the Omarchy VM, registering the name before the change showed :1.328/StatusNotifierItem and org.kde.StatusNotifierItem-123846-1/StatusNotifierItem; after it, one item, the icon still in the bar. go vet, go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build pass; built with -tags gtk3 on the VM. yetone 2026-09-30 17:10:02 +08:00
  • 8931a4b3ac davsync: sync can keep its file in an S3-compatible bucket as well as a WebDAV folder (#296) — AWS S3, Cloudflare R2, Backblaze B2, MinIO, Garage or a NAS. Settings › Sync and backup picks WebDAV or S3: endpoint (empty is AWS), bucket, prefix, region (empty is us-east-1, auto for R2), access key ID and secret, path-style; CLI magpie s3 on s3://bucket/prefix access-key-id=… endpoint=… region=… path-style. The bucket holds the same passphrase-sealed file at <prefix>/magpie/magpie.magpie-backup, signed with SigV4 (no SDK). It is read with its ETag and written with If-Match (If-None-Match: * for the first), so two computers syncing at once don't overwrite each other; a server that refuses conditional writes (501, or 400 naming the header) is remembered and gets a HEAD-then-PUT instead. An IP, localhost or a dotted bucket on https goes path-style on its own. Errors say what to do: wrong key/secret, clock skew, bucket in another region (names it), no such bucket, 403 on the first read on AWS without s3:ListBucket. The secret is kept as the WebDAV password is, sent only to the endpoint and key it was typed for, and may not be the passphrase. sigv4_test (AWS's published vectors), s3_test (fake server: conditions, conflict, fallback, regions, errors), s3_cli_test and s3-sync.test.cjs (Chromium, WebKit, en/zh) fail without the change; tested against MinIO. go vet, go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui ./... pass (TestGroupAddSetRemove, a TempDir-cleanup flake under load, passes alone); click-scroll, old-webkit pass. yetone 2026-09-30 17:07:46 +08:00
  • e31384b1f9 gateway: Codex on magpie as its provider has its approvals auto-reviewed again (shihao H on X: 自动审批未执行 … 404: magpie knows no model "codex-auto-review") — Codex asks for its guardian review by OpenAI's reviewer model, codex-auto-review, whatever the provider ("independent of current login"), and magpie, knowing no such model, answered 404, which Codex reports as the review service failing. A model Codex names that magpie does not serve now goes to the model Codex is set to (config.toml model), when magpie is its provider, as Codex itself reviews with the turn's model when its catalog lacks the reviewer; the Routing view shows it as the Approval check. Signed in, Codex's own names go on to OpenAI as before. TestCodexStandIn fails without the change; TestCodexAutoReviewStandIn has the Codex request go to the stand-in. go vet, go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui ./... pass. yetone 2026-09-30 16:55:49 +08:00
  • 771506cf05 gui: a model chip's right-click tests that model alone (yonghe on Discord: 供应商编辑里只想测一个模型,「测试模型」会测全部) — the chip's menu, the one an agent row's right-click opens (now openRowMenu, shared), has Test this model, which posts provider/test with that model alone; its dot and title show the answer as Test models shows them, other models' results are kept, and the footer names the model and how it answered. The right-click neither picks the chip nor moves the page; Esc closes the menu, not the editor, and puts focus back without a scroll. The chip's title and Test models' say so; no menu where Test models isn't (signed-in accounts, classifiers). model-test-one.test.cjs (Chromium, WebKit, en/zh) fails without the change. go vet, go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui ./... pass; click-scroll, old-webkit, provider-proxy, nokey-editor, model-pick GUI tests pass. yetone 2026-09-30 16:48:02 +08:00
  • 5e3c7b8049 plugin: a plugin's provider has its models on a magpie that never fetched models.dev — the host took the models OpenCode would from models.dev's cache, so on a fresh install (magpie plugin add before the app or gateway ever ran) opencode-gemini-auth and opencode-copilot-auth listed 0 models, and plugin-providers.json kept that. Starting the host now fetches the catalog first when there is none (30 s at most). Tried in a sandbox HOME with the real packages: google-plugin 38 models, github-copilot 34; with a Copilot sign-in copied into the sandbox, magpie provider test github-copilot answered on Responses (gpt-5.4) through the plugin's fetch. go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test ./internal/plugin pass. yetone 2026-09-30 16:48:29 +08:00
  • e1fd75de6a agents: a routing group in an agent's model list shows its providers' icons stacked, as it does in the pickers and on the Routing page (这些路由组里的模型图标应该是它对应的 providers 的图标堆叠) — the list gave a group the logo of the maker its name suggested (GPT-6 Sol: OpenAI's alone, whatever it routes over) and the fan to the rest, though the icons were already in the response. Now 2+ providers stack (17px discs, the first on top, +n past 3), a lone provider sits in one disc like them, and a group with none keeps the fan; every group row has one 56px slot, the icons at its left, so the names start in a column. The server no longer sends a group a maker's logo. New agent-model-groups.test.cjs (Chromium, WebKit: groups of 4, 2, 1 and 0 providers, names in one column clear of the widest stack, icons at the row's left, a model keeps its maker's logo) fails without the change; agent-models passes; click-scroll's WebKit case flakes on main as well. go vet, go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui ./... pass. yetone 2026-09-30 16:45:28 +08:00
  • a31f97b30d plugin: OpenCode's provider plugins sign in to subscriptions in magpie — an npm package OpenCode users install for a plan (its auth hook: loader, methods, oauth authorize/callback, api keys, chat.headers) runs in a Bun host magpie downloads the first time a plugin needs it, speaking JSON-RPC over stdio; its sign-ins are kept in plugin-auth.json (0600). A provider a plugin signs in to is a provider like the others: its models the plugin lists, each spoken to on the API its AI SDK package speaks (anthropic → Messages, openai/azure → Responses, google → Gemini, Copilot's GPT-5+ → Responses, else chat completions), its requests carried by the plugin's fetch to the base URL its loader gives; an id magpie already has is <id>-plugin. CLI: magpie plugin [list|add|rm|update|on|off|login|logout]. App: Settings → Plugins adds, updates, switches off and removes them and says why one didn't load; Add provider → From plugins signs in with the plugin's methods, its questions (picks and checked text), the browser and a pasted code or a key, followed as the built-in sign-ins are; removing the account signs out. The built-in subscriptions stay. TestFakePlugin, TestPluginProvider (chat, Anthropic and Gemini models through the gateway) and TestPluginSignIn (a wrong code fails, the right one signs in, removing signs out, a key) run a fake plugin on Bun; plugin-signin.test.cjs (Chromium, WebKit, en/zh) covers the add sheet and Settings. signin-callback, add-sheet, zed-sub, account-proxy, own-signin-remove, routing-signin, click-scroll GUI tests pass; go vet, go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui ./... pass (TestUserPath, TestClaudeSignedOut, TestClaudeAccountNamedAsItsLogin, TestClaudeIdentityServedWhileAsked, known flakes under load, pass alone). Not yet tried with a real plugin and account. yetone 2026-09-30 16:38:01 +08:00
  • e0e93c2ec5 gateway: a Codex conversation compacted on GPT goes on after switching to Grok (waroy on Discord: Grok (SuperGrok): Could not decrypt the provided encrypted_content) — when Codex compacts on OpenAI's own models, the earlier turns become a compaction item sealed by OpenAI (encrypted_content), which magpie kept for magpie models. Grok can't read it, any more than another vendor's reasoning. The retry after such a refusal (2476cd5) left out only the sealed reasoning and asked once, so Grok refused the compaction too and Codex got xAI's error. It now asks again without the compaction when the refusal comes back (or there was no reasoning to leave out), and remembers the compaction with the reasoning as refused for that conversation and account, so later turns leave both out up front while Grok's own reasoning goes along; each step remembers only what it took out. The summary of the turns before the compaction is lost to Grok; what was said since stays. TestForeignCompactionToGrok (Codex path, a SuperGrok-like upstream refusing in the stream) fails without the change with the reporter's error. go vet, go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui ./... pass (TestClaudeSignedOut, TestClaudeAccountNamedAsItsLogin, TestClaudeIdentityServedWhileAsked, known flakes under load, pass alone). Not tested against xAI itself. yetone 2026-09-30 16:40:41 +08:00
  • bcb9db9356 fix(gui): 用量文字与提示显示最多一位小数 (#287) 2026-09-30 16:37:23 +08:00
  • 7c0b5441a8 routing: a group can name the reasoning levels agents are offered, so one member with few doesn't take the rest from the others (#295) — a group offered only the levels every member has, so deepseek-flash (low/high/max) beside models with medium and xhigh left the group without them. Group.Levels (CLI magpie group set <id> levels=low,medium,high,xhigh, levels= clears; TUI l; GUI group editor's Levels: its models' shared / named) is offered instead in /v1/models, Codex's catalog, Pi and the rest; only levels magpie knows are taken, stored lowest first; a group in a group that names its own offers them for its models; unset is as before. A member without the level asked is sent its nearest, as it already was. The GUI's group save also keeps a family set from the CLI. TestGroupLevelsOverride, TestGroupLevelsReachAgents, TestGroupLevelsNamed (passthrough and translated: medium→high, xhigh→max on the low/high/max member, as asked on the one with all), TestGroupSetLevels and routing-levels.test.cjs (Chromium, WebKit, en/zh) fail without the change. go vet, go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui ./... pass (TestUserPath, a known flake under load, passes alone); click-scroll, old-webkit, routing-effort(-row), routing-levels, settings-groups GUI tests pass. yetone 2026-09-30 16:26:34 +08:00
  • 7f007660d6 gui: magpie web shows the logo and the name in the header again (magpie web 竟然没有显示 magpie logo 和名字) — the rule that hides them in the Windows window, whose title bar already has them, matched every window that is neither mac nor linux, and in magpie web the page is given neither class, so the brand was hidden in the browser on every system. The rule now leaves out .web too. brand.test.cjs (Chromium, WebKit: web on Windows, macOS, Linux shows logo and name beside the tabs; the Windows window hides them; the Mac window shows them) fails without the change; old-webkit, omarchy, mask-emails, click-scroll pass. go vet, go build -tags nogui (GOOS=linux), GOOS=windows go build pass. yetone 2026-09-30 16:25:13 +08:00
  • 4a64ee78c0 gui: the header's refresh button checks for a newer magpie too, and its tooltip says so (inaction on Discord: the Update pill beside it read as the refresh's own "upgrade", and a refresh didn't look for one) — it refreshed only the model lists, so a new version was noticed only by the six-hourly check or Settings → Version. A click now also asks POST /api/update/check, without holding up the model refresh, and draws the Update pill from its answer. Tooltip: "Refresh model lists (models.dev and every vendor) and check for a newer magpie" / "刷新模型列表(models.dev 与各供应商),并检查 magpie 更新". sync-update.test.cjs (Chromium, WebKit, en/zh: one click, one check, the pill shows) fails without the change; update-check, click-scroll, old-webkit, rail-tip, menu-scroll pass. go vet, go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui ./... pass (TestUserPath, TestClaudeSignedOut, known flakes under load, pass alone). yetone 2026-09-30 16:16:18 +08:00
  • ee8a371cf4 gui, update: saving the Settings page keeps the models hidden from each agent, and update checks go through magpie's proxy and say so (#294: 设置代理之后只有模型代理生效了,代理没有用于检查 magpie 是否有更新) — POST /api/settings carried over the models' names, levels and images but not HiddenModels, which the page never sends, so changing the theme, the language or anything else there showed every model hidden one by one on the Agents page again; it is carried over with the others. The update feed and the release came through http.DefaultTransport, proxied only because main installs netproxy first; the update client now takes netproxy.Func on a transport of its own (Settings' Proxy, else *_PROXY, else the system's). A check that failed at start-up, before a proxy was set, stayed failed for six hours: saving a new proxy now checks again through it. And the Settings row read "requests to vendors go through {proxy}", which says update checks don't: it now names both. TestSettingsSaveKeepsWhatItDoesNotSend (every field prefsKeep doesn't send survives a save of the theme alone; fails on main with HiddenModels emptied), TestUpdateGoesThroughTheProxy (feed and asset at hosts only a local proxy answers; fails without the transport), TestSettingsProxyRechecksAFailedUpdate (fails without the re-check). account-proxy, provider-proxy and click-scroll GUI tests pass; go vet, go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui ./... pass (TestUserPath, TestClaudeSignedOut, TestClaudeAccountNamedAsItsLogin, known flakes under load, pass alone). yetone 2026-09-30 16:03:52 +08:00
  • a69ae948dc library: Claude Desktop's MCP servers go into Claude-3p/claude_desktop_config.json as well, where its 3p mode reads them (#297: Claude Desktop 使用 magpie 三方网关(3p)后,library 写入的 MCP 在 Desktop 里看不到) — in 3p mode, which magpie's own provider switch turns on, Desktop's whole userData is Claude-3p (~/Library/Application Support/Claude-3p, %LOCALAPPDATA%\Claude-3p on Windows, ~/.config/Claude-3p), its MCP servers included, so the servers the library wrote into Claude/'s file never showed in Settings → Developer. Where the Claude-3p folder is, its file is now written with Claude/'s, in one edit with each file's own keys kept, read for servers to bring in, and cleared on removal even when Claude/'s no longer has the entry — both, not whichever mode is on, so switching the provider leaves nothing stale and Desktop finds them in either mode; no Claude-3p folder is made for a Desktop never in 3p, and one only ever in 3p (no Claude folder) gets its servers there alone. A server given before reaches Claude-3p on the next sync. Tested with new desktop3p_test.go in a sandbox HOME (both files written and cleared, the user's servers and keys kept, sync of an older server, 3p only); each fails without the change. go vet, go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui ./... pass. Not tried against a real Desktop in 3p. yetone 2026-09-30 15:56:52 +08:00
  • 3810dc3f8c zed: every tool result sent to Zed says is_error, false as well as true, so a turn after a tool call is answered again (yetone/magpie-releases#9, aurevae: "Zed: failed to parse Anthropic request: missing field is_error" on Pi through /v1/chat/completions and on /v1/messages) — magpie's Anthropic request leaves is_error out when it is false, as Anthropic's API allows, but Zed's cloud refuses a tool result without it, so with a Zed account every request after the model's first tool call failed with a 400. The reporter's own table shows the same request going through with is_error true. Zed's Anthropic request now has is_error on each tool_result; the tool inputs' numbers are kept as they were. TestZedToolResultSaysIsError checks a successful and a failed tool result, and a 20-digit number in a tool's input; it fails without the change. go vet, go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui ./... pass (TestDevinSignedInOnlyInMagpie, a known flake under load, passes alone). yetone 2026-09-30 15:53:29 +08:00
  • de07d8de24 ci: a UI preview goes by the diff, not the PR's description — on #287 the description (out of date) promised a 用量小数位 setting the code doesn't have, and the preview repeated it as the change, captioned a screenshot "默认 0 位" of a setting that wasn't there. The planner is told the description may be wrong, to plan and summarise from the diff, and what the sandbox lacks (subscription quotas above all); the preview now says, apart from the summary (改动(按代码)), where the description and the code disagree (描述与代码不符) and what the sandbox can't show (沙盒里看不到). Tested locally with run.sh on #287's code: it names the missing setting, says the sandbox has only a DeepSeek balance and no percentage windows, and shows the two places the change would be. yetone 2026-09-30 15:45:23 +08:00
  • 3dc5577d43 pi: changing the thinking level writes only that level, and only when it changed — with the model on magpie it rebuilt providers.magpie, so a contextWindow (or any other field) edited by hand was replaced. Default still clears defaultThinkingLevel and leaves the model and the provider where they are. Picking a model, and the catalog sync, still refresh the provider. OmO shares this path. (#285) Eachann 2026-09-30 15:41:17 +08:00
  • bce583dc6e codexcat: each model magpie lists for Codex says it takes parallel tool calls, so Codex 0.147 loads the catalog again (#298) — it failed on every start with "failed to parse model_catalog_json … missing field supports_parallel_tool_calls", leaving Codex unusable with magpie. Codex 0.147 requires the field; later Codex (0.159) default it and ask for parallel calls whatever it says, so true is what they already do. Reproduced in a sandbox HOME with codex-cli 0.147.0 and magpie's catalog; with the field, 0.147.0 and 0.159.2 both load it and send the request (parallel_tool_calls true, as before on 0.159). TestCodexCatalogParallelToolCalls checks every entry; it fails without the field. go vet, go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui ./... pass. yetone 2026-09-30 15:39:34 +08:00
  • 795a4d7130 ci: the UI preview checks out a fork's PR — actions/checkout now refuses fork code under pull_request_target unless told allow-unsafe-pr-checkout, and every fork PR failed there (#287, run 36682482508); #289 came from a branch here, so it passed. The record job is the one meant to run the PR's code: read-only token, no credentials kept. yetone 2026-09-30 15:34:30 +08:00
  • d0d0fcef76 providers: DimAgent's subscription is taken out of magpie (a DimAgent team member on X: dim doesn't allow its subscription used through a proxy, and users doing so get banned or lose their discount) — #226 signed in to DimAgent as its desktop client does and served its models through the gateway. The sign-in, the account provider and its dimagent/<model> models, the quota card, magpie accounts … dimagent and the icon are gone, with internal/dimagent. An account signed in before is left out when logins.json is read and dropped from the file at its next write, so its tokens aren't kept. The pasted-callback field and route stay, generic, with no sign-in using them. Tested with TestDimAgentLoginDropped (fails without the filter); in a sandbox HOME with a DimAgent login left in logins.json, magpie accounts --json lists nothing and magpie accounts add dimagent is refused. signin-callback (fixture renamed), old-webkit and click-scroll pass; go vet, go build (GUI, -tags nogui, GOOS=linux, GOOS=windows) and go test -tags nogui ./... pass (TestUserPath and the Claude/Devin sign-in tests flaked under the full run and pass alone). yetone 2026-09-30 15:33:38 +08:00
  • c46370ad9a gateway: a Codex compaction for a magpie model asks for its summary as Codex asks for its own — streamed, with tool_choice and parallel_tool_calls — so an upstream that checks a Codex request's shape no longer refuses it with "invalid codex request" (#292, byhooi). Codex compacts remotely (a compaction_trigger in /responses) whenever it is on the OpenAI provider, which is how it reaches magpie, and it has no setting to turn that off (remote_compaction_v2 is gone). magpie answers the trigger by asking the model for a summary, but it sent that with stream false and no tool_choice or parallel_tool_calls. The tools are still left out, so the reply is text; compactReply reads the summary from a stream as well as from a whole response, and a failed stream is a 502 with the upstream's message rather than an empty summary. Tested with TestCodexCompactsThroughCodexRelay (a fake upstream that refuses a request not streamed or without tool_choice, parallel_tool_calls, prompt_cache_key or include; fails on main with "invalid codex request") and TestCodexCompactsMagpieModel. go vet, go build -tags nogui (darwin, GOOS=linux, GOOS=windows) and go test -tags nogui ./... pass. yetone 2026-09-30 15:29:15 +08:00
  • 4dc3d93343 kimi: the new Kimi Code (2.x) is wired in ~/.kimi-code, where it reads its settings — a model picked in magpie never reached it, nor did magpie's models reach its /model (#290, kyzhouxu: Gemini 3.8 flash picked, Kimi Code still starts on the old one). magpie wrote only ~/.kimi/config.toml, the old kimi-cli's; the new CLI copies that over once when it first starts and never reads it again. KimiDir picks $KIMI_CODE_HOME, ~/.kimi-code if there, $KIMI_SHARE_DIR, ~/.kimi if there, else ~/.kimi-code. The new CLI's model tables also say tool_use (kimi-cli refuses the word, so it isn't written there), and its skills go to ~/.agents/skills, which it reads (not ~/.config/agents/skills). Tested with TestKimiCode (fails on main: path .kimi/config.toml, want .kimi-code/config.toml) and TestKimi; in a sandbox HOME the new CLI's kimi provider list shows magpie and the new default model, kimi doctor finds the config valid, and kimi -p went through a sandbox gateway to a fake upstream asked for gemini-3.8-flash with its 26 tools. go vet, go build -tags nogui (darwin, GOOS=linux, GOOS=windows) and go test -tags nogui ./... pass (flaky tests under load pass alone). yetone 2026-09-30 15:29:14 +08:00
  • 972355cfbe gui: a provider with no key opens its editor again, and a key's plan and balance show the last reading while the vendor is failing — willz on Discord: a local Ollama added without a key only toggled its row when clicked, with "undefined is not an object (evaluating 'copied?.key.set')". drawEditor set copied to false when the provider wasn't a copy, and false?.key is undefined; it is null now. chalice on Discord: a Command Code key's card read "Usage unavailable" — Command Code answers billing/credits 503 at times, and a key's cards, unlike a subscription's, kept no last reading: the balance showed the error and the plan card went. Both now keep the last good reading (quotas.json, under a sha256 digest of the key, never the key) and show it with its time while the failure passes; a key never read still has no plan card. Tested with nokey-editor.test.cjs (an Ollama preset and a custom one without a key open in Chromium and WebKit, English and Chinese; fails without the change) and TestKeyQuotasKeepLast (a fake Command Code going 503, readings reloaded from disk; fails without the change). click-scroll passes; go vet, go build -tags nogui (darwin, GOOS=linux, GOOS=windows) and go test -tags nogui ./... pass (two Claude account tests flaky under load pass alone). yetone 2026-09-30 15:22:28 +08:00
  • 190248d204 ci: the UI preview records without waiting for approval (the user: 去掉批准吧) — the ui-preview environment keeps the DeepSeek key but no longer names a reviewer; the comments and the "recording" note stop mentioning one. yetone 2026-09-30 15:07:06 +08:00
  • c9d9652380 library: an agent whose folder is a file isn't written, and skills reach 13 more agents with one link in ~/.agents/skills for those that share it (neiko on Discord: with DeepSeek Harness not installed the Library kept failing on "open ~/.dsh/AGENTS.md: not a directory" and ~/.dsh/config.yaml; inaction: the skills list is short of the Agents page, Kimi Code has none). An agent counted as there when its folder path existed or its command was on PATH, never asking whether the path was a folder — a file at ~/.dsh, or another dsh, made DeepSeek Harness look installed. Now an agent whose folder is a file (or under one) isn't detected (agent.Taken; Goose's, Cindy's and Claude Desktop's own checks want a folder too), a Library target leaves out each place a file stands in the way of, one with none left isn't listed, and an agent given no MCP server with none of magpie's isn't read. Skills: Kimi Code, Goose and Cindy get ~/.agents/skills (Kimi's ~/.config/agents/skills when there, as Kimi reads it first; never ~/.kimi/skills, which would hide Claude Code's from it); Command Code, Devin, Hermes, Droid, Cline, Qoder, Qoder CN, Grok, WorkBuddy, Hanako and fx their own skills folder; Kimi, Goose and Grok a project's .agents/skills. The shared folder is filled first, and an agent that reads ~/.agents/skills besides its own (Codex, Gemini CLI, OpenCode, Crush, DeepSeek Harness and those above) gets no second link to a skill already there. Tested with new taken_test.go (neiko's two errors without the change) and agent_skills_test.go (skills folders, written once, a shared skill imported for Kimi), in sandbox HOMEs; library-items and shared-skills 24/24 in Chromium and WebKit; go vet, go build (-tags nogui, GOOS=linux, GOOS=windows), go test -tags nogui ./... pass. Not tried with real Kimi Code, Goose or Cindy. yetone 2026-09-30 14:40:39 +08:00
  • e79343b1bb ci: the UI preview's planner is shown how to reach what the PR changes — the pages are outlined closed, so on #289 (a hover colour inside an agent's model list) it once said nothing visible changed and once opened the drag handle's menu instead. It now also gets the PR's own front-end code around the diff: where the changed classes and functions are built and, two hops up, what calls them (here modelsEntry's .ag-models button opening openAgentModels), and is told that something missing from the outlines is to be opened, not taken as absent. A close-up no longer fails on clip (declared inside the try it was read after), and a DeepSeek reply that is cut short or not JSON is asked for again instead of ending the repair. Tested locally with run.sh on #289's source: 2 screenshots of the list, the button red on hover, and the video, no errors. yetone 2026-09-30 14:40:01 +08:00
  • 4828c66aa7 gui: the model picker's rail names an icon beside it, on the right, level with it — not in the browser's tooltip under the pointer, which put Devin's name over ZCode's Z below it, as if that were ZCode's (Elan on X: 切换的时候zcode的logo是一个动画bug吗). The name comes after a moment, moves at once from one icon to the next, and goes on a click, on leaving the rail, on its scroll and when the picker closes; a rail icon focused from the keyboard is named too. Tested with rail-tip.test.cjs in Chromium and WebKit, English light and Chinese dark: the name to the right of its icon, level with it, over no rail icon, no title left on the button, the next icon named at once, gone on click, leave and Esc; it fails on the old code. click-scroll, old-webkit, menu-scroll and model-pick pass; go vet, go build -tags nogui (darwin, GOOS=linux, GOOS=windows), go test -tags nogui ./internal/gui/... pass. yetone 2026-09-30 14:31:04 +08:00
  • 3a193423b6 ci: a UI preview's close-up keeps what's around the thing it shows (at least 560×340) and no screenshot is stretched past its own size in the PR — the first real run (#289) cropped a hover to the button alone, 100×68, then showed it 760 wide. Checked on #289 with workflow_dispatch. yetone 2026-09-30 14:27:19 +08:00
  • 1721fac530 devin: an account signed in only in magpie works without the Devin CLI signed in, and is listed once (蓝猫 on Discord: every request from Claude Code failed until devin auth login, then the same account showed twice) — the model list that turns a family and effort into the variant Devin takes (claude-sonnet-5-5 + high → claude-sonnet-5-5-high) was asked only of the CLI's own sign-in, so with that signed out or its key refused the bare family id went to Devin and was refused. It is now asked of the signed-in accounts in their own homes, the one in use first, the CLI's own last. And the CLI's own sign-in isn't listed beside a working magpie account of the same user (an old logins.json entry renamed to that user when the CLI signed in again). Tested with a new TestDevinSignedInOnlyInMagpie in a sandbox HOME with a fake devin CLI and exchange server, following 蓝猫's steps: it fails without either change (variant stays claude-sonnet-5-5; dev@example.com listed twice). go vet, go build (-tags nogui, GOOS=linux, GOOS=windows), go test -tags nogui ./... pass (TestUserPath and TestClaudeAccountNamedAsItsLogin flake under load, pass alone). Not tried against the real Devin API. yetone 2026-09-30 14:24:51 +08:00
  • 59aec42beb agents: an agent's model list shows each model's maker's logo (这里能否显示模型的图标?) — the list had a logo only on a provider's heading, so a relay's or a routing group's rows were names alone. Now each row has its maker's: known by the model's family (gpt → OpenAI, claude, gemini, grok, qwen, glm, mimo/xiaomi, step, …; "vendor/model" ids too), a routing group by the name it was given or its id, a model of a vendor's own provider by the provider's logo; a routing group whose maker isn't known shows the routing mark, anything else keeps the slot empty so the names line up; a model turned off has its logo dimmed. Checked in a sandbox HOME with a copy of the real providers (keys blanked): 路由组 shows OpenAI for GPT-6 Sol/Luna/Astra, Claude, MiMo, Grok, Qwen and the routing mark for semantic/test; AiHubMix's rows each their maker's; light and dark yetone 2026-09-30 14:23:48 +08:00
  • 7396b11998 ci: a pull request that changes the UI shows it — the real app built from the PR, run with a real DeepSeek key, screenshots and, when the change spans pages or needs clicking, an mp4 recording with the pointer, its trail and every click drawn in, in a fixed block at the foot of the PR's description (the user: 如果某个PR涉及了UI/UX改动,那么就把这个PR中的Magpie真实的运行起来…录屏中一定要画出鼠标指针的位置,以及鼠标的轨迹,以及鼠标的点击; 一定要贴在原始的PR Description的下面的固定区域). yetone 2026-09-30 14:21:53 +08:00
  • 49e5c99c73 alma: a provider whose models Alma lists as the models themselves ({"id","name","enabled","capabilityOverrides",…}) is read as their ids, so it no longer leaves all of Alma's providers unread — Alma showed as not set with "json: cannot unmarshal object into Go struct field almaProvider.models of type string" (the user, a custom provider "Relay Test" with gpt-5.5 at medium/xhigh). Alma keeps a provider's models as ids or as objects and reads either as its id (server.ts updateProviderModels, database-sqlite, references/providers); magpie read only ids. The ids are still what magpie writes. Tested with TestAlmaModelObjects (a provider with object models beside the user's: magpie's model is applied and read back, magpie's own listed as objects syncs nothing, no drift; fails on main with that error) and by decoding the running Alma's /api/providers. go vet, go test -tags nogui ./..., go build -tags nogui (darwin, GOOS=linux, GOOS=windows) pass. yetone 2026-09-30 14:21:32 +08:00
  • 575a8f5fe3 tests: TestDriftUnwiredEveryAgent leaves WSL agents alone — it walks every agent and picks a magpie model to see the wiring come back, and on a Windows machine with a real distro running, the WSL agents in All() are the user's own: the pick wrote fake/m1 into their real ~/.codex/config.toml, ~/.claude/settings.json and ~/.pi/agent/settings.json through \wsl.localhost. Their files live in the distro, beyond the sandbox home, so the test skips them (a.WSL set). Verified on such a machine: the full ./internal/agent suite leaves the three files' hashes untouched. (#283) Anx 2026-09-30 14:11:30 +08:00
  • 85fa42c172 devin: a tool's description goes in the instructions and the tool with a pointer to it, so WorkBuddy's requests are answered (silenx on X: devin 的到 workbuddy 下使用一直出现 502,不知哪里设置问题) — Devin answers "an internal error occurred" to some agents' tools as they describe themselves, whatever the model: WorkBuddy's Read ("Reads a file from the local filesystem. You can access any file directly by using this tool.", also with case or spacing changed, or with words before it) and its TaskOutput ("Takes a task_id parameter identifying the task"), which magpie passed on as a 502 on every request. The same words in a message go through, so the descriptions are now at the head of the first message after the instructions, in <tool_descriptions>, and each tool is sent with its name, its schema and a pointer to its own. Reproduced in a sandbox HOME on a Devin Teams sign-in with WorkBuddy's own request, captured from its bundled CLI: 502 on SWE-2, Claude Opus 5.5 and SWE-1.7 Lightning, 200 without tools; now 200, and WorkBuddy's CLI on all three read a file and wrote another through its Read and Write tools; Claude Code on SWE-2 did the same. TestBuildDevin checks the descriptions in the instructions, each tool's pointer, and none of either when no tool is to be called. go vet, go build -tags nogui (darwin, GOOS=linux, GOOS=windows) and go test -tags nogui ./... pass. yetone 2026-09-30 14:09:57 +08:00
  • 033a15112d gui: an agent's model list hides every model at once — "Hide all" at its foot beside "Show all" (which was "Show all again"), so picking a few means hiding all and ticking those back, not a group at a time (the user: 能有一个便捷按钮来整体的全部隐藏?现在只能按照组全部隐藏). The model the agent is set to stays; "Shown" keeps what was just hidden till the view changes; each button is disabled when it has nothing to do. Tested with agent-models.test.cjs in Chromium and WebKit, English and Chinese: Hide all sends the 30 models but the one in use, the line under the name reads 1 / 31, then Show all sends none. click-scroll and old-webkit pass; go vet, GOOS=linux build, go test -tags nogui ./internal/gui/... pass. yetone 2026-09-30 13:33:51 +08:00
  • c6ca0ec3e5 gui: the tray panel's link to one request opens the Routing page on it again — ShowMain escaped the whole view it was given (831df85, v0.1.458, for magpie gui <tab>), so "routing&req=42" reached the window as one view named that, which is none: the window came up on no tab, the request not picked. Found reviewing #239, whose "Open Usage" link hit the same. The view mainView builds keeps its req as its own parameter (mainURL); only a tab named on the command line — magpie gui settings, a restart to update — is escaped (argView). Tested with a new TestMainURL (routing&req=42 comes through as view=routing, req=42; a command-line tab is escaped); it fails with the old escaping. panel-routing.test.cjs passes; go vet, go build (GUI, -tags nogui, GOOS=linux, GOOS=windows), go test -tags nogui ./... pass. yetone 2026-09-30 13:09:27 +08:00
  • 5b35b139c3 zcode: requests to zcode.z.ai name the machine with X-Device-Mid, so a Z.ai account with only ZCode's Start Plan signs in again (#282 volcanicll: /api/v1/zcode-plan/billing/balance now refuses a request without it, 400 code 3001 "parameter error", and sign-in read that as the Start Plan having ended) — ZCode's apiClient sends it on every request; the server asks only that it is there. magpie makes one v4 UUID for this machine, kept as zcode-device-mid beside providers.json (never ZCode's deviceMid, never ~/.zcode), and sends it on zcode.z.ai calls and Start Plan model requests; Z.ai's and BigModel's business APIs don't get it. A Start Plan balance that can't be read is now said as such in the sign-in error, with the server's code ("parameter error (400, code 3001)"), instead of "has ended or was never started", which stays for an account whose Start Plan really has. Tested with a fake zcode.z.ai refusing a request without the header like the real one (TestZCodeSignInStartPlan, TestZCodeStartPlanOwnAccount, TestZCodeStartPlanRouting and a new TestZCodeSignInStartPlanDeviceMid fail without it): the id is a v4 UUID, kept across a restart, sent to zcode.z.ai only; with it stripped sign-in says the server's reason; an ended Start Plan still says ended. go vet, go build (GUI, -tags nogui, GOOS=linux, GOOS=windows), go test -tags nogui ./... pass. Not tried against the real zcode.z.ai. yetone 2026-09-30 12:51:59 +08:00
  • 3df75514b9 agents: each agent's own model list — under an agent's name on the Agents page, "Showing 5 / 32 models" (or "All 41 models") opens a list to take models out of that agent's picker one by one and put them back (@ChongkaiX on X: 在 magpie 里选哪些模型出现在 Codex 的模型列表里) — until now an agent's list could only be narrowed by whole providers or families (Visible). settings.HiddenModels holds, by agent, the catalog entries taken out, after Visible: a deny list, so a new model is shown. provider.CatalogFor drops them, so the gateway's /models, the agents' files and pickers and Codex's catalog all follow in one place, and catalog.Touched rewrites the agents' files; Codex's own ChatGPT models are dropped from the backend's list too (CodexNativeHidden), and the list's ETag and models_cache tag carry them (CodexListTag) so Codex asks again. The model an agent is set to is marked 在用 and never taken out, on the server as well. The list groups routing groups first, then providers with their logos and on/total, a long group folded; search, 全部/已显示, a group's 全部显示/全部隐藏 on hover, 恢复全部显示; a click on a row applies at once, Esc or a click elsewhere closes it, and nothing scrolls. A renamed provider's hidden models follow it. Tested with agent-models.test.cjs in Chromium and WebKit, English and Chinese; TestHiddenModels (provider), TestCodexModelListHidesOnAgentsPage (gateway) and TestAgentModelsAPI (gui) in sandbox HOMEs. click-scroll, panel-fold, old-webkit, model-pick, menu-scroll, add-button pass; GOOS=linux go vet, linux and windows builds, go test -tags nogui ./... pass. Not tried in the real Wails window or against a real Codex. yetone 2026-09-30 12:42:49 +08:00
  • 98d1674a52 library: Antigravity is a Library target — rules, MCP servers and skills reach agy and the Antigravity app alike (#277) — both read their user-wide customizations from ~/.gemini/config: GEMINI.md there, mcp_config.json (a remote server written as agy's own mcp add writes it, its serverUrl, "type": "sse" only for an SSE one so magpie reads it back as one; a url agy read in place of serverUrl is read too; the user's own servers and anything they added to an entry, disabled included, are kept) and skills; a project's skills go in the shared .agents/skills. The rules card says Antigravity also reads Gemini CLI's ~/.gemini/GEMINI.md, so text given to both is read twice. RTK is left out: rtk 0.50 has no user-wide hook for Antigravity. Tested with antigravity_test.go in a sandbox home (target paths and the antigravity-cli alias; stdio, http and sse servers written and read back, user keys and servers kept); library-items.test.cjs 18/18; go vet, go build (GUI, -tags nogui, GOOS=linux, GOOS=windows), go test -tags nogui ./... pass. yetone 2026-09-30 12:42:56 +08:00
  • 831df8579e update: a restart to update from the open window (Windows, Linux) opens the window again, on the tab it was asked from, instead of the tray icon alone (#275 Aimer779: 设置页更新重启后主窗口消失,只剩托盘) — RelaunchBinary always started magpie tray, autostart's argument, so the new version came up as a tray icon however the update was asked for; the Mac reopens its .app and was fine. The window's Update pill and Settings' "Restart to update" send the tab they're on, the new process starts as magpie gui <tab>; the tray menu's Restart to Update brings the window back if it was open and keeps to the tray if not; autostart stays tray. magpie gui <tab> / magpie app <tab> open the window on that tab, and hand it to a magpie already running. The MAGPIE_REPLACES handover is unchanged: the new process waits for the old one before its single-instance lock. Tested with TestRelaunchArgs and TestRelaunchBinaryArgs (a script in magpie's place is started with gui settings <pid> / tray <pid>); go vet, go build (GUI and -tags nogui, GOOS=linux, GOOS=windows), go test -tags nogui ./... pass. Not tried: a real restart to update on Windows or Linux. yetone 2026-09-30 12:33:36 +08:00
  • 60e4591315 routing: a request's row shows the reasoning the agent asked for before the one it was sent at when they differ (xhigh → max), so a level the agent didn't pick reads as the agent's or as magpie's at a glance (呆滞 on X: pi 里面选择是 xhigh 但是magpie 里面显示的是max) — the row showed only the level sent, the asked one only in its title. Pi sends max itself for xhigh when its thinkingLevelMap for the model has xhigh null (a level unticked in magpie) — the row then reads "max"; magpie fitting xhigh to a model without it reads "xhigh → max". Tested with routing-effort.test.cjs in Chromium and WebKit, English and Chinese: asked/sent xhigh/max, max/max, xhigh/xhigh and none, each inside its row and unclipped, the title naming both. click-scroll, routing-kind, routing-steady, old-webkit pass; go vet, go build -tags nogui, go test -tags nogui ./internal/gui/... pass. yetone 2026-09-30 12:23:14 +08:00
  • 99a6c0d354 codex: a subscription switched off narrows nothing, its models kept whole (switching the codex provider off left the one picked ChatGPT model as the only one Codex listed) — the codex provider's picks narrow the ChatGPT account's own model list as well as magpie's added models (CodexNativePicked, aed12ac), and ownCodex narrows the picker's OpenAI half the same way. Neither looked at Off, so a provider switched off — which serves no agent anything, by Provider.Off's own promise — still narrowed the account's list to the picks it kept, and a single pick left Codex listing that one model alone. Both now treat a switched-off provider as picking nothing: the account's list is left whole, and the picks stay in providers.json, narrowing again as soon as it is switched on. Tested with a new TestCodexModelListNotNarrowedWhileOff in internal/gateway (a fake backend listing two models; switched off with picks=[gpt-6-sol] both come through, switched on just the pick; fails on main with [gpt-6-sol]) and a new TestCodexOwnModelsNotNarrowedWhileOff in internal/agent (Codex's own picker: both models while off, just the pick while on; fails on main with [gpt-b]). go build -tags nogui ./..., go vet ./internal/agent ./internal/provider ./internal/gateway and go test -tags nogui -count=1 ./... pass (#274) Zhaofeng 2026-09-30 12:24:56 +08:00
  • f21007b164 devin: devin auth status 失败或超时不再把账号当成未登录(供应商列表和路由里间歇消失,同 #154),查询预算从 10 秒放宽到 30 秒 (#221) minuque 2026-09-30 12:14:48 +08:00
  • d99d130d63 gui: TestExplorerWithoutPath is built where explorer is: windows and not nogui — it had no build tag, so go test -tags nogui ./internal/gui, which the project runs, failed to compile on Windows: the test was in while folder_windows.go, which has explorer and fileExists, was left out by !nogui. (#272) Anx 2026-09-30 12:14:41 +08:00
  • ca63863698 routing: a request's row lays itself out by the width of the list it is in, so the reasoning ("· high") no longer lands on top of the numbers (#273 yangkx1024: "7.1 秒" and "high" drawn over each other) — the rows switched to their stacked layout by the width of the page's two-column area, which is 860px or more whenever there are two columns, so a list ~470px wide kept its rows to one line; the numbers took their width first, where it went got none, and the reasoning, which never shrinks, spilled over them. Each column is now a container: over 640px one line as before, below it where it went and the numbers share a line under the model, below 460px each has its own and the numbers may wrap; old WebKit without subgrid stacks from 700px. Tested with routing-effort-row.test.cjs in Chromium and WebKit, English and Chinese, at 1440, 1000 and 480px: no text run in a row over another or out of the row, "high" whole; 4 fail on the old CSS at 1000px. click-scroll, routing-*, old-webkit, panel-routing, mask-emails pass; go vet, builds, go test -tags nogui ./... pass. yetone 2026-09-30 12:10:32 +08:00
  • 26f3a68a7a dsh: DeepSeek Harness's thinking takes low as well — off, low, high, max, as llm-deepseek's reasoningEffort has since 0.1.1 (#269) — magpie offered off/high/max from an older dsh. Checked in dsh 0.1.1 to 0.2.0-rc.2 and master, nothing between; in a sandbox HOME dsh 0.2.0-rc.2 with reasoningEffort low sent output_config.effort low to a fake server. TestDshEffort takes low, still refuses medium; fails on the old list. yetone 2026-09-30 11:50:03 +08:00
  • 128156c134 cursor: a sign-in link cursor-agent wrapped over lines is joined whole before it is opened (#261: the page said "This sign-in link is incomplete or has expired" for https://cursor.com/loginDeepControl?, the link cut after its "?") — runCLISignIn took the first https:// run of a line, so a link broken where the terminal wrapped lost everything after the break. A Cursor link without challenge and uuid now takes the lines after it that are only URL characters; one that has them takes only more of its query read with it, not a word printed after it; a link never made whole is used as it was once the wait is over. Grok's link is handed on as printed. Not reproduced with cursor-agent 2025.08.08, 2026.09.23 or 2026.09.28, which print it on one line. Tested with TestCursorSignInJoinsAWrappedLink (a break after ?, over three lines, in and after the uuid, the rest 0.3s later, whole then "Waiting...", never whole; fails without the change) and TestCLISignInFirstLinkAsPrinted. yetone 2026-09-30 11:50:03 +08:00
  • ad164b6311 providers: a provider can be duplicated, the signed-in account paused, and a Balance URL names each key with {key} (#268, #263, #266) yetone 2026-09-30 11:49:37 +08:00
  • 04f374fa8c agent: OmO (oh-my-openagent, omo-ai), a fork of Pi that keeps its own ~/.omo/agent, gets its own row: a magpie model sets settings.json's defaultProvider/defaultModel and magpie's provider in models.json there as Pi's does, $OMO_CODING_AGENT_DIR / $SENPI_CODING_AGENT_DIR are honoured, the Library writes its AGENTS.md, mcp.json and skills, and omo update updates it (#264 rhyme227). Pi's own ~/.pi is left alone. piIn becomes piLike(at, id, name, dir) so both share one wiring. Tested with omo_test.go in agent and library (fail without the change); go vet, go build -tags nogui (darwin, GOOS=linux, GOOS=windows), go test -tags nogui ./... pass. yetone 2026-09-30 11:38:07 +08:00
  • f464e70b61 workbuddy: a check-in that never reached WorkBuddy is tried again a minute later, then 2, 4, 8, 16 and 30, not after half an hour (#265 WhyWhatHow: the check-in went out the second Windows woke, before its network was back, "no such host", then 签到失败 for 30 minutes) yetone 2026-09-30 11:30:11 +08:00
  • 2122b5a8a2 gateway: a provider's floor on max_tokens said with >= or > is read, so a one-token probe to Command Code is asked again at 16 instead of failing with a 400 (#260 HuHong-Tao: "Expected a value >= 16, but got 1 instead." — Claude Desktop's gateway check reported the model unavailable) yetone 2026-09-30 11:30:11 +08:00
  • 4e8cda60ed web: the page in a browser has magpie's icon in its tab (Discord, ARNO: magpie的web页面没有icon) yetone 2026-09-30 11:26:32 +08:00
  • 6927bedfcb windows: the title bar is drawn in the page's own colour, light or dark as magpie's theme is, and a narrow agent row gives up the update pill's words and the version before the agent's name (X, ChengYue: "Mac上是真好看,Windows上的UI简直不要太丑~") yetone 2026-09-30 11:22:19 +08:00
  • fd1427de0b codex: with magpie as Codex's provider, openai_base_url points at magpie as well, so a thread started on Codex's built-in provider and reopened sends a magpie model to magpie, not to ChatGPT's backend (#259 isymbo: ChatGPT Desktop, "The 'xiaomi/mimo-v2.6-pro' model is not supported when using Codex with a ChatGPT account") yetone 2026-09-30 11:16:59 +08:00
  • 982c636018 settings: the Masking rules row shows its words in full and puts its fields on one line under them — a name, prefix or regex, the match taking the room left, Add — from the row's left edge to its right (TechVerser on X: 这个地方错位了吗?😢 — the name field floated mid-row, Prefix/Regex sat at the right, the match and Add wrapped to a line of their own and the description was cut off). The four fields were squeezed into the row's value cell, capped at 60% of the row and wrapped from the right, with the switch's push to the right still on it; the row now wraps, its words take the full width without an ellipsis and its fields the line below. Tested with redact-rule-row.test.cjs in Chromium and WebKit, English and Chinese, at 1100px and 640px: the words not cut, the four fields on one line in order with none on top of another, flush with the row's edges, the added rule's row as before, picking Regex moves nothing; it fails on the old CSS. go vet, go build -tags nogui (darwin, GOOS=linux, GOOS=windows), go test -tags nogui ./..., click-scroll and old-webkit pass. yetone 2026-09-30 11:11:00 +08:00
  • 4a3e236a7b gui: a second magpie on the same config hands over to the one running whatever its path, so Omarchy's tray no longer shows two magpies (songlairui on X: 为什么我的 omarchy 上会有两只喜鹊,一起来,一起退) — the single-instance id off the Mac covered the executable's path as well as the config dir, so a copy started from another path (autostarted from where it was first run, opened later from where it was put, or a package beside a download) ran as a second app with its own tray item beside the first, both on the gateway's one port. Omarchy's tray, Quickshell's watcher and host each drop a duplicate registration, so two icons can only come from two processes. The id now covers the config dir alone; a sandboxed HOME or config dir still runs on its own. Tested in the Omarchy VM (Hyprland 0.56): v0.1.428 at ~/.local/bin and a copy at /tmp ran side by side, RegisteredStatusNotifierItems listing two; this build at ~/ and a copy at /tmp: the copy's panel opened the running one's panel and exited, one item registered. go vet, go build -tags nogui (GOOS=linux, GOOS=windows) and go test -tags nogui ./internal/gui/... pass. yetone 2026-09-30 11:03:08 +08:00
  • 23365f2216 library: a long link or description stays inside its row, an icon whose picture failed is drawn again, and what the Library couldn't write is listed on the page (#257 long links break an item; Discord, neiko: Claude's icon missing until a refresh; JasonLeeForOnly: the toast says "see Library" and the Library shows nothing) yetone 2026-09-30 11:01:17 +08:00
  • 224304ab08 codex: third-party and group models get Codex's tool search, so the first request no longer carries every MCP and ChatGPT-app tool's schema (#258: 190K tokens before the first word on a magpie model, ~26K on Codex's own) yetone 2026-09-30 10:49:28 +08:00
  • 446b39ae44 factory: an account with no organization asks Factory's whoami for it before it sends, as droid does, and every new token asks again, so its model requests carry X-Factory-Org-Id (#242 tasselx: "Factory: Forbidden" on GLM-5.2) yetone 2026-09-30 10:46:55 +08:00
  • 5b6ce747f2 gateway: a group whose later members are all out of allowance retries a passing failure on the member that may still answer, and tells the agent that failure rather than the allowance's (Discord, waroy: Codex run out, Grok busy a moment — Codex was told usage exhausted and stopped taking input) yetone 2026-09-30 10:44:18 +08:00
  • 3fe2ff9958 rules: a routing group's rule can be for the agent compacting its conversation, so a cheaper, faster model writes the summary while the conversation stays on its own (X, MR.Guo: 希望 Magpie 能像 OpenCode 一样支持单独指定 context compression model:当 Agent 需要做上下文压缩时…指定一个更便宜、更快的小模型专门负责总结和压缩历史上下文) — a compaction went to whatever model the conversation was on, the priciest in the group as often as not, and a long session paid for each summary at that price. A rule's new condition, compact (provider.Rule.Compact, "compacting"; magpie group rule add <group> use=<model> compact, compact=yes|no; the GUI editor's 压缩上下文时 / compacting toggle; the TUI hint), matches a request that is an agent compacting, told from the agent's own prompt (internal/gateway/compacting.go, read from each one's release): the system prompt of Claude Code 2.1's /compact and auto-compact, OpenCode's compaction agent, Pi, Gemini CLI's state_snapshot and Qwen Code, or the last user message of Claude Code's, Codex's (and magpie's own for a Codex compaction_trigger) and Kimi Code's; a prompt only quoted earlier in a conversation doesn't count. Only in a group with such a rule is a request looked at this way, so nothing else changes. A compaction is decided on its own, never held by the turn's rule, its classifier never asked; it isn't remembered for the turn, and neither the conversation's affinity nor the turn's size is taken from its answer, so the requests after it go on on the model the turn was on and its cache (without that, the next request of a Claude Code turn followed the summary to the cheap model). A member whose context is less than the compaction is passed over for the next rule that matches, or the group's order (RuleHit.Small). The trace tells it ("The agent is compacting the conversation, so rule 1 sends the summary to …", the models passed over named), and the editor notes a compact rule's model taking less than another member. Docs: start#rules (en, zh). Tested: internal/gateway/compacting_test.go — each agent's prompt taken, a plain and a quoted one not; in a group with rules compact→a/small and tokens≥2000→b/big, a long Claude Code turn on b, its /compact to a (Compact, N=1), the turn's next request back on b held, Codex's compaction to a too; a 70k-token compaction skipping a (64k) for rule 2 with Small [a/small]; a group with no compact rule routing a compaction as before; ParseRule/Line/Conditions/Matches for compact. TestRuleCompact failed with the affinity kept from the summary. internal/gui/tests/routing-compact.test.cjs, Chromium and WebKit, en and zh: the compaction in the trace, the rule's words, the toggle on and off with nothing moved, the room note, a compacting-only rule saved as compact:true and not refused. routing-kind/served/signin/steady, click-scroll and old-webkit pass. go vet, go build -tags nogui (and GOOS=linux, GOOS=windows) and go test -tags nogui ./... pass. Not tried against real agents' compactions through the gateway (the prompts are matched as read from their releases) nor in the real Wails window. yetone 2026-09-30 10:24:13 +08:00
  • ffa55fc277 copilot: Auto — Copilot's own pick, the only choice a Student plan has — is listed for every Copilot account and works through magpie, as Copilot's clients do it (#256; Discord, CakeJskSUMM: Copilot Student can only use Auto) — Copilot's /models doesn't list Auto; its clients add it themselves (Copilot CLI 1.0.79: {id:"auto",name:"Auto"}, AUTO_MODEL_ID) and ask POST {api}/models/session with {"auto_mode":{"model_hints":["auto"]}} for a session_token, the selected_model and expires_at, each request then naming that model with the token in Copilot-Session-Token. magpie listed no Auto, failed an account whose list left nothing to pick by hand ("Copilot lists no chat model for this account"), and sent "auto" as a model. Now copilot_auto.go keeps the session per GitHub account, asked again two minutes before it expires; the gateway resolves Auto before choosing the API, so a picked model served only on /responses goes there; a body still naming "auto" (the model test) is rewritten; the endpoints of every model Copilot lists, hidden ones too, are kept for the picked one. A reply to "auto" from another model isn't counted as swapped (Cursor's auto too); the Copilot CLI's picker, which offers auto itself, leaves out the listed one. Tested with fakes in a sandbox HOME: provider/copilot_auto_test.go (a Student-like account whose models are all disabled lists [auto], the session body sent as above, requests carry the picked gpt-5-mini and the token, one session reused, renewed near expiry, an object-shaped selected_model, a refusal reported) and gateway/copilot_auto_test.go (a chat request for copilot/auto goes to /responses as gpt-5-mini with the token, two turns one session; fails without the gateway change); served_test gains the auto cases. go vet ./internal/..., go build -tags nogui (and GOOS=linux, GOOS=windows) and go test -tags nogui ./... pass. Not tried with a real Student account: what its /models returns, whether the plain session request suffices (Auto v2's /models/session/intent is not done), and the body is from VS Code's Copilot Chat, not captured traffic. yetone 2026-09-30 10:17:15 +08:00
  • be4298b294 library: Pi 0.99's own MCP — with Pi 0.99 or later on PATH and no MCP extension replacing it, MCP servers go in the mcp.json Pi reads itself, in its shape (command/args/env, url/headers, no adapter transport keys; SSE refused, which it has none of) (Discord feedback, 何似在人间: Pi now has native MCP) — Pi 0.99 (extensions/mcp/config.ts) reads mcpServers from <agent dir>/mcp.json, unless pi-mcp-adapter or pi-mcp-extension is installed (now also found in Pi's extensions folder) or settings.json has -builtin:mcp; then the files are as before. Servers magpie or the user had in mcp-adapter.json, which nothing reads now, are moved into mcp.json, the adapter's settings and anything mcp.json has differently left where they are and still found, both files backed up and written together (edit.Atomically). Without an adapter found, mcp.json is no longer moved to mcp-adapter.json, since it may be Pi's own. agent.InstalledVersion asks the CLI on PATH, once per binary. Tested with TestPiMCPNative in a sandbox HOME with a fake pi on PATH: 0.98 writes mcp-adapter.json, 0.99 moves the servers over with the user's fields kept, writes and removes in Pi's shape, SSE refused, the adapter (listed or in extensions) or -builtin:mcp keeps the old file; TestPiNativeVersions, TestInstalledVersion. go vet ./internal/..., go build -tags nogui (and GOOS=linux, GOOS=windows) and go test -tags nogui ./... pass. Not tried with a real Pi 0.99. yetone 2026-09-30 10:14:38 +08:00
  • f8512b836e gui: Command Code's subscription tile lists the Go plan and warns, as Qoder's and Factory's do, that a Go account is used through Command Code's private interface, which Command Code has said may get an account banned when used from other tools (Discord, HuanCheng Bai) — Pro, Max and the other plans go through its Provider API. The risk note in en and zh. commandcode-risk.test.cjs, Chromium and WebKit, en and zh (new): the tile shows Go and the warning in both languages; click-scroll and old-webkit pass. yetone 2026-09-30 10:12:28 +08:00
  • 30995a8185 gateway: a request the vendor's safety filter refuses with an error status (OpenAI's 400 bio_policy, cyber_policy, content_policy_violation, invalid_prompt flagged against its usage policy, Azure's content_filter) goes on to the next account or model, as a refusal with nothing said already did (#248 Koohoko: Codex was handed "This content was flagged for possible biological risk" with the other accounts never asked) — settle read only whole 2xx replies for a refusal, so an error status was passed straight through. policyRefusal tells such an error from a malformed request, in a body, an error object or a stream's error event (Responses' own error event included); keepRetry notes it in X-Magpie-Refused beside the error it rewrites, for settle, and the header never reaches the agent. Tested with refusal_accounts_test.go: a 400 bio_policy on the first Codex account is answered by the second, a stream error event likewise, a plain 400 invalid_request_error is not retried. go vet ./internal/..., go build -tags nogui (and GOOS=linux, GOOS=windows) and go test -tags nogui ./... pass. yetone 2026-09-30 10:09:29 +08:00
  • dd12c75bff codex: a turn relayed through Codex accounts says it is the Codex client it came from, or newer, so a model Codex CLI reaches on its own is no longer refused through magpie (Discord, lantern2077 / TuskerManshu: gpt-6.1-sol through a routing group answered 400 "The 'gpt-6.1-sol' model is not supported when using Codex with a ChatGPT account" while Codex CLI alone could use it) — the ChatGPT backend serves a model only to a client new enough for it. Re-signing a pool turn, codexSign set User-Agent codex_cli_rs/<codexVersion()>, the newest of a constant 0.154.0, ~/.codex/models_cache.json's client_version (written by the Codex before the last update) and codex --version when codex was on PATH, ~/.local/bin or Homebrew's — nvm, mise, fnm, npm prefixes, pnpm and Volta were out of reach, above all in magpie serve — while the client's own version header was relayed: one turn saying two versions, the older in the User-Agent. Now codexSign writes one version in both, never older than the relayed one; the gateway notes the newest version a Codex client (originator or codex… User-Agent) has come with (SawCodexClient) and codexVersion, also asked by the accounts' model lists, takes it; the fallback is 0.159.0; the models cache is read from CODEX_HOME when set; codexExecutable also looks in proc.UserBinDirs() (every Node of nvm, mise and fnm, ~/.npmrc's prefix, pnpm, bun, Volta; %APPDATA%\npm and the like on Windows; codex.cmd/.exe there). Tested with TestCodexPoolSaysTheClientsVersion against a fake backend refusing gpt-6.1-sol below 0.161.0: Codex CLI 0.161.0 through the pool failed on main with codex_cli_rs/0.154.0 and passes; TestCodexVersionSeen (Claude Code's User-Agent ignored, CLI and Desktop versions read, never lowered, the model list asked with it), TestCodexVersion with CODEX_HOME, TestCodexExecutableOffPath (nvm, mise, an .npmrc prefix with PATH /usr/bin:/bin). go vet ./internal/..., go build -tags nogui (and GOOS=linux, GOOS=windows) and go test -tags nogui ./... pass. Not tried against the real backend: the version gating is read from the error and the models cache, not seen on the wire. yetone 2026-09-30 10:06:15 +08:00
  • 872bba3c92 wsl: Claude Code installed in a WSL distro is found and wired as its own agent, Claude Code · WSL <distro> (#254 Xws-s: magpie finds Codex and Pi in WSL but not Claude Code) — the WSL registry gains Claude Code: the probe looks for ~/.claude and command -v claude, and claude@wsl:<distro> edits the distro's ~/.claude/settings.json through \\wsl.localhost, with the gateway as the distro reaches it (127.0.0.1 when mirrored, Windows' address under NAT). claude(home) is now claudeIn(here(home)) like Codex and Pi: the base URL written, the routed test and Check use the place's gateway; what Claude Code had before magpie (model, base URL, token) is kept under claude@wsl:<distro> so it never mixes with Windows'; Check reads the distro's own /etc/claude-code/managed-settings.json. A stopped distro shows Claude Code's and magpie's models without being opened. Claude Code's model stand-in (a Claude name magpie doesn't serve) also looks at the routed distros seen running, never starting one. Sign-in, sessions, prompt history and Last used stay Windows-only, as for the other WSL agents. Native Claude Code is as before. Tested in wsl_claude_test.go with fake distros in a sandbox HOME: probe and discovery (folder only, program only, beside Codex and Pi), a pick under NAT and mirrored writing the distro's address and otherwise the same settings.json, Check and managed settings, restoring on the way back, a stopped distro never opened, the stand-in. go vet ./internal/..., go build -tags nogui (and GOOS=linux, GOOS=windows) and go test -tags nogui ./... pass. Not tried on real Windows/WSL. yetone 2026-09-30 10:02:51 +08:00
  • 7a3337dd25 gateway: Claude Code's auto mode classifier gets an answer through magpie on models that reason, and never lingers as a kept Claude Code run (#250 netfishx: the classifier often fails; Claude Code notes the session "isn't eligible" for free classifier requests through 127.0.0.1:3425) — per code.claude.com/docs/en/auto-mode-classifier-billing and Claude Code 2.1.285, behind a gateway its first requests ask the API to classify (the dangerous-tool-use-2026-09-03 beta, a safeguards field, safeguard_results in the reply); keyed Anthropic already relays them as sent and returns the reply as it came (now guarded by a test), and no other model answers them, so Claude Code falls back to classifying locally: a request of its own on the session's model, the transcript in <transcript> blocks, a system prompt asking <block>yes|no</block>, thinking off, 64 tokens for the first stage stopping at </block>, 60s. A model that reasons whatever it is told (OpenAI Responses/Chat, DeepSeek, a vendor's model behind an Anthropic API) spent the 64 tokens reasoning and answered nothing, which Claude Code reads as no verdict and blocks. Now (automode.go) such a request to a model that isn't Claude gets 2048 tokens more when it asked for less, as Claude Code gives a Claude model that can't turn thinking off, and the least reasoning its levels allow (fitEffort "none"), left to the vendor when they aren't known; a Claude model's request goes byte for byte. Through the Claude subscription the classifier, whose transcript spans several user messages, was kept as an idle claude run (the one-off test was fewer than two messages), pushing out the conversations' own: a request with no tools and no reply is one-off now, and the classifier runs at --effort low. magpie never answers safeguard_results itself. Tested in automode_test.go against httptest fakes and a fake claude: Responses and Chat (budget raised, effort none/low), unknown levels (room given, effort left, a title ask untouched), a vendor on an Anthropic endpoint, the bridge (no idle runs, --effort low) fail on main; TestAutoModeServerSideReviewRelayed keeps the beta, safeguards, safeguard_results and tool-use ids as they are. go vet ./internal/..., go build -tags nogui (and GOOS=linux, GOOS=windows) and go test -tags nogui ./... pass. Not tried in a real auto-mode session nor with real vendors taking effort none. yetone 2026-09-30 09:36:27 +08:00
  • 1cdc349145 factory: X-Factory-Org-Id carries Factory's own id for the org, as droid sends it, not WorkOS's, and a refused org is mended once (#242 tasselx: "403 permission_error: Factory: Requested active organization is not accessible by this user") — read again from droid 0.229.0 (still the latest @factory/cli-darwin-arm64): its header builder (Ad) sends X-Factory-Org-Id only from the stored active_organization_id, left off when null; that field is only ever filled with Factory's (Firestore) org ids, whoami's orgId or /api/app/auth/me's memberships, never WorkOS's org_… ids, which go only to WorkOS's refresh; moving a token that names no org into the first of /api/cli/org's workosOrgIds (gt) saves active_organization_id null; routine refresh names no org. magpie sent the WorkOS org id as the header, which Factory looks up as one of its own and refuses. Now factoryCreds keeps Active (activeOrganizationId) from whoami's orgId, asked without an org header, and only it is sent; Org, the WorkOS id, is kept but never sent; logins saved by v0.1.432 have no Active and so send none; refresh names no org. The sign-in checks the JWT's external_org_id then org_id, carries on as droid does when /api/cli/org fails or lists none, and when whoami fails but the token says whose it is. On that 403 an account mends itself once (Account.retry, Provider.Retries/Retry, asked by gateway.forward and by the usage read): an active org sent is dropped and the request goes again without the header; with none sent, the token is put in the first org /api/cli/org lists (droid does this at sign-in, not on a 403). Other 403s and 401s are passed on as they were. Tested in factory_test.go against fake WorkOS and Factory servers that answer droid's way: TestFactoryActiveOrg (a token WorkOS put in org_A keeps fac_A and gets a 200; a v0.1.432-format login sends no header and gets a 200) and TestFactoryOrgRefused (a left active org dropped and retried with no refresh; a left token org refreshed into org_A and retried; the usage read the same; other 403s and 401s not retried) fail on main with the reporter's 403; the existing tests now check whoami and /api/cli/org get no org header and refresh sends no organization_id. go vet ./internal/..., go build -tags nogui (and GOOS=linux, GOOS=windows) and go test -tags nogui ./... pass. Not tried against a real Factory account: that the server reads the header as a Factory org id is from droid's code, not seen on the wire; the gateway's retry is covered only by a provider-level test; accounts with several orgs take the first, with no picker. yetone 2026-09-30 09:34:21 +08:00
  • d37b821908 toml: Codex's config.toml with a multi-line array (notify = [ … ]) is edited right, and an edit that fails leaves every file as it was (#253 kerwinliu00001: "edited TOML is invalid: line 2, column 1: incomplete number", config.toml left with model_provider gone and [model_providers.magpie] added but no model) — the top-level key edits (GetTOMLTop, SetTOMLTop, DelTOMLTop) went line by line with regexes: a new key went after the last line that looked like key = …, which for notify = [ was inside the array; key = value lines inside a multi-line string were read as keys and a line starting with [ as a table; replacing or deleting a multi-line value touched only its first line. They now locate the top-level keys with the TOML parser, as the table edits already did, each with its value's whole range however many lines it takes (parseTOMLFile, the unnamed root table beside the tables); SetTOMLTop finds all its keys in the file as read and writes once. A file the parser refuses is still read line by line, so a broken config keeps showing its model, and writes to it are refused untouched. And a pick is several writes (Codex: drop model_provider, add magpie's provider table, set openai_base_url and model), each checked, so one failing left the earlier ones on disk: edit.Atomically(fn, paths…) puts each file back to its bytes and mode, or removes one it created, when fn fails, and every field set and Sync of Codex (config.toml and magpie-models.json, so Codex in WSL too), Kimi and Grok goes through it, as do Codex's MCP server writes (putCodex, delCodex). Tested in temp dirs and a temp HOME: the reporter's file read, set and deleted directly and through Codex's model field (signed in, with model_provider and model above the array, with magpie as the provider), the array's text kept and the TOML valid, and back to a Codex model; nested arrays, trailing commas, comments in arrays, [x] and a = b in strings, an inline table in an array; """ and ''' strings holding model = … and [table] lines not read as keys, new keys after their closing quotes, one deleted whole; a forced failure of Codex's last write leaving config.toml byte-identical; TestAtomically. They fail on main (the Codex ones with the reporter's error). go vet ./internal/..., go build -tags nogui (and GOOS=linux, GOOS=windows) and go test -tags nogui ./... pass. Not tried against a real ~/.codex, in WSL or in the GUI; multi-line inline tables (TOML 1.1) are refused by the parser and so left untouched with an error. yetone 2026-09-30 09:31:59 +08:00
  • 9643276b94 wsl: Pi installed in a WSL distro is found and pointed at magpie there, as Codex in WSL is (#251 X4fun) — magpie looked in each running distro only for Codex (~/.codex, command -v codex), so Pi's ~/.pi/agent there was never listed. The agents magpie looks for in a distro are now a small registry (wslKinds: id, name, the folder under $HOME, the program, the agent at a place, the restart advice, a stopped distro's options), from which the probe script is built and the distro's agents listed; Codex and Pi are in it. Pi · WSL <distro> (pi@wsl:<distro>) reads and writes ~/.pi/agent/settings.json and models.json through \\wsl.localhost\<distro>, with the same models.json block as Pi on this machine (thinkingLevelMap, reasoning, compat, contextWindow, maxTokens) but its baseUrl, and the Anthropic models' /messages baseUrl, at the gateway as the distro reaches it: 127.0.0.1 when mirrored, Windows' address under NAT, with the same NAT notice as Codex; its config check and refresh keep that address. A stopped distro shows what magpie last saw (wsl.json keeps Pi's fields as pi.model and pi.effort, Codex's bare keys unchanged so existing files still read), reads nothing, and a pick starts it first. Pi on this machine is unchanged (pi(home) is piIn(here(home))). Tested with a new internal/agent/wsl_pi_test.go against fake distros in temp dirs: the probe script and its dir:.pi / bin:pi lines; a distro with Pi only by folder, only by program, with both agents and with neither (ids, order, name, icon, current model and thinking level, the two agents' values kept apart); a pick under NAT and mirrored writing settings.json and models.json equal to Pi's native block but for the address, the check passing and a refresh keeping the distro's address, the real ~/.pi never made; a stopped distro listed from wsl.json with nothing opened, a pick starting then writing. All four fail on main; the Codex WSL tests pass unchanged. go vet ./internal/... (and GOOS=windows for internal/agent), go build -tags nogui (and GOOS=linux, GOOS=windows) and go test -tags nogui ./... pass (TestUserPath and three Claude status tests timed out under load and pass alone). Not tried on real Windows/WSL nor with a real Pi in a distro. yetone 2026-09-30 09:29:28 +08:00
  • 255bf7fd1b icon: a site's icon is fetched with Clash's TUN fake-ip or Surge's enhanced mode on, and through a proxy on this computer (#252 loosheng: "the icon host … resolves to 198.18.0.x, which is not public") — the guard on icon fetches ("From the website", an import link's icon) refused 198.18.0.0/15, RFC 2544's benchmarking range, where fake-ip proxies answer every name and carry the connection to the real host; it is now let through, since no machine on the user's network sits there. Loopback, private, link-local, CGNAT and the documentation ranges are still refused. And with a proxy set (Clash's system proxy at 127.0.0.1:7890), the transport dialed the proxy through the same guard and refused it as loopback: the proxy's own address is now dialed as it is, and the site's name is judged before the proxy is asked, since the proxy resolves it where magpie can't see (a name that doesn't resolve here is left to the proxy). Tested: TestPublicIP and TestPublicDialFakeIP (198.18.0.5 and 198.19.0.200 dialed, 127.0.0.1, 10.1.2.3, 192.168.1.10, 169.254.169.254, 100.64.0.1 refused before any dial); TestGuardClientThroughLocalProxy, a fake proxy on 127.0.0.1 fetching a public site's icon while 10.x and 192.168.x sites are refused without asking the proxy; both fail on main. go vet ./internal/..., go build -tags nogui (and GOOS=linux, GOOS=windows) and go test -tags nogui ./... pass. Not tried with a real Clash or Surge, nor in the real Wails window. yetone 2026-09-30 09:23:22 +08:00
  • a08b061b2f commandcode: a Command Code Go-plan account is served through the gateway in the CLI's own format, at /alpha/generate (Discord #general, Mikan: 有计划支持command code的go plan吗,走他们的私有格式) — Go has no Provider API access, so magpie's Command Code Plan subscription, which asks api.commandcode.ai/provider, could not serve a Go account. Read from command-code 1.72.2's bundle: the CLI asks POST /alpha/generate for the models Command Code hosts, with {config, memory:null, taste:null, skills:null, permissionMode, params:{model, messages, tools, system, max_tokens, stream, temperature?, reasoning_effort?}} (createModelClient, toWireMessages: text / tool-call / reasoning parts, tool results as role "tool" tool-result parts, images as data URLs), the headers of buildCommandAuthHeaders (User-Agent cli, x-command-code-version, x-cli-environment production, x-project-slug, x-taste-learning, x-session-id, Bearer key), and reads newline-delimited JSON back (text-delta, reasoning-delta, tool-call, finish with totalUsage, error, abort). Its plan table lets individual-go / individual-go-v1 use the "opensource" models less a blocked list. Now: an account's plan is read from /alpha/billing/subscriptions and kept 10 minutes per key (the Usage page's read refreshes it; a failed read falls back to the sign-in's plan and is retried after a minute; GOAT is not taken for Go). A Go account is asked at /alpha/generate, translated to and from Anthropic Messages, OpenAI chat and Responses, streamed or not, tool calls included, and lists Go's models from the CLI's table (GPT-6 Luna, GPT-5.6 Luna, DeepSeek V4 Pro/Flash, Kimi K3, GLM-5.3, MiniMax M3, Qwen 3.8 Max/Flash, MiMo V2.6 Pro); MODEL_NOT_IN_PLAN is a 403 and exhausted credits a 402, so routing moves on; token counts are estimated locally. Pro, Max, Ultra, GOAT and Teams accounts go on the Provider API as before. Tested with internal/gateway/commandcode_test.go and internal/provider/commandcode_go_test.go against a fake Command Code in a temp HOME: a Go account routed to /alpha/generate and a Pro one to /provider/v1/messages, the request body and headers, streamed Anthropic, chat and Responses (streamed and not) with text, reasoning, a tool call and usage, MODEL_NOT_IN_PLAN over HTTP and in the stream, credits run out, a stream error, a reply cut short, the plan cached, re-read and falling back; also under -race. They fail with the routing off. go vet, go build -tags nogui (and GOOS=linux, GOOS=windows) and go test -tags nogui ./... pass. Not tried with a real Go account: which models Go allows today, whether the server checks config, project slug or session id, the usage arithmetic with cache tokens, real errors' codes, and pause_turn (ended rather than resent). yetone 2026-09-30 08:36:08 +08:00
  • 899ddf594c mimo: a Xiaomi MiMo account (the one MiMo's app signs in to) is signed in inside magpie and its models served through the gateway to any agent, and MiMo Code is an agent magpie configures (#249 thedavidweng) — magpie had neither. The subscription (id mimo-app, shown as Xiaomi MiMo) signs in with Xiaomi's long-poll login (account.xiaomi.com/longPolling/loginUrl, sid mimosgp) and keeps the MiMo server's cookie session (serviceToken, userId) in logins.json at 0600; the passToken is replayed to renew the session after a day or on a 401. Requests go to POST <base>/route/chat/completions on mimo-server-{sgp,ru,in}.xiaomimimo.com/api with X-Mimo-Source mimocode-cli-free and X-Client-Version; mimo-auto is sent as mimo-pro; the models are mimo-pro and mimo-flash. Usage from /user/usage and /user/xiaomi/subscription/self. Several accounts, switched, on/off and forgotten as the other subscriptions. The add list has a Xiaomi MiMo tile (Free · Starter · Plus · Pro · Ultra) that says Xiaomi may treat this as third-party use before anything is posted; en and zh. magpie accounts takes mimo. MiMo Code (agent mimocode, OpenCode-like) is configured in its config dir (MIMOCODE_HOME/config when set), reading mimocode.jsonc, mimocode.json or config.json, whichever is there. Tested with internal/provider/mimo_test.go against fake Xiaomi SSO and MiMo servers in a temp HOME, internal/agent tests for MiMo Code's config, and internal/gui/tests/mimo-sub.test.cjs, Chromium and WebKit, en and zh; they fail without the change. factory-sub, click-scroll, old-webkit, zed-sub and add-sheet pass; go vet, go build -tags nogui (and GOOS=linux, GOOS=windows) and go test -tags nogui ./... pass. Not tried with a real Xiaomi account: the long-poll's success payload, the SSO cookie chain, whether /route accepts magpie's requests, the UTC+8 times, and whether MiMo Code picks up the config; not tried in the real Wails window. yetone 2026-09-30 07:37:42 +08:00
  • 16e493149b factory: a Factory subscription (the account Droid signs in to) is signed in inside magpie and its models served through the gateway to any agent (#242 thedavidweng: 希望支持 Factory 订阅作为 provider) — read from droid 0.229.0's shipped bundle (npm @factory/cli-darwin-arm64), droid not run: the sign-in is WorkOS's device flow (api.workos.com/user_management, droid's client id: /authorize/device, then /authenticate with the device-code grant, authorization_pending / slow_down / access_denied / expired_token), refresh with grant_type=refresh_token and the org, each refresh a new refresh token; a token naming no org is moved into the first of GET /api/cli/org; GET /api/cli/whoami's region picks api.eu.factory.ai for eu. Claude and MiniMax M2.7 go to /api/llm/a/v1/messages, GPT and Grok to /api/llm/o/v1/responses, the open models Factory hosts to /api/llm/o/v1/chat/completions, with droid's headers (Bearer, X-Factory-Client cli, X-Client-Version, User-Agent factory-cli/0.229.0, X-Factory-Org-Id, x-session-id, x-assistant-message-id, x-api-provider, x-provider-routing-source, OpenAI-Platform for OpenAI's); Gemini, on Factory's own /api/llm/g format, is left out. Usage from GET /api/billing/limits: 5 hours, 7 days and 30 days for the standard pool and "Droid Core · …" for the hosted models, and Extra usage in dollars, a full window not stopping routing while extra usage is left. Droid's own login is encrypted with a keychain key and is not read: magpie keeps its own in logins.json. Several accounts, switched, on/off and forgotten as the other subscriptions; a token renewed before its end, a refused refresh marking the account for a new sign-in. The add list has a Factory tile (Pro · Plus · Max) that says Factory may treat this as third-party use before anything is posted, and the code screen names Factory's sign-in page; en and zh. magpie accounts takes factory or droid. Token counts are estimated locally. The model list is droid 0.229.0's registry (30 models), fixed. Tested with internal/provider/factory_test.go against fake WorkOS and Factory servers in a temp HOME (the device flow, org lookup and refresh, an EU account's requests on the EU host with each model on its route and headers, usage read; a token near its end renewed once and the new refresh token kept, a refused refresh marking the account and a new sign-in clearing it; two accounts each signing with its own org, on/off, switch, forget), also under -race; internal/gui/tests/factory-sub.test.cjs, Chromium and WebKit, en and zh (the tile, its warning, nothing posted before Sign in anyway, then {agent:"factory"}, the code with Factory's wording, nothing scrolled). Both fail without the change. click-scroll, old-webkit, zed-sub and add-sheet pass; go vet, go build -tags nogui (and GOOS=linux, GOOS=windows) and go test -tags nogui ./... pass. Not tried with a real Factory account: no real sign-in, request or usage call, the EU region, windowEnd's format, or whether Factory wants more than droid's code shows; not tried in the real Wails window. yetone 2026-09-30 07:31:05 +08:00
  • cb164c1164 providers: a Zhipu or Z.ai key on a team's GLM Coding Plan can be given the team's organization and project IDs, and the Usage page then shows the team's 5-hour and weekly windows (#236 EZN7L2C3: the IDs are in the BigModel console; CC Switch asks the team quota with them) — a key pasted alone was asked for the team's quota (type=2) without the Bigmodel-Organization / Bigmodel-Project headers, which magpie can't find from a key, so a team key showed nothing. providers.json keeps zhipuTeam {org, project} for those key providers (trimmed; both empty removes it; a save without it keeps it, {} clears it; carried through from the Zhipu and Z.ai presets). The team query sends the typed IDs, else those of a ZCode team login holding the key, and is now asked at the key's own host (open.bigmodel.cn, as CC Switch asks it) first and bigmodel.cn (where ZCode asks it) only when that gives no windows. It still runs only when the key's personal quota fails or has no windows. The provider editor shows Team org ID / Team project ID (团队组织 ID / 团队项目 ID), optional, for Zhipu and Z.ai key providers alone, adding and editing; one without the other is refused before anything is posted. Tested with internal/provider/zhipu_team_key_test.go against a fake open.bigmodel.cn / bigmodel.cn in a temp HOME: the IDs kept trimmed, the team asked with type=2, the key and both headers at open.bigmodel.cn and read as 5 hours 42% and weekly 10%, a key without them asked with no team headers at both hosts and showing nothing, a pay-as-you-go key with blank IDs no card, Z.ai taking the fields and DeepSeek not. internal/gui/tests/zhipu-team.test.cjs, Chromium and WebKit, en and zh: the fields for a saved Zhipu provider and from the add sheet for Z.ai, none for DeepSeek or a relay, trimmed values posted, one alone refused, both empty posting {}, nothing scrolled on a click. Both fail without the change. click-scroll, old-webkit, provider-proxy and account-proxy pass; go vet, go build -tags nogui (and GOOS=linux, GOOS=windows) and go test -tags nogui ./... pass. Not tried with a real team key or in the real Wails window; the team's reset counts are not read for a key (the reset list is asked only with a ZCode sign-in token). yetone 2026-09-30 05:49:19 +08:00
  • 301e25c185 gateway: a vendor's safety filter refusing a request before saying anything goes on to the next account or model instead of reaching the agent as an empty reply (#248 Koohoko) — a stream that ended in a refusal (Anthropic's stop_reason "refusal", OpenAI's content_filter on Chat and Responses, Gemini's SAFETY / PROHIBITED_CONTENT / BLOCKLIST / SPII / IMAGE_SAFETY, or an error event with such a code) with no text, reasoning or tool call before it was passed to the agent as an answer, which it would ask again for, paying each time. The held stream now counts framing events that say nothing (block starts and stops, signatures, empty items, parts and deltas) as lead-in, so a refusal is seen while nothing has been sent; the try is marked "refused", logged at status 400 with its tokens, the account not rested, and the next member asked. With nobody left, the agent gets a 400 invalid_request_error "<model> refused this request (safety filter: …) via <provider>". A refusal after content has been sent passes through as before. A Responses reply rendered from another API says incomplete with reason content_filter. Routing shows "refused (safety filter)" with its explanation, en and zh. Tested with internal/gateway/refusal_test.go against fake vendors: a refusal fails over to the next member, content_filter fails over, a refusal with nobody left is the 400, a refusal after text passes through, and each vendor's refusal shape is recognised; the new tests fail without the change. go vet, go build -tags nogui (and GOOS=linux, GOOS=windows), go test -tags nogui ./... and -race on the gateway pass; routing-kind, routing-steady, panel-routing, old-webkit and click-scroll pass. Not tried against real vendors' refusals or the real claude CLI; the Gemini shapes are from its API docs; a stream that only sends empty framing is held until its first text, within the existing 15s / 1MB bound. yetone 2026-09-30 05:24:00 +08:00
  • 74834748b9 usage, routing: Hide emails hides the whole of an address a vendor has half masked itself (#236 EZN7L2C3: Zhipu shows an imported account as abc***gh@abcd.com and only ***gh@abcd.com was hidden, leaving abc in sight) — the address pattern took no * in the part before @, so the match began after the stars. It now takes * and • in both parts, so the address is one match and all of it is swapped for stand-in letters; a tooltip masked already (its dots now reading as an address too) is left alone instead of being masked again over the original it keeps, which would have lost it. Tested with a new internal/gui/tests/mask-emails.test.cjs, Chromium and WebKit: abc***gh@abcd.com and a plain address in one line are two masked addresses with nothing of either left in the text, the tooltip dotted, an address drawn later masked as it comes, and turning it off puts the text and the tooltip back as they were; fails on main. click-scroll and old-webkit pass; go build -tags nogui passes. yetone 2026-09-30 05:07:47 +08:00
  • bf25a3ae80 omarchy: on Omarchy, magpie is drawn in Omarchy's own design language, follows its theme, drops its panel under the bar as Omarchy's own menus do, and can put its icon in Omarchy's bar (让整个UI/UX完全符合Omarchy的设计语言…单独对Omarchy做这个支持,现在的UI/UX当然要保留) — on Hyprland magpie looked like any other GTK app: its own light/dark palette, rounded shadowed popups, and a tray panel floated framed and dimmed in the middle of the screen, labelled with the program's name, never closing on a click elsewhere (focus follows the mouse there, so Wails turns off focus-lost hiding), and its tray icon was hidden in the bar's drawer. Now, only when Omarchy is detected (internal/omarchy: ~/.local/share/omarchy and its current theme), boot.js hands the page the current theme (colors.toml, the shell's theme TOML, Hyprland's border size, colour and rounding, Omarchy's font) as CSS variables before the first paint, and omarchy.css, all under html.omarchy, draws flat as Omarchy's shell does: the theme's background, no shadows or blur, corners as round as Hyprland's windows, popups and dialogs framed by the active border's colour, uppercase letter-spaced labels, rows and picks as a faint fill with the text in the accent. /api/omarchy is polled every 2.5s, so a theme picked in Omarchy's menu reaches the windows at once; Settings → Appearance shows the theme's name instead of Light/Dark/System. Under Hyprland the tray panel gets a title of its own ("magpie panel", set through GTK since Wails leaves frameless windows untitled), and each opening gives Hyprland a named Lua window rule placing it under the bar at the click, 6px in from the edges, pinned, opaque, borderless; its height is capped to the work area. While it is open a non-consuming left-click bind emits a custom event on Hyprland's socket2; a click outside the panel and below the bar closes it (the bar is left to its icons, so the icon toggles), and the bind is removed on close or, after Escape, on the next click. The tray item's SNI id is "magpie" instead of "Wails", so it can be pinned. Settings → Bar icon (状态栏图标), shown only in the app on Omarchy's Hyprland, puts an Omarchy bar-widget plugin (usemagpie.magpie, a bird glyph, in ~/.config/omarchy/plugins, then rescanPlugins and plugin enable) beside Omarchy's own icons: a click runs magpie panel, a right-click magpie app; Off disables and removes it; a widget pointing at a moved or older magpie is rewritten at start. magpie panel (new) opens the quick panel, starting magpie if needed, or toggles it in the running instance. Elsewhere nothing changes. Tested in a Hyper-V VM on the latest Omarchy (Hyprland 0.56, 2528×960 at scale 1.6): the look with tokyo-night; the panel at [1134,32] under the bar; clicks on the panel and the bar keep it open, a click on another window closes it, the icon toggles it, Escape closes it and the bind goes; Bar icon On adds the bird to the bar, its click opens the panel under it (also with magpie not running), a second click closes it, right-click opens the window, Off removes it; autostart becomes app-magpie@autostart through systemd's XDG generator; the launcher entry and icon are there. Go: internal/omarchy tests (theme parsing, panel placement on scaled and turned monitors, click-outside geometry, widget files quoted with the executable, KeepWidget rewriting only a stale one). A new internal/gui/tests/omarchy.test.cjs, Chromium and WebKit, en and zh: with no Omarchy theme the page has no omarchy class or theme style, the Appearance choices show, no Bar icon row, nothing asked of /api/omarchy; with one, the theme's background, square corners, its name, and Bar icon's On and Off each posted with nothing scrolled, hidden when the bar isn't there. old-webkit now lets omarchy.js load in the head beside boot.js (it does nothing off Omarchy). The full GUI suite, gofmt, go vet, go build (GOOS=linux, GOOS=windows) and go test -tags nogui ./... pass. Not tried: other Hyprland setups than Omarchy's, a multi-monitor VM, and the GTK4 build in the VM (gtk3 was used). yetone 2026-09-30 04:50:53 +08:00
  • 3dc8f00f06 providers: each account of a subscription holding several can go through its own proxy — the provider's, none, or its own address (Discord, gakki: 是否可以为不同的codex账号设置不用的代理,现在是同一个供应商下使用相同的代理) — the per-provider proxy (#237) was one for all of Codex's signed-in accounts. A subscription's entry in providers.json now keeps accountProxies, by the account's name in lower case, each checked by settings.CheckProxy on save (ftp:// refused), addresses trimmed, accounts following the provider's proxy left out. Provider.ProxyChoice picks an account's own proxy, else the provider's, else the global one, so everything that already went through Provider.Via follows the account: gateway turns and fallback across accounts, usage and quota, token refresh, Codex's warm-up, reset and saved accounts' model lists, Claude's warm-up and the *_PROXY of Claude Code runs (ViaLogin / ProxyOfLogin), and Codex's own sign-in relayed as it came (ViaSignedIn). Extra accounts of Copilot, ZCode, Kiro and the like, made afresh without the provider's settings, now read them from providers.json, so they honour the provider's proxy too. A save without accountProxies keeps them; {} clears them. The provider editor, for a subscription with two or more accounts, lists each under the Proxy row: Provider's proxy | Direct | Custom, the address field's room kept so a pick moves nothing; Custom with no address is refused before anything is posted, its field focused without scrolling. en and zh together; no border stripes. Tested with internal/gateway/account_proxy_test.go in a sandbox HOME against fake proxies and a fake ChatGPT backend: three Codex accounts (own proxy, direct, following the provider) each reach the backend their own way in one turn and for usage; clearing an account's proxy sends it through the provider's, clearing the provider's through the global one, direct staying direct; ftp:// refused; TestClaudeAccountProxyEnv covers each Claude account's HTTPS_PROXY. Both fail with the per-account lookup off. account-proxy.test.cjs, Chromium and WebKit, en and zh (new), provider-proxy, click-scroll and old-webkit pass. go vet ./internal/..., go build -tags nogui (and GOOS=linux, GOOS=windows) and go test -tags nogui ./... pass. Not tried against a real proxy, real accounts or the real Wails window; Grok's per-account usage and Kiro's usage on a key still use the provider's proxy. yetone 2026-09-30 04:52:03 +08:00
  • 4f01baa65f catalog: fetched again each day magpie stays open, and sooner when a call's model has no price, so a model models.dev lists after magpie started is priced (#224 Koohoko: gpt-6.1-sol shows no cost) — the models.dev catalog, where list prices come from, was fetched only at the app's start (when older than a week), so a magpie left open, or magpie serve, never learned a new model's price. KeepFresh (internal/catalog/fresh.go), run by the app's backend and by magpie serve, looks every hour and fetches when the cache is older than a day; a call whose model has no price asks for a fetch (catalog.Missing, from usage's pricer) when the cache is older than six hours and none was tried in that time, only in a process keeping it fresh, so CLI commands and tests never fetch on their own. Loading and Reset now share a mutex instead of a sync.Once reassigned under a running Do, which a background Sync could hit while a call was being priced. Tested with fresh_test.go against a fake models.dev in a temp XDG_CACHE_HOME: stale after 25h and not before, Missing fetching nothing when not keeping, fetching once when keeping and gpt-6.1-sol then priced, and not again; passes under -race (failed under -race with the Once). go vet, go build -tags nogui (and GOOS=linux, GOOS=windows) and go test -tags nogui ./... pass. yetone 2026-09-30 04:28:01 +08:00
  • 9a599428bd warmup: a test for the weekly warm-up waiting for the daily time (#246): held at 08:32 before 09:30, both windows in one request at 09:30, at once with no daily time or with the 5-hour window running; fails on main yetone 2026-09-30 04:00:31 +08:00
  • 8117d6f4a9 zcode: a seat on a team's GLM Coding Plan (团队套餐) is signed in, routed and metered as ZCode does it, on BigModel (智谱) as well as Z.ai, and a Zhipu key on a team plan shows its 5-hour and weekly windows and resets (#236 EZN7L2C3: a BigModel team GLM Coding Plan, the sign-in refused it and the usage was never read) — magpie's ZCode sign-in only went to Z.ai and only looked for the account's own coding-plan key, so a member of a team's plan, whose seat lives in a team project, was refused, and a key on a team plan read nothing from /api/monitor/usage/quota/limit. Read from ZCode.app 3.14.3 (host/index.js and config/provider/zcode-builtin.json): the team rules account:{zai|bigmodel}-team-coding-plan use the individual plans' base URLs with no extra inference headers; getCustomerInfo lists the projects, projectType "2" being a team's (isBigModelTeamCodingPlanProject); fetchTeamCodingPlanEntitlement asks /api/biz/team/subscribe/product/querySubscribeDetail (EFFECTIVE + memberGrantStatus VALID usable, UNASSIGNED no seat, EXPIRED run out); ensureBigModelTeamPlanProjectApiKeyWithStatus / copyBigModelTeamPlanProjectApiKeySecret find or create "zcode-team-api-key" (keyType 2) in the team project and copy its secret; createBigModelBizHeaders sends the access token bare in Authorization with bigmodel-organization / bigmodel-project; resolveAccountTeamPlanRuntimeApiKey takes the stored oauth:{family}:access_token (skipping BigModel's when it equals zcodejwttoken); buildQuotaLimitUrl adds type=2 and createBigModelUsageHeaders sends the key with the org/project headers; ZCode keeps the chosen team in ~/.zcode/v2/setting.json providerFamilyConnectionSelections (legacy modelProviderFamilySelectedKeys "team-plan:builtin:…", migrateLegacyAccountConnectionSettings); pollPendingOAuth/startOAuthWithPolling init with {provider:"zai"|"bigmodel"}, BigModel's redirect param being redirect, its token the poll's bigmodel.access_token. Now: the sign-in first asks where the account is — Z.ai or BigModel (智谱) (POST /api/signin takes a site, StartSignInAt, "zcode:bigmodel" too) — and on its site tries the account's own plan, then a team project with a usable seat (a new zcode_team.go: zcodeTeamSignIn keeps the team key, org, project and the biz token, the plan named by its product, "GLM Coding Team" otherwise), then the Start Plan; a team seat not yet granted or a team plan expired is refused saying so ("…has no seat on it yet — ask the team's admin…"), and an account with none of them names both plans and the site's subscribe page. ZCode's own team login is read from its credentials and setting.json (zcodeOwnTeam), its key minted lazily and kept (zcodeTeamKeyOf, retried after a minute, "sign in to ZCode again" when refused). Team requests go to the family's Anthropic base signed with the team key, no ZCode headers, never the Start Plan. Team usage asks bigmodel.cn (or api.z.ai) /api/monitor/usage/quota/limit?type=2 with the key and Authorization, Bigmodel-Organization, Bigmodel-Project, Set-Language, Accept-Language: CREDIT_LIMIT windows, unit 3 "5 hours" and unit 6 weekly, "current / total" when there is no remaining; /api/biz/customer-package-reset/list?targetType=TEAM gives the resets, fiveHourResets and weekResets counted where available is true, the soonest expireTime as until; the plan's end from querySubscribeDetail, Renew off. ResetCredits gains byWindow/fiveHour/weekly and Words(), "2 five-hour resets · 1 weekly reset" in the CLI, TUI and GUI (en and zh), with no "Use a reset" button for them since they are spent on the vendor's page. The Zhipu GLM key provider (planquota.go) falls back to the team query when the personal one fails or has no windows, adding the team headers when a ZCode team login holds that key. Personal accounts, Coding Plan and Start Plan, are routed, signed and metered as before. Tested with a new internal/provider/zcode_team_test.go against a fake bigmodel.cn/Z.ai in a sandbox HOME, no real tokens: a BigModel team sign-in through StartSignIn("zcode:bigmodel") is added (redirect param, plan "GLM Coding Team Pro", key made with keyType 2, org/project kept), its requests at the BigModel base signed with the team key and no ZCode headers, usage 5 hours 42% "420 / 1000" with a 5h span and weekly 10%, the quota headers sent, resets 2 five-hour and 1 weekly until 2026-10-05, plan until 2026-12-31; an unassigned seat is refused with no key made; ZCode's own team login from credentials + setting.json is found and metered while an individual plan stays as it was; the key provider's team fallback. TestPlanQuotas gains the type=2 case; the zcode Start Plan and account tests pass. A new internal/gui/tests/zcode-site.test.cjs, Chromium and WebKit, en and zh: starting ZCode's sign-in asks the site and posts nothing, BigModel posts {agent:"zcode",site:"bigmodel"} and keeps it, and the by-window resets read in both languages; signin-callback, click-scroll and login-import pass. go vet ./internal/..., go build -tags nogui (and GOOS=linux, GOOS=windows) and go test -tags nogui ./... pass (TestUserPath timed out under load and passes alone). Not tried against a real account: BigModel's OAuth sign-in and poll shape, the Set-Language values, customer-package-reset's field formats, querySubscribeDetail on Z.ai, type=2 for a pasted key with no org/project, and the time formats are from ZCode's code, not seen on the wire; not tried in the real Wails window. yetone 2026-09-30 03:53:33 +08:00
  • aa826a7644 warmup: hold weekly warm-ups that would start the 5-hour window before the daily time (#247) lainbo 2026-09-30 04:14:34 +08:00
  • d173bb0f79 serve: the banner no longer says the gateway only listens on localhost when MAGPIE_ADDR puts it on the network (the Docker image of #245 runs it on 0.0.0.0) — there it takes any key from anyone who reaches it until it is shared from Settings, and it now says so; once shared, it says other machines need the key under Settings → Share on local network. Tested with TestKeyNote (loopback, localhost, [::1], 0.0.0.0, :port, a LAN address, and shared). yetone 2026-09-30 03:22:36 +08:00
  • 6d69272732 agents: a model picked for Claude Code (or any agent) shows on its row at once, in the window and the tray panel, and /api/set answers in ~0.1s instead of seconds (Discord: switching Claude Code's model took 5–10s to show, magpie 0.1.410+) — /api/set writes the config and answers with the whole state, and the frontend drew the pick only from that answer. Every state asked the devin CLI (devin models list, ~1.5–3s a run) again several times whenever Devin was installed: its model's family, its options, its effort's options and Drift's re-read, and a CLI that gave no list (signed out, offline) was asked anew on every state, since only a list was cached. With a 4s CLI one set took 28s (measured, before). Now DevinFamilies keeps a failure for a minute, serves a list past its 5 minutes while it is read again in the background, and only a caller with nothing known waits — never one marked DevinNoWait, which Devin's field reads and options are (its Set still waits, so what is written is unchanged). Goose's detection read a binary's whole build info on every state (~150ms); the answer is kept while the file's size and time are the same. In the page, commit() draws the picked model and its flash before /api/set is sent, then draws the answer (a later pick's answer winning over an earlier one's); a refused pick puts the old model back and gives the reason. What is written to the agent's config is as before. Measured in a sandbox HOME with magpie web: /api/state 2.8–3.1s and /api/set 2.9s before, 0.1–0.5s after (state() ~110ms steady in-process); a provider whose endpoint never answers made no difference either way. Tested with internal/gui/set_fast_unix_test.go (devin CLI that sleeps 4s and a provider that never answers: three sets and the states after them each under 1.5s, ~5ms, settings.json as before; fails before the fix at 28s a set) and internal/gui/tests/model-pick.test.cjs (a set held 2.5s: the pick shown within 600ms in the window scrolled and in the tray panel's opened row and line, the page not moved, the answer keeping it, a refusal restoring the old model, en and zh, Chromium and WebKit; fails on main). click-scroll, panel-fold, menu-scroll, agy-launch, cli-update and old-webkit pass. go build -tags nogui (and GOOS=linux, GOOS=windows), go vet and go test -tags nogui ./... pass (TestClaudeAccountNamedAsItsLogin failed under load and passes alone). Not tried in the real Wails window or tray, nor with a real signed-in Devin CLI. yetone 2026-09-30 03:17:01 +08:00
  • 1a704bd6ba docker: a server image and a multi-arch build workflow (#245) 2026-09-30 03:21:19 +08:00
  • 28396deabd zed: a Zed subscription (Pro, Student, Business) signed in from magpie, several accounts, served through Zed's own cloud (yosocoli5 on X: 买了 Zed 订阅,可以加进供应商吗 — yetone: 当然可以!) — magpie had no way to use Zed's hosted models. Zed is now a subscription in the add sheet (Zed's own logo from zed-industries/zed, lobe-icons has none), signed in as the Zed editor signs in (new internal/zed, the protocol): an RSA key is made, the browser opens zed.dev/native_app_signin with its PKCS#1 public half, magpie's loopback port and a system id, and comes back with user_id and the access token encrypted to the key (OAEP SHA-256, PKCS#1 v1.5 also read), answered with a redirect to zed.dev's succeeded page. The account is asked of /client/users/me (signed "<uid> <token>" with x-zed-system-id), kept with its GitHub login, its default organization and plan; an organization with hosted models turned off is refused. Several accounts can be added and switched like the other subscriptions. Requests get an LLM token from /client/llm_tokens (kept in memory, minted again when Zed answers 401 or x-zed-expired-token/x-zed-outdated-token, once; a refused pair marks the account lapsed, "sign in again"). The model list is Zed's /models for the account (disabled and unknown providers left out; context, output, images and effort levels kept). The gateway asks cloud.zed.dev/completions in Zed's envelope {provider, model, provider_request}, the request written in the model's own provider's API as Zed writes it — Anthropic Messages without stream, OpenAI Responses, xAI chat completions with max_completion_tokens, Gemini's request with models/<id> — and reads the NDJSON reply's events with that API's decoder: a failed status is an error with its status (upstream_http_N, rate limit 429, overloaded 529, payment 402), a reply without stream_ended is an error, not a shorter answer, a 402 says the plan doesn't include hosted models or its allowance is used up. relayQoder's status-keeping relay is now relayStatus, shared by Qoder and Zed. Usage shows the plan and its period's end; Zed reports no usage counter, so no windows. Signing in says first, as Antigravity's does, that Zed serves these models to its own editor and may treat magpie's use as third-party use (en and zh); the import box now uses a subscription's own risk note too. Tested in Go against fake zed.dev/cloud.zed.dev servers, no real account: internal/zed TestSignInKey, TestParseModels, TestReadLines; provider TestZedSignIn (the whole sign-in through the loopback callback, what is kept, the account and its models, the token kept and renewed, the quota's plan and end, a refused pair lapsing) and TestZedFreePlanQuota; gateway TestServeZedProviders (each of the four providers, streaming and not), TestServeZedFailures (stale token retried once, 402, upstream 529 with retry_after, a failed 429, cut short → 502), TestZedRequestShapes. New zed-sub.test.cjs in Chromium and WebKit, en and zh: the Zed row with its plans and logo, the risk note in Zed's words with nothing asked, then Sign in anyway asking for zed. add-sheet, add-button, old-webkit, signin-callback and own-signin-remove pass. go vet, go build -tags nogui (and GOOS=linux, GOOS=windows) and go test -tags nogui ./... pass. Not tried with a real Zed account: the sign-in page's real callback, the LLM token and completions endpoints' acceptance of magpie's headers, and each provider's real event shapes are from Zed's source, not seen on the wire. yetone 2026-09-30 03:19:15 +08:00
  • 575e694ed7 settings: a Text size preference (文字大小) that makes everything in magpie's windows larger, 100/110/125/150%, with Ctrl/Cmd + = − 0 in the window and the tray panel, and Windows' text drawn in its own UI faces (Discord, Clarence Dan: the font size is hard on the eyes on Windows) — Settings → Preferences gains a Text size row under Language whose pick posts to a new POST /api/settings/text-size (saved as settings.TextSize, validated against 100/110/125/150, 0 read as 100, and kept by the Settings page's general save, which never sends it). The size is each webview's own zoom, as a browser zooms, not CSS zoom: CSS zoom leaves getBoundingClientRect and clientX in zoomed pixels while offsets, scrollTop and 100vh are not, and app.js, routing.js and library.js place menus, popovers and the panel's height by those measures; under the webview's zoom the page is laid out again in fewer, larger CSS pixels and every measure agrees. Windows and Linux use Wails' SetZoom (WebView2's zoom factor, WebKitGTK's zoom level; the Zoom window option at creation, so the pages open at the saved size), the Mac WKWebView.pageZoom through cgo (macOS 11+), since Wails' Mac zoom is magnification, which lays nothing out again. None is under 100% because Wails clamps SetZoom below 1 on Windows and Linux. The windows grow around the pages: the panel's width and the height its page asks for (still in CSS pixels) are multiplied by the zoom, capped at the screen's work area (textsize.go panelFrame), and resized at once when the size changes; the main window's minimum is 560×420 CSS pixels at the size, capped by the screen, so a window smaller than that grows. Runtime changes wait for the app to be ready (whenReady), and at startup the Mac applies the zoom once its webviews exist. The Mac header, drawn under the native traffic lights which do not grow, divides its height, left padding and the modal's top by --zoom (set from boot.js before the first paint and from the settings), so it stays 50 points tall and level with them. The shortcuts are Cmd on the Mac and Ctrl elsewhere (the other modifier does nothing), step through the sizes and 0 goes back to 100%, with a "Text size 125%" toast; in magpie web the row is hidden and the keys are left to the browser's own zoom. On Windows the page names its faces (html.win): Segoe UI Variable Text, Segoe UI, then Microsoft YaHei UI for Chinese rather than whatever WebView2 falls back to; the Mac and Linux stack is unchanged, and there is no automatic size bump. New strings in en and zh. Tested: internal/gui/tests/text-size.test.cjs in Chromium and WebKit, en and zh, light and dark — a pick posts and does not scroll the settings page, stays picked after a reload with --zoom from boot.js, the Settings page's other saves leave it, the shortcuts step and reset in the window and the panel, and at 150% (the smallest window, 840×630 points, as 560×420 CSS pixels at device scale 1.5) no view, the header or the panel runs off to the side, the Mac header is 50 points; screenshots at 100% and 150% looked at. currency.test.cjs's viewport is 480 tall instead of 420, as settings-groups' is: the new row put the Currency control under the footer, where WebKit's hover could not reach it. Go: TestTextSizeFrames (panel and window sizes) and TestTextSizeSetting (the endpoint saves, zooms the windows, rejects 133, survives the general save, boot.js carries it) in internal/gui, and the settings round trip. go build -tags nogui (and GOOS=linux, GOOS=windows, plain darwin), go test -tags nogui ./... (TestUserPath flaked once under load and passes alone) and the whole GUI suite with --test-concurrency=2 pass. Not tried in the real Wails window on any platform: the Mac pageZoom, WebView2's zoom factor with the resized panel and the Windows font are untested there. yetone 2026-09-30 02:49:24 +08:00
  • 2493bd72c5 pi: a model's thinkingLevelMap in models.json names every one of Pi's levels, the model's own mapped to its effort and the rest null, so Pi's /thinking offers only what the vendor takes (#243) — magpie wrote only xhigh and max (glm-5.3-flash at low/high/max got {"max":"max"}), and Pi offers every level a map leaves out but xhigh and max, so minimal and medium were offered and sent efforts the vendor turned away. Now glm-5.3-flash gets {"off":null,"minimal":null,"low":"low","medium":null,"high":"high","xhigh":null,"max":"max"}. Pi's off (read from pi-ai 0.99's getSupportedThinkingLevels and its openai-completions, openai-responses and anthropic-messages streams) maps to none for a model that takes none, so off really turns reasoning off; a model without none has off null, since Pi's off asks a Responses model for effort none and leaves a Chat model on the vendor's default; on Anthropic's Messages API off is left out, as before, since there Pi's off sends thinking disabled, which Claude takes, and Claude keeps its xhigh and max. A model whose levels magpie doesn't know still gets no map and reasoning false. Tested with TestPiThinkingLevels (full maps with and without none, Anthropic keeping off, none for unknown) and TestOpenCodeNoResellerLevelsForSwitchModel, which now checks the full map written for a relay's low/high/max model and no map for an unknown one (fails on main with map[max:max]). go vet ./internal/agent, go build -tags nogui (and GOOS=linux), GOOS=windows go build and go test -tags nogui ./... pass (TestUserPath, TestClaudeSignedOut, TestClaudeAccountNamedAsItsLogin, TestClaudeIdentityServedWhileAsked, TestCodexVersion and TestAddSecondOfPreset failed in the full run under load and pass alone). Not tried in a real Pi session. yetone 2026-09-30 02:41:20 +08:00
  • cb65895ac3 agents: Factory's Droid CLI is an agent magpie sets up (#242 tasselx) — droid keeps its settings in ~/.factory/settings.json ($FACTORY_HOME_OVERRIDE/.factory when set), its BYOK models the customModels array ({model, displayName, baseUrl, apiKey, provider "anthropic" | "openai" (Responses) | "generic-chat-completion-api", maxOutputTokens, noImageSupport …}), picked as "custom:<id>". Picking a catalog model through magpie appends one entry per catalog model after the user's own (so their ids never move), each with an explicit id "custom:magpie/<provider>/<model>", the catalog's label as displayName, the gateway and token magpie, its context (maxContextLimit) and output when known, noImageSupport always written (droid takes a Chat Completions model as blind otherwise), and the API its provider speaks natively so the gateway relays it as it is: Responses → "openai" at /v1, Messages → "anthropic" at the gateway root (Anthropic's SDK adds /v1), anything else "generic-chat-completion-api" at /v1; and sets sessionDefaultSettings.model. The model shows in magpie as magpie/<ref>; the user's sessionDefaultSettings.model is stashed and put back on reset (a top-level "model", the docs' spelling, is left alone and is what droid falls back to), magpie's entries go and an emptied customModels or sessionDefaultSettings with them; everything else in the file, the user's entries included, is kept byte for byte. The picker offers the user's own custom models by the id droid gives them (its "id", else the displayName with whitespace as dashes and a count of the same name before it), including the legacy ~/.factory/config.json custom_models, which magpie never writes. Sync rewrites magpie's entries as providers change; Check reports a missing entry or a changed baseUrl/apiKey; its requests are Droid's in usage (User-Agent factory-cli/<version>). Icon is Factory's own favicon glyph (factory.ai/favicon.svg); lobehub has none. From the docs: docs.factory.ai/cli/byok/overview (customModels fields, the three providers, config.json legacy snake_case merged under settings.json), /cli/configuration/settings (model, sessionDefaultSettings.*), /droid-exec/overview (custom:<Display-Name>-<index>). Verified by reading the shipped droid 0.229.0 bundle (@factory/cli-linux-x64, unpacked, never installed or run): the zod schema of an entry (also id, index, maxContextLimit, enableThinking, reasoningEffort, bedrock-converse), ids as custom:${displayName.trim().replace(/\s+/g,"-")}-${n} with n counting equal names, an entry's own id winning, the lookup by id, top-level model/reasoningEffort/specModeModel moved into sessionDefaultSettings only when unset there, getModel reading sessionDefaultSettings.model, noImageSupport defaulting by provider, FACTORY_HOME_OVERRIDE, and BYOK requests carrying factory-cli/<version>. Tested in a sandbox HOME (internal/agent/droid_test.go): set/sync/reset on a settings.json as droid leaves it with a Groq BYOK model (its entry byte-identical, the file restored exactly), each protocol's provider and baseUrl, a moved baseUrl caught by Check and healed by Sync, picking the user's own model while on magpie's, no file at all, a top-level model, FACTORY_HOME_OVERRIDE and legacy config.json ids (custom:Local-Qwen-0/-1) offered and never written. go build -tags nogui (and GOOS=linux, GOOS=windows) and go test -tags nogui ./... pass (TestClaudeSignedOut and TestClaudeAccountNamedAsItsLogin flaked under load and pass alone). Not tried with a real droid: whether droid's /model lists the entries, that it reaches the gateway on each protocol, and how it sends thinking for a custom model (magpie sets no reasoningEffort/enableThinking) are unverified. yetone 2026-09-30 02:31:12 +08:00
  • 2693cd8828 feat(gui): choose macOS session terminal (#241) Karenandyetone 2026-09-30 02:36:12 +08:00
  • 2364f8b5ff providers: the add sheet's rows carry no region tag, the logos stay put when a dialog opens or closes, and "Add provider" sits at the view's foot and reaches the sheet on the first click (Image #27: 国际中国还是有点像狗皮膏药; 每次打开或者关闭弹窗的时候,Provider的logo都会重新刷新一遍; Image #28: 添加供应商要固定在底栏底部…第一次点击的时候没有任何事情发生) — a vendor's merged global/China row lost its "Global · China" tag (design B1): the region is picked in the editor's Region switch and the row's title still lists each region's host. Every redraw of the Providers page made each logo anew, so each one faded in again whenever a dialog opened or closed; renderProviders now hands the logos it drew to icon() to reuse (the editor drawing its own), so they are moved, not remade. "Add provider" is sticky at the view's foot over a long list and steps aside while the sheet's head is in sight; the first click opened the sheet but WebKit clamped the scroll before it grew, and unrollSheet took that for the reader scrolling and gave up — it now starts from where the view really is, and a click with the sheet open eases the view to it. Tested with add-button.test.cjs (new: the bar at the foot at the top and the end, one click reaching the sheet, the bar away and back, a dialog opened and closed keeping every logo; fails on main) and add-sheet.test.cjs (no tag, the hosts in the title), Chromium and WebKit, en and zh; click-scroll passes; the full GUI suite passes but login-import, which fails on main under load. go vet, go build -tags nogui (and GOOS=linux, GOOS=windows) and go test -tags nogui ./... pass. yetone 2026-09-30 02:18:29 +08:00
  • c35471cf5b provider: Azure OpenAI, asked at the user's own resource on its v1 API with the key in api-key, for Codex, Claude Code and chat clients alike, and brought in from other apps instead of left out — Azure OpenAI had no preset, and importing an Azure entry from Alma was skipped with "magpie doesn't support Azure OpenAI yet". A new Azure OpenAI preset (vendor) has no URL of its own: its editor asks for the resource's endpoint in a field above the key, focused first, with https://<resource>.openai.azure.com as the placeholder and a hint on where the portal shows it and that the model ids are the deployments' names (en and zh); adding without one says "Your resource's endpoint is needed" and sends nothing, and Save refuses one without an endpoint too. Whatever is pasted — the bare endpoint, …/openai, …/openai/v1/, a classic deployment URL with its api-version, a *.cognitiveservices.azure.com or *.services.ai.azure.com host, or only the resource's name — is saved as https://<host>/openai/v1 for both chat completions and Responses. magpie uses the v1 GA API rather than the classic /openai/deployments/<name>/…?api-version= paths: the paths match OpenAI's, the body's model is the deployment's name, and no api-version is needed (the reasoning is in internal/provider/azure.go). The key goes in api-key, never as a Bearer, which Azure takes as an Entra ID token; nothing sends Authorization or x-api-key there. Deployments named after GPT/o/codex models go to Responses first, as OpenAI's do, and others to chat completions. Chat bodies ask max_completion_tokens instead of max_tokens and drop thinking/enable_thinking, both in passthrough and in Claude Code's translated requests, and the Test probe asks max_completion_tokens as well. The model list is the resource's deployments (/openai/deployments?api-version=2022-12-01), falling back to /openai/v1/models, with the key in api-key; when neither answers, the error says to type the deployment names in as model ids. Before a list, no catalog models are offered, since a model the resource hasn't deployed would be turned away. An Alma azure entry, and any imported entry at a resource's host, becomes the preset at its v1 base; an Alma azure entry with no endpoint is skipped as "it has no Azure OpenAI endpoint", and one at a non-Azure host (an API Management gateway) keeps its URL and is still signed with api-key. Adds the lobe-icons Azure icon. Tested in Go against fake resources (httptest): TestAzureRoutes checks that Codex /v1/responses, a chat client's /v1/chat/completions and Claude Code's /v1/messages on both a GPT-named and another deployment reach /openai/v1/responses or /openai/v1/chat/completions with api-key set, no Authorization or x-api-key, the deployment name as the model, max_completion_tokens and no thinking; TestAzureBase covers each pasted form and the hosts that aren't resources; TestAzurePresetSaved covers the endpoint required, both URLs on the v1 base, api-key-only headers, Native, the probe's URL and body, and nothing available before a list; TestAzureModels covers deployments first, the v1 fallback and a refused key's message; TestImportAzure covers Alma rows with an endpoint, with a bare resource name and with none, plus a classic URL from another app. azure-endpoint.test.cjs, in Chromium and WebKit, en and zh, checks the field, placeholder, hint and focus, the warning without an endpoint with nothing sent and the page not scrolled, the endpoint sent as chat and responses with the key, and that OpenAI's editor has no such field. add-sheet and old-webkit pass. go build -tags nogui (and GOOS=linux, GOOS=windows), go vet and go test -tags nogui ./... pass (TestClaudeAccountNamedAsItsLogin failed under load and passes alone). Not tried against a real Azure resource: whether a key still lists deployments with api-version 2022-12-01, whether Azure's Responses API accepts every field Codex sends, Claude models on Azure AI Foundry's Anthropic endpoint (not wired), Entra ID tokens, and API Management custom domains beyond keeping their URL. Not tried in the real Wails window. yetone 2026-09-30 02:16:23 +08:00