Koohoko: on a routing group in Ultra or normal mode, every subagent Codex spawned sometimes failed with "An OpenAI lead sent a sealed subagent task that a Magpie-served model cannot read". A group with a Codex (ChatGPT) account in it has that member's turns passed through as they came, encrypted marks on spawn_agent's message and all, so when it answered the lead the ChatGPT backend sealed the task (gAAAAA…), and the subagent's request on the same group was refused before routing although the group had an account able to read it: sometimes, as it depended on which member answered the lead. The check moves from codexBackend into serve: a request carrying a sealed agent task goes only to ChatGPT accounts (sealedReader, p.Account.Agent == codex) of the group or model, and the account that answered the lead (its affinity under the parent thread, x-codex-parent-thread-id or the turn metadata's parent_thread_id) goes first while the subagent's own conversation isn't kept anywhere yet, as another account may not open what it sealed, as it doesn't another's reasoning. With none in the group or model it is turned away as before, before any rule or classifier, now saying a group with a Codex account in it would do. TestCodexSealedAgentHandoffGoesToGroupsChatGPTAccount (group xai/m then codex/gpt-5.5, lead pinned to the spare account) fails without the change (400) and without the lead-first part (acct-1 asked). Whether another ChatGPT account than the sealing one can read the task isn't known; not tried with a real account. go vet ./internal/..., go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui -count=1 ./... under a temp HOME pass.
* fix(agent/codex): recognize CC Switch official OpenAI mirror for grouping and failover (#504)
CC Switch writes Codex's built-in OpenAI provider as model_provider = "custom"
with a [model_providers.custom] table having name = "OpenAI", requires_openai_auth = true,
and no base_url. Because model_provider was not empty or "openai", Codex's models
were filed under a group named "custom" instead of "OpenAI", and account failover was
skipped.
Recognize a model_provider table without a base_url and named OpenAI as the official
provider for model option grouping and account failover.
* fix(agent/codex): route CC Switch official mirror failover through table base_url (#504)
Codex ignores openai_base_url when model_provider = "custom" is set, so
setting openai_base_url during account failover caused requests to bypass
magpie and fall back directly to chatgpt.com.
Instead, point the mirror table itself at magpie (base_url = at.codexURL())
during failover, and remove base_url when failover turns off, returning the
config byte-identical to the original. Exclude the official mirror from
takeTables so relays remain distinct, and preserve the original picker
grouping logic for magpieID and openai.
* fix(agent/codex): clear mirror failover marker when switching table and preserve relay base_url
Distinguish decision-only providers from gateways that also serve conversations. Only Jev models use the decision API; ordinary models remain in the catalog, automatic groups and conversation routes. Keep the full model cache when probing a mixed provider’s decision endpoint, and allow conversation model tests in its editor.
Verified with go vet -tags gtk3 ./..., go test -tags gtk3 ./..., node --check internal/gui/assets/app.js, and an isolated browser fixture.
Amp-Thread-ID: https://ampcode.com/threads/T-01a0fe05-afd7-70df-97cf-fdb69c4f4680
Co-authored-by: Amp <amp@ampcode.com>
* fix(factory): open Anthropic requests with Droid's line
Factory only accepts a subscription request whose body opens as Droid
(#242, #506). Responses and chat completions already do. Anthropic
Messages now puts that line in the first system block, as droid2api
does, and keeps the agent's own prompt after it.
* fix(factory): serve Gemini on Factory's generate route
Every agent already reaches Factory's Claude, GPT and open models:
the gateway translates its own API into the model's, and the body
opens with Droid's line. Gemini was the wire still missing.
droid2api posts those models to /api/llm/g/v1/generate and prepends
the same line to systemInstruction. Factory now does that too.
The models are the ones droid 0.232.0's CLI registry still offers
(gemini-3.1-pro-preview, 3.8/3.7/3.6/3.5 Flash, gemini-3-flash-preview).
2.5 and Gemini 3 Pro Image are availableInCLI false there and stay out.
They are not Droid Core. A streamed Gemini reply comes back in the
client's own API.
TestFactoryFromEveryAgentWire posts Claude Code, Codex, Pi, OpenCode
and Gemini CLI at Claude, GPT, GLM and Gemini. TestFactoryGeminiStreamsToChat
reads a Gemini SSE chunk back as chat completions. Not tried against
a real Factory account.
* fix(factory): open other agents with droid2api's system prompt
droid2api's config.json system_prompt is Droid's line and a blank line,
written in front of the agent's own prompt on Responses, chat, Messages,
and Gemini generate. A body that already opens with the line is unchanged.
* fix(factory): answer non-streaming Gemini clients with JSON
Factory's generate route always returns SSE and droid sends no stream
field. A generateContent call is translated into JSON; a relayed
streamGenerateContent drops stream before it goes upstream. The provider
Test probes generate instead of failing the protocol, and a Gemini model
is tested on that route. The system prompt is joined into the one
systemInstruction part droid sends.
* fix(factory): join Droid's opening with one newline
droid writes the line and one newline in front of the agent's prompt.
The extra blank line from droid2api's system_prompt is dropped.
Anthropic still keeps the line as its own first block.
* fix(factory): send Droid's line alone when there is no prompt
droid joins blocks with "\n", so a request with no system prompt is
the line and nothing after it. Responses, chat and Gemini were
sending the line plus a trailing newline. Anthropic already sent
the line alone.
The base .pop.proto-menu set max-height:none with no overflow, so a
proto menu grew to its full height. The menu-bar quota picker lists one
row per account, so a subscription with several accounts made its menu
taller than the window: it ran past the bottom edge, the last
subscriptions were unreachable, and scrolling did nothing because the
box itself never scrolled — openProtoMenu's flip-up only fires when
there is room above, and near the top of Settings there is not.
Clamp the base proto menu to min(360px,70vh) with overflow-y:auto and
overscroll-behavior:contain, as its siblings (.sess-menu,
.lan-address-menu) already are, so a long list scrolls within the
window instead of overflowing it. This covers every openProtoMenu
caller, including the Search-API vendor picker, which had the same
latent overflow.
proto-menu-overflow.test.cjs opens the quota picker with enough
accounts to overflow a 600px window and checks the menu stays inside
the viewport and scrolls to the last row; it fails without the clamp
(the menu's bottom lands at 1071px) and passes with it.
In the GUI, an agent magpie is in has "Disconnect from magpie" (断开 magpie(还原配置)) in its row's menu (its logo, or a right-click; beside Apply again), with a tooltip saying what it does, and in its model picker right under Default, so the two aren't taken for each other; the logo's title says the menu has it. It asks first in the app's own dialog (askDisconnect, as askReset and askForgetSaved do; no native confirm), and the row is then drawn from the answer: the model the agent had, no longer wired, and the status line says so. Every new string has its Chinese. The drag grip stays hover-only; no click moves the page.
What it leaves: Codex keeps magpie's [model_providers.magpie] table, as its own-model pick does, for threads started on it; with several ChatGPT accounts in magpie, Codex failover may write openai_base_url again on a later sync. A relay's own ANTHROPIC_MODEL in Claude Code's env was never stashed, so it doesn't come back; modelSettings effort entries for magpie models may stay in settings.json.
Tests: TestCodexDisconnect (own provider, own provider signed in, nothing set: the top keys as before, the user's provider table untouched, no subagent model, no catalog file, no stash, not wired, no drift), TestClaudeDisconnect (own relay and nothing set: settings.json as before), TestGeminiDisconnect, TestDisconnectUnwiredLeavesConfig, TestAgentDisconnectAPI; internal/gui/tests/agent-disconnect.test.cjs (WebKit, English and Chinese: only a wired agent has it in the menu and the picker, the in-app dialog, Cancel posts nothing, Disconnect posts and the row shows the old model, no native dialog, the page doesn't move). click-scroll, agent-grip, agent-layout, agent-models, agent-model-groups, model-pick, favorite-star, claude-picker-fold and panel-effort pass under WebKit; menu-scroll fails on /api/plugins/updates unmocked, as it does on main. go vet ./internal/..., go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui -count=1 ./... under a temp HOME pass. Not run against a live agent config.
The copy fallback skipped symbolic links and then removed the source,
permanently losing them when a session moved to or from the trash across
volumes. Copy each link with its original target and propagate link errors
so a failed copy leaves the source in place.
Cover relative, absolute, directory, dangling and nested links, a root link,
and destination conflicts. The copy regressions fail without this change.
The group offered no levels: its levels were what every member has, and a member whose levels magpie doesn't know (a Token Plan's deepseek-v4-pro-202606, which models.dev doesn't list) has none, so the intersection was empty and models.json had the group "reasoning": false. A model a provider was given that its vendor's models.dev list leaves out (TokenHub's deepseek-v4-pro, its list holding the Hy models alone) also had none in the catalog, where the gateway (Provider.Efforts) fits efforts to DeepSeek's low, high, max. Magpie's Pi field offered all seven levels whatever the model.
- provider.Exposed: a picked model the vendor's list doesn't have takes the levels Provider.Known gives it (knownElsewhere), not none.
- groupEntries: a member levelsUnknown (no levels, no account list, its vendor and maker don't list it, models.dev knows no model of its id — catalog.Knows) doesn't narrow the group's levels; the gateway sends it the effort as asked (fitEffort). A member listed with none still narrows them.
- Pi/OmO thinking field: for a model of magpie's, Options are the levels Pi offers for the entry magpie writes (piSupported, as pi-ai's getSupportedThinkingLevels), and Get shows a level it doesn't offer as Pi's clamp of it (piClamp).
Now Pi at max on that group sends reasoning_effort "max", relayed as max to both members (DeepSeek's levels have max; the unknown one is sent it as asked).
New tests: TestGroupLevelsWithUnknownMember (provider), TestPiGroupThinkingIsWhatPiRuns and TestPiClamp (agent); each fails without the change (group entry "reasoning": false; catalog levels []; field offering all seven). TestMoveCursorEfforts gives its model levels, since Pi is offered off alone for one without. go vet, nogui/linux/windows builds and the full test suite pass but the known flake TestFXInState.
"Add from folder" pointed at a skill already in ~/.config/magpie/library/skills emptied every file of it: InstallSkills' symlink failed (it was there) and its fallback copied the folder onto itself, O_TRUNC first. A folder from elsewhere by a name the library's folder already had was copied into it the same way. Such a folder is now refused ("…is in the library's folder already: bring it in from the skills found there"), anything by that name in the library's folder is never written into, the copy fallback runs only when the symlink failed for another reason, and copyDir refuses a destination that is its source or inside it.
A skill put in the library's folder by hand, not in library.json, was shown nowhere: foundSkills now scans the library's folder too, first, and such a one gets FoundSkill.Library (magpie's links to it in the agents count as the agents having it). Bringing it in lists it where it is: nothing moved or linked, Source nil for a folder of its own, {folder, its real path} for a link. The found row says "in the library's folder, not listed" with its path, and Bring in that it is listed where it is (en, zh).
Codex, Gemini CLI and the others that read ~/.agents/skills themselves got a ~/.codex/skills/X link to a library skill whose real folder is in ~/.agents/skills, and saw it twice. A user's own entry there (not magpie's link) that is the very folder the library's is now counts as the skill being in the shared folder: no link of their own is made, magpie's made before are taken away, and a byte copy of their own is kept aside with the backups as before. Nothing in ~/.agents/skills is moved or removed. SkillView.Always names the agents that read that folder, and their chips are lit, disabled, and say "Codex reads ~/.agents/skills itself, where this skill is kept — it has it whatever is ticked here" (en, zh); a click on another chip or All neither sends nor darkens them.
A skill's kind and "linked from" are now what its library entry is, not what was written when it came in: a linked folder moved in by hand is the library's own, shown without "linked from", and its Remove says the folder goes to the backups, as removeSkill does (which now also counts a junction as a link, as the page does).
TestAddFromLibraryFolderKeepsIt (the skill's folder and the library's skills folder as the source, another folder by that name, copyDir onto and into itself: bytes unchanged), TestFoundInLibraryFolder, TestSharedSkillNotLinkedTwice and TestSkillKindIsWhatTheFolderIs are new and fail without the change (the first with SKILL.md and run.sh emptied); TestSharedAgentsSkills now asserts Codex and Gemini CLI get no second link (Pi keeps its) and are Always. library-own-folder.test.cjs (WebKit, en and zh) is new and fails without the change but for its Remove case; library-all, library-items, library-loading, library-row-open-scroll, library-toggle-status, click-scroll, import-all-skills, own-skill, every-skill, remove-all-skills, by-agent and market-have pass in WebKit. go vet ./internal/..., go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui -count=1 ./... under a temp HOME pass.
* agent: configure Zed models and gateway credentials automatically
Add Zed to the CLI and Agents page with an OpenAI-compatible magpie provider, a synced model catalog and default model selection. Preserve existing JSONC settings and restore the previous provider and default model on reset.
Store the gateway credential in Secret Service, macOS Keychain or Windows Credential Manager so Zed needs no manual API key entry. Cover config preservation, catalog sync, model limits, Flatpak paths and credential failures.
Verified with the full nogui test suite, go vet, macOS and Windows cross compilation, and a local Zed conversation through the gateway.
Amp-Thread-ID: https://ampcode.com/threads/T-01a0fd03-00d7-75fd-956f-c505e07ba60d
Co-authored-by: Amp <amp@ampcode.com>
* agent: store Zed credentials directly through Secret Service D-Bus
Replace the Linux and FreeBSD secret-tool subprocess with the existing godbus client. Open a session, create the default collection when needed, unlock it and replace the matching Zed credential. Handle collection, unlock and item prompts, dismissal and timeout.
Verify the protocol on isolated D-Bus instances with no helper executables on PATH, including immediate completion signals and failed writes. The full nogui suite, targeted vet and race checks, macOS and Windows cross compilation pass; local credential readback confirms replacement without duplication.
Amp-Thread-ID: https://ampcode.com/threads/T-01a0fd03-00d7-75fd-956f-c505e07ba60d
Co-authored-by: Amp <amp@ampcode.com>
* docs: remove the separate Zed configuration paragraph
Amp-Thread-ID: https://ampcode.com/threads/T-01a0fd03-00d7-75fd-956f-c505e07ba60d
Co-authored-by: Amp <amp@ampcode.com>
* agent: fix Zed reset, keychain access and translated notices
Avoid restoring magpie’s own provider after a model change in Zed, and cover reselection followed by reset. Remove the unsupported Flatpak config path.
Authorize the installed Zed app and security command to access the macOS gateway credential. Translate Zed’s notice and apply translation when selecting or reapplying an agent model.
Verified with the full nogui test suite, go vet, Zed race tests, cross compilation and browser checks. macOS keychain behavior still needs runtime verification on a Mac.
Amp-Thread-ID: https://ampcode.com/threads/T-01a0fd70-98a1-737f-93bd-77e181f904c1
Co-authored-by: XYenon <register@xyenon.bid>
* agent: preserve Zed model picks when resetting magpie
Restore the stashed default model only while Zed still selects magpie. Preserve a model chosen in Zed itself and discard the stale model stash, while restoring the previous provider.
Find Zed Preview in Applications folders and Spotlight without requiring a CLI, and use any in the isolated D-Bus tests.
Verified the reset regression fails before the fix and passes afterward. The full nogui suite, go vet, Zed race tests and macOS/Windows test cross compilation pass.
Amp-Thread-ID: https://ampcode.com/threads/T-01a0fd70-98a1-737f-93bd-77e181f904c1
Co-authored-by: XYenon <register@xyenon.bid>
* agent: retain Zed provider backup when reselecting magpie
When Zed’s provider already points at the gateway, update only the model stash and preserve the original provider backup. Reset can then restore the user’s provider after a native model pick followed by another magpie pick.
Add a regression covering the full sequence and checking the restored provider, model catalog, custom headers and latest native model. Confirm it fails before the fix and passes afterward; all Zed tests, the full nogui suite and go vet pass.
Amp-Thread-ID: https://ampcode.com/threads/T-01a0fd70-98a1-737f-93bd-77e181f904c1
Co-authored-by: XYenon <register@xyenon.bid>
---------
Co-authored-by: Amp <amp@ampcode.com>
DashScope (qwen-cn, qwen) answers 400 "InternalError.Algo.InvalidParameter:
The value of the enable_thinking parameter is restricted to True." for the
models of its own that always think (glm-5.3, glm-5.3-prime). Claude Code's
session title and its auto-mode classifier ask without thinking, on the
session's model (the title request carries max_tokens 32000 and no thinking
field), and magpie's Anthropic passthrough turns thinking off for a
non-Claude model unless the request says otherwise (#250) — so both calls
came back the 400, and the retry that asks again with thinking left to the
model knew only Z.ai's ("…always engages in thinking…") and OpenRouter's
("reasoning is mandatory…") words for that refusal, not DashScope's. The
400 reached the agent as it was: Claude Code reads a failed title call as
no title and a failed classifier call as no verdict, so a session on
DashScope's glm-5.3 never got its title (Lody, which dispatches Claude Code
through magpie, showed it as every child session keeping its draft title)
and auto mode blocked every action it meant to check.
alwaysThinks learns DashScope's words ("enable_thinking … restricted to
true"); the request is asked again with its thinking left to the model,
once, as for Z.ai. Another 400 is the agent's to see.
Tested with TestAnthropicPassthroughDashScopeThinkingRestricted, a fake
upstream answering 400 exactly as DashScope does when the request says
thinking disabled and 200 for the one with it left to the model, with the
unrelated 400 still not asked again; TestAnthropicPassthroughModelThatAlwaysThinks
and TestOpenRouterMandatoryReasoningRetries pass as they were. go vet,
go test -tags nogui ./internal/gateway/, GOOS=linux and GOOS=windows builds
pass (internal/netproxy's TestBunThroughBridge fails the same on origin/main
here, its SOCKS check). Against the real DashScope: through the stock
gateway Claude Code's title request (Agent SDK 0.3.284, claude-code
2.1.284, qwen-cn glm-5.3) returned null — no title; through the patched one
the session was titled. glm-5.3-prime answers the thinking-left request as
well, its thinking inside the title call's own 32000 tokens.
Reject duplicate keys and multiple documents instead of silently editing
them. Validate rendered output before writing so edits cannot leave
dangling aliases or introduce duplicate keys. Errors leave files untouched.
Apply the same validation to YAMLText.
JSONToYAML now rejects duplicate keys without creating or replacing the
target, including omp's models.json conversion. Treat a document containing
only --- as empty, matching the top-level YAML editor.
Add regression tests for document loss, dangling aliases and duplicate
keys, alongside existing-behavior coverage for malformed input.
The model picker (gatewayModels) and the fallback picker
(renderFallback) both iterated every provider's models regardless of
its Off flag. The backend already refuses an off provider's models
(providerEntries skips !p.On(), so /v1/models never lists them), and
the routing page's own providerEntries and the fallback picker's
ready check already honour the off gate - but the two pickers above
still offered ids the gateway would turn away, showing a model the
picker says works but a request to it cannot use.
Both now skip a provider whose Off is set, matching the backend's
behaviour so the picker and the gateway agree on what is usable.
Co-authored-by: Claude Code <noreply@anthropic.com>
The menu bar's account-in-use card (trayInUseCard via InUseLogin)
picked the account by sign-in state alone: Active or first, unless
paused. The gateway moves its requests as soon as an account's
allowance is spent (Allowances, SpentShare), but the sign-in follows
only every few minutes (SwitchWhenSpent, codex/claude only). Between
those - and for every other multi-account provider - the card showed
the exhausted account while traffic flowed through another.
InUseLogin now weighs the leading account against the same allowances
the gateway routes by (Allowances + loginSwitching's SpentShareOf).
When it is known spent and another account is on with room, that one
is reported instead. An allowance not known is never taken for spent,
so a single account, an unread one, or a failed read keeps the
signed-in account as before.
Tests: the existing inUseOf table gains a room parameter (nil-safe);
new cases pin the follow-the-gateway behaviour and its guards;
TestLoginRoom verifies the wiring against injected allowances.
Co-authored-by: Claude Code <noreply@anthropic.com>