Commit Graph

1877 Commits

  • fix(usage): correct inflated input_tokens in Claude stream parsing
    Some Anthropic-compatible SSE providers (e.g. qwen, minimax) report the
    full context (fresh + cached) as input_tokens in message_start, double
    counting the cached portion that is also reported in
    cache_read_input_tokens. This inflated the cacheable-input denominator
    and pushed the displayed cache hit rate artificially low.
    
    When a message_delta carries a smaller positive input_tokens, prefer it
    over the message_start value and adopt the cache counts from the same
    usage block to avoid double counting; fall back to the start cache
    values when the delta omits them. Native Claude (no input in delta) and
    OpenRouter-converted (input only in delta) paths are unchanged.
    
    Refs #3580
  • fix(opencode): use OpenAI-compatible SDK for APINebula preset
    APINebula is an OpenAI-compatible relay (its base URL ends in /v1, matching
    its Codex/OpenClaw/Hermes presets), but the OpenCode preset loaded the
    @ai-sdk/openai package, which targets the OpenAI Responses API and fails
    against chat-completions-only upstreams. Switch the npm field to
    @ai-sdk/openai-compatible so requests use the OpenAI Chat Completions format.
  • feat(usage): add official subscription quota template with unified tier rendering
    Changes:
    - Add official_subscription template type for Claude/Codex/Gemini
    - Replace implicit 'category=official auto-query' with explicit opt-in template
    - Default disabled; users enable via usage script modal with configurable interval
    - Unify tier→label mapping across subscription and script paths via labeled_tier_parts()
    - Fix tray rendering: week aliases (seven_day/opus/sonnet) now use highest utilization
    - Add depth guard: official_subscription checks enabled flag in query_provider_usage_inner
    - Add cache invalidation symmetry: invalidate_subscription() for disabled providers
    - i18n: add templateOfficialSubscription + hint in zh/en/ja/zh-TW
    
    Backend (Rust):
    - provider.rs: add TEMPLATE_TYPE_OFFICIAL_SUBSCRIPTION branch, flatten SubscriptionQuota→UsageData
    - tray.rs: extract labeled_tier_parts() shared by both summary functions, use max_by for multi-alias groups
    - usage_cache.rs: add invalidate_subscription() method
    - Test coverage: add week-alias highest-utilization tests for both paths
    
    Frontend (TypeScript):
    - UsageScriptModal: add official_subscription to templates, auto-detect for official providers
    - ProviderCard: gate useUsageQuery with !isOfficialSubscriptionUsage, pass autoQueryInterval to footer
    - SubscriptionQuotaFooter: accept autoQueryInterval prop, default 0 (disabled)
    - constants.ts: add TEMPLATE_TYPES.OFFICIAL_SUBSCRIPTION
    
    Fixes tier rendering regression where:
    - Claude/Codex: seven_day was missed (only weekly_limit matched) → lost 7-day window in tray
    - Gemini: gemini_pro/flash/flash_lite fell through to fallback → leaked machine names
    - Multi-window (opus+sonnet): find() took first, not worst → underestimated utilization and emoji color
    
    All tests pass (cargo test + cargo clippy clean).
  • fix: polish usage statistics ui (#3426)
    * fix: improve usage statistics ui
    
    * chore: remove unused token suffix translation
    
    ---------
    
    Co-authored-by: Jason <farion1231@gmail.com>
  • fix: disable auto-capitalize on Input component for macOS (#3626)
    Add autoComplete, autoCorrect, autoCapitalize, and spellCheck attributes
    to prevent macOS from auto-capitalizing the first letter in input fields.
  • fix(coding-plan): route Zhipu quota query to the user's configured base URL (#3702)
    Fixes #3701.
    
    `query_zhipu` was hard-coded to `https://api.z.ai`, so a user who
    configured the mainland China preset (`Zhipu GLM` on
    `open.bigmodel.cn`) could not retrieve usage once the international
    endpoint became unreachable from their network (or vice versa).
    
    The two endpoints share the same quota path (`/api/monitor/usage/quota/limit`)
    and return JSON in the same shape, and — crucially — each user only
    ever uses one of them: the quota host is the same host they're already
    running coding on. So we can route by the configured `base_url` and
    skip the cross-host fallback entirely.
    
    What this PR changes
    --------------------
    
    A single helper that maps the user's `base_url` to the matching quota
    host, and `query_zhipu` rebuilt to take `base_url` and pick the right
    host:
    
        fn zhipu_quota_base(base_url: &str) -> &'static str {
            if base_url.contains("bigmodel.cn") {
                "https://open.bigmodel.cn"
            } else {
                "https://api.z.ai"
            }
        }
    
        async fn query_zhipu(base_url: &str, api_key: &str) -> SubscriptionQuota {
            let url = format!(
                "{}/api/monitor/usage/quota/limit",
                zhipu_quota_base(base_url),
            );
            // ... original 401/403 -> Expired / make_error / parse path, unchanged
        }
    
    The dispatcher already distinguishes `ZhipuCn` from `ZhipuEn` via
    `detect_provider()` and routes the call through
    `query_zhipu(base_url, api_key)` in the same match arm.
    
    Why no cross-host fallback
    --------------------------
    
    Farion's review pointed out that adding a fallback would be
    over-engineered and actively harmful:
    
    1. Reachability is determined by the preset the user chose. Their
       configured host is the host they are already using to run coding;
       if it were unreachable, the user could not have reached the
       "query usage" step at all.
    
    2. The fallback path required distinguishing "both 401/403" (genuine
       bad key) from "one 401/403 + one network error" (regional block),
       which silently misclassified the second case as a generic query
       failure and hid the upstream "Session expired" UX for invalid
       keys.
    
    3. It also cost the worst-case ~10s+10s≈20s serial timeout for users
       on a working primary.
    
    With the URL-based routing in place, 401/403 returns to the original
    `CredentialStatus::Expired` semantics — same UX as `query_kimi` and
    `query_minimax`.
    
    Files changed
    -------------
    
    - `src-tauri/src/services/coding_plan.rs` — 1 file, +35 / -20
    
    Testing
    -------
    
    - 3 new `zhipu_quota_base_*` routing tests
    - 15 existing `coding_plan` parser tests still pass
    - `cargo fmt --check` clean
    - `cargo clippy --lib --no-deps -- -D warnings` clean
    
    Co-authored-by: Yongmao Luo <yongmao.luo@columbia.edu>
    Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
  • fix(proxy): skip backup/restore when Live is already a proxy placeholder (#3689)
    When previous stop_with_restore() failed to restore the user's original
    Live (e.g. app crash mid-stop, settings.json unwritable, or any pre-existing
    state where Live carries the proxy placeholders), the next
    start_with_takeover would read the still-placeholder Live and overwrite the
    good backup row with the proxy config itself. After that, every subsequent
    stop would restore the proxy placeholder back to Live — making the proxy
    toggle a no-op and leaving the client pinned at http://127.0.0.1:15721.
    
    Fix: in both backup write paths (`backup_live_configs` and
    `backup_live_config_strict`) detect that Live is already a proxy
    placeholder and skip the save, preserving any existing good backup. In
    `restore_live_config_for_app_with_fallback_inner`, detect the same
    condition in the parsed backup and fall through to the existing
    SSOT (current provider DB) path that was added in c3d810a.
    
    Both sides share a new `live_has_proxy_placeholder_for_app` dispatch
    helper so the placeholder check stays in lockstep with the existing
    per-app detection functions.
    
    Co-authored-by: Yongmao Luo <yongmao.luo@columbia.edu>
    Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
  • feat(usage): add OpenCode session usage sync (#3215)
    * feat(usage): add OpenCode session usage sync
    
    Add OpenCode as a fourth app type in the usage statistics system.
    Reads per-message token data from opencode's local SQLite database
    (~/.local/share/opencode/opencode.db) and imports into proxy_request_logs.
    
    - New session_usage_opencode.rs module following Codex/Gemini pattern
    - Parses assistant message.data JSON for tokens, cost, model
    - Adds "opencode" to AppType union and filter tabs
    - Updates dedup filters to include opencode_session data_source
    - Adds i18n keys for all 4 locales
    
    * fix(usage): add opencode to UsageHero title themes
    
    * fix: respect XDG_DATA_HOME and platform defaults for OpenCode DB path
    
    - Support OPENCODE_DB env var override (absolute and relative paths)
    - Use ~/Library/Application Support/opencode/ on macOS
    - Use XDG_DATA_HOME/opencode/ when set
    - Fall back to ~/.local/share/opencode/ on Linux
    - Rename misleading test to test_parse_message_data_ignores_role
    
    * fix(usage): use ~/.local/share/opencode on all platforms
    
    OpenCode relies on xdg-basedir, which ignores macOS/Windows conventions,
    so its DB always lives at ~/.local/share/opencode. The previous macOS
    default pointed at ~/Library/Application Support/opencode, which does not
    exist, making the sync a silent no-op for macOS users without
    XDG_DATA_HOME set.
    
    * fix(usage): include opencode -wal mtime in freshness check
    
    OpenCode runs its SQLite DB in WAL mode; new commits land in the -wal
    file and the main DB file's mtime only advances on checkpoint. Keying
    the freshness gate solely on opencode.db could skip newly written
    sessions until a checkpoint occurred. Take the max of the db and -wal
    mtimes instead.
    
    * style(usage): apply cargo fmt to opencode session sync
    
    Fixes Backend Checks cargo fmt --check failure.
    
    Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
    
    * fix(usage): ignore empty OPENCODE_DB and XDG_DATA_HOME env vars
    
    An empty OPENCODE_DB collapsed the path to the data dir (dropping the opencode.db filename); an empty XDG_DATA_HOME produced a relative "opencode" path. Treat empty strings as unset, per the XDG spec.
    
    Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
    
    * fix(usage): harden OpenCode session sync error handling and labeling
    
    - Map '_opencode_session' provider_id to 'OpenCode (Session)' display name
    - Return accurate inserted flag from insert_opencode_message (was always true)
    - Do not advance file/session sync_state when a session errors, so failed
      inserts are retried next run instead of being permanently skipped
    - Surface per-message insert failures into the sync result errors
    - Add opencode_session data-source icon and i18n labels (zh/zh-TW/en/ja)
    - Add provider-stats labeling test for opencode session rows
    
    * fix(usage): only import finalized OpenCode messages
    
    An in-progress assistant message holds partial tokens; OpenCode updates the same message_id with final values later. Since request_id is fixed and the insert uses INSERT OR IGNORE, a partial row could never be corrected. Skip messages without time.completed so each turn is imported once with final usage.
    
    Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
    
    * fix(usage): keep OpenCode incomplete sessions retryable
    
    ---------
    
    Co-authored-by: Eira Hazel <kip3vx9ma@mozmail.com>
    Co-authored-by: Eria hazel <git config --global user.email your@email.com>
    Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
    Co-authored-by: Jason <farion1231@gmail.com>
  • feat: 新增 S3 兼容云存储同步 (#1351)
    * Add S3 Cloud Sync design document
    
    Design for adding AWS S3 as a new Cloud Sync backend alongside WebDAV.
    Hybrid approach: extract shared sync protocol, add independent S3 transport.
    
    Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
    
    * Add S3 cloud sync implementation design (reqwest + Sig V4)
    
    Updated design based on 2026-03-06 draft: switches from rust-s3 crate
    to hand-rolled AWS Sig V4 on existing reqwest for broader S3-compatible
    service support (AWS, MinIO, R2, Alibaba OSS, Tencent COS, Huawei OBS).
    
    Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
    
    * Add S3 cloud sync implementation plan (11 tasks, TDD)
    
    Detailed step-by-step plan covering: sync_protocol extraction, S3 Sig V4
    transport, settings, sync/auto-sync modules, Tauri commands, frontend
    presets/dynamic form, and i18n.
    
    Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
    
    * deps: add hmac crate for S3 Sig V4 signing
    
    Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
    
    * refactor: extract sync_protocol.rs from webdav_sync.rs for shared use
    
    Move transport-agnostic sync protocol logic (constants, types, snapshot
    building, manifest validation, artifact verification, snapshot application,
    utilities) into a new shared sync_protocol module so both WebDAV and the
    upcoming S3 transport can reuse it.
    
    Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
    
    * fix: use transport-neutral error keys in sync_protocol
    
    Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
    
    * feat: add S3 transport layer with AWS Sig V4 signing
    
    Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
    
    * feat: add S3SyncSettings to AppSettings
    
    Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
    
    * feat: add S3 sync module with upload/download/fetch
    
    Implements the S3 sync protocol layer (s3_sync.rs) that combines the
    shared sync_protocol with the S3 transport. Mirrors the WebDAV sync
    module structure with independent sync mutex, connection check,
    upload, download, fetch_remote_info, and sync status persistence.
    
    Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
    
    * feat: add S3 auto sync worker with debounce
    
    Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
    
    * feat: add S3 sync Tauri commands and auto sync worker startup
    
    Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
    
    * feat: add S3 sync TypeScript types and API layer
    
    Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
    
    * feat: add S3 sync i18n translations (en/zh/ja)
    
    Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
    
    * feat: add S3 sync presets and dynamic form to sync settings
    
    Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
    
    * fix: preserve HTTP scheme for S3 custom endpoints (MinIO support)
    
    Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
    
    * test: add live S3 integration tests (env-var driven, --ignored)
    
    Run with: S3_TEST_AK=... S3_TEST_SK=... S3_TEST_BUCKET=... cargo test --lib services::s3::integration_tests -- --ignored
    
    Verifies test_connection, put_object, get_object, head_object, and 404
    handling against a real S3 bucket using the project's own Sig V4 signing.
    
    Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
    
    * chore: remove internal design docs before PR
    
    * fix: wire S3 auto-sync to DB hook & sync UI state on async load
    
    - P1: Add s3_auto_sync::notify_db_changed call in SQLite update_hook
      so S3 auto-sync worker receives DB change signals (was only wired
      for WebDAV, leaving S3 worker idle)
    
    - P2: Add useEffect to update syncType selector when s3Config loads
      asynchronously, preventing stale "webdav" default for S3 users
    
    * fix: satisfy clippy for s3 sync
    
    * fix: address s3 sync review feedback
    
    ---------
    
    Co-authored-by: Keith (via OpenClaw) <keithyt06@users.noreply.github.com>
    Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
    Co-authored-by: Jason <farion1231@gmail.com>
  • fix(codex): drop tool_choice when tools is empty in Responses→Chat conversion (#3640)
    Strict OpenAI-compatible upstreams (vLLM, enterprise gateways) reject
    requests that carry tool_choice or parallel_tool_calls without a
    non-empty tools array, returning 503/400 with:
      "When using `tool_choice`, `tools` must be set."
    
    The Responses→Chat Completions converter unconditionally forwarded
    tool_choice and parallel_tool_calls even when tools ended up absent
    or empty after conversion. This commit adds a guard that removes both
    fields when the resulting tools array is missing or empty.
    
    Added 9 regression tests covering:
    - tool_choice dropped when tools absent
    - tool_choice dropped when tools is empty array
    - parallel_tool_calls dropped when tools absent
    - tool_choice dropped when all tools filtered (e.g., missing name)
    - tool_choice preserved when tools present (auto + function type)
    - clean output when neither tool_choice nor tools present
    - tool_choice 'none' dropped when no tools
    - tool_search_output providing tools keeps tool_choice
    
    Refs #3557
    
    Co-authored-by: yueqi.guo <guo_yueqi@qq.com>
  • fix(proxy): clarify Codex 413 error as upstream limit, not local proxy
    An upstream gateway (e.g. nginx client_max_body_size) rejecting an oversized request body with HTTP 413 was wrapped as "CC Switch local proxy failed ..." with the full nginx HTML page echoed as the cause. This misled users into thinking CC Switch imposed the limit, when the local DefaultBodyLimit is already 200MB and the real cap lives on the provider's server.
    
    413 now gets a dedicated message that points at the upstream gateway, states it is the provider's server-side limit (not CC Switch), and gives actionable recovery steps (/compact, drop large logs/images, ask the provider to raise its limit). Structured fields (upstream_status, provider, model, endpoint) are preserved; other error paths are unchanged.
    
    Refs #666
  • fix(claude-desktop): strip [1m] suffix before proxy route lookup
    Claude Desktop appends a local [1m] marker to the model name when the
    1M-context beta is active (e.g. claude-opus-4-8[1m]). The proxy route
    matcher compared this raw name against clean route IDs, so every match
    tier failed and the is_claude_safe_model_id guard also blocked the role
    keyword fallback, surfacing as route_unknown (HTTP 400) when switching
    to a 1M-capable model mid-conversation.
    
    Strip the [1m] suffix inside map_proxy_request_model before lookup so
    exact/alias/legacy/role matching all see the clean ID, while keeping the
    original name in the route_unknown error for diagnostics. The upstream
    request still carries the mapped real model; 1M capability is negotiated
    via the anthropic-beta header, not the model-name suffix.
    
    Fixes #3588
  • Add media fallback rectifier for text-only models
    Replace unsupported Anthropic image blocks with an [Unsupported Image] marker when routed models are text-only or upstream rejects image input.
    
    Add rectifier settings for media fallback and heuristic model detection, wire the controls into the settings UI, and cover the sanitizer and forwarder gates with regression tests.
  • fix(copilot): 调高无限空白检测阈值 20 → 500 (#2647)
    * fix(copilot): raise infinite-whitespace threshold from 20 to 500
    
    The previous threshold of 20 falsely aborted legitimate tool calls whose
    arguments contain indented code (write_file / edit_file with 4-8 levels
    of indentation in Python / YAML / Rust / Markdown easily exceed 20
    consecutive whitespace chars, especially when newlines are counted).
    
    The real infinite-whitespace bug emits hundreds to thousands of
    consecutive whitespace characters, so 500 keeps the safety net while
    drastically reducing false positives.
    
    Refs #2646
    
    Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
    
    * docs: clarify whitespace threshold comment
    
    ---------
    
    Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
    Co-authored-by: Jason <farion1231@gmail.com>
  • 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
  • feat: add CherryIN preset provider for Claude Code and Codex (#3643)
    * feat: add CherryIN preset provider for Claude Code and Codex
    
    CherryIN (open.cherryin.net) is an API aggregator gateway. Add it as a quick-config preset for both Claude Code (Anthropic format) and Codex (OpenAI-compatible), placed next to AiHubMix, with the official brand icon. Endpoints and model IDs verified against CherryIN's live pricing API.
    
    * feat: add CherryIN preset to Gemini, Claude Desktop, OpenCode, OpenClaw, Hermes
    
    Extend CherryIN coverage to all remaining apps, each placed next to AiHubMix. Anthropic-native (open.cherryin.net) for Claude Desktop/OpenClaw/Hermes, @ai-sdk/anthropic (/v1) for OpenCode, Gemini-compatible endpoint for Gemini CLI. Model IDs verified against CherryIN's live pricing API.
  • [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>
  • docs: refresh user manual for current app support (#3411)
    * docs(usage): document pricing model matching rules
    
    * docs: refresh user manual for current app support
    
    * docs: clarify Hermes configuration files
    
    * docs: align feature docs with visible app support
  • 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.
  • Simplify Codex takeover notice copy and match hint styling
    Restyle the proxy-takeover notice in the Codex editor from the boxed
    Alert to the amber inline-hint style used by the endpoint hints, and drop
    implementation jargon (127.0.0.1 / PROXY_MANAGED) that users could not
    interpret. The notice and the auth/config hints now simply state that the
    form shows the stored provider config rather than the proxy-managed live
    config. i18n synced across en/zh/ja/zh-TW.
  • 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 edit dialog masking live OAuth during proxy takeover
    The reported "OAuth access token disappears when enabling Codex proxy
    takeover" was a display artifact, not data loss: auth.json on disk kept
    the OAuth login the whole time. During takeover the edit dialog falls
    back to the stored provider config (so it does not surface the proxy
    placeholder), which for a third-party provider shows that provider's own
    key instead of the live auth.json, making the OAuth token look gone.
    
    Thread an isProxyTakeover flag from App through ProviderForm into the
    Codex editor and show an explicit notice plus storage-aware auth/config
    hints clarifying that the form displays the stored provider config while
    the live config is temporarily managed by the proxy. Drop the
    proxy-running condition so the notice shows whenever takeover is active,
    even with the proxy stopped.
    
    Add a regression test asserting the dialog does not read live settings
    during takeover and renders the database config. i18n synced across
    en/zh/ja/zh-TW.
  • 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.