Files
magpie/docs/subsystems
yetone 87487c5ef0 plugin, provider: a plugin can press its vendor's daily check-in itself (auth.checkin), and magpie runs it as its own check-ins (Lemon on Discord #feedback: provider 每日签到的方法能放到插件里面去做吗; ARNO earlier asked for Qoder's) — the check-ins magpie had (WorkBuddy, Trae CN, MiniMax Code, Qoder) are each written in magpie against the vendor's endpoint, even for a subscription that moved to its plugin, so a plugin author had no way to give their provider one. A plugin's auth hook may now have checkin(getAuth, provider) beside usage: the host lists the provider with checkin: true (plugin.Provider.Checkin) and answers checkin({provider, account}) (plugin.AccountCheckin), refreshing the account as usage does, with {outcome, credit, streak, message}; outcome is claimed, done, ineligible, inactive, captcha or failed, and a throw or an outcome outside these is failed with the reason. internal/provider/plugin_checkin.go runs these through checkin.go's checkiner, as the built-ins are: each signed-in account in use once a Beijing day (first look 2 minutes after start, then every 30), a failure asked again after wbCheckinRetry, every other outcome settled for the day, results kept in plugin-checkin.json by provider|account; a failure is recorded as failed and resets nothing. captcha is new (CheckinCaptcha): the vendor wants a captcha, which magpie never solves, so it is said as "check in in its own app", not as a failure, and not asked again that day. The results mark the account's Usage card (checkinBy "plugin:<provider>", WithCheckins), so the card's check-in row, its Auto check-in and Check in now work for it (POST /api/settings/plugin-checkin {provider, on}, POST /api/usage/plugin-checkin {provider}); Settings → Usage gains a Plugins tab, shown while such a plugin is signed in, a Daily check-in row per provider with Off/On and how each account went; magpie accounts checkin and c on the TUI's Usage page include them (the TUI's "no account" line names check-in plugins). The switch (settings.PluginCheckins, by provider) is off by default; for WorkBuddy, Trae CN, MiniMax Code and Qoder it follows that vendor's existing switch until set on its own, and magpie's own check-in through the plugin's fetch leaves a provider whose plugin checks in to it (pluginChecksIn), so no account is checked in twice. The Settings page's save carries PluginCheckins over (TestSettingsSaveKeepsWhatItDoesNotSend's fixture names it). No plugin declares the hook yet; Qoder's check-in stays magpie's own here. TestPluginCheckinRunsAsTheBuiltinsDo (the fake plugin with FAKE_CHECKIN, five accounts answering claimed, done, captcha, a throw and an unknown outcome: off presses nothing; on, each outcome kept with its vendor; a minute on nothing pressed again; past the retry only the two failures; the next day all five; PluginCheckins and the cards marked; another provider's press asks none) and TestPluginCheckinTakesOverTheVendors (a qoder plugin without the hook leaves magpie's Qoder check-in its account, with it none; PluginCheckinOn's fallbacks) pass -race -count=10, and the latter fails with pluginChecksIn stubbed false; TestTUIChecksPluginsIn passes -race -count=20. plugin-checkin.test.cjs (the card's row with the captcha said, Auto check-in posting {provider, on}, no page move; the Settings Plugins tab at 440px with the row fitting, no sideways scroll, On posting) passes on Chromium and WebKit in en, zh, ja and de; usage-checkin, i18n-keys, gui-ja, gui-de and settings-* pass. go test -tags nogui ./internal/..., go vet, gofmt -l, linux (nogui) and windows builds pass. Not tried with a real plugin or account: no published plugin has the hook yet. docs/subsystems/provider-plugins.md (the hook's contract) and docs/reference.md say it.
2026-10-06 18:40:45 +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
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
plugin, gui, docs: Plugins says which plugins are providers and which are middleware, lists the community's ready-made middleware, and edits a middleware's options in its row (the owner) — the Plugins page still read as if every plugin were a subscription, and nothing showed what a middleware does or let you set its options without editing plugins.json. Every Discover card and Installed row now carries a Provider or Middleware chip (both for a plugin that is both), also for one that is off (isMiddleware/middlewareOnly from the server, read from its files); Discover splits into Subscriptions and Gateway middleware ("runs in magpie's gateway on what every agent sends and gets back, whichever provider serves it"), the latter from market entries with "kind": "middleware" and no providers or icon (the built-in market.json lists the five new @magpie-community/middleware-* packages: param-override, model-map, system-prompt, word-guard, think-tags, mostly New API's channel settings with the same JSON). A middleware row has Options: a JSON editor in the row, starting from the package's magpie.options example when none are set, which validates it is an object, saves (POST /api/plugins/options) for the next request, clears, and cancels with Esc. magpie plugin options takes a short name (model-map for @magpie-community/middleware-model-map; plugin.ShortName, also in SetOptions). en/zh/ja/de. Docs: usemagpie.ai/docs/plugins (en/zh/ja) gains Ready-made middleware and Options and the registry's kind; reference.md, both READMEs and docs/subsystems/gateway-middleware.md likewise. Tested with a real DeepSeek account and real Claude Code through magpie web --gateway in a sandbox HOME, with all five installed and options set by short name: a mapped model named back, max_tokens capped, a prompt appended for claude only, word-guard's 400 and param-override's 403 in the agent's shape, <think> stripped from Chat and Responses streams and a whole Anthropic reply, reasoning_content moved into the text; /api/plugins showed the kinds and the example. TestSetOptionsShortName is new; TestBuiltinMarket checks a middleware entry has no providers or icon and no other kind. plugin-middleware.test.cjs (Chromium and WebKit, en and zh: the chips, Discover's two sections, the options editor with its errors, no select, no scroll, no left border) fails without the change; the other plugin tests and click-scroll pass in WebKit (plugin-market's section name updated; plugin-move fails only under parallel load and passes alone). go vet, go build (darwin, GOOS=linux nogui, GOOS=windows) and go test -tags nogui ./... under a temp HOME pass.
2026-10-05 20:50:09 +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
plugin, provider: a plugin can press its vendor's daily check-in itself (auth.checkin), and magpie runs it as its own check-ins (Lemon on Discord #feedback: provider 每日签到的方法能放到插件里面去做吗; ARNO earlier asked for Qoder's) — the check-ins magpie had (WorkBuddy, Trae CN, MiniMax Code, Qoder) are each written in magpie against the vendor's endpoint, even for a subscription that moved to its plugin, so a plugin author had no way to give their provider one. A plugin's auth hook may now have checkin(getAuth, provider) beside usage: the host lists the provider with checkin: true (plugin.Provider.Checkin) and answers checkin({provider, account}) (plugin.AccountCheckin), refreshing the account as usage does, with {outcome, credit, streak, message}; outcome is claimed, done, ineligible, inactive, captcha or failed, and a throw or an outcome outside these is failed with the reason. internal/provider/plugin_checkin.go runs these through checkin.go's checkiner, as the built-ins are: each signed-in account in use once a Beijing day (first look 2 minutes after start, then every 30), a failure asked again after wbCheckinRetry, every other outcome settled for the day, results kept in plugin-checkin.json by provider|account; a failure is recorded as failed and resets nothing. captcha is new (CheckinCaptcha): the vendor wants a captcha, which magpie never solves, so it is said as "check in in its own app", not as a failure, and not asked again that day. The results mark the account's Usage card (checkinBy "plugin:<provider>", WithCheckins), so the card's check-in row, its Auto check-in and Check in now work for it (POST /api/settings/plugin-checkin {provider, on}, POST /api/usage/plugin-checkin {provider}); Settings → Usage gains a Plugins tab, shown while such a plugin is signed in, a Daily check-in row per provider with Off/On and how each account went; magpie accounts checkin and c on the TUI's Usage page include them (the TUI's "no account" line names check-in plugins). The switch (settings.PluginCheckins, by provider) is off by default; for WorkBuddy, Trae CN, MiniMax Code and Qoder it follows that vendor's existing switch until set on its own, and magpie's own check-in through the plugin's fetch leaves a provider whose plugin checks in to it (pluginChecksIn), so no account is checked in twice. The Settings page's save carries PluginCheckins over (TestSettingsSaveKeepsWhatItDoesNotSend's fixture names it). No plugin declares the hook yet; Qoder's check-in stays magpie's own here. TestPluginCheckinRunsAsTheBuiltinsDo (the fake plugin with FAKE_CHECKIN, five accounts answering claimed, done, captcha, a throw and an unknown outcome: off presses nothing; on, each outcome kept with its vendor; a minute on nothing pressed again; past the retry only the two failures; the next day all five; PluginCheckins and the cards marked; another provider's press asks none) and TestPluginCheckinTakesOverTheVendors (a qoder plugin without the hook leaves magpie's Qoder check-in its account, with it none; PluginCheckinOn's fallbacks) pass -race -count=10, and the latter fails with pluginChecksIn stubbed false; TestTUIChecksPluginsIn passes -race -count=20. plugin-checkin.test.cjs (the card's row with the captcha said, Auto check-in posting {provider, on}, no page move; the Settings Plugins tab at 440px with the row fitting, no sideways scroll, On posting) passes on Chromium and WebKit in en, zh, ja and de; usage-checkin, i18n-keys, gui-ja, gui-de and settings-* pass. go test -tags nogui ./internal/..., go vet, gofmt -l, linux (nogui) and windows builds pass. Not tried with a real plugin or account: no published plugin has the hook yet. docs/subsystems/provider-plugins.md (the hook's contract) and docs/reference.md say it.
2026-10-06 18:40:45 +08:00

Subsystem design references

These pages describe current responsibilities, runtime paths, state, and contracts. Start with the subsystem relevant to your task, then follow its source links to verify the implementation you change.

Subsystem Reference
Built-in subscriptions moving to plugins Provider and plugin ownership
Plugins that run on the gateway's requests and replies Gateway middleware
Claude subscription turns across Claude Code runs Claude subscription bridge
How the gateway picks a provider, key or account, and falls back Gateway routing and fallback
Connecting an agent's config to the gateway, drift, disconnect, WSL twins Agent wiring
Providers, keys, subscription accounts, refresh and allowance Providers and accounts
Instructions, MCP servers, skills and RTK written into agents Library
Tray, panel, window, magpie web and the page's JSON API GUI app shell

A subsystem can span several packages or repositories. Its reference describes the behavior those parts provide together; it does not need to list every function.

Maintain a reference

Update the owning reference in the same PR when a change alters a documented responsibility, interface, state transition, runtime path, compatibility rule, or failure behavior. An internal refactor that preserves these contracts does not require a prose change.

Keep each fact in one reference and link to it from agent instructions or other pages. Link to source files and named symbols rather than copying implementation details that change frequently. Explain field meanings and constraints that signatures alone do not express.

For a new reference, include the responsibilities and boundaries, source of truth, main runtime paths, state transitions, important constraints, and existing verification commands. Include failure and compatibility behavior where relevant. Document current behavior separately from proposed changes; the proposal belongs in its issue or PR.

Describe a PR's semantic changes

For each affected subsystem, explain the observable change with its trigger and before/after behavior. Include changes to interfaces, state, compatibility, and failure behavior when relevant. Link the affected reference, implementation, and verification evidence. State when a refactor preserves the documented behavior.

For example, a change to plugin adoption should explain which migration state and account condition trigger it, which implementation handles requests before and after adoption, and which test exercises that transition.

This is an author-provided explanation that a reviewer verifies against the diff and surrounding code. It is not an automatically proven compatibility verdict.

Review a change

  1. Identify the affected subsystem and the actual runtime path, including plugin or built-in ownership.
  2. Compare the claimed before/after behavior with the base and head implementation. Trace relevant callers and state transitions beyond the changed lines.
  3. Check that changed contracts appear in the owning reference and that source links still resolve.
  4. Check that the cited tests or runtime evidence exercise the claimed behavior. Report checks that were not run.

A documentation update alone does not prove the implementation. Existing tests and runtime evidence remain necessary for behavior changes.