4eb90b69f434d512c0ed6484d40df221a2425de8
209
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
61e296688e |
gateway: Codex's model list keeps codex-auto-review when the user picked models on the ChatGPT account, so auto-review runs on it again, not on the conversation's model at low (#1460)
10086gg reported that Codex's auto-review through magpie almost never used codex-auto-review. It ran on the conversation's own model at low effort, which costs more. Cause: codexModels narrows the ChatGPT backend's list to the models the user picked on the codex provider (CodexNativePicked). Hidden entries (visibility "hide") never reach magpie's catalog (parseCodexModels), so no one can pick them, and the narrowing always dropped them. codex-auto-review is one of those hidden entries. Codex 0.162 picks the reviewer this way (codex-rs/ext/guardian-reviewer/src/model.rs, select_review_model): 1. the entry's auto_review_model_override; 2. else codex-auto-review, if the list has it; 3. else the conversation's model, at low effort. So once the list was narrowed, every review fell to step 3. On a relay model it was charged to the relay; on a ChatGPT model it ran on that model. OpenCodex had the same bug: lidge-jun/opencodex#6712 recorded direct ChatGPT login as codex-auto-review/low and their proxy as Sol/low. Fix: the narrowing keeps hidden entries. No picker shows them, so the user's picks still decide what Codex's /model lists. A user who wants reviews on a relay model can still set Codex's auto-review model (#938), and that setting still wins (step 1). Which account pays changes for a user with picks whose conversation runs on a relay model: auto-review moves from the relay to the ChatGPT account's codex-auto-review. That is what Codex does without magpie. OpenAI's help article "Using Codex with your ChatGPT plan" says: "When you're signed in with a ChatGPT account, auto-review safety checks are free and don't count toward your plan's usage limits." New test TestCodexModelListPicksKeepHidden. It fails on the old code: after the picks only gpt-6-sol is listed, with no codex-auto-review. docs/subsystems/gateway-routing.md says this. Run: - gofmt -l internal/gateway - go vet -tags nogui ./internal/gateway, on darwin, linux and windows - go test -tags nogui ./... with a temp HOME and pinned caches: green Not run: a real Codex signed in to ChatGPT through the gateway. The reviewer choice was read from the Codex source. |
||
|
|
259c71078c |
docs: gateway routing says reasoning state goes through same-protocol translation as the vendor wrote it (#1445, XYenon)
What changed: gateway-routing.md gains the #1445 contract (sealed parts, the new IR events, each vendor's case, no cross-API envelope) and no longer says a translated Responses reply never carries sealed reasoning. Run: read against the code in the three commits before this one. |
||
|
|
793af76242 |
gui: Codex's subagents setting, unset, says the lead picks the model and only falls back to the main model, not just "same as model" (willz on Discord)
willz set Codex's "subagents" to 同主模型 (same as model), and a subagent
still ran on another model. Codex 0.162 lets the spawn_agent call's own
`model` argument win over `[agents] default_subagent_model`
(core/src/agent/child_config.rs:
requested_model.or(agent_default_subagent_model)). The default only
applies to a spawn that names no model, and the spawn_agent tool lists
up to 5 models the lead may name. So "same as model" was false whenever
the lead named a model.
This was reproduced with the real codex-cli 0.162.0 in a sandbox
HOME/CODEX_HOME, against a fake Responses server on port 3683 that logs
each request's model and x-openai-subagent header. The lead ran on
gpt-6.1-sol in every case:
a. lead names astra, default unset -> subagent on gpt-6-astra
b. lead names none, default unset -> subagent on gpt-6.1-sol
c. lead names astra, default sol -> subagent on gpt-6-astra
d. lead names none, default astra -> subagent on gpt-6-astra
magpie already holds every subagent to one model with the "subagent
model" setting (the gateway's codexSubagentPick,
|
||
|
|
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. |
||
|
|
d7a1b02032 |
redact: more kinds of personal data, each with its own switch: SSNs, passports, IBANs, birthdays, MAC addresses and serial numbers on by default; home-folder names, user@host, public IPs and cloud buckets off (lc on Discord)
lc asked in #feedback (message 1558300896142696549) for magpie's masking to cover more of what identifies a person or a machine. Under Settings > Privacy > Mask personal data, each kind now has its own Off/On (settings.redactKinds, redact.Options.Kinds). What each one masks: - US SSNs: dashed numbers, except ones never issued (000, 666 and 9xx areas) and well-known examples. Undashed ones only next to "SSN" or "social security". - Passport numbers, next to passport, Reisepass, 护照 or パスポート. - IBANs: compact or in groups of four, with the country's length and the mod-97 check. - Birthdays: a date only next to birthday, DOB, born, 生日 and the like. A date on its own stays. - MAC addresses, but not documentation examples or all-decimal groups. - Serial numbers, next to S/N, serial or 序列号. - The user name in /Users/x, /home/x and C:\Users\x. The name is masked and the path is kept. - User and host in a shell prompt or an ssh/scp/rsync target (user@host goes out as two placeholders, not as an email address), and on hostname/user lines whose value ends the line. Code such as `hostname = socket.gethostname()` is left alone. - Public IPv4 and IPv6 addresses. Private, loopback, link-local, CGNAT, documentation, multicast and well-known resolver addresses stay, and so do version numbers. - Cloud bucket names in s3://, gs://, oss://, cos://, abfss:// and storage URLs. The object path is kept. The last four are off by default. Agents send their working folder, the prompt and log lines in nearly every request, and the model then has to copy the placeholder exactly inside a path or URL. The first six are rare and sensitive, so they are on. Each placeholder is restored in replies, in streamed text and in tool-call JSON. Context rules run only in a window around their words (rule.matches), so masking with the defaults on 400KB of app.js takes 136ms (112ms before; 222ms with every kind on). Tests, run at this head: - gofmt -l is clean. go vet passes on darwin, GOOS=linux and GOOS=windows (-tags nogui) for internal/redact, gateway, gui and settings. - go test -tags nogui ./... under a temp HOME with pinned caches passed everywhere except internal/proc's TestNoCommandBypassesProc. That failure came from |
||
|
|
9db2ed3dc6 |
gateway: temperature, top_p and top_k are left out for the Claudes that refuse them, so Haiku 5.5 no longer answers auto mode's classifier "temperature is deprecated for this model" (#1454)
Claude Code's auto mode classifier asks at temperature 0, and magpie's
own classifiers do too. Through the aihubmix relay, claude-haiku-5-5
answered 400 "`temperature` is deprecated for this model." (and the same
for `top_p` and `top_k`). magpie relayed sampling parameters as they
came, and built them from a chat request's temperature or top_p.
samplingAsTaken, beside thinkingOffAsTaken in forwardOnce, so relayed and
built requests and retries all go through it, leaves the three out for
the models Anthropic documents as refusing them:
- Opus 4.7, 4.8, 5 and 5.5, Fable 5 and Mythos 5 refuse them at any
value.
- Sonnet 5 and 5.5 refuse anything but the default.
- Haiku 5.5 takes only temperature 1 and top_p 0.99, never both, and no
top_k.
Left out, each model samples at its default. The model is read as the
vendor names it, from the body or, for Bedrock and Vertex, the path.
Older Claudes (Haiku 4.5, Sonnet 4.6, Opus 4.6 and before), other
vendors' models and later lines go byte for byte as they came. A Claude
under a name these rules can't read, answering that a sampling parameter
is deprecated or not supported (samplingDeprecated), is asked again
without them, once.
aihubmix itself took every value for the other models and refused it
only on Haiku 5.5. The other models are left out by the docs' rule, for
direct keys and relays that pass the vendor's 400 on.
Verified:
- Tests:
- TestSamplingAsTheModelTakesIt covers each refusing model's
spellings, a Bedrock path, and byte-identical bodies for Haiku 4.5,
Sonnet 4.6/4.5, Opus 4.6/4.1, Claude 3.7, Sonnet/Haiku 6, deepseek,
glm, kimi and gpt.
- TestHaikuFiveFiveSamplingOnTheWire uses a fake vendor answering
with the relay's literal 400s. It covers the classifier relayed,
top_p/top_k, magpie's own classifier built from chat, Haiku 4.5
keeping temperature 0, and an unreadable name retried once.
- Without the forwardOnce call, three wire cases FAIL. Without the
retry, the unreadable-name case FAILs.
- Live, through this build's `magpie serve` under a sandbox HOME with
the aihubmix key, in the classifier's shape (temperature 0, stop
</block>, thinking disabled), all 200:
- claude-haiku-5-5 at default and max effort and with thinking
omitted;
- claude-haiku-4-5, claude-sonnet-5-5, claude-opus-5-5 and
claude-sonnet-5;
- chat to claude-haiku-5-5 at temperature 0 and at top_p 0.5.
Straight to aihubmix, Haiku 5.5 at temperature 0, top_p 0.5 or
top_k 5 gives the 400.
- gofmt -l, GOOS=linux and GOOS=windows go vet -tags nogui
./internal/gateway, and go test -tags nogui ./internal/gateway under a
temp HOME with pinned caches.
|
||
|
|
a9cb0ef988 |
gateway: Claude Sonnet 5.5 is sent thinking between_tools in place of disabled, and Opus 5.5, Fable and Mythos no thinking, so Claude Code's auto mode classifier no longer gets a 400 (#1454)
911Liam's auto mode, on claude-sonnet-5.5 through a relay, got 400 "To
turn thinking off on this model, send "thinking": {"type":
"between_tools"} instead of {"type": "disabled"}". Claude Code turns
thinking off for its classifier and side queries unless it knows the
model refuses that, and a model reached through a gateway can fall
outside what it knows. magpie forwarded the disabled as it came, and
turns thinking off itself too (an effort of none fitted, a budget that
doesn't fit, thinkingOffUnlessAsked for other vendors' models).
thinkingOffAsTaken, in forwardOnce next to adaptiveThinking, so that
relayed requests, count_tokens, magpie's own thinking-off and retries
all go through it, follows Anthropic's rules for each model. It reads
the model's name as the vendor gives it, from the body, or from the path
for Bedrock and Vertex.
- Sonnet 5.5: between_tools with no other field, and xhigh or max asked
as high (between_tools is a 400 above high).
- Opus 5.5, Fable 5.x and Mythos 5.x refuse disabled at every effort.
Thinking is left out, effort is low unless one was asked, and
classifierRoom is added to a reply limit below it, as Claude Code
itself does for these models.
- Opus 5 and Haiku 5.5: disabled is kept, with xhigh or max asked as
high.
- between_tools sent to any other Claude goes as disabled, or with no
thinking where disabled is refused too.
Other vendors' models, Sonnet 5, Opus 4.8 and older, and lines later than
these go byte for byte as they came. A Sonnet 5.5 under a name these
rules can't read is asked again with between_tools, once, on the 400
that asks for it (asksBetweenTools). A refusal of disabled at any effort
(thinkingOffRefused) is asked again with thinking left to the model,
beside alwaysThinks.
TestAdaptiveOnlyModelsNeverGetABudget's "disabled stays" case pinned
disabled for claude-opus-5.5, which the vendor refuses. It now checks an
older Claude keeps disabled and Opus 5.5 is sent none.
Verified:
- Tests:
- TestThinkingOffAsTheModelTakesIt and
TestThinkingOffLeavesOtherModelsAlone (deepseek, glm-5.3, kimi,
Sonnet 5, Opus 5 at high, Haiku 5.5, Opus 4.8 at max, Sonnet 4.6,
Haiku 4.5, Claude 3.7, dated Sonnet 5 and Sonnet 6 unchanged).
- TestClaudeFiveFiveThinkingOffOnTheWire: a fake vendor answers with
the reporter's literal 400, covering the classifier body,
count_tokens and the retry.
- Without the forwardOnce call, the wire test's Sonnet 5.5, Opus 5.5 and
count_tokens cases and the Opus 5.5 adaptive case FAIL. Without the
retry, its unreadable-name case FAILs.
- Live, through this build's `magpie serve` under a sandbox HOME with
the aihubmix relay key, in the classifier's shape (max_tokens 64,
temperature 0, stop </block>, thinking disabled):
- claude-sonnet-5-5 answers 200 at default and xhigh effort (sent
straight to aihubmix with disabled it gives the reporter's 400);
- count_tokens 200;
- claude-opus-5-5 200;
- claude-haiku-5-5 at max 200;
- claude-sonnet-5 200.
- gofmt -l, GOOS=linux and GOOS=windows go vet -tags nogui
./internal/gateway, and go test -tags nogui ./internal/gateway under a
temp HOME with pinned caches.
|
||
|
|
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
|
||
|
|
eb035e29e1 |
sessions: a session seen only through the gateway can be deleted: its row and recorded conversation go to magpie's trash, and Restore brings them back (lc on Discord)
lc ("只可列出?不可删除?agent claude code") saw Claude Code sessions named by
uuid under "No folder" at "0 B", with "These Claude Code sessions are read
only; magpie can list them, but cannot resume or delete them." Those rows are
gateway projections from the usage ledger: Claude Code ran with its own files
somewhere magpie can't see (another computer, a container, another config
folder), so nothing native won on the same agent+id. An agent with only such
rows was sent as not deletable, and every row was read_only, so the page had
no Delete at all.
What changed:
- sessions.DeleteGateway moves the session's recorded text folders
(gateway-conversations/<date>/<identity hash>) into
trash/sessions/gateway/<ts>-<hash>/files/<date> with a note marked gateway,
and records {agent, id, last} in gateway-sessions-hidden.json.
/api/sessions and /api/sessions/manage leave it out up to that last
request; a newer request lists it again. The usage ledger is untouched, so
Usage, /api/sessions/one and stats keep what it spent. A session with a
request in the last minute is refused as active.
- Restore moves the text back (skipping what expired or was cleared, refusing
rather than overwriting a same-day re-recording), checks each item is this
session's identity folder inside the store, and unhides it. Delete forever
keeps it hidden.
- The recording's promises hold in the trash: the hourly prune erases
trashed gateway text past 7 days, "Delete saved conversations…" erases it
too, and HasGatewayConversations counts it.
- The manage route marks an agent with gateway rows deletable and sends
those rows deletable, with gateway: true and the size of their recorded
text; the delete route sends gateway ids to DeleteGateway and native ones
to Delete as before.
- Sessions page: gateway rows group under "Seen through the gateway"
instead of "No folder", show "{size} recorded" only when something was
recorded (no "0 B"), and the note says why they can't be resumed and what
Delete takes and keeps. The delete dialog says the same for gateway rows
instead of "Their files are moved…"; the Trash marks them and notes the
7-day expiry. New strings in zh, zh-TW, ja and de.
- docs/subsystems/gateway-sessions.md describes the delete, hidden list,
restore, expiry and clear.
Verified:
- go test -tags nogui ./... (sandbox HOME) green; gofmt -l clean; go vet
./...; GOOS=linux and GOOS=windows go vet -tags nogui ./internal/sessions
./internal/gui; go build, GOOS=linux -tags nogui and GOOS=windows builds.
- TestSessionsDeleteGatewaySession fails on the old code with "Claude Code
with only gateway sessions isn't deletable: [{Agent:claude
Deletable:false}]"; sessions-gateway-delete.test.cjs fails there too.
TestGatewayDeleteTrashExpires, TestGatewayPurgeKeepsItHidden,
TestGatewayDeleteNothingRecorded and TestGatewayRestoreRefusesForeignItems
cover expiry, purge, an unrecorded session and a forged note.
- GUI: sessions-gateway-delete.test.cjs in Chromium and WebKit, en, zh,
zh-TW, ja, de, at 1000 and 390px (in-app dialog, Cancel sends nothing,
click doesn't scroll, no left-border accents, no page overflow); all
sessions-*, gateway-conversations, checkbox-look, old-webkit,
sess-agent-strip, i18n-keys, gui-ja, gui-de and gui-zh-tw tests pass.
- Real try: magpie web from this tree on a sandbox HOME seeded with three
gateway-only Claude Code sessions (two recorded): deleted a recorded and an
unrecorded one, checked the store, trash and hidden list on disk, restored
the recorded one (its transcript reads again), purged the other (stays
hidden). That try found the Trash page breaking on an unrecorded session's
null items; DeleteGateway now writes [] (TestGatewayDeleteNothingRecorded
fails without it) and the page reads a missing list as empty.
|
||
|
|
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. |
||
|
|
c4476d5658 |
library: skills from GitHub are checked as the skills page opens, and one changed here is marked and asked about before an update replaces it (#1449)
YangZhiqiang98 asked (#1449) that the Library find which skills have updates and update them. magpie already checked skills from GitHub (a market install among them) by hand and could update them, but an update deleted the library's folder, so a skill the user had changed (by hand, through an agent's link, or in an agent's copy not yet taken in) was lost without a word, and nothing checked unless the user pressed Check. - A skill is "changed here" when its library folder no longer hashes to what was fetched (Finder's .DS_Store aside), or an agent's copy was edited since its mark and not yet taken in (editState, skill_changed.go). The view says so (SkillView.edited) and the row shows "Changed here". - UpdateSkill refuses such a skill with *EditedError unless told to replace it. The row's Update asks in the page first (Keep mine / Update; Update sends replace). Update N, the group's Update N and the CLI's update-all leave it as it is and say so. The CLI takes `magpie library skill update <name> --replace-edits`. - The version an update replaces is moved to the backups when it was changed, or when it can't be told (a skill with no hash from before). - View.checkDue: no check since magpie started, or none for 12 hours. The page then runs the check once by itself as the skills open, and says only what there is to update or add. Not checked, by design: skills the skills CLI manages (`npx skills update` updates them; magpie only groups them), linked local folders, skills imported from an agent with no source, and From-only skills have no remote; CC Switch-installed skills can still be updated by hand. Verified: - go test -tags nogui ./... (sandbox HOME); new TestUpdateKeepsASkillChangedHere, TestUpdateKeepsACopyEditedInAnAgent, TestUpdateKeepsWhatItCantTell, TestCheckDue, TestCheckSkillsMarksItDone, all from httptest. With editState forced to "not changed" the first three fail. - gofmt -l, go vet, GOOS=linux and GOOS=windows go vet -tags nogui on ./internal/library ./internal/gui ./internal/tui . - GUI: new skill-update-edited.test.cjs (Chromium and WebKit, 860 and 400px, en and zh; no scroll on click; fails with the row's ask removed), plus the 22 other library skill tests, gui-ja, gui-de, gui-zh-tw, i18n-keys and click-scroll: 262 pass. - Real use in a sandbox HOME with github.com/anthropics/skills: installed pdf and docx, the real check said both current; an edit through Claude Code's link was refused by `skill update pdf`, skipped by `skill update` (docx updated), replaced by `--replace-edits` with the edit in backups/<time>/library/skills/pdf; docx edited later shows edited, a .DS_Store doesn't. |
||
|
|
62d7cd7be3 |
sessions: recording gateway conversations is a setting card over the list, with what it keeps and where, and a labelled delete that shows only when text is kept (lee on Discord)
What changed:
|
||
|
|
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.
|
||
|
|
e50a958014 |
gateway: an unknown model's 404 names the nearest ids, display names first, and how a model is named (MOMO on Discord)
MOMO asked for "Zen-Space-Bunny", the model's name as a picker shows it, and the 404 said only "magpie knows no model" and then every id. It named neither the id meant (opencode-zen-free/space-bunny-free, "Space Bunny Free") nor the forms a model id can take. What changed: - provider/closest.go: Closest(asked, n) splits asked into lowercase letter and digit words and scores every catalog entry: a word in the display name (the user's or the vendor's) counts 4, in the model id 3, the provider's id or name 1 and only alongside one of the others; the whole of asked found in the name or id adds 4. - gateway/unknown_model.go: unknownModel puts "did you mean …?" (up to 3), then the forms (provider/model, a routing group's id or name, a bare model id and where an ambiguous one goes), then the ids as before. Used by the chat 404 and the embeddings 404. Switched-off, disabled-group and empty-group 404s are unchanged. Draw and video keep their own 404: their models are a different list, and the chat catalog's nearest ids would point at models that can't draw. - docs: gateway-routing.md model resolution row. Tests: unknown_model_test.go: "Zen-Space-Bunny" against opencode-zen-free serving space-bunny-free named "Space Bunny Free" is told that id first and the forms; a provider's name alone suggests nothing; a display name ranks before an id with the same words. The 404 test fails with the gateway 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 and the root package 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. |
||
|
|
3e5bc3d8fe |
gateway, usage: conversation text is kept only under a session the client named, and a client's "request-…" session counts like any other (#1355)
What changed: with recording on, a request that names no session used to get a made-up "request-…" id and its text saved under it. Nothing listed that id and GatewaySessionByID turned it away, so the text could never be opened. magpie now makes up no id: such a request isn't recorded, and the X-Magpie-Session reply header only echoes the client's own session. With no made-up ids left, the "request-" prefix checks in appendUsage, OTel, Usage's sessions, Routing's vias and the gateway Sessions projection only dropped sessions clients named themselves (appendUsage wiped them from the ledger). They are gone. The tests that held the old rule now say the new one, and gateway-sessions.md says it. Run: TestGatewayConversationNeedsTheClientsSession and TestGatewayConversationClientRequestPrefixedSession fail on the PR's code (a made-up id is returned; the ledger keeps 0 of 2 calls) and pass here; putting appendUsage's prefix check back turns the second red again. gofmt -l clean; go vet; GOOS=linux and GOOS=windows go vet -tags nogui on gateway, gui, sessions, settings, usage; go test -race -timeout 900s on those five: ok (gateway's TestCORSKeyThroughTheServer rerun after its fix). GUI: gateway-conversations, gateway-mode and all sessions-* tests, 216 cases in Chromium and WebKit: pass. |
||
|
|
c9b8263378 |
sessions, gateway: Sessions lists gateway sessions, and can keep their text on this computer when turned on (#1355, mu-zi-lee)
What changed: the Sessions page lists sessions seen only through the gateway (from the usage ledger, read-only, a native session of the same id wins), and shows in gateway mode. A Sessions switch, off by default and confirmed before it turns on, records each gateway exchange's text under the config dir (scrubbed, at most 7 days and 256 MiB, cleaned hourly). Settings sync and restore can't turn it on; POST /api/sessions/recording turns it on or off and can clear what was kept. Why: users of magpie as a gateway (gateway mode, LAN clients) had no way to see their conversations; the owner chose this design on the PR. Squashed from PR head 6a343ba1 onto origin/main. Conflicts: gui-shell.md (kept main's placeMenu text, took the PR's Sessions in gateway mode) and settings_keep_test.go (main's fields plus GatewayConversations). Run: go build ./..., go vet on gateway, gui, sessions, settings, usage; go test -race -timeout 900s on those five packages: all ok. |
||
|
|
916c1cc7ea |
gateway: a custom provider's video models, marked "kind": "video" by its own model list, are started, polled and fetched at its OpenAI Videos API (#1446, Edward4226)
What changed
- Videomakers: a custom provider (no account, not a remote magpie, no
preset plan's videos, a Chat or Responses base) makes videos with the
models its fetched /v1/models marks "kind": "video"
(catalog.LiveVideomakers). Nothing is guessed from a name: sora-like-2
without the kind, a chat model and grok-imagine-video named on it all
still get the 400 and reach no upstream video route.
- /v1/videos, /v1/videos/{id} and /v1/videos/{id}/content forward to
{base}/videos the way Remote magpie's already do (filmsOpenAI), where
base is the Chat base, else the Responses base (videoBase). The vendor
gets its own User-Agent and no caller headers; passOnCaller stays
remote-magpie only.
- Custom video ids base64 the vendor's id (its own may hold dots) and end
with the key's fingerprint (customVideoID, markedKey), so a poll and its
download use the key that started the video, and a key turned off since
answers 404 "no longer on". Remote magpie's and Volcengine's id formats
are unchanged.
- Gateway key holds on all three routes: account/key holds before any
upstream call (videoAsker), model holds once the model is known
(videoModelHeld). Grok's and Volcengine's polls and downloads ignored a
key's model holds before; they now apply them too.
- videoObject's done/succeeded mapping is untouched: the custom path passes
the vendor's OpenAI object through (asOurs), so its "completed" reaches
the client as sent.
- docs/subsystems/gateway-routing.md says so under Remote magpie APIs.
Why
- Edward4226's provider serves OpenAI's Videos API, and magpie answered
400 "can't make videos" because only Grok, Volcengine and Remote magpie
were allowed.
What was run
- New tests in video_custom_test.go (httptest OpenAI Videos fake whose id
holds a dot; start, two polls, content bytes; exact upstream request log;
Responses-only base; two keys with holds; Grok poll/content held by key
models). On the old video.go 4 of 5 fail (videomakers [], 400 "can't
make videos", held Grok poll 200); the name-guard test passes on both,
as it should. Breaking markedKey by hand, or the content model check,
turns TestCustomProviderVideoKeys red.
- go test -tags nogui -count=1 ./internal/gateway/ ./internal/catalog/
./internal/provider/: ok. gofmt -l clean; go vet -tags nogui
./internal/gateway/ on darwin, GOOS=linux and GOOS=windows: clean.
- End to end: the built magpie (nogui) under a scratchpad HOME, `magpie
provider add Studio url=http://127.0.0.1:3561/v1 key=…`, `magpie serve`
on 127.0.0.1:3562, against a Go fake of the Videos API on 3561. curl
start -> queued, poll -> in_progress, poll -> completed, content ->
video/mp4 whose 200000 bytes cmp equal to what the fake served. The fake
saw POST /v1/videos with model vid-1, the polls and /content, each with
the provider's key and no X-Magpie-Agent.
- No real video vendor was used. Multipart uploads to a real OpenAI
upstream weren't tried. The GUI lists no video models per provider, so
it has nothing to check.
|
||
|
|
af1e477d83 |
gateway: a resumed reply to a model that takes no prefill is answered after it, not refused (#1447, Edward4226)
OpenCode Go's Claude models (over Anthropic Messages) refuse a conversation ending with the assistant's message, "This model does not support assistant message prefill. The conversation must end with a user message.", and Volcengine's Agent Plan does too, "The last message cannot be from the assistant for a model that does not support prefill". magpie took every Anthropic upstream to go on from a prefill, so both a client's own resume (Codex sends what it has of a cut reply as the last assistant message) and magpie's own continuation of a cut reply (#853) ended with that refusal. The refusal is about the model, so it is remembered for that provider, model and API in unfit (prefillRefused); the provider's other models, and every upstream that takes a prefill, are unchanged. - A Chat or Responses client's conversation ending with the assistant's message, which those APIs read as a turn already said, is asked again with a user turn after it ("Continue."), the conversation the refusal asks for, and so from then on: translated (forwardTranslated, whole and streamed) and relayed as is (passthrough, Chat and Responses: the Volcengine case). - An Anthropic client's prefill is its own ask and still gets the refusal as the vendor sent it. - magpie's own continuation is not asked of that model again, and the try it refused ends the reply with what cut it, as a reply that can't go on did before #853, not with the refusal of an ask the client never made. No user-turn continuation there: a re-answer spliced onto the part the client has is the garble continuation.go avoids. The thinking-block refusal in the same report (a signed thinking block with no text) is a separate fix, in its own commit. Tests (prefill_test.go), from the report's literal messages: the issue's Responses repro and a streamed Chat request to an Anthropic upstream (TestTrailingAssistantToNoPrefillModel, also checks another model keeps its prefill), Volcengine's refusal relayed on Responses and Chat (TestTrailingAssistantRelayedToNoPrefillModel), a cut reply (TestStreamCutOnNoPrefillModel), an Anthropic client's prefill (TestAnthropicPrefillToNoPrefillModel), userLastBody. With the refusal unrecognised (the old code's behaviour) the first three fail with the report's "UP: Upstream request failed: [invalid_request_error] This model does not support assistant message prefill…" / "…the reply didn't end with what cut it"; with the remembering removed they fail with "calls 4 … asked one again" and "calls = 2 … asked to go on again". Run: gofmt -l (clean), go vet -tags nogui ./internal/gateway on darwin, GOOS=linux and GOOS=windows, go test -tags nogui ./... under a scratch HOME with pinned caches (all ok). Not run for real: the owner's OpenCode Go key answers 403 "An active OpenCode Go subscription is required", and there is no Volcengine Agent Plan account. |
||
|
|
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> |
||
|
|
599d784f59 |
library: magpie-image runs this computer's magpie, whatever path another computer or an old install saved (#1439, dawn1095)
magpie-image was kept in library.json with one computer's magpie by full path (os.Executable at install). A WebDAV sync carried C:\Users\...\Downloads\magpie-windows-amd64.exe to a Mac (and a Mac's app to Windows), and a magpie moved out of Downloads left its old path behind: the Library page said it couldn't start, and agents were given a program that isn't there. - heal (self_here.go): a server that is magpie's own -- `mcp ...` run by a program named magpie (magpie, magpie.exe, magpie-<os>-<arch>[.exe]) -- whose program isn't an existing absolute path here is pointed at this magpie, as the library is read (load) and after every change (so a bundle Put from another computer is healed before it is written into the agents). Every agent writer, project MCP files, the WSL side and CheckServers read through load, so all of them get it. A magpie program that does exist here (another build) and any server run by another program are left as they are. - Collect carries magpie's own server as plain "magpie": two computers with the same library carry the same bundle, so davsync's library hash matches and a sync doesn't write one's path over the other's. - The path given is update.Program (exported cliProgram): the AppImage rather than its mount, Homebrew's opt link rather than the Cellar. - TestWSLMagpieImage now says which magpie this (simulated Windows) computer runs. Verified: - New tests: TestOwnServerFromElsewhereRunsThisMagpie (Windows path read on macOS, a Mac path, an exe moved out of Downloads, a bare "magpie"), TestUsersOwnServerStaysAsItIs, TestCarriedOwnServerIsEachComputersMagpie. Broken by hand five ways (heal off; no heal in change; no marker in Collect; ownServer matching every server; existing magpie builds not kept) -- each went red with a FAIL naming the behaviour, green restored. - gofmt -l clean; go vet -tags nogui ./...; GOOS=linux and GOOS=windows go vet (library, update, davsync); go build, GOOS=linux -tags nogui and GOOS=windows builds; go test -tags nogui ./... green in a sandbox HOME. - Sandbox app (own HOME, ports 3561/3562) seeded with a library.json holding the reporter's Windows path for magpie-image plus a user's own missing Windows tool: on start library.json, Codex's config.toml and Claude Code's .claude.json got the running binary; /api/library/mcp/check answered magpie-image ok with 3 tools, the user's server unchanged and notfound. Not verified: on the Windows box, or a real two-computer WebDAV sync. |
||
|
|
7f62dbf3ed |
gateway, agents, gui: a model's reply limit is published within the window beside it, on /v1/models, Cursor's and Muse's lists, the Gateway page and magpie model output (#1438, yhong91)
#1438 (yhong91): GET /v1/models listed grok/grok-4.7 with context_window 256000 (the Grok backend's own) and max_output_tokens 500000 (models.dev's x-ai entry, which catalog.Decorate fills in when the backend gives no output). A client that reads both sizes its replies past what the window holds. The agents' files already kept output within the window (maxTokens, #338/#347), but the gateway's lists, the GUI and the CLI did not. What changed: - provider.OutputWithin(window, output) and Entry.PublishedOutput(): the output is kept within the window; an unknown window or output (0) is left as it is. - Every place that publishes the pair uses it: modelObject (/v1/models), cursorLocalModel (Cursor Private Inference), Muse Code's list, the Gateway page's models and groups (providerInfo, providersState), the agents' maxTokens, and `magpie model output` (which also says "500k, kept within its 200k window" when it lowers one). - Entry.Output stays the model's own limit. The request-path cap (outputLimit/withMaxOutput) and the user's Max output setting are unchanged, so no request is lowered further than before. This is why the clamp is not in replyLimit/entryFor as the issue suggests, and why the commenter's patch (modelObject only) was not taken as is: it was right but missed Cursor, Muse, the GUI and the CLI. - docs/subsystems/gateway-routing.md: the rule and its routes. Tests (each fails without the change): - gateway TestModelListsKeepOutputWithinTheWindow: models.dev 500000 over a backend 256000 window, plus output within the window, unknown window and unknown output, on /v1/models, Cursor's capabilities and Muse's limit; a group request with max_tokens 300000 still reaches upstream as 300000. - provider TestOutputWithin, gui TestProviderModelOutputWithinTheWindow, TestModelOutputCmdWithinTheWindow. Breaking it by hand: OutputWithin returning output unclamped turns all of them red, plus agent TestMaxTokensWithinContextWindow; gateway sites back on e.Output turn the gateway test red on all three lists; moving the clamp into Entry.Output turns the fixture and the group-request guard red. Real data: an offline dump of the owner's catalog (copies of ~/.config/magpie and its caches, 203 entries) has 3 models with output over their window, grok/grok-4.5, 4.6 and 4.7 (256000 vs 500000), now published as 256000; 0 are not Grok. The live gateway on 3425 was not running, so /v1/models itself was not read, and no real Grok request was made. Run: gofmt -l; go vet -tags nogui ./... on darwin, linux and windows; go test -tags nogui ./... -count=1: all green except TestLoopEndsWorkBuddysReply/plugin/anthropic ("let go only after 3207 lines", bound 2000) at load average ~35 with other suites running; it passes 5 of 5 alone and does not touch the output limit or request path. |
||
|
|
b4bdd59f72 |
gui: a long app menu stays in the window and scrolls inside itself, so the last removed group can be brought back (#1437, Hipple)
With 28 found groups removed, Routing's "28 found groups removed" menu ran
off the bottom of a ~717px WebView2 window and couldn't be scrolled.
openRowMenu had no height cap (max-height: none, and .pop clips with
overflow: hidden), it only flipped up when the whole menu fit above, and any
scroll, even one inside the menu, closed it.
What changed:
- placeMenu (app.js) places every app menu: under its anchor, over it when
the window runs out below, and one that fits neither way takes the larger
room with its height held to it. openProtoMenu's placement moved into it
unchanged; openRowMenu (agent rows, model chips, Routing's removed groups
and its other row menus) and openSessCombo (the Sessions/Usage filters,
which could run off the bottom the same way) now use it too.
- .row-menu scrolls inside itself (overflow-y: auto, overscroll-behavior:
contain), and openRowMenu closes only on a scroll outside the menu, as
openProtoMenu and openSessCombo already did.
- docs/subsystems/gui-shell.md names placeMenu.
Verified:
- New removed-groups-menu-scroll.test.cjs: 30 removed groups, 1100x700 and
560x700, en and zh, Chromium and WebKit, 8/8 pass. It wheels the page to
the line, clicks it where it is, checks the menu is inside the window and
scrolls, wheels the menu until the last entry shows inside it, clicks it
(posts groups/show auto-gone-30), and checks the page never scrolled and
has no sideways scroll.
- Broken by hand three ways, each red in every case: the old app.js/app.css
("the menu stays in the window": bottom 1494 of 700), the old
scroll-closes-menu handler ("scrolling the menu doesn't close it"), and no
overflow-y on .row-menu ("the last entry is scrolled into view inside the
menu").
- The 63 GUI test files that open app menus or guard click-scroll, both
engines: 595 pass, 1 fail: search-api.test.cjs webkit zh, at page load
(the row is redrawn between waitFor and scrollIntoViewIfNeeded) before any
menu opens; it passes 5/5 alone. Not run: the rest of the GUI suite, and
gui-zh-tw/ja/de (no new strings).
- gofmt (no Go changed), go vet ./internal/gui (darwin, GOOS=windows),
GOOS=linux and GOOS=windows go vet -tags nogui ./internal/gui, and
go test -tags nogui ./... under a sandbox HOME: all pass.
- Not tried in a real Windows WebView2 window.
|
||
|
|
ca0867e412 |
gateway: a Grok model's 2500.0 in a call's arguments reaches the agent as 2500, on every route and protocol, deltas and done alike (#1431, timonwong)
Grok's models (grok-4.6, grok-4.7) write a whole number in a tool call's
arguments with a fraction of zeros. Codex reads exec_command's
yield_time_ms and max_output_tokens as u64/usize and write_stdin's
session_id as i32, though their schemas say "number", and serde turned the
whole call away ("failed to parse function arguments: invalid type:
floating point `2500.0`, expected u64 at line 1 column 77"; `17277.0`,
expected i32), so the turn stopped at its first command. Reported 11 of 11
turns through a sub2api relay in front of grok-4.6 and grok-4.7.
What changed (grok_integral.go):
- grokModel/grokServed: a Grok model is one whose id, as magpie names it
or as it is sent upstream, has a last `/` segment beginning "grok"
(relay/grok-4.6, x-ai/grok-4, the plugin's grok-4.7).
- wholeNums: a streaming scanner over the arguments' JSON text. A number
written with a fraction of zeros and no exponent loses the fraction
(2500.0 -> 2500, -3.00 -> -3, [1.0,2.0] -> [1,2]). Strings, keys, other
fractions (2.5, 2.50) and exponents (1.0e3) stay as written. It holds
only a run of ".0…" until the next byte, so arguments cut into deltas
anywhere come out the same as whole, and the deltas add up to what the
done events say.
- Relayed (passthrough): intTidy, beside nsTidy and built like it, on
Responses (function_call items, argument deltas and done, the completed
response, a whole reply), Chat (tool_calls deltas and messages) and
Messages (tool_use input and input_json_delta), streamed or whole. A
line that carries no call passes as it came. Plugin-served Grok and the
built-in subscription go through the same passthrough.
- Translated: translate sets Request.Grok; encoder() wraps the client's
encoder in wholeEncoder (outside usageEncoder) and render() rewrites the
Result's calls (wholeResult), so every client protocol is covered at
those two choke points.
- Any other model's reply is relayed byte for byte, as before.
- gateway-routing.md says so, and lists the tests.
Scope, against the reporter's 9fd6855b: that commit rewrote only fields a
tool schema typed "integer" or a fixed list of Codex's (codexInts), only
on Responses, and left deltas as they came, so the deltas disagreed with
the done events, and the list missed Codex's other integers (shell's
timeout_ms, sleep's duration_ms, tool_search's limit, the multi-agent
tools' timeout_ms). Codex's own schemas say "number", and JSON has one number type, so 2500 is the same value as
2500.0 for every client; this drops the zero fraction everywhere in a
Grok model's call arguments instead. Its rollout fixtures are kept
verbatim (testdata/grok_integral_args) and its test helpers adapted.
Tests: TestWholeNumsAnyCut (every split point and byte by byte),
TestGrokCallsGiveCodexIntegers (relayed Responses, streamed with deltas
cut whole, byte by byte and at every '.'/zero, and whole),
TestGrokCallsGiveCodexIntegersTranslated (Chat upstream -> Codex),
TestGrokCallsGiveIntegersOnEveryProtocol (Chat relayed, Messages relayed,
Messages translated from Chat), TestGrokCallsByUpstreamName,
TestGrokPluginCallsGiveCodexIntegers (fake plugin, $FAKE_GROK),
TestOnlyGrokCallsGetIntegers (gpt-5.5 byte for byte on Responses, Chat
and Messages relayed; translated keeps 2500.0), TestGrokModel. Each
recorded case checks that the field Codex's own error names is now an
integer of the Rust type it named, wherever Codex reads the call.
With grokModel forced false, every Grok test fails; with the delta
rewriting turned off (intTidy and wholeEncoder), every streamed case
fails on deltas != done.
Run: gofmt -l (clean); go vet -tags nogui ./... on darwin, GOOS=linux
and GOOS=windows; go build ./... and GOOS=linux/windows -tags nogui
builds; go test -tags nogui ./... under a temp HOME; -race -count=3 on
the new tests. End to end: the built binary (`serve`, sandbox HOME, port
47812) in front of a fake upstream streaming 2500.0 a byte per delta on
Responses and Chat; a client reading /v1/responses and
/v1/chat/completions, streamed and whole, got integers in deltas,
arguments.done, output_item.done, completed and output for grok-4.6
(relayed) and grok-4.7 (translated from Chat), and 2500.0 unchanged for
gpt-5.5. The same client against a binary of the parent commit failed
all 12 Grok checks.
Not verified: a real Grok model or a real Codex turn; Messages and Chat
Grok replies have not been seen in the field; a relayed Messages
tool_use input that is rewritten has its keys re-marshalled in sorted
order; exponent forms (2.5e3) are left as written.
Co-authored-by: timonwong <timon86.wang@gmail.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
|
||
|
|
059171006b |
library: DeepSeek Harness is given a server's ${NAME} as a !!js expression it reads from its environment (#1435, CnsMaple)
A Library MCP server whose headers or env named a variable as ${NAME}
(#1250) was refused for dsh, because dsh had no reference syntax in
refsOf. dsh does have one: a YAML !!js value is evaluated as the row
activates (dsh 0.2.0-rc.2: cordis-plugin-include's entryListSchema
reads the tag, quoted or not; cordis-plugin-loader's interpolate
evaluates it across the whole config), and dsh-mcp-client's README uses
GITHUB_TOKEN: !!js process.env.GITHUB_TOKEN. A stdio server needs it so:
it inherits dsh's environment without credential-shaped names
(scrubbedParentEnv).
What magpie writes now (refsOf fmtDsh -> refDsh, dshRef):
- a whole ${NAME}: !!js process.env.NAME (bare, as the README has it)
- text mixed with variables: the text as JSON string literals joined
with (process.env.NAME ?? ""), YAML-quoted. The user's text is only
ever inside a JSON string literal, so nothing in it runs as code, and
NAME is the envRef pattern [A-Za-z_][A-Za-z0-9_]*. An unset variable
inside a mixed value is "", not "undefined".
dshUnref reads exactly those forms back as ${NAME}, so an entry magpie
wrote reads as same and a sync changes nothing; any other !!js still
keeps the entry the user's (hasJS), as before.
The reporter asked for the value to be resolved into a literal when the
file is written. This writes a reference instead, keeping #1250's rule
that the token never lands in an agent's file as plain text. dsh
evaluates it from its own environment at load time. Their side note
that a quoted !!js isn't evaluated is not what dsh 0.2.0-rc.2 does: its
own --dump-config prints !!js quoted, and the quoted forms evaluated in
the run below.
Since AgentView.NoEnvRefs comes from refsOf, dsh's chip is no longer
greyed and the GUI notice drops it with no string change.
docs/subsystems/library.md says what dsh gets.
Run:
- real dsh 0.2.0-rc.2 `dsh headless` in a sandbox (HOME and DSH_HOME
under a scratch dir, never ~/.dsh), on a cordis.patch.yml written by
magpie's mcpFile: a stdio fixture got T1435_A=<token> (whole) and
"Bearer <token>" (mixed); an injection string with quotes, backticks,
${, backslashes and "'); process.exit(3); ('" arrived verbatim; an
unset variable in a mixed value gave "pre--post"; a streamable-http
fixture received Authorization: Bearer <token>, the whole-variable
header and the literal header. magpie read every entry back as same.
A whole !!js process.env.UNSET makes dsh leave the server out as an
invalid config.
- go test -tags nogui ./... (temp HOME, pinned caches): green.
- break by hand: refsOf without fmtDsh, encode writing raw values, and
decode not unref'ing each make TestEnvRefsDsh fail (decode also
TestDshImportKeepsJS).
- gofmt -l; go vet ./...; GOOS=linux and GOOS=windows go vet -tags nogui
./internal/library; go build for darwin, linux and windows (nogui).
Not run: dsh on Windows (the reporter's system), and a real LLM turn
calling a tool through the server (no dsh account in the sandbox; the
MCP servers had connected before dsh stopped for its missing LLM
credential). No GUI test was run, as no GUI file changed.
|
||
|
|
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). |
||
|
|
2c1b593675 |
library: Zed takes MCP servers, in its settings.json context_servers (lc on Discord)
lc on Discord ("agent未识别到zed"): the MCP server editor's agent chips
had no Zed. magpie already edits Zed's settings.json for its model, and
Zed reads its MCP servers from context_servers in that same file, so Zed
is now an MCP target like the other agents.
What Zed takes, from its own source (zed-industries/zed 2c99f547):
- crates/settings_content/src/project.rs ContextServerSettingsContent is
untagged: a command as command/args/env (enabled, remote, timeout
beside it), a url as url/headers (enabled, timeout, oauth), or an
extension's entry as settings. docs/src/ai/mcp.md writes the same.
- crates/context_server/src/transport/http.rs only POSTs (and DELETEs):
streamable HTTP, no SSE. So an SSE server is refused for Zed and its
chip is greyed with the existing reason (AgentView.NoSSE).
- stdio_transport.rs hands command.envs over as written and http.rs sends
headers as written: no ${NAME}. Zed is refNone, so a server naming a
variable is refused and its chip greyed (AgentView.NoEnvRefs).
Changes:
- targets.go: "zed" gets mcpFile{agent.zed's settings.json, fmtZed}; the
path is agent.zed's for every OS (~/.config/zed, $XDG_CONFIG_HOME/zed,
%APPDATA%\Zed, $MAGPIE_ZED_CONFIG_DIR).
- mcp.go: fmtZed under "context_servers", encoded as Cursor/Desktop are;
magpie owns url/headers/command/args/env only, so enabled, remote,
timeout and oauth the user sets on magpie's entry stay. The file is
JSONC and edited in place (edit.SetJSON/DelJSON), so comments, other
settings and the user's own servers are kept.
- mcp.go, sync.go: an entry Zed's extensions configure (settings, no
command or url) is never written over: a library server by that name
is a Problem for Zed (errZedExtension), asking for another name.
- envref.go: a comment citing why Zed has no syntax.
- docs/subsystems/library.md: Zed's file, format, no SSE, no ${NAME},
the extension guard, and the new tests.
Tests:
- internal/library/zed_mcp_test.go TestZedMCP, on a Zed-style JSONC
settings.json with comments, trailing commas, a user stdio server
(enabled:false, timeout), a user remote server and an extension's
entry, at the default path and under MAGPIE_ZED_CONFIG_DIR: found
servers, the page's chips, written entries, SSE/${NAME}/extension
refusals, user keys kept through an edit, uncheck and remove leaving
only the user's, comments kept, a Sync rewriting nothing.
- internal/gui/tests/library-zed-mcp.test.cjs: Zed's chip on every row
and in the editor, a click gives it the server, greyed with the reason
for SSE and ${NAME}; Chromium and WebKit, en/zh/zh-TW/ja/de, 1100 and
560px, no sideways scroll. No new strings.
Broken by hand, each went red and was restored: no zed target, no
extension guard, SSE allowed, enabled owned by magpie, wrong key (Go);
Zed without noSSE (GUI).
Run (temp HOME, Go caches pinned): gofmt -l internal/ (clean); go vet
-tags nogui ./... on darwin, GOOS=linux and GOOS=windows; go vet
./internal/library ./internal/agent; go build ./..., GOOS=linux go build
-tags nogui ./..., GOOS=windows go build ./...; go test -tags nogui ./...
(all ok); node --test library-zed-mcp.test.cjs (22/22, both engines).
Not verified: a real Zed reading the written file (the live
~/.config/zed was never touched); Linux and Windows beyond vet/build;
Zed found in WSL (not in ownFolder, so no MCP target there); a
project's .zed/settings.json (not added).
|
||
|
|
189586612e |
agents, gateway: Codex's subagents go on a model set in its row, a ChatGPT account's (willz on Discord)
Codex's lead names each subagent's model itself in spawn_agent, and
Codex's [agents] config can't overrule it. The Agents page's Codex row
now has a "subagent model" square (the app's model picker, Default
first). Its options are only models a ChatGPT account in magpie serves,
because a subagent's task is sealed for ChatGPT accounts.
- settings.CodexSubagentModel holds it. It is an agent Field
(subagent_model), so the TUI/CLI, profiles, davsync's "agents" part
and WSL twins carry it too. The setter refuses another provider's
model. It keeps a ChatGPT account's model that isn't served now, so a
backup restored on another computer still applies (ApplyFields stops
at the first failed Set). POST /api/settings keeps it.
- The gateway (codexSubagentPick) moves a collab_spawn request onto it.
codexBackend does this for Codex's own bare models; serve() does it
for any other model. When no ChatGPT account on in magpie serves it,
or a held gateway key may not use it, the request stays on the model
asked for. Route.Subagent records asked/to/kept. The lead's turns,
titles, guardian and the rest are never moved. Off (the default)
changes nothing.
- Routing page: kindWhy says the subagent was put on the model set, or
stayed on its own and why. "only account on for" names the model it
went on. Strings added in zh, zh-TW, ja and de.
- docs/subsystems: gateway-routing.md and agent-wiring.md describe it.
Run (darwin, rebased on origin/main):
- gofmt -l internal/: clean.
- go vet -tags nogui on gateway, provider, agent, gui and settings:
darwin, GOOS=linux and GOOS=windows.
- go build ./...
- HOME=<temp> go test -tags nogui ./...: all ok.
TestSettingsSaveKeepsWhatItDoesNotSend flagged the new field; it is
now set there.
- GUI tests, Chromium + WebKit:
- codex-subagent-model.test.cjs: 20/20 (en, zh, zh-TW, ja, de at
1100 and 560 px).
- Also green: routing-kind, subagent-effort, codex-memories,
codex-memories-settings, i18n-keys, gui-ja, gui-de, gui-zh-tw and
click-scroll.
Failing without the change (codexSubagentPick returning nil):
- TestCodexSubagentPutOnTheModelSet
- TestCodexSubagentOnAnotherProviderPutOnTheModelSet
- TestCodexSubagentModelNotServedKeepsTheAsked
- TestCodexSubagentModelHeldKeyKeepsTheAsked
TestCodexSubagentModelLeavesOtherRequests guards the other side: the
lead's turns, titles and guardian stay untouched.
Not tried with a real Codex CLI and ChatGPT account. A subagent spawn
needs a signed-in Codex home, and this session may not touch the live
~/.codex. The fake ChatGPT backend tests cover both routes.
|
||
|
|
aa82288fb6 |
usage: Volcengine Ark's Coding Plan and Agent Plan windows, read with the account's access key (#1427, yayoinoyume)
Ark tells a Coding Plan's or Agent Plan's 5-hour, weekly and monthly
windows only to the account's control plane (GetCodingPlanUsage,
GetAFPUsage on open.volcengineapi.com), signed with the account's
AccessKey ID and Secret, never to the plan's inference key. An Ark
provider's editor now takes that access key, and the Usage page shows the
plan's windows as a card.
From yayoinoyume's feat/volcengine-usage, reviewed and reworked:
- Signing is volc-sdk-golang's (base/sign.go GetSignRequest, 06f9ef66):
signed headers sorted (content-type;host;x-content-sha256;x-date), only
Action and Version in the query. The branch signed them unsorted and put
Region in the query. TestVolcSignMatchesSDK pins two signatures the SDK
made.
- The card is in PlanQuotas beside the Step Plan's (its cache, last
reading, refresh and `magpie quota wait`), not in the subscription
cards. One per access key and plan.
- The plan is the endpoint's: /api/coding asks GetCodingPlanUsage,
/api/plan GetAFPUsage, pay-as-you-go none, in the host's region. The
branch asked both and showed whichever answered.
- Dropped guessed reply fields; the Coding Plan reply is the one
pi-ark-usage, dsh-ark-quota and ArkCodingPlanUsage read. AFP buckets are
read in the forms dsh-ark-quota and arkcli read (Used of Quota or Total,
or Percent), plus Tier; none was seen from a real account.
- An Ark provider is known by its host, not the preset id, so a copy or a
custom provider takes the key too.
- The Secret is never sent to the page (accessKey {id, secretSet}); a
blank Secret keeps it, a save without the ID keeps both, Remove
(clearAccessKey) drops both, an ID with no Secret is refused.
- The pair goes where keys go: AddCopy, Restore, Mirror, backups and
davsync carry it only with keys (withoutKeys), and a keyless restore or
sync keeps this machine's. CLI: access.key, access.secret.
- A changed access key drops the cached cards (ForgetPlanQuotas).
- UI strings in zh, zh-TW, ja and de.
Nothing was tried against the real Volcengine API: there was no access
key. A local fake checked the real request PlanQuotas makes against
volc-sdk-golang's own GetSignRequest (both actions, two regions); it
lives outside the repo, so go.mod is unchanged.
Run: gofmt -l; go vet (darwin), GOOS=linux and GOOS=windows go vet
-tags nogui on provider, gui, backup, davsync and the root; go test
-tags nogui ./internal/provider ./internal/gui ./internal/backup
./internal/davsync . under a temp HOME; node --test
volc-access-key, zhipu-team, gui-ja, gui-de, i18n-keys in Chromium and
WebKit. Fail without the change: TestVolcSignMatchesSDK (unsorted
headers), TestAddCopy, TestBackupBalanceToken, TestNoKeys,
TestTakeBalanceToken, TestSyncBalanceToken (pair not carried),
TestProviderSaveKeepsVolcengineSecret (blank Secret dropped),
volc-access-key (Remove not posted).
Co-authored-by: YAYOI27 <111715843+yayoinoyume@users.noreply.github.com>
Co-authored-by: Claude <noreply@anthropic.com>
|
||
|
|
c3e673b061 |
codex: the account Codex is signed in to goes by the name it is saved under, so two Team seats of one email are weighed apart (#1424, lingqiqi5211)
One email in two Team workspaces is saved as "me@example.com · Team" and "me@example.com · Team · <workspace>" (codexName). codexAccount still named the account Codex is on by codexUser alone, "me@example.com · Team". With Codex signed in to the second seat, its candidate used the second seat's credentials under the first seat's name. Everything keyed on Account.User then read the first seat's records: Smart's allowance (98% used, so the seat with 2% left was ranked last), rests and sticks (restKey/who), the usage cap, per-account proxy and model list, a gateway key's account list (LoginID), OwnPaused, and the Routing page's name. - codexLiveName (logins.go): codexName over the saved logins, shared by liveLogin and codexAccount, so the provider's Account.User is the saved name. - SessionIdentities: a saved Codex identity goes by its saved name, and the one from auth.json by that of the saved one of its workspace and member, so a session of the second seat is told as that seat's. - providers-accounts.md: the Codex naming rule. Stored records: nothing keyed under a name that goes away. The first seat keeps "email · Team"; the second seat's rests, caps, proxy, model list and usage are already under its saved name, written while it wasn't the one in use. Records written under the first seat's name while the second was in use can't be told apart now. Rests run out and readings are read again, so none is migrated. Tests: TestCodexTeamSeatsOfOneEmailWeighedApart (gateway): Team A 98%, Team B (Codex on it) 2% with its week renewing first, another email 20%. On the old code it fails with "sent to ws-other; want the seat Codex is on" (and, with its name check, "goes by \"me@example.com · Team\""). With session_identity.go reverted, TestCodexWorkspaceSessionIdentities (provider) fails with "the seat Codex is on: [me@example.com · Team]". Run: gofmt -l (clean); go vet, GOOS=linux and GOOS=windows go vet -tags nogui on provider, gateway, usage; go build -tags nogui ./... on darwin, linux, windows; go test -tags nogui ./internal/provider ./internal/gateway ./internal/usage under a temp HOME with pinned caches: all ok. Not tried with real accounts: no two Team seats of one email are available here. |
||
|
|
acdaa28aa1 |
codex: a conversation that searched the web declares cached search to the ChatGPT backend, so its compaction and turns aren't refused "response protection is unavailable" (#1270, #1099)
The ChatGPT backend answers HTTP 200 and only `event: error` "response
protection is unavailable" to any request whose input replays one of its
own web_search_call items while the request doesn't declare the web_search
tool. Probed on a real ChatGPT Pro account (gpt-6.1-sol, 2026-10-09) with
a real searched turn: refused with tools [], with function tools only and
with a compaction_trigger; answered once {"type":"web_search",
"external_web_access":false} is declared. A Responses Lite request refuses
a top-level web_search (400) but takes it in an additional_tools item. So
xjdata's case (Codex compacting a custom provider's conversation itself, a
plain /responses with tools [] that magpie's codexCompact never sees) failed
every time once the session had searched, and congee949's thread
descriptions too.
What changed:
- provider.DeclareSearch / declareSearch: when the input holds a
web_search_call and nothing declares search, add cached-only web_search to
the tools, or for Lite (header, reasoning.context all_turns, or an
additional_tools item present) to the additional_tools item (prepended
when there is none). With no tools offered, tool_choice becomes "none" so
the model can't search; a forced choice is kept. Otherwise the body is
unchanged (bytes untouched when no web_search_call is in it).
- codexBody (every ChatGPT account route: serveAgent, codexCompact, warmup,
magpie's codex/* models) calls it; codexUpstreamOn calls it for Codex's
own sign-in relayed to the backend's /responses.
- Not changed: siwc, API-key passthrough to api.openai.com, the legacy
/responses/compact endpoint (no evidence they are refused so; not probed).
- docs/subsystems/gateway-routing.md names the cause and the fix.
Tests: internal/gateway/search_history_test.go replays the backend's real
items from that probe against a fake that refuses as the backend did.
TestSearchedConversationCompactsOnAnAccount, ...FunctionTools and
...OnCodexSignIn fail on the old code with "event: error"; ...CompactionTrigger
fails with "asked 2 times, not once". TestUnsearchedConversationUnchanged
and provider TestDeclareSearch cover what's left alone.
Run: gofmt -l (clean); go vet, GOOS=linux go vet -tags nogui, GOOS=windows
go vet -tags nogui on internal/provider and internal/gateway; HOME=<temp>
go test -tags nogui ./internal/provider ./internal/gateway (ok). Real
account, sandbox magpie on 127.0.0.1:3491 under a scratch HOME, replaying
the real searched history: old binary -> /v1/responses codex/gpt-6.1-sol
text compaction with tools [] and /backend-api/codex/responses (standard
and Lite compaction_trigger) all "response protection is unavailable";
new binary -> all response.completed, Lite with a compaction item.
Codex CLI 0.161.0 end to end was not done: on a custom provider it
offered no web_search tool, so no search could get into its history.
|
||
|
|
69c21cdd77 |
gateway, settings, gui: a reply whose reasoning or text has become a loop of the same few lines is ended with an error the agent can act on, instead of running on to its output limit (#1359, Moody-Sin: WorkBuddy's DeepSeek v4.1 flash at max effort reasoned "Now." "Writing." "Go." "Let me write." "OK." "Producing." … until max tokens, while dsh waited and Pi hung)
holdWriter.Write, which every streamed try goes through (passed through or translated; a key's, a built-in subscription's, or a plugin's fetch, as a moved WorkBuddy's), feeds a loopGuard (loop_guard.go). It reads the reasoning and the text of each protocol's deltas (Anthropic, Chat, Responses, Gemini) and cuts each into units: lines and sentences, compared without case or a closing stop. A list's "1." or a decimal doesn't end a sentence. A unit with no word in it (braces, rules, 0x00, hashes, rows of numbers) isn't counted. When the last 256 counted units of either are among 24 or fewer distinct ones, the try is cut. What came with that write goes out, then the protocol's stream error (Anthropic error event, Chat error chunk, Responses response.failed server_error, Gemini 502 UNAVAILABLE), naming the loop and quoting its units, and the vendor's request is cancelled. The cycle needn't be exact; the reporter's has 9 units in two variants. It falls back to nobody and rests nobody (fail "loop"), and the usage record's err_type is reply_loop. A reply sent whole (no stream, or Gemini without alt=sse) isn't read. Settings › Models › Replies › Stop looping replies (noLoopGuard, on by default) turns it off; new strings in zh, zh-TW, ja and de. Tested: - loop_guard_test.go, with the screenshot's 28 lines cut into random 1–7 byte pieces, as one line, and in Chinese. It trips within the window. It reads every protocol's shapes, and tool-call arguments never trip it. - No trip on magpie's own sources fed as both reasoning and text: gateway.go, fallback.go, anthropic.go, app.js, i18n.js, app.css, routing.js, sessions.js, index.html, docs, LESSONS.md and go.sum. Nor on generated numbered lists, logs, CSV, hex, rules and base64. - Through the gateway on all 4 protocols with a fake DeepSeek, and on WorkBuddy both built-in and through the community plugin's fetch in the plugin host (TestLoopEndsWorkBuddysReply). In each case the agent gets the reasoning and then the error, the vendor is let go within 2000 lines, nobody rests, and usage keeps reply_loop. - gateway.go as 4,900 lines of reasoning passes whole on every protocol. With the setting off, a loop runs to its end. - With the guard removed, the end-to-end and WorkBuddy tests fail on every route. - The built binary (magpie serve on 127.0.0.1:3592, temp HOME, fake DeepSeek upstream), checked with curl on chat, messages, responses and gemini alt=sse. Each loop ended with that protocol's error, the upstream was let go after 256–271 lines, and the usage log had err_type reply_loop and routing fail "loop". The long gateway.go reasoning came through to [DONE], message_stop, response.completed and STOP. With noLoopGuard, all 100,000 lines passed. - GUI: loop-guard-setting.test.cjs in Chromium and WebKit, en/zh/ja/de, at 1100 and 420 wide. gui-ja, gui-de, gui-zh-tw, i18n-keys, member-model-setting, click-scroll and settings-sections/groups pass. - go vet, GOOS=linux/windows vet and builds, go test -race ./internal/gateway ./internal/settings, and the full go test -tags nogui ./... all pass. Not tried with the real WorkBuddy or DeepSeek model. |
||
|
|
ab3fe0fe41 |
gateway: a Claude subscription turn whose saved session Claude Code can't load goes on in a new Claude Code instead of 502 "Claude Code ended", and a Claude Code that ends early says why (FrierenF on Discord)
What: a conversation let go past idleMost is resumed from its saved session (unshelve, `claude --resume <sid>`). Claude Code 2.1.295 that can't load the session prints "No conversation found with session ID" and exits 1 before it reads anything, without answering a control request (tried with the real binary in a scratch CLAUDE_CONFIG_DIR: a missing and an empty session file both do this; a loadable one answers initialize). With the safety context set (Claude Code's auto mode), start's initialize got its reply channel closed and the turn was answered 502 "Claude Code: Claude Code ended", with no reason; without it, the turn got an empty 200. Now: - start sends initialize to a run resumed from a saved session too, as the probe that Claude Code loaded it; when that run fails, the session is let go and a new Claude Code is told the whole conversation in the same request (start / startRun). - cliControl reports a run that ended before answering with its exit status and stderr tail (endedBefore, shared with whyEnded), and its timeout no longer says "safety context" for every request. - The other paths that can meet an ended run already start a new one: resume (setSafeguards / tell failing -> abort, fall through) and the tool-results path (claimResume / continueWith -> runExpired). - The resume harness's fake Claude Code now acknowledges control requests as Claude Code does. Not known: why FrierenF's saved session couldn't be loaded, or whether their run ended for another reason; their next failure will now name it. Run: - TestClaudeUnloadableSessionStartsAnew (both with and without safeguards) and TestClaudeEndedRunSaysWhy fail on origin/main: "502 Claude Code ended"; no run started anew (empty 200); "Claude Code: Claude Code ended" lacks exit status 3 and stderr. They pass with the change, -race -count=10. - go test -tags nogui ./internal/gateway/ under a temp HOME, pinned caches: ok (174s). - gofmt -l clean; go vet (darwin), GOOS=linux go vet -tags nogui, GOOS=windows go vet (with and without nogui) for internal/gateway; go build, GOOS=linux go build -tags nogui, GOOS=windows go build: ok. - Not run: a real signed-in Claude account (no credentials copied), the Windows box, the rest of the repo's packages. |
||
|
|
3691c2047d |
plugin, agent: bun is given a SOCKS proxy through the bridge, so a plugin installs with only a SOCKS system proxy (#1409)
With only a SOCKS system proxy on macOS (127.0.0.1:4401), every plugin
install failed: netproxy.Env put socks5://… in bun add's *_PROXY, and Bun
takes only http:// and https:// proxies, so it stopped with
"UnsupportedProxyProtocol downloading package manifest". The plugin
host's own fetches already went through netproxy.Bridge (hostEnv), but
bunCommand, which runs bun add, update and remove, did not.
- netproxy.EnvForBun rewrites a SOCKS5 HTTPS_PROXY/HTTP_PROXY/ALL_PROXY
(any case) to the loopback HTTP bridge in front of it; others as they are.
- bunCommand uses it, so add, update, remove and the host's run all get it.
- Sibling: an agent CLI installed with bun is updated by `bun add -g`
with the same env (updateEnv in cliupdate.go); npm is left as it is.
- provider-plugins.md says so.
Run:
- TestInstallThroughSOCKSProxy (real bun 1.4.2, fake registry, SOCKS
proxy only): on the old bunCommand it fails with the reporter's exact
"error: UnsupportedProxyProtocol downloading package manifest"; passes
with the fix, through the SOCKS proxy.
- TestEnvForBunBridgesSystemSOCKS (system SOCKS proxy via Env) and
TestUpdateWithBunBridgesSOCKS fail with EnvForBun / the bun branch
broken ("bun is given …socks5://…"), pass with it.
- Real use: a throwaway test with a real SOCKS5 server and only
*_PROXY=socks5://… installed @magpie-community/opencode-cursor-auth
0.2.2 from registry.npmjs.org through it; on the old code the same
failed as UnsupportedProxyProtocol. Scratch HOME, not committed.
- gofmt -l clean; go vet ./...; GOOS=linux go vet -tags nogui ./...;
GOOS=windows go vet ./...; go build; GOOS=linux go build -tags nogui;
GOOS=windows go build; go test -tags nogui ./... (scratch HOME) all ok.
|
||
|
|
0b5b10da2a |
agent, gateway, gui, tui: a Codex that goes through magpie only to fail over to the user's other ChatGPT accounts says so on the Agents row and keeps to its own models (#1385, shenghsi)
With several ChatGPT accounts on, failover writes openai_base_url into ~/.codex/config.toml so a turn the signed-in account can't pay for goes on to the next account. With Codex's switch off, the row said only "not connected", but every request still went through the gateway, and the gateway's /backend-api/codex/models answer added all of magpie's models (62 for the reporter). - agent: Agent.FailingOver means not wired, while the stash's codex.failover (or a CC Switch mirror's codex.mirror_failover) base URL points at magpie. FailoverSaid is the line the CLI list and the TUI show. CodexOwnOnly is true for this machine's Codex when it is failing over and no WSL Codex is connected, since a WSL Codex shares the gateway path. Sync ages a models_cache.json that isn't tagged with the own-only list. - gateway: codexModels leaves out provider.CodexListed when gateway.CodexOwnOnly (set by main to agent.CodexOwnOnly) says so. The ETag and X-Models-Etag carry provider.CodexOwnListTag. Neither tag contains the other, so Codex fetches the list again when it is connected or disconnected. - gui: /api/state carries failover. The row says "Not connected · goes through magpie only to fail over to your other ChatGPT accounts". Its title says what turns it off. A "ChatGPT accounts" button opens Providers › Codex. Strings are in zh, zh-TW, ja and de. openProvider no longer draws the Providers list before it has loaded. It was only safe once the tab had been opened, and this is its first caller from another page. - docs: agent-wiring and gateway-routing describe the failover-only Codex and list the new tests. A user's hand-written openai_base_url (#856) isn't failover's, so it stays a plain not-connected row. Verified: - TestCodexFailoverOnlyKeepsOwnModels (agent) builds the reporter's state in a temp HOME: Codex signed in, two more ChatGPT logins on, Sync, so the stash has codex.failover=1 and no joined/beside. It checks that the row is not wired and is FailingOver, that FailoverSaid names Providers › Codex, and that the real gateway handler's /backend-api/codex/models lists only the backend's gpt-5.5. It also checks that the cached full list is aged, and that after Join magpie's fake/m1 is back. - TestCodexModelListOwnOnlyForFailover (gateway) checks the list and both ETags with the hook on and off. - agent-failover.test.cjs checks the row text, title and button in en, zh, zh-TW, ja and de, at 980px and 420px, in Chromium and WebKit (20 pass). It also checks that the button opens Codex's editor, that the page doesn't scroll sideways, and that no click moves it. - Broken by hand, each test goes red: - codexModels ignoring the hook fails both Go tests. - failingOver returning false fails the agent test. - dropping the Sync cache case fails the agent test. - removing the GUI branch fails the GUI test. - drawing the list before it has loaded (the old openProvider) fails the GUI test. - go test -tags nogui ./internal/agent ./internal/gateway ./internal/provider ./internal/gui ./internal/tui ./internal/codexcat . in a temp HOME: all ok. - node --test agent-*.test.cjs (254 pass) and i18n-keys, gui-ja, gui-de, click-scroll: all pass. - gofmt -l is clean. - go vet -tags nogui on darwin, linux and windows: clean. - go build, and GOOS=windows go build: ok. |
||
|
|
1bc0ed404a |
minimax-code: magpie's models have one slash in the whole ref, so SubAgents take them (#1387, worksyang)
MiniMax Code 3.1.0 checks a SubAgent's model against /^[^/\s]+\/[^/\s]+$/ (uQn in @minimax-ai/code's chunk-2XPMNZVR.js), and magpie wrote its models under custom_provider.magpie by catalog id, so custom_provider:magpie/bb-codex/gpt-6.1-sol was refused (AGENT_CONFIG_INVALID). - The writer keys each model by its catalog id with "~" for each slash (bb-codex~gpt-6.1-sol, provider.FlatID), when exactly one served id spells that way (provider.Flattened); else it keeps the slashed id. - The gateway takes such an id back to that one provider's model or group in validateModel and unprefixed (unflat / provider.Unflat): only an id with "~", no slash, that resolves as nothing else; two ids spelled alike are neither. Never a bare-name first hit. Key limits, middleware and usage see the catalog id, so a key held to bb-codex/* is refused other~gpt-6.1-sol on every route, count_tokens too. - A config an older magpie wrote moves on the next sync: the user's keys on a model go to its new key, defaultModel, defaultLightModel and memory.captureModel/consolidationModel follow, and the slashed key stays with enabled: false, so a session begun on it still resumes (checked with mcode exec --continue). Get still reads magpie/<catalog id>, and Disconnect still puts back the stashed default and variant. Other agents: no installed agent bundle here (cline, command-code, qwen-code, deepseek, pi, omp, codebuff, openclaw) has this regex; native agents were not checked. Verified: - go test -tags nogui ./internal/agent ./internal/gateway ./internal/provider (temp HOME), gofmt -l, go vet darwin/linux/windows. - New TestMiniMaxCodeOneSlash uses the reporter's regex and ids; TestFlatModelIDsReachTheirProvider has two providers serving gpt-6.1-sol. Breaking unflat, mcodeKey, the key carry-over, the legacy enabled:false and the ref rewrite each turned the tests red. - Real run: built magpie serve in a temp HOME on 47812, two fake upstreams; `magpie mcode magpie/other/gpt-6.1-sol` wrote other~gpt-6.1-sol; real mcode 0.6.5 exec reached other's upstream with gpt-6.1-sol (usage: other/gpt-6.1-sol); its agent loader accepted an agent.md with custom_provider:magpie/other~gpt-6.1-sol and refused the two-slash one; `magpie mcode default` put minimax/MiniMax-M2.7 back. |
||
|
|
71ee5e3519 |
gateway: a Claude subscription conversation whose client took its images out goes on in its run (#1382, Songxiaor)
Hana's pre-step hook replaces each image before the last assistant message
with "[图片已省略:历史图片保留为文件引用,避免重复发送原始 base64]" in the
tool result or user message that held it. Over pi-ai's openai-completions
the user message of the results' images ("Attached image(s) from tool
result:" and the images) is gone and each result's text has the line added
to its end. The step after one that read an image missed its run both
ways: tool results failed follows() (the hash of what the run heard), and a
new turn failed turnKey and looseTurn.rewrote (message count changed). A
new claude -p was told the whole conversation: 10 of 67 requests, 79% of
the session's cache writes.
lostMedia (claude_lost_media.go) tells this rewrite by its shape, not by
Hana's string: comparing the conversation a run had (each part kept as the
hash and length of its words, heardRuns) with the request's, a role's run
of messages at a time, it holds when at least one image or file was taken
out and nothing else changed: each image/file is still there, gone, or has
text in its place; a tool result may have text added to its end only when
it carried images or the images after the results went; the caption
before those images goes with them only when a result before it grew.
follows() checks it when the hash misses (toldRuns), and looseTurn.rewrote
when the #912 cut rule doesn't match; the looseTurn key drops the message
count, which the per-turn comparison covers. messageWords is now built from
messageParts, the same words, so the hash and the comparison can't drift.
The shape over the string: any client that strips media the same way is
covered, and a compaction or edit (#459) changes words lostMedia never
accepts. Not covered: a tool result of an image alone, whose pi-ai text
"(see attached image)" is replaced rather than added to.
claude_lost_media_test.go builds its fixtures from pi-ai's literal Chat
Completions bodies (read from the installed HanaAgent server seed, 1.0.0-beta: pi-ai
openai-completions.js and Hana's media-strip hook): a table of 18 shapes
for lostMedia, an end-to-end new turn through serveClaudeSubscription with
the script Claude Code (a tool's image, a user's image), and tool results
after the image step through serveSubscription (goes on; a changed user
message or a changed result still starts anew). The two end-to-end tests
fail with lostMedia unhooked from follows/rewrote; each guard in
lostFromRun was broken by hand and turned a table case red.
docs/subsystems/claude-subscription-bridge.md has the rule.
Run: gofmt -l; go vet -tags nogui ./internal/gateway/ (darwin, GOOS=linux,
GOOS=windows); go test -tags nogui ./internal/gateway/ -count=1 under a
temp HOME (ok); -race -count=5 on the lost-media, rewritten, #459, letGo,
given-up and fork tests (ok). Not tried against a real Hana or a sandbox
magpie with a real claude: the harness's script stands in for Claude Code.
|
||
|
|
3cd0b5f78d |
gui: lightweight mode no longer lets go of a main window that is open under another app's, so Command-Tab back to magpie finds it there (#1381, ttmouse)
Cause: lightweight mode (#580) let go of the main window once two of its 30 s looks found it "not shown", and it asked Wails' IsVisible, which on the Mac is the window's occlusion state. A window the user had Command-Tabbed away from, covered by the other app's window, counted as closed: 30-60 s later its webview was dropped and the window closed, and Command-Tab back to magpie found a blank shell or no window at all. Hidden with magpie (Command-H) it went the same way, since hiding orders every window out. Change: windowUp says whether a window is up as the user sees it: ordered in on any Space, covered or not, minimised, or open as magpie was hidden (watchHide records those on NSApplicationWillHideNotification). Only a window magpie closed itself is down. lighten (through lightShown) and mainShown use it, so the Dock-while-open setting and a restart to update also keep a covered window as open. Elsewhere windowUp is IsVisible as before (visible there covers covered and minimised). Run: - Reproduced with a sandbox build of origin/main (own HOME and CFFIXED_USER_HOME, port 3481, dock+lightweight on): with the open window covered by a full-screen window of a helper process for 70 s the log said "the main window's webview let go" and the window was a blank white shell. With this change, the same run let go only the closed panel and the window kept its page (screenshot of the window). - Activation itself was checked by handing activation from the helper to magpie (NSRunningApplication activateFromApplication): with the window there it came forward. A real Command-Tab keystroke was not sent. - New TestCoveredWindowStaysUp (AppKit, own process): open, covered by another window (until IsVisible says false), hidden with the app, minimised -> up; closed by magpie, also with the app hidden -> down. With windowUp put back to IsVisible it fails: "open: the window counts as up=false, want true"; without the hidden clause: "hidden with magpie: the window counts as up=false, want true". Passes -count=10. - gofmt -l; go vet ./internal/gui; GOOS=linux go vet -tags nogui and GOOS=windows go vet (with and without nogui) ./internal/gui; go build, GOOS=linux go build -tags nogui, GOOS=windows go build; go test ./internal/gui and go test -tags nogui ./internal/gui under a temp HOME: ok. |
||
|
|
2018882e47 |
davsync, gui: a server file that isn't a backup is said as what it is, and can be replaced by this computer's setup (JasonLeeForOnly on Discord)
JasonLeeForOnly's WebDAV sync failed with only "not a magpie backup" for
several versions, and nothing on the page got past it. A WebDAV GET of the
backup can come back as a body that isn't the file in several ways; each is
now reproduced with an httptest server that answers the way the real one
does:
Unpacked (WebDAV and S3): gzip with or without Content-Encoding (a proxy
that drops the header), x-gzip, zstd with or without the header, deflate
(zlib or raw), and a header that says gzip over a plain body. Bodies are
told by magic number; a backup is JSON, so either magic is never the file.
Brotli (br) can't be unpacked and is said as itself, with what to turn off.
Said, with no upload offered: an HTML page answered with 200 (Nextcloud
login after a redirect, Cloudflare Access, an AList/OpenList web app, an
rclone directory listing, a captive portal), with its <title> and the
host/path a redirect ended at (query, fragment and user dropped), and
where the WebDAV address is for 坚果云 and Nextcloud; an XML answer, by its
root element.
Said, with Upload as the way out: an empty (0-byte) file, a backup cut
short or damaged (the automatic rebuild is unchanged and still goes first
when its rules allow), another app's file ("format": "…"), other JSON,
and any other file by its content type. Messages carry the size, kind and a
page title only, never the file's contents.
backup.Open now tells ErrNotBackup (doesn't begin like a backup) from
ErrCorrupt (begins like one, cut short). The status carries the kind as
serverFile; Settings › Sync and backup says it in the reader's language,
wraps it whole, and shows "Upload this computer's setup" only when the file
may be replaced. davsync.Upload (POST /api/davsync/upload, `magpie webdav
upload`, `magpie s3 upload`) asks first in the GUI, re-reads the file,
refuses a backup this computer opens, one sealed with another passphrase,
a newer magpie's, a page and an XML answer, keeps the server's copy in the
sync folder (0600, newest 5) and writes nothing if that fails, then writes
with If-Match on the version read. New strings in en, zh, zh-TW, ja, de.
Verified: internal/davsync/not_backup_test.go (TestReadCompressedBackup,
TestReadBrotliSaid, TestServerAnswerSaid, TestCutBackupSaidAndUploaded,
TestS3ReadCompressedObject; Upload-only parts stripped to compile) fails
on
|
||
|
|
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.
|
||
|
|
3c7473eb3f |
redact: a value masked once is masked again wherever a request has it, whatever words are around it
The masking rules find a value by what is around it: DB_PASSWORD= before
a password, a bound an email may not touch (so an email that ends a
sentence isn't one). The vendor writes about a placeholder in words of
its own ("your password is {{SECRET_…}}", "I will write to {{EMAIL_…}}."),
the agent reads the value there (Restore), and on the next turn that text
is in the history, where no rule matches it, so it went out as written.
Now every value a placeholder was made for (redact's in-memory values,
bounded by maxValues, never written anywhere) is also matched as an exact
string in each string Mask and MaskJSON look at, and goes as the same
placeholder it had (same kind, same keyed hash), so history and vendor
caches stay stable:
- only from 8 characters (the assignment rule's minimum): the 3-character
passwords the user:password@ rule takes, and the user's own words
(which Options.Words matches everywhere already), are not;
- personal values only while personal masking is on, secrets only while
secret masking is on;
- not as a piece of a longer value: a phone or card number with a digit
beside it, an email inside a longer address (a full stop after it may
end the sentence), a secret with a letter or digit against it;
- where a rule finds the same span, the rule's kind still wins, so
nothing masked before changes placeholder;
- in what a vendor signed (
|
||
|
|
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,
|
||
|
|
e4a90aa62f |
groups, routing: a found group the user changed drops a removed provider's members, and a switched-off provider's members are said as switched off (Discord)
A user on Discord: in magpie's found (auto-…) routing groups, a provider
removed or switched off stayed in the group and had to be taken out by
hand; one switched on again should be back by itself.
A found group magpie derives already followed its providers (built from
the providers that are on). The gap was a found group the user had changed
(any edit on its card stores it under its auto-… id with its members):
its members stayed as they were.
Now:
- provider: groupsIn reads a stored auto-… group without the members of
providers magpie no longer has (withoutRemoved). This is read-time only,
never written back, so a provider back under its id (an account shown
again) is back in the group. A manual group's pick and a model a rule
sends to are kept, since taking them out would change where requests go
without asking, or stop the group from saving. If no member would be
left, all of them stay. The user's own groups (other ids) keep their
members as they were.
- A switched-off provider's members stay in any group. The gateway
already skips them (membersIn), and they are routed to again once the
provider is on.
- gui: groupsState marks such a member providerOff, with its provider's
name, icon and model. The Routing page says it as "<provider> · <model>
(switched off)" on the card. A manual group's pick and the editor's row
say why: "{name} is switched off: the group skips it until it is on
again" (new string in zh, zh-TW, ja, de). Before, it was a bare id.
Run:
- go test ./internal/provider TestFoundGroupFollowsItsProviders. It fails
on the old group.go: "c removed, auto-x's members: [a/x b/x c/x]".
- go test ./internal/gui TestGroupMemberOfASwitchedOffProvider.
- New internal/gui/tests/routing-provider-off.test.cjs in Chromium and
WebKit, en/zh/zh-TW/ja/de, at 1100 and 420: 10/10 each. On the previous
assets it fails: the card said "B Cloud · sol" with no "(switched off)".
- The full ./internal/provider and ./internal/gui go test suites, in a
sandbox HOME: ok.
- ./internal/gateway -run 'TestGroup|TestAuto|TestRouting': ok.
- gofmt -l clean. go vet -tags nogui on darwin, linux and windows: ok.
- All routing-*, panel-routing, click-scroll, gui-ja, gui-de, gui-zh-tw
and i18n-keys GUI tests: 265/265 in Chromium and 265/265 in WebKit.
Not tried with real providers in the live app.
|
||
|
|
55544f09f6 |
routing, gui: a provider's accounts and a group in the group fold on the Routing page, long ones folded at first, what the reader picks kept (Aiirobyte on Discord)
Aiirobyte: a routing group of many accounts took so much of the page that editing the groups below it was a long scroll. The stage folded only runs of three or more API keys (#1319). Now: - Three or more accounts of one provider side by side for one model fold into one row the way keys do: the count, the lit one and what it does, how many are available and resting. A request to a folded account flies to the fold. - A group in the group's heading folds from a click, Enter or Space (it is a button now, with a chevron). Folded, it says "{n} on" and what its seats do together; requests under it fly to the heading and a failure tags it. - Accounts and keys sums a provider's accounts up in one row too. - Keys start folded as before. Accounts and group headings start open under six, folded from six. What the reader opens is kept in magpie.routingKeysOpen as before, what they fold in the new magpie.routingFolded, so both directions survive a reload. - Redraws go through steady(), so a click keeps the page where it is. - Strings in en, zh, zh-TW, ja, de. Display only: the gateway's order and choice don't change. routing-account-order now opens the relay's key fold by :not(.open), since the account folds there start open. Run: new internal/gui/tests/routing-group-fold.test.cjs (defaults, click and Enter in place with the page not moving, kept across reloads both ways, a live request to a folded group heading with its tag, Accounts and keys folds, 420px fit) in Chromium and WebKit, en/zh/zh-TW/ja/de, 40/40. On origin/main's assets it fails (no account fold, heading not foldable). All routing-* tests, panel-routing, click-scroll, gui-ja, gui-de, gui-zh-tw and i18n-keys: 255/255 in Chromium and 255/255 in WebKit. Not tried in the real window with a live gateway. |
||
|
|
7137feb4fc |
redact: what a vendor wrote and checks goes back as it wrote it — signed thinking and tool call ids (Jeremy.Zhou on Discord: OpenCode Zen answered 400 "invalid …" until 脱敏密钥 and 脱敏个人信息 were off)
Masking ran its rules over every string of the request except a fixed list
of keys, including two kinds of text the vendor itself wrote and checks
when it comes back:
- A thinking block (Anthropic's signature; also Gemini's thoughtSignature
and Responses' encrypted reasoning). The vendor thinks about the
placeholders it was given, the agent gets the values back (Restore), and
on the next turn the rules masked that text again. Rules depend on
what is around a value: "the password is {{SECRET_…}}" came back as
"the password is hunter2abc1", which no rule matches without its
DB_PASSWORD=, an email ending a sentence fails the email rule's bound,
and a number the vendor wrote of its own ("13800138000") turned into a
{{PHONE_…}}. The text no longer matched its signature, and Anthropic
answers 400 "messages.1.content.0: Invalid `signature` in `thinking`
block" — and the value itself went to the vendor in the clear.
Now an object that carries a non-empty signature, thoughtSignature,
thought_signature or encrypted_content is masked only by putting back
the placeholders of the values masked elsewhere in the same request
(longest first): what the vendor wrote, its own words untouched.
- A chat tool result's tool_call_id. "id", "tool_use_id" and "call_id"
were kept, "tool_call_id" wasn't. GLM's ids are call_ and 19 digits,
and ~5% of them pass Luhn as a bank card number, so the tool message's
id became call_-{{BANK_CARD_…}} while the assistant's tool_calls id
stayed, and the upstream refused the orphaned tool result ("messages
with role 'tool' must be a response to a preceeding message with
'tool_calls'"). Now every key named id, …_id, …Id (and their plurals)
is left alone.
Account masking (账号打码) is display only and doesn't touch requests.
Tests: TestSignedThinkingGoesBackAsSigned (a fake Anthropic upstream that
signs its thinking with an HMAC and checks it, two turns through the
gateway) and TestToolCallIDsGoBackAsGiven (a fake chat upstream that
refuses a tool result with no matching call) fail on origin/main with
exactly those 400s; TestMaskJSONSignedGoesBackAsWritten (Anthropic,
Gemini thought and functionCall, Responses reasoning shapes, plus an
unsigned thinking block still masked) and TestMaskJSONKeepsIDs fail there
too. All pass with the change.
Run: gofmt -l (nothing), go vet -tags nogui ./... on darwin, GOOS=linux
and GOOS=windows, go build -tags nogui (darwin, linux, windows),
go test -tags nogui ./... under a temp HOME: all ok. Not tried against the
real OpenCode Zen, and the reporter's exact error text and model weren't
seen; both upstream checks are modeled on Anthropic's and OpenAI's
documented errors.
|
||
|
|
16dc154589 |
claude: a new Claude Code told a whole conversation is told which turns were answered already and which turn to answer, and the gateway logs that it happened (#1365)
What changed: renderClaudePrompt told a run started anew the conversation as one stretch of Human:/Assistant: text, every image of every turn in it, and the model in #1365 took the earlier images for new ones. When the conversation has a reply that calls no tool, the turns up to it now go in <conversation_history>, with a note that the images and files in them were sent with those earlier messages and need no answer again, and the rest (from the user message after that reply, as nextTurn splits it, historyEnd) in <current_turn>. Images stay in place inside their section; none is dropped. A first turn, a conversation with no reply in it (one-off asks, the auto mode classifier's transcript), or messages ending in a reply are told as before. start logs "a new Claude Code is told the whole conversation" with the message and image counts, after the "tool results <why>" line that says why no run took it. Why: a rebuild can't always be avoided (a run past its idle time with no saved session, a run started anew for the turn and given up on, a conversation the client changed). Its replay should not read as the user sending everything again. Only the text of the one message a new run is told changes, no header or other request field. What was run: - New TestClaudePromptMarksEarlierTurns: the earlier turn and its image are inside <conversation_history>, the current turn's text, tool round and two images inside <current_turn>, and a first turn or reply-less conversation has no markers. With historyEnd's split turned off it fails. - go test -tags nogui ./internal/gateway/ (full package) under a temp HOME, including the existing renderClaudePrompt tests (prompt cache, foreign calls, images); -race -count=20 on the new tests; gofmt -l; go vet for darwin, linux and windows (-tags nogui); GOOS=linux/windows go build. Not tried against a real Claude model: whether it now leaves earlier images alone is untested. |
||
|
|
7ea1cb6743 |
claude: a client that goes away while the run answers its tool results takes the turn back, not the whole conversation to a new Claude Code (#1365)
What changed: ended() kept a turn's id (turnUUID, backKey) only until the reply that called a tool. From then on, a client that went away while the run answered the turn's tool results (a cut connection, or the user interrupting to send a message queued during the tool call) could not be rewound: letGo ended the run. The client's next request, its results sent again or the next turn, logged "tool results no run waiting", and a new Claude Code was told the whole conversation in one message, with every image in it, which the model in #1365 took for new. Now the turn's id lasts across its tool rounds, until a reply ends without calling a tool. letGo rewinds the run to the turn's user message, its tool rounds with it, and the run waits at the conversation before the turn. The next request is resumed there (nextTurn's key is backKey) and told the turn since its user message. resume logs "a turn taken back goes on in its run" after the "no run waiting" line. A run started anew for the turn, or one with a call still in the client's hands, is ended as before. Nothing else sent to Anthropic changes. Why: the issue's log showed "no run waiting" four times, then a whole- conversation replay. On current main a queued message with an image is sent beside the tool_result (checked with Claude Code 2.1.295 against a local mock) and match keeps it with the result, so it does not break matching by itself. The MCP call reaches magpie ~3ms after the tool reply's message_stop, well inside match's 500ms grace. What drops the run is a client going away during a tool-result continuation. The exact client trigger in the report is not confirmed. What was run: - New TestClaudeToolResultsGivenUpOnAreTakenBack (fake Claude Code that makes its call through magpie's MCP callback and answers rewind_conversation): the turn's results are given up on mid-reply, the run is rewound to the turn's message, and the resend goes on in the same process, told the turn and not turn 1's words. With ended() clearing the id as before, it fails: no rewind is asked. - Real Claude Code 2.1.295 against a local mock of Anthropic: rewind_conversation to a user message, sent while the reply to that turn's tool result is streaming, answers rewound:true, and the next request's messages drop the turn and its tool round. - go test -tags nogui ./internal/gateway/ (full package) under a temp HOME; -race -count=20 on the new test and the TestClaudeTurnGivenUpOn* / TestClaudeFirstTurnGivenUpOnEnds tests; gofmt -l; go vet for darwin, linux and windows (-tags nogui); GOOS=linux/windows go build. Not tried with a real Claude account or Claude Desktop. |
||
|
|
3e9bbdaae9 |
gateway: a paused Codex sign-in isn't spent by native compaction either (JasonLeeForOnly on Discord: a ChatGPT account turned off in magpie still had its quota used)
The account Codex is signed in to can be paused in magpie while another of its accounts is on (#263: the user's own ChatGPT account kept for Codex's remote control and Computer Use, another doing the work). Turns on Codex's own models already passed over it (codexAccounts, routing and OwnPaused), but /responses/compact for one of those models still relayed as it came, on the paused account's own token, and on history the others had sealed. compactOn now picks, for that case only, the first of the other accounts on that isn't resting, and codexUpstreamOn relays the compaction signed as that account (its token and chatgpt-account-id, through its proxy), keeping the Accept Codex sent. A gateway key held to some accounts (#905) is asked of the account it goes to, not the paused sign-in. Not paused, nothing else on, an API-key Codex, or a request naming an account: the relay is as before. Not changed: a Codex provider switched off (Off) still relays Codex's own model requests (the Codex app's gpt-5.6-luna/terra side generations, memories, codex-auto-review, native compaction) on the sign-in as they came; that is a design question, reported separately. Tested: TestCodexPausedOwnAccountNotCompactedOn (in use: on the sign-in; paused: acct-2 with its own token; key held to the paused sign-in: 403 with nothing asked; held to the spare: acct-2; the spare off: the sign-in again) fails on origin/main at the paused step (tried [acct-1]) and passes. gofmt; go vet -tags nogui ./internal/gateway on darwin, linux, windows; go test -tags nogui ./internal/gateway ./internal/provider ./internal/agent under a temp HOME pass. Not tried with real accounts. |
||
|
|
bf4d55acdd |
gateway: a Codex task sealed by a custom Responses provider still goes back to it after magpie forgets who answered the lead — a day later, after a restart, or after the lead's reply broke off — and the refusal says how to go on (#1367)
Autsunset (#1367): Codex with the lead and its subagents all on xxxx/gpt-6-astra, a relay of ChatGPT Pro behind new-api. After quota and overload errors, continuing the task was refused before xxxx was asked: "This subagent's task was sealed by the ChatGPT backend that answered its lead ... xxxx/gpt-6-astra is neither." The cause: who sealed a task (#1109, #1237) was read only from the affinity stick of the lead's conversation, and that record is lost in three ways: it is kept 24 hours; a restart reads back only the latest 512 conversations from affinity.json; and a reply that breaks off after its 200 (response.failed server_is_overloaded) deletes it (unanswered, #733). Once lost, every later turn carrying a sealed message was refused before anyone was asked, so nothing could answer the lead again to remember it: the task stayed stuck. When the sticks don't make the asked provider a reader, the usage log's answers to the lead's thread and the request's own conversation in the last 30 days now count too (loggedAnswers). A refused or failed call, a title and Codex's own sign-in (Provider "openai" on /backend-api/codex, still told of as the ChatGPT backend) count for nothing. The sibling leadFirst, which puts the lead's ChatGPT account first in a group, reads the lead's key or account from the log the same way. The refusal names who answered the lead when the log knows, and both messages now say how to go on: use the lead's model, or have the lead spawn the subagent again. Verified: - TestSealedTaskStillGoesToTheLeadsProviderAfterItsReplyBrokeOff and TestSealedTaskStillGoesToTheLeadsProviderADayLater (fake Responses relay over httptest, Fernet-shaped gAAAAAB... payloads, lead and worker on xxxx/gpt-6-astra) fail on origin/main with the reporter's exact 400 and pass with the change; the sealed payload reaches xxxx as it came. - TestSealedTaskLogDoesNotOpenItToAnotherProvider: a lead answered by another provider names it; a lead never answered (refused twice) and an answer older than 30 days stay refused. With the 30-day bound removed it fails. - TestSealedTaskGoesToTheLeadsLoggedAccountFirst: with the leadFirst fallback removed the group's other account (acct-1) is tried first. - TestSealedTaskFromChatGPTLeadStillRefused fails if Codex's own sign-in is not left out of the log's answerers. - TestSealedReplyRefusedWhereTheLeadWasNotAnswered now expects the lead moved to another provider to be named "(third)". - go test -tags nogui ./internal/gateway passes (sandbox HOME, pinned caches); sealed tests pass under -race -count=5; gofmt -l clean; go vet -tags nogui ./internal/gateway for darwin, linux and windows. Not verified: real Codex with a real CPA/new-api relay of ChatGPT Pro. If such a relay sends the subagent's request to another ChatGPT account than the one that sealed the task, that account can't open it either; magpie can't see or fix that. |