main
80
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
ddd4f43144 |
provider: a routing group can be duplicated, from its menu on the Routing page or with magpie group copy (lc on Discord)
provider.CopyGroup(id, name) saves a copy of the group as it is now: its
models and patterns, routing, stays, rules, classifier, effort, context,
levels, and fast and off members. The copy is deep, so its rules' time
windows aren't shared. Its name is the one given, or "<name> copy", with a
number added past a name in use. Its id comes from the name, numbered past
any id in use, removed ones included. It is listed right after the group
it copies. The copy is a group of the user's and switched on, even when
it copies a found group or a switched-off one. The original is unchanged.
Surfaces:
- GUI: a group row's menu (right-click, or a click on its logos) has
Duplicate under Move up and Move down. It posts /api/groups/copy with
the id and t("{name} copy"), so the name is in the reader's language.
It asks before discarding an editor with unsaved changes. The status
then says "Duplicated {name}". No click scrolls the page.
- CLI: magpie group copy <id> [name]; duplicate, dup and clone work too.
- zh, zh-TW, ja and de strings. docs/reference.md and the site's start
page (en, zh, ja) list the command.
Run:
- TestGroupCopy (internal/gui): the copy has the models, pattern,
routing, rule and off member, is on, and is listed after the original.
The original stays off and unchanged. A second copy is "Fast copy 2" /
fast-copy-2. An unknown id is refused.
- TestGroupCopyCLI (root): the default name, a given name, a found
group's copy as the user's, and the errors.
- group-duplicate.test.cjs: Chromium and WebKit, en/zh/zh-TW/ja/de at
420px, 10/10. It fails 10/10 on the old routing.js. Siblings
group-arrange, removed-groups-menu-scroll, group-bulk-remove and
click-scroll pass 36/36.
- Sandbox CLI (sandbox HOME): group copy and duplicate made "DupTest copy"
and "DupTest copy 2", listed after DupTest.
- gofmt -l clean. go vet darwin/linux/windows on the root, provider and
gui packages: ok.
|
||
|
|
a154197bbb |
accounts: ChatGPT accounts come in from codexbar's config.json (碳碳双键)
碳碳双键 on Discord (#general): several Codex accounts switched with
codexbar on the old computer; after a magpie backup and restore the new
one had only the account Codex itself was signed in to. A backup carries
no sign-in by design (a refresh token rotates, so two computers can't
share one); importing is how accounts move. But codexbar's own store,
~/.codexbar/config.json, wasn't a shape the importer knew: its accounts
sit under providers[].accounts, so the whole file was read as one entry
with no token ("#1: failed").
codexbarEntries reads it as codexbar (lizhelang/codexbar 9f2e0f53)
writes it (CodexBarConfig.swift, CodexBarConfigStore): the accounts of
the provider of kind openai_oauth and openAI.remoteConnectionAccounts,
camelCase tokens, the workspace from openAIAccountId (its id is
codexbar's own). api_key accounts are said to be keys; other providers
(API endpoints of other vendors) are left alone; a sign-in in both places
is read once. codexbar signs in with Codex's own OAuth client, so the
refresh works as for Codex's auth.json. Its own export (Sub2API's shape)
was already read.
The import window's texts (en, zh, zh-TW, ja, de), the CLI's warning and
the docs name codexbar; reference.md says a backup carries no sign-in and
that importing the old computer's file is how accounts move.
Run:
- TestImportCodexbarConfig, fixture in codexbar's encoder output (pretty,
sorted keys, ISO dates; two Team seats, a duplicate remote account, an
access-only account, a DeepSeek api_key provider): fails on the old code
("got #1:failed"), passes on the new; seats kept apart by
openAIAccountId with an id token that doesn't name the workspace.
- Built nogui binary, sandbox HOME: `magpie accounts import codex
config.json --yes` lists the three accounts, ChatGPT refusing the fake
tokens; no real codexbar install here to try with real accounts.
- login-import.test.cjs (chromium, webkit; en, zh) and i18n-keys.test.cjs
pass.
- go test -tags nogui ./... (temp HOME, pinned caches): all ok.
- go vet (darwin, GOOS=linux, GOOS=windows) on . and internal/provider;
gofmt -l clean.
|
||
|
|
494dedcf8f |
desktopdir: Claude Desktop installed from the Microsoft Store (package AnthropicPBC.Claude_*) has its folders found, as the setup.exe's Claude_* package's are
Desktop 2.31226.1's own code knows two MSIX package families, Claude_pzs8sxrjxfjjc and AnthropicPBC.Claude_fnn82j28hfe8t (hze/_ze in index.chunk-Cyt2WTko.js). packageDir only globbed Packages\Claude_*, so a Store install's LocalCache was never found: magpie wrote Claude Desktop's gateway, MCP servers and read its sessions and skills in the unpackaged folders instead. Checked while following up v0.1.1156: the same bundle has a %LOCALAPPDATA%\Claude-Data folder for 1p userData, but in 2.31226.1 it is dormant. globalThis.userDataMoved is only read, never set, and the Claude-Data dir is chosen only when %APPDATA% is a network share, while relocateUncUserData does nothing in index.pre.js. So 1p userData is still %APPDATA%\Claude (or the package's copy), and desktopdir is left as it is for that. Run: go test ./internal/desktopdir ./internal/library ./internal/agent (macOS); TestFindMSIX fails on the old packageDir for the Store family; desktopdir's Windows test binary passes on the Windows box; go vet for linux, windows and darwin. |
||
|
|
e04824714d |
library: a skill's templates and other big files go with a backup and a sync (Crispin)
Crispin on Discord (#help): a skill's template files, a PPT among them,
didn't reach the other computer. Collect read each skill file with a 2 MB
cap and left everything bigger out, so a .pptx template with its pictures
never travelled. Links, copies and WSL copies on one computer already carry
every file (copyDir and hashFiles have no filter); only the backup/sync
carry dropped them.
- A file goes up to 16 MiB, all skills together up to 32 MiB. A sealed
backup is JSON with the bytes base64'd twice (~x16/9), and a sync reads
at most 64 MiB of one, so 32 MiB raw is what still fits.
- readSkills fills that budget smallest file first across every skill, so
one skill's big file never keeps another skill's SKILL.md from coming.
- What is left out (Left) and the computer being put to already has is
kept (keepLeft): a Put no longer sets the folder aside without it. A Left
name is checked like a carried one.
Run:
- New tests fail on the old code: TestCarryTemplates (template left),
TestCarrySmallFirst (zzz's notes.md left), TestCarryKeepsWhatWasLeft
(the local file went), TestSkillTemplatesTravel (0 bytes of 6291456).
Pass on the new. TestSkillTemplateReachesEveryAgent covers link, copy
and WSL copies of a pptx.
- Break check: maxCarriedAll = 48 MiB makes TestSkillTemplatesTravel fail
("the backup is 64 MiB, more than a sync reads").
- Real use: built nogui binary, sandbox HOMEs, a real 2.8 MB and a real
14 MB .pptx in a library skill, `magpie backup` then `restore
--no-agents` on another HOME: sha256 identical both times.
- go test -tags nogui ./... (temp HOME, pinned caches): all ok.
- go vet (darwin, GOOS=linux, GOOS=windows) on internal/library and
internal/backup; gofmt -l clean.
|
||
|
|
282c9feb23 |
agent: a packaged Claude Desktop gets magpie's gateway in the real %LOCALAPPDATA%\Claude-3p too, which 2.31226 reads, not only the package's (bg5eau on X)
bg5eau on X: Claude Desktop 2.31226.1, installed as an MSIX package on
Windows, never reached magpie. magpie wrote deploymentMode and its
configLibrary profile only into the package's
LocalCache\Local\Claude-3p, and that build reads the real
%LOCALAPPDATA%\Claude-3p. Copying configLibrary there and setting
deploymentMode 3p fixed it for them.
What the build does: 2.31226.1's asar (index.chunk-Cyt2WTko.js) resolves
the 3p userData with Ou() = path.join(LOCALAPPDATA, "Claude-3p"). 2.7032.0
has the same code (GY()). It reads deploymentMode from
Ou()/claude_desktop_config.json and the gateway from Ou()/configLibrary.
The path didn't change; the package stopped redirecting it into
LocalCache. An install's folders don't say which build it is, so:
- desktopdir.Sets returns every set of folders: Find's choice first, then
the other one for an MSIX install. Everywhere lists every set's folders.
- Claude Desktop's switch writes and removes deploymentMode, the profile
and _meta.json in each set. The package's set keeps its own stash keys
(".package"). A stash that an older magpie wrote for the package's
folders moves onto those keys, so turning it off still restores them.
- The check runs on each set, names the folder when there are two, and
checks Claude-3p's deploymentMode as well.
- The siblings: the library's MCP servers go into every Claude-3p file
that exists, and Cowork skills and Desktop sessions are read from every
set.
Run:
- go test ./internal/{desktopdir,agent,library,sessions,usage}: green.
- The new tests (TestClaudeDesktopMSIX, TestClaudeDesktopMSIXStashBefore,
TestClaudeDesktopMSIXBoth3p) and TestCallsCoworkMSIX are red against
the old code. StashBefore also goes red with desktopAdoptStash disabled.
- gofmt; go vet for darwin, linux and windows.
- On the Windows box, Windows test binaries for the four packages:
- desktopdir and library pass.
- agent and sessions fail exactly the set that main's binaries fail
there (#1388, and newly filed #1468).
- Every Desktop test passes.
|
||
|
|
34f2078db8 |
site: a partner's website and key links go through usemagpie.ai/go, which counts the click; fetches of the partner list are counted too
The app's own partner counts only come from users who leave the stats on, so a sponsor's reach and clicks are now also counted where the server sees them. /api/partners hands out each partner's website and keysUrl (and a region's) as usemagpie.ai/go/<id>/site|keys[/<region>]; /go sends a "magpie partner go" event to PostHog (partner, link, region, country) and 302s to partners.js's own URL. One in ten list fetches sends "magpie partners fetch" with weight 10 while someone is listed. Events carry a random id each, no install id or address. Ended entries leave the list but /go still resolves them, since added providers keep the links. Privacy text updated in README (en, zh), docs/reference.md and the site docs (en, zh, ja). Run: node --test site/*.test.mjs (27 pass); a scratch copy with a fake partner served rewritten links, 302'd /go to the real URLs and posted both events; PostHog's /i/v0/e/ answered 200 to the payload shape. |
||
|
|
9bb2a467e4 |
agent: Snow CLI is an agent magpie sets up: a magpie profile in ~/.snow, made the active one, that Snow App takes in with "Sync Snow CLI API config" (#1457)
Snow CLI (npm snow-ai, MIT, MayDay-wpf/snow-cli) keeps one profile per file under ~/.snow/profiles (or $SNOW_CONFIG_DIR), names the active one in active-profile.json, and copies it over config.json at every start, headless `snow --ask` included. Each profile is the whole snowcfg: base URL, key, request method and models. Snow App (MayDay-wpf/snow-app) reads ~/.snow/profiles/*.json when "Sync Snow CLI API config" is pressed. magpie writes profiles/magpie.json from the profile the user was on (their settings kept, Snow's OAuth sign-in left out): baseUrlMode base at the gateway's /v1, requestMethod chat, the agent's key, the model as advancedModel and basicModel, and the catalog's window and output (200000 and 64000 when not known, the output cut to the window). It makes that profile active and copies it to config.json. The profile the user was on, whether there was an active-profile.json at all, and a magpie.json of their own are stashed; Disconnect, `magpie snow default`, or picking a model of one of the user's profiles puts those back. A Snow from before profiles (config.json alone) gets the default.json Snow itself would make, so its next start doesn't take magpie's config for the user's. Check reports a base URL, key or request method moved off magpie's; Sync rewrites the profile on a port move. A WSL distro's ~/.snow is found and written the same way. Registered: agents.go, the WSL kinds, SNOW_CONFIG_DIR in agentenv, the npm update spec (snow-ai), the icon (Snow App's own, trimmed to 96px), README, docs/reference.md, docs/subsystems/agent-wiring.md, the site's agent list and start.html (en, zh, ja). Run: - go test -tags nogui ./... in a scratch HOME: 44 packages ok. New: TestSnow, TestSnowOwnProfile, TestSnowNoProfiles, TestSnowUsersMagpieProfile, TestSnowCheckSync, TestAgentSnowDefault; snow added to TestOwnProviderNamedAsMagpies, the WSL more-agents tests and TestMaxTokensWithinContextWindow. All fail before this commit (no agent "snow"). - go vet -tags nogui for darwin, linux and windows; gofmt -l clean. - Real Snow CLI 0.9.2 in a sandbox HOME against a sandbox magpie (`magpie serve` on 127.0.0.1:3571, a fake OpenAI upstream on 3561): `magpie snow fake/pro` made the magpie profile active, `snow --ask` answered through the gateway (POST /v1/chat/completions, model pro, logged as agent snow), and `magpie snow default` left profiles, active-profile.json and config.json byte-identical to before. - GUI: agent-layout, agent-connect, agent-models-picker, agent-field-names and models-add-and-icons in Chromium and WebKit; a Snow CLI row checked at 520-960px in en, zh, zh-TW, ja and de in both engines. No new GUI strings. |
||
|
|
f2611b7154 |
accounts: import ChatGPT accounts from Cockpit Tools' and Sub2API's exports, several files at once, from the window or magpie accounts import; the box says the source tool is signed out (#1453)
wlj521 asked for cockpit-tools' import: several auth.json files at once.
magpie's window already imported Codex's auth.json and CLIProxyAPI's
files; this brings it the rest of what Cockpit Tools exports and a command
for it.
What changed:
- login_import.go reads Sub2API's export ({accounts: [{platform, type,
credentials}]}), so every Cockpit Tools export format comes in (its own,
auth.json, CPA, Sub2API). An API key, another vendor's account and an
entry with only an access token (a web session: it can't be renewed and
would stop within days) are refused with the reason.
- ImportLogins and ImportGoogleAccounts no longer skip one file of several
that holds no account: it is a failed row with its place
(ImportedAccount.File), which the window and the command name by file.
- `magpie accounts import <codex|claude|antigravity|factory> <file>...
[--yes]`: says first what importing does to the tool the file came
from, imports nothing without a yes, only reads the files, and lists
each account's outcome.
- The window's import box names Cockpit Tools and Sub2API and says
plainly that the source tool is signed out of the account and the file
is only read. Claude's box no longer claims a refresh it doesn't do.
zh, zh-TW, ja and de for every new string; the retired strings removed.
- docs: reference.md and providers-accounts.md describe the import.
Decision: keep the refresh on import. It checks the file and makes magpie
the token's one holder at a moment the user chose. Storing the token
as-is, as Cockpit Tools does, only delays the rotation conflict and makes
which side gets signed out a race.
Run:
- go test -tags nogui ./... under a sandbox HOME (pinned caches)
- TestImportCockpitExports red on the old login_import.go (Sub2API rows
unread, the garbage file passed over); TestAccountsImport red with the
confirmation removed by hand; login-import.test.cjs red on the old app.js
- node --test login-import, factory-key, i18n-keys, gui-ja, gui-de,
gui-zh-tw (Chromium and WebKit; en and zh, results at 380px)
- gofmt -l; go vet, GOOS=linux and GOOS=windows go vet -tags nogui on .
and ./internal/provider; darwin, linux and windows builds
- no live ~/.codex: fixtures and a fake token endpoint only
|
||
|
|
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. |
||
|
|
c1c887798b |
provider, docs: a bare model id several providers serve goes by the Providers order, said and tested (MOMO on Discord)
MOMO's {"model":"glm-5.3"} was answered by opencode-go though a6api
serves glm-5.3 too, and they asked for a 409 or for the response to say
which provider was used. The pick was already deterministic, but
Resolve's comment said "exactly one provider" and no doc said what
happens.
Decision: keep serving bare ids, no 409. magpie writes only
provider/model ids into agents' configs (agent/route.go magpieModels),
so a bare id comes from a user's own config, and a 409 would break every
such client the day a second provider adds the model. The rule is now
written down and pinned:
- found groups on (the default): GroupFor takes the bare id to its found
group auto-<slug>, members in the Providers order, smart routing and
fail-over to the next provider; a conversation stays with the member
that answered it while its cache is worth it (affinity).
- found groups off: Resolve takes the first provider in the Providers
order that exposes it, alone.
- every reply already carries X-Magpie-Provider / X-Magpie-Model
(member.go), and the usage log has the provider.
What changed: Resolve's comment; gateway-routing.md model resolution
row; reference.md, the ways a model is named and the headers;
bare_slug_test.go: two providers serving glm-5.3, both modes: the first
in the Providers order answers every new conversation, reordering moves
new ones, an answered one stays in the found group, and a 503 from the
first falls over only in the found group. It passes on the old code
too: it pins behaviour that was already there, now documented.
Run: gofmt -l; go vet -tags nogui on darwin, linux, windows for provider
and gateway; go test -tags nogui ./internal/gateway ./internal/provider
in a sandbox HOME.
|
||
|
|
0311c720c1 |
gateway, provider: a model its vendor answers is deprecated leaves that provider's lists until it is answered again, and the next member is asked (MOMO on Discord)
MOMO's /v1/models listed opencode-zen-free/exo-free, and a request for it got 410 "Model exo-free has been deprecated.". OpenCode Zen's own /zen/v1/models still lists exo-free (checked 2026-10-10), and models.dev gives it no deprecated status, so the zen-free plugin's discovery keeps it and a refresh of the list would bring it straight back. No preset hardcodes it. What changed: - fallback.go: retiredWords / modelRetired: a 400, 404, 410 or 422 saying the model is deprecated/retired (Zen's "ModelDeprecated", OpenAI's "The model ... has been deprecated"), without quota words. It is retryable (410 wasn't), and modelRefused, so only that model rests. - gateway.go: such an answer calls provider.Retire(provider, model); routing.go: servedCandidate calls Unretire. - provider/retired.go: retired.json beside providers.json, re-read by mtime; Retire/Unretire touch the catalog so agents' lists are written again. buildEntries leaves a retired model out, for that provider only, for at most a week. provider/model requests are still sent. - docs: gateway-routing.md failure handling, reference.md. Tests: retired_test.go with Zen's literal 410 body: the model leaves /v1/models while the provider's other model stays; a group falls over to the next member and only the 410's provider is marked; modelRetired cases. All three fail with the hooks reverted. Run: gofmt -l; go vet -tags nogui on darwin, linux, windows for provider and gateway; go test -tags nogui ./internal/provider ./internal/gateway ./internal/agent ./internal/catalog in a sandbox HOME. |
||
|
|
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> |
||
|
|
7d0e415e62 |
plugins: two plugins that sign in to one provider are both listed, each row says which serves it, and Use for picks the other (neiko on Discord)
neiko on Discord installed a third-party plugin beside a plugin of their
own, added by its folder. Only the third-party one could be seen. Both
plugins' auth hooks name the same provider id. The host's auths() keeps
the last plugin in plugins.json for an id, as OpenCode does, so the
user's own plugin silently lost it:
- its row read "Signs in to nothing magpie can use";
- its provider was gone from the Add sheet;
- keepUnloaded could list the id a second time, from the
plugin-providers.json kept before.
A package-name clash and a display-name clash were tried as well. Both
plugins stay visible in those cases, because a folder plugin's name is
its path.
What changed:
- host.js:
- init gives each loaded plugin the ids it signs in to (provides) and,
for each id another plugin serves, that plugin's spec (servedBy).
- auths() honours plugins.json's new prefer map (id -> spec).
- Only the serving plugin's provider.models hook runs for the id.
- plugin:
- Clashes() turns provides and servedBy into {id, by, with} per spec.
- Prefer() records a pick and restarts the host.
- Remove drops the removed plugin's picks.
- Re-adding a plugin under a new spec carries its picks over.
- remove, off, options and prefer find a plugin by its exact spec
before its package name (indexOf), so acting on one plugin never
reaches another of the same package.
- keepUnloaded no longer re-adds a provider from before when another
plugin serves its id now.
- GUI:
- /api/plugins rows carry clashes, with the provider's listed name.
- POST /api/plugins/prefer {spec, provider}.
- The serving row says "{other} signs in to {name} too; this one
serves it".
- The other row says, in amber, "{name} is served by {other}, which
signs in to it too", with a "Use for {name}" button.
- New strings in zh, zh-TW, ja and de.
- CLI: `magpie plugin` prints the same lines, with the command that
fixes it, and `magpie plugin use <name> <provider>` makes the pick.
- Docs: docs/subsystems/provider-plugins.md, docs/reference.md and
site/public/docs/{,zh/,ja/}plugins.html.
What was run:
- Reproduced under a sandbox HOME with magpie web on 3562: a fake npm
plugin and a folder plugin, both on id "acme".
- Old binary: the local row had providers [] and /api/providers
listed acme twice.
- New binary: both rows carry the clash; prefer moves acme to the
folder plugin; remove clears the pick and leaves the other plugin
serving acme.
- New Go tests:
- TestTwoPluginsOneProvider, a real Bun host: Prefer, restart,
off/on and Remove.
- TestClashes, TestPluginNamedExactlyFirst and
TestKeepUnloadedLeavesAServedID.
- TestPluginsClashRows (gui) and TestPluginListSaysClash (CLI).
- Each fails when its piece is broken by hand: prefer ignored in
host.js, the keepUnloaded id check removed, name before exact spec,
Remove keeping picks, no provides, no clashes in the rows, no
prefer route.
- plugin-clash.test.cjs, in Chromium and WebKit, in en, zh, zh-TW, ja
and de, at 1100 and 560px: 22/22 pass. It times out on the old
plugins.js.
- gui-de, gui-ja, gui-zh-tw, i18n-keys and every plugin-*.test.cjs:
248 tests.
- 245 passed in the 4-wide run.
- plugin-moved-signin (webkit) failed once in that run, then passed
3/3 alone and 26/26 in a smaller 4-wide rerun.
- gofmt -l; go vet ./...; GOOS=linux and GOOS=windows go vet -tags
nogui ./...; go build ./...
- The full go test -tags nogui ./... under a temp HOME with the caches
pinned: 44 packages ok, none failed.
- After rebasing onto
|
||
|
|
a3a8e25792 |
plugins: a repository tagged magpie-plugin shows the icon its package.json gives (yetone: 非官方的插件能否支持 icon)
magpie.icon, an https URL or a data:image URI, is read with the rest of package.json and kept through pluginOwnIcon as an installed plugin's is; the card shows it, else the puzzle piece. Ran: go test ./internal/plugin ./internal/gui ./internal/provider; the plugin GUI tests in Chromium and WebKit (en/zh/ja/de, 980 and 440). |
||
|
|
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.
|
||
|
|
ee657c0969 |
usage: a Kimi Code membership's k3 is counted at Kimi's API price for kimi-k3, not ≈¥0.000, and kimi-for-coding is unpriced rather than free (#1370, maicent: Kimi Code 的模型计费都是 0)
Why it was 0: magpie already counts a subscription's calls at the API list
price (a Claude or Codex account at its maker's,
|
||
|
|
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.
|
||
|
|
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. |
||
|
|
ed31293c0e |
usage, gui: a reply its vendor let go in a burst over a couple of hundred milliseconds tells no speed, not 2,237 tok/s (John on Discord)
John's Kimi Code rows after v0.1.1119 (OpenCode, group/k3-glm, api.kimi.ai,
Chat, k3-256k): 550 tokens, 22 of them reasoning, in 16 s with the first
content at 5.2 s read 2,237 tok/s, and 395 tokens in 19 s 2,012. 528 answer
tokens at 2,237 tok/s is 236 ms: the burst the vendor let go at the end.
|
||
|
|
ae39bc8ec8 |
agents: Qwen Code (qwen) as a managed agent (#1311)
* agents: Qwen Code (qwen) as a managed agent (#1279) Qwen Code (QwenLM/qwen-code, a Gemini CLI fork) keeps custom model providers in ~/.qwen/settings.json modelProviders.<protocol> entries ({baseUrl, envKey, id, name}, envKey naming a variable in the same file's env object) and its session model in settings.model {name, baseUrl}. magpie writes one modelProviders.openai entry per catalog model — wire id the gateway takes, name magpie/<provider>/<model>, every one on env.MAGPIE_API_KEY holding the gateway's key, which is how an entry of magpie's is told from the user's own — and points settings.model at a pick. Stash, Check/drift, Sync, port-move Rewire and disconnect work as for the other JSON-config agents; on disconnect the user's entries, model and key are restored, the file byte-identical to before. Verified: go build, GOOS=linux nogui + GOOS=windows builds, go vet ./...; go test -tags nogui ./internal/agent (TestQwen* pass; TestDriftUnwiredEveryAgent/qwen and TestPortMovesEveryAgent pass after binding drift identity to the entry, not the address), go test -race -count=3 -run TestQwen, go test . (TestAgentQwenDefault: set then 'default' restores the settings byte-for-byte). TestQwen fails with 'unknown agent' when the registration is reverted. Real use: a copy of a live 341-line settings.json in a sandbox HOME — wire, then 'qwen default' left it byte-identical. Not run: GUI suite (no gui/assets change); TestReasonixCLI* and TestPluginHTTP2TakesTheProxy fail on main too (fixture/network), unchanged by this. * agents: qwen honors QWEN_HOME, sets auth type, owns a qwen-specific key name, carries context windows (#1279 review) - $QWEN_HOME stands for ~/.qwen, resolved as the other agents' (via place.getenv, appdir.Getenv; a distro's isn't read), and is in agentenv.Vars so the sandbox clears it - wiring sets security.auth.selectedType openai so qwen skips the /auth prompt; the user's own (including a pre-existing "openai", told from magpie's by a stash marker) is restored on disengage - envKey is MAGPIE_QWEN_API_KEY: a shell-exported or ~/.qwen/.env value wins over the settings' env, so the magpie-generic name is not used - entries carry generationConfig.contextWindowSize from the catalog - a settings.json without modelProviders reads as no models of anyone, instead of gjson's 'cannot use null as iterable' Verified: TestQwen* incl. TestQwenAuthOpenAIOfTheUsersOwn and TestQwenHomeOverride, TestAgentQwenDefault (CLI), -race -count=3; unwired-drift and port-move generic tests pass. Real end-to-end: dev gateway on 127.0.0.1:3999 in a sandbox HOME, real qwen binary in a sandbox QWEN_HOME ran a prompt through the gateway (PONG), and wire → default left the settings byte-identical. Reasonix fixture tests fail on main too (environment). * agents: qwen's requests are attributed by their QwenCode/<version> User-Agent |
||
|
|
2cdae80a10 |
stats: the day's event no longer names an omp profile or a model the user named; an omp profile goes as omp, and on a key's provider a model id models.dev doesn't list (an Ollama model, a fine-tune with the user's organisation in it) goes as <provider>/other
|
||
|
|
de5455b8fe |
gateway, usage, gui: a reply whose vendor sent its first words and held the rest to send in one burst tells no speed, not 1,367 tok/s (John on Discord)
John's request (OpenCode, a group served by Kimi Code at api.kimi.ai, Chat) showed 1,367 token/s: its tokens counted over the time from its first token to its end, though nothing came in between and the rest came at once. The 100 ms / 10,000 tok/s bounds catch a burst only when the first content comes with it. firstToken now reads on past the first text and marks when each content event's bytes came (one mark a millisecond, up to 65,536 marks and the 8 MB it reads), and flowMs is how long the content took to come from where the speed is counted: the span from a tenth of its bytes to nine tenths, scaled to the whole. A steady stream gets about its whole window; one let go in a burst about 1 ms. It is kept as Call/Try/Route .flow and usage.Record.flow_ms (and the Requests page's packed rows), and usage.DecodeOf, routing.js's decodeOf and app.js's ledDecode count no longer a window than it. A record without it reads as before. Verified: TestBurstTellsNoSpeed, a fake Chat vendor that sends a word, waits 600 ms, then 199 words and the end in one write (1,000 tokens), for a Chat and a Responses agent: no speed; before, 1636 and 1655 tok/s. Its steady case (a word every 60 ms) keeps 200 tokens over most of its window. TestCodexBackendBurstTellsNoSpeed is the same through Codex's own ChatGPT backend (before: 1,000 tokens in 601 ms). TestFirstTokenKinds now expects the stream read past its first text. Go tests ran on Linux (golang:1.26 in Docker; this Mac's syspolicyd held new test binaries at launch). GUI: routing-burst-speed (a route with John's 5.1 s first token, 6.7 s and a 2 ms flow: was "1367 tok/s", now none) and usage-speed (a row with flow_ms 2 shows "—") fail without the JS change and pass with it, with reasoning-speed, routing-request-metrics and usage-ledger-detail, in Chromium and WebKit (en/zh, ja/de where the tests have them); no strings changed. Not reproduced with Kimi Code itself: whether it holds the stream so is inferred from the numbers. |
||
|
|
7611c36e6f |
agent: wiring Alma also makes the model magpie draws with Alma's image generation model, when Alma has it on Auto (Sorghum on Discord)
Alma has one: Settings › Image Generation, settings.imageGen.model,
"<providerId>:<model>" like chat.defaultModel, "" for Auto (Alma's
src/services/settingsApiService.ts defaults). Its `alma image gen` and
in-chat image requests send an openai-type provider's model to
{baseURL}/images/generations, which the gateway draws with.
Choosing one of magpie's models for Alma now also sets imageGen.model to
magpie's provider and the model magpie draws with (gateway.Drawer: the
Settings' Image generation model, else AutoDrawer's), only when it is
Auto: a model the user picked, theirs or magpie's, is left. Sync leaves
an Auto alone (the user may have gone back to it) and only replaces one of
magpie's that magpie no longer offers. That model is listed on magpie's
provider with capabilityOverrides.imageOutput, as Alma's picker lists only
a provider's models that make images and shows another as "unavailable".
Going back to Alma's own model puts an image model of magpie's back to
Auto with the provider. Nothing changes when magpie draws with nothing.
Verified: TestAlmaImageGen against the fake Alma (settings kept whole,
aspectRatio untouched): Auto filled with p1:img/gpt-image-1 and the model
listed with imageOutput; the user's own pick and a magpie chat model kept
by Apply and Sync; Auto kept by Sync; a stale magpie drawer replaced; Auto
again on going back. It fails without almaSetImage ("image generation:
\"\", want \"p1:img/gpt-image-1\""). TestAlma checks nothing is written
when magpie draws with nothing. Go tests ran on Linux (golang:1.26 in
Docker). Not tried with the real Alma: Alma's settings page reading the
override as an image model, and an image drawn through it, are unverified.
|
||
|
|
cf10394527 |
agent: alma-server on Linux is found and wired as Alma, so its requests show as Alma, not "ai-sdk" (Lutra.x on Discord)
alma-server (Alma without a desktop, Linux x64/arm64) keeps its data in
$ALMA_DATA_DIR, else $XDG_DATA_HOME/alma, else ~/.local/share/alma
(Alma's electron/headless/electron-shim.ts getDefaultUserDataPath), and
answers on the same localhost:23001. magpie looked only in the desktop's
folder (os.UserConfigDir()/alma), so on such a machine Alma's Sync never
ran, its magpie provider kept the old shared key, and the gateway read its
requests by their User-Agent alone, the AI SDK's
("ai-sdk/openai/4.0.15 ai-sdk/provider-utils/5.0.10 runtime/node.js/22",
@ai-sdk/openai 4.0.15 and provider-utils 5.0.10 as alma-server bundles
them; Alma sends no header of its own). almaDir now also looks in
alma-server's folders on systems other than macOS and Windows, the
desktop's first; ALMA_DATA_DIR is in agentenv's Vars.
Verified: TestAlmaServerKeyed (a Linux home with only
~/.local/share/alma and a fake Alma whose magpie provider has the old
key): Sync re-keys it to magpie-alma; XDG_DATA_HOME and ALMA_DATA_DIR are
followed, the desktop's folder wins, and macOS/Windows look only in the
desktop's. It fails without the change ("Alma's folder:
.../.config/alma, want alma-server's .../.local/share/alma").
TestAgentOf has alma-server's real User-Agent bytes: Alma with magpie's
Alma key, "ai-sdk" without. Go tests ran on Linux (golang:1.26 in Docker;
this Mac's syspolicyd held every new test binary at launch).
Not tried with a real alma-server, and an alma-server on another
computer, reaching magpie with a gateway key, still reads as the AI SDK:
its User-Agent says nothing of Alma.
|
||
|
|
169c23c18e | stats: the day's event says which agents, providers and models magpie is used with, by magpie's own ids (a provider of the user's own, and its models, only "custom"), with how many of each and of routing groups: one "magpie uses" event per id in the same PostHog batch, so unique users break down by agent, provider and model. Settings → Privacy → Share the agents, providers and models I use turns it off (on by default; gone while Count me as a user is off); no names, URLs, accounts, keys or usage. go test ./internal/stats (payload, off switch, labels), the Privacy switch in Chromium and WebKit (en, zh), go test -tags nogui ./... in a temp HOME, go vet, GOOS=linux/windows builds pass; read against the real config: 9 agents, 24 providers, 25 groups in 0.75 s | ||
|
|
0d55d9f6c2 | provider, gateway: a Codex account at its usage cap is held from the next turn on: each ChatGPT reply's codex.rate_limits event and x-codex-* headers update the account's reading, and routing's reading ages from when it was made, not when routing took it (up to two minutes old before); the cap's tooltip says a turn under way can still pass it (#1295) | ||
|
|
4b8bfd4676 |
gui: Routing's By session heads a Claude Code (and every other agent's) session with its own name, not its UUID (#1293, luzhuang)
Only Codex sessions were named (from Codex's index). The page now also asks POST /api/gateway/session-titles for the other agents' sessions it lists, as agent:id keys, and sessions.Titles reads each from the agent's own files the way the Sessions page does: the name given in magpie, else the agent's own title (Claude Code's ai-title / custom-title), else the first prompt. Each key is asked once; the names are kept on the page, so a live update that replaces the rows keeps them. A session with no file here keeps its id. TestSessionTitlesNameOtherAgents fails without the change (titles = map[]), and routing-sessions' "other agents' sessions show their own names" times out waiting for the name without the routing.js change. Run: HOME=<temp> go test -tags nogui ./internal/gui ./internal/sessions; go vet for darwin, linux and windows; the 72 routing GUI test files in Chromium and WebKit (491 pass, 0 fail). No new t() strings. |
||
|
|
fe405b23e2 |
gateway: a Codex on another computer reads magpie's models with a gateway key from GET /v1/codex/models, in Codex's own catalog shape, as its model_catalog_url; /v1/models stays OpenAI's list (#1281, jskorlol)
Codex decodes a model_catalog_url as ModelsResponse {models:[ModelInfo]}
(codex-rs model-provider/src/models_endpoint.rs), which /v1/models' OpenAI
list isn't, and /backend-api/codex/models relays to the ChatGPT backend,
which answers a gateway key (replaced by magpie's token) with 401. The new
route lists the models magpie shows Codex, as codexcat writes them for
model_catalog_json, kept to the key's models (keyAllowed, as /v1/models),
the ChatGPT account's own included (provider.CodexCatalog). Codex reads at
most 1 MiB of such a catalog (MAX_MODEL_CATALOG_BYTES); entries past it are
left out and counted in X-Magpie-Left-Out rather than failing the list.
Verified:
- TestCodexCatalogForAGatewayKey through lanGuard(Handler) from a LAN
address: required ModelInfo fields, unique slugs, held key sees only its
models, no key / wrong key 401, /v1/models unchanged. Without the route it
fails "404 ... magpie serves /v1/chat/completions ..."; with keyAllowed
removed it fails "held key: [plan/m1 spare/m2]".
- TestCodexCatalogFitsCodexsLimit: 300 models -> 127 in 1047147 bytes, 173
left out. Fails 404 without the route.
- Real Codex CLI 0.161.0 (npm) and 0.160.1 in sandbox CODEX_HOMEs against a
sandbox `magpie serve` (0.0.0.0:3591, LAN on, two gateway keys) over
10.0.0.111 with a fake chat upstream: models_cache.json holds the catalog
(held key: fake/fake-coder only; free key: both), no "Unknown model"
fallback warning, codex exec answered PONG-1281 through /v1/responses,
also with the reporter's requires_openai_auth = true. 0.160.1 needs
[features] api_key_model_discovery = true.
- go test -tags nogui ./internal/gateway ./internal/provider in a temp HOME:
ok. Full ./...: 44 packages ok (internal/agent had timed out once
under load; alone it passes in 45s). go vet; GOOS=linux and
GOOS=windows vet of gateway+provider and nogui builds.
Not verified: Codex's TUI /model picker driven by hand.
|
||
|
|
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.
|
||
|
|
d2241a5798 | agent: Copilot in JetBrains IDEs takes magpie's models as a Bring Your Own Key custom endpoint provider, written into the Copilot language server's byok.json beside the user's own (KamToHung on Discord) | ||
|
|
e5e87d980a |
支持 Ante:把 magpie 作为 provider 写进 ~/.ante/catalog.json (#1210)
* agent: Ante takes magpie as a provider in ~/.ante/catalog.json Ante (antigma.ai, AntigmaLabs/ante) is asked for in #1179. Its catalog is ~/.ante/catalog.json ($ANTE_HOME moves the folder), merged on top of the providers it ships: a providers map keyed by the id --provider takes, each with base_url, wire_style (AnthropicMessage, OpenAiCompatible, OpenAiResponse or Gemini), an optional auth, and preferred_models, the ids its model picker shows (docs.antigma.ai, reference/catalog-reference). magpie adds the entry "magpie" at the gateway's /v1 on OpenAiCompatible, which is the wire style Ante speaks Chat Completions with and what the gateway takes every model on, and lists magpie's models in its preferred_models. The user's own providers and the models section are left as they are, and magpie's entry goes again when magpie steps out; a file magpie made goes with it. No auth block is written. The gateway listens on loopback and lets any token in from this machine (gateway.Token's own comment, lan.go's identifyCaller: "Loopback takes any token"), and Ante counts a provider with no auth as authenticated. A bearer env_key would mean putting the gateway's key into Ante's environment, which the user would have to do themselves -- the same decision Empryo's entry makes, which sends no key of the user's either. A gateway shared on the local network is another matter, and the same one every agent magpie wires has. Two things are deliberately left off a model's entry. A routing group is not told what it reasons at: which member answers, and what that member takes, is the group's own to decide per turn, and magpieModels sets no APIs for a group for the same reason. And support_vision is never written: Ante takes a model as seeing images unless told otherwise, and what magpie knows is only as good as a provider's own list -- a relay's models are often not described at all, so writing false would have Ante drop the reader's images before the gateway, which describes them to a model that can't see them itself (provider.Described). No other agent magpie wires declares it either. Ante ships macOS and Linux builds alone and suggests WSL on Windows, so the Windows machine's Ante is usually the one in a distro: wslKinds gains it beside Empryo, which also means the gateway address Ante is pointed at is the one that distro reaches magpie on (place.base), and a distro not known to be in mirrored networking gets the notice it already gets for the others. TestAnte covers a user's own providers and models section kept as they are, magpie's entry added and taken away, Sync repointing it, and a file magpie made going again. TestAnteKeepsTheUsersFile covers a catalog magpie can't parse costing magpie's provider and not the user's. TestAnteGroupSaysNoCapab ilities covers a group declaring neither effort nor support_vision. TestAnteEfforts covers the ladder: Ante's has no "none" or "minimal", its two lowest, so both are its "min", and the rungs come out in Ante's order whatever order magpie lists them in. Tried with a real Ante 0.2.9 in WSL Ubuntu: magpie wrote the catalog, and `ante catalog` then read 16 providers with magpie's among them, 39 models, and Ante filled in support_vision: true for each itself. Ante then ran a real turn through the gateway — `ante --provider magpie --model group/auto-deepseek-v4-1-flash -p …` answered, and magpie's own usage log has the request (ep /v1/chat/completions, req the group's id, served by one of its members, out 10) — and so did a second model on another provider. The gateway was reached over the Windows host's address from the distro (172.17.128.1), this machine's own being on 127.0.0.1 alone, which is what notMirrored tells the user about. go vet, gofmt -l and go build (linux nogui, windows) pass. go test -tags nogui ./internal/agent: 12 failures here, the same 12 on unmodified main (the Windows ones AGENTS.md lists). ./internal/gui and ./internal/provider: TestGatewayFixQuitsOnlyAMagpie, TestPurposeAssetMatchesUsage, TestUpdateThatWontStart and TestLoginUsageClaudeAskedMeanwhile, all four on unmodified main here too. docs/reference.md's adapter table gains Ante. * agent: Ante keeps the user's own keys, reads ANTE_HOME, sends no effort it wasn't told to, and starts Ante on magpie (#1210) Seven points from the review, each with a test that fails without it. 1. ANTE_HOME was in the file's own comment and in docs/reference.md but never read: the path was fixed to $HOME/.ante, so a user who set it had magpie write a catalog Ante does not look at. anteDir reads it through place.getenv, the way MiniMax Code reads MINIMAX_DATA_DIR, so a distro's own ANTE_HOME - which magpie can't read - still means ~/.ante there. "ANTE_HOME" joins agentenv.Vars so a sandbox clears it. 2. providers.magpie was written whole, so every key of the user's that magpie doesn't write - the auth block a gateway shared on the LAN or reached from WSL needs to send a key, http_headers, extra_body, stream_idle_timeout_secs, thinking_display - went at the next sync, and an unchanged catalog was rewritten every round. It goes through theirsKept and syncJSON now, as #1103 did for the others: the user's keys in magpie's entry stay, preferred_models is magpie's list alone, and a file that says the same as it does is left as it is. 3. The two files were written in separate steps, so a change that failed halfway left Ante's catalog without magpie's entry and its settings still naming magpie. Each field's Set and its Sync are wrapped in atomic(), as empryo, minimax and kimi do. 4. The comment said Ante's "min" was the rung it sends no effort at, and folded both magpie's "none" and its "minimal" into it. Ante's reference says what min is: "OpenAI-compatible providers send the selected level as the same-named reasoning_effort (min is sent as minimal)" - so min is a sent level, and magpie's "none" has no rung of Ante's to fold into. Ante's "min" is magpie's "minimal"; a model magpie knows only "none" of comes out as the empty list, which is Ante's own way of saying it takes no effort setting at all. 5. The default was the highest rung the model takes, so every model magpie listed ran at Ante's slowest and dearest level. No effort is written: "Omit for models with no reasoning knob (Ante then sends no effort parameter and hides the effort slider)". 6. Having magpie in the catalog never meant Ante used it. What a session starts on is the provider and the model of ~/.ante/settings.json; with neither set Ante "uses the first authenticated provider in catalog order", and Ante's own presets come before magpie's entry - so while the user has any of those signed in, that is the one picked and magpie is not. (Measured: nothing named in settings, two keyless providers ordered aaa_other then magpie, and the request went to aaa_other's port; settings provider=magpie and it went to magpie's.) magpie sets that pair now and saves what it found there, so Disconnect hands it back; a provider the user picks in Ante's own /providers while magpie is wired is theirs and stands, and Check says so instead of keeping quiet. Ante's settings name the model apart from the provider, so magpie writes the catalog id it put in preferred_models, un-prefixed, and reads a bare id as magpie's only beside magpie's provider. Tests lean on the package's own testenv, not a real ~/.ante. TestAnteSyncKeepsTheUsersKeys is modelled on ad3b7fe5's TestAirProviderWritesKeepCustomFields and goes both ways - sync and pick - and wants an unchanged file to keep its mtime. Tried with a real Ante 0.2.9 in WSL Ubuntu on this machine (Ante ships macOS and Linux builds alone; there is no Windows Ante here). The strongest of these is the whole of it: magpie's own Go code - ante(home).Connect(), then the model field - wrote a catalog.json and settings.json into a temp folder, that folder was handed to the real Ante as ANTE_HOME, and Ante ran a turn through it with no --provider, no --model and nothing typed by hand. - `ante doctor` read `home ok /tmp/ante-e2e` and `16 providers (1 authed), 108 models` from ANTE_HOME, and magpie's entry came back with the base_url it was given: ANTE_HOME really moves the folder, and a base_url already ending in /v1 is not doubled (the request went to /v1/chat/completions). - What a session starts on is settings.json's provider, measured against two keyless providers each answering on its own port, so the logged port names the one Ante asked. Catalog ordered aaa_other then magpie, nothing named in settings: Ante asked 44331, aaa_other - the first authenticated in catalog order, exactly the review's "很可能选不到 magpie". settings provider=magpie: it asked 44220, magpie. settings provider=aaa_other: 44331 again, so a pick the user makes in Ante's own /providers overrules what magpie wrote, which is why stepping out leaves it rather than handing back the one magpie found. And with magpie the only authenticated provider, auto-detection does reach it - so the catalog entry is not useless, it is simply not enough while the user has any other provider signed in, which is every real install. - What Ante sends for effort, from the same files: no reasoning_effort at all for a model with no effort written (item 5, as its reference says omitting it sends none); `--effort min` on a model sent `reasoning_effort: "minimal"` - Ante's min is magpie's minimal, exactly item 4; and a model Ante was told `"supported_efforts": []` for sent no reasoning_effort even when asked `--effort low`, which is what a none-only model now gets instead of a level that refuses it. - The user's own keys inside magpie's entry are ones Ante reads, not decoration: `ante catalog` hands back the entry's auth, http_headers, extra_body, stream_idle_timeout_secs and thinking_display unchanged, and a bearer env_key there makes `ante doctor` count the provider as the one authed. That is item 2. - The no-auth decision holds on Ante's side, isolated by doctor's counts: Ante's own presets alone read `15 providers (0 authed)`; a magpie with no auth block reads `(1 authed)`; the same magpie given a bearer env_key that isn't set drops back to `(0 authed)`; with the key set, `(1 authed)`. So an entry with no auth is authenticated and one that asks for a key the user doesn't have is not - writing an env_key, which the first version's comment considered, would have made magpie the one provider Ante refuses to auto-detect. - Ante fills `support_vision: true` in for itself, as the PR's own note said, and a routing group declared with no capabilities at all still worked. Not covered by that run: the stand-in server answered inside the distro, so this proves Ante's config handling end to end, not a live magpie gateway relay (the first commit's message records a real turn through one). Every probe used ANTE_HOME into a temp folder; this machine's real ~/.ante gained no catalog.json or settings.json. What each fix failed without it, one test each: taking ANTE_HOME out -> TestAnteHome "$ANTE_HOME ignored"; theirsKept out -> "the user's auth lost or changed"; syncJSON out -> "an unchanged catalog was rewritten"; atomic out -> "settings.json kept half of a change that failed"; the old min mapping -> "anteEfforts([minimal high]) = [high], want [min high]"; a written default -> "deepseek/pro: effort written"; settings never pointed at magpie -> TestAnte; a model never written -> TestAnteModelField; Check left quiet -> "moved off magpie: \"\""; handing magpie back over a pick the user made while wired -> TestAnteKeepsWhatTheUserPickedWhileWired; never restoring the model -> TestAnteConnectDisconnect "not put back"; Connect picking the model field instead of magpie as provider -> "Connect left the user's model beside magpie's provider"; ANTE_HOME kept out of agentenv.Vars -> TestFolderVarsAreListed. Taking preferred_models out of the whole list is the one mutation no test turns red on: mergeTheirs cannot merge a JSON array (sync.go returns want for it), so the file behaves the same either way and the argument is there for intent and for the other keys, not for behaviour. go vet ./... and go build ./... pass, as do GOOS=linux go build -tags nogui ./... and GOOS=windows go build ./..., and gofmt is clean on LF content (this clone has core.autocrlf and no .gitattributes, so gofmt -l reports every file). go test -tags nogui ./internal/agent fails twelve tests, and an untouched worktree of origin/main run the same command on this machine fails the same twelve, test for test; ./internal/agentenv, ./internal/appdir and ./internal/edit pass, and every Ante test passes at -count=5. The whole ./... was run on both: the failing sets are alike, origin/main having one more, TestLoginUsageClaudeAskedMeanwhile, already filed as #1228. None of those twelve is this change's, and none is fixed here. * docs: agent-wiring records that Ante keeps the user's keys in magpie's entry and starts on settings.json's provider (#1210) The two rules the review asked for, written where the repo keeps them - the same lines |
||
|
|
e52d1cb820 | docs: README rewritten for what magpie does now — stat line, flow diagram, warm-up, context, image MCP, keep awake, agent updates, all 47 agents; "coding agent" is now just "agent"; per-agent notes moved to the reference | ||
|
|
1b8c33b5e2 |
provider: a routing group reasons when any member does, not only when all of them do (#950)
* provider: a routing group reasons when any member does, not only when all of them do groupEntries started a group at Reasoning: true and intersected it with each member's (e.Reasoning = e.Reasoning && thinks), so one member that doesn't think made the whole group 不支持推理. Pi reads that off /v1/models and turns /thinking off for the model: a routing group of deepseek-v4-flash-0731-max (a Trae plugin model magpie read as one that doesn't reason) with a DeepSeek that does lost reasoning for both. The group now reasons when any member does. Images already worked this way (#756: a text-only member doesn't turn every image away), and the gateway fits a request's effort to the member that gets it (fitFor), failing over when a member refuses the level (failEffort). What the group offers as levels is not changed: a member known to take none still leaves the group none (#597), so such a group is Levelless — a shape magpie serves — rather than one that doesn't reason. group_reasoning_test.go (6 tests) fails without the change: TestGroupReasoningFromAMemberThatThinks, TestGroupReasoningLevels and TestGroupReasoningQuietMemberKeepsTheRest report reasoning off. TestModelsListReasoning, the /v1/models contract test, fails the same way and is updated to the new semantics. go vet, go build, GOOS=linux GOARCH=amd64 go build -tags nogui and GOOS=windows go build pass; go test -tags nogui ./internal/provider ./internal/plugin pass under a temp HOME. internal/gateway passes except TestPluginStreamStalledClientKeepsOther RequestsMoving, which fails on this machine with and without the change. * gateway, provider: a group's member that can't think is sent no reasoning, rather than a 400 A routing group reasons when any member does (#950, 533887c2), so an agent asks a group for reasoning that a member of it may not think at all. The effort reached every member, and some vendors turn a reasoning ask away with a 400 on a model that can't think — the review's second point: the comment said the gateway routes a reasoning request to the members that take it, and nothing did. The gateway now sends such a member no reasoning ask, in the agent's own protocol: Chat's reasoning_effort and reasoning, Responses' reasoning (with reasoning_effort, which some relays take), Anthropic's thinking when it asks to think — thinking disabled is no ask and stays — and output_config's effort alone, the rest kept. Everything else goes as the agent sent it, the Responses effort-update history is skipped for it, and the trace's Effort is "" (nothing was sent) with Fixed kept, so the user's own pin is still read. Which member goes first is unchanged: the group routes as it did, and only the body changes. provider.Entry.Quiet is the judgment: magpie has a word on the model — its vendor's or its maker's list, or its provider's own account list — and that word says no reasoning. A model nothing speaks for isn't quiet: it is sent the effort the agent asked for, as #597 leaves it. Nor is one that takes levels, which includes a model the user gave levels of their own: those take the effort the gateway fits to them. Direct requests outside a group are untouched, as TestPluginThinkingOff pins. TestModelsReasoningWithoutLevels (thinks_test.go) still expected group/mixed false; the review's first point. It is updated with a comment saying why the #402 expectation changes, and a group of two quiet members keeps the "none of them thinks" case. TestGroupNonThinkingMemberNoEffort (group_reasoning_test.go) fails without the change: with the withoutReasoningAsk call disabled it reports "B: the quiet member was sent map[... reasoning_effort:high]". TestEntryQuiet (provider) pins the judgment, and TestWithoutReasoningAsk the field set per protocol. go vet ./..., go build ./..., GOOS=linux go build -tags nogui ./... and GOOS=windows go build ./... pass. go test ./internal/provider, ./internal/catalog and ./internal/plugin pass whole; internal/gateway passes in three chunks covering every test (TestGroup|TestFixed|TestFast| TestModels|TestPluginThink|TestWithout|TestMember, TestEffort|TestResponse| TestReasoning|TestQuiet|TestTrace|TestRoute|TestRules|TestRule, and the rest), 43s + 36s + 790s. The whole suite in one process times out on this machine in an unrelated test (TestRuleDecidesAtTurnStartAndHolds here, TestRequestBodyProgressRenewsIdleDeadline on another run) — the base commit 533887c2 times out the same way, at 800s — so the chunks are what was run. The GUI is untouched, so no Playwright run. * gateway, provider: a member the user fixed at an effort keeps it, quiet or not The review's remaining point: Quiet() rests on magpie's catalog or an account's own model list, and what this PR is about is that word being wrong — the Trae plugin gave no capabilities, and magpie read a thinking model as one that doesn't. A member the user fixed at an effort ("provider/model:high", #189) is the user saying this model does think, so magpie's word doesn't override theirs: the gateway sends the fixed member its effort and strips the reasoning ask from the others as before. quiet := isGroup && c.effort == "" && quietTo(c.p.ID, c.model). c.effort is only ever the member's own fixed effort (Member.Effort, planLevel), never the agent's, a group's Group.Levels, the turn's pick or the Settings' — so those are stripped as they were. TestGroupNonThinkingMemberNoEffort's E is now the other way: the fixed quiet member is sent reasoning_effort "high", and the trace's Effort is "high" with Fixed "high". Dropping the c.effort == "" guard makes it fail with "E: the fixed member was sent map[... model:plain]". quiet.go and Entry.Quiet say why. gofmt, go vet ./..., go build ./..., GOOS=linux go build -tags nogui ./... and GOOS=windows go build ./... pass. The review's own set passes — go test -count=1 -run 'TestGroup|TestWithoutReasoningAsk|TestEntryQuiet| TestModelsReasoning|TestModelsListReasoning|Thinks|TestPluginThinking' ./internal/provider ./internal/gateway — and so does internal/provider whole (208s). internal/gateway passes in chunks (Test[A-D] 340s, Test[S-Z] 216s, Test[^A-DS-Z] split over TestEffort|TestResponse|TestReasoning|TestQuiet| TestTrace|TestRoute|TestRules|TestRule|TestFixed|TestFast 38s and the rest earlier): the whole suite in one process still hangs here on an unrelated test (TestCodexBodyReadsPastOldLimit this time, TestRuleDecidesAtTurnStart AndHolds and TestRequestBodyProgressRenewsIdleDeadline before), which the base commit does too and which passes alone in 0.9s. |
||
|
|
5d0c1dcad7 |
agent, library, sessions: a Claude Desktop installed as an MSIX package on Windows is found, wired, given MCP servers and skills, and its Cowork calls read, in the package's folders (Kilig on Discord, Windows 10 LTSC 21H2)
Desktop from its setup.exe there runs packaged, so Windows keeps what it
writes under %APPDATA% and %LOCALAPPDATA% in
%LOCALAPPDATA%\Packages\Claude_<publisher>\LocalCache\{Roaming,Local}.
Nothing was in %APPDATA%\Claude or %LOCALAPPDATA%\Claude*, so magpie didn't
recognise Desktop at all. The packaged app reads its own copy before the
real folder's, so writing the usual folders wouldn't reach it either.
New internal/desktopdir is the one resolver for Desktop's folders: Data
(its own userData: %APPDATA%\Claude, the package's LocalCache\Roaming\Claude),
Mode (the Claude folder deploymentMode goes into, %LOCALAPPDATA%\Claude as
before) and ThreeP (Claude-3p). The package is found by globbing
Packages\Claude_* (the publisher id isn't assumed) and used unless Desktop
runs in the usual folders: it has run there (Chromium's "Local State"; an
%APPDATA%\Claude holding only another tool's claude_desktop_config.json
doesn't count) and not later in the package's. Every place that built
these paths goes through it:
- agent: desktopDirs (its 3p switch's writes, DesktopConfig3p, Claude
Code's desktop check in claude.go) and detection, which also looks at
Data (replacing the separate %APPDATA%\Claude check). windowsDesktopDir
moved into desktopdir unchanged.
- library: apps() (Desktop's MCP file, was os.UserConfigDir) and
desktopSkillRoots (Cowork's skills-plugin).
- sessions: desktopDataDirs, a copy of agent's resolver, now desktopdir's
All(); it also adds %APPDATA%\Claude on a usual Windows install, where
Desktop keeps 1p Cowork sessions (only %LOCALAPPDATA%'s were read). usage's
request_page and session_provider read it through sessions.DesktopDataDirs.
Tests: TestFindMSIX/TestFindWindowsClassic/TestFindOthers (desktopdir),
TestClaudeDesktopMSIX (agent: detected, switch writes LocalCache\Local\Claude
and Claude-3p, nothing in the usual folders), TestClaudeDesktopMSIXLibrary
(MCP file and skills-plugin in LocalCache\Roaming\Claude),
TestCallsCoworkMSIX (sessions). With the call sites put back to origin/main
the last three fail: "an MSIX Claude Desktop isn't detected", "no Claude
Desktop target: <nil>", "dirs: [.../Claude .../Claude-3p]".
Run: go test -tags nogui ./... under a temp HOME with pinned caches (all
ok); GOOS=darwin|linux|windows go vet -tags nogui on desktopdir, agent,
library, sessions, usage; GOOS=windows go build, GOOS=linux go build -tags
nogui. On the Windows 11 box (Squirrel install, no MSIX package): the
windows test binaries pass every Desktop/MSIX/Cowork test; the full agent
and sessions binaries fail the same 16 and 76 tests (run outside their
testdata) as origin/main's; a read-only probe of desktopdir.Here() gives
the usual %APPDATA%\Claude, %LOCALAPPDATA%\Claude and Claude-3p, Package "".
Not tried against a real MSIX Desktop: none on the box, and Claude-3p's
place in the package (LocalCache\Local, as Local\Claude is in Kilig's
listing) is inferred, not seen.
|
||
|
|
be1ce5d4d2 |
davsync: incomplete S3 listings keep cached usage days (#1243)
Reject truncated listings with missing or repeated tokens and listings that exceed the page limit. On the old code the regression reports successful partial listings and shareWith removes a cached day. Focused tests pass in a temporary HOME. |
||
|
|
ac142bb1d2 |
agent, gateway: Claude Code through magpie can keep its claude.ai sign-in (#1218)
* gateway: Claude Code signed in to claude.ai is told a provider's 401 as a 502, not as its own sign-in refused
Claude Code signed in to claude.ai, its base URL magpie's and no key set,
sends its own sign-in as its key (Authorization: Bearer sk-ant-oat…),
which magpie never passes on. A 401 it reads as that sign-in refused: it
renews the sign-in before each of its retries (ten for one request) and
then asks to /login; a 403 saying "OAuth token has been revoked" the
same. Whatever magpie answers such a request with is never about that
sign-in: a provider refused magpie's credential, or one of magpie's own
Claude accounts lapsed (the bridge's 401 or revoked 403). keepsSignIn,
on /v1/messages, /messages and /v1/messages/count_tokens, tells such a
request's 401 as a 502, and a 403 too when its body says the token was
revoked; the body is the provider's message as it was. Recent calls and
the usage log keep the provider's status. Any other 403, and a request
with magpie's key or an API key, is answered as before; no
x-should-retry is added, so Claude Code retries a 502 as it does a 5xx.
Verified: TestClaudeSignInNotToldItsSignInFailed (401 on /v1/messages,
/messages and count_tokens; revoked 403; another 403; magpie's key; an
API key; a reply and a stream untouched; Recent calls' status) fails
without the route change ("401: 401 … want 502", "revoked: 403 … want
502", "401 counting: 401 … want 502"); TestSignInWriterReleasesA403. go
test -tags nogui ./internal/gateway, go vet and gofmt are clean.
* gateway: a provider never gets the oauth beta of Claude Code's claude.ai sign-in
Claude Code signed in to claude.ai asks oauth-2025-04-20 in
anthropic-beta with its sign-in. magpie relayed the agent's betas to
every Anthropic-protocol provider, so that one went too, naming a
credential that never leaves magpie: a provider got a request claiming
an OAuth sign-in it was not sent, beside magpie's key for it. askedBetas
leaves the agent's oauth- betas out of what forwardOnce asks, so a
provider gets the request as Claude Code with magpie's key sends it. A
beta the user set on the provider (header.anthropic-beta, fitUserBetas)
still goes, oauth- or not.
Verified: TestSignInBetaStaysWithMagpie (the oauth beta among others,
alone, and the user's own on a provider) fails without the forwardOnce
change (sent ["claude-code-20250219,oauth-2025-04-20,…"], and
["oauth-2025-04-20"] where none was wanted); TestUserBetas* and
TestClaudeCodeHeadersReachAnthropicRelay pass. go test -tags nogui
./internal/gateway, go vet and gofmt are clean.
* agent, gui: Claude Code through magpie can keep its claude.ai sign-in
Claude Code's new sign-in field is magpie's key (the default) or claude.ai.
claude.ai writes ANTHROPIC_AUTH_TOKEN empty, which Claude Code reads as no
key: it stays signed in to claude.ai (/usage plan limits, connectors, voice,
/teleport) and sends that sign-in to magpie, which never passes it on. Every
later write (a model, a tier, the subagents, a /model pick followed, Reapply,
a port move, Disconnect and Connect) keeps it so, and a key put there by hand
is reported by Check rather than taken for magpie's. A token emptied or taken
out by hand in key mode is reported with the way to keep it so.
It is offered only where the gateway takes any key (loopback, a WSL distro
mirrored), not from a distro under NAT while magpie is shared on the network.
The GUI draws no square for a Claude Code with no other sign-in to take.
Verified: go test -tags nogui ./internal/agent (new claude_login_test.go,
each test fails with its change reverted) and -run 'SignIn|Claude|Beta'
./internal/gateway; go vet; GOOS=linux/windows builds; claude-signin.test.cjs
(fails without the app.js change) plus i18n-keys, agent-field-names,
agent-layout, claude-direct, claude-effort, codex-signin-always,
subagent-effort and menu-scroll, Chromium and WebKit, en/zh/ja/de, 1100 and
440px. Real Claude Code 2.1.292 with the token empty sent Bearer sk-ant-oat
and the oauth beta to the base URL, took a 502 as a server error, and stayed
signed in to claude.ai.
* agent: Claude Code given a magpie model at an older gateway address keeps the endpoint it had before magpie
When the gateway moved while magpie wasn't running to follow it, a magpie
model picked next found Claude Code at the older address and stashed what
was there as the user's own: the dead gateway URL, magpie's key or the
empty one of a claude.ai sign-in kept, and magpie's own tier models. That
overwrote the endpoint and token stashed when magpie was first wired in, so
switching off put the dead address back, and with the claude.ai sign-in
kept "model default" left it and five magpie tiers too.
What's there at an older address is now taken for magpie's (keyOurs: its
key, or an empty token with the claude.ai sign-in kept), and the stash made
when magpie was wired in stays. The same test replaces "model default"'s
guess from the model's name, which could take out a user's own endpoint
with an empty token.
Verified: TestClaudeAtAnOlderGatewayAddress (both sign-ins, Disconnect and
model default; fails on the previous commit, the key-mode case on main
too); go test -tags nogui ./internal/agent; -race -count=5 on the Claude
and WSL Claude tests; go vet; GOOS=windows build.
* gateway: Claude Code signed in to claude.ai is told a provider's refusal in magpie's words, never mentioning x-api-key
Claude Code 2.1.293 tells any error whose message mentions x-api-key as
"Not logged in · Please run /login", whatever its status, so the 502 alone
still signed it out (yetone on #1218). A 401, or a 403 saying the OAuth
token was revoked, now reaches it as an api_error "<provider> refused
magpie's credential (HTTP 401): invalid API key", and a stream's error
after its 200 says "API key" too. Recent calls keep the provider's status
and message.
Checked with Claude Code 2.1.292 signed in to claude.ai, through the
gateway to a provider answering 401 invalid x-api-key: it shows "API
Error: 502 Fake refused magpie's credential (HTTP 401): invalid API key.
This is a server-side issue" and stays signed in.
* agent: an empty token at an endpoint of the user's own stays theirs, though Claude Code once kept its claude.ai sign-in through magpie
keyOurs took any empty ANTHROPIC_AUTH_TOKEN for magpie's once the sign-in
was kept, so after Disconnect a user's own https://corp.example with an
empty token lost both to the model default (yetone on #1218). The empty
token is magpie's only beside the address magpie last wrote, stashed as
claude.wired_at, which Rewire carries to the gateway's new port.
* gateway: no error Claude Code signed in to claude.ai is told mentions x-api-key, whatever its status
Claude Code reads any error naming x-api-key as its own sign-in refused,
so a provider's 403 "invalid x-api-key" or 400 "x-api-key header is
required" still told it "Not logged in · Please run /login" (yetone on
#1218, Claude Code 2.1.293). Every error such a request is told is held
until its body and says "API key" there, at the status it had; a 401 and
a revoked 403 stay the 502 in magpie's words.
|
||
|
|
202031642a |
provider: an account refused while its allowance is being read has it read again once that reading is back, and keeps the share it had meanwhile (#1142)
* provider: an account refused while its allowance is being read has it read again once that reading is back, and keeps the share it had meanwhile
StaleAllowance zeroed the time an agent's allowances were read, and
dropped the account's own reading, but a reading Allowances already had
out, asked before the refusal, came back after it, set that time to now
and was trusted for a minute. For that minute the Routing page and the
trace showed the share from before the refusal, the tray's account in
use kept the refused account, and a second refusal of the account found
its window not full: a quota refusal with no reset time rested 15
minutes by "quota" instead of until the window renews, and the account
was tried and refused again after it. Every caller of StaleAllowance had
it: the gateway after a refusal, Claude's limit headers, a Codex reset
spent and both warm-ups.
StaleAllowance now marks the agent stale when a reading is out, as
StaleKeyAllowance does a key (
|
||
|
|
a2c3905368 |
provider, gateway: a usage window whose reset has passed is renewed on every path, and an account resting out of it is back once a reading finds it renewed (#1200)
* provider: a usage window whose reset has passed is renewed on every path, so a fresh Claude /usage reading at 100% a minute after its reset no longer counts the account spent or moves Claude Code off it
Claude Code's /usage gives a reset to the minute and can lag it, so a run
just after a reset can still say "100% used · resets <a minute ago>".
claudeWindows handed that fresh reading back raw while its cached and
header paths already went through elapsed, and usedPast judged Used
without looking at ResetsAt. NextLogin dropped passed windows only for a
Claude reading kept from before (AsOf) and only for the account the agent
is on, not for the account to go back to or the spares.
- claudeWindows: the fresh /usage path returns its windows as of now
(elapsed), as the cached and header paths do. What is kept in
claudeUsage stays as read.
- resetPassed is the one test for a window that has started again.
usedPast, usedUp, UsedUp and BackAt take now and leave such a window out,
for Codex and Claude Code alike; NextLogin's Claude-and-AsOf special case
goes, since every account it looks at (on, back, spares) is now judged
that way. capReached already was.
- quota_last's reading still decides whether a Codex hold came with a
used-up window from the reading as it was read (usedUp at the zero time),
so the hold ends when that window starts again, as before.
- magpie quota wait and the Codex expiring resets pass their own now, so
quota wait no longer waits out a window that has already reset.
- docs/subsystems/providers-accounts.md says so, and that a reset /usage
gives as a time alone ("resets 3pm") is read as its next one by
claudeResetTime, which this change can't see as passed.
Verified under a temporary HOME with GOPATH, GOMODCACHE and GOCACHE
pinned: go vet ./..., go build, GOOS=linux go build -tags nogui and
GOOS=windows go build pass. go test -tags nogui ./... passes except
internal/gui's TestSetAnswersWhileDevinAndProvidersHang once in the full
run (set slow/slow-1 took 1.67s against its 1.5s limit, with other
sessions' suites running); it passed -race -count=20 on this branch and on
upstream/main, and internal/gui passed on its own. go test -race -count=20
on the new tests passes.
New tests (window_renewed_test.go) and what fails without the change:
- TestFreshClaudeReadingPastItsResetIsNotSpent: with the fresh path
returning raw ws, "the session reads 100% (reset ... 14:20:00 UTC)". The
control with its reset an hour away is spent and moves to the spare.
- TestNextLoginTakesAPassedResetAsRenewed (codex and claude): with usedPast
and BackAt ignoring ResetsAt and the old AsOf drop back, NextLogin moves
off an account whose full window reset a minute ago (fresh, and for Codex
kept), won't move to a spare or back to the first whose full window has
reset; the controls with resets an hour away are unchanged.
- TestUsedUpLeavesOutARenewedWindow: with BackAt and usedUp ignoring
ResetsAt, "a window reset a minute ago stops the account".
TestClaudeUsageExpiredReadingDoesNotSwitchAccounts and TestCodexHeld* pass
unchanged. Not tried against a real Claude or Codex account here.
* provider, gateway, docs: a subscription account resting out of its window is back once a reading finds it renewed
A Claude account rested for a full five hours or week waited out the whole
rest (the time Claude Code's refusal named, or c.full), even when a new
/usage reading or the reset passing showed the window started again: only
a Codex reset spent and a key's reading ever told OnRenewed.
provider: Allowances, the reading routing goes by for every account,
keeps each account's last reading and when it was taken (usedCache.seen,
kept through a failed reading). A new reading that finds the account full
till sooner than the last (renewedFrom, the rule keyAllowances' keep uses
for keys) tells OnRenewed as the agent its usage is read under
(Account.UsageAgent: "claude", or "plugin:<id>" for a plugin-served
subscription) and the user. Scopes are the whole account, and each pool of
some models with the whole account's windows over it. A window used below
SpentShareOf its routing, or whose reset passed, is full no more; one full
with its reset unknown is full for good. Only windows in both readings
are compared. So a first, failed, alike or partial reading tells nothing,
nor does a five hours renewed inside a used-up week, nor two windows of
one name. Hooks are told after usedCache is unlocked and before the
reading's waiters are released. renewedNow drops the account's seen
reading so a spent reset is told once.
gateway: renewed lifts only an account's rests out of quota or by
"window"; a rest for a rate limit, credit, verification or a failure
stays, as a key's already did. Before, it only ran for a Codex reset spent;
now readings reach it for every subscription.
docs: providers-accounts (Allowance step, test list), gateway-routing
(account rests lifted by a reading, test lists), reference (when an
account runs out).
Verified: TestClaudeAccountToldRenewed (a fake /usage: 100% tells
nothing, 99% tells nothing, 3% tells once, more use doesn't tell again; a
kept window whose reset passes tells once), TestPluginAccountToldRenewed
(plugin:fakeco; a failed reading in between, a reset spent, and a reset
passed while the vendor still says 100%), TestRenewedFrom (table, including
the five hours inside a full week and same-named windows), and
TestClaudeAccountBackOnceItsWindowRenews (gateway: a Claude account resting
by "resets" is back once a reading shows 4%, another still full keeps its
rest). TestQuotaRestsUntilItsReset now checks a rate-limit and a backoff
rest survive renewed. Without the tell, all three renewal tests fail (told
[], still rests by resets); mutating each branch of renewedFrom/fullTill
fails TestRenewedFrom or the plugin test. Under a temporary HOME: go vet
./..., go build, GOOS=linux go build -tags nogui, GOOS=windows go build,
go test -tags nogui ./... and go test -race -count=20 (the gateway test also -count=50) on the new and
changed tests pass. Not tried with a real Claude account.
Not covered here: once a /usage run prints Claude's limit refusal,
claudeWindows returns that error until /usage or a header succeeds again,
so Allowances skips the account and the reset passing tells nothing. That
path is claudeWindows' own; the change making passed windows count as
renewed there lets this tell reach it.
* gateway: a Codex subagent's sealed task goes back to the Responses provider that answered its lead (a Sub2API relay of the ChatGPT backend), relayed as it came, instead of being refused before it is asked; the refusal names who sealed it (#1109)
Codex on Ultra with s2a/gpt-6-astra for both the lead and the subagent,
s2a a Sub2API relay: the relay's ChatGPT backend sealed the lead's
spawn_agent task, and magpie turned the subagent away because only a
Codex account counted as able to read it, telling a user already on a
magpie model to "use a Magpie-served model for the lead".
The provider that answered the lead (leadProvider, from the lead
thread's affinity in any scope) now reads it too when it is a key
provider serving the model on Responses; the task is relayed, even
when Codex offers web_search (it was translated, which dropped the
agent_message's encrypted_content). A subagent on another provider, or
whose lead magpie never answered, is still refused before anyone is
asked, with a message naming the lead's provider when known.
Verified: TestSealedTaskGoesBackToTheRelayThatSealedIt (lead and
subagent on s2a/gpt-6-astra, with Codex's web_search tool) fails without
the change with the old 400; without only the relay part it fails with
the ciphertext dropped from the upstream request.
TestSealedTaskRefusedByAnotherRelay covers a lead on s2b and an unknown
lead. go test -race ./internal/gateway, go vet ./..., GOOS=linux and
GOOS=windows builds pass.
* codexcat: a third-party entry for a model Codex has its own entry for (s2a/gpt-6-astra) says the same multi_agent_reasoning_effort, so Codex's Ultra sends xhigh for it as for OpenAI's gpt-6-astra instead of falling back to max (#1108)
Codex resolves Ultra to the entry's multi_agent_reasoning_effort, and to
max when the entry has none and lists max. magpie's entries never carried
the field, so the same model ran at max through a relay and at xhigh
through the ChatGPT account. The value is taken from Codex's own entry in
models_cache.json for the model's slug (after the provider prefix and a
dated snapshot suffix), only when the third-party entry offers Ultra and
the effort itself; models whose own entry says nothing (gpt-6-sol today)
keep Codex's max. The gateway's literal "ultra" -> max mapping is
unchanged: Codex sends the resolved effort, not "ultra".
TestUltraEffortAsCodexOwnEntry, built from the reporter's entries, fails
without the change ("s2a/gpt-6-astra: multi_agent_reasoning_effort <nil>,
want xhigh"). go test -race ./internal/codexcat ./internal/gateway
./internal/agent and go vet ./... pass in a sandbox HOME.
* gateway: a DeepSeek model whose Codex request is passed through as it is (OpenCode Go's deepseek-v4.1-flash) gets its reasoning back as reasoning_text, and is never asked without it (#1104)
Codex hands a reasoning item back as it got it; one from a turn magpie
translated, or another provider in a mixed group served, has only its
summary. Passed through to OpenCode Go's /responses, DeepSeek refused the
request with "The `reasoning_text` in the thinking mode must be passed back
to the API". That refusal names the input, so passthrough asked once more
without every unsealed reasoning item (withoutBareReasoning) and marked the
model as refusing them, which left the reasoning out of every later request.
For a model that wants its reasoning back (replaysReasoning, the predicate
#388's buildResponses uses), a reasoning item without reasoning_text now
gets its summary as one, and withoutBareReasoning is never applied to it.
TestDeepSeekReasoningPassedThrough plays OpenCode Go with the reporter's
error word for word. Without the change it fails ("reasoning_text sent =
\"\", want the summary"); with only the strip guards taken out it fails ("a
reasoning item with nothing in it was answered"). go test -race
./internal/gateway, -race -count=10 on the new test, and go vet ./... pass.
* agent: a catalog sync or a model pick keeps the keys a user or another tool added to magpie's provider block (Pi's providers.magpie.compat.sendSessionAffinityHeaders from pi-cache-optimizer) instead of writing the block whole (#1103)
syncJSON and Pi's writeMagpie set providers.magpie to a freshly built
object, so a key magpie doesn't write there was dropped on every sync.
theirsKept now merges magpie's block into what the file holds: keys magpie
writes are its own, objects in both are merged key by key, and anything
else (compat, an options key, Crush's extra_headers) stays. The model list
is still magpie's whole, so a model gone from the catalog leaves it, and a
sync with nothing new writes nothing.
The same goes for every agent that writes the block whole: Pi and OmO,
Aside, Pencil, OpenCode (and the agents built on it), OpenChamber, Crush
and Command Code.
Verified: TestPiKeepsTheirKeysInMagpiesProvider (the reporter's
compat.sendSessionAffinityHeaders: true in Pi's models.json, through
SyncCatalog and a pick) and TestOpenCodeAndCrushKeepTheirKeysInMagpiesProvider
fail without the change ("compat.sendSessionAffinityHeaders dropped by the
sync", "opencode sync") and pass with it. go test -race ./internal/agent
and go vet ./... pass in a sandbox HOME.
* providers: a SiliconFlow key's card says SiliconFlow retired its balance query and has no API in its place yet, and that the balance and vouchers are on its console, instead of "410 Gone: {...}" (#1094)
SiliconFlow stopped /v1/user/info on 2026-08-14 (release notes, 2026.08.11)
and answers it with 410 {"code":20092,"message":"This endpoint is
deprecated and is no longer available.","data":null}. Its docs and release
notes name no replacement yet ("new interfaces will be announced
separately"), and the balance given before 2025-12 became vouchers, seen
under 余额充值 → 代金券 on the console. So the card now says that and links
cloud.siliconflow.cn (cloud.siliconflow.com for the international site). A
key SiliconFlow refuses (401 30014 "Token is invalid.") is still said as it
came, and a 200 is still read.
TestSiliconFlowRetiredBalanceQuery replays the reporter's literal 410 body
for both hosts, plus the real 401 body and the documented 200; without the
change it fails with `410: true 410 Gone: {"code":20092,...}, want it
retired and https://cloud.siliconflow.cn named`. go test -race
./internal/provider and go vet ./... pass. Not tried with a real
SiliconFlow key (none on this machine); the 401 body was read from the
live endpoint with a dummy key.
* gui tests: routing-failed-open measures the 400's row once the live page has followed the newest request and drawn its story
In WebKit the live cases read the row's top as soon as the list was there,
before the page had followed the newest request and drawn its story above
the list (.rt-log grows 17 → 104px), so the row was 87px lower by the click
and "picking the request moved the page" failed — with the test's own
commit's assets too (
|
||
|
|
c83c0d5fef |
gateway: sharing keeps the host MAGPIE_ADDR names (127.0.0.1 behind Tailscale Serve, one interface's address) instead of listening on every interface, and still asks what reaches it from elsewhere for a gateway key (#1112)
listenAddr turned sharing into 0.0.0.0:<port> and dropped the host of an
explicit MAGPIE_ADDR, so requiring gateway keys from remote callers also
widened the listener. A host MAGPIE_ADDR names is now kept while shared;
0.0.0.0, :: or no host still means every interface, and an unset
MAGPIE_ADDR still shares on every interface. LANURLs offers the pinned
address alone, or none on loopback (MAGPIE_PUBLIC_URL says the proxy's).
Verified: TestSharedKeepsPinnedAddr fails without the lan.go change
("shared, MAGPIE_ADDR on loopback, listens on 0.0.0.0:4555") and passes
with it, -race -count=20. go test -race ./internal/gateway ./internal/gui
green under a temp HOME.
|
||
|
|
6b6113aa7a |
gateway keys: a key can be created with a value of the user's own, one their clients already send, so moving from another gateway (CLIProxyAPI's api-keys) doesn't mean re-keying every client (love1sbug on X)
Gateway → Gateway keys → Add gateway key has a second field, "Your own key
(optional)"; `magpie gateway-key add <name> --key <value>` (or `--key -`
from stdin) does the same. Left empty, magpie makes a sk-magpie-key- as
before. A value is 8–256 printable ASCII characters with no spaces, can't
start with sk-magpie-, and can't be another key's. The list shows only its
last four characters ("…0042"); rotating it gives a magpie-made key.
From another computer any enabled key was already checked, so an own value
works there as is. From this computer the gateway only identified a request
whose key looked like sk-magpie-…, so an own value would have been let in
unidentified: not counted as the key, nor held to its limit, models or
accounts. managedKey now asks access.Named, which also knows the store's
own values (cached by the file's size and time).
Tested: go test -race . ./internal/access ./internal/gateway ./internal/gui;
-count=20 on the new and LAN guard tests; TestOwnKeyValueIsTheKey fails with
managedKey back on access.Managed. gateway-own-key.test.cjs passes 16/16
(chromium and webkit, en/zh/ja/de, 1000 and 440px), and the gateway-key
suites, i18n-keys, gui-ja and gui-de pass. A built binary in a sandbox HOME
with sharing on: own key from the LAN address 200, a wrong or no key 401,
from 127.0.0.1 200, the old value after rotate 401. GOOS=linux and windows
builds pass.
|
||
|
|
05683637d5 |
plugins: the Bun the plugin host runs on is kept as magpie-bun, so a proxy app's PROCESS-NAME rule can route the requests plugins (Cursor and the other moved subscriptions) make (#1048)
magpie downloads its own Bun into ~/.cache/magpie/bun/<version>/. It is now written there as magpie-bun (magpie-bun.exe on Windows), and a Bun downloaded before is renamed in place the first time it is looked for, so nothing is downloaded again and no second copy takes disk. The host, bun add and bun update all run it, so all of their traffic carries the one name. A $MAGPIE_BUN the user set is run as it is. Verified: TestBunDownloadedAsMagpieBun, TestBunDownloadedBeforeTakesTheName and TestHostRunsAsMagpieBun fail without the change (the Bun is .../bun, the host runs .../bun). TestHostRunsAsMagpieBun starts the real host on a copy of Bun 1.4.2 and `ps -o comm=` names it magpie-bun on macOS. On the Windows box the three tests pass with the Windows build of Bun 1.4.2, a running bun.exe renames to magpie-bun.exe, and Get-Process/Win32_Process name it magpie-bun. Linux not run here (CI's ubuntu job runs the test with Bun on PATH). go test -tags nogui ./... green; -race -count=10 on the new tests green. |
||
|
|
33e4f165aa |
docs: how magpie picks an account — the steps a request's accounts go through, what Make first counts under each routing, Smart's and Weekly pace's real rules, rests after running out, gateway vs. on its own, and how often an allowance is read (#1032, Cyb3rN8)
docs/reference.md gets a section "How magpie picks an account" after Routing
groups; docs/subsystems/gateway-routing.md links it and notes the two
points users most often misread. Docs only; no behavior changes.
Every claim in the issue was checked against origin/main (
|
||
|
|
dd027bc433 | sessions: a Claude Code workflow's agents are read as their session's calls (ksinverse on X: 发现Claude的用量和会话不太准,codex就非常好。是不是sub-agents没有被统计进去?默认开ultracode) — Claude Code writes a workflow's (ultracode) agents a folder deeper than other subagents, <session>/subagents/workflows/<run>/agent-*.jsonl beside the run's journal.jsonl, and ccFiles read only <session>/subagents/*.jsonl, so with ultracode on every workflow agent's calls and tokens were missing from Usage, the Requests page and the session's totals. ccFiles now reads each run's agent-*.jsonl too (the journal isn't a transcript and stays out), and sessionOfPath gives them their session, so they count as the session's like any subagent's; Claude Desktop's and Qoder's readers, which share ccFiles, follow. TestCallsWorkflowAgents (a session, a workflow agent's file and a journal with a line that reads as a call) fails without the change (1 call, the agent's missing) and passes. On this machine's real ~/.claude (read only), 34 workflow-agent calls of one session were unread and are now that session's. go test ./internal/sessions, -race -count=10 on TestCalls*/Subagent*/Trace*, go vet (darwin, windows), gofmt -l, linux and windows (nogui) builds pass. Not tried against the reporter's own transcripts. docs/reference.md says it. | ||
|
|
a334fe091c | gateway: a request a proxy or tunnel on this computer forwards to loopback is answered as one from another machine, and needs the gateway key (#1022, znjhahaha: asked for a Cloudflare Tunnel in magpie, with the gateway key checked for requests through it) — local() was the request's address alone, so cloudflared, ngrok, Tailscale serve or funnel, frp's HTTP proxy, or a Caddy or nginx here, all reaching 127.0.0.1:3425 for clients anywhere, were taken for this computer's agents: whoever had the tunnel's address used the user's subscriptions with any token, read /v1/magpie/quotas (the accounts and their balances) and the remote cards, and their usage was counted as local, all without sharing on. reference.md said a proxy here had to sign its clients in itself; a quick tunnel signs in nobody. A request on loopback carrying a forwarding header (Forwarded, X-Forwarded-For, X-Real-IP, Cf-Connecting-IP, True-Client-IP, Tailscale-User-Login), which no agent sends, is now proxied, and local() is false for it, so every check built on local() takes it as from the network: lanGuard refuses it while magpie isn't shared, saying it came through a proxy or tunnel and that Settings → Share on local network and a gateway key let it in; shared, it needs an enabled gateway key and is then sharedWith, as a LAN client is, for the model APIs, quotas, remote cards, concurrency, session route and MCP sign-ins; the Omarchy theme and the Codex title link stay this machine's only, and its usage is no longer marked local. A gateway MAGPIE_ADDR opens to the network stays open to a tunnel as to anyone; one MAGPIE_ADDR keeps on loopback is closed to it. MAGPIE_TRUST_PROXY=1 says a proxy here signs its clients in itself and keeps what it forwards local, as before (listed in agentenv_test's notAnAgent). A tunnel forwarding bare TCP (frp tcp, ssh -R, socat) adds no header and can't be told from an agent: reference.md says to point one at the LAN address with sharing on. Running cloudflared from magpie with a pasted Tunnel Token is not done here: magpie would have to download or run a program it doesn't ship; reference.md says how to serve magpie through a Cloudflare Tunnel today. TestProxiedLoopbackNeedsTheKey (from 127.0.0.1 with cloudflared's, ngrok's, nginx's, RFC 7239's, Akamai's and Tailscale serve's headers: refused unshared, 401 shared without a key, with magpie's token or a wrong key, let in with the key under its identity, a disabled key refused; an agent on loopback let in with any token; MAGPIE_TRUST_PROXY; MAGPIE_ADDR on loopback and open) and TestProxiedLoopbackRoutes (quotas, quotas history, models, Omarchy and /mcp through a tunnel, unshared and shared, with and without the key) fail on main (200 for every tunnel unshared) and pass -race -count=10 with TestLANGuard, TestQuotasOverLAN, TestManagedLANGuard and TestExplicitOpenGatewayAcceptsAllRemoteTokens; go test -race -tags nogui ./internal/gateway (579 s, under -timeout 30m) and go test -tags nogui ./internal/... under a temp HOME pass; go vet (darwin, windows), gofmt -l, linux and windows (nogui) builds pass. No GUI change. Not tried with a real cloudflared (not installed here); its Cf-Connecting-IP and X-Forwarded-For are what Cloudflare documents it sends. | ||
|
|
555f570660 | gateway, provider: the Routing page says what a lone key or account has left, and a key bought as a plan (Kimi Code, GLM Coding Plan, MiniMax, OpenCode Go, Command Code) is weighed by its plan's windows (#1016, gphysign: Kimi Code (China) with one key on, Smart routing; the card said only "Kimi Code (China) 只启用了一个 Key,无需选择。" while /v1/magpie/quotas had its 5 hours 49 used, 51 left and its 7 days 45 used) — two gaps. weighRouted returned before reading anyone's allowance when there was one candidate, so a lone key or a lone subscription account was never known in the trace, and the Routing page, which shows a bar and "{n} used · renews in {d}" for a known row, had nothing to show; every single-account subscription said "what's left not known yet" the same way. And KeyAllowance read only a sub2api key's limits, so a key whose plan the Usage page reads (planQuotaSourceOf: Kimi Code, GLM Coding Plan and Z.ai, MiniMax Coding Plan, OpenCode Go, Command Code) was never known to routing, with one key or several. weighRouted now reads what each candidate has left before returning a lone one (one alone is still weighed against nobody). readsKeyWindows takes a plan key too; keyWindows reads it through planKeyWindows (planWindows, with Zhipu's team quota behind a key that has none of its own, as PlanQuotas does), and PlanQuotas' own read of a card is noted for routing at once (noteKeyAllowance), so the Usage page and routing never ask twice; the card kept on disk seeds it after a restart (keyCardTag keys a plan card as plan:, a sub2api one as balance:). A key at such a vendor that turns out to be pay-as-you-go, with no windows, is asked again every 30 minutes (keyNoWindowsAge), not every minute it is used. So, beyond the issue: several plan keys on one provider are now weighed by their windows under Smart, usage and pace, and a plan key the vendor refuses as out of quota rests until its window renews and comes back once read short of full, as a sub2api key does. affine no longer says a lone key or account all but used up "moved" its conversation: there is nobody to move to (at > 0). The GUI needed no change. TestLoneOneTellsWhatItHasLeft (a lone Kimi key known at 49% with both renews, the week first; a lone Codex account known; a plain DeepSeek key not), TestLoneSpentKeyKeepsItsConversation (fails with the at > 0 guard taken out), TestPlanKeyAllowance (the windows read behind the request, asked once; DeepSeek and Moonshot keys never asked), TestPlanKeyAllowanceFromItsCard (PlanQuotas' read known at once without a second ask; after a restart with the vendor down, the card on disk stands in and a failed read keeps it) and TestPayAsYouGoKeyAskedSeldom (a GLM pay-as-you-go key asked once, not two minutes on, again 31 minutes on) all fail without the change and pass -race -count=20 (provider) / -count=10 (gateway). routing-key-windows.test.cjs gains a lone Kimi Code key at 1100px and 440px (the one-key story, the row's bar at 49%, its provider row "49% used · renews in …" fitting, no sideways scroll) and passes in full on Chromium and WebKit in en, zh, ja and de; routing-quota-left and panel-routing pass. go test -tags nogui ./internal/..., go test -race on ./internal/provider and ./internal/gateway in full, go vet, gofmt -l, darwin, linux and windows (nogui) builds pass. Not tried with a real Kimi Code key. StepFun's Step Plan is read through a sign-in, not the key, so it isn't one of these. docs/subsystems/gateway-routing.md, providers-accounts.md and docs/reference.md say it. | ||
|
|
e6dd5d20bb | usage, gateway, gui: a reply that reasoned is timed by its answer, from its first text, not by all its output from its first token (tony on Discord #general: Usage › Requests read "gpt-6.1-sol 163 token/秒 · 8,036 个请求 · 首字 14 秒" and asked whether subagents were added up) — speed was every output token over the time after the first content (text, reasoning or a tool call). Output counts the reasoning, and a model whose reasoning is hidden (OpenAI's encrypted reasoning, a summary sent when it is done, Claude's omitted thinking) writes it before the stream shows anything, inside the TTFT; so a 2,000-token think and a 200-token answer over the 1.5 s after the first token read 2,200 in 1.5 s, 1,467 tok/s. Concurrency was not the cause: the aggregate is Σ tokens over Σ per-request windows, so subagents running at once never add up. In the owner's own usage.jsonl codex/gpt-6-luna turns read up to 600 tok/s (out 496, reasoning 224, 11,055 ms, ttft 10,228) and its aggregate 133; it is 80 now. usage.DecodeOf is the one rule: a reply with reasoning counts out − reasoning over ms − firstText, one that reasoned and wrote only tool calls tells no speed, one without reasoning is timed from its TTFT as before; DecodeWindow's burst cut (#731) still applies. Record.Decode feeds summarize, the ledger's Totals and its timeline Parts; a route keeps its reply's reasoning (Route.Reasoning, from gateway.go and codex_backend.go), and routing.js's decodeOf/speedOf times its rows, the story and a session's average by it (an older route takes its served usage's reasoning); app.js's ledDecode does the Usage page's rows, whose tooltip says the reasoning left out, and the Output speed help says the rule, in en, zh, ja and de. History is read the same way, as it keeps the reasoning and the first text. Routing never ranks by speed. TestReasoningReplyTimedByItsAnswer (the 2200/2000 turn, a summary 10 ms before the text, a concurrent subagent, tool calls only, reasoning over output, no reasoning, not streamed: 1,200 tokens in 11 s through summarize, addRow, LedgerSeries and Decode) fails without the change and passes -race -count=10; TestUsageKeepsTimeToFirstToken now has its paced replies report reasoning and checks the route keeps it. reasoning-speed.test.cjs (Routing rows 133, 100, none and 100, the story never 1,467, the session 105; the Usage rows, tooltip and KPI the same; no sideways scroll) passes on Chromium and WebKit in en, zh, ja and de, and fails against the old assets (1467 tok/s); routing-request-metrics, routing-burst-speed, usage-speed, usage-ledger-detail, i18n-keys, gui-ja, gui-de, ledger-hscroll and click-scroll pass on both. go test -race -tags nogui ./internal/... under a temp HOME passes but internal/proc's TestUserPath ("the login shell's PATH never came"), which failed under the full run's load, passes 3/3 alone and is not touched here; go vet, gofmt -l, linux and windows (nogui) builds pass. Not tried with tony's data. docs/reference.md says how output speed is counted. | ||
|
|
490d2ea362 | gui: the tray panel's Allowances tab hides subscriptions and moves them, from an Arrange at its foot (H20 on Discord: "菜单栏的详情页能不能选择某些不显示,有点额度并不想看到或者可以调整顺序,目前的还不行") — the panel's tab already followed the Usage page's order (usageOrder, byUsageOrder), but it could be set only by dragging a card's logo on the window's Usage page, which nothing in the panel said, and there was no way to leave a subscription out of the panel at all. The tab's foot now has Arrange, and "{n} hidden" beside it when some are: Arrange lists a row a subscription (balances too) in the shared order, its logo the handle (a drag, or Alt+arrows; the grip by it on hover only, as on the Usage page) and a Hide/Show pill, under a note that hiding is the panel's only and the order is the Usage page's too; Done draws the cards again. What's hidden is kept in magpie's settings (settings.PanelUsageHidden, panelUsageHidden), not the browser: routing, caps, the Usage page and the menu bar's cells still have it, and a menu bar cell for a hidden subscription still opens its card (panelPeek) until the panel is put away or the tab left. POST /api/usage/arrange takes order and panelHidden and changes only the one it is sent (both were whole-order before; an order alone still works), and the Settings save keeps panelUsageHidden as it does usageOrder. Saves draw at once and are put back with the error if they fail; no click moves the page (the view's scroll is kept across the redraw); no native select, no left-border stripe. Which allowances the menu bar shows beside its icon stays Settings' pick (trayUsages). New t() strings have zh, ja and de. TestUsageArrangePanelHidden (each field set and cleared on its own, a Settings save between) passes -race -count=20, TestSettingsSaveKeepsWhatItDoesNotSend's fixture names the field. panel-arrange.test.cjs (440px: the hidden card left out and counted, Arrange keeping the reader's scroll, the rows and note, the hover-only grip, no stripe, no sideways overflow, Show/Hide posting panelHidden without a scroll and a failed save put back, Alt+arrow and a drag posting the order, Done's cards in it, a menu bar cell opening a hidden card until the panel hides, the Usage page keeping the hidden card) passes on Chromium and WebKit in en, zh, ja and de; click-scroll, gui-ja, gui-de, i18n-keys, panel-fold, panel-usage, quota-*, routing-quota-left, tray-cell-click, tray-inuse, tray-logos, tray-usages, usage-accounts(-fold), usage-arrange, usage-fold, usage-quota-head, usage-card-refresh, usage-stale-answer, usage-remote-cards, api-key-usage, claude-usage-asked and usage-checkin pass on both. go test -tags nogui ./internal/... under a temp HOME, go vet, gofmt -l, linux (nogui) and windows builds pass. Not tried in the real tray panel on a Mac. docs/reference.md and docs/subsystems/gui-shell.md say it. | ||
|
|
c5a54bc74b | agent: VSCodium's magpie group is written for the provider its installed Copilot Chat declares, OpenAI Compatible (customoai) on the Marketplace's 0.48.1 (#1014, xybio: VSCodium's Chat picker showed only Auto) — VSCodium has no Copilot Chat of its own, and the last Marketplace Copilot Chat it can install, 0.48.1, declares no Custom Endpoint provider (its contributes.languageModelChatProviders: copilot, copilotcli, claude-code, anthropic, xai, gemini, openrouter, openai, ollama, customoai, azure), so the customendpoint group magpie wrote in chatLanguageModels.json was never read. magpie now reads the package.json of the github.copilot-chat installed under ~/.vscode-oss/extensions (vscodeChatOf: the one extensions.json lists, else the highest github.copilot-chat-<version> folder not in .obsolete) and writes customendpoint when it is declared, customoai when only that is: each model's window as maxInputTokens beside maxOutputTokens (0.48.1 adds them up), and the token as x-api-key, since 0.48.1 drops an Authorization in requestHeaders and its apiKey has to be a ${input:} secret; the gateway already takes a key from x-api-key, past an empty Authorization, so Usage still counts VSCodium. No extension found, or a package.json that can't be read, keeps customendpoint as before; one declaring neither is said in Notice and Check. Writing the group removes magpie's group for the other provider in the default's and each profile's file, and Disconnect removes both; the user's own groups, under either provider, stay. Sync (at magpie's start and on catalog changes) rewrites a group written for the other provider, and Check reports it until then. VS Code and Insiders, whose chat is built in, keep customendpoint unchanged. The Notice says 0.48.1 registers its providers only while signed in to GitHub with a personal Copilot plan (its BYOK registration is gated on the Copilot token's individual/BYOK flag). TestVSCodiumCopilotChatOpenAICompatible (the real 0.48.1 provider list installed in a temp ~/.vscode-oss: Connect writes customoai with x-api-key magpie-vscodium and no Authorization or contextWindow, the user's groups kept, Check empty; a Custom Endpoint group an older magpie wrote is flagged and rewritten by Sync; a Copilot Chat declaring customendpoint switches it back; one declaring neither is reported; Disconnect removes both) and TestVSCodeChatInstalledWithoutExtensionsJSON (folder scan, .obsolete, unreadable package.json) fail on main, which writes customendpoint, and pass; -race -count=10 on the VS Code tests, go test -tags nogui ./internal/..., go vet, gofmt -l, linux and windows (nogui) builds pass. No GUI change. Not tried with a real VSCodium. docs/subsystems/agent-wiring.md and docs/reference.md say it. | ||
|
|
b0a96cbbd7 |
gateway, provider, gui: a Sub2API key given 5h/1d/7d limits is weighed and rested by its own windows, like an account (#1004)
* gateway: a Sub2API key out of its 5h/1d/7d limit ("…限额已用完") rests as out of quota, not a 1-minute rate limit
Sub2API refuses a key that has spent the 5-hour, day or 7-day limit its
owner gave it with 429 "api key 7天限额已用完" (or 日限额, 5小时限额),
typed rate_limit_exceeded. It matched rateWords and not plannedWords
("限额" is not "额度"), so failure() said rate: the key rested a minute,
was tried again, refused again, and sank, for the days its 7-day window
had left. plannedWords now has 限额已用完, so it is failQuota.
The relay's other refusals keep their classes, fixed by the test: the
pool out of accounts (v0.1.149's 503 "No available accounts": other;
later versions' 429 "All available accounts are currently
rate-limited": rate), its upstream's rate limit (rate), the key's own
total quota API_KEY_QUOTA_EXHAUSTED (quota) and the wallet's 403
INSUFFICIENT_BALANCE (credit).
Verified: TestSub2APIKeyLimitIsQuota, with Sub2API v0.1.149's message
bodies. Without the change it fails: "429 {…api key 7天限额已用完…}:
rate, want quota", and the same for 日限额 and 5小时限额.
* provider: a Sub2API key's /v1/usage shows its 5 hours / 1 day / 7 days windows instead of nothing at "remaining"
A Sub2API key its owner gave a 5-hour, day or 7-day spending limit
answers /v1/usage in quota_limited mode with rate_limits — each
window's limit, used and reset_at in dollars — and, without a total
quota of its own, no "remaining". With the Balance URL set to it, the
key's card said `nothing at "remaining" in the reply`.
readSub2APIKeyLimits reads rate_limits into the windows the cards
already name 5 hours, 1 day and 7 days, in USD. A window not started
(used 0, no reset_at) is taken to renew a span from now; a window with
a limit of 0 is none. The card shows them, beside a balance field the
user wrote; with none, Check balance and `magpie provider show` give
what the fullest window has left. A wallet key's reply is unchanged.
KeyAllowance gives routing the same windows, per provider and key: read
behind the request, never waited for, at most once a minute (once for
any number of asks at a time); a failed read keeps the last; after a
restart it is seeded from the card's last reading in quotas.json;
StaleKeyAllowance has it read again at the next ask. ForgetBalances (a
key or Balance URL changed) has every key read again, a read begun
before dropped, the last reading standing until then. Only keys whose
Balance URL is a Sub2API /v1/usage are asked: any other key, and every
account, costs nothing here.
Verified with real bytes of a limited key's reply, its key removed
(testdata/sub2api_usage_key_limits*.json): TestReadSub2APIKeyLimits,
TestSub2APIKeyLimitsOnItsCard, TestKeyAllowance*. Each fails without
its part, e.g. the card without the windows: `Error:nothing at
"remaining" in the reply`; KeyAllowance waiting for the read: "waited
30.0s"; asking every time: "asked 11 times"; a failed read asked again
at once: "asked 22 times after failing"; not seeded: "seeded = [],
false"; any key asked: "asked /api/usage/token"; ForgetBalances
dropping what was read: "after ForgetBalances, and a read begun before
it: []"; leaving a read under way loading: "the key's windows were
never read"; a stale-generation read landing: it replaced 95%; a limit
of 0 taken: "a day's limit of 0 = [{… Used:100 …}]"; Balance from the
first window only: "$300.00" for $80 left.
* gateway, gui: keys whose windows magpie reads are weighed and rested by them, like accounts
Smart, least-used and pace routing weighed only subscriptions by their
windows: weighRouted skipped every key (c.p.Account == nil), so a
Sub2API key beside Codex accounts in a group went by its place in the
order, and, refused for its 7-day limit, rested a quarter of an hour at
a time while the window had days to run.
A key whose own windows magpie reads (KeyAllowance: a Sub2API key given
a 5h/1d/7d limit, its Balance URL /v1/usage) is now weighed as an
account is: fine, low (90%) or spent (98%), the one renewing soonest
first, in a provider and in a group alike, and its trace row carries
them. Out of quota it rests till the window it filled renews, by
candidate.full; when its windows as last read aren't full it rests
quotaRest, and either way they are read again (staleKeyAllowance).
Every other failure of a key backs off as before, never to the window:
the relay out of accounts for everyone (503/429) says nothing of the
key's own spend. capHeld and pooled stay accounts' only. Keys magpie
reads no windows of are ordered exactly as before.
The Routing page tells such a key as it does an account: its row's bar,
used share and renewal, why it went first by its allowance, "Every key
and account here is at 90% or more", and the Smart header. A key not
read yet beside one read is told as weighed with it, not "keys go in
their order" or by its tokens. New strings in en, zh, ja and de.
Other users of the relay draining its accounts don't show in a key's
windows, which count only its spend: they show as the pool's 503/429,
which back off and fall over to the next key, account or fallback.
Verified: TestKeysWeighedByTheirWindows, TestGroupWeighsKeyWindows,
TestKeyOutOfItsWindowRestsUntilReset, TestKeyPoolErrorDoesNotRestForWindow;
and routing-key-windows.test.cjs in Chromium and WebKit (en/zh/ja/de).
Without the change they fail: "smart: kw#a kw#b kw#c"; "the key renewing
first: codex sub2api"; "full: rests 14m59.99s by quota"; "read again:
[]"; backing off to the window: "pool empty: rests 71h59m59.99s by
window (other)"; and on the page "relay-a goes first: keys go in their
order, those that suit the request first." where all are at 90% or
more, or a key not read yet beside one read.
* gateway, provider: a Sub2API key out of its limit is back once its windows read short of full, not only when the window renews
A key refused for its 5h/1d/7d limit rests till the window it filled
renews: days, for the week. Its windows are read again behind the next
requests (a minute apart) and by its card, but nothing lifted the rest
when they came back short of full, so the owner raising the key's limit
in the relay's panel, or resetting its usage, left the key out till the
week turned, though the relay would take it at once. Accounts have this
already: a Codex reset spent tells OnRenewed, and the gateway lifts the
account's rests (renewed).
A key's reading, routing's or its card's, now tells OnRenewed the same
way, as agent "" and the key's KeyAllowanceID, when a window it was full
in (Allowance.Full at SpentShareOf its routing, as candidate.full counts
it) is full no more, or full till sooner. Its first reading, a failed
read, or one that finds it fuller tells nothing. The gateway notes the
key's KeyAllowanceID on its rest out of quota only, so renewed lifts
that rest and no other: the relay out of accounts for everyone (503)
still backs off, and another key still out of its week stays out.
Where nothing weighs the key by its windows, nothing read them again
either: an ordered group weighs each member's keys alone, so a member
with one key, like a provider with one key, never asks for them. After
the one read its 429 starts, the key sat out its week on that reading.
restingOf, which plans a resting key behind the rest, now asks for the
windows of a key resting out of them (agent "" and a user on its rest),
so the next request planned past a minute reads them, behind it, and a
reading short of full brings the key back there too. No card is read
for it, and a key resting for any other reason asks nothing.
Verified: TestKeyAllowanceRenewed (provider) and
TestKeyBackOnceItsWindowIsNotFull (gateway, end to end through the key
cards' reads and restAfter), each failing with a piece of the change
undone: never telling, "its week's limit raised to $1,000: told []" and
"a, its limit raised: still rests by window till …"; telling on any
change, "its first reading, out of its week: told [/sub2api#…]"; a
failed read telling, "a read that failed: told [… …]"; the card not
telling, "its usage reset, read on its card: told [/sub2api#…]"; the
rest not noted, "a, its limit raised: still rests by window"; every
rest of a key noted, "c, the relay out of accounts: …, false"; renewed
not matching the key, "b, still out of its week: …, false". And
TestRestingKeyReadAgainInAnOrderedGroup (gateway: an ordered group, its
first member one key on a fake /v1/usage, no card read after the 429),
failing without restingOf's ask: "its limit raised: still rests by
window till …, its windows read 1 times".
Tried on a real Sub2API v0.1.149 relay, in a sandbox HOME, its key's 7d
limit lowered under its spend for the test and then restored, with
magpie serve on a group ordered: the key, then a local stand-in. The
key answered 429 "api key 7天限额已用完"; it rested by window till its
7d reset, 2026-10-13 00:00 +08:00, and the stand-in served. Planned
again a minute later, still over, it still rested. With the limit back
at $800, the next request was planned with it resting, the stand-in
serving, and read its windows; the one after went to the key, 200.
|
||
|
|
87487c5ef0 | plugin, provider: a plugin can press its vendor's daily check-in itself (auth.checkin), and magpie runs it as its own check-ins (Lemon on Discord #feedback: provider 每日签到的方法能放到插件里面去做吗; ARNO earlier asked for Qoder's) — the check-ins magpie had (WorkBuddy, Trae CN, MiniMax Code, Qoder) are each written in magpie against the vendor's endpoint, even for a subscription that moved to its plugin, so a plugin author had no way to give their provider one. A plugin's auth hook may now have checkin(getAuth, provider) beside usage: the host lists the provider with checkin: true (plugin.Provider.Checkin) and answers checkin({provider, account}) (plugin.AccountCheckin), refreshing the account as usage does, with {outcome, credit, streak, message}; outcome is claimed, done, ineligible, inactive, captcha or failed, and a throw or an outcome outside these is failed with the reason. internal/provider/plugin_checkin.go runs these through checkin.go's checkiner, as the built-ins are: each signed-in account in use once a Beijing day (first look 2 minutes after start, then every 30), a failure asked again after wbCheckinRetry, every other outcome settled for the day, results kept in plugin-checkin.json by provider|account; a failure is recorded as failed and resets nothing. captcha is new (CheckinCaptcha): the vendor wants a captcha, which magpie never solves, so it is said as "check in in its own app", not as a failure, and not asked again that day. The results mark the account's Usage card (checkinBy "plugin:<provider>", WithCheckins), so the card's check-in row, its Auto check-in and Check in now work for it (POST /api/settings/plugin-checkin {provider, on}, POST /api/usage/plugin-checkin {provider}); Settings → Usage gains a Plugins tab, shown while such a plugin is signed in, a Daily check-in row per provider with Off/On and how each account went; magpie accounts checkin and c on the TUI's Usage page include them (the TUI's "no account" line names check-in plugins). The switch (settings.PluginCheckins, by provider) is off by default; for WorkBuddy, Trae CN, MiniMax Code and Qoder it follows that vendor's existing switch until set on its own, and magpie's own check-in through the plugin's fetch leaves a provider whose plugin checks in to it (pluginChecksIn), so no account is checked in twice. The Settings page's save carries PluginCheckins over (TestSettingsSaveKeepsWhatItDoesNotSend's fixture names it). No plugin declares the hook yet; Qoder's check-in stays magpie's own here. TestPluginCheckinRunsAsTheBuiltinsDo (the fake plugin with FAKE_CHECKIN, five accounts answering claimed, done, captcha, a throw and an unknown outcome: off presses nothing; on, each outcome kept with its vendor; a minute on nothing pressed again; past the retry only the two failures; the next day all five; PluginCheckins and the cards marked; another provider's press asks none) and TestPluginCheckinTakesOverTheVendors (a qoder plugin without the hook leaves magpie's Qoder check-in its account, with it none; PluginCheckinOn's fallbacks) pass -race -count=10, and the latter fails with pluginChecksIn stubbed false; TestTUIChecksPluginsIn passes -race -count=20. plugin-checkin.test.cjs (the card's row with the captcha said, Auto check-in posting {provider, on}, no page move; the Settings Plugins tab at 440px with the row fitting, no sideways scroll, On posting) passes on Chromium and WebKit in en, zh, ja and de; usage-checkin, i18n-keys, gui-ja, gui-de and settings-* pass. go test -tags nogui ./internal/..., go vet, gofmt -l, linux (nogui) and windows builds pass. Not tried with a real plugin or account: no published plugin has the hook yet. docs/subsystems/provider-plugins.md (the hook's contract) and docs/reference.md say it. |