When Codex is on the official provider, a request carrying the proxy
placeholder key comes from one of two places: a Codex process that has
not reloaded its config, or a session created under a third-party
provider. Codex resumes a session with the provider id it recorded, so
the latter keeps using the dormant `custom` table that points at the
local proxy with the placeholder key, and restarting Codex does not
help.
The error used to suggest restarting first, which sent users of the
second case into a loop. It now says that an old third-party session
keeps its original provider and needs a new session, and falls back to
the restart hint otherwise.
Since 4.0 every quota window reads "x% left". Some users had grown used
to the 3.x "x% used" and asked for a way back, while others had asked
for "left" to match Codex itself, so this becomes a setting instead of a
flip: Settings > General > Appearance gains "Quota display" with Left
(default) and Used.
The setting is stored as quotaDisplay in the device-local settings.json
so the tray, rendered in Rust, follows it too. Only per-window
percentages change: balances, Codex Credits and saved resets keep
showing what is left, and a used-up window still reads "used up".
Colors, the "almost out" hint and which windows the card keeps are
still decided by what is left, so 94% used is orange just like 6% left.
The expanded bars now take their length from the number shown, so a
window at 30% used draws a 30% bar instead of a 70% one; balances keep
drawing what is left. SubscriptionQuotaView stays a pure view and gets
the mode from its wrappers.
Closes#8024
The stale-client check reads the process table, which Windows does not
have, so neither the model list notice nor the cached-account notice
ever appeared there, and the restart button always reported that the
daemon was not running.
On Windows the managed daemon is now found through its own daemon.pid:
the process is opened by pid and counts as running only while it is
still active and its creation time matches the recorded one, so a
reused pid is not mistaken for it. Restart is only offered for a daemon
known to be running, since Codex's restart command starts one otherwise.
The desktop app and editor plugins cannot be seen there, so the notice
falls back to "changed since you last dismissed it": the model list and
login fingerprints are recorded in codex-client-ack.json when the user
closes the notice, through the new acknowledge_codex_stale_clients
command, and a later change shows it again with wording that says
CC Switch cannot tell whether those clients are open. Without a record
the current state is taken as acknowledged, so upgrading does not show
it, and the first login seen becomes the baseline, so signing in is not
reported as an account switch.
Closing the notice used to hide it until the app, mode or provider
changed, so changing the model list again on the same provider stayed
silent on every platform. The stack view now carries a revision derived
from the same fingerprints, and the notice reappears once it changes.
Since 4.0, an official Codex login was identified by the id_token subject
alone. The Plus and Team workspaces of one OpenAI user share that subject
and differ only in tokens.account_id, so switching between their cards
was treated as "already signed in as this person": auth.json was never
rewritten and the restart notice never fired. The device login stash was
keyed the same way, so all of that user's workspaces collapsed into one
entry. 3.20.4 wrote the card's login without comparing owners, which is
why downgrading helped.
A login is now identified by user plus workspace, matching how Codex
itself decides whether the account changed.
The stash file gains a version. An older stash is upgraded once on load:
its entries are rekeyed by the new identity, and workspaces that were
merged away are restored from the official cards, but only for users
still present in the stash or in the live auth.json. A user found in
neither has logged out and is not brought back. Switching still restores
a login only from the stash, never from a card snapshot, so a logout is
not revived later either.
Refs #8054
cmd.exe decodes a .bat file with the console code page, but the batch
files are written as UTF-8. With a non-ASCII Windows username, paths
under the user profile were garbled.
Tool install/update scripts now start with `chcp 65001`. They run in a
hidden console of their own, so the code page change does not reach
the user's terminal. This fixes upgrades failing with "The system
cannot find the path specified" for npm installs under the profile.
The Claude provider launcher refers to the temporary settings file as
%~dp0<name>, since it sits next to the batch file, so the --settings
path no longer goes through the file bytes and Claude stops silently
starting with its default config. A non-ASCII project directory is
passed through CC_SWITCH_INTERNAL_CWD instead of being embedded. ASCII
directories are still written literally, so terminals that do not
inherit the caller's environment keep working as before. chcp is not
used here because it would persist in the user's interactive session.
Fixes#8034
The Codex desktop app now lists sessions from every provider, so the
unified history toggle only matters for the CLI resume list, which still
filters by the current provider id. Reword the setting description and
both enable/disable confirmations in all four locales to say so, and
note that desktop-only users don't need to turn it on.
A Codex provider whose config carries base_url (and wire_api) at the top
level with no model_provider selector was projected as a route-less card:
the editor showed an empty request address, and saving stripped both keys
as key fields with nothing to write them back, so the proxy then failed
with "Codex Provider 缺少 base_url 配置". Switching to such a card in direct
mode also sent Codex to the built-in OpenAI route.
The projection now treats a top-level base_url like the legacy
openai_base_url shape and normalizes it into [model_providers.custom], so
the editor shows the address and saving keeps it.
The form no longer produces this shape: with no selector, setCodexBaseUrl
selects a custom table and writes the address into it. The line-based
rewrite is checked against the parsed TOML and falls back to the old
top-level write (which the backend now normalizes) when it cannot be done
safely, e.g. inline tables, commented table headers or multi-line strings.
Fixes#8039
- Explain why Codex sub-agents on another provider cannot read encrypted tasks
and how the classic sub-agent tools setting avoids it
- Explain that Codex locks the picker to Luna Reserve once the official 5-hour
quota runs out, and describe the workaround of temporarily making a
third-party provider the default (#7945, #7962, #7974)
- Update the Chinese guide and the Chinese, English, and Japanese manuals
Codex can compress rollouts untouched for 7 days into a sibling
rollout-*.jsonl.zst and delete the original, either through
[features] local_thread_store_compression or through the app-server
rollout/compress RPC, which ignores that flag. It decompresses a rollout
back to .jsonl when the session is resumed. CC Switch only read .jsonl,
so compressed sessions vanished from the session list, their usage was
never imported, and history bucket migrations skipped them while still
recording themselves as done.
- Add codex_rollout_file, shared by the readers below: recognise both
forms, read .jsonl.zst transparently, write a rollout back in its own
form, and keep only the plain file when both forms briefly coexist.
- Session list: decompress only the head of a compressed rollout. Use
its mtime, which Codex preserves, as the last active time and show no
last-reply preview, so scanning tens of GB stays fast. Content refs are
resolved against the decompressed stream. A stale source path falls
back to the other form, and deleting a session removes both forms.
- Usage import: a cursor is inherited from any path with the same
logical file name, taking the furthest line offset, in any directory
and in both directions. Request-id dedup only covers the 30 days of
detail rows kept before rollup, and compressed rollouts are older, so
this is what prevents double counting across compress and resume.
Compressed files persist their on-disk size so unchanged ones are
skipped without decompressing.
- History bucket migrations rewrite compressed rollouts in place, keep
them compressed, and keep their mtime so old sessions do not jump to
the top of the list.
- Settings > Codex gets a "Compress old Codex sessions" switch backed
directly by config.toml (write true to enable, remove the key to
disable) through the live write engine, showing the current size of
the sessions directories. It re-reads when the saved Codex directory
changes.
The test added in dc93c4962 builds an on-disk SQLite fixture in a temp
dir. The WSL2 nightly points TEMP at a \\wsl.localhost 9P share, where
Windows cannot take SQLite locks, so creating the table failed with
"database is locked" on every retry. Guard it with
sqlite_unsupported_in_temp_dir() like the other SQLite fixture tests.
8919d05cb dropped the neutral switch tone but left the apps page test
asserting data-tone="neutral". Assert that the visibility switches no
longer carry a tone.
Provider search could only be opened with Cmd/Ctrl+F, so users with many
providers did not know it existed.
- Add a search button next to "Add provider" in the page header. It
toggles the existing search panel; the panel closes when switching apps
or leaving the provider page.
- Match the API address as well as name, notes, and website. The address
is read from every app's settingsConfig shape by a new shared
extractProviderBaseUrl, which ProviderCard now also uses.
- Only filter while the panel is open. Closing it with Esc used to leave
the list silently filtered.
- Portal the search panel to document.body. As a child of the list's
space-y container it counted as a sibling, so opening it pushed every
provider card down by 1rem.
- Add a "Show provider search" setting (on by default, device-local).
Hiding the button keeps the Cmd/Ctrl+F shortcut.
With the official account as the aggregation default, CC Switch fetches
the official model list with the version of the `codex` on PATH. The
server trims the list by client_version, and for 0.140 or older it only
returns hidden entries (gpt-5.5, codex-auto-review). Desktop-only users
often keep an old CLI on PATH, so the catalog was written with every
official row hidden and the picker showed only the aggregated third-party
models.
- Use the newest Codex in use: the PATH CLI or the cli_version recorded
on the 20 most recently created threads in Codex's state DB, which also
covers the desktop app and editor extensions on every platform.
- A source with no `visibility = "list"` row is not usable. When the
fetched list has none, fall back to the bundled list (no catalog if that
has none either) and show a new notice asking to update Codex.
Fixes#8014
The app row title kept only the two tiers with the least quota left. With
three windows (5-hour / weekly / monthly, e.g. Volcengine Agent/Coding
Plan, Zhipu, MiniMax) the 5-hour window resets most often and usually has
the most left, so it was almost always dropped. 3.x listed every tier.
Write all tiers in their original order. The "almost out" suffix still
follows the tier with the least left. Remove the now unused pick_lines.
Closes#8011
The neutral switch tone drew the checked track in dark gray next to a
medium-gray unchecked track, so on and off were nearly indistinguishable
in light mode. Drop the neutral tone (its only caller was the apps page
"Show" column) and let those switches use the same action color as every
other switch.
Closes#8010
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
Updating meant checking, then filtering the table to "updates", then
finding "Update all" in the filter row. The confirm dialog also only gave
a count, and it stayed open until every Skill had finished downloading.
- The "Check updates" button turns into "Update all (N)" once updates are
known; re-checking moves to the "..." menu. A check that finds updates
opens the confirm dialog directly instead of a toast.
- Drop the duplicate "Update all" button from the filter row.
- The confirm dialog lists each Skill with a clickable source repo that
opens the same page as the row link. V7ConfirmDialog gains a `details`
slot because the body renders inside a <p>.
- Confirming closes the dialog at once; the toolbar button shows
"Updating 1/2..." and the row menu spins until the batch is done.
- Remove the now unused `skills.updatesFound` string; add
`skillsPage.confirm.updateListAria` and `skillsPage.updatingProgress`
in all four locales.
ProxyChat and Copilot catalogs cloned the gpt-5.5 entry from Codex's own
models_cache.json (falling back to `codex debug models --bundled`, then the
bundled copy). The template only supplies the tool contract — slug, name,
context window and visibility are all overwritten — but every change
OpenAI made to that cached entry leaked into third-party models: the
hidden flag behind #7924/#7954, and newer entries carry
`tool_mode: code_mode_only`, v2 sub-agents and experimental tools the
Chat converter was never built for. The cache also announces GPT-5.5's
retirement on 2026-10-14, after which every switch would fall through to
spawning the Codex CLI.
- Always use the bundled template for ProxyChat / Copilot; drop the cache
and CLI lookups and the per-process template cache.
- Refresh the bundled template to match Codex 0.162's own gpt-5.5 entry:
`shell_type: unified_exec`, and include the skills / plugin usage
instructions (both default to false in codex-rs when absent).
- Rename the loader to `load_codex_classic_tool_template` and document
why it stays on the classic gpt-5.5 shape.
- Tests lock the classic tool contract and that a hostile
models_cache.json no longer affects generated entries.
Existing ProxyChat sessions lose the cached `comp_hash` and get compacted
once on resume.
The sidebar used the same neutral dot for two different things: tool
updates on "Apps" and a new CC Switch release on "Settings". The dot was
barely darker than the icon, and clicking "Settings" landed on General,
so the update button was still two steps away.
- The CC Switch update dot on "Settings" and on the About row is now the
success green. Tool updates on "Apps" keep the neutral dot.
- Clicking "Settings" in the sidebar while an update is available opens
About directly. Cmd+, still opens General.
- Add the Chinese aggregation guide with the final cover, client screenshots, and request-routing diagram
- Update Chinese, English, and Japanese manuals to distinguish model fetching, models.dev metadata completion, and manual entry
- Clarify official subscription support, default models, and client restart requirements; add README links
- Validate 90 local documentation links, five images, and Git diff formatting
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
A v2 sub-agent's task arrives as the trailing agent_message whose
payload only the main agent's backend can decrypt. The opaque-state
rectifier used to replace that payload with a placeholder like any
other foreign ciphertext, so a retry sent the sub-agent an empty task.
The upstream also often rejects it inside a 200 stream, which bypassed
the rectifier entirely and left Codex retrying the same failure.
- Keep the current task untouched when rectifying; earlier agent
messages and other foreign state are still cleared.
- For native Responses passthrough requests ending in an encrypted task,
hold the stream until the first non-lifecycle event under one fixed
deadline (first-byte timeout, at most 10s), so heartbeats cannot extend
it. A self-identified rejection before output joins the normal
rectifier path; anything else replays byte for byte.
- When nothing else is left to clear, or the retry is still rejected,
return a neutral 400 unreadable_encrypted_agent_task that names the
classic sub-agent toggle, instead of letting Codex retry.
Codex multi-agent v2 has the main agent's backend encrypt the task it
hands to a sub-agent, so in aggregation mode a sub-agent on a different
provider cannot read its task. Codex picks the tool version from each
catalog row's multi_agent_version, which the aggregated catalog copies
from the official catalog (v2 for Sol and Terra).
Add a device-level setting, off by default, that stamps
multi_agent_version = "v1" on every row of the aggregated catalog
(official rows included: ciphertext minted by OpenAI is just as
unreadable elsewhere). Classic tools pass the task as plain text at the
cost of v2-only tools such as followup_task and interrupt_agent. With
the setting off the catalog is unchanged.
Changing the setting rewrites the catalog right away when Codex is in
aggregation mode and rolls the setting back if the write fails. When
config.toml forces [features] multi_agent_v2, which Codex applies before
catalog rows, the settings row says the toggle has no effect.
The all-gray quota text made it hard to tell a healthy limit from one
that is running low. Restore the v3 green / orange / red, but color only
the number ("88%", "¥82.10", "$2500", the reset count) so tier names and
"left" stay gray. Lines with no number (used up, expired, query failed)
are still colored as a whole.
- Warn once a tier has less than 20% left (was 10%), in the card and in
the tray's "almost out".
- The "used $X" line has no total, so it stays uncolored.
- The expanded bars and the auth center keep neutral numbers while
normal, since the bar already carries the color.
The all-time heatmap only covered the last 53 weeks, so older usage was
cut off. It now keeps the last 53 weeks as the default view and adds a
"Show more" toggle that lists earlier history in contiguous 53-week
spans, back to the first recorded day. Each span is titled with its
exact date range and totals, and shading is bucketed across all spans
so they stay comparable.
A new get_usage_first_date command returns the earliest local day across
request logs and daily rollups, honoring the dashboard's app, provider
and model filters, so the heatmap knows how many spans to fetch in one
trends query.
The 9P share sometimes aborts a write mid-test with os error 995
(ERROR_OPERATION_ABORTED): a Claude Desktop config test passed in 1.3s
on one run and failed after 17s on the next with no code change.
Retry up to twice in the nightly only. The ci profile used by PRs keeps
failing on the first error, and nextest still lists retried passes as
FLAKY so repeat offenders stay visible.
The WSL2 nightly now reaches the lib suite: 3299 of 3302 pass. The rest
are limits of \\wsl.localhost, not product bugs:
- Windows cannot create symlinks on the share ("Incorrect function"),
so the catalog escape and grokbuild symlink-cycle tests cannot build
their fixtures. Skip when the link fails under a WSL temp dir and
keep failing anywhere else.
- Extracting 13,248 files takes over the 180s per-test timeout on 9P.
Production extracts to the system temp dir, so skip the entry-limit
test when TEMP is a WSL share.
The nightly has failed since 10-05 with LNK1327: the contract step
re-invoked cargo with TEMP on \\wsl.localhost, cargo re-linked the lib
tests, and mt.exe cannot create its manifest temp file on a UNC path.
Build the lib tests once into a nextest archive with the native TEMP,
and run both the contract test and the lib suite from that archive, so
nothing is built while TEMP points at the WSL share. The archive is
extracted to the runner temp, and --no-tests fail replaces the manual
check that the contract test was discovered.
Supersedes #6856.
Bring the zh, en and ja user manuals up to the v4.0.4 interface: the
sidebar layout, the six Settings groups, the Direct / Routing /
Aggregation connection modes, Accounts, and the Apps page. Old
Settings paths (Advanced, Routing, Auth) and takeover wording are gone.
- Add 4.6 Aggregation Mode in all three languages and list it in the
manual indexes.
- Rewrite 1.3 Interface Overview and 1.5 Settings around the new
layout; refresh provider, extension, routing, usage and FAQ pages.
- Use the v4.0 release screenshots where they fit and leave
TODO(v4 screenshot) markers elsewhere.
- Update the version section of each manual index to v4.0.4.
- Remove the 40 v3 screenshots that are no longer referenced.
The settle window that delays Grok Build session imports reused the
±10-minute takeover guard window, so a finished turn in Direct mode took
10-11 minutes to show up in usage stats. The settle window only needs to
cover the few seconds between turn_completed being written and the proxy
rows for that turn landing in the database, so cut it to 60 seconds and
keep the guard window unchanged.
A compile-time assertion keeps the settle window below the guard window,
and two tests cover a 90-second-old turn importing and a proxy row that
lands a few seconds after the event still blocking the import.
Add Claude Desktop, Grok Build, Hermes, Pi and MiniMax Code to the app
dropdowns, bump the version examples to 4.0.4, and point to
Settings → About for the version number.
- Replace the v3.8 screenshots with six v4.0 screenshots (main, apps,
aggregation, Codex aggregated models, session reader, usage)
- Rewrite Quick Start around the sidebar and the Direct / Routing /
Aggregation tabs, add the Apps page and Accounts, and note that the
project switcher is now hidden by default
- Add an Aggregation column and section; tray switching now covers
Claude Desktop and Pi now supports MCP
- Replace v3 settings paths in the FAQ with the v4 locations and add an
entry on restarting Codex after the aggregated model list changes
- Point the release notes link at v4.0.4
At the minimum window width (900) with the sidebar expanded, the request
log table area is only 644px wide, but the table had min-w-[700px]. The
rightmost speed column was pushed into the table's horizontal scroll,
whose scrollbar sits below the last row and is usually off-screen, so
users saw the column as missing.
Lower the minimum to 620px, matching the provider, model and pricing
tables. The provider and model columns already truncate (max-w-0), so
they absorb the squeeze.
The project switcher showed on the providers page header and in the tray
for every new install, even though most users never create a project.
Flip the default so it stays out of the way until turned on in Settings.
- Use plain #[serde(default)] for show_profile_switcher and drop the
default_show_profile_switcher() helper; AppSettings::default() now
sets it to false
- Change the frontend fallbacks in App.tsx and GeneralSection from
?? true to ?? false so the header does not flash the switcher before
settings load
Existing users keep their current behavior: the field has been
persisted to settings.json since v3.17.0, so anyone who has saved
settings already has an explicit true.
The star prompt sat at the end of the link row with ms-auto, so it
wrapped onto a line of its own and read like a stray underlined caption.
- Move it into a tinted strip at the bottom of the card with a slowly
spinning star (stops under prefers-reduced-motion) and a "Star on
GitHub" button that opens the repo
- Drop the separate GitHub button while the strip is shown, since the
star button goes to the same place
- Add a close button; dismissal is remembered per device in
localStorage, and the plain GitHub button returns to the link row
- Add settings.starOnGithub to all four locales
tauri::process::restart spawns the binary directly. Since macOS 14 app
activation is cooperative, so the new process's activation request is
denied and its window stays behind other apps after an in-app update or
a config-dir restart.
restart_process now launches the bundle with `open -n` on macOS, letting
the still-frontmost old instance hand activation to the new one, and
falls back to tauri::process::restart if that fails. restart_app uses
restart_process instead of app.restart() so both restart paths behave
the same.
first_user_messages_is_scoped_to_listed_sessions inserts 1002 rows, each
in its own autocommit. On the Windows runner every commit flushes to disk,
so the test took 150-180s and intermittently hit nextest's 180s timeout
(2.5s on Linux). Wrap the fixture inserts in a single transaction.
The toggle shipped without a manual row. Describe what it changes, the
per-session notice it avoids on gateways, the new-provider default and
that official endpoints should leave it unchecked; the per-provider note
now covers three options instead of two.
The quick toggles above the Claude config editor had no explanation,
except a native title tooltip on the auto mode one. Put the standard "?"
help tip after each label: one sentence on what the toggle does, why
you would want it, and whether it is global or follows the provider.
Render the toggles from one list so every entry gets the same
checkbox + help layout, with copy under claudeConfig.<key> and
claudeConfig.<key>Help. The help button sits outside the <label>, so
opening the tip never flips the checkbox.
CLAUDE_CODE_EFFORT_LEVEL=max pins every session to the most expensive
effort and turns /effort into a no-op ("Not applied: ... overrides
effort this session"), while Claude Code deliberately keeps max
session-only (effortLevel in settings.json does not accept it). Users
who want max can still pick it per session with /effort max, or set the
variable by hand.
Remove the checkbox, its i18n keys, tests and the manual rows. Configs
that already carry the variable keep it untouched.
Claude Code now gives every session a single implicit team: named
subagents, SendMessage and ListAgents are available without
CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS (on 2.1.287 the tool list is
identical with and without it). The variable only gates tmux/iTerm2
split-pane teammates and the structured shutdown/plan-approval
messages, which no longer justifies a slot among the quick toggles.
Remove the checkbox, its i18n keys and the manual rows. Configs that
already carry the variable keep it untouched.
Add a "Disable auto mode server check" quick toggle to the Claude
provider editor that writes CLAUDE_CODE_AUTO_MODE_SERVER=0, with a hint
explaining that third-party gateways cannot serve the check and that
official endpoints should leave it unchecked.
New Claude providers now get the key by default unless the preset is
official or a cloud provider, or the preset already sets it. The default
form (no live base yet) also goes through the overlay so the custom
provider starts with the same value.
Backup & Restore only listed database snapshots, while other backups kept
growing unseen under ~/.cc-switch: Codex session history migration copies
can reach several GB, plus skill uninstall backups, config first-write
backups, Hermes/OpenClaw config backups and files left by older versions.
A collapsible "Other Backups" section below the database list now shows
each location with its path, size and item count, and lets the user reveal
it in the file manager or delete a whole category after a confirmation that
explains what is lost (e.g. deleting the Codex unify migration backups
removes the "restore backup" option when turning that switch off).
Database snapshots are only summarized there and stay managed per file in
the list above.
The frontend only passes category ids; paths are computed by the backend
from each writer's own location, and deletes never follow symlinks. Codex
migration backups are deleted under the migration lock and skill backups
under the skills state lock, so a running migration or uninstall cannot
lose its directory mid-write.
OpenCode ended its referral program on 2026-09-28. Since then any /go page
opened with a ?ref= parameter shows a warning that referral links no longer
earn credit, so the "Get API Key" link on the five OpenCode Go presets
greeted users with that banner. Point them at the plain /go page instead.
Large, well-established aggregator platforms such as OpenRouter and
SiliconFlow can still get a regular built-in preset; the sponsors-only rule
applies to the remaining relay and aggregator services.
OpenCode Zen is the pay-as-you-go side of the same workspace key that
OpenCode Go uses, so both now sit in one "OpenCode" family row (Go as the
coding subscription, Zen as pay-as-you-go).
Both presets connect directly without local routing: Claude Code talks to
Zen's native /v1/messages with ANTHROPIC_API_KEY (the endpoint only reads
x-api-key), and Codex talks to /v1/responses. Codex defaults to gpt-6-sol
because Zen currently returns 403 "Model access is disabled" for
gpt-5.6-sol. Free Zen models are rejected outside OpenCode, so no preset
defaults to them. OpenCode and Pi already ship Zen as a built-in provider
and get no preset.
Closes#6016
Built-in presets are limited to model vendors and cloud platforms that serve
models under their own brand. Relay and aggregator services that resell other
vendors' models are included only as project sponsors; everything else keeps
working through Custom Provider.
The model stats tab lacked the success rate and speed columns the provider
tab has, and carried an extra average-cost column, so the two tabs read
inconsistently.
Compute success count and the exact / estimated speed totals per model in
get_model_stats, mirroring get_provider_stats (rollup rows count toward the
success rate but not speed, as they have no per-request timing). Extract the
success-rate and speed header/cells into shared statsColumns so both tables
render them identically, rename the cost header to "Cost", and move the
average cost per request into the cost cell's hover title.
The request log table truncated "Claude Code · Session logs" in its narrow
provider column, and the request detail repeated the app name already shown
next to it. Show just "Session logs" in those two places; the provider
filter and provider stats table keep the full name since they have no app
column to tell the session-log rows of different apps apart. The hover
title still carries the full name and the explanation.
When Claude Code requests are converted to OpenAI Chat / Responses, the
effort was forwarded verbatim even with thinking turned off. Claude Code
keeps sending `output_config.effort: "high"` when thinking is off, and
Workflow subagents send `thinking: disabled` together with an effort, so
GPT and Grok upstreams still reasoned at full strength.
- Clamp the effort to `low` when thinking is absent or disabled. `none`
and `minimal` are not accepted by every whitelisted model.
- Clamp to the tiers the target model accepts: gpt-5-pro only takes
`high`, and gpt-5.2/5.4/5.5-pro take medium/high/xhigh, so `low` is
raised to `medium`. This also covers an explicit `/effort low` or a
small thinking budget sent to a pro model, which used to return 400.
- The 5.x Claude models (Fable, Mythos, Opus, Sonnet) keep adaptive
thinking on when `thinking` is omitted, and Claude Code omits it for
Fable and Opus 5.5 when thinking is "off" because they cannot turn it
off. That default is lost once the model is mapped to another vendor,
so the forwarder now makes `thinking: adaptive` explicit from the
client's model before the Chat / Responses transform. The Messages
passthrough is unchanged.
- Add Opus 5 / 5.5 to `adaptive_thinking_is_default`, matching the
per-model table in the Claude thinking docs.
Usage imported from client session logs (routing off) was labelled with
hardcoded English placeholders like "Claude (Session)", which showed up
untranslated in every locale and read ambiguously next to Claude Desktop.
Display them as "<full app name> · Session logs" in all four locales,
matching the "Session logs" wording already used elsewhere on the usage
page, with a hover hint explaining why the provider can't be told apart.
The backend placeholder names stay unchanged because the provider filter
sends them back for exact matching; only the rendered label is mapped.
Importing a skill discovered in one app while ticking other apps only
stored the enabled flags. The UI then showed the skill as enabled for
those apps, but their skill directories never received it until the next
provider switch happened to re-sync, so the client could not read it.
Project the skill into each ticked app that does not have it yet as part
of the import. Copies that already exist in an app directory are left as
they are. If deploying to an app fails, that app's flag is not recorded,
so the stored state always matches what is on disk, and the failure is
listed in the import error next to any skipped MiniMax Code entries.
wham/usage already returns a `credits` block next to the rate-limit
windows ({has_credits, unlimited, balance}, with balance as a string such
as "62500"). Parse it into `SubscriptionQuota.credits_balance`; a zero,
unlimited, missing or malformed balance is simply left out and never
fails the quota itself. No extra request is made.
Codex credits are metered at API prices: the official rate card's
credits per 1M tokens equal the API dollar price x 25 for every model,
so one credit is $0.04. The conversion lives in a single constant.
- Expanded view / auth center: a "Credits" row with the credit count in
the middle column and "about $2500" as the value.
- Provider card: credits are only spent once a limit runs out, so the
balance stays off the card until a tier is used up, then appears in
dollars ahead of the saved resets.
- Card reset drop-down: the balance is listed below the expiries; a
single saved reset can now drop down too when there is a balance.
The provider and model tables summed only fresh input + output, while
the metric cards and trend chart show real total tokens (fresh input +
output + cache writes + cache reads). With a high cache hit rate the
table rows added up to a tiny fraction of the headline total.
Add real_total_tokens_sql and use it in both the detail and rollup
branches of get_provider_stats and get_model_stats, so the rows sum to
the summary's real_total_tokens.
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.
In Aggregation mode all four model tiers point at the default
provider's first model, and Claude Code keeps its built-in Opus /
Sonnet / Haiku rows (plus Fable once its tier is set) above the
discovered models. /model therefore opened with four identical rows for
that model, and the model's own discovered row was deduplicated away.
Dropping the tier variables does not hide those rows: they fall back to
claude-opus-5-5 and friends, which would go to the upstream as is.
- Aggregation mode now writes a top-level modelPicker to settings.json
with replaceBuiltInOptions: true and one row per aggregated model
(Stack id, display name, "<upstream model> · <window>" description).
The picker shows Default plus these rows only. The four tier variables
are still written: Default, --model opus and subagent aliases resolve
through them.
- modelPicker is a Claude top-level key field, so every switch replaces
it: Aggregation writes the list, routing and direct leave none. A
picker the user set by hand is not kept, the same rule as Codex's
model_catalog_json pointer.
- CLAUDE_CODE_ENABLE_GATEWAY_MODEL_DISCOVERY is no longer written. A
value written by an older contract is removed on the next rewrite like
any other exclusive field, and a user's own value is left alone.
- The Claude contract digest includes top-level fields only when there
are any, so routing contracts stay byte-for-byte unchanged.
Checked against Claude Code 2.1.287 with an isolated config directory
and a fake gateway: the picker lists only the aggregated models, and a
running session picks up changes to modelPicker and to the tier and
window variables without a restart.
The add and edit forms chose the Aggregation layout from the mode the
app was actually in, with a link to toggle between the simple and the
full form. Opening a provider from the Direct or Routing tab while
Aggregation was on still showed the simple form, and the toggle was one
more control to explain.
- SwitchModePanel reports the tab being viewed (Direct / Routing /
Aggregation), and App passes it to the add and edit dialogs as
modeView. Claude Code and Codex use the simple form only when the form
is opened from the Aggregation tab; without a tab (the tray's "add"
switches apps first) the app's actual mode decides as before.
- The "Show full form" / "Back to the Aggregation form" link and its
providerForm.stackLayout.fullForm / simpleForm keys are removed.
- The dialog header shows "<app> (Aggregation mode)" while the simple
form is in use (new provider.formSubtitleStack key in all four
locales).
Fetching the model list and picking a model is the easy way to fill a
row: the context window and reasoning levels are filled from presets and
models.dev. In the full Codex form, though, fetching only added a small
dropdown arrow beside each row's model field, and nothing visible at all
when the table was empty, so users had to add a row, find the arrow and
pick a model.
- The model mapping section now shows the same searchable picker as the
Aggregation layout once models are fetched. Picked models become rows
named after the model ID, with the context window and reasoning levels
filled. The per-row dropdown stays for swapping one row's model.
- The add buttons next to "Fetch models" in the Codex model mapping and
model list, the Claude Code Aggregation model list and the OpenCode
model list are renamed to "Add manually", marking them as the fallback
for models the fetched list does not have.
- The picker's "Add selected" button is neutral instead of solid, so
Save stays the only solid button on the provider form.
Picking many reasoning levels for a Codex model catalog row pushed the
levels column wider, and the rows stopped lining up with each other and
with the header.
- Each catalog row is its own grid, and its `1fr` tracks could not shrink
below their content, so a long label such as
"none,low,medium,high,xhigh,max" widened its own row. The header and
row templates now use `minmax(0,1fr)`, so every row splits the width the
same way and the label's existing truncation takes effect.
- The trigger label collapses each run of three or more adjacent levels
into "first → last": "low → max", or "none, low → max" when minimal is
skipped. A range never spans a skipped level, so it cannot suggest a
level that was not picked. Shorter runs, gapped picks such as
"low, high, max" and lists that are not in canonical order are shown in
full.
- Hovering or keyboard-focusing the trigger shows every picked level and,
when one is set, the default level (new codexConfig.defaultReasoningLevelTip
key in all four locales).
Drafted from the commits since v4.0.1, ordered by impact: the 400 error
that broke Claude Code on a ChatGPT account (#7876), Codex aggregated
models hidden by a leftover model catalog setting, MCP sync to Pi 1.0
(#7862), and the time MCP preset that exited on start (#7863).
Provider icons all sat as an 18-20px glyph inside the same 32px bordered
box, so icons that carry their own solid background (88API, Qiniu,
CherryIN, ...) showed up as a small square inside a square.
- A new ProviderIconBox replaces the box duplicated across the provider
card, the name field's icon button and the two preset selector lists.
Icons marked `shape: "tile"` in metadata fill the whole box, clipped
to its radius, with an inset ring drawn above the image so white
tiles keep an edge on white cards. Transparent logos keep the bordered
box and grow from 18px (forms) / 20px (card) to a shared 22px, which
matches the visual weight of the tiles without crowding the border.
- 28 icons are marked as tiles: solid squares, rounded tiles, circles
and white-background images. UCloud and SSSAiCode gain metadata
entries for this, which also makes them searchable in the icon picker.
- Icons with wide transparent margins are cropped to their content:
a6api, byteplus, nekocode, etok, sudocode-us, claudeapi and
teamorouter as lossless square crops (kept pixels identical), and
crazyrouter, aigocode, yi and zenmux through a tighter viewBox (paths
untouched). The Pi mark keeps its padding: it is the official README
mark and its viewBox is pinned by a test.
- Xiaomi MiMo switches from a one-line wordmark, which shrank to a thin
line at icon size and turned black-on-dark in dark mode, to the
official favicon style: the two-line lobe-icons wordmark in white on a
black tile.
- FennoAI's webp had a fake transparency checkerboard baked in. It is
replaced by a 256px PNG with real transparency, cropped to the fox.
The in-app updater only follows stable releases and its manifest notes
just say "Release vX", so users jumping between stable versions never
saw what the versions in between changed. The full release notes are
too long for a popup.
- Each version gets src/whats-new/<version>.json: up to 4 one-line
items (new / fix / improve) in zh, zh-TW, en and ja, bundled into the
app at build time. "items": [] marks a version that should not pop up.
- On the first launch after an update, a dialog lists every non-empty
version between the last one seen and the current one, newest first.
Older versions beyond the first three are collapsed. Each version
links to its page in the website changelog.
- The last seen version is stored per device in settings.json
(whatsNewSeenVersion) and only moves forward, so a downgrade and
re-upgrade does not repeat anything. Without a record, only the
current version is shown. When there is nothing to show, the version
is recorded silently so the next update has a correct starting point.
- The dialog waits for the welcome and new-layout dialogs. Confirming
either of them also records the current version, so a 3.x user does
not get two dialogs in a row.
- Settings > About gets a "Recent updates" button to open the summaries
again. The 4.0.0 summary is included so that list starts from 4.0; no
update popup can reach it, since only 4.0.2 and later record a version.
- The release workflow first requires the tagged version's summary file
and validates every bundled summary with the unit test, since a
malformed item would crash the dialog and put the whole app on the
error screen.
A model_catalog_json in config.toml that no provider owned was never
touched, and CC Switch skipped writing its own pointer while it was
there. A pointer left over from an older version could keep the
generated catalog (model mapping or aggregated models) from taking
effect, with no warning at all in direct mode. Users then tried to write
the pointer by hand, which every later write removed again.
- The pointer now follows the target provider like model does: the
provider's own pointer if its config has one, otherwise CC Switch's
catalog when one is generated, otherwise none. Whatever config.toml
held before is replaced.
- Drop the configOwnsCatalog warning and the live-pointer handling behind
"Use CC Switch's model catalog". The button stays for a routed provider
whose own config points at another catalog.
- The provider editor no longer shows a pointer it would only carry over
from config.toml, so the special case that kept it out of the card is
gone too.
When the routed provider's row or config.toml points model_catalog_json at
another file, Codex reads only that file and aggregated models never show
up. The warning only told users to edit the config by hand.
- The routeOwnsCatalog / configOwnsCatalog warning now has a "Use CC
Switch's model catalog" button. It removes the pointer from the routed
provider's row, removes the user's pointer from config.toml (only while
it still holds the same value), and rewrites Codex even when the
contract is unchanged, so the merged catalog takes effect.
- The provider editor no longer stores a model_catalog_json it only
carried over from config.toml. Saving a card as is used to turn the
user's global pointer into that card's own pointer (seen on the seeded
OpenAI Official card, whose config starts empty). A pointer the user
changes in the editor is still kept on the card.
- macOS used one runner that compiled aarch64 and x86_64 one after the
other inside `tauri build --target universal-apple-darwin`, and every
notarization retry recompiled both. Now `macos-binary` compiles each
arch on its own runner with --no-bundle, and `macos-release` merges
them with lipo and runs `tauri bundle --target universal-apple-darwin
--bundles app`. Only that bundle/sign/notarize step is retried. The
styled DMG from create-dmg, the zip and the updater tar.gz are made as
before. `macos-binary` carries the release environment, so a release
is still approved once at the start; `macos-release` must not, or it
would wait for a second approval halfway.
- The Tauri signing key preparation moves into a composite action shared
by the Linux/Windows matrix and `macos-release`. Its output matches
the old inline script for every key format tried.
- Windows x64 builds only the MSI. Tauri also built an NSIS setup.exe
that was never uploaded; the portable zip still comes from the exe.
- latest.json is generated inside publish-release from the downloaded
signatures, before the release is created, so a missing platform
fails without leaving a half-published release. The
assemble-latest-json job is gone.
- workflow_dispatch runs the full build, signing and notarization as a
dry run, with assets kept as workflow artifacts. Only a tag push
publishes, even when a dispatch is started on a tag; the concurrency
group includes the event name so a dry run cannot cancel a release in
progress; and `/` in branch names becomes `-` in asset file names.
- Quote the $GITHUB_OUTPUT redirects that shellcheck flagged.
Pushes to main ran every job on the theory that they keep the caches
warm, but a cache whose key hits exactly is never saved again, so those
full runs refreshed nothing. Pushes now skip unaffected jobs like pull
requests do.
A scheduled run every Monday (03:41 Beijing) and workflow_dispatch run
everything, rebuilding caches after eviction: the filter step only runs
for pull_request and push, and the `|| 'true'` fallback on its outputs
turns a skipped filter into "both areas changed". The concurrency group
now includes the event name so the scheduled run and a push to main do
not cancel each other.
- Backend tests run with `cargo nextest run --profile ci`: one process
per test, a 180s per-test timeout (src-tauri/.config/nextest.toml),
and no fail-fast. The WSL2 nightly uses it too, still one test at a
time and with the same selection as the old `--skip database::`, so a
hung test is killed instead of eating the whole job.
- Integration tests shared one fixed test home, and each process wiped
it on startup. Under nextest they ran concurrently and failed en
masse, so tests/support.rs now picks a directory per
NEXTEST_TEST_GLOBAL_SLOT. Plain `cargo test` keeps the old directory.
- Swatinem/rust-cache replaces the hand-rolled actions/cache step. It
caches dependencies only, not the cc-switch crate that is rebuilt
every run, and keys on OS, rustc version and Cargo.lock. The WSL2 CI
job and the nightly share the `backend` key but only restore it, so
their lib-only builds never save over the full Windows cache.
- Quote the $GITHUB_OUTPUT redirect that shellcheck flagged.
`cargo clippy --all-targets -- -D warnings` failed on two tests: an
`op_ref` comparison against `&data_url` in transform_codex_chat and a
`type_complexity` row tuple in usage_stats. Compare by value and name
the row type. CI's clippy does not lint test targets, so this only
unblocks the stricter local run.
With codegen-units = 1, LLVM optimizes the large main crate on a single
thread, and that dominates release build time. 16 units make the main
crate compile about 64% faster for a binary about 17% larger, measured
locally. Applies to every release target; thin LTO and opt-level "s"
are unchanged.
dtolnay/rust-toolchain@stable installed the latest stable, then rustup
switched to the 1.95 channel from rust-toolchain.toml on the first cargo
call, so every job downloaded two toolchains. Read the channel from
rust-toolchain.toml and pass it as `toolchain` in the CI backend and
WSL2 jobs, the release build and the WSL2 nightly, keeping the file as
the single source of the compiler version.
- macOS: tee each build attempt to a log, and stop on HTTP 401/403 from
Apple instead of retrying. An expired developer agreement cannot be
fixed by a retry; for v4.0.0 the loop rebuilt both architectures twice
more and ran into the job timeout.
- Treat a missing macOS .tar.gz.sig, Windows MSI or MSI signature
(either arch), AppImage or AppImage signature as an error. They used to
print a warning, and latest.json then silently left that platform out,
so its users would get no update.
- assemble-latest-json downloads only the *.sig files it reads and fails
if any of the six platforms lacks a signature.
The nightly has not passed in weeks: 37 tests failed on the
\\wsl.localhost share before the run hung. They fall into a few causes
that come from the 9P filesystem, not from product bugs:
- Windows cannot lock SQLite files there, so every on-disk SQLite
fixture fails with "database is locked". Skip `database::` by name and
let fixture tests return early via config::sqlite_unsupported_in_temp_dir.
The account recovery test now uses an in-memory DB that it reuses
across the simulated restart.
- Integration tests all create the app database, so run --lib only.
Users keep that database in the Windows home unless they override the
app config dir.
- The share exposes no file identity, so parent timelines are not cached.
The five cache tests now check reuse only where parent_cache_supported.
- Remote symlinks are not followed. The catalog escape test asserts None
when the link resolves, and otherwise that the result is unreadable.
With one test binary left, the timeout drops from 90 to 30 minutes.
When the Codex config dir points into a WSL distro (\\wsl$ or
\\wsl.localhost), Windows reaches state_*.sqlite through the 9P
redirector, which has no byte-range locks. Every open waits out the 5s
busy timeout and fails with "database is locked", so the history
migration never wrote its completion marker and retried on each start,
and every session scan stalled loading thread titles.
Skip such DBs in the history migration, its restore path and the
session title loader, logging one warning. The migration counts them as
zero rows and completes; titles fall back to the session index.
is_wsl_path moves from claude_mcp into config so both can share it.
When the replay cache mutex was poisoned, sync_single_codex_file went
through `if let Ok(caches) = replay_caches().lock() { .. } else { .. }`.
Under Rust 2021 the scrutinee temporary lives until the end of the else
branch, so the PoisonError still held the guard while
resolve_parent_signatures locked the same mutex again, and the thread
hung. The WSL2 nightly hit this after an unrelated test panicked while
holding the lock.
Copy the cache lookup out before releasing the lock, and have
clear_codex_replay_caches recover from and clear the poison. A
regression test poisons the lock and times out after 30s on the old code.
Rewrite the unused skipClaudeOnboardingDescription in all four locales to
say what the toggle actually does (writes or removes hasCompletedOnboarding
in ~/.claude.json) and wire it to the row's help popover.
When the reset-credits dropdown sits on its own card row, the chevron now
takes the countdown slot's icon position, lining up under the clock of the
row above; text right edges stay aligned. Merged rows are unchanged.
The Grok Build form passed the preset's websiteUrl to the API key link,
so sponsor presets opened the plain homepage instead of their referral
signup URL. Prefer the selected preset's apiKeyUrl, matching
useApiKeyLink, without changing the saved websiteUrl.
OpenCode only generates thinking variants (ctrl+t) for models whose
capabilities.reasoning is true, and falls back to models.dev only when
the provider key matches a models.dev provider. CC Switch writes custom
provider keys, so preset and fetched models never got any variants.
- Fill `reasoning: true` when a fetched model is known to reason (preset
row or models.dev); never write false, which would shadow models.dev.
- Mark reasoning-capable models in OpenCode presets, cross-checked with
models.dev; skip non-reasoning, router (`auto`) and mixed entries, and
LongCat 2.0 whose preset turns thinking off.
- Let OpenCode derive the variants itself instead of hardcoding effort
tables that drift between OpenCode releases.
- Remove the unused getPresetModelDefaults helper.
Balances under 10% of their total were drawn darker and bolder (and the
tray title added "running low"), so a few cards showed the same kind of
number in different shades with nothing saying why. Balances now stay
gray and only turn red at zero, on the card, in the expanded bar and in
the tray. The left percentage is still computed from the total, so bar
length and which lines get picked are unchanged. Per-tier percentages
keep their under-10% emphasis.
The v7 quota redesign moved reset times into the native hover title, which
takes over a second to appear and gives no hint it is there, so in practice
they were gone.
- Provider cards: each quota row now ends with a fixed-width "⏱ 2h30m" slot
(empty when that row has no reset, so rows stay aligned). A merged row
shows its earliest reset; the hover text still lists every tier. The
column only widens (136 → 194px) when some tier has a reset time.
- Expanded quota bars write "Resets in 2h30m" after the value.
- Auth center account rows get the same countdown slot.
- Countdowns are computed at render time from resetsAt instead of being
baked into the line when built, so they no longer go stale between
queries.
- Copilot no longer overwrites the hover detail, which dropped its reset.
Add the v4.0 release notes and the screenshots they reference
(main page, Apps page, Aggregation, Codex model picker, session reader
and usage, in Chinese and English) under assets/release-notes/v4.0.0.
Tu-zi (api.tu-zi.com) is a new-api based aggregator. It is not a
sponsor, so the presets sit in the non-sponsor block of each file.
- Presets for Claude Code, Claude Desktop, Codex, Gemini CLI, Grok
Build, OpenCode, OpenClaw, Hermes and Pi. Endpoints follow the vendor
wiki (wiki.tu-zi.com): Anthropic- and Gemini-style clients use the bare
host; Codex, Grok Build and the OpenCode Anthropic SDK use /v1.
- Claude Code uses ANTHROPIC_API_KEY, as the vendor's own CC Switch
guide tells users to switch from ANTHROPIC_AUTH_TOKEN.
- Claude Desktop uses the bare host although the vendor's Desktop guide
shows /v1: Desktop appends /v1/messages itself, and /v1/v1/messages
returns 404 on this gateway while /v1/messages reaches auth.
- Multi-model apps list Claude Opus 5, Sonnet 5 and Haiku 4.5. The
public pricing catalog marks Opus/Sonnet 5.5 with a "claude code"
endpoint type rather than "anthropic", so they are left out of the
SDK-based clients. Codex defaults to gpt-5.6-sol, Gemini to
gemini-3.8-flash, Grok Build to its default grok-4.5.
- Endpoint candidates add the vendor's Guangzhou (api.ourzhishi.top),
Los Angeles (api.sydney-ai.com) and Cloudflare (apicdn.tu-zi.com)
relays, which front the same backend, in the five apps whose presets
support candidates.
- Display name is localized through providerForm.presets.tuzi:
"兔子API" in zh / zh-TW, "Tu-zi" in en / ja. Search aliases add
兔子 / 兔子api / tuzi.
- 128x128 raster icon from the API site's logo.png, plus metadata.
- Claude preset rows in the selector test go from 72 to 73.