Commit Graph

278 Commits

Author SHA1 Message Date
Jason 2683af57cb Add Codex auth preservation setting 2026-05-30 14:13:58 +08:00
Jason 3a15420770 Update default models and pricing across presets
Bump default model names project-wide: gpt-5.4 -> gpt-5.5,
gemini-3.x -> gemini-3.5-flash, glm-5 -> glm-5.1, and
grok-code-fast-1 -> grok-build-0.1 across all provider presets
(claude, codex, gemini, hermes, openclaw, opencode, universal),
Gemini config, and stream check defaults.

Pricing:
- Seed gemini-3.5-flash, gemini-3.1-flash-lite, step-3.5-flash-2603,
  doubao-seed-2.0-code, mimo-v2.5(/pro), qwen3-coder-480b, grok-build-0.1.
- Correct deepseek-v4-flash/pro, glm-5/5.1, grok pricing.
- Add repair_current_model_pricing: idempotent pass that fixes only
  rows still equal to the outdated built-in values, preserving any
  user-customized prices (seed uses INSERT OR IGNORE and cannot update
  existing rows).

Fixes from review:
- opencode: drop duplicate gemini-3.5-flash variant (unreachable via
  .find), keep the entry with the full minimal/low/medium/high set.
- Align stale display names/costs to gemini-3.5-flash (hermes, openclaw,
  opencode); openclaw cost -> {1.5, 9, 0.15} to match seed.
- i18n (zh/en/ja/zh-TW): refresh OMO category tooltips for new model
  names; fix writing tooltip to Kimi K2.5 to match its recommended.

Update tests accordingly and add a regression test asserting unique
model ids in the Google opencode preset variants.
2026-05-29 22:52:25 +08:00
Jason 0877b9e35d Upgrade default Claude Opus model to 4.8
Bump the default Opus route/model from claude-opus-4-7 to claude-opus-4-8
across provider presets (claude, claudeDesktop, hermes, openclaw, opencode,
universal), i18n locales (zh/en/ja/zh-TW), pricing seed data, and the
user-manual docs.

- Add claude-opus-4-8 pricing row ($5/$25/$0.50/$6.25); keep the 4-7 row
  for historical usage stats (seeded via INSERT OR IGNORE).
- Claude Desktop proxy: accept bidirectional opus 4-7 <-> 4-8 route alias
  during rollout so previously saved routes keep resolving.
- thinking_optimizer: route opus-4-8 through adaptive thinking and normalize
  dotted model ids (also fixes dotted 4-6/4-7 falling back to legacy).
- usage_stats: normalize Bedrock/Vertex/aggregator opus-4-8 ids to base
  pricing.

Also merge role:"system" messages into the Gemini systemInstruction in the
Anthropic->Gemini transform.
2026-05-29 22:52:25 +08:00
in30mn1a bc1467db8d feat(usage): real-time stats refresh + fix codex sync panic on non-ASCII model names (#3027)
The usage dashboard previously only refreshed on app restart for users
who don't route through the cc-switch proxy. Two issues were involved:

1. The session-sync background task panicked when a Codex model name
   contained non-ASCII characters (e.g. `【官】glm-5.1`), because
   `normalize_codex_model` sliced `&name[name.len() - 11..]` without
   verifying char boundaries. Once the task panicked, no session logs
   were imported until the app was restarted (where startup-time
   `rollup_and_prune` happened to flush pending data).

2. Even with sync working, the dashboard only polled every 30s and
   skipped polling when the window was unfocused, so freshly-imported
   data was invisible until the next poll or window refocus.

Fixes
-----

* `normalize_codex_model`: guard the 11-byte ISO-date suffix slice with
  `is_char_boundary` + `is_ascii` checks. ASCII-only suffix means the
  date-stripping logic is correct, and non-ASCII names (which can never
  be valid date suffixes anyway) now bypass the slice safely.

* New `usage_events` module that emits `usage-log-recorded` to the
  frontend whenever `proxy_request_logs` actually gains a new row.
  Sources covered: proxy `log_request`, Claude/Codex/Gemini session
  sync, and startup `rollup_and_prune`. Notifications use a global
  `OnceLock<AppHandle>` so call sites that don't already hold an
  `AppHandle` (e.g. `UsageLogger`) can notify without signature churn.

* 200ms debounce in `notify_log_recorded` collapses bursts (a single
  Codex sync importing 3000+ entries triggers ~2 emits, not 3000) so
  the frontend's `invalidateQueries` is never spammed.

* Frontend `useUsageEventBridge` listens for the event and invalidates
  `usageKeys.all`. Hook is mounted only on `UsageDashboard`, so the
  listener is unsubscribed automatically when the user navigates away.

Verification
------------

* `cargo check` passes (existing 25 dead-code warnings in
  `commands/misc.rs` are pre-existing and unrelated).
* `tsc --noEmit` passes.
* Manually verified end-to-end: a Codex sync run that imported 3145
  entries produced 2 debounced emits, both logged as `emit
  usage-log-recorded 成功`, and the dashboard updated within ~200ms.

Behaviour notes
---------------

* `INSERT OR IGNORE` paths (Claude/Codex session sync) only notify when
  the row is actually inserted, so dedup-skipped writes don't trigger
  empty refreshes.
* Gemini's `INSERT … ON CONFLICT … DO UPDATE` path reuses the existing
  `conn.changes() > 0` check and only notifies when token counts truly
  changed.
* `rollup_and_prune` notifies once per pruning cycle (at most once per
  app start) so the dashboard reflects the new aggregate state.

Co-authored-by: in30mn1a <in30mn1a@users.noreply.github.com>
2026-05-29 22:47:51 +08:00
Jason a2ac21d0a6 refactor(codex): stop force-rewriting user's model_provider field in live config
The one-time history migration already consolidates legacy provider IDs
into a single bucket; there is no need to normalize on every write.
This preserves user-chosen provider identities through the full
write/backup/restore cycle.
2026-05-28 11:39:54 +08:00
MelorTang 05ba28016c fix: sync Claude Desktop profile during proxy takeover (#3157)
* fix: sync Claude Desktop profile during proxy takeover

* fix(provider): skip Claude Desktop backup refresh during takeover

- Route Claude Desktop takeover updates directly through the 3P profile writer.
- Keep takeover startup backup state unchanged when provider metadata changes.
- Narrow platform-specific test helpers and environment setup with cfg gates.

* fix(provider): restore PathBuf import for CI tests

- Restore an unconditional PathBuf import for provider tests.

- Keep Linux cargo test builds compiling while preserving Claude Desktop cfg-gated helpers.

---------

Co-authored-by: Jason <farion1231@gmail.com>
2026-05-26 23:01:33 +08:00
Jason 177eef6682 fix(quota): sort ZhiPu tiers so missing nextResetTime maps to five_hour bucket
When the 5-hour bucket is at 0% utilization, ZhiPu's API omits
nextResetTime. The old i64::MAX sentinel sorted these entries last,
causing the weekly bucket to incorrectly claim the five_hour slot.
2026-05-25 22:20:34 +08:00
Jason 95f2dd4126 feat(codex): preserve OAuth login state during third-party provider switching
Codex provider switches now only write config.toml for third-party providers,
injecting the API key as experimental_bearer_token. The user's auth.json
(ChatGPT OAuth tokens) is preserved. Official providers with login material
still write auth.json normally. Backfill restores bearer tokens into stored
provider auth.OPENAI_API_KEY to maintain canonical shape.
2026-05-25 22:20:34 +08:00
Jason e25682d345 fix(proxy): source takeover model fields from target provider on managed accounts
When a managed-account provider (GitHub Copilot or Codex OAuth) is taken
over by the local proxy, the *_MODEL_NAME entries written to Claude live
config used to be snapshotted from the live file on disk, which still
held the *previous* provider's values during a switch. The result was a
stale display name lingering after the switch.

Read the model snapshot from the target provider's effective settings
(common-config merged) when the provider uses managed-account auth;
preserve the existing behavior of reading from the live config for
normal providers. The three takeover entry points now resolve the
effective provider up front so common-config writes flow through.
2026-05-25 22:20:34 +08:00
Jason 72bc912e0d feat: route Codex Chat providers through Stream Check
Codex Chat providers (apiFormat=openai_chat, e.g. DeepSeek, MiniMax, Kimi)
were incorrectly probed via /v1/responses by Stream Check, which the upstream
rejects. Detect chat routing via codex_provider_uses_chat_completions and
issue the probe against /chat/completions with a Chat-shaped body.

Also align URL fallback order with the production CodexAdapter::build_url:
origin-only base URLs (https://api.deepseek.com) must hit /v1/<endpoint>
first; bare /<endpoint> only as a fallback. The previous order made any
non-404 error on the bare path (401/400/405) flag the provider as down even
though /v1/<endpoint> would have succeeded.

- Lift origin-only detection into proxy::providers::is_origin_only_url so
  both build_url and Stream Check share a single source of truth.
- Collapse resolve_codex_stream_urls and resolve_codex_chat_stream_urls into
  a generic resolve_codex_endpoint_urls(base_url, is_full_url, endpoint),
  which also gives the Responses path a symmetric "full endpoint without
  is_full_url flag" fallback for free.
- Restrict reasoning_effort propagation in the Chat branch to OpenAI o-series
  models, mirroring transform_codex_chat's runtime check.
2026-05-25 22:20:33 +08:00
Jason ad8bdf16ae fix: preserve Codex model catalog on live read of active provider
Editing the currently active Codex provider triggered `read_live_settings`,
which returned only { auth, config }, omitting `modelCatalog`. The form's
mapping table then initialized to empty, and a subsequent save wiped the
DB's `modelCatalog` field — silently destroying user-configured model
mappings after every CC Switch restart.

The mappings already live on disk as a projection in
`~/.codex/cc-switch-model-catalog.json` (pointed at by `config.toml`'s
`model_catalog_json`). Reverse-parse that file so live reads return the
same shape the save path writes.

- Add `read_codex_model_catalog_simplified_from_live` to recover
  `{ model, displayName?, contextWindow? }` entries from the catalog file.
- Skip user-managed external catalogs (filename != cc-switch-model-catalog.json)
  so we don't downgrade their richer structure to the simplified table.
- Squash display_name == slug and context_window == default (config's
  `model_context_window`, or 128_000 fallback) so blank inputs round-trip
  back to blank instead of materializing fallback values in the UI.
- Collapse all failure modes (missing file, parse error, no matching
  field) to Ok(None) so the editor stays openable when the projection
  file is absent or corrupt.
- Wire the new function into `read_live_settings`'s Codex branch.
- Cover the new pure helpers with 7 unit tests in codex_config::tests.
2026-05-25 22:20:33 +08:00
Jason b44f83f7c5 feat: unify Codex third-party providers into stable "custom" history bucket
Codex filters resume history by `model_provider`, so switching between
provider-specific ids like `rightcode` and `aihubmix` made past sessions
appear to vanish. Collapse all third-party providers into a single
stable bucket so cross-switch history stays visible.

- Normalize live `model_provider` to "custom" on every Codex write
  (reserved built-in ids like openai/ollama are preserved).
- Add device-level one-shot migration that rewrites historical JSONL
  session files and the `state_5.sqlite` threads table from legacy
  provider ids into the "custom" bucket. Backs up originals under
  `~/.cc-switch/backups/codex-history-provider-migration-v1/` and uses
  the SQLite Backup API for the state DB.
- Record completion in `settings.json` under `localMigrations` so the
  migration is strictly idempotent across launches.
- Update Codex provider preset templates to emit `model_provider = "custom"`
  out of the box.
2026-05-25 22:20:33 +08:00
Jason 791ced0034 fix: Codex model catalog WYSIWYG and config consolidation
- Remove mergeCodexDefaultCatalogModelForSave implicit injection (P1)
  The model mapping table is now the single source of truth; no hidden
  entries are prepended on save.

- Sync first catalog row model into config.toml on save
  Ensures Codex default request model matches the table's first entry
  instead of retaining a stale template value.

- Remove API Format selector from CodexFormFields (P3)
  wire_api is always 'responses'; the selector confused users into
  thinking they were changing the upstream protocol. Only the 'Needs
  Local Routing' toggle remains.

- Add restart hint to model mapping i18n text (P2)
  model_catalog_json is loaded at Codex startup; users are now informed
  that a restart is needed after changes.

- Unify write_codex_live_with_catalog helper (P4)
  Replaces three scattered prepare+write call sites in config.rs,
  provider/live.rs, and proxy.rs with a single entry point.

- Clean up useCodexConfigState dead state (P3 follow-up)
  Remove codexModelName, codexContextWindow, codexAutoCompactLimit and
  their handlers/effects since no component consumes them after the UI
  consolidation.
2026-05-25 22:20:33 +08:00
Jason 74acf1e387 Add Codex Chat-to-Responses bridge
- Add Codex provider API format selection and model mapping for Chat-only upstreams.
- Convert Codex Responses requests to Chat Completions and rebuild Chat responses as Responses output.
- Preserve reasoning_content across non-streaming, streaming, tool calls, and previous_response_id follow-ups.
- Add a bounded Codex Chat history cache for restoring tool calls before tool outputs.
- Cover Chat bridge, compact routing, streaming, and history recovery with focused tests.
2026-05-25 22:20:33 +08:00
Jason 1c82b8a3fa Add Chat Completions routing for Codex providers
- Add a Codex API format selector and routing badge for Chat Completions providers.
- Convert Codex Responses requests to upstream Chat Completions when routing is required.
- Convert Chat Completions JSON and SSE responses back to Responses format.
- Keep generated Codex wire_api values on Responses for Codex compatibility.
- Add i18n labels, provider metadata handling, and focused conversion tests.
2026-05-19 10:23:48 +08:00
Jason 61e68d754c fix(proxy): inject only ANTHROPIC_API_KEY for managed-account Claude takeover
- Provider: add uses_managed_account_auth / is_github_copilot helpers
  to identify managed-account providers (GitHub Copilot / Codex OAuth)
- ProxyService: choose auth policy by provider type when taking over
  Claude Live config. Managed accounts drop token env keys and write
  only the ANTHROPIC_API_KEY placeholder; other providers keep the
  existing ANTHROPIC_AUTH_TOKEN fallback behavior
- Forwarder: add outbound guard that refuses to send the PROXY_MANAGED
  placeholder upstream to *.githubcopilot.com and chatgpt.com
  /backend-api/codex
- Add unit tests covering detection, injection, and the outbound guard
2026-05-18 22:46:18 +08:00
Roger Deng 0977dcd1c1 fix skill sync copy fallback (#2791) 2026-05-18 11:40:45 +08:00
LaoYueHanNi bbce75fcda fix(session): 修复session log模式下子Agent token统计遗漏 (#2821)
* fix(session): 修复session log模式下子Agent token统计遗漏

collect_jsonl_files() 只扫描了两层目录,遗漏了子Agent的JSONL日志文件,
导致子Agent的独立token使用数据完全未统计到session费用中。
(仅影响session log模式,proxy代理模式不受影响)

* refactor(session): optimize collect_jsonl_files logic

- Replace two independent if statements with if-else for mutually exclusive conditions
- Remove unnecessary clone() when pushing file paths
- Add clarifying comments for main session vs subagent files
- Apply cargo fmt for consistent formatting

Performance improvement: Eliminates redundant clone() operations when
processing .jsonl files, as a path cannot be both a file and a directory.

---------

Co-authored-by: Jason <farion1231@gmail.com>
2026-05-18 10:00:57 +08:00
Jason b642ef0633 fix(failover): patch P1-P3 reliability gaps surfaced by team review
- Forwarder buffers non-streaming bodies and primes streaming first
  chunk before signaling success, so body timeouts and SSE first-chunk
  failures route through the circuit breaker instead of being recorded
  as success on response-header arrival
- Atomic enable-failover: switch to P1 before persisting the flag, and
  roll back auto-added queue entries when the switch is rejected
  (e.g. official providers)
- Hot-reload circuit breaker config on per-app proxy config change
  instead of waiting for a proxy restart
- FailoverToggle / FailoverQueueManager / AutoFailoverConfigPanel
  require proxy takeover for the active app; the backend command also
  rejects enabling when takeover is off
- ProviderHealthBadge consumes the backend is_healthy flag instead of
  hardcoding the 5-failure threshold

Cleanup:
- impl From<&AppProxyConfig> for CircuitBreakerConfig and use it from
  the command layer
- Collapse three identical TabsContent blocks into a single map
2026-05-14 21:35:02 +08:00
Jason 73bc4eb65d 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.
2026-05-14 15:03:31 +08:00
Jason f93b935d5f 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.
2026-05-14 12:16:00 +08:00
Jason 402570ce31 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
2026-05-14 11:53:51 +08:00
Jason 206125b4e3 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
2026-05-14 09:23:21 +08:00
Jason c3d810a22b 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.
2026-05-13 23:12:15 +08:00
Jason 58648a9c53 chore: drop trailing blank line in sql_helpers tests
Rustfmt cleanup, no behavioral change.
2026-05-13 23:11:52 +08:00
Jason aa5e58d060 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.
2026-05-13 17:51:35 +08:00
Jason edf28b6422 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.
2026-05-13 10:27:29 +08:00
Jason 83f4e1d0ad 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.
2026-05-08 22:34:46 +08:00
Jason 8b3ad9caf9 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.
2026-05-08 22:34:46 +08:00
Dhruv_S b05be92aa1 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
2026-05-08 08:41:50 +08:00
Jason bdc4c1e8b8 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
2026-05-01 23:07:46 +08:00
Jason 518d945eb8 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.
2026-05-01 23:07:46 +08:00
xpfo 295dd9a944 fix(proxy): preserve Vertex AI full URLs (#2415) 2026-05-01 17:46:10 +08:00
jimmy db66348ff8 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
2026-05-01 17:35:18 +08:00
涂瑜 fafc122d82 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.
2026-04-30 22:38:34 +08:00
ayxwi 7965862e66 Make import existing side-effect free (#2429)
Co-authored-by: Xvvln <3369759202@qq.com>
2026-04-30 21:59:47 +08:00
tison 151af0ffa3 Fix log message for session usage codex (#2473)
* Fix log message for session usage codex

* Fix comments in session_usage_codex.rs
2026-04-30 11:07:06 +08:00
SaladDay a1e6c3b65d 修复 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>
2026-04-30 09:54:25 +08:00
Jason f061b777b7 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
2026-04-29 22:41:46 +08:00
Jason 2ee7cb4101 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.
2026-04-29 22:41:46 +08:00
Jason d2556be5b9 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)".
2026-04-29 22:41:46 +08:00
allen_xln 85f0be9e1d 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>
2026-04-25 09:28:28 +08:00
Jason 67dbfc0a8c 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).
2026-04-24 23:20:10 +08:00
Jason 4737158439 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.
2026-04-23 17:34:39 +08:00
涂瑜 dc04165f18 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>
2026-04-23 16:17:15 +08:00
Coconut-Fish 010b163430 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>
2026-04-23 15:36:03 +08:00
santugege 59735f976b fix(skill): resolve root-level repo skills consistently (#2231)
Co-authored-by: luoyaoqi <luoyaoqi@robam.com>
2026-04-23 12:21:47 +08:00
Jesus Díaz Rivas 10e0772d8c 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>
2026-04-23 12:05:17 +08:00
nmsn 3ad7d0b1cc 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>
2026-04-23 10:38:08 +08:00
Jason 7ac822d241 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.
2026-04-21 23:00:37 +08:00