Files
magpie/docs/reference.md
T
yetone 83c963c466 gateway: a gateway MAGPIE_ADDR puts on the network asks callers from other machines, a container's host among them, for an enabled gateway key, as sharing does
What changed
- lanGuard: a request whose peer isn't loopback (or that carries a
  forwarding header) is remote. While the gateway is shared, or
  MAGPIE_ADDR names a host other than loopback/localhost (OnNetwork:
  0.0.0.0 in the Docker image, a server's address), a remote request is
  answered only with an enabled gateway key, on every route behind the
  guard; otherwise it is refused (403) as before. Before, a MAGPIE_ADDR on
  the network with sharing off took remote requests with any key.
  A remote request let in with a key is marked as shared-mode ones are,
  so quotas, quota cards, refresh and prices answer it.
  callerGuard follows the same condition. OpenToAnyone is replaced by
  OnNetwork.
- Loopback is unchanged: any key still works on the gateway's own
  machine and inside the container (HEALTHCHECK included).
- CLI keyNote and the console's Connect note no longer describe an open
  gateway; on the network they say other machines need a gateway key
  (`magpie gateway-key add <name>`). The console shows the Gateway keys
  block and network addresses when OnNetwork (gatewayJSON.onNetwork
  replaces open); the "Open to the network" strings are removed.
- Docs: docker.html (en/zh/ja) and remote.html (en/zh/ja) say callers
  outside the container need a gateway key, how to make one, that
  sharing isn't needed for it, and what an upgrade means for agents that
  sent `magpie`; docs/reference.md and providers-accounts.md likewise.
  A MAGPIE_ADDR pinned to one interface's address now asks a key of this
  computer's agents calling that address too (documented).

Tests
- New TestNetworkGatewayGuardsEveryRoute: from 172.17.0.1 and
  192.168.1.9 every registered route (model APIs, count_tokens,
  systemone, the Responses websocket GET, MCP, Codex backend and
  catalog, /v1/magpie/*, images, embeddings, rerank, videos, Gemini)
  gets 401 without a valid key; with one, models and quotas answer 200.
- TestNetworkGatewayNeedsAKey (was TestExplicitOpenGatewayAccepts...),
  TestLANGuard, TestProxiedLoopbackNeedsTheKey, TestQuotasOverLAN,
  TestMCPProxyStaysHere, session route and remote prices tests, TestOnNetwork,
  serve_cli and gui public_url tests updated. With the old lanGuard
  restored, TestNetworkGatewayNeedsAKey, TestNetworkGatewayGuardsEveryRoute,
  TestLANGuard and TestProxiedLoopbackNeedsTheKey fail.

Run
- go test -tags nogui -count=1 ./internal/gateway ./internal/gui
  ./internal/provider . under a temp HOME: ok.
- gofmt -l clean; GOOS=linux and GOOS=windows go vet -tags nogui on
  those packages: clean.
- GUI: public-url and gateway-caller-keys tests, Chromium and WebKit: 22/22.
- Locally built image (OrbStack), -p 127.0.0.1:3481:3425: from the host,
  /v1/models with no key, `magpie` and another made-up key -> 401;
  count_tokens and /mcp/x -> 401; a key from `docker exec ... magpie
  gateway-key add Host` -> models 200, quotas 200; inside the container
  wget /v1/models -> 200; healthcheck exits 0. `web --addr 0.0.0.0:3430`:
  /api/state 401 without the web key, 200 with its cookie.
2026-10-10 09:26:23 +08:00

1956 lines
117 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# magpie reference
The full manual: every agent magpie wires, every provider option, the gateway, routing, plugins, the CLI, Docker, sync and the files magpie keeps. For the overview, see the [README](../README.md).
One place to pick every agent's model: Codex on DeepSeek, Claude Code
on Kimi, Gemini CLI on GLM, from the menu bar. [usemagpie.ai](https://usemagpie.ai)
[![Discord](https://img.shields.io/badge/Discord-join%20the%20community-5865F2?logo=discord&logoColor=white)](https://discord.gg/vGSnD3ZKQF)
`magpie` is a single screen that lists each AI agent on your machine and
the model it is set to. Click a value, pick a model. That is the whole app.
It lives in the menu bar: click the icon and a panel drops down; the same
screen also opens as a normal window (`magpie`, or *Open magpie* in the tray menu),
and there is a terminal version (`magpie tui`) and a plain CLI. The
terminal version serves the gateway while it is open when no other magpie
(the app, `magpie web`, `magpie serve`) does, and says so on its status
line; agents connected to magpie lose it when it quits.
```
◉ magpie
▸ Claude Code claude-fable-5-1[1m] ~/.claude/settings.json
Codex gpt-6-astra effort medium
Gemini CLI gemini-3.1-pro
OpenCode anthropic/claude-sonnet-5 small anthropic/claude-haiku-4-5
MiMo Code anthropic/claude-sonnet-5
Pi openrouter/z-ai/glm-5.2:batch
Goose anthropic/claude-sonnet-5
Cursor auto
Copilot CLI claude-fable-5
↑↓ agent · ←→ field · ↵ change · s save profile · p profiles · q quit
```
- **One binary.** About 30 MB with the desktop app (it uses the system
webview through [Wails](https://wails.io), nothing bundled; a 15 MB
download on a Mac), and under 30 MB for the terminal-only build. macOS,
Linux and Windows.
- **Edits config files surgically.** Only the one key you change is touched;
comments, ordering and indentation in your `settings.json`, `config.toml`,
`opencode.jsonc` or `config.yaml` survive intact. Writes are atomic.
- **One endpoint for every agent.** magpie runs a local gateway that speaks
OpenAI chat completions, OpenAI Responses and the Anthropic Messages API,
and forwards to whichever vendor serves the model. Codex, Claude Code,
OpenCode and the rest all point at `http://127.0.0.1:3425/v1` and pick
from one catalog; the translation between APIs happens in magpie, streaming
and tool calls included.
- **Your subscriptions, shared.** Sign in to Claude Code, Codex (ChatGPT)
or Copilot and that login shows up as a provider: every other agent can
use its models through the gateway, with nothing copied and no key to
paste.
- **Providers with one field.** Pick a preset (Anthropic, OpenAI, Gemini,
DeepSeek, Kimi, GLM, MiniMax, StepFun, Qwen, Baidu Qianfan, Tencent Cloud Token Plan,
Huawei Cloud MaaS, Volcengine Ark, Mistral, Groq, xAI, OpenRouter, Together,
Fireworks, SiliconFlow, NVIDIA NIM, ModelScope, AiHubMix, PipeLLM, 302.AI, Ollama, LM Studio…),
paste a key, done. Custom vendors need a name and a base URL. magpie never
reads keys from your shell environment.
- **Real model lists, nothing compiled in.** With a key in hand magpie asks
the vendor which models it serves and offers exactly those; the
[models.dev](https://models.dev) catalog fills in names, reasoning efforts
and the list for vendors that have none (a model the vendor's own entry
doesn't list is named as the other providers serving it name it, so
glm-5-turbo reads GLM-5-Turbo under Zhipu as under ZCode; an Azure
deployment keeps its own name), and refreshes itself in the
background once it goes stale. Choose which models each provider exposes,
or expose them all — a model released this morning is in the picker on
the next refresh.
- **Each agent's own model list.** Under an agent's name on the Agents page,
"Showing 5 / 32 models" opens its list: click a model to take it out of
that agent's picker (Codex's `/model` included, its ChatGPT models too) or
put it back; other agents still use it, and a new model is shown. Its
"Only models I pick" switch turns the list around: the models ticked
when it is switched on stay, and a model added later, of any provider,
stays out of that agent's picker and its config until it is ticked
(`magpie visible <agent> --only-picked`, back with `--show-new`). A
model asked for by name still works either way.
- **Profiles.** Snapshot every agent's settings under a name and switch all of
them back in one move.
- **Real logos, no framework.** Plain HTML over the system webview; brand
icons from [lobehub/icons](https://github.com/lobehub/lobe-icons).
## Agents
| Agent | File | Fields |
| ------------ | --------------------------------- | --------------- |
| Claude Code | `~/.claude/settings.json` | provider, model, opus/sonnet/haiku/fable, sign-in (through magpie) |
| Claude Desktop | `Claude/` + `Claude-3p/configLibrary/` in `~/Library/Application Support` (`%LOCALAPPDATA%` on Windows, `~/.config` on Linux). A Desktop installed as an MSIX package on Windows keeps them in its package instead (`%LOCALAPPDATA%\Packages\Claude_<publisher>\LocalCache\Local`, its own data in `LocalCache\Roaming\Claude`), and magpie uses those ([`desktopdir`](../internal/desktopdir/desktopdir.go)) | provider (its third-party gateway mode: Code and Cowork on magpie, no Anthropic sign-in; restart Desktop) |
| Codex | `~/.codex/config.toml` | provider, model, effort |
| Gemini CLI | `~/.gemini/settings.json`, `~/.gemini/.env` | auth, model |
| OpenCode | `~/.config/opencode/opencode.json(c)` (`$OPENCODE_CONFIG_DIR`) | model, small |
| OpenChamber | `~/.config/openchamber/preferences.json` (`$OPENCHAMBER_DATA_DIR`; magpie's provider in OpenCode's config) | model, small (its own defaults, over OpenCode's) |
| MiMo Code | `~/.config/mimocode/mimocode.json(c)` | model, small |
| Pi | `~/.pi/agent/settings.json` | model |
| Aside | `~/.aside/u/0/settings.json` (+ `models.json`; the first account, the only one magpie wires) | model, effort, fast, standard, deep, visual, image |
| OmO (omo-ai) | `~/.omo/agent/settings.json` (+ `models.json`; `$OMO_CODING_AGENT_DIR`, `$SENPI_CODING_AGENT_DIR`) | model |
| Goose | `~/.config/goose/config.yaml` | model |
| Cursor CLI | `~/.cursor/cli-config.json` | model |
| Zed | `~/.config/zed/settings.json` (`$XDG_CONFIG_HOME` on Linux, `%APPDATA%\Zed` on Windows) | model (a `magpie` OpenAI-compatible provider; its catalog in Zed's picker) |
| VS Code (Chat) | `~/Library/Application Support/Code/User/settings.json` + `chatLanguageModels.json` (`~/.config/Code/User` on Linux, `%APPDATA%\Code\User` on Windows) | model (`chat.defaultModel`; a `magpie` Custom Endpoint group, its catalog in Chat's model picker; VS Code 1.122+, no Copilot sign-in or key needed). Each profile's own pair under `User/profiles/<id>/` (listed in `globalStorage/storage.json`) gets the same, unless the profile uses the default's |
| VS Code Insiders (Chat) | the same files under `Code - Insiders/User` in place of `Code/User` | as VS Code, a row of its own; its models send the token `magpie-vscode-insiders`, since its chat's User-Agent is VS Code's |
| VSCodium (Chat) | the same files under `VSCodium/User` in place of `Code/User` | as VS Code, a row of its own; its models send the token `magpie-vscodium`, so Usage counts them as VSCodium; its Chat features must be turned on (`chat.disableAIFeatures=false`, and `defaultChatAgent` plus `trustedExtensionAuthAccess` for GitHub.copilot-chat in its product.json). With the Marketplace's GitHub Copilot Chat 0.48.1, which has no Custom Endpoint provider, the group is written for its OpenAI Compatible provider (`customoai`, the token sent as `x-api-key`), which lists models only while Copilot Chat is signed in to GitHub with a personal Copilot plan |
| Copilot (JetBrains) | `byok.json` in `~/.config/github-copilot` (`$XDG_CONFIG_HOME/github-copilot` when set, `%USERPROFILE%\AppData\Local\github-copilot` on Windows), the Copilot language server's own file, which Xcode's and Eclipse's Copilot read too | provider (a `magpie` custom endpoint provider beside the user's: every magpie model at the gateway's `/v1/chat/completions`, sent with `magpie-copilot-jetbrains`; a model hidden in Manage Models stays hidden through a sync; off removes magpie's three keys alone). Found by the `github-copilot-intellij` plugin in a JetBrains IDE or Android Studio. Listed after the IDE restarts, and only while Copilot is signed in to GitHub on a plan with Bring Your Own Key (Free, Pro, Pro+; Business and Enterprise when the organisation allows it); the model is picked in Copilot Chat |
| JetBrains Air | `acp.json` in `~/Library/Application Support/JetBrains/Air` (`~/.config/JetBrains/Air` on Linux, `%APPDATA%\JetBrains\Air` on Windows) + `magpie-opencode.json` beside it | model (a `Magpie` ACP agent: OpenCode's `opencode acp` on magpie's provider alone, its models and routing groups in Air's model menu; needs OpenCode installed) |
| Copilot CLI | `~/.copilot/settings.json` | model |
| Crush | `~/.config/crush/crush.json` | large, small |
| DeepSeek Harness (dsh) | `~/.dsh/profiles/*/cordis.patch.yml` (`$DSH_HOME`; a custom provider, Magpie, its key `MAGPIE_GATEWAY_KEY` in `~/.dsh/.env` and dsh's own key store `~/.dsh/.credentials.yaml`, which the desktop app reads; on one of magpie's models its `web-search-deepseek` row also goes to the gateway, which searches with the model or Settings › Web search, unless dsh has a `DEEPSEEK_API_KEY` or a row of your own), or `~/.dsh/config.yaml` before dsh 0.1.5 | model, effort |
| Reasonix Studio (2.x, or a native Go `reasonix` 2.x / 1.39.x CLI) | `~/.reasonix/config.toml` + `.env` (`%APPDATA%/reasonix` on Windows; `$REASONIX_HOME`) | model (Executor), planner (Plan), effort (a dedicated `magpie` provider, shared by Studio and the native CLI) |
| Command Code | `~/.commandcode/settings.json` (+ `providers.json`) | model |
| fx | `~/.fx/settings.json` | model (a keyless `magpie` provider) |
| omp (oh-my-pi) | `~/.omp/agent/config.yml` (+ `models.yml`) | model |
| Devin | `~/.config/devin/config.json` (`%APPDATA%\devin\config.json` on Windows) | model |
| Hermes Agent | `~/.hermes/config.yaml` (`$HERMES_HOME`) | model |
| Mister Morph | `~/.morph/config.yaml` (`$MISTER_MORPH_CONFIG`) | model, effort (`llm` on the gateway as `openai_response_compatible`, the Responses API; what it had comes back when you switch away) |
| Kimi Code | `~/.kimi/config.toml` (`$KIMI_SHARE_DIR`) | model (a `magpie` provider; magpie's models in Kimi's /model) |
| Qwen Code | `~/.qwen/settings.json` (`$QWEN_HOME`) | model (magpie's models as `modelProviders.openai` entries on a `MAGPIE_QWEN_API_KEY` env var; settings.model; `security.auth.selectedType` openai while wired) |
| Muse Code | `~/.config/muse/settings.json` (`$XDG_CONFIG_HOME`) | model (endpoint_transport to the gateway, auth none; magpie's models in Muse's list) |
| Empryo | `~/.empryo/config.json` | defaultModel (a `magpie` provider at the gateway in `providers`) |
| Ante | `~/.ante/catalog.json` and `settings.json` (`$ANTE_HOME`) | provider, model (a `magpie` provider on OpenAiCompatible, with no auth of magpie's: the gateway lets this machine in with any token and Ante counts a provider with none as authenticated; magpie's models in Ante's model picker, and Ante started on magpie, which is what its settings' `provider` decides) |
| MiniMax Code (mcode) | `~/.minimax/config.yaml` (`$MINIMAX_DATA_DIR`) | model (a `magpie` custom provider; magpie's models in its /model) |
| Droid (Factory) | `~/.factory/settings.json` (`$FACTORY_HOME_OVERRIDE`) | model (magpie's models as BYOK `customModels`, in Droid's /model) |
| Cline (CLI) | `~/.cline/data/settings/providers.json` (`$CLINE_DIR`) | model, effort (magpie takes its openai-compatible provider) |
| Qoder (CLI) | `~/.qoder/settings.json` (`$QODER_CONFIG_DIR`) | model, effort (a `magpie` custom provider; needs a Qoder plan with BYOK) |
| Qoder CN (CLI) | `~/.qoder-cn/settings.json` (`$QODERCN_CONFIG_DIR`) | model, effort (as Qoder; its own accounts, a Qoder CN plan with BYOK) |
| Grok Build | `~/.grok/config.toml` (`$GROK_HOME`) | model, effort |
| ZCode | `~/.zcode/v2/config.json` | provider (magpie's models in ZCode's picker) |
| WorkBuddy | `~/.workbuddy/models.json` (`$WORKBUDDY_CONFIG_DIR`) | provider (magpie's models in WorkBuddy's picker) |
| CodeBuddy Code | `~/.codebuddy/models.json` (`$CODEBUDDY_CONFIG_DIR`), `settings.json` | model (magpie's models in its list; the session model) |
| T3 Code | `~/.t3/userdata/settings.json` (`$T3CODE_HOME/userdata`) | provider (a `magpie` provider instance on Claude Code, magpie's models as its custom models) |
| OpenHanako | `~/.hanako/provider-catalog.json` + `agents/<id>/config.yaml` (`$HANA_HOME`; its local API while it runs) | model (the primary agent's; magpie's models as a provider) |
| AtomCode | `~/.atomcode/config.toml` (`$ATOMCODE_HOME`) | model, effort (a `magpie` provider account, one model table per catalog model as its own sign-in writes) |
| Alma | Alma's local API (`localhost:23001`, while Alma runs; alma-server's data in `$ALMA_DATA_DIR`, `$XDG_DATA_HOME/alma` or `~/.local/share/alma` on Linux) | model (Alma's default; magpie's models as a provider), and Image Generation's model when it is Auto: the one magpie draws with |
Provider-scoped agents (OpenCode, MiMo Code, Pi, OmO, Aside, Goose, Crush, omp, Hermes Agent) take `provider/model`.
Only agents that are installed or configured are shown.
### Notes on some agents
#### Reasonix Studio
Reasonix Studio is detected from its desktop installation or a native Go
`reasonix` 2.x or 1.39.x CLI; historical npm wrappers and a shared config alone
do not count. Tested with
[Studio 2.24.0](https://github.com/esengine/DeepSeek-Reasonix/releases/tag/studio-v2.24.0).
Pick the Executor model in the app, or use
`magpie reasonix magpie/deepseek/deepseek-chat` (`magpie reasonix
magpie/group/code` for a routing group). Select Plan independently with
`magpie reasonix planner magpie/deepseek/deepseek-chat`, or use `magpie reasonix
planner off` to disable the separate planner. `magpie reasonix effort high` sets
an advertised Executor reasoning level. `magpie reasonix default` restores the
previous Executor selection; `magpie reasonix planner default` restores Plan.
Magpie's provider and private `.env` key are removed when neither role needs
them. Restart Studio for new sessions; project/session overrides still take
precedence. Other providers and credentials are preserved. A non-managed
provider named `magpie` is a conflict, reported without overwriting it. Studio
and the native CLI share these files, so updating only Studio does not isolate
their settings.
The released-client stream/tool contract test is run with
`MAGPIE_TEST_REASONIX_CLI=/path/to/reasonix go test ./internal/agent -run
'^TestReasonixStudioCLIIntegration$' -count=1`. It uses isolated settings and
a local test upstream, with no vendor credentials.
#### VSCodium Chat
VSCodium's Chat features are disabled by default. To use the Chat model
picker with magpie, set `"chat.disableAIFeatures": false` in VSCodium's
settings and add the `defaultChatAgent` and `trustedExtensionAuthAccess`
entries required by [VSCodium's Copilot guide](https://github.com/VSCodium/vscodium/blob/master/docs/ext-github-copilot.md)
to VSCodium's `product.json`. The guide also explains how to install a
compatible GitHub Copilot Chat extension, since the Open VSX registry does
not normally provide Microsoft's extension. Restart VSCodium, or run
**Developer: Reload Window**, after changing these files. Magpie's models
then appear under the `magpie` custom endpoint group.
#### Zed-compatible Agent paths
Magpie can use a Zed-compatible fork or installation whose executable or
configuration directory is different from the upstream defaults. Set these
variables before starting Magpie:
```sh
export MAGPIE_ZED_BIN=/Applications/ZedG.app/Contents/MacOS/zedg
export MAGPIE_ZED_CONFIG_DIR="$HOME/.config/zed"
export MAGPIE_ZED_PROCESS_NAMES=zedg,ZedG
magpie serve
```
`MAGPIE_ZED_BIN` controls installation detection, `MAGPIE_ZED_CONFIG_DIR`
selects the directory containing `settings.json`, and
`MAGPIE_ZED_PROCESS_NAMES` supplies comma-separated process names used for
restart notices. The existing Zed configuration adapter is reused, so this is
intended for forks that keep Zed's `settings.json` and Agent model schema, such
as [ZedG](https://github.com/x6nux/zed-globalization). These variables affect
the Magpie process in which they are set; put them in the service environment
when running Magpie under systemd or Docker. They configure a Zed-compatible
Agent on the same machine as Magpie; they do not discover or modify an Agent
running on another host. On macOS, if the configured binary does not resolve to
a `.app` bundle, Magpie falls back to the standard Zed application locations
when authorizing the gateway credential.
## Providers and the gateway
Every model an agent can pick is spelled `provider/model` and served by
magpie's gateway, so agents never hold vendor keys or vendor URLs. Add a
provider, and its models appear in every agent's picker:
```sh
magpie presets # the vendors magpie knows, grouped: vendors, relays, local
magpie provider add deepseek sk-… # a preset needs only the key
magpie provider add ollama # local servers need none
magpie provider add "My Relay" url=https://relay.example.com/v1 key=sk-… models=gpt-5.5,claude-sonnet-5
magpie providers # host, key, exposed models, who uses what
magpie provider deepseek # one provider in detail
magpie provider models deepseek # re-fetch the vendor's list (add ids to choose which to expose)
magpie provider models deepseek +deepseek-v4 -deepseek-chat # expose one more, take one out; the rest stay
magpie provider models deepseek a b c # the whole list of models to expose, replacing it (all: the default)
magpie provider refresh deepseek # re-fetch it, and drop picks it no longer has (the TUI: m)
magpie provider test deepseek # one tiny request per API, with latency
magpie provider key deepseek sk-… # replace the key
magpie provider rm deepseek
magpie models # the catalog agents see
magpie claude deepseek/deepseek-chat # use it
```
Custom providers take `url=` (an OpenAI-compatible base), `anthropic=` (an
Anthropic-compatible base), or both, plus `responses=` when the vendor has a
separate Responses endpoint, `catalog=` to borrow a models.dev list, and
`models=` to name the models to expose. Anything a preset does not know can
be overridden the same way.
A model whose vendor answers that it is deprecated or retired (OpenCode
Zen's 410 "Model exo-free has been deprecated.") leaves that provider's
models in `magpie models`, `/v1/models` and every agent's list, even while
the vendor's own list still names it. It comes back once a request for it
as `provider/model` is answered, or after a week.
`magpie provider set <id> header.<Name>=<value>` sends a header of your own
on every request to a key+URL provider (an empty value removes it; signed-in
accounts ignore them). It replaces a header of the same name magpie would
send, except `anthropic-beta`, which is a list: your betas are added after
the ones the agent asked for that request (Claude Code's, its 1M context's
`context-1m-2025-08-07` for a `[1m]` model, fast mode's), each once. A beta
the provider turns away (`Unexpected value(s) … for the anthropic-beta
header`) is dropped from the retry and from then on, yours as well as the
agent's; any other of yours is always sent. The beta of the agent's own
sign-in (`oauth-2025-04-20`, which Claude Code signed in to claude.ai asks)
never goes, as the sign-in never does; one you set yourself does.
`magpie usage` also lists **upstream provider keys** to help check upstream bills.
Each request records the fingerprint and saved name of the key that actually
served it, including image calls and account/key failover. The CSV adds
`provider_key_id` and `provider_key_name`. JSON uses `providerKeyId` and `providerKeyName`, distinct from gateway caller
keys. System One calls use the same attribution. No raw credential is stored
in usage records.
Rotating the provider's first key does not move old usage to its replacement;
deleted keys keep their historical identity. Older records appear as
**key not recorded**, never inferred from today's configured key.
These are upstream credentials, not keys clients use to call Magpie.
The CSV's `cache_write_tokens` is still every cache write; the last two
columns, `cache_write_5m_tokens` and `cache_write_1h_tokens`, split it by how
long Anthropic keeps it (a record from before the split is all 5-minute).
The app shows the split on a request's Cache and the usage tiles' tooltips.
**Output speed** (tok/s on the Usage and Routing pages, `magpie usage`) is
output tokens over the time after the first token, summed per request: the
tokens of every timed request over the sum of their windows, so concurrent
subagents never add up. A reply that reasoned counts only its answer, from its
first text (`usage.DecodeOf`): hidden reasoning is written before the stream
shows anything, so counting it in a window that starts after it read
gpt-6.1-sol at hundreds of tok/s. A reply that reasoned and then wrote only
tool calls, one not streamed, and a burst (under 100 ms, or over 10,000 tok/s)
tell no speed. So does a reply its vendor held back and let go in a burst at
its end, however long the burst took to come: one whose content came in under
a quarter of its window (`flow_ms`, from a tenth of its bytes to nine tenths,
scaled to the whole), or that reasoned and whose answer would have come over
20 times faster than its reasoning came before its first text (its text came
with the burst). The time a burst takes to come is not the time it took to
write, and that time isn't seen. History is read the same way, as it keeps the
reasoning, the first text and the flow; a record from before the flow was kept
is told by its reasoning's pace alone.
It lists **accounts** too: each Codex, Claude or other subscription account's
tokens and cost, by the account that actually answered — the one that took
over after a failover, the one `X-Magpie-Account` pinned. The account is named
as the Routing view names it (its email or login, never a token); the CSV
adds `provider_account` (JSON `providerAccount`), `magpie usage --csv
--account <name>` keeps one account's calls, and the app's Usage page has an
Accounts list and an Account filter on Requests. An older record names the
account its call went out as when its host says so (`chatgpt.com as
dee@example.com`); otherwise it appears as **account not recorded**, never
inferred from today's sign-in. OTLP export never carries the account.
One magpie can serve several computers (an office one, a personal one):
share it on the network (Settings → Share on local network), and on each
other computer add it as a **Remote magpie** — in the app's Add sheet, on the
TUI's Providers page (`a`, then its address and key; `w` changes the address,
`m` fetches its list again), or
`magpie provider add remote-magpie sk-magpie-… url=http://192.168.1.20:3425 id=office`.
Each computer's own magpie still wires its agents, while the providers,
routing groups (`office/group/…`) and usage are the shared one's. A request
goes on in the API the agent spoke — Anthropic Messages, Responses, Chat
Completions, token counting — and a model the shared magpie's provider serves
on another API only is turned into that API once, never on both computers.
Its list is the models the shared magpie's agents are shown, each named with
its provider there (`Claude Sonnet 5 · Relay A · office`) — the ids stay ids
(`office/relay-a/claude-sonnet-5`), only these labels carry the names.
Its image and video models are discovered automatically and served through
its image generation/edit and video creation/status/content APIs. System One
is also configured automatically at its `/v1/systemone`: refresh the remote
provider's model list to discover its decision models, including Jev, Clef
and models with custom names. These appear as decision models rather than
chat models. A bare provider id such as `office` asks the first listed
decision model; it does not pick another model to satisfy a key's limits.
If the key permits only another model, use its full `office/provider/model`
id to avoid a 403. Embeddings and rerank requests use the remote's corresponding
APIs, and its exposed retrieval models are kept when fetching its list.
These remain in agent model lists as on the remote; being listed does not
mean an embedding or rerank model can hold a conversation.
The remote must serve the requested API and model, and allow the supplied
key to use them. Responses uses HTTP, including SSE, rather than WebSocket.
An older remote without decision-model discovery still serves its existing
APIs; update it and refresh the list to expose System One models here.
Its quotas show too: the Usage page, the menu bar and `magpie quota` list the
shared magpie's cards named with it (`Codex · office`, id `office/codex`), as
that magpie last read them — only the computer holding the sign-ins asks the
vendors, and it answers from what it has kept (`GET /v1/magpie/quotas/cards`).
A card's refresh here has it read that card once more
(`POST /v1/magpie/quotas/refresh`, at most once in 30 seconds a card); until it
has read anything, the remote's one card says so. Their history is shown with
its own. A Codex reset or a check-in is pressed on that computer, not here.
Codex's native image tool first asks the provider that served its conversation
turn for the requested image model. If that provider does not list the model,
or the turn cannot be identified, it uses Settings → Images → Image generation
(including Automatic). Off disables this fallback, not a matching model on the
turn's provider. An explicit `provider/model` keeps its destination; an upstream
image error does not retry through the setting. Magpie Image MCP is not required.
WorkBuddy (China)'s daily check-in (签到) is pressed from the app's Usage card
(*Check in now*), `magpie accounts checkin`, or `c` on the TUI's Usage page,
which checks in every account signed in on this computer at once; its
WorkBuddy lines say how today's went. A shared magpie's accounts are checked
in on that magpie, from its own app, TUI or CLI.
A plugin whose `auth` hook has `checkin` checks its own accounts in the same
way: once a day while its switch under **Settings → Usage → Plugins** is on,
with the result on each account's Usage card. A check-in the vendor wants a
captcha for is shown as such (check in in the vendor's app); magpie never
solves captchas. See [provider-plugins.md](subsystems/provider-plugins.md#a-plugins-daily-check-in).
The gateway issues **gateway keys** for clients, separate from a provider's
upstream API keys. Turn on **Settings → Share on local network**, then open
**Gateway → Gateway keys → Add gateway key**. This block appears only while
sharing is on. Create a named key for each client and copy it from its row.
To move from another gateway without touching its clients, enter the key they
already send in **Your own key (optional)** (8–256 characters, no spaces, not
starting with `sk-magpie-`, unlike any other key); left empty, magpie makes
one. The list shows only its last four characters, and it is counted,
limited and restricted as any other key, from this computer too.
Rename, disable, rotate or remove keys independently; rotation and removal
ask for confirmation. Rotation keeps the name, enabled state and usage
history; other keys are unchanged. While sharing is on, **Gateway → Connect**
offers loopback and shared addresses, plus enabled gateway keys, for all
connection examples. With sharing off, Connect keeps the original **API key**
field and local `magpie` token, without a gateway-key picker. Its arbitrary
local option is **This computer**, distinct from the named **Magpie** key.
For a headless gateway, use the CLI before exposing the port:
```sh
magpie gateway-key add "Remote laptop" # prints the new credential once
magpie gateway-key add Family --key - # keeps a key clients already send, read from stdin
magpie gateway-key list # ids, names, enabled state and masked keys
magpie gateway-key rotate <id> # prints the replacement; identity stays the same
magpie gateway-key remove <id> # revokes remote access
magpie gateway-key limit <id> week --tokens 2m --cost 5 # its own limit
magpie gateway-key limit <id> # limit, used, left and reset
magpie gateway-key limit <id> off # no limit
magpie gateway-key models <id> openai/gpt-5 anthropic/* # only these models
magpie gateway-key models <id> all # every model
magpie gateway-key accounts <id> codex/me@example.com # only these accounts
magpie gateway-key accounts <id> all # every account
```
Each gateway key can have its own **limit**: a token total, an estimated
cost in US$, or both, per day, week or month (calendar windows in local
time: from midnight, from Monday, from the 1st). Set it with **Limit** on
the key's row (saved with Save) or `magpie gateway-key limit`; the row shows
what the key has used, what is left and when it resets. Tokens counted are a
call's uncached input, output and cache writes, plus cache reads when **Count
cache reads too** is on. Cost is an estimate at the Usage page's prices; a
call with no known price adds none. A call counts in the window it started
in. Once a key is spent, its requests are refused before any provider is
asked, with a 429 in the API's own error shape that names the key, the
limit and the reset time, plus `Retry-After`; other keys are unaffected. The
counts are read from the usage log, so they survive a restart. A request in
flight holds a reservation (its body's size in tokens plus the key's mean
output per call), so requests sent at once overshoot by about one call; a
streamed reply is settled when it ends with the usage its vendor reported.
A key can read its own status with `GET /v1/magpie/limit`. Requests from
this computer that send no gateway key are not limited; a gateway key used
from this computer is.
A gateway key can also be held to some **models** (#882): pick them with the
**All models** badge on the key's row, or run `magpie gateway-key models <id>
openai/gpt-5 anthropic/*` (`all` takes the restriction off). A pattern is
`<provider>/<model>` or `<provider>/*`, matched against the provider that
serves the call, so a bare model name is resolved first, or a routing group,
`group/<id>` (`group/*` for every group). A key that names a group may use
the group with every member in it, though not those members asked for by
name. Such a key sees only its models in `/v1/models`, `/v1/codex/models`
and the Anthropic and Gemini lists, a routing group it doesn't name only when it may use every
member, and is refused any other model with a 403 in the API's error shape
before a provider is asked; a fallback it may not use is skipped. A key with
no models listed may use every model.
A gateway key can also be held to some **accounts** (#905): the accounts
and keys its requests may use, picked in the same menu right after their
provider's models — an account by who is signed in, a key by its
fingerprint — or with
`magpie gateway-key accounts <id> codex/me@example.com openai/<key id>`
(`all` takes the restriction off). The list holds a key to some accounts
**of the providers it names**: a provider it names no account of, the key
uses as it always did. The list keeps an account by its stable id, kept
through renames, shown by who is signed in; one gone later — a key
rotated, an account signed out, an account of an agent that keeps no
logins renamed — is kept as it is, matching nothing, so the key is held
closer, never wider. A routing group the key names is its members'
through the accounts it may use only: naming a group is not naming its
accounts, and magpie's own calls for the key — a web search, a picture
described, a Codex title — are of the models the user picked, but their
spend lands on an account, so the accounts hold them too. When every
account or key behind a model 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 it may use, before a provider is asked — after a usage cap's 429
and before an account barred for the model's — and `/v1/models` lists the
key only the models an account or key it may use serves, the Routing
view's left-out list marking what the key held out. A key with no
accounts listed may use every account, as keys always did.
While LAN sharing is enabled, or `MAGPIE_ADDR` puts the gateway on the
network (a host other than loopback or `localhost`: `0.0.0.0` in the Docker
image, a server's address), remote requests require an enabled gateway key
sent as Bearer, `x-api-key`, `x-goog-api-key` or `?key=`; one without gets
401. A request is remote when its peer isn't loopback, which includes
Docker's port publishing (the peer is the bridge, not the host's loopback)
and a WSL distro under NAT. Loopback remains permissive: any token works,
including a stale or disabled gateway key. Only a valid, enabled key is
attributed to its named identity. Neither shared nor on the network, a
remote request is refused (403). A `MAGPIE_ADDR` pinned to one interface's
address asks a key even of this computer's agents calling that address;
`0.0.0.0` with agents on `127.0.0.1` doesn't.
Sharing listens on every interface, unless `MAGPIE_ADDR` names a host of its
own: `MAGPIE_ADDR=127.0.0.1:3425` behind Tailscale Serve, or one interface's
address, stays where it is while shared, and what reaches it from elsewhere
still needs an enabled gateway key (#1112).
**Codex on another computer** (#1281) can name the shared magpie its model
provider, with a gateway key, and read magpie's models in Codex's own
catalog shape from `GET /v1/codex/models`. `/v1/models` is OpenAI's list,
which Codex can't read, and `/backend-api/codex/models` is the ChatGPT
backend's, for a Codex signed in to ChatGPT on the gateway's own computer;
a gateway key gets a 401 there.
```toml
model_provider = "magpie"
model = "provider/model"
[model_providers.magpie]
name = "magpie"
base_url = "http://192.168.1.20:3425/v1"
model_catalog_url = "http://192.168.1.20:3425/v1/codex/models"
env_key = "MAGPIE_API_KEY" # the gateway key
wire_api = "responses"
supports_websockets = false
```
The list is the models the shared magpie shows Codex (its Agents page
picks), each as the `model_catalog_json` magpie writes for a Codex on its
own computer describes it, and only the models the key may use. Codex 0.161
reads it by default; 0.160 needs `[features] api_key_model_discovery = true`.
Codex reads at most 1 MiB of a catalog, and every entry carries Codex's
prompt, so past about 125 models the list keeps the first that fit and the
answer's `X-Magpie-Left-Out` header says how many it left out: pick fewer
for Codex, or hold the key to fewer.
A request that reaches loopback through a proxy or tunnel on this computer
(Cloudflare Tunnel's `cloudflared`, ngrok, Tailscale serve or funnel, frp's
HTTP proxy, Caddy, nginx) is someone else's, and is answered as one from
another machine (#1022): it carries a forwarding header (`Forwarded`,
`X-Forwarded-For`, `X-Real-IP`, `Cf-Connecting-IP`, `True-Client-IP`,
`Tailscale-User-Login`), which no agent sends. Without sharing it is refused;
with sharing, or a `MAGPIE_ADDR` on the network, it needs an enabled gateway
key, as from the network. This
covers every route: the model APIs, quotas, MCP sign-ins. To serve magpie
through a Cloudflare Tunnel, turn on **Settings → Share on local network**,
add a gateway key for each client, point the tunnel's service at
`http://127.0.0.1:3425`, and give clients the tunnel's address with that key.
A tunnel that forwards bare TCP (frp's `tcp`, `ssh -R`, socat) adds no header
and can't be told from an agent here: point it at this computer's LAN
address with sharing on, not at loopback. A proxy here that signs its
clients in itself can be trusted with `MAGPIE_TRUST_PROXY=1`, which keeps
what it forwards local, as before.
**Usage → Overview → Gateway keys** groups calls by the client's key, never
the provider's credential. **Usage → Requests** offers the same filter;
CSV includes `caller_key_id` and `caller_key_name`. Deleted keys keep their
history. The usual local `magpie` token and older records stay unattributed.
Caller attribution includes chat, images, video creation and System One.
Provider attribution stays independent; CSV puts `provider_key_*` before
`caller_key_*`.
An existing LAN key becomes **Magpie** without changing the credential.
`lanKey` remains in settings for older Magpie versions. Disabling or removing
the default key replaces that mirror with a random non-empty revoked value;
rotation does not re-enable it. The key store records migration completion,
even if `lanKeyId` cannot be saved, so reads do not keep retrying that write.
A migration write failure is logged without preventing gateway startup,
CLI key management, or the Settings and key-list pages from opening.
If settings are read-only, changing the default key fails without changing
it: make `settings.json` writable and retry so older versions cannot keep
accepting its old credential. Independent named keys remain manageable.
Gateway credentials stay in `~/.config/magpie/caller-keys.json` (XDG-aware,
mode `0600`), never in usage records or list responses.
Baidu Qianfan's [Token Plans](https://cloud.baidu.com/doc/qianfan/s/Dmrabu8b6)
are available as `baidu-qianfan`: a personal (个人版) and an enterprise (企业版)
plan and pay as you go, each with its own Chat Completions, Responses and
Anthropic Messages endpoints, and a key that works only on its own plan. Add
it with `magpie provider add baidu-qianfan <api-key>` — the id it carried its
first day, `qianfan-token-plan`, is taken too. The plans serve no model list,
so the preset carries their documented models; pay as you go serves its own
at `/v2/models`.
### Plugins
A subscription magpie doesn't sign in to itself can come from an
[OpenCode](https://opencode.ai) provider plugin or a
[pi](https://github.com/earendil-works/pi) package: the npm packages OpenCode
users install to sign in to a plan (their `auth` hook), and the pi packages
that register a provider (`pi.registerProvider`), work in magpie as they do
there. magpie runs them on [Bun](https://bun.sh), downloaded the first
time a plugin needs it, and the plugin signs in, lists the models and makes
each request; magpie serves them to agents like any provider's.
```sh
magpie plugin add opencode-gemini-auth # an npm package, or a path to a plugin of your own
magpie plugin add pi-antigravity # a pi package, the same way
magpie plugin # the plugins, what each signs in to, and whether you are
magpie plugin login google-plugin # its sign-in: the method, its questions, the browser or a key
magpie plugin logout google-plugin
magpie plugin off opencode-gemini-auth # on brings it back; rm removes it; update updates them all
```
A provider id magpie already has (google, openai, anthropic) is
`<id>-plugin`. In the app, Settings → Plugins adds and removes them, and
the providers they sign in to are in Add provider → From plugins.
Sometimes two installed plugins sign in to the same provider id, for
example a third-party plugin and your own copy of it. One of them serves
that id: the one you picked, or else the one added last. Both plugins stay
listed, and each row in Plugins › Installed says which one serves the
provider. The other row has **Use for <provider>**, which switches the
provider to it. `magpie plugin` prints the same thing, and
```sh
magpie plugin use ~/dev/my-acme-auth acme # this plugin serves acme from now on
```
makes the pick. Each plugin can still be switched off or removed on its
own.
A package is pi's when its `package.json` has a `pi` manifest, the
`pi-package` keyword, or depends on `@earendil-works/pi-coding-agent`.
magpie installs pi beside it and loads it with pi's own loader; the
providers it registers are what magpie uses, and its commands, tools and
renderers are left alone. Its sign-in runs as in pi: an OAuth login's
questions and pages, and the dialogs and pickers it shows (`ctx.ui`, its
terminal components too), are asked in magpie's sign-in; an API key works
as well. pi's own files (its `auth.json` and what a package keeps) are in
`pi/` under magpie's config directory, not your `~/.pi`, unless
`PI_CODING_AGENT_DIR` says otherwise. Requests reach the package as pi
sends them, and its replies keep their thinking and tool calls.
#### For plugin authors
A plugin is an OpenCode plugin or a pi package; nothing magpie-specific is
needed. For an OpenCode plugin magpie reads a few more fields, which
OpenCode ignores:
- **The provider's icon**: `icon` on the `auth` hook, or `"magpie": {
"icon": "…" }` in the plugin's `package.json` (for every provider it
signs in to that names none). An `https://` URL of a picture on a public
host, which magpie fetches once and keeps, or a `data:image/…` URI;
PNG, JPEG, GIF, WebP, ICO or SVG, at most 1 MB. Anything else is ignored,
and the icon the plugin market lists for the plugin is shown instead.
The `package.json` icon is also the card's picture under Discover →
Unofficial, on GitHub for a repository tagged `magpie-plugin`.
- **An API key's field**: a `type: "api"` method's `label` titles the key's
field, as OpenCode's dialog does (one that only says "API key" reads
"<provider> API key"), and its `placeholder` is the hint inside the field
(and after the question in `magpie plugin login`). The method's `prompts`
are asked first, as in OpenCode, and reach `authorize(inputs)`.
```js
export const LemonPlugin = async () => ({
auth: {
provider: "lemon",
icon: "https://lemon.example/icon.png", // or "data:image/svg+xml;base64,…"
methods: [
{ type: "api", label: "Lemon API key (lemon.example/keys)", placeholder: "sk-lemon-…" },
],
},
})
```
In TypeScript, `icon` and `placeholder` aren't in OpenCode's types: build
the hook as a variable (or cast it), or put the icon in `package.json`.
#### Gateway middleware
A plugin can also be gateway middleware: `onRequest(body, ctx)` sees each
request an agent sends before it is routed, `onEvent(event, ctx)` each
event of a streamed reply and `onResponse(body, ctx)` a whole one, all in
the agent's own API. A hook returns the changed object, or nothing to
leave it; `onEvent` returns `null` to drop an event, and
`ctx.reject(status, message)` turns a request away. Declare it with
`"magpie": { "middleware": "./mw.js" }` in `package.json`, or add a single
`*.middleware.js` file:
```js
// alias.middleware.js — magpie plugin add ./alias.middleware.js
export const events = ["message_start"]
export function onRequest(body, ctx) {
ctx.state.asked = body.model
if (body.model === "fast") body.model = "deepseek/deepseek-chat"
return body
}
export function onEvent(ev, ctx) {
ev.message.model = ctx.state.asked // the agent sees the name it asked for
return ev
}
```
Middleware runs inside the gateway in [moejs](https://github.com/Calcium-Ion/moejs),
a JavaScript engine written in Go (about a microsecond a streamed event),
not on Bun: no timers, `fetch` or Node APIs, and imports only of files
beside it. A hook that throws or takes too long (250 ms a request, 50 ms an
event) leaves what it was given as it was. Settings → Plugins shows each
middleware's calls, time and failures. The [plugin guide](https://usemagpie.ai/docs/plugins#middleware)
has the rest.
`ctx.options` is the middleware's entry's `options` in `plugins.json`: set
them with its **Options** button in Plugins › Installed, or
```sh
magpie plugin options model-map '{"mapping": {"fast": "deepseek/deepseek-chat"}}'
magpie plugin options model-map off # clears them
```
The name can be a package's short name (`model-map` for
`@magpie-community/middleware-model-map`). A package's
`"magpie": { "options": {…} }` is the example the editor starts from.
Ready-made middleware, under Plugins › Discover › Gateway middleware
(`@magpie-community/middleware-<name>`), mostly New API's channel settings
with the same JSON: `param-override` (`param_override`), `model-map`
(`model_mapping`), `system-prompt`, `word-guard` (sensitive words) and
`think-tags` (strip `<think>…</think>`, or `thinking_to_content`). A
middleware's entry in the community `registry.json` has
`"kind": "middleware"` and no `providers`.
### What a model costs
A call is counted at its **effective price**: what you set for that provider
and model if you did, otherwise what the provider's own catalogue lists, and
otherwise what models.dev lists for the model's maker. Out of the box that
last one is the whole story, and it is the wrong number for any provider that
does not charge list price — a relay reselling at a discount or a multiplier
is counted at whatever the model's maker charges. Say what a provider
actually charges, and the usage ledger and the session totals use that:
```sh
magpie model price relay-a/gpt-5.5 # what it is counted at, and where that came from
magpie model price relay-a/gpt-5.5 0.12,0.60,0.01,0.15 # input,output,cache read,cache write
magpie model price relay-a/gpt-5.5 --reset # take your price off this model
magpie model prices # every model you priced
```
The four numbers are USD per million tokens. All four are asked for, because
a price missing one would understate the rest of every call; `0` is a model
served at no cost, which is a price, not the absence of one. Decimals take a
point or, in the app's boxes, a comma (`0,25`).
A **Kimi Code** membership's models are counted at the Kimi API model each
one is, not at the $0 models.dev lists them at for the plan: `k3` and
`k3-256k` at `kimi-k3`, `kimi-for-coding-highspeed` at
`kimi-k2.7-code-highspeed`. `kimi-for-coding` is K2.8 Preview, which the API
doesn't sell, so it has no price until you give it one
(`magpie model price kimi-code/kimi-for-coding …`).
A **fifth number** is a 1-hour cache write's price. Anthropic bills a cache
write kept for 5 minutes at 1.25× input and one kept for an hour at 2× input,
and its usage says which were which (`cache_creation.ephemeral_5m_input_tokens`
/ `ephemeral_1h_input_tokens`, from the API and from Claude Code's
transcripts). The fourth number is the 5-minute price; the fifth, when not
given, is 2× input for a Claude model — Anthropic's rule. Any other model has
no 1-hour price unless you give one: a 1-hour write it reports is counted at
the 5-minute price, and neither `magpie model price` nor the app's boxes show
a 1-hour price for it. A call recorded
before magpie kept the split counts all its writes at the 5-minute price, as
it did.
A **long-context price** is what the whole request costs once its prompt is
over a size, as OpenAI bills gpt-6-astra, gpt-6.1-sol and gpt-6-luna over
272K input (2× input and cache, 1.5× output):
```sh
magpie model price openai/gpt-6-astra 10,50,1,12.5 --tier 272k 20,75,2,25
```
The size counts the prompt the way OpenAI does: uncached input + cache reads
+ cache writes, and the tier applies when that is **over** the size (272,000
is not, 272,001 is). Then every part of the call — input, output, cache — is
at the tier's price, not only the tokens past the size. `--tier` may be given
again for another size. The built-in prices carry a tier only where
models.dev lists one (`cost.tiers` of type `context`), as it does for those
three. Session and day totals sum many calls, so they are counted at the base
price; the ledger and each request are counted at the tier they reached. In
the app, a model's price row has a quiet **Long-context price** row under it,
shown with the list's tier greyed in, or behind a link where there is none.
The order a price is looked for in is: **the price for this model → the price
for `<provider id>/*`, which covers every model of that provider → what the
provider's own catalogue lists → what models.dev lists for the maker.**
`--reset` removes the first, and says so when a `<provider id>/*` price is
still in force; reset that one by name to take it away too.
A price is **one provider's tariff for one model**, not the model's own: the
same model through two providers is two prices, and each keeps its own.
Nothing an agent can see changes. The model list, the agents' own settings
and the pickers that choose a model for a background task — an image, a web
search, a description — all still work from the catalogue; only the cost
reports read the effective price.
Two things worth knowing. The ledger and the session totals re-price when they
are read, so adding or changing a price restates earlier figures: they are
estimates at the effective price, not settled charges. And a price is per
provider and model. Records now identify upstream API keys, but a provider
charging different tariffs per key still cannot be costed exactly from a
single provider-wide price.
### What a model takes
A provider that serves a model models.dev does not list, or lists at the
wrong size, has a window and a reply limit magpie cannot know. Say what they
are:
```sh
magpie model context relay-a/gpt-5.5 262144 # the window a request may hold, or 1m
magpie model output relay-a/gpt-5.5 131072 # the most a reply may hold, or 128k
magpie model context "relay-a/*" 200000 # every model of that provider
magpie model output relay-a/gpt-5.5 --reset # take your limit off this model
```
The `*` is quoted because zsh treats a name it cannot expand as a command
that failed, rather than passing the name on as bash does.
Both are looked for in this order: **this model → this provider's `*` → the
provider's own list → models.dev.** `--reset` removes only the value this
model has of its own.
A provider you keep unlisted, or switch off, takes them like any other: the
numbers are kept, and are what its models take once it is serving again.
A window is a number agents are shown **and a routing input**: at 95% of the
window a request held on a routing-group member moves to one that takes more,
so overstating a window makes that move happen too late. A reply limit is
advertised in `/models` and is what a group advertises the smallest of; the
gateway does not itself cap a reply by it.
Saving a window or a reply limit writes that number into the model lists magpie
keeps in the agents' own files — Pi's `contextWindow` and `maxTokens`, OpenCode's
`limit`, and Crush's, droid's, Cline's, Qoder's and Zcode's — which an agent reads
at start-up. A session already running therefore keeps the window it began with,
while the gateway's own `/models` and every request from then on are right at
once.
### The name a vendor knows a model by
A relay often serves a model under an id of its own — a prefix it namespaces
with, a dated name, a `-latest` that is not what models.dev calls it. Say
which name to ask for:
```sh
magpie model wire relay-b/model-2 # what the vendor is asked for
magpie model wire relay-b/model-2 vendor-c/model-2-preview # ask for it by this
magpie model wire 'relay-b/*' 'vendor-c/*' # every model, * being the model
magpie model wire relay-b/model-2 --reset # ask for it by its own name again
magpie model wires # every name your vendors are asked for models by
```
Quote the arguments with a `*` in them: zsh reads a bare `*` as a glob and
answers `no matches found`. In the name, `*` stands for the model itself, so
`'relay-b/*' 'vendor-c/*'` sends `vendor-c/model-3` for `model-3` and
`vendor-c/model-2` for `model-2` — one name for a relay that namespaces its
models, each still asked for by its own. A name with no `*` in it sends every
model of that key under that one name, which is the right answer only for a
relay that does serve them all alike. The model's own key wins over the
provider's, and `--reset` takes away only the one it is given, so resetting
`relay-b/model-2` while `'relay-b/*'` is set leaves the provider's name in
force — the CLI says which of the two is in force after every change, naming
the models a name for the whole provider leaves to their own, and
`--reset` over a name that was never given says so rather than ticking a
removal that took nothing away.
Only the request that goes out carries that name, in the model field of a
chat, Responses or Anthropic Messages request and of an Anthropic token
count; image requests are left with the name magpie knows the model by.
Gemini CLI and Antigravity sign-ins go on Code Assist, and there the model
an effort picks is a variant of the model magpie knows — `gemini-3.7-flash`
at `high` is sent as `gemini-3.7-flash-high` — so a `*` in the name is that
variant: `'antigravity/*' 'vendor-c/*'` asks for `vendor-c/gemini-3.7-flash-high`
and `vendor-c/gemini-3.7-flash-low` each by its own, as it does everywhere
else. A name with no `*` in it, or one given for `antigravity/gemini-3.7-flash`
itself, is that one name at every level.
Everything else keeps the name magpie knows the model by: the catalog agents
pick from, the routing groups' membership, `GET /models`, and what a call is
recorded and priced as. What the vendor's own reply said answered is kept
beside that, in the ledger's `served_model`, which is what it is for, and it
is compared with the name the vendor was asked for — so a model answered
under the relay's own id is not read as a swap. That is against the names
in force when the ledger is read, since a record keeps what the vendor
answered and not the name the request went out under: naming a model after
the call re-judges that call, which is then left reading as a swap. What a
provider *supports* — whether it takes a temperature, which reasoning levels
fit — is still asked about the model magpie knows, so a rename upstream does
not change how magpie behaves towards the model.
An upstream name is **one provider's**, not the model's: another provider
serving the same id is asked for it under its own name, or this one. The
model it is given for has to be one that provider serves — `<provider>/*`
is the way to say one for every model — because a name for a model magpie
would never ask the provider for is not a name of its own: the model is
asked for by the name magpie knows it by, and nothing anywhere would say the
name given for it is not the one in force. A relay that serves a model under
an id of its own *and* lists it does get the name; what is refused is a
model the provider's list does not have at all. The model test uses the name
too, so a relay that only knows its own ids does not report a working model
as broken. An image model is the exception that follows the rule above: it is
tested by the name magpie knows it by, on the images API and on the chat it
falls back to, because that is how a drawing is asked for.
### Routing groups
A routing group is several models, from one provider or many, that an agent
picks as one: `group/<id>`. The gateway routes each request over every
member's keys and accounts together. A model two of your providers serve
under the same name becomes a group on its own; the Routing view in the app
and `magpie group` make any other:
```sh
magpie groups # yours, then those magpie found
magpie group add "Opus anywhere" models=claude/claude-opus-5-5,copilot/claude-opus-5.5 routing=order stays=session
magpie group opus-anywhere # one group, its models in order
magpie group set opus-anywhere models+=openrouter/anthropic/claude-opus-5.5 routing=usage
magpie group set opus-anywhere models-=copilot/claude-opus-5.5
magpie group rm opus-anywhere # one magpie found is hidden; magpie group restore <id> brings it back
magpie claude group/opus-anywhere # use it
```
`routing=` is `smart` (the default: of the subscriptions with quota to
spare, the one whose allowance renews soonest first), `order` (the first
model until it can't answer, then the next), `rotate` (each turn to the next
member), `usage` (least used first) or `pace` (weekly pace: the account with
the most of its week left per hour until it renews first, so less of a week
is lost at its reset). A key has no allowance to weigh, so it goes by its
order or by what magpie sent it lately — except a sub2api key its owner gave
a 5-hour, day or 7-day limit: with the provider's Balance URL set to the
relay's `/v1/usage`, its card shows those windows and routing weighs it by
them as it does a subscription, resting it till the window renews once the
relay says `api key 7天限额已用完` — or till its windows are next read short
of full, its limit raised or its usage reset. A key bought as a plan — Kimi Code, GLM Coding Plan
or Z.ai, MiniMax Coding Plan, OpenCode Go, Command Code — is weighed by its
plan's windows the same way. With one key or account, the Routing page still
shows what it has left. `stays=` is how long a conversation
stays with the key or account that answered it: `auto` (the default, while
the vendor's cache of it is worth keeping), `session`, `turn` or `off`.
`models=` replaces the whole list, in order; a bare model id works when only
one provider serves it.
The Routing page's Requests list defaults to the time-ordered By request view.
Choose By session to group calls by the agent's session ID; the page remembers
your choice across reloads.
Codex title helpers with an explicit parent or fork source join their originating
chat, retaining their title badge and contributing to its cost. Titles without
ancestry and ordinary forked chats stay separate.
Codex chat names come from its local name index and follow renames. Other
agents' sessions (Claude Code and the rest the Sessions page reads) take the
name the Sessions page shows for them, read from their own files on this
computer. Unknown or remote-only names fall back to the ID; the full ID
remains in the heading tooltip.
Expand a session to see each request. Each request and session shows its estimated cost at the effective model
prices, including cache reads and writes. Session totals cover the listed
requests only (the live trace or the selected day's retained history), and a
`+` marks a partial estimate. Calls without a session ID are listed separately;
old history without token tiers, or a model without a known price, shows `—`.
Opening the Providers page automatically checks Alma, CC Switch, Claude Code
and Codex for local provider configurations that can be imported. With no
providers yet, a hint shows how many were found and which apps they came from;
otherwise a small **Import local configurations** link sits beside **Add
provider**. Both open the existing picker with only the offered configurations
selected; nothing is added until you confirm the selection. **Ignore** remembers
the offered configurations on this device across window restarts; new or changed
configurations can be offered again. **Add provider → Import…** still includes ignored configurations.
Discovery runs in the background, at most once a minute when opening or
refreshing the page, and refreshes after an import. Configurations already in
magpie, entries that cannot be imported, providers turned off in their source
app, and ID collisions that cannot join as another key are excluded from the
hint. The latter two remain available through manual import. Counts refer to
configurations in source apps, which may include the same provider
in more than one app. Only app names and opaque configuration fingerprints
reach the automatic hint; ignored fingerprints are kept in local browser storage.
The app's Import from other apps dialog can copy providers from Claude Code's
`settings.json` (`CLAUDE_CONFIG_DIR` when set) and Codex's `config.toml`
(`CODEX_HOME` when set) into magpie. Codex imports custom
`[model_providers.*]` entries with an inline `experimental_bearer_token`,
including fixed headers for custom providers in
`[model_providers.*.http_headers]` and models from
`[profiles.*]` or `model_catalog_json`. Review the entries before importing;
subsequent changes to agent settings are not automatically copied to magpie.
Entries that point back to magpie or only name an `env_key` are skipped.
### How magpie picks an account
When a subscription has several accounts on (Claude Code's or Codex's
saved accounts, say), or a routing group has several members, the gateway
orders them for each request in these steps. The Routing page shows the
result for every request: who went first, and why.
1. **Who can answer.** The account the agent is signed in to, unless it
is paused, and the other accounts that are ticked. An account set not
to serve the model, or held at its usage cap, is never tried. An account whose plan doesn't list the model (a Free one behind a
Plus) is tried only when no other lists it. A Claude account that has to
be signed in again stays in the list, but is passed over when its turn
comes, without resting.
2. **The routing.** What is left is put in order by the routing the
subscription or group uses (below).
3. **Rate limited ones last**, only with *Sink* on: an account a 429 rate
limited while it still had quota goes behind every one not rate limited
since.
4. **The provider's fallback models** come after its own accounts.
5. **Resting ones last.** An account resting after a failure goes to the
back, behind every fallback. It is never dropped: when it is the only
one, it is tried anyway.
6. **The conversation's account first.** Once an account has answered a
conversation, it is moved to the front for the conversation's next
requests, as *Stays* says. In auto mode that is within a turn, and
across turns while at least 1,024 tokens of the last request were read
from the vendor's cache and it answered in the last 5 minutes. This
never brings back an account that is resting, nor one that has used 98%
of a window (100% In order).
So routing decides who goes first only for a conversation nobody has
answered yet. An account that has used 90% goes behind the others for new
conversations, but a conversation already on it stays there until 98%.
**The routings.** Smart, Least used first and Weekly pace go by the share
used of the fullest window that counts the model, the 5-hour window
included. Smart and Weekly pace put an account behind the others once a
window is at 90% or more, and behind those once one is at 98% or more,
each of these groups ordered by the share used.
- **Smart** (the default): of the accounts under 90%, the one whose
allowance renews soonest goes first, because what it has left is lost at
the reset. The biggest window decides: the week, then the 5 hours only
when the weeks renew in the same hour. Reset times are compared to the
hour, by the clock: 13:55 and 14:05 are different hours, 14:05 and
14:55 the same. An account
with no week (Claude Enterprise) goes by its 5 hours. A Claude account
magpie knows nothing of yet goes first once, to learn what it has left
from the answer; any other unknown account goes after those known.
- **In order**: the accounts in their order. The first takes every request
until it is used up (100%) or fails.
- **In turn**: each turn of a conversation goes to the next account. A new
conversation goes to the account the fewest other conversations of the
last 30 minutes are on.
- **Least used first**: the lowest share used first; then the account
magpie sent the fewest tokens lately (half of them stop counting after an
hour). An account not known counts as unused.
- **Weekly pace**: the account with the highest pace first. Pace is the
share of the week left divided by the hours until the week renews (at
least 1): 60% left with 20 hours to go is 3 per hour. With two weekly
windows that count the model (Opus's own and the general one), the lower
pace counts. An account with no week goes by its 5 hours. Accounts whose
pace is within a tenth of the highest in their band count as alike, and
among them the one magpie sent the fewest tokens lately goes first. A
Claude account not known goes first once, as in Smart; another unknown
account counts as a fresh week.
**Make first** sets the accounts' order. How much that order counts
depends on the routing:
| Routing | What the order decides |
| --- | --- |
| In order | Everything: the first takes every request it can. |
| In turn | Where the rotation goes next. |
| Smart | Only between accounts whose windows all renew in the same hours, or whose shares used are equal. |
| Least used first | Only between accounts with the same share used and the same tokens sent lately. |
| Weekly pace | Only between accounts in the same pace band that magpie sent the same tokens lately, in practice none since magpie started. |
For Claude Code and Codex, *Make first* also signs the agent in to that
account, because the account the agent is signed in to is first. With
*Keep … signed in to* on, *Make first* changes only the gateway's order,
and the agent stays signed in where it is kept.
**When an account runs out.** A 429 that says the allowance is used up
rests the account until it is back, and the same request goes to the next
candidate at once, as long as none of the reply has been sent. How long it
rests comes from the first of these that is known: the reset time in the
refusal (Claude Code's `usage limit reached|<time>`, ChatGPT's
`resets_at`), the reset of a window magpie last read as used up (98%, 100%
In order), the vendor's `Retry-After` or rate-limit reset header (an hour
at most), else 15 minutes. It is never longer than 8 days. The account's
windows are read again right away, and once a reading finds the window it
filled started again (for Claude, a new `/usage`; for any subscription,
its reset gone by), it is back at once, not at the time the refusal
named. A five hours started again while its week is still used up doesn't
bring it back. A 429 that is a short rate limit rests
the account for as long as the vendor asks (an hour at most), or a
minute, doubled each time it comes back right after its rest, up to 30
minutes. Out of credit rests half an hour. Any other failure rests a minute, longer each time it fails
again, up to 10 minutes; a subscription that fails with a window used up
rests until that window renews.
**Through the gateway, or on its own.** Everything above is the gateway's,
for requests sent through magpie. Claude Code or Codex used on its own
talks to the vendor with the account it is signed in to, and routing
never sees it. For that, magpie looks at the account the agent is signed
in to a minute after it starts and every 5 minutes after: once it has used
98% of a window (100% In order), or reached its usage cap, magpie signs
the agent in to the next ticked account, in their order, whose allowance
is known and under that share. A saved Claude account magpie knows
nothing of yet (see below) is not moved to. Once the account it moved the
agent off has every window under 90% again, it signs the agent back in to
it. *Keep … signed in to* turns this off: the agent stays on the first
account, or on the account you picked, whatever it has left. It changes nothing in the gateway's routing.
**How often an allowance is read.** Routing uses what was read last and
never waits for a reading, except the first one after magpie starts (3
seconds at most). A reading over a minute old is read again in the
background as a request is routed, and an account that fails for its
quota is read again at once; if a reading was already under way as it
failed, the account is read again as soon as that reading is back. A
reading the Usage page made counts as one, its age from when it was made.
Codex and most other subscriptions read every
account from the vendor this way. A Codex account is also known from each
reply ChatGPT sends magpie for it, which says what the account has used:
an account near its usage cap is held from the next turn on (#1295). A
usage cap is set on the account's row for each of its windows, and a
window can have one of its own, set from its meter: a five-hour window at
50% and a weekly one at 40%, or no cap on one window. The account is held
while any window is past its own. A
usage cap is still a stop on what magpie has read, not a guarantee: a turn
already under way can take an account past it, so a 99% cap doesn't
promise 1% is left. Claude is different: magpie never asks
Anthropic itself. It reads only the account Claude Code is signed in to,
by running Claude Code's `/usage`:
- once after magpie starts;
- when you open or refresh the Usage page (in the app or the TUI), or run
`magpie quota` or `magpie accounts`, at most every 30 seconds;
- otherwise again after 5 to 15 minutes (drawn at random each time), and
only if Claude Code was used since.
A saved Claude account that isn't signed in is never read. What it has
left is known only from what Claude Code says as that account answers a
request through the gateway, and its Usage card says so until then. So
the windows routing goes by for such an account can be hours old; a window
whose reset has passed counts as empty again.
### Signed-in agents as providers
An agent you have signed in to is a subscription with models behind it, so
magpie offers it as a provider too. Claude Code (an OAuth login in the macOS
Keychain or `~/.claude/.credentials.json`), Codex (a ChatGPT login in
`~/.codex/auth.json`), Copilot (a GitHub login in
`~/.config/github-copilot/apps.json`), Devin (`devin auth login`, kept in
`~/.local/share/devin/credentials.toml`) and Qoder (signed in from magpie with
its OAuth device flow, kept in magpie's own config; Qoder CN is its own
subscription beside it, for accounts on qoder.cn made with an Alibaba Cloud
account or a phone number, which can't sign in on qoder.com) appear in `magpie providers` and in
the Providers tab as *signed in as …*, with their models spelled
`claude/claude-sonnet-5`, `codex/gpt-5.5`, `copilot/claude-sonnet-4.5` or
`devin/swe-2-max` in every other agent's picker. magpie reads the agent's own credentials each
time, refreshes tokens the way the agent does — writing a rotated token
back where the agent will find it — and stores nothing but your model
picks; sign out of the agent and the provider is gone. The model list is
the vendor's own too: magpie asks Anthropic's, Copilot's or Codex's API with
that same sign-in, so a model added upstream appears on the next refresh.
The ChatGPT backend only streams and rejects a few parameters, so magpie
translates non-streaming requests and drops what it would refuse.
Claude subscriptions are different: Anthropic classifies another agent's
system prompt as third-party traffic even when the OAuth request otherwise
looks like Claude Code. magpie therefore drives the genuine local `claude`
binary for every Claude subscription generation. The caller's tools are
bridged into that live turn over MCP, and tool results resume the same Claude
Code process; Pi, OpenCode and every other agent use this path automatically.
The generated harness stays out of Anthropic's system-prompt classifier while
its instructions remain part of the user context. This requires Claude Code
to be installed and signed in.
A Grok subscription (SuperGrok, signed in with Grok Build) talks straight
to the Responses API the grok CLI uses, a Devin subscription to the API the
devin CLI uses, and a Cursor subscription to the agent API cursor-agent
uses, each with the CLI's sign-in and the caller's tools passed through
(Cursor's model calls them as MCP tools; none of Cursor's own tools run).
Google sign-ins — Gemini CLI's and Antigravity's — talk to Google's Code
Assist API directly: magpie reads Gemini CLI's own login from `~/.gemini` or
signs one in itself, and refreshes the token in memory. Google no longer
serves Gemini CLI's sign-in to individual accounts, only to Gemini Code
Assist Standard and Enterprise, which need a Google Cloud project named
(`magpie accounts project gemini <email> <project-id>`, or
`GOOGLE_CLOUD_PROJECT` in `~/.gemini/.env`). Google may suspend an
Antigravity account it sees used outside Antigravity, so magpie asks before
adding one; use an account you can afford to lose.
### Connecting anything else
The gateway listens on `127.0.0.1:3425` (`MAGPIE_ADDR` changes it) and starts
with the app; `magpie serve` runs it alone. For reverse-proxied or container
deployments, set `MAGPIE_PUBLIC_URL=https://magpie.example.com` to the base
URL shown in the console and CLI, including connection examples. Local
agent configs still use the local gateway address.
Building an app or agent that should use magpie, or get a row on the
Agents page: see [Integrating your app or agent](integrating.md).
External clients behind a reverse proxy need an enabled gateway key
(Gateway → Gateway keys): with Settings → Share on local network on, or a
`MAGPIE_ADDR` on the network, magpie asks for one; otherwise it refuses
them, unless the proxy authenticates its clients itself
(`MAGPIE_TRUST_PROXY=1`, below). A public URL with no port of
its own — a reverse proxy's `https://magpie.example.com` — is the address
`magpie web` prints for its own page too, so the proxy must forward `/v1`
and `/v1beta` to the gateway's port and the rest to the page's. When the
proxy and magpie run on the same machine, a request it forwards over
loopback carries a forwarding header and needs a gateway key as one from
the network does (see [Providers and the gateway](#providers-and-the-gateway)); a proxy that
authenticates its clients itself can be trusted with `MAGPIE_TRUST_PROXY=1`.
It exposes:
| Path | API |
| ------------------------ | -------------------------- |
| `/v1/chat/completions` | OpenAI chat completions |
| `/v1/responses` | OpenAI Responses (HTTP, streamed as SSE; a WebSocket upgrade is answered 426) |
| `/v1/messages` | Anthropic Messages |
| `/v1/messages/count_tokens` | Anthropic token counting |
| `/v1beta/models/{model}:generateContent` | Google Gemini (also `:streamGenerateContent`, `:countTokens`) |
| `/v1/models`, `/v1beta/models` | the catalog |
| `/v1/codex/models` | the catalog as Codex's `model_catalog_url` reads it ([Codex on another computer](#providers-and-the-gateway)) |
Each `/v1/models` entry includes `reasoning` and `supported_reasoning_levels`
(`[{"effort":"low"}, ...]`). A routing group is marked `reasoning` when any
member thinks, even where it offers no levels to pick from (a member known
to take none still leaves the group none); `supported_reasoning_levels`
lists only the levels every member supports, as before. A member magpie
knows does not think is sent no reasoning ask when the group routes to it,
so the effort the group is asked for reaches the members that take it and
doesn't turn a vendor away as a 400; a member nothing speaks for is sent
what the agent asked, as before. `native_endpoints`
(`["/v1/messages"]`) names the APIs a request for the model is passed
straight through on; it is left out of a routing group, and of a model every
request to which is translated anyway.
Requests pass straight through when the vendor speaks the agent's API and
are translated otherwise, streaming, tool calls and reasoning included. The
key is `magpie` (any value works; the gateway only listens on loopback), and
models are named `provider/model`.
- A routing group is asked by its id (`group/<id>`) or its name.
- A bare model id (`glm-5.3`) works too. When several providers serve it,
it goes to the routing group magpie found for it, which asks them in the
order of the Providers page and falls over to the next. With found
groups off, it goes to the first of them in that order alone.
- Every reply names who answered in `X-Magpie-Provider` (the provider)
and `X-Magpie-Model` (`provider/model`), and so does the usage log.
Anything with a base-URL setting can use it:
| Tool speaks | Base URL | Environment |
| ----------- | -------------------------- | --------------------------------------------- |
| OpenAI | `http://127.0.0.1:3425/v1` | `OPENAI_BASE_URL`, `OPENAI_API_KEY=magpie` |
| Anthropic | `http://127.0.0.1:3425` | `ANTHROPIC_BASE_URL`, `ANTHROPIC_API_KEY=magpie` |
| Gemini | `http://127.0.0.1:3425` | `GOOGLE_GEMINI_BASE_URL`, `GEMINI_API_KEY=magpie` |
An optional `X-Magpie-Account: <account>` header (the account's email
or login, or its id on the Routing page) pins a request to one account of a
subscription with several: only it is tried, and an unknown account, one
whose plan lacks the model, or one resting is an error rather than another
account's reply. The header is not sent on to the vendor.
A status bar can show where a turn went before its first token arrives:
send `X-Magpie-Session: <id>` with the requests (an agent's own session
header, such as Pi's or Claude Code's, works too) and read
`GET /v1/magpie/route?session=<id>`. It answers the session's latest
request as routing has it so far — `asked` (the model the agent named),
`group`, `rule` (the group's rule that matched, and `rule.pick`, the
effort its decision model picked), `model` and `effort` (the member being
tried now, as `provider/model`, and the reasoning it was sent at), and
`tries`, one per member tried, each failed one a fallback with its `fail`
— then `done`, `status` and `served` once the reply is over; `route` is
`null` before the session has one. The route appears once routing has
decided, before the vendor is asked. `after=<seq>&wait=<seconds>` (up to 60)
holds the answer until the route changes past the `seq` of the last one,
so a UI can follow a turn with one request at a time. Only the session
named is told; like `/v1/magpie/quotas`, it answers this machine, and
another only with the key of a gateway shared on the local network.
To show the one in use without a session, `GET /v1/magpie/quotas` (and
`magpie quota --json`, the same list) has `lastServedAt` on each
subscription account, plan and key that answered a request through the
gateway in the last 30 days, and `last: true` on the latest. It is kept
in `served.json` beside `providers.json`, so a restart keeps it.
`magpie quota wait <provider|account>` blocks until that subscription (any
of its accounts magpie has on) or that one account has allowance again — no
window that stops it used up — then exits 0, so a long task stopped by its
limit can go on unattended:
```sh
until codex exec "…"; do magpie quota wait codex || break; done
```
It reads the vendors itself, whether or not the gateway runs, and again
shortly after the soonest reset it knows (every 1 to 10 minutes; an
allowance it can't read is asked again less often each time), saying on
stderr what it waits for and until when. Name an account by its email or
login, or as `<provider>/<account>` when two subscriptions share it.
`--timeout 6h` exits 1 if it passes first, `--quiet` says nothing; an
unknown name exits 2 and Ctrl+C 130.
A ChatGPT account that holds credits keeps answering once a usage window is
used up: the vendor spends the credits, so a task goes on. That is the
default. `magpie quota credits <account> off` (or *Use credits* on the
account's Usage card) turns it off for that account: once magpie's latest
reading of its windows, refreshed about every minute, shows one used up,
the gateway holds it as used up till the window renews, and requests go to
the other accounts, groups and fallbacks; with none left, the request gets a
429 saying why, unless the account spends its resets by itself and its week
is used up, when a reset is spent first. `magpie quota credits` lists the
accounts set not to spend them, `magpie quota credits <account>` says one's.
It changes the gateway's routing only, never the account Codex is signed in
to. The credits an account holds show beside its windows in `magpie quota`,
`magpie accounts`, the Usage page and the menu bar panel.
The Usage page's cards and the menu bar panel's *Allowances* tab are in one
order: drag a card's logo on the Usage page (or Alt+arrow keys on it), or
press *Arrange* at the foot of the panel's tab and move the rows there. The
same *Arrange* hides a subscription from the panel's tab (*Hide*, *Show*
puts it back), and the tab's foot says how many are hidden. Hiding is the
panel's only: routing, caps, the Usage page and the menu bar's own cells
still have it, and a menu bar cell for it still opens its card. Both are
kept in magpie's settings (`usageOrder`, `panelUsageHidden`). Which
allowances the menu bar itself shows beside its icon is picked in Settings.
The *Gateway* tab in the app has this as copy buttons and ready-made
snippets (shell, curl, Python, Node) for each API, the list of model ids,
and the recent calls; `MAGPIE_DEBUG=1` logs every call to the terminal.
**Claude Code** gets `ANTHROPIC_BASE_URL`, `ANTHROPIC_AUTH_TOKEN` and the
model variables in the `env` block of `settings.json`; picking a native
model (`opus`, `sonnet`…) removes them and restores whatever was there.
A key in `ANTHROPIC_AUTH_TOKEN` signs Claude Code out of claude.ai while it
runs through magpie: claude.ai's plan limits in `/usage`, its connectors,
voice and `/teleport` are off. Set its sign-in to claude.ai to keep that
login: magpie writes the token empty, Claude Code sends magpie its claude.ai
sign-in, and magpie never passes it on (nor its `oauth-2025-04-20` beta). A
provider that refuses magpie's own key is then reported to Claude Code as
a 502, not as its sign-in refused, so Claude Code doesn't log out. Remote
Control and ultrareview stay off: Claude Code has them only on Anthropic's
own address. The choice is offered where the gateway takes any key, so not
from a WSL distro under NAT while magpie is shared on the network.
**Codex** gets a `[model_providers.magpie]` table, `model_catalog_json`
pointing at `~/.codex/magpie-models.json` (written from the catalog, so the
models show in Codex's own list) and a valid `model`/`effort`; picking a
native model or disconnecting removes `model_provider` and the catalog. The
`[model_providers.magpie]` table stays, so a thread started on magpie can
still be opened. Codex won't load its config at all when `model_provider =
"magpie"` has no table ("Model provider `magpie` not found"). If another
tool leaves that state behind, magpie writes the table back the next time
it syncs. Signed in to ChatGPT (the sign-in field's default), Codex keeps
its own provider and sign-in, and magpie's models join its list through
`openai_base_url`. magpie becomes Codex's provider then only while the
Codex app holds the account: OpenAI no longer allows it and it has no
credits left, except a workspace account still within its overage. The
app sends nothing for a held account, so magpie steps in, and it steps
back once the account has room again. A window at 100% with credits left
doesn't count: Codex stays signed in and keeps sending on those. Your
ChatGPT sign-in is never touched.
Codex reads its model list at start-up, so restart it after a switch.
**OpenCode, Pi, Crush** get a `magpie` provider entry and `magpie/provider/model`.
**Gemini CLI** switches `auth` between API key, Google account and Vertex;
the API key goes to `~/.gemini/.env`. Picking a catalog model points
`GOOGLE_GEMINI_BASE_URL` at the gateway (which speaks the Gemini API), sets
`auth` to API key with the gateway token, and names the model in
`settings.json`; a native model puts the previous auth back.
### Import links
A vendor or relay can hand its users a ready-made provider as a link:
```
magpie://import?preset=deepseek&key=sk-…
magpie://import?name=Acme%20Relay&chat=https://api.acme.example/v1&anthropic=https://api.acme.example&key=sk-…&models=gpt-5.5,claude-sonnet-5
```
Opening one brings up magpie with what the link would add: the name, the
hosts your prompts and key would go to, the models. Nothing is saved until
you press *Add*. `magpie import <link>` does the same in a terminal.
| Parameter | Meaning |
| ----------- | ------------------------------------------------------------------ |
| `preset` | a preset id (`magpie presets`); its endpoints are used |
| `region` | with a preset that has regions, which one |
| `name` | the provider's name; required without a preset |
| `id` | its id; derived from the name when absent |
| `key` | the API key; the user pastes one when absent |
| `chat` | OpenAI Chat Completions base URL (`…/v1`) |
| `responses` | OpenAI Responses base URL (`…/v1`) |
| `anthropic` | Anthropic Messages base URL (the root, without `/v1`) |
| `models` | model ids to expose, comma separated |
| `catalog` | models.dev provider id, for model names and reasoning levels |
| `website`, `keys` | the vendor's site and its API-key page (https) |
| `icon` | an https picture of the vendor's own (PNG, JPEG, GIF, WebP, ICO, SVG, at most 1 MB). magpie downloads it once, after you confirm the import, into its icons folder; without one it falls back to the catalog's logo or a plain mark |
Base URLs must be https (plain http only to this machine or the local
network). Web pages and GitHub don't link custom schemes reliably, so link
to `https://usemagpie.ai/import#<same parameters>` instead: it opens
magpie, and offers the download when it is not installed. The parameters
stay in the fragment, which browsers never send to a server. The full guide,
with a link builder: <https://usemagpie.ai/docs/import>.
## Install
Download the app for macOS, Windows or Linux from
[usemagpie.ai](https://usemagpie.ai), or install it from a terminal (on
Linux, the desktop app when WebKitGTK 4.1 is installed, the command
otherwise):
```sh
curl -fsSL https://usemagpie.ai/install.sh | sh
```
Behind a firewall, or without access to GitHub, both the installer and
`magpie update` take `--proxy <url>` (http, https, socks5, socks5h; or
`MAGPIE_PROXY`, and `HTTPS_PROXY` / `ALL_PROXY` as usual) and
`--mirror <prefix>`, a GitHub download mirror of your choosing put before
the release's github.com URL (or `MAGPIE_MIRROR`; `magpie update mirror
<prefix>|off` keeps one for every update, the app's too):
```sh
curl -fsSL https://usemagpie.ai/install.sh | sh -s -- --proxy http://127.0.0.1:7890
magpie update --mirror https://mirror.example/
```
No mirror is used unless you give one, and the SHA-256 every download is
checked against still comes from usemagpie.ai, never from the mirror.
Mac releases are signed and notarised; the Windows and Linux builds are not
signed yet (Windows SmartScreen may ask before the first run). Every build
keeps itself current: the app
downloads a new version in the background and installs it when you restart
(*Restart to Update* in the menu) or quit; `magpie update` does the same from
a terminal. Every release is on
[yetone/magpie-releases](https://github.com/yetone/magpie-releases/releases).
From source:
```sh
go install github.com/yetone/magpie@latest
```
or build locally:
```sh
make build # ./magpie with the desktop app (needs cgo + the platform webview)
make app # macOS: magpie.app, a menu bar app with no Dock icon
make cli # terminal-only build, no cgo, cross-compiles anywhere
make release # dist/: native app build + cli builds for every platform
make release-windows # dist/: the Windows app, amd64 and arm64 (cross-compiles)
make release-linux # dist/: the Linux app for this machine's arch
```
Linux needs `libgtk-3-dev` and `libwebkit2gtk-4.1-dev` for the app build
(the Makefile adds the `gtk3` tag; with plain `go build`, pass `-tags gtk3`);
Windows uses the WebView2 runtime that ships with the OS.
Termux uses the Android terminal build: the installer puts it in
`$PREFIX/bin`, and `make build` selects it automatically. With plain
`go build` or `go install`, pass `-tags nogui`; use `magpie web` or
`magpie tui` for the interface. `magpie autostart on` requires Termux:Boot.
### Docker
`docker build` makes a server image: the terminal-only binary on
distroless (`cc`, for the glibc the plugins' Bun needs) with bash and
busybox for a terminal in the container (`docker exec -it magpie bash`, or a
NAS panel's terminal; `magpie` is on its PATH), run as nonroot,
with everything it keeps in a volume at `/config`: magpie's own files
(`/config/magpie`), the sign-ins kept where their agent keeps them (HOME is
`/config/home`, so `~/.codex`, `~/.claude`… are in it) and the cache with the
Bun plugins run on (`/config/cache`, downloaded once). A volume made by an
older image keeps working: magpie adds these folders to it on start, and only
sign-ins made with that older image, which lived outside the volume, have to
be made again.
```sh
docker build -t magpie .
docker run -d --name magpie -p 127.0.0.1:3425:3425 -p 127.0.0.1:3430:3430 -v magpie-config:/config magpie
```
3425 is the gateway for agents. Inside the container it takes any key; a
request from outside it, the host's own included, must carry an enabled
gateway key as its API key, and one without gets 401. Make one in the
browser UI (Gateway → Gateway keys) or with
`docker exec magpie magpie gateway-key add "Docker client"`. Sharing
(Settings → Share on local network) isn't needed for this; turning it on
also creates a key named **Magpie**. The ports above are published on the
host's loopback; Docker's `-p 3425:3425` puts the gateway on every interface
of the host, for other machines to reach. Upgrading from an earlier image:
an agent outside the container that sends `magpie`, or another key that
isn't an enabled gateway key, now gets 401 ("the API key sent is not an
enabled magpie gateway key"); give it a gateway key.
Inside the container
magpie only sees the container's own address (Docker's 172.17.x), so set
`-e MAGPIE_PUBLIC_URL=http://<the host's or NAS's address>:3425` (the port
published on the host) for the address it shows and prints to be the one
other machines use; behind a reverse proxy, set it to that external base URL
and follow the [authentication requirements above](#connecting-anything-else),
especially when the proxy reaches magpie over loopback.
For the browser UI run the image with `magpie web --addr 0.0.0.0:3430 --no-open`
in place of the default `serve`, and open
`http://localhost:3430/?k=<key from docker logs magpie>` (set `MAGPIE_WEB_KEY`
to keep one key across restarts). There you add providers, sign in to
subscriptions and import sign-ins from a file. The vendor sends a sign-in's
browser back to `localhost` (ChatGPT to `http://localhost:1455/auth/callback?code=…`),
which is your own machine, not the container, so that page won't load: copy
its whole address from the address bar and paste it into the sign-in's
*Callback URL* field. `docker exec -it magpie /magpie accounts add codex`
does the same in a terminal: open the link it prints, then paste the address
the browser ended on. Keys, sign-ins and plugins live in the volume, so a
restart, or a new container on the same volume, keeps them.
The image has a `HEALTHCHECK`: `magpie healthcheck` exits 0 while the gateway
answers on `MAGPIE_ADDR`, under `serve` and `web` alike, so `docker ps` shows
the container as healthy (Compose: `depends_on: condition: service_healthy`)
with no curl in the image. Bind-mounting a folder at `/config` in place of a
named volume works too; it has to be writable by uid 65532.
#### NAS with agents on other machines
Magpie can run on a headless NAS as a shared gateway even when the agents that
use it (Claude Code, Codex, OpenCode, and so on) run on other computers. The
container does not discover agent installations on the NAS host or on remote
machines: configure those agents to use the NAS address and an enabled gateway
key. See [Connecting anything else](#connecting-anything-else) for the
protocol-specific base URLs. Keep `3425` behind a firewall or VPN and do not
expose it publicly without gateway-key authentication.
Such a page is in **gateway mode**: it shows Providers, Gateway, Routing,
Usage, Plugins and Settings, with no Agents, Sessions or Library tab, and
Settings leaves out what is written into this machine's agents (provider in
model names, Codex subagents, long conversations, Codex thread titles) and
the desktop's alerts and tray. `magpie web` is in it by itself when it finds
no agents on its machine, as in the container, and with
`magpie web --gateway`. *Settings › General › Gateway mode* picks Automatic,
On or Off for this machine (it wins over both); Off brings every page back.
The app's own window is never in gateway mode.
For a bind-mounted configuration directory, the directory must be writable by
the non-root container user (uid 65532):
```sh
sudo chown -R 65532:65532 /volume1/docker/magpie/config
```
An example Docker Compose project is:
```yaml
services:
magpie:
image: ghcr.io/yetone/magpie:latest
restart: unless-stopped
ports:
# 127.0.0.1: the host alone; every caller outside the container needs a gateway key.
- "127.0.0.1:3425:3425"
- "127.0.0.1:3430:3430"
environment:
MAGPIE_ADDR: 0.0.0.0:3425
MAGPIE_PUBLIC_URL: http://<nas-address>:3425
MAGPIE_WEB_KEY: <random-key>
volumes:
- ./config:/config
command: [web, --addr, 0.0.0.0:3430, --no-open]
```
Open `http://127.0.0.1:3430/?k=<random-key>` through an SSH tunnel to
configure providers, then create a gateway key for each remote agent host.
To let remote agents reach it, change the gateway mapping to `3425:3425` (or
bind it only to the NAS interface or VPN address) and recreate the
container. For Internet access, prefer a
VPN such as Tailscale or WireGuard; a reverse proxy or tunnel forwarding to
Magpie over loopback needs a gateway key from its clients, as the network does.
### Developing
```sh
make dev
```
builds with `-tags dev` and opens the app with the UI served straight from
`internal/gui/assets`: save `app.css`, `app.js` or `index.html` and the window
reloads itself. With `fswatch` installed (`brew install fswatch`), a change to a
Go file rebuilds and relaunches the app too. The dev build uses its own gateway
port (`DEV_ADDR`, default 127.0.0.1:3426), so a magpie you already run keeps
serving your agents. Point it at a scratch home to keep your real agent
configs out of it:
```sh
HOME=/tmp/magpie-home XDG_CONFIG_HOME=/tmp/magpie-home/.config make dev
```
`MAGPIE_THEME=light|dark` forces the palette and `MAGPIE_DEBUG=1` prints what the
gateway translates.
## Use
```sh
magpie # open the app: a window plus the menu bar icon
magpie tray # menu bar icon only (use this in your login items)
magpie tui # the same thing, in the terminal; serves the gateway while open when no other magpie does
magpie web # the app's window in a browser (WSL, a server over SSH); --lan, --addr, --no-open, --gateway
# (a new key each run; MAGPIE_WEB_KEY keeps one, for a page run as a service)
magpie ls # list every agent and its current settings
magpie claude opus # set a model (agent names accept prefixes: cc, oc, gem …)
magpie codex gpt-5.6-sol
magpie codex effort high # other fields
magpie codex xhigh # bare effort levels are recognised too
magpie codex deepseek/deepseek-chat # any catalog model, through the gateway
magpie claude moonshot/kimi-k2.5
magpie claude haiku deepseek/deepseek-v4-flash # one tier on its own model
magpie claude haiku "" # back to the main model
magpie gemini auth api-key
magpie opencode anthropic/claude-sonnet-5
magpie oc small anthropic/claude-haiku-4-5
magpie mimo anthropic/claude-sonnet-5
magpie save work # snapshot everything as a profile
magpie use work # switch back
magpie profiles
magpie rm work
magpie sync # refresh the models.dev catalog and every live model list
magpie quota # what is left of every subscription, plan and key balance
magpie quota wait codex # block until a Codex account has allowance again
magpie quota credits me@example.com off # hold a ChatGPT account at its limit instead of spending credits
```
In the app, click any value to open a filtered list; type to search or to
enter something that is not listed; `esc` closes the panel. Profiles are the
chips at the bottom: click to apply, `×` to delete, *+ save current* to add.
The *Providers* tab of the window lists your providers with the agents on
each; click a row to change the key or the exposed models, *Test* it, or
click an agent icon to point that agent at one of its models. *Add
provider* shows the presets as tiles: pick one, paste the key.
In *Usage → Requests* and the tray's *Usage* tab, click a daily bar to see
that day's totals and details. Other days turn gray; click the selected day
again to return to the whole period. Changing the period clears the selection.
On macOS, *Settings → Preferences → Session terminal* chooses which installed
app opens a session from the terminal button in *Usage → Sessions*. The list
contains apps registered to open `.command` files, with the current system
default listed once. With no saved choice, magpie follows that default, or
Terminal when the default is not a terminal. The *Resume* button still copies
the command.
Keys in the terminal version:
| Key | Action |
| ---------- | ------------------------------------- |
| `↑` `↓` | choose agent |
| `←` `→` | choose field (model, effort, small …) |
| `↵` | open the picker |
| type | filter; enter accepts custom values |
| `s` | save current setup as a profile |
| `p` | apply or delete (`ctrl+d`) a profile |
| `S` | sync the model catalog |
| `q` | quit |
Agents read their config at startup, so a running session keeps its model
until you start a new one.
### Moving to another machine
```sh
magpie backup # writes magpie.magpie-backup, asks for a passphrase twice
magpie backup --no-keys ~/b.magpie-backup # the same with no API keys in it
magpie restore magpie.magpie-backup # on the other machine
magpie restore --no-agents b.magpie-backup # providers, settings, profiles; agents left as they are
magpie restore --no-library b.magpie-backup # the library here left as it is
```
A backup holds your providers (with their keys, unless `--no-keys`), the
pictures picked for them, the settings, the profiles, every agent's model and
the library (unless `--no-library`): the instruction sets, the MCP servers and
the skills with their files (a file over 2 MB is left out). Without keys, a
server's environment variables and headers that look like a key go empty.
Gateway credentials, their names, ids and disabled state travel encrypted
with Settings too. Restoring Settings replaces the gateway-key store with
the backed-up one. `--no-keys` leaves gateway credentials and their legacy
mirror out; restoring it preserves the destination's existing keys instead.
Restoring the library replaces the one there — what it replaces is kept with
the library's backups — and writes it into the agents on that machine.
It is encrypted on your machine (AES-256-GCM, the key derived from the
passphrase with PBKDF2-SHA256); nothing in it can be read without the
passphrase. Restoring replaces providers with the same id and adds the rest;
one that came without a key keeps the key already there. Agent models are set
only for agents installed on that machine. Subscriptions are not in it: sign
in to them on each machine. Piped in, the passphrase is the first line of
stdin.
### Keeping machines in sync
*Settings → Sync and backup → WebDAV or S3 sync* keeps the same backup on a
server and brings every machine up to date with it, every 3 minutes while the
gateway runs. Choose one of these:
- **WebDAV**: a folder on a WebDAV server such as 坚果云, Nextcloud or a
Synology.
- **S3**: a bucket on AWS S3, Cloudflare R2, Backblaze B2, MinIO, Garage, a
NAS or any other S3-compatible server.
The file is sealed on your machine with the passphrase, so the server only
ever stores ciphertext. Each machine writes only over the version it read (a
conditional write), so an update that another machine made in between is
merged rather than lost.
```sh
magpie webdav on https://dav.jianguoyun.com/dav/ user=me@example.com
magpie s3 on s3://my-bucket/magpie endpoint=https://<account>.r2.cloudflarestorage.com access-key-id=…
magpie s3 on s3://backups endpoint=http://nas.local:9000 path-style=yes access-key-id=…
magpie s3 # where it syncs to and how the last sync went; magpie s3 now, off
```
For S3:
- `endpoint` is empty for AWS.
- `region` defaults to `us-east-1`, or to `auto` on R2.
- `path-style=yes` puts the bucket in the path, which MinIO and most servers
you run yourself need.
- The secret is asked for and saved like the WebDAV password. It is used only
with the endpoint and access key it was given for.
- The bucket must already exist.
- The access key needs to read and write `<prefix>/magpie/`. On AWS it also
needs to list the bucket.
- A server without conditional writes is supported. There magpie checks the
object's ETag just before each write.
- Shared usage is reconciled only after a complete S3 listing. A missing or
repeated continuation token, or a listing still truncated after 100 pages,
reports a sync error and keeps previously downloaded usage days in place.
## OTLP export
Settings → Observability can export gateway request metadata over OTLP/HTTP
(JSON). Export is off by default. Set the collector's base URL and optional
headers, then enable **OTLP export**. **Export metrics** is separately off by
default; enable it for a collector that accepts duration and token histograms.
No restart is needed for saved settings.
For `magpie serve`, environment variables override the saved preferences:
```sh
MAGPIE_OTEL_ENABLED=true MAGPIE_OTEL_ENDPOINT=http://localhost:4318 magpie serve
```
- `MAGPIE_OTEL_ENABLED`: `true` or `false`; an endpoint alone does not enable export.
- `MAGPIE_OTEL_ENDPOINT`: an HTTP(S) base URL; `/v1/traces` and `/v1/metrics` are appended.
- `MAGPIE_OTEL_HEADERS`: comma-separated `name=value` pairs, for example
`Authorization=Bearer%20token`. Percent-encode spaces and commas in values.
- `MAGPIE_OTEL_METRICS`: `true` or `false`, off by default.
- `MAGPIE_OTEL_SESSIONS`: `true` or `false`, off by default; trace supported local
agent interactions from newly recorded session events.
- `MAGPIE_OTEL_BODIES`: `true` or `false`, off by default; sends each call's
request and reply as the trace's Langfuse input and output.
- `MAGPIE_OTEL_BODIES_WHOLE`: `true` or `false`, off by default; with bodies on,
keeps them entire rather than cut at 256 KB. A long reply is written to a
temporary file; a body too large for the collector is still refused.
For Langfuse, use `https://<your-langfuse-host>/api/public/otel` as the base
URL and `Authorization=Basic%20<base64(public-key:secret-key)>` as the header.
Leave metrics off. This uses Langfuse's OTLP ingestion endpoint. For Langfuse
v4, add the header `x-langfuse-ingestion-version=4` for real-time ingestion.
Traces include agent, provider, model, token counts (including cache and
reasoning), HTTP status, timing, and route ID. Attempts with the same route ID
share a trace ID. Chat, Responses, Anthropic and Gemini gateway requests
also export a parent request span and child spans for every routing attempt,
including unbilled failures, retries and fallbacks, so Langfuse can render a
waterfall. A valid incoming W3C version-00 `traceparent` connects these spans
to the caller's trace. Attempt spans carry token usage; the parent does not
duplicate it. Attempt timings include any wait for a concurrency slot, while
routing and retry delays remain visible as gaps inside the parent span.
Tool execution inside the caller is outside the gateway's trace.
Model-call spans also send estimated USD input, output and total costs to
Langfuse via `langfuse.observation.cost_details`, using the same effective
prices as Magpie's usage ledger (custom prices first, then catalog prices).
Input cost includes cache reads and writes; reasoning tokens are already
included in output cost. Explicit zero prices are exported too. Unknown
prices are omitted so Langfuse can use its own model pricing. Parent and tool
spans carry no costs, avoiding duplicate counting. Subscription costs at
catalog prices are API-equivalent estimates, not subscription charges.
Enable **Trace agent conversations** to instead export one trace per
user interaction, grouping model calls and tool executions under an agent
root. Conversation IDs group those traces into Langfuse sessions. Gateway traces
remain available until a visible local store covers that exact agent/session.
For Codex and Pi, a readable session header establishes readiness without
waiting for a model/tool span. The first request checks a header bounded to
256 KiB when needed (including Codex base instructions); subsequent polls keep
idle sessions ready while their files remain among the reader's 200 newest main
files. Claude children have a separate 200-file quota and share the same 8 MiB
polling read budget. Header checks never upload history or bodies. Discovery is shared across
request IDs for two seconds. The request that refreshes discovery waits for the
scan; other unresolved concurrent requests retain gateway traces without waiting.
Polling reuses headers by path, size and mtime.
Other adapters still use recent observations (five minutes).
Only loopback requests without a gateway key can be deduplicated. The native
client session ID takes precedence over a Magpie routing override; unknown
sessions retain gateway traces, including WSL mirrored and Docker Desktop
clients whose sessions are not visible locally. Requests without a session ID
fall back to recent observations for that agent.
Claude Code/Cowork readers also follow `<session>/subagents/*.jsonl` and a
workflow's (ultracode) `<session>/subagents/workflows/<run>/agent-*.jsonl`
(not its `journal.jsonl`). Child
interactions share their parent session and use separate trace/span IDs, even
when a user UUID is copied. Child final responses are combined across content
blocks and exported when the transcript is unchanged for two seconds; no
`turn_duration` is required.
Their start times remain inferred; a parent tool link is not guessed. Main-thread calls can therefore be deduplicated. Claude
tool-less small requests (`max_tokens` at most 4096), including title/haiku
helpers, retain gateway traces because they may not enter a transcript. This
conservative rule can retain a short tool-less main request as well. Explicitly
classified auxiliary calls also retain gateway traces for other agents. This
includes Codex calls with an explicit kind (subagent, review, memgen, title or
compact): even if a child has a readable rollout, these can appear twice. Only
its unclassified calls use local-session deduplication; a rollout alone does not
prove every auxiliary request was recorded. Keyed and non-loopback clients
always retain gateway traces. Magpie reads new events from local session stores
every two seconds; it does not upload
completed history when enabled. Restarting or changing the destination starts
an observation window. Span IDs remain stable across repeated records.
Pi and Codex have been tested end to end with real clients. The other adapters
are covered by format fixtures but have **not been tested end to end**.
Supported clients and formats:
| Client | Local store | Timing |
| --- | --- | --- |
| Codex | JSONL rollouts (`token_usage_record`, `item_completed`, `response_item`) | Recorded operations and paired native tools; inferred model/native-tool intervals |
| Pi | Version-3 JSONL; optional `timing-final` | Recorded model times; inferred tool intervals |
| Oh My Pi | Pi-compatible JSONL, including `model_usage` | Pi timing; auxiliary calls have inferred zero duration |
| Claude Code / Cowork | `projects/*/*.jsonl`, `<session>/subagents/*.jsonl` and `<session>/subagents/workflows/*/agent-*.jsonl`, repeated assistant blocks and paired tool results | Inferred model/tool starts; recorded transcript boundaries |
| OpenCode | SQLite V1/V2 (`message`/`part` or `session_message`); legacy JSON storage | Recorded model and tool timestamps |
| Gemini CLI | `~/.gemini/tmp/*/chats/session-*.json[l]`, including patches and rewinds | Inferred intervals from message/tool event boundaries |
The adapters follow upstream schemas:
[OpenCode V1](https://github.com/anomalyco/opencode/blob/dev/packages/schema/src/v1/session.ts),
[OpenCode V2](https://github.com/anomalyco/opencode/blob/dev/packages/schema/src/session-message.ts),
[Gemini CLI](https://github.com/google-gemini/gemini-cli/blob/main/packages/core/src/services/chatRecordingTypes.ts),
[Oh My Pi](https://github.com/can1357/oh-my-pi/blob/main/packages/coding-agent/src/session/session-manager.ts).
Claude transcript records are checked against local Claude Code sessions;
its [hook documentation](https://code.claude.com/docs/en/hooks) describes
transcript locations. Separate subagent transcripts are not joined: the parent
agent's task/delegation tool is included, but its children's internal work is
not yet linked. No hooks or client changes are required.
Polling uses a soft 8 MiB reading budget and up to 200 recent files per store.
JSONL files are read incrementally; incomplete lines wait for the next poll.
OpenCode SQLite is opened read-only, including WAL changes, and unchanged
stores are skipped. SQLite message/part updates are read incrementally in bounded
batches on separate read-only connections; oversized conversations resume on the
next poll. OpenCode retains up to 256 messages and an 8 MiB body window per
session; individual rows above 8 MiB export metadata only. Legacy JSON and
Gemini snapshot reads cap at 32 MiB; Gemini retains the current prompt plus
255 recent messages.
Completed observations are deduplicated with bounded metadata. Claude's final
response and Gemini's final token records may take one extra poll to settle.
Inferred timings carry `magpie.timing.source=inferred`. Tool arguments/results
and user input/output follow **Include request and response bodies**, masking
secrets and respecting the whole-body preference. Session directory/title
metadata, credentials and account names are not exported.
Conversation tracing replaces standalone gateway traces for these local
clients to avoid duplicate token usage. Unsupported clients and remote
forwarded requests retain gateway tracing; the local usage ledger is unchanged.
This mode shows agent activity rather than gateway retries or provider-routing
details. It reads this computer's local sessions, including calls made without
Magpie as the proxy.
Metrics group duration and input/output token histograms by agent, provider,
model, operation and error status. Prompt/reply text is exported only when
**Include request and response bodies** is enabled (`MAGPIE_OTEL_BODIES=true`);
secrets are masked and each body is limited to 256 KiB unless **Include the
whole bodies** is enabled. Conversation IDs are exported only with
conversation tracing; provider account names/keys are never exported.
Whole bodies increase transient memory and allocation costs during read-back,
secret scrubbing and JSON encoding; a 32 MiB request and reply can roughly
double total allocations compared with truncated export. The export limits
queued bodies to 128 MiB, but this does not bound in-flight processing memory.
Recent calls retain only the first 256 KiB of each body.
Export runs in the background with a bounded queue (128 records) and batches
of up to 32 records, flushed every five seconds. A full queue drops telemetry
without delaying gateway requests. Network errors and HTTP 429/502/503/504
are retried up to two times, with retry delays capped at 60 seconds; other
errors and partial rejection are logged
without the collector's response body. Each HTTP attempt times out after
three seconds. Graceful gateway shutdown allows at most three seconds to
drain; `magpie serve` currently exits on SIGTERM without draining, so its last
batch may be lost. This is best-effort export; local usage records remain
available if it fails.
Requests follow magpie's proxy setting, with loopback collectors going direct.
Queued records are discarded if export is disabled or the destination or
credentials change before sending. Redirects are not followed.
Backups without keys omit OTLP headers; restoring one preserves this machine's
headers only when the collector endpoint is unchanged.
## Mirrors
The Plugins page's **Mirrors in China** (国内镜像) switch has what that page
downloads asked of a mirror in China first and of its official address
after: npm's registry (plugin packages, their versions and search) at
`registry.npmmirror.com`, Bun's releases at npmmirror's binary mirror, and
the plugin list at jsDelivr (`cdn.jsdelivr.net/gh/magpie-community/plugins`).
Each is a copy of the very file — Bun's zip is still checked against Bun's
SHA-256s, and a package against the integrity npm gave — and what the
mirror hasn't copied yet is asked of the official address. npm's weekly
download counts stay npmjs's own. `bun add` is pointed at npmmirror too,
unless `BUN_CONFIG_REGISTRY` or `NPM_CONFIG_REGISTRY` names a registry
already. The switch never sends anything through a proxy such as
gh-proxy.
Mirror fallback is off by default. Set `MAGPIE_MIRRORS=on` to use it. When
it is on, the official source is tried first; a mirror is only tried after
a network error, HTTP 429, HTTP 5xx, or GitHub's rate-limit 403. The
defaults are `registry.npmmirror.com` for npm and `gh-proxy.com` for
GitHub. `MAGPIE_NPM_REGISTRY` and `MAGPIE_GITHUB_MIRROR` replace them.
Only two downloads are checked against a checksum that does not come from
the mirror: Magpie's own update assets (their SHA-256 comes from the
unmirrored update feed) and Bun's zip. Bun's default version carries its
SHA-256s in the code, so it can be downloaded through a mirror even when
Bun's official `SHASUMS256.txt` is unreachable; a newer Bun still needs
that official file. Skill tarballs always use the official source, and the plugin-market
registry does unless the Mirrors in China switch is on (jsDelivr, above). npm metadata, plugin README/search results and
version checks are not checksummed; a mirror can report a different
version.
The official source and a mirror share one request budget, so a hanging
official source doesn't make the whole wait twice as long.
Requests that carry credentials, such as an `Authorization`, `Cookie`,
API key, or URL userinfo/query token, never use a mirror; they go to the
official source alone.
```sh
MAGPIE_MIRRORS=on \
MAGPIE_NPM_REGISTRY=https://registry.example.com \
MAGPIE_GITHUB_MIRROR=https://gh.example magpie serve
```
## Files
- `~/.config/magpie/profiles.json` — saved profiles
- `~/.config/magpie/providers.json` — your providers, keys included (0600)
- `~/.config/magpie/stash.json` — values magpie replaced, restored on switch-back
- `~/.config/magpie/plugins.json`, `plugins/` — the plugins added, and their packages
- `~/.config/magpie/plugin-auth.json` — the plugins' sign-ins (0600)
- `~/.cache/magpie/bun/<version>/magpie-bun` — the Bun plugins run on, named
so that a proxy app's PROCESS-NAME rule can match it (`magpie-bun.exe` on
Windows)
- `~/.cache/magpie/models.json` — models.dev catalog (OpenCode's cache at
`~/.cache/opencode/models.json` is used when present)
- `~/.cache/magpie/models/<provider>.json` — model lists fetched from vendors
`XDG_CONFIG_HOME` and `XDG_CACHE_HOME` are respected.
## Counting users
Once a day, a running magpie (the app, or `magpie serve`) sends one event to
PostHog so we know how many people use it: a random id made up on your
computer (`~/.config/magpie/install-id`), magpie's version, and your system
and architecture.
With it goes what magpie is used with, so we know which agents, providers
and models to look after first, by magpie's own ids only:
- the agents connected to magpie (`claude`, `codex`); an omp profile is
only `omp`;
- the providers that are on: a preset's id (`deepseek`), a subscription's
(`codex`, `copilot`), a community plugin's (`plugin:kiro`); a provider you
added yourself is only `custom`, another plugin only `plugin`;
- the models the connected agents are set to, as the provider's id and the
vendor's model id (`deepseek/deepseek-v4`); on a key's provider, a model
id that [models.dev](https://models.dev) doesn't list (a local model, a
fine-tune) is only `<provider>/other`; one on a provider of your own is
only `custom`, a routing group only `group`;
- how many of each, and how many routing groups you have.
- for each partner (a sponsor listed first in the add sheet), by its id, how
many times a day the add sheet showed it, its row was opened, its key
page was opened and a provider was added from it (at most 20 of each,
5 adds), sent the day after as `magpie partner` events.
No names, base URLs, accounts, keys, prompts or usage go. Turn that part off
in Settings → Privacy → Share the agents, providers and models I use; the
day's event then has the id, version and system only. Turn it all off in
Settings → Privacy → Count me as a user, or with `DO_NOT_TRACK=1` or
`MAGPIE_NO_STATS=1`. Builds from source never send it. The code is
[internal/stats](../internal/stats/stats.go).
## Community
Questions, setups worth sharing, ideas, bugs: come talk to us and other
magpie users on [Discord](https://discord.gg/vGSnD3ZKQF). Issues and pull
requests are welcome here too.
## License
MIT. See [LICENSE](../LICENSE).