* 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.
* 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.
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.
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.
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).
* 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>
#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.
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.
* 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
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.
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.
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.
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.
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.
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.
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>