fix: decompress body before forward and support zstd#3817
Conversation
|
@codex review |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 599fea9a4b
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
|
希望能加急合并一下这个 PR,这是会影响体验的 Bug。 |
|
Codex Desktop sends zstd-compressed request bodies when authenticated against the Codex backend, which broke local proxy routing because the handlers parsed the raw bytes with serde_json directly. Reworked on top of current main so it preserves the response_processor behavior that landed after this PR was first opened: - Extract content-encoding helpers into a shared proxy::content_encoding module. decompress_body keeps returning Option<Vec<u8>> so unknown encodings stay pass-through with their content-encoding header intact, and keeps the deflate zlib-then-raw fallback (RFC 9110). - Add zstd/zst support (zstd 0.13) and disable reqwest's auto zstd decompression via .no_zstd() for parity with gzip/br/deflate. - Decompress the request body before JSON parsing in the three Codex handlers (chat_completions / responses / responses_compact) and strip the stale content-encoding / content-length / transfer-encoding headers so the forwarder regenerates them. - Support stacked codings (e.g. "gzip, zstd") by decoding in reverse order and merge repeated Content-Encoding headers via get_all. Fixes farion1231#3764 Fixes farion1231#3696 Co-authored-by: chenx-dust <[email protected]>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 7ae4ce38ad
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
The forwarder error branch consumes non-2xx responses via String::from_utf8 directly, bypassing read_decoded_body. reqwest has no auto-decompression feature enabled, so a compressed error body (gzip/br/deflate/zstd) arrives as raw bytes, fails from_utf8, and gets dropped, hiding upstream rate-limit and auth details from the client. Decode the error body with the shared proxy::content_encoding helper, mirroring the success path. Falls back to the raw bytes when the encoding is unsupported or decoding fails. Co-authored-by: chenx-dust <[email protected]>
|
我说呢,公司自己部署的不好使了今天,我还寻思啥情况呢 |
Sync upstream v3.16.4: - proxy/forwarder.rs (+275): local proxy request overrides (farion1231#4589) with protected-header whitelist + stream protection + Copilot skip - proxy/content_encoding.rs (new): unified gzip/deflate/zstd decompression, shared by forwarder + response_processor (farion1231#3817) - provider.rs: LocalProxyRequestOverrides type - 118 unchanged files synced + database/mod.rs etc. Fix critical rustls CryptoProvider panic in cc-switch-server: - hyper_client.rs: ClientConfig::builder() (panics without process default) → builder_with_provider(ring) (self-contained, no external install needed) - main.rs: install_default() at startup (mirrors src-tauri/src/lib.rs), covering all TLS paths (reqwest, hyper-rustls) - Without this, the first HTTPS upstream request panicked → 502 fetch failed Release cc-switch-client 0.1.4: - Bump main + 5 platform packages to 0.1.4 - Add 0.1.4 CHANGELOG entry Verified: cc-switch-core 1457 unit + 5 integration tests pass; cc-switch-server 13 tests pass (0 failures).
proxy 链路(3.16.4): - farion1231#3817 转发前解压请求体(zstd):新增 content_encoding 模块,Codex Desktop 登录态压缩请求体不再破坏代理 JSON 解析,压缩的错误响应体也一并解压 - farion1231#4583 Copilot/Codex OAuth 走全局代理:不再写死 Client::new() 绕过代理 - farion1231#4159 Chat 工具调用缺函数名则跳过(流式/非流式/legacy 三路,去掉 unknown_tool 回退) 凭据安全(3.16.4/3.16.5): - 通用配置片段剥离全部凭据:extract_claude_common_config 改用 is_sensitive_config_key 模式匹配,剥掉任意 *_API_KEY/*_AUTH_TOKEN/*secret*/*token* - farion1231#4654 用量脚本凭据仅作显式覆盖:ProviderService add/update 归一化 + deeplink 导入 + 前端缓存失效 - codex_history_migration 探测 CODEX_SQLITE_HOME 环境变量 Claude MCP(3.16.4): - farion1231#3431 自定义配置目录时 MCP 文件存到目录内(dir/.claude.json), 默认 ~/.claude 仍用 ~/.claude.json;移除旧的"访问即拷贝"迁移 farion1231#4160/farion1231#4239/farion1231#4316 经核对 fork 3.16.3 基线已含,跳过。 farion1231#4654 provider/mod.rs 测试与 farion1231#3431 mcp_commands 集成测试因 fork UsageScript/McpApps 字段差异未移植,功能逻辑完整,待后续补测试。 Co-Authored-By: Claude Opus 4.8 <[email protected]>
* docs(readme): add Kimi sponsor call-to-action link Append a clickable CTA hook to the end of the Kimi K2.6 slogan in all four README locales (en/zh/ja/de), pointing to the existing aff=cc-switch Moonshot console link. * fix: align Claude MCP path for custom config dirs (#3431) * fix claude mcp path for custom config dir * fix claude mcp custom profile isolation * fix claude mcp override path edges * fix ci test isolation * fix(ui): 修复 Skills 管理与模型配置交互展示问题 (#4323) * fix(ui): improve skills and provider interactions * fix(skills): keep repo manager available on skills.sh * test(skills): cover repo switch after refresh * fix(skills): keep refresh available for empty repo results --------- Co-authored-by: thisTom <[email protected]> * feat(provider): add CLAUDE_CODE_AUTO_COMPACT_WINDOW for Kimi For Coding preset (#4401) Set the default Claude Code auto-compact window to 262144 for the Kimi For Coding provider preset, matching the official Kimi docs: https://www.kimi.com/code/docs/en/third-party-tools/other-coding-agents.html Use templateValues so users can customize the value (e.g. for future models or performance tuning) while keeping 262144 as the default. * ci: add Windows ARM64 release support (#3950) * ci: add Windows ARM64 release artifacts * ci: keep release matrix jobs independent * ci: fix pnpm cache path on Windows runners * ci: setup pnpm with corepack on Windows ARM64 * ci: fix Windows ARM64 release build * ci: retry transient release bundler downloads * ci: remove non-minimal release workflow changes * ci: keep release matrix jobs independent macOS signing fails in forks without Apple secrets and, with default matrix fail-fast, cancels the sibling jobs (including Windows ARM64) before they finish. Disable fail-fast so each platform runs to completion. --------- Co-authored-by: MoonDreamStars <[email protected]> * feat(usage): add live end time option for custom date range (#4438) * add glm-5.2 pricing * feat(usage): add live end time option for custom date range Add a "End time follows current time" checkbox in the custom date range picker. When enabled, the end time becomes read-only and automatically tracks the current moment, so usage data always reflects up-to-the-second consumption from the chosen start time. This is especially useful under the Coding Plan 5-hour quota window — users can set the start time to when their 5h window began and keep the end time live to monitor real-time token consumption. * style(usage): fix prettier formatting for UsageDashboard * fix(usage): include liveEndTime in React Query cache keys Without liveEndTime in the query key, a live custom range and a fixed custom range with the same stored endpoints share the same cache entry. After the live range refreshes (endDate = now), switching to the fixed range shows stale data fetched through "now" instead of the original end time. * feat(ui): 为多处 JsonEditor 组件接入暗色模式 (#4556) 在为 UsageScriptModal、ProviderForm、UniversalProviderFormModal 中 的 JsonEditor CodeMirror 编辑器添加 darkMode 支持,使用 useDarkMode() hook 监听 App 主题,自动切换 oneDark 编辑器主题。 * feat(db): in-app recovery screen with upgrade button when DB version is too new (#4575) * feat(db): in-app recovery screen with upgrade button when DB version is too new When the SQLite user_version is newer than the app supports (SCHEMA_VERSION), Database::init() fails and previously dead-ended in a native Retry/Exit dialog (Retry just fails again). The app now boots a dedicated recovery screen instead. - Detect the recoverable "version too new" case and surface it via init_status (kind="db_version_too_new" + db_version/supported_version); force-show the main window and skip normal AppState boot (recovery commands need only AppHandle). - The recovery screen first checks for an available update: - update available -> "Upgrade app" runs the updater (download + install + restart) with a download progress bar. - no update (already latest) -> warns that the DB is too new even for the latest build (likely a third-party client), so upgrading cannot help. - install_update_and_restart now emits `update-download-progress` events; new `check_app_update_available` command. - i18n: en / ja / zh / zh-TW. Verified: pnpm typecheck + format:check + test:unit (351) green; cross-model review of Rust correctness and recovery-mode safety (no AppState panic). * fix(db): pre-check too-new DB before schema writes; exit on close in recovery mode Addresses review feedback on the too-new-DB recovery flow. - P1: stored_user_version_exceeds_supported() is now checked BEFORE Database::init(), so create_tables()'s DDL (incl. the unconditional DROP INDEX/DROP TABLE IF EXISTS failover_queue) never runs against a database whose user_version we cannot understand. The earlier post-init-failure recovery branch is removed; the pre-check is the single authoritative guard, so "don't write to a DB we can't read" now holds from the very first DB access. - P2: the window CloseRequested handler now detects recovery mode (init_error kind = db_version_too_new) and exits the app instead of honoring minimize_to_tray_on_close. Recovery mode returns before the tray is created, so hiding the window would leave the app running with no tray to bring it back; native-close now quits cleanly. Verified: cargo fmt --check clean; cross-model Rust review of both fixes (compile-correctness + no normal-startup/close regression). * feat(proxy): 添加本地代理请求覆盖功能,支持自定义请求头和请求体 (#4589) * feat(proxy): 添加本地代理请求覆盖功能,支持自定义请求头和请求体 * fix(proxy): harden local request overrides validation * feat(proxy): 添加受保护的本地代理请求头名称验证功能 * fix(i18n): 更新本地代理请求覆盖的错误提示信息格式 --------- Co-authored-by: jason.mei <[email protected]> * fix(proxy): 修复 Copilot/Codex OAuth 模块绕过全局代理导致 Claude 模型 400 错误 (#4583) * fix(copilot): 修复 GitHub Copilot 出站请求不走全局代理导致 model not supported * fix(proxy): 修复全局代理客户端配置未被应用到 Copilot/Codex OAuth 模块 两个关键问题: 1. CopilotAuthManager 在构造时写死 Client::new(),使得拉取 /models 列表、 换取 token 等认证流程无视全局代理配置(global_proxy_url),直连目标服务。 结果:直连时 /models 返回 0 个 Claude 模型 → live resolution 失效 → 模型 ID 无法正确归一化/匹配 → Copilot 上游返回 400 model_not_supported。 2. CodexOAuthManager 也有完全相同的问题,导致 Codex OAuth 认证请求绕过代理。 改动:删除两个模块的自持 http_client 字段,改为每次请求时从全局客户端现取 (crate::proxy::http_client::get())。这样: - 遵循全局代理 URL 配置 - 支持运行时热更新代理设置 - 符合代码库设计意图(http_client.rs 注释明确说"所有 HTTP 请求应使用此模块") 修复覆盖范围: - copilot_auth.rs: 7 处调用(token 获取、/models 拉取、model vendor 判断) - codex_oauth_auth.rs: 4 处调用(device code、OAuth token 刷新) Fixes #2016 #2931 * fix: decompress body before forward and support zstd (#3817) * fix(proxy): decompress Codex request body before forward, support zstd Codex Desktop sends zstd-compressed request bodies when authenticated against the Codex backend, which broke local proxy routing because the handlers parsed the raw bytes with serde_json directly. Reworked on top of current main so it preserves the response_processor behavior that landed after this PR was first opened: - Extract content-encoding helpers into a shared proxy::content_encoding module. decompress_body keeps returning Option<Vec<u8>> so unknown encodings stay pass-through with their content-encoding header intact, and keeps the deflate zlib-then-raw fallback (RFC 9110). - Add zstd/zst support (zstd 0.13) and disable reqwest's auto zstd decompression via .no_zstd() for parity with gzip/br/deflate. - Decompress the request body before JSON parsing in the three Codex handlers (chat_completions / responses / responses_compact) and strip the stale content-encoding / content-length / transfer-encoding headers so the forwarder regenerates them. - Support stacked codings (e.g. "gzip, zstd") by decoding in reverse order and merge repeated Content-Encoding headers via get_all. Fixes #3764 Fixes #3696 Co-authored-by: chenx-dust <[email protected]> * fix(proxy): decompress upstream error bodies before reading them The forwarder error branch consumes non-2xx responses via String::from_utf8 directly, bypassing read_decoded_body. reqwest has no auto-decompression feature enabled, so a compressed error body (gzip/br/deflate/zstd) arrives as raw bytes, fails from_utf8, and gets dropped, hiding upstream rate-limit and auth details from the client. Decode the error body with the shared proxy::content_encoding helper, mirroring the success path. Falls back to the raw bytes when the encoding is unsupported or decoding fails. Co-authored-by: chenx-dust <[email protected]> --------- Co-authored-by: Jason <[email protected]> Co-authored-by: chenx-dust <[email protected]> * feat(pricing): add Doubao Seed 2.1 Pro/Turbo model pricing Volcengine official list price (CNY converted at ~7.14, USD/1M): pro input 0.84 / output 4.2 / cache-hit 0.17; turbo input 0.42 / output 2.1 / cache-hit 0.08. cache_creation kept 0 (Doubao bills cache storage by time, not per-token writes). Appended via INSERT OR IGNORE; 2.0 rows retained for historical usage accounting. * feat(providers): upgrade DouBaoSeed preset to Doubao Seed 2.1 Pro Point DouBaoSeed at doubao-seed-2-1-pro across all six clients (claude, claude-desktop, codex, opencode, openclaw, hermes), replacing doubao-seed-2-0-code-preview-latest. Sync display names to "Doubao Seed 2.1 Pro" and correct the openclaw cost field (0.002/0.006 -> 0.84/4.2 USD/1M) to match the new model. * feat(usage): show direct Volcengine key console link in usage panel The account-level AccessKey is buried in the Volcengine console menus, so surface a clickable, visible link to the IAM key management page (https://console.volcengine.com/iam/keymanage) right under the AK/SK hint. Opens via settingsApi.openExternal; adds the volcengineKeyConsoleLink label across all four locales. * feat(codex): decouple upstream format selector from model-mapping toggle The Codex provider form tied Chat-format conversion and route takeover (model mapping) to one toggle, so a provider serving a native Responses API could not use model mapping without forcing Chat Completions conversion. - Promote the upstream format (Chat Completions / Responses) to an independent, always-visible selector that triggers no sub-menus on its own. - The local-routing toggle now solely gates the advanced sub-sections: model mapping catalog, plus reasoning capability when the format is Chat. - Persist modelCatalog / codexChatReasoning based on the takeover toggle and derive its initial state from saved catalog presence (no new persisted field). - Refresh codexConfig i18n (zh/en/ja/zh-TW): add upstreamFormat* keys and reword the routing/advanced hints to reflect the decoupling. - Fix codexChatProviderPresets test expectation for the Doubao Seed 2.1 Pro rename. * feat(skills): flag unmanaged skills with a green dot on the Import button - Add an optional `enabled` flag to useScanUnmanagedSkills; the Skills panel scans once on mount (enabled:true), with 30s staleTime + keepPreviousData to dedupe disk IO across navigations. - App subscribes to the shared query (enabled:false) and renders a green dot + tooltip on the top-bar Import button when unmanaged skills are available. * feat(codex): use native Responses API for CN providers that support it Several Chinese providers now expose a native OpenAI Responses API endpoint, so Codex can reach them directly without the Responses->Chat route-takeover conversion. Switch these presets to apiFormat "openai_responses" and drop the now-unused codexChatReasoning and modelCatalog (removing modelCatalog also keeps the "local route mapping" toggle unchecked by default): - Qwen / DashScope Bailian (/compatible-mode/v1/responses) - Xiaomi MiMo + Token Plan (api.xiaomimimo.com/v1) - Volcengine Doubao (/api/v3/responses) - Meituan LongCat (/openai/v1) - MiniMax CN + intl (/v1/responses, confirmed in the official API reference) SiliconFlow-hosted MiniMax stays openai_chat (third-party endpoint, not MiniMax's own base_url). Also refresh stale model ids on the remaining chat-only providers: GLM 5.1->5.2, StepFun 3.5-flash-2603->3.7-flash, Ling 2.5-1T->2.6-1T. * docs(readme): replace Kimi sponsor banner with local images Switch the Kimi K2.6 sponsor banner from the Moonshot CDN URL to in-repo assets. Chinese README uses kimi-banner-zh.png; English, Japanese and German use kimi-banner-en.png. * test(codex): sync chat preset snapshot after native Responses migration Commit 273cc48c migrated 7 CN providers from openai_chat to native openai_responses and refreshed model ids on the remaining chat-only providers, but the codexChatProviderPresets snapshot was not updated, breaking the unit tests on main. Drop the migrated providers (DouBaoSeed, Bailian, Longcat, MiniMax, MiniMax en, Xiaomi MiMo x2) from the chat list, sync GLM/StepFun/Ling model ids, and add a test locking the migrated presets to openai_responses with no modelCatalog (so the local route-mapping toggle stays unchecked). * docs(readme): add star-history global rank badge beside Trendshift * docs(readme): update Kimi sponsor copy to K2.7 Code Refresh the Kimi sponsor blurb to the K2.7 Code release (coding-focused agentic model, ~30% lower thinking-token usage vs K2.6) and bump the banner alt text from K2.6 to K2.7 Code. EN/ZH copy provided by the vendor; JA/DE translated to match. * Add SubRouter provider presets (#4522) Co-authored-by: abingyyds <[email protected]> * feat(presets): add OpenCode Go subscription preset for Codex and OpenCode Add OpenCode Go (opencode.ai/zen/go) provider presets across clients: - Codex: openai_chat conversion with model catalog (GLM/Kimi/DeepSeek/MiMo), no static codexChatReasoning so per-model capability is inferred - OpenCode: @ai-sdk/openai-compatible against /zen/go/v1 - Claude: add apiKeyUrl (opencode.ai/auth) and align websiteUrl to /go Auth is a plain pasteable API key (no OAuth). Go splits wire format by model: OpenAI /chat/completions for GLM/Kimi/DeepSeek/MiMo, Anthropic /messages for MiniMax/Qwen; these presets target the chat-format models. * feat(presets): add OpenCode Go referral link and promo copy without partner badge Decouple the in-app promotion banner from isPartner so a preset can show a promo phrase plus referral link without earning the paid-partner star. - ApiKeySection: gate the banner on partnerPromotionKey alone; the star is still driven solely by isPartner. This also un-suppresses the existing MiniMax cn/en promos, which already carried copy without isPartner. - OpenCode Go (claude/codex/opencode): point apiKeyUrl at the referral link and add partnerPromotionKey "opencode_go" (no isPartner set). - i18n: add the opencode_go promo string across zh/en/ja/zh-TW. * feat(presets): point SubRouter apiKeyUrl to aff referral link Swap the SubRouter apiKeyUrl across all 7 client presets from the neutral console/token page to the affiliate registration link (?aff=l3ri), matching the project's referral-link convention. Sync the preset test constant accordingly. * fix(presets): sync OpenCode Go referral link and promo to Claude Desktop The Claude Desktop OpenCode Go preset was missed by the referral/promo update that landed for Claude Code / Codex / OpenCode, so its websiteUrl pointed at the bare opencode.ai domain with no apiKeyUrl or partnerPromotionKey. Align it with the other three: websiteUrl -> /go, apiKeyUrl -> the ref=2YTRG2NGTX referral link, and partnerPromotionKey "opencode_go" so the in-app promo banner shows. All four OpenCode Go presets now carry the referral link and promo copy. * docs(changelog): add 3.16.4 release notes Summarize the commits since v3.16.3 across Added/Changed/Fixed/Docs: the SubRouter and OpenCode Go partner presets, native Codex Responses migration for CN providers, proxy hardening (zstd decompression, OAuth-over-proxy, DeepSeek effort strip), and usage/pricing tooling (Volcengine Ark AK/SK queries, models.dev import, live date ranges, GLM-5.2/Doubao pricing). * chore(about): remove Fable 5 verified commemorative banner * docs(changelog): record Fable 5 banner removal, refresh 3.16.4 stats Add a Changed entry for the removed Fable 5 Verified commemorative banner (16922917) and refresh the stats line to the content-scoped count (53 commits | 126 files | +8,149 / -1,016), excluding the changelog meta commits. * chore(release): bump version to 3.16.4 * docs: add v3.16.4 trilingual release notes and Codex custom-model FAQ - Add v3.16.4 release notes in zh/en/ja. - Add a trilingual FAQ explaining why custom models don't show in the Codex desktop model picker (upstream Codex Desktop login-identity gating, not a cc-switch bug) and the keep-official-login mitigation; cross-linked both ways with the official-auth preservation guide. - Align the CHANGELOG 3.16.4 release date to 2026-06-27. * docs(release-notes): add GitHub global top-100 milestone banner to v3.16.4 notes * fix(usage): treat usage_script credentials as explicit overrides (#4654) * fix(usage): treat usage_script credentials as explicit overrides * fix: treat usage script credentials as explicit overrides * fix(hermes): Windows 上 Hermes 供应商配置不生效 (#4680) * fix(hermes): resolve config dir via HERMES_HOME and platform defaults On Windows, CC Switch hardcoded `~/.hermes` as the Hermes config directory, but Hermes resolves it through `get_hermes_home()`: `HERMES_HOME` env var, then a platform default (Windows `%LOCALAPPDATA%\hermes`, mac/Linux `~/.hermes`). So CC Switch wrote configs where Hermes never reads them and provider changes had no effect (refs #3178). `get_hermes_dir()` now mirrors Hermes' own resolution order: 1. `settings.hermes_config_dir` — CC Switch explicit override 2. `HERMES_HOME` env var — trimmed, non-empty, taken as-is (no `~` expansion, matching Hermes' `Path(val)`) 3. platform default — Windows reads the `LOCALAPPDATA` env var (the exact value Hermes reads, not a re-derived Known-Folder path) and falls back to `~\AppData\Local\hermes`; mac/Linux keep `~/.hermes` Relation to #3470: that PR dropped `HERMES_HOME` for "consistency with Codex/Claude". But unlike those tools Hermes treats `HERMES_HOME` as a first-class mechanism (its own Windows installer sets it), so ignoring it reintroduces the same mismatch for relocated installs. This keeps it. Tests cover HERMES_HOME precedence, settings-override > HERMES_HOME, blank HERMES_HOME fall-through, the platform-correct default, and a pure `windows_local_hermes_dir` helper exercising the LOCALAPPDATA-set and LOCALAPPDATA-empty fallback paths on every host. * test(hermes): isolate config-dir tests from ambient HERMES_HOME/LOCALAPPDATA `get_hermes_dir()` now consults `HERMES_HOME` (all platforms) and `LOCALAPPDATA` (Windows). The `with_test_home` helper only neutralized `CC_SWITCH_TEST_HOME`, so an ambient `HERMES_HOME` — which Hermes' own Windows installer sets — would make tests that write `get_hermes_config_path()` escape the temp home and touch a real Hermes config (and the same hazard exists via `LOCALAPPDATA` on Windows). Clear and restore both env vars in `with_test_home`. This is safe: only this module reads those env vars, and `dirs` uses the Known-Folder API rather than the env var. Adds a test asserting both are neutralized inside `with_test_home`. Addresses the Codex review comment on #4680. * fix(hermes): trim LOCALAPPDATA to match Hermes' .strip() `windows_local_hermes_dir` checked `OsStr::is_empty()` on the raw value, but Hermes does `os.environ.get("LOCALAPPDATA", "").strip()`. A whitespace/padded `LOCALAPPDATA` would make CC Switch write under the literal padded path while Hermes trims and falls back, hiding the config from Hermes. Trim before the empty check, matching the existing `HERMES_HOME` handling. Adds `windows_local_hermes_dir_trims_localappdata`. Addresses the Codex review comment on #4680. --------- Co-authored-by: thisTom <[email protected]> * feat(codex): support native Responses direct-connect with generated model catalog Codex providers can now run in two modes per provider: - Proxy-Chat (route takeover): apiFormat=openai_chat, existing Responses<->Chat conversion. Unchanged. - Native-Responses (direct): apiFormat=openai_responses, no proxy. cc-switch generates ~/.codex/cc-switch-model-catalog.json so Codex shows the custom models and tools work without the freeform apply_patch (type=custom) tool that native gateways like MiMo reject; editing falls back to shell_command. Catalog generation is keyed on apiFormat (CodexCatalogToolProfile), decoupled from the takeover toggle, so native providers persist a catalog without enabling local route mapping. Codex's catalog parser requires base_instructions on every entry; the native template carries a neutral default and per-vendor official text overrides it (MiMo, MiniMax). Synthesized catalogs for Qwen/Doubao/LongCat use the neutral default. Existing native providers must be re-saved once to regenerate a valid catalog (no DB migration). * fix(codex): disable web_search for native gateways that reject it Some native /responses gateways whose first-party models lack OpenAI's hosted web_search tool (MiMo, LongCat, MiniMax) reject a web_search tool with "tool type 'web_search' is not supported by this gateway phase". Codex sends the tool by default (config-driven, not gated by the catalog's supports_search_tool), so cc-switch now writes the top-level `web_search = "disabled"` line per those vendors' official Codex docs. Scope is a blacklist (default-on): only providers matched by base_url host or model brand prefix are disabled; relays serving real GPT, DouBao, Qwen, and any unknown provider keep Codex's default. Matching by model brand (not just host) also catches aggregators (e.g. SiliconFlow) fronting a reject vendor's model. The field is injected alongside model_catalog_json at switch time and removed via an ownership sentinel, so existing providers need no re-save and switching back re-enables web search. * fix(codex): reject web_search for Qwen3-Coder and correct LongCat context window - Add `qwen3-coder` to the web_search reject model-prefix blacklist so the native qwen3-coder-plus direct-connect preset suppresses the built-in tool (百炼 rejects it for the coder series), while general Qwen models sharing the DashScope host keep web_search enabled. Matched on the model axis, not host. - Correct LongCat-2.0-Preview context window from 128K to its real 1M (1048576), aligning with the MiMo/Qwen 2^20 convention. - Tighten native Responses preset tests to assert exact model→contextWindow catalogs instead of only checking catalog presence. * refactor(codex): decouple model mapping from local-routing toggle Align the Codex provider form with Claude Code. Model mapping (the catalog) is independent from route takeover: native Responses providers (MiMo, Doubao, MiniMax) need it for direct connect with no proxy, while Chat providers use the proxy regardless of any per-provider flag. - Remove the "Needs Local Routing" toggle. It had no backend field and only gated catalog/reasoning persistence, which is equivalent to "is the mapping filled". - Always show model mapping for non-official providers; persist when the list is non-empty (backend already keys off modelCatalog.models). - Gate reasoning visibility/persistence on Chat format, not the toggle. - Mark the Chat upstream-format option "routing required" and refresh the advanced-section hint (zh/en/ja/zh-TW); drop dead localRouting* keys. - Move the model-mapping block above the custom User-Agent block. fix(codex): preserve native-profile hidden catalog fields on edit useCodexConfigState dropped supportsParallelToolCalls / inputModalities / baseInstructions when loading a saved provider, so editing and saving a native Responses provider lost parallel tools, image input and the official base instructions from the generated catalog. Preserve them on load (camelCase + snake_case) and compare them in the row sync. Add a regression test. * fix(presets): point Volcengine/Doubao/BytePlus website links to official sites The websiteUrl for the 火山Agentplan, BytePlus and DouBaoSeed presets was accidentally set to the same invite/console link as apiKeyUrl, so the "visit official site" button took users to the referral/API-key page instead of the product homepage. Restore clean official homepages across all six app preset files; the apiKeyUrl referral links are kept intact. - 火山Agentplan -> https://www.volcengine.com/product/ark - DouBaoSeed -> https://www.volcengine.com/product/doubao - BytePlus -> https://www.byteplus.com/en/product/modelark (drop utm) * fix(presets): use dated Doubao model id and price 6-digit dated models Volcengine Ark rejects the bare model name doubao-seed-2-1-pro with a 404 ("model does not exist or you do not have access to it") even after the model is activated; the API requires the full dated id doubao-seed-2-1-pro-260628. Update the Doubao preset model id across all apps (config default, generated catalog, and OpenClaw namespaced refs). Pricing lookup only stripped 8-digit (YYYYMMDD) and ISO date suffixes, not the 6-digit YYMMDD format Volcengine uses (-260628, -250615), so real Doubao usage never matched the bare-name pricing seed and showed $0 cost. Extend strip_model_date_suffix to also strip 6-digit YYMMDD (with month/day validation to avoid eating non-date version suffixes), keeping the bare-name pricing seed as the canonical identity. This also fixes pricing for every other Volcengine Doubao model. Add unit and end-to-end regression tests. * fix(openclaw): sync Doubao context window to 262144 The OpenClaw DouBaoSeed preset hard-coded contextWindow 128000 while the Codex preset/catalog uses 262144 for the same model, giving OpenClaw users a too-small window that could compress or truncate long context early. Align to 262144 and add a cross-preset consistency test asserting the OpenClaw and Codex Doubao context windows stay equal, so neither side can drift again. * chore(presets): update SiliconFlow referral link Replace the SiliconFlow invite code drGuwc9k with YflgU2Ve across all README locales and provider presets (claude, claude-desktop, codex, hermes, openclaw). * feat(presets): add Qiniu (七牛云) provider preset for all apps Add the Qiniu Cloud AI gateway (api.qnaigc.com primary, api.modelink.ai overseas mirror) as a partner aggregator preset across Claude Code, Claude Desktop, Codex, Gemini, OpenCode, OpenClaw, and Hermes. Qiniu relays native Claude/GPT/Gemini, so Claude Code/Codex pass through native models (no domestic-model pinning); the OpenAI-compatible apps default to gpt-5.5. - Claude / Claude Desktop: Anthropic-compatible bare host, native passthrough - Codex: native Responses at /bypass/openai/v1, gpt-5.5 - Gemini: gemini-3.1-pro-preview via /bypass/vertex - OpenCode / OpenClaw / Hermes: gpt-5.5 over OpenAI-compatible /v1 - Localized display name via nameKey + partner promotion blurb in zh/en/ja/zh-TW - Register qiniu.png icon (URL import + iconUrls + metadata) * feat(presets): add FennoAI provider preset for 6 apps Add FennoAI (api.fenno.ai) as a partner aggregator preset across Claude Code, Claude Desktop, Codex, OpenCode, OpenClaw, and Hermes (Gemini not supported). FennoAI relays native Claude/GPT, so Claude Code/Codex pass through native models; the OpenAI-compatible apps default to gpt-5.5. - Claude / Claude Desktop: Anthropic-compatible bare host (api.fenno.ai) - Codex: native Responses at api.fenno.ai, gpt-5.5 - OpenCode / OpenClaw / Hermes: gpt-5.5 over api.fenno.ai/v1 - Partner referral link as apiKeyUrl + short partner-promotion blurb in zh/en/ja/zh-TW - Add FennoAI sponsor entry to all four READMEs (fenno-banner.png) - Register fenno-icon.webp icon (URL import + iconUrls + metadata) * feat(providers): show API key link in Claude Desktop, OpenClaw and Hermes forms The "Get API Key" link (and partner promotion) below the API key input was only wired for claude/codex/gemini/opencode. Claude Desktop uses a separate form that never rendered it, and OpenClaw/Hermes were blocked by two gaps: - useApiKeyLink whitelisted only 4 app ids, so the link was suppressed for claude-desktop/openclaw/hermes even when a preset carried apiKeyUrl. - useProviderCategory only parsed claude/codex/gemini/opencode preset ids, so OpenClaw/Hermes category stayed undefined and the link condition (cn_official/aggregator/third_party) never held. Changes: - ClaudeDesktopProviderForm: call useApiKeyLink and replace the bare API key Input with the shared ApiKeySection. - useApiKeyLink: add claude-desktop/openclaw/hermes to the whitelist and add ClaudeDesktopProviderPreset to the PresetEntry union. - useProviderCategory: parse and resolve openclaw/hermes preset categories. - Hermes/OpenClaw form fields: don't let an "official" category disable the API key input, since these apps have no OAuth-only official providers (e.g. Hermes' Nous Research is "official" but still needs a user key). * feat(providers): auto-sync Claude common config from live on switch When switching away from a Claude provider that opted into common config, re-extract the shareable portion of its live settings.json and replace the stored snippet. This captures config the user added directly in the app (plugins/hooks/shared prefs) so it isn't lost on switch, and propagates deletions so a removed key isn't re-injected on the next switch. Scoped to Claude providers with common_config_enabled, skipped when the snippet was explicitly cleared. All failures are non-fatal (warn only) and never block the switch. Also harden extract_claude_common_config to strip ALL credential-like keys via pattern match (*_API_KEY / *_AUTH_TOKEN / secret / token / etc.), not just ANTHROPIC_API_KEY / ANTHROPIC_AUTH_TOKEN. Claude providers legitimately use OPENROUTER_API_KEY / GOOGLE_API_KEY (and may carry OpenAI/Gemini/AWS Bedrock/Vertex creds), which previously could leak into the shared snippet and be injected into other providers. This also fixes the pre-existing leak in the manual Extract and one-time auto-extract paths. Plural _TOKENS (e.g. MAX_OUTPUT_TOKENS) is preserved as shareable. Tests: 4 integration (capture / delete-sync / opt-out / cleared) + 1 unit (credential stripping). * chore(kimi): update referral links to new platform.kimi.com domain Moonshot rebranded its console to Kimi, so refresh all Kimi sponsor referral links across provider presets and READMEs: - API (domestic): platform.moonshot.cn/console -> platform.kimi.com - API key page: platform.moonshot.cn/console/api-keys -> platform.kimi.com/console/api-keys - Coding Plan: www.kimi.com/code/docs/ -> www.kimi.com/code/ Also add the missing ?aff=cc-switch param to the codex and openclaw entries that previously had no referral attribution. API endpoints (api.moonshot.cn, api.kimi.com/coding) are intentionally left unchanged. The overseas link (platform.kimi.ai) is not wired up yet since there is no overseas API preset. * docs(kimi): point overseas READMEs to platform.kimi.ai and add Coding Plan link * feat(presets): add ZetaAPI provider preset for 6 apps Add ZetaAPI (api.zetaapi.ai) as a partner aggregator preset across Claude Code, Claude Desktop, Codex, OpenCode, OpenClaw, and Hermes (Gemini not supported). ZetaAPI relays native Claude/GPT, so Claude Code/Codex pass through native models; the OpenAI-compatible apps default to gpt-5.5. - Claude / Claude Desktop: Anthropic-compatible bare host (api.zetaapi.ai) - Codex: native Responses at api.zetaapi.ai/v1, gpt-5.5 - OpenCode / OpenClaw / Hermes: gpt-5.5 over api.zetaapi.ai/v1 - Partner referral link (zetaapi.ai/go/ccs) as apiKeyUrl + short partner-promotion blurb in zh/en/ja/zh-TW - Add ZetaAPI sponsor entry to all four READMEs (zetaapi-banner.png), first-recharge 10% off with promo code CC-SWITCH - Register zetaapi-icon.png icon (URL import + iconUrls + metadata) * perf(icons): downscale ZetaAPI provider icon to 256px The ZetaAPI icon shipped as a 1254x1254 / 940KB PNG, roughly 30x larger than its on-screen render size (~32px in ProviderIcon). Downscale to 256x256, cutting it to ~40KB (-96%) so it no longer bloats the bundle. Same filename, so no import/code change. * perf(icons): downscale relaxcode and sudocode icons to 256px Both shipped far larger than their on-screen render size: relaxcode was 1462x1076 / 1.16MB and sudocode 2048x2048 / 432KB. Downscale to a 256px max dimension (aspect ratio preserved; ProviderIcon renders via <img> object-contain), cutting them to ~41KB and ~36KB. Same filenames. * perf(icons): shrink ccsub and shengsuanyun embedded-image SVGs Both were "fake vector" SVGs wrapping a single oversized base64 PNG (ccsub embedded 1446x1328 / 1.16MB, shengsuanyun 720x720 / 212KB), rendered at ~32px. Downscale the embedded bitmap to a 256px max dimension and re-embed, keeping the SVG wrapper, filename, and import unchanged (no code change). Result: ccsub 1.16MB -> 60KB, shengsuanyun 212KB -> 51KB. * perf(icons): downscale hermes, claudecn and atlascloud icons to 256px All three shipped well above their ~32px render size (hermes/claudecn at 512x512, atlascloud at 3525x3300). Downscale to a 256px max dimension: hermes 125KB->38KB, claudecn 109KB->46KB, atlascloud 105KB->9KB. Same filenames, no code change. * chore(icons): remove unused dds.svg orphan dds.svg (1.4MB genuine vector, 2222 paths) was not imported in index.ts nor referenced anywhere in src/, so it never shipped in the app or appeared in the icon system — it only bloated the repo. Remove it. * feat(presets): add TeamoRouter provider preset for 6 apps Add TeamoRouter (enterprise Agentic LLM gateway) as a sponsor partner across Claude Code, Claude Desktop, Codex, OpenCode, OpenClaw and Hermes. - Claude Code uses bare host https://api.teamorouter.com; Codex uses https://api.teamorouter.com/v1 (native Responses); OpenCode/OpenClaw/ Hermes use gpt-5.5 over the OpenAI-compatible /v1 endpoint. No Gemini. - apiKeyUrl uses the English referral link with UTM params (in-app stays English; README links are per-language). - Add localized in-app promo blurb (zh/en/ja/zh-TW). - Add README sponsor rows below PatewayAI in all four READMEs. - Register the teamorouter provider icon and pad the README banner to the standard 2.40 aspect ratio. * fix(linux): allow overriding AppImage's forced GDK_BACKEND=x11 via CC_SWITCH_GDK_BACKEND (#4351) The AppImage GTK hook (linuxdeploy-plugin-gtk.sh) hardcodes `export GDK_BACKEND=x11`, forcing XWayland to avoid a historical Wayland crash (tauri-apps/tauri#8541). On modern Wayland + NVIDIA setups this forced XWayland instead makes the WebKitGTK webview unable to receive pointer events (GTK titlebar clickable, web content completely dead) and black-screens on resize. Read an opt-in CC_SWITCH_GDK_BACKEND var before GTK init (the hook does not touch it) so affected users can switch back to native Wayland without unpacking the AppImage. Default behavior is unchanged when the var is unset. Refs #4350 Co-authored-by: BoneLiu <[email protected]> Co-authored-by: Cursor <[email protected]> * fix(detect): dedupe Windows Codex npm shims (#4782) * Fix scroll bounds for long select dropdowns (#4798) Co-authored-by: Xvvln <[email protected]> * fix(i18n): rename "Write Common Config" checkbox to "Apply Common Config" (#4829) * fix(i18n): rename "Write Common Config" checkbox to "Apply Common Config" The original label "Write Common Config" (写入通用配置) was ambiguous about data flow direction — it read as "write current config INTO the common config" when the actual behavior is the reverse: merge the saved common config snippet INTO this provider's config. Rename to "Apply Common Config" across zh/zh-TW/en/ja, including all hint/guide/notice references and component defaultValue fallbacks, to make the action unambiguous. Also aligns the Gemini hint wording with the claude/codex "when checked" phrasing. * docs(ja): sync Japanese docs with renamed 'Apply Common Config' label Codex review on #4829 pointed out the Japanese user manual and README still referenced the old checkbox label after the rename. - docs/user-manual/ja/2-providers/2.1-add.md: 共通設定を書き込み → 共通設定を適用 - README_JA.md: 共有設定を書き込む → 共有設定を適用 * feat(sessions): 新增会话分类视图与分组管理 (#4776) * feat(sessions): 新增会话分类查看方式 * test(sessions): 补充会话分类视图覆盖 * feat(sessions): 适配分类视图批量管理 * test(sessions): 补充分类批量选择覆盖 * feat(sessions): 新增分类视图收起状态控制 * test(sessions): 补充分类收起状态覆盖 * fix(usage): keep date-range picker calendar on-screen in narrow popovers (#4860) The custom date-range picker switched to its two-column layout (date fields | calendar) based on the viewport width via Tailwind `sm:` (640px). But the popover is clamped to `100vw - 2rem` and anchored to its trigger with `align="end"`, so its actual available width is not the viewport width. On narrow windows the wide two-column layout could activate while the popover only had room for one column, pushing the calendar column off the right edge of the window where it was clipped and unusable (month header and 4 of 7 weekday columns cut off). Gate the two-column layout on the popover's own inline size via a CSS container query instead of the viewport, and let the popover width shrink to fit the viewport. When the popover is genuinely wide it shows two columns; when it is clamped narrow it stacks vertically so the calendar stays fully visible. The base stacked layout is the safe fallback for engines without container-query support. Closes #4669 * feat(presets): add Amux provider preset for 6 apps Non-partner aggregator across Claude Code, Claude Desktop, Codex, OpenCode, OpenClaw, and Hermes. Claude Code uses bare host api.amux.ai; Codex uses /v1 native Responses; the other three apps default to gpt-5.5. No Gemini preset. Registers the amux inline icon (currentColor) in the icon index and metadata. * docs: document CC_SWITCH_GDK_BACKEND Linux Wayland escape hatch Document the opt-in CC_SWITCH_GDK_BACKEND environment variable (added in #4351) that overrides the AppImage's hardcoded GDK_BACKEND=x11, letting Wayland+NVIDIA users switch back to native Wayland when the webview goes click-dead and black-screens on resize. The override is generic, so tiling-Wayland users can set it to x11 for the inverse input bug. - CHANGELOG.md: new Unreleased/Fixed entry (#4351, fixes #4350) - README.md / README_ZH / README_JA / README_DE: FAQ entry across all locales - docs/user-manual/{zh,en,ja}/5-faq/5.2-questions.md: Linux troubleshooting entry * feat(pricing): add Claude Sonnet 5 model pricing Seed claude-sonnet-5 at Anthropic list price ($3/$15 in/out, $0.30/$3.75 cache read/write per Mtok, matching Sonnet 4.6). The introductory $2/$10 promo through 2026-08-31 is intentionally not seeded. Applied on next app start via ensure_model_pricing_seeded; no SCHEMA_VERSION bump. * feat(presets): switch Claude Sonnet tier default to Sonnet 5 Bump every claude-sonnet-4-6/claude-sonnet-4.6 default pin to claude-sonnet-5 across all provider presets (claude, claudeDesktop, hermes, openclaw, opencode, universal NEWAPI_DEFAULT_MODELS) and the Claude Desktop DEFAULT_PROXY_ROUTES sonnet route id. Update route-derivation test expectations accordingly. Non-Anthropic pins (gpt/gemini/glm/sonnet-4-5) left untouched. * feat(presets): add Code0.ai partner preset for 7 apps Partner aggregator (New API gateway) across Claude Code, Claude Desktop, Codex, Gemini, OpenCode, OpenClaw, and Hermes. Claude Code, Claude Desktop, and Gemini use the bare host (gemini-3.1-pro-preview for Gemini); Codex, OpenCode, OpenClaw, and Hermes use /v1 with gpt-5.5. Marks isPartner with ?source=ccswitch attribution on apiKeyUrl only (website URL stays clean). Adds the partnerPromotion blurb in all four locales and a sponsor row in all four READMEs. Positioned after ClaudeAPI in the Claude Code and Claude Desktop lists (the two apps that carry a ClaudeAPI preset). Registers the favicon-derived code0.png icon via URL import; the README banner is normalized to the standard 1920x798 aspect ratio. * feat(presets): add NekoCode partner preset for 6 apps Partner aggregator (New API gateway) across Claude Code, Claude Desktop, Codex, OpenCode, OpenClaw, and Hermes -- no Gemini, since its /v1beta surface falls through to the web app. Claude Code and Claude Desktop use the bare host https://nekocode.ai; Codex, OpenCode, OpenClaw, and Hermes use /v1 with gpt-5.5. Marks isPartner with the ?aff=CCSWITCH referral on apiKeyUrl only (the website URL and every API endpoint stay bare, so the attribution param never rides on a real request). Adds the partnerPromotion blurb in all four locales and a sponsor row appended to the end of all four READMEs, surfacing the CC Switch offer: 10% off top-ups with promo code cc-switch. Registers the logo-derived nekocode-icon.png via URL import; the README banner is normalized to the standard 1920x798 aspect ratio. * fix(tests): align Sonnet tier expectations with claude-sonnet-5 The default Sonnet tier was bumped to claude-sonnet-5, but the TheRouter preset tests and ClaudeDesktopProviderForm tests still asserted claude-sonnet-4.6/4-6. Update the expected values to match the presets; input fixtures are left as-is to keep exercising legacy-route migration. * docs(release-notes): add v3.16.5 notes (zh/en/ja) * chore(release): v3.16.5 * fix: 更新 OpenCode 会话恢复命令 (#2359) * Update Longcat presets to LongCat-2.0 (#4838) * Update Longcat presets to LongCat-2.0 * fix(proxy): classify LongCat-2.0 as text-only for media sanitizer The Longcat presets now use LongCat-2.0, but the known_text_only_model allowlist still only matched the retired longcat-flash-chat tail. Without this, images pasted into a text-only LongCat-2.0 session are forwarded upstream instead of being replaced with the unsupported-image marker, causing a hard rejection. Add longcat-2.0 (keeping the retired name for saved configs) and a regression test. --------- Co-authored-by: chengzifeng <[email protected]> Co-authored-by: Jason <[email protected]> * feat(universal-provider): Auto-sync after adding and drop unused addSuccess i18n key (#2811) * feat(universal-provider): Auto-sync after adding and drop unused addSuccess i18n key Signed-off-by: Hu Butui <[email protected]> * fix(i18n): sync zh-TW universal provider strings --------- Signed-off-by: Hu Butui <[email protected]> Co-authored-by: Jason <[email protected]> * fix(subscription): display Codex free-plan 30-day quota window (#3651) (#4886) * fix(subscription): display Codex free-plan 30-day quota window (#3651) Codex free accounts are metered on a rolling 30-day secondary window (limit_window_seconds = 2,592,000) instead of the weekly window used by paid plans. The backend already maps this correctly to the tier name "30_day", but the frontend TIER_I18N_KEYS whitelist had no entry for it, so SubscriptionQuotaView filtered the tier out. When that was the only surviving tier (as happens for free accounts), the whole quota footer rendered nothing — free accounts showed no remaining quota or reset time while paid accounts worked. - Add "30_day" -> subscription.thirtyDay to TIER_I18N_KEYS - Add the thirtyDay label to all four locales (zh/en/ja/zh-TW) - Add a unit test locking in window_seconds_to_tier_name mappings, including the 30-day case Fixes #3651 * fix(tray): render Codex free-plan 30-day window in tray menu The tray keeps its own tier-name whitelist (TIER_LABEL_GROUPS) independent of the frontend TIER_I18N_KEYS. Its month group only listed "monthly", so a free Codex account whose only tier is "30_day" produced empty labeled parts → format_subscription_summary returned None → the tray showed no quota, even though the footer now does. That breaks the invariant the existing gemini_summary_lite_only_still_renders test guards: any tier visible in the footer must not leave the tray blank. - Add TIER_THIRTY_DAY ("30_day") constant so the string is single-sourced across backend mapping, tray grouping, and the frontend whitelist - Map 2_592_000s explicitly to it in window_seconds_to_tier_name - Add TIER_THIRTY_DAY to the tray month ("m") group - Add codex_summary_thirty_day_only_still_renders regression test Follow-up to the review on #3651 / PR for the 30-day footer fix. * fix(codex): display renamed session titles (#4927) * fix(codex): display renamed session titles * fix(codex): resolve custom state db for titles * refactor(codex): dedupe state-db resolution and harden title lookup - Extract a shared `codex_state_db` module for `state_5.sqlite` path resolution (config `sqlite_home` / `CODEX_SQLITE_HOME`), previously copy-pasted verbatim between codex_history_migration and the codex session provider. `state_5.sqlite` is now a single source of truth. - Add `busy_timeout` to the read-only title query; without it a read during a concurrent Codex write fails immediately (SQLITE_BUSY) and renamed titles silently fall back to the first user message. - Push the `title == first_user_message` comparison into the SQL WHERE clause (NULL-safe, aligning with Codex's `distinct_thread_metadata_title` semantics) and stop SELECTing `first_user_message`, which can hold large values (openai/codex#29007) — the comparison belongs in SQL anyway, so the column no longer needs to cross into Rust. * feat: add Claude subagent model config (#4830) * feat: add Claude subagent takeover config * feat: add Claude subagent model field * i18n: add Claude subagent model labels * fix(proxy): preserve configured subagent model mapping * fix(providers): exclude subagent model from Claude common config * style: format rust code * feat: add project profiles for snapshot-based config switching Add a profile feature that captures the current provider, MCP, skills and prompt state for Claude Code and Codex as a named snapshot, and re-applies it in one click from the header switcher or the tray Projects submenu. - New profiles table (schema v12) with current marker in settings - ProfileService orchestrates the four existing switch primitives (provider first, then MCP diff, skills diff, prompt enable) - Best-effort apply: dangling references become warnings, no rollback - Header combobox switcher + snapshot-style manage dialog - Tray Projects submenu shared with the UI apply/event pipeline - i18n for zh/en/ja/zh-TW under the new profiles domain - Integration tests covering roundtrip, dangling refs and clear * fix(profiles): scope switcher to supported app tabs and relocate it - Render the profile switcher only when the active tab is Claude or Codex (frontend mirror of backend PROFILE_APPS), so viewing an unsupported app no longer suggests its config was switched - Move the switcher from beside the logo to the right of the route toggles, where the flexible spacer absorbs its appearance and other header controls no longer shift when switching tabs * feat(profiles): include Claude Desktop provider in project profiles Claude Desktop's only dimension managed by cc-switch is its provider (MCP/Skills are hardcoded unsupported and prompts have no live file), so snapshots capture just the current desktop provider while the empty MCP/Skills sets and None prompt make apply a natural no-op for the other dimensions - no per-dimension special casing needed. - Add claude-desktop slot to PROFILE_APPS and PerApp (serde key uses the hyphenated app id); old payloads without the key deserialize to None and leave Claude Desktop untouched on apply - Gate the tray Projects submenu by iterating PROFILE_APPS instead of hardcoding Claude/Codex visibility - Show the profile switcher on the Claude Desktop tab, mirror the PerApp type, invalidate the claude-desktop providers cache on apply, and extend the switcher tooltip in all four locales - Extend the roundtrip integration test with the desktop provider dimension; the desktop switch is cfg-gated to macOS/Windows because desktop live writes error on Linux where CI runs cargo test * refactor(profiles): shared project entity with per-scope switching Projects are now global shared entities; Claude and Codex groups switch independently via scoped current pointers and scoped payload slots. - Remove scope column from profiles; keep current_profile_id_<scope> - Use Option<Vec<String>> for mcp/skills to distinguish 'never captured' from 'captured empty', preventing cross-side accidental disable - Update/apply operations scoped to the active group via merge_scope_from - Tray menu nests same shared list under Claude Code/Codex groups - Add i18n for per-scope tooltips and 'not saved for this side' hint Refs: profile P1 shared-entity redesign * feat(profiles): autosave previous profile state on switch When switching to a different project, the current configuration of the previously active project is automatically captured into that project's snapshot before the target project's snapshot is applied. Scope is limited to the active group (Claude/Codex); autosave failures are collected as warnings and do not block the switch. - Move autosave logic into ProfileService::apply for consistent behavior across UI, tray, and tests - Add integration test covering bidirectional autosave roundtrip Refs: profile switch B方案 * feat(profiles): unconditionally disable proxy takeover before applying profile * fix(ui): invalidate proxy takeover status after profile switch * fix(profiles): stop proxy server when profile switch leaves no takeovers active * feat(profiles): split Claude Desktop into independent profile scope * fix(profiles): use camelCase keys for current profile ids in frontend * refactor(profiles): drop manual snapshot update now that switching autosaves Since switching projects automatically saves the current configuration back to the project being left, the per-project "update from current" button is redundant and even harmful: it allowed overwriting another project's snapshot with the current state, breaking the invariant that a project holds the state you last left it in. - Remove the resnapshot button and confirm dialog from the manage dialog; drop the now-unused scope prop and PROFILE_SCOPE_LABELS - Add a footer Close button to the manage dialog (overlay click is disabled app-wide, so it previously could only be closed via Esc) - Update manageDescription in all four locales to describe the autosave behavior; remove the two dead i18n keys - Fix a shadowed `warnings` vec in ProfileService::apply that silently dropped autosave-failure warnings before they reached the frontend - Mention auto-capture on next switch in the "no snapshot for this scope yet" warning and doc comments The backend update command keeps its resnapshot parameter; it is now only exercised by the pre-switch autosave path. * chore(code0): update partner invite link to agent register URL Replace the ?source=ccswitch referral with the new agent register invite link (https://code0.ai/agent/register/B2XHxGjGmRvqgznY) on code0's apiKeyUrl across all seven presets and the sponsor rows in all four READMEs. API endpoints stay bare per the referral red-line. * test(profiles): gate desktop-scope assertion by platform in profile roundtrip The desktop switch to d2 is cfg-gated to macOS/Windows, but the assertion expecting d2 was not, so on Linux CI the desktop provider stays at the seeded d1 and the assertion panics — poisoning the shared test mutex and cascading into two more failures. Expect d1 on non-desktop platforms. * fix(proxy): close media fallback gaps for Volcano GLM 5.2 image 400s Fixes #5025. The rectifier's media fallback missed Volcano Coding Plan's GLM 5.2 on both paths: - Preventive: known_text_only_model had glm-5.1 but not glm-5.2, so image blocks were forwarded verbatim. Added glm-5.2 as an exact tail match (not a prefix, to avoid stripping images from a future glm-5.2v multimodal variant following Zhipu's 4v/5v naming). - Reactive: the upstream error "Model only support text input" never mentions image/media, so the mentions_image gate rejected it before hints ran; and the existing "only supports text" hint misses the gateway's missing third-person "s". Added a self-evident phrase list ("only support text" / "only supports text") that asserts a modality rejection on its own and bypasses the image-mention gate. Includes regression tests using the verbatim #5025 error body and the glm-5.2[1M] mapped-model form, plus a glm-5.2v negative assertion. * ci: harden release supply chain - Add CODEOWNERS so PRs to main require owner review (closes the gap where two collaborators could approve each other's changes). - Gate the release job behind a 'release' environment, so signing secrets are only unlocked after manual approval. - Stop ignoring the whole .github directory; this had been silently swallowing labeler.yml and workflows/labeler.yml, which are now tracked. Paired with a tag ruleset (created out-of-band) that restricts creation of v* tags to admins, this prevents a write collaborator from pushing a tag to trigger a signed release build. * fix(usage): reject transient transport failures so retry and keep-last-good work Usage/quota queries frequently showed spurious "query failed" states that manual refresh could not clear (#3820). Root cause: all transport-level failures were folded into Ok(success:false), so react-query's retry never fired and the failure body poisoned the cache as regular data. Backend: - balance/coding_plan/subscription services now return Err for send failures and body-read failures (read body via bytes() before serde_json::from_slice; reqwest's json() wraps read errors as Decode, making error-kind checks on it dead code). Auth/4xx/parse errors stay Ok(success:false) and surface immediately. - Script path maps transient AppError keys (request_failed / read_response_failed) to Err; Volcengine adds a Transient call variant. - Command layer skips snapshot persistence, usage-cache-updated emit and tray refresh on Err so the cache bridge cannot overwrite retained data. - Expired-credential retry propagates transient errors instead of rewriting them as "OAuth token has expired". Frontend: - resolveDisplayUsage generalized to subscription quotas; 5th param is now an options object with a `rejected` flag: stale success data retained by react-query across rejections is re-anchored to dataUpdatedAt and expires through the same 10-minute keep-last-good window instead of being shown indefinitely (a total outage used to mask longer than a single 5xx). - useSubscriptionQuota / useCodexOauthQuota gain keep-last-good with per-scope snapshot reset; rejected queries with no displayable value synthesize a failure placeholder carrying the real error message so footers keep rendering a retry entry point. - HTTP 429 is classified transient (retry-later) alongside 5xx. - UsageScriptModal surfaces string rejections via extractErrorMessage. Tests: 6 backend behavior tests drive real HTTP against a local listener to pin the Err/Ok channel semantics; frontend suite extends keepLastGoodUsage coverage (405 passing). * chore(pnpm): settle build-script approvals for esbuild and msw pnpm 10.13+ appends "set this to true or false" allowBuilds placeholders to pnpm-workspace.yaml on every install for unreviewed dependencies with lifecycle scripts. Approve esbuild (postinstall ensures the platform binary) and ignore msw (postinstall only prints an integration notice) so the file stops mutating itself. * docs: add Codex Kimi routing guides * fix: OpenCode live provider import updates (#4712) * fix: sync existing opencode providers from live config * fix: preserve OpenCode provider display names * docs: update comments and log messages to reflect new update behavior - fix stale comment in lib.rs that said existing providers are skipped - change log message from 'Imported' to 'Synced' since count now includes both new imports and updates * fix: sync openclaw and hermes live provider updates (#5098) * fix: sync openclaw live provider updates * fix: sync hermes live provider updates * fix test: hermes live import stores models as array after denormalize The test import_hermes_providers_from_live_updates_existing_provider_from_live seeded models as a dict, but import_hermes_providers_from_live reads via get_providers() which calls denormalize_provider_models_for_read(), converting models from YAML dict to UI-friendly array. The test assertion accessed models as dict -> Null -> assertion failure. Fix: access models as array by index, matching the actual storage format after live import. Also verify the id field is preserved in the denormalized output. * refactor(presets): drop redundant 'OpenAI Compatible' preset Remove the 'OpenAI Compatible' custom-template preset from the OpenCode and OpenClaw preset lists. It was a duplicate entry point: the built-in 'custom' provider flow already exposes an interface-format selector whose default (@ai-sdk/openai-compatible / openai-completions) produces a byte-identical starting config. Having both left two list items pointing to the same place. Existing providers are unaffected — presets only seed form defaults on creation; saved providers store concrete npm/baseUrl/apiKey/models values. The '@ai-sdk/openai-compatible' dropdown label is intentionally kept; it is the format option users pick in the custom flow, not the deleted preset. * Revert "fix(presets): point Volcengine/Doubao/BytePlus website links to official sites" The websiteUrl for 火山Agentplan, DouBaoSeed and BytePlus is intentionally the ccswitch-attributed apiKeyUrl link, per the Volcengine partner's request. Restore the invite/attribution links across all six app preset files. This reverts the websiteUrl changes from 56248087; apiKeyUrl was already left intact there. * fix(mcp): fail closed when Codex config.toml is unparseable during MCP sync sync_single_server_to_codex silently fell back to an empty DocumentMut when the existing config.toml failed to parse, then wrote the whole file back -- wiping every other section (model, model_providers, comments) and leaving only the single synced [mcp_servers.<id>] entry. Introduced by 3a548152, which fixed the removal path but left the destructive fallback on the sync path. Now the sync path returns an McpValidation error and leaves the file untouched, matching the fail-closed behavior of the read/validate path. * fix(codex): strip synced [mcp_servers] from provider snapshots on backfill MCP servers are owned by the DB mcp_servers table; the [mcp_servers] section in live config.toml is only a projection re-synced after every live write. When switching away baked that projection into the stored provider snapshot, servers deleted in the app were resurrected the next time that provider was activated -- per-entry reconcile only knows rows that still exist in the DB, so it can never clean up such orphans. Strip [mcp_servers] (and the legacy [mcp.servers] form) from the live settings during switch-away backfill. Previously polluted snapshots self-heal the next time the user switches away from them. * fix(provider): exclude injected artifacts and routing fields from Codex common-config extraction extract_codex_common_config kept several fields in the shared snippet that must never cross providers: - [mcp_servers] and the legacy [mcp.servers] form: owned by the DB mcp_servers table; once in the snippet they get merged into every opted-in provider and no sync path can ever clean them up. - top-level experimental_bearer_token: normally lives inside [model_providers.<id>] (stripped with the whole table), but three fallbacks write it at top level -- leaking the API key into the shared snippet. - model_catalog_json: per-provider catalog projection pointer. - web_search, only when it equals the injected "disabled" sentinel; a user-set value remains a shareable preference. - top-level wire_api: same provider-routing semantics as top-level base_url (the fallback target when no model_provider is set); leaking it would rewrite the next provider's protocol selection. This makes the extractor safe as the source for switch-time common-config autosync. * fix(provider): re-project Codex MCP after unified-session toggle rewrites live config Toggling unify_codex_session_history rewrites the current official provider's live config.toml in full (intended design), which drops the [mcp_servers] projection -- and nothing put it back, so enabled MCP servers silently vanished until the next provider switch (#C2). Re-project after the rewrite, with two deliberate choices: - Project Codex only (new McpService::sync_enabled_for_app) instead of sync_all_enabled: the all-apps sync short-circuits in AppType::all() order, so a corrupt ~/.claude.json would error before Codex is ever reached and the freshly wiped [mcp_servers] would stay missing. Only Codex's live file was rewritten here, so only Codex needs re-projection. - Degrade projection failure to a warning: by this point the live file already carries the new bucket state, so the toggle has taken effect. Propagating the error would make save_settings roll back the setting, creating the exact "setting=old, live=new bucket" session split the rollback exists to prevent. The projection self-heals on the next switch or any MCP toggle. * feat(provider): extend switch-time common-config autosync to Codex Claude already re-extracts the live config into the shared common-config snippet right before switching away, so shared tweaks (plugins, preferences) made in live propagate to all opted-in providers. Codex was gated out because its TOML pipeline leaked provider-specific and injected content into the snippet. With the extractor now stripping all injection artifacts and routing fields, and backfill stripping the MCP projection, open the gate to Claude + Codex. The autosync-before-strip ordering also self-heals stale snippet values previously baked into provider snapshots: the re-extracted snippet matches the live values, so the value-match strip removes them on the same switch. End-to-end tests cover: new shared keys captured, deletions synced, secrets/injected artifacts never entering the snippet, and provider A's key not leaking into provider B's live. * fix(mcp): stop cross-app failures from blocking MCP re-projection sync_all_enabled iterated AppType::all() with `?`, so one app's corrupt live file (e.g. a broken ~/.claude.json, which passes the existence gate but fails to parse) blocked every app behind it in the iteration order -- and bubbled the error into whatever operation triggered the sync: - switch/save had just rewritten only the target app's live file, yet a broken unrelated file failed the whole operation after DB and live were already updated, reporting a false "switch failed" to the user. Both now project only the target app (sync_enabled_for_app) and degrade projection failure to a warning: the primary operation has already taken effect, and the projection self-heals on the next switch or MCP toggle. - sync_current_provider_for_app_to_live syncs a single app; it now projects that app only, keeping failures (which can only concern the target app) as errors. - sync_current_to_live (config import / cloud-sync restore) keeps the all-apps sweep, but sync_all_enabled is now best-effort: it projects every app, collects failures, and reports them aggregated. Its error is held until after skill sync so an MCP failure no longer skips it. * fix(mcp): surface per-app failures when importing MCP servers from apps import_mcp_from_apps swallowed every importer error with unwrap_or(0), so a corrupt config.toml surfaced as "imported 0 servers" with no hint that anything went wrong. Move the aggregation into McpService::import_from_all_apps: each app imports best-effort (one bad file doesn't block the rest), and failures are collected into a single error naming the failing apps alongside the count that did import. The frontend now refreshes the server list on settle rather than success, since a partial failure still means new servers were persisted. * fix(codex)…
Summary / 概述
Codex Desktop 在最新版本中,登录状态下会向端点请求经过 zstd 压缩的 body,这导致当前的本地路由功能和一部分中转站失效。
此 PR 做了一下更改:
Related Issue / 关联 Issue
Fixes #3764
Fixes #3696
Checklist / 检查清单
pnpm typecheckpasses / 通过 TypeScript 类型检查pnpm format:checkpasses / 通过代码格式检查cargo clippypasses (if Rust code changed) / 通过 Clippy 检查(如修改了 Rust 代码)Updated i18n files if user-facing text changed / 如修改了用户可见文本,已更新国际化文件