Dragging a scrollbar did nothing on any page with content taller than the
window: the thumb would not move, and letting go left it where it started
(emo172, #1089). The wheel worked, so only the drag was broken, and a
scrollbar dragged in a container the app does not listen to scrolled
normally.
"Where the reader is" holds a view still unless the reader asked for the
scroll: the wheel, a touch, the keys, or a drag of the content, each of
which says so through an event. A scrollbar dragged by the mouse has no
such event — Chromium and WebView2 give it no pointermove — so every
scroll it produced was put back as it came, the thumb sliding back under
the pointer. The wheel works because wheel is one of the events.
The press on a scrollbar does reach the view, with the pointer past its
content in the scrollbar's band, and the lift ends the drag. Held from
that press to the lift, the drag's scrolls count as the reader's.
Two more things hold the page still that a wheel already lets go of, and
the drag lets go of the same two:
- what the last click was holding in place, which keeps what was clicked
on the screen frame by frame for a few seconds after it;
- the reader's window itself, which WebKit needs held open past the lift:
it sends the drag's last scrolls once the gesture is over, after the
pointerup, so closing the window on the lift put those back.
A scroll nothing asked for — code setting scrollTop — is still put back:
only the bar's own drag counts as the reader's, not every scroll.
view-scrollbar-drag.test.cjs is new. It drags a page's own scrollbar in
both engines in English and Chinese, straight away and after a click in
the page that holds what was clicked; it checks the page follows the bar
and stays where it was left, that the wheel still scrolls, that a scroll
code set is still put back, and that a scroll arriving after the lift is
still the reader's. It uses the pages' own scrollbar CSS rather than one
of its own: a scrollbar-width set on the view makes Chromium drop every
::-webkit-scrollbar rule (app.css) and draw Windows' classic bar. The
Routing page is dragged at a narrow window, where it has room to scroll:
at a wide one it lays its stage beside the list and has ~35px left, not
enough to tell a drag from a nudge. The scroll arriving after the lift is
checked on the list page only. It was left off Routing while #1249 was
open — WebKit clamped Routing's last stretch there, so a scroll sent
after the lift measured that bug rather than the window held open past
it. #1249 is fixed in main now, so the check could be widened to Routing;
it is not, because that clamp was only ever checked on Windows headless
WebKit here and not on the macOS WKWebView, and widening a check is
beyond what review asked for.
Three of the checks were reworked after they were run four test files at
a time and each was found to pass for the wrong reason, or to race:
- the click in the page lands on a part that leaves the page as it is.
The Routing page's own header opens the group editor in English,
growing the page and scrolling it on its own, so where the drag left
the page passed without the drag having moved it. The page is measured
again where it is now and the drag is judged by how far it moves it.
- the scroll arriving after the lift is written from the pointerup
itself, installed in the page before the lift. A round trip between the
lift and that scroll measured 6-180ms alone and 314ms four files at a
time — past the 250ms window the app holds open for it.
- the wheel's scroll is waited for to come to rest, not for a fixed
time, which under load was not always long enough.
The scroll code sets a value the page is not already at, and runs before
any click: a click holds what was clicked on the screen for a few
seconds, and that hold moves the page on its own, which would answer for
the app.
It fails on main (the drag leaves the page at 0) and passes with this.
Verified at this head, rebased onto main 0d8f85e4 — which brought
#1249's fix, a change to the same scroll handler (its early return and
puttingBack), and #1236's scrollbar CSS. The merge kept main's structure
and folded barDrag === v into its first branch, so a dragged bar is
treated as the reader's and never recorded as a put-back.
node --check; go vet ./...; go build (windows); GOOS=linux build -tags
nogui. view-scrollbar-drag 5/5 in Chromium and WebKit, en and zh, three
solo runs each with no flake, and 5/5 again four test files at a time,
three rounds each on both engines. The nineteen scroll-related GUI files
(click-scroll, menu-scroll, strip-scrollbar, view-scrollbar,
view-no-sideways, sessions-toolbar, sess-agent-strip, agent-grip,
group-member-drag, usage-arrange, provider-order, purpose-filter,
proto-menu-overflow, routing-scroll-end, routing-purpose-state,
routing-steady, routing-served, fold-show-grip) 94/94 in both engines,
main's new view-no-sideways included since the base changed the
scrollbar CSS this test measures against; gui-ja / gui-de / gui-zh-tw /
i18n-keys 10/10 in both. Two mutations at this head: main's app.js fails
all five (the drag leaves the page at 0), and removing only the merged
barDrag === v condition — leaving the rest of the feature — fails WebKit
5/5 and Chromium 2/5 ("a scroll arriving after the lift is still the
reader's"), so the line the rebase folded in is guarded and the new
scrollbar CSS has not let the page pass without it. The other four
per-guard mutations (the whole barDrag rule, the hold let go on the
press, the window held open past the lift, the reader-position
put-back) were each run red without their guard at the pre-rebase head;
the rebase only folded barDrag into main's first branch and left those
four paths as they were.
Not verified here: only on Windows, in Chromium and WebKit through
Playwright. Not tried on macOS or Linux.
Co-authored-by: wizzy-yang <wizzy-yang@users.noreply.github.com>
30d952cf (v0.1.976) forgetSideLogin no longer refuses the account a subscription uses first; magpie accounts forget goes through the same ForgetLogin, so a plugin's in-use account is forgotten and the remaining one becomes the one in use. Test-only; go vet and go test -tags nogui . ./internal/provider/... pass.
magpie group pick <id> <model> (or group set <id> pick=<model>, a bare model id when it names one member) picks from the CLI; magpie groups and group show mark the pick, the TUI shows "manual → model". A renamed provider or model renames the pick. TestManualGroup, TestGroupPick and routing-manual.test.cjs (Chromium, WebKit, en/zh) fail without the change. go vet, go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui ./... pass; click-scroll, old-webkit, routing-levels, routing-steady, panel-routing, settings-groups pass.
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.
magpie omp auto sets omp's thinking to auto, and the panel's effort slider shows a level it doesn't list as it is (#339)
until codex exec …; do magpie quota wait codex || break; done) — it reads the vendors itself as magpie quota does, gateway running or not (provider.Allotted: the subscriptions' and plans' readings, no key balances; AskUsage has each look wait for a new reading, and only a wait for Claude runs Claude Code's /usage, through AskClaudeUsage, which now calls it). An account has allowance when routing's usedUp says so (no window that stops it, every model, at 100%; exported as provider.UsedUp), and BackAt is when the last of those windows starts again. While all are used up it sleeps to the soonest reset plus 30s, at least a minute and at most ten, saying on stderr what it waits for and until when; an allowance it can't read (an error, or only the last reading kept in its place) is asked again after 1, 2, 4, 8 then every 10 minutes. A provider's accounts switched off or signed out in magpie are left out, as the agent is never moved to them; an account is named by its user, or <provider>/<user> when two subscriptions share it. --timeout <duration> exits 1 when it passes first, --quiet says nothing, an unknown name or flag exits 2 (main now honours an exitError's code) and Ctrl+C 130. The loop takes its reading, clock and sleep as functions: quota_wait_test.go runs it on a fake clock (already available → no sleep; used up → 10m, 10m, then to just past the reset; one account named ignores another's room; a reset under a minute away is a minute; --timeout ends at the deadline after its last look; unreadable usage backs off and the wait goes on once read; an untold reset is looked at every 10 minutes; an off account is passed over; Ctrl+C), plus resolving names and the usage errors. Help in magpie quota's and magpie's usage, and the README. go vet, builds (darwin, GOOS=linux, GOOS=windows), the new tests and ./internal/provider pass; not run against real accounts.
magpie
Every agent's model. One place.
Claude Code on Kimi. Codex on DeepSeek. Gemini CLI on GLM. OpenCode on your ChatGPT plan.
Switch any of them from the menu bar. One local gateway serves them all,
and when a quota runs out, it quietly moves on to the next account.
Download · Docs · Reference · Plugins · Discord · English · 简体中文
45+agents, one list |
4wire APIs, one gateway |
5routing modes |
0config files edited by hand |
Is magpie for you?
Each agent keeps its model in its own file, in its own format, with its own keys and base URLs. A subscription you pay for works in one agent and nowhere else. magpie puts all of that behind one app and one local gateway:
- Pick any model for any agent from one list. magpie edits the one key that matters in the agent's own config.
- Use one provider everywhere: an API key, a local model, or the Claude, ChatGPT, Copilot, Gemini or Grok plan you're signed in to.
- Keep going when a quota runs out: a routing group moves to the next account or model, and the agent never sees the error.
It's for you if you use more than one agent, more than one provider, or more than one account. If you use one agent on its vendor's own plan and that's enough, you don't need it.
Runs on macOS, Windows and Linux (menu bar app, window, TUI, web UI and CLI), in Docker, and on Termux.
Quick start
1 · Install. Download the app from usemagpie.ai, or run:
curl -fsSL https://usemagpie.ai/install.sh | sh
Mac builds are signed and notarised, and every build updates itself. Behind a firewall, use --proxy or --mirror. Or go install github.com/yetone/magpie@latest, or the Docker image.
2 · Add a provider. Open magpie and go to Providers → Add provider. Pick a preset and paste a key, or sign in with a subscription.
3 · Pick a model for each agent on the Agents page. Start a new session and it's on the new model.
Or do it all from the terminal:
magpie provider add deepseek sk-… # a preset needs only the key
magpie claude deepseek/deepseek-v4-pro # Claude Code on DeepSeek
magpie codex moonshot/kimi-k2.5 # Codex on Kimi
magpie group add "Opus anywhere" models=claude/claude-opus-5-5,copilot/claude-opus-5.5 routing=smart
magpie claude group/opus-anywhere # fails over between subscriptions
magpie save work && magpie use work # profiles
magpie quota # what's left on every plan
magpie tui # the whole thing, in a terminal
Supported agents
|
Claude Code · Claude Desktop · Codex · Gemini CLI · Antigravity CLI · OpenCode · OpenChamber · MiMo Code · Pi · oh-my-pi · Aside · OmO · Goose · Cursor CLI · Cursor Private Inference · Zed · VS Code Chat · VS Code Insiders · VSCodium Chat · JetBrains Air · Copilot (JetBrains) · Copilot CLI · Crush · DeepSeek Harness · Reasonix Studio · Command Code · fx · Devin · Hermes Agent · Mister Morph · Kimi Code · Qwen Code · Muse Code · Empryo · MiniMax Code · Droid · Cline · Qoder · Qoder CN · Grok Build · ZCode · WorkBuddy · CodeBuddy Code · Pencil · T3 Code · OpenHanako · AtomCode · Alma · Cindy |
magpie shows only the agents installed on your machine; setup notes for some of them are in the reference. Anything else that takes a base URL can use the gateway too:
export OPENAI_BASE_URL=http://127.0.0.1:3425/v1 OPENAI_API_KEY=magpie
export ANTHROPIC_BASE_URL=http://127.0.0.1:3425 ANTHROPIC_API_KEY=magpie
export GOOGLE_GEMINI_BASE_URL=http://127.0.0.1:3425 GEMINI_API_KEY=magpie
How it fits together
The gateway at 127.0.0.1:3425 speaks OpenAI Chat, OpenAI Responses, Anthropic Messages and Gemini, and translates between them, streaming, tool calls and reasoning included.
flowchart LR
subgraph A["Your agents"]
direction TB
a1["Claude Code"]
a2["Codex"]
a3["Gemini CLI"]
a4["OpenCode · Zed · Cursor CLI · 40+ more"]
end
subgraph M["magpie · 127.0.0.1:3425"]
direction TB
g["Gateway<br/>Chat · Responses · Messages · Gemini"]
r["Routing groups<br/>fail-over · smart · pace"]
p["Plugins & middleware"]
g --> r --> p
end
subgraph P["Your providers"]
direction TB
p1["API keys<br/>DeepSeek · Kimi · GLM · OpenRouter …"]
p2["Subscriptions<br/>Claude · ChatGPT · Copilot · Gemini · Grok"]
p3["Local models<br/>Ollama · LM Studio"]
end
A --> M --> P
What it does
Any model, any agent
Click a value and a searchable list opens: every model of every provider you added, as provider/model. The agent's config is rewritten atomically, and its comments, order and formatting stay as they were. Profiles save every agent's setup under one name ("Budget", "Focus") and switch them all back in one move. Each agent can also have its own short list of models; a profile keeps that list too.
|
|
Providers
Pick a preset, paste a key. Model lists come from the vendor, with names and reasoning levels filled in from models.dev, so a model released this morning is in your picker on the next refresh.
Presets include Anthropic · OpenAI · Google Gemini · DeepSeek · Kimi · Zhipu GLM · MiniMax · StepFun · Qwen · Baidu Qianfan · Tencent Cloud · Huawei Cloud MaaS · Volcengine Ark · Mistral · Groq · xAI · OpenRouter · Together · Fireworks · SiliconFlow · NVIDIA NIM · ModelScope · Ollama · LM Studio … and any OpenAI- or Anthropic-compatible URL.
Import brings in the providers from CC Switch, Claude Code, Codex and Alma. Add to magpie links let a provider's website hand its config to magpie in one click. Each key and each account can go through a proxy of its own.
Subscriptions
An agent you're signed in to is a subscription with models behind it, so magpie offers it as a provider to every other agent. Add several accounts per subscription and magpie fails over between them.
- Claude: drives the real local
claudebinary and bridges your agent's tools over MCP - Codex / ChatGPT: your ChatGPT plan's models, in every other agent
- GitHub Copilot, Gemini (Code Assist), Antigravity, Grok (SuperGrok) and more
- Any OpenCode auth plugin or pi package from npm
Daily warm-up starts each Claude and ChatGPT account's 5-hour window at the times you choose, or the moment the last one resets.
Routing groups
A routing group is several models an agent picks as one, such as group/daily-coding. The gateway spreads requests over every member's keys and accounts:
| Mode | What it does |
|---|---|
smart |
Of the subscriptions with quota left, use the one that renews soonest, so less is wasted at the reset |
order |
Use the first model until it can't answer, then the next |
rotate |
Move to the next member on each turn |
usage |
Use the least-used member first |
pace |
Use the account with the most of its week left per hour until it resets |
A conversation stays with the account that answered it while the vendor's prompt cache is worth keeping. Groups can contain groups. Intent routing has a small model of your choice read each new turn, so tests go to the strong model and quick questions to the cheap one. The Routing view shows every decision live.
Plugins
magpie plugin add opencode-gemini-auth # an OpenCode plugin from npm
magpie plugin add pi-antigravity # a pi package, the same way
magpie plugin login google-plugin # its own sign-in flow, in magpie
Plugins run on Bun, downloaded the first time one needs it. A plugin signs in, lists its models and makes each request; agents use those models like any other provider's. Browse magpie-community/plugins, or write your own.
A plugin can also be gateway middleware: JavaScript that sees what every agent sends and gets back. onRequest can rewrite a request or turn it away, onEvent sees each streamed event, onResponse the whole reply. A hook that throws or runs too long leaves the request as it was.
// alias.middleware.js — magpie plugin add ./alias.middleware.js
export function onRequest(body, ctx) {
if (body.model === "fast") return { ...body, model: "deepseek/deepseek-chat" };
}
Ready-made middleware, most of it what New API does for its channels, with the same JSON
| Package | What it does |
|---|---|
param-override |
New API's param_override: set, delete, move or rewrite request fields, under conditions, or turn a request away |
model-map |
New API's model_mapping: send a model under another name; replies keep the name asked for |
system-prompt |
Your system prompt on every request, or some agents' or models' |
word-guard |
New API's sensitive-word filter: turn away or mask words in what users send, and in replies |
think-tags |
Take <think>…</think> out of replies, or put reasoning_content into them |
magpie plugin add @magpie-community/middleware-model-map
magpie plugin options model-map '{"mapping": {"fast": "deepseek/deepseek-chat"}}'
Find them under Plugins › Discover › Gateway middleware. See Gateway middleware.
Usage and cost
| Tokens and cost | Tokens, cache reads and writes, reasoning and calls, with cost at list price in dollars or yuan; set your own price for any model |
| Quota | Balances and quota windows for every key, plan and account (magpie quota), and a reminder before a window renews with much of it unused |
| Sessions | Every agent conversation with its cost and title, reopened in your terminal with one click |
| Context | How full each request's context window is, how much came from the cache, and what fills it |
| Bills | By account and by upstream key, and OTLP export to your own observability stack |
Sharing
| Local network | Turn on Share on local network and give each client a named gateway key with its own daily, weekly or monthly token and cost limit |
| Remote magpie | A laptop uses the providers, accounts and groups of the magpie on your desktop, and still wires its own agents |
| Docker | Run ghcr.io/yetone/magpie on a server or a NAS and manage it from the web UI |
| Sync | Back up to a file, or sync machines over WebDAV (Nutstore, Nextcloud…) or S3 |
Also
- Library: write instructions, MCP servers and skills once; magpie writes them into each agent's files in its format, and removes only what it wrote.
- Images and video: an image tool for any agent over MCP, with the model you choose, and videos with a Grok subscription.
- Agents kept current: the Agents page shows each CLI's version, updates it the way it was installed, and gives the commands for the ones you don't have.
- WSL: on Windows, agents inside WSL distros are wired too, and their sessions counted.
- Keep awake, light and dark, and your theme's look on Omarchy.
Documentation
| Get started | The guided tour |
| Reference | Every agent, provider option, gateway endpoint, CLI command and file |
| Plugins | Use plugins and write your own |
| Intent routing | Route each turn by what it asks for |
| Import links | "Add to magpie" buttons for provider websites |
Privacy
Your prompts, replies, keys and accounts go only to the providers you use. Once a day a released magpie tells us it is in use: a random id, its version and system, and which agents, providers and models it is used with, by magpie's own ids (a provider you added yourself is only custom), and for each partner listed first in the add sheet, how many times a day it was shown, opened and added (counts only). No names, URLs, accounts, keys, prompts or usage. Turn part or all of it off in Settings → Privacy, or with DO_NOT_TRACK=1. What is sent, exactly.
Community
Questions, ideas, or a model that won't show up? Tell us on Discord. That is where feedback goes.
This repository doesn't take pull requests. Only maintainers can open them. If you'd like a fix or a feature, describe it on Discord and we'll build it.
If magpie saved you from editing one more config file, a ⭐ helps others find it.