64 Commits
Author SHA1 Message Date
yetone df2b44ef6c provider: a model's test can be asked as Codex or Claude Code asks, for a relay that serves only them (耍赖天都爱 on Discord)
A relay that serves only Codex or Claude Code tells them by their headers
("This account only allows Codex official clients"). The gateway already
lets those headers go on to a provider reached with a key, so the agents'
own requests were served, but the provider/model test is magpie's own
tiny request (Go's User-Agent), so it was turned away and said the
provider didn't work.

provider.TestAs(ctx, "codex"|"claude-code") asks Test and TestModels as
that agent asks. It goes to that agent's API (Codex: Responses, else Chat;
Claude Code: Anthropic Messages), streamed, store:false on Responses, and
with the headers the gateway last let through for it. The gateway notes
them with provider.SawClient beside the existing pass-through, without the
session, thread, turn, request id, subagent kind, anthropic-beta or
anything that signs. They are kept in client-shapes.json so the CLI has
them too.

Claude Code's headers are never made up. Until a Claude Code request has
come through magpie, the test says so. They are sent only to a relay,
never to anthropic.com, googleapis.com, Bedrock or Azure. A signed-in
account, a decision-only provider, and OpenCode/Cline/Kilo (which already
ask as their own client) are not offered it. Codex's fall back to the
headers a ChatGPT account's requests use (codexSign) until Codex has come
through.

Surfaces:
- CLI: magpie provider test <id> [--as codex|claude-code] [model…]
- GUI: a model chip's right-click has "Test as Codex" and "Test as Claude
  Code" where the provider offers them (providerJSON.testsAs). An item that
  can't run yet is off, with the reason shown. zh, zh-TW, ja, de strings.

Run:
- TestModelTestAsClient (internal/gateway). A fake relay 403s anything but
  codex_*/claude-cli/. The plain test gives 403. As Codex it is OK on
  /v1/responses with stream+store:false and codex_cli_rs headers. As
  Claude Code it is refused before Claude Code came, then OK after one
  Claude Code request through the gateway, with its UA/x-app/x-stainless
  but no session id or beta. Anthropic's API is refused. With the beta
  filter removed by hand the test goes red.
- model-test-as.test.cjs, Chromium and WebKit, en/zh/zh-TW/ja/de at
  420px: 10/10. On the old app.js it fails (no such items). Siblings
  model-test-one, model-copy-id, model-test-why, provider-typed: 28/28.
- Sandbox CLI (fake chat relay on :3571, sandbox HOME):
  - provider test relaya --as codex deepseek-chat sent
    codex_cli_rs/0.162.0 … with originator and version, stream:true.
  - --as claude-code said the relay has no Anthropic endpoint.
- go test -tags nogui ./internal/{provider,gateway,gui} and the root
  package under a temp HOME with pinned caches: ok. gofmt -l clean.
  go vet darwin/linux/windows on those packages: ok.
2026-10-10 15:08:14 +08:00
yetone e3bd766fae providers: a saved provider's editor keeps the add form's Fetch models button, "Fetch models again" once a list was fetched, to re-fetch after a key moves to another group (耍赖天都爱 on Discord)
耍赖天都爱 on Discord: 「provider中模型拉取一次就没有这个按钮了 能不能加上 有
的provider换一个分组要从新拉取模型」. The add form offers "Fetch models"
(获取模型); once saved, the editor's same action was a generic "Refresh"
(刷新) in the models footer, so after the first fetch the button looked
gone, while a relay key moved to another group serves another list.

The editor's button now says "Fetch models", and "Fetch models again"
(重新获取模型) when a vendor list was fetched before. Its title says it asks
with the key typed here and what happens to the picks. The fetch itself is
unchanged: it sends the typed form (asTyped), and Refetch replaces the list,
drops picks the new list no longer has and keeps ids added by hand. The
hints that named Refresh ("from models.dev · …", "magpie's list · …",
"Jev's names · …", Forget's title, the empty-list hint) name the button now,
in zh, zh-TW, ja and de. The TUI's "m fetch models" and `magpie provider
refresh` already re-fetch at any time; the CLI help names the app button.

Verified:
- new internal/gui/tests/provider-fetch-again.test.cjs (Chromium + WebKit;
  en and zh at 900 and 420px, zh-TW and ja at 900, de at 420): a never
  fetched relay shows Fetch models and its hint names it; a fetched one
  shows Fetch models again; with group B's key typed it asks with that key,
  shows group B's list, drops the pick B lacks, keeps an id added by hand,
  the button is still there and asks again; nothing scrolls. On the old
  app.js/i18n.js it fails: "waiting for ... getByRole('button', { name:
  'Fetch models', exact: true }) to be visible" (en), "again has its zh".
- provider-typed, provider-picks-stay (labels updated), gui-zh-tw, gui-ja,
  gui-de, click-scroll: 51 pass, 0 fail, both engines.
- go test -tags nogui . ./internal/tui ./internal/gui under a temp HOME: ok.
  go vet -tags nogui . ./internal/tui, GOOS=linux and GOOS=windows: ok.
2026-10-10 10:13:43 +08:00
yetone e611fc65d6 codex: magpie restarts the codex app-server a change left on the old model list when no codex session is on it, and the Agents page offers Restart when one is (luci on Discord)
luci (Codex CLI 0.162, magpie 0.1.1150): after Codex was wired back to
magpie, codex in a new terminal listed Codex's own models although
config.toml was magpie's.

Cause, reproduced with real codex 0.162.0 in a sandbox CODEX_HOME:
codex sessions attach to a background app-server (the managed daemon)
that is per CODEX_HOME, not per terminal (socket under
$CODEX_HOME/app-server-control). It is detached, has no idle exit and
builds its model list once, at start. A daemon started while Codex was
off magpie keeps the official list for every later session, in this
terminal or a new one, until it restarts. magpie writes only PATH to
shell rc files and doesn't unwire Codex on quit, so nothing
per-terminal is involved. luci's "a new terminal fixed it" isn't
explained by the daemon. Codex's hourly updater restarting it is a
guess, not proven.

What changed:
- provider.KeepCodexDaemonCurrent(ctx, home, changed) compares the
  daemon's start time with when magpie last changed what Codex reads at
  start. The start time is the mtime of a pid file in that home that
  names the running daemon's pid, so another home's daemon (ChatGPT.app,
  other sandboxes) is never acted on.
  - With no codex session attached, it runs
    `codex app-server daemon restart` with CODEX_HOME set to that home.
  - Otherwise it reports Behind and how many sessions are attached.
  - isCodexClient leaves out apps' and editors' own codex, exec-server,
    mcp-server and the daemon's own processes. It reads the whole ps line,
    since ps leaves a path's spaces unquoted ("Codex Computer
    Use.app/...").
- agent: Codex's Notice after a change runs this check first. It says
  either that magpie restarted the daemon, or that the daemon is behind
  and how to restart it (Agents page or the command).
- The GUI backend and `magpie serve` re-check every 15s. A session that
  was open at the change is usually closed later, and that is when the
  daemon can be restarted.
- Agents page: the stale "daemon" copy line has a Restart button that
  POSTs /api/codex/daemon/restart (timeout now 2 min, as a running turn
  is let finish). It reuses translated strings. On Windows, where
  processes aren't listed on each look, the row shows the last check.
- Restarts are serialized by one mutex.
- WSL's Codex is not handled automatically. Its row keeps the advice
  text, which names the restart command.

What was run:
- Real codex 0.162.0 in a sandbox CODEX_HOME, with a fake gateway, on
  luci's sequence: the daemon started on the official config, then
  magpie's config and catalog were written.
  - With a TUI open, /model listed GPT-6.x. The check said Behind with
    a session attached and did not restart.
  - With the TUI quit, the check restarted the daemon. A new codex in
    the same terminal listed Magpie Alpha and Beta.
- Earlier in the sandbox: a daemon restart under an idle attached 0.162
  TUI reconnects, and /model then shows magpie's models. A running turn
  is waited for up to 60s.
- New tests:
  - TestKeepCodexDaemonCurrent(Fails), TestIsCodexClient (with the
    space-in-path case from the real process list) and
    TestCodexNoticeKeepsTheDaemonCurrent. They don't build or fail on
    the old code. The space case failed before the ps-line fix.
  - The GUI test "the daemon's line restarts it" times out on the old
    app.js.
- Results:
  - agent-stale-copies.test.cjs: 60/60 in Chromium and WebKit, in en,
    zh, zh-TW, ja and de, at widths 980 and 440.
  - go test (temp HOME) for ./internal/agent, ./internal/provider,
    ./internal/gui and the root package: ok.
  - gofmt -l: clean.
  - GOOS=linux and GOOS=windows go vet -tags nogui: ok.
- The Windows path is checked only by vet and not tried on a Windows
  box.
2026-10-10 10:05:10 +08:00
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
a998a36367 provider, gateway: remote magpie serves decision and retrieval APIs (#1282)
* provider, gateway: remote magpie serves decision and retrieval APIs

Configure System One from the remote address and discover explicitly marked
decision models, including Clef and custom names. Keep exposed retrieval
models when importing the remote catalog, without treating decisions as
chat or image generators. Refresh removes stale decisions and the editor
receives an explicit empty decision list when none remain.

Forward caller/session attribution for decision, retrieval and media APIs.
Preserve TUI model-list errors for a remote that cannot be reached. Document
the HTTP API surface and old-peer compatibility.

Verified with the full nogui Go suite, focused race checks, go vet, native
macOS and Linux/Windows cross-builds, and the Remote magpie browser tests in
Chromium and WebKit in English and Chinese. The new System One integration
test fails on the old code with an empty model list, and the retrieval test
fails because the remote embedding model is filtered out. Integration tests
exercise two gateways and simulated upstreams; live vendor accounts were
not used.

* gateway, provider: remote forwards keep official client headers

Claude Code and Codex keep their User-Agent through both gateways, so a
relay behind the remote that admits only official clients accepts their
requests. Only decision, retrieval and media calls set magpie's User-Agent
at their own call sites. The two-hop test covers Messages, Responses, Chat
Completions and a Codex originator with another User-Agent.

Keep entryFor limited to remote decision providers. Test rejection of
unlisted remote decisions and remove the redundant resolveDecideModel
early return: the explicit model marker and list-only Jev already exclude
vendor aliases. Document retrieval entries remaining in the shared agent
catalog and TUI list errors being shown for other mixed providers too.

Verified with the Remote magpie provider/gateway tests. Before the fix,
TestRemoteMagpieClientHeaders fails all four cases with the relay receiving
magpie/dev and returning 403 official client required; all pass after it.
Follow-up to yetone's review of #1282.

* provider, gateway, gui: remote discovery reports only the decisions it lists (#1282)

Show CLI list failures and catalog counts for mixed providers. Derive the
remote decision endpoint from its one editable address, name custom decisions
by their ids, and reuse successful discovery in endpoint tests without an
empty decide OK row. Bare-provider decision selection is documented.

Simplify decision classification around the explicit remote list and reuse
model markers while building Available, Catalog and Deciders, avoiding a
whole-list copy and scan for every model. Other providers keep their rules.

The regression tests failed before the fixes on swallowed CLI errors, blank
labels, extra endpoint fields, empty decide results and repeated discovery.
Deliberately removing retrieval/video User-Agent or System One caller
attribution makes all three receiving-side tests fail with lost agent/via.

Verified with sandboxed Go tests, targeted race checks, Linux GUI/nogui and
Windows vet/build, Chromium/WebKit en/zh browser tests and zh-TW/ja/de checks.
The two-hop tests use real gateway handlers with simulated vendor replies;
real vendor accounts and native Windows/macOS runs were not used.

Co-authored-by: XYenon <register@xyenon.bid>
Amp-Thread-ID: https://ampcode.com/threads/T-01a11cd6-93aa-767c-9d6e-f0db2a9c5bab

* gui, gateway tests: remote decision refresh leaves only its current model choices (#1282)

The empty-decision browser fixture cleared deciders but kept decision
entries in models, so its screenshot still showed the same chips. Remove
those entries and assert the visible choices in both states. Check the
real providerInfo model list after refresh too, with Jev Router retained
as chat, and verify an unnamed remote decision reaches the local picker
with its id and provider label.

No production code changes. Verified all Remote magpie tests in root,
provider, gateway and gui under a sandbox HOME with -race; the new
assertions in gateway/gui pass. Chromium and WebKit pass all six browser
cases in English and Chinese; the corrected screenshot run passes 3/3.
Targeted Linux and Windows nogui vet and gofmt are clean.

Amp-Thread-ID: https://ampcode.com/threads/T-01a11cd6-93aa-767c-9d6e-f0db2a9c5bab
Co-authored-by: XYenon <register@xyenon.bid>

---------

Co-authored-by: Amp <amp@ampcode.com>
2026-10-10 03:55:45 +08:00
YAYOI27andClaude aa82288fb6 usage: Volcengine Ark's Coding Plan and Agent Plan windows, read with the account's access key (#1427, yayoinoyume)
Ark tells a Coding Plan's or Agent Plan's 5-hour, weekly and monthly
windows only to the account's control plane (GetCodingPlanUsage,
GetAFPUsage on open.volcengineapi.com), signed with the account's
AccessKey ID and Secret, never to the plan's inference key. An Ark
provider's editor now takes that access key, and the Usage page shows the
plan's windows as a card.

From yayoinoyume's feat/volcengine-usage, reviewed and reworked:
- Signing is volc-sdk-golang's (base/sign.go GetSignRequest, 06f9ef66):
  signed headers sorted (content-type;host;x-content-sha256;x-date), only
  Action and Version in the query. The branch signed them unsorted and put
  Region in the query. TestVolcSignMatchesSDK pins two signatures the SDK
  made.
- The card is in PlanQuotas beside the Step Plan's (its cache, last
  reading, refresh and `magpie quota wait`), not in the subscription
  cards. One per access key and plan.
- The plan is the endpoint's: /api/coding asks GetCodingPlanUsage,
  /api/plan GetAFPUsage, pay-as-you-go none, in the host's region. The
  branch asked both and showed whichever answered.
- Dropped guessed reply fields; the Coding Plan reply is the one
  pi-ark-usage, dsh-ark-quota and ArkCodingPlanUsage read. AFP buckets are
  read in the forms dsh-ark-quota and arkcli read (Used of Quota or Total,
  or Percent), plus Tier; none was seen from a real account.
- An Ark provider is known by its host, not the preset id, so a copy or a
  custom provider takes the key too.
- The Secret is never sent to the page (accessKey {id, secretSet}); a
  blank Secret keeps it, a save without the ID keeps both, Remove
  (clearAccessKey) drops both, an ID with no Secret is refused.
- The pair goes where keys go: AddCopy, Restore, Mirror, backups and
  davsync carry it only with keys (withoutKeys), and a keyless restore or
  sync keeps this machine's. CLI: access.key, access.secret.
- A changed access key drops the cached cards (ForgetPlanQuotas).
- UI strings in zh, zh-TW, ja and de.

Nothing was tried against the real Volcengine API: there was no access
key. A local fake checked the real request PlanQuotas makes against
volc-sdk-golang's own GetSignRequest (both actions, two regions); it
lives outside the repo, so go.mod is unchanged.

Run: gofmt -l; go vet (darwin), GOOS=linux and GOOS=windows go vet
-tags nogui on provider, gui, backup, davsync and the root; go test
-tags nogui ./internal/provider ./internal/gui ./internal/backup
./internal/davsync . under a temp HOME; node --test
volc-access-key, zhipu-team, gui-ja, gui-de, i18n-keys in Chromium and
WebKit. Fail without the change: TestVolcSignMatchesSDK (unsorted
headers), TestAddCopy, TestBackupBalanceToken, TestNoKeys,
TestTakeBalanceToken, TestSyncBalanceToken (pair not carried),
TestProviderSaveKeepsVolcengineSecret (blank Secret dropped),
volc-access-key (Remove not posted).

Co-authored-by: YAYOI27 <111715843+yayoinoyume@users.noreply.github.com>
Co-authored-by: Claude <noreply@anthropic.com>
2026-10-09 23:25:04 +08:00
yetone 34a598372c providers, gateway, gui, cli: each usage window of an account can have a cap of its own beside the account's (willz on Discord)
willz asked for a cap per window: a friend's account should stop at 50%
of its five hours, while its week has its own 40%. Until now an account
had one usage cap, the same share of every window.

What the user sees: the account's pill still sets the cap for all its
windows and stays the default. Each window's meter on the account's row
is now a button: a click opens the app's own menu headed "Cap for
<window>" with the account's cap, No cap on this window, 50–90%, and
Other… typed in place. A window with its own cap says it beside its
figure ("Cap 50%", or "No cap"), its mark sits there, and the account's
pill says "Caps by window" when only windows have caps. The account is
held while any window is past its own cap, and the row's note, the
gateway's 429 and the Routing trace name that window's cap. CLI:
magpie provider account-cap <id> <account> --window "5 hours" 50
(none: no cap on that window; default: back to the account's), and
`account-cap <id>` lists the overrides.

How: Provider.AccountWindowCaps (providers.json, by account and the
window's capId, its name in lower case; 1–99, or 100 for none) beside
AccountCaps. provider.WindowCaps gives a window its share (its own, else
the account's); Allowance.CapHeld and capReached judge each window by
its own share and report the fullest holding window's cap.
provider.HoldShare becomes HoldCaps, used by gateway capHeld (plan and
group filtering, cappedError, trace) and codex_backend's route-or-relay
choice; codex_switch's NextLogin moves the agent off at per-window caps.
QuotaWindow carries capId; Limit.name is exported as Limit.Name. A
config with no window caps reads and routes exactly as before
(TestSetWindowCap reads an old one). The meter is a span with
role=button, Enter/Space: a <button> as a flex item of the wrapping
meter line kept WebKit's row at its one-line height until the next
layout, so the row grew under the pointer at the first click.

Verified:
- go test -tags nogui ./... in a sandbox HOME: all ok. New
  TestWindowCapHeld and TestSetWindowCap (provider), TestWindowCapsHoldTheAccount
  through the real gateway (5h 60%/cap 50 goes to the spare; 20/30 under
  50/40 is used; 20/45 with the spare past its own cap is a 429 naming
  "past its 40% cap"), TestAccountCapCmd --window cases.
- Break check: with WindowCaps.Of ignoring the window's own share,
  TestWindowCapHeld fails ("5 hours at 60% of 50%: <nil>") and
  TestWindowCapsHoldTheAccount fails (the request is answered 200 by the
  capped account).
- internal/gui/tests/window-cap.test.cjs: Chromium and WebKit, 420 and
  1100 wide, en, zh, zh-TW, ja, de (60 tests): held note per window,
  marks, menu from the meter with no native select, keyboard, posts of
  50, typed 45, 100 and 0, no page scroll on a click, no sideways scroll,
  no left border stripes, every new string translated. With
  windowOwnCapOf returning 0 it fails.
- account-cap, gui-zh-tw, gui-ja, gui-de, i18n-keys pass; full GUI suite
  in both engines (483 files, concurrency 6): 4315 of 4323
  pass; the 8 that failed (library-servers-bulk, purpose-filter,
  routing-proxy, routing-zcode-blocked) touch no account row and all 32
  of their tests pass when run alone
- gofmt -l clean; go vet -tags nogui ./... on darwin, linux and windows,
  and windows without the tag for ., internal/gui, internal/provider,
  internal/gateway; go build ./...
- Not tried in the real window with a live gateway or real accounts.

Docs: gateway-routing.md (HoldCaps, window caps, tests),
providers-accounts.md (AccountWindowCaps, tests), reference.md.
2026-10-09 12:00:26 +08:00
yetone 5da6c24e80 agents, gui, cli: an agent can be shown only the models picked for it, so a model added later stays out of its list until ticked (#1337, nianlee-official)
nianlee-official asked for an agent whose model list grows only by hand:
today the Agents page's per-agent list hides models one at a time, and
every new model, of a new provider or an old one, shows up in the
agent's picker and config on its own ("New models are shown").

What changed:
- settings: PickedModels, per agent. An agent named in it is shown only
  the catalog entries listed for it (after Visible); HiddenModels is not
  read for it.
- provider: ModelOff(agent) is the one test CatalogFor, the Agents page
  count and Codex's own ChatGPT models (CodexNativeHidden) use, so the
  gateway's /v1/models for that agent's key, the lists magpie writes into
  its config and Codex's /model all narrow the same way hidden models do.
  SetOnlyPicked switches it on or off keeping what is shown as it is
  (on: what is shown becomes the picks; off: the listed models not
  picked become hidden). SetPickedModels keeps a pick whose provider is
  missing for now. renameModelPrefs moves picks with a provider rename.
  A model asked for by name still resolves against the full catalog.
- GUI: the list's foot "New models are shown" note is now a switch
  "Only models I pick", with a tooltip saying what each side does. On,
  a click sends what is ticked ({shown}), not what is hidden, so a model
  that arrived after the list was opened isn't picked by accident; the
  server still reads {hidden} from an older page. The model in use is
  always kept. Hide all / Show all's armed state takes the switch's room.
  zh, zh-TW, ja and de strings; the old note string is gone.
- CLI: magpie visible <agent> --only-picked | --show-new; magpie visible
  lists such an agent as "only models picked"; magpie models <agent>
  says how many are not picked, and what to do when none is.
- /api/settings keeps PickedModels like HiddenModels.
- docs/reference.md: the per-agent list bullet.
Not touched: #1302 (bulk agent visibility) and its files.

Run:
- go test -tags nogui ./... in a temp HOME: 44 packages ok.
- New: provider TestOnlyPickedModels, gui TestAgentModelsOnlyPicked,
  gateway TestOnlyPickedModelsList (opencode's key: a new provider's and
  an old provider's new model are left out of /v1/models, cursor's key
  has them, and a chat request naming fresh/x1 reaches the upstream) and
  TestCodexModelListOnlyPicked. With ModelOff made to ignore picks, all
  four fail.
- GUI: new agent-models-only-picked.test.cjs, Chromium and WebKit, en/zh/de,
  420 and 1100 wide (12/12). With save() sending {hidden} in pick mode it
  fails. agent-model*, agent-models*, codex-model-order, cursor-local-models,
  desktop-model-menu, click-scroll, gui-ja, gui-de, gui-zh-tw: 85/85.
- gofmt -l clean; go vet -tags nogui ./... on darwin, GOOS=linux and
  GOOS=windows; go build, and linux/windows builds.
- The built CLI in a sandbox HOME: visible codex --only-picked, a model
  added after it is left out of `magpie models codex` and kept in claude's;
  --show-new keeps it out and new ones show again.
Not tried: the real window against a live gateway, or a real agent
reading the narrowed config.
2026-10-09 08:50:14 +08:00
yetone 05b5965872 providers, gui: a partner listed since the add sheet last showed the partners puts a small dot on the "+ Add provider" button, its name in the title, until the sheet shows it (yetone: 有了新的合作伙伴的时候,这里能否有个小小的精致优雅的提醒吸引用户点击查看)
Ran: go test ./internal/provider ./internal/gui ./internal/tui .;
partners.test.cjs (chromium + webkit, en/zh at 900 and 440), i18n-keys,
gui-ja, gui-de; GOOS=linux -tags nogui and GOOS=windows builds.
2026-10-09 01:45:43 +08:00
yetone 7d663a2769 providers, stats: count how often each partner is shown, opened, has its key page opened and is added, per day and capped, and send the days that have ended with the daily stats as "magpie partner" events; off with the stats or with what magpie is used with (yetone: 能否统计每个合作伙伴的展示数和点击数)
Ran: go test ./internal/provider ./internal/stats ./internal/gui ./internal/tui .;
partners.test.cjs (chromium + webkit), privacy-usage-stats, i18n-keys,
gui-ja, gui-de; GOOS=linux -tags nogui and GOOS=windows builds.
2026-10-09 01:40:02 +08:00
yetone 844b9ca3ac provider, gateway, gui: a custom provider's Base URL can be a Gemini API (generateContent): …/v1beta, its models listed from it, the key sent in x-goog-api-key (#1346, NagaseMinato)
The custom provider editor's Base URL has a fifth API, "Gemini compatible"
(Gemini 兼容 / Gemini 互換 / Gemini-kompatibel), beside OpenAI, Anthropic and
Responses; More endpoints has a Gemini URL; the CLI takes gemini=. A
Gemini URL is kept as its base (…/v1beta, a relay's path prefix kept, a
pasted models/…:generateContent cut off). Every client's request, Chat,
Messages, Responses or Gemini, streamed or not, goes upstream to
models/{model}:streamGenerateContent?alt=sse under it, with no model in
the body and the key in x-goog-api-key; models are listed from its
/models, a page at a time, keeping those that serve generateContent.
Detect doesn't ask a Gemini API and is hidden while it is picked.
Factory's Gemini route and Bearer token are unchanged.

With five APIs the pick no longer fits a 900px editor in English and
German, so it wraps to a second row, and its thumb now follows the
picked option onto that row.

Verified against fake httptest upstreams only; no real Gemini key or
relay was tried:
- TestCustomGeminiUpstream (gateway): chat, chat stream, messages, a
  Gemini client's stream and non-stream, behind /proxy/v1beta. Fails with
  "chat: 404" without the gateway.go change.
- TestCustomGeminiProvider, TestGeminiBase, TestFactoryGeminiUnchanged
  (provider).
- go vet, HOME=$(mktemp -d) go test -tags nogui ./... (44 packages ok),
  GOOS=linux and GOOS=windows go vet on the touched packages.
- provider-gemini.test.cjs (new, en/zh/ja/de, Chromium + WebKit, 8 pass;
  fails with "900px: the thumb is 22px off" without the slide() fix),
  and in both engines: provider-base-api, provider-detect,
  provider-detect-models, provider-add-models, provider-output-picks,
  provider-search, provider-url-each-api, seg-contrast, settings-groups,
  menu-scroll, click-scroll, ledger-hscroll, strip-scrollbar,
  view-scrollbar, usage-ledger-chart, system-one, gui-ja, gui-de,
  gui-zh-tw: all pass.
2026-10-09 01:10:25 +08:00
yetone 10578ce683 providers: the add sheet lists Partners first, above Subscriptions, under a heading that says they sponsor magpie; who they are comes from usemagpie.ai/api/partners (site/partners.js), so a paid listing changes with a deploy, not a release (yetone)
The app fetches the list behind the call every 6 hours (10 minutes after a
failure), never waiting, and keeps it in the cache folder. Each entry is
checked: its own id, never a built-in preset's; a name and an endpoint;
https only; at most 12; no decision API, no-key or user endpoint. Its logo
goes through FetchIcon. A partner is a preset to the rest of magpie, and
one that has left still is to the providers added from it, unsponsored.
The window lists a partner in the languages it names only, with its tagline
in the page's language; `magpie presets` and the TUI list them first too.
MAGPIE_PARTNERS moves the list, off lists none.

Run: go test -tags nogui -race ./internal/provider ./internal/gui
./internal/tui . (ok); TestPartners* -race -count=3; node --test
internal/gui/tests/partners.test.cjs add-sheet gui-ja gui-de i18n-keys in
Chromium and WebKit (en/zh/ja/de, 900 and 440px); node --test
site/worker.test.mjs site/install.test.mjs; the built binary in a sandbox
HOME against a local feed: listed, an http entry left out, listed from the
cache with the site down, `provider add` from it.
2026-10-08 22:52:30 +08:00
yetone 0470411122 providers: a provider has a Requests per minute limit beside Concurrency: each key or account sends the vendor at most that many requests in any rolling minute, one more waits for room (coeo91 on Discord: OpenRouter's free models take 20 a minute, which Concurrency 1 can't keep to)
Set in the provider editor (under Concurrency; empty or 0 is no limit) or
with `magpie provider rpm <id> [n|off]`; kept per provider as MaxRPM, a
subscription's with its picks. Over the limit the gateway waits until the
oldest request of the minute is a minute old, in the order they came,
after the lane's slot and right before sending. A wait longer than 2
minutes (or the provider's Queue wait, when shorter) is turned away at once
with a 429 and a Retry-After of when the minute has room; nothing rests for
it. An agent gone while waiting gives its place back and is never sent.

Every request that reaches the vendor counts: each try of the forward
loop (retries, the next key, fallbacks) waits in serve, and requests a try
makes on the way plus the gateway's own (count_tokens, System One
decisions, embeddings, drawing/video, Kimi search) are counted by an
rpmTransport on s.client. count_tokens never waits: a key with no room is
skipped and the local estimate answers. A key or account with no room in
its minute gives way to a ready one of the provider or the group, as a
full lane does (laneMate). No wait happens under rpms.mu. Not counted: a
plugin account's own extra requests (its own transport; its tries are).

Verified (Go tests on Linux in Docker, golang:1.26.8, since new binaries
hang at launch on this Mac while syspolicyd is pegged):
- internal/gateway rpm_test.go through the real server stack
  (lanGuard(s.Handler())): limit 2, third request reaches the vendor a
  window after the first, all 200; past the bound a 429 with Retry-After
  and no rest; a cancelled waiter is never sent and gives its place back;
  a 503 retry and a count_tokens count, and a count with no room is
  estimated; a group member with no room gives way. With RPMLimit forced
  to 0, those five gateway tests fail. -race -count=5 passes.
- internal/provider TestSetRPM (clamp, subscription picks reach each
  account), TestRPMCmd, internal/gui TestRPMSave.
- go test -tags nogui ./... on Linux: all pass except internal/agent's
  TestAside* write-refusal tests, which fail only as root in Docker and
  pass as uid 1000.
- go vet -tags nogui for linux, windows and darwin; gofmt -l clean.
- GUI: new provider-rpm.test.cjs (en/zh/ja/de, 360 and 440px, Chromium
  and WebKit; 32 pass, and all 16 Chromium cases fail on the old app.js),
  plus gui-ja, gui-de, gui-zh-tw, i18n-keys, provider-concurrency,
  lane-limit, provider-price-rate and clinepass-pin in both engines.
Not tried against the real OpenRouter.
2026-10-08 20:13:26 +08:00
yetone ad4361fb93 cli: magpie models and magpie provider <id> name the models a provider's list has that it doesn't expose, and why (not picked, or only the first ones with none picked); a group's member that is one of them says "not exposed · magpie provider models <id> +<model>" rather than only "not served" (MOMO on Discord: 35 exposed of 37, opencode-go/deepseek-flash missing from magpie models)
Verified: TestModelsSayWhatIsNotExposed and TestGroupMemberNotExposedSaysHow
fail on the old code (magpie models and magpie provider og never name
deepseek-flash; magpie groups says only "(not served)") and pass with it.
2026-10-08 18:13:11 +08:00
yetone 3d5b212f6a cli: magpie provider models <id> +model adds a model to those exposed and -model takes one out, the rest kept; the whole-list form still replaces, and now says so with the count before and after and how to add one instead (MOMO on Discord: 35 exposed became 8 with nothing said)
provider set models= says the same. Taking out a model that isn't
exposed, mixing +/- with plain ids, or leaving none exposed (which would
mean the default list) is an error that saves nothing.

Verified: TestProviderModelsAddsRemovesAndSaysWhatItReplaced fails on
the old code ("+deepseek-flash left [+deepseek-flash]") and passes with it.
2026-10-08 18:13:11 +08:00
yetone 698da36f31 gateway, providers, gui: a model on this computer or the local network can be set to get requests unmasked by Settings' redaction (lc on Discord: 本地模型可以不脱敏)
The provider editor has a Redaction row, "Send requests unmasked", while
every address of the provider is on this computer or the local network
(localhost, *.local, a loopback or private IP): Ollama, LM Studio, a vLLM
box. Ticked (provider.Unredacted; CLI `magpie provider set <id>
unmasked=yes`), a request whose provider and fallbacks, or whose routing
group's models, all skip redaction (provider.SkipsRedaction) goes to them
as the agent wrote it; anything that may reach another provider stays
masked for all of them. An address edited to a vendor's masks it again; a
signed-in account, a plugin's provider and another magpie never skip. The
log and recent calls keep the masked body. count_tokens and embeddings
follow the same rule.

It is a per-provider choice, not automatic for local addresses, because
a local address isn't a local model: a relay run locally (new-api,
CLIProxyAPI) or Ollama's cloud models pass the request on to a vendor.

Verified: TestLocalUnredactedProviderGetsTheRequestWhole (set, unset,
fallback to a vendor, fallback to another local one, a group of it alone,
a group with a vendor; chat and count_tokens) and TestLocalUnredactedEmbeddings
fail without the gateway change ("sent whole false, want true: … my key is
{{API_KEY_…}}"; count_tokens alone: "counted whole false"); TestSkipsRedaction;
TestProviderSaveKeepsUnredacted fails without the preset save line.
provider-unmasked.test.cjs (Chromium+WebKit, en/zh, 900 and 440 wide) 32/32,
3 of 4 fail on the old app.js; gui-ja, gui-de, i18n-keys, provider-search
and the other provider editor suites pass. go test -race on gateway,
provider, redact, gui; -race -count=10 on the new tests; go vet,
GOOS=linux/windows builds. Real: a sandbox gateway in front of the local
Ollama (llama3 8B) through a logging proxy: unset, Ollama was sent
{{API_KEY_…}}; with unmasked=yes it was sent the key as written, and
answered both times.
2026-10-07 00:33:15 +08:00
yetone 39af4adda5 gateway, gui, cli: each account or key can have its own limit on requests at once, with a bounded queue that turns requests away with a 429 (#892, margbug01) — a provider's Concurrency applied to every key and account alike, the queue past it had no bound and no wait, and nothing showed what ran or waited. A key or account now may have its own limit over the provider's (Provider.AccountConcurrency, by lowercased account or KeyID; 0 none; unset takes the provider's, as before), counted in one lane across every model, routing group and agent. A provider's Queue size bounds how many wait for each of its lanes (one more is turned away at once) and Queue wait how many seconds one waits (then it leaves the queue); both 0 by default, as before. A request turned away goes on to the next key, account or group member without resting anything, else gets a 429 in its API's own shape (Anthropic rate_limit_error, OpenAI's, Gemini's) with Retry-After: 1, saying the limit and the queue. A slot is given back when the reply is read to the end, fails or the agent goes away; a waiter that goes away leaves the queue, and one granted as it gave up hands the slot on. In a routing group, or among a provider's keys and accounts, a full one gives way once to another with a free slot now, so a group spreads before anyone waits; a fallback provider is never asked for want of a slot, and with all full the request waits for the one routing chose. The Claude subscription path and plugin providers take their slot the same way, around the attempt; a parked Claude Code tool turn has already answered and holds none. GUI: each account and key row has a pill — "1/1 running · 2 queued" while requests are out (GET /api/lanes, read every two seconds and written into the pill in place), else its limit; none set shows on hover only — opening the app's menu (provider's, no limit, 1/2/3/5/10, Other… typed in place), posting provider/accountconcurrency; the editor has Queue size and Queue wait under Concurrency. CLI: magpie provider account-concurrency <id> [account|key [n|off|default]] and magpie provider queue <id> [size [seconds]]. en/zh/ja/de. TestAccountLimitSharedAcrossGroups, TestQueueFullAndWaitedOut, TestLaneReleasedOnFailure and TestFullMemberGivesWay (pass with -race, with TestMaxConcurrency and TestLanesLimitLifted), TestLaneLimitRoutes and TestAccountConcurrencyCmd are new and fail without the change; lane-limit.test.cjs (WebKit, en and zh: a key's busy/queued pill updated in place from /api/lanes, set from the menu, provider's posts null, Other… typed, an account's own limit, none on hover only, queue fields saved and refused out of range, labels on one line, every string in zh/ja/de, no select, no scroll, no left border) fails without the change. key-weight-fit, account-cap, provider-concurrency, click-scroll, account-models and api-key-usage pass in WebKit; the other WebKit failures fail the same on main. go vet, go build (darwin, GOOS=linux nogui, GOOS=windows) and go test -tags nogui ./... under a temp HOME pass. 2026-10-05 18:53:16 +08:00
yetone f3c05b5ed8 tui, cli: a plugin's account whose model list failed says why on the Providers page and in magpie providers (gnayiab on X) — v0.1.1001 had the app's editor say why Cursor's plugin account listed Auto alone, but gnayiab runs magpie's TUI in WSL, where the Providers page still showed "1 models" and nothing else (only m, asking again, said why). Each provider's row now ends in "couldn't list its models: <why>" (red, as other faults there) when provider.ListError has one, and magpie providers says the same in amber after the row. TestProvidersSayWhyAListFellBack fails without the change. go vet, go test -tags nogui ./internal/tui . under a temp HOME, and go build (GOOS=linux nogui, GOOS=windows) pass. 2026-10-05 18:44:12 +08:00
yetone ed79a72927 provider: a subscription account can be capped at a share of its usage windows, and routing treats it as used up there (Discord: hold a new Claude account at 70% of its five hours and its week to keep its ban risk down, and let magpie go on to the other accounts) — an account's cap (1–99, keyed by the provider id and the account in lower case, AccountCaps in providers.json beside AccountModels and AccountProxies) holds it once any window that applies to the model is at or past the cap and hasn't reset (Allowance.CapHeld; Aside windows and windows kept for another model are left out). One share for every window rather than one per window: the windows an account has differ by vendor (five hours and week for Claude and Codex, a month or a day for others, per-model weeks), the request was one figure for both, and a lower share for one window would be set by lowering the cap. The gateway's perKeyBarred moves a capped account to the candidates left out (barredOf marks it Capped with its use and when it is back, so the routing trace says "held at its 70% usage cap"), so groups, fallback, Ordered and Spread routing and the Codex/Claude Code switch list (NextLogin: a capped account is moved off, not returned to, and isn't a spare) all go on to the next account; a pinned capped account and a draw over only capped accounts answer 429. When every account that serves the model is capped, the client is turned away as when all are used up — 429 rate_limit_error with Retry-After at the soonest reset — and told the cap was reached, which accounts, at what use and cap, and how to lift it. A cap never spends a Codex reset: weekUsedUp, usedUp, BackAt and spendExpiringNow still go by ChatGPT's own 100%, so an account held at 80% with auto reset on is held and nothing is spent; a lone Codex account with a cap goes through serve() instead of being relayed as it came (codexAccounts), so the cap holds it. The Providers page shows a cap pill on each account's row (on hover until set) that opens the app's own menu — No cap, 50/60/70/80/90%, Other… (typed inline, focused without scrolling) — posting provider/accountcap; each window's bar gets a tick at the cap (from the right when the bar shows what is left), and an account held by its cap says so and until when. zh strings in i18n.js. CLI: magpie provider account-cap <id> [account [percent|off]] shows or sets caps. Tests: TestCapHeld, TestCapReached, TestParseCap; TestCapNeverSpendsACodexReset (80% windows, 70% cap: not used up, no reset due, routing held); TestCodexSwitchedAtCap (the active account at 75% with a 70% cap is switched off to the one at 20%, not back while capped, back once lifted; 100 and an unknown account refused); gateway TestAccountCapMovesOn (first account at 75% capped at 70%: the second is tried and the trace says 75/70; lifted: the first), TestAccountCapEveryAccount (429 rate_limit_error naming both caps, nothing sent, logged 429) and TestAccountCapSpendsNoCodexReset (one account at an 80% week, auto reset on: 429, nothing tried or spent; lifted: 200, nothing spent) — the three gateway tests fail with the cap check taken out; TestAccountCapCmd; account-cap.test.cjs (webkit, en and zh: pill, menu, Other…, No cap, cap ticks, held note, no <select>, no border stripes, no scroll on click). account-models, click-scroll, keep-login, codex-auto-reset, account-return, routing-blocked, routing-account-order, routing-quota-left, routing-auto-reset, routing-manual, quota-pools, quota-families and usage-accounts GUI tests pass in webkit. go vet, builds (darwin, GOOS=linux, GOOS=windows) and go test -tags nogui -count=1 ./... under a temp HOME pass. Not tried with real accounts; a cap acts on usage as last read, so a burst between reads can run a little past it. 2026-10-04 10:18:50 +08:00
yetone 2719dd6ac3 tui, cli: adding a provider says how many models its list had, or why it had none, and m (magpie provider refresh <id>) fetches the list again (akic404 on Discord: 还是tui cli问题,没有获取上游模型功能,新建之后模型数是0)
The TUI's add fetched the new provider's list and dropped any error, so a remote magpie that couldn't be listed (wrong address, the other magpie not shared, a gateway key it refuses) showed just "added Remote magpie" and 0 models, and the Providers page had no way to ask again: t tests the APIs, the models picker said "t tests it, which asks the vendor for them", which it doesn't. Now the add says "added X · N models from its list · M for agents (↵ picks)", or "added X · no model list · <the first URL's answer> · m asks again" in red; m on the Providers page is the app editor's Refresh (Refetch: picks the new list no longer has are dropped and named), with the same note; the footer lists it and the empty picker points at it. The CLI already said a failed list on add and fetched with provider models <id>; provider refresh <id> (and fetch) is that by its GUI name, and models <id> now goes through Refetch too, naming dropped picks. The ids were never names: a remote magpie's are <its id>/<the other magpie's provider id>/<model> (remote-magpie/custom/qwen3.6-max); the Chinese is the label, "qwen3.6-max · 中转站", the other magpie's provider name it sends as magpie_label, which magpie models and the TUI's picker show beside the id. README says so.

TestTUIFetchesAProvidersModels (add over a fake magpie: 2 models; its list grows, m: 3; it refuses, m: the error and the 3 kept) and TestProviderRefreshCmd fail on the old code; TestTUIAddsARemoteMagpieWithItsAddress now expects the error at a dead address. End to end with two sandbox magpies (temp HOMEs, 3461/3462, a fake upstream on 3463): A with "中转站" over the upstream (id custom), B's TUI in tmux adding A at 127.0.0.1:3461: "added Remote magpie · 2 models from its list · 2 for agents", row 2 models, m again the same; a second at 127.0.0.1:3464: "added Remote magpie 2 · no model list · … connection refused · m asks again", 0 models; provider refresh remote-magpie lists remote-magpie/custom/qwen3.6-max "qwen3.6-max · 中转站". go vet, builds (darwin, GOOS=linux, GOOS=windows) and go test -tags nogui -count=1 ./... under a temp HOME pass.
2026-10-04 01:24:47 +08:00
yetone 89d305229b providers: a removed plugin subscription is named as its row was, and can be signed out for good (#694, mintonight: 点了移除,国际版账号没有消失,放在 plugin 里无法彻底删除;再添加回来还是旧账号) — Add a provider's "Removed from magpie — still signed in" listed a removed plugin account by its agent, which for every plugin's sign-in is "plugin", with the generic logo; and its only way out was Add it back, which brought the old account back, since Remove only hides an account and nothing signed it out. Now a removed account is named and drawn as its row was (accountLabel, shared with the provider row: a plugin's by the provider it signs in to, a moved built-in's by the built-in's logo; Exclusion.Name carries it to the CLI's list too). Its row in the add sheet opens the app's menu (openProtoMenu, the page left where it was): Add it back as before, or Sign out…, which asks first and posts provider/forget; the Providers page's reminder line has Sign out beside Add it back and Don't remind me. provider.ForgetAccount signs a plugin's accounts out of plugin-auth.json (the host's signOut, else under plugin-auth.json.lock), takes them out of logins.json and, for a moved built-in, out of the move's backup so moving back doesn't restore them; an agent's own sign-in that magpie only reads is hidden, as an account's Remove hides it; a built-in's saved accounts go through ForgetLogin, the one in use last. Signing in again then starts afresh. zh strings added. TestForgetRemovedPluginAccount (bun fake plugin: removed is named FakeCo, forgotten leaves plugin-auth.json, logins.json and the removed list empty, Add back brings nothing) and removed-forget.test.cjs (webkit, en/zh: the row named Qoder, the menu with no request and no scroll, Sign out asks then posts forget with the provider id, Add it back still posts show, the reminder's Sign out) fail on the old code and pass; add-sheet, click-scroll, plugin-two-subs and saved-signedout-remove still pass. go vet, builds (darwin, GOOS=linux, GOOS=windows) and go test -tags nogui -count=1 ./... under a temp HOME pass. Not tried with a real Qoder account; the plugins repo needs no change (the Qoder plugin keeps its sign-ins only in plugin-auth.json). 2026-10-03 23:09:13 +08:00
yetone 919ceb3a68 provider: Bailian's decision model as a routing preset, and System One for custom providers (#647, Aimer779: Alibaba Cloud Bailian's decision-model-preview speaks TypeSafe's System One at POST <root>/systemone, and magpie only took it from TypeSafe's Jev) — a new preset, Bailian Decision Model (qwen icon, decide-only like TypeSafe Jev), asks decision-model-preview at its plan's root: Workspace · Beijing / Workspace · Singapore (https://{WorkspaceId}.<region>.maas.aliyuncs.com/compatible-mode/v1, the editor asks for the workspace ID and fills it in) or Token Plan · Beijing (token-plan.cn-beijing…, sk-sp- keys). Saving with the workspace still unfilled is refused, both in the editor and in provider.Add. A Bailian host has no /models for it, so its list is the one model, checked by a System One ask. A custom provider can now be a System One API: the CLI's decide=<root> (with models=… named anything, not only jev-*), workspace= and region=/plan= for a preset's plan; the editor's API seg has a fourth option, System One, whose Detect APIs posts a test question to <root>/systemone with the model typed, and a decide-only custom provider passes the "base URL is needed" check. A decide-only provider's first model is what it is asked as (Jev() no longer assumes jev-*), and its models stay routing groups' classifiers, never agents'. TestBailianDecisionPreset, TestCustomSystemOne, TestProviderSystemOne and TestProviderDecidePairs fail on the old code (undefined BailianDecision / DetectDecide, unknown decide=); internal/gui/tests/system-one.test.cjs (webkit, en/zh) covers the workspace, the Singapore and Token Plan plans, TypeSafe's unchanged editor and a custom System One detected and added. go vet, builds (darwin, GOOS=linux, GOOS=windows) and go test -tags nogui -count=1 ./... under a temp HOME pass but TestProvidersAnswerWhileListsComeIn, a timing test that passes alone. Not tried against a real Bailian key. 2026-10-03 15:21:15 +08:00
yetone 01d595817c providers: each account of a subscription, and each key, can be kept for some of the provider's models only (#474: 增加账号级别的模型选项, a low-quota but verified account kept for the cheap models) — every account or key a provider had served every model it did, so one couldn't be spared for some. An account's or key's row in its provider's editor now has a models badge: a click opens the provider's models as chips, Save keeps the account for those picked, All models gives it every one again; one kept for some says how many, always shown. magpie provider account-models <id> [account|key [models…|all]] does the same and lists them. It is kept in providers.json as accountModels, an account by its name in lower case, a key by its id; one not in it serves every model, as before. The gateway never sends a model to an account or key whose list leaves it out, in every routing mode, in drag order, in a group and as a fallback, and never falls back to it all the same; the trace says it was left out as set to serve other models. When every account or key there is leaves the model out, the request is refused with a 403 saying so and how to change it, not "none of X's models is ready". TestKeyModelsNarrowCandidates, TestAccountModelsNeverChosen, TestAccountModelsCmd and account-models.test.cjs fail without the change. go vet, go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui ./... pass; account-models, click-scroll, old-webkit, account-arrange, account-proxy, account-return, routing-account-order, panel-routing, unlisted-models pass. 2026-10-02 05:50:23 +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
yetone 9c54a560cc provider: a providers.json that can't be read is said where the providers are listed, never backed up or synced as none, and is replaced through a rename (PR #415's review) — #415 made edits refuse a file they couldn't read or decode, but every read for the user still took it as empty: magpie providers said "no providers yet", magpie models "no models yet", the Providers page opened the Add sheet as for a first use, and backup.Collect, so an export and a WebDAV/S3 sync, carried it as no providers, which another computer's Mirror would then take as every provider removed. magpie providers and magpie models now list what is left (the signed-in accounts) and end in the error, the TUI says it over its list, and the Providers page shows a "Your providers file can't be read" box (无法读取供应商配置文件) over the list with magpie's reason and no Add sheet; start-up, the gateway and the catalog still go on with it as empty. Stored and StoredGroups return the read error, so a backup and a sync stop on it with nothing pushed. The error, for reads and refused writes alike, names the file, says magpie left it unchanged and to fix it or move it aside (errors.Is ErrUnreadable). store writes through writePrivate, a 0600 temp file renamed over it (steady.Rename waits out a Windows reader holding it, a symlink keeps its target), instead of os.WriteFile's truncate-then-write, and the read goes through steady.ReadFile. TestFileErrorSaysUnreadable, TestWriteErrorsSayLeftUnchanged, TestStoredRefusesUnreadable, TestStoreReplacesAtomically, TestSyncStopsOnUnreadableProviders, TestProvidersCmdSaysUnreadable and provider-file-error.test.cjs fail without the change. go vet, go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui ./... pass; provider-file-error, click-scroll, old-webkit, providers-off-fold, add-sheet pass. 2026-10-01 19:45:24 +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 f5a2b5d406 gui: a moved provider's editor reads as the built-in's — its model list dated, its agent's own sign-in said, its move a button (要保证插件订阅 provider 跟内置的订阅 provider所有逻辑都要一模一样) — after a move the list said "magpie's list · Refresh asks the vendor" where the built-in said "vendor list · 9m ago"; it is now dated by when the plugin last listed it, in the editor and in magpie providers. "Command Code itself stays signed in as it is." is kept for Devin, ZCode, WorkBuddy, Kiro and Command Code, whose agents keep their own sign-in through the move; and Move to the plugin / Use the built-in again is drawn as a button, not a line of text. plugin-move.test.cjs fails without the kept line. go vet, go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui ./... pass; plugin-move, click-scroll, old-webkit, plugin-market, plugin-signin, nokey-editor pass. 2026-10-01 11:15:27 +08:00
RandyandClaude e8315b18ed gateway: a relay that searches the web by itself is sent Claude Code's WebSearch as it was sent (#359) (#365)
magpie tells the APIs that search by themselves by their host (api.anthropic.com), so a relay in front of Anthropic's API was given magpie's search instead: the request was parsed and built again, and with no searcher set up in magpie the WebSearch tool was left out altogether. A provider now has Searches (magpie provider set <id> search=yes, or the editor's Web search row for a custom provider): a web search offered on its Anthropic or Responses API goes to it as sent. It counts only on an API the provider has an address for, so a Chat-only one isn't taken as searching, and the editor shows the row only while an Anthropic or Responses URL is filled in. A provider said to search isn't one magpie searches with for other models: that would spend the relay's quota, and a relay serving only Claude Code refuses magpie's own request, which has no metadata.user_id.

TestWebSearchOfARelayThatSearches fails without the change (the request built again, magpie searching with the relay, a Chat-only relay taken as searching); provider-search.test.cjs added (Chromium, WebKit, en/zh), its Chat-only case failing without the row's gate. Checked against real Claude Code 2.1.285 through a sub2api relay in claude_code_only mode: WebSearch returns results with search=yes. go vet, go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui ./... pass but for TestPluginProxy, which fails on main (813fdcb) as well here.

Co-authored-by: Claude <noreply@anthropic.com>
2026-10-01 11:10:41 +08:00
yetone 140b6161bc cli: magpie provider shows a moved built-in as the built-in read (插件版…保持一模一样) — Zed moved onto its plugin said its account came "from plugin's own sign-in" and listed plugin://zed endpoints no one can reach; it now says zed's own sign-in (another plugin's provider its name's sign-in) and leaves the plugin:// URLs out. TestShowMovedProvider fails without the change. 2026-10-01 04:04:22 +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 52c8e838d2 providers, sign-in, tray, plugins: four of StringKe's reports on Discord — (1) Removing a provider left its editor open and listed, and a second Remove said there was no such provider: the removal had been made, but moving the agents off its models failed on one agent whose config couldn't be written, and Reseat returned that as the change's error; now such an agent is a move with its error ("still on X, not moved: …", a warning in the panel, a line in the CLI and TUI), the others are moved, and the panel, told a delete failed, re-reads the list and closes the editor when the provider is gone. (2) The sign-in waiting sheet shows the sign-in link itself, selected whole on a click, with a copy button, for a browser other than the one that opens. (3) A subscription with several accounts offers "Account in use" among the menu bar's usages, following the account the gateway goes to first. (4) The Antigravity plugin's sign-in never started: magpie called every plugin's authorize with an inputs object, the way OpenCode's CLI does, and opencode-antigravity-auth then asked its Project ID on the terminal — magpie's stdin — until the minute ran out; inputs now go only to a method with prompts, as OpenCode's TUI passes them (checked against the real package under Bun in a sandbox: the Google sign-in URL comes back at once). TestReseatUnmovable, TestTrayInUse, TestInUseOf, TestAuthorizeNoPrompts, provider-remove.test.cjs, signin-link.test.cjs and tray-inuse.test.cjs 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, tray-usages, plugin-signin, signin-callback, factory-sub pass. 2026-09-30 20:45:23 +08:00
yetone 4f01baa65f catalog: fetched again each day magpie stays open, and sooner when a call's model has no price, so a model models.dev lists after magpie started is priced (#224 Koohoko: gpt-6.1-sol shows no cost) — the models.dev catalog, where list prices come from, was fetched only at the app's start (when older than a week), so a magpie left open, or magpie serve, never learned a new model's price. KeepFresh (internal/catalog/fresh.go), run by the app's backend and by magpie serve, looks every hour and fetches when the cache is older than a day; a call whose model has no price asks for a fetch (catalog.Missing, from usage's pricer) when the cache is older than six hours and none was tried in that time, only in a process keeping it fresh, so CLI commands and tests never fetch on their own. Loading and Reset now share a mutex instead of a sync.Once reassigned under a running Do, which a background Sync could hit while a call was being priced. Tested with fresh_test.go against a fake models.dev in a temp XDG_CACHE_HOME: stale after 25h and not before, Missing fetching nothing when not keeping, fetching once when keeping and gpt-6.1-sol then priced, and not again; passes under -race (failed under -race with the Once). go vet, go build -tags nogui (and GOOS=linux, GOOS=windows) and go test -tags nogui ./... pass. 2026-09-30 04:28:01 +08:00
yetone d173bb0f79 serve: the banner no longer says the gateway only listens on localhost when MAGPIE_ADDR puts it on the network (the Docker image of #245 runs it on 0.0.0.0) — there it takes any key from anyone who reaches it until it is shared from Settings, and it now says so; once shared, it says other machines need the key under Settings → Share on local network. Tested with TestKeyNote (loopback, localhost, [::1], 0.0.0.0, :port, a LAN address, and shared). 2026-09-30 03:22:36 +08:00
yetone 037189ebdb agents: a provider switched off or removed, or its last account signed out, moves the agents on its models to others (#200: 希望关闭某个llm provider的时候,更改agent选择的model到可用的model … 关闭这个provider之后,第一次使用会报错,因为copilot/ 开头的模型不存在) — switching Copilot off took copilot/… out of the catalog and the agents' model lists, but an agent picked on one kept it in its config and its next request was refused as switched off. Now every field on one of magpie's models (the model, Claude Code's opus/sonnet/haiku/fable tiers, any other) whose model the change stopped serving is moved: to the same model from a provider still on, the same id first, else one spelt another way (claude-sonnet-4.5 and claude-sonnet-4-5-20250929), else back to the agent's own default (a Claude Code tier: following the main model). It is done around switching off, removing, a key removed or off, and a sign-in signed out or off, in the window, the tray panel, the TUI and the CLI; the page's status line, the TUI and the CLI say which agent went to what. A session started before the switch still asks for the old model: the gateway now takes a switched-off provider's model as unserved, so Claude Code's stand-in (its model as set now) answers it rather than a refusal. Tested in a sandbox HOME: Claude Code on the main model and three tiers plus Hermes, switched off and removed (same id, other spelling, default, a tier following), switched on again moving nothing; /api/provider/off returns the moves; the gateway serves a switched-off model through the stand-in (fails without the change) and still refuses it to an agent left on it 2026-09-29 17:56:43 +08:00
yetone efed707066 providers: a provider can be switched off, kept with its keys but out of every agent's list (希望服务商可以有一个开关按钮,关闭后不再写入模型) — a provider whose quota ran out could only be removed, losing its key and settings, or left in, its models still written into the agents and requests still sent to it. Now each provider (a signed-in account too) has a switch on its row in Providers and in its editor's header; off, the row is dimmed and says Switched off (已关闭), its models leave the catalog, so the model lists magpie writes into agents' files are rewritten without them as after any provider edit, and it is left out of routing groups, fallbacks, the Automatic image, vision and search picks, the routing pools and balance and usage polling. A request naming one of its models is answered 404 saying the provider is switched off in Magpie, not that the model is unknown. Switched on, all of it comes back. Saving the provider from its editor keeps it off; magpie provider off|on <id> and the TUI's o do the same. Tested: an off provider's models are not in the catalog or served, a group with it routes to the member left, a fallback to it is not tried, a request for it is refused as switched off, and turned on it is listed and answers again; an account's off is kept. Checked the Providers page in a sandbox HOME in light and dark: switching a row off and on, the editor's switch and its note 2026-09-29 02:49:36 +08:00
yetone 111f5ba8ef provider: a base URL with a version in its path is used as written, never given a /v1 (tcdw: 提供一个选项能够取消自动给 LLM 提供商地址补全 /v1/ 的默认行为) — a custom provider at Volcengine Ark's Agent Plan, https://ark.cn-beijing.volces.com/api/plan/v3 (its 其他工具 page, docs/82379/2373746), was asked for its list at …/v3/models and then …/v3/v1/models, and only the last failure was shown, so the error named a URL the user never gave. Ark answers both 401 without a valid key (checked unauthenticated: auth comes before routing), so which path it serves can't be told from outside; and had …/v3/v1/models answered, fixV1 would have rewritten the chat base to …/v3/v1 for good. Now an OpenAI-style base with a version segment anywhere in its path (v1, v3, v4, v1beta: Ark's /api/plan/v3 and /api/coding/v3, Zhipu's /api/paas/v4, Gemini's /v1beta/openai) is asked only at base/models and fixV1 leaves it alone; a bare host or an unversioned path (a relay's …/api) is still looked around for the /v1 it may lack, and an Anthropic base still has its /v1/models. A failed list names every URL asked with its status and the vendor's own message ("401 Unauthorized (The API key doesn't exist.)") and says the model ids can be typed in by hand or a models URL given — both already work, the base stays as typed; magpie provider add now prints it instead of saying nothing. No "don't touch my URL" toggle: a versioned URL is never touched, and an unversioned one is only fixed when its /v1 alone answered, when requests at the base as written would miss too. Tested against an httptest server shaped like Ark (/api/plan/v3/chat/completions served, /models 404, a lenient /v3/v1/models) in a sandbox HOME: before, …/v3/v1/models: 404 and, lenient, chat moved to …/v3/v1; after, only …/api/plan/v3/models is asked and the base is kept 2026-09-29 01:19:06 +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 08309e8c04 providers: Test models sends each model agents see a tiny request of its own, on the endpoint it's served on and with a key that sees it, and marks its chip with a dot — green with how long it took, red with what the vendor said — so a relay whose other models answer can't hide one that doesn't (magpie provider test <id> <model>… does it in the terminal). Usage has Hide emails too, one setting with Routing's, to screenshot the subscription cards 2026-09-27 12:26:00 +08:00
yetone d7999aefc7 providers: a balance endpoint named on a provider can be asked with an access token of its own (balance.token), with the provider's headers — a new-api relay's /api/user/self tells the whole account's quota to its access token and New-Api-User, not to a key: magpie provider set my-relay balance=…/api/user/self balance.path='$data.quota / 500000' balance.token=… header.New-Api-User=… 2026-09-27 02:30:55 +08:00
yetone 6fd85894ea routing: Jev (TypeSafe's System One) as a group's classifier — asked once as a turn begins which of the rules' intents the message is, with how sure it is, and, for a group with effort auto, how hard the turn is to think about; the turn's requests then ask their model for that reasoning, where the agent asked for any. TypeSafe is a decision provider: it serves no conversation and is no group's member 2026-09-27 01:53:04 +08:00
yetone f12a75899d providers: a subscription whose models come from its own CLI (Kiro, Devin, Cursor …) has no host, so magpie provider models said its models were fetched "from" nothing; it names the provider. Kiro signed in with Google or GitHub says so, not SocialGoogle. Checked against a real Kiro account: plain and streamed text, a tool call and its result, OpenAI chat with another model, and Claude Code reading a file through Kiro 2026-09-27 00:51:17 +08:00
yetone 47e12df418 models: a provider's model can go by a name of the user's, and offer only some of its reasoning levels — Claude Opus 5.5 · Claude Code can be Opus 5.5, and a model with six levels can offer just low, medium and high. Both are kept in settings.json by provider/model (modelNames, modelEfforts), apart from the vendor's list, so a Refresh, a restart or an upgrade leaves them, and the same model from another provider keeps its own; a provider renamed takes them along. They are applied where the catalog is made, so the gateway's /v1/models, the lists magpie writes into the agents (Codex's model catalog, ZCode, Pi, OpenCode, OMP …) and magpie's own pages all show them, and a change rewrites the agents' lists as any other change of the catalog does. Only what is shown changes: the id, where requests go, and the levels the vendor is asked at stay as they were; a found routing group keeps the model's own name. The provider's editor has Names & levels under its models — a name typed in place, a tick per level, Restore default — and the terminal has magpie model name <provider/model> <name>|--reset and magpie model efforts <provider/model> low,medium,high|--reset. Issue #92 2026-09-26 23:23:45 +08:00
yetone c4268e524f providers: a provider's id can change — its editor has an ID field (magpie provider set <id> id=<new> in the CLI), and renaming it moves what named it by the old one: the agents on its models (each spelled its own way, recorded as magpie's), the routing groups' members, rules and classifier, other providers' fallbacks, which agents are shown it, and the vendor's fetched list. The old id stays with it, so an agent still running on the old config reaches it, a profile saved before applies to the new id, and its usage so far is counted under the new one. Asked on Discord 2026-09-26 20:22:30 +08:00
yetone 9b06c30790 balance: a custom provider's balance field can name several amounts, each with a label, apart by ";" — Command Code's credits reply is 5h: windowLimits.fiveHour.used / windowLimits.fiveHour.cap %; week: windowLimits.weekly.used / windowLimits.weekly.cap %; $credits.monthlyCredits, shown as "5h 0% · week 3.4% · $70.00" wherever the balance is (Usage, magpie provider, magpie quota). Asked on Discord 2026-09-26 18:41:01 +08:00
yetone 73be42eca8 models: each agent can be shown only some of them — a provider or group gets a family (magpie provider set relay-x family=relay), and magpie visible zcode relay shows zcode only that family's models (or named providers and groups): in the files magpie writes for it and in what /v1/models answers it, told apart by its User-Agent; magpie models zcode lists what it is shown and what is kept from it, and magpie visible zcode all shows it everything again. A model kept from an agent is still served when asked for by name. Asked for on Discord 2026-09-26 16:02:29 +08:00
yetone c83e03d5c7 provider: how long a request a model takes can be set — magpie provider set <id> context=272k (all its models) or context.<model>=1m, and magpie group set <id> context=… for a group, which otherwise takes its shortest model's; agents are told it (Pi's contextWindow, ZCode, Codex's catalog, Grok, omp, dsh) over what the vendor or models.dev says, and the Providers and Routing views keep it when saved. An empty value clears it 2026-09-26 14:39:55 +08:00
yetone 5f839eae99 provider: saved Claude or Codex accounts that are not offered say why — under a HOME where the agent is not signed in (a magpie serve with its own HOME, no ~/.codex/auth.json or ~/.claude/.credentials.json there) they went missing without a word; magpie serve, magpie providers and the Providers view now tell 2026-09-26 14:32:48 +08:00
yetone 6b39a69bf4 provider: a balance field can be a sum — paths into the reply and numbers with + - * / and brackets, "(1 - credits.monthlyCredits / 70) %" for what is left of a plan, a "%" after it showing a percent of 1; "data.total / 500000" and "$data.x" read as before 2026-09-26 14:26:28 +08:00
yetone c330e20231 provider: an account's "only through routing groups" holds, and one removed stays removed — All() merged a signed-in account's picks without Unlisted, so provider listed claude no saved but claude/* stayed in /v1/models; and any save of an account rebuilt its entry without Hidden, so a removed one came back; a save now keeps it removed, and Add it back (and magpie provider add <account>, which made claude-2) goes through ShowAccount, keeping its picks; and magpie provider set <id> k=v… changes a provider with add's pairs (the v0.1.77 notes named it, it didn't exist) 2026-09-26 01:38:37 +08:00