Replace the passthrough/splice YAML layer with a single omp_config.rs:
#6747's verified agent-dir resolution (profiles, PI_CONFIG_DIR, XDG data
dir) plus #8308's block-level editor that rewrites only the one provider
entry, refuses anchors/tags/flow style instead of re-emitting them, and
rolls the file back byte for byte when the database commit fails.
- Validate only the node being written; model-level `api` also accepts
omp's runner APIs.
- Drop ProviderMeta.ompPassthroughYaml, validation.rs and omp_state; the
current-state command lives in commands/omp.rs and reuses PiCurrentState.
- Usage scripts go through the Pi command with an `app` argument.
- MCP reuses Pi's transport helpers; mcp.json writes are revision checked.
- Skills follow Pi: no DB column, state derived from <agent>/skills, and
uninstall keeps an unverifiable same-name directory in either app.
- Move enabled_omp to a new v22 -> v23 migration (v22 shipped in 4.0.19)
and add it to mcp_servers only; revert the .gitignore additions.
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.
When a relay's model is not recognized by address, the metadata lookup
falls back to the vendor's entry: first the entry that same-name entries
point at via canonical_model_id, and only if none resolves, an entry
listed under a provider that some canonical id points at.
That last step only checked whether the provider counts as a vendor, not
whether the entry claims to be its own. models.dev has no
deepseek/deepseek-v4.1-flash-1017, so the ~40 entries pointing there all
miss, and thinkingmachines (counted as a vendor for its own models) won
with its LoRA fine-tuning entry DeepSeek-V4.1-Flash-1017:peft:262144,
whose canonical id points back at deepseek. It is text-only with a 262144
window, so picking the model filled every app with text-only input and a
quarter of the real window; Codex additionally turned image input off.
Skip entries whose canonical id names another vendor in that fallback.
Against the current models.dev data, 27 ids stop borrowing from hosts of
other vendors' models (thinkingmachines, NVIDIA, vivgrid, alibaba) and
leave the fields empty instead, and laguna-xs-2.1 now resolves to
poolside's own entry rather than being ambiguous. Skipping ':' variants
instead was rejected: it fixes this id but newly resolves nine
thinkingmachines entries carrying 65K training windows (gpt-oss-120b and
others).
Refs #8765, #8682
The request log has more columns than the other usage tables, so at the
default window size the last column (first token) was cut off and the
table had to scroll sideways. Its columns are already sized to their
content, so the model column had no slack to give back.
Narrow the gaps for this table only: 16px between columns (was 24px) and
32px between column groups (was 40px), keeping the 16px outer edges. The
table shrinks by 72px with the same rows; model names are not truncated.
The provider, model and pricing tables keep their spacing.
getPresetLookbackStart subtracted (N-1) * 24h from the current instant
and then rounded to local midnight. Across a DST offset change that
subtraction can land on the adjacent date, so the 7d/14d/30d presets
included one extra local day after spring-forward and dropped one after
fall-back (e.g. America/New_York, Europe/Berlin). The shifted range fed
the usage summary, model/provider stats and request-log queries.
Build the start date from today's local year/month/day minus (N-1) days
instead, which yields the first instant of that calendar day, including
zones where midnight itself is skipped by DST. The 1d rolling window,
today, all-time and custom ranges are unchanged.
Refs #8719
Refreshing the Antigravity official quota on Windows popped up a black
console window. Outside macOS the quota falls back to spawning
`agy --print /usage`, and the model list in the provider form spawns
`agy models`. The release build is a GUI-subsystem app, so a console
child spawned without CREATE_NO_WINDOW gets its own console window.
Both spawn sites now go through agy_config::background_command, which
sets CREATE_NO_WINDOW on Windows like the other background tool probes.
Behaviour on macOS and Linux is unchanged.
Refs #8700
While provider link A was still importing (awaiting the backend import or
the follow-up cache refresh), a second link B could arrive, replace the
displayed request and be acknowledged in the backend. When A's import
completed, handleImport unconditionally called setIsOpen(false), closing
B's confirmation even though B was never imported and could no longer be
fetched again.
Track the request currently shown in a ref (set together with setRequest
in showPending) and close the dialog after an import only if it still
shows the request that import started with. Add a regression test that
holds A's import, delivers B, then checks B stays open and still imports
and closes on its own.
Refs #8696
Claude Code routed to an OpenAI Responses upstream showed "Unsupported
content type: redacted_thinking" before tool calls in the VS Code
extension. Reasoning items that only carry encrypted_content (summary is
empty) were converted into redacted_thinking blocks, which the extension
cannot render.
Emit them as an empty thinking block whose signature holds the reasoning
envelope instead, the same shape Anthropic returns for omitted thinking.
Replay already decodes thinking.signature and redacted_thinking.data the
same way, so replayed upstream requests are byte-identical and histories
written by older versions keep working.
Also recover summary text that never arrived as deltas: when
output_item.done carries a summary that strictly extends what was
streamed (e.g. a later summary part had no deltas), emit only the
missing tail before the signature. Snapshots that do not extend the
streamed text are ignored, so nothing is re-emitted.
The upstream request is unchanged; reasoning.summary is not injected.
Refs #8691
On Windows, pointing the Hermes config directory at a WSL path
(\\wsl$\<distro>\... or \\wsl.localhost\<distro>\...) only redirected
config, sessions and the version probe. When the Web UI was offline, the
"launch dashboard" action still opened a Windows terminal running
`hermes dashboard` natively, so it started the Windows hermes (or failed
with command not found) instead of the instance inside the distribution.
Build the dashboard command line from the existing per-tool WSL distro
detection: for a WSL override run it as
`wsl.exe -d <distro> -- sh -c "\"${SHELL:-sh}\" -lic 'hermes dashboard'"`
so the user's login shell PATH (e.g. ~/.local/bin) applies. Only one
shell flag is used, since a Ctrl+C exit is non-zero and a fallback retry
would relaunch the dashboard. The Web UI probe on 127.0.0.1 is unchanged;
WSL2 forwards localhost to the distribution. Other platforms keep the
plain `hermes dashboard`.
Refs #8689
Official Antigravity cards on macOS often showed only the Claude and GPT
quota. Google's retrieveUserQuotaSummary decides what to return from the
User-Agent: "cc-switch/<version> (antigravity)" passes the license check
but gets only the Claude and GPT group, and the Gemini group is returned
only to "antigravity/cli/<version>" from 1.2.0 on. agy's own /usage sends
that, which is why the fallback path always showed all four tiers.
The User-Agent is now "cc-switch/<version> (antigravity/cli/<agy
version>)". The version comes from the local "agy --version", probed once
per process, and falls back to 1.4.6 when agy cannot be found or read.
List V-API's mirror lines (api.vveai.com, run.v3.cm, api1.v3.cm,
api2.v3.cm, us.gpt.ge) as endpoint candidates after the main
api.gpt.ge in the Claude Code, Claude Desktop, Gemini CLI, Codex and
Grok Build presets, with /v1 on the OpenAI-format apps.
Move FluxA TokenPlan up to Shengsuanyun's former slot after V-API and
Shengsuanyun down to FluxA's former slot after SubRouter, in the four
README sponsor tables and the seven preset files that carry both.
Add V-API (gpt.ge) as a full sponsor placed after BuyToken: README rows
and banner in all four languages, the partner promotion copy, an inline
tile icon, and presets for Claude Code, Claude Desktop, Codex, Gemini
CLI, Grok Build, OpenCode, OpenClaw, Hermes and Pi. The gateway prices
and gates models per token group, so each preset notes which group it
needs. It has no undated claude-haiku-4-5, so the Desktop Haiku tier is
mapped to claude-haiku-5-5.
Add BuyToken as a full sponsor placed after DIT.ai: README rows and
banner in all four languages, the partner promotion copy, an inline tile
icon, and presets for Claude Code, Claude Desktop, Codex, Grok Build,
OpenCode, OpenClaw, Hermes and Pi. Keys are bound to a model group, so
each preset notes which group's key it needs. The gateway only accepts
the dated claude-haiku-4-5-20251001 ID, so the Haiku tier is mapped to
it. There is no Gemini preset because /v1beta is not served.
The provider key, the model ID and display name column headings, and
the context window and max output labels now use REQUIRED_LABEL like
the rest of the provider forms, instead of a hand-written "*" span.
The asterisk is drawn by CSS, so it no longer becomes part of the
label text that screen readers and tests read.
The provider key names the provider node in models.json, so it is now
shown for presets too (prefilled, editable) instead of only for custom
and edited providers, matching Kimi Code. A key that is already used
by another Pi card is flagged under the field and blocks saving, so
adding the same preset twice no longer fails only at the backend.
Models are now one row each (expand arrow, model ID, display name,
delete) with capabilities and the context window in the expanded
details, instead of one large card per model. Fields follow the same
order as Pi and OpenCode (API format, API key, base URL), the API
format menu shows readable protocol names, request headers are shown
inline, and required fields use the shared asterisk style.
The new display name column writes the model's display_name, which
Kimi Code shows in /model; clearing it removes the field. The model
selection hint moves into a "?" beside the models heading, and the
Kimi-only labels that duplicated OpenCode's are removed from all
locales.
Small buttons shrink slightly while pressed. On the full-width,
left-aligned "Advanced options" toggle the shrink is centered, so the
label visibly shifts. Keep these toggles at full size in the Claude,
Codex and Kimi Code forms.
Custom headers are rarely needed for Kimi Code providers, so keep them
under an "Advanced options" section like the Claude and Codex forms.
It opens by itself when headers are already set, from an existing card
or a preset that carries them.
"Always thinking" and "Load tools on demand" don't say what they do.
Add a "?" help tip next to each in all four locales: the first is for
models whose thinking can't be turned off, the second only works with
Kimi's own models and needs KIMI_CODE_EXPERIMENTAL_TOOL_SELECT. Move
"Always thinking" next to the last one so the common three come first.
models.dev moved OpenCode Zen and Go to /inference/... endpoints, while
presets and existing providers still use /zen/... (both work and bill
the same). The endpoint lookup no longer found them, so models only
listed there, such as deepseek-v4.1-flash, filled nothing. Map the old
normalized addresses to the new ones; this applies to every app's fill.
The Kimi Code form only let users type model IDs and context windows by
hand, unlike the other apps' forms. Add the same "Fetch models" picker
and per-row dropdown: picking models adds them with tool use enabled,
then fills the context window, thinking and image input from the
matching preset or models.dev entry (the endpoint decides which). A
window the user typed is never overwritten; rows still on the
placeholder window take the looked-up value. Anthropic and Gemini
providers fetch with their own auth header.
The version column already says the tool is not installed, so the extra
"not installed or not executable" caption repeated it in English. A plain
not-found probe now reports no error; WSL failures with real diagnostics
(invalid shell, exec failed) still surface.
Kimi Code saves every /model pick as default_model, so the default shown
in CC Switch can go stale while the page stays open. The backend rereads
config.toml and refuses to remove the provider Kimi Code is using, but
the page kept showing the old default and an untranslated English reason.
Reread the additive app's live state after a failed removal as well as a
successful one (now including the OpenCode, OpenClaw and Hermes default
model queries), reread Kimi providers after failed edits and deletions,
and explain the two Kimi "model in use" refusals in all four locales.
Point --chart-usage back at --chart-1 and restore the heatmap's darkest
light-mode step (#0e7490), so the usage charts look as they did before
#8711. The separate token stays, so a later color change does not touch
the quota bars that also use --chart-1.
When the user sat on page 2+ and the total dropped (the "today" window
rolled over, Codex usage was rebuilt), the page index stayed out of
range. With a single page the pagination bar is hidden, leaving an
empty table with no way back. Pull the page back to the last valid one
once real (non-placeholder) data arrives.
Each FullScreenPanel adds its own keydown listener on window, and
stopPropagation does not stop other listeners on the same target, so
stacked panels (pricing -> pricing editor, edit provider -> endpoint
speed test) all closed on a single Escape. Track open panels in a
module-level stack and only let the top one react.
The default provider in OpenCode, OpenClaw, Hermes and Kimi used a gray
card and dot. Use the same blue as an in-use switch-mode card instead,
and drop the now-unused neutral card tone.
Kimi is additive, but both card groups only offered "Switch", which
wrote the provider into config.toml and changed default_model at once,
so the Added / Available split did nothing.
- Available cards get "Add": a new add_provider_to_live_config command
writes the provider and its models into config.toml without changing
default_model, so they show up in Kimi's /model list. A node already in
config.toml belongs to Kimi and is never overwritten from the archive.
- Added cards get a "Remove" button; "Set as default" (select the
provider's model) moves into the more menu, as for OpenCode/OpenClaw.
- The provider owning default_model is marked with the gray "Default"
status like OpenCode, and cannot be removed.
Kimi Code shows a provider's key as its name in the model picker, so
separate accounts appeared as "kimi-code-ccs-<hash>". New separate
accounts are now keyed "Kimi 订阅 <nickname>" ("Kimi Subscription" when
CC Switch is not in Chinese), matching Kimi's own label for its default
login and limited to the characters Kimi accepts, numbered on clashes.
The credential slot and model aliases keep the stable
"kimi-code-ccs-<hash>" name, so default_model stays an ASCII key and
ownership checks are unchanged. A returning account is also recognised
by its credential slot, not only by the local identity cache. Card names
fall back to the readable key when no nickname is cached.
Kimi's account list drew its own layout (inline description, a permanent
region picker, highlighted current row, extra badges). It now uses the
same ManagedAccountsGroup as Copilot, ChatGPT, xAI and Antigravity:
help behind "?", region chosen when adding an account, "In use by Kimi"
badge, nickname and avatar, shared pending/error blocks, and a
"needs re-login" state for signed-out accounts.
The shared group gains optional per-row remove label and disabled
reason, an optional "remove all" action, and a looser device-code type.
Account action failures now toast instead of reusing the login error
block. Unused Kimi strings are removed from all locales.
The icon-only star on added OpenCode/OpenClaw cards was hard to read and
had no selected state. "Set as default" now sits at the top of the card's
more menu, like the aggregation page; providers with several models open
a submenu to pick the default model. The card keeps only the remove
button, and the primary button no longer needs its own dropdown.
Follow-up to #8726, which lets the Authorization Center hold several Kimi
official subscriptions, each with its own provider and credential slot.
Identity: after sign-in (and for logins found on disk) read /me to show
the account's nickname on its row and card. Signing in to an account that
is already listed writes back to its slot instead of adding a duplicate,
and the slot is chosen when the login completes, so a native Kimi login
made meanwhile is never overwritten. New separate slots are named from a
hash of region and user id, stable across devices. Opening the Kimi page
reads identities too, so card names carry the nickname without a visit to
the Authorization Center first.
Remove instead of sign out: a separate account is deleted together with
its provider node, models, credential and card (refused while in use);
web search/fetch pointing at it moves to Kimi's own default login. Kimi's
own login is only hidden from the list - CC Switch never logs Kimi out -
and stays hidden across Kimi's token rotations until the user signs in to
it again here. An empty list caused by that says so instead of "signed
out".
Quota: each account row and official card shows its quota. Tokens close
to expiry are refreshed under the same proper-lockfile-compatible lock
Kimi uses (mkdir <slot>.lock, 5 s stale takeover), with Kimi's revoked
token recovery. Windows, where Kimi takes no lock, only reads.
Also: re-login keeps custom fields such as custom_headers on the node;
deleting a card whose node was removed elsewhere deletes the leftover
CC Switch-owned credential; the card's switch button works for official
accounts after checking they are signed in on this machine; account rows
use the same compact layout as the other Authorization Center groups.
The parent_rollout_* tests asserted on the total size of the process-wide
CODEX_REPLAY_CACHES.parent_timelines map. #[serial_test::serial] only
excludes other serial tests, while non-serial tests such as
test_parse_codex_file_recognizes_branch_thread_paginated_continuation
reach load_parent_timeline (via resolve_continuation_baseline) and insert
their own entries concurrently. Under multi-threaded `cargo test` that
occasionally turned an expected count of 1 into 2; nextest's
process-per-test model hid it in CI.
Count only entries under the test's temp dir instead. Production code is
unchanged.
Picking a model from the fetched list fills its empty catalog fields from
known metadata (same-endpoint preset, models.dev). For a model the lookup
reported as text-only, the Codex fill wrote inputModalities ["text"].
For Codex an absent inputModalities is not "unknown = text": the backend
checks the exact text-only registry and otherwise fails open to
["text", "image"]. Writing ["text"] therefore switched image input off,
against the fill rule that modalities are only ever turned toward
"supported", and the Codex form has no control to switch it back, so
Codex kept refusing image attachments for that model.
Only fill inputModalities when the metadata includes image; otherwise
leave it absent and let the registry and fail-open default decide.
Refs #8682
try_get_version still ran `$SHELL -lic "<tool> --version"` via a bare
output(), so when CC Switch was launched from a terminal on Linux/WSL the
interactive shell kept the controlling tty, got stopped by SIGTTIN, and
opening Settings -> About froze the whole app.
Spawn it like resolve_path_default / login_shell_path already do: stdin
null, isolate_child_process_group (setsid) and the 10s probe deadline.
-lic is kept so PATH entries from .bashrc are still picked up; a timeout
falls back to the existing directory scan.
Fixes#3241
In the merged (stack) catalog every third-party row shared one comp_hash,
"cc-switch", so Codex never compacted when switching between them (#8485).
Third-party GPT rows mirror the official entry and carry
`tool_mode: "code_mode_only"`, where shell calls are recorded as the
custom tool `exec`; DeepSeek, Kimi and other rows are direct and only have
`exec_command`. After switching from a GPT row to a direct row without
compaction, the direct model copied the `exec` calls from history and
every turn failed with `unsupported call: exec`.
Group rows by tool protocol instead: a row with a tool_mode gets
"cc-switch-<tool_mode>", direct rows keep "cc-switch" so existing
sessions see no change. Switching within a group still skips compaction;
switching across groups compacts with the previous model first, which
folds the other protocol's calls into the summary. tool_mode and
supports_search_tool keep the template values, so third-party GPT rows
keep code mode. The cost is that a cross-group switch away from a
provider that is no longer usable fails to compact, as before #8485;
switching within the group or starting a new session avoids it.
Fixes#8676
macOS GUI apps started from Finder or a login item inherit launchd's
soft RLIMIT_NOFILE of 256. The local router holds a client socket and an
upstream connection per in-flight request on top of the log, database
and config files, so a burst of parallel requests exhausted the
descriptors: accept() failed with EMFILE, upstream connects failed,
reading live-state.json failed and SQLite could not open the database.
Right after the logger is initialized, lift the soft limit to the hard
limit capped at 10240 (macOS OPEN_MAX; setrlimit rejects larger values
there). The hard limit is never touched and a limit that is already high
enough is left alone; failures are only logged.
Refs #8668
When CC Switch is launched from Finder, its PATH is launchd's narrow
/usr/bin:/bin:/usr/sbin:/sbin. run_detected_tool_command_with_timeout
located the tool through a login shell but then spawned it with only the
tool's own directory prepended to that narrow PATH. An npm-installed CLI
is a `#!/usr/bin/env node` script, so when node lives in a different
directory (codex in /opt/homebrew/bin, node in /usr/local/bin) the
"Restart Codex daemon" action failed with `env: node: No such file or
directory`. The same helper runs `opencode models`.
Merge the login-shell PATH in front of the inherited PATH, as tool
install/update already does. login_shell_path now honours the caller's
deadline and spawns like resolve_path_default (stdin null, own session);
when it fails or times out the previous PATH is used unchanged.
Refs #8653
Opening a terminal on Windows ran `cmd /C start ...` through
Command::output(). The console that `start` opens inherits the wrapper's
stdout/stderr pipe, so the pipe only reached EOF once the user closed
that window, and the command (and the open-terminal dialog) kept
spinning until then.
Point stdio at NUL and wait only for the wrapper's exit status. Launch
failures still surface through the exit code. Same approach as the
community PR #6716 by @umutcankurt, applied to the current code path.
Refs #6712
Add hao.ai as a full sponsor placed above Fenno: README rows and banner
in all four languages, the partner promotion copy, an inline tile icon,
and presets for Claude Code, Claude Desktop, Codex, Grok Build, OpenCode,
OpenClaw, Hermes and Pi. hao.ai serves Anthropic Messages under
/anthropic, OpenAI-compatible APIs under /v1 and xAI under /x-ai/v1, and
its catalog IDs carry a provider prefix. There is no Gemini preset since
the catalog has no Gemini models.
DIT.ai (dit.ai), an AI token exchange routing each request across
competing providers, joins as a sponsor, placed directly below GrooRoute
Pro in the sponsor order (OpenModel in the Gemini file, which has no
GrooRoute preset).
- One key on api.dit.ai serves Chat Completions, Responses, Anthropic
Messages and native Gemini (protocol docs at dit.ai/docs, catalog at
dit.ai/models, checked 2026-10-21).
- The gateway has no Haiku 5.5 and only the dated
claude-haiku-4-5-20251001, so Claude Code overrides the Haiku tier with
it and Claude Desktop maps Sonnet 5.5 / Opus 5.5 / that Haiku.
- OpenCode, OpenClaw, Hermes and Pi use Anthropic Messages with Claude
Opus 5.5 and Sonnet 5.5; Codex uses Responses with gpt-5.6-sol, Grok
Build grok-4.7, Gemini CLI gemini-3.8-flash.
- Four-locale README sponsor rows with a banner, four-locale promo copy
with the "ccswitch" coupon, and an inline icon from the official logo.
- Claude preset rows in the selector test go from 105 to 106.
ZetaAPI is no longer a sponsor; its presets go too.
- Drop the four-locale README sponsor rows and the banner.
- Remove the ZetaAPI presets from Claude Code, Claude Desktop, Codex,
Grok Build, OpenCode, OpenClaw, Hermes and Pi.
- Remove partnerPromotion.zetaapi from every locale, Turkish included.
- Keep the zetaapi icon: existing providers saved from the preset still
reference that icon key.
EveryAPI moves from the basic tier (in-app presets and website only) to
a full sponsor, placed directly below OpenModel in the sponsor order.
- Four-locale README sponsor rows with the new copy and a 1200px banner.
- The EveryAPI preset blocks in the seven preset files move below
OpenModel so file order matches the README tables; the presets
themselves are unchanged.
- The in-app promo copy adds "GPT from 15% of the official price" and
folds the first top-up bonus and the "ccswitch" code into one perk,
since they are the same offer.
The frontend map of session placeholder names missed
"Antigravity (Session)", so the provider column showed the raw English
name and truncated it. Add the entry and a test that every placeholder
name in usage_stats.rs is translated.
No other app shows a pricing note above the usage tables, although
Codex, Gemini and the rest also estimate session-log amounts from API
prices. Remove the note, its locale key and the test that pinned it.
The card keeps one line per group (Gemini, Claude/GPT) to fit, so the
5-hour and weekly windows could not both be seen anywhere on it. On
Antigravity cards the quota column is now one dropdown, like the resets
dropdown on Codex official cards, listing every window with its own
reset time. Refreshing moves to the ↻ button that appears on hover.
Other cards keep click-to-refresh.
An official Antigravity card bound to an Auth Center account showed the
quota of whatever account agy was signed in with, which is a different
account until the card's terminal is opened. It now shows the bound
account's quota, shared with that account's row in the Auth Center, and
like Codex account cards it shows even when the card is not current.
Cards that follow agy's sign-in, and platforms that cannot switch
accounts, keep the signed-in quota.
Official Antigravity cards asked agy's own /usage command for quota, which
starts agy and takes seven to nine seconds. On macOS the card now takes the
account agy is signed in with from the keychain, adds it to the Auth Center
if it is new, and asks Google directly with CC Switch's copy of the token,
the same way the Auth Center does (about one second).
agy's /usage is still used when the keychain holds no recognizable login,
when the user removed that account from CC Switch, and on platforms that
cannot switch accounts.
Every Antigravity row in the Auth Center now has a quota column, the
same one Copilot, ChatGPT and xAI rows use: Gemini and Claude/GPT, each
with a 5-hour and a weekly window. It is queried once when the page
opens and on the refresh button, through agy_account_quota, so accounts
agy is not signed in with show quota too. Accounts that need to sign in
again get no quota column.
Accounts that Google does not allow to use Antigravity, such as those
restricted by age, get the same 403 "You do not have a valid license,
contact your administrator" from the quota endpoint. That reads like a
CC Switch or licensing problem. On a 403 the quota query now asks
loadCodeAssist once, and when the account only has ineligible tiers it
shows Google's own reason instead.
The Auth Center can now show quota for every Antigravity account, not
only the one agy is signed in with. The new agy_account_quota command
calls Google's retrieveUserQuotaSummary with the account's own token, so
no switch is needed. The response carries the same buckets as agy's
/usage, and both now go through one tier builder.
The endpoint only answers clients that identify as Antigravity, so the
User-Agent is "cc-switch/<version> (antigravity)": it names CC Switch
and adds the word the server checks for. It only reads quota; it never
calls a model, warms up accounts or switches on low quota.
Tokens that expire within two minutes are refreshed first. The refreshed
token is saved only to CC Switch's copy, never to the keychain: agy keeps
refreshing its own login, and Google does not rotate the refresh token,
so both access tokens stay valid. For the account agy is using, the
keychain login is absorbed first so its newer token is used. A rejected
token triggers one forced refresh; invalid_grant marks the account as
needing sign-in again.
Third-party upstreams such as OpenCode Go, OpenRouter (deepseek/
namespace) and the Hermes presets call DeepSeek V4.1 Flash
deepseek-v4.1-flash. Pricing had no row for that id: none of the lookup
fallbacks turn it into deepseek-flash, and the prefix fallback only
matches longer ids. These requests showed "Unpriced" and were counted
at $0.
Add a deepseek-v4.1-flash row at the same official peak price as
deepseek-flash (0.3 / 1.2 / 0.006 per million). Default prices are added
again on every start, so existing installs get the row on next launch.
Requests already logged are not repriced.
An official Antigravity card can now pick one of the Auth Center accounts
(macOS only). With an account picked, Open Terminal first switches agy's
global sign-in to it; when it is already signed in, nothing is written.
Without one, the card keeps following whatever agy is signed in with.
The binding is stored as authBinding with authProvider "agy_oauth". The
account id is the Google subject, so a card synced to another computer
works once the same account is added there; if it is missing, opening the
terminal stops with a message instead of launching under another account.
Platforms that cannot switch ignore the binding and keep it on the card.
The Open Terminal dialog says when opening will switch agy's sign-in, and
warns when agy is already running. The Auth Center counts bound cards as
users of an account, so the removal dialog lists them.
On macOS the Auth Center now lists Antigravity accounts next to Copilot,
ChatGPT and xAI. The account agy is signed in with is tagged "In use by
agy", and "Switch agy to this account" in the row menu replaces agy's
sign-in. After switching, CC Switch checks whether agy is running and
warns that open sessions keep the old account and may write it back.
Adding an account opens Google's sign-in page in the browser. The shared
login hook treats a response without a user code as this browser flow: it
leaves the clipboard alone, polls the local result every 1.5 seconds and
cancels the backend flow so the callback port is released. The pending
block shows a "reopen sign-in page" button instead of a device code.
Removing an Antigravity account says that agy stays signed in.
The backend now records the sign-in time in seconds, which is what the
Auth Center formats.
agy keeps a single global login in the system keychain (service "gemini",
account "antigravity"). CC Switch now keeps its own copy of each account
and switches agy by writing the chosen one back into that entry.
- Sign in through the browser with agy's own OAuth client (PKCE, loopback
callback on 127.0.0.1), requesting the same scopes agy does, so agy can
keep refreshing the token on its own.
- Read and write the keychain through /usr/bin/security, the program the
entry already trusts: the value goes over `security -i` stdin, `-U`
updates in place and keeps the entry's access control, and the write is
read back and checked.
- Before overwriting, take back the current login: refresh tokens agy
renewed go to their account, an unknown account is added, and an
unreadable value is stashed to agy-login-stash.json.
- Removing an account only drops CC Switch's copy; agy is never signed out.
- Exposed as the "agy_oauth" provider of the generic auth_* commands, where
the default account is the one agy is signed in with. macOS only for now.
The usage dashboard listed every known app as a filter chip regardless of
the visibility switches on the Apps page. Filter the chips the same way the
session manager does: Claude Code stays while Claude Desktop is shown, and
the currently selected app is kept even if it has been hidden.
Since route and aggregation mode keep the ChatGPT login (#8431), the
route table writes requires_openai_auth = true whenever a login is on
disk, so making a third-party card the aggregation default no longer
keeps Codex from locking the send button to Luna Reserve once the
official 5-hour quota runs out.
The guide and the 4.6 manual pages now describe the method that still
works: switch to Direct, keep "Keep official login for direct switches"
off, switch to a third-party card and fully restart Codex; switch back
to Aggregation once the quota resets. The old method is marked as no
longer working.
Refs #7962
The long paragraph above the Antigravity form explained a case that should
not exist: a non-official card without a key is just the official login
card again. Drop the paragraph and warn on save when a custom card has no
API key, as Gemini CLI already does. A key without an endpoint stays valid
and uses Google's default API.
The endpoint and model rules move into "?" tips next to their fields. The
card's terminal button now reads "Open Terminal with This Provider"; the
shorter "Open Terminal" label stays for the Claude Code card menu.
CC Switch can only reach the agy it starts itself: a third-party key
travels in that process's environment, so "enabling" a key card never
changed what a plain `agy` used. Antigravity cards now only launch.
- Every card's primary action is Open Terminal; there is no "in use"
state, the official quota shows on official cards at all times, and
any card can be deleted.
- Official cards start `agy --model=<display name>`, which applies to
that session only. Key cards keep their private --gemini_dir.
- The real ~/.gemini/antigravity-cli/settings.json is never written any
more: switching is refused, adding the first card, full syncs and
deep links with enabled=true leave it alone. The only remaining write
removes the modelProvider left behind by the #8268 preview.
- The terminal dialog and form hints no longer talk about switching.
agy writes a zero-byte ~/.gemini/config/mcp_config.json the first time it
starts. Parsing it as JSON failed, so enabling any MCP server for
Antigravity showed "Invalid Antigravity MCP JSON" and importing from it
failed too. Treat a blank file as having no servers.
- Use the Antigravity logo for the app instead of reusing Gemini's.
- List Antigravity CLI on the Apps page: install with the official script,
update with `agy update` (also for the Homebrew cask, which is
auto_updates), read the latest version from the release manifest, and
uninstall official-script and cask installs.
- Find install.ps1's %LOCALAPPDATA%\agy\bin on Windows.
- agy stores every prompt wrapped in <USER_REQUEST>, followed by context it
adds itself (<ADDITIONAL_METADATA>, <USER_SETTINGS_CHANGE>). Session titles
and user messages now show only the text inside <USER_REQUEST>; an empty
request keeps the original content.
- The official-quota hint said CC Switch reads the CLI's OAuth credentials.
For agy it runs agy's own /usage command instead, so the usage dialog now
says that (and that a check takes about 10 seconds).
- Ask agy's own /usage command (agy -p /usage --output-format json) instead of
reading credentials; it makes no model call and opens no conversation.
- Keep the Gemini group and the Claude/GPT group, each with its 5-hour and
weekly window, as four tiers; unknown buckets are skipped.
- On the card each group shows only its window with the least left, since
four tiers do not fit; the expanded view lists all four.
- A missing agy is treated as no login (nothing shown); a timeout propagates
so the last good value is kept; other failures do not echo agy's output.
- Store the agy prompt in ~/.gemini/config/AGENTS.md, which agy reads as a
standalone customization root and every private key profile links to.
- List sessions recorded under each key profile and resume them with the
matching --gemini_dir and the card's key in a launched terminal.
- Fetch the model list for API-key cards from agy's own offline catalog in a
throwaway profile, so the form offers display names agy actually accepts.
- Record agy session usage under priced model IDs (display name kept as the
request model) and estimate cost from Gemini API prices.
- Treat an imported settings file with a model as the official login card.
Ported from #8608:
- Model discovery works before agy has written a settings.json; the
official login lives outside that file.
- Configurations imported from live are marked official: the key is never
on disk, so an import is always agy's own login.
- Antigravity provider cards show the configured model.
- Session usage rows take their model from the `Model Selection` step agy
injects at the start of a conversation, since responses carry no model.
Not ported: running `agy models` while importing on startup or when the form
opens. It takes several seconds on a signed-in machine and may prompt for
sign-in on one that never logged in; the Fetch Models button stays explicit.
Co-authored-by: ajia1206 <491387123@qq.com>
agy only reads GEMINI_API_KEY and GOOGLE_GEMINI_BASE_URL from the process
environment, and modelProvider is global. The previous approach wrote the key
to a file, installed a wrapper script and edited .zshenv/.zprofile/.zshrc so
every agy picked it up.
Now the native settings.json always stays in login mode: login cards set the
model (display name) and drop modelProvider; key cards only drop
modelProvider. A key card applies to agy started from the card's "Open
Terminal": `agy --gemini_dir=<private dir>` under ~/.cc-switch/agy/<id>, whose
settings carry modelProvider and the model and whose config links back to
~/.gemini/config so MCP, Skills and prompts stay shared. The key and endpoint
reach that one process through the environment only.
The terminal launcher now takes a per-app launch description (command, env,
temp files) so Claude Code and agy share the terminal layer. On macOS/Linux
env values are read from a 0600 file that is deleted right away and passed
via `env`, so values exported by the user's rc files cannot override them;
on Windows they live in a setlocal block. Claude's temporary settings file is
now written 0600 as well, and temp files are removed before handing over to
the interactive shell (exec skipped the EXIT trap).
On startup, wrappers, env files and PATH blocks left by earlier test builds
are removed (only CC Switch-marked content) and the current provider is
reapplied so modelProvider returns to login mode.
Hermes routes model.provider to its built-in provider whenever the name is a
canonical built-in id (runtime_provider_custom._shadowed_by_builtin), so a card
keyed "deepseek" or "openrouter" is ignored: Hermes asks for the built-in's own
API key env var and never uses the card's endpoint or key. #8538 only fixed the
two MiniMax presets by prefilling cc-switch-minimax.
The provider key field now shows a warning for any of the 52 canonical ids
(generated from hermes-agent rc.3-v0.22.0) and suggests cc-switch-<key>. A
locked key gets a variant that says to re-add the provider under another key.
Aliases such as kimi still reach the custom provider and are not flagged.
Saving is not blocked.
Lightweight mode frees memory by destroying the main window's WebView.
Since the tray panel became the default tray UI, the panel is a second
WebView window that is created on the first tray click and afterwards
only hidden, so a WebView stays alive and on Windows the WebView2
processes never exit after entering lightweight mode.
In lightweight mode the panel is now destroyed whenever it is hidden
(and right away when entering lightweight mode while it is already
hidden); the next tray click creates it again. The destroy is deferred
to a later main-thread turn and re-checks that the panel is still hidden
and not about to be revealed, so it never tears the WebView down inside
its own window-event callback or IPC command. toggle() decides "close"
before creating the window so a click that only closes the panel does
not recreate it, and a recreated window starts with a fresh theme state
and, on macOS, fresh glass views.
Refs #8612
toggle_mcp_app and set_mcp_enabled called McpService::toggle_app without
the per-app switch lock. Each single-server write reads the app's live
config, edits it and writes it back, so two toggles in flight at once (or
a toggle racing a provider switch, a proxy enter or an MCP resync, which
all hold that lock) could overwrite each other and silently drop servers
from ~/.claude.json, config.toml and friends while the database still
showed them enabled.
Add McpService::toggle_app_locked, which takes the same switch lock
resync_mcp_to_apps already uses and then runs toggle_app, and route both
commands through it. toggle_app itself stays lock-free because the tokio
lock is not reentrant and internal callers may already hold it; MiniMax
Code and Pi keep their own file lock inside sync_and_commit, which never
takes the switch lock. A test fires 16 concurrent toggles at Claude and
checks every server lands in the live file (it fails without the lock).
Refs #8580
A unified MCP spec can lack a `type` field when it was pasted in JSON
mode or created via a deeplink, e.g. `{"url": "https://..."}`. Hermes'
and OpenCode's projections defaulted a missing type to stdio, whose
branch only copies command/args/env. Hermes therefore got an entry with
nothing but `enabled: true`, and OpenCode got `type: local` with an
empty command, while the UI reported a successful sync.
Codex's projection already handled this in `spec_transport_type`
(missing or stdio type, no command, non-empty url => http). Move that
helper unchanged into `mcp/validation.rs` and use it from the Codex,
Hermes and OpenCode projections, so all three infer the transport the
same way. Add regression tests for url-only and command-only specs.
Refs #8579
Since #8431, entering or attaching the proxy for a third-party Codex
route gives back the most recently stashed ChatGPT login when auth.json
has none. The stash can hold a login set aside months ago by a direct
switch with "keep official login" off. Restoring it made the route
table require OpenAI auth, Codex desktop tried to refresh the dead
token and got stuck on the sign-in page, and every exit/enter put it
back again.
The restore now skips a login whose access_token JWT has already
expired. Codex refreshes the token every 8 days and it lives about 10,
so an expired one means the login has not been used for a long time.
Tokens that cannot be decoded are still restored, and the skipped login
stays in the stash so switching back to an official card returns it.
Refs #8573
normalizeTomlText replaced every curly/fullwidth quote in the text before
TOML parsing, including quotes inside already-valid strings. MCP import and
validation (tomlUtils, mcpDraft) and the Codex config editor therefore
silently rewrote values such as args = ["don’t"] to "don't", and rejected
valid configs like args = ["say“hello”"] or args = ['don’t'] because the
inner quotes became delimiters.
Scan the text instead: smart quotes outside strings are delimiters and
become ASCII; strings opened by an ASCII quote keep smart quotes as
content; strings opened by a smart quote may close with either form;
multi-line strings and comments are handled. To keep the Chinese IME case
working (the editor normalizes each keystroke, so the left quote is already
ASCII when the right smart quote is typed), an ASCII-opened string left
unterminated at end of line is re-scanned with smart closers, and if the
strict result still fails to parse, a lenient scan is used only when it
parses. Escaped smart double quotes in basic strings become \". Text
without smart quotes is returned unchanged.
Refs #8559
CSTCloud Data Capsule's WebDAV server answers PUT manifest.json with
500 "Uploaded object size does not match the upload session" whenever
the body is multi-line JSON sent as application/json. The same pretty
JSON is stored byte-for-byte when sent as application/octet-stream, and
db.sql / skills.zip already upload fine with their own types.
Send the manifest with application/octet-stream. The manifest bytes and
hash stay the same and readers never look at Content-Type, so old and
new versions keep reading each other's manifests. S3 sync is unchanged.
Refs #8554
On Windows the proxy port could stay LISTENING after CC Switch exited,
owned by the PID of the dead process, so the next start failed to bind
with os error 10048 and fell back to direct mode until a reboot or a
port change.
The listener comes from tokio -> mio, and the locked mio 1.1.1 created
Windows sockets with plain socket(), which yields an inheritable handle.
CC Switch spawns long-lived children (terminals via `cmd /c start`, CLI
tools), and std's Command passes inheritable handles to them, so those
children kept the listening socket open after CC Switch was gone.
Bump mio to 1.2.1, which creates sockets with WSA_FLAG_NO_HANDLE_INHERIT
(tokio-rs/mio#1946), and add a Windows-only test asserting the listener
handle is not inheritable so the lockfile cannot silently regress.
Refs #8552
Carry an `emphasis` field on tray quota lines and the panel payload,
set to the amount only when a pay-as-you-go balance is left (matching
the card's `balanceLine`), so the panel can color just the number.
Lay out each quota block as one grid (label | bar | reset) so bars start
and end at the same x across tiers, and let the quota row extend under
the chevron / "In use" column so bars are no longer squeezed. Replace the
absolute reset date with a compact time-until-reset (at most two units,
e.g. "1d 23h"), with unit strings added to all panel text branches, and
keep quota labels from wrapping.
The API key field is a plain input, so it missed the autocorrect,
auto-capitalization and spellcheck opt-outs the shared Input component
already sets. With the key shown in plain text, a typed "sk-..." key
could be saved as "Sk-..." and fail authentication. Pasted keys were
not affected.
Clicking the tray icon on macOS and Windows opens the panel again by
default. A settings.json without nativeTrayMenu now reads as the panel;
an explicitly saved true keeps the native menu, which stays available
under Settings > General > Tray style.
Add AllTokens SI (alltokens.si), the enterprise new-api deployment, as
a basic-tier sponsor separate from the alltokens.cc entry, with its own
icon and partner promotion copy in all four locales.
Models were checked against the public catalog (/api/pricing):
- Claude IDs use dots (claude-sonnet-5.5) and there is no Haiku, so the
Claude Code preset spells out all four tiers with Haiku on Sonnet,
and Claude Desktop maps its routes instead of passing them through.
- The GPT group only declares Chat Completions, so OpenCode, OpenClaw,
Hermes and Pi use Chat; Codex keeps Responses, which new-api's OpenAI
channels forward.
- Gemini and Grok are outside the default auto groups and need a key
from their own group; Gemini uses gemini-3.8-flash-high and Grok
Build grok-4.7, as the group has no grok-4.5.
OpenRouter, Vercel AI Gateway, Cloudflare AI Gateway and DigitalOcean
bill GPT through the OpenAI API, so the 272K subscription cap from
v4.0.13 understated their windows. Restore the values those presets had
before the cap: Codex DigitalOcean, Hermes OpenRouter / Vercel /
Cloudflare and OpenClaw Vercel / Cloudflare. Pi presets read windows from
the shared model catalog, which stays capped, so the Pi Vercel preset
overrides GPT-6 Astra explicitly.
The preset guard now allows GPT windows above 272K only for these four
hosts, and checks that each of them still uses the exception. Catalogs
without an address (Pi catalog, OpenCode variants) get no exception.
The 272K GPT cap (v4.0.13) applied to every window the metadata lookup
returned, so platforms that really serve the 1.05M OpenAI API window
(OpenRouter and other providers models.dev recognizes by address) were
capped too. The risk the cap guards against is narrower: a relay that
models.dev does not know falls back to the vendor's own entry, and its
1.05M lands on a model that is often served from a ChatGPT subscription
with a 272K window.
- Same-endpoint presets and the models.dev provider matched by address
keep their own windows; only the window borrowed from the vendor entry
(including when the matched provider lists none) is capped at 272K.
- A window the user types is kept as is. When a GPT model's window is
above 272K, the Codex, Hermes, OpenClaw, OpenCode and Pi forms show a
note under the field: relay GPT usually has the subscription window, and
an oversized one delays compaction until the upstream rejects the turn.
Text added in zh / en / ja / zh-TW.
Existing rows are not migrated. Codex GPT rows saved before v4.0.13 with
an auto-filled 1.05M now take effect since #8385; the note shows when the
form is opened.
StorageLocationGuard writes settings.json both when it is set and when it
is dropped. Eight skill tests created it before TestHomeGuard, so the setup
write and the drop-time restore (locals drop in reverse order) hit the
runner's real ~/.cc-switch/settings.json, which intermittently failed on
Windows with os error 32 / 1176 when another process held the file.
Make the guard borrow an active TestHomeGuard so it can only be created
after HOME isolation and is always dropped before HOME is restored, and
assert that the settings directory lives under the test home.
OpenCode picks its startup model from the top-level `model`
("<provider>/<model>") in opencode.json, but CC Switch had no way to set
it, so users had to edit the file by hand after adding providers.
Reuse the OpenClaw "Set as default" card action for OpenCode:
- Backend: `opencode_config::{get,set}_default_model` read/write only the
top-level `model` through the existing format-preserving edit path
(comments kept, `small_model` untouched), rejecting values that are not
"<provider>/<model>". New commands `get_opencode_default_model` and
`set_opencode_default_model`.
- Frontend: the additive section builder's `openclawDefault/openclawModels`
become generic `defaultModel/defaultModelOptions`; the default card is
marked and other added cards get "Set as default" with a model menu.
Model options come from the same fragment the backend writes (a stored
full config is unwrapped) and skip natively disabled models. Built-in
providers that declare no models get no button. Unlike OpenClaw, the
default provider can still be removed, matching the old behavior.
Existing generic notification texts are reused, so no new strings.
Refs #8509
Deleting a switch-mode provider was refused if the device-local direct
pointer, the DB is_current flag or the proxy route pointed at it. The
UI only disables delete for the effective direct pointer and the route,
so a card that only DB is_current still pointed at looked deletable but
failed with "无法删除当前正在使用的供应商".
This happens after upgrading with an old Claude Desktop mapping card:
the startup migration moves the local Desktop pointer to the official
card and routes to the mapping card, but leaves is_current on it. Once
the user routes to another provider, that card is idle yet undeletable.
is_referenced now checks the effective direct pointer (provider_for
Direct: the local pointer when valid, else DB is_current) plus the
route, the same set the UI disables. The fallback still protects
is_current when there is no valid local pointer.
Refs #8506
On Windows fnm only exposes Node through `fnm env`, which prepends a
per-shell %LOCALAPPDATA%\fnm_multishells\<id> link to that shell's PATH.
The registry PATH never contains it, so the effective PATH CC Switch builds
for CLI detection and lifecycle commands had no node, npm or npm global
shims: fnm-installed Codex/Claude/OpenCode showed as not installed, and the
in-app install failed with "'npm' is not recognized". The only fnm search
path we had is the Unix multishell layout.
Append fnm's stable <fnm dir>\aliases\default (FNM_DIR, legacy ~\.fnm, then
%APPDATA%\fnm; only if it exists) as the last segment of the effective
PATH. On Windows that alias points at the default Node installation, which
holds node.exe, npm.cmd and the global CLI shims directly, so detection,
where.exe resolution and install/update commands all see it, while every
explicit PATH entry keeps precedence. Multishell links are not scanned
since they grow without bound (#8313).
Refs #8484
The About card showed src/assets/icons/app-icon.png, a 32x32 bitmap,
in a 40px box, so it was upscaled and looked blurry, especially on
Retina displays (80 device pixels).
Use the existing logo.svg (already used by the sidebar header) instead.
The PNG carried about 4px of transparent padding while the SVG is
tight, so keep the 40px box and add p-1 to keep the mark at its
previous visual size. The PNG had no other references and is removed.
Refs #8470
The white rounded-square tile baked into icon.icns in v4.0.15 reads too
bright in the Dock, especially in dark mode. Recolor the tile to #e8e8e8
while keeping the tile geometry and the artwork pixels unchanged: the
artwork coverage of each pixel is solved from the white-tile icon itself,
and only the exposed tile area is moved from white to grey, so edges keep
their original anti-aliasing at every size in the iconset.
Windows (.ico) and Linux (PNG) icons stay transparent.
Refs #8495
Volcengine's plan quota is queried through its OpenAPI with an
account-level AccessKey ID + Secret, not the inference API key. Adding a
Volcengine Agent/Coding Plan provider (or importing one via deep link)
auto-enabled the token_plan query anyway, so every new card showed a red
"quota not found" right away and users read it as the provider not
working.
Preselect Volcengine in the usage settings but leave the query off until
the user fills in the AccessKey. Deep links that set usageEnabled are
still honored; the import dialog badge and the backend agree.
Refs #8494