Commit Graph

1890 Commits

  • fix(providers): only block explicit official providers under proxy takeover
    The proxy-takeover block previously fell back to the isOfficial heuristic
    (empty base_url / missing key) when category was absent. That misjudged
    custom providers whose endpoint lives in meta or whose fields are simply
    unfilled: their switch button got disabled, making users think the config
    was broken. That extra UI block was also "virtual" — the executor in
    useProviderActions only ever honored category === "official", so the
    front end blocked more than the backend would enforce.
    
    Gate the block solely on explicit category === "official", matching the
    executor and unifying both verdicts on a single source of truth.
    
    Also rework the blocked-state UI:
    - drop the red "blocked" badge for a plain disabled Enable button
    - move title/cursor onto a wrapper span (disabled buttons set
      pointer-events:none, so an on-button title/cursor never fired)
    - replace the account-ban warning tooltip with a lighter hint
      (provider.blockedByProxyHint), four locales kept in sync
  • feat(proxy): map input_file and input_audio content parts to chat
    Convert Responses input_file (requiring file_id or file_data, never file_url which Chat file parts do not support) and input_audio parts into their Chat Completions equivalents, and handle top-level input_* items that previously fell through and were dropped, clearing stale pending reasoning for non-assistant messages.
  • fix(proxy): distinguish truncated chat streams from normal completion
    Replace the unconditional finalize at chat-to-responses stream end with a three-way guard: complete normally when finish_reason or [DONE] arrived, emit an incomplete response when substantive output exists without a finish_reason, and emit a failed (stream_truncated) event for empty truncation instead of masking it as completed. Also propagate late-arriving reasoning_content onto still-active tool-call items.
  • fix(proxy): cache reasoning across turns for custom_tool_call and tool_search_call
    Generalize the cross-turn reasoning cache in codex chat history from function_call only to the full tool-call triad (function_call, custom_tool_call, tool_search_call) and their *_output counterparts, so apply_patch and tool-search calls keep their reasoning_content when restored via previous_response_id.
  • chore(presets): update SSSAiCode domain and endpoint nodes
    Switch website/apiKey URLs to sssaicodeapi.com and replace base URL
    nodes with node-hk.sssaicodeapi.com (default), node-hk.sssaiapi.com,
    and node-cf.sssaicodeapi.com across all 7 app presets.
  • fix(proxy): resolve actual port for ephemeral (port 0) listen config
    When listen_port is 0 the OS assigns the port at bind time, so the
    configured value can no longer be trusted for building takeover URLs.
    
    - server: read listener.local_addr() after bind and propagate the
      actual port to the global proxy port, status, and ProxyServerInfo
    - services: start the proxy before takeover when port is 0 so live
      configs get the real port instead of :0, and persist the resolved
      port back to the DB for DB-only URL paths; stop the pre-started
      server on any takeover failure
    - claude_desktop: reject an unresolved :0 port instead of emitting a
      broken gateway URL
    - build_proxy_urls: prefer the running server's port and error out if
      the port is still 0
    
    Add tests for takeover with an ephemeral port and the claude_desktop
    :0 rejection; switch existing codex takeover tests to an ephemeral
    port for isolation.
  • feat(proxy): add GET /v1/models endpoint for Codex CLI reachability check (#3818)
    * feat(proxy): add GET /v1/models endpoint for Codex CLI reachability check
    
    Codex CLI probes GET /v1/models at startup. Without this endpoint the proxy
    returns 404, causing Codex to fail before any request reaches the upstream
    LLM.
    
    Return an OpenAI-compatible model list derived from the cc-switch–managed
    model catalog file.
    
    Fixes #3812
    
    * fix(proxy): return Codex catalog schema from /v1/models
    
    Codex deserializes the response as a catalog with a top-level `models`
    field, not the OpenAI `{"object":"list","data":[...]}` envelope.
    Return the catalog file content directly so the format matches what
    Codex expects.
    
    Co-authored-by: Codex review bot
    
    * fix(proxy): guard /v1/models against serving stale catalog
    
    Only return the model catalog when config.toml still references it via
    `model_catalog_json`.  After switching to a provider without a custom
    catalog, the old file lingers on disk — serving it unconditionally
    would advertise the previous provider's models to Codex.
    
    Co-authored-by: Codex review bot
    
    * fix(proxy): match relative model_catalog_json in stale-guard
    
    cc-switch writes `model_catalog_json = "cc-switch-model-catalog.json"`
    (relative) via set_codex_model_catalog_json_field.  Match on the
    filename constant rather than the absolute path so the guard works
    with both relative and absolute paths.
    
    Co-authored-by: Codex review bot
    
    * fix(proxy): parse model_catalog_json field instead of substring match
    
    Replace raw config_text.contains() with proper TOML field parsing so
    commented-out lines and stray mentions of the filename in other fields
    don't defeat the stale guard.  Also switch from contains() to exact
    filename match (Path::new(val).file_name() == Some(...)) to stay
    consistent with resolve_cc_switch_catalog_path in codex_config.rs.
    
    Add log::debug! when the guard blocks serving so the operator can
    distinguish "no models configured" from "guard blocked stale catalog".
    
    * refactor(proxy): reuse resolve_cc_switch_catalog_path in handle_models
    
    Replace the inline config.toml parsing and filename match in
    handle_models with the existing resolve_cc_switch_catalog_path helper
    (now pub(crate)). This removes the duplicated stale-guard logic, keeps
    a single source of truth for catalog-path ownership, and makes the
    handler honor absolute model_catalog_json paths the same way Codex
    live-setting import does.
    
    ---------
    
    Co-authored-by: Jason <farion1231@gmail.com>
  • [codex] Fix VS Code session previews (#3593)
    * Fix Codex VS Code session previews
    
    * fix(codex): use last IDE request heading for session previews
    
    A markdown heading inside the active selection / open file could precede the real injected request, so matching the first "## My request for Codex:" heading picked selection content instead of the user prompt. Scan for the last matching heading (the IDE injects the real request as the final section) on both the Rust title path and the frontend TOC preview path.
    
    Add regression tests for the selection-heading case, and pin the known best-effort limitation when the request body itself repeats the heading.
    
    ---------
    
    Co-authored-by: Jason <farion1231@gmail.com>
  • fix: normalize path separators in scan_dir_recursive for Windows (#3430)
    On Windows, Path::strip_prefix produces backslash-separated relative
    paths. The update-check matching logic uses rsplit('/') to extract the
    install name, so subdirectory skills (e.g. skills/my-skill) never
    matched and updates were silently skipped. Replace backslashes with
    forward slashes when building the directory string.
  • 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.