Commit Graph

680 Commits

  • 修复 Codex 切换供应商后历史记录变化 (#2349)
    * Keep Codex history stable across provider switches
    
    * Restore template Codex provider id when backfilling live config
    
    Backfill writes the current Codex live config back to the previous
    provider's stored template after a switch. Because the live file now
    carries a normalized stable model_provider id, the previous provider's
    template would lose its own provider-specific id (and any matching
    [profiles.*] references) on every subsequent switch.
    
    Reverse the normalization at backfill time by rewriting model_provider,
    the active model_providers section, and matching profile references back
    to the template's original id.
    
    ---------
    
    Co-authored-by: Jason <farion1231@gmail.com>
  • feat(usage): add Hermes Agent tracking + fix zero-cost bug + perf
    Hermes:
    - Parse ~/.hermes/state.db sessions (incl. profiles/*/state.db) into
      proxy_request_logs with data_source='hermes_session', WAL-aware
      incremental sync, Hermes-reported cost preferred over model_pricing
      fallback
    
    Zero-cost bug (dashboard showed \$0 totals):
    - GPT-5.5 family default pricing (~83% of affected rows used GPT-5.5)
    - find_model_pricing_row: ASCII-lowercase normalization so
      "OpenAI/GPT-5.5@HIGH" matches seeded "gpt-5.5"
    - Startup cost backfill in async task: scan rows where total_cost <= 0
      but tokens > 0, recompute via model_pricing in a single transaction
    
    Performance:
    - Add (app_type, created_at DESC) covering index for dashboard range
      queries
    - Add expression index on COALESCE(data_source, 'proxy') so dedup EXISTS
      subqueries use index lookup instead of full scan; drop superseded
      idx_request_logs_dedup_lookup
    
    Refactor:
    - row_to_request_log_detail helper (3-way de-dup; fixes cost_multiplier
      \"1\" vs \"1.0\" drift between callers)
    - Promote get_sync_state/update_sync_state to shared session_usage
      module (4 copies -> 1)
    - run_step helper in lib.rs replaces 9 if-let-Err blocks
    - maybe_backfill_log_costs returns bool to skip duplicate total_cost
      parsing in caller
  • fix(usage): prevent double-counting between proxy and session-log sources
    Proxy writes and session-log sync wrote to proxy_request_logs with
    mismatched request_ids: only Claude on a native Anthropic backend used the
    shared `session:{message_id}` key. Codex/Gemini and Claude-through-OpenAI
    providers always produced distinct ids, so primary-key dedup never fired
    and every real request was recorded twice.
    
    Adds a 7-dim fingerprint dedup (app_type, 4 token counts, 2xx status,
    model with case-insensitive match, ±10min window) wired into three layers:
    
    - Write path: should_skip_session_insert() blocks duplicate session rows
      before INSERT, unifying the previously-divergent Claude/Codex/Gemini
      paths through a single DedupKey-based helper.
    - Read path: effective_usage_log_filter() excludes already-covered session
      rows from every aggregation query.
    - Rollup path: same filter applied so usage_daily_rollups never absorbs
      duplicates.
    
    Also adds a covering index (idx_request_logs_dedup_lookup) so the EXISTS
    subquery stays index-only, and a transform.rs regression test that pins
    openai_to_anthropic id preservation - the missing piece that lets
    Claude+OpenAI-compatible providers reuse the session: id scheme.
  • fix(balance): show USD on SiliconFlow international site (was CNY)
    The query_siliconflow function received an is_cn flag that only switched
    the request domain (.cn vs .com) but the response builder hardcoded
    unit="CNY" for both sites. International users at api.siliconflow.com
    saw their USD balance labelled as CNY. Now unit and plan_name follow
    is_cn, so the EN site shows USD and "SiliconFlow (EN)".
  • fix(proxy): preserve scoped reasoning_content for tool calls (#2367)
    - Preserve `reasoning_content` for Kimi/Moonshot OpenAI Chat compatibility paths.
    - Keep generic OpenAI-compatible requests free of non-standard `reasoning_content` fields.
    - Continue skipping thinking-only assistant messages.
    - Add regressions for generic skip and Kimi/Moonshot preservation behavior.
  • fix(proxy): dedupe streaming message_delta (#2366)
    - Deduplicate repeated upstream `finish_reason` chunks so only one Anthropic `message_delta` is emitted.
    - Preserve late `choices: []` usage-only chunks before sending the final `message_delta`.
    - Keep stream error paths from emitting successful terminal events.
    - Add regressions for duplicate finish reasons, usage-only chunks, missing `[DONE]`, and truncated streams.
  • feat(provider-form): soften validation with "save anyway" prompt (#2307)
    * feat(provider-form): soften business-rule validation with "save anyway" prompt
    
    Refactor handleSubmit so empty-field / missing-item validations (provider
    name, endpoint, API key, opencode model, template variables, provider key
    required) no longer hard-reject with toast.error. Instead they are collected
    into an issues list and presented via a ConfirmDialog; the user can cancel
    or choose "Save anyway" to proceed.
    
    Integrity constraints stay as hard rejections:
    - providerKey regex / duplicate (would corrupt other providers)
    - Copilot / Codex OAuth not authenticated (no token, cannot establish)
    - omo Other Fields JSON not an object / parse failure
    
    This aligns the frontend with the backend's existing "relaxed save / strict
    switch" split (see gemini_config.rs: validate_gemini_settings vs
    validate_gemini_settings_strict) and unblocks legitimate configs such as
    AWS Bedrock, Vertex AI, and custom Gemini base URLs that the UI previously
    refused to save.
    
    Refs: #2196, #1204
    
    * fix(provider-form): address review feedback on soft-validation
    
    P1: move empty providerKey back to hard rejection for OpenCode / OpenClaw /
    Hermes. Since providerKey is the primary identity for these apps and the
    mutations layer throws "Provider key is required" when absent, letting users
    click "save anyway" would surface a generic error toast instead of a
    precise, actionable one. Treat empty providerKey as an integrity constraint
    alongside regex / duplicate checks.
    
    P2: give the soft-confirm submit path its own submitting state. The
    confirm-dialog path bypassed react-hook-form's isSubmitting lifecycle, so
    slow or failing saves left the outer submit button responsive and could
    spawn unhandled rejections. Now the confirm handler awaits performSubmit
    inside try/catch/finally, uses an isConfirmSubmitting flag to gate both
    confirm and cancel clicks, and folds the flag into the outer disabled
    state and onSubmittingChange callback.
    
    Refs: #2307 review comments
    
    * chore(clippy): use push for single char '…' in truncate_body
    
    Clippy 1.95 added single_char_add_str which flagged the push_str("…")
    in truncate_body. Rebased onto latest upstream/main and applied the
    suggested fix so the Backend Checks clippy job passes.
    
    Unrelated to this PR's core changes; bundled in so the PR is mergeable
    without waiting for a separate upstream fix.
    
    ---------
    
    Co-authored-by: Allen <allen@AllenMacBook-M4-Pro.local>
  • feat(deepseek): switch presets to V4 (flash/pro) and add pricing
    DeepSeek released V4 flash/pro; legacy IDs deepseek-chat / deepseek-reasoner
    now alias to deepseek-v4-flash and will be deprecated.
    
    - Update claude/hermes/opencode/openclaw presets to v4-pro / v4-flash,
      context 128K -> 1M; Claude Anthropic-compat endpoint routes OPUS/SONNET
      to v4-pro and HAIKU to v4-flash, plus an explicit modelsUrl override.
    - Seed deepseek-v4-flash ($0.14/$0.28 per 1M) and deepseek-v4-pro
      ($1.68/$3.36 per 1M) into model_pricing; older v3.x / chat / reasoner
      rows kept for historical usage stats (INSERT OR IGNORE).
    - Refresh user-manual (zh/en/ja) pricing table and note that legacy model
      IDs are billed at v4-flash rates.
  • fix(model-fetch): support /models for Anthropic-compat subpath providers
    Providers like DeepSeek, Kimi, Zhipu GLM and MiniMax expose the
    Anthropic-compatible API on a subpath (e.g. /anthropic) while the
    OpenAI-style /models endpoint lives at the API root. The previous
    heuristic blindly appended /v1/models to the Base URL, so every such
    provider returned 404 and the UI mislabeled it as "provider does not
    support fetching models".
    
    Backend now generates a candidate list and tries them in order:
    preset override -> baseURL /v1/models -> stripped-subpath /v1/models ->
    stripped-subpath /models. Non-404/405 responses (auth, network) stop
    immediately so we never retry against hostile status codes. Known
    compat suffixes are kept in a length-descending constant so the
    longest match wins; response bodies are truncated to 512 chars to
    avoid HTML 404 pages bloating the error string.
    
    Preset type gains an optional modelsUrl (DeepSeek points at
    https://api.deepseek.com/models). Frontend threads the override
    through fetchModelsForConfig when the current Base URL still matches
    the preset default. A new fetchModelsEndpointNotFound i18n key
    replaces the misleading "not supported" toast for exhausted-candidate
    and 404/405 cases (zh/en/ja).
  • fix(copilot): resolve Claude model IDs against live /models list
    Copilot upstream returns model_not_supported when the client sends
    dash-form Claude IDs (claude-sonnet-4-6, claude-sonnet-4-6[1m]) while
    /models only accepts dot form (claude-sonnet-4.6, -1m suffix).
    
    - Add copilot_model_map: syntax normalize (dash->dot, [1m]->-1m) plus
      live /models exact match and family-version fallback, reusing the
      existing 5 min auth cache. Returns None when the whole family is
      absent so upstream surfaces an explicit error instead of silently
      switching families.
    - Wire into forwarder Copilot hook; runs before anthropic_to_openai
      conversion.
    - Default Opus slot in the Copilot preset maps to Sonnet 4.6: Pro
      dropped all Opus on 2026-04-20 and Pro+ bills Opus 4.7 at 7.5x.
      Users who want real Opus can switch manually in the UI.
    
    Refs: https://github.com/farion1231/cc-switch/issues/2016
  • chore(release): bump version to 3.14.1
    - Add v3.14.1 release notes (en/zh/ja) covering tray usage visibility,
      Codex OAuth stability fixes, Skills import/install reliability, and
      removal of the Hermes config health scanner
    - Cut [Unreleased] into [3.14.1] in CHANGELOG with PR references
    - Bump version in package.json, Cargo.toml, Cargo.lock, tauri.conf.json
  • feat(tray): show coding-plan usage for Kimi / Zhipu / MiniMax
    dc04165f surfaced tray usage badges for Claude/Codex/Gemini official
    OAuth only. Chinese coding-plan providers already expose 5h + weekly
    windows through coding_plan::get_coding_plan_quota, but two gaps kept
    the tray from rendering them.
    
    - format_script_summary read only data.first(), truncating the tier-
      flattened UsageResult to a single window. Detect plan_name matching
      TIER_FIVE_HOUR / TIER_WEEKLY_LIMIT and emit the "🟢 h12% w80%" layout
      used by format_subscription_summary; worst utilization drives the
      emoji. Copilot / balance / custom scripts keep the legacy single-
      bucket output via fallback.
    
    - usage_script previously required manual activation through
      UsageScriptModal. Auto-inject meta.usage_script on Claude provider
      creation when ANTHROPIC_BASE_URL matches a known coding plan, so the
      tray lights up without the user opening the modal. Does not overwrite
      existing usage_script on update.
    
    Extract the URL route table out of UsageScriptModal into a shared
    codingPlanProviders module so the modal, the creation hook, and the
    Rust coding_plan::detect_provider mirror all agree on one list.
    Add TIER_WEEKLY_LIMIT alongside TIER_FIVE_HOUR and a createUsageScript()
    factory to collapse the duplicated default fields across four call
    sites and drop the remaining stringly-typed tier names.
  • refactor(hermes): drop config health check scanner
    The Hermes config.yaml schema has stabilized and users have migrated to
    the current provider fields, so the value of scanning for model.provider
    dangling references, custom_providers shape errors, v12 migration residue
    etc. no longer justifies the maintenance surface — and the scan produces
    false positives when users keep some providers under Hermes' v12+
    providers: dict (Hermes' runtime merges both shapes, but CC Switch's
    scanner only looked at the list form).
    
    Removes the whole HermesHealthWarning type, scan_hermes_config_health
    command, HermesHealthBanner React component, useHermesHealth hook,
    warnings field on HermesWriteOutcome, and the three helper functions
    (yaml_as_non_empty_str, collect_mapping_string_keys, hermes_warning)
    that only served the scanner. Drops the matching i18n keys in
    zh/en/ja and the fixInWebUI button label that only the banner used.
  • feat(tray): show cached provider usage in the system tray menu (#2184)
    * feat: add Rust-side write-through usage cache
    
    Introduce an in-memory UsageCache on AppState that the existing usage
    query commands populate on success. The cache is read-only to the rest
    of the app today; the next commit consumes it from the tray menu.
    
    - New services::usage_cache module with split maps: subscription keyed
      by AppType, script keyed by (AppType, provider_id).
    - AppType gains Eq + Hash so it can be used as a HashMap key.
    - commands::subscription::get_subscription_quota now takes State<AppState>
      and writes through on success (signature change is invisible to the
      frontend — Tauri injects State automatically).
    - commands::provider::queryProviderUsage body extracted into an inner
      async fn; the public command wraps it with write-through, covering
      Copilot, coding-plan, balance, and generic script paths uniformly.
    
    Cache is in-memory only; auto-query interval and the upcoming tray
    refresh action rebuild it after restarts.
    
    * feat(tray): surface cached usage in the system tray menu
    
    Read UsageCache populated by the previous commit and render it in three
    places, scoped to whatever TRAY_SECTIONS covers (Claude/Codex/Gemini):
    
    1. Inline suffix on each provider submenu item
       "AnyProvider  · 🟢 5h 18% / 7d 23%"
    2. Disabled summary row per visible app under "Show Main"
       "Claude · Anthropic Official · 🟢 5h 18% / 7d 23%"
    3. "Refresh all usage" menu item that triggers get_subscription_quota +
       queryProviderUsage for every applicable provider, then rebuilds the
       tray menu via the existing refresh_tray_menu path.
    
    Color encoding uses emoji (🟢 <70% / 🟠 70-89% / 🔴 ≥90%) since Tauri 2
    tray labels are plain text. Missing cache entry leaves the label
    unchanged — tray never issues network requests when opened. Three new
    i18n-ready strings live in TrayTexts (en/zh/ja), following the existing
    pattern for tray text.
    
    Closes #2178.
    
    * feat(usage): bridge tray UsageCache writes to frontend React Query
    
    Why: tray hover triggers backend-only refresh that wrote to UsageCache but
    never notified the frontend, leaving main UI stale while tray showed fresh
    numbers. Emit a payload-carrying event after each cache write so React Query
    can setQueryData directly, keeping both views in sync without duplicate fetches.
    
    * fix(tray): skip hidden apps on hover refresh and drop stale disabled-script cache
    
    Address P2 findings from automated review on #2184:
    
    1. refresh_all_usage_in_tray now filters TRAY_SECTIONS by settings.visible_apps
       before scheduling subscription/script queries, matching create_tray_menu and
       preventing wasted external API calls (and rate-limit/auth-error log noise)
       for apps the user has hidden.
    
    2. format_usage_suffix only trusts the script cache when provider.meta.usage_script
       is still enabled; when a script is disabled/removed the cached suffix is now
       invalidated so the tray label no longer shows stale data indefinitely.
    
    * refactor: consolidate codex provider helpers and fix test semantics
    
    - Add Provider::is_codex_oauth() and Provider::codex_fast_mode_enabled()
      to eliminate duplicated meta extraction in claude.rs and stream_check.rs
    - Fix non-codex-oauth tests to pass codex_fast_mode=false (was true, harmless
      but semantically misleading)
    - Remove redundant is_dir() guard after resolve_skill_source_dir already
      guarantees the returned path is a directory
    
    * style: apply cargo fmt
    
    * fix(tray): reflect failed refreshes in cache and support Gemini flash-lite
    
    Follow-up to the tray usage-display feature addressing review feedback:
    
    - Write snapshots for both Ok(success:false) and Err paths in
      queryProviderUsage / get_subscription_quota so stale success data
      no longer persists across failed refreshes; the original Err is
      still returned to the frontend onError handler.
    - Include gemini_flash_lite tier in the tray summary with label "l".
      Matches the frontend SubscriptionQuotaFooter and keeps the worst
      emoji correct when lite is the highest utilization.
    - Add TIER_GEMINI_PRO / _FLASH / _FLASH_LITE constants in
      services/subscription.rs and reuse them in classify_gemini_model
      and sort_order.
    - Extract Provider::has_usage_script_enabled() to remove the
      duplicated meta.usage_script chain at two call sites.
    - Use db.get_provider_by_id in refresh_all_usage_in_tray instead of
      materialising the full provider map, and parallelise subscription
      and script futures via futures::future::join.
    - Narrow refresh_all_usage_in_tray to each section's effective
      current provider (script if enabled, else subscription when the
      provider is official). Hover refreshes now issue at most
      TRAY_SECTIONS.len() outbound requests.
    - Add 10 unit tests in tray::tests covering Claude/Codex h/w dispatch,
      Gemini p/f/l dispatch (including lite-only and lite-worst cases),
      and success/failure guards.
    
    ---------
    
    Co-authored-by: Jason <farion1231@gmail.com>
  • Fix/一键配置失效 (#2249)
    * style(FailoverQueueManager): 显示供应商备注信息
    
    * style(FailoverQueueItem): 添加供应商备注字段以支持备注信息显示
    
    * style(FailoverQueueManager): 显示供应商备注信息
    
    * style(FailoverQueueItem): 添加供应商备注字段以支持备注信息显示
    
    * style(FailoverQueueManager): 更新供应商备注信息的显示样式
    
    * style(FailoverQueueItem): 添加条件序列化以优化供应商备注字段
    
    * fix: 优化模型状态管理,确保配置更新时正确引用最新设置
    
    * fix(skill): improve error handling for skill source directory resolution
    
    Co-authored-by: Copilot <copilot@github.com>
    
    * fix(gemini): simplify project directory retrieval in scan_sessions function
    
    * fix(useModelState): optimize latestConfigRef assignment in useModelState hook
    
    * fix(useModelState): remove unnecessary blank line in useModelState hook
    
    ---------
    
    Co-authored-by: Copilot <copilot@github.com>
  • feat: Add Codex OAuth FAST mode toggle (#2210)
    * Add Codex OAuth FAST mode toggle
    
    * fix(codex-oauth): default FAST mode to off to avoid surprise quota burn
    
    service_tier="priority" consumes ChatGPT subscription quota at a higher
    rate. Users must now opt in explicitly rather than inherit FAST mode
    silently when this feature ships.
    
    ---------
    
    Co-authored-by: Jason <farion1231@gmail.com>
  • [codex] Stabilize Codex OAuth cache routing (#2218)
    * Stabilize Codex OAuth cache routing
    
    Codex OAuth-backed Claude proxy requests now reuse a client-provided session identity for prompt cache routing and send Codex-like session headers when that identity exists. Generated proxy UUIDs are intentionally excluded so they do not fragment cache locality.\n\nThe same path exposed two runtime issues during validation: rustls needed an explicit process crypto provider, and Codex OAuth can return Responses SSE even when the original Claude request is non-streaming. Those are handled so cache-routed requests can complete instead of panicking or being parsed as JSON.\n\nConstraint: Official Codex uses conversation identity and Responses session headers for prompt cache routing.\nRejected: Always use generated proxy session IDs | generated IDs change per request and reduce cache reuse.\nConfidence: medium\nScope-risk: moderate\nDirective: Do not remove the client-provided-session guard unless generated session IDs become stable per conversation.\nTested: cargo test codex_oauth\nTested: Local dev app health check on 127.0.0.1:15721\nTested: Local proxy logs showed cache_read_tokens after restart\nNot-tested: Full cargo test without local cc-switch port conflict\nRelated: #2217
    
    * feat(proxy): aggregate forced Codex OAuth SSE into JSON for non-streaming clients
    
    Narrow override on top of #2235's streaming fallback.
    
    Codex OAuth always forces upstream openai_responses into SSE, even
    when the original Claude request is stream:false. #2235 handles this
    by routing such responses through the streaming transform so the
    client receives text/event-stream — that avoids the 422 that JSON
    parsing would produce, and it also protects any other provider that
    unexpectedly returns SSE (the response.is_sse() guard).
    
    But for Claude SDK callers that sent stream:false, returning SSE
    still violates the Anthropic non-streaming contract. This commit
    adds an override on exactly one combination — non-streaming client
    + codex_oauth + openai_responses — to aggregate the upstream
    Responses SSE into a synthetic Responses JSON and then run the
    regular responses_to_anthropic non-streaming transform. All other
    paths, including the generic response.is_sse() fallback, remain
    on the streaming path from #2235.
    
    The aggregator reuses proxy::sse::take_sse_block / strip_sse_field,
    which support both \n\n and \r\n\r\n delimiters; a hand-rolled
    split("\n\n") would silently fail on real HTTPS upstreams.
    
    Tests cover the happy path, CRLF delimiters, response.failed
    errors, and the missing response.completed defensive branch.
    
    ---------
    
    Co-authored-by: Jason <farion1231@gmail.com>
  • fix: use TOML parser instead of regex for Codex model extraction (#2222) (#2227)
    * fix(codex): use TOML parser instead of regex for model extraction
    
    Regex only matched model=... on first line, TOML parser handles
    multiline TOML correctly.
    
    Fixes #2222
    
    * fix(stream_check): drop unused regex::Regex import
    
    The previous commit replaced the only Regex usage in stream_check.rs
    with toml::Table parsing, leaving `use regex::Regex;` orphaned.
    Without this removal, `cargo clippy -- -D warnings` (run in CI)
    fails with `unused import: regex::Regex`.
    
    ---------
    
    Co-authored-by: Jason <farion1231@gmail.com>
  • fix(pricing): correct Kimi K2.6 seed prices to match official rates
    Moonshot's official USD pricing for kimi-k2.6 is $0.95 input /
    $4.00 output / $0.16 cache-hit per 1M tokens (~58-60% higher than
    K2.5). The previous commit copied K2.5's $0.60/$2.50/$0.10, which
    would have under-billed K2.6 traffic in the usage dashboard.
    
    No migration needed since this version is unreleased; INSERT OR
    IGNORE will write the correct values on first launch.
  • feat(presets): upgrade Kimi K2.5 to K2.6 in direct Moonshot configs
    Bump model id and display name from K2.5 to K2.6 in Hermes, OpenClaw,
    OpenCode, and Claude (direct api.moonshot.cn) presets. Pricing,
    context window, and base URL are unchanged.
    
    Add kimi-k2.6 row to model_pricing seed; no migration needed since
    seed_model_pricing uses INSERT OR IGNORE and runs on every startup
    via ensure_model_pricing_seeded. Old kimi-k2.5 row is kept to
    preserve historical usage stats.
    
    Nvidia aggregator forwards (moonshotai/kimi-k2.5) intentionally keep
    the K2.5 SKU until Nvidia's catalog confirms K2.6.
  • fix(hermes): stop health check from borrowing OpenClaw schema
    Hermes providers were routed through check_additive_app_stream, the
    OpenClaw dispatcher, which reads camelCase fields (baseUrl/apiKey/api)
    and emits "OpenClaw is missing ..." errors. Hermes stores snake_case
    fields (base_url/api_key/api_mode) with different protocol tags, so
    users saw "OpenClaw provider is missing baseUrl" even after filling in
    every Hermes field correctly.
    
    Introduce check_hermes_stream with Hermes-specific extractors. Route
    api_mode (chat_completions / anthropic_messages / codex_responses) to
    the matching check_claude_stream api_format, and return bedrock_converse
    as unsupported. Resolve api_mode before extracting URL/API key so users
    who picked bedrock_converse see the real cause first rather than a
    misleading "missing base_url" message.
  • feat(hermes): launch dashboard from toolbar when Web UI is offline
    When the Hermes Web UI probe fails, the toolbar entry now opens an info
    confirm dialog offering to run `hermes dashboard` in the user's preferred
    terminal. Accepting spawns it via a temp bash/batch script; `hermes
    dashboard` itself opens the browser once ready, so we do not poll.
    The Memory panel and Health banner keep the existing toast behavior.
    
    Also corrects the stale `hermes web` hint in the offline toast (the real
    command is `hermes dashboard`) and reorders Linux terminal detection to
    try `which` before stat'ing /usr/bin, /bin, /usr/local/bin.
  • feat(skills): enable Hermes in unified Skills management
    Wire hermes through SkillApps struct, DAO SQL, command parser, and
    SKILLS_APP_IDS. Add a Skills entry to the Hermes toolbar. Simplify
    skill_sync test fixtures to use SkillApps::default().
  • feat(hermes): align provider schema with Hermes Agent 0.10.0
    Hermes 0.10.0 tightened custom_providers validation (commit 2cdae233):
    invalid base_urls are rejected, unknown fields produce warnings, and
    new fields (rate_limit_delay, bedrock_converse, key_env) landed.
    
    - Add bedrock_converse to the api_mode selector (and i18n labels)
    - Expose rate_limit_delay in a provider-level advanced panel
    - Validate base_url client-side (URL shape, template-token friendly)
    - Drop per-model max_tokens — not in _VALID_CUSTOM_PROVIDER_FIELDS
    - Round-trip test asserts set_provider preserves rate_limit_delay /
      key_env / any unknown forward-compat field
  • style: apply prettier and rustfmt
    No behavior changes. Brings three files back in line with the project
    formatters (CI runs `pnpm format:check` and `cargo fmt --check`).
  • test: sync stale fixtures and isolate openclaw env tests
    Three unrelated test failures surfaced after rebase:
    
    - McpFormModal expected the apps boolean set without `hermes`; Hermes MCP
      support is now wired, so the fixture must include `hermes: false`.
    - therouter Gemini preset was bumped to `gemini-3.1-pro` in a later
      commit; update the assertions to match current config.
    - openclaw_config tests mutate process-level `CC_SWITCH_TEST_HOME` and
      `HOME` inside a module-local Mutex, but hermes_config does the same
      under its own separate Mutex. Running both modules in parallel let the
      env races corrupt hermes_config's `with_test_home`. Tag the four
      env-mutating openclaw tests with `#[serial]` so they serialize across
      modules via serial_test's process-wide default key.
  • refactor(hermes): drop "Auto" api_mode and require explicit protocol
    Hermes' built-in api_mode detection only matches a handful of official
    endpoints (api.openai.com, api.anthropic.com, api.x.ai, AWS Bedrock);
    third-party / proxy endpoints silently fall back to chat_completions,
    which causes opaque 401/404s on Anthropic-protocol or Codex-Responses
    providers. The "Auto" option was misleading for the common third-party
    case.
    
    - Drop the "Auto" option from the API Mode dropdown; remove the
      HermesApiModeChoice sentinel type so writes always emit api_mode.
    - Default new providers and legacy entries lacking api_mode to
      chat_completions (only persisted on user save).
    - Deeplink imports now write api_mode: chat_completions explicitly
      instead of relying on URL heuristics; test renamed accordingly.
    - Rename the "Codex Responses (Copilot / OpenCode)" label to
      "OpenAI Responses" to match OpenAI's /v1/responses naming.
  • refactor(hermes): share provider-source marker constants and write guard
    After /simplify review of the P1-3 second wave, two small cleanups:
    
    - Lift the `_cc_source` / `providers_dict` magic strings out of
      ProviderCard into a shared helper (`isHermesReadOnlyProvider`) and
      named constants in hermesProviderPresets.ts. Front-end and back-end
      now document the same marker contract in two mirrored places
      instead of drifting strings.
    
    - Replace the duplicate `is_dict_only_provider` + `format!` branches
      at the top of `set_provider` / `remove_provider` with a single
      `ensure_provider_writable(config, name, verb)` guard. Future error
      copy tweaks only have to happen once.
    
    No behaviour change; all 52 hermes_config tests stay green.
  • feat(hermes): surface providers: dict entries read-only
    Hermes v12+ migrated some provider entries from the `custom_providers:`
    list into a `providers:` dict (keyed by id). CC Switch previously
    ignored that source entirely, leaving users blind to providers they had
    configured via Hermes' own Web UI; the only feedback was a generic
    migration warning in the health banner.
    
    `get_providers()` now unions both sources, matching upstream
    `get_compatible_custom_providers` dedup order (list wins on name
    collision). Entries coming from the dict carry a `_cc_source =
    "providers_dict"` marker plus the original `provider_key`, which the
    UI layer will use to render them read-only. `set_provider` and
    `remove_provider` now refuse to touch dict-only entries, steering the
    user to Hermes Web UI. `sanitize_hermes_provider_keys` strips the UI
    markers on write so they never reach YAML.
    
    The `schema_migrated_v12` health warning copy reframes the situation:
    entries are shown read-only in CC Switch rather than invisible.
  • fix(hermes): prevent YAML pollution and drop of OAuth mcp auth
    DeepLink Hermes import was emitting camelCase (baseUrl / apiKey /
    apiMode) that the Hermes runtime does not recognise, poisoning
    `custom_providers:` entries on activation. The MCP sync path was
    also stripping `auth: oauth` on round-trip, silently downgrading
    OAuth-type servers to unauthenticated calls.
    
    The Hermes deeplink branch now emits snake_case via a dedicated
    builder; `sanitize_hermes_provider_keys` runs on both `set_provider`
    and `get_providers` so legacy DB records heal on next access.
    `HERMES_EXTRA_FIELDS` preserves `auth`. The `api_mode` dropdown gains
    `codex_responses` (Copilot / OpenCode), and the schema-migrated
    warning copy no longer hard-codes "v12" (upstream `_config_version`
    is now 19).
  • refactor(hermes): simplify schema handling + preserve unknown provider fields
    Drops the v11→v12 providers-dict compat layer: CC Switch now only
    reads/writes `custom_providers:`, leaving migrated `providers:` dict
    entries to Hermes Web UI for reconciliation (Hermes' runtime already
    merges both shapes via `get_compatible_custom_providers`). The
    `schema_migrated_v12` health warning now points users there when a
    dict-migrated config is detected.
    
    Adds forward-compat merge to `set_provider`: when updating an existing
    entry, on-disk fields the UI payload didn't submit (e.g. Hermes-only
    `request_timeout_seconds`, `key_env`) are carried over. Without this,
    editing one field via CC Switch would silently strip the rest.
    
    Adds `set_memory_enabled` + `set_hermes_memory_enabled` Tauri command
    for the upcoming memory-switch UI. Writes go through a merge-aware
    section replacement so character budgets and external-provider fields
    survive toggle operations.
    
    Removes four dict-only helpers (`normalize_providers_dict_entry_for_read`,
    `rename_alias_key`, `json_obj_non_empty_str`,
    `resolve_provider_name_from_yaml_entry`) and the multi-section write
    helper. Simplifies `get_providers` / `remove_provider` / health scan
    back to list-only. Replaces nine obsolete dict-related tests with
    `set_provider_preserves_unknown_fields_on_update` and
    `set_memory_enabled_preserves_other_fields`.
  • feat(hermes): replace Prompts entry with Memory panel
    Hermes has no slash-prompt concept (templates live as Skills), so the
    Prompts tab for the Hermes app was always empty. Swap the toolbar Book
    button for a Brain button that opens a new Memory panel editing
    ~/.hermes/memories/{MEMORY,USER}.md — Hermes' first-class memory store
    which its Web UI exposes only as on/off toggles, never as an editor.
    
    The panel shows each file in its own tab with a character-budget bar
    read from config.yaml's nested memory.* section (memory_char_limit /
    user_char_limit, default 2200 / 1375). Edits are written atomically;
    Hermes picks them up on the next session start per MemoryStore.
    
    Also extract useDarkMode to src/hooks/useDarkMode.ts — the codebase
    already repeats the same MutationObserver pattern in 12+ places; this
    PR introduces the shared hook and uses it once, leaving the migration
    of the other copies to a follow-up.
  • refactor(hermes): delegate deep config to Hermes Web UI
    Slim the Hermes surface in CC Switch to match its core positioning —
    cross-client provider switching and shared MCP/prompts/skills — and
    delegate deep configuration (model, agent, env, skills, cron, logs)
    to the Hermes Web UI at http://127.0.0.1:9119.
    
    - Drop AgentPanel/EnvPanel/ModelPanel and their mutation commands,
      hooks, types, and i18n keys across zh/en/ja.
    - Add open_hermes_web_ui Tauri command that probes /api/status and
      launches the URL in the system browser. Hermes injects its own
      session token into the returned HTML, so CC Switch doesn't need
      to touch auth.
    - Surface the launcher from the Hermes toolbar and the health banner
      via a shared useOpenHermesWebUI() hook; the offline error code is
      defined once per side and referenced across the contract.
    - Keep read-only access to model.provider so ProviderList can still
      highlight the active supplier; apply_switch_defaults continues to
      write the top-level model section when switching providers.
    
    Net diff: +152 / -1253.
  • fix(hermes): persist providers under custom_providers so api_mode/model survive
    Writing to the v12+ `providers:` dict broke every anthropic_messages
    provider. Hermes `runtime_provider.py::_get_named_custom_provider` has a
    bug in its `providers:` branch: the returned entry drops `api_mode`,
    `transport`, `models`, and singular `model:`, and
    `_resolve_named_custom_runtime` then falls back to `chat_completions` —
    so an Anthropic-format endpoint receives OpenAI-format requests and
    returns 404.
    
    Keep using the legacy `custom_providers:` list; its normalization path
    (`_normalize_custom_provider_entry`) preserves every field. In addition,
    write a singular `model:` alongside the plural `models:` dict so the
    Hermes runtime and `/model` picker see the default model id.
    
    Also keep the `apply_switch_defaults` fix from the prior attempt:
    `model.provider` is always updated, and `model.default` is only
    overwritten when the new provider declares at least one model — so
    switching to an incomplete provider no longer silently no-ops.
  • feat(hermes): bind per-provider models to top-level model: on switch
    Hermes custom_providers entries now carry an ordered models array
    (id / context_length / max_tokens) plus suggestedDefaults. The backend
    serializes the array to the YAML dict shape Hermes expects on write and
    inverts it on read, preserving insertion order via the preserve_order
    feature on serde_json.
    
    When a user switches providers, switch_normal calls apply_switch_defaults
    so the top-level model.default / model.provider follow the selected
    provider's first model. Previously switching a Hermes provider only
    shuffled custom_providers[] and left Hermes pointing at whatever
    model.provider was set before.
    
    Seven existing Hermes presets now ship with a curated models list so
    switching lands on a working default without a detour through the
    Model panel.
  • feat(copilot): strip thinking blocks before forwarding to save premium quota
    Copilot routes through OpenAI-compatible endpoints that reject Anthropic's
    thinking and redacted_thinking blocks. Previously the request would fail
    upstream, burning one premium interaction, and only then trigger
    thinking_rectifier to retry. This adds a proactive strip_thinking_blocks
    pass in the Copilot optimization pipeline (step 3.5, after tool_result
    merging). Signature fields and top-level thinking are left alone — those
    are the reactive rectifier's job on the error path.
    
    Also fixes a default-value inconsistency where CopilotOptimizerConfig's
    Default impl used "gpt-4o-mini" while the serde default function returned
    "gpt-5-mini" (aligned to gpt-5-mini, matching the reference implementation).
    
    Aligned with yuegongzi/copilot-api's /v1/messages handler behavior.
  • feat(presets): migrate all aggregator and Bedrock presets to Claude Opus 4.7
    - OpenClaw: replace opus-4-6 with opus-4-7 across 17 aggregator presets
      (id, name, primary, modelCatalog); AWS Bedrock entry rewritten to new
      SKU anthropic.claude-opus-4-7 (drops -v1 and dated suffix per official
      4.7 model card) and pricing corrected to $5/$25/$0.50/$6.25 during the
      SKU swap, aligning with schema.rs source of truth
    - OpenCode: same replacement for 13 aggregators plus
      OPENCODE_PRESET_MODEL_VARIANTS entries for @ai-sdk/amazon-bedrock and
      @ai-sdk/anthropic, plus AWS Bedrock provider models map
    - OpenRouter / TheRouter / GitHub Copilot in claudeProviderPresets use
      dot-style id; update to anthropic/claude-opus-4.7 (missed by 509d2250)
    - omo: switch agent/category recommended to opus-4-7; replace key in
      OMO_BACKGROUND_TASK_PLACEHOLDER priority map
    - hermes_config.rs: update doc comments and test fixtures to opus-4-7;
      Hermes ModelPanel placeholder and i18n defaultHint examples follow
    - i18n unspecifiedHigh category description bumped to 'Claude Opus 4.7
      max variant' to match omo recommended
    - Test fixtures updated: therouter preset assertion and opencode Bedrock
      variant lookup now check for opus-4-7
    - Sonnet 4.6 / Haiku 4.5 untouched - no official 4.7 release for them
  • feat(pricing): add Claude Opus 4.7 with adaptive thinking and Bedrock SKU
    - Seed claude-opus-4-7 pricing (same tier as 4.6: $5 / $25 / $0.50 /
      $6.25 per million tokens). Relies on incremental INSERT OR IGNORE
      seeding; no SCHEMA_VERSION bump needed.
    - Whitelist opus-4-7 in thinking optimizer so it uses adaptive
      thinking + max effort + 1M context beta, matching 4.6 behavior.
    - Bump default OPUS model in PIPELLM and AWS Bedrock (AKSK / API Key)
      presets to 4.7. Bedrock SKU drops the -v1 suffix per the official
      4.7 model card (anthropic.claude-opus-4-7 and
      global.anthropic.claude-opus-4-7).
  • chore(lint): pin Rust toolchain to 1.95 and adopt clippy 1.95 suggestions
    - Add rust-toolchain.toml to align local and CI Rust versions, eliminating
      clippy roulette caused by `dtolnay/rust-toolchain@stable` drift.
    - Fix 9 clippy 1.95 findings introduced by Hermes Phase 4-8 modules:
      * 4x unnecessary_sort_by  -> sort_by_key (with Reverse for desc)
      * 3x collapsible_match    -> match guards
      * 1x while_let_loop       -> while let
      * 1x useless_conversion   -> drop redundant .into_iter()
  • fix: address Hermes review findings (5 medium issues)
    - Add missing Hermes MCP import on first launch (lib.rs)
    - Add Hermes branch in ProviderForm defaultValues fallback
    - Include Hermes in session manager subtitle (zh/en/ja)
    - Rename check_openclaw_stream to check_additive_app_stream
    - Cache parsed HERMES_DEFAULT_CONFIG to avoid repeated JSON.parse
  • feat: implement Hermes session manager with SQLite + JSONL support (Phase 6)
    - Add hermes.rs session provider with dual-source scanning:
      SQLite (state.db) as primary, JSONL transcripts as fallback
    - Dynamic schema discovery via PRAGMA table_info for SQLite resilience
    - Use read_head_tail_lines for efficient JSONL metadata extraction
      (head 30 lines for metadata, tail 10 for last_active_at)
    - Support both flat and nested JSONL message formats
    - Add SQLite session loading and transactional deletion
    - Register hermes in parallel session scan (thread::scope)
    - Add "hermes" to frontend ProviderFilter type
    - 7 unit tests covering JSONL parsing, SQLite source parsing, deletion
  • feat: add Hermes UI components, presets, and config panels (Phase 8)
    - Add 7 provider presets (OpenRouter, Anthropic, OpenAI, Google, DeepSeek, Together, Nous)
    - Create HermesFormFields + useHermesFormState for provider form integration
    - Create Model/Agent/Env config panels with save/load functionality
    - Create HermesHealthBanner for config warnings
    - Add hermes icon (violet winged H) to icon system
    - Integrate into App.tsx: 3 new view types (hermesModel/hermesAgent/hermesEnv),
      sidebar buttons (Brain/Bot/KeyRound), health banner, session support
    - Integrate into ProviderForm: presets, form state, key validation, rendering
    - Integrate into AddProviderDialog: universal tab exclusion, providerKey, base_url extraction
    - Add i18n keys for all Hermes UI (zh/en/ja)
  • feat: implement Hermes MCP sync module (Phase 4)
    Add mcp/hermes.rs with bidirectional MCP format conversion:
    - convert_to_hermes_format: strip type field, infer from command/url
    - convert_from_hermes_format: infer type, strip Hermes-specific fields
    - Merge-on-write: existing Hermes fields (tools, sampling, timeout,
      roots, enabled) preserved when user has customized them
    - update_mcp_servers_yaml: closure-based read-modify-write under write
      lock to prevent TOCTOU races in concurrent sync operations
    - 9 unit tests for format conversion and merge logic
    
    Wire up all MCP service dispatch:
    - Replace Hermes TODO stubs with real sync/remove calls
    - Remove Hermes from sync_all_enabled skip list
    - Enable deep link hermes MCP flag (apps.hermes = true)
    - Add Hermes import to import_mcp_from_apps command
  • feat: implement Hermes config module and commands (Phase 3)
    Add hermes_config.rs (~1190 lines) with YAML section-level replacement
    that preserves comments and formatting in unmanaged sections:
    - Type definitions: HermesModelConfig, HermesAgentConfig, HermesEnvConfig
    - YAML section finder (find_yaml_section_range) with column-0 key detection
    - Provider CRUD on custom_providers array (indexed by name field)
    - Model/Agent config get/set via yaml<->json conversion
    - .env dotenv read/write preserving comments and line ordering
    - Health check, backup with rotation, write lock (OnceLock<Mutex>)
    - MCP section access stubs for Phase 4
    - 19 unit tests
    
    Add commands/hermes.rs with 10 Tauri commands registered in lib.rs.
    Replace all Hermes TODO stubs in services/provider/live.rs with real
    implementations (import, remove, write-to-live, read-live-settings).
  • feat: add database migration v9→v10 for Hermes support (Phase 2)
    - Bump SCHEMA_VERSION from 9 to 10
    - Add enabled_hermes column to mcp_servers and skills tables
    - Add migrate_v9_to_v10 with table_exists guard for skills (may not
      exist in databases migrated from very old versions)
    - Update dao/mcp.rs to fully read/write enabled_hermes in all queries
    - Update dao/skills.rs: don't SELECT enabled_hermes (Hermes doesn't
      support Skills yet), keep column indices clean