Commit Graph

732 Commits

  • 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.
  • refactor(claude-desktop): lock route IDs to sonnet/opus/haiku roles
    Adapt to Claude Desktop 1.6259.1+ fail-all validation which only
    accepts claude-(sonnet|opus|haiku)-* route IDs. Branded model names
    (DeepSeek, Kimi, GLM, etc.) now live in a new labelOverride field
    instead of being embedded in route IDs.
    
    - Backend auto-repairs legacy unsafe routes to the next free
      sonnet/opus/haiku slot instead of erroring
    - Frontend swaps the free-form route input for a role dropdown plus
      menu display name field
    - Add CLAUDE_DESKTOP_ROLE_ROUTE_IDS as the single source of truth
      for role-to-route mapping; presets and form both consume it
    - Drop the dead displayName alias on ClaudeDesktopModelRoute and the
      ineffective /v1/models display_name injection (UI ignores it)
    - Update i18n (en/ja/zh) and form focus test for the new fields
  • feat(usage): filter-driven Hero with cache-normalized totals
    - Normalize OpenAI/Gemini input_tokens semantics in SQL via the new
      fresh_input_sql helper (cache_read subtracted at query time, no data
      migration). Recovers correct cache hit rates for Codex/Gemini.
    - Add get_usage_summary_by_app endpoint for per-app split (single
      UNION ALL + GROUP BY, avoids N+1).
    - Replace UsageSummaryCards + AppBreakdownRail with a single
      filter-driven UsageHero card; clicking a filter button now truly
      changes the displayed numbers and the title accent color.
    - Tighten KNOWN_APP_TYPES to the 3 app_types whose token data is
      reliably collected (claude/codex/gemini); hide claude-desktop,
      hermes, opencode, openclaw filter buttons and i18n keys.
    - Flag cache_creation as N/A for OpenAI-style protocols (Codex,
      Gemini); show a "partial" tooltip when the All view mixes both
      protocol families.
  • feat(claude-desktop): rework Claude Code import flow
    - Derive route keys from the upstream model name (pass-through style)
      instead of fixed Claude aliases, and translate the legacy [1M] suffix
      into the supports1m field at the import boundary. Three Claude aliases
      mapped to the same upstream now collapse to a single route (e.g.
      MiniMax-M2 across SONNET/OPUS/HAIKU env produces one
      claude-MiniMax-M2 -> MiniMax-M2 row), with [1M] OR-aggregated.
    - Add an import-time safety net that rebuilds claude-desktop-official
      when missing, so users who deleted it can recover via the normal
      import button without losing customizations on other providers.
    - Hide API key and endpoint URL inputs in the official provider edit
      form to mirror Claude Code's behavior and prevent user confusion.
    - Reword the empty-state import button label for clarity.
  • refactor(claude-desktop): replace [1M] suffix with supports1m field
    inferenceModels entries now emit {name, supports1m: true} objects when
    1M is enabled (plain strings otherwise), instead of appending a " [1M]"
    suffix to model IDs. Route IDs and upstream model IDs are stored
    verbatim; the suffix is rejected on input rather than silently stripped,
    and proxy request mapping now requires an exact route_id match.
  • - 支持 Claude Desktop 使用 Copilot/Codex OAuth 供应商
    - 放开本地路由托管 OAuth 供应商校验,允许动态 Token
    - 新增 Claude Desktop Copilot/Codex 预设与账号选择
    - 添加 OAuth proxy 回归测试
  • refactor(claude-desktop): drop displayName from model route schema
    Claude Desktop's new model menu reads model IDs directly and ignores the
    display_name field, so a separate displayName slot added UI noise without
    any product value. Collapse the routeId / model / displayName tuple down
    to routeId / model, and let the route ID carry the user-visible name
    through a non-editable claude- prefix rendered next to the input.
    
    Drop display_name from ClaudeDesktopModelRoute, ClaudeDesktopDefaultRoute,
    and ResolvedModelRoute on the Rust side plus the matching TS interfaces,
    stop emitting it in /v1/models responses, derive route IDs from upstream
    model IDs when picked via the model dropdown, and update zh/en/ja copy to
    describe the new two-field layout.
  • chore(brand): surface ccswitch.io as the sole official website
    Add an "Only Official Website" header to the three READMEs, an
    About panel button, and a tray menu entry — all pointing to
    ccswitch.io. Consolidates brand and SEO signals on the canonical
    domain across docs, GUI, and system tray.
  • perf(proxy): trim per-request hot-path work and db wait
    - Guard debug body serialization with `log::log_enabled!`; previously
      serialized the filtered body to a throwaway String on every forward,
      even with debug logging off.
    - Skip SSE parse + UTF-8 buffer loop when no usage collector and debug
      is off; the per-chunk `serde_json::from_str::<Value>` ran even in
      pure passthrough mode.
    - Add cheap per-app SSE event pre-filter (string `contains`) so usage
      collectors only parse events that could contain usage (e.g. Claude
      `message_start` / `message_delta`).
    - Skip non-streaming response body JSON parse when usage logging is
      disabled.
    - Move `ProviderRouter::record_result` off the success response path
      via `tokio::spawn` for non-HalfOpen state; that call internally does
      `get_proxy_config_for_app` + `update_provider_health`, two SQLite
      ops that previously blocked TTFB.
    
    Also: dedupe `usage_logging_enabled` (was duplicated in handlers.rs)
    and merge `SseUsageCollector::{new, new_filtered}` into a single
    constructor that takes `Option<StreamUsageEventFilter>`.
  • fix(proxy): improve cache hit rate for Codex/Responses requests
    prompt_cache_key was falling back to provider.id when the client did not
    supply a session, which collapsed every conversation onto a single key
    and defeated upstream prefix caching. Only emit the key when a real
    client-provided session/thread identity is available; otherwise let the
    upstream use its default matching behaviour.
    
    Additional fixes that affect cache stability:
    - Canonicalise (sort) JSON keys in outgoing request bodies and in
      tool_call arguments / tool_result content so semantically identical
      requests produce identical byte sequences for upstream prefix caches.
    - Exempt JSON Schema property maps (properties, patternProperties,
      definitions, \$defs) from the underscore-prefix filter so user-defined
      schema keys like _id and _meta survive.
    - Add a [CacheTrace] debug log with stable hashes for instructions,
      tools, input and include to help diagnose cache misses.
    - Thread session_id into the usage logger for request correlation.
  • fix(proxy): drop empty pages from Read tool input (#2472)
    * fix(proxy): drop empty pages from Read tool input
    
    * fix(proxy): preserve Read args across duplicate tool starts
  • - fix(ci): restore frontend formatting and Linux clippy
    - Format Claude Desktop provider presets with Prettier
    
    - Gate platform-specific Claude Desktop path helpers behind cfg
  • chore(backend): satisfy cargo fmt and clippy --all-targets
    - Apply rustfmt diffs in claude_desktop_config.rs
    - Allow needless_return on current_platform_paths (cfg-mirrored arms)
    - Allow too_many_arguments on RequestForwarder::forward
    - Replace `let mut + reassign` with struct literals in tests
      (settings, backup, provider, response_processor)
    - Use Path::new instead of PathBuf::from to fix cmp_owned in misc tests
    - Replace 3.14 with 3.5 in config test to avoid approx_constant lint
  • fix(claude-desktop): match proxy model route without [1M] suffix
    Claude Desktop strips the [1M] suffix from model IDs when sending
    requests, causing route lookup to fail with "model route is not
    configured". Fall back to base-name comparison when exact match misses.
  • refactor(claude-desktop): trim duplication in proxy and switch flows
    - services/proxy.rs: collapse 10 repeated `OpenCode | OpenClaw | Hermes |
      ClaudeDesktop` match arms into `_` fallthroughs.
    - claude_desktop_config.rs: extract a `with_rollback` closure shared by
      apply_provider_to_paths and restore_official_at_paths.
    - useProviderActions.ts: replace the triple-nested ternary picking the
      switch-success toast message with a flat let/if/else block.
    
    Net -36 lines. No behavior change; cargo test and pnpm typecheck pass.
  • feat(claude-desktop): add 3P provider switching with proxy gateway
    Adds a new ClaudeDesktop AppType that writes Claude Desktop's third-party
    inference profile under configLibrary/, sharing _meta.json with other
    launchers (Ollama-compatible) so cc-switch can coexist with them.
    
    Two switch modes:
    - direct: provider already exposes claude-* / anthropic/claude-* model
      ids on Anthropic Messages, Claude Desktop connects to it directly.
    - proxy: cc-switch's local proxy acts as the inference gateway,
      presenting only claude-* route names to Claude Desktop and mapping
      them to real upstream models. Required after Anthropic restricted
      Claude Desktop to claude-family ids.
    
    Backend:
    - New module claude_desktop_config with snapshot/rollback, official seed
      bypass, /claude-desktop/v1/{models,messages} routes, and a single
      source of truth for default proxy routes.
    - Gateway token persisted in SQLite, validated on every proxied request.
    - get_claude_desktop_status surfaces drift signals (stale models,
      missing routes, proxy stopped, base URL mismatch, missing token).
    
    Frontend:
    - Slim ClaudeDesktopProviderForm independent from ProviderForm,
      controlled by a top-level appId guard.
    - ProviderList banner consumes the status query (5s polling) and
      renders actionable diagnostics.
    - ClaudeDesktopRouteToggle in the header to start/stop the local
      gateway without touching takeover state.
    - Three-locale i18n synchronised.
  • fix(proxy): reuse pooled HTTPS connections for non-Anthropic backends
    The hyper raw-write path preserves original header casing but rebuilds
    TCP+TLS on every request — there is no connection pool — which was the
    root cause of slow reverse-proxy throughput.
    
    Only Anthropic-native requests actually need exact header-case
    preservation. Route OpenAI/Copilot/Codex/Gemini/codex_oauth requests
    through the pooled reqwest client (pool_max_idle_per_host=10,
    tcp_keepalive=60s) instead, so warm connections get reused.
    
    Streaming requests get a precise first-byte timeout via
    tokio::time::timeout around reqwest's send() (which resolves on
    response headers), with the body phase handed off to response_processor.
    The streaming-detection helper now also covers Gemini SSE endpoints
    and Accept: text/event-stream, not just body.stream.
  • Fix Codex startup live import duplication (#2590)
    * Fix Codex startup live import duplication
    
    * Fix: Prevent duplicate Codex default provider on restart & add startup import tests
  • feat: return reasoning_content with tool_calls for DeepSeek models (#2543)
    * feat: return reasoning_content with tool_calls for DeepSeek models
    
    * fix: correct reasoning_content handling for DeepSeek tool_calls
    
    * test: cover DeepSeek reasoning content round trip
    
    ---------
    
    Co-authored-by: Jason <farion1231@gmail.com>
  • fix(proxy): derive Claude auth strategy from ANTHROPIC env var name
    Anthropic SDK assigns distinct semantics to the two env vars:
    
    - ANTHROPIC_API_KEY    -> x-api-key
    - ANTHROPIC_AUTH_TOKEN -> Authorization: Bearer
    
    The Claude adapter previously collapsed both into AuthStrategy::Anthropic
    and then emitted Authorization: Bearer regardless, breaking strict
    Anthropic-protocol endpoints (Anthropic official, Cloudflare AI Gateway,
    OpenCode Go, DashScope) and silently overriding the user's intended auth
    scheme.
    
    - claude::extract_auth: infer strategy from env var name
      (ANTHROPIC_AUTH_TOKEN -> ClaudeAuth, ANTHROPIC_API_KEY -> Anthropic),
      matching the precedence already used by extract_key.
    - claude::get_auth_headers: split the Anthropic arm so it emits
      x-api-key, while ClaudeAuth and Bearer continue to use Bearer.
    - stream_check: reuse ClaudeAdapter::get_auth_headers as the single
      source of truth, replacing the prior "always Bearer + maybe x-api-key"
      double injection that produced auth conflicts and false-negative
      health checks.
    - Cover each strategy -> header mapping and env-var precedence with
      new unit tests in claude.rs.
    
    Refs #2368, #2380
  • fix(proxy): strip leading billing header from system content (#2350)
    Claude Code injects a dynamic `x-anthropic-billing-header` line at the
    start of `system` content. Its rotating `cch=` token was forwarded into
    OpenAI Responses `instructions` and Chat system messages, which broke
    upstream prefix prompt cache reuse — a stable ~95k-token prefix was
    getting re-charged on every request.
    
    Strip only the leading occurrence in both anthropic_to_openai and
    anthropic_to_responses; later occurrences are preserved so user-authored
    prompt text containing the same string is not lost.
  • chore(usage): drop Hermes Agent tracking integration
    Hermes aggregates all in-process API calls into a single sessions row
    with the `model` field locked to the initial model, so the usage
    dashboard cannot cleanly surface per-call billing context. Two rounds
    of UI workarounds (raw mapping, then `<model> @ <host>` display) did
    not resolve the user-facing confusion, so the whole tracking
    integration is dropped for now.
    
    Removes session_usage_hermes service (and its 17 tests), sync wiring
    in commands/usage.rs and lib.rs, _hermes_session/hermes_session
    entries in usage_stats SQL (provider_name_coalesce CASE and
    effective_usage_log_filter IN clause), frontend Tab/banner/dropdown/
    icon entries, and four i18n keys per locale.
    
    Hermes app integration outside usage tracking (proxy routing,
    session manager, config) is preserved. Pre-existing hermes rows in
    proxy_request_logs are left as orphans — filtered out by the
    updated SQL and never surfaced in the UI.
  • feat: persist Tauri window state (#2377)
    Add the window-state plugin and explicitly save size and position across app exit, restart, and lightweight-mode transitions.
  • feat(providers): add Baidu Qianfan Coding Plan for Claude Code (#2322)
    * feat(providers): add baidu qianfan coding plan presets
    
    * refactor(providers): align qianfan presets with existing format
    
    * chore(providers): narrow qianfan coding plan scope
  • fix(coding-plan): correct zhipu weekly tier name by reset time (#2420)
    Zhipu's `data.limits[]` returns 1 entry for legacy plans (subscribed
    before 2026-02-12) and 2 entries for current plans. Previously every
    TOKENS_LIMIT entry was hardcoded as `five_hour`, so the weekly bucket
    was rendered with the 5-hour i18n label.
    
    Sort TOKENS_LIMIT entries by nextResetTime ascending and assign
    `five_hour` to index 0, `weekly_limit` to index 1. Legacy plans
    naturally degrade to a single five_hour tier.
    
    Also harden the parser: case-insensitive type match (defends against
    upstream casing changes), reuse TIER_FIVE_HOUR/TIER_WEEKLY_LIMIT
    constants, and add 8 unit tests covering both plan shapes plus
    defensive edge cases.
  • fix(dashscope): enhance usage parsing robustness to prevent VSCode cr… (#2425)
    * fix(dashscope): enhance usage parsing robustness to prevent VSCode crashes
    
    Enhanced build_anthropic_usage_from_responses() to handle null, missing, empty,
    and partial usage fields gracefully. This prevents VSCode Extension crashes with
    "Cannot read properties of null (reading 'output_tokens')" when connecting to
    DashScope (Alibaba Cloud Bailian) models.
    
    Changes:
    - Added defensive null checks and empty object detection
    - Implemented OpenAI field name fallbacks (prompt_tokens/completion_tokens)
    - Added comprehensive logging for malformed usage scenarios
    - Fixed streaming SSE event handlers with null-safe usage access
    - Preserved cache token fields even when input/output tokens are missing
    
    This ensures the proxy never crashes on malformed Responses API usage objects,
    returning valid Anthropic-compatible usage structures (input_tokens/output_tokens)
    in all cases.
    
    * fix(proxy): tighten Responses API usage fix per review
    
    - Drop redundant fallback in streaming.rs Chat Completions path; the
      existing if-let-Some guard already prevents usage:null, so the extra
      layer was dead code and caused a fmt-breaking indentation issue.
    - Demote partial-usage warn to debug. Streaming chunks legitimately
      arrive with partial token counts and the warn-level log was noisy.
    - Rewrite CHANGELOG entry: reference #2422, broaden scope from
      DashScope-only to all api_format=openai_responses users (Codex OAuth
      is the strongest signal; DashScope compatible-mode/v1/responses is
      the original report).
    - cargo fmt to clear 12 formatting differences vs main.
    
    ---------
    
    Co-authored-by: Jason <farion1231@gmail.com>
  • fix(config): sort JSON keys alphabetically for deterministic output (#2469)
    * fix(config): sort JSON keys alphabetically for deterministic output
    
    Ensures settings.json keys are written in sorted order, preventing
    non-deterministic git diffs when switching configs.
    
    * test(config): add unit tests for sort_json_keys and fix formatting
    
    Cover top-level sort, nested recursion, array order preservation,
    primitive pass-through, empty collections, and the core determinism
    guarantee (different insertion orders must yield identical output).
    
    Also fix line-length in write_json_file flagged by `cargo fmt --check`.
    
    ---------
    
    Co-authored-by: Jason <farion1231@gmail.com>