yetone
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.
2026-09-30 10:17:15 +08:00
yetone
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.
2026-09-30 10:14:38 +08:00
yetone
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.
2026-09-30 10:12:28 +08:00
yetone
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.
2026-09-30 10:09:29 +08:00
yetone
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.
2026-09-30 10:06:15 +08:00
yetone
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.
2026-09-30 10:02:51 +08:00
yetone
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.
2026-09-30 09:36:27 +08:00
yetone
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.
2026-09-30 09:34:21 +08:00
yetone
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.
2026-09-30 09:31:59 +08:00
yetone
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.
2026-09-30 09:29:28 +08:00
yetone
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.
2026-09-30 09:23:22 +08:00
yetone
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).
2026-09-30 08:37:57 +08:00
yetone
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.
2026-09-30 07:43:58 +08:00
yetone
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.
2026-09-30 07:31:05 +08:00
yetone
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).
2026-09-30 05:49:19 +08:00
yetone
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.
2026-09-30 05:32:04 +08:00
yetone
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.
2026-09-30 05:07:47 +08:00
yetone
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).
2026-09-30 04:58:11 +08:00
yetone
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.
2026-09-30 04:52:03 +08:00
yetone
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.
2026-09-30 04:28:01 +08:00
yetone
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
2026-09-30 04:15:07 +08:00
yetone
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.
2026-09-30 04:15:06 +08:00
lainbo
aa826a7644
warmup: hold weekly warm-ups that would start the 5-hour window before the daily time ( #247 )
...
With a daily warm-up time set, a 5-hour window that would still be running
at that time is held on its reset. A weekly warm-up in that gap still went
out, and its request started the 5-hour window too, so the daily warm-up
was skipped, every week on that weekday. The weekly warm-ups now wait for
the daily time in that case, and one request starts both windows.
Fixes #246
2026-09-30 04:14:34 +08:00
yetone
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).
2026-09-30 03:22:36 +08:00
yetone
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.
2026-09-30 03:21:24 +08:00
1a704bd6ba
docker: a server image and a multi-arch build workflow ( #245 )
...
* docker: a server image and a multi-arch build workflow
The image is the terminal-only binary built with -tags nogui on
distroless static, run as nonroot with magpie's files on a /config
volume, created and chowned at build time so a fresh named volume
is writable by uid 65532.
The workflow builds amd64 and arm64 on native runners, smoke-tests
each image before anything is pushed (ready probe, unknown-model
4xx JSON, /config write), pushes by digest, merges a multi-arch
manifest (semver trio plus latest on v* tags, edge otherwise) and
attests build provenance. PR runs build and smoke-test only.
The README documents running the image, the web UI mode, and why
subscription sign-ins must start where a browser shares loopback
with magpie.
Co-Authored-By: glm-5.3-flash <hakase@ooooo.space >
Generated-By: omp 18.4.3
* docker: README publishes the ports on the host's loopback and says how to require a key — until the gateway is shared from Settings it takes any key from anyone who reaches it, and Docker's -p 3425:3425 puts it on every interface of the host past its firewall. Checked in the built image: with settings lan+lanKey a request from the host through the published port without the key gets 401 and with it 200; without them any key gets 200.
---------
Co-authored-by: glm-5.3-flash <hakase@ooooo.space >
Co-authored-by: yetone <yetoneful@gmail.com >
2026-09-30 03:21:19 +08:00
yetone
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.
2026-09-30 03:19:19 +08:00
yetone
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.
2026-09-30 02:51:17 +08:00
yetone
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.
2026-09-30 02:48:10 +08:00
yetone
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.
2026-09-30 02:35:59 +08:00
Karen and yetone
2693cd8828
feat(gui): choose macOS session terminal ( #241 )
...
* feat(gui): choose macOS session terminal
* session terminal: build the darwin CLI without cgo, drop the dead macOS 11 path, and open Terminal when the .command default is not a terminal
- terminal_apps_darwin.go imports "C" but had no cgo tag, and terminal_apps_other.go was !darwin only, so `CGO_ENABLED=0 GOOS=darwin go build -tags nogui .` (make release-cli's darwin targets) failed with undefined discoverTerminals. The cgo file and its test are now darwin && cgo; the stub is !darwin || !cgo.
- magpie needs macOS 12 (LSMinimumSystemVersion, -mmacosx-version-min=12.0), so the macOS 11 branch (temp .command file, deprecated LSCopyApplicationURLsForURL, CFRelease) never ran; it is gone along with the CoreServices link.
- With no saved choice, a default .command handler that is not a listed terminal, or a failed lookup, now opens Terminal as before the PR instead of failing; Settings labels the system option "System default (Terminal)" then, and the unused "System default (unavailable)" string is dropped.
- session-terminal.test.cjs covers an editor as the default in Chromium and WebKit, en and zh, and is on the README's node --test line.
---------
Co-authored-by: yetone <yetoneful@gmail.com >
2026-09-30 02:36:12 +08:00
yetone
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.
2026-09-30 02:26:28 +08:00
yetone
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.
2026-09-30 02:17:15 +08:00
yetone
46c56021a4
usage: DimAgent's quota is asked through its own provider's proxy, like every other subscription's (per-provider proxy landed beside #226 )
2026-09-30 02:15:51 +08:00
yetone
125da38943
providers: each provider, a signed-in account like Codex's among them, goes through its own proxy — the one in Settings, none, or its own address ( #237 xosi: 目前似乎是全局设置代理的,codex和国模供应商无法单独区分设置代理) — magpie had one proxy, Settings' (or the environment's, or the system's), for every request it made, so Codex, which needs a proxy from China, and a vendor at home, which is slower or refused through one, couldn't both be right. A provider now has a proxy, "" (following the global one, as before and the default), "direct" (none, whatever Settings says) or an http://, https:// or socks5:// address (host:port meaning http), kept as proxy in providers.json only when set, and for an account provider in its picks entry beside its models. It is checked on save by the same rule Settings' proxy is (settings.CheckProxy, now shared), so ftp:// and the like are refused. netproxy.With puts a provider's choice on a request's context and netproxy.Func honours it; netproxy.Dispatch sends such a request through a clone of the transport kept for that proxy alone (at most 32, cleared past that), since a transport shares an HTTP/2 connection by host whatever proxy dialled it; loopback is never proxied. The gateway's client and http.DefaultClient go through Dispatch, and every request made on a provider's behalf carries its choice: the gateway's turns (attempt, forward, draw, decide, the Codex backend), and the model lists, balances, plan and subscription usage, tests, sign-in, Codex's warm-up and reset, while the Claude subscription's and warm-up's CLI runs get the account's proxy in their *_PROXY (netproxy.EnvWith). The provider editor, a key provider's and a signed-in account's alike, has a Proxy row after Fallback: Global proxy | Direct | Custom (全局代理 | 直连 | 自定义), the address field beside the options only for Custom, its room kept otherwise so a pick never changes the centred dialog's height and moves nothing, and a line under it saying what the pick does; Save posts proxy ("" , "direct" or the trimmed address), a save without it keeps the old one, and Custom with no address is refused before anything is posted ("Proxy: type its address, like http://127.0.0.1:7890 "). No border stripes; en and zh strings together. Tested with a new internal/gateway/proxy_test.go in a sandbox HOME: with a global proxy set, provider A's turns and model list go through its own fake proxy, B's (direct) straight to the vendor, C's (following) through the global one, none through another, and A set back to following goes through the global one; ftp:// is refused (fails with With neutered). netproxy_test.go's TestWith covers Func, Dispatch's per-proxy transports and EnvWith. A new internal/gui/tests/provider-proxy.test.cjs in Chromium and WebKit, en and zh: a key provider opens on Global proxy with no address, Custom unfilled is refused and nothing posted, a padded socks5 address saved trimmed, Direct saved as "direct" and Global as ""; Codex's account opens on Custom with its address and saves Direct with its models; every pick leaves the option within 1px; no left border. go build -tags nogui (and GOOS=linux, GOOS=windows), go test -tags nogui ./..., provider-proxy, click-scroll, old-webkit, balance-fix and add-sheet pass (click-scroll's WebKit case failed once alongside the others and passes alone, twice). Not tried against a real proxy or the real Wails window.
2026-09-30 02:15:50 +08:00
yetone
9992678c64
zcode: an account with no GLM Coding Plan uses ZCode's Start Plan (体验套餐) where ZCode does, with ZCode's own token, and shows its allowance ( #236 tasselx: ZCode 3.14.4, magpie 0.1.410, a trial-only account, the free quota not read and every request refused with rate_limit_error "ZCode: [1113][Insufficient balance or no resource package. Please recharge.]"; #225 xiaozhu1337: ZCode ships a built-in trial package) — magpie sent every ZCode account to the Coding Plan's endpoint, api.z.ai/api/anthropic, with the account's zcode-api-key, and read its allowance only from /api/monitor/usage/quota/limit. That key is billed to a Coding Plan, or else to the account's API balance, so an account with only the Start Plan got 1113. ZCode itself (ZCode.app 3.14.3's zcode-builtin.json providerRules and host/index.js: resolveCurrent, buildZaiStartPlanBalanceUrl, hasActiveStartPlan, isZaiStartPlanIdentity, normalizeStartPlanExpiry, normalizeZaiStartPlanBalanceLimits, pollPendingOAuth) serves the Start Plan (account:zai-start-plan and account:bigmodel-start-plan) from zcode.z.ai/api/v1/zcode-plan/anthropic, keyed by its own session token (the credential store's zcodejwttoken, the token its sign-in's poll hands back, which magpie dropped), and reads what is left from /api/v1/zcode-plan/billing/balance?app_version= with that token. Now, in a new zcode_start.go: an account keeps ZCode's token beside its key (zcodeKey.JWT; zcodeOwn reads zcodejwttoken, and user_info from oauth:bigmodel:user_info when there is no Z.ai one; an account with the token and no coding plan key is an account). Whether it is on the Start Plan is asked of /api/biz/subscription/list (a Coding Plan: as before; none: the Start Plan; can't tell: the Start Plan if its balance has an active one), kept 10 minutes (1 when unsure); with no token it is the Coding Plan as before, asking nothing, and with no key it is the Start Plan. On the Start Plan a request is moved to zcode-plan/anthropic, signed with the token (x-api-key and Bearer) and ZCode's own headers (User-Agent ZCode/<ver>, X-ZCode-App-Version, X-Title, HTTP-Referer, X-Platform), refused with "sign in to ZCode again" once the token has run out; its models are the Start Plan's (zcodePlanID account:zai-start-plan for ZCode's config, zcodeModels less GLM-5.3 before it is read). Its allowance is the balance as ZCode reads it: a plan still "active" past ends_at (by server_time) is expired and the buckets of expired plans left out; the plan's name (Start Plan) with Until its end and Renew off; a window per bucket, named by its show_name (or its models, or Credits), Used and "used / total" from total/used/remaining units (numbers or strings), ResetsAt its expires_at, Span from its entitlement's period (daily/weekly/monthly) or period_start..end, and matched to the bucket's models. Signing in from magpie no longer refuses an account with no Coding Plan: with an active Start Plan it is added on that (its key kept too, so a Coding Plan bought later is used), and refused only when the Start Plan is over as well ("this Z.ai account has no GLM Coding Plan, and ZCode's Start Plan has ended or was never started — subscribe at z.ai/subscribe, then add it again"). Accounts with a Coding Plan are routed, signed and metered as before. The provider keeps the name ZCode ( #225 asked for "GLM Coding Plan"; ZCode names these plans "Z.AI Individual Coding Plan" and "Start Plan", and the plan line already shows "GLM Coding <Level>"). Tested with a new internal/provider/zcode_start_test.go against a fake Z.ai/zcode.z.ai in a sandbox HOME, no real tokens: ZCode signed in with only its token (BigModel user_info) is an account, its provider served at zcode-plan/anthropic, a request signed with the token and ZCode's headers, the Start Plan's models and plan id, its allowance (Start Plan, Until, Renew off, GLM-5.1's window 25%, "250000 / 1000000", reset, a day's span, matching GLM-5.1 only); an expired plan and one active past its end give none; with a key and the token, no Coding Plan moves the request (query kept) to the Start Plan and it reaches it with the token, a Coding Plan keeps it on api.z.ai with the key and no ZCode headers, no token asks nothing, an expired token is refused; a sign-in with only the Start Plan is added on it, with a Coding Plan on that plan, with neither refused naming both. TestZCodeAccounts passes unchanged. go build -tags nogui (and GOOS=linux, GOOS=windows), go vet and go test -tags nogui ./... pass (TestUpdateCLI, TestClaudeSignedOut, TestClaudeAccountNamedAsItsLogin and TestUserPath timed out once each under load beside other builds and pass alone). Not tried with a real Start Plan account: the endpoint's acceptance of the token as x-api-key/Bearer and whether it needs ZCode's headers, the balance's real field values and period strings, what subscription/list returns for a trial-only account, and ZCode 3.14.4 (read from 3.14.3) are from ZCode's code, not seen on the wire.
2026-09-30 02:15:50 +08:00
yetone
12d9195a2c
usage: a currency of ¥ CNY chosen before a restart is the one costs show in, not $ ( #212 follow-up, L1cardo: after a restart the setting still showed RMB but the costs were back in USD) — the Go side sends the exchange rate the cny choice converts at in two places: inside /api/settings' answer (settingsJSON.fx) and, in /api/state, beside the settings rather than in them (stateJSON.FX, filled only when cny is chosen). At start the page runs applyPrefs(state.settings), which set the currency to cny from the settings but looked for the rate only as s.fx, inside them, where /api/state never puts it; fx.rate stayed 0, and fmtCost converts only when the rate is above 0, so every cost (the Usage page, the requests ledger, sessions) was drawn in $. Picking ¥ CNY in Settings worked, because the save's answer is /api/settings' and carries fx, and the Settings row reads the stored setting, so after a restart the two disagreed. Nothing was wrong with what was stored: settings.json keeps currency, and internal/fx reads its cached rate from disk; the TUI and CLI call fx.Get directly and were never affected. Now load() passes state.fx to applyPrefs, which takes the rate from there or from the settings' own fx, ignores a rate of 0 (what /api/state sends for usd, which never looks one up), and redraws the costs when the rate arrives or changes while cny is chosen, not only when the currency changes. Tested with a new case in internal/gui/tests/currency.test.cjs, in Chromium and WebKit, en and zh: /api/state faked as the Go side sends it after a restart (settings with currency cny, fx beside them), the Usage page opened straight away shows ≈¥88.85 (12.34 at 7.2), Settings shows ¥ CNY picked, and Usage still shows ¥ after it; without the fix it shows ≈$12.34 in all four. currency, old-webkit, usage-ledger, click-scroll and settings-groups tests pass (58); go build -tags nogui (and GOOS=linux, GOOS=windows) and go test -tags nogui ./... pass in a sandbox HOME. Not tried in the real Wails window.
2026-09-30 02:15:49 +08:00
yetone
30431e0450
usage: a Grok id named at a reasoning effort is priced as its model ( #224 Aimer779: calls made with Grok Build show no cost on the Usage page) — the Usage page, the Requests ledger and the Sessions page price a model by its models.dev id, and xAI lists grok-4.7 and grok-4.6 ($2 / $6 / $0.5 cache read per million in the cached catalog) but not the ids Grok's models are spelled at an effort: grok-4.7-low, -medium, -high, -xhigh (Cursor's ids, and grok-4.7 at the levels Grok's own model list gives it). Such an id is the same model at another setting, so when its own id has no price it is now looked up as the model it is an effort of (provider.pricedNames: grok-<version>[-…]-minimal|low|medium|high|xhigh|extra-high, with or without a path, any case), in Provider.ListPrice and MakerPrice (the gateway's ledger and summary) and in the Sessions page's priceOf (through provider.PricedName). An id priced by its own name keeps that price; what is recorded is unchanged. Only Grok's ids are read this way, and -max is not stripped. Left unpriced, as no catalog in use lists them and no price is made up: fast ids (grok-4.7-fast, grok-4.7-low-fast, … which cursor_models.go treats as models of their own), grok-4.7-build and grok-4.7-build-fast (Grok Build's own models, from its model list and sessions) and grok-4.7-mini; they keep the + as before. Tested with a new TestGrokEffortListPrice (MakerPrice and the Grok account's ListPrice price grok-4.7 at each effort, in upper case and with x-ai/, and grok-4.6-minimal, at grok's price; the fast, build, mini and -max ids, gpt-6-astra-high and cursor-grok-4.7-high stay unpriced) and TestPriceOfGrokEffort in internal/sessions (the same for a session's models, grok-4.7-build, grok-4.7-mini and the fast ones unpriced); TestSubscriptionListPrice unchanged. go build -tags nogui (and GOOS=linux, GOOS=windows), go test -tags nogui ./... pass (TestUserPath, TestClaudeSignedOut, TestClaudeAccountNamedAsItsLogin, TestClaudeIdentityServedWhileAsked and TestCodexVersion hit their 5s waits in the full run and pass alone). Not tried against the reporter's usage log or in the GUI.
2026-09-30 02:15:49 +08:00
yetone
4455c7f998
gui: the Windows tray panel no longer shows a faint close X over its Settings gear — its caption buttons are hidden there ( #238 mintonight: a blurry red X at the panel's top-right corner, just above and right of the gear) — Wails v3 makes a frameless window on Windows WS_OVERLAPPEDWINDOW and, for the shadow and round corners, extends DWM's frame into the whole client area; the panel left its close button at the zero value, ButtonEnabled, which sets WS_SYSMENU, so DWM drew the caption's close X into that extended frame at the window's top right, and with the panel translucent (BackgroundTypeTranslucent, WS_EX_NOREDIRECTIONBITMAP, a see-through webview) it showed through the page, faint, over the gear. It was not the page: #winclose is removed everywhere but Linux's window, and is a grey pill in the header row. The panel's options move into panelOptions(goos, theme), unchanged, and on Windows its close, minimise and maximise buttons are ButtonHidden, which takes WS_SYSMENU and the min/max boxes off, so DWM has no caption buttons to draw; DisableFramelessWindowDecorations stays off, so the shadow and Windows 11's round corners stay. The Mac and Linux get the same options as before (their button states left at the zero value), and the main window is not touched. Tested with a new internal/gui/panel_options_test.go (!nogui): on windows the three buttons are hidden and the panel still frameless, translucent, off the taskbar and decorated; on darwin and linux the buttons are as before and URL, backdrop and corner radius unchanged; run on the Mac and compiled for Windows (GOOS=windows go test -c). go build -tags nogui (and GOOS=linux), GOOS=windows go build and go test -tags nogui ./... pass (TestUserPath, TestClaudeSignedOut and TestClaudeAccountNamedAsItsLogin timed out once at a load average near 60 and pass alone). Not tried on a real Windows desktop: that DWM stops drawing the X without WS_SYSMENU is from the Win32 custom-frame behaviour and Wails' code, not seen.
2026-09-30 02:15:48 +08:00
yetone
5353ed54fe
gateway: a vendor refusing the client outright (WorkBuddy's 400 "Illegal API invocation from an unapproved channel") hands a group's request to its next member (PR #232 Y-C-Fan: a routing group stopped at a WorkBuddy member refusing Claude Code's chats) — retryable let a 400 fall over only on quota or unserved-model words, so this refusal, which is of the provider and not the request, went back to the agent. A new refusedWords (unapproved channel, illegal api invocation) joins them. The request body is not changed: a chat opening with Claude Code's own system prompt is still sent as it came. Tested with a new case in TestGroupFailsOverAnUnservedModel's retryable table (false before) and the gateway package.
2026-09-30 02:15:47 +08:00
yetone
ac1f522154
library: skills in the user-wide ~/.agents/skills are found, one row with the agents that link or junction to them, and brought in by a link that leaves the shared folder as it is ( #227 Aimer779: the Library's "In your agents" never looked in ~/.agents/skills, the shared folder many agents read and where the user keeps skills, many as symlinks to D:\aimer-skills; a skill junctioned from there into Pi and ZCode showed as Pi's with "differs in ZCode") — foundSkills walked only each target's own skills folder, and realDir used filepath.EvalSymlinks, which since Go 1.23 leaves a Windows junction (a mount point, reported as irregular, not a symlink) as it is, so two junctions to one folder were two skills, and the Link check (ModeSymlink) took a junction for a folder of its own, one ImportSkill would move. Now foundSkills reads ~/.agents/skills first (hidden entries, ones without a SKILL.md, ones the library has or linked, skipped as for the agents' folders), a skill there carrying shared (its entry) and the agents whose entry resolves to the very same folder; a byte copy in an agent's folder is still another ("differs in"), as copies were. realDir follows links a part at a time itself (symlinks, and on Windows junctions through os.Readlink, relative targets, links to links, a loop or a missing part giving the path back), and a new linked() (symlink, or junction on Windows) replaces the ModeSymlink checks in foundSkills and ImportSkill. ImportSkill links the library to a skill in the shared folder (Source folder), real folder or link alike, and never moves it; the agents' links to it are replaced by the library's as before; an agent's own folder still moves in. No target writes to ~/.agents/skills: targets.go gives no agent a user-wide ~/.agents/skills (only projects.go's project .agents/skills), so it stays a place skills are found in. The page: a found skill in the shared folder says "shared in ~/.agents/skills/<name>" (共享于) above any "linked from", its Bring in title says it stays where it is and can be given to any agent; the shared path is revealable. Tested with TestSharedAgentsSkills (sandbox HOME/USERPROFILE: a symlink in the shared folder to a folder elsewhere, real folders there, Pi and Codex links to one of them, absolute and relative, Gemini's link to the shared link, a Codex copy, a folder with no SKILL.md and a hidden one; one row each with the right agents, shared, link and others; importing all three leaves the shared folder and the folder elsewhere as they were, the library's entries links to them, the agents' links now the library's, Codex's copy untouched, nothing found after), TestRealDir (absolute, relative, a linked parent, .., a loop, a missing path, linked, within) and TestJunction (mklink /J, Windows only, skipped here), and a new internal/gui/tests/shared-skills.test.cjs in Chromium and WebKit, en and zh. go build -tags nogui (and GOOS=linux, GOOS=windows), go vet for windows and go test -tags nogui ./... pass (TestClaudeSignedOut flaked once, passes alone); the GUI suite passes but for login-import (passes alone) and panel-profiles in WebKit, which fails on main the same under this load. Not tried on real Windows, with real junctions, or in the real window.
2026-09-30 02:15:47 +08:00
yetone
6df6de1010
sessions: the indexing show no longer comes back on every visit — the index is written after the read, in the background, and not counted as indexing (Image #15 : 这个还是会出现无数次)
...
Each visit re-reads the few session files agents appended to since the last
read, which takes milliseconds. But refresh() then saved the whole ~20 MB
~/.cache/magpie/sessions.json before clearing the indexing flag, and that
save took over a second, so /api/sessions/progress said "indexing" past the
0.7s threshold and the hero played every time. The flag now drops as soon as
the parse ends; the index is written from a snapshot (parses are never
changed in place, so a shallow clone is safe) by one background writer that
coalesces saves. Reset and the CLI's exit wait for the write.
Tested: TestSaveBehindTheRead holds the save open and requires List to
return and Indexing to be off meanwhile, then the file to be written (fails
with the synchronous save). Against a copy of the real index (196 sessions)
the flag stayed on at most 22ms per visit. go test -race ./internal/sessions,
go test -tags nogui ./... (TestAddSecondOfPreset flaky, passes alone), go build
-tags nogui (and GOOS=linux, GOOS=windows), go build ./..., and the GUI tests
sessions-overview, old-webkit, menu-scroll, click-scroll pass. Not tried in the
real Wails window.
2026-09-30 02:07:50 +08:00
yetone
b713d5cd99
gateway: Gemini behind a proxy on this machine or the LAN is asked for its thoughts too, and a proxy that turns the fields away is asked as before (X @saoyan25: 本地反代的是谷歌AI studio的provider … agent客户端没看到思考链的内容,请求响应体也没看到reasoning_content) — v0.1.413 asked for Gemini's thoughts only when the provider's host was generativelanguage.googleapis.com, and Ben's AI Studio goes through a reverse proxy on his machine, so nothing changed for him. Now a request translated to a Chat upstream is taken for Gemini's OpenAI-compatible API (Request.GeminiCompat, set in forwardTranslated) when the host is AI Studio's, or when the model's id names Gemini and the host is localhost, a .local name, or a loopback or private address, a proxy there being most often one in front of Google; a relay elsewhere (OpenRouter, aihubmix …) and other local models are asked as before. As such a proxy may not pass thinking_config on, a 400 naming extra_body, thinking or include_thoughts marks the provider unfit for it (as the cache key is) and the request is sent again at once with reasoning_effort, as before v0.1.413, and so are its next ones — which also guards AI Studio itself, whose taking of the fields hasn't been tried live. Tested with TestGeminiCompat (AI Studio, 127.0.0.1, localhost, 192.168.x, [::1], .local, a local non-Gemini model, OpenRouter, aihubmix) and TestGeminiThoughtsThroughLocalProxy (Claude Desktop's request, thinking adaptive at effort high, to a fake proxy on 127.0.0.1: extra_body.google.thinking_config include_thoughts and thinking_level high with no reasoning_effort, the <thought> block reaching the client as a thinking_delta and no tags; a proxy answering 400 Unknown name "extra_body": asked again with reasoning_effort high, the reply whole, the next turn not sent the fields), which fail on main; TestAIStudioThinking now sets the flag. go vet, go build -tags nogui (and GOOS=linux, GOOS=windows) and go test -tags nogui ./... pass.
2026-09-30 01:58:30 +08:00
Allen Cao and heyallencao
db4d68894d
gateway: retry OpenRouter mandatory reasoning without disabling it ( #240 )
...
Co-authored-by: heyallencao <258265534+heyallencao@users.noreply.github.com >
2026-09-30 01:26:57 +08:00
yetone
9d4f420d27
sessions: the search sits at the right of Latest sessions, the list it filters, and the indexing show waits for a real index run instead of replaying on every catch-up read (Image #14 : 搜索会话的输入框不应该在这里吗; 会播放两次或甚至多次正在为你建立索引的那个百分比动画) — the search box moves from the filter bar into the Latest sessions heading's right, and it stays visible with an empty-state line when nothing matches, so the query can be cleared. Every reload or range change re-read the few session files agents had written to since the last read; each short read counted as indexing and replayed the percentage show. The show now appears only once indexing has run for 0.7s, and once it has shown it runs to the end. Tested in sessions-overview.test.cjs, Chromium and WebKit, en and zh: the search inside the heading at its right edge and centred on it, filtering, no-match and Escape; a 0.4s run shows no hero and a 2s one shows exactly one (fails with the delay at 0). menu-scroll's outside click now lands on the filter bar. menu-scroll, old-webkit, go build -tags nogui (and GOOS=linux, GOOS=windows) and go test -tags nogui ./... pass.
2026-09-30 01:15:58 +08:00
yetone
317bc70875
sessions: one index run a page shows once, a spinner, a session-shape chart that reads, and every session counted in it (Image #9–#13: 排版不对…土土的; 这个原点的动画可以换成别的吗; 这个图我着实没看懂; 出现之后又消失又出现…跑完之后突然又出现; 为什么只有5个会话)
...
- The index hero flickered and ran again: the sessions list, the stats and a
range change each started their own refresh of just the files they wanted,
and the page showed a fallback hero after 700 ms whether or not anything was
being read. List and stats now refresh every changed file in one run, so the
read after it has nothing to do and a wider range finds the files read; the
hero shows only while the server says it is indexing, and once it is done it
stays at 100% ("Indexed · putting the page together…") and never starts over.
- The hero: a dot field in the accent alone, a big percentage, a hairline bar;
a thin spinning ring in place of the pulsing dot, still when done or for
reduced motion.
- Session shape: bars stand on a baseline with no grey column behind them,
an empty bin is "0" over a stub, the busiest bin is in full accent (only
when one is busiest), "Autonomy" is "Tool calls" (工具调用), and a line says
what each axis is (横轴:… · 柱高:会话数).
- Only 5 sessions in the shape: parses kept by an early cacheVersion 4 had no
prompts or replies, and were never read again, so the chart skipped them.
cacheVersion 5 reads them again (on real data 385 of 462 sessions over 30
days, rather than 5). Sessions of agents that tell no messages (pi, grok…)
now count by their minutes at work.
Tested: TestShapesMinutesWithoutMessages; sessions-overview.test.cjs in Chromium
and WebKit, en and zh (the bars, zero stub, peak, both footers, the tool-calls
label); go test -tags nogui ./..., builds for darwin, linux, windows. Two GUI
files failing in the parallel run (click-scroll WebKit, login-import chromium
zh) pass alone.
2026-09-30 01:15:58 +08:00
Ethan Xu and yetone
2f00db65e6
feat: add DimAgent subscription support ( #226 )
...
* feat: add DimAgent subscription support
Add OAuth PKCE login with automatic and pasted callbacks, account management, model discovery, quota reporting and gateway integration. Safely rotate credentials during concurrent requests and preserve fresh tokens when updating model caches. Cover protocol, provider, GUI and callback behavior with regression tests.
* dimagent: a refresh hiccup inside the day's lead keeps the token that still runs
The fallback checked !dimagentNearExpiry, which is false whenever a refresh
runs at all, so a 5xx/timeout on /oauth/token failed every request for the
last 24h of a still-valid token. A 401/403 still lapses the account.
* dimagent: review tests (sign forwarding, parallel refresh once, hiccup in lead, LoginUsage)
* dimagent: signing in again takes the callback port from the sign-in it replaces
StartSignIn begins the new flow before it cancels the old one, so the new
flow's listen on 54321 failed (ignored) and the old listener then shut
down a second later: the browser's callback was refused and only pasting
could finish. The older DimAgent flow is now ended and its server closed
first, and the listen retried briefly while the port is let go.
* dimagent: the fake-upstream tests get a name of their own
---------
Co-authored-by: yetone <yetoneful@gmail.com >
2026-09-30 01:11:29 +08:00
yetone
27c90c8d88
panel: profiles open from Profiles at the Agents tab's foot, on its left, instead of a tab of their own (Image #65 : tray window 中方案不应该有一个单独的 tab,在 Agents tab 左下角有个方案即可) — the tray panel's tabs are Agents and Routing; the footer's left corner, on the Agents tab only, has Profiles (方案) with how many there are, and a click opens them upward over the list as a popover (the .pop look and fade, reduced motion kept): the chips, + Save current and its name field as before, the popover scrolling within itself when there are many, sized between the tabs and the footer. A click outside, Escape (the name field's Escape closes the field alone first), another tab or a profile applied closes it; the list under it is never moved. A remembered Profiles tab opens Agents. fit() no longer measures the profiles. Tested: panel-profiles.test.cjs reworked (no Profiles tab; the popover above the button at the foot's left and under the tabs, the count shown; scrolling to + Save current inside it, the field whole and nothing moving; Escape, outside click, Routing tab and applying a profile close it; Profiles only on Agents) and old-webkit.test.cjs opening it before Save, in Chromium and WebKit, en and zh. Screenshots looked at in light and dark, en and zh. go build -tags nogui (and GOOS=linux, GOOS=windows), go test -tags nogui ./... and the GUI suite pass (click-scroll, panel-profiles, panel-routing and routing-kind failed under --test-concurrency=4 and pass alone). Not tried in the real Wails window.
2026-09-30 01:05:43 +08:00
drunkduckdrown and yetone
7103ba1997
provider: a model can be told to accept images ( #228 )
...
* provider: a model can be told to accept images
Reasoning levels can be narrowed per model, but whether a model sees
images came only from the vendor's list. A relay or a model the list
hasn't caught yet could not be marked as taking images, or as text
only. The names editor now has Accepts images. The choice is kept
beside the name and the levels, and is what agents and the gateway's
image check are told. The same answer as the vendor's is not stored.
Restore default clears it.
Tested with TestModelImage. go build -tags nogui passes.
* settings: a save of the Settings page keeps the models' image choices, as it keeps their names and levels
POST /api/settings copied Visible, ModelNames and ModelEfforts over from the current settings but not the new ModelImages, and the Settings page never sends it, so switching the theme or language dropped every "Accepts images" override. Tested with TestSettingsSaveKeepsModelChoices (fails without the fix: images map[]).
---------
Co-authored-by: yetone <yetoneful@gmail.com >
2026-09-30 00:52:07 +08:00
yetone
0f33e53454
gateway: AI Studio's Gemini is asked for its thoughts, so Claude Desktop sees it think instead of waiting in silence (X @saoyan25: Claude Desktop through magpie took 20 s for 你好, 首字 19983 ms, where another client showed Gemini's first output in 2 s) — a request translated to Google AI Studio's OpenAI-compatible endpoint carried only reasoning_effort, and Gemini gives its thoughts there only when asked (extra_body.google.thinking_config.include_thoughts), so the whole think was silence: nothing for the client to show and the first token counted only at the answer. Now a client that asked to see the model think (Anthropic thinking enabled/adaptive, and the like) has AI Studio asked for its thoughts at its effort as thinking_config — thinking_level for Gemini 3 and later (xhigh and max as high), thinking_budget 1024/8192/24576 for 2.x, Google's own steps — in place of reasoning_effort, which Google won't take beside it; with no effort named, the model's own level. A request with thinking disabled (Claude Desktop's title requests) is sent reasoning_effort minimal, the least Gemini 3 thinks (it can't stop), where before it thought at its default. Other hosts get what they did. As the endpoint's way of giving thoughts isn't documented, both are read: reasoning_content, as before, and a leading <thought>…</thought> in the text, the tags split anywhere across chunks, which the chat decoder now passes on as thinking (the rest as the answer; text that only starts like the tag, or has one later, stays text). Tested with TestAIStudioThinking (each effort, 2.5's budget, no effort, off, an effort without thinking, another host), TestChatThoughtTags (whole, split, one byte a chunk, a leading newline, <b>, a lone <, a tag later, never closed) and TestChatThoughtTagsToAnthropic (a thinking block ahead of the text, its first delta written as soon as it came, no tags left), which fail on main; go vet, go build -tags nogui (and GOOS=linux, GOOS=windows) and go test -tags nogui ./... pass (three internal/provider tests timed out under the parallel run and pass alone). Not tried against the live AI Studio API: no key for it here.
2026-09-30 00:46:39 +08:00