Files
ggbdpqandfactory-droid[bot] d81706f974 gateway: a Chat or Responses client's xhigh on Claude Opus 4.7 and later, Sonnet 5 and later or Haiku 5.5 goes as xhigh, not max
buildAnthropic asked every Claude that thinks only adaptively for max
when the client asked xhigh, on the grounds that 4.6 has no xhigh. Only
Opus and Sonnet 4.6 lack it: Anthropic's effort docs list xhigh on
Fable 5 and 5.1, Mythos 5 and 5.1, Opus 5.5, 5, 4.8 and 4.7, Sonnet 5.5
and 5, and Haiku 5.5, and describe max as no constraint on token
spending. So Codex, Cursor or any Chat client picking xhigh on
claude-opus-5-5 was billed for max. Claude Code's own levels in magpie
(claudeEffortsFor) already say only Opus and Sonnet 4.6 skip xhigh.

Now xhigh becomes max only on 4.6 (noXhigh, which reads the version
through claudeVersionOf, shared with adaptiveOnly). A relayed thinking
budget is unchanged: budgetOf gives xhigh and max the same budget, and
31999 is Claude Code's old ultrathink, so a budget above high still
goes as max. docs/subsystems/gateway-routing.md now says which models
are asked adaptively (#1470) and where xhigh stays xhigh.

What was run: TestXhighStaysXhighWhereClaudeTakesIt (Chat and Responses
through the server stack to a fake Anthropic upstream) fails on the old
main with output_config = map[effort:max], want "xhigh" for Opus 5.5
(Chat and Responses), Opus 4.7, Sonnet 5 and Haiku 5.5, and passes with
the change; Opus and Sonnet 4.6 get max before and after, and a Fable
5.1 row asserts xhigh. TestAdaptiveThinking's Opus 5.5 xhigh case now
asserts xhigh, beside a 4.6 case asserting max. Broken by hand (noXhigh
always false) the 4.6 cases fail. gofmt -l prints nothing; go vet -tags
nogui ./... passes for darwin, linux and windows; go test -tags nogui
-count=1 ./... under a temp HOME passes. Through the aihubmix relay,
claude-opus-5-5 with adaptive thinking and effort xhigh answers 200.
Not tried against Anthropic directly (no credential outside the
keychain); a relay that checks effort against an older list could still
refuse xhigh.

Reported and fixed by ggbdpq (#1471).

Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
2026-10-10 15:17:13 +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
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
agent, gateway: Cursor Private Inference's row has an effort, which magpie's gateway asks its requests for (#1003, gjf7) — other agents' rows pick a reasoning effort that magpie writes into their config, but Cursor Private Inference reads only its base URL and key from the environment, and its app offers no Reasoning control for most of magpie's models, so there was no way to set one short of a routing group with a member fixed at a level, which the reporter used as a workaround. Its row now has an effort field like the others': the default (as Cursor asks) first, then every level the models it lists have (cursorLocalEfforts: their levels in MemberEfforts order, low/medium/high where a model reasons with levels unknown, none at all when nothing it lists reasons or it isn't connected). The pick is kept in magpie, settings' agentEfforts by agent (provider.AgentEffort / SetAgentEffort, a level of MemberEfforts or "" for none), and the gateway's candidate loop asks a cursor-local request for it with withFixedEffort, fitted to the model's levels and bounded by a group's, on a model that reasons (the new Provider.Thinks: levels, its list's Reasoning, or models.dev's). Precedence: a group member's fixed effort, then a :effort suffix, then a routing rule's turn pick, then the agent's pick, then the request's own; the pick wins over a level Cursor sent, as Cursor sends its own default as often as a choice, and Default hands the choice back to Cursor. A model that doesn't reason and every other agent's requests go as they came. The Settings page's save carries agentEfforts over as it does fastPicks (TestSettingsSaveKeepsWhatItDoesNotSend's fixture names it). In the window the field sits under "Settings" rather than "New sessions" for this agent alone, as it applies to every request from then on, not to what a session starts on; no new t() strings (Settings, effort and default are translated in zh, ja and de already). TestAgentEffortOnCursorLocal (Chat and Responses with and without a level of their own, Claude on Messages made to think, OpenCode's request untouched, a non-reasoning model untouched, a member fixed at low kept, the pick taken off) fails without the gateway change and passes; TestCursorLocalEffort covers the options (none with no reasoning model, the union of levels, the low/medium/high fallback, none while disconnected), Apply and a bad level refused; both pass -race (-count=20 gateway, -count=10 agent). cursor-local-effort.test.cjs (the window at 560px and the tray panel at 440px: the field under Settings, the slider's five stops with Default first, a pick posting the field, no sideways scroll) passes on Chromium and WebKit in en, zh, ja and de; claude-effort, subagent-effort, agent-field-names, agent-connect, agent-layout, agent-tweak-fold, agent-models, cursor-local-launch and i18n pass on both. go test -tags nogui ./internal/..., go vet, gofmt -l, linux (nogui) and windows builds pass. Not tried with the real Cursor Private Inference: whether it sends a reasoning level of its own on models it shows a Reasoning control for is unverified, which the precedence makes moot. docs/subsystems/gateway-routing.md and agent-wiring.md say it.
2026-10-06 17:29:28 +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: reasoning magpie made isn't relayed to Azure OpenAI or OpenAI's API, and a summary request refused for an item not found is asked again as text (#1008, zhouxiong100: Codex on an Azure OpenAI deployment failed every compaction with "Azure OpenAI: Item with id 'rs_18dbe693b3dce4800000003c' not found", Reconnecting 5/5, store=true by a middleware no help) — Codex offers web search on every turn, so its turns on Azure went out translated, and the translated replies gave Codex reasoning items magpie made: rs_ and newID (the reporter's id is one, a UnixNano in hex and a counter), a summary and nothing sealed. Codex hands them back on every turn. buildResponses replays no reasoning to Azure, so the turns worked; Codex's compaction offers no tools, so it isn't translated but relayed as it is, and Azure, with store false, looked rs_… up among stored items, found none and answered 400 for the whole request. codexCompact asked again as text only on a 404 (#866), so the 400 went back to Codex. passthrough now drops reasoning with no encrypted_content from a Responses request to OpenAI's API or Azure OpenAI unless store is true (withoutBareReasoning): both look such an item up and can't have it, its summary being the model's notes no model reads back; their own sealed reasoning still goes, and every other host gets the request as before (a relay may read the summary for a vendor that wants reasoning back). The same holds for any turn relayed as it is, Codex's with web search off for one. codexCompact's fallback also takes a 400 naming an item not found (itemNotFound: unreadableItem and "not found"), as a relay in front of either passes on; any other 400 still goes back as it came, and the local summary names the status it got. TestAzureCompactionWithoutMagpiesReasoning (the Azure preset at a fake /openai/v1/responses answering a storeless request with a bare reasoning item Azure's 400 word for word; the compaction and a relayed turn, each asking Azure once with the sealed reasoning kept and magpie's dropped) fails without the passthrough change (the turn 400 with the reporter's exact message, the compaction asking twice) and passes; TestCodexCompactItemNotFoundAsksAgainAsText fails without the codexCompact change (400 to Codex) and passes; TestCodexCompactOther400Stays guards other 400s. -race -count=10 on them, TestCodexCompact* and TestAzureRoutes. go test -tags nogui ./internal/gateway ./internal/provider, go vet, gofmt -l, linux and windows (nogui) builds pass. Not tried with a real Azure OpenAI resource (none on this machine); the 400's text is from the reporter's screenshot and issue. docs/subsystems/gateway-routing.md says it.
2026-10-06 17:53:18 +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
claude: Claude Code's auto mode checks read their prompt from the cache, and a request with thinking off runs with it off (0xAncientTwo on X: Claude 额度在 magpie 下掉得特别快) — behind a gateway Claude Code's auto mode classifier is a request of its own for each action it checks, billed to the subscription (on Claude Code's own sign-in Anthropic runs it server-side), with a ~37k-token security-monitor prompt. Its system starts with x-anthropic-billing-header: cc_version=….<hash>, the hash taken from the request's first user message, which for the classifier differs on every check; the bridge put the system, header first, into the cached instructions block, so every check wrote all ~37k tokens to the cache again (2x the input price at the 1h TTL) and read 2303. renderClaudePrompt now leaves the header out (Claude Code's runs send their own). Claude Code's small asks and its classifier also send thinking disabled, and the bridge ran them at Claude Code's default, which on Haiku thought 1.8k–5k output tokens before a 64-token verdict or a title; a request with thinking off now runs with --thinking disabled, and that is part of the run's key, so it never resumes a run that thinks or the other way round. With the owner's real Claude subscription, the captured classifier request replayed twice with two hashes: on origin/main each check wrote 36482 and read 0; now the second wrote 343 and read 36086 (Sonnet 4.6, 10 tokens out). On Haiku 4.5 a check had 2599 reasoning tokens before and 0 now. TestClaudeInstructionsWithoutBillingHeader and TestAutoModeClassifierOnTheClaudeBridge's --thinking disabled check fail on the old code; the gateway's TestClaude* and TestAutoMode* pass.
2026-10-04 20:33:12 +08:00
gateway: a Claude subscription conversation the client sends back rewritten goes on in the run that had its last turn, and Claude Code's temp images go with their session (#912, margbug01) — Alma takes the [Context: …] block out of its last user message once the turn is over, the <alma_notification> block out of the end of the reply, and picks the tools for each turn, so turnKey, which hashes all of it, never matched and every turn started a new claude -p told the whole conversation, written to the prompt cache again with its images. ended keeps a looseTurn beside the key: the settings without the tools, the conversation before the last turn hashed, and the last turn's messages one by one. When the exact key misses, rewritten takes the one idle run or saved session whose looseTurn has the same owner, settings and earlier turns, the same roles, tool calls and results in the last turn, where a user message only had one part taken out (cutFrom) and the reply is a non-empty start of what the run said. Two that could be it start a new run, as does an idle run not told a tool offered now (offers) or a saved session one of whose tools is gone (toolsCover). Claude Code's <CLAUDE_CODE_TMPDIR or /tmp>/claude-<uid>/<work project>/<session> folder, where it keeps the images it was given, is removed with the session (removeSession), and the sweep removes those a stopped gateway left once their session file is gone and nothing in them changed for a day; CLAUDE_CODE_TMPDIR joins agentenv.Vars. Old images are still sent to a new run: replacing them would change what the model sees. docs/subsystems/claude-subscription-bridge.md is new. claude_rewritten_test.go has a case for each relaxation (failing before the change) and one for each rule that keeps another conversation out (each failing with that rule taken out). Tested for real with the owner's Claude Max account in a sandbox HOME: turn one asked with a [Context] block and two tools, turn two sent it back with the block taken out, the reply cut short and one tool, and went on in the same claude process ("goes on in its run", cache_read 1932). go vet, go build (darwin, GOOS=linux nogui, GOOS=windows) and go test -tags nogui ./... under a temp HOME pass.
2026-10-05 22:56:30 +08:00
claude: on Windows with no Claude Code of its own, the gateway runs the one in WSL (X: kyotiger, Claude Code installed in WSL only, magpie on Windows found no claude) — claudeBinary and the sign-in's findClaudeCLI now fall back, when no claude is on Windows, to one in a running WSL distro (internal/wslrun: the default distro first, Docker Desktop's left out, a stopped one never started; found by a login shell, then an interactive one for nvm's node, then ~/.local/bin and /usr/local/bin, never Windows' own under /mnt). A run is wsl.exe -d <distro> --exec /usr/bin/env PATH=<its PATH> <claude> …, so no shell rc runs and Claude Code there uses its own sign-in and quota; what the bridge sets goes in through WSLENV, CLAUDE_CONFIG_DIR (a saved account's folder) and the sign-in's BROWSER as paths (/p), the proxy variables with a loopback proxy pointed at the Windows host under NAT networking. The MCP tools server stays magpie.exe on Windows, started from WSL by its /mnt path. Killing wsl.exe leaves the Linux process running, so each run is marked (MAGPIE_WSL_RUN) and a canceled one is ended inside the distro by that mark. The Claude provider's editor says "Runs through WSL · <distro>" under Accounts (en, zh); the page never waits on WSL for it. A claude on Windows itself is still used first. With the owner's real Claude account on the Windows test box (Claude Code 2.1.289 in WSL only, magpie in a sandbox): /v1/models listed the Claude models, a haiku request answered from WSL with the MCP server at /mnt/c/…/magpie.exe, a tool_use round trip worked, the quotas came from its headers (5h 13%, 7d 37%), "Add another Claude account" opened claude.com's OAuth page through the BROWSER hand-off with its localhost callback reachable from Windows, canceling it ended claude auth login in WSL, and the editor showed the line in en and zh. Not tried: finishing a browser sign-in (needs a person). Tests: internal/wslrun (the probe's parsing, path and WSLENV translation, the proxy rewrite, Find's distro order and cache, a run's argv). go vet (darwin, GOOS=windows), builds (darwin, GOOS=linux, GOOS=windows) and go test -tags nogui -count=1 ./... under a temp HOME pass (the gateway package flaked once under load and passes alone).
2026-10-04 17:49:53 +08:00
claude: Claude Code runs share a folder only the user owns, the caller's system prompt is cached apart, and auto memory is off (#626, yg3415) — main already ran every Claude Code run in one $TMPDIR/magpie-claude so Anthropic's prompt cache is read again; this takes yg3415's version of that: the folder is magpie-claude-work-<uid> (the uid where the temp folder is shared, as /tmp is on Linux), made 0700 and used only when it is a real folder the user owns that no one else can write to (else the run works in a folder of its own, as before), so nobody else can put a CLAUDE.md in front of every run; the caller's system prompt goes to Claude Code as a block of its own with a 1h cache_control breakpoint (CLAUDE_CODE_PROMPT_CACHE_TTL=1h, since Anthropic refuses a 1h breakpoint after a 5m one), so requests with one system prompt and different conversations share its cache entry; and CLAUDE_CODE_DISABLE_AUTO_MEMORY=1, so a memory Claude Code writes in one caller's conversation doesn't show up in every other one in the shared folder. Merged onto main by hand: the run still starts through binary.command/binary.env, so a Claude Code in WSL works in the shared folder too; on Windows the folder is in the user's own %TEMP% and Go reports every folder there as 0777, so the mode check is skipped there (it refused every run before). Tests from the PR (TestClaudeRunsShareAWorkDir with a home kept in git, TestClaudeWorkDirOthersCanWriteIsAvoided, TestClaudeInstructionsCachedApart) pass; go vet (darwin, GOOS=windows), builds (darwin, GOOS=linux, GOOS=windows) and go test -tags nogui -count=1 ./... under a temp HOME pass.
2026-10-04 18:25:44 +08:00
claude: Claude Code runs share a folder only the user owns, the caller's system prompt is cached apart, and auto memory is off (#626, yg3415) — main already ran every Claude Code run in one $TMPDIR/magpie-claude so Anthropic's prompt cache is read again; this takes yg3415's version of that: the folder is magpie-claude-work-<uid> (the uid where the temp folder is shared, as /tmp is on Linux), made 0700 and used only when it is a real folder the user owns that no one else can write to (else the run works in a folder of its own, as before), so nobody else can put a CLAUDE.md in front of every run; the caller's system prompt goes to Claude Code as a block of its own with a 1h cache_control breakpoint (CLAUDE_CODE_PROMPT_CACHE_TTL=1h, since Anthropic refuses a 1h breakpoint after a 5m one), so requests with one system prompt and different conversations share its cache entry; and CLAUDE_CODE_DISABLE_AUTO_MEMORY=1, so a memory Claude Code writes in one caller's conversation doesn't show up in every other one in the shared folder. Merged onto main by hand: the run still starts through binary.command/binary.env, so a Claude Code in WSL works in the shared folder too; on Windows the folder is in the user's own %TEMP% and Go reports every folder there as 0777, so the mode check is skipped there (it refused every run before). Tests from the PR (TestClaudeRunsShareAWorkDir with a home kept in git, TestClaudeWorkDirOthersCanWriteIsAvoided, TestClaudeInstructionsCachedApart) pass; go vet (darwin, GOOS=windows), builds (darwin, GOOS=linux, GOOS=windows) and go test -tags nogui -count=1 ./... under a temp HOME pass.
2026-10-04 18:25:44 +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
gateway: a Cline reply that is not streamed reaches the agent as the completion itself, not in Cline's {success, data} envelope (6094S on Discord) — Cline's API answers a request with "stream": false as {"success":true,"data":{chat.completion}}, and magpie passed that on byte for byte to an agent asking the Chat API of ClinePass (cline-pass/deepseek-v4.1-flash): a client looking for choices at the top failed, Alma's tool-model calls with "Invalid JSON response". Streams were fine, and a translated request is asked of Cline as a stream, so only the Chat API's whole replies were hit. forwardOnce now takes the envelope off a Cline provider's (IsCline: the ClinePass preset or a cline.bot host) JSON reply (clineUnwrapped): success true with an object data goes on as data, so chatWhole, the usage sniffer and the agent see the completion; success false goes on as {"error":{"message":…}}, a 502 when it came with a 2xx, which writeError puts in the agent's API's shape. A stream, a compressed reply, a body without success and another provider's replies pass as they came. The community plugin @magpie-community/opencode-cline-auth already unwraps it (unwrapped in index.mjs) and needs nothing. TestClineEnvelopeUnwrapped (the completion on the Chat API, another provider's body untouched, a refusal as an error, a stream untouched) fails without the change. No Cline account to test with; Cline's real API answers a request without a key with a plain {"error":…} 401, which is unchanged. go vet, go build (darwin, GOOS=linux nogui, GOOS=windows) and go test -tags nogui ./... under a temp HOME pass.
2026-10-05 21:01:28 +08:00
gateway: a Cline reply that is not streamed reaches the agent as the completion itself, not in Cline's {success, data} envelope (6094S on Discord) — Cline's API answers a request with "stream": false as {"success":true,"data":{chat.completion}}, and magpie passed that on byte for byte to an agent asking the Chat API of ClinePass (cline-pass/deepseek-v4.1-flash): a client looking for choices at the top failed, Alma's tool-model calls with "Invalid JSON response". Streams were fine, and a translated request is asked of Cline as a stream, so only the Chat API's whole replies were hit. forwardOnce now takes the envelope off a Cline provider's (IsCline: the ClinePass preset or a cline.bot host) JSON reply (clineUnwrapped): success true with an object data goes on as data, so chatWhole, the usage sniffer and the agent see the completion; success false goes on as {"error":{"message":…}}, a 502 when it came with a 2xx, which writeError puts in the agent's API's shape. A stream, a compressed reply, a body without success and another provider's replies pass as they came. The community plugin @magpie-community/opencode-cline-auth already unwraps it (unwrapped in index.mjs) and needs nothing. TestClineEnvelopeUnwrapped (the completion on the Chat API, another provider's body untouched, a refusal as an error, a stream untouched) fails without the change. No Cline account to test with; Cline's real API answers a request without a key with a plain {"error":…} 401, which is unchanged. go vet, go build (darwin, GOOS=linux nogui, GOOS=windows) and go test -tags nogui ./... under a temp HOME pass.
2026-10-05 21:01:28 +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: Codex's /model lists its models in the order dragged on the Agents page (M3chD09, #855) — Codex lists the models by the priority magpie gives them: a ChatGPT account's own first, as the backend has them, then magpie's in the catalog's order, with no way to change it. Codex's model list (Pick on its row) now has a third view, Order: the models shown as Codex lists them, numbered, each dragged elsewhere (its grip shows on hover) or moved with Alt+↑/↓; it is saved at once (settings orderedModels, by agent and entry id), and the account's own models and magpie's go in one order. A model not in the order (one added later) goes after those that are, as it stood; the hidden stay hidden and out of the order view, hiding keeps the order, and a provider renamed renames its ids in the order too. Default order puts magpie's back. Codex gets it at once: signed in to ChatGPT, the gateway's /models gives the backend's and magpie's models priorities 1..n in that order and the list's ETag carries the order so Codex asks again; not signed in, the catalog file magpie writes for Codex (ListedFor → InOrder) is rewritten in it. Only Codex's list can be ordered; base_instructions are untouched. zh, ja, de strings. TestCodexModelListInUsersOrder (gateway: default order, an order across the account's and magpie's, the ETag changing, CatalogFor, opencode's untouched, none restoring it) and TestAgentModelOrderAPI (gui: orderable, an order read back, a new model last, hidden kept, hiding keeping it, reset, opencode refused) fail on the old code (no SetModelOrder/ModelOrder); codex-model-order.test.cjs (WebKit, en and zh: Order lists the own first and leaves the hidden out, Alt+↓ and a drag post the order, Default order posts none, nothing scrolls; a list that can't be ordered has no Order) fails on the old code (no Order). agent-models, agent-models-all-hidden, agent-models-loading, agent-model-groups, click-scroll, gui-ja and gui-de pass on WebKit. go vet, builds (darwin, GOOS=linux nogui, GOOS=windows) and go test -tags nogui -count=1 ./... under a temp HOME pass.
2026-10-05 11:52:34 +08:00
gateway: Codex's /models builds magpie's providers once, and cursor-agent's Keychain token is read once a while (#746, sperwe: /backend-api/codex/models called All() over and over, rescanning accounts and running /usr/bin/security some 200 times, 5.3–5.6 s against about 1 s straight to the backend, past Codex's 5 s model refresh, so Codex fell back to the official list) — after the ChatGPT backend's list, codexModels read magpie's: CodexListed (CatalogFor's catalog, and AutoVision through codexListed's descriptions), CodexNativeWindow, CodexNativePicked and CodexListTag (CodexListed again, codexWindowsTag), seven builds of All() in a test with one provider, each going through Accounts() and every agent's sign-in; a plugin's accounts ask pluginOwnUser, and for a Cursor moved onto its plugin that is cursorSignedOut, a security find-generic-password each time. All() now goes through the hold the GUI's reads use (heldOf, dff14ff4): while a request holds the catalog it is built once (a clone each call, so a caller's change stays its own; resolveIn's own heldOf("all", All) is plain All() now), and codexModels takes the hold (provider.Hold) for what follows the backend's answer. A hold is dropped by a write, as before (store → catalog.Touched → Changed, a plugin's sign-in), and now also by writeLogins and forgetAccountCaches, so an account signed in, switched or removed while one is held is seen at once; a sign-in changed outside magpie shows within the hold's second (heldFor). cursor-agent's Keychain token (readCursorToken, cursorToken) is read at most once in 10 s (cursorTokenTTL, as claudeCacheTTL); forgetCursorStatus, which a sign-in or out in magpie calls, drops it, and CursorToken reads it afresh after asking cursor-agent to renew it, so a renewed or new token isn't missed. AllBuilt counts builds for tests. TestCodexModelListBuildsProvidersOnce counts one build for one /models request (7 on the old code), TestHoldBuildsProvidersOnce one build for two All() calls under a hold, a caller's change not leaking, a provider saved and logins written while held being seen, and TestCursorKeychainReadOnce, with a fake security on PATH, one run for three reads, a fresh one after forgetCursorStatus and after a fresh read. go vet, builds (darwin, GOOS=linux, GOOS=windows) and go test -tags nogui -count=1 ./... under a temp HOME pass. Not timed with the reporter's accounts: here the request built the providers 7 times and builds them once, so their ~200 security runs should come to about a seventh, Cursor's from one a build to one in 10 s; the GUI's held reads (GET /api/*) now share one All() too.
2026-10-04 12:15:40 +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
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
gateway, gui, cli: each account or key can have its own limit on requests at once, with a bounded queue that turns requests away with a 429 (#892, margbug01) — a provider's Concurrency applied to every key and account alike, the queue past it had no bound and no wait, and nothing showed what ran or waited. A key or account now may have its own limit over the provider's (Provider.AccountConcurrency, by lowercased account or KeyID; 0 none; unset takes the provider's, as before), counted in one lane across every model, routing group and agent. A provider's Queue size bounds how many wait for each of its lanes (one more is turned away at once) and Queue wait how many seconds one waits (then it leaves the queue); both 0 by default, as before. A request turned away goes on to the next key, account or group member without resting anything, else gets a 429 in its API's own shape (Anthropic rate_limit_error, OpenAI's, Gemini's) with Retry-After: 1, saying the limit and the queue. A slot is given back when the reply is read to the end, fails or the agent goes away; a waiter that goes away leaves the queue, and one granted as it gave up hands the slot on. In a routing group, or among a provider's keys and accounts, a full one gives way once to another with a free slot now, so a group spreads before anyone waits; a fallback provider is never asked for want of a slot, and with all full the request waits for the one routing chose. The Claude subscription path and plugin providers take their slot the same way, around the attempt; a parked Claude Code tool turn has already answered and holds none. GUI: each account and key row has a pill — "1/1 running · 2 queued" while requests are out (GET /api/lanes, read every two seconds and written into the pill in place), else its limit; none set shows on hover only — opening the app's menu (provider's, no limit, 1/2/3/5/10, Other… typed in place), posting provider/accountconcurrency; the editor has Queue size and Queue wait under Concurrency. CLI: magpie provider account-concurrency <id> [account|key [n|off|default]] and magpie provider queue <id> [size [seconds]]. en/zh/ja/de. TestAccountLimitSharedAcrossGroups, TestQueueFullAndWaitedOut, TestLaneReleasedOnFailure and TestFullMemberGivesWay (pass with -race, with TestMaxConcurrency and TestLanesLimitLifted), TestLaneLimitRoutes and TestAccountConcurrencyCmd are new and fail without the change; lane-limit.test.cjs (WebKit, en and zh: a key's busy/queued pill updated in place from /api/lanes, set from the menu, provider's posts null, Other… typed, an account's own limit, none on hover only, queue fields saved and refused out of range, labels on one line, every string in zh/ja/de, no select, no scroll, no left border) fails without the change. key-weight-fit, account-cap, provider-concurrency, click-scroll, account-models and api-key-usage pass in WebKit; the other WebKit failures fail the same on main. go vet, go build (darwin, GOOS=linux nogui, GOOS=windows) and go test -tags nogui ./... under a temp HOME pass.
2026-10-05 18:53:16 +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
gateway, copilot: Auto's pick is asked as Copilot Chat asks it, and a refused pick is made again on its own API (infinitr0us, #256) — on a Student plan copilot/auto's pick from POST /models/session (gpt-5.4-mini, gpt-5.3-codex) was refused 400 "The requested model is not supported.", and the retry with the next pick, gpt-4.1, went to the same /responses and got "model gpt-4.1 is not supported via Responses API." (1) Copilot Chat 0.69 (now in microsoft/vscode extensions/copilot, automodeService.ts / AutoV2Fetcher) no longer asks /models/session: it POSTs {capiBase}/auto with the prompt at X-GitHub-Api-Version 2026-08-01 and gets session_token, expires_at and selected_model with its supported_endpoints; /models/session answers a fixed, stale list (asked with the owner's account it still offers gpt-5.3-codex/gpt-5.4-mini while /auto picks gpt-6-luna). magpie now asks /auto first, with the last user message as the prompt, takes the pick's endpoints for its API, keeps the session at most an hour, and falls back to /models/session and then the hand-picked model as before. (2) After a refusal the retry with Auto's next pick breaks out of forward's same-API loop when that model isn't served on it, and the candidates' loop makes the request again (at most twice) on the new model's own API; a held refusal is passed on only when no retry is left. TestCopilotAutoRepickTakesItsAPI (gpt-5.3-codex refused on /responses, gpt-4.1 refused there and served on chat) fails on the old code with infinitr0us's exact error; TestCopilotAutoAsksAuto (version, prompt, token, /models/session not asked) fails with a 502. With the owner's real Copilot account in a sandbox gateway, copilot/auto on /v1/chat/completions came back 200 from gpt-6-luna, picked by /auto and sent on /responses. go vet, builds (darwin, GOOS=linux nogui, GOOS=windows) and go test -tags nogui -count=1 ./... under a temp HOME pass.
2026-10-05 04:07:20 +08:00
gateway, copilot: Auto's pick is asked as Copilot Chat asks it, and a refused pick is made again on its own API (infinitr0us, #256) — on a Student plan copilot/auto's pick from POST /models/session (gpt-5.4-mini, gpt-5.3-codex) was refused 400 "The requested model is not supported.", and the retry with the next pick, gpt-4.1, went to the same /responses and got "model gpt-4.1 is not supported via Responses API." (1) Copilot Chat 0.69 (now in microsoft/vscode extensions/copilot, automodeService.ts / AutoV2Fetcher) no longer asks /models/session: it POSTs {capiBase}/auto with the prompt at X-GitHub-Api-Version 2026-08-01 and gets session_token, expires_at and selected_model with its supported_endpoints; /models/session answers a fixed, stale list (asked with the owner's account it still offers gpt-5.3-codex/gpt-5.4-mini while /auto picks gpt-6-luna). magpie now asks /auto first, with the last user message as the prompt, takes the pick's endpoints for its API, keeps the session at most an hour, and falls back to /models/session and then the hand-picked model as before. (2) After a refusal the retry with Auto's next pick breaks out of forward's same-API loop when that model isn't served on it, and the candidates' loop makes the request again (at most twice) on the new model's own API; a held refusal is passed on only when no retry is left. TestCopilotAutoRepickTakesItsAPI (gpt-5.3-codex refused on /responses, gpt-4.1 refused there and served on chat) fails on the old code with infinitr0us's exact error; TestCopilotAutoAsksAuto (version, prompt, token, /models/session not asked) fails with a 502. With the owner's real Copilot account in a sandbox gateway, copilot/auto on /v1/chat/completions came back 200 from gpt-6-luna, picked by /auto and sent on /responses. go vet, builds (darwin, GOOS=linux nogui, GOOS=windows) and go test -tags nogui -count=1 ./... under a temp HOME pass.
2026-10-05 04:07:20 +08:00
copilot: Auto falls back to gpt-4.1 first, the reply names the pick that answered, and the route says each pick's API and session token (infinitr0us, #256) — the route data from v0.1.921 showed /auto does answer a Student account: it picked gpt-5.6-luna, which Copilot refused "The requested model is not supported." magpie then went down /models in its order, but Copilot's list no longer flags any model as the chat default or fallback, nor bills any, so the order was the list's own: gpt-5.4-mini, gpt-6-luna, kimi-k3, mai-code-1.1-flash, gpt-5-mini all refused and the request failed with 400 before reaching gpt-4.1, last on the list and the one the plan is served; a second request reached it after claude-haiku-4.5. gpt-4.1 (Copilot's base model) now ranks right after a flagged fallback or default. X-Magpie-Model named mai-code-1.1-flash / claude-haiku-4.5, a refused pick: each new pick now updates it, so it ends on the one sent last. Each pick in tries[].auto now carries api (the path it went to) and session (whether Auto's Copilot-Session-Token went with it), shown on the Routing page's pick line (zh, ja, de strings), since why /auto's own pick is refused is still open: VS Code's /auto request matches magpie's. TestCopilotAutoFallsBackToBase (a Student list as the reporter's, /auto picking a refused model, only gpt-4.1 served) fails on the old code: eight refusals, X-Magpie-Model copilot/claude-haiku-4.5; routing-copilot-auto.test.cjs (WebKit, en and zh) asserts the API/session text. routing-wb-refused, routing-effort, routing-kind, routing-zcode-blocked pass on WebKit; go vet, builds (darwin, GOOS=linux nogui, GOOS=windows) and go test -tags nogui -count=1 ./... under a temp HOME pass.
2026-10-05 06:40:06 +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
agent: Claude Desktop's row picks which magpie models Desktop's own model menu lists, several at once, and the menu names them by the model's own name (蓝猫 on Discord: wanted to pick a few models and switch among them in Desktop, and Desktop showed "DeepSeek V4.1 Flash (opencode-go)" where they want "DeepSeek V4.1 Flash") — Desktop already read its menu from the gateway's /v1/models for claude-desktop (desktopModels over provider.CatalogFor, i.e. every visible model minus the agent's HiddenModels, each under a claudeLooking alias), but its row offered no way to pick them: the start button stood the opus tier there as 「上次的选择」, and display_name was "Name (provider/model)" with that id again in description. Now Desktop's row (MENU_FROM_LIST in app.js) puts a model button where another agent's start pick stands: it names the first two picked models (+N, or "No models"), its title says Desktop reads the list as it starts so it must be quit and reopened after a change, and it opens the existing per-agent model list (openAgentModels, the am-pop multi-select posting {hidden} to /api/agent-models/claude-desktop), which stays open while several are ticked; the button, the line under the name ("Connected · n models in Claude Desktop's model menu") and, in the tray panel, the row's summary are refilled on each save instead of the row being redrawn; the panel's opened row has the same button under the provider, the expand link says Details, and the opened row's "In Claude Desktop" says the menu lists the models picked here and that Desktop reads it as it starts. The provider switch (default vs magpie) and the tiers are as before. /api/state's model count carries the first three shown models' names (modelCountJSON.Names). desktopNames gives each model its own name and keeps a suffix only where two picked models share one, case-insensitively: the provider id, or the whole id if that still collides; the description is gone. Models added later still show by default, as for every agent's list. zh strings added (no ja). TestClaudeDesktopPickedModels (two providers' DeepSeek V4.1 Flash read "(opencode-go)"/"(relay)" while both are listed, and after relay's, glm-5 and the routing groups are hidden Desktop lists exactly the three left, the DeepSeek plain, while another agent still lists all) and TestDesktopNames are new; the effort and tier tests map Desktop's rows by DesktopCatalogID instead of the description. internal/gui/tests/desktop-model-menu.test.cjs (window and panel, en and zh: the button's names and title, no 「上次的选择」, the list's ticks, unticking GLM-5 posts the whole hidden list and updates button, line and summary while the list stays open, ticking Kimi K3 back, no scroll, no native select) passes in webkit, as do desktop-tiers, agent-models, agent-connect, agent-start-pick, agent-layout, agent-grip, click-scroll, agent-models-all-hidden, agent-model-groups, agent-models-loading, agent-expand-steady, agent-disconnect, agent-tweak-fold, show-unset-agent, tray-cell-click, library-desktop-skills and by-agent. go vet ./internal/..., builds (darwin, GOOS=linux -tags nogui, GOOS=windows) and go test -tags nogui -count=1 ./... under a temp HOME pass. Not tried in real Claude Desktop: that its menu shows the plain names and only the picked models after a restart is from the /v1/models it is served, not seen in the app.
2026-10-04 05:01:12 +08:00
agent: Claude Desktop's row picks which magpie models Desktop's own model menu lists, several at once, and the menu names them by the model's own name (蓝猫 on Discord: wanted to pick a few models and switch among them in Desktop, and Desktop showed "DeepSeek V4.1 Flash (opencode-go)" where they want "DeepSeek V4.1 Flash") — Desktop already read its menu from the gateway's /v1/models for claude-desktop (desktopModels over provider.CatalogFor, i.e. every visible model minus the agent's HiddenModels, each under a claudeLooking alias), but its row offered no way to pick them: the start button stood the opus tier there as 「上次的选择」, and display_name was "Name (provider/model)" with that id again in description. Now Desktop's row (MENU_FROM_LIST in app.js) puts a model button where another agent's start pick stands: it names the first two picked models (+N, or "No models"), its title says Desktop reads the list as it starts so it must be quit and reopened after a change, and it opens the existing per-agent model list (openAgentModels, the am-pop multi-select posting {hidden} to /api/agent-models/claude-desktop), which stays open while several are ticked; the button, the line under the name ("Connected · n models in Claude Desktop's model menu") and, in the tray panel, the row's summary are refilled on each save instead of the row being redrawn; the panel's opened row has the same button under the provider, the expand link says Details, and the opened row's "In Claude Desktop" says the menu lists the models picked here and that Desktop reads it as it starts. The provider switch (default vs magpie) and the tiers are as before. /api/state's model count carries the first three shown models' names (modelCountJSON.Names). desktopNames gives each model its own name and keeps a suffix only where two picked models share one, case-insensitively: the provider id, or the whole id if that still collides; the description is gone. Models added later still show by default, as for every agent's list. zh strings added (no ja). TestClaudeDesktopPickedModels (two providers' DeepSeek V4.1 Flash read "(opencode-go)"/"(relay)" while both are listed, and after relay's, glm-5 and the routing groups are hidden Desktop lists exactly the three left, the DeepSeek plain, while another agent still lists all) and TestDesktopNames are new; the effort and tier tests map Desktop's rows by DesktopCatalogID instead of the description. internal/gui/tests/desktop-model-menu.test.cjs (window and panel, en and zh: the button's names and title, no 「上次的选择」, the list's ticks, unticking GLM-5 posts the whole hidden list and updates button, line and summary while the list stays open, ticking Kimi K3 back, no scroll, no native select) passes in webkit, as do desktop-tiers, agent-models, agent-connect, agent-start-pick, agent-layout, agent-grip, click-scroll, agent-models-all-hidden, agent-model-groups, agent-models-loading, agent-expand-steady, agent-disconnect, agent-tweak-fold, show-unset-agent, tray-cell-click, library-desktop-skills and by-agent. go vet ./internal/..., builds (darwin, GOOS=linux -tags nogui, GOOS=windows) and go test -tags nogui -count=1 ./... under a temp HOME pass. Not tried in real Claude Desktop: that its menu shows the plain names and only the picked models after a restart is from the /v1/models it is served, not seen in the app.
2026-10-04 05:01:12 +08:00
agent: Claude Desktop's row picks which magpie models Desktop's own model menu lists, several at once, and the menu names them by the model's own name (蓝猫 on Discord: wanted to pick a few models and switch among them in Desktop, and Desktop showed "DeepSeek V4.1 Flash (opencode-go)" where they want "DeepSeek V4.1 Flash") — Desktop already read its menu from the gateway's /v1/models for claude-desktop (desktopModels over provider.CatalogFor, i.e. every visible model minus the agent's HiddenModels, each under a claudeLooking alias), but its row offered no way to pick them: the start button stood the opus tier there as 「上次的选择」, and display_name was "Name (provider/model)" with that id again in description. Now Desktop's row (MENU_FROM_LIST in app.js) puts a model button where another agent's start pick stands: it names the first two picked models (+N, or "No models"), its title says Desktop reads the list as it starts so it must be quit and reopened after a change, and it opens the existing per-agent model list (openAgentModels, the am-pop multi-select posting {hidden} to /api/agent-models/claude-desktop), which stays open while several are ticked; the button, the line under the name ("Connected · n models in Claude Desktop's model menu") and, in the tray panel, the row's summary are refilled on each save instead of the row being redrawn; the panel's opened row has the same button under the provider, the expand link says Details, and the opened row's "In Claude Desktop" says the menu lists the models picked here and that Desktop reads it as it starts. The provider switch (default vs magpie) and the tiers are as before. /api/state's model count carries the first three shown models' names (modelCountJSON.Names). desktopNames gives each model its own name and keeps a suffix only where two picked models share one, case-insensitively: the provider id, or the whole id if that still collides; the description is gone. Models added later still show by default, as for every agent's list. zh strings added (no ja). TestClaudeDesktopPickedModels (two providers' DeepSeek V4.1 Flash read "(opencode-go)"/"(relay)" while both are listed, and after relay's, glm-5 and the routing groups are hidden Desktop lists exactly the three left, the DeepSeek plain, while another agent still lists all) and TestDesktopNames are new; the effort and tier tests map Desktop's rows by DesktopCatalogID instead of the description. internal/gui/tests/desktop-model-menu.test.cjs (window and panel, en and zh: the button's names and title, no 「上次的选择」, the list's ticks, unticking GLM-5 posts the whole hidden list and updates button, line and summary while the list stays open, ticking Kimi K3 back, no scroll, no native select) passes in webkit, as do desktop-tiers, agent-models, agent-connect, agent-start-pick, agent-layout, agent-grip, click-scroll, agent-models-all-hidden, agent-model-groups, agent-models-loading, agent-expand-steady, agent-disconnect, agent-tweak-fold, show-unset-agent, tray-cell-click, library-desktop-skills and by-agent. go vet ./internal/..., builds (darwin, GOOS=linux -tags nogui, GOOS=windows) and go test -tags nogui -count=1 ./... under a temp HOME pass. Not tried in real Claude Desktop: that its menu shows the plain names and only the picked models after a restart is from the /v1/models it is served, not seen in the app.
2026-10-04 05:01:12 +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, provider: a request turning reasoning off stops Factory's DeepSeek V4.1 Flash, Kimi K3 and GLM-5.2 thinking (#899, tedestar) — they offered only low/high/max (GLM-5.2 high/max), so an agent's none (or Claude Code's thinking disabled) was asked for low, or nothing, and Fireworks thought anyway. droid 0.231.0 lists off for them and sends reasoning_effort "none"; through OpenRouter pinned to Fireworks, none came back with 0 reasoning tokens on DeepSeek V4.1 Flash (2 completion tokens; 19 at the default, 16 reasoning at low) and Kimi K3 (8; 71 at the default), and on Baseten's GLM-5.2 (2; 114 at the default), while GLM-5.3 and GLM-5.3-Flash turn none away ("Reasoning is mandatory for this endpoint and cannot be disabled") and stay at low, their least (Qwen3.8 Max and MiniMax have no off in droid either). The three now have none among their levels, in the built-in and in @magpie-community/opencode-factory-auth 0.1.17, which the mover's min is raised to; and a translated request that turned reasoning off (Effort reads it as low) is asked for none on a model with none among its levels, whatever the provider, else as before (a refusal still falls back to the lowest level on). No Factory account was at hand to send through Factory itself. TestFactoryThinkingOff (the built-in: chat, Responses and Anthropic's disabled; GLM-5.3 still low) and TestPluginThinkingOff (the plugin host, a model whose variants have none and one without) are new and fail without the change. go vet, go build (darwin, GOOS=linux nogui, GOOS=windows) and go test -tags nogui ./... under a temp HOME pass (TestSavedButSignedOutClaudeServed timed out once under load and passes alone).
2026-10-05 20:09:00 +08:00
gui: a model with a fast mode is sent fast, or not, from its row in the agent's picker (#954, Gyarados4157) — Fast was only a routing group member's (Group.Fast), so a GPT on a ChatGPT account picked straight in Codex or OpenCode could only go fast through a group of one. A model provider.CanFast says has a fast mode (a ChatGPT account's GPT, an OpenAI key's Responses GPT/o-series, Cursor's -fast, Claude Opus on an Anthropic key) now has a bolt beside its star in the agent's picker and in the same picker opened from a provider's row; a click posts /api/agent-fast and switches it for that agent (settings FastPicks, by agent, "<provider>/<model>", moved by a provider rename and kept by the Settings page's save) without changing the pick or closing the picker; a refusal puts it back. The field shows a bolt after a model sent fast. The gateway asks for the vendor's fast mode (service_tier "priority" / speed "fast", as for a group member) on that agent's requests for that entry, every account and key of it, not its fallbacks, another agent's requests or the same model at another provider; the Routing trace marks the try Fast. A routing group is never switched: its members say it. zh/ja/de added. fast-pick (window and 440px panel, en/zh/ja/de, Chromium and WebKit; the bolt only on models with a fast mode, the post, the refusal, the provider row's picker) passes and fails without the fix; TestFastPick (gateway, Chat/Responses/Anthropic in, fails with the gateway hunk off), TestSetFastPick, TestFastPickOptions and the settings-keep test (fails without the carry) pass with -race; i18n-keys, gui-ja, gui-de, favorite-star, agent-models-picker, claude-picker-fold and click-scroll pass; go test ./internal/gui/ ./internal/provider/ ./internal/agent/ ./internal/gateway/ ./internal/settings/ pass.
2026-10-06 11:15:52 +08:00
routing: a group can let a member go that hasn't begun answering after a set time and ask the next, and a request's detail splits its first token into magpie's time and the vendor's (Chen on X: answers through magpie felt slow, with no way to tell whether magpie or the vendor was the wait) — a group's editor has a Slow to start row (Wait, 30 s, 1 min, 2 min; Wait, the default, keeps today's behaviour). With a time set, a streamed request to a member that has sent no text, reasoning or tool call by then is cancelled before any of its bytes reach the agent (the held start is dropped; keepalives go on meanwhile) and the next member is asked; the try is marked slow ("slow to start", narrated in the route), and the member doesn't rest, since nothing is wrong with it. The last member left is always waited for. The card says "Next after 1 min without a first token". Each request now records when it went to the vendor (httptrace WroteRequest, sent_ms), and its detail shows In magpie and Vendor's first token beside First token; after a failover In magpie includes the time spent on the members let go. With the owner's real DeepSeek key in a sandbox, a group [a local member that sends its role chunk then stalls 60 s, deepseek-flash] with 3 s: the answer came from DeepSeek in 4.4 s, none of the stalled member's stream reached the client, its try read slow / "no first token in 3s" with no rest, and the ledger had ttft 3622, sent 3003. TestGroupFirstTokenFailsOver (failover, the last one waited for, one in time kept; passes -race) and group-first-token.test.cjs and usage-ledger-detail.test.cjs (WebKit, en and zh) fail on the old code. group-from-model-add's narrow-window case fails the same on origin/main; Chromium doesn't launch in this sandbox. go vet, builds (darwin, GOOS=linux nogui, GOOS=windows) and go test -tags nogui -count=1 ./... under a temp HOME pass (TestProvidersAnswerWhileListsComeIn flaked once, passes alone).
2026-10-04 19:33:55 +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
gateway: Gemini's thought signatures come back on its tool calls, through Pi and every other client (#687) — Pi on the built-in Google Gemini (an API key, gemini-3.8-flash at high) failed the turn after every tool call with 400 "Function call is missing a thought_signature in functionCall parts … default_api:bash, position 3". Gemini 3 signs the first call of each step it thinks through and wants the signature back; its OpenAI-compatible API gives it in tool_calls[].extra_content.google.thought_signature, which magpie relayed to Pi as it came, and Pi's openai-completions (like the AI SDK) keeps a call's id but drops extra_content, so the call went back unsigned. magpie's own translation (Claude Code, Codex, Gemini CLI on the same provider) never read the field at all. Now a call Gemini signed reaches the client with an id carrying the signature (<id>__ts__<signature as base64url>, letters, digits, _ and - only), which every client sends back as given: a Chat request relayed to Gemini has it taken off the call's and the result's ids and put in extra_content, and parse does the same for a translated request (the signature on the call's Part, given back by buildChat). A turn with no signature at all — another model's calls, or an id a client shortened — goes to Gemini with skip_thought_signature_validator on its calls, the value Google documents and the Code Assist path already sends; a later call of a signed step goes without, as Gemini signs the first. Any other upstream gets the calls' own ids, never the long ones. TestGeminiThoughtSignatureThroughPi (streamed and whole) and TestGeminiThoughtSignatureThroughAnthropic, against an upstream that checks signatures as Google does, fail on the old code with the issue's 400 and pass; TestGeminiCallsWithoutSignature and TestSignedID cover the fallback, other upstreams and the id's round trip. go vet, builds (darwin, GOOS=linux, GOOS=windows) and go test -tags nogui -count=1 ./internal/gateway/... under a temp HOME pass. Not tried against generativelanguage.googleapis.com: there is no Gemini API key on this machine.
2026-10-03 22:34:36 +08:00
gateway: Gemini's thought signatures come back on its tool calls, through Pi and every other client (#687) — Pi on the built-in Google Gemini (an API key, gemini-3.8-flash at high) failed the turn after every tool call with 400 "Function call is missing a thought_signature in functionCall parts … default_api:bash, position 3". Gemini 3 signs the first call of each step it thinks through and wants the signature back; its OpenAI-compatible API gives it in tool_calls[].extra_content.google.thought_signature, which magpie relayed to Pi as it came, and Pi's openai-completions (like the AI SDK) keeps a call's id but drops extra_content, so the call went back unsigned. magpie's own translation (Claude Code, Codex, Gemini CLI on the same provider) never read the field at all. Now a call Gemini signed reaches the client with an id carrying the signature (<id>__ts__<signature as base64url>, letters, digits, _ and - only), which every client sends back as given: a Chat request relayed to Gemini has it taken off the call's and the result's ids and put in extra_content, and parse does the same for a translated request (the signature on the call's Part, given back by buildChat). A turn with no signature at all — another model's calls, or an id a client shortened — goes to Gemini with skip_thought_signature_validator on its calls, the value Google documents and the Code Assist path already sends; a later call of a signed step goes without, as Gemini signs the first. Any other upstream gets the calls' own ids, never the long ones. TestGeminiThoughtSignatureThroughPi (streamed and whole) and TestGeminiThoughtSignatureThroughAnthropic, against an upstream that checks signatures as Google does, fail on the old code with the issue's 400 and pass; TestGeminiCallsWithoutSignature and TestSignedID cover the fallback, other upstreams and the id's round trip. go vet, builds (darwin, GOOS=linux, GOOS=windows) and go test -tags nogui -count=1 ./internal/gateway/... under a temp HOME pass. Not tried against generativelanguage.googleapis.com: there is no Gemini API key on this machine.
2026-10-03 22:34:36 +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
gui: a model with a fast mode is sent fast, or not, from its row in the agent's picker (#954, Gyarados4157) — Fast was only a routing group member's (Group.Fast), so a GPT on a ChatGPT account picked straight in Codex or OpenCode could only go fast through a group of one. A model provider.CanFast says has a fast mode (a ChatGPT account's GPT, an OpenAI key's Responses GPT/o-series, Cursor's -fast, Claude Opus on an Anthropic key) now has a bolt beside its star in the agent's picker and in the same picker opened from a provider's row; a click posts /api/agent-fast and switches it for that agent (settings FastPicks, by agent, "<provider>/<model>", moved by a provider rename and kept by the Settings page's save) without changing the pick or closing the picker; a refusal puts it back. The field shows a bolt after a model sent fast. The gateway asks for the vendor's fast mode (service_tier "priority" / speed "fast", as for a group member) on that agent's requests for that entry, every account and key of it, not its fallbacks, another agent's requests or the same model at another provider; the Routing trace marks the try Fast. A routing group is never switched: its members say it. zh/ja/de added. fast-pick (window and 440px panel, en/zh/ja/de, Chromium and WebKit; the bolt only on models with a fast mode, the post, the refusal, the provider row's picker) passes and fails without the fix; TestFastPick (gateway, Chat/Responses/Anthropic in, fails with the gateway hunk off), TestSetFastPick, TestFastPickOptions and the settings-keep test (fails without the carry) pass with -race; i18n-keys, gui-ja, gui-de, favorite-star, agent-models-picker, claude-picker-fold and click-scroll pass; go test ./internal/gui/ ./internal/provider/ ./internal/agent/ ./internal/gateway/ ./internal/settings/ pass.
2026-10-06 11:15:52 +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
group: a routing group's members can be patterns, so a group follows a provider's catalog (#766: magpie group add my-free-pool models='openrouter/*:free,opencode-zen/*-free') — a group names its models one by one, so a pool of a vendor's free models went stale as the vendor added and dropped them. A group now keeps patterns apart from the models it names (Group.Match): a glob, * any run of characters (/ too) over the provider/model id in any case, or re:<regexp> over the whole id. groupsIn, which every reader goes through (Groups, FindGroup, /v1/models, MemberGroups, GroupsWith, RenameGroup, rules, Off, Pick, the GUI, TUI and CLI), puts after the named models every enabled provider's model a pattern matches, in catalog order, deduplicated, the named one keeping its place; a pattern never matches a group. What was matched is derived (Group.Matched) and never stored: store() and SaveGroup keep only the named members, so a save never freezes matches. Off and rules may name a matched model. A group with only patterns is allowed; one matching nothing routes nowhere and says so: magpie group <id> lists each pattern with how many models it matches now ("matches nothing now" in amber), the group list and the TUI too, and the GUI card shows a line per pattern with its count. CLI: models= takes patterns among the ids, models+= adds them (naming a matched model moves it to where it is named), models-=<pattern> drops one, models-= of a matched model is refused naming its pattern; the help documents patterns. GUI: the editor has a Patterns box (Add pattern / Remove, validated as the server does), matched models follow those named marked "by pattern" with their off switch and no Remove, and saves send members without the matched ones plus match; zh strings added. Tests: TestGroupPatterns, TestGroupPatternMembersOffAndRules, TestGroupPatternMatchingNothing, TestPatternSyntax (provider), TestPatternGroupRoutes (gateway: a request reaches a matched model, a model a provider adds joins, Off on a matched one, /v1/models lists the group), TestGroupPatternsCLI, and group-patterns.test.cjs (WebKit, en+zh); they fail on the old code (the Go ones don't build, the GUI one finds no pattern lines). GUI: group-patterns, group-found-off, group-member-fast, group-member-off, settings-groups, agent-model-groups, remote-group-served, routing-manual pass on WebKit; group-from-model-add has the same 2 failures (600x420) as origin/main. A request to a group whose patterns match nothing now is a 404 naming the group and its patterns (emptyGroupError), not 'magpie knows no model' and every model; TestEmptyPatternGroupSaysSo. With the owner's real config under a sandbox HOME: group/ds-pool of models='deepseek/*' listed deepseek-flash and deepseek-v4-pro by pattern, stored only the pattern, was in /v1/models and answered from deepseek-flash; none-pool of 'nobody/*' said its pattern matches nothing. go vet, builds (darwin, GOOS=linux, GOOS=windows) and go test -tags nogui -count=1 ./... under a temp HOME pass.
2026-10-04 16:52:15 +08:00
group: a routing group's members can be patterns, so a group follows a provider's catalog (#766: magpie group add my-free-pool models='openrouter/*:free,opencode-zen/*-free') — a group names its models one by one, so a pool of a vendor's free models went stale as the vendor added and dropped them. A group now keeps patterns apart from the models it names (Group.Match): a glob, * any run of characters (/ too) over the provider/model id in any case, or re:<regexp> over the whole id. groupsIn, which every reader goes through (Groups, FindGroup, /v1/models, MemberGroups, GroupsWith, RenameGroup, rules, Off, Pick, the GUI, TUI and CLI), puts after the named models every enabled provider's model a pattern matches, in catalog order, deduplicated, the named one keeping its place; a pattern never matches a group. What was matched is derived (Group.Matched) and never stored: store() and SaveGroup keep only the named members, so a save never freezes matches. Off and rules may name a matched model. A group with only patterns is allowed; one matching nothing routes nowhere and says so: magpie group <id> lists each pattern with how many models it matches now ("matches nothing now" in amber), the group list and the TUI too, and the GUI card shows a line per pattern with its count. CLI: models= takes patterns among the ids, models+= adds them (naming a matched model moves it to where it is named), models-=<pattern> drops one, models-= of a matched model is refused naming its pattern; the help documents patterns. GUI: the editor has a Patterns box (Add pattern / Remove, validated as the server does), matched models follow those named marked "by pattern" with their off switch and no Remove, and saves send members without the matched ones plus match; zh strings added. Tests: TestGroupPatterns, TestGroupPatternMembersOffAndRules, TestGroupPatternMatchingNothing, TestPatternSyntax (provider), TestPatternGroupRoutes (gateway: a request reaches a matched model, a model a provider adds joins, Off on a matched one, /v1/models lists the group), TestGroupPatternsCLI, and group-patterns.test.cjs (WebKit, en+zh); they fail on the old code (the Go ones don't build, the GUI one finds no pattern lines). GUI: group-patterns, group-found-off, group-member-fast, group-member-off, settings-groups, agent-model-groups, remote-group-served, routing-manual pass on WebKit; group-from-model-add has the same 2 failures (600x420) as origin/main. A request to a group whose patterns match nothing now is a 404 naming the group and its patterns (emptyGroupError), not 'magpie knows no model' and every model; TestEmptyPatternGroupSaysSo. With the owner's real config under a sandbox HOME: group/ds-pool of models='deepseek/*' listed deepseek-flash and deepseek-v4-pro by pattern, stored only the pattern, was in /v1/models and answered from deepseek-flash; none-pool of 'nobody/*' said its pattern matches nothing. go vet, builds (darwin, GOOS=linux, GOOS=windows) and go test -tags nogui -count=1 ./... under a temp HOME pass.
2026-10-04 16:52:15 +08:00
2026-10-05 16:46:59 +08:00
gateway: a lone provider's Claude or GPT reasoning streams as it comes, not held for a refusal no one else could answer (iTianbao on X: 请加一个开关,让思考实时透传; through a relay Claude Code showed the thinking only all at once with the text, Codex none until then) — holdWriter held Claude's, GPT's and Gemini's reasoning (refusesAfterThinking, up to holdThinking) on every candidate, so a refusal after it could go to the next one (#248), but a refusal on the last candidate is never asked again (the !last checks in serve), so there it only delayed the thinking. On the last candidate Claude's and GPT's reasoning is now shown at once (thinkingShown), and a refusal after it reaches the agent as the vendor's own stop; Gemini's stays held there, for a reply that only reasoned to be asked again (#667, TestGeminiEmptyRepliesAskedAgain). A group with members after it still holds as before. Before: on a slow fake relay (4 thoughts 300ms apart, then text) every relay shape (Messages, Chat with reasoning_content and reasoning, Responses) for claude-sonnet-4-5 and gpt-5.1 gave Claude Code's thinking_delta and Codex's reasoning_summary_text.delta all at ~1.2–1.5s with the text; after, 0/300/600/900ms. TestThinkingStreamsWithNoOneElse (Messages, Chat for Claude and GPT, Responses; each asked from /v1/messages and /v1/responses) fails without the change in every case and passes; TestHeldThinkingKeepsTheAgentAlive's nobody-left case now expects the thinking and Claude's refusal rather than a held error. -race -count=5 on TestThinking*, TestRefusal*, TestHeld*, TestGeminiEmpty*; go test -tags nogui ./internal/gateway, go vet, gofmt -l, linux and windows (nogui) builds pass. Not tried with the reporter's relay: whether it sends GPT's reasoning at all (OpenAI's Chat API doesn't) is unknown, and no switch was added. docs/subsystems/gateway-routing.md says it.
2026-10-06 19:34:24 +08:00
usage, gateway: a model the vendor spells with other separators isn't marked as another one (vincentzhang on Discord: 这个说明有点奇怪。看不明白。) — Volcengine Ark answers deepseek-v4.1-flash as deepseek-v4-1-flash, the dots written as hyphens, and the Usage page's Requests and the Routing page marked every such row amber 「实际 deepseek-v4-1-flash」, saying another model had answered, not the one asked for under a dated name. Swapped now compares the bare names without their separators (. - _ and spaces) and case, so deepseek-v4-1-flash, deepseek_v4_1_flash, GLM-4-6 for glm-4.6 and gpt-4-1-2025-04-14 for gpt-4.1 are the model asked for, while deepseek-v4-flash or deepseek-v4.1-pro for deepseek-v4.1-flash still are another. Rows and routes kept marked swapped before are read back as not swapped when the names differ only so (usage.SameSpelled, in rowChunk.row and Route.routedAgain). The tag's explanation says it in plainer words: asked for {sent}, the reply says {served} answered, likely another model; the same model under a dated name or spelled otherwise isn't marked (zh, ja, de). TestSwappedIgnoresSeparators fails on the old code; TestKeptRowSameSpelledNotSwapped, TestRouteKeptSpelledOtherwiseNotSwapped. usage-ledger (its expected title updated), remote-group-served, panel-routing, gui-ja and gui-de pass on WebKit. go vet, builds (darwin, GOOS=linux nogui, GOOS=windows) and go test -tags nogui -count=1 ./... under a temp HOME pass (TestCodexVersion timed out once under load and passes alone).
2026-10-05 12:49:59 +08:00
group: a routing group takes images when one of its members sees them, and an image goes to the members that do (#756, HuHong-Tao on v0.1.813: group/auto-deepseek-v4-1-flash turned 1198 image requests away with 400 "model does not support image input" in a day, where commandcode's deepseek-v4.1-flash of the group took them) — groupEntries said a group took images only when every member did (Images ANDed, ImageInput through sharedImageInput, where one explicit text-only answer wins), and the gateway's membersImageInput said the same of a request, so one text-only member turned away every image before any member was tried, and /v1/models gave the group input ["text"]. Since a rule for images can send one past a text-only member, the gateway already dropped text-only members for a request with a new image; it now does so for every group without a model to describe images: a new image goes to the members that see it (an explicit answer, or models.dev's estimate as blindTo reads it for a model of its own), to members magpie knows nothing of when none is known to see, and is turned away (400) only when every member is text-only. With a describer the members keep their order, a text-only one given the image described, as before. groupEntries ORs Images and takes ImageInput through anyImageInput (true when a member's is, false when all are, else unknown); membersImageInput drops the rule's members (the rule's member first changes nothing: one that can't see is passed over). With the owner's real accounts (a copy of the config, Vision off): an image to group/auto-deepseek-v4-1-flash was 400 on origin/main and is answered by the Devin account's deepseek-v4-1-flash ("White"); group/auto-kimi-k3, whose only member that sees is an OpenCode Go key without a subscription, went from 400 to that member's 403. TestGroupImages (a group with one member that sees takes images, all text-only doesn't), TestGroupImagesFromAMemberThatSees, TestKnownTextOnlyModelRejectsImagesBeforeUpstream (an ordered group with its text-only member first sends the image to the one that sees), TestRuleImages and TestRuleImagesUnlistedMember now expect the image answered by the member that sees; they fail on the old code. go vet, builds (darwin, GOOS=linux, GOOS=windows) and go test -tags nogui -count=1 ./... under a temp HOME pass.
2026-10-04 15:01:11 +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 gateway key can be held to some accounts (#905, LCorleone) — a key could burn any account a provider had: two coding plans of one vendor, and a key given to someone had them both. A key now keeps an optional list of accounts and keys (caller-keys.json "accounts", none = every account, so older files read as before): "<provider>/<account>", an account by its stable id — the login's, set once it is written and kept through renames (logins.json's savedLogin id, written by every write and stood in for by one made of the name until then, so an entry made before stands), a key by its fingerprint — every one a provider has when the list is set, and one gone later (a key rotated, an account signed out) kept as it is, matching nothing, holding the key closer, never wider. The command line and the menu name an account by who is signed in and keep its id. Routing holds a plan to the key's accounts beside its models: a candidate whose account the key may not use is left out — through a routing group the key names no less, for naming a group is not naming its accounts — the plan's order keeping step and its left-out list marking what the key held out (Weighed's held); magpie's own calls for the key — a web search, a picture described, a Codex title — are of the models the user picked, so the models don't hold them, but their spend lands on an account all the same, and the accounts hold them too. When every account or key a model has is one the key may not use, the request is refused with a 403 in the API's own error shape naming the model and the accounts the key may use, logged as a rejected call: after a usage cap's 429 and before an account barred for the model's, itself before the no-member 404. /v1/models and the Anthropic and Gemini lists show the key only the models an account or key it may use serves (a provider's account set not to serve one, #474, is none of the key's); a provider with no account and no key (a local ollama) is one the key names no account of — its models stay listed and its asks pass — while an entry kept after its provider lost its last account or key serves none, holding the key closer. The embeddings, images and videos endpoints hold their candidate to the key's accounts the same way, and counting tokens counts on those alone (the local estimate where it may use none). System One asks its decision model of an account or key the key may use — and sends on that key, of those in use, not the provider's first, which a key held to a later one was spent on; the videos and images keyed paths send on one the key may use too, held to a later key no longer refused for the first alone. Set it in the app's menu the models are picked in — the accounts after the models, an account by who is signed in with their plan in the note, a key by its fingerprint, what the CLI kept that no provider has now kept as "Not signed in now" — or magpie gateway-key accounts <id> codex/me@example.com openai/<key id> (all takes it off); gateway-key list has an ACCOUNTS column. en/zh/ja/de, the German "{n} accounts" main already had left to it rather than written twice. The images API's fan-out over a subscription's accounts is held to the key's accounts as one: one of the key's whose plan refuses to draw no longer hands the pen to an account it may not use, and the second in the order, when it is the key's, draws what the first may not (drawOnAccounts filters the candidates, the account pinned to with the account header told plainly when the key may not use it). The Routing view's left-out list says an account the key held out for what it is, not that its plan lacks the model, in Chinese, Japanese and German too; the key's badge names its accounts by who is signed in, not by the stable ids it keeps. (#932)
2026-10-06 10:51:50 +08:00
gateway tests: TestRestingKeyReadAgainInAnOrderedGroup has the relay hold a reading made after the key refused until the test has raised its limit (red on ubuntu CI -race at d7bdb076: "its limit raised: still rests by window till …, its windows read 3 times") — the third order 3e38bd76 didn't cover. The planning's reading, still out as the key refused, came back between the refusal and the test raising the limit; ee2588c2's re-read that follows it at once reached the relay first too, read the week still used up, and was trusted for its minute, so the planning after the raise didn't ask again and nothing lifted the rest within the test's 5 s. That is the app as designed (a key's windows are read at most once a minute; a reading made after the refusal is a true one), so the test was wrong: it assumed no reading could land between restAfter and the raise. It now models the owner raising the limit before magpie reads again: a reading that reaches the relay after the refusal waits until the limit is raised. Red before: with the first reading held 100 ms and 300 ms between the refusal and the raise (a preempted test goroutine), the old test fails 3 of 5 with CI's message ("… its windows read 3 times"); the new one passes 10 of 10, and 10 of 10 with each delay alone. Still guards ee2588c2: with the planning's reading held until after the refusal and the re-read taken out of readKeyAllowance, the new test fails 3 of 3 ("still rests by window …, read 2 times"), and passes 3 of 3 with it. go test -race -count=40 -cpu 1,2,4 -run 'TestRestingKey|TestKey|Lone|Allowance' ./internal/gateway at load average ~124, go test -race ./internal/provider, go test -race ./internal/gateway, go vet (darwin, GOOS=linux, GOOS=windows) and gofmt -l pass.
2026-10-08 18:54:59 +08:00
gateway: a key held to some accounts may not compact on the sign-in Codex is on either, the last path #967 left open (a follow-up to #967, on the maintainer's invitation, LCorleone) — /responses/compact for one of Codex's own models relays as it came, deliberately (#876): the encrypted history it carries is readable only by the ChatGPT backend that sealed it, so native compaction keeps its own sign-in. But that relay spent the account Codex is signed in to with nothing of the calling key asked, so a key held to some accounts (#905) could spend an account it may not use through compaction alone, after #967 held its turns. compactSigninHeld asks, before the relay, whether the sign-in (resolved as codex/<model>, the id the catalog gives Codex's own) is one of the key's accounts — and fails closed, as the review asked: a Codex switched off in magpie, or a sign-in nowhere resolvable, is refused as an account the key may not use is, the relay reaching the sign-in all the same; the one allowance is Codex signed in with its own API key, told from auth.json's auth_mode (provider.CodexAPIKeySignedIn — the request's own Authorization can't tell it, magpie's token already in it for a keyed request). A refusal carries the API's own 403 shape naming the model and the accounts the key may use, and the two ways out: add the signed-in account to the key, or use one of magpie's models, whose compaction goes through a compaction_trigger on /responses and the key's accounts — native compaction has no other route, its history bound to that account. The account in use compacts on it as it always did, the relay untouched; a key that names no Codex account (AllowsAccount's rule), one held to some models alone, and a free key are relayed as they were. TestGatewayKeyAccountsHoldCodexCompact (held to a spare account: refused with nothing asked of OpenAI; held to the sign-in: the compaction relays as it came; naming no Codex account: allowed as ever; a free key: unchanged; and the provider switched off: refused with nothing upstream, the review's repro) fails without the gate and passes, TestGatewayKeyCompactOnCodexAPIKey pins the allowance; go test ./internal/gateway/ ./internal/provider/, go vet, linux(nogui)/windows(nogui) builds pass. (#976)
2026-10-06 16:38:16 +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
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
gateway, provider: the Routing page says what a lone key or account has left, and a key bought as a plan (Kimi Code, GLM Coding Plan, MiniMax, OpenCode Go, Command Code) is weighed by its plan's windows (#1016, gphysign: Kimi Code (China) with one key on, Smart routing; the card said only "Kimi Code (China) 只启用了一个 Key,无需选择。" while /v1/magpie/quotas had its 5 hours 49 used, 51 left and its 7 days 45 used) — two gaps. weighRouted returned before reading anyone's allowance when there was one candidate, so a lone key or a lone subscription account was never known in the trace, and the Routing page, which shows a bar and "{n} used · renews in {d}" for a known row, had nothing to show; every single-account subscription said "what's left not known yet" the same way. And KeyAllowance read only a sub2api key's limits, so a key whose plan the Usage page reads (planQuotaSourceOf: Kimi Code, GLM Coding Plan and Z.ai, MiniMax Coding Plan, OpenCode Go, Command Code) was never known to routing, with one key or several. weighRouted now reads what each candidate has left before returning a lone one (one alone is still weighed against nobody). readsKeyWindows takes a plan key too; keyWindows reads it through planKeyWindows (planWindows, with Zhipu's team quota behind a key that has none of its own, as PlanQuotas does), and PlanQuotas' own read of a card is noted for routing at once (noteKeyAllowance), so the Usage page and routing never ask twice; the card kept on disk seeds it after a restart (keyCardTag keys a plan card as plan:, a sub2api one as balance:). A key at such a vendor that turns out to be pay-as-you-go, with no windows, is asked again every 30 minutes (keyNoWindowsAge), not every minute it is used. So, beyond the issue: several plan keys on one provider are now weighed by their windows under Smart, usage and pace, and a plan key the vendor refuses as out of quota rests until its window renews and comes back once read short of full, as a sub2api key does. affine no longer says a lone key or account all but used up "moved" its conversation: there is nobody to move to (at > 0). The GUI needed no change. TestLoneOneTellsWhatItHasLeft (a lone Kimi key known at 49% with both renews, the week first; a lone Codex account known; a plain DeepSeek key not), TestLoneSpentKeyKeepsItsConversation (fails with the at > 0 guard taken out), TestPlanKeyAllowance (the windows read behind the request, asked once; DeepSeek and Moonshot keys never asked), TestPlanKeyAllowanceFromItsCard (PlanQuotas' read known at once without a second ask; after a restart with the vendor down, the card on disk stands in and a failed read keeps it) and TestPayAsYouGoKeyAskedSeldom (a GLM pay-as-you-go key asked once, not two minutes on, again 31 minutes on) all fail without the change and pass -race -count=20 (provider) / -count=10 (gateway). routing-key-windows.test.cjs gains a lone Kimi Code key at 1100px and 440px (the one-key story, the row's bar at 49%, its provider row "49% used · renews in …" fitting, no sideways scroll) and passes in full on Chromium and WebKit in en, zh, ja and de; routing-quota-left and panel-routing pass. go test -tags nogui ./internal/..., go test -race on ./internal/provider and ./internal/gateway in full, go vet, gofmt -l, darwin, linux and windows (nogui) builds pass. Not tried with a real Kimi Code key. StepFun's Step Plan is read through a sign-in, not the key, so it isn't one of these. docs/subsystems/gateway-routing.md, providers-accounts.md and docs/reference.md say it.
2026-10-06 20:25:04 +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 reply says which provider and model answered it, in X-Magpie-Provider and X-Magpie-Model, and Settings › Models › Replies can have its model say magpie's provider/model id too (liuweifeng, #822) — a reply's model was only the vendor's own name (glm-5.3-flash), naming neither the provider nor the group's member, so pi, which counts usage by provider/responseModel, put every member's calls in one bucket. Every reply now carries X-Magpie-Provider (the provider id) and X-Magpie-Model (provider/model of the member that answered, after a failover the one that did), relayed or translated, streamed or not; the body is left as the vendor wrote it by default, since agents read it themselves (Claude Code its features, Codex its catalog). Name the member in replies (memberModel, off by default; zh, ja, de strings), or a request's X-Magpie-Response-Model: member, has the body's model say the member's id as well: Chat's model, Responses' response.model, Anthropic's message.model / model, Gemini's modelVersion, in each event of a stream as it goes and in a JSON body once whole (memberWriter, between the masking and the request log, which keeps the vendor's). Claude Code, Claude Desktop and Codex always get the vendor's name; X-Magpie-Response-Model: vendor asks for it too. The vendor's own name (dated versions included) stays in the usage ledger's Served. With the owner's real DeepSeek key in a sandbox, a group [a dead member, deepseek/deepseek-flash]: the headers said deepseek and deepseek/deepseek-flash on Chat, Messages and Responses, streamed and not; with member asked the body said deepseek/deepseek-flash in all six, with the setting on pi's token got it and Codex's token and a claude-cli User-Agent got deepseek-flash. TestReplyNamesMember (6 paths × streamed or not × default, on, asked, vendor asked, Codex, Claude Code, each after a failover; passes -race) fails on the old code, TestMemberLine; member-model-setting.test.cjs (WebKit, en and zh). settings-sections, settings-groups and searcher-pick pass on WebKit; redact-rule-row fails 2 the same on origin/main. go vet, builds (darwin, GOOS=linux nogui, GOOS=windows) and go test -tags nogui -count=1 ./... under a temp HOME pass.
2026-10-05 02:27:03 +08:00
gateway, provider: a Claude model is asked on Messages from Chat and Responses too, where its provider has an Anthropic URL (ReturnTrue on Discord, after #997/050e629b: OpenCode on supergpt@claude-opus-5-5 had no cache with Chat, Responses and Anthropic URLs filled; Chat → Anthropic with Anthropic alone wrote 16,318 to the cache) — OpenCode speaks Chat to magpie for every model, Claude's too, and a request on an API the provider serves was relayed as it is, so with a Chat URL beside the Anthropic one OpenCode's Chat went on as Chat and the cache breakpoints the translation to Messages adds never reached the relay; Codex's Responses at a relay with a Responses URL the same. MessagesFirst only ordered the APIs a request is translated to. Provider.OnMessages (MessagesFirst, no "Asked on" set for the model, and no vendor list naming its APIs) now stops the pass-through for Chat and Responses when Messages is usable, and the request is translated there. A model the user pinned to Chat stays on Chat; Copilot's Claude, whose list names Chat and Messages, and every non-Claude model are relayed as before; Messages is relayed as it is. The reporter's second setup (Responses and Anthropic URLs, Chat → Responses in the screenshot) already went to Messages on v0.1.1081 — Chat isn't served there and 050e629b put Messages first — so that screenshot was from before it. TestClaudeFromChatIsAskedOnMessages (the reporter's three setups plus Chat+Anthropic, OpenCode's streamed Chat with a system prompt and a tool and Codex's Responses, each asserting /v1/messages upstream with cache_control in the body) fails without the change on the Chat+Responses+Anthropic and Chat+Anthropic setups and passes; TestClaudePinnedToChatStaysOnChat guards the pin; TestClaudeGoesOnMessagesWhereTheRelayHasThem and TestAutoRelaysOnTheClientsOwnAPI now expect Messages for Claude (GLM and GPT unchanged). -race -count=5 on them and the Copilot tests. go test ./internal/gateway ./internal/provider, go vet (darwin, windows), gofmt -l, darwin, linux (nogui) and windows builds pass. Not tried with the real SuperGPT relay or a real OpenCode. OpenCode's config still names @ai-sdk/openai-compatible for every model; a per-model Anthropic SDK was not added, as it isn't tried with the real OpenCode and the gateway now does the same. docs/subsystems/gateway-routing.md says it.
2026-10-06 16:42:07 +08:00
plugin, gateway, gui: a plugin can be gateway middleware — onRequest, onEvent and onResponse run in magpie's gateway on what agents send and get back (moejs) — a plugin could only be a subscription provider, run in the Bun host, a process away from the gateway. A plugin may now declare middleware (package.json magpie.middleware, or a *.middleware.js / .mjs file), run in-process by moejs (pure-Go JS engine): compiled once, one runtime per request from a pool, so every hook of a request shares ctx.state. onRequest(body, ctx) sees the agent's body in its own API before routing; what it returns is the body the next middleware and the gateway get, undefined leaves the bytes as they were, ctx.reject(status, msg) answers with an error in the agent's API's shape. onEvent(ev, ctx) sees each SSE event's JSON (export const events = [...] filters by name; an object changes it, null drops it, undefined passes the bytes through untouched; data: [DONE] is no JSON and passes by). onResponse(body, ctx) sees a whole JSON reply, with ctx.status. ctx has protocol, model, stream, path, agent, the plugin's options and state. A hook that throws, returns a non-object or runs past its limit (250ms request/response, 50ms an event, 1s load; interrupted) leaves the request or reply as it was and is counted and logged (60 lines a minute); a compressed reply passes as-is. Files are reloaded within a second of a change. Only agents' requests run through it (serveAgent at the gateway's agent entry points and the Codex backend), never magpie's own titles, classifier or stand-ins; it sees bodies before redaction. A middleware-only package is skipped by the Bun host. The Plugins page marks a middleware with a chip, its hooks, calls and µs per call (JSON in and out counted), failures in red with the last error, and a load error. en/zh/ja/de; docs/reference.md and usemagpie.ai/docs/plugins#middleware (en/zh/ja) with an alias example. Tested with a real DeepSeek account through magpie web --gateway in a sandbox HOME: an alias middleware served "fast" and named it in message_start, a reject came back as 403 permission_error, a Chat stream uppercased; real Claude Code (sandbox CLAUDE_CONFIG_DIR) did Read, Bash and Glob tool turns through three middlewares with 0 failures: ≈1–1.6µs an event, 0.2–0.7ms an onRequest that parses and rewrites Claude Code's ≈80KB body. Real use found two bugs fixed here: [DONE] parsed as JSON (SyntaxError on every Chat stream) and stats leaving out the JSON time. internal/middleware tests (chain, undefined keeps bytes, reject, fail-open on throw/spin/string with the runtime reused, async hook, load errors, off and Bun skip, reload, split/CRLF/filtered/dropped stream events, [DONE] untouched, whole reply and gzip, 64 in parallel; BenchmarkEvent ≈0.9µs, 11 allocs) and TestMiddlewareThroughGateway (alias routed, events by protocol, redaction still upstream, reject in Anthropic's shape; fails without the change) are new; plugin-middleware.test.cjs (Chromium and WebKit, en and zh: chip, hooks and stats, failures with last error, broken and off, no left border) fails without it. The other plugin GUI tests and click-scroll pass in WebKit. go vet, go build (darwin, GOOS=linux nogui, GOOS=windows) and go test -tags nogui ./... under a temp HOME pass (internal/provider failed once under load and passes alone).
2026-10-05 19:14:31 +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
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
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: an upstream that answers 200 with a web page, with nothing at all, or with JSON that isn't, has failed as a 502 does, so a group asks its next member (#1012, cth123456: Codex on group/medium, agentrouter/deepseek-v4-flash now and then answered 200 text/html; the log said status=200 done=true in=0 out=0 served=null, codex/gpt-6-luna was never asked, and Codex failed with "stream disconnected before completion: stream closed before response.completed") — the candidate loop judged a try by its status alone, and holdWriter passes a 200 that is neither text/event-stream nor application/json straight to the agent, so the page went to Codex as the reply. notAnAPIReply now reads every 2xx (but 204) an upstream gives, in forwardOnce (every relayed and translated request, every API, plugins included) and in the built-in clients' own requests (the ChatGPT backend's /responses, Kiro, Zed, Qoder, Devin, Cursor, Command Code): a text/html reply, a body that begins as a web page whatever its Content-Type, an empty body, or one sent as JSON that doesn't begin with { or [ is turned into the 502 it stands for, with a JSON error saying what came back ("answered 200 OK with a web page (text/html), not an API reply: “Just a moment...”", the firewall hint for a block page). It then fails over, rests a minute and is logged as any 502 before the reply begins does; a provider with nobody after it hands the agent that 502 rather than the page. Only the first bytes are peeked, which a stream waits for anyway; a read that fails is left to the reader, a compressed body and anything else as it came. TestNotAnAPIReplyFailsOver (Chat, Responses and Messages, streamed and not, a member answering a Cloudflare page as text/html, as JSON and as SSE, an empty body as JSON and as SSE, and plain text as JSON; the next member must answer and the log name why) fails without the change in all 36 cases (the page relayed to the agent, or b never asked, or no reason logged) and passes; TestNotAnAPIReplyAlone (a 502 with the reason, logged 502) and TestCodexBackendWebPage (the ChatGPT backend's page reached Codex as a 200) fail without it and pass; TestAPIReplyUntouched keeps JSON after whitespace, SSE with and without a Content-Type, gzip, XML, a 204 and a 404 as they came. -race -count=10 on them; go test -tags nogui ./internal/gateway ./internal/provider, go vet (darwin, windows), gofmt -l, linux and windows (nogui) builds pass. Not tried with the reporter's relay (agentrouter) itself; the page is a Cloudflare challenge's, as the issue has only its Content-Type. docs/subsystems/gateway-routing.md says it.
2026-10-06 18:26:31 +08:00
gateway: an upstream that answers 200 with a web page, with nothing at all, or with JSON that isn't, has failed as a 502 does, so a group asks its next member (#1012, cth123456: Codex on group/medium, agentrouter/deepseek-v4-flash now and then answered 200 text/html; the log said status=200 done=true in=0 out=0 served=null, codex/gpt-6-luna was never asked, and Codex failed with "stream disconnected before completion: stream closed before response.completed") — the candidate loop judged a try by its status alone, and holdWriter passes a 200 that is neither text/event-stream nor application/json straight to the agent, so the page went to Codex as the reply. notAnAPIReply now reads every 2xx (but 204) an upstream gives, in forwardOnce (every relayed and translated request, every API, plugins included) and in the built-in clients' own requests (the ChatGPT backend's /responses, Kiro, Zed, Qoder, Devin, Cursor, Command Code): a text/html reply, a body that begins as a web page whatever its Content-Type, an empty body, or one sent as JSON that doesn't begin with { or [ is turned into the 502 it stands for, with a JSON error saying what came back ("answered 200 OK with a web page (text/html), not an API reply: “Just a moment...”", the firewall hint for a block page). It then fails over, rests a minute and is logged as any 502 before the reply begins does; a provider with nobody after it hands the agent that 502 rather than the page. Only the first bytes are peeked, which a stream waits for anyway; a read that fails is left to the reader, a compressed body and anything else as it came. TestNotAnAPIReplyFailsOver (Chat, Responses and Messages, streamed and not, a member answering a Cloudflare page as text/html, as JSON and as SSE, an empty body as JSON and as SSE, and plain text as JSON; the next member must answer and the log name why) fails without the change in all 36 cases (the page relayed to the agent, or b never asked, or no reason logged) and passes; TestNotAnAPIReplyAlone (a 502 with the reason, logged 502) and TestCodexBackendWebPage (the ChatGPT backend's page reached Codex as a 200) fail without it and pass; TestAPIReplyUntouched keeps JSON after whitespace, SSE with and without a Content-Type, gzip, XML, a 204 and a 404 as they came. -race -count=10 on them; go test -tags nogui ./internal/gateway ./internal/provider, go vet (darwin, windows), gofmt -l, linux and windows (nogui) builds pass. Not tried with the reporter's relay (agentrouter) itself; the page is a Cloudflare challenge's, as the issue has only its Content-Type. docs/subsystems/gateway-routing.md says it.
2026-10-06 18:26:31 +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
gateway: a conversation too long for one member of a group goes on to a member with room for it, as before (#700: ynky007 — Kimi Code on DeepSeek V4.1 Flash through magpie showed a 176k context after updating to 0.1.768) — magpie still writes max_context_size = 1048576 for it into ~/.kimi-code/config.toml (checked in a temp HOME with the models.dev cache). 176k is Kimi Code's own: on a context overflow it takes floor(0.85 × the request's tokens) as the model's window for the session (observeContextOverflow, OVERFLOW_CONTEXT_SAFETY_RATIO, 2.1.1), so it was told a request of about 207k overflowed. Since 11a09d41 an overflow (a 400/413/422 saying so, which tooLongRe now knows in many more words) is not retryable and goes to the agent at once, and b3ab3e42 tells some unexplained failures as one; before, a 400 with "exceeded" failed over to the next member, which answered. That is right for other accounts of the same model, not for a group member of another model or vendor, whose window may be larger. Now an overflow goes on to the members left that are not the same provider and model and not known (windowOf) to be smaller than the request; nobody rests (Fail "overflow", shown as "too long for its model"), and if they fail on quota the agent is still told the overflow, to compact. With none left the agent is told at once, as now. TestOverflowGoesToAMemberWithRoom fails on the old code (400 context_length_exceeded, the second member never asked) and passes; it also checks a member known too small and a second key of the same model are not asked. go vet, builds (darwin, GOOS=linux, GOOS=windows) and go test -tags nogui -count=1 ./... under a temp HOME pass. Not tried with the reporter's providers: which member refused at ~207k is not known.
2026-10-03 23:44:06 +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
2026-10-05 12:59:32 +08:00
gateway, provider: a request turning reasoning off stops Factory's DeepSeek V4.1 Flash, Kimi K3 and GLM-5.2 thinking (#899, tedestar) — they offered only low/high/max (GLM-5.2 high/max), so an agent's none (or Claude Code's thinking disabled) was asked for low, or nothing, and Fireworks thought anyway. droid 0.231.0 lists off for them and sends reasoning_effort "none"; through OpenRouter pinned to Fireworks, none came back with 0 reasoning tokens on DeepSeek V4.1 Flash (2 completion tokens; 19 at the default, 16 reasoning at low) and Kimi K3 (8; 71 at the default), and on Baseten's GLM-5.2 (2; 114 at the default), while GLM-5.3 and GLM-5.3-Flash turn none away ("Reasoning is mandatory for this endpoint and cannot be disabled") and stay at low, their least (Qwen3.8 Max and MiniMax have no off in droid either). The three now have none among their levels, in the built-in and in @magpie-community/opencode-factory-auth 0.1.17, which the mover's min is raised to; and a translated request that turned reasoning off (Effort reads it as low) is asked for none on a model with none among its levels, whatever the provider, else as before (a refusal still falls back to the lowest level on). No Factory account was at hand to send through Factory itself. TestFactoryThinkingOff (the built-in: chat, Responses and Anthropic's disabled; GLM-5.3 still low) and TestPluginThinkingOff (the plugin host, a model whose variants have none and one without) are new and fail without the change. go vet, go build (darwin, GOOS=linux nogui, GOOS=windows) and go test -tags nogui ./... under a temp HOME pass (TestSavedButSignedOutClaudeServed timed out once under load and passes alone).
2026-10-05 20:09:00 +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
gateway: a rate limit that comes back as soon as its rest is over rests longer each time, until the account answers (01huadalang on Discord: WorkBuddy 429s a free model while the account has credits, and magpie asks it again every minute) — a 429 the vendor gave no time for rested a minute, every time: an account refused again the moment it was back was asked once a minute forever, a request lost each time before the group's next model took it. The failover is magpie's (restAfter), so this is the same for the built-in and for the plugin, which passes WorkBuddy's 429 and its headers on as they are. Now rateRest counts the rate limits that come as soon as a rest ends (within half an hour of it) and rests 1, 2, 4, 8, 16 then 30 minutes; an answer starts it over (clearRest drops the note it counts by), one long after the last rest a minute again, and one while still resting — the last one left, tried anyway, or one request's own retries — keeps the count where it is. What the vendor says still counts: Retry-After or a reset header, or a reset time in the error as keepRetry notes it, when longer than the backoff. A rest is by the account (restKey) as before, by the model for OpenRouter's shared-pool free models. A usage that says the account has credits never put a resting account first (restLast orders after weighing); the test holds that. The Routing page says "rate limited again as soon as it was back (n times in a row)" for such a rest. TestRateLimitBacksOffWhenItComesBack fails on the old code (no count, a minute every time). go vet, builds (darwin, GOOS=linux, GOOS=windows) and go test -tags nogui -count=1 ./... under a temp HOME pass but TestUserPath, TestSavedButSignedOutClaudeServed and one internal/gui test, which pass rerun. Not tried against WorkBuddy itself: its 429's body and headers weren't seen.
2026-10-03 22:34:36 +08:00
provider: a Codex reset about to run out is spent half an hour before it does, not three hours, and at once only when the account is held up past then (#718, thedavidweng: account A, held up for its five hours until 17:09 with 32% of its week used, spent a reset expiring that afternoon at 15:20 — within resetExpiryLead (3h) and its windows used — losing the 68% of its week it could have used between 17:09 and the reset running out). Spending a reset starts the windows again and loses what they have left at that moment, while the fresh windows are the same whenever it is spent, so check now spends it as late as is safe: resetExpiryLead is 30 minutes; an account holding a reset that runs out is looked at every 30 minutes (resetExpiryWatch), never past the start of the reset's last hour, and every 5 minutes within it (resetExpiryClose, nextResetLook), a failed read (by the reset last read, kept per account) or spend there tried again in 5, not 30; the loop goes over the accounts every 5 minutes (each read only when due), and check compares wall-clock times (Round(0)), so a Mac asleep past the lead spends it on the first pass awake. It is spent at once when the account is held up now (usedUp: a window that stops it at 100%) and won't be free again (BackAt; not known counts as not) before the reset would be spent: waiting can use nothing more, and spending it frees the account — any held reset that runs out, however far off. Windows not used still spend nothing; the week-used-up AutoUseCodexReset path is as before. expiringResetSpent takes whether the account is stopped and when it is back, and routing's Allowance.Restarts passes the same from its limits (heldUp), so Weekly pace's and Smart's deadline (dueRestart) is when check spends it. The GUI's tooltips (en, zh: 到期前约半小时, 若账号正被限流、要到那之后才解封,则立即用掉), quota auto-reset's help and settings' comment say so. TestResetSpentAsLateAsSafe walks the report's afternoon in 5-minute passes (reset running out 18:15: five hours free at 17:09 or 17:40 → spent 17:45; free at 17:50, after it runs out, not saying when, or the week used up past it → spent 15:20; not held up → 17:45; nothing used → never; at most 20 reads), TestResetExpiryLastHour (look cadence, a failed read and spend retried in 5 minutes, spent after sleeping past the lead) and TestRestartsWhenHeldUp fail on the old code; the existing tables move from 3h to the new times. go vet, builds (darwin, GOOS=linux, GOOS=windows) and go test -tags nogui -count=1 ./... under a temp HOME pass (TestDevinSignedInOnlyInMagpie failed once under load, passes alone and in the package rerun); codex-auto-reset and routing-reset-deadline pass in webkit. Not tried with the reporter's accounts. The spend reads the account again under autoReset's lock (stillExpiring, spendExpiringNow) and spends nothing if the week's used-up spend has started its windows again since the look, so a held-up account isn't given two resets; TestExpiringSpendReadAgain.
2026-10-04 07:03:16 +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 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
group: a routing group takes images when one of its members sees them, and an image goes to the members that do (#756, HuHong-Tao on v0.1.813: group/auto-deepseek-v4-1-flash turned 1198 image requests away with 400 "model does not support image input" in a day, where commandcode's deepseek-v4.1-flash of the group took them) — groupEntries said a group took images only when every member did (Images ANDed, ImageInput through sharedImageInput, where one explicit text-only answer wins), and the gateway's membersImageInput said the same of a request, so one text-only member turned away every image before any member was tried, and /v1/models gave the group input ["text"]. Since a rule for images can send one past a text-only member, the gateway already dropped text-only members for a request with a new image; it now does so for every group without a model to describe images: a new image goes to the members that see it (an explicit answer, or models.dev's estimate as blindTo reads it for a model of its own), to members magpie knows nothing of when none is known to see, and is turned away (400) only when every member is text-only. With a describer the members keep their order, a text-only one given the image described, as before. groupEntries ORs Images and takes ImageInput through anyImageInput (true when a member's is, false when all are, else unknown); membersImageInput drops the rule's members (the rule's member first changes nothing: one that can't see is passed over). With the owner's real accounts (a copy of the config, Vision off): an image to group/auto-deepseek-v4-1-flash was 400 on origin/main and is answered by the Devin account's deepseek-v4-1-flash ("White"); group/auto-kimi-k3, whose only member that sees is an OpenCode Go key without a subscription, went from 400 to that member's 403. TestGroupImages (a group with one member that sees takes images, all text-only doesn't), TestGroupImagesFromAMemberThatSees, TestKnownTextOnlyModelRejectsImagesBeforeUpstream (an ordered group with its text-only member first sends the image to the one that sees), TestRuleImages and TestRuleImagesUnlistedMember now expect the image answered by the member that sees; they fail on the old code. go vet, builds (darwin, GOOS=linux, GOOS=windows) and go test -tags nogui -count=1 ./... under a temp HOME pass.
2026-10-04 15:01:11 +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
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
catalog: a model the provider's catalog doesn't list is named as the other providers name it (John on Discord #feedback: GLM Coding Plan and ZCode name the same models in different case, in the provider list and the group picker) — the Coding Plan's /models (open.bigmodel.cn and api.z.ai, read with a real key) gives ids alone, glm-5-turbo, glm-5.3-flashx …; catalog.Decorate names only those Zhipu's models.dev entry (zhipuai) lists, so glm-5-turbo kept its id as its name beside ZCode's GLM-5-Turbo. models.dev's load now keeps each model's name as most providers serving it give it (a name that is only the id casts no vote), catalog.NameOf reads it by bare id in any case, and catalog.Named fills it in for a model still named by its id: in Available's live list and for a pick the list doesn't have. Ids, and what is sent upstream, are unchanged; a name the user gave stays theirs (Catalog's Name, with the new one as Default); an Azure provider is left out, its deployments named as their owner named them. TestModelNamedAsElsewhere (the Coding Plan's real ids, a user name, an Azure deployment) fails without the provider change and passes; TestNameOf is new. TestSearcherChosen read whatever catalog an earlier test had left loaded, and now resets it: with none, its lists' models go by their ids, as it expects. go test ./internal/catalog/ ./internal/provider/ ./internal/gateway/ ./internal/gui/ . pass; -race -count=5 for the two new tests; go vet, linux(nogui)/windows builds pass. No GUI change (the GUI and the group picker show name || id).
2026-10-06 13:09:40 +08:00
search: a Kimi Code plan searches the web for its own models with its own search service, before the Codex account or whatever magpie picks, and Settings may name it to search for every model (Discord: on Claude Code with the Kimi Code plan, why does web search use Codex's model?) — kimi-cli's SearchWeb (kimi_cli/tools/web/search.py, 1.52.0) POSTs {text_query, limit, enable_page_crawling, timeout_seconds:30} to the plan's search_url, which auth/platforms.py makes <base_url>/search (https://api.kimi.com/coding/v1/search) with the same key as the models (setup.py and oauth.py write services.moonshot_search with the plan's key or sign-in), User-Agent KimiCLI/<ver> and X-Msh-Tool-Call-Id, and reads search_results [{site_name,title,url,snippet,content,date}]. provider.KimiCodeSearch finds a plan by api.kimi.com/api.kimi.ai /coding or its kimi-code preset; magpie asks it the same way (no model asked, pages as the search APIs' are, date as page_age). A model served by a plan is given magpie's web_search even with nothing else set up, and its searches go to the plan first; failing, magpie's pick, then the search APIs, as before. For other providers' models a plan is never picked automatically (it would spend the plan's allowance and change today's searcher): Settings › 联网搜索 offers it once, by its web search with no model, and says it searches for its own models first. TestKimiCodePlanSearchesForItsModels fails on the old code (no search tool with nothing else set up; with a searching API and a search API, the old code asked them and never Kimi) and passes; TestKimiCodePlanNamedToSearch and TestKimiCodeSearchAddress cover naming it and the address; searcher-pick.test.cjs (webkit, en/zh) the picker. Against the real account's key from a temp copy of the config: api.kimi.com/coding/v1/search exists (404 on a wrong path, 400 on a bad body) but answers 403 access_terminated_error for search and chat alike, as that Kimi Code subscription has lapsed, so a real search result wasn't seen. go vet, builds (darwin, GOOS=linux, GOOS=windows) and go test -tags nogui -count=1 ./... under a temp HOME pass.
2026-10-04 02:00:32 +08:00
gateway: a request offering web search is translated on the client's own API where the provider serves it, not on Chat (#997, wlj521: Codex on Xiaomi MiMo's pay as you go showed "Responses → Chat" on v0.1.1082 with all three URLs filled) — Codex offers its web_search tool on every turn, and a provider that doesn't search by itself can't be relayed it: magpie searches for it, which takes a translated request. That translation went to usable's first API, Chat for every model but OpenAI's and Claude's, so on any provider serving Responses (MiMo, whose api.xiaomimimo.com/v1/responses answers 401 to a made-up key, a wrong path 404) each Codex turn went out as Chat, and Claude Code's WebSearch on a provider with an Anthropic URL the same. The translation now stays on the client's API when the provider serves it there and neither ResponsesFirst nor MessagesFirst prefers another (firstElsewhere); the OnMessages check moved before the search check so a Claude model still goes to Messages. The search tool itself isn't sent to the provider (forwardTranslated drops it where the provider doesn't search), and magpie's own web_search function is given on Responses as it was on Chat. TestSearchOfferedStaysOnTheClientsAPI (the preset's three URLs; Codex's Responses with a function and web_search, Claude Code's Messages with web_search_20250305, Chat's web_search_options) and TestCodexSearchAnsweredOnResponses (a searcher configured: the model is asked on /v1/responses twice, the search runs once, Codex gets the answer) fail without the change (/v1/chat/completions) and pass; -race -count=5 on them and TestWebSearch*, TestClaudeFromChat*, TestAutoRelays*. go test ./internal/gateway ./internal/provider, go vet, gofmt -l, linux/windows (nogui) builds pass. Not tried with a real MiMo key (none on this machine) or a real Codex. docs/subsystems/gateway-routing.md says it.
2026-10-06 17:16:50 +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
routing: a failed request whose route has no order opens when clicked (Discord, mythfish on v0.1.810: on the Routing page's 实时 list, clicking "11:28:08 AM • workbuddy-ai/deepseek-v4.1-flash • 400 · 失败 6.5 秒" did nothing; the request was refused with WorkBuddy AI 400 {"message":"WorkBuddy AI: Invalid request parameters","type":"invalid_request_error"}) — Route.Order has no omitempty and the trace's snapshots (Trace, the saved copy in update, sessionLatest) copied it with append([]Weighed(nil), r.Order...), so a route begun with no seats weighed reached the page as "order": null, while its tries were there. routing.js reads r.order as a list throughout: a row's click runs pick → sync → rebuild, whose setOf(r) is r.order.map(seat), and renderActs runs r.order.forEach, so pick threw (webkit: "null is not an object (evaluating 'r.order.map')") before the story was drawn and the click looked dead. Reproduced with a live trace fixture: a 400 with order null fails to open; the same 400 with order [] or a one-seat order, every failure kind, and an order whose seats don't match the try all open, so it is the null and not the 400 or the WorkBuddy error. The page now fills in a route's lists where routes come in (whole: order, tries and left default to []), for the live trace, a kept day's history and openRoute's fetched route (the tray panel's and Sessions' links), and the gateway writes Order as [] when it is empty in all three snapshots, so an older page reading a newer gateway is fine too. Tests: routing-failed-open.test.cjs (webkit, en and zh, live and a kept day: a request that answered and the report's 400 with "order": null; clicking the 400 presses its row, shows the story "How the request at … was routed" / "… 的请求是怎么路由的" with "It said: WorkBuddy AI: Invalid request parameters" / "原话:…", doesn't move the page, and the answered one opens again after; no page errors) fails all four on the old routing.js and passes; TestTraceOrderIsAList (a route begun with no order and refused with a 400: Trace's JSON has "order":[], sessionLatest's copy a non-nil Order) fails on the old trace.go with "order":null and passes. The routing-*.test.cjs and click-scroll GUI tests pass in webkit (routing-time's zh case failed once in a 4-way parallel run and passes on its own). go vet, builds (darwin, GOOS=linux, GOOS=windows) and go test -tags nogui -count=1 ./... under a temp HOME pass. Not found: which gateway path began the reporter's route with no order — the plain chat path weighs at least the provider it was asked for — so the fix is to the shape, not to that path; not tried with the reporter's WorkBuddy AI account. The 400 itself (WorkBuddy refusing the request's parameters) is a separate question this doesn't touch.
2026-10-04 12:23:43 +08:00
gateway, gui: a provider's keys can be routed by weight, a key weighing 3 taking three requests for every one a key weighing 1 takes (dengtongcai, #841) — Routing already spread requests over a provider's keys in order, in turn, least used first or by pace, but never in a set proportion: two team keys meant to split 75/25 could only go evenly or one after the other. Routing now has By weight (routing "weight") for a provider with keys: each key that's on shows its weight and its share beside it in the editor (Weight 3 · 75%), a click edits it in place and posts keys/weight (provider.SetKeyWeight, 1 to 1000; none or 0 counts as 1, kept out of the file), and providers.json takes it as "weight" on each of keys (keyWeight for the first). The gateway picks as NGINX's smooth weighted round robin does (weighted.go): 3, 1 and 4 go round in every 8 requests in that ratio, never bunched; the others follow in their order for failover. A key resting after a failure is left out of the round while it rests, the others taking its share by their weights, and goes last. A conversation still stays with its key as Stays says (cache-warm across turns by default, within a turn always), and a request kept so is counted against the key that took it, not the one the round picked, so new conversations make up the share; Stays Off gives the ratio request by request. The weight goes with its key through Make first, the editor's Save, backups and WebDAV sync. The Routing page offers By weight for key pools and explains a pick by it; sink (Goes to the back) is off for it as for In turn. zh, ja, de strings. TestWeightedKeys (3:1:4 exact in each of 50 rounds of 8 through perKey and route; with one resting the other two 75/25 and the resting one last; a kept request made up by the next picks) and TestSetKeyWeight fail on the old code (no Weighted); key-weight.test.cjs (WebKit, en and zh: By weight shows the weights and shares, a click sets one and the shares redraw, Smart hides them, no scroll) fails on the old code (no By weight). key-import, panel-routing, routing-save, rate-limited-sink, routing-reset-deadline, account-arrange, gui-ja, gui-de and click-scroll pass on WebKit. go vet, builds (darwin, GOOS=linux nogui, GOOS=windows) and go test -tags nogui -count=1 ./... under a temp HOME pass.
2026-10-05 08:50:57 +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
gateway: a lone provider's Claude or GPT reasoning streams as it comes, not held for a refusal no one else could answer (iTianbao on X: 请加一个开关,让思考实时透传; through a relay Claude Code showed the thinking only all at once with the text, Codex none until then) — holdWriter held Claude's, GPT's and Gemini's reasoning (refusesAfterThinking, up to holdThinking) on every candidate, so a refusal after it could go to the next one (#248), but a refusal on the last candidate is never asked again (the !last checks in serve), so there it only delayed the thinking. On the last candidate Claude's and GPT's reasoning is now shown at once (thinkingShown), and a refusal after it reaches the agent as the vendor's own stop; Gemini's stays held there, for a reply that only reasoned to be asked again (#667, TestGeminiEmptyRepliesAskedAgain). A group with members after it still holds as before. Before: on a slow fake relay (4 thoughts 300ms apart, then text) every relay shape (Messages, Chat with reasoning_content and reasoning, Responses) for claude-sonnet-4-5 and gpt-5.1 gave Claude Code's thinking_delta and Codex's reasoning_summary_text.delta all at ~1.2–1.5s with the text; after, 0/300/600/900ms. TestThinkingStreamsWithNoOneElse (Messages, Chat for Claude and GPT, Responses; each asked from /v1/messages and /v1/responses) fails without the change in every case and passes; TestHeldThinkingKeepsTheAgentAlive's nobody-left case now expects the thinking and Claude's refusal rather than a held error. -race -count=5 on TestThinking*, TestRefusal*, TestHeld*, TestGeminiEmpty*; go test -tags nogui ./internal/gateway, go vet, gofmt -l, linux and windows (nogui) builds pass. Not tried with the reporter's relay: whether it sends GPT's reasoning at all (OpenAI's Chat API doesn't) is unknown, and no switch was added. docs/subsystems/gateway-routing.md says it.
2026-10-06 19:34:24 +08:00
providers: Tencent Cloud is one preset, its regions Plan · China, Pay as you go · China and Pay as you go · Global (Discord, Jorben: Tencent Cloud's Token Plan, TokenHub and TokenHub China were three tiles of one vendor) — tencent-token-plan, tencent-tokenhub-cn and tencent-tokenhub become tencent-cloud's regions plan, cn and intl, as Xiaomi MiMo's are, each made as its own preset made it: the plan's /plan endpoints with no Responses, its own models and no catalog; TokenHub's hosts with Responses, models.dev's tencent-tokenhub and its own keys page and docs. A region now carries its Website, Catalog and Models, and a saved provider's region is found by its endpoints (regionOf, by exact URL before path, since TokenHub China and global share their paths): normalize gives it that region's keys page, docs and catalog (a catalog the user picked is kept), and planModels its models. The old ids are aliases, as qianfan-token-plan is of baidu-qianfan: Preset and FromPreset take them, FromPreset builds at their region, normalize rewrites a saved provider's preset to tencent-cloud, and a provider's own id never changes, so its key, models, routing groups, usage and prices stay where they were; magpie://import links by an old id land at its region, and region= now also gives a region's docs and catalog. Imports from other apps match a region's endpoints. The editor's docs link follows the region picked with the segmented control, en and zh ("按量 · 中国", "按量 · 国际"); the add sheet has one Tencent Cloud tile; the site's import docs list tencent-cloud with its regions and import.html still names the old ids. TestTencentCloud (each region, old ids, FromPreset, imports, import links) and TestTencentCloudOldPresetsKeepWorking (a providers.json saved by an older magpie under all three old presets, with a routing group over them, read, re-saved as the editor saves and switched between regions) fail on the old code; TestTencentTokenHubRoutes routes through old ids and tencent-cloud alike. tencent-cloud.test.cjs (WebKit en and zh: one tile, the three regions, no select, the docs and Get a key links following the region, an old id's provider opening at its region and saved under its id) fails on the old app.js; add-sheet, zhipu-team, system-one, strip-scrollbar and click-scroll pass (Chromium doesn't launch in this sandbox). Not tried with a Tencent account. go vet, builds (darwin, GOOS=linux, GOOS=windows) and go test -tags nogui -count=1 ./... under a temp HOME pass.
2026-10-04 19:38:14 +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
routing: a failed request whose route has no order opens when clicked (Discord, mythfish on v0.1.810: on the Routing page's 实时 list, clicking "11:28:08 AM • workbuddy-ai/deepseek-v4.1-flash • 400 · 失败 6.5 秒" did nothing; the request was refused with WorkBuddy AI 400 {"message":"WorkBuddy AI: Invalid request parameters","type":"invalid_request_error"}) — Route.Order has no omitempty and the trace's snapshots (Trace, the saved copy in update, sessionLatest) copied it with append([]Weighed(nil), r.Order...), so a route begun with no seats weighed reached the page as "order": null, while its tries were there. routing.js reads r.order as a list throughout: a row's click runs pick → sync → rebuild, whose setOf(r) is r.order.map(seat), and renderActs runs r.order.forEach, so pick threw (webkit: "null is not an object (evaluating 'r.order.map')") before the story was drawn and the click looked dead. Reproduced with a live trace fixture: a 400 with order null fails to open; the same 400 with order [] or a one-seat order, every failure kind, and an order whose seats don't match the try all open, so it is the null and not the 400 or the WorkBuddy error. The page now fills in a route's lists where routes come in (whole: order, tries and left default to []), for the live trace, a kept day's history and openRoute's fetched route (the tray panel's and Sessions' links), and the gateway writes Order as [] when it is empty in all three snapshots, so an older page reading a newer gateway is fine too. Tests: routing-failed-open.test.cjs (webkit, en and zh, live and a kept day: a request that answered and the report's 400 with "order": null; clicking the 400 presses its row, shows the story "How the request at … was routed" / "… 的请求是怎么路由的" with "It said: WorkBuddy AI: Invalid request parameters" / "原话:…", doesn't move the page, and the answered one opens again after; no page errors) fails all four on the old routing.js and passes; TestTraceOrderIsAList (a route begun with no order and refused with a 400: Trace's JSON has "order":[], sessionLatest's copy a non-nil Order) fails on the old trace.go with "order":null and passes. The routing-*.test.cjs and click-scroll GUI tests pass in webkit (routing-time's zh case failed once in a 4-way parallel run and passes on its own). go vet, builds (darwin, GOOS=linux, GOOS=windows) and go test -tags nogui -count=1 ./... under a temp HOME pass. Not found: which gateway path began the reporter's route with no order — the plain chat path weighs at least the provider it was asked for — so the fix is to the shape, not to that path; not tried with the reporter's WorkBuddy AI account. The 400 itself (WorkBuddy refusing the request's parameters) is a separate question this doesn't touch.
2026-10-04 12:23:43 +08:00
gateway, gui: a request an aggregator passed on says which provider answered it behind it, "Upstream: DeepInfra" (leslie_luo on Discord: 像 CPA 那样指出实际供应商) — through OpenRouter a request was only "OpenRouter · deepseek/deepseek-chat", with no word of the provider (DeepInfra, StreamLake, Novita…) OpenRouter had sent it on to. OpenRouter names it in its replies, as tried with the owner's real key: Chat's body and every chunk of its stream have a top-level "provider", Messages' body has one and its stream's message_start a message.provider; its Responses replies and every reply's headers name none. The gateway now reads it (upstreamOf) from a body relayed as it came and from each event of one it translated, Chat, Messages or Responses, streamed or not, and keeps it on the request's try and route (upstream) and in the usage ledger (Record.Upstream; the CSV export's last column, searched as the rest). The Routing page's Requests list says Upstream: DeepInfra beside where it went, giving way after the provider's name when short of room, with its story saying the aggregator passed it on; a request's details on the Usage page have Upstream provider. A reply naming none says nothing. zh, ja, de strings. ClinePass and Cline weren't tried (no account); Vercel's gateway documents no such field in its replies, and a vendor whose reply has a top-level "provider" string is read the same way. In a sandbox magpie with the owner's real OpenRouter key: Chat, Messages and Responses (translated to Chat), streamed and not, all six said StreamLake in the trace, the ledger and on the Routing page. TestUpstreamRecorded (6 paths × streamed or not: the route, its try, Recent and the ledger) fails on the old code, TestUpstreamOf; routing-upstream.test.cjs (Chromium and WebKit, en and zh: the tag inside its row, the story, the Usage details, a click moves nothing, no left-border accent) fails on the old assets. ledger-hscroll, request-archive-usage, usage-ledger and usage-ledger-detail pass on WebKit; routing-served fails the same on origin/main (its swap wording). go vet, builds (darwin, GOOS=linux nogui, GOOS=windows) and go test -tags nogui ./... under a temp HOME pass but for TestTranslatedStreamKeepsClientAlive, which passes alone 5 times here and on origin/main.
2026-10-05 14:08:08 +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
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, gui: a provider's keys can be routed by weight, a key weighing 3 taking three requests for every one a key weighing 1 takes (dengtongcai, #841) — Routing already spread requests over a provider's keys in order, in turn, least used first or by pace, but never in a set proportion: two team keys meant to split 75/25 could only go evenly or one after the other. Routing now has By weight (routing "weight") for a provider with keys: each key that's on shows its weight and its share beside it in the editor (Weight 3 · 75%), a click edits it in place and posts keys/weight (provider.SetKeyWeight, 1 to 1000; none or 0 counts as 1, kept out of the file), and providers.json takes it as "weight" on each of keys (keyWeight for the first). The gateway picks as NGINX's smooth weighted round robin does (weighted.go): 3, 1 and 4 go round in every 8 requests in that ratio, never bunched; the others follow in their order for failover. A key resting after a failure is left out of the round while it rests, the others taking its share by their weights, and goes last. A conversation still stays with its key as Stays says (cache-warm across turns by default, within a turn always), and a request kept so is counted against the key that took it, not the one the round picked, so new conversations make up the share; Stays Off gives the ratio request by request. The weight goes with its key through Make first, the editor's Save, backups and WebDAV sync. The Routing page offers By weight for key pools and explains a pick by it; sink (Goes to the back) is off for it as for In turn. zh, ja, de strings. TestWeightedKeys (3:1:4 exact in each of 50 rounds of 8 through perKey and route; with one resting the other two 75/25 and the resting one last; a kept request made up by the next picks) and TestSetKeyWeight fail on the old code (no Weighted); key-weight.test.cjs (WebKit, en and zh: By weight shows the weights and shares, a click sets one and the shares redraw, Smart hides them, no scroll) fails on the old code (no By weight). key-import, panel-routing, routing-save, rate-limited-sink, routing-reset-deadline, account-arrange, gui-ja, gui-de and click-scroll pass on WebKit. go vet, builds (darwin, GOOS=linux nogui, GOOS=windows) and go test -tags nogui -count=1 ./... under a temp HOME pass.
2026-10-05 08:50:57 +08:00
gateway, gui: a provider's keys can be routed by weight, a key weighing 3 taking three requests for every one a key weighing 1 takes (dengtongcai, #841) — Routing already spread requests over a provider's keys in order, in turn, least used first or by pace, but never in a set proportion: two team keys meant to split 75/25 could only go evenly or one after the other. Routing now has By weight (routing "weight") for a provider with keys: each key that's on shows its weight and its share beside it in the editor (Weight 3 · 75%), a click edits it in place and posts keys/weight (provider.SetKeyWeight, 1 to 1000; none or 0 counts as 1, kept out of the file), and providers.json takes it as "weight" on each of keys (keyWeight for the first). The gateway picks as NGINX's smooth weighted round robin does (weighted.go): 3, 1 and 4 go round in every 8 requests in that ratio, never bunched; the others follow in their order for failover. A key resting after a failure is left out of the round while it rests, the others taking its share by their weights, and goes last. A conversation still stays with its key as Stays says (cache-warm across turns by default, within a turn always), and a request kept so is counted against the key that took it, not the one the round picked, so new conversations make up the share; Stays Off gives the ratio request by request. The weight goes with its key through Make first, the editor's Save, backups and WebDAV sync. The Routing page offers By weight for key pools and explains a pick by it; sink (Goes to the back) is off for it as for In turn. zh, ja, de strings. TestWeightedKeys (3:1:4 exact in each of 50 rounds of 8 through perKey and route; with one resting the other two 75/25 and the resting one last; a kept request made up by the next picks) and TestSetKeyWeight fail on the old code (no Weighted); key-weight.test.cjs (WebKit, en and zh: By weight shows the weights and shares, a click sets one and the shares redraw, Smart hides them, no scroll) fails on the old code (no By weight). key-import, panel-routing, routing-save, rate-limited-sink, routing-reset-deadline, account-arrange, gui-ja, gui-de and click-scroll pass on WebKit. go vet, builds (darwin, GOOS=linux nogui, GOOS=windows) and go test -tags nogui -count=1 ./... under a temp HOME pass.
2026-10-05 08:50:57 +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
2026-10-05 06:07:03 +08:00