yetone
04dc3bca72
panel: a Routing tab shows the gateway's latest requests, and the allowances' tab is named Allowances (v on Discord #feedback: the panel's Usage tab and the window's Usage page mean different things, and the panel had no routing) — the tray panel had Agents, Usage and Profiles, its Usage being each subscription's allowance while the window's Usage counts requests and Routing shows where they went. The panel's tab now reads Allowances (额度; the window keeps Usage/用量, and the tab's id and remembered choice are unchanged), and a new Routing tab (路由) lists the gateway's last 8 requests as they come, from the trace the Routing page already long-polls (/api/gateway/trace, which routing.js polled in the panel too; no new endpoint): each a two-line row with the agent's dot, icon and name and the model it asked for, the time, then where it went (provider · account or key), the model that try sent and, when the reply named another model (the new served/swapped fields), the amber "served gpt-6-luna" (实际 …) tag of the Requests list, beside the tokens and time taken; a failed one says its status and why (429 · rate limited), one under way who is answering. Over them, today's calls and tokens (the Usage page's /api/usage?period=today, read when the tab is shown and after a request ends, at most every 5 s) and Open Routing. A click on a request opens the window's Routing page on it: POST /api/window/main takes req beside view=routing (mainView lets only a positive id through), and the page, loaded so, picks that request as a click in its list would (its story told) and drops req from the address. The panel's height counts the tab as its tallest, as the others; the tab redraws keeping the panel's scroll, the click moves nothing, no border stripe. Tested with a new internal/gui/tests/panel-routing.test.cjs in Chromium and WebKit, en and zh: the Routing and Allowances labels, four fixture requests listed newest first with agent, model, "Relay · team" and "Claude · ann@example.com", the served tag on the swapped one only (not on a dated name), the 429 row, times, no row overflowing, today's 42 calls · 1.2M tokens, a click with the panel scrolled asking for view=routing&req=100 and leaving the scroll as it was, Open Routing, and the window opened on ?view=routing&req=101 with that request picked and its story shown; and TestMainView. go build -tags nogui (and GOOS=linux, GOOS=windows), go test -tags nogui ./... and the whole GUI suite (102 tests) pass. Not tried in the real tray panel or with a live gateway: the window opening on the request is tested in the page, not through Wails.
2026-09-29 20:22:19 +08:00
yetone
1cd7b4d086
settings: the warm-ups and WorkBuddy's check-in sit under Codex, Claude Code and WorkBuddy headings, with short lines ( #124 Mrhe525: a tidier Settings page) — after Open at login, Dock and Proxy the Preferences list ran on with Codex warm-up, Codex daily warm-up, Claude Code warm-up, Claude Code daily warm-up and Check in to WorkBuddy daily, each with a long grey sentence, one flat list. They now come after it in three groups under small headings as Local network has: Codex (Warm up on reset · 窗口重置时预热, Daily warm-up · 每日定时预热), Claude Code (the same two) and WorkBuddy (Daily check-in · 每日自动签到), the rows' names no longer saying the service. The lines say the purpose alone ("One tiny request starts the next window at once", "Starts each account's 5-hour window at this time every day", "Claims each signed-in China account's check-in credits once a day"), the last warm-up (上次启动于17:01) and each account's check-in (今日已签到 +1…) still after them; the rest (06:00 gives three by 21:00, sent through Claude Code, 签到 as in WorkBuddy) moved into the lines' tooltips with the fine print already there. The WorkBuddy group, heading and all, shows only while an account is signed in or the check-in is on, as the row did; a daily warm-up's time field was already there only while it is on. Same settings keys and the same /api/settings posts; en and zh strings replaced together, the old ones removed. No border stripes. Tested with a new internal/gui/tests/settings-groups.test.cjs in Chromium and WebKit, en and zh: the headings in order after Preferences and before Local network, each group's rows and none left in Preferences, the status on the lines, no left border; with the page scrolled, each segment (Weekly, Weekly and 5-hour, daily On, a new time, Off, check-in On) posts codexWarmup, claudeWarmup, codexWarmAt, claudeWarmAt and workbuddyCheckin as before with the page left where it was, the time field coming and going with its On/Off; no WorkBuddy group with no account. All internal/gui/tests/*.cjs pass (108), go build -tags nogui ./... and go test -tags nogui ./... pass.
2026-09-29 20:22:09 +08:00
yetone
d2112865ef
dsh: a profile DeepSeek Harness makes after magpie set it up gets magpie's model too (Discord 莫: 模型为什么WEB版本有,桌面版就只有官方自带的?) — every dsh profile has its own patch list (~/.dsh/profiles/<name>/cordis.patch.yml), and picking a model in magpie writes the ones there then. The desktop app's is made the first time it opens, so opened after the web's was set up it had none of magpie's entries and listed DeepSeek's own models alone, while Check read the web's (the first) and said all was well. Now Check looks at every profile and says which has none of magpie's models (the row's unwired mark, whose fix sets the model again in all of them), and the sync run at start-up and on each catalog change gives a profile with no llm-deepseek entry at all the model the wired one has; a profile with an llm-deepseek of the user's own is left as it is (reported, until the model is set again). Tested with a new TestDshProfileMadeLater (the web profile set, then a desktop template and a profile with the user's own entry: both reported, the sync fills the desktop's and leaves the user's untouched, setting the model again clears it; fails on main). go build -tags nogui (and GOOS=linux, GOOS=windows), go test -tags nogui ./... pass. Not tried with the dsh desktop app itself.
2026-09-29 19:59:20 +08:00
yetone
c816c9f828
routing: a request answered by another model than the one asked for is marked, from the model the vendor's reply names (v on Discord #feedback: 模型路由情况其实有的时候还是想知道的,比如之前 openai 会把 sol 降智到 luna 这样的,现在只有走 s2a 和 newapi 这些网关服务才能明确看到) — magpie read the tokens off each reply but not the model it says answered, so a vendor serving sol's requests with luna looked, on the Routing page, like sol answering. The reply's model is now kept as it goes by, never delayed or changed: on a relayed reply by the usage sniffer, which already reads every event and JSON body (Chat Completions' model in its JSON and each chunk, the Responses API's response.model in response.created/completed and model in its JSON, Anthropic's message.model in message_start and model in its JSON); on a translated one from the decoders' start event, which already carried it (Chat, Responses, Anthropic, and Gemini Code Assist's modelVersion), streamed or collected; on a Claude subscription's from the CLI's message_start; and on Codex's own models relayed to the ChatGPT backend. It goes in Usage.Served (and so the request log), each try's Served and the route's (the last try's, when it answered), with Swapped when it is another model than the one the try sent the vendor. Vendors mostly echo a dated or pinned name, which is the same model: the names are compared without a maker or path prefix (openai/, models/, Bedrock's us.anthropic.), case, and a version at the end — a date (-2025-08-07, -20250929, -06-05), a build number (-0613, -001), Bedrock's -v1:0, -latest, -preview — so gpt-5 answered as gpt-5-2025-08-07 or claude-3-5-sonnet-latest as claude-3-5-sonnet-20241022 is not marked, while gpt-5 as gpt-5-mini, gpt-5 as gpt-5.1, claude-opus-4 as claude-opus-4-1-… and deepseek-v3 as deepseek-v2 are. A reply that names no model marks nothing. The Routing page's Requests list puts an amber "served luna" (实际 luna) tag by the answer, its title saying what it means, and the request's story a line "requested sol · served luna" (请求 sol · 实际 luna) with the same sentence; no border stripe, and picking the request leaves the page where it is. Cursor, Devin, Kiro and Qoder name the model they were asked for themselves, so they are never marked.
...
Tested with new internal/gateway/served_test.go: TestServedModelRecorded (fake vendors in each protocol, streamed and not, relayed — Anthropic, Chat, Responses — and translated — Chat from Anthropic, Anthropic from Chat, Anthropic from Responses — each answering luna (marked), sol-2026-01-01 and models/SOL (not): the route, its try and the request log keep the name, Swapped as said), TestServedModelUnnamed, TestSwappedModel (the names above), TestSniffServedModel and TestCodexOwnModelServedTraced; and new internal/gui/tests/routing-served.test.cjs in Chromium and WebKit, en and zh (the tag on the swapped request alone, whole and in its row, no left border, its title; the story's line; a click that doesn't move the page; the dated and unnamed ones unmarked). With three requests alone the Routing page above the list jiggles by a few to 70px in WebKit on its own (on main too, with no served model in the fixture), so the fixture lists six. go build -tags nogui (and GOOS=linux, GOOS=windows), go test -tags nogui ./... and all GUI tests (80) pass. Not tested against a real vendor serving another model.
2026-09-29 19:41:29 +08:00
yetone
729f6bdbdf
models: a model its maker lists with a thinking switch alone takes no borrowed levels, and the gateway sends it no more than high ( #214 Moody-Max: opencode 的 mimo-v2.6 Flash 你这边设置的默认推理强度是 max 导致400报错)
...
A model not under its provider's own models.dev vendor (a relay or provider of the user's, a live id that doesn't match, or a preview/-exp or typed id Known looks up) took the levels most listed for it across all of models.dev. For mimo-v2.6-flash those are the resellers' (llmgateway, counted once per upstream host it lists, and requesty, up to max), while Xiaomi lists it with a thinking switch alone. OpenCode was offered variants up to max, Pi and Codex the same levels, and the gateway passed max on unchanged (fitEffort keeps an effort when no levels are known), which Xiaomi turned away with a 400. The same held for Zhipu's glm-5/5.1/5v-turbo, Kimi k2.6, MiniMax M2.x, gemma-4 and others whose makers give no levels while resellers give up to max.
Now catalog.ListedBy looks a model up in given catalogs (vendor prefix, case, a ':free' or '(…)' suffix aside) and gives the levels listed there, none for a switch or no options, and skips an entry with a token budget alone (Claude 4.5), which says nothing of efforts. Known looks in the provider's own catalogs first, and every borrow (Known, effortsOf) asks the makers' catalogs (vendor presets, less Groq and Ollama Cloud, marked Hosts as they serve others' models) before the crowd. The crowd vote counts a provider once per list, so an aggregator listing a model under several hosts no longer outvotes the rest. Provider.Levelless says a model is known to have no levels, and the gateway (fitFor, on the direct and fallback paths) sends such a model no more than high; a model whose levels are simply unknown is still sent what was asked.
Tests: TestEffortsOfOneVoteAProvider and TestListedBy (catalog); TestMakerLevelsBeforeResellers (provider: Xiaomi's preset, a preview left out of the list and a relay get no levels, an unlisted model and a budget-only Claude one still borrow, Levelless); TestOpenCodeNoResellerLevelsForSwitchModel (opencode.json has empty variants for Xiaomi's and a relay's mimo, three for a reseller-only model; Pi has no reasoning or thinkingLevelMap for mimo); TestSwitchModelSentNoMoreThanHigh (gateway: mimo asked max/xhigh sent high, low as asked, an unknown model max). The agent and gateway tests fail on main. An audit of every preset against the current models.dev snapshot found the cases above. Not tested with a real Xiaomi key or a running OpenCode. go build -tags nogui (and linux, windows), go test -tags nogui ./... pass.
2026-09-29 19:41:29 +08:00
yetone
6aa99ef1c0
usage: costs show in RMB at a live rate, when Settings picks it ( #212 L1cardo: 添加费用用人名币结算的选项,最好是能根据实时汇率来计算) — the Usage page, the tray panel, the TUI and the CLI all price a call in USD, magpie's list prices being dollars; Settings now has a Currency row (after Usage shows) offering $ USD, kept by default, and ¥ CNY, which converts every one of those at display time only — usage.Totals.Cost and the JSONL records it comes from stay in USD always, and a vendor's own balance (already in its own currency, a relay's ¥) is never touched.
...
The rate is internal/fx: a free, no-key endpoint (open.er-api.com/v6/latest/USD), cached in memory and in a small file beside magpie's other caches (XDG_CACHE_HOME/magpie/fxrate.json) for 12h, refetched only once that's stale, and falling back to the last rate it ever had — or a fixed 7.2 — while offline; the currency row's tooltip names the rate and when it was learned. usage.FormatCost carries the $/¥ formatting (2 decimals above $1, 3 below, whole numbers above 100) that usage_cli.go and internal/tui/pages.go used to each have their own copy of; both now read Settings' currency once per command/redraw rather than per row. internal/gui/api.go exposes the rate on /api/settings always and on /api/state only when cny is actually chosen, so usd users' hot path never touches the network for a rate nothing shows with.
Tested with new internal/fx/fx_test.go (a fake server: fetches and caches, answers from memory then from the cache file with no further calls, refetches once stale, falls back to a stale cache or Fallback when offline, ignores a reply with no CNY rate), TestFXInState in internal/gui/fx_test.go, TestFmtCostShowsChosenCurrency in internal/tui/usage_test.go, and a new internal/gui/tests/currency.test.cjs in Chromium and WebKit, English and Chinese (the Usage page's $ by default and ¥12.34×7.2 once cny is picked, the currency row's labels/state/tooltip, a real wheel scroll deep into Settings left alone by picking a currency, the choice surviving a reload). go build -tags nogui, GOOS=linux and GOOS=windows builds, and go test -tags nogui -count=1 ./... all pass clean.
A fetch that failed is not tried again for 10 minutes (the stale or fixed rate shown at once meanwhile), so offline each state() does not wait up to 4s on the network: TestGetDoesNotRetryAFailedFetchAtOnce.
2026-09-29 19:41:29 +08:00
yetone
55a0c35f30
agents: Antigravity CLI (agy) takes magpie's models (Discord Str: asked to use agy through magpie) — agy is on the Agents page like the others. Picking one of magpie's models writes ~/.gemini/antigravity-cli/settings.json: modelProvider "gemini", an entry per magpie model under customModelsConfig.customModels (magpie/<ref> → {apiProvider: API_PROVIDER_GOOGLE_GEMINI, modelName: <ref>}) beside the user's own, and model. Every other key is kept, and "as installed" gives back the user's model and modelProvider and takes magpie's entries out (the file byte-identical to before). agy reads its endpoint and key only from GEMINI_API_KEY / GOOGLE_GEMINI_BASE_URL, and in agy 1.2.13 a custom model chosen by settings or its picker fails ("model is empty"): only --model resolves it. So the agent has a Launch command, GEMINI_API_KEY=magpie GOOGLE_GEMINI_BASE_URL=<gateway> agy --model '<model>' (a $env: form on Windows), which magpie agy prints as its notice and the row's new square copies in one click (tooltip shows it; no scroll; zh strings). Check says unwired when the entry is gone or modelProvider isn't gemini. Tested with TestAgy (set/switch/drift/restore, a user's custom model kept, a file with nothing of the user's left as it was) and agy-launch.test.cjs in Chromium and WebKit, en and zh (fails without the app.js change); end to end in a sandbox HOME with agy 1.2.13 against a sandbox gateway on 38472 and a fake OpenAI upstream: magpie agy magpie/fakeup/echo-1 wrote the settings, agy (interactive and --print) answered from the upstream through the gateway, magpie agy default restored the file. Not tested: the Windows command, a real Gemini upstream; agy's requests show in usage as google-genai-sdk (its UA), and its title requests for gemini-3.1-flash-lite-preview get a 404. go build (nogui, linux, windows) and go test -tags nogui ./... pass; the GUI suite passes but for click-scroll's WebKit case, failing already
2026-09-29 19:41:29 +08:00
yetone
13309b07fa
panel: a profile saved or deleted shows at once and stays in sight (Image #62 : 点击保存没有效果,删除方案也没有效果。你为什么UI UX做这么烂?) — both did their work (profiles.json had the profile), but the page showed nothing of it: every profile action answers with the whole state, which read logins.json (166KB, the accounts' credentials) again for each agent's drift and models, dozens of times — 2.5–4s a state on a real setup — and only then redrew the chips. And with no profiles yet the chip saved came in above + Save current, which the click held where it was on the screen: the hold gave the view room and scrolled it 30px, and the chip went up under the tabs, out of sight. Now the chip saved is there at once, dimmed till magpie answers, the name field closed; × takes the chip away at once; a save or delete refused puts the list back and says why. A view at its top when clicked stays at its top instead of being given room to scroll (what came in above the control moves it down). logins.json is parsed once until it changes (filememo, a copy to each caller): a state went from 2.4s to 0.9s here. Tested with a new case in internal/gui/tests/panel-profiles.test.cjs, Chromium and WebKit, en and zh: magpie answering after 1.5s with no profiles, the chip there and dimmed before the answer, the list unscrolled and the chip below the tabs, × removing it before the answer, a refused save taken off with its reason (fails on main); against a sandbox magpie web in WebKit, the view stays at 0 where main scrolled it to 30. click-scroll passes in both engines; go test -tags nogui ./... (TestClaudeSignedOut flaky under the full run, passes alone), go build for linux and windows
2026-09-29 19:23:14 +08:00
yetone
0b7781ba18
providers: Claude Code is signed in to its next account once the one it is on is spent, as Codex is, and an account keeps its own affinity, backoff and usage across a switch ( #208 Cyb3rN8: Claude Code 用自己的模型时,登录账号额度用满不会自动切换 · #209 : Smart 选中的账号和 Claude Code 登录账号互不同步…切换登录账号还会把按 id 记的状态串到另一个账号上) — Claude Code on its own model asks Anthropic as the account it is signed in to, never through magpie, so with A spent its sessions got "You've hit your session limit" while the gateway had moved on to B. The Codex watcher (KeepCodexOnAnAccountWithRoom, now KeepOnAnAccountWithRoom, same place: started by the magpie serving the gateway, a minute in and every 5 minutes) now looks at Claude Code too: when the account it is signed in to is spent and another ticked account's allowance is known and has room, it is signed in to that one through SwitchLogin (credentials and .claude.json's oauthAccount), as Use on the Accounts page does; not when the Claude account was removed from magpie. The rule for #209 : the sign-in follows Smart, not the other way round — it moves at the share Smart counts an account spent (provider.SpentShare = 98, which the gateway's usedShare now is), for Codex too (it moved at 100%). The Routing field of a Codex or Claude subscription with several accounts says so: routing picks the account for what comes through magpie; the agent on its own uses the one it is signed in to, moved on at 98%. Sessions already running keep the account they started with until restarted, as SwitchLogin says; routing Claude Code's own model through the gateway (the codexFailover way) is not done here. #209 's second part was a real bug: the account the agent is on is the candidate "claude" and the others "claude@user", so a switch handed "claude" from A to B — a conversation A answered was kept on B and traced kept=true, and the Least used tokens and the backoff count followed the id. who(), routed.used and routed.failures are now keyed by restKey (provider@user, as rests already were); rest itself (group members, the trace) is unchanged. Tested with new TestClaudeSwitchedWhenSpent (sandbox HOME, keychain off, a fake usage endpoint: 97% stays, 98% signs Claude Code in to b — credentials and .claude.json — a stays ticked; on b it stays; both spent it stays), TestCodexSwitchedWhenUsedUp still passing, new TestLoginSwitchKeepsEachAccountItself (fails on main: the conversation went to b, kept) and new internal/gui/tests/routing-signin.test.cjs in Chromium and WebKit, en and zh; login-import and routing-kind GUI tests pass. go build -tags nogui (and GOOS=linux, GOOS=windows), go test -tags nogui ./... pass (TestUserPath, TestClaudeAccountNamedAsItsLogin, TestClaudeIdentityServedWhileAsked and TestCodexVersion timed out at 5s under the full run's load and pass on their own). Not tested against real Claude accounts or a running Claude Code.
2026-09-29 18:56:33 +08:00
yetone
3676d2e713
providers: an account signed in while magpie runs is listed with its vendor's models ( #204 uclort: 首次添加 WorkBuddy 模型显示不全,点击刷新后模型全部显示) — a signed-in account's own list was fetched only at start-up, for the accounts there then (and once: a fetch that failed was not tried again). WorkBuddy AI signed in afterwards, in magpie or in WorkBuddy itself, showed magpie's built-in list ("magpie's list · Refresh asks the vendor", 14 models) until Refresh fetched /v3/config's 20-odd. Now provider.FetchNew, which start-up uses in place of its loop, also runs before the providers are listed (providersState, 8s each): each ready account with no fetched list is asked for it, so the first listing after a sign-in is the vendor's. One that fails is asked again after 10 minutes, not on each listing; one fetched is not asked again. Tested with a new TestFetchNewAccount (a fake WorkBuddy /v3/config: nothing asked with no account; a WorkBuddy auth file written while running lists the built-in models, then the vendor's three after FetchNew; not asked again; a 502 asked once over two listings, then again after the retry time). go build -tags nogui (and GOOS=linux, GOOS=windows), go test -tags nogui ./... pass (TestUserPath and three Claude sign-in tests timed out once under load, pass on their own and on a rerun of the package). Not tried in the GUI with a real WorkBuddy account
2026-09-29 18:56:32 +08:00
yetone
559ce02147
models: a model's own name given by the user keeps its provider after it too ( #203 uclort: 如果在供应商模型页自定义模型名称,在 Codex 中就不会显示供应商了) — Entry.Label() showed "name · provider" only for a model's own (vendor's or models.dev's) name; a name the user gave it (provider's model page → Names & levels) dropped the provider entirely, so a renamed model in Codex's picker (and Pi's, ZCode's, OMP's, magpie's own gateway list — everything magpieModels/Label feeds) could no longer be told apart by vendor at a glance. Label() now appends " · provider" (or "routing group") to a custom name too, the same as it does for the vendor's own, and only leaves it off when the name already contains the provider's (case-insensitively), so a name like "Sol on A" isn't doubled to "Sol on A · A". No settings toggle added: the fix makes the provider suffix unconditional (what the issue actually asked for), so a preference to hide/show it would only reintroduce the gap; there's also no existing "model list" preferences surface to hang it on cheaply — Names & levels only holds the name and reasoning-level pickers. Tested with TestModelName (internal/provider/modelprefs_test.go, sandbox HOME): a custom name now reads "My Sol · A" in Label() and in CodexListed(), and "Sol on A" (already containing the provider) is left alone; and TestModelNameAndLevelsReachAgentFiles (internal/agent/modelprefs_test.go, sandbox HOME) checks the generated Codex model_catalog_json and Pi models.json entries carry "GLM · Relay" for a custom name and don't double it to "GLM via Relay · Relay" when the name already says Relay. go build -tags nogui (and GOOS=linux, GOOS=windows), go test -tags nogui -count=1 ./... pass except TestUserPath and TestClaudeAccountNamedAsItsLogin, which are load-flaky (pass alone, unrelated to this change) same as the pre-existing rtk.go gofmt / TestConfiguredModelsAdvertiseImageInput flakiness
2026-09-29 18:56:32 +08:00
yetone
be32242fda
providers: Bedrock's list offers Claude Sonnet 5.5 ( #176 KylinDC: 模型列表还是不全,比如没有sonnet 5.5 … Sonnet 5.5: global.anthropic.claude-sonnet-5-5) — Bedrock's runtime has no list to ask, so its preset's models are the list; global.anthropic.claude-sonnet-5-5, the inference profile id KylinDC gave, is added after Opus 5.5. Any other id can still be typed in the provider's model editor. go build -tags nogui, go test ./internal/provider ./internal/gateway pass
2026-09-29 18:42:11 +08:00
yetone
3ba6ff7d2e
providers: ChatGPT and Claude accounts come in from CLIProxyAPI's auth files ( #124 Mrhe525: 要是能把那什么cpa的json文件可以导入直接使用就无敌了) — the ChatGPT (Codex) and Claude subscriptions now take the import Antigravity had: Import accounts from a file… under the accounts, and Import from a file instead… while a browser sign-in waits. It reads CLIProxyAPI's files ({type:"codex"|"claude", access_token, refresh_token, id_token, account_id, email, …}), one or a list or {accounts:[…]}, Codex's own auth.json ({tokens:{…}}), Claude Code's .credentials.json ({claudeAiOauth:{…}}) and bare refresh tokens one a line. Each account is checked as a sign-in in magpie is finished: its refresh token traded with the agent's own OAuth client (codexRefresh / claudeRefresh), then the ChatGPT claims or Claude's profile read (plan, tier, account and organization), and it is kept as that sign-in would keep it: Codex's or Claude Code's own if the agent has none, else added and ticked. The refresh rotates the token, so the file's copy stops working; the box says so. A refresh token magpie holds already (saved or the agent's own) is "Already in magpie" and never refreshed; another app's file ("a claude sign-in, not Codex's"), an API key, a token in the files twice and one the vendor turns down ("used since … expired or signed out") are listed as not added with the reason; tokens are never in what comes back. codexExchange and claudeExchange now build their sign-in through the same codexLogin / claudeLogin. Also: a Claude sign-in with no ~/.claude yet failed writing .credentials.json (the directory is made now), and the magpie://import dialog was drawn by the subscription import's renderImport, declared later under the same name (renamed renderLoginImport). zh strings added. Tested with new TestImportCodexLogins (a fake token endpoint: a CPA file and a native auth.json added with the new tokens and the file's account id, the live account untouched and reported as there, a Claude file, a spent token and a duplicate refused, a second import "exists", garbage an error) and TestImportClaudeLogins (with no sign-in the CPA file becomes Claude Code's credentials, plan max, active; a spent .credentials.json and a Codex file refused), and a new internal/gui/tests/login-import.test.cjs in Chromium and WebKit, en and zh (both ways in, the texts, what is posted, the outcomes, the account listed). go build -tags nogui (and GOOS=windows, GOOS=linux), go test -tags nogui ./... and the other GUI tests pass on fe22692 (click-scroll's WebKit case fails after f1a8caf, with or without this change)
2026-09-29 18:34:08 +08:00
yetone
7c24e3ab29
panel: a profile is saved by clicking Save, and the name field opens beside it without moving the list (上边缘被裁了 · 每次点保存方案,它都会自动向下滚动一下 · 我点保存并没有反应,这个功能没做完吗?) — in the tray panel's Profiles tab, + Save current put a name field at the head of the list and focused it: the list scrolled to it (drawn up under the tabs, 2px away, the page's focus ring over the field's own accent border cut off at the top), and with a long list it went out of sight above the button. Only Enter saved: a click on the button took the focus from the field, whose blur removed it, so the click found nothing and the next click opened it again. Now the field opens beside the button (in the window at the row's end, in the panel on the button's line under the chips, the button as tall as the field so the line keeps its height), focused without scrolling, with one ring; the button reads Save (保存) while it is open, a click on it saves as Enter does (the press keeps the field's focus), an empty name keeps the field open, Escape closes it and gives + Save current back, as does the list redrawn after a save. Tested with a new internal/gui/tests/panel-profiles.test.cjs in Chromium and WebKit, en and zh: 30 profiles scrolled to the end, the field opened beside the button with the list unmoved, field and button in the view, no outline, Save saving, an empty name, Enter, Escape (fails on main); the other GUI tests pass
2026-09-29 18:29:56 +08:00
Ns2Kracy
6646a37170
ui: simplify LAN sharing address display ( #198 )
...
* ui: compact LAN sharing addresses into one row
* ui: refine LAN address picker for narrow settings
* ui: preserve LAN address row top spacing on wrap
* ui: animate LAN address wrap at narrow widths
2026-09-29 18:27:01 +08:00
yetone
36b0d23fe7
providers: a provider of the user's saved on a subscription's id is moved off it, so the subscription is listed once signed in (LingYi_Stu on X: workbuddy 国内版本无法使用呢 — "凌一 已登录,但 magpie 无法列出它,请反馈此问题", magpie 0.1.384, WorkBuddy 5.6.2 signed in, the international build listed with 3 accounts beside it) — before WorkBuddy's plan was a subscription (v0.1.261), a provider added as "WorkBuddy" was saved with the id workbuddy, and All() skips an account whose id a saved provider has: the sign-in said done, logins.json kept the account, and the Providers page listed only the user's own provider, the card still saying Free · Pro. Now start-up (agent.MoveOffAccountIDs, beside RenameLegacy) moves each provider with a base URL saved on a subscription's id to the next free one (workbuddy-2) the way a rename does: routing groups, fallbacks and the agents on its models follow; the old id is not kept as one it Was, being the subscription's. Rename allows it for such a provider alone, a subscription still keeps its id, and Find takes an id before a name, so workbuddy is the subscription and not a provider called WorkBuddy. Tested with a new TestWorkBuddyUnderUsersOwnID (the sign-in done and unlisted, then both listed after the move, a fallback moved, the subscription refused a rename), and end to end in a sandbox HOME with a WorkBuddy auth file and providers.json: main lists the custom WorkBuddy alone; with the change magpie providers lists workbuddy-2 (its fallback moved) and WorkBuddy signed in as 凌一. go test -tags nogui ./... (TestUserPath failed once under load, passes on its own), go build for linux and windows
2026-09-29 18:17:14 +08:00
yetone
dbe2f01e7c
routing: the requests sit right under the stage, sized to stay in sight with it (Image #23 : 这一块离那个路由动画距离太远了,用户点击了这里都不知道发生了什么) — the groups and the providers with several accounts or keys came between them, so a request picked in the list played on a stage scrolled out of sight above. They now come after the requests, and the list is as tall as leaves the stage in view above it (216–420px), scrolling on its own rather than the page. Picking a request no longer sends the list back to its top in WebKit (it was emptied and refilled, which reset its scroll). click-scroll.test.cjs brings a row into the list before picking it, as a reader would; Chromium and WebKit pass
2026-09-29 18:11:10 +08:00
yetone
fe2269288c
agents: each agent's CLI shows its version and updates in one click ( #202 JoeyMa-zh: Agents 页面增加"更新 CLI"功能 — 检查各 agent CLI 的版本并一键更新) — the Agents page now puts each CLI's installed version after its name (read with <cli> --version, 10s timeout, no stdin) and, when a newer one is out and magpie can tell how it was installed, a small "Update to x.y.z" pill. How it was installed comes from the resolved binary: the vendor's own installer first (Claude Code's ~/.local/share/claude/versions → claude update, Codex standalone → codex update, ~/.opencode/bin → opencode upgrade), then a global node_modules (bun, pnpm, else npm, run as npm install -g --prefix <prefix> pkg@latest with that prefix's npm, and only where the prefix is writable), then a Homebrew formula or cask allowlisted per agent (brew upgrade [--cask]); anything else shows the version alone, with no button. The latest comes from the npm registry or brew info --json=v2 (no auto-update), cached 6h (15min after a failure); installed versions are cached by the binary's path, size and mtime. GET /api/agents/cli waits at most 3s and says pending, and the page asks again; POST /api/agents/cli/{id} runs the update (one at a time per agent, 10min cap), reads the version again and says so if it did not change, or gives the command's last lines when it fails. The pill goes busy in place and the page never scrolls; not in the tray panel. zh strings added. Tested with a new internal/agent/cliupdate_test.go (version order and parsing; npm/bun/pnpm/brew formula and cask/native installs, a read-only prefix, a local node_modules and unknown binaries in a fake PATH; an update against a fake registry and updater) and a new internal/gui/tests/cli-update.test.cjs in Chromium and WebKit (versions, pills only where magpie can update, row heights, an update with the view scrolled, a failing one, zh). No real update was run. go build (nogui, GOOS=windows, GOOS=linux), go test -tags nogui ./... and the GUI tests pass
2026-09-29 17:56:43 +08:00
yetone
c9e47f043e
balance: a custom provider's balance token can't be left where it is never asked, and new-api's refusals say what to do (TJHHHH: OK了,希望有防呆机制 — a new-api relay's System Access Token pasted beside a Balance URL of /api/usage/token, no New-Api-User header, and the Usage page never showed a balance) — new-api has two balance queries: /api/usage/token tells a key what is left on it and takes the key alone, and /api/user/self tells the account's quota to the access token with the user's id in New-Api-User. A token beside the first could only ever be turned down, and the second without the header answered {"success":false,"message":"无权进行此操作,未提供 New-Api-User"}, which a 401 showed as JSON and a 200 read as a field missing. The editor now says so under the token as soon as it is there: beside …/api/usage/token, or with no Balance URL at all (the token field is now offered on every custom provider, not only one with a URL saved), a warning with one button, Use https://<site>/api/user/self, which sets the URL, sets the Balance field to $data.quota / 500000 when it was empty or named /api/usage/token's fields, and opens More endpoints to show it; once the URL is /api/user/self and no New-Api-User header has a value, it asks for New-Api-User = your user ID (shown in the site's personal settings) until one is typed. The URL is not switched behind the user's back: what the editor shows is what is asked, and the field typed for /api/usage/token's reply wouldn't read /api/user/self's anyway. Behind it, Balance never sends a token to /api/usage/token and says what to set instead; a reply of success:false with a message, at a 401 or a 200, is that message as the error, with the header to add put in front when it is about New-Api-User (not provided, format error or mismatch, in new-api's Chinese or English); and /api/user/self with an empty field reads the quota, $1 to 500000. The Balance field has a Check balance button (POST /api/provider/balance) that asks as the form has it, before a Save, the saved provider with the form's URL, field, headers and token; it and the Usage page's card say either fix plainly, the relay's own words on hover. A JWT beside new-api's URL is not warned about: new-api's own dashboard sign-ins are JWTs now, so a token's shape can't tell new-api from a sub2api panel. Tested with new TestKeyUsageWithAccessToken (never asked, the fix said, the key alone still asks), TestUserSelfDefaultField and TestNewAPIUserRefusal (401 and 200, Chinese and English, the mismatch, and an unrelated refusal left as it came), which all fail on main; and a new internal/gui/tests/balance-fix.test.cjs in Chromium and WebKit (the warning on the saved setup, the click, the header hint going once typed, Check balance with and without the header and what it posted, Save, the Usage card's words, every new string in Chinese, and a new provider with a token and no URL). go build (and GOOS=linux, GOOS=windows), go test -tags nogui ./... and the other GUI tests pass
2026-09-29 17:56:43 +08:00
yetone
037189ebdb
agents: a provider switched off or removed, or its last account signed out, moves the agents on its models to others ( #200 : 希望关闭某个llm provider的时候,更改agent选择的model到可用的model … 关闭这个provider之后,第一次使用会报错,因为copilot/ 开头的模型不存在) — switching Copilot off took copilot/… out of the catalog and the agents' model lists, but an agent picked on one kept it in its config and its next request was refused as switched off. Now every field on one of magpie's models (the model, Claude Code's opus/sonnet/haiku/fable tiers, any other) whose model the change stopped serving is moved: to the same model from a provider still on, the same id first, else one spelt another way (claude-sonnet-4.5 and claude-sonnet-4-5-20250929), else back to the agent's own default (a Claude Code tier: following the main model). It is done around switching off, removing, a key removed or off, and a sign-in signed out or off, in the window, the tray panel, the TUI and the CLI; the page's status line, the TUI and the CLI say which agent went to what. A session started before the switch still asks for the old model: the gateway now takes a switched-off provider's model as unserved, so Claude Code's stand-in (its model as set now) answers it rather than a refusal. Tested in a sandbox HOME: Claude Code on the main model and three tiers plus Hermes, switched off and removed (same id, other spelling, default, a tier following), switched on again moving nothing; /api/provider/off returns the moves; the gateway serves a switched-off model through the stand-in (fails without the change) and still refuses it to an agent left on it
2026-09-29 17:56:43 +08:00
yetone
f9c99cf370
gateway: a call Codex makes for itself says so on the Routing page, and goes to a pool account with what Codex says about it (碳碳双键: 为何 codex 桌面选的 5.6sol,但是请求记录…显示的全是 gpt-6-luna) — magpie changes no model: Codex sends its own calls on Luna (the guardian review of an approval, x-openai-subagent: guardian; a thread's title; memories) and, on Luna Reserve once the plan's allowance is used up, every turn with x-openai-codex-luna-reserve. Relayed to Codex's own sign-in these headers went on as they came, but a turn the pool of Codex's accounts served was signed afresh and went without them. Now x-openai-*, x-codex-* (turn metadata, installation, window, parent thread), session_id, conversation_id, x-client-request-id and version go on with it too; never a sign-in (Authorization, chatgpt-account-id, anything with authorization in its name) nor Codex's device attestation, the pool account's own going in its place, nor what holds for Codex's own account alone (x-openai-codex-luna-reserve, as another account's allowance may be left, and x-codex-turn-state, the backend's routing of the turn for that account). The trace, the Recent calls and the usage log keep the call's kind (kind, from x-openai-subagent; memgen; luna_reserve), and the Requests list, a replay and the request's story show it as a grey tag by the model — Approval check / 审批判定, Title / 标题, Memory / 记忆, Subagent / 子代理, Review / 审查, Compaction / 压缩, Luna Reserve / Luna 储备, an unknown one as Codex names it — the story saying Codex made the call itself and picks its model. The Requests list's model column fits its content (up to 36%) so model and tag keep their room, the tag giving way first. Tested with a fake ChatGPT backend on the pool path (acct-1 used up, acct-2 answering: both get the headers and neither the client's sign-in; fails without the change), callKind/codexHeader cases, and a new internal/gui/tests/routing-kind.test.cjs in Chromium and WebKit, en and zh (tags, the model shown whole, the story, a click leaving the row where it is); the other GUI tests, go build -tags nogui, GOOS=windows go build and go test -tags nogui pass
2026-09-29 17:56:43 +08:00
yetone
bdb1dfdd1b
providers: a Command Code sign-in goes through when whoami errs on the new key (tigger_ultra on X: Sign-in didn't finish · Command Code didn't take the new key: Internal Server Error) — magpie asked /alpha/whoami to name the account of the key Studio had just posted, and failed the sign-in on any answer but a 200; whoami answered that key with a 500. The CLI's own browser sign-in never asks whoami (it keeps the key and the name Studio posts; whoami is its check on a key typed in, and only a 401 there means a bad key). Now only whoami turning the key down (401, 403) fails it; any other error is asked again twice, a second apart, and then passed over for the name Studio sent, the plan still read from billing/subscriptions. With no name from Studio either, it fails saying so. Tested: TestCommandCodeSignInWhoamiDown, a fake Command Code whose whoami answers 500: signed in as the posted name with its plan after three asks, a 401 key refused, no name at all refused; go test -tags nogui ./... (TestClaudeSignedOut failed once under load, passes on its own with and without the change and in a rerun), GOOS=linux build
2026-09-29 17:41:26 +08:00
yetone
dc31c483da
providers: Codex on Amazon Bedrock's GPT models goes to the runtime's Responses API ( #176 : Amazon Bedrock: Function tools with reasoning_effort are not supported for global.openai.gpt-6-luna in /v1/chat/completions. To use function tools, use /v1/responses or set reasoning_effort to 'none'.) — the preset gave Bedrock chat completions alone for every model not Claude, so Codex's Responses, tools and reasoning in it, were translated to chat, which the runtime refuses for its GPT models. bedrock-runtime serves the Responses API at the same /openai/v1 with the same Bedrock API key as a Bearer, for OpenAI's closed GPT models (not gpt-oss, which stays on chat): the preset and each region now have it, a Bedrock provider saved before is given it where its chat is, and a GPT model is asked there first (Codex's request relayed as it came; a chat client's taken there once chat refuses it). A chat endpoint with no Responses to go to that refuses tools with reasoning so is asked again with reasoning_effort "none". Tested with a fake runtime returning that 400 on chat and serving /openai/v1/responses (Codex and a chat client with tools, a provider saved without Responses, gpt-oss still on chat) and a chat-only relay refusing the same (retried at none); both fail on main. Not tested against a real Bedrock account
2026-09-29 17:25:34 +08:00
yetone
7ca2debfe0
gateway: Connect folds away under its heading (zhanglolo: 把网关 tab 这里面接入加一个折叠的功能,这个接入使用很低频,但是太占面积了,相对来说下面的最近调用使用频率更高) — Connect is set up once and seldom looked at again, yet took most of the page above Recent calls. Its heading is now a fold with a chevron, as Models has been: folded, its fields go and the head keeps the base URL for the API picked, with a copy button, so the one thing reached for is still a click away; the fold is remembered per machine (magpie.gwConnectFolded) and a click on it leaves the heading where it is. The tray panel has no Connect. Tested with a new internal/gui/tests/gateway-fold.test.cjs in Chromium and WebKit (fold with the view scrolled, the URL in the head, a reload, unfold); the other GUI tests and go build -tags nogui pass
2026-09-29 17:03:46 +08:00
yetone
0a11252a27
groups: a routing group with a ChatGPT model in it offers Codex's ultra (TJHHHH: 似乎在使用路由时不支持ultra模式 — GPT-5.6 Terra · routing group in the Codex app had no ultra) — a group offers the reasoning levels its members have in common, and ultra (max, with Codex handing parts of the task to agents of its own) is a level of ChatGPT's own models alone, so the Terra group with codex/gpt-5.6-terra and the API's and Copilot's gpt-5.6-terra lost it and stopped at max. A group now offers ultra too when a member has it and every member that follows the agent's effort reaches max; one with a member below max, or with no ChatGPT model, is as before. At the gateway ultra was worse than missing: a Responses request made again for a member's own endpoint (Codex to a Chat or Anthropic member) read ultra as no effort at all and asked for none, and one passed on as it came sent a member whose levels magpie doesn't know "ultra" as it was. Ultra is now max to every member without an ultra of its own (or the nearest it has below), and a ChatGPT model keeps getting ultra, which it sends the backend as max as before. Tested with a new TestGroupUltra: the group's levels with and without a ChatGPT member and with a member at high, and Codex's ultra through the group to a member translated from Responses, a Chat member passed as it came and one with unknown levels (the levels and the translated and unknown-level cases fail on main). go build (and GOOS=windows) and go test -tags nogui ./... pass
2026-09-29 17:03:46 +08:00
yetone
3efba14ed5
qoder: thinking turned off reaches Qoder as off, a refused usage sign-in no longer lapses the account, only a gone sign-in is a 401, and a tool's images are sent ( #162 review follow-ups)
...
- Chat reasoning_effort "none" (and Responses reasoning.effort "none") was read as low by effortOf before qoderEffort saw it, so a model that can stop thinking (qfmodel) was sent enable_thinking true at low, and an Anthropic request with thinking disabled got the model's default (max on an always-thinking one). The request now carries ThinkOff beside Effort — set for effort "none" and for Anthropic thinking {"type":"disabled"} — and Qoder reads it as none: off where the model can stop, its lowest effort where it can't. effortOf and Effort are unchanged, so every other vendor gets what it did.
- The device token serves only Qoder's account pages (usage); chat runs on the job token. A refused device-token refresh marked the whole account "sign-in has expired" though chat still worked. Now only a refused job-token refresh lapses the account; a refused device one leaves usage unavailable with that reason ("Qoder refused the account-page sign-in (chat still works) — sign in again to see usage").
- askQoder answered 401 for any credential error, so a refresh that timed out made Claude Code ask to log in again. Errors that mean the sign-in is gone (not signed in, no such account, unreadable, no refresh token, a refused job refresh) now wrap provider.ErrQoderSignIn and are 401; anything else (a timeout, the network) is 502.
- Images a tool returned were dropped. Qoder's tool turns hold text, but its user turns take image_url blocks, so the images now follow the tool turns in a user turn — "[From the result of <tool> (tool call <id>):]" then the images — joined to the start of the user's own next turn, and the tool turn notes that they follow, the way buildChat does for Chat upstreams.
Tested: TestQoderThinkingOff (Chat, Responses, Anthropic off on qfmodel and on an always-thinking model; other vendors' Effort still low; thinking unmentioned keeps the default), TestQoderCredentialStatus (timeout and network 502, a gone sign-in 401), TestQoderToolResultImages, TestQoderRefusedRefreshLapses (device refused: not lapsed, usage reason; job refused: lapsed, ErrQoderSignIn) and TestQoderRefreshNetworkErrorNotSignIn — each of the first four fails on main. go build (darwin, GOOS=windows) and go test -tags nogui ./... pass
2026-09-29 17:03:45 +08:00
yetone
2ed6dd0520
routing: a request clicked in the Requests list leaves the page where it is (点击这里的任何一个空间都会自动滚动到页面的最顶部,这是重大bug) — picking a request scrolled the view up to the stage on purpose (scrollIntoView with the reader's click in hand, the one scroll the click hold lets through), and the stage is at the top of the Routing page, so every row clicked threw the reader to the top. It no longer scrolls: the row stays under the pointer and the stage and its story change above it; the 'errors your agent saw' count does the same. Live, a day and Replay them all were already held. Tested: a new click-scroll.test.cjs case clicks three rows of a day and Replay them all, each must stay within 1px, in Chromium and WebKit; it fails on main (picking request 3 moved the page 393px) and passes with the change; menu-scroll.test.cjs passes
2026-09-29 16:58:02 +08:00
yetone
ae597fc3d9
tray: Gemini CLI with no sign-in chosen folds away with the other agents at their default (willz: 电脑上没有安装 gemini cli,第二个选项无法设置为默认,我不需要 gemini 的配置,它应该像其他agent 一样,默认后就被收起来了) — the tray keeps up the list the agents that have something set (a field with a value) and folds the rest under "Show {n} more"; Gemini CLI's sign-in field read as Google when settings.json chose none (security.auth.selectedType unset, or no settings at all, ~/.gemini left by another tool with the CLI not installed), so it always had a value, stayed up the list, and its default could not be picked back. It now reads as the default when none is chosen, as with no CLI at all; one chosen reads as before. Tested with a new internal/agent/unset_test.go: every agent in an empty HOME reads every field empty (Gemini's sign-in was the only one that didn't), and Gemini's sign-in unset, with only the look set, each chosen type, picked Google and back to the default. go build and go test -tags nogui ./... pass
2026-09-29 16:40:03 +08:00
yetone
680435965f
tray: expanding and collapsing on the Agents page keeps what was clicked where it is, and "Show {n} more" unrolls the rest under it (碳碳双键: 这个地方展开 ui 会偶现疯狂抖动,必现拉扯; #199 : agent页面展开与收纳会抽搐 甚至跑在窗口外面去; willz: 点击展开的时候,列表不是向下展开 而是向上展开) — the rest unrolled above the button, so in a panel at its tallest with the list scrolled the button slid down and off the foot of the view. What a click holds in place was passed over while anything played on it, a colour easing in included: the button's own hover fade and its label's fade meant the page was held by the list instead, so the button went off-screen for ~300ms and then the page snapped 90px back to it; and a button slid out from under the pointer and back fades its hover again, so the page swung between the two (484 → 563 → 484 → 572 … frame by frame in Chromium). Held elements were also put back a frame late, from requestAnimationFrame, so what was clicked trembled by a frame's growth. Now only a play that moves an element (transform, top, margin …) passes it over, the hold is put back as soon as a growing part is laid out (a ResizeObserver, before paint), and the button comes before what it unrolls: the rest come in under it, the rows above stay with it, and in a view with no room below the view goes down with them (scrollOnPurpose with the click), led by the fold's foot frame by frame, never past the button's top; Show less rolls them up under the button, which stays put. Tested in Playwright Chromium and WebKit with a stub panel (13 agents, Codex open, list scrolled to its end) and the window: new internal/gui/tests/panel-fold.test.cjs checks each painted frame of Show more (the view only goes down, the row above keeps with the button, the rest below it, then in sight), Show less and a row opened and closed (what was clicked within 2px); it fails on main (button moved 99px, the swinging in Chromium). All node tests and go test -tags nogui ./internal/gui/... pass; click-scroll's "the view must be at its end" also flakes on main under load
2026-09-29 16:40:03 +08:00
yetone
d924802de5
usage: a streamed reply's time to its first token is kept, and its first text's ( #196 : usage 记录首字延迟(TTFT),用于计算模型的真实生成速度) — usage.jsonl had only a call's whole ms and its out tokens, so how fast a model writes couldn't be told from how long it waited or thought. The reply is now read as it goes to the agent, in the agent's protocol whatever the vendor spoke (Anthropic content_block_delta text/thinking/input_json and a tool_use's start, Chat content/reasoning_content/reasoning/tool_calls, Responses output_text/reasoning/function_call_arguments deltas and a function call's start, Gemini parts), and each record gets ttft_ms to the first content and first_text_ms to the first text, counted from the request as ms is, so ms − ttft_ms is the writing time; a reply that isn't streamed gets neither, and omitempty leaves old records as they were. Only the stream's events up to its first text are read (a JSON reply, or past 8 MB, not at all). Each try in the routing trace keeps its own from when it was sent, so a group that fails over shows which member was slow, and the route, the request log and the Codex backend's relay keep them too. Shown on the Routing page (TTFT in the list, first token / first text / tok/s in a try's story), in the Gateway's request log, and on the Usage page and in magpie usage per model as the mean TTFT and tok/s = out ÷ (ms − ttft) over the answered streamed calls. Tested with fake vendors streaming a gap apart for Anthropic, Chat and Responses, as they came and translated (Chat from Anthropic, Responses from Chat), the ChatGPT backend without a Content-Type, a whole reply, a group whose first member fails (its try untimed, the second's timed from its own start, the request's including the failed try), each protocol's events and split writes, and the summary's sums; the GUI's browser tests pass
2026-09-29 16:40:03 +08:00
yetone
30300c40d0
providers: NVIDIA and ModelScope are presets ( #197 : 增加 NVIDIA NIM 和魔搭 ModelScope 作为供应商预设) — NVIDIA NIM at integrate.api.nvidia.com/v1 with a key from build.nvidia.com, and 魔搭's API-Inference at api-inference.modelscope.cn/v1 with a ModelScope access token (not DashScope, which the Qwen presets are), each with its models.dev catalog (nvidia, modelscope) and its logo. NVIDIA's /v1/responses answers 404 for its models but a few, so it has chat completions only; ModelScope's /v1/responses is served (401 without a token, 404 for a path it doesn't know), so it has both. Neither has an Anthropic endpoint here. An entry imported at either base URL is the preset, and the import link pages and README list them. Tested: the presets' endpoints, catalogs and import, a /v1/models answer shaped as each sends it (NVIDIA's embedding models left out, ModelScope's object "" rows kept), and catalog.FetchAt against the live integrate.api.nvidia.com (74 models) and api-inference.modelscope.cn (35) lists
2026-09-29 16:40:02 +08:00
yetone
95d71138bf
bedrock: Claude Code's betas are fitted to what Bedrock takes ( #176 : API Error: 400 Amazon Bedrock: Unexpected value(s) advanced-tool-use-2025-11-20, prompt-caching-scope-2026-01-05, redact-thinking-2026-02-12 for the anthropic-beta header) — Bedrock's runtime checks anthropic-beta by name and turns the whole request away for one it doesn't know, and magpie passed Claude Code's betas on as it sent them. Now, at Bedrock, tool search is asked by Bedrock's name for it (tool-search-tool-2025-10-19, as Claude Code asks Bedrock itself) and the two it refused are left out; and at any provider, a 400 naming betas as unexpected is asked again once without them, and they're not sent to that provider again, so a beta Claude Code adds later costs one retry, not a failed turn. An anthropic_beta list in the body (how Bedrock's InvokeModel takes betas) is fitted alike. Tested against a fake Bedrock runtime that refuses unknown betas as the real one does: the issue's betas plus an unknown one go through on the first turn with one retry and straight on the next, and body betas (fails on main)
2026-09-29 16:40:01 +08:00
Ethan Xu
81736b2fb2
subscription: Qoder is a subscription you sign in to and route throug… ( #162 )
...
* subscription: Qoder is a subscription you sign in to and route through — Qoder lists no public API, so magpie speaks the endpoint its own desktop client uses: api3.qoder.sh's agent_chat_generation SSE, wrapped in the COSY envelope the client signs with (an RSA-encrypted key and a signed request line), the body through the client's own base64 codec, and the account reached by Qoder's OAuth device flow (qoder.com -> a dt- device token -> a jt- job token that serves models and rotates from its refresh token). The protocol lives in internal/qoder (endpoints, codec, cosy, oauth, models, a credential store, and /api/v1/userinfo, asked with the device token at sign-in so the account is labelled by its email and name rather than a bare uid); provider/qoder.go is who is signed in, its models, and the sign-in; gateway/qoder.go turns a request into a Qoder body and reads the SSE back, folding tool calls and results the way the client does. It is offered as a single, risky subscription in the Providers page: Qoder has no public API for this, and may treat a request that isn't its own client's as third-party use — the tile says so and points at an account you can afford to lose. Tested: the codec, the COSY signing, the model list, the request body and a served round-trip against stubs, and live — a signed-in account answered a Pi request for qfmodel end to end. Not tested: the userinfo email/name path against a fresh real sign-in (the device token is short-lived), and multi-turn tool use against the real client's exact framing.
* subscription: Qoder uses the chosen model and keeps rotating tokens consistent
Qoder used qfmodel regardless of the caller's model, refreshed the same rotating token concurrently, and could return upstream failures as empty successful replies. Requests now use the selected account's enabled model key and complete model_config; unknown or disabled models are refused. Token checking, rotation and atomic persistence are serialized, each caller receives a separate credential, and a rotated pair is retained for a persistence retry if its disk write fails.
Accounts are kept in logins.json with multiple accounts, selection, secondary-account toggles, usage and removal, including the last account. The old qoder.json is migrated after a successful write. A sign-in generates one machine ID that the account keeps for subsequent requests; the OS and architecture follow the running platform. Login polling expires after 15 minutes.
Qoder's response path maps non-200 SSE envelope statuses to HTTP errors before output and terminal error events after streaming begins, including requests using magpie's search rounds. Tool-result headings stay beside their own results; normal EOF flushes pending text and emits a stop event even after tool calls. Thinking, effort and context settings follow the selected model (low/medium/high is the fallback when no effort levels are declared). Corrected the Chinese risk wording and account plan label, removed unused protocol fields and the unsupported China-client claim, and included ufec/CLIProxyAPI's MIT copyright and license. Codec and signature tests now have fixed expected answers instead of relying only on round trips or an absolute Windows capture path.
The shared relay in claude_subscription.go is unchanged. Its error-status propagation and handling of failures after partial non-streaming output are existing upstream issues outside this change; Qoder handles these failures in its own relayQoder response path.
Tested: Qoder, codec, COSY and shared stream-error regressions passed 20 consecutive runs, including eight concurrent requests refreshing once, persistence failure recovery, account migration and removal, sign-in and usage against local HTTP stubs, and error handling across Chat, Responses, Anthropic and Gemini. go vet ./..., the production build, JavaScript syntax checks and git diff --check pass. The complete Windows test run has the same 72 failing tests/subtests as HEAD, with no new failures; the existing Claude shell-client test still times out. Not tested against a live Qoder account. The race detector could not build because gcc is unavailable.
* subscription: Qoder keeps its rotating tokens, speaks tools in its own
API's way, and thinks at the effort each model declares
An aborted request could sign the account out. A refresh ran on the
request's own context, so one cancelled after Qoder had rotated the pair
but before its reply arrived dropped the new pair and left the spent one
saved: every later call failed with "qoder job token refresh: status 401"
until the account was signed in again. The failure wasn't recorded
either, so the account looked healthy in the app while every request
failed. Refreshing now runs on context.WithoutCancel with its own 20s
timeout, in provider/qoder.go and the device-token refresh in
qoder_usage.go, and a refresh Qoder refuses (401 or 403, either token)
marks the account lapsed: Qoder's job-token refresh returns a
JobTokenRefreshHTTPError with its status, as the device one did. Side
logins don't carry Lapsed out, so qoderLogins passes it on.
A refresh blocked every other subscription. Both held qoderMu and the
global loginsMu across the HTTP call, up to 20s — with a 2s refresh in
flight, Logins("workbuddy") blocked 1.9s. qoderMu still serializes read,
rotate and write-back so one pair is rotated once; loginsMu is taken only
to read (qoderLookup) and to write back (qoderPersist), and the write-back
re-reads logins.json, so a change made meanwhile is kept. migrateQoder
takes no lock when there's no legacy file to move, or listing Qoder's own
accounts would wait behind a refresh too.
Tool calls were carried as XML in the text with values magpie HTML-
escaped, so an argument reached the model as "x", the model took
to imitating the escaping, and a value it meant literally — "< b &&"
— was unescaped on the way back and corrupted. A call had no id while
its result said "Function result for call A". Qoder's endpoint serves
OpenAI's own tool calls: asked with a tools array it answers with
tool_calls carrying ids, and accepts assistant tool_calls plus a role:tool
turn named by tool_call_id. So they are sent and read that way now: no
XML contract in the prompt, no escaping on either side, and a result
paired with its call by the id Qoder sent. The XML splitter stays as a
fallback for a model that still writes a call in its text.
Effort was chosen from reasoning_efforts, a field no model lists, so
every model got the low/medium/high fallback: gfmodel, which offers only
high and max, was asked for medium and refused with "该模型始终思考,
不支持关闭思考", and xhigh was clamped to high. The levels and the
default come from thinking_config now — asked for nothing, or for a
level Qoder doesn't name, a model thinks at its own default; a model
that can't stop thinking thinks at its lowest when asked for none; one
that declares no levels is sent none. A model whose only thinking_config
is enabled now thinks, as it does in Qoder's own client. Upstream
failures carry the vendor's reason from details, which had been dropped.
Tested: go test ./... and go vet on the changed packages, and the darwin
and windows builds. New regressions cover a refresh outliving an aborted
request, a refused refresh lapsing the account, logins and a concurrent
write-back during a refresh in flight, tool turns paired by id, native
tools and streamed tool_calls, and effort defaults. Live, against a
signed-in account: two parallel calls and a follow-up call each got
their own result; a file written with "< b &&" kept them; gfmodel,
asked for nothing, answered at max; qfmodel's xhigh reached Qoder as
xhigh. Not tested: a refresh the upstream refuses for real, and models
the account isn't offered.
2026-09-29 16:40:20 +08:00
yetone
e4404dbd6b
agents: Kimi Code, Moonshot's kimi CLI, is an agent magpie sets up (agent求个kimi code支持) — it keeps its settings in ~/.kimi/config.toml ($KIMI_SHARE_DIR's), its default_model a key of its [models."…"] tables, each naming a [providers.…] one. Picking a model through magpie adds the provider "magpie" (the gateway, as Kimi's own chat completions type, so the thinking goes out as reasoning_effort and comes back, and is sent back, as reasoning_content) and a table per catalog model keyed "magpie/<provider>/<model>", with its context (128K when not known) and thinking/image_in capabilities, so the catalog is in Kimi's /model picker and kept up to date as providers change; the Kimi Code sign-in's default is stashed and put back on reset, and the tables go in and out in the order Kimi's own check wants (a default or a provider it can't find makes it refuse the file). Its requests are Kimi Code's in usage (KimiCLI/…). The gateway also drops Kimi's thinking: {type: …} on a request relayed to api.openai.com, as it does Qwen's enable_thinking. Tested: the config in the shape a Kimi Code sign-in leaves it (set, sync, a moved base_url, reset, its own model, no config at all), and the real kimi 1.24.0 --print against a sandbox gateway and a fake upstream: reply and thinking shown, reasoning_effort high on a thinking model, reasoning_content sent back on the next turn, usage under Kimi Code; not with a real vendor behind it
2026-09-29 16:36:35 +08:00
yetone
b95dab91d8
agents: "Show 7 more" unrolls the rows downwards (Image #22 : 点击这里为什么是向上展开) — a click holds what was clicked where it is on the screen (v0.1.377), and the button sits under the rows it opens, so as they grew above it the view scrolled to keep it put and the page rode up past them. A control marked data-unrolls is held by the part it is in instead: the rows open down and the button goes down with them. click-scroll.test.cjs checks it in Chromium and WebKit (it fails without the change)
2026-09-29 16:29:33 +08:00
yetone
9e9c2ce96c
providers: StepFun and StepFun (China) show the Step Plan's 5-hour, weekly and credit windows on the Usage page (StepFun 国外版跟国内版这两个provider都应该支持显示额度) — the platform tells them only to a sign-in on platform.stepfun.ai or .com, never to a key ("api key not permitted for this method"), and its session cookie is HttpOnly with no way back to an app, so the user signs in in their own browser and brings the session over: the provider's editor has a bookmarklet that, on the signed-in platform page, asks the platform to renew the session as the page does and copies it, and a box to paste it into (the Oasis-Token cookie from developer tools is taken too). magpie keeps it in stepfun-auth.json (0600), one per site, renews it before it runs out and once more when it is refused, and asks QueryStepPlanRateLimit and GetStepPlanStatus for a card with the plan, its windows and when it ends; an anonymous session, another site's, or one renewed as someone else (past the refresh token's 30 days) is not kept or shown. Tested with a fake StepFun platform (renewal, early expiry, another account, pastes of each kind), the bookmarklet run against a stubbed page, and the field in a sandbox magpie web (signed out, a bad paste, signed in, sign out); not with a real StepFun sign-in
2026-09-29 16:04:47 +08:00
yetone
f770ad2aef
gateway: a conversation moved from Grok to Codex's GPT on a ChatGPT sign-in goes on (waroy: The encrypted content for item rs_ could not be verified … routing grok > codex GPT) — the ChatGPT backend streams its refusal of Grok's sealed reasoning as response.failed, after a codex.rate_limits event of its own; magpie held a stream only through the events it knew come before a reply, took codex.rate_limits for content and let the stream through, so the refusal went to Codex rather than being asked again without the sealed reasoning, and nothing was noted to leave it out of later turns. codex.* events now count as coming before the reply, so the refusal is caught, the member asked again without the reasoning, and what it refused left out up front on later turns. (The id reads rs_ as magpie sends the ChatGPT backend no item ids.) Tested with a fake Grok member and a fake ChatGPT backend in a group, refusing as a 400 and in the stream after codex.rate_limits (the stream case fails on main)
2026-09-29 15:28:08 +08:00
yetone
4216d2b338
redact: a gateway's key in "key": "oc_sk_…" is masked, and Settings → Privacy takes the user's own masking rules ( #195 ) — an OpenCode Go key in providers.json went to the vendor as it was: the API key rules wanted sk- with a hyphen, and a field called key alone was none of the secret field's names. Now a key of a gateway or relay, two to four letters and _sk_ (oc_sk_, or_sk_) with digits in it, is an API_KEY wherever it is, and a field named key and nothing more (not sort_key, monkey or obj.key) is a SECRET when its value is 16 or more letters and digits both, so a map's "key": "value" and key = "someIdentifier" are left alone; \"key\": \"…\" in JSON inside a string is found too. The secret field's names and its markers come from one list, so they can't drift apart, and of two rules finding the same value the first keeps it, so its placeholder no longer changes between an API_KEY and a SECRET from one request to the next. Masking rules, next to 脱敏词: a name for the placeholders (upper case letters, digits and _, 24 at most) and a prefix (then 8 or more key characters) or an RE2 regular expression, group 1 the value if it has one, masked with the secrets while 脱敏密钥 is on and put back as the others are, the user's name winning where one of magpie's finds the same value. They are set on their own: a pattern that doesn't compile, matches nothing at all, or is too long (300), a prefix under 3 or over 64, both at once, or more than 32 rules, is refused with the reason under the add row and what was typed kept. Tested: every row of the issue's table and the false positives, the rules' tidying, errors, stable placeholders, restore, overlaps, and kinds with digits in a stream; the settings file and the gateway's count_tokens with a rule; the page in a stub magpie web (add, a bad regex, remove, zh dark)
2026-09-29 15:26:01 +08:00
yetone
04fb5cbf92
providers: StepFun and StepFun (China) show what is left on the account (StepFun 国外版跟国内版这两个provider都应该支持显示额度) — both answer a key, a Step Plan one too, with the account at /v1/accounts (api.stepfun.ai in dollars, api.stepfun.com in yuan), whose balance is what is left to spend, vouchers included; it is now a card on the Usage page beside the others. Checked against the live api.stepfun.ai with a Step Plan key, on the plan and the pay-as-you-go address. The Step Plan windows are told only to a platform sign-in, not to a key
2026-09-29 15:23:14 +08:00
yetone
4f5699aae5
providers: Kiro and Devin keep several accounts, as the other subscriptions do (Kiro要支持增加多个账号…Devin也要能够增加多个账号) — both only borrowed their CLI's one sign-in (Devin wrote devin's credentials.toml, Kiro kept one kiro-auth-token.json), so signing in again replaced the account. The CLI's or IDE's own account is now read as it is and each further account gets a home of magpie's: Kiro's token file is kept and refreshed there, and Devin's credentials.toml sits in a data folder the devin CLI is run with (XDG_DATA_HOME / APPDATA), so devin auth status and models list answer for it. They are listed, switched, ticked on and forgotten like the others, and the gateway moves on to the next when one is out; the single Kiro sign-in kept before is moved into a home of its own. Tested with a fake Kiro and a fake devin CLI (second account, signing in again, switching, forgetting); not with real accounts
2026-09-29 15:20:22 +08:00
yetone
31f2fc8ed4
bedrock: GPT models are asked a reply's length as max_completion_tokens, and Claude Code's metadata is left out ( #176 : 400 Unsupported parameter: 'max_tokens' is not supported with this model; Request metadata contains a value that violates the regular expression) — Bedrock's OpenAI endpoint turns max_tokens away for its GPT models, so the endpoint test and every chat request to it (sent on as it came or built from another API) now ask max_completion_tokens; and Claude Code's metadata.user_id, a JSON string now, is not the plain id Bedrock's Anthropic endpoint checks it against, so it is dropped there. Tested against a fake Bedrock runtime (both fail on main)
2026-09-29 15:12:44 +08:00
yetone
12d79dd9f5
providers: Baidu Qianfan Token Plan lists no kimi-k2.6 ( #193 follow-up) — Baidu's Token Plan 个人版 page takes Kimi-K2.6 (and DeepSeek-V4-Flash, already left out) offline 2026-09-29, the day the preset landed, so it was dropped from the models the preset lists; it can still be typed in by hand while it answers. gofmt on the preset and its gateway test
2026-09-29 14:38:54 +08:00
yetone
a284b61bb1
providers: an image model's Test models draws one small picture on the images API, as the gateway draws with it (mamba: 图像模型点测试模型是不是发送的请求不对啊,总是提示响应异常) — every model picked was sent a chat completion ("hi", max_tokens 16), so gpt-image, flux, seedream and the like, which a vendor serves only at /images/generations, were turned away and always showed as not answering. Now a model the gateway draws with on an images API (catalog.ImagesAPI, the list viaFor used, shared) is asked there for one picture of "a dot", n=1, at gpt-image's low quality and the vendor's default size, waiting up to two minutes (the request up to three); a 404 or 405 there is tried in chat as the gateway does. Chat models and gemini-*-image / gpt-5-image (which draw in chat) are asked as before, and the vendor-wide Test asks a chat model ahead of one that draws. Tested with a fake vendor that serves images only at /images/generations and chat only at /chat/completions
2026-09-29 14:37:09 +08:00
Austie
6ba83dbbbb
providers: add the Baidu Qianfan Token Plan (个人版) preset ( #193 )
2026-09-29 14:37:24 +08:00
Ns2Kracy
62ded971c0
codex: retry unused 5-hour windows after warm-up ( #192 )
...
* codex: retry warm-ups for unused five-hour windows
Keep the idle retry schedule when Codex reports zero usage and a reset a full window away. A successful warm-up request can leave the window unstarted, and a fresh reset_after_seconds on each read otherwise makes the saved reset drift indefinitely. Retry with the existing backoff until the reset begins counting down.\n\nCover the relative reset field, retries at 15 and 45 minutes, stopping once the window runs, and warming the next reset.
* test: cover a used window after Codex warm-up
Confirm the retry schedule stops both when an unused window starts counting down and when subsequent usage becomes nonzero, before the next reset triggers another warm-up.
2026-09-29 14:29:06 +08:00
yetone
3c02cbd5c9
routing: Stop replay and the speed answer a click in the app, and the clock's fast-forward reads as that (Discord: willz — 停止重放无效, 1× 速度一会儿 4x 一会儿 8x) — the replay's bar wrote its buttons' text every frame, and WebKit sends no click to a button whose text was replaced between the press and the release, so in magpie's window Stop and the speed did nothing and the replay ran to its end. Now each is written only when it changes. The N× beside them was the clock running fast through a quiet stretch cut short, not the speed: it now says so (跳过空闲时段 · 时钟 N×). Checked in WebKit against a sandbox magpie web: before, mousedown and mouseup reach Stop but no click and the replay goes on; after, it stops
2026-09-29 14:24:18 +08:00
yetone
bd71a587ef
routing: a failed try shows what its vendor said, and the errors count opens the newest failed request (Discord: 蓝猫, Devin 502 — Agent 收到的错误 did nothing when clicked) — the trace kept each try's error word for word, but the page only told magpie's reading of it (403 · failed), so the vendor's own reason was nowhere to be seen. Now it is under that try, and clicking the errors count picks the latest request whose agent got an error. Checked in a sandbox magpie web with a fake vendor answering 403
2026-09-29 14:08:03 +08:00
yetone
17ad8f9332
agent: a goose on PATH that is pressly's migration tool isn't taken for Goose (Discord: Jun, Goose listed with no ~/.config/goose) — any goose on PATH counted as the agent, and the Go database migration tool of that name is common among Go developers. Now a goose on PATH counts only when it isn't a Go program (Block's goose is Rust); goose's config dir still counts as before
2026-09-29 14:04:32 +08:00
yetone
38cbbaaf01
devin: fast runs are one model with an effort, and the Devin agent's own picker is families + effort too (蓝猫 on Discord: Fast 模型还是存在同样的问题,给 Agent 选择模型的时候还是同样显示) — v0.1.371 made each family one model with its efforts, but a variant with a tier after its effort (claude-opus-5-5-high-fast, gpt-6-sol-high-priority) still stood alone, one per level, and the Devin agent's model field still listed every variant flat. Now a family's fast (and priority) variants are one model, claude-opus-5-5-fast, whose effort the gateway turns into the variant at it (nearest level; the family's default with none asked), and ids ending -none join their family at none. The Devin agent offers the same families with an effort field beside the model; Devin's config has only agent.model, so the pair is written as the variant at the effort (the family's own id with none, so it keeps following the family) and read back apart. Ids an agent was set to before, and one typed in, go to Devin as they are; with Devin's list unreadable the id is read and written as it is. Checked against the real devin models list (85 models, the Opus fast runs low–max) and with tests of the agent's get/set on a temp config
2026-09-29 13:55:56 +08:00
yetone
ba9f0c610d
routing: a group's member may be a model at an effort of its own, provider/model:low ( #189 , @luw2007: model + effort 才是完整的组合项) — a member was only a model, reasoning as the agent asked or as the group's classifier picked, so "the same model, cheap and quick" and "the same model, thinking hard" could not both be in a group, nor could a rule send to one of them. A member now may carry an effort (none … max): a request routed to it is asked for that much reasoning whatever the agent asked or effort=auto picked, at the level the model has nearest, while the vendor is asked for the model's own id. The same model at two efforts is two members that rules (use=m:high), stickiness and the trace tell apart; an id whose own last part is a colon (a :free, :7b, :0) stays the model's. The group's reasoning levels are those of its members that follow the agent, and the editor has a reasoning button on each member. Tested with a fake provider through a sandbox magpie: the upstream bodies carry the fixed effort in Chat, Responses and Anthropic shapes, and the routing view shows m1:low / m1:high as two seats, light and dark
2026-09-29 13:45:29 +08:00