Commit Graph

765 Commits

  • feat(settings): anchor tool upgrades to the actual install
    Rework the About tool-upgrade flow so upgrades target the install the
    command line is actually using, instead of running bare `npm i -g` and
    landing on PATH's first npm — which used to move upgrades to the wrong
    location or fail outright when the resolved bin had no sibling npm.
    
    Anchored upgrade command (update path, non-Windows):
    - claude native installer (~/.local/share/claude/versions/) -> `claude update`
    - Homebrew formula (canonical path under /Cellar/) -> `brew upgrade <formula>`
    - volta / bun -> `volta install` / `bun add -g`
    - Node managers with sibling npm (nvm/fnm/mise/homebrew-npm) -> that
      directory's npm with `i -g <pkg>@latest`
    - system / unknown sources (e.g. opencode install.sh under ~/.opencode/bin,
      ~/go/bin) -> fall back to the bare command, surfaced as anchored=false
      so the UI shows an honest "default entry not determinable" hint instead
      of pretending the upgrade is targeted.
    
    Multi-install confirmation:
    - When the probe finds >=2 installs, pop ToolUpgradeConfirmDialog showing
      each install (source badge + path + version + Default badge) and the
      resolved command before executing. Top-level text only asserts what is
      universally true ("won't update all of them; see each tool below");
      per-card text adapts to anchored vs unanchored.
    - Pending upgrade state stores the full upgrade set; the dialog only
      shows the subset that needs confirmation, so non-conflicting tools in
      a batch still get upgraded after confirm.
    
    Unified probe command:
    - Merge the previous diagnose_tool_installations and plan_tool_upgrades
      into a single probe_tool_installations returning installs + is_conflict
      + needs_confirmation + command + anchored. Diagnose button, post-upgrade
      auto-diagnose, and pre-upgrade confirmation all share one IPC. The
      diagnose button's previous Promise.all of 6 IPCs is now a single call.
    
    resolve_path_default robustness:
    - The previous lines().next() was poisoned by interactive .zshrc output
      (welcome banners, p10k instant prompts, etc.), causing default-install
      detection to fail and dropping multi-install scenarios into the
      unanchored fallback even when one install was clearly the native one.
    - Replaced with first_abs_path_line that scans for the first
      absolute-path line and ignores noise, mirroring how try_get_version
      already tolerates banner noise via a regex.
    
    Diagnostic staleness fix:
    - diagnoseToolSilently now clears stale diagnostics in the else branch
      when the conflict no longer holds (previously only wrote on conflict,
      never cleared, so externally resolving a conflict left the card stuck
      on a stale list until the user clicked Diagnose).
    - Auto-diagnose now triggers after any successful update, not only when
      the version didn't change, so the card reflects the latest conflict
      state even when an anchored upgrade succeeds while another install
      remains.
    
    Other:
    - Shared ToolInstallRow between the conflict list and the confirmation
      dialog removes the duplicated per-install row JSX.
    - Drop the new shell_quote_path helper in favor of reusing the file's
      existing POSIX-correct shell_single_quote, gated on whitespace.
    - Collapse displayName lookup into a stable module-level helper to remove
      the `as ToolName` cast and avoid recreating the closure each render.
    - 17 backend unit tests covering anchored command generation across each
      source, brew formula extraction, is_conflicting thresholds, default
      install resolution, and the welcome-banner scenario.
    - i18n (zh/en/ja): new keys for confirmation dialog
      title/hint/will-run/confirm-button and the unanchored hint.
  • feat(settings): diagnose conflicting tool installations with uninstall hints
    Add a "Diagnose installs" action to the About section that enumerates every
    installation of each managed CLI, flags real conflicts (diverging versions
    or mixed runnable state), and lists them under the affected tool card.
    
    - backend: enumerate_tool_installations + diagnose_tool_installations command,
      sharing build_tool_search_paths with the single-probe scan; resolve the PATH
      default via the same login shell as version probing so the "Default" marker
      matches what the command line (and an upgrade) actually targets
    - frontend: global diagnose button left of Refresh, per-card conflict list,
      and a copy-only uninstall command per install -- source-aware (npm/volta/
      bun/pip) and anchored to that install's own npm to avoid removing the wrong
      node version; marked with a red trash icon, never executed
    - auto-diagnose silently after an upgrade that leaves the version unchanged
      or installs something that can't run
    - i18n: zh/en/ja
  • fix(misc): probe tool versions faithfully instead of masking unrunnable installs
    Unify the three probe paths (try_get_version / try_get_version_wsl /
    scan_cli_version) on a cross-platform ShellProbe enum that distinguishes
    "not installed" (shell exit 127) from "installed but --version failed"
    (e.g. Node too old to run the freshly-upgraded CLI).
    
    The found-but-broken case is now reported as-is and no longer falls back to
    scan_cli_version. Previously the fallback surfaced a stale runnable copy
    installed elsewhere (e.g. a Homebrew node vs the nvm node the upgrade wrote
    to), making an upgrade look like it had no effect ("succeeded but version
    unchanged").
    
    Expose the state via a structured `installed_but_broken` flag on ToolVersion
    so the frontend no longer infers it by string-matching the error text. Also
    fixes a WSL path that could mislabel a genuinely-missing tool as broken, and
    extracts the NOT_INSTALLED constant.
  • feat(misc): broaden CLI version detection to PATH and Windows managers
    Extend scan_cli_version search paths with the PATH environment variable
    and common Windows Node/version-manager locations (pnpm, Volta, nvm,
    Scoop, Yarn). Entries from PATH are skipped when they point at the
    Microsoft\WindowsApps App Execution Alias directory, which is what
    previously caused bogus launches on Windows.
    
    Add unit tests for PATH dedup, child-dir suffix expansion, and the
    WindowsApps alias skip.
  • feat: add silent install/update lifecycle for managed CLI tools
    Expand tool version detection to all six managed apps (including Hermes
    via PyPI), and add a run_tool_lifecycle_action command that installs or
    updates CLI tools silently: it captures the subprocess output and blocks
    until the command actually finishes instead of popping a terminal window
    (Windows uses CREATE_NO_WINDOW). On failure the last lines of stderr are
    returned so the UI can surface a useful message.
    
    Also restores the Windows version-detection path by gating try_get_version
    behind cfg(not(windows)) so it no longer risks triggering the App
    Execution Alias / protocol handler that previously disabled Windows.
  • refactor(proxy): replace panic-prone unwrap/expect with safe patterns
    Harden the proxy module against panics by removing optimistic
    unwrap()/expect() calls in favor of pattern matching and graceful
    fallbacks:
    
    - copilot_optimizer/cache_injector: bind Value::Array/String directly
      instead of is_array()+as_array().unwrap(); use is_none_or and in-place
      string mutation
    - hyper_client: gate the raw-write path with if-let + filter instead of
      has_cases + unwrap()
    - gemini_shadow: recover poisoned RwLock via into_inner() rather than
      panicking, with warn logging
    - streaming_codex_chat: replace expect("tool state exists") with
      let-else (return/continue)
    - merge_tool_results: early-return when messages is absent
    - sse: fall back to from_utf8_lossy on the UTF-8 boundary slice
    
    No behavior change on the happy path; all proxy tests pass.
  • test: align stale tests with merged Codex preset and bucket changes
    CI on main was red because two tests did not follow intentional changes
    that were already merged into the codebase:
    
    - codexChatProviderPresets.test.ts still expected the "Kimi For Coding"
      preset, which was removed in 16c3ef3f. Drop the corresponding entry
      from the expected presets map.
    - import_export_sync.rs still asserted the legacy per-provider
      model_provider id ("rightcode"), but 9fac15b8 unified Codex
      third-party providers into the stable "custom" history bucket. Mirror
      the assertion update already applied to provider_service.rs.
    
    No production code changed; both fixes only update test expectations.
  • fix: coerce empty tool-call arguments to {} in Codex Chat transform
    No-argument tool calls produced empty argument strings that passed
    through both the streaming response path and the request transform
    verbatim. Strict OpenAI-compatible upstreams (e.g. Minimax) reject
    `arguments: ""` with a 400 "invalid function arguments json string",
    while lenient ones (OpenAI, Kimi) silently treat it as an empty object.
    
    Add canonicalize_tool_arguments / canonicalize_tool_arguments_str helpers
    that coerce empty/whitespace arguments to "{}" and apply them at the four
    tool-call argument sites (streaming finalize + three request/response
    transform paths). Leave function_call_output content untouched, where an
    empty string is valid.
  • feat: adaptive reasoning detection for Codex Chat providers
    Auto-detect each Chat-routed Codex provider's reasoning interface from
    its name, base URL, and model, then inject the matching thinking
    parameter without manual configuration:
    
    - Platform-first inference (OpenRouter, SiliconFlow) overrides model
      rules, since the same model exposes different reasoning controls
      depending on the hosting platform.
    - Effort tiers are forwarded only to providers that support them
      (DeepSeek, OpenRouter, and StepFun's step-3.5-flash-2603); on/off-only
      providers (Kimi, GLM, Qwen, MiniMax, MiMo, SiliconFlow) drop the level
      instead of sending a field the upstream rejects.
    - OpenRouter uses the native reasoning:{effort} object, clamps max to
      xhigh (its enum has no max), and forwards an explicit effort:"none" so
      reasoning can be turned off.
    - StepFun falls back to inference so per-model effort support is honored
      (the static preset would have forced effort on step-3.5-flash too).
    
    Includes the Codex provider-form reasoning controls, i18n strings
    (zh/en/ja), and response-side reasoning extraction.
  • fix: inject stream_options.include_usage for Codex Chat streaming
    Responses-to-Chat conversion did not set stream_options.include_usage,
    so OpenAI-compatible upstreams (kimi/MiniMax, etc.) omitted the trailing
    usage chunk in streaming mode. This caused token/cost/cache stats to be
    recorded as zero for third-party streaming requests routed through the
    Codex Chat path.
    
    Inject include_usage when the request is streaming, merging into any
    client-provided stream_options instead of overwriting it. Add tests for
    the streaming, non-streaming, and merge cases.
  • fix: backfill placeholder reasoning_content for bare tool-call messages
    Thinking models like kimi/Moonshot and DeepSeek reject any assistant
    message carrying tool_calls without a non-empty reasoning_content. When
    cross-turn history recovery misses (proxy restart drops the in-memory
    cache, ambiguous call_id, or a turn produced no reasoning upstream), the
    converted assistant message has none and the whole request fails with
    'reasoning_content is missing in assistant tool call message'.
    
    Add a final-stage backfill that runs after all input items are
    processed, so genuine trailing reasoning items still attach first and a
    placeholder is only injected when none remains. Mirrors the placeholder
    behavior in transform::anthropic_to_openai_with_reasoning_content.
  • feat: convert Codex Chat error responses to Responses envelope
    The Codex Chat-to-Responses bridge already rewrites successful upstream
    responses to the Responses shape, but the error branch in
    handle_codex_chat_to_responses_transform passed Chat-shaped error bodies
    through untouched (MiniMax base_resp, raw OpenAI Chat error, text/plain
    "Unauthorized" pages, etc.), leaving Codex clients unable to recognize the
    error.
    
    Add chat_error_to_response_error in transform_codex_chat to regularize all
    upstream error shapes into the standard {error: {message, type, code, param}}
    envelope, then wire it through a new handle_codex_chat_error_response that
    preserves the original HTTP status code. Non-JSON error bodies (HTML, plain
    text) are wrapped as Value::String and truncated to 1KB at a UTF-8 char
    boundary to keep diagnostic context without flooding the response.
    
    Also fix a pre-existing append-vs-insert pitfall in three rebuilt-body
    branches (Claude transform, Codex Chat normal, Codex Chat error): http
    Builder::header is append, so leaking the upstream Content-Type produced
    two Content-Type headers when the rewritten body was JSON. Remove the
    upstream value before writing application/json.
  • feat: route Codex Chat providers through Stream Check
    Codex Chat providers (apiFormat=openai_chat, e.g. DeepSeek, MiniMax, Kimi)
    were incorrectly probed via /v1/responses by Stream Check, which the upstream
    rejects. Detect chat routing via codex_provider_uses_chat_completions and
    issue the probe against /chat/completions with a Chat-shaped body.
    
    Also align URL fallback order with the production CodexAdapter::build_url:
    origin-only base URLs (https://api.deepseek.com) must hit /v1/<endpoint>
    first; bare /<endpoint> only as a fallback. The previous order made any
    non-404 error on the bare path (401/400/405) flag the provider as down even
    though /v1/<endpoint> would have succeeded.
    
    - Lift origin-only detection into proxy::providers::is_origin_only_url so
      both build_url and Stream Check share a single source of truth.
    - Collapse resolve_codex_stream_urls and resolve_codex_chat_stream_urls into
      a generic resolve_codex_endpoint_urls(base_url, is_full_url, endpoint),
      which also gives the Responses path a symmetric "full endpoint without
      is_full_url flag" fallback for free.
    - Restrict reasoning_effort propagation in the Chat branch to OpenAI o-series
      models, mirroring transform_codex_chat's runtime check.
  • fix: collapse mid-stream system messages in Codex Responses to Chat conversion
    MiniMax's OpenAI-compatible chat endpoint strict-rejects any non-leading
    role=system message with "invalid params, chat content has invalid message
    role: system (2013)". The Codex client uses role=developer (and occasionally
    role=system) to inject collaboration_mode / permissions / skills blocks
    mid-conversation, and responses_role_to_chat_role maps both to chat's
    system role. The converted messages array therefore frequently contained
    system entries past index 0, which DeepSeek and OpenAI tolerate but MiniMax
    flags as 2013.
    
    Collapse all system messages into a single leading system message before
    returning the chat request. The rewrite preserves every system fragment
    (joined by "\n\n" in original order) and leaves non-system messages
    untouched, so it is lossless for permissive backends as well.
    
    - Add collapse_system_messages_to_head in transform_codex_chat.rs.
    - Run it on the messages vector at the end of responses_to_chat_completions
      before serializing.
    - Cover the new path with two unit tests: one repros the MiniMax-shaped
      input (developer items between users) and asserts no system role past
      index 0; the other verifies non-system order is preserved and content
      is joined with "\n\n".
  • fix: preserve Codex model catalog on live read of active provider
    Editing the currently active Codex provider triggered `read_live_settings`,
    which returned only { auth, config }, omitting `modelCatalog`. The form's
    mapping table then initialized to empty, and a subsequent save wiped the
    DB's `modelCatalog` field — silently destroying user-configured model
    mappings after every CC Switch restart.
    
    The mappings already live on disk as a projection in
    `~/.codex/cc-switch-model-catalog.json` (pointed at by `config.toml`'s
    `model_catalog_json`). Reverse-parse that file so live reads return the
    same shape the save path writes.
    
    - Add `read_codex_model_catalog_simplified_from_live` to recover
      `{ model, displayName?, contextWindow? }` entries from the catalog file.
    - Skip user-managed external catalogs (filename != cc-switch-model-catalog.json)
      so we don't downgrade their richer structure to the simplified table.
    - Squash display_name == slug and context_window == default (config's
      `model_context_window`, or 128_000 fallback) so blank inputs round-trip
      back to blank instead of materializing fallback values in the UI.
    - Collapse all failure modes (missing file, parse error, no matching
      field) to Ok(None) so the editor stays openable when the projection
      file is absent or corrupt.
    - Wire the new function into `read_live_settings`'s Codex branch.
    - Cover the new pure helpers with 7 unit tests in codex_config::tests.
  • feat: unify Codex third-party providers into stable "custom" history bucket
    Codex filters resume history by `model_provider`, so switching between
    provider-specific ids like `rightcode` and `aihubmix` made past sessions
    appear to vanish. Collapse all third-party providers into a single
    stable bucket so cross-switch history stays visible.
    
    - Normalize live `model_provider` to "custom" on every Codex write
      (reserved built-in ids like openai/ollama are preserved).
    - Add device-level one-shot migration that rewrites historical JSONL
      session files and the `state_5.sqlite` threads table from legacy
      provider ids into the "custom" bucket. Backs up originals under
      `~/.cc-switch/backups/codex-history-provider-migration-v1/` and uses
      the SQLite Backup API for the state DB.
    - Record completion in `settings.json` under `localMigrations` so the
      migration is strictly idempotent across launches.
    - Update Codex provider preset templates to emit `model_provider = "custom"`
      out of the box.
  • feat: preserve user-selected catalog model in Codex Chat requests
    When Codex client sends a model from the catalog (e.g., user selected via /model),
    preserve that choice instead of always replacing with config.toml's default model.
    
    - Check if request model exists in modelCatalog before falling back to config model
    - Remove CODEX_CHAT_CLIENT_MODEL constant (no longer needed)
    - Add test coverage for catalog model preservation
  • fix: Codex model catalog WYSIWYG and config consolidation
    - Remove mergeCodexDefaultCatalogModelForSave implicit injection (P1)
      The model mapping table is now the single source of truth; no hidden
      entries are prepended on save.
    
    - Sync first catalog row model into config.toml on save
      Ensures Codex default request model matches the table's first entry
      instead of retaining a stale template value.
    
    - Remove API Format selector from CodexFormFields (P3)
      wire_api is always 'responses'; the selector confused users into
      thinking they were changing the upstream protocol. Only the 'Needs
      Local Routing' toggle remains.
    
    - Add restart hint to model mapping i18n text (P2)
      model_catalog_json is loaded at Codex startup; users are now informed
      that a restart is needed after changes.
    
    - Unify write_codex_live_with_catalog helper (P4)
      Replaces three scattered prepare+write call sites in config.rs,
      provider/live.rs, and proxy.rs with a single entry point.
    
    - Clean up useCodexConfigState dead state (P3 follow-up)
      Remove codexModelName, codexContextWindow, codexAutoCompactLimit and
      their handlers/effects since no component consumes them after the UI
      consolidation.
  • - Restore Codex Chat reasoning fallback
    - Add unique call-id fallback when Codex omits or rewrites previous_response_id.
    
    - Keep ambiguous call ids unresolved to avoid cross-request history leaks.
    
    - Cover fallback and ambiguity behavior with regression tests.
  • - Optimize Codex Chat cache stability
    - Stop deriving Codex session/cache identity from `previous_response_id`.
      - Canonicalize parseable JSON string payloads in Chat and Responses tool conversions.
      - Add regression coverage for cache-sensitive conversion behavior.
  • Add Codex Chat-to-Responses bridge
    - Add Codex provider API format selection and model mapping for Chat-only upstreams.
    - Convert Codex Responses requests to Chat Completions and rebuild Chat responses as Responses output.
    - Preserve reasoning_content across non-streaming, streaming, tool calls, and previous_response_id follow-ups.
    - Add a bounded Codex Chat history cache for restoring tool calls before tool outputs.
    - Cover Chat bridge, compact routing, streaming, and history recovery with focused tests.
  • Handle inline thinking in Codex Chat conversion
    - Split leading <think> blocks from Chat content into Responses reasoning output.
    - Keep assistant text output free of inline thinking tags.
    - Support streamed inline thinking blocks before normal text deltas.
    - Add regression coverage for MiniMax-style inline thinking responses.
  • Improve Codex Chat reasoning conversion
    - Convert Chat reasoning_content and reasoning fields into Responses reasoning output items.
    - Emit Responses reasoning summary SSE events for streamed Chat reasoning deltas.
    - Preserve output ordering when reasoning, text, and tool calls are streamed together.
    - Add regression coverage for data-only Chat SSE errors, multiple tool calls, and compact routing.
  • Add Chat Completions routing for Codex providers
    - Add a Codex API format selector and routing badge for Chat Completions providers.
    - Convert Codex Responses requests to upstream Chat Completions when routing is required.
    - Convert Chat Completions JSON and SSE responses back to Responses format.
    - Keep generated Codex wire_api values on Responses for Codex compatibility.
    - Add i18n labels, provider metadata handling, and focused conversion tests.
  • fix(proxy): inject only ANTHROPIC_API_KEY for managed-account Claude takeover
    - Provider: add uses_managed_account_auth / is_github_copilot helpers
      to identify managed-account providers (GitHub Copilot / Codex OAuth)
    - ProxyService: choose auth policy by provider type when taking over
      Claude Live config. Managed accounts drop token env keys and write
      only the ANTHROPIC_API_KEY placeholder; other providers keep the
      existing ANTHROPIC_AUTH_TOKEN fallback behavior
    - Forwarder: add outbound guard that refuses to send the PROXY_MANAGED
      placeholder upstream to *.githubcopilot.com and chatgpt.com
      /backend-api/codex
    - Add unit tests covering detection, injection, and the outbound guard
  • fix(terminal): Ghostty opens clean window instead of cloning existing tabs (#2801)
    When Ghostty is already running, `open -a` silently ignores `--args`,
    and `open -na` clones all existing tabs into the new instance.
    
    Add a dedicated `launch_macos_ghostty` that uses
    `--quit-after-last-window-closed=true` and `-e bash <script>` to spawn
    a single clean window running claude.
    
    Also change `launch_macos_open_app` from `open -a` to `open -na` so
    other terminals (Alacritty/Kitty/WezTerm/Kaku) correctly open a new
    window when already running.
    
    Closes #2798
  • fix(gemini-native): resolve functionResponse.name and replay thought_signature for synthesized tool call IDs (#2814)
    * fix(gemini-native): resolve functionResponse.name and thought_signature replay for synthesized tool call IDs
    
    Two related bugs in the Gemini Native format conversion layer:
    
    1. **functionResponse.name resolution** (422 error): When Gemini's parallel
       function calls omit the id field, cc-switch synthesizes gemini_synth_*
       IDs. These are stored in the shadow store but can be lost in long sessions,
       causing subsequent tool_result blocks to fail. Fix: pre-scan all assistant
       messages in the request body to seed the tool_name_by_id map, and add a
       last-resort fallback that scans the current content array for matching
       tool_use blocks.
    
    2. **thought_signature replay** (400 error): The Anthropic Messages format
       strips thoughtSignature from tool_use blocks, but Gemini requires it on
       every functionCall in multi-turn tool-use exchanges. Fix: build a
       thought_signature_by_id map from shadow turns and attach thoughtSignature
       when converting tool_use back to functionCall.
    
    Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
    
    * style: run cargo fmt on transform_gemini.rs
    
    ---------
    
    Co-authored-by: Tiancrimson <tiancrimson@gmail.com>
    Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
    Co-authored-by: Jason <farion1231@gmail.com>
  • fix(session): 修复session log模式下子Agent token统计遗漏 (#2821)
    * fix(session): 修复session log模式下子Agent token统计遗漏
    
    collect_jsonl_files() 只扫描了两层目录,遗漏了子Agent的JSONL日志文件,
    导致子Agent的独立token使用数据完全未统计到session费用中。
    (仅影响session log模式,proxy代理模式不受影响)
    
    * refactor(session): optimize collect_jsonl_files logic
    
    - Replace two independent if statements with if-else for mutually exclusive conditions
    - Remove unnecessary clone() when pushing file paths
    - Add clarifying comments for main session vs subagent files
    - Apply cargo fmt for consistent formatting
    
    Performance improvement: Eliminates redundant clone() operations when
    processing .jsonl files, as a path cannot be both a file and a directory.
    
    ---------
    
    Co-authored-by: Jason <farion1231@gmail.com>
  • - Fix proxy test helper clippy warning
    - Mark `should_force_identity_encoding` as test-only.
    
    - Keep runtime forwarding behavior unchanged.
    
    - Verified with local CI checks and no-bundle Tauri build.
  • fix(failover): patch P1-P3 reliability gaps surfaced by team review
    - Forwarder buffers non-streaming bodies and primes streaming first
      chunk before signaling success, so body timeouts and SSE first-chunk
      failures route through the circuit breaker instead of being recorded
      as success on response-header arrival
    - Atomic enable-failover: switch to P1 before persisting the flag, and
      roll back auto-added queue entries when the switch is rejected
      (e.g. official providers)
    - Hot-reload circuit breaker config on per-app proxy config change
      instead of waiting for a proxy restart
    - FailoverToggle / FailoverQueueManager / AutoFailoverConfigPanel
      require proxy takeover for the active app; the backend command also
      rejects enabling when takeover is off
    - ProviderHealthBadge consumes the backend is_healthy flag instead of
      hardcoding the 5-failure threshold
    
    Cleanup:
    - impl From<&AppProxyConfig> for CircuitBreakerConfig and use it from
      the command layer
    - Collapse three identical TabsContent blocks into a single map
  • feat(codex-oauth): fetch model list from ChatGPT backend on demand
    - Add `get_codex_oauth_models` Tauri command reusing the managed OAuth
      access token to hit `chatgpt.com/backend-api/codex/models`; HTTP and
      multi-shape JSON parsing live in `services::codex_oauth_models` so the
      command stays thin.
    - Unify the Claude form's "fetch models" button across normal / Copilot /
      Codex OAuth presets, drop the auto-load effect for Copilot in favor of
      explicit clicks, and guard against stale responses with a requestId ref.
    - Add Vitest coverage for both Copilot and Codex OAuth paths asserting no
      request on mount and the correct account id on click; add Rust unit
      tests for the four model-list payload shapes.
  • fix(proxy): expose real provider model names in Claude Code menu under takeover
    When proxy takeover is active, write per-role *_MODEL aliases for routing
    and *_MODEL_NAME with the upstream provider's real model name so the
    Claude Code model menu reflects the active provider instead of stale
    display names from a previous switch. Preserves the [1M] capability marker
    for Sonnet/Opus, and strips it from implicit display names.
  • fix(usage): pricing routing, SSE lifecycle, and validation hardening
    * model pricing routing: extend prefix-match families (gpt-/o1-o5/
      gemini-/deepseek-/qwen-/glm-/kimi-/minimax-) with per-family dash
      thresholds so short base IDs like gpt-5 no longer mis-match
      gpt-5-mini; strip ISO and 8-digit date suffixes via UTF-8-safe
      byte matching so claude-haiku-4-5-20251001 falls back to
      claude-haiku-4-5 pricing
    * SSE collector: SseUsageFinishGuard (RAII) guarantees finish() on
      early return or panic; AtomicBool fast path lets push() skip the
      Mutex once first-event time is recorded
    * validation: shared validate_cost_multiplier / validate_pricing_source
      helpers across DAO and service layers; PRICING_SOURCE_RESPONSE /
      PRICING_SOURCE_REQUEST constants replace string literals; price
      fields in update_model_pricing now reject empty / non-decimal /
      negative input before INSERT
    * backfill: add backfill_missing_usage_costs_for_model so a single
      price edit only scans matching rows instead of the full log table;
      startup backfill remains full-scan
    * session_usage{,_codex,_gemini}: share find_model_pricing helper from
      usage_stats; metadata_modified_nanos centralizes mtime precision
    * frontend: NON_NEGATIVE_DECIMAL_REGEX + isNonNegativeDecimalString
      replace three copies of the same multiplier regex; isUnpricedUsage
      surfaces zero-cost rows that have usage tokens (cached per row to
      avoid double evaluation); invalidate usageKeys.all on pricing mutate
      so backfilled rows refresh
  • fix(proxy): patch P0-P3 routing/lifecycle issues across forwarder paths
    * stream_check: thread Result from get_auth_headers via map_err so
      the workspace builds again
    * forwarder: scope rectifier / budget-rectifier flags per-provider so
      failover can still apply rectification on the next attempt
    * forwarder: categorize before record_result; route NonRetryable and
      ClientAbort through release_permit_neutral so client-side failures
      don't pollute circuit breaker or DB health
    * handler_context: parse Gemini model from uri.path() and strip both
      ?query and :action verb defensively in extract_gemini_model_from_path
    * forwarder + response_processor + handlers: introduce
      ActiveConnectionGuard (RAII) so active_connections decrement covers
      the full streaming body lifetime, not just response headers
    * claude_desktop_config: use sort_by_key to clear the clippy gate
  • refactor(proxy): extract handle_rectifier_retry_failure helper
    The signature (RECT-003) and budget (RECT-012) rectifier branches each
    carried ~50 lines of identical "provider error -> record + continue /
    client error -> release permit + return" handling. The only piece that
    varied between them was a log label ("整流" vs "budget 整流").
    
    Move the shared logic into RequestForwarder::handle_rectifier_retry_failure
    that returns Option<ForwardError> — None means "continue to the next
    provider", Some(err) means "terminal failure, return to the client".
    Each call site shrinks from ~50 lines to ~17, drops one level of
    indentation, and the two branches now provably cannot drift apart.
    
    forwarder.rs nets ~40 lines smaller.
  • refactor(proxy): share auth_header_value helper across provider adapters
    claude.rs and gemini.rs each defined an identical `hv` closure that wrapped
    `HeaderValue::from_str` into a ProxyError::AuthError result, and codex.rs
    spelled the same conversion out inline. /simplify reviewers flagged this
    as drift-prone copy-paste.
    
    Move the conversion into a single `pub fn auth_header_value` in
    providers/adapter.rs and have the three adapters import it locally. Same
    error wording everywhere, one place to update if HeaderValue semantics
    ever change.
  • feat(proxy): forward client HTTP method instead of hard-coding POST
    The forwarder used to call client.post(&url) / http::Method::POST in
    both the reqwest and hyper paths, and the Gemini route table only
    registered POST /v1beta/*. As a result anything the Gemini SDK / CLI
    sent as GET (models list, models/<id> info) hit a 404 at the router
    and bypassed the local proxy's stats, rectifiers, and failover.
    
    Thread the request method end-to-end:
    
    - ProviderAdapter forwarder API now takes the http::Method by reference
      per attempt and dispatches client.request(method, &url) for reqwest
      and method.clone() for the hyper raw path.
    - All five callers in handlers.rs (handle_messages_for_app for Claude /
      Claude Desktop, handle_chat_completions, handle_responses,
      handle_responses_compact, handle_gemini) pull the method out of the
      incoming axum::extract::Request and pass it on.
    - handle_gemini tolerates an empty body (GET endpoints have none) and
      the forwarder skips serializing / sending a body for GET / HEAD —
      attaching JSON to a GET makes Gemini reject the request.
    - server.rs swaps the Gemini routes to any(handle_gemini) so the same
      handler handles GET / POST / PUT / DELETE, and adds /gemini/v1/*
      for the GA path version.
  • fix(proxy): move client-request counters out of per-attempt loop
    Three statistics-shape issues fixed together so the dashboard reflects
    client requests, not provider attempts:
    
    1. active_connections never moved off zero — the field had no caller in
       the entire crate. Wrap forward_with_retry into a thin entry point
       that saturating_add(1) on enter and saturating_sub(1) on exit; every
       inner return path is covered automatically.
    
    2. total_requests counted attempts, not requests. A single client call
       that failed over P1 -> P2 -> success was recorded as
       total=2 / success=1 -> 50% success rate. Move the increment and the
       last_request_at refresh into the wrapper so they fire once per
       client request regardless of how many providers were tried.
    
    3. current_provider / current_provider_id stay inside the inner loop
       because they are intentionally per-attempt ("what am I trying right
       now?") — moving them would break the live-failover indicator.
    
    Refactor: split forward_with_retry into a public wrapper + private
    forward_with_retry_inner. Every existing `return Err(...)` inside inner
    remains correct because the wrapper always runs the decrement on its
    return.
  • fix(proxy): wire AppProxyConfig.max_retries into request forwarder
    The UI has exposed "请求失败时的重试次数 (0-10, default 3)" since the
    auto-failover panel was added, but the value was silently dropped —
    RequestForwarder never received it and the per-provider loop walked the
    whole list regardless. From the user's perspective the setting was
    inert.
    
    Thread AppProxyConfig.max_retries through create_forwarder into
    RequestForwarder, derive max_attempts = max_retries + 1 (so max_retries=0
    matches the UI copy "0 retries" = single attempt), and break the loop
    once attempts hit the cap. The check is placed before the circuit
    breaker allow-permit so an over-cap iteration does not waste a HalfOpen
    probe slot.
    
    When auto-failover is disabled we also force max_retries to 0, mirroring
    how timeouts already bypass in that mode — "no failover" should mean
    "one provider, one try", not "limited retries against the same list".
  • fix(proxy): map Anthropic tool_choice to OpenAI Chat nested form
    The Chat-Completions transformer used to forward tool_choice verbatim,
    but the two APIs disagree on shape:
    
      Anthropic   "any" | {"type":"tool","name":"X"}
      OpenAI Chat "required" | {"type":"function","function":{"name":"X"}}
    
    Pass-through made the upstream return 400 for any tool-forcing client
    (Claude Code, Copilot, etc.). The Responses-API transformer already had
    the equivalent map_tool_choice_to_responses helper; this commit adds a
    sibling map_tool_choice_to_chat with the chat-specific *nested* function
    selector and five regression tests covering string / object × any /
    auto / none / tool.
    
    The two helpers are intentionally not merged: the difference between
    flat and nested function selectors is exactly what the original bug
    was, so keeping them as separate self-documenting functions reduces the
    chance of the same regression returning.
  • fix(proxy): refine failover decisions in forwarder
    Two related changes to make per-provider failover behave correctly.
    
    1. Bucket UpstreamError by status code in categorize_proxy_error.
    
       The old "every UpstreamError is Retryable" rule meant a malformed
       client request (400 / 422) would be replayed against every provider
       in the queue: errors amplified N-fold, the circuit breaker accrued
       unwarranted failure counts, and quota was burned. Now
       400 / 405 / 406 / 413 / 414 / 415 / 422 / 501 are NonRetryable since
       the request itself is wrong and no provider will accept it.
       401 / 403 / 404 / 408 / 409 / 429 / 451 and all 5xx remain Retryable
       because the next provider may carry a different key, quota, region,
       or model mapping.
    
    2. Make the rectifier-retry path participate in failover.
    
       Both the signature (RECT-003) and budget (RECT-012) rectifier branches
       used to "return Err(...)" after the retry failed, short-circuiting the
       per-provider loop. A provider-side failure (5xx / Timeout /
       ForwardFailed) now records the circuit breaker, accumulates into
       last_error / last_provider, and "continue"s to the next provider —
       matching the normal Retryable arm. Client-side failures still return
       immediately since a different provider cannot fix a malformed payload.
  • fix(proxy): tighten takeover detection and use fallback restore on disable
    Two related drift bugs in the takeover state machine:
    
    1. The "already taken over?" guard used has_backup OR live_taken_over, so
       either condition alone would short-circuit. After a user or anomalous
       flow restores Live manually the backup row still made set_takeover
       return success, leaving the UI claiming takeover while requests bypass
       the local proxy. Tighten to AND so the rebuild branch repairs the two
       "split brain" states (backup-only and placeholder-only).
    
    2. Disabling takeover called the bare restore_live_config_for_app, which
       silently Ok()s when the backup is missing. If the backup was lost while
       Live still held proxy placeholders (PROXY_MANAGED token / local proxy
       URL), the client config was left broken with no error surfaced. Route
       the disable path through the already-existing
       restore_live_config_for_app_with_fallback (backup → SSOT → cleanup).
       The line 354 takeover-failure rollback intentionally keeps the bare
       variant since that path must preserve the backup for retry.
  • fix(proxy): extract Gemini request model from URI path correctly
    split('/') strips the slashes, so find(|s| s.starts_with("models/")) never
    matched any segment and request_model fell through to "unknown" for every
    Gemini call, poisoning usage records, per-request billing, and logs.
    
    Match the literal "models" segment and take the next one, stripping any
    :action suffix and query string. The extraction is now a pub(crate) free
    function so it can be unit-tested directly; seven regression tests cover
    action suffixes, dotted versions, the /gemini/ proxy prefix, query
    strings, the bare list endpoint, and missing-segment paths.
  • fix(proxy): return Result from get_auth_headers to avoid panic on bad credentials
    User-pasted API keys can contain control chars or CR/LF that make
    HeaderValue::from_str return Err; the previous unwrap inside every
    adapter turned such input into a process-wide panic instead of a request
    error. The trait now returns Result<_, ProxyError>; Claude/Codex/Gemini
    impls propagate ProxyError::AuthError so the client sees a 401 with the
    underlying parse error instead of a crash. Adds a regression test that
    pastes a CRLF-containing key and asserts AuthError.
  • chore: drop trailing blank line in sql_helpers tests
    Rustfmt cleanup, no behavioral change.
  • fix(usage): correct cache cost semantics and silence pricing warn storm
    - Split CostCalculator into per-app cache semantics: Anthropic's
      input_tokens is already fresh input, while Codex/Gemini include
      cached tokens in their prompt count. The old shared formula
      double-subtracted cache_read for Claude, under-billing input cost.
    - Backfill now reads cost_multiplier from the per-log snapshot column
      instead of re-querying providers.meta, so historical rows are no
      longer rewritten with the current multiplier.
    - Move the "pricing not found" warn out of find_model_pricing_row;
      emit it only when a brand new log is written, and skip placeholder
      models (unknown / empty / null / none) entirely.
    - Broaden model id normalization: strip namespace prefixes
      (anthropic./openai./global./bedrock.), bedrock-style -vN suffixes,
      reasoning effort suffixes (-low/-medium/-high/-xhigh/-minimal),
      Claude Desktop's claude-<non-anthropic> wrapper, dot-to-dash for
      Claude, and try a LIKE prefix match for Claude short route ids
      (e.g. claude-haiku-4-5 -> claude-haiku-4-5-20251001).
    - Fall back to request_model when the stored model is missing, so
      early Codex session rows with model=unknown can still be priced.
  • feat(claude-code): role-based model mapping with display names and 1M flag
    - Replace the four flat env inputs with a Sonnet/Opus/Haiku role table.
      Each row exposes ANTHROPIC_DEFAULT_*_MODEL plus a new display name
      field ANTHROPIC_DEFAULT_*_MODEL_NAME, and Sonnet/Opus gain a
      "Declare 1M" checkbox that toggles the [1M] suffix.
    - Strip the [1M] context-capability marker before forwarding non-Copilot
      requests upstream. Copilot keeps its existing [1m]->-1m normalization.
    - Claude Desktop import now consumes ANTHROPIC_DEFAULT_*_MODEL_NAME as
      label_override, closing the Claude Code -> Claude Desktop displayName
      pipeline; add_route's merge logic is shared between hashmap branches.
    - Unify the [1M] marker as ONE_M_CONTEXT_MARKER across
      claude_desktop_config and proxy::model_mapper; rename the strip
      helper to strip_one_m_suffix_for_upstream.
    - Collapse useModelState's seven duplicated useState initializers and
      the useEffect parse block into a single parseModelsFromConfig call.
    - Add tests/hooks/useModelState.test.tsx and a Claude Desktop import
      test covering Kimi K2 -> label_override. i18n (en/ja/zh) updated.