Compare commits

..

4 Commits

Author SHA1 Message Date
YoVinchen 5bccb4f65b Keep cache-control merge regressions easy to review
Reflow the two long cache-control regression assertions in transform.rs so the neighboring merge cases stay rustfmt-aligned and easier to scan.

This keeps the preserved code change separate from the untracked Markdown design notes the user did not want committed.

Constraint: Exclude Markdown design files from the commit while preserving the local code change
Rejected: Include docs in the same commit | user explicitly asked to leave Markdown files out
Confidence: high
Scope-risk: narrow
Reversibility: clean
Directive: Treat this as a readability-only test change; do not infer runtime behavior changes from it
Tested: cargo test --manifest-path src-tauri/Cargo.toml test_anthropic_to_openai_drops_ --lib
Tested: cargo check --manifest-path src-tauri/Cargo.toml --tests
Tested: pnpm format:check
Tested: pnpm typecheck
Not-tested: Full application integration and manual flows
2026-04-11 23:48:45 +08:00
YoVinchen b4f0f7cec3 Merge branch 'main' into codex/issue-1881-cache-control 2026-04-11 23:42:18 +08:00
YoVinchen ad89c4719a Tighten cache hint inheritance for merged system prompts
The follow-up cache hint fix still treated mixed present/absent\ncache_control across fragmented system prompts as inheritable, which\nexpanded the cache scope after prompt collapse. Treat that mix as\nambiguous and only preserve cache_control when every merged fragment\nexplicitly agrees on the same value.\n\nConstraint: Must preserve strict-backend system prompt normalization from #1942\nRejected: Inherit first present cache_control | widens cache scope when later fragments were intentionally uncached\nConfidence: high\nScope-risk: narrow\nReversibility: clean\nDirective: Any future merged-system cache hint logic should treat missing cache_control as semantically significant\nTested: cargo test proxy::providers::transform --manifest-path src-tauri/Cargo.toml\nNot-tested: End-to-end upstream caching behavior against cache-aware relays\nRelated: #1881\nRelated: #1946
2026-04-08 11:16:52 +08:00
YoVinchen b11e36963f Preserve cache hints when collapsing system prompts
Strict OpenAI-compatible chat backends still need fragmented Claude\nsystem prompts collapsed into one leading system message, but that\nnormalization should not silently drop stable cache hints. Preserve\nmessage-level cache_control when the merged system fragments agree,\nand fall back to omitting it when the fragments conflict.\n\nConstraint: Must keep single-system normalization for Nvidia/Qwen-style chat backends\nRejected: Always copy the first cache_control | could misrepresent conflicting cache boundaries\nConfidence: high\nScope-risk: narrow\nReversibility: clean\nDirective: If system prompt merging changes again, preserve cache_control whenever the merged metadata is unambiguous\nTested: cargo test proxy::providers::transform --manifest-path src-tauri/Cargo.toml\nNot-tested: End-to-end prompt caching behavior against cache-aware OpenAI-compatible upstreams\nRelated: #1881
2026-04-08 10:17:16 +08:00
347 changed files with 6252 additions and 43103 deletions
+3 -3
View File
@@ -16,7 +16,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v6
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v6
@@ -24,7 +24,7 @@ jobs:
node-version: "20"
- name: Setup pnpm
uses: pnpm/action-setup@v6
uses: pnpm/action-setup@v5
with:
version: 10.12.3
run_install: false
@@ -58,7 +58,7 @@ jobs:
runs-on: ubuntu-22.04
steps:
- name: Checkout
uses: actions/checkout@v6
uses: actions/checkout@v4
- name: Setup Rust
uses: dtolnay/rust-toolchain@stable
-61
View File
@@ -1,61 +0,0 @@
name: Claude
on:
issue_comment:
types: [created]
pull_request_review_comment:
types: [created]
pull_request_review:
types: [submitted]
issues:
types: [opened, assigned]
jobs:
claude:
if: |
((github.event_name == 'issue_comment' && contains(github.event.comment.body, '@claude') &&
(github.event.comment.author_association == 'OWNER' ||
github.event.comment.author_association == 'MEMBER' ||
github.event.comment.author_association == 'COLLABORATOR'))
||
(github.event_name == 'pull_request_review_comment' && contains(github.event.comment.body, '@claude') &&
(github.event.comment.author_association == 'OWNER' ||
github.event.comment.author_association == 'MEMBER' ||
github.event.comment.author_association == 'COLLABORATOR'))
||
(github.event_name == 'pull_request_review' && contains(github.event.review.body, '@claude') &&
(github.event.review.author_association == 'OWNER' ||
github.event.review.author_association == 'MEMBER' ||
github.event.review.author_association == 'COLLABORATOR'))
||
(github.event_name == 'issues' &&
(contains(github.event.issue.body, '@claude') || contains(github.event.issue.title, '@claude')) &&
(github.event.issue.author_association == 'OWNER' ||
github.event.issue.author_association == 'MEMBER' ||
github.event.issue.author_association == 'COLLABORATOR')))
runs-on: ubuntu-latest
permissions:
contents: read
pull-requests: write
issues: write
id-token: write
steps:
- name: Checkout
uses: actions/checkout@v6
with:
fetch-depth: 1
ref: ${{ github.event.pull_request.head.sha || github.sha }}
- name: Run Claude (review only)
uses: anthropics/claude-code-action@v1
with:
claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
claude_args: >-
--model claude-opus-4-7
--disallowedTools Edit,Write,MultiEdit,NotebookEdit
--append-system-prompt "You are reviewing PRs for cc-switch — a small open-source Tauri 2.0 + React + TypeScript desktop app maintained by a single developer who values pragmatism over perfection.
ONLY flag HIGH SIGNAL issues. An issue qualifies only if it meets at least one of: code that fails to compile/parse/run; code that will definitely produce wrong results; a security vulnerability with a concrete trigger (SSRF, injection, unsafe deserialization, credential or data leaks); database/migration correctness (schema mismatch, data loss on upgrade, broken SCHEMA_VERSION transition); cross-platform breakage (Windows/macOS/Linux specific); clear, quotable violations of CLAUDE.md.
Each finding must include a confidence score from 0 to 100. Do NOT post any finding scored below 80. Each finding must carry a severity tag — Important (bug that should be fixed before merge), Nit (minor, worth fixing but not blocking), or Pre-existing (exists already, not introduced by this PR).
DO NOT flag any of the following: pedantic nitpicks a senior engineer would not raise; anything a linter, typechecker, pnpm format, or cargo fmt would catch; missing test coverage unless CLAUDE.md explicitly mandates it; style, naming, or 'could be more idiomatic' suggestions; speculative future-proofing ('what if X is added later'); issues on lines the PR did not modify; potential bugs for which you cannot construct a concrete failure scenario; positive feedback or 'what is well done' sections.
OUTPUT RULES: Cap Nit-level findings at 5 per review. If you found more nits, append 'plus N similar nits' to the summary instead of listing them inline. If only nits were found, lead the review with 'No blocking issues.' If nothing material is wrong, say 'LGTM' explicitly and stop — approval with zero comments is a valid and expected outcome for most PRs. Do NOT post a final APPROVE or REQUEST CHANGES verdict; leave the merge decision to the maintainer.
Never modify files, push commits, or create PRs."
+3 -5
View File
@@ -26,7 +26,7 @@ jobs:
steps:
- name: Checkout
uses: actions/checkout@v6
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v6
@@ -74,7 +74,7 @@ jobs:
|| sudo apt-get install -y --no-install-recommends libsoup2.4-dev
- name: Setup pnpm
uses: pnpm/action-setup@v6
uses: pnpm/action-setup@v5
with:
version: 10.12.3
run_install: false
@@ -535,7 +535,7 @@ jobs:
ls -la release-assets
- name: Upload Release Assets
uses: softprops/action-gh-release@v3
uses: softprops/action-gh-release@v2
with:
tag_name: ${{ github.ref_name }}
name: CC Switch ${{ github.ref_name }}
@@ -543,8 +543,6 @@ jobs:
body: |
## CC Switch ${{ github.ref_name }}
🌐 **Only Official Website / 唯一官方网站 / 唯一の公式サイト**: [ccswitch.io](https://ccswitch.io)
Claude Code 供应商切换工具
### 下载
+1 -1
View File
@@ -13,7 +13,7 @@ jobs:
stale:
runs-on: ubuntu-latest
steps:
- uses: actions/stale@v10
- uses: actions/stale@v9
with:
# --- Timing ---
days-before-stale: 60
-1
View File
@@ -29,4 +29,3 @@ copilot-api
.history
CODEBUDDY.md
.github
mainWindow.js
-246
View File
@@ -5,252 +5,6 @@ All notable changes to CC Switch will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [Unreleased]
## [3.15.0] - 2026-05-16
Development since v3.14.1 focuses on a dedicated Claude Desktop surface with third-party provider switching through a proxy gateway, a large reverse-proxy hardening pass (reliability, retries, cache, takeover, Gemini/Vertex/Codex paths), expansion of the third-party provider preset catalog (BytePlus / Volcengine / ClaudeAPI / ClaudeCN / RunAPI / RelaxyCode / PatewayAI / Baidu Qianfan), role-based model mapping with a 1M context flag, Codex OAuth live model discovery, and a long tail of usage, OAuth, Codex, and session quality-of-life fixes.
**Stats**: 127 commits | 211 files changed | +17,980 insertions | -2,748 deletions
### Highlights
- **Claude Desktop becomes a first-class managed surface** with third-party provider switching through an in-app proxy gateway, role-based model mapping (sonnet / opus / haiku) with a 1M context flag, Copilot/Codex OAuth provider reuse, and 44 imported provider presets translated from the Claude Code catalog. Note: 20 Claude Desktop presets now default to direct mode instead of routing through the proxy — verify connectivity if you previously relied on proxy routing for these presets.
- **Major reverse-proxy hardening**: P0P3 lifecycle, retry, failover, and rectifier patches; pooled HTTPS reuse for non-Anthropic backends; Codex/Responses cache hit-rate improvements; correct Anthropic ↔ OpenAI `tool_choice` mapping; Vertex AI URL preservation; Gemini path-based model extraction; takeover detection refinement; IPv6 listen address support.
- **Provider Ecosystem Expansion**: Added BytePlus, Volcengine Agentplan, ClaudeAPI, ClaudeCN, RunAPI, RelaxyCode, PatewayAI, and Baidu Qianfan Coding Plan partner presets; promoted DouBao Seed to partner status; routing-support badges now surface on provider cards.
- **Role-Based Model Mapping for Claude Code**: Display-name-aware sonnet / opus / haiku route mapping with a `supports1m` flag replaces the legacy `[1M]` suffix and decouples routing from raw model IDs.
- **Codex OAuth Live Model Discovery**: ChatGPT Codex providers now fetch the live model list from the ChatGPT backend on demand instead of relying on a static list.
- **Usage Dashboard Filter-Driven Hero**: A new filter-driven Hero card with cache-normalized totals replaces the legacy summary block, paired with cache-cost-semantics fixes that silence a noisy pricing warning storm.
- **DeepSeek tool-call reasoning and zero-usage final deltas**: DeepSeek tool calls now return `reasoning_content` alongside `tool_calls` (#2543), and the final `message_delta` event always includes a usage block (even when zero) so strict Anthropic clients no longer crash on `null` (#2485).
### Added
- **Claude Desktop Third-Party Provider Switching via Proxy Gateway**: Added a dedicated Claude Desktop surface that brokers third-party Claude providers through CC Switch's in-app proxy, with a routing-support badge for providers that need it, role-based model route mapping locked to `sonnet` / `opus` / `haiku`, Copilot/Codex OAuth provider reuse, a redesigned Claude Code import flow, app-switcher differentiation between "Claude Code" and "Claude Desktop", and 44 provider presets translated from the Claude Code catalog.
- **Routing Support Badges on Provider Cards**: Provider cards in both Claude Code and Codex panels now show a routing-support badge so users can tell at a glance which providers can be served through Local Routing.
- **Codex OAuth Live Model List**: ChatGPT Codex providers now fetch the current model list from the ChatGPT backend on demand, replacing the previously hardcoded selection.
- **Role-Based Model Mapping with 1M Flag**: Claude Code model mapping is now role-based (`sonnet` / `opus` / `haiku`) with display names and a `supports1m` flag, replacing the legacy `[1M]` suffix to decouple routing from raw model IDs.
- **Filter-Driven Usage Hero**: The usage dashboard's Hero summary is now filter-driven with cache-normalized totals so the figures line up with the active date range and provider filters.
- **Provider Form "Save Anyway" Prompt**: Softened provider form validation with a "save anyway" prompt so non-blocking input issues no longer prevent saving (#2307).
- **Universal Provider Duplicate Action**: Added a duplicate action for universal providers from the provider list (#2416).
- **Persisted Tauri Window State**: Window position and size now persist across launches (#2377).
- **Tray Icon Tooltip**: The system tray icon now shows a tooltip on hover for clearer at-a-glance state (#2417).
- **Warp Terminal Session Launch**: Added support for launching Warp and executing a saved session inside it (#2466).
- **DeepSeek `reasoning_content` for Tool Calls**: DeepSeek tool-calling responses now return `reasoning_content` together with `tool_calls` so callers can render both (#2543).
- **Baidu Qianfan Coding Plan for Claude Code**: Added a Baidu Qianfan Coding Plan preset for Claude Code (#2322).
- **Compshare Coding Plan Preset (Cross-App)**: Added Compshare Coding Plan preset across claude / codex / hermes / openclaw.
- **Partner Provider Presets**: Added BytePlus, Volcengine Agentplan, ClaudeAPI, ClaudeCN, RunAPI, RelaxyCode, and PatewayAI provider presets; promoted DouBao Seed to partner status with refreshed endpoint and links.
- **44 Claude Desktop Provider Presets**: Translated 44 provider presets from the Claude Code catalog into the new Claude Desktop surface.
### Changed
- **20 Claude Desktop Presets Switched from Proxy to Direct Mode**: 20 Claude Desktop presets now ship in direct mode instead of routing through the proxy by default, reducing setup friction for users who don't need proxy-specific compatibility shims. Verify connectivity if you previously relied on proxy routing for these presets.
- **Claude Desktop Operational Notes**: Switching a Claude Desktop provider now writes CC Switch's managed 3P profile and requires restarting Claude Desktop to take effect; proxy-mode providers require CC Switch Local Routing to stay running.
- **Failover / Local Routing Guardrails**: Failover controls now require the target app's Local Routing takeover to be enabled, and stopping only the proxy service is blocked while any app still depends on takeover state.
- **Usage Accounting Semantics**: Usage summaries now report cache-normalized real total tokens and cache hit rate; historical token and cost totals may shift after deduplication and pricing recalculation, but should be more accurate.
- **Provider Preset Rendering Order**: Provider preset lists now render in author-defined array order with partners prioritized at the top, replacing the previous implicit sort.
- **Model Mapping Hint Copy Simplified**: `modelMappingOffHint` was rewritten as action-oriented copy across zh / en / ja.
- **CC Switch Brand Surface Unified to ccswitch.io**: All in-app and README references now point at ccswitch.io as the sole official website; the release notes template also surfaces ccswitch.io.
- **Theme Switch Simplified**: Removed the circular reveal animation; theme changes are now an instant cross-fade.
- **Claude Code App Switcher Differentiation**: The app switcher now visually distinguishes "Claude Code" from "Claude Desktop" and uses the "Claude Code" label in the app visibility settings.
- **CI: Claude Review on Opus 4.7**: Upgraded the Claude review GitHub Action to Opus 4.7, tuned the prompt to reduce nitpick noise, added an `@claude` review-only Code Action, pinned PR head SHA for checkout, and dropped a `--max-turns 5` limit.
- **Dependency Bumps**: `actions/checkout` 4 → 6 (#2517), `pnpm/action-setup` 5 → 6 (#2518), `softprops/action-gh-release` 2 → 3 (#2519), `actions/stale` 9 → 10 (#2520).
- **DeepSeek Presets Switched to V4**: DeepSeek presets now ship V4 (flash / pro) with refreshed pricing seeds.
- **Codex 1M Context Toggle Hidden in Provider Edit Form**: The 1M context-window toggle is no longer surfaced in the Codex provider edit form to reduce knob count for a setting that has no effect in current Codex deployments.
- **OpenClaudeCode Migrated to MicuAPI Domain**: Updated the OpenClaudeCode preset to the MicuAPI domain; refreshed Micu API links to `micuapi.ai`.
- **CrazyRouter Endpoints Switched to `cn` Subdomain**: Updated CrazyRouter preset endpoints to the `cn` subdomain.
- **RelaxyCode Custom Icon**: Switched RelaxyCode preset icon to a custom `relaxcode.png` asset.
- **Kimi For Coding Doc URL**: Updated Kimi For Coding website URL to the `/code/docs/` path.
- **SiliconFlow International Site Shows USD**: Balance display now correctly shows USD for the SiliconFlow international site (was incorrectly displaying CNY).
### Fixed
- **OpenAI Responses API Usage Parsing Robustness**: Hardened `build_anthropic_usage_from_responses()` and the Responses → Anthropic SSE translator so a missing or malformed upstream `usage` no longer produces `"usage": null` in `message_delta`. This unblocks strict Anthropic clients (notably the VSCode Claude Code extension) that crashed with "Cannot read properties of null (reading 'output_tokens')" against providers such as Codex OAuth and DashScope's `compatible-mode/v1/responses` endpoint. Added OpenAI field-name fallbacks (`prompt_tokens` / `completion_tokens`), null/empty/partial object handling, and preserved cache token fields even when input/output tokens are missing (#2422).
- **Proxy Reliability Patches (P0P3)**: Multiple rounds of routing, lifecycle, retry, and rectifier patches across the request-forwarder paths; extracted a shared `handle_rectifier_retry_failure` helper and a shared `auth_header_value` helper across provider adapters.
- **Proxy: Pooled HTTPS Connection Reuse**: Non-Anthropic backends now reuse pooled HTTPS connections instead of opening a fresh TLS session per request, materially reducing per-request latency.
- **Proxy: Forward Client HTTP Method**: The proxy forwards the client's actual HTTP method instead of hard-coding `POST`, so non-POST upstream endpoints (e.g. GET `/v1/models`) work correctly.
- **Proxy: Per-Attempt Counters and `max_retries` Wiring**: Client-request counters moved out of the per-attempt loop, and `AppProxyConfig.max_retries` is now correctly wired into the request forwarder.
- **Proxy: Failover Decision Refinements**: Refined failover decision logic in the forwarder so retryable / unretryable errors are classified more accurately.
- **Proxy: Takeover Detection Tightening**: Tightened takeover detection and use fallback restore when disabling takeover so leftover state no longer strands a provider.
- **Proxy: Anthropic ↔ OpenAI `tool_choice` Mapping**: Anthropic `tool_choice` is now correctly mapped to the OpenAI Chat nested form during format conversion.
- **Proxy: Gemini Request Model Extraction**: Gemini request model is now correctly extracted from the URI path (not the body) so transformed traffic reports the right model.
- **Proxy: Auth Header Error Handling**: `get_auth_headers` now returns `Result` instead of panicking on bad credentials.
- **Proxy: IPv6 Listen Address Validation**: The Proxy panel now accepts IPv6 listen addresses.
- **Proxy: Codex / Responses Cache Hit Rate**: Improved cache hit rate for Codex and OpenAI Responses requests by stabilizing cache key derivation; only emit `prompt_cache_key` when a real client-provided session identity is available so unrelated conversations no longer collapse onto a single key; canonicalize (sort) JSON keys in outgoing request bodies and `tool_call` arguments / `tool_result` content for byte-identical prefix-cache reuse; thread `session_id` into the usage logger for request correlation.
- **Proxy: JSON Schema Underscore Fields Preserved**: Private-parameter filtering now preserves underscore-prefixed field names inside JSON Schema name maps such as `properties`, `patternProperties`, `definitions`, and `$defs`, so user-defined schema keys like `_id` and `_meta` survive the filter.
- **Proxy: Read Tool Empty Pages**: Drop empty pages from `Read` tool inputs so providers don't reject the request (#2472).
- **Proxy: Per-Request Hot-Path Trim**: Trimmed per-request hot-path work and database wait time.
- **Proxy: Real Provider Model Names Under Takeover**: The Claude Code menu now exposes the real provider model names when running under takeover, instead of a stale alias.
- **Proxy: Zero Usage in Final Message Delta**: The final `message_delta` event always includes a usage block (even when zero) so strict Anthropic clients no longer crash on `null` (#2485).
- **Proxy: Streaming `message_delta` Deduplication**: Deduplicated streaming `message_delta` events that some upstreams emit twice (#2366).
- **Proxy: Scoped `reasoning_content` Preserved for Tool Calls**: Tool-call paths now correctly preserve the scoped `reasoning_content` field during transformation; Kimi / Moonshot OpenAI Chat compatibility paths keep the field while generic OpenAI-compatible requests stay free of it (#2367).
- **Proxy: Vertex AI Full URL Preserved**: Full Vertex AI URLs are no longer truncated during proxy forwarding (#2415).
- **Proxy: Leading Billing Header Stripped from System Content**: Strips the leading billing-header content that some upstreams prepend to the system message (#2350).
- **Proxy: Claude Auth Strategy from `ANTHROPIC_*` Env Var**: The Claude auth strategy is now derived from the actual `ANTHROPIC_*` env variable name rather than an opaque heuristic.
- **Third-Party Claude Providers: Disable Model Test**: Model probing is now disabled for third-party Claude providers where the gateway doesn't implement `/v1/models` consistently.
- **Model-Fetch: `/models` Subpath for Anthropic-Compatible Providers**: `/models` discovery now works for Anthropic-compatible subpath providers.
- **Copilot: Claude Model ID Resolution Against Live `/models`**: Copilot-backed providers now resolve Claude model IDs against the live `/models` list to avoid stale ID mismatches.
- **Codex: Skip `environment_context` Injection When Extracting Session Title**: Session title extraction no longer pulls in `environment_context` noise (#2439).
- **Codex: Hide Subagent Sessions**: Codex subagent sessions are now hidden from the main session list (#2445).
- **Codex Startup Live Import Duplication**: Fixed a duplicate-import bug in the Codex startup live-import path (#2590).
- **Codex Provider Switch History Drift**: Switching the active Codex provider no longer changes existing session history (#2349).
- **Codex Usage Log Message**: Corrected a misleading log message for Codex session usage (#2473).
- **Claude: Persist Max Effort via Env**: `max` effort now correctly persists via the env variable on restart (#2493).
- **Claude Desktop: Match Proxy Model Route Without `[1M]` Suffix**: Route matching no longer requires the legacy `[1M]` suffix.
- **Claude Desktop: Provider Form Focus Loss**: Fixed an input that lost focus while editing in the Claude Desktop provider form.
- **Claude Desktop: Spurious Proxy-Stopped Status Alert**: Removed an alert that fired spuriously when the proxy was intentionally stopped.
- **Claude Desktop: Empty Toolbar Capsule Hidden**: Hides the empty toolbar capsule when Claude Desktop is the active app.
- **UI: Monitor Badge Icon Centering**: Centered the Monitor badge icon in the app switcher.
- **Linux: Theme Selection Segfault**: Prevented selecting a theme from causing a segfault on Linux (#2502).
- **Terminal: iTerm Fallback on Cold Launch**: Prevented iTerm from being selected as a fallback on cold launch when not actually present (#2448).
- **Config: Sort JSON Keys Alphabetically**: Config writes now sort JSON keys alphabetically for deterministic output (#2469).
- **Import Existing Side-Effect Free**: Made "import existing" side-effect free (#2429).
- **Coding Plan: Zhipu Weekly Tier by Reset Time**: Corrected the Zhipu weekly tier name to match the actual reset time (#2420).
- **DashScope: Usage Parsing Robustness**: Hardened DashScope usage parsing so a malformed payload no longer crashes the VSCode Claude Code extension (#2425).
- **Usage: Prevent Double-Counting Between Proxy and Session-Log Sources**: Deduplicated usage records sourced from both the proxy and session logs.
- **Usage: Cache Cost Semantics + Pricing Warn Storm**: Corrected cache-cost semantics and silenced a noisy pricing warning storm that fired on every request.
- **CI: Frontend Formatting and Linux Clippy Restored**: Restored frontend formatting and Linux clippy checks in CI.
- **Proxy Test Helper Clippy Warning**: Fixed a clippy warning in the proxy test helper.
### Removed
- **Hermes Agent Usage Tracking Integration**: Removed the in-cycle Hermes Agent usage tracking integration after upstream behavior changes made it impractical to keep in sync. The integration was never enabled in any released version — a zero-cost rendering bug found during its development was fixed before the integration was rolled back.
- **Theme Switch Circular Reveal Animation**: Removed the circular reveal animation used during theme switching; the animation caused jank on slower compositors and added little visible value.
- **DDSHub Partner Integration**: Removed DDSHub as a partner preset and dropped the cross-link blurbs across READMEs.
### Docs
- **README Sponsor Refresh (zh / en / ja)**: Added BytePlus, ClaudeCN, RunAPI, and PatewayAI sponsor entries; cross-linked BytePlus and Volcengine entries; refreshed the Crazyrouter $2 credit claim flow, the Compshare blurb, the Right Code blurb, and other sponsor logos and listings; flattened the LionCC logo onto a white background; switched the Chinese README's sponsor logo to the Volcengine artwork; added Hermes Agent to the README subtitles.
- **Release Notes Template**: Surfaces `ccswitch.io` in the release notes template.
- **Brand Surface**: Documented `ccswitch.io` as the sole official website across READMEs and in-app references.
## [3.14.1] - 2026-04-23
Development since v3.14.0 focuses on Codex OAuth stability, tray usage visibility, Skills import/install reliability, Gemini session restore paths, and simplifying Hermes configuration health handling.
**Stats**: 13 commits | 48 files changed | +1,883 insertions | -808 deletions
### Added
- **Tray Usage Visibility**: System tray submenus now show cached usage for the current Claude / Codex / Gemini provider, including subscription and script-based usage summaries with utilization color markers. Tray-triggered refreshes are throttled, limited to visible apps, and synchronized back into React Query so the main window and tray share fresh usage data (#2184).
- **Tray Coding-Plan Usage (Kimi / Zhipu / MiniMax)**: System tray now renders 5-hour + weekly window usage for Chinese coding-plan providers using the same `🟢 h12% w80%` two-window layout as official subscription badges (worst utilization drives the emoji). Creating a Claude provider whose `ANTHROPIC_BASE_URL` matches a known coding-plan host now auto-injects `meta.usage_script`, so the tray lights up without opening the Usage Script modal. Existing `usage_script` values are preserved on update.
- **Codex OAuth FAST Mode**: Added an explicit FAST mode toggle for Codex OAuth-backed Claude providers. When enabled, converted Responses requests send `service_tier="priority"` for lower latency; the toggle stays off by default to avoid unexpectedly increasing ChatGPT quota consumption (#2210).
### Changed
- **Session and Settings Layout Polish**: Hardened the scroll-area viewport with width containment to fix horizontal overflow, and tightened app bottom spacing plus settings footer spacing so long session/settings views fit more cleanly (#2201).
### Removed
- **Hermes Config Health Scanner**: Removed the in-app Hermes config health scanner, warning banner, `scan_hermes_config_health` command, `HermesHealthWarning` type, and `HermesWriteOutcome.warnings` payload. CC Switch now keeps the Hermes surface focused on active provider display, provider switching defaults, memory editing, and launching the Hermes Web UI for deep configuration.
### Fixed
- **Codex OAuth Cache Routing**: Stabilized ChatGPT Codex reverse-proxy cache identity by using client-provided session IDs for `prompt_cache_key` and Codex session headers, preserving explicit cache keys, and avoiding generated UUID cache churn (#2218).
- **Codex OAuth Responses SSE Aggregation**: Non-streaming Anthropic clients now receive JSON even when the ChatGPT Codex upstream forces OpenAI Responses SSE; CC Switch aggregates the upstream SSE events before running the non-streaming transform (#2235).
- **Codex OAuth Stream Check Parity**: Stream checks now build Codex OAuth test requests with the same `store: false`, encrypted reasoning include, and provider FAST mode setting as production proxy requests (#2210).
- **Codex Model Extraction**: Replaced first-line regex matching with TOML parsing when reading Codex config models, so multiline TOML is handled correctly (#2227).
- **Model Quick-Set / One-Click Config**: Model quick-set updates now apply against the latest provider form config, preventing stale state from making one-click configuration fail (#2249).
- **Skills Import Duplicates**: The Skills import dialog disables actions while import is pending and the installed-skills cache deduplicates imported results by ID, preventing double-clicks from adding duplicate installed entries (#2139, #2211).
- **Root-Level Skill Repos**: Skill install and update flows now consistently resolve three source patterns: direct nested paths, install-name recursive search, and repository-root `SKILL.md` sources (#2231).
- **Gemini Session Restore Paths**: Gemini session scanning now reads `.project_root` metadata so restore flows can pass the original project directory when available (#2240).
- **Provider Hover Names**: Provider icons now expose the provider name on hover for inline SVG, image URL, and fallback initials render paths (#2237).
## [3.14.0] - 2026-04-21
Development since v3.13.0 focuses on onboarding Hermes Agent as a first-class managed app, rolling out Claude Opus 4.7 across the preset matrix, adding a Gemini Native API proxy, and sharpening session, usage, and proxy workflows.
**Stats**: 100 commits | 219 files changed | +20,548 insertions | -3,569 deletions
### Added
- **Hermes Agent Support (6th Managed App)**: Added Hermes Agent as a first-class managed app with database migration v9→v10, full Rust command surface, YAML-backed `~/.hermes/config.yaml` read/write with atomic backups, MCP sync, Skills sync, session manager with SQLite + JSONL support, and dedicated frontend panels. Supports four API protocols (`chat_completions`, `anthropic_messages`, `codex_responses`, `bedrock_converse`) aligned with Hermes Agent 0.10.0 schema. Read-only rendering for providers owned by the user-authored `providers:` dict, with deep configuration delegated to the Hermes Web UI.
- **Hermes Memory Panel**: Added a Memory panel for editing `MEMORY.md` and `USER.md` directly from CC Switch, with an enable switch, character-count limits, and a live save flow. Replaces the Prompts entry for Hermes.
- **Hermes Provider Presets**: Added ~50 Hermes provider presets spanning Nous Research, Shengsuanyun, OpenRouter, DeepSeek, Together AI, StepFun, Zhipu GLM, Bailian, Kimi, MiniMax, DouBao, BaiLing, ModelScope, KAT-Coder, PackyCode, Cubence, AIGoCode, RightCode, AICodeMirror, AICoding, CrazyRouter, SSSAiCode, Micu, CTok.ai, DDSHub, E-FlowCode, LionCCAPI, PIPELLM, Compshare, SiliconFlow, AiHubMix, DMXAPI, TheRouter, Novita, Nvidia, and Xiaomi MiMo.
- **Claude Opus 4.7 Support**: Added Claude Opus 4.7 with adaptive thinking whitelisting, per-million pricing seed, and Bedrock SKU (`anthropic.claude-opus-4-7` / `global.anthropic.claude-opus-4-7`, dropping the legacy `-v1` suffix). Migrated all aggregator and Bedrock presets to Opus 4.7 as the default Opus model.
- **Claude `max` Effort Tier**: Upgraded the Claude effort dropdown from `high` to `max` for extended reasoning capacity.
- **Gemini Native API Proxy**: Added `api_format = "gemini_native"` so the proxy can forward to Google's `generateContent` API with full streaming, schema conversion, and shadow request support. Adds `gemini_url.rs`, `gemini_schema.rs`, `gemini_shadow.rs`, `streaming_gemini.rs`, and `transform_gemini.rs` under the proxy providers module.
- **GitHub Copilot Enterprise Server**: Added GHES authentication and endpoint configuration for Copilot-backed Claude providers, plus thinking-block stripping before upstream to preserve premium interaction quota.
- **Session List Virtualization**: Virtualized the session list via `@tanstack/react-virtual` so long conversations (thousands of records) scroll smoothly; long session messages are now collapsed by default to reduce text layout cost.
- **Codex / OpenClaw Session Title Extraction**: Added meaningful title auto-extraction for Codex and OpenClaw sessions with 2-line display; strips OpenClaw `message_id` suffix noise.
- **Usage Date Range Picker**: Added a date range selector to the usage dashboard with preset tabs (Today / 1d / 7d / 14d / 30d), a custom date + time calendar picker, and a page-jump input on paginated lists.
- **Model Mapping Quick-Set**: Added a quick-set button next to model mapping fields in provider forms for faster edits.
- **Stream Check Error Classification**: Classified Stream Check errors and surfaced them as color-coded toasts; refreshed default probe models and added explicit detection for "model not found" responses.
- **Block Official Provider Switching During Local Routing**: Blocks switching to official providers while Local Routing is active, since routing official API traffic through the local proxy carries account-suspension risk. A warning toast surfaces the block.
- **Pricing Database Refresh (v8 → v9)**: Added ~50 new model pricing entries and corrected stale prices via a reseed-on-migration step, including Claude 4.7, Opus 4.7 Adaptive Thinking, Grok 4, Qwen 3.5/3.6, MiniMax M2.5/M2.7, Doubao Seed 2.0 series, and GLM-5/5.1. DeepSeek and Kimi K2.5 prices updated.
- **Application-Level Window Controls**: Added an opt-in setting to render CC Switch's own minimize / toggle-maximize / close buttons instead of the system decorations, materially improving the experience on Linux Wayland where compositor-drawn buttons can become inert.
- **Hermes in Unified Skills Management**: Added Hermes to the unified Skills surface; skill install, enable, and filter now cover the Hermes app alongside Claude / Codex / Gemini / OpenCode / OpenClaw.
- **OpenClaw Config Directory Override**: Added a settings option to point CC Switch at a custom `openclaw.json` location.
- **Hermes Config Directory Override**: Added a settings option to point CC Switch at a custom `~/.hermes/config.yaml` location, backed by data-driven dispatch.
- **StepFun Step Plan Preset**: Added StepFun Step Plan (EN/ZH) provider presets.
- **New API Usage Script Template**: Added a User-Agent header to the New API usage script template for better upstream compatibility.
- **Launch Hermes Dashboard from Toolbar**: When the Hermes Web UI probe fails, the toolbar entry now offers to run `hermes dashboard` in the user's preferred terminal via a temp bash/batch script. `hermes dashboard` opens the browser itself once ready, so no polling is required. 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`.
- **LemonData Provider Preset (All Six Apps)**: Registered LemonData as a third-party partner preset across Claude, Codex, Gemini, OpenCode, OpenClaw, and Hermes, with icon assets and zh/en/ja partner-promotion copy. Claude uses `ANTHROPIC_API_KEY` auth; OpenAI-compatible apps target `gpt-5.4`.
- **DDSHub Codex Preset**: Added a Codex-compatible endpoint for DDSHub at the same host as its Claude service; base URL omits the `/v1` suffix because the gateway auto-routes OpenAI SDK paths.
### Changed
- **"Local Proxy Takeover" → "Local Routing"**: Unified terminology across UI copy, README, and docs in all three locales. Functional behavior is unchanged.
- **Hermes `Auto` api_mode Removed**: Users must now pick an explicit protocol; new deeplinks default to `chat_completions`. Eliminates URL-based heuristic surprises.
- **Hermes Provider Form**: Added an API mode dropdown and per-provider model editor; bound per-provider models to the top-level `model:` when switching active providers.
- **Hermes Deep Config Delegation**: Deep YAML knobs are now delegated to the Hermes Web UI via a direct launch action, rather than duplicated in the CC Switch form.
- **`ANTHROPIC_REASONING_MODEL` Removed from Claude Quick-Set**: Decoupled the reasoning capability from model selection; the legacy field is no longer surfaced in the quick-set form.
- **Per-Provider Proxy Config Removed**: Consolidated into global Local Routing; the provider-level proxy toggle and associated storage are gone.
- **Unified Toolbar Icon Button Width**: Normalized icon-button widths across Claude / Codex / Gemini / OpenCode / OpenClaw / Hermes panels for a consistent header look.
- **Rust Toolchain Pinned to 1.95**: Adopted clippy 1.95 suggestions across the workspace and pinned the toolchain to prevent nightly drift.
- **Tray Menu ID Constant**: The tray identifier moved from the hardcoded string `"main"` to a `TRAY_ID` constant (`"cc-switch"`) across all call sites.
- **Copilot Request Classification**: Refined request routing inside the Copilot optimizer to further reduce unnecessary premium interaction consumption.
- **Usage Script Intranet Support**: Removed private-IP / suspicious-hostname blocking from usage scripts, unblocking enterprise intranet, Docker, and self-hosted API endpoints. Built-in templates still enforce HTTPS (except localhost) and same-origin checks; custom templates remain user-controlled with those request-URL checks skipped.
- **Failover Queue Notes**: Provider notes now appear in failover queue selectors and queue rows for easier identification across multi-provider queues.
- **Hermes Toolbar Layout**: Swapped the Hermes Web UI button from `ExternalLink` to `LayoutDashboard` (clicking may spawn `hermes dashboard` rather than just opening a URL), and moved MCP to the final toolbar slot so Hermes matches the Claude / Codex / Gemini / OpenCode layout.
### Fixed
- **Header Auto-Compact Latching After Maximize**: The toolbar no longer stays compacted after maximize/restore; compaction now reevaluates on size changes.
- **Hermes YAML Pollution & OAuth MCP Auth Drop**: Round-tripping through CC Switch no longer drops OAuth MCP `auth` blocks or pollutes unrelated YAML keys; guard tests added via `tests/hermes_roundtrip.rs`.
- **Hermes Active Provider Display**: Hermes UI now correctly surfaces the active provider and wires add / enable / remove actions.
- **Hermes Provider Persistence**: Providers persist under `custom_providers:` so `api_mode` and `model` survive restarts and config reloads.
- **Codex `cache_control` Preservation**: Preserve `cache_control` when merging system prompts during Codex format conversion (#1946).
- **Claude Prompt Cache Key Leak**: Stopped sending prompt cache keys during Claude chat conversions (#2003).
- **Proxy Hop-by-Hop Header Stripping**: Strip hop-by-hop response headers (Connection, Keep-Alive, Transfer-Encoding, etc.) per RFC 7230.
- **Permissive Proxy CORS Removed**: Removed the permissive CORS layer from the proxy (#1915).
- **Copilot Premium Consumption**: Further reduced unnecessary Copilot premium interaction consumption during pass-through traffic.
- **Backend Error Details in Proxy Toast**: Surface backend error payload details in proxy-related toast messages instead of a generic failure string.
- **Usage Log Deduplication**: Deduplicated proxy and session-log usage records so the same request is no longer double-counted; synced the request log time range with the dashboard's 1d / 7d / 30d selector.
- **Common Config Checkbox Persistence**: Checkbox state for Claude / Codex / Gemini common-config toggles now persists correctly across reopens.
- **Claude Plugin `settings.json` Sync**: Editing the current provider now syncs back to `settings.json` for the Claude plugin path.
- **Google Official Gemini Env Preservation**: Saving the Google Official Gemini provider no longer clobbers the `env` block.
- **OpenCode JSON5 Parser for Trailing Commas**: OpenCode config reads now tolerate trailing commas via a JSON5 parser.
- **Preset Refreshes**: Refreshed stale context windows for DeepSeek and Claude 1M; refreshed stale model IDs; backfilled Hermes model lists; fixed the Nous endpoint and replaced the Hermes placeholder icon with Nous brand artwork; pruned unused official Hermes presets.
- **Auto-Expand Collapsed Messages on Search Hit**: Collapsed messages now auto-expand when a search match lands inside hidden content.
- **Unknown Subscription Quota Tiers Hidden**: Provider cards no longer render unknown subscription quota tiers.
- **Weekly Limit Label Unified**: Aligned the weekly_limit tier label with the official 7-day naming across locales.
- **Root-Level Skill Repo Install**: Fixed skill installation when the repository root itself is a skill.
- **Session ID Parsing Clippy**: Removed a redundant closure in session ID parsing (clippy warning).
- **Usage Log Stat Dedup**: Deduplicated proxy-sourced and session-log-sourced usage records for accurate totals.
- **Stream Check Default Models Refresh**: Updated stream-check default probe models to match each vendor's current lineup.
- **Skills Import Sync**: Imported Skills are now immediately synced into enabled app directories instead of only being recorded in the database, so the UI no longer shows "installed" while the target app directory is missing the skill.
- **Ghostty Session Restore**: Fixed Ghostty session restore launch by using shell execution with `--working-directory`, avoiding `cwd` escaping issues when the path contains spaces or special characters.
- **Hermes Health Check Borrowing OpenClaw Schema**: Hermes providers were routed through `check_additive_app_stream` (the OpenClaw dispatcher), which reads camelCase `baseUrl` / `apiKey` / `api` and surfaced "OpenClaw provider is missing baseUrl" even when every Hermes field was filled. Introduced `check_hermes_stream` with Hermes-specific extractors that map `api_mode` (`chat_completions` / `anthropic_messages` / `codex_responses`) to the matching `check_claude_stream` `api_format`, and returns `bedrock_converse` as unsupported. `api_mode` is now resolved before URL / API key extraction, so `bedrock_converse` users see the real cause rather than a misleading "missing base_url".
- **Usage Query Modal for Hermes & OpenClaw**: `getProviderCredentials` now reads flat `settingsConfig` fields for Hermes (snake_case `base_url` / `api_key`) and OpenClaw (camelCase `baseUrl` / `apiKey`), so the "official balance" template auto-selects for matching providers like SiliconFlow. Also refactored the BALANCE and TOKEN_PLAN test paths to reuse the precomputed `providerCredentials` instead of re-reading `env.ANTHROPIC_*` directly, fixing the "empty key" error for non-Claude apps even when the key was configured.
### Docs
- **README Sponsor Updates**: Updated SiliconFlow signup bonus to ¥16, trimmed the SSSAiCode sponsor blurb, updated partner logos, and added LemonData as a new sponsor.
- **Global Proxy Hint Clarified**: Clarified the global proxy hint about local routing across all three locales.
- **Takeover → Routing Rename**: Renamed takeover docs to routing and updated anchors across all languages.
- **PIPELLM Website URL**: Updated the PIPELLM sponsor website URL to `code.pipellm.ai`.
### Breaking
- **Hermes requires explicit `api_mode`**: The `Auto` mode is gone; imported or deeplinked providers default to `chat_completions`. Users with prior `Auto` configs will be prompted to pick a protocol.
- **`ANTHROPIC_REASONING_MODEL` removed from Claude quick-set**: The legacy field is no longer exposed; existing settings are cleaned up automatically.
- **Per-provider proxy configuration removed**: Migrate to the global Local Routing setting. Existing per-provider proxy values are ignored.
- **Database schema bumped v9 → v10**: Adds `enabled_hermes` columns to `mcp_servers` and `skills` (auto-migrated with `DEFAULT 0`; no data loss).
- **Pricing table reseeded (v8 → v9)**: The `model_pricing` table is cleared and reseeded on first launch to pick up new models and corrected prices.
- **XCodeAPI preset removed**: Users of the XCodeAPI preset should switch to another provider.
---
## [3.13.0] - 2026-04-10
Development since v3.12.3 focuses on quota visibility, provider workflow upgrades, stronger proxy compatibility, and lower-overhead tray / session workflows.
+31 -49
View File
@@ -2,7 +2,7 @@
# CC Switch
### The All-in-One Manager for Claude Code, Codex, Gemini CLI, OpenCode, OpenClaw & Hermes Agent
### The All-in-One Manager for Claude Code, Codex, Gemini CLI, OpenCode & OpenClaw
[![Version](https://img.shields.io/github/v/release/farion1231/cc-switch?color=blue&label=version)](https://github.com/farion1231/cc-switch/releases)
[![Platform](https://img.shields.io/badge/platform-Windows%20%7C%20macOS%20%7C%20Linux-lightgrey.svg)](https://github.com/farion1231/cc-switch/releases)
@@ -11,8 +11,6 @@
<a href="https://trendshift.io/repositories/15372" target="_blank"><img src="https://trendshift.io/api/badge/repositories/15372" alt="farion1231%2Fcc-switch | Trendshift" style="width: 250px; height: 55px;" width="250" height="55"/></a>
### 🌐 The Only Official Website: **[ccswitch.io](https://ccswitch.io)**
English | [中文](README_ZH.md) | [日本語](README_JA.md) | [Changelog](CHANGELOG.md)
</div>
@@ -39,38 +37,26 @@ MiniMax-M2.7 is a next-generation large language model designed for autonomous e
</tr>
<tr>
<td width="180"><a href="https://aigocode.com/invite/CC-SWITCH"><img src="assets/partners/logos/aigocode.png" alt="AIGoCode" width="150"></a></td>
<td>Thanks to AIGoCode for sponsoring this project! AIGoCode is an all-in-one platform that integrates Claude Code, Codex, and the latest Gemini models, providing you with stable, efficient, and highly cost-effective AI coding services. The platform offers flexible subscription plans, zero risk of account suspension, direct access with no VPN required, and lightning-fast responses. AIGoCode has prepared a special benefit for CC Switch users: if you register via <a href="https://aigocode.com/invite/CC-SWITCH">this link</a>, you'll receive an extra 10% bonus credit on your first top-up!</td>
<td width="180"><a href="https://cloud.siliconflow.cn/i/drGuwc9k"><img src="assets/partners/logos/silicon_en.jpg" alt="SiliconFlow" width="150"></a></td>
<td>Thanks to SiliconFlow for sponsoring this project! SiliconFlow is a high-performance AI infrastructure and model API platform, providing fast and reliable access to language, speech, image, and video models in one place. With pay-as-you-go billing, broad multimodal model support, high-speed inference, and enterprise-grade stability, SiliconFlow helps developers and teams build and scale AI applications more efficiently. Register via <a href="https://cloud.siliconflow.cn/i/drGuwc9k">this link</a> and complete real-name verification to receive ¥20 in bonus credit, usable across models on the platform. SiliconFlow is also now compatible with OpenClaw, allowing users to connect a SiliconFlow API key and call major AI models for free.</td>
</tr>
<tr>
<td width="180"><a href="https://www.shengsuanyun.com/?from=CH_4HHXMRYF"><img src="assets/partners/logos/shengsuanyun.png" alt="Shengsuanyun" width="150"></a></td>
<td width="180"><a href="https://www.shengsuanyun.com/?from=CH_4HHXMRYF"><img src="assets/partners/logos/shengsuanyun.svg" alt="Shengsuanyun" width="150"></a></td>
<td>Thanks to Shengsuanyun for sponsoring this project! Shengsuanyun is a super factory serving AI Native Teams — an industrial-grade AI task parallel execution platform. Its model marketplace aggregates Claude, ChatGPT, Gemini, and other domestic and international LLM and multimedia model capabilities with direct supply. Absolutely no reverse engineering or dilution — platform-wide model SLA availability reaches 99.7%, with <a href="https://watch.shengsuanyun.com/status/shengsuanyun">monitoring dashboards</a> showing green across the board. It also offers enterprise-grade custom gateways for fine-grained team cost and permission management, smart routing, security protection, and BYOK (Bring Your Own Key) hosting. The platform charges on a pay-per-use and tokens plan (coming soon) basis, with invoicing available. Register via <a href="https://www.shengsuanyun.com/?from=CH_4HHXMRYF">this link</a> as a new user to receive ¥10 in credits plus a 10% bonus on your first top-up.</td>
</tr>
<tr>
<td width="180"><a href="https://aigocode.com/invite/CC-SWITCH"><img src="assets/partners/logos/aigocode.png" alt="AIGoCode" width="150"></a></td>
<td>Thanks to AIGoCode for sponsoring this project! AIGoCode is an all-in-one platform that integrates Claude Code, Codex, and the latest Gemini models, providing you with stable, efficient, and highly cost-effective AI coding services. The platform offers flexible subscription plans, zero risk of account suspension, direct access with no VPN required, and lightning-fast responses. AIGoCode has prepared a special benefit for CC Switch users: if you register via <a href="https://aigocode.com/invite/CC-SWITCH">this link</a>, you'll receive an extra 10% bonus credit on your first top-up!</td>
</tr>
<tr>
<td width="180"><a href="https://www.aicodemirror.com/register?invitecode=9915W3"><img src="assets/partners/logos/aicodemirror.jpg" alt="AICodeMirror" width="150"></a></td>
<td>Thanks to AICodeMirror for sponsoring this project! AICodeMirror provides official high-stability relay services for Claude Code / Codex / Gemini CLI, with enterprise-grade concurrency, fast invoicing, and 24/7 dedicated technical support.
Claude Code / Codex / Gemini official channels at 38% / 2% / 9% of original price, with extra discounts on top-ups! AICodeMirror offers special benefits for CC Switch users: register via <a href="https://www.aicodemirror.com/register?invitecode=9915W3">this link</a> to enjoy 20% off your first top-up, and enterprise customers can get up to 25% off!</td>
</tr>
<tr>
<td width="180"><a href="https://pateway.ai/?ch=etzpm8&aff=WB6M6F67#/"><img src="assets/partners/logos/pateway.png" alt="PatewayAI" width="150"></a></td>
<td>Thanks to PatewayAI for sponsoring this project! PatewayAI is an API relay service provider built for heavy AI developers, focused on directly relaying official high-quality model APIs. It offers the full Claude lineup and the Codex series, 100% sourced from official channels — no dilution, no fakes, verification welcome. Billing is transparent and every token-level invoice can be audited line by line.
It also supports enterprise-grade concurrency and provides a dedicated management platform for enterprise customers — formal contracts and invoicing are available; visit the official website for contact details.
Register now via <a href="https://pateway.ai/?ch=etzpm8&aff=WB6M6F67#/">this link</a> to receive $3 in trial credit. Top-ups go as low as 60% of the original price, with a two-way referral bonus of up to $150!</td>
</tr>
<tr>
<td width="180"><a href="https://www.byteplus.com/en/product/modelark?utm_campaign=hw&utm_content=ccswitch&utm_medium=devrel_tool_web&utm_source=OWO&utm_term=ccswitch"><img src="assets/partners/logos/byteplus.png" alt="BytePlus" width="150"></a></td>
<td>Thanks to Dola seed for sponsoring this project! Dola Seed 2.0 is a fullmodal general large model independently developed by ByteDance for the global market. Built on a unified multimodal architecture, it supports joint understanding and generation of text, images, audio, and video. It natively enables agent collaboration, with strong reasoning, longtask execution, tool integration, and coding capabilities. It is widely applicable to smart cockpits, personal assistants, education, customer support, marketing, retail, and other scenarios. It excels in multimodal perception, endtoend complex task delivery, stable interaction, and data security, and is readily accessible and deployable via the ModelArk platform.Register via <a href="https://www.byteplus.com/en/product/modelark?utm_campaign=hw&utm_content=ccswitch&utm_medium=devrel_tool_web&utm_source=OWO&utm_term=ccswitch">this link</a> to get 500,000 tokens of free inference quota per model.<a href="https://www.volcengine.com/activity/agentplan?utm_campaign=hw&utm_content=ccswitch&utm_medium=devrel_tool_web&utm_source=OWO&utm_term=ccswitch"> >>中国大陆地区的开发者请点击这里</a></td>
</tr>
<tr>
<td width="180"><a href="https://cloud.siliconflow.cn/i/drGuwc9k"><img src="assets/partners/logos/silicon_en.jpg" alt="SiliconFlow" width="150"></a></td>
<td>Thanks to SiliconFlow for sponsoring this project! SiliconFlow is a high-performance AI infrastructure and model API platform, providing fast and reliable access to language, speech, image, and video models in one place. With pay-as-you-go billing, broad multimodal model support, high-speed inference, and enterprise-grade stability, SiliconFlow helps developers and teams build and scale AI applications more efficiently. Register via <a href="https://cloud.siliconflow.cn/i/drGuwc9k">this link</a> and complete real-name verification to receive ¥16 in bonus credit, usable across models on the platform. SiliconFlow is also now compatible with OpenClaw, allowing users to connect a SiliconFlow API key and call major AI models for free.</td>
</tr>
<tr>
<td width="180"><a href="https://cubence.com/signup?code=CCSWITCH&source=ccs"><img src="assets/partners/logos/cubence.png" alt="Cubence" width="150"></a></td>
<td>Thanks to Cubence for sponsoring this project! Cubence is a reliable and efficient API relay service provider, offering relay services for Claude Code, Codex, Gemini, and more with flexible billing options including pay-as-you-go and monthly plans. Cubence provides special discounts for CC Switch users: register using <a href="https://cubence.com/signup?code=CCSWITCH&source=ccs">this link</a> and enter the "CCSWITCH" promo code during recharge to get 10% off every top-up!</td>
@@ -83,7 +69,12 @@ Register now via <a href="https://pateway.ai/?ch=etzpm8&aff=WB6M6F67#/">this lin
<tr>
<td width="180"><a href="https://www.compshare.cn/coding-plan?ytag=GPU_YY_YX_git_cc-switch"><img src="assets/partners/logos/ucloud.png" alt="Compshare" width="150"></a></td>
<td>Thanks to Compshare for sponsoring this project! Compshare is UCloud's AI cloud platform, providing stable and comprehensive domestic and international model APIs with just one key. Featuring cost-effective monthly and per-use domestic-model Coding Plan packages, alongside stable officially-relayed overseas models. Supports Claude Code, Codex, and API access. Enterprise-grade high concurrency, 24/7 technical support, and self-service invoicing. Users who register via <a href="https://www.compshare.cn/coding-plan?ytag=GPU_YY_YX_git_cc-switch">this link</a> will receive a free 5 CNY platform trial credit!</td>
<td>Thanks to Compshare for sponsoring this project! Compshare is UCloud's AI cloud platform, providing stable and comprehensive domestic and international model APIs with just one key. Featuring cost-effective monthly and pay-as-you-go Coding Plan packages at 60-80% off official prices. Supports Claude Code, Codex, and API access. Enterprise-grade high concurrency, 24/7 technical support, and self-service invoicing. Users who register via <a href="https://www.compshare.cn/coding-plan?ytag=GPU_YY_YX_git_cc-switch">this link</a> will receive a free 5 CNY platform trial credit!</td>
</tr>
<tr>
<td width="180"><a href="https://www.right.codes/register?aff=CCSWITCH"><img src="assets/partners/logos/rightcode.jpg" alt="RightCode" width="150"></a></td>
<td>Thank you to Right Code for sponsoring this project! Right Code reliably provides routing services for models such as Claude Code, Codex, and Gemini. It features a highly cost-effective Codex monthly subscription plan and <strong>supports quota rollovers—unused quota from one day can be carried over and used the next day.</strong> Invoices are available upon top-up. Enterprise and team users can receive dedicated one-on-one support. Right Code also offers an exclusive discount for CC Switch users: register via <a href="https://www.right.codes/register?aff=CCSWITCH">this link</a>, and with every top-up you will receive pay-as-you-go credit equivalent to 25% of the amount paid.</td>
</tr>
<tr>
@@ -92,28 +83,23 @@ Register now via <a href="https://pateway.ai/?ch=etzpm8&aff=WB6M6F67#/">this lin
</tr>
<tr>
<td width="180"><a href="https://crazyrouter.com/register?aff=OZcm&ref=cc-switch"><img src="assets/partners/logos/crazyrouter.png" alt="Crazyrouter" width="150"></a></td>
<td>Thanks to Crazyrouter for sponsoring this project! Crazyrouter is a high-performance AI API aggregation platform — one API key for 300+ models including Claude Code, Codex, Gemini CLI, and more. All models at 55% of official pricing with auto-failover, smart routing, and unlimited concurrency. Crazyrouter offers an exclusive deal for CC Switch users: register via <a href="https://crazyrouter.com/register?aff=OZcm&ref=cc-switch">this link</a> and contact customer support to claim <strong>$2 free credit</strong>, plus enter promo code `CCSWITCH` on your first top-up for an extra <strong>30% bonus credit</strong>! </td>
</tr>
<tr>
<td width="180"><a href="https://www.right.codes/register?aff=CCSWITCH"><img src="assets/partners/logos/rightcode.jpg" alt="RightCode" width="150"></a></td>
<td>Thank you to Right Code for sponsoring this project! Right Code reliably provides routing services for models such as Claude Code, Codex, and Gemini, with both pay-as-you-go and monthly subscription billing options available. Invoices are available upon top-up, and enterprise and team users can receive dedicated one-on-one support. Right Code also offers an exclusive discount for CC Switch users: register via <a href="https://www.right.codes/register?aff=CCSWITCH">this link</a>, and with every top-up you will receive pay-as-you-go credit equivalent to 5% of the amount paid.</td>
<td width="180"><a href="https://crazyrouter.com/register?aff=OZcm&ref=cc-switch"><img src="assets/partners/logos/crazyrouter.jpg" alt="Crazyrouter" width="150"></a></td>
<td>Thanks to Crazyrouter for sponsoring this project! Crazyrouter is a high-performance AI API aggregation platform — one API key for 300+ models including Claude Code, Codex, Gemini CLI, and more. All models at 55% of official pricing with auto-failover, smart routing, and unlimited concurrency. Crazyrouter offers an exclusive deal for CC Switch users: register via <a href="https://crazyrouter.com/register?aff=OZcm&ref=cc-switch">this link</a> to get <strong>$2 free credit</strong> instantly, plus enter promo code `CCSWITCH` on your first top-up for an extra <strong>30% bonus credit</strong>! </td>
</tr>
<tr>
<td width="180"><a href="https://www.sssaicode.com/register?ref=DCP0SM"><img src="assets/partners/logos/sssaicode.png" alt="SSSAiCode" width="150"></a></td>
<td>Thanks to SSSAiCode for sponsoring this project! SSSAiCode is a stable and reliable API relay service, dedicated to providing stable, reliable, and affordable Claude and Codex model services, with same-day fast invoicing. SSSAiCode offers a special deal for CC Switch users: register via <a href="https://www.sssaicode.com/register?ref=DCP0SM">this link</a> to enjoy $10 extra credit on every top-up!</td>
<td>Thanks to SSSAiCode for sponsoring this project! SSSAiCode is a stable and reliable API relay service, dedicated to providing stable, reliable, and affordable Claude and Codex model services, <strong>offering high cost-effective official Claude service at just ¥0.5/$ equivalent</strong>, supporting monthly and pay-as-you-go billing plans with same-day fast invoicing. SSSAiCode offers a special deal for CC Switch users: register via <a href="https://www.sssaicode.com/register?ref=DCP0SM">this link</a> to enjoy $10 extra credit on every top-up!</td>
</tr>
<tr>
<td width="180"><a href="https://www.micuapi.ai/register?aff=aOYQ"><img src="assets/partners/logos/mikubanner.svg" alt="Micu" width="150"></a></td>
<td>Thanks to Micu API for sponsoring this project! Micu API is a global LLM relay service provider dedicated to delivering the best cost-performance ratio with high stability. Backed by a registered enterprise for core assurance, eliminating any risk of service discontinuation, with fast official invoicing support! We champion "zero cost to try": top up from as low as ¥1 with no minimum, and get fee-free refunds anytime! Micu API offers an exclusive deal for CC Switch users: register via <a href="https://www.micuapi.ai/register?aff=aOYQ">this link</a> and enter promo code "ccswitch" when topping up to enjoy a <strong>10% discount</strong>!</td>
<td width="180"><a href="https://www.openclaudecode.cn/register?aff=aOYQ"><img src="assets/partners/logos/mikubanner.svg" alt="Micu" width="150"></a></td>
<td>Thanks to Micu API for sponsoring this project! Micu API is a global LLM relay service provider dedicated to delivering the best cost-performance ratio with high stability. Backed by a registered enterprise for core assurance, eliminating any risk of service discontinuation, with fast official invoicing support! We champion "zero cost to try": top up from as low as ¥1 with no minimum, and get fee-free refunds anytime! Micu API offers an exclusive deal for CC Switch users: register via <a href="https://www.openclaudecode.cn/register?aff=aOYQ">this link</a> and enter promo code "ccswitch" when topping up to enjoy a <strong>10% discount</strong>!</td>
</tr>
<tr>
<td width="180"><a href="https://lemondata.cc/r/FFX1ZDUP"><img src="assets/partners/logos/lemondata.png" alt="LemonData" width="150"></a></td>
<td>Thanks to LemonData for sponsoring this project! LemonData is a high-performance AI API aggregation platform — one API key for 300+ models including GPT, Claude, Gemini, DeepSeek, and more. All models priced 3070% below official rates with auto-failover, smart routing, and unlimited concurrency. New users get $1 free credit instantly upon registration — sign up via <a href="https://lemondata.cc/r/FFX1ZDUP">this link</a>to claim your bonus and start building right away</strong>!</td>
<td width="180"><a href="https://x-code.cc/register?aff=IbPp"><img src="assets/partners/logos/xcodeapi.png" alt="XCodeAPI" width="150"></a></td>
<td>Thanks to XCodeAPI for sponsoring this project! XCodeAPI offers a special benefit for CC Switch users: register via <a href="https://x-code.cc/register?aff=IbPp">this link</a> and get an extra 10% credit bonus on your first order! (Contact the site admin to claim)</td>
</tr>
<tr>
@@ -121,24 +107,20 @@ Register now via <a href="https://pateway.ai/?ch=etzpm8&aff=WB6M6F67#/">this lin
<td>Thanks to CTok.ai for sponsoring this project! CTok.ai is dedicated to building a one-stop AI programming tool service platform. We offer professional Claude Code packages and technical community services, with support for Google Gemini and OpenAI Codex. Through carefully designed plans and a professional tech community, we provide developers with reliable service guarantees and continuous technical support, making AI-assisted programming a true productivity tool. Click <a href="https://ctok.ai">here</a> to register!</td>
</tr>
<tr>
<td width="180"><a href="https://chefshop.ai"><img src="assets/partners/logos/chefshop.png" alt="ChefShop" width="150"></a></td>
<td>Thanks to ChefShop AI for sponsoring this project! ChefShop AI is a premium account service provider tailored for heavy AI subscription users. The platform offers official top-up and stable account services for mainstream large models including ChatGPT Plus/Pro, Claude Max, Grok Super/Heavy, and Gemini. Click <a href="https://chefshop.ai">here</a> to purchase!</td>
</tr>
<tr>
<td width="180"><a href="https://vibecodingapi.ai"><img src="assets/partners/logos/lioncc.png" alt="LionCC" width="150"></a></td>
<td>Thanks to LionCC for sponsoring this project! LionCC is built for Vibe Coders who pursue the ultimate development experience. We provide stable, low-latency, and competitively priced computing services for Claude Code, Codex, and OpenClaw, saving up to 50% in costs. After registering, add customer service on WeChat (HSQBJ088888888) with the code "cc-switch" to receive $10 in free credits (10 million tokens). For other collaborations, follow the blog @LionCC.ai. Click <a href="https://vibecodingapi.ai">here</a> to register!</td>
</tr>
<tr>
<td width="180"><a href="https://console.claudeapi.com/register?aff=pCLD"><img src="assets/partners/logos/claudeapi.png" alt="ClaudeAPI" width="150"></a></td>
<td>This project is sponsored by <a href="https://console.claudeapi.com/register?aff=pCLD">Claude API</a>. Direct Claude API access — connect Claude Code and Agent apps in 3 minutes. New users can claim a free trial credit.Powered by official Anthropic API keys + AWS Bedrock official channels. No reverse engineering, no model degradation. Full support for Opus / Sonnet / Haiku model lineup, with official capabilities preserved including Tool Use, 1M context window, and more. Built for Claude Code power users, Agent engineers, and enterprise engineering teams. Invoicing and dedicated team support available. Click <a href="https://console.claudeapi.com/register?aff=pCLD">here</a> to register!</td>
</tr>
<tr>
<td width="180"><a href="https://claudecn.top"><img src="assets/partners/logos/claudecn.jpg" alt="ClaudeCN" width="150"></a></td>
<td>Thanks to ClaudeCN for sponsoring this project! ClaudeCN is an enterprise-grade AI gateway platform operated by a registered company. It delivers high-availability commercial API access to popular models including Claude, GPT, and DeepSeek, and is built around formal enterprise procurement workflows — corporate bank transfers, signed contracts, and full compliance. Register via <a href="https://claudecn.top">this link</a>!</td>
</tr>
<tr>
<td width="180"><a href="https://runapi.co"><img src="assets/partners/logos/runapi.jpg" alt="RunAPI" width="150"></a></td>
<td>Thanks to RunAPI for sponsoring this project! RunAPI is a high-performance and reliable AI model API gateway — one API key gives you access to 150+ mainstream models including OpenAI, Claude, Gemini, DeepSeek, and Grok, with prices as low as 10% of the official rate and excellent stability. It works seamlessly with Claude Code, OpenClaw, and other tools. Exclusive benefit for CC Switch users: register and contact customer support to claim a free ¥14 credit. Register via <a href="https://runapi.co">this link</a>!</td>
<td width="180"><a href="https://ddshub.short.gy/ccswitch"><img src="assets/partners/logos/dds.png" alt="DDS" width="150"></a></td>
<td>Thanks to DDS for sponsoring this project! DDS Hub is a reliable and high-performance Claude API proxy service. We provides cost-effective domestic Claude direct acceleration services for both individual and enterprise users. We offer stable and low-latency Claude Max number pools, with full support for Claude Haiku, Opus, Sonnet and other flagship models. Invoices are available for recharges of 1000 RMB or more. Enterprise customers can also enjoy customized grouping and dedicated technical support services.
Exclusive benefit for CC Switch users: Register via <a href="https://ddshub.short.gy/ccswitch">the link </a>below and enjoy an extra 10% credit on your first recharge (please contact the group admin to claim after recharging)!</td>
</tr>
</table>
@@ -167,7 +149,7 @@ Modern AI-powered coding relies on CLI tools like Claude Code, Codex, Gemini CLI
## Features
[Full Changelog](CHANGELOG.md) | [Release Notes](docs/release-notes/v3.15.0-en.md)
[Full Changelog](CHANGELOG.md) | [Release Notes](docs/release-notes/v3.12.3-en.md)
### Provider Management
+33 -49
View File
@@ -2,7 +2,7 @@
# CC Switch
### Claude Code、Codex、Gemini CLI、OpenCode、OpenClaw、Hermes Agent のオールインワン管理ツール
### Claude Code、Codex、Gemini CLI、OpenCode、OpenClaw のオールインワン管理ツール
[![Version](https://img.shields.io/github/v/release/farion1231/cc-switch?color=blue&label=version)](https://github.com/farion1231/cc-switch/releases)
[![Platform](https://img.shields.io/badge/platform-Windows%20%7C%20macOS%20%7C%20Linux-lightgrey.svg)](https://github.com/farion1231/cc-switch/releases)
@@ -11,8 +11,6 @@
<a href="https://trendshift.io/repositories/15372" target="_blank"><img src="https://trendshift.io/api/badge/repositories/15372" alt="farion1231%2Fcc-switch | Trendshift" style="width: 250px; height: 55px;" width="250" height="55"/></a>
### 🌐 唯一の公式サイト:**[ccswitch.io](https://ccswitch.io)**
[English](README.md) | [中文](README_ZH.md) | 日本語 | [Changelog](CHANGELOG.md)
</div>
@@ -39,38 +37,26 @@ MiniMax-M2.7 は、自律的進化と実世界の生産性向上のために設
</tr>
<tr>
<td width="180"><a href="https://aigocode.com/invite/CC-SWITCH"><img src="assets/partners/logos/aigocode.png" alt="AIGoCode" width="150"></a></td>
<td>本プロジェクトは AIGoCode のスポンサー提供でお届けしています。AIGoCode は、Claude Code・Codex・最新の Gemini モデルを統合したオールインワンのAIコーディングプラットフォームで、安定性・高速性・コストパフォーマンスに優れた開発サービスを提供します。柔軟なサブスクリプションプランを備え、レスポンスも非常に高速です。さらに、CC Switch ユーザー向けの特典として、<a href="https://aigocode.com/invite/CC-SWITCH">このリンク</a>から登録すると、初回チャージ時に10%分のボーナスクレジットが付与されます!</td>
<td width="180"><a href="https://cloud.siliconflow.cn/i/drGuwc9k"><img src="assets/partners/logos/silicon_en.jpg" alt="SiliconFlow" width="150"></a></td>
<td>SiliconFlow のご支援に感謝します!SiliconFlow は高性能 AI インフラストラクチャおよびモデル API プラットフォームで、言語・音声・画像・動画モデルへの高速かつ信頼性の高いアクセスをワンストップで提供します。従量課金制、豊富なマルチモーダルモデル対応、高速推論、エンタープライズグレードの安定性を備え、開発者やチームがより効率的に AI アプリケーションを構築・拡張できるようサポートします。<a href="https://cloud.siliconflow.cn/i/drGuwc9k">このリンク</a>から登録し、本人確認を完了すると、プラットフォーム内の全モデルで利用可能な ¥20 のボーナスクレジットが付与されます。SiliconFlow は OpenClaw にも対応しており、SiliconFlow の API キーを接続することで主要な AI モデルを無料で呼び出すことができます。</td>
</tr>
<tr>
<td width="180"><a href="https://www.shengsuanyun.com/?from=CH_4HHXMRYF"><img src="assets/partners/logos/shengsuanyun.png" alt="Shengsuanyun" width="150"></a></td>
<td width="180"><a href="https://www.shengsuanyun.com/?from=CH_4HHXMRYF"><img src="assets/partners/logos/shengsuanyun.svg" alt="Shengsuanyun" width="150"></a></td>
<td>胜算雲(Shengsuanyun)のご支援に感謝します!胜算雲は AI ネイティブチーム向けのスーパーファクトリーであり、産業グレードの AI タスク並列実行プラットフォームです。モデルマーケットプレイスでは Claude、ChatGPT、Gemini をはじめとする国内外の LLM およびマルチメディアモデルの計算リソースを集約・直接提供。リバースエンジニアリングや品質低下は一切なく、プラットフォーム全体のモデル SLA 可用性は 99.7% に達し、<a href="https://watch.shengsuanyun.com/status/shengsuanyun">監視ダッシュボード</a>は常時グリーン表示です。さらにエンタープライズ向けカスタムゲートウェイを提供し、チームのきめ細かなコスト・権限管理、スマートルーティング、セキュリティ保護、BYOK(自社キー持ち込み)ホスティングを実現します。従量課金およびトークンプラン(近日公開)対応で、請求書発行にも対応。<a href="https://www.shengsuanyun.com/?from=CH_4HHXMRYF">このリンク</a>から新規登録すると 10 元分のクレジットと初回チャージ 10% ボーナスが付与されます。</td>
</tr>
<tr>
<td width="180"><a href="https://aigocode.com/invite/CC-SWITCH"><img src="assets/partners/logos/aigocode.png" alt="AIGoCode" width="150"></a></td>
<td>本プロジェクトは AIGoCode のスポンサー提供でお届けしています。AIGoCode は、Claude Code・Codex・最新の Gemini モデルを統合したオールインワンのAIコーディングプラットフォームで、安定性・高速性・コストパフォーマンスに優れた開発サービスを提供します。柔軟なサブスクリプションプランを備え、レスポンスも非常に高速です。さらに、CC Switch ユーザー向けの特典として、<a href="https://aigocode.com/invite/CC-SWITCH">このリンク</a>から登録すると、初回チャージ時に10%分のボーナスクレジットが付与されます!</td>
</tr>
<tr>
<td width="180"><a href="https://www.aicodemirror.com/register?invitecode=9915W3"><img src="assets/partners/logos/aicodemirror.jpg" alt="AICodeMirror" width="150"></a></td>
<td>AICodeMirror のご支援に感謝します!AICodeMirror は Claude Code / Codex / Gemini CLI の公式高安定リレーサービスを提供しており、エンタープライズ級の同時接続、迅速な請求書発行、24時間年中無休の専用テクニカルサポートを備えています。
Claude Code / Codex / Gemini 公式チャンネルが最安で元価格の 38% / 2% / 9%、チャージ時にはさらに割引!AICodeMirror は CC Switch ユーザー向けに特別特典を用意:<a href="https://www.aicodemirror.com/register?invitecode=9915W3">このリンク</a>から登録すると初回チャージ 20% オフ、法人のお客様は最大 25% オフ!</td>
</tr>
<tr>
<td width="180"><a href="https://pateway.ai/?ch=etzpm8&aff=WB6M6F67#/"><img src="assets/partners/logos/pateway.png" alt="PatewayAI" width="150"></a></td>
<td>PatewayAI のご支援に感謝します!PatewayAI はヘビーな AI 開発者向けに、公式直結の高品質モデル API 中継サービスを専門に提供するプロバイダーです。Claude シリーズ全モデルおよび Codex シリーズに対応し、100% 公式ソースから直接提供。混ぜ物・水増しは一切なく、検証も歓迎します。課金は透明で、トークン単位の請求書を 1 件ずつ照合可能です。
エンタープライズ級の高同時接続にも対応し、法人のお客様には専用の管理プラットフォームを提供。正式契約および請求書発行に対応しており、詳細は公式サイトの連絡先よりお問い合わせください。
現在、<a href="https://pateway.ai/?ch=etzpm8&aff=WB6M6F67#/">このリンク</a>からご登録いただくと $3 のトライアルクレジットを進呈。チャージは最安で元価格の 60%、友達紹介は双方にボーナスが付与され、紹介報酬は最大 $150!</td>
</tr>
<tr>
<td width="180"><a href="https://www.byteplus.com/en/product/modelark?utm_campaign=hw&utm_content=ccswitch&utm_medium=devrel_tool_web&utm_source=OWO&utm_term=ccswitch"><img src="assets/partners/logos/byteplus.png" alt="BytePlus" width="150"></a></td>
<td>Dola seed のご支援に感謝します!Dola Seed 2.0 は ByteDance がグローバル市場向けに独自開発したフルモーダル汎用大規模モデルです。統一されたマルチモーダルアーキテクチャを基盤に、テキスト・画像・音声・動画の統合的な理解と生成をサポートします。エージェント連携をネイティブに実現し、強力な推論、長時間タスクの実行、ツール統合、コーディング能力を備えています。スマートコックピット、パーソナルアシスタント、教育、カスタマーサポート、マーケティング、リテールなど幅広いシナリオに適用可能で、マルチモーダル認識、エンドツーエンドの複雑なタスク遂行、安定したインタラクション、データセキュリティに優れ、ModelArk プラットフォームを通じて手軽に利用・デプロイできます。<a href="https://www.byteplus.com/en/product/modelark?utm_campaign=hw&utm_content=ccswitch&utm_medium=devrel_tool_web&utm_source=OWO&utm_term=ccswitch">このリンク</a>からご登録いただくと、モデルごとに 500,000 トークンの無料推論クォータを進呈します。</td>
</tr>
<tr>
<td width="180"><a href="https://cloud.siliconflow.cn/i/drGuwc9k"><img src="assets/partners/logos/silicon_en.jpg" alt="SiliconFlow" width="150"></a></td>
<td>SiliconFlow のご支援に感謝します!SiliconFlow は高性能 AI インフラストラクチャおよびモデル API プラットフォームで、言語・音声・画像・動画モデルへの高速かつ信頼性の高いアクセスをワンストップで提供します。従量課金制、豊富なマルチモーダルモデル対応、高速推論、エンタープライズグレードの安定性を備え、開発者やチームがより効率的に AI アプリケーションを構築・拡張できるようサポートします。<a href="https://cloud.siliconflow.cn/i/drGuwc9k">このリンク</a>から登録し、本人確認を完了すると、プラットフォーム内の全モデルで利用可能な ¥16 のボーナスクレジットが付与されます。SiliconFlow は OpenClaw にも対応しており、SiliconFlow の API キーを接続することで主要な AI モデルを無料で呼び出すことができます。</td>
</tr>
<tr>
<td width="180"><a href="https://cubence.com/signup?code=CCSWITCH&source=ccs"><img src="assets/partners/logos/cubence.png" alt="Cubence" width="150"></a></td>
<td>Cubence のご支援に感謝します!Cubence は Claude Code、Codex、Gemini などのリレーサービスを提供する信頼性の高い API 中継プラットフォームで、従量課金や月額プランなど柔軟な料金体系を提供しています。CC Switch ユーザー向けの特別割引:<a href="https://cubence.com/signup?code=CCSWITCH&source=ccs">このリンク</a>で登録し、チャージ時に「CCSWITCH」クーポンを入力すると、毎回 10% オフになります!</td>
@@ -83,7 +69,12 @@ Claude Code / Codex / Gemini 公式チャンネルが最安で元価格の 38% /
<tr>
<td width="180"><a href="https://www.compshare.cn/coding-plan?ytag=GPU_YY_YX_git_cc-switch"><img src="assets/partners/logos/ucloud.png" alt="Compshare" width="150"></a></td>
<td>Compshare のご支援に感謝します!Compshare は UCloud 傘下の AI クラウドプラットフォームで、国内外の安定した包括的なモデル API を 1 つのキーだけで利用可能。月額・都度課金のコストパフォーマンスに優れた国内モデル Coding Plan パッケージを提供し、公式リレーによる安定した海外モデルも利用できます。Claude Code、Codex および API アクセスに対応。エンタープライズ級の高同時接続、24 時間年中無休のテクニカルサポート、セルフサービス請求書発行に対応。<a href="https://www.compshare.cn/coding-plan?ytag=GPU_YY_YX_git_cc-switch">こちらのリンク</a>から登録すると、無料で 5 元分のプラットフォーム体験クレジットがもらえます!</td>
<td>Compshare のご支援に感謝します!Compshare は UCloud 傘下の AI クラウドプラットフォームで、国内外の安定した包括的なモデル API を 1 つのキーだけで利用可能。月額・従量課金のコストパフォーマンスに優れた Coding Plan パッケージを提供し、公式価格の 60〜80% オフで利用できます。Claude Code、Codex および API アクセスに対応。エンタープライズ級の高同時接続、24 時間年中無休のテクニカルサポート、セルフサービス請求書発行に対応。<a href="https://www.compshare.cn/coding-plan?ytag=GPU_YY_YX_git_cc-switch">こちらのリンク</a>から登録すると、無料で 5 元分のプラットフォーム体験クレジットがもらえます!</td>
</tr>
<tr>
<td width="180"><a href="https://www.right.codes/register?aff=CCSWITCH"><img src="assets/partners/logos/rightcode.jpg" alt="RightCode" width="150"></a></td>
<td>本プロジェクトへのご支援として、Right Code にご協賛いただき誠にありがとうございます。Right Code は、Claude Code、Codex、Gemini などのモデルに対応した中継(プロキシ)サービスを安定して提供しています。特に高いコストパフォーマンスを誇る Codex の月額プランを主力としており、<strong>未使用分の利用枠を翌日に繰り越して利用できる(繰越対応)</strong>点が特長です。チャージ(入金)後に請求書の発行が可能で、企業・チーム向けには専任担当による個別対応も行っています。さらに CC Switch ユーザー向けの特別優待として、<a href="https://www.right.codes/register?aff=CCSWITCH">こちらのリンク</a>からご登録いただくと、チャージのたびに実支払額の 25% 相当の従量課金クレジットが付与されます。</td>
</tr>
<tr>
@@ -92,27 +83,23 @@ Claude Code / Codex / Gemini 公式チャンネルが最安で元価格の 38% /
</tr>
<tr>
<td width="180"><a href="https://crazyrouter.com/register?aff=OZcm&ref=cc-switch"><img src="assets/partners/logos/crazyrouter.png" alt="Crazyrouter" width="150"></a></td>
<td>Crazyrouter のご支援に感謝します!Crazyrouter は高性能 AI API アグリゲーションプラットフォームです。1 つの API キーで Claude Code、Codex、Gemini CLI など 300 以上のモデルにアクセス可能。全モデルが公式価格の 55% で利用でき、自動フェイルオーバー、スマートルーティング、無制限同時接続に対応。CC Switch ユーザー向けの限定特典:<a href="https://crazyrouter.com/register?aff=OZcm&ref=cc-switch">こちらのリンク</a>から登録後、カスタマーサポートまでご連絡いただくと <strong>$2 の無料クレジット</strong> を受け取れます。さらに初回チャージ時にプロモコード `CCSWITCH` を入力すると <strong>30% のボーナスクレジット</strong> が追加されます!</td>
</tr>
<tr>
<td width="180"><a href="https://www.right.codes/register?aff=CCSWITCH"><img src="assets/partners/logos/rightcode.jpg" alt="RightCode" width="150"></a></td>
<td>本プロジェクトへのご支援として、Right Code にご協賛いただき誠にありがとうございます。Right Code は、Claude Code、Codex、Gemini などのモデル向け中継サービスを安定して提供しており、従量課金と月額プランの 2 つの料金体系から選択できます。チャージ後に請求書の発行が可能で、法人・チームのお客様には専任担当による個別対応も行っています。さらに、CC Switch ユーザー向けの特別優待として、<a href="https://www.right.codes/register?aff=CCSWITCH">こちらのリンク</a>から登録すると、チャージのたびに実際の支払額の 5% 相当の従量課金クレジットが付与されます。</td>
<td width="180"><a href="https://crazyrouter.com/register?aff=OZcm&ref=cc-switch"><img src="assets/partners/logos/crazyrouter.jpg" alt="Crazyrouter" width="150"></a></td>
<td>Crazyrouter のご支援に感謝します!Crazyrouter は高性能 AI API アグリゲーションプラットフォームです。1 つの API キーで Claude Code、Codex、Gemini CLI など 300 以上のモデルにアクセス可能。全モデルが公式価格の 55% で利用でき、自動フェイルオーバー、スマートルーティング、無制限同時接続に対応。CC Switch ユーザー向けの限定特典:<a href="https://crazyrouter.com/register?aff=OZcm&ref=cc-switch">こちらのリンク</a>から登録すると <strong>$2 の無料クレジット</strong> を即時進呈。さらに初回チャージ時にプロモコード `CCSWITCH` を入力すると <strong>30% のボーナスクレジット</strong> が追加されます!</td>
</tr>
<tr>
<td width="180"><a href="https://www.sssaicode.com/register?ref=DCP0SM"><img src="assets/partners/logos/sssaicode.png" alt="SSSAiCode" width="150"></a></td>
<td>SSSAiCode のご支援に感謝します!SSSAiCode は安定性と信頼性に優れた API 中継サービスで、安定的で信頼性が高く、手頃な価格の Claude・Codex モデルサービスを提供しています。当日の迅速な請求書発行をサポート。CC Switch ユーザー向けの特別特典:<a href="https://www.sssaicode.com/register?ref=DCP0SM">こちらのリンク</a>から登録すると、毎回のチャージで $10 の追加ボーナスを受けられます!</td>
<td>SSSAiCode のご支援に感謝します!SSSAiCode は安定性と信頼性に優れた API 中継サービスで、安定的で信頼性が高く、手頃な価格の Claude・Codex モデルサービスを提供しています。<strong>高コストパフォーマンスの公式 Claude サービスを 0.5¥/$ 換算で提供</strong>、月額制・Paygo など多様な課金方式に対応し、当日の迅速な請求書発行をサポート。CC Switch ユーザー向けの特別特典:<a href="https://www.sssaicode.com/register?ref=DCP0SM">こちらのリンク</a>から登録すると、毎回のチャージで $10 の追加ボーナスを受けられます!</td>
</tr>
<tr>
<td width="180"><a href="https://www.micuapi.ai/register?aff=aOYQ"><img src="assets/partners/logos/mikubanner.svg" alt="Micu" width="150"></a></td>
<td>Micu API のご支援に感謝します!Micu API は、最高のコストパフォーマンスと高い安定性を追求するグローバル大規模言語モデル中継サービスプロバイダーです。法人企業がバックアップしており、サービス停止のリスクを排除、迅速な正規請求書発行に対応!「試行コストゼロ」をモットーに、最低 1 元からチャージ可能で手数料無料、いつでも返金可能!CC Switch ユーザー向けの限定特典:<a href="https://www.micuapi.ai/register?aff=aOYQ">こちらのリンク</a>から登録し、チャージ時にプロモコード「ccswitch」を入力すると <strong>10% 割引</strong> が適用されます!</td>
<td width="180"><a href="https://www.openclaudecode.cn/register?aff=aOYQ"><img src="assets/partners/logos/mikubanner.svg" alt="Micu" width="150"></a></td>
<td>Micu API のご支援に感謝します!Micu API は、最高のコストパフォーマンスと高い安定性を追求するグローバル大規模言語モデル中継サービスプロバイダーです。法人企業がバックアップしており、サービス停止のリスクを排除、迅速な正規請求書発行に対応!「試行コストゼロ」をモットーに、最低 1 元からチャージ可能で手数料無料、いつでも返金可能!CC Switch ユーザー向けの限定特典:<a href="https://www.openclaudecode.cn/register?aff=aOYQ">こちらのリンク</a>から登録し、チャージ時にプロモコード「ccswitch」を入力すると <strong>10% 割引</strong> が適用されます!</td>
</tr>
<tr>
<td width="180"><a href="https://lemondata.cc/r/FFX1ZDUP"><img src="assets/partners/logos/lemondata.png" alt="LemonData" width="150"></a></td>
<td>LemonData のご支援に感謝します!LemonData は高性能 AI API アグリゲーションプラットフォームで、GPT、Claude、Gemini、DeepSeek など 300 以上のモデルに 1 つの API キーでアクセス可能。全モデルが公式価格の 30〜70% オフで自動フェイルオーバー、スマートルーティング、無制限同時接続に対応。新規ユーザーは登録だけで即座に $1 の無料クレジットを獲得 — <a href="https://lemondata.cc/r/FFX1ZDUP">こちらのリンク</a>から登録してボーナスを獲得し、すぐに開発を始めましょう!</td>
<td width="180"><a href="https://x-code.cc/register?aff=IbPp"><img src="assets/partners/logos/xcodeapi.png" alt="XCodeAPI" width="150"></a></td>
<td>XCodeAPI のご支援に感謝します!CC Switch ユーザー向けの特別特典:<a href="https://x-code.cc/register?aff=IbPp">こちらのリンク</a>から登録すると、初回注文で 10% の追加クレジットボーナスがもらえます!(サイト管理者に連絡して受け取りください)</td>
</tr>
<tr>
@@ -120,24 +107,21 @@ Claude Code / Codex / Gemini 公式チャンネルが最安で元価格の 38% /
<td>CTok.ai のご支援に感謝します!CTok.ai はワンストップ AI プログラミングツールサービスプラットフォームの構築に取り組んでいます。Claude Code のプロフェッショナルプランと技術コミュニティサービスを提供し、Google Gemini や OpenAI Codex にも対応しています。丁寧に設計されたプランと専門的な技術コミュニティを通じて、開発者に安定したサービス保証と継続的な技術サポートを提供し、AI アシストプログラミングを真の生産性ツールにします。<a href="https://ctok.ai">こちら</a>から登録してください!</td>
</tr>
<tr>
<td width="180"><a href="https://chefshop.ai"><img src="assets/partners/logos/chefshop.png" alt="ChefShop" width="150"></a></td>
<td>ChefShop AI のご支援に感謝します!ChefShop AI は、AI ヘビーユーザー向けにカスタマイズされたプレミアムアカウントサービスプロバイダーです。ChatGPT Plus/Pro、Claude Max、Grok Super/Heavy、Gemini など主流の大規模モデルの公式チャージと安定したアカウントサービスを提供しています。<a href="https://chefshop.ai">こちら</a>から購入してください!</td>
</tr>
<tr>
<td width="180"><a href="https://vibecodingapi.ai"><img src="assets/partners/logos/lioncc.png" alt="LionCC" width="150"></a></td>
<td>LionCC のご支援に感謝します!LionCC は究極の開発体験を追求する「Vibe Coders」のために生まれました。Claude Code、Codex、OpenClaw 向けに安定・低遅延・お得な価格の計算リソースサービスを提供し、最大 50% のコスト削減を実現します。登録後、カスタマーサービスの WeChatHSQBJ088888888)を追加し、合言葉「cc-switch」を送信すると、10 ドル分のクレジット(1,000 万トークン)がもらえます。その他のコラボレーションについてはブログ @LionCC.ai をフォローしてください。<a href="https://vibecodingapi.ai">こちら</a>から登録してください!</td>
</tr>
<tr>
<td width="180"><a href="https://console.claudeapi.com/register?aff=pCLD"><img src="assets/partners/logos/claudeapi.png" alt="ClaudeAPI" width="150"></a></td>
<td>本プロジェクトは <a href="https://console.claudeapi.com/register?aff=pCLD">Claude API</a> がスポンサーです。Claude API 直結 — わずか 3 分で Claude Code や Agent アプリに接続可能。新規ユーザーにはテストクレジットを提供しています。Anthropic 公式キーおよび AWS Bedrock 公式チャネルに基づいており、リバースエンジニアリングや性能劣化はありません。Opus / Sonnet / Haiku の全モデルラインナップをサポートし、Tool Use や 1M コンテキストなどの公式機能をすべて保持しています。Claude Code ヘビーユーザー、Agent エンジニア、企業技術チームに最適です。請求書発行およびチーム対応が可能です。<a href="https://console.claudeapi.com/register?aff=pCLD">こちら</a>から登録してください!</td>
</tr>
<tr>
<td width="180"><a href="https://claudecn.top"><img src="assets/partners/logos/claudecn.jpg" alt="ClaudeCN" width="150"></a></td>
<td>本プロジェクトのスポンサーである ClaudeCN に感謝いたします!ClaudeCN は、実体のある企業によって運営されるエンタープライズ向け AI ゲートウェイプラットフォームです。Claude、GPT、DeepSeek など主要モデルへの高可用な商用 API アクセスを提供し、企業の調達プロセスにも対応 — 法人振込や正式契約に対応し、コンプライアンス面でも安心してご利用いただけます。<a href="https://claudecn.top">こちら</a>からご登録ください!</td>
</tr>
<tr>
<td width="180"><a href="https://runapi.co"><img src="assets/partners/logos/runapi.jpg" alt="RunAPI" width="150"></a></td>
<td>本プロジェクトのスポンサーである RunAPI に感謝いたします!RunAPI は高効率で安定した AI モデル API ゲートウェイです。一つの API Key で、OpenAI、Claude、Gemini、DeepSeek、Grok など 150 種類以上の主要モデルにアクセス可能。料金は公式価格の最大 10%、安定性にも優れ、Claude Code や OpenClaw などのツールとシームレスに連携できます。CC Switch ユーザー限定特典:ご登録後にカスタマーサポートへご連絡いただくと、14 元の無料クレジットを進呈いたします。<a href="https://runapi.co">こちら</a>からご登録ください!</td>
<td width="180"><a href="https://ddshub.short.gy/ccswitch"><img src="assets/partners/logos/dds.png" alt="DDS" width="150"></a></td>
<td>本プロジェクトのスポンサーである DDS に感謝いたします! DDS(呆呆獣 / DDS Hub)は、Claude に特化した信頼性とパフォーマンスの高い API プロキシサービスです。個人および企業ユーザーの皆様に、圧倒的なコストパフォーマンスを誇る Claude 直結アクセラレーションサービスを提供しています。Claude Haiku / Opus / Sonnet などのフルスペックモデルを完全サポートし、安定した低遅延のアクセスを実現します。
1,000人民元以上のチャージで領収書(発票)の発行が可能です。さらに、企業のお客様にはカスタマイズされたグループ管理や専用テクニカルサポートをご提供しています。
CC Switch ユーザー限定特典: 専用リンクからご<a href="https://ddshub.short.gy/ccswitch">登録</a>いただくと、初回チャージ時に 10% の追加ボーナスクレジット をプレゼントいたします!(※チャージ完了後、グループ管理人へご連絡の上お受け取りください。)</td>
</tr>
</table>
@@ -166,7 +150,7 @@ Claude Code / Codex / Gemini 公式チャンネルが最安で元価格の 38% /
## 特長
[完全な更新履歴](CHANGELOG.md) | [リリースノート](docs/release-notes/v3.15.0-ja.md)
[完全な更新履歴](CHANGELOG.md) | [リリースノート](docs/release-notes/v3.12.3-ja.md)
### プロバイダ管理
+31 -49
View File
@@ -2,7 +2,7 @@
# CC Switch
### Claude Code、Codex、Gemini CLI、OpenCodeOpenClaw 和 Hermes Agent 的全方位管理工具
### Claude Code、Codex、Gemini CLI、OpenCodeOpenClaw 的全方位管理工具
[![Version](https://img.shields.io/github/v/release/farion1231/cc-switch?color=blue&label=version)](https://github.com/farion1231/cc-switch/releases)
[![Platform](https://img.shields.io/badge/platform-Windows%20%7C%20macOS%20%7C%20Linux-lightgrey.svg)](https://github.com/farion1231/cc-switch/releases)
@@ -11,8 +11,6 @@
<a href="https://trendshift.io/repositories/15372" target="_blank"><img src="https://trendshift.io/api/badge/repositories/15372" alt="farion1231%2Fcc-switch | Trendshift" style="width: 250px; height: 55px;" width="250" height="55"/></a>
### 🌐 唯一官方网站:**[ccswitch.io](https://ccswitch.io)**
[English](README.md) | 中文 | [日本語](README_JA.md) | [更新日志](CHANGELOG.md)
</div>
@@ -39,38 +37,26 @@ MiniMax M2.7 是 MiniMax 首个深度参与自我迭代的模型,可自主构
</tr>
<tr>
<td width="180"><a href="https://aigocode.com/invite/CC-SWITCH"><img src="assets/partners/logos/aigocode.png" alt="AIGoCode" width="150"></a></td>
<td>感谢 AIGoCode 赞助了本项目!AIGoCode 是一个集成了 Claude Code、Codex 以及 Gemini 最新模型的一站式平台,为你提供稳定、高效且高性价比的AI编程服务。本站提供灵活的订阅计划,零封号风险,国内直连,无需魔法,极速响应。AIGoCode 为 CC Switch 的用户提供了特别福利,通过<a href="https://aigocode.com/invite/CC-SWITCH">此链接</a>注册的用户首次充值可以获得额外10%奖励额度!</td>
<td width="180"><a href="https://cloud.siliconflow.cn/i/drGuwc9k"><img src="assets/partners/logos/silicon_zh.jpg" alt="SiliconFlow" width="150"></a></td>
<td>感谢硅基流动赞助了本项目!硅基流动是一个高性能 AI 基础设施与模型 API 平台,一站式提供语言、语音、图像、视频等多模态模型的快速、可靠访问。平台支持按量计费、丰富的多模态模型选择、高速推理和企业级稳定性,帮助开发者和团队更高效地构建和扩展 AI 应用。通过<a href="https://cloud.siliconflow.cn/i/drGuwc9k">此链接</a>注册并完成实名认证,即可获得 ¥20 奖励金,可在平台内跨模型使用。硅基流动现已兼容 OpenClaw,用户可接入硅基流动 API Key 免费调用主流 AI 模型。</td>
</tr>
<tr>
<td width="180"><a href="https://www.shengsuanyun.com/?from=CH_4HHXMRYF"><img src="assets/partners/logos/shengsuanyun.png" alt="Shengsuanyun" width="150"></a></td>
<td width="180"><a href="https://www.shengsuanyun.com/?from=CH_4HHXMRYF"><img src="assets/partners/logos/shengsuanyun.svg" alt="Shengsuanyun" width="150"></a></td>
<td>感谢胜算云赞助了本项目!胜算云是专为AI Native Teams服务的超级工厂,工业级AI任务并行执行平台,模型商城集采直供聚合接入了Claude、Chatgpt、Gemini等海内外LLM及图片视频多媒体模型算力,绝无逆向掺水、全站模型SLA可用性高达99.7%、<a href="https://watch.shengsuanyun.com/status/shengsuanyun">监测接口</a>日常全绿。更有企业级专属定制网关,实现团队精细化成本与权限管控,智能路由+安全防护+BYOK企业自带密钥托管。平台按量及tokens plan(即将上线)计费,可开票,使用<a href="https://www.shengsuanyun.com/?from=CH_4HHXMRYF">此链接</a>注册新用户可获10元模力及首充10%赠送。</td>
</tr>
<tr>
<td width="180"><a href="https://aigocode.com/invite/CC-SWITCH"><img src="assets/partners/logos/aigocode.png" alt="AIGoCode" width="150"></a></td>
<td>感谢 AIGoCode 赞助了本项目!AIGoCode 是一个集成了 Claude Code、Codex 以及 Gemini 最新模型的一站式平台,为你提供稳定、高效且高性价比的AI编程服务。本站提供灵活的订阅计划,零封号风险,国内直连,无需魔法,极速响应。AIGoCode 为 CC Switch 的用户提供了特别福利,通过<a href="https://aigocode.com/invite/CC-SWITCH">此链接</a>注册的用户首次充值可以获得额外10%奖励额度!</td>
</tr>
<tr>
<td width="180"><a href="https://www.aicodemirror.com/register?invitecode=9915W3"><img src="assets/partners/logos/aicodemirror.jpg" alt="AICodeMirror" width="150"></a></td>
<td>感谢 AICodeMirror 赞助了本项目!AICodeMirror 提供 Claude Code / Codex / Gemini CLI 官方高稳定中转服务,支持企业级高并发、极速开票、7×24 专属技术支持。
Claude Code / Codex / Gemini 官方渠道低至 3.8 / 0.2 / 0.9 折,充值更有折上折!AICodeMirror 为 CCSwitch 的用户提供了特别福利,通过<a href="https://www.aicodemirror.com/register?invitecode=9915W3">此链接</a>注册的用户,可享受首充8折,企业客户最高可享 7.5 折!</td>
</tr>
<tr>
<td width="180"><a href="https://pateway.ai/?ch=etzpm8&aff=WB6M6F67#/"><img src="assets/partners/logos/pateway.png" alt="PatewayAI" width="150"></a></td>
<td>感谢 PatewayAI 赞助了本项目!PatewayAI 是一家面向重度 AI 开发者、专注官方直连高品质模型 API 中转服务商。提供 Claude 全系列与 Codex 系列模型,100% 官方源直供,不掺假不注水,欢迎检验。计费透明,Token 级账单可逐笔核验。
同时支持企业级高并发,并为企业客户提供了专业的管理平台,企业客户可签订正式合同并开具发票,更多详情进入官网获取联系方式。
现在通过<a href="https://pateway.ai/?ch=etzpm8&aff=WB6M6F67#/">此链接</a>注册即送 $3 试用额度,用户充值低至 6 折,邀请好友双向赠送,邀请奖励可达 $150!</td>
</tr>
<tr>
<td width="180"><a href="https://www.volcengine.com/activity/agentplan?utm_campaign=hw&utm_content=ccswitch&utm_medium=devrel_tool_web&utm_source=OWO&utm_term=ccswitch"><img src="assets/partners/logos/huoshan.png" alt="HuoShan" width="150"></a></td>
<td>感谢火山方舟Agent Plan 模型赞助了本项目!方舟Agent Plan 模型订阅套餐集成了包含Doubao-Seed、Doubao-Seedance、Doubao-Seedream等在内的字节跳动自研SOTA级模型,覆盖文本、代码、图像、视频等多模态任务。同时支持一站式接入DeepSeek V4、GLM 5.1等主流大模型。超全模态模型与 Harness 升级一步到位,深度支持 Agent 框架与 AI 编程工具。方舟 Agent Plan 为 CC Switch 的用户提供了专属福利:通过<a href="https://www.volcengine.com/activity/agentplan?utm_campaign=hw&utm_content=ccswitch&utm_medium=devrel_tool_web&utm_source=OWO&utm_term=ccswitch">此链接</a>订阅方舟AgentPlan,新客户首月40元起!<a href="https://www.byteplus.com/en/product/modelark?utm_campaign=hw&utm_content=ccswitch&utm_medium=devrel_tool_web&utm_source=OWO&utm_term=ccswitch">>>For developers outside Mainland China, please click here</a></td>
</tr>
<tr>
<td width="180"><a href="https://cloud.siliconflow.cn/i/drGuwc9k"><img src="assets/partners/logos/silicon_zh.jpg" alt="SiliconFlow" width="150"></a></td>
<td>感谢硅基流动赞助了本项目!硅基流动是一个高性能 AI 基础设施与模型 API 平台,一站式提供语言、语音、图像、视频等多模态模型的快速、可靠访问。平台支持按量计费、丰富的多模态模型选择、高速推理和企业级稳定性,帮助开发者和团队更高效地构建和扩展 AI 应用。通过<a href="https://cloud.siliconflow.cn/i/drGuwc9k">此链接</a>注册并完成实名认证,即可获得 ¥16 奖励金,可在平台内跨模型使用。硅基流动现已兼容 OpenClaw,用户可接入硅基流动 API Key 免费调用主流 AI 模型。</td>
</tr>
<tr>
<td width="180"><a href="https://cubence.com/signup?code=CCSWITCH&source=ccs"><img src="assets/partners/logos/cubence.png" alt="Cubence" width="150"></a></td>
<td>感谢 Cubence 赞助本项目!Cubence 是一家可靠高效的 API 中继服务提供商,提供对 Claude Code、Codex、Gemini 等模型的中继服务,并提供按量、包月等灵活的计费方式。Cubence 为 CC Switch 的用户提供了特别优惠:使用 <a href="https://cubence.com/signup?code=CCSWITCH&source=ccs">此链接</a> 注册,并在充值时输入 "CCSWITCH" 优惠码,每次充值均可享受九折优惠!</td>
@@ -84,7 +70,12 @@ Claude Code / Codex / Gemini 官方渠道低至 3.8 / 0.2 / 0.9 折,充值更
<tr>
<td width="180"><a href="https://www.compshare.cn/coding-plan?ytag=GPU_YY_YX_git_cc-switch"><img src="assets/partners/logos/ucloud.png" alt="优云智算" width="150"></a></td>
<td>感谢优云智算赞助了本项目!优云智算是UCloud旗下AI云平台,提供稳定、全面的国内外模型API,仅一个key即可调用。主打包月、按的高性价比 国模Coding Plan套餐,同时提供官转稳定海外模型。支持接入 Claude Code、Codex 及 API 调用。支持企业高并发、7*24技术支持、自助开票。通过<a href="https://www.compshare.cn/coding-plan?ytag=GPU_YY_YX_git_cc-switch">此链接</a>注册的用户,可得免费5元平台体验金!</td>
<td>感谢优云智算赞助了本项目!优云智算是UCloud旗下AI云平台,提供稳定、全面的国内外模型API,仅一个key即可调用。主打包月、按的高性价比 Coding Plan 套餐,基于官方2~5折优惠。支持接入 Claude Code、Codex 及 API 调用。支持企业高并发、7*24技术支持、自助开票。通过<a href="https://www.compshare.cn/coding-plan?ytag=GPU_YY_YX_git_cc-switch">此链接</a>注册的用户,可得免费5元平台体验金!</td>
</tr>
<tr>
<td width="180"><a href="https://www.right.codes/register?aff=CCSWITCH"><img src="assets/partners/logos/rightcode.jpg" alt="RightCode" width="150"></a></td>
<td>感谢 Right Code 赞助了本项目!Right Code 稳定提供 Claude Code、Codex、Gemini 等模型的中转服务。主打<strong>极高性价比</strong>的Codex包月套餐,<strong>提供额度转结,套餐当天用不完的额度,第二天还能接着用!</strong>充值即可开票,企业、团队用户一对一对接。同时为 CC Switch 的用户提供了特别优惠:通过<a href="https://www.right.codes/register?aff=CCSWITCH">此链接</a>注册,每次充值均可获得实付金额25%的按量额度!</td>
</tr>
<tr>
@@ -93,27 +84,23 @@ Claude Code / Codex / Gemini 官方渠道低至 3.8 / 0.2 / 0.9 折,充值更
</tr>
<tr>
<td width="180"><a href="https://crazyrouter.com/register?aff=OZcm&ref=cc-switch"><img src="assets/partners/logos/crazyrouter.png" alt="Crazyrouter" width="150"></a></td>
<td>感谢 Crazyrouter 赞助了本项目!Crazyrouter 是一个高性能 AI API 聚合平台——一个 API Key 即可访问 300+ 模型,包括 Claude Code、Codex、Gemini CLI 等。全部模型低至官方定价的 55%,支持自动故障转移、智能路由和无限并发。Crazyrouter 为 CC Switch 用户提供了专属优惠:通过<a href="https://crazyrouter.com/register?aff=OZcm&ref=cc-switch">此链接</a>注册后联系客服即可领取 <strong>$2 免费额度</strong>,首次充值时输入优惠码 `CCSWITCH` 还可获得额外 <strong>30% 奖励额度</strong></td>
</tr>
<tr>
<td width="180"><a href="https://www.right.codes/register?aff=CCSWITCH"><img src="assets/partners/logos/rightcode.jpg" alt="RightCode" width="150"></a></td>
<td>感谢 Right Code 赞助了本项目!Right Code 稳定提供 Claude Code、Codex、Gemini 等模型的中转服务,并可选按量、包月两种计费模式。充值即可开票,企业、团队用户一对一对接。同时为 CC Switch 的用户提供了特别优惠:通过<a href="https://www.right.codes/register?aff=CCSWITCH">此链接</a>注册,每次充值均可获得实付金额5%的按量额度!</td>
<td width="180"><a href="https://crazyrouter.com/register?aff=OZcm&ref=cc-switch"><img src="assets/partners/logos/crazyrouter.jpg" alt="Crazyrouter" width="150"></a></td>
<td>感谢 Crazyrouter 赞助了本项目!Crazyrouter 是一个高性能 AI API 聚合平台——一个 API Key 即可访问 300+ 模型,包括 Claude Code、Codex、Gemini CLI 等。全部模型低至官方定价的 55%,支持自动故障转移、智能路由和无限并发。Crazyrouter 为 CC Switch 用户提供了专属优惠:通过<a href="https://crazyrouter.com/register?aff=OZcm&ref=cc-switch">此链接</a>注册即可获得 <strong>$2 免费额度</strong>,首次充值时输入优惠码 `CCSWITCH` 还可获得额外 <strong>30% 奖励额度</strong></td>
</tr>
<tr>
<td width="180"><a href="https://www.sssaicode.com/register?ref=DCP0SM"><img src="assets/partners/logos/sssaicode.png" alt="SSSAiCode" width="150"></a></td>
<td>感谢 SSSAiCode 赞助了本项目!SSSAiCode 是一家稳定可靠的API中转站,致力于提供稳定、可靠、平价的Claude、CodeX模型服务,支持当日快速开票,SSSAiCode为本软件的用户提供特别优惠,使用<a href="https://www.sssaicode.com/register?ref=DCP0SM">此链接</a>注册每次充值均可享受10$的额外奖励!</td>
<td>感谢 SSSAiCode 赞助了本项目!SSSAiCode 是一家稳定可靠的API中转站,致力于提供稳定、可靠、平价的Claude、CodeX模型服务,<strong>提供高性价比折合0.5¥/$的官方Claude服务</strong>,支持包月、Paygo多种计费方式、支持当日快速开票,SSSAiCode为本软件的用户提供特别优惠,使用<a href="https://www.sssaicode.com/register?ref=DCP0SM">此链接</a>注册每次充值均可享受10$的额外奖励!</td>
</tr>
<tr>
<td width="180"><a href="https://www.micuapi.ai/register?aff=aOYQ"><img src="assets/partners/logos/mikubanner.svg" alt="Micu" width="150"></a></td>
<td>感谢 米醋API 赞助了本项目!米醋API 是一家致力于提供极致性价比与高稳定性的全球大模型中转服务商。米醋API 背后有实体企业做核心保障,杜绝跑路风险,支持极速正规开票!我们主打“试错零成本”:1 元起充低门槛,0 手续费随时退款!米醋API 为本软件的用户提供了特别优惠,使用<a href="https://www.micuapi.ai/register?aff=aOYQ">此链接</a>注册并在充值时填写"ccswitch"优惠码可享九折优惠!</td>
<td width="180"><a href="https://www.openclaudecode.cn/register?aff=aOYQ"><img src="assets/partners/logos/mikubanner.svg" alt="Micu" width="150"></a></td>
<td>感谢 米醋API 赞助了本项目!米醋API 是一家致力于提供极致性价比与高稳定性的全球大模型中转服务商。米醋API 背后有实体企业做核心保障,杜绝跑路风险,支持极速正规开票!我们主打“试错零成本”:1 元起充低门槛,0 手续费随时退款!米醋API 为本软件的用户提供了特别优惠,使用<a href="https://www.openclaudecode.cn/register?aff=aOYQ">此链接</a>注册并在充值时填写"ccswitch"优惠码可享九折优惠!</td>
</tr>
<tr>
<td width="180"><a href="https://lemondata.cc/r/FFX1ZDUP"><img src="assets/partners/logos/lemondata.png" alt="LemonData" width="150"></a></td>
<td>感谢 LemonData 赞助了本项目!LemonData 是一个高性能 AI API 聚合平台——一个 API Key 即可访问 GPT、Claude、Gemini、DeepSeek 等 300+ 模型。所有模型定价为官方价格的 30%-70%,支持自动故障转移、智能路由和无限并发。新用户注册即获 $1 免费额度——通过<a href="https://lemondata.cc/r/FFX1ZDUP">此链接</a>注册即可领取奖励,立即开始开发!</td>
<td width="180"><a href="https://x-code.cc/register?aff=IbPp"><img src="assets/partners/logos/xcodeapi.png" alt="XCodeAPI" width="150"></a></td>
<td>感谢 XCodeAPI 赞助了本项目!XCodeAPI 为本软件的用户提供特别福利,使用<a href="https://x-code.cc/register?aff=IbPp">此链接</a>注册后首单加赠10%的额度!(联系站长领取)</td>
</tr>
<tr>
@@ -121,24 +108,19 @@ Claude Code / Codex / Gemini 官方渠道低至 3.8 / 0.2 / 0.9 折,充值更
<td>感谢 CTok.ai 赞助了本项目!CTok.ai 致力于打造一站式 AI 编程工具服务平台。我们提供 Claude Code 专业套餐及技术社群服务,同时支持 Google Gemini 和 OpenAI Codex。通过精心设计的套餐方案和专业的技术社群,为开发者提供稳定的服务保障和持续的技术支持,让 AI 辅助编程真正成为开发者的生产力工具。点击<a href="https://ctok.ai">这里</a>注册!</td>
</tr>
<tr>
<td width="180"><a href="https://chefshop.ai"><img src="assets/partners/logos/chefshop.png" alt="ChefShop" width="150"></a></td>
<td>感谢 厨师长AI小铺 赞助了本项目!厨师长AI小铺 是一家专为 AI 重度订阅用户量身定制的优质账号服务商。平台提供涵盖 ChatGPT Plus/Pro、Claude Max、Grok Super/Heavy 以及 Gemini 等主流大模型的官方代充与稳定成品账号服务。点击<a href="https://chefshop.ai">这里</a>购买!</td>
</tr>
<tr>
<td width="180"><a href="https://vibecodingapi.ai"><img src="assets/partners/logos/lioncc.png" alt="LionCC" width="150"></a></td>
<td>感谢 LionCC 狮子API 赞助了本项目!LionCC 专为追求极致开发体验的”Vibe Coders”而生。我们提供稳定、低延迟、优惠价格的 Claude Code、Codex 及 OpenClaw 算力服务,可节约 50% 成本。注册后添加客服微信 HSQBJ088888888,发暗号 cc-switch 备注即可送 10 美金额度(1000 万 token 算力)。其他项目合作关注博客 @LionCC.ai,点击<a href=”https://vibecodingapi.ai”>这里</a>注册!</td>
</tr>
<tr>
<td width="180"><a href="https://console.claudeapi.com/register?aff=pCLD"><img src="assets/partners/logos/claudeapi.png" alt="ClaudeAPI" width="150"></a></td>
<td>本项目由 <a href="https://console.claudeapi.com/register?aff=pCLD">Claude API</a> 赞助。Claude API 直连,三分钟接入 Claude Code 与 Agent 应用 新用户可领取测试额度。基于 Anthropic 官方 Key + AWS Bedrock 官方渠道,非逆向、非降智,支持 Opus / Sonnet / Haiku 全系列模型,保留 Tool Use、1M 上下文等官方能力。适合 Claude Code 深度用户、Agent 工程师与企业技术团队,支持开票和团队对接。点击<a href="https://console.claudeapi.com/register?aff=pCLD">这里</a>注册</td>
</tr>
<tr>
<td width="180"><a href="https://claudecn.top"><img src="assets/partners/logos/claudecn.jpg" alt="ClaudeCN" width="150"></a></td>
<td>感谢 ClaudeCN 赞助本项目!ClaudeCN 由是一家实体企业运营的企业级AI中转平台。平台可提供高可用性的商用API服务,提供Claude、GPT、Deepseek等热门模型,支持企业采购流程,可对公打款、签约,服务合规有保障。点击<a href="https://claudecn.top">此链接</a>注册!</td>
</tr>
<tr>
<td width="180"><a href="https://runapi.co"><img src="assets/partners/logos/runapi.jpg" alt="RunAPI" width="150"></a></td>
<td>感谢 RunAPI 赞助本项目!RunAPI 是高效稳定的 AI 模型 API 中转平台,一个 API Key 即可访问 OpenAI、Claude、Gemini、DeepSeek、Grok 等 150+ 主流模型,低至 1 折,极其稳定,可以无缝兼容 Claude Code、OpenClaw 等工具。RunAPI为CC switch的用户提供了特别福利,注册后联系客服可以领取14元额度,点击<a href="https://runapi.co">此链接</a>注册!</td>
<td width="180"><a href="https://ddshub.short.gy/ccswitch"><img src="assets/partners/logos/dds.png" alt="DDS" width="150"></a></td>
<td>感谢 DDS 赞助本项目!呆呆兽是一家专注 Claude 的可靠高效 API 中转站,为个人和企业用户提供极具性价比的国内 Claude 直连加速服务。支持 Claude Haiku / Opus / Sonnet 等满血模型。充值满 1000 元即可开具发票,企业客户更可享受定制化分组和技术支持服务。CC Switch 用户专属福利:通过<a href="https://ddshub.short.gy/ccswitch">此链接</a>注册后,首单充值可额外赠送 10% 额度(充值后请联系群主领取)</td>
</tr>
</table>
@@ -167,7 +149,7 @@ Claude Code / Codex / Gemini 官方渠道低至 3.8 / 0.2 / 0.9 折,充值更
## 功能特性
[完整更新日志](CHANGELOG.md) | [发布说明](docs/release-notes/v3.15.0-zh.md)
[完整更新日志](CHANGELOG.md) | [发布说明](docs/release-notes/v3.12.3-zh.md)
### 供应商管理
Binary file not shown.

Before

Width:  |  Height:  |  Size: 62 KiB

After

Width:  |  Height:  |  Size: 68 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 270 KiB

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 84 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 415 KiB

After

Width:  |  Height:  |  Size: 280 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 101 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 54 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 42 KiB

After

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 138 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 511 KiB

After

Width:  |  Height:  |  Size: 246 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 99 KiB

After

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 193 KiB

After

Width:  |  Height:  |  Size: 99 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 60 KiB

After

Width:  |  Height:  |  Size: 41 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 105 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 129 KiB

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 162 KiB

After

Width:  |  Height:  |  Size: 8.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 92 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 206 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 170 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 43 KiB

After

Width:  |  Height:  |  Size: 76 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 54 KiB

After

Width:  |  Height:  |  Size: 70 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 400 KiB

After

Width:  |  Height:  |  Size: 447 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 170 KiB

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.4 KiB

-469
View File
@@ -1,469 +0,0 @@
# CC Switch v3.14.0
> Hermes Agent becomes the 6th managed app, Claude Opus 4.7 rolls out across the preset matrix, Gemini Native API proxy, "Local Routing" rename, and application-level window controls
**[中文版 →](v3.14.0-zh.md) | [日本語版 →](v3.14.0-ja.md)**
---
## Overview
CC Switch v3.14.0 is a major release centered on onboarding **Hermes Agent as the 6th first-class managed app** and rolling out **Claude Opus 4.7** across the full aggregator and Bedrock preset matrix. Hermes support covers a database v9 → v10 migration, a complete Rust command surface, YAML-backed `~/.hermes/config.yaml` read/write with atomic backups, MCP sync, Skills sync, SQLite + JSONL session management, and dedicated frontend panels including a Memory editor. All four API protocols aligned with Hermes Agent 0.10.0 (`chat_completions`, `anthropic_messages`, `codex_responses`, `bedrock_converse`) are selectable. Providers owned by the user-authored `providers:` dict are rendered as read-only cards, and deep YAML configuration is delegated directly to the Hermes Web UI.
Beyond Hermes, this release adds a **Gemini Native API proxy** (`api_format = "gemini_native"`) so the proxy can forward directly to Google's `generateContent` endpoint with full streaming, schema conversion, and shadow request support; renames the legacy "Local Proxy Takeover" to **Local Routing** across UI copy, README, and docs in all three locales; introduces **application-level window controls**, an opt-in setting that materially improves the experience on Linux Wayland where compositor-drawn buttons can become inert; and bundles late additions for launching `hermes dashboard` from the toolbar, a LemonData preset across all six apps, a DDSHub Codex endpoint, plus several Hermes health-check and Usage modal fixes.
On the session side, the message list is **virtualized** via `@tanstack/react-virtual` so conversations with thousands of records scroll smoothly and long messages collapse by default; the Usage dashboard adds a **date range picker** (Today / 1d / 7d / 14d / 30d + custom date-time calendar) and a page-jump input; **Stream Check error classification** now surfaces color-coded toasts with refreshed default probe models and an explicit "model not found" branch; and switching to official providers is **blocked while Local Routing is active** to avoid account-suspension risk. The pricing database is reseeded from v8 → v9 with ~50 new model entries (Claude 4.7, Opus 4.7 Adaptive Thinking, Grok 4, Qwen 3.5/3.6, MiniMax M2.5/M2.7, Doubao Seed 2.0 series, GLM-5/5.1 and others) and corrected stale prices.
**Release Date**: 2026-04-21
**Update Scale**: 100 commits | 219 files changed | +20,548 / -3,569 lines
---
## Highlights
- **Hermes Agent Support (6th Managed App)**: Database v9 → v10 migration, full Rust command surface, YAML read/write with atomic backups, MCP sync, Skills sync, SQLite + JSONL session management, dedicated frontend panels, and four API protocols (`chat_completions` / `anthropic_messages` / `codex_responses` / `bedrock_converse`)
- **Claude Opus 4.7 Rollout**: Adaptive thinking whitelisting, per-million pricing seed, Bedrock SKU (`anthropic.claude-opus-4-7` / `global.anthropic.claude-opus-4-7`, dropping the legacy `-v1` suffix); all aggregator and Bedrock presets migrated to Opus 4.7 as the default Opus model
- **Claude `max` Effort Tier**: Effort dropdown upgraded from `high` to `max`
- **Gemini Native API Proxy**: New `api_format = "gemini_native"` forwards directly to Google's `generateContent` with full streaming / schema conversion / shadow request support
- **GitHub Copilot Enterprise Server**: GHES authentication and endpoint configuration for Copilot-backed Claude providers
- **Copilot Premium Consumption Deep Optimization**: Proactive thinking-block stripping before forwarding, `tool_result` classification fix, subagent detection, `x-interaction-id` billing merge, orphan `tool_result` sanitization, and default warmup downgrade — a systematic reduction in premium interaction consumption
- **Session List Virtualization**: Long conversations scroll smoothly and long messages collapse by default to reduce text layout cost
- **Codex / OpenClaw Session Title Extraction**: Meaningful title extraction with 2-line display; strips OpenClaw `message_id` suffix noise
- **Usage Date Range Picker**: Today / 1d / 7d / 14d / 30d preset tabs + custom date-time calendar; page-jump input on paginated lists
- **Stream Check Error Classification**: Color-coded error toasts; refreshed default probe models; explicit "model not found" detection
- **Block Official Provider Switching During Local Routing**: Routing official API traffic through the local proxy carries account-suspension risk — switches are blocked with a warning toast
- **Pricing Database Refresh (v8 → v9)**: ~50 new model entries and corrected stale prices
- **Application-Level Window Controls**: Opt-in setting to render CC Switch's own min/max/close buttons, materially improving Linux Wayland experience
- **Hermes in Unified Skills Management**: Skill install, enable, and filter now cover Hermes
- **Hermes / OpenClaw Config Directory Override**: Point CC Switch at a custom `~/.hermes/config.yaml` or `openclaw.json` location
- **Launch Hermes Dashboard from Toolbar**: When the Hermes Web UI probe fails, the toolbar entry offers to run `hermes dashboard` in the user's preferred terminal
- **New Partner Presets**: LemonData across all six apps; DDSHub Codex endpoint; StepFun Step Plan
---
## Added
### Hermes Agent Support (6th Managed App)
CC Switch now treats Hermes Agent as a first-class managed app alongside Claude / Codex / Gemini / OpenCode / OpenClaw.
- **Database Migration v9 → v10**: Adds `enabled_hermes` columns to `mcp_servers` and `skills` tables (`DEFAULT 0`, auto-migrated, no data loss)
- **YAML Configuration Read/Write**: `~/.hermes/config.yaml` read/write with atomic backups; `tests/hermes_roundtrip.rs` guards against dropped OAuth MCP `auth` blocks or pollution of unrelated YAML keys
- **Four API Protocols**: Aligned with Hermes Agent 0.10.0 — `chat_completions` / `anthropic_messages` / `codex_responses` / `bedrock_converse`; new deeplinks default to `chat_completions`
- **User `providers:` Dict Read-Only Rendering**: User-authored providers in the YAML appear as read-only cards in CC Switch; deep configuration delegates to the Hermes Web UI
- **Additive Switching**: Unlike Claude / Codex's "override" style, all Hermes providers coexist in the same YAML
### Hermes Memory Panel
- New Memory panel for editing `MEMORY.md` / `USER.md` directly, with an enable switch, character-count limits, and a live save flow
- Replaces the Prompts entry for Hermes
### Hermes Provider Presets (~50)
- Covers Nous Research, Shengsuanyun, OpenRouter, DeepSeek, Together AI, StepFun, Zhipu GLM, Bailian, Kimi, MiniMax, DouBao, BaiLing, ModelScope, KAT-Coder, PackyCode, Cubence, AIGoCode, RightCode, AICodeMirror, AICoding, CrazyRouter, SSSAiCode, Micu, CTok.ai, DDSHub, E-FlowCode, LionCCAPI, PIPELLM, Compshare, SiliconFlow, AiHubMix, DMXAPI, TheRouter, Novita, Nvidia, and Xiaomi MiMo
### Launch Hermes Dashboard from Toolbar
- When the Hermes Web UI probe fails, the toolbar entry opens a confirm dialog offering to run `hermes dashboard` in the user's preferred terminal
- Spawned via a temp bash / batch script; `hermes dashboard` opens the browser itself once ready, so no polling is required
- 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`)
- Linux terminal detection reordered to try `which` before stat'ing `/usr/bin`, `/bin`, `/usr/local/bin`
### Claude Opus 4.7 Support
- New Claude Opus 4.7 with adaptive thinking whitelisting, per-million pricing seed, and Bedrock SKU (`anthropic.claude-opus-4-7` / `global.anthropic.claude-opus-4-7`, dropping the legacy `-v1` suffix)
- All aggregator and Bedrock presets migrated to Opus 4.7 as the default Opus model
### Claude `max` Effort Tier
- Claude effort dropdown upgraded from `high` to `max` for extended reasoning capacity
### Gemini Native API Proxy
- New `api_format = "gemini_native"` so the proxy can forward directly to Google's `generateContent` API (#1918, thanks @yovinchen)
- Full streaming, schema conversion, and shadow request support
- Adds `gemini_url.rs`, `gemini_schema.rs`, `gemini_shadow.rs`, `streaming_gemini.rs`, and `transform_gemini.rs` under the proxy providers module
### GitHub Copilot Enterprise Server (GHES)
- GHES authentication and endpoint configuration for Copilot-backed Claude providers (#2175, thanks @hotelbe)
### Session List Virtualization
- Virtualized the session list via `@tanstack/react-virtual` so long conversations (thousands of records) scroll smoothly
- Long session messages are collapsed by default to reduce text layout cost
### Codex / OpenClaw Session Title Extraction
- Meaningful title auto-extraction for Codex and OpenClaw sessions with 2-line display
- Strips OpenClaw `message_id` suffix noise
### Usage Date Range Picker
- New date range selector on the usage dashboard with preset tabs (Today / 1d / 7d / 14d / 30d) + custom date + time calendar (#2002, thanks @yovinchen)
- Page-jump input added on paginated lists
### Model Mapping Quick-Set
- New quick-set button next to model mapping fields in provider forms for faster edits (#2179, thanks @lispking)
### Stream Check Error Classification
- Stream Check errors are classified and surfaced as color-coded toasts
- Refreshed default probe models to match each vendor's current lineup
- Explicit detection for "model not found" responses
### Block Official Provider Switching During Local Routing
- Switching to official providers is blocked while Local Routing is active, with a warning toast
- Reason: routing official API traffic through the local proxy carries account-suspension risk
### Pricing Database Refresh (v8 → v9)
- Reseed-on-migration pricing table
- ~50 new model pricing entries including Claude 4.7, Opus 4.7 Adaptive Thinking, Grok 4, Qwen 3.5/3.6, MiniMax M2.5/M2.7, Doubao Seed 2.0 series, GLM-5/5.1
- Corrected stale prices for DeepSeek, Kimi K2.5, and others
### Application-Level Window Controls
- Opt-in setting to render CC Switch's own minimize / toggle-maximize / close buttons instead of system decorations (#1119, thanks @git1677967754)
- Materially improves the experience on Linux Wayland where compositor-drawn buttons can become inert
### Hermes in Unified Skills Management
- Hermes is added to the unified Skills surface
- Skill install, enable, and filter now cover the Hermes app alongside Claude / Codex / Gemini / OpenCode / OpenClaw
### OpenClaw Config Directory Override
- New settings option to point CC Switch at a custom `openclaw.json` location (#1518, thanks @mrFranklin)
### Hermes Config Directory Override
- New settings option to point CC Switch at a custom `~/.hermes/config.yaml` location, backed by data-driven dispatch
### StepFun Step Plan Preset
- StepFun Step Plan (EN / ZH) provider presets (#2155, thanks @hengm3467)
### New API Usage Script Template
- Added a User-Agent header to the New API usage script template for better upstream compatibility
### LemonData Provider Preset (All Six Apps)
- LemonData registered as a third-party partner preset across Claude, Codex, Gemini, OpenCode, OpenClaw, and Hermes
- Icon assets and zh / en / ja partner-promotion copy
- Claude preset uses `ANTHROPIC_API_KEY` auth; OpenAI-compatible apps target `gpt-5.4`
### DDSHub Codex Preset
- Added a Codex-compatible endpoint for DDSHub at the same host as its Claude service
- Base URL omits the `/v1` suffix because the gateway auto-routes OpenAI SDK paths
---
## Changed
### "Local Proxy Takeover" → "Local Routing"
- Unified the terminology across UI copy, README, and docs in all three locales
- Functional behavior is unchanged
### Hermes `Auto` api_mode Removed
- Users must pick an explicit protocol; new deeplinks default to `chat_completions`
- Eliminates URL-based heuristic surprises
### Hermes Provider Form
- Added an API mode dropdown and per-provider model editor
- Binds per-provider models to the top-level `model:` when switching active providers
### Hermes Deep Config Delegation
- Deep YAML knobs are no longer duplicated in the CC Switch form — they are delegated to the Hermes Web UI via a direct launch action
### Hermes Toolbar Layout
- Swapped the Hermes Web UI button from `ExternalLink` to `LayoutDashboard` (clicking may spawn `hermes dashboard` rather than just opening a URL)
- Moved MCP to the final toolbar slot so Hermes matches the Claude / Codex / Gemini / OpenCode layout
### `ANTHROPIC_REASONING_MODEL` Removed from Claude Quick-Set
- Decoupled the reasoning capability from model selection; the legacy field is no longer surfaced in the quick-set form
### Per-Provider Proxy Config Removed
- Consolidated into global Local Routing
- Provider-level proxy toggle and associated storage are gone
### Unified Toolbar Icon Button Width
- Normalized icon-button widths across Claude / Codex / Gemini / OpenCode / OpenClaw / Hermes panels for a consistent header look
### Rust Toolchain Pinned to 1.95
- Adopted clippy 1.95 suggestions across the workspace and pinned the toolchain to prevent nightly drift
### Tray Menu ID Constant
- The tray identifier moved from the hardcoded string `"main"` to a `TRAY_ID` constant (`"cc-switch"`) across all call sites (#1978, thanks @lidaxian121)
### Copilot Premium Consumption Deep Optimization
A systematic overhaul to reduce Copilot reverse-proxy premium interaction consumption across multiple dimensions:
- **Proactive Thinking Block Stripping Before Forwarding**: Anthropic's `thinking` / `redacted_thinking` blocks are rejected by OpenAI-compatible endpoints. Previously, the request failed upstream, burning one premium interaction before the `thinking_rectifier` could retry. A new proactive strip step (Copilot optimization pipeline step 3.5, after `tool_result` merging) eliminates that wasted interaction
- **Request Classification Fix**: Messages containing `tool_result` are now classified as agent continuation instead of user-initiated, preventing every tool call from being falsely counted as a premium interaction
- **Subagent Detection**: Identifies subagents via `__SUBAGENT_MARKER__` with `metadata._agent_` fallback, setting `x-interaction-type=conversation-subagent`
- **Deterministic `x-interaction-id` Billing Merge**: Derives `x-interaction-id` from the session ID so multiple requests within the same session collapse into a single billing interaction
- **Orphan `tool_result` Sanitization**: Cleans up orphan `tool_result` entries to prevent upstream errors that would trigger retries and duplicate billing
- **Warmup Downgrade Enabled by Default**: Uses `gpt-5-mini` as the default downgrade model
- **Optimization Pipeline Reorder**: classify → sanitize → merge → warmup, so classification sees raw `tool_result` semantics
- Fixed a `CopilotOptimizerConfig` default-value inconsistency (unified to `gpt-5-mini`)
### Usage Script Intranet Support
- Removed private-IP / suspicious-hostname blocking from usage scripts, unblocking enterprise intranet, Docker, and self-hosted API endpoints
- Built-in templates still enforce HTTPS (except localhost) and same-origin checks; custom templates remain user-controlled with those request-URL checks skipped
### Failover Queue Notes
- Provider notes now appear in failover queue selectors and queue rows for easier identification across multi-provider queues (#2138, thanks @Coconut-Fish)
---
## Fixed
### Header Auto-Compact Latching After Maximize
- The toolbar no longer stays compacted after maximize/restore; compaction now reevaluates on size changes
### Hermes YAML Pollution & OAuth MCP `auth` Drop
- Round-tripping through CC Switch no longer drops OAuth MCP `auth` blocks or pollutes unrelated YAML keys
- Guard tests added via `tests/hermes_roundtrip.rs`
### Hermes Active Provider Display
- Hermes UI now correctly surfaces the active provider and wires add / enable / remove actions
### Hermes Provider Persistence
- Providers persist under `custom_providers:` so `api_mode` and `model` survive restarts and config reloads
### Hermes Health Check Borrowing OpenClaw Schema
- Hermes providers were routed through `check_additive_app_stream` (the OpenClaw dispatcher), which reads camelCase `baseUrl` / `apiKey` / `api` and surfaced "OpenClaw provider is missing baseUrl" even when every Hermes field was filled
- Introduced `check_hermes_stream` with Hermes-specific extractors that map `api_mode` (`chat_completions` / `anthropic_messages` / `codex_responses`) to the matching `check_claude_stream` `api_format`; `bedrock_converse` returns as unsupported
- `api_mode` is now resolved before URL / API key extraction, so `bedrock_converse` users see the real cause rather than a misleading "missing base_url"
### Usage Query Modal for Hermes & OpenClaw
- `getProviderCredentials` now reads flat `settingsConfig` fields for Hermes (snake_case `base_url` / `api_key`) and OpenClaw (camelCase `baseUrl` / `apiKey`), so the "official balance" template auto-selects for matching providers like SiliconFlow
- Refactored the BALANCE and TOKEN_PLAN test paths to reuse the precomputed `providerCredentials` instead of re-reading `env.ANTHROPIC_*` directly, fixing the "empty key" error for non-Claude apps even when the key was configured
### Codex `cache_control` Preservation
- Preserve `cache_control` when merging system prompts during Codex format conversion (#1946, thanks @yovinchen)
### Claude Prompt Cache Key Leak
- Stopped sending prompt cache keys during Claude chat conversions (#2003, thanks @yovinchen)
### Proxy Hop-by-Hop Header Stripping
- Strip hop-by-hop response headers (Connection, Keep-Alive, Transfer-Encoding, etc.) per RFC 7230 (#2060, thanks @yovinchen)
### Permissive Proxy CORS Removed
- Removed the permissive CORS layer from the proxy (#1915, thanks @zerone0x)
### Backend Error Details in Proxy Toast
- Surface backend error payload details in proxy-related toast messages instead of a generic failure string
### Usage Log Deduplication
- Deduplicated proxy and session-log usage records so the same request is no longer double-counted
- Synced the request log time range with the dashboard's 1d / 7d / 30d selector
### Common Config Checkbox Persistence
- Checkbox state for Claude / Codex / Gemini common-config toggles now persists correctly across reopens (#2191, thanks @zxZeng)
### Claude Plugin `settings.json` Sync
- Editing the current provider now syncs back to `settings.json` for the Claude plugin path (#1905, thanks @chengww5217)
### Google Official Gemini Env Preservation
- Saving the Google Official Gemini provider no longer clobbers the `env` block
### OpenCode JSON5 Parser for Trailing Commas
- OpenCode config reads now tolerate trailing commas via a JSON5 parser (#2023, thanks @wwminger)
### Preset Refreshes
- Refreshed stale context windows for DeepSeek and Claude 1M
- Refreshed stale model IDs; backfilled Hermes model lists
- Fixed the Nous endpoint and replaced the Hermes placeholder icon with Nous brand artwork
- Pruned unused official Hermes presets
### Auto-Expand Collapsed Messages on Search Hit
- Collapsed messages now auto-expand when a search match lands inside hidden content
### Unknown Subscription Quota Tiers Hidden
- Provider cards no longer render unknown subscription quota tiers
### Weekly Limit Label Unified
- Aligned the `weekly_limit` tier label with the official 7-day naming across locales
### Root-Level Skill Repo Install
- Fixed skill installation when the repository root itself is a skill
### Session ID Parsing Clippy
- Removed a redundant closure in session ID parsing (clippy warning)
### Stream Check Default Models Refresh
- Updated stream-check default probe models to match each vendor's current lineup
### Skills Import Sync
- Imported Skills are now immediately synced into enabled app directories instead of only being recorded in the database (#2101, thanks @yaoguohh)
- The UI no longer shows "installed" while the target app directory is missing the skill
### Ghostty Session Restore
- Fixed Ghostty session restore launch by using shell execution with `--working-directory` (#1976, thanks @Suda202)
- Avoids `cwd` escaping issues when the path contains spaces or special characters
---
## Docs
### README Sponsor Updates
- Updated SiliconFlow signup bonus to ¥16
- Trimmed the SSSAiCode sponsor blurb
- Updated partner logos
- Added LemonData as a new sponsor
### Global Proxy Hint Clarified
- Clarified the global proxy hint about local routing across all three locales
### Takeover → Routing Rename
- Renamed takeover docs to routing and updated anchors across all languages
### PIPELLM Website URL
- Updated the PIPELLM sponsor website URL to `code.pipellm.ai`
---
## ⚠️ Breaking Changes
### Hermes requires explicit `api_mode`
- The `Auto` mode is gone; imported or deeplinked providers default to `chat_completions`
- Users with prior `Auto` configs will be prompted to pick a protocol
### `ANTHROPIC_REASONING_MODEL` removed from Claude quick-set
- The legacy field is no longer exposed; existing settings are cleaned up automatically
### Per-provider proxy configuration removed
- Migrate to the global Local Routing setting
- Existing per-provider proxy values are ignored
### Database schema v9 → v10
- Adds `enabled_hermes` columns to `mcp_servers` and `skills`
- Auto-migrated with `DEFAULT 0`; no data loss
### Pricing table reseeded (v8 → v9)
- The `model_pricing` table is cleared and reseeded on first launch to pick up new models and corrected prices
### XCodeAPI preset removed
- Users of the XCodeAPI preset should switch to another provider
---
## ⚠️ Risk Notice
This release inherits the risk notices originally introduced in v3.12.3 / v3.13.0 for reverse-proxy-style features.
**GitHub Copilot Reverse Proxy**: Using Copilot's reverse-proxy path may violate GitHub / Microsoft's terms of service. See [v3.12.3 release notes](v3.12.3-en.md#-risk-notice).
**Codex OAuth Reverse Proxy**: Using the Codex OAuth reverse proxy with a ChatGPT subscription may violate OpenAI's terms of service. See [v3.13.0 release notes](v3.13.0-en.md#-risk-notice).
By enabling these features, users **accept all associated risks**. CC Switch is not responsible for any account restrictions, warnings, or service suspensions that result from using these features.
---
## Download & Installation
Visit [Releases](https://github.com/farion1231/cc-switch/releases/latest) to download the appropriate version.
### System Requirements
| OS | Minimum Version | Architecture |
| ------- | ----------------------- | ----------------------------------- |
| Windows | Windows 10 or later | x64 |
| macOS | macOS 12 (Monterey) or later | Intel (x64) / Apple Silicon (arm64) |
| Linux | See table below | x64 |
### Windows
| File | Description |
| ---------------------------------------- | ----------------------------------------------- |
| `CC-Switch-v3.14.0-Windows.msi` | **Recommended** - MSI installer, supports auto-update |
| `CC-Switch-v3.14.0-Windows-Portable.zip` | Portable, extract and run, no registry writes |
### macOS
| File | Description |
| -------------------------------- | -------------------------------------------------------- |
| `CC-Switch-v3.14.0-macOS.dmg` | **Recommended** - DMG installer, drag into Applications |
| `CC-Switch-v3.14.0-macOS.zip` | Extract and drag into Applications, Universal Binary |
| `CC-Switch-v3.14.0-macOS.tar.gz` | For Homebrew installation and auto-update |
> macOS builds are Apple code-signed and notarized — install directly.
### Homebrew (macOS)
```bash
brew tap farion1231/ccswitch
brew install --cask cc-switch
```
Update:
```bash
brew upgrade --cask cc-switch
```
### Linux
| Distribution | Recommended | Installation |
| --------------------------------------- | ----------- | ---------------------------------------------------------------------- |
| Ubuntu / Debian / Linux Mint / Pop!\_OS | `.deb` | `sudo dpkg -i CC-Switch-*.deb` or `sudo apt install ./CC-Switch-*.deb` |
| Fedora / RHEL / CentOS / Rocky Linux | `.rpm` | `sudo rpm -i CC-Switch-*.rpm` or `sudo dnf install ./CC-Switch-*.rpm` |
| openSUSE | `.rpm` | `sudo zypper install ./CC-Switch-*.rpm` |
| Arch Linux / Manjaro | `.AppImage` | Add execute permission and run, or use AUR |
| Other distros / not sure | `.AppImage` | `chmod +x CC-Switch-*.AppImage && ./CC-Switch-*.AppImage` |
-469
View File
@@ -1,469 +0,0 @@
# CC Switch v3.14.0
> Hermes Agent が 6 番目の管理対象アプリに、Claude Opus 4.7 をプリセットマトリクス全体へ展開、Gemini Native API プロキシ、「Local Routing」への名称統一、アプリケーションレベルのウィンドウコントロール
**[中文版 →](v3.14.0-zh.md) | [English →](v3.14.0-en.md)**
---
## 概要
CC Switch v3.14.0 は、**Hermes Agent を 6 番目の一等管理対象アプリケーション**として CC Switch に取り込み、**Claude Opus 4.7** をアグリゲーターおよび Bedrock プリセットのマトリクス全体に展開することを中心に据えた大型リリースです。Hermes サポートは、データベース v9 → v10 マイグレーション、完全な Rust コマンド面、アトミックバックアップ付きの YAML ベースな `~/.hermes/config.yaml` 読み書き、MCP 同期、Skills 同期、SQLite + JSONL セッション管理、および Memory エディターを含む専用のフロントエンドパネルをカバーします。Hermes Agent 0.10.0 スキーマに整合する 4 つの API プロトコル(`chat_completions``anthropic_messages``codex_responses``bedrock_converse`)すべてを選択可能です。ユーザーが直接記述した `providers:` dict のエントリは読み取り専用カードとして表示され、深い YAML 設定は Hermes Web UI に委譲されます。
Hermes に加えて、本リリースでは **Gemini Native API プロキシ**`api_format = "gemini_native"`)を追加し、プロキシがリクエストを Google の `generateContent` エンドポイントに直接転送できるようにしました(完全なストリーミング、スキーマ変換、シャドウリクエストをサポート)。また、旧「Local Proxy Takeover」を三言語の UI / README / ドキュメント全体で **Local Routing** に統一リネームし、コンポジターが描画するボタンが無反応になり得る Linux Wayland などのシーンで、CC Switch が自前で最小化 / 最大化 / 閉じるボタンを描画できるオプション「**アプリケーションレベルのウィンドウコントロール**」を導入しました。さらにリリース直前に、ツールバーからの `hermes dashboard` 直接起動、LemonData の全アプリプリセット、DDSHub の Codex エンドポイント、および複数の Hermes ヘルスチェックと Usage モーダルの修正が追加されました。
セッション側では、`@tanstack/react-virtual` によるセッションリストの**仮想化**で数千件のレコードを持つ長い会話も滑らかにスクロールでき、長いメッセージはデフォルトで折り畳まれます。Usage ダッシュボードには**日付範囲ピッカー**(今日 / 1d / 7d / 14d / 30d + カスタム日時カレンダー)とページジャンプ入力が追加され、**Stream Check エラー分類**は色分けされたトーストで提示され、デフォルトの探索モデルが更新され、「モデルが見つからない」レスポンスを個別に識別するようになりました。また、Local Routing が有効な間に公式プロバイダーへの切り替えを**強制的にブロック**する保護を追加し、公式 API トラフィックがローカルプロキシを経由することによるアカウント停止リスクを防ぎます。Pricing データベースは v8 → v9 で再シードされ、約 50 件の新しいモデルエントリ(Claude 4.7、Opus 4.7 Adaptive Thinking、Grok 4、Qwen 3.5/3.6、MiniMax M2.5/M2.7、Doubao Seed 2.0 系列、GLM-5/5.1 など)を追加し、いくつかの古い価格を修正しました。
**リリース日**: 2026-04-21
**更新規模**: 100 commits | 219 files changed | +20,548 / -3,569 lines
---
## ハイライト
- **Hermes Agent サポート(6 番目の管理対象アプリ)**: データベース v9 → v10 マイグレーション、完全な Rust コマンド面、アトミックバックアップ付き YAML 読み書き、MCP 同期、Skills 同期、SQLite + JSONL セッション管理、専用フロントエンドパネル、4 つの API プロトコル(`chat_completions` / `anthropic_messages` / `codex_responses` / `bedrock_converse`
- **Claude Opus 4.7 の全面展開**: 適応的思考のホワイトリスト、百万トークン単位の価格シード、Bedrock SKU(`anthropic.claude-opus-4-7` / `global.anthropic.claude-opus-4-7`、旧 `-v1` サフィックスを廃止)、全アグリゲーター / Bedrock プリセットを Opus 4.7 をデフォルト Opus モデルに移行
- **Claude `max` エフォートティア**: エフォートのドロップダウンを `high` から `max` に引き上げ
- **Gemini Native API プロキシ**: 新しい `api_format = "gemini_native"` により、プロキシが Google の `generateContent` に直接転送可能に(完全なストリーミング / スキーマ変換 / シャドウリクエスト対応)
- **GitHub Copilot Enterprise Server**: Copilot ベースの Claude プロバイダーに GHES 認証とエンドポイント設定を追加
- **Copilot 交互消費の大幅最適化**: 転送前の thinking ブロック主動削除、`tool_result` メッセージ分類修正、subagent 検出、`x-interaction-id` 課金マージ、孤立 `tool_result` のサニタイズ、Warmup ダウングレードのデフォルト有効化など、premium 交互消費を系統的に削減
- **セッションリスト仮想化**: 長い会話が滑らかにスクロール。長いメッセージはデフォルトで折り畳まれ、テキストレイアウトコストを削減
- **Codex / OpenClaw セッションタイトル抽出**: 意味のあるタイトルを自動抽出(2 行表示)、OpenClaw の `message_id` 末尾ノイズを除去
- **Usage 日付範囲ピッカー**: Today / 1d / 7d / 14d / 30d プリセットタブ + カスタム日時カレンダー。ページネーションリストにページジャンプ入力
- **Stream Check エラー分類**: エラーを分類し色分けトーストで提示。デフォルト探索モデル更新。「モデルが見つからない」レスポンスを明示的に検出
- **Local Routing 有効時の公式プロバイダー切り替えブロック**: 公式 API トラフィックをローカルプロキシ経由で流すとアカウント停止のリスクがあるため、切り替えを強制ブロックして警告トーストを表示
- **Pricing データベース刷新(v8 → v9)**: 約 50 件の新しいモデルエントリを追加し、古い価格を修正
- **アプリケーションレベルのウィンドウコントロール**: CC Switch が自前で最小化 / 最大化トグル / 閉じるボタンを描画するオプション設定。Linux Wayland での体験を大きく改善
- **統一 Skills 管理への Hermes 追加**: Skill のインストール / 有効化 / フィルターが Hermes をカバー
- **Hermes / OpenClaw 設定ディレクトリのカスタマイズ**: 設定で `~/.hermes/config.yaml``openclaw.json` のカスタム位置を指定可能
- **ツールバーからの Hermes Dashboard 起動**: Hermes Web UI のプローブに失敗した際、ツールバーエントリからユーザーの優先ターミナルで `hermes dashboard` を実行可能
- **新パートナープリセット**: LemonData を全 6 アプリにわたって追加、DDSHub の Codex エンドポイント、StepFun Step Plan
---
## 新機能
### Hermes Agent サポート(6 番目の管理対象アプリ)
CC Switch は Hermes Agent を Claude / Codex / Gemini / OpenCode / OpenClaw と並ぶ一等の管理対象アプリとして初めてサポートします。
- **データベースマイグレーション v9 → v10**: `mcp_servers``skills` テーブルに `enabled_hermes` カラムを追加(`DEFAULT 0`、自動マイグレーション、データ損失なし)
- **YAML 設定の読み書き**: `~/.hermes/config.yaml` をアトミックバックアップ付きで読み書き。`tests/hermes_roundtrip.rs` が OAuth MCP `auth` ブロックの消失や無関係なキーの汚染を防止
- **4 つの API プロトコル**: Hermes Agent 0.10.0 と整合する `chat_completions` / `anthropic_messages` / `codex_responses` / `bedrock_converse`。新しいディープリンクはデフォルトで `chat_completions`
- **ユーザー `providers:` dict の読み取り専用表示**: YAML に手書きされたプロバイダーエントリは CC Switch で読み取り専用カードとして表示され、深い設定は Hermes Web UI に委譲
- **加算的な切り替え**: Claude / Codex の「上書き」型切り替えと異なり、Hermes ではすべてのプロバイダーが同じ YAML に共存
### Hermes Memory パネル
- `MEMORY.md` / `USER.md` を直接編集できる Memory パネルを追加(有効化スイッチ、文字数制限、ライブ保存フロー付き)
- Hermes の Prompts エントリを置き換え
### Hermes プロバイダープリセット(約 50 個)
- Nous Research、Shengsuanyun(胜算云)、OpenRouter、DeepSeek、Together AI、StepFun、Zhipu GLM、Bailian(百炼)、Kimi、MiniMax、DouBao(豆包)、BaiLing(百灵)、ModelScope(魔搭)、KAT-Coder、PackyCode、Cubence、AIGoCode、RightCode、AICodeMirror、AICoding、CrazyRouter、SSSAiCode、Micu、CTok.ai、DDSHub、E-FlowCode、LionCCAPI、PIPELLM、Compshare、SiliconFlow、AiHubMix、DMXAPI、TheRouter、Novita、Nvidia、Xiaomi MiMo をカバー
### ツールバーからの Hermes Dashboard 起動
- Hermes Web UI のプローブに失敗した際、ツールバーエントリがユーザーの優先ターミナルで `hermes dashboard` を実行する確認ダイアログを表示
- 一時 bash / batch スクリプト経由で起動。`hermes dashboard` 自身が準備完了後にブラウザを開くため、ポーリングは不要
- Memory パネルと Health バナーは既存のトースト動作を維持
- オフラインのトーストにあった古い `hermes web` のヒントも修正(正しいコマンドは `hermes dashboard`
- Linux ターミナル検出の順序を変更し、`/usr/bin``/bin``/usr/local/bin` を stat する前に `which` を試すように
### Claude Opus 4.7 サポート
- Claude Opus 4.7 を追加。適応的思考のホワイトリスト、百万トークン単位の価格シード、Bedrock SKU(`anthropic.claude-opus-4-7` / `global.anthropic.claude-opus-4-7`、旧 `-v1` サフィックスを廃止)
- 全アグリゲーター / Bedrock プリセットをデフォルト Opus モデルとして Opus 4.7 に移行
### Claude `max` エフォートティア
- Claude エフォートドロップダウンを `high` から `max` に引き上げ、より強力な推論容量を解放
### Gemini Native API プロキシ
- 新しい `api_format = "gemini_native"` により、プロキシが Google の `generateContent` API に直接転送可能 (#1918, 感謝 @yovinchen)
- 完全なストリーミング、スキーマ変換、シャドウリクエストに対応
- proxy providers モジュール下に `gemini_url.rs``gemini_schema.rs``gemini_shadow.rs``streaming_gemini.rs``transform_gemini.rs` を追加
### GitHub Copilot Enterprise ServerGHES
- Copilot ベースの Claude プロバイダーに GHES 認証とエンドポイント設定を追加 (#2175, 感謝 @hotelbe)
### セッションリスト仮想化
- `@tanstack/react-virtual` によりセッションリストを仮想化。数千件のレコードを持つ長い会話も滑らかにスクロール
- 長いセッションメッセージはデフォルトで折り畳まれ、テキストレイアウトコストを削減
### Codex / OpenClaw セッションタイトル抽出
- Codex と OpenClaw セッションから意味のあるタイトルを自動抽出し、2 行表示
- OpenClaw の `message_id` 末尾ノイズを除去
### Usage 日付範囲ピッカー
- Usage ダッシュボードに日付範囲セレクターを追加。プリセットタブ(Today / 1d / 7d / 14d / 30d+ カスタム日時カレンダー (#2002, 感謝 @yovinchen)
- ページネーションリストにページジャンプ入力を追加
### モデルマッピングのクイック入力
- プロバイダーフォームのモデルマッピングフィールドの横にクイック入力ボタンを追加し、編集を高速化 (#2179, 感謝 @lispking)
### Stream Check エラー分類
- Stream Check エラーを分類し、色分けトーストとして提示
- デフォルトの探索モデルを各ベンダーの現行ラインナップに合わせて更新
- 「モデルが見つからない」レスポンスを明示的に検出
### Local Routing 有効時の公式プロバイダー切り替えブロック
- Local Routing が有効な状態で公式プロバイダーに切り替えようとすると、強制的にブロックされ警告トーストが表示される
- 理由: 公式 API トラフィックをローカルプロキシ経由で流すとアカウント停止のリスクがあるため
### Pricing データベース刷新(v8 → v9)
- マイグレーション時に定価テーブルを再シード
- Claude 4.7、Opus 4.7 Adaptive Thinking、Grok 4、Qwen 3.5/3.6、MiniMax M2.5/M2.7、Doubao Seed 2.0 系列、GLM-5/5.1 などを含む約 50 件の新しいモデルエントリを追加
- DeepSeek、Kimi K2.5 などの古い価格を修正
### アプリケーションレベルのウィンドウコントロール
- CC Switch が自前で最小化 / 最大化トグル / 閉じるボタンを描画するオプション設定を追加。システム装飾の代わりに使用 (#1119, 感謝 @git1677967754)
- コンポジター描画ボタンが無反応になり得る Linux Wayland での体験を大きく改善
### 統一 Skills 管理への Hermes 追加
- 統一 Skills サーフェスに Hermes を追加
- Skill のインストール / 有効化 / フィルターが、Claude / Codex / Gemini / OpenCode / OpenClaw と並んで Hermes アプリをカバー
### OpenClaw 設定ディレクトリのカスタマイズ
- CC Switch が参照する `openclaw.json` のカスタム位置を設定できるオプションを追加 (#1518, 感謝 @mrFranklin)
### Hermes 設定ディレクトリのカスタマイズ
- CC Switch が参照する `~/.hermes/config.yaml` のカスタム位置を設定できるオプションを追加。データ駆動 dispatch でサポート
### StepFun Step Plan プリセット
- StepFun Step PlanEN / ZH)プロバイダープリセットを追加 (#2155, 感謝 @hengm3467)
### New API 用量スクリプトテンプレート
- New API の用量スクリプトテンプレートに User-Agent ヘッダーを追加し、上流互換性を向上
### LemonData プロバイダープリセット(全 6 アプリ)
- LemonData をサードパーティパートナープリセットとして Claude、Codex、Gemini、OpenCode、OpenClaw、Hermes の全 6 アプリに登録
- アイコンアセットと zh / en / ja 三言語のパートナー推奨文面を追加
- Claude プリセットは `ANTHROPIC_API_KEY` 認証を使用。OpenAI 互換アプリは `gpt-5.4` をターゲット
### DDSHub Codex プリセット
- DDSHub の Codex 互換エンドポイントを追加(Claude サービスと同じホスト)
- ベース URL は `/v1` サフィックスを省略(ゲートウェイが OpenAI SDK パスを自動ルーティング)
---
## 変更
### 「Local Proxy Takeover」→「Local Routing」
- 三言語の UI 文言、README、ドキュメント全体で用語を統一リネーム
- 機能的な動作は変更なし
### Hermes `Auto` api_mode の削除
- ユーザーは明示的にプロトコルを選択する必要あり。新しいディープリンクはデフォルトで `chat_completions`
- URL ベースのヒューリスティックによる意外な挙動を排除
### Hermes プロバイダーフォーム
- API モードドロップダウンとプロバイダー単位のモデルエディターを追加
- アクティブなプロバイダーを切り替える際、プロバイダー単位のモデルをトップレベルの `model:` にバインド
### Hermes 深い設定の委譲
- 深い YAML 設定は CC Switch フォームで重複させず、「Hermes Web UI を起動」ボタン経由で Web UI に直接委譲
### Hermes ツールバーレイアウト
- Hermes Web UI ボタンのアイコンを `ExternalLink` から `LayoutDashboard` に変更(クリック時に単に URL を開くのではなく `hermes dashboard` を起動する場合があるため、パネル型アイコンのほうが意味的に正確)
- MCP をツールバーの末尾に移動し、Hermes のレイアウトを Claude / Codex / Gemini / OpenCode と揃える
### Claude Quick-Set から `ANTHROPIC_REASONING_MODEL` を削除
- 推論能力とモデル選択を分離。レガシーフィールドは Quick-Set フォームから除外
### プロバイダー単位のプロキシ設定を削除
- グローバルな Local Routing に統合
- プロバイダー単位のプロキシトグルと関連ストレージは削除済み
### ツールバーアイコンボタン幅の統一
- Claude / Codex / Gemini / OpenCode / OpenClaw / Hermes パネルの間でアイコンボタン幅を正規化し、ヘッダーの見た目を統一
### Rust Toolchain を 1.95 にピン留め
- ワークスペース全体で clippy 1.95 の提案を採用し、nightly ドリフトを防ぐためツールチェーンをピン留め
### トレイメニュー ID 定数
- トレイ識別子をハードコーディング文字列 `"main"` から `TRAY_ID` 定数(`"cc-switch"`)に移行。すべての呼び出し箇所で同期 (#1978, 感謝 @lidaxian121)
### Copilot 交互消費の大幅最適化
Copilot リバースプロキシの premium 交互消費を削減するための系統的な最適化。以下の複数の改善をカバー:
- **転送前に thinking ブロックを主動削除**: Anthropic の `thinking` / `redacted_thinking` ブロックは OpenAI 互換エンドポイントに拒否される。従来は上流でリクエストが失敗して premium 交互を 1 回消費した後、`thinking_rectifier` によってリトライされていた。新しい主動削除ステップ(Copilot 最適化パイプラインの 3.5 ステップ目、`tool_result` マージ後)により、この無駄な premium 消費を直接解消
- **リクエスト分類の修正**: `tool_result` を含むメッセージをユーザー発起の新規リクエストではなく、エージェント継続として分類。ツール呼び出しが毎回 premium 交互としてカウントされる問題を防止
- **subagent 検出**: `__SUBAGENT_MARKER__``metadata._agent_` フォールバックで subagent を識別し、`x-interaction-type=conversation-subagent` を設定
- **決定論的 `x-interaction-id` による課金マージ**: セッション ID から `x-interaction-id` を導出し、同一セッション内の複数リクエストを 1 回の課金交互に統合
- **孤立 `tool_result` のサニタイズ**: 孤立した `tool_result` を整理し、上流エラーによるリトライおよび重複課金を防止
- **Warmup ダウングレードをデフォルトで有効化**: `gpt-5-mini` をデフォルトのダウングレードモデルとして使用
- **最適化パイプラインの並び替え**: classify → sanitize → merge → warmup の順序で、分類が生の `tool_result` セマンティクスを参照可能に
- `CopilotOptimizerConfig` のデフォルト値の不一致を修正(`gpt-5-mini` に統一)
### 用量スクリプトのイントラネットサポート
- 用量スクリプトからプライベート IP / 不審なホスト名のブロッキングを削除し、エンタープライズイントラネット、Docker、自己ホスト API エンドポイントを解放
- ビルトインテンプレートは引き続き HTTPS(localhost を除く)と同一オリジンチェックを強制。カスタムテンプレートはユーザー制御のまま、リクエスト URL のチェックをスキップ
### Failover キューの備考表示
- プロバイダーの備考が failover キューセレクターとキュー行に表示され、マルチプロバイダーキューでの識別が容易に (#2138, 感謝 @Coconut-Fish)
---
## バグ修正
### 最大化後のツールバー自動折り畳みラッチ
- ウィンドウの最大化 / 復元後、ツールバーが折り畳まれたままになる問題を修正。折り畳み判定はサイズ変更時に再評価される
### Hermes YAML 汚染と OAuth MCP `auth` 消失
- CC Switch 経由でラウンドトリップしても OAuth MCP `auth` ブロックが消失したり、無関係な YAML キーが汚染されたりしなくなった
- `tests/hermes_roundtrip.rs` をガードテストとして追加
### Hermes アクティブプロバイダー表示
- Hermes UI がアクティブプロバイダーを正しく表示するようになり、追加 / 有効化 / 削除アクションが正しく動作
### Hermes プロバイダーの永続化
- プロバイダーは `custom_providers:` の下に永続化され、`api_mode``model` が再起動 / 設定再読み込みを生き延びる
### Hermes ヘルスチェックが OpenClaw のスキーマを流用していた問題
- 以前 Hermes プロバイダーは `check_additive_app_stream`(OpenClaw のディスパッチャー)にルーティングされており、これは camelCase の `baseUrl` / `apiKey` / `api` を読むため、Hermes フィールドをすべて記入しても "OpenClaw provider is missing baseUrl" と表示されていた
- `check_hermes_stream` を導入し、Hermes 専用のエクストラクターで `api_mode``chat_completions` / `anthropic_messages` / `codex_responses`)を対応する `check_claude_stream``api_format` にマッピング。`bedrock_converse` は非対応として返す
- URL / API キーの抽出前に `api_mode` を解決することで、`bedrock_converse` を選んだユーザーには「missing base_url」という誤解を招くメッセージではなく実際の原因が表示される
### Hermes / OpenClaw 向け Usage クエリモーダル
- `getProviderCredentials` が Hermessnake_case の `base_url` / `api_key`)と OpenClawcamelCase の `baseUrl` / `apiKey`)のフラットな `settingsConfig` フィールドを読むようになり、SiliconFlow などマッチするプロバイダーで「official balance」テンプレートが自動選択される
- BALANCE と TOKEN_PLAN テストパスをリファクタリングし、`env.ANTHROPIC_*` を直接再読するのではなく、事前計算された `providerCredentials` を再利用するように変更。これにより非 Claude アプリでキーが設定されていても「empty key」エラーが出ていた問題を修正
### Codex `cache_control` 保持
- Codex フォーマット変換中に system prompt をマージする際の `cache_control` を保持 (#1946, 感謝 @yovinchen)
### Claude プロンプトキャッシュキーのリーク
- Claude chat 変換時にプロンプトキャッシュキーを送信しないように修正 (#2003, 感謝 @yovinchen)
### プロキシ Hop-by-Hop レスポンスヘッダーの削除
- RFC 7230 に従ってプロキシレスポンスの hop-by-hop ヘッダー(Connection、Keep-Alive、Transfer-Encoding など)を削除 (#2060, 感謝 @yovinchen)
### プロキシの寛容な CORS レイヤー削除
- プロキシの寛容な CORS レイヤーを削除 (#1915, 感謝 @zerone0x)
### プロキシトーストでのバックエンドエラー詳細表示
- プロキシ関連のトーストメッセージで、汎用的な失敗文字列ではなくバックエンドのエラーペイロードの詳細を表示
### Usage ログの重複排除
- プロキシとセッションログの用量レコードを重複排除し、同じリクエストが二重にカウントされないように修正
- リクエストログの時間範囲をダッシュボードの 1d / 7d / 30d セレクターと同期
### Common Config チェックボックスの永続化
- Claude / Codex / Gemini の common-config トグルのチェック状態が再オープンをまたいで正しく保持されるように修正 (#2191, 感謝 @zxZeng)
### Claude プラグイン `settings.json` 同期
- 現在のプロバイダーを編集すると、Claude プラグインパスの `settings.json` に同期されるように修正 (#1905, 感謝 @chengww5217)
### Google Official Gemini の env 保持
- Google Official Gemini プロバイダーを保存しても `env` ブロックが消えないように修正
### OpenCode の JSON5 による末尾カンマ解析
- OpenCode 設定読み取りが JSON5 パーサーにより末尾カンマを許容するように修正 (#2023, 感謝 @wwminger)
### プリセットの刷新
- DeepSeek と Claude 1M の古いコンテキストウィンドウを刷新
- 古いモデル ID を刷新。Hermes のモデルリストをバックフィル
- Nous エンドポイントを修正し、Hermes のプレースホルダーアイコンを Nous ブランドのアートワークに置き換え
- 未使用の公式 Hermes プリセットを整理
### 検索ヒット時の折り畳みメッセージの自動展開
- 隠されたコンテンツ内部で検索マッチが発生した場合、折り畳みメッセージを自動展開してマッチを示す
### 不明なサブスクリプション配額ティアの非表示
- プロバイダーカードは不明なサブスクリプション配額ティアを表示しないように変更
### weekly_limit ラベルの統一
- `weekly_limit` ティアラベルを公式の「7 日」命名にロケール間で揃えた
### ルートレベルの Skill リポジトリインストール
- リポジトリのルート自体が skill の場合のインストール失敗を修正
### Session ID 解析の clippy 警告
- session ID 解析内の冗長なクロージャを削除(clippy 警告)
### Stream Check デフォルトモデルの刷新
- Stream Check のデフォルト探索モデルを各ベンダーの現行ラインナップに合わせて更新
### Skills インポートの同期
- インポートされた Skills はデータベースに記録されるだけでなく、有効化されたアプリディレクトリにも即座に同期されるように変更 (#2101, 感謝 @yaoguohh)
- UI が「インストール済み」と表示しているのに対象アプリディレクトリに skill が存在しない状態を解消
### Ghostty セッション復元
- Ghostty セッション復元の起動を `--working-directory` 付きのシェル実行に変更 (#1976, 感謝 @Suda202)
- パスにスペースや特殊文字が含まれる場合の `cwd` エスケープ問題を回避
---
## ドキュメント
### README スポンサー更新
- SiliconFlow のサインアップボーナスを ¥16 に更新
- SSSAiCode のスポンサー文面を簡潔化
- パートナーロゴを更新
- 新しいスポンサーとして LemonData を追加
### グローバルプロキシヒントの明確化
- 三言語でグローバルプロキシと Local Routing の関係を明確化
### Takeover → Routing ドキュメントのリネーム
- テイクオーバー関連ドキュメントを三言語で routing にリネームし、アンカーを同期更新
### PIPELLM ウェブサイト URL
- PIPELLM スポンサーのウェブサイト URL を `code.pipellm.ai` に更新
---
## ⚠️ 重要な変更(Breaking
### Hermes は明示的な `api_mode` が必須
- `Auto` モードは廃止。インポートまたはディープリンクで取得したプロバイダーはデフォルトで `chat_completions`
- 既存の `Auto` 設定のユーザーはプロトコルを選択するよう促される
### Claude Quick-Set から `ANTHROPIC_REASONING_MODEL` を削除
- レガシーフィールドは公開されなくなった。既存の設定は自動的にクリーンアップされる
### プロバイダー単位のプロキシ設定を削除
- グローバル Local Routing 設定に移行
- 既存のプロバイダー単位のプロキシ値は無視される
### データベーススキーマ v9 → v10
- `mcp_servers``skills``enabled_hermes` カラムを追加
- `DEFAULT 0` で自動マイグレーション、データ損失なし
### Pricing テーブルの再シード(v8 → v9)
- 新しいモデルと修正済み価格を取り込むため、初回起動時に `model_pricing` テーブルがクリアされ再シードされる
### XCodeAPI プリセットの削除
- XCodeAPI プリセットを使用していたユーザーは別のプロバイダーに切り替える必要がある
---
## ⚠️ リスクに関する注意事項
本リリースは、リバースプロキシ型機能について v3.12.3 / v3.13.0 で提起された既存のリスク注意事項を継承します。
**GitHub Copilot リバースプロキシ**: Copilot のリバースプロキシパスを使用すると、GitHub / Microsoft の利用規約に違反する可能性があります。詳細は [v3.12.3 リリースノート](v3.12.3-ja.md#-リスクに関する注意事項) を参照してください。
**Codex OAuth リバースプロキシ**: ChatGPT サブスクリプションで Codex OAuth リバースプロキシを使用すると、OpenAI の利用規約に違反する可能性があります。詳細は [v3.13.0 リリースノート](v3.13.0-ja.md#-リスクに関する注意事項) を参照してください。
これらの機能を有効にすることで、ユーザーは**すべての関連リスクを自己責任で受諾**したものとみなされます。CC Switch はこれらの機能の使用に起因するアカウントの制限、警告、サービス停止について一切の責任を負いません。
---
## ダウンロード・インストール
[Releases](https://github.com/farion1231/cc-switch/releases/latest) から対応バージョンをダウンロードしてください。
### システム要件
| OS | 最小バージョン | アーキテクチャ |
| ------- | ---------------------------- | ----------------------------------- |
| Windows | Windows 10 以降 | x64 |
| macOS | macOS 12 (Monterey) 以降 | Intel (x64) / Apple Silicon (arm64) |
| Linux | 下表参照 | x64 |
### Windows
| ファイル | 説明 |
| ---------------------------------------- | ------------------------------------------- |
| `CC-Switch-v3.14.0-Windows.msi` | **推奨** - MSI インストーラー、自動更新対応 |
| `CC-Switch-v3.14.0-Windows-Portable.zip` | ポータブル版、解凍して実行、レジストリ不要 |
### macOS
| ファイル | 説明 |
| -------------------------------- | -------------------------------------------------------- |
| `CC-Switch-v3.14.0-macOS.dmg` | **推奨** - DMG インストーラー、Applications にドラッグ |
| `CC-Switch-v3.14.0-macOS.zip` | 解凍して Applications にドラッグ、Universal Binary |
| `CC-Switch-v3.14.0-macOS.tar.gz` | Homebrew インストールと自動更新用 |
> macOS 版は Apple のコード署名および公証済みで、直接インストールして使用できます。
### HomebrewmacOS
```bash
brew tap farion1231/ccswitch
brew install --cask cc-switch
```
更新:
```bash
brew upgrade --cask cc-switch
```
### Linux
| ディストリビューション | 推奨形式 | インストール方法 |
| --------------------------------------- | ----------- | ---------------------------------------------------------------------- |
| Ubuntu / Debian / Linux Mint / Pop!\_OS | `.deb` | `sudo dpkg -i CC-Switch-*.deb` または `sudo apt install ./CC-Switch-*.deb` |
| Fedora / RHEL / CentOS / Rocky Linux | `.rpm` | `sudo rpm -i CC-Switch-*.rpm` または `sudo dnf install ./CC-Switch-*.rpm` |
| openSUSE | `.rpm` | `sudo zypper install ./CC-Switch-*.rpm` |
| Arch Linux / Manjaro | `.AppImage` | 実行権限を付与して実行、または AUR を使用 |
| その他のディストリビューション / 不明 | `.AppImage` | `chmod +x CC-Switch-*.AppImage && ./CC-Switch-*.AppImage` |
-468
View File
@@ -1,468 +0,0 @@
# CC Switch v3.14.0
> Hermes Agent 成为第 6 个受管应用、Claude Opus 4.7 全面接入、Gemini Native API 代理、Local Routing 统一重命名、应用级窗口控件
**[English →](v3.14.0-en.md) | [日本語版 →](v3.14.0-ja.md)**
---
## 概览
CC Switch v3.14.0 是一次大版本更新,核心焦点是把 **Hermes Agent 作为第 6 个一等受管应用**接入 CC Switch,并把 **Claude Opus 4.7** 铺设到全部聚合器与 Bedrock 预设矩阵。Hermes 支持覆盖数据库 v9 → v10 迁移、完整的 Rust 命令面、基于 YAML 的 `~/.hermes/config.yaml` 读写(含原子备份)、MCP 同步、Skills 同步、SQLite + JSONL 会话管理,以及专属的前端面板和 Memory 编辑面板;与 Hermes Agent 0.10.0 schema 对齐的四种协议(`chat_completions``anthropic_messages``codex_responses``bedrock_converse`)全部可选。用户自行维护的 `providers:` dict 条目以只读卡片形式呈现,深度 YAML 配置则直接委托给 Hermes Web UI。
除了 Hermes,本次还新增了 **Gemini Native API 代理**`api_format = "gemini_native"`),让代理可以把请求直接转发到 Google 的 `generateContent` 端点,完整支持流式、schema 转换和 shadow 请求;把老的 "Local Proxy Takeover" 在三语 UI / README / 文档中统一重命名为 **Local Routing**;新增 **应用级窗口控件**,在 Linux Wayland 等合成器绘制按钮失灵的场景下可选让 CC Switch 自绘最小化 / 最大化 / 关闭按钮;并在本版本发布前额外合入了从工具栏直接启动 `hermes dashboard`、LemonData 全应用预设、DDSHub Codex 端点以及若干 Hermes 健康检查与 Usage 模态框的修复。
会话侧通过 `@tanstack/react-virtual` **虚拟化会话列表**,让上千条记录的长会话也能流畅滚动,长消息默认折叠;Usage 面板新增**日期范围选择器**(今日 / 1d / 7d / 14d / 30d + 自定义日期时间)和翻页输入;**Stream Check 错误分类**以彩色 toast 呈现,默认探测模型重新梳理,"模型不存在"响应被单独识别;并新增在 Local Routing 激活时**阻止切换到官方供应商**的保护,以免官方流量被引入本地代理造成账号风险。Pricing 数据库 v8 → v9 重新种入约 50 个新模型条目(包括 Claude 4.7、Opus 4.7 Adaptive Thinking、Grok 4、Qwen 3.5/3.6、MiniMax M2.5/M2.7、Doubao Seed 2.0 系列、GLM-5/5.1 等),并修正了多项陈旧价格。
**发布日期**2026-04-21
**更新规模**100 commits | 219 files changed | +20,548 / -3,569 lines
---
## 重点内容
- **Hermes Agent 支持(第 6 个受管应用)**:数据库 v9 → v10 迁移、完整 Rust 命令面、YAML 读写带原子备份、MCP 同步、Skills 同步、SQLite + JSONL 会话管理、专属前端面板、四种 API 协议(`chat_completions` / `anthropic_messages` / `codex_responses` / `bedrock_converse`
- **Claude Opus 4.7 全面接入**:自适应思维白名单、按百万 token 定价种子、Bedrock SKU`anthropic.claude-opus-4-7` / `global.anthropic.claude-opus-4-7`,丢弃老 `-v1` 后缀),全部聚合器 / Bedrock 预设升级为默认 Opus 模型
- **Claude `max` 推理力度**:推理下拉从 `high` 升级到 `max`
- **Gemini Native API 代理**:新增 `api_format = "gemini_native"`,代理可直达 Google `generateContent`,完整流式 / schema 转换 / shadow 请求
- **GitHub Copilot 企业版**:为 Copilot 型 Claude 供应商新增 GHES 认证与端点配置
- **Copilot 次数消耗深度优化**:转发前主动剥离 thinking 块、`tool_result` 消息归类修正、subagent 检测、`x-interaction-id` 合并计费、orphan `tool_result` 清理、默认启用 warmup 降级 —— 系统性降低 premium 交互消耗
- **会话列表虚拟化**:长会话流畅滚动,长消息默认折叠降低文字布局成本
- **Codex / OpenClaw 会话标题提取**:自动抽取有意义标题,两行显示,剥离 OpenClaw `message_id` 尾噪声
- **Usage 日期范围选择器**Today / 1d / 7d / 14d / 30d 预设 + 自定义日期时间日历;分页列表支持页码跳转输入
- **Stream Check 错误分类**:错误按类别分色 toast;默认探测模型刷新;单独识别 "model not found"
- **Local Routing 激活时阻止官方供应商切换**:官方流量走本地代理有账号暂停风险,强制拦截并 toast 警告
- **Pricing 数据库刷新(v8 → v9)**:新增 ~50 条模型条目并修正陈旧价格
- **应用级窗口控件**:可选让 CC Switch 自绘 min/max/close,显著改善 Linux Wayland 体验
- **Hermes 接入统一 Skills 管理**Skills 安装 / 启用 / 过滤现覆盖 Hermes
- **Hermes / OpenClaw 配置目录自定义**:在设置里指定 `~/.hermes/config.yaml``openclaw.json` 的自定义位置
- **从工具栏启动 Hermes Dashboard**Web UI 探测失败时,点击可在用户首选终端中启动 `hermes dashboard`
- **新合作伙伴预设**:LemonData 覆盖全部 6 个应用;DDSHub 新增 Codex 端点;StepFun Step Plan
---
## 新功能
### Hermes Agent 支持(第 6 个受管应用)
CC Switch 首次支持 Hermes Agent 作为一等受管应用,与 Claude / Codex / Gemini / OpenCode / OpenClaw 并列。
- **数据库迁移 v9 → v10**:为 `mcp_servers``skills` 表新增 `enabled_hermes` 列(`DEFAULT 0` 自动迁移,无数据丢失)
- **YAML 配置读写**`~/.hermes/config.yaml` 读写带原子备份;`tests/hermes_roundtrip.rs` 守护不损坏不相关键和 OAuth MCP `auth`
- **四种 API 协议**:与 Hermes Agent 0.10.0 对齐的 `chat_completions` / `anthropic_messages` / `codex_responses` / `bedrock_converse`;新 deeplink 默认为 `chat_completions`
- **用户 `providers:` dict 只读呈现**:用户在 YAML 里手写的 providers 条目在 CC Switch 中以只读卡片展示,深度配置跳转到 Hermes Web UI
- **累加式切换**:与 Claude / Codex 的"覆盖式"切换不同,Hermes 所有供应商共存于同一 YAML
### Hermes Memory 面板
- 新增 Memory 面板直接编辑 `MEMORY.md` / `USER.md`,带启用开关、字符数限制和保存流
- 替换 Hermes 的 Prompts 入口
### Hermes 供应商预设(约 50 个)
- 覆盖 Nous Research、胜算云、OpenRouter、DeepSeek、Together AI、StepFun、智谱 GLM、百炼、Kimi、MiniMax、豆包、百灵、魔搭、KAT-Coder、PackyCode、Cubence、AIGoCode、RightCode、AICodeMirror、AICoding、CrazyRouter、SSSAiCode、Micu、CTok.ai、DDSHub、E-FlowCode、LionCCAPI、PIPELLM、Compshare、SiliconFlow、AiHubMix、DMXAPI、TheRouter、Novita、Nvidia、小米 MiMo
### 从工具栏启动 Hermes Dashboard
- Hermes Web UI 探测失败时,工具栏按钮改为弹出确认框,提供在用户首选终端里运行 `hermes dashboard`
- 通过临时 bash / batch 脚本启动,`hermes dashboard` 就绪后自动打开浏览器,无需轮询
- Memory 面板和 Health banner 保留原有 toast 行为
- 顺便修正了离线 toast 里过时的 `hermes web` 提示(正确命令是 `hermes dashboard`
- Linux 终端探测改为先 `which` 后 stat,提升兼容性
### Claude Opus 4.7 支持
- 新增 Claude Opus 4.7 及其自适应思维白名单、按百万 token 定价种子、Bedrock SKU`anthropic.claude-opus-4-7` / `global.anthropic.claude-opus-4-7`,丢弃老 `-v1` 后缀)
- 全部聚合器 / Bedrock 预设升级为默认 Opus 模型
### Claude `max` 推理力度
- Claude 推理下拉从 `high` 升级到 `max`,解锁更强的思考容量
### Gemini Native API 代理
- 新增 `api_format = "gemini_native"`,代理可直接转发到 Google `generateContent` API (#1918, 感谢 @yovinchen)
- 完整支持流式、schema 转换、shadow 请求
- 在 proxy providers 模块下新增 `gemini_url.rs``gemini_schema.rs``gemini_shadow.rs``streaming_gemini.rs``transform_gemini.rs`
### GitHub Copilot 企业版(GHES
- 为 Copilot 型 Claude 供应商新增 GHES 认证与端点配置 (#2175, 感谢 @hotelbe)
### 会话列表虚拟化
- 通过 `@tanstack/react-virtual` 虚拟化会话列表,上千条记录流畅滚动
- 长会话消息默认折叠,减少文字布局开销
### Codex / OpenClaw 会话标题提取
- Codex 和 OpenClaw 会话自动抽取有意义的标题,两行显示
- 剥离 OpenClaw `message_id` 后缀噪声
### Usage 日期范围选择器
- Usage 面板新增日期范围选择器,预设 TabToday / 1d / 7d / 14d / 30d+ 自定义日期 + 时间日历 (#2002, 感谢 @yovinchen)
- 分页列表新增页码跳转输入
### 模型映射快速填入
- 供应商表单的模型映射字段旁新增快速填入按钮,加快编辑 (#2179, 感谢 @lispking)
### Stream Check 错误分类
- 按类别为 Stream Check 错误上色并以 toast 呈现
- 刷新所有厂商默认探测模型到当前主力机型
- 对 "model not found" 响应做单独识别
### Local Routing 激活时阻止官方供应商切换
- 在 Local Routing 激活状态下,切换到官方供应商会被强制拦截并弹出警告 toast
- 原因:官方 API 流量经由本地代理存在账号暂停风险
### Pricing 数据库刷新(v8 → v9
- 迁移时重新种入定价表
- 新增约 50 条模型条目,覆盖 Claude 4.7、Opus 4.7 Adaptive Thinking、Grok 4、Qwen 3.5/3.6、MiniMax M2.5/M2.7、Doubao Seed 2.0 系列、GLM-5/5.1
- 修正 DeepSeek、Kimi K2.5 等陈旧价格
### 应用级窗口控件
- 新增可选设置,让 CC Switch 自绘最小化 / 切换最大化 / 关闭按钮,代替系统装饰 (#1119, 感谢 @git1677967754)
- 在合成器按钮可能失灵的 Linux Wayland 上显著改善体验
### Hermes 接入统一 Skills 管理
- 统一的 Skills 界面新增 Hermes
- Skills 安装 / 启用 / 过滤现覆盖 Hermes,与 Claude / Codex / Gemini / OpenCode / OpenClaw 并列
### OpenClaw 配置目录自定义
- 新增设置项,允许把 CC Switch 指向自定义的 `openclaw.json` 位置 (#1518, 感谢 @mrFranklin)
### Hermes 配置目录自定义
- 新增设置项,允许把 CC Switch 指向自定义的 `~/.hermes/config.yaml` 位置,底层通过数据驱动 dispatch
### StepFun Step Plan 预设
- 新增 StepFun Step PlanEN / ZH)供应商预设 (#2155, 感谢 @hengm3467)
### New API 用量脚本模板
- 为 New API 用量脚本模板新增 User-Agent 头,提升上游兼容性
### LemonData 全应用预设
- LemonData 作为第三方合作伙伴预设覆盖 Claude / Codex / Gemini / OpenCode / OpenClaw / Hermes 全部 6 个应用
- 含图标资源和 zh / en / ja 三语合作伙伴推广文案
- Claude 预设使用 `ANTHROPIC_API_KEY` 认证,OpenAI 兼容应用目标为 `gpt-5.4`
### DDSHub Codex 预设
- 新增 DDSHub 的 Codex 兼容端点(与 Claude 服务同 host
- base URL 省略 `/v1` 后缀,由网关自动路由 OpenAI SDK 路径
---
## 变更
### "Local Proxy Takeover" → "Local Routing"
- 三语 UI 文案、README、文档中全部统一重命名
- 功能行为保持不变
### Hermes `Auto` api_mode 移除
- 用户必须显式选择协议;新 deeplink 默认为 `chat_completions`
- 消除了基于 URL 的启发式识别带来的意外
### Hermes 供应商表单
- 新增 API mode 下拉和按供应商的模型编辑器
- 切换激活供应商时,把按供应商的模型绑定到顶层 `model:`
### Hermes 深度配置委托
- 深度 YAML 配置不再在 CC Switch 表单里重复,直接通过"启动 Hermes Web UI"按钮交给 Web UI
### Hermes 工具栏布局
- Web UI 按钮图标从 `ExternalLink` 换成 `LayoutDashboard` —— 点击可能启动 `hermes dashboard` 而非仅仅打开 URL,面板式图标语义更准
- MCP 移到工具栏末尾,与 Claude / Codex / Gemini / OpenCode 的布局对齐
### Claude Quick-Set 移除 `ANTHROPIC_REASONING_MODEL`
- 把推理能力和模型选择解耦,quick-set 表单不再暴露该遗留字段
### 按供应商代理配置移除
- 统一到全局的 Local Routing
- 按供应商的代理开关和存储都已移除
### 统一工具栏图标按钮宽度
- 在 Claude / Codex / Gemini / OpenCode / OpenClaw / Hermes 面板之间规格化图标按钮宽度,表头视觉一致
### Rust Toolchain 锁定 1.95
- 全仓库采纳 clippy 1.95 建议并锁定 toolchain,防止 nightly 漂移
### 托盘菜单 ID 常量
- 托盘标识符从硬编码字符串 `"main"` 改为 `TRAY_ID` 常量(`"cc-switch"`),所有调用点同步 (#1978, 感谢 @lidaxian121)
### Copilot 次数消耗深度优化
一次系统性优化专门降低 Copilot 反向代理的 premium 交互消耗,涵盖以下多项改进:
- **转发前主动剥离 thinking 块**Anthropic 的 `thinking` / `redacted_thinking` 块会被 OpenAI 兼容端点拒绝,过去一次请求先失败消耗一次 premium 交互、再由 `thinking_rectifier` 触发重试。新增主动剥离步骤(Copilot 优化管线第 3.5 步,位于 `tool_result` 合并之后),直接省掉那一次无谓的 premium 消耗
- **请求分类修正**:含 `tool_result` 的消息归类为代理继续,而不是用户发起的新请求 —— 避免每次工具调用都被错误计入 premium 次数
- **subagent 检测**:通过 `__SUBAGENT_MARKER__``metadata._agent_` 回退识别 subagent,设置 `x-interaction-type=conversation-subagent`
- **确定性 `x-interaction-id` 合并计费**:从 session ID 推导 `x-interaction-id`,把同一会话内的多次请求合并为一次计费交互
- **Orphan `tool_result` 清理**:清理孤立的 `tool_result`,避免触发上游错误导致重试和重复计费
- **Warmup 降级默认开启**:使用 `gpt-5-mini` 作为默认降级模型
- **优化管线重排**classify → sanitize → merge → warmup,让分类看到原始 `tool_result` 语义
- 修复 `CopilotOptimizerConfig` 默认值不一致(统一到 `gpt-5-mini`
### 用量脚本内网支持
- 移除 usage script 的私网 IP / 可疑主机名屏蔽,解锁企业内网、Docker、自建 API 端点
- 内置模板仍强制 HTTPS(localhost 除外)和同源检查;自定义模板仍由用户控制,这类请求 URL 检查跳过
### Failover 队列备注
- 供应商备注现在在 failover 队列选择器和队列行中显示,方便在多供应商队列里识别 (#2138, 感谢 @Coconut-Fish)
---
## Bug 修复
### 工具栏最大化后持续折叠
- 窗口最大化 / 还原后,工具栏不再卡在折叠状态;折叠判定会随尺寸变化重新计算
### Hermes YAML 污染与 OAuth MCP `auth` 丢失
- 经 CC Switch 往返写入不再丢失 OAuth MCP `auth` 块、也不污染不相关的 YAML 键
- 新增 `tests/hermes_roundtrip.rs` 作为守护测试
### Hermes 激活供应商展示
- Hermes UI 现在正确展示激活供应商,并连通添加 / 启用 / 移除动作
### Hermes 供应商持久化
- 供应商持久化到 `custom_providers:` 下,`api_mode``model` 可跨重启 / 配置重载存活
### Hermes 健康检查错借 OpenClaw schema
- 以前 Hermes 供应商被路由到 `check_additive_app_stream`(OpenClaw 的调度器),后者读 camelCase 的 `baseUrl` / `apiKey` / `api`,导致即便 Hermes 字段全填还是报 "OpenClaw provider is missing baseUrl"
- 新增 `check_hermes_stream`,用 Hermes 专用提取器把 `api_mode``chat_completions` / `anthropic_messages` / `codex_responses`)映射到对应的 `check_claude_stream` `api_format``bedrock_converse` 明确标记为不支持
- 先解析 `api_mode` 再抽 URL / API key,让 `bedrock_converse` 用户看到真实原因,而不是误导性的 "missing base_url"
### Usage 查询模态框支持 Hermes / OpenClaw
- `getProviderCredentials` 新增对 Hermessnake_case `base_url` / `api_key`)和 OpenClawcamelCase `baseUrl` / `apiKey`)的扁平 `settingsConfig` 字段读取,让 SiliconFlow 等匹配供应商自动选中 "official balance" 模板
- 重构 BALANCE 和 TOKEN_PLAN 测试路径复用 `providerCredentials`,不再直接读 `env.ANTHROPIC_*`,修正了非 Claude 应用即使配置了 key 也报 "empty key" 的问题
### Codex `cache_control` 保留
- 在 Codex 格式转换合并 system prompt 时保留 `cache_control` (#1946, 感谢 @yovinchen)
### Claude prompt cache key 泄漏
- Claude chat 转换时不再发送 prompt cache key (#2003, 感谢 @yovinchen)
### 代理逐跳响应头剥离
- 按 RFC 7230 剥离代理响应的 hop-by-hop 头(Connection、Keep-Alive、Transfer-Encoding 等) (#2060, 感谢 @yovinchen)
### 代理 CORS 层移除
- 移除代理中过于宽松的 CORS 层 (#1915, 感谢 @zerone0x)
### 代理 toast 显示后端错误详情
- 代理相关 toast 现在展示后端错误 payload 的详情,而不是一句笼统的失败
### Usage 日志去重
- 代理和会话日志的用量记录去重,相同请求不再被重复计数
- 请求日志时间范围与面板的 1d / 7d / 30d 选择器同步
### Common Config 勾选持久化
- Claude / Codex / Gemini common-config 勾选状态重开后正确保留 (#2191, 感谢 @zxZeng)
### Claude 插件 `settings.json` 同步
- 编辑当前供应商时,会同步回 Claude 插件路径下的 `settings.json` (#1905, 感谢 @chengww5217)
### Google Official Gemini env 保留
- 保存 Google Official Gemini 供应商时不再清空 `env`
### OpenCode JSON5 尾逗号解析
- OpenCode 配置读取容忍尾逗号(JSON5) (#2023, 感谢 @wwminger)
### 预设刷新
- 刷新 DeepSeek 和 Claude 1M 的陈旧 context 窗口
- 刷新陈旧模型 ID,回填 Hermes 模型列表
- 修正 Nous 端点,Hermes 占位图替换为 Nous 品牌图
- 移除未使用的官方 Hermes 预设
### 搜索命中时折叠消息自动展开
- 搜索匹配落在折叠内容内部时,消息自动展开以定位匹配
### 未知订阅配额等级隐藏
- 供应商卡片不再渲染未知订阅配额等级
### weekly_limit 标签统一
- 跨语言把 `weekly_limit` 等级标签对齐到官方的"7 天"命名
### 根级 Skill 仓库安装
- 修复当仓库根本身就是一个 skill 时的安装失败
### Session ID 解析 clippy
- 移除 session ID 解析里的冗余闭包(clippy 警告)
### Stream Check 默认探测模型刷新
- 默认探测模型更新到每家厂商当前主力
### Skills 导入同步
- 导入的 Skills 即时同步到启用应用目录,不再仅记录在数据库里导致 UI 显示"已安装"但目标目录空缺 (#2101, 感谢 @yaoguohh)
### Ghostty 会话恢复
- 改为通过 shell 执行 + `--working-directory` 启动 Ghostty 会话恢复 (#1976, 感谢 @Suda202)
- 避免路径含空格 / 特殊字符时 `cwd` 转义问题
---
## 文档
### README 赞助商更新
- SiliconFlow 注册赠送更新为 ¥16
- 精简 SSSAiCode 赞助文案
- 更新合作伙伴 logo
- 新增 LemonData 赞助商
### 全局代理提示澄清
- 三语澄清全局代理与 Local Routing 的关系
### Takeover → Routing 文档重命名
- 接管相关文档在三语下重命名为 routing,同步更新锚点
### PIPELLM 网站 URL
- PIPELLM 赞助商网站 URL 更新为 `code.pipellm.ai`
---
## ⚠️ 重要变更(Breaking
### Hermes 必须显式 `api_mode`
- `Auto` 模式移除;导入或 deeplink 得到的供应商默认落到 `chat_completions`
- 既有 `Auto` 配置的用户会被提示选择协议
### Claude Quick-Set 移除 `ANTHROPIC_REASONING_MODEL`
- 该遗留字段不再暴露;既有设置自动清理
### 按供应商代理配置移除
- 迁移到全局 Local Routing 设置
- 既有按供应商代理值被忽略
### 数据库 schema v9 → v10
-`mcp_servers``skills` 表新增 `enabled_hermes`
- 自动迁移,`DEFAULT 0`,无数据丢失
### Pricing 表 v8 → v9 重置
- 首次启动时 `model_pricing` 表被清空并重新种入,以应用新模型和修正后的价格
### XCodeAPI 预设移除
- 使用 XCodeAPI 预设的用户请迁移到其它供应商
---
## ⚠️ 风险提示
本版本在涉及反向代理类功能上沿用 v3.12.3 / v3.13.0 提出的风险提示。
**GitHub Copilot 反向代理**:使用 Copilot 的反代路径可能违反 GitHub / Microsoft 服务条款。详情见 [v3.12.3 release notes](v3.12.3-zh.md#-风险提示)。
**Codex OAuth 反向代理**:使用 ChatGPT 订阅的 Codex OAuth 反代可能违反 OpenAI 服务条款,详情见 [v3.13.0 release notes](v3.13.0-zh.md#-风险提示)。
用户启用上述功能即表示**自行承担所有风险**。CC Switch 不对因使用这些功能而导致的任何账号限制、警告或服务暂停承担责任。
---
## 下载与安装
访问 [Releases](https://github.com/farion1231/cc-switch/releases/latest) 下载对应版本。
### 系统要求
| 系统 | 最低版本 | 架构 |
| ------- | -------------------------- | ----------------------------------- |
| Windows | Windows 10 及以上 | x64 |
| macOS | macOS 12 (Monterey) 及以上 | Intel (x64) / Apple Silicon (arm64) |
| Linux | 见下表 | x64 |
### Windows
| 文件 | 说明 |
| ---------------------------------------- | ----------------------------------- |
| `CC-Switch-v3.14.0-Windows.msi` | **推荐** - MSI 安装包,支持自动更新 |
| `CC-Switch-v3.14.0-Windows-Portable.zip` | 便携版,解压即用,不写入注册表 |
### macOS
| 文件 | 说明 |
| -------------------------------- | --------------------------------------------- |
| `CC-Switch-v3.14.0-macOS.dmg` | **推荐** - DMG 安装包,拖入 Applications 即可 |
| `CC-Switch-v3.14.0-macOS.zip` | 解压后拖入 ApplicationsUniversal Binary |
| `CC-Switch-v3.14.0-macOS.tar.gz` | 用于 Homebrew 安装和自动更新 |
> macOS 版本已通过 Apple 代码签名和公证,可直接安装使用。
### HomebrewmacOS
```bash
brew tap farion1231/ccswitch
brew install --cask cc-switch
```
更新:
```bash
brew upgrade --cask cc-switch
```
### Linux
| 发行版 | 推荐格式 | 安装方式 |
| --------------------------------------- | ----------- | ---------------------------------------------------------------------- |
| Ubuntu / Debian / Linux Mint / Pop!\_OS | `.deb` | `sudo dpkg -i CC-Switch-*.deb``sudo apt install ./CC-Switch-*.deb` |
| Fedora / RHEL / CentOS / Rocky Linux | `.rpm` | `sudo rpm -i CC-Switch-*.rpm``sudo dnf install ./CC-Switch-*.rpm` |
| openSUSE | `.rpm` | `sudo zypper install ./CC-Switch-*.rpm` |
| Arch Linux / Manjaro | `.AppImage` | 添加执行权限后直接运行,或使用 AUR |
| 其他发行版 / 不确定 | `.AppImage` | `chmod +x CC-Switch-*.AppImage && ./CC-Switch-*.AppImage` |
-185
View File
@@ -1,185 +0,0 @@
# CC Switch v3.14.1
> Tray usage visibility, Codex OAuth stability fixes, Skills import/install reliability, and removal of the Hermes config health scanner
**[中文版 →](v3.14.1-zh.md) | [日本語版 →](v3.14.1-ja.md)**
---
## Overview
CC Switch v3.14.1 is a patch release following v3.14.0, focused on **Codex OAuth reverse-proxy stability**, **tray usage visibility**, **Skills import / install reliability**, **Gemini session restore paths**, and **simplifying Hermes configuration health handling**.
For the first time, the system tray surfaces **cached usage** for the current Claude / Codex / Gemini provider directly in its submenus — including subscription summaries and usage-script summaries with color-coded utilization markers. For Chinese coding-plan providers like Kimi / Zhipu / MiniMax, the tray additionally renders a **5-hour + weekly window** layout in the `🟢 h12% w80%` style (worst utilization drives the emoji), semantically identical to the official subscription badges. Creating a Claude provider whose `ANTHROPIC_BASE_URL` matches a known coding-plan host now auto-injects `meta.usage_script` so the tray lights up without opening the Usage Script modal.
Several Codex OAuth reverse-proxy stability issues are addressed this release: client-provided session IDs are now used as both `prompt_cache_key` and the Codex session header to avoid UUID-driven cache churn; non-streaming Anthropic clients receive proper JSON responses even when the ChatGPT Codex upstream forces OpenAI Responses SSE; and Stream Check now builds probes with the same `store: false`, encrypted reasoning include, and provider FAST mode setting as production requests, eliminating the "check fails but it actually works" mismatch. Paired with a new explicit **FAST mode toggle**, users can now opt into `service_tier="priority"` on Codex OAuth-backed Claude providers, trading latency against ChatGPT quota consumption on their own terms.
Additionally, the in-app **Hermes config health scanner** and its warning banner are removed (along with the `scan_hermes_config_health` command, `HermesHealthWarning` type, and `HermesWriteOutcome.warnings` payload), refocusing the Hermes surface on active provider display, switching defaults, memory editing, and launching the Hermes Web UI — deep configuration health is now Hermes's own responsibility.
**Release Date**: 2026-04-23
**Update Scale**: 13 commits | 48 files changed | +1,883 / -808 lines
---
## Highlights
- **Tray Usage Visibility**: Claude / Codex / Gemini tray submenus show cached usage for the current provider, including subscription and script-based summaries with color markers; refreshes are throttled, limited to visible apps, and synchronized back into React Query (#2184, thanks @TuYv)
- **Tray Coding-Plan Usage (Kimi / Zhipu / MiniMax)**: The tray renders 5-hour + weekly window usage using the `🟢 h12% w80%` layout; Claude providers whose base URL matches a known host auto-inject `meta.usage_script`
- **Codex OAuth FAST Mode**: New explicit FAST mode toggle for Codex OAuth-backed Claude providers; when enabled, converted Responses requests send `service_tier="priority"`. Off by default (#2210, thanks @JesusDR01)
- **Codex OAuth Stability**: Fixed reverse-proxy cache routing (#2218, thanks @majiayu000), Responses SSE aggregation (#2235, thanks @xpfo-go), and Stream Check parity with production (#2210, thanks @JesusDR01)
- **Hermes Config Health Scanner Removed**: Refocuses the Hermes surface on provider management, memory editing, and launching the Web UI — no longer duplicates deep configuration health judgments
- **Skills Import / Install Reliability**: Import dialog disables actions while pending and deduplicates results by ID (#2211, thanks @TuYv); model quick-set / one-click config applies against the latest form state (#2249, thanks @Coconut-Fish); root-level `SKILL.md` repo installs are stable (#2231, thanks @santugege)
- **Gemini Session Restore Paths**: Session scanning reads `.project_root` metadata and passes the original project directory back into restore flows (#2240, thanks @tisonkun)
- **Session / Settings Layout Polish**: Hardened the scroll-area viewport with width containment to fix horizontal overflow; tightened app bottom and settings footer spacing (#2201, thanks @Coconut-Fish)
---
## Added
### Tray Usage Visibility
- System tray submenus now show **cached usage** for the current Claude / Codex / Gemini provider (#2184, thanks @TuYv)
- Includes subscription quota summaries and usage-script summaries with color-coded utilization markers
- Tray-triggered refreshes are **throttled**, **limited to visible apps**, and synchronized back into React Query so the main window and tray share the same usage data
### Tray Coding-Plan Usage (Kimi / Zhipu / MiniMax)
- The tray renders **5-hour + weekly window** usage for Chinese coding-plan providers
- Uses the same `🟢 h12% w80%` two-window layout as official subscription badges (worst utilization drives the emoji color)
- Creating a Claude provider whose `ANTHROPIC_BASE_URL` matches a known coding-plan host **auto-injects** `meta.usage_script`, so the tray lights up without opening the Usage Script modal
- Existing `usage_script` values are **preserved on update**, never clobbering user customizations
### Codex OAuth FAST Mode
- New explicit FAST mode toggle for Codex OAuth-backed Claude providers (#2210, thanks @JesusDR01)
- When enabled, converted Responses requests send `service_tier="priority"` for lower latency
- Off by default to avoid unexpectedly increasing ChatGPT quota consumption
---
## Changed
### Session and Settings Layout Polish
- Hardened the scroll-area viewport with width containment to fix horizontal overflow (#2201, thanks @Coconut-Fish)
- Tightened app bottom and settings footer spacing so long session / settings views fit more cleanly
---
## Removed
### Hermes Config Health Scanner
- Removed the in-app Hermes config health scanner and its warning banner
- Removed the `scan_hermes_config_health` command, `HermesHealthWarning` type, and `HermesWriteOutcome.warnings` payload
- The CC Switch Hermes surface now focuses on its core job: active provider display, default provider switching, memory editing, and launching the Hermes Web UI for deep configuration
---
## Fixed
### Codex OAuth Cache Routing
- Use the client-provided session ID as both `prompt_cache_key` and the Codex session header, preserving explicit cache keys (#2218, thanks @majiayu000)
- Stop generating UUIDs that caused cache-identity churn, stabilizing the ChatGPT Codex reverse-proxy cache identity
### Codex OAuth Responses SSE Aggregation
- Non-streaming Anthropic clients now receive proper JSON even when the ChatGPT Codex upstream forces OpenAI Responses SSE (#2235, thanks @xpfo-go)
- CC Switch aggregates the upstream SSE events before running the non-streaming transform
### Codex OAuth Stream Check Parity
- Stream Check now builds Codex OAuth probe requests with the same `store: false`, encrypted reasoning include, and provider FAST mode setting as production proxy traffic (#2210, thanks @JesusDR01)
- Eliminates the "check fails but it actually works" mismatch
### Codex Model Extraction
- Reading the `model` field from Codex config now uses TOML parsing instead of first-line regex matching (#2227, thanks @nmsn)
- Multiline TOML is handled correctly
### Model Quick-Set / One-Click Config
- Model quick-set now applies against the **latest** provider form config (#2249, thanks @Coconut-Fish)
- Fixes stale form state preventing one-click configuration from succeeding
### Skills Import Duplicates
- The Skills import dialog disables actions while import is pending (#2211, thanks @TuYv)
- The installed-skills cache deduplicates imported results by ID, preventing double-clicks from adding duplicate installed entries (#2139)
### Root-Level Skill Repos
- Skill install and update flows now consistently resolve three source patterns: direct nested paths, install-name recursive search, and repository-root `SKILL.md` sources (#2231, thanks @santugege)
### Gemini Session Restore Paths
- Gemini session scanning now reads `.project_root` metadata (#2240, thanks @tisonkun)
- Restore flows can pass the original project directory when available
### Provider Hover Names
- Provider icons now expose the provider name on hover for inline SVG, image URL, and fallback initials render paths (#2237, thanks @tisonkun)
---
## Notes & Caveats
- **Hermes Health Scanner Removed**: If you were relying on CC Switch to surface deep Hermes YAML configuration issues, switch to the "Launch Hermes Web UI" toolbar button and inspect them in Hermes's own panel. Day-to-day provider management, switching, memory editing, and MCP / Skills sync continue to be handled by CC Switch.
- **Codex OAuth FAST Mode Off by Default**: Only turn it on if you accept potentially increased ChatGPT quota consumption in exchange for lower latency.
- **Tray Cached Usage**: Refreshes are throttled and limited to the currently visible app to avoid unnecessary upstream API calls; values are synchronized into React Query so the main window and tray stay in sync.
---
## Download & Installation
Visit [Releases](https://github.com/farion1231/cc-switch/releases/latest) to download the appropriate version.
### System Requirements
| OS | Minimum Version | Architecture |
| ------- | ---------------------------- | ----------------------------------- |
| Windows | Windows 10 or later | x64 |
| macOS | macOS 12 (Monterey) or later | Intel (x64) / Apple Silicon (arm64) |
| Linux | See table below | x64 |
### Windows
| File | Description |
| ---------------------------------------- | ----------------------------------------------------- |
| `CC-Switch-v3.14.1-Windows.msi` | **Recommended** - MSI installer, supports auto-update |
| `CC-Switch-v3.14.1-Windows-Portable.zip` | Portable, extract and run, no registry writes |
### macOS
| File | Description |
| -------------------------------- | ------------------------------------------------------- |
| `CC-Switch-v3.14.1-macOS.dmg` | **Recommended** - DMG installer, drag into Applications |
| `CC-Switch-v3.14.1-macOS.zip` | Extract and drag into Applications, Universal Binary |
| `CC-Switch-v3.14.1-macOS.tar.gz` | For Homebrew installation and auto-update |
> macOS builds are Apple code-signed and notarized — install directly.
### Homebrew (macOS)
```bash
brew tap farion1231/ccswitch
brew install --cask cc-switch
```
Update:
```bash
brew upgrade --cask cc-switch
```
### Linux
| Distribution | Recommended | Installation |
| --------------------------------------- | ----------- | ---------------------------------------------------------------------- |
| Ubuntu / Debian / Linux Mint / Pop!\_OS | `.deb` | `sudo dpkg -i CC-Switch-*.deb` or `sudo apt install ./CC-Switch-*.deb` |
| Fedora / RHEL / CentOS / Rocky Linux | `.rpm` | `sudo rpm -i CC-Switch-*.rpm` or `sudo dnf install ./CC-Switch-*.rpm` |
| openSUSE | `.rpm` | `sudo zypper install ./CC-Switch-*.rpm` |
| Arch Linux / Manjaro | `.AppImage` | Add execute permission and run, or use AUR |
| Other distros / not sure | `.AppImage` | `chmod +x CC-Switch-*.AppImage && ./CC-Switch-*.AppImage` |
-185
View File
@@ -1,185 +0,0 @@
# CC Switch v3.14.1
> トレイでの用量可視化、Codex OAuth の複数の安定性修正、Skills インポート/インストールの信頼性向上、Hermes 設定ヘルススキャナーの削除
**[中文版 →](v3.14.1-zh.md) | [English →](v3.14.1-en.md)**
---
## 概要
CC Switch v3.14.1 は v3.14.0 に続くパッチリリースで、**Codex OAuth リバースプロキシの安定性**、**トレイでの用量可視化**、**Skills インポート / インストールの信頼性**、**Gemini セッション復元パス**、および **Hermes 設定ヘルス処理の簡素化**を中心に据えています。
システムトレイは初めて、現在の Claude / Codex / Gemini プロバイダーの**キャッシュ済み用量**をサブメニューに直接表示するようになりました — サブスクリプション要約と用量スクリプト要約を、使用率に応じた色分けマーカーとともに表示します。Kimi / Zhipu / MiniMax のような中国系コーディングプランプロバイダーには、公式サブスクリプションバッジと同じ `🟢 h12% w80%` スタイルで **5 時間 + 週次ウィンドウ**の 2 ウィンドウレイアウトを追加描画します(より厳しい方の使用率が絵文字色を決定)。`ANTHROPIC_BASE_URL` が既知のコーディングプランホストに一致する Claude プロバイダーを作成すると、`meta.usage_script` が自動注入されるため、Usage Script モーダルを開かなくてもトレイが点灯します。
Codex OAuth 側では、複数のリバースプロキシ安定性の問題を修正しました: クライアント提供の session ID を `prompt_cache_key` と Codex session ヘッダーの両方に使用し、UUID 生成によるキャッシュ揺らぎを回避。ChatGPT Codex 上流が OpenAI Responses SSE を強制する場合でも、非ストリーミングの Anthropic クライアントが適切な JSON レスポンスを受け取れるようになりました。Stream Check は、本番環境と同じ `store: false`、暗号化 reasoning include、およびプロバイダーの FAST モード設定でプローブを構築するようになり、「検出は失敗するのに実際は動く」というズレが解消されました。新しい明示的な **FAST モードトグル**と組み合わせることで、ユーザーは Codex OAuth バックの Claude プロバイダーで `service_tier="priority"` を選択的に送信でき、レイテンシと ChatGPT 配額消費の間で自分で選べるようになりました。
さらに、CC Switch 内蔵の **Hermes 設定ヘルススキャナー**と警告バナー(および対応する `scan_hermes_config_health` コマンド、`HermesHealthWarning` 型、`HermesWriteOutcome.warnings` ペイロード)を削除し、Hermes サーフェスをアクティブプロバイダー表示、デフォルト切り替え、Memory 編集、および Hermes Web UI の起動に再フォーカスしました — 深い設定ヘルスは Hermes 自身の責任になります。
**リリース日**: 2026-04-23
**更新規模**: 13 commits | 48 files changed | +1,883 / -808 lines
---
## ハイライト
- **トレイでの用量可視化**: Claude / Codex / Gemini のトレイサブメニューに、現在のプロバイダーのキャッシュ済み用量(サブスクリプション要約とスクリプト要約、色分けマーカー付き)を表示。リフレッシュはスロットル、可視アプリに限定、React Query に同期 (#2184, 感謝 @TuYv)
- **トレイのコーディングプラン用量(Kimi / Zhipu / MiniMax**: トレイが 5 時間 + 週次ウィンドウの用量を `🟢 h12% w80%` レイアウトで描画。既知のホストにマッチする Claude プロバイダーは `meta.usage_script` を自動注入
- **Codex OAuth FAST モード**: Codex OAuth バックの Claude プロバイダーに明示的な FAST モードトグルを追加。有効時は変換された Responses リクエストに `service_tier="priority"` を送信、デフォルトは OFF (#2210, 感謝 @JesusDR01)
- **Codex OAuth 安定性**: リバースプロキシのキャッシュルーティング (#2218, 感謝 @majiayu000)、Responses SSE 集約 (#2235, 感謝 @xpfo-go)、Stream Check と本番の一致性 (#2210, 感謝 @JesusDR01) を修正
- **Hermes 設定ヘルススキャナー削除**: Hermes サーフェスをプロバイダー管理、Memory 編集、Web UI 起動に再フォーカス。深い設定ヘルス判定を重複して担わなくなる
- **Skills インポート / インストールの信頼性**: インポート中はダイアログのアクションを無効化し、結果を ID で重複排除 (#2211, 感謝 @TuYv); ワンクリック設定は最新のフォーム状態に基づいて適用 (#2249, 感謝 @Coconut-Fish); ルートレベルの `SKILL.md` リポジトリインストールが安定 (#2231, 感謝 @santugege)
- **Gemini セッション復元パス**: セッションスキャン時に `.project_root` メタデータを読み、元のプロジェクトディレクトリを復元フローに渡す (#2240, 感謝 @tisonkun)
- **セッション / 設定レイアウトの磨き込み**: スクロールエリアビューポートに幅制約を追加して横方向のはみ出しを修正。アプリ下部と設定フッター間隔をよりタイトに (#2201, 感謝 @Coconut-Fish)
---
## 新機能
### トレイでの用量可視化
- システムトレイサブメニューに、現在の Claude / Codex / Gemini プロバイダーの**キャッシュ済み用量**を表示 (#2184, 感謝 @TuYv)
- サブスクリプション配額要約と用量スクリプト要約を含み、使用率に応じた色分けマーカー付き
- トレイ起因のリフレッシュは**スロットル**、**可視アプリに限定**、React Query に同期されるため、メインウィンドウとトレイが同じ用量データを共有
### トレイのコーディングプラン用量(Kimi / Zhipu / MiniMax
- 中国系コーディングプランプロバイダー向けに、トレイが **5 時間 + 週次ウィンドウ**の用量を描画
- 公式サブスクリプションバッジと同じ `🟢 h12% w80%` の 2 ウィンドウレイアウトを使用(より厳しい使用率が絵文字色を決定)
- `ANTHROPIC_BASE_URL` が既知のコーディングプランホストにマッチする Claude プロバイダーを作成すると、`meta.usage_script` が**自動注入**され、Usage Script モーダルを開かなくてもトレイが点灯
- 更新時は既存の `usage_script` 値を**保持**し、ユーザーカスタマイズを上書きしない
### Codex OAuth FAST モード
- Codex OAuth バックの Claude プロバイダーに明示的な FAST モードトグルを追加 (#2210, 感謝 @JesusDR01)
- 有効時は変換された Responses リクエストに `service_tier="priority"` を送信してレイテンシを低減
- 予期せぬ ChatGPT 配額消費の増加を避けるため、デフォルトは OFF
---
## 変更
### セッション・設定レイアウトの磨き込み
- スクロールエリアビューポートに幅制約を追加して横方向のはみ出しを修正 (#2201, 感謝 @Coconut-Fish)
- アプリ下部と設定フッター間隔をよりタイトにし、長いセッション / 設定ビューをすっきり表示
---
## 削除
### Hermes 設定ヘルススキャナー
- アプリ内の Hermes 設定ヘルススキャナーと警告バナーを削除
- `scan_hermes_config_health` コマンド、`HermesHealthWarning` 型、`HermesWriteOutcome.warnings` ペイロードを削除
- CC Switch の Hermes サーフェスは本来の役割に回帰: アクティブプロバイダー表示、デフォルトプロバイダー切り替え、Memory 編集、および深い設定用の Hermes Web UI 起動
---
## バグ修正
### Codex OAuth キャッシュルーティング
- クライアント提供の session ID を `prompt_cache_key` と Codex session ヘッダーの両方に使用し、明示的なキャッシュキーを保持 (#2218, 感謝 @majiayu000)
- キャッシュアイデンティティの揺らぎを引き起こしていた UUID 生成を停止し、ChatGPT Codex リバースプロキシのキャッシュアイデンティティを安定化
### Codex OAuth Responses SSE 集約
- ChatGPT Codex 上流が OpenAI Responses SSE を強制する場合でも、非ストリーミングの Anthropic クライアントが適切な JSON を受け取れるように修正 (#2235, 感謝 @xpfo-go)
- CC Switch が非ストリーミング変換を実行する前に上流 SSE イベントを集約
### Codex OAuth Stream Check の一致性
- Stream Check が構築する Codex OAuth プローブリクエストは、本番プロキシと同じ `store: false`、暗号化 reasoning include、プロバイダー FAST モード設定を使用するように修正 (#2210, 感謝 @JesusDR01)
- 「検出は失敗するのに実際は動く」ズレを解消
### Codex モデル抽出
- Codex 設定の `model` フィールドを読む際、先頭行の正規表現マッチではなく TOML パーサーを使用するように変更 (#2227, 感謝 @nmsn)
- 複数行 TOML も正しく処理
### モデルのクイック入力 / ワンクリック設定
- モデルクイック入力は**最新の**プロバイダーフォーム設定に対して適用されるように修正 (#2249, 感謝 @Coconut-Fish)
- 古いフォーム状態によってワンクリック設定が失敗する問題を修正
### Skills インポートの重複排除
- Skills インポートダイアログは、インポート中にすべてのアクションボタンを無効化 (#2211, 感謝 @TuYv)
- インストール済み Skills のキャッシュを ID で重複排除し、ダブルクリックによる重複したインストール済みエントリを防止 (#2139)
### ルートレベルの Skill リポジトリ
- Skill のインストールと更新フローが 3 つのソースパターンを一貫して解決: 直接ネストパス、install-name の再帰検索、およびリポジトリルートの `SKILL.md` ソース (#2231, 感謝 @santugege)
### Gemini セッション復元パス
- Gemini セッションスキャンが `.project_root` メタデータを読み取るように修正 (#2240, 感謝 @tisonkun)
- 復元フローは利用可能な場合に元のプロジェクトディレクトリを渡せる
### プロバイダー名のホバー表示
- プロバイダーアイコンは、inline SVG、画像 URL、およびフォールバックの頭文字レンダリングパスで、ホバー時にプロバイダー名を表示 (#2237, 感謝 @tisonkun)
---
## 備考・注意事項
- **Hermes ヘルススキャナー削除済み**: Hermes YAML の深い設定の問題提示を CC Switch に頼っていた場合は、ツールバーの「Hermes Web UI を起動」ボタンから Hermes 自身のパネルで確認してください。日常のプロバイダー管理、切り替え、Memory 編集、MCP / Skills 同期は引き続き CC Switch が担います。
- **Codex OAuth FAST モードはデフォルト OFF**: レイテンシ低減と引き換えに ChatGPT 配額消費が増える可能性を許容する場合にのみ有効化してください。
- **トレイのキャッシュ用量**: リフレッシュはスロットル済み、かつ現在可視のアプリに限定されており、不要な上流 API 呼び出しを回避します。値は React Query に同期されるため、メインウィンドウとトレイで同じ値が見えます。
---
## ダウンロード・インストール
[Releases](https://github.com/farion1231/cc-switch/releases/latest) から対応バージョンをダウンロードしてください。
### システム要件
| OS | 最小バージョン | アーキテクチャ |
| ------- | ------------------------ | ----------------------------------- |
| Windows | Windows 10 以降 | x64 |
| macOS | macOS 12 (Monterey) 以降 | Intel (x64) / Apple Silicon (arm64) |
| Linux | 下表参照 | x64 |
### Windows
| ファイル | 説明 |
| ---------------------------------------- | ------------------------------------------- |
| `CC-Switch-v3.14.1-Windows.msi` | **推奨** - MSI インストーラー、自動更新対応 |
| `CC-Switch-v3.14.1-Windows-Portable.zip` | ポータブル版、解凍して実行、レジストリ不要 |
### macOS
| ファイル | 説明 |
| -------------------------------- | ------------------------------------------------------ |
| `CC-Switch-v3.14.1-macOS.dmg` | **推奨** - DMG インストーラー、Applications にドラッグ |
| `CC-Switch-v3.14.1-macOS.zip` | 解凍して Applications にドラッグ、Universal Binary |
| `CC-Switch-v3.14.1-macOS.tar.gz` | Homebrew インストールと自動更新用 |
> macOS 版は Apple のコード署名および公証済みで、直接インストールして使用できます。
### HomebrewmacOS
```bash
brew tap farion1231/ccswitch
brew install --cask cc-switch
```
更新:
```bash
brew upgrade --cask cc-switch
```
### Linux
| ディストリビューション | 推奨形式 | インストール方法 |
| --------------------------------------- | ----------- | -------------------------------------------------------------------------- |
| Ubuntu / Debian / Linux Mint / Pop!\_OS | `.deb` | `sudo dpkg -i CC-Switch-*.deb` または `sudo apt install ./CC-Switch-*.deb` |
| Fedora / RHEL / CentOS / Rocky Linux | `.rpm` | `sudo rpm -i CC-Switch-*.rpm` または `sudo dnf install ./CC-Switch-*.rpm` |
| openSUSE | `.rpm` | `sudo zypper install ./CC-Switch-*.rpm` |
| Arch Linux / Manjaro | `.AppImage` | 実行権限を付与して実行、または AUR を使用 |
| その他のディストリビューション / 不明 | `.AppImage` | `chmod +x CC-Switch-*.AppImage && ./CC-Switch-*.AppImage` |
-185
View File
@@ -1,185 +0,0 @@
# CC Switch v3.14.1
> 托盘用量可见化、Codex OAuth 多项稳定性修复、Skills 导入/安装可靠性提升、Hermes 配置健康扫描器移除
**[English →](v3.14.1-en.md) | [日本語版 →](v3.14.1-ja.md)**
---
## 概览
CC Switch v3.14.1 是 v3.14.0 之后的一次补丁版本,围绕 **Codex OAuth 反代稳定性**、**托盘用量可见化**、**Skills 导入 / 安装可靠性**、**Gemini 会话恢复路径**,以及**简化 Hermes 配置健康处理**展开。
系统托盘第一次把当前 Claude / Codex / Gemini 供应商的**缓存用量**直接呈现在子菜单里——包含订阅额度摘要和用量脚本摘要,并用颜色标记利用率;针对 Kimi / 智谱 / MiniMax 这类中国编码套餐供应商,托盘还会额外渲染 `🟢 h12% w80%` 风格的 **5 小时 + 周窗口**双窗口排版,语义与官方订阅徽章完全一致(取更紧的那个驱动 emoji)。创建 Claude 供应商时,如果 `ANTHROPIC_BASE_URL` 命中已知的编码套餐 host,会自动注入 `meta.usage_script`,托盘可以不打开 Usage Script 模态框就直接点亮。
Codex OAuth 侧修复了多项反代稳定性问题:使用客户端自带的 session ID 作为 `prompt_cache_key` 和 Codex session 头,避免生成 UUID 造成缓存抖动,显著提高缓存命中率;非流式 Anthropic 客户端在 ChatGPT Codex 上游强制 OpenAI Responses SSE 时也能正确拿到 JSON 响应;Stream Check 现在会以和生产一致的 `store: false`、encrypted reasoning include 以及供应商 FAST 模式构造探测请求,避免出现"检测失败但实际能用"的错位。配合新增的 **FAST 模式显式开关**,让用户可以在 Codex OAuth 型 Claude 供应商上按需发 `service_tier="priority"`,在延迟和 ChatGPT 配额消耗之间自己选。
另外,移除了 CC Switch 内置的 **Hermes 配置健康扫描器**及其警告横幅(以及对应的 `scan_hermes_config_health` 命令、`HermesHealthWarning` 类型和 `HermesWriteOutcome.warnings` 载荷),把 Hermes 面板聚焦回当前供应商展示、默认切换、Memory 编辑和启动 Hermes Web UI,深度配置健康度由 Hermes 自己负责。
**发布日期**2026-04-23
**更新规模**13 commits | 48 files changed | +1,883 / -808 lines
---
## 重点内容
- **托盘用量可见化**Claude / Codex / Gemini 托盘子菜单展示当前供应商缓存用量,含订阅与脚本摘要及颜色标记;刷新带节流、仅针对可见应用、并回写到 React Query (#2184, 感谢 @TuYv)
- **托盘编码套餐用量(Kimi / 智谱 / MiniMax**:托盘渲染 5 小时 + 周窗口双窗口用量,沿用 `🟢 h12% w80%` 排版;命中已知 host 的 Claude 供应商自动注入 `meta.usage_script`
- **Codex OAuth FAST 模式**:为 Codex OAuth 型 Claude 供应商新增显式 FAST 开关,开启后转换后的 Responses 请求发 `service_tier="priority"`,默认关闭 (#2210, 感谢 @JesusDR01)
- **Codex OAuth 稳定性**:修复反代缓存路由 (#2218, 感谢 @majiayu000)、Responses SSE 聚合 (#2235, 感谢 @xpfo-go)、Stream Check 与生产一致性 (#2210, 感谢 @JesusDR01)
- **Hermes 配置健康扫描器移除**:把 Hermes 面板聚焦回供应商管理、Memory 编辑和 Web UI 启动,不再重复承担深度配置健康判断
- **Skills 导入 / 安装可靠性**:导入过程中禁用操作按钮、结果按 ID 去重 (#2211, 感谢 @TuYv);一键配置基于最新表单状态 (#2249, 感谢 @Coconut-Fish);根级 `SKILL.md` 仓库安装稳定 (#2231, 感谢 @santugege)
- **Gemini 会话恢复路径**:扫描会话时读取 `.project_root` 元数据,把原始项目目录带回恢复流程 (#2240, 感谢 @tisonkun)
- **Session / 设置布局打磨**:滚动区域视口加宽度约束修复横向溢出,应用底部和设置页底部间距更紧凑 (#2201, 感谢 @Coconut-Fish)
---
## 新功能
### 托盘用量可见化
- 系统托盘子菜单新增当前 Claude / Codex / Gemini 供应商的**缓存用量**展示 (#2184, 感谢 @TuYv)
- 包含订阅额度摘要和用量脚本摘要,并用颜色标记利用率
- 托盘触发的刷新**带节流**、**只覆盖可见应用**,并同步回 React Query,主窗口和托盘共享同一份用量数据
### 托盘编码套餐用量(Kimi / 智谱 / MiniMax
- 托盘为中国编码套餐供应商渲染 **5 小时 + 周窗口**双窗口用量
- 使用与官方订阅徽章一致的 `🟢 h12% w80%` 两窗口排版,取更紧的那个利用率驱动 emoji 颜色
- 创建 Claude 供应商时,如果 `ANTHROPIC_BASE_URL` 匹配已知编码套餐 host,会**自动注入** `meta.usage_script`,托盘不打开 Usage Script 模态框也能直接点亮
- 更新时会**保留已有** `usage_script` 值,不覆盖用户自定义
### Codex OAuth FAST 模式
- 为 Codex OAuth 型 Claude 供应商新增显式 FAST 模式开关 (#2210, 感谢 @JesusDR01)
- 开启时,转换后的 Responses 请求会发 `service_tier="priority"` 以降低延迟
- 默认关闭,避免意外增加 ChatGPT 配额消耗
---
## 变更
### Session 与设置布局打磨
- 滚动区域视口加上宽度约束,修复横向溢出 (#2201, 感谢 @Coconut-Fish)
- 应用底部和设置页底部间距更紧凑,让长 Session / 设置视图看起来更干净
---
## 移除
### Hermes 配置健康扫描器
- 移除应用内的 Hermes 配置健康扫描器和警告横幅
- 移除 `scan_hermes_config_health` 命令、`HermesHealthWarning` 类型以及 `HermesWriteOutcome.warnings` 载荷
- CC Switch 的 Hermes 面板回归核心职责:当前供应商展示、切换默认供应商、Memory 编辑、以及启动 Hermes Web UI 处理深度配置
---
## 修复
### Codex OAuth 缓存路由
- 使用客户端自带的 session ID 作为 `prompt_cache_key` 和 Codex session 头,保留显式缓存 key (#2218, 感谢 @majiayu000)
- 停止生成 UUID 导致的缓存抖动,让 ChatGPT Codex 反代的缓存身份更稳定
### Codex OAuth Responses SSE 聚合
- ChatGPT Codex 上游强制 OpenAI Responses SSE 时,非流式 Anthropic 客户端也能正确拿到 JSON (#2235, 感谢 @xpfo-go)
- CC Switch 会在非流式转换之前先聚合上游 SSE 事件
### Codex OAuth Stream Check 对齐
- Stream Check 构造的 Codex OAuth 测试请求现在与生产代理一致,使用相同的 `store: false`、加密 reasoning include 和供应商 FAST 模式设置 (#2210, 感谢 @JesusDR01)
- 避免"检测失败但实际能用"的错位
### Codex 模型提取
- 读取 Codex 配置的 `model` 字段时,改用 TOML 解析替代首行正则匹配 (#2227, 感谢 @nmsn)
- 多行 TOML 也能正确处理
### 模型快速填入 / 一键配置
- 模型快速填入现在基于**最新的**供应商表单配置应用 (#2249, 感谢 @Coconut-Fish)
- 修复陈旧表单状态导致一键配置失败的问题
### Skills 导入去重
- Skills 导入对话框在导入进行时禁用所有操作按钮 (#2211, 感谢 @TuYv)
- 已安装 Skills 的缓存按 ID 去重,避免双击造成重复的已安装条目 (#2139)
### 根级 Skill 仓库
- Skill 的安装与更新流程现在能一致地识别三种源路径:直接嵌套路径、按 install-name 递归搜索、以及仓库根的 `SKILL.md` 源 (#2231, 感谢 @santugege)
### Gemini 会话恢复路径
- Gemini 会话扫描时读取 `.project_root` 元数据 (#2240, 感谢 @tisonkun)
- 恢复流程可以在可用时把原始项目目录传回
### 供应商名悬浮提示
- 供应商图标在 inline SVG、图像 URL、以及首字母回退渲染路径下都会在 hover 时展示供应商名称 (#2237, 感谢 @tisonkun)
---
## 说明与注意事项
- **Hermes 健康扫描器已移除**:如果你依赖 CC Switch 提示 Hermes YAML 的深度配置问题,请改为通过工具栏的"启动 Hermes Web UI"按钮在 Hermes 原生面板里查看。日常供应商管理、切换、Memory 编辑、MCP 与 Skills 同步仍然由 CC Switch 负责。
- **Codex OAuth FAST 模式默认关闭**:只有在你接受可能增加 ChatGPT 配额消耗换取更低延迟时,才需要打开。
- **托盘缓存用量**:刷新带节流,只覆盖当前显示的应用,避免无必要的上游 API 调用;数据会回写到 React Query,因此主窗口和托盘看到的值一致。
---
## 下载与安装
访问 [Releases](https://github.com/farion1231/cc-switch/releases/latest) 下载对应版本。
### 系统要求
| 系统 | 最低版本 | 架构 |
| ------- | -------------------------- | ----------------------------------- |
| Windows | Windows 10 及以上 | x64 |
| macOS | macOS 12 (Monterey) 及以上 | Intel (x64) / Apple Silicon (arm64) |
| Linux | 见下表 | x64 |
### Windows
| 文件 | 说明 |
| ---------------------------------------- | ----------------------------------- |
| `CC-Switch-v3.14.1-Windows.msi` | **推荐** - MSI 安装包,支持自动更新 |
| `CC-Switch-v3.14.1-Windows-Portable.zip` | 便携版,解压即用,不写入注册表 |
### macOS
| 文件 | 说明 |
| -------------------------------- | --------------------------------------------- |
| `CC-Switch-v3.14.1-macOS.dmg` | **推荐** - DMG 安装包,拖入 Applications 即可 |
| `CC-Switch-v3.14.1-macOS.zip` | 解压后拖入 ApplicationsUniversal Binary |
| `CC-Switch-v3.14.1-macOS.tar.gz` | 用于 Homebrew 安装和自动更新 |
> macOS 版本已通过 Apple 代码签名和公证,可直接安装使用。
### HomebrewmacOS
```bash
brew tap farion1231/ccswitch
brew install --cask cc-switch
```
更新:
```bash
brew upgrade --cask cc-switch
```
### Linux
| 发行版 | 推荐格式 | 安装方式 |
| --------------------------------------- | ----------- | ---------------------------------------------------------------------- |
| Ubuntu / Debian / Linux Mint / Pop!\_OS | `.deb` | `sudo dpkg -i CC-Switch-*.deb``sudo apt install ./CC-Switch-*.deb` |
| Fedora / RHEL / CentOS / Rocky Linux | `.rpm` | `sudo rpm -i CC-Switch-*.rpm``sudo dnf install ./CC-Switch-*.rpm` |
| openSUSE | `.rpm` | `sudo zypper install ./CC-Switch-*.rpm` |
| Arch Linux / Manjaro | `.AppImage` | 添加执行权限后直接运行,或使用 AUR |
| 其他发行版 / 不确定 | `.AppImage` | `chmod +x CC-Switch-*.AppImage && ./CC-Switch-*.AppImage` |
-498
View File
@@ -1,498 +0,0 @@
# CC Switch v3.15.0
> Claude Desktop becomes a first-class managed surface with third-party provider switching via proxy gateway, role-based model mapping, major reverse-proxy hardening, Codex OAuth live model discovery, and a filter-driven usage dashboard Hero card
**[中文版 →](v3.15.0-zh.md) | [日本語版 →](v3.15.0-ja.md)**
---
## Overview
CC Switch v3.15.0 is a major release following the v3.14.x line, centered on **promoting Claude Desktop to a first-class managed surface**. It ships third-party provider switching through the in-app proxy gateway, role-based model mapping (`sonnet` / `opus` / `haiku`) with a `supports1m` long-context flag, Copilot/Codex OAuth provider reuse, a redesigned Claude Code import flow, app-switcher differentiation between "Claude Code" and "Claude Desktop", and 44 provider presets translated from the Claude Code catalog into the new Claude Desktop surface.
Around proxy reliability, this release performs a systematic hardening pass: P0P3 patches across routing / lifecycle / retry / failover / rectifier paths; pooled HTTPS connection reuse for non-Anthropic backends to cut per-request latency; cache hit-rate improvements for Codex and OpenAI Responses (emit `prompt_cache_key` only when a real client-provided session identity exists, canonicalize JSON keys in outgoing request bodies plus `tool_call` arguments and `tool_result` content, and thread `session_id` into the usage logger); correct Anthropic ↔ OpenAI `tool_choice` mapping; Vertex AI full URLs are no longer truncated; Gemini request models are now extracted from the URI path; takeover detection is tightened; and IPv6 listen addresses are supported. ChatGPT Codex OAuth providers no longer depend on hardcoded model lists — CC Switch now fetches a live model list from the ChatGPT backend on demand.
Claude Code's model mapping is now role-based (`sonnet` / `opus` / `haiku`) with display names and a new `supports1m` boolean flag, replacing the legacy `[1M]` suffix and decoupling routing decisions from raw model IDs. The usage dashboard adds a **filter-driven Hero card** that exposes cache-normalized real total tokens and cache hit rate, updated live as the active date range / provider / model filters change; paired with a fix for cache-cost semantics and the noisy pricing warning storm that fired on every request. Robustness improvements in the OpenAI Responses API usage parsing path mean missing or malformed upstream `usage` no longer crashes the VSCode Claude Code extension with a `null` output.
The provider ecosystem expands further: new BytePlus, Volcengine Agentplan, ClaudeAPI, ClaudeCN, RunAPI, RelaxyCode, PatewayAI, and Baidu Qianfan Coding Plan partner presets; DouBao Seed is promoted to partner status; and provider cards now surface a "routing support" badge so users can tell at a glance which providers can be served through Local Routing. This release also fixes a long tail of issues across Codex sessions, OAuth, Claude Desktop forms, Linux segfaults, terminal fallbacks, and ships several GitHub Actions dependency bumps.
**Release date**: 2026-05-16
**Stats**: 127 commits | 211 files changed | +17,980 insertions | -2,748 deletions
---
## Highlights
- **Claude Desktop Becomes a First-Class Managed Surface**: Third-party provider switching through the in-app proxy gateway, role-based model mapping (`sonnet` / `opus` / `haiku`) with a `supports1m` long-context flag, Copilot/Codex OAuth provider reuse, and 44 provider presets translated from the Claude Code catalog. Note: 20 Claude Desktop presets now default to direct mode instead of proxy mode — verify connectivity after upgrade if you previously relied on proxy routing.
- **Major Reverse-Proxy Hardening**: P0P3 lifecycle / retry / failover / rectifier patches; pooled HTTPS reuse for non-Anthropic backends; Codex / Responses cache hit-rate improvements; correct Anthropic ↔ OpenAI `tool_choice` mapping; Vertex AI URL preservation; Gemini path-based model extraction; refined takeover detection; IPv6 listen address support.
- **Provider Ecosystem Expansion**: New BytePlus, Volcengine Agentplan, ClaudeAPI, ClaudeCN, RunAPI, RelaxyCode, PatewayAI, and Baidu Qianfan Coding Plan partner presets; DouBao Seed promoted to partner status; routing-support badges on provider cards.
- **Role-Based Model Mapping with 1M Flag**: Role-based `sonnet` / `opus` / `haiku` routing with display names and a `supports1m` flag replaces the legacy `[1M]` suffix.
- **Codex OAuth Live Model Discovery**: ChatGPT Codex providers fetch the live model list from the ChatGPT backend on demand.
- **Usage Dashboard Filter-Driven Hero**: Surfaces cache-normalized real total tokens and cache hit rate, updated live as date / provider / model filters change.
- **DeepSeek Tool Calls + Zero-Usage Final Delta**: DeepSeek tool calls now return `reasoning_content` alongside `tool_calls` (#2543, thanks @bling-yshs); the final `message_delta` always includes a usage block (even when zero) so strict Anthropic clients no longer crash on `null` (#2485, thanks @Myoontyee).
- **OpenAI Responses API Usage Parsing Robustness**: Missing or malformed upstream `usage` no longer crashes the VSCode Claude Code extension (#2422, thanks @magucas).
---
## Added
### Claude Desktop Third-Party Provider Switching via Proxy Gateway
CC Switch now treats **Claude Desktop** as a first-class managed surface alongside Claude Code / Codex / Gemini / OpenCode / OpenClaw / Hermes.
- New dedicated Claude Desktop panel that brokers third-party providers to Claude Desktop through CC Switch's in-app proxy gateway
- Routing-support badge on cards for providers that need Local Routing
- Role-based model route mapping locked to `sonnet` / `opus` / `haiku`
- Copilot / Codex OAuth providers can be reused in the Claude Desktop panel
- Redesigned Claude Code settings import flow
- App switcher visually distinguishes "Claude Code" from "Claude Desktop", and the app visibility settings use the "Claude Code" label
- 44 Claude Desktop provider presets translated from the Claude Code preset catalog
### Routing Support Badges on Provider Cards
Provider cards in both the Claude Code and Codex panels now show a routing-support badge so users can tell at a glance which providers can be served through Local Routing.
### Codex OAuth Live Model List
ChatGPT Codex providers no longer rely on a hardcoded model selection — CC Switch fetches a **live model list** from the ChatGPT backend on demand.
### Role-Based Model Mapping with 1M Flag
Claude Code model mapping is now role-based (`sonnet` / `opus` / `haiku`) with display names and a `supports1m` boolean flag, replacing the legacy `[1M]` suffix and decoupling routing from raw model IDs.
### Filter-Driven Usage Hero
The usage dashboard's Hero summary is now filter-driven, updating live as the active date range / provider / model filters change; it surfaces **cache-normalized real total tokens** and cache hit rate so the Hero figures line up with the detail list below.
### Provider Form "Save Anyway" Prompt
Softened provider form input validation by turning non-blocking input issues into a "save anyway" prompt, so a harmless field issue no longer blocks saving (#2307, thanks @allenxln).
### Universal Provider Duplicate Action
Added a "duplicate" button for universal providers from the provider list (#2416, thanks @hubutui).
### Persisted Tauri Window State
Window position and size now persist across launches (#2377, thanks @BillSaul).
### Tray Icon Tooltip
The system tray icon now surfaces a status tooltip on hover (#2417, thanks @Coconut-Fish).
### Warp Terminal Session Launch
Added support for launching Warp and executing a saved session inside it (#2466, thanks @tisonkun).
### DeepSeek `reasoning_content` for Tool Calls
DeepSeek tool-call responses now return `reasoning_content` and `tool_calls` together, so callers can render both (#2543, thanks @bling-yshs).
### Baidu Qianfan Coding Plan (Claude Code)
Added a Baidu Qianfan Coding Plan preset (#2322, thanks @jimmyzhuu).
### Compshare Coding Plan Preset (Cross-App)
The Compshare Coding Plan preset now lands across claude / codex / hermes / openclaw.
### Partner Provider Presets
Added **BytePlus**, **Volcengine Agentplan**, **ClaudeAPI**, **ClaudeCN**, **RunAPI**, **RelaxyCode**, and **PatewayAI** partner presets; promoted **DouBao Seed** to partner status (refreshed endpoint and links).
### 44 Claude Desktop Provider Presets
Translated 44 provider presets from the Claude Code preset catalog into the new Claude Desktop panel.
---
## Changed
### 20 Claude Desktop Presets Default to Direct Mode
20 Claude Desktop presets now ship in direct mode instead of routing through the proxy by default, reducing setup friction for users who don't need proxy-specific compatibility shims. If you previously relied on proxy routing for these presets, verify connectivity after upgrading.
### Claude Desktop Operational Notes
Switching a Claude Desktop provider writes CC Switch's managed 3P profile and **requires restarting Claude Desktop** to take effect; proxy-mode providers require CC Switch's Local Routing to stay running while in use.
### Failover / Local Routing Guardrails
Failover controls now require the target app's Local Routing takeover to be enabled before they can be turned on; stopping only the proxy service is blocked while any app still depends on takeover state, preventing the "proxy stopped but the app still thinks takeover is running" inconsistency.
### Usage Accounting Semantics Changed
Usage summaries now report **cache-normalized real total tokens** and **cache hit rate**. Historical token and cost figures may **shift** after deduplication and pricing recalculation — the new numbers are more accurate but will not equal the values reported in earlier versions.
### Provider Preset Rendering Order
Preset lists now render in the author-defined array order, with partners prioritized first, replacing the previous implicit sort.
### Model Mapping Hint Copy Simplified
`modelMappingOffHint` was rewritten as action-oriented copy across zh / en / ja.
### CC Switch Brand Surface Unified to ccswitch.io
All in-app and README "official website" references now point at ccswitch.io as the sole official site; the release notes template also surfaces ccswitch.io.
### Theme Switch Simplified
Removed the circular reveal animation during theme switches; theme changes are now an instant cross-fade.
### Claude Code App Switcher Differentiation
The app switcher visually distinguishes "Claude Code" from "Claude Desktop", and the app visibility settings use the "Claude Code" label.
### CI: Claude Review Upgraded to Opus 4.7
The Claude review GitHub Action is upgraded to Opus 4.7; the prompt is tuned to reduce nitpick noise; a new `@claude` review-only Code Action is added; PR head SHA is pinned for checkout; the `--max-turns 5` limit is removed.
### GitHub Actions Dependency Bumps
- `actions/checkout` 4 → 6 (#2517)
- `pnpm/action-setup` 5 → 6 (#2518)
- `softprops/action-gh-release` 2 → 3 (#2519)
- `actions/stale` 9 → 10 (#2520)
### DeepSeek Presets Switched to V4
DeepSeek presets now ship V4 (flash / pro) with refreshed pricing seeds.
### Codex 1M Context Toggle Hidden in Edit Form
The 1M context-window toggle is no longer surfaced in the Codex provider edit form, reducing the density of knobs that have no effect in current Codex deployments.
### OpenClaudeCode Migrated to MicuAPI Domain
The OpenClaudeCode preset is migrated to the MicuAPI domain; Micu API links are refreshed to `micuapi.ai`.
### CrazyRouter Endpoints Switched to `cn` Subdomain
CrazyRouter preset endpoints now use the `cn` subdomain.
### RelaxyCode Custom Icon
The RelaxyCode preset icon is switched to a custom `relaxcode.png` asset.
### Kimi For Coding Doc URL
The Kimi For Coding website URL is updated to the `/code/docs/` path.
### SiliconFlow International Site Shows USD
The SiliconFlow international site now correctly shows USD for balance display (it previously displayed CNY incorrectly).
---
## Fixed
### OpenAI Responses API Usage Parsing Robustness
Hardened `build_anthropic_usage_from_responses()` and the Responses → Anthropic SSE translator so a missing or malformed upstream `usage` no longer produces `"usage": null` in `message_delta`. This unblocks strict Anthropic clients (notably the VSCode Claude Code extension) that crashed with `Cannot read properties of null (reading 'output_tokens')` against providers such as Codex OAuth and DashScope's `compatible-mode/v1/responses` endpoint. Added OpenAI field-name fallbacks (`prompt_tokens` / `completion_tokens`), null / empty / partial object handling, and preserved cache token fields even when input/output tokens are missing (#2422, thanks @magucas).
### Proxy Reliability Patches (P0P3)
Multiple rounds of routing / lifecycle / retry / rectifier patches across the request-forwarder paths; extracted a shared `handle_rectifier_retry_failure` helper and a shared `auth_header_value` helper.
### Proxy: Pooled HTTPS Connection Reuse for Non-Anthropic Backends
Non-Anthropic backends now reuse pooled HTTPS connections instead of opening a fresh TLS session per request, materially reducing per-request latency.
### Proxy: Forward Client's Actual HTTP Method
The proxy no longer hard-codes `POST` — it forwards the client's actual HTTP method, so non-POST upstream endpoints (e.g. GET `/v1/models`) now work correctly.
### Proxy: Per-Attempt Counters and `max_retries` Wiring
Client-request counters are moved out of the per-attempt loop; `AppProxyConfig.max_retries` is now correctly wired into the request forwarder.
### Proxy: Failover Decision Refinements
Refined retryable vs. unretryable error classification in the request forwarder.
### Proxy: Takeover Detection Tightening
Takeover detection is tightened; disabling takeover uses fallback restore, so leftover state no longer strands a provider.
### Proxy: Anthropic ↔ OpenAI `tool_choice` Mapping
During format conversion, Anthropic's `tool_choice` is now correctly mapped to the OpenAI Chat nested form.
### Proxy: Gemini Request Model Extracted from URI Path
Gemini request models are now extracted from the URI path (instead of the body), so transformed traffic reports the right model name.
### Proxy: Auth Header Error Handling
`get_auth_headers` now returns `Result` instead of panicking on bad credentials.
### Proxy: IPv6 Listen Address Validation
The Proxy panel now accepts IPv6 listen addresses.
### Proxy: Codex / Responses Cache Hit Rate
Improved cache hit rate for Codex and OpenAI Responses requests by stabilizing cache key derivation: emit `prompt_cache_key` only when the client actually carries a session identity, so unrelated conversations no longer collapse onto a single key; canonicalize (sort) JSON keys in outgoing request bodies and in `tool_call` arguments / `tool_result` content for byte-identical prefix-cache reuse; thread `session_id` into the usage logger for request correlation.
### Proxy: JSON Schema Underscore Fields Preserved
Private-parameter filtering now preserves underscore-prefixed field names inside JSON Schema name maps (`properties`, `patternProperties`, `definitions`, `$defs`), so user-defined schema keys like `_id` and `_meta` pass through the filter intact.
### Proxy: Read Tool Empty Pages
Drop empty pages from `Read` tool inputs so providers no longer reject the request (#2472, thanks @Kwensiu).
### Proxy: Per-Request Hot-Path Trim
Trimmed per-request hot-path work and database wait time.
### Proxy: Real Provider Model Names Under Takeover
Under takeover, the Claude Code menu now exposes the real provider model names instead of a stale alias.
### Proxy: Zero Usage in Final `message_delta`
The final `message_delta` event now always includes a usage block (even when zero) so strict Anthropic clients no longer crash on `null` (#2485, thanks @Myoontyee).
### Proxy: Streaming `message_delta` Deduplication
Deduplicated `message_delta` events that some upstreams emit twice (#2366, thanks @codeasier).
### Proxy: Scoped `reasoning_content` Preserved for Tool Calls
Tool-call paths now correctly preserve the scoped `reasoning_content` field during transformation; Kimi / Moonshot's OpenAI Chat compatibility path keeps the field while generic OpenAI-compatible requests stay free of it (#2367, thanks @codeasier).
### Proxy: Vertex AI Full URL Preserved
Full Vertex AI URLs are no longer truncated during proxy forwarding (#2415, thanks @xpfo-go).
### Proxy: Leading Billing Header Stripped from System Content
Some upstreams prepend a billing-header chunk to the system message; this content is now stripped (#2350).
### Proxy: Claude Auth Strategy Derived from `ANTHROPIC_*` Env Var
The Claude auth strategy is now derived from the actual `ANTHROPIC_*` env variable name rather than an opaque heuristic.
### Third-Party Claude Providers: Disable Model Test
Model probing is disabled for third-party Claude gateways that don't implement `/v1/models` consistently.
### Model-Fetch: `/models` for Anthropic-Compatible Subpath Providers
`/models` discovery now works for Anthropic-compatible subpath providers.
### Copilot: Claude Model IDs Resolved Against Live `/models`
Copilot-backed providers now resolve Claude model IDs against the live `/models` list to avoid stale ID mismatches.
### Codex: Session Title No Longer Pulls in `environment_context`
Codex session title extraction no longer pulls in the `environment_context` noise (#2439, thanks @eclipsehx).
### Codex: Subagent Sessions Hidden
Codex subagent sessions are now hidden from the main session list (#2445, thanks @LanternCX).
### Codex Startup Live Import Duplication
Fixed a duplicate-import bug in the Codex startup live-import path (#2590, thanks @DhruvShankpal).
### Codex Provider Switch No Longer Disturbs History
Switching the active Codex provider no longer changes existing session history (#2349, thanks @SaladDay).
### Codex Usage Log Wording
Corrected a misleading log message for Codex session usage (#2473, thanks @tisonkun).
### Claude: Persist `max` Effort via Env
`max` effort now correctly persists across restart via the env variable (#2493, thanks @makoMakoGo).
### Claude Desktop: Model Route Matching Without `[1M]` Suffix
Route matching no longer requires the legacy `[1M]` suffix.
### Claude Desktop: Provider Form Input Focus Loss
Fixed an input in the Claude Desktop provider form that lost focus while being edited.
### Claude Desktop: Spurious Proxy-Stopped Status Alert
Removed an alert that fired spuriously when the proxy was intentionally stopped.
### Claude Desktop: Empty Toolbar Capsule Hidden
The empty toolbar capsule is now hidden when Claude Desktop is the active app.
### UI: Monitor Badge Icon Centering
Centered the Monitor badge icon in the app switcher.
### Linux: Theme Selection Segfault
Prevented a segfault triggered by selecting a theme on Linux (#2502, thanks @definfo).
### Terminal: iTerm Fallback on Cold Launch
Prevented iTerm from being selected as a fallback on cold launch when it isn't actually installed (#2448, thanks @hulkbig).
### Config: JSON Keys Sorted Alphabetically
Config writes now sort JSON keys alphabetically for deterministic output (#2469, thanks @fuleinist).
### "Import Existing" Made Side-Effect Free
The "import existing" action is now side-effect free (#2429, thanks @xwil1).
### Coding Plan: Zhipu Weekly Tier Named by Reset Time
Corrected the Zhipu weekly tier name to match the actual reset time (#2420, thanks @TuYv).
### DashScope: Usage Parsing Robustness
Hardened DashScope usage parsing so a malformed payload no longer crashes the VSCode Claude Code extension (#2425, thanks @magucas).
### Usage: Deduplicate Proxy and Session-Log Sources
Deduplicated usage records sourced from both the proxy and session logs.
### Usage: Cache Cost Semantics + Pricing Warn Storm
Corrected cache-cost semantics and silenced the noisy pricing warning that fired on every request.
### CI: Frontend Formatting + Linux Clippy Restored
Restored frontend formatting and Linux clippy checks in CI.
### Proxy Test Helper Clippy Warning
Fixed a clippy warning in the proxy test helper.
---
## Removed
### Hermes Agent Usage Tracking Integration
Removed the Hermes Agent usage tracking integration originally planned for this cycle — upstream behavior changes made the integration impractical to maintain. The integration was **never enabled in any released version**; the "zero-cost rendering" bug discovered during its development was fixed before the integration was rolled back.
### Theme Switch Circular Reveal Animation
Removed the circular reveal animation used during theme switches — it stuttered on slower compositors and added little visible value.
### DDSHub Partner Integration
Removed DDSHub as a partner preset and dropped the cross-link blurbs from the READMEs.
---
## Docs
### README Sponsor Refresh (zh / en / ja)
Added BytePlus, ClaudeCN, RunAPI, and PatewayAI sponsor entries; cross-linked BytePlus and Volcengine entries; refreshed the CrazyRouter $2 credit claim flow, the Compshare blurb, the Right Code blurb, and other sponsor logos and listings; flattened the LionCC logo onto a white background; switched the Chinese README's sponsor logo to the Volcengine artwork; added Hermes Agent to the README subtitles.
### Release Notes Template
The release notes template now surfaces `ccswitch.io`.
### Brand Surface
Documented `ccswitch.io` as the sole official website across READMEs and in-app references.
---
## ⚠️ Upgrade Notes
### 20 Claude Desktop Presets Default to Direct Mode
These 20 presets previously routed through the proxy by default and now default to direct mode. If you were using one of these presets pre-upgrade and depended on the proxy path for connectivity (for example because the proxy applies a special rectifier or transformation layer), verify connectivity after upgrading; you can manually switch them back to proxy mode from the CC Switch panel if needed.
### Claude Desktop Operational Constraints
Switching a Claude Desktop provider **requires restarting Claude Desktop** to take effect; proxy-mode providers require CC Switch's Local Routing to stay running while in use — quitting CC Switch or stopping Local Routing will cut off any proxy-mode Claude Desktop providers.
### Failover Requires Takeover Enabled
Before enabling Failover, make sure the target app's Local Routing takeover is enabled, otherwise the Failover control will refuse to start; stopping the proxy service while any app still depends on takeover state is blocked, so you need to disable takeover at the app layer first before stopping the proxy.
### Usage Figures May Diverge from History
Usage summaries now use cache-normalized real total tokens + cache hit rate. Historical token and cost figures may **shift** after deduplication and pricing recalculation — the new numbers are more accurate but will not equal what earlier versions reported.
---
## ⚠️ Risk Notice
This release inherits the risk notices originally introduced in v3.12.3 / v3.13.0 for reverse-proxy-style features.
**GitHub Copilot Reverse Proxy**: Using Copilot's reverse-proxy path may violate GitHub / Microsoft's terms of service. See the [v3.12.3 release notes](v3.12.3-en.md#-risk-notice) for details.
**Codex OAuth Reverse Proxy**: Using the Codex OAuth reverse proxy with a ChatGPT subscription may violate OpenAI's terms of service. See the [v3.13.0 release notes](v3.13.0-en.md#-risk-notice) for details.
**Claude Desktop Third-Party Provider Switching via Proxy Gateway**: Routing Claude Desktop traffic through CC Switch's in-app proxy gateway to a third-party provider exposes those requests to that provider's billing, compliance, and data-retention policies — read the target provider's terms of service before using.
By enabling these features, users **accept all associated risks**. CC Switch is not responsible for any account restrictions, warnings, or service suspensions that result from using these features.
---
## Download & Installation
Visit [Releases](https://github.com/farion1231/cc-switch/releases/latest) to download the appropriate version.
### System Requirements
| OS | Minimum Version | Architecture |
| ------- | ---------------------------- | ----------------------------------- |
| Windows | Windows 10 or later | x64 |
| macOS | macOS 12 (Monterey) or later | Intel (x64) / Apple Silicon (arm64) |
| Linux | See table below | x64 / ARM64 |
### Windows
| File | Description |
| ---------------------------------------- | ----------------------------------------------------- |
| `CC-Switch-v3.15.0-Windows.msi` | **Recommended** - MSI installer, supports auto-update |
| `CC-Switch-v3.15.0-Windows-Portable.zip` | Portable, extract and run, no registry writes |
### macOS
| File | Description |
| -------------------------------- | ------------------------------------------------------- |
| `CC-Switch-v3.15.0-macOS.dmg` | **Recommended** - DMG installer, drag into Applications |
| `CC-Switch-v3.15.0-macOS.zip` | Extract and drag into Applications, Universal Binary |
| `CC-Switch-v3.15.0-macOS.tar.gz` | For Homebrew installation and auto-update |
> macOS builds are Apple code-signed and notarized — install directly.
### Homebrew (macOS)
```bash
brew tap farion1231/ccswitch
brew install --cask cc-switch
```
Update:
```bash
brew upgrade --cask cc-switch
```
### Linux
> Linux artifacts are published for both **x86_64** and **ARM64** (`aarch64`). The architecture is included in the asset filename — pick the one matching your machine's `uname -m` output:
>
> - `CC-Switch-v3.15.0-Linux-x86_64.AppImage` / `.deb` / `.rpm`
> - `CC-Switch-v3.15.0-Linux-arm64.AppImage` / `.deb` / `.rpm`
| Distribution | Recommended | Installation |
| --------------------------------------- | ----------- | ---------------------------------------------------------------------- |
| Ubuntu / Debian / Linux Mint / Pop!\_OS | `.deb` | `sudo dpkg -i CC-Switch-*.deb` or `sudo apt install ./CC-Switch-*.deb` |
| Fedora / RHEL / CentOS / Rocky Linux | `.rpm` | `sudo rpm -i CC-Switch-*.rpm` or `sudo dnf install ./CC-Switch-*.rpm` |
| openSUSE | `.rpm` | `sudo zypper install ./CC-Switch-*.rpm` |
| Arch Linux / Manjaro | `.AppImage` | Add execute permission and run, or use AUR |
| Other distros / not sure | `.AppImage` | `chmod +x CC-Switch-*.AppImage && ./CC-Switch-*.AppImage` |
-498
View File
@@ -1,498 +0,0 @@
# CC Switch v3.15.0
> Claude Desktop が一等管理パネルに昇格(プロキシゲートウェイ経由のサードパーティプロバイダー切り替えを含む)、ロールベースのモデルマッピング、リバースプロキシの大幅強化、Codex OAuth ライブモデル検出、Usage ダッシュボードのフィルター駆動 Hero カード
**[English →](v3.15.0-en.md) | [中文 →](v3.15.0-zh.md)**
---
## 概要
CC Switch v3.15.0 は v3.14.x に続く大型バージョンアップで、コアの焦点は **Claude Desktop を一等管理パネルに昇格させること**にあります。これに合わせて、内蔵プロキシゲートウェイを介したサードパーティプロバイダーの切り替え、ロールベースのモデルマッピング(sonnet / opus / haiku+ `supports1m` ロングコンテキストフラグ、Copilot / Codex OAuth プロバイダーの再利用、再設計された Claude Code インポートフロー、App スイッチャーでの「Claude Code」と「Claude Desktop」の視覚的な区別、そして Claude Code プリセットディレクトリから翻訳された 44 個の Claude Desktop プリセットを提供します。
リバースプロキシの信頼性については、本リリースで系統的なハードニングを行いました: P0–P3 の複数回にわたるルーティング / ライフサイクル / リトライ / フェイルオーバー / 補正器の修正; 非 Anthropic バックエンドで HTTPS コネクションプールを再利用してリクエスト単位のレイテンシを低減; Codex と OpenAI Responses のキャッシュヒット率改善(`prompt_cache_key` は本物のクライアントセッション識別子がある場合のみ送信、外部リクエストボディと `tool_call` 引数 / `tool_result` 内容の JSON キーを正規化してソート、`session_id` を Usage ロガーに通す); Anthropic ↔ OpenAI `tool_choice` の正しい相互変換; Vertex AI の完全な URL を切り詰めない; Gemini は URI パスからモデル名を抽出するように変更; Local Routing のテイクオーバー検出をより精緻化; IPv6 リッスンアドレスのサポート。Codex OAuth 系の Claude プロバイダーはハードコードされたモデルリストに依存しなくなり、CC Switch が必要に応じて ChatGPT バックエンドからライブモデルリストを取得します。
Claude Code のモデルマッピングはロールベース(`sonnet` / `opus` / `haiku`+ 表示名に変更され、`supports1m` 真偽値フラグが導入されました。これは旧来の `[1M]` サフィックス記法に取って代わり、ルーティング判定と元のモデル ID を分離します。Usage ダッシュボードには**フィルター駆動 Hero カード**が追加され、キャッシュ正規化後の真の総トークン数とキャッシュヒット率を表示し、現在の日付範囲 / プロバイダー / モデルのフィルターに追従してリアルタイム更新します。あわせてキャッシュコストのセマンティクスエラーと、リクエストごとに発生していた pricing 警告ノイズを修正しました。OpenAI Responses API の usage 解析パスを堅牢化し、上流の欠損または不正な `usage` のせいで VSCode Claude Code プラグインが `null` 出力でクラッシュしないようにしました。
プロバイダーエコシステムはさらに拡張されました: BytePlus、Volcengine Agentplan、ClaudeAPI、ClaudeCN、RunAPI、RelaxyCode、PatewayAI、Baidu Qianfan Coding Plan のパートナープリセットを追加; Doubao Seed をパートナープリセットに昇格; プロバイダーカードに「Local Routing 対応」バッジを表示。本リリースでは、Codex セッション、OAuth、Claude Desktop フォーム、Linux segfault、ターミナルフォールバックなどに関する多くの細部の問題も修正し、複数の GitHub Actions 依存関係をアップグレードしました。
**リリース日**: 2026-05-16
**Stats**: 127 commits | 211 files changed | +17,980 insertions | -2,748 deletions
---
## ハイライト
- **Claude Desktop が一等管理パネルに**: 内蔵プロキシゲートウェイを介したサードパーティプロバイダーの切り替え、ロールベースのモデルマッピング(sonnet / opus / haiku+ `supports1m` ロングコンテキストフラグ、Copilot / Codex OAuth プロバイダーの再利用、Claude Code プリセットディレクトリから翻訳された 44 個のプリセットを提供。注意: 20 個の Claude Desktop プリセットがデフォルトでプロキシモードから直接接続モードに切り替わったため、アップグレード後にプロキシルーティングに依存している場合は接続性を検証してください
- **リバースプロキシの大幅強化**: P0–P3 のライフサイクル / リトライ / フェイルオーバー / 補正器の修正; 非 Anthropic バックエンドの HTTPS コネクションプール再利用; Codex / Responses キャッシュヒット率改善; Anthropic ↔ OpenAI `tool_choice` の正しいマッピング; Vertex AI URL の完全保持; Gemini パスベースのモデル抽出; テイクオーバー検出の精緻化; IPv6 リッスンアドレスのサポート
- **プロバイダーエコシステムの拡張**: BytePlus、Volcengine Agentplan、ClaudeAPI、ClaudeCN、RunAPI、RelaxyCode、PatewayAI、Baidu Qianfan Coding Plan のパートナープリセットを追加; Doubao Seed をパートナーに昇格; プロバイダーカードに「ルーティングプロキシ対応」バッジを表示
- **ロールベースのモデルマッピング + 1M フラグ**: ロールベースの sonnet / opus / haiku ルーティング + 表示名 + `supports1m` フラグ。旧来の `[1M]` サフィックスに取って代わる
- **Codex OAuth ライブモデル検出**: ChatGPT Codex 系プロバイダーは必要に応じて ChatGPT バックエンドからライブモデルリストを取得
- **Usage ダッシュボードのフィルター駆動 Hero**: キャッシュ正規化後の真の総トークン数とキャッシュヒット率を表示し、現在の日付 / プロバイダー / モデルフィルターに追従してリアルタイム更新
- **DeepSeek ツール呼び出し + ゼロ usage 最終 delta**: DeepSeek のツール呼び出しが `reasoning_content` も返却するように (#2543, 感謝 @bling-yshs); 最終 `message_delta` は常に usage ブロックを含む(すべてゼロでも)ため、厳格な Anthropic クライアントが `null` でクラッシュしなくなった (#2485, 感謝 @Myoontyee)
- **OpenAI Responses API usage 解析の堅牢化**: 上流の欠損または不正な usage によって VSCode Claude Code プラグインがクラッシュしないように (#2422, 感謝 @magucas)
---
## 追加機能
### Claude Desktop サードパーティプロバイダーのプロキシ切り替え
CC Switch は初めて **Claude Desktop** を一等管理対象パネルとして扱い、Claude Code / Codex / Gemini / OpenCode / OpenClaw / Hermes と並列に位置づけます。
- Claude Desktop 専用パネルを追加し、CC Switch 内蔵プロキシゲートウェイを介してサードパーティプロバイダーを Claude Desktop に代理転送
- ルーティングプロキシを必要とするプロバイダーには、カード上に「Local Routing 対応」バッジを表示
- ロールベースのモデルルーティングマッピングで `sonnet` / `opus` / `haiku` にロック
- Copilot / Codex OAuth プロバイダーを Claude Desktop パネルで再利用可能
- 再設計された Claude Code 設定インポートフロー
- App スイッチャーで「Claude Code」と「Claude Desktop」を視覚的に区別、アプリ可視性設定では「Claude Code」ラベルを使用
- Claude Code プリセットディレクトリから翻訳された 44 個の Claude Desktop プロバイダープリセット
### プロバイダーカード: ルーティングプロキシ対応バッジ
Claude Code と Codex パネルのプロバイダーカードに「ルーティングプロキシ対応」バッジを追加し、Local Routing 経由で提供可能なプロバイダーを一目で識別できるようにしました。
### Codex OAuth ライブモデルリスト
ChatGPT Codex 系プロバイダーはハードコードされたモデル選択に依存しなくなり、CC Switch が必要に応じて ChatGPT バックエンドから**ライブモデルリスト**を取得します。
### ロールベースのモデルマッピング + 1M フラグ
Claude Code のモデルマッピングはロールベース(`sonnet` / `opus` / `haiku`+ 表示名に変更され、`supports1m` 真偽値フラグが導入されました。これは旧来の `[1M]` サフィックス記法に取って代わり、ルーティング判定と元のモデル ID を分離します。
### Usage ダッシュボードのフィルター駆動 Hero
Usage ダッシュボードの Hero サマリーがフィルター駆動になり、現在の日付範囲 / プロバイダー / モデルフィルターに追従してリアルタイムに変化します。**キャッシュ正規化後の真の総トークン数**とキャッシュヒット率を表示することで、Hero の数値が下部の詳細リストと整合するようになります。
### プロバイダーフォームの「とりあえず保存」
プロバイダーフォームの入力検証を緩和し、非ブロッキングな入力上の問題を「とりあえず保存」型のヒントに変更しました。無害な軽微なフィールド問題が原因で保存が阻まれることがなくなります (#2307, 感謝 @allenxln)。
### Universal プロバイダーの複製アクション
プロバイダーリスト内の universal プロバイダーに「複製」ボタンを追加しました (#2416, 感謝 @hubutui)。
### Tauri ウィンドウ状態の永続化
ウィンドウの位置とサイズが再起動をまたいで保持されるようになりました (#2377, 感謝 @BillSaul)。
### トレイアイコンのホバーヒント
システムトレイアイコンにホバー時のステータスヒントを表示するようになりました (#2417, 感謝 @Coconut-Fish)。
### Warp ターミナルセッション起動
Warp ターミナルのサポートを追加し、保存されたセッションを Warp で実行できるようになりました (#2466, 感謝 @tisonkun)。
### DeepSeek ツール呼び出し `reasoning_content`
DeepSeek のツール呼び出しレスポンスが `reasoning_content``tool_calls` を同時に返却するようになり、呼び出し側が両方を一緒にレンダリングできるようになりました (#2543, 感謝 @bling-yshs)。
### Baidu Qianfan Coding PlanClaude Code
Baidu Qianfan Coding Plan プリセットを追加しました (#2322, 感謝 @jimmyzhuu)。
### Compshare Coding Plan プリセット(クロスアプリ)
Compshare Coding Plan プリセットを claude / codex / hermes / openclaw の全アプリに展開しました。
### パートナープロバイダープリセット
**BytePlus**、**Volcengine Agentplan**、**ClaudeAPI**、**ClaudeCN**、**RunAPI**、**RelaxyCode**、**PatewayAI** のパートナープリセットを追加; **Doubao Seed** をパートナープリセットに昇格(エンドポイントとリンクをリフレッシュ)。
### 44 個の Claude Desktop プロバイダープリセット
Claude Code プリセットディレクトリから 44 個のプロバイダープリセットを翻訳し、新しい Claude Desktop パネルに投入しました。
---
## 変更
### 20 個の Claude Desktop プリセットがデフォルトで直接接続モードに
20 個の Claude Desktop プリセットがデフォルトでプロキシモードから直接接続モードに切り替わり、プロキシ互換シムを必要としないユーザーの導入摩擦を低減しました。アップグレード前にこれらのプリセットのプロキシルーティング経由の接続性に依存していた場合は、アップグレード後に検証してください。
### Claude Desktop の操作制約
Claude Desktop のプロバイダーを切り替えると CC Switch 管理の 3P プロファイルが書き込まれます。**Claude Desktop の再起動**が必要です; プロキシモードのプロバイダーは、使用中 CC Switch の Local Routing が動作し続けている必要があります。
### Failover / Local Routing 連動検証
Failover コントロールは、ターゲットアプリの Local Routing テイクオーバーが有効になっていないと開けないように変更しました。プロキシサービスのみを止めてもテイクオーバー状態に依存するアプリがある場合はブロックされ、「プロキシは止めたがアプリはまだテイクオーバー中と認識している」という不整合を回避します。
### Usage 統計のセマンティクス変更
Usage サマリーは**キャッシュ正規化後の真の総トークン数**と**キャッシュヒット率**を報告するようになりました。データの重複排除と価格再計算により、過去のトークン数とコスト数値は**ずれる可能性があります** — 新しい数値の方が正確ですが、旧バージョンの数値とは一致しません。
### プロバイダープリセットのレンダリング順序
プリセットリストは作者が定義した配列順序でレンダリングされるようになり、パートナーが先頭に並びます。以前の暗黙的なソートを置き換えます。
### モデルマッピングヒント文面の簡素化
`modelMappingOffHint` を中 / 英 / 日でアクション指向の簡潔な文面に書き直しました。
### CC Switch ブランド公式サイトを ccswitch.io に統一
アプリ内および README 内のすべての「公式サイト」参照を、唯一の公式サイトとして ccswitch.io に統一しました; Release notes テンプレートにも ccswitch.io を反映。
### テーマ切り替えの簡素化
テーマ切り替え時の円形拡散アニメーションを削除し、即座にクロスフェードする方式に変更しました。
### Claude Code App スイッチャーの視覚的な区別
App スイッチャーで「Claude Code」と「Claude Desktop」を視覚的に区別し、アプリ可視性設定では「Claude Code」ラベルを使用するようにしました。
### CI: Claude Review を Opus 4.7 にアップグレード
Claude review GitHub Action を Opus 4.7 にアップグレード; nitpick ノイズを減らすためプロンプトを調整; `@claude` レビュー専用 Code Action を追加; PR head SHA を checkout 用にロック; `--max-turns 5` 制限を削除。
### GitHub Actions 依存関係のアップグレード
- `actions/checkout` 4 → 6 (#2517)
- `pnpm/action-setup` 5 → 6 (#2518)
- `softprops/action-gh-release` 2 → 3 (#2519)
- `actions/stale` 9 → 10 (#2520)
### DeepSeek プリセットを V4 に
DeepSeek プリセットが V4flash / pro)+ リフレッシュされた価格シードを出荷するようになりました。
### Codex 1M コンテキストトグルを編集フォームから隠す
Codex プロバイダー編集フォームでは 1M コンテキストトグルを表示しなくなり、現在の Codex デプロイメントには実効性のないノブの密度を低減しました。
### OpenClaudeCode を MicuAPI ドメインに移行
OpenClaudeCode プリセットを MicuAPI ドメインに移行; Micu API リンクを `micuapi.ai` にリフレッシュ。
### CrazyRouter エンドポイントを `cn` サブドメインに切り替え
CrazyRouter プリセットのエンドポイントを `cn` サブドメインに変更しました。
### RelaxyCode カスタムアイコン
RelaxyCode プリセットのアイコンをカスタム `relaxcode.png` アセットに変更しました。
### Kimi For Coding ドキュメント URL
Kimi For Coding のウェブサイト URL を `/code/docs/` パスに更新しました。
### SiliconFlow 国際版で USD 表示
SiliconFlow 国際版の残高を正しく USD で表示するように修正しました(以前は誤って CNY と表示)。
---
## 修正
### OpenAI Responses API usage 解析の堅牢化
`build_anthropic_usage_from_responses()` と Responses → Anthropic SSE トランスレーターを強化し、上流の欠損または不正な `usage``message_delta` 内で `"usage": null` を生成しないようにしました。これにより、厳格な Anthropic クライアント(典型例: VSCode Claude Code プラグイン)が一部のプロバイダー(Codex OAuth、DashScope の `compatible-mode/v1/responses` エンドポイント)で `Cannot read properties of null (reading 'output_tokens')` でクラッシュしていた問題が解消されます。OpenAI フィールド名のフォールバック(`prompt_tokens` / `completion_tokens`)、null / 空 / 部分オブジェクトの処理、input/output tokens が欠損していても cache token フィールドを保持する処理を追加しました (#2422, 感謝 @magucas)。
### プロキシ信頼性パッチ(P0–P3)
request-forwarder パス全体で複数回にわたるルーティング / ライフサイクル / リトライ / 補正器の修正を実施; 共有された `handle_rectifier_retry_failure` ヘルパーと `auth_header_value` ヘルパーを抽出。
### プロキシ: 非 Anthropic バックエンドの HTTPS コネクションプール再利用
非 Anthropic バックエンドはプールされた HTTPS コネクションを再利用し、リクエストごとに新しい TLS セッションを開かなくなりました。リクエスト単位のレイテンシが大幅に低減します。
### プロキシ: クライアントの実際の HTTP メソッドを転送
`POST` のハードコーディングをやめ、クライアントの実際の HTTP メソッドに従って転送するようになりました; 上流の非 POST エンドポイント(例: GET `/v1/models`)が正常に動作します。
### プロキシ: 試行ごとのカウンター + `max_retries` の接続
クライアントリクエストカウンターを試行ごとのループから外に移動; `AppProxyConfig.max_retries` がリクエストフォワーダーに正しく接続されるようになりました。
### プロキシ: フェイルオーバー判定の精緻化
リクエストフォワーダー内でのリトライ可能 / 不可能エラーの分類がより正確になりました。
### プロキシ: テイクオーバー検出の精緻化
テイクオーバー検出をより厳密にしました; テイクオーバー OFF 時はフォールバック復旧パスを通り、残留状態によってプロバイダーが固まらないようにします。
### プロキシ: Anthropic ↔ OpenAI `tool_choice` の相互変換
フォーマット変換時に Anthropic の `tool_choice` を OpenAI Chat のネスト形式に正しくマッピングするようになりました。
### プロキシ: Gemini リクエストのモデルを URI パスから抽出
Gemini リクエストのモデルを URI パスから抽出するようになりました(body からは取らない)。変換後のトラフィックが正しいモデル名を報告します。
### プロキシ: 認証ヘッダーのエラー処理
`get_auth_headers``Result` を返すようになり、認証情報に問題がある場合にパニックしなくなりました。
### プロキシ: IPv6 リッスンアドレスの検証
プロキシパネルが IPv6 リッスンアドレスを受け付けるようになりました。
### プロキシ: Codex / Responses キャッシュヒット率の改善
安定したキャッシュキー導出によって Codex と OpenAI Responses リクエストのキャッシュヒット率を改善: クライアントが本当にセッション識別子を持参してきた場合にのみ `prompt_cache_key` を送信し、無関係な会話が同じキーに潰されないようにする; 外部リクエストボディと `tool_call` 引数 / `tool_result` 内容内の JSON キーを正規化してソートし、プレフィックスキャッシュがバイト単位でマッチできるようにする; `session_id` を usage ロガーに通してリクエストを関連付けする。
### プロキシ: JSON Schema のアンダースコアフィールド保持
プライベートパラメータフィルタリングが JSON Schema name map`properties``patternProperties``definitions``$defs`)内のアンダースコア接頭辞のフィールド名を保持するようになりました。ユーザー定義 schema キー(`_id``_meta` など)がフィルターを正常に通り抜けられます。
### プロキシ: Read ツールの空白ページ除去
`Read` ツールの入力から空白ページを除去し、プロバイダーがリクエストを拒否しないようにしました (#2472, 感謝 @Kwensiu)。
### プロキシ: リクエスト単位のホットパス軽量化
リクエストごとのホットパスのオーバーヘッドとデータベース待ち時間を削減しました。
### プロキシ: テイクオーバー下で真のプロバイダーモデル名を表示
テイクオーバー実行時に、Claude Code メニューが古いエイリアスではなく真のプロバイダーモデル名を露出するようになりました。
### プロキシ: 最終 `message_delta` は常に usage を含む
最終 `message_delta` イベントには常に usage ブロックが含まれるようになりました(すべてゼロでも)。厳格な Anthropic クライアントが `null` でクラッシュしなくなります (#2485, 感謝 @Myoontyee)。
### プロキシ: ストリーミング `message_delta` の重複排除
一部の上流が二重に送信する `message_delta` イベントの重複排除を行います (#2366, 感謝 @codeasier)。
### プロキシ: ツール呼び出しパスでの `reasoning_content` 保持
ツール呼び出しパスの変換時に scoped `reasoning_content` フィールドを正しく保持するようにしました; Kimi / Moonshot の OpenAI Chat 互換パスではこのフィールドを保持し、汎用 OpenAI 互換リクエストでは引き続き付加しません (#2367, 感謝 @codeasier)。
### プロキシ: Vertex AI の完全 URL 保持
Vertex AI の完全 URL がプロキシ転送時に切り詰められないようにしました (#2415, 感謝 @xpfo-go)。
### プロキシ: system content 先頭の課金ヘッダーを除去
一部の上流が system message の先頭に挿入する課金ヘッダー内容を除去するようにしました (#2350)。
### プロキシ: Claude 認証ストラテジーを `ANTHROPIC_*` 環境変数名から導出
不透明なヒューリスティックに依存するのをやめ、認証ストラテジーを実際の `ANTHROPIC_*` 環境変数名から導出するようにしました。
### サードパーティ Claude プロバイダー: モデルテストの無効化
`/v1/models` を一貫して実装していないサードパーティ Claude ゲートウェイに対して、モデルプローブを無効化しました。
### Model-Fetch: Anthropic 互換サブパスプロバイダーの `/models`
`/models` ディスカバリーが Anthropic 互換のサブパスプロバイダーに対しても動作するようになりました。
### Copilot: Claude モデル ID をライブ `/models` と照合
Copilot バックエンドのプロバイダーはライブ `/models` リストを使って Claude モデル ID を照合し、古い ID の不整合を回避するようになりました。
### Codex: セッションタイトルが `environment_context` を取り込まないように
Codex のセッションタイトル抽出が `environment_context` のノイズを引き込まなくなりました (#2439, 感謝 @eclipsehx)。
### Codex: subagent セッションを非表示
Codex の subagent セッションをメインセッションリストから非表示にしました (#2445, 感謝 @LanternCX)。
### Codex 起動時の live import 重複排除
Codex 起動時の live import パスにおける重複インポートのバグを修正しました (#2590, 感謝 @DhruvShankpal)。
### Codex プロバイダー切り替えで履歴を擾乱しないように
アクティブな Codex プロバイダーの切り替えが既存のセッション履歴を変更しなくなりました (#2349, 感謝 @SaladDay)。
### Codex usage ログの文言修正
Codex セッション usage の誤解を招くログを 1 件修正しました (#2473, 感謝 @tisonkun)。
### Claude: `max` effort を env 経由で永続化
`max` effort が再起動をまたいで env 変数経由で正しく永続化されるようになりました (#2493, 感謝 @makoMakoGo)。
### Claude Desktop: モデルルーティングで `[1M]` サフィックスを要求しないように
ルーティングマッチングがレガシーな `[1M]` サフィックスを要求しなくなりました。
### Claude Desktop: プロバイダーフォームの入力フォーカス消失
Claude Desktop プロバイダーフォームで入力ボックス編集中にフォーカスを失う問題を修正しました。
### Claude Desktop: 偽の「プロキシ停止」ステータス通知
プロキシが能動的に停止された際に誤って発火するヒントを削除しました。
### Claude Desktop: 空のツールバーカプセル非表示
Claude Desktop がアクティブアプリの場合、空のツールバーカプセルを非表示にします。
### UI: Monitor バッジアイコンのセンタリング
App スイッチャー内の Monitor バッジアイコンをセンタリングしました。
### Linux: テーマ選択で segfault
Linux でテーマを選択した際の segfault を防止しました (#2502, 感謝 @definfo)。
### ターミナル: コールドスタート時の iTerm fallback
コールドスタート時に存在しない iTerm をフォールバックに選んでしまうのを防止しました (#2448, 感謝 @hulkbig)。
### 設定: JSON キーを辞書順でソート
設定の書き込みが JSON キーを辞書順にソートするようになり、出力が決定的になりました (#2469, 感謝 @fuleinist)。
### 「既存をインポート」を副作用なしに
「既存をインポート」操作を副作用なしに変更しました (#2429, 感謝 @xwil1)。
### Coding Plan: Zhipu の週次ウィンドウをリセット時刻で命名
Zhipu の週次ウィンドウのティア名を実際のリセット時刻に合うように修正しました (#2420, 感謝 @TuYv)。
### DashScope: usage 解析の堅牢化
DashScope の usage 解析を強化し、不正なペイロードが VSCode Claude Code プラグインをクラッシュさせないようにしました (#2425, 感謝 @magucas)。
### Usage: プロキシとセッションログの重複排除
プロキシとセッションログという 2 つのソースをまたいで usage レコードの重複排除を行います。
### Usage: キャッシュコストのセマンティクス + pricing 警告の嵐
キャッシュコストのセマンティクスを修正し、リクエストごとに発生していたノイズの多い pricing 警告を解消しました。
### CI: フロントエンドフォーマット + Linux clippy の復活
CI のフロントエンドフォーマットと Linux clippy の実行を復活させました。
### プロキシテストヘルパー clippy 警告
プロキシテストヘルパーの clippy 警告を 1 件修正しました。
---
## 削除
### Hermes Agent usage トラッキング統合
本サイクルでオンラインにする予定だった Hermes Agent usage トラッキング統合を削除しました — 上流の動作変更によって、この統合のメンテナンスが現実的でなくなりました。この統合は**いかなるリリース版でも有効化されたことはなく**; 開発過程で発見された「ゼロコストレンダリング」バグは統合をロールバックする前に修正済みです。
### テーマ切り替えの円形拡散アニメーション
テーマ切り替え時の円形拡散アニメーションを削除しました — 性能の弱いコンポジターでカクつき、視覚的なメリットが限定的でした。
### DDSHub パートナー統合
DDSHub をパートナープリセットから削除し、各 README 内の相互リンクセクションも削除しました。
---
## ドキュメント
### README スポンサー更新(中 / 英 / 日)
BytePlus、ClaudeCN、RunAPI、PatewayAI のスポンサーエントリを追加; BytePlus と Volcengine のエントリを相互リンク; CrazyRouter の $2 クレジット受領フロー、Compshare の説明、Right Code の説明、その他スポンサーのロゴおよびリストアイテムをリフレッシュ; LionCC のロゴを白背景にフラット化; 中国語 README のスポンサーロゴを Volcengine 画像に切り替え; README のサブタイトルに Hermes Agent を追加。
### Release notes テンプレート
Release notes テンプレート内に `ccswitch.io` を反映しました。
### ブランド公式サイト
各 README およびアプリ内参照で `ccswitch.io` を唯一の公式サイトとしてドキュメント化しました。
---
## ⚠️ アップグレード時の注意
### 20 個の Claude Desktop プリセットがデフォルトで直接接続モードに
これら 20 個のプリセットは以前はデフォルトでプロキシ経由でルーティングされていましたが、現在はデフォルトで直接接続です。アップグレード前にこのうちのいずれかを使用しており、かつプロキシルーティングの接続性に依存していた場合(例: プロキシに特殊な補正器や変換層がある場合)、接続性を検証してください; 必要に応じて、CC Switch パネル内で手動でプロキシモードに戻すことができます。
### Claude Desktop の操作制約
Claude Desktop プロバイダーの切り替えには、**Claude Desktop の再起動**が必要です; プロキシモードのプロバイダーは、使用中 CC Switch の Local Routing が動作し続けている必要があります — CC Switch を終了させたり Local Routing を停止させたりすると、プロキシモードの Claude Desktop プロバイダーへの接続が切断されます。
### Failover にはテイクオーバーの有効化が必要
Failover を有効化する前に、ターゲットアプリの Local Routing テイクオーバーが有効になっていることを確認してください。さもないと Failover コントロールは起動を拒否します; プロキシサービスを止めたいがテイクオーバーに依存するアプリがある場合はブロックされるため、アプリ層で先にテイクオーバーを止めてからプロキシを停止する必要があります。
### Usage 統計の数値が過去と一致しない可能性
Usage サマリーはキャッシュ正規化後の真の総トークン数 + キャッシュヒット率を使用するようになりました。データの重複排除と価格再計算により、過去のトークン数とコスト数値は**ずれる可能性があります** — 新しい数値の方が正確ですが、旧バージョンの数値とは一致しません。
---
## ⚠️ リスク通知
本リリースは、リバースプロキシ系機能について v3.12.3 / v3.13.0 で提起されたリスク通知を継承します。
**GitHub Copilot リバースプロキシ**: Copilot のリバースプロキシパスを使用すると、GitHub / Microsoft の利用規約に違反する可能性があります。詳細は [v3.12.3 リリースノート](v3.12.3-ja.md#-リスクに関する注意事項) を参照してください。
**Codex OAuth リバースプロキシ**: ChatGPT サブスクリプションを使用した Codex OAuth リバースプロキシは、OpenAI の利用規約に違反する可能性があります。詳細は [v3.13.0 リリースノート](v3.13.0-ja.md#-リスクに関する注意事項) を参照してください。
**Claude Desktop サードパーティプロバイダーのプロキシ切り替え**: CC Switch 内蔵プロキシゲートウェイ経由で Claude Desktop のリクエストをサードパーティプロバイダーに転送する際、サードパーティプロバイダーの課金、コンプライアンス、データ保持に関する制約はそれぞれ異なります。利用前にターゲットプロバイダーの利用規約をお読みください。
ユーザーが上記機能を有効化することで、**すべてのリスクを自己責任で**受諾したものとみなされます。CC Switch は、これらの機能の使用に起因するアカウントの制限、警告、サービス停止について一切の責任を負いません。
---
## ダウンロード・インストール
[Releases](https://github.com/farion1231/cc-switch/releases/latest) から対応バージョンをダウンロードしてください。
### システム要件
| OS | 最小バージョン | アーキテクチャ |
| ------- | ------------------------ | ----------------------------------- |
| Windows | Windows 10 以降 | x64 |
| macOS | macOS 12 (Monterey) 以降 | Intel (x64) / Apple Silicon (arm64) |
| Linux | 下表参照 | x64 / ARM64 |
### Windows
| ファイル | 説明 |
| ---------------------------------------- | ------------------------------------------- |
| `CC-Switch-v3.15.0-Windows.msi` | **推奨** - MSI インストーラー、自動更新対応 |
| `CC-Switch-v3.15.0-Windows-Portable.zip` | ポータブル版、解凍して実行、レジストリ不要 |
### macOS
| ファイル | 説明 |
| -------------------------------- | ------------------------------------------------------ |
| `CC-Switch-v3.15.0-macOS.dmg` | **推奨** - DMG インストーラー、Applications にドラッグ |
| `CC-Switch-v3.15.0-macOS.zip` | 解凍して Applications にドラッグ、Universal Binary |
| `CC-Switch-v3.15.0-macOS.tar.gz` | Homebrew インストールと自動更新用 |
> macOS 版は Apple のコード署名および公証済みで、直接インストールして使用できます。
### HomebrewmacOS
```bash
brew tap farion1231/ccswitch
brew install --cask cc-switch
```
更新:
```bash
brew upgrade --cask cc-switch
```
### Linux
> Linux 向けの成果物は **x86_64** と **ARM64**`aarch64`)の両方が提供されます。ファイル名にアーキテクチャ識別子が含まれているため、`uname -m` の出力に応じて選択してください:
>
> - `CC-Switch-v3.15.0-Linux-x86_64.AppImage` / `.deb` / `.rpm`
> - `CC-Switch-v3.15.0-Linux-arm64.AppImage` / `.deb` / `.rpm`
| ディストリビューション | 推奨形式 | インストール方法 |
| --------------------------------------- | ----------- | -------------------------------------------------------------------------- |
| Ubuntu / Debian / Linux Mint / Pop!\_OS | `.deb` | `sudo dpkg -i CC-Switch-*.deb` または `sudo apt install ./CC-Switch-*.deb` |
| Fedora / RHEL / CentOS / Rocky Linux | `.rpm` | `sudo rpm -i CC-Switch-*.rpm` または `sudo dnf install ./CC-Switch-*.rpm` |
| openSUSE | `.rpm` | `sudo zypper install ./CC-Switch-*.rpm` |
| Arch Linux / Manjaro | `.AppImage` | 実行権限を付与して実行、または AUR を使用 |
| その他のディストリビューション / 不明 | `.AppImage` | `chmod +x CC-Switch-*.AppImage && ./CC-Switch-*.AppImage` |
-498
View File
@@ -1,498 +0,0 @@
# CC Switch v3.15.0
> Claude Desktop 升级为一等管理面板(含第三方供应商代理切换)、按角色的模型映射、反向代理大幅强化、Codex OAuth 实时模型发现、用量看板筛选驱动 Hero 卡
**[English →](v3.15.0-en.md) | [日本語版 →](v3.15.0-ja.md)**
---
## 概览
CC Switch v3.15.0 是 v3.14.x 之后的一次大版本更新,核心聚焦在**把 Claude Desktop 升级为一等管理面板**,并配套提供第三方供应商通过内置代理网关进行切换、按角色的模型映射(sonnet / opus / haiku+ `supports1m` 长上下文标志、Copilot/Codex OAuth 供应商复用、重新设计的 Claude Code 导入流程、App 切换器对"Claude Code"和"Claude Desktop"的可视化区分,以及 44 个从 Claude Code 预设目录翻译而来的 Claude Desktop 预设。
围绕反向代理的可靠性,本版本进行了一次系统性硬化:P0–P3 多轮针对路由 / 生命周期 / 重试 / 故障转移 / 补正器的修补;非 Anthropic 后端启用 HTTPS 连接池复用以降低单请求延迟;Codex 与 OpenAI Responses 缓存命中率提升(`prompt_cache_key` 仅在有真实客户端会话标识时发送、对外请求体与 `tool_call` 参数 / `tool_result` 内容的 JSON key 规范化排序、`session_id` 串入用量记录器);Anthropic ↔ OpenAI `tool_choice` 正确互转;Vertex AI 完整 URL 不再被截断;Gemini 改为从 URI 路径提取模型名;Local Routing 接管检测更精细;可监听 IPv6 地址。Codex OAuth 类 Claude 供应商不再依赖硬编码的模型列表,CC Switch 会按需从 ChatGPT 后端拉取实时模型列表。
Claude Code 的模型映射改为基于角色(`sonnet` / `opus` / `haiku`+ 显示名,并引入 `supports1m` 布尔标志,替代旧版的 `[1M]` 后缀写法,把路由决策与原始模型 ID 解耦。用量看板新增**筛选驱动的 Hero 卡**,展示缓存归一化后的真实总 token 与缓存命中率,并跟随当前日期范围 / 供应商 / 模型筛选实时更新;配套修复了缓存成本语义错误以及每个请求都触发的定价警告噪声。在 OpenAI Responses API usage 解析路径上做了鲁棒化处理,让上游缺失或畸形的 `usage` 不再让 VSCode Claude Code 插件因 `null` 输出崩溃。
供应商生态进一步扩张:新增 BytePlus、火山 Agentplan、ClaudeAPI、ClaudeCN、RunAPI、RelaxyCode、PatewayAI、百度千帆 Coding Plan 合作伙伴预设;豆包 Seed 升级为合作伙伴预设;供应商卡片现在会显示"是否支持 Local Routing"的徽章。本版本还修复了大量 Codex 会话、OAuth、Claude Desktop 表单、Linux 段错误、终端 fallback 等场景下的细节问题,并完成了多项 GitHub Actions 依赖升级。
**发布日期**2026-05-16
**更新规模**127 commits | 211 files changed | +17,980 / -2,748 lines
---
## 重点内容
- **Claude Desktop 成为一等管理面板**:通过内置代理网关提供第三方供应商切换、按角色的模型映射(sonnet / opus / haiku+ `supports1m` 长上下文标志、Copilot/Codex OAuth 供应商复用、44 个从 Claude Code 预设目录翻译过来的预设。注意:20 个 Claude Desktop 预设默认从代理模式切到直连模式,升级后如依赖代理路由请验证连通性
- **反向代理大幅强化**:P0–P3 生命周期 / 重试 / 故障转移 / 补正器修补;非 Anthropic 后端 HTTPS 连接池复用;Codex/Responses 缓存命中率提升;Anthropic ↔ OpenAI `tool_choice` 正确映射;Vertex AI URL 完整保留;Gemini 路径式模型提取;接管检测细化;支持 IPv6 监听地址
- **供应商生态扩张**:新增 BytePlus、火山 Agentplan、ClaudeAPI、ClaudeCN、RunAPI、RelaxyCode、PatewayAI、百度千帆 Coding Plan 合作伙伴预设;豆包 Seed 升级合作伙伴;供应商卡片显示"路由代理支持"徽章
- **按角色的模型映射 + 1M 标志**:基于角色的 sonnet / opus / haiku 路由 + 显示名 + `supports1m` 标志,替代旧的 `[1M]` 后缀
- **Codex OAuth 实时模型发现**ChatGPT Codex 类供应商按需从 ChatGPT 后端拉取实时模型列表
- **用量看板筛选驱动 Hero**:展示缓存归一化的真实总 token 和缓存命中率,跟随当前日期 / 供应商 / 模型筛选实时更新
- **DeepSeek 工具调用 + 零 usage 最终 delta**DeepSeek 工具调用一并返回 `reasoning_content` (#2543, 感谢 @bling-yshs);最终 `message_delta` 总是带 usage 块(即便全为 0),严格 Anthropic 客户端不再因 `null` 崩溃 (#2485, 感谢 @Myoontyee)
- **OpenAI Responses API usage 解析鲁棒化**:让上游缺失或畸形 usage 不再让 VSCode Claude Code 插件崩溃 (#2422, 感谢 @magucas)
---
## 新功能
### Claude Desktop 第三方供应商代理切换
CC Switch 第一次把 **Claude Desktop** 作为一等受管面板对待,与 Claude Code / Codex / Gemini / OpenCode / OpenClaw / Hermes 并列。
- 新增 Claude Desktop 专属面板,通过 CC Switch 内置代理网关把第三方供应商代理给 Claude Desktop
- 为需要路由代理的供应商在卡片上呈现"是否支持 Local Routing"的徽章
- 按角色的模型路由映射,锁定到 `sonnet` / `opus` / `haiku`
- Copilot / Codex OAuth 供应商在 Claude Desktop 面板中可复用
- 重新设计的 Claude Code 设置导入流程
- App 切换器视觉区分"Claude Code"与"Claude Desktop",应用可见性设置中使用"Claude Code"标签
- 44 个从 Claude Code 预设目录翻译过来的 Claude Desktop 供应商预设
### 供应商卡片:路由代理支持徽章
Claude Code 与 Codex 面板中的供应商卡片新增"路由代理支持"徽章,方便一眼识别哪些供应商可以通过 Local Routing 提供服务。
### Codex OAuth 实时模型列表
ChatGPT Codex 类供应商不再依赖硬编码的模型选择,CC Switch 会按需从 ChatGPT 后端拉取**实时模型列表**。
### 按角色的模型映射 + 1M 标志
Claude Code 模型映射改为基于角色(`sonnet` / `opus` / `haiku`+ 显示名,并引入 `supports1m` 布尔标志,替代旧版 `[1M]` 后缀,把路由决策与原始模型 ID 解耦。
### 用量看板筛选驱动 Hero
用量看板的 Hero 摘要现在是筛选驱动的,跟随当前日期范围 / 供应商 / 模型筛选实时变化;展示**缓存归一化后的真实总 token**与缓存命中率,让 Hero 数字与下方明细列表对齐。
### 供应商表单"先存上再说"
软化供应商表单的输入校验,把非阻塞性的输入问题改为"先存上再说"的提示,不会因为一个无伤大雅的字段问题阻止保存 (#2307, 感谢 @allenxln)。
### Universal 供应商复制动作
为 universal 供应商在供应商列表中新增"复制"按钮 (#2416, 感谢 @hubutui)。
### 持久化 Tauri 窗口状态
窗口位置和尺寸现在跨重启保留 (#2377, 感谢 @BillSaul)。
### 托盘图标 hover 提示
系统托盘图标现在悬浮显示状态提示 (#2417, 感谢 @Coconut-Fish)。
### Warp 终端会话启动
新增对 Warp 终端的支持,可在 Warp 中执行保存的会话 (#2466, 感谢 @tisonkun)。
### DeepSeek 工具调用 `reasoning_content`
DeepSeek 工具调用响应现在同时返回 `reasoning_content``tool_calls`,调用方可以两者一并渲染 (#2543, 感谢 @bling-yshs)。
### 百度千帆 Coding PlanClaude Code
新增百度千帆 Coding Plan 预设 (#2322, 感谢 @jimmyzhuu)。
### Compshare Coding Plan 预设(跨应用)
Compshare Coding Plan 预设跨 claude / codex / hermes / openclaw 全应用就位。
### 合作伙伴供应商预设
新增 **BytePlus**、**火山 Agentplan**、**ClaudeAPI**、**ClaudeCN**、**RunAPI**、**RelaxyCode**、**PatewayAI** 合作伙伴预设;**豆包 Seed** 升级合作伙伴预设(端点和链接刷新)。
### 44 个 Claude Desktop 供应商预设
从 Claude Code 预设目录翻译 44 个供应商预设进入新的 Claude Desktop 面板。
---
## 变更
### 20 个 Claude Desktop 预设默认切到直连模式
20 个 Claude Desktop 预设默认从代理模式切到直连模式,降低对不需要代理兼容垫片的用户的上手摩擦。如果你升级前依赖代理路由这些预设的连通性,请升级后验证。
### Claude Desktop 操作约束
切换 Claude Desktop 供应商会写入 CC Switch 管理的 3P profile**需要重启 Claude Desktop** 才能生效;代理模式的供应商在使用期间需要 CC Switch 的 Local Routing 保持运行。
### Failover / Local Routing 联动校验
Failover 控件现在要求目标应用的 Local Routing 接管已启用才能开启;只关代理服务但仍有应用依赖接管状态的情况会被拦下,避免出现"代理关了但应用仍以为接管在跑"的不一致。
### 用量统计语义变化
用量摘要现在报告**缓存归一化后的真实总 token**和**缓存命中率**。历史 token 与成本数字在数据去重 + 价格重算后**可能会有偏移**——新数字更准,但不会等于旧版给出的数字。
### 供应商预设渲染顺序
预设列表现在按作者定义的数组顺序渲染,合作伙伴排前面,替代之前的隐式排序。
### 模型映射提示文案简化
`modelMappingOffHint` 跨中 / 英 / 日重写为动作导向的简洁文案。
### CC Switch 品牌官网统一到 ccswitch.io
应用内和 README 中所有"官网"引用都统一到 ccswitch.io 作为唯一官方网站;Release notes 模板也呈现 ccswitch.io。
### 主题切换简化
移除主题切换时的圆形扩散动画,改为即时交叉淡入。
### Claude Code App 切换器视觉区分
App 切换器视觉上区分"Claude Code"和"Claude Desktop",应用可见性设置中使用"Claude Code"标签。
### CIClaude Review 升级到 Opus 4.7
Claude review GitHub Action 升级到 Opus 4.7;调整 prompt 降低 nitpick 噪声;新增 `@claude` 仅 review 的 Code Action;锁定 PR head SHA 用于 checkout;移除 `--max-turns 5` 限制。
### GitHub Actions 依赖升级
- `actions/checkout` 4 → 6 (#2517)
- `pnpm/action-setup` 5 → 6 (#2518)
- `softprops/action-gh-release` 2 → 3 (#2519)
- `actions/stale` 9 → 10 (#2520)
### DeepSeek 预设切到 V4
DeepSeek 预设现在出货 V4flash / pro)+ 刷新定价种子。
### Codex 1M 上下文开关在编辑表单隐藏
Codex 供应商编辑表单中不再呈现 1M 上下文开关,降低对当前 Codex 部署无实际效果的旋钮密度。
### OpenClaudeCode 迁移到 MicuAPI 域名
OpenClaudeCode 预设迁移到 MicuAPI 域名;Micu API 链接刷新到 `micuapi.ai`
### CrazyRouter 端点切到 `cn` 子域
CrazyRouter 预设端点改用 `cn` 子域。
### RelaxyCode 自定义图标
RelaxyCode 预设图标改用自定义 `relaxcode.png` 资源。
### Kimi For Coding 文档 URL
Kimi For Coding 网站 URL 更新到 `/code/docs/` 路径。
### SiliconFlow 国际站显示 USD
SiliconFlow 国际站的余额显示正确为 USD(之前错显 CNY)。
---
## 修复
### OpenAI Responses API usage 解析鲁棒化
强化 `build_anthropic_usage_from_responses()` 与 Responses → Anthropic SSE 翻译器,让上游缺失或畸形的 `usage` 不再在 `message_delta` 中产出 `"usage": null`。这解决了严格 Anthropic 客户端(典型如 VSCode Claude Code 插件)在某些供应商(Codex OAuth、DashScope 的 `compatible-mode/v1/responses` 端点)下崩在 `Cannot read properties of null (reading 'output_tokens')` 的问题。增加 OpenAI 字段名回退(`prompt_tokens` / `completion_tokens`)、null / 空 / 部分对象处理、即使 input/output tokens 缺失也保留缓存 token 字段 (#2422, 感谢 @magucas)。
### 代理可靠性补丁(P0P3
跨 request-forwarder 路径多轮路由 / 生命周期 / 重试 / 补正器修补;抽取共享的 `handle_rectifier_retry_failure` helper 与 `auth_header_value` helper。
### 代理:非 Anthropic 后端 HTTPS 连接池复用
非 Anthropic 后端复用池化的 HTTPS 连接,不再每个请求开新 TLS session,显著降低单请求延迟。
### 代理:转发客户端真实 HTTP 方法
不再硬编码 `POST`,按客户端实际的 HTTP 方法转发;上游的非 POST 端点(如 GET `/v1/models`)现在能正常工作。
### 代理:每次尝试计数器 + `max_retries` 接线
客户端请求计数器移出每次尝试的循环;`AppProxyConfig.max_retries` 现在正确接到请求转发器。
### 代理:故障转移判定细化
请求转发器中重试 / 不可重试错误的分类更准确。
### 代理:接管检测细化
接管检测更紧;关接管时走 fallback 恢复,避免遗留状态把供应商卡住。
### 代理:Anthropic ↔ OpenAI `tool_choice` 互转
格式转换时把 Anthropic 的 `tool_choice` 正确映射到 OpenAI Chat 的嵌套形式。
### 代理:Gemini 请求模型从 URI 路径提取
Gemini 请求模型从 URI 路径提取(不再从 body 取),转换后的流量上报正确的模型名。
### 代理:认证 header 错误处理
`get_auth_headers` 现在返回 `Result`,凭据有问题时不再 panic。
### 代理:IPv6 监听地址校验
代理面板现在接受 IPv6 监听地址。
### 代理:Codex / Responses 缓存命中率提升
通过稳定缓存键派生提高 Codex 与 OpenAI Responses 请求的缓存命中率;只在客户端确实带了会话标识时才发 `prompt_cache_key`,避免不相关对话被坍缩到同一个 key 上;对外请求体与 `tool_call` 参数 / `tool_result` 内容里的 JSON key 做规范化排序以便前缀缓存能字节级匹配;把 `session_id` 串到 usage 日志记录器做请求关联。
### 代理:JSON Schema 下划线字段保留
私参过滤现在保留 JSON Schema name map`properties``patternProperties``definitions``$defs`)内的下划线前缀字段名,用户自定义 schema key(如 `_id``_meta`)能正常穿过过滤。
### 代理:Read 工具空白页剔除
`Read` 工具输入中剔除空白页,避免供应商拒绝请求 (#2472, 感谢 @Kwensiu)。
### 代理:单请求热路径瘦身
缩减每个请求的热路径开销和数据库等待时间。
### 代理:接管下展示真实供应商模型名
接管运行时,Claude Code 菜单现在暴露真实供应商模型名,不是陈旧的 alias。
### 代理:最终 `message_delta` 总是带 usage
最终 `message_delta` 事件现在总是包含 usage 块(即使全为 0),严格 Anthropic 客户端不再因为 `null` 崩溃 (#2485, 感谢 @Myoontyee)。
### 代理:流式 `message_delta` 去重
对某些上游会发两次的 `message_delta` 事件做去重 (#2366, 感谢 @codeasier)。
### 代理:工具调用路径的 `reasoning_content` 保留
工具调用路径转换时正确保留 scoped `reasoning_content` 字段;Kimi / Moonshot 的 OpenAI Chat 兼容路径保留该字段,通用 OpenAI 兼容请求保持不带 (#2367, 感谢 @codeasier)。
### 代理:Vertex AI 完整 URL 保留
Vertex AI 的完整 URL 在代理转发时不再被截断 (#2415, 感谢 @xpfo-go)。
### 代理:剥离 system content 开头的计费 header
某些上游会在 system message 开头插一段计费 header 内容,现在被剥离 (#2350)。
### 代理:Claude 鉴权策略从 `ANTHROPIC_*` 环境变量名派生
不再依赖不透明的启发式,鉴权策略从实际的 `ANTHROPIC_*` 环境变量名派生。
### 第三方 Claude 供应商:禁用模型测试
对那些不一致实现 `/v1/models` 的第三方 Claude 网关,关闭模型探测。
### Model-FetchAnthropic 兼容子路径供应商的 `/models`
`/models` 发现现在对 Anthropic 兼容的子路径供应商生效。
### CopilotClaude 模型 ID 对比实时 `/models`
Copilot 后端的供应商现在用实时 `/models` 列表来比对 Claude 模型 ID,避免陈旧 ID 不一致。
### Codex:会话标题不再吸入 `environment_context`
Codex 会话标题提取不再把 `environment_context` 的噪声拉进来 (#2439, 感谢 @eclipsehx)。
### Codex:隐藏 subagent 会话
Codex subagent 会话从主会话列表隐藏 (#2445, 感谢 @LanternCX)。
### Codex 启动期 live import 去重
修复 Codex 启动期 live import 路径里的重复导入 bug (#2590, 感谢 @DhruvShankpal)。
### Codex 供应商切换不再扰动历史
切换激活 Codex 供应商不再改动现有会话历史 (#2349, 感谢 @SaladDay)。
### Codex 用量日志措辞修正
修正一条 Codex 会话用量的误导性日志 (#2473, 感谢 @tisonkun)。
### Claude`max` effort 通过 env 持久化
`max` effort 现在能跨重启正确通过 env 变量持久化 (#2493, 感谢 @makoMakoGo)。
### Claude Desktop:模型路由不再要求 `[1M]` 后缀
路由匹配不再要求遗留的 `[1M]` 后缀。
### Claude Desktop:供应商表单输入失焦
修复 Claude Desktop 供应商表单中输入框编辑时丢失焦点的问题。
### Claude Desktop:假的"代理停止"状态提示
移除代理主动停止时假触发的提示。
### Claude Desktop:空工具栏胶囊隐藏
当 Claude Desktop 是激活应用时,空的工具栏胶囊会被隐藏。
### UIMonitor 徽章图标居中
在 App 切换器里居中 Monitor 徽章图标。
### Linux:选择主题触发 segfault
防止在 Linux 上选择主题触发 segfault (#2502, 感谢 @definfo)。
### 终端:冷启动时 iTerm fallback
防止冷启动时把不存在的 iTerm 选为 fallback (#2448, 感谢 @hulkbig)。
### 配置:JSON key 字母序排序
配置写入现在按字母序排 JSON key,输出确定 (#2469, 感谢 @fuleinist)。
### "导入已有"无副作用化
"导入已有"操作改为无副作用 (#2429, 感谢 @xwil1)。
### Coding Plan:智谱周窗口按重置时间命名
修正智谱周窗口的等级名以匹配实际重置时间 (#2420, 感谢 @TuYv)。
### DashScopeusage 解析鲁棒化
强化 DashScope usage 解析,畸形 payload 不会再让 VSCode Claude Code 插件崩 (#2425, 感谢 @magucas)。
### 用量:代理和会话日志去重
跨代理和会话日志两个来源的用量记录去重。
### 用量:缓存成本语义 + 定价警告风暴
修正缓存成本语义,并消除每个请求都触发的噪声定价警告。
### CI:前端格式化 + Linux clippy 恢复
恢复前端格式化与 Linux clippy 在 CI 中的运行。
### 代理测试 helper clippy 警告
修复代理测试 helper 中的一个 clippy 警告。
---
## 移除
### Hermes Agent 用量追踪集成
移除原本计划在本周期上线的 Hermes Agent 用量追踪集成——上游行为变化让维护这个集成变得不切实际。该集成**从未在任何已发布版本中启用**;开发过程中发现的"零成本渲染" bug 在回滚集成前已经修复。
### 主题切换圆形扩散动画
移除主题切换时的圆形扩散动画——在性能较弱的合成器上会卡顿,视觉收益有限。
### DDSHub 合作伙伴整合
移除 DDSHub 作为合作伙伴预设,并删除各 README 中的交叉链接段落。
---
## 文档
### README 赞助商刷新(中 / 英 / 日)
新增 BytePlus、ClaudeCN、RunAPI、PatewayAI 赞助商条目;交叉链接 BytePlus 与火山条目;刷新 CrazyRouter 的 $2 额度领取流程、Compshare 描述、Right Code 描述、其他赞助商的 logo 与列表项;把 LionCC logo 抹平到白底;中文 README 的赞助商 logo 切到火山图;在 README 副标题中加入 Hermes Agent。
### Release notes 模板
Release notes 模板中呈现 `ccswitch.io`
### 品牌官网
在各 README 与应用内引用中把 `ccswitch.io` 文档化为唯一官方网站。
---
## ⚠️ 升级提醒
### 20 个 Claude Desktop 预设默认切到直连模式
这 20 个预设之前默认通过代理路由,现在默认直连。如果你升级前正好用着其中某个、又依赖代理路由的连通性(比如代理里有特殊补正器或转换层),请验证一下连通性;如有需要,可在 CC Switch 面板里手动切回代理模式。
### Claude Desktop 操作约束
切换 Claude Desktop 供应商需要**重启 Claude Desktop** 才能生效;代理模式的供应商在使用期间需要 CC Switch 的 Local Routing 保持运行——退出 CC Switch 或停止 Local Routing 会让代理模式的 Claude Desktop 供应商断流。
### Failover 需要接管启用
启用 Failover 前请先确认目标应用的 Local Routing 接管已开启,否则 Failover 控件会拒绝启动;想关代理服务但仍有应用依赖接管的情况会被拦下,需要先在应用层关掉接管再停代理。
### 用量统计数字可能与历史不一致
用量摘要现在用缓存归一化的真实总 token + 缓存命中率。历史 token 与成本数字在数据去重 + 价格重算后**可能会有偏移**——新数字更准,但不会等于旧版给出的数字。
---
## ⚠️ 风险提示
本版本在涉及反向代理类功能上沿用 v3.12.3 / v3.13.0 提出的风险提示。
**GitHub Copilot 反向代理**:使用 Copilot 的反代路径可能违反 GitHub / Microsoft 服务条款。详情见 [v3.12.3 release notes](v3.12.3-zh.md#-风险提示)。
**Codex OAuth 反向代理**:使用 ChatGPT 订阅的 Codex OAuth 反代可能违反 OpenAI 服务条款,详情见 [v3.13.0 release notes](v3.13.0-zh.md#-风险提示)。
**Claude Desktop 第三方供应商代理切换**:通过 CC Switch 内置代理网关把 Claude Desktop 的请求转到第三方供应商时,第三方供应商对计费、合规与数据留存的约束各不相同,请在使用前阅读目标供应商的服务条款。
用户启用上述功能即表示**自行承担所有风险**。CC Switch 不对因使用这些功能而导致的任何账号限制、警告或服务暂停承担责任。
---
## 下载与安装
访问 [Releases](https://github.com/farion1231/cc-switch/releases/latest) 下载对应版本。
### 系统要求
| 系统 | 最低版本 | 架构 |
| ------- | -------------------------- | ----------------------------------- |
| Windows | Windows 10 及以上 | x64 |
| macOS | macOS 12 (Monterey) 及以上 | Intel (x64) / Apple Silicon (arm64) |
| Linux | 见下表 | x64 / ARM64 |
### Windows
| 文件 | 说明 |
| ---------------------------------------- | ----------------------------------- |
| `CC-Switch-v3.15.0-Windows.msi` | **推荐** - MSI 安装包,支持自动更新 |
| `CC-Switch-v3.15.0-Windows-Portable.zip` | 便携版,解压即用,不写入注册表 |
### macOS
| 文件 | 说明 |
| -------------------------------- | --------------------------------------------- |
| `CC-Switch-v3.15.0-macOS.dmg` | **推荐** - DMG 安装包,拖入 Applications 即可 |
| `CC-Switch-v3.15.0-macOS.zip` | 解压后拖入 ApplicationsUniversal Binary |
| `CC-Switch-v3.15.0-macOS.tar.gz` | 用于 Homebrew 安装和自动更新 |
> macOS 版本已通过 Apple 代码签名和公证,可直接安装使用。
### HomebrewmacOS
```bash
brew tap farion1231/ccswitch
brew install --cask cc-switch
```
更新:
```bash
brew upgrade --cask cc-switch
```
### Linux
> Linux 资产同时提供 **x86_64** 和 **ARM64**`aarch64`)两种架构。资产文件名中包含架构标识,请按你机器的 `uname -m` 输出选择对应版本:
>
> - `CC-Switch-v3.15.0-Linux-x86_64.AppImage` / `.deb` / `.rpm`
> - `CC-Switch-v3.15.0-Linux-arm64.AppImage` / `.deb` / `.rpm`
| 发行版 | 推荐格式 | 安装方式 |
| --------------------------------------- | ----------- | ---------------------------------------------------------------------- |
| Ubuntu / Debian / Linux Mint / Pop!\_OS | `.deb` | `sudo dpkg -i CC-Switch-*.deb``sudo apt install ./CC-Switch-*.deb` |
| Fedora / RHEL / CentOS / Rocky Linux | `.rpm` | `sudo rpm -i CC-Switch-*.rpm``sudo dnf install ./CC-Switch-*.rpm` |
| openSUSE | `.rpm` | `sudo zypper install ./CC-Switch-*.rpm` |
| Arch Linux / Manjaro | `.AppImage` | 添加执行权限后直接运行,或使用 AUR |
| 其他发行版 / 不确定 | `.AppImage` | `chmod +x CC-Switch-*.AppImage && ./CC-Switch-*.AppImage` |
+1 -1
View File
@@ -509,7 +509,7 @@ When editing Claude providers, a set of **quick toggles** is available above the
| **Hide Attribution** | Clears commit/PR attribution metadata | Sets `attribution: {commit: "", pr: ""}` |
| **Enable Teammates** | Enables the agent teams feature | Sets `env.CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS = "1"` |
| **Enable Tool Search** | Enables tool search functionality | Sets `env.ENABLE_TOOL_SEARCH = "true"` |
| **Max Effort** | Sets effort level to max | Sets `env.CLAUDE_CODE_EFFORT_LEVEL = "max"` |
| **High Effort** | Sets effort level to high | Sets `effortLevel = "high"` |
| **Disable Auto Upgrade** | Prevents Claude Code auto-updates | Sets `env.DISABLE_AUTOUPDATER = "1"` |
When a toggle is unchecked, its corresponding config entry is removed entirely. Changes are reflected in the JSON editor in real-time.
-195
View File
@@ -1,195 +0,0 @@
# 4.2 App Routing
## Overview
App routing means letting CC Switch route a specific application's API requests through the local routing service.
When routing is enabled:
- The app's API requests are forwarded through local routing
- Request logs and usage statistics can be recorded
- Failover functionality becomes available
## Prerequisites
The routing service must be started before using the app routing feature.
## Enable Routing
### Location
Settings > Advanced > Routing Service > App Routing area
### Steps
1. Ensure the routing service is started
2. Find the "App Routing" area
3. Enable the toggle for the desired apps
### Routing Toggles
| Toggle | Effect |
|--------|--------|
| Claude Routing | Route Claude Code requests |
| Codex Routing | Route Codex requests |
| Gemini Routing | Route Gemini CLI requests |
Multiple app routings can be enabled simultaneously.
## How Routing Works
### Configuration Changes
When routing is enabled, CC Switch modifies the app's configuration file to point the API endpoint to the local routing service.
**Claude configuration change**:
```json
// Before routing
{
"env": {
"ANTHROPIC_BASE_URL": "https://api.anthropic.com"
}
}
// After routing
{
"env": {
"ANTHROPIC_BASE_URL": "http://127.0.0.1:15721"
}
}
```
**Codex configuration change**:
```toml
# Before routing
base_url = "https://api.openai.com/v1"
# After routing
base_url = "http://127.0.0.1:15721/v1"
```
**Gemini configuration change**:
```bash
# Before routing
GOOGLE_GEMINI_BASE_URL=https://generativelanguage.googleapis.com
# After routing
GOOGLE_GEMINI_BASE_URL=http://127.0.0.1:15721
```
### Request Forwarding
When the routing service receives a request:
1. Identifies the request source (Claude/Codex/Gemini)
2. Looks up the currently enabled provider for that app
3. Forwards the request to the provider's actual endpoint
4. Records the request log
5. Returns the response to the app
## Routing Status Indicators
### Main Interface Indicators
When routing is enabled, the main interface shows the following changes:
- **Routing logo color**: Changes from colorless to green
- **Provider cards**: The currently active provider shows a green border
### Provider Card States
| State | Border Color | Description |
|-------|--------------|-------------|
| Currently Active | Blue | Provider in the config file (non-routing mode) |
| Routing Active | Green | Provider actually used by routing |
| Normal | Default | Unused provider |
## Disable Routing
### Steps
1. Turn off the corresponding app's routing toggle in the routing panel
2. Or directly stop the routing service
### Configuration Restoration
When disabling routing, CC Switch will:
1. Restore the app configuration to its pre-routing state
2. Save current request logs
## Routing and Provider Switching
### Switching Providers in Routing Mode
When switching providers in routing mode:
1. Click the "Enable" button on a provider in the main interface
2. The routing service immediately uses the new provider to forward requests
3. **No need to restart the CLI tool**
This is a major advantage of routing mode: provider switching takes effect instantly.
### Switching Without Routing
When switching providers without routing:
1. Configuration file is modified
2. CLI tool must be restarted for changes to take effect
## Multi-app Routing
Multiple apps can be routed simultaneously, each managed independently:
- Independent provider configurations
- Independent failover queues
- Independent request statistics
## Use Cases
### Scenario 1: Usage Monitoring
Enable routing + log recording to monitor API usage.
### Scenario 2: Quick Switching
With routing enabled, switching providers does not require restarting CLI tools.
### Scenario 3: Failover
Enabling routing is a prerequisite for using the failover feature.
## Notes
### Performance Impact
Routing adds minimal latency (typically < 10ms), negligible for most scenarios.
### Network Requirements
In routing mode, CLI tools must be able to access the local routing address.
### Configuration Backup
Before enabling routing, CC Switch backs up the original configuration and restores it when disabled.
## FAQ
### Requests Fail After Enabling Routing
Check:
- Is the routing service running normally
- Is the provider configuration correct
- Is the network working properly
### Configuration Not Restored After Disabling Routing
Possible causes:
- Routing service exited abnormally
- Configuration file was modified by another program
Solutions:
- Manually edit the provider and re-save
- Or re-enable and then disable routing
+195
View File
@@ -0,0 +1,195 @@
# 4.2 App Takeover
## Overview
App takeover means letting CC Switch's proxy intercept and forward a specific application's API requests.
When takeover is enabled:
- The app's API requests are forwarded through the local proxy
- Request logs and usage statistics can be recorded
- Failover functionality becomes available
## Prerequisites
The proxy service must be started before using the app takeover feature.
## Enable Takeover
### Location
Settings > Advanced > Proxy Service > App Takeover area
### Steps
1. Ensure the proxy service is started
2. Find the "App Takeover" area
3. Enable the toggle for the desired apps
### Takeover Toggles
| Toggle | Effect |
|--------|--------|
| Claude Takeover | Intercept Claude Code requests |
| Codex Takeover | Intercept Codex requests |
| Gemini Takeover | Intercept Gemini CLI requests |
Multiple app takeovers can be enabled simultaneously.
## How Takeover Works
### Configuration Changes
When takeover is enabled, CC Switch modifies the app's configuration file to point the API endpoint to the local proxy.
**Claude configuration change**:
```json
// Before takeover
{
"env": {
"ANTHROPIC_BASE_URL": "https://api.anthropic.com"
}
}
// After takeover
{
"env": {
"ANTHROPIC_BASE_URL": "http://127.0.0.1:15721"
}
}
```
**Codex configuration change**:
```toml
# Before takeover
base_url = "https://api.openai.com/v1"
# After takeover
base_url = "http://127.0.0.1:15721/v1"
```
**Gemini configuration change**:
```bash
# Before takeover
GOOGLE_GEMINI_BASE_URL=https://generativelanguage.googleapis.com
# After takeover
GOOGLE_GEMINI_BASE_URL=http://127.0.0.1:15721
```
### Request Forwarding
When the proxy receives a request:
1. Identifies the request source (Claude/Codex/Gemini)
2. Looks up the currently enabled provider for that app
3. Forwards the request to the provider's actual endpoint
4. Records the request log
5. Returns the response to the app
## Takeover Status Indicators
### Main Interface Indicators
When takeover is enabled, the main interface shows the following changes:
- **Proxy logo color**: Changes from colorless to green
- **Provider cards**: The currently active provider shows a green border
### Provider Card States
| State | Border Color | Description |
|-------|--------------|-------------|
| Currently Active | Blue | Provider in the config file (non-proxy mode) |
| Proxy Active | Green | Provider actually used by the proxy |
| Normal | Default | Unused provider |
## Disable Takeover
### Steps
1. Turn off the corresponding app's takeover toggle in the proxy panel
2. Or directly stop the proxy service
### Configuration Restoration
When disabling takeover, CC Switch will:
1. Restore the app configuration to its pre-takeover state
2. Save current request logs
## Takeover and Provider Switching
### Switching Providers in Takeover Mode
When switching providers in takeover mode:
1. Click the "Enable" button on a provider in the main interface
2. The proxy immediately uses the new provider to forward requests
3. **No need to restart the CLI tool**
This is a major advantage of takeover mode: provider switching takes effect instantly.
### Switching Without Takeover
When switching providers without takeover:
1. Configuration file is modified
2. CLI tool must be restarted for changes to take effect
## Multi-app Takeover
Multiple apps can be taken over simultaneously, each managed independently:
- Independent provider configurations
- Independent failover queues
- Independent request statistics
## Use Cases
### Scenario 1: Usage Monitoring
Enable takeover + log recording to monitor API usage.
### Scenario 2: Quick Switching
With takeover enabled, switching providers does not require restarting CLI tools.
### Scenario 3: Failover
Enabling takeover is a prerequisite for using the failover feature.
## Notes
### Performance Impact
The proxy adds minimal latency (typically < 10ms), negligible for most scenarios.
### Network Requirements
In takeover mode, CLI tools must be able to access the local proxy address.
### Configuration Backup
Before enabling takeover, CC Switch backs up the original configuration and restores it when disabled.
## FAQ
### Requests Fail After Takeover
Check:
- Is the proxy service running normally
- Is the provider configuration correct
- Is the network working properly
### Configuration Not Restored After Disabling Takeover
Possible causes:
- Proxy exited abnormally
- Configuration file was modified by another program
Solutions:
- Manually edit the provider and re-save
- Or re-enable and then disable takeover
+3 -4
View File
@@ -277,16 +277,15 @@ CC Switch includes preset official prices for common models (per million tokens)
**Chinese Provider Models**:
> Note: Currency follows each provider's official pricing page. StepFun is currently listed in USD.
>
> **DeepSeek compatibility**: Legacy model IDs `deepseek-chat` / `deepseek-reasoner` now alias to `deepseek-v4-flash` (non-thinking / thinking modes) and are billed at v4-flash rates.
| Model | Input | Output | Cache Read |
|-------|-------|--------|------------|
| **StepFun** | | | |
| step-3.5-flash | $0.10 | $0.30 | $0.02 |
| **DeepSeek** | | | |
| deepseek-v4-flash | ¥1.00 | ¥2.00 | ¥0.20 |
| deepseek-v4-pro | ¥12.00 | ¥24.00 | ¥1.00 |
| deepseek-v3.2 | ¥2.00 | ¥3.00 | ¥0.40 |
| deepseek-v3.1 | ¥4.00 | ¥12.00 | ¥0.80 |
| deepseek-v3 | ¥2.00 | ¥8.00 | ¥0.40 |
| **Kimi (Moonshot)** | | | |
| kimi-k2-thinking | ¥4.00 | ¥16.00 | ¥1.00 |
| kimi-k2 | ¥4.00 | ¥16.00 | ¥1.00 |
+1 -1
View File
@@ -79,7 +79,7 @@ CC Switch User Manual
| File | Description |
|------|-------------|
| [4.1-service.md](./4-proxy/4.1-service.md) | Start proxy, configuration, running status |
| [4.2-routing.md](./4-proxy/4.2-routing.md) | App routing, configuration changes, status indicators |
| [4.2-takeover.md](./4-proxy/4.2-takeover.md) | App takeover, configuration changes, status indicators |
| [4.3-failover.md](./4-proxy/4.3-failover.md) | Failover queue, circuit breaker, health status |
| [4.4-usage.md](./4-proxy/4.4-usage.md) | Usage statistics, trend charts, pricing configuration |
| [4.5-model-test.md](./4-proxy/4.5-model-test.md) | Model test, health check, latency testing |
+1 -1
View File
@@ -509,7 +509,7 @@ Claude プロバイダーの編集時、JSON エディタの上部に **クイ
| **帰属情報を非表示** | コミット/PR の帰属メタデータをクリア | `attribution: {commit: "", pr: ""}` を設定 |
| **チームメイトを有効化** | エージェントチーム機能を有効化 | `env.CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS = "1"` を設定 |
| **ツール検索を有効化** | ツール検索機能を有効化 | `env.ENABLE_TOOL_SEARCH = "true"` を設定 |
| **最大強度思考** | エフォートレベルを max に設定 | `env.CLAUDE_CODE_EFFORT_LEVEL = "max"` を設定 |
| **高強度** | エフォートレベルをに設定 | `effortLevel = "high"` を設定 |
| **自動アップグレードを無効化** | Claude Code の自動更新を防止 | `env.DISABLE_AUTOUPDATER = "1"` を設定 |
トグルのチェックを外すと、対応する設定エントリが完全に削除されます。変更は JSON エディタにリアルタイムで反映されます。
-195
View File
@@ -1,195 +0,0 @@
# 4.2 アプリケーションルーティング
## 機能説明
アプリケーションルーティングとは、CC Switch のルーティングサービスが特定アプリの API リクエストをルーティングすることです。
ルーティングを有効にすると:
- アプリの API リクエストがローカルルーティング経由で転送される
- リクエストログと使用量の統計を記録できる
- フェイルオーバー機能を使用できる
## 前提条件
アプリケーションルーティング機能を使用する前に、ルーティングサービスを起動する必要があります。
## ルーティングの有効化
### 操作場所
設定 → 詳細 → ルーティングサービス → アプリケーションルーティングエリア
### 操作手順
1. ルーティングサービスが起動していることを確認
2. 「アプリケーションルーティング」エリアを見つける
3. 必要なアプリのスイッチをオンにする
### ルーティングスイッチ
| スイッチ | 作用 |
|------|------|
| Claude ルーティング | Claude Code のリクエストをルーティング |
| Codex ルーティング | Codex のリクエストをルーティング |
| Gemini ルーティング | Gemini CLI のリクエストをルーティング |
複数のアプリのルーティングを同時に有効にできます。
## ルーティングの仕組み
### 設定の変更
ルーティングを有効にすると、CC Switch はアプリの設定ファイルを変更し、API エンドポイントをローカルルーティングに向けます。
**Claude 設定の変更**
```json
// ルーティング前
{
"env": {
"ANTHROPIC_BASE_URL": "https://api.anthropic.com"
}
}
// ルーティング後
{
"env": {
"ANTHROPIC_BASE_URL": "http://127.0.0.1:15721"
}
}
```
**Codex 設定の変更**
```toml
# ルーティング前
base_url = "https://api.openai.com/v1"
# ルーティング後
base_url = "http://127.0.0.1:15721/v1"
```
**Gemini 設定の変更**
```bash
# ルーティング前
GOOGLE_GEMINI_BASE_URL=https://generativelanguage.googleapis.com
# ルーティング後
GOOGLE_GEMINI_BASE_URL=http://127.0.0.1:15721
```
### リクエストの転送
ルーティングサービスがリクエストを受信すると:
1. リクエスト元を識別(Claude/Codex/Gemini
2. そのアプリで現在有効なプロバイダーを検索
3. プロバイダーの実際のエンドポイントにリクエストを転送
4. リクエストログを記録
5. アプリにレスポンスを返却
## ルーティングステータスの表示
### メイン画面の表示
ルーティングを有効にすると、メイン画面に以下の変化があります:
- **ルーティング Logo の色**:無色から緑に変化
- **プロバイダーカード**:現在アクティブなプロバイダーに緑の枠が表示
### プロバイダーカードの状態
| 状態 | 枠の色 | 説明 |
|------|----------|------|
| 現在有効 | 青 | 設定ファイル内のプロバイダー(非ルーティングモード) |
| ルーティングアクティブ | 緑 | ルーティングが実際に使用しているプロバイダー |
| 通常 | デフォルト | 使用されていないプロバイダー |
## ルーティングの無効化
### 操作手順
1. ルーティングパネルで対応するアプリのルーティングスイッチをオフにする
2. またはルーティングサービスを直接停止
### 設定の復元
ルーティングを無効にすると、CC Switch は以下を実行します:
1. アプリの設定をルーティング前の状態に復元
2. 現在のリクエストログを保存
## ルーティングとプロバイダーの切り替え
### ルーティングモードでのプロバイダー切り替え
ルーティングモードでプロバイダーを切り替える場合:
1. メイン画面でプロバイダーの「有効化」ボタンをクリック
2. ルーティングサービスが新しいプロバイダーを使用してリクエストを即座に転送
3. **CLI ツールの再起動は不要**
これがルーティングモードの大きなメリットです:プロバイダーの切り替えが即座に反映されます。
### 非ルーティングモードでのプロバイダー切り替え
非ルーティングモードでプロバイダーを切り替える場合:
1. 設定ファイルを変更
2. CLI ツールの再起動が必要
## 複数アプリのルーティング
複数のアプリを同時にルーティングでき、それぞれ独立して管理されます:
- 独立したプロバイダー設定
- 独立したフェイルオーバーキュー
- 独立したリクエスト統計
## 使用シーン
### シーン 1:使用量の監視
ルーティング + ログ記録を有効にして、API の使用状況を監視します。
### シーン 2:素早い切り替え
ルーティングを有効にすると、プロバイダーの切り替えに CLI ツールの再起動が不要になります。
### シーン 3:フェイルオーバー
ルーティングの有効化はフェイルオーバー機能を使用するための前提条件です。
## 注意事項
### パフォーマンスへの影響
ルーティングにより少量のレイテンシ(通常 < 10ms)が追加されますが、ほとんどのシーンでは無視できます。
### ネットワーク要件
ルーティングモードでは、CLI ツールがローカルルーティングアドレスにアクセスできる必要があります。
### 設定のバックアップ
ルーティングを有効にする前に、CC Switch は元の設定をバックアップし、無効化時に復元します。
## よくある質問
### ルーティング後にリクエストが失敗する
確認事項:
- ルーティングサービスが正常に実行されているか
- プロバイダーの設定が正しいか
- ネットワークが正常か
### ルーティングを無効にしても設定が復元されない
考えられる原因:
- ルーティングサービスの異常終了
- 設定ファイルが他のプログラムに変更された
解決方法:
- プロバイダーを手動で編集して保存し直す
- または再度ルーティングを有効にしてから無効にする
+195
View File
@@ -0,0 +1,195 @@
# 4.2 アプリケーション接管
## 機能説明
アプリケーション接管とは、CC Switch のプロキシが特定アプリの API リクエストを接管することです。
接管を有効にすると:
- アプリの API リクエストがローカルプロキシ経由で転送される
- リクエストログと使用量の統計を記録できる
- フェイルオーバー機能を使用できる
## 前提条件
アプリケーション接管機能を使用する前に、プロキシサービスを起動する必要があります。
## 接管の有効化
### 操作場所
設定 → 詳細 → プロキシサービス → アプリケーション接管エリア
### 操作手順
1. プロキシサービスが起動していることを確認
2. 「アプリケーション接管」エリアを見つける
3. 必要なアプリのスイッチをオンにする
### 接管スイッチ
| スイッチ | 作用 |
|------|------|
| Claude 接管 | Claude Code のリクエストを接管 |
| Codex 接管 | Codex のリクエストを接管 |
| Gemini 接管 | Gemini CLI のリクエストを接管 |
複数のアプリの接管を同時に有効にできます。
## 接管の仕組み
### 設定の変更
接管を有効にすると、CC Switch はアプリの設定ファイルを変更し、API エンドポイントをローカルプロキシに向けます。
**Claude 設定の変更**
```json
// 接管前
{
"env": {
"ANTHROPIC_BASE_URL": "https://api.anthropic.com"
}
}
// 接管後
{
"env": {
"ANTHROPIC_BASE_URL": "http://127.0.0.1:15721"
}
}
```
**Codex 設定の変更**
```toml
# 接管前
base_url = "https://api.openai.com/v1"
# 接管後
base_url = "http://127.0.0.1:15721/v1"
```
**Gemini 設定の変更**
```bash
# 接管前
GOOGLE_GEMINI_BASE_URL=https://generativelanguage.googleapis.com
# 接管後
GOOGLE_GEMINI_BASE_URL=http://127.0.0.1:15721
```
### リクエストの転送
プロキシがリクエストを受信すると:
1. リクエスト元を識別(Claude/Codex/Gemini
2. そのアプリで現在有効なプロバイダーを検索
3. プロバイダーの実際のエンドポイントにリクエストを転送
4. リクエストログを記録
5. アプリにレスポンスを返却
## 接管ステータスの表示
### メイン画面の表示
接管を有効にすると、メイン画面に以下の変化があります:
- **プロキシ Logo の色**:無色から緑に変化
- **プロバイダーカード**:現在アクティブなプロバイダーに緑の枠が表示
### プロバイダーカードの状態
| 状態 | 枠の色 | 説明 |
|------|----------|------|
| 現在有効 | 青 | 設定ファイル内のプロバイダー(非プロキシモード) |
| プロキシアクティブ | 緑 | プロキシが実際に使用しているプロバイダー |
| 通常 | デフォルト | 使用されていないプロバイダー |
## 接管の無効化
### 操作手順
1. プロキシパネルで対応するアプリの接管スイッチをオフにする
2. またはプロキシサービスを直接停止
### 設定の復元
接管を無効にすると、CC Switch は以下を実行します:
1. アプリの設定を接管前の状態に復元
2. 現在のリクエストログを保存
## 接管とプロバイダーの切り替え
### 接管モードでのプロバイダー切り替え
接管モードでプロバイダーを切り替える場合:
1. メイン画面でプロバイダーの「有効化」ボタンをクリック
2. プロキシが新しいプロバイダーを使用してリクエストを即座に転送
3. **CLI ツールの再起動は不要**
これが接管モードの大きなメリットです:プロバイダーの切り替えが即座に反映されます。
### 非接管モードでのプロバイダー切り替え
非接管モードでプロバイダーを切り替える場合:
1. 設定ファイルを変更
2. CLI ツールの再起動が必要
## 複数アプリの接管
複数のアプリを同時に接管でき、それぞれ独立して管理されます:
- 独立したプロバイダー設定
- 独立したフェイルオーバーキュー
- 独立したリクエスト統計
## 使用シーン
### シーン 1:使用量の監視
接管 + ログ記録を有効にして、API の使用状況を監視します。
### シーン 2:素早い切り替え
接管を有効にすると、プロバイダーの切り替えに CLI ツールの再起動が不要になります。
### シーン 3:フェイルオーバー
接管の有効化はフェイルオーバー機能を使用するための前提条件です。
## 注意事項
### パフォーマンスへの影響
プロキシにより少量のレイテンシ(通常 < 10ms)が追加されますが、ほとんどのシーンでは無視できます。
### ネットワーク要件
接管モードでは、CLI ツールがローカルプロキシアドレスにアクセスできる必要があります。
### 設定のバックアップ
接管を有効にする前に、CC Switch は元の設定をバックアップし、無効化時に復元します。
## よくある質問
### 接管後にリクエストが失敗する
確認事項:
- プロキシサービスが正常に実行されているか
- プロバイダーの設定が正しいか
- ネットワークが正常か
### 接管を無効にしても設定が復元されない
考えられる原因:
- プロキシの異常終了
- 設定ファイルが他のプログラムに変更された
解決方法:
- プロバイダーを手動で編集して保存し直す
- または接管を再度有効にしてから無効にする
+3 -4
View File
@@ -277,16 +277,15 @@ CC Switch は一般的なモデルの公式価格(100 万 Token あたり)
**中国メーカーのモデル**
> 注: 通貨は各プロバイダーの公式料金ページに従います。StepFun は現在 USD 表記です。
>
> **DeepSeek 互換**: 旧モデル名 `deepseek-chat` / `deepseek-reasoner` は `deepseek-v4-flash`(非思考/思考モード)と等価になり、v4-flash 料金で課金されます。
| モデル | 入力 | 出力 | キャッシュ読取 |
|------|------|------|----------|
| **StepFun** | | | |
| step-3.5-flash | $0.10 | $0.30 | $0.02 |
| **DeepSeek** | | | |
| deepseek-v4-flash | ¥1.00 | ¥2.00 | ¥0.20 |
| deepseek-v4-pro | ¥12.00 | ¥24.00 | ¥1.00 |
| deepseek-v3.2 | ¥2.00 | ¥3.00 | ¥0.40 |
| deepseek-v3.1 | ¥4.00 | ¥12.00 | ¥0.80 |
| deepseek-v3 | ¥2.00 | ¥8.00 | ¥0.40 |
| **Kimi (月之暗面)** | | | |
| kimi-k2-thinking | ¥4.00 | ¥16.00 | ¥1.00 |
| kimi-k2 | ¥4.00 | ¥16.00 | ¥1.00 |
+1 -1
View File
@@ -79,7 +79,7 @@ CC Switch ユーザーマニュアル
| ファイル | 内容 |
|------|------|
| [4.1-service.md](./4-proxy/4.1-service.md) | プロキシの起動、設定項目、実行状態 |
| [4.2-routing.md](./4-proxy/4.2-routing.md) | アプリケーションルーティング、設定変更、ステータス表示 |
| [4.2-takeover.md](./4-proxy/4.2-takeover.md) | アプリケーション接管、設定変更、ステータス表示 |
| [4.3-failover.md](./4-proxy/4.3-failover.md) | フェイルオーバーキュー、サーキットブレーカー、ヘルスステータス |
| [4.4-usage.md](./4-proxy/4.4-usage.md) | 使用量統計、トレンドグラフ、料金設定 |
| [4.5-model-test.md](./4-proxy/4.5-model-test.md) | モデルテスト、ヘルスチェック、レイテンシテスト |
+1 -1
View File
@@ -509,7 +509,7 @@ v3.13.0 起新增的高级选项。默认情况下,CC Switch 会把配置的 `
| **隐藏署名** | 清除提交/PR 的署名元数据 | 设置 `attribution: {commit: "", pr: ""}` |
| **启用 Teammates** | 启用 Agent 团队功能 | 设置 `env.CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS = "1"` |
| **启用工具搜索** | 启用工具搜索功能 | 设置 `env.ENABLE_TOOL_SEARCH = "true"` |
| **最大强度思考** | 将 effort 级别设为 max | 设置 `env.CLAUDE_CODE_EFFORT_LEVEL = "max"` |
| **高效能模式** | 将 effort 级别设为 high | 设置 `effortLevel = "high"` |
| **禁用自动更新** | 阻止 Claude Code 自动更新 | 设置 `env.DISABLE_AUTOUPDATER = "1"` |
取消勾选开关时,对应的配置项会被完全移除。更改会实时反映在 JSON 编辑器中。
-195
View File
@@ -1,195 +0,0 @@
# 4.2 应用路由
## 功能说明
应用路由是指让 CC Switch 路由特定应用的 API 请求。
开启路由后:
- 应用的 API 请求会通过本地路由转发
- 可以记录请求日志和统计用量
- 可以使用故障转移功能
## 前提条件
使用应用路由功能前,需要先启动路由服务。
## 开启路由
### 操作位置
设置 → 高级 → 路由服务 → 应用路由区域
### 操作步骤
1. 确保路由服务已启动
2. 找到「应用路由」区域
3. 为需要的应用开启开关
### 路由开关
| 开关 | 作用 |
|------|------|
| Claude 路由 | 路由 Claude Code 的请求 |
| Codex 路由 | 路由 Codex 的请求 |
| Gemini 路由 | 路由 Gemini CLI 的请求 |
可以同时开启多个应用的路由。
## 路由原理
### 配置修改
开启路由后,CC Switch 会修改应用的配置文件,将 API 端点指向本地路由。
**Claude 配置变更**
```json
// 路由前
{
"env": {
"ANTHROPIC_BASE_URL": "https://api.anthropic.com"
}
}
// 路由后
{
"env": {
"ANTHROPIC_BASE_URL": "http://127.0.0.1:15721"
}
}
```
**Codex 配置变更**
```toml
# 路由前
base_url = "https://api.openai.com/v1"
# 路由后
base_url = "http://127.0.0.1:15721/v1"
```
**Gemini 配置变更**
```bash
# 路由前
GOOGLE_GEMINI_BASE_URL=https://generativelanguage.googleapis.com
# 路由后
GOOGLE_GEMINI_BASE_URL=http://127.0.0.1:15721
```
### 请求转发
路由收到请求后:
1. 识别请求来源(Claude/Codex/Gemini
2. 查找该应用当前启用的供应商
3. 将请求转发到供应商的实际端点
4. 记录请求日志
5. 返回响应给应用
## 路由状态指示
### 主界面指示
开启路由后,主界面会有以下变化:
- **路由 Logo 颜色**:从无色变为绿色
- **供应商卡片**:当前活跃的供应商显示绿色边框
### 供应商卡片状态
| 状态 | 边框颜色 | 说明 |
|------|----------|------|
| 当前启用 | 蓝色 | 配置文件中的供应商(非路由模式) |
| 路由活跃 | 绿色 | 路由实际使用的供应商 |
| 普通 | 默认 | 未使用的供应商 |
## 关闭路由
### 操作步骤
1. 在路由面板中关闭对应应用的路由开关
2. 或直接停止路由服务
### 配置恢复
关闭路由时,CC Switch 会:
1. 将应用配置恢复到路由前的状态
2. 保存当前的请求日志
## 路由与供应商切换
### 路由模式下切换供应商
在路由模式下切换供应商:
1. 在主界面点击供应商的「启用」按钮
2. 路由立即使用新供应商转发请求
3. **无需重启 CLI 工具**
这是路由模式的一大优势:切换供应商即时生效。
### 非路由模式下切换
在非路由模式下切换供应商:
1. 修改配置文件
2. 需要重启 CLI 工具才能生效
## 多应用路由
可以同时路由多个应用,每个应用独立管理:
- 独立的供应商配置
- 独立的故障转移队列
- 独立的请求统计
## 使用场景
### 场景一:用量监控
开启路由 + 日志记录,监控 API 使用情况。
### 场景二:快速切换
开启路由后,切换供应商无需重启 CLI 工具。
### 场景三:故障转移
开启路由是使用故障转移功能的前提。
## 注意事项
### 性能影响
路由会增加少量延迟(通常 < 10ms),对于大多数场景可以忽略。
### 网络要求
路由模式下,CLI 工具需要能够访问本地路由地址。
### 配置备份
开启路由前,CC Switch 会备份原始配置,关闭时恢复。
## 常见问题
### 路由后请求失败
检查:
- 路由服务是否正常运行
- 供应商配置是否正确
- 网络是否正常
### 关闭路由后配置未恢复
可能原因:
- 路由异常退出
- 配置文件被其他程序修改
解决方法:
- 手动编辑供应商,重新保存
- 或重新启用再关闭路由
+195
View File
@@ -0,0 +1,195 @@
# 4.2 应用接管
## 功能说明
应用接管是指让 CC Switch 代理接管特定应用的 API 请求。
开启接管后:
- 应用的 API 请求会通过本地代理转发
- 可以记录请求日志和统计用量
- 可以使用故障转移功能
## 前提条件
使用应用接管功能前,需要先启动代理服务。
## 开启接管
### 操作位置
设置 → 高级 → 代理服务 → 应用接管区域
### 操作步骤
1. 确保代理服务已启动
2. 找到「应用接管」区域
3. 为需要的应用开启开关
### 接管开关
| 开关 | 作用 |
|------|------|
| Claude 接管 | 接管 Claude Code 的请求 |
| Codex 接管 | 接管 Codex 的请求 |
| Gemini 接管 | 接管 Gemini CLI 的请求 |
可以同时开启多个应用的接管。
## 接管原理
### 配置修改
开启接管后,CC Switch 会修改应用的配置文件,将 API 端点指向本地代理。
**Claude 配置变更**
```json
// 接管前
{
"env": {
"ANTHROPIC_BASE_URL": "https://api.anthropic.com"
}
}
// 接管后
{
"env": {
"ANTHROPIC_BASE_URL": "http://127.0.0.1:15721"
}
}
```
**Codex 配置变更**
```toml
# 接管前
base_url = "https://api.openai.com/v1"
# 接管后
base_url = "http://127.0.0.1:15721/v1"
```
**Gemini 配置变更**
```bash
# 接管前
GOOGLE_GEMINI_BASE_URL=https://generativelanguage.googleapis.com
# 接管后
GOOGLE_GEMINI_BASE_URL=http://127.0.0.1:15721
```
### 请求转发
代理收到请求后:
1. 识别请求来源(Claude/Codex/Gemini
2. 查找该应用当前启用的供应商
3. 将请求转发到供应商的实际端点
4. 记录请求日志
5. 返回响应给应用
## 接管状态指示
### 主界面指示
开启接管后,主界面会有以下变化:
- **代理 Logo 颜色**:从无色变为绿色
- **供应商卡片**:当前活跃的供应商显示绿色边框
### 供应商卡片状态
| 状态 | 边框颜色 | 说明 |
|------|----------|------|
| 当前启用 | 蓝色 | 配置文件中的供应商(非代理模式) |
| 代理活跃 | 绿色 | 代理实际使用的供应商 |
| 普通 | 默认 | 未使用的供应商 |
## 关闭接管
### 操作步骤
1. 在代理面板中关闭对应应用的接管开关
2. 或直接停止代理服务
### 配置恢复
关闭接管时,CC Switch 会:
1. 将应用配置恢复到接管前的状态
2. 保存当前的请求日志
## 接管与供应商切换
### 接管模式下切换供应商
在接管模式下切换供应商:
1. 在主界面点击供应商的「启用」按钮
2. 代理立即使用新供应商转发请求
3. **无需重启 CLI 工具**
这是接管模式的一大优势:切换供应商即时生效。
### 非接管模式下切换
在非接管模式下切换供应商:
1. 修改配置文件
2. 需要重启 CLI 工具才能生效
## 多应用接管
可以同时接管多个应用,每个应用独立管理:
- 独立的供应商配置
- 独立的故障转移队列
- 独立的请求统计
## 使用场景
### 场景一:用量监控
开启接管 + 日志记录,监控 API 使用情况。
### 场景二:快速切换
开启接管后,切换供应商无需重启 CLI 工具。
### 场景三:故障转移
开启接管是使用故障转移功能的前提。
## 注意事项
### 性能影响
代理会增加少量延迟(通常 < 10ms),对于大多数场景可以忽略。
### 网络要求
接管模式下,CLI 工具需要能够访问本地代理地址。
### 配置备份
开启接管前,CC Switch 会备份原始配置,关闭时恢复。
## 常见问题
### 接管后请求失败
检查:
- 代理服务是否正常运行
- 供应商配置是否正确
- 网络是否正常
### 关闭接管后配置未恢复
可能原因:
- 代理异常退出
- 配置文件被其他程序修改
解决方法:
- 手动编辑供应商,重新保存
- 或重新启用再关闭接管
+3 -4
View File
@@ -277,16 +277,15 @@ CC Switch 预设了常用模型的官方价格(每百万 Token)。v3.13.0
**中国厂商模型**
> 注:币种遵循各供应商官方定价页面。StepFun 当前按美元列出。
>
> **DeepSeek 兼容**:旧模型名 `deepseek-chat` / `deepseek-reasoner` 现等价于 `deepseek-v4-flash`(非思考/思考模式),按 v4-flash 价格计费。
| 模型 | 输入 | 输出 | 缓存读取 |
|------|------|------|----------|
| **StepFun** | | | |
| step-3.5-flash | $0.10 | $0.30 | $0.02 |
| **DeepSeek** | | | |
| deepseek-v4-flash | ¥1.00 | ¥2.00 | ¥0.20 |
| deepseek-v4-pro | ¥12.00 | ¥24.00 | ¥1.00 |
| deepseek-v3.2 | ¥2.00 | ¥3.00 | ¥0.40 |
| deepseek-v3.1 | ¥4.00 | ¥12.00 | ¥0.80 |
| deepseek-v3 | ¥2.00 | ¥8.00 | ¥0.40 |
| **Kimi (月之暗面)** | | | |
| kimi-k2-thinking | ¥4.00 | ¥16.00 | ¥1.00 |
| kimi-k2 | ¥4.00 | ¥16.00 | ¥1.00 |
+1 -1
View File
@@ -79,7 +79,7 @@
| 文件 | 内容 |
|------|------|
| [4.1-service.md](./4-proxy/4.1-service.md) | 启动代理、配置项、运行状态 |
| [4.2-routing.md](./4-proxy/4.2-routing.md) | 应用路由、配置修改、状态指示 |
| [4.2-takeover.md](./4-proxy/4.2-takeover.md) | 应用接管、配置修改、状态指示 |
| [4.3-failover.md](./4-proxy/4.3-failover.md) | 故障转移队列、熔断器、健康状态 |
| [4.4-usage.md](./4-proxy/4.4-usage.md) | 用量统计、趋势图表、定价配置 |
| [4.5-model-test.md](./4-proxy/4.5-model-test.md) | 模型检查、健康检测、延迟测试 |
+1 -2
View File
@@ -1,6 +1,6 @@
{
"name": "cc-switch",
"version": "3.15.0",
"version": "3.13.0",
"description": "All-in-One Assistant for Claude Code, Codex & Gemini CLI",
"type": "module",
"scripts": {
@@ -67,7 +67,6 @@
"@radix-ui/react-tooltip": "^1.2.8",
"@radix-ui/react-visually-hidden": "^1.2.4",
"@tanstack/react-query": "^5.90.3",
"@tanstack/react-virtual": "^3.13.23",
"@tauri-apps/api": "^2.8.0",
"@tauri-apps/plugin-dialog": "^2.4.0",
"@tauri-apps/plugin-process": "^2.0.0",
-20
View File
@@ -89,9 +89,6 @@ importers:
'@tanstack/react-query':
specifier: ^5.90.3
version: 5.90.3(react@18.3.1)
'@tanstack/react-virtual':
specifier: ^3.13.23
version: 3.13.23(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
'@tauri-apps/api':
specifier: ^2.8.0
version: 2.8.0
@@ -1471,15 +1468,6 @@ packages:
peerDependencies:
react: ^18 || ^19
'@tanstack/react-virtual@3.13.23':
resolution: {integrity: sha512-XnMRnHQ23piOVj2bzJqHrRrLg4r+F86fuBcwteKfbIjJrtGxb4z7tIvPVAe4B+4UVwo9G4Giuz5fmapcrnZ0OQ==}
peerDependencies:
react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
'@tanstack/virtual-core@3.13.23':
resolution: {integrity: sha512-zSz2Z2HNyLjCplANTDyl3BcdQJc2k1+yyFoKhNRmCr7V7dY8o8q5m8uFTI1/Pg1kL+Hgrz6u3Xo6eFUB7l66cg==}
'@tauri-apps/api@2.8.0':
resolution: {integrity: sha512-ga7zdhbS2GXOMTIZRT0mYjKJtR9fivsXzsyq5U3vjDL0s6DTMwYRm0UHNjzTY5dh4+LSC68Sm/7WEiimbQNYlw==}
@@ -4287,14 +4275,6 @@ snapshots:
'@tanstack/query-core': 5.90.3
react: 18.3.1
'@tanstack/react-virtual@3.13.23(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
dependencies:
'@tanstack/virtual-core': 3.13.23
react: 18.3.1
react-dom: 18.3.1(react@18.3.1)
'@tanstack/virtual-core@3.13.23': {}
'@tauri-apps/api@2.8.0': {}
'@tauri-apps/cli-darwin-arm64@2.8.1':
-4
View File
@@ -1,4 +0,0 @@
[toolchain]
channel = "1.95"
components = ["rustfmt", "clippy"]
profile = "minimal"
+1 -17
View File
@@ -735,7 +735,7 @@ dependencies = [
[[package]]
name = "cc-switch"
version = "3.15.0"
version = "3.13.0"
dependencies = [
"anyhow",
"arboard",
@@ -785,7 +785,6 @@ dependencies = [
"tauri-plugin-single-instance",
"tauri-plugin-store",
"tauri-plugin-updater",
"tauri-plugin-window-state",
"tempfile",
"thiserror 2.0.18",
"tokio",
@@ -5688,21 +5687,6 @@ dependencies = [
"zip 4.6.1",
]
[[package]]
name = "tauri-plugin-window-state"
version = "2.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "73736611e14142408d15353e21e3cca2f12a3cfb523ad0ce85999b6d2ef1a704"
dependencies = [
"bitflags 2.11.0",
"log",
"serde",
"serde_json",
"tauri",
"tauri-plugin",
"thiserror 2.0.18",
]
[[package]]
name = "tauri-runtime"
version = "2.10.1"
+1 -2
View File
@@ -1,6 +1,6 @@
[package]
name = "cc-switch"
version = "3.15.0"
version = "3.13.0"
description = "All-in-One Assistant for Claude Code, Codex & Gemini CLI"
authors = ["Jason Young"]
license = "MIT"
@@ -35,7 +35,6 @@ tauri-plugin-updater = "2"
tauri-plugin-dialog = "2"
tauri-plugin-store = "2"
tauri-plugin-deep-link = "2"
tauri-plugin-window-state = "2"
dirs = "5.0"
toml = "0.8"
toml_edit = "0.22"
-5
View File
@@ -11,11 +11,6 @@
"updater:default",
"core:window:allow-set-skip-taskbar",
"core:window:allow-start-dragging",
"core:window:allow-minimize",
"core:window:allow-toggle-maximize",
"core:window:allow-is-maximized",
"core:window:allow-close",
"core:window:allow-set-decorations",
"process:allow-restart",
"dialog:default"
]
+7 -101
View File
@@ -15,8 +15,6 @@ pub struct McpApps {
pub gemini: bool,
#[serde(default)]
pub opencode: bool,
#[serde(default)]
pub hermes: bool,
}
impl McpApps {
@@ -28,8 +26,6 @@ impl McpApps {
AppType::Gemini => self.gemini,
AppType::OpenCode => self.opencode,
AppType::OpenClaw => false, // OpenClaw doesn't support MCP
AppType::Hermes => self.hermes,
AppType::ClaudeDesktop => false,
}
}
@@ -41,8 +37,6 @@ impl McpApps {
AppType::Gemini => self.gemini = enabled,
AppType::OpenCode => self.opencode = enabled,
AppType::OpenClaw => {} // OpenClaw doesn't support MCP, ignore
AppType::Hermes => self.hermes = enabled,
AppType::ClaudeDesktop => {} // Claude Desktop 3P provider config doesn't support MCP here
}
}
@@ -61,15 +55,12 @@ impl McpApps {
if self.opencode {
apps.push(AppType::OpenCode);
}
if self.hermes {
apps.push(AppType::Hermes);
}
apps
}
/// 检查是否所有应用都未启用
pub fn is_empty(&self) -> bool {
!self.claude && !self.codex && !self.gemini && !self.opencode && !self.hermes
!self.claude && !self.codex && !self.gemini && !self.opencode
}
}
@@ -84,8 +75,6 @@ pub struct SkillApps {
pub gemini: bool,
#[serde(default)]
pub opencode: bool,
#[serde(default)]
pub hermes: bool,
}
impl SkillApps {
@@ -96,9 +85,7 @@ impl SkillApps {
AppType::Codex => self.codex,
AppType::Gemini => self.gemini,
AppType::OpenCode => self.opencode,
AppType::Hermes => self.hermes,
AppType::OpenClaw => false, // OpenClaw doesn't support Skills
AppType::ClaudeDesktop => false,
}
}
@@ -109,9 +96,7 @@ impl SkillApps {
AppType::Codex => self.codex = enabled,
AppType::Gemini => self.gemini = enabled,
AppType::OpenCode => self.opencode = enabled,
AppType::Hermes => self.hermes = enabled,
AppType::OpenClaw => {} // OpenClaw doesn't support Skills, ignore
AppType::ClaudeDesktop => {} // Claude Desktop 3P profiles don't use CC Switch skill sync
}
}
@@ -130,15 +115,12 @@ impl SkillApps {
if self.opencode {
apps.push(AppType::OpenCode);
}
if self.hermes {
apps.push(AppType::Hermes);
}
apps
}
/// 检查是否所有应用都未启用
pub fn is_empty(&self) -> bool {
!self.claude && !self.codex && !self.gemini && !self.opencode && !self.hermes
!self.claude && !self.codex && !self.gemini && !self.opencode
}
/// 仅启用指定应用(其他应用设为禁用)
@@ -259,14 +241,6 @@ pub struct McpRoot {
/// 旧的分应用存储(v3.6.x 及以前,保留用于迁移)
#[serde(default, skip_serializing_if = "McpConfig::is_empty")]
pub claude: McpConfig,
#[serde(
rename = "claude-desktop",
alias = "claudeDesktop",
alias = "claude_desktop",
default,
skip_serializing_if = "McpConfig::is_empty"
)]
pub claude_desktop: McpConfig,
#[serde(default, skip_serializing_if = "McpConfig::is_empty")]
pub codex: McpConfig,
#[serde(default, skip_serializing_if = "McpConfig::is_empty")]
@@ -277,9 +251,6 @@ pub struct McpRoot {
/// OpenClaw MCP 配置(v4.1.0+,实际使用 openclaw.json
#[serde(default, skip_serializing_if = "McpConfig::is_empty")]
pub openclaw: McpConfig,
/// Hermes MCP 配置(实际使用 config.yaml
#[serde(default, skip_serializing_if = "McpConfig::is_empty")]
pub hermes: McpConfig,
}
impl Default for McpRoot {
@@ -289,12 +260,10 @@ impl Default for McpRoot {
servers: Some(HashMap::new()),
// 旧结构保持空,仅用于反序列化旧配置时的迁移
claude: McpConfig::default(),
claude_desktop: McpConfig::default(),
codex: McpConfig::default(),
gemini: McpConfig::default(),
opencode: McpConfig::default(),
openclaw: McpConfig::default(),
hermes: McpConfig::default(),
}
}
}
@@ -311,13 +280,6 @@ pub struct PromptConfig {
pub struct PromptRoot {
#[serde(default)]
pub claude: PromptConfig,
#[serde(
rename = "claude-desktop",
alias = "claudeDesktop",
alias = "claude_desktop",
default
)]
pub claude_desktop: PromptConfig,
#[serde(default)]
pub codex: PromptConfig,
#[serde(default)]
@@ -326,8 +288,6 @@ pub struct PromptRoot {
pub opencode: PromptConfig,
#[serde(default)]
pub openclaw: PromptConfig,
#[serde(default)]
pub hermes: PromptConfig,
}
use crate::config::{copy_file, get_app_config_dir, get_app_config_path, write_json_file};
@@ -336,57 +296,43 @@ use crate::prompt_files::prompt_file_path;
use crate::provider::ProviderManager;
/// 应用类型
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum AppType {
Claude,
#[serde(
rename = "claude-desktop",
alias = "claude_desktop",
alias = "claudeDesktop"
)]
ClaudeDesktop,
Codex,
Gemini,
OpenCode,
OpenClaw,
Hermes,
}
impl AppType {
pub fn as_str(&self) -> &str {
match self {
AppType::Claude => "claude",
AppType::ClaudeDesktop => "claude-desktop",
AppType::Codex => "codex",
AppType::Gemini => "gemini",
AppType::OpenCode => "opencode",
AppType::OpenClaw => "openclaw",
AppType::Hermes => "hermes",
}
}
/// Check if this app uses additive mode
///
/// - Switch mode (false): Only the current provider is written to live config (Claude, Codex, Gemini)
/// - Additive mode (true): All providers are written to live config (OpenCode, OpenClaw, Hermes)
/// - Additive mode (true): All providers are written to live config (OpenCode, OpenClaw)
pub fn is_additive_mode(&self) -> bool {
matches!(
self,
AppType::OpenCode | AppType::OpenClaw | AppType::Hermes
)
matches!(self, AppType::OpenCode | AppType::OpenClaw)
}
/// Return an iterator over all app types
pub fn all() -> impl Iterator<Item = AppType> {
[
AppType::Claude,
AppType::ClaudeDesktop,
AppType::Codex,
AppType::Gemini,
AppType::OpenCode,
AppType::OpenClaw,
AppType::Hermes,
]
.into_iter()
}
@@ -399,16 +345,14 @@ impl FromStr for AppType {
let normalized = s.trim().to_lowercase();
match normalized.as_str() {
"claude" => Ok(AppType::Claude),
"claude-desktop" | "claude_desktop" | "claudedesktop" => Ok(AppType::ClaudeDesktop),
"codex" => Ok(AppType::Codex),
"gemini" => Ok(AppType::Gemini),
"opencode" => Ok(AppType::OpenCode),
"openclaw" => Ok(AppType::OpenClaw),
"hermes" => Ok(AppType::Hermes),
other => Err(AppError::localized(
"unsupported_app",
format!("不支持的应用标识: '{other}'。可选值: claude, claude-desktop, codex, gemini, opencode, openclaw, hermes"),
format!("Unsupported app id: '{other}'. Allowed: claude, claude-desktop, codex, gemini, opencode, openclaw, hermes."),
format!("不支持的应用标识: '{other}'。可选值: claude, codex, gemini, opencode, openclaw。"),
format!("Unsupported app id: '{other}'. Allowed: claude, codex, gemini, opencode, openclaw."),
)),
}
}
@@ -431,9 +375,6 @@ pub struct CommonConfigSnippets {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub openclaw: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub hermes: Option<String>,
}
impl CommonConfigSnippets {
@@ -441,12 +382,10 @@ impl CommonConfigSnippets {
pub fn get(&self, app: &AppType) -> Option<&String> {
match app {
AppType::Claude => self.claude.as_ref(),
AppType::ClaudeDesktop => None,
AppType::Codex => self.codex.as_ref(),
AppType::Gemini => self.gemini.as_ref(),
AppType::OpenCode => self.opencode.as_ref(),
AppType::OpenClaw => self.openclaw.as_ref(),
AppType::Hermes => self.hermes.as_ref(),
}
}
@@ -454,12 +393,10 @@ impl CommonConfigSnippets {
pub fn set(&mut self, app: &AppType, snippet: Option<String>) {
match app {
AppType::Claude => self.claude = snippet,
AppType::ClaudeDesktop => {}
AppType::Codex => self.codex = snippet,
AppType::Gemini => self.gemini = snippet,
AppType::OpenCode => self.opencode = snippet,
AppType::OpenClaw => self.openclaw = snippet,
AppType::Hermes => self.hermes = snippet,
}
}
}
@@ -497,12 +434,10 @@ impl Default for MultiAppConfig {
fn default() -> Self {
let mut apps = HashMap::new();
apps.insert("claude".to_string(), ProviderManager::default());
apps.insert("claude-desktop".to_string(), ProviderManager::default());
apps.insert("codex".to_string(), ProviderManager::default());
apps.insert("gemini".to_string(), ProviderManager::default());
apps.insert("opencode".to_string(), ProviderManager::default());
apps.insert("openclaw".to_string(), ProviderManager::default());
apps.insert("hermes".to_string(), ProviderManager::default());
Self {
version: 2,
@@ -659,12 +594,10 @@ impl MultiAppConfig {
pub fn mcp_for(&self, app: &AppType) -> &McpConfig {
match app {
AppType::Claude => &self.mcp.claude,
AppType::ClaudeDesktop => &self.mcp.claude_desktop,
AppType::Codex => &self.mcp.codex,
AppType::Gemini => &self.mcp.gemini,
AppType::OpenCode => &self.mcp.opencode,
AppType::OpenClaw => &self.mcp.openclaw,
AppType::Hermes => &self.mcp.hermes,
}
}
@@ -672,12 +605,10 @@ impl MultiAppConfig {
pub fn mcp_for_mut(&mut self, app: &AppType) -> &mut McpConfig {
match app {
AppType::Claude => &mut self.mcp.claude,
AppType::ClaudeDesktop => &mut self.mcp.claude_desktop,
AppType::Codex => &mut self.mcp.codex,
AppType::Gemini => &mut self.mcp.gemini,
AppType::OpenCode => &mut self.mcp.opencode,
AppType::OpenClaw => &mut self.mcp.openclaw,
AppType::Hermes => &mut self.mcp.hermes,
}
}
@@ -693,7 +624,6 @@ impl MultiAppConfig {
Self::auto_import_prompt_if_exists(&mut config, AppType::Gemini)?;
Self::auto_import_prompt_if_exists(&mut config, AppType::OpenCode)?;
Self::auto_import_prompt_if_exists(&mut config, AppType::OpenClaw)?;
Self::auto_import_prompt_if_exists(&mut config, AppType::Hermes)?;
Ok(config)
}
@@ -711,12 +641,10 @@ impl MultiAppConfig {
fn maybe_auto_import_prompts_for_existing_config(&mut self) -> Result<bool, AppError> {
// 如果任一应用已经有提示词配置,说明用户已经在使用 Prompt 功能,避免再次自动导入
if !self.prompts.claude.prompts.is_empty()
|| !self.prompts.claude_desktop.prompts.is_empty()
|| !self.prompts.codex.prompts.is_empty()
|| !self.prompts.gemini.prompts.is_empty()
|| !self.prompts.opencode.prompts.is_empty()
|| !self.prompts.openclaw.prompts.is_empty()
|| !self.prompts.hermes.prompts.is_empty()
{
return Ok(false);
}
@@ -730,7 +658,6 @@ impl MultiAppConfig {
AppType::Gemini,
AppType::OpenCode,
AppType::OpenClaw,
AppType::Hermes,
] {
// 复用已有的单应用导入逻辑
if Self::auto_import_prompt_if_exists(self, app)? {
@@ -798,12 +725,10 @@ impl MultiAppConfig {
// 插入到对应的应用配置中
let prompts = match app {
AppType::Claude => &mut config.prompts.claude.prompts,
AppType::ClaudeDesktop => &mut config.prompts.claude_desktop.prompts,
AppType::Codex => &mut config.prompts.codex.prompts,
AppType::Gemini => &mut config.prompts.gemini.prompts,
AppType::OpenCode => &mut config.prompts.opencode.prompts,
AppType::OpenClaw => &mut config.prompts.openclaw.prompts,
AppType::Hermes => &mut config.prompts.hermes.prompts,
};
prompts.insert(id, prompt);
@@ -840,12 +765,10 @@ impl MultiAppConfig {
] {
let old_servers = match app {
AppType::Claude => &self.mcp.claude.servers,
AppType::ClaudeDesktop => continue, // Claude Desktop 3P profiles don't use MCP here
AppType::Codex => &self.mcp.codex.servers,
AppType::Gemini => &self.mcp.gemini.servers,
AppType::OpenCode => &self.mcp.opencode.servers,
AppType::OpenClaw => continue, // OpenClaw MCP is still in development, skip
AppType::Hermes => continue, // Hermes didn't exist in v3.6.x, skip
};
for (id, entry) in old_servers {
@@ -961,23 +884,6 @@ mod tests {
use std::fs;
use tempfile::TempDir;
#[test]
fn app_type_parses_claude_desktop_aliases() {
assert_eq!(
"claude-desktop".parse::<AppType>().unwrap(),
AppType::ClaudeDesktop
);
assert_eq!(
"claude_desktop".parse::<AppType>().unwrap(),
AppType::ClaudeDesktop
);
assert_eq!(
"claudeDesktop".parse::<AppType>().unwrap(),
AppType::ClaudeDesktop
);
assert_eq!(AppType::ClaudeDesktop.as_str(), "claude-desktop");
}
struct TempHome {
#[allow(dead_code)] // 字段通过 Drop trait 管理临时目录生命周期
dir: TempDir,
File diff suppressed because it is too large Load Diff
-511
View File
@@ -11,20 +11,6 @@ use std::fs;
use std::path::Path;
use toml_edit::DocumentMut;
pub const CC_SWITCH_CODEX_MODEL_PROVIDER_ID: &str = "ccswitch";
/// Reserved built-in provider IDs from OpenAI Codex's config/model-provider
/// catalog. Keep in sync with Codex `RESERVED_MODEL_PROVIDER_IDS` and legacy
/// removed provider aliases.
const CODEX_RESERVED_MODEL_PROVIDER_IDS: &[&str] = &[
"amazon-bedrock",
"openai",
"ollama",
"lmstudio",
"oss",
"ollama-chat",
];
/// 获取 Codex 配置目录路径
pub fn get_codex_config_dir() -> PathBuf {
if let Some(custom) = crate::settings::get_codex_override_dir() {
@@ -151,268 +137,6 @@ pub fn read_and_validate_codex_config_text() -> Result<String, AppError> {
Ok(s)
}
fn active_codex_model_provider_id(doc: &DocumentMut) -> Option<String> {
doc.get("model_provider")
.and_then(|item| item.as_str())
.map(str::trim)
.filter(|id| !id.is_empty())
.map(str::to_string)
}
fn is_custom_codex_model_provider_id(id: &str) -> bool {
let id = id.trim();
!id.is_empty()
&& !CODEX_RESERVED_MODEL_PROVIDER_IDS
.iter()
.any(|reserved| reserved.eq_ignore_ascii_case(id))
}
fn stable_codex_model_provider_id_from_config(config_text: &str) -> Option<String> {
let doc = config_text.parse::<DocumentMut>().ok()?;
let provider_id = active_codex_model_provider_id(&doc)?;
if is_custom_codex_model_provider_id(&provider_id) {
Some(provider_id)
} else {
None
}
}
fn codex_model_provider_id_with_table_from_config(
config_text: &str,
) -> Result<Option<String>, AppError> {
if config_text.trim().is_empty() {
return Ok(None);
}
let doc = config_text
.parse::<DocumentMut>()
.map_err(|e| AppError::Message(format!("Invalid Codex config.toml: {e}")))?;
let Some(provider_id) = active_codex_model_provider_id(&doc) else {
return Ok(None);
};
let has_provider_table = doc
.get("model_providers")
.and_then(|item| item.as_table())
.and_then(|table| table.get(provider_id.as_str()))
.is_some();
Ok(has_provider_table.then_some(provider_id))
}
fn normalize_codex_live_config_model_provider_with_anchors<'a>(
config_text: &str,
anchor_config_texts: impl IntoIterator<Item = &'a str>,
) -> Result<String, AppError> {
if config_text.trim().is_empty() {
return Ok(config_text.to_string());
}
let mut doc = config_text
.parse::<DocumentMut>()
.map_err(|e| AppError::Message(format!("Invalid Codex config.toml: {e}")))?;
let Some(source_provider_id) = active_codex_model_provider_id(&doc) else {
return Ok(config_text.to_string());
};
let has_source_provider_table = doc
.get("model_providers")
.and_then(|item| item.as_table())
.and_then(|table| table.get(source_provider_id.as_str()))
.is_some();
if !has_source_provider_table {
return Ok(config_text.to_string());
}
let stable_provider_id = anchor_config_texts
.into_iter()
.find_map(stable_codex_model_provider_id_from_config)
.or_else(|| {
is_custom_codex_model_provider_id(&source_provider_id)
.then(|| source_provider_id.clone())
})
.unwrap_or_else(|| CC_SWITCH_CODEX_MODEL_PROVIDER_ID.to_string());
if stable_provider_id == source_provider_id {
return Ok(config_text.to_string());
}
if let Some(model_providers) = doc
.get_mut("model_providers")
.and_then(|item| item.as_table_mut())
{
let Some(provider_table) = model_providers.remove(source_provider_id.as_str()) else {
return Ok(config_text.to_string());
};
model_providers[stable_provider_id.as_str()] = provider_table;
}
rewrite_codex_profile_model_provider_refs(&mut doc, &source_provider_id, &stable_provider_id);
doc["model_provider"] = toml_edit::value(stable_provider_id.as_str());
Ok(doc.to_string())
}
fn rewrite_codex_profile_model_provider_refs(
doc: &mut DocumentMut,
source_provider_id: &str,
stable_provider_id: &str,
) {
let Some(profiles) = doc
.get_mut("profiles")
.and_then(|item| item.as_table_like_mut())
else {
return;
};
let profile_keys: Vec<String> = profiles.iter().map(|(key, _)| key.to_string()).collect();
for profile_key in profile_keys {
let Some(profile_table) = profiles
.get_mut(&profile_key)
.and_then(|item| item.as_table_like_mut())
else {
continue;
};
let references_source = profile_table
.get("model_provider")
.and_then(|item| item.as_str())
== Some(source_provider_id);
if references_source {
profile_table.insert("model_provider", toml_edit::value(stable_provider_id));
}
}
}
/// Keep Codex's active `model_provider` stable across CC Switch provider changes.
///
/// Codex stores and filters resume history by `model_provider`, so switching between
/// provider-specific ids like `rightcode` and `aihubmix` makes history appear to move.
/// We preserve an existing custom provider id when possible and only rewrite the
/// live config text that Codex sees at provider-driven write boundaries.
pub fn normalize_codex_settings_config_model_provider(
settings: &mut Value,
anchor_config_text: Option<&str>,
) -> Result<(), AppError> {
let Some(config_text) = settings
.get("config")
.and_then(|value| value.as_str())
.map(str::to_string)
else {
return Ok(());
};
let current_config_text = read_codex_config_text().ok();
let anchors = anchor_config_text
.into_iter()
.chain(current_config_text.as_deref());
let normalized =
normalize_codex_live_config_model_provider_with_anchors(&config_text, anchors)?;
if let Some(obj) = settings.as_object_mut() {
obj.insert("config".to_string(), Value::String(normalized));
}
Ok(())
}
fn restore_codex_backfill_model_provider_id(
config_text: &str,
template_config_text: &str,
) -> Result<String, AppError> {
let Some(template_provider_id) =
codex_model_provider_id_with_table_from_config(template_config_text)?
else {
return Ok(config_text.to_string());
};
if config_text.trim().is_empty() {
return Ok(config_text.to_string());
}
let mut doc = config_text
.parse::<DocumentMut>()
.map_err(|e| AppError::Message(format!("Invalid Codex config.toml: {e}")))?;
let Some(live_provider_id) = active_codex_model_provider_id(&doc) else {
return Ok(config_text.to_string());
};
if live_provider_id == template_provider_id {
return Ok(config_text.to_string());
}
if let Some(model_providers) = doc
.get_mut("model_providers")
.and_then(|item| item.as_table_mut())
{
let Some(provider_table) = model_providers.remove(live_provider_id.as_str()) else {
return Ok(config_text.to_string());
};
model_providers[template_provider_id.as_str()] = provider_table;
} else {
return Ok(config_text.to_string());
}
rewrite_codex_profile_model_provider_refs(&mut doc, &live_provider_id, &template_provider_id);
doc["model_provider"] = toml_edit::value(template_provider_id.as_str());
Ok(doc.to_string())
}
/// Convert a Codex live config that was normalized for history stability back
/// to the provider-specific id used by the stored provider template.
pub fn restore_codex_settings_config_model_provider_for_backfill(
settings: &mut Value,
template_settings: &Value,
) -> Result<(), AppError> {
let Some(config_text) = settings
.get("config")
.and_then(|value| value.as_str())
.map(str::to_string)
else {
return Ok(());
};
let Some(template_config_text) = template_settings
.get("config")
.and_then(|value| value.as_str())
else {
return Ok(());
};
let restored = restore_codex_backfill_model_provider_id(&config_text, template_config_text)?;
if let Some(obj) = settings.as_object_mut() {
obj.insert("config".to_string(), Value::String(restored));
}
Ok(())
}
/// Atomically write Codex live config after normalizing provider-specific ids.
///
/// Use this for provider-driven live writes. Keep `write_codex_live_atomic` available
/// for exact restore/backup paths that must preserve the config text byte-for-byte.
pub fn write_codex_live_atomic_with_stable_provider(
auth: &Value,
config_text_opt: Option<&str>,
) -> Result<(), AppError> {
match config_text_opt {
Some(config_text) => {
let mut settings = serde_json::Map::new();
settings.insert("config".to_string(), Value::String(config_text.to_string()));
let mut settings = Value::Object(settings);
normalize_codex_settings_config_model_provider(&mut settings, None)?;
let config_text = settings
.get("config")
.and_then(|value| value.as_str())
.unwrap_or(config_text);
write_codex_live_atomic(auth, Some(config_text))
}
None => write_codex_live_atomic(auth, None),
}
}
/// Update a field in Codex config.toml using toml_edit (syntax-preserving).
///
/// Supported fields:
@@ -530,241 +254,6 @@ pub fn remove_codex_toml_base_url_if(toml_str: &str, predicate: impl Fn(&str) ->
mod tests {
use super::*;
#[test]
fn normalize_live_config_preserves_current_custom_model_provider_id() {
let current = r#"model_provider = "rightcode"
[model_providers.rightcode]
name = "RightCode"
base_url = "https://rightcode.example/v1"
wire_api = "responses"
"#;
let target = r#"model_provider = "aihubmix"
model = "gpt-5.4"
[model_providers.aihubmix]
name = "AiHubMix"
base_url = "https://aihubmix.example/v1"
wire_api = "responses"
requires_openai_auth = true
[mcp_servers.context7]
command = "npx"
"#;
let result =
normalize_codex_live_config_model_provider_with_anchors(target, Some(current)).unwrap();
let parsed: toml::Value = toml::from_str(&result).unwrap();
assert_eq!(
parsed.get("model_provider").and_then(|v| v.as_str()),
Some("rightcode")
);
let model_providers = parsed
.get("model_providers")
.and_then(|v| v.as_table())
.expect("model_providers should exist");
assert!(
model_providers.get("aihubmix").is_none(),
"source provider id should not remain in live config"
);
let stable_provider = model_providers
.get("rightcode")
.expect("stable provider table should exist");
assert_eq!(
stable_provider.get("base_url").and_then(|v| v.as_str()),
Some("https://aihubmix.example/v1")
);
assert!(
parsed.get("mcp_servers").is_some(),
"unrelated config should be preserved"
);
}
#[test]
fn normalize_live_config_uses_target_custom_provider_when_current_is_reserved() {
let current = r#"model_provider = "openai""#;
let target = r#"model_provider = "aihubmix"
[model_providers.aihubmix]
name = "AiHubMix"
base_url = "https://aihubmix.example/v1"
wire_api = "responses"
"#;
let result =
normalize_codex_live_config_model_provider_with_anchors(target, Some(current)).unwrap();
let parsed: toml::Value = toml::from_str(&result).unwrap();
assert_eq!(
parsed.get("model_provider").and_then(|v| v.as_str()),
Some("aihubmix")
);
assert!(
parsed
.get("model_providers")
.and_then(|v| v.get("aihubmix"))
.is_some(),
"target provider id should be kept when there is no reusable live custom id"
);
}
#[test]
fn normalize_live_config_leaves_official_empty_config_unchanged() {
let current = r#"model_provider = "rightcode"
[model_providers.rightcode]
base_url = "https://rightcode.example/v1"
"#;
let result =
normalize_codex_live_config_model_provider_with_anchors("", Some(current)).unwrap();
assert_eq!(result, "");
}
#[test]
fn normalize_live_config_rewrites_matching_profile_model_provider_refs() {
let current = r#"model_provider = "session_anchor"
[model_providers.session_anchor]
name = "Session Anchor"
base_url = "https://anchor.example/v1"
wire_api = "responses"
"#;
let target = r#"model_provider = "vendor_alpha"
model = "gpt-5.4"
profile = "work"
[model_providers.vendor_alpha]
name = "Vendor Alpha"
base_url = "https://alpha.example/v1"
wire_api = "responses"
[profiles.work]
model_provider = "vendor_alpha"
model = "gpt-5.4"
"#;
let result =
normalize_codex_live_config_model_provider_with_anchors(target, Some(current)).unwrap();
let parsed: toml::Value = toml::from_str(&result).unwrap();
assert_eq!(
parsed.get("model_provider").and_then(|v| v.as_str()),
Some("session_anchor")
);
assert_eq!(
parsed
.get("profiles")
.and_then(|v| v.get("work"))
.and_then(|v| v.get("model_provider"))
.and_then(|v| v.as_str()),
Some("session_anchor"),
"profile override matching the rewritten provider should stay valid"
);
}
#[test]
fn normalize_live_config_keeps_unrelated_profile_model_provider_refs() {
let current = r#"model_provider = "session_anchor"
[model_providers.session_anchor]
name = "Session Anchor"
base_url = "https://anchor.example/v1"
wire_api = "responses"
"#;
let target = r#"model_provider = "vendor_alpha"
model = "gpt-5.4"
[model_providers.vendor_alpha]
name = "Vendor Alpha"
base_url = "https://alpha.example/v1"
wire_api = "responses"
[model_providers.local_profile]
name = "Local Profile"
base_url = "http://localhost:11434/v1"
wire_api = "responses"
[profiles.local]
model_provider = "local_profile"
model = "local-model"
"#;
let result =
normalize_codex_live_config_model_provider_with_anchors(target, Some(current)).unwrap();
let parsed: toml::Value = toml::from_str(&result).unwrap();
assert_eq!(
parsed
.get("profiles")
.and_then(|v| v.get("local"))
.and_then(|v| v.get("model_provider"))
.and_then(|v| v.as_str()),
Some("local_profile"),
"unrelated profile provider references should be preserved"
);
assert!(
parsed
.get("model_providers")
.and_then(|v| v.get("local_profile"))
.is_some(),
"unrelated provider tables should also remain available"
);
}
#[test]
fn normalize_live_config_keeps_stable_provider_across_repeated_switches() {
let anchor = r#"model_provider = "session_anchor"
[model_providers.session_anchor]
name = "Session Anchor"
base_url = "https://anchor.example/v1"
wire_api = "responses"
"#;
let first_target = r#"model_provider = "vendor_alpha"
[model_providers.vendor_alpha]
name = "Vendor Alpha"
base_url = "https://alpha.example/v1"
wire_api = "responses"
"#;
let second_target = r#"model_provider = "vendor_beta"
[model_providers.vendor_beta]
name = "Vendor Beta"
base_url = "https://beta.example/v1"
wire_api = "responses"
"#;
let first =
normalize_codex_live_config_model_provider_with_anchors(first_target, Some(anchor))
.unwrap();
let second = normalize_codex_live_config_model_provider_with_anchors(
second_target,
Some(first.as_str()),
)
.unwrap();
let parsed: toml::Value = toml::from_str(&second).unwrap();
assert_eq!(
parsed.get("model_provider").and_then(|v| v.as_str()),
Some("session_anchor"),
"stable provider id should not drift across repeated switches"
);
assert_eq!(
parsed
.get("model_providers")
.and_then(|v| v.get("session_anchor"))
.and_then(|v| v.get("base_url"))
.and_then(|v| v.as_str()),
Some("https://beta.example/v1")
);
}
#[test]
fn base_url_writes_into_correct_model_provider_section() {
let input = r#"model_provider = "any"
+2 -9
View File
@@ -18,7 +18,6 @@ pub struct ManagedAuthAccount {
pub avatar_url: Option<String>,
pub authenticated_at: i64,
pub is_default: bool,
pub github_domain: String,
}
#[derive(Debug, Clone, serde::Serialize)]
@@ -60,7 +59,6 @@ fn map_account(
login: account.login,
avatar_url: account.avatar_url,
authenticated_at: account.authenticated_at,
github_domain: account.github_domain,
}
}
@@ -81,7 +79,6 @@ fn map_device_code_response(
#[tauri::command(rename_all = "camelCase")]
pub async fn auth_start_login(
auth_provider: String,
github_domain: Option<String>,
copilot_state: State<'_, CopilotAuthState>,
codex_state: State<'_, CodexOAuthState>,
) -> Result<ManagedAuthDeviceCodeResponse, String> {
@@ -90,7 +87,7 @@ pub async fn auth_start_login(
AUTH_PROVIDER_GITHUB_COPILOT => {
let auth_manager = copilot_state.0.read().await;
let response = auth_manager
.start_device_flow(github_domain.as_deref())
.start_device_flow()
.await
.map_err(|e| e.to_string())?;
Ok(map_device_code_response(auth_provider, response))
@@ -111,7 +108,6 @@ pub async fn auth_start_login(
pub async fn auth_poll_for_account(
auth_provider: String,
device_code: String,
github_domain: Option<String>,
copilot_state: State<'_, CopilotAuthState>,
codex_state: State<'_, CodexOAuthState>,
) -> Result<Option<ManagedAuthAccount>, String> {
@@ -119,10 +115,7 @@ pub async fn auth_poll_for_account(
match auth_provider {
AUTH_PROVIDER_GITHUB_COPILOT => {
let auth_manager = copilot_state.0.write().await;
match auth_manager
.poll_for_token(&device_code, github_domain.as_deref())
.await
{
match auth_manager.poll_for_token(&device_code).await {
Ok(account) => {
let default_account_id = auth_manager.get_status().await.default_account_id;
Ok(account.map(|account| {
+1 -33
View File
@@ -3,10 +3,9 @@
//! 提供 OpenAI ChatGPT Plus/Pro OAuth 认证相关的 Tauri 命令。
//!
//! 大部分认证命令通过通用 `auth_*` 命令(参见 `commands::auth`)暴露给前端,
//! 此处定义 State wrapper 以及 Codex OAuth 专属的订阅额度和模型列表查询命令。
//! 此处定义 State wrapper 以及 Codex OAuth 专属的订阅额度查询命令。
use crate::proxy::providers::codex_oauth_auth::CodexOAuthManager;
use crate::services::model_fetch::FetchedModel;
use crate::services::subscription::{query_codex_quota, CredentialStatus, SubscriptionQuota};
use std::sync::Arc;
use tauri::State;
@@ -57,34 +56,3 @@ pub async fn get_codex_oauth_quota(
)
.await)
}
/// 获取 Codex OAuth (ChatGPT Plus/Pro) 可用模型列表
///
/// ChatGPT Codex 反代使用 `chatgpt.com/backend-api/codex/*`,不是 OpenAI 兼容
/// `/v1/models`。这里复用托管 OAuth 账号的 access_token,直接读取 Codex 后端
/// 暴露的模型列表端点。
#[tauri::command(rename_all = "camelCase")]
pub async fn get_codex_oauth_models(
account_id: Option<String>,
state: State<'_, CodexOAuthState>,
) -> Result<Vec<FetchedModel>, String> {
let manager = state.0.read().await;
let resolved = match account_id
.as_deref()
.map(str::trim)
.filter(|id| !id.is_empty())
{
Some(id) => Some(id.to_string()),
None => manager.default_account_id().await,
};
let Some(id) = resolved else {
return Err("No ChatGPT account available".to_string());
};
let token = manager
.get_valid_token_for_account(&id)
.await
.map_err(|e| format!("Codex OAuth token unavailable: {e}"))?;
crate::services::codex_oauth_models::fetch_models_with_token(&token, &id).await
}
+2 -34
View File
@@ -1,6 +1,6 @@
#![allow(non_snake_case)]
use tauri::{AppHandle, State};
use tauri::AppHandle;
use tauri_plugin_dialog::DialogExt;
use tauri_plugin_opener::OpenerExt;
@@ -8,7 +8,6 @@ use crate::app_config::AppType;
use crate::codex_config;
use crate::config::{self, get_claude_settings_path, ConfigStatus};
use crate::settings;
use crate::store::AppState;
#[tauri::command]
pub async fn get_claude_config_status() -> Result<ConfigStatus, String> {
@@ -63,23 +62,9 @@ fn validate_common_config_snippet(app_type: &str, snippet: &str) -> Result<(), S
}
#[tauri::command]
pub async fn get_config_status(
state: State<'_, AppState>,
app: String,
) -> Result<ConfigStatus, String> {
pub async fn get_config_status(app: String) -> Result<ConfigStatus, String> {
match AppType::from_str(&app).map_err(|e| e.to_string())? {
AppType::Claude => Ok(config::get_claude_config_status()),
AppType::ClaudeDesktop => {
let status = crate::claude_desktop_config::get_status(
state.db.as_ref(),
state.proxy_service.is_running().await,
)
.map_err(|e| e.to_string())?;
Ok(ConfigStatus {
exists: status.configured,
path: status.config_library_path.unwrap_or_default(),
})
}
AppType::Codex => {
let auth_path = codex_config::get_codex_auth_path();
let exists = auth_path.exists();
@@ -116,15 +101,6 @@ pub async fn get_config_status(
Ok(ConfigStatus { exists, path })
}
AppType::Hermes => {
let config_path = crate::hermes_config::get_hermes_config_path();
let exists = config_path.exists();
let path = crate::hermes_config::get_hermes_dir()
.to_string_lossy()
.to_string();
Ok(ConfigStatus { exists, path })
}
}
}
@@ -137,14 +113,10 @@ pub async fn get_claude_code_config_path() -> Result<String, String> {
pub async fn get_config_dir(app: String) -> Result<String, String> {
let dir = match AppType::from_str(&app).map_err(|e| e.to_string())? {
AppType::Claude => config::get_claude_config_dir(),
AppType::ClaudeDesktop => {
crate::claude_desktop_config::get_config_library_path().map_err(|e| e.to_string())?
}
AppType::Codex => codex_config::get_codex_config_dir(),
AppType::Gemini => crate::gemini_config::get_gemini_dir(),
AppType::OpenCode => crate::opencode_config::get_opencode_dir(),
AppType::OpenClaw => crate::openclaw_config::get_openclaw_dir(),
AppType::Hermes => crate::hermes_config::get_hermes_dir(),
};
Ok(dir.to_string_lossy().to_string())
@@ -154,14 +126,10 @@ pub async fn get_config_dir(app: String) -> Result<String, String> {
pub async fn open_config_folder(handle: AppHandle, app: String) -> Result<bool, String> {
let config_dir = match AppType::from_str(&app).map_err(|e| e.to_string())? {
AppType::Claude => config::get_claude_config_dir(),
AppType::ClaudeDesktop => {
crate::claude_desktop_config::get_config_library_path().map_err(|e| e.to_string())?
}
AppType::Codex => codex_config::get_codex_config_dir(),
AppType::Gemini => crate::gemini_config::get_gemini_dir(),
AppType::OpenCode => crate::opencode_config::get_opencode_dir(),
AppType::OpenClaw => crate::openclaw_config::get_openclaw_dir(),
AppType::Hermes => crate::hermes_config::get_hermes_dir(),
};
if !config_dir.exists() {
+3 -12
View File
@@ -20,12 +20,11 @@ pub struct CopilotAuthState(pub Arc<RwLock<CopilotAuthManager>>);
/// 返回设备码和用户码,用于 OAuth 认证
#[tauri::command]
pub async fn copilot_start_device_flow(
github_domain: Option<String>,
state: State<'_, CopilotAuthState>,
) -> Result<GitHubDeviceCodeResponse, String> {
let auth_manager = state.0.read().await;
auth_manager
.start_device_flow(github_domain.as_deref())
.start_device_flow()
.await
.map_err(|e| e.to_string())
}
@@ -37,14 +36,10 @@ pub async fn copilot_start_device_flow(
#[tauri::command(rename_all = "camelCase")]
pub async fn copilot_poll_for_auth(
device_code: String,
github_domain: Option<String>,
state: State<'_, CopilotAuthState>,
) -> Result<bool, String> {
let auth_manager = state.0.write().await;
match auth_manager
.poll_for_token(&device_code, github_domain.as_deref())
.await
{
match auth_manager.poll_for_token(&device_code).await {
Ok(Some(_account)) => {
log::info!("[CopilotAuth] 用户已授权");
Ok(true)
@@ -66,14 +61,10 @@ pub async fn copilot_poll_for_auth(
#[tauri::command(rename_all = "camelCase")]
pub async fn copilot_poll_for_account(
device_code: String,
github_domain: Option<String>,
state: State<'_, CopilotAuthState>,
) -> Result<Option<GitHubAccount>, String> {
let auth_manager = state.0.write().await;
match auth_manager
.poll_for_token(&device_code, github_domain.as_deref())
.await
{
match auth_manager.poll_for_token(&device_code).await {
Ok(account) => Ok(account),
Err(crate::proxy::providers::copilot_auth::CopilotAuthError::AuthorizationPending) => {
Ok(None)
+18 -29
View File
@@ -86,19 +86,11 @@ pub async fn set_auto_failover_enabled(
"[Failover] Setting auto_failover_enabled: app_type='{app_type}', enabled={enabled}"
);
// 读取当前配置
let mut config = state
.db
.get_proxy_config_for_app(&app_type)
.await
.map_err(|e| e.to_string())?;
if enabled && !config.enabled {
return Err("需要先启用该应用的代理接管,再开启故障转移".to_string());
}
// 队列为空时把当前供应商自动加入作为 P1,避免用户陷入"必须先加队列才能开启"的死锁
let mut auto_added_provider_id: Option<String> = None;
// 强一致语义:开启故障转移后立即切到队列 P1(并确保队列非空)
//
// 说明:
// - 仅在 enabled=true 时执行“切到 P1”
// - 若队列为空,则尝试把“当前供应商”自动加入队列作为 P1,避免用户在 UI 上陷入死锁(无法先加队列再开启)
let p1_provider_id = if enabled {
let mut queue = state
.db
@@ -120,7 +112,6 @@ pub async fn set_auto_failover_enabled(
.db
.add_to_failover_queue(&app_type, &current_id)
.map_err(|e| e.to_string())?;
auto_added_provider_id = Some(current_id);
queue = state
.db
@@ -136,20 +127,12 @@ pub async fn set_auto_failover_enabled(
String::new()
};
// 开启前先切到 P1。只有切换成功后才写入 auto_failover_enabled=true
// 避免 P1 不可切换(例如 official provider)时留下“开关已开但目标未切”的脏状态。
if enabled {
if let Err(e) = state
.proxy_service
.switch_proxy_target(&app_type, &p1_provider_id)
.await
{
if let Some(provider_id) = auto_added_provider_id {
let _ = state.db.remove_from_failover_queue(&app_type, &provider_id);
}
return Err(e);
}
}
// 读取当前配置
let mut config = state
.db
.get_proxy_config_for_app(&app_type)
.await
.map_err(|e| e.to_string())?;
// 更新 auto_failover_enabled 字段
config.auto_failover_enabled = enabled;
@@ -161,7 +144,13 @@ pub async fn set_auto_failover_enabled(
.await
.map_err(|e| e.to_string())?;
// 开启后立即切到 P1:更新 is_current + 本地 settings + Live 备份(接管模式下)
if enabled {
state
.proxy_service
.switch_proxy_target(&app_type, &p1_provider_id)
.await?;
// 发射 provider-switched 事件(让前端刷新当前供应商)
let event_data = serde_json::json!({
"appType": app_type,
@@ -173,7 +162,7 @@ pub async fn set_auto_failover_enabled(
// 刷新托盘菜单,确保状态同步
if let Ok(new_menu) = crate::tray::create_tray_menu(&app, &state) {
if let Some(tray) = app.tray_by_id(crate::tray::TRAY_ID) {
if let Some(tray) = app.tray_by_id("main") {
let _ = tray.set_menu(Some(new_menu));
}
}
-143
View File
@@ -1,143 +0,0 @@
use std::time::Duration;
use tauri::{AppHandle, State};
use tauri_plugin_opener::OpenerExt;
use crate::hermes_config;
use crate::store::AppState;
/// Error string returned when `open_hermes_web_ui` cannot reach the Hermes
/// FastAPI server. Kept in sync with the `HERMES_WEB_OFFLINE_ERROR` constant
/// in `src/hooks/useHermes.ts` so the frontend can branch on it.
const HERMES_WEB_OFFLINE_ERROR: &str = "hermes_web_offline";
// ============================================================================
// Hermes Provider Commands
// ============================================================================
/// Import providers from Hermes live config to database.
///
/// Hermes uses additive mode — users may already have providers
/// configured in config.yaml.
#[tauri::command]
pub fn import_hermes_providers_from_live(state: State<'_, AppState>) -> Result<usize, String> {
crate::services::provider::import_hermes_providers_from_live(state.inner())
.map_err(|e| e.to_string())
}
/// Get provider names in the Hermes live config.
#[tauri::command]
pub fn get_hermes_live_provider_ids() -> Result<Vec<String>, String> {
hermes_config::get_providers()
.map(|providers| providers.keys().cloned().collect())
.map_err(|e| e.to_string())
}
/// Get a single Hermes provider fragment from live config.
#[tauri::command]
pub fn get_hermes_live_provider(
#[allow(non_snake_case)] providerId: String,
) -> Result<Option<serde_json::Value>, String> {
hermes_config::get_provider(&providerId).map_err(|e| e.to_string())
}
// ============================================================================
// Model Configuration Commands
// ============================================================================
/// Get Hermes model config (model section of config.yaml). Read-only — writes
/// happen implicitly through `apply_switch_defaults` when switching providers.
#[tauri::command]
pub fn get_hermes_model_config() -> Result<Option<hermes_config::HermesModelConfig>, String> {
hermes_config::get_model_config().map_err(|e| e.to_string())
}
// ============================================================================
// Memory Files Commands
// ============================================================================
#[tauri::command]
pub fn get_hermes_memory(kind: hermes_config::MemoryKind) -> Result<String, String> {
hermes_config::read_memory(kind).map_err(|e| e.to_string())
}
#[tauri::command]
pub fn set_hermes_memory(kind: hermes_config::MemoryKind, content: String) -> Result<(), String> {
hermes_config::write_memory(kind, &content).map_err(|e| e.to_string())
}
#[tauri::command]
pub fn get_hermes_memory_limits() -> Result<hermes_config::HermesMemoryLimits, String> {
hermes_config::read_memory_limits().map_err(|e| e.to_string())
}
#[tauri::command]
pub fn set_hermes_memory_enabled(
kind: hermes_config::MemoryKind,
enabled: bool,
) -> Result<hermes_config::HermesWriteOutcome, String> {
hermes_config::set_memory_enabled(kind, enabled).map_err(|e| e.to_string())
}
// ============================================================================
// Hermes Web UI launcher
// ============================================================================
/// Probe the local Hermes Web UI (FastAPI) and open it in the system browser.
///
/// Port discovery priority:
/// 1. `HERMES_WEB_PORT` environment variable
/// 2. Default 9119
///
/// Hermes wraps all `/api/*` routes in a Bearer-token middleware, so a GET
/// against `/api/status` returning **either 200 or 401** confirms the server
/// is live. The session token lives only in the Hermes process memory and is
/// injected into the returned HTML via `window.__HERMES_SESSION_TOKEN__`, so
/// there is no need (and no way) for CC Switch to inject it — we just open
/// the URL and let Hermes handle auth.
#[tauri::command]
pub async fn open_hermes_web_ui(app: AppHandle, path: Option<String>) -> Result<(), String> {
let port = std::env::var("HERMES_WEB_PORT")
.ok()
.and_then(|raw| raw.trim().parse::<u16>().ok())
.unwrap_or(9119);
let base = format!("http://127.0.0.1:{port}");
// Probe /api/status with a short timeout. Hermes returns 200 when open or
// 401 when the session token is required — either way the server is live.
// Only a connection error / timeout means the server isn't running.
let probe_url = format!("{base}/api/status");
let client = reqwest::Client::builder()
.timeout(Duration::from_millis(1200))
.no_proxy()
.build()
.map_err(|e| format!("failed to build probe client: {e}"))?;
match client.get(&probe_url).send().await {
Ok(_) => {}
Err(_) => return Err(HERMES_WEB_OFFLINE_ERROR.to_string()),
}
let target = match path.as_deref() {
Some(p) if p.starts_with('/') => format!("{base}{p}"),
Some(p) if !p.is_empty() => format!("{base}/{p}"),
_ => format!("{base}/"),
};
app.opener()
.open_url(&target, None::<String>)
.map_err(|e| format!("failed to open Hermes Web UI: {e}"))
}
/// Open the preferred terminal and run `hermes dashboard`. Non-blocking —
/// callers should reinvoke `open_hermes_web_ui` once the server is ready,
/// since Hermes startup can take several seconds and may fail outright if
/// the `hermes-agent[web]` extras are missing.
#[tauri::command]
pub async fn launch_hermes_dashboard() -> Result<(), String> {
tokio::task::spawn_blocking(|| {
crate::commands::misc::launch_terminal_running("hermes dashboard", "hermes_dashboard")
})
.await
.map_err(|e| format!("launch task join error: {e}"))?
}
-1
View File
@@ -202,6 +202,5 @@ pub async fn import_mcp_from_apps(state: State<'_, AppState>) -> Result<usize, S
total += McpService::import_from_codex(&state).unwrap_or(0);
total += McpService::import_from_gemini(&state).unwrap_or(0);
total += McpService::import_from_opencode(&state).unwrap_or(0);
total += McpService::import_from_hermes(&state).unwrap_or(0);
Ok(total)
}
+20 -320
View File
@@ -300,13 +300,8 @@ fn try_get_version(tool: &str) -> (Option<String>, Option<String>) {
#[cfg(not(target_os = "windows"))]
let output = {
let shell = std::env::var("SHELL")
.ok()
.filter(|s| is_valid_shell(s))
.unwrap_or_else(|| "sh".to_string());
let flag = default_flag_for_shell(&shell);
Command::new(shell)
.arg(flag)
Command::new("sh")
.arg("-c")
.arg(format!("{tool} --version"))
.output()
};
@@ -350,6 +345,7 @@ fn is_valid_wsl_distro_name(name: &str) -> bool {
}
/// Validate that the given shell name is one of the allowed shells.
#[cfg(target_os = "windows")]
fn is_valid_shell(shell: &str) -> bool {
matches!(
shell.rsplit('/').next().unwrap_or(shell),
@@ -364,6 +360,7 @@ fn is_valid_shell_flag(flag: &str) -> bool {
}
/// Return the default invocation flag for the given shell.
#[cfg(target_os = "windows")]
fn default_flag_for_shell(shell: &str) -> &'static str {
match shell.rsplit('/').next().unwrap_or(shell) {
"dash" | "sh" => "-c",
@@ -784,7 +781,7 @@ fn extract_env_vars_from_config(
// 处理 base_url: 根据应用类型添加对应的环境变量
let base_url_key = match app_type {
AppType::Claude | AppType::ClaudeDesktop => Some("ANTHROPIC_BASE_URL"),
AppType::Claude => Some("ANTHROPIC_BASE_URL"),
AppType::Gemini => Some("GOOGLE_GEMINI_BASE_URL"),
_ => None,
};
@@ -947,7 +944,6 @@ exec bash --norc --noprofile
// Note: Kitty doesn't need the -e flag, others do
let result = match terminal {
"iterm2" => launch_macos_iterm2(&script_file),
"warp" => launch_macos_warp(&script_file),
"alacritty" => launch_macos_open_app("Alacritty", &script_file, true),
"kitty" => launch_macos_open_app("kitty", &script_file, false),
"ghostty" => launch_macos_open_app("Ghostty", &script_file, true),
@@ -1000,48 +996,23 @@ end tell"#,
Ok(())
}
/// macOS: iTerm2
#[cfg(target_os = "macos")]
fn build_macos_iterm2_applescript(script_file: &std::path::Path) -> String {
format!(
r#"set launcher_script to "bash '{}'"
set was_running to application "iTerm" is running
tell application "iTerm"
if was_running then
activate
if (count of windows) = 0 then
create window with default profile
else
tell current window
create tab with default profile
end tell
end if
else
activate
set waited to 0
repeat while (count of windows) = 0
delay 0.1
set waited to waited + 1
if waited >= 30 then exit repeat
end repeat
if (count of windows) = 0 then
create window with default profile
end if
end if
tell current session of current window
write text launcher_script
end tell
end tell"#,
script_file.display()
)
}
/// macOS: iTerm2
#[cfg(target_os = "macos")]
fn launch_macos_iterm2(script_file: &std::path::Path) -> Result<(), String> {
use std::process::Command;
let applescript = build_macos_iterm2_applescript(script_file);
let applescript = format!(
r#"tell application "iTerm"
activate
tell current window
create tab with default profile
tell current session
write text "bash '{}'"
end tell
end tell
end tell"#,
script_file.display()
);
let output = Command::new("osascript")
.arg("-e")
@@ -1095,57 +1066,6 @@ fn launch_macos_open_app(
Ok(())
}
#[cfg(target_os = "macos")]
fn launch_macos_warp(script_file: &std::path::Path) -> Result<(), String> {
use std::io::Write;
use std::os::unix::fs::PermissionsExt;
use std::process::Command;
let mut cmd = Command::new("open");
cmd.arg("-a").arg("Warp");
// Warp URI scheme cannot work well with script_file, because:
//
// 1. script_file's name ends up with .sh, so Warp would open the file rather than execute it
// 2. script_file has no execution permission, so we need to add one more indirection
let mut second_script_file = tempfile::Builder::new()
.disable_cleanup(true)
.permissions(std::fs::Permissions::from_mode(0o755))
.tempfile()
.map_err(|e| format!("Failed to create temporary script file: {e}"))?;
writeln!(
&mut second_script_file,
r#"#!/usr/bin/env sh
rm -- "$0"
exec bash {}
"#,
script_file.display(),
)
.map_err(|e| format!("Failed to write to temporary script file for Warp: {e}"))?;
let mut warp_url = url::Url::parse("warp://action/new_tab").unwrap();
warp_url
.query_pairs_mut()
.append_pair("path", &second_script_file.path().to_string_lossy());
let warp_url = warp_url.to_string();
cmd.arg(warp_url);
let output = cmd.output().map_err(|e| format!("启动 Warp 失败: {e}"))?;
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
return Err(format!(
"Warp 启动失败 (exit code: {:?}): {}",
output.status.code(),
stderr
));
}
Ok(())
}
/// Linux: 根据用户首选终端启动
#[cfg(target_os = "linux")]
fn launch_linux_terminal(config_file: &std::path::Path, cwd: Option<&Path>) -> Result<(), String> {
@@ -1390,189 +1310,6 @@ fn run_windows_start_command(args: &[&str], terminal_name: &str) -> Result<(), S
Ok(())
}
/// 打开用户首选终端并在其中执行一条命令行。脚本尾部 `read -n 1` / `pause`
/// 是刻意设计的——让命令退出后窗口不要瞬间关闭,用户才看得到 `command
/// not found` / `ModuleNotFoundError` 这类诊断信息。
///
/// **Security**`command_line` 会被原样拼进 shell/batch 脚本,调用方必须
/// 保证它是可信字符串(当前只由后端硬编码调用)。
pub(crate) fn launch_terminal_running(command_line: &str, label: &str) -> Result<(), String> {
let temp_dir = std::env::temp_dir();
let pid = std::process::id();
#[cfg(any(target_os = "macos", target_os = "linux"))]
let (script_file, script_content) = {
let file = temp_dir.join(format!("cc_switch_{}_{}.sh", label, pid));
let content = format!(
r#"#!/bin/bash
trap 'rm -f "{script_path}"' EXIT
echo "[cc-switch] Starting: {cmd}"
echo ""
{cmd}
echo ""
echo "[cc-switch] Command exited. Press any key to close."
read -n 1 -s
"#,
script_path = file.display(),
cmd = command_line,
);
(file, content)
};
#[cfg(target_os = "macos")]
{
use std::os::unix::fs::PermissionsExt;
std::fs::write(&script_file, &script_content)
.map_err(|e| format!("写入启动脚本失败: {e}"))?;
std::fs::set_permissions(&script_file, std::fs::Permissions::from_mode(0o755))
.map_err(|e| format!("设置脚本权限失败: {e}"))?;
let preferred = crate::settings::get_preferred_terminal();
let terminal = preferred.as_deref().unwrap_or("terminal");
let result = match terminal {
"iterm2" => launch_macos_iterm2(&script_file),
"warp" => launch_macos_warp(&script_file),
"alacritty" => launch_macos_open_app("Alacritty", &script_file, true),
"kitty" => launch_macos_open_app("kitty", &script_file, false),
"ghostty" => launch_macos_open_app("Ghostty", &script_file, true),
"wezterm" => launch_macos_open_app("WezTerm", &script_file, true),
"kaku" => launch_macos_open_app("Kaku", &script_file, true),
_ => launch_macos_terminal_app(&script_file),
};
if result.is_err() && terminal != "terminal" {
log::warn!(
"首选终端 {} 启动失败,回退到 Terminal.app: {:?}",
terminal,
result.as_ref().err()
);
return launch_macos_terminal_app(&script_file);
}
result
}
#[cfg(target_os = "linux")]
{
use std::os::unix::fs::PermissionsExt;
use std::process::Command;
std::fs::write(&script_file, &script_content)
.map_err(|e| format!("写入启动脚本失败: {e}"))?;
std::fs::set_permissions(&script_file, std::fs::Permissions::from_mode(0o755))
.map_err(|e| format!("设置脚本权限失败: {e}"))?;
let preferred = crate::settings::get_preferred_terminal();
let default_terminals = [
("gnome-terminal", vec!["--"]),
("konsole", vec!["-e"]),
("xfce4-terminal", vec!["-e"]),
("mate-terminal", vec!["--"]),
("lxterminal", vec!["-e"]),
("alacritty", vec!["-e"]),
("kitty", vec!["-e"]),
("ghostty", vec!["-e"]),
];
let terminals_to_try: Vec<(&str, Vec<&str>)> = if let Some(ref pref) = preferred {
let pref_args = default_terminals
.iter()
.find(|(name, _)| *name == pref.as_str())
.map(|(_, args)| args.to_vec())
.unwrap_or_else(|| vec!["-e"]);
let mut list = vec![(pref.as_str(), pref_args)];
for (name, args) in &default_terminals {
if *name != pref.as_str() {
list.push((*name, args.to_vec()));
}
}
list
} else {
default_terminals
.iter()
.map(|(name, args)| (*name, args.to_vec()))
.collect()
};
let mut last_error = String::from("未找到可用的终端");
for (terminal, args) in terminals_to_try {
let terminal_exists = which_command(terminal)
|| ["/usr/bin", "/bin", "/usr/local/bin"]
.iter()
.any(|dir| std::path::Path::new(&format!("{}/{}", dir, terminal)).exists());
if terminal_exists {
let spawn_result = Command::new(terminal)
.args(&args)
.arg("bash")
.arg(script_file.to_string_lossy().as_ref())
.spawn();
match spawn_result {
Ok(_) => return Ok(()),
Err(e) => {
last_error = format!("执行 {} 失败: {}", terminal, e);
}
}
}
}
let _ = std::fs::remove_file(&script_file);
Err(last_error)
}
#[cfg(target_os = "windows")]
{
let preferred = crate::settings::get_preferred_terminal();
let terminal = preferred.as_deref().unwrap_or("cmd");
let bat_file = temp_dir.join(format!("cc_switch_{}_{}.bat", label, pid));
let content = format!(
"@echo off\r\necho [cc-switch] Starting: {cmd}\r\necho.\r\n{cmd}\r\necho.\r\necho [cc-switch] Command exited. Press any key to close.\r\npause >nul\r\ndel \"%~f0\" >nul 2>&1\r\n",
cmd = command_line,
);
std::fs::write(&bat_file, &content).map_err(|e| format!("写入批处理文件失败: {e}"))?;
let bat_path = bat_file.to_string_lossy();
let ps_cmd = format!("& '{}'", bat_path);
let result = match terminal {
"powershell" => run_windows_start_command(
&["powershell", "-NoExit", "-Command", &ps_cmd],
"PowerShell",
),
"wt" => run_windows_start_command(&["wt", "cmd", "/K", &bat_path], "Windows Terminal"),
_ => run_windows_start_command(&["cmd", "/K", &bat_path], "cmd"),
};
let final_result = if result.is_err() && terminal != "cmd" {
log::warn!(
"首选终端 {} 启动失败,回退到 cmd: {:?}",
terminal,
result.as_ref().err()
);
run_windows_start_command(&["cmd", "/K", &bat_path], "cmd")
} else {
result
};
// The .bat self-deletes (`del "%~f0"`) after it runs, but that only
// fires if *some* terminal actually launched it. If every attempt
// failed, sweep the temp file ourselves to avoid pollution.
if final_result.is_err() {
let _ = std::fs::remove_file(&bat_file);
}
final_result
}
#[cfg(not(any(target_os = "macos", target_os = "linux", target_os = "windows")))]
{
let _ = (temp_dir, pid, command_line, label);
Err("不支持的操作系统".to_string())
}
}
/// 设置窗口主题(Windows/macOS 标题栏颜色)
/// theme: "dark" | "light" | "system"
#[tauri::command]
@@ -1591,7 +1328,7 @@ pub async fn set_window_theme(window: tauri::Window, theme: String) -> Result<()
#[cfg(test)]
mod tests {
use super::*;
use std::path::{Path, PathBuf};
use std::path::PathBuf;
#[test]
fn test_extract_version() {
@@ -1679,7 +1416,7 @@ mod tests {
let count = paths
.iter()
.filter(|path| path.as_path() == Path::new("/same/path"))
.filter(|path| **path == PathBuf::from("/same/path"))
.count();
assert_eq!(count, 1);
}
@@ -1691,7 +1428,7 @@ mod tests {
let count = paths
.iter()
.filter(|path| path.as_path() == Path::new("/home/tester/.bun/bin"))
.filter(|path| **path == PathBuf::from("/home/tester/.bun/bin"))
.count();
assert_eq!(count, 1);
}
@@ -1752,43 +1489,6 @@ mod tests {
assert_eq!(command, "cd '/tmp/project O'\"'\"'Brien' || exit 1\n");
}
#[cfg(target_os = "macos")]
#[test]
fn iterm2_applescript_cold_start_avoids_current_window_before_one_exists() {
let script = build_macos_iterm2_applescript(Path::new("/tmp/cc_switch_launcher.sh"));
let cold_start_branch = script
.split("else\n activate")
.nth(1)
.expect("cold start branch should be present")
.split(" end if\n tell current session")
.next()
.expect("cold start branch should end before writing command");
assert!(cold_start_branch.contains("repeat while (count of windows) = 0"));
assert!(cold_start_branch.contains("create window with default profile"));
assert!(!cold_start_branch.contains("tell current window"));
assert!(!cold_start_branch.contains("create tab with default profile"));
}
#[cfg(target_os = "macos")]
#[test]
fn iterm2_applescript_keeps_new_tab_behavior_for_existing_windows() {
let script = build_macos_iterm2_applescript(Path::new("/tmp/cc_switch_launcher.sh"));
let running_branch = script
.split("if was_running then")
.nth(1)
.expect("already-running branch should be present")
.split("else\n activate")
.next()
.expect("already-running branch should end before cold start branch");
assert!(running_branch.contains("if (count of windows) = 0 then"));
assert!(running_branch.contains("create window with default profile"));
assert!(running_branch.contains("create tab with default profile"));
}
#[test]
fn build_windows_cwd_command_str_uses_cd_for_drive_paths() {
let command = build_windows_cwd_command_str(r"C:\work\repo");
-2
View File
@@ -10,7 +10,6 @@ mod deeplink;
mod env;
mod failover;
mod global_proxy;
mod hermes;
mod import_export;
mod mcp;
mod misc;
@@ -43,7 +42,6 @@ pub use deeplink::*;
pub use env::*;
pub use failover::*;
pub use global_proxy::*;
pub use hermes::*;
pub use import_export::*;
pub use mcp::*;
pub use misc::*;
+3 -10
View File
@@ -6,20 +6,13 @@ use crate::services::model_fetch::{self, FetchedModel};
/// 获取供应商的可用模型列表
///
/// 使用 OpenAI 兼容的 GET /v1/models 端点。优先使用 `models_url` 精确覆写;
/// 否则对 baseURL 生成候选列表(含「剥离 Anthropic 兼容子路径」兜底),按序尝试
/// 使用 OpenAI 兼容的 GET /v1/models 端点。
/// 主要面向第三方聚合站(硅基流动、OpenRouter 等)
#[tauri::command(rename_all = "camelCase")]
pub async fn fetch_models_for_config(
base_url: String,
api_key: String,
is_full_url: Option<bool>,
models_url: Option<String>,
) -> Result<Vec<FetchedModel>, String> {
model_fetch::fetch_models(
&base_url,
&api_key,
is_full_url.unwrap_or(false),
models_url.as_deref(),
)
.await
model_fetch::fetch_models(&base_url, &api_key, is_full_url.unwrap_or(false)).await
}
+5 -464
View File
@@ -1,10 +1,10 @@
use indexmap::IndexMap;
use tauri::{Emitter, State};
use tauri::State;
use crate::app_config::AppType;
use crate::commands::copilot::CopilotAuthState;
use crate::error::AppError;
use crate::provider::{ClaudeDesktopMode, Provider};
use crate::provider::Provider;
use crate::services::{
EndpointLatency, ProviderService, ProviderSortUpdate, SpeedtestService, SwitchResult,
};
@@ -150,266 +150,22 @@ pub fn import_default_config(state: State<'_, AppState>, app: String) -> Result<
import_default_config_internal(&state, app_type).map_err(Into::into)
}
#[tauri::command]
pub async fn get_claude_desktop_status(
state: State<'_, AppState>,
) -> Result<crate::claude_desktop_config::ClaudeDesktopStatus, String> {
let proxy_running = state.proxy_service.is_running().await;
crate::claude_desktop_config::get_status(state.db.as_ref(), proxy_running)
.map_err(|e| e.to_string())
}
#[tauri::command]
pub fn get_claude_desktop_default_routes(
) -> Vec<crate::claude_desktop_config::ClaudeDesktopDefaultRoute> {
crate::claude_desktop_config::default_proxy_routes()
}
#[tauri::command]
pub fn import_claude_desktop_providers_from_claude(
state: State<'_, AppState>,
) -> Result<usize, String> {
let claude_providers = state
.db
.get_all_providers(AppType::Claude.as_str())
.map_err(|e| e.to_string())?;
let existing_ids = state
.db
.get_provider_ids(AppType::ClaudeDesktop.as_str())
.map_err(|e| e.to_string())?;
let mut imported = 0usize;
for provider in claude_providers.values() {
if existing_ids.contains(&provider.id) {
continue;
}
let mut desktop_provider = provider.clone();
desktop_provider.in_failover_queue = false;
let meta = desktop_provider.meta.get_or_insert_with(Default::default);
if crate::claude_desktop_config::is_compatible_direct_provider(provider)
&& claude_provider_models_are_claude_safe(provider)
{
meta.claude_desktop_mode = Some(ClaudeDesktopMode::Direct);
} else if let Some(routes) = suggested_claude_desktop_routes(provider) {
meta.claude_desktop_mode = Some(ClaudeDesktopMode::Proxy);
meta.claude_desktop_model_routes = routes;
} else {
continue;
}
state
.db
.save_provider(AppType::ClaudeDesktop.as_str(), &desktop_provider)
.map_err(|e| e.to_string())?;
imported += 1;
}
// Safety net: 用户可能手动删除过 claude-desktop-official seed。
// 用户主动点 import 是"重新整理 ClaudeDesktop 表"的隐式信号,把官方入口补回来。
// 失败只 warn,不影响 imported 主流程;imported 计数语义保持纯净。
if let Err(e) = state.db.ensure_official_seed_by_id(
crate::database::CLAUDE_DESKTOP_OFFICIAL_PROVIDER_ID,
AppType::ClaudeDesktop,
) {
log::warn!("Failed to ensure claude-desktop-official seed during import: {e}");
}
Ok(imported)
}
fn claude_provider_models_are_claude_safe(provider: &Provider) -> bool {
let Some(env) = provider
.settings_config
.get("env")
.and_then(|value| value.as_object())
else {
return true;
};
[
"ANTHROPIC_MODEL",
"ANTHROPIC_DEFAULT_HAIKU_MODEL",
"ANTHROPIC_DEFAULT_SONNET_MODEL",
"ANTHROPIC_DEFAULT_OPUS_MODEL",
]
.into_iter()
.filter_map(|key| env.get(key).and_then(|value| value.as_str()))
.map(str::trim)
.filter(|value| !value.is_empty())
.all(crate::claude_desktop_config::is_claude_safe_model_id)
}
pub(crate) fn suggested_claude_desktop_routes(
provider: &Provider,
) -> Option<std::collections::HashMap<String, crate::provider::ClaudeDesktopModelRoute>> {
let env = provider
.settings_config
.get("env")
.and_then(|value| value.as_object())?;
let mut routes = std::collections::HashMap::new();
let supports_1m_default = !matches!(
provider
.meta
.as_ref()
.and_then(|meta| meta.provider_type.as_deref()),
Some("github_copilot") | Some("codex_oauth")
);
fn add_route(
routes: &mut std::collections::HashMap<String, crate::provider::ClaudeDesktopModelRoute>,
env: &serde_json::Map<String, serde_json::Value>,
route_key: &str,
env_key: &str,
supports_1m_default: bool,
) {
let Some(raw_model) = env
.get(env_key)
.and_then(|value| value.as_str())
.map(str::trim)
.filter(|value| !value.is_empty())
else {
return;
};
// Claude 端 env 值可能带 [1M] 后缀;Claude Desktop schema 不接受后缀,
// 改用 supports1m 字段表达 1M 能力。在 import 边界做单向翻译。
let marker = crate::claude_desktop_config::ONE_M_CONTEXT_MARKER.as_bytes();
let raw_bytes = raw_model.as_bytes();
let has_1m_marker = raw_bytes.len() >= marker.len()
&& raw_bytes[raw_bytes.len() - marker.len()..].eq_ignore_ascii_case(marker);
let stripped_model: &str = if has_1m_marker {
raw_model[..raw_model.len() - marker.len()].trim_end()
} else {
raw_model
};
if stripped_model.is_empty() {
return;
}
let effective_supports_1m = supports_1m_default || has_1m_marker;
let explicit_label_override = env
.get(&format!("{env_key}_NAME"))
.and_then(|value| value.as_str())
.map(str::trim)
.filter(|value| !value.is_empty())
.map(str::to_string);
let label_override = explicit_label_override.clone().or_else(|| {
(!crate::claude_desktop_config::is_claude_safe_model_id(stripped_model))
.then(|| stripped_model.to_string())
});
// 何时覆盖既有 label_override:原本为空 / 这次来的是 explicit _NAME /
// 既有值只是 stripped_model 派生的占位(被 explicit 或更具体的值挤掉)。
let should_overwrite = |existing: Option<&str>| {
existing.is_none()
|| explicit_label_override.is_some()
|| existing == Some(stripped_model)
};
let merge_into = |existing: &mut crate::provider::ClaudeDesktopModelRoute| {
let merged = existing.supports_1m.unwrap_or(false) || effective_supports_1m;
existing.supports_1m = Some(merged);
if should_overwrite(existing.label_override.as_deref()) {
existing.label_override = label_override.clone();
}
};
if let Some(existing) = routes
.values_mut()
.find(|existing| existing.model == stripped_model)
{
merge_into(existing);
return;
}
routes
.entry(route_key.to_string())
.and_modify(merge_into)
.or_insert_with(|| crate::provider::ClaudeDesktopModelRoute {
model: stripped_model.to_string(),
label_override,
supports_1m: Some(effective_supports_1m),
});
}
for spec in crate::claude_desktop_config::DEFAULT_PROXY_ROUTES {
add_route(
&mut routes,
env,
spec.route_id,
spec.env_key,
supports_1m_default,
);
}
// 三个 default env_key 全空时用 ANTHROPIC_MODEL 派生兜底路由。
if routes.is_empty() {
let primary_route = crate::claude_desktop_config::DEFAULT_PROXY_ROUTES[0].route_id;
add_route(
&mut routes,
env,
primary_route,
"ANTHROPIC_MODEL",
supports_1m_default,
);
}
(!routes.is_empty()).then_some(routes)
}
#[allow(non_snake_case)]
#[tauri::command]
pub async fn queryProviderUsage(
app_handle: tauri::AppHandle,
state: State<'_, AppState>,
copilot_state: State<'_, CopilotAuthState>,
#[allow(non_snake_case)] providerId: String, // 使用 camelCase 匹配前端
app: String,
) -> Result<crate::provider::UsageResult, String> {
let app_type = AppType::from_str(&app).map_err(|e| e.to_string())?;
// inner 可能以两种形式失败:
// 1) 返回 Ok(UsageResult { success: false, .. }) —— 业务失败(401、脚本报错等)
// 2) 返回 Err(String) —— RPC/DB/Copilot fetch_usage 等 transport 层失败
// 两种都要把"失败"写进 UsageCache 并刷新托盘,让 format_script_summary 的
// success 守卫生效、suffix 自然消失,避免旧 success 快照长期滞留。
// 同时保持原始 Err 返回给前端 React Query 的 onError 回调,不吞错误。
let inner =
query_provider_usage_inner(&state, &copilot_state, app_type.clone(), &providerId).await;
let snapshot = match &inner {
Ok(r) => r.clone(),
Err(err_msg) => crate::provider::UsageResult {
success: false,
data: None,
error: Some(err_msg.clone()),
},
};
let payload = serde_json::json!({
"kind": "script",
"appType": app_type.as_str(),
"providerId": &providerId,
"data": &snapshot,
});
if let Err(e) = app_handle.emit("usage-cache-updated", payload) {
log::error!("emit usage-cache-updated (script) 失败: {e}");
}
state.usage_cache.put_script(app_type, providerId, snapshot);
crate::tray::schedule_tray_refresh(&app_handle);
inner
}
async fn query_provider_usage_inner(
state: &AppState,
copilot_state: &CopilotAuthState,
app_type: AppType,
provider_id: &str,
) -> Result<crate::provider::UsageResult, String> {
// 从数据库读取供应商信息,检查特殊模板类型
let providers = state
.db
.get_all_providers(app_type.as_str())
.map_err(|e| format!("Failed to get providers: {e}"))?;
let provider = providers.get(provider_id);
let provider = providers.get(&providerId);
let usage_script = provider
.and_then(|p| p.meta.as_ref())
.and_then(|m| m.usage_script.as_ref());
@@ -538,7 +294,7 @@ async fn query_provider_usage_inner(
}
// ── 通用 JS 脚本路径 ──
ProviderService::query_usage(state, app_type, provider_id)
ProviderService::query_usage(state.inner(), app_type, &providerId)
.await
.map_err(|e| e.to_string())
}
@@ -650,7 +406,7 @@ pub fn update_providers_sort_order(
use crate::provider::UniversalProvider;
use std::collections::HashMap;
use tauri::AppHandle;
use tauri::{AppHandle, Emitter};
#[derive(Clone, serde::Serialize)]
pub struct UniversalProviderSyncedEvent {
@@ -742,218 +498,3 @@ pub fn get_opencode_live_provider_ids() -> Result<Vec<String>, String> {
// ============================================================================
// OpenClaw 专属命令 → 已迁移至 commands/openclaw.rs
// ============================================================================
#[cfg(test)]
mod import_claude_desktop_tests {
use super::suggested_claude_desktop_routes;
use crate::provider::{Provider, ProviderMeta};
use serde_json::json;
fn make_provider(env: serde_json::Value, provider_type: Option<&str>) -> Provider {
let mut p = Provider::with_id(
"test-claude".to_string(),
"Test".to_string(),
json!({ "env": env }),
None,
);
if let Some(pt) = provider_type {
p.meta = Some(ProviderMeta {
provider_type: Some(pt.to_string()),
..ProviderMeta::default()
});
}
p
}
#[test]
fn route_strips_1m_suffix_and_sets_supports_1m() {
let p = make_provider(
json!({
"ANTHROPIC_DEFAULT_SONNET_MODEL": "claude-sonnet-4-5-20250929[1M]",
}),
None,
);
let routes = suggested_claude_desktop_routes(&p).expect("routes built");
let r = routes
.get("claude-sonnet-4-6")
.expect("sonnet route present");
assert_eq!(r.model, "claude-sonnet-4-5-20250929");
assert!(
!r.model.to_ascii_lowercase().contains("[1m]"),
"model must not contain [1m] suffix"
);
assert_eq!(r.label_override, None);
assert_eq!(r.supports_1m, Some(true));
}
#[test]
fn route_preserves_model_without_suffix() {
let p = make_provider(
json!({
"ANTHROPIC_DEFAULT_SONNET_MODEL": "kimi-k2",
}),
None,
);
let routes = suggested_claude_desktop_routes(&p).expect("routes built");
let r = routes
.get("claude-sonnet-4-6")
.expect("sonnet route present");
assert_eq!(r.model, "kimi-k2");
assert_eq!(r.label_override.as_deref(), Some("kimi-k2"));
// 默认 provider_type 缺省 → supports_1m_default = true
assert_eq!(r.supports_1m, Some(true));
}
#[test]
fn route_uses_claude_code_model_name_as_label_override() {
let p = make_provider(
json!({
"ANTHROPIC_DEFAULT_SONNET_MODEL": "kimi-k2",
"ANTHROPIC_DEFAULT_SONNET_MODEL_NAME": "Kimi K2",
}),
None,
);
let routes = suggested_claude_desktop_routes(&p).expect("routes built");
let r = routes
.get("claude-sonnet-4-6")
.expect("sonnet route present");
assert_eq!(r.model, "kimi-k2");
assert_eq!(r.label_override.as_deref(), Some("Kimi K2"));
}
#[test]
fn route_1m_suffix_overrides_provider_type_default() {
// github_copilot 默认 supports_1m_default = false,但 [1M] 后缀应强制 true
let p = make_provider(
json!({
"ANTHROPIC_DEFAULT_SONNET_MODEL": "gpt-5-codex[1M]",
}),
Some("github_copilot"),
);
let routes = suggested_claude_desktop_routes(&p).expect("routes built");
let r = routes
.get("claude-sonnet-4-6")
.expect("sonnet route present");
assert_eq!(r.model, "gpt-5-codex");
assert_eq!(r.label_override.as_deref(), Some("gpt-5-codex"));
assert_eq!(r.supports_1m, Some(true));
}
#[test]
fn route_github_copilot_without_suffix_keeps_false() {
let p = make_provider(
json!({
"ANTHROPIC_DEFAULT_SONNET_MODEL": "gpt-5-codex",
}),
Some("github_copilot"),
);
let routes = suggested_claude_desktop_routes(&p).expect("routes built");
let r = routes
.get("claude-sonnet-4-6")
.expect("sonnet route present");
assert_eq!(r.model, "gpt-5-codex");
assert_eq!(r.label_override.as_deref(), Some("gpt-5-codex"));
assert_eq!(r.supports_1m, Some(false));
}
#[test]
fn same_upstream_across_three_aliases_merges_to_one_route() {
let p = make_provider(
json!({
"ANTHROPIC_DEFAULT_SONNET_MODEL": "MiniMax-M2",
"ANTHROPIC_DEFAULT_OPUS_MODEL": "MiniMax-M2",
"ANTHROPIC_DEFAULT_HAIKU_MODEL": "MiniMax-M2",
}),
None,
);
let routes = suggested_claude_desktop_routes(&p).expect("routes built");
assert_eq!(routes.len(), 1, "three aliases → one merged route");
let r = routes
.get("claude-sonnet-4-6")
.expect("merged route present");
assert_eq!(r.model, "MiniMax-M2");
assert_eq!(r.label_override.as_deref(), Some("MiniMax-M2"));
}
#[test]
fn same_upstream_with_partial_1m_marker_takes_or_aggregation() {
// sonnet 带 [1M]opus/haiku 不带 → 合并后 supports_1m == Some(true)
let p = make_provider(
json!({
"ANTHROPIC_DEFAULT_SONNET_MODEL": "MiniMax-M2[1M]",
"ANTHROPIC_DEFAULT_OPUS_MODEL": "MiniMax-M2",
"ANTHROPIC_DEFAULT_HAIKU_MODEL": "MiniMax-M2",
}),
None,
);
let routes = suggested_claude_desktop_routes(&p).expect("routes built");
assert_eq!(routes.len(), 1);
let r = routes
.get("claude-sonnet-4-6")
.expect("merged route present");
assert_eq!(r.supports_1m, Some(true));
}
#[test]
fn different_upstream_models_produce_separate_routes() {
let p = make_provider(
json!({
"ANTHROPIC_DEFAULT_SONNET_MODEL": "GLM-4.6",
"ANTHROPIC_DEFAULT_OPUS_MODEL": "GLM-4-Air",
"ANTHROPIC_DEFAULT_HAIKU_MODEL": "GLM-4-Flash",
}),
None,
);
let routes = suggested_claude_desktop_routes(&p).expect("routes built");
assert_eq!(routes.len(), 3);
assert_eq!(routes.get("claude-sonnet-4-6").unwrap().model, "GLM-4.6");
assert_eq!(routes.get("claude-opus-4-7").unwrap().model, "GLM-4-Air");
assert_eq!(routes.get("claude-haiku-4-5").unwrap().model, "GLM-4-Flash");
assert_eq!(
routes
.get("claude-sonnet-4-6")
.unwrap()
.label_override
.as_deref(),
Some("GLM-4.6")
);
}
#[test]
fn anthropic_model_fallback_only_triggers_when_empty() {
// 三个 default env_key 都不填,仅 ANTHROPIC_MODEL
let p = make_provider(
json!({
"ANTHROPIC_MODEL": "kimi-k2",
}),
None,
);
let routes = suggested_claude_desktop_routes(&p).expect("routes built");
assert_eq!(routes.len(), 1);
let r = routes
.get("claude-sonnet-4-6")
.expect("fallback route present");
assert_eq!(r.model, "kimi-k2");
assert_eq!(r.label_override.as_deref(), Some("kimi-k2"));
}
#[test]
fn existing_claude_prefix_not_duplicated() {
let p = make_provider(
json!({
"ANTHROPIC_DEFAULT_SONNET_MODEL": "claude-sonnet-4-5-20250929",
}),
None,
);
let routes = suggested_claude_desktop_routes(&p).expect("routes built");
assert!(routes.contains_key("claude-sonnet-4-6"));
assert!(!routes.contains_key("claude-claude-sonnet-4-5-20250929"));
assert_eq!(
routes
.get("claude-sonnet-4-6")
.expect("route")
.label_override,
None
);
}
}
+1 -40
View File
@@ -15,24 +15,6 @@ pub async fn start_proxy_server(
state.proxy_service.start().await
}
/// 停止代理服务器(仅停止服务,不恢复/清理 Live 接管状态)
#[tauri::command]
pub async fn stop_proxy_server(state: tauri::State<'_, AppState>) -> Result<(), String> {
let takeover = state.proxy_service.get_takeover_status().await?;
if takeover.claude
|| takeover.codex
|| takeover.gemini
|| takeover.opencode
|| takeover.openclaw
{
return Err(
"仍有应用处于代理接管状态,请先在设置中关闭对应应用接管后再停止本地路由。".to_string(),
);
}
state.proxy_service.stop().await
}
/// 停止代理服务器(恢复 Live 配置)
#[tauri::command]
pub async fn stop_proxy_with_restore(state: tauri::State<'_, AppState>) -> Result<(), String> {
@@ -133,17 +115,9 @@ pub async fn update_proxy_config_for_app(
config: AppProxyConfig,
) -> Result<(), String> {
let db = &state.db;
let app_type = config.app_type.clone();
let circuit_config = CircuitBreakerConfig::from(&config);
db.update_proxy_config_for_app(config)
.await
.map_err(|e| e.to_string())?;
state
.proxy_service
.update_circuit_breaker_config_for_app(&app_type, circuit_config)
.await
.map_err(|e| e.to_string())
}
async fn get_default_cost_multiplier_internal(
@@ -279,19 +253,6 @@ pub async fn switch_proxy_provider(
app_type: String,
provider_id: String,
) -> Result<(), String> {
// Block official providers during proxy takeover
let provider = state
.db
.get_provider_by_id(&provider_id, &app_type)
.map_err(|e| format!("读取供应商失败: {e}"))?
.ok_or_else(|| format!("供应商不存在: {provider_id}"))?;
if provider.category.as_deref() == Some("official") {
return Err(
"代理接管模式下不能切换到官方供应商 (Cannot switch to official provider during proxy takeover)"
.to_string(),
);
}
state
.proxy_service
.switch_proxy_target(&app_type, &provider_id)
+49 -65
View File
@@ -42,8 +42,6 @@ pub async fn save_settings(settings: crate::settings::AppSettings) -> Result<boo
/// 重启应用程序(当 app_config_dir 变更后使用)
#[tauri::command]
pub async fn restart_app(app: AppHandle) -> Result<bool, String> {
crate::save_window_state_before_exit(&app);
// 在后台延迟重启,让函数有时间返回响应
tauri::async_runtime::spawn(async move {
tokio::time::sleep(tokio::time::Duration::from_millis(100)).await;
@@ -87,15 +85,13 @@ mod tests {
#[test]
fn save_settings_should_preserve_existing_webdav_when_payload_omits_it() {
let existing = AppSettings {
webdav_sync: Some(WebDavSyncSettings {
base_url: "https://dav.example.com".to_string(),
username: "alice".to_string(),
password: "secret".to_string(),
..WebDavSyncSettings::default()
}),
..AppSettings::default()
};
let mut existing = AppSettings::default();
existing.webdav_sync = Some(WebDavSyncSettings {
base_url: "https://dav.example.com".to_string(),
username: "alice".to_string(),
password: "secret".to_string(),
..WebDavSyncSettings::default()
});
let incoming = AppSettings::default();
let merged = merge_settings_for_save(incoming, &existing);
@@ -109,25 +105,21 @@ mod tests {
#[test]
fn save_settings_should_keep_incoming_webdav_when_present() {
let existing = AppSettings {
webdav_sync: Some(WebDavSyncSettings {
base_url: "https://dav.old.example.com".to_string(),
username: "old".to_string(),
password: "old-pass".to_string(),
..WebDavSyncSettings::default()
}),
..AppSettings::default()
};
let mut existing = AppSettings::default();
existing.webdav_sync = Some(WebDavSyncSettings {
base_url: "https://dav.old.example.com".to_string(),
username: "old".to_string(),
password: "old-pass".to_string(),
..WebDavSyncSettings::default()
});
let incoming = AppSettings {
webdav_sync: Some(WebDavSyncSettings {
base_url: "https://dav.new.example.com".to_string(),
username: "new".to_string(),
password: "new-pass".to_string(),
..WebDavSyncSettings::default()
}),
..AppSettings::default()
};
let mut incoming = AppSettings::default();
incoming.webdav_sync = Some(WebDavSyncSettings {
base_url: "https://dav.new.example.com".to_string(),
username: "new".to_string(),
password: "new-pass".to_string(),
..WebDavSyncSettings::default()
});
let merged = merge_settings_for_save(incoming, &existing);
@@ -143,26 +135,22 @@ mod tests {
/// must NOT overwrite the existing one.
#[test]
fn save_settings_should_preserve_password_when_incoming_has_empty_password() {
let existing = AppSettings {
webdav_sync: Some(WebDavSyncSettings {
base_url: "https://dav.example.com".to_string(),
username: "alice".to_string(),
password: "secret".to_string(),
..WebDavSyncSettings::default()
}),
..AppSettings::default()
};
let mut existing = AppSettings::default();
existing.webdav_sync = Some(WebDavSyncSettings {
base_url: "https://dav.example.com".to_string(),
username: "alice".to_string(),
password: "secret".to_string(),
..WebDavSyncSettings::default()
});
// Simulate frontend sending settings with cleared password
let incoming = AppSettings {
webdav_sync: Some(WebDavSyncSettings {
base_url: "https://dav.example.com".to_string(),
username: "alice".to_string(),
password: "".to_string(),
..WebDavSyncSettings::default()
}),
..AppSettings::default()
};
let mut incoming = AppSettings::default();
incoming.webdav_sync = Some(WebDavSyncSettings {
base_url: "https://dav.example.com".to_string(),
username: "alice".to_string(),
password: "".to_string(),
..WebDavSyncSettings::default()
});
let merged = merge_settings_for_save(incoming, &existing);
@@ -177,25 +165,21 @@ mod tests {
/// work without panicking and keep the empty state.
#[test]
fn save_settings_should_handle_both_empty_passwords() {
let existing = AppSettings {
webdav_sync: Some(WebDavSyncSettings {
base_url: "https://dav.example.com".to_string(),
username: "alice".to_string(),
password: "".to_string(),
..WebDavSyncSettings::default()
}),
..AppSettings::default()
};
let mut existing = AppSettings::default();
existing.webdav_sync = Some(WebDavSyncSettings {
base_url: "https://dav.example.com".to_string(),
username: "alice".to_string(),
password: "".to_string(),
..WebDavSyncSettings::default()
});
let incoming = AppSettings {
webdav_sync: Some(WebDavSyncSettings {
base_url: "https://dav.example.com".to_string(),
username: "alice".to_string(),
password: "".to_string(),
..WebDavSyncSettings::default()
}),
..AppSettings::default()
};
let mut incoming = AppSettings::default();
incoming.webdav_sync = Some(WebDavSyncSettings {
base_url: "https://dav.example.com".to_string(),
username: "alice".to_string(),
password: "".to_string(),
..WebDavSyncSettings::default()
});
let merged = merge_settings_for_save(incoming, &existing);
+7 -2
View File
@@ -12,7 +12,6 @@ use crate::services::skill::{
SkillsShSearchResult,
};
use crate::store::AppState;
use std::str::FromStr;
use std::sync::Arc;
use tauri::State;
@@ -21,7 +20,13 @@ pub struct SkillServiceState(pub Arc<SkillService>);
/// 解析 app 参数为 AppType
fn parse_app_type(app: &str) -> Result<AppType, String> {
AppType::from_str(app).map_err(|e| e.to_string())
match app.to_lowercase().as_str() {
"claude" => Ok(AppType::Claude),
"codex" => Ok(AppType::Codex),
"gemini" => Ok(AppType::Gemini),
"opencode" => Ok(AppType::OpenCode),
_ => Err(format!("不支持的 app 类型: {app}")),
}
}
// ========== 统一管理命令 ==========
+9 -19
View File
@@ -116,25 +116,15 @@ pub async fn stream_check_all_providers(
claude_api_format_override,
)
.await
.unwrap_or_else(|e| {
let (http_status, message) = match &e {
crate::error::AppError::HttpStatus { status, .. } => (
Some(*status),
StreamCheckService::classify_http_status(*status).to_string(),
),
_ => (None, e.to_string()),
};
StreamCheckResult {
status: HealthStatus::Failed,
success: false,
message,
response_time_ms: None,
http_status,
model_used: String::new(),
tested_at: chrono::Utc::now().timestamp(),
retry_count: 0,
error_category: None,
}
.unwrap_or_else(|e| StreamCheckResult {
status: HealthStatus::Failed,
success: false,
message: e.to_string(),
response_time_ms: None,
http_status: None,
model_used: String::new(),
tested_at: chrono::Utc::now().timestamp(),
retry_count: 0,
});
let _ = state
+4 -35
View File
@@ -1,41 +1,10 @@
use std::str::FromStr;
use tauri::{Emitter, State};
use crate::app_config::AppType;
use crate::services::subscription::{CredentialStatus, SubscriptionQuota};
use crate::store::AppState;
use crate::services::subscription::SubscriptionQuota;
/// 查询官方订阅额度
///
/// 读取 CLI 工具已有的 OAuth 凭据并调用官方 API 获取使用额度。
/// 结果(无论业务失败还是 transport 层 Err)都会写入 `UsageCache`、通知托盘
/// 刷新,并 emit `usage-cache-updated`,让前端 React Query 与托盘共享同一份
/// 最新数据。失败快照写入后 `format_subscription_summary` 会通过 `success=false`
/// 守卫返回 `None`,托盘 suffix 自然消失,避免长期滞留旧配额数字。
/// Err 原样向前端返回,React Query 的 onError 不会被吞掉。
/// 不需要 AppState(不访问数据库),直接读文件 + 发 HTTP。
#[tauri::command]
pub async fn get_subscription_quota(
app: tauri::AppHandle,
state: State<'_, AppState>,
tool: String,
) -> Result<SubscriptionQuota, String> {
let inner = crate::services::subscription::get_subscription_quota(&tool).await;
let snapshot = match &inner {
Ok(q) => q.clone(),
// transport 层 Err —— 凭据状态不明,用 Valid 表达"凭据没问题,是通信/parse 出错"。
Err(err_msg) => SubscriptionQuota::error(&tool, CredentialStatus::Valid, err_msg.clone()),
};
if let Ok(app_type) = AppType::from_str(&tool) {
let payload = serde_json::json!({
"kind": "subscription",
"appType": app_type.as_str(),
"data": &snapshot,
});
if let Err(e) = app.emit("usage-cache-updated", payload) {
log::error!("emit usage-cache-updated (subscription) 失败: {e}");
}
state.usage_cache.put_subscription(app_type, snapshot);
crate::tray::schedule_tray_refresh(&app);
}
inner
pub async fn get_subscription_quota(tool: String) -> Result<SubscriptionQuota, String> {
crate::services::subscription::get_subscription_quota(&tool).await
}
+18 -82
View File
@@ -3,8 +3,6 @@
use crate::error::AppError;
use crate::services::usage_stats::*;
use crate::store::AppState;
use rust_decimal::Decimal;
use std::str::FromStr;
use tauri::State;
/// 获取使用量汇总
@@ -20,16 +18,6 @@ pub fn get_usage_summary(
.get_usage_summary(start_date, end_date, app_type.as_deref())
}
/// 获取按 app_type 拆分的使用量汇总
#[tauri::command]
pub fn get_usage_summary_by_app(
state: State<'_, AppState>,
start_date: Option<i64>,
end_date: Option<i64>,
) -> Result<Vec<UsageSummaryByApp>, AppError> {
state.db.get_usage_summary_by_app(start_date, end_date)
}
/// 获取每日趋势
#[tauri::command]
pub fn get_usage_trends(
@@ -47,26 +35,18 @@ pub fn get_usage_trends(
#[tauri::command]
pub fn get_provider_stats(
state: State<'_, AppState>,
start_date: Option<i64>,
end_date: Option<i64>,
app_type: Option<String>,
) -> Result<Vec<ProviderStats>, AppError> {
state
.db
.get_provider_stats(start_date, end_date, app_type.as_deref())
state.db.get_provider_stats(app_type.as_deref())
}
/// 获取模型统计
#[tauri::command]
pub fn get_model_stats(
state: State<'_, AppState>,
start_date: Option<i64>,
end_date: Option<i64>,
app_type: Option<String>,
) -> Result<Vec<ModelStats>, AppError> {
state
.db
.get_model_stats(start_date, end_date, app_type.as_deref())
state.db.get_model_stats(app_type.as_deref())
}
/// 获取请求日志列表
@@ -151,67 +131,23 @@ pub fn update_model_pricing(
cache_creation_cost: String,
) -> Result<(), AppError> {
let db = state.db.clone();
let model_id = model_id.trim().to_string();
let display_name = display_name.trim().to_string();
if model_id.is_empty() {
return Err(AppError::localized(
"usage.modelIdRequired",
"模型 ID 不能为空",
"Model ID is required",
));
}
if display_name.is_empty() {
return Err(AppError::localized(
"usage.displayNameRequired",
"显示名称不能为空",
"Display name is required",
));
}
let conn = crate::database::lock_conn!(db.conn);
for (label, value) in [
("input_cost", &input_cost),
("output_cost", &output_cost),
("cache_read_cost", &cache_read_cost),
("cache_creation_cost", &cache_creation_cost),
] {
let parsed = Decimal::from_str(value.trim()).map_err(|e| {
AppError::localized(
"usage.invalidPrice",
format!("{label} 价格无效: {value} - {e}"),
format!("{label} price is invalid: {value} - {e}"),
)
})?;
if parsed < Decimal::ZERO {
return Err(AppError::localized(
"usage.invalidPrice",
format!("{label} 价格必须为非负数: {value}"),
format!("{label} price must be non-negative: {value}"),
));
}
}
{
let conn = crate::database::lock_conn!(db.conn);
conn.execute(
"INSERT OR REPLACE INTO model_pricing (
model_id, display_name, input_cost_per_million, output_cost_per_million,
cache_read_cost_per_million, cache_creation_cost_per_million
) VALUES (?1, ?2, ?3, ?4, ?5, ?6)",
rusqlite::params![
model_id,
display_name,
input_cost.trim(),
output_cost.trim(),
cache_read_cost.trim(),
cache_creation_cost.trim()
],
)
.map_err(|e| AppError::Database(format!("更新模型定价失败: {e}")))?;
}
if let Err(e) = db.backfill_missing_usage_costs_for_model(&model_id) {
log::warn!("模型定价更新后回填历史用量成本失败 (model_id={model_id}): {e}");
}
conn.execute(
"INSERT OR REPLACE INTO model_pricing (
model_id, display_name, input_cost_per_million, output_cost_per_million,
cache_read_cost_per_million, cache_creation_cost_per_million
) VALUES (?1, ?2, ?3, ?4, ?5, ?6)",
rusqlite::params![
model_id,
display_name,
input_cost,
output_cost,
cache_read_cost,
cache_creation_cost
],
)
.map_err(|e| AppError::Database(format!("更新模型定价失败: {e}")))?;
Ok(())
}
+3 -120
View File
@@ -1,5 +1,4 @@
use serde::{Deserialize, Serialize};
use serde_json::{Map, Value};
use std::fs;
use std::io::Write;
use std::path::{Path, PathBuf};
@@ -160,34 +159,15 @@ pub fn read_json_file<T: for<'a> Deserialize<'a>>(path: &Path) -> Result<T, AppE
serde_json::from_str(&content).map_err(|e| AppError::json(path, e))
}
/// 递归排序 JSON 对象的键(按字母顺序),确保序列化输出是确定性的
fn sort_json_keys(value: &Value) -> Value {
match value {
Value::Object(map) => {
let mut sorted_map = Map::new();
let mut keys: Vec<_> = map.keys().collect();
keys.sort();
for key in keys {
sorted_map.insert(key.clone(), sort_json_keys(&map[key]));
}
Value::Object(sorted_map)
}
Value::Array(arr) => Value::Array(arr.iter().map(sort_json_keys).collect()),
other => other.clone(),
}
}
/// 写入 JSON 配置文件(键按字母排序,确保确定性输出)
/// 写入 JSON 配置文件
pub fn write_json_file<T: Serialize>(path: &Path, data: &T) -> Result<(), AppError> {
// 确保目录存在
if let Some(parent) = path.parent() {
fs::create_dir_all(parent).map_err(|e| AppError::io(parent, e))?;
}
let value = serde_json::to_value(data).map_err(|e| AppError::JsonSerialize { source: e })?;
let sorted_value = sort_json_keys(&value);
let json = serde_json::to_string_pretty(&sorted_value)
.map_err(|e| AppError::JsonSerialize { source: e })?;
let json =
serde_json::to_string_pretty(data).map_err(|e| AppError::JsonSerialize { source: e })?;
atomic_write(path, json.as_bytes())
}
@@ -291,103 +271,6 @@ mod tests {
let override_dir = PathBuf::from("/");
assert!(derive_mcp_path_from_override(&override_dir).is_none());
}
#[test]
fn sort_json_keys_sorts_top_level_object() {
let input = serde_json::json!({
"z": 1,
"a": 2,
"m": 3,
});
let sorted = sort_json_keys(&input);
let serialized = serde_json::to_string(&sorted).unwrap();
assert_eq!(serialized, r#"{"a":2,"m":3,"z":1}"#);
}
#[test]
fn sort_json_keys_recurses_into_nested_objects() {
let input = serde_json::json!({
"outer_b": {"z": 1, "a": 2},
"outer_a": {"y": 3, "b": 4},
});
let sorted = sort_json_keys(&input);
let serialized = serde_json::to_string(&sorted).unwrap();
assert_eq!(
serialized,
r#"{"outer_a":{"b":4,"y":3},"outer_b":{"a":2,"z":1}}"#
);
}
#[test]
fn sort_json_keys_preserves_array_order() {
let input = serde_json::json!([3, 1, 2]);
let sorted = sort_json_keys(&input);
let serialized = serde_json::to_string(&sorted).unwrap();
assert_eq!(serialized, "[3,1,2]");
}
#[test]
fn sort_json_keys_sorts_objects_inside_arrays_but_keeps_array_order() {
let input = serde_json::json!([
{"z": 1, "a": 2},
{"y": 3, "b": 4},
]);
let sorted = sort_json_keys(&input);
let serialized = serde_json::to_string(&sorted).unwrap();
assert_eq!(serialized, r#"[{"a":2,"z":1},{"b":4,"y":3}]"#);
}
#[test]
fn sort_json_keys_passes_through_primitives() {
let cases = vec![
serde_json::json!("hello"),
serde_json::json!(42),
serde_json::json!(3.5),
serde_json::json!(true),
serde_json::json!(null),
];
for value in cases {
let sorted = sort_json_keys(&value);
assert_eq!(sorted, value);
}
}
#[test]
fn sort_json_keys_handles_empty_collections() {
let empty_obj = serde_json::json!({});
assert_eq!(
serde_json::to_string(&sort_json_keys(&empty_obj)).unwrap(),
"{}"
);
let empty_arr = serde_json::json!([]);
assert_eq!(
serde_json::to_string(&sort_json_keys(&empty_arr)).unwrap(),
"[]"
);
}
#[test]
fn sort_json_keys_produces_identical_output_for_different_insertion_orders() {
// 核心保证:同一逻辑配置无论键的插入顺序如何,写出的字节序列必须一致。
let mut a = Map::new();
a.insert("env".to_string(), serde_json::json!({"PATH": "/usr/bin"}));
a.insert("model".to_string(), serde_json::json!("claude-sonnet-4-5"));
a.insert("permissions".to_string(), serde_json::json!({"allow": []}));
let mut b = Map::new();
b.insert("permissions".to_string(), serde_json::json!({"allow": []}));
b.insert("model".to_string(), serde_json::json!("claude-sonnet-4-5"));
b.insert("env".to_string(), serde_json::json!({"PATH": "/usr/bin"}));
let sorted_a = sort_json_keys(&Value::Object(a));
let sorted_b = sort_json_keys(&Value::Object(b));
assert_eq!(
serde_json::to_string(&sorted_a).unwrap(),
serde_json::to_string(&sorted_b).unwrap(),
);
}
}
/// 复制文件
+2 -4
View File
@@ -791,10 +791,8 @@ mod tests {
std::fs::create_dir_all(&test_home).expect("create test home");
std::env::set_var("CC_SWITCH_TEST_HOME", &test_home);
let settings = AppSettings {
backup_interval_hours: Some(0),
..AppSettings::default()
};
let mut settings = AppSettings::default();
settings.backup_interval_hours = Some(0);
update_settings(settings).expect("disable auto backup");
let db = Database::memory()?;
+1 -4
View File
@@ -14,8 +14,6 @@ pub struct FailoverQueueItem {
pub provider_id: String,
pub provider_name: String,
pub sort_index: Option<usize>,
#[serde(skip_serializing_if = "Option::is_none")]
pub provider_notes: Option<String>,
}
impl Database {
@@ -25,7 +23,7 @@ impl Database {
let mut stmt = conn
.prepare(
"SELECT id, name, sort_index, notes
"SELECT id, name, sort_index
FROM providers
WHERE app_type = ?1 AND in_failover_queue = 1
ORDER BY COALESCE(sort_index, 999999), id ASC",
@@ -38,7 +36,6 @@ impl Database {
provider_id: row.get(0)?,
provider_name: row.get(1)?,
sort_index: row.get(2)?,
provider_notes: row.get(3)?,
})
})
.map_err(|e| AppError::Database(e.to_string()))?
+3 -6
View File
@@ -13,7 +13,7 @@ impl Database {
pub fn get_all_mcp_servers(&self) -> Result<IndexMap<String, McpServer>, AppError> {
let conn = lock_conn!(self.conn);
let mut stmt = conn.prepare(
"SELECT id, name, server_config, description, homepage, docs, tags, enabled_claude, enabled_codex, enabled_gemini, enabled_opencode, enabled_hermes
"SELECT id, name, server_config, description, homepage, docs, tags, enabled_claude, enabled_codex, enabled_gemini, enabled_opencode
FROM mcp_servers
ORDER BY name ASC, id ASC"
).map_err(|e| AppError::Database(e.to_string()))?;
@@ -31,7 +31,6 @@ impl Database {
let enabled_codex: bool = row.get(8)?;
let enabled_gemini: bool = row.get(9)?;
let enabled_opencode: bool = row.get(10)?;
let enabled_hermes: bool = row.get(11)?;
let server = serde_json::from_str(&server_config_str).unwrap_or_default();
let tags = serde_json::from_str(&tags_str).unwrap_or_default();
@@ -47,7 +46,6 @@ impl Database {
codex: enabled_codex,
gemini: enabled_gemini,
opencode: enabled_opencode,
hermes: enabled_hermes,
},
description,
homepage,
@@ -72,8 +70,8 @@ impl Database {
conn.execute(
"INSERT OR REPLACE INTO mcp_servers (
id, name, server_config, description, homepage, docs, tags,
enabled_claude, enabled_codex, enabled_gemini, enabled_opencode, enabled_hermes
) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12)",
enabled_claude, enabled_codex, enabled_gemini, enabled_opencode
) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11)",
params![
server.id,
server.name,
@@ -89,7 +87,6 @@ impl Database {
server.apps.codex,
server.apps.gemini,
server.apps.opencode,
server.apps.hermes,
],
)
.map_err(|e| AppError::Database(e.to_string()))?;
-150
View File
@@ -535,22 +535,6 @@ impl Database {
Ok(ids)
}
/// 判断指定 app 下是否已存在任意 provider。
///
/// 启动阶段的 live import 需要使用这个更严格的判断:
/// 只要该 app 已经有任何 provider(包括官方 seed),就不应再自动导入 `default`。
pub fn has_any_provider_for_app(&self, app_type: &str) -> Result<bool, AppError> {
let conn = lock_conn!(self.conn);
let exists: bool = conn
.query_row(
"SELECT EXISTS(SELECT 1 FROM providers WHERE app_type = ?1)",
params![app_type],
|row| row.get(0),
)
.map_err(|e| AppError::Database(e.to_string()))?;
Ok(exists)
}
/// 判断指定 app 下是否存在非官方种子的供应商。
///
/// 比 `get_all_providers` 轻量得多:只读 id 列、无 endpoint 子查询、首条命中即返回。
@@ -649,138 +633,4 @@ impl Database {
Ok(inserted)
}
/// 按 id 兜底插入单条 official seed(仅当目标表中该 id 不存在时插入)。
///
/// 与 `init_default_official_providers` 不同:
/// - 不触碰 `official_providers_seeded` 全局 flag,是 on-demand 修复
/// - 只处理一条 seed,由调用方决定 id + app_type
/// - 已存在则尊重用户自定义,不覆盖
///
/// 返回 Ok(true) 表示插入了新行,Ok(false) 表示已存在被跳过。
pub fn ensure_official_seed_by_id(
&self,
seed_id: &str,
app_type: crate::app_config::AppType,
) -> Result<bool, AppError> {
use crate::database::dao::providers_seed::OFFICIAL_SEEDS;
let seed = OFFICIAL_SEEDS
.iter()
.find(|s| s.id == seed_id && s.app_type == app_type)
.ok_or_else(|| {
AppError::Database(format!(
"unknown official seed: id={seed_id}, app_type={}",
app_type.as_str()
))
})?;
let app_type_str = seed.app_type.as_str();
if self.get_provider_by_id(seed_id, app_type_str)?.is_some() {
return Ok(false);
}
let settings_config: serde_json::Value = serde_json::from_str(seed.settings_config_json)
.map_err(|e| {
AppError::Database(format!("Seed JSON parse failed for {}: {e}", seed.id))
})?;
let next_sort_index = self.next_sort_index_for_app(app_type_str)?;
let now_ms = chrono::Utc::now().timestamp_millis();
let mut provider = Provider::with_id(
seed.id.to_string(),
seed.name.to_string(),
settings_config,
Some(seed.website_url.to_string()),
);
provider.category = Some("official".to_string());
provider.icon = Some(seed.icon.to_string());
provider.icon_color = Some(seed.icon_color.to_string());
provider.sort_index = Some(next_sort_index);
provider.created_at = Some(now_ms);
self.save_provider(app_type_str, &provider)?;
Ok(true)
}
}
#[cfg(test)]
mod ensure_official_seed_tests {
use crate::app_config::AppType;
use crate::database::{Database, CLAUDE_DESKTOP_OFFICIAL_PROVIDER_ID};
#[test]
fn ensure_inserts_when_missing() {
let db = Database::memory().expect("memory db");
let inserted = db
.ensure_official_seed_by_id(CLAUDE_DESKTOP_OFFICIAL_PROVIDER_ID, AppType::ClaudeDesktop)
.expect("ensure ok");
assert!(inserted, "should insert when missing");
let provider = db
.get_provider_by_id(
CLAUDE_DESKTOP_OFFICIAL_PROVIDER_ID,
AppType::ClaudeDesktop.as_str(),
)
.expect("query ok")
.expect("provider exists after ensure");
assert_eq!(provider.id, CLAUDE_DESKTOP_OFFICIAL_PROVIDER_ID);
assert_eq!(provider.name, "Claude Desktop Official");
assert_eq!(provider.category.as_deref(), Some("official"));
assert_eq!(provider.icon.as_deref(), Some("anthropic"));
assert_eq!(provider.icon_color.as_deref(), Some("#D4915D"));
}
#[test]
fn ensure_skips_when_present_and_preserves_customization() {
let db = Database::memory().expect("memory db");
db.init_default_official_providers().expect("seed");
let mut renamed = db
.get_provider_by_id(
CLAUDE_DESKTOP_OFFICIAL_PROVIDER_ID,
AppType::ClaudeDesktop.as_str(),
)
.expect("query ok")
.expect("seed present");
renamed.name = "My Custom Backup".to_string();
db.save_provider(AppType::ClaudeDesktop.as_str(), &renamed)
.expect("save customization");
let inserted = db
.ensure_official_seed_by_id(CLAUDE_DESKTOP_OFFICIAL_PROVIDER_ID, AppType::ClaudeDesktop)
.expect("ensure ok");
assert!(!inserted, "should skip when present");
let after = db
.get_provider_by_id(
CLAUDE_DESKTOP_OFFICIAL_PROVIDER_ID,
AppType::ClaudeDesktop.as_str(),
)
.expect("query ok")
.expect("still present");
assert_eq!(
after.name, "My Custom Backup",
"customization must not be overwritten"
);
}
#[test]
fn ensure_rejects_unknown_seed() {
let db = Database::memory().expect("memory db");
let result = db.ensure_official_seed_by_id("nonexistent-id", AppType::ClaudeDesktop);
assert!(result.is_err(), "unknown seed id should be Err");
}
#[test]
fn ensure_rejects_seed_app_type_mismatch() {
let db = Database::memory().expect("memory db");
let result =
db.ensure_official_seed_by_id(CLAUDE_DESKTOP_OFFICIAL_PROVIDER_ID, AppType::Claude);
assert!(result.is_err(), "(id, app_type) mismatch should be Err");
}
}
+1 -29
View File
@@ -10,8 +10,6 @@
use crate::app_config::AppType;
pub(crate) const CLAUDE_DESKTOP_OFFICIAL_PROVIDER_ID: &str = "claude-desktop-official";
/// 单条官方供应商种子定义。
pub(crate) struct OfficialProviderSeed {
pub id: &'static str,
@@ -24,7 +22,7 @@ pub(crate) struct OfficialProviderSeed {
pub settings_config_json: &'static str,
}
/// Claude / Claude Desktop / Codex / Gemini 的官方预设。
/// Claude / Codex / Gemini 三个应用的官方预设。
///
/// id 固定,便于幂等检查;name 直接用英文原名(与前端预设一致),不做 i18n。
pub(crate) const OFFICIAL_SEEDS: &[OfficialProviderSeed] = &[
@@ -38,16 +36,6 @@ pub(crate) const OFFICIAL_SEEDS: &[OfficialProviderSeed] = &[
// 空 env 让用户走 Claude CLI 默认认证流程
settings_config_json: r#"{"env":{}}"#,
},
OfficialProviderSeed {
id: CLAUDE_DESKTOP_OFFICIAL_PROVIDER_ID,
app_type: AppType::ClaudeDesktop,
name: "Claude Desktop Official",
website_url: "https://claude.ai/download",
icon: "anthropic",
icon_color: "#D4915D",
// 空 env 只是占位;切换该 provider 时会恢复 Claude Desktop 1P 模式
settings_config_json: r#"{"env":{}}"#,
},
OfficialProviderSeed {
id: "codex-official",
app_type: AppType::Codex,
@@ -76,19 +64,3 @@ pub(crate) const OFFICIAL_SEEDS: &[OfficialProviderSeed] = &[
pub(crate) fn is_official_seed_id(id: &str) -> bool {
OFFICIAL_SEEDS.iter().any(|seed| seed.id == id)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn official_seeds_include_claude_desktop() {
let seed = OFFICIAL_SEEDS
.iter()
.find(|seed| seed.id == CLAUDE_DESKTOP_OFFICIAL_PROVIDER_ID)
.expect("claude desktop official seed");
assert_eq!(seed.app_type, AppType::ClaudeDesktop);
assert!(is_official_seed_id(CLAUDE_DESKTOP_OFFICIAL_PROVIDER_ID));
}
}
+23 -59
View File
@@ -2,56 +2,12 @@
//!
//! 处理代理配置、Provider健康状态和使用统计的数据库操作
use std::str::FromStr;
use crate::error::AppError;
use crate::proxy::types::*;
use rust_decimal::Decimal;
use super::super::{lock_conn, Database};
pub(crate) const PRICING_SOURCE_RESPONSE: &str = "response";
pub(crate) const PRICING_SOURCE_REQUEST: &str = "request";
pub(crate) fn validate_cost_multiplier(value: &str) -> Result<Decimal, AppError> {
let trimmed = value.trim();
if trimmed.is_empty() {
return Err(AppError::localized(
"error.multiplierEmpty",
"倍率不能为空",
"Multiplier cannot be empty",
));
}
let parsed = Decimal::from_str(trimmed).map_err(|e| {
AppError::localized(
"error.invalidMultiplier",
format!("无效倍率: {value} - {e}"),
format!("Invalid multiplier: {value} - {e}"),
)
})?;
if parsed < Decimal::ZERO {
return Err(AppError::localized(
"error.invalidMultiplier",
format!("无效倍率: {value} - 倍率不能为负数"),
format!("Invalid multiplier: {value} - multiplier cannot be negative"),
));
}
Ok(parsed)
}
pub(crate) fn validate_pricing_source(value: &str) -> Result<&str, AppError> {
let trimmed = value.trim();
if trimmed == PRICING_SOURCE_RESPONSE || trimmed == PRICING_SOURCE_REQUEST {
Ok(trimmed)
} else {
Err(AppError::localized(
"error.invalidPricingMode",
format!("无效计费模式: {value}"),
format!("Invalid pricing mode: {value}"),
))
}
}
impl Database {
// ==================== Global Proxy Config ====================
@@ -147,8 +103,21 @@ impl Database {
app_type: &str,
value: &str,
) -> Result<(), AppError> {
validate_cost_multiplier(value)?;
let trimmed = value.trim();
if trimmed.is_empty() {
return Err(AppError::localized(
"error.multiplierEmpty",
"倍率不能为空",
"Multiplier cannot be empty",
));
}
trimmed.parse::<Decimal>().map_err(|e| {
AppError::localized(
"error.invalidMultiplier",
format!("无效倍率: {value} - {e}"),
format!("Invalid multiplier: {value} - {e}"),
)
})?;
// 确保行存在
self.ensure_proxy_config_row_exists(app_type)?;
@@ -181,7 +150,7 @@ impl Database {
Ok(value) => Ok(value),
Err(rusqlite::Error::QueryReturnedNoRows) => {
self.init_proxy_config_rows().await?;
Ok(PRICING_SOURCE_RESPONSE.to_string())
Ok("response".to_string())
}
Err(e) => Err(AppError::Database(e.to_string())),
}
@@ -193,7 +162,14 @@ impl Database {
app_type: &str,
value: &str,
) -> Result<(), AppError> {
let trimmed = validate_pricing_source(value)?;
let trimmed = value.trim();
if !matches!(trimmed, "response" | "request") {
return Err(AppError::localized(
"error.invalidPricingMode",
format!("无效计费模式: {value}"),
format!("Invalid pricing mode: {value}"),
));
}
// 确保行存在
self.ensure_proxy_config_row_exists(app_type)?;
@@ -935,18 +911,6 @@ mod tests {
}
));
let err = db
.set_default_cost_multiplier("claude", "-0.5")
.await
.unwrap_err();
assert!(matches!(
err,
AppError::Localized {
key: "error.invalidMultiplier",
..
}
));
Ok(())
}
}
+12 -15
View File
@@ -23,7 +23,7 @@ impl Database {
.prepare(
"SELECT id, name, description, directory, repo_owner, repo_name, repo_branch,
readme_url, enabled_claude, enabled_codex, enabled_gemini, enabled_opencode,
enabled_hermes, installed_at, content_hash, updated_at
installed_at, content_hash, updated_at
FROM skills ORDER BY name ASC",
)
.map_err(|e| AppError::Database(e.to_string()))?;
@@ -44,11 +44,10 @@ impl Database {
codex: row.get(9)?,
gemini: row.get(10)?,
opencode: row.get(11)?,
hermes: row.get(12)?,
},
installed_at: row.get(13)?,
content_hash: row.get(14)?,
updated_at: row.get::<_, i64>(15).unwrap_or(0),
installed_at: row.get(12)?,
content_hash: row.get(13)?,
updated_at: row.get::<_, i64>(14).unwrap_or(0),
})
})
.map_err(|e| AppError::Database(e.to_string()))?;
@@ -68,7 +67,7 @@ impl Database {
.prepare(
"SELECT id, name, description, directory, repo_owner, repo_name, repo_branch,
readme_url, enabled_claude, enabled_codex, enabled_gemini, enabled_opencode,
enabled_hermes, installed_at, content_hash, updated_at
installed_at, content_hash, updated_at
FROM skills WHERE id = ?1",
)
.map_err(|e| AppError::Database(e.to_string()))?;
@@ -88,11 +87,10 @@ impl Database {
codex: row.get(9)?,
gemini: row.get(10)?,
opencode: row.get(11)?,
hermes: row.get(12)?,
},
installed_at: row.get(13)?,
content_hash: row.get(14)?,
updated_at: row.get::<_, i64>(15).unwrap_or(0),
installed_at: row.get(12)?,
content_hash: row.get(13)?,
updated_at: row.get::<_, i64>(14).unwrap_or(0),
})
});
@@ -109,9 +107,9 @@ impl Database {
conn.execute(
"INSERT OR REPLACE INTO skills
(id, name, description, directory, repo_owner, repo_name, repo_branch,
readme_url, enabled_claude, enabled_codex, enabled_gemini, enabled_opencode, enabled_hermes,
readme_url, enabled_claude, enabled_codex, enabled_gemini, enabled_opencode,
installed_at, content_hash, updated_at)
VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, ?13, ?14, ?15, ?16)",
VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, ?13, ?14, ?15)",
params![
skill.id,
skill.name,
@@ -125,7 +123,6 @@ impl Database {
skill.apps.codex,
skill.apps.gemini,
skill.apps.opencode,
skill.apps.hermes,
skill.installed_at,
skill.content_hash,
skill.updated_at,
@@ -157,8 +154,8 @@ impl Database {
let conn = lock_conn!(self.conn);
let affected = conn
.execute(
"UPDATE skills SET enabled_claude = ?1, enabled_codex = ?2, enabled_gemini = ?3, enabled_opencode = ?4, enabled_hermes = ?5 WHERE id = ?6",
params![apps.claude, apps.codex, apps.gemini, apps.opencode, apps.hermes, id],
"UPDATE skills SET enabled_claude = ?1, enabled_codex = ?2, enabled_gemini = ?3, enabled_opencode = ?4 WHERE id = ?5",
params![apps.claude, apps.codex, apps.gemini, apps.opencode, id],
)
.map_err(|e| AppError::Database(e.to_string()))?;
Ok(affected > 0)
+17 -174
View File
@@ -4,62 +4,13 @@
use crate::database::{lock_conn, Database};
use crate::error::AppError;
use crate::services::usage_stats::effective_usage_log_filter;
use chrono::{Duration, Local, TimeZone};
/// Compute the rollup/prune cutoff aligned to a local-day boundary.
///
/// Anything strictly older than the returned timestamp will be aggregated into
/// `usage_daily_rollups` and deleted from `proxy_request_logs`. Aligning to the
/// next local midnight after `(now - retain_days)` guarantees that the youngest
/// rollup row always represents a *complete* local day. Without this alignment
/// the cutoff falls mid-day, leaving the day half-rolled-up and half-pruned —
/// which would silently under-count any range query that touches that day
/// after `compute_rollup_date_bounds` trims partial-coverage rollup days.
fn compute_local_midnight_cutoff(
now: chrono::DateTime<Local>,
retain_days: i64,
) -> Result<i64, AppError> {
let target_day = now
.checked_sub_signed(Duration::days(retain_days))
.ok_or_else(|| AppError::Database("rollup cutoff overflow".to_string()))?
.date_naive();
// Use the *next* day's midnight so anything before it has fully been bucketed.
let next_day = target_day
.succ_opt()
.ok_or_else(|| AppError::Database("rollup cutoff next-day overflow".to_string()))?;
let naive_midnight = next_day
.and_hms_opt(0, 0, 0)
.ok_or_else(|| AppError::Database("rollup cutoff midnight overflow".to_string()))?;
let local_dt = match Local.from_local_datetime(&naive_midnight) {
chrono::LocalResult::Single(dt) => dt,
chrono::LocalResult::Ambiguous(earliest, _) => earliest,
chrono::LocalResult::None => {
// DST gap: fall back to one hour later, which always exists.
let bumped = naive_midnight + Duration::hours(1);
match Local.from_local_datetime(&bumped) {
chrono::LocalResult::Single(dt) => dt,
chrono::LocalResult::Ambiguous(earliest, _) => earliest,
chrono::LocalResult::None => {
return Err(AppError::Database(
"rollup cutoff fell into DST gap".to_string(),
))
}
}
}
};
Ok(local_dt.timestamp())
}
impl Database {
/// Aggregate proxy_request_logs older than `retain_days` into usage_daily_rollups,
/// then delete the aggregated detail rows.
/// Returns the number of deleted detail rows.
pub fn rollup_and_prune(&self, retain_days: i64) -> Result<u64, AppError> {
let cutoff = compute_local_midnight_cutoff(Local::now(), retain_days)?;
let cutoff = chrono::Utc::now().timestamp() - retain_days * 86400;
let conn = lock_conn!(self.conn);
// Check if there are any rows to process
@@ -102,8 +53,7 @@ impl Database {
fn do_rollup_and_prune(conn: &rusqlite::Connection, cutoff: i64) -> Result<u64, AppError> {
// Aggregate old logs, merging with any pre-existing rollup rows via LEFT JOIN.
let effective_filter = effective_usage_log_filter("l");
let aggregation_sql = format!(
conn.execute(
"INSERT OR REPLACE INTO usage_daily_rollups
(date, app_type, provider_id, model,
request_count, success_count,
@@ -126,30 +76,27 @@ impl Database {
ELSE 0 END
FROM (
SELECT
date(l.created_at, 'unixepoch', 'localtime') as d,
l.app_type as a, l.provider_id as p, l.model as m,
date(created_at, 'unixepoch', 'localtime') as d,
app_type as a, provider_id as p, model as m,
COUNT(*) as new_req,
SUM(CASE WHEN l.status_code >= 200 AND l.status_code < 300 THEN 1 ELSE 0 END) as new_succ,
COALESCE(SUM(l.input_tokens), 0) as new_in,
COALESCE(SUM(l.output_tokens), 0) as new_out,
COALESCE(SUM(l.cache_read_tokens), 0) as new_cr,
COALESCE(SUM(l.cache_creation_tokens), 0) as new_cc,
COALESCE(SUM(CAST(l.total_cost_usd AS REAL)), 0) as new_cost,
COALESCE(AVG(l.latency_ms), 0) as new_lat
FROM proxy_request_logs l
WHERE l.created_at < ?1 AND {effective_filter}
SUM(CASE WHEN status_code >= 200 AND status_code < 300 THEN 1 ELSE 0 END) as new_succ,
COALESCE(SUM(input_tokens), 0) as new_in,
COALESCE(SUM(output_tokens), 0) as new_out,
COALESCE(SUM(cache_read_tokens), 0) as new_cr,
COALESCE(SUM(cache_creation_tokens), 0) as new_cc,
COALESCE(SUM(CAST(total_cost_usd AS REAL)), 0) as new_cost,
COALESCE(AVG(latency_ms), 0) as new_lat
FROM proxy_request_logs WHERE created_at < ?1
GROUP BY d, a, p, m
) agg
LEFT JOIN usage_daily_rollups old
ON old.date = agg.d AND old.app_type = agg.a
AND old.provider_id = agg.p AND old.model = agg.m"
);
AND old.provider_id = agg.p AND old.model = agg.m",
[cutoff],
)
.map_err(|e| AppError::Database(format!("Rollup aggregation failed: {e}")))?;
conn.execute(&aggregation_sql, [cutoff])
.map_err(|e| AppError::Database(format!("Rollup aggregation failed: {e}")))?;
// INSERT uses the effective-log filter to exclude duplicate session rows.
// DELETE intentionally prunes all old details so those duplicates are discarded.
// Delete the aggregated detail rows
let deleted = conn
.execute(
"DELETE FROM proxy_request_logs WHERE created_at < ?1",
@@ -163,49 +110,8 @@ impl Database {
#[cfg(test)]
mod tests {
use super::compute_local_midnight_cutoff;
use crate::database::Database;
use crate::error::AppError;
use chrono::{Local, TimeZone};
fn local_dt(
year: i32,
month: u32,
day: u32,
hour: u32,
minute: u32,
second: u32,
) -> chrono::DateTime<Local> {
match Local.with_ymd_and_hms(year, month, day, hour, minute, second) {
chrono::LocalResult::Single(dt) => dt,
chrono::LocalResult::Ambiguous(earliest, _) => earliest,
chrono::LocalResult::None => panic!("invalid local datetime in test fixture"),
}
}
#[test]
fn cutoff_is_aligned_to_local_midnight_after_target_day() -> Result<(), AppError> {
// now = 2026-04-16 14:32:17 local; retain_days = 30
// target day = 2026-03-17; cutoff should be 2026-03-18 00:00 local.
let now = local_dt(2026, 4, 16, 14, 32, 17);
let cutoff_ts = compute_local_midnight_cutoff(now, 30)?;
let cutoff_dt = Local.timestamp_opt(cutoff_ts, 0).single().unwrap();
let expected = local_dt(2026, 3, 18, 0, 0, 0);
assert_eq!(cutoff_dt, expected);
Ok(())
}
#[test]
fn cutoff_at_local_midnight_now_still_lands_on_midnight() -> Result<(), AppError> {
// If `now` is itself local midnight, the math should not introduce drift.
let now = local_dt(2026, 4, 16, 0, 0, 0);
let cutoff_ts = compute_local_midnight_cutoff(now, 7)?;
let cutoff_dt = Local.timestamp_opt(cutoff_ts, 0).single().unwrap();
// (2026-04-16 - 7d) = 2026-04-09; cutoff = 2026-04-10 00:00 local.
let expected = local_dt(2026, 4, 10, 0, 0, 0);
assert_eq!(cutoff_dt, expected);
Ok(())
}
#[test]
fn test_rollup_and_prune() -> Result<(), AppError> {
@@ -259,69 +165,6 @@ mod tests {
Ok(())
}
#[test]
fn test_rollup_uses_effective_usage_logs() -> Result<(), AppError> {
let db = Database::memory()?;
let now = chrono::Utc::now().timestamp();
let old_ts = now - 40 * 86400;
{
let conn = crate::database::lock_conn!(db.conn);
conn.execute(
"INSERT INTO proxy_request_logs (
request_id, provider_id, app_type, model, request_model,
input_tokens, output_tokens, cache_read_tokens, cache_creation_tokens,
total_cost_usd, latency_ms, status_code, created_at, data_source
) VALUES (?1, 'openai', 'codex', 'gpt-5.4', 'gpt-5.4', 100, 20, 10, 0, '0.10', 100, 200, ?2, 'proxy')",
rusqlite::params!["codex-proxy-old", old_ts],
)?;
conn.execute(
"INSERT INTO proxy_request_logs (
request_id, provider_id, app_type, model, request_model,
input_tokens, output_tokens, cache_read_tokens, cache_creation_tokens,
total_cost_usd, latency_ms, status_code, created_at, data_source
) VALUES (?1, '_codex_session', 'codex', 'gpt-5.4', 'gpt-5.4', 100, 20, 10, 0, '0.10', 0, 200, ?2, 'codex_session')",
rusqlite::params!["codex-session-old-dup", old_ts + 60],
)?;
}
let deleted = db.rollup_and_prune(30)?;
assert_eq!(deleted, 2);
let conn = crate::database::lock_conn!(db.conn);
let mut stmt = conn.prepare(
"SELECT provider_id, request_count, input_tokens, output_tokens, cache_read_tokens
FROM usage_daily_rollups WHERE app_type = 'codex'",
)?;
let rows = stmt
.query_map([], |row| {
Ok((
row.get::<_, String>(0)?,
row.get::<_, i64>(1)?,
row.get::<_, i64>(2)?,
row.get::<_, i64>(3)?,
row.get::<_, i64>(4)?,
))
})?
.collect::<Result<Vec<_>, _>>()?;
assert_eq!(rows.len(), 1);
let (provider_id, request_count, input_tokens, output_tokens, cache_read_tokens) = &rows[0];
assert_eq!(provider_id, "openai");
assert_eq!(*request_count, 1);
assert_eq!(*input_tokens, 100);
assert_eq!(*output_tokens, 20);
assert_eq!(*cache_read_tokens, 10);
let remaining: i64 =
conn.query_row("SELECT COUNT(*) FROM proxy_request_logs", [], |row| {
row.get(0)
})?;
assert_eq!(remaining, 0);
Ok(())
}
#[test]
fn test_rollup_noop_when_no_old_data() -> Result<(), AppError> {
let db = Database::memory()?;
+1 -6
View File
@@ -32,11 +32,6 @@ mod schema;
mod tests;
// DAO 类型导出供外部使用
pub(crate) use dao::providers_seed::CLAUDE_DESKTOP_OFFICIAL_PROVIDER_ID;
pub(crate) use dao::proxy::{
validate_cost_multiplier, validate_pricing_source, PRICING_SOURCE_REQUEST,
PRICING_SOURCE_RESPONSE,
};
pub use dao::FailoverQueueItem;
use crate::config::get_app_config_dir;
@@ -49,7 +44,7 @@ use std::sync::Mutex;
/// 当前 Schema 版本号
/// 每次修改表结构时递增,并在 schema.rs 中添加相应的迁移逻辑
pub(crate) const SCHEMA_VERSION: i32 = 10;
pub(crate) const SCHEMA_VERSION: i32 = 8;
/// 安全地序列化 JSON,避免 unwrap panic
pub(crate) fn to_json_string<T: Serialize>(value: &T) -> Result<String, AppError> {
+18 -364
View File
@@ -65,8 +65,7 @@ impl Database {
id TEXT PRIMARY KEY, name TEXT NOT NULL, server_config TEXT NOT NULL,
description TEXT, homepage TEXT, docs TEXT, tags TEXT NOT NULL DEFAULT '[]',
enabled_claude BOOLEAN NOT NULL DEFAULT 0, enabled_codex BOOLEAN NOT NULL DEFAULT 0,
enabled_gemini BOOLEAN NOT NULL DEFAULT 0, enabled_opencode BOOLEAN NOT NULL DEFAULT 0,
enabled_hermes BOOLEAN NOT NULL DEFAULT 0
enabled_gemini BOOLEAN NOT NULL DEFAULT 0, enabled_opencode BOOLEAN NOT NULL DEFAULT 0
)",
[],
)
@@ -94,7 +93,6 @@ impl Database {
enabled_codex BOOLEAN NOT NULL DEFAULT 0,
enabled_gemini BOOLEAN NOT NULL DEFAULT 0,
enabled_opencode BOOLEAN NOT NULL DEFAULT 0,
enabled_hermes BOOLEAN NOT NULL DEFAULT 0,
installed_at INTEGER NOT NULL DEFAULT 0,
content_hash TEXT,
updated_at INTEGER NOT NULL DEFAULT 0
@@ -214,7 +212,6 @@ impl Database {
[],
)
.map_err(|e| AppError::Database(e.to_string()))?;
Self::create_request_logs_usage_indexes_if_supported(conn)?;
// 11. Model Pricing 表
conn.execute(
@@ -421,16 +418,6 @@ impl Database {
Self::migrate_v7_to_v8(conn)?;
Self::set_user_version(conn, 8)?;
}
8 => {
log::info!("迁移数据库从 v8 到 v9(全面补充模型定价)");
Self::migrate_v8_to_v9(conn)?;
Self::set_user_version(conn, 9)?;
}
9 => {
log::info!("迁移数据库从 v9 到 v10(添加 Hermes Agent 支持)");
Self::migrate_v9_to_v10(conn)?;
Self::set_user_version(conn, 10)?;
}
_ => {
return Err(AppError::Database(format!(
"未知的数据库版本 {version},无法迁移到 {SCHEMA_VERSION}"
@@ -1108,7 +1095,6 @@ impl Database {
"data_source",
"TEXT NOT NULL DEFAULT 'proxy'",
)?;
Self::create_request_logs_usage_indexes_if_supported(conn)?;
}
// 2. 创建会话日志同步状态表
@@ -1158,62 +1144,11 @@ impl Database {
Ok(())
}
/// v8 → v9: 全面补充模型定价(清空 + 重新 seed)
fn migrate_v8_to_v9(conn: &Connection) -> Result<(), AppError> {
conn.execute(
"CREATE TABLE IF NOT EXISTS model_pricing (
model_id TEXT PRIMARY KEY, display_name TEXT NOT NULL,
input_cost_per_million TEXT NOT NULL, output_cost_per_million TEXT NOT NULL,
cache_read_cost_per_million TEXT NOT NULL DEFAULT '0',
cache_creation_cost_per_million TEXT NOT NULL DEFAULT '0'
)",
[],
)
.map_err(|e| AppError::Database(format!("创建 model_pricing 表失败: {e}")))?;
conn.execute("DELETE FROM model_pricing", [])
.map_err(|e| AppError::Database(format!("清空模型定价失败: {e}")))?;
Self::seed_model_pricing(conn)?;
log::info!("v8 -> v9 迁移完成:已刷新全部模型定价数据");
Ok(())
}
/// v9 -> v10 迁移:添加 Hermes Agent 支持
fn migrate_v9_to_v10(conn: &Connection) -> Result<(), AppError> {
Self::add_column_if_missing(
conn,
"mcp_servers",
"enabled_hermes",
"BOOLEAN NOT NULL DEFAULT 0",
)?;
// skills table may not exist in databases migrated from very old versions
if Self::table_exists(conn, "skills")? {
Self::add_column_if_missing(
conn,
"skills",
"enabled_hermes",
"BOOLEAN NOT NULL DEFAULT 0",
)?;
}
log::info!("v9 -> v10 迁移完成:已添加 Hermes Agent 支持");
Ok(())
}
/// 插入默认模型定价数据
/// 格式: (model_id, display_name, input, output, cache_read, cache_creation)
/// 注意: model_id 使用短横线格式(如 claude-haiku-4-5),与 API 返回的模型名称标准化后一致
fn seed_model_pricing(conn: &Connection) -> Result<(), AppError> {
let pricing_data = [
// Claude 4.7 系列
(
"claude-opus-4-7",
"Claude Opus 4.7",
"5",
"25",
"0.50",
"6.25",
),
// Claude 4.6 系列
(
"claude-opus-4-6-20260206",
@@ -1298,13 +1233,6 @@ impl Database {
"0.30",
"3.75",
),
// GPT-5.5 系列
("gpt-5.5", "GPT-5.5", "5", "30", "0.50", "0"),
("gpt-5.5-low", "GPT-5.5", "5", "30", "0.50", "0"),
("gpt-5.5-medium", "GPT-5.5", "5", "30", "0.50", "0"),
("gpt-5.5-high", "GPT-5.5", "5", "30", "0.50", "0"),
("gpt-5.5-xhigh", "GPT-5.5", "5", "30", "0.50", "0"),
("gpt-5.5-minimal", "GPT-5.5", "5", "30", "0.50", "0"),
// GPT-5.4 系列
("gpt-5.4", "GPT-5.4", "2.50", "15", "0.25", "0"),
("gpt-5.4-mini", "GPT-5.4 Mini", "0.75", "4.50", "0.075", "0"),
@@ -1563,38 +1491,6 @@ impl Database {
"0.02",
"0",
),
(
"doubao-seed-2-0-pro",
"Doubao Seed 2.0 Pro",
"0.47",
"2.37",
"0",
"0",
),
(
"doubao-seed-2-0-code",
"Doubao Seed 2.0 Code",
"0.47",
"2.37",
"0",
"0",
),
(
"doubao-seed-2-0-lite",
"Doubao Seed 2.0 Lite",
"0.25",
"2",
"0",
"0",
),
(
"doubao-seed-2-0-mini",
"Doubao Seed 2.0 Mini",
"0.03",
"0.31",
"0",
"0",
),
// DeepSeek 系列
(
"deepseek-v3.2",
@@ -1616,36 +1512,19 @@ impl Database {
(
"deepseek-chat",
"DeepSeek Chat",
"0.27",
"1.10",
"0.07",
"0.28",
"0.42",
"0.028",
"0",
),
(
"deepseek-reasoner",
"DeepSeek Reasoner",
"0.55",
"2.19",
"0.14",
"0",
),
// DeepSeek V4 系列(官方 CNY 按 1 USD ≈ 7.14 折算)
(
"deepseek-v4-flash",
"DeepSeek V4 Flash",
"0.14",
"0.28",
"0.42",
"0.028",
"0",
),
(
"deepseek-v4-pro",
"DeepSeek V4 Pro",
"1.68",
"3.36",
"0.14",
"0",
),
// Kimi (月之暗面)
(
"kimi-k2-thinking",
@@ -1664,8 +1543,7 @@ impl Database {
"0.14",
"0",
),
("kimi-k2.5", "Kimi K2.5", "0.60", "2.50", "0.10", "0"),
("kimi-k2.6", "Kimi K2.6", "0.95", "4.00", "0.16", "0"),
("kimi-k2.5", "Kimi K2.5", "0.60", "3.00", "0.10", "0"),
// MiniMax 系列
("minimax-m2.1", "MiniMax M2.1", "0.27", "0.95", "0.03", "0"),
(
@@ -1677,211 +1555,35 @@ impl Database {
"0",
),
("minimax-m2", "MiniMax M2", "0.27", "0.95", "0.03", "0"),
("minimax-m2.5", "MiniMax M2.5", "0.12", "0.95", "0.03", "0"),
(
"minimax-m2.5-lightning",
"MiniMax M2.5 Lightning",
"0.30",
"2.40",
"0.03",
"0",
),
(
"minimax-m2.7",
"MiniMax M2.7",
"0.30",
"1.20",
"0.06",
"0.375",
),
(
"minimax-m2.7-highspeed",
"MiniMax M2.7 Highspeed",
"0.60",
"2.40",
"0.06",
"0.375",
),
// GLM (智谱)
("glm-4.7", "GLM-4.7", "0.39", "1.75", "0.04", "0"),
("glm-4.6", "GLM-4.6", "0.28", "1.11", "0.03", "0"),
("glm-5", "GLM-5", "0.72", "2.30", "0", "0"),
("glm-5.1", "GLM-5.1", "0.95", "3.15", "0", "0"),
// MiMo (小米)
// Mimo (小米)
(
"mimo-v2-flash",
"MiMo V2 Flash",
"Mimo V2 Flash",
"0.09",
"0.29",
"0.009",
"0",
),
("mimo-v2-pro", "MiMo V2 Pro", "1", "3", "0", "0"),
// Qwen 系列 (阿里巴巴)
("qwen3.6-plus", "Qwen3.6 Plus", "0.325", "1.95", "0", "0"),
("qwen3.5-plus", "Qwen3.5 Plus", "0.26", "1.56", "0", "0"),
("qwen3-max", "Qwen3 Max", "0.78", "3.90", "0", "0"),
(
"qwen3-235b-a22b",
"Qwen3 235B-A22B",
"0.70",
"8.40",
"0",
"0",
),
(
"qwen3-coder-plus",
"Qwen3 Coder Plus",
"0.65",
"3.25",
"0",
"0",
),
(
"qwen3-coder-flash",
"Qwen3 Coder Flash",
"0.195",
"0.975",
"0",
"0",
),
(
"qwen3-coder-next",
"Qwen3 Coder Next",
"0.12",
"0.75",
"0",
"0",
),
("qwq-plus", "QwQ Plus", "0.80", "2.40", "0", "0"),
("qwq-32b", "QwQ 32B", "0.20", "0.60", "0", "0"),
("qwen3-32b", "Qwen3 32B", "0.16", "0.64", "0", "0"),
// Grok 系列 (xAI)
(
"grok-4.20-0309-reasoning",
"Grok 4.20 Reasoning",
"2",
"6",
"0.20",
"0",
),
(
"grok-4.20-0309-non-reasoning",
"Grok 4.20",
"2",
"6",
"0.20",
"0",
),
(
"grok-4-1-fast-reasoning",
"Grok 4.1 Fast Reasoning",
"0.20",
"0.50",
"0.05",
"0",
),
(
"grok-4-1-fast-non-reasoning",
"Grok 4.1 Fast",
"0.20",
"0.50",
"0.05",
"0",
),
("grok-4", "Grok 4", "3", "15", "0.75", "0"),
(
"grok-code-fast-1",
"Grok Code Fast",
"0.20",
"1.50",
"0.02",
"0",
),
("grok-3", "Grok 3", "3", "15", "0.75", "0"),
("grok-3-mini", "Grok 3 Mini", "0.25", "0.50", "0.075", "0"),
// Mistral 系列
("codestral-2508", "Codestral", "0.30", "0.90", "0.03", "0"),
(
"devstral-small-1.1",
"Devstral Small 1.1",
"0.07",
"0.28",
"0.01",
"0",
),
("devstral-2-2512", "Devstral 2", "0.40", "0.90", "0.04", "0"),
(
"devstral-medium",
"Devstral Medium",
"0.40",
"2",
"0.04",
"0",
),
(
"mistral-large-3-2512",
"Mistral Large 3",
"0.50",
"1.50",
"0.05",
"0",
),
(
"mistral-medium-3.1",
"Mistral Medium 3.1",
"0.40",
"2",
"0.04",
"0",
),
(
"mistral-small-3.2-24b",
"Mistral Small 3.2",
"0.075",
"0.20",
"0.01",
"0",
),
("magistral-medium", "Magistral Medium", "2", "5", "0", "0"),
// Cohere 系列
("command-a", "Cohere Command A", "2.50", "10", "0", "0"),
(
"command-r-plus",
"Cohere Command R+",
"2.50",
"10",
"0",
"0",
),
("command-r", "Cohere Command R", "0.15", "0.60", "0", "0"),
// OpenAI 补充
("o3-pro", "OpenAI o3-pro", "20", "80", "0", "0"),
("o3-mini", "OpenAI o3-mini", "0.55", "2.20", "0.55", "0"),
("o1", "OpenAI o1", "15", "60", "7.50", "0"),
("o1-mini", "OpenAI o1-mini", "0.55", "2.20", "0.55", "0"),
("codex-mini", "Codex Mini", "0.75", "3", "0.025", "0"),
("gpt-5-mini", "GPT-5 Mini", "0.25", "2", "0.025", "0"),
("gpt-5-nano", "GPT-5 Nano", "0.05", "0.40", "0.005", "0"),
];
let mut stmt = conn
.prepare(
for (model_id, display_name, input, output, cache_read, cache_creation) in pricing_data {
conn.execute(
"INSERT OR IGNORE INTO model_pricing (
model_id, display_name, input_cost_per_million, output_cost_per_million,
cache_read_cost_per_million, cache_creation_cost_per_million
) VALUES (?1, ?2, ?3, ?4, ?5, ?6)",
rusqlite::params![
model_id,
display_name,
input,
output,
cache_read,
cache_creation
],
)
.map_err(|e| AppError::Database(format!("准备模型定价语句失败: {e}")))?;
for (model_id, display_name, input, output, cache_read, cache_creation) in pricing_data {
stmt.execute(rusqlite::params![
model_id,
display_name,
input,
output,
cache_read,
cache_creation
])
.map_err(|e| AppError::Database(format!("插入模型定价失败: {e}")))?;
}
@@ -1917,54 +1619,6 @@ impl Database {
Ok(())
}
fn create_request_logs_usage_indexes_if_supported(conn: &Connection) -> Result<(), AppError> {
if !Self::table_exists(conn, "proxy_request_logs")? {
return Ok(());
}
let has_app_type = Self::has_column(conn, "proxy_request_logs", "app_type")?;
let has_created_at = Self::has_column(conn, "proxy_request_logs", "created_at")?;
if has_app_type && has_created_at {
conn.execute(
"CREATE INDEX IF NOT EXISTS idx_request_logs_app_created_at
ON proxy_request_logs(app_type, created_at DESC)",
[],
)
.map_err(|e| AppError::Database(format!("创建使用量应用时间索引失败: {e}")))?;
}
let required_columns = [
"app_type",
"data_source",
"input_tokens",
"output_tokens",
"cache_read_tokens",
"created_at",
"cache_creation_tokens",
];
for column in required_columns {
if !Self::has_column(conn, "proxy_request_logs", column)? {
return Ok(());
}
}
conn.execute("DROP INDEX IF EXISTS idx_request_logs_dedup_lookup", [])
.map_err(|e| AppError::Database(format!("删除旧使用量去重索引失败: {e}")))?;
// 查询层为了兼容历史 NULL data_source 行,会使用
// COALESCE(data_source, 'proxy')。普通 data_source 索引无法匹配该表达式,
// 会让跨源去重子查询退化成大量扫描;表达式索引让 SQLite 能按同一表达式查找。
conn.execute(
"CREATE INDEX IF NOT EXISTS idx_request_logs_dedup_lookup_expr
ON proxy_request_logs(app_type, COALESCE(data_source, 'proxy'), input_tokens,
output_tokens, cache_read_tokens, created_at,
cache_creation_tokens)",
[],
)
.map_err(|e| AppError::Database(format!("创建使用量去重表达式索引失败: {e}")))?;
Ok(())
}
fn validate_identifier(s: &str, kind: &str) -> Result<(), AppError> {
if s.is_empty() {
return Err(AppError::Database(format!("{kind} 不能为空")));
-2
View File
@@ -167,7 +167,6 @@ pub(crate) fn parse_mcp_apps(apps_str: &str) -> Result<McpApps, AppError> {
codex: false,
gemini: false,
opencode: false,
hermes: false,
};
for app in apps_str.split(',') {
@@ -180,7 +179,6 @@ pub(crate) fn parse_mcp_apps(apps_str: &str) -> Result<McpApps, AppError> {
// OpenClaw doesn't support MCP, ignore silently
log::debug!("OpenClaw doesn't support MCP, ignoring in apps parameter");
}
"hermes" => apps.hermes = true,
other => {
return Err(AppError::InvalidInput(format!(
"Invalid app in 'apps': {other}"
+1 -1
View File
@@ -31,7 +31,7 @@ pub use skill::import_skill_from_deeplink;
///
/// Represents a parsed ccswitch:// URL ready for processing.
/// This struct contains all possible fields for all resource types.
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct DeepLinkImportRequest {
/// Protocol version (e.g., "v1")
+6 -6
View File
@@ -81,10 +81,10 @@ fn parse_provider_deeplink(
// Validate app type
if !matches!(
app.as_str(),
"claude" | "codex" | "gemini" | "opencode" | "openclaw" | "hermes"
"claude" | "codex" | "gemini" | "opencode" | "openclaw"
) {
return Err(AppError::InvalidInput(format!(
"Invalid app type: must be 'claude', 'codex', 'gemini', 'opencode', 'openclaw', or 'hermes', got '{app}'"
"Invalid app type: must be 'claude', 'codex', 'gemini', 'opencode', or 'openclaw', got '{app}'"
)));
}
@@ -190,10 +190,10 @@ fn parse_prompt_deeplink(
// Validate app type
if !matches!(
app.as_str(),
"claude" | "codex" | "gemini" | "opencode" | "openclaw" | "hermes"
"claude" | "codex" | "gemini" | "opencode" | "openclaw"
) {
return Err(AppError::InvalidInput(format!(
"Invalid app type: must be 'claude', 'codex', 'gemini', 'opencode', 'openclaw', or 'hermes', got '{app}'"
"Invalid app type: must be 'claude', 'codex', 'gemini', 'opencode', or 'openclaw', got '{app}'"
)));
}
@@ -262,10 +262,10 @@ fn parse_mcp_deeplink(
let trimmed = app.trim();
if !matches!(
trimmed,
"claude" | "codex" | "gemini" | "opencode" | "openclaw" | "hermes"
"claude" | "codex" | "gemini" | "opencode" | "openclaw"
) {
return Err(AppError::InvalidInput(format!(
"Invalid app in 'apps': must be 'claude', 'codex', 'gemini', 'opencode', 'openclaw', or 'hermes', got '{trimmed}'"
"Invalid app in 'apps': must be 'claude', 'codex', 'gemini', 'opencode', or 'openclaw', got '{trimmed}'"
)));
}
}
+10 -140
View File
@@ -5,7 +5,7 @@
use super::utils::{decode_base64_param, infer_homepage_from_endpoint};
use super::DeepLinkImportRequest;
use crate::error::AppError;
use crate::provider::{ClaudeDesktopMode, Provider, ProviderMeta, UsageScript};
use crate::provider::{Provider, ProviderMeta, UsageScript};
use crate::services::ProviderService;
use crate::store::AppState;
use crate::AppType;
@@ -142,20 +142,15 @@ pub(crate) fn build_provider_from_request(
request: &DeepLinkImportRequest,
) -> Result<Provider, AppError> {
let settings_config = match app_type {
AppType::Claude | AppType::ClaudeDesktop => build_claude_settings(request),
AppType::Claude => build_claude_settings(request),
AppType::Codex => build_codex_settings(request),
AppType::Gemini => build_gemini_settings(request),
AppType::OpenCode => build_opencode_settings(request),
AppType::OpenClaw => build_additive_app_settings(request),
AppType::Hermes => build_hermes_settings(request),
AppType::OpenClaw => build_openclaw_settings(request),
};
// Build usage script configuration if provided
let mut meta = build_provider_meta(request)?;
if matches!(app_type, AppType::ClaudeDesktop) {
meta.get_or_insert_with(ProviderMeta::default)
.claude_desktop_mode = Some(ClaudeDesktopMode::Direct);
}
let meta = build_provider_meta(request)?;
let provider = Provider {
id: String::new(), // Will be generated by caller
@@ -398,11 +393,11 @@ fn build_opencode_settings(request: &DeepLinkImportRequest) -> serde_json::Value
})
}
/// Build settings for OpenClaw (camelCase live config).
/// Format: { baseUrl, apiKey, api, models }
fn build_additive_app_settings(request: &DeepLinkImportRequest) -> serde_json::Value {
fn build_openclaw_settings(request: &DeepLinkImportRequest) -> serde_json::Value {
let endpoint = get_primary_endpoint(request);
// Build OpenClaw provider config
// Format: { baseUrl, apiKey, api, models }
let mut config = serde_json::Map::new();
if !endpoint.is_empty() {
@@ -413,49 +408,10 @@ fn build_additive_app_settings(request: &DeepLinkImportRequest) -> serde_json::V
config.insert("apiKey".to_string(), json!(api_key));
}
// Default to OpenAI-compatible API
config.insert("api".to_string(), json!("openai-completions"));
if let Some(model) = &request.model {
config.insert(
"models".to_string(),
json!([{ "id": model, "name": model }]),
);
}
json!(config)
}
/// Build Hermes provider settings (snake_case YAML-native fields).
///
/// Hermes' `custom_providers:` entries use `base_url` / `api_key` / `api_mode`
/// (see `_VALID_CUSTOM_PROVIDER_FIELDS` in upstream `hermes_cli/config.py`).
/// Emitting camelCase here — as the OpenClaw path does — would poison the
/// YAML with unknown root fields the Hermes runtime ignores.
///
/// `api_mode` is always written explicitly. Deeplinks have no field to carry
/// it, so we default to `chat_completions` (the most widely compatible
/// protocol) and let the user adjust via the UI after import. We never rely
/// on Hermes' built-in URL heuristics, which only recognize a handful of
/// official endpoints.
fn build_hermes_settings(request: &DeepLinkImportRequest) -> serde_json::Value {
let endpoint = get_primary_endpoint(request);
let mut config = serde_json::Map::new();
if let Some(name) = request.name.as_deref().filter(|s| !s.is_empty()) {
config.insert("name".to_string(), json!(name));
}
if !endpoint.is_empty() {
config.insert("base_url".to_string(), json!(endpoint));
}
if let Some(api_key) = &request.api_key {
config.insert("api_key".to_string(), json!(api_key));
}
config.insert("api_mode".to_string(), json!("chat_completions"));
// Build models array
if let Some(model) = &request.model {
config.insert(
"models".to_string(),
@@ -528,7 +484,7 @@ pub fn parse_and_merge_config(
"codex" => merge_codex_config(&mut merged, &config_value)?,
"gemini" => merge_gemini_config(&mut merged, &config_value)?,
// Additive mode apps use JSON config directly; pass through as-is
"openclaw" | "opencode" | "hermes" => {
"openclaw" | "opencode" => {
merge_additive_config(&mut merged, &config_value)?;
}
"" => {
@@ -755,89 +711,3 @@ fn extract_codex_base_url(toml_value: &toml::Value) -> Option<String> {
}
None
}
#[cfg(test)]
mod tests {
use super::*;
fn hermes_request() -> DeepLinkImportRequest {
DeepLinkImportRequest {
resource: "provider".to_string(),
app: Some("hermes".to_string()),
name: Some("MyHermes".to_string()),
endpoint: Some("https://api.example.com/v1".to_string()),
api_key: Some("sk-test".to_string()),
model: Some("anthropic/claude-opus-4-7".to_string()),
..Default::default()
}
}
#[test]
fn build_hermes_settings_emits_snake_case() {
let settings = build_hermes_settings(&hermes_request());
let obj = settings.as_object().expect("settings must be object");
assert_eq!(obj.get("name").unwrap(), "MyHermes");
assert_eq!(obj.get("base_url").unwrap(), "https://api.example.com/v1");
assert_eq!(obj.get("api_key").unwrap(), "sk-test");
// camelCase and legacy fields must NOT be present
assert!(obj.get("baseUrl").is_none(), "no camelCase baseUrl");
assert!(obj.get("apiKey").is_none(), "no camelCase apiKey");
assert!(obj.get("api").is_none(), "no legacy 'api' field");
// models array with the deeplink model id
let models = obj.get("models").unwrap().as_array().unwrap();
assert_eq!(models.len(), 1);
assert_eq!(models[0]["id"], "anthropic/claude-opus-4-7");
}
#[test]
fn build_hermes_settings_writes_default_api_mode() {
let settings = build_hermes_settings(&hermes_request());
assert_eq!(
settings.as_object().unwrap().get("api_mode").unwrap(),
"chat_completions",
"api_mode must be written explicitly so Hermes never falls back to URL auto-detection"
);
}
#[test]
fn build_hermes_settings_skips_missing_optional_fields() {
let request = DeepLinkImportRequest {
resource: "provider".to_string(),
app: Some("hermes".to_string()),
name: Some("Minimal".to_string()),
endpoint: None,
api_key: None,
model: None,
..Default::default()
};
let settings = build_hermes_settings(&request);
let obj = settings.as_object().unwrap();
assert_eq!(obj.get("name").unwrap(), "Minimal");
assert!(obj.get("base_url").is_none());
assert!(obj.get("api_key").is_none());
assert!(obj.get("models").is_none());
assert_eq!(obj.get("api_mode").unwrap(), "chat_completions");
}
#[test]
fn openclaw_still_uses_camel_case() {
// OpenClaw's live config natively uses camelCase; guard against a
// refactor accidentally flipping it to snake_case.
let request = DeepLinkImportRequest {
resource: "provider".to_string(),
app: Some("openclaw".to_string()),
name: Some("c".to_string()),
endpoint: Some("https://api.example.com".to_string()),
api_key: Some("k".to_string()),
..Default::default()
};
let settings = build_additive_app_settings(&request);
let obj = settings.as_object().unwrap();
assert!(obj.contains_key("baseUrl"));
assert!(obj.contains_key("apiKey"));
}
}
-2
View File
@@ -44,8 +44,6 @@ pub enum AppError {
McpValidation(String),
#[error("{0}")]
Message(String),
#[error("HTTP {status}: {body}")]
HttpStatus { status: u16, body: String },
#[error("{zh} ({en})")]
Localized {
key: &'static str,
File diff suppressed because it is too large Load Diff

Some files were not shown because too many files have changed in this diff Show More