Commit Graph
84 Commits
Author SHA1 Message Date
Aliceandraper 0260716c9d feat(usage): 可选的 OTLP 元数据导出(Trace/Metrics,支持 Langfuse) (#412)
* usage: add opt-in OTLP HTTP metadata export

Export gateway traces and optional duration/token histograms asynchronously with a bounded queue, retries, proxy support and a bounded shutdown. Add bilingual Settings controls and MAGPIE_OTEL_* overrides. Keep prompts, replies, sessions and provider credentials local; use the same OTLP path for Langfuse.

Validated: full Go tests (nogui), usage race tests, focused go vet, and Chromium/WebKit settings tests in English and Chinese.

Refs #213

* otel: protect collector credentials in backups and settings

Remove all OTLP headers from keyless backups and restrict both new and existing settings files to 0600 before saving. Clarify SIGTERM export behavior. Addresses the two blocking review findings in #412.

* otel: preserve local credentials and read-only settings on restore

---------

Co-authored-by: raper <raper@raperdeMacBook-Air.local>
2026-10-02 01:51:04 +08:00
czyt bde39aef5e gateway: advertise MAGPIE_PUBLIC_URL in the console and CLIs (#373) 2026-10-02 00:46:45 +08:00
Ns2Kracy bb1aa3acbd feat(gateway): named gateway keys with per-client usage (#344)
* feat(gui): show and filter usage for each API key

* feat(gateway): issue per-user caller keys and attribute usage

* feat(gui): manage gateway users and filter caller-key usage

* test: verify caller-key failover and document user management

* refactor(gateway): use named caller keys without user accounts

* feat(gui): manage named caller keys with existing account styles

* fix: remove user breakdown from CLI and leftover gateway styling

* fix(usage): drop stale user field from request response

* feat(gateway): authenticate LAN calls with named caller keys

* fix(gateway): preserve LAN key rotation in the named caller store

* fix(gui): retain native LAN key copy and rotation controls

* fix(gui): inherit the global theme for API key inputs

* fix(usage): show client API keys using the existing terminology

* fix(gateway): rotate named keys without changing usage identity

* fix(gui): unify gateway keys and selectable connection examples

Keep sharing controls in Settings and manage credentials only in Gateway. Refresh the named-key list on navigation, ignore stale reads after key actions, and generate all API examples from the selected address and enabled key.

* fix(gui): keep caller key names literal in connection menus

* fix(access): use Magpie as default key and drop user migration

* refactor(gui): name caller key source and tests consistently

* refactor(usage): split provider-key accounting out of caller-key PR

* fix(gateway): preserve LAN compatibility and add headless key management

* fix(gui): clarify gateway keys and confirm destructive actions

* test(gateway): guard caller attribution under 120 concurrent requests

* fix(gateway): keep open access, tolerate migration errors and revoke legacy keys

* fix(backup): preserve named gateway keys and honor no-keys restores

* fix(gui): keep local API-key semantics and disambiguate shared credentials

* docs(gateway): keep review scope focused and append browser test documentation

* fix(backup): retain existing default keys when old backups lack their store

* fix(gateway): attribute video creation usage to the caller key

* fix(access): explain default key revocation blocked by read-only settings

* test(usage): preserve provider and caller identities after rebase

* test(gui): reconcile caller key fixtures with merged main

* docs(usage): clarify caller coverage and independent provider accounting

* fix(gui): initialize fast members for model-shortcut groups

Latest main introduced fast-member arrays but omitted the model shortcut's draft. The existing unlisted-models browser regression fails unchanged on main. Initialize fast to an empty list and assert a standard-speed group is saved, without relaxing existing assertions.

* fix(access): retain legacy LAN access when the key store cannot be created

Allow only the configured legacy credential before either migration marker exists. Refuse revoked mirrors, disabled matches and unreadable stores; keep the fallback's caller identity stable when migration later succeeds. Cover read-only configuration directories, remote credential transports and revocation.

* fix(backup): save settings before replacing gateway credentials

Leave the key store untouched when settings validation or writes fail. Restore the previous settings association when the key store refuses its write, and report a rollback failure separately. Cover invalid settings and read-only settings/store failures.

* fix(gui): reset missing caller filters and focus key controls without scrolling

* test(provider): synchronize Zhipu quota fixture observations
2026-10-01 21:28:54 +08:00
Zhu Wang 1adb2b571e feat(routing): group requests by session with names and estimated costs (#396)
* feat(routing): group requests by session with names and estimated costs

* fix(routing): address session view review feedback

Preserve Luna Reserve and other header fallbacks for ordinary Codex turn metadata; reuse metadata and the kind across trace and native accounting. Default to By request and persist the grouping choice. Store only token accounting fields in route history while retaining legacy JSON compatibility.

Add regressions for helper precedence, native kind accounting, history compatibility and grouping preferences.
2026-10-01 20:50:15 +08:00
yetone 1474b78f19 gateway: GET /v1/magpie/route tells a session's route as it happens (#405: a status bar wants the routed model and effort before the first token) — routing decides a group's member, and its decision model the effort, before the vendor is asked, but an agent's UI only learned it from the reply. A route now remembers the session its request named (X-Magpie-Session, or the agent's own session header; kept out of the trace's JSON and history), and GET /v1/magpie/route?session=<id> answers that session's latest route: the model asked, the group, the rule matched (with the effort picked), the member being tried and the effort it was sent at, every try with why a fallen-over one failed, then done/status/served. after=<seq>&wait=<s> (up to 60) waits for it to change. Only the named session is told; like /v1/magpie/quotas it answers this machine, and another only with the shared gateway's key. TestSessionRoute fails without the change (404). go vet, go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui ./... pass (TestUserPath and two Claude account tests flaked under load and pass alone). 2026-10-01 17:16:38 +08:00
Ns2Kracy c900734020 feat(usage): account for upstream provider keys (#364)
* feat(usage): attribute gateway usage to individual API keys

* refactor(usage): keep provider accounting focused on CLI and CSV

* fix(usage): attribute System One calls and name upstream key fields explicitly

* fix(usage): leave keyless System One requests unattributed
2026-10-01 14:33:18 +08:00
yetone 23472507c0 agents: Muse Code, Meta's muse CLI, is an agent magpie sets up (Discord: Muse Code 能加进来吗) — magpie points its endpoint_transport (~/.config/muse/settings.json, $XDG_CONFIG_HOME) at the gateway with auth "none", so the Meta sign-in's token is neither read nor sent, and sets its model to the catalog id; the endpoint and model the user had are stashed and put back on reset, the file's other keys (mcpServers, tui, proxy…) kept, and a settings file magpie had to make (with the schema_version Muse refuses one without) goes again. Muse lists its models from /muse-code/models on its endpoint's host before every session and starts none without it, so the gateway now serves that list, each row with the metadata["muse-code"] Muse shows and a limit with both window and reply size (one without either is hidden; none has it ask for 128K-token replies). A Responses stream the gateway translates from another API started with output null, which Muse reads as a broken stream and retries ten times; it is an empty list, as OpenAI's is. Its requests (muse-build/…) count under Muse Code in usage; Meta's icon from dev.meta.ai. Checked with muse 1.4.2 in a sandbox HOME against a sandbox gateway: exec answered through a Responses fake and through a chat fake (25 tools flattened, a read_file round trip), and switching back left settings.json byte-identical, or removed the one magpie made. TestMuse, TestMuseNoSettings, TestMuseModels and TestResponsesStartOutputList fail without the change (TestDriftUnwiredEveryAgent covers it too). go vet, go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui ./... pass (TestSavedButSignedOutClaudeSaysWhy flaked once, passes alone); click-scroll, old-webkit pass. 2026-10-01 14:23:04 +08:00
Comic Changandyetone 024f494bc4 Allow provider-specific upstream model names (#280)
A provider may serve a model under an id of its own: namespaced with a
prefix, dated, a `-latest` that is not what models.dev calls it, or simply
a different spelling. magpie sends the name it knows the model by, and the
provider answers that it has no such model.

  magpie model wire relay-b/model-2 vendor-c/model-2-preview
  magpie model wire 'relay-b/*' 'vendor-c/*'
  magpie model wire relay-b/model-2 --reset

A `*` in the **value** stands for the model, so `relay-b/*` set to
`vendor-c/*` sends `vendor-c/model-3` for model-3. A value with no `*` sends
every model of that provider under one name, which is the case where a relay
really does serve them alike. The CLI shows the pattern the way it was
given, with a concrete line for one of the models, so what is in force is
not something you have to imagine. An exact key wins over the provider's.

The canonical id stays canonical. `Entry.Model` is not rewritten: it is the
key automatic routing groups are built from, what `Resolve` returns as the
model's identity, what `GET /models` lists, what an agent asks for, and what
a call is recorded and priced as. The alias goes into the request's model
field and nowhere else — which is the part that was wrong at first.
Handing it to the serializer gave the provider's name to the code that
decides what a request should look like: `buildChat` checks the model for
Gemini compatibility, `buildAnthropic` matches it for adaptive-only and
effort handling, `buildCodeAssist` maps by model. A provider serving a
model under a namespaced id got a request shaped as if it were that other
model. So the request is shaped from the model magpie knows and the field is
rewritten once at the end, with `rewriteModel` — which already existed to
put one provider's own model name in a body and keeps every other byte as
it was.

It has to reach every path. A request the provider understands as it came
is relayed by `passthrough` without being rebuilt, and the Anthropic token
count is asked of the provider directly; both rewrite the field too, so on
a provider speaking the client's own protocol — the common case for a relay
fronting one vendor — the alias still applies.

"Swapped" compared the canonical id with what the vendor's reply named, so
an alias that is not a prefix or a dated variant marked every call swapped.
It compares the name that was actually sent, in the gateway and in the
ledger alike; the ledger reads the map once for the lot, since its rows are
what a reader sets beside a bill. The Codex backend path is left alone: it
sends magpie's own pick and never an alias.

`provider.Test` asked with the canonical id, so a provider that only accepts
the alias reported a failure while its real requests worked. It asks with
the alias now and still reports the canonical id, since that is the one the
user picked.

A blank name was a hole: the setter trimmed what it was given, so a value of
only whitespace was removed on the way in, while the read path would have
used it had anything else written it — a hand-edited file, or a build from
before the trim. Both ends treat a blank as no name now. The key is
checked where it is written rather than in Save, so one hand-edited entry
cannot block every save, and #288's walk moves it on a provider rename
without a line here.

Co-authored-by: yetone <yetoneful@gmail.com>
2026-10-01 13:03:57 +08:00
yetone b69fd168c2 remote magpie: its image models reach this magpie, and look-alike models say which provider they are from (Jorben and 莫 on Discord) — another magpie's /v1/models listed chat models only, so its image models were never found here unless their id said "image"; a magpie now asks for them (X-Magpie-Drawers), the other lists them marked kind:image, and they show in Settings → Images and draw through its /v1/images/generations, which asks the model's vendor the way it draws there. Each model also comes with its name and its provider there (magpie_label), so two relays' "Claude Sonnet 5" no longer read the same. An older magpie on either side reads the list as before. TestRemoteMagpieModels fails without the change; TestRemoteMagpieOldList covers an old remote. go vet, go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui ./... pass; remote-magpie passes. 2026-10-01 12:05:21 +08:00
yetone 4ba51bb627 plugin: a plugin names its own icon and the key field's title and hint (Lemon on Discord) — a plugin's provider took only the market's icon, and an api method's key field always read "<name> API key" with no hint. auth.icon (or package.json "magpie": {"icon"}) as an https URL, fetched once through the public-only icon path, or a data:image URI now comes before the market icon; an api method's label titles the key field as in OpenCode, and a new placeholder hints it, in the window and in magpie plugin login. README has a For plugin authors section. TestPluginOwnIcon, TestPluginOwnIconAndKeyHint and plugin-key-hint.test.cjs (Chromium, WebKit, en/zh) fail without the change. 2026-10-01 11:42:45 +08:00
yetone 0bb119f87f docker: sign-ins and plugins survive a restart, and the image checks its own health (Jorben on Discord) — only /config was a volume, while HOME (/home/nonroot) held the agents' sign-ins (~/.codex, ~/.claude…) and the cache held Bun: a new container lost them all, and distroless/static has no libc, so the Bun magpie downloads couldn't start (fork/exec … no such file or directory). HOME and XDG cache/data/state now live under /config (made on start, so an old volume keeps working), the base is distroless/cc (glibc), and magpie healthcheck backs a HEALTHCHECK with no curl needed. Verified in Docker: an old volume carried over, a market plugin installed and stayed across a recreate without downloading Bun again, the container reported healthy. TestHealthcheck and TestMakeDirs fail without the change. 2026-10-01 11:38:37 +08:00
yetone b4c6772677 lan: magpie in Docker shares an address other computers can reach (莫 on Discord) — inside a container the LAN address shown was the container's own (172.17…). MAGPIE_PUBLIC_URL now names the share address; without it, a page opened through the host's IP shows that host, and the Gateway page says it runs in a container and how to set the address. lan-docker.test.cjs (Chromium, WebKit, en/zh) and the serve/lan tests fail without the change. 2026-10-01 11:29:05 +08:00
yetone 471956c837 agents: dsh takes magpie as a custom provider of its own, Magpie, not by taking over its DeepSeek one (01huadalang on Discord: 集成进 dsh 会有 bug,会被 dsh 覆写,应该走 dsh 的那个另外的供应商) — magpie rewrote dsh's llm-deepseek entry in every profile to point at the gateway with the key MAGPIE_API_KEY. dsh's Models page showed it as DeepSeek, so a DeepSeek key typed there went into dsh's key store under that name, which dsh reads before .env: the next request carried the DeepSeek key and failed, magpie seemingly overwritten; dsh's own DeepSeek models were gone meanwhile. magpie now adds a magpie provider under llm-pi-ai, as dsh's Add model provider → Custom model API writes one (gateway URL, openai-completions, MAGPIE_GATEWAY_KEY, magpie's models with their limits, images and effort levels), and points the default model at it; llm-deepseek and the user's other providers are left alone, a profile wired the old way is moved on sync with its model and effort kept, new profiles get it, switching to a dsh model or clearing takes it out, and the check says when dsh's key store holds another MAGPIE_GATEWAY_KEY. Verified with dsh 0.2.0-rc.2 in a sandbox HOME against a fake gateway: headless and web requests carry magpie's token, a DeepSeek key saved in dsh's Models page no longer displaces it. TestDshCustomProvider and TestDshMovesOffDeepSeekRow fail without the change. go vet, go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui ./... pass. 2026-10-01 06:34:13 +08:00
yetone 09e7a3f147 signin: a sign-in whose browser can't reach magpie finishes with the address pasted (Jorben on Discord: ChatGPT sign-in in Docker on a server never finished) — ChatGPT sends the browser back only to localhost:1455, which is the user's own computer, not the server, and magpie's paste field refused every built-in sign-in. Claude, Codex, Devin, Kiro (both steps of an AWS sign-in), Gemini, Antigravity and Zed now take the address the browser ended on, in the app and in magpie accounts add: only this sign-in's own port, path and state, traded through the same handler as the browser's, and once, whichever comes first. TestCodexSignInFromPastedAddress, TestPastedAddressAndCallbackFinishOnce, TestLoopbackSignInsTakePastedAddress and signin-paste-codex.test.cjs fail without the change. 2026-10-01 04:09:16 +08:00
yetone 04805abf39 agent: MiniMax Code (#362) — MiniMax's mcode CLI (and its desktop app, same runtime) reads $MINIMAX_DATA_DIR/config.yaml, else ~/.minimax/config.yaml. Choosing a magpie model adds custom_provider.magpie there (the gateway, spoken to as Anthropic messages, with the catalog's models, efforts, limits and image support) and sets defaultModel to custom_provider:magpie/<model>; the user's own default and variant are kept and put back on reset, their comments, providers and keys on magpie's entry stay, and an emptied custom_provider goes. The Library installs skills into its skills folder; it reads no user-wide MCP file, so it gets no MCP target. Checked against @minimax-ai/code 0.5.10 in a sandbox HOME: mcode provider list shows magpie's entry and mcode exec reaches a fake upstream through it with magpie's key, model and effort; after a reset it loads the restored file. TestMiniMaxCode, TestMiniMaxCodeSync, TestMiniMaxCodeLeavesNoEmptyProviders, TestMiniMaxCodeDataDir and TestMiniMaxCodeSkills fail without the change. go vet, go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui ./... pass (TestUserPath and the Claude account tests pass alone). 2026-10-01 03:24:34 +08:00
Comic Chang 49f40b463f Configure per-model context windows and output limits (#279)
A provider that serves a model the catalogue does not list, or lists at the
wrong size, has a window and a reply limit magpie cannot know: both fall
back to whatever the catalogue says about the model's maker, which for such
a provider is nothing at all.

  magpie model context relay-a/gpt-5.5 262144
  magpie model output  relay-a/gpt-5.5 131072
  magpie model context 'relay-a/*' 200000
  magpie model output  relay-a/gpt-5.5 --reset

Both are looked for in the same order as each other: this model, then
`<provider id>/*`, then the provider's own list, then the catalogue. A
model's own value wins over its provider's. The reply limit is new; the
window is not — `Provider.Contexts` has been there, with a `"*"` of its own,
settable through `magpie provider set <id> context.<model>=`, and this
reuses it rather than shadowing it.

That reuse is the point rather than a detail. A context is a routing input
and not only a number agents are shown: at 95% of a held member's window a
request moves to a member that takes more, so two sources of truth would
mean the gateway routing on one number while agents are told another.
`magpie model context` is a second way to set the same field, not a second
field.

Both reach the agents. Pi keeps a model's window and reply limit in its own
models.json, OpenCode keeps its limit, and Crush, Droid, Cline, Qoder and
Zcode keep their own; an agent reads those files when it starts, so a limit
set here that never reaches them is a limit that does not take effect.
`magpie provider set` has always told the agents, and this behaves the same
way rather than introducing a second, quieter path. The test asserts each
setter reaches them exactly once — the two paths differ (one saves the
provider file, the other settings and then says so) and that difference is
worth pinning. An end-to-end pass confirms it: with a Pi models.json in
place, `magpie model context relay-a/sol 999999` writes `contextWindow:
999999` into `providers.magpie.models`.

A `0` is refused rather than read as a reset, naming `--reset`; a mistyped
model id is refused the way `magpie model name` refuses the same id; a reset
says what is actually in force instead of claiming to be back to the
vendor's list while a provider-wide value still applies; and
`magpie model context <provider>/*` reports that value rather than saying
the provider serves no such model. Validation happens where the entry is
written, so one hand-edited bad key cannot stop a theme or a window size
from saving, and #288's walk moves these on a provider rename without a
line here.
2026-10-01 02:14:22 +08:00
yetone 4f9acf72a4 subscriptions: Qoder CN, for accounts on qoder.cn (#349) — Qoder's China site (qoder.cn, Alibaba Cloud or phone sign-in) has its own accounts, sign-in page, account API (openapi.qoder.com.cn) and gateway (gateway.qoder.com.cn), so a qoder.cn account couldn't sign in through magpie's Qoder, which only knows qoder.com. A new Qoder CN subscription signs in there with the client id Qoder CN's own CLI sends, and everything after — polling, job token and refresh, user info, models, usage and chat — follows the site saved with the account; if qoder.cn refuses the job token, the account chats with its device token and renews it, as that CLI does. Qoder's tile is now Qoder (international), each tile saying which accounts it is for. Accounts saved before keep working on qoder.com unchanged. Not tried with a real qoder.cn account. TestSites, TestCNDeviceChatRefresh, TestQoderCNSignIn, TestQoderCNDeviceChat, TestQoderGlobalUnchanged and TestQoderCNGateway fail without the change; qoder-cn-tile.test.cjs (Chromium, WebKit, en/zh) added. go vet, go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui ./... pass but for TestCursorPluginContext, which fails on main (1ce05f0) as well; click-scroll, old-webkit pass. 2026-10-01 01:42:12 +08:00
yetone ba3c26dec4 providers: a Remote magpie provider uses another computer's shared magpie, each request translated at most once (Jorben on Discord: 远程 magpie / magpie 接力) — a custom provider pointed at another magpie has one API per address and couldn't know which API each model's own provider serves, so a request was translated on both computers (Chat → Messages here, then again there). magpie's /v1/models now tells each model's native_endpoints (the APIs its provider serves it on; none for a routing group) and its image input, and reads them, with max_output_tokens and supported_reasoning_levels, from a list; the new remote-magpie preset takes the other magpie's address however typed (host:port, /v1, /v1/messages) for all three APIs and lists all its models, its user's choice already. Add sheet tile with the address field and a hint to Settings → Share on local network; CLI: magpie provider add remote-magpie <key> url=http://<ip>:3425. Checked with two sandbox magpies over the LAN address with the key check on and a Messages-only fake upstream: Chat, Messages (streamed), count_tokens, Responses, a routing group and Gemini all reached it as /v1/messages, translated once. TestRemoteMagpieNativeAPI, TestFetchMagpieFields and TestRemoteMagpie fail without the change; remote-magpie.test.cjs (Chromium, WebKit, en/zh) added. go vet, go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui ./... pass but for TestCursorPluginContext, which fails on main (1ce05f0) as well; click-scroll, old-webkit pass. 2026-10-01 01:35:18 +08:00
yetone d1491a43e8 gateway: X-Magpie-Account pins a request to one account of a subscription (gakki on Discord: 想单独测某个账号) — a subscription with several accounts took any of them in turn, so a probe of one account could be answered by another. The header (the account's email or login, or its id on the Routing page) narrows the request to that account alone: an unknown one is a 404 naming the accounts there are, one whose plan lacks the model a 400, one resting a 429 with until when, and nothing else is tried in its place (nor a Codex reset spent by itself). Pooled Codex accounts are pinned the same through /backend-api; the header is never sent on to the vendor, and the Routing page's trace records it. TestAccountPinned, TestAccountPinnedUnknown and TestAccountPinnedResting fail without the change. go vet, go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui ./... pass (TestClaudeAccountNamedAsItsLogin timed out once under load, passes alone). 2026-09-30 22:02:11 +08:00
Comic Chang 901ee08280 Count usage at the configured provider/model price (#278)
Magpie reports models.dev's list price for every call. That is the right
number for a vendor's own key and the wrong one for a provider that
resells or exposes a model at a different tariff, and it leaves a model the
catalogue does not list with no cost at all — a provider's own /models
gives ids, not economics.

  magpie model price relay-a/gpt-5.5 0.12,1.20,0.01,0.15
  magpie model price relay-a/* 0.12,1.20,0.01,0.15
  magpie model price relay-a/gpt-5.5 --reset
  magpie model prices

Absolute USD per million tokens. All four parts are required: a price
missing one would understate the rest of every call, and a hand-edited file
that left a part out would bill it at zero, so each part is a pointer and
the setter refuses a price that is negative, NaN or infinite, naming the
part at fault. Zero stays distinct throughout — a model served at no cost
has a price, not the absence of one. A `<provider>/*` entry covers every
model of that provider and an exact entry wins over it; --reset removes
only the exact entry and says so when a provider-wide one is still in
force.

The order a price is looked for in is the price for this model, then
`<provider>/*`, then the provider's own catalogue, then models.dev for the
maker. A price is a provider's tariff, not the model's: the same model
through two providers has two prices, and each keeps its own. A provider is
resolved by its id, or by an id it was renamed from, and never by its
display name — Find answers those too, and a record naming one was counted
at another provider's price.

provider.EffectivePrice is the one lookup every cost caller shares, so the
usage ledger and a session's totals both go through it, and a session
naming a model bare is still priced at its maker's list price, which is a
different question from what one provider charges. catalog.PriceOf and
PricedBy keep returning the models.dev price for anything wanting an
unbiased number. The background-task pickers (draw, search, vision) still
rank by catalogue price: they choose which model answers, not what a call
is reported to cost.

Unlike every other per-model preference this does not call
catalog.Touched. What a call costs is not what an agent picks a model by,
and the model lists magpie keeps in the agents' own files are not its to
rewrite over a number in a cost report. A rename moves the prices with it
through the walk #288 adds, so nothing here has to say so, and an id
recorded before a rename still resolves.

Validation happens where the entry is written, not in Save: one hand-edited
bad price in the file must not make saving a theme or a window size fail. A
price for a model the provider does not serve is refused the way
`magpie model name` refuses the same id, since it is a price that never
applies and nothing reports it as dead; a removal is exempt, so an entry
left for a model that has since gone can still be taken away.
2026-09-30 20:31:11 +08:00
yetone 1ba6939185 agents: OpenChamber gets its own row, its model and small model set to magpie's (#321) — OpenChamber keeps its own default model (preferences.json, a copy in settings.json, under $OPENCHAMBER_DATA_DIR or ~/.config/openchamber) and sends it with every session over OpenCode's, so setting OpenCode's model changed nothing it used. Its row writes defaultModel (a fresh updatedAt, defaultVariant cleared as its own settings do) and the small-model override, keeping every other key and refusing a preferences.json it can't read; magpie's provider goes into the OpenCode config it runs on, and stays there while either uses it. OpenCode's config now follows $OPENCODE_CONFIG_DIR: the OpenCode 2 OpenChamber bundles reads only that folder, so what magpie wrote to ~/.config/opencode was never read; OpenCode 1 reads it last, so it wins there too. A project with its own model in OpenChamber is named in the row. Checked against @openchamber/web 2.0.4 on OpenCode 2.0.18 in a sandbox HOME: its settings API returned magpie's model and OpenCode listed magpie's provider. TestOpenCodeConfigDir, TestOpenChamberModel and TestOpenChamberSettingsOnly fail without the change. go vet, go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui ./... pass; click-scroll, old-webkit pass. 2026-09-30 20:12:07 +08:00
yetone 8931a4b3ac davsync: sync can keep its file in an S3-compatible bucket as well as a WebDAV folder (#296) — AWS S3, Cloudflare R2, Backblaze B2, MinIO, Garage or a NAS. Settings › Sync and backup picks WebDAV or S3: endpoint (empty is AWS), bucket, prefix, region (empty is us-east-1, auto for R2), access key ID and secret, path-style; CLI magpie s3 on s3://bucket/prefix access-key-id=… endpoint=… region=… path-style. The bucket holds the same passphrase-sealed file at <prefix>/magpie/magpie.magpie-backup, signed with SigV4 (no SDK). It is read with its ETag and written with If-Match (If-None-Match: * for the first), so two computers syncing at once don't overwrite each other; a server that refuses conditional writes (501, or 400 naming the header) is remembered and gets a HEAD-then-PUT instead. An IP, localhost or a dotted bucket on https goes path-style on its own. Errors say what to do: wrong key/secret, clock skew, bucket in another region (names it), no such bucket, 403 on the first read on AWS without s3:ListBucket. The secret is kept as the WebDAV password is, sent only to the endpoint and key it was typed for, and may not be the passphrase. sigv4_test (AWS's published vectors), s3_test (fake server: conditions, conflict, fallback, regions, errors), s3_cli_test and s3-sync.test.cjs (Chromium, WebKit, en/zh) fail without the change; tested against MinIO. go vet, go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui ./... pass (TestGroupAddSetRemove, a TempDir-cleanup flake under load, passes alone); click-scroll, old-webkit pass. 2026-09-30 17:07:47 +08:00
yetone a31f97b30d plugin: OpenCode's provider plugins sign in to subscriptions in magpie — an npm package OpenCode users install for a plan (its auth hook: loader, methods, oauth authorize/callback, api keys, chat.headers) runs in a Bun host magpie downloads the first time a plugin needs it, speaking JSON-RPC over stdio; its sign-ins are kept in plugin-auth.json (0600). A provider a plugin signs in to is a provider like the others: its models the plugin lists, each spoken to on the API its AI SDK package speaks (anthropic → Messages, openai/azure → Responses, google → Gemini, Copilot's GPT-5+ → Responses, else chat completions), its requests carried by the plugin's fetch to the base URL its loader gives; an id magpie already has is <id>-plugin. CLI: magpie plugin [list|add|rm|update|on|off|login|logout]. App: Settings → Plugins adds, updates, switches off and removes them and says why one didn't load; Add provider → From plugins signs in with the plugin's methods, its questions (picks and checked text), the browser and a pasted code or a key, followed as the built-in sign-ins are; removing the account signs out. The built-in subscriptions stay. TestFakePlugin, TestPluginProvider (chat, Anthropic and Gemini models through the gateway) and TestPluginSignIn (a wrong code fails, the right one signs in, removing signs out, a key) run a fake plugin on Bun; plugin-signin.test.cjs (Chromium, WebKit, en/zh) covers the add sheet and Settings. signin-callback, add-sheet, zed-sub, account-proxy, own-signin-remove, routing-signin, click-scroll GUI tests pass; go vet, go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui ./... pass (TestUserPath, TestClaudeSignedOut, TestClaudeAccountNamedAsItsLogin, TestClaudeIdentityServedWhileAsked, known flakes under load, pass alone). Not yet tried with a real plugin and account. 2026-09-30 16:44:32 +08:00
yetone d0d0fcef76 providers: DimAgent's subscription is taken out of magpie (a DimAgent team member on X: dim doesn't allow its subscription used through a proxy, and users doing so get banned or lose their discount) — #226 signed in to DimAgent as its desktop client does and served its models through the gateway. The sign-in, the account provider and its dimagent/<model> models, the quota card, magpie accounts … dimagent and the icon are gone, with internal/dimagent. An account signed in before is left out when logins.json is read and dropped from the file at its next write, so its tokens aren't kept. The pasted-callback field and route stay, generic, with no sign-in using them. Tested with TestDimAgentLoginDropped (fails without the filter); in a sandbox HOME with a DimAgent login left in logins.json, magpie accounts --json lists nothing and magpie accounts add dimagent is refused. signin-callback (fixture renamed), old-webkit and click-scroll pass; go vet, go build (GUI, -tags nogui, GOOS=linux, GOOS=windows) and go test -tags nogui ./... pass (TestUserPath and the Claude/Devin sign-in tests flaked under the full run and pass alone). 2026-09-30 15:33:40 +08:00
yetone 3df75514b9 agents: each agent's own model list — under an agent's name on the Agents page, "Showing 5 / 32 models" (or "All 41 models") opens a list to take models out of that agent's picker one by one and put them back (@ChongkaiX on X: 在 magpie 里选哪些模型出现在 Codex 的模型列表里) — until now an agent's list could only be narrowed by whole providers or families (Visible). settings.HiddenModels holds, by agent, the catalog entries taken out, after Visible: a deny list, so a new model is shown. provider.CatalogFor drops them, so the gateway's /models, the agents' files and pickers and Codex's catalog all follow in one place, and catalog.Touched rewrites the agents' files; Codex's own ChatGPT models are dropped from the backend's list too (CodexNativeHidden), and the list's ETag and models_cache tag carry them (CodexListTag) so Codex asks again. The model an agent is set to is marked 在用 and never taken out, on the server as well. The list groups routing groups first, then providers with their logos and on/total, a long group folded; search, 全部/已显示, a group's 全部显示/全部隐藏 on hover, 恢复全部显示; a click on a row applies at once, Esc or a click elsewhere closes it, and nothing scrolls. A renamed provider's hidden models follow it. Tested with agent-models.test.cjs in Chromium and WebKit, English and Chinese; TestHiddenModels (provider), TestCodexModelListHidesOnAgentsPage (gateway) and TestAgentModelsAPI (gui) in sandbox HOMEs. click-scroll, panel-fold, old-webkit, model-pick, menu-scroll, add-button pass; GOOS=linux go vet, linux and windows builds, go test -tags nogui ./... pass. Not tried in the real Wails window or against a real Codex. 2026-09-30 12:44:59 +08:00
yetone 04f374fa8c agent: OmO (oh-my-openagent, omo-ai), a fork of Pi that keeps its own ~/.omo/agent, gets its own row: a magpie model sets settings.json's defaultProvider/defaultModel and magpie's provider in models.json there as Pi's does, $OMO_CODING_AGENT_DIR / $SENPI_CODING_AGENT_DIR are honoured, the Library writes its AGENTS.md, mcp.json and skills, and omo update updates it (#264 rhyme227). Pi's own ~/.pi is left alone. piIn becomes piLike(at, id, name, dir) so both share one wiring. Tested with omo_test.go in agent and library (fail without the change); go vet, go build -tags nogui (darwin, GOOS=linux, GOOS=windows), go test -tags nogui ./... pass. 2026-09-30 11:38:07 +08:00
1a704bd6ba docker: a server image and a multi-arch build workflow (#245)
* docker: a server image and a multi-arch build workflow

The image is the terminal-only binary built with -tags nogui on
distroless static, run as nonroot with magpie's files on a /config
volume, created and chowned at build time so a fresh named volume
is writable by uid 65532.

The workflow builds amd64 and arm64 on native runners, smoke-tests
each image before anything is pushed (ready probe, unknown-model
4xx JSON, /config write), pushes by digest, merges a multi-arch
manifest (semver trio plus latest on v* tags, edge otherwise) and
attests build provenance. PR runs build and smoke-test only.

The README documents running the image, the web UI mode, and why
subscription sign-ins must start where a browser shares loopback
with magpie.

Co-Authored-By: glm-5.3-flash <hakase@ooooo.space>
Generated-By: omp 18.4.3

* docker: README publishes the ports on the host's loopback and says how to require a key — until the gateway is shared from Settings it takes any key from anyone who reaches it, and Docker's -p 3425:3425 puts it on every interface of the host past its firewall. Checked in the built image: with settings lan+lanKey a request from the host through the published port without the key gets 401 and with it 200; without them any key gets 200.

---------

Co-authored-by: glm-5.3-flash <hakase@ooooo.space>
Co-authored-by: yetone <yetoneful@gmail.com>
2026-09-30 03:21:19 +08:00
yetone cb65895ac3 agents: Factory's Droid CLI is an agent magpie sets up (#242 tasselx) — droid keeps its settings in ~/.factory/settings.json ($FACTORY_HOME_OVERRIDE/.factory when set), its BYOK models the customModels array ({model, displayName, baseUrl, apiKey, provider "anthropic" | "openai" (Responses) | "generic-chat-completion-api", maxOutputTokens, noImageSupport …}), picked as "custom:<id>". Picking a catalog model through magpie appends one entry per catalog model after the user's own (so their ids never move), each with an explicit id "custom:magpie/<provider>/<model>", the catalog's label as displayName, the gateway and token magpie, its context (maxContextLimit) and output when known, noImageSupport always written (droid takes a Chat Completions model as blind otherwise), and the API its provider speaks natively so the gateway relays it as it is: Responses → "openai" at /v1, Messages → "anthropic" at the gateway root (Anthropic's SDK adds /v1), anything else "generic-chat-completion-api" at /v1; and sets sessionDefaultSettings.model. The model shows in magpie as magpie/<ref>; the user's sessionDefaultSettings.model is stashed and put back on reset (a top-level "model", the docs' spelling, is left alone and is what droid falls back to), magpie's entries go and an emptied customModels or sessionDefaultSettings with them; everything else in the file, the user's entries included, is kept byte for byte. The picker offers the user's own custom models by the id droid gives them (its "id", else the displayName with whitespace as dashes and a count of the same name before it), including the legacy ~/.factory/config.json custom_models, which magpie never writes. Sync rewrites magpie's entries as providers change; Check reports a missing entry or a changed baseUrl/apiKey; its requests are Droid's in usage (User-Agent factory-cli/<version>). Icon is Factory's own favicon glyph (factory.ai/favicon.svg); lobehub has none. From the docs: docs.factory.ai/cli/byok/overview (customModels fields, the three providers, config.json legacy snake_case merged under settings.json), /cli/configuration/settings (model, sessionDefaultSettings.*), /droid-exec/overview (custom:<Display-Name>-<index>). Verified by reading the shipped droid 0.229.0 bundle (@factory/cli-linux-x64, unpacked, never installed or run): the zod schema of an entry (also id, index, maxContextLimit, enableThinking, reasoningEffort, bedrock-converse), ids as custom:${displayName.trim().replace(/\s+/g,"-")}-${n} with n counting equal names, an entry's own id winning, the lookup by id, top-level model/reasoningEffort/specModeModel moved into sessionDefaultSettings only when unset there, getModel reading sessionDefaultSettings.model, noImageSupport defaulting by provider, FACTORY_HOME_OVERRIDE, and BYOK requests carrying factory-cli/<version>. Tested in a sandbox HOME (internal/agent/droid_test.go): set/sync/reset on a settings.json as droid leaves it with a Groq BYOK model (its entry byte-identical, the file restored exactly), each protocol's provider and baseUrl, a moved baseUrl caught by Check and healed by Sync, picking the user's own model while on magpie's, no file at all, a top-level model, FACTORY_HOME_OVERRIDE and legacy config.json ids (custom:Local-Qwen-0/-1) offered and never written. go build -tags nogui (and GOOS=linux, GOOS=windows) and go test -tags nogui ./... pass (TestClaudeSignedOut and TestClaudeAccountNamedAsItsLogin flaked under load and pass alone). Not tried with a real droid: whether droid's /model lists the entries, that it reaches the gateway on each protocol, and how it sends thinking for a custom model (magpie sets no reasoningEffort/enableThinking) are unverified. 2026-09-30 02:35:59 +08:00
Karenandyetone 2693cd8828 feat(gui): choose macOS session terminal (#241)
* feat(gui): choose macOS session terminal

* session terminal: build the darwin CLI without cgo, drop the dead macOS 11 path, and open Terminal when the .command default is not a terminal

- terminal_apps_darwin.go imports "C" but had no cgo tag, and terminal_apps_other.go was !darwin only, so `CGO_ENABLED=0 GOOS=darwin go build -tags nogui .` (make release-cli's darwin targets) failed with undefined discoverTerminals. The cgo file and its test are now darwin && cgo; the stub is !darwin || !cgo.
- magpie needs macOS 12 (LSMinimumSystemVersion, -mmacosx-version-min=12.0), so the macOS 11 branch (temp .command file, deprecated LSCopyApplicationURLsForURL, CFRelease) never ran; it is gone along with the CoreServices link.
- With no saved choice, a default .command handler that is not a listed terminal, or a failed lookup, now opens Terminal as before the PR instead of failing; Settings labels the system option "System default (Terminal)" then, and the unused "System default (unavailable)" string is dropped.
- session-terminal.test.cjs covers an editor as the default in Chromium and WebKit, en and zh, and is on the README's node --test line.

---------

Co-authored-by: yetone <yetoneful@gmail.com>
2026-09-30 02:36:12 +08:00
Ethan Xuandyetone 2f00db65e6 feat: add DimAgent subscription support (#226)
* feat: add DimAgent subscription support

Add OAuth PKCE login with automatic and pasted callbacks, account management, model discovery, quota reporting and gateway integration. Safely rotate credentials during concurrent requests and preserve fresh tokens when updating model caches. Cover protocol, provider, GUI and callback behavior with regression tests.

* dimagent: a refresh hiccup inside the day's lead keeps the token that still runs

The fallback checked !dimagentNearExpiry, which is false whenever a refresh
runs at all, so a 5xx/timeout on /oauth/token failed every request for the
last 24h of a still-valid token. A 401/403 still lapses the account.

* dimagent: review tests (sign forwarding, parallel refresh once, hiccup in lead, LoginUsage)

* dimagent: signing in again takes the callback port from the sign-in it replaces

StartSignIn begins the new flow before it cancels the old one, so the new
flow's listen on 54321 failed (ignored) and the old listener then shut
down a second later: the browser's callback was refused and only pasting
could finish. The older DimAgent flow is now ended and its server closed
first, and the listen retried briefly while the port is let go.

* dimagent: the fake-upstream tests get a name of their own

---------

Co-authored-by: yetone <yetoneful@gmail.com>
2026-09-30 01:11:29 +08:00
Austie 86a2b8cf47 providers: qianfan personal, team and pay-as-you-go endpoints (#211)
* providers: qianfan personal, team and pay-as-you-go endpoints as its preset regions

* providers: qianfan review fixes — pay-as-you-go fetches /v2/models, enterprise naming, the plans' union models, a zh note

* providers: qianfan plans list nothing, pay as you go does — NoList scoped by region

* providers: qianfan review round two — old id maps to the new preset, the union models, IAM keys and a README section

* providers: qianfan key link follows the region — normalize takes it from the endpoints, the button from the plan picked

* providers: ParseImport takes a region key page
2026-09-29 23:25:56 +08:00
yetone 30300c40d0 providers: NVIDIA and ModelScope are presets (#197: 增加 NVIDIA NIM 和魔搭 ModelScope 作为供应商预设) — NVIDIA NIM at integrate.api.nvidia.com/v1 with a key from build.nvidia.com, and 魔搭's API-Inference at api-inference.modelscope.cn/v1 with a ModelScope access token (not DashScope, which the Qwen presets are), each with its models.dev catalog (nvidia, modelscope) and its logo. NVIDIA's /v1/responses answers 404 for its models but a few, so it has chat completions only; ModelScope's /v1/responses is served (401 without a token, 404 for a path it doesn't know), so it has both. Neither has an Anthropic endpoint here. An entry imported at either base URL is the preset, and the import link pages and README list them. Tested: the presets' endpoints, catalogs and import, a /v1/models answer shaped as each sends it (NVIDIA's embedding models left out, ModelScope's object "" rows kept), and catalog.FetchAt against the live integrate.api.nvidia.com (74 models) and api-inference.modelscope.cn (35) lists 2026-09-29 16:40:02 +08:00
Ethan Xu 81736b2fb2 subscription: Qoder is a subscription you sign in to and route throug… (#162)
* subscription: Qoder is a subscription you sign in to and route through — Qoder lists no public API, so magpie speaks the endpoint its own desktop client uses: api3.qoder.sh's agent_chat_generation SSE, wrapped in the COSY envelope the client signs with (an RSA-encrypted key and a signed request line), the body through the client's own base64 codec, and the account reached by Qoder's OAuth device flow (qoder.com -> a dt- device token -> a jt- job token that serves models and rotates from its refresh token). The protocol lives in internal/qoder (endpoints, codec, cosy, oauth, models, a credential store, and /api/v1/userinfo, asked with the device token at sign-in so the account is labelled by its email and name rather than a bare uid); provider/qoder.go is who is signed in, its models, and the sign-in; gateway/qoder.go turns a request into a Qoder body and reads the SSE back, folding tool calls and results the way the client does. It is offered as a single, risky subscription in the Providers page: Qoder has no public API for this, and may treat a request that isn't its own client's as third-party use — the tile says so and points at an account you can afford to lose. Tested: the codec, the COSY signing, the model list, the request body and a served round-trip against stubs, and live — a signed-in account answered a Pi request for qfmodel end to end. Not tested: the userinfo email/name path against a fresh real sign-in (the device token is short-lived), and multi-turn tool use against the real client's exact framing.

* subscription: Qoder uses the chosen model and keeps rotating tokens consistent

Qoder used qfmodel regardless of the caller's model, refreshed the same rotating token concurrently, and could return upstream failures as empty successful replies. Requests now use the selected account's enabled model key and complete model_config; unknown or disabled models are refused. Token checking, rotation and atomic persistence are serialized, each caller receives a separate credential, and a rotated pair is retained for a persistence retry if its disk write fails.

Accounts are kept in logins.json with multiple accounts, selection, secondary-account toggles, usage and removal, including the last account. The old qoder.json is migrated after a successful write. A sign-in generates one machine ID that the account keeps for subsequent requests; the OS and architecture follow the running platform. Login polling expires after 15 minutes.

Qoder's response path maps non-200 SSE envelope statuses to HTTP errors before output and terminal error events after streaming begins, including requests using magpie's search rounds. Tool-result headings stay beside their own results; normal EOF flushes pending text and emits a stop event even after tool calls. Thinking, effort and context settings follow the selected model (low/medium/high is the fallback when no effort levels are declared). Corrected the Chinese risk wording and account plan label, removed unused protocol fields and the unsupported China-client claim, and included ufec/CLIProxyAPI's MIT copyright and license. Codec and signature tests now have fixed expected answers instead of relying only on round trips or an absolute Windows capture path.

The shared relay in claude_subscription.go is unchanged. Its error-status propagation and handling of failures after partial non-streaming output are existing upstream issues outside this change; Qoder handles these failures in its own relayQoder response path.

Tested: Qoder, codec, COSY and shared stream-error regressions passed 20 consecutive runs, including eight concurrent requests refreshing once, persistence failure recovery, account migration and removal, sign-in and usage against local HTTP stubs, and error handling across Chat, Responses, Anthropic and Gemini. go vet ./..., the production build, JavaScript syntax checks and git diff --check pass. The complete Windows test run has the same 72 failing tests/subtests as HEAD, with no new failures; the existing Claude shell-client test still times out. Not tested against a live Qoder account. The race detector could not build because gcc is unavailable.

* subscription: Qoder keeps its rotating tokens, speaks tools in its own
API's way, and thinks at the effort each model declares

An aborted request could sign the account out. A refresh ran on the
request's own context, so one cancelled after Qoder had rotated the pair
but before its reply arrived dropped the new pair and left the spent one
saved: every later call failed with "qoder job token refresh: status 401"
until the account was signed in again. The failure wasn't recorded
either, so the account looked healthy in the app while every request
failed. Refreshing now runs on context.WithoutCancel with its own 20s
timeout, in provider/qoder.go and the device-token refresh in
qoder_usage.go, and a refresh Qoder refuses (401 or 403, either token)
marks the account lapsed: Qoder's job-token refresh returns a
JobTokenRefreshHTTPError with its status, as the device one did. Side
logins don't carry Lapsed out, so qoderLogins passes it on.

A refresh blocked every other subscription. Both held qoderMu and the
global loginsMu across the HTTP call, up to 20s — with a 2s refresh in
flight, Logins("workbuddy") blocked 1.9s. qoderMu still serializes read,
rotate and write-back so one pair is rotated once; loginsMu is taken only
to read (qoderLookup) and to write back (qoderPersist), and the write-back
re-reads logins.json, so a change made meanwhile is kept. migrateQoder
takes no lock when there's no legacy file to move, or listing Qoder's own
accounts would wait behind a refresh too.

Tool calls were carried as XML in the text with values magpie HTML-
escaped, so an argument reached the model as &#34;x&#34;, the model took
to imitating the escaping, and a value it meant literally — "< b &&"
— was unescaped on the way back and corrupted. A call had no id while
its result said "Function result for call A". Qoder's endpoint serves
OpenAI's own tool calls: asked with a tools array it answers with
tool_calls carrying ids, and accepts assistant tool_calls plus a role:tool
turn named by tool_call_id. So they are sent and read that way now: no
XML contract in the prompt, no escaping on either side, and a result
paired with its call by the id Qoder sent. The XML splitter stays as a
fallback for a model that still writes a call in its text.

Effort was chosen from reasoning_efforts, a field no model lists, so
every model got the low/medium/high fallback: gfmodel, which offers only
high and max, was asked for medium and refused with "该模型始终思考,
不支持关闭思考", and xhigh was clamped to high. The levels and the
default come from thinking_config now — asked for nothing, or for a
level Qoder doesn't name, a model thinks at its own default; a model
that can't stop thinking thinks at its lowest when asked for none; one
that declares no levels is sent none. A model whose only thinking_config
is enabled now thinks, as it does in Qoder's own client. Upstream
failures carry the vendor's reason from details, which had been dropped.

Tested: go test ./... and go vet on the changed packages, and the darwin
and windows builds. New regressions cover a refresh outliving an aborted
request, a refused refresh lapsing the account, logins and a concurrent
write-back during a refresh in flight, tool turns paired by id, native
tools and streamed tool_calls, and effort defaults. Live, against a
signed-in account: two parallel calls and a follow-up call each got
their own result; a file written with "< b &&" kept them; gfmodel,
asked for nothing, answered at max; qfmodel's xhigh reached Qoder as
xhigh. Not tested: a refresh the upstream refuses for real, and models
the account isn't offered.
2026-09-29 16:40:20 +08:00
yetone e4404dbd6b agents: Kimi Code, Moonshot's kimi CLI, is an agent magpie sets up (agent求个kimi code支持) — it keeps its settings in ~/.kimi/config.toml ($KIMI_SHARE_DIR's), its default_model a key of its [models."…"] tables, each naming a [providers.…] one. Picking a model through magpie adds the provider "magpie" (the gateway, as Kimi's own chat completions type, so the thinking goes out as reasoning_effort and comes back, and is sent back, as reasoning_content) and a table per catalog model keyed "magpie/<provider>/<model>", with its context (128K when not known) and thinking/image_in capabilities, so the catalog is in Kimi's /model picker and kept up to date as providers change; the Kimi Code sign-in's default is stashed and put back on reset, and the tables go in and out in the order Kimi's own check wants (a default or a provider it can't find makes it refuse the file). Its requests are Kimi Code's in usage (KimiCLI/…). The gateway also drops Kimi's thinking: {type: …} on a request relayed to api.openai.com, as it does Qwen's enable_thinking. Tested: the config in the shape a Kimi Code sign-in leaves it (set, sync, a moved base_url, reset, its own model, no config at all), and the real kimi 1.24.0 --print against a sandbox gateway and a fake upstream: reply and thinking shown, reasoning_effort high on a thinking model, reasoning_content sent back on the next turn, usage under Kimi Code; not with a real vendor behind it 2026-09-29 16:36:35 +08:00
Austie 6ba83dbbbb providers: add the Baidu Qianfan Token Plan (个人版) preset (#193) 2026-09-29 14:37:24 +08:00
yetone 4150dfc079 provider: Volcengine Ark (火山方舟) is a preset, its Coding Plan, Agent Plan and pay-as-you-go (tcdw: 提供一个选项能够取消自动给 LLM 提供商地址补全 /v1/ 的默认行为) — volcengine, a first-party vendor with a Plan picker: Coding Plan (default) at https://ark.cn-beijing.volces.com/api/coding/v3 for chat completions and Responses and /api/coding for messages (套餐概览 docs/82379/1925114, 快速开始 1928261; its Codex page 2556056 sets wire_api "responses"); Agent Plan, which tcdw is on, at /api/plan/v3 (chat completions and Responses, its Hermes Agent page 2373743) and /api/plan (其他工具 2373746, Claude Code 2373740), with a key of its own; pay-as-you-go at /api/v3 (兼容 OpenAI SDK 1330626, 创建 Response 1569618), which both plans' pages warn bills apart from the plan — no Anthropic endpoint is documented for it. Its models (ark-code-latest, whichever the console picked, then doubao-seed-evolving, doubao-seed-2.1-pro/-lite, doubao-seed-2.0-mini, minimax-m3, glm-5.3, glm-5.3-flash, deepseek-v4.1-flash, deepseek-v4-flash, deepseek-v4-pro, kimi-k2.7-code, kimi-k2.8-preview, kimi-k3, lowercase as the quick-start page gives them) are shown until a list is fetched; no models.dev catalog is tied to it, as its volcengine one has pay-as-you-go's dated ids and volcengine-coding-plan misses the Agent Plan's. Key from ark.volcengine.com/region:cn-beijing/apikey. Their versioned paths go out as written (previous commit). Icon is lobehub's volcengine-color, as the others are; the import page offers its three plans 2026-09-29 01:19:06 +08:00
yetone 841855ca5a provider: Huawei Cloud's MaaS Token Plan is a preset (Lemon: 能否支持华为 codearts 订阅啊) — huaweicloud, Huawei Cloud MaaS: the Token Plan (personal accounts, 西南-贵阳一 only) is spent only at its own endpoints, https://api.modelarts-maas.com/plan/v2 for chat completions (its OpenClaw, Cherry Studio and CodeArts pages, tokenplan-maas-0004/0009/0014) and /plan/anthropic for messages (its Claude Code page, tokenplan-maas-0003); pay-as-you-go MaaS is the other Plan, /openai/v1 (qs-maas-0001, OpenCode page maas-modelarts-0909) and /anthropic (model-call-060). No Responses on either. Its models (glm-5.3, glm-5.1, kimi-k2.6, deepseek-v4.1-flash, deepseek-v4-flash, lowercase as its OpenClaw page has them) are given until a list is fetched; the key is a MaaS API key from console.huaweicloud.com/modelarts/?#/model-studio/authmanage; no usage API is documented (quota is shown in the console's 订阅管理), so none is read. CodeArts (码道) itself isn't a provider: it has no official API, its private endpoints are reverse-engineered, and its own doc has it reach the Token Plan through these same endpoints. Icon is lobehub's huaweicloud-color, as the others are. 2026-09-29 00:22:50 +08:00
yetone 63f9fa7465 agent: Claude Desktop runs on magpie in its third-party mode (#156: Claude Desktop 不会解锁 Claude Code 面板 — CC Switch 可以,咱这个支持吗?) — signed in with a Free account, Desktop's Code tab is only "Get Claude Code"; in its 3p deployment mode it needs no Anthropic sign-in and Code and Cowork run on the gateway. A Claude Desktop agent whose provider field switches it: on writes deploymentMode "3p" into Claude/ and Claude-3p/claude_desktop_config.json, a magpie profile in Claude-3p/configLibrary/<id>.json (inferenceProvider gateway, inferenceGatewayBaseUrl the gateway's address, inferenceGatewayApiKey magpie-claude-desktop, inferenceGatewayAuthScheme bearer; disableDeploymentModeChooser and coworkEgressAllowedHosts ["*"] only where unset) and its entry in _meta.json, applied — the keys and files CC Switch writes (claude_desktop_config.rs). No inferenceModels: Desktop lists /v1/models, which the gateway already gives it by ids it keeps. Off puts each deploymentMode back as it was (gone where there was none), the profile applied before applied again, magpie's profile and entry removed, and files and folders magpie made gone if nothing else went in; other keys and profiles are never touched. Folders as CC Switch finds them: ~/Library/Application Support, %LOCALAPPDATA% (a Claude… folder there if not Claude itself), $XDG_CONFIG_HOME. Desktop reads them at start-up, which the picker and notice say. The Library keeps giving Desktop's MCP servers to %APPDATA%'s file on Windows, listed once. Tested in a sandbox home: fresh, with the user's settings and 3p profiles (off restores every byte), on twice, on → off → on, drift, odd files refused, each system's folders; and against Claude Desktop 2.9939.2 on Windows 11 (winget): on, it started in 3P mode (provider gateway, apiHost the gateway) with the Code tab and no sign-in, and asked the gateway's /v1/models; off, it went back to claude.ai 2026-09-28 22:37:26 +08:00
yetone 0de3094d67 agent: WorkBuddy takes magpie's models (能否作为Agent的支持WorkBuddy) — a WorkBuddy agent whose provider field puts every magpie model into WorkBuddy's picker: entries in ~/.workbuddy/models.json ($WORKBUDDY_CONFIG_DIR), the custom-model list WorkBuddy reads and watches (no restart), as OpenAI chat-completions models at the gateway's /v1/chat/completions under magpie-workbuddy, with each model's context, output, images and reasoning levels (reasoning.supportedEfforts, canDisableThinking where it can think off). WorkBuddy's own entries (its local models) and the CLI's {models, availableModels} form are kept; a magpie model turned off in WorkBuddy stays off; off takes only magpie's out. Its requests say CLI/<ver> WorkBuddy/<ver>, so they're known by the key. Tested with WorkBuddy 5.5.6's own CLI against a sandbox gateway: deepseek/deepseek-v4-pro answered, reasoning_effort sent, usage under WorkBuddy 2026-09-28 19:24:40 +08:00
yetone 3f3cdff119 stats: magpie counts its users — once a day a running app or magpie serve sends PostHog (the Magpie project) one "magpie active" event under a random id made up on this computer (~/.config/magpie/install-id), with the version, the system and the architecture and nothing else, no person profile kept; off in Settings → Privacy → Count me as a user, or with DO_NOT_TRACK=1 or MAGPIE_NO_STATS=1, and never from a build from source; the README says what goes 2026-09-28 17:23:41 +08:00
yetone 29c706d1e7 provider: Tencent Cloud Token Plan is a preset — TokenHub's coding subscription (its general and Hy plans on one sk-tp- key, from console.cloud.tencent.com/tokenhub/tokenplan), served at its own endpoints under api.lkeap.cloud.tencent.com/plan: v3 for chat completions and anthropic for messages, no Responses (its Codex page asks for wire_api chat); models.dev lists only its Hy models, so the preset gives the plan's (tc-code-latest, GLM 5–5.3, Kimi K3 and K2.7 Code, DeepSeek V4, MiniMax M3 and M2.7, hy4-preview, hy3) until the plan's own list is fetched, and an entry imported at those endpoints is the preset 2026-09-28 15:41:57 +08:00
yetone 31169a17f6 agent: fx (fx.sh, Vercel Labs' coding agent) — its model picked in magpie like any agent's; a model through magpie adds magpie to ~/.fx/settings.json as a keyless provider of fx's own (openai-chat-completions at the gateway, auth none) with the catalog as its model_metadata, the model in use first so fx's 256-model limit never cuts it, and sets provider magpie and models.magpie; its own models stay as they are, and default takes magpie's entry out. fx sends no effort to such a provider, so there is none to pick. Tested live from a sandbox: fx 0.0.11 listed magpie's models and read and wrote files through several tool rounds on DeepSeek 2026-09-28 11:17:20 +08:00
yetone 6c9a0aa4d7 agents: OpenHanako — magpie is a provider there, chat completions at the gateway's /v1 under the key magpie-hanako with every model it serves, their windows, output, images and reasoning said; picking one makes it the primary agent's chat model, and default puts back the one it had and removes the provider, marked deleted as OpenHanako marks one it removes. While OpenHanako runs it goes through its local API (server-info.json's port and token), which reloads at once; otherwise into provider-catalog.json and agents/<id>/config.yaml for its next start. Asked on Discord; checked with OpenHanako's own provider registry and model manager, which list magpie's models and resolve the chat model to the gateway 2026-09-28 01:12:46 +08:00
yetone d6c3baa0b8 cursor: served straight through Cursor's agent API, not cursor-agent over ACP — each request is a Run on AgentService, the stream cursor-agent itself opens, with its sign-in (the keychain's or auth.json's token, renewed by cursor-agent near its end): the conversation goes whole as AI SDK messages the server reads as blobs, the caller's tools as MCP tools the model calls through CallDynamicTool, and the reply streams back text, thinking and calls, the Run closed once the step's calls are all made. None of Cursor's own tools run. Tested live from a sandbox: Claude Code and OpenCode on Sonnet 5 and GPT-5.4 mini read and write files through several tool rounds; parallel calls, a continuation and an image answered 2026-09-27 20:41:29 +08:00
yetone 90fd780eef grok: served straight through the Responses API the grok CLI talks to, not the CLI run behind the gateway — each request goes to cli-chat-proxy.grok.com with the CLI's sign-in and version, as the CLI's own do: the conversation as it is, the caller's tools, images and all, relayed to a Responses client and translated for the rest. The models are the backend's own list, with each one's context window and efforts, and Grok searches the web by itself. A further account signs with its own home. Tested live from a sandbox: Claude Code and OpenCode on grok-4.7 list, read and write files through several tool rounds; an image and a web search answered 2026-09-27 19:41:12 +08:00
Arvak 84ef25805e web: MAGPIE_WEB_KEY keeps the page's key across runs — for magpie web kept running as a service, where a new key each start signs every browser out; with it the cookie outlives the browser session too (400 days, Chrome's most). Unset, each run makes its own key in a session cookie, as before. A key under 16 characters is refused. (#109) 2026-09-27 17:26:32 +08:00
yetone e173710982 backup: the library goes with a backup and a WebDAV sync — the instructions, MCP servers and skills with their files, a fifth part, ticked by default (magpie backup/restore --no-library). A backup without keys leaves the servers' secret env and headers empty and a restore fills them from the servers kept here; a skill's file over 2 MB, or past 32 MB in all, stays behind; a path that climbs out of its skill is refused. What it replaces is kept with the library's backups. Tested from a sandbox: exported without keys, changed and removed things, imported — the server back with the local key kept, the skill back as files, run.sh still executable 2026-09-27 17:24:53 +08:00
yetone 59e4f417b2 web: magpie web serves the app's window to a browser, with the gateway beside it — for a computer that can't show the app: no desktop, WSL, a server over SSH, or the CLI-only build, which now carries the page too (what of the app needs Wails is left out of nogui builds). Every request needs this run's key, which the printed link carries and trades for a cookie other sites' pages can't send with a POST; --lan serves it on the network too, with a warning, --addr elsewhere, --no-open leaves the browser shut. In the browser the page opens links and sign-ins itself, names a folder instead of opening it, and leaves out the tray, the Dock and open at login. Tested from a nogui build in a sandbox: no key, a wrong key and a cookieless POST are refused; with the link Agents, Providers and Settings load and a setting saves. Issue #102 2026-09-27 16:21:26 +08:00
yetone ce8d0e065c devin: served straight through Devin's API, not the CLI over ACP — each request goes to the GetChatMessage RPC the devin CLI itself uses, with its sign-in: the conversation as it is, the caller's tools, images and all, and the reply streamed back text, thinking and tool calls as they come. A family's name is sent as its variant at the effort asked for; Adaptive and Fusion, which the API doesn't serve, are no longer listed. The instructions go at the head of the first message, as Kiro's do, since Devin turns away Claude Code's own in its system field. Tested live from a sandbox: Claude Code and OpenCode on Opus 5.5, SWE-2 and GPT-6 Sol list, read and write files through several tool rounds; images and a tool round trip answered on Claude, SWE, GPT and Gemini models 2026-09-27 15:30:24 +08:00
smallnest 03a01a548d agent: MiMo Code — Xiaomi's fork of OpenCode, sharing its config shape (#98)
* agent: MiMo Code — a terminal coding assistant that is Xiaomi's fork of OpenCode, whose config keeps OpenCode's shape: its own mimocode.json(c) under ~/.config/mimocode, its auth beside it, and a magpie provider block with npm/@ai-sdk/openai-compatible, model and small_model fields. opencode() becomes openCodeLike() for the two to share it, and magpieProviderJSON splits shape from the catalog id (magpieProviderJSONFor), so a visibility narrowed on mimocode (magpie visible mimocode …) narrows its list and leaves OpenCode's alone. The library gives it the same instructions, MCP servers (its mcp key) and skills as OpenCode, except the shared skills go into its own skills/ folder: it reads Claude Code's skills only behind a flag, though it reads its CLAUDE.md like OpenCode, and the page says so. Its requests are told by the mimocode/ User-Agent. Checked against XiaomiMiMo/MiMo-Code

* icon: MiMo Code's own favicon, from packages/ui/src/assets/favicon — its frame with the bottom bar, in the monochrome the app masks to the text colour
2026-09-27 08:42:44 +08:00