Let users switch between the built-in OpenAI provider and third-party Codex providers directly from the provider panel while takeover mode remains active.
Centralize the frontend capability predicate so only the fixed codex-official seed receives native-login routing support, and keep copied UUID-based official entries clearly marked as unsupported.
Add an idempotent backend command that recreates the deleted official seed, wire it into the add-provider flow, refresh localized guidance, and add mutation and provider-action regression coverage.
* feat(codex): support native Anthropic Messages protocol as upstream
Allow gateways that only expose the native Anthropic Messages protocol
(/v1/messages) to be used by Codex: the local proxy performs bidirectional
request/response/streaming conversion between Responses and Anthropic.
Backend:
- Add two conversion modules: transform_codex_anthropic / streaming_codex_anthropic
- codex.rs: add routing detection and auth: ANTHROPIC_AUTH_TOKEN→Bearer (default),
ANTHROPIC_API_KEY→x-api-key, mutually exclusive
- handlers.rs: add handle_codex_anthropic_to_responses_transform
- forwarder.rs: support optional Claude Code client fingerprint impersonation
(User-Agent / anthropic-beta / x-app / system prompt first-line injection)
and /responses→/v1/messages rewriting
- codex_config: the anthropic format reuses the NativeResponses profile to strip
custom tools
- ProviderMeta: add impersonateClaudeCode
Frontend:
- CodexApiFormat: add "anthropic"; the form adds auth field selection and an
impersonation toggle
- Add en/ja/zh/zh-TW copy
Robustness:
- Downgrade when tool history / forced tool_choice conflicts with extended
thinking, avoiding upstream 400s
- Emit cache_creation_input_tokens in usage and use saturating_add to guard
against overflow
- Append a unique suffix to non-streaming/streaming output-item ids to avoid
multi-segment text/thinking overwriting each other
* fix(codex): harden Anthropic bridge against empty text blocks and truncated streams
- Drop empty/whitespace-only text content blocks when rebuilding Anthropic
messages from Responses history; Anthropic 400s on empty text blocks (e.g. an
empty assistant text emitted alongside a tool_use), which broke follow-up and
tool-result requests. Also drop messages left without content.
- Do not report a truncated Anthropic stream as completed: when the SSE
connection ends before message_stop with no stop_reason, emit an incomplete
response if partial output exists, or a failed (stream_truncated) response
otherwise, mirroring the chat converter's EOF handling.
* fix(codex): resolve Anthropic-bridge review findings
Blocking:
1. Defer stripping the [1m] long-context marker until after catalog matching and model write-back, re-stripping it on the final Codex→Anthropic body and setting a flag to emit the context-1m-2025-08-07 beta header, so the marker is no longer lost or overridden by the default model.
2. Gate Anthropic thinking on the trailing turn only (via trailing_turn_allows_thinking) instead of scanning full history, so a Codex session that resends history each round no longer permanently loses thinking after the first tool call.
3. Inject 5m ephemeral cache_control on the Codex→Anthropic body by reusing cache_injector (handling the system string→array conversion), so system/tools/history are cached instead of re-sent at full price every round.
4. Add a shared base_url_is_full_endpoint helper (normalizing whitespace/query/fragment/trailing slash) used by both the Anthropic and Chat paths, so a base URL already ending in /v1/messages is treated as a full endpoint instead of double-appending to /v1/messages/v1/messages.
5. Align the catalog tool-profile predicate with the routing predicate so resolve_codex_catalog_tool_profile returns the Anthropic profile whenever the request converts to Anthropic, preventing freeform tools like apply_patch from being silently filtered by a ProxyChat catalog.
6. Explicitly disable native web_search for the Anthropic profile (including the no-catalog branch) via set_codex_native_web_search_field, so Codex no longer treats it as available while the transform silently drops it.
7. Only forward tool_choice when tools survive filtering, dropping it otherwise, to avoid a non-retryable 400 from upstream when tool_choice is sent with no tools.
8. Lower the fallback default to max_tokens=8192 (only when max_output_tokens is omitted) and clamp the thinking budget to max_tokens/2, disabling thinking below the 1024 floor, to avoid hard 400s on low-output-ceiling models/gateways.
Minor:
9. Centralize the Codex/OpenAI fingerprint-header denylist in is_codex_client_fingerprint_header so impersonating Claude Code uniformly drops originator/session_id/conversation_id/chatgpt-account-id/x-client-request-id/openai-* and the x-stainless-*/x-codex-* prefixes.
10. Retain content_block_start.input as start_input and fall back to it at block close when no input_json_delta arrived, so a gateway that carries the full tool input on the start event no longer yields empty tool arguments.
11. Extract a shared codex_responses_sse module as the single Responses SSE envelope builder that both the chat and anthropic streaming emitters delegate to, with byte-for-byte-unchanged wire output, so future event-format fixes touch one place instead of two.
* fix(codex): add per-provider max_output_tokens override for Codex→Anthropic path
Codex does not forward model_max_output_tokens in the request body,
causing the proxy to fall back to a conservative 8192 default. This
truncates long or thinking-heavy responses (stop_reason=max_tokens).
- Add maxOutputTokens field to ProviderMeta (Rust + TypeScript)
- Inject the value into the Anthropic request body before transform,
taking precedence over request-supplied and default values
- Add numeric input in Codex form fields (Anthropic format only)
- Add i18n entries for label, placeholder, and hint (en/zh/zh-TW/ja)
- Include roundtrip and omission unit tests for the new field
* fix(codex): harden Anthropic protocol bridge
* fix(codex): address Anthropic bridge review
* fix(codex): preserve flattened Anthropic inputs
* fix(codex): harden Anthropic recovery paths
* fix(ci): satisfy Rust clippy
---------
Co-authored-by: Jason <farion1231@gmail.com>
import_mcp_from_apps swallowed every importer error with unwrap_or(0),
so a corrupt config.toml surfaced as "imported 0 servers" with no hint
that anything went wrong.
Move the aggregation into McpService::import_from_all_apps: each app
imports best-effort (one bad file doesn't block the rest), and failures
are collected into a single error naming the failing apps alongside the
count that did import. The frontend now refreshes the server list on
settle rather than success, since a partial failure still means new
servers were persisted.
- Add an optional `enabled` flag to useScanUnmanagedSkills; the Skills panel
scans once on mount (enabled:true), with 30s staleTime + keepPreviousData to
dedupe disk IO across navigations.
- App subscribes to the shared query (enabled:false) and renders a green dot +
tooltip on the top-bar Import button when unmanaged skills are available.
The provider panel health check sent a real streaming model request, which many third-party providers block (401/403/WAF), causing false negatives while only stable official endpoints passed. Replace it with a lightweight reachability probe: GET the provider base_url and treat any HTTP response (200/4xx/5xx) as reachable; only DNS/connect/TLS/timeout count as failure. Latency is the probe's TTFB.
Backend (services/stream_check.rs): rewrite ~2200 -> ~350 lines, dropping real-request building, format conversion, auth and API-path resolution while keeping per-app base_url extraction. Defaults: 8s timeout, 1 retry, 1500ms degraded threshold.
Failover invariant: the reachability check must never reset the circuit breaker (reachable != usable; a 403 host is reachable but broken for real traffic). Remove the resetCircuitBreaker call from useStreamCheck; failover failure detection stays driven solely by real proxy traffic (forwarder/circuit_breaker untouched). useResetCircuitBreaker is kept dormant for a future manual-recovery entry.
Open the check to all providers: drop the official/copilot/codex-oauth/third-party gating and the 'sends a real request' confirm dialog. For official providers whose base_url is intentionally empty, fall back to the endpoint the client actually uses (Claude -> api.anthropic.com, Codex -> chatgpt.com/backend-api/codex, Gemini -> generativelanguage). Non-official providers with a missing base_url still error to avoid a false green light. Claude Desktop Official is native 1P mode (talks to claude.ai, cc-switch not in the request path, no reliable endpoint) so its button stays hidden.
Slim StreamCheckConfig and per-provider testConfig to timeout/threshold/retries (drop test model + prompt); sync zh/en/ja/zh-TW. Retain the now-unused anthropic_to_openai/anthropic_to_gemini transform utilities and their test suites.
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).
* Add S3 Cloud Sync design document
Design for adding AWS S3 as a new Cloud Sync backend alongside WebDAV.
Hybrid approach: extract shared sync protocol, add independent S3 transport.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Add S3 cloud sync implementation design (reqwest + Sig V4)
Updated design based on 2026-03-06 draft: switches from rust-s3 crate
to hand-rolled AWS Sig V4 on existing reqwest for broader S3-compatible
service support (AWS, MinIO, R2, Alibaba OSS, Tencent COS, Huawei OBS).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Add S3 cloud sync implementation plan (11 tasks, TDD)
Detailed step-by-step plan covering: sync_protocol extraction, S3 Sig V4
transport, settings, sync/auto-sync modules, Tauri commands, frontend
presets/dynamic form, and i18n.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* deps: add hmac crate for S3 Sig V4 signing
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* refactor: extract sync_protocol.rs from webdav_sync.rs for shared use
Move transport-agnostic sync protocol logic (constants, types, snapshot
building, manifest validation, artifact verification, snapshot application,
utilities) into a new shared sync_protocol module so both WebDAV and the
upcoming S3 transport can reuse it.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: use transport-neutral error keys in sync_protocol
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* feat: add S3 transport layer with AWS Sig V4 signing
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* feat: add S3SyncSettings to AppSettings
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* feat: add S3 sync module with upload/download/fetch
Implements the S3 sync protocol layer (s3_sync.rs) that combines the
shared sync_protocol with the S3 transport. Mirrors the WebDAV sync
module structure with independent sync mutex, connection check,
upload, download, fetch_remote_info, and sync status persistence.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* feat: add S3 auto sync worker with debounce
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* feat: add S3 sync Tauri commands and auto sync worker startup
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* feat: add S3 sync TypeScript types and API layer
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* feat: add S3 sync i18n translations (en/zh/ja)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* feat: add S3 sync presets and dynamic form to sync settings
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: preserve HTTP scheme for S3 custom endpoints (MinIO support)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* test: add live S3 integration tests (env-var driven, --ignored)
Run with: S3_TEST_AK=... S3_TEST_SK=... S3_TEST_BUCKET=... cargo test --lib services::s3::integration_tests -- --ignored
Verifies test_connection, put_object, get_object, head_object, and 404
handling against a real S3 bucket using the project's own Sig V4 signing.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* chore: remove internal design docs before PR
* fix: wire S3 auto-sync to DB hook & sync UI state on async load
- P1: Add s3_auto_sync::notify_db_changed call in SQLite update_hook
so S3 auto-sync worker receives DB change signals (was only wired
for WebDAV, leaving S3 worker idle)
- P2: Add useEffect to update syncType selector when s3Config loads
asynchronously, preventing stale "webdav" default for S3 users
* fix: satisfy clippy for s3 sync
* fix: address s3 sync review feedback
---------
Co-authored-by: Keith (via OpenClaw) <keithyt06@users.noreply.github.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: Jason <farion1231@gmail.com>
Flip preserve_codex_official_auth_on_switch from true to false so
third-party Codex switches overwrite auth.json by default, matching the
expectation that switching providers also swaps credentials. Users who
rely on keeping the ChatGPT login in auth.json while on a third-party
provider (for official plugins / remote login) can enable it in
Settings -> Codex Authentication.
The toggle field ships for the first time here (it is not in v3.16.0),
so no existing settings.json holds an explicit value -- every user lands
on the new default and no migration is required.
Also set the flag explicitly in the preservation unit test instead of
relying on the global default, keeping it valid now that the default is
false.
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>
* Add Traditional Chinese localization
* fix: address zh-TW formatting and token units
- Format `zh-TW.json` with Prettier.
- Use Traditional Chinese `萬` and `億` units for zh-TW token summaries.
- Add usage formatting coverage for Traditional Chinese locale aliases.
---------
Co-authored-by: Jason <farion1231@gmail.com>
* refactor: replace JSON.parse(JSON.stringify()) with structuredClone and extract useTauriEvent hook
Replace all `JSON.parse(JSON.stringify())` deep copy patterns with native
`structuredClone()` across production source (9 occurrences), tests (11
occurrences), and a hand-rolled `deepClone` utility in providerConfigUtils.ts.
Add "ES2022" to tsconfig lib for type support.
Extract a `useTauriEvent` hook to eliminate the repeated Tauri event listener
boilerplate (`useEffect` + `active/disposed` flag + async `listen`) that was
duplicated across App.tsx (3 listeners) and useUsageCacheBridge.ts. The hook
handles async registration, race-condition guards, and cleanup automatically.
* fix: add compatible deepClone helper
- Add a shared deepClone helper with a structuredClone runtime guard and fallback.
- Route clone call sites through the helper.
- Preserve universal-provider-synced listener ordering and drop the dead-directory diff.
* fix: harden Tauri event handling
- Guard WebDAV sync status events against missing payloads.
- Preserve settings query invalidation ordering before showing auto-sync errors.
- Simplify useTauriEvent subscriptions to avoid dependency-driven re-listens.
---------
Co-authored-by: zcb <zhangchongbiao@qiyuanlab.com>
Co-authored-by: Jason <farion1231@gmail.com>
- 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.
- Add active flag pattern to 3 useEffect hooks in App.tsx to prevent
event listener leaks when component unmounts before async setup completes
- Add guard check in useSettings.ts to prevent undefined from being
stored in localStorage when payload.language is missing
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
- 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.
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.
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.
The Hermes config.yaml schema has stabilized and users have migrated to
the current provider fields, so the value of scanning for model.provider
dangling references, custom_providers shape errors, v12 migration residue
etc. no longer justifies the maintenance surface — and the scan produces
false positives when users keep some providers under Hermes' v12+
providers: dict (Hermes' runtime merges both shapes, but CC Switch's
scanner only looked at the list form).
Removes the whole HermesHealthWarning type, scan_hermes_config_health
command, HermesHealthBanner React component, useHermesHealth hook,
warnings field on HermesWriteOutcome, and the three helper functions
(yaml_as_non_empty_str, collect_mapping_string_keys, hermes_warning)
that only served the scanner. Drops the matching i18n keys in
zh/en/ja and the fixInWebUI button label that only the banner used.
* feat: 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>
Closes#2139
Two related defects let the installed-skills count balloon when users
tap the import confirm button multiple times — either deliberately or
because the button is still clickable while a slow import is in flight:
- The confirm button only disabled itself while `selected.size === 0`,
so it stayed clickable during a pending mutation. Each extra click
triggered another `importFromApps` mutation.
- `useImportSkillsFromApps` appended the server response to the
installed cache without deduping, so re-firing the mutation stacked
the same skills into the list again.
Disable the confirm (and cancel) buttons while the mutation is pending
— matching the `isRestoring` / `isDeleting` pattern already used by
`RestoreSkillsDialog` — and merge success payloads by
`InstalledSkill.id` so repeated results overwrite rather than
accumulate.
The merge is extracted as a pure `mergeImportedSkills` reducer to make
the behaviour unit-testable and to short-circuit on an empty payload,
returning the existing reference so React Query does not notify
subscribers about a no-op cache update.
When the Hermes Web UI probe fails, the toolbar entry now opens an info
confirm dialog offering to run `hermes dashboard` in the user's preferred
terminal. Accepting spawns it via a temp bash/batch script; `hermes
dashboard` itself opens the browser once ready, so we do not poll.
The Memory panel and Health banner keep the existing toast behavior.
Also corrects the stale `hermes web` hint in the offline toast (the real
command is `hermes dashboard`) and reorders Linux terminal detection to
try `which` before stat'ing /usr/bin, /bin, /usr/local/bin.
useAutoCompact cached normalWidthRef = el.scrollWidth on every
non-compact resize, but per DOM spec scrollWidth === clientWidth
when content fits. Maximizing the window (content no longer
overflows) therefore wrote the container width into
normalWidthRef, making it impossible to re-enter compact when
the window was restored to its original size.
Move the assignment inside the overflow branch so the cache is
only written at the actual compact threshold, where scrollWidth
reflects the real content width.
Replaces the greyed-out "Memory is disabled" banner with a real Switch
at the top of each memory tab. Users can now toggle Hermes' memory/user
blobs without leaving CC Switch; the underlying write goes through the
merge-aware `set_memory_enabled`, so budgets and external-provider
settings survive toggle operations. The new `useToggleHermesMemoryEnabled`
mutation invalidates the limits query so the Switch state and the
amber disabled-hint update in lockstep.
Reworks the `schema_migrated_v12` health banner copy to match the
simplified "CC Switch only manages custom_providers" posture — it now
tells users to reconcile migrated dict entries via Hermes Web UI,
instead of the earlier (and now inaccurate) "CC Switch reads both".
Adds a dedicated Hermes row to the directory-override settings so users
can point CC Switch at alternate Hermes config locations (e.g. a second
profile directory for work/personal split). `get_config_dir` on the
Rust side already supports hermes; this just wires up the frontend row.
Wiring it through `useDirectorySettings` revealed a scaling problem:
every supported app required five parallel ternary chains across
`computeDefaultConfigDir`, `updateDirectory`, `browseDirectory`,
`resetDirectory`, and `updateDirectoryState`. Replaces those with two
lookup tables (`APP_DIRECTORY_META`, `DIRECTORY_KEY_TO_SETTINGS_FIELD`)
so adding the next app is two entries, not fifteen edit sites.
Drive-by cleanup from the same touch:
* `resetAllDirectories` takes a `ResolvedAppDirectoryOverrides` object
instead of five positional optional strings.
* `setResolvedDirs` returns the same reference when the sanitized
value is unchanged, so no-op edits don't cascade renders.
Also lands all i18n updates for this series (`hermesConfigDir` and
placeholder, Memory section's enable/disable/toggleFailed copy, and
the reworded `schemaMigratedV12` warning) in zh/en/ja together.
Hermes has no slash-prompt concept (templates live as Skills), so the
Prompts tab for the Hermes app was always empty. Swap the toolbar Book
button for a Brain button that opens a new Memory panel editing
~/.hermes/memories/{MEMORY,USER}.md — Hermes' first-class memory store
which its Web UI exposes only as on/off toggles, never as an editor.
The panel shows each file in its own tab with a character-budget bar
read from config.yaml's nested memory.* section (memory_char_limit /
user_char_limit, default 2200 / 1375). Edits are written atomically;
Hermes picks them up on the next session start per MemoryStore.
Also extract useDarkMode to src/hooks/useDarkMode.ts — the codebase
already repeats the same MutationObserver pattern in 12+ places; this
PR introduces the shared hook and uses it once, leaving the migration
of the other copies to a follow-up.
Slim the Hermes surface in CC Switch to match its core positioning —
cross-client provider switching and shared MCP/prompts/skills — and
delegate deep configuration (model, agent, env, skills, cron, logs)
to the Hermes Web UI at http://127.0.0.1:9119.
- Drop AgentPanel/EnvPanel/ModelPanel and their mutation commands,
hooks, types, and i18n keys across zh/en/ja.
- Add open_hermes_web_ui Tauri command that probes /api/status and
launches the URL in the system browser. Hermes injects its own
session token into the returned HTML, so CC Switch doesn't need
to touch auth.
- Surface the launcher from the Hermes toolbar and the health banner
via a shared useOpenHermesWebUI() hook; the offline error code is
defined once per side and referenced across the contract.
- Keep read-only access to model.provider so ProviderList can still
highlight the active supplier; apply_switch_defaults continues to
write the top-level model section when switching providers.
Net diff: +152 / -1253.
Switching a Hermes provider previously only fired a toast because the frontend treated Hermes as non-additive (unlike backend AppType::is_additive_mode, which lists OpenCode | OpenClaw | Hermes) and relied on the unused is_current DB flag for highlighting. Align the UI model with the backend:
- Include Hermes in ProviderActions' isAdditiveMode so the main button switches between "Add" and "Remove".
- Drive the "current" highlight from model.provider (via useHermesModelConfig) instead of the DB is_current field; model.provider is Hermes's real SSOT for the active provider.
- Reuse OpenClaw's set-as-default button slot to expose an "Enable" action on Hermes that calls switchProvider, so providers already in config can be activated without re-adding. switch_normal + apply_switch_defaults already atomically update custom_providers and model.provider, so no backend change is needed.
- Invalidate liveProviderIds + modelConfig + health in parallel after add/update/delete/switch via a new invalidateHermesProviderCaches helper, replacing four copies of three sequential awaits.
- Add "hermes" to AppId union type and all exhaustive Record<AppId>
- Add HermesModelConfig, HermesAgentConfig, HermesEnvConfig types
- Add hermes field to VisibleApps, McpApps, ProxyTakeoverStatus
- Create src/lib/api/hermes.ts with Tauri invoke wrappers
- Create src/hooks/useHermes.ts with 5 query + 3 mutation hooks
- Register hermes in APP_IDS, APP_ICON_MAP (violet color scheme)
- Split MCP_SKILLS_APP_IDS into MCP_APP_IDS (includes hermes) and
SKILLS_APP_IDS (excludes hermes, since Hermes has no Skills support)
- Wire hermes additive-mode into App.tsx (remove/duplicate handlers),
ProviderList.tsx (live provider ID query + In Config badge),
mutations.ts (cache invalidation on switch/add/delete)
- Add Hermes checkbox to McpFormModal
- Add basic hermes i18n keys (en/zh/ja)
The takeover.failed i18n template lacked the {{detail}} placeholder
and three useProxyStatus onError callbacks omitted the detail variable,
so proxy start/stop/takeover failures all displayed a generic message
regardless of the underlying cause.
* fix(claude-plugin): sync current provider config to settings.json on toggle enable
- Extract syncClaudePluginIfChanged to share logic between autoSaveSettings and saveSettings
- Fix P1: enableClaudePluginIntegration toggle in General tab now actually syncs ~/.claude/settings.json
- Fix P2: check syncCurrentProvidersLiveSafe() return value and show toast on failure
- Fix P3: sync providers on both enable and disable, not just enable
- Fix P4: avoid double syncCurrentProvidersLiveSafe when plugin toggle + dir change happen together
- Remove duplicate comment
- Add missing providersApi.getCurrent/getAll mocks in tests
* style: reformat after rebase onto main
Prettier flagged a line-break introduced by the openclaw directory
change (from main) after rebase.
* fix(claude-plugin): read prev enabled state from live cache to avoid stale closure
syncClaudePluginIfChanged compared enabled against data?.enableClaudePluginIntegration
captured in a useCallback closure. After invalidateQueries + refetch, the React
Query cache is up to date, but the consuming hook's closure does not see the new
value until React re-renders. Quick on->off toggles could therefore skip
applyClaudePluginConfig, leaving ~/.claude/config.json in the previously enabled
state even though settings.json was persisted as disabled.
Read the previous value synchronously from queryClient.getQueryData(["settings"])
before saveMutation.mutateAsync(), then pass it to the helper as prevEnabled.
getQueryData bypasses the closure and reflects the live cache at call time.
Test covers the race: closure data stays at false while the cache reports true;
the helper must still call applyClaudePluginConfig({ official: true }).
---------
Co-authored-by: Jason <farion1231@gmail.com>
* chore(stream-check): update default health check models to latest
Replaces deprecated gpt-5.1-codex@low with gpt-5.4@low and switches
the Gemini default from gemini-3-pro-preview to gemini-3-flash-preview
to pick the lightest variant of the latest series for fast, low-cost
health checks.
https://claude.ai/code/session_01NGWLchcTP76rJHjiP5Ehte
* feat(stream-check): detect model-not-found errors with dedicated toast
Health check previously classified failures purely by HTTP status code,
which meant deprecated/invalid models showed up as a generic "Not found
(404)" error pointing users to check the Base URL — misleading when the
URL is fine and only the test model is wrong (e.g. gpt-5.1-codex after
it was retired).
Backend: add detect_error_category() that inspects 4xx response bodies
for model-not-found indicators (model_not_found, does not exist,
invalid model, not_found_error, etc.) and returns a "modelNotFound"
category. Thread the resolved test model through build_stream_check_result
so the failed result carries it in model_used. Add StreamCheckResult
.error_category field (serde-skipped when None).
Frontend: useStreamCheck branches on errorCategory === "modelNotFound"
before the HTTP-status fallback and renders a toast.error with the model
name and a description pointing to Model Test Config. Add i18n keys
(modelNotFound / modelNotFoundHint) for zh/en/ja.
Tests: unit-test detect_error_category against real OpenAI/Anthropic
error shapes, 5xx false-positive avoidance, and plain 401 auth errors.
https://claude.ai/code/session_01NGWLchcTP76rJHjiP5Ehte
* fix(stream-check): add missing error_category field in fallback
The error_category field was added to StreamCheckResult in this branch
but the fallback constructor in stream_check_all_providers was not
updated, which broke cargo build.
---------
Co-authored-by: Claude <noreply@anthropic.com>
Distinguish between "provider rejects probe" (yellow warning) and
"genuinely broken" (red error) in health check results.
Backend: add AppError::HttpStatus variant to carry structured HTTP
status codes, populate http_status on error results, classify codes
into short labels (e.g. "Auth rejected (401)"), and truncate overly
long response bodies.
Frontend: route 401/403/400/429/5xx to toast.warning with localized
hints explaining the error may not indicate actual unusability; route
404/402/connection errors to toast.error. Add i18n keys for all three
locales (zh/en/ja).
Also deduplicate check_once by reusing build_stream_check_result.
Prevent users from switching to official providers (Anthropic/OpenAI/Google)
when proxy takeover is active, as using a proxy with official APIs may cause
account bans.
Defense-in-depth across 4 layers:
- Backend: ProviderService::switch(), hot_switch_provider(), switch_proxy_provider command
- Frontend: useProviderActions soft guard with error toast
- UI: ProviderActions button disabled with ShieldAlert icon
- Tray menu: official provider items disabled with ⛔ indicator
Also warns when enabling proxy takeover while current provider is official.
* feat(window): add app-level window controls with settings toggle
Add a persistent settings toggle to enable app-level minimize/maximize/close controls and hide system decorations when enabled, providing a Wayland-friendly fallback for broken native titlebar interactions.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(window): restrict app-level window controls to Linux only and fix startup flicker
- Guard useAppWindowControls with isLinux() in App.tsx so it's always
false on macOS/Windows even if persisted as true
- Wrap set_decorations call in lib.rs with #[cfg(target_os = "linux")]
- Only show the toggle in WindowSettings on Linux
- Skip setDecorations effect while settingsData is still loading to
prevent the Rust-side decoration state from being overridden by the
undefined->false fallback, which caused a brief title bar flicker
---------
Co-authored-by: wzk <wx13571681304@outlook.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Jason <farion1231@gmail.com>
- Pre-filter sessions by provider before indexing to prevent result
truncation when FlexSearch limit cuts across providers
- Switch tokenizer from "forward" to "full" for Chinese substring matching
- Preserve FlexSearch relevance ranking when search query is present
When switching to Copilot/ChatGPT/OpenAI-format providers with the proxy
not running, two toasts appeared: a "proxy required" warning followed by
a "switch success" toast. Unify the post-switch toast logic so that all
provider types show a single success toast, and skip it entirely when
a proxy-required warning was already shown.
Add skills.sh API integration allowing users to search and install from
a catalog of 91K+ agent skills directly within CC Switch. The search
results are converted to DiscoverableSkill objects and reuse the existing
install pipeline. Includes fallback directory search for repos where
skills are nested in subdirectories, and filters out non-GitHub sources.
- Add content_hash and updated_at fields to skills table (DB migration v6→v7)
- Compute directory content hash on install/import/restore for version tracking
- Add check_updates command: downloads repos, compares hashes, returns update list
- Add update_skill command: backs up old files, re-downloads and replaces SSOT
- Backfill content_hash for existing skills on first update check
- Add "Check Updates" button and per-skill update badge/button in UnifiedSkillsPanel
- Add i18n keys for zh/en/ja
Remove the hard block that prevented switching to providers requiring
proxy (OpenAI format, Copilot, full URL mode) when the proxy is not
running. Now the switch proceeds with a warning toast. Also deduplicate
the proxy hint info toast so it doesn't appear alongside the warning.
* feat(provider): support additive provider key lifecycle management
Add `addToLive` parameter to add_provider so callers can opt out of
writing to the live config (e.g. when duplicating an inactive provider).
Add `originalId` parameter to update_provider to support provider key
renames — the old key is removed from live config before the new one
is written.
Frontend: ProviderForm now exposes provider-key input for openclaw app
type, and EditProviderDialog forwards originalId on save. Deep-link
import passes addToLive=true to preserve existing behavior.
* test(provider): add integration tests for additive provider key flows
Cover openclaw provider duplication scenario to verify that a generated
provider key is assigned automatically. Add MSW handlers for
get_openclaw_live_provider_ids, get_openclaw_default_model,
scan_openclaw_config_health, and check_env_conflicts endpoints.
Update EditProviderDialog mock to pass originalId alongside provider.
* fix(openclaw): replace json-five serializer to prevent panic on empty collections
json-five 0.3.1 panics when pretty-printing nested empty maps/arrays.
Switch value_to_rt_value() to serde_json::to_string_pretty() which
produces valid JSON5 output without the panic. Add regression test for
removing the last provider (empty providers map).
* style: apply rustfmt formatting to proxy and provider modules
Reformat chained .header() calls in ClaudeAdapter and StreamCheckService
for consistent alignment. Reorder imports alphabetically in stream_check.
Fix trailing whitespace in transform.rs and merge import lines in
provider/mod.rs.
* style: fix clippy warnings in live.rs and tray.rs
* refactor(provider): simplify live_config_managed and deduplicate tolerant live config checks
- Change live_config_managed from Option<bool> to bool with #[serde(default)]
- Extract repeated tolerant live config query into check_live_config_exists helper
- Fix duplicate key generation to also check live-only provider IDs
- Fix updateProvider test to match new { provider, originalId } call signature
- Add streaming_responses test type annotation for compiler inference
* fix(provider): distinguish legacy providers from db-only when tolerating live config errors
Change `ProviderMeta.live_config_managed` from `bool` to `Option<bool>`
to introduce a three-state semantic:
- `Some(true)`: provider has been written to live config
- `Some(false)`: explicitly db-only, never written to live config
- `None`: legacy data or unknown state (pre-existing providers)
Previously, legacy providers defaulted to `live_config_managed = false`
via `#[serde(default)]`, which silently swallowed live config parse
errors. This could mask genuine configuration issues for providers that
had actually been synced to live config before the field was introduced.
Now, only providers with an explicit `Some(false)` marker tolerate parse
errors; legacy `None` providers surface errors as before, preserving
safety for already-managed configurations.
Also wrap the `ensureQueryData` call for live provider IDs during
duplication in a try/catch so that a malformed config file shows a
user-facing toast instead of silently failing.
Add tests for both the legacy error propagation path and the frontend
duplication failure scenario.
* refactor(provider): unify OMO variant updates with atomic file-then-db writes and rollback
Consolidate the duplicated omo/omo-slim update branches into a single
match on the variant. Write the OMO config file from the in-memory
provider state *before* persisting to the database, so a file-write or
plugin-sync failure leaves the database unchanged. If `add_plugin`
fails after the config file is already written, roll back to the
previous on-disk contents via snapshot/restore.
Also:
- `sync_all_providers_to_live` now skips db-only providers
(`live_config_managed == Some(false)`) instead of attempting to write
them to live config.
- `import_{opencode,openclaw}_providers_from_live` mark imported
providers as `live_config_managed: Some(true)` so they are correctly
recognized during subsequent syncs.
- Extract OmoService helpers: `profile_data_from_provider`,
`snapshot_config_file`, `restore_config_file`, `write_profile_config`,
and the new public `write_provider_config_to_file`.
- Add 9 new tests covering sync skip, legacy restore, import marking,
OMO persistence, file-write failure, and plugin-sync rollback.
* fix(provider): fix additive provider delete/switch regressions and redundancy
- fix(delete): replace stale live_config_managed flag check with
check_live_config_exists so providers written to live before the
flag-flip logic was introduced are still cleaned up on delete
- fix(switch): make write_live_with_common_config return Err instead of
silently returning Ok when config structure is invalid, preventing
live_config_managed from being incorrectly flipped to true
- fix(update): block provider key rename for OMO/OMO Slim categories to
prevent orphaned current-state markers breaking OMO file syncs
- fix(switch): flip live_config_managed to true after successful live
write for DB-only additive providers so sync_all_providers_to_live
includes them on future syncs; roll back live write if DB update fails
- refactor(delete): merge symmetric OMO/OMO-Slim blocks into single
match-on-variant path; hoist DB read to top of additive branch
- refactor(remove_from_live_config): merge OMO/OMO-Slim if/else-if
into single match-on-variant path
- refactor(switch_normal): merge two OMO/OMO-Slim if blocks into one
OpenCode guard with (enable, disable) variant pair
- fix(update): remove redundant duplicate return Ok(true) after OMO
current-state write
* fix(test): use preferred_filename after OMO field rename
The merge from main brought in #1746 which renamed
OmoVariant.filename → preferred_filename, but the test helper
omo_config_path() was not updated, breaking compilation of all
new provider tests.
---------
Co-authored-by: Jason <farion1231@gmail.com>
* style(frontend): reformat provider forms, constants and hooks
Apply prettier formatting across 5 frontend files. No logic changes.
Changed files:
- AddProviderDialog.tsx: reformat generic type annotation and callback
- ClaudeFormFields.tsx: consolidate multi-line useState and Collapsible props
- CodexConfigSections.tsx: expand single-line React imports to multi-line,
collapse removeCodexTopLevelField() call
- constants.ts: merge TemplateType into single line
- useSkills.ts: expand single-line TanStack Query imports to multi-line,
reformat uninstallSkill mutationFn chain
* deps(proxy): add hyper ecosystem crates and manual decompression libs
reqwest internally normalizes all header names to lowercase and does not
preserve insertion order, causing proxied requests to differ from the
original client requests. To achieve transparent header forwarding with
original casing and order, introduce lower-level hyper HTTP client libs.
New dependencies:
- hyper-util 0.1: TokioExecutor + legacy Client with
preserve_header_case support for HTTP/1.1
- hyper-rustls 0.27: rustls-based TLS connector for hyper
- http 1 / http-body 1 / http-body-util 0.1: HTTP type crates for
hyper 1.x request/response construction
- flate2 1: manual gzip/deflate decompression (replaces reqwest auto)
- brotli 7: manual brotli decompression
Changed dependencies:
- serde_json: enable preserve_order feature to keep JSON field order
- reqwest: drop gzip feature to prevent reqwest from overriding the
client's original accept-encoding header
* refactor(proxy): use hyper client for header-case preserving forwarding
Previously the proxy used reqwest for all upstream requests. reqwest
normalizes header names to lowercase and reorders them internally,
making proxied requests distinguishable from direct CLI requests.
Some upstream providers are sensitive to these differences.
This commit replaces reqwest with a hyper-based HTTP client on the
default (non-proxy) path, achieving wire-level header fidelity:
Server layer (server.rs):
- Replace axum::serve with a manual hyper HTTP/1.1 accept loop
- Enable preserve_header_case(true) so incoming header casing is
captured in a HeaderCaseMap extension on each request
- Bridge hyper requests to axum Router via tower::Service
New hyper client module (hyper_client.rs):
- Lazy-initialized hyper-util Client with preserve_header_case
- ProxyResponse enum wrapping both hyper::Response and reqwest::Response
behind a unified interface (status, headers, bytes, bytes_stream)
- send_request() builds requests with ordered HeaderMap + case map
Request handlers (handlers.rs):
- Switch from (HeaderMap, Json<Value>) extractors to raw
axum::extract::Request to preserve Extensions (containing the
HeaderCaseMap from the accept loop)
- Pass extensions through the forwarding chain
Forwarder (forwarder.rs):
- Remove HEADER_BLACKLIST array; replace with ordered header iteration
that preserves original header sequence and casing
- Build ordered_headers by iterating client headers, skipping only
auth/host/content-length, and inserting auth headers at the original
authorization position to maintain order
- Handle anthropic-beta (ensure claude-code-20250219 tag) and
anthropic-version (passthrough or default) inline during iteration
- Remove should_force_identity_encoding() — accept-encoding is now
transparently forwarded to upstream
- Use hyper client by default; fall back to reqwest only when an
HTTP/SOCKS5 proxy tunnel is configured
Provider adapters (adapter.rs, claude.rs, codex.rs, gemini.rs):
- Replace add_auth_headers(RequestBuilder) -> RequestBuilder with
get_auth_headers(AuthInfo) -> Vec<(HeaderName, HeaderValue)>
- Adapters now return header pairs instead of mutating a reqwest builder
- Claude adapter: merge Anthropic/ClaudeAuth/Bearer into single branch;
move Copilot fingerprint headers into get_auth_headers
Response processing (response_processor.rs):
- Add manual decompression (gzip/deflate/brotli via flate2 + brotli)
for non-streaming responses, since reqwest auto-decompression is now
disabled to allow accept-encoding passthrough
- Add compressed-SSE warning log for streaming responses
- Accept ProxyResponse instead of reqwest::Response
HTTP client (http_client.rs):
- Disable reqwest auto-decompression (.no_gzip/.no_brotli/.no_deflate)
on both global and per-provider clients
Streaming adapters (streaming.rs, streaming_responses.rs):
- Generalize stream error type from reqwest::Error to generic E: Error
Misc:
- log_codes.rs: add SRV-005 (ACCEPT_ERR) and SRV-006 (CONN_ERR)
- stream_check.rs: reformat copilot header lines
- transform.rs: fix trailing whitespace alignment
* fix(lint): resolve 35 clippy warnings across Rust codebase
Fix all clippy warnings reported by `cargo clippy --lib`:
- codex_config.rs: fix doc_overindented_list_items (3 spaces -> 2)
- commands/copilot.rs: inline format args in 2 log::error! calls
- commands/provider.rs: inline format args in 3 map_err closures
- proxy/hyper_client.rs: inline format arg in log::debug! call
- proxy/providers/copilot_auth.rs: inline format args in 16 locations
(log macros, format! in headers, error constructors)
- proxy/thinking_optimizer.rs: inline format args in 2 log::info! calls
- services/skill.rs: inline format args in log::debug! call
- services/webdav_sync.rs: inline format args in 6 format! calls
(version compat messages, download limit messages)
- services/webdav_sync/archive.rs: inline format args in 2 format! calls
- session_manager/providers/opencode.rs: inline format args in
source_path format!
All fixes use the clippy::uninlined_format_args suggestion pattern:
format!("msg: {}", var) -> format!("msg: {var}")
* deps(proxy): add raw HTTP write and native TLS cert dependencies
Add crates required for the raw TCP/TLS write path that bypasses
hyper's header encoder to preserve original header name casing:
- httparse: parse raw TCP peek bytes to capture header casings
- tokio-rustls + rustls: direct TLS connections for raw write path
- webpki-roots: Mozilla CA bundle baseline
- rustls-native-certs: load system keychain CAs (trusts proxy MITM
certificates from Clash, mitmproxy, etc.)
* fix(proxy): address code review feedback on response handling
Fixes from PR #1714 code review:
- Extract `read_decoded_body()` and `strip_entity_headers_for_rebuilt_body()`
in response_processor to properly clean content-encoding/content-length
headers after decompression
- Reuse `read_decoded_body()` in handlers.rs for Claude transform path,
ensuring compressed responses are decoded before format conversion
- Make `build_proxy_url_from_config()` public so forwarder can pass proxy
URL to the hyper raw write path
- Add `has_system_proxy_env()` utility with test coverage
- Add 50ms backoff after accept() failures in server.rs to prevent
tight-loop CPU spin on transient socket errors
* feat(proxy): implement raw TCP/TLS write with HTTP CONNECT tunnel
Rewrite hyper_client with a two-tier strategy for header case preservation:
Primary path (raw write):
- Peek raw TCP bytes in server.rs to capture OriginalHeaderCases before
hyper lowercases them
- Build raw HTTP/1.1 request bytes with exact original header name casing
- Write directly to TLS stream, then use WriteFilter to let hyper parse
the response while discarding its duplicate request writes
- Support HTTP CONNECT tunneling through upstream proxies, so header case
is preserved even when a proxy (Clash, V2Ray) is configured
Fallback path (hyper-util Client):
- Used when OriginalHeaderCases is empty or raw write fails
- Configured with title_case_headers(true) for best-effort casing
TLS improvements:
- Load native system certificates alongside webpki roots so proxy MITM
CAs (installed in system keychain) are trusted through CONNECT tunnels
Key types added:
- OriginalHeaderCases: maps lowercase name → original wire-casing bytes
- WriteFilter<S>: AsyncRead+AsyncWrite wrapper that discards writes
- connect_via_proxy(): HTTP CONNECT tunnel establishment
- ExtensionDebugMarker: diagnostic marker for extension chain debugging
* refactor(proxy): route requests through hyper with proxy-aware forwarding
Rework forwarder request dispatch to always prefer the hyper raw write
path (header case preservation) over reqwest:
Request routing:
- HTTP/HTTPS proxy: hyper raw write through CONNECT tunnel (case preserved)
- SOCKS5 proxy: reqwest fallback (CONNECT not supported for SOCKS5)
- No proxy: hyper raw write direct connection
Header handling improvements:
- Replace host header in-place at original position instead of
skip-and-append, preserving client's header ordering
- Preserve client's original accept-encoding for transparent passthrough;
only force identity encoding when transform path needs decompression
- Add should_force_identity_encoding() to centralize the decision
- Remove hardcoded 'br, gzip, deflate' override that masked client values
Proxy URL resolution (priority order):
1. Provider-specific proxy config (if enabled)
2. Global proxy URL configured in CC Switch
3. Direct connection (no proxy)
* chore(proxy): remove dead code, redundant tests and debug scaffolding
- Inline should_force_identity_encoding() (was just `needs_transform`)
and delete its 5 test cases
- Remove ExtensionDebugMarker diagnostic type
- Remove unused has_system_proxy_env() and its test
- Remove strip_entity_headers test
- Simplify hyper path: remove redundant is_socks_proxy ternary
- Update hyper_client module doc to reflect CONNECT tunnel support
* fix(proxy): block direct-connect fallback and complete CONNECT tunnel support
* feat(hooks): improve proxy requirement warnings with specific reasons
- Remove redundant OpenAI format hint toast messages
- Add detailed reason detection for proxy requirements (OpenAI Chat, OpenAI Responses, full URL mode)
- Update i18n files with new reason-specific keys
* style(*): format code with prettier
- Remove extra whitespace in http_client.rs
- Fix formatting issues in useProviderActions.ts
* fix(proxy): post-merge fixes for forward return type and clippy warnings
- Restore forward() return type to (ProxyResponse, Option<String>)
to pass claude_api_format through to callers
- Inline format args in log::warn! macro (clippy::uninlined_format_args)
- Suppress too_many_arguments for check_claude_stream
* refactor(proxy): preserve original header wire order and add non-streaming body timeout
- Rewrite build_raw_request to emit headers in original
client-sent sequence instead of hash-map order
- Remove unused OriginalHeaderCases::get_all method
- Add body_timeout to read_decoded_body to prevent
requests hanging when upstream stalls after headers
* fix: route copilot claude openai models to responses
* fix(i18n): add copilotProxyHint translation key for all locales
The copilotProxyHint message was using inline defaultValue with Chinese
text, which would show Chinese to English and Japanese users. Added
proper translation keys in zh/en/ja locale files and removed the
hardcoded defaultValue fallback.
---------
Co-authored-by: Jason <farion1231@gmail.com>