8576e3cc3370ce761ffade4e07b3c142d75c4dd2
150
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
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.
|
||
|
|
2b5f19fc73 |
gateway: a request whose tool results another request already answered (Claude Code's fork sub-agents and their lead, a turn sent again) gets a Claude Code run of its own instead of a 409 "the agent's turn is already being resumed" (ylorn on Discord)
A Claude Code fork sub-agent's first request is its lead's conversation
with a result for each of the lead's in-flight calls ("Fork started —
processing in background") and its directive after. The lead's own resume
and every fork it started therefore match the same run at once; the first
to claim it won and the rest got a terminal 409 that Claude Code does not
retry, so the forks died. The 409 dates from
|
||
|
|
24bef0f9be |
gateway: a streamed request waiting for its key's slot (Concurrency) or for room in its minute (Requests per minute) has its agent sent the stream's 200 and SSE keepalives after 15s, as a held try's stream is, instead of nothing until it is sent (coeo91 on Discord: WorkBuddy, Trae and Qoder said it timed out)
serve's try took its lane slot and waited for the minute ( |
||
|
|
d9caa3dd13 |
library: Hermes Agent in a WSL distro takes skills and MCP servers there, and so does every other WSL agent that keeps them in its own folder (omp, OpenCode, MiMo Code, Droid, Grok Build, Command Code, AtomCode, Qoder, MiniMax Code, fx, dsh) (#1323, Kevinzz224)
wslTargetOf knew only Claude Code, Codex, Pi and OmO, so hermes@wsl:Ubuntu
was never a Library target and a skill meant for it could only go to
Windows' ~/.hermes. An agent whose places are all its own folder (a.Dir,
or beside a.Path) now gets the target targetOf makes on this machine, at
the distro's home the agent was found with: copies for skills, its MCP
file marked as WSL's (a Windows program is refused, magpie's own image
server goes by its WSL path), and the skills it reads besides its own are
the same distro's agents'. Agents whose places depend on this machine's
variables or home (Kimi Code, Crush, Cline, CodeBuddy, Goose) stay out.
Run: TestWSLOwnFolderTargets fails without the change (hermes@wsl,
omp@wsl, droid@wsl, opencode@wsl aren't targets; no copy of the skill),
passes with it. go test -tags nogui ./internal/library ./internal/agent
under a temp HOME on macOS (library ok; agent's TestFolderVarsAreListed is
red on main since
|
||
|
|
10578ce683 |
providers: the add sheet lists Partners first, above Subscriptions, under a heading that says they sponsor magpie; who they are comes from usemagpie.ai/api/partners (site/partners.js), so a paid listing changes with a deploy, not a release (yetone)
The app fetches the list behind the call every 6 hours (10 minutes after a failure), never waiting, and keeps it in the cache folder. Each entry is checked: its own id, never a built-in preset's; a name and an endpoint; https only; at most 12; no decision API, no-key or user endpoint. Its logo goes through FetchIcon. A partner is a preset to the rest of magpie, and one that has left still is to the providers added from it, unsponsored. The window lists a partner in the languages it names only, with its tagline in the page's language; `magpie presets` and the TUI list them first too. MAGPIE_PARTNERS moves the list, off lists none. Run: go test -tags nogui -race ./internal/provider ./internal/gui ./internal/tui . (ok); TestPartners* -race -count=3; node --test internal/gui/tests/partners.test.cjs add-sheet gui-ja gui-de i18n-keys in Chromium and WebKit (en/zh/ja/de, 900 and 440px); node --test site/worker.test.mjs site/install.test.mjs; the built binary in a sandbox HOME against a local feed: listed, an http entry left out, listed from the cache with the site down, `provider add` from it. |
||
|
|
8db24fd62f |
gui: Routing folds three or more of a provider's keys for one model into one row that shows the count and their state, and opens in place to each key (#1319, tkhs101)
53 agnes-3.0-flash keys were 53 near-identical stage rows and 53 rows under Accounts and keys. Now three or more keys of one provider standing together for one model (same fixed effort, fallback and group-in-group heading) fold into one stage row: "Agnes 53 keys agnes-3.0-flash", then the lit key and what it does, "51 available · 2 resting". A request on a folded key flies to the fold; a key failing and the next answering show there (its 429 tag too). A click, Enter or Space opens it in place: each key keeps its own row, state and wire, drawn from the fold. Accounts and keys folds the provider's keys into one row with their summed tally and models, and opens the same way. Open folds are kept per reader in localStorage magpie.routingKeysOpen. Two keys stay two rows; one key is as before. Display only: the gateway's order and choice don't change. New strings in zh, zh-TW, ja and de. docs/subsystems/gui-shell.md says how. routing-account-order's relay has three keys, so it now opens their fold to read their order. Run: routing-keys-fold.test.cjs (32: folded, open/persist/Enter, live updates folded and open, Accounts and keys, 440px; Chromium and WebKit; en, zh, ja, de) — fails on main (no fold). All routing-*.test.cjs, panel-routing, click-scroll, fold-show-grip, gui-ja, gui-de, gui-zh-tw, i18n-keys: 409 pass. Every other test that opens Routing (29 files, 152 tests) passes but purpose-filter's WebKit "returning focus does not scroll the page", which fails the same alone on origin/main (off by 1px). go vet -tags nogui ./internal/gui (darwin, GOOS=linux, GOOS=windows). |
||
|
|
137ebee82f |
provider: a remote magpie's cards carry what was read behind the requests — the account switching's, the Codex warm-up's and routing's readings of each account (LoginUsage), routing's of each key (KeyAllowance) — and after a restart the readings kept in quotas.json, not only what the Usage page last read (#1313, jorben)
A magpie serving only other magpies (`magpie serve` in a container) has nobody on its Usage page, so GET /v1/magpie/quotas/cards, answered by CachedCards from the page's caches (subscriptionUsageCache, planQuotaCache, keyBalanceCache), said "nothing read on that magpie yet" while its Codex account's usage was read every few minutes behind the requests: those readings land in loginUsageCache (and a key's in keyAllowances), which CachedCards never looked at; after a restart even a page-read card was gone until the page was opened, though quotas.json held it.
CachedCards still asks nobody. It now lays each account's latest LoginUsage reading over its card when it is newer by readSeq (perLogin's cards are that same reading, so the page's own later read stands), adds a card for an account read that way and never on the page, named and drawn as the page names it (accountCard: perLogin now takes that face), and falls back to the reading kept on disk, dated (AsOf). Keys the same way: routing's KeyAllowance reading now keeps what was read with the windows (plan, balance, parts, readSeq) and is laid over the key's plan or balance card, or makes one; with no card read since start, the plan and balance cards kept in quotas.json are told. Cursor (known by asking its CLI), Grok (read by home) and a Kiro key keep the page's cards only. The local Usage page is unchanged.
Not fixed here: the quota history (/v1/magpie/quotas/history) is still written only by the page's reads.
Red before: with CachedCards as on origin/main and everything else in place, TestRemoteMagpieShowsBackgroundReadings (gateway, through lanGuard(New().Handler()) and a remote-magpie provider: CodexUsedUp, then RemoteCards) fails with the reporter's card ("nothing read on that magpie yet; refresh to have it read"), and TestCachedCardsTellBackgroundReadings, TestCachedCardsKeepALaterPageRead and TestCachedCardsTellKeyReadings fail; all pass with it. ForgetKeptCardsForTest is called from the gateway's fresh() so TestRemoteMagpieQuotas's "nothing read" holds under -count.
Run (Docker golang:1.26.8, the Mac's syspolicyd pegged): go test -tags nogui ./internal/... (internal/agent as uid 1000), go test -race ./internal/provider ./internal/gateway, go test -count=2 -run TestRemoteMagpie ./internal/gateway; go vet -tags nogui for darwin, linux and windows on internal/provider and internal/gateway; gofmt -l. No app.js change.
|
||
|
|
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 |
||
|
|
b56476b970 |
fix(provider): omit Anthropic version headers from OpenAI probes (#1312)
Reported by mac0923: Agent Space rejects Responses probes with anthropic-version as 400 invalid_headers. Add the default version only for Anthropic Messages while preserving custom headers through Sign. TestProviderProbesUseProtocolHeaders fails before the fix across endpoint tests, model tests and both detection paths, then passes with the fix, including 20 race-detector runs. go vet and macOS, Linux and Windows builds pass. |
||
|
|
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
|
||
|
|
0470411122 |
providers: a provider has a Requests per minute limit beside Concurrency: each key or account sends the vendor at most that many requests in any rolling minute, one more waits for room (coeo91 on Discord: OpenRouter's free models take 20 a minute, which Concurrency 1 can't keep to)
Set in the provider editor (under Concurrency; empty or 0 is no limit) or with `magpie provider rpm <id> [n|off]`; kept per provider as MaxRPM, a subscription's with its picks. Over the limit the gateway waits until the oldest request of the minute is a minute old, in the order they came, after the lane's slot and right before sending. A wait longer than 2 minutes (or the provider's Queue wait, when shorter) is turned away at once with a 429 and a Retry-After of when the minute has room; nothing rests for it. An agent gone while waiting gives its place back and is never sent. Every request that reaches the vendor counts: each try of the forward loop (retries, the next key, fallbacks) waits in serve, and requests a try makes on the way plus the gateway's own (count_tokens, System One decisions, embeddings, drawing/video, Kimi search) are counted by an rpmTransport on s.client. count_tokens never waits: a key with no room is skipped and the local estimate answers. A key or account with no room in its minute gives way to a ready one of the provider or the group, as a full lane does (laneMate). No wait happens under rpms.mu. Not counted: a plugin account's own extra requests (its own transport; its tries are). Verified (Go tests on Linux in Docker, golang:1.26.8, since new binaries hang at launch on this Mac while syspolicyd is pegged): - internal/gateway rpm_test.go through the real server stack (lanGuard(s.Handler())): limit 2, third request reaches the vendor a window after the first, all 200; past the bound a 429 with Retry-After and no rest; a cancelled waiter is never sent and gives its place back; a 503 retry and a count_tokens count, and a count with no room is estimated; a group member with no room gives way. With RPMLimit forced to 0, those five gateway tests fail. -race -count=5 passes. - internal/provider TestSetRPM (clamp, subscription picks reach each account), TestRPMCmd, internal/gui TestRPMSave. - go test -tags nogui ./... on Linux: all pass except internal/agent's TestAside* write-refusal tests, which fail only as root in Docker and pass as uid 1000. - go vet -tags nogui for linux, windows and darwin; gofmt -l clean. - GUI: new provider-rpm.test.cjs (en/zh/ja/de, 360 and 440px, Chromium and WebKit; 32 pass, and all 16 Chromium cases fail on the old app.js), plus gui-ja, gui-de, gui-zh-tw, i18n-keys, provider-concurrency, lane-limit, provider-price-rate and clinepass-pin in both engines. Not tried against the real OpenRouter. |
||
|
|
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.
|
||
|
|
56d1665251 |
library: a skill an agent has of its own is removed from every agent that has it, from its row under "In your agents" (trash) or with d twice in the TUI, without bringing it into the library first (#1303, sxwedo)
Until now the only action on such a row was Bring in: removing it meant bringing it in and then removing it from the library. RemoveFoundSkill (skills/remove-found) takes out of each agent that has it its folder, or its copy of the very same files, and its links; its entry in the shared ~/.agents/skills, which agents read too, and a folder of it in the library's own folder go as well. Nothing is deleted: folders go to the backups (setAside), links are taken away, and a folder elsewhere that the agents only linked to stays where it is. Another skill by that name (Others) is a different skill and stays. The page asks first and says which agents it leaves and what stays. TestRemoveFoundSkill (library), TestLibraryPage's new part (TUI) and remove-found-skill.test.cjs (GUI) fail without the change. The GUI test fails 4 of 4 with library.js from before it, and passes in Chromium and WebKit, in en, zh, ja and de, and at 440px wide. Run: go test -tags nogui ./internal/library ./internal/tui ./internal/gui, and go vet for linux, windows and darwin with -tags nogui, in Docker (golang:1.26.8, a temporary HOME). The Mac's syspolicyd held test binaries at launch. On the Mac, in Chromium and WebKit: remove-found-skill, gui-ja, gui-de, i18n-keys, import-all-skills, own-skill, shared-skills, library-own-folder, library-items, remove-all-skills, library-desktop-skills, skill-name-many-agents and mobile-web, 133 of 133. In one earlier run of that set at load ~40, mobile-web's WebKit "idle scripts cannot scroll" failed once. It passed 3 of 3 alone with this change, and passed in the full rerun. Not tried: the reporter's own agents, and a WSL agent's folder. |
||
|
|
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 | ||
|
|
78d90571cf |
agent: each named omp profile is an agent of its own, on Windows and in each WSL distro (#1233)
* agent: each named omp profile is an agent of its own, on Windows and in each WSL distro (#1187) The Agents tab's omp row only ever wrote the one folder ompDir picks, so a profile started with omp --profile kept the model it had. ~/.omp/profiles/* is now listed per location: omp#<name> on Windows, omp#<name>@wsl:<distro> in a distro, each with the omp row's model, role and thinking controls writing that profile's own config.yml and models.yml. The profile the default row already points at (OMP_PROFILE) is not listed twice, and a stopped distro is not started to list its profiles: the names come from the probe and stay in wsl.json. * gui: an omp profile's id is encoded in the Agents page's requests (#1187) omp#<name> went into /api/agents/connect and the rest raw, so a browser read #work as the fragment and connected, disconnected or previewed the default omp row instead. api() now encodes each path segment, which is what agent-models and agents/cli already did for the id alone. * agent, gui: an omp profile name is one omp takes, and only its # is encoded (#1187) api() encoded every character of a path, so a query's ? = & became part of the path and Usage, quotas and Sessions 404ed. Only # is encoded now, which is what omp#<name> needs; a query stays a query. ompProfileOK also refuses the Windows device names omp refuses on every platform (aux, nul, com1, lpt9.x, con.work), and work. is checked through that rule rather than by making the folder, which Windows would turn into work. |
||
|
|
9275abcddb |
gateway: Codex's compaction on a CC Switch relay table that magpie points at /v1 completes, with a summary magpie makes, instead of failing "remote compaction v2 expected exactly one compaction output item, got 0 from 1 output items" (#1301, ylacmilan)
Codex compacts remotely (compaction v2: a compaction_trigger item on /responses, and exactly one compaction item back) for any provider table named "OpenAI" (codex-rs ProviderCapabilities::from_config, is_openai), and CC Switch names its relay tables so. While a magpie model is on, magpie points those tables at /v1 (takeTables). /v1/responses relayed the trigger as it came; the relay answered with a message, and Codex counted 0 compaction items out of 1. Only CodexPath turned the trigger into a summary request (codexInput, codexCompact). handle now does the same for a Responses request on /v1 that carries a compaction_trigger: the model summarises, and the summary goes back as magpie's compaction item. A later turn's magpie1: compaction item reaches the model as the summary's text, not as an item the relay can't open. TestCodexCompactionTriggerOnV1 fails without the change (one output_item.done of type "message", the relay's reply, where Codex needs one "compaction") and passes with it. Real use: codex-cli 0.160.1 in a sandbox CODEX_HOME with [model_providers.custom] name = "OpenAI", base_url = magpie's /v1 (built from this tree, `magpie serve` in a sandbox HOME on 127.0.0.1:3592), model token/gpt-6.1-sol on a fake Responses relay that reports 90K tokens and model_auto_compact_token_limit = 1000. Old build: `codex exec resume` printed the reporter's exact error, and the relay got the compaction_trigger. New build: "context compacted", the relay got Codex's compaction prompt, and the next two turns went on with the summary as a message. go test ./internal/gateway, go vet (darwin, GOOS=linux, GOOS=windows) pass. Not tried: the reporter's own relay and CC Switch version. |
||
|
|
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.
|
||
|
|
e387c5a2ed |
library: Alma takes the Library's MCP servers, in ~/.config/alma/mcp.json as its own MCP settings write them (#1292)
Alma 0.4.164 (read from a copy of its app.asar, out/main/index.js) reads
its servers from <home>/.config/alma/mcp.json on every system, as
{mcpServers:{name:{command,args,env} | {url,headers,transport?}}}: a
server with a url is streamable HTTP falling back to SSE, and SSE only
when transport is "sse". It has no ${VAR} expansion, so a server naming
a variable is refused for Alma as for Kimi or Devin. Alma reads the file
as it starts and when its MCP settings refresh; there is no watcher.
The user's own servers (the real file's computer-use entry with
ELECTRON_RUN_AS_NODE) are kept: TestAgentMCP's alma case writes one
first. Without the change TestAgentMCP/alma fails ("alma has no MCP
target") and TestTakesRefusesAlma refuses mcp.
Run: HOME=<temp> go test -tags nogui ./internal/library ./internal/agent;
go vet for darwin, linux and windows. Not tried with a running Alma.
|
||
|
|
d226f87e4c |
gui: on KDE Plasma's Wayland session the quick panel has no KWin title bar or frame, as the main window has none (#1283)
GTK 3, which the Linux builds link, announces server-side decorations over org_kde_kwin_server_decoration for every window that isn't client-decorated, an undecorated (frameless) one too (gtkwindow.c realize: announce_ssd), and KWin then draws its title bar on it. The main window escapes it through plainTitlebar's hidden titlebar widget. The panel now announces client-side decorations after each realize (ownFrame: gdk_wayland_window_announce_csd), at start and when lightweight mode makes it again. GTK 4 already asks for none, X11 and compositors without the protocol never see it. Run: on real KWin 6.3.6 Wayland (kwin_wayland --virtual with Breeze, Debian trixie arm64 container, -tags gtk3 test binary): WAYLAND_DEBUG shows request_mode(1) and KWin answers mode(1); a KWin script reads the panel's frame 440x520 = its client 440x520. With ownFrame skipped: request_mode(2), frame 442x557 over client 440x520 (KWin's title bar and borders, the reporter's picture). go vet -tags gtk3 ./internal/gui ok there; macOS go vet/go build ok; GOOS=linux/windows go vet -tags nogui ./internal/gui ok. Not tried: the reporter's Manjaro Plasma session, a GTK 4 build, GNOME/Hyprland (no KDE protocol there, so unchanged). |
||
|
|
23cd1bb609 |
gui: closing the quick panel from its title bar (or Alt+F4, a window manager's close key) hides it as Escape does, so the tray icon opens it again; it was closed for good and the icon did nothing until a restart (#1283, KDE Wayland)
The system's close reaches Wails as WindowClosing, which destroyed the panel while h.panel and the tray still held it. makePanel now hooks WindowClosing: the panel cancels and hides; one lightweight mode let go (h.panel no longer it) still closes. Run: TestPanelCloseHidesIt (AppKit, new) passes; with the hook's check forced to let the close through it fails "panel close: the panel closed for good; the tray icon can't open it again". On real KWin 6.3.6 Wayland (kwin_wayland --virtual, Debian trixie container, GTK 3 build) a KWin script closeWindow() on the panel (what its title bar X does): with the hook alive=true and a second Show is visible; without it alive=false and Show shows nothing. go test ./internal/gui ok (darwin, GUI tag); go vet, go build ok; GOOS=linux/windows go vet -tags nogui ./internal/gui ok, GOOS=windows go vet ./internal/gui ok. go test -tags nogui ./...: all ok but internal/agent (11m timeout) and internal/plugin TestHostRunsAsMagpieBun under load average ~110; neither imports internal/gui, and TestHostRunsAsMagpieBun passes alone. Not tried: the reporter's own Manjaro Plasma session; Windows' Alt+F4 on the box. |
||
|
|
b66481a953 | provider, gateway: Volcengine plans expose their own models by kind (#1265) | ||
|
|
c28a819a8e |
library: an agent that can't be given instructions, skills or MCP servers says where they go instead and lists the agents here that take them: OpenChamber has OpenCode's (give them to opencode), Claude Desktop keeps its instructions in its own settings and its Code tab reads Claude Code's (MOMO on Discord)
OpenChamber's server reads the global AGENTS.md, skills folder and opencode.json from OpenCode's config folder (openchamber/openchamber packages/web/server/lib/opencode/shared.js at 866dffd), so no path of its own is added; a second target on the same files would write them twice. Verified: TestTakesSaysWhereInstead fails on the old Takes with "Claude Desktop has no user-wide place for instructions that magpie knows of"; internal/library passes in a sandbox HOME. |
||
|
|
7712ef8e7c |
gateway: a routing group is served by its name too, so a client the user typed a renamed group's name into (ZCode) reaches it rather than 404 "magpie knows no model"; magpie group set <id> name=… says the id stays and what agents ask for (MOMO on Discord)
provider.GroupFor tries the group's id and the model's spellings first, as before, then the one group whose name (by its slug) is the id asked. A name two groups share names neither, and a model a provider serves by that id stays the provider's. Verified: TestGroupAskedByItsName fails on the old GroupFor with 404 "magpie knows no model \"DS Flash\""; TestGroupRenameSaysTheIDStays fails without the message. internal/provider and internal/gateway pass in a sandbox HOME. |
||
|
|
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) | ||
|
|
d1fca72f69 |
library: the Library's MCP servers go to WorkBuddy and CodeBuddy Code (#1266)
WorkBuddy had skills only, and CodeBuddy Code had no library target at all,
so their MCP chips could not be switched on.
WorkBuddy 5.5.6 (app.asar) reads ConnectorService.customMcpConfigPath:
$WORKBUDDY_CONFIG_DIR, then $CODEBUDDY_CONFIG_DIR, then ~/.workbuddy, plus
mcp.json, as {"mcpServers": {...}} in Claude's shape. It connects a server
only once the user has switched it on in its MCP settings
(mcp-approvals.json, module.app-server.js). magpie doesn't write that
approval. Instead, the servers page tells the user to switch it on once,
in en/zh/zh-TW/ja/de.
CodeBuddy Code 2.162.0 (PathUtils.resolveMcpFilePath) reads the first of
<dir>/.mcp.json, <dir>/mcp.json and <base>/.codebuddy.json that exists.
<dir> is $CODEBUDDY_CONFIG_DIR or ~/.codebuddy. The file holds mcpServers
in Claude's shape, and ${NAME} is expanded (McpConfigEnvExpandService).
User scope needs no approval. Skills go in <dir>/skills.
Verified: the real CodeBuddy Code 2.162.0 CLI, in a sandbox HOME, read
SaveServer's file back with `mcp get` and `mcp list`. That covered stdio
with ${MY_TOKEN} expanded, http and sse, and the user's own entry and
disabledMcpServers were kept. WorkBuddy.app was not run; its side is
from its code.
Tests: TestAgentMCPFiles (4 new cases) and TestCodeBuddyMCPFile fail
without targets.go. library-workbuddy-trust fails without library.js.
Run: go test ./internal/library ./internal/gui; go vet for darwin, linux
and windows; in Chromium and WebKit: gui-ja, gui-de, gui-zh-tw, i18n-keys,
click-scroll and all library-* GUI tests.
|
||
|
|
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 | ||
|
|
b6d88ce66e |
provider, gui: each of a provider's keys can go through a proxy of its own, as a subscription's accounts can (Beyfish_Wang on X: 不同 key 走不同的代理)
A provider of keys kept one proxy for all of them: Save dropped
AccountProxies for it. Now AccountProxies holds a key's proxy by its
KeyID, ProxyChoice reads it for the key a try uses (WithKey), and Save
keeps only the keys the provider still holds. The key editor's Proxy
row has a line per key (Provider's proxy | Direct | Custom), named as
the Accounts list names it, when the provider has two or more.
Run:
- go test ./internal/provider ./internal/gui ./internal/gateway: ok;
TestKeyProxy fails without the provider change ("kept map[]").
- account-proxy.test.cjs, Chromium + WebKit, en + zh: 16/16; the new
key cases fail without the app.js change. Also at 440px: 16/16.
- gui-ja, gui-de, i18n-keys, account-models, provider-many-keys,
provider-typed, provider-proxy: all pass.
- Built binary, `magpie serve` in a sandbox HOME, three keys, four
local proxies: k-1 (429) through its own proxy, k-2 (429) through its
own, k-3 through the provider's; the global proxy got nothing.
|
||
|
|
f00ed2901c |
routing: filter by several purposes with checkboxes (#1247)
* routing: filter by several purposes with checkboxes Ticking a purpose immediately shows the union of the selected purposes while keeping the menu open. All purposes, Clear filter and removing the last selection restore every purpose. Counts, the story and history follow the same list. Live menus ignore renderer-induced scrolling while deliberate outside scrolling still dismisses them. Update the GUI shell reference and browser coverage for multi-purpose counts, keyboard toggling, history, aliases and narrow windows in five languages. Verified 115 isolated Playwright checks in Chromium and WebKit, JavaScript syntax and git diff --check. The new checkbox case fails against the old assets with 0 checkbox choices instead of 5; restoring the old menu scroll handler separately detaches the menu during selection in WebKit. * routing: All purposes returns keyboard focus to its button Address the keyboard-focus regression reported in PR #1247. Choosing All purposes closes its menu and focuses the purpose button without scrolling. Unticking the last purpose keeps the live menu and its checkbox focused. The expanded checkbox test fails without the focus restoration in Chromium and WebKit: All purposes returns keyboard focus to the purpose button (false !== true). With the fix, 162 isolated Playwright checks pass across both engines, including five locales, narrow widths, purpose state, history, request/session grouping, Usage and scroll/menu regressions. JavaScript syntax and git diff --check pass. The full GUI/Go suites and the packaged desktop app were not run. |
||
|
|
01e66e0f4a |
gateway: a reply a Claude subscription account answered carries its anthropic-ratelimit-unified-5h/7d headers, so a Claude Code signed in to claude.ai shows the 5-hour and weekly limits in its status line (claude-hud, #1257)
Claude Code tells the allowance in its rate_limit_event, not as headers, so magpie says what it keeps of the account again as Anthropic does: a share 0-1 and a Unix-seconds reset per window, set as the request comes in and again as the reply begins, after the turn's own event. Claude Code 2.1.293 reads them only when it is signed in to claude.ai (the sign-in field's claudeai); with magpie's key it ignores them. Run: go test -tags nogui ./... (temp HOME); the two new tests fail without limitHeaders; GOOS=linux/windows go vet of gateway and provider; real Claude Code 2.1.293 in a sandbox HOME against a fake server sending these headers: its statusline stdin had rate_limits.five_hour/seven_day with used_percentage 42/17 when OAuth-signed in, none with ANTHROPIC_AUTH_TOKEN. Not run: a real Claude account through a running magpie. |
||
|
|
db0feb3615 |
library, gui: a remote MCP server's headers and a command's env can name ${NAME}: the library keeps the placeholder and each agent gets its own syntax, so the token is never written into an agent's settings (#1250)
The library keeps `${NAME}` as typed. refsOf (internal/library/envref.go)
gives each MCP format the agent's own syntax, with the source beside each:
${NAME} for Claude Code, Droid, Grok, Gemini, Crush, Copilot, Pi, OmO, omp,
Qoder, AtomCode, Hermes and Command Code; {env:NAME} for OpenCode and MiMo;
${env:NAME} for Cursor; Codex's bearer_token_env_var, env_http_headers and
env_vars; Goose's ${NAME} plus env_keys. An agent with no syntax (Claude
Desktop, Antigravity, Kimi, Cline, ZCode, dsh, Devin, Pi with
pi-mcp-extension) is never given such a server: supports() refuses it with
the reason, an entry magpie wrote before is taken out, and its chip is
greyed with the reason on the row and in the editor. Codex's and Goose's
extra keys are magpie's only while the server names a variable, so the
user's own env_vars / env_keys stay. The editor says how to write one under
Headers and Environment; a check whose variable isn't set where magpie runs
says which one ("GH_TOKEN not set").
Verified:
- go test -tags nogui ./... (sandbox HOME): all ok.
- New TestEnvRefs* (per-agent encode/read-back, left-out agents, library
keeps the placeholder, user values kept, health check novar) fail with
the behaviour turned off (refsOf -> ${NAME} for all, no refsProblem /
refKeys, no expansion).
- library-env-refs.test.cjs (Chromium + WebKit, en/zh/ja/de at 440px)
fails on the old library.js (4/4 subtests); gui-ja, gui-de, i18n-keys,
library-servers-bulk, library-all, by-agent, project-servers,
library-mcp-health, library-null-agents, library-items: 109/109 pass.
- Real: magpie built from this tree synced a library server with
Authorization: Bearer ${FAKE_MCP_TOKEN} and X-Api-Key: ${FAKE_KEY} into a
sandbox HOME, then against a fake MCP on 127.0.0.1:3561 that answers only
the right bearer: Claude Code 2.1.293 `mcp list` Connected, Gemini CLI
0.62.0 Connected, Codex 0.160.1 app-server listed the tool (authStatus
bearerToken), cursor-agent sent both headers expanded; no agent file held
the token. Antigravity got the Problem and no entry. Not run for real:
OpenCode (its mcp list goes through the user's background service),
Goose, Crush, Copilot and the rest; their syntax is from their source.
- go vet (darwin, GOOS=linux, GOOS=windows) for internal/library and
internal/gui.
|
||
|
|
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.
|
||
|
|
69b6772794 |
provider: Qoder check-ins follow the campaign window, so a campaign open past Beijing midnight is claimed and the next one isn't missed (#1173)
Qoder and Qoder CN campaigns run on [startAt, endAt) windows that cross Beijing midnight, so a campaign claimed yesterday could stand in for the whole new day and the next campaign was never claimed. - A campaign is active by its upstream window, so one still open after Beijing midnight can be claimed. - A saved result records the campaign's end in `until`, and is reused only on the same Beijing day, within 30 minutes and before that end; after that the list is read again (about 48 list reads per account per day, `inactive` included). - Manual check-in always verifies upstream; failures and older records without `until` keep the old retry behaviour. - Only Qoder sets `recheck`; WorkBuddy, Trae CN and MiniMax keep their once-a-day settlement. |
||
|
|
3aeb7d1bf8 |
gateway: a model a key's plan doesn't serve rests that model alone, not the key (#1235)
SenseNova answers a Token Plan key asked for deepseek-v4.1-flash with
403 "model is not available in the current token plan" while the same
key serves deepseek-v4-flash. That refusal was a plain failure: it
rested the whole key, its backoff counted per key and doubling up to
10 minutes, so an agent that kept asking for the model left every key
of the provider resting for every model (Sun1090: both keys "失败 · 9
分钟后恢复").
A 400/403/404/422 whose body says the model isn't served
(unservedWords), with no quota, credit, rate-limit or channel words,
now rests only that key's or account's model (modelRefused, keyed by
restID, which is now restKey/model for every candidate), its backoff
counted by the model and cleared when the model answers. The key's
other models stay first in order, and its row shows no rest.
TestModelNotInPlanRestsTheModelNotTheKey (two keys, four requests for
the refused model, SenseNova's error shape) fails without the change:
"key One rests for a model its plan lacks: {Why:other Status:403 ...
By:backoff Failures:3}" and "deepseek-v4.1-flash doesn't rest". With
it: go test -tags nogui ./... under a temp HOME passes; -race -count=5
over TestModelNotInPlan|TestSeveralKeys|TestFallback|TestRouting|
TestPool|TestGroup passes; go vet (darwin, GOOS=linux, GOOS=windows,
nogui) passes. Not tried against a real SenseNova key: the body's
"code" field in the fixture is a guess; the message is the reporter's.
|
||
|
|
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. |
||
|
|
2d9db1b60d |
edit: malformed JSON configs stay untouched when agents connect (#1242)
Validate the complete comment-stripped document before object and array edits, including previews. Regression tests fail on the old implementation because edits and VS Code Connect succeed and alter malformed files. Targeted edit and VS Code tests pass in an isolated HOME. |
||
|
|
814dca9390 |
gateway: the ChatGPT backend's 502 "response protection is unavailable" is the request's, not the account's: asked once, no account rests, and a magpie-model compaction refused so is asked again as text (vs on Discord)
A long Codex conversation's compaction refused so went to every ChatGPT account in turn, each rested on a growing backoff, then the last was asked twice more: 5 upstream tries per Codex request with 3 accounts, times Codex's own retries. Now the provider's other accounts and models are not asked it, nor the same one after a moment; another vendor left in a group is asked once. A reply broken off with it rests nobody either. codexCompact falls back to plainCompact on it, as vs's control experiment (the same history as text) was answered. TestProtectionRefusalAskedOnceNobodyRests fails on the old code with "asked 5 times ([acct-1 acct-3 acct-2 acct-2 acct-2]), not once" and two accounts resting; TestProtectionRefusedCompactionAsText with status 502. The error body's exact bytes are not the reporter's; no real ChatGPT account was tried. |
||
|
|
ad3b7fe5c2 | agents: keep Air provider fields on sync and model picks (#1239) | ||
|
|
70ec514872 |
provider: a Copilot editor token GitHub refuses gives way to the Copilot CLI's sign-in of the same account, and a sign-in from magpie of that account is kept (#1238, eightHundreds)
A stale oauth_token for octocat left in ~/.config/github-copilot/apps.json was read before everything else, so its 401 Bad credentials left Copilot with no models although the Copilot CLI was signed in to octocat and worked; signing in again from magpie didn't help, since the new token was let go as the editors' own account. - copilotToken remembers an editor token whose trade GitHub refuses (401 or 403; tried again after an hour, or as soon as one trade works). - copilotApp.session then uses the CLI's sign-in of the same login on the same host, sent as the CLI sends it, and copilotLogin reads that sign-in for the account from then on. A CLI signed in to another account is never used in its place; the error names the editors' file, and that the CLI is another account. - The Usage card (loginQuota) reads the CLI's sign-in the same way when the editor token gets 401. - The device sign-in asks GitHub about the editors' token of the same account first (copilotProbeEditor); refused, addCopilotLogin keeps magpie's token, standing for the account. With a working editor token it is let go as before. - TestMain stubs copilotCLISecret, so no test reads the runner's keychain. Tests: copilot_stale_editor_test.go (fake exchange, user and API servers, temp HOME). Without the fix TestCopilotStaleEditorFallsBackToCLI and TestCopilotStaleEditorNotAnotherAccount fail "Copilot is signed out (Bad credentials); sign in to Copilot again", TestCopilotSignInKeptOverStaleEditor lists octocat with the editors' gho_x, TestCopilotStaleEditorUsageReadsCLI reads "Unauthorized". Not tried with a real stale apps.json or a real Copilot CLI login. |
||
|
|
1efa4c642f |
gateway: a Codex lead's turn carrying its subagent's sealed reply goes back to the Responses provider that answered the lead, instead of being refused as a subagent task "sealed by the ChatGPT backend" (#1237)
SuperrNaruto (#1237), and PennyVibe on #1109 with v0.1.1108: Codex with the lead and its subagents all on one custom Responses provider serving gpt-6-astra. #1109's fix sends the subagent's sealed task back to that provider, and the subagents ran. But when a subagent answers the lead with send_message, Codex hands the lead an agent_message to /root whose payload the provider sealed, and hasSealedAgentMessage marks the lead's next turn as a sealed task. The lead has no parent thread, so leadProvider found nobody, sealedReader accepted only a ChatGPT account, and the lead was refused before anyone was asked with "This subagent's task was sealed by the ChatGPT backend that answered its lead ... is neither". The main conversation stopped while the subagents finished. The providers that can read a sealed message are now those that answered the lead (the parent thread, as before) and the one that answered the request's own conversation (leadProvider of its conversationID): a subagent sent a task sealed by the lead's provider had to be on that provider, so its reply was sealed there too. A lead moved to another provider, one magpie never answered, and a subagent whose lead a ChatGPT account answered are still refused before anyone is asked, with the same messages. Verified: TestSealedReplyGoesBackToTheProviderThatAnsweredTheLead (lead, subagent, then the lead with the subagent's sealed FINAL_ANSWER reply, all on third/gpt-6-astra with Codex's web_search tool) fails on origin/main with the reporter's exact 400 and passes with the change; the reply's ciphertext reaches the provider as it came. TestSealedReplyRefusedWhereTheLeadWasNotAnswered and TestSealedTaskFromChatGPTLeadStillRefused (lead on gpt-5.5 through the ChatGPT sign-in, subagent on the custom provider) pass on both. go test -tags nogui ./internal/gateway passes; the sealed tests pass under -race -count=5; go vet -tags nogui ./internal/gateway passes for darwin, linux and windows. Not tried with real Codex and a real relay of the ChatGPT backend: the reply's item is built from the "Message Type: FINAL_ANSWER / Task name / Sender / Payload" layout in the codex 0.160.1 binary and PennyVibe's description, not from a captured request. |
||
|
|
40c5b92a93 |
gateway: structured output crosses every translation: a Responses text.format or a Chat response_format reaches a Chat, Responses, Messages or Gemini upstream in its own words (frostming on Discord: responses.parse through magpie to OpenRouter came back as plain text)
The IR held a schema only from Anthropic's output_config.format, and only Messages (and Claude Code's --json-schema) were asked for one. A Responses request to a provider magpie speaks Chat or Messages to (OpenRouter has no Responses URL) lost text.format; a Chat request translated to Responses or Messages lost response_format; an Anthropic schema translated to Chat or Responses was lost too. Format (format.go) now carries json_schema (name, description, schema, strict) or json_object from Responses, Chat, Anthropic and Gemini, and each builder asks for it in its upstream's field. Where a target can't say it (json_object on Messages or Claude Code; the vendor bridges Cursor, Devin, Kiro, Qoder, Command Code, Zed) it is told in the system prompt. An upstream answering 400/422 naming the field (DeepSeek's "This response_format type is unavailable now") is asked again with it told in words, and so from then on. Relayed requests are unchanged. |
||
|
|
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 (
|
||
|
|
84f37ac977 |
provider: a Copilot account renamed with its host keeps its proxy, models, cap and concurrency, moved from its bare login before the new name is written (#1220)
Before #1220 a bare Copilot login named one account (a same-named one
wrote over it), so the per-account settings in providers.json kept under
it are that account's. When an account is renamed login -> login@host
(an old enterprise record read by nameAlike, or the editors' own
enterprise account renamed in sideLogins), renameTo remembers the old
name (savedLogin.was, never written) and pins the account's stable id
from it. writeLogins then moves accountProxies, accountModels,
accountCaps and accountConcurrency of the copilot provider from the old
key to the new (moveAccountSettings), and writes the new name only once
that has worked (renameSettled):
- a setting the new name has already stays, and the old with it;
- an old name another account still bears is left to that account;
- providers.json that can't be read or written stops the move, and the
account is written under its old name, its settings with it, retried
a minute later (renameDue) rather than at every listing.
sideLogins writes a pending rename the first time copilot is listed,
on a copy so the listing keeps its order.
Not moved: usage history, quota history, usage-alert marks and the
GUI's expanded/brief card state (localStorage) - records and view
state, not settings. Gateway keys' account lists go by the stable
login id, which is kept.
Tests (each FAILs with moveAccountSettings stubbed to do nothing):
- TestCopilotRenameKeepsSettings: proxy, cap, models, concurrency under
"mona" are mona@a.ghe.com's after the upgrade; a github.com mona added
after has neither proxy nor cap.
- TestCopilotOwnGHERenameKeepsSettings: the editors' own GHE account
keeps cap and proxy.
- TestCopilotRenameWaitsForSettings: an unreadable providers.json is
left as it is and logins.json keeps "mona"; once readable, the
settings move and the new name is written.
- TestCopilotRenameKeepsNewNamesSetting.
Ran: rebased onto origin/main (
|