Some managed ChatGPT workspaces disable device-code authentication, so those accounts could not be added to CC Switch. Add browser login (authorization code + PKCE with a loopback callback on port 1455) to the Auth Center and the Codex provider form, including reauthentication; device-code login stays available in a method selector.
Fixes#8284
Drop the copied OmpProviderForm and the YAML editor; Oh My Pi now uses
PiProviderForm with an omp mode (no provider-level name, no
thinkingLevelMap, plus a discovery type selector). Presets are derived
from the Pi catalog. Pi-family helpers (isPiFamilyApp, parametrized
current-state queries and usage-script command) replace the scattered
pi/omp branches, and omp error keys reuse the Pi error translator.
Co-authored-by: allenxu09 <s23372.xu@stu.scie.com.cn>
Match the binary name, the Apps page tool id and the i18n keys that
already used omp. The app is unreleased, so nothing persisted needs a
migration; ohmypi stays accepted as an alias when parsing app ids.
Show the app by default like Pi and Kimi Code.
Squashed from #6747 (head 50c3cec1d) onto current main: providers in
models.yml, MCP in mcp.json, Skills, sessions, app visibility and
directory override for Oh My Pi. Conflicts with the Kimi Code refresh
refactor in App.tsx and the locale files were resolved by keeping both
sides and routing Oh My Pi through refreshAdditiveLiveState.
Co-authored-by: allenxu09 <s23372.xu@stu.scie.com.cn>
Add Antigravity CLI (agy) as a managed app: provider switching for the native login and API-key modes, MCP and Skills sync through ~/.gemini/config, session browsing and session-log usage import.
Co-authored-by: peterLwk888 <lwk1124@outlook.com>
Providers don't remember which preset they came from, so the edit form had
no preset and only linked the "Website" field; with that field empty the
link vanished. When no preset is selected, look the preset up by the
form's current base URL and use its apiKeyUrl and partner promotion, same
as the add form. If presets sharing that address disagree on the link or
the promotion, don't guess and fall back to the Website field.
Route switches now return what was actually applied (remembered / route / stack) and whether the client configuration was rewritten, and the success toast is built from that instead of always asking Codex / Gemini CLI / Grok Build users to restart. A restart hint is shown only when client files changed. Direct-mode switches serialize unchanged. Fixes#8347.
The Send User-Agent switch was only derived from settingsConfig.headers
on mount and on preset reset. Editing headers directly in the
Configuration JSON editor left it stale in both directions: adding a UA
in JSON kept the switch off (and clicking it then replaced the custom
value with the default UA), removing it kept the switch on.
Add syncOpenclawUserAgentFromConfig to useOpenclawFormState and call it
from the OpenClaw JSON editor's onChange. It only reads valid JSON
objects, sets the switch to the case-insensitive presence of a
User-Agent header, never rewrites the JSON, and keeps the last state
while the draft is invalid.
Refs #8256
Converted amounts no longer start with ≈; it took room and the USD value
is still shown on hover. Since the rate is entered by hand, converted
amounts now show at most two decimals (yen keeps two fewer), instead of
the 4–6 used for USD in logs and hover text. An amount that would round
to zero is written with two significant digits rather than as 0.00.
Add a Settings tab to the usage page that replaces the Pricing tab: an
amount display section on top (currency + hand-entered exchange rate) and
the existing pricing configuration below.
- Display currencies: USD, CNY, EUR, JPY, TWD, TRY; USD stays the default.
Rates are entered manually; untouched preset rates are not written to
settings.json so later preset updates still reach those users.
- Costs stay stored in USD; conversion only happens at display time.
Converted amounts carry a "≈" prefix and hovers add the USD value.
- Chart data is converted before plotting so axis ticks stay round.
- Yen drops two decimals, and usage amounts now group thousands.
- The sidebar "Today" amount drops decimals from 100 up so it no longer
squeezes the nav label.
- Settings are device-local (`usageDisplay` in ~/.cc-switch/settings.json).
The Desktop page now uses the same mode tabs as the routing apps instead of
the per-card access bar:
- `MODE_APP_IDS` / `isModeAppId` (routing apps + Desktop) drive the mode
panel, the sidebar mode tags and the "apps using routing" list;
`PROXY_APP_IDS` stays for failover and routing settings, which Desktop
does not have (no failover queries, no route settings sheet).
- Desktop config checks move into the panel's notice slot.
- Entering route mode explains that the model list follows the routed
provider and that Desktop must be restarted; switching or leaving route
mode reminds to restart. The needs-routing dialog hides "switch anyway"
for Desktop because direct mode cannot write a mapping card.
- Drop the Desktop-only card sections, the "mapping" nav tag and the
mapping-service strings in all four locales.
The usage page's auto-refresh interval only re-read the database, so
session usage still waited for the fixed 60-second background scan, and
routed usage already updated in real time through usage-log-recorded.
The interval now controls how often session logs are scanned:
- The background scan drops to every 10 minutes as a safety net against
CLIs pruning old session files.
- While the usage page is visible it scans right away, again whenever
the window becomes visible, and then at the selected interval
(5/10/30/60s, default 60s). The first scan waits until settings load,
so it never runs when automatic scanning is off.
- The "Off" option is removed (a saved 0 falls back to 60s), and the
interval menu is hidden when automatic scanning is off.
- Statistics no longer poll. Imports refresh them through the
usage-log-recorded event, which is always on again, and time-relative
ranges re-run once a minute so "Today" rolls over at midnight and
rolling windows drop old records without new usage.
Copy is updated in zh, zh-TW, en, ja and tr, and in the zh/en/ja manual.
* feat(i18n): add Turkish interface and tray localization
* fix(i18n): preserve technical literals in Turkish help
* fix(i18n): treat Turkish as a community translation
Turkish falls back to English for strings it does not have yet, so new
English strings no longer fail CI or the build until someone translates
them:
- Drop the full-catalog parity check and the Pi coverage checks for tr;
keep the non-empty and interpolation checks for existing strings
- Build the Turkish tray texts from English and override the translated
ones, so new tray items no longer need a Turkish entry to compile
- Show usage script errors in English for Turkish instead of Chinese
- Label the option as a community translation, next to the other
languages, using the native name like the existing options
- Restore technical literals that were translated (config.toml, AKSK,
env placeholders, # SOUL.md, skills.sh, my-provider) and lock them in
the literal regression test
- Format usage dates with tr-TR
- Document in CONTRIBUTING that Turkish strings are optional, and list
Turkish as a community translation in the READMEs
---------
Co-authored-by: Jason <farion1231@gmail.com>
Each model in the picker dialog now shows its context window, reasoning
and image input, resolved with the same lookup as the parameter fill
(same-endpoint preset, then models.dev by endpoint, then the vendor's own
entry), so what the dialog shows is what Apply fills in. Prices are left
out: models.dev only has them when the endpoint is recognized. The vendor
column has a fixed width so the parameters line up. The dialog body mounts
only while open, which is also when it subscribes to the models.dev query.
Since the dialog already showed the values, models added from it are
filled without the "parameters filled, please check" toast. The fill
function of useModelMetadataFill now takes { baseUrl, silent } instead of a
bare base URL (the Pi form passes its per-model address that way). Picking
a model from a row's dropdown still shows the toast.
ClaudeStackModelsField takes the provider's base URL so its dialog can
show parameters too; its rows have no such fields to fill.
The "Send User-Agent" toggle in the OpenClaw form replaced the whole
headers object when turned on and deleted it when turned off, so every
other custom header was lost on save.
Update only the User-Agent entry: drop existing variants
case-insensitively, add a canonical User-Agent when enabled, and remove
the headers object only when it ends up empty. Detecting the toggle
state is case-insensitive as well.
Fixes#7846
The extra-options row editor keeps every value as display text, and any
row change re-parsed all rows with JSON.parse. Untouched string values
such as "123", "false" or "null" were silently turned into numbers,
booleans or null on save, in both the OpenCode and MiniMax Code forms.
Merge row edits into the stored options instead: a row whose text still
matches the stored value's display form keeps the original value and
type, and a renamed key inherits its old value. Renaming an option onto
a key that already exists is rejected, matching the model field editor.
Fixes#7833
The import mutation's onSettled awaited every affected refresh, including
the discover list, which downloads each repository from GitHub (up to 60s
per repo). TanStack Query only settles a mutation after onSettled resolves,
so isPending stayed true and the import dialog could not be cancelled or
closed until discover finished. The discover segment stays mounted once
opened, so this hit anyone who had visited it on a slow GitHub connection.
Only await the installed and unmanaged refreshes the dialog depends on;
repos and discover are still invalidated in the background.
Fixes#7994
Deleting a managed ChatGPT account (or logging out of all of them) left
every Codex provider bound to that account pointing at a dead local ID.
#7395 made such cards recoverable by hand (switch away, pick another
account); this removes the dangling bindings where they are created.
- Remove/logout detach the affected Codex providers in one SQLite
transaction: drop the binding, store an empty auth like a manual
unbind, backfill category=official for legacy cards, keep unknown
meta keys. The card then follows the native Codex login.
- Only this device's account IDs are unbound. Provider rows sync across
devices, so a row bound to another device's account stays bound.
- The command reports "account removed but unbinding failed" separately.
Any row still left dangling (failed unbinding, older versions, synced
rows) keeps going through the #7395 recovery path, whose tests now
create such rows directly.
- Frontend invalidates the Codex provider list after remove/logout,
including on partial failure.
Thanks @SaladDay for the report and the first fix in #7060.
Fixes#7055
Refs #6969
Add a GitHub Copilot preset for Codex that uses a managed GitHub account.
The local route resolves each request's model against the account's live
Copilot /models catalogue and picks the upstream protocol the model
advertises (native Responses first, Chat Completions as fallback in auto
mode), with an optional explicit protocol override.
- Response handling, the opaque-state rectifier and Stack web_search
stripping follow the transport actually used for each request rather
than the stored apiFormat.
- Fetching models fills the catalogue from the account's live prompt
limit, parallel tool calls and reasoning levels; presets keep
conservative fallbacks.
- Codex only treats a card as managed Copilot by its provider type, so
self-hosted relays keep their own key and endpoint.
- Copilot request identity headers are shared between the Claude and
Codex paths.
Co-authored-by: yuwk <52391318+ump45nose@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Jason <farion1231@gmail.com>
Aggregated models used to reach Claude Code through gateway discovery,
which only runs at startup, so the UI asked for a restart after every
change. Now that they are written into settings.json as modelPicker, a
running Claude Code picks up added and removed models, a new default
and window changes on its own. Codex still reads its model catalog only
at startup, so its hints stay as they are.
- useStackModelsChangedHint, which only existed to toast "restart
Claude Code" when the published models changed, is removed together
with its test and provider.stackModelsChanged.
- Adding or removing an aggregated provider says
"Saved. Takes effect in Claude Code right away" for Claude Code
(new provider.stackSavedLive); entering Aggregation says the models
are now in /model (new mode.toast.enteredStackLive), and the entry
dialog no longer lists a restart note for it
(mode.dialog.stackNoteRestart removed).
- The Claude Code model list hint drops "Restart Claude Code after
changes", and the Aggregation setting's description now says only
Codex needs a restart.
- The unused proxy.stackMode.tooltip keys are removed.
All four locales are updated.
The settings page no longer unmounts after saving, but the saved
app-config-dir baseline was only captured on mount. After saving a new
data directory and choosing "restart later" the save button stayed,
resetting to the old value could not be saved, and every later save
reported "restart required" again. Keep the baseline in state and commit
it to the just-saved value once the override is persisted.
Unify the visual and interaction rules on top of the v7 redesign.
- Theme color #F97315 for --action-* and --ring; dark mode uses #EA580C
with white text, toned-down mode colors and translucent soft fills
- Buttons mapped to three sizes (28 / 32 / 36); inputs, selects and
search fields share fieldClass; self-drawn checkbox and radio
- HoverTip replaces title on about 100 icon buttons; it stays closed
when a closing menu returns focus to its trigger
- Install tailwindcss-animate so menu, popover, dialog and tooltip
enter/exit animations take effect; sliding segment thumb and tab
underline
- Remove the command palette and the header prompt entry; provider card
primary actions become icon buttons
- Virtualize the session list and the Skills discovery list; optimistic
toggles for MCP and Skills with a 300ms delayed disabled state
- Toasts drawn unstyled with status-colored icons and de-duplicated
- Windows: hide the system title bar and use in-app window controls;
macOS: brand row under the traffic lights
- Focus rings only for keyboard users
- Skills discovery and the prompt list show the last result from a
local cache and refresh in the background
- Drop the trailing ellipsis from 36 dialog-opening labels
- Provider list fills the main area
Co-authored-by: 千羽 <qyinter@users.noreply.github.com>
Presets: name the Tencent family "Tencent Cloud" (腾讯云), since it also
covers pay-as-you-go plans in some apps, and let a family list its
versions in design order (Kimi by region, Tencent Lite before Pro).
Families without an order keep the preset file order.
Tray: a failed tray switch no longer expires after ten minutes. It stays
until the app switches successfully or its page is opened in the main
window, reported through the new tray_app_page_seen command.
Skills storage and sync: turn the sheet into the spec's dialog with a
real three-way sync method (auto, symlink only, copy only; auto was
shown as symlink before), radio buttons for where the main copy lives,
the real CC Switch skills path with an Open button, and a visible line
explaining why "Move and switch" is unavailable.
Presets: add optional family/versionKey fields so one vendor's regional
and plan variants show as a single row in step 1 (Claude 94 presets ->
71 rows) and as a version switch in step 2. Switching versions after the
user typed into the form asks first, since the form is refilled. The
category field and preset order are unchanged.
Tray: show a dot and a problem tooltip on the icon only while the
routing service failed to start, fell back to direct, or a tray action
failed; spell out that quitting stops the routing service while it is in
use; name the default provider in the Stack submenu title; add the
"restart X to apply" line after tray switches; refresh the tray when the
routing service starts or stops.
Panels: record when the background session scan last finished and show
the newer of that and a manual sync on the usage page; add
resync_mcp_to_apps and resync_skills_to_apps; report per-repo failures
from skill discovery and update checks; report ZIP skills skipped
because the directory is taken. MCP/Skills bulk actions keep applying
to the filtered rows only.
The native menu now lists Claude Code, Claude Desktop, Codex, Gemini CLI
and Grok Build (hidden apps are skipped; additive apps stay out). Each
app row reads "name · mode · provider · quota" and opens a submenu that
states the mode and only offers providers that switching can actually
use: providers that need routing open the app page with the same
needs-routing dialog, official subscriptions are disabled under routing,
and the menu is read-only while failover is on instead of silently
turning failover off. Projects move into the app submenus.
Quota text matches the cards (remaining, "running low" under 10%, no
emoji). A problems section appears only when something needs action:
the routing service is down while apps use it, an app fell back to
direct at startup, or a tray switch failed.
On Windows a left click opens the window and a right click shows the
menu; macOS and Linux keep the menu on left click.
Pages (phase 4):
- Usage: v7 layout (filter row, four metrics with more on demand,
switchable trend, request log / provider / model / pricing tabs, data
sources sheet). Latency becomes output speed: output / (duration -
first token), shown only for requests with at least 100 output tokens;
provider totals sum output and generation time instead of averaging.
- Sessions: single-column list grouped by project or time, drilling into
a reader with resume, find, question jump and copy actions.
- MCP and Skills share one app matrix (neutral checks, retry on failed
writes, column bulk actions with undo); MCP add/edit moves into a
drawer with form/JSON modes; Skills merges installed and discover and
always shows a Pi column.
- Prompts: one page for eight apps including Hermes SOUL.md, enable and
disable as buttons, delete with undo instead of a confirm, copy to
other apps, import existing files; app pages show the active prompt.
- Add provider is two steps: pick a preset (always-on search including
Chinese names and domains, categories inferred for display, sorted by
name with no promoted entries), then fill the form. Edit pages name
the provider and warn when it is the one in use.
- App page menu links to that app's usage.
Visual pass (phase 6):
- Secondary pages cover only the content area so the sidebar stays.
- Cmd/Ctrl+K jumps to apps, pages and settings sections.
- Legacy button variants, primary color, dialogs, toasts and sheets
follow v7 tokens; hard-coded palette classes and 10px text across
the remaining components are replaced with tokens.
Claude Desktop model-mapping providers send every request through the local
routing service, but the service lifecycle only counted the four routing apps:
when Claude Code left routing, stop_server_if_unused stopped the service Desktop
was still using, and nothing started it on launch.
- stop_server_if_unused now also treats a current Desktop model-mapping
provider as a user of the service.
- ensure_desktop_mapping_service starts the service when that provider is in
use; it runs on startup, after switching a Desktop provider and after
applying a project that covers Claude Desktop.
- The frontend no longer warns that routing must be started when switching to
a Desktop model-mapping provider, since the backend starts it; drop the now
unused isProxyRunning hook argument and proxyReasonClaudeDesktop string.
Claude Code fetches the model list from the proxy once at startup, so a
change to the published Stack models only shows up after a restart.
Adding or removing a member already said so in its saved toast, but
editing a member's models or deleting a member did not.
In Claude Code Stack mode the provider list now compares the published
model ids each time the Stack list is refetched and shows "Stack models
changed. Restart Claude Code to apply" when the set differs. The refetch
right after adding or removing a member is skipped, since that toast
already asked for a restart; the skip is used up by that refetch even
when nothing changed, so it cannot swallow a later edit. Reordering
models or editing fields that are not published (keys, addresses) does
not trigger it. Codex is left to its process-based banner.
Without a thinking map, Pi shows off..high for a custom model and hides
xhigh and max, even when the upstream supports them. When a fetched
model is picked and has no map yet, fill one:
- Use the reviewed map of the Pi preset at the same endpoint, but only
under the same API and when the compat it relies on does not conflict
with what the user wrote. Missing compat keys are added to the model,
and for openai-completions the thinking format and reasoning_effort
support that Pi detects must match the preset's.
- Otherwise, when Pi sends levels verbatim as reasoning_effort
(openai-responses, or openai-completions detected as the OpenAI
thinking format), derive the map from models.dev effort levels using
Pi's own getEffortThinkingLevelMap rule.
A model with its own baseUrl is now looked up by that address. Update
the Pi design docs accordingly.
A provider's /models endpoint only returns model IDs, so a model picked
from the fetched list left its context window, output limit, reasoning
levels, and modalities empty, while the same model in a preset had them.
When a model is picked in the Codex, OpenClaw, Hermes, OpenCode, or
MiniMax Code form, look up its metadata field by field:
1. the preset model with the same endpoint (hand-verified values);
2. the models.dev provider whose API endpoint matches;
3. the vendor's own models.dev entry, without its price.
Endpoints match only when host and path are equal after dropping a
trailing version segment or request path, so plans that share a host
(z.ai standard vs Coding Plan, OpenCode Zen vs Go) never borrow each
other's values. Models are never matched by name across providers: the
same model ID often has very different context windows per deployment.
Only empty fields are filled, and reasoning or image input is only ever
turned on. models.dev is prefetched once the model list is fetched and
shared with the usage pricing import; when it arrives late the row is
filled again, located by its stable row key. A single toast asks the
user to review what was filled.
* feat: Enhance OpenCode provider configuration and import functionality
- Added support for built-in provider defaults in OpenCode configuration.
- Updated OpenCode provider options to allow omitting npm and model definitions.
- Improved import logic for OpenCode providers from live config, ensuring existing credentials are respected.
- Introduced tests for credential handling and built-in provider editing.
- Enhanced UI components to reflect changes in provider configuration and selection.
- Added localization for new built-in defaults feature.
* refactor(tests): enhance test environment isolation and settings management
* Refactor OpenCode provider handling and validation
- Removed TestSettingsGuard to simplify settings management during tests.
- Enhanced validation in handleDuplicateProvider to ensure npm and model definitions are provided for non-OMO providers.
- Updated OpenCodeFormFields to conditionally allow built-in defaults based on props.
- Improved ProviderForm to manage existing OpenCode overrides and prevent incomplete copies from being added.
- Added comprehensive tests for OpenCode provider duplication and validation scenarios.
- Introduced new error messages for better user feedback on provider requirements.
- Created opencode_tests.rs to cover various scenarios for OpenCode provider imports and updates.
---------
Co-authored-by: Jason <farion1231@gmail.com>
- A multi-file operation records that it has started publishing in
live-state.json before the first file is replaced; if that cannot be
recorded nothing is published. Recovery rolls such an operation forward
even when a published file was changed again afterwards, so an
unpublished Codex login stash is no longer discarded.
- controller::lock_settled fails when the previous operation cannot be
finished, and entry points refuse to save or switch instead of writing
rows the leftover operation cannot restore. Startup, detach and exit
log the failure per app and continue.
- Saving, adding and syncing the current provider take the proxy switch
lock before reading mode and pointer, so entering proxy mode can no
longer be overwritten by direct key fields. A new proxy address is
written to every attached app even if one of them fails.
- Codex: the row-level model catalog pointer leaves with its provider; a
profile selecting the built-in openai provider counts as the official
route; exclusive fields the editor shows only because live has them are
compared by value, so an untouched field changed by the client stays
with the client. The add dialog sends the preset draft it projected, and
the backend uses it as the field origin like an existing row.
- Claude editor keeps a legacy Bedrock apiKey at the top level where older
versions read it; the Gemini editor recognizes a legacy Google card the
same way switching does.
- The request path passes the provider it already resolved to the router
instead of reading live-state.json twice per request.
Gemini CLI loads ~/.gemini/.env only at startup (and when validating the
auth method), and never overwrites a variable already in its environment,
so a running session keeps the old key, endpoint and model after a
switch. The switch toast now asks for a restart, as it already does for
Codex and Grok Build.
The user manual said the opposite in three places (the quick start
table, the switch page and the FAQ), claiming Gemini CLI re-reads .env on
every request; all three languages now say a restart is needed. The
README and the routing page now list Gemini CLI among the apps that show
a restart notice.
The add dialog for these apps now follows the same rules as the editor
and Claude Code's add dialog. Picking a preset (or the custom template)
projects it onto the current client files, so the editor shows what the
files will look like after switching to the new provider. On save, key
fields and provider-exclusive fields go to the row and other edits go to
live as global settings with a three-way check. Before, anything typed
there besides key fields was stored in the row and never took effect.
- Adding and editing share one backend path per app. When the new
provider is the first one, its key fields and the direct pointer are
committed in the same write; the Gemini and Grok editor writers take a
set_pointer flag for this, as Codex's already did.
- The form reports each projection to the dialog as the three-way base.
Only the latest request counts; while a projection is pending or after
it fails, the add saves the row only. Every reset of the shown content
re-projects or clears the base, otherwise global settings missing from
the editor would be written to live as deletions. Grok's official card
has no editor and keeps its seed flow.
- Remove six unused common-config strings and rewrite the empty-state
hint that still described the snippet; it now explains key-field
switching and also shows for Grok Build.
Switching, adding, editing, sync and entering or leaving local routing now
write Gemini CLI and Grok Build through the write engine. Whole-file
rewrites, backfill before switching and snippet merging are gone.
Gemini CLI:
- Only the key-field lines in .env change; comments, order and the user's
own variables stay. In settings.json only security.auth.selectedType and
model.name change. Both files commit as one operation.
- selectedType follows the provider type (Google login for official cards,
API key otherwise) instead of the row, so a /auth choice an older version
stored in a row can no longer send a third-party card through Google
login.
- Local routing writes the route's model name along with the local address
and placeholder key, so switching to a route with another model rewrites
the client, as Claude Code does.
- Vertex rows pass validation with GOOGLE_API_KEY or
GOOGLE_GENAI_USE_VERTEXAI instead of GEMINI_API_KEY.
- The common-config snippet is frozen like Claude's and Codex's; its UI and
the explainer dialog are removed. New rows keep commonConfigEnabled: true
for older versions that sync them.
Grok Build:
- Only models.default and the [model."<name>"] table CC Switch wrote
change; MCP servers and the user's other tables stay.
- The written table name is recorded in live-state.json in the same
operation, and switching away removes the recorded table rather than
whatever models.default points to now, so a default changed in Grok's
/settings no longer leaves the old provider's table behind. Without a
record (just upgraded), the table is inferred from the current row.
Tables holding the proxy placeholder key are always removed.
- Local routing writes the route's table pointing at the local
/grokbuild/v1 endpoint with the placeholder key through the engine. An
official card cannot be a route.
Editors:
- Both editors show what the client files will look like after switching.
On save, key fields go to the row and other edits go to live as global
settings with a three-way check. Gemini row fields that never reach live
can be added to the global settings; Grok's can be copied, as in Codex.
The TOML diffing shared with Codex moves to editor_toml.rs.
Also:
- First-run import no longer moves key fields (GEMINI_MODEL, Codex
model_reasoning_effort and the like) from the default row into the
snippet, where a frozen snippet would lose them.
- The .env patch can remove keys unconditionally and parse entries.
Switching, adding, editing, sync, universal providers and entering or
leaving local routing now write Codex through the write engine. Only
routing, credential, model, reasoning, context-window and compatibility
fields are replaced; MCP servers, [projects], comments and the user's own
provider tables stay byte-for-byte. auth.json, config.toml, the model
catalog, the managed-account marker and a device-local login stash commit
as one operation, so a crash at any point rolls forward on startup.
- Third-party routes are always written as [model_providers.custom] with
the key in experimental_bearer_token. Rows that use another id, a
top-level openai_base_url or a legacy reserved table are normalized on
the way out. Switching to an official card leaves a dormant custom table
(local proxy address plus placeholder credential) so older third-party
sessions can still resume.
- requires_openai_auth follows the route's auth and whether a login is on
disk; keyring and ephemeral credential stores are handled separately.
- A switch is refused before anything is written when the active profile
overrides the route, the selected table is missing or built in, a
keyless row would fall back to the official login, or the row has no
auth.
- Provider tables an older version wrote are removed only when they can
be proven to be CC Switch's. Tables a profile references are kept, and
reserved ids that cannot be proven are renamed.
- Provider-exclusive fields, including web_search and a row's own
model_catalog_json, are removed on the way out only when live still
holds the previous provider's value.
- No more backfill. A native ChatGPT login that CC Switch deletes or
overwrites is kept in ~/.cc-switch/codex-login-stash.json (0600, not
synced) and restored when switching back to an official card; logging
out on an official card discards it. Login snapshots already in
official rows seed the stash once. A leftover API key in auth.json is
deleted only when it matches a third-party row's key.
- Managed accounts fetch their token before taking the lock. If the CLI
refreshes the login during a switch, the switch stops before writing
anything. Once files are published, a later failure no longer rolls the
row back; startup finishes the switch.
- Local routing: third-party routes get a fixed local-proxy table and the
official route uses cc-switch-official plus the client's own login. The
contract covers every field written, so a route switch with the same
contract touches no files.
- The Codex editor shows what config.toml will look like after switching.
On save, key fields go to the row and other edits go to live as global
settings with a three-way check. Row fields that never reach live are
listed read-only and can be copied.
- The Codex common-config snippet is frozen like Claude's; its UI and the
TOML snippet command are removed. New Codex rows keep
commonConfigEnabled: true for older versions that sync them.
- The write engine can delete a file as part of an operation.
- Integration-test mutexes tolerate poisoning so one failure no longer
cascades into unrelated tests.
Switching, adding the first provider, editing the current provider,
sync, universal providers and restoring after the local router all
write settings.json through the write engine now. Only endpoint,
credential, model and protocol fields plus the provider's own switches
are replaced; hooks, plugins, permissions and everything else stay
byte-for-byte. The file write and the current-provider pointer commit
as one operation, so a crash in between rolls forward on startup.
- No more backfill, no common-config snippet and no context-window
injection for Claude. Window values left behind by the old injection
are cleaned up from a frozen list.
- The provider editor shows what settings.json will look like after
switching. On save, provider fields go back to the row and other
edits go to live as global settings, with a three-way check against
external changes and a dialog to resolve conflicts. Row fields that
never reach live are listed with a one-click add.
- New Claude rows keep commonConfigEnabled: true so older versions that
sync the row still merge their snippet.
- Presets drop timeout and telemetry keys; a test now requires Claude
presets to contain only key or provider-owned fields.
Reject an ordinary Codex managed-account login when both the ChatGPT workspace and the stable user identity (id_token sub) already match an existing account. The check runs under the storage lock so concurrent login completions cannot create duplicate records, and the refresh lock is now taken only for targeted re-auth so a rejected add leaves no lock entry behind. Distinct users in the same workspace remain supported. The duplicate-account error code is mapped to a localized message in the managed-auth UI.
* fix(codex): keep provider's own key on switch-away backfill
When switching away from a Codex provider, cc-switch backfills the
outgoing provider's stored config from the live ~/.codex files so that
in-app changes are captured. restore_codex_provider_token_for_backfill
decides what to persist as the provider's auth.OPENAI_API_KEY:
- When the live config.toml carries a per-provider
experimental_bearer_token, that token is provider-scoped and is lifted
back into the stored auth. Correct.
- When it does NOT (the default preserve_codex_official_auth_on_switch
= false mode keeps the active key in the shared auth.json), the
function returned early and the caller adopted the live auth.json
wholesale as the provider's stored auth.
The problem: auth.json is a single-slot shared file with no provider
identity; it always holds the most-recently-activated provider's key.
Any time it holds ANOTHER provider's key (after a proxy-takeover
backup/restore cycle, an in-app ChatGPT login overwriting auth.json,
a cloud-sync current/live divergence, or any current-vs-live mismatch),
the switch-away backfill overwrote the outgoing provider's stored key
with another's. Repeated switches made keys silently converge across
providers that share a base URL, surfacing as "model xxx not found"
because the wrong key reached a model it was not entitled to.
Fix: in the no-bearer branch, preserve the provider's own DB-stored
auth instead of adopting the shared live auth.json. Live config.toml
changes (model/base_url/mcp/env) are still captured by the caller; only
the credential slot is no longer taken from the shared file. This
mirrors the #6277 restore-side rule of never letting the shared live
credential clobber per-provider storage.
Adds two regression tests:
- backfill_keeps_provider_own_key_when_live_auth_holds_another_key
(the failing case under the old code)
- backfill_restores_key_from_live_bearer_token_when_present
(positive control for the already-correct bearer path)
Fixes#6414.
* Revert "fix(codex): keep provider's own key on switch-away backfill"
This reverts commit 4ddf304eca458a1eadcf93f5851579c7dde94828.
* fix(codex): reconcile edit form key with live bearer token
In bearer-token mode (preserveCodexOfficialAuthOnSwitch enabled — used to
keep a ChatGPT login while routing third-party providers), switching to a
provider writes its key to config.toml's experimental_bearer_token and
preserves the shared ~/.codex/auth.json. auth.json is a single shared slot
with no provider identity, so it may hold ANOTHER provider's stale key
(left over from a previous default-mode switch or a different provider).
When the user then opened the edit dialog for the current provider,
EditProviderDialog loaded liveSettings as the form base, and
useCodexConfigState initialized codexAuth from the stale auth.json while
pickCodexApiKey preferred auth.OPENAI_API_KEY over the bearer token. The
form therefore displayed — and on save persisted — the stale key back
into the provider's DB record. Repeated edits made keys silently converge
across providers that share a base URL, surfacing as "model xxx not found"
once the wrong key reached a model it was not entitled to (#6414).
Fix: when loading Codex config into the form, lift the config's
experimental_bearer_token into auth.OPENAI_API_KEY (when present and
differing) so both the displayed key and the saved auth carry the
correct per-provider key. This mirrors the backend
restore_codex_provider_token_for_backfill lift and is a no-op when the
config has no bearer (default mode keeps auth.json as the active key
slot, and manual live auth edits are preserved exactly — covered by
existing tests).
Adds 4 regression tests in tests/hooks/useCodexConfigState.bearer.test.ts.
Fixes#6414.
* style: prettier-format useCodexConfigState.ts
Fixes the frontend CI "Check formatting" failure on the previous commit.
Pure formatting (collapses a multi-line call to one line); no behavior
change.
* fix(codex): scope bearer reconciliation to live edits
* Add managed ChatGPT account binding for Codex
* Consolidate managed account entry points
* Clarify unmanaged Codex login option
* fix(provider-form): show Codex FAST mode toggle in account-select mode
The FAST mode switch was gated on mode === "manage", but the Claude and
Claude Desktop Codex OAuth forms render CodexOAuthSection in select mode
while still passing onFastModeChange. The toggle was therefore hidden:
new providers couldn't enable FAST mode and existing ones couldn't turn
it off, even though the backend still honors meta.codexFastMode. Gate the
toggle on onFastModeChange presence instead, restoring the pre-refactor
behavior without affecting the manage panel or native Codex form (which
do not pass the handler).
* fix(i18n): localize Codex managed-account none option label
ProviderForm passed a hardcoded Chinese string as codexOauthNoneOptionLabel,
so en/ja/zh-TW users saw Chinese text in the Codex account selector's
"none" option. Add codexOauth.noneOptionLabel to all four locales and
resolve it via t().
* feat(codex): write managed ChatGPT id_token into live auth + reauth prompt
Bind the selected ChatGPT account's id_token into the Codex official
managed auth.json so it matches a native browser login for the fields
that drive behavior (auth_mode, account/plan/email via id_token claims).
- Persist id_token on CodexAccountData; capture at login and on refresh
(empty string treated as missing). Add get_valid_token_and_id_token_for_account.
- codex_managed_oauth_live_auth now writes tokens.id_token when available.
- Keep the managed-vs-native safety marker intact: extract_codex_managed_oauth_auth
tolerates id_token but still rejects native logins (which carry refresh_token /
top-level last_refresh), so cc-switch never clears a real browser login.
refresh_token and last_refresh are intentionally NOT written for this reason.
- Surface reauth_required (account has no stored id_token) through
GitHubAccount -> ManagedAuthAccount -> TS. Legacy accounts get a styled
amber prompt + one-click re-login (device flow) in CodexOAuthSection,
flagged in both the selector dropdown and the select-mode hint.
- i18n: reauth strings added for en / ja / zh / zh-TW.
Verified: pnpm typecheck + build:renderer green; cross-model review of Rust
correctness, the managed-vs-native safety invariant, and the id_token lifecycle.
* style(codex): satisfy prettier format check for reauth UI
* fix(i18n): add missing managed-account selector keys
The managed OAuth account selector referenced codexOauth.chatgptAccount,
codexOauth.manageAccounts, copilot.githubAccount, copilot.manageAccounts and
providerForm.providerKeyStatusLoading with Chinese defaultValue fallbacks, so
the en/ja/zh-TW UIs rendered Chinese text. Add the translations across all four
locales.
Addresses review feedback on #3879.
* fix(providers): don't clear managed OAuth binding on status-query failure
useManagedAuth now exposes isStatusSuccess. The account-clear effect in the
Codex/Copilot selector sections only unbinds when the status query has
*successfully* loaded and the bound account is genuinely gone. Previously a
failed or still-pending query yielded an empty accounts array, which silently
cleared the selection; the subsequent save then dropped providerType/authBinding
and turned a managed provider into a broken unbound config.
Addresses review feedback on #3879.
* fix(codex-oauth): make managed accounts safe & usable with the bare CLI
Addresses review feedback on #3879. These backend changes are deeply
intertwined across shared files, so they are committed together.
Refreshable managed auth (bare `codex` support):
- Write a full, native-shaped auth.json bundle (tokens.refresh_token + top-level
last_refresh) instead of an access-only token, so the Codex CLI can self-refresh
and the managed account keeps working past ~1h without a proxy.
- last_refresh reflects the access token's real obtained-at time (not write time),
so the CLI doesn't treat a cached token as freshly refreshed.
- Read back the CLI-rotated refresh_token from ~/.codex/auth.json before writing
(account_id-matched, chatgpt-mode guarded) so re-switching doesn't clobber the
CLI's valid login with a stale refresh_token.
Never persist managed tokens at rest:
- Backfill of a managed provider always replaces live auth with the stored
placeholder (even on marker mismatch), so native tokens can't leak into the
provider's DB config.
- Sanitize managed auth out of Live backups on the initial/strict snapshot paths
and in update_live_backup_from_provider_inner before serialization.
Concurrency / blocking:
- Drop the redundant outer Arc<RwLock<CodexOAuthManager>> (all methods are &self)
so token refresh no longer holds a coarse lock across the network; give OAuth
token/device requests a 30s timeout instead of the shared 600s client default.
- Add a persistence lock and linearize the access-token cache under a consistent
accounts -> access_tokens order (existence-checked reads/writes; remove/clear
clear the cache atomically) to prevent stale or resurrected cache entries.
Switch ordering:
- Preflight the managed token before committing the current provider on
switch/add/update, so a failed token fetch can't leave DB/UI pointing at a
provider whose live config was never written.
Cleanup:
- Remove the now-dead content-fingerprint marker machinery; managed writes are
now shape-identical to a native login, so ownership is judged by account_id.
Known, documented limitations (B-scheme, narrow & recoverable): a remove+re-login
of the same account within an in-flight refresh window (ABA), a login authorized
after logout, and switch-away cleanup being unable to distinguish our write from a
user's native login of the same account.
* fix(codex-oauth): keep managed token in restore backup; fix failing tests
CI caught three test failures from the previous commit. Root cause: the Live
backup (proxy_live_backup) is local restore state that is replayed to
~/.codex/auth.json when proxy takeover ends, so it must contain the real auth to
restore a working login. Stripping it (the earlier "don't leak into backup"
change) broke restore and over-stripped a user's native login.
- Revert the backup auth stripping: remove sanitize_codex_backup_auth from the
initial/strict snapshot paths and the forced auth={} in
update_live_backup_from_provider_inner. The managed token belongs in the
restore backup (the refresh_token is already persisted by CodexOAuthManager, so
this is not a new exposure).
- Restore the ownership marker (account_id + access_token fingerprint) so backup
cleanup can tell our managed write from a user's native `codex login` of the
same account. extract now also tolerates the full-bundle shape (refresh_token +
last_refresh) so it fingerprints ①'s refreshable writes. clear_codex_auth_in_backup
and clear_codex_live_auth_for_managed_account use the marker again; the
account_id-only helper is dropped.
- Fix the adopt unit test: adopt now invalidates the cached access token, so the
test asserts the stored refresh_token/id_token were updated and the cache was
cleared instead of reading it back through get_valid_token_bundle (which would
trigger a network refresh).
Token stays out of the exported provider settings_config (backfill strip) — only
the local restore backup keeps it, matching pre-existing behavior.
* fix(codex): harden managed OAuth credential lifecycle
* fix: preserve Codex restore auth state
* feat(codex): support multiple official accounts
* feat(codex): refine official account login selection
Keep native Codex login separate from managed ChatGPT accounts, migrate provider identity safely when switching modes, and cover the account-selection states with regression tests.
* style(codex): satisfy clippy for login guard
---------
Co-authored-by: Jason <farion1231@gmail.com>
Co-authored-by: YUZHEthefool <2804776511@qq.com>
Managed-OAuth providers (github_copilot / codex_oauth / xai_oauth) need
proxy takeover to inject credentials, but the "needs routing" badge and
the switch-time warning only keyed off apiFormat — missing OAuth providers
whose upstream format is native (e.g. the new Codex xAI Grok OAuth preset
on openai_responses).
- Add isOAuthProviderType / OAUTH_PROVIDER_TYPES as the SSOT for OAuth types
- Extract providerNeedsRouting() as the authoritative predicate: managed
OAuth always needs routing (the backend rejects these accounts' direct
connection) regardless of apiFormat or Claude Desktop mode
- ProviderCard badges (claude / codex / claude-desktop) and the switch
warning consume the shared predicate instead of raw apiFormat
- Gate the warning on per-app routing readiness: claude-desktop uses
isProxyRunning (backend takeover status has no such field), other apps
use isProxyTakeover — fixes false warnings on Desktop and missing
warnings when the proxy runs but the app isn't taken over
- Force proxy mode for all managed-OAuth providers in the Claude Desktop
form (lock the mode toggle), not only xai_oauth
- Add unit tests for providerNeedsRouting, the switch routing gate, and
Desktop OAuth preset enforcement
- i18n(zh/en/ja/zh-TW): add notifications.proxyReasonManagedOAuth