Commit Graph
49 Commits
Author SHA1 Message Date
yetone 588e51ea4c agent, gui: an agent connected by its config to an address where nothing answers is told, not shown connected (#1013, cjyrainbow: Codex Desktop's WSL environment reads Windows' ~/.codex/config.toml, whose openai_base_url and [model_providers.magpie].base_url the reporter pointed at Windows as WSL sees it, forwarded to 127.0.0.1:3425 by a portproxy of their own; after a reboot the portproxy listened no more and the Agents page still said Codex was connected) — Drift looked only at the config, which was right. Now an agent pointed at an address off loopback (this machine's Codex at an address the user kept, #816, through Agent.reach; every WSL twin under NAT, at Windows as the distro sees it, through Agent.Gateway) has that address tried from here (reach.go: GET /v1/models, any HTTP status an answer, 2 s timeout, in the background, at most every 20 s per address, never waited for), and one that doesn't answer while the gateway answers on loopback is a new drift, "unreachable", with Addr and the advice in Detail: under WSL (or on Windows) magpie with Settings › Share on local network on, or the user's netsh interface portproxy, which may need setting up again after a restart; Windows' firewall allowing the port for the WSL network; on Windows 11, networkingMode=mirrored; elsewhere, the forward to start again or gateway.URL() to point at. An answer not had yet, and a gateway down on loopback too, are no drift. The config is never changed for it, except where the address is one a WSL distro reached Windows at before (distro.Was, the last 4 kept in wsl.json as a re-probe finds Gateway changed) and the distro reaches Windows at another now: Drift's Move names that one, and Reapply moves the config there (Agent.move; Codex's codexMoveGateway rewrites openai_base_url and magpie's own provider's base_url, only where they begin with the old address). An address no distro was seen at is the user's own and is never offered a move. In the GUI the row's line says "{agent} is set up, but nothing answers at the address it reaches magpie by" in red with the advice as its tooltip, and its pill is "How to fix" (an i, not the reapply arrow), which opens the advice in a dialog (too long for the status line; it wraps and can be copied) and sets nothing — or "Use <address>" (the existing "Use {url}" string) when there is a Move, which reapplies; two new t() strings with zh, ja and de. The probe runs from Windows, not from inside WSL, so a firewall keeping WSL out isn't seen; the advice names it. TestKeptAddressNotAnswering (the reporter's case: Codex at 172.28.96.1, unreachable with the address and loopback in the detail; no drift once it answers, none with the gateway down too), TestKeptAddressProbeNeverWaitedFor, TestKeptAddressMovedWithWSL (Move from a Was address, Reapply rewrites both URLs, Check empty and no drift after; a 10.0.0.7 address is told but never moved) and TestWSLAgentAddressNotAnswering (Codex in a NAT distro told, with Share on local network, portproxy and firewall; a mirrored one never probed) fail without the Drift change and pass; TestLoopbackNotProbed, TestReachProbe (httptest: a 401 is an answer, a closed port none) and TestWSLWas pass. TestMain stubs reachProbe so no test dials out. -race -count=10 on them; go test -tags nogui ./internal/... under a temp HOME, go vet (darwin, windows), gofmt -l, linux and windows (nogui) builds pass. agent-unreachable.test.cjs (the window at 560px and the tray panel at 440px: the red line and tooltip, How to fix's dialog posting nothing, Use <address> posting the reapply, no sideways scroll) fails on main and passes on Chromium and WebKit in en, zh, ja and de; agent-connect, agent-layout, agent-field-names, aside-native, gui-de, gui-ja and i18n-keys pass on both. Not tried on a real WSL NAT setup (the Windows test box didn't answer ssh). docs/subsystems/agent-wiring.md says it. 2026-10-06 19:17:55 +08:00
forever a7c4e5ddde provider: a Codex account the Codex app still sends for keeps Codex signed in — spend cap or overage with credits, limit_reached alone; a hold survives a failed read (#996)
* provider: a Codex account at a spend cap or past its overage with credits left keeps Codex signed in, as the Codex app's composer still sends for it

#966 held such an account (and one whose limit_reached alone was true),
following the readiness check Tht in ChatGPT.app 26.930. Tht has one
caller, the work-credit-restriction-recovery poller; it isn't the
composer's gate. The composer's submit is disabled only by hP (search
"n.rate_limit?.allowed!==!1||Jpe(n)||SIe(") and hardBlocked (search
"hardBlocked:r.rate_limit?.allowed===!1"): both read rate_limit.allowed
alone and let credits (has_credits or unlimited) through before any spend
cap or overage. So the app sends for that account, and holding it gains
nothing: CodexUsedUp only moves Codex to magpie as provider while Codex is
on a magpie model, whose turns already reach magpie through
openai_base_url. It only takes the app out of ChatGPT (durable threads,
remote control), the loss #966 set out to stop.

codexHeld is back to that gate: held when allowed is false, no credits,
and not a workspace within its overage (no spend cap, an ordinary
reason). Its comment names the build, both gates and why Tht isn't one,
each with a string to search for. The spend-cap and overage subcases now
expect not held; new subcases pin allowed as the only trigger. Five
subcases fail on the merged codexHeld. agent-wiring.md and reference.md
say the same.

Follow-up to #966.

* provider: a Sci, Law, FinServ or Enterprise trial/view-only workspace within its overage keeps Codex signed in

codexHeld lets a workspace with no credits through while it is within
its overage, as the Codex app's composer does, but the workspace list
came from codex-rs's PlanType alone. The app's own plan_type switch for
its usage banner (ChatGPT.app 26.930.61225, app-initial) also names sci,
law, finserv, enterprise_cbp_trial and enterprise_cbp_view_only, so such
an account read held and magpie took Codex off its ChatGPT sign-in while
the app still sent. They are added; the comment cites that switch. guest
there is a personal plan and stays out, and a plan not listed still
counts as no workspace (held).

Verification: "Law without credits, within its overage" in
TestCodexUsedUpIsWhatTheAppHolds fails without the list change
(CodexUsedUp = true, want false) and passes with it.

* provider: a held Codex reading with no window at 100% stays held through a failed read

A reading kept for when /wham/usage fails dropped Held whenever none of
its windows read 100%. That is right for a hold that came with a used-up
window which has since started again, but a hold with no window used up
(the backend says not allowed, no credits, every window under 100%) has
no reset to wait for: the first 503 after it read not held and put Codex
back on its ChatGPT sign-in for an account its app sends nothing for.
Held is now cleared only when the saved reading had a window used up and
none is after the passed resets are applied; any other hold stands till
a fresh read.

Verification: TestCodexHeldWithNoWindowUpKeptTillAFreshRead (the real Pro
reply, credits spent, its window at 40%: a good read, then a 503) fails
without the change ("CodexUsedUp = false on the kept held reading, no
window used up") and passes with it. TestCodexHeldKeptOnlyTillTheReset
still passes; both share their setup now. -race -count=10 passes.

* agent, gui, docs: Codex's sign-in notes say magpie steps in while the Codex app blocks the account, not whenever it is used up

#981's sign-in notes said magpie becomes Codex's provider "while its
account is used up". Since codexHeld, a ChatGPT account at 100% with
credits left keeps Codex signed in; magpie steps in only while the Codex
app blocks the account (out of allowance, no credits left), and the notes
now say that, Always ChatGPT's to match, in English, Chinese, Japanese
and German. agent-wiring.md's sign-in line and reference.md's Codex
paragraph say when the app holds an account as codexHeld reads it: OpenAI
no longer allows it and it has no credits left, a workspace still within
its overage excepted; at 100% with credits Codex stays signed in.

Verification: codex-signin-always (Chromium and WebKit, en/zh/ja/de, 1100
and 440px) 8/8, i18n-keys 1/1, gui-ja 3/3, gui-de 3/3. git grep finds the
old English notes nowhere under internal/gui/tests or internal/gui/assets.
2026-10-06 15:28:41 +08:00
forever 8005f17551 provider, codex: a ChatGPT account at 100% with credits left keeps Codex signed in (#966)
* provider, codex: a ChatGPT account at 100% with credits left keeps Codex signed in — magpie becomes its provider only while the Codex app itself holds the account

magpie made itself Codex's provider (model_provider = "magpie", #540)
whenever any of the active ChatGPT account's windows read 100%. A Pro
account at 100% on its week with credits left keeps working in the Codex
app: the backend answers every turn (about 490 200s in one day's usage log
at 100%), yet magpie moved Codex off it, and as a provider without
requires_openai_auth the app lost its ChatGPT state ("Sign in to ChatGPT
to start a durable thread", no remote control).

CodexUsedUp now reads codexHeld, which mirrors the Codex app's own
composer block (ChatGPT.app webview bundle, hP and its helpers): the
backend says the account isn't allowed (rate_limit.allowed = false), it
has no credits (has_credits / unlimited), and it isn't a workspace still
within its overage (overage_limit_reached = false, no spend cap, an
ordinary rate_limit_reached). Window percent never decides it. Where the
app goes by what /wham/usage can't show (a statsig gate, the gpt-reserve
fallback, Team's reserve experiment that ignores overage), it counts as
held: a wrong move costs ChatGPT extras, a missed one every turn. A
reading kept through a failed read (keepLast) stops saying held once its
used-up window has started again, as its usage does. NextLogin's account
rotation and its thresholds are unchanged.

A user stuck on the old move (codex.out = 1) is put back beside the
sign-in by the next Sync, as #540 already does once the account has room.

Verification:
- TestCodexUsedUpIsWhatTheAppHolds (real /wham/usage replies, ids
  replaced) fails on the old trigger: 7 subcases, "CodexUsedUp = true,
  want false". Each workspace guard (spend cap, reason, overage, plan
  list, Team reserve) was mutated and fails its own subcase.
- TestCodexHeldKeptOnlyTillTheReset fails without the quota_last change.
- The real Pro account (7-day window 100%, ~28k credits, allowed false):
  CodexUsedUp true before, false after.
- go vet ./...; go build (darwin), GOOS=linux -tags nogui, GOOS=windows;
  go test -tags nogui ./... under a sandbox HOME; -race -count=20 on the
  new and related Codex tests.
- Not run: a live Codex app turn on an account truly held (none at hand),
  Windows/WSL runtime, GUI suites (no GUI change).

* provider: a Codex account at a spend cap or past its overage is held, credits or not; limit_reached counts as not allowed

codexHeld let credits through before the spend cap, so a Team seat at its
cap (spend_control.reached, allowed false) with has_credits read not held
and magpie left Codex on an account the backend refuses. The app's
readiness check (26.930.61225, app-primary) returns limited on
spend_control.reached or credits.overage_limit_reached before it looks at
credits, and treats rate_limit.limit_reached like allowed false; held now
reads the same. A spend cap with the rate limit still allowed (a
workspace's included usage) stays not held.
2026-10-06 14:50:39 +08:00
forever 7f45f45cb5 codex: a sign-in kept on ChatGPT when its account is used up (#981)
Codex's sign-in, ChatGPT, keeps Codex signed in to ChatGPT with magpie's
models beside its own, but once the ChatGPT account is used up magpie
makes itself Codex's provider (#540): the Codex app sends nothing at all
on an account it knows is out, a magpie model's turn included. That move
went unsaid, and a user who chose ChatGPT expected Codex to stay there.

ChatGPT now says it, and a third way, Always ChatGPT (login "chatgpt"),
never makes the move, by a pick or by Sync; choosing it puts a Codex moved
before back beside the sign-in. What it costs is said beside it: the Codex
app may send nothing till the account has room, while Codex CLI's turns
still reach magpie by the base URL, its routing and other accounts serving
them.
2026-10-06 13:31:16 +08:00
yetone 17f790b145 agent: five package-level twins nothing calls are gone — isCodexGateway, ccProviderJSON, fxProvider, hermesProvider and writeMiniMaxEntry
Each was a one-line wrapper of its *At function for the local gateway.
The callers moved to closures of the same name over the agent's own
address (at.v1(), at.gw()), which shadow the package function, so the
wrappers were reached by nothing, tests included (deadcode -test). Their
doc comments now sit on the *At functions, and isCodexGatewayOn has its
own again: it had been left above codexKeptGateway.

go vet (darwin, GOOS=linux nogui, GOOS=windows) and go test -tags nogui
./internal/agent under a temp HOME pass.
2026-10-06 04:04:47 +08:00
yetone ab47042ba8 agent: Codex on one of magpie's models beside its ChatGPT sign-in stays connected when Codex writes one of its own models back, and connecting keeps the model it was on through magpie where it can't join (#940, xybio)
Picked "via magpie" on a ChatGPT-signed Codex, a magpie model is set beside the sign-in: openai_base_url to the gateway, the magpie table, no model_provider. When Codex itself (its /model, or a Codex app still running on its old pick) then wrote one of its own models back (gpt-6.1-sol), Wired found nothing to say magpie had put it there, so the row read Not connected while its config still pointed at magpie's gateway. Set so, Codex keeps a codex.beside mark (gone when magpie becomes its provider, on a pick of its own through the picker, or on disconnect), and Agent.Beside makes Wired true on it; the model written back is still told as a change from outside (#750's drift stays). Connecting where Codex can't join (its ChatGPT allowance used up, or a model_provider of its own) went to the first of magpie's other models (Grok 4.7, "magpie doesn't serve the model it was on"), since Codex's field leaves out magpie's codex/* models: Agent.OwnVia now finds the model it is on as magpie serves it on the user's ChatGPT account (codex/gpt-6.1-sol), and Connect keeps it there.

TestCodexOwnPickAfterMagpieModelStaysConnected and TestCodexConnectKeepsItsOwnModelThroughMagpie are new; with the change taken out the first reads the reported config (model gpt-6.1-sol, openai_base_url, [model_providers.magpie], no model_provider) as not connected and the second connects to fake/m1 "first". Tested in temp HOMEs only. go vet, go build (GOOS=linux nogui, GOOS=windows) and go test -tags nogui ./... under a temp HOME pass (internal/proc TestUserPath timed out waiting on a login shell under load once, and passes on its own).
2026-10-06 02:30:31 +08:00
yetone 528d6c12dc agent, gui: Codex left on model_provider = "magpie" with no [model_providers.magpie] loads its config again, and connecting with nothing in magpie says what to do, in the reader's language (Tystem on Discord) — the Codex app said "无法加载 config.toml … Model provider magpie not found", magpie showed Codex as not connected on "deepseek-flash", and its switch answered only "Codex can't be connected to magpie", in English. Codex looks model_provider up in [model_providers.*] as it loads config.toml (codex-rs: "Model provider {id} not found"), so the key without the table keeps it from loading any config, a new thread too, not just an old one. magpie never writes one without the other: it takes model_provider out whenever it steps out as the provider (disconnect, Unwire, a model of Codex's own, Join) and keeps the table so the threads started on magpie still open. That is unchanged, and TestCodexSteppingOutLeavesNoHalfState now covers it. Another tool or a hand edit had taken the table away. Sync, at magpie's start and whenever the catalog changes, now writes the table back whenever the config names magpie as its provider, at the top or in a profile, and has no table, whether Codex is connected or not. Check reports that state too. A config magpie can't parse is still only reported, never written to. A Connect that finds none of magpie's models because no provider or subscription has been added answers agent.NoModelsError, "Add a provider or subscription in magpie first, then connect Codex". /api answers it with code no_models and the agent's name, which app.js says through t() (en/zh/ja/de). docs/reference.md no longer says the table goes with a native model. TestCodexProviderWithoutTable (top and profile: Check says so, Sync writes the table) and TestConnectWithNoModels in internal/agent, TestAgentConnectNoModels in internal/gui, and agent-connect-no-models.test.cjs (WebKit, en/zh/ja/de: the toast in the reader's language, the switch back off, no scroll, no native dialog) are new and fail without the change; the latter's en case passes either way, as the server's message is English. Chromium wouldn't launch on this machine for this test or the existing agent-connect.test.cjs. TestCodexSyncAddsBaseURLToProvider now expects the table added to a hand-made provider that had none. go vet, go build (darwin, GOOS=linux nogui, GOOS=windows) and go test -tags nogui ./... under a temp HOME pass. In the first full run, internal/gui's TestProvidersAnswerWhileListsComeIn and internal/proc's TestUserPath failed under load; both passed alone and in the second full run. 2026-10-05 21:09:13 +08:00
yetone eafb1845e8 agent, gui: Codex's row has a square for the model it writes its memories with (Yc on Discord) — Codex sums each thread up with gpt-5.6-luna and consolidates its memories with gpt-5.6-terra (medium), whatever model it runs, and nothing in magpie could change that. Codex reads both from config.toml's [memories] extract_model and consolidation_model (codex-rs/config/src/types.rs; the defaults are memory_extraction_preferred_model / memory_consolidation_preferred_model in model-provider). Codex's row now has a memories square beside the subagents' one, opening the app's model picker: a model picked writes both keys, Default removes them (and a [memories] table left empty), the user's other [memories] keys stay. A model of magpie's is offered and taken only while Codex runs through magpie, as for subagents, and is removed when Codex steps out of magpie or is disconnected, since Codex could no longer find it. Unset, its title says what Codex uses. en/zh/ja/de. TestCodexMemoriesModel and TestCodexMemoriesUnwire (temp HOME) fail without the change; codex-memories.test.cjs (WebKit, en and zh: the square and its unset title, no square with nothing to pick, the model picker with Default first, a pick posted and lit, Default posts "", no select, no scroll, no left border) fails without it. subagent-effort, click-scroll, codex-titles, agent-start-pick and desktop-tiers pass in WebKit. go vet, go build (darwin, GOOS=linux nogui, GOOS=windows) and go test -tags nogui ./... under a temp HOME pass. 2026-10-05 19:11:34 +08:00
maYandyetone 6fe8169502 gateway, agent: Codex draws with its native image tool through magpie (#870)
* fix(agent): give magpie's provider table the actor-authorization header

Codex offers its built-in image tool (image_gen.imagegen) — and the other
OpenAI-provider tools — only to a provider it reads as the OpenAI actor:
ModelProviderInfo::uses_openai_actor_authorization(), i.e. a provider that
does not require OpenAI auth but carries x-openai-actor-authorization.
magpie writes [model_providers.magpie] but carried neither signal, so a
Codex on magpie was never given the tool and could not draw through a
Codex backend such as sub2api's.

Write http_headers = { "x-openai-actor-authorization" = "magpie" } into
that table. magpie does not forward the header upstream: codexHeader
rejects anything naming "authorization".

Test: TestCodexMagpieProviderCarriesActorAuthorization.

* gateway/provider: let a Responses-only relay (Codex backend, sub2api) draw

A provider added as 'OpenAI Responses' only (what a Codex backend such as
sub2api speaks) has no chat base, so Drawers() returned nil before ever
looking at the vendor's list, draw() refused with 'can't draw', and
drawImages() posted to an empty base. The vendor's own /v1/models does list
its image models (sub2api serves gpt-image-2.5), and they are meant to be
picked in Settings → Images.

- Drawers: a Responses-only provider draws when it has any OpenAI-shaped
  base, not only a chat one.
- draw: refuse only when neither Chat nor Responses is set.
- drawImages: fall back to the Responses base when there is no chat base.
- List: the add form's 'Fetch models' keeps the vendor's image models so
  one can be picked while the provider is added.

Tests: TestResponsesOnlyProviderDraws, TestListOffersImageModels.

* fix: route Codex native images through its turn provider

---------

Co-authored-by: yetone <yetoneful@gmail.com>
2026-10-05 17:19:42 +08:00
yetone e77a373792 agent: every agent in a WSL distro under NAT is given the LAN sharing key, not "magpie" (whqtian on Discord) — v0.1.972 gave it to omp only, and OpenCode in WSL was still written "apiKey": "magpie" with Windows' address, which the gateway takes from loopback only. keyAt(gw) is the key for a gateway URL: the LAN sharing key when the URL isn't this machine's and sharing is on, else gateway.Token; OpenCode (and MiMo Code), Crush, Pi/omo, Codex, Claude Code, Kimi Code, Hermes, Grok, MiniMax Code, Droid, Morph, dsh, Qoder/Qoder CN and omp write it, their Checks compare against it (an old "magpie" there shows as drift), and ourKey still sees a LAN-keyed config as magpie's. TestGatewayKeyBeyondLoopback fails without the change. go vet ./..., go test -tags nogui ./internal/agent ./internal/gui ./internal/gateway ./internal/access ./internal/provider under a temp HOME, GOOS=linux/windows go build pass. 2026-10-05 15:19:09 +08:00
yetone 1d70277385 agent: Codex keeps an openai_base_url the user wrote by hand (sbtxdy, #856) — openai_base_url = "http://127.0.0.1:3425/backend-api/codex" written in ~/.codex/config.toml by hand, Codex on a model of its own and signed in to ChatGPT, was gone again each time magpie started (after every update): Sync's failover, which points Codex's own models at magpie while another of its ChatGPT accounts is on there and takes the base URL away when none is, took it for its own and removed it. Failover now marks the base URL it writes (codex.failover in the stash) and takes away only that one; one already there while failover is on is taken as its own, as a magpie from before the mark wrote it. Connecting Codex in magpie (Join), a magpie model, and disconnecting are as before. TestCodexHandWrittenBaseURLStays fails on the old code (Sync left only model = "gpt-5.5"); TestCodexOwnModelThroughMagpieWhileAccountsOn and the other TestCodex* pass. go vet, builds (darwin, GOOS=linux nogui, GOOS=windows) and go test -tags nogui -count=1 ./... under a temp HOME pass, but for TestClaudeAccountNamedAsItsLogin and TestClaudeIdentityServedWhileAsked (5 s timeouts under the full run's load), which pass run alone. 2026-10-05 12:45:16 +08:00
yetone bfcb7b0647 agent, gui: every agent's picker lists its own models and says which way each goes; a hidden row's Show is centred and grips show whole (EZN7L2C3 #834, Hu9956 #842)
#834: after v0.1.931 Claude Code's row showed its current model and a full
picker, but Grok Build and Antigravity CLI hadn't caught up and Codex's
list had no "via magpie". Grok Build listed none of its own models
unless a Grok subscription was signed in in magpie. Now it lists:
- the models it last listed itself (models_cache.json, hidden ones left
  out, by their names);
- those magpie listed for a Grok subscription;
- the user's own [model."<id>"] tables.
Each says "direct, not via magpie" (xAI, or the table's host).
Antigravity CLI's own custom models say direct, to Anthropic, OpenAI or
Google by their apiProvider. Codex's own models say "direct, not via
magpie" (OpenAI, or the user's own provider) when not routed, and
"via magpie" (Option.Via) while its base URL or its provider points at
magpie's gateway.

#842: under the Agents page's fold, a hidden agent's Show sat on its
name's line, about 9px above the row's middle. It now stands in the
row, level with the row's picker. A grip's dots, hover-only, were cut:
their box was 5px wide with columns 3.5px apart, so the right column was
half gone. This was most visible on the Usage page's cards beside the
logo. The Agents, Providers and Usage grips' boxes now hold both
columns whole: 6px, columns 3px apart, 1px further left. Checked in
screenshots (dark theme, Qoder CN card).

Tests:
- TestOwnModelsSayTheirPath covers:
  - Codex not routed, routed by base URL, and on a provider of its own;
  - Grok's cache, hidden model and own table;
  - agy's custom model.
- agent-own-path.test.cjs and fold-show-grip.test.cjs (WebKit, en and
  zh) fail on the old code:
  - no tag on Codex's routed own model;
  - Show 9px above the picker;
  - grip w 5 < its two columns.

agent-start-pick, agent-grip, show-unset-agent, usage-arrange,
usage-quota-head, click-scroll, provider-order, import-row,
agent-layout, desktop-model-menu, agent-leave-magpie and
usage-card-refresh pass on WebKit. go vet and the builds pass (darwin,
GOOS=linux nogui, GOOS=windows). go test -tags nogui -count=1 ./...
under a temp HOME passes; TestCloudflareDecideModelsTested failed once
in the full run and passed on rerun, alone and in its package.
2026-10-05 09:32:04 +08:00
yetone cd217c174b agent: Codex Desktop with WSL counts as connected when its gateway address is the one the user set for WSL, and magpie keeps that address (#816) — with Codex Desktop's "run in WSL" on, Codex runs inside WSL: the absolute Windows path magpie wrote for model_catalog_json didn't load there, and 127.0.0.1 is WSL's own loopback, not Windows'. After the user pointed openai_base_url and [model_providers.magpie] at the Windows host's address, magpie's check still said 未接入 (it asked for its own loopback URL), and a re-wire or sync would have put 127.0.0.1 back. On Windows magpie now writes model_catalog_json = "magpie-models.json", which Codex resolves against CODEX_HOME on either side (checked with Codex 0.160), and still reads the absolute path it wrote before as its own. A gateway address on a non-loopback host, on magpie's port path and with magpie's token, is taken as magpie's: Check passes, the agent is Routed, and setting a model or syncing keeps the address; a relay's address is not taken. TestCodexWSLAppAddressKept (fails on the old code with "no longer points at magpie's gateway") and TestCodexKeptGatewayOnlyMagpies. go vet, builds (darwin, GOOS=linux nogui, GOOS=windows) and go test -tags nogui -count=1 ./... under a temp HOME pass. 2026-10-05 02:03:10 +08:00
yetone 7cc44b68a9 agent: Codex on a routing group's model name is still connected, not "switched off it outside magpie" (#750, Windows 11, magpie 0.1.812, Codex 0.160.0: Codex set to group/auto-gpt-6-1-sol in magpie, its config.toml then reading model = "gpt-6.1-sol" with openai_base_url still magpie's gateway, and the Agents page kept saying 在 magpie 之外被从 … 切走了 with 重新接入, coming back a while after each reconnect) — the gateway takes a model's name without a provider in it as the group of that model (provider.GroupFor: gpt-6.1-sol is group/auto-gpt-6-1-sol), so Codex's requests still went to the group; but Drift compared the field with what magpie recorded setting (applied.json) by string, and gpt-6.1-sol is neither that nor one of magpie's models by magpieValue (isMagpie wants a provider/model id, and Codex's options list it as its own or not at all), so it read as "replaced". What writes gpt-6.1-sol into config.toml is not magpie (no agent path writes a group's model name; set writes the group id as picked) and wasn't found — the Codex app persisting its thread's model by its own name is the likely one — which is why it came back after each reapply. Drift now passes over a field whose value and magpie's recorded one reach the same routing group, one by the group's id and one by its model's name (sameGroup, groupNamed: a group/… id as it is, "magpie/" taken off, else GroupFor); Codex's internal wired() and Agent.Wired (through a new Agent.Routed hook, Codex's routed(): openai_base_url or magpie as its provider) count the group's model name as magpie's only when magpie set that group on the field and Codex's config still sends its model to the gateway, so a Codex taken off magpie on gpt-6.1-sol is not connected, and a Codex's own gpt-5.5 that happens to have an auto group is no more magpie's than before (failover's base URL is untouched). Disconnect takes the field off the group's model name as it does off the group's id (back to Codex's default), rather than leaving Codex asking OpenAI for gpt-6.1-sol. Another group's model (gpt-6.1) or one no group has (gpt-5.5) is still "replaced". The reporter's ask to say which setting differs: the drift's detail already names it ("model is …, not … as magpie set it", the base URL or provider table for unwired) in the status line's and the Apply again pill's tooltip; that is left as it is. Tests: TestCodexGroupByItsModelName (two providers serving gpt-6.1-sol and gpt-6.1, signed in with ChatGPT and with an API key: group/auto-gpt-6-1-sol set, then config.toml's model rewritten to gpt-6.1-sol — Wired and no drift; no route to magpie — not wired; gpt-6.1 and gpt-5.5 — replaced) and TestCodexGroupByItsModelNameDisconnect fail on the old code (not wired, drift replaced, gpt-6.1-sol and the base URL left after Disconnect) and pass. go vet, builds (darwin, GOOS=linux, GOOS=windows) and go test -tags nogui -count=1 ./... under a temp HOME pass. Not tried on Windows or with the reporter's Codex and groups; not checked: what in Codex rewrites the model to gpt-6.1-sol. 2026-10-04 13:11:23 +08:00
yetone c47fee0b26 agents: Codex keeps its own last pick when connected, and every connected line starts 「已接入 ·」 (owner: 让 Codex 记住上次的选择,要统一)
Connecting Codex wrote a start model (it picked stepfun/step-1-32k, a dead one, for a ChatGPT sign-in). Signed in with ChatGPT, Codex now joins magpie by the base URL alone (Agent.Join): openai_base_url to magpie's gateway, magpie's provider table for the threads started on it, CC Switch's tables taken over, the models cache aged — and model untouched, the one last picked in its /model. Agent.Joined keeps it Wired while it is on a model of its own; failover leaves a joined Codex alone; a model of its own picked in magpie keeps it connected; Disconnect leaves Codex on the model it is on rather than the one before connecting. An API key or a provider of its own still connects on one of magpie's models, as its own can't be reached through magpie's provider.

The connected line for agents picking in their own list (Codex, OpenCode, …) read 「120 个模型在 OpenCode 的 /models 里」; now 「已接入 · OpenCode 的 /models 里有 120 个模型」, as the others.

TestCodexConnectKeepsItsPick fails on the old code (connected on fake/m1). Real Codex in a sandbox HOME with the owner's ChatGPT account: connect left model = "gpt-5.4" (Codex's), wired, no drift; codex exec on gpt-5.5 and on deepseek/deepseek-v4-flash both answered through magpie. go vet, builds (darwin, GOOS=linux, GOOS=windows) and go test -tags nogui -count=1 ./... under a temp HOME pass; agent GUI tests pass but cli-update, menu-scroll and old-webkit, which fail on main too.
2026-10-04 02:05:10 +08:00
yetone 5cf4d1dae1 codex: on magpie API, Codex's own model as the default keeps magpie as its provider and magpie's models in its picker (#701, nullburn: Codex 选回原生默认模型后,第三方模型目录消失(已选择 magpie API)) — with Codex's sign-in set to magpie API, picking one of Codex's own models (gpt-6-luna) as its default in the Agents row ran unroute(): model_provider, model_catalog_json, openai_base_url and magpie-models.json all went, so every third-party model left Codex's picker while the row still said magpie API; and choosing magpie API while on Codex's own model wired nothing (login.Set only re-ran set for a magpie model). Now, on magpie API, Codex's own model goes as magpie serves it on the ChatGPT account there (codexOwnViaMagpie: gpt-6-luna -> codex/gpt-6-luna, the entries shown to Codex first), so set() takes the magpie branch and only the default changes; the picker's OpenAI group offers those ids under their own names (Label gpt-6-luna) so the value matches; one magpie doesn't serve (the ChatGPT subscription off in magpie) is refused with a message naming both ways out, the config untouched, rather than quietly unwired; choosing magpie API on Codex's own model wires it at once; Sync wires a config an older magpie left so (#701's: model = "gpt-6-luna", sign-in api). Back on ChatGPT, a codex/… default turns into Codex's own model again, straight to OpenAI as before (codexOwnOf); the ChatGPT sign-in's paths are otherwise unchanged. TestCodexAPIKeepsMagpieOnOwnModel (api on an own model, magpie model then own model again, picker values, refusal, back to ChatGPT) and TestCodexAPISyncWiresOwnModel fail on the old code and pass; TestCodexProviderAlsoSetsBaseURL's api case now expects magpie to stay wired on Codex's own model. End to end in a temp HOME with a copy of a real models_cache.json: api, fake/m1, then gpt-6-luna leaves model = "codex/gpt-6-luna" with model_provider, model_catalog_json and openai_base_url, and codex-cli 0.160.0's codex debug models with CODEX_HOME on that sandbox (gateway ports pointed at :1) lists fake/m1 beside codex/gpt-6-luna and the other codex/ models. go vet, builds (darwin, GOOS=linux, GOOS=windows) and go test -tags nogui -count=1 ./... under a temp HOME pass. Not tried in the Codex app or with a turn sent through a real ChatGPT account. 2026-10-03 23:40:06 +08:00
Luis Araya 49ee7d82d9 fix(agent/codex): recognize CC Switch official OpenAI mirror for grouping and failover (#504) (#579)
* 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
2026-10-03 06:48:49 +08:00
yetone dfc074afdb agents: Disconnect from magpie takes out everything magpie wrote into an agent's config and puts back what it had before (Fate on Discord: 移除由Magpie写入的配置,也就是重置功能,这个好像不是很明显。初步体验好像是选择默认就会清除写入的配置吗?希望能有一个更直接的交互动作,来代表移除由Magpie写入的配置。) — there was no one action for it, and picking Default isn't it: Default is the agent as installed, and for Codex, Claude Code and Gemini CLI it also forgets the stash, so the user's own provider or relay (model_provider + its table, ANTHROPIC_BASE_URL/AUTH_TOKEN, GOOGLE_GEMINI_BASE_URL/GEMINI_API_KEY and sign-in) was lost rather than put back. agent.Disconnect reuses each agent's own restore path: a new Agent.Unwire, for those three, runs the unroute their own-model pick already used (codex.go and claude.go have it pulled out of set as unroute; gemini.go's was there) and then forgets the stash, and every other field magpie set — each one reading a magpie value, or the value applied.json recorded and Unwire left — is set to "" through its own Set, which for every other agent unstashes what it had. applied.json forgets the agent (Keep), so no drift says it was switched off magpie. Agent.Wired (magpie or a magpie model in any field) goes to the GUI as agentJSON.wired; an agent magpie isn't in is left byte for byte. POST /api/agents/disconnect/{id} runs it under edit.Atomically as Sync and the fields' Sets are.
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.
2026-10-03 02:42:09 +08:00
yetone 27f0e4ebdd codex: when the ChatGPT account runs out of its allowance after a magpie model was picked, magpie becomes Codex's provider, and goes back beside the sign-in when the allowance is back (#540: once the Codex 5-hour/weekly quota is used up, a third-party model through magpie sends nothing in new or old sessions; Dazzle-sys: the session was on the magpie model all along, the quota went elsewhere) — the Codex desktop app refuses to send for a ChatGPT account that is out of its allowance before any request is made, whichever model the turn is on, so a magpie model beside the sign-in (openai_base_url, ChatGPT mode) is blocked with it; Codex's open-source core and TUI only report a usage limit after a response, the block is the app's, keyed on the account. bd5bf422 made magpie the provider (model_provider = "magpie", model_catalog_json; the app-server's getAuthStatus then answers requires_openai_auth false, the app's API state) only when a model was picked while the account was already out, so an allowance running out later left Codex as it was. KeepOnAnAccountWithRoom now also looks, every 5 minutes, whether the active Codex account is out (CodexUsedUp, now read through LoginUsage's cache, fetched at most once a minute, as the agents' sync asks it on every catalog sync) and tells the agents' sync when that changes (noteCodexUsedUp → catalog.Touched); Codex's Sync then sets the magpie model again, which wires it as the provider while the account is out, marked codex.out in the stash, and puts it back beside the sign-in once the allowance is back, only where it was moved for that (a provider asked for with sign-in "api" stays). Threads started on Codex's own provider and reopened may still be held by the app; the Codex app may need a restart to take the new config. agent's TestMain stubs codexUsedUp so no test asks OpenAI. TestCodexRunsOutAfterPick (ChatGPT mode, then out: provider; out again: stays; back: beside the sign-in; api stays) fails without the change; TestNoteCodexUsedUp checks only a change is told. No request was made to OpenAI. 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. 2026-10-02 16:48:26 +08:00
yetone 4c69ff336b agent: Codex's subagents can start at an effort of their own, and Claude Code's subagents can have a model of their own (#468, #469) — Codex reads [agents] default_subagent_reasoning_effort, the effort a spawned subagent starts at when the spawn names none (openai/codex core/src/agent/child_config.rs, config_toml.rs; in codex 0.159.2), and magpie never wrote it. Codex gets a subagent effort field: a square beside its subagents' one that opens the effort slider, unset said to mean the session's effort, or the subagent model's own default. Codex refuses a spawn at a level the subagents' model lacks, so only one it has is taken, and one a new subagent or main model lacks is dropped. Claude Code takes the Agent call's model, then the agent's frontmatter, then CLAUDE_CODE_SUBAGENT_MODEL, then the session's (2.1.287); magpie wrote that env only by itself, the main model while every tier followed it. It gets a subagents field, offered once routed through magpie, whose model outlives a new main model and a tier of its own; unset, the env is written as before. Claude Code has no setting for its subagents' effort beyond each agent's own frontmatter, and omp's task role already takes a thinking level, so neither changes. TestCodexSubagentEffort, TestClaudeSubagentModel and subagent-effort.test.cjs fail without the change. go vet, go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui ./... pass; subagent-effort, omp-roles, claude-effort, panel-effort, agent-layout, model-pick, click-scroll, old-webkit pass. 2026-10-02 03:18:26 +08:00
yetone 953bce2cfb agents: Codex signed in with only an API key gets magpie as its provider, its picker listing magpie's models (#322, dakango: switching the Codex app to one of its own models went to OpenAI, 502) — magpie counted an API key in auth.json as a ChatGPT sign-in and only set openai_base_url, counting on Codex to ask the gateway for its models; but Codex asks for none with an API key (models-manager refresh_available_models returns early), so the picker showed Codex's built-in models and the one magpie model, and a built-in one went to OpenAI with the relay's key. Now only a ChatGPT sign-in takes that route; with an API key alone magpie is the model_provider with model_catalog_json listing every magpie model on for Codex, as when signed out, and a sync moves a setup an older magpie wrote; going back to Codex's own model restores the user's model_provider. A Codex model whose OpenAI can't be reached now fails naming the model and what to do instead of a bare 502. TestCodexAPIKeyUsesProvider, TestCodexAPIKeyBaseURLMoved and TestCodexOwnModelUnreached fail without the change. go vet, go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui ./... pass. 2026-09-30 20:08:27 +08:00
yetone 6c5ed2acf5 agents: CC Switch's provider tables in Codex's config go through magpie too, and the Library brings in every skill found at once (JasonLeeForOnly on Discord: after moving from CC Switch, Codex threads still went to the relay, "model not found" for a magpie model) — CC Switch writes a [model_providers.custom] table (cc-switch, cc-switch-N in older versions) and moves every third-party Codex thread onto it, and a reopened thread takes the provider it was saved with, not model_provider; magpie left those tables pointing at the relay. While a magpie model is on, signed in or not, their base_url is magpie's /v1, the relay's kept in the stash and the table's other keys as they were; a table a profile names, and cc-switch-official, are left alone; Codex's own model or a reset puts the URL back, unless the table was changed since; one pointed away again is named on the agent row and taken back on sync. The Skills tab's "In your agents" gets Bring in all (2+ found), one library write, one that fails named and the others brought in. TestCodexTakesCCSwitchTables, TestImportSkills and import-all-skills.test.cjs (Chromium, WebKit, en/zh) fail without the change. go vet, go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui ./... pass; click-scroll, old-webkit, shared-skills pass. 2026-09-30 18:16:22 +08:00
yetone 98cbea0ffb agents: Pi's models go to PI_CODING_AGENT_DIR (#304), and the Codex app offers magpie's models' efforts (#310) — Pi keeps settings.json and models.json in PI_CODING_AGENT_DIR when set (~ expanded; @earendil-works/pi-coding-agent config.js), and magpie wrote ~/.pi/agent regardless; omp is read by its own rules (PI_CONFIG_DIR, OMP_PROFILE/PI_PROFILE, then PI_CODING_AGENT_DIR), and the library's Pi and omp targets follow. The Codex app's model picker offers only the efforts in [desktop] enabled-reasoning-efforts of config.toml (ChatGPT.app 26.924: default low…xhigh, ultra, persistent), so a magpie model's max or minimal was never offered; routed through magpie, the efforts its models take are added to the user's list (or the default), nothing else changed, only values the app knows (it drops the setting for any other), comments kept, nothing written when nothing is missing. TestPiAgentDir, TestOmpAgentDir and TestCodexDesktopEfforts fail without the change. go vet, go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui ./... pass. 2026-09-30 18:09:09 +08:00
yetone e31384b1f9 gateway: Codex on magpie as its provider has its approvals auto-reviewed again (shihao H on X: 自动审批未执行 … 404: magpie knows no model "codex-auto-review") — Codex asks for its guardian review by OpenAI's reviewer model, codex-auto-review, whatever the provider ("independent of current login"), and magpie, knowing no such model, answered 404, which Codex reports as the review service failing. A model Codex names that magpie does not serve now goes to the model Codex is set to (config.toml model), when magpie is its provider, as Codex itself reviews with the turn's model when its catalog lacks the reviewer; the Routing view shows it as the Approval check. Signed in, Codex's own names go on to OpenAI as before. TestCodexStandIn fails without the change; TestCodexAutoReviewStandIn has the Codex request go to the stand-in. go vet, go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui ./... pass. 2026-09-30 16:55:50 +08:00
yetone 3df75514b9 agents: each agent's own model list — under an agent's name on the Agents page, "Showing 5 / 32 models" (or "All 41 models") opens a list to take models out of that agent's picker one by one and put them back (@ChongkaiX on X: 在 magpie 里选哪些模型出现在 Codex 的模型列表里) — until now an agent's list could only be narrowed by whole providers or families (Visible). settings.HiddenModels holds, by agent, the catalog entries taken out, after Visible: a deny list, so a new model is shown. provider.CatalogFor drops them, so the gateway's /models, the agents' files and pickers and Codex's catalog all follow in one place, and catalog.Touched rewrites the agents' files; Codex's own ChatGPT models are dropped from the backend's list too (CodexNativeHidden), and the list's ETag and models_cache tag carry them (CodexListTag) so Codex asks again. The model an agent is set to is marked 在用 and never taken out, on the server as well. The list groups routing groups first, then providers with their logos and on/total, a long group folded; search, 全部/已显示, a group's 全部显示/全部隐藏 on hover, 恢复全部显示; a click on a row applies at once, Esc or a click elsewhere closes it, and nothing scrolls. A renamed provider's hidden models follow it. Tested with agent-models.test.cjs in Chromium and WebKit, English and Chinese; TestHiddenModels (provider), TestCodexModelListHidesOnAgentsPage (gateway) and TestAgentModelsAPI (gui) in sandbox HOMEs. click-scroll, panel-fold, old-webkit, model-pick, menu-scroll, add-button pass; GOOS=linux go vet, linux and windows builds, go test -tags nogui ./... pass. Not tried in the real Wails window or against a real Codex. 2026-09-30 12:44:59 +08:00
Zhaofeng 99a6c0d354 codex: a subscription switched off narrows nothing, its models kept whole (switching the codex provider off left the one picked ChatGPT model as the only one Codex listed) — the codex provider's picks narrow the ChatGPT account's own model list as well as magpie's added models (CodexNativePicked, aed12ac), and ownCodex narrows the picker's OpenAI half the same way. Neither looked at Off, so a provider switched off — which serves no agent anything, by Provider.Off's own promise — still narrowed the account's list to the picks it kept, and a single pick left Codex listing that one model alone. Both now treat a switched-off provider as picking nothing: the account's list is left whole, and the picks stay in providers.json, narrowing again as soon as it is switched on. Tested with a new TestCodexModelListNotNarrowedWhileOff in internal/gateway (a fake backend listing two models; switched off with picks=[gpt-6-sol] both come through, switched on just the pick; fails on main with [gpt-6-sol]) and a new TestCodexOwnModelsNotNarrowedWhileOff in internal/agent (Codex's own picker: both models while off, just the pick while on; fails on main with [gpt-b]). go build -tags nogui ./..., go vet ./internal/agent ./internal/provider ./internal/gateway and go test -tags nogui -count=1 ./... pass (#274) 2026-09-30 12:24:56 +08:00
yetone fd1427de0b codex: with magpie as Codex's provider, openai_base_url points at magpie as well, so a thread started on Codex's built-in provider and reopened sends a magpie model to magpie, not to ChatGPT's backend (#259 isymbo: ChatGPT Desktop, "The 'xiaomi/mimo-v2.6-pro' model is not supported when using Codex with a ChatGPT account")
ChatGPT Desktop 26.924.22138 is the Codex app renamed (bundle com.openai.codex, codex-cli 0.158.0-alpha.2.1): it reads ~/.codex and honours openai_base_url, model_provider and chatgpt_base_url; the error text comes from chatgpt.com. Codex keeps a thread on the provider it was started on, so an old ChatGPT conversation reopened while magpie is Codex's provider (signed out, allowance used up, or the sign-in set to api) stays on the built-in provider, and magpie removed openai_base_url in that mode, so a magpie model picked in it went to chatgpt.com, which refused it. Provider mode now sets openai_base_url to magpie's gateway too (whose /backend-api/codex serves "provider/model" and passes Codex's own models on), unless the user has one of their own; picking one of Codex's own models or a reset still removes magpie's. Sync adds it to configs an older magpie wrote in provider mode, when the catalog file is magpie's. Verified by driving ChatGPT Desktop's bundled `codex app-server` in a sandbox HOME with a fake ChatGPT auth.json and fake servers: before, a thread started on the built-in provider and reopened with model_provider = "magpie" went to the built-in base URL; now it reaches magpie's /backend-api/codex/responses, and a new thread /v1/responses. Tests: codex_thread_test.go (TestCodexProviderAlsoSetsBaseURL signed out and api, TestCodexProviderKeepsOwnBaseURL, TestCodexSyncAddsBaseURLToProvider; the first and third fail without the change), codex_route_test.go updated for the base URL in provider mode. go vet, go build -tags nogui (and GOOS=linux, GOOS=windows) and go test -tags nogui ./... pass. Not tried in the real ChatGPT Desktop window or with a real account; a thread already open in a running app keeps the settings it was loaded with, and cloud threads never read the local config.
2026-09-30 11:16:59 +08:00
yetone d37b821908 toml: Codex's config.toml with a multi-line array (notify = [ … ]) is edited right, and an edit that fails leaves every file as it was (#253 kerwinliu00001: "edited TOML is invalid: line 2, column 1: incomplete number", config.toml left with model_provider gone and [model_providers.magpie] added but no model) — the top-level key edits (GetTOMLTop, SetTOMLTop, DelTOMLTop) went line by line with regexes: a new key went after the last line that looked like key = …, which for notify = [ was inside the array; key = value lines inside a multi-line string were read as keys and a line starting with [ as a table; replacing or deleting a multi-line value touched only its first line. They now locate the top-level keys with the TOML parser, as the table edits already did, each with its value's whole range however many lines it takes (parseTOMLFile, the unnamed root table beside the tables); SetTOMLTop finds all its keys in the file as read and writes once. A file the parser refuses is still read line by line, so a broken config keeps showing its model, and writes to it are refused untouched. And a pick is several writes (Codex: drop model_provider, add magpie's provider table, set openai_base_url and model), each checked, so one failing left the earlier ones on disk: edit.Atomically(fn, paths…) puts each file back to its bytes and mode, or removes one it created, when fn fails, and every field set and Sync of Codex (config.toml and magpie-models.json, so Codex in WSL too), Kimi and Grok goes through it, as do Codex's MCP server writes (putCodex, delCodex). Tested in temp dirs and a temp HOME: the reporter's file read, set and deleted directly and through Codex's model field (signed in, with model_provider and model above the array, with magpie as the provider), the array's text kept and the TOML valid, and back to a Codex model; nested arrays, trailing commas, comments in arrays, [x] and a = b in strings, an inline table in an array; """ and ''' strings holding model = … and [table] lines not read as keys, new keys after their closing quotes, one deleted whole; a forced failure of Codex's last write leaving config.toml byte-identical; TestAtomically. They fail on main (the Codex ones with the reporter's error). go vet ./internal/..., go build -tags nogui (and GOOS=linux, GOOS=windows) and go test -tags nogui ./... pass. Not tried against a real ~/.codex, in WSL or in the GUI; multi-line inline tables (TOML 1.1) are refused by the parser and so left untouched with an error. 2026-09-30 09:31:59 +08:00
yetone 52b3ad3a9e agent: on Windows, Codex installed in a WSL distro is an agent of its own, codex@wsl:<distro> ("Codex · WSL Ubuntu") — the distros are listed with wsl.exe -l -q (UTF-16LE, or UTF-8 under WSL_UTF8), each asked once through proc for its $HOME, its ~/.codex, a codex on its PATH and its default route, and shown only where Codex is; its files are Codex's own reading and writing (codexIn, at a place rather than a home) through \\wsl.localhost\<distro> (\\wsl$ before that), with the catalog path written in as the distro spells it and what was there stashed under the distro's agent; the gateway written in is 127.0.0.1 when .wslconfig has networkingMode=mirrored and otherwise the Windows host by the distro's default route, with a notice that NAT needs the gateway beyond loopback and through Windows' firewall and that mirrored is the way; its requests stay counted as Codex's, and a profile key is split at its last dot for distro names like Ubuntu-24.04. Nothing runs off Windows. Kimi CLI isn't an agent magpie has yet, so there is no Kimi in WSL. Tested on a Windows box under NAT with a throwaway Alpine distro: detected (the real distros without Codex left out), a magpie model set and reset from the CLI and a test binary, the catalog read inside WSL at /root/.codex; mirrored networking tested only in unit tests 2026-09-28 17:22:30 +08:00
yetone 6e677aabb6 codex: a thread started with magpie as Codex's provider opens again, and Codex can be kept in its API state (#129) — signed in to ChatGPT, a magpie model is routed with openai_base_url (c3931c9), and moving there from magpie as the provider (an older magpie's wiring, or one made while the account had used its allowance up, bd5bf42) took [model_providers.magpie] away, so the Codex app, which resumes a thread on the provider it was started on, said "Model provider magpie not found" for every one of them and came back signed in to ChatGPT. The table now stays once written, a base-URL wiring writes it too, and a sync puts back one an older magpie took away. Codex gets a sign-in field (magpie codex login api, a small square beside its pickers): api keeps magpie as Codex's provider while it is signed in, the app in its API state with magpie's models only; ChatGPT, the default, is as before 2026-09-28 14:11:17 +08:00
yetone 0f0b8fc87e gateway: Codex on one of its own models moves on to the next ChatGPT account when the one it is signed in to has used its allowance up — with more of Codex's accounts on in magpie, Codex's requests go through magpie even for its own models (openai_base_url kept, and put back or taken out as accounts are switched on or off) and are served as the codex subscription's, which fails over as its other models do; the account out of allowance rests until ChatGPT says it resets (resets_at), up to an hour at a time. Asked on Discord 2026-09-26 21:11:42 +08:00
yetone 73be42eca8 models: each agent can be shown only some of them — a provider or group gets a family (magpie provider set relay-x family=relay), and magpie visible zcode relay shows zcode only that family's models (or named providers and groups): in the files magpie writes for it and in what /v1/models answers it, told apart by its User-Agent; magpie models zcode lists what it is shown and what is kept from it, and magpie visible zcode all shows it everything again. A model kept from an agent is still served when asked for by name. Asked for on Discord 2026-09-26 16:02:29 +08:00
Zhizhen HeandClaude Code f3d567e07e agent: TOML tables magpie edits end where the parser says — a table no longer swallows the [[skills.config]] blocks that follow it. A write that would leave an invalid config is refused with its file, line and column
Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-09-26 13:46:03 +08:00
yetone a9aeb4d45f agent(codex): a Codex that couldn't reach magpie says so — the Codex app writes no history.jsonl, so a Codex it started while magpie wasn't running (the gateway refused it, and it built its model list without magpie's) or before magpie set it up (its requests still went to chatgpt.com or an old port) looked fine; magpie now reads the newest model request Codex logged since it was set (logs_*.sqlite, read-only, a day at most) and shows Apply again with the time and why, and to quit and reopen Codex 2026-09-26 01:16:27 +08:00
yetone 1b5d1f87db agent: every agent on a magpie model says when it was taken off magpie behind its back, not only Codex — each checks what magpie wired into it to reach the gateway (Claude Code's env base URL and token, and a managed-settings base URL that wins over them; Gemini CLI's .env and its auth type; OpenCode, Pi, Crush, Command Code, Hermes, omp and Grok Build's magpie provider; DeepSeek Harness's llm-deepseek entry and its key in .env), and one pointed elsewhere or gone shows Apply again with what is off in its tooltip; Claude Code, like Codex, is also said to be bypassed when it was used and nothing of it reached the gateway (its history.jsonl), and slash commands no longer count as use. A test takes every such agent off the gateway and back 2026-09-26 01:10:40 +08:00
yetone 04302b1e63 agent(codex): a Codex taken off magpie behind its back says so, with Apply again — something else rewriting config.toml (another switcher, an installer, a profile of its own) left the row showing a magpie model while Codex asked OpenAI for it, and the user blamed magpie; magpie now remembers what it set on each agent (applied.json) and tells three kinds of drift apart: the wiring gone (the provider table, catalog or base URL, or a profile overriding them), a magpie model replaced by one of Codex's own, and a Codex used since with none of its requests reaching the gateway (history.jsonl against when the gateway last saw it — an old process on an old config). A drifted row carries a coral Apply again pill on its name's line (a badge on the logo in the panel), the reason in its tooltip, Keep current settings in the row menu; the window asks /api/drift every 15s, so it shows without a reload 2026-09-26 01:05:19 +08:00
yetone d6784bd114 codex: a model a custom provider serves takes the reasoning levels models.dev gives it from the others serving it, so the Codex app offers an effort for it (a Volcengine endpoint's glm-5.3-flash showed 中 with nothing to pick, and magpie's effort control a "none" Codex never used) — levels were only taken from models.dev's entry for the vendor itself, and a vendor it doesn't list (a custom provider, a proxy) had none, so Codex's catalog carried no supported_reasoning_levels; a model models.dev doesn't list for its vendor now takes, by bare id as the context window is found, the levels most of the providers giving any give it (glm-5.3-flash: low, high, max, default high), one no one gives levels for keeps none. Codex's effort is settled on one the model takes when a model is picked and when the catalog is synced, magpie shows the catalog's default where none is set, and the gateway sends a Chat vendor reasoning_effort at the model's nearest level (Codex's medium goes to glm-5.3-flash as high), as asked where the model's are not known 2026-09-25 14:54:47 +08:00
yetone 09c635cf03 agents: a model through magpie tells Pi, Crush, omp and Codex how much it holds, and a provider added, edited or removed reaches the model lists agents keep in files of their own — the catalog carried no context window, so Pi took every model through magpie for a 128K one (a CLIProxyAPI's gpt-5.5 holds 922K) and Crush for a 200K one; each model now carries models.dev's input limit, else its context, looked up by bare id as most of the providers serving it give it (a proxy's "openai/gpt-5.4", "gpt-5.4(high)", "glm-4.6:free" find it too; a group holds the least of its members'). And those lists were written only when a model was picked: Codex's magpie-models.json, Pi's models.json, OpenCode's, Crush's, Command Code's, omp's and Hermes' provider blocks and dsh's llm-deepseek entry kept the catalog as it was then, so a provider added later never showed. Saving a provider, a vendor's list fetched anew or models.dev synced now rewrites each one magpie wrote, where it differs, and the app does once at start-up; a Codex signed in to ChatGPT, which asks the gateway, kept its cached list under the backend's ETag, which a change of magpie's never changed — the ETag (and a reply's X-Models-Etag) now carries a tag of magpie's models, and a cache from before a change is aged so the next Codex asks again 2026-09-25 12:32:14 +08:00
yetone 00744ca373 codex: subagents can be put on a magpie model of their own — Codex's spawn_agent tool lists only the first five models it knows, its own ahead of magpie's, so signed in a model spawning a subagent never saw a magpie model to pick; Codex gets a subagents field that writes [agents] default_subagent_model, the model a subagent starts on when none is asked for, offered once Codex runs through magpie and taken out again when it steps back to its own models 2026-09-25 09:23:33 +08:00
yetone bd5bf422ed codex: a ChatGPT account out of allowance gets magpie as a provider of Codex's, and a routing group is named as one in the Codex app's list — signed in through openai_base_url, the Codex app won't send anything once the account has used its allowance up, a magpie model's request too, so magpie is then wired as it is when signed out (model_provider, as the account's own models can't run anyway); and the gateway's model list for the signed-in app named a group after its first member's provider ("z-ai/glm-5.3 · nvidia"), reading as that provider's own model, where it now says routing group, as magpie's other lists do 2026-09-25 06:49:01 +08:00
yetone a9e1a6769a agent + usage: each agent says what its User-Agent begins with, beside its name and icon, and the gateway tells requests apart by that — usage kept a list of its own, which had to be kept in step with the agents by hand and wasn't (DeepSeek Harness, omp and Command Code were missing from it, so they were drawn with no logo); a new agent is now known by its requests as soon as it is described 2026-09-25 05:51:32 +08:00
yetone c3931c91df codex: magpie's models join Codex's own rather than replacing them — signed in (ChatGPT or an API key), Codex keeps its built-in OpenAI provider and sign-in and is pointed at magpie with openai_base_url alone, so its official models still go to OpenAI on the user's own account, its threads (listed per provider) stay together, and the Codex app keeps its model picker; magpie's gateway answers at /backend-api/codex, passing Codex's own models through as they came, serving magpie's (provider/model) itself, and handing out OpenAI's model list with magpie's after it for /model. Compacting a magpie model's conversation is done by that model with Codex's own prompt; Codex's compressed request bodies are read, and usage is counted for both. Not signed in, magpie stays a provider of Codex's as before; a magpie set up that way moves over the next time a magpie model is picked 2026-09-25 02:42:24 +08:00
yetone 4a0719f236 codex: its own models, picked in Codex through magpie beside third-party ones, keep what Codex knows of them — image input, context window, tools, as its models cache has them — under magpie's id; they were given the entry of a third-party model, text only 2026-09-25 01:09:22 +08:00
yetone 82a109eab5 picker: a model starred in one agent is a favorite in every agent — magpie's models are starred by their catalog id, not the agent's own spelling of it, so the favorites tab no longer comes up empty (#33); Codex's own models follow the ones ticked on its ChatGPT subscription (#36); the effort slider shows a dot at every level (#37); and a relay Claude Code was pointed at in its own settings.json can be brought in from Import from other apps, as a provider like any other (#35) 2026-09-24 22:27:59 +08:00
yetone 402a96c39d Rename dial to magpie, with a magpie for a logo
The product, module path (github.com/yetone/magpie), binaries, env vars
(MAGPIE_ADDR, MAGPIE_DEV_UI, MAGPIE_THEME, MAGPIE_DEBUG), config and cache
folders (~/.config/magpie, ~/.cache/magpie), bundle id (com.yetone.magpie),
gateway provider id and token, and every UI string now say magpie.

On first run magpie copies ~/.config/dial and ~/.cache/dial to the new
folders when they don't exist yet (copy, not move, so a still-running dial
keeps working; the old folders can be deleted by hand later).

The logo is a small round magpie in profile: one path for the header and
the tray, and the app tile adds the white belly and the blue-green tail.
The header mark is drawn in ink rather than the accent colour, which
fought the text in light mode.
2026-09-23 23:55:11 +08:00
yetone 9174a6510b provider: signed-in agents (Codex, Copilot) become providers
A Codex (ChatGPT) or Copilot login is a subscription with models behind
it, so dial offers it as a provider: every other agent can use its models
through the gateway. The agent's own credential file is read each time,
tokens are refreshed the way the agent does it, and only the model picks
are stored. Claude Code is found but deliberately not offered (Anthropic's
terms), and the CLI and API say so.

The gateway signs upstream requests through the provider (Sign/Prepare),
translates non-streaming requests for backends that only stream, and now
recognises an event stream by its body when the vendor sends no
Content-Type, which is what the ChatGPT backend does.
2026-09-23 15:25:05 +08:00
yetone 0b1626be6d agents: a Default entry that puts an agent back the way it shipped
Setting a field to nothing now means the agent's own default: dial's
wiring (gateway provider, model catalog, env) comes out and the key is
removed, so Codex is back on OpenAI's catalog and Claude Code on
Anthropic's. The picker lists it first; the CLI takes `dial <agent>
default`. Also the vendor icon inside a field pill was 22px, not 14px:
the row icon rule outranked it.
2026-09-23 14:37:42 +08:00
yetone 8fb93f8fc2 Make dial a local LLM gateway with preset providers and one catalog
dial now serves OpenAI chat completions, OpenAI Responses and the
Anthropic Messages API on 127.0.0.1:3425 and forwards each request to the
vendor that serves the model, translating between the APIs when the vendor
speaks a different one (streaming, tool calls and reasoning included).
Every agent points at that one URL; models are spelled provider/model.

Providers are added from presets (vendors, relays, local servers) with
just an API key, or as custom OpenAI/Anthropic-compatible endpoints.
Keys live in ~/.config/dial/providers.json; nothing is read from the
shell environment. Each provider chooses which of its models to expose.

Agents get gateway wiring instead of vendor-specific tables: Claude Code
via the env block, Codex via a dial model provider plus a model catalog
file, OpenCode/Pi/Crush via a dial provider entry; native settings are
stashed and restored when a native model is picked again.

The Providers page shows the gateway, each provider with the agents on it,
an inline editor with model chips, and an add sheet of preset tiles.
CLI: dial presets | providers | provider add|key|models|test|rm | models | serve.
2026-09-22 22:43:51 +08:00
yetoneandClaude 0a234ce41d dial: one dial for every coding agent's model
Wails v3 tray panel and window, TUI and CLI. Switches model, effort and
provider for Claude Code, Codex, Gemini CLI, OpenCode, Pi, Goose, Cursor,
Copilot CLI and Crush with format-preserving config edits. Providers page
and registry, stored keys, live vendor model lists, profiles.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-09-22 21:47:29 +08:00