Commit Graph

1938 Commits

  • feat(presets): add GLM 5.1 context window for AtlasCloud Codex preset
    Declare the 200000-token context window for zai-org/glm-5.1, matching the other GLM 5.1 preset entries.
  • chore: remove stray nested preset file accidentally committed in #667
    cc-switch-main/src/config/universalProviderPresets.ts was an outdated duplicate of the real preset file, introduced by mistake in #667 and referenced by no code. Remove the orphaned nested directory.
  • feat: add Kimi K2.7 Code model and upgrade official Kimi presets
    Add kimi-k2.7-code pricing seed (in $0.95 / out $4.00 / cache-read $0.19
    per 1M tokens, 256K context) and point all six official Moonshot Kimi
    presets (Claude Code, Codex, Claude Desktop, Hermes, OpenCode, OpenClaw)
    at the new model. Rename the version-tagged OpenCode/OpenClaw presets to
    "Kimi K2.7 Code" and correct the OpenClaw context window to 262144.
    
    The seed is applied via the idempotent INSERT OR IGNORE path that runs on
    every startup, so existing users pick up the new pricing without a schema
    migration. Kimi For Coding and Nvidia presets are intentionally untouched.
  • fix(providers): scope preset search to provider names only
    The preset search text also included websiteUrl and the shared category label, producing imprecise matches: a single category term matched the whole group, and URL fragments like "com"/"api" matched nearly everything. Restrict the search text to the display name and raw name; category labels are still used for rendering.
  • fix(ui): raise popover/tooltip z-index above fullscreen panels
    PopoverContent and TooltipContent used z-50, below FullScreenPanel's z-[60] opaque overlay. Popovers such as the provider preset search therefore rendered behind the panel and looked unresponsive on click. Bump both to z-[100], matching the select dropdown: above fullscreen panels, below modal dialogs (z-[110]).
  • fix(ui): make Claude Desktop model-mapping placeholders role-consistent
    The menu display name and request model columns used mismatched example
    brands (DeepSeek vs Kimi), implying a display name maps to an unrelated
    request model. Derive both placeholders from the row role so each row
    stays brand-consistent, and route the lightweight Haiku tier to a flash
    example (deepseek-v4-flash) while other tiers use deepseek-v4-pro.
  • feat: add Claude Fable 5 model mapping across Claude Code and Desktop
    - Wire claude-fable-5 as a fourth tier on both proxy paths, with a
      fable -> opus -> default fallback mirroring the official downgrade.
    - Whitelist the fable- prefix for the Desktop 1.12603.1+ validator.
    - Clarify fallbackModelHint (zh/en/ja/zh-TW): a blank tier on
      third-party endpoints forwards the literal model name and 404s.
    
    Refs #3980, #4026, #4049.
  • fix(usage): compact toolbar controls and unify visual style
    - Reduce all four filter controls to w-[100px] h-9
    - Add ChevronDown icon to date picker trigger for consistency
    - Suppress focus border highlight on select triggers after close
  • feat(codex): add opt-in migration and ledger-based restore for unified session history
    - Enable dialog gains a checkbox (default off) to migrate existing
      official sessions from the built-in "openai" bucket into the shared
      "custom" bucket, with per-generation backups; failed migrations retry
      at startup
    - Disable dialog offers a precise restore driven by the backup ledger:
      only sessions recorded as "openai" in backups are flipped back, and
      sessions created while the toggle was on are never touched
    - Completion marker and backup generations are bound to the canonical
      Codex config dir; migrate/restore serialize on an op lock and the
      marker is written conditionally inside the settings write lock
    - save_settings rolls back the toggle and fails the save when the live
      rewrite fails; migration additionally requires the live config to
      actually route to the shared bucket (skips with live_not_unified so
      refused injection or proxy takeover can't split history)
    - Restore refuses to run while the toggle is (re-)enabled and reports
      nothing_to_restore instead of a zero-count success; local migration
      markers are now backend-owned in merge_settings_for_save so stale
      frontend payloads can't resurrect them
    - Settings autosave reverts optimistic form state on failure so a
      failed toggle change can't be replayed by an unrelated save
    - ConfirmDialog supports an optional checkbox; all four locales updated
  • feat(codex): add unified session history toggle for official providers
    Codex buckets resume history by the model_provider id recorded in each
    session: official runs (no key, built-in "openai") and cc-switch
    third-party runs (shared "custom") are mutually invisible in the resume
    picker. Add an opt-in setting that runs official providers under the
    shared "custom" id so future official sessions land in the same history
    bucket as third-party ones. Forward-only by design: existing sessions
    are not migrated.
    
    When enabled, official live config.toml gets model_provider = "custom"
    plus a [model_providers.custom] entry that mirrors the built-in openai
    provider (requires_openai_auth routes auth to the ChatGPT login in
    auth.json, name "OpenAI" keeps is_openai() feature gates, explicit
    supports_websockets/wire_api restore built-in defaults). auth.json is
    untouched.
    
    Key invariants:
    - Injection lives only in the live config: switch-away backfill strips
      the exact injected shape, so stored provider configs stay clean and
      turning the toggle off fully reverts on the next write.
    - Toggle changes apply immediately via a takeover-aware reapply: when
      the proxy owns the live config (backup/placeholder present), only the
      live backup is updated, mirroring the provider-switch path.
    - The takeover backup path runs the same injection so a takeover
      release restores a config that still carries the unified routing.
    - Injection refuses to activate a foreign [model_providers.custom]
      table (e.g. stale entry with a third-party base_url) to avoid routing
      ChatGPT OAuth traffic to an unknown backend.
    
    The toggle lives under Settings → Codex App Enhancements; the
    description warns that resuming old sessions across providers may fail
    because encrypted_content reasoning only decrypts on the backend that
    created it (upstream treats cross-provider resume as unsupported).
  • chore(presets): remove LemonData provider and demote SudoCode to regular provider
    - LemonData: delete the provider preset from all apps (claude, claude-desktop,
      codex, gemini, hermes, opencode, openclaw), remove partner promotion copy
      (zh/en/ja/zh-TW), icon index/metadata entries, sponsor ads in all READMEs,
      and the logo/icon PNG assets.
    - SudoCode: drop the isPartner flag and partnerPromotionKey across all app
      presets and remove the now-orphaned partner promotion copy; it stays as a
      regular third_party provider, keeping its icon.
  • feat(usage): turn refresh interval into a select and align control widths
    Replace the click-to-cycle refresh button with a Select matching the
    source/model filters. The "off" option now shows a localized label
    (zh/en/ja/zh-TW) instead of the cryptic "--", and changing the interval
    still invalidates all usage queries for an immediate refresh.
    
    Align the top-bar controls into two width groups: source and model
    selects at 120px, refresh select and date range trigger at 150px. The
    date range button moves from auto width to fixed, so its long custom
    range label gets truncate + hover title, and the calendar icon is
    shrink-proofed.
  • feat(usage): lift provider/model filters to dashboard-wide scope
    The provider/model filters only lived inside the request-log table, so
    there was no way to see "how much did app X spend on source Y" across
    the whole dashboard. Promote them to the top bar next to the app
    filter, applying globally to the hero summary, trend chart, request
    logs, and both stats tabs.
    
    Backend: the five stats queries (summary, summary-by-app, trends,
    provider stats, model stats) accept optional provider_name/model
    filters, applied to both the detail and daily-rollup branches (the
    rollup PK already carries provider_id/model/pricing_model). Sources
    match by exact display name via provider_name_coalesce, so session
    placeholder rows like "Claude (Session)" are selectable; models match
    by effective pricing model (pricing_model falling back to model), the
    same grouping key the model-stats tab uses. Request-log filtering
    switches from LIKE to these exact semantics.
    
    Frontend: two truncating dropdowns list only sources/models that have
    data in the current range, with the model list cascading from the
    selected source. Dynamic option values are prefix-encoded so a source
    literally named "all" cannot collide with the sentinel, query keys
    fall back to null instead of "all", and the option queries follow the
    dashboard refresh interval (otherwise their default 30s polling drags
    same-key stats queries along even with refresh disabled). The
    request-log filter bar keeps only the log-specific status-code select.
    Labels read "sources" rather than "providers" because direct-connect
    session buckets sit alongside real providers. i18n updated across
    zh/en/ja/zh-TW.
  • feat(usage): replace app filter text labels with app icons
    The text tabs (All / Claude Code / Codex / Gemini / OpenCode) wrapped
    awkwardly in narrow windows. Render app icons via ProviderIcon instead,
    mirroring AppSwitcher's icon mapping (codex reuses the openai icon),
    with a LayoutGrid icon for "all". Localized labels are kept as
    title/aria-label on each button.
  • refactor(usage): fold claude-desktop into claude in the dashboard
    The Desktop gateway's proxy traffic is still recorded under its own
    app_type for route-takeover billing audit (the request detail panel
    shows the real value), but the dashboard now folds it into `claude`
    for display. A standalone "Claude Desktop" bucket only ever showed a
    partial number: Desktop chat usage never passes through the proxy and
    has no scannable local source, while its Code-tab sessions are just the
    embedded Claude Code runtime writing into the shared ~/.claude/projects
    tree — so a separate bucket misled users into reading it as Desktop's
    full usage.
    
    Backend: new `folded_app_type_sql` helper wraps the app_type column in
    every dashboard read path (10 filter sites + the by-app projection) so
    `= 'claude'` also matches claude-desktop and GROUP BY merges the two,
    without changing bound-param counts. Dedup matching and provider-limit
    checks keep exact comparison; get_request_logs folds only the WHERE
    filter and keeps the raw app_type in its row projection.
    
    Frontend: drop claude-desktop from the dashboard AppType/KNOWN_APP_TYPES
    filter list, the UsageHero theme, and the now-dead appFilter locale key
    in all four languages (the managed-app apps.claude-desktop label stays).
    
    Adds test_claude_desktop_folds_into_claude_for_display.
  • fix(proxy): preserve Codex OAuth auth token on takeover (#3789)
    * fix(proxy): preserve Codex OAuth auth token on takeover
    
    * style(proxy): format Codex OAuth takeover fix
    
    * fix(proxy): unconditionally inject AUTH_TOKEN placeholder for codex takeover
    
    The preserve-if-exists condition left #3784 unfixed on three paths:
    hot-switch passes the provider's settings (presets carry no
    ANTHROPIC_AUTH_TOKEN key), fresh installs never had the key, and live
    configs already stripped by older releases stay stripped.
    
    - Fold the bool parameter into the policy enum as
      ManagedAccount { keep_auth_token } so every construction site
      declares intent
    - Decide via !is_github_copilot() within the managed branch so
      URL-only codex providers (no provider_type meta) are covered,
      matching the predicate family used for policy selection
    - Inject the placeholder unconditionally instead of only when the
      key pre-exists; Copilot behavior is unchanged (API_KEY only)
    - Pin the previously uncovered cases with tests: codex without a
      pre-existing key, URL-only codex, and Copilot removing a stale
      AUTH_TOKEN
    
    ---------
    
    Co-authored-by: codeasier <liuyekang@huawei.com>
    Co-authored-by: Jason <farion1231@gmail.com>
  • Add claude-mythos-5 model to schema (#4077)
    Insert a new 'claude-mythos-5' model tuple into src-tauri/src/database/schema.rs. The tuple ("claude-mythos-5", "Claude Mythos 5", "10", "50", "1.00", "12.50") is added to the models list (placed before the Claude 4.8 series) to register the Mythos 5 model with the Database schema.
  • fix(proxy): harden takeover-residue recovery across config-dir switches (#4076)
    Changing app_config_dir relocates the SQLite database, so a restart
    triggered while proxy takeover is active used to strand the live
    configs: the new instance reads a fresh DB with no live backups, the
    first-run import then persisted the PROXY_MANAGED placeholder as the
    `default` provider, and the no-backup recovery path wrote that
    placeholder right back to the live files — leaving Claude/Codex/Gemini
    pointed at a dead local proxy with no automatic way out.
    
    Three orthogonal fixes, defense in depth:
    
    - restart_app now awaits cleanup_before_exit() before app.restart().
      Since #4069 the ExitRequested handler intentionally defers restart
      requests to Tauri's default re-exec without custom cleanup, which is
      correct for same-DB restarts but not for this command's dir-change
      use case: only the old instance holds the backups needed to restore
      the taken-over live files, so it must restore them while its event
      loop is still alive.
    - import_default_config refuses to import a live config that is under
      proxy takeover (placeholder detected), instead of persisting it as
      the current provider.
    - restore_live_from_ssot_for_app validates that the current provider's
      settings_config does not itself contain takeover placeholders before
      writing it back; polluted SSOT now falls through to the placeholder
      cleanup fallback.
    
    Regression tests cover the import guard and the no-backup recovery
    path (the latter fails before this change by writing PROXY_MANAGED
    back to live).
  • fix(updater): drive download/install/restart from backend to avoid hang (#4074)
    * fix(updater): drive download/install/restart from backend to avoid hang
    
    The 3.16/3.16.1 update flow was frontend-driven: downloadAndInstall()
    then relaunchApp(). relaunch() routed through AppHandle::restart(), and
    the old WebView had to keep running JS after the .app bundle was already
    swapped — an unstable window that could hang the update or leave the old
    version running until a manual restart.
    
    Move the whole download -> install -> cleanup -> restart chain into a new
    backend command install_update_and_restart, so it no longer depends on the
    old WebView running JS after the bundle is swapped.
    
    Platform-aware install ordering (install() behaves differently per OS):
    - Windows: install() launches the external installer and exits the process
      internally, so cleanup + single-instance destroy must run before install.
      Surface a recovery hint on failure since the proxy may already be stopped.
    - macOS/Linux: install() returns, so install first then cleanup — an install
      failure no longer wrongly stops the proxy / reverts takeover.
    
    Eliminate the restart vs single-instance race: restart_process() destroys
    the single-instance lock (remove socket on macOS, ReleaseMutex on Windows)
    before tauri::process::restart(), so the freshly spawned process can't
    connect to the old listener and exit itself.
    
    Also remove the now-dead frontend update plumbing (relaunchApp,
    UpdateHandle, mapUpdateHandle) and surface backend errors in the toast.
    
    Adapted from the original af4271f4 while rebasing onto #4069: the
    ExitRequested handler changes were dropped entirely — the classifier from
    #4069 already routes RESTART_EXIT_CODE to Tauri's default restart flow,
    and the original should_restart branch (prevent_exit + async cleanup)
    would have reintroduced the window-state deadlock that #4069 fixed.
    install_update_and_restart bypasses ExitRequested entirely, so the two
    fixes compose cleanly.
    
    * fix(updater): clear tray icon on the direct-restart update path
    
    restart_process re-execs via tauri::process::restart (spawn + exit(0)),
    which skips Tauri's internal cleanup_before_exit and all RunEvent::Exit
    plugin hooks. Window state, proxy/live restore and the single-instance
    lock were already compensated explicitly; the tray icon was not.
    
    On macOS/Linux the OS drops the status item when the process dies, so
    the gap there was cosmetic at most. The real residue risk is the
    Windows branch, which never reaches restart_process at all:
    update.install() exits the process inside the updater plugin
    (std::process::exit(0)), bypassing TrayIcon::drop — no NIM_DELETE is
    sent and a stale icon lingers in the shell until hovered, the same
    failure remove_tray_icon_before_exit was originally added for on the
    quit path.
    
    Call remove_tray_icon_before_exit (set_visible(false), proxied to the
    main thread via run_item_main_thread) in restart_process and before
    the Windows install. Deliberately not AppHandle::cleanup_before_exit():
    it drops tray icons on the calling thread, which is not safe off the
    main thread on macOS (NSStatusItem).
  • fix: prevent deadlock when relaunching after in-app update (#4069)
    The updater's relaunch() (and app.restart()) triggers ExitRequested
    with code RESTART_EXIT_CODE, which the handler treated as a regular
    exit: it called api.prevent_exit() and spawned an async cleanup task.
    
    However Tauri silently ignores prevent_exit() for restart requests
    (see ExitRequestApi::prevent_exit docs), so the event loop keeps
    shutting down regardless and fires every plugin's RunEvent::Exit hook.
    Two threads then deadlock:
    
    - the spawned cleanup task runs save_window_state on a tokio worker,
      holding the window-state plugin's internal mutex while querying
      window geometry, which dispatches to the main thread and blocks;
    - the main thread, already inside the plugin's own RunEvent::Exit
      hook, blocks on that same mutex.
    
    The app freezes forever on the restarting screen with the update
    already installed; force-quit + reopen comes back on the new version
    (#3998). Confirmed on macOS by sampling the frozen process: main
    thread parked in tauri_plugin_window_state save_window_state mutex
    lock, tokio worker parked in is_maximized -> mpsc recv.
    
    Fix: classify exit requests (None / restart / user exit) and let
    restart requests fall through untouched to Tauri's default restart
    flow (RunEvent::Exit -> re-exec). On that path window state is saved
    by the plugin's exit hook on the main thread, the tray icon is
    cleaned up by Tauri's internal cleanup_before_exit, and proxy/live
    config restore is unnecessary because the new instance takes over
    immediately. The regular exit path (tray quit) is unchanged.
    
    With the fix, a simulated updater relaunch (request_restart) re-execs
    the new process in under 200ms, 3/3 runs; normal quit still performs
    full cleanup.
    
    Co-authored-by: thisTom <19346741+thisTom@users.noreply.github.com>
  • feat(presets): add Unity2.ai partner provider across seven apps
    Add Unity2.ai, a high-performance AI API relay partner, as a preset for
    Claude, Codex, Gemini, OpenCode, OpenClaw, Claude Desktop, and Hermes.
    Each preset carries the referral signup link as apiKeyUrl.
    
    - Register the unity2 icon via iconUrls (PNG URL import) + metadata
    - Add partnerPromotion copy in zh/en/ja/zh-TW; backfill the missing
      zh-TW ccsub entry
    - List Unity2.ai in the sponsor section of all README locales
    - Codex uses the bare base URL (gateway exposes /responses at root);
      OpenCode/OpenClaw/Hermes use the /v1 chat-completions endpoint with
      gpt-5.5 as the only preset model
    - Trim CCSub OpenCode/OpenClaw/Hermes model lists to gpt-5.5 to match
    - Normalize unity2/ccsub banners to the standard 2.41 aspect ratio
  • fix(proxy): aggregate mislabeled SSE bodies in transform fallback (#2234)
    The Claude/Codex format-transform non-stream branch returned an opaque 422
    "Failed to parse upstream response" whenever a 2xx upstream body was not
    valid JSON. The common case: MaaS gateways force-stream a stream:false
    request and return an SSE body with a non-SSE Content-Type, defeating the
    header-only is_sse() check.
    
    On serde failure, sniff for SSE and aggregate the chunks into a single
    JSON, then run the existing converter so clients still receive a valid
    non-stream response.
    
    - chat_sse_to_response_value: aggregate chat.completion.chunk SSE
      (content / reasoning / refusal / tool_calls / legacy function_call),
      tool_calls index-keyed via BTreeMap to avoid unbounded densification,
      first-wins finish_reason, message-snapshot override, completeness and
      error-event guards; synthesize an id when the upstream omits one
    - responses_sse_to_response_value: process the residual trailing block,
      tolerating truncation and skipping it once a completed event was seen
    - enrich remaining parse failures with content-type / content-encoding /
      body-snippet diagnostics
    - deflate: try zlib (RFC 9110) before raw; keep the content-encoding
      header for unsupported encodings
    - gate zero-usage rows on the Claude transform path
  • feat(provider-form): consolidate codex form into advanced options section
    - Fold local routing toggle, model mapping, reasoning overrides and custom
      User-Agent into a single collapsible advanced section, mirroring the
      Claude form (auto-expands when UA is set or local routing is enabled)
    - Custom User-Agent becomes configurable for native Responses providers;
      it was previously reachable only when openai_chat routing was on
    - Collapsed hint names local routing as the entry point for Chat
      Completions / non-GPT providers
    - Backfill all missing codexConfig keys in zh-TW locale
  • feat(provider-form): custom User-Agent presets dropdown in advanced settings
    Polish the provider-level User-Agent override UI on the Claude and Codex forms.
    
    - Add a shared CustomUserAgentField (label + input + preset dropdown + live
      validation) so both forms stay in sync.
    - Provide curated UA presets (Claude Code / Kilo Code families that pass
      coding-plan UA whitelists per #3671); the first is Claude Code's real
      `claude-cli/x (external, cli)` format. Whitelists gate on the name prefix,
      not the version, so static values stay valid across upgrades.
    - Expose presets via a dropdown to the right of the input (z-[200] so it
      renders above the dialog layers) instead of inline chips.
    - Move the field into the existing advanced/reasoning collapsibles.
    - userAgent.ts mirrors the backend byte rule (reject only control chars;
      non-ASCII is allowed) for a non-blocking inline hint.
    - i18n for all four locales (zh/en/ja/zh-TW).
  • feat(proxy): honor custom User-Agent across stream check and model fetch
    Extract a shared `parse_custom_user_agent` helper in provider.rs returning
    `Result<Option<HeaderValue>>`, and reuse it in the forwarder, stream check,
    and model fetch paths so detection, forwarding, and model listing all apply
    the same provider-level User-Agent. Previously only the forwarder honored it,
    so stream check could fail (or model listing 403) on UA-gated upstreams that
    the proxy itself handled fine.
    
    - stream_check injects the provider's custom UA on the claude/codex paths and
      still skips the GitHub Copilot fingerprint UA.
    - model_fetch service + command and the model-fetch.ts wrapper thread an
      optional UA through to GET /v1/models.
    - runtime callers silently ignore invalid values via `.ok().flatten()`
      (no save-time block, so deeplink imports stay lenient).
  • fix: omit customUserAgent when provider category is official
    Stale custom UA values from non-official presets were persisted even
    after switching to an official preset, silently altering request headers.
  • feat(usage): claude-desktop filter and pricing-model audit display
    - add claude-desktop to AppType/KNOWN_APP_TYPES and the dashboard app
      filter; it was hidden because its rows looked like pure failure
      noise, which was the app_type attribution bug fixed on the backend
    - request detail panel now shows the requested model and the pricing
      model when they differ from the response model, making route-takeover
      bills auditable from the UI
    - locale keys added for zh/en/ja/zh-TW
  • fix(proxy): bill route-takeover traffic by the real upstream model
    The model mapped for takeover (env mapping, Claude Desktop routes,
    Copilot normalization, Codex chat override) was discarded inside the
    forwarder, so usage attribution depended entirely on the upstream
    echoing it back. When the upstream omitted the model or mirrored the
    client alias, kimi/glm tokens were recorded and priced as claude-*
    (roughly 5-25x overstatement).
    
    - capture the final outbound model in forward(), return it via
      ForwardResult, and store it on the request context
    - attribution fallback order is now: upstream echo (empty string
      treated as missing) -> outbound model -> client-requested model
    - 'request' pricing mode anchors to the outbound model instead of the
      pre-mapping client alias; unchanged when no mapping applies
    - persist the resolved pricing_model on every usage row
    - Claude Desktop rows now log app_type "claude-desktop" on streaming
      and transform paths too (was hardcoded "claude", silently dropping
      desktop provider pricing overrides and splitting the cost basis by
      the stream flag); its global pricing defaults inherit the claude
      config since proxy_config only allows claude/codex/gemini rows
  • feat(usage): persist pricing basis and takeover dimensions in storage (schema v11)
    - proxy_request_logs: add pricing_model column recording the basis actually
      used at write time (NULL = pre-v11 rows, '' = unpriced error rows)
    - cost backfill recomputes strictly by the persisted basis; the
      request_model fallback now only applies to placeholder models, so
      real-but-unpriced takeover rows stay at zero cost until pricing is
      added instead of being permanently frozen at the alias's price
    - backfill_missing_usage_costs_for_model can locate rows by pricing_model
    - usage_daily_rollups: rebuild with request_model + pricing_model in the
      primary key so the alias-to-real-model mapping and the pricing basis
      survive the 30-day prune; legacy rows migrate with ''
    - rollup_and_prune backfills costs before pruning: prune is irreversible
      and used to run before the startup backfill, permanently booking
      then-unpriced rows as zero
    - get_model_stats groups by the effective pricing model
      (COALESCE(NULLIF(pricing_model,''), model)) so costs aggregate under
      the model whose prices produced them; response-mode behavior unchanged
  • fix(coding-plan): classify Zhipu quota windows by unit field instead of reset-time order (#3036)
    The Zhipu quota API returns two TOKENS_LIMIT entries whose identity was
    inferred by sorting nextResetTime ascending (nearest = five_hour). In the
    last hours of each weekly cycle the weekly window resets sooner than the
    current 5-hour session window, so the two buckets were swapped exactly
    when users check their weekly quota most.
    
    Classify by the explicit unit field instead (3 = hour window -> five_hour,
    6 = week window -> weekly_limit; same shape on bigmodel.cn and api.z.ai,
    weekly observed with number 7 and 1 so only unit is matched), falling back
    to the old reset-time heuristic when the field is missing.
  • feat(usage): refresh model pricing seed — add Fable 5 + 8 models, fix 28 prices
    Full audit of seed_model_pricing against current official vendor pricing.
    
    New models: claude-fable-5 (10/50), grok-4.3, step-3.7-flash,
    mistral-medium-3.5, mistral-small-4, devstral-small-2-2512, magistral-small,
    qwen3.7-max, qwen3.7-plus.
    
    Price fixes (Chinese vendors standardized on official list price, CNY/~7.14):
    - GLM 4.6/4.7 -> Z.ai official 0.6/2.2/0.11 (were reseller/OpenRouter rates)
    - Grok 4.20 reasoning/non-reasoning -> 1.25/2.50 (xAI price cut)
    - MiMo v2.5 / v2.5-pro / v2-pro -> post-2026-05-27 rates + cache
    - Doubao Seed 2.0 lite corrected + cache-hit prices across the family
    - Kimi k2.5 output 3.00, MiniMax m2.5 input 0.15, Mistral devstral-2 output 2
    - Qwen 3.5/3.6-plus + coder-plus/flash cache_read (official 20%-of-input rule)
    
    Each fix updates the seed value (fresh installs) and adds an old->new guard to
    repair_current_model_pricing (existing DBs; won't clobber user-edited rows).
  • feat(usage): app-aware hero icon and neutral Codex theme
    - Replace the fixed Zap glyph in the usage hero with the selected app's
      brand icon via a new AppGlyph component, reusing APP_ICON_MAP
      (cloneElement scales 14px -> 20px); falls back to Zap for the "all" view.
    - Recolor the Codex title theme from emerald to neutral gray to match
      OpenAI's monochrome branding. neutral-500/10 stays visible in both
      light and dark modes, unlike a flat black tint.
  • fix(proxy): extend image rectifier to Codex /responses text-only path
    Codex /responses requests routed to text-only OpenAI-chat upstreams
    (e.g. DeepSeek deepseek-v4-flash) failed with HTTP 400 "unknown variant
    image_url" when images were sent: the responses->chat conversion turns
    input_image items into image_url blocks the model rejects. The media
    rectifier previously covered only the Claude adapter, so neither the
    proactive strip nor the reactive retry fired for Codex.
    
    - media_retry_should_trigger: accept "Codex" adapter, not just "Claude"
    - contains_image_blocks / replace_images: also scan responses `input`
      (input_image) in addition to chat `messages`
    - is_image_block_type: match image | image_url | input_image
    - is_unsupported_image_error: add "unknown variant" hint for the
      deserialize error
    - forward(): proactively run apply_media_prevention for Codex after the
      responses->chat conversion
    
    Proactively strips images for known text-only models (heuristic on by
    default) and reactively retries with images replaced on upstream
    image-unsupported errors. Adds tests for chat image_url, codex
    input_image, the reactive trigger, and the deserialize error match.
  • fix(proxy): exclude cache_read and cache_creation from input on Claude←OpenAI paths
    Builds on #2774 (which fixed cache_read for the streaming openai_chat path).
    Two gaps remained, both double-counting cache tokens when a Claude client
    meters as app_type="claude" (input_includes_cache_read=false):
    
    1. cache_read was still added to input on the non-streaming openai_chat path
       (transform.rs openai_to_anthropic) and the whole openai_responses family
       (transform_responses.rs build_anthropic_usage_from_responses, covering the
       non-streaming call site and both streaming_responses call sites).
    
    2. cache_creation was never subtracted on any converted path, including the
       streaming openai_chat path #2774 had already touched. Claude billing treats
       cache_creation as a separate bucket, so an inclusive upstream carrying a
       direct cache_creation_input_tokens field billed it twice.
    
    All four metering points now compute:
      input = prompt_tokens - cache_read - cache_creation
    restoring the invariant input + cache_read + cache_creation == prompt_tokens.
    Pure OpenAI upstreams are unaffected (no cache_creation concept/field).
    
    Tests: update direct-cache assertions (40->20), add a streaming conservation
    regression test, and pin prompt<cache underflow (saturating clamp to 0) for all
    three metering functions. cargo test 1573 pass, clippy clean.
    
    Note: fix is forward-only; historical rows are not recomputed (cost is frozen at
    log time and app_type="claude" mixes native + converted rows).
  • fix(proxy): correct usage accounting on format-conversion paths
    Audited all proxy format-conversion paths (Chat<->Message, Chat<->Response,
    Gemini<->Message) for usage/cache metering. Five issues found and fixed.
    The dedup mechanism (request_id PK, proxy/session source isolation) is
    untouched, so no double-counting is introduced.
    
    - A (Claude + openai_chat, streaming): inject stream_options.include_usage
      so OpenAI-compatible upstreams emit usage in the SSE tail. Without it the
      converted Anthropic message_delta was all-zero and the whole request's
      input/output/cache was dropped. Same root cause as the already-fixed
      Codex Chat path; the injection is extracted into a shared helper
      (transform::inject_openai_stream_include_usage) reused by both paths.
    
    - C (Claude + gemini_native): subtract cachedContentTokenCount from
      input_tokens in build_anthropic_usage so input becomes fresh input
      (Anthropic semantics). Previously the cache-hit tokens were billed twice
      because this path meters as app_type="claude" (input_includes_cache_read
      = false) while Gemini's promptTokenCount includes the cache.
    
    - D (Codex + openai_chat, streaming): gate log_usage on
      has_billable_tokens() to skip the synthetic all-zero usage the converter
      emits when a non-compliant upstream omits usage, preventing empty-row
      request-count inflation.
    
    - P2 (from_claude_stream_events): use has_billable_tokens() for the return
      gate instead of input>0||output>0, so a fully-cached streamed request
      (cache_read>0, input==output==0) is still recorded. Affects all
      Claude-streaming paths, not just Gemini.
    
    - P3 (Codex Chat->Responses, non-streaming): apply the same
      has_billable_tokens() filter the streaming branch got, since the
      synthesized all-zero usage makes from_codex_response return Some and
      bypass the `if let Some` guard.
    
    Add TokenUsage::has_billable_tokens() as the unified predicate. New tests
    cover include_usage injection, gemini input subtraction, the gate itself,
    cache-only stream recording, and synthetic all-zero codex usage.
    Full lib suite: 1569 passed.
  • fix(usage): import billable session messages without stop_reason
    The local session-log scanner dropped any assistant message that lacked
    a stop_reason or had output_tokens==0. Claude Code Workflow / sub-agent
    fan-out frequently produces messages that only wrote a message_start
    snapshot (output=1, stop_reason=None) without a final block, yet their
    input + cache_read + cache_creation tokens are already billed by
    Anthropic (charged once the request is accepted). Dropping them
    under-counted usage by ~4.1% overall, 92% concentrated in
    workflow/subagent transcripts.
    
    Replace the stop_reason/output gate with a billable-token check (any of
    input/output/cache_read/cache_creation > 0). The per-message-id dedup
    selection is unchanged, and request_id = "session:"+msg_id PRIMARY KEY
    with INSERT OR IGNORE keeps each message single-inserted, so relaxing
    the gate cannot double-count. Add a regression test covering a
    stop_reason-less message with real cache cost plus an all-zero skip.
    
    This is the parser-layer half of the Workflow under-counting fixed at
    the collector layer in 8d332925.
  • fix(usage): count Claude Code Workflow sub-agent token usage
    collect_jsonl_files only walked <project>/<session>/subagents/*.jsonl,
    so it missed Workflow sub-agent transcripts which live one level deeper
    at subagents/workflows/wf_*/agent-*.jsonl. As a result all Workflow
    token usage was invisible to the no-proxy session-log accounting.
    
    Descend into subagents/workflows/wf_*/ as well, via a new
    push_jsonl_children helper that keeps the fixed-depth, no-recursion
    design. journal.jsonl carries no assistant rows so it is skipped at
    parse time and needs no filename special-casing. Existing dedup
    (request_id PK + INSERT OR IGNORE + should_skip_session_insert) keeps
    the next sync's backfill idempotent.
    
    Add test_collect_jsonl_files_includes_workflow_subagents.
  • fix: usage script provider credential resolution (#1479)
    The JS-script usage path resolved {{apiKey}}/{{baseUrl}} with env-only
    field guessing, so apps that store credentials elsewhere (Codex:
    auth.OPENAI_API_KEY + config.toml base_url) always got empty values and
    custom-template queries failed despite a fully configured provider.
    
    - query_usage / test_usage_script now delegate to
      Provider::resolve_usage_credentials, the same per-app resolver used by
      the native balance/coding-plan path and mirrored by the frontend
      getProviderCredentials; explicit non-empty script values still win
    - test_usage_script loads the provider and applies the same fallback,
      so testing matches what a saved script does
    - the custom-template variable preview shows the effective values
      (script overrides first, then provider config) instead of always
      showing provider credentials
    - extract_codex_base_url documents and test-locks the frontend-mirror
      invariant: non-active [model_providers.*] sections are never read
    
    Reworked from the original patch to reuse the existing resolver instead
    of duplicating per-app extraction.
    
    Co-authored-by: Jason <farion1231@gmail.com>
  • fix: prevent duplicate YAML keys in Hermes config (#3267)
    * fix: prevent duplicate YAML keys in Hermes config
    
    Three changes in hermes_config.rs:
    1. deduplicate_top_level_keys() - scan and remove duplicate top-level
       keys before YAML parsing, preventing "duplicate entry" parse errors
    2. remove_all_sections() - helper to strip all occurrences of a given
       top-level key from raw YAML text
    3. replace_yaml_section() now calls remove_all_sections() on the
       remainder after replacing the primary occurrence, preventing
       duplicate sections from accumulating on repeated writes
    
    Fixes the issue where mcp_servers (or any top-level key) gets
    duplicated in config.yaml, causing "Failed to parse Hermes config
    as YAML: duplicate entry with key" errors.
    
    Co-Authored-By: que3sui <204201112+que3sui@users.noreply.github.com>
    
    * fix: handle CRLF and LF line endings in top-level key deduplication
    
    is_top_level_key_line only accepted empty, space, or tab after the colon,
    but deduplicate_top_level_keys uses split_inclusive('\n'), so lines end
    with \n (LF) or \r\n (CRLF). Without accepting \r and \n as valid
    post-colon characters, the dedup safety net never activates.
    
    Add \r and \n checks to is_top_level_key_line, and three tests covering
    LF, CRLF, and first-occurrence preservation.
    
    Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
    
    * refactor(hermes): keep last occurrence when healing duplicate YAML keys
    
    Reworks the healing layers on top of the CRLF root-cause fix:
    
    - deduplicate_top_level_keys: keep the LAST occurrence of each duplicated
      key instead of the first. Duplicates come from section replacement
      degrading into appends (#3633), so the last block is the newest data --
      and Hermes itself reads the config with PyYAML, whose duplicate-key
      semantics are last-wins. Keeping the first occurrence would silently
      roll users back to stale config and diverge from what Hermes runs with.
      Healthy files take a fast path and are returned untouched.
    - Drop the unused dup_key variable (fails cargo clippy -- -D warnings,
      which CI enforces).
    - replace_yaml_section: clean residual duplicate sections from the
      remainder via remove_all_sections; values come from the keep-last
      healed read, so dropping all stale on-disk copies loses nothing.
    - Add regression tests for the actual root cause (find/replace on CRLF
      input must replace in place, not append), keep-last semantics,
      identity on healthy files, end-to-end heal-then-parse, and duplicate
      cleanup on write.
    
    Fixes #3633 #2973 #2529 #3310 #3762
    
    ---------
    
    Co-authored-by: que3sui <204201112+que3sui@users.noreply.github.com>
    Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
    Co-authored-by: Jason <farion1231@gmail.com>
  • 修复 Completions转Anthropic时不记录实际返回模型、Input token记录错误问题 (#2774)
    * fix(proxy): 修复completions转claude格式流式响应未记录实际命中模型
    
    * style: cargo fmt fix
    
    * fix(proxy): 修复completions转claude格式时input与cache_read重复计费
    
    * fix(proxy): 修复完全缓存命中时input_tokens计算错误
    
    * test: 更新input_tokens期望值匹配去重逻辑
  • fix(presets): add Kimi affiliate links (#3809)
    Problem: Kimi and Moonshot preset links were user-clickable without the cc-switch affiliate query.\n\nDecision: Update only UI-facing preset website/API-key links and leave API request endpoints untouched.\n\nChange: Add aff=cc-switch to Kimi/Moonshot websiteUrl values and Codex/OpenCode API-key links.
    
    Co-authored-by: xumingyuan <xumingyuan@msh.team>
  • refactor(presets): align CCSub to end of partner block across apps
    Move the CCSub preset to sit right after DouBaoSeed, at the end of the
    partner block and before the first non-partner provider, so its position
    is consistent across all six apps:
    
    - Codex / OpenCode: moved up from the 2nd slot (between Shengsuanyun and
      the next partner) to the block tail
    - OpenClaw / Hermes: moved up from the aggregator section to the block tail
    - Claude / Claude Desktop: already at the block tail
    
    Also add the missing CHANGELOG entry for the CCSub preset, and drop the
    provider preset order test that enforced a now-unneeded ordering invariant.
  • feat(presets): add CCSub provider across six apps
    Add CCSub, a multi-model aggregator partner, as a preset for Claude, Codex, OpenCode, OpenClaw, Claude Desktop, and Hermes. Each preset carries the referral signup link as apiKeyUrl.
    
    - Register the ccsub icon via iconUrls (1.1MB SVG URL import) + metadata
    - Add partnerPromotion copy in zh/en/ja
    - List CCSub in the sponsor section of all README locales
    - Use gpt-5.5 and gemini-3.1-pro as the OpenAI/Gemini model ids
  • chore(release): prepare v3.16.2
    Add the v3.16.2 CHANGELOG entry covering the 41 commits since v3.16.1,
    bump the version across package.json, tauri.conf.json, Cargo.toml, and
    Cargo.lock, and add trilingual (zh/en/ja) release notes.
  • fix(proxy): strip cache_control from OpenAI format conversion (#3841)
    * fix(proxy): strip cache_control from OpenAI format conversion (#3805)
    
    - Remove cache_control passthrough from system messages, text blocks,
      and tools to prevent 400 errors on strict OpenAI-compatible endpoints
    - Always simplify single text block content to plain string format
    - Fixes two format conversion bugs reported in issue #3805
    
    * fix(proxy): apply cargo fmt to fix CI formatting check