Commit Graph

821 Commits

  • fix(codex): preserve custom tool metadata in chat routing (#3644)
    * fix(codex): preserve custom tool metadata in chat routing
    
    * fix(codex): reduce custom tool metadata description overhead
    
    * fix(codex): stabilize custom tool metadata descriptions
    
    * refactor(codex): remove unreachable custom tool description fallback
  • [codex] fix Zhipu coding plan presets (#3524)
    * fix(presets): update Zhipu coding plan endpoints
    
    * fix(model-fetch): probe /models on versioned /vN base URLs
    
    The model-list probe assumed any base URL not ending in /v1 needs /v1/models appended. For providers whose base URL already ends in a version segment like /v4 (Zhipu/Z.AI GLM Coding Plan at .../api/coding/paas/v4), this produced .../v4/v1/models which 404s, so the "Fetch models" button always failed.
    
    Detect a trailing /v{N} version segment and probe {base}/models first, keeping /v1/models as a fallback candidate for non-/v1 versions. Fixes Codex/OpenCode/OpenClaw/Hermes GLM presets and any other vN-style endpoint, with no behavior change for /v1 or non-versioned URLs.
    
    ---------
    
    Co-authored-by: Jason <farion1231@gmail.com>
  • fix(codex): use relative filename for model_catalog_json (#3614)
    * fix(codex): use relative filename for model_catalog_json
    
    Instead of writing an absolute path (which breaks on WSL/symlink setups),
    write only the filename "cc-switch-model-catalog.json" to config.toml.
    Codex CLI resolves relative paths from the config directory, and both
    files always reside in the same directory (~/.codex/).
    
    This eliminates the need for UNC-to-Linux path translation and makes
    the config portable across Windows, WSL, and symlinked directories.
    
    Also simplifies ownership checks in resolve_cc_switch_catalog_path()
    and set_codex_model_catalog_json_field() by removing dead string-equality
    comparisons that never matched on WSL.
    
    Closes farion1231/cc-switch#3573
    Related: farion1231/cc-switch#3569
    
    * style(codex): fix rustfmt violations in model_catalog tests
    
    Wrap a >100-col UNC-path line and remove a trailing blank line that broke 'cargo fmt --check' in Backend CI. Style-only, no logic change.
    
    ---------
    
    Co-authored-by: steponeerror <huxaio0207@qq.com>
    Co-authored-by: Jason <farion1231@gmail.com>
  • feat: 新增 ZenMux Token Plan 供应商,支持手动凭证与 USD 额度富展示 (#2709)
    * feat(Token plan): 增加 ZenMux 支持
    
    * chore: format code with prettier
    
    * chore: format code with cargo fmt
    
    ---------
    
    Co-authored-by: 明桓 <jihaodong.jhd@oceanbase.com>
    Co-authored-by: Jason <farion1231@gmail.com>
  • fix(coding-plan): 适配 MiniMax 余额查询新接口 + 默认定价 (#3518)
    * fix(coding-plan): 适配 MiniMax 余额查询新接口 + 默认定价
    
    ## 余额查询修复
    
    /v1/api/openplatform/coding_plan/remains 新接口不再返回
    current_*_total_count / current_*_usage_count(恒为 0),
    改为返回 current_*_remaining_percent(剩余百分比)。
    旧代码会因守卫失败导致 tiers 为空,tray 不再显示用量。
    
    - 切换数据源到 *_remaining_percent,反转为已用百分比
    - 过滤 model_name == "general",跳过 video 等非编程模型
    - 提取 parse_minimax_tiers 纯函数,便于无 mock 单元测试
    - 复用 TIER_FIVE_HOUR / TIER_WEEKLY_LIMIT 常量
    - 新增 6 个单元测试覆盖主路径与边界
    
    ## 默认定价新增
    按 2026-06-01 上线的人民币价格 (CNY/7.0 汇率,口径与 M2.7 反推一致)
    换算为 USD,在 seed_model_pricing 数组中追加 minimax-m3:
    | model_id     | display_name | input | output | cache_read | cache_creation |
    |--------------|--------------|-------|--------|------------|----------------|
    | minimax-m3   | MiniMax M3   | 0.60  | 2.40   | 0.12       | 0              |
    不取512K上下文定价(参照当前模型标准)
    
    * fix(coding-plan): MiniMax 兼容无周限额套餐 (current_weekly_status=3)
  • fix(codex): always include output_tokens_details.reasoning_tokens in … (#3514)
    * fix(codex): always include output_tokens_details.reasoning_tokens in chat→responses transform
    
    Codex CLI strictly requires reasoning_tokens in the response.completed
    usage object. Custom providers using /chat/completions often omit
    completion_tokens_details, causing repeated parse failures and retries.
    
    * fix(codex): guard non-object completion_tokens_details before insertion
    
    ---------
    
    Co-authored-by: yeeyzy <yeeyzy@yangzhiying05@gmail.com>
  • Stream Codex custom tools with native input events
    Custom (freeform) Codex tools are bridged through Chat Completions as
    JSON `{"input": "..."}` functions, but the Chat->Responses stream still
    re-emitted them via `response.function_call_arguments.*`, leaking the
    JSON wrapper and using event types the Codex client does not route for
    freeform tools.
    
    Emit `response.custom_tool_call_input.delta`/`.done` (with the unwrapped
    input text) for custom tool calls instead, suppressing the intermediate
    function-argument deltas since a partial `{"input":` fragment cannot be
    safely unwrapped mid-stream. Custom tool call items now use a `ctc_`
    item-id prefix (matching the input events' item_id) consistently across
    the streaming and non-streaming paths via a shared
    response_tool_call_item_id_from_chat_name helper.
    
    Covered by new streaming and non-streaming custom-tool tests.
  • Restore Codex tool plugins over Chat Completions third-party proxy
    When proxying Codex Responses requests through the Chat Completions
    format (api_format=openai_chat), the request transform only forwarded
    plain `function` tools and silently dropped `tool_search`, `namespace`
    (MCP), and `custom` tools, so third-party APIs never saw the official
    Codex plugins and could not call them.
    
    Introduce CodexToolContext, built from the original Responses request,
    which flattens all four tool kinds into Chat functions and keeps a
    chat_name -> CodexToolSpec map. The response path (streaming and
    non-streaming) looks names up in this map to restore the original
    tool_search_call / custom_tool_call / namespaced function_call items
    instead of reparsing the flattened name. Long namespaced names are
    truncated with a sha256 suffix to fit the 64-char Chat tool-name limit.
    
    Covered by new round-trip tests for all four tool kinds across both
    streaming and non-streaming paths.
  • Refresh Codex provider label on proxy takeover hot-switch
    During proxy takeover, switching third-party Codex providers left the
    client-visible provider name stale: sync_codex_live_from_provider_while_proxy_active
    based the live config on the existing live file and only patched
    base_url/wire_api/model, never refreshing model_provider or
    model_providers.<id>.name. The Codex app kept showing the previous
    provider in its bottom-right label.
    
    Rebuild the effective settings from the DB for the selected provider so
    the live config carries the correct provider key and display name, then
    merge MCP servers back from the existing live config. base_url stays
    pointed at the local proxy, and official OAuth in auth.json is untouched
    (takeover writes config.toml only when auth preservation is enabled).
    
    Generalize preserve_codex_mcp_servers_in_backup ->
    preserve_codex_mcp_servers_from_existing_config since it now serves both
    the backup and live-sync paths.
  • Harden Codex takeover ownership signaling and serialize switch/takeover
    Gate provider sync and switching on the restore backup / live placeholder
    ("is this live file owned by takeover?") instead of the lagging
    proxy_config.enabled and proxy-running flags. The backup is created
    before enabled=true is committed, so during that activation window the
    old guards were blind and a concurrent sync/switch could rewrite the
    taken-over live file, clearing Codex auth.json for a mis-categorized
    provider.
    
    Acquire a per-app switch lock around both set_takeover_for_app and
    provider switching so the two cannot interleave, splitting the locking
    entry points into outer (lock) / inner (no-lock) pairs to stay
    deadlock-free. Preserve the official OAuth auth in provider-rebuilt
    restore backups by routing the provider token into config.toml. Refine
    takeover idempotency to require the live config to point at the current
    proxy URL, rebuilding from backup when it does not.
    
    Add unit and integration tests covering the official -> DeepSeek ->
    takeover on/off lifecycle and the stopped-proxy switch path.
  • Fix Codex OAuth auth cleared on takeover when provider mis-categorized
    Preserve-mode takeover routes the PROXY_MANAGED placeholder into
    config.toml and must leave auth.json (the ChatGPT OAuth login) intact.
    c9cadd6e covered only the None-provider write branch; the
    Some(provider) branch still ran the category-based auth decision in
    codex_config::write_codex_live_for_provider, whose first clause
    (category == "official" && has_login_material) ignores the preserve
    flag entirely. Because takeover stamps OPENAI_API_KEY = PROXY_MANAGED
    into auth, codex_auth_has_login_material returns true, so a provider
    stored with a stale/mis-classified "official" category (e.g. DeepSeek)
    had its real auth.json overwritten with the placeholder — the access
    token vanished on takeover and reappeared on cleanup.
    
    Fix at the takeover entry instead of patching the fragile category
    logic: add write_codex_takeover_live_for_provider, which under
    preservation detects the PROXY_MANAGED placeholder in auth and writes
    only config.toml (bearer token + catalog projection), never touching
    auth.json. Gating on the placeholder is orthogonal to category, so any
    mis-classification is handled. All four takeover sites
    (sync-while-active, takeover_live_configs, _strict, _best_effort) now
    route through it; restore (verbatim) and non-takeover provider writes
    are unchanged.
    
    Also projects the model catalog via
    prepare_codex_live_config_text_with_optional_catalog, so the
    model_catalog_json pointer survives takeover too.
    
    Add a regression test: official-category provider + preserve enabled +
    proxy takeover must keep auth.json byte-identical while moving the
    placeholder into config.toml.
  • Fix Codex model catalog lost on proxy takeover-off restore
    Turning proxy takeover off restores Live from a stored backup via
    write_codex_live_verbatim. That path mishandled the Codex model catalog
    for two backup shapes that need opposite treatment:
    
    - Snapshot backup (read_codex_live_settings -> {auth, config}): no inline
      modelCatalog, but the config.toml text already carries the live
      model_catalog_json pointer. The old code ran catalog projection, which
      saw no specs and stripped the pointer.
    - Provider-rebuilt backup (update_live_backup_from_provider): inline
      modelCatalog (DB SSOT) with a pointer-less config text. A pure verbatim
      write ignored the inline catalog and never regenerated the pointer.
    
    The projection decision is orthogonal to auth: a provider-rebuilt backup
    can pair an inline modelCatalog with empty auth.json ({}) when the API key
    lives in the config's experimental_bearer_token. The empty-auth branch
    raw-wrote config and skipped projection entirely, so that shape lost its
    mapping too.
    
    Decide the config.toml text once, before splitting on auth, via the new
    prepare_codex_live_config_text_with_optional_catalog helper: project only
    when an inline modelCatalog is present, else keep the text raw (preserving
    any existing pointer). Every config-writing branch (write-auth,
    delete-auth, no-auth) now applies it consistently. Add regression tests
    covering all three shapes.
  • Fix Codex model catalog being wiped by live-config backfill
    `modelCatalog` is a cc-switch-private field whose SSOT is the database; Live's
    config.toml only carries a lossy `model_catalog_json` projection. Proxy
    takeover/restore cycles and the official Codex.app rewriting config.toml can
    drop that projection, so `read_live_settings` reconstructs an empty catalog.
    Two paths then overwrote the stored mapping with that empty Live snapshot:
    
    - Switch-away backfill (`switch_normal` -> `restore_live_settings_for_provider_backfill`):
      now overlays the DB provider's `modelCatalog`, falling back to the
      Live-reconstructed one only when the DB has none.
    - Edit dialog (`EditProviderDialog`): when editing the active Codex provider it
      preferred Live over the DB SSOT; now keeps the DB `modelCatalog` so opening +
      saving no longer clears the mapping table.
    
    Add Rust backfill tests (preserve DB catalog when Live lacks it; keep Live
    catalog when DB has none) and a frontend regression test for the edit dialog.
  • fix(codex): always update model catalog JSON on provider switch (#3360)
    * fix(codex): always update model catalog JSON on provider switch
    
    Without this fix, the cc-switch-model-catalog.json file and the
    model_catalog_json field in config.toml were only regenerated when
    should_sync_backup was true (proxy active or backup exists). Switching
    providers while the proxy was idle left the catalog pointing at the
    previous provider's models, requiring a full restart to take effect.
    
    After the existing should_sync_backup block, unconditionally call
    write_codex_provider_live_with_catalog when switching a Codex provider
    and the proxy is not currently active (live_taken_over == false). This
    mirrors what live.rs already does during a normal provider apply.
    
    Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
    
    * fix(codex): refresh catalog on restored hot switch
    
    ---------
    
    Co-authored-by: yueqi.guo <guo_yueqi@qq.com>
    Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
    Co-authored-by: Jason <farion1231@gmail.com>
  • fix(usage): resolve per-app credentials for native balance/coding-plan queries (#3355)
    * fix(usage): resolve per-app credentials for native balance/coding-plan queries
    
    The native usage-query paths (balance + coding_plan) in
    `query_provider_usage_inner` read credentials only from `env.ANTHROPIC_*`.
    That matches Claude providers, but Codex stores its key in
    `auth.OPENAI_API_KEY` with the base URL inside a TOML `config` string,
    Hermes/OpenClaw flatten them at the top level, and OpenCode nests them under
    `options`. So the card "refresh usage" / auto-query returned empty
    credentials and failed ("查询失败") for those apps, even though the
    config-page "Test" button worked (the frontend extracts per-app correctly).
    
    Introduce a single per-app resolver `Provider::resolve_usage_credentials`
    that mirrors the frontend `getProviderCredentials`, and route both native
    branches through it. Add `extract_codex_base_url` to `codex_config` as the
    canonical Codex TOML base-URL parser and make the proxy adapter delegate to
    it (removing a duplicate copy). Align the frontend `getProviderCredentials`
    to cover OpenCode and Claude Desktop and to use the same OpenRouter/Google
    key fallbacks as the backend.
    
    Fixes #3158
    Fixes #3100
    Fixes #2625
    
    * refactor(usage): explicit AppType arms + frontend trailing-slash trim
    
    Address two review nits on the per-app credential resolver:
    
    - provider.rs: replace the catch-all `_` arm in resolve_usage_credentials with an explicit `AppType::Claude | AppType::ClaudeDesktop` arm, so a new AppType variant fails to compile here instead of silently defaulting to the Anthropic env shape.
    - UsageScriptModal.tsx: normalize getProviderCredentials baseUrl through a single trailing-slash trim so the frontend 'Test' path matches the backend resolver (trim_end_matches), keeping front/back truly in lockstep.
    
    No behavior change for well-formed configs; tests/typecheck/fmt/clippy clean.
    
    * fix(usage): skip empty primary credential fields in fallback chain
    
    `obj.get(key)` returns `Some` for a present-but-empty field, so the
    `.or_else()` fallback chains for the Claude/ClaudeDesktop and Gemini api-key
    lookups only skipped *absent* keys, not empty ones. Presets seed fields like
    `ANTHROPIC_AUTH_TOKEN` as present-but-empty placeholders, so a provider whose
    real key lives in a fallback field (`ANTHROPIC_API_KEY` / `OPENROUTER_API_KEY`
    / `GOOGLE_API_KEY`, or `GOOGLE_API_KEY` for Gemini) resolved to an empty key on
    the native balance/coding-plan path — while the frontend `a || b` (which skips
    empty strings) still found it, reproducing the same Test-works / refresh-fails
    divergence this PR removes.
    
    Add a `first_non_empty` helper that skips present-but-empty values, matching
    the frontend `||` semantics, and use it for both fallback chains. Tests cover
    empty primary + populated fallback for Claude and Gemini.
  • fix: Claude Desktop 官方供应商添加报错 #3402 (#3405)
    * fix: Claude Desktop 官方供应商添加时缺少 ANTHROPIC_BASE_URL 报错
    
    根因:前端 mutation 为 claude-desktop 生成随机 UUID 作为 provider id,
    后端 is_official_provider 通过 id 匹配跳过校验,随机 UUID 不匹配导致
    走入普通 direct 模式校验并要求 ANTHROPIC_BASE_URL。
    
    修复:
    - 前端:claude-desktop + category=official 时使用固定 id "claude-desktop-official"
    - 后端:validate_provider / validate_direct_provider / validate_proxy_provider /
      apply_provider_to_paths 增加 category=="official" 兜底检查
    
    Fixes #3402
    
    * fix: restrict Claude Desktop official provider detection
    
    * fix: add Claude Desktop official provider via seed
    
    ---------
    
    Co-authored-by: 金恩光 <enguang.jin@gmail.com>
    Co-authored-by: Jason <farion1231@gmail.com>
  • fix(codex): add multi-platform CLI discovery and static gpt-5.5 template fallback (#3382)
    * fix(codex): add multi-platform CLI discovery and static gpt-5.5 template fallback for model catalog generation
    
    When cc-switch generates a Codex model catalog for third-party providers,
    it needs the gpt-5.5 model definition as a template. Previously it relied
    on either ~/.codex/models_cache.json (only exists when Codex has connected
    to OpenAI) or running 'codex debug models --bundled' (fails when codex
    is not on the Tauri app's PATH, common in macOS GUI environments).
    
    This commit adds a three-tier fallback:
    1. Try 'codex' from PATH, then platform-specific common paths
       (/opt/homebrew/bin/codex, /usr/local/bin/codex, etc.)
    2. If all CLI attempts fail, use a compile-time embedded gpt-5.5
       template (extracted from codex 0.135.0 bundled models)
    
    Also adds tests for the static template validity and CLI candidates.
    
    * fix(codex): discover user node codex installs
    
    ---------
    
    Co-authored-by: Jason <farion1231@gmail.com>
  • fix: add kimi/moonshot to Anthropic tool thinking history normalizer (#3377)
    * fix: add kimi/moonshot to Anthropic tool thinking history normalizer
    
    Kimi's Anthropic-compatible endpoint requires reasoning_content in
    assistant tool call messages when thinking is enabled, same as DeepSeek.
    The is_reasoning_content_compatible_identifier already included kimi/moonshot
    but is_anthropic_tool_thinking_history_identifier was missing them, causing
    400 errors on multi-turn conversations with tool calls.
    
    Closes #3351
    
    Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
    
    * refactor: unify reasoning vendor hints into a single SSOT list
    
    Replace the two separately-maintained vendor identifier lists with one
    shared REASONING_VENDOR_HINTS constant and a single
    is_reasoning_vendor_identifier predicate. The Anthropic
    tool-thinking-history matcher and the openai_chat reasoning_content
    matcher previously kept independent lists, which is exactly how
    kimi/moonshot ended up in one but not the other (#3351). A single source
    of truth keeps the two from drifting apart again.
    
    Also add a Kimi regression test for the Anthropic tool-history
    normalization path.
    
    ---------
    
    Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
    Co-authored-by: Jason <farion1231@gmail.com>
  • Fix Codex OAuth auth being cleared during preserve-mode takeover
    When "preserve official auth on switch" is enabled, proxy takeover routes
    the PROXY_MANAGED placeholder into config.toml's experimental_bearer_token
    and leaves auth.json (the ChatGPT OAuth login) untouched. Takeover detection
    only inspected auth.json's OPENAI_API_KEY, so it never recognized this state
    and returned a false negative, which led downstream paths to clobber the
    preserved OAuth login.
    
    - Detection: is_codex_live_taken_over now also matches a config.toml
      experimental_bearer_token equal to the placeholder, fixing detect/cleanup/
      restore/startup-recovery in one place.
    - Cleanup: remove the config.toml bearer token only when it equals the
      placeholder (new remove_codex_experimental_bearer_token_if predicate), so a
      real third-party key is never stripped.
    - Write: under preservation, the None-provider takeover path writes only
      config.toml and keeps auth.json intact, matching the provider path.
    - Settings: rename the section to "Codex App Enhancements" and reword the
      description across all four locales.
    - Add tests covering OAuth preservation on takeover and placeholder-only
      cleanup.
  • Refactor Codex live-write routing and cover default auth overwrite
    Collapse the two duplicated write_codex_live_atomic branches in
    write_codex_live_for_provider into a single should_write_auth guard.
    This is behavior-preserving: `if A {X} else if B {X} else {Y}` becomes
    `if A || B {X} else {Y}`.
    
    Adapt the Codex switch tests to the new opt-in default for
    preserve_codex_official_auth_on_switch (flipped off in 3f59ab37):
    add an enable_codex_official_auth_preservation() test helper for the
    cases that assert the auth-preserving path, and tag the official login
    provider with category="official" so it routes through the official
    branch rather than relying on the global preservation flag.
    
    Add a regression test locking the default (preservation off) behavior:
    switching to a third-party provider rewrites auth.json with the new
    API key and discards the existing ChatGPT OAuth login. This is the
    dual of the existing preserve-and-backfill test, which only covered
    the opt-in path.
  • Enrich Codex proxy forwarding-error responses with context
    When forward_with_retry fails for Codex endpoints (/chat/completions, /responses, /responses/compact), the handlers previously returned the bare ProxyError, surfacing a terse body to the client. Build a richer JSON error instead: the message embeds provider, model, endpoint and upstream status, and the error object carries those as structured fields alongside a stable cc_switch_* code.
    
    Align map_proxy_error_to_status with ProxyError::into_response so the manually built responses use the canonical status codes (AuthError->401, Config/InvalidRequest->400, TransformError->422, StreamIdleTimeout->504, AlreadyRunning->409, NotRunning->503); previously these forwarder-level errors collapsed to 500.
  • Default Codex auth preservation to off (opt-in)
    Flip preserve_codex_official_auth_on_switch from true to false so
    third-party Codex switches overwrite auth.json by default, matching the
    expectation that switching providers also swaps credentials. Users who
    rely on keeping the ChatGPT login in auth.json while on a third-party
    provider (for official plugins / remote login) can enable it in
    Settings -> Codex Authentication.
    
    The toggle field ships for the first time here (it is not in v3.16.0),
    so no existing settings.json holds an explicit value -- every user lands
    on the new default and no migration is required.
    
    Also set the flag explicitly in the preservation unit test instead of
    relying on the global default, keeping it valid now that the default is
    false.
  • Fix garbled output and false "not runnable" in Windows version probe
    The version probe ran `cmd /C "\"{path}\" --version"` on Windows. Rust's
    own arg quoting plus the manual quotes produced nested quotes that cmd
    (without /S) misparsed, so it reported even working tools as an
    unrecognized command -> non-zero exit -> shown as "installed but not
    runnable". cmd's error is emitted in the OEM code page (e.g. GBK on
    zh-CN) and was decoded as UTF-8, turning it into mojibake.
    
    - Add decode_command_output: try strict UTF-8, then OEM (GetOEMCP), then
      ANSI (GetACP) code page via MultiByteToWideChar; route all command
      output decoding through it (probes, lifecycle output, osascript,
      terminal launchers).
    - Add run_windows_tool_version_command: run .exe directly (no cmd);
      invoke .cmd/.bat via `cmd /D /S /C call <quoted> --version` with
      raw_arg to bypass Rust's quoting and keep deterministic quote handling.
    - Add windows-sys Win32_Globalization dependency.
  • chore(release): bump version to 3.16.0 and add release notes
    - Bump version to 3.16.0 in package.json, Cargo.toml, tauri.conf.json, and Cargo.lock
    - Add v3.16.0 release notes (zh/en/ja) with contributor credits
  • Align Claude Desktop model mapping with Claude Code three-role tiers
    Claude Desktop's 3P validation only accepts claude-{sonnet,opus,haiku}-*
    role IDs, so providers must map every tier. Bring the Desktop mapping flow
    in line with Claude Code and fix the fallout that broke sub-agent Haiku calls.
    
    - Proxy form: replace the dynamic route list with fixed Sonnet/Opus/Haiku
      tiers; blank tiers backfill from the first filled tier (Sonnet first) on
      submit and inherit its supports1m flag
    - Backend: add a role-keyword fallback in map_proxy_request_model so dated
      official names (e.g. claude-haiku-4-5-20251001) resolve to the right tier,
      guarded by is_claude_safe_model_id so [1m]-suffixed IDs stay rejected
    - Tighten is_claude_safe_model_id / isClaudeSafeRoute to reject degenerate
      role IDs like "claude-sonnet-"
    - Fix the seed-effect race where normalizing empty routes to three blank
      tiers blocked the default-route backfill
    - Sync switch hints, placeholders, and the zh/en/ja/zh-TW locales to the
      three-role-ID rule
    - Update zh/en/ja user manual (2.1, 2.6), calling out legacy Claude IDs
      (claude-3-5-sonnet-...) as a rejected example
    
    Tests: 282 frontend + 34 backend claude_desktop; typecheck, clippy, fmt clean.
  • Update default models and pricing across presets
    Bump default model names project-wide: gpt-5.4 -> gpt-5.5,
    gemini-3.x -> gemini-3.5-flash, glm-5 -> glm-5.1, and
    grok-code-fast-1 -> grok-build-0.1 across all provider presets
    (claude, codex, gemini, hermes, openclaw, opencode, universal),
    Gemini config, and stream check defaults.
    
    Pricing:
    - Seed gemini-3.5-flash, gemini-3.1-flash-lite, step-3.5-flash-2603,
      doubao-seed-2.0-code, mimo-v2.5(/pro), qwen3-coder-480b, grok-build-0.1.
    - Correct deepseek-v4-flash/pro, glm-5/5.1, grok pricing.
    - Add repair_current_model_pricing: idempotent pass that fixes only
      rows still equal to the outdated built-in values, preserving any
      user-customized prices (seed uses INSERT OR IGNORE and cannot update
      existing rows).
    
    Fixes from review:
    - opencode: drop duplicate gemini-3.5-flash variant (unreachable via
      .find), keep the entry with the full minimal/low/medium/high set.
    - Align stale display names/costs to gemini-3.5-flash (hermes, openclaw,
      opencode); openclaw cost -> {1.5, 9, 0.15} to match seed.
    - i18n (zh/en/ja/zh-TW): refresh OMO category tooltips for new model
      names; fix writing tooltip to Kimi K2.5 to match its recommended.
    
    Update tests accordingly and add a regression test asserting unique
    model ids in the Google opencode preset variants.
  • Upgrade default Claude Opus model to 4.8
    Bump the default Opus route/model from claude-opus-4-7 to claude-opus-4-8
    across provider presets (claude, claudeDesktop, hermes, openclaw, opencode,
    universal), i18n locales (zh/en/ja/zh-TW), pricing seed data, and the
    user-manual docs.
    
    - Add claude-opus-4-8 pricing row ($5/$25/$0.50/$6.25); keep the 4-7 row
      for historical usage stats (seeded via INSERT OR IGNORE).
    - Claude Desktop proxy: accept bidirectional opus 4-7 <-> 4-8 route alias
      during rollout so previously saved routes keep resolving.
    - thinking_optimizer: route opus-4-8 through adaptive thinking and normalize
      dotted model ids (also fixes dotted 4-6/4-7 falling back to legacy).
    - usage_stats: normalize Bedrock/Vertex/aggregator opus-4-8 ids to base
      pricing.
    
    Also merge role:"system" messages into the Gemini systemInstruction in the
    Anthropic->Gemini transform.
  • fix(deeplink): preserve custom env fields when importing Claude providers (#2928)
    When importing a Claude provider via deep link, the preview dialog
    correctly shows every env field carried in the `config` payload — but
    on confirmation `build_claude_settings` only persisted the standard
    fields mapped from URL params (`apiKey`, `endpoint`, `model`,
    `haiku/sonnet/opusModel`). Any non-standard env keys such as
    `ANTHROPIC_CUSTOM_HEADERS`, `API_TIMEOUT_MS`, or
    `CLAUDE_CODE_DISABLE_EXPERIMENTAL_BETAS` were silently dropped,
    making the preview and the actual write diverge.
    
    Start the env map from the inline `config` payload (best-effort
    decode via the new `extract_claude_config_env` helper) and then
    overlay the URL-param-derived standard fields on top, so:
    
    - URL params remain authoritative for standard fields (unchanged)
    - Custom env keys present in `config` survive the import
    - Deep links without a `config` payload behave exactly as before
    
    Adds two regression tests:
    - `test_build_claude_provider_preserves_custom_env_fields` proves
      custom fields are kept and URL params override same-named config
      fields.
    - `test_build_claude_provider_without_config_unchanged` guards the
      backward-compatible path (no `config` → only standard env keys).
    
    Closes #2927
    
    Co-authored-by: lipeng31 <lipeng31@tanyang.co>
  • feat(usage): real-time stats refresh + fix codex sync panic on non-ASCII model names (#3027)
    The usage dashboard previously only refreshed on app restart for users
    who don't route through the cc-switch proxy. Two issues were involved:
    
    1. The session-sync background task panicked when a Codex model name
       contained non-ASCII characters (e.g. `【官】glm-5.1`), because
       `normalize_codex_model` sliced `&name[name.len() - 11..]` without
       verifying char boundaries. Once the task panicked, no session logs
       were imported until the app was restarted (where startup-time
       `rollup_and_prune` happened to flush pending data).
    
    2. Even with sync working, the dashboard only polled every 30s and
       skipped polling when the window was unfocused, so freshly-imported
       data was invisible until the next poll or window refocus.
    
    Fixes
    -----
    
    * `normalize_codex_model`: guard the 11-byte ISO-date suffix slice with
      `is_char_boundary` + `is_ascii` checks. ASCII-only suffix means the
      date-stripping logic is correct, and non-ASCII names (which can never
      be valid date suffixes anyway) now bypass the slice safely.
    
    * New `usage_events` module that emits `usage-log-recorded` to the
      frontend whenever `proxy_request_logs` actually gains a new row.
      Sources covered: proxy `log_request`, Claude/Codex/Gemini session
      sync, and startup `rollup_and_prune`. Notifications use a global
      `OnceLock<AppHandle>` so call sites that don't already hold an
      `AppHandle` (e.g. `UsageLogger`) can notify without signature churn.
    
    * 200ms debounce in `notify_log_recorded` collapses bursts (a single
      Codex sync importing 3000+ entries triggers ~2 emits, not 3000) so
      the frontend's `invalidateQueries` is never spammed.
    
    * Frontend `useUsageEventBridge` listens for the event and invalidates
      `usageKeys.all`. Hook is mounted only on `UsageDashboard`, so the
      listener is unsubscribed automatically when the user navigates away.
    
    Verification
    ------------
    
    * `cargo check` passes (existing 25 dead-code warnings in
      `commands/misc.rs` are pre-existing and unrelated).
    * `tsc --noEmit` passes.
    * Manually verified end-to-end: a Codex sync run that imported 3145
      entries produced 2 debounced emits, both logged as `emit
      usage-log-recorded 成功`, and the dashboard updated within ~200ms.
    
    Behaviour notes
    ---------------
    
    * `INSERT OR IGNORE` paths (Claude/Codex session sync) only notify when
      the row is actually inserted, so dedup-skipped writes don't trigger
      empty refreshes.
    * Gemini's `INSERT … ON CONFLICT … DO UPDATE` path reuses the existing
      `conn.changes() > 0` check and only notifies when token counts truly
      changed.
    * `rollup_and_prune` notifies once per pruning cycle (at most once per
      app start) so the dashboard reflects the new aggregate state.
    
    Co-authored-by: in30mn1a <in30mn1a@users.noreply.github.com>
  • fix(about): handle prerelease tools in version check
    Use semver comparison instead of string inequality so locally-ahead prerelease builds aren't misreported as outdated; backend now fetches the full npm dist-tags and, when the local version leads latest, surfaces the tool's prerelease channel (claude=next) as latest.
  • refactor(codex): unify custom model_provider routing key to "custom"
    - Always emit `model_provider = "custom"` from deep link, UniversalProvider, and the universal form modal so future writes share one stable routing key.
    - Add `codex_provider_template_v1` local migration that rewrites legacy keys (aihubmix/ccswitch/...) under `[model_providers.custom]`, updates profile refs, and backs up the original settings_config under `~/.cc-switch/backups/<timestamp>/providers/`.
    - Tighten history migration source detection to a whitelist plus `[model_providers.<id>]` existence check so user-authored keys are never rewritten in jsonl/state DB.
    - Encode deep link name/model/endpoint through `toml_edit::Value` so display names containing quotes or backslashes no longer break the generated config.toml.
    - Stabilize provider settings backup filename hash with Sha256 (was process-random SipHash).
  • refactor(codex): stop force-rewriting user's model_provider field in live config
    The one-time history migration already consolidates legacy provider IDs
    into a single bucket; there is no need to normalize on every write.
    This preserves user-chosen provider identities through the full
    write/backup/restore cycle.
  • fix(session): include Codex archived sessions (#2861)
    Scan Codex archived_sessions alongside active sessions so archived conversations appear in the session manager.
    
    Allow deletion from any validated Codex session root while keeping path safety checks in place.
  • fix(proxy): 修复 Claude 兼容模式下流式响应因空 tool_calls 数组导致 block 状态重置 (#2915)
    问题:OpenAI 兼容 Provider(如 MiniMax)在发送 reasoning_content 流时,
          每个 chunk 包含 "tool_calls": [] 空数组,导致每个字符被当作独立 block 处理
    
    根因:streaming.rs:343 的 if let Some(tool_calls) 匹配空数组进入处理分支,
          发送 content_block_stop 并重置 current_non_tool_block_type 为 None
    
    修改:在 tool_calls 处理前添加 if !tool_calls.is_empty() 判断
    
    示例输入:
    ```jsonl
    {"delta": {"content": null, "reasoning_content": "用户", "role": "assistant", "tool_calls": []}}
    {"delta": {"content": null, "reasoning_content": "用", "role": "assistant", "tool_calls": []}}
    {"delta": {"content": null, "reasoning_content": "中文", "role": "assistant", "tool_calls": []}}
    ...
    ```
    示例错误输出:
    ```text
    event: content_block_start → index 0, type thinking
    event: content_block_delta → thinking "用户"
    event: content_block_stop → index 0
    event: content_block_start → index 1, type thinking
    event: content_block_delta → thinking "用"
    event: content_block_stop → index 1
    event: content_block_start → index 2, type thinking
    event: content_block_delta → thinking "中文"
    event: content_block_stop → index 2
    ...
    ```
    
    Co-authored-by: WangQiang <wangqiang75@huawei.com>
  • feat(i18n): add Traditional Chinese localization (#3093)
    * Add Traditional Chinese localization
    
    * fix: address zh-TW formatting and token units
    
    - Format `zh-TW.json` with Prettier.
    - Use Traditional Chinese `萬` and `億` units for zh-TW token summaries.
    - Add usage formatting coverage for Traditional Chinese locale aliases.
    
    ---------
    
    Co-authored-by: Jason <farion1231@gmail.com>
  • fix: sync Claude Desktop profile during proxy takeover (#3157)
    * fix: sync Claude Desktop profile during proxy takeover
    
    * fix(provider): skip Claude Desktop backup refresh during takeover
    
    - Route Claude Desktop takeover updates directly through the 3P profile writer.
    - Keep takeover startup backup state unchanged when provider metadata changes.
    - Narrow platform-specific test helpers and environment setup with cfg gates.
    
    * fix(provider): restore PathBuf import for CI tests
    
    - Restore an unconditional PathBuf import for provider tests.
    
    - Keep Linux cargo test builds compiling while preserving Claude Desktop cfg-gated helpers.
    
    ---------
    
    Co-authored-by: Jason <farion1231@gmail.com>
  • feat: add MiMo reasoning_content support for Claude Code proxy (#2990)
    * feat: add MiMo reasoning_content support for Claude Code proxy
    
    MiMo requires reasoning_content to be passed back in multi-turn
    conversations with tool calls. Add "mimo" and "xiaomimimo" to the
    preserve_reasoning_content whitelist so thinking blocks are converted
    to reasoning_content when proxying Claude Code → MiMo.
    
    Also handle redacted_thinking blocks (encrypted by Claude Code across
    turns) by injecting a placeholder to prevent MiMo 400 errors.
    
    Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
    
    * fix: support MiMo reasoning histories
    
    - Fix the PR CI clippy failure for redacted thinking handling.
    - Preserve MiMo reasoning content on the OpenAI Chat proxy path.
    - Normalize Claude Desktop Anthropic thinking history for MiMo local routes.
    - Reject unsupported Claude Desktop direct model remaps.
    
    ---------
    
    Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
    Co-authored-by: Jason <farion1231@gmail.com>
  • chore: satisfy clippy::nonminimal_bool and prettier to unblock CI
    - codex_config: rewrite `!(A && !B)` as `!A || B` (De Morgan-
      equivalent) — also reads more directly as "no OAuth, or
      already has provider API key".
    - UsageScriptModal: reflow conditional onto one line per prettier.
  • feat(codex): migrate third-party history provider bucket to unified "custom" ID
    Codex CLI filters conversations by model_provider — after normalizing
    all third-party providers to "custom", old sessions with their original
    provider IDs become invisible. This migration rewrites JSONL session
    files and state_5.sqlite threads to use the unified ID.
    
    Uses a three-tier trust model for source ID collection:
    - Known legacy preset whitelist (46 IDs, case-insensitive)
    - cc-switch-created custom providers (identified by created_at)
    - User-maintained custom providers are explicitly skipped
    
    Adds scanned_history_files flag to prevent stale v1 markers from
    blocking re-runs with the improved source ID collection logic.
  • fix(quota): sort ZhiPu tiers so missing nextResetTime maps to five_hour bucket
    When the 5-hour bucket is at 0% utilization, ZhiPu's API omits
    nextResetTime. The old i64::MAX sentinel sorted these entries last,
    causing the weekly bucket to incorrectly claim the five_hour slot.
  • feat(codex): preserve OAuth login state during third-party provider switching
    Codex provider switches now only write config.toml for third-party providers,
    injecting the API key as experimental_bearer_token. The user's auth.json
    (ChatGPT OAuth tokens) is preserved. Official providers with login material
    still write auth.json normally. Backfill restores bearer tokens into stored
    provider auth.OPENAI_API_KEY to maintain canonical shape.
  • feat(settings): unify unix installers on mktemp+bash, fix WSL install missing native installer
    Extend the mktemp+bash installer pattern from hermes (commit 20d943be) to
    claude and opencode, and remove the cfg gate that locked install_command_for
    to non-Windows targets — Windows host + WSL tools now correctly route
    through the POSIX install priority instead of falling back to bare npm.
    Frontend "one-click install commands" displayed in the About section now
    diverge per platform to match what the backend actually runs.
    
    Backend (src-tauri/src/commands/misc.rs):
    - New CLAUDE_INSTALL_UNIX and OPENCODE_INSTALL_UNIX constants use the same
      `bash -c 'tmp=$(mktemp) && curl -fsSL .../install.sh -o $tmp && bash
      $tmp; status=$?; rm -f $tmp; exit $status'` shape as HERMES_INSTALL_UNIX.
      The shared comment about why we avoid `curl | bash` is now hoisted to the
      top of all three constants — the constraint (WSL `sh -c` sub-shells
      don't inherit outer pipefail; default shell may be dash/ash) applies to
      every shell installer, not just Hermes.
    - New installer_with_npm_fallback helper deduplicates the
      "official installer || npm fallback" chain construction; reuses
      chain_update_commands(LifecycleCommandShell::Posix) so the wiring matches
      the update-chain logic.
    - install_command_for split into cross-platform posix_install_command_for
      + thin #[cfg(not(target_os = "windows"))] wrapper. The cfg gate had
      been hiding a real bug: on Windows hosts, the WSL branch called
      wsl_tool_action_shell_command which forwarded install actions to the
      Windows-target tool_action_shell_command, yielding bare `npm i -g` for
      claude/opencode even though they have working native POSIX installers.
    - wsl_tool_action_shell_command now branches on action: Install dispatches
      to posix_install_command_for (native installer || npm chain); Update
      keeps the existing behavior. Empty install commands collapse to None so
      callers error on unsupported tools instead of running an empty command.
    - build_tool_lifecycle_command pipefail comment updated: the original
      justification (covering install's `curl | bash` path) no longer applies
      to any current command, but the directive stays as defense-in-depth for
      future pipe additions.
    - build_tool_action_line WSL branch comment updated to reflect the
      install/update split.
    
    Frontend (src/components/settings/AboutSection.tsx):
    - New posixScriptInstallCommand(url) helper builds the same mktemp+bash
      string template the backend uses.
    - New powershellEncodedCommand(script) mirrors the backend's UTF-16 LE +
      base64 encoder (charCodeAt + String.fromCharCode(lo, hi) + btoa) so the
      PowerShell EncodedCommand shown to users matches what the backend
      executes.
    - ONE_CLICK_INSTALL_COMMANDS split into POSIX_ONE_CLICK_INSTALL_COMMANDS
      and WINDOWS_ONE_CLICK_INSTALL_COMMANDS, selected by isWindows():
      POSIX shows claude/opencode/hermes with the mktemp+bash installer;
      Windows shows claude/codex/gemini/opencode/openclaw with npm (matching
      backend static_fallback_command on Windows) and hermes with the
      PowerShell EncodedCommand. Display now mirrors backend execution
      per platform, removing the show-vs-run mismatch.
    - Stale comment in the probe-concurrency note mentioning "curl | bash"
      updated to "官方 installer".
    
    Tests (src-tauri/src/commands/misc.rs):
    - New wsl_install_uses_posix_install_priority covers three
      representatives: claude (positive: native installer with npm fallback),
      opencode (positive), codex (negative: no native installer so falls back
      to bare `npm i -g`). Reverse-asserts !contains("| bash") to lock out
      the old pipe form.
    - claude_install_chains_native_then_npm and
      opencode_install_chains_native_then_npm strengthened with
      !parts[0].contains('|') so the native installer portion stays
      pipe-free even if future edits to the template sneak a pipe back in.
    
    Validated:
    - cargo check --tests: clean
    - cargo test --lib commands::misc:: : 59 passed, 0 failed
    - pnpm typecheck: 0 errors
    - cargo fmt: clean
    - pnpm format: clean (no files changed)
  • feat(settings): self-update first chain, switch hermes to native installer
    Promote anchored upgrades to "<bin> update || <package fallback>" for the
    tools whose CLI knows how to self-update safely, and replace pip-based
    hermes install/update with the official NousResearch installer scripts.
    Frontend learns to detect "update reported success but version did not
    change" so silent upstream no-ops surface as a soft warning.
    
    Backend (src-tauri/src/commands/misc.rs):
    - New prefers_official_update(tool, shell) per-platform allow-list:
      POSIX {claude, codex, opencode, openclaw}. Windows drops opencode
      pending anomalyco/opencode#17295 (its silent `upgrade` may prompt for
      install-source detection and deadlock our lifecycle). gemini is absent
      from both because `gemini update` is not implemented upstream
      (google-gemini/gemini-cli#18618 / #16122 OPEN).
    - New chain_update_commands joins primary/fallback per shell: POSIX
      `||`, Windows `|| call` — batch transfers control to the `||` RHS
      without `call`, skipping subsequent tools in multi-tool actions.
    - New LifecycleCommandShell {Posix, WindowsBatch} threads the target
      shell through tool_action_shell_command_for_shell, replacing the old
      hermes-only WSL substitution hack with a uniform mechanism.
    - anchored_command_from_paths refactored: hermes anchors to
      `<bin> update`; tools in prefers_official_update chain self-update
      with the package-manager command as fallback; brew formulae remain
      brew-managed (never self-update what Homebrew owns); others use the
      prior npm/volta/bun/pnpm anchoring unchanged. Mirror on Windows.
    - Hermes commands switch from pip to the official scripts. POSIX/WSL
      runs `bash -c 'tmp=\$(mktemp) && curl -fsSL .../install.sh -o \$tmp
      && bash \$tmp; status=\$?; rm -f \$tmp; exit \$status'`. The mktemp +
      two-step download avoids `curl | bash` because WSL sub-shells inherit
      neither outer `set -o pipefail` nor a guaranteed bash (default may be
      dash/ash). Windows uses `powershell -NoProfile -ExecutionPolicy Bypass
      -EncodedCommand <b64>` with a hand-rolled UTF-16 LE base64 encoder
      for `irm .../install.ps1 | iex`, hiding the PowerShell pipe from
      cmd.exe. Pip is abandoned because macOS system python3 is often 3.9
      while hermes-agent requires >=3.11, and the pyenv `python` shim may
      not exist — pip errors then misclassify as "command not found".
    
    Frontend (src/components/settings/AboutSection.tsx):
    - New versionUnchangedAfterUpdate four-AND short-circuit detects when
      the upgrade command succeeded, the tool still reports a version, but
      that version equals the pre-upgrade version while a different
      latest_version is known. Guards against upstream no-op updaters that
      return exit 0 without applying anything (openai/codex#21897).
    - Soft-failure shape gains kind?: "notRunnable" | "versionUnchanged"
      so the warning toast title can disambiguate the two cases.
    - Hermes install command snippet swapped from `python3 -m pip ...` to
      the official curl-bash one-liner shown to users.
    
    i18n (zh / en / ja):
    - New settings.toolActionVersionUnchangedTitle and
      settings.toolActionVersionUnchanged, synchronized across all locales.
    
    Tests (src-tauri/src/commands/misc.rs):
    - Anchored upgrade assertions updated to `<bin> update || <pkg>` shape
      across nvm / volta / bun / homebrew-npm-global / spaces / fnm
      branches; brew formula explicitly does NOT chain self-update.
    - New hermes anchor tests replace hermes_has_no_npm_anchor on both
      POSIX and Windows. WSL hermes tests rewritten for the mktemp+bash
      flow with `hermes update` fallback.
    - Reverse-lock tests: gemini static fallback must NOT contain
      `gemini update`; opencode Windows static fallback must NOT contain
      `opencode upgrade`; hermes commands must NOT contain
      `python`/`pip`/`powershell`/forbidden pipes.
    
    Validated:
    - cargo check --tests: clean
    - pnpm typecheck: 0 errors
    - cargo fmt: clean