Files
magpie/internal/gateway
yetone 23eed59f7a gateway: a group's auto effort keeps a conversation's reasoning while its cache is warm, going up but not down (#617: Kenoia — with group/core effort "auto", 40 of 42 effort changes between a Pi session's turns on gpt-6.1-sol read 0 cached tokens, against 4 of 834 turns without one)
A group with effort "auto" asks its decision model (Jev or a model asked in words) how hard each user turn is as it begins, and the turn's requests ask for that level. Within a turn it holds, but from one turn to the next it moved freely (xhigh → high → xhigh …), and the reasoning effort is part of what the vendor caches a conversation under: a turn asking for another level reads none of the cache back. Kenoia's production data shows it: 237,632 uncached input tokens on a 26 s xhigh → high switch that had just read 230,912 from cache. The agent kept its own effort fixed; magpie changed it.

Now a new turn keeps the conversation's level when the decision model picks a lower one while the cache is warm (keepsEffort): a higher pick is still taken at once, so a hard turn gets the reasoning it needs and a conversation pays for at most one miss per level. A lower pick is taken on a turn whose cache is lost anyway: one after cacheCold (5 min) with neither a request nor an answer (ruleAnswered now keeps when the vendor last answered), or one after the agent compacted the conversation (under half as long as its last request). Tool-result rounds keep their turn's level as before. The trace's rule shows the level kept (pick) and the one the decision model wanted (wanted, beside classified.effort). Where the change would go as a configuration_update that keeps the cache (the MAGPIE_EFFORT_UPDATES preview: Responses, a GPT-6 model, a ChatGPT account or an OpenAI key), the wanted level is sent and the update carries it, so with the preview on auto still follows every pick.

TestAutoEffortKeepsTheCache (high, xhigh up at once, high held at xhigh on the turn and its tool rounds, medium taken after the cache went cold) fails on the old code (the held turn sent high); TestKeepsEffort covers warm, cold, answered since, compacted, up, same and no pick; TestAutoEffortGoesAsUpdate (a group/core of codex/gpt-6-luna, MAGPIE_EFFORT_UPDATES=on, Responses) sends xhigh at the top and u:high before the next user message. go vet, builds (darwin, GOOS=linux, GOOS=windows) and go test -tags nogui -count=1 ./... under a temp HOME pass. Not tried against a real account.
2026-10-03 20:54:46 +08:00
..
providers: each account of a subscription, and each key, can be kept for some of the provider's models only (#474: 增加账号级别的模型选项, a low-quota but verified account kept for the cheap models) — every account or key a provider had served every model it did, so one couldn't be spared for some. An account's or key's row in its provider's editor now has a models badge: a click opens the provider's models as chips, Save keeps the account for those picked, All models gives it every one again; one kept for some says how many, always shown. magpie provider account-models <id> [account|key [models…|all]] does the same and lists them. It is kept in providers.json as accountModels, an account by its name in lower case, a key by its id; one not in it serves every model, as before. The gateway never sends a model to an account or key whose list leaves it out, in every routing mode, in drag order, in a group and as a fallback, and never falls back to it all the same; the trace says it was left out as set to serve other models. When every account or key there is leaves the model out, the request is refused with a 403 saying so and how to change it, not "none of X's models is ready". TestKeyModelsNarrowCandidates, TestAccountModelsNeverChosen, TestAccountModelsCmd and account-models.test.cjs fail without the change. go vet, go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui ./... pass; account-models, click-scroll, old-webkit, account-arrange, account-proxy, account-return, routing-account-order, panel-routing, unlisted-models pass.
2026-10-02 05:50:23 +08:00
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
usage: each subscription account's tokens and cost, by the account that answered (#557: 希望用量账本能按订阅账号统计,每个 Codex / Claude 账号各用了多少 token、多少钱) — a call's record named the upstream key it went out on (ProviderKeyID/Name, #500) but never the subscription account, so with two Codex accounts signed in, or a Claude account that took over after a failover, the ledger could say what Codex cost and not what each account did. A record now carries providerAccount, the account that answered as the Routing trace names it (Account.User: its email or login, never a token), set by the gateway at the try that went out (the candidate loop's last try, so a failover's answer is the account that took over and the refused try stays the first account's; an X-Magpie-Account pin is the pinned one), and on the decide, draw and video paths too. Record.Account() gives it, or for a record written before the field the account its host named (Where() has long written "chatgpt.com as dee@example.com" for an account), and otherwise "" — shown as "account not recorded", never inferred from today's sign-in. The summary groups calls by provider and account (Summary.Accounts, the same email on Codex and on Claude being two accounts), priced as every other group; the ledger's Filter has Account, the search finds a call by its account, the CSV has a provider_account column beside provider_key_name (the provider's identity columns together, the caller's still last), and the Requests page's packed rows keep it and offer the period's accounts. magpie usage prints an accounts table after the provider keys, and magpie usage --csv --account <name> keeps one account's calls. The app's Usage page lists Accounts on the Overview (tokens and cost per account, the provider under its name, an unrecorded one in the faint colour, no stripe down a row's side) and has an Account filter on Requests, each account noting the providers it answered for, which asks for and exports that account's calls without moving the page. OTLP export is an allowlist and does not carry the account. TestUsageNamesPinnedAccount (both pinned accounts on magpie's endpoint and on Codex's own, the trace's Who and the record agreeing, no token in usage.jsonl) and TestUsageNamesAccountThatTookOver (Ordered routing, the first account refusing: the answer recorded as the spare, the refusal as the first) fail without the gateway change; TestSummaryByAccount, TestLedgerByAccount and TestRequestPageByAccount cover the grouping, the host fallback, "not recorded", keyed providers kept out, the filter, the search, the CSV column and the packed rows; TestLedger and TestLedgerWithSessionLogCalls take the new column. usage-accounts.test.cjs (WebKit and Chromium, en and zh: the Overview's accounts and costs, the Account picker's notes, its request and export, All accounts clearing it, scrollTop unchanged) fails without the asset change; api-key-usage, click-scroll, panel-usage, request-archive-usage, tray-usages, claude-usage-asked and every usage-* test pass in WebKit. go vet ./internal/..., go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui -count=1 ./... under a temp HOME pass (TestFXInState failed once on its TempDir cleanup, passes alone).
2026-10-02 20:15:33 +08:00
gateway: a Claude that thinks only adaptively is never sent a thinking budget, whoever wrote it (Keenc on Discord: claude opus 5.5 已经不支持旧的 thinking.type.enabled 了,必须用 thinking.type.adaptive + output_config.effort,magpie 还在按老格式发参数,会导致 API Error: 400) — magpie turned thinking adaptive only where it built an Anthropic request itself, from the model's name in magpie. An Anthropic client's own thinking {type: enabled, budget_tokens} was relayed as it came, so an agent that doesn't know the model (asked by a relay's or magpie's name for it) got the vendor's 400 "claude-opus-5-5 requires adaptive thinking"; and a model named otherwise in magpie than at the vendor (an upstream name, e.g. "opus" for claude-opus-5-5[1m]) was built with a budget, the check reading magpie's name before the vendor's went in. Now forwardOnce, which every request to an Anthropic endpoint goes through (relayed, built, a group's effort fitted, count_tokens), reads the model the body goes with and, for one from 4.6 on, sends thinking {type: adaptive} (its display kept) with the budget as the effort it is nearest in output_config.effort (<=4096 low, <=12000 medium, <=24000 high, above max, as buildAnthropic asks), unless the request names an effort already; "disabled" and adaptive go as sent, and older Claudes (4.5 and before) keep enabled with their budget. claudeVersion also reads opus-5.5 without "claude-" and the old order claude-N-N-family, and caps the major at two digits so a dated id is never taken for a version. TestAdaptiveOnlyModelsNeverGetABudget (a fake Anthropic upstream answering 400 to enabled for 5.5: a relayed budget, Bedrock's spelling, an effort beside a budget, a name mapped after, Chat's reasoning_effort onto a mapped name, disabled kept, Sonnet 4.5 kept on its budget) fails on the old code and passes. Against the owner's real relay in a sandbox gateway under a temp HOME: yylx/claude-opus-5-5 with thinking enabled, budget 10000 answered the 400 with the old build and 391 with this one; budget 31999 (effort max) and Chat's reasoning_effort high answered too. AiHubMix and OpenRouter took enabled on the old build already (they convert it themselves). go vet, builds (darwin, GOOS=linux, GOOS=windows) and go test -tags nogui -count=1 ./... under a temp HOME pass but TestCodexVersion, a timing test that passes alone.
2026-10-03 20:51:25 +08:00
gateway: a Claude that thinks only adaptively is never sent a thinking budget, whoever wrote it (Keenc on Discord: claude opus 5.5 已经不支持旧的 thinking.type.enabled 了,必须用 thinking.type.adaptive + output_config.effort,magpie 还在按老格式发参数,会导致 API Error: 400) — magpie turned thinking adaptive only where it built an Anthropic request itself, from the model's name in magpie. An Anthropic client's own thinking {type: enabled, budget_tokens} was relayed as it came, so an agent that doesn't know the model (asked by a relay's or magpie's name for it) got the vendor's 400 "claude-opus-5-5 requires adaptive thinking"; and a model named otherwise in magpie than at the vendor (an upstream name, e.g. "opus" for claude-opus-5-5[1m]) was built with a budget, the check reading magpie's name before the vendor's went in. Now forwardOnce, which every request to an Anthropic endpoint goes through (relayed, built, a group's effort fitted, count_tokens), reads the model the body goes with and, for one from 4.6 on, sends thinking {type: adaptive} (its display kept) with the budget as the effort it is nearest in output_config.effort (<=4096 low, <=12000 medium, <=24000 high, above max, as buildAnthropic asks), unless the request names an effort already; "disabled" and adaptive go as sent, and older Claudes (4.5 and before) keep enabled with their budget. claudeVersion also reads opus-5.5 without "claude-" and the old order claude-N-N-family, and caps the major at two digits so a dated id is never taken for a version. TestAdaptiveOnlyModelsNeverGetABudget (a fake Anthropic upstream answering 400 to enabled for 5.5: a relayed budget, Bedrock's spelling, an effort beside a budget, a name mapped after, Chat's reasoning_effort onto a mapped name, disabled kept, Sonnet 4.5 kept on its budget) fails on the old code and passes. Against the owner's real relay in a sandbox gateway under a temp HOME: yylx/claude-opus-5-5 with thinking enabled, budget 10000 answered the 400 with the old build and 391 with this one; budget 31999 (effort max) and Chat's reasoning_effort high answered too. AiHubMix and OpenRouter took enabled on the old build already (they convert it themselves). go vet, builds (darwin, GOOS=linux, GOOS=windows) and go test -tags nogui -count=1 ./... under a temp HOME pass but TestCodexVersion, a timing test that passes alone.
2026-10-03 20:51:25 +08:00
codex: a group's effort for the turn keeps Codex's reasoning.context, and Codex's custom tools (code mode's exec, apply_patch) reach a model magpie translates for (#534, #520 follow-up, azir12345: since v0.1.637 a Plus account's gpt-6.1-sol fails with 400 "X-OpenAI-Internal-Codex-Responses-Lite requires reasoning.context to be all_turns", and Codex said it had only the collaboration and wait tools, no terminal) — ChatGPT's catalog marks gpt-6.1-sol (as gpt-6-sol, gpt-6-luna, gpt-5.6-*) use_responses_lite with tool_mode code_mode_only, and Codex asks for the bare slug with its native metadata: the X-OpenAI-Internal-Codex-Responses-Lite header (add_responses_lite_header, which magpie forwards with the other x-openai-* headers), reasoning {"effort","summary","context":"all_turns"} (build_reasoning), and its tools moved into an additional_tools item, every other tool reached through exec, a custom (freeform, lark grammar) tool in the "functions" namespace, beside wait and the collaboration namespace. magpie routes the bare id to the group auto-gpt-6-1-sol, and the effort the group picks for the turn went through withEffort, whose Responses branch rebuilt reasoning as {effort, summary} and dropped context, so ChatGPT refused the Lite request; it now sets only the effort (withBodyEffort), keeping every other field of reasoning as Codex sent it. A member served by translation (Chat, Anthropic, a Responses plugin) had parseResponses drop custom tools and ignore custom_tool_call / custom_tool_call_output items, so exec vanished and the model saw only wait and the collaboration tools; a custom tool (top-level or in a namespace) is now offered as a function taking one string "input", its description its own plus a note on input and its grammar, an earlier custom_tool_call goes to the model as a call with {"input":…} and its output as the call's result, and the model's call goes back to Codex as the custom_tool_call it runs (name and namespace as Codex knows them, input from the "input" argument, else the arguments as given), streamed without function_call_arguments events and in the non-streamed reply alike. This also gives translated models Codex's freeform apply_patch, which was dropped silently before. The Lite header is still forwarded as it was; nothing showed it needs dropping. Codex's base_instructions and identity line are untouched. No request was made to ChatGPT. TestWithEffort's Lite case fails without the decide.go change ("want \"context\":\"all_turns\""); TestCodexCustomToolsTranslated (a code-mode Lite turn through a fake Chat provider: exec offered with input and its grammar, the earlier call and output shown, the model's exec call back as a custom_tool_call with its input and no arguments) and TestCodexCustomToolNamespaced (namespaced and non-JSON calls, renderResponses) cover the tools; TestAdditionalToolsTranslated, TestNamespacedToolsOffered and TestGrokPluginGetsNamespacedTools now expect the custom tools offered. go vet ./internal/..., go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui -count=1 ./... under a temp HOME pass (TestDevinSignedInOnlyInMagpie failed once on its wait, passes alone; untouched).
2026-10-02 15:59:14 +08:00
gateway: a conversation stays with the account that answered it across a restart of magpie, and routing goes by each account's last known allowance until the first reading since the start is back (vincentzhang on Discord: in smart routing a restart moved a session to the first account, an update and restart back to the second, and the prompt cache was read by neither) — who answered a conversation last was kept in memory only, so after a restart every conversation was "first" and went to whoever routing put first; and until the accounts' allowances were read again (a first wait of 3s, the read itself up to 30s) none was known, every account counted as unused and the first in order went first, until the reading came back and put the one renewing soonest first again. Who answered is now also kept in affinity.json next to the providers (0600, written atomically, the latest 512 conversations, none older than the 24h a conversation is remembered), read again when a conversation isn't known here and the file changed — after a restart, or written by the magpie handing over. The rules for when a conversation moves are unchanged: within a turn it stays, across turns while the vendor read enough from its cache within the last five minutes, and never to an account resting, all but used up or gone. While the first reading is out, Allowances returns each account's last reading kept in quotas.json, its windows past their reset counted empty. Prompts and headers are unchanged. TestAffinitySurvivesRestart and TestAllowancesBeforeFirstReading fail without the change; TestAffinityFileBounded keeps the file bounded. go vet, go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui ./... pass (TestFetchNewLeavesListedPlugins and TestFXInState flake under load on main too, pass alone).
2026-10-01 20:09:25 +08:00
routing: In order keeps sending to the first account until it is used up or refused, and a tick keeps Codex or Claude Code signed in to the first account (#530: an account In order at 98% of its week, its five hours untouched, stopped getting requests; #524: "不允许切换首选账号的登录状态", the agent's own sign-in must stay on the account made first while the gateway still spreads requests) — since #209 magpie signs Codex and Claude Code in to the next ticked account with room once the one they are on reaches 98% (provider.SpentShare, the share Smart counts an account spent at), and since #408 back once the first is under 90%. The account the agent is signed in to is the one every routing tries first (the others stand behind it, AlsoOn), so In order the 98% switch put the second account ahead of the first, which is what #530 saw as the first being dropped; and the gateway counted an account at 98% as full whatever the routing, so a passing failure on it (a 502) rested it until its week renewed (39 hours there), not for the usual backoff. Now SpentShareOf gives 100 In order and 98 otherwise: the login switch moves the agent off an account In order only once a window of it is used up, the gateway's full (the rest after a failure, the quota rest's window) and affinity's "spent" go by the same share, and a vendor's 429 or usage-limit refusal still rests the account until it says it's back, as Smart does; a window used up is out, as before. Smart, In turn and Least used are unchanged. For #524, the subscription keeps KeepLogin (providers.json, beside Routing, kept across an editor Save; POST provider/keeplogin, only for Codex and Claude Code): with it on, magpie never moves the agent off the account it is signed in to, however used up, while the gateway still routes requests through magpie to the other ticked accounts; a pending return to the first (#408) still happens, that being the first. The tick sits at the foot of the Accounts list, under the rows where Make first is, shown only for Codex and Claude Code with more than one account on: "Keep {agent} signed in to the first account" / "{agent} 始终登录首选账号", its tooltip saying requests through magpie still go to the other ticked accounts. The Routing note follows it: kept, it says the agent stays signed in to the first whatever it has left; In order, that magpie moves it on once it is used up; otherwise the 98% wording as before. TestCodexLoginOrderedAndKept (In order stays at 98% and moves at 100%, back once renewed; Smart moves at 98%; kept, it stays at 100% and the setting survives a SetRouting; let go, it moves) and TestOrderedKeepsAnAccountAt98 (In order an account at 98% isn't full and a 502 rests it by backoff, Smart rests it to its week, a refusal rests it either way) fail without the change. keep-login.test.cjs (en and zh, Chromium and WebKit: the tick, its tooltip and Chinese strings, the post, the note when kept and In order, no tick with one account, the page doesn't move) fails without the UI and passes with it (12/12, Playwright 1.61); account-return, routing-account-order and account-arrange pass; click-scroll fails in Chromium here on origin/main too and passes in WebKit. No request was made to any vendor and no live agent config was touched. go vet ./internal/..., go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui ./... pass (TestClaudeRunEndsWhenTheClientLeaves, TestClaudeAccountNamedAsItsLogin and TestClaudeIdentityServedWhileAsked failed once under the full run and pass alone). Turned on after magpie has moved the agent off the first, it goes back to it at once rather than once that has room again.
2026-10-02 15:48:16 +08:00
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
gateway: a Claude that thinks only adaptively is never sent a thinking budget, whoever wrote it (Keenc on Discord: claude opus 5.5 已经不支持旧的 thinking.type.enabled 了,必须用 thinking.type.adaptive + output_config.effort,magpie 还在按老格式发参数,会导致 API Error: 400) — magpie turned thinking adaptive only where it built an Anthropic request itself, from the model's name in magpie. An Anthropic client's own thinking {type: enabled, budget_tokens} was relayed as it came, so an agent that doesn't know the model (asked by a relay's or magpie's name for it) got the vendor's 400 "claude-opus-5-5 requires adaptive thinking"; and a model named otherwise in magpie than at the vendor (an upstream name, e.g. "opus" for claude-opus-5-5[1m]) was built with a budget, the check reading magpie's name before the vendor's went in. Now forwardOnce, which every request to an Anthropic endpoint goes through (relayed, built, a group's effort fitted, count_tokens), reads the model the body goes with and, for one from 4.6 on, sends thinking {type: adaptive} (its display kept) with the budget as the effort it is nearest in output_config.effort (<=4096 low, <=12000 medium, <=24000 high, above max, as buildAnthropic asks), unless the request names an effort already; "disabled" and adaptive go as sent, and older Claudes (4.5 and before) keep enabled with their budget. claudeVersion also reads opus-5.5 without "claude-" and the old order claude-N-N-family, and caps the major at two digits so a dated id is never taken for a version. TestAdaptiveOnlyModelsNeverGetABudget (a fake Anthropic upstream answering 400 to enabled for 5.5: a relayed budget, Bedrock's spelling, an effort beside a budget, a name mapped after, Chat's reasoning_effort onto a mapped name, disabled kept, Sonnet 4.5 kept on its budget) fails on the old code and passes. Against the owner's real relay in a sandbox gateway under a temp HOME: yylx/claude-opus-5-5 with thinking enabled, budget 10000 answered the 400 with the old build and 391 with this one; budget 31999 (effort max) and Chat's reasoning_effort high answered too. AiHubMix and OpenRouter took enabled on the old build already (they convert it themselves). go vet, builds (darwin, GOOS=linux, GOOS=windows) and go test -tags nogui -count=1 ./... under a temp HOME pass but TestCodexVersion, a timing test that passes alone.
2026-10-03 20:51:25 +08:00
archive: a request the archive kept is opened and downloaded from the Usage page, with its bodies whole (jorben, #447: 请求存档 reachable from usage history, full bodies, download) — the archive's name was only on the Gateway page's recent calls, gone once a call left the list or the gateway restarted, and the archive kept the first 256 KB of each body, the recent list's cut. The usage ledger's row now names the call's archive (archive in usage.jsonl, kept through the Requests page's packed rows), and its details on Usage → Requests show the Request archive panel, Fetch from archive and Download; with the archive on, a request it has no copy of says "Not archived". The bodies are kept whole up to requestArchiveMaxMB in settings.json (32 MB unless set, at most 1024): the response is teed to a temp file as it is sent, the request body written to one when the call ends, so the upload queue holds file handles, not bodies, and one upload at a time reads them; a body past the limit is kept to it and marked cut, with its whole size. What goes upstream is unchanged, the archive stays off unless turned on, headers and bodies are scrubbed as before, and the temp files are removed after the upload or when the queue is full. The page is sent a body to show only when it is 256 KB or less (a longer one is said by its size), an archive over 80 MB is only said to be; Download streams the object whole, to Downloads in the app (archive/export, -2, -3… for a name taken) and as the browser's download in magpie web (archive/file). TestArchiveWholeBodies, TestArchiveLimit, TestRowKeepsArchive and TestArchivePreviewAndDownload fail without the change; TestArchiveOffUsage, TestArchiveTooLargeToPreview, TestArchiveOldPreview keep the rest. go vet, go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui ./... pass; request-archive-usage (new), request-archive, click-scroll, old-webkit, usage-ledger, usage-ledger-detail, usage-ledger-content, web-page, sse-body pass.
2026-10-02 00:57:51 +08:00
archive: a request the archive kept is opened and downloaded from the Usage page, with its bodies whole (jorben, #447: 请求存档 reachable from usage history, full bodies, download) — the archive's name was only on the Gateway page's recent calls, gone once a call left the list or the gateway restarted, and the archive kept the first 256 KB of each body, the recent list's cut. The usage ledger's row now names the call's archive (archive in usage.jsonl, kept through the Requests page's packed rows), and its details on Usage → Requests show the Request archive panel, Fetch from archive and Download; with the archive on, a request it has no copy of says "Not archived". The bodies are kept whole up to requestArchiveMaxMB in settings.json (32 MB unless set, at most 1024): the response is teed to a temp file as it is sent, the request body written to one when the call ends, so the upload queue holds file handles, not bodies, and one upload at a time reads them; a body past the limit is kept to it and marked cut, with its whole size. What goes upstream is unchanged, the archive stays off unless turned on, headers and bodies are scrubbed as before, and the temp files are removed after the upload or when the queue is full. The page is sent a body to show only when it is 256 KB or less (a longer one is said by its size), an archive over 80 MB is only said to be; Download streams the object whole, to Downloads in the app (archive/export, -2, -3… for a name taken) and as the browser's download in magpie web (archive/file). TestArchiveWholeBodies, TestArchiveLimit, TestRowKeepsArchive and TestArchivePreviewAndDownload fail without the change; TestArchiveOffUsage, TestArchiveTooLargeToPreview, TestArchiveOldPreview keep the rest. go vet, go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui ./... pass; request-archive-usage (new), request-archive, click-scroll, old-webkit, usage-ledger, usage-ledger-detail, usage-ledger-content, web-page, sse-body pass.
2026-10-02 00:57:51 +08:00
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
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
update: a restart to update waits until the gateway is idle instead of cutting off what the agents have in flight (#577: 正式版更新时保持网关请求与工具链不中断 — a Codex reply streaming through magpie, or a tool chain under way, was cut off by Restart to update) — the app serves the gateway in its own process and a restart to update is install, quit, relaunch, so whatever was in flight ended with it: a stream mid-reply, and a Claude Code turn of the subscription bridge waiting on its caller's tool results, whose Claude Code lives only in this process. The gateway now says what a restart would cut short (Server.Busy): its requests in flight (all but GETs and the bridge's own MCP callbacks), the bridge's runs parked on tool results, and when the last request ended. Restart to update (Settings → Version, the header's Update pill, the tray's item) restarts at once only when nothing is in flight, no turn waits on tools and nothing ended in the last 10 s (a tool chain's gap, the agent running a tool between two requests); otherwise it waits, saying what for: Settings shows "restarts once the gateway is idle · 2 in flight · 1 awaiting tool results" (网关空闲后自动重启 · 2 个请求进行中 · 1 个工具调用待返回) with Restart now and Cancel, the pill reads "Waiting to update" (等待更新) and its next click (not a double click's second half) restarts now, the tray item becomes "Restart Now to Update (3 in flight)" (立即重启以更新(3 个进行中)). The wait asks the feed again once idle, so versions out meanwhile collapse into the newest, downloaded before the restart; it gives up after an hour, saying so, the update then going in at the next restart or quit. A gateway served by another magpie (magpie serve, an older one) is not this one's to wait for. Handing the gateway over to the new version instead (the dev handover, e0c6892) is not used for releases: it rests on SO_REUSEPORT, which Windows lacks, and the new app would have to start beside the old one (single-instance, the Mac's bundle swapped under it). TestBusy (a stream counts until it ends, a GET never, a parked bridge run until it is answered or ends), TestRestartToUpdateWaitsForTheGateway (fails without the change: the click restarted with 2 requests in flight), TestRestartWhenIdle (not while streaming, parked or in a tool chain's gap; then once; an hour's wait gives up), TestRestartWhenIdleIntoTheNewest (0.1.11 staged, 0.1.12 and 0.1.13 out while waiting: restarted into 0.1.13) and TestTrayRestartNow are new; update-wait.test.cjs (WebKit, en and zh: what is in flight, waiting with Restart now and Cancel, counts following the gateway, the hour's give-up, the pill's wait and restart now, an idle click restarting at once, no click scrolling, no left border) fails without the change and passes with it. update-hide, update-check, sync-update, update-auto and click-scroll pass in WebKit. go vet ./internal/..., go build (darwin GUI), go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui -count=1 ./... under a temp HOME pass (TestFXInState's known TempDir cleanup flake aside; passes on rerun).
2026-10-02 23:36:43 +08:00
library: a remote MCP server signed in once in magpie is used by every agent without signing in again (#615) — a streamable-HTTP server behind OAuth (Neon, Notion, Supabase) had each agent run its own sign-in. The server's editor now has Sign in: magpie finds the authorization server from the server's 401 (RFC 9728 protected-resource metadata, RFC 8414/OIDC metadata, else the 2025-03 /authorize, /token, /register at its origin), registers itself (RFC 7591, a 127.0.0.1 redirect), opens the browser with PKCE S256 and the resource, and keeps the tokens in mcp-signins.json beside its other account secrets (0600, never the keychain). While signed in, the agents are given the gateway's /mcp/<name> for that server, without the Authorization the library has for it; the gateway passes POST (JSON or event stream), the GET stream and DELETE on with the Mcp-Session-Id and magpie's token, renews it 2 minutes before it runs out and once on a 401, one renewal per server at a time, and a sign-in the server won't renew is answered 502 "sign in to it again" (never a 401 the agent would act on). Signing out, a new URL or an SSE server gives the agents the server's own address as before; a rename keeps the sign-in and removing the server drops it. /mcp/ is local-only or LAN-shared with the key, and refuses a request with an Origin or a Host not naming this computer, so a web page can't borrow the sign-in through the browser (form posts, DNS rebinding), and isn't counted as busy or key-limited. WSL agents are given their distro's gateway address. The row says Signed in or Sign-in ran out; zh strings added. Tests: TestSignIn, TestSignInWithoutMetadata, TestSignInPage, TestNothingToSignIn, TestTokenRenewsBeforeItRunsOut (8 calls, 1 renewal), TestRenewRefused, TestRenewRefusedForGood, TestMCPProxy, TestMCPProxyStaysHere, TestMCPProxyNotForWebPages (fails without the Origin/Host check), TestSignedInServerGoesThroughMagpie against a local fake OAuth+MCP server; mcp-signin.test.cjs (webkit, en and zh). Not tried against a real Neon/Notion/Supabase sign-in. go vet ./internal/..., go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui -count=1 ./... under a temp HOME pass.
2026-10-03 09:37:39 +08:00
gateway: a custom tool's call magpie hands Codex has a ctc_ id, and a magpie model or group on OpenAI's API gets call ids OpenAI takes (Lullaby on Discord: Invalid 'input[155].id': 'fc_18da5b9a10256a3e00000011'. Expected an ID that begins with 'tsc'. 这个问题貌似还是没有修复, then "Expected an ID that begins with 'ctc'", auto-compaction failing with it) — two causes. A vendor's apply_patch, translated to Codex's custom_tool_call, was given an fc_ id as a function call is (itemPrefix), and OpenAI turns away a custom_tool_call whose id isn't a ctc_ one. And the tsc_ rename (1a9c4ca6) ran only for Codex's own models: a magpie model or routing group relayed to OpenAI's API on a key (passthrough) sent the session's fc_ ids as they were. The id kinds are now one table, openaiItemPrefix (tool_search_call tsc_, custom_tool_call ctc_, the two OpenAI's errors name): calls are minted with them in the stream and the rendered reply, and callItemIDs (was searchCallIDs) renames fc_X to tsc_X / ctc_X before Codex's own /responses and /responses/compact and before a passthrough to an openai.com host, call_ids and the rest of the body byte for byte. A ChatGPT account's request keeps no ids (provider.codexInput), so it is left alone. TestCustomToolCallHasCtcID, TestMagpieModelOpenAICallIDs (a fake OpenAI behind the provider's proxy answering the same 400s, direct and through a group) and TestCodexOwnModelCustomCallIDs (turn and compaction) fail without the change; TestCustomCallIDsUntouched keeps a clean request byte for byte. go vet ./internal/..., go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui -count=1 ./... under a temp HOME pass.
2026-10-03 10:28:46 +08:00
gateway: an earlier assistant turn's thinking goes to Mistral as the thinking part Mistral itself returns, and a vendor that turns it away inside messages is asked again without it (#494: Mistral's 422 extra_forbidden for OpenCode's reasoning_content, after any tool call) — OpenCode sends an earlier assistant turn back with its thinking in reasoning_content, a tool-call turn too (content null), and magpie relayed it as sent; Mistral refuses any field it doesn't know, loc ["body","messages",2,"assistant","reasoning_content"], so a Mistral reasoning model (GLM on api.mistral.ai) in OpenCode failed as soon as it used a tool. The #393 retry only dropped a fixed list of top-level fields. To Mistral (its preset, or api.mistral.ai) an assistant message's reasoning_content (or reasoning, or reasoning_details' text) now goes as a leading {"type":"thinking","thinking":[{"type":"text","text":…}]} part of its content, its text after it as a text part, its tool_calls as they were, so the thinking stays in context, as the reporter found Mistral takes it; the other messages keep their bytes. Any other Chat upstream answering extra_forbidden located in messages for one of those fields is asked again with it left out of the messages, and once that works it is left out for that provider from then on; DeepSeek, Kimi and every vendor that takes it are sent it as before. Prompts and headers are unchanged. TestMistralReasoningAsThinking (the issue's request, and a tool-call turn) and TestMessageReasoningRefused fail without the change. go vet and the darwin build pass; the full test and cross-build chain could not run, the disk being full.
2026-10-02 09:23:29 +08:00
claude: an account Claude Code was moved off is never answered, nor counted, as the one it was moved to (nil_1024, v0.1.641, routing Smart: Claude Code signed in to A, B added in magpie and made first, B's 5 hours spent; the Usage page showed A's 5 hours spent too though A was never used, and every Claude request ended 499 until magpie's Claude was turned off and Claude Code signed in to A again; Claude Code's /status said B) — "Make first" on a saved Claude account signs Claude Code itself in to it (SwitchLogin): B's sign-in goes into Claude Code's keychain and ~/.claude.json, A's is saved and from then on run in a config directory of its own. But the Claude Code processes the bridge keeps for a conversation's next turn were found by owner, p.ID + the account's name alone, and A's conversations were kept in Claude Code's own home, from when A was its sign-in. Claude Code reads its keychain again every 30 s (2.1.287, the keychain reader's ASt=30000), so those processes went on as B: A's next turns, routed to the saved A, resumed them and were B's, refused as spent, and the rate_limit_event they printed (B's five_hour, rejected, 100%) was noted under A, so A showed spent and rested until B's reset; with B spent too, every request was turned away with 429, which the agent kept retrying until it gave up (the gateway's 499, "the agent canceled the request"). Signing Claude Code in to A again made those processes A's again, which is why that fixed it. Now a run in Claude Code's own home has "own" in its owner (Account.AgentsOwn: no token of magpie's), so a saved account's turn never resumes one kept from when it was Claude Code's own sign-in, and starts a Claude Code on its own sign-in in its own directory; what such a run says of the account it is on is noted only while Claude Code is still signed in to its owner (ClaudeCodeMovedOff); and Claude Code's /usage reading for the account it is signed in to is kept only when Claude Code was on that account before and after it ran (claudeWindows), so a reading taken across a switch, or asked for an account Claude Code was just moved off, isn't kept as that account's. TestClaudeMadeFirstKeepsTheOtherAccountItsOwn (A's turn after the switch was answered "tok-b turn 2" by the Claude Code kept from before, and A's 5 hours read 100%, without the change; B is still asked as itself and reads 100%) and TestClaudeMadeFirstSpentGoesToTheOther (through the gateway, routing Smart, B refusing as spent: A's conversation was a 429 "You've hit your limit", now answered on A) run a fake Claude Code that answers as the sign-in it reads at each turn, in a temp HOME, with the real SwitchLogin; TestClaudeUsageOfAnotherAccountNotKept (Claude Code moved to B while /usage ran: A read 100% without the change) covers the reading. No real Claude Code, account or keychain was used, and no request was made to Anthropic. go vet ./internal/..., go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui -count=1 ./... under a temp HOME pass.
2026-10-02 17:02:42 +08:00
providers: ClinePass lists Cline's real free models and asks for them as Cline's desktop app (lml and ARNO on Discord: the free models didn't match Cline's) — a3962602 listed the Cline API's OpenRouter-style ":free" ids (qwen/qwen3.8-27b:free…), which are not what Cline offers free: Cline's clients take their picker from GET /api/v1/ai/cline/recommended-models (cline/cline sdk/packages/llms/src/catalog/catalog-cline-recommended.ts), whose free bucket is the desktop's FREE section ("Try with limited usage, separate from ClinePass quota": cline-free/deepseek-v4.1-flash, stealth/space-bunny-alpha, cline-free/mimo-v2.6-flash, cline-free/muse-spark-1.3-contributor) and whose clinePass bucket is the plan's. ClinePass now fetches that feed, asked as Cline's desktop app and with no key, as Cline asks it (pi-freeflow measured that X-CLIENT-TYPE cline-desktop is what the feed keys its free list on); the plan's models are its clinePass ones, the free ones are marked Free (the FREE badge) and priced at nothing, and its recommended paid models are left out, as before. When the feed can't be had the API's list is asked as before, and the free models are the desktop's list of today (from Cline's generated fallback, cline-recommended.generated.ts); the ":free" ids are dropped with FreeTag, as Cline's own clients never offer them as free and their cost on a Cline key was never shown. Requests to the Cline API (the gateway's and the Test button's) carry the desktop's headers — HTTP-Referer https://cline.bot, X-Title Cline, X-IS-MULTIROOT false, X-CLIENT-TYPE cline-desktop, X-CLIENT-VERSION 3.5.54, User-Agent Cline/3.5.54 — since the cline-free/ models answer 403 "only available via Cline product surfaces" without them (pi-freeflow, measured live); the key goes as before. ListPrice prices a model free by its cline-free/ id, by the fetched list or by the fallback at zero. No request was made to Cline and nothing was tried with a real Cline key. TestClineFeed (the feed asked as the desktop with no key; plan, free and no paid models listed; a free model the feed alone names priced at zero), TestClineClientHeaders (the headers on a cline-free request to api.cline.bot and on no other provider's) and the updated TestPlanModels fail without the change. go vet ./internal/..., go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui ./internal/provider/ ./internal/gateway/ pass (TestClaudeSignInOverStaleCredentials failed once under the full run, passes alone and on the rerun; Cline untouched by it).
2026-10-02 13:07:03 +08:00
claude: magpie no longer asks Anthropic for a Claude account's usage, and adding a Claude account warns of a ban (the user: 不要伪造任何的 Claude 请求,能否通过 claude cli 获取 / 我害怕被封; 添加账号时要像其他的 Subscription Provider 一样显示风险提示) — the usage was read by magpie itself calling Anthropic's /api/oauth/usage with the account's token, on a timer, for every saved account, and Claude's usage-limit resets (cedar_ember) were listed, spent and auto-spent the same way. Now the usage is what Claude Code's own /usage prints (claude -p /usage, a command Claude Code answers itself without asking a model), run only when Usage is opened or Refresh pressed (GUI, magpie quota, accounts, TUI), for the account Claude Code is signed in to, at most once in 30s; a saved account shows only what Claude Code reported while using it, or the last reading kept. The Claude resets, Auto-use and their settings are gone (Codex's stay). Session jsonl files hold no usage figures, so they aren't read. Starting a Claude sign-in or import (GUI and CLI) first says Anthropic may suspend or ban the account and that it is at the user's own risk. TestClaudeWindowsAsked, TestParseClaudeUsage, TestClaudeUsageRunsClaudeCode, claude-usage-asked.test.cjs and claude-risk.test.cjs fail without the change. go vet, go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui ./... pass; routing-auto-reset, codex-auto-reset, quota-families, tray-usages, click-scroll, old-webkit, plugin-signin, usage-alert, usage-ledger pass.
2026-10-01 12:38:25 +08:00
codex: a group's effort for the turn keeps Codex's reasoning.context, and Codex's custom tools (code mode's exec, apply_patch) reach a model magpie translates for (#534, #520 follow-up, azir12345: since v0.1.637 a Plus account's gpt-6.1-sol fails with 400 "X-OpenAI-Internal-Codex-Responses-Lite requires reasoning.context to be all_turns", and Codex said it had only the collaboration and wait tools, no terminal) — ChatGPT's catalog marks gpt-6.1-sol (as gpt-6-sol, gpt-6-luna, gpt-5.6-*) use_responses_lite with tool_mode code_mode_only, and Codex asks for the bare slug with its native metadata: the X-OpenAI-Internal-Codex-Responses-Lite header (add_responses_lite_header, which magpie forwards with the other x-openai-* headers), reasoning {"effort","summary","context":"all_turns"} (build_reasoning), and its tools moved into an additional_tools item, every other tool reached through exec, a custom (freeform, lark grammar) tool in the "functions" namespace, beside wait and the collaboration namespace. magpie routes the bare id to the group auto-gpt-6-1-sol, and the effort the group picks for the turn went through withEffort, whose Responses branch rebuilt reasoning as {effort, summary} and dropped context, so ChatGPT refused the Lite request; it now sets only the effort (withBodyEffort), keeping every other field of reasoning as Codex sent it. A member served by translation (Chat, Anthropic, a Responses plugin) had parseResponses drop custom tools and ignore custom_tool_call / custom_tool_call_output items, so exec vanished and the model saw only wait and the collaboration tools; a custom tool (top-level or in a namespace) is now offered as a function taking one string "input", its description its own plus a note on input and its grammar, an earlier custom_tool_call goes to the model as a call with {"input":…} and its output as the call's result, and the model's call goes back to Codex as the custom_tool_call it runs (name and namespace as Codex knows them, input from the "input" argument, else the arguments as given), streamed without function_call_arguments events and in the non-streamed reply alike. This also gives translated models Codex's freeform apply_patch, which was dropped silently before. The Lite header is still forwarded as it was; nothing showed it needs dropping. Codex's base_instructions and identity line are untouched. No request was made to ChatGPT. TestWithEffort's Lite case fails without the decide.go change ("want \"context\":\"all_turns\""); TestCodexCustomToolsTranslated (a code-mode Lite turn through a fake Chat provider: exec offered with input and its grammar, the earlier call and output shown, the model's exec call back as a custom_tool_call with its input and no arguments) and TestCodexCustomToolNamespaced (namespaced and non-JSON calls, renderResponses) cover the tools; TestAdditionalToolsTranslated, TestNamespacedToolsOffered and TestGrokPluginGetsNamespacedTools now expect the custom tools offered. go vet ./internal/..., go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui -count=1 ./... under a temp HOME pass (TestDevinSignedInOnlyInMagpie failed once on its wait, passes alone; untouched).
2026-10-02 15:59:14 +08:00
gateway: a developer message Codex adds mid-thread stays where it is, so the thread's cached prefix does too (#502, Koohoko: after a memory request the next turn of the same session wrote the whole conversation to the cache again) — Codex adds a developer message to a thread where its context changed (a world-state or skills diff, a settings or model change, and so after its memory pipeline has run) and keeps it there in every later request, which leaves OpenAI's prefix as it was. parseResponses lifted every developer and system message, wherever it stood, into the system prompt, so on a translated route that turn's system changed: Anthropic's system comes before the messages, so the whole conversation was written to the cache again, and a Claude subscription's waiting Claude Code is found by its system (turnKey), so a new one was started and told the whole conversation in one message. Developer messages before the thread's first reply (Codex's initial context) are lifted as before; one after it now stays in place as a user message inside <system-reminder>, which the router's classifier already leaves out of the user's words. The memory request itself (another model, its own prompt, the same session) moves nothing in magpie: affinity is scoped by model or by a group's first words, the Claude bridge's runs by the conversation. TestCodexLaterDeveloperMessageKeepsTheSystem and TestCodexThreadKeepsItsClaudeRunAcrossAMemoryCall (a Codex thread on a Claude subscription, a memory call in the same session between, then a developer message: the same Claude Code goes on) fail without the change. No real vendor request was made. go vet ./internal/..., go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui ./internal/gateway/ ./internal/provider/ pass.
2026-10-02 13:00:22 +08:00
codex: Ultra on GPT-6.1 Sol wherever it is served, Copilot's too, with Codex's agents taking parts of the task (#656) — Codex offers Ultra on a model whose entry lists "ultra", and on a thread at multi-agent V2 hands parts of the task to agents of its own; its catalog (models-manager/models.json) lists it for gpt-6.1-sol, gpt-6-sol, gpt-6-astra, gpt-5.6-sol and gpt-5.6-terra, not the Lunas, which stop at max. magpie listed Copilot's gpt-6.1-sol only up to max, so Codex had no Ultra for it. Now an entry for one of those models (by the id the vendor serves: openai/…, dated snapshots) that reaches max lists ultra too, in Codex's catalog and /v1/models, and its Codex entry says multi_agent_version v2, as Codex's own does; GPT-6 Luna still stops at max. A ChatGPT account's entry stays as the backend lists it, and a group with a ChatGPT account in it offers ultra but isn't told V2 (its lead's subagent tasks come sealed, #141); the "subagents on any model" setting still stamps v1 on OpenAI's entries. Ultra is no API's level: the gateway now always sends max for it (fitEffort), even to a model whose list says ultra. Subagent tasks with web_search kept (the translated path) were fixed in v0.1.689 (#613); TestCopilotUltraSubagentWithWebSearch now covers it against a stub Copilot: the lead's spawn_agent comes back with plain arguments and encrypted_function_args [], no "encrypted" goes upstream, and the subagent's agent_message reaches Copilot as text. TestCopilotSolOffersUltra (sol has ultra in /v1/models and Codex's catalog with v2, luna not; ultra goes to Copilot as max, with and without web_search) and TestUltraIsSentAsMax fail on the old code; TestUltraOnModelsThatOfferIt and TestUltraEntriesSayV2 cover entries, groups and the catalog. go vet, builds (darwin, GOOS=linux, GOOS=windows) and go test -tags nogui -count=1 ./... under a temp HOME pass. Not tried in the Codex App with a real Copilot account.
2026-10-03 16:08:26 +08:00
docs: every built-in subscription a plugin serves once moved says so in its code, and AGENTS.md says where a fix for it goes (user: 对内置的订阅 provider 在代码层面和文档层面加上注释,防止又有别的 agent 在改内置订阅 provider 的代码而不知道生效的其实是对应的插件代码) — a moved Cursor, Devin, Grok, Command Code plan, Kiro, Zed, Qoder, Factory, Xiaomi MiMo, ZCode or WorkBuddy (provider.Moved; since v0.1.642 also the default for a new sign-in, provider.Adopt) is signed in, listed, requested and metered by its @magpie-community plugin, and none of the built-in's code in internal/provider/<name>*.go, internal/gateway/<name>.go, internal/zed or internal/qoder serves it; a fix made there alone never reaches those users. Each of those files now opens with a PLUGIN-SERVED notice naming the subscription's id, its plugin package, where the plugin lives (magpie-community/plugins, packages/<name>) and the mover whose min to raise so moved users get the fixed plugin (keepMovedCurrent); Qoder's adds that Qoder CN isn't moved and still runs there. AGENTS.md (CLAUDE.md imports it) lists the moved built-ins with their packages and movers, how to tell whether a user's is moved (move.state "plugin", onPlugins), the order a fix goes in (the plugin, publish, raise min, the built-in only for those not moved), the magpie code that does run for a moved one (internal/plugin, plugins.go, plugin_usage.go, pluginsignin.go, migrate*.go, app.js's subOf/pluginSubs/startPluginSignIn), and the parity tests. movers in migrate.go and SUBS in app.js point there too. TestMovedBuiltinsSayTheirPlugin fails for a mover whose code carries no PLUGIN-SERVED notice naming its id and package (it failed while a wrapped package name was split). Comments and docs only, no behaviour changed. go vet ./internal/..., go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui -count=1 ./... under a temp HOME pass (TestProvidersAnswerWhileListsComeIn and TestClaudeSignedOut failed once on their timing under load, pass alone; untouched).
2026-10-02 19:37:02 +08:00
rules: a routing group's rule can be for the agent compacting its conversation, so a cheaper, faster model writes the summary while the conversation stays on its own (X, MR.Guo: 希望 Magpie 能像 OpenCode 一样支持单独指定 context compression model:当 Agent 需要做上下文压缩时…指定一个更便宜、更快的小模型专门负责总结和压缩历史上下文) — a compaction went to whatever model the conversation was on, the priciest in the group as often as not, and a long session paid for each summary at that price. A rule's new condition, compact (provider.Rule.Compact, "compacting"; magpie group rule add <group> use=<model> compact, compact=yes|no; the GUI editor's 压缩上下文时 / compacting toggle; the TUI hint), matches a request that is an agent compacting, told from the agent's own prompt (internal/gateway/compacting.go, read from each one's release): the system prompt of Claude Code 2.1's /compact and auto-compact, OpenCode's compaction agent, Pi, Gemini CLI's state_snapshot and Qwen Code, or the last user message of Claude Code's, Codex's (and magpie's own for a Codex compaction_trigger) and Kimi Code's; a prompt only quoted earlier in a conversation doesn't count. Only in a group with such a rule is a request looked at this way, so nothing else changes. A compaction is decided on its own, never held by the turn's rule, its classifier never asked; it isn't remembered for the turn, and neither the conversation's affinity nor the turn's size is taken from its answer, so the requests after it go on on the model the turn was on and its cache (without that, the next request of a Claude Code turn followed the summary to the cheap model). A member whose context is less than the compaction is passed over for the next rule that matches, or the group's order (RuleHit.Small). The trace tells it ("The agent is compacting the conversation, so rule 1 sends the summary to …", the models passed over named), and the editor notes a compact rule's model taking less than another member. Docs: start#rules (en, zh). Tested: internal/gateway/compacting_test.go — each agent's prompt taken, a plain and a quoted one not; in a group with rules compact→a/small and tokens≥2000→b/big, a long Claude Code turn on b, its /compact to a (Compact, N=1), the turn's next request back on b held, Codex's compaction to a too; a 70k-token compaction skipping a (64k) for rule 2 with Small [a/small]; a group with no compact rule routing a compaction as before; ParseRule/Line/Conditions/Matches for compact. TestRuleCompact failed with the affinity kept from the summary. internal/gui/tests/routing-compact.test.cjs, Chromium and WebKit, en and zh: the compaction in the trace, the rule's words, the toggle on and off with nothing moved, the room note, a compacting-only rule saved as compact:true and not refused. routing-kind/served/signin/steady, click-scroll and old-webkit pass. go vet, go build -tags nogui (and GOOS=linux, GOOS=windows) and go test -tags nogui ./... pass. Not tried against real agents' compactions through the gateway (the prompts are matched as read from their releases) nor in the real Wails window.
2026-09-30 10:24:18 +08:00
rules: a routing group's rule can be for the agent compacting its conversation, so a cheaper, faster model writes the summary while the conversation stays on its own (X, MR.Guo: 希望 Magpie 能像 OpenCode 一样支持单独指定 context compression model:当 Agent 需要做上下文压缩时…指定一个更便宜、更快的小模型专门负责总结和压缩历史上下文) — a compaction went to whatever model the conversation was on, the priciest in the group as often as not, and a long session paid for each summary at that price. A rule's new condition, compact (provider.Rule.Compact, "compacting"; magpie group rule add <group> use=<model> compact, compact=yes|no; the GUI editor's 压缩上下文时 / compacting toggle; the TUI hint), matches a request that is an agent compacting, told from the agent's own prompt (internal/gateway/compacting.go, read from each one's release): the system prompt of Claude Code 2.1's /compact and auto-compact, OpenCode's compaction agent, Pi, Gemini CLI's state_snapshot and Qwen Code, or the last user message of Claude Code's, Codex's (and magpie's own for a Codex compaction_trigger) and Kimi Code's; a prompt only quoted earlier in a conversation doesn't count. Only in a group with such a rule is a request looked at this way, so nothing else changes. A compaction is decided on its own, never held by the turn's rule, its classifier never asked; it isn't remembered for the turn, and neither the conversation's affinity nor the turn's size is taken from its answer, so the requests after it go on on the model the turn was on and its cache (without that, the next request of a Claude Code turn followed the summary to the cheap model). A member whose context is less than the compaction is passed over for the next rule that matches, or the group's order (RuleHit.Small). The trace tells it ("The agent is compacting the conversation, so rule 1 sends the summary to …", the models passed over named), and the editor notes a compact rule's model taking less than another member. Docs: start#rules (en, zh). Tested: internal/gateway/compacting_test.go — each agent's prompt taken, a plain and a quoted one not; in a group with rules compact→a/small and tokens≥2000→b/big, a long Claude Code turn on b, its /compact to a (Compact, N=1), the turn's next request back on b held, Codex's compaction to a too; a 70k-token compaction skipping a (64k) for rule 2 with Small [a/small]; a group with no compact rule routing a compaction as before; ParseRule/Line/Conditions/Matches for compact. TestRuleCompact failed with the affinity kept from the summary. internal/gui/tests/routing-compact.test.cjs, Chromium and WebKit, en and zh: the compaction in the trace, the rule's words, the toggle on and off with nothing moved, the room note, a compacting-only rule saved as compact:true and not refused. routing-kind/served/signin/steady, click-scroll and old-webkit pass. go vet, go build -tags nogui (and GOOS=linux, GOOS=windows) and go test -tags nogui ./... pass. Not tried against real agents' compactions through the gateway (the prompts are matched as read from their releases) nor in the real Wails window.
2026-09-30 10:24:18 +08:00
providers: Max concurrent requests — over it, an agent's requests wait in line and go out in the order they came (Lemon on Discord: 希望 provider 或者插件能够支持一个参数类似 max_concurrency,如果超过上限,放到无界队列里面依序请求; Memo: ChatGPT/Codex subscriptions' risk control trips at about 5-6 requests at once) — the gateway sent every request to its provider the moment it came, so a few agents (or one agent's subagents) on one subscription could have any number out at the vendor at once, and nothing in magpie could hold them to what an account takes. A provider now has maxConcurrency (providers.json; nil or 0 is no limit, a negative one is saved as 0), and a signed-in agent's accounts keep theirs with their picks. The limit is per key or account, as the gateway already tells them apart (candidate.who(): provider#keyid, provider@user, else the provider), so a subscription pooling several accounts lets each one have that many out. Before a try is forwarded it takes a slot in that lane (internal/gateway/concurrency.go); with none free it waits in a FIFO queue with no bound, and is let in, first come first served, as one before it ends. The slot is held for the whole try (attempt reads the vendor's reply to its end or until the agent hangs up, and its body is closed when it returns) and given back once whatever happens (sync.Once), and a lane with nothing out and no one waiting is dropped. An agent that hangs up while waiting leaves the queue at once, is never sent and takes no slot (a slot granted in the same instant is handed on), and goes the way of any canceled request (499, nothing recorded). Waiting is not a failure: a request in line stays for its provider and is not moved on to a fallback or another group member, the account isn't rested, and the time it waited is told in its route's try (Try.Queued, ms). A plugin can say how many each of its accounts takes, as its own field: the auth hook's maxConcurrency, else package.json's magpie.maxConcurrency (a whole number over 0, at most 1000); the user's setting on the provider goes over it. GET /v1/magpie/concurrency (local or shared callers, as quotas) gives each lane's out, waiting and limit. The provider editor (keys, custom, signed-in accounts) has "Max concurrent requests" / 最大并发请求数 under Proxy, "超出时排队,按顺序发出;0 或留空为不限制" below it (with a plugin's own number as its placeholder), refuses anything but a whole number from 0 to 1000 before anything is posted, and an empty field saves none (the plugin's, if it has one); a save that doesn't send the field keeps what the provider had. No coloured left border, and a click in the field leaves the page where it is. TestMaxConcurrency (limit 2, five streams: never more than 2 at the vendor, 3 waiting, let in a..e in order; one canceled while waiting leaves the queue, takes no slot and never reaches the vendor; slots given back when each stream ends and when an agent hangs up mid-stream; the next one served at once) fails without the change (all five reach the vendor at once, no lane), as do TestLanesLimitLifted, TestProviderConcurrency and the updated TestPluginOwnIconAndKeyHint (the auth hook's 3, package.json's 5 when the hook's is 2.5); provider-concurrency.test.cjs (chromium and webkit, en and zh: typed and saved, 2.5 refused with nothing posted, empty saved as null, an account opening on its 5 and saving 0, a plugin's 4 as the placeholder, every string translated, no stripe, no scroll on click) passes. No vendor was asked. go vet ./internal/..., go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui -count=1 ./... (under a temp HOME) and go test -race on the new tests pass. provider-proxy.test.cjs's account test fails the same on HEAD without the change (its editor redrawn under it).
2026-10-02 13:38:23 +08:00
providers: Max concurrent requests — over it, an agent's requests wait in line and go out in the order they came (Lemon on Discord: 希望 provider 或者插件能够支持一个参数类似 max_concurrency,如果超过上限,放到无界队列里面依序请求; Memo: ChatGPT/Codex subscriptions' risk control trips at about 5-6 requests at once) — the gateway sent every request to its provider the moment it came, so a few agents (or one agent's subagents) on one subscription could have any number out at the vendor at once, and nothing in magpie could hold them to what an account takes. A provider now has maxConcurrency (providers.json; nil or 0 is no limit, a negative one is saved as 0), and a signed-in agent's accounts keep theirs with their picks. The limit is per key or account, as the gateway already tells them apart (candidate.who(): provider#keyid, provider@user, else the provider), so a subscription pooling several accounts lets each one have that many out. Before a try is forwarded it takes a slot in that lane (internal/gateway/concurrency.go); with none free it waits in a FIFO queue with no bound, and is let in, first come first served, as one before it ends. The slot is held for the whole try (attempt reads the vendor's reply to its end or until the agent hangs up, and its body is closed when it returns) and given back once whatever happens (sync.Once), and a lane with nothing out and no one waiting is dropped. An agent that hangs up while waiting leaves the queue at once, is never sent and takes no slot (a slot granted in the same instant is handed on), and goes the way of any canceled request (499, nothing recorded). Waiting is not a failure: a request in line stays for its provider and is not moved on to a fallback or another group member, the account isn't rested, and the time it waited is told in its route's try (Try.Queued, ms). A plugin can say how many each of its accounts takes, as its own field: the auth hook's maxConcurrency, else package.json's magpie.maxConcurrency (a whole number over 0, at most 1000); the user's setting on the provider goes over it. GET /v1/magpie/concurrency (local or shared callers, as quotas) gives each lane's out, waiting and limit. The provider editor (keys, custom, signed-in accounts) has "Max concurrent requests" / 最大并发请求数 under Proxy, "超出时排队,按顺序发出;0 或留空为不限制" below it (with a plugin's own number as its placeholder), refuses anything but a whole number from 0 to 1000 before anything is posted, and an empty field saves none (the plugin's, if it has one); a save that doesn't send the field keeps what the provider had. No coloured left border, and a click in the field leaves the page where it is. TestMaxConcurrency (limit 2, five streams: never more than 2 at the vendor, 3 waiting, let in a..e in order; one canceled while waiting leaves the queue, takes no slot and never reaches the vendor; slots given back when each stream ends and when an agent hangs up mid-stream; the next one served at once) fails without the change (all five reach the vendor at once, no lane), as do TestLanesLimitLifted, TestProviderConcurrency and the updated TestPluginOwnIconAndKeyHint (the auth hook's 3, package.json's 5 when the hook's is 2.5); provider-concurrency.test.cjs (chromium and webkit, en and zh: typed and saved, 2.5 refused with nothing posted, empty saved as null, an account opening on its 5 and saving 0, a plugin's 4 as the placeholder, every string translated, no stripe, no scroll on click) passes. No vendor was asked. go vet ./internal/..., go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui -count=1 ./... (under a temp HOME) and go test -race on the new tests pass. provider-proxy.test.cjs's account test fails the same on HEAD without the change (its editor redrawn under it).
2026-10-02 13:38:23 +08:00
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
cursor: a conversation's Runs keep one conversation_id, so Grok's prompt cache on Cursor sticks, and Cursor's usage no longer counts the cache twice (#498, congee949). buildCursorRun put a new UUID in AgentRunRequest's conversation_id (field 5) on every Run, so Cursor's backend had nothing to keep a conversation on the machine holding its cached prefix — the x-grok-conv-id grokSigned sends xAI direct — and a Codex thread on cursor/grok-4.7-fast read about 2% from cache against 94% on the grok provider, its first token growing with the whole prompt. The id is now a UUID (v4-shaped, like the official client's) hashed from what names the session — the client's prompt_cache_key (Codex's thread id), else the agent's own session header (Claude Code's, OpenCode's) — together with the conversation's first user message, which every later request repeats; with neither key nor session header each Run still gets a fresh one, since a first message alone ("hi") would put strangers under one id. On concurrent subagents: Cursor's own client keeps one conversationId per agent session, and whether its backend treats the id as more than a cache-affinity key isn't known, so subagents sharing a session (Claude Code's Task agents share its session id; a Codex collab agent has its own thread and key anyway) get ids of their own through their own first message, never one id sent twice at once. TurnEndedUpdate's input_tokens is the whole prompt, cache read and written included, as Cursor's client has it; decode kept it as Usage.Input, which everywhere else is the uncached rest that prompt() and pricing add the cache to, so a cached turn was counted twice (in=137300 with cache_read=132352 is 4948 uncached). Input is now input_tokens less cache read and write, never below 0, and reasoning_tokens (field 5) is kept as Reasoning. TestCursorConversationID (same Codex key → same id across turns, other key → other id; same Claude Code session → same id, a subagent's first message → another; nothing naming it → a new id each Run), TestCursorUsageTakesTheCacheOut and the updated TestCursorDecodesTheEnd fail without the change. No real Cursor request was made. go vet ./internal/..., go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui ./internal/gateway/ ./internal/provider/ pass.
2026-10-02 11:08:14 +08:00
cursor: a model Cursor serves only in Max Mode is asked for again in Max Mode, and the models Cursor's picker offers beyond cursor-agent models (GLM-5.3, GLM-5.3 Flash) are listed (ARNO on Discord: 调用cursor模型报错:AI_APICallError: Cursor: Max Mode Required: The model "gpt-5.6-luna-low" requires Max Mode to be enabled; cursor provider里少了glm-5.3和glm-5.3-flash模型) — the built-in Cursor (deprecated, served by @magpie-community/opencode-cursor-auth once moved; that plugin's 0.1.7, magpie-community/plugins 3ae3e43, carries the same two fixes) asked every model with no max_mode in the Run's ModelDetails (7) or RequestedModel (2), and a model whose picker entry has supportsNonMaxMode false or a variant isMaxMode (gpt-5.6-luna-*) is refused MAX_MODE_REQUIRED, where cursor-agent turns Max Mode on itself. askCursor now asks such a model again with max_mode set in both when Cursor refuses it so, and keeps asking it in Max Mode from then on (cursorMaxOnly). The list came from cursor-agent models (GetUsableModels) alone; Cursor's picker, AiService/AvailableModels, also offers models that list leaves out, so the account's list now adds them (cursorPickerModels: one per variant slug, else the model's name; hidden, Tab-only, chat-only, non-agent ones and the four the CLI drops stay out, and so does a model the list has under any of its slugs), the picker best-effort. TestServeCursorMaxModeRequired (a fake Cursor refusing gpt-5.6-luna-low without Max Mode; it fails without the change) and TestCursorPickerModels (no GLM-5.3 without the change) are new, against fakes; no request was made to Cursor. The mover's min stays 0.1.5 until 0.1.7 is published. go vet ./internal/..., go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui -count=1 ./... under a temp HOME pass (TestProvidersAnswerWhileListsComeIn, TestUserPath and TestFXInState failed once each under load and pass on a rerun).
2026-10-02 22:18:22 +08:00
cursor: a model Cursor serves only in Max Mode is asked for again in Max Mode, and the models Cursor's picker offers beyond cursor-agent models (GLM-5.3, GLM-5.3 Flash) are listed (ARNO on Discord: 调用cursor模型报错:AI_APICallError: Cursor: Max Mode Required: The model "gpt-5.6-luna-low" requires Max Mode to be enabled; cursor provider里少了glm-5.3和glm-5.3-flash模型) — the built-in Cursor (deprecated, served by @magpie-community/opencode-cursor-auth once moved; that plugin's 0.1.7, magpie-community/plugins 3ae3e43, carries the same two fixes) asked every model with no max_mode in the Run's ModelDetails (7) or RequestedModel (2), and a model whose picker entry has supportsNonMaxMode false or a variant isMaxMode (gpt-5.6-luna-*) is refused MAX_MODE_REQUIRED, where cursor-agent turns Max Mode on itself. askCursor now asks such a model again with max_mode set in both when Cursor refuses it so, and keeps asking it in Max Mode from then on (cursorMaxOnly). The list came from cursor-agent models (GetUsableModels) alone; Cursor's picker, AiService/AvailableModels, also offers models that list leaves out, so the account's list now adds them (cursorPickerModels: one per variant slug, else the model's name; hidden, Tab-only, chat-only, non-agent ones and the four the CLI drops stay out, and so does a model the list has under any of its slugs), the picker best-effort. TestServeCursorMaxModeRequired (a fake Cursor refusing gpt-5.6-luna-low without Max Mode; it fails without the change) and TestCursorPickerModels (no GLM-5.3 without the change) are new, against fakes; no request was made to Cursor. The mover's min stays 0.1.5 until 0.1.7 is published. go vet ./internal/..., go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui -count=1 ./... under a temp HOME pass (TestProvidersAnswerWhileListsComeIn, TestUserPath and TestFXInState failed once each under load and pass on a rerun).
2026-10-02 22:18:22 +08:00
cursor: a model Cursor serves only in Max Mode is asked for again in Max Mode, and the models Cursor's picker offers beyond cursor-agent models (GLM-5.3, GLM-5.3 Flash) are listed (ARNO on Discord: 调用cursor模型报错:AI_APICallError: Cursor: Max Mode Required: The model "gpt-5.6-luna-low" requires Max Mode to be enabled; cursor provider里少了glm-5.3和glm-5.3-flash模型) — the built-in Cursor (deprecated, served by @magpie-community/opencode-cursor-auth once moved; that plugin's 0.1.7, magpie-community/plugins 3ae3e43, carries the same two fixes) asked every model with no max_mode in the Run's ModelDetails (7) or RequestedModel (2), and a model whose picker entry has supportsNonMaxMode false or a variant isMaxMode (gpt-5.6-luna-*) is refused MAX_MODE_REQUIRED, where cursor-agent turns Max Mode on itself. askCursor now asks such a model again with max_mode set in both when Cursor refuses it so, and keeps asking it in Max Mode from then on (cursorMaxOnly). The list came from cursor-agent models (GetUsableModels) alone; Cursor's picker, AiService/AvailableModels, also offers models that list leaves out, so the account's list now adds them (cursorPickerModels: one per variant slug, else the model's name; hidden, Tab-only, chat-only, non-agent ones and the four the CLI drops stay out, and so does a model the list has under any of its slugs), the picker best-effort. TestServeCursorMaxModeRequired (a fake Cursor refusing gpt-5.6-luna-low without Max Mode; it fails without the change) and TestCursorPickerModels (no GLM-5.3 without the change) are new, against fakes; no request was made to Cursor. The mover's min stays 0.1.5 until 0.1.7 is published. go vet ./internal/..., go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui -count=1 ./... under a temp HOME pass (TestProvidersAnswerWhileListsComeIn, TestUserPath and TestFXInState failed once each under load and pass on a rerun).
2026-10-02 22:18:22 +08:00
models: Provider in model names has a third way, a name the user gave used just as they wrote it (#92 marsxxl: v0.1.591 的 Codex 下拉里自定义名称又带上了 · WorkBuddy / · Claude Code) — 559ce021 (#203) had a custom name keep " · <provider>" after it like any other, so "Opus 5.5" read "Opus 5.5 · Claude Code" in every list Labels feeds (Codex's model_catalog_json and the gateway's Codex /models, Pi's, OpenCode's, ZCode's, OMP's, Muse's), while #92 had been promised the name as written. Both are kept: the setting is now Off / Not on names I set (自定义名称不带供应商) / On. On stays the default and keeps #203's behaviour, so nobody's list changes unasked; own leaves a name the user gave as it is and a vendor's name with its provider's; off is #335's plain names. Stored as plainOwnNames beside plainNames, so a settings.json of before reads as it did (plainNames: true is off). POST /api/settings/plain-names takes mode on|own|off (a body of on alone still works), magpie model suffix on|own|off, a Settings save keeps it, and /v1/models' magpie_label now follows the setting through Labels as the agents' lists do. The row's hint is one line that fits (en, zh), and its zh name says 供应商 as the rest of the app. Prompts and headers are unchanged. TestSuffixMode, TestSuffixModesReachAgentFiles (Codex model_catalog_json and Pi models.json), TestModelsListSuffixModes (/v1/models magpie_label) and model-suffix-own.test.cjs (Chromium and WebKit, en and zh) fail without the change; TestModelName and TestModelNameAndLevelsReachAgentFiles keep #203's assertions. go vet, go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui ./... pass (TestConfiguredModelsAdvertiseImageInput load-flaky, passes alone); plain-names, click-scroll, old-webkit pass.
2026-10-01 23:20:57 +08:00
gateway: Claude Desktop offers its thinking-effort picker for magpie's models that take reasoning levels (ARNO: 接入claude desktop后无法设置思考强度,不确定是哪里的问题) — in a gateway's mode Desktop reads no effort from /v1/models (IIt keeps id, display_name, description, supports_1m, anthropic_family_tier), and its model catalog must be Anthropic's signed file, so a model's levels come only from tIt in index.chunk-D3OyLXgG.js: let t=IC(e),n=HFt[t]??(UFt.test(t)?VFt:void 0). HFt is its own table (claude-sonnet-4-6, claude-sonnet-5, claude-opus-4-6/4-7/4-8, claude-opus-5), UFt /^(?:claude-)?(?:fable|mythos)(?:-|$)/ with VFt low…max, and IC the lowercased id with a "<label>.anthropic." prefix and a date taken off (qS). magpie's ids (provider/model, anthropic/magpie-<n>) were neither, so no model had the picker, not even a real Claude one. A model with reasoning levels is now listed as mythos-magpie-<n> (UFt matches; no haiku/sonnet/opus in it, so Desktop's small_fast pick is unchanged), or, for a real Claude model, as magpie-<n>.anthropic.<claude model> (IC reads it as that model, so HFt gives its own levels). One without levels is listed as before. aliased serves all three forms, with or without [1m]. Checked against Desktop's bundled Claude Code 2.1.280 in a sandbox HOME, pointed at a local logger: mythos-magpie-<n> goes out exactly as anthropic/magpie-<n> did (thinking adaptive, output_config.effort as picked, 32000 tokens); magpie-<n>.anthropic.claude-opus-4-8 goes out exactly as x/claude-opus-4-8 did, known as Opus 4.8 (no unrecognized_model warning, 64000 tokens). The effort then reaches the model fitted to its levels: Anthropic passthrough now fits output_config.effort as Chat's reasoning_effort and Responses' reasoning.effort already were (bodyEffort/withBodyEffort), so Desktop's max goes to a low/high model as high. VFt always thinks: Desktop offers no "off" for a non-Claude model with levels. A session saved on an old id must be moved once to the new entry in the picker; magpie still serves the old id.
2026-09-29 00:53:16 +08:00
gateway: Claude Desktop offers its thinking-effort picker for magpie's models that take reasoning levels (ARNO: 接入claude desktop后无法设置思考强度,不确定是哪里的问题) — in a gateway's mode Desktop reads no effort from /v1/models (IIt keeps id, display_name, description, supports_1m, anthropic_family_tier), and its model catalog must be Anthropic's signed file, so a model's levels come only from tIt in index.chunk-D3OyLXgG.js: let t=IC(e),n=HFt[t]??(UFt.test(t)?VFt:void 0). HFt is its own table (claude-sonnet-4-6, claude-sonnet-5, claude-opus-4-6/4-7/4-8, claude-opus-5), UFt /^(?:claude-)?(?:fable|mythos)(?:-|$)/ with VFt low…max, and IC the lowercased id with a "<label>.anthropic." prefix and a date taken off (qS). magpie's ids (provider/model, anthropic/magpie-<n>) were neither, so no model had the picker, not even a real Claude one. A model with reasoning levels is now listed as mythos-magpie-<n> (UFt matches; no haiku/sonnet/opus in it, so Desktop's small_fast pick is unchanged), or, for a real Claude model, as magpie-<n>.anthropic.<claude model> (IC reads it as that model, so HFt gives its own levels). One without levels is listed as before. aliased serves all three forms, with or without [1m]. Checked against Desktop's bundled Claude Code 2.1.280 in a sandbox HOME, pointed at a local logger: mythos-magpie-<n> goes out exactly as anthropic/magpie-<n> did (thinking adaptive, output_config.effort as picked, 32000 tokens); magpie-<n>.anthropic.claude-opus-4-8 goes out exactly as x/claude-opus-4-8 did, known as Opus 4.8 (no unrecognized_model warning, 64000 tokens). The effort then reaches the model fitted to its levels: Anthropic passthrough now fits output_config.effort as Chat's reasoning_effort and Responses' reasoning.effort already were (bodyEffort/withBodyEffort), so Desktop's max goes to a low/high model as high. VFt always thinks: Desktop offers no "off" for a non-Claude model with levels. A session saved on an old id must be moved once to the new entry in the picker; magpie still serves the old id.
2026-09-29 00:53:16 +08:00
docs: every built-in subscription a plugin serves once moved says so in its code, and AGENTS.md says where a fix for it goes (user: 对内置的订阅 provider 在代码层面和文档层面加上注释,防止又有别的 agent 在改内置订阅 provider 的代码而不知道生效的其实是对应的插件代码) — a moved Cursor, Devin, Grok, Command Code plan, Kiro, Zed, Qoder, Factory, Xiaomi MiMo, ZCode or WorkBuddy (provider.Moved; since v0.1.642 also the default for a new sign-in, provider.Adopt) is signed in, listed, requested and metered by its @magpie-community plugin, and none of the built-in's code in internal/provider/<name>*.go, internal/gateway/<name>.go, internal/zed or internal/qoder serves it; a fix made there alone never reaches those users. Each of those files now opens with a PLUGIN-SERVED notice naming the subscription's id, its plugin package, where the plugin lives (magpie-community/plugins, packages/<name>) and the mover whose min to raise so moved users get the fixed plugin (keepMovedCurrent); Qoder's adds that Qoder CN isn't moved and still runs there. AGENTS.md (CLAUDE.md imports it) lists the moved built-ins with their packages and movers, how to tell whether a user's is moved (move.state "plugin", onPlugins), the order a fix goes in (the plugin, publish, raise min, the built-in only for those not moved), the magpie code that does run for a moved one (internal/plugin, plugins.go, plugin_usage.go, pluginsignin.go, migrate*.go, app.js's subOf/pluginSubs/startPluginSignIn), and the parity tests. movers in migrate.go and SUBS in app.js point there too. TestMovedBuiltinsSayTheirPlugin fails for a mover whose code carries no PLUGIN-SERVED notice naming its id and package (it failed while a wrapped package name was split). Comments and docs only, no behaviour changed. go vet ./internal/..., go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui -count=1 ./... under a temp HOME pass (TestProvidersAnswerWhileListsComeIn and TestClaudeSignedOut failed once on their timing under load, pass alone; untouched).
2026-10-02 19:37:02 +08:00
gateway: WorkBuddy's plans draw with the image models their config lists (freecss on Discord: let WorkBuddy's subscription image models be magpie's image models) — Settings › Models › image model, the Magpie Image MCP and /v1/images/* now offer a WorkBuddy plan's image models, for the China build (workbuddy) and WorkBuddy AI (workbuddy-ai), built-in or moved onto @magpie-community/opencode-workbuddy-auth. Which models are taken from GET <site>/v3/config, signed as a chat is, by their tags (text-to-image, image-to-image), with no ids written in: today the China desktop sign-in lists hunyuan-image-v3.0-art, WorkBuddy AI gpt-image-2.5-sunburst (which edits too). The list is kept an hour per account (5 min after a failed ask); Drawers answers at once and asks in the background. A drawing goes as WorkBuddy's CLI sends it, to <site>/v2/images/generations or /v2/images/edits in JSON: GPT Image with quality, background, output_format and b64_json; Hunyuan with model, prompt, n and size only; an edit's images as data URLs, to the model itself when it takes images or to its -edit beside it, else a 400 that says the model draws from a prompt only. WorkBuddy's {code, msg, data} is unwrapped (a non-zero code is a 502 with its msg); Hunyuan's COS URL is passed on as the other URL drawers' are. These models are never AutoDrawer's pick, since each image costs the plan's credits. For a moved plan the plugin's fetch signs and sends it: pluginFetch now also takes an absolute URL, only to the plugin base's own host, and Provider.Origin gives a provider's site. TestWorkBuddyDrawsWithItsConfigsModels, TestWorkBuddyAIDrawsGPTImage and TestWorkBuddyPluginDraws (fake upstream: config, generation, edit, envelope error, plugin path, foreign host refused) fail on the old code. Against the owner's real accounts (moved onto the plugin) in a sandbox gateway under a temp HOME: workbuddy/hunyuan-image-v3.0-art drew a 1024x1024 PNG (URL), workbuddy-ai/gpt-image-2.5-sunburst one in b64, an edit of it with gpt-image-2.5-sunburst turned the bird red, and an edit asked of hunyuan-image-v3.0-art got the 400 without a call; no token rotated. go vet, builds (darwin, GOOS=linux, GOOS=windows) and go test -tags nogui -count=1 ./... under a temp HOME pass. The GUI's model picker wasn't looked at (nogui sandbox).
2026-10-03 19:55:07 +08:00
gateway: WorkBuddy's plans draw with the image models their config lists (freecss on Discord: let WorkBuddy's subscription image models be magpie's image models) — Settings › Models › image model, the Magpie Image MCP and /v1/images/* now offer a WorkBuddy plan's image models, for the China build (workbuddy) and WorkBuddy AI (workbuddy-ai), built-in or moved onto @magpie-community/opencode-workbuddy-auth. Which models are taken from GET <site>/v3/config, signed as a chat is, by their tags (text-to-image, image-to-image), with no ids written in: today the China desktop sign-in lists hunyuan-image-v3.0-art, WorkBuddy AI gpt-image-2.5-sunburst (which edits too). The list is kept an hour per account (5 min after a failed ask); Drawers answers at once and asks in the background. A drawing goes as WorkBuddy's CLI sends it, to <site>/v2/images/generations or /v2/images/edits in JSON: GPT Image with quality, background, output_format and b64_json; Hunyuan with model, prompt, n and size only; an edit's images as data URLs, to the model itself when it takes images or to its -edit beside it, else a 400 that says the model draws from a prompt only. WorkBuddy's {code, msg, data} is unwrapped (a non-zero code is a 502 with its msg); Hunyuan's COS URL is passed on as the other URL drawers' are. These models are never AutoDrawer's pick, since each image costs the plan's credits. For a moved plan the plugin's fetch signs and sends it: pluginFetch now also takes an absolute URL, only to the plugin base's own host, and Provider.Origin gives a provider's site. TestWorkBuddyDrawsWithItsConfigsModels, TestWorkBuddyAIDrawsGPTImage and TestWorkBuddyPluginDraws (fake upstream: config, generation, edit, envelope error, plugin path, foreign host refused) fail on the old code. Against the owner's real accounts (moved onto the plugin) in a sandbox gateway under a temp HOME: workbuddy/hunyuan-image-v3.0-art drew a 1024x1024 PNG (URL), workbuddy-ai/gpt-image-2.5-sunburst one in b64, an edit of it with gpt-image-2.5-sunburst turned the bird red, and an edit asked of hunyuan-image-v3.0-art got the 400 without a call; no token rotated. go vet, builds (darwin, GOOS=linux, GOOS=windows) and go test -tags nogui -count=1 ./... under a temp HOME pass. The GUI's model picker wasn't looked at (nogui sandbox).
2026-10-03 19:55:07 +08:00
gateway: WorkBuddy's plans draw with the image models their config lists (freecss on Discord: let WorkBuddy's subscription image models be magpie's image models) — Settings › Models › image model, the Magpie Image MCP and /v1/images/* now offer a WorkBuddy plan's image models, for the China build (workbuddy) and WorkBuddy AI (workbuddy-ai), built-in or moved onto @magpie-community/opencode-workbuddy-auth. Which models are taken from GET <site>/v3/config, signed as a chat is, by their tags (text-to-image, image-to-image), with no ids written in: today the China desktop sign-in lists hunyuan-image-v3.0-art, WorkBuddy AI gpt-image-2.5-sunburst (which edits too). The list is kept an hour per account (5 min after a failed ask); Drawers answers at once and asks in the background. A drawing goes as WorkBuddy's CLI sends it, to <site>/v2/images/generations or /v2/images/edits in JSON: GPT Image with quality, background, output_format and b64_json; Hunyuan with model, prompt, n and size only; an edit's images as data URLs, to the model itself when it takes images or to its -edit beside it, else a 400 that says the model draws from a prompt only. WorkBuddy's {code, msg, data} is unwrapped (a non-zero code is a 502 with its msg); Hunyuan's COS URL is passed on as the other URL drawers' are. These models are never AutoDrawer's pick, since each image costs the plan's credits. For a moved plan the plugin's fetch signs and sends it: pluginFetch now also takes an absolute URL, only to the plugin base's own host, and Provider.Origin gives a provider's site. TestWorkBuddyDrawsWithItsConfigsModels, TestWorkBuddyAIDrawsGPTImage and TestWorkBuddyPluginDraws (fake upstream: config, generation, edit, envelope error, plugin path, foreign host refused) fail on the old code. Against the owner's real accounts (moved onto the plugin) in a sandbox gateway under a temp HOME: workbuddy/hunyuan-image-v3.0-art drew a 1024x1024 PNG (URL), workbuddy-ai/gpt-image-2.5-sunburst one in b64, an edit of it with gpt-image-2.5-sunburst turned the bird red, and an edit asked of hunyuan-image-v3.0-art got the 400 without a call; no token rotated. go vet, builds (darwin, GOOS=linux, GOOS=windows) and go test -tags nogui -count=1 ./... under a temp HOME pass. The GUI's model picker wasn't looked at (nogui sandbox).
2026-10-03 19:55:07 +08:00
gateway: a ChatGPT account whose plan doesn't take the reasoning effort asked for hands the request to another account, and isn't asked for it again (#520: 在同时登录plus和免费账户时,如果将免费账户的优先级设置到最高,此时会错误的将6-luna的high路由给免费账户,但是免费账户不提供high及以上的思考强度,返回400错误) — accounts of one subscription were picked by their order alone, with nothing of what each plan takes at what reasoning level: perKeyBarred puts an account whose plan lacks the model after the others (Account.Lists), but a Free account that lists gpt-6-luna and turns high and above away was asked first, and its 400 ("Unsupported value: 'high' is not supported with the 'gpt-6-luna' model. Supported values are: …", param reasoning.effort, code unsupported_value) either reached the agent, or — where retryable's unserved words happened to match "invalid_request_error" — set the Free account aside for a minute for every effort. Now (effort_accounts.go): an account candidate known not to take the level it would be sent — it turned that level away within the hour, or its own model list (Account.Levels, the account's supported_reasoning_levels) gives the model levels without it — is moved behind the next account of the same member that may take it; an account whose 4xx is that refusal (effortRefused: the "Unsupported value: '<level>'" form for the level sent, or one naming reasoning.effort; another field's value such as text.verbosity's 'high' isn't one), of a level the model has, is noted, doesn't rest, and the next is asked (failEffort, "effort" in the routing trace, its matesFirst); when no account left takes it, the agent gets a 400 saying which account doesn't take which effort on which model, the levels it takes and the vendor's words, never the effort lowered behind its back. The codex provider's levels for a model are now its on accounts' together (codexPoolLevels), so a Free account signed in to Codex no longer has a Plus one's high lowered to medium. A request at fault otherwise (a 400 over its input) still goes to the agent, unchanged. Codex's prompts are untouched. TestEffortRefusedGoesToAnotherAccount (Free first: high goes Free then Plus, then straight to Plus, xhigh too; medium is the Free one's; a bad input is not sent on), TestEffortTheAccountListsLacks (Free signed in, its list without high: high goes to Plus alone, low to Free, with Plus out of quota the Free one's refusal is told as such) fail without the change; TestEffortRefusedWords. No request was made to OpenAI. go vet ./internal/..., go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui -count=1 ./... under a temp HOME pass, except internal/proc's TestUserPath (the login shell's PATH not coming within 5s under the temp HOME; passes under the real one; untouched).
2026-10-02 14:10:55 +08:00
gateway: a ChatGPT account whose plan doesn't take the reasoning effort asked for hands the request to another account, and isn't asked for it again (#520: 在同时登录plus和免费账户时,如果将免费账户的优先级设置到最高,此时会错误的将6-luna的high路由给免费账户,但是免费账户不提供high及以上的思考强度,返回400错误) — accounts of one subscription were picked by their order alone, with nothing of what each plan takes at what reasoning level: perKeyBarred puts an account whose plan lacks the model after the others (Account.Lists), but a Free account that lists gpt-6-luna and turns high and above away was asked first, and its 400 ("Unsupported value: 'high' is not supported with the 'gpt-6-luna' model. Supported values are: …", param reasoning.effort, code unsupported_value) either reached the agent, or — where retryable's unserved words happened to match "invalid_request_error" — set the Free account aside for a minute for every effort. Now (effort_accounts.go): an account candidate known not to take the level it would be sent — it turned that level away within the hour, or its own model list (Account.Levels, the account's supported_reasoning_levels) gives the model levels without it — is moved behind the next account of the same member that may take it; an account whose 4xx is that refusal (effortRefused: the "Unsupported value: '<level>'" form for the level sent, or one naming reasoning.effort; another field's value such as text.verbosity's 'high' isn't one), of a level the model has, is noted, doesn't rest, and the next is asked (failEffort, "effort" in the routing trace, its matesFirst); when no account left takes it, the agent gets a 400 saying which account doesn't take which effort on which model, the levels it takes and the vendor's words, never the effort lowered behind its back. The codex provider's levels for a model are now its on accounts' together (codexPoolLevels), so a Free account signed in to Codex no longer has a Plus one's high lowered to medium. A request at fault otherwise (a 400 over its input) still goes to the agent, unchanged. Codex's prompts are untouched. TestEffortRefusedGoesToAnotherAccount (Free first: high goes Free then Plus, then straight to Plus, xhigh too; medium is the Free one's; a bad input is not sent on), TestEffortTheAccountListsLacks (Free signed in, its list without high: high goes to Plus alone, low to Free, with Plus out of quota the Free one's refusal is told as such) fail without the change; TestEffortRefusedWords. No request was made to OpenAI. go vet ./internal/..., go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui -count=1 ./... under a temp HOME pass, except internal/proc's TestUserPath (the login shell's PATH not coming within 5s under the temp HOME; passes under the real one; untouched).
2026-10-02 14:10:55 +08:00
gateway: a reasoning effort changed mid-thread keeps the upstream's cached prompt on GPT-6 (#617) — an agent switching effort changed the request's reasoning.effort, and with it the prefix OpenAI caches. OpenAI's Responses API takes the change as an input item instead (developers.openai.com/api/docs/guides/reasoning, "Change reasoning mid-conversation": {"type":"configuration_update","reasoning":{"effort":…}} before the next user message, the request-level effort unchanged, GPT-6 family). For a GPT-6 model through a ChatGPT account or OpenAI's own API, asked in Responses, magpie keeps per thread (conversation, prompt_cache_key, account, model) the effort it started at and where each change went: the top stays at the first effort, and the updates are put back at the same places on every later request (before the turn's first user message, else at the input's end; a turn asked again at another effort replaces its own update). A history that isn't the last one with more after it (compaction, edits), previous_response_id, truncation auto, context_management, a request without an effort or an agent sending its own updates goes as the agent sent it and starts the thread again — what every request cost before. An upstream that answers 400 to a request with updates is asked again as the agent sent it (trace: update); if that one goes, the account and model get no updates for a day. Other vendors (forVendor strips the items anyway) and older models are untouched. A preview, off unless MAGPIE_EFFORT_UPDATES=on, until it is seen on the real backends: one that took the item and ignored it would leave an effort change doing nothing, unseen. Tests: TestEffortChangeKeepsTheCache, TestEffortUpdateRefused (both fail with it off), TestTakesEffortUpdates, TestEffortChangeOtherVendor against a fake ChatGPT backend. Not tried against the real ChatGPT backend or OpenAI API: whether the backend honours the item (rather than ignoring it) and cached_tokens across a switch are unverified. go vet ./internal/..., go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui -count=1 ./... under a temp HOME pass.
2026-10-03 09:58:00 +08:00
gateway: a reasoning effort changed mid-thread keeps the upstream's cached prompt on GPT-6 (#617) — an agent switching effort changed the request's reasoning.effort, and with it the prefix OpenAI caches. OpenAI's Responses API takes the change as an input item instead (developers.openai.com/api/docs/guides/reasoning, "Change reasoning mid-conversation": {"type":"configuration_update","reasoning":{"effort":…}} before the next user message, the request-level effort unchanged, GPT-6 family). For a GPT-6 model through a ChatGPT account or OpenAI's own API, asked in Responses, magpie keeps per thread (conversation, prompt_cache_key, account, model) the effort it started at and where each change went: the top stays at the first effort, and the updates are put back at the same places on every later request (before the turn's first user message, else at the input's end; a turn asked again at another effort replaces its own update). A history that isn't the last one with more after it (compaction, edits), previous_response_id, truncation auto, context_management, a request without an effort or an agent sending its own updates goes as the agent sent it and starts the thread again — what every request cost before. An upstream that answers 400 to a request with updates is asked again as the agent sent it (trace: update); if that one goes, the account and model get no updates for a day. Other vendors (forVendor strips the items anyway) and older models are untouched. A preview, off unless MAGPIE_EFFORT_UPDATES=on, until it is seen on the real backends: one that took the item and ignored it would leave an effort change doing nothing, unseen. Tests: TestEffortChangeKeepsTheCache, TestEffortUpdateRefused (both fail with it off), TestTakesEffortUpdates, TestEffortChangeOtherVendor against a fake ChatGPT backend. Not tried against the real ChatGPT backend or OpenAI API: whether the backend honours the item (rather than ignoring it) and cached_tokens across a switch are unverified. go vet ./internal/..., go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui -count=1 ./... under a temp HOME pass.
2026-10-03 09:58:00 +08:00
gateway: a request well past the model's window that a vendor turns away without saying why reaches the agent as too long, at once; Qoder's plugin to 0.2.4 for its token usage (found testing real accounts) — Devin answered a conversation of about 300k tokens for swe-1.7-lightning (a 202,752 window) with a 502 "We are currently experiencing capacity issues…", Qoder one of 1.8M for qmodel (1M) with a 400 "Error in upstream response", each answered at a size the model holds; magpie asked Devin's again in place three times and handed the agent a 502 api_error, so Claude Code and Codex never compacted. tooLongUnsaid (fallback.go) takes a 400/413/422/5xx that says nothing of quota, a rate limit, the account, the model or the conversation's length, for a request whose estimate is 1.2 times the model's window or more (the user's context, else the catalog's; estimate counts four bytes a token, which most text runs under: Devin counted 173,954 tokens of one estimated at 151,851), as one too long: "prompt is too long: about N tokens, more than <model>'s context window of M (<vendor> answered <status>: …)", sent the agent's way (context_length_exceeded, Anthropic's "prompt is too long"), not asked again of it or another account, and nobody rests. TestOverflowTheVendorDidntSay fails on the old code (a 502 asked three times, a 400 handed on) and passes; a request near the window, a small one, a 429, a 401 and a used-up balance keep their errors. Qoder's plugin read its stream only up to the finish, and Qoder sends the usage in a chunk of its own after it, so every Qoder request was counted as 0 tokens: @magpie-community/opencode-qoder-auth 0.2.4 reads it, and the mover's min is raised to it. Tried with real accounts in a sandbox magpie: Devin (chat and Anthropic) and Qoder now give 400 context_length_exceeded after one try, a WorkBuddy and Kiro overflow as before, small requests to all three answer, and Qoder reports its tokens (streamed, not, and with a tool call). go vet, builds (darwin, GOOS=linux, GOOS=windows) and go test -tags nogui -count=1 ./... under a temp HOME pass but a provider timing test that passes rerun.
2026-10-03 16:14:38 +08:00
antigravity: a Gemini reply that says nothing is an error, asked again, not a turn ended as it should (#667: antigravity/gemini-3.8-flash handed pi content [] and zero usage as a normal stop, the agent's run ending mid-task; #636 Bug 3) — Antigravity's Code Assist stream (and any Gemini one) now and then ends with a STOP and no parts, with only reasoning, or with no candidate at all. translate passed the decoder's KStop on and finished the client's stream as it does any reply: Chat's finish_reason "stop" and [DONE], Anthropic's end_turn and message_stop, Responses' response.completed, and a non-streamed reply of 200 with no content — nothing an agent can tell from an answer, and nothing the holdWriter took for a failure, so nobody asked again. Now a reply from a Gemini upstream (CodeAssist, Gemini) holds its end (KStop, KUsage) until something is said in it — text, a call, a search, an image — and one that ended with nothing said, not cut at its length nor by a safety filter (still a refusal, #248), ends with an error event, "<provider>: an empty reply (the model answered nothing; try again)", or a 502 when not streamed. As a 502 before any content it is held, asked again of the same account (lastRetries) or of the next one, and an agent told it retries rather than ends its turn, as Cursor's and Devin's empty replies already are. Other upstreams aren't changed: Claude ending a turn with nothing after a tool's result is its answer. TestGeminiEmptyReplyIsAnError (Factory's Gemini route, which shares the decoder and translate: STOP with no parts, only reasoning, no candidate; Chat, Anthropic and Responses streamed and Chat not; asked again; text and reasoning cut at MAX_TOKENS still go as they came) fails on the old code with the issue's wire shape and passes. Against the owner's real Antigravity account in a sandbox gateway (temp HOME, port 3488): a prompt asking gemini-3.8-flash-low for no output gave, on the old build, finish_reason stop with no content 3/3, and on this one the error event after the retries 3/3; ordinary replies on Chat (streamed and not), Anthropic and Responses, and a bash tool call, are unchanged. go vet, builds (darwin, GOOS=linux -tags nogui, GOOS=windows) and go test -tags nogui -count=1 ./... under a temp HOME pass (a first run had TestClaudeAccountNamedAsItsLogin and an internal/gui fetching test fail, timing tests that passed on the rerun).
2026-10-03 19:07:36 +08:00
gateway: an image model's picture reaches a Gemini client (#620, tao-bai: antigravity/gemini-3.1-flash-image 返回 parts: []) — through /v1beta generateContent and streamGenerateContent the answer came back with no parts, though candidatesTokenCount (1567) said the image was made. The IR had no event for a picture the model made: codeAssistDecoder (Antigravity, Gemini CLI and Factory's Gemini route) turned functionCall, thought and text parts into events and dropped inlineData, and geminiParts / geminiEncoder had nothing to write it back as. A new KImage event (Name the media type, Text the base64) carries it; the collector keeps it as an Image part, and the Gemini renderer and stream encoder send it as inlineData in its place among the text. A thought image (a draft, thought: true) stays with the reasoning and isn't sent, as before; thought text and signatures are untouched. Chat, Responses and Anthropic clients still don't get the picture (their renderers skip Image parts). TestGeminiImageModelReturnsItsImage (a fake Factory Gemini upstream, generateContent) and TestCodeAssistStreamsImages (Code Assist chunks through the Gemini stream encoder) fail without it with parts: []. Not tried against a real Antigravity account; the image part's thoughtSignature isn't returned to the client, so multi-turn image editing on Gemini 3 image models may still need it. go vet ./internal/..., go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui -count=1 ./... under a temp HOME pass.
2026-10-03 10:24:30 +08:00
2026-10-01 16:20:10 +08:00
docs: every built-in subscription a plugin serves once moved says so in its code, and AGENTS.md says where a fix for it goes (user: 对内置的订阅 provider 在代码层面和文档层面加上注释,防止又有别的 agent 在改内置订阅 provider 的代码而不知道生效的其实是对应的插件代码) — a moved Cursor, Devin, Grok, Command Code plan, Kiro, Zed, Qoder, Factory, Xiaomi MiMo, ZCode or WorkBuddy (provider.Moved; since v0.1.642 also the default for a new sign-in, provider.Adopt) is signed in, listed, requested and metered by its @magpie-community plugin, and none of the built-in's code in internal/provider/<name>*.go, internal/gateway/<name>.go, internal/zed or internal/qoder serves it; a fix made there alone never reaches those users. Each of those files now opens with a PLUGIN-SERVED notice naming the subscription's id, its plugin package, where the plugin lives (magpie-community/plugins, packages/<name>) and the mover whose min to raise so moved users get the fixed plugin (keepMovedCurrent); Qoder's adds that Qoder CN isn't moved and still runs there. AGENTS.md (CLAUDE.md imports it) lists the moved built-ins with their packages and movers, how to tell whether a user's is moved (move.state "plugin", onPlugins), the order a fix goes in (the plugin, publish, raise min, the built-in only for those not moved), the magpie code that does run for a moved one (internal/plugin, plugins.go, plugin_usage.go, pluginsignin.go, migrate*.go, app.js's subOf/pluginSubs/startPluginSignIn), and the parity tests. movers in migrate.go and SUBS in app.js point there too. TestMovedBuiltinsSayTheirPlugin fails for a mover whose code carries no PLUGIN-SERVED notice naming its id and package (it failed while a wrapped package name was split). Comments and docs only, no behaviour changed. go vet ./internal/..., go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui -count=1 ./... under a temp HOME pass (TestProvidersAnswerWhileListsComeIn and TestClaudeSignedOut failed once on their timing under load, pass alone; untouched).
2026-10-02 19:37:02 +08:00
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
routing: a group's member can be sent in its vendor's fast mode, beside its own reasoning (vs on Discord: a member "gpt-6.1 sol · high · fast") — a member's effort was already its own (#189, "provider/model:effort") but there was no way to ask for speed, so a group could not hold "gpt-6.1-sol at high, priority" next to slower members. A group now lists in fast the members it sends fast, as Members spells them, kept beside Members rather than in their ids so an older version still routes to them, only not fast (checked: HEAD's build on the same providers.json sends the same members at their effort, no tier, and leaves the file as it was). When the gateway sends to such a member it asks in the upstream's own words, only where the model has a fast mode magpie can ask for (provider.CanFast): service_tier "priority" for a ChatGPT account's GPT models and an OpenAI key's GPT/o-series models on its Responses API, Cursor's -fast variant, and speed "fast" with the fast-mode-2026-02-01 beta for Claude Opus 4.8/5/5.5 on an Anthropic key (api.anthropic.com); a relay, Bedrock, a Claude subscription, a model without one, or a provider since moved off those hosts is sent untouched. It is applied in the agent's own API and carried through translation (parseAnthropic now reads speed), so Chat, Responses and Anthropic agents all get it. A translated request for OpenAI's Chat API is still told no tier (TestCursorPluginFast), hence the Responses requirement. The trace marks the try, the order and the group (fast), shown as "fast" beside the reasoning in Requests and the order. Typed, a member is provider/model[:effort]:fast: magpie group add/set models=… takes it, fast=<m1>,… sets them, models-= takes either spelling, and the group lists and shows members as typed with "· fast"; a model with no fast mode, or a group in the group, is refused. The GUI group editor shows a "Standard speed"/"Fast" toggle (标准速度/快速) beside a member's reasoning only for a model that has one, the card reads "· fast" after the reasoning, and fast follows a member's reasoning change and leaves with a removed member; the TUI's group view takes E to step a member's effort and F to toggle fast. Provider renames carry it. TestFastMemberOpenAI, TestFastMemberAnthropic, TestWithFastWire (gateway, per wire format, a fake vendor recording body and anthropic-beta), TestCanFast, TestSaveGroupFast, TestGroupMembersFast, TestGroupMemberEffortAndFast, TestGroupSaveFast and group-member-fast.test.cjs fail without the change. A sandbox magpie (own HOME, gateway on 3966) with providers at http://api.openai.com and http://api.anthropic.com through a local recording proxy sent priority+high to OpenAI and speed fast + the beta to Anthropic from Chat, Responses and Anthropic clients, and nothing extra for Sonnet or a non-fast member. go vet, go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui ./... pass; group-member-fast, click-scroll, old-webkit, routing-levels, routing-manual, routing-effort, settings-groups pass (Chromium, WebKit, en/zh).
2026-10-01 17:13:34 +08:00
routing: a group in a group is routed by its own routing whatever the outer group's, and an account with five hours and no week is ranked by its five hours (#576: 只有五小时额度的账号在智能/重置前用完下是否合理;嵌套路由组时上层路由是否会干预下层) — a group routed other than In order (Smart, In turn, Least used, Weekly pace, Manual) put every account and key of every group in it into one pool and weighed them together, so A (Smart) holding A-1 and A-2 (each In order) ranked all five of their accounts and keys by Smart and A-1's and A-2's own In order did nothing; only an outer In order let each inner group route itself. planLevel now plans each group in the group by its own routing, whatever the outer's, and weighs it as one beside the outer group's own models' keys, by the first of its own order that isn't resting; where that lands, the inner group is tried whole in its own order, the trace in the same order. As for the five hours: Smart already ranked such an account (Claude Enterprise) by its five hours, the biggest window it has, so ahead of every week renewing later, but a five hours not started (no reset told) counted as a reset not known and went after every week; Allowance.Renewal now has a window not started that says its span renew that long from now, which Smart sorts by (the trace's renews unchanged). Weekly pace took an account with no week as a fresh week with the share used (100/168 per hour), so a week with 80% left and five days to go went ahead of an Enterprise account whose five hours were being lost; it now goes by what its five hours have left per hour until they renew, the whole span when not started, except where the reading may leave a week out — a Claude account only heard of from Claude Code's rate_limit_event, never read whole by /usage (QuotaWindow.partial, claudeUsageEntry.whole) — which keeps the fresh week. The group editor's Smart and Weekly pace hints and the trace's say how an account with five hours and no week is ranked, a hint under Routing says a group among the models keeps its own routing, and the docs (en, zh) say both. TestGroupInGroupKeepsItsOwnRouting (Outer rotating over two In order groups: b c d a without the change, c d a b with it), TestSmartFiveHoursAlone (a five hours not started went after a week in two days), TestFiveHoursAlone, TestClaudeHeardOnlyIsPartial; TestWeeklyPace and TestAllowancePace now pin the five hours alone. settings-groups, group-member-fast, group-found-off, routing-idle-frame pass in WebKit. go vet ./internal/..., go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui -count=1 ./... under a temp HOME pass (TestFXInState, TestProvidersAnswerWhileListsComeIn, TestUserPath, TestClaudeSignedOut flaked; the last passes 3/3 alone).
2026-10-02 23:05:15 +08:00
search: a searcher is asked on the API it searches on, and one that finds nothing gives way to the next searcher and the search APIs (#669: with Automatic resolving to deepseek/deepseek-flash and Tavily set up, every client's web_search got web_search_tool_result.content [], the routing log saying 200) — magpie asks its searcher on Anthropic's API, and DeepSeek's Anthropic API wasn't counted among those that search, so the request was translated onto DeepSeek's first API, Chat; translate chose the API a provider searches on (DeepSeek's Responses) only for a client's request, never for magpie's own search, so DeepSeek got no search tool at all, and deepseek-flash said it couldn't search or wrote its own DSML tool-call markup as text. That text went to the model as what was found, with no page, and was taken as a success, so Tavily was never asked. Now translate sends a searcher to the API it searches on as it does a client; api.deepseek.com searches on Anthropic too (tried: web_search_20250305 there answers web_search_tool_result blocks of real pages with DeepSeek's encrypted_content, and takes magpie's own blocks back in a later turn); a searcher's results are read one by one, title, address and page_age (now carried to the client's block), never its encrypted_content, which stays "" in what magpie sends; a reply with no page and no address in its text is "found nothing", and the search goes to the next searcher magpie would pick (up to 3, within the 2 min), then to the search APIs. TestSearcherAskedWhereItSearches (DeepSeek searching on Responses only: old code asked Chat and gave the DSML text and []), TestDeepSeekNativeSearchResults (DeepSeek's native reply: pages, their ages, no encrypted_content passed on) and TestSearcherFindingNothingGivesWay (a 200 with nothing found goes to Tavily, or to the next searcher first) fail on the old code; TestDeepSeekSearchesOnResponses now expects the Anthropic API to search. With the real DeepSeek key in a sandbox gateway (temp HOME, copy of providers.json, searcher deepseek/deepseek-flash) the issue's curl on aihubmix/glm-5.3-flash got 10 web_search_result pages (weather.com.cn, moji, …) and an answer from them; deepseek/deepseek-flash asked directly with that reply in its history searched natively and answered. No Tavily key here, so the fallback to it was tried only against the fake. go vet ./internal/..., builds (darwin -tags nogui, GOOS=linux -tags nogui, GOOS=windows) and go test -tags nogui -count=1 ./... under a temp HOME pass but TestClaudeRunEndsWhenTheClientLeaves, a timing test that passes alone 3/3.
2026-10-03 19:38:13 +08:00
accounts: Codex or Claude Code can be kept signed in to any of its accounts, not only the first, while the gateway still tries them in their order (Koohoko on #524: 建议不只是支持始终登录"首选"账号,应该可以支持始终登录任一选定的账号。有时候首选只是为了按顺序先消耗它的额度,不一定是用户期望的常用登录账号) — making an account first did two things at once: the gateway tried it first (the account the agent is signed in to always stood ahead of the others, AlsoOn behind it), and Codex or Claude Code was signed in to it (SwitchLogin). "Keep {agent} signed in to the first account" (817a5f50) only stopped magpie moving the sign-in on, so a user who wanted one account's allowance spent first had to sign the agent in to it too. Now beside that tick is a pick, "Keep {agent} signed in to [the first account ▾ | each saved account]" ({agent} 始终登录 [首选账号 ▾]): picking one keeps Provider.KeepLoginAs (providers.json, kept across an editor Save like KeepLogin; POST provider/keeplogin with keepLoginAs) and signs the agent in to it at once, the order the gateway went by then — the one signed in to first — written as the accounts' own first, so the first stays first. While it is kept: the login switcher signs the agent back in to it whenever it is on another and never moves it off, however used up (NextLogin); a drag or Make first sets the order without a sign-in (arrangeLoginsAs), Make first posting provider/arrange; the gateway puts the account signed in to at its own place in that order, not first (perKeyBarred by Provider.LoginRanks), so In order spends the first's allowance first; InUseLogin (the tray's in-use account) is the first in use in the order. The rows keep their order, the first saying First and the one kept saying Signed in (已登录), an account not in use ticked on by its dot rather than a Use that would sign the agent in; the Routing note says the agent stays signed in to that account and requests go in the accounts' order. Picking "the first account", or unticking, lets it go and signs the agent in to the first again, so the one signed in to is the first once more. TestKeepLoginAsChosen (signed in to the chosen at once, the order kept, stays at 100%, moved off it goes back, a drag changes the order not the sign-in, the first's again when let go) and TestKeptLoginStandsAtItsPlace (through the gateway In order: kept on spare, the first, acct-1, is tried first; acct-2 without the change) are new; keep-login.test.cjs now also picks an account (the post, First and Signed in, Make first arranging, back to the first, every string's Chinese, the page not moving) and passes 8/8 in WebKit; account-return, routing-account-order, account-arrange and click-scroll pass in WebKit. No live agent config, keychain or vendor was touched. go vet ./internal/..., go build (darwin GUI), go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui -count=1 ./... under a temp HOME pass.
2026-10-02 23:30:54 +08:00
gateway: a translated or relayed stream keeps its client alive while its provider is (#436, congee949: Chat → Responses dropped the upstream's ": keepalive" and Codex hit "idle timeout waiting for SSE" on a live connection) — a reply magpie translates was read event by event and written only when an event had something for the client, so a provider's SSE keepalives, and events with nothing to translate, reached no one, and a reply that thought quietly for five minutes ended in Codex's idle timeout and a retry. Now, whenever the provider is heard from and the client has heard nothing for a second, magpie sends the client a keepalive in its own protocol: response.in_progress again on Responses (Codex's timeout counts events, never a comment — a sandboxed codex 0.159.2 on a fake provider sending only comments timed out, one sending response.in_progress didn't), ping on Anthropic (as Anthropic does; Claude Code's watchdog counts it), an SSE comment on Chat Completions, nothing on Gemini (Google's SDK can't read past one). A provider that sends nothing at all is not covered for, so a dead connection still ends at the client's timeout. A reply relayed from the Claude subscription's bridge, Kiro, Cursor, Devin, search, Qoder, Zed or Command Code is kept alive every 15s while it goes quiet, for up to 5 minutes since its last event. Native passthrough is unchanged, and prompts and headers are unchanged. The routing trace no longer drops a request still going for the 60 finished after it: the oldest finished ones go first, unless more than 120 are going at once. TestTranslatedStreamKeepsClientAlive (the issue's repro: 20ms keepalives, 150ms idle timeout; Chat → Responses, Chat → Anthropic, Responses → Chat), TestRelayKeepsClientAlive and TestTraceKeepsInflight fail without the change; TestTranslatedSilenceStillTimesOut keeps a silent provider uncovered. In a sandbox CODEX_HOME, codex exec through the gateway on a provider sending 6s of keepalives with stream_idle_timeout_ms 2500 answers, and without the change fails with idle timeout waiting for SSE. go vet, go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui ./... pass.
2026-10-01 22:36:36 +08:00
gateway: each gateway key can have its own limit, a token total and/or an estimated cost per day, week or month, and a spent key is refused before any provider is asked (margbug01 on #585, v0.1.671: 每个网关密钥都能设置自己的用量上限) — a gateway key named who called and the Usage page grouped its calls, but nothing held a key to an amount, so one client shared on the network could spend a subscription's whole window. A key now carries limit {period day|week|month, tokens, cost, cacheReads} in caller-keys.json, set with the new "limit-key" change. Windows are calendar ones in local time (from midnight, from Monday, from the 1st) and a call counts in the window it started in. Tokens are a call's uncached input, output and cache writes, plus cache reads when the limit says so; cost is an estimate at the Usage page's effective prices (usage.NewCoster), a call with no known price adding none and counted as unpriced. What a key used is summed from usage.jsonl (usage.Visit), so it survives a restart; the new internal/budget keeps the sums in memory per window and appendUsage writes a key's record through budget.Append, which appends and counts under one lock so a window read meanwhile never counts a call twice. Before a keyed POST reaches the mux, keyLimited reserves for it: its body's size in tokens (4 bytes a token) plus the key's mean output per call this window, at the window's cost per token or the named model's input price; it is let in while used + reserved is under the cap, so requests sent at once overshoot by about one call, and the reservation is released when the request ends, a streamed reply being settled then with the usage its vendor reported. A spent key gets a 429 in its API's error shape (Anthropic, OpenAI chat/responses, Gemini) naming the key, the cap and the reset time, with Retry-After, X-Should-Retry: false and X-Magpie-Limit-Reset, recorded as a rejected call (gateway_key_limit); other keys go on. Requests from this computer without a gateway key are not limited; a key used from this computer is. count_tokens and bridge callbacks are never held. GET /v1/magpie/limit tells a key its own status. Gateway keys rows show a Limit badge (what is used of it, per what, Spent in red) and a line under a limited key (used of limit, left, resets / refused until, in flight; unpriced calls in its title); Limit opens an editor under the row (window, tokens with k/m/万/亿, cost in US$, Count cache reads too, what is counted) staged until Save, marked unsaved, Cancel/Escape dropping it, a bad amount disabling Save, No limit removing it; en and zh; no left-border accent; no click scrolls. The CLI gains magpie gateway-key limit <id> [off | day|week|month --tokens N --cost USD --cache-reads] and a LIMIT column in list. TestKeyLimitRefusesBeforeUpstream (two calls within 700 tokens, the third a 429 the fake upstream never sees, message/type/headers, the rejected record, Anthropic's shape on /v1/messages, another key and a keyless loopback request let in, still refused after budget.Forget as after a restart, a raised and a removed limit let in, /v1/magpie/limit), TestKeyLimitHoldsParallelRequests (20 at once against 2000 tokens: some refused, at most one call over, no reservation left; without the reservation all 20 got in and 7350 were used), TestKeyLimitWindowResets, TestLimitWindow, TestLimitValid and TestGatewayKeyLimitCLI are new; gateway-key-limit.test.cjs (WebKit, en and zh) is new and passes, as do gateway-caller-keys, gateway-connect-keys, api-key-theme and click-scroll (whose request-pick case flaked once under a parallel run and passes alone, as on main). go vet ./internal/..., go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui -count=1 ./... under a temp HOME pass (TestFXInState's known TempDir-cleanup flake aside).
2026-10-03 00:18:16 +08:00
library: a remote MCP server signed in once in magpie is used by every agent without signing in again (#615) — a streamable-HTTP server behind OAuth (Neon, Notion, Supabase) had each agent run its own sign-in. The server's editor now has Sign in: magpie finds the authorization server from the server's 401 (RFC 9728 protected-resource metadata, RFC 8414/OIDC metadata, else the 2025-03 /authorize, /token, /register at its origin), registers itself (RFC 7591, a 127.0.0.1 redirect), opens the browser with PKCE S256 and the resource, and keeps the tokens in mcp-signins.json beside its other account secrets (0600, never the keychain). While signed in, the agents are given the gateway's /mcp/<name> for that server, without the Authorization the library has for it; the gateway passes POST (JSON or event stream), the GET stream and DELETE on with the Mcp-Session-Id and magpie's token, renews it 2 minutes before it runs out and once on a 401, one renewal per server at a time, and a sign-in the server won't renew is answered 502 "sign in to it again" (never a 401 the agent would act on). Signing out, a new URL or an SSE server gives the agents the server's own address as before; a rename keeps the sign-in and removing the server drops it. /mcp/ is local-only or LAN-shared with the key, and refuses a request with an Origin or a Host not naming this computer, so a web page can't borrow the sign-in through the browser (form posts, DNS rebinding), and isn't counted as busy or key-limited. WSL agents are given their distro's gateway address. The row says Signed in or Sign-in ran out; zh strings added. Tests: TestSignIn, TestSignInWithoutMetadata, TestSignInPage, TestNothingToSignIn, TestTokenRenewsBeforeItRunsOut (8 calls, 1 renewal), TestRenewRefused, TestRenewRefusedForGood, TestMCPProxy, TestMCPProxyStaysHere, TestMCPProxyNotForWebPages (fails without the Origin/Host check), TestSignedInServerGoesThroughMagpie against a local fake OAuth+MCP server; mcp-signin.test.cjs (webkit, en and zh). Not tried against a real Neon/Notion/Supabase sign-in. go vet ./internal/..., go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui -count=1 ./... under a temp HOME pass.
2026-10-03 09:37:39 +08:00
providers: a Kilo Gateway preset serves Kilo Code's free models with no key (lml on Discord: the community plugin's free models come from OpenCode Zen, KiloCode Gateway and Cline) — Kilo's gateway is OpenRouter's API at https://api.kilo.ai/api/openrouter (KILO_OPENROUTER_BASE in Kilo-Org/kilocode packages/kilo-gateway; /api/gateway is the same), its list GET {base}/models in OpenRouter's shape with an isFree flag, readable with no key, and its docs (kilo-docs gateway/authentication.md) and backend (Kilo-Org/cloud web-shared ai-gateway/handlers/llm-proxy.ts, is-free-model.ts: kilo-auto/free, ":free", openrouter/free, stealth/*-alpha, Kilo's own free models) serve a free model to a caller with no credential, by IP, 200 requests an hour. The new "kilo" preset (Kilo Gateway, NoKey) is Ready with no key: its list is the gateway's own, asked as the Kilo CLI asks it, the free models alone without a key and every model with one, the free ones (isFree, or free by id) marked Free and priced at nothing in ListPrice; a model that says it takes no tools, or that makes no text (Lyria), is left out. Requests to Kilo's host (the gateway's and the Test button's) carry the Kilo CLI 7.8.3's headers (packages/opencode session/llm/request.ts, kilocode/const.ts, kilo-gateway headers.ts): HTTP-Referer https://kilocode.ai, X-Title Kilo Code, User-Agent Kilo-Code/7.8.3, X-KILOCODE-EDITORNAME "Kilo CLI 7.8.3", and on a chat x-kilocode-mode code with the conversation as its task (X-KILOCODE-TASKID, x-session-affinity, X-Session-Id, a ses_ id as for OpenCode), which the gateway keys its prompt cache on; no other provider gets them. Without a key no Authorization is sent: Kilo's clients send "Bearer anonymous", which the gateway reads as no credential only until no released client sends it (tokens.ts ANONYMOUS_BEARER_SENTINEL, "Delete this constant once no released client sends it"), and any other bearer it can't verify is refused with 401 rather than served as anonymous; pi-freeflow measured that a placeholder bearer gets 401 INVALID_TOKEN and no Authorization gets 200 on every free model. A key, when given, goes as a Bearer and reaches the paid models. The key field can now say more than "optional for local servers" (PresetDef.KeyHint: "optional: free models need no key", 可不填:免费模型无需密钥); Kilo's icon is its own (kilocode packages/ui provider/kilo.svg, MIT). Only Chat is set; Kilo's Responses and Anthropic routes are not used. One GET of the public /models was made while looking (399 models, 18 free); no chat request was made to Kilo. TestKiloModels (a fake gateway: asked as the CLI with no Authorization, the free models alone without a key and all with one, a model free by its flag priced at nothing), TestKiloClient and TestKiloClientHeaders (the CLI's headers and no Authorization on a keyless request through the gateway, the key on a keyed one, none of them on another provider) fail without the change. go vet ./internal/..., go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui ./internal/... pass (TestClaudeRunEndsWhenTheClientLeaves, TestUserPath and TestClaudeSignedOut failed once under the full run's load, pass on the rerun; none touches Kilo).
2026-10-02 14:03:31 +08:00
docs: every built-in subscription a plugin serves once moved says so in its code, and AGENTS.md says where a fix for it goes (user: 对内置的订阅 provider 在代码层面和文档层面加上注释,防止又有别的 agent 在改内置订阅 provider 的代码而不知道生效的其实是对应的插件代码) — a moved Cursor, Devin, Grok, Command Code plan, Kiro, Zed, Qoder, Factory, Xiaomi MiMo, ZCode or WorkBuddy (provider.Moved; since v0.1.642 also the default for a new sign-in, provider.Adopt) is signed in, listed, requested and metered by its @magpie-community plugin, and none of the built-in's code in internal/provider/<name>*.go, internal/gateway/<name>.go, internal/zed or internal/qoder serves it; a fix made there alone never reaches those users. Each of those files now opens with a PLUGIN-SERVED notice naming the subscription's id, its plugin package, where the plugin lives (magpie-community/plugins, packages/<name>) and the mover whose min to raise so moved users get the fixed plugin (keepMovedCurrent); Qoder's adds that Qoder CN isn't moved and still runs there. AGENTS.md (CLAUDE.md imports it) lists the moved built-ins with their packages and movers, how to tell whether a user's is moved (move.state "plugin", onPlugins), the order a fix goes in (the plugin, publish, raise min, the built-in only for those not moved), the magpie code that does run for a moved one (internal/plugin, plugins.go, plugin_usage.go, pluginsignin.go, migrate*.go, app.js's subOf/pluginSubs/startPluginSignIn), and the parity tests. movers in migrate.go and SUBS in app.js point there too. TestMovedBuiltinsSayTheirPlugin fails for a mover whose code carries no PLUGIN-SERVED notice naming its id and package (it failed while a wrapped package name was split). Comments and docs only, no behaviour changed. go vet ./internal/..., go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui -count=1 ./... under a temp HOME pass (TestProvidersAnswerWhileListsComeIn and TestClaudeSignedOut failed once on their timing under load, pass alone; untouched).
2026-10-02 19:37:02 +08:00
gateway: the model list comes as ids one a line with ?format=text, to paste into ZCode's custom provider from a magpie on another computer (悠悠哥 on Discord, v0.1.671: ZCode pointed at magpie on a NAS, http://192.168.0.200:3425, Anthropic Messages, never got the model list, while at 127.0.0.1 it did) — ZCode (3.14.3) asks a custom provider for no list at all: its settings take the models typed one a line ("模型列表 · 每行一个模型名称"), its "智能配置" matches built-in metadata by id and base URL, and neither its desktop app nor its CLI requests /models anywhere. What filled it at 127.0.0.1 was a magpie on that computer, which writes its models into ZCode's own provider_config.json (personalModelIds) and config.json under the provider "magpie" at its own 127.0.0.1 gateway; a magpie on a NAS can't write that computer's files, so the provider pointed at it is left with what was typed. The gateway itself answers another computer as it answers this one (no Host check, the key read wherever the client puts it, 1c047f36 in v0.1.664). GET /v1/models?format=text (and /models) now answers text/plain, the ids in the list's order one a line, so a browser on that computer opening http://<nas>:3425/v1/models?format=text&key=<gateway key> gets what to paste into ZCode's model list; without a key from the network it is refused as the JSON list is. TestLANModelsAsText (from 192.168.0.9 to Host 192.168.0.200:3425: text/plain, the JSON list's ids in its order, none empty or JSON; no key 401) fails without the change. go vet ./internal/..., go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui -count=1 ./... under a temp HOME pass.
2026-10-02 23:47:13 +08:00
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
test: one list of the agent folders a sandbox clears, so no sandbox misses an agent (yetone, on #522: A shared list of env vars for the other sandboxes would be a welcome follow-up) — every sandbox kept its own list of the variables that point at an agent's folder, and each had drifted: the gateway's cleared 5 of the 32 magpie reads, the provider package's 2, the tui's 10, the library's 20. A sandbox clearing 2 meets whatever Grok, Kimi, Hermes, MiniMax, Qoder or ZCode the developer has installed, which is how #522 came about — Qoder's two were in the library's list and in none of the four that failed. internal/agentenv is now the one list, and the 21 sandboxes that fake an environment with no agent in it clear it, each keeping beside it the variables that are not an agent's: APPDATA and LOCALAPPDATA, which a sandbox sets to a folder of its own rather than clearing, VISUAL and EDITOR. A name in the list is one magpie reads, and TestVarsAreRead walks the sources to say so, which is how two names a sandbox had been clearing turned out to be read by nothing at all — GEMINI_CLI_HOME and OPENCODE_CONFIG in internal/backup's, while OPENCODE_CONFIG_DIR, the one internal/agent reads (agents.go:545), was not in it. Where a sandbox set an agent's folder to a fixture of its own, the clearing now comes first, since a list of 32 wipes what a list of 5 left standing. Not converted: the omp tests' four variables, which are the ones omp's own lookup consults (a Pi fork) rather than every agent's, and the warm-up's Claude Code traffic flags, which are not folders — CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC stays out of the list for that reason too, its one test setting it on purpose. On Windows 11 the whole suite answers as it did before the change: 31 packages pass, and the same 15 tests in internal/library, internal/gateway and internal/agent fail for what they failed for before (symlinks this machine has no privilege for, and TestSuffixModesReachAgentFiles). go vet ./..., go build -tags nogui (windows, GOOS=linux, GOOS=darwin), and go vet -tags nogui for linux and darwin over every package touched pass. (#543)
2026-10-02 17:22:16 +08:00
library: a remote MCP server signed in once in magpie is used by every agent without signing in again (#615) — a streamable-HTTP server behind OAuth (Neon, Notion, Supabase) had each agent run its own sign-in. The server's editor now has Sign in: magpie finds the authorization server from the server's 401 (RFC 9728 protected-resource metadata, RFC 8414/OIDC metadata, else the 2025-03 /authorize, /token, /register at its origin), registers itself (RFC 7591, a 127.0.0.1 redirect), opens the browser with PKCE S256 and the resource, and keeps the tokens in mcp-signins.json beside its other account secrets (0600, never the keychain). While signed in, the agents are given the gateway's /mcp/<name> for that server, without the Authorization the library has for it; the gateway passes POST (JSON or event stream), the GET stream and DELETE on with the Mcp-Session-Id and magpie's token, renews it 2 minutes before it runs out and once on a 401, one renewal per server at a time, and a sign-in the server won't renew is answered 502 "sign in to it again" (never a 401 the agent would act on). Signing out, a new URL or an SSE server gives the agents the server's own address as before; a rename keeps the sign-in and removing the server drops it. /mcp/ is local-only or LAN-shared with the key, and refuses a request with an Origin or a Host not naming this computer, so a web page can't borrow the sign-in through the browser (form posts, DNS rebinding), and isn't counted as busy or key-limited. WSL agents are given their distro's gateway address. The row says Signed in or Sign-in ran out; zh strings added. Tests: TestSignIn, TestSignInWithoutMetadata, TestSignInPage, TestNothingToSignIn, TestTokenRenewsBeforeItRunsOut (8 calls, 1 renewal), TestRenewRefused, TestRenewRefusedForGood, TestMCPProxy, TestMCPProxyStaysHere, TestMCPProxyNotForWebPages (fails without the Origin/Host check), TestSignedInServerGoesThroughMagpie against a local fake OAuth+MCP server; mcp-signin.test.cjs (webkit, en and zh). Not tried against a real Neon/Notion/Supabase sign-in. go vet ./internal/..., go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui -count=1 ./... under a temp HOME pass.
2026-10-03 09:37:39 +08:00
library: a remote MCP server signed in once in magpie is used by every agent without signing in again (#615) — a streamable-HTTP server behind OAuth (Neon, Notion, Supabase) had each agent run its own sign-in. The server's editor now has Sign in: magpie finds the authorization server from the server's 401 (RFC 9728 protected-resource metadata, RFC 8414/OIDC metadata, else the 2025-03 /authorize, /token, /register at its origin), registers itself (RFC 7591, a 127.0.0.1 redirect), opens the browser with PKCE S256 and the resource, and keeps the tokens in mcp-signins.json beside its other account secrets (0600, never the keychain). While signed in, the agents are given the gateway's /mcp/<name> for that server, without the Authorization the library has for it; the gateway passes POST (JSON or event stream), the GET stream and DELETE on with the Mcp-Session-Id and magpie's token, renews it 2 minutes before it runs out and once on a 401, one renewal per server at a time, and a sign-in the server won't renew is answered 502 "sign in to it again" (never a 401 the agent would act on). Signing out, a new URL or an SSE server gives the agents the server's own address as before; a rename keeps the sign-in and removing the server drops it. /mcp/ is local-only or LAN-shared with the key, and refuses a request with an Origin or a Host not naming this computer, so a web page can't borrow the sign-in through the browser (form posts, DNS rebinding), and isn't counted as busy or key-limited. WSL agents are given their distro's gateway address. The row says Signed in or Sign-in ran out; zh strings added. Tests: TestSignIn, TestSignInWithoutMetadata, TestSignInPage, TestNothingToSignIn, TestTokenRenewsBeforeItRunsOut (8 calls, 1 renewal), TestRenewRefused, TestRenewRefusedForGood, TestMCPProxy, TestMCPProxyStaysHere, TestMCPProxyNotForWebPages (fails without the Origin/Host check), TestSignedInServerGoesThroughMagpie against a local fake OAuth+MCP server; mcp-signin.test.cjs (webkit, en and zh). Not tried against a real Neon/Notion/Supabase sign-in. go vet ./internal/..., go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui -count=1 ./... under a temp HOME pass.
2026-10-03 09:37:39 +08:00
2026-10-02 06:07:57 +08:00
gateway: an earlier assistant turn's thinking goes to Mistral as the thinking part Mistral itself returns, and a vendor that turns it away inside messages is asked again without it (#494: Mistral's 422 extra_forbidden for OpenCode's reasoning_content, after any tool call) — OpenCode sends an earlier assistant turn back with its thinking in reasoning_content, a tool-call turn too (content null), and magpie relayed it as sent; Mistral refuses any field it doesn't know, loc ["body","messages",2,"assistant","reasoning_content"], so a Mistral reasoning model (GLM on api.mistral.ai) in OpenCode failed as soon as it used a tool. The #393 retry only dropped a fixed list of top-level fields. To Mistral (its preset, or api.mistral.ai) an assistant message's reasoning_content (or reasoning, or reasoning_details' text) now goes as a leading {"type":"thinking","thinking":[{"type":"text","text":…}]} part of its content, its text after it as a text part, its tool_calls as they were, so the thinking stays in context, as the reporter found Mistral takes it; the other messages keep their bytes. Any other Chat upstream answering extra_forbidden located in messages for one of those fields is asked again with it left out of the messages, and once that works it is left out for that provider from then on; DeepSeek, Kimi and every vendor that takes it are sent it as before. Prompts and headers are unchanged. TestMistralReasoningAsThinking (the issue's request, and a tool-call turn) and TestMessageReasoningRefused fail without the change. go vet and the darwin build pass; the full test and cross-build chain could not run, the disk being full.
2026-10-02 09:23:29 +08:00
providers: a relay's APIs found at a click, and a model asked on the one API its vendor serves it on (01huadalang on Discord: 能不能加个供应商检测协议功能,类似 cc 的功能一键检测支持什么协议,发一个最小请求看是否返回200,还有每个模型自定义协议,例如有的供应商一个 api 里有很多模型但是不同协议) — a custom provider was given one base URL and a protocol picked by hand, and which of Chat Completions, Responses and Anthropic Messages a relay answered was learned by trying, one Save and Test at a time. A model's API came only from the vendor's list or a built-in (Copilot, Factory, Bedrock, OpenCode), so a relay serving some models on chat completions and others only on /v1/messages behind one key had every model tried on chat first, and the ones it had no channel for there failed before the gateway got to Anthropic's. The editor's Base URL now has Detect APIs, with a model to try beside it: it posts provider/detect with the form as it stands (the key typed, or the saved one), and Provider.Detect sends each of the three APIs Test's smallest request (tiny: 16 tokens, not streamed) at the URL the provider has for it, else at the URL typed as that API takes it (DetectBase: an endpoint pasted whole cut to its base, …/v1 for OpenAI's two, the root for Anthropic's), in parallel, and says what each answered. With no model typed, one is picked from the provider's list, else from the vendor's /models (asked then and kept nowhere): a Claude model for Anthropic's, an OpenAI one for Responses. Only the URLs typed are asked, only at the click, and nothing is saved. The editor shows each API's URL with its time or its status and error; "Use these" sets the URLs of those that answered (one found missing there, 404 or 405, cleared) and moves the Base URL's protocol to one that did, with no redraw and no scroll. A model's API is new in Names & levels (settings.ModelAPIs, "<provider>/<model>"): for a key's provider with URLs for two or more APIs, each model can be set to Auto or to one of them. It is staged with the rest and made with the Save (ModelPref.API, SetModelAPI), which refuses an API the provider has no URL for and a sign-in's provider. Once set, APIs() returns that API alone, so the gateway, Native and a model's test all ask it there; it is ignored once its URL is gone. After a detection with a model typed where only some APIs answered, "Ask <model> on <API> only" stages the same thing. Gemini isn't probed, since magpie never asks a vendor on it.
2026-10-02 21:54:07 +08:00
agents: Muse Code, Meta's muse CLI, is an agent magpie sets up (Discord: Muse Code 能加进来吗) — magpie points its endpoint_transport (~/.config/muse/settings.json, $XDG_CONFIG_HOME) at the gateway with auth "none", so the Meta sign-in's token is neither read nor sent, and sets its model to the catalog id; the endpoint and model the user had are stashed and put back on reset, the file's other keys (mcpServers, tui, proxy…) kept, and a settings file magpie had to make (with the schema_version Muse refuses one without) goes again. Muse lists its models from /muse-code/models on its endpoint's host before every session and starts none without it, so the gateway now serves that list, each row with the metadata["muse-code"] Muse shows and a limit with both window and reply size (one without either is hidden; none has it ask for 128K-token replies). A Responses stream the gateway translates from another API started with output null, which Muse reads as a broken stream and retries ten times; it is an empty list, as OpenAI's is. Its requests (muse-build/…) count under Muse Code in usage; Meta's icon from dev.meta.ai. Checked with muse 1.4.2 in a sandbox HOME against a sandbox gateway: exec answered through a Responses fake and through a chat fake (25 tools flattened, a read_file round trip), and switching back left settings.json byte-identical, or removed the one magpie made. TestMuse, TestMuseNoSettings, TestMuseModels and TestResponsesStartOutputList fail without the change (TestDriftUnwiredEveryAgent covers it too). go vet, go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui ./... pass (TestSavedButSignedOutClaudeSaysWhy flaked once, passes alone); click-scroll, old-webkit pass.
2026-10-01 14:23:04 +08:00
models: Provider in model names has a third way, a name the user gave used just as they wrote it (#92 marsxxl: v0.1.591 的 Codex 下拉里自定义名称又带上了 · WorkBuddy / · Claude Code) — 559ce021 (#203) had a custom name keep " · <provider>" after it like any other, so "Opus 5.5" read "Opus 5.5 · Claude Code" in every list Labels feeds (Codex's model_catalog_json and the gateway's Codex /models, Pi's, OpenCode's, ZCode's, OMP's, Muse's), while #92 had been promised the name as written. Both are kept: the setting is now Off / Not on names I set (自定义名称不带供应商) / On. On stays the default and keeps #203's behaviour, so nobody's list changes unasked; own leaves a name the user gave as it is and a vendor's name with its provider's; off is #335's plain names. Stored as plainOwnNames beside plainNames, so a settings.json of before reads as it did (plainNames: true is off). POST /api/settings/plain-names takes mode on|own|off (a body of on alone still works), magpie model suffix on|own|off, a Settings save keeps it, and /v1/models' magpie_label now follows the setting through Labels as the agents' lists do. The row's hint is one line that fits (en, zh), and its zh name says 供应商 as the rest of the app. Prompts and headers are unchanged. TestSuffixMode, TestSuffixModesReachAgentFiles (Codex model_catalog_json and Pi models.json), TestModelsListSuffixModes (/v1/models magpie_label) and model-suffix-own.test.cjs (Chromium and WebKit, en and zh) fail without the change; TestModelName and TestModelNameAndLevelsReachAgentFiles keep #203's assertions. go vet, go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui ./... pass (TestConfiguredModelsAdvertiseImageInput load-flaky, passes alone); plain-names, click-scroll, old-webkit pass.
2026-10-01 23:20:57 +08:00
codex: a group's effort for the turn keeps Codex's reasoning.context, and Codex's custom tools (code mode's exec, apply_patch) reach a model magpie translates for (#534, #520 follow-up, azir12345: since v0.1.637 a Plus account's gpt-6.1-sol fails with 400 "X-OpenAI-Internal-Codex-Responses-Lite requires reasoning.context to be all_turns", and Codex said it had only the collaboration and wait tools, no terminal) — ChatGPT's catalog marks gpt-6.1-sol (as gpt-6-sol, gpt-6-luna, gpt-5.6-*) use_responses_lite with tool_mode code_mode_only, and Codex asks for the bare slug with its native metadata: the X-OpenAI-Internal-Codex-Responses-Lite header (add_responses_lite_header, which magpie forwards with the other x-openai-* headers), reasoning {"effort","summary","context":"all_turns"} (build_reasoning), and its tools moved into an additional_tools item, every other tool reached through exec, a custom (freeform, lark grammar) tool in the "functions" namespace, beside wait and the collaboration namespace. magpie routes the bare id to the group auto-gpt-6-1-sol, and the effort the group picks for the turn went through withEffort, whose Responses branch rebuilt reasoning as {effort, summary} and dropped context, so ChatGPT refused the Lite request; it now sets only the effort (withBodyEffort), keeping every other field of reasoning as Codex sent it. A member served by translation (Chat, Anthropic, a Responses plugin) had parseResponses drop custom tools and ignore custom_tool_call / custom_tool_call_output items, so exec vanished and the model saw only wait and the collaboration tools; a custom tool (top-level or in a namespace) is now offered as a function taking one string "input", its description its own plus a note on input and its grammar, an earlier custom_tool_call goes to the model as a call with {"input":…} and its output as the call's result, and the model's call goes back to Codex as the custom_tool_call it runs (name and namespace as Codex knows them, input from the "input" argument, else the arguments as given), streamed without function_call_arguments events and in the non-streamed reply alike. This also gives translated models Codex's freeform apply_patch, which was dropped silently before. The Lite header is still forwarded as it was; nothing showed it needs dropping. Codex's base_instructions and identity line are untouched. No request was made to ChatGPT. TestWithEffort's Lite case fails without the decide.go change ("want \"context\":\"all_turns\""); TestCodexCustomToolsTranslated (a code-mode Lite turn through a fake Chat provider: exec offered with input and its grammar, the earlier call and output shown, the model's exec call back as a custom_tool_call with its input and no arguments) and TestCodexCustomToolNamespaced (namespaced and non-JSON calls, renderResponses) cover the tools; TestAdditionalToolsTranslated, TestNamespacedToolsOffered and TestGrokPluginGetsNamespacedTools now expect the custom tools offered. go vet ./internal/..., go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui -count=1 ./... under a temp HOME pass (TestDevinSignedInOnlyInMagpie failed once on its wait, passes alone; untouched).
2026-10-02 15:59:14 +08:00
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
site: usemagpie.ai draws itself in Omarchy's look on Omarchy, in the theme Omarchy has on (owner: 整个官网和文档页面能否智能识别是不是 Omarchy 在访问,如果是 Omarchy 在访问,那么整个页面风格能否变成 Omarchy 风格?而且能够适配当前 Omarchy 就更牛逼了) — every page (home, /import, the docs in en/zh/ja) loads omarchy.js in a Linux desktop browser only; it tells Omarchy by its font (a page may load local("omarchy"), which Omarchy installs system-wide and nothing else has), remembers it, and from then on paints before the page shows: the theme's palette, the system monospace (Omarchy's font), square corners, no shadows or glass, a solid bar, buttons and what's chosen in the accent, images framed in it. Until magpie tells the theme it takes Omarchy's default (tokyo-night, or catppuccin-latte on a light desktop). magpie's gateway answers GET /v1/magpie/omarchy (name, mode, stamp and the CSS variables magpie's own windows use) to this machine only, readable only by usemagpie.ai (CORS + Private Network Access; MAGPIE_SITE_ORIGIN adds an origin to try the site locally), 404 off Omarchy. The page asks it by itself only once the browser allows it to reach this computer; a chip in the corner (Omarchy's logo and the theme) asks once on a click, lists the 22 built-in themes to pick one, and turns the look off; ?omarchy=1|0|<theme> sets it anywhere. Followed while the page shows: a theme set in Omarchy's menu comes through in about 3 s. Tried in the omarchy VM's Chromium 152 with this magpie (nogui serve): detection, the home page and zh docs in tokyo-night, the chip's menu, following the computer, and omarchy-theme-set catppuccin-latte repainting the open page light. TestOmarchyThemeForTheSite covers 404 off Omarchy, the theme and its CORS for usemagpie.ai only, LAN refused and the preflight. go vet, builds (darwin, GOOS=linux, GOOS=windows) and go test -tags nogui -count=1 ./... under a temp HOME pass. The permission prompt from the public site wasn't clicked through in a real browser.
2026-10-03 16:01:53 +08:00
site: usemagpie.ai draws itself in Omarchy's look on Omarchy, in the theme Omarchy has on (owner: 整个官网和文档页面能否智能识别是不是 Omarchy 在访问,如果是 Omarchy 在访问,那么整个页面风格能否变成 Omarchy 风格?而且能够适配当前 Omarchy 就更牛逼了) — every page (home, /import, the docs in en/zh/ja) loads omarchy.js in a Linux desktop browser only; it tells Omarchy by its font (a page may load local("omarchy"), which Omarchy installs system-wide and nothing else has), remembers it, and from then on paints before the page shows: the theme's palette, the system monospace (Omarchy's font), square corners, no shadows or glass, a solid bar, buttons and what's chosen in the accent, images framed in it. Until magpie tells the theme it takes Omarchy's default (tokyo-night, or catppuccin-latte on a light desktop). magpie's gateway answers GET /v1/magpie/omarchy (name, mode, stamp and the CSS variables magpie's own windows use) to this machine only, readable only by usemagpie.ai (CORS + Private Network Access; MAGPIE_SITE_ORIGIN adds an origin to try the site locally), 404 off Omarchy. The page asks it by itself only once the browser allows it to reach this computer; a chip in the corner (Omarchy's logo and the theme) asks once on a click, lists the 22 built-in themes to pick one, and turns the look off; ?omarchy=1|0|<theme> sets it anywhere. Followed while the page shows: a theme set in Omarchy's menu comes through in about 3 s. Tried in the omarchy VM's Chromium 152 with this magpie (nogui serve): detection, the home page and zh docs in tokyo-night, the chip's menu, following the computer, and omarchy-theme-set catppuccin-latte repainting the open page light. TestOmarchyThemeForTheSite covers 404 off Omarchy, the theme and its CORS for usemagpie.ai only, LAN refused and the preflight. go vet, builds (darwin, GOOS=linux, GOOS=windows) and go test -tags nogui -count=1 ./... under a temp HOME pass. The permission prompt from the public site wasn't clicked through in a real browser.
2026-10-03 16:01:53 +08:00
routing: In order keeps sending to the first account until it is used up or refused, and a tick keeps Codex or Claude Code signed in to the first account (#530: an account In order at 98% of its week, its five hours untouched, stopped getting requests; #524: "不允许切换首选账号的登录状态", the agent's own sign-in must stay on the account made first while the gateway still spreads requests) — since #209 magpie signs Codex and Claude Code in to the next ticked account with room once the one they are on reaches 98% (provider.SpentShare, the share Smart counts an account spent at), and since #408 back once the first is under 90%. The account the agent is signed in to is the one every routing tries first (the others stand behind it, AlsoOn), so In order the 98% switch put the second account ahead of the first, which is what #530 saw as the first being dropped; and the gateway counted an account at 98% as full whatever the routing, so a passing failure on it (a 502) rested it until its week renewed (39 hours there), not for the usual backoff. Now SpentShareOf gives 100 In order and 98 otherwise: the login switch moves the agent off an account In order only once a window of it is used up, the gateway's full (the rest after a failure, the quota rest's window) and affinity's "spent" go by the same share, and a vendor's 429 or usage-limit refusal still rests the account until it says it's back, as Smart does; a window used up is out, as before. Smart, In turn and Least used are unchanged. For #524, the subscription keeps KeepLogin (providers.json, beside Routing, kept across an editor Save; POST provider/keeplogin, only for Codex and Claude Code): with it on, magpie never moves the agent off the account it is signed in to, however used up, while the gateway still routes requests through magpie to the other ticked accounts; a pending return to the first (#408) still happens, that being the first. The tick sits at the foot of the Accounts list, under the rows where Make first is, shown only for Codex and Claude Code with more than one account on: "Keep {agent} signed in to the first account" / "{agent} 始终登录首选账号", its tooltip saying requests through magpie still go to the other ticked accounts. The Routing note follows it: kept, it says the agent stays signed in to the first whatever it has left; In order, that magpie moves it on once it is used up; otherwise the 98% wording as before. TestCodexLoginOrderedAndKept (In order stays at 98% and moves at 100%, back once renewed; Smart moves at 98%; kept, it stays at 100% and the setting survives a SetRouting; let go, it moves) and TestOrderedKeepsAnAccountAt98 (In order an account at 98% isn't full and a 502 rests it by backoff, Smart rests it to its week, a refusal rests it either way) fail without the change. keep-login.test.cjs (en and zh, Chromium and WebKit: the tick, its tooltip and Chinese strings, the post, the note when kept and In order, no tick with one account, the page doesn't move) fails without the UI and passes with it (12/12, Playwright 1.61); account-return, routing-account-order and account-arrange pass; click-scroll fails in Chromium here on origin/main too and passes in WebKit. No request was made to any vendor and no live agent config was touched. go vet ./internal/..., go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui ./... pass (TestClaudeRunEndsWhenTheClientLeaves, TestClaudeAccountNamedAsItsLogin and TestClaudeIdentityServedWhileAsked failed once under the full run and pass alone). Turned on after magpie has moved the agent off the first, it goes back to it at once rather than once that has room again.
2026-10-02 15:48:16 +08:00
dsh: magpie's route keeps the protocol picked for it in dsh's Models page, OpenAI Responses or Anthropic Messages, where each round put Chat Completions back (01huadalang on Discord: 这些协议怎么改 responses,在 dsh 改了一会就会被 magpie 接管) — the route was written whole each round, api: openai-completions included (dshRouteConfig), so a route switched to OpenAI Responses in dsh's Models page went back to Chat Completions within the 30 s the loop takes. Each round now reads the route's api from the profile's patch list and keeps it when it is one of the three llm-pi-ai protocols the gateway serves (dshAPI: openai-completions at /v1/chat/completions, openai-responses at /v1/responses, anthropic-messages at /v1/messages); none, or one the gateway has no endpoint for (google-generative-ai), goes back to openai-completions. The base goes with the protocol (dshBaseURL): pi-ai hands it to the vendor's SDK as it is, and Anthropic's adds /v1/messages itself, so anthropic-messages is at the gateway's root and the two OpenAI ones at its /v1 (as omp and Pi have it); dshCheck asks for the same base, so a route on Messages isn't reported as pointed elsewhere. On Messages a Claude that thinks only adaptively gets compat.forceAdaptiveThinking, which llm-pi-ai offers there, since pi-ai asks every other model for a thinking budget it turns away. reasoningEfforts are as before on all three: llm-pi-ai (0.2.0-rc.2) makes them pi-ai's thinkingLevelMap, sent as reasoning_effort on Chat, reasoning.effort on Responses (off as the map's none, a model without none having off hidden) and output_config.effort for an adaptive Claude on Messages. TestDshRouteKeepsTheAPIPickedInDsh (a loop round and a sync on a route set to openai-responses, to anthropic-messages at the /v1 base, to google-generative-ai and to none; dsh's check clean, a second round writing nothing) fails without the change (openai-responses written back as openai-completions). TestDshResponsesReachChatVendor sends pi-ai's Responses request, with dsh's User-Agent, to the gateway for a model served on Chat Completions alone: it is asked in Chat with reasoning_effort high, the image, tool call and output carried over, and the reply comes back as Responses events. Read from the installed dsh's llm-pi-ai and pi-ai only; no dsh profile, live agent config or running magpie was touched. go vet ./internal/..., go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui -count=1 ./... under a temp HOME pass (TestFXInState failed once on its TempDir cleanup, passes alone).
2026-10-02 19:41:21 +08:00
plugins: a plugin's subscription does what a built-in's does, in seven places it didn't (the user: 要保证插件订阅 provider 跟内置的订阅 provider所有逻辑都要一模一样) — an installed plugin's provider id is kept for it signed in or not, as a built-in's is: a provider of the user's own isn't saved on it, renamed onto it or imported as it (freeID gives id-2). A plugin's account whose plan lacks the model is tried after the ones that list it, as a Codex Free account behind a Plus, by the list the plugin told for it. Cursor's plugin has fast mode where the built-in Cursor has it: a model with a -fast one is saved :fast in a group and sent service_tier "priority", one without is refused. Removing a plugin's provider hides it as a built-in's is, its accounts kept and shown again from Hidden or by signing in, rather than signing every account out ("stays signed in" said so already). magpie accounts lists, adds, switches and forgets a plugin's accounts by its provider's id or name. Grok's plugin beside the built-in Grok (grok-plugin) searches by itself as Grok does. A plugin's plan is named as its built-in's ("Cursor Pro", "SuperGrok"), and a plugin's resets are shown but have no Use button or Auto-use, which spent Codex's reset of the same user. TestPluginIDReservedSignedOut, TestPluginAccountLackingTheModelGoesLast, TestCursorPluginGroupFast, TestAccountsOfAPlugin, TestGrokPluginSearchesItself, plugin-plan and codex-auto-reset fail without the change; TestPluginSignIn and TestPluginAccounts now expect a removed plugin hidden. go vet, go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui ./... pass (TestClaudeSignedOut failed once under load, passes alone); the gateway, provider and plugin suites and TestAccountsOfAPlugin pass on Windows, the plugin tests on Linux.
2026-10-01 20:26:55 +08:00
codex: a group's effort for the turn keeps Codex's reasoning.context, and Codex's custom tools (code mode's exec, apply_patch) reach a model magpie translates for (#534, #520 follow-up, azir12345: since v0.1.637 a Plus account's gpt-6.1-sol fails with 400 "X-OpenAI-Internal-Codex-Responses-Lite requires reasoning.context to be all_turns", and Codex said it had only the collaboration and wait tools, no terminal) — ChatGPT's catalog marks gpt-6.1-sol (as gpt-6-sol, gpt-6-luna, gpt-5.6-*) use_responses_lite with tool_mode code_mode_only, and Codex asks for the bare slug with its native metadata: the X-OpenAI-Internal-Codex-Responses-Lite header (add_responses_lite_header, which magpie forwards with the other x-openai-* headers), reasoning {"effort","summary","context":"all_turns"} (build_reasoning), and its tools moved into an additional_tools item, every other tool reached through exec, a custom (freeform, lark grammar) tool in the "functions" namespace, beside wait and the collaboration namespace. magpie routes the bare id to the group auto-gpt-6-1-sol, and the effort the group picks for the turn went through withEffort, whose Responses branch rebuilt reasoning as {effort, summary} and dropped context, so ChatGPT refused the Lite request; it now sets only the effort (withBodyEffort), keeping every other field of reasoning as Codex sent it. A member served by translation (Chat, Anthropic, a Responses plugin) had parseResponses drop custom tools and ignore custom_tool_call / custom_tool_call_output items, so exec vanished and the model saw only wait and the collaboration tools; a custom tool (top-level or in a namespace) is now offered as a function taking one string "input", its description its own plus a note on input and its grammar, an earlier custom_tool_call goes to the model as a call with {"input":…} and its output as the call's result, and the model's call goes back to Codex as the custom_tool_call it runs (name and namespace as Codex knows them, input from the "input" argument, else the arguments as given), streamed without function_call_arguments events and in the non-streamed reply alike. This also gives translated models Codex's freeform apply_patch, which was dropped silently before. The Lite header is still forwarded as it was; nothing showed it needs dropping. Codex's base_instructions and identity line are untouched. No request was made to ChatGPT. TestWithEffort's Lite case fails without the decide.go change ("want \"context\":\"all_turns\""); TestCodexCustomToolsTranslated (a code-mode Lite turn through a fake Chat provider: exec offered with input and its grammar, the earlier call and output shown, the model's exec call back as a custom_tool_call with its input and no arguments) and TestCodexCustomToolNamespaced (namespaced and non-JSON calls, renderResponses) cover the tools; TestAdditionalToolsTranslated, TestNamespacedToolsOffered and TestGrokPluginGetsNamespacedTools now expect the custom tools offered. go vet ./internal/..., go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui -count=1 ./... under a temp HOME pass (TestDevinSignedInOnlyInMagpie failed once on its wait, passes alone; untouched).
2026-10-02 15:59:14 +08:00
plugins: a plugin subscription goes through every path a built-in one does (the user: 要保证插件订阅 provider 跟内置的订阅 provider所有逻辑都要一模一样) — a SOCKS5 proxy on a plugin provider or account now carries its requests: Bun speaks http(s) proxies only, so magpie stands a loopback http proxy in front of the SOCKS one, and a proxy that refuses says so as Go's transport does. A plugin's model list and its loader are asked through the account's own proxy, as a built-in's list is fetched through it. The Grok plugin draws and makes videos at Grok's Imagine API through the plugin, and keeps Codex's namespaced tools, as the built-in does; an official plugin beside its built-in (kiro-plugin, zcode-plugin) counts tokens by estimate as the built-in and the moved plugin do. FetchNew no longer re-lists plugin providers every ten minutes, their list being told by the plugin host; a plugin account's usage is cached under one name whichever way it is asked for; and a fetch hook that throws on its sign-in (a refused refresh) marks the account lapsed, as a built-in's refused refresh does. TestPluginSOCKSProxy, TestPluginModelListThroughAccountProxy, TestGrokPluginDraws, TestPluginBesideBuiltinCountsByEstimate, TestFetchNewLeavesListedPlugins, TestPluginUsageReadOnceWhicheverName and TestPluginFetchSignInExpiredLapses fail without the change. go vet, go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui ./... pass (the known load flakes pass alone); internal/plugin, netproxy and the plugin/proxy tests of provider and gateway pass on Linux and Windows.
2026-10-01 19:07:12 +08:00
plugins: a plugin's subscription does what a built-in's does, in seven places it didn't (the user: 要保证插件订阅 provider 跟内置的订阅 provider所有逻辑都要一模一样) — an installed plugin's provider id is kept for it signed in or not, as a built-in's is: a provider of the user's own isn't saved on it, renamed onto it or imported as it (freeID gives id-2). A plugin's account whose plan lacks the model is tried after the ones that list it, as a Codex Free account behind a Plus, by the list the plugin told for it. Cursor's plugin has fast mode where the built-in Cursor has it: a model with a -fast one is saved :fast in a group and sent service_tier "priority", one without is refused. Removing a plugin's provider hides it as a built-in's is, its accounts kept and shown again from Hidden or by signing in, rather than signing every account out ("stays signed in" said so already). magpie accounts lists, adds, switches and forgets a plugin's accounts by its provider's id or name. Grok's plugin beside the built-in Grok (grok-plugin) searches by itself as Grok does. A plugin's plan is named as its built-in's ("Cursor Pro", "SuperGrok"), and a plugin's resets are shown but have no Use button or Auto-use, which spent Codex's reset of the same user. TestPluginIDReservedSignedOut, TestPluginAccountLackingTheModelGoesLast, TestCursorPluginGroupFast, TestAccountsOfAPlugin, TestGrokPluginSearchesItself, plugin-plan and codex-auto-reset fail without the change; TestPluginSignIn and TestPluginAccounts now expect a removed plugin hidden. go vet, go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui ./... pass (TestClaudeSignedOut failed once under load, passes alone); the gateway, provider and plugin suites and TestAccountsOfAPlugin pass on Windows, the plugin tests on Linux.
2026-10-01 20:26:55 +08:00
plugins: a plugin subscription goes through every path a built-in one does (the user: 要保证插件订阅 provider 跟内置的订阅 provider所有逻辑都要一模一样) — a SOCKS5 proxy on a plugin provider or account now carries its requests: Bun speaks http(s) proxies only, so magpie stands a loopback http proxy in front of the SOCKS one, and a proxy that refuses says so as Go's transport does. A plugin's model list and its loader are asked through the account's own proxy, as a built-in's list is fetched through it. The Grok plugin draws and makes videos at Grok's Imagine API through the plugin, and keeps Codex's namespaced tools, as the built-in does; an official plugin beside its built-in (kiro-plugin, zcode-plugin) counts tokens by estimate as the built-in and the moved plugin do. FetchNew no longer re-lists plugin providers every ten minutes, their list being told by the plugin host; a plugin account's usage is cached under one name whichever way it is asked for; and a fetch hook that throws on its sign-in (a refused refresh) marks the account lapsed, as a built-in's refused refresh does. TestPluginSOCKSProxy, TestPluginModelListThroughAccountProxy, TestGrokPluginDraws, TestPluginBesideBuiltinCountsByEstimate, TestFetchNewLeavesListedPlugins, TestPluginUsageReadOnceWhicheverName and TestPluginFetchSignInExpiredLapses fail without the change. go vet, go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui ./... pass (the known load flakes pass alone); internal/plugin, netproxy and the plugin/proxy tests of provider and gateway pass on Linux and Windows.
2026-10-01 19:07:12 +08:00
zed: an OpenAI model's request reaches Zed's cloud in the Responses types it reads, so Codex on a Zed GPT model is answered (nico on Discord: "failed to parse OpenAI Responses API request: unknown variant developer, expected one of user, assistant, system, tool", and with it no model switch mid-thread and no auto-review) — Zed's cloud parses the request into its own types (zed-industries/zed crates/open_ai/src/responses.rs: Role user/assistant/system/tool, items message/function_call/function_call_output/custom_tool_call(_output)/reasoning/compaction/compaction_trigger, tools function/custom, include reasoning.encrypted_content only) and refuses the whole request over anything else. The Zed plugin (@magpie-community/opencode-zed-auth) gets Codex's Responses request relayed as Codex sent it, developer messages and all — its permissions message on every request, the one a model switch adds, the auto-review's — so every Codex turn on a Zed OpenAI model failed. The plugin's account now fits the body as grokBody does Grok's (ZedBody): a developer message goes as a system one where it stands (Zed's own client sends system messages too), the shorthand {role, content} gets its type and a string content becomes a list, reasoning's null content goes, an agent_message is the user's turn, an item Zed has no type for (a web_search_call an OpenAI model ran) is left out, namespaced tools go flat and their calls come back under the namespace (as Grok's), other tool types go, include keeps reasoning.encrypted_content alone, a function tool_choice is required, and a reasoning summary Zed doesn't know goes. The built-in Zed account already folded developer messages in; its OpenAI request goes through the same fitting, so a web search offered with no search API no longer leaves a web_search tool and web_search_call.action.sources in it. Codex's prompts and instructions are unchanged. TestZedPluginGetsNoDeveloperRole (a Codex-shaped request through the fake plugin to a fake upstream that parses as Zed's cloud: fails with the exact 400 without the change) and TestZedBuiltInResponsesParse fail without it. No request was made to Zed. go vet ./internal/..., go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui ./... pass.
2026-10-02 13:23:41 +08:00
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
usage: quota cards draw what was left over time, against an even burn, and the tray card its cycle's line (#651, songlairui) — each reading magpie makes anyway of a subscription's windows (and of a plan bought with a key) is kept as a point in quota-history.json beside the config: provider, account, window, when read, percent left, the window's start and reset — no token, no prompt. A failed reading, one kept from before (As of), an unlimited window and a model family's aren't kept; Antigravity's pools are kept by pool. Within a cycle points are 5 minutes apart at least and a run of one figure is its two ends; a cycle ends where the window starts again (its reset passed, or moved further than the time between two readings, so a rolling window's isn't one). 45 days and 6000 points a window are kept. The Usage page's card has, under its meters, a line a window (several tiers of one account on one chart), broken at each reset, a dashed even burn from each window's start to its reset and an upright now line, with 2 days / Cycle (the account's longest current window, start to reset) in the app's segmented control, remembered, turning every card without moving the page. The tray's card has a thin line of its first window's current cycle. GET /v1/magpie/quotas/history (?days=, ?provider=, ?user=; local or LAN with the key, as /v1/magpie/quotas) and /api/usage/quotas/history serve it; /v1/magpie/quotas and magpie quota stay snapshots. Computers sharing usage (WebDAV/S3, Settings › Sync › usage) put their history beside their days, sealed, as <id>.magpie-quotas, and merge the others': the union by account, window and time, two points within 30 s the newer, nothing twice however often they sync; an older magpie never reads the file. TestQuotaHistoryRecords, TestQuotaHistoryCycles, TestMergeQuotaHistory, TestQuotasHistory and TestQuotaHistorySharedBetweenComputers cover recording, cycles, pruning, merge, the API and two computers on one WebDAV folder; quota-curve.test.cjs (WebKit, en/zh, light and dark) the curves, the toggle, no curve without readings, and the tray's line (Chromium doesn't start in this sandbox). go vet, builds (darwin, GOOS=linux, GOOS=windows) and go test -tags nogui -count=1 ./... under a temp HOME pass. Not tried with real accounts; pay-as-you-go balances aren't drawn.
2026-10-03 15:19:19 +08:00
quota: each subscription account, plan and key says when it last answered, and the latest is marked last (#570: quota --json / GET /v1/magpie/quotas 增加每个订阅、plan、key 最近一次应答请求的时间) — under routing Smart nobody is first for good, so a menu-bar widget had nothing to tell the one in use by: a Quota's asOf is when its reading was taken, accounts --json covers signed-in Codex and Claude accounts only, and /v1/magpie/route wants a session. Each Quota in magpie quota --json and GET /v1/magpie/quotas now has lastServedAt (RFC3339, left out when it hasn't answered) and last: true on the latest (each of a key's plan and balance, which are one key). The gateway notes each answer where servedCandidate counts it (provider.NoteServed): an account by its agent and user, a key by its provider and key id, matched to a card by its user, or by the key's name or mask, or, for a card with no user, by any of its provider's. It is kept in served.json beside providers.json, entries older than 30 days dropped, since magpie quota runs in a process of its own and computes the report itself (provider.QuotaReport) rather than asking the gateway: both read the file, so they say the same, and a restart keeps it. TestQuotasLastServedAt (gateway: a request answered by one key, its balance card has lastServedAt and last, the other key's neither) and TestQuotaJSONLastServedAt (the CLI's --json, fake CLIs and keychain on PATH) fail without the change; TestQuotasLastServed covers the matching (account by user, key by name and by mask, a card with no user, the tie, read back from the file alone). README tells the fields. go vet ./internal/..., go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui -count=1 ./... under a temp HOME pass.
2026-10-02 22:16:17 +08:00
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
gateway: a remote magpie's routing group answering with the member it routed to isn't a swap (莫 on Discord: 发送模型 group/auto-deepseek-v4-1-flash, 实际模型 deepseek/deepseek-v4.1-flash shown amber) — a request for one of another magpie's groups ("group/<id>", the name only a magpie answers to) comes back with the model its member's vendor named, and the Usage page's Requests and the Routing page marked that as the vendor serving another model. Now it is routed, not swapped (usage.GroupRouted): the try, the route and the ledger row say so, the served member shows plain, muted on the Usage page, with a title (and a line in the request's story) saying the remote magpie's group routed the request to it, in English and Chinese. Rows recorded before are judged again as the ledger lists them, and routes kept in the Routing page's history as they are read. A real swap through a remote magpie, and a swap within a group of this magpie's own, are still marked; a swap behind the remote's group shows on that magpie's own Routing page. Checked end to end with two sandbox magpies over a fake vendor: the local ledger and routing history mark group/… false (swapped before the change) and fake/sol served as luna true. TestRemoteMagpieGroupNotSwapped, TestRemoteMagpieGroupLedgerRow, TestRemoteMagpieGroupHistory and remote-group-served.test.cjs (Chromium, WebKit, en/zh) fail without the change. go vet ./internal/..., go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui ./... pass (load flakes pass alone); routing-served, usage-ledger, panel-routing, remote-magpie, click-scroll, old-webkit pass.
2026-10-01 13:46:14 +08:00
providers: a Remote magpie provider uses another computer's shared magpie, each request translated at most once (Jorben on Discord: 远程 magpie / magpie 接力) — a custom provider pointed at another magpie has one API per address and couldn't know which API each model's own provider serves, so a request was translated on both computers (Chat → Messages here, then again there). magpie's /v1/models now tells each model's native_endpoints (the APIs its provider serves it on; none for a routing group) and its image input, and reads them, with max_output_tokens and supported_reasoning_levels, from a list; the new remote-magpie preset takes the other magpie's address however typed (host:port, /v1, /v1/messages) for all three APIs and lists all its models, its user's choice already. Add sheet tile with the address field and a hint to Settings → Share on local network; CLI: magpie provider add remote-magpie <key> url=http://<ip>:3425. Checked with two sandbox magpies over the LAN address with the key check on and a Messages-only fake upstream: Chat, Messages (streamed), count_tokens, Responses, a routing group and Gemini all reached it as /v1/messages, translated once. TestRemoteMagpieNativeAPI, TestFetchMagpieFields and TestRemoteMagpie fail without the change; remote-magpie.test.cjs (Chromium, WebKit, en/zh) added. go vet, go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui ./... pass but for TestCursorPluginContext, which fails on main (1ce05f0) as well; click-scroll, old-webkit pass.
2026-10-01 01:35:18 +08:00
usage: a Codex call through the gateway that wrote to its upstream's cache is no longer listed a second time as a local session call (laowangba233 on #589: 用量中的统计是不是重复了 … 为啥会一个是提供商的一个是本地会话的) — the Usage page drops a Codex rollout's call when it matches a gateway record by session, end time within 2s and equal tokens. Codex reads input_tokens_details.cache_write_tokens from a Responses reply and keeps it as cache_write_input_tokens; the gateway's rUsage read only cached_tokens, so for a call whose upstream told a write the record had CacheWrite 0 and the rollout didn't, the tokens never matched, and the call stayed listed under Local session beside its provider's row. Calls without a write matched, so only some calls were counted twice (Maru Code 92 calls plus 22 "本地会话" in the report). Also, sessions' spent took only the cache read out of Codex's input_tokens, which holds the write too, so such a call's write was counted twice in its own input. Now: the gateway reads cache_write_tokens (input = input_tokens - cached - written) and tells it to Codex when it translates a reply; spent takes the write out of the input as well; and the matcher compares a call's input plus cache writes, its output and its cache reads, so a record and a rollout that split the write differently — an older Codex without the field, or records already in usage.jsonl from before this change — still match, and the duplicates already on a user's page go too. The session caches' versions go up (sessions.json 11, calls-v4) so kept parses are read again, and older calls-v* folders are removed. Under a provider filter the provider ranking still lists the other providers, as it is a picker; the duplicates were what made it read as double counting. TestCodexCacheWriteMatchesGatewayCall (a Codex rollout and gateway records for two calls, one with a 9000-token cache write; with the rollout naming the write and the record not, both naming it, and an older Codex without it: two rows, no Local session row, the provider ranking maru-code alone, unfiltered and filtered to the provider, by QueryPage and LedgerOf; fails without the change with a third row from the log), TestCodexCacheWriteNotInInput and TestResponsesUsageCacheWrite (parse and round trip through what Codex is told) are new and fail without the change; TestCallsCodex's call with a write now expects input 200 (2500 - 2000 read - 300 written). go vet ./internal/..., go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui -count=1 ./internal/... under a temp HOME pass (TestFXInState's known TempDir-cleanup flake aside, which passes alone).
2026-10-03 00:47:33 +08:00
gateway: a custom tool's call magpie hands Codex has a ctc_ id, and a magpie model or group on OpenAI's API gets call ids OpenAI takes (Lullaby on Discord: Invalid 'input[155].id': 'fc_18da5b9a10256a3e00000011'. Expected an ID that begins with 'tsc'. 这个问题貌似还是没有修复, then "Expected an ID that begins with 'ctc'", auto-compaction failing with it) — two causes. A vendor's apply_patch, translated to Codex's custom_tool_call, was given an fc_ id as a function call is (itemPrefix), and OpenAI turns away a custom_tool_call whose id isn't a ctc_ one. And the tsc_ rename (1a9c4ca6) ran only for Codex's own models: a magpie model or routing group relayed to OpenAI's API on a key (passthrough) sent the session's fc_ ids as they were. The id kinds are now one table, openaiItemPrefix (tool_search_call tsc_, custom_tool_call ctc_, the two OpenAI's errors name): calls are minted with them in the stream and the rendered reply, and callItemIDs (was searchCallIDs) renames fc_X to tsc_X / ctc_X before Codex's own /responses and /responses/compact and before a passthrough to an openai.com host, call_ids and the rest of the body byte for byte. A ChatGPT account's request keeps no ids (provider.codexInput), so it is left alone. TestCustomToolCallHasCtcID, TestMagpieModelOpenAICallIDs (a fake OpenAI behind the provider's proxy answering the same 400s, direct and through a group) and TestCodexOwnModelCustomCallIDs (turn and compaction) fail without the change; TestCustomCallIDsUntouched keeps a clean request byte for byte. go vet ./internal/..., go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui -count=1 ./... under a temp HOME pass.
2026-10-03 10:28:46 +08:00
gateway: a rate limit on the last provider left is waited out even when it doesn't say for how long (#503, ynky007) — a 429 was tried again only when it came with a Retry-After (or a ratelimit-reset header) of 8 s at most, so a relay's 429 that says nothing of when ("Rate limit exceeded, please try again later", "当前分组上游负载已饱和,请稍后再试") went straight to the agent. Where another member, key or account is left a 429 already moves on to it, as before; on the last one left (or the last that may answer, the others out of their allowance) a 429 failure() reads as a rate limit is now asked again up to 3 times (rateRetries; busy 5xx/529 keep lastRetries = 2), after 1, 2 and 4 s or what Retry-After says (each 8 s at most, under half a minute in all), while nothing has been sent to the agent and only until it goes away. A 429 that says the plan is used up (usage limit, resets in hours: failQuota), that the money is gone (creditWords), or a Retry-After past 8 s (ChatGPT's resets_at goes on as one) is passed on at once, as before. passing() now takes the error body and the try count, and the last one's reply is held for as many tries as a rate limit gets. TestRateLimitIsWaitedOut (no Retry-After, a relay's own words, three in a row, then 200; four in a row → 429 after 4 tries; usage limit, 余额不足, Retry-After: 3600 → 429 after one try, at once) fails without the change (429 after 1 try in the first three, 3 tries in the fourth); TestLastOneLeftIsTriedAgain still passes. go vet ./internal/..., go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui ./internal/gateway/ ./internal/provider/ pass.
2026-10-02 13:00:22 +08:00
routing: a group in a group is routed by its own routing whatever the outer group's, and an account with five hours and no week is ranked by its five hours (#576: 只有五小时额度的账号在智能/重置前用完下是否合理;嵌套路由组时上层路由是否会干预下层) — a group routed other than In order (Smart, In turn, Least used, Weekly pace, Manual) put every account and key of every group in it into one pool and weighed them together, so A (Smart) holding A-1 and A-2 (each In order) ranked all five of their accounts and keys by Smart and A-1's and A-2's own In order did nothing; only an outer In order let each inner group route itself. planLevel now plans each group in the group by its own routing, whatever the outer's, and weighs it as one beside the outer group's own models' keys, by the first of its own order that isn't resting; where that lands, the inner group is tried whole in its own order, the trace in the same order. As for the five hours: Smart already ranked such an account (Claude Enterprise) by its five hours, the biggest window it has, so ahead of every week renewing later, but a five hours not started (no reset told) counted as a reset not known and went after every week; Allowance.Renewal now has a window not started that says its span renew that long from now, which Smart sorts by (the trace's renews unchanged). Weekly pace took an account with no week as a fresh week with the share used (100/168 per hour), so a week with 80% left and five days to go went ahead of an Enterprise account whose five hours were being lost; it now goes by what its five hours have left per hour until they renew, the whole span when not started, except where the reading may leave a week out — a Claude account only heard of from Claude Code's rate_limit_event, never read whole by /usage (QuotaWindow.partial, claudeUsageEntry.whole) — which keeps the fresh week. The group editor's Smart and Weekly pace hints and the trace's say how an account with five hours and no week is ranked, a hint under Routing says a group among the models keeps its own routing, and the docs (en, zh) say both. TestGroupInGroupKeepsItsOwnRouting (Outer rotating over two In order groups: b c d a without the change, c d a b with it), TestSmartFiveHoursAlone (a five hours not started went after a week in two days), TestFiveHoursAlone, TestClaudeHeardOnlyIsPartial; TestWeeklyPace and TestAllowancePace now pin the five hours alone. settings-groups, group-member-fast, group-found-off, routing-idle-frame pass in WebKit. go vet ./internal/..., go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui -count=1 ./... under a temp HOME pass (TestFXInState, TestProvidersAnswerWhileListsComeIn, TestUserPath, TestClaudeSignedOut flaked; the last passes 3/3 alone).
2026-10-02 23:05:15 +08:00
test: every package's tests pass on Windows, and none writes outside its sandbox (the user: 把 Windows 上那几个失败的测试修掉) — on a Windows box the suite failed in most packages, and the root package hung for 25 minutes. Tests set HOME, which os.UserHomeDir ignores on Windows (it reads USERPROFILE), so they read and wrote the runner's own home; each now sets USERPROFILE beside HOME, and Devin's, Goose's, Crush's and Claude Desktop's tests set APPDATA/LOCALAPPDATA, where those keep their files there. said() read the command's output only once it returned, and a pipe holds a few KB on Windows, so magpie model --help blocked forever; it reads while the command writes. Unix permission bits (the gateway key store's 0600 too), exec bits and symlinks Windows lacks are checked only off Windows; TOML strings hold Windows paths as literal strings, JSON as marshalled ones; a URL's path is compared as one. The provider and usage packages run in a home of their own (TestMain, as the gateway's already does), since a CLI's identity kept by a refresh still going when its test ended landed in the runner's ~/.config/magpie — and on a Mac in the user's own. Two product fixes came out of it: imagemcp took a file:///C:/… root or image for the path /C:/…, which doesn't exist on Windows, and now makes a Windows path of it (a host for a UNC share); and session discovery could keep a new Cowork session hidden on Linux, where a folder's mtime is a kernel tick and its size doesn't change, so an entry added within the tick of the last listing left both as they were — a listing made within 2s of its folder's last change is read again, as git does for racy files. TestFilePath and TestRacyDirectoryListedAgain fail without the change. On Windows 11 every package passes (32 of 32) and a run of each package in a fresh home leaves nothing in home, APPDATA or LOCALAPPDATA; on Linux (WSL) and macOS go test -tags nogui ./... passes. go vet, go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build pass.
2026-10-01 23:15:07 +08:00
search: which provider searches the web for a model that can't is picked in Settings → Web search, Automatic as before (01huadalang on Discord: 能不能在供应商联网搜索里加一个入口,能用自己能联网搜索的地方来替代不能联网搜索的供应商) — magpie picked it by itself (a Claude account, then a Codex account, then a provider that searches by itself on Anthropic's API, the Responses API, Chat, each by its small model) and Settings only said who. A "Searches for other models" row now comes first in the section: its picker offers Automatic (magpie's pick, named), and each provider able to search — on, and searching by itself or a Claude/Codex account, in the order magpie would pick them — by its small model or by any model of it. The pick is saved as settings' searcher ("<provider>" or "<provider>/<model>"), sent with every other setting saved (prefsKeep), and refused when the provider can't search. searcher() takes it while it can (chosenSearcher); one gone, turned off or no longer able to search gives way to magpie's pick, and the row says which and why, showing Automatic; a model it no longer lists gives way to its small model. A relay said to search is still never asked for other models (#359: it would spend the relay's quota, and one that serves only Claude Code refuses magpie's own request), so it isn't offered, and the row names such relays and says why. TestSearcherChosen (the automatic order, a provider, a model of it, a model gone, one that can't search, a relay said to search, one gone, one turned off and back on, Automatic again) and searcher-pick.test.cjs (WebKit and Chromium, en and zh: Automatic shown with magpie's pick, the relay not offered and named, a model and a provider picked and saved, another setting saved keeping the pick, one turned off said and Automatic shown, Automatic sent back, no click moving the page) are new; it passes in WebKit (this machine's Playwright Chromium fails to launch, icudtl.dat missing, for every test). search-api, settings-sections, provider-search, click-scroll pass in WebKit. go vet ./internal/..., go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui -count=1 ./... under a temp HOME pass (TestDevinSignedInOnlyInMagpie failed once, passes 3/3 alone).
2026-10-02 23:33:31 +08:00
gateway: a custom tool's call magpie hands Codex has a ctc_ id, and a magpie model or group on OpenAI's API gets call ids OpenAI takes (Lullaby on Discord: Invalid 'input[155].id': 'fc_18da5b9a10256a3e00000011'. Expected an ID that begins with 'tsc'. 这个问题貌似还是没有修复, then "Expected an ID that begins with 'ctc'", auto-compaction failing with it) — two causes. A vendor's apply_patch, translated to Codex's custom_tool_call, was given an fc_ id as a function call is (itemPrefix), and OpenAI turns away a custom_tool_call whose id isn't a ctc_ one. And the tsc_ rename (1a9c4ca6) ran only for Codex's own models: a magpie model or routing group relayed to OpenAI's API on a key (passthrough) sent the session's fc_ ids as they were. The id kinds are now one table, openaiItemPrefix (tool_search_call tsc_, custom_tool_call ctc_, the two OpenAI's errors name): calls are minted with them in the stream and the rendered reply, and callItemIDs (was searchCallIDs) renames fc_X to tsc_X / ctc_X before Codex's own /responses and /responses/compact and before a passthrough to an openai.com host, call_ids and the rest of the body byte for byte. A ChatGPT account's request keeps no ids (provider.codexInput), so it is left alone. TestCustomToolCallHasCtcID, TestMagpieModelOpenAICallIDs (a fake OpenAI behind the provider's proxy answering the same 400s, direct and through a group) and TestCodexOwnModelCustomCallIDs (turn and compaction) fail without the change; TestCustomCallIDsUntouched keeps a clean request byte for byte. go vet ./internal/..., go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui -count=1 ./... under a temp HOME pass.
2026-10-03 10:28:46 +08:00
search: which provider searches the web for a model that can't is picked in Settings → Web search, Automatic as before (01huadalang on Discord: 能不能在供应商联网搜索里加一个入口,能用自己能联网搜索的地方来替代不能联网搜索的供应商) — magpie picked it by itself (a Claude account, then a Codex account, then a provider that searches by itself on Anthropic's API, the Responses API, Chat, each by its small model) and Settings only said who. A "Searches for other models" row now comes first in the section: its picker offers Automatic (magpie's pick, named), and each provider able to search — on, and searching by itself or a Claude/Codex account, in the order magpie would pick them — by its small model or by any model of it. The pick is saved as settings' searcher ("<provider>" or "<provider>/<model>"), sent with every other setting saved (prefsKeep), and refused when the provider can't search. searcher() takes it while it can (chosenSearcher); one gone, turned off or no longer able to search gives way to magpie's pick, and the row says which and why, showing Automatic; a model it no longer lists gives way to its small model. A relay said to search is still never asked for other models (#359: it would spend the relay's quota, and one that serves only Claude Code refuses magpie's own request), so it isn't offered, and the row names such relays and says why. TestSearcherChosen (the automatic order, a provider, a model of it, a model gone, one that can't search, a relay said to search, one gone, one turned off and back on, Automatic again) and searcher-pick.test.cjs (WebKit and Chromium, en and zh: Automatic shown with magpie's pick, the relay not offered and named, a model and a provider picked and saved, another setting saved keeping the pick, one turned off said and Automatic shown, Automatic sent back, no click moving the page) are new; it passes in WebKit (this machine's Playwright Chromium fails to launch, icudtl.dat missing, for every test). search-api, settings-sections, provider-search, click-scroll pass in WebKit. go vet ./internal/..., go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui -count=1 ./... under a temp HOME pass (TestDevinSignedInOnlyInMagpie failed once, passes 3/3 alone).
2026-10-02 23:33:31 +08:00
search: a searcher is asked on the API it searches on, and one that finds nothing gives way to the next searcher and the search APIs (#669: with Automatic resolving to deepseek/deepseek-flash and Tavily set up, every client's web_search got web_search_tool_result.content [], the routing log saying 200) — magpie asks its searcher on Anthropic's API, and DeepSeek's Anthropic API wasn't counted among those that search, so the request was translated onto DeepSeek's first API, Chat; translate chose the API a provider searches on (DeepSeek's Responses) only for a client's request, never for magpie's own search, so DeepSeek got no search tool at all, and deepseek-flash said it couldn't search or wrote its own DSML tool-call markup as text. That text went to the model as what was found, with no page, and was taken as a success, so Tavily was never asked. Now translate sends a searcher to the API it searches on as it does a client; api.deepseek.com searches on Anthropic too (tried: web_search_20250305 there answers web_search_tool_result blocks of real pages with DeepSeek's encrypted_content, and takes magpie's own blocks back in a later turn); a searcher's results are read one by one, title, address and page_age (now carried to the client's block), never its encrypted_content, which stays "" in what magpie sends; a reply with no page and no address in its text is "found nothing", and the search goes to the next searcher magpie would pick (up to 3, within the 2 min), then to the search APIs. TestSearcherAskedWhereItSearches (DeepSeek searching on Responses only: old code asked Chat and gave the DSML text and []), TestDeepSeekNativeSearchResults (DeepSeek's native reply: pages, their ages, no encrypted_content passed on) and TestSearcherFindingNothingGivesWay (a 200 with nothing found goes to Tavily, or to the next searcher first) fail on the old code; TestDeepSeekSearchesOnResponses now expects the Anthropic API to search. With the real DeepSeek key in a sandbox gateway (temp HOME, copy of providers.json, searcher deepseek/deepseek-flash) the issue's curl on aihubmix/glm-5.3-flash got 10 web_search_result pages (weather.com.cn, moji, …) and an answer from them; deepseek/deepseek-flash asked directly with that reply in its history searched natively and answered. No Tavily key here, so the fallback to it was tried only against the fake. go vet ./internal/..., builds (darwin -tags nogui, GOOS=linux -tags nogui, GOOS=windows) and go test -tags nogui -count=1 ./... under a temp HOME pass but TestClaudeRunEndsWhenTheClientLeaves, a timing test that passes alone 3/3.
2026-10-03 19:38:13 +08:00
search: a searcher is asked on the API it searches on, and one that finds nothing gives way to the next searcher and the search APIs (#669: with Automatic resolving to deepseek/deepseek-flash and Tavily set up, every client's web_search got web_search_tool_result.content [], the routing log saying 200) — magpie asks its searcher on Anthropic's API, and DeepSeek's Anthropic API wasn't counted among those that search, so the request was translated onto DeepSeek's first API, Chat; translate chose the API a provider searches on (DeepSeek's Responses) only for a client's request, never for magpie's own search, so DeepSeek got no search tool at all, and deepseek-flash said it couldn't search or wrote its own DSML tool-call markup as text. That text went to the model as what was found, with no page, and was taken as a success, so Tavily was never asked. Now translate sends a searcher to the API it searches on as it does a client; api.deepseek.com searches on Anthropic too (tried: web_search_20250305 there answers web_search_tool_result blocks of real pages with DeepSeek's encrypted_content, and takes magpie's own blocks back in a later turn); a searcher's results are read one by one, title, address and page_age (now carried to the client's block), never its encrypted_content, which stays "" in what magpie sends; a reply with no page and no address in its text is "found nothing", and the search goes to the next searcher magpie would pick (up to 3, within the 2 min), then to the search APIs. TestSearcherAskedWhereItSearches (DeepSeek searching on Responses only: old code asked Chat and gave the DSML text and []), TestDeepSeekNativeSearchResults (DeepSeek's native reply: pages, their ages, no encrypted_content passed on) and TestSearcherFindingNothingGivesWay (a 200 with nothing found goes to Tavily, or to the next searcher first) fail on the old code; TestDeepSeekSearchesOnResponses now expects the Anthropic API to search. With the real DeepSeek key in a sandbox gateway (temp HOME, copy of providers.json, searcher deepseek/deepseek-flash) the issue's curl on aihubmix/glm-5.3-flash got 10 web_search_result pages (weather.com.cn, moji, …) and an answer from them; deepseek/deepseek-flash asked directly with that reply in its history searched natively and answered. No Tavily key here, so the fallback to it was tried only against the fake. go vet ./internal/..., builds (darwin -tags nogui, GOOS=linux -tags nogui, GOOS=windows) and go test -tags nogui -count=1 ./... under a temp HOME pass but TestClaudeRunEndsWhenTheClientLeaves, a timing test that passes alone 3/3.
2026-10-03 19:38:13 +08:00
search: a searcher is asked on the API it searches on, and one that finds nothing gives way to the next searcher and the search APIs (#669: with Automatic resolving to deepseek/deepseek-flash and Tavily set up, every client's web_search got web_search_tool_result.content [], the routing log saying 200) — magpie asks its searcher on Anthropic's API, and DeepSeek's Anthropic API wasn't counted among those that search, so the request was translated onto DeepSeek's first API, Chat; translate chose the API a provider searches on (DeepSeek's Responses) only for a client's request, never for magpie's own search, so DeepSeek got no search tool at all, and deepseek-flash said it couldn't search or wrote its own DSML tool-call markup as text. That text went to the model as what was found, with no page, and was taken as a success, so Tavily was never asked. Now translate sends a searcher to the API it searches on as it does a client; api.deepseek.com searches on Anthropic too (tried: web_search_20250305 there answers web_search_tool_result blocks of real pages with DeepSeek's encrypted_content, and takes magpie's own blocks back in a later turn); a searcher's results are read one by one, title, address and page_age (now carried to the client's block), never its encrypted_content, which stays "" in what magpie sends; a reply with no page and no address in its text is "found nothing", and the search goes to the next searcher magpie would pick (up to 3, within the 2 min), then to the search APIs. TestSearcherAskedWhereItSearches (DeepSeek searching on Responses only: old code asked Chat and gave the DSML text and []), TestDeepSeekNativeSearchResults (DeepSeek's native reply: pages, their ages, no encrypted_content passed on) and TestSearcherFindingNothingGivesWay (a 200 with nothing found goes to Tavily, or to the next searcher first) fail on the old code; TestDeepSeekSearchesOnResponses now expects the Anthropic API to search. With the real DeepSeek key in a sandbox gateway (temp HOME, copy of providers.json, searcher deepseek/deepseek-flash) the issue's curl on aihubmix/glm-5.3-flash got 10 web_search_result pages (weather.com.cn, moji, …) and an answer from them; deepseek/deepseek-flash asked directly with that reply in its history searched natively and answered. No Tavily key here, so the fallback to it was tried only against the fake. go vet ./internal/..., builds (darwin -tags nogui, GOOS=linux -tags nogui, GOOS=windows) and go test -tags nogui -count=1 ./... under a temp HOME pass but TestClaudeRunEndsWhenTheClientLeaves, a timing test that passes alone 3/3.
2026-10-03 19:38:13 +08:00
gateway: a remote magpie's routing group answering with the member it routed to isn't a swap (莫 on Discord: 发送模型 group/auto-deepseek-v4-1-flash, 实际模型 deepseek/deepseek-v4.1-flash shown amber) — a request for one of another magpie's groups ("group/<id>", the name only a magpie answers to) comes back with the model its member's vendor named, and the Usage page's Requests and the Routing page marked that as the vendor serving another model. Now it is routed, not swapped (usage.GroupRouted): the try, the route and the ledger row say so, the served member shows plain, muted on the Usage page, with a title (and a line in the request's story) saying the remote magpie's group routed the request to it, in English and Chinese. Rows recorded before are judged again as the ledger lists them, and routes kept in the Routing page's history as they are read. A real swap through a remote magpie, and a swap within a group of this magpie's own, are still marked; a swap behind the remote's group shows on that magpie's own Routing page. Checked end to end with two sandbox magpies over a fake vendor: the local ledger and routing history mark group/… false (swapped before the change) and fake/sol served as luna true. TestRemoteMagpieGroupNotSwapped, TestRemoteMagpieGroupLedgerRow, TestRemoteMagpieGroupHistory and remote-group-served.test.cjs (Chromium, WebKit, en/zh) fail without the change. go vet ./internal/..., go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui ./... pass (load flakes pass alone); routing-served, usage-ledger, panel-routing, remote-magpie, click-scroll, old-webkit pass.
2026-10-01 13:46:14 +08:00
gateway: a translated or relayed stream keeps its client alive while its provider is (#436, congee949: Chat → Responses dropped the upstream's ": keepalive" and Codex hit "idle timeout waiting for SSE" on a live connection) — a reply magpie translates was read event by event and written only when an event had something for the client, so a provider's SSE keepalives, and events with nothing to translate, reached no one, and a reply that thought quietly for five minutes ended in Codex's idle timeout and a retry. Now, whenever the provider is heard from and the client has heard nothing for a second, magpie sends the client a keepalive in its own protocol: response.in_progress again on Responses (Codex's timeout counts events, never a comment — a sandboxed codex 0.159.2 on a fake provider sending only comments timed out, one sending response.in_progress didn't), ping on Anthropic (as Anthropic does; Claude Code's watchdog counts it), an SSE comment on Chat Completions, nothing on Gemini (Google's SDK can't read past one). A provider that sends nothing at all is not covered for, so a dead connection still ends at the client's timeout. A reply relayed from the Claude subscription's bridge, Kiro, Cursor, Devin, search, Qoder, Zed or Command Code is kept alive every 15s while it goes quiet, for up to 5 minutes since its last event. Native passthrough is unchanged, and prompts and headers are unchanged. The routing trace no longer drops a request still going for the 60 finished after it: the oldest finished ones go first, unless more than 120 are going at once. TestTranslatedStreamKeepsClientAlive (the issue's repro: 20ms keepalives, 150ms idle timeout; Chat → Responses, Chat → Anthropic, Responses → Chat), TestRelayKeepsClientAlive and TestTraceKeepsInflight fail without the change; TestTranslatedSilenceStillTimesOut keeps a silent provider uncovered. In a sandbox CODEX_HOME, codex exec through the gateway on a provider sending 6s of keepalives with stream_idle_timeout_ms 2500 answers, and without the change fails with idle timeout waiting for SSE. go vet, go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui ./... pass.
2026-10-01 22:36:36 +08:00
gateway: every full id of a Claude family Claude Code names goes to its tier's model, served by a provider of the user's or not (KevinXC on Discord, v0.1.665, T3 Code 0.0.45 over Claude Code 2.1.287: claude-haiku-4-5, claude-opus-5, claude-fable-5 and claude-sonnet-4-6 went to the haiku/opus/fable/sonnet tiers, but claude-fable-5-1, claude-sonnet-5 and claude-sonnet-5-5 went to the Claude account, 429 "requires usage credits" / "weekly limit") — the stand-in (4cbd96a3) was only asked for a model magpie shows no entry for, so a full id a provider of the user's serves bare (the Claude account lists claude-sonnet-5, claude-fable-5-1, …; a found auto-<model> group has it too) went there, while one none listed went to the tier: which tier a model reached depended on what the user's providers happen to list, not on the model. T3 Code names every model by its full id rather than Claude Code's alias, so it hit both. Now an id of the opus, sonnet, haiku or fable family (claudeFamily: claude-<family>-<any version>[-date][1m], and claude-3-5-sonnet-… alike, so a release magpie has never heard of is one too) from Claude Code routed through magpie goes to the model its tier is set to before groups or providers are looked at, count_tokens counting it there too. Kept as asked: a provider's "anth/claude-…", another agent's request, Claude Code not routed through magpie (StandIn says nothing), a routing group of the user's own named by the id (one magpie found, auto-…, was never picked for it), and the main model when Claude Code's ANTHROPIC_MODEL is that very id. A tier at an effort of its own (#536, "fake/sol:high[1m]") now reaches its model at that level through a stand-in too, where it was looked up whole and refused. TestClaudeTierStandInByFamily (claude-haiku-4-5, -20251001, claude-haiku-5, claude-opus-5, -5-5, -5-5[1m], claude-opus-6, claude-fable-5, -5-1, -5-1[1m], claude-sonnet-4-6, -5, -5-5, -5-5-20261001, claude-3-5-sonnet-20241022 through /v1/messages, beside a provider serving seven of them bare) fails without the change for the nine served ones; TestClaudeStandIn gains the families and the main model. go vet ./internal/..., go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui -count=1 ./... under a temp HOME pass (TestDevinSignedInOnlyInMagpie, a known flake, aside).
2026-10-02 23:50:42 +08:00
providers: Tencent Cloud TokenHub pay as you go is a preset, its global endpoint and China's one row in the add sheet (Jorben on Discord: magpie's Tencent Cloud is TokenHub's Token Plan subscription, whose endpoints pay-as-you-go keys can't use; TokenHub's pay as you go is at tokenhub.tencentmaas.com and tokenhub-intl.tencentmaas.com and speaks Chat Completions, Responses and Anthropic Messages) — the only Tencent preset was tencent-token-plan, at the plan's own api.lkeap.cloud.tencent.com/plan/v3 and /plan/anthropic, where a pay-as-you-go key is no good, so such a key could only go in a custom provider by hand. Now tencent-tokenhub ("Tencent Cloud TokenHub", Singapore, tokenhub-intl.tencentmaas.com) and tencent-tokenhub-cn ("Tencent Cloud TokenHub (China)", Guangzhou, tokenhub.tencentmaas.com) are presets, as TokenHub's language-model API page (1823/130079) gives them: chat completions and Responses at <host>/v1 (Responses converted from chat on TokenHub's side for the models that speak chat alone, 1823/133813), Anthropic messages at <host>/v1/messages with the key as x-api-key, the OpenAI APIs with it as a Bearer, and the model list at /v1/models. Being id and id-cn they pair into one row in the add sheet, its editor switching between them with the key kept, beside the plan's Tencent Cloud row, with the plan's icon (tencentcloud-color), the note "Pay as you go" (按量付费), and models.dev's tencent-tokenhub entry for names and prices; Hosts, as it serves DeepSeek, GLM, Kimi, MiniMax and Qwen besides Hy, so its list is no maker's word on theirs. Keys are made at console.tencentcloud.com/tokenhub/apikey and console.cloud.tencent.com/tokenhub/apikey. An entry imported from another app at either host, on any of the three APIs, is the preset. The plan's preset, its endpoints, models and saved providers are untouched; its note "TokenHub · subscription" now has its Chinese (TokenHub · 订阅). TestTencentTokenHub (endpoints, pair, import from each API, the plan unmoved), TestTencentTokenHubList (its /v1/models as its docs show it, asked with a Bearer, an embedding model left out) and TestTencentTokenHubRoutes (both presets at a fake TokenHub: chat, Responses and messages each reach /v1/chat/completions, /v1/responses and /v1/messages untranslated, Bearer on the first two, x-api-key on messages) are new and fail without the change (no such preset). No request was made to Tencent and no real key used. add-sheet.test.cjs passes in WebKit. go vet ./internal/..., go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui -count=1 ./... under a temp HOME pass (TestFXInState's known TempDir cleanup flake aside).
2026-10-02 21:11:03 +08:00
gateway: a model that won't be made to call a tool is asked again with the call left to it (#668: OmO's first turn forces its todo tool, and Claude Opus 5.5 answers 400) — the issue reads the 400 as Anthropic wanting additionalProperties:false on every object of a named tool's schema; it doesn't. Against Anthropic's API (the owner's Claude subscription) the same two tools, schemas without additionalProperties, with tool_choice {type:"tool"} are taken by claude-sonnet-4-5, claude-opus-4-8, claude-opus-5 and claude-sonnet-5, and refused by claude-opus-5-5, claude-sonnet-5-5 and claude-fable-5-1 with tool_choice: type "tool" and "any" are not supported for this model., with or without additionalProperties:false on every object; OpenRouter (Anthropic, Bedrock, Azure, Claude Platform on AWS) answers the same. Anthropic says "tools.N.custom: For 'object' type, 'additionalProperties' must be explicitly set to false" only of a tool sent with strict:true, auto or forced alike, and buildAnthropic sends no strict; the reporter's own capture shows none either, so it is the vendor behind their Claude Pro endpoint holding the forced tool to strict mode. Either way the 400 came from forcing the call on a model that can't be forced, and it was relayed as is, with no fallback. forwardTranslated now asks again when a forced tool_choice (a named tool, or required) is turned away with either error: the call is left to the model (auto), only the named tool offered, as the Claude subscription's bridge does, and the provider isn't sent a forced choice for that model again (unfit, as reasoning-off refusals are). Schemas are unchanged. TestForcedToolChoiceRefused (Chat to an Anthropic-only provider, with each error) fails on the old code. Through a sandbox gateway on an Anthropic-only copy of the owner's OpenRouter provider, a Chat request forcing todo to anthropic/claude-opus-5.5 answered 400 with the old build and a todo call with this one, twice. go vet ./internal/..., builds (darwin, GOOS=linux -tags nogui, GOOS=windows) and go test -tags nogui -count=1 ./... under a temp HOME pass but TestProvidersAnswerWhileListsComeIn, a timing test that passes alone. A Chat client on a Chat endpoint (passthrough, e.g. OpenRouter's chat) is not changed.
2026-10-03 19:04:19 +08:00
gateway: every vendor's "too long" reaches the agent as context_length_exceeded and fails over to nobody, and a spent budget isn't asked again in place (owner: 继续做第 4 项,错误分类接入 failover) — pi's two tables (packages/ai/src/utils/overflow.ts, retry.ts) against magpie's classifier: 15 of pi's 25 overflow texts weren't known, so the agent got the vendor's words and didn't compact, and four were worse: Kimi's "exceeded model token limit" rested the account as out of quota, and z.ai's model_context_window_exceeded, Ollama's "prompt too long; exceeded max context length" and "token limit exceeded" went to every other account in turn (quotaWords' "exceeded"). tooLongRe takes pi's patterns (request_too_large, Gemini, xAI, Groq, OpenRouter, Together, llama.cpp, LM Studio, Copilot, MiniMax, Kimi, DS4, z.ai, Ollama, DashScope), and overflowed (a 400/413/422 saying so) is neither retryable nor a quota or credit failure, so nobody rests and it is answered at once. "out of budget" / "budget exceeded|exhausted" are a used-up plan: a 429 was a rate limit asked three times in place, a 400 was answered without trying the next account. TestEveryVendorsOverflow and TestOutOfBudgetIsntRetriedInPlace fail on the old code (26 failures). Real accounts, sandbox magpie, a 2.2M-token request: Qoder's "invalid params, context window exceeds limit" came back from the old build as 413 api_error with no code (Claude Code and Codex wouldn't compact) and is now 400 context_length_exceeded / Anthropic "prompt is too long: …"; WorkBuddy, WorkBuddy AI, Kiro and Grok (SuperGrok) answer context_length_exceeded too, and ordinary requests through each still answer. go vet, GOOS=linux/windows builds and go test -tags nogui -count=1 ./... under a temp HOME pass. Silent overflow (usage past the window on a 200) isn't detected: the reply is already the agent's by then.
2026-10-03 14:55:37 +08:00
gateway: a request well past the model's window that a vendor turns away without saying why reaches the agent as too long, at once; Qoder's plugin to 0.2.4 for its token usage (found testing real accounts) — Devin answered a conversation of about 300k tokens for swe-1.7-lightning (a 202,752 window) with a 502 "We are currently experiencing capacity issues…", Qoder one of 1.8M for qmodel (1M) with a 400 "Error in upstream response", each answered at a size the model holds; magpie asked Devin's again in place three times and handed the agent a 502 api_error, so Claude Code and Codex never compacted. tooLongUnsaid (fallback.go) takes a 400/413/422/5xx that says nothing of quota, a rate limit, the account, the model or the conversation's length, for a request whose estimate is 1.2 times the model's window or more (the user's context, else the catalog's; estimate counts four bytes a token, which most text runs under: Devin counted 173,954 tokens of one estimated at 151,851), as one too long: "prompt is too long: about N tokens, more than <model>'s context window of M (<vendor> answered <status>: …)", sent the agent's way (context_length_exceeded, Anthropic's "prompt is too long"), not asked again of it or another account, and nobody rests. TestOverflowTheVendorDidntSay fails on the old code (a 502 asked three times, a 400 handed on) and passes; a request near the window, a small one, a 429, a 401 and a used-up balance keep their errors. Qoder's plugin read its stream only up to the finish, and Qoder sends the usage in a chunk of its own after it, so every Qoder request was counted as 0 tokens: @magpie-community/opencode-qoder-auth 0.2.4 reads it, and the mover's min is raised to it. Tried with real accounts in a sandbox magpie: Devin (chat and Anthropic) and Qoder now give 400 context_length_exceeded after one try, a WorkBuddy and Kiro overflow as before, small requests to all three answer, and Qoder reports its tokens (streamed, not, and with a tool call). go vet, builds (darwin, GOOS=linux, GOOS=windows) and go test -tags nogui -count=1 ./... under a temp HOME pass but a provider timing test that passes rerun.
2026-10-03 16:14:38 +08:00
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
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
gateway: a model that can't see images is given what a model that can says of them (让Magpie支持配置图像识别模型) — until now an image sent to a model the catalog knows takes text only was turned away in the latest turn and left out of older turns and tool results, so an agent on DeepSeek, GLM or any text-only model couldn't be shown a screenshot at all. Now each image in such a request is described once by the vision model and the description goes in its place, in every protocol (Chat, Responses, Anthropic, Gemini), current turn, older turns and tool results alike: "[Image, as <model> describes it for this model, which can't see images:] … [End of the image's description]". The describer is asked through the gateway itself (User-Agent magpie-vision/1, recorded as agent magpie), with a system prompt to transcribe text, code, errors and UI state exactly and not to answer or guess; images are described four at a time, and a description is kept (256, by model+image) since an agent sends a conversation's images again every turn, a failure not kept. An image of the latest turn that can't be described is a 502 saying why; one in an older turn is left out as before. Settings → Images → Image recognition picks the model: Automatic (a small model that sees — a Claude account's, a Codex account's, the cheapest of an API key's, another account's), Off (images turned away as before), or any model or group that sees; an id that doesn't resolve is refused. In a routing group the description is made only when a text-only member is actually tried, so a rule that routes images to a member that sees costs nothing; a member whose image can't be described is skipped for the next, the last one answers 502. The describer's own request is never described again, so a Vision set to a text-only model is turned away, not looped. Checked in a sandbox HOME against a local vendor: an image sent over Chat and over Anthropic to a text-only model reached it as the description, the describer was asked once per image, and the Settings row and its picker in light and dark.
2026-09-29 01:20:31 +08:00
vision: a model whose list says nothing of images is given the image described, and the description is in Routing as its own call (Discord, Fate: since v0.1.725 images reach a text-only model in Codex, but it reads them badly and Routing shows no call to the Vision model) — 19e239c7 tells agents every model takes images while a describer is set, but the gateway described an image only for a model whose list says explicitly it takes none (ImageInput false); a model the vendor's /models says nothing of (ImageInput nil — most API keys' models, which magpie counts text-only and agents were told so before) had the image sent on as it came, so the text-only upstream got the raw image (dropped, or read by whatever it does with it) and the Vision model was never asked. Now, while a describer is set, a model that isn't known to see (its catalog entry's Images: the list's answer, the user's override, else models.dev) is given the description, on every API (Responses and Codex's /backend-api/codex, Chat, Anthropic, Gemini), for its current turn, older turns and tool results (function_call_output's input_image), and as a routing group's member (blindTo). Without a describer an unknown model is passed images as before. The description was already a call through the gateway (agent magpie-vision) but untied to anything; it now has kind "vision" ("Image description" in Routing, zh 图片描述), For naming the agent and model it described for, and the request's session, so it shows in that conversation's Routing beside the turn. The describer's prompt (every text transcribed, layout, values) and the image (sent whole, never downscaled) were already right. TestModelNotKnownToSeeIsGivenTheDescription (data: and https input_image on /v1/responses and the Codex backend path, Chat), TestModelNotKnownToSeeToolAndGroupImages and TestDescriptionIsInRouting fail on the old code and pass. go vet, builds (darwin, GOOS=linux, GOOS=windows) and go test -tags nogui -count=1 ./... under a temp HOME pass but TestUserPath, TestCodexVersion, TestClaudeRunEndsWhenTheClientLeaves and davsync, timing tests under load that pass alone and in their packages rerun. Not tried in the Codex App with a real account.
2026-10-03 16:05:17 +08:00
vision: a model whose list says nothing of images is given the image described, and the description is in Routing as its own call (Discord, Fate: since v0.1.725 images reach a text-only model in Codex, but it reads them badly and Routing shows no call to the Vision model) — 19e239c7 tells agents every model takes images while a describer is set, but the gateway described an image only for a model whose list says explicitly it takes none (ImageInput false); a model the vendor's /models says nothing of (ImageInput nil — most API keys' models, which magpie counts text-only and agents were told so before) had the image sent on as it came, so the text-only upstream got the raw image (dropped, or read by whatever it does with it) and the Vision model was never asked. Now, while a describer is set, a model that isn't known to see (its catalog entry's Images: the list's answer, the user's override, else models.dev) is given the description, on every API (Responses and Codex's /backend-api/codex, Chat, Anthropic, Gemini), for its current turn, older turns and tool results (function_call_output's input_image), and as a routing group's member (blindTo). Without a describer an unknown model is passed images as before. The description was already a call through the gateway (agent magpie-vision) but untied to anything; it now has kind "vision" ("Image description" in Routing, zh 图片描述), For naming the agent and model it described for, and the request's session, so it shows in that conversation's Routing beside the turn. The describer's prompt (every text transcribed, layout, values) and the image (sent whole, never downscaled) were already right. TestModelNotKnownToSeeIsGivenTheDescription (data: and https input_image on /v1/responses and the Codex backend path, Chat), TestModelNotKnownToSeeToolAndGroupImages and TestDescriptionIsInRouting fail on the old code and pass. go vet, builds (darwin, GOOS=linux, GOOS=windows) and go test -tags nogui -count=1 ./... under a temp HOME pass but TestUserPath, TestCodexVersion, TestClaudeRunEndsWhenTheClientLeaves and davsync, timing tests under load that pass alone and in their packages rerun. Not tried in the Codex App with a real account.
2026-10-03 16:05:17 +08:00
routing: a group in a group is routed by its own routing whatever the outer group's, and an account with five hours and no week is ranked by its five hours (#576: 只有五小时额度的账号在智能/重置前用完下是否合理;嵌套路由组时上层路由是否会干预下层) — a group routed other than In order (Smart, In turn, Least used, Weekly pace, Manual) put every account and key of every group in it into one pool and weighed them together, so A (Smart) holding A-1 and A-2 (each In order) ranked all five of their accounts and keys by Smart and A-1's and A-2's own In order did nothing; only an outer In order let each inner group route itself. planLevel now plans each group in the group by its own routing, whatever the outer's, and weighs it as one beside the outer group's own models' keys, by the first of its own order that isn't resting; where that lands, the inner group is tried whole in its own order, the trace in the same order. As for the five hours: Smart already ranked such an account (Claude Enterprise) by its five hours, the biggest window it has, so ahead of every week renewing later, but a five hours not started (no reset told) counted as a reset not known and went after every week; Allowance.Renewal now has a window not started that says its span renew that long from now, which Smart sorts by (the trace's renews unchanged). Weekly pace took an account with no week as a fresh week with the share used (100/168 per hour), so a week with 80% left and five days to go went ahead of an Enterprise account whose five hours were being lost; it now goes by what its five hours have left per hour until they renew, the whole span when not started, except where the reading may leave a week out — a Claude account only heard of from Claude Code's rate_limit_event, never read whole by /usage (QuotaWindow.partial, claudeUsageEntry.whole) — which keeps the fresh week. The group editor's Smart and Weekly pace hints and the trace's say how an account with five hours and no week is ranked, a hint under Routing says a group among the models keeps its own routing, and the docs (en, zh) say both. TestGroupInGroupKeepsItsOwnRouting (Outer rotating over two In order groups: b c d a without the change, c d a b with it), TestSmartFiveHoursAlone (a five hours not started went after a week in two days), TestFiveHoursAlone, TestClaudeHeardOnlyIsPartial; TestWeeklyPace and TestAllowancePace now pin the five hours alone. settings-groups, group-member-fast, group-found-off, routing-idle-frame pass in WebKit. go vet ./internal/..., go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui -count=1 ./... under a temp HOME pass (TestFXInState, TestProvidersAnswerWhileListsComeIn, TestUserPath, TestClaudeSignedOut flaked; the last passes 3/3 alone).
2026-10-02 23:05:15 +08:00
gateway: ZCode's count_tokens is estimated, and a vendor's firewall block is said in plain words (ZCode Start Plan: HTTP 405 "request has been blocked due to unusual activity") — Claude Code asks count_tokens before every turn, and magpie sent each to zcode.z.ai/api/v1/zcode-plan/anthropic/v1/messages/count_tokens, which answers it with an error every time and which ZCode itself never asks; a ZCode account's count, built-in or on its plugin, is now estimated as Cursor's, Grok's and Kiro's are, and zcode.z.ai is not asked. The 405 is Alibaba Cloud's edge firewall (ESA) in front of zcode.z.ai blocking the address: its HTML page was shown as "405 Method Not Allowed" and, a 405 not passing as a provider's failure, no other member of a group was asked. A block page (request has been blocked, errors.aliyun.com) now reads "the provider's network firewall blocked requests from this IP; wait a while, or switch to another network or proxy" to the agent and in the trace, where the Routing page gives it on its own line in English or Chinese, and the group goes on to its next member, the blocked one resting as for any other failure. No request header, user agent or device id changes. TestZCodeCountTokensEstimated, TestMovedZCodeCountTokensEstimated, TestEdgeBlockedSaidPlainly and routing-blocked.test.cjs fail without the change. go vet, go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui ./... pass; routing-blocked, routing-wb-refused, routing-proxy, routing-shape, click-scroll, old-webkit pass.
2026-10-01 15:04:16 +08:00
docs: every built-in subscription a plugin serves once moved says so in its code, and AGENTS.md says where a fix for it goes (user: 对内置的订阅 provider 在代码层面和文档层面加上注释,防止又有别的 agent 在改内置订阅 provider 的代码而不知道生效的其实是对应的插件代码) — a moved Cursor, Devin, Grok, Command Code plan, Kiro, Zed, Qoder, Factory, Xiaomi MiMo, ZCode or WorkBuddy (provider.Moved; since v0.1.642 also the default for a new sign-in, provider.Adopt) is signed in, listed, requested and metered by its @magpie-community plugin, and none of the built-in's code in internal/provider/<name>*.go, internal/gateway/<name>.go, internal/zed or internal/qoder serves it; a fix made there alone never reaches those users. Each of those files now opens with a PLUGIN-SERVED notice naming the subscription's id, its plugin package, where the plugin lives (magpie-community/plugins, packages/<name>) and the mover whose min to raise so moved users get the fixed plugin (keepMovedCurrent); Qoder's adds that Qoder CN isn't moved and still runs there. AGENTS.md (CLAUDE.md imports it) lists the moved built-ins with their packages and movers, how to tell whether a user's is moved (move.state "plugin", onPlugins), the order a fix goes in (the plugin, publish, raise min, the built-in only for those not moved), the magpie code that does run for a moved one (internal/plugin, plugins.go, plugin_usage.go, pluginsignin.go, migrate*.go, app.js's subOf/pluginSubs/startPluginSignIn), and the parity tests. movers in migrate.go and SUBS in app.js point there too. TestMovedBuiltinsSayTheirPlugin fails for a mover whose code carries no PLUGIN-SERVED notice naming its id and package (it failed while a wrapped package name was split). Comments and docs only, no behaviour changed. go vet ./internal/..., go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui -count=1 ./... under a temp HOME pass (TestProvidersAnswerWhileListsComeIn and TestClaudeSignedOut failed once on their timing under load, pass alone; untouched).
2026-10-02 19:37:02 +08:00