Checked against omp 18.12.6 with real DeepSeek and PackyCode requests:
- Usage queries sent an omp apiKey verbatim. omp reads it as an
environment variable name first (process environment, then .env in the
agent dir, the config root and home) and falls back to the literal, with
no $NAME syntax, so "apiKey: DEEPSEEK_API_KEY" reached the balance API as
"Bearer DEEPSEEK_API_KEY" and got 401. Resolve it the same way; names
match exactly, and "!command" values are still never run.
- With only a legacy models.json present, adding a provider created
models.yml, which stops omp from migrating models.json and drops the
user's providers. Refuse instead and ask the user to run omp once.
- omp disables every custom provider when one models.yml node fails its
schema. Reject the enum values it rejects before writing: compat
thinkingFormat, maxTokensField and the other enumerated fields
(including whenThinking), reasoningEffortMap levels, auth, authHeader,
and discovery type, injectV1 and timeoutMs.
- The form kept discovery.injectV1 when switching away from
openai-models-list, which omp allows only there. Drop it on switch.
Anthropic-protocol presets still omit auth on purpose: omp then shapes
requests like Claude Code, which relays such as PackyCode require (they
answer 403 to auth: apiKey).
Pi lets a models.json provider's apiKey be "$NAME" / "${NAME}" (read from
the environment, with "$$" and "$!" as literal "$" and "!") or "!command".
CC Switch's own usage queries (Coding Plan, balance, usage-script
{{apiKey}}) read the Pi apiKey verbatim through resolve_usage_credentials,
so the provider received "Bearer $COMMAND_API_KEY" and answered 401.
Resolve the value with Pi's template rules before sending it. A referenced
variable that is unset or empty leaves the value unresolved, as in Pi, and
"!command" values are still never run by CC Switch; both now give an empty
key instead of sending the reference as the credential. The resolved key is
only used for the outgoing request and is never written back.
Refs #8822
The shared reqwest client has no default User-Agent (it also serves the
proxy forwarding path, where request headers must stay untouched), so
skill install and update downloaded GitHub repository archives without
one. GitHub throttles UA-less archive requests more aggressively; behind
a shared proxy exit the reporter got persistent HTTP 429 on update while
the same URL returned 200 once a User-Agent was present.
Set `CC-Switch/<version>` as the User-Agent on the archive download
request in `download_and_extract`, which covers both install and update,
matching the format already used by the deeplink icon fetcher.
Refs #8813
DeepSeek's /user/balance returns `is_available`, which only says whether
the balance is enough for API calls. It turns false as soon as the
balance reaches zero or goes negative, while the key itself still works.
The parser mapped it straight to `is_valid`, so the provider card and the
tray showed "plan expired" and dropped the balance entirely.
Always report the balance as valid and leave invalid keys to the
existing 401/403 auth-error path. A zero or negative balance now goes
through the normal balance line, which shows the amount in red on the
card and amber in the tray, the same as OpenRouter and Novita.
Refs #8812
Kimi Code writes every subagent to its own agents/agent-N/wire.jsonl,
but the usage importer only scanned agents/main/wire.jsonl and only read
assistant agent.message.appended records. Subagent replies never become
such records, so all subagent token usage was missing from the stats.
Scan every agents/<agent>/wire.jsonl. The main wire is imported exactly
as before. For a subagent wire, import each step.end that carries usage
(one per LLM call, matching usage.record 1:1 on real Kimi Code 2.3.1
data), keyed by its messageId in the dedup ledger, and resolve the
model and timing through a RequestIndex scoped to that agent instead of
discarding every request whose agentId is not "main". The parent's
subagent.completed summary is left alone so nothing is counted twice.
Refs #8792
Claude Desktop was grouped by whether it was connected to CC Switch, so
an installed but unconnected Desktop showed up under "Not installed".
`get_claude_desktop_status` now reports `installed`. Desktop has no
command to probe, so it is inferred: connected, or Desktop's own data
directory (`Claude`, created on its first launch; `Claude-3p` is not
counted because CC Switch writes it too) exists, or on macOS a
Claude.app bundle is in /Applications or ~/Applications. The apps page
groups the Desktop row by it; the row still says whether it is connected.
Installed apps on the apps page can be reordered by dragging the handle
that appears at the start of a row on hover, the same way provider cards
are sorted. Only the installed section is sortable: where a missing app
sits does not matter, and while the first probe has not finished it is
not yet known which section a row belongs to.
There is a single order for all apps, saved as `appOrder` in the
device-level settings.json next to `visibleApps` (not cloud-synced). The
sidebar and the app opened by default follow it. Each section shows the
order filtered by install state, so an app keeps its relative position
when it moves between sections. Apps missing from a saved order, such as
ones added in a later version, are inserted after their default
predecessor; unknown ids are ignored.
The dragged row is kept inside its section's list box, which clips
overflow for the rounded corners, so it cannot disappear mid-drag.
The Simplified and Traditional Chinese copy for a failed quota query,
"额度没查到" / "額度沒查到", read as a casual remark rather than a status.
Use "额度查询失败" / "額度查詢失敗" on the provider card, in the auth
center's account quota column (including its defaultValue fallback) and
in the tray, where the two variants with a reason now read
"额度查询失败:登录已过期" and "额度查询失败:令牌待刷新".
English, Japanese and Turkish are unchanged. Tests and comments that
quoted the old wording are updated.
A balance at or below zero used to be replaced with "Balance used up" on
the provider card and in the tray. That hid how far an account was in
debt (DeepSeek reports -24.96 the same way as 0.00), and the red
sentence read like the red "quota unavailable" line, so a spent balance
and a failed query were hard to tell apart.
Always write "Balance <amount> <unit>"; at or below zero the whole line
turns red on the card and amber in the tray, as before. A negative zero
or a tiny negative amount that rounds to zero is written as 0.00 rather
than -0.00. The balanceUsedUp copy and the tray's balance_used_up text
are removed. Currency units stay as they are.
Refs #8812
Antigravity responses without a model field or a Model Selection event (subagents, ACP sessions) were imported as unknown. Recover the model from the native conversations/<id>.db: prefer the tiered model ID (field 21, matching Model Selection and pricing) and fall back to the base model name (field 19). The database is opened read-only, never waits on a writer, and keeps already recovered names when it cannot be read.
Co-authored-by: Jason <farion1231@gmail.com>
Some managed ChatGPT workspaces disable device-code authentication, so those accounts could not be added to CC Switch. Add browser login (authorization code + PKCE with a loopback callback on port 1455) to the Auth Center and the Codex provider form, including reauthentication; device-code login stays available in a method selector.
Fixes#8284
Official Kimi accounts added in the Authorization Center were written
straight into config.toml with the model list discovered at sign-in.
Kimi Code only refreshes its own managed:kimi-code node, so separate
accounts kept that snapshot forever, and their cards could neither be
edited nor removed.
Official cards can now be edited. The editor hides the connection and
fetches the account's own catalog through a new kimi_oauth_models
command (token refreshed under Kimi's lock, aliases keyed by the
credential slot), so picked models carry the official context window,
capabilities and efforts. apply_provider pins an official node's
connection to the signed-in one, and ProviderService::update rejects
edits that would turn another card into an official account.
Official cards can also be removed from config.toml like other Kimi
providers: the card and credential stay, services pointing at a
separate account move to Kimi's own login, and "Add" restores it.
Deleting one still requires removing it first or using the
Authorization Center.
Add Kimi K3.1 Pro (released 2026-10-23) to the built-in pricing table
at the official list price: 2/10 per MTok, cache read 0.20, 5-minute
cache write 2.00. HighSpeed doubles input, output and cache write but
keeps cache read at 0.20, unlike K2.7 Code HighSpeed where every column
is doubled.
Also seed the bare k3.1-pro / k3.1-pro-highspeed ids used by the Kimi
Code plan, mirroring the existing k3 row; the pricing prefix fallback
only matches longer ids, so they would otherwise bill at 0.
omp also ships as a prebuilt binary (official installer, Homebrew), and
its npm package only runs under Bun. Install now runs the official
installer, which uses Bun when present and the verified binary otherwise.
Updates go through `omp update`, which routes every install source
itself: Homebrew keeps `brew upgrade`, Bun keeps `bun add -g` as the
fallback, Nix is left to the user, and npm is never offered. The
installer's binary is now recognized for uninstall.
Oh My Pi reads its user-level instructions from AGENTS.md in the
profile-scoped agent directory, so prompts now project there like Kimi
Code's. Drop the prompt-sync skip and the stale deep-link notes that
assumed there was no prompt panel.
Oh My Pi keeps Pi's session tree format, so the Pi reader now takes a
small dialect (provider id, root resolution, resume command, the leading
title slot and the combined provider/model selector). The omp provider
keeps only its root resolution, resume command and artifact cleanup on
delete, and listing no longer renders every transcript to find a title.
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.
Squashed from #6747 (head 50c3cec1d) onto current main: providers in
models.yml, MCP in mcp.json, Skills, sessions, app visibility and
directory override for Oh My Pi. Conflicts with the Kimi Code refresh
refactor in App.tsx and the locale files were resolved by keeping both
sides and routing Oh My Pi through refreshAdditiveLiveState.
Co-authored-by: allenxu09 <s23372.xu@stu.scie.com.cn>
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
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.
Qianwen / Bailian Token Plan Anthropic endpoints (e.g.
https://token-plan.cn-beijing.maas.aliyuncs.com/apps/anthropic) return 404 for
/v1/models and /models; the model list is only served under the
OpenAI-compatible path {root}/compatible-mode/v1/models. Append it as the last
candidate when the base URL ends with /apps/anthropic.
Co-authored-by: Claude Code <noreply@anthropic.com>
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 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.
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.
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
* feat(kimi): manage multiple official accounts in auth center
Isolate account credentials, providers and model aliases; preserve native logins and protect official OAuth nodes. Fix session image access, drop unpublished usage repair paths and the kimi-cli alias, and synchronize all four READMEs.
* fix(kimi): address account compatibility review feedback
Restore the native default credential slot for the first account, stop rewriting tool services on account switches, allow deleting profiles removed from native config, and fix official-account test fixtures and documentation.
* fix(kimi): pin OAuth endpoints to the login region
Use fixed official hosts for mainland and global sign-ins, keep existing account regions and credential slots during reauthentication, and preserve sanitized transport error causes. Add regional routing and network-error regression coverage.
Add Kimi Code as the 12th managed app (additive mode): native multi-provider config in ~/.kimi-code/config.toml (honors KIMI_CODE_HOME), presets derived from the Pi catalog, MCP (stdio/HTTP/SSE), Skills with native-directory protection, global prompts, session browsing/resume, and session-log usage import. Local routing and aggregation do not apply; official OAuth stays managed by Kimi Code. Schema v21 -> v22 adds enabled_kimi columns.
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
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.
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.