Compare commits

..

40 Commits

Author SHA1 Message Date
dependabot[bot] 655707bc09 chore(deps): bump actions/cache from 5 to 6
Bumps [actions/cache](https://github.com/actions/cache) from 5 to 6.
- [Release notes](https://github.com/actions/cache/releases)
- [Changelog](https://github.com/actions/cache/blob/main/RELEASES.md)
- [Commits](https://github.com/actions/cache/compare/v5...v6)

---
updated-dependencies:
- dependency-name: actions/cache
  dependency-version: '6'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-07-01 18:26:54 +00:00
Jason 8d1b3306d0 chore(release): v3.16.5 2026-07-01 23:55:55 +08:00
Jason 6d84082d27 docs(release-notes): add v3.16.5 notes (zh/en/ja) 2026-07-01 23:55:55 +08:00
Jason 76b8620ffe fix(tests): align Sonnet tier expectations with claude-sonnet-5
The default Sonnet tier was bumped to claude-sonnet-5, but the TheRouter preset tests and ClaudeDesktopProviderForm tests still asserted claude-sonnet-4.6/4-6. Update the expected values to match the presets; input fixtures are left as-is to keep exercising legacy-route migration.
2026-07-01 23:55:55 +08:00
Jason 9079935d72 feat(presets): add NekoCode partner preset for 6 apps
Partner aggregator (New API gateway) across Claude Code, Claude Desktop,
Codex, OpenCode, OpenClaw, and Hermes -- no Gemini, since its /v1beta
surface falls through to the web app. Claude Code and Claude Desktop use
the bare host https://nekocode.ai; Codex, OpenCode, OpenClaw, and Hermes
use /v1 with gpt-5.5.

Marks isPartner with the ?aff=CCSWITCH referral on apiKeyUrl only (the
website URL and every API endpoint stay bare, so the attribution param
never rides on a real request). Adds the partnerPromotion blurb in all
four locales and a sponsor row appended to the end of all four READMEs,
surfacing the CC Switch offer: 10% off top-ups with promo code cc-switch.
Registers the logo-derived nekocode-icon.png via URL import; the README
banner is normalized to the standard 1920x798 aspect ratio.
2026-07-01 22:27:10 +08:00
Jason a8657d229a feat(presets): add Code0.ai partner preset for 7 apps
Partner aggregator (New API gateway) across Claude Code, Claude Desktop,
Codex, Gemini, OpenCode, OpenClaw, and Hermes. Claude Code, Claude
Desktop, and Gemini use the bare host (gemini-3.1-pro-preview for
Gemini); Codex, OpenCode, OpenClaw, and Hermes use /v1 with gpt-5.5.

Marks isPartner with ?source=ccswitch attribution on apiKeyUrl only
(website URL stays clean). Adds the partnerPromotion blurb in all four
locales and a sponsor row in all four READMEs. Positioned after
ClaudeAPI in the Claude Code and Claude Desktop lists (the two apps
that carry a ClaudeAPI preset). Registers the favicon-derived code0.png
icon via URL import; the README banner is normalized to the standard
1920x798 aspect ratio.
2026-07-01 22:27:10 +08:00
Jason cd9e025b5a feat(presets): switch Claude Sonnet tier default to Sonnet 5
Bump every claude-sonnet-4-6/claude-sonnet-4.6 default pin to claude-sonnet-5 across all provider presets (claude, claudeDesktop, hermes, openclaw, opencode, universal NEWAPI_DEFAULT_MODELS) and the Claude Desktop DEFAULT_PROXY_ROUTES sonnet route id. Update route-derivation test expectations accordingly. Non-Anthropic pins (gpt/gemini/glm/sonnet-4-5) left untouched.
2026-07-01 22:27:10 +08:00
Jason 778d5b9268 feat(pricing): add Claude Sonnet 5 model pricing
Seed claude-sonnet-5 at Anthropic list price ($3/$15 in/out, $0.30/$3.75 cache read/write per Mtok, matching Sonnet 4.6). The introductory $2/$10 promo through 2026-08-31 is intentionally not seeded. Applied on next app start via ensure_model_pricing_seeded; no SCHEMA_VERSION bump.
2026-07-01 22:27:10 +08:00
Jason 97512cabbd docs: document CC_SWITCH_GDK_BACKEND Linux Wayland escape hatch
Document the opt-in CC_SWITCH_GDK_BACKEND environment variable (added in
#4351) that overrides the AppImage's hardcoded GDK_BACKEND=x11, letting
Wayland+NVIDIA users switch back to native Wayland when the webview goes
click-dead and black-screens on resize. The override is generic, so
tiling-Wayland users can set it to x11 for the inverse input bug.

- CHANGELOG.md: new Unreleased/Fixed entry (#4351, fixes #4350)
- README.md / README_ZH / README_JA / README_DE: FAQ entry across all locales
- docs/user-manual/{zh,en,ja}/5-faq/5.2-questions.md: Linux troubleshooting entry
2026-07-01 22:27:10 +08:00
Jason 332a3c16a8 feat(presets): add Amux provider preset for 6 apps
Non-partner aggregator across Claude Code, Claude Desktop, Codex,
OpenCode, OpenClaw, and Hermes. Claude Code uses bare host
api.amux.ai; Codex uses /v1 native Responses; the other three apps
default to gpt-5.5. No Gemini preset. Registers the amux inline icon
(currentColor) in the icon index and metadata.
2026-07-01 22:27:10 +08:00
SaladDay 586fb46b10 fix(usage): keep date-range picker calendar on-screen in narrow popovers (#4860)
The custom date-range picker switched to its two-column layout (date
fields | calendar) based on the viewport width via Tailwind `sm:`
(640px). But the popover is clamped to `100vw - 2rem` and anchored to
its trigger with `align="end"`, so its actual available width is not the
viewport width. On narrow windows the wide two-column layout could
activate while the popover only had room for one column, pushing the
calendar column off the right edge of the window where it was clipped
and unusable (month header and 4 of 7 weekday columns cut off).

Gate the two-column layout on the popover's own inline size via a CSS
container query instead of the viewport, and let the popover width
shrink to fit the viewport. When the popover is genuinely wide it shows
two columns; when it is clamped narrow it stacks vertically so the
calendar stays fully visible. The base stacked layout is the safe
fallback for engines without container-query support.

Closes #4669
2026-07-01 16:33:07 +08:00
Nothing 251b13e5d2 feat(sessions): 新增会话分类视图与分组管理 (#4776)
* feat(sessions): 新增会话分类查看方式

* test(sessions): 补充会话分类视图覆盖

* feat(sessions): 适配分类视图批量管理

* test(sessions): 补充分类批量选择覆盖

* feat(sessions): 新增分类视图收起状态控制

* test(sessions): 补充分类收起状态覆盖
2026-07-01 12:23:46 +08:00
Arc f21d6f7a42 fix(i18n): rename "Write Common Config" checkbox to "Apply Common Config" (#4829)
* fix(i18n): rename "Write Common Config" checkbox to "Apply Common Config"

The original label "Write Common Config" (写入通用配置) was ambiguous
about data flow direction — it read as "write current config INTO the
common config" when the actual behavior is the reverse: merge the
saved common config snippet INTO this provider's config.

Rename to "Apply Common Config" across zh/zh-TW/en/ja, including all
hint/guide/notice references and component defaultValue fallbacks, to
make the action unambiguous. Also aligns the Gemini hint wording with
the claude/codex "when checked" phrasing.

* docs(ja): sync Japanese docs with renamed 'Apply Common Config' label

Codex review on #4829 pointed out the Japanese user manual and README
still referenced the old checkbox label after the rename.

- docs/user-manual/ja/2-providers/2.1-add.md: 共通設定を書き込み → 共通設定を適用
- README_JA.md: 共有設定を書き込む → 共有設定を適用
2026-07-01 11:58:31 +08:00
ayxwi ab3e628bad Fix scroll bounds for long select dropdowns (#4798)
Co-authored-by: Xvvln <180168103+Xvvln@users.noreply.github.com>
2026-07-01 11:57:17 +08:00
迷渡 8f484c54ce fix(detect): dedupe Windows Codex npm shims (#4782) 2026-07-01 11:33:40 +08:00
Bone 5546815441 fix(linux): allow overriding AppImage's forced GDK_BACKEND=x11 via CC_SWITCH_GDK_BACKEND (#4351)
The AppImage GTK hook (linuxdeploy-plugin-gtk.sh) hardcodes
`export GDK_BACKEND=x11`, forcing XWayland to avoid a historical Wayland crash
(tauri-apps/tauri#8541). On modern Wayland + NVIDIA setups this forced XWayland
instead makes the WebKitGTK webview unable to receive pointer events (GTK
titlebar clickable, web content completely dead) and black-screens on resize.

Read an opt-in CC_SWITCH_GDK_BACKEND var before GTK init (the hook does not
touch it) so affected users can switch back to native Wayland without unpacking
the AppImage. Default behavior is unchanged when the var is unset.

Refs #4350

Co-authored-by: BoneLiu <bone_liu@sina.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-30 23:26:51 +08:00
Jason 01bf003783 feat(presets): add TeamoRouter provider preset for 6 apps
Add TeamoRouter (enterprise Agentic LLM gateway) as a sponsor partner
across Claude Code, Claude Desktop, Codex, OpenCode, OpenClaw and Hermes.

- Claude Code uses bare host https://api.teamorouter.com; Codex uses
  https://api.teamorouter.com/v1 (native Responses); OpenCode/OpenClaw/
  Hermes use gpt-5.5 over the OpenAI-compatible /v1 endpoint. No Gemini.
- apiKeyUrl uses the English referral link with UTM params (in-app stays
  English; README links are per-language).
- Add localized in-app promo blurb (zh/en/ja/zh-TW).
- Add README sponsor rows below PatewayAI in all four READMEs.
- Register the teamorouter provider icon and pad the README banner to the
  standard 2.40 aspect ratio.
2026-06-30 23:01:49 +08:00
Jason e1e3dadf89 chore(icons): remove unused dds.svg orphan
dds.svg (1.4MB genuine vector, 2222 paths) was not imported in index.ts
nor referenced anywhere in src/, so it never shipped in the app or
appeared in the icon system — it only bloated the repo. Remove it.
2026-06-30 17:02:19 +08:00
Jason ba87e8f69d perf(icons): downscale hermes, claudecn and atlascloud icons to 256px
All three shipped well above their ~32px render size (hermes/claudecn at
512x512, atlascloud at 3525x3300). Downscale to a 256px max dimension:
hermes 125KB->38KB, claudecn 109KB->46KB, atlascloud 105KB->9KB. Same
filenames, no code change.
2026-06-30 17:02:19 +08:00
Jason 37f2e248dc perf(icons): shrink ccsub and shengsuanyun embedded-image SVGs
Both were "fake vector" SVGs wrapping a single oversized base64 PNG
(ccsub embedded 1446x1328 / 1.16MB, shengsuanyun 720x720 / 212KB),
rendered at ~32px. Downscale the embedded bitmap to a 256px max dimension
and re-embed, keeping the SVG wrapper, filename, and import unchanged
(no code change). Result: ccsub 1.16MB -> 60KB, shengsuanyun 212KB -> 51KB.
2026-06-30 17:00:53 +08:00
Jason 6f8e12a1ac perf(icons): downscale relaxcode and sudocode icons to 256px
Both shipped far larger than their on-screen render size: relaxcode was
1462x1076 / 1.16MB and sudocode 2048x2048 / 432KB. Downscale to a 256px
max dimension (aspect ratio preserved; ProviderIcon renders via <img>
object-contain), cutting them to ~41KB and ~36KB. Same filenames.
2026-06-30 16:50:38 +08:00
Jason 8fb6ebb6bd perf(icons): downscale ZetaAPI provider icon to 256px
The ZetaAPI icon shipped as a 1254x1254 / 940KB PNG, roughly 30x larger
than its on-screen render size (~32px in ProviderIcon). Downscale to
256x256, cutting it to ~40KB (-96%) so it no longer bloats the bundle.
Same filename, so no import/code change.
2026-06-30 16:49:35 +08:00
Jason e54a342301 feat(presets): add ZetaAPI provider preset for 6 apps
Add ZetaAPI (api.zetaapi.ai) as a partner aggregator preset across Claude
Code, Claude Desktop, Codex, OpenCode, OpenClaw, and Hermes (Gemini not
supported). ZetaAPI relays native Claude/GPT, so Claude Code/Codex pass
through native models; the OpenAI-compatible apps default to gpt-5.5.

- Claude / Claude Desktop: Anthropic-compatible bare host (api.zetaapi.ai)
- Codex: native Responses at api.zetaapi.ai/v1, gpt-5.5
- OpenCode / OpenClaw / Hermes: gpt-5.5 over api.zetaapi.ai/v1
- Partner referral link (zetaapi.ai/go/ccs) as apiKeyUrl + short
  partner-promotion blurb in zh/en/ja/zh-TW
- Add ZetaAPI sponsor entry to all four READMEs (zetaapi-banner.png),
  first-recharge 10% off with promo code CC-SWITCH
- Register zetaapi-icon.png icon (URL import + iconUrls + metadata)
2026-06-30 16:39:32 +08:00
Jason 997bad94f0 docs(kimi): point overseas READMEs to platform.kimi.ai and add Coding Plan link 2026-06-30 09:53:02 +08:00
Jason d4feaa4e53 chore(kimi): update referral links to new platform.kimi.com domain
Moonshot rebranded its console to Kimi, so refresh all Kimi sponsor
referral links across provider presets and READMEs:

- API (domestic): platform.moonshot.cn/console -> platform.kimi.com
- API key page: platform.moonshot.cn/console/api-keys -> platform.kimi.com/console/api-keys
- Coding Plan: www.kimi.com/code/docs/ -> www.kimi.com/code/

Also add the missing ?aff=cc-switch param to the codex and openclaw
entries that previously had no referral attribution. API endpoints
(api.moonshot.cn, api.kimi.com/coding) are intentionally left unchanged.
The overseas link (platform.kimi.ai) is not wired up yet since there is
no overseas API preset.
2026-06-30 09:15:04 +08:00
Jason 16f988bd1a feat(providers): auto-sync Claude common config from live on switch
When switching away from a Claude provider that opted into common config, re-extract the shareable portion of its live settings.json and replace the stored snippet. This captures config the user added directly in the app (plugins/hooks/shared prefs) so it isn't lost on switch, and propagates deletions so a removed key isn't re-injected on the next switch.

Scoped to Claude providers with common_config_enabled, skipped when the snippet was explicitly cleared. All failures are non-fatal (warn only) and never block the switch.

Also harden extract_claude_common_config to strip ALL credential-like keys via pattern match (*_API_KEY / *_AUTH_TOKEN / secret / token / etc.), not just ANTHROPIC_API_KEY / ANTHROPIC_AUTH_TOKEN. Claude providers legitimately use OPENROUTER_API_KEY / GOOGLE_API_KEY (and may carry OpenAI/Gemini/AWS Bedrock/Vertex creds), which previously could leak into the shared snippet and be injected into other providers. This also fixes the pre-existing leak in the manual Extract and one-time auto-extract paths. Plural _TOKENS (e.g. MAX_OUTPUT_TOKENS) is preserved as shareable.

Tests: 4 integration (capture / delete-sync / opt-out / cleared) + 1 unit (credential stripping).
2026-06-29 23:30:58 +08:00
Jason 36e0378e78 feat(providers): show API key link in Claude Desktop, OpenClaw and Hermes forms
The "Get API Key" link (and partner promotion) below the API key input was
only wired for claude/codex/gemini/opencode. Claude Desktop uses a separate
form that never rendered it, and OpenClaw/Hermes were blocked by two gaps:

- useApiKeyLink whitelisted only 4 app ids, so the link was suppressed for
  claude-desktop/openclaw/hermes even when a preset carried apiKeyUrl.
- useProviderCategory only parsed claude/codex/gemini/opencode preset ids, so
  OpenClaw/Hermes category stayed undefined and the link condition
  (cn_official/aggregator/third_party) never held.

Changes:
- ClaudeDesktopProviderForm: call useApiKeyLink and replace the bare API key
  Input with the shared ApiKeySection.
- useApiKeyLink: add claude-desktop/openclaw/hermes to the whitelist and add
  ClaudeDesktopProviderPreset to the PresetEntry union.
- useProviderCategory: parse and resolve openclaw/hermes preset categories.
- Hermes/OpenClaw form fields: don't let an "official" category disable the
  API key input, since these apps have no OAuth-only official providers
  (e.g. Hermes' Nous Research is "official" but still needs a user key).
2026-06-29 23:30:58 +08:00
Jason 238cd5d7c9 feat(presets): add FennoAI provider preset for 6 apps
Add FennoAI (api.fenno.ai) as a partner aggregator preset across Claude
Code, Claude Desktop, Codex, OpenCode, OpenClaw, and Hermes (Gemini not
supported). FennoAI relays native Claude/GPT, so Claude Code/Codex pass
through native models; the OpenAI-compatible apps default to gpt-5.5.

- Claude / Claude Desktop: Anthropic-compatible bare host (api.fenno.ai)
- Codex: native Responses at api.fenno.ai, gpt-5.5
- OpenCode / OpenClaw / Hermes: gpt-5.5 over api.fenno.ai/v1
- Partner referral link as apiKeyUrl + short partner-promotion blurb in
  zh/en/ja/zh-TW
- Add FennoAI sponsor entry to all four READMEs (fenno-banner.png)
- Register fenno-icon.webp icon (URL import + iconUrls + metadata)
2026-06-29 23:30:58 +08:00
Jason 3476707321 feat(presets): add Qiniu (七牛云) provider preset for all apps
Add the Qiniu Cloud AI gateway (api.qnaigc.com primary, api.modelink.ai
overseas mirror) as a partner aggregator preset across Claude Code, Claude
Desktop, Codex, Gemini, OpenCode, OpenClaw, and Hermes.

Qiniu relays native Claude/GPT/Gemini, so Claude Code/Codex pass through
native models (no domestic-model pinning); the OpenAI-compatible apps
default to gpt-5.5.

- Claude / Claude Desktop: Anthropic-compatible bare host, native passthrough
- Codex: native Responses at /bypass/openai/v1, gpt-5.5
- Gemini: gemini-3.1-pro-preview via /bypass/vertex
- OpenCode / OpenClaw / Hermes: gpt-5.5 over OpenAI-compatible /v1
- Localized display name via nameKey + partner promotion blurb in zh/en/ja/zh-TW
- Register qiniu.png icon (URL import + iconUrls + metadata)
2026-06-29 23:30:58 +08:00
Jason 64a94ba3f7 chore(presets): update SiliconFlow referral link
Replace the SiliconFlow invite code drGuwc9k with YflgU2Ve across all
README locales and provider presets (claude, claude-desktop, codex,
hermes, openclaw).
2026-06-29 23:30:58 +08:00
Jason d916b4e12d fix(openclaw): sync Doubao context window to 262144
The OpenClaw DouBaoSeed preset hard-coded contextWindow 128000 while the
Codex preset/catalog uses 262144 for the same model, giving OpenClaw users
a too-small window that could compress or truncate long context early.
Align to 262144 and add a cross-preset consistency test asserting the
OpenClaw and Codex Doubao context windows stay equal, so neither side can
drift again.
2026-06-29 23:30:58 +08:00
Jason 7f83feb222 fix(presets): use dated Doubao model id and price 6-digit dated models
Volcengine Ark rejects the bare model name doubao-seed-2-1-pro with a 404
("model does not exist or you do not have access to it") even after the
model is activated; the API requires the full dated id
doubao-seed-2-1-pro-260628. Update the Doubao preset model id across all
apps (config default, generated catalog, and OpenClaw namespaced refs).

Pricing lookup only stripped 8-digit (YYYYMMDD) and ISO date suffixes, not
the 6-digit YYMMDD format Volcengine uses (-260628, -250615), so real
Doubao usage never matched the bare-name pricing seed and showed $0 cost.
Extend strip_model_date_suffix to also strip 6-digit YYMMDD (with
month/day validation to avoid eating non-date version suffixes), keeping
the bare-name pricing seed as the canonical identity. This also fixes
pricing for every other Volcengine Doubao model. Add unit and end-to-end
regression tests.
2026-06-29 23:30:58 +08:00
Jason 562480870d fix(presets): point Volcengine/Doubao/BytePlus website links to official sites
The websiteUrl for the 火山Agentplan, BytePlus and DouBaoSeed presets was
accidentally set to the same invite/console link as apiKeyUrl, so the
"visit official site" button took users to the referral/API-key page
instead of the product homepage. Restore clean official homepages across
all six app preset files; the apiKeyUrl referral links are kept intact.

- 火山Agentplan -> https://www.volcengine.com/product/ark
- DouBaoSeed    -> https://www.volcengine.com/product/doubao
- BytePlus      -> https://www.byteplus.com/en/product/modelark (drop utm)
2026-06-29 23:30:58 +08:00
Jason 85b38c1bed refactor(codex): decouple model mapping from local-routing toggle
Align the Codex provider form with Claude Code. Model mapping (the
catalog) is independent from route takeover: native Responses providers
(MiMo, Doubao, MiniMax) need it for direct connect with no proxy, while
Chat providers use the proxy regardless of any per-provider flag.

- Remove the "Needs Local Routing" toggle. It had no backend field and
  only gated catalog/reasoning persistence, which is equivalent to
  "is the mapping filled".
- Always show model mapping for non-official providers; persist when the
  list is non-empty (backend already keys off modelCatalog.models).
- Gate reasoning visibility/persistence on Chat format, not the toggle.
- Mark the Chat upstream-format option "routing required" and refresh the
  advanced-section hint (zh/en/ja/zh-TW); drop dead localRouting* keys.
- Move the model-mapping block above the custom User-Agent block.

fix(codex): preserve native-profile hidden catalog fields on edit

useCodexConfigState dropped supportsParallelToolCalls / inputModalities /
baseInstructions when loading a saved provider, so editing and saving a
native Responses provider lost parallel tools, image input and the
official base instructions from the generated catalog. Preserve them on
load (camelCase + snake_case) and compare them in the row sync. Add a
regression test.
2026-06-29 23:30:58 +08:00
Jason 26f0d221c0 fix(codex): reject web_search for Qwen3-Coder and correct LongCat context window
- Add `qwen3-coder` to the web_search reject model-prefix blacklist so the
  native qwen3-coder-plus direct-connect preset suppresses the built-in tool
  (百炼 rejects it for the coder series), while general Qwen models sharing the
  DashScope host keep web_search enabled. Matched on the model axis, not host.
- Correct LongCat-2.0-Preview context window from 128K to its real 1M
  (1048576), aligning with the MiMo/Qwen 2^20 convention.
- Tighten native Responses preset tests to assert exact model→contextWindow
  catalogs instead of only checking catalog presence.
2026-06-29 23:30:58 +08:00
Jason d380b410af fix(codex): disable web_search for native gateways that reject it
Some native /responses gateways whose first-party models lack OpenAI's
hosted web_search tool (MiMo, LongCat, MiniMax) reject a web_search tool
with "tool type 'web_search' is not supported by this gateway phase".
Codex sends the tool by default (config-driven, not gated by the catalog's
supports_search_tool), so cc-switch now writes the top-level
`web_search = "disabled"` line per those vendors' official Codex docs.

Scope is a blacklist (default-on): only providers matched by base_url host
or model brand prefix are disabled; relays serving real GPT, DouBao, Qwen,
and any unknown provider keep Codex's default. Matching by model brand (not
just host) also catches aggregators (e.g. SiliconFlow) fronting a reject
vendor's model. The field is injected alongside model_catalog_json at
switch time and removed via an ownership sentinel, so existing providers
need no re-save and switching back re-enables web search.
2026-06-29 23:30:58 +08:00
Jason 15e712e779 feat(codex): support native Responses direct-connect with generated model catalog
Codex providers can now run in two modes per provider:

- Proxy-Chat (route takeover): apiFormat=openai_chat, existing Responses<->Chat conversion. Unchanged.

- Native-Responses (direct): apiFormat=openai_responses, no proxy. cc-switch generates ~/.codex/cc-switch-model-catalog.json so Codex shows the custom models and tools work without the freeform apply_patch (type=custom) tool that native gateways like MiMo reject; editing falls back to shell_command.

Catalog generation is keyed on apiFormat (CodexCatalogToolProfile), decoupled from the takeover toggle, so native providers persist a catalog without enabling local route mapping.

Codex's catalog parser requires base_instructions on every entry; the native template carries a neutral default and per-vendor official text overrides it (MiMo, MiniMax). Synthesized catalogs for Qwen/Doubao/LongCat use the neutral default.

Existing native providers must be re-saved once to regenerate a valid catalog (no DB migration).
2026-06-29 23:30:58 +08:00
thisTom 61d7ac01fb fix(hermes): Windows 上 Hermes 供应商配置不生效 (#4680)
* fix(hermes): resolve config dir via HERMES_HOME and platform defaults

On Windows, CC Switch hardcoded `~/.hermes` as the Hermes config directory,
but Hermes resolves it through `get_hermes_home()`: `HERMES_HOME` env var,
then a platform default (Windows `%LOCALAPPDATA%\hermes`, mac/Linux
`~/.hermes`). So CC Switch wrote configs where Hermes never reads them and
provider changes had no effect (refs #3178).

`get_hermes_dir()` now mirrors Hermes' own resolution order:
  1. `settings.hermes_config_dir` — CC Switch explicit override
  2. `HERMES_HOME` env var — trimmed, non-empty, taken as-is (no `~`
     expansion, matching Hermes' `Path(val)`)
  3. platform default — Windows reads the `LOCALAPPDATA` env var (the exact
     value Hermes reads, not a re-derived Known-Folder path) and falls back
     to `~\AppData\Local\hermes`; mac/Linux keep `~/.hermes`

Relation to #3470: that PR dropped `HERMES_HOME` for "consistency with
Codex/Claude". But unlike those tools Hermes treats `HERMES_HOME` as a
first-class mechanism (its own Windows installer sets it), so ignoring it
reintroduces the same mismatch for relocated installs. This keeps it.

Tests cover HERMES_HOME precedence, settings-override > HERMES_HOME, blank
HERMES_HOME fall-through, the platform-correct default, and a pure
`windows_local_hermes_dir` helper exercising the LOCALAPPDATA-set and
LOCALAPPDATA-empty fallback paths on every host.

* test(hermes): isolate config-dir tests from ambient HERMES_HOME/LOCALAPPDATA

`get_hermes_dir()` now consults `HERMES_HOME` (all platforms) and `LOCALAPPDATA`
(Windows). The `with_test_home` helper only neutralized `CC_SWITCH_TEST_HOME`,
so an ambient `HERMES_HOME` — which Hermes' own Windows installer sets — would
make tests that write `get_hermes_config_path()` escape the temp home and touch
a real Hermes config (and the same hazard exists via `LOCALAPPDATA` on Windows).

Clear and restore both env vars in `with_test_home`. This is safe: only this
module reads those env vars, and `dirs` uses the Known-Folder API rather than the
env var. Adds a test asserting both are neutralized inside `with_test_home`.

Addresses the Codex review comment on #4680.

* fix(hermes): trim LOCALAPPDATA to match Hermes' .strip()

`windows_local_hermes_dir` checked `OsStr::is_empty()` on the raw value, but
Hermes does `os.environ.get("LOCALAPPDATA", "").strip()`. A whitespace/padded
`LOCALAPPDATA` would make CC Switch write under the literal padded path while
Hermes trims and falls back, hiding the config from Hermes. Trim before the
empty check, matching the existing `HERMES_HOME` handling. Adds
`windows_local_hermes_dir_trims_localappdata`.

Addresses the Codex review comment on #4680.

---------

Co-authored-by: thisTom <19346741+thisTom@users.noreply.github.com>
2026-06-28 17:49:52 +08:00
yyhhyyyyyy d1f6c74be8 fix(usage): treat usage_script credentials as explicit overrides (#4654)
* fix(usage): treat usage_script credentials as explicit overrides

* fix: treat usage script credentials as explicit overrides
2026-06-28 17:37:58 +08:00
Jason eb847642d0 docs(release-notes): add GitHub global top-100 milestone banner to v3.16.4 notes 2026-06-27 13:32:18 +08:00
105 changed files with 6570 additions and 2821 deletions
+2 -2
View File
@@ -35,7 +35,7 @@ jobs:
run: echo "path=$(pnpm store path --silent)" >> $GITHUB_OUTPUT
- name: Cache pnpm store
uses: actions/cache@v5
uses: actions/cache@v6
with:
path: ${{ steps.pnpm-store.outputs.path }}
key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
@@ -77,7 +77,7 @@ jobs:
|| sudo apt-get install -y --no-install-recommends libsoup2.4-dev
- name: Cache Cargo registry and build
uses: actions/cache@v5
uses: actions/cache@v6
with:
path: |
~/.cargo/registry
+1 -1
View File
@@ -106,7 +106,7 @@ jobs:
- name: Setup pnpm cache
if: runner.os != 'Windows' || matrix.arch != 'arm64'
uses: actions/cache@v5
uses: actions/cache@v6
with:
path: ${{ steps.pnpm-store.outputs.path }}
key: ${{ runner.os }}-${{ runner.arch }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
+47
View File
@@ -5,6 +5,53 @@ 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).
## [3.16.5] - 2026-07-01
Development since v3.16.4 reworks the Codex native-Responses path — restoring a generated model catalog for proxy-less direct-connect, decoupling model mapping from the local-routing toggle, and adding a host/model-prefix blacklist that disables Codex's built-in web_search on gateways that reject it — alongside a broad wave of new provider presets (Qiniu and Code0.ai across all seven apps, the FennoAI/ZetaAPI/TeamoRouter/NekoCode partners, and the non-partner Amux), Claude Sonnet 5 pricing plus a default-tier bump to it, a categorized two-level session view with group-level batch selection, live auto-sync of the shared Claude common config on switch, and a run of credential-safety, tool-detection, Doubao model-id, branding, and icon-size fixes.
**Stats**: 36 commits | 93 files changed | +5,678 insertions | -2,804 deletions
### Added
- **Codex Native Responses Direct-Connect Regenerates a Model Catalog**: Reverses the v3.16.4 direction that dropped the catalog — Codex providers now run in two explicit modes, and native direct-connect once again ships a catalog file. Providers with `apiFormat: "openai_responses"` run with no proxy, and cc-switch generates `~/.codex/cc-switch-model-catalog.json` (`CC_SWITCH_CODEX_MODEL_CATALOG_FILENAME`, provider id `custom`) so Codex Desktop actually shows the custom models and tools work without the freeform `apply_patch` (`type=custom`) tool that native gateways like MiMo reject — editing falls back to `shell_command`. Catalog generation is keyed on `apiFormat` via `CodexCatalogToolProfile` and decoupled from the route-takeover toggle, so a native provider persists a catalog without enabling local route mapping, while `openai_chat` keeps the existing Responses↔Chat proxy conversion unchanged. Codex's parser requires `base_instructions` on every entry, so the native template (`codex_native_responses_template.json`, slug `gpt-5.5`) carries a neutral default that per-vendor official text overrides (MiMo, MiniMax); synthesized catalogs for Qwen/Doubao/LongCat use the neutral default. Existing native providers must be re-saved once to regenerate a valid catalog (no DB migration).
- **Categorized Session View With Two-Level Grouping**: The Session Manager gains a grouped view mode alongside the flat list, toggled via a List/ListTree Select in the toolbar and persisted to `localStorage` under `cc-switch.sessionManager.listViewMode`. The new `groupSessionsByProviderAndDirectory` helper builds a two-level hierarchy (provider group → project-directory group): directory groups are keyed by `getSessionDirectoryGroupKey(providerId, projectDir)` and labeled with `getBaseName(projectDir)`, and sessions lacking a `projectDir` fall into an "unknown directory" bucket (`UNKNOWN_PROJECT_DIR_KEY = __unknown_project_dir__`). Both levels are Radix Collapsible sections whose expansion state is serialized to `cc-switch.sessionManager.groupExpansionState` (pruned to valid keys after load), with a "collapse all" button; in batch mode each group header gains a tri-state checkbox that selects/deselects every selectable session (those with a `sourcePath`) at once, backed by a `Minus`-glyph indeterminate state added to `ui/checkbox.tsx` and a selected/selectable count badge. New keys were added across all four locales (zh/en/ja/zh-TW). The change is entirely frontend — no Tauri commands or DAO changes. (#4776)
- **Qiniu (七牛云) Provider Preset Across All Seven Apps**: Added the Qiniu Cloud AI gateway as a partner aggregator spanning Claude Code, Claude Desktop, Codex, Gemini, OpenCode, OpenClaw, and Hermes — the widest coverage of the batch and the only one carrying a Gemini preset. Because Qiniu relays native Claude/GPT/Gemini rather than remapping to domestic models, Claude Code/Desktop use the Anthropic-compatible bare host with native passthrough (no model pinning), Codex hits native Responses at `https://api.qnaigc.com/bypass/openai/v1` defaulting to `gpt-5.5`, and Gemini uses `gemini-3.1-pro-preview` via `https://api.qnaigc.com/bypass/vertex`; OpenCode/OpenClaw/Hermes default to `gpt-5.5` over the OpenAI-compatible `/v1`. Each type carries `endpointCandidates` listing the `api.qnaigc.com` primary plus the `api.modelink.ai` overseas mirror for failover. It is marked `isPartner` with referral `https://s.qiniu.com/nMvAvy`, a localized display name via `nameKey` (`providerForm.presets.qiniu`) and a partner-promotion blurb in all four locales (zh/en/ja/zh-TW), and registers the `qiniu.png` icon through URL import plus `iconUrls` and metadata.
- **FennoAI, ZetaAPI, and TeamoRouter Partner Presets**: Added three partner aggregators — FennoAI (`api.fenno.ai`), ZetaAPI (`api.zetaapi.ai`), and TeamoRouter (`api.teamorouter.com`) — each covering Claude Code, Claude Desktop, Codex, OpenCode, OpenClaw, and Hermes with no Gemini preset, since these relay native Claude/GPT only. In every case Claude Code/Desktop use the Anthropic-compatible bare host with native passthrough, Codex uses native Responses (FennoAI at `api.fenno.ai`, ZetaAPI and TeamoRouter at their `/v1` paths) defaulting to `gpt-5.5`, and OpenCode/OpenClaw/Hermes default to `gpt-5.5` over the OpenAI-compatible `/v1`. All three are `isPartner` with referral `apiKeyUrls` (FennoAI's ¥9.9 Coding Plan link, ZetaAPI's `zetaapi.ai/go/ccs`, TeamoRouter's `teamorouter.com` with `utm_source=cc_switch` — its in-app link stays English while README links are per-language), each ships a localized promo blurb in zh/en/ja/zh-TW plus a sponsor row in all four READMEs, and each registers its own icon (`fenno-icon.webp`, `zetaapi-icon.png`, `teamorouter`). Two carry surfaced discount codes: ZetaAPI gives a first-recharge 10% discount with promo code `CC-SWITCH`, and TeamoRouter gives new users 10% off their first top-up.
- **Amux Aggregator Preset**: Added Amux (`api.amux.ai`) as a non-partner aggregator across Claude Code, Claude Desktop, Codex, OpenCode, OpenClaw, and Hermes, with no Gemini preset. Claude Code/Desktop use the bare Anthropic-compatible host, Codex uses native Responses over the `/v1` path, and the remaining three apps default to `gpt-5.5` over the OpenAI-compatible endpoint. Unlike the partner presets in this batch it carries no `isPartner` flag, referral link, or promo copy; the `amuxapi-icon.svg` is registered as an inline `currentColor` icon in `src/icons/extracted/index.ts` and `metadata.ts` rather than a raster URL import.
- **Code0.ai Partner Preset Across All Seven Apps**: Added Code0.ai (`code0.ai`) as a partner aggregator spanning all seven apps — Claude Code, Claude Desktop, Codex, Gemini, OpenCode, OpenClaw, and Hermes. Claude Code, Claude Desktop, and Gemini use the bare `https://code0.ai` host (Anthropic-native passthrough with no model pinning for the two Claude apps, `gemini-3.1-pro-preview` for Gemini), Codex hits native Responses at `https://code0.ai/v1`, and OpenCode/OpenClaw/Hermes default to `gpt-5.5` over the OpenAI-compatible `/v1`. It is marked `isPartner` with a `?source=ccswitch` referral on the API-key signup link, a partner-promotion blurb surfacing the CC Switch test-credit offer in all four locales (zh/en/ja/zh-TW), a sponsor row in all four READMEs, and the `code0.png` icon registered via URL import.
- **NekoCode Partner Preset Across Six Apps**: Added NekoCode (`nekocode.ai`) as a partner aggregator spanning six apps — Claude Code, Claude Desktop, Codex, OpenCode, OpenClaw, and Hermes (no Gemini preset). Claude Code and Claude Desktop use the bare `https://nekocode.ai` host (Anthropic-native passthrough with no model pinning), Codex hits native Responses at `https://nekocode.ai/v1`, and OpenCode/OpenClaw/Hermes default to `gpt-5.5` over the OpenAI-compatible `/v1`. It is marked `isPartner` with a `?aff=CCSWITCH` referral on the API-key signup link (kept off the actual request endpoints), a partner-promotion blurb in all four locales (zh/en/ja/zh-TW), and a sponsor row in all four READMEs, both surfacing the CC Switch offer — 10% off top-ups with promo code `cc-switch` at recharge. The `nekocode-icon.png` icon is registered via URL import.
- **Claude Sonnet 5 Model Pricing**: Seeded a `claude-sonnet-5` pricing row in `schema.rs` at Anthropic list price — $3/$15 per Mtok input/output and $0.30/$3.75 cache read/write, identical to the Sonnet 4.6 rates. The introductory $2/$10 promo (valid through 2026-08-31) is deliberately not seeded, so accounting reflects steady-state list pricing rather than a temporary discount. The row is applied on next app start via `ensure_model_pricing_seeded` and requires no `SCHEMA_VERSION` bump.
### Changed
- **Decoupled Codex Model Mapping From the Local-Routing Toggle**: Aligns the Codex provider form with Claude Code — the model-mapping catalog is now independent of route takeover, since native Responses providers (MiMo, Doubao, MiniMax) need it for proxy-less direct connect while Chat providers use the proxy regardless of any per-provider flag. The "Needs Local Routing" toggle is removed: it had no backend field and only gated catalog/reasoning persistence, which is equivalent to whether the mapping is filled. Model mapping is now always shown for non-official providers and persisted whenever the list is non-empty (the backend already keys off `modelCatalog.models`), while reasoning visibility/persistence is gated on the Chat format instead of the toggle. The Chat upstream-format option is marked "routing required" with a refreshed advanced-section hint across all four locales (zh/en/ja/zh-TW), dead `localRouting*` keys are dropped, and the model-mapping block moves above the custom User-Agent block. Also fixes `useCodexConfigState` dropping `supportsParallelToolCalls`/`inputModalities`/`baseInstructions` when loading a saved provider — which silently lost parallel tools, image input, and the official base instructions on edit — by preserving them on load (camelCase and snake_case) and comparing them in the row sync.
- **Auto-Sync Claude Common Config From Live settings.json on Switch**: When switching away from a Claude provider that has `common_config_enabled`, the service now re-extracts the shareable portion of its live `settings.json` and replaces the stored common-config snippet, instead of only writing the snippet one way. This captures config the user added directly in the running app (`enabledPlugins`, `hooks`, `env`, `theme`, shared prefs) so it isn't silently lost on the next switch, and it propagates deletions so a removed key isn't re-injected later. The sync in `services/provider/mod.rs` is scoped strictly to Claude providers with `common_config_enabled`, is skipped when the snippet was explicitly cleared, and all failures are non-fatal (warn-only) and never block the switch. Four integration tests cover capture / delete-sync / opt-out / cleared.
- **Default Sonnet Tier Now Pins to Claude Sonnet 5**: Bumped every default Sonnet pin from `claude-sonnet-4-6`/`claude-sonnet-4.6` to `claude-sonnet-5` across all provider presets (`claudeProviderPresets`, `claudeDesktopProviderPresets`, `hermesProviderPresets`, `openclawProviderPresets`, `opencodeProviderPresets`, and universal `NEWAPI_DEFAULT_MODELS`), covering the `ANTHROPIC_MODEL` / `ANTHROPIC_DEFAULT_SONNET_MODEL` / `ANTHROPIC_DEFAULT_OPUS_MODEL` env keys and prefixed variants like `anthropic/claude-sonnet-5` and `global.anthropic.claude-sonnet-5`. The Claude Desktop `DEFAULT_PROXY_ROUTES` sonnet `route_id` in `claude_desktop_config.rs` also moves to `claude-sonnet-5` (`supports_1m` preserved), and the route-derivation test expectations were updated to match. Non-Anthropic pins (gpt/gemini/glm/sonnet-4-5) were left untouched.
- **Doubao Dated Model Id and YYMMDD Pricing Normalization**: Switched the Doubao (DouBaoSeed) preset model id to the dated form `doubao-seed-2-1-pro-260628` across every app surface (config default, generated Codex catalog, and OpenClaw namespaced refs), because Volcengine Ark rejects the bare `doubao-seed-2-1-pro` with a 404 ("model does not exist or you do not have access to it") even after activation and only accepts the full dated id. Because real usage now arrives with a date suffix, `strip_model_date_suffix` in `usage_stats.rs` was extended to also strip Volcengine's 6-digit YYMMDD form (`-260628`, `-250615`) in addition to the existing 8-digit YYYYMMDD/ISO handling; the 6-digit branch validates month 01-12 and day 01-31 to avoid eating non-date version suffixes like `-123456`, then falls back to `None` for exact matching. This keeps the bare-name seed row as the canonical pricing identity, fixing the $0-cost display for every Volcengine Doubao model. Unit and end-to-end regression tests were added.
- **"Write Common Config" Renamed to "Apply Common Config"**: The original label "Write Common Config" (写入通用配置) was ambiguous about data-flow direction, reading as "write the current config INTO the common config" when the actual behavior is the reverse — the saved common-config snippet is merged INTO this provider's config. The checkbox is renamed to "Apply Common Config" across all four locales (zh/en/ja/zh-TW), including every hint/guide/notice reference and the `CommonConfigEditor` / `GeminiConfigSections` defaultValue fallbacks, and the Gemini hint wording is aligned with the claude/codex "when checked" phrasing. The Japanese user manual (`docs/user-manual/ja/2-providers/2.1-add.md`) and `README_JA.md` were also synced to 共通設定を適用 / 共有設定を適用. (#4829)
- **OpenClaw Doubao Context Window Aligned to 262144**: The OpenClaw DouBaoSeed preset hard-coded `contextWindow` 128000 while the Codex preset/catalog used 262144 for the same model, giving OpenClaw users a too-small window that could compress or truncate long context prematurely. Bumped `openclawProviderPresets.ts` to 262144 and added a cross-preset consistency test asserting the OpenClaw and Codex Doubao context windows stay equal so the two sides cannot silently drift apart again.
- **Volcengine/Doubao/BytePlus Website Links Restored to Official Homepages**: The `websiteUrl` for the 火山Agentplan, BytePlus, and DouBaoSeed presets had accidentally been set to the same invite/console link as `apiKeyUrl`, so the "visit official site" button routed users to the referral/API-key page instead of the product homepage. Restored clean official homepages across all six app preset files — 火山Agentplan to `https://www.volcengine.com/product/ark`, DouBaoSeed to `https://www.volcengine.com/product/doubao`, and BytePlus to `https://www.byteplus.com/en/product/modelark` (dropping the utm params) — while leaving the `apiKeyUrl` referral links intact.
- **Kimi and SiliconFlow Referral Links Refreshed**: Updated two sponsor referral sets across presets and READMEs. Following Moonshot's console rebrand to Kimi, the Kimi preset referral links and the domestic (ZH) README banner moved to the new `platform.kimi.com` domain — the domestic console `platform.moonshot.cn/console``platform.kimi.com`, the API-key page → `platform.kimi.com/console/api-keys`, and the Coding Plan link `www.kimi.com/code/docs/``www.kimi.com/code/` — and the missing `?aff=cc-switch` attribution param was added to the codex and openclaw entries. API endpoints (`api.moonshot.cn`, `api.kimi.com/coding`) were left unchanged, and the presets have no overseas variant (the separate EN/DE/JA README switch to `platform.kimi.ai` is covered under Docs below). Separately, the SiliconFlow invite code was rotated from `drGuwc9k` to `YflgU2Ve` across all README locales and the claude, claude-desktop, codex, hermes, and openclaw presets.
- **Downscaled Oversized Provider Icons to 256px**: Shrank a batch of bundled provider icons that shipped far larger than their ~32px on-screen render size in `ProviderIcon`, cutting bundle weight with no code, filename, or import changes (aspect ratio preserved, rendered via `<img>` object-contain). Raster PNGs: ZetaAPI 1254×1254/940KB→40KB, relaxcode 1462×1076/1.16MB→42KB, sudocode 2048×2048/432KB→37KB, hermes 512×512/125KB→38KB, claudecn 512×512/109KB→46KB, atlascloud 3525×3300/105KB→9KB. Two "fake vector" SVGs wrapping a single oversized base64 PNG were re-embedded at a 256px max dimension keeping the SVG wrapper: ccsub 1.16MB→60KB and shengsuanyun 212KB→51KB. Also removed `dds.svg`, a 1.4MB genuine 2222-path vector that was never imported in `index.ts` nor referenced under `src/`, so it only bloated the repo without ever shipping.
### Fixed
- **Disable web_search for Native Codex Gateways That Reject It**: Some native `/responses` gateways whose first-party models lack OpenAI's hosted `web_search` tool reject it with "tool type 'web_search' is not supported by this gateway phase" (`responses_feature_not_supported`), and Codex sends the tool by default (config-driven, not gated by the catalog's `supports_search_tool`), producing hard 400s. cc-switch now writes the top-level TOML line `web_search = "disabled"` for those vendors via `set_codex_native_web_search_field`, injected alongside `model_catalog_json` at switch time. Scope is a blacklist (default-on): only providers matched by `base_url` host — `CODEX_WEB_SEARCH_REJECT_HOSTS = xiaomimimo.com, longcat.chat, minimax.io, minimaxi.com` — or by model brand prefix — `CODEX_WEB_SEARCH_REJECT_MODEL_PREFIXES = mimo, longcat, minimax, qwen3-coder` — are disabled, so relays serving real GPT, DouBao, general Qwen, and any unknown provider keep Codex's default. The `qwen3-coder` prefix suppresses the tool for the native `qwen3-coder-plus` direct-connect preset (百炼/DashScope marks built-in tools unsupported for the coder series) while general Qwen models sharing the DashScope host stay enabled — matching is on the model axis (after stripping any aggregator `vendor/` path segment like `MiniMaxAI/MiniMax-M3` or `qwen/qwen3-coder-plus`), so it also catches aggregators such as SiliconFlow fronting a reject vendor's model. A blacklist was chosen over a fuzzy "is this GPT?" whitelist because wrongly keeping `web_search` ON fails with a hard 400, and an ownership sentinel means cc-switch only ever removes a `web_search` key whose value equals its own `disabled`, so existing providers need no re-save and switching back re-enables web search. Also corrects the LongCat-2.0-Preview preset context window from 131072 (128K) to its real 1048576 (1M), aligning with the MiMo/Qwen 2^20 convention, and tightens the native Responses preset tests to assert exact model→contextWindow catalogs instead of only checking catalog presence.
- **Strip All Credential-Like Keys From the Shared Claude Common-Config Snippet**: `extract_claude_common_config` previously only redacted `ANTHROPIC_API_KEY` and `ANTHROPIC_AUTH_TOKEN`, but Claude providers legitimately carry other credentials (`OPENROUTER_API_KEY`, `GOOGLE_API_KEY`, and possibly OpenAI/Gemini/AWS Bedrock/Vertex secrets), which could leak into the shared snippet and then be injected into other providers. Extraction now pattern-matches and strips any credential-shaped env key (`*_API_KEY` / `*_AUTH_TOKEN` / `*secret*` / `*token*`, etc.), while preserving plural `*_TOKENS` values like `MAX_OUTPUT_TOKENS` as legitimately shareable. This also closes the same pre-existing leak in the manual Extract and one-time auto-extract paths, and is covered by a dedicated credential-stripping unit test.
- **Usage-Script Credentials Persisted Only as Explicit Overrides**: Provider usage scripts store optional `api_key`/`base_url` fields that override the provider's live credentials when querying quota, but these were silently mirroring the provider's own credentials — so copying a provider or editing its main API key/base URL left the usage script pinned to the old endpoint and key, and quota queries kept hitting the stale target. `ProviderService::add`/`update` now run `normalize_usage_script_credential_overrides` before persisting: if the script's trimmed `api_key` or slash-normalized `base_url` matches the provider's resolved usage credentials (or is blank) it is cleared to `None` so the query falls back to `resolve_usage_credentials` from the live config, while genuinely distinct overrides are kept and `template_type == "token_plan"` scripts are left untouched. The deeplink import path gained matching `normalize_deeplink_api_key`/`base_url` helpers, and the frontend now invalidates `usageKeys.script(id, appId)` (including the `originalId` when a provider is renamed) on update so the homepage re-queries with the corrected config instead of the test-time cache. (#4654)
- **Hermes Config Dir Now Resolves Correctly on Windows**: CC Switch hardcoded `~/.hermes` as the Hermes config directory, but Hermes itself resolves it via `get_hermes_home()` — the `HERMES_HOME` env var, then a platform default of `%LOCALAPPDATA%\hermes` on Windows (`~/.hermes` on mac/Linux). On Windows this meant CC Switch wrote provider configs to a path Hermes never reads, so provider switches had no effect. `get_hermes_dir()` now mirrors Hermes' own resolution order — `settings.hermes_config_dir` explicit override, then `HERMES_HOME` taken verbatim (trimmed, non-empty, no `~` expansion, matching Hermes' `Path(val)`), then the platform default reading the actual `LOCALAPPDATA` env var (falling back to `~\AppData\Local\hermes`) on Windows and `~/.hermes` elsewhere. This deliberately re-honors the `HERMES_HOME` that #3470 had dropped, since unlike Codex/Claude the Hermes Windows installer sets `HERMES_HOME` as a first-class mechanism for relocated installs. Config-dir tests were also isolated from ambient `HERMES_HOME`/`LOCALAPPDATA`. (#4680, refs #3178, #3470)
- **Linux Wayland: Override the AppImage's Forced `GDK_BACKEND=x11`**: The AppImage's GTK launch hook (`linuxdeploy-plugin-gtk.sh`) unconditionally exports `GDK_BACKEND=x11` to dodge a historical native-Wayland crash (tauri-apps/tauri#8541). On newer Wayland + NVIDIA setups this forced XWayland leaves the WebKitGTK web content unable to receive pointer events — the GTK title bar stays clickable but the page is dead — and black-screens on resize; the existing `WEBKIT_DISABLE_*` mitigations don't help because the root cause is the forced window backend, not rendering. Since the hook also overrides any user-set `GDK_BACKEND`, there was no way to switch back without unpacking the AppImage. `main.rs` now reads an opt-in `CC_SWITCH_GDK_BACKEND` escape hatch (which the hook never touches) before GTK init: leaving it unset keeps the current x11 behavior unchanged (zero regression), while `CC_SWITCH_GDK_BACKEND=wayland` forces native Wayland. The override is generic, so users on tiling Wayland compositors hitting the inverse input bug can set `CC_SWITCH_GDK_BACKEND=x11`. (#4351, fixes #4350)
- **Get API Key Link Now Shows in Claude Desktop, OpenClaw, and Hermes Forms**: The "Get API Key" link and partner-promotion block below the API key input was only wired for claude/codex/gemini/opencode. Claude Desktop rendered a bare Input that never showed it, and OpenClaw/Hermes were blocked by two gaps: `useApiKeyLink` whitelisted only those four appIds (so the link was suppressed even when a preset carried `apiKeyUrl`), and `useProviderCategory` only parsed the `/^(claude|codex|gemini|opencode)-(\d+)$/` preset-id pattern (so OpenClaw/Hermes category stayed undefined and the cn_official/aggregator/third_party link condition never held). `ClaudeDesktopProviderForm` now calls `useApiKeyLink` and uses the shared `ApiKeySection`; the `useApiKeyLink` whitelist and its `PresetEntry` union add claude-desktop/openclaw/hermes; and `useProviderCategory` now resolves openclaw/hermes preset categories. Additionally, `HermesFormFields` and `OpenClawFormFields` no longer let an "official" category disable the key input, since these apps have no OAuth-only official providers (e.g. Hermes' Nous Research is official but still needs a user-supplied key).
- **Deduplicated Windows Codex npm Shims in Tool Detection**: On Windows, npm installs a tool as three sibling files — `codex.cmd`, `codex.exe`, and an extensionless Unix shim named `codex` — and CC Switch's `tool_executable_candidates` listed all three, so the extensionless shim (which Windows cannot execute directly) was probed as a redundant/failing candidate. `tool_executable_candidates` now appends the extensionless path only when `windows_runnable_sibling_for_extensionless_tool` finds no adjacent `.cmd`/`.exe` sibling, and `resolve_path_default` likewise prefers a runnable `.cmd`/`.exe` sibling before canonicalizing a bare extensionless PATH hit. This keeps version detection and launching anchored to the actually-runnable Windows shim instead of the shadowed Unix one. (#4782)
- **Scroll Bounds for Long Select Dropdowns**: The `SelectContent` popover used `overflow-hidden` with no height cap, so dropdowns with many options (e.g. long model or provider lists) rendered taller than the viewport and clipped their overflowing items with no way to reach them. It now sets `max-h-[min(24rem,var(--radix-select-content-available-height))]` and `overflow-y-auto overflow-x-hidden`, bounding the content to 24rem or the Radix-computed available height and letting the list scroll vertically while still clipping horizontally. (#4798)
- **Date-Range Picker Calendar Stays On-Screen in Narrow Popovers**: The custom date-range picker switched to its two-column layout (date fields | calendar) based on the viewport width via Tailwind's `sm:` (640px) breakpoint, but the popover is clamped to `100vw - 2rem` and anchored to its trigger with `align="end"`, so its real available width is narrower than the viewport. On narrow windows the two-column layout could activate while the popover only had room for one column, pushing the calendar column off the right edge where it was clipped — the month header and 4 of 7 weekday columns cut off and unreachable. The layout now keys off the popover's own inline size via a CSS container query (`w-[620px] max-w-[calc(100vw-2rem)]`) instead of the viewport, so it collapses to one column exactly when the popover itself is narrow, keeping the calendar fully visible at any window width. (#4860)
### Docs
- **Documented the `CC_SWITCH_GDK_BACKEND` Escape Hatch in README and User Manual**: Added an FAQ entry for the opt-in `CC_SWITCH_GDK_BACKEND` environment variable (see the corresponding Fixed entry) across all four README locales (`README.md`, `README_ZH.md`, `README_JA.md`, `README_DE.md`) and the zh/en/ja user-manual troubleshooting pages (`docs/user-manual/{zh,en,ja}/5-faq/5.2-questions.md`), explaining how Wayland + NVIDIA users can switch back to native Wayland when the webview goes click-dead and black-screens on resize, and how tiling-Wayland users can set it to `x11` for the inverse input bug.
- **Overseas Kimi READMEs Point to platform.kimi.ai With a Coding Plan Link**: Updated the Kimi K2.7 Code partner section across the English, German, and Japanese READMEs so the banner image and both inline CTAs point to `https://platform.kimi.ai?aff=cc-switch` instead of the old `platform.kimi.com` host, keeping the `aff=cc-switch` referral tag intact. All four localized READMEs (`README.md`, `README_DE.md`, `README_JA.md`, `README_ZH.md`) also gained a new line promoting the Kimi For Coding subscription plan linked to `https://www.kimi.com/code/?aff=cc-switch`; note the ZH README kept its existing `platform.kimi.com` link and only received the Coding Plan addition.
- **GitHub Global Top-100 Milestone Banner in v3.16.4 Release Notes**: Prepended a celebratory callout blockquote to all three localized v3.16.4 release notes (`docs/release-notes/v3.16.4-en.md`, `-ja.md`, `-zh.md`) announcing that CC Switch has entered the global top 100 GitHub projects by star count, thanking users, contributors, and stargazers. This is a docs-only addition to the release-notes header and does not change any product behavior.
## [3.16.4] - 2026-06-27
Development since v3.16.3 focuses on tightening the Codex proxy path — native OpenAI Responses migration for the major Chinese providers, a decoupled upstream-format selector, zstd request/error-body decompression, and a run of tool-call and OAuth-over-proxy fixes — alongside richer usage and pricing tooling (models.dev pricing import, Volcengine Ark coding/agent-plan quotas, live-tracking date ranges, GLM-5.2/Doubao Seed 2.1 pricing), new proxy and resilience capabilities (custom request header/body overrides, an in-app recovery screen for too-new databases, native Windows ARM64 builds), and a broad wave of preset and branding updates (the SubRouter and OpenCode Go subscriptions, the CTok→ETok rename, Kimi rebranding and prime-partner badges, and a Kimi K2.7 Code sponsor banner).
+46 -4
View File
@@ -25,9 +25,11 @@ English | [中文](README_ZH.md) | [日本語](README_JA.md) | [Deutsch](README_
<details open>
<summary>Click to collapse</summary>
[![Kimi K2.7 Code](assets/partners/banners/kimi-banner-en.png)](https://platform.moonshot.cn/console?aff=cc-switch)
[![Kimi K2.7 Code](assets/partners/banners/kimi-banner-en.png)](https://platform.kimi.ai?aff=cc-switch)
Kimi K2.7 Code is an open-source, coding-focused agentic model developed by Moonshot AI. It delivers stronger coding and agent performance, with substantial improvements in real-world long-horizon coding tasks. These gains translate into higher end-to-end task success rates across complex software engineering workflows. K2.7 Code also improves reasoning efficiency, reducing thinking-token usage by approximately 30% compared with K2.6. **[Click here to start using Kimi](https://platform.moonshot.cn/console?aff=cc-switch)**
Kimi K2.7 Code is an open-source, coding-focused agentic model developed by Moonshot AI. It delivers stronger coding and agent performance, with substantial improvements in real-world long-horizon coding tasks. These gains translate into higher end-to-end task success rates across complex software engineering workflows. K2.7 Code also improves reasoning efficiency, reducing thinking-token usage by approximately 30% compared with K2.6. **[Click here to start using Kimi](https://platform.kimi.ai?aff=cc-switch)**
Doing mostly coding work? Try the **[Kimi For Coding plan](https://www.kimi.com/code/?aff=cc-switch)** — a subscription service built for coding!
---
@@ -60,14 +62,21 @@ It also supports enterprise-grade concurrency and provides a dedicated managemen
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://teamorouter.com/?utm_source=cc_switch&utm_medium=referral&utm_campaign=ai_directory"><img src="assets/partners/logos/TeamoRouter-banner.png" alt="TeamoRouter" width="150"></a></td>
<td>Thanks to TeamoRouter for sponsoring this project! TeamoRouter is an enterprise-grade Agentic LLM gateway built for developers, AI teams, and businesses. Without requiring any subscriptions, it lets you access Claude Code, Codex, Gemini CLI, OpenAI Codex, and other popular AI agents through a single unified API, while offering API pricing at discounts of up to 90%.
Unlike typical API relay services, TeamoRouter aggregates hundreds of official model providers and trusted infrastructure partners, including OpenAI, Anthropic, Vertex, Azure, and AWS bedrock. Every provider is verified for 100% Agent protocol compatibility, cache performance, and request traceability, ensuring stable quality instead of reverse-engineered or diluted endpoints. The platform delivers near-official TTFT, 99.6% SLA, enterprise-scale throughput up to 5,000 QPM, and industry-leading cache hit rates that dramatically reduce token costs for long-running agent workflows.
TeamoRouter also offers enterprise features including centralized billing, team management, BYOK, smart routing, usage analytics, dynamic provider optimization, and dedicated support. For an even simpler experience, Teamo Desktop lets you use Claude Code, Codex, Gemini CLI, and other popular AI agents with one-click setup—no API key management or manual gateway configuration required. Register via <a href="https://teamorouter.com/?utm_source=cc_switch&utm_medium=referral&utm_campaign=ai_directory">this link</a> as a new user to receive 10% off your first top-up.</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/ai618?utm_campaign=hw&utm_content=hw&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>
<td width="180"><a href="https://cloud.siliconflow.cn/i/YflgU2Ve"><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/YflgU2Ve">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>
@@ -115,6 +124,11 @@ Register now via <a href="https://pateway.ai/?ch=etzpm8&aff=WB6M6F67#/">this lin
<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://code0.ai?source=ccswitch"><img src="assets/partners/logos/code0.png" alt="code0.ai" width="150"></a></td>
<td>Thanks to <a href="https://code0.ai?source=ccswitch">code0.ai</a> for sponsoring this project! code0.ai is an AI coding service platform built for developers, supporting Claude Code, Codex, Gemini, and other mainstream AI coding capabilities. It helps individual developers and teams use AI Agents more stably and efficiently for coding, debugging, refactoring, and automation workflows. ccswitch users can contact customer support via the <a href="https://code0.ai?source=ccswitch">code0.ai website</a> to claim test credits and experience a reliable AI coding service.</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>
@@ -150,6 +164,21 @@ Register now via <a href="https://pateway.ai/?ch=etzpm8&aff=WB6M6F67#/">this lin
<td>Thanks to Unity2.ai for sponsoring this project! Unity2.ai is a high-performance AI model API relay platform for individual developers, teams, and enterprises. Long trusted by leading companies in China, it serves over 30 billion tokens per day and supports high concurrency at the 5,000 RPM level. It offers balance-based billing, first top-up bonuses, bundle subscriptions, corporate invoicing, and dedicated support. Register via <a href="https://unity2.ai/register?source=ccs">this link</a> to get $2 in credits, plus another $10 for joining the official group — up to $12 in free credits!</td>
</tr>
<tr>
<td width="180"><a href="https://api.fenno.ai/register?redirect=/purchase?tab=subscription%26group=16&aff=P9MR3D3PLCNL"><img src="assets/partners/logos/fenno-banner.png" alt="Fenno.ai" width="150"></a></td>
<td>Thanks to Fenno.ai for sponsoring this project! Fenno.ai is a stable and efficient API relay service provider, currently focused on Codex relay. It is compatible with the OpenAI and Anthropic protocols and can be flexibly used from Codex, Claude Code, OpenCode, and other mainstream coding tools. It reliably supports enterprise-grade workloads of hundreds of billions of tokens per day, with corporate (B2B) settlement and invoicing for both domestic and overseas entities. Fenno.ai offers an exclusive benefit for CC Switch users: subscribe via <a href="https://api.fenno.ai/register?redirect=/purchase?tab=subscription%26group=16&aff=P9MR3D3PLCNL">this link</a> to the incredible ¥9.9 Coding Plan worth $150 in credits, and earn up to 20% in referral rewards — invite more, earn more!</td>
</tr>
<tr>
<td width="180"><a href="https://zetaapi.ai/go/ccs"><img src="assets/partners/logos/zetaapi-banner.png" alt="ZetaAPI" width="150"></a></td>
<td>Thanks to ZetaAPI for sponsoring this project! ZetaAPI focuses on real model fidelity, no watered-down responses, no quality degradation, and pricing as low as 35% of official rates. The platform does not mix traffic, secretly replace models with lower-quality alternatives, or use fake model routing. It supports Claude Code, Codex, Gemini, ChatGPT, and other mainstream AI models, helping users significantly reduce API costs while maintaining reliable model quality. At the same time, ZetaAPI provides enterprise-grade SLA-backed stability, standard API compatibility, one API key for multiple models, fast integration, and pay-as-you-go billing, making it suitable for AI products, coding agents, internal business tools, customer service systems, content generation, and automation workflows. If any model is verified to be inconsistent with its stated quality, ZetaAPI backs it with a 10x compensation guarantee, giving users a more stable, transparent, and trustworthy experience. Register via <a href="https://zetaapi.ai/go/ccs">this link</a> and use the promo code CC-SWITCH during your first recharge to enjoy an exclusive 10% discount on your first top-up, just for CC Switch users!</td>
</tr>
<tr>
<td width="180"><a href="https://nekocode.ai?aff=CCSWITCH"><img src="assets/partners/logos/nekocode-banner.png" alt="NekoCode" width="150"></a></td>
<td>Thanks to <a href="https://nekocode.ai?aff=CCSWITCH">NekoCode</a> for sponsoring this project! NekoCode provides developers with a stable, efficient, and reliable API relay service for Claude, Codex, and other AI models. With transparent pricing and flexible pay-as-you-go billing, it offers a simple and cost-effective way to access AI models. CC Switch users can enjoy an exclusive 10% discount: register via <a href="https://nekocode.ai?aff=CCSWITCH">this link</a> and enter promo code <code>cc-switch</code> during recharge to receive 10% off your top-up!</td>
</tr>
</table>
</details>
@@ -265,6 +294,19 @@ Add an official provider from the preset list. After switching to it, run the Lo
</details>
<details>
<summary><strong>Linux (Wayland + NVIDIA): clicks don't register and the window black-screens on resize</strong></summary>
The AppImage forces `GDK_BACKEND=x11` (XWayland) to avoid a historical native-Wayland crash. On newer Wayland + NVIDIA setups this can leave the web content area unclickable (the title-bar buttons still work) and black-screen on resize. Launch with the opt-in escape hatch to switch back to native Wayland:
```bash
CC_SWITCH_GDK_BACKEND=wayland ./CC-Switch-*.AppImage
```
If you launch from a desktop icon, add it to the `.desktop` `Exec=` line (e.g. `env CC_SWITCH_GDK_BACKEND=wayland /path/to/AppImage`) or set it in your session environment. The variable is generic: on tiling Wayland compositors (sway/Hyprland) where clicks don't register, try `CC_SWITCH_GDK_BACKEND=x11` instead. Leaving it unset keeps the default behavior.
</details>
## Documentation
For detailed guides on every feature, check out the **[User Manual](docs/user-manual/en/README.md)** — covering provider management, MCP/Prompts/Skills, proxy & failover, and more.
+46 -4
View File
@@ -25,9 +25,11 @@
<details open>
<summary>Zum Einklappen klicken</summary>
[![Kimi K2.7 Code](assets/partners/banners/kimi-banner-en.png)](https://platform.moonshot.cn/console?aff=cc-switch)
[![Kimi K2.7 Code](assets/partners/banners/kimi-banner-en.png)](https://platform.kimi.ai?aff=cc-switch)
Kimi K2.7 Code ist ein quelloffenes, auf Programmierung spezialisiertes Agenten-Modell von Moonshot AI. Es bietet eine stärkere Programmier- und Agentenleistung mit erheblichen Verbesserungen bei realen, langfristigen Programmieraufgaben. Diese Fortschritte führen zu höheren End-to-End-Erfolgsraten in komplexen Software-Engineering-Workflows. Zudem verbessert K2.7 Code die Reasoning-Effizienz und reduziert den Verbrauch an Thinking-Tokens um rund 30 % gegenüber K2.6. **[Hier klicken, um Kimi auszuprobieren](https://platform.moonshot.cn/console?aff=cc-switch)**
Kimi K2.7 Code ist ein quelloffenes, auf Programmierung spezialisiertes Agenten-Modell von Moonshot AI. Es bietet eine stärkere Programmier- und Agentenleistung mit erheblichen Verbesserungen bei realen, langfristigen Programmieraufgaben. Diese Fortschritte führen zu höheren End-to-End-Erfolgsraten in komplexen Software-Engineering-Workflows. Zudem verbessert K2.7 Code die Reasoning-Effizienz und reduziert den Verbrauch an Thinking-Tokens um rund 30 % gegenüber K2.6. **[Hier klicken, um Kimi auszuprobieren](https://platform.kimi.ai?aff=cc-switch)**
Hauptsächlich mit Programmierung beschäftigt? Probieren Sie den **[Kimi-For-Coding-Plan](https://www.kimi.com/code/?aff=cc-switch)** ein Abo, das speziell fürs Programmieren entwickelt wurde!
---
@@ -60,14 +62,21 @@ Er unterstützt zudem unternehmensgerechte Nebenläufigkeit und stellt Unternehm
Registrieren Sie sich jetzt über <a href="https://pateway.ai/?ch=etzpm8&aff=WB6M6F67#/">diesen Link</a> und erhalten Sie ein Testguthaben von 3 $. Aufladungen sind ab 60 % des Originalpreises möglich, mit einem beidseitigen Empfehlungsbonus von bis zu 150 $!</td>
</tr>
<tr>
<td width="180"><a href="https://teamorouter.com/?utm_source=cc_switch&utm_medium=referral&utm_campaign=ai_directory"><img src="assets/partners/logos/TeamoRouter-banner.png" alt="TeamoRouter" width="150"></a></td>
<td>Danke an TeamoRouter für die Unterstützung dieses Projekts! TeamoRouter ist ein Agentic-LLM-Gateway in Enterprise-Qualität, das für Entwickler, KI-Teams und Unternehmen entwickelt wurde. Ganz ohne Abonnement können Sie über eine einzige einheitliche API auf Claude Code, Codex, Gemini CLI, OpenAI Codex und weitere beliebte KI-Agenten zugreifen — bei API-Preisen mit Rabatten von bis zu 90 %.
Anders als typische API-Relay-Dienste bündelt TeamoRouter Hunderte offizieller Modellanbieter und vertrauenswürdiger Infrastrukturpartner, darunter OpenAI, Anthropic, Vertex, Azure und AWS Bedrock. Jeder Anbieter wird auf 100%ige Kompatibilität mit dem Agent-Protokoll, Cache-Performance und Nachverfolgbarkeit von Anfragen geprüft und liefert so stabile Qualität statt reverse-engineerter oder verwässerter Endpunkte. Die Plattform bietet nahezu offizielle TTFT, 99,6 % SLA, Durchsatz in Unternehmensgröße von bis zu 5.000 QPM und branchenführende Cache-Trefferquoten, die die Token-Kosten für langlaufende Agent-Workflows drastisch senken.
TeamoRouter bietet außerdem Enterprise-Funktionen wie zentrale Abrechnung, Team-Verwaltung, BYOK, intelligentes Routing, Nutzungsanalysen, dynamische Anbieter-Optimierung und dedizierten Support. Für ein noch einfacheres Erlebnis können Sie mit Teamo Desktop Claude Code, Codex, Gemini CLI und weitere beliebte KI-Agenten per Ein-Klick-Einrichtung nutzen — ohne Verwaltung von API-Schlüsseln oder manuelle Gateway-Konfiguration. Registrieren Sie sich als neuer Nutzer über <a href="https://teamorouter.com/?utm_source=cc_switch&utm_medium=referral&utm_campaign=ai_directory">diesen Link</a> und erhalten Sie 10 % Rabatt auf Ihre erste Aufladung.</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>Danke an Dola seed für die Unterstützung dieses Projekts! Dola Seed 2.0 ist ein voll-modales Allzweck-Großmodell, das von ByteDance eigenständig für den globalen Markt entwickelt wurde. Aufbauend auf einer einheitlichen multimodalen Architektur unterstützt es das gemeinsame Verstehen und Generieren von Text, Bildern, Audio und Video. Es ermöglicht von Haus aus die Zusammenarbeit von Agenten und verfügt über starke Fähigkeiten in den Bereichen Schlussfolgern, Ausführung langer Aufgaben, Werkzeugintegration und Programmierung. Es ist breit einsetzbar — etwa für intelligente Cockpits, persönliche Assistenten, Bildung, Kundensupport, Marketing, Einzelhandel und weitere Szenarien. Es überzeugt bei multimodaler Wahrnehmung, der Ende-zu-Ende-Bewältigung komplexer Aufgaben, stabiler Interaktion und Datensicherheit und ist über die ModelArk-Plattform einfach zugänglich und bereitstellbar. Registrieren Sie sich über <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">diesen Link</a> und erhalten Sie pro Modell ein kostenloses Inferenzkontingent von 500.000 Token.<a href="https://www.volcengine.com/activity/ai618?utm_campaign=hw&utm_content=hw&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>Danke an SiliconFlow für die Unterstützung dieses Projekts! SiliconFlow ist eine leistungsstarke KI-Infrastruktur- und Modell-API-Plattform, die schnellen und zuverlässigen Zugriff auf Sprach-, Audio-, Bild- und Videomodelle an einem Ort bietet. Mit nutzungsbasierter Abrechnung, breiter Unterstützung multimodaler Modelle, Hochgeschwindigkeitsinferenz und unternehmensgerechter Stabilität hilft SiliconFlow Entwicklern und Teams, KI-Anwendungen effizienter zu erstellen und zu skalieren. Registrieren Sie sich über <a href="https://cloud.siliconflow.cn/i/drGuwc9k">diesen Link</a> und schließen Sie die Identitätsverifizierung ab, um ein Bonusguthaben von ¥16 zu erhalten, das für alle Modelle der Plattform nutzbar ist. SiliconFlow ist zudem nun mit OpenClaw kompatibel, sodass Nutzer einen SiliconFlow-API-Schlüssel verbinden und große KI-Modelle kostenlos aufrufen können.</td>
<td width="180"><a href="https://cloud.siliconflow.cn/i/YflgU2Ve"><img src="assets/partners/logos/silicon_en.jpg" alt="SiliconFlow" width="150"></a></td>
<td>Danke an SiliconFlow für die Unterstützung dieses Projekts! SiliconFlow ist eine leistungsstarke KI-Infrastruktur- und Modell-API-Plattform, die schnellen und zuverlässigen Zugriff auf Sprach-, Audio-, Bild- und Videomodelle an einem Ort bietet. Mit nutzungsbasierter Abrechnung, breiter Unterstützung multimodaler Modelle, Hochgeschwindigkeitsinferenz und unternehmensgerechter Stabilität hilft SiliconFlow Entwicklern und Teams, KI-Anwendungen effizienter zu erstellen und zu skalieren. Registrieren Sie sich über <a href="https://cloud.siliconflow.cn/i/YflgU2Ve">diesen Link</a> und schließen Sie die Identitätsverifizierung ab, um ein Bonusguthaben von ¥16 zu erhalten, das für alle Modelle der Plattform nutzbar ist. SiliconFlow ist zudem nun mit OpenClaw kompatibel, sodass Nutzer einen SiliconFlow-API-Schlüssel verbinden und große KI-Modelle kostenlos aufrufen können.</td>
</tr>
<tr>
@@ -115,6 +124,11 @@ Registrieren Sie sich jetzt über <a href="https://pateway.ai/?ch=etzpm8&aff=WB6
<td>Dieses Projekt wird von <a href="https://console.claudeapi.com/register?aff=pCLD">Claude API</a> gesponsert. Direkter Claude-API-Zugriff — verbinden Sie Claude Code und Agent-Apps in 3 Minuten. Neukunden können ein kostenloses Testguthaben einlösen. Betrieben mit offiziellen Anthropic-API-Schlüsseln + offiziellen AWS-Bedrock-Kanälen. Kein Reverse Engineering, keine Modellverschlechterung. Volle Unterstützung der Modellreihe Opus / Sonnet / Haiku, mit erhaltenen offiziellen Fähigkeiten einschließlich Tool Use, 1M-Kontextfenster und mehr. Entwickelt für Claude-Code-Power-User, Agent-Ingenieure und technische Unternehmensteams. Rechnungsstellung und dedizierter Team-Support verfügbar. Klicken Sie <a href="https://console.claudeapi.com/register?aff=pCLD">hier</a>, um sich zu registrieren!</td>
</tr>
<tr>
<td width="180"><a href="https://code0.ai?source=ccswitch"><img src="assets/partners/logos/code0.png" alt="code0.ai" width="150"></a></td>
<td>Vielen Dank an <a href="https://code0.ai?source=ccswitch">code0.ai</a> für die Unterstützung dieses Projekts! code0.ai ist eine für Entwickler entwickelte AI-Coding-Service-Plattform, die Claude Code, Codex, Gemini und weitere gängige AI-Coding-Funktionen unterstützt. Sie hilft einzelnen Entwicklern und Teams, AI-Agents stabiler und effizienter für Programmierung, Debugging, Refactoring und Automatisierungs-Workflows zu nutzen. ccswitch-Nutzer können über die <a href="https://code0.ai?source=ccswitch">code0.ai-Website</a> den Kundensupport kontaktieren, um Testguthaben zu erhalten und einen zuverlässigen AI-Coding-Service zu erleben.</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>Danke an ClaudeCN für die Unterstützung dieses Projekts! ClaudeCN ist eine unternehmensgerechte KI-Gateway-Plattform, die von einem eingetragenen Unternehmen betrieben wird. Sie bietet hochverfügbaren kommerziellen API-Zugriff auf beliebte Modelle wie Claude, GPT und DeepSeek und ist auf formelle Unternehmensbeschaffungsprozesse ausgerichtet — Banküberweisungen von Firmen, unterzeichnete Verträge und volle Compliance. Registrieren Sie sich über <a href="https://claudecn.top">diesen Link</a>!</td>
@@ -150,6 +164,21 @@ Registrieren Sie sich jetzt über <a href="https://pateway.ai/?ch=etzpm8&aff=WB6
<td>Danke an Unity2.ai für die Unterstützung dieses Projekts! Unity2.ai ist eine leistungsstarke AI-Modell-API-Relay-Plattform für Einzelentwickler, Teams und Unternehmen. Sie wird seit Langem von führenden Unternehmen in China genutzt, verarbeitet täglich über 30 Milliarden Tokens und unterstützt hohe Parallelität auf 5.000-RPM-Niveau. Geboten werden Guthaben-Abrechnung, Ersteinzahlungsbonus, Kombi-Abonnements, Firmenrechnungen und persönliche Betreuung. Registrieren Sie sich über <a href="https://unity2.ai/register?source=ccs">diesen Link</a> und erhalten Sie $2 Guthaben, plus weitere $10 für den Beitritt zur offiziellen Gruppe — bis zu $12 Gratis-Guthaben!</td>
</tr>
<tr>
<td width="180"><a href="https://api.fenno.ai/register?redirect=/purchase?tab=subscription%26group=16&aff=P9MR3D3PLCNL"><img src="assets/partners/logos/fenno-banner.png" alt="Fenno.ai" width="150"></a></td>
<td>Danke an Fenno.ai für die Unterstützung dieses Projekts! Fenno.ai ist ein stabiler und effizienter API-Relay-Dienstleister, der sich derzeit hauptsächlich auf Codex-Relay konzentriert. Er ist mit den OpenAI- und Anthropic-Protokollen kompatibel und lässt sich flexibel mit Codex, Claude Code, OpenCode und anderen gängigen Coding-Tools nutzen. Er unterstützt zuverlässig Workloads auf Unternehmensniveau von Hunderten Milliarden Tokens pro Tag und bietet B2B-Abrechnung sowie Rechnungsstellung für Unternehmen im In- und Ausland. Fenno.ai bietet einen exklusiven Vorteil für CC-Switch-Nutzer: Abonnieren Sie über <a href="https://api.fenno.ai/register?redirect=/purchase?tab=subscription%26group=16&aff=P9MR3D3PLCNL">diesen Link</a> den unschlagbaren ¥9,9-Coding-Plan im Wert von $150 Guthaben und erhalten Sie bis zu 20% Empfehlungsprämien — je mehr Einladungen, desto mehr Belohnung!</td>
</tr>
<tr>
<td width="180"><a href="https://zetaapi.ai/go/ccs"><img src="assets/partners/logos/zetaapi-banner.png" alt="ZetaAPI" width="150"></a></td>
<td>Danke an ZetaAPI für die Unterstützung dieses Projekts! ZetaAPI legt den Fokus auf echte Modelltreue — keine verwässerten Antworten, keine Qualitätsminderung — und Preise von nur 35 % der offiziellen Tarife. Die Plattform mischt keinen Traffic, ersetzt Modelle nicht heimlich durch minderwertige Alternativen und nutzt kein gefälschtes Modell-Routing. Sie unterstützt Claude Code, Codex, Gemini, ChatGPT und weitere gängige KI-Modelle und hilft Nutzern, die API-Kosten deutlich zu senken und gleichzeitig eine zuverlässige Modellqualität zu gewährleisten. Gleichzeitig bietet ZetaAPI eine SLA-gestützte Stabilität auf Unternehmensniveau, Standard-API-Kompatibilität, einen API-Key für mehrere Modelle, schnelle Integration und nutzungsbasierte Abrechnung — geeignet für KI-Produkte, Coding-Agents, interne Unternehmenstools, Kundenservice-Systeme, Content-Erstellung und Automatisierungs-Workflows. Falls bei einem Modell nachgewiesen wird, dass es nicht der angegebenen Qualität entspricht, sichert ZetaAPI dies mit einer 10-fachen Entschädigungsgarantie ab und bietet Nutzern ein stabileres, transparenteres und vertrauenswürdigeres Erlebnis. Registrieren Sie sich über <a href="https://zetaapi.ai/go/ccs">diesen Link</a> und verwenden Sie bei Ihrer ersten Aufladung den Promo-Code CC-SWITCH, um als CC-Switch-Nutzer einen exklusiven Rabatt von 10 % auf Ihre erste Aufladung zu erhalten!</td>
</tr>
<tr>
<td width="180"><a href="https://nekocode.ai?aff=CCSWITCH"><img src="assets/partners/logos/nekocode-banner.png" alt="NekoCode" width="150"></a></td>
<td>Vielen Dank an <a href="https://nekocode.ai?aff=CCSWITCH">NekoCode</a> für die Unterstützung dieses Projekts! NekoCode bietet Entwicklern einen stabilen, effizienten und zuverlässigen API-Relay-Dienst für Claude, Codex und weitere KI-Modelle. Mit transparenter Preisgestaltung und flexibler nutzungsbasierter Abrechnung bietet es einen einfachen und kostengünstigen Zugang zu KI-Modellen. CC-Switch-Nutzer erhalten einen exklusiven Rabatt von 10 %: Registrieren Sie sich über <a href="https://nekocode.ai?aff=CCSWITCH">diesen Link</a> und geben Sie beim Aufladen den Gutscheincode <code>cc-switch</code> ein, um 10 % Rabatt auf Ihre Aufladung zu erhalten!</td>
</tr>
</table>
</details>
@@ -265,6 +294,19 @@ Fügen Sie einen offiziellen Anbieter aus der Preset-Liste hinzu. Führen Sie na
</details>
<details>
<summary><strong>Linux (Wayland + NVIDIA): Klicks im Webinhalt reagieren nicht, schwarzer Bildschirm beim Größenändern</strong></summary>
Das AppImage erzwingt `GDK_BACKEND=x11` (XWayland), um einen historischen nativen Wayland-Absturz zu vermeiden. Auf neueren Wayland-+-NVIDIA-Systemen kann das dazu führen, dass der Webinhalt nicht anklickbar ist (die Titelleisten-Schaltflächen funktionieren weiterhin) und das Fenster beim Größenändern schwarz wird. Starten Sie mit dem optionalen Notausgang, um zu nativem Wayland zu wechseln:
```bash
CC_SWITCH_GDK_BACKEND=wayland ./CC-Switch-*.AppImage
```
Wenn Sie über ein Desktop-Symbol starten, fügen Sie es der `Exec=`-Zeile der `.desktop`-Datei hinzu (z. B. `env CC_SWITCH_GDK_BACKEND=wayland /pfad/zum/AppImage`) oder setzen Sie es in Ihrer Sitzungsumgebung. Die Variable ist generisch: Auf Tiling-Wayland-Compositors (sway/Hyprland), bei denen Klicks nicht reagieren, versuchen Sie umgekehrt `CC_SWITCH_GDK_BACKEND=x11`. Bleibt sie ungesetzt, bleibt das Standardverhalten erhalten.
</details>
## Dokumentation
Ausführliche Anleitungen zu jeder Funktion finden Sie im **[Benutzerhandbuch](docs/user-manual/en/README.md)** — es deckt Anbieterverwaltung, MCP/Prompts/Skills, Proxy & Failover und mehr ab.
+47 -5
View File
@@ -25,9 +25,11 @@
<details open>
<summary>クリックで折りたたむ</summary>
[![Kimi K2.7 Code](assets/partners/banners/kimi-banner-en.png)](https://platform.moonshot.cn/console?aff=cc-switch)
[![Kimi K2.7 Code](assets/partners/banners/kimi-banner-en.png)](https://platform.kimi.ai?aff=cc-switch)
Kimi K2.7 Code は Moonshot AI が開発した、コーディングに特化したオープンソースのエージェントモデルです。コーディング能力とエージェント性能が全面的に強化され、実世界の長程コーディングタスクで大幅な向上を実現し、複雑なソフトウェアエンジニアリングのワークフロー全体でエンドツーエンドのタスク成功率を高めます。さらに K2.7 Code は推論効率を改善し、K2.6 と比べて推論トークンの消費を約 30% 削減します。**[ここをクリックして Kimi を体験する](https://platform.moonshot.cn/console?aff=cc-switch)**
Kimi K2.7 Code は Moonshot AI が開発した、コーディングに特化したオープンソースのエージェントモデルです。コーディング能力とエージェント性能が全面的に強化され、実世界の長程コーディングタスクで大幅な向上を実現し、複雑なソフトウェアエンジニアリングのワークフロー全体でエンドツーエンドのタスク成功率を高めます。さらに K2.7 Code は推論効率を改善し、K2.6 と比べて推論トークンの消費を約 30% 削減します。**[ここをクリックして Kimi を体験する](https://platform.kimi.ai?aff=cc-switch)**
コーディング作業がメインですか?コーディングのために作られたサブスクリプション、**[Kimi For Coding プラン](https://www.kimi.com/code/?aff=cc-switch)** をぜひお試しください!
---
@@ -60,14 +62,21 @@ Claude Code / Codex / Gemini 公式チャンネルが最安で元価格の 38% /
現在、<a href="https://pateway.ai/?ch=etzpm8&aff=WB6M6F67#/">このリンク</a>からご登録いただくと $3 のトライアルクレジットを進呈。チャージは最安で元価格の 60%、友達紹介は双方にボーナスが付与され、紹介報酬は最大 $150!</td>
</tr>
<tr>
<td width="180"><a href="https://teamorouter.com/?utm_source=cc_switch&utm_medium=referral&utm_campaign=ai_directory"><img src="assets/partners/logos/TeamoRouter-banner.png" alt="TeamoRouter" width="150"></a></td>
<td>このプロジェクトをご支援いただいている TeamoRouter に感謝します!TeamoRouter は、開発者、AI チーム、企業向けに構築されたエンタープライズグレードの Agentic LLM ゲートウェイです。サブスクリプション不要で、単一の統合 API を通じて Claude Code、Codex、Gemini CLI、OpenAI Codex、その他の人気 AI エージェントにアクセスでき、API 料金は最大 90% オフで利用できます。
一般的な API リレーサービスとは異なり、TeamoRouter は OpenAI、Anthropic、Vertex、Azure、AWS Bedrock など、数百の公式モデルプロバイダーと信頼できるインフラパートナーを集約しています。各プロバイダーは、Agent プロトコルとの 100% 互換性、キャッシュ性能、リクエストの追跡可能性について検証されており、リバースエンジニアリングされたものや品質が薄められたエンドポイントではなく、安定した品質を提供します。プラットフォームは公式に近い TTFT、99.6% の SLA、最大 5,000 QPM のエンタープライズ規模のスループット、そして業界トップクラスのキャッシュヒット率を提供し、長時間稼働するエージェントワークフローのトークンコストを大幅に削減します。
TeamoRouter は、集中請求、チーム管理、BYOK、スマートルーティング、利用状況分析、動的なプロバイダー最適化、専任サポートなどのエンタープライズ機能も提供しています。さらにシンプルな体験を求める場合は、Teamo Desktop を使うことで、Claude Code、Codex、Gemini CLI、その他の人気 AI エージェントをワンクリックで利用できます。API キー管理や手動のゲートウェイ設定は不要です。新規ユーザーとして<a href="https://teamorouter.com/?utm_source=cc_switch&utm_medium=referral&utm_campaign=ai_directory">こちらのリンク</a>から登録すると、初回チャージが 10% オフになります。</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 トークンの無料推論クォータを進呈します。<a href="https://www.volcengine.com/activity/ai618?utm_campaign=hw&utm_content=hw&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>SiliconFlow のご支援に感謝します!SiliconFlow は高性能 AI インフラストラクチャおよびモデル API プラットフォームで、言語・音声・画像・動画モデルへの高速かつ信頼性の高いアクセスをワンストップで提供します。従量課金制、豊富なマルチモーダルモデル対応、高速推論、エンタープライズグレードの安定性を備え、開発者やチームがより効率的に AI アプリケーションを構築・拡張できるようサポートします。<a href="https://cloud.siliconflow.cn/i/drGuwc9k">このリンク</a>から登録し、本人確認を完了すると、プラットフォーム内の全モデルで利用可能な ¥16 のボーナスクレジットが付与されます。SiliconFlow は OpenClaw にも対応しており、SiliconFlow の API キーを接続することで主要な AI モデルを無料で呼び出すことができます。</td>
<td width="180"><a href="https://cloud.siliconflow.cn/i/YflgU2Ve"><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/YflgU2Ve">このリンク</a>から登録し、本人確認を完了すると、プラットフォーム内の全モデルで利用可能な ¥16 のボーナスクレジットが付与されます。SiliconFlow は OpenClaw にも対応しており、SiliconFlow の API キーを接続することで主要な AI モデルを無料で呼び出すことができます。</td>
</tr>
<tr>
@@ -115,6 +124,11 @@ Claude Code / Codex / Gemini 公式チャンネルが最安で元価格の 38% /
<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://code0.ai?source=ccswitch"><img src="assets/partners/logos/code0.png" alt="code0.ai" width="150"></a></td>
<td>本プロジェクトをご支援いただいている <a href="https://code0.ai?source=ccswitch">code0.ai</a> に感謝します!code0.ai は開発者向けの AI コーディングサービスプラットフォームで、Claude Code、Codex、Gemini などの主要な AI コーディング機能に対応しています。個人開発者やチームが、コーディング、デバッグ、リファクタリング、自動化ワークフローで AI Agent をより安定かつ効率的に活用できるよう支援します。ccswitch ユーザーは <a href="https://code0.ai?source=ccswitch">code0.ai 公式サイト</a> からカスタマーサポートに連絡することで、テストクレジットを受け取り、信頼性の高い AI コーディングサービスを体験できます。</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>
@@ -150,6 +164,21 @@ Claude Code / Codex / Gemini 公式チャンネルが最安で元価格の 38% /
<td>Unity2.ai のご支援に感謝します!Unity2.ai は個人開発者・チーム・企業向けの高性能 AI モデル API リレープラットフォームです。中国の大手企業に長年利用されており、1 日 300 億トークン以上を処理し、5000 RPM クラスの高並列に対応しています。残高課金、初回チャージボーナス、組み合わせサブスクリプション、企業向け請求書発行、専任サポートを提供。<a href="https://unity2.ai/register?source=ccs">こちらのリンク</a>から登録すると $2 のクレジット、公式グループへの参加でさらに $10、最大 $12 の無料クレジットがもらえます!</td>
</tr>
<tr>
<td width="180"><a href="https://api.fenno.ai/register?redirect=/purchase?tab=subscription%26group=16&aff=P9MR3D3PLCNL"><img src="assets/partners/logos/fenno-banner.png" alt="Fenno.ai" width="150"></a></td>
<td>Fenno.ai のご支援に感謝します!Fenno.ai は安定かつ高効率な API 中継サービスプロバイダーで、現在は主に Codex の中継を提供しています。OpenAI および Anthropic プロトコルに対応し、Codex・Claude Code・OpenCode などの主要なコーディングツールから柔軟に利用できます。1 日あたり数千億トークンというエンタープライズ級の呼び出し需要を安定して支え、国内外の法人による B2B 決済・請求書発行に対応しています。Fenno.ai は CC Switch 利用者限定の特典を用意しています:<a href="https://api.fenno.ai/register?redirect=/purchase?tab=subscription%26group=16&aff=P9MR3D3PLCNL">こちらのリンク</a>から 9.9 元(150 ドル相当)のお得な Coding Plan を購入でき、友達紹介で最大 20% の特典がもらえます。紹介すればするほどお得です!</td>
</tr>
<tr>
<td width="180"><a href="https://zetaapi.ai/go/ccs"><img src="assets/partners/logos/zetaapi-banner.png" alt="ZetaAPI" width="150"></a></td>
<td>本プロジェクトをご支援いただいている ZetaAPI に感謝します!ZetaAPI は、モデル品質の忠実性、水増しなし、性能劣化なし、公式価格の 35% から利用できる低価格を主な特徴としています。プラットフォームはトラフィックの混在、低品質モデルへの密かな切り替え、虚偽のモデルルーティングを行わず、Claude Code、Codex、Gemini、ChatGPT などの主要 AI モデルに対応しており、モデル品質を維持しながら API 利用コストを大幅に削減できます。同時に、ZetaAPI はエンタープライズ級の SLA 安定性保証、標準 API 互換、1つの Key による複数モデル接続、迅速な導入、従量課金などの機能を提供し、AI プロダクト、コード生成、企業内ツール、カスタマーサポート、コンテンツ生成、自動化ワークフローなどの用途に適しています。万が一、モデル品質が表記内容と一致しないことが確認された場合、ZetaAPI は 10 倍補償保証を提供し、ユーザーがより安定して、透明性高く、安心して利用できる環境を実現します。<a href="https://zetaapi.ai/go/ccs">こちらのリンク</a>から登録し、初回チャージ時にプロモコード CC-SWITCH を使用すると、CC Switch ユーザー限定の初回チャージ 10% オフ特典をご利用いただけます!</td>
</tr>
<tr>
<td width="180"><a href="https://nekocode.ai?aff=CCSWITCH"><img src="assets/partners/logos/nekocode-banner.png" alt="NekoCode" width="150"></a></td>
<td>本プロジェクトをご支援いただいている <a href="https://nekocode.ai?aff=CCSWITCH">NekoCode</a> に感謝します!NekoCode は、Claude や Codex などの AI モデルに対応した、安定性・効率性・信頼性に優れた API 中継サービスを提供しています。料金体系は明瞭で、柔軟な従量課金にも対応しています。CC Switch ユーザー限定の 10%オフ特典:<a href="https://nekocode.ai?aff=CCSWITCH">こちらのリンク</a> から登録し、チャージ時にクーポンコード <code>cc-switch</code> を入力すると、チャージが 10%オフになります!</td>
</tr>
</table>
</details>
@@ -229,7 +258,7 @@ CC Switch は **Claude Code**、**Claude Desktop**、**Codex**、**Gemini CLI**
<details>
<summary><strong>プロバイダを切り替えた後、プラグイン設定が消えてしまいました。どうすればよいですか?</strong></summary>
CC Switch には「共有設定スニペット」機能があり、APIキーやエンドポイント以外の共通データをプロバイダ間で引き継ぐことができます。「プロバイダ編集」→「共有設定パネル」→「現在のプロバイダから抽出」をクリックして、すべての共通データを保存してください。新しいプロバイダを作成する際に「共有設定を書き込む」にチェック(デフォルトで有効)を入れれば、プラグインなどのデータが新しいプロバイダ設定に含まれます。すべての設定項目は、アプリ初回起動時にインポートされたデフォルトプロバイダに保存されており、失われることはありません。
CC Switch には「共有設定スニペット」機能があり、APIキーやエンドポイント以外の共通データをプロバイダ間で引き継ぐことができます。「プロバイダ編集」→「共有設定パネル」→「現在のプロバイダから抽出」をクリックして、すべての共通データを保存してください。新しいプロバイダを作成する際に「共有設定を適用」にチェック(デフォルトで有効)を入れれば、プラグインなどのデータが新しいプロバイダ設定に含まれます。すべての設定項目は、アプリ初回起動時にインポートされたデフォルトプロバイダに保存されており、失われることはありません。
</details>
@@ -265,6 +294,19 @@ CC Switch は「最小限の介入」という設計原則に従っています
</details>
<details>
<summary><strong>LinuxWayland + NVIDIA):Web コンテンツがクリックできない・リサイズで黒画面になる</strong></summary>
AppImage は過去のネイティブ Wayland クラッシュを避けるため `GDK_BACKEND=x11`(XWayland)を強制します。新しい Wayland + NVIDIA 環境ではこれが原因で Web コンテンツ領域がクリックできなくなり(タイトルバーのボタンは動作します)、リサイズ時に黒画面になることがあります。内蔵のエスケープハッチでネイティブ Wayland に戻せます:
```bash
CC_SWITCH_GDK_BACKEND=wayland ./CC-Switch-*.AppImage
```
デスクトップアイコンから起動する場合は、`.desktop``Exec=` 行に追記するか(例:`env CC_SWITCH_GDK_BACKEND=wayland /path/to/AppImage`)、セッション環境で設定してください。この変数は汎用です:タイル型 Wayland コンポジタ(sway/Hyprland)でクリックが効かない場合は、逆に `CC_SWITCH_GDK_BACKEND=x11` を試してください。未設定の場合は既定の動作のままです。
</details>
## ドキュメント
各機能の詳しい使い方については、**[ユーザーマニュアル](docs/user-manual/ja/README.md)** をご覧ください。プロバイダ管理、MCP/Prompts/Skills、プロキシとフェイルオーバーなど、すべての機能を網羅しています。
+47 -5
View File
@@ -25,9 +25,11 @@
<details open>
<summary>点击折叠</summary>
[![Kimi K2.7 Code](assets/partners/banners/kimi-banner-zh.png)](https://platform.moonshot.cn/console?aff=cc-switch)
[![Kimi K2.7 Code](assets/partners/banners/kimi-banner-zh.png)](https://platform.kimi.com?aff=cc-switch)
Kimi K2.7 Code 是 Moonshot AI 开发的编程专用开源智能体模型。它在编程与智能体执行能力上全面增强,在真实长程编程任务中实现显著提升,带来复杂软件工程工作流中更高的端到端任务成功率。同时,K2.7 Code 优化了推理效率,相较 K2.6 平均减少约 30% 的推理 token 消耗。**[点击此处开启 Kimi 使用体验](https://platform.moonshot.cn/console?aff=cc-switch)**
Kimi K2.7 Code 是 Moonshot AI 开发的编程专用开源智能体模型。它在编程与智能体执行能力上全面增强,在真实长程编程任务中实现显著提升,带来复杂软件工程工作流中更高的端到端任务成功率。同时,K2.7 Code 优化了推理效率,相较 K2.6 平均减少约 30% 的推理 token 消耗。**[点击此处开启 Kimi 使用体验](https://platform.kimi.com?aff=cc-switch)**
主要做编程工作?可以试试 **[Kimi For Coding 编程套餐](https://www.kimi.com/code/?aff=cc-switch)**,专为编程打造的订阅服务!
---
@@ -60,14 +62,21 @@ Claude Code / Codex / Gemini 官方渠道低至 3.8 / 0.2 / 0.9 折,充值更
现在通过<a href="https://pateway.ai/?ch=etzpm8&aff=WB6M6F67#/">此链接</a>注册即送 $3 试用额度,用户充值低至 6 折,邀请好友双向赠送,邀请奖励可达 $150!</td>
</tr>
<tr>
<td width="180"><a href="https://teamorouter.com/zh?utm_source=cc_switch&utm_medium=referral&utm_campaign=ai_directory"><img src="assets/partners/logos/TeamoRouter-banner.png" alt="TeamoRouter" width="150"></a></td>
<td>感谢 TeamoRouter 赞助本项目!TeamoRouter 是一款面向开发者、AI 团队和企业的企业级 Agentic LLM 网关。无需任何订阅,你就可以通过一个统一 API 访问 Claude Code、Codex、Gemini CLI、OpenAI Codex 以及其他热门 AI Agent,同时享受最高可达 90% 折扣的 API 价格。
不同于常见的 API 中转服务,TeamoRouter 聚合了数百家官方模型提供商和可信基础设施合作伙伴,包括 OpenAI、Anthropic、Vertex、Azure 和 AWS Bedrock。每个提供商都经过验证,确保 100% 兼容 Agent 协议,并具备可靠的缓存性能和请求可追踪性,从而提供稳定质量,而不是反向工程或缩水后的接口。平台提供接近官方水平的 TTFT、99.6% SLA、最高 5,000 QPM 的企业级吞吐量,以及行业领先的缓存命中率,可大幅降低长时间运行的 Agent 工作流中的 token 成本。
TeamoRouter 还提供企业级功能,包括集中账单、团队管理、BYOK、智能路由、用量分析、动态提供商优化和专属支持。为了获得更简单的使用体验,Teamo Desktop 支持你一键使用 Claude Code、Codex、Gemini CLI 和其他热门 AI Agent,无需管理 API Key,也无需手动配置网关。新用户通过<a href="https://teamorouter.com/zh?utm_source=cc_switch&utm_medium=referral&utm_campaign=ai_directory">此链接</a>注册,首次充值可享受 10% 折扣。</td>
</tr>
<tr>
<td width="180"><a href="https://www.volcengine.com/activity/ai618?utm_campaign=hw&utm_content=hw&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 级模型,覆盖文本、代码、图像、视频等多模态任务。最新支持 MiniMax-M3、DeepSeek-V4 系列、GLM-5.1、Doubao-Seed-2.0 系列、Kimi-K2.6 等模型,工具不限。超全模态模型与 Harness 升级一步到位,深度支持 Agent 框架与 AI 编程工具。一次订阅,可以为不同任务切换合适的 AI 引擎。方舟 Coding Plan 为 CC Switch 的用户提供了专属福利:通过<a href="https://www.volcengine.com/activity/ai618?utm_campaign=hw&utm_content=hw&utm_medium=devrel_tool_web&utm_source=OWO&utm_term=ccswitch">此链接</a>订阅方舟 Coding Plan,新客户首两个月享 2.5 折优惠,再用专属邀请码 6J6FV5N2 领取奖励叠加 9.5 折,低至 9.4 元/月!<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>
<td width="180"><a href="https://cloud.siliconflow.cn/i/YflgU2Ve"><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/YflgU2Ve">此链接</a>注册并完成实名认证,即可获得 ¥16 奖励金,可在平台内跨模型使用。硅基流动现已兼容 OpenClaw,用户可接入硅基流动 API Key 免费调用主流 AI 模型。</td>
</tr>
<tr>
@@ -116,6 +125,11 @@ Claude Code / Codex / Gemini 官方渠道低至 3.8 / 0.2 / 0.9 折,充值更
<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://code0.ai?source=ccswitch"><img src="assets/partners/logos/code0.png" alt="code0.ai" width="150"></a></td>
<td>感谢 <a href="https://code0.ai?source=ccswitch">code0.ai</a> 赞助本项目!code0.ai 是专为开发者打造的 AI 编程服务平台,支持 Claude Code、Codex、Gemini 等主流 AI 编程能力,帮助个人开发者和团队更稳定、更高效地使用 AI Agent 完成代码开发、调试与自动化任务。ccswitch 用户可通过 <a href="https://code0.ai?source=ccswitch">code0.ai 官网</a> 联系客服领取测试额度,体验高效稳定的 AI 编程服务!</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>
@@ -151,6 +165,21 @@ Claude Code / Codex / Gemini 官方渠道低至 3.8 / 0.2 / 0.9 折,充值更
<td>感谢 Unity2.ai 赞助了本项目!Unity2.ai 是面向个人开发者、团队和企业的高性能 AI 模型 API 中转平台,长期服务国内头部企业,日均承载超 300 亿 token 调用,支持 5000 RPM 级高并发。支持余额计费、首充赠额、组合订阅、企业开票和专属对接。通过<a href="https://unity2.ai/register?source=ccs">此链接</a>注册可领取 $2 余额,加入官方群再送 $10 余额,最高可领 $12 免费额度!</td>
</tr>
<tr>
<td width="180"><a href="https://api.fenno.ai/register?redirect=/purchase?tab=subscription%26group=16&aff=P9MR3D3PLCNL"><img src="assets/partners/logos/fenno-banner.png" alt="Fenno.ai" width="150"></a></td>
<td>感谢 Fenno.ai 赞助了本项目!Fenno.ai 是一家稳定、高效的 API 中转服务商,目前主要提供 Codex 中转服务,兼容 OpenAI 及 Anthropic 协议,可灵活接入 Codex、Claude Code、OpenCode 等主流编程工具,可稳定支撑千亿 Token/日的企业级调用需求,支持国内及海外主体公对公结算、开票。Fenno.ai 为 CC Switch 的用户提供了专属福利:通过<a href="https://api.fenno.ai/register?redirect=/purchase?tab=subscription%26group=16&aff=P9MR3D3PLCNL">此链接</a>即可订阅 9.9 元/150 刀额度的超值 Coding Plan,邀请好友最高可享 20% 奖励,多邀多得!</td>
</tr>
<tr>
<td width="180"><a href="https://zetaapi.ai/go/ccs"><img src="assets/partners/logos/zetaapi-banner.png" alt="ZetaAPI" width="150"></a></td>
<td>感谢 ZetaAPI 赞助本项目!ZetaAPI 主打模型不掺水、保真不降智、价格低至官方价 35 折,平台不混量、不暗中替换低质量模型、不做虚假路由,支持 Claude Code、Codex、Gemini、ChatGPT 等主流模型接入,帮助用户在保证模型质量的同时大幅降低 API 使用成本。同时,ZetaAPI 提供企业级 SLA 稳定性保障、标准接口兼容、一个 Key 接入多模型、快速集成、按量计费等能力,适用于 AI 产品、代码生成、企业内部工具、客服系统、内容生产和自动化流程等场景。若经验证发现模型质量与标称不符,ZetaAPI 承诺假一赔十,让用户用得更稳定、更透明、更放心。通过<a href="https://zetaapi.ai/go/ccs">此链接</a>注册,并在首次充值时使用优惠码 CC-SWITCH,即可享受 CC Switch 用户专属的首次充值九折优惠!</td>
</tr>
<tr>
<td width="180"><a href="https://nekocode.ai?aff=CCSWITCH"><img src="assets/partners/logos/nekocode-banner.png" alt="NekoCode" width="150"></a></td>
<td>感谢 <a href="https://nekocode.ai?aff=CCSWITCH">NekoCode</a> 赞助本项目!NekoCode 为开发者提供稳定、高效、可靠的 Claude、Codex 等 AI 模型 API 中转服务,价格透明,接入便捷,支持灵活的按量计费。CC Switch 用户专享 9 折福利:通过 <a href="https://nekocode.ai?aff=CCSWITCH">此链接</a> 注册,并在充值时输入优惠码 <code>cc-switch</code>,即可享受充值 9 折优惠!</td>
</tr>
</table>
</details>
@@ -230,7 +259,7 @@ CC Switch 支持七个工具:**Claude Code**、**Claude Desktop**、**Codex**
<details>
<summary><strong>切换供应商之后我的插件配置怎么不见了?</strong></summary>
CC Switch 使用“通用配置片段”功能,在不同的供应商之间传递 Key 和请求地址之外的通用数据,您可以在“编辑供应商”菜单的“通用配置面板”里,点击“从当前供应商提取”,把所有的通用数据提取到通用配置中,之后在新建“供应商”的时候,只要勾选“写入通用配置”(默认勾选),就会把插件等数据写入到新的供应商配置中。您的所有配置项都会保存在运行本软件的时候,第一次导入的默认供应商里面,不会丢失。
CC Switch 使用“通用配置片段”功能,在不同的供应商之间传递 Key 和请求地址之外的通用数据,您可以在“编辑供应商”菜单的“通用配置面板”里,点击“从当前供应商提取”,把所有的通用数据提取到通用配置中,之后在新建“供应商”的时候,只要勾选“应用通用配置”(默认勾选),就会把插件等数据写入到新的供应商配置中。您的所有配置项都会保存在运行本软件的时候,第一次导入的默认供应商里面,不会丢失。
</details>
@@ -268,6 +297,19 @@ CC Switch macOS 版本已通过 Apple 代码签名和公证,可直接下载安
</details>
<details>
<summary><strong>LinuxWayland + NVIDIA):网页内容点不动、缩放后黑屏</strong></summary>
AppImage 会强制 `GDK_BACKEND=x11`(走 XWayland)以规避历史上的原生 Wayland 崩溃。但在较新的 Wayland + NVIDIA 环境下,这会导致网页内容区点不动(标题栏按钮仍可点)、窗口缩放后黑屏。可用内置的逃生开关切回原生 Wayland:
```bash
CC_SWITCH_GDK_BACKEND=wayland ./CC-Switch-*.AppImage
```
如果你是从桌面图标启动的,请把它写进 `.desktop``Exec=` 行(如 `env CC_SWITCH_GDK_BACKEND=wayland /path/to/AppImage`),或在会话环境中设置。该变量是通用的:在 tiling Wayland 合成器(sway/Hyprland)下若出现点击失效,可反过来设 `CC_SWITCH_GDK_BACKEND=x11`。不设置则保持默认行为。
</details>
## 文档
如需了解各项功能的详细使用方法,请查阅 **[用户手册](docs/user-manual/zh/README.md)** — 涵盖供应商管理、MCP/Prompts/Skills、代理与故障转移等全部功能。
Binary file not shown.

After

Width:  |  Height:  |  Size: 5.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 137 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 424 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

+3
View File
@@ -1,5 +1,8 @@
# CC Switch v3.16.4
> 🎉 **CC Switch is now in the global top 100 on GitHub by stars!**
> Thank you to every user, contributor, and Star — you brought it here. 🙏
> After v3.16.3 made usage billing accurate, this release shifts the focus to polishing the Codex proxy chain and enriching the usage / pricing tooling — migrating Chinese providers to native Responses, decoupling the upstream-format selector from model mapping, decompressing zstd request / error bodies, and a batch of tool-call and OAuth-through-proxy fixes — while also adding local proxy request overrides, an in-app recovery screen when the database version is too new, native Windows ARM64 builds, and a wave of preset and branding updates (SubRouter, OpenCode Go, the CTok→ETok rename, the Kimi brand refresh, and a prime-partner badge).
**[中文版 →](v3.16.4-zh.md) | [日本語版 →](v3.16.4-ja.md)**
+3
View File
@@ -1,5 +1,8 @@
# CC Switch v3.16.4
> 🎉 **CC Switch が GitHub の全世界 Star ランキングでトップ 100 入り!**
> ここまで支えてくださったすべてのユーザー、コントリビューター、そして Star に感謝します。🙏
> v3.16.3 で「使用量の課金を正確にする」ことに取り組んだのに続き、本リリースは Codex プロキシ経路の磨き込みと、使用量 / 価格ツールの拡充に重きを置いています——国産プロバイダーのネイティブ Responses への移行、上流の形式セレクタとモデルマッピングの分離、zstd リクエスト / エラーボディの展開、そしてツール呼び出しと OAuth がプロキシを経由するようにする一連の修正です。あわせて、ローカルプロキシのリクエストオーバーライド、データベースのバージョンが新しすぎる場合のアプリ内リカバリ画面、ネイティブ Windows ARM64 ビルドを新設し、一連のプリセットとブランドの更新(SubRouter、OpenCode Go、CTok→ETok の改名、Kimi のブランド刷新と prime-partner バッジ)を届けます。
**[English →](v3.16.4-en.md) | [中文版 →](v3.16.4-zh.md)**
+3
View File
@@ -1,5 +1,8 @@
# CC Switch v3.16.4
> 🎉 **CC Switch 跻身 GitHub 全球 Star 排行榜前 100**
> 感谢每一位用户、贡献者与 Star —— 是你们让它走到这里。🙏
> 继 v3.16.3 把「用量计费做准」之后,这一版把重心放在打磨 Codex 代理链路与丰富用量 / 定价工具上——国产供应商原生 Responses 迁移、上游格式选择器与模型映射解耦、zstd 请求 / 错误体解压,以及一批工具调用与 OAuth 走代理的修复;同时新增本地代理请求覆盖、数据库版本过新时的应用内恢复屏、原生 Windows ARM64 构建,并带来一波预设与品牌更新(SubRouter、OpenCode Go、CTok→ETok 改名、Kimi 品牌刷新与 prime-partner 徽标)。
**[English →](v3.16.4-en.md) | [日本語版 →](v3.16.4-ja.md)**
+278
View File
@@ -0,0 +1,278 @@
# CC Switch v3.16.5
> The centerpiece of this release is **getting native-Responses direct-connect properly adapted for domestic (Chinese) model providers** — generating Codex model catalogs for providers with native Responses endpoints (Xiaomi MiMo, Volcengine Doubao, Qwen3-Coder, Meituan LongCat, MiniMax) so the Codex desktop app can actually see these models and their built-in tools work, and automatically disabling `web_search` for the few domestic gateways that reject it so requests are no longer hard-rejected. Two other important improvements: when you switch providers, the plugins, environment variables, etc. you added inside the app are **automatically written back to the common config and carried over to the next provider**; and Linux (Wayland + NVIDIA) users hitting the "title bar clicks, page is dead, black screen on resize" problem now have an environment-variable escape hatch. This release also brings Claude Sonnet 5 pricing and a default-tier bump, a two-level grouped session view, and a batch of credential-safety and platform-compatibility fixes.
**[中文版 →](v3.16.5-zh.md) | [日本語版 →](v3.16.5-ja.md)**
---
## Usage Guides
The new capabilities in this release land mainly in the Codex provider form, the session panel, and usage / common config. The following docs are worth reading alongside it:
- **[Can't see custom models in the Codex desktop app?](../guides/codex-desktop-custom-model-visibility-en.md)**: this release reworks **model-catalog generation for native direct-connect** — when a Codex provider connects directly via native Responses (`openai_responses`), CC Switch generates `~/.codex/cc-switch-model-catalog.json` so the Codex desktop app can display the configured custom models and their tools work. If you previously configured a native Codex provider, **re-save it once** to regenerate the catalog (see "Upgrade Notes" below).
- **[Usage Statistics](../user-manual/en/4-proxy/4.4-usage.md)**: understand the Usage Dashboard's data sources and how the statistics are counted. This release adds Claude Sonnet 5 pricing and fixes usage-script credentials being persisted as "explicit overrides".
- **[Settings](../user-manual/en/1-getting-started/1.5-settings.md)**: the Codex upstream-format selector and local routing toggle, and Claude's common config (now renamed "Apply Common Config" and auto-synced on switch) all live in the provider form's advanced options.
---
> [!WARNING]
>
> ## Only Official Channels (Please Read)
>
> CC Switch is a **fully free and open-source** desktop app, and we **do not charge users any fees**. Please only obtain the software through the official channels listed below:
>
> | Channel | Only Official |
> | ------------------ | ------------------------------------------------------------------------------ |
> | Website | **[ccswitch.io](https://ccswitch.io)** |
> | Source | **[github.com/farion1231/cc-switch](https://github.com/farion1231/cc-switch)** |
> | Downloads | **[GitHub Releases](https://github.com/farion1231/cc-switch/releases)** |
> | Author | **[@farion1231](https://github.com/farion1231)** |
> | Report an Imposter | **[GitHub Issues](https://github.com/farion1231/cc-switch/issues)** |
>
> **Any "CC Switch" website or client that asks you for payment, top-ups, or login credentials is fake.** If you have been tricked into paying, stop the transaction immediately and file a report through GitHub Issues.
---
## Overview
CC Switch v3.16.5 is a maintenance update following v3.16.4, centered on **making native Codex direct-connect work end-to-end for domestic model providers**. v3.16.4 already switched Qwen / DashScope, Xiaomi MiMo, Volcengine Doubao, Meituan LongCat, and MiniMax to native Responses endpoints; this release goes a step further and generates the **model catalog** Codex needs (`~/.codex/cc-switch-model-catalog.json`), so the Codex desktop app can really see these custom models and invoke their built-in tools, and it fully decouples model mapping from the "local routing" toggle. For the few domestic gateways whose first-party models don't support OpenAI's built-in `web_search` (MiMo, LongCat, MiniMax, Qwen3-Coder), this release also disables that tool automatically, so Codex's default doesn't trigger a hard 400.
For everyday use, this release makes Claude's **common config auto-sync and carry over when you switch providers** — the plugins, environment variables, theme, etc. you added inside the running app are first written back to the common config and then handed to the next provider, so they're not lost on switch; it adds an escape-hatch environment variable for Linux (Wayland + NVIDIA) users hitting click-dead / black-screen issues; it adds Claude Sonnet 5 pricing and bumps the default Sonnet tier to it; it brings a two-level "provider → project directory" grouped session view; and it fixes a string of credential-safety (the common-config snippet now strips all secrets, usage-script credentials are only kept as explicit overrides), platform-compatibility (Hermes Windows config directory, Windows Codex npm shims), and UI (long dropdown scrolling, narrow-window date-range picker) issues. It also adds several new provider presets, ready to pick out of the box.
**Release date**: 2026-07-01
**Stats**: 36 commits | 93 files changed | +5,678 / -2,804 lines
---
## Highlights
- **Native Codex direct-connect actually works for domestic model providers**: CC Switch generates a Codex model catalog (`~/.codex/cc-switch-model-catalog.json`) for domestic providers like Xiaomi MiMo, Volcengine Doubao, Qwen3-Coder, Meituan LongCat, and MiniMax, so the Codex desktop app can see these models and their built-in tools work; and it auto-disables `web_search` for the domestic gateways that reject it (MiMo, LongCat, MiniMax, Qwen3-Coder) to avoid hard 400s. **Existing native providers need a one-time re-save** to regenerate the catalog.
- **Common config auto-synced and carried over on switch**: when you switch away from a Claude provider that has the common config enabled, the plugins, environment variables, theme, and hooks you added inside the app are first written back to the common config and then handed to the next provider — no longer overwritten and lost on switch.
- **Escape hatch for Linux Wayland click-dead / black-screen**: when you hit "title bar clicks, page doesn't, black screen on resize" on Wayland + NVIDIA, launch with `CC_SWITCH_GDK_BACKEND=wayland` to switch back to native Wayland (set it to `x11` for the inverse problem on tiling compositors).
- **Claude Sonnet 5**: adds Sonnet 5 pricing and bumps each preset's default Sonnet tier to `claude-sonnet-5`.
- **Categorized session view with grouping**: the session panel gains a two-level "provider → project directory" grouped view, with a tri-state checkbox on each group header for one-click batch selection.
- **New provider presets**: adds Qiniu, FennoAI, ZetaAPI, TeamoRouter, NekoCode, Code0.ai, and Amux presets across the managed apps, ready to pick out of the box.
---
## Added
### Native Codex Direct-Connect for Domestic Model Providers (Generated Model Catalog)
This release makes native Codex direct-connect work for domestic providers. After v3.16.4 switched Xiaomi MiMo, Volcengine Doubao, Qwen3-Coder, Meituan LongCat, and MiniMax to native Responses (`apiFormat: "openai_responses"`), this release reverses the then-current "drop the model catalog on native direct-connect" approach: when these providers connect directly without the local proxy, CC Switch generates `~/.codex/cc-switch-model-catalog.json` for them, so the Codex desktop app really shows these custom models and their built-in tools work — without triggering the freeform `apply_patch` (`type=custom`) tool that native gateways like MiMo reject (editing falls back to `shell_command`). Catalog generation is keyed on `apiFormat` and decoupled from the "local routing" toggle, so a native provider persists a catalog without turning on local route mapping, while `openai_chat` keeps the existing Responses↔Chat proxy conversion unchanged. Because Codex's parser requires `base_instructions` on every entry, the native template carries a neutral default that each vendor's official copy overrides (MiMo, MiniMax). **Existing native providers need a one-time re-save to generate a valid catalog** (no database migration).
Alongside that, for the few domestic gateways whose first-party models don't support OpenAI's built-in `web_search` tool (MiMo, LongCat, MiniMax, Qwen3-Coder), this release auto-disables the tool on switch, so Codex's default doesn't include it and get hard-rejected with a 400 (see "Fixed" below).
### Categorized Session View with Grouping
The Session Manager panel gains a grouped view alongside the existing flat list, toggled via a List / ListTree selector in the toolbar, with view mode and expansion state persisted to `localStorage`. Grouping builds a two-level "provider → project directory" hierarchy: sessions are grouped by project directory name, and sessions with no project directory fall into an "unknown directory" bucket. Both levels are collapsible sections, with a "collapse all" button; in batch mode, each group header shows a tri-state checkbox that selects / deselects every selectable session in that group at once, along with a selected / selectable count badge. Copy across all four locales (zh / en / ja / zh-TW) is in sync. The change is entirely front-end, with no backend commands or data-access changes. ([#4776](https://github.com/farion1231/cc-switch/pull/4776))
### Claude Sonnet 5 Model Pricing
Added a `claude-sonnet-5` pricing row in `schema.rs` at Anthropic list pricing — \$3 / \$15 per million input / output tokens and \$0.30 / \$3.75 cache read / write, matching Sonnet 4.6. The introductory \$2 / \$10 promotion (valid through 2026-08-31) is deliberately not seeded, so accounting reflects steady-state list pricing rather than a temporary discount. The row is applied on the app's next start via `ensure_model_pricing_seeded`, with no `SCHEMA_VERSION` bump.
### New Provider Presets
This release adds a batch of provider presets; pick one and fill in your own API key to use it:
- **Qiniu**: covers all seven managed apps (including Gemini), relaying native Claude / GPT / Gemini.
- **FennoAI / ZetaAPI / TeamoRouter / NekoCode**: each covers six apps (Claude, Claude Desktop, Codex, OpenCode, OpenClaw, Hermes).
- **Code0.ai**: covers all seven apps (including Gemini).
- **Amux**: covers six apps.
Each preset's endpoints and default models are configured for the corresponding app — the Claude family connects directly to an Anthropic-compatible host, Codex uses native Responses, and the rest use the OpenAI-compatible `/v1`.
---
## Changed
### Common Config Auto-Synced and Carried Over on Provider Switch
This is a very practical change in this release: when you switch away from a Claude provider that has the common config enabled, the service first **re-extracts the shareable portion from its live `settings.json` and updates the common config**, then hands it to the next provider, instead of only writing one way. As a result, the plugins (`enabledPlugins`), hooks, environment variables (`env`), theme (`theme`), and other shared config you added directly in the running app are not silently lost on switch, but automatically follow along to the next provider; deletions sync too (a removed key is not re-injected). This sync is strictly scoped to Claude providers that have the common config enabled, is skipped when it was explicitly cleared, and all failures are non-fatal (warn only) and never block the switch.
### Codex Model Mapping Decoupled from the "Local Routing" Toggle
The Codex provider form aligns with Claude Code — the model-mapping catalog is now independent of route takeover, because native Responses providers (MiMo, Doubao, MiniMax) need it for proxy-less direct-connect, while Chat providers go through the proxy regardless. The "needs local routing" toggle is removed (it had no backend field and only gated catalog / reasoning persistence, which is equivalent to whether the mapping is filled in). Model mapping is now always shown for non-official providers and persisted whenever it's non-empty, while reasoning visibility / persistence is gated on the Chat format instead. Copy across all four locales (zh / en / ja / zh-TW) was rewritten accordingly. As a side fix, `useCodexConfigState` no longer drops `supportsParallelToolCalls` / `inputModalities` / `baseInstructions` when loading a saved provider (which used to silently lose parallel tools, image input, and the official base instructions on edit).
### Default Sonnet Tier Bumped to Claude Sonnet 5
Bumped the default Sonnet tier in each provider preset from `claude-sonnet-4-6` to `claude-sonnet-5` (across the claude / claude-desktop / hermes / openclaw / opencode presets and the universal `NEWAPI_DEFAULT_MODELS`), covering keys like `ANTHROPIC_MODEL` / `ANTHROPIC_DEFAULT_SONNET_MODEL` / `ANTHROPIC_DEFAULT_OPUS_MODEL` and their prefixed variants. Claude Desktop's default-route sonnet `route_id` also migrates to `claude-sonnet-5`. Non-Anthropic pins (gpt / gemini / glm / sonnet-4-5) are left unchanged.
### Doubao Dated Model Id and Pricing Normalization
The Doubao (DouBaoSeed) preset's model id switches to the dated form `doubao-seed-2-1-pro-260628` (across all apps), because Volcengine Ark rejects the bare `doubao-seed-2-1-pro` with a 404 and only accepts the full dated id. Since real usage now carries a date suffix, `strip_model_date_suffix` was extended to also strip Volcengine's 6-digit YYMMDD form (validating month 01-12 and day 01-31 to avoid eating non-date version suffixes like `-123456`), so it normalizes back to hit the bare-name seed row in the pricing table, fixing the \$0-cost display for Doubao models.
### "Write Common Config" Renamed to "Apply Common Config"
The original label "Write Common Config" was ambiguous about data-flow direction (it read like "write the current config into the common config"), whereas the actual behavior is the reverse — it merges the saved common-config snippet into this provider's config. The checkbox is renamed to "Apply Common Config" across all four locales (zh / en / ja / zh-TW), including every hint / guide / notice reference, with the Japanese user manual and `README_JA.md` synced too. ([#4829](https://github.com/farion1231/cc-switch/pull/4829))
### Other Preset and Asset Adjustments
- **OpenClaw Doubao context aligned to 262144**: OpenClaw's DouBaoSeed preset previously hardcoded 128000 while the Codex side used 262144 for the same model, giving OpenClaw users too small a window; the two are now aligned, with a cross-preset consistency test to prevent drift.
- **Volcengine / Doubao / BytePlus website links corrected**: the "visit website" links on these three presets had been mistakenly set to console / signup links, and are restored to clean product homepages.
- **Downscale oversized provider icons to 256px**: a batch of bundled icons were far larger than their ~32px on-screen render size; downscaling significantly reduces their size with no code / filename / import changes (e.g. ZetaAPI 940KB→40KB, relaxcode 1.16MB→42KB), and the never-referenced 1.4MB `dds.svg` orphan was removed.
---
## Fixed
### Disable web_search for Native Codex Gateways That Reject It
Some native `/responses` gateways whose first-party models lack OpenAI's hosted `web_search` tool reject it with "tool type 'web_search' is not supported", and Codex sends the tool by default, producing a hard 400. CC Switch now writes the top-level TOML line `web_search = "disabled"` for those vendors on switch. The scope is a blacklist (default-on): only providers matched by `base_url` host (`xiaomimimo.com`, `longcat.chat`, `minimax.io`, `minimaxi.com`) or by model brand prefix (`mimo`, `longcat`, `minimax`, `qwen3-coder`) are disabled, so relays serving real GPT, Doubao, general Qwen, and any unknown provider keep Codex's default. The `qwen3-coder` prefix only suppresses native `qwen3-coder-plus` (Bailian / DashScope marks built-in tools unsupported for the coder series), while general Qwen sharing the same host stays enabled; matching is on the model axis (stripping any aggregator `vendor/` path segment), so it also catches cases like SiliconFlow fronting a reject vendor's model. A blacklist was chosen over a fuzzy "is this GPT?" whitelist because wrongly keeping `web_search` on fails with a hard 400; an ownership sentinel ensures CC Switch only ever removes a `disabled` value it wrote itself, so existing providers need no re-save and switching back re-enables it. As a side fix, the LongCat-2.0-Preview preset's context window is corrected from 131072 (128K) to the real 1048576 (1M).
### Strip All Credential-Like Keys from the Shared Claude Common-Config Snippet
`extract_claude_common_config` previously only redacted `ANTHROPIC_API_KEY` and `ANTHROPIC_AUTH_TOKEN`, but Claude providers legitimately carry other credentials (`OPENROUTER_API_KEY`, `GOOGLE_API_KEY`, and possibly OpenAI / Gemini / AWS Bedrock / Vertex secrets), which could leak into the shared snippet and then be injected into other providers. Extraction now pattern-matches and strips any credential-shaped env key (`*_API_KEY` / `*_AUTH_TOKEN` / `*secret*` / `*token*`, etc.), while preserving legitimately shareable plural `*_TOKENS` values like `MAX_OUTPUT_TOKENS`. This also closes the same leak on the manual "Extract" and one-time auto-extract paths.
### Usage-Script Credentials Persisted Only as Explicit Overrides
Provider usage scripts store optional `api_key` / `base_url` fields that override the live credentials when querying quota, but they used to silently mirror the provider's own credentials — so copying a provider or editing its main API key / base URL left the usage script pinned to the old endpoint and key, and quota queries kept hitting a stale target. `ProviderService` now normalizes before persisting: if the script's `api_key` or `base_url` matches the provider's resolved usage credentials (or is blank), it is cleared to `None` so queries fall back to the live config; genuinely different overrides are kept (`token_plan` scripts are left alone). The deeplink import path gets matching normalization too, and the front-end invalidates the relevant cache keys on update so the home page re-queries with the corrected config. ([#4654](https://github.com/farion1231/cc-switch/pull/4654))
### Hermes Config Directory Resolves Correctly on Windows
CC Switch hardcoded `~/.hermes` as the Hermes config directory, but Hermes itself resolves it via the `HERMES_HOME` environment variable, then a platform default (`%LOCALAPPDATA%\hermes` on Windows). On Windows this meant CC Switch wrote provider configs to a path Hermes never reads, so provider switches had no effect. `get_hermes_dir()` now mirrors Hermes' own resolution order — explicit override, then `HERMES_HOME` (taken verbatim, no `~` expansion), then the platform default — re-honoring the `HERMES_HOME` that #3470 had dropped (Hermes' Windows installer sets it as the first-class mechanism for relocated installs). ([#4680](https://github.com/farion1231/cc-switch/pull/4680), see #3178, #3470)
### Linux Wayland: Override the AppImage's Forced `GDK_BACKEND=x11`
The AppImage's GTK launch hook unconditionally exports `GDK_BACKEND=x11` to dodge a historical native-Wayland crash. On newer Wayland + NVIDIA setups, this forced XWayland leaves the WebKitGTK web content unable to receive pointer events (the title bar clicks, the page is dead) and black-screens on resize, and the existing `WEBKIT_DISABLE_*` mitigations don't help because the root cause is the forced window backend, not rendering. `main.rs` now reads an optional `CC_SWITCH_GDK_BACKEND` escape hatch before GTK init (the AppImage's launch hook never touches it): leaving it unset keeps current behavior (zero regression). When you hit the problem above, launch with it set to switch back to native Wayland:
```bash
CC_SWITCH_GDK_BACKEND=wayland ./CC-Switch-*.AppImage
```
The override is generic — if you're on a tiling Wayland compositor hitting the inverse input problem, set `CC_SWITCH_GDK_BACKEND=x11` instead. ([#4351](https://github.com/farion1231/cc-switch/pull/4351), fixes #4350)
### "Get API Key" Link Now Shows in Claude Desktop, OpenClaw, and Hermes Forms
The "Get API Key" link and partner-promotion block below the API key input was only wired for claude / codex / gemini / opencode. Claude Desktop rendered a bare input that never showed it, and OpenClaw / Hermes were blocked by two gaps (the whitelist only listed those four appIds, and category parsing only recognized those four preset-id patterns). Claude Desktop now uses the shared `ApiKeySection`, and both the whitelist and category parsing were extended to claude-desktop / openclaw / hermes; additionally, the Hermes / OpenClaw forms no longer let an "official" category disable the key input (these apps have no OAuth-only official providers — e.g. Hermes' Nous Research is official but still needs a user-supplied key).
### Deduplicate Windows Codex npm Shims
On Windows, npm installs a tool as three sibling files — `codex.cmd`, `codex.exe`, and an extensionless Unix shim `codex` — and CC Switch previously listed all three as candidates, so the non-executable extensionless shim was probed as a redundant / failing candidate. It now appends the extensionless path only when there's no runnable `.cmd` / `.exe` sibling adjacent, and path resolution prefers the runnable `.cmd` / `.exe`, anchoring version detection and launch to the actually-runnable Windows shim. ([#4782](https://github.com/farion1231/cc-switch/pull/4782))
### Scroll Bounds for Long Select Dropdowns
`SelectContent` previously used `overflow-hidden` with no height cap, so dropdowns with many options (e.g. long model / provider lists) rendered taller than the viewport and clipped their overflow with no way to reach it. It now sets `max-h-[min(24rem,var(--radix-select-content-available-height))]` and `overflow-y-auto`, bounding the content to 24rem or the Radix-computed available height and allowing vertical scrolling. ([#4798](https://github.com/farion1231/cc-switch/pull/4798))
### Date-Range Picker Calendar Stays On-Screen in Narrow Popovers
The custom date-range picker previously switched to its two-column layout (date fields | calendar) based on the **viewport** width (Tailwind's `sm:` 640px breakpoint), but the popover is clamped to `100vw - 2rem` and anchored to its trigger, so its real available width is narrower than the viewport. On narrow windows the two-column layout could activate while the popover only had room for one column, pushing the calendar column off the right edge where it was clipped (the month header and 4 of 7 weekday columns cut off and unreachable). The layout now keys off the popover's own inline size via a CSS container query, so it collapses to one column exactly when the popover itself is narrow, keeping the calendar fully visible at any window width. ([#4860](https://github.com/farion1231/cc-switch/pull/4860))
---
## Documentation
### `CC_SWITCH_GDK_BACKEND` Escape Hatch Documented
Added an FAQ entry for the optional `CC_SWITCH_GDK_BACKEND` environment variable across all four README languages and the zh / en / ja user-manual troubleshooting pages, explaining how Wayland + NVIDIA users can switch back to native Wayland when the web content goes "click-dead + black screen on resize", and how tiling-Wayland users can set it to `x11` for the inverse input problem.
### Overseas Kimi READMEs Point to platform.kimi.ai
The Kimi K2.7 Code partner section in the English, German, and Japanese READMEs now points its banner and inline calls to action at `https://platform.kimi.ai?aff=cc-switch` (keeping the referral tag), and all four READMEs gained a line promoting the Kimi For Coding subscription linked to `https://www.kimi.com/code/?aff=cc-switch`.
---
## Upgrade Notes
### Existing Native Codex Providers Need a One-Time Re-Save
This release reworks model-catalog generation for native Responses direct-connect. If you previously configured a Codex provider using native Responses (`openai_responses`), **re-pick the preset or open the provider and save it once** to generate the new `~/.codex/cc-switch-model-catalog.json` — that's what lets the Codex desktop app show the custom models and makes the tools work. This requires no database migration and does not affect providers on the `openai_chat` format.
### web_search Blacklist Is Default Behavior
For known-to-reject-`web_search` domestic native gateways (Xiaomi MiMo, Meituan LongCat, MiniMax, Qwen3-Coder), this release automatically writes `web_search = "disabled"` on switch. Relays serving real GPT, Doubao, general Qwen, and unknown providers are unaffected and keep Codex's default. The switch is managed by CC Switch with an ownership sentinel, so switching back to a provider not on the blacklist restores it automatically, with no manual intervention.
### Default Sonnet Tier Change
Claude-family providers newly created from a preset now have their default Sonnet tier pointing to `claude-sonnet-5`. Existing configured providers are unaffected and keep their configuration as-is; to switch to Sonnet 5, re-pick the preset and save.
---
## Risk Notice
This release continues the risk notices from previous versions for reverse-proxy-style features.
**Codex OAuth reverse proxy**: using a ChatGPT subscription's Codex OAuth through a reverse proxy may violate OpenAI's terms of service. See the [v3.13.0 release notes](v3.13.0-en.md#-risk-notice) for details.
**Codex third-party provider Chat routing**: when CC Switch local proxy converts and forwards Codex requests to third-party providers, each provider may have different requirements for billing, compliance, and data retention. Read the target provider's terms before use.
**Claude Desktop third-party provider proxy switching**: when CC Switch's built-in proxy gateway forwards Claude Desktop requests to third-party providers, you must also follow the target provider's billing, compliance, and data-retention terms.
By enabling these features, users accept the related risks. CC Switch is not responsible for account restrictions, warnings, or service suspensions caused by using these features.
---
## Thanks
Thanks to the following contributors for the features and fixes in v3.16.5:
- [#4776](https://github.com/farion1231/cc-switch/pull/4776): add the categorized session view and group management, thanks @alkaid616.
- [#4829](https://github.com/farion1231/cc-switch/pull/4829): rename "Write Common Config" to "Apply Common Config", thanks @arichyx.
- [#4654](https://github.com/farion1231/cc-switch/pull/4654): persist usage-script credentials only as explicit overrides, thanks @yyhhyyyyyy.
- [#4680](https://github.com/farion1231/cc-switch/pull/4680): fix Hermes provider config not taking effect on Windows, thanks @thisTom.
- [#4782](https://github.com/farion1231/cc-switch/pull/4782): deduplicate Windows Codex npm shims, thanks @justjavac.
- [#4798](https://github.com/farion1231/cc-switch/pull/4798): fix long dropdown lists not being scrollable, thanks @xwil1.
- [#4351](https://github.com/farion1231/cc-switch/pull/4351): allow overriding the AppImage's forced `GDK_BACKEND=x11` via `CC_SWITCH_GDK_BACKEND`, thanks @BoneLiu.
- [#4860](https://github.com/farion1231/cc-switch/pull/4860): keep the date-range picker calendar on-screen in narrow popovers, thanks @SaladDay.
Thanks also to everyone who reported native Codex direct-connect, common config, credential reuse, and platform compatibility issues after the v3.16.4 release. Many of these patches came directly from real-world reproduction clues.
---
## Download & Install
Visit [Releases](https://github.com/farion1231/cc-switch/releases/latest) and download the build for your system.
### System Requirements
| System | Minimum Version | Architecture |
| ------- | ------------------------ | ----------------------------------- |
| Windows | Windows 10 and later | x64 / ARM64 |
| macOS | macOS 12 (Monterey)+ | Intel (x64) / Apple Silicon (arm64) |
| Linux | See table below | x64 / ARM64 |
### Windows
| File | Description |
| ---------------------------------------- | ------------------------------------------------ |
| `CC-Switch-v3.16.5-Windows.msi` | **Recommended** - MSI installer with auto-update |
| `CC-Switch-v3.16.5-Windows-Portable.zip` | Portable build, unzip and run |
Windows ARM64 devices should pick the artifact whose file name carries the `arm64` tag.
### macOS
| File | Description |
| -------------------------------- | ----------------------------------------------------- |
| `CC-Switch-v3.16.5-macOS.dmg` | **Recommended** - DMG installer, drag to Applications |
| `CC-Switch-v3.16.5-macOS.zip` | Unzip and drag to Applications, Universal Binary |
| `CC-Switch-v3.16.5-macOS.tar.gz` | For Homebrew install and auto-update |
Homebrew install:
```bash
brew install --cask cc-switch
```
Upgrade:
```bash
brew upgrade --cask cc-switch
```
### Linux
Linux assets are available for both **x86_64** and **ARM64** (`aarch64`). Choose the file whose architecture tag matches your machine's `uname -m` output:
- `CC-Switch-v3.16.5-Linux-x86_64.AppImage` / `.deb` / `.rpm`
- `CC-Switch-v3.16.5-Linux-arm64.AppImage` / `.deb` / `.rpm`
| Distribution | Recommended Format | Install Command |
| --------------------------------------- | ------------------ | --------------------------------------------------------------------- |
| 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` | Make executable and run directly, or use AUR |
| Other distributions / unsure | `.AppImage` | `chmod +x CC-Switch-*.AppImage && ./CC-Switch-*.AppImage` |
+278
View File
@@ -0,0 +1,278 @@
# CC Switch v3.16.5
> 本リリースの目玉は、**ネイティブ Responses 形式の国産モデルプロバイダーをきちんと適合させたこと**です——ネイティブ Responses endpoint を備えるプロバイダー(小米 MiMo、火山 Doubao、Qwen3-Coder、美団 LongCat、MiniMax)向けに Codex モデルカタログを生成し、Codex デスクトップがこれらのモデルを表示でき、内蔵ツールも正しく動くようにしました。また、`web_search` を拒否する一部の国産ゲートウェイに対しては、そのツールを自動で無効化し、リクエストがハード拒否されないようにします。ほかに 2 つの重要な改善があります: プロバイダーを切り替えるとき、アプリ内で追加したプラグインや環境変数などが**自動的に共通設定へ書き戻され、次のプロバイダーへ引き継がれます**。そして LinuxWayland + NVIDIA)で「タイトルバーは押せるのにページが反応しない、リサイズで黒画面」になる問題も、環境変数のスイッチで自力回避できるようになりました。本リリースはさらに Claude Sonnet 5 の価格と既定階層の引き上げ、2 段階グルーピングのセッションビュー、そして一連の認証情報の安全性とプラットフォーム互換性の修正を届けます。
**[English →](v3.16.5-en.md) | [中文版 →](v3.16.5-zh.md)**
---
## 利用ガイド
本リリースの新機能は、主に Codex プロバイダーフォーム、セッションパネル、使用量 / 共通設定に収まっています。以下のドキュメントとあわせてご覧ください:
- **[Codex デスクトップでカスタムモデルが見えない?](../guides/codex-desktop-custom-model-visibility-ja.md)**: 本リリースは**ネイティブ直結時のモデルカタログ生成**を作り直しました——Codex プロバイダーがネイティブ Responses(`openai_responses`)で直結するとき、CC Switch が `~/.codex/cc-switch-model-catalog.json` を生成し、Codex デスクトップが設定済みのカスタムモデルを表示でき、ツールも動くようにします。以前にネイティブ Codex プロバイダーを設定していた場合は、新しいカタログを生成するために**一度保存し直して**ください(下記「アップグレード時の注意」を参照)。
- **[使用量統計](../user-manual/ja/4-proxy/4.4-usage.md)**: 使用量ダッシュボードのデータソースと集計の仕組みを確認できます。本リリースでは Claude Sonnet 5 の価格を追加し、使用量スクリプトの認証情報が「明示的なオーバーライド」として永続化される問題を修正しました。
- **[設定](../user-manual/ja/1-getting-started/1.5-settings.md)**: Codex の上流形式セレクタとローカルルーティングのトグル、Claude の共通設定(「共通設定を適用」に改名され、切り替え時に自動同期)は、いずれもプロバイダーフォームの高度なオプションにあります。
---
> [!WARNING]
>
> ## 唯一の公式チャネル(必ずお読みください)
>
> CC Switch は**完全に無料・オープンソース**のデスクトップアプリで、**ユーザーから料金を徴収することはありません**。本ソフトウェアは下記の公式チャネルからのみ入手してください:
>
> | チャネル | 唯一の公式 |
> | ------------ | ------------------------------------------------------------------------------ |
> | 公式サイト | **[ccswitch.io](https://ccswitch.io)** |
> | ソースコード | **[github.com/farion1231/cc-switch](https://github.com/farion1231/cc-switch)** |
> | ダウンロード | **[GitHub Releases](https://github.com/farion1231/cc-switch/releases)** |
> | 作者 | **[@farion1231](https://github.com/farion1231)** |
> | 偽サイト通報 | **[GitHub Issues](https://github.com/farion1231/cc-switch/issues)** |
>
> **料金請求・チャージ・認証情報の提供を求める「CC Switch」サイトやクライアントはすべて偽物です。** 支払いを誘導された場合は直ちに操作を中止し、GitHub Issues からご報告ください。
---
## 概要
CC Switch v3.16.5 は v3.16.4 に続くメンテナンスアップデートで、その中心は**国産モデルプロバイダーの Codex ネイティブ直結をエンドツーエンドで通すこと**です。v3.16.4 ですでに千問 / DashScope 百炼、小米 MiMo、火山 Doubao、美団 LongCat、MiniMax をネイティブ Responses endpoint へ切り替えていましたが、本リリースはさらに Codex が必要とする**モデルカタログ**(`~/.codex/cc-switch-model-catalog.json`)を生成し、Codex デスクトップがこれらのカスタムモデルを本当に表示でき、内蔵ツールも呼び出せるようにし、モデルマッピングを「ローカルルーティング」トグルから完全に分離しました。第一方モデルが OpenAI 内蔵の `web_search` に対応しない一部の国産ゲートウェイ(MiMo、LongCat、MiniMax、Qwen3-Coder)については、本リリースがそのツールを自動で無効化し、Codex の既定でそれが付いてハード 400 を招くことを避けます。
日々の使い勝手の面では、本リリースは Claude の**共通設定をプロバイダー切り替え時に自動で同期・引き継ぐ**ようにしました——アプリ内で直接追加したプラグイン、環境変数、テーマなどが、まず共通設定へ書き戻され、次のプロバイダーへ渡されるため、切り替え時に失われません。Linux(Wayland + NVIDIA)でクリック無反応 / 黒画面になるユーザー向けに、自力回避できる環境変数を追加し、Claude Sonnet 5 の価格を補って既定の Sonnet 階層をそれへ引き上げ、「プロバイダー → プロジェクトディレクトリ」の 2 段階グルーピングのセッションビューを届け、一連の認証情報の安全性(共通設定スニペットからすべての秘密情報を除去、使用量スクリプトの認証情報は明示的なオーバーライドとしてのみ保持)、プラットフォーム互換性(Hermes の Windows 設定ディレクトリ、Windows の Codex npm シム)、UI(長いドロップダウンのスクロール、狭いウィンドウの日付範囲セレクタ)の問題を修正しました。あわせて、いくつかのプロバイダープリセットも追加し、そのまますぐ選べます。
**リリース日**: 2026-07-01
**Stats**: 36 commits | 93 files changed | +5,678 / -2,804 lines
---
## ハイライト
- **国産モデルプロバイダーの Codex ネイティブ直結が本当に使える**: 小米 MiMo、火山 Doubao、Qwen3-Coder、美団 LongCat、MiniMax などの国産プロバイダー向けに Codex モデルカタログ(`~/.codex/cc-switch-model-catalog.json`)を生成し、Codex デスクトップがこれらのモデルを表示でき、内蔵ツールが動くようにします。あわせて `web_search` を拒否する国産ゲートウェイ(MiMo、LongCat、MiniMax、Qwen3-Coder)に対してはそのツールを自動で無効化し、ハード 400 を避けます。**既存のネイティブプロバイダーは、新しいカタログ生成のために一度保存し直す必要があります。**
- **共通設定を切り替え時に自動同期・引き継ぎ**: 共通設定を有効にした Claude プロバイダーから切り替えるとき、アプリ内で追加したプラグイン、環境変数、テーマ、hooks が、まず共通設定へ書き戻され、次のプロバイダーへ渡されます——切り替え時に上書きで失われることがなくなりました。
- **Linux Wayland のクリック無反応 / 黒画面の回避スイッチ**: Wayland + NVIDIA で「タイトルバーは押せるのにページが反応しない、リサイズで黒画面」に遭遇したら、`CC_SWITCH_GDK_BACKEND=wayland` で起動すればネイティブ Wayland へ切り替えられます(タイリング系コンポジタで逆の問題に遭遇する場合は `x11` に設定)。
- **Claude Sonnet 5**: Sonnet 5 の価格を追加し、各プリセットの既定 Sonnet 階層を `claude-sonnet-5` へ引き上げます。
- **セッションの分類ビューとグルーピング**: セッションパネルに「プロバイダー → プロジェクトディレクトリ」の 2 段階グルーピングビューを新設し、グループヘッダーの 3 状態チェックボックスでワンクリックの一括選択に対応します。
- **新しいプロバイダープリセット**: 七牛云、FennoAI、ZetaAPI、TeamoRouter、NekoCode、Code0.ai、Amux などのプロバイダープリセットを、管理対象アプリ全体に追加し、そのまますぐ選べます。
---
## 追加機能
### 国産モデルプロバイダーの Codex ネイティブ直結(モデルカタログの生成)
本リリースは、国産プロバイダーの Codex ネイティブ直結を通しました。v3.16.4 で小米 MiMo、火山 Doubao、Qwen3-Coder、美団 LongCat、MiniMax をネイティブ Responses`apiFormat: "openai_responses"`)へ切り替えたのに続き、本リリースは当時の「ネイティブ直結ならモデルカタログを削除する」やり方を覆しました: これらのプロバイダーがローカルプロキシを経由せず直結するとき、CC Switch が `~/.codex/cc-switch-model-catalog.json` を生成し、Codex デスクトップがこれらのカスタムモデルを本当に表示でき、内蔵ツールも動くようにします——MiMo のようなネイティブゲートウェイが拒否する freeform `apply_patch``type=custom`)ツールを発生させません(編集は `shell_command` へフォールバック)。カタログ生成は `apiFormat` で判定され、「ローカルルーティング」トグルから分離されているため、ネイティブプロバイダーはローカルルートマッピングを有効にしなくてもカタログを永続化します。一方 `openai_chat` 形式は、既存の Responses↔Chat プロキシ変換をそのまま保ちます。Codex のパーサーは各項目に `base_instructions` を要求するため、ネイティブテンプレートは中立の既定値を携え、各ベンダーの公式文面がそれを上書きします(MiMo、MiniMax)。**既存のネイティブプロバイダーは、有効なカタログを生成するために一度保存し直す必要があります**(データベース移行は不要)。
あわせて、第一方モデルが OpenAI 内蔵の `web_search` ツールに対応しない一部の国産ゲートウェイ(MiMo、LongCat、MiniMax、Qwen3-Coder)については、本リリースが切り替え時にそのツールを自動で無効化し、Codex の既定でそれが付いてハード 400 で拒否されるのを避けます(下記「修正」を参照)。
### セッションの分類ビューとグルーピング
セッション管理パネルに、既存のフラットなリストに加えてグルーピングビューを新設し、ツールバーの List / ListTree セレクタで切り替え、ビューモードと展開状態を `localStorage` に永続化します。グルーピングは「プロバイダー → プロジェクトディレクトリ」の 2 段階階層を構築します: プロジェクトディレクトリ名でグループ化し、プロジェクトディレクトリを持たないセッションは「不明なディレクトリ」バケットに入ります。両方の段階が折りたたみ可能な区画で、「すべて折りたたむ」ボタンを備えます。バッチモードでは、各グループヘッダーに 3 状態チェックボックスが現れ、そのグループ内の選択可能なセッションをすべて一括で選択 / 解除でき、選択数 / 選択可能数のバッジも表示します。4 言語(zh / en / ja / zh-TW)の文言は同期済みです。この変更は完全にフロントエンドで、バックエンドのコマンドやデータアクセス層には手を加えていません。([#4776](https://github.com/farion1231/cc-switch/pull/4776)
### Claude Sonnet 5 のモデル価格
`schema.rs``claude-sonnet-5` の価格行を Anthropic の定価で追加しました——入力 / 出力が 100 万 token あたり \$3 / \$15、キャッシュ読み書きが \$0.30 / \$3.75 で、Sonnet 4.6 と同一です。導入期の \$2 / \$10 プロモーション(2026-08-31 まで有効)は意図的にシードせず、記帳が一時的な割引ではなく定常の定価を反映するようにしています。この行はアプリの次回起動時に `ensure_model_pricing_seeded` 経由で適用され、`SCHEMA_VERSION` の引き上げは不要です。
### 新しいプロバイダープリセット
本リリースは一連のプロバイダープリセットを追加しました。選択して自分の API key を入力すればすぐ使えます:
- **七牛云(Qiniu)**: 管理対象の 7 アプリすべて(Gemini を含む)をカバーし、ネイティブ Claude / GPT / Gemini を中継します。
- **FennoAI / ZetaAPI / TeamoRouter / NekoCode**: それぞれ 6 アプリ(Claude、Claude Desktop、Codex、OpenCode、OpenClaw、Hermes)をカバーします。
- **Code0.ai**: 7 アプリすべて(Gemini を含む)をカバーします。
- **Amux**: 6 アプリをカバーします。
各プリセットの endpoint と既定モデルは対応するアプリに合わせて設定済みです——Claude 系は Anthropic 互換ホストへ直結、Codex はネイティブ Responses、その他は OpenAI 互換の `/v1` を使います。
---
## 変更
### プロバイダー切り替え時に共通設定を自動同期・引き継ぎ
これは本リリースのとても実用的な変更です: 共通設定を有効にした Claude プロバイダーから切り替えるとき、サービスはまずその live の `settings.json` から**共有可能な部分を再抽出して共通設定を更新し**、次のプロバイダーへ渡します。以前のような一方向の書き込みだけではありません。これにより、実行中のアプリで直接追加したプラグイン(`enabledPlugins`)、hooks、環境変数(`env`)、テーマ(`theme`)などの共有設定が、切り替え時に静かに失われることなく、自動的に次のプロバイダーへ引き継がれます。削除も同期されます(削除されたキーは再注入されません)。この同期は共通設定を有効にした Claude プロバイダーに厳密に限定され、明示的にクリアされた場合はスキップされ、すべての失敗は非致命的(警告のみ)で、切り替えを妨げることは決してありません。
### Codex のモデルマッピングと「ローカルルーティング」トグルの分離
Codex プロバイダーフォームが Claude Code に揃いました——モデルマッピングカタログがルーティングテイクオーバーから独立しました。ネイティブ Responses プロバイダー(MiMo、Doubao、MiniMax)はプロキシなし直結のためにそれを必要とする一方、Chat プロバイダーはいずれにせよプロキシを経由するためです。「ローカルルーティングが必要」トグルは削除されました(バックエンドのフィールドを持たず、カタログ / 推論の永続化を制御していただけで、これはマッピングが記入されているかどうかと等価です)。モデルマッピングは非公式プロバイダーに対して常に表示され、空でなければ永続化されます。一方、推論能力の表示 / 永続化は Chat 形式で制御されるようになりました。4 言語(zh / en / ja / zh-TW)の文言もあわせて書き直しました。副次的な修正として、`useCodexConfigState` が保存済みプロバイダーを読み込むときに `supportsParallelToolCalls` / `inputModalities` / `baseInstructions` を落とす問題(編集時に並列ツール、画像入力、公式の base instructions を静かに失っていた)も修正しました。
### 既定の Sonnet 階層を Claude Sonnet 5 へ引き上げ
各プロバイダープリセットの既定 Sonnet 階層を `claude-sonnet-4-6` から `claude-sonnet-5` へ引き上げました(claude / claude-desktop / hermes / openclaw / opencode のプリセットとユニバーサルの `NEWAPI_DEFAULT_MODELS` をカバー)。`ANTHROPIC_MODEL` / `ANTHROPIC_DEFAULT_SONNET_MODEL` / `ANTHROPIC_DEFAULT_OPUS_MODEL` などのキーとそのプレフィックス付き変種が対象です。Claude Desktop の既定ルートの sonnet `route_id` もあわせて `claude-sonnet-5` へ移行しました。非 Anthropic の pingpt / gemini / glm / sonnet-4-5)はそのままです。
### Doubao の日付付き model id と価格の正規化
DoubaoDouBaoSeed)プリセットの model id を日付付きの `doubao-seed-2-1-pro-260628` へ切り替えました(各アプリをカバー)。火山方舟が素の `doubao-seed-2-1-pro` を 404 で拒否し、完全な日付付き id のみを受け付けるためです。実際の使用量が日付サフィックスを伴うようになったため、`strip_model_date_suffix` を火山の 6 桁 YYMMDD 形式も剥がせるように拡張し(`-123456` のような非日付のバージョンサフィックスを誤って食わないよう、月 01-12・日 01-31 を検証)、価格表の素の名前のシード行にマッチするよう正規化して、Doubao モデルが \$0 コストで表示される問題を修正しました。
###「共通設定を書き込む」を「共通設定を適用」へ改名
元のラベル「共通設定を書き込む」はデータの流れの方向が曖昧でした(「現在の設定を共通設定へ書き込む」と読めた)。実際の挙動は逆で——保存済みの共通設定スニペットをこのプロバイダーの設定へマージするものです。チェックボックスを 4 言語(zh / en / ja / zh-TW)で「共通設定を適用」へ改名し、すべてのヒント / ガイド / 説明の参照を含め、日本語ユーザーマニュアルと `README_JA.md` もあわせて同期しました。([#4829](https://github.com/farion1231/cc-switch/pull/4829)
### その他のプリセットと素材の調整
- **OpenClaw の Doubao コンテキストを 262144 へ整合**: OpenClaw の DouBaoSeed プリセットは以前 128000 をハードコードしていましたが、Codex 側は同モデルで 262144 を使っており、OpenClaw ユーザーのウィンドウが小さすぎました。両者を整合させ、再びずれないようクロスプリセットの一貫性テストを追加しました。
- **火山 / Doubao / BytePlus の公式サイトリンクを訂正**: これら 3 つのプリセットの「公式サイトを訪問」リンクが、誤ってコンソール / 登録リンクに設定されていたため、クリーンな製品トップページへ戻しました。
- **過大なプロバイダーアイコンを 256px へダウンスケール**: 一連のバンドルアイコンが、実際の描画サイズ(~32px)よりはるかに大きかったため、ダウンスケールで大幅に容量を削減しました(コード / ファイル名 / インポートの変更なし。例: ZetaAPI 940KB→40KB、relaxcode 1.16MB→42KB)。また、一度も参照されていない 1.4MB の `dds.svg` 孤立ファイルを削除しました。
---
## 修正
### `web_search` を拒否するネイティブ Codex ゲートウェイでは無効化
一部のネイティブ `/responses` ゲートウェイは、第一方モデルが OpenAI のホスト型 `web_search` ツールを備えないため、それを「tool type 'web_search' is not supported」で拒否します。そして Codex は既定でそのツールを送るため、ハード 400 を招きます。CC Switch は現在、これらのベンダーに対して切り替え時にトップレベルの TOML 行 `web_search = "disabled"` を書き込みます。スコープはブラックリスト(既定オン)で、`base_url` ホスト(`xiaomimimo.com``longcat.chat``minimax.io``minimaxi.com`)または model のブランドプレフィックス(`mimo``longcat``minimax``qwen3-coder`)に一致するプロバイダーだけが無効化されるため、本物の GPT、Doubao、一般の Qwen、そして未知のプロバイダーはいずれも Codex の既定を保ちます。`qwen3-coder` プレフィックスはネイティブの `qwen3-coder-plus` だけを抑制し(百炼 / DashScope は coder 系で内蔵ツールを非対応と示します)、同じホストを共有する一般の Qwen は有効のままです。マッチングは model 軸で行われ(アグリゲーターの `vendor/` パスセグメントを剥がす)、SiliconFlow が拒否ベンダーのモデルを前面に立てるようなケースも捕捉します。曖昧な「これは GPT か?」というホワイトリストではなくブラックリストを選んだのは、`web_search` を誤ってオンのままにするとハード 400 で失敗するためです。所有権のセンチネルにより、CC Switch は自らが書いた `disabled` 値だけを削除するため、既存プロバイダーは保存し直す必要がなく、切り替えて戻せば再び有効になります。副次的な修正として、LongCat-2.0-Preview プリセットのコンテキストウィンドウを 131072(128K)から本来の 1048576(1M)へ訂正しました。
### 共有される Claude 共通設定スニペットからすべての認証情報系キーを除去
`extract_claude_common_config` は以前 `ANTHROPIC_API_KEY``ANTHROPIC_AUTH_TOKEN` だけをマスクしていましたが、Claude プロバイダーは正当に他の認証情報(`OPENROUTER_API_KEY``GOOGLE_API_KEY`、場合によっては OpenAI / Gemini / AWS Bedrock / Vertex の秘密情報)を携えることがあり、それらが共有スニペットへ漏れ、他のプロバイダーへ注入されるおそれがありました。抽出は現在、認証情報の形をした環境変数キー(`*_API_KEY` / `*_AUTH_TOKEN` / `*secret*` / `*token*` など)をパターンマッチで除去し、`MAX_OUTPUT_TOKENS` のような正当に共有可能な複数形 `*_TOKENS` の値は保持します。手動の「抽出」と一度きりの自動抽出の経路にあった同じ漏れも、あわせて塞ぎました。
### 使用量スクリプトの認証情報は明示的なオーバーライドとしてのみ永続化
プロバイダーの使用量スクリプトは、クォータ照会時に live の認証情報を上書きする任意の `api_key` / `base_url` フィールドを保持しますが、これらが以前はプロバイダー自身の認証情報を静かにミラーしていました——そのため、プロバイダーを複製したり、メインの API key / base URL を編集したりすると、使用量スクリプトが古い endpoint と key に固定されたまま残り、クォータ照会が古い対象を叩き続けていました。`ProviderService` は現在、永続化の前に正規化します: スクリプトの `api_key` または `base_url` がプロバイダーの解決済み使用量認証情報と一致する(または空の)場合は `None` にクリアして、照会が live 設定へフォールバックするようにします。本当に異なるオーバーライドは保持されます(`token_plan` スクリプトはそのまま)。deeplink インポート経路にも同様の正規化を加え、フロントエンドは更新時に関連するキャッシュキーを無効化して、ホームページが訂正済みの設定で再照会するようにします。([#4654](https://github.com/farion1231/cc-switch/pull/4654)
### Hermes 設定ディレクトリが Windows で正しく解決されるように
CC Switch は Hermes の設定ディレクトリとして `~/.hermes` をハードコードしていましたが、Hermes 自身は `HERMES_HOME` 環境変数、次にプラットフォーム既定(Windows では `%LOCALAPPDATA%\hermes`)で解決します。Windows ではこれにより、CC Switch がプロバイダー設定を Hermes が決して読まないパスへ書いていたため、プロバイダーの切り替えが効きませんでした。`get_hermes_dir()` は現在、Hermes 自身の解決順序——明示的なオーバーライド、次に `HERMES_HOME`(そのまま使用、`~` 展開なし)、次にプラットフォーム既定——をミラーし、#3470 が落としていた `HERMES_HOME` を再び尊重します(Hermes の Windows インストーラーは、再配置されたインストールの第一級の仕組みとしてそれを設定します)。([#4680](https://github.com/farion1231/cc-switch/pull/4680)、#3178#3470 を参照)
### Linux Wayland: AppImage が強制する `GDK_BACKEND=x11` を上書き可能に
AppImage の GTK 起動フックは、歴史的なネイティブ Wayland のクラッシュを避けるために `GDK_BACKEND=x11` を無条件でエクスポートします。新しめの Wayland + NVIDIA 環境では、この強制された XWayland により WebKitGTK のウェブコンテンツがポインタイベントを受け取れなくなり(タイトルバーは押せてもページは反応しない)、リサイズで黒画面になります。既存の `WEBKIT_DISABLE_*` の緩和策は効きません。根本原因が描画ではなく、強制されたウィンドウバックエンドだからです。`main.rs` は現在、GTK の初期化前に任意の `CC_SWITCH_GDK_BACKEND` 回避スイッチを読みます(AppImage の起動フックはそれに触れません): 未設定なら現状のまま(回帰ゼロ)。上記の問題に遭遇したら、これを設定してネイティブ Wayland へ切り替えて起動できます:
```bash
CC_SWITCH_GDK_BACKEND=wayland ./CC-Switch-*.AppImage
```
この上書きは汎用です——タイリング系 Wayland コンポジタで逆の入力問題に遭遇する場合は、代わりに `CC_SWITCH_GDK_BACKEND=x11` に設定してください。([#4351](https://github.com/farion1231/cc-switch/pull/4351)、#4350 を修正)
###「API Key を取得」リンクが Claude Desktop / OpenClaw / Hermes フォームでも表示されるように
API key 入力欄の下の「API Key を取得」リンクとパートナー宣伝ブロックは、以前 claude / codex / gemini / opencode にしか配線されていませんでした。Claude Desktop はそれを表示しない素の入力欄を描画し、OpenClaw / Hermes は 2 つの欠落(ホワイトリストがその 4 つの appId しか列挙しておらず、カテゴリ解析もその 4 つのプリセット id パターンしか認識しなかった)にブロックされていました。Claude Desktop は現在、共有の `ApiKeySection` を使い、ホワイトリストとカテゴリ解析の両方を claude-desktop / openclaw / hermes へ拡張しました。さらに、Hermes / OpenClaw のフォームは「公式」カテゴリで key 入力欄を無効化しなくなりました(これらのアプリには OAuth 専用の公式プロバイダーが存在しません——例えば Hermes の Nous Research は公式ですが、それでもユーザー入力の key が必要です)。
### Windows の Codex npm シムの重複排除
Windows では、npm がツールを 3 つの兄弟ファイル——`codex.cmd``codex.exe`、拡張子なしの Unix シム `codex`——としてインストールし、CC Switch は以前この 3 つすべてを候補に列挙していたため、実行できない拡張子なしシムが冗長 / 失敗する候補としてプローブされていました。現在は、隣接する実行可能な `.cmd` / `.exe` の兄弟が見つからない場合にのみ拡張子なしパスを追加し、パス解決も実行可能な `.cmd` / `.exe` を優先して、バージョン検出と起動を実際に実行可能な Windows シムに固定します。([#4782](https://github.com/farion1231/cc-switch/pull/4782)
### 長い Select ドロップダウンのスクロール境界
`SelectContent` は以前、高さの上限なしで `overflow-hidden` を使っていたため、選択肢の多いドロップダウン(長いモデル / プロバイダーのリストなど)がビューポートより高く描画され、あふれた項目が切り取られて届かなくなっていました。現在は `max-h-[min(24rem,var(--radix-select-content-available-height))]``overflow-y-auto` を設定し、内容を 24rem または Radix が計算した利用可能高さに収め、縦方向のスクロールを許可します。([#4798](https://github.com/farion1231/cc-switch/pull/4798)
### 日付範囲セレクタのカレンダーが狭いポップオーバーでも画面内に収まるように
カスタム日付範囲セレクタは以前、2 列レイアウト(日付フィールド | カレンダー)への切り替えを**ビューポート**幅(Tailwind の `sm:` 640px ブレークポイント)で判定していましたが、ポップオーバーは `100vw - 2rem` に制限され、トリガーにアンカーされているため、実際に使える幅はビューポートより狭くなります。狭いウィンドウでは、ポップオーバーに 1 列分しか収まらないのに 2 列レイアウトが有効化されることがあり、カレンダー列が右端の外へ押し出されて切り取られていました(月ヘッダーと 7 列中 4 列の曜日が切れて届かない)。レイアウトは現在、CSS コンテナクエリでポップオーバー自身のインラインサイズに基づいて切り替わるため、ポップオーバー自体が狭いときにちょうど 1 列へ折りたたまれ、どのウィンドウ幅でもカレンダーが完全に見えるようになりました。([#4860](https://github.com/farion1231/cc-switch/pull/4860)
---
## ドキュメント
### `CC_SWITCH_GDK_BACKEND` 回避スイッチのドキュメント化
任意の `CC_SWITCH_GDK_BACKEND` 環境変数について、4 言語すべての README と zh / en / ja のユーザーマニュアルのトラブルシューティングページに FAQ 項目を追加し、Wayland + NVIDIA ユーザーがウェブコンテンツの「クリック無反応 + リサイズで黒画面」時にネイティブ Wayland へ切り替える方法、そしてタイリング系 Wayland ユーザーが逆の入力問題で `x11` に設定する方法を解説しました。
### 海外向け Kimi README が platform.kimi.ai を指すように
英語・ドイツ語・日本語の README の Kimi K2.7 Code パートナー段落で、バナーとインラインの行動喚起を `https://platform.kimi.ai?aff=cc-switch` へ向け(紹介タグは維持)、4 言語すべての README に `https://www.kimi.com/code/?aff=cc-switch` へリンクした Kimi For Coding サブスクリプションの宣伝行を追加しました。
---
## アップグレード時の注意
### 既存のネイティブ Codex プロバイダーは一度保存し直しが必要
本リリースは、ネイティブ Responses 直結のモデルカタログ生成を作り直しました。以前にネイティブ Responses(`openai_responses`)を使う Codex プロバイダーを設定していた場合は、新しい `~/.codex/cc-switch-model-catalog.json` を生成するために、**プリセットから選び直すか、プロバイダーを開いて一度保存し直して**ください——それが、Codex デスクトップにカスタムモデルを表示させ、ツールを動かすための鍵です。これはデータベース移行を必要とせず、`openai_chat` 形式のプロバイダーには影響しません。
### `web_search` ブラックリストは既定の挙動
`web_search` を拒否することが分かっている国産ネイティブゲートウェイ(小米 MiMo、美団 LongCat、MiniMax、Qwen3-Coder)に対しては、本リリースが切り替え時に自動で `web_search = "disabled"` を書き込みます。本物の GPT、Doubao、一般の Qwen、そして未知のプロバイダーは影響を受けず、Codex の既定を保ちます。このスイッチは CC Switch が所有権のセンチネルで管理するため、ブラックリストにないプロバイダーへ切り替えて戻せば自動的に復元され、手動の介入は不要です。
### 既定の Sonnet 階層の変化
プリセットから新規作成した Claude 系プロバイダーは、既定の Sonnet 階層が `claude-sonnet-5` を指すようになりました。設定済みの既存プロバイダーは影響を受けず、設定はそのまま保たれます。Sonnet 5 へ切り替えたい場合は、プリセットから選び直して保存してください。
---
## リスク通知
本リリースは、リバースプロキシ系機能に関する以前のリスク通知を引き続き適用します。
**Codex OAuth リバースプロキシ**: ChatGPT サブスクリプションの Codex OAuth をリバースプロキシ経由で使用すると、OpenAI の利用規約に違反する可能性があります。詳細は [v3.13.0 release notes](v3.13.0-ja.md#-リスクに関する注意事項) を参照してください。
**Codex サードパーティプロバイダー Chat ルーティング**: CC Switch ローカルプロキシで Codex リクエストを変換し、サードパーティプロバイダーへ転送する場合、課金・コンプライアンス・データ保持に関する制約はプロバイダーごとに異なります。利用前に対象プロバイダーの利用規約を確認してください。
**Claude Desktop サードパーティプロバイダープロキシ切り替え**: CC Switch 内蔵のプロキシゲートウェイで Claude Desktop のリクエストをサードパーティプロバイダーへ転送する場合も、対象プロバイダーの課金・コンプライアンス・データ保持に関する規約に従う必要があります。
上記機能を有効化したユーザーは、関連するリスクを自ら負うものとします。CC Switch は、これらの機能の利用によって発生したアカウント制限、警告、サービス停止について責任を負いません。
---
## 謝辞
v3.16.5 で機能と修正を届けてくださった以下のコントリビューターに感謝します:
- [#4776](https://github.com/farion1231/cc-switch/pull/4776): セッションの分類ビューとグループ管理を追加、@alkaid616 に感謝。
- [#4829](https://github.com/farion1231/cc-switch/pull/4829):「共通設定を書き込む」を「共通設定を適用」へ改名、@arichyx に感謝。
- [#4654](https://github.com/farion1231/cc-switch/pull/4654): 使用量スクリプトの認証情報を明示的なオーバーライドとしてのみ永続化、@yyhhyyyyyy に感謝。
- [#4680](https://github.com/farion1231/cc-switch/pull/4680): Windows で Hermes プロバイダー設定が効かない問題を修正、@thisTom に感謝。
- [#4782](https://github.com/farion1231/cc-switch/pull/4782): Windows の Codex npm シムを重複排除、@justjavac に感謝。
- [#4798](https://github.com/farion1231/cc-switch/pull/4798): 長いドロップダウンリストがスクロールできない問題を修正、@xwil1 に感謝。
- [#4351](https://github.com/farion1231/cc-switch/pull/4351): AppImage が強制する `GDK_BACKEND=x11``CC_SWITCH_GDK_BACKEND` で上書き可能に、@BoneLiu に感謝。
- [#4860](https://github.com/farion1231/cc-switch/pull/4860): 日付範囲セレクタのカレンダーが狭いポップオーバーでも画面内に収まるように、@SaladDay に感謝。
v3.16.4 リリース後に Codex ネイティブ直結、共通設定、認証情報の再利用、プラットフォーム互換性の問題を報告してくださったすべてのユーザーにも感謝します。今回の多くのパッチは、こうした実際の利用シーンから得られた再現の手がかりに基づいています。
---
## ダウンロードとインストール
[Releases](https://github.com/farion1231/cc-switch/releases/latest) から、お使いのシステムに対応するビルドをダウンロードしてください。
### システム要件
| システム | 最低バージョン | アーキテクチャ |
| -------- | ------------------------ | ----------------------------------- |
| Windows | Windows 10 以降 | x64 / ARM64 |
| macOS | macOS 12 (Monterey) 以降 | Intel (x64) / Apple Silicon (arm64) |
| Linux | 下表を参照 | x64 / ARM64 |
### Windows
| ファイル | 説明 |
| ---------------------------------------- | -------------------------------------------- |
| `CC-Switch-v3.16.5-Windows.msi` | **推奨** - 自動更新対応の MSI インストーラー |
| `CC-Switch-v3.16.5-Windows-Portable.zip` | ポータブル版、展開してそのまま実行できます |
Windows ARM64 デバイスをお使いの場合は、ファイル名に `arm64` 識別子が含まれる対応する制品を選択してください。
### macOS
| ファイル | 説明 |
| -------------------------------- | ------------------------------------------------------ |
| `CC-Switch-v3.16.5-macOS.dmg` | **推奨** - DMG インストーラー、Applications へドラッグ |
| `CC-Switch-v3.16.5-macOS.zip` | 展開して Applications へドラッグ、Universal Binary |
| `CC-Switch-v3.16.5-macOS.tar.gz` | Homebrew インストールと自動更新用 |
Homebrew インストール:
```bash
brew install --cask cc-switch
```
更新:
```bash
brew upgrade --cask cc-switch
```
### Linux
Linux アセットは **x86_64****ARM64**`aarch64`)の両方を提供します。ファイル名にアーキテクチャ識別子が含まれているため、マシンの `uname -m` 出力に合わせて選択してください:
- `CC-Switch-v3.16.5-Linux-x86_64.AppImage` / `.deb` / `.rpm`
- `CC-Switch-v3.16.5-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` |
+278
View File
@@ -0,0 +1,278 @@
# CC Switch v3.16.5
> 这一版的重头戏是**让原生 Responses 格式的国产模型供应商真正适配到位**——为小米 MiMo、火山豆包、千问 Qwen3-Coder、美团 LongCat、MiniMax 等具备原生 Responses 端点的供应商生成 Codex 模型目录,让 Codex 桌面能看到这些模型、内置工具也能正常工作,并对少数拒收 `web_search` 的国产网关自动禁用该工具、避免请求被硬性拒绝。另有两处重要改进:切换供应商时,你在应用内新增的插件、环境变量等会**自动回写到通用配置并带给下一个供应商**;LinuxWayland + NVIDIA)上「标题栏能点、页面点不动、缩放黑屏」的问题,现在也能用一个环境变量开关自救。本版还带来 Claude Sonnet 5 定价与默认档升级、两级分组的会话视图,以及一批凭据安全与平台兼容修复。
**[English →](v3.16.5-en.md) | [日本語版 →](v3.16.5-ja.md)**
---
## 使用攻略
本版的新能力主要落在 Codex 供应商表单、会话面板与用量 / 通用配置里,建议结合以下文档了解:
- **[Codex 桌面看不到自定义模型?](../guides/codex-desktop-custom-model-visibility-zh.md)**:本版重做了**原生直连时的模型目录生成**——当 Codex 供应商使用原生 Responses`openai_responses`)直连时,CC Switch 会生成 `~/.codex/cc-switch-model-catalog.json`,让 Codex 桌面能显示配置的自定义模型、工具也可用。若你此前配过原生 Codex 供应商,请**重新保存一次**以生成新目录(详见下方「升级提醒」)。
- **[用量统计](../user-manual/zh/4-proxy/4.4-usage.md)**:了解用量看板的数据来源与统计口径。本版新增了 Claude Sonnet 5 定价,并修复了用量脚本凭据被当作「显式覆盖」持久化的问题。
- **[设置](../user-manual/zh/1-getting-started/1.5-settings.md)**:Codex 上游格式选择器与本地路由开关、Claude 通用配置(现更名为「应用通用配置」并支持切换时自动同步)都在供应商表单的高级选项里。
---
> [!WARNING]
>
> ## 唯一官方渠道声明(请务必阅读)
>
> CC Switch 是**完全免费、开源**的桌面应用,**不会向用户收取任何费用**。请仅通过下列官方渠道获取本软件:
>
> | 类别 | 唯一官方 |
> | -------- | ------------------------------------------------------------------------------ |
> | 官网 | **[ccswitch.io](https://ccswitch.io)** |
> | 源码 | **[github.com/farion1231/cc-switch](https://github.com/farion1231/cc-switch)** |
> | 下载 | **[GitHub Releases](https://github.com/farion1231/cc-switch/releases)** |
> | 作者 | **[@farion1231](https://github.com/farion1231)** |
> | 举报山寨 | **[GitHub Issues](https://github.com/farion1231/cc-switch/issues)** |
>
> **任何向你收费、要求充值、或索取登录凭据的"CC Switch"网站或客户端均为假冒**。如果你被诱导支付了费用,请立即停止操作并通过 GitHub Issues 反馈。
---
## 概览
CC Switch v3.16.5 是 v3.16.4 之后的一版维护更新,核心是把**国产模型供应商的 Codex 原生直连做通**。v3.16.4 已经把千问 / 百炼、小米 MiMo、火山豆包、美团 LongCat、MiniMax 等供应商切到了原生 Responses 端点,本版进一步为它们生成 Codex 所需的**模型目录**(`~/.codex/cc-switch-model-catalog.json`),让 Codex 桌面真正能看到这些自定义模型、内置工具也能正常调用,并把模型映射从「本地路由」开关里彻底解耦。针对少数第一方模型不支持 OpenAI 内置 `web_search` 的国产网关(MiMo、LongCat、MiniMax、Qwen3-Coder),本版还会自动禁用该工具,避免 Codex 默认带上它触发硬 400。
围绕日常使用体验,本版让 Claude 的**通用配置在切换供应商时自动同步并传递**——你在应用内新增的插件、环境变量、主题等会先回写到通用配置、再带给下一个供应商,不会在切换时丢失;给 LinuxWayland + NVIDIA)上点击失灵 / 黑屏的用户加了一个可自救的环境变量开关;补上 Claude Sonnet 5 定价并把默认 Sonnet 档升级到它;带来「供应商 → 项目目录」两级分组的会话视图;并修了一串凭据安全(通用配置片段剥离全部密钥、用量脚本凭据仅作显式覆盖)、平台兼容(Hermes Windows 配置目录、Windows Codex npm 影子命令)与界面(长下拉滚动、窄窗口日期选择器)的问题。此外也新增了若干供应商预设,开箱即可选用。
**发布日期**2026-07-01
**更新规模**36 commits | 93 files changed | +5,678 / -2,804 lines
---
## 重点内容
- **让国产模型供应商的 Codex 原生直连真正可用**:为小米 MiMo、火山豆包、千问 Qwen3-Coder、美团 LongCat、MiniMax 等国产供应商生成 Codex 模型目录(`~/.codex/cc-switch-model-catalog.json`),让 Codex 桌面能看到这些模型、内置工具可用;并对拒收 `web_search` 的国产网关(MiMo、LongCat、MiniMax、Qwen3-Coder)自动禁用该工具、避免硬 400。**存量原生供应商需重存一次**以生成新目录。
- **通用配置切换时自动同步并传递**:切走一个启用了通用配置的 Claude 供应商时,你在应用内新增的插件、环境变量、主题、hooks 会先自动回写到通用配置,再带给下一个供应商——不再在切换时被覆盖丢失。
- **Linux Wayland 点击失灵 / 黑屏的自救开关**:遇到 Wayland + NVIDIA 上「标题栏能点、页面点不动、缩放黑屏」时,用 `CC_SWITCH_GDK_BACKEND=wayland` 启动即可切回原生 Wayland(平铺式合成器上遇到反向问题可设为 `x11`)。
- **Claude Sonnet 5**:新增 Sonnet 5 定价,并把各预设的默认 Sonnet 档升级到 `claude-sonnet-5`
- **会话分类视图与分组管理**:会话面板新增「供应商 → 项目目录」两级分组视图,分组头支持三态复选框一键批量选择。
- **新增供应商预设**:新增七牛云、FennoAI、ZetaAPI、TeamoRouter、NekoCode、Code0.ai、Amux 等供应商预设,覆盖各受管应用,开箱即可选用。
---
## 新功能
### 国产模型供应商的 Codex 原生直连(生成模型目录)
本版把国产供应商的 Codex 原生直连做通了。继 v3.16.4 把小米 MiMo、火山豆包、千问 Qwen3-Coder、美团 LongCat、MiniMax 等供应商切换到原生 Responses(`apiFormat: "openai_responses"`)之后,本版推翻了当时「原生直连就删掉模型目录」的做法:这些供应商不经过本地代理直连时,CC Switch 会为它们生成 `~/.codex/cc-switch-model-catalog.json`,让 Codex 桌面真正显示这些自定义模型、内置工具也能用——不会触发像 MiMo 这类原生网关会拒绝的 freeform `apply_patch``type=custom`)工具(编辑回退到 `shell_command`)。目录生成按 `apiFormat` 判定、与「本地路由」开关解耦,因此一个原生供应商无需开启本地路由映射也会持久化目录;而 `openai_chat` 格式仍保持既有的 Responses↔Chat 代理转换不变。由于 Codex 解析器要求每个条目都带 `base_instructions`,原生模板携带一个中性默认值、由各厂商官方文案覆盖(MiMo、MiniMax)。**存量原生供应商需重新保存一次以生成有效目录**(无需数据库迁移)。
配套地,对少数第一方模型不支持 OpenAI 内置 `web_search` 工具的国产网关(MiMo、LongCat、MiniMax、Qwen3-Coder),本版会在切换时自动禁用该工具,避免 Codex 默认带上它、被网关以硬 400 拒绝(详见下方「修复」)。
### 会话分类视图与分组管理
会话管理面板在原有平铺列表之外新增了分组视图,通过工具栏的 List / ListTree 选择器切换,视图模式与展开状态都持久化到 `localStorage`。分组构建「供应商 → 项目目录」两级层级:按项目目录名归组,缺少项目目录的会话落入「未知目录」桶。两级都是可折叠区块,并提供「全部折叠」按钮;在批量模式下,每个分组头会出现一个三态复选框,可一键选中 / 取消该组内全部可选会话,并显示已选 / 可选计数徽标。四语(zh / en / ja / zh-TW)文案已同步。该改动完全在前端,不涉及后端命令或数据访问层。([#4776](https://github.com/farion1231/cc-switch/pull/4776)
### Claude Sonnet 5 模型定价
`schema.rs` 里按 Anthropic list 价新增 `claude-sonnet-5` 定价行——输入 / 输出 \$3 / \$15 每百万 token、缓存读写 \$0.30 / \$3.75,与 Sonnet 4.6 一致。介绍期 \$2 / \$10 促销(有效期至 2026-08-31)刻意不入表,让记账反映稳态 list 价而非临时折扣。该行在应用下次启动时通过 `ensure_model_pricing_seeded` 应用,无需 `SCHEMA_VERSION` 变更。
### 新增供应商预设
本版新增了一批供应商预设,选中后填入自己的 API Key 即可使用:
- **七牛云(Qiniu)**:覆盖全部 7 个受管应用(含 Gemini),中转原生 Claude / GPT / Gemini。
- **FennoAI / ZetaAPI / TeamoRouter / NekoCode**:各覆盖 6 个应用(Claude、Claude Desktop、Codex、OpenCode、OpenClaw、Hermes)。
- **Code0.ai**:覆盖全部 7 个应用(含 Gemini)。
- **Amux**:覆盖 6 个应用。
各预设的端点与默认模型已按对应应用配好——Claude 类走 Anthropic 兼容主机直连、Codex 走原生 Responses、其余走 OpenAI 兼容 `/v1`
---
## 变更
### 切换供应商时自动同步并传递通用配置
这是本版一个很实用的改动:切走一个启用了通用配置的 Claude 供应商时,服务会先从它的 live `settings.json` 里**重新提取可共享部分、更新到通用配置**,再带给下一个供应商,而不再只是单向写入。这样一来,你在运行中的应用里直接新增的插件(`enabledPlugins`)、hooks、环境变量(`env`)、主题(`theme`)等共享配置就不会在切换时被静默丢失,而是自动跟着走到下一个供应商;删除也会同步(移除的键不会被再次注入)。该同步严格限定在启用了通用配置的 Claude 供应商,被显式清空时会跳过,且所有失败都是非致命(仅告警)、永不阻断切换。
### Codex 模型映射与「本地路由」开关解耦
Codex 供应商表单向 Claude Code 对齐——模型映射目录现在独立于路由接管,因为原生 Responses 供应商(MiMo、豆包、MiniMax)需要它来做无代理直连,而 Chat 供应商无论如何都走代理。「需要本地路由」开关被移除(它没有后端字段,只是门控目录 / 推理的持久化,等价于「映射是否填了」)。模型映射现在对非官方供应商始终显示、非空即持久化,而推理能力的显示 / 持久化改由 Chat 格式门控。四语(zh / en / ja / zh-TW)文案随之重写。顺带修复了 `useCodexConfigState` 在加载已存供应商时丢掉 `supportsParallelToolCalls` / `inputModalities` / `baseInstructions` 的问题(会在编辑时静默丢失并行工具、图像输入与官方 base instructions)。
### 默认 Sonnet 档升级到 Claude Sonnet 5
把各供应商预设里的默认 Sonnet 档从 `claude-sonnet-4-6` 升级到 `claude-sonnet-5`(覆盖 claude / claude-desktop / hermes / openclaw / opencode 预设与通用 `NEWAPI_DEFAULT_MODELS`),涉及 `ANTHROPIC_MODEL` / `ANTHROPIC_DEFAULT_SONNET_MODEL` / `ANTHROPIC_DEFAULT_OPUS_MODEL` 等键及其带前缀变体。Claude Desktop 的默认路由 sonnet `route_id` 也一并迁移到 `claude-sonnet-5`。非 Anthropic 的 pingpt / gemini / glm / sonnet-4-5)保持不变。
### 豆包带日期 model id 与定价归一化
豆包(DouBaoSeed)预设的 model id 切换到带日期的 `doubao-seed-2-1-pro-260628`(覆盖各应用),因为火山方舟会以 404 拒绝裸名 `doubao-seed-2-1-pro`、只接受完整带日期 id。由于真实用量现在带日期后缀,`strip_model_date_suffix` 扩展为也能剥掉火山的 6 位 YYMMDD 形式(并校验月 01-12、日 01-31 以免误伤 `-123456` 这类非日期版本后缀),从而归一化命中定价表里的裸名 seed 行、修复豆包模型显示 \$0 成本的问题。
###「写入通用配置」更名为「应用通用配置」
原标签「写入通用配置」在数据流向上有歧义(读起来像「把当前配置写进通用配置」),而实际行为相反——是把已存的通用配置片段合并进本供应商配置。复选框在四语(zh / en / ja / zh-TW)里更名为「应用通用配置」,包括所有提示 / 攻略 / 说明引用,日文用户手册与 `README_JA.md` 也一并同步。([#4829](https://github.com/farion1231/cc-switch/pull/4829)
### 其它预设与资源调整
- **OpenClaw 豆包上下文对齐 262144**OpenClaw 的 DouBaoSeed 预设此前硬编码 128000,而 Codex 侧同模型用 262144,导致 OpenClaw 用户窗口偏小;已对齐并加了跨预设一致性测试防止再次漂移。
- **火山 / 豆包 / BytePlus 官网链接订正**:这三个预设的「访问官网」链接被误设成了控制台 / 注册链接,已恢复为干净的产品主页。
- **过大的供应商图标降采样到 256px**:一批捆绑图标此前远大于其 ~32px 的实际渲染尺寸,降采样后显著减小体积、无代码 / 文件名 / 导入改动(如 ZetaAPI 940KB→40KB、relaxcode 1.16MB→42KB),并删除了从未被引用的 1.4MB `dds.svg` 孤儿。
---
## 修复
### 对拒收 `web_search` 的原生 Codex 网关禁用该工具
一些原生 `/responses` 网关的第一方模型不具备 OpenAI 内置的 `web_search` 工具,会以「tool type 'web_search' is not supported」拒绝,而 Codex 默认就会带上该工具,导致硬 400。CC Switch 现在会为这些厂商写入顶层 TOML 行 `web_search = "disabled"`。作用域是一份黑名单(默认开启):仅命中 `base_url` 主机(`xiaomimimo.com``longcat.chat``minimax.io``minimaxi.com`)或模型品牌前缀(`mimo``longcat``minimax``qwen3-coder`)的供应商会被禁用,因此中转真 GPT、豆包、通用 Qwen 及任何未知供应商都保持 Codex 默认。其中 `qwen3-coder` 前缀只压制原生 `qwen3-coder-plus`(百炼 / DashScope 对 coder 系标记内置工具不支持),共享同一主机的通用 Qwen 保持开启;匹配走模型轴(会剥掉聚合器的 `vendor/` 路径段),因此也能兜住硅基流动这类中转拒收厂商模型的情形。选黑名单而非模糊的「是不是 GPT」白名单,是因为误让 `web_search` 保持开启会以硬 400 失败;同时用归属哨兵保证 CC Switch 只会移除由它自己写入的 `disabled` 值,因此存量供应商无需重存、切回也会重新启用。此外顺带把 LongCat-2.0-Preview 预设的上下文窗口从 131072128K)订正为真实的 10485761M)。
### 通用配置片段剥离全部凭据类键
`extract_claude_common_config` 此前只脱敏 `ANTHROPIC_API_KEY``ANTHROPIC_AUTH_TOKEN`,但 Claude 供应商合法地携带其它凭据(`OPENROUTER_API_KEY``GOOGLE_API_KEY`,可能还有 OpenAI / Gemini / AWS Bedrock / Vertex 密钥),这些可能泄漏进共享片段、再被注入到其它供应商。提取现在会按模式匹配并剥掉任何凭据形态的环境变量键(`*_API_KEY` / `*_AUTH_TOKEN` / `*secret*` / `*token*` 等),同时保留 `MAX_OUTPUT_TOKENS` 这类合法可共享的复数 `*_TOKENS` 值。手动「提取」与一次性自动提取路径的同一泄漏也一并堵上。
### 用量脚本凭据仅作显式覆盖持久化
供应商用量脚本存有可选的 `api_key` / `base_url` 字段用于查询配额时覆盖 live 凭据,但它们此前会静默镜像供应商自身的凭据——因此复制供应商或修改主 API key / base URL 后,用量脚本仍 pin 在旧端点旧 key,配额查询一直打向陈旧目标。现在 `ProviderService` 在持久化前会归一化:若脚本的 `api_key``base_url` 与供应商解析出的用量凭据相同(或为空)就清为 `None`,让查询回退到 live 配置;真正不同的覆盖才保留(`token_plan` 类脚本不动)。deeplink 导入路径也加了对应的归一化,前端在更新时会失效相关缓存键让首页用修正后的配置重新查询。([#4654](https://github.com/farion1231/cc-switch/pull/4654)
### Hermes 配置目录在 Windows 上正确解析
CC Switch 此前硬编码 `~/.hermes` 作为 Hermes 配置目录,但 Hermes 自身是按 `HERMES_HOME` 环境变量、再退到平台默认(Windows 上 `%LOCALAPPDATA%\hermes`)解析的。在 Windows 上这意味着 CC Switch 把供应商配置写到了 Hermes 根本不读的路径,导致供应商切换无效。`get_hermes_dir()` 现在镜像 Hermes 自己的解析顺序——显式覆盖、`HERMES_HOME`(原样取用、不做 `~` 展开)、平台默认——从而重新尊重被 #3470 丢掉的 `HERMES_HOME`Hermes 的 Windows 安装器把它作为重定位安装的首要机制)。([#4680](https://github.com/farion1231/cc-switch/pull/4680),参见 #3178#3470
### Linux Wayland:允许覆盖 AppImage 强制的 `GDK_BACKEND=x11`
AppImage 的 GTK 启动钩子无条件导出 `GDK_BACKEND=x11` 以规避一个历史上的原生 Wayland 崩溃。在较新的 Wayland + NVIDIA 环境上,这个被强制的 XWayland 会让 WebKitGTK 网页内容收不到指针事件(标题栏可点、页面却死了)、并在缩放时黑屏,而既有的 `WEBKIT_DISABLE_*` 缓解不起作用,因为根因是被强制的窗口后端而非渲染。`main.rs` 现在会在 GTK 初始化前读取一个可选的 `CC_SWITCH_GDK_BACKEND` 逃生开关(AppImage 的启动钩子从不改动它):不设保持现状(零回归)。遇到上述问题时,用它切回原生 Wayland 启动即可:
```bash
CC_SWITCH_GDK_BACKEND=wayland ./CC-Switch-*.AppImage
```
该覆盖是通用的——若你在平铺式 Wayland 合成器上遇到的是反向的输入问题,则改设为 `CC_SWITCH_GDK_BACKEND=x11`。([#4351](https://github.com/farion1231/cc-switch/pull/4351),修复 #4350
### Claude Desktop / OpenClaw / Hermes 表单显示「获取 API Key」链接
API key 输入框下的「获取 API Key」链接与合作推广块此前只对 claude / codex / gemini / opencode 生效。Claude Desktop 渲染的是不显示它的裸输入框,而 OpenClaw / Hermes 则被两处遗漏挡住(白名单只列了那四个 appId、供应商分类解析只认那四类预设 id 模式)。现在 Claude Desktop 改用共享的 `ApiKeySection`,白名单与分类解析都补上了 claude-desktop / openclaw / hermes;此外 Hermes / OpenClaw 表单不再让「官方」分类禁用 key 输入(这两个应用没有只走 OAuth 的官方供应商,如 Hermes 的 Nous Research 虽是官方但仍需用户自填 key)。
### 去重 Windows 上的 Codex npm 影子命令
在 Windows 上,npm 会把一个工具装成三个同名兄弟文件——`codex.cmd``codex.exe` 和一个无扩展名的 Unix shim `codex`——而 CC Switch 此前把三者都列为候选,导致无法直接执行的无扩展名 shim 被当作多余 / 失败候选去探测。现在仅当相邻没有可执行的 `.cmd` / `.exe` 兄弟时才追加无扩展名路径,路径解析也会优先选可执行的 `.cmd` / `.exe`,从而把版本探测与启动锚定到真正可运行的 Windows shim 上。([#4782](https://github.com/farion1231/cc-switch/pull/4782)
### 长下拉列表的滚动边界
`SelectContent` 弹层此前用 `overflow-hidden` 且没有高度上限,因此选项很多的下拉(如长模型 / 供应商列表)会渲染得比视口还高、把溢出项裁掉且无法触及。现在它设了 `max-h-[min(24rem,var(--radix-select-content-available-height))]``overflow-y-auto`,把内容限制在 24rem 或 Radix 计算出的可用高度内并允许纵向滚动。([#4798](https://github.com/farion1231/cc-switch/pull/4798)
### 日期范围选择器的日历在窄弹层里保持可见
自定义日期范围选择器此前按**视口宽度**(Tailwind `sm:` 640px 断点)切换两列布局(日期字段 | 日历),但弹层被夹在 `100vw - 2rem` 且锚定到触发器,实际可用宽度比视口窄。在窄窗口上,两列布局可能在弹层只放得下一列时被激活,把日历列挤出右边界裁掉(月份头与 7 列里的 4 列被切掉且无法触及)。现在布局改用 **CSS 容器查询**按弹层自身的行内尺寸切换,因此只有当弹层本身窄时才收成一列,让日历在任意窗口宽度下都完整可见。([#4860](https://github.com/farion1231/cc-switch/pull/4860)
---
## 文档
### `CC_SWITCH_GDK_BACKEND` 逃生开关文档
为可选的 `CC_SWITCH_GDK_BACKEND` 环境变量新增了 FAQ 条目,覆盖全部四种 README 语言与 zh / en / ja 用户手册的排障页,说明 Wayland + NVIDIA 用户如何在网页内容「点击失灵 + 缩放黑屏」时切回原生 Wayland,以及平铺式 Wayland 用户如何设为 `x11` 处理反向输入问题。
### Kimi 海外 README 指向 platform.kimi.ai
英语、德语、日语 README 的 Kimi K2.7 Code 合作段落的横幅与内联行动号召改指 `https://platform.kimi.ai?aff=cc-switch`(保留推荐标签),四语 README 也都新增了一行指向 `https://www.kimi.com/code/?aff=cc-switch` 的 Kimi For Coding 订阅推广。
---
## 升级提醒
### 原生 Codex 供应商需重存一次
本版重做了原生 Responses 直连的模型目录生成。如果你此前配过使用原生 Responses(`openai_responses`)的 Codex 供应商,请**重新从预设选择或打开该供应商并保存一次**,以生成新的 `~/.codex/cc-switch-model-catalog.json`——这样 Codex 桌面才能显示自定义模型、工具才可用。此过程无需数据库迁移,也不影响走 `openai_chat` 格式的供应商。
### `web_search` 黑名单是默认行为
对小米 MiMo、美团 LongCat、MiniMax、千问 Qwen3-Coder 这些已知拒收 `web_search` 的原生网关,本版会在切换时自动写入 `web_search = "disabled"`。中转真 GPT、豆包、通用 Qwen 及未知供应商不受影响、保持 Codex 默认。该开关由 CC Switch 用归属哨兵管理,切回到未命中黑名单的供应商会自动恢复,无需手动干预。
### 默认 Sonnet 档变化
新从预设创建的 Claude 类供应商,其默认 Sonnet 档现在指向 `claude-sonnet-5`。已配置好的存量供应商不受影响、配置保持原样;如需改用 Sonnet 5,可重新从预设选择一次并保存。
---
## 风险提示
本版本继续沿用此前版本对反向代理类功能的风险提示。
**Codex OAuth 反向代理**:使用 ChatGPT 订阅的 Codex OAuth 反代可能违反 OpenAI 服务条款,详情见 [v3.13.0 release notes](v3.13.0-zh.md#-风险提示)。
**Codex 第三方供应商 Chat 路由**:通过 CC Switch 本地代理把 Codex 请求转换并转发到第三方供应商时,各供应商对计费、合规与数据留存的约束不同,请在使用前阅读目标供应商的服务条款。
**Claude Desktop 第三方供应商代理切换**:通过 CC Switch 内置代理网关把 Claude Desktop 的请求转到第三方供应商时,同样需要遵守目标供应商的计费、合规与数据留存约束。
用户启用上述功能即表示自行承担相关风险。CC Switch 不对因使用这些功能而导致的任何账号限制、警告或服务暂停承担责任。
---
## 致谢
感谢以下贡献者在 v3.16.5 中提交的功能与修复:
- [#4776](https://github.com/farion1231/cc-switch/pull/4776):新增会话分类视图与分组管理,感谢 @alkaid616
- [#4829](https://github.com/farion1231/cc-switch/pull/4829):把「写入通用配置」更名为「应用通用配置」,感谢 @arichyx
- [#4654](https://github.com/farion1231/cc-switch/pull/4654):让用量脚本凭据仅作显式覆盖持久化,感谢 @yyhhyyyyyy
- [#4680](https://github.com/farion1231/cc-switch/pull/4680):修复 Windows 上 Hermes 供应商配置不生效,感谢 @thisTom
- [#4782](https://github.com/farion1231/cc-switch/pull/4782):去重 Windows 上的 Codex npm 影子命令,感谢 @justjavac
- [#4798](https://github.com/farion1231/cc-switch/pull/4798):修复长下拉列表无法滚动,感谢 @xwil1
- [#4351](https://github.com/farion1231/cc-switch/pull/4351):允许通过 `CC_SWITCH_GDK_BACKEND` 覆盖 AppImage 强制的 `GDK_BACKEND=x11`,感谢 @BoneLiu
- [#4860](https://github.com/farion1231/cc-switch/pull/4860):让日期范围选择器的日历在窄弹层里保持可见,感谢 @SaladDay
也感谢所有在 v3.16.4 发布后反馈 Codex 原生直连、通用配置、凭据复用与平台兼容性问题的用户,很多补丁都来自这些真实使用场景里的复现线索。
---
## 下载与安装
访问 [Releases](https://github.com/farion1231/cc-switch/releases/latest) 下载对应版本。
### 系统要求
| 系统 | 最低版本 | 架构 |
| ------- | -------------------------- | ----------------------------------- |
| Windows | Windows 10 及以上 | x64 / ARM64 |
| macOS | macOS 12 (Monterey) 及以上 | Intel (x64) / Apple Silicon (arm64) |
| Linux | 见下表 | x64 / ARM64 |
### Windows
| 文件 | 说明 |
| ---------------------------------------- | ----------------------------------- |
| `CC-Switch-v3.16.5-Windows.msi` | **推荐** - MSI 安装包,支持自动更新 |
| `CC-Switch-v3.16.5-Windows-Portable.zip` | 便携版,解压即用,不写入注册表 |
Windows ARM64 设备请选择文件名中带 `arm64` 标识的对应制品。
### macOS
| 文件 | 说明 |
| -------------------------------- | --------------------------------------------- |
| `CC-Switch-v3.16.5-macOS.dmg` | **推荐** - DMG 安装包,拖入 Applications 即可 |
| `CC-Switch-v3.16.5-macOS.zip` | 解压后拖入 ApplicationsUniversal Binary |
| `CC-Switch-v3.16.5-macOS.tar.gz` | 用于 Homebrew 安装和自动更新 |
Homebrew 安装:
```bash
brew install --cask cc-switch
```
更新:
```bash
brew upgrade --cask cc-switch
```
### Linux
Linux 资产同时提供 **x86_64****ARM64**`aarch64`)两种架构。资产文件名中包含架构标识,请按你机器的 `uname -m` 输出选择对应版本:
- `CC-Switch-v3.16.5-Linux-x86_64.AppImage` / `.deb` / `.rpm`
- `CC-Switch-v3.16.5-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
@@ -554,7 +554,7 @@ When editing Claude providers, a set of **quick toggles** is available above the
When a toggle is unchecked, its corresponding config entry is removed entirely. Changes are reflected in the JSON editor in real-time.
Additionally, the **Write Common Config** checkbox enables merging a global config snippet into the provider. Click **Edit Common Config** to customize the shared snippet.
Additionally, the **Apply Common Config** checkbox enables merging a global config snippet into the provider. Click **Edit Common Config** to customize the shared snippet.
### Codex Local Routing and Model Mapping
@@ -29,6 +29,22 @@ chmod +x CC-Switch-*.AppImage
./CC-Switch-*.AppImage --no-sandbox
```
### Linux: Clicks Don't Register / Black Screen on Resize (Wayland + NVIDIA)
**Problem**: The web content area is completely unclickable (the title-bar minimize/maximize/close buttons still work), and the window black-screens on resize or maximize-restore. Common on Wayland sessions with an NVIDIA GPU.
**Cause**: The AppImage's GTK launch hook unconditionally forces `GDK_BACKEND=x11` (XWayland) to dodge a historical native-Wayland crash. On newer Wayland + NVIDIA setups, forced XWayland leaves the WebKitGTK web content unable to receive pointer events. The existing `WEBKIT_DISABLE_*` mitigations don't help here because the root cause is the forced window backend, not rendering.
**Solution**: Use the dedicated `CC_SWITCH_GDK_BACKEND` environment variable to switch back to native Wayland (it is read before GTK init, and the hook never overrides it):
```bash
CC_SWITCH_GDK_BACKEND=wayland ./CC-Switch-*.AppImage
```
- When launching from a desktop icon, add it to the `.desktop` `Exec=` line (e.g. `env CC_SWITCH_GDK_BACKEND=wayland /path/to/AppImage`) or set it in your session environment — otherwise an icon launch won't see the variable.
- The variable is generic: on tiling Wayland compositors (sway/Hyprland) where clicks don't register, set `CC_SWITCH_GDK_BACKEND=x11` instead.
- Leaving it unset behaves exactly as before (still x11), with no side effects.
## Provider Issues
### Provider Switch Doesn't Take Effect
+1 -1
View File
@@ -554,7 +554,7 @@ Claude プロバイダーの編集時、JSON エディタの上部に **クイ
トグルのチェックを外すと、対応する設定エントリが完全に削除されます。変更は JSON エディタにリアルタイムで反映されます。
また、**共通設定を書き込み** チェックボックスを有効にすると、グローバル設定スニペットをプロバイダーにマージできます。**共通設定を編集** をクリックして共有スニペットをカスタマイズできます。
また、**共通設定を適用** チェックボックスを有効にすると、グローバル設定スニペットをプロバイダーにマージできます。**共通設定を編集** をクリックして共有スニペットをカスタマイズできます。
### Codex ローカルルーティングとモデルマッピング
@@ -29,6 +29,22 @@ chmod +x CC-Switch-*.AppImage
./CC-Switch-*.AppImage --no-sandbox
```
### Linux:クリックが効かない / リサイズで黒画面(Wayland + NVIDIA
**問題**:Web コンテンツ領域がまったくクリックできません(タイトルバーの最小化/最大化/閉じるボタンは動作します)。ウィンドウのリサイズや最大化-復元後に黒画面になります。Wayland セッション + NVIDIA GPU でよく発生します。
**原因**AppImage の GTK 起動フックが、過去のネイティブ Wayland クラッシュを避けるために `GDK_BACKEND=x11`(XWayland)を無条件で強制します。新しい Wayland + NVIDIA 環境では、強制された XWayland によって WebKitGTK の Web コンテンツがポインタイベントを受け取れなくなります。既存の `WEBKIT_DISABLE_*` の緩和策は、根本原因が強制されたウィンドウバックエンドであり描画ではないため、ここでは効きません。
**解決方法**:専用の環境変数 `CC_SWITCH_GDK_BACKEND` でネイティブ Wayland に戻します(GTK 初期化前に読み取られ、フックが上書きしません):
```bash
CC_SWITCH_GDK_BACKEND=wayland ./CC-Switch-*.AppImage
```
- デスクトップアイコンから起動する場合は、`.desktop``Exec=` 行に追記するか(例:`env CC_SWITCH_GDK_BACKEND=wayland /path/to/AppImage`)、セッション環境で設定してください。そうしないとアイコン起動では変数が読み取られません。
- この変数は汎用です:タイル型 Wayland コンポジタ(sway/Hyprland)でクリックが効かない場合は、`CC_SWITCH_GDK_BACKEND=x11` を設定してください。
- 未設定の場合は現状とまったく同じ動作(x11 のまま)で、副作用はありません。
## プロバイダーに関する問題
### プロバイダーを切り替えても反映されない
+1 -1
View File
@@ -554,7 +554,7 @@ v3.13.0 起新增的高级选项。默认情况下,CC Switch 会把配置的 `
取消勾选开关时,对应的配置项会被完全移除。更改会实时反映在 JSON 编辑器中。
此外,**写入通用配置** 复选框可将全局配置片段合并到供应商中。点击 **编辑通用配置** 可自定义共享的配置片段。
此外,**应用通用配置** 复选框可将全局配置片段合并到供应商中。点击 **编辑通用配置** 可自定义共享的配置片段。
### Codex 本地路由与模型映射
@@ -29,6 +29,22 @@ chmod +x CC-Switch-*.AppImage
./CC-Switch-*.AppImage --no-sandbox
```
### Linux 点击无响应 / 缩放后黑屏(Wayland + NVIDIA
**问题**:主界面网页内容区完全点不动(标题栏的最小化/最大化/关闭仍可点),窗口缩放或最大化-还原后黑屏。常见于 Wayland 会话 + NVIDIA 显卡。
**原因**AppImage 的 GTK 启动钩子会无条件强制 `GDK_BACKEND=x11`(走 XWayland)以规避历史上的原生 Wayland 崩溃;但在较新的 Wayland + NVIDIA 环境下,强制 XWayland 反而使 WebKitGTK 的网页内容收不到指针事件。现有的 `WEBKIT_DISABLE_*` 缓解措施对此无效,因为根因是被强制的窗口后端,而非渲染。
**解决方法**:用专用环境变量 `CC_SWITCH_GDK_BACKEND` 切回原生 Wayland(该开关在 GTK 初始化前生效,钩子不会覆盖它):
```bash
CC_SWITCH_GDK_BACKEND=wayland ./CC-Switch-*.AppImage
```
- 从桌面图标启动时,把它写进 `.desktop``Exec=` 行(如 `env CC_SWITCH_GDK_BACKEND=wayland /path/to/AppImage`),或在会话环境中设置,否则图标启动读不到该变量。
- 该变量是通用的:在 tiling Wayland 合成器(sway/Hyprland)下若反而出现点击失效,可设 `CC_SWITCH_GDK_BACKEND=x11`
- 不设置时行为与现状完全一致(仍走 x11),无副作用。
## 供应商问题
### 切换供应商后不生效
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "cc-switch",
"version": "3.16.4",
"version": "3.16.5",
"description": "All-in-One Assistant for Claude Code, Codex & Gemini CLI",
"type": "module",
"scripts": {
+1 -1
View File
@@ -758,7 +758,7 @@ dependencies = [
[[package]]
name = "cc-switch"
version = "3.16.4"
version = "3.16.5"
dependencies = [
"anyhow",
"arboard",
+1 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "cc-switch"
version = "3.16.4"
version = "3.16.5"
description = "All-in-One Assistant for Claude Code, Codex & Gemini CLI"
authors = ["Jason Young"]
license = "MIT"
+3 -3
View File
@@ -46,7 +46,7 @@ pub struct ClaudeDesktopDefaultRoute {
pub const DEFAULT_PROXY_ROUTES: &[ClaudeDesktopDefaultRoute] = &[
ClaudeDesktopDefaultRoute {
route_id: "claude-sonnet-4-6",
route_id: "claude-sonnet-5",
env_key: "ANTHROPIC_DEFAULT_SONNET_MODEL",
supports_1m: true,
},
@@ -1966,9 +1966,9 @@ mod tests {
},
),
(
"claude-sonnet-4-6".to_string(),
"claude-sonnet-5".to_string(),
ClaudeDesktopModelRoute {
model: "claude-sonnet-4-6".to_string(),
model: "claude-sonnet-5".to_string(),
label_override: None,
supports_1m: Some(false),
},
+544 -25
View File
@@ -13,8 +13,115 @@ use toml_edit::DocumentMut;
pub const CC_SWITCH_CODEX_MODEL_PROVIDER_ID: &str = "custom";
pub const CC_SWITCH_CODEX_MODEL_CATALOG_FILENAME: &str = "cc-switch-model-catalog.json";
/// Top-level `config.toml` key that controls Codex's built-in web-search tool.
const CODEX_WEB_SEARCH_FIELD: &str = "web_search";
/// Value that disables the web-search tool. Some native `/responses` gateways
/// reject a `web_search` tool with `responses_feature_not_supported` ("tool type
/// 'web_search' is not supported by this gateway phase"), so for those we write
/// this per the vendors' official Codex docs. Also doubles as cc-switch's
/// ownership sentinel: we only ever remove a `web_search` key whose value equals
/// this string, never a user's own setting.
const CODEX_WEB_SEARCH_DISABLED: &str = "disabled";
/// Native `/responses` gateways whose first-party models do NOT support the Codex
/// `web_search` hosted tool. A BLACKLIST (default-on): everything not listed keeps
/// Codex's default, so relays/aggregators fronting real GPT — and any unknown
/// provider — are never touched. This avoids a whitelist's dangerous failure mode
/// (a fragile "is this GPT?" heuristic wrongly keeping web_search ON → hard 400);
/// the blacklist's failure mode is the safe, recoverable one (a not-yet-listed
/// broken gateway errors once → add it here).
///
/// Matched two ways so an aggregator (e.g. SiliconFlow) fronting these vendors'
/// models is also caught:
/// - `base_url` host substring, and
/// - the model id's brand prefix (after stripping any `vendor/` path segment).
///
/// Verified 2026-06-28 doc audit — reject: MiMo (hard 400), LongCat (official
/// config ships `web_search = "disabled"`), MiniMax (tool-type enum `['function']`
/// only), and Qwen3-Coder models (百炼 marks built-in tools unsupported for
/// the coder series). Deliberately NOT listed by host: 火山方舟豆包, general
/// 阿里百炼 Qwen models that support built-in web_search, and GPT-native relays.
const CODEX_WEB_SEARCH_REJECT_HOSTS: &[&str] = &[
"xiaomimimo.com", // Xiaomi MiMo (api.xiaomimimo.com, token-plan-cn.xiaomimimo.com)
"longcat.chat", // Meituan LongCat (api.longcat.chat)
"minimax.io", // MiniMax global (api.minimax.io)
"minimaxi.com", // MiniMax CN (api.minimaxi.com)
];
/// Brand prefixes of models whose native gateways reject `web_search`, matched
/// against the model id's last `/`-segment so aggregator ids like
/// `MiniMaxAI/MiniMax-M3` are caught. Exact brand names (not a fuzzy heuristic),
/// so a supporting gateway is never wrongly matched.
const CODEX_WEB_SEARCH_REJECT_MODEL_PREFIXES: &[&str] =
&["mimo", "longcat", "minimax", "qwen3-coder"];
/// Top-level `model` id from a Codex `config.toml`.
fn codex_top_level_model(config_text: &str) -> Option<String> {
let doc = config_text.parse::<toml::Value>().ok()?;
doc.get("model")
.and_then(|value| value.as_str())
.map(|value| value.trim().to_string())
.filter(|value| !value.is_empty())
}
/// Whether a native `/responses` provider's gateway is known to reject the Codex
/// `web_search` hosted tool — by `base_url` host OR by the active model's brand
/// (so an aggregator fronting a reject vendor's model is caught too). Driven by
/// the live `config.toml`, so it applies to existing providers without a re-save.
fn codex_native_gateway_rejects_web_search(config_text: &str) -> bool {
if let Some(base_url) = extract_codex_base_url(config_text) {
let base_url = base_url.to_ascii_lowercase();
if CODEX_WEB_SEARCH_REJECT_HOSTS
.iter()
.any(|host| base_url.contains(host))
{
return true;
}
}
if let Some(model) = codex_top_level_model(config_text) {
let model = model.to_ascii_lowercase();
// Strip any aggregator "vendor/" prefix, e.g. "MiniMaxAI/MiniMax-M3"
// or "qwen/qwen3-coder-plus".
let model = model.rsplit('/').next().unwrap_or(model.as_str());
if CODEX_WEB_SEARCH_REJECT_MODEL_PREFIXES
.iter()
.any(|prefix| model.starts_with(prefix))
{
return true;
}
}
false
}
const CODEX_MODEL_CATALOG_TEMPLATE_SLUG: &str = "gpt-5.5";
/// Which Codex tool surface the generated model catalog should target.
///
/// - `ProxyChat`: cc-switch's proxy takes over and converts Responses<->Chat,
/// so the catalog keeps Codex's default tool set (incl. the freeform
/// `apply_patch` custom tool, which the proxy rewrites to a function tool).
/// - `NativeResponses`: Codex talks directly to a provider's native
/// `/responses` endpoint (no proxy). Such gateways (e.g. Xiaomi MiMo,
/// MiniMax) reject `type=="custom"` tools, so the catalog must suppress the
/// freeform `apply_patch` and rely on `shell_type="shell_command"` for edits.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum CodexCatalogToolProfile {
ProxyChat,
NativeResponses,
}
impl CodexCatalogToolProfile {
/// Pick the catalog tool profile from a provider's `apiFormat` meta value.
/// Native (direct) Responses providers must suppress the custom apply_patch
/// tool; everything else keeps the proxy-chat behavior.
pub fn from_api_format(api_format: Option<&str>) -> Self {
match api_format {
Some("openai_responses") => CodexCatalogToolProfile::NativeResponses,
_ => CodexCatalogToolProfile::ProxyChat,
}
}
}
/// 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.
@@ -313,27 +420,63 @@ fn extract_codex_top_level_u64(config_text: &str, field: &str) -> Option<u64> {
fn codex_catalog_model_entry(
template: &Value,
model: &str,
display_name: &str,
context_window: u64,
spec: &CodexCatalogModelSpec,
priority: usize,
profile: CodexCatalogToolProfile,
) -> Value {
let mut entry = template.clone();
let Some(entry_obj) = entry.as_object_mut() else {
return json!({});
};
entry_obj.insert("slug".to_string(), json!(model));
entry_obj.insert("display_name".to_string(), json!(display_name));
entry_obj.insert("description".to_string(), json!(display_name));
entry_obj.insert("context_window".to_string(), json!(context_window));
entry_obj.insert("max_context_window".to_string(), json!(context_window));
entry_obj.insert("slug".to_string(), json!(spec.model));
entry_obj.insert("display_name".to_string(), json!(spec.display_name));
entry_obj.insert("description".to_string(), json!(spec.display_name));
entry_obj.insert("context_window".to_string(), json!(spec.context_window));
entry_obj.insert("max_context_window".to_string(), json!(spec.context_window));
entry_obj.insert("priority".to_string(), json!(1000 + priority));
entry_obj.insert("additional_speed_tiers".to_string(), json!([]));
entry_obj.insert("service_tiers".to_string(), json!([]));
entry_obj.insert("availability_nux".to_string(), Value::Null);
entry_obj.insert("upgrade".to_string(), Value::Null);
if profile == CodexCatalogToolProfile::NativeResponses {
// Native `/responses` gateways reject Codex's freeform `apply_patch`
// (type=="custom") tool. Strip any key that would make Codex emit a
// custom/freeform tool, and rely on shell_type="shell_command" for
// edits. Defensive even though the native template is already clean
// (guards against template drift / an accidental gpt-5.5 clone).
//
// NOTE: `base_instructions` is NOT stripped — Codex's catalog parser
// treats it as a REQUIRED field and refuses to load the file without
// it ("missing field `base_instructions`"). The template carries a
// neutral identity default; per-vendor official text overrides below.
for key in [
"apply_patch_tool_type",
"web_search_tool_type",
"tools",
"model_messages",
] {
entry_obj.remove(key);
}
entry_obj.insert("shell_type".to_string(), json!("shell_command"));
if let Some(base_instructions) = spec
.base_instructions
.as_deref()
.map(str::trim)
.filter(|s| !s.is_empty())
{
entry_obj.insert("base_instructions".to_string(), json!(base_instructions));
}
if let Some(parallel) = spec.supports_parallel_tool_calls {
entry_obj.insert("supports_parallel_tool_calls".to_string(), json!(parallel));
}
if let Some(modalities) = &spec.input_modalities {
entry_obj.insert("input_modalities".to_string(), json!(modalities));
}
}
entry
}
@@ -342,6 +485,18 @@ struct CodexCatalogModelSpec {
model: String,
display_name: String,
context_window: u64,
/// Per-row override for the native template's `supports_parallel_tool_calls`
/// (e.g. MiniMax=true, MiMo=false). Only consulted for `NativeResponses`.
supports_parallel_tool_calls: Option<bool>,
/// Per-row override for the native template's `input_modalities`
/// (e.g. `["text","image"]`). Only consulted for `NativeResponses`.
input_modalities: Option<Vec<String>>,
/// Per-row override for the native template's `base_instructions` (the
/// model identity / system preamble). Carries each vendor's OFFICIAL value
/// (e.g. MiMo "developed by Xiaomi", MiniMax "based on MiniMax-M3"); falls
/// back to the template default when absent. Only consulted for
/// `NativeResponses`.
base_instructions: Option<String>,
}
fn codex_catalog_model_specs(settings: &Value, config_text: &str) -> Vec<CodexCatalogModelSpec> {
@@ -386,10 +541,38 @@ fn codex_catalog_model_specs(settings: &Value, config_text: &str) -> Vec<CodexCa
)
.unwrap_or(default_context_window);
let supports_parallel_tool_calls = model_config
.get("supportsParallelToolCalls")
.or_else(|| model_config.get("supports_parallel_tool_calls"))
.and_then(|value| value.as_bool());
let input_modalities = model_config
.get("inputModalities")
.or_else(|| model_config.get("input_modalities"))
.and_then(|value| value.as_array())
.map(|items| {
items
.iter()
.filter_map(|item| item.as_str())
.map(str::to_string)
.collect::<Vec<_>>()
})
.filter(|items| !items.is_empty());
let base_instructions = model_config
.get("baseInstructions")
.or_else(|| model_config.get("base_instructions"))
.and_then(|value| value.as_str())
.map(str::trim)
.filter(|text| !text.is_empty())
.map(str::to_string);
specs.push(CodexCatalogModelSpec {
model: model.to_string(),
display_name: display_name.to_string(),
context_window,
supports_parallel_tool_calls,
input_modalities,
base_instructions,
});
}
@@ -628,6 +811,18 @@ fn load_codex_model_template_static() -> Option<Value> {
}
}
/// Bundled clean template for native `/responses` providers. Unlike the
/// gpt-5.5 template it carries NO freeform `apply_patch` / `web_search` tool
/// declarations and no GPT-5 base_instructions, so Codex never emits a
/// `type=="custom"` tool that native gateways (MiMo/MiniMax/…) reject. Edits
/// flow through `shell_type="shell_command"` instead. We deliberately do NOT
/// fall back to `models_cache.json` here (that would reintroduce gpt-5.5's
/// freeform apply_patch).
fn load_codex_native_responses_template() -> Value {
let text = include_str!("resources/codex_native_responses_template.json");
serde_json::from_str(text).expect("bundled codex native responses template must be valid JSON")
}
fn load_codex_model_catalog_template() -> Result<Value, AppError> {
// ① models_cache.json (created by Codex when it connects to OpenAI)
if let Some(template) = load_codex_model_template_from_cache()? {
@@ -647,19 +842,15 @@ fn load_codex_model_catalog_template() -> Result<Value, AppError> {
)))
}
fn codex_model_catalog_from_specs(specs: &[CodexCatalogModelSpec], template: &Value) -> Value {
fn codex_model_catalog_from_specs(
specs: &[CodexCatalogModelSpec],
template: &Value,
profile: CodexCatalogToolProfile,
) -> Value {
let entries: Vec<Value> = specs
.iter()
.enumerate()
.map(|(index, spec)| {
codex_catalog_model_entry(
template,
&spec.model,
&spec.display_name,
spec.context_window,
index,
)
})
.map(|(index, spec)| codex_catalog_model_entry(template, spec, index, profile))
.collect();
json!({ "models": entries })
@@ -668,14 +859,23 @@ fn codex_model_catalog_from_specs(specs: &[CodexCatalogModelSpec], template: &Va
fn codex_model_catalog_from_settings(
settings: &Value,
config_text: &str,
profile: CodexCatalogToolProfile,
) -> Result<Option<Value>, AppError> {
let specs = codex_catalog_model_specs(settings, config_text);
if specs.is_empty() {
return Ok(None);
}
let template = load_codex_model_catalog_template()?;
Ok(Some(codex_model_catalog_from_specs(&specs, &template)))
// Native providers use the bundled clean template (no freeform apply_patch,
// no cache dependency); proxy-chat providers keep cloning Codex's gpt-5.5
// entry so the proxy can rewrite custom<->function tools as before.
let template = match profile {
CodexCatalogToolProfile::NativeResponses => load_codex_native_responses_template(),
CodexCatalogToolProfile::ProxyChat => load_codex_model_catalog_template()?,
};
Ok(Some(codex_model_catalog_from_specs(
&specs, &template, profile,
)))
}
fn set_codex_model_catalog_json_field(
@@ -708,20 +908,60 @@ fn set_codex_model_catalog_json_field(
Ok(doc.to_string())
}
/// Pure toggle for the top-level `web_search` field that turns Codex's built-in
/// web-search tool off. When `disable` is true we write `web_search = "disabled"`
/// (the catalog's `supports_search_tool` does NOT gate this — the request-time
/// tool comes from the config, defaulting on). When false we *remove* the field,
/// but only when it carries cc-switch's own `"disabled"` sentinel, so switching
/// back to a web-search-capable provider re-enables it without clobbering a
/// user's manual setting.
///
/// The caller decides `disable` (see `codex_native_gateway_rejects_web_search`);
/// lifecycle is bound to the cc-switch catalog pointer so the field is set/cleaned
/// up wherever the native catalog is written/removed.
fn set_codex_native_web_search_field(config_text: &str, disable: bool) -> Result<String, AppError> {
let mut doc = config_text
.parse::<DocumentMut>()
.map_err(|e| AppError::Message(format!("Invalid Codex config.toml: {e}")))?;
if disable {
doc[CODEX_WEB_SEARCH_FIELD] = toml_edit::value(CODEX_WEB_SEARCH_DISABLED);
} else {
let owned = doc
.get(CODEX_WEB_SEARCH_FIELD)
.and_then(|item| item.as_str())
== Some(CODEX_WEB_SEARCH_DISABLED);
if owned {
doc.as_table_mut().remove(CODEX_WEB_SEARCH_FIELD);
}
}
Ok(doc.to_string())
}
/// Generate Codex `model_catalog_json` from provider settings and inject/remove
/// the top-level TOML field that points Codex to the generated file.
pub fn prepare_codex_config_text_with_model_catalog(
settings: &Value,
config_text: &str,
profile: CodexCatalogToolProfile,
) -> Result<String, AppError> {
let catalog_path = get_codex_model_catalog_path();
if let Some(catalog) = codex_model_catalog_from_settings(settings, config_text)? {
if let Some(catalog) = codex_model_catalog_from_settings(settings, config_text, profile)? {
let config_text = set_codex_model_catalog_json_field(config_text, Some(&catalog_path))?;
// Disable web_search only for native gateways on the reject blacklist
// (MiMo/LongCat/MiniMax by host or model brand; Qwen3-Coder by model).
// Everything else — relays, DouBao, web-search-capable Qwen models,
// unknown providers — keeps Codex's default.
let disable_web_search = profile == CodexCatalogToolProfile::NativeResponses
&& codex_native_gateway_rejects_web_search(&config_text);
let config_text = set_codex_native_web_search_field(&config_text, disable_web_search)?;
write_json_file(&catalog_path, &catalog)?;
Ok(config_text)
} else {
set_codex_model_catalog_json_field(config_text, None)
let config_text = set_codex_model_catalog_json_field(config_text, None)?;
set_codex_native_web_search_field(&config_text, false)
}
}
@@ -839,6 +1079,26 @@ fn build_simplified_catalog_from_texts(config_text: &str, catalog_text: &str) ->
obj.insert("contextWindow".to_string(), json!(context_window));
}
// Preserve native-profile per-row overrides so a DB-SSOT-missing
// fallback round-trip doesn't silently drop them (they are ignored by
// the ProxyChat profile, so carrying them is harmless).
if let Some(parallel) = entry
.get("supports_parallel_tool_calls")
.and_then(|v| v.as_bool())
{
obj.insert("supportsParallelToolCalls".to_string(), json!(parallel));
}
if let Some(modalities) = entry.get("input_modalities").and_then(|v| v.as_array()) {
let mods: Vec<String> = modalities
.iter()
.filter_map(|m| m.as_str())
.map(str::to_string)
.collect();
if !mods.is_empty() {
obj.insert("inputModalities".to_string(), json!(mods));
}
}
entries.push(Value::Object(obj));
}
@@ -876,9 +1136,10 @@ fn build_simplified_catalog_from_texts(config_text: &str, catalog_text: &str) ->
pub fn prepare_codex_live_config_text_with_optional_catalog(
settings: &Value,
config_text: &str,
profile: CodexCatalogToolProfile,
) -> Result<String, AppError> {
if settings.get("modelCatalog").is_some() {
prepare_codex_config_text_with_model_catalog(settings, config_text)
prepare_codex_config_text_with_model_catalog(settings, config_text, profile)
} else {
Ok(config_text.to_string())
}
@@ -889,9 +1150,10 @@ pub fn write_codex_provider_live_with_catalog(
category: Option<&str>,
auth: &Value,
config_text: Option<&str>,
profile: CodexCatalogToolProfile,
) -> Result<(), AppError> {
let prepared_config = config_text
.map(|text| prepare_codex_config_text_with_model_catalog(settings, text))
.map(|text| prepare_codex_config_text_with_model_catalog(settings, text, profile))
.transpose()?;
write_codex_live_for_provider(category, auth, prepared_config.as_deref())
@@ -2070,7 +2332,8 @@ base_url = "https://production.api/v1"
}
});
let specs = codex_catalog_model_specs(&settings, r#"model_context_window = 128000"#);
let catalog = codex_model_catalog_from_specs(&specs, &template);
let catalog =
codex_model_catalog_from_specs(&specs, &template, CodexCatalogToolProfile::ProxyChat);
let models = catalog
.get("models")
.and_then(|value| value.as_array())
@@ -2121,6 +2384,137 @@ base_url = "https://production.api/v1"
);
}
#[test]
fn native_responses_profile_suppresses_apply_patch_and_keeps_shell() {
// Native (direct) /responses providers must NOT emit a freeform
// apply_patch (type=="custom") tool — gateways like MiMo reject it.
// The native profile uses the bundled clean template and relies on
// shell_type="shell_command" for edits, plus per-row overrides.
let settings = json!({
"modelCatalog": {
"models": [
{
"model": "MiniMax-M3",
"displayName": "MiniMax-M3",
"contextWindow": 1_000_000,
"supportsParallelToolCalls": true,
"inputModalities": ["text", "image"],
"baseInstructions": "You are Codex, a coding agent based on MiniMax-M3."
}
]
}
});
let catalog = codex_model_catalog_from_settings(
&settings,
"",
CodexCatalogToolProfile::NativeResponses,
)
.expect("native catalog generation should not error")
.expect("non-empty modelCatalog must yield a catalog");
let entry = &catalog["models"][0];
assert_eq!(
entry.get("slug").and_then(|v| v.as_str()),
Some("MiniMax-M3")
);
assert_eq!(
entry.get("shell_type").and_then(|v| v.as_str()),
Some("shell_command"),
"native entries edit via shell, not the custom apply_patch tool"
);
assert!(
entry.get("apply_patch_tool_type").is_none(),
"native entries must NOT declare a freeform apply_patch tool"
);
// `base_instructions` is REQUIRED by Codex's catalog parser, so it must
// be present — and the per-row official override must win over the
// template default.
assert_eq!(
entry.get("base_instructions").and_then(|v| v.as_str()),
Some("You are Codex, a coding agent based on MiniMax-M3."),
"per-row baseInstructions override must apply (and field must exist)"
);
assert!(
entry.get("model_messages").is_none(),
"native entries must not carry the gpt-5.5 model_messages persona text"
);
assert_eq!(
entry.get("supports_parallel_tool_calls"),
Some(&json!(true)),
"per-row supportsParallelToolCalls override must apply"
);
assert_eq!(
entry.get("input_modalities"),
Some(&json!(["text", "image"])),
"per-row inputModalities override must apply"
);
assert_eq!(
entry.get("context_window").and_then(|v| v.as_u64()),
Some(1_000_000)
);
}
#[test]
fn native_responses_catalog_always_carries_base_instructions() {
// Regression guard for the "missing field `base_instructions`" parse
// error: Codex refuses to load a model catalog whose entries lack
// base_instructions. Synthesized presets carry no per-row override, so
// the entry MUST inherit the template's neutral default rather than
// dropping the field entirely.
let settings = json!({
"modelCatalog": { "models": [{ "model": "qwen3-coder-plus" }] }
});
let catalog = codex_model_catalog_from_settings(
&settings,
"",
CodexCatalogToolProfile::NativeResponses,
)
.expect("native catalog generation should not error")
.expect("non-empty modelCatalog must yield a catalog");
let base = catalog["models"][0]
.get("base_instructions")
.and_then(|v| v.as_str());
assert!(
base.is_some_and(|s| !s.trim().is_empty()),
"every native entry must carry a non-empty base_instructions (Codex requires it)"
);
}
#[test]
fn proxy_chat_profile_still_keeps_apply_patch() {
// Regression guard for Mode A: the proxy-chat profile must keep the
// freeform apply_patch tool (the proxy rewrites custom<->function).
let template = load_codex_native_responses_template();
let specs = vec![CodexCatalogModelSpec {
model: "x".to_string(),
display_name: "x".to_string(),
context_window: 128_000,
supports_parallel_tool_calls: None,
input_modalities: None,
base_instructions: None,
}];
// Using a gpt-5.5-shaped template under ProxyChat must NOT strip
// apply_patch_tool_type. (The native template lacks it, so synthesize
// one with the field present to prove ProxyChat leaves it intact.)
let mut proxy_template = template.clone();
proxy_template["apply_patch_tool_type"] = json!("freeform");
let catalog = codex_model_catalog_from_specs(
&specs,
&proxy_template,
CodexCatalogToolProfile::ProxyChat,
);
assert_eq!(
catalog["models"][0]
.get("apply_patch_tool_type")
.and_then(|v| v.as_str()),
Some("freeform"),
"ProxyChat must preserve apply_patch_tool_type (no native stripping)"
);
}
#[test]
fn model_catalog_json_field_writes_relative_filename() {
let input = r#"model_provider = "any"
@@ -2148,6 +2542,131 @@ name = "any"
);
}
#[test]
fn native_web_search_field_disables_at_top_level() {
// Native `/responses` gateways reject the web_search tool, so the
// NativeResponses profile must write the top-level disable line even
// when sections are present (it must NOT land inside a section).
let input = r#"model_provider = "custom"
[model_providers.custom]
name = "xiaomi_mimo"
"#;
let result = set_codex_native_web_search_field(input, true).unwrap();
let parsed: toml::Value = toml::from_str(&result).unwrap();
assert_eq!(
parsed.get("web_search").and_then(|value| value.as_str()),
Some("disabled")
);
assert!(
parsed
.get("model_providers")
.and_then(|value| value.get("custom"))
.and_then(|value| value.get("web_search"))
.is_none(),
"web_search should stay top-level"
);
}
#[test]
fn native_web_search_field_removes_own_sentinel_when_not_disabled() {
// Switching away from a native provider must re-enable web search by
// removing cc-switch's own "disabled" sentinel.
let input = r#"model = "gpt-5.5"
web_search = "disabled"
"#;
let result = set_codex_native_web_search_field(input, false).unwrap();
let parsed: toml::Value = toml::from_str(&result).unwrap();
assert!(
parsed.get("web_search").is_none(),
"cc-switch's disabled sentinel should be removed when not native"
);
}
#[test]
fn native_web_search_field_preserves_user_value() {
// A user's own web_search value must never be clobbered by cleanup,
// only cc-switch's "disabled" sentinel is owned/removable.
let input = r#"web_search = "enabled"
"#;
let result = set_codex_native_web_search_field(input, false).unwrap();
let parsed: toml::Value = toml::from_str(&result).unwrap();
assert_eq!(
parsed.get("web_search").and_then(|value| value.as_str()),
Some("enabled"),
"a user-set web_search value must be preserved"
);
}
#[test]
fn web_search_blacklist_disables_only_known_reject_gateways() {
let cfg = |model: &str, base_url: &str| {
format!(
"model_provider = \"custom\"\nmodel = \"{model}\"\n\n[model_providers.custom]\nname = \"x\"\nbase_url = \"{base_url}\"\nwire_api = \"responses\"\n"
)
};
// Blacklisted by host (first-party reject gateways) → disable.
for (model, host) in [
("mimo-v2.5-pro", "https://api.xiaomimimo.com/v1"),
("mimo-v2.5", "https://token-plan-cn.xiaomimimo.com/v1"),
("LongCat-2.0-Preview", "https://api.longcat.chat/openai/v1"),
("MiniMax-M3", "https://api.minimax.io/v1"),
("MiniMax-M3", "https://api.minimaxi.com/v1"),
] {
assert!(
codex_native_gateway_rejects_web_search(&cfg(model, host)),
"{host} should be blacklisted"
);
}
// Blacklisted by MODEL brand even on an aggregator host (SiliconFlow
// fronting a reject vendor's model) → disable.
for (model, host) in [
("MiniMax-M3", "https://api.siliconflow.cn/v1"),
("MiniMaxAI/MiniMax-M3", "https://api.siliconflow.cn/v1"),
("mimo-v2.5-pro", "https://some-aggregator.example/v1"),
(
"qwen/qwen3-coder-plus",
"https://some-aggregator.example/v1",
),
] {
assert!(
codex_native_gateway_rejects_web_search(&cfg(model, host)),
"{model} @ {host} should be blacklisted by model brand"
);
}
// Qwen3-Coder is blacklisted by model, not by DashScope host. This keeps
// general Qwen models that support built-in web_search on the same host
// enabled while protecting the native qwen3-coder-plus preset.
assert!(codex_native_gateway_rejects_web_search(&cfg(
"qwen3-coder-plus",
"https://dashscope.aliyuncs.com/compatible-mode/v1",
)));
assert!(!codex_native_gateway_rejects_web_search(&cfg(
"qwen3.7-plus",
"https://dashscope.aliyuncs.com/compatible-mode/v1",
)));
// NOT blacklisted → keep Codex default (relays/GPT, DouBao, general Qwen,
// and any unknown provider incl. an aggregator serving a non-reject model).
for (model, host) in [
("gpt-5.5", "https://www.packyapi.com/v1"),
("gpt-5-codex", "https://aihubmix.com/v1"),
(
"doubao-seed-2-1-pro-260628",
"https://ark.cn-beijing.volces.com/api/v3",
),
("Pro/moonshotai/Kimi-K2.6", "https://api.siliconflow.cn/v1"),
] {
assert!(
!codex_native_gateway_rejects_web_search(&cfg(model, host)),
"{model} @ {host} should NOT be blacklisted"
);
}
}
#[test]
fn resolve_catalog_path_returns_none_when_config_missing_field() {
let generated = PathBuf::from("/tmp/.codex/cc-switch-model-catalog.json");
+52 -4
View File
@@ -1445,11 +1445,15 @@ fn opencode_extra_search_paths(
fn tool_executable_candidates(tool: &str, dir: &Path) -> Vec<std::path::PathBuf> {
#[cfg(target_os = "windows")]
{
vec![
let extensionless = dir.join(tool);
let mut candidates = vec![
dir.join(format!("{tool}.cmd")),
dir.join(format!("{tool}.exe")),
dir.join(tool),
]
];
if windows_runnable_sibling_for_extensionless_tool(&extensionless).is_none() {
candidates.push(extensionless);
}
candidates
}
#[cfg(not(target_os = "windows"))]
@@ -1619,6 +1623,18 @@ fn is_windows_command_script(path: &Path) -> bool {
.unwrap_or(false)
}
#[cfg(target_os = "windows")]
fn windows_runnable_sibling_for_extensionless_tool(path: &Path) -> Option<std::path::PathBuf> {
if path.extension().is_some() {
return None;
}
["cmd", "exe"]
.iter()
.map(|ext| path.with_extension(ext))
.find(|candidate| candidate.is_file())
}
#[cfg(target_os = "windows")]
fn run_windows_tool_version_command(
tool_path: &Path,
@@ -1821,7 +1837,10 @@ fn resolve_path_default(tool: &str) -> Option<std::path::PathBuf> {
if first.is_empty() {
return None;
}
std::fs::canonicalize(first).ok()
let path = Path::new(first);
let preferred =
windows_runnable_sibling_for_extensionless_tool(path).unwrap_or_else(|| path.to_path_buf());
std::fs::canonicalize(preferred).ok()
}
/// 枚举工具在系统中的所有安装(不短路)。与 `scan_cli_version` 共用
@@ -5063,6 +5082,35 @@ mod tests {
);
}
#[cfg(target_os = "windows")]
#[test]
fn tool_executable_candidates_windows_skips_shadowed_npm_unix_shim() {
let dir = tempfile::tempdir().expect("temp dir should be created");
let extensionless = dir.path().join("codex");
let cmd = dir.path().join("codex.cmd");
std::fs::write(&extensionless, "").expect("extensionless shim should be created");
std::fs::write(&cmd, "").expect("cmd shim should be created");
let candidates = tool_executable_candidates("codex", dir.path());
assert_eq!(candidates, vec![cmd.clone(), dir.path().join("codex.exe")]);
assert!(!candidates.contains(&extensionless));
}
#[cfg(target_os = "windows")]
#[test]
fn windows_runnable_sibling_prefers_cmd_over_extensionless_tool() {
let dir = tempfile::tempdir().expect("temp dir should be created");
let extensionless = dir.path().join("codex");
let cmd = dir.path().join("codex.cmd");
std::fs::write(&extensionless, "").expect("extensionless shim should be created");
std::fs::write(&cmd, "").expect("cmd shim should be created");
let preferred = windows_runnable_sibling_for_extensionless_tool(&extensionless);
assert_eq!(preferred.as_deref(), Some(cmd.as_path()));
}
#[test]
fn resolve_launch_cwd_accepts_existing_directory() {
let resolved =
+12 -29
View File
@@ -888,9 +888,7 @@ mod import_claude_desktop_tests {
None,
);
let routes = suggested_claude_desktop_routes(&p).expect("routes built");
let r = routes
.get("claude-sonnet-4-6")
.expect("sonnet route present");
let r = routes.get("claude-sonnet-5").expect("sonnet route present");
assert_eq!(r.model, "claude-sonnet-4-5-20250929");
assert!(
!r.model.to_ascii_lowercase().contains("[1m]"),
@@ -909,9 +907,7 @@ mod import_claude_desktop_tests {
None,
);
let routes = suggested_claude_desktop_routes(&p).expect("routes built");
let r = routes
.get("claude-sonnet-4-6")
.expect("sonnet route present");
let r = routes.get("claude-sonnet-5").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
@@ -928,9 +924,7 @@ mod import_claude_desktop_tests {
None,
);
let routes = suggested_claude_desktop_routes(&p).expect("routes built");
let r = routes
.get("claude-sonnet-4-6")
.expect("sonnet route present");
let r = routes.get("claude-sonnet-5").expect("sonnet route present");
assert_eq!(r.model, "kimi-k2");
assert_eq!(r.label_override.as_deref(), Some("Kimi K2"));
}
@@ -945,9 +939,7 @@ mod import_claude_desktop_tests {
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");
let r = routes.get("claude-sonnet-5").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));
@@ -962,9 +954,7 @@ mod import_claude_desktop_tests {
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");
let r = routes.get("claude-sonnet-5").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));
@@ -982,9 +972,7 @@ mod import_claude_desktop_tests {
);
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");
let r = routes.get("claude-sonnet-5").expect("merged route present");
assert_eq!(r.model, "MiniMax-M2");
assert_eq!(r.label_override.as_deref(), Some("MiniMax-M2"));
}
@@ -1002,9 +990,7 @@ mod import_claude_desktop_tests {
);
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");
let r = routes.get("claude-sonnet-5").expect("merged route present");
assert_eq!(r.supports_1m, Some(true));
}
@@ -1020,12 +1006,12 @@ mod import_claude_desktop_tests {
);
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-sonnet-5").unwrap().model, "GLM-4.6");
assert_eq!(routes.get("claude-opus-4-8").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")
.get("claude-sonnet-5")
.unwrap()
.label_override
.as_deref(),
@@ -1045,7 +1031,7 @@ mod import_claude_desktop_tests {
let routes = suggested_claude_desktop_routes(&p).expect("routes built");
assert_eq!(routes.len(), 1);
let r = routes
.get("claude-sonnet-4-6")
.get("claude-sonnet-5")
.expect("fallback route present");
assert_eq!(r.model, "kimi-k2");
assert_eq!(r.label_override.as_deref(), Some("kimi-k2"));
@@ -1060,13 +1046,10 @@ mod import_claude_desktop_tests {
None,
);
let routes = suggested_claude_desktop_routes(&p).expect("routes built");
assert!(routes.contains_key("claude-sonnet-4-6"));
assert!(routes.contains_key("claude-sonnet-5"));
assert!(!routes.contains_key("claude-claude-sonnet-4-5-20250929"));
assert_eq!(
routes
.get("claude-sonnet-4-6")
.expect("route")
.label_override,
routes.get("claude-sonnet-5").expect("route").label_override,
None
);
}
+9
View File
@@ -1301,6 +1301,15 @@ impl Database {
"0.50",
"6.25",
),
// Claude Sonnet 5list 价,与 Sonnet 4.6 一致;促销 $2/$10 至 2026-08-31 不入表)
(
"claude-sonnet-5",
"Claude Sonnet 5",
"3",
"15",
"0.30",
"3.75",
),
// Claude 4.7 系列
(
"claude-opus-4-7",
+44 -14
View File
@@ -185,6 +185,48 @@ fn get_primary_endpoint(request: &DeepLinkImportRequest) -> String {
.unwrap_or_default()
}
fn normalize_deeplink_api_key(api_key: &str) -> String {
api_key.trim().to_string()
}
fn normalize_deeplink_base_url(base_url: &str) -> String {
base_url.trim().trim_end_matches('/').to_string()
}
fn usage_api_key_override(request: &DeepLinkImportRequest) -> Option<String> {
let usage_api_key = normalize_deeplink_api_key(request.usage_api_key.as_deref()?);
if usage_api_key.is_empty() {
return None;
}
let provider_api_key = request
.api_key
.as_deref()
.map(normalize_deeplink_api_key)
.unwrap_or_default();
if !provider_api_key.is_empty() && usage_api_key == provider_api_key {
None
} else {
Some(usage_api_key)
}
}
fn usage_base_url_override(request: &DeepLinkImportRequest) -> Option<String> {
let usage_base_url = normalize_deeplink_base_url(request.usage_base_url.as_deref()?);
if usage_base_url.is_empty() {
return None;
}
let provider_base_url = normalize_deeplink_base_url(&get_primary_endpoint(request));
if !provider_base_url.is_empty() && usage_base_url == provider_base_url {
None
} else {
Some(usage_base_url)
}
}
/// Build provider meta with usage script configuration
fn build_provider_meta(request: &DeepLinkImportRequest) -> Result<Option<ProviderMeta>, AppError> {
// Check if any usage script fields are provided
@@ -211,25 +253,13 @@ fn build_provider_meta(request: &DeepLinkImportRequest) -> Result<Option<Provide
// Determine enabled state: explicit param > has code > false
let enabled = request.usage_enabled.unwrap_or(!code.is_empty());
// Build UsageScript - use provider's API key and endpoint as defaults
// Note: use primary endpoint only (first one if comma-separated)
let usage_script = UsageScript {
enabled,
language: "javascript".to_string(),
code,
timeout: Some(10),
api_key: request
.usage_api_key
.clone()
.or_else(|| request.api_key.clone()),
base_url: request.usage_base_url.clone().or_else(|| {
let primary = get_primary_endpoint(request);
if primary.is_empty() {
None
} else {
Some(primary)
}
}),
api_key: usage_api_key_override(request),
base_url: usage_base_url_override(request),
access_token: request.usage_access_token.clone(),
user_id: request.usage_user_id.clone(),
template_type: None, // Deeplink providers don't specify template type (will use backward compatibility logic)
+148
View File
@@ -260,6 +260,154 @@ fn test_build_gemini_provider_without_model() {
assert!(env.get("GEMINI_MODEL").is_none());
}
#[test]
fn test_deeplink_usage_script_does_not_copy_provider_credentials() {
use super::provider::build_provider_from_request;
let request = DeepLinkImportRequest {
version: "v1".to_string(),
resource: "provider".to_string(),
app: Some("claude".to_string()),
name: Some("Test Claude".to_string()),
homepage: Some("https://example.com".to_string()),
endpoint: Some("https://api.example.com/v1/".to_string()),
api_key: Some("sk-main".to_string()),
icon: None,
model: None,
notes: None,
haiku_model: None,
sonnet_model: None,
opus_model: None,
config: None,
config_format: None,
config_url: None,
apps: None,
repo: None,
directory: None,
branch: None,
content: None,
description: None,
enabled: None,
usage_enabled: Some(true),
usage_script: None,
usage_api_key: None,
usage_base_url: None,
usage_access_token: None,
usage_user_id: None,
usage_auto_interval: None,
};
let provider = build_provider_from_request(&AppType::Claude, &request).unwrap();
let script = provider
.meta
.as_ref()
.and_then(|meta| meta.usage_script.as_ref())
.expect("usage script should be created");
assert!(script.enabled);
assert_eq!(script.api_key, None);
assert_eq!(script.base_url, None);
}
#[test]
fn test_deeplink_usage_script_omits_explicit_credentials_that_match_provider() {
use super::provider::build_provider_from_request;
let request = DeepLinkImportRequest {
version: "v1".to_string(),
resource: "provider".to_string(),
app: Some("claude".to_string()),
name: Some("Test Claude".to_string()),
homepage: Some("https://example.com".to_string()),
endpoint: Some("https://api.example.com/v1/".to_string()),
api_key: Some("sk-main".to_string()),
icon: None,
model: None,
notes: None,
haiku_model: None,
sonnet_model: None,
opus_model: None,
config: None,
config_format: None,
config_url: None,
apps: None,
repo: None,
directory: None,
branch: None,
content: None,
description: None,
enabled: None,
usage_enabled: Some(true),
usage_script: None,
usage_api_key: Some(" sk-main ".to_string()),
usage_base_url: Some(" https://api.example.com/v1/ ".to_string()),
usage_access_token: None,
usage_user_id: None,
usage_auto_interval: None,
};
let provider = build_provider_from_request(&AppType::Claude, &request).unwrap();
let script = provider
.meta
.as_ref()
.and_then(|meta| meta.usage_script.as_ref())
.expect("usage script should be created");
assert_eq!(script.api_key, None);
assert_eq!(script.base_url, None);
}
#[test]
fn test_deeplink_usage_script_preserves_distinct_usage_credentials() {
use super::provider::build_provider_from_request;
let request = DeepLinkImportRequest {
version: "v1".to_string(),
resource: "provider".to_string(),
app: Some("claude".to_string()),
name: Some("Test Claude".to_string()),
homepage: Some("https://example.com".to_string()),
endpoint: Some("https://api.example.com/v1".to_string()),
api_key: Some("sk-main".to_string()),
icon: None,
model: None,
notes: None,
haiku_model: None,
sonnet_model: None,
opus_model: None,
config: None,
config_format: None,
config_url: None,
apps: None,
repo: None,
directory: None,
branch: None,
content: None,
description: None,
enabled: None,
usage_enabled: Some(true),
usage_script: None,
usage_api_key: Some(" sk-usage ".to_string()),
usage_base_url: Some(" https://usage.example/api/ ".to_string()),
usage_access_token: None,
usage_user_id: None,
usage_auto_interval: None,
};
let provider = build_provider_from_request(&AppType::Claude, &request).unwrap();
let script = provider
.meta
.as_ref()
.and_then(|meta| meta.usage_script.as_ref())
.expect("usage script should be created");
assert_eq!(script.api_key.as_deref(), Some("sk-usage"));
assert_eq!(
script.base_url.as_deref(),
Some("https://usage.example/api")
);
}
#[test]
fn test_parse_and_merge_config_claude() {
// Prepare Base64 encoded Claude config
+250 -2
View File
@@ -46,16 +46,55 @@ use std::sync::{Mutex, OnceLock};
/// 获取 Hermes 配置目录
///
/// 默认路径: `~/.hermes/`
/// 可通过 settings.hermes_config_dir 覆盖
/// 解析顺序对齐 Hermes 自身的 `get_hermes_home()`:
/// 1. CCS 设置 `hermes_config_dir`(显式覆盖)
/// 2. `HERMES_HOME` 环境变量(trim 后非空;按原样,不展开 `~`,与 Hermes `Path(val)` 一致)
/// 3. 平台默认(Windows: `%LOCALAPPDATA%\hermes`,Mac/Linux: `~/.hermes`)
pub fn get_hermes_dir() -> PathBuf {
if let Some(override_dir) = get_hermes_override_dir() {
return override_dir;
}
if let Some(raw) = std::env::var_os("HERMES_HOME") {
let value = raw.to_string_lossy();
let trimmed = value.trim();
if !trimmed.is_empty() {
return PathBuf::from(trimmed);
}
}
default_hermes_dir()
}
/// 平台默认 Hermes 目录(Windows):对齐 Hermes `_get_platform_default_hermes_home()`——
/// 读 `LOCALAPPDATA` 环境变量,缺失/空时回退 `~\AppData\Local`,再拼 `hermes`。
#[cfg(target_os = "windows")]
fn default_hermes_dir() -> PathBuf {
windows_local_hermes_dir(
std::env::var_os("LOCALAPPDATA").as_deref(),
&crate::config::get_home_dir(),
)
}
/// 平台默认 Hermes 目录(Mac/Linux):`~/.hermes`。
#[cfg(not(target_os = "windows"))]
fn default_hermes_dir() -> PathBuf {
crate::config::get_home_dir().join(".hermes")
}
/// Windows `%LOCALAPPDATA%\hermes` 路径计算(纯函数,便于跨平台单测)。
/// 对齐 Hermes 的 `os.environ.get("LOCALAPPDATA", "").strip()`:trim 后为空
/// (缺失/空/纯空白)则回退 `<home>\AppData\Local\hermes`。
#[cfg(any(target_os = "windows", test))]
fn windows_local_hermes_dir(localappdata: Option<&std::ffi::OsStr>, home: &Path) -> PathBuf {
localappdata
.map(|value| value.to_string_lossy().trim().to_string())
.filter(|value| !value.is_empty())
.map(PathBuf::from)
.unwrap_or_else(|| home.join("AppData").join("Local"))
.join("hermes")
}
/// 获取 Hermes 配置文件路径
///
/// 返回 `~/.hermes/config.yaml`
@@ -1120,7 +1159,22 @@ mod tests {
let tmp = tempfile::tempdir().unwrap();
let old_test_home = std::env::var_os("CC_SWITCH_TEST_HOME");
std::env::set_var("CC_SWITCH_TEST_HOME", tmp.path());
// Neutralize the env vars get_hermes_dir() consults, so an ambient
// HERMES_HOME / LOCALAPPDATA (e.g. set by a Hermes install) can't make
// tests escape the temp home. Restored below.
let old_hermes_home = std::env::var_os("HERMES_HOME");
let old_local_appdata = std::env::var_os("LOCALAPPDATA");
std::env::remove_var("HERMES_HOME");
std::env::remove_var("LOCALAPPDATA");
let result = test_fn();
match old_local_appdata {
Some(value) => std::env::set_var("LOCALAPPDATA", value),
None => std::env::remove_var("LOCALAPPDATA"),
}
match old_hermes_home {
Some(value) => std::env::set_var("HERMES_HOME", value),
None => std::env::remove_var("HERMES_HOME"),
}
match old_test_home {
Some(value) => std::env::set_var("CC_SWITCH_TEST_HOME", value),
None => std::env::remove_var("CC_SWITCH_TEST_HOME"),
@@ -2193,4 +2247,198 @@ user_profile_enabled: false
assert_eq!(user, MemoryKind::User);
assert!(serde_json::from_str::<MemoryKind>("\"bogus\"").is_err());
}
// ---- get_hermes_dir resolution (platform default + HERMES_HOME) ----
#[test]
#[serial]
fn hermes_home_env_takes_precedence_over_platform_default() {
with_test_home(|| {
// Clear any settings override so resolution reaches the HERMES_HOME branch.
let mut s = crate::settings::get_settings();
s.hermes_config_dir = None;
crate::settings::update_settings(s).unwrap();
let old = std::env::var_os("HERMES_HOME");
let custom = std::env::temp_dir().join("ccs-hermes-home-precedence");
std::env::set_var("HERMES_HOME", &custom);
let dir = get_hermes_dir();
match old {
Some(v) => std::env::set_var("HERMES_HOME", v),
None => std::env::remove_var("HERMES_HOME"),
}
assert_eq!(
dir, custom,
"HERMES_HOME should take precedence over the platform default, got {dir:?}"
);
});
}
#[test]
#[serial]
fn settings_override_takes_precedence_over_hermes_home() {
with_test_home(|| {
let custom = tempfile::tempdir().unwrap();
let custom_path = custom.path().to_path_buf();
let mut s = crate::settings::get_settings();
s.hermes_config_dir = Some(custom_path.to_string_lossy().to_string());
crate::settings::update_settings(s).unwrap();
let old = std::env::var_os("HERMES_HOME");
std::env::set_var("HERMES_HOME", "/tmp/should-be-ignored");
let dir = get_hermes_dir();
match old {
Some(v) => std::env::set_var("HERMES_HOME", v),
None => std::env::remove_var("HERMES_HOME"),
}
let mut s = crate::settings::get_settings();
s.hermes_config_dir = None;
crate::settings::update_settings(s).unwrap();
assert_eq!(
dir, custom_path,
"settings override should win over HERMES_HOME, got {dir:?}"
);
});
}
#[test]
#[serial]
fn blank_hermes_home_falls_through_to_platform_default() {
with_test_home(|| {
let mut s = crate::settings::get_settings();
s.hermes_config_dir = None;
crate::settings::update_settings(s).unwrap();
let old = std::env::var_os("HERMES_HOME");
std::env::set_var("HERMES_HOME", " ");
let dir = get_hermes_dir();
match old {
Some(v) => std::env::set_var("HERMES_HOME", v),
None => std::env::remove_var("HERMES_HOME"),
}
// Blank HERMES_HOME is ignored (matches Hermes' `.strip()` non-empty check),
// so resolution must reach the platform default, never the literal blank path.
assert_ne!(dir, PathBuf::from(" "));
assert_eq!(dir, default_hermes_dir());
});
}
#[test]
#[serial]
fn default_hermes_dir_without_override_is_platform_correct() {
with_test_home(|| {
let mut s = crate::settings::get_settings();
s.hermes_config_dir = None;
crate::settings::update_settings(s).unwrap();
let old = std::env::var_os("HERMES_HOME");
std::env::remove_var("HERMES_HOME");
let dir = get_hermes_dir();
if let Some(v) = old {
std::env::set_var("HERMES_HOME", v);
}
#[cfg(target_os = "windows")]
assert!(
dir.ends_with("hermes") && dir.to_string_lossy().to_lowercase().contains("local"),
"Windows default should be %LOCALAPPDATA%\\hermes, got {dir:?}"
);
#[cfg(not(target_os = "windows"))]
assert!(
dir.ends_with(".hermes"),
"Unix default should be ~/.hermes, got {dir:?}"
);
});
}
#[test]
fn windows_local_hermes_dir_uses_localappdata_when_set() {
let local = std::ffi::OsString::from("C:\\Users\\tester\\AppData\\Local");
let home = Path::new("C:\\Users\\tester");
// Uses LOCALAPPDATA (ignoring home) and appends `hermes`. Build the expected
// path with `join` so the separator matches the host OS.
assert_eq!(
windows_local_hermes_dir(Some(local.as_os_str()), home),
PathBuf::from(&local).join("hermes"),
);
}
#[test]
fn windows_local_hermes_dir_falls_back_to_home_when_localappdata_missing_or_empty() {
let home = Path::new("C:\\Users\\tester");
let expected = home.join("AppData").join("Local").join("hermes");
assert_eq!(windows_local_hermes_dir(None, home), expected);
let empty = std::ffi::OsString::from("");
assert_eq!(
windows_local_hermes_dir(Some(empty.as_os_str()), home),
expected,
);
}
#[test]
fn windows_local_hermes_dir_trims_localappdata() {
// Mirror Hermes' `os.environ.get("LOCALAPPDATA", "").strip()`.
let home = Path::new("C:\\Users\\tester");
// Whitespace-only -> treated as unset, fall back to <home>\AppData\Local.
let blank = std::ffi::OsString::from(" ");
assert_eq!(
windows_local_hermes_dir(Some(blank.as_os_str()), home),
home.join("AppData").join("Local").join("hermes"),
);
// Padded value -> trimmed before use.
let padded = std::ffi::OsString::from(" C:\\Custom\\Local ");
assert_eq!(
windows_local_hermes_dir(Some(padded.as_os_str()), home),
PathBuf::from("C:\\Custom\\Local").join("hermes"),
);
}
#[test]
#[serial]
fn with_test_home_neutralizes_hermes_env_vars() {
// An ambient HERMES_HOME / LOCALAPPDATA (e.g. set by a Hermes install)
// must not leak into the test home — otherwise other tests in this
// module would read/write a real Hermes config via get_hermes_config_path().
let saved_hh = std::env::var_os("HERMES_HOME");
let saved_la = std::env::var_os("LOCALAPPDATA");
std::env::set_var("HERMES_HOME", "/ambient/hermes-home");
std::env::set_var("LOCALAPPDATA", "/ambient/local-appdata");
let inside = with_test_home(|| {
(
std::env::var_os("HERMES_HOME"),
std::env::var_os("LOCALAPPDATA"),
)
});
match saved_hh {
Some(v) => std::env::set_var("HERMES_HOME", v),
None => std::env::remove_var("HERMES_HOME"),
}
match saved_la {
Some(v) => std::env::set_var("LOCALAPPDATA", v),
None => std::env::remove_var("LOCALAPPDATA"),
}
assert_eq!(
inside.0, None,
"with_test_home must clear ambient HERMES_HOME"
);
assert_eq!(
inside.1, None,
"with_test_home must clear ambient LOCALAPPDATA"
);
}
}
+13
View File
@@ -16,6 +16,19 @@ fn main() {
if std::env::var("WEBKIT_DISABLE_COMPOSITING_MODE").is_err() {
std::env::set_var("WEBKIT_DISABLE_COMPOSITING_MODE", "1");
}
// AppImage 的 GTK 启动钩子 (linuxdeploy-plugin-gtk.sh) 会无条件
// `export GDK_BACKEND=x11` 强制走 XWayland,以规避历史上的 Wayland 崩溃
// (tauri-apps/tauri#8541)。但在较新的 Wayland + NVIDIA 环境下,强制 XWayland
// 反而使 WebKitGTK 的 webview 收不到指针事件(标题栏可点、网页内容点不动),
// resize 后黑屏;改回原生 Wayland 即可解决,且该崩溃在 WebKitGTK 2.52 上已不复现。
// 由于该钩子会覆盖用户预设的 GDK_BACKEND,这里提供一个钩子不会触碰的逃生开关:
// 设置 CC_SWITCH_GDK_BACKEND=wayland 即可强制覆盖,默认行为保持不变(零回归)。
if let Ok(backend) = std::env::var("CC_SWITCH_GDK_BACKEND") {
if !backend.is_empty() {
std::env::set_var("GDK_BACKEND", backend);
}
}
}
cc_switch_lib::run();
@@ -0,0 +1,38 @@
{
"slug": "native-responses-template",
"display_name": "native-responses-template",
"description": "native-responses-template",
"base_instructions": "You are Codex, a coding agent. You and the user share the same workspace and collaborate to achieve the user's goals.",
"default_reasoning_level": "high",
"supported_reasoning_levels": [
{
"effort": "none",
"description": "Disable Thinking"
},
{
"effort": "high",
"description": "Enabled Thinking"
}
],
"shell_type": "shell_command",
"visibility": "list",
"supported_in_api": true,
"priority": 0,
"supports_reasoning_summaries": true,
"default_reasoning_summary": "none",
"support_verbosity": false,
"truncation_policy": {
"mode": "bytes",
"limit": 10000
},
"supports_parallel_tool_calls": false,
"supports_image_detail_original": false,
"context_window": 262144,
"max_context_window": 262144,
"effective_context_window_percent": 95,
"experimental_supported_tools": [],
"input_modalities": [
"text"
],
"supports_search_tool": false
}
+5
View File
@@ -159,11 +159,16 @@ impl ConfigService {
}
let cfg_text = settings.get("config").and_then(Value::as_str);
let profile = crate::codex_config::CodexCatalogToolProfile::from_api_format(
provider.meta.as_ref().and_then(|m| m.api_format.as_deref()),
);
crate::codex_config::write_codex_provider_live_with_catalog(
&provider.settings_config,
provider.category.as_deref(),
auth,
cfg_text,
profile,
)?;
// 注意:MCP 同步在 v3.7.0 中已通过 McpService 进行,不再在此调用
// sync_enabled_to_codex 使用旧的 config.mcp.codex 结构,在新架构中为空
+8
View File
@@ -760,11 +760,19 @@ pub(crate) fn write_live_snapshot(app_type: &AppType, provider: &Provider) -> Re
.ok_or_else(|| AppError::Config("Codex 供应商配置缺少 'auth' 字段".to_string()))?;
let config_str = obj.get("config").and_then(|v| v.as_str());
// Native (direct) Responses providers must suppress Codex's freeform
// apply_patch custom tool via the generated catalog; chat/proxy
// providers keep the default tool set. Keyed on provider.meta.apiFormat.
let profile = crate::codex_config::CodexCatalogToolProfile::from_api_format(
provider.meta.as_ref().and_then(|m| m.api_format.as_deref()),
);
crate::codex_config::write_codex_provider_live_with_catalog(
&provider.settings_config,
provider.category.as_deref(),
auth,
config_str,
profile,
)?;
}
AppType::Gemini => {
+635 -11
View File
@@ -101,9 +101,9 @@ mod tests {
use crate::claude_desktop_config::PROFILE_ID;
use crate::config::{get_claude_settings_path, read_json_file, write_json_file};
use crate::database::Database;
use crate::provider::ProviderMeta;
#[cfg(any(target_os = "macos", windows))]
use crate::provider::{ClaudeDesktopMode, ClaudeDesktopModelRoute};
use crate::provider::{ProviderMeta, UsageScript};
use crate::proxy::types::ProxyConfig;
use crate::store::AppState;
use serde_json::json;
@@ -226,6 +226,68 @@ mod tests {
result
}
fn codex_settings(base_url: &str, api_key: &str) -> Value {
json!({
"auth": {
"OPENAI_API_KEY": api_key
},
"config": format!(
"model_provider = \"custom\"\n\
[model_providers.custom]\n\
name = \"custom\"\n\
base_url = \"{base_url}\"\n\
wire_api = \"chat\"\n"
)
})
}
fn usage_script_with_credentials(
api_key: Option<&str>,
base_url: Option<&str>,
template_type: Option<&str>,
) -> UsageScript {
UsageScript {
enabled: true,
language: "javascript".to_string(),
code: "return { remaining: 1, unit: 'USD' };".to_string(),
timeout: Some(10),
api_key: api_key.map(str::to_string),
base_url: base_url.map(str::to_string),
access_token: None,
user_id: None,
template_type: template_type.map(str::to_string),
auto_query_interval: None,
coding_plan_provider: None,
access_key_id: Some("ak-test".to_string()),
secret_access_key: Some("sk-test".to_string()),
}
}
fn codex_provider_with_usage(
id: &str,
base_url: &str,
api_key: &str,
usage_api_key: Option<&str>,
usage_base_url: Option<&str>,
template_type: Option<&str>,
) -> Provider {
let mut provider = Provider::with_id(
id.to_string(),
format!("Provider {id}"),
codex_settings(base_url, api_key),
None,
);
provider.meta = Some(ProviderMeta {
usage_script: Some(usage_script_with_credentials(
usage_api_key,
usage_base_url,
template_type,
)),
..Default::default()
});
provider
}
fn openclaw_provider(id: &str) -> Provider {
Provider {
id: id.to_string(),
@@ -326,6 +388,255 @@ mod tests {
})
}
#[test]
#[serial]
fn add_clears_usage_credentials_that_match_provider_config() {
with_test_home(|state, _| {
let provider = codex_provider_with_usage(
"codex-a",
"https://api.a.example/v1/",
"sk-a",
Some(" sk-a "),
Some(" https://api.a.example/v1/ "),
None,
);
ProviderService::add(state, AppType::Codex, provider, false).expect("add provider");
let saved = state
.db
.get_provider_by_id("codex-a", AppType::Codex.as_str())
.expect("query saved provider")
.expect("saved provider should exist");
let script = saved
.meta
.as_ref()
.and_then(|meta| meta.usage_script.as_ref())
.expect("usage script should remain");
assert_eq!(script.api_key, None);
assert_eq!(script.base_url, None);
});
}
#[test]
#[serial]
fn update_preserves_usage_credentials_that_only_match_previous_config() {
with_test_home(|state, _| {
let provider = codex_provider_with_usage(
"codex-usage-old",
"https://api.a.example/v1/",
"sk-a",
Some("sk-a"),
Some("https://api.a.example/v1/"),
None,
);
state
.db
.save_provider(AppType::Codex.as_str(), &provider)
.expect("seed provider with explicit usage credentials");
let mut updated = provider.clone();
updated.settings_config = codex_settings("https://api.b.example/v1/", "sk-b");
ProviderService::update(state, AppType::Codex, None, updated)
.expect("update provider main credentials");
let saved = state
.db
.get_provider_by_id("codex-usage-old", AppType::Codex.as_str())
.expect("query updated provider")
.expect("updated provider should exist");
let script = saved
.meta
.as_ref()
.and_then(|meta| meta.usage_script.as_ref())
.expect("usage script should remain");
assert_eq!(script.api_key.as_deref(), Some("sk-a"));
assert_eq!(
script.base_url.as_deref(),
Some("https://api.a.example/v1/")
);
assert_eq!(
saved.resolve_usage_credentials(&AppType::Codex),
("https://api.b.example/v1".to_string(), "sk-b".to_string())
);
});
}
#[test]
#[serial]
fn copied_provider_uses_edited_credentials_after_add_clears_mirrored_usage_credentials() {
with_test_home(|state, _| {
let copied_provider = codex_provider_with_usage(
"codex-copy",
"https://api.a.example/v1/",
"sk-a",
Some("sk-a"),
Some("https://api.a.example/v1/"),
None,
);
ProviderService::add(state, AppType::Codex, copied_provider, false)
.expect("add copied provider");
let saved_after_add = state
.db
.get_provider_by_id("codex-copy", AppType::Codex.as_str())
.expect("query copied provider")
.expect("copied provider should exist");
let script_after_add = saved_after_add
.meta
.as_ref()
.and_then(|meta| meta.usage_script.as_ref())
.expect("usage script should remain");
assert_eq!(script_after_add.api_key, None);
assert_eq!(script_after_add.base_url, None);
let mut edited_provider = saved_after_add.clone();
edited_provider.settings_config = codex_settings("https://api.b.example/v1/", "sk-b");
ProviderService::update(state, AppType::Codex, None, edited_provider)
.expect("edit copied provider credentials");
let saved_after_update = state
.db
.get_provider_by_id("codex-copy", AppType::Codex.as_str())
.expect("query edited provider")
.expect("edited provider should exist");
let script_after_update = saved_after_update
.meta
.as_ref()
.and_then(|meta| meta.usage_script.as_ref())
.expect("usage script should remain");
assert_eq!(script_after_update.api_key, None);
assert_eq!(script_after_update.base_url, None);
assert_eq!(
saved_after_update.resolve_usage_credentials(&AppType::Codex),
("https://api.b.example/v1".to_string(), "sk-b".to_string())
);
});
}
#[test]
#[serial]
fn update_clears_usage_credentials_that_match_current_config() {
with_test_home(|state, _| {
let provider = codex_provider_with_usage(
"codex-current",
"https://api.a.example/v1",
"sk-a",
Some("sk-usage"),
Some("https://usage.example/api"),
None,
);
state
.db
.save_provider(AppType::Codex.as_str(), &provider)
.expect("seed provider with distinct usage credentials");
let mut updated = provider.clone();
updated.settings_config = codex_settings("https://api.b.example/v1/", "sk-b");
updated.meta = Some(ProviderMeta {
usage_script: Some(usage_script_with_credentials(
Some(" sk-b "),
Some(" https://api.b.example/v1/ "),
None,
)),
..Default::default()
});
ProviderService::update(state, AppType::Codex, None, updated)
.expect("update provider with redundant usage credentials");
let saved = state
.db
.get_provider_by_id("codex-current", AppType::Codex.as_str())
.expect("query updated provider")
.expect("updated provider should exist");
let script = saved
.meta
.as_ref()
.and_then(|meta| meta.usage_script.as_ref())
.expect("usage script should remain");
assert_eq!(script.api_key, None);
assert_eq!(script.base_url, None);
});
}
#[test]
#[serial]
fn add_preserves_distinct_usage_credentials() {
with_test_home(|state, _| {
let provider = codex_provider_with_usage(
"codex-distinct",
"https://api.main.example/v1",
"sk-main",
Some("sk-usage"),
Some("https://usage.example/api"),
None,
);
ProviderService::add(state, AppType::Codex, provider, false).expect("add provider");
let saved = state
.db
.get_provider_by_id("codex-distinct", AppType::Codex.as_str())
.expect("query saved provider")
.expect("saved provider should exist");
let script = saved
.meta
.as_ref()
.and_then(|meta| meta.usage_script.as_ref())
.expect("usage script should remain");
assert_eq!(script.api_key.as_deref(), Some("sk-usage"));
assert_eq!(
script.base_url.as_deref(),
Some("https://usage.example/api")
);
});
}
#[test]
#[serial]
fn add_does_not_clear_token_plan_credentials() {
with_test_home(|state, _| {
let provider = codex_provider_with_usage(
"codex-token-plan",
"https://api.plan.example/v1",
"sk-plan",
Some("sk-plan"),
Some("https://api.plan.example/v1"),
Some("token_plan"),
);
ProviderService::add(state, AppType::Codex, provider, false).expect("add provider");
let saved = state
.db
.get_provider_by_id("codex-token-plan", AppType::Codex.as_str())
.expect("query saved provider")
.expect("saved provider should exist");
let script = saved
.meta
.as_ref()
.and_then(|meta| meta.usage_script.as_ref())
.expect("usage script should remain");
assert_eq!(script.api_key.as_deref(), Some("sk-plan"));
assert_eq!(
script.base_url.as_deref(),
Some("https://api.plan.example/v1")
);
assert_eq!(script.access_key_id.as_deref(), Some("ak-test"));
assert_eq!(script.secret_access_key.as_deref(), Some("sk-test"));
});
}
#[test]
fn validate_provider_settings_rejects_missing_auth() {
let provider = Provider::with_id(
@@ -342,6 +653,83 @@ mod tests {
);
}
#[test]
fn extract_claude_common_config_strips_all_credentials_keeps_shareable() {
// env 混入多种凭据(Anthropic/OpenRouter/Google/OpenAI/Gemini + AWS/Vertex
// 与可共享配置;顶层混入非标准的 apiKey/api_key 凭据与正常设置。
let settings = json!({
"env": {
"ANTHROPIC_API_KEY": "sk-ant",
"ANTHROPIC_AUTH_TOKEN": "tok-ant",
"OPENROUTER_API_KEY": "sk-or",
"GOOGLE_API_KEY": "g-key",
"OPENAI_API_KEY": "sk-oai",
"GEMINI_API_KEY": "g-gem",
"AWS_ACCESS_KEY_ID": "AKIA",
"AWS_SECRET_ACCESS_KEY": "secret",
"AWS_SESSION_TOKEN": "sess",
"GOOGLE_APPLICATION_CREDENTIALS": "/path/creds.json",
"AWS_BEARER_TOKEN_BEDROCK": "bedrock-tok",
"ANTHROPIC_BASE_URL": "https://example.com",
"ANTHROPIC_MODEL": "claude-x",
// 可共享、非机密配置(复数 _TOKENS 不应被误剥)
"ENABLE_TOOL_SEARCH": "true",
"CLAUDE_CODE_MAX_OUTPUT_TOKENS": "8192"
},
"apiKey": "sk-top",
"api_key": "sk-top2",
"theme": "dark",
"includeCoAuthoredBy": false
});
let snippet = ProviderService::extract_claude_common_config(&settings)
.expect("extract should succeed");
let value: Value = serde_json::from_str(&snippet).expect("snippet is valid JSON");
// 所有凭据都不得出现在共享片段里
let env = value.get("env");
for leaked in [
"ANTHROPIC_API_KEY",
"ANTHROPIC_AUTH_TOKEN",
"OPENROUTER_API_KEY",
"GOOGLE_API_KEY",
"OPENAI_API_KEY",
"GEMINI_API_KEY",
"AWS_ACCESS_KEY_ID",
"AWS_SECRET_ACCESS_KEY",
"AWS_SESSION_TOKEN",
"GOOGLE_APPLICATION_CREDENTIALS",
"AWS_BEARER_TOKEN_BEDROCK",
] {
assert!(
env.and_then(|e| e.get(leaked)).is_none(),
"credential {leaked} must not leak into common config"
);
}
assert!(
value.get("apiKey").is_none() && value.get("api_key").is_none(),
"top-level credentials must be stripped"
);
// 端点/模型(provider-specific 非机密)也应剥掉
assert!(env.and_then(|e| e.get("ANTHROPIC_BASE_URL")).is_none());
assert!(env.and_then(|e| e.get("ANTHROPIC_MODEL")).is_none());
// 可共享的非机密配置必须保留(含复数 _TOKENS 不被误剥)
assert_eq!(
env.and_then(|e| e.get("ENABLE_TOOL_SEARCH"))
.and_then(|v| v.as_str()),
Some("true")
);
assert_eq!(
env.and_then(|e| e.get("CLAUDE_CODE_MAX_OUTPUT_TOKENS"))
.and_then(|v| v.as_str()),
Some("8192")
);
assert_eq!(value.get("theme").and_then(|v| v.as_str()), Some("dark"));
assert_eq!(value.get("includeCoAuthoredBy"), Some(&json!(false)));
}
#[test]
fn validate_provider_settings_rejects_negative_cost_multiplier() {
let mut provider = Provider::with_id(
@@ -1196,6 +1584,72 @@ impl ProviderService {
.live_config_managed = Some(managed);
}
fn normalize_usage_script_credential_overrides(app_type: &AppType, provider: &mut Provider) {
let current_credentials = provider.resolve_usage_credentials(app_type);
let Some(usage_script) = provider
.meta
.as_mut()
.and_then(|meta| meta.usage_script.as_mut())
else {
return;
};
if usage_script.template_type.as_deref() == Some("token_plan") {
return;
}
if usage_script.api_key.as_deref().is_some_and(|api_key| {
Self::should_clear_usage_api_key_override(api_key, &current_credentials)
}) {
usage_script.api_key = None;
}
if usage_script.base_url.as_deref().is_some_and(|base_url| {
Self::should_clear_usage_base_url_override(base_url, &current_credentials)
}) {
usage_script.base_url = None;
}
}
fn should_clear_usage_api_key_override(
script_api_key: &str,
current_credentials: &(String, String),
) -> bool {
let candidate = script_api_key.trim();
if candidate.is_empty() {
return true;
}
let matches_provider_key = |api_key: &str| {
let api_key = api_key.trim();
!api_key.is_empty() && api_key == candidate
};
matches_provider_key(&current_credentials.1)
}
fn should_clear_usage_base_url_override(
script_base_url: &str,
current_credentials: &(String, String),
) -> bool {
let candidate = Self::normalize_usage_base_url_for_compare(script_base_url);
if candidate.is_empty() {
return true;
}
let matches_provider_base_url = |base_url: &str| {
let base_url = Self::normalize_usage_base_url_for_compare(base_url);
!base_url.is_empty() && base_url == candidate
};
matches_provider_base_url(&current_credentials.0)
}
fn normalize_usage_base_url_for_compare(base_url: &str) -> String {
base_url.trim().trim_end_matches('/').to_string()
}
/// List all providers for an app type
pub fn list(
state: &AppState,
@@ -1232,6 +1686,7 @@ impl ProviderService {
Self::normalize_provider_if_claude(&app_type, &mut provider);
Self::validate_provider_settings(&app_type, &provider)?;
normalize_provider_common_config_for_storage(state.db.as_ref(), &app_type, &mut provider)?;
Self::normalize_usage_script_credential_overrides(&app_type, &mut provider);
if app_type.is_additive_mode() {
Self::set_provider_live_config_managed(&mut provider, add_to_live);
}
@@ -1286,6 +1741,7 @@ impl ProviderService {
Self::normalize_provider_if_claude(&app_type, &mut provider);
Self::validate_provider_settings(&app_type, &provider)?;
normalize_provider_common_config_for_storage(state.db.as_ref(), &app_type, &mut provider)?;
Self::normalize_usage_script_credential_overrides(&app_type, &mut provider);
if provider_id_changed {
if !app_type.is_additive_mode() {
@@ -1743,6 +2199,17 @@ impl ProviderService {
// Only backfill when switching to a different provider
if let Ok(live_config) = read_live_settings(app_type.clone()) {
if let Some(mut current_provider) = providers.get(&current_id).cloned() {
// 切走前先把 live 里的可共享改动(含用户直接在应用内
// 装插件/加 hook/改偏好)同步进通用配置片段,再做剥离回填。
// 详见 sync_common_config_snippet_from_live 的文档。
Self::sync_common_config_snippet_from_live(
state,
&app_type,
&current_provider,
&live_config,
&mut result,
);
current_provider.settings_config =
strip_common_config_from_live_settings(
state.db.as_ref(),
@@ -1963,6 +2430,100 @@ impl ProviderService {
Self::migrate_legacy_common_config_usage(state, app_type, &snippet)
}
/// 切走某供应商前,把它 live 配置里的可共享部分重新提取并**整体替换**到
/// 通用配置片段,使在 live 应用里直接做的改动不会因切换而丢失。
///
/// 采用"整体重提取 + 替换"而非"只合并新增",是为了同时覆盖三种情况:
/// - **新增**:用户直接在应用里装了插件、加了 hook、改了 env/主题/权限等共享
/// 偏好,被捕获进通用配置,切到别的供应商也带得过去;
/// - **删除**:被删掉的键不在新提取结果里,于是从片段里消失、下次切换不会被
/// 重新注入——否则会出现"插件怎么删也删不掉"的反直觉 bug;
/// - **密钥安全**:提取器已剥掉 auth / model / endpoint,密钥永不进共享片段。
///
/// 之所以"整体替换"是安全的:每次写 live 都会把当前片段合并进去,所以切走时
/// 读到的 live 一定是"片段 + 本地改动"的超集,重提取只会丢掉用户真正删掉的键,
/// 不会误删其它供应商共享的内容。
///
/// **作用域**:仅 Claude。Codex 的 live 是 TOML 且端点藏在 `[model_providers]`
/// 表里(现有提取器不剥),自动同步会泄漏端点并与 modelCatalog / 统一会话桶 /
/// auth 还原逻辑冲突;Gemini 暂未纳入。两者如需支持应各自单独验证后再加。
///
/// 仅对**显式勾选"写入通用配置"**`meta.common_config_enabled == Some(true)`)的
/// 供应商生效;用户**显式清空**过片段(`_cleared`)时跳过,避免把用户主动清掉的
/// 配置又塞回来。所有失败均为非致命,只记 warning,绝不阻断切换。
fn sync_common_config_snippet_from_live(
state: &AppState,
app_type: &AppType,
provider: &Provider,
live_config: &Value,
result: &mut SwitchResult,
) {
// 作用域限定 Claude(见函数文档)。
if !matches!(app_type, AppType::Claude) {
return;
}
let opted_in = provider
.meta
.as_ref()
.and_then(|meta| meta.common_config_enabled)
== Some(true);
if !opted_in {
return;
}
match state.db.is_config_snippet_cleared(app_type.as_str()) {
Ok(true) => return, // 用户显式清空过通用配置,尊重其选择,不再自动塞回
Ok(false) => {}
Err(err) => {
log::warn!(
"Failed to read common config cleared flag for {}: {err}",
app_type.as_str()
);
return;
}
}
let new_snippet = match Self::extract_common_config_snippet_from_settings(
app_type.clone(),
live_config,
) {
Ok(snippet) => snippet,
Err(err) => {
log::warn!(
"Failed to extract common config from live for {} provider '{}': {err}",
app_type.as_str(),
provider.id
);
return;
}
};
// 未变化则跳过,避免无谓写库(不切 live 配置时这是常态路径)。
let current = state
.db
.get_config_snippet(app_type.as_str())
.ok()
.flatten();
if current.as_deref() == Some(new_snippet.as_str()) {
return;
}
if let Err(err) = state
.db
.set_config_snippet(app_type.as_str(), Some(new_snippet))
{
log::warn!(
"Failed to persist synced common config for {} provider '{}': {err}",
app_type.as_str(),
provider.id
);
result
.warnings
.push(format!("common_config_sync_failed:{}", provider.id));
}
}
/// Extract common config snippet from current provider
///
/// Extracts the current provider's configuration and removes provider-specific fields
@@ -2009,16 +2570,63 @@ impl ProviderService {
}
}
/// 判断一个 env / 顶层配置键名是否为凭据/机密:凡命中一律不得写入共享的
/// 通用配置片段。**故意从严**——多剥一个非机密键只是它不被共享(可恢复的小
/// 不便),漏剥一个凭据则会把密钥注入到每个供应商(不可恢复的泄漏)。因此用
/// 模式匹配覆盖整类,而非枚举具体名字(枚举永远会漏掉下一个 `*_API_KEY`)。
///
/// 覆盖:Anthropic / OpenRouter / Google / OpenAI / Gemini 等 `*_API_KEY`
/// Claude provider 的凭据见 `Provider::resolve_usage_credentials`,确实支持
/// `OPENROUTER_API_KEY` / `GOOGLE_API_KEY` 等回退)、各类 `*_AUTH_TOKEN` /
/// 单数 `*_TOKEN`、AWS Bedrock / Vertex 凭据、以及通用 secret / password /
/// 私钥命名。
fn is_sensitive_config_key(name: &str) -> bool {
let upper = name.to_ascii_uppercase();
// 单数 `_TOKEN` 命中 AWS_SESSION_TOKEN 等,但**不**误伤复数 `_TOKENS`
// CLAUDE_CODE_MAX_OUTPUT_TOKENS / MAX_THINKING_TOKENS 是正常可共享配置)。
const SENSITIVE_SUFFIXES: &[&str] = &[
"_API_KEY",
"_APIKEY",
"_AUTH_TOKEN",
"_TOKEN",
"_ACCESS_KEY",
"_ACCESS_KEY_ID",
"_KEY_ID",
"_PRIVATE_KEY",
];
const SENSITIVE_EXACT: &[&str] = &[
"APIKEY",
"API_KEY",
"TOKEN",
"SECRET",
"PASSWORD",
"CREDENTIALS",
];
// contains:覆盖 AWS_SECRET_ACCESS_KEY / *_CLIENT_SECRET /
// GOOGLE_APPLICATION_CREDENTIALS / AWS_BEARER_TOKEN_BEDROCK 等变体。
const SENSITIVE_CONTAINS: &[&str] = &[
"SECRET",
"PASSWORD",
"PASSWD",
"CREDENTIAL",
"PRIVATE_KEY",
"BEARER_TOKEN",
];
SENSITIVE_EXACT.contains(&upper.as_str())
|| SENSITIVE_SUFFIXES.iter().any(|s| upper.ends_with(s))
|| SENSITIVE_CONTAINS.iter().any(|c| upper.contains(c))
}
/// Extract common config for Claude (JSON format)
fn extract_claude_common_config(settings: &Value) -> Result<String, AppError> {
let mut config = settings.clone();
// Fields to exclude from common config
const ENV_EXCLUDES: &[&str] = &[
// Auth
"ANTHROPIC_API_KEY",
"ANTHROPIC_AUTH_TOKEN",
// Models and Claude Code model-menu display names
// 供应商专属的**非机密**字段(模型 + 端点),不应共享。凭据/机密不在此列举,
// 改由 `is_sensitive_config_key`(模式匹配)统一剥离,新供应商的 `*_API_KEY`
// 等无需再手工补名单即可被覆盖。
const ENV_PROVIDER_SPECIFIC_EXCLUDES: &[&str] = &[
"ANTHROPIC_MODEL",
"ANTHROPIC_REASONING_MODEL", // legacy: 已废弃,但旧配置可能残留
"ANTHROPIC_DEFAULT_HAIKU_MODEL",
@@ -2027,7 +2635,6 @@ impl ProviderService {
"ANTHROPIC_DEFAULT_OPUS_MODEL_NAME",
"ANTHROPIC_DEFAULT_SONNET_MODEL",
"ANTHROPIC_DEFAULT_SONNET_MODEL_NAME",
// Endpoint
"ANTHROPIC_BASE_URL",
];
@@ -2038,22 +2645,39 @@ impl ProviderService {
"smallFastModel",
];
// Remove env fields
// Remove env fields: provider-specific (models/endpoint) + 任何凭据键。
if let Some(env) = config.get_mut("env").and_then(|v| v.as_object_mut()) {
for key in ENV_EXCLUDES {
let sensitive: Vec<String> = env
.keys()
.filter(|k| Self::is_sensitive_config_key(k))
.cloned()
.collect();
for key in ENV_PROVIDER_SPECIFIC_EXCLUDES {
env.remove(*key);
}
for key in &sensitive {
env.remove(key);
}
// If env is empty after removal, remove the env object itself
if env.is_empty() {
config.as_object_mut().map(|obj| obj.remove("env"));
}
}
// Remove top-level fields
// Remove top-level fields: legacy model fields + 任何凭据键
// (例如非标准的顶层 apiKey / api_key / *_TOKEN)。
if let Some(obj) = config.as_object_mut() {
let sensitive: Vec<String> = obj
.keys()
.filter(|k| Self::is_sensitive_config_key(k))
.cloned()
.collect();
for key in TOP_LEVEL_EXCLUDES {
obj.remove(*key);
}
for key in &sensitive {
obj.remove(key);
}
}
// Check if result is empty
+21 -2
View File
@@ -2145,12 +2145,16 @@ impl ProxyService {
.get("auth")
.ok_or_else(|| "Codex 供应商缺少 auth 配置".to_string())?;
let config_str = effective_settings.get("config").and_then(|v| v.as_str());
let profile = crate::codex_config::CodexCatalogToolProfile::from_api_format(
provider.meta.as_ref().and_then(|m| m.api_format.as_deref()),
);
crate::codex_config::write_codex_provider_live_with_catalog(
&effective_settings,
provider.category.as_deref(),
auth,
config_str,
profile,
)
.map_err(|e| format!("写入 Codex 配置失败: {e}"))?;
}
@@ -2410,12 +2414,16 @@ impl ProxyService {
.get("auth")
.ok_or_else(|| "Codex 配置缺少 auth 字段".to_string())?;
let config_str = config.get("config").and_then(|v| v.as_str());
let profile = crate::codex_config::CodexCatalogToolProfile::from_api_format(
provider.meta.as_ref().and_then(|m| m.api_format.as_deref()),
);
crate::codex_config::write_codex_provider_live_with_catalog(
config,
provider.category.as_deref(),
auth,
config_str,
profile,
)
.map_err(|e| format!("写入 Codex 配置失败: {e}"))
}
@@ -2435,9 +2443,12 @@ impl ProxyService {
.filter(|auth| Self::codex_auth_has_proxy_placeholder(auth))
{
let config_str = config.get("config").and_then(|v| v.as_str()).unwrap_or("");
let profile = crate::codex_config::CodexCatalogToolProfile::from_api_format(
provider.and_then(|p| p.meta.as_ref()?.api_format.as_deref()),
);
let prepared_config =
crate::codex_config::prepare_codex_live_config_text_with_optional_catalog(
config, config_str,
config, config_str, profile,
)
.map_err(|e| format!("写入 Codex 配置失败: {e}"))?;
let live_config =
@@ -2471,10 +2482,18 @@ impl ProxyService {
// living in the config's `experimental_bearer_token`). Computing it up here
// keeps every config-writing branch — write-auth, delete-auth, no-auth —
// consistent instead of letting the empty-auth path skip projection.
// Verbatim restore has no Provider in hand (we only have the stored
// backup config), so the catalog tool profile can't be recovered here.
// Default to ProxyChat: a restored native-direct backup keeps its inline
// modelCatalog but would not get apply_patch re-stripped until the next
// provider switch rewrites it via write_live_snapshot. Acceptable known
// limitation (restore-of-deleted-provider-backup only).
let prepared_cfg = config_str
.map(|cfg| {
crate::codex_config::prepare_codex_live_config_text_with_optional_catalog(
config, cfg,
config,
cfg,
crate::codex_config::CodexCatalogToolProfile::ProxyChat,
)
})
.transpose()
+67 -2
View File
@@ -2197,8 +2197,24 @@ fn strip_model_date_suffix(model_id: &str) -> Option<String> {
}
let (base, suffix) = model_id.rsplit_once('-')?;
(!base.is_empty() && suffix.len() == 8 && suffix.chars().all(|c| c.is_ascii_digit()))
.then(|| base.to_string())
if base.is_empty() || !suffix.chars().all(|c| c.is_ascii_digit()) {
return None;
}
// 8 位 YYYYMMDD(如 -20250615OpenAI / Claude / 通义千问等)。
if suffix.len() == 8 {
return Some(base.to_string());
}
// 6 位 YYMMDD(如 -260628;火山方舟 doubao-seed-*、部分国产厂商)。
// 6 位比 8 位更易误伤非日期尾巴(如 -123456 的版本号),故额外校验
// 月 01-12、日 01-31 才剥离;剥不动时退回 None 由上层精确匹配兜底。
if suffix.len() == 6 {
let month: u32 = suffix[2..4].parse().unwrap_or(0);
let day: u32 = suffix[4..6].parse().unwrap_or(0);
if (1..=12).contains(&month) && (1..=31).contains(&day) {
return Some(base.to_string());
}
}
None
}
fn strip_reasoning_effort_suffix(model_id: &str) -> Option<String> {
@@ -3859,6 +3875,55 @@ mod tests {
assert_eq!(strip_model_date_suffix("abc🚀12345678"), None);
}
#[test]
fn test_strip_model_date_suffix_handles_six_digit_yymmdd() {
// 火山方舟 6 位 YYMMDD 后缀应被剥离(doubao 全系都用这种格式)。
assert_eq!(
strip_model_date_suffix("doubao-seed-2-1-pro-260628").as_deref(),
Some("doubao-seed-2-1-pro")
);
assert_eq!(
strip_model_date_suffix("doubao-seed-1-6-250615").as_deref(),
Some("doubao-seed-1-6")
);
// 8 位 YYYYMMDD 仍照旧剥离。
assert_eq!(
strip_model_date_suffix("claude-3-5-sonnet-20241022").as_deref(),
Some("claude-3-5-sonnet")
);
// 月/日非法的 6 位尾巴(版本号等)不剥离,避免误伤。
assert_eq!(strip_model_date_suffix("foo-bar-123456"), None); // 月=34
assert_eq!(strip_model_date_suffix("widget-209900"), None); // 月=99
assert_eq!(strip_model_date_suffix("gizmo-251200"), None); // 日=00
}
#[test]
fn test_pricing_resolves_volcengine_dated_model_to_bare_seed_row() -> Result<(), AppError> {
// 回归:火山真实用量带 6 位日期后缀(doubao-seed-2-1-pro-260628),
// 必须能归一化命中定价表里的裸名 seed 行(doubao-seed-2-1-pro),否则成本显示 $0。
let db = Database::memory()?;
let conn = 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 ('doubao-seed-2-1-pro', 'Doubao Seed 2.1 Pro', '0.84', '4.2', '0.17', '0')",
[],
)?;
let row = find_model_pricing_row(&conn, "doubao-seed-2-1-pro-260628")?;
assert!(
row.is_some(),
"带日期的火山模型应通过 6 位日期剥离命中裸名定价行"
);
let (input, output, ..) = row.unwrap();
assert_eq!(input, "0.84");
assert_eq!(output, "4.2");
Ok(())
}
#[test]
fn test_prefix_pricing_does_not_match_short_base_model_to_variant() -> Result<(), AppError> {
let db = Database::memory()?;
+1 -1
View File
@@ -1,7 +1,7 @@
{
"$schema": "https://schema.tauri.app/config/2",
"productName": "CC Switch",
"version": "3.16.4",
"version": "3.16.5",
"identifier": "com.ccswitch.desktop",
"build": {
"frontendDist": "../dist",
+361
View File
@@ -1696,6 +1696,367 @@ fn provider_service_switch_claude_updates_live_and_state() {
);
}
/// 切走勾选了通用配置的 Claude 供应商时,应把它 live 里新增的可共享键
/// (用户直接在应用内装插件/改偏好)捕获进通用配置片段,并带到下一个供应商。
#[test]
fn switch_claude_syncs_new_shared_keys_from_live_into_common_config() {
let _guard = test_mutex().lock().expect("acquire test mutex");
reset_test_fs();
let _home = ensure_test_home();
let settings_path = get_claude_settings_path();
if let Some(parent) = settings_path.parent() {
std::fs::create_dir_all(parent).expect("create claude settings dir");
}
// A 的 live = A 私有密钥(含非 Anthropic 的 OpenRouter 凭据)+ 已共享的 theme
// + 用户刚在应用内新增的 enableAllProjectMcpServers
let live = json!({
"env": { "ANTHROPIC_API_KEY": "a-key", "OPENROUTER_API_KEY": "sk-or-leak" },
"theme": "dark",
"enableAllProjectMcpServers": true
});
std::fs::write(
&settings_path,
serde_json::to_string_pretty(&live).expect("serialize live"),
)
.expect("seed claude live config");
let mut config = MultiAppConfig::default();
{
let manager = config
.get_manager_mut(&AppType::Claude)
.expect("claude manager");
manager.current = "a".to_string();
let mut provider_a = Provider::with_id(
"a".to_string(),
"A".to_string(),
json!({ "env": { "ANTHROPIC_API_KEY": "a-key" } }),
None,
);
provider_a.meta = Some(ProviderMeta {
common_config_enabled: Some(true),
..Default::default()
});
manager.providers.insert("a".to_string(), provider_a);
let mut provider_b = Provider::with_id(
"b".to_string(),
"B".to_string(),
json!({ "env": { "ANTHROPIC_API_KEY": "b-key" } }),
None,
);
provider_b.meta = Some(ProviderMeta {
common_config_enabled: Some(true),
..Default::default()
});
manager.providers.insert("b".to_string(), provider_b);
}
let state = create_test_state_with_config(&config).expect("create test state");
state
.db
.set_config_snippet(
AppType::Claude.as_str(),
Some(r#"{"theme":"dark"}"#.to_string()),
)
.expect("seed common config snippet");
ProviderService::switch(&state, AppType::Claude, "b").expect("switch should succeed");
// 片段应捕获到新增键,并保留已有共享键,且绝不含密钥
let snippet = state
.db
.get_config_snippet(AppType::Claude.as_str())
.expect("read snippet")
.expect("snippet present");
let snippet_value: serde_json::Value =
serde_json::from_str(&snippet).expect("snippet is valid JSON");
assert_eq!(
snippet_value.get("enableAllProjectMcpServers"),
Some(&json!(true)),
"newly added shared key should be captured into common config"
);
assert_eq!(
snippet_value.get("theme").and_then(|v| v.as_str()),
Some("dark"),
"previously shared key should be preserved"
);
assert!(
snippet_value
.get("env")
.and_then(|env| env.get("ANTHROPIC_API_KEY"))
.is_none(),
"secrets must never leak into the shared snippet"
);
assert!(
snippet_value
.get("env")
.and_then(|env| env.get("OPENROUTER_API_KEY"))
.is_none(),
"non-Anthropic Claude credentials must never leak into the shared snippet"
);
// 新增键应通过通用配置带到 B 的 live
let live_after: serde_json::Value =
read_json_file(&settings_path).expect("read live after switch");
assert_eq!(
live_after.get("enableAllProjectMcpServers"),
Some(&json!(true)),
"shared key should propagate to the next provider's live config"
);
assert!(
live_after
.get("env")
.and_then(|env| env.get("OPENROUTER_API_KEY"))
.is_none(),
"leaked credential must not be injected into the next provider's live"
);
assert_eq!(
live_after
.get("env")
.and_then(|env| env.get("ANTHROPIC_API_KEY"))
.and_then(|v| v.as_str()),
Some("b-key"),
"live should reflect new provider's own auth"
);
}
/// 用户在应用内删掉一个已共享的键后,切换应把删除同步进通用配置,
/// 且不会在切到下一个供应商时被重新注入(否则会"删不掉")。
#[test]
fn switch_claude_syncs_deletions_from_live_into_common_config() {
let _guard = test_mutex().lock().expect("acquire test mutex");
reset_test_fs();
let _home = ensure_test_home();
let settings_path = get_claude_settings_path();
if let Some(parent) = settings_path.parent() {
std::fs::create_dir_all(parent).expect("create claude settings dir");
}
// live 里 theme 还在,但用户已删掉 enableAllProjectMcpServers
let live = json!({
"env": { "ANTHROPIC_API_KEY": "a-key" },
"theme": "dark"
});
std::fs::write(
&settings_path,
serde_json::to_string_pretty(&live).expect("serialize live"),
)
.expect("seed claude live config");
let mut config = MultiAppConfig::default();
{
let manager = config
.get_manager_mut(&AppType::Claude)
.expect("claude manager");
manager.current = "a".to_string();
let mut provider_a = Provider::with_id(
"a".to_string(),
"A".to_string(),
json!({ "env": { "ANTHROPIC_API_KEY": "a-key" } }),
None,
);
provider_a.meta = Some(ProviderMeta {
common_config_enabled: Some(true),
..Default::default()
});
manager.providers.insert("a".to_string(), provider_a);
let mut provider_b = Provider::with_id(
"b".to_string(),
"B".to_string(),
json!({ "env": { "ANTHROPIC_API_KEY": "b-key" } }),
None,
);
provider_b.meta = Some(ProviderMeta {
common_config_enabled: Some(true),
..Default::default()
});
manager.providers.insert("b".to_string(), provider_b);
}
let state = create_test_state_with_config(&config).expect("create test state");
// 片段里仍残留 enableAllProjectMcpServers(上次共享的)
state
.db
.set_config_snippet(
AppType::Claude.as_str(),
Some(r#"{"theme":"dark","enableAllProjectMcpServers":true}"#.to_string()),
)
.expect("seed common config snippet");
ProviderService::switch(&state, AppType::Claude, "b").expect("switch should succeed");
let snippet = state
.db
.get_config_snippet(AppType::Claude.as_str())
.expect("read snippet")
.expect("snippet present");
let snippet_value: serde_json::Value =
serde_json::from_str(&snippet).expect("snippet is valid JSON");
assert!(
snippet_value.get("enableAllProjectMcpServers").is_none(),
"deleted key should be removed from common config"
);
assert_eq!(
snippet_value.get("theme").and_then(|v| v.as_str()),
Some("dark"),
"untouched shared key should remain"
);
// 切到 B 后 live 不应再出现被删除的键
let live_after: serde_json::Value =
read_json_file(&settings_path).expect("read live after switch");
assert!(
live_after.get("enableAllProjectMcpServers").is_none(),
"deleted shared key must not be re-injected into the next provider"
);
}
/// 未勾选"写入通用配置"的供应商,其 live 改动不应自动污染通用配置片段。
#[test]
fn switch_claude_does_not_sync_common_config_for_opted_out_provider() {
let _guard = test_mutex().lock().expect("acquire test mutex");
reset_test_fs();
let _home = ensure_test_home();
let settings_path = get_claude_settings_path();
if let Some(parent) = settings_path.parent() {
std::fs::create_dir_all(parent).expect("create claude settings dir");
}
let live = json!({
"env": { "ANTHROPIC_API_KEY": "a-key" },
"providerSpecific": "x"
});
std::fs::write(
&settings_path,
serde_json::to_string_pretty(&live).expect("serialize live"),
)
.expect("seed claude live config");
let mut config = MultiAppConfig::default();
{
let manager = config
.get_manager_mut(&AppType::Claude)
.expect("claude manager");
manager.current = "a".to_string();
// A 未勾选通用配置(meta = None
manager.providers.insert(
"a".to_string(),
Provider::with_id(
"a".to_string(),
"A".to_string(),
json!({ "env": { "ANTHROPIC_API_KEY": "a-key" } }),
None,
),
);
manager.providers.insert(
"b".to_string(),
Provider::with_id(
"b".to_string(),
"B".to_string(),
json!({ "env": { "ANTHROPIC_API_KEY": "b-key" } }),
None,
),
);
}
let state = create_test_state_with_config(&config).expect("create test state");
state
.db
.set_config_snippet(
AppType::Claude.as_str(),
Some(r#"{"theme":"dark"}"#.to_string()),
)
.expect("seed common config snippet");
ProviderService::switch(&state, AppType::Claude, "b").expect("switch should succeed");
let snippet = state
.db
.get_config_snippet(AppType::Claude.as_str())
.expect("read snippet")
.expect("snippet present");
let snippet_value: serde_json::Value =
serde_json::from_str(&snippet).expect("snippet is valid JSON");
assert!(
snippet_value.get("providerSpecific").is_none(),
"opted-out provider's live changes must not pollute the shared snippet"
);
assert_eq!(
snippet_value.get("theme").and_then(|v| v.as_str()),
Some("dark"),
"snippet should stay unchanged for opted-out providers"
);
}
/// 用户显式清空过通用配置(_cleared)后,切换不应把片段重新塞回来。
#[test]
fn switch_claude_respects_explicitly_cleared_common_config() {
let _guard = test_mutex().lock().expect("acquire test mutex");
reset_test_fs();
let _home = ensure_test_home();
let settings_path = get_claude_settings_path();
if let Some(parent) = settings_path.parent() {
std::fs::create_dir_all(parent).expect("create claude settings dir");
}
let live = json!({
"env": { "ANTHROPIC_API_KEY": "a-key" },
"theme": "dark"
});
std::fs::write(
&settings_path,
serde_json::to_string_pretty(&live).expect("serialize live"),
)
.expect("seed claude live config");
let mut config = MultiAppConfig::default();
{
let manager = config
.get_manager_mut(&AppType::Claude)
.expect("claude manager");
manager.current = "a".to_string();
let mut provider_a = Provider::with_id(
"a".to_string(),
"A".to_string(),
json!({ "env": { "ANTHROPIC_API_KEY": "a-key" } }),
None,
);
provider_a.meta = Some(ProviderMeta {
common_config_enabled: Some(true),
..Default::default()
});
manager.providers.insert("a".to_string(), provider_a);
let mut provider_b = Provider::with_id(
"b".to_string(),
"B".to_string(),
json!({ "env": { "ANTHROPIC_API_KEY": "b-key" } }),
None,
);
provider_b.meta = Some(ProviderMeta {
common_config_enabled: Some(true),
..Default::default()
});
manager.providers.insert("b".to_string(), provider_b);
}
let state = create_test_state_with_config(&config).expect("create test state");
state
.db
.set_config_snippet_cleared(AppType::Claude.as_str(), true)
.expect("mark snippet cleared");
ProviderService::switch(&state, AppType::Claude, "b").expect("switch should succeed");
assert!(
state
.db
.get_config_snippet(AppType::Claude.as_str())
.expect("read snippet")
.is_none(),
"explicitly cleared snippet must not be resurrected by switch-away sync"
);
}
#[test]
fn provider_service_switch_missing_provider_returns_error() {
let _guard = test_mutex().lock().expect("acquire test mutex");
@@ -39,9 +39,11 @@ import { Switch } from "@/components/ui/switch";
import { BasicFormFields } from "./BasicFormFields";
import { CodexOAuthSection } from "./CodexOAuthSection";
import { CopilotAuthSection } from "./CopilotAuthSection";
import { ApiKeySection } from "./shared/ApiKeySection";
import { EndpointField } from "./shared/EndpointField";
import { ModelDropdown } from "./shared/ModelDropdown";
import { ProviderPresetSelector } from "./ProviderPresetSelector";
import { useApiKeyLink } from "./hooks/useApiKeyLink";
import { providerSchema, type ProviderFormData } from "@/lib/schemas/provider";
import type {
ClaudeApiFormat,
@@ -383,6 +385,21 @@ export function ClaudeDesktopProviderForm({
activeProviderType === "github_copilot" ||
activeProviderType === "codex_oauth";
// API Key 获取/邀请链接(与 Claude Code 表单同款,见 ClaudeFormFields
const apiKeyLinkCategory = activePreset?.category ?? initialData?.category;
const {
shouldShowApiKeyLink,
websiteUrl: apiKeyLinkWebsiteUrl,
isPartner: apiKeyLinkIsPartner,
partnerPromotionKey: apiKeyLinkPromotionKey,
} = useApiKeyLink({
appId: "claude-desktop",
category: apiKeyLinkCategory,
selectedPresetId,
presetEntries,
formWebsiteUrl: form.watch("websiteUrl") || "",
});
const applyDesktopPreset = (preset: ClaudeDesktopProviderPreset) => {
form.setValue("name", preset.nameKey ? t(preset.nameKey) : preset.name);
form.setValue("websiteUrl", preset.websiteUrl);
@@ -766,15 +783,15 @@ export function ClaudeDesktopProviderForm({
)}
</div>
) : (
<div className="space-y-1">
<Label>{"API Key"}</Label>
<Input
value={apiKey}
onChange={(event) => setApiKey(event.target.value)}
type="password"
placeholder="sk-..."
/>
</div>
<ApiKeySection
value={apiKey}
onChange={setApiKey}
category={apiKeyLinkCategory}
shouldShowLink={shouldShowApiKeyLink}
websiteUrl={apiKeyLinkWebsiteUrl}
isPartner={apiKeyLinkIsPartner}
partnerPromotionKey={apiKeyLinkPromotionKey}
/>
)}
<EndpointField
@@ -69,13 +69,6 @@ interface CodexFormFieldsProps {
autoSelect: boolean;
onAutoSelectChange: (checked: boolean) => void;
// Local routing / takeover
// takeoverEnabled gates model mapping + reasoning visibility; it is decoupled
// from the wire format so a native Responses provider can use model mapping
// without Chat Completions conversion.
takeoverEnabled: boolean;
onTakeoverEnabledChange: (enabled: boolean) => void;
// API Format
// Note: wire_api is always "responses" for Codex; apiFormat controls proxy-layer conversion
apiFormat: CodexApiFormat;
@@ -107,13 +100,24 @@ function createCatalogRow(seed?: Partial<CodexCatalogModel>): CodexCatalogRow {
model: seed?.model ?? "",
displayName: seed?.displayName ?? "",
contextWindow: seed?.contextWindow ?? "",
// Carry native-profile overrides verbatim (not user-editable in the row UI,
// but must survive load->save so the official catalog fidelity is kept).
...(seed?.supportsParallelToolCalls !== undefined
? { supportsParallelToolCalls: seed.supportsParallelToolCalls }
: {}),
...(seed?.inputModalities ? { inputModalities: seed.inputModalities } : {}),
...(seed?.baseInstructions
? { baseInstructions: seed.baseInstructions }
: {}),
};
}
// Compares rows (with rowId) to incoming models (without) by data fields only,
// so both sync effects can use the same equality definition.
// so both sync effects can use the same equality definition. Hidden native-profile
// fields are included so switching between providers with identical visible fields
// but different base_instructions / tools / modalities still rebuilds the rows.
function catalogRowsMatchModels(
rows: Array<Pick<CodexCatalogRow, "model" | "displayName" | "contextWindow">>,
rows: CodexCatalogModel[],
models: CodexCatalogModel[],
): boolean {
if (rows.length !== models.length) return false;
@@ -122,7 +126,13 @@ function catalogRowsMatchModels(
return (
row.model === (incoming.model ?? "") &&
(row.displayName ?? "") === (incoming.displayName ?? "") &&
String(row.contextWindow ?? "") === String(incoming.contextWindow ?? "")
String(row.contextWindow ?? "") ===
String(incoming.contextWindow ?? "") &&
(row.supportsParallelToolCalls ?? null) ===
(incoming.supportsParallelToolCalls ?? null) &&
(row.baseInstructions ?? "") === (incoming.baseInstructions ?? "") &&
JSON.stringify(row.inputModalities ?? []) ===
JSON.stringify(incoming.inputModalities ?? [])
);
});
}
@@ -146,8 +156,6 @@ export function CodexFormFields({
onCustomEndpointsChange,
autoSelect,
onAutoSelectChange,
takeoverEnabled,
onTakeoverEnabledChange,
apiFormat,
onApiFormatChange,
codexChatReasoning = {},
@@ -166,8 +174,8 @@ export function CodexFormFields({
const [fetchedModels, setFetchedModels] = useState<FetchedModel[]>([]);
const [isFetchingModels, setIsFetchingModels] = useState(false);
// takeoverEnabled 控制模型映射/思考能力的显示;isChatFormat 仅在选了
// Chat Completions 上游格式时为真(思考能力是 Chat 专属)
// 思考能力随 Chat 格式显示(仅 Chat Completions 转换路径用得上);模型映射常驻
//(填了才生成 catalog)。两者都已与「路由接管」概念解耦
const isChatFormat = apiFormat === "openai_chat";
const canEditCatalog = Boolean(onCatalogModelsChange);
const canEditReasoning = Boolean(onCodexChatReasoningChange);
@@ -176,13 +184,18 @@ export function CodexFormFields({
codexChatReasoning.supportsEffort === true;
const supportsEffort = codexChatReasoning.supportsEffort === true;
// takeoverEnabled 取代了旧的 needsLocalRouting:上游格式已与路由解耦。
// takeoverEnabled 为真说明预设/用户启用了本地路由;请求头/请求体覆盖也算高级值
// 高级区在有任何可见配置时自动展开(仅折叠→展开,不会自动折叠):自定义 UA /
// 请求覆盖 / 已填模型映射 / 原生 Responses(需维护 catalog/ 已配置思考能力
const hasRequestOverrides = Boolean(
localProxyHeadersOverride.trim() || localProxyBodyOverride.trim(),
);
const hasAnyAdvancedValue =
!!customUserAgent || hasRequestOverrides || takeoverEnabled;
!!customUserAgent ||
hasRequestOverrides ||
catalogModels.length > 0 ||
apiFormat === "openai_responses" ||
supportsThinking ||
supportsEffort;
const [advancedExpanded, setAdvancedExpanded] = useState(hasAnyAdvancedValue);
// 预设/编辑加载填充高级值后自动展开(仅从折叠→展开,不会自动折叠)
@@ -370,7 +383,7 @@ export function CodexFormFields({
/>
)}
{/* 高级选项 —— 本地路由映射/模型映射/思考能力/自定义 UA;预设供应商通常无需展开 */}
{/* 高级选项 —— 上游格式/模型映射/思考能力/自定义 UA;预设供应商通常无需展开 */}
{category !== "official" && (
<Collapsible
open={advancedExpanded}
@@ -398,17 +411,15 @@ export function CodexFormFields({
<p className="mt-1 ml-1 text-xs text-muted-foreground">
{t("codexConfig.advancedSectionHint", {
defaultValue:
"包含本地路由映射、模型映射、思考能力与自定义 User-Agent。供应商使用 Chat Completions 协议或非 GPT 模型时,需在此开启本地路由映射。",
"包含上游格式、模型映射、思考能力与自定义 User-Agent。使用 Chat Completions 协议的供应商需开启路由接管才能使用。",
})}
</p>
)}
<CollapsibleContent className="space-y-3 pt-3">
{/* +
Responses
{/* Chat Responses
沿 shouldShowSpeedTest cloud_provider */}
{shouldShowSpeedTest && (
<div className="space-y-3">
{/* 上游格式 —— 顶层独立选择,与路由开关解耦 */}
<div className="space-y-1.5">
<FormLabel htmlFor="codex-upstream-format">
{t("codexConfig.upstreamFormatLabel", {
@@ -430,7 +441,7 @@ export function CodexFormFields({
<SelectContent>
<SelectItem value="openai_chat">
{t("codexConfig.upstreamFormatChat", {
defaultValue: "Chat Completions转换",
defaultValue: "Chat Completions需开启路由",
})}
</SelectItem>
<SelectItem value="openai_responses">
@@ -443,43 +454,14 @@ export function CodexFormFields({
<p className="text-xs leading-relaxed text-muted-foreground">
{t("codexConfig.upstreamFormatHint", {
defaultValue:
"供应商原生是 Responses API 就选 Responses(直连,不转换格式);使用 Chat Completions 协议就选 Chat(转换为 Chat Completions)。",
"供应商原生是 Responses API 就选 Responses(直连,不转换格式);使用 Chat Completions 协议就选 Chat需开启路由接管才能转换为 Chat Completions)。",
})}
</p>
</div>
{/* 需要本地路由映射 —— 纯模型映射门控,与上游格式无关 */}
<div className="flex items-center justify-between gap-4 border-t border-border-default pt-3">
<div className="space-y-1">
<FormLabel>
{t("codexConfig.localRoutingToggle", {
defaultValue: "需要本地路由映射",
})}
</FormLabel>
<p className="text-xs leading-relaxed text-muted-foreground">
{takeoverEnabled
? t("codexConfig.localRoutingOnHint", {
defaultValue:
"打开后可在下方配置模型映射:让 Codex 的 /model 菜单显示自定义模型名,并把请求映射到真实上游模型。",
})
: t("codexConfig.localRoutingOffHint", {
defaultValue:
"供应商模型名无需改写、也无需在 /model 菜单展示自定义名称时,可保持关闭;需要模型映射时打开。",
})}
</p>
</div>
<Switch
checked={takeoverEnabled}
onCheckedChange={onTakeoverEnabledChange}
aria-label={t("codexConfig.localRoutingToggle", {
defaultValue: "需要本地路由映射",
})}
/>
</div>
</div>
)}
{takeoverEnabled && isChatFormat && canEditReasoning && (
{isChatFormat && canEditReasoning && (
<div
className={cn(
"space-y-3",
@@ -548,33 +530,17 @@ export function CodexFormFields({
</div>
)}
<div
className={cn(
"space-y-3",
(shouldShowSpeedTest ||
(takeoverEnabled && isChatFormat && canEditReasoning)) &&
"border-t border-border-default pt-3",
)}
>
<CustomUserAgentField
id="codex-custom-user-agent"
value={customUserAgent}
onChange={onCustomUserAgentChange}
/>
<div className="border-t border-border-default pt-3">
<LocalProxyRequestOverridesField
headersJson={localProxyHeadersOverride}
bodyJson={localProxyBodyOverride}
onHeadersJsonChange={onLocalProxyHeadersOverrideChange}
onBodyJsonChange={onLocalProxyBodyOverrideChange}
/>
</div>
</div>
{/* +
Responses UA 线 */}
{takeoverEnabled && canEditCatalog && (
<div className="space-y-4 border-t border-border-default pt-3">
{/* 模型映射 /
catalogChat Responses
model-catalogs.json UA */}
{canEditCatalog && (
<div
className={cn(
"space-y-4",
(shouldShowSpeedTest || (isChatFormat && canEditReasoning)) &&
"border-t border-border-default pt-3",
)}
>
<div className="space-y-1">
<div className="flex items-center justify-between gap-3">
<FormLabel>
@@ -713,6 +679,30 @@ export function CodexFormFields({
)}
</div>
)}
<div
className={cn(
"space-y-3",
(shouldShowSpeedTest ||
(isChatFormat && canEditReasoning) ||
canEditCatalog) &&
"border-t border-border-default pt-3",
)}
>
<CustomUserAgentField
id="codex-custom-user-agent"
value={customUserAgent}
onChange={onCustomUserAgentChange}
/>
<div className="border-t border-border-default pt-3">
<LocalProxyRequestOverridesField
headersJson={localProxyHeadersOverride}
bodyJson={localProxyBodyOverride}
onHeadersJsonChange={onLocalProxyHeadersOverrideChange}
onBodyJsonChange={onLocalProxyBodyOverrideChange}
/>
</div>
</div>
</CollapsibleContent>
</Collapsible>
)}
@@ -172,7 +172,7 @@ export function CommonConfigEditor({
/>
<span>
{t("claudeConfig.writeCommonConfig", {
defaultValue: "写入通用配置",
defaultValue: "应用通用配置",
})}
</span>
</label>
@@ -69,7 +69,7 @@ export const GeminiEnvSection: React.FC<GeminiEnvSectionProps> = ({
className="w-4 h-4 text-blue-500 bg-white dark:bg-gray-800 border-border-default rounded focus:ring-blue-500 dark:focus:ring-blue-400 focus:ring-2"
/>
{t("geminiConfig.writeCommonConfig", {
defaultValue: "写入通用配置",
defaultValue: "应用通用配置",
})}
</label>
</div>
@@ -330,7 +330,9 @@ export function HermesFormFields({
<ApiKeySection
value={apiKey}
onChange={onApiKeyChange}
category={category}
// Hermes 没有 OAuth-only 的免 key 官方供应商:即便是 official 预设
// (如 Nous Research)也需用户自填 key,故不让 official 禁用输入框。
category={category === "official" ? undefined : category}
shouldShowLink={shouldShowApiKeyLink}
websiteUrl={websiteUrl}
isPartner={isPartner}
@@ -257,7 +257,9 @@ export function OpenClawFormFields({
<ApiKeySection
value={apiKey}
onChange={onApiKeyChange}
category={category}
// OpenClaw 的 API key 始终由用户自填,没有 OAuth-only 的免 key 官方供应商,
// 故不让 official 禁用输入框(与 Hermes 对齐)。
category={category === "official" ? undefined : category}
shouldShowLink={shouldShowApiKeyLink}
websiteUrl={websiteUrl}
isPartner={isPartner}
+18 -27
View File
@@ -150,16 +150,6 @@ const codexApiFormatFromWireApi = (
}
};
// 从已保存的 settingsConfig 推断 Codex 模型映射条目数(用于决定本地路由初始开关)。
const codexCatalogCountFromSettings = (settingsConfig: unknown): number => {
if (settingsConfig && typeof settingsConfig === "object") {
const models = (settingsConfig as { modelCatalog?: { models?: unknown } })
.modelCatalog?.models;
return Array.isArray(models) ? models.length : 0;
}
return 0;
};
export const normalizeCodexCatalogModelsForSave = (
models: CodexCatalogModel[],
): CodexCatalogModel[] => {
@@ -180,10 +170,24 @@ export const normalizeCodexCatalogModelsForSave = (
? Number.parseInt(rawContextWindow, 10)
: undefined;
const inputModalities = item.inputModalities?.filter(
(m) => typeof m === "string" && m.trim(),
);
const baseInstructions = item.baseInstructions?.trim();
normalized.push({
model,
...(displayName ? { displayName } : {}),
...(contextWindow && contextWindow > 0 ? { contextWindow } : {}),
// Native Responses profile overrides (ignored by the chat/proxy profile).
...(typeof item.supportsParallelToolCalls === "boolean"
? { supportsParallelToolCalls: item.supportsParallelToolCalls }
: {}),
...(inputModalities && inputModalities.length > 0
? { inputModalities }
: {}),
...(baseInstructions ? { baseInstructions } : {}),
});
}
@@ -594,14 +598,6 @@ function ProviderFormFull({
const [localCodexApiFormat, setLocalCodexApiFormat] =
useState<CodexApiFormat>(initialCodexApiFormat);
// 本地路由(接管)开关 —— 纯模型映射门控,与上游格式完全独立。
// 没有独立持久化字段,初值仅按「是否已配置模型映射」推断(有 catalog 即视为
// 接管已开)。只在 useState 初始化与预设重置点设置,跟 localCodexApiFormat
// 对称,避免漂移。
const [codexTakeoverEnabled, setCodexTakeoverEnabled] = useState<boolean>(
() => codexCatalogCountFromSettings(initialData?.settingsConfig) > 0,
);
const { configError: codexConfigError, debouncedValidate } =
useCodexTomlValidation();
@@ -631,7 +627,6 @@ function ProviderFormFull({
const template = getCodexCustomTemplate();
resetCodexConfig(template.auth, template.config);
setCodexChatReasoning({});
setCodexTakeoverEnabled(false);
}
}, [appId, initialData, selectedPresetId, resetCodexConfig]);
@@ -1277,8 +1272,11 @@ function ProviderFormFull({
category !== "official" && (codexConfig ?? "").trim()
? setCodexWireApi(codexConfig ?? "", "responses")
: (codexConfig ?? "");
// 模型映射与「路由接管」解耦:对所有非官方供应商,填了就持久化
//(Chat 生成兼容路由、原生 Responses 生成 model-catalogs.json),
// 留空归一化为 [] 即不写。后端只看 modelCatalog.models 是否非空。
const normalizedCatalogModels =
category !== "official" && codexTakeoverEnabled
category !== "official"
? normalizeCodexCatalogModelsForSave(codexCatalogModels)
: [];
// Sync first catalog row's model into config.toml so Codex uses it as default
@@ -1474,7 +1472,6 @@ function ProviderFormFull({
codexChatReasoning:
appId === "codex" &&
category !== "official" &&
codexTakeoverEnabled &&
localCodexApiFormat === "openai_chat"
? normalizeCodexChatReasoningForSave(codexChatReasoning)
: undefined,
@@ -1627,8 +1624,6 @@ function ProviderFormFull({
codexApiFormatFromWireApi(extractCodexWireApi(template.config)) ??
"openai_responses",
);
// 自定义模板无模型映射,路由默认关闭
setCodexTakeoverEnabled(false);
}
if (appId === "gemini") {
resetGeminiConfig({}, {});
@@ -1671,8 +1666,6 @@ function ProviderFormFull({
codexApiFormatFromWireApi(extractCodexWireApi(config)) ??
"openai_responses",
);
// 路由开关与格式无关,仅按预设是否带模型映射决定
setCodexTakeoverEnabled((preset.modelCatalog?.length ?? 0) > 0);
form.reset({
name: preset.nameKey ? t(preset.nameKey) : preset.name,
@@ -2145,8 +2138,6 @@ function ProviderFormFull({
}
autoSelect={endpointAutoSelect}
onAutoSelectChange={setEndpointAutoSelect}
takeoverEnabled={codexTakeoverEnabled}
onTakeoverEnabledChange={setCodexTakeoverEnabled}
apiFormat={localCodexApiFormat}
onApiFormatChange={handleCodexApiFormatChange}
codexChatReasoning={codexChatReasoning}
@@ -5,6 +5,7 @@ import type { ProviderPreset } from "@/config/claudeProviderPresets";
import type { CodexProviderPreset } from "@/config/codexProviderPresets";
import type { GeminiProviderPreset } from "@/config/geminiProviderPresets";
import type { OpenCodeProviderPreset } from "@/config/opencodeProviderPresets";
import type { ClaudeDesktopProviderPreset } from "@/config/claudeDesktopProviderPresets";
type PresetEntry = {
id: string;
@@ -12,7 +13,8 @@ type PresetEntry = {
| ProviderPreset
| CodexProviderPreset
| GeminiProviderPreset
| OpenCodeProviderPreset;
| OpenCodeProviderPreset
| ClaudeDesktopProviderPreset;
};
interface UseApiKeyLinkProps {
@@ -80,9 +82,12 @@ export function useApiKeyLink({
return {
shouldShowApiKeyLink:
appId === "claude" ||
appId === "claude-desktop" ||
appId === "codex" ||
appId === "gemini" ||
appId === "opencode"
appId === "opencode" ||
appId === "openclaw" ||
appId === "hermes"
? shouldShowApiKeyLink
: false,
websiteUrl: getWebsiteUrl,
@@ -66,23 +66,51 @@ export function useCodexConfigState({ initialData }: UseCodexConfigStateProps) {
: [];
setCodexCatalogModels(
rawCatalogModels
.map((item: any) => ({
model: typeof item?.model === "string" ? item.model : "",
displayName:
typeof item?.displayName === "string"
? item.displayName
: typeof item?.display_name === "string"
? item.display_name
: "",
contextWindow:
typeof item?.contextWindow === "string" ||
typeof item?.contextWindow === "number"
? item.contextWindow
: typeof item?.context_window === "string" ||
typeof item?.context_window === "number"
? item.context_window
: "",
}))
.map((item: any) => {
// 隐藏字段(原生 Responses profile 用)不在行 UI 暴露,但必须 load→save
// 原样保留,否则编辑保存 MiMo/MiniMax 等会丢官方 base_instructions、
// 并行工具、图像模态。DB SSOT 为 camelCase、live 反解兜底可能为 snake_case
// 双格式兼容(与 displayName/contextWindow 一致)。
const supportsParallelToolCalls =
typeof item?.supportsParallelToolCalls === "boolean"
? item.supportsParallelToolCalls
: typeof item?.supports_parallel_tool_calls === "boolean"
? item.supports_parallel_tool_calls
: undefined;
const inputModalities = Array.isArray(item?.inputModalities)
? item.inputModalities
: Array.isArray(item?.input_modalities)
? item.input_modalities
: undefined;
const baseInstructions =
typeof item?.baseInstructions === "string"
? item.baseInstructions
: typeof item?.base_instructions === "string"
? item.base_instructions
: undefined;
return {
model: typeof item?.model === "string" ? item.model : "",
displayName:
typeof item?.displayName === "string"
? item.displayName
: typeof item?.display_name === "string"
? item.display_name
: "",
contextWindow:
typeof item?.contextWindow === "string" ||
typeof item?.contextWindow === "number"
? item.contextWindow
: typeof item?.context_window === "string" ||
typeof item?.context_window === "number"
? item.context_window
: "",
...(supportsParallelToolCalls !== undefined
? { supportsParallelToolCalls }
: {}),
...(inputModalities ? { inputModalities } : {}),
...(baseInstructions ? { baseInstructions } : {}),
};
})
.filter((item: CodexCatalogModel) => item.model.trim()),
);
@@ -5,6 +5,8 @@ import { providerPresets } from "@/config/claudeProviderPresets";
import { codexProviderPresets } from "@/config/codexProviderPresets";
import { geminiProviderPresets } from "@/config/geminiProviderPresets";
import { opencodeProviderPresets } from "@/config/opencodeProviderPresets";
import { openclawProviderPresets } from "@/config/openclawProviderPresets";
import { hermesProviderPresets } from "@/config/hermesProviderPresets";
interface UseProviderCategoryProps {
appId: AppId;
@@ -44,7 +46,7 @@ export function useProviderCategory({
// 从预设 ID 提取索引
const match = selectedPresetId.match(
/^(claude|codex|gemini|opencode)-(\d+)$/,
/^(claude|codex|gemini|opencode|openclaw|hermes)-(\d+)$/,
);
if (!match) return;
@@ -75,6 +77,16 @@ export function useProviderCategory({
if (preset) {
setCategory(preset.category || undefined);
}
} else if (type === "openclaw" && appId === "openclaw") {
const preset = openclawProviderPresets[index];
if (preset) {
setCategory(preset.category || undefined);
}
} else if (type === "hermes" && appId === "hermes") {
const preset = hermesProviderPresets[index];
if (preset) {
setCategory(preset.category || undefined);
}
}
}, [appId, selectedPresetId, isEditMode, initialCategory]);
+593 -21
View File
@@ -16,6 +16,11 @@ import {
FileText,
X,
CheckSquare,
ListTree,
List,
ChevronDown,
ChevronRight,
ChevronsDownUp,
} from "lucide-react";
import {
useDeleteSessionMutation,
@@ -27,6 +32,7 @@ import type { SessionMeta } from "@/types";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Badge } from "@/components/ui/badge";
import { Checkbox } from "@/components/ui/checkbox";
import {
Select,
SelectContent,
@@ -35,6 +41,11 @@ import {
} from "@/components/ui/select";
import { Card, CardHeader, CardTitle, CardContent } from "@/components/ui/card";
import { ScrollArea } from "@/components/ui/scroll-area";
import {
Collapsible,
CollapsibleContent,
CollapsibleTrigger,
} from "@/components/ui/collapsible";
import { ConfirmDialog } from "@/components/ConfirmDialog";
import {
Tooltip,
@@ -56,10 +67,19 @@ import {
getBaseName,
getProviderIconName,
getProviderLabel,
getSessionDirectoryGroupKey,
getSessionKey,
groupSessionsByProviderAndDirectory,
type SessionDirectoryGroup,
type SessionProviderGroup,
shouldHideCodexMessageFromToc,
} from "./utils";
const SESSION_LIST_VIEW_MODE_STORAGE_KEY =
"cc-switch.sessionManager.listViewMode";
const SESSION_GROUP_EXPANSION_STORAGE_KEY =
"cc-switch.sessionManager.groupExpansionState";
type ProviderFilter =
| "all"
| "codex"
@@ -69,6 +89,102 @@ type ProviderFilter =
| "gemini"
| "hermes";
type SessionListViewMode = "flat" | "grouped";
type GroupSelectionState = {
checked: boolean | "indeterminate";
isSelected: boolean;
selectedCount: number;
selectableCount: number;
};
type SessionGroupExpansionState = {
expandedProviderIds: Set<string>;
expandedDirectoryKeys: Set<string>;
};
const readInitialSessionListViewMode = (): SessionListViewMode => {
if (typeof window === "undefined") return "flat";
const stored = window.localStorage.getItem(
SESSION_LIST_VIEW_MODE_STORAGE_KEY,
);
return stored === "grouped" || stored === "flat" ? stored : "flat";
};
const readInitialSessionGroupExpansionState =
(): SessionGroupExpansionState => {
if (typeof window === "undefined") {
return {
expandedProviderIds: new Set(),
expandedDirectoryKeys: new Set(),
};
}
try {
const stored = window.localStorage.getItem(
SESSION_GROUP_EXPANSION_STORAGE_KEY,
);
const parsed = stored ? JSON.parse(stored) : null;
if (!parsed || typeof parsed !== "object") {
return {
expandedProviderIds: new Set(),
expandedDirectoryKeys: new Set(),
};
}
const expandedProviderIds = Array.isArray(parsed.expandedProviderIds)
? parsed.expandedProviderIds.filter(
(providerId: unknown): providerId is string =>
typeof providerId === "string",
)
: [];
const expandedDirectoryKeys = Array.isArray(parsed.expandedDirectoryKeys)
? parsed.expandedDirectoryKeys.filter(
(directoryKey: unknown): directoryKey is string =>
typeof directoryKey === "string",
)
: [];
return {
expandedProviderIds: new Set(expandedProviderIds),
expandedDirectoryKeys: new Set(expandedDirectoryKeys),
};
} catch {
return {
expandedProviderIds: new Set(),
expandedDirectoryKeys: new Set(),
};
}
};
const serializeSessionGroupExpansionState = (
expandedProviderGroups: Set<string>,
expandedDirectoryGroups: Set<string>,
) =>
JSON.stringify({
expandedProviderIds: Array.from(expandedProviderGroups).sort(),
expandedDirectoryKeys: Array.from(expandedDirectoryGroups).sort(),
});
const filterSetToAllowedValues = (
current: Set<string>,
allowedValues: Set<string>,
) => {
let changed = false;
const next = new Set<string>();
current.forEach((value) => {
if (allowedValues.has(value)) {
next.add(value);
} else {
changed = true;
}
});
return changed ? next : current;
};
export function SessionManagerPage({ appId }: { appId: string }) {
const { t } = useTranslation();
const queryClient = useQueryClient();
@@ -96,6 +212,18 @@ export function SessionManagerPage({ appId }: { appId: string }) {
appId as ProviderFilter,
);
const [selectedKey, setSelectedKey] = useState<string | null>(null);
const [listViewMode, setListViewMode] = useState<SessionListViewMode>(
readInitialSessionListViewMode,
);
const [initialGroupExpansionState] = useState(
readInitialSessionGroupExpansionState,
);
const [expandedProviderGroups, setExpandedProviderGroups] = useState<
Set<string>
>(() => initialGroupExpansionState.expandedProviderIds);
const [expandedDirectoryGroups, setExpandedDirectoryGroups] = useState<
Set<string>
>(() => initialGroupExpansionState.expandedDirectoryKeys);
// 使用 FlexSearch 全文搜索
const { search: searchSessions } = useSessionSearch({
@@ -107,6 +235,57 @@ export function SessionManagerPage({ appId }: { appId: string }) {
return searchSessions(search);
}, [searchSessions, search]);
const groupedSessions = useMemo(
() =>
groupSessionsByProviderAndDirectory(
filteredSessions,
t("sessionManager.unknownDirectory", {
defaultValue: "未知目录",
}),
),
[filteredSessions, t],
);
const validGroupExpansionKeys = useMemo(
() => ({
providerIds: new Set(sessions.map((session) => session.providerId)),
directoryKeys: new Set(
sessions.map((session) =>
getSessionDirectoryGroupKey(session.providerId, session.projectDir),
),
),
}),
[sessions],
);
useEffect(() => {
window.localStorage.setItem(
SESSION_LIST_VIEW_MODE_STORAGE_KEY,
listViewMode,
);
}, [listViewMode]);
useEffect(() => {
window.localStorage.setItem(
SESSION_GROUP_EXPANSION_STORAGE_KEY,
serializeSessionGroupExpansionState(
expandedProviderGroups,
expandedDirectoryGroups,
),
);
}, [expandedDirectoryGroups, expandedProviderGroups]);
useEffect(() => {
if (isLoading) return;
setExpandedProviderGroups((current) =>
filterSetToAllowedValues(current, validGroupExpansionKeys.providerIds),
);
setExpandedDirectoryGroups((current) =>
filterSetToAllowedValues(current, validGroupExpansionKeys.directoryKeys),
);
}, [isLoading, validGroupExpansionKeys]);
useEffect(() => {
if (filteredSessions.length === 0) {
setSelectedKey(null);
@@ -131,6 +310,15 @@ export function SessionManagerPage({ appId }: { appId: string }) {
);
}, [filteredSessions, selectedKey]);
const listViewModeLabel =
listViewMode === "grouped"
? t("sessionManager.viewModeGrouped", {
defaultValue: "分类",
})
: t("sessionManager.viewModeFlat", {
defaultValue: "列表",
});
const { data: messages = [], isLoading: isLoadingMessages } =
useSessionMessagesQuery(
selectedSession?.providerId,
@@ -402,6 +590,28 @@ export function SessionManagerPage({ appId }: { appId: string }) {
selectedSessionKeys.has(getSessionKey(session)),
);
const getGroupSelectionState = (
groupSessions: SessionMeta[],
): GroupSelectionState => {
const selectableSessions = groupSessions.filter((session) =>
Boolean(session.sourcePath),
);
const selectedCount = selectableSessions.filter((session) =>
selectedSessionKeys.has(getSessionKey(session)),
).length;
const isSelected =
selectableSessions.length > 0 &&
selectedCount === selectableSessions.length;
return {
checked:
selectedCount === 0 ? false : isSelected ? true : "indeterminate",
isSelected,
selectedCount,
selectableCount: selectableSessions.length,
};
};
const toggleSessionChecked = (session: SessionMeta, checked: boolean) => {
if (!session.sourcePath) return;
const key = getSessionKey(session);
@@ -416,6 +626,140 @@ export function SessionManagerPage({ appId }: { appId: string }) {
});
};
const toggleSessionGroupChecked = (
groupSessions: SessionMeta[],
checked: boolean,
) => {
const selectableSessions = groupSessions.filter((session) =>
Boolean(session.sourcePath),
);
if (selectableSessions.length === 0) return;
setSelectedSessionKeys((current) => {
const next = new Set(current);
selectableSessions.forEach((session) => {
const sessionKey = getSessionKey(session);
if (checked) {
next.add(sessionKey);
} else {
next.delete(sessionKey);
}
});
return next;
});
};
const toggleProviderGroup = (providerId: string) => {
setExpandedProviderGroups((current) => {
const next = new Set(current);
if (next.has(providerId)) {
next.delete(providerId);
} else {
next.add(providerId);
}
return next;
});
};
const toggleDirectoryGroup = (directoryKey: string) => {
setExpandedDirectoryGroups((current) => {
const next = new Set(current);
if (next.has(directoryKey)) {
next.delete(directoryKey);
} else {
next.add(directoryKey);
}
return next;
});
};
const handleCollapseAllGroups = () => {
setExpandedProviderGroups(new Set());
setExpandedDirectoryGroups(new Set());
};
const renderSessionItem = (session: SessionMeta) => {
const sessionKey = getSessionKey(session);
const isSelected = selectedKey !== null && sessionKey === selectedKey;
return (
<SessionItem
key={sessionKey}
session={session}
isSelected={isSelected}
selectionMode={selectionMode}
searchQuery={search}
isChecked={selectedSessionKeys.has(sessionKey)}
isCheckDisabled={!session.sourcePath}
onSelect={setSelectedKey}
onToggleChecked={(checked) => toggleSessionChecked(session, checked)}
/>
);
};
const renderGroupSelectionBadge = (
selectionState: GroupSelectionState,
totalCount: number,
variant: "secondary" | "outline",
) => (
<Badge variant={variant} className="shrink-0 text-xs">
{selectionMode
? `${selectionState.selectedCount}/${selectionState.selectableCount}`
: totalCount}
</Badge>
);
const renderProviderGroupCheckbox = (
providerGroup: SessionProviderGroup,
providerLabel: string,
selectionState: GroupSelectionState,
) => {
if (!selectionMode) return null;
return (
<Checkbox
checked={selectionState.checked}
disabled={selectionState.selectableCount === 0}
aria-label={t("sessionManager.selectProviderGroupForBatch", {
defaultValue: "选择 {{provider}} 供应商分组内会话",
provider: providerLabel,
})}
onClick={(event) => event.stopPropagation()}
onCheckedChange={() =>
toggleSessionGroupChecked(
providerGroup.sessions,
!selectionState.isSelected,
)
}
/>
);
};
const renderDirectoryGroupCheckbox = (
directoryGroup: SessionDirectoryGroup,
selectionState: GroupSelectionState,
) => {
if (!selectionMode) return null;
return (
<Checkbox
checked={selectionState.checked}
disabled={selectionState.selectableCount === 0}
aria-label={t("sessionManager.selectDirectoryGroupForBatch", {
defaultValue: "选择 {{directory}} 目录分组内会话",
directory: directoryGroup.label,
})}
onClick={(event) => event.stopPropagation()}
onCheckedChange={() =>
toggleSessionGroupChecked(
directoryGroup.sessions,
!selectionState.isSelected,
)
}
/>
);
};
const handleToggleSelectAll = () => {
setSelectedSessionKeys((current) => {
const next = new Set(current);
@@ -570,6 +914,85 @@ export function SessionManagerPage({ appId }: { appId: string }) {
</TooltipContent>
</Tooltip>
)}
<Select
value={listViewMode}
onValueChange={(value) =>
setListViewMode(value as SessionListViewMode)
}
>
<Tooltip>
<TooltipTrigger asChild>
<SelectTrigger
className="size-7 p-0 justify-center border-0 bg-transparent hover:bg-muted"
aria-label={t(
"sessionManager.viewModeTooltip",
{
defaultValue: "查看方式",
},
)}
>
<span className="sr-only">
{t("sessionManager.viewModeTooltip", {
defaultValue: "查看方式",
})}
</span>
{listViewMode === "grouped" ? (
<ListTree className="size-3.5" />
) : (
<List className="size-3.5" />
)}
</SelectTrigger>
</TooltipTrigger>
<TooltipContent>{listViewModeLabel}</TooltipContent>
</Tooltip>
<SelectContent className="w-40">
<SelectItem value="flat">
<div className="flex items-center gap-2">
<List className="size-3.5" />
<span>
{t("sessionManager.viewModeFlat", {
defaultValue: "列表",
})}
</span>
</div>
</SelectItem>
<SelectItem value="grouped">
<div className="flex items-center gap-2">
<ListTree className="size-3.5" />
<span>
{t("sessionManager.viewModeGrouped", {
defaultValue: "分类",
})}
</span>
</div>
</SelectItem>
</SelectContent>
</Select>
{listViewMode === "grouped" && (
<Tooltip>
<TooltipTrigger asChild>
<Button
variant="ghost"
size="icon"
className="size-7"
aria-label={t(
"sessionManager.collapseAllGroups",
{
defaultValue: "全部收起",
},
)}
onClick={handleCollapseAllGroups}
>
<ChevronsDownUp className="size-3.5" />
</Button>
</TooltipTrigger>
<TooltipContent>
{t("sessionManager.collapseAllGroups", {
defaultValue: "全部收起",
})}
</TooltipContent>
</Tooltip>
)}
<Tooltip>
<TooltipTrigger asChild>
<Button
@@ -600,7 +1023,20 @@ export function SessionManagerPage({ appId }: { appId: string }) {
>
<Tooltip>
<TooltipTrigger asChild>
<SelectTrigger className="size-7 p-0 justify-center border-0 bg-transparent hover:bg-muted">
<SelectTrigger
className="size-7 p-0 justify-center border-0 bg-transparent hover:bg-muted"
aria-label={t(
"sessionManager.providerFilterTooltip",
{
defaultValue: "供应商筛选",
},
)}
>
<span className="sr-only">
{t("sessionManager.providerFilterTooltip", {
defaultValue: "供应商筛选",
})}
</span>
<ProviderIcon
icon={
providerFilter === "all"
@@ -784,32 +1220,168 @@ export function SessionManagerPage({ appId }: { appId: string }) {
{t("sessionManager.noSessions")}
</p>
</div>
) : (
<div className="space-y-1">
{filteredSessions.map((session) => {
const isSelected =
selectedKey !== null &&
getSessionKey(session) === selectedKey;
) : listViewMode === "grouped" ? (
<div className="space-y-2">
{groupedSessions.map((providerGroup) => {
const providerOpen = expandedProviderGroups.has(
providerGroup.providerId,
);
const providerLabel = getProviderLabel(
providerGroup.providerId,
t,
);
const providerSelectionState = getGroupSelectionState(
providerGroup.sessions,
);
return (
<SessionItem
key={getSessionKey(session)}
session={session}
isSelected={isSelected}
selectionMode={selectionMode}
searchQuery={search}
isChecked={selectedSessionKeys.has(
getSessionKey(session),
)}
isCheckDisabled={!session.sourcePath}
onSelect={setSelectedKey}
onToggleChecked={(checked) =>
toggleSessionChecked(session, checked)
<Collapsible
key={providerGroup.providerId}
open={providerOpen}
onOpenChange={() =>
toggleProviderGroup(providerGroup.providerId)
}
/>
>
<div className="flex w-full items-center gap-2 rounded-md border bg-muted/40 px-2.5 py-2 transition-colors hover:bg-muted">
{renderProviderGroupCheckbox(
providerGroup,
providerLabel,
providerSelectionState,
)}
<CollapsibleTrigger asChild>
<button
type="button"
className="flex min-w-0 flex-1 items-center gap-2 text-left"
aria-label={t(
"sessionManager.toggleProviderGroup",
{
defaultValue:
"展开或折叠 {{provider}} 供应商分组",
provider: providerLabel,
},
)}
>
{providerOpen ? (
<ChevronDown className="size-3.5 shrink-0 text-muted-foreground" />
) : (
<ChevronRight className="size-3.5 shrink-0 text-muted-foreground" />
)}
<ProviderIcon
icon={getProviderIconName(
providerGroup.providerId,
)}
name={providerGroup.providerId}
size={16}
/>
<span className="min-w-0 flex-1 truncate text-sm font-medium">
{providerLabel}
</span>
{renderGroupSelectionBadge(
providerSelectionState,
providerGroup.sessions.length,
"secondary",
)}
</button>
</CollapsibleTrigger>
</div>
<CollapsibleContent className="mt-1 space-y-1 pl-2">
{providerGroup.directories.map(
(directoryGroup) => {
const directoryOpen =
expandedDirectoryGroups.has(
directoryGroup.key,
);
const directorySelectionState =
getGroupSelectionState(
directoryGroup.sessions,
);
return (
<Collapsible
key={directoryGroup.key}
open={directoryOpen}
onOpenChange={() =>
toggleDirectoryGroup(
directoryGroup.key,
)
}
>
<div className="flex w-full items-center gap-2 rounded-md px-2.5 py-1.5 text-muted-foreground transition-colors hover:bg-muted hover:text-foreground">
{renderDirectoryGroupCheckbox(
directoryGroup,
directorySelectionState,
)}
<CollapsibleTrigger asChild>
<button
type="button"
className="flex min-w-0 flex-1 items-center gap-2 text-left"
aria-label={t(
"sessionManager.toggleDirectoryGroup",
{
defaultValue:
"展开或折叠 {{directory}} 目录分组",
directory:
directoryGroup.label,
},
)}
>
{directoryOpen ? (
<ChevronDown className="size-3.5 shrink-0" />
) : (
<ChevronRight className="size-3.5 shrink-0" />
)}
<FolderOpen className="size-3.5 shrink-0" />
<Tooltip>
<TooltipTrigger asChild>
<span className="min-w-0 flex-1 truncate text-xs font-medium">
{directoryGroup.label}
</span>
</TooltipTrigger>
<TooltipContent
side="bottom"
className="max-w-xs"
>
<p className="font-mono text-xs break-all">
{directoryGroup.projectDir ??
t(
"sessionManager.unknownDirectory",
{
defaultValue:
"未知目录",
},
)}
</p>
</TooltipContent>
</Tooltip>
{renderGroupSelectionBadge(
directorySelectionState,
directoryGroup.sessions.length,
"outline",
)}
</button>
</CollapsibleTrigger>
</div>
<CollapsibleContent className="mt-1 space-y-1 pl-3">
{directoryGroup.sessions.map(
(session) =>
renderSessionItem(session),
)}
</CollapsibleContent>
</Collapsible>
);
},
)}
</CollapsibleContent>
</Collapsible>
);
})}
</div>
) : (
<div className="space-y-1">
{filteredSessions.map((session) =>
renderSessionItem(session),
)}
</div>
)}
</div>
</ScrollArea>
+75
View File
@@ -4,6 +4,20 @@ import { SessionMeta } from "@/types";
const CODEX_IDE_CONTEXT_PREFIX = "# Context from my IDE setup:";
const CODEX_REQUEST_MARKER = "my request for codex";
export const UNKNOWN_PROJECT_DIR_KEY = "__unknown_project_dir__";
export interface SessionDirectoryGroup {
key: string;
projectDir: string | null;
label: string;
sessions: SessionMeta[];
}
export interface SessionProviderGroup {
providerId: string;
sessions: SessionMeta[];
directories: SessionDirectoryGroup[];
}
const getCodexRequestHeadingPayload = (lineText: string) => {
if (!lineText.startsWith("#")) return null;
@@ -55,6 +69,14 @@ const extractCodexPromptFromIdeContext = (content: string) => {
export const getSessionKey = (session: SessionMeta) =>
`${session.providerId}:${session.sessionId}:${session.sourcePath ?? ""}`;
export const getSessionDirectoryGroupKey = (
providerId: string,
projectDir?: string | null,
) => {
const trimmed = projectDir?.trim();
return `${providerId}:${trimmed || UNKNOWN_PROJECT_DIR_KEY}`;
};
export const getBaseName = (value?: string | null) => {
if (!value) return "";
const trimmed = value.trim();
@@ -131,6 +153,59 @@ export const formatSessionTitle = (session: SessionMeta) => {
);
};
export const groupSessionsByProviderAndDirectory = (
sessions: SessionMeta[],
unknownDirectoryLabel: string,
): SessionProviderGroup[] => {
const providerGroups: SessionProviderGroup[] = [];
const providerGroupMap = new Map<string, SessionProviderGroup>();
const directoryGroupMaps = new Map<
string,
Map<string, SessionDirectoryGroup>
>();
sessions.forEach((session) => {
let providerGroup = providerGroupMap.get(session.providerId);
if (!providerGroup) {
providerGroup = {
providerId: session.providerId,
sessions: [],
directories: [],
};
providerGroupMap.set(session.providerId, providerGroup);
providerGroups.push(providerGroup);
directoryGroupMaps.set(session.providerId, new Map());
}
providerGroup.sessions.push(session);
const trimmedProjectDir = session.projectDir?.trim() || null;
const directoryKey = getSessionDirectoryGroupKey(
session.providerId,
trimmedProjectDir,
);
const directoryGroups = directoryGroupMaps.get(session.providerId)!;
let directoryGroup = directoryGroups.get(directoryKey);
if (!directoryGroup) {
directoryGroup = {
key: directoryKey,
projectDir: trimmedProjectDir,
label: trimmedProjectDir
? getBaseName(trimmedProjectDir) || trimmedProjectDir
: unknownDirectoryLabel,
sessions: [],
};
directoryGroups.set(directoryKey, directoryGroup);
providerGroup.directories.push(directoryGroup);
}
directoryGroup.sessions.push(session);
});
return providerGroups;
};
export const shouldHideCodexMessageFromToc = (content: string) => {
const trimmed = content.trim();
return (
+7 -2
View File
@@ -1,6 +1,6 @@
import * as React from "react";
import * as CheckboxPrimitive from "@radix-ui/react-checkbox";
import { Check } from "lucide-react";
import { Check, Minus } from "lucide-react";
import { cn } from "@/lib/utils";
@@ -12,6 +12,7 @@ const Checkbox = React.forwardRef<
ref={ref}
className={cn(
"grid place-content-center peer h-4 w-4 shrink-0 rounded-sm border border-primary ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground",
"data-[state=indeterminate]:bg-primary data-[state=indeterminate]:text-primary-foreground",
className,
)}
{...props}
@@ -19,7 +20,11 @@ const Checkbox = React.forwardRef<
<CheckboxPrimitive.Indicator
className={cn("grid place-content-center text-current")}
>
<Check className="h-4 w-4" />
{props.checked === "indeterminate" ? (
<Minus className="h-4 w-4" />
) : (
<Check className="h-4 w-4" />
)}
</CheckboxPrimitive.Indicator>
</CheckboxPrimitive.Root>
));
+1 -1
View File
@@ -37,7 +37,7 @@ const SelectContent = React.forwardRef<
<SelectPrimitive.Content
ref={ref}
className={cn(
"relative z-[100] min-w-[8rem] overflow-hidden rounded-md border border-border-default bg-popover text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
"relative z-[100] max-h-[min(24rem,var(--radix-select-content-available-height))] min-w-[8rem] overflow-y-auto overflow-x-hidden rounded-md border border-border-default bg-popover text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
className,
)}
position={position}
@@ -331,7 +331,7 @@ export function UsageDateRangePicker({
</Button>
</PopoverTrigger>
<PopoverContent
className="w-[340px] max-w-[calc(100vw-2rem)] p-3 sm:w-[620px]"
className="usage-range-popover w-[620px] max-w-[calc(100vw-2rem)] p-3"
align="end"
>
{/* Preset shortcuts */}
@@ -353,9 +353,9 @@ export function UsageDateRangePicker({
))}
</div>
<div className="flex flex-col gap-3 sm:flex-row">
<div className="usage-range-layout flex flex-col gap-3">
{/* Left: date fields */}
<div className="space-y-2 sm:w-[250px] sm:flex-none">
<div className="usage-range-fields space-y-2">
<p className="text-xs text-muted-foreground">
{t("usage.customRangeHint", "支持日期与时间,最长 30 天")}
</p>
@@ -403,7 +403,7 @@ export function UsageDateRangePicker({
</div>
{/* Right: calendar */}
<div className="rounded-lg border border-border/50 bg-muted/30 p-2.5 sm:min-w-0 sm:flex-1">
<div className="usage-range-calendar rounded-lg border border-border/50 bg-muted/30 p-2.5">
{/* Month navigation */}
<div className="flex items-center justify-between mb-1.5">
<Button
+110 -20
View File
@@ -32,7 +32,7 @@ export interface ClaudeDesktopRoutePreset {
* `next_catalog_safe_route_id` routeId
*/
export const CLAUDE_DESKTOP_ROLE_ROUTE_IDS = {
sonnet: "claude-sonnet-4-6",
sonnet: "claude-sonnet-5",
opus: "claude-opus-4-8",
fable: "claude-fable-5",
haiku: "claude-haiku-4-5",
@@ -163,7 +163,7 @@ export const claudeDesktopProviderPresets: ClaudeDesktopProviderPreset[] = [
mode: "direct",
apiFormat: "anthropic",
modelRoutes: mappedRoutes(
"anthropic/claude-sonnet-4.6",
"anthropic/claude-sonnet-5",
"anthropic/claude-opus-4.8",
"anthropic/claude-haiku-4.5",
),
@@ -184,10 +184,76 @@ export const claudeDesktopProviderPresets: ClaudeDesktopProviderPreset[] = [
partnerPromotionKey: "patewayai",
icon: "pateway",
},
{
name: "Qiniu",
nameKey: "providerForm.presets.qiniu",
websiteUrl: "https://s.qiniu.com/nMvAvy",
apiKeyUrl: "https://s.qiniu.com/nMvAvy",
category: "aggregator",
baseUrl: "https://api.qnaigc.com",
mode: "direct",
apiFormat: "anthropic",
modelRoutes: passthroughRoutes(),
endpointCandidates: ["https://api.qnaigc.com", "https://api.modelink.ai"],
isPartner: true,
partnerPromotionKey: "qiniu",
icon: "qiniu",
},
{
name: "FennoAI",
websiteUrl: "https://api.fenno.ai",
apiKeyUrl:
"https://api.fenno.ai/register?redirect=/purchase?tab=subscription%26group=16&aff=P9MR3D3PLCNL",
category: "aggregator",
baseUrl: "https://api.fenno.ai",
mode: "direct",
apiFormat: "anthropic",
modelRoutes: passthroughRoutes(),
isPartner: true,
partnerPromotionKey: "fenno",
icon: "fenno",
},
{
name: "ZetaAPI",
websiteUrl: "https://zetaapi.ai",
apiKeyUrl: "https://zetaapi.ai/go/ccs",
category: "aggregator",
baseUrl: "https://api.zetaapi.ai",
mode: "direct",
apiFormat: "anthropic",
modelRoutes: passthroughRoutes(),
isPartner: true,
partnerPromotionKey: "zetaapi",
icon: "zetaapi",
},
{
name: "TeamoRouter",
websiteUrl: "https://teamorouter.com",
apiKeyUrl:
"https://teamorouter.com/?utm_source=cc_switch&utm_medium=referral&utm_campaign=ai_directory",
category: "aggregator",
baseUrl: "https://api.teamorouter.com",
mode: "direct",
apiFormat: "anthropic",
modelRoutes: passthroughRoutes(),
isPartner: true,
partnerPromotionKey: "teamorouter",
icon: "teamorouter",
},
{
name: "Amux",
websiteUrl: "https://amux.ai",
apiKeyUrl: "https://amux.ai",
category: "aggregator",
baseUrl: "https://api.amux.ai",
mode: "direct",
apiFormat: "anthropic",
modelRoutes: passthroughRoutes(),
icon: "amux",
},
{
name: "火山Agentplan",
websiteUrl:
"https://www.volcengine.com/activity/codingplan?ac=MMAP8JTTCAQ2&rc=6J6FV5N2&utm_campaign=hw&utm_content=ccswitch&utm_medium=devrel_tool_web&utm_source=OWO&utm_term=ccswitch",
websiteUrl: "https://www.volcengine.com/product/ark",
apiKeyUrl:
"https://www.volcengine.com/activity/codingplan?ac=MMAP8JTTCAQ2&rc=6J6FV5N2&utm_campaign=hw&utm_content=ccswitch&utm_medium=devrel_tool_web&utm_source=OWO&utm_term=ccswitch",
category: "cn_official",
@@ -206,8 +272,7 @@ export const claudeDesktopProviderPresets: ClaudeDesktopProviderPreset[] = [
},
{
name: "BytePlus",
websiteUrl:
"https://www.byteplus.com/en/product/modelark?utm_campaign=hw&utm_content=ccswitch&utm_medium=devrel_tool_web&utm_source=OWO&utm_term=ccswitch",
websiteUrl: "https://www.byteplus.com/en/product/modelark",
apiKeyUrl:
"https://www.byteplus.com/en/product/modelark?utm_campaign=hw&utm_content=ccswitch&utm_medium=devrel_tool_web&utm_source=OWO&utm_term=ccswitch",
category: "cn_official",
@@ -226,8 +291,7 @@ export const claudeDesktopProviderPresets: ClaudeDesktopProviderPreset[] = [
},
{
name: "DouBaoSeed",
websiteUrl:
"https://console.volcengine.com/ark/region:ark+cn-beijing/apiKey?apikey=%7B%7D&utm_campaign=hw&utm_content=ccswitch&utm_medium=devrel_tool_web&utm_source=OWO&utm_term=ccswitch",
websiteUrl: "https://www.volcengine.com/product/doubao",
apiKeyUrl:
"https://console.volcengine.com/ark/region:ark+cn-beijing/apiKey?apikey=%7B%7D&utm_campaign=hw&utm_content=ccswitch&utm_medium=devrel_tool_web&utm_source=OWO&utm_term=ccswitch",
category: "cn_official",
@@ -235,9 +299,9 @@ export const claudeDesktopProviderPresets: ClaudeDesktopProviderPreset[] = [
mode: "proxy",
apiFormat: "anthropic",
modelRoutes: brandedRoutes(
"doubao-seed-2-1-pro",
"doubao-seed-2-1-pro",
"doubao-seed-2-1-pro",
"doubao-seed-2-1-pro-260628",
"doubao-seed-2-1-pro-260628",
"doubao-seed-2-1-pro-260628",
),
isPartner: true,
partnerPromotionKey: "doubaoseed",
@@ -311,8 +375,8 @@ export const claudeDesktopProviderPresets: ClaudeDesktopProviderPreset[] = [
providerType: "github_copilot",
requiresOAuth: true,
modelRoutes: brandedRoutes(
"claude-sonnet-4.6",
"claude-sonnet-4.6",
"claude-sonnet-5",
"claude-sonnet-5",
"claude-haiku-4.5",
),
icon: "github",
@@ -431,7 +495,7 @@ export const claudeDesktopProviderPresets: ClaudeDesktopProviderPreset[] = [
{
name: "Kimi",
primePartner: true,
websiteUrl: "https://platform.moonshot.cn/console?aff=cc-switch",
websiteUrl: "https://platform.kimi.com?aff=cc-switch",
category: "cn_official",
baseUrl: "https://api.moonshot.cn/anthropic",
mode: "proxy",
@@ -447,7 +511,7 @@ export const claudeDesktopProviderPresets: ClaudeDesktopProviderPreset[] = [
{
name: "Kimi For Coding",
primePartner: true,
websiteUrl: "https://www.kimi.com/code/docs/?aff=cc-switch",
websiteUrl: "https://www.kimi.com/code/?aff=cc-switch",
category: "cn_official",
baseUrl: "https://api.kimi.com/coding/",
mode: "proxy",
@@ -587,7 +651,7 @@ export const claudeDesktopProviderPresets: ClaudeDesktopProviderPreset[] = [
mode: "direct",
apiFormat: "anthropic",
modelRoutes: mappedRoutes(
"anthropic/claude-sonnet-4.6",
"anthropic/claude-sonnet-5",
"anthropic/claude-opus-4.8",
"anthropic/claude-haiku-4.5",
),
@@ -597,7 +661,7 @@ export const claudeDesktopProviderPresets: ClaudeDesktopProviderPreset[] = [
{
name: "SiliconFlow",
websiteUrl: "https://siliconflow.cn",
apiKeyUrl: "https://cloud.siliconflow.cn/i/drGuwc9k",
apiKeyUrl: "https://cloud.siliconflow.cn/i/YflgU2Ve",
category: "aggregator",
baseUrl: "https://api.siliconflow.cn",
mode: "proxy",
@@ -615,7 +679,7 @@ export const claudeDesktopProviderPresets: ClaudeDesktopProviderPreset[] = [
{
name: "SiliconFlow en",
websiteUrl: "https://siliconflow.com",
apiKeyUrl: "https://cloud.siliconflow.cn/i/drGuwc9k",
apiKeyUrl: "https://cloud.siliconflow.cn/i/YflgU2Ve",
category: "aggregator",
baseUrl: "https://api.siliconflow.com",
mode: "proxy",
@@ -727,6 +791,32 @@ export const claudeDesktopProviderPresets: ClaudeDesktopProviderPreset[] = [
partnerPromotionKey: "claudeapi",
icon: "claudeapi",
},
{
name: "Code0",
websiteUrl: "https://code0.ai",
apiKeyUrl: "https://code0.ai?source=ccswitch",
category: "aggregator",
baseUrl: "https://code0.ai",
mode: "direct",
apiFormat: "anthropic",
modelRoutes: passthroughRoutes(),
isPartner: true,
partnerPromotionKey: "code0",
icon: "code0",
},
{
name: "NekoCode",
websiteUrl: "https://nekocode.ai",
apiKeyUrl: "https://nekocode.ai?aff=CCSWITCH",
category: "aggregator",
baseUrl: "https://nekocode.ai",
mode: "direct",
apiFormat: "anthropic",
modelRoutes: passthroughRoutes(),
isPartner: true,
partnerPromotionKey: "nekocode",
icon: "nekocode",
},
{
name: "ClaudeCN",
websiteUrl: "https://claudecn.top",
@@ -950,7 +1040,7 @@ export const claudeDesktopProviderPresets: ClaudeDesktopProviderPreset[] = [
mode: "proxy",
apiFormat: "anthropic",
modelRoutes: mappedRoutes(
"anthropic/claude-sonnet-4.6",
"anthropic/claude-sonnet-5",
"anthropic/claude-opus-4.8",
"anthropic/claude-haiku-4.5",
true,
@@ -967,7 +1057,7 @@ export const claudeDesktopProviderPresets: ClaudeDesktopProviderPreset[] = [
mode: "proxy",
apiFormat: "anthropic",
modelRoutes: mappedRoutes(
"anthropic/claude-sonnet-4.6",
"anthropic/claude-sonnet-5",
"anthropic/claude-opus-4.8",
"anthropic/claude-haiku-4.5",
true,
+132 -28
View File
@@ -99,9 +99,9 @@ export const providerPresets: ProviderPreset[] = [
env: {
ANTHROPIC_BASE_URL: "https://router.shengsuanyun.com/api",
ANTHROPIC_AUTH_TOKEN: "",
ANTHROPIC_MODEL: "anthropic/claude-sonnet-4.6",
ANTHROPIC_MODEL: "anthropic/claude-sonnet-5",
ANTHROPIC_DEFAULT_HAIKU_MODEL: "anthropic/claude-haiku-4.5",
ANTHROPIC_DEFAULT_SONNET_MODEL: "anthropic/claude-sonnet-4.6",
ANTHROPIC_DEFAULT_SONNET_MODEL: "anthropic/claude-sonnet-5",
ANTHROPIC_DEFAULT_OPUS_MODEL: "anthropic/claude-opus-4.8",
},
},
@@ -128,8 +128,7 @@ export const providerPresets: ProviderPreset[] = [
},
{
name: "火山Agentplan",
websiteUrl:
"https://www.volcengine.com/activity/codingplan?ac=MMAP8JTTCAQ2&rc=6J6FV5N2&utm_campaign=hw&utm_content=ccswitch&utm_medium=devrel_tool_web&utm_source=OWO&utm_term=ccswitch",
websiteUrl: "https://www.volcengine.com/product/ark",
apiKeyUrl:
"https://www.volcengine.com/activity/codingplan?ac=MMAP8JTTCAQ2&rc=6J6FV5N2&utm_campaign=hw&utm_content=ccswitch&utm_medium=devrel_tool_web&utm_source=OWO&utm_term=ccswitch",
settingsConfig: {
@@ -150,8 +149,7 @@ export const providerPresets: ProviderPreset[] = [
},
{
name: "BytePlus",
websiteUrl:
"https://www.byteplus.com/en/product/modelark?utm_campaign=hw&utm_content=ccswitch&utm_medium=devrel_tool_web&utm_source=OWO&utm_term=ccswitch",
websiteUrl: "https://www.byteplus.com/en/product/modelark",
apiKeyUrl:
"https://www.byteplus.com/en/product/modelark?utm_campaign=hw&utm_content=ccswitch&utm_medium=devrel_tool_web&utm_source=OWO&utm_term=ccswitch",
settingsConfig: {
@@ -173,8 +171,7 @@ export const providerPresets: ProviderPreset[] = [
},
{
name: "DouBaoSeed",
websiteUrl:
"https://console.volcengine.com/ark/region:ark+cn-beijing/apiKey?apikey=%7B%7D&utm_campaign=hw&utm_content=ccswitch&utm_medium=devrel_tool_web&utm_source=OWO&utm_term=ccswitch",
websiteUrl: "https://www.volcengine.com/product/doubao",
apiKeyUrl:
"https://console.volcengine.com/ark/region:ark+cn-beijing/apiKey?apikey=%7B%7D&utm_campaign=hw&utm_content=ccswitch&utm_medium=devrel_tool_web&utm_source=OWO&utm_term=ccswitch",
settingsConfig: {
@@ -182,10 +179,10 @@ export const providerPresets: ProviderPreset[] = [
ANTHROPIC_BASE_URL: "https://ark.cn-beijing.volces.com/api/compatible",
ANTHROPIC_AUTH_TOKEN: "",
API_TIMEOUT_MS: "3000000",
ANTHROPIC_MODEL: "doubao-seed-2-1-pro",
ANTHROPIC_DEFAULT_SONNET_MODEL: "doubao-seed-2-1-pro",
ANTHROPIC_DEFAULT_OPUS_MODEL: "doubao-seed-2-1-pro",
ANTHROPIC_DEFAULT_HAIKU_MODEL: "doubao-seed-2-1-pro",
ANTHROPIC_MODEL: "doubao-seed-2-1-pro-260628",
ANTHROPIC_DEFAULT_SONNET_MODEL: "doubao-seed-2-1-pro-260628",
ANTHROPIC_DEFAULT_OPUS_MODEL: "doubao-seed-2-1-pro-260628",
ANTHROPIC_DEFAULT_HAIKU_MODEL: "doubao-seed-2-1-pro-260628",
},
},
category: "cn_official",
@@ -239,6 +236,83 @@ export const providerPresets: ProviderPreset[] = [
partnerPromotionKey: "unity2",
icon: "unity2",
},
{
name: "Qiniu",
nameKey: "providerForm.presets.qiniu",
websiteUrl: "https://s.qiniu.com/nMvAvy",
apiKeyUrl: "https://s.qiniu.com/nMvAvy",
settingsConfig: {
env: {
ANTHROPIC_BASE_URL: "https://api.qnaigc.com",
ANTHROPIC_AUTH_TOKEN: "",
},
},
endpointCandidates: ["https://api.qnaigc.com", "https://api.modelink.ai"],
category: "aggregator",
isPartner: true,
partnerPromotionKey: "qiniu",
icon: "qiniu",
},
{
name: "FennoAI",
websiteUrl: "https://api.fenno.ai",
apiKeyUrl:
"https://api.fenno.ai/register?redirect=/purchase?tab=subscription%26group=16&aff=P9MR3D3PLCNL",
settingsConfig: {
env: {
ANTHROPIC_BASE_URL: "https://api.fenno.ai",
ANTHROPIC_AUTH_TOKEN: "",
},
},
category: "aggregator",
isPartner: true,
partnerPromotionKey: "fenno",
icon: "fenno",
},
{
name: "ZetaAPI",
websiteUrl: "https://zetaapi.ai",
apiKeyUrl: "https://zetaapi.ai/go/ccs",
settingsConfig: {
env: {
ANTHROPIC_BASE_URL: "https://api.zetaapi.ai",
ANTHROPIC_AUTH_TOKEN: "",
},
},
category: "aggregator",
isPartner: true,
partnerPromotionKey: "zetaapi",
icon: "zetaapi",
},
{
name: "TeamoRouter",
websiteUrl: "https://teamorouter.com",
apiKeyUrl:
"https://teamorouter.com/?utm_source=cc_switch&utm_medium=referral&utm_campaign=ai_directory",
settingsConfig: {
env: {
ANTHROPIC_BASE_URL: "https://api.teamorouter.com",
ANTHROPIC_AUTH_TOKEN: "",
},
},
category: "aggregator",
isPartner: true,
partnerPromotionKey: "teamorouter",
icon: "teamorouter",
},
{
name: "Amux",
websiteUrl: "https://amux.ai",
apiKeyUrl: "https://amux.ai",
settingsConfig: {
env: {
ANTHROPIC_BASE_URL: "https://api.amux.ai",
ANTHROPIC_AUTH_TOKEN: "",
},
},
category: "aggregator",
icon: "amux",
},
{
name: "Gemini Native",
websiteUrl: "https://ai.google.dev/gemini-api",
@@ -386,7 +460,7 @@ export const providerPresets: ProviderPreset[] = [
{
name: "Kimi",
primePartner: true,
websiteUrl: "https://platform.moonshot.cn/console?aff=cc-switch",
websiteUrl: "https://platform.kimi.com?aff=cc-switch",
settingsConfig: {
env: {
ANTHROPIC_BASE_URL: "https://api.moonshot.cn/anthropic",
@@ -404,7 +478,7 @@ export const providerPresets: ProviderPreset[] = [
{
name: "Kimi For Coding",
primePartner: true,
websiteUrl: "https://www.kimi.com/code/docs/?aff=cc-switch",
websiteUrl: "https://www.kimi.com/code/?aff=cc-switch",
settingsConfig: {
env: {
ANTHROPIC_BASE_URL: "https://api.kimi.com/coding/",
@@ -616,9 +690,9 @@ export const providerPresets: ProviderPreset[] = [
env: {
ANTHROPIC_BASE_URL: "https://open.cherryin.net",
ANTHROPIC_AUTH_TOKEN: "",
ANTHROPIC_MODEL: "anthropic/claude-sonnet-4.6",
ANTHROPIC_MODEL: "anthropic/claude-sonnet-5",
ANTHROPIC_DEFAULT_HAIKU_MODEL: "anthropic/claude-haiku-4.5",
ANTHROPIC_DEFAULT_SONNET_MODEL: "anthropic/claude-sonnet-4.6",
ANTHROPIC_DEFAULT_SONNET_MODEL: "anthropic/claude-sonnet-5",
ANTHROPIC_DEFAULT_OPUS_MODEL: "anthropic/claude-opus-4.8",
},
},
@@ -629,7 +703,7 @@ export const providerPresets: ProviderPreset[] = [
{
name: "SiliconFlow",
websiteUrl: "https://siliconflow.cn",
apiKeyUrl: "https://cloud.siliconflow.cn/i/drGuwc9k",
apiKeyUrl: "https://cloud.siliconflow.cn/i/YflgU2Ve",
settingsConfig: {
env: {
ANTHROPIC_BASE_URL: "https://api.siliconflow.cn",
@@ -649,7 +723,7 @@ export const providerPresets: ProviderPreset[] = [
{
name: "SiliconFlow en",
websiteUrl: "https://siliconflow.com",
apiKeyUrl: "https://cloud.siliconflow.cn/i/drGuwc9k",
apiKeyUrl: "https://cloud.siliconflow.cn/i/YflgU2Ve",
settingsConfig: {
env: {
ANTHROPIC_BASE_URL: "https://api.siliconflow.com",
@@ -787,6 +861,36 @@ export const providerPresets: ProviderPreset[] = [
partnerPromotionKey: "claudeapi",
icon: "claudeapi",
},
{
name: "Code0",
websiteUrl: "https://code0.ai",
apiKeyUrl: "https://code0.ai?source=ccswitch",
settingsConfig: {
env: {
ANTHROPIC_BASE_URL: "https://code0.ai",
ANTHROPIC_AUTH_TOKEN: "",
},
},
category: "aggregator",
isPartner: true,
partnerPromotionKey: "code0",
icon: "code0",
},
{
name: "NekoCode",
websiteUrl: "https://nekocode.ai",
apiKeyUrl: "https://nekocode.ai?aff=CCSWITCH",
settingsConfig: {
env: {
ANTHROPIC_BASE_URL: "https://nekocode.ai",
ANTHROPIC_AUTH_TOKEN: "",
},
},
category: "aggregator",
isPartner: true,
partnerPromotionKey: "nekocode",
icon: "nekocode",
},
{
name: "ClaudeCN",
websiteUrl: "https://claudecn.top",
@@ -1045,9 +1149,9 @@ export const providerPresets: ProviderPreset[] = [
env: {
ANTHROPIC_BASE_URL: "https://openrouter.ai/api",
ANTHROPIC_AUTH_TOKEN: "",
ANTHROPIC_MODEL: "anthropic/claude-sonnet-4.6",
ANTHROPIC_MODEL: "anthropic/claude-sonnet-5",
ANTHROPIC_DEFAULT_HAIKU_MODEL: "anthropic/claude-haiku-4.5",
ANTHROPIC_DEFAULT_SONNET_MODEL: "anthropic/claude-sonnet-4.6",
ANTHROPIC_DEFAULT_SONNET_MODEL: "anthropic/claude-sonnet-5",
ANTHROPIC_DEFAULT_OPUS_MODEL: "anthropic/claude-opus-4.8",
},
},
@@ -1064,9 +1168,9 @@ export const providerPresets: ProviderPreset[] = [
ANTHROPIC_BASE_URL: "https://api.therouter.ai",
ANTHROPIC_AUTH_TOKEN: "",
ANTHROPIC_API_KEY: "",
ANTHROPIC_MODEL: "anthropic/claude-sonnet-4.6",
ANTHROPIC_MODEL: "anthropic/claude-sonnet-5",
ANTHROPIC_DEFAULT_HAIKU_MODEL: "anthropic/claude-haiku-4.5",
ANTHROPIC_DEFAULT_SONNET_MODEL: "anthropic/claude-sonnet-4.6",
ANTHROPIC_DEFAULT_SONNET_MODEL: "anthropic/claude-sonnet-5",
ANTHROPIC_DEFAULT_OPUS_MODEL: "anthropic/claude-opus-4.8",
},
},
@@ -1098,10 +1202,10 @@ export const providerPresets: ProviderPreset[] = [
settingsConfig: {
env: {
ANTHROPIC_BASE_URL: "https://api.githubcopilot.com",
ANTHROPIC_MODEL: "claude-sonnet-4.6",
ANTHROPIC_MODEL: "claude-sonnet-5",
ANTHROPIC_DEFAULT_HAIKU_MODEL: "claude-haiku-4.5",
ANTHROPIC_DEFAULT_SONNET_MODEL: "claude-sonnet-4.6",
ANTHROPIC_DEFAULT_OPUS_MODEL: "claude-sonnet-4.6",
ANTHROPIC_DEFAULT_SONNET_MODEL: "claude-sonnet-5",
ANTHROPIC_DEFAULT_OPUS_MODEL: "claude-sonnet-5",
},
},
category: "third_party",
@@ -1161,7 +1265,7 @@ export const providerPresets: ProviderPreset[] = [
ANTHROPIC_AUTH_TOKEN: "",
ANTHROPIC_MODEL: "claude-opus-4-8",
ANTHROPIC_DEFAULT_HAIKU_MODEL: "claude-haiku-4-5-20251001",
ANTHROPIC_DEFAULT_SONNET_MODEL: "claude-sonnet-4-6",
ANTHROPIC_DEFAULT_SONNET_MODEL: "claude-sonnet-5",
ANTHROPIC_DEFAULT_OPUS_MODEL: "claude-opus-4-8",
},
includeCoAuthoredBy: false,
@@ -1218,7 +1322,7 @@ export const providerPresets: ProviderPreset[] = [
ANTHROPIC_MODEL: "global.anthropic.claude-opus-4-8",
ANTHROPIC_DEFAULT_HAIKU_MODEL:
"global.anthropic.claude-haiku-4-5-20251001-v1:0",
ANTHROPIC_DEFAULT_SONNET_MODEL: "global.anthropic.claude-sonnet-4-6",
ANTHROPIC_DEFAULT_SONNET_MODEL: "global.anthropic.claude-sonnet-5",
ANTHROPIC_DEFAULT_OPUS_MODEL: "global.anthropic.claude-opus-4-8",
CLAUDE_CODE_USE_BEDROCK: "1",
},
@@ -1256,7 +1360,7 @@ export const providerPresets: ProviderPreset[] = [
ANTHROPIC_MODEL: "global.anthropic.claude-opus-4-8",
ANTHROPIC_DEFAULT_HAIKU_MODEL:
"global.anthropic.claude-haiku-4-5-20251001-v1:0",
ANTHROPIC_DEFAULT_SONNET_MODEL: "global.anthropic.claude-sonnet-4-6",
ANTHROPIC_DEFAULT_SONNET_MODEL: "global.anthropic.claude-sonnet-5",
ANTHROPIC_DEFAULT_OPUS_MODEL: "global.anthropic.claude-opus-4-8",
CLAUDE_CODE_USE_BEDROCK: "1",
},
+231 -14
View File
@@ -71,7 +71,20 @@ requires_openai_auth = true`;
function modelCatalog(
models: Array<
string | { model: string; displayName?: string; contextWindow?: number }
| string
| {
model: string;
displayName?: string;
contextWindow?: number;
// Native Responses (direct) overrides for the generated
// model-catalogs.json; omit to inherit the native template defaults
// (supports_parallel_tool_calls=false, input_modalities=["text"]).
supportsParallelToolCalls?: boolean;
inputModalities?: string[];
// Vendor's OFFICIAL base_instructions; omit to inherit the neutral
// template default. Required by Codex, so the backend always emits one.
baseInstructions?: string;
}
>,
): CodexCatalogModel[] {
return models.map((entry) =>
@@ -81,6 +94,9 @@ function modelCatalog(
model: entry.model,
displayName: entry.displayName,
contextWindow: entry.contextWindow,
supportsParallelToolCalls: entry.supportsParallelToolCalls,
inputModalities: entry.inputModalities,
baseInstructions: entry.baseInstructions,
},
);
}
@@ -135,8 +151,7 @@ export const codexProviderPresets: CodexProviderPreset[] = [
},
{
name: "火山Agentplan",
websiteUrl:
"https://www.volcengine.com/activity/codingplan?ac=MMAP8JTTCAQ2&rc=6J6FV5N2&utm_campaign=hw&utm_content=ccswitch&utm_medium=devrel_tool_web&utm_source=OWO&utm_term=ccswitch",
websiteUrl: "https://www.volcengine.com/product/ark",
apiKeyUrl:
"https://www.volcengine.com/activity/codingplan?ac=MMAP8JTTCAQ2&rc=6J6FV5N2&utm_campaign=hw&utm_content=ccswitch&utm_medium=devrel_tool_web&utm_source=OWO&utm_term=ccswitch",
auth: generateThirdPartyAuth(""),
@@ -162,8 +177,7 @@ export const codexProviderPresets: CodexProviderPreset[] = [
},
{
name: "BytePlus",
websiteUrl:
"https://www.byteplus.com/en/product/modelark?utm_campaign=hw&utm_content=ccswitch&utm_medium=devrel_tool_web&utm_source=OWO&utm_term=ccswitch",
websiteUrl: "https://www.byteplus.com/en/product/modelark",
apiKeyUrl:
"https://www.byteplus.com/en/product/modelark?utm_campaign=hw&utm_content=ccswitch&utm_medium=devrel_tool_web&utm_source=OWO&utm_term=ccswitch",
auth: generateThirdPartyAuth(""),
@@ -191,19 +205,27 @@ export const codexProviderPresets: CodexProviderPreset[] = [
},
{
name: "DouBaoSeed",
websiteUrl:
"https://console.volcengine.com/ark/region:ark+cn-beijing/apiKey?apikey=%7B%7D&utm_campaign=hw&utm_content=ccswitch&utm_medium=devrel_tool_web&utm_source=OWO&utm_term=ccswitch",
websiteUrl: "https://www.volcengine.com/product/doubao",
apiKeyUrl:
"https://console.volcengine.com/ark/region:ark+cn-beijing/apiKey?apikey=%7B%7D&utm_campaign=hw&utm_content=ccswitch&utm_medium=devrel_tool_web&utm_source=OWO&utm_term=ccswitch",
auth: generateThirdPartyAuth(""),
config: generateThirdPartyConfig(
"doubaoseed",
"https://ark.cn-beijing.volces.com/api/v3",
"doubao-seed-2-1-pro",
"doubao-seed-2-1-pro-260628",
),
endpointCandidates: ["https://ark.cn-beijing.volces.com/api/v3"],
// 火山方舟主数据面 /api/v3 原生支持 Responses API/api/v3/responses),无需路由接管转换
apiFormat: "openai_responses",
// 无官方 catalog:合成 MiMo 式(shell_command 编辑、不发 freeform apply_patch),
// 让 Codex 直连显示模型并避免 custom 工具被网关拒绝
modelCatalog: modelCatalog([
{
model: "doubao-seed-2-1-pro-260628",
displayName: "Doubao Seed 2.1 Pro",
contextWindow: 262144,
},
]),
category: "cn_official",
isPartner: true,
partnerPromotionKey: "doubaoseed",
@@ -258,6 +280,118 @@ export const codexProviderPresets: CodexProviderPreset[] = [
partnerPromotionKey: "unity2",
icon: "unity2",
},
{
name: "Qiniu",
nameKey: "providerForm.presets.qiniu",
websiteUrl: "https://s.qiniu.com/nMvAvy",
apiKeyUrl: "https://s.qiniu.com/nMvAvy",
category: "aggregator",
auth: generateThirdPartyAuth(""),
config: generateThirdPartyConfig(
"qiniu",
"https://api.qnaigc.com/bypass/openai/v1",
"gpt-5.5",
),
endpointCandidates: [
"https://api.qnaigc.com/bypass/openai/v1",
"https://api.modelink.ai/bypass/openai/v1",
],
isPartner: true,
partnerPromotionKey: "qiniu",
icon: "qiniu",
},
{
name: "FennoAI",
websiteUrl: "https://api.fenno.ai",
apiKeyUrl:
"https://api.fenno.ai/register?redirect=/purchase?tab=subscription%26group=16&aff=P9MR3D3PLCNL",
category: "aggregator",
auth: generateThirdPartyAuth(""),
config: generateThirdPartyConfig(
"fenno",
"https://api.fenno.ai",
"gpt-5.5",
),
endpointCandidates: ["https://api.fenno.ai"],
isPartner: true,
partnerPromotionKey: "fenno",
icon: "fenno",
},
{
name: "ZetaAPI",
websiteUrl: "https://zetaapi.ai",
apiKeyUrl: "https://zetaapi.ai/go/ccs",
category: "aggregator",
auth: generateThirdPartyAuth(""),
config: generateThirdPartyConfig(
"zetaapi",
"https://api.zetaapi.ai/v1",
"gpt-5.5",
),
endpointCandidates: ["https://api.zetaapi.ai/v1"],
isPartner: true,
partnerPromotionKey: "zetaapi",
icon: "zetaapi",
},
{
name: "TeamoRouter",
websiteUrl: "https://teamorouter.com",
apiKeyUrl:
"https://teamorouter.com/?utm_source=cc_switch&utm_medium=referral&utm_campaign=ai_directory",
category: "aggregator",
auth: generateThirdPartyAuth(""),
config: generateThirdPartyConfig(
"teamorouter",
"https://api.teamorouter.com/v1",
"gpt-5.5",
),
endpointCandidates: ["https://api.teamorouter.com/v1"],
isPartner: true,
partnerPromotionKey: "teamorouter",
icon: "teamorouter",
},
{
name: "Amux",
websiteUrl: "https://amux.ai",
apiKeyUrl: "https://amux.ai",
category: "aggregator",
auth: generateThirdPartyAuth(""),
config: generateThirdPartyConfig(
"amux",
"https://api.amux.ai/v1",
"gpt-5.5",
),
endpointCandidates: ["https://api.amux.ai/v1"],
icon: "amux",
},
{
name: "Code0",
websiteUrl: "https://code0.ai",
apiKeyUrl: "https://code0.ai?source=ccswitch",
category: "aggregator",
auth: generateThirdPartyAuth(""),
config: generateThirdPartyConfig("code0", "https://code0.ai/v1", "gpt-5.5"),
endpointCandidates: ["https://code0.ai/v1"],
isPartner: true,
partnerPromotionKey: "code0",
icon: "code0",
},
{
name: "NekoCode",
websiteUrl: "https://nekocode.ai",
apiKeyUrl: "https://nekocode.ai?aff=CCSWITCH",
category: "aggregator",
auth: generateThirdPartyAuth(""),
config: generateThirdPartyConfig(
"nekocode",
"https://nekocode.ai/v1",
"gpt-5.5",
),
endpointCandidates: ["https://nekocode.ai/v1"],
isPartner: true,
partnerPromotionKey: "nekocode",
icon: "nekocode",
},
{
name: "Azure OpenAI",
websiteUrl:
@@ -411,6 +545,14 @@ requires_openai_auth = true`,
endpointCandidates: ["https://dashscope.aliyuncs.com/compatible-mode/v1"],
// 阿里百炼 DashScope 原生支持 OpenAI Responses API/compatible-mode/v1/responses,同一 base_url),无需路由接管转换
apiFormat: "openai_responses",
// 无官方 catalog:合成 MiMo 式(shell_command 编辑、不发 freeform apply_patch
modelCatalog: modelCatalog([
{
model: "qwen3-coder-plus",
displayName: "Qwen3 Coder Plus",
contextWindow: 1048576,
},
]),
category: "cn_official",
icon: "bailian",
iconColor: "#624AFF",
@@ -418,8 +560,8 @@ requires_openai_auth = true`,
{
name: "Kimi",
primePartner: true,
websiteUrl: "https://platform.moonshot.cn/console?aff=cc-switch",
apiKeyUrl: "https://platform.moonshot.cn/console/api-keys?aff=cc-switch",
websiteUrl: "https://platform.kimi.com?aff=cc-switch",
apiKeyUrl: "https://platform.kimi.com/console/api-keys?aff=cc-switch",
auth: generateThirdPartyAuth(""),
config: generateThirdPartyConfig(
"kimi",
@@ -449,8 +591,8 @@ requires_openai_auth = true`,
{
name: "Kimi For Coding",
primePartner: true,
websiteUrl: "https://www.kimi.com/code/docs/",
apiKeyUrl: "https://www.kimi.com/code/",
websiteUrl: "https://www.kimi.com/code/?aff=cc-switch",
apiKeyUrl: "https://www.kimi.com/code/?aff=cc-switch",
auth: generateThirdPartyAuth(""),
config: generateThirdPartyConfig(
"kimi_coding",
@@ -586,6 +728,15 @@ requires_openai_auth = true`,
endpointCandidates: ["https://api.longcat.chat/openai/v1"],
// 美团 LongCat 官方 Codex 文档用 wire_api=responses 对自家 base_url,原生 Responses,无需路由接管转换
apiFormat: "openai_responses",
// 无官方 catalog:合成 MiMo 式(shell_command 编辑、不发 freeform apply_patch)。
// 注:LongCat 的 /responses 工具类型契约文档化程度最低,建议真机冒烟一次
modelCatalog: modelCatalog([
{
model: "LongCat-2.0-Preview",
displayName: "LongCat 2.0 Preview",
contextWindow: 1048576,
},
]),
category: "cn_official",
icon: "longcat",
iconColor: "#29E154",
@@ -603,6 +754,19 @@ requires_openai_auth = true`,
endpointCandidates: ["https://api.minimaxi.com/v1"],
// MiniMax 官方 API 参考已列 /v1/responses 为正式端点(CN/intl 双区,POST /v1/responses),原生 Responses,无需路由接管转换
apiFormat: "openai_responses",
// 官方 Codex catalogplatform.minimaxi.com/docs/token-plan/codex-cli):
// shell_command 编辑、并行工具、文本+图像,不声明 freeform apply_patch
modelCatalog: modelCatalog([
{
model: "MiniMax-M3",
displayName: "MiniMax-M3",
contextWindow: 1000000,
supportsParallelToolCalls: true,
inputModalities: ["text", "image"],
baseInstructions:
"You are Codex, a coding agent based on MiniMax-M3. You and the user share the same workspace and collaborate to achieve the user's goals.",
},
]),
category: "cn_official",
partnerPromotionKey: "minimax_cn",
theme: {
@@ -625,6 +789,19 @@ requires_openai_auth = true`,
endpointCandidates: ["https://api.minimax.io/v1"],
// MiniMax 官方 API 参考已列 /v1/responses 为正式端点(CN/intl 双区,POST /v1/responses),原生 Responses,无需路由接管转换
apiFormat: "openai_responses",
// 官方 Codex catalogplatform.minimax.io/docs/token-plan/codex):
// shell_command 编辑、并行工具、文本+图像,不声明 freeform apply_patch
modelCatalog: modelCatalog([
{
model: "MiniMax-M3",
displayName: "MiniMax-M3",
contextWindow: 1000000,
supportsParallelToolCalls: true,
inputModalities: ["text", "image"],
baseInstructions:
"You are Codex, a coding agent based on MiniMax-M3. You and the user share the same workspace and collaborate to achieve the user's goals.",
},
]),
category: "cn_official",
partnerPromotionKey: "minimax_en",
theme: {
@@ -668,6 +845,26 @@ requires_openai_auth = true`,
endpointCandidates: ["https://api.xiaomimimo.com/v1"],
// 小米 MiMo 官方 Codex 文档已声明原生支持 Responses APIwire_api=responses 对自家 base_url),无需路由接管转换
apiFormat: "openai_responses",
// 官方 Codex catalogmimo.mi.com/.../codex-configuration):
// shell_command 编辑、不声明 freeform apply_patch
modelCatalog: modelCatalog([
{
model: "mimo-v2.5-pro",
displayName: "MiMo V2.5 Pro",
contextWindow: 1048576,
inputModalities: ["text"],
baseInstructions:
"You are MiMo, an AI assistant developed by Xiaomi. Today's date: {date} {week}. Your knowledge cutoff date is December 2024.",
},
{
model: "mimo-v2.5",
displayName: "MiMo V2.5",
contextWindow: 1048576,
inputModalities: ["text", "image"],
baseInstructions:
"You are MiMo, an AI assistant developed by Xiaomi. Today's date: {date} {week}. Your knowledge cutoff date is December 2024.",
},
]),
category: "cn_official",
icon: "xiaomimimo",
iconColor: "#000000",
@@ -685,6 +882,26 @@ requires_openai_auth = true`,
endpointCandidates: ["https://token-plan-cn.xiaomimimo.com/v1"],
// 小米 MiMo 官方 Codex 文档已声明原生支持 Responses APIwire_api=responses 对自家 base_url),无需路由接管转换
apiFormat: "openai_responses",
// 官方 Codex catalogmimo.mi.com/.../codex-configuration):
// shell_command 编辑、不声明 freeform apply_patch
modelCatalog: modelCatalog([
{
model: "mimo-v2.5-pro",
displayName: "MiMo V2.5 Pro",
contextWindow: 1048576,
inputModalities: ["text"],
baseInstructions:
"You are MiMo, an AI assistant developed by Xiaomi. Today's date: {date} {week}. Your knowledge cutoff date is December 2024.",
},
{
model: "mimo-v2.5",
displayName: "MiMo V2.5",
contextWindow: 1048576,
inputModalities: ["text", "image"],
baseInstructions:
"You are MiMo, an AI assistant developed by Xiaomi. Today's date: {date} {week}. Your knowledge cutoff date is December 2024.",
},
]),
category: "cn_official",
icon: "xiaomimimo",
iconColor: "#000000",
@@ -692,7 +909,7 @@ requires_openai_auth = true`,
{
name: "SiliconFlow",
websiteUrl: "https://siliconflow.cn",
apiKeyUrl: "https://cloud.siliconflow.cn/i/drGuwc9k",
apiKeyUrl: "https://cloud.siliconflow.cn/i/YflgU2Ve",
auth: generateThirdPartyAuth(""),
config: generateThirdPartyConfig(
"siliconflow",
@@ -717,7 +934,7 @@ requires_openai_auth = true`,
{
name: "SiliconFlow en",
websiteUrl: "https://siliconflow.com",
apiKeyUrl: "https://cloud.siliconflow.cn/i/drGuwc9k",
apiKeyUrl: "https://cloud.siliconflow.cn/i/YflgU2Ve",
auth: generateThirdPartyAuth(""),
config: generateThirdPartyConfig(
"siliconflow_en",
+41
View File
@@ -107,6 +107,47 @@ export const geminiProviderPresets: GeminiProviderPreset[] = [
endpointCandidates: ["https://subrouter.ai/v1beta"],
icon: "subrouter",
},
{
name: "Qiniu",
nameKey: "providerForm.presets.qiniu",
websiteUrl: "https://s.qiniu.com/nMvAvy",
apiKeyUrl: "https://s.qiniu.com/nMvAvy",
settingsConfig: {
env: {
GOOGLE_GEMINI_BASE_URL: "https://api.qnaigc.com/bypass/vertex",
GEMINI_MODEL: "gemini-3.1-pro-preview",
},
},
baseURL: "https://api.qnaigc.com/bypass/vertex",
model: "gemini-3.1-pro-preview",
description: "Qiniu",
category: "aggregator",
isPartner: true,
partnerPromotionKey: "qiniu",
endpointCandidates: [
"https://api.qnaigc.com/bypass/vertex",
"https://api.modelink.ai/bypass/vertex",
],
icon: "qiniu",
},
{
name: "Code0",
websiteUrl: "https://code0.ai",
apiKeyUrl: "https://code0.ai?source=ccswitch",
settingsConfig: {
env: {
GOOGLE_GEMINI_BASE_URL: "https://code0.ai",
GEMINI_MODEL: "gemini-3.1-pro-preview",
},
},
baseURL: "https://code0.ai",
model: "gemini-3.1-pro-preview",
description: "Code0",
category: "aggregator",
isPartner: true,
partnerPromotionKey: "code0",
icon: "code0",
},
{
name: "PackyCode",
websiteUrl: "https://www.packyapi.com",
+163 -32
View File
@@ -149,10 +149,143 @@ export const hermesProviderPresets: HermesProviderPreset[] = [
model: { default: "openai/gpt-5.5", provider: "shengsuanyun" },
},
},
{
name: "Qiniu",
nameKey: "providerForm.presets.qiniu",
websiteUrl: "https://s.qiniu.com/nMvAvy",
apiKeyUrl: "https://s.qiniu.com/nMvAvy",
settingsConfig: {
name: "qiniu",
base_url: "https://api.qnaigc.com/v1",
api_key: "",
api_mode: "chat_completions",
models: [{ id: "gpt-5.5", name: "GPT-5.5" }],
},
category: "aggregator",
isPartner: true,
partnerPromotionKey: "qiniu",
icon: "qiniu",
suggestedDefaults: {
model: { default: "gpt-5.5", provider: "qiniu" },
},
},
{
name: "FennoAI",
websiteUrl: "https://api.fenno.ai",
apiKeyUrl:
"https://api.fenno.ai/register?redirect=/purchase?tab=subscription%26group=16&aff=P9MR3D3PLCNL",
settingsConfig: {
name: "fenno",
base_url: "https://api.fenno.ai/v1",
api_key: "",
api_mode: "chat_completions",
models: [{ id: "gpt-5.5", name: "GPT-5.5" }],
},
category: "aggregator",
isPartner: true,
partnerPromotionKey: "fenno",
icon: "fenno",
suggestedDefaults: {
model: { default: "gpt-5.5", provider: "fenno" },
},
},
{
name: "ZetaAPI",
websiteUrl: "https://zetaapi.ai",
apiKeyUrl: "https://zetaapi.ai/go/ccs",
settingsConfig: {
name: "zetaapi",
base_url: "https://api.zetaapi.ai/v1",
api_key: "",
api_mode: "chat_completions",
models: [{ id: "gpt-5.5", name: "GPT-5.5" }],
},
category: "aggregator",
isPartner: true,
partnerPromotionKey: "zetaapi",
icon: "zetaapi",
suggestedDefaults: {
model: { default: "gpt-5.5", provider: "zetaapi" },
},
},
{
name: "TeamoRouter",
websiteUrl: "https://teamorouter.com",
apiKeyUrl:
"https://teamorouter.com/?utm_source=cc_switch&utm_medium=referral&utm_campaign=ai_directory",
settingsConfig: {
name: "teamorouter",
base_url: "https://api.teamorouter.com/v1",
api_key: "",
api_mode: "chat_completions",
models: [{ id: "gpt-5.5", name: "GPT-5.5" }],
},
category: "aggregator",
isPartner: true,
partnerPromotionKey: "teamorouter",
icon: "teamorouter",
suggestedDefaults: {
model: { default: "gpt-5.5", provider: "teamorouter" },
},
},
{
name: "Amux",
websiteUrl: "https://amux.ai",
apiKeyUrl: "https://amux.ai",
settingsConfig: {
name: "amux",
base_url: "https://api.amux.ai/v1",
api_key: "",
api_mode: "chat_completions",
models: [{ id: "gpt-5.5", name: "GPT-5.5" }],
},
category: "aggregator",
icon: "amux",
suggestedDefaults: {
model: { default: "gpt-5.5", provider: "amux" },
},
},
{
name: "Code0",
websiteUrl: "https://code0.ai",
apiKeyUrl: "https://code0.ai?source=ccswitch",
settingsConfig: {
name: "code0",
base_url: "https://code0.ai/v1",
api_key: "",
api_mode: "chat_completions",
models: [{ id: "gpt-5.5", name: "GPT-5.5" }],
},
category: "aggregator",
isPartner: true,
partnerPromotionKey: "code0",
icon: "code0",
suggestedDefaults: {
model: { default: "gpt-5.5", provider: "code0" },
},
},
{
name: "NekoCode",
websiteUrl: "https://nekocode.ai",
apiKeyUrl: "https://nekocode.ai?aff=CCSWITCH",
settingsConfig: {
name: "nekocode",
base_url: "https://nekocode.ai/v1",
api_key: "",
api_mode: "chat_completions",
models: [{ id: "gpt-5.5", name: "GPT-5.5" }],
},
category: "aggregator",
isPartner: true,
partnerPromotionKey: "nekocode",
icon: "nekocode",
suggestedDefaults: {
model: { default: "gpt-5.5", provider: "nekocode" },
},
},
{
name: "火山Agentplan",
websiteUrl:
"https://www.volcengine.com/activity/codingplan?ac=MMAP8JTTCAQ2&rc=6J6FV5N2&utm_campaign=hw&utm_content=ccswitch&utm_medium=devrel_tool_web&utm_source=OWO&utm_term=ccswitch",
websiteUrl: "https://www.volcengine.com/product/ark",
apiKeyUrl:
"https://www.volcengine.com/activity/codingplan?ac=MMAP8JTTCAQ2&rc=6J6FV5N2&utm_campaign=hw&utm_content=ccswitch&utm_medium=devrel_tool_web&utm_source=OWO&utm_term=ccswitch",
settingsConfig: {
@@ -181,8 +314,7 @@ export const hermesProviderPresets: HermesProviderPreset[] = [
},
{
name: "BytePlus",
websiteUrl:
"https://www.byteplus.com/en/product/modelark?utm_campaign=hw&utm_content=ccswitch&utm_medium=devrel_tool_web&utm_source=OWO&utm_term=ccswitch",
websiteUrl: "https://www.byteplus.com/en/product/modelark",
apiKeyUrl:
"https://www.byteplus.com/en/product/modelark?utm_campaign=hw&utm_content=ccswitch&utm_medium=devrel_tool_web&utm_source=OWO&utm_term=ccswitch",
settingsConfig: {
@@ -211,8 +343,7 @@ export const hermesProviderPresets: HermesProviderPreset[] = [
},
{
name: "DouBaoSeed",
websiteUrl:
"https://console.volcengine.com/ark/region:ark+cn-beijing/apiKey?apikey=%7B%7D&utm_campaign=hw&utm_content=ccswitch&utm_medium=devrel_tool_web&utm_source=OWO&utm_term=ccswitch",
websiteUrl: "https://www.volcengine.com/product/doubao",
apiKeyUrl:
"https://console.volcengine.com/ark/region:ark+cn-beijing/apiKey?apikey=%7B%7D&utm_campaign=hw&utm_content=ccswitch&utm_medium=devrel_tool_web&utm_source=OWO&utm_term=ccswitch",
settingsConfig: {
@@ -222,7 +353,7 @@ export const hermesProviderPresets: HermesProviderPreset[] = [
api_mode: "anthropic_messages",
models: [
{
id: "doubao-seed-2-1-pro",
id: "doubao-seed-2-1-pro-260628",
name: "Doubao Seed 2.1 Pro",
},
],
@@ -234,7 +365,7 @@ export const hermesProviderPresets: HermesProviderPreset[] = [
iconColor: "#3370FF",
suggestedDefaults: {
model: {
default: "doubao-seed-2-1-pro",
default: "doubao-seed-2-1-pro-260628",
provider: "doubao_seed",
},
},
@@ -331,8 +462,8 @@ export const hermesProviderPresets: HermesProviderPreset[] = [
context_length: 1000000,
},
{
id: "anthropic/claude-sonnet-4-6",
name: "Claude Sonnet 4.6",
id: "anthropic/claude-sonnet-5",
name: "Claude Sonnet 5",
context_length: 1000000,
},
{
@@ -542,7 +673,7 @@ export const hermesProviderPresets: HermesProviderPreset[] = [
{
name: "Kimi",
primePartner: true,
websiteUrl: "https://platform.moonshot.cn/console?aff=cc-switch",
websiteUrl: "https://platform.kimi.com?aff=cc-switch",
settingsConfig: {
name: "kimi",
base_url: "https://api.moonshot.cn/v1",
@@ -560,7 +691,7 @@ export const hermesProviderPresets: HermesProviderPreset[] = [
{
name: "Kimi For Coding",
primePartner: true,
websiteUrl: "https://www.kimi.com/code/docs/?aff=cc-switch",
websiteUrl: "https://www.kimi.com/code/?aff=cc-switch",
settingsConfig: {
name: "kimi_coding",
base_url: "https://api.kimi.com/coding/",
@@ -741,7 +872,7 @@ export const hermesProviderPresets: HermesProviderPreset[] = [
api_mode: "anthropic_messages",
models: [
{ id: "anthropic/claude-opus-4.8", name: "Claude Opus 4.8" },
{ id: "anthropic/claude-sonnet-4.6", name: "Claude Sonnet 4.6" },
{ id: "anthropic/claude-sonnet-5", name: "Claude Sonnet 5" },
],
},
category: "aggregator",
@@ -753,7 +884,7 @@ export const hermesProviderPresets: HermesProviderPreset[] = [
{
name: "SiliconFlow",
websiteUrl: "https://siliconflow.cn",
apiKeyUrl: "https://cloud.siliconflow.cn/i/drGuwc9k",
apiKeyUrl: "https://cloud.siliconflow.cn/i/YflgU2Ve",
settingsConfig: {
name: "siliconflow",
base_url: "https://api.siliconflow.cn/v1",
@@ -781,7 +912,7 @@ export const hermesProviderPresets: HermesProviderPreset[] = [
{
name: "SiliconFlow en",
websiteUrl: "https://siliconflow.com",
apiKeyUrl: "https://cloud.siliconflow.cn/i/drGuwc9k",
apiKeyUrl: "https://cloud.siliconflow.cn/i/YflgU2Ve",
settingsConfig: {
name: "siliconflow_en",
base_url: "https://api.siliconflow.com/v1",
@@ -830,7 +961,7 @@ export const hermesProviderPresets: HermesProviderPreset[] = [
api_mode: "anthropic_messages",
models: [
{ id: "claude-opus-4-8", name: "Claude Opus 4.8" },
{ id: "claude-sonnet-4-6", name: "Claude Sonnet 4.6" },
{ id: "claude-sonnet-5", name: "Claude Sonnet 5" },
{ id: "claude-haiku-4-5-20251001", name: "Claude Haiku 4.5" },
],
},
@@ -858,8 +989,8 @@ export const hermesProviderPresets: HermesProviderPreset[] = [
context_length: 1000000,
},
{
id: "claude-sonnet-4-6",
name: "Claude Sonnet 4.6",
id: "claude-sonnet-5",
name: "Claude Sonnet 5",
context_length: 1000000,
},
{
@@ -958,7 +1089,7 @@ export const hermesProviderPresets: HermesProviderPreset[] = [
api_mode: "anthropic_messages",
models: [
{ id: "claude-opus-4-8", name: "Claude Opus 4.8" },
{ id: "claude-sonnet-4-6", name: "Claude Sonnet 4.6" },
{ id: "claude-sonnet-5", name: "Claude Sonnet 5" },
{ id: "claude-haiku-4-5-20251001", name: "Claude Haiku 4.5" },
],
},
@@ -982,7 +1113,7 @@ export const hermesProviderPresets: HermesProviderPreset[] = [
api_mode: "anthropic_messages",
models: [
{ id: "claude-opus-4-8", name: "Claude Opus 4.8" },
{ id: "claude-sonnet-4-6", name: "Claude Sonnet 4.6" },
{ id: "claude-sonnet-5", name: "Claude Sonnet 5" },
{ id: "claude-haiku-4-5", name: "Claude Haiku 4.5" },
],
},
@@ -998,7 +1129,7 @@ export const hermesProviderPresets: HermesProviderPreset[] = [
},
},
suggestedDefaults: {
model: { default: "claude-sonnet-4-6", provider: "claudecn" },
model: { default: "claude-sonnet-5", provider: "claudecn" },
},
},
{
@@ -1012,7 +1143,7 @@ export const hermesProviderPresets: HermesProviderPreset[] = [
api_mode: "anthropic_messages",
models: [
{ id: "claude-opus-4-8", name: "Claude Opus 4.8" },
{ id: "claude-sonnet-4-6", name: "Claude Sonnet 4.6" },
{ id: "claude-sonnet-5", name: "Claude Sonnet 5" },
{ id: "claude-haiku-4-5", name: "Claude Haiku 4.5" },
],
},
@@ -1028,7 +1159,7 @@ export const hermesProviderPresets: HermesProviderPreset[] = [
},
},
suggestedDefaults: {
model: { default: "claude-sonnet-4-6", provider: "runapi" },
model: { default: "claude-sonnet-5", provider: "runapi" },
},
},
{
@@ -1042,7 +1173,7 @@ export const hermesProviderPresets: HermesProviderPreset[] = [
api_mode: "anthropic_messages",
models: [
{ id: "claude-opus-4-8", name: "Claude Opus 4.8" },
{ id: "claude-sonnet-4-6", name: "Claude Sonnet 4.6" },
{ id: "claude-sonnet-5", name: "Claude Sonnet 5" },
{ id: "claude-haiku-4-5-20251001", name: "Claude Haiku 4.5" },
],
},
@@ -1066,7 +1197,7 @@ export const hermesProviderPresets: HermesProviderPreset[] = [
api_mode: "anthropic_messages",
models: [
{ id: "claude-opus-4-8", name: "Claude Opus 4.8" },
{ id: "claude-sonnet-4-6", name: "Claude Sonnet 4.6" },
{ id: "claude-sonnet-5", name: "Claude Sonnet 5" },
{ id: "claude-haiku-4-5-20251001", name: "Claude Haiku 4.5" },
],
},
@@ -1090,7 +1221,7 @@ export const hermesProviderPresets: HermesProviderPreset[] = [
api_mode: "anthropic_messages",
models: [
{ id: "claude-opus-4-8", name: "Claude Opus 4.8" },
{ id: "claude-sonnet-4-6", name: "Claude Sonnet 4.6" },
{ id: "claude-sonnet-5", name: "Claude Sonnet 5" },
{ id: "claude-haiku-4-5-20251001", name: "Claude Haiku 4.5" },
],
},
@@ -1114,7 +1245,7 @@ export const hermesProviderPresets: HermesProviderPreset[] = [
api_mode: "anthropic_messages",
models: [
{ id: "claude-opus-4-8", name: "Claude Opus 4.8" },
{ id: "claude-sonnet-4-6", name: "Claude Sonnet 4.6" },
{ id: "claude-sonnet-5", name: "Claude Sonnet 5" },
{ id: "claude-haiku-4-5-20251001", name: "Claude Haiku 4.5" },
],
},
@@ -1138,7 +1269,7 @@ export const hermesProviderPresets: HermesProviderPreset[] = [
api_mode: "anthropic_messages",
models: [
{ id: "claude-opus-4-8", name: "Claude Opus 4.8" },
{ id: "claude-sonnet-4-6", name: "Claude Sonnet 4.6" },
{ id: "claude-sonnet-5", name: "Claude Sonnet 5" },
{ id: "claude-haiku-4-5-20251001", name: "Claude Haiku 4.5" },
],
},
@@ -1206,7 +1337,7 @@ export const hermesProviderPresets: HermesProviderPreset[] = [
api_mode: "anthropic_messages",
models: [
{ id: "claude-opus-4-8", name: "Claude Opus 4.8" },
{ id: "claude-sonnet-4-6", name: "Claude Sonnet 4.6" },
{ id: "claude-sonnet-5", name: "Claude Sonnet 5" },
{ id: "claude-haiku-4-5-20251001", name: "Claude Haiku 4.5" },
],
},
@@ -1230,7 +1361,7 @@ export const hermesProviderPresets: HermesProviderPreset[] = [
api_mode: "anthropic_messages",
models: [
{ id: "claude-opus-4-8", name: "Claude Opus 4.8" },
{ id: "claude-sonnet-4-6", name: "Claude Sonnet 4.6" },
{ id: "claude-sonnet-5", name: "Claude Sonnet 5" },
{ id: "claude-haiku-4-5-20251001", name: "Claude Haiku 4.5" },
],
},
@@ -1254,7 +1385,7 @@ export const hermesProviderPresets: HermesProviderPreset[] = [
api_mode: "anthropic_messages",
models: [
{ id: "claude-opus-4-8", name: "Claude Opus 4.8" },
{ id: "claude-sonnet-4-6", name: "Claude Sonnet 4.6" },
{ id: "claude-sonnet-5", name: "Claude Sonnet 5" },
{ id: "claude-haiku-4-5-20251001", name: "Claude Haiku 4.5" },
],
},
@@ -1335,7 +1466,7 @@ export const hermesProviderPresets: HermesProviderPreset[] = [
api_mode: "anthropic_messages",
models: [
{ id: "claude-opus-4-8", name: "Claude Opus 4.8" },
{ id: "claude-sonnet-4-6", name: "Claude Sonnet 4.6" },
{ id: "claude-sonnet-5", name: "Claude Sonnet 5" },
{
id: "claude-haiku-4-5-20251001",
name: "Claude Haiku 4.5",
+336 -86
View File
@@ -116,8 +116,8 @@ export const openclawProviderPresets: OpenClawProviderPreset[] = [
cost: { input: 5, output: 25 },
},
{
id: "anthropic/claude-sonnet-4.6",
name: "Claude Sonnet 4.6",
id: "anthropic/claude-sonnet-5",
name: "Claude Sonnet 5",
contextWindow: 1000000,
cost: { input: 3, output: 15 },
},
@@ -137,18 +137,17 @@ export const openclawProviderPresets: OpenClawProviderPreset[] = [
suggestedDefaults: {
model: {
primary: "shengsuanyun/anthropic/claude-opus-4.8",
fallbacks: ["shengsuanyun/anthropic/claude-sonnet-4.6"],
fallbacks: ["shengsuanyun/anthropic/claude-sonnet-5"],
},
modelCatalog: {
"shengsuanyun/anthropic/claude-opus-4.8": { alias: "Opus" },
"shengsuanyun/anthropic/claude-sonnet-4.6": { alias: "Sonnet" },
"shengsuanyun/anthropic/claude-sonnet-5": { alias: "Sonnet" },
},
},
},
{
name: "火山Agentplan",
websiteUrl:
"https://www.volcengine.com/activity/codingplan?ac=MMAP8JTTCAQ2&rc=6J6FV5N2&utm_campaign=hw&utm_content=ccswitch&utm_medium=devrel_tool_web&utm_source=OWO&utm_term=ccswitch",
websiteUrl: "https://www.volcengine.com/product/ark",
apiKeyUrl:
"https://www.volcengine.com/activity/codingplan?ac=MMAP8JTTCAQ2&rc=6J6FV5N2&utm_campaign=hw&utm_content=ccswitch&utm_medium=devrel_tool_web&utm_source=OWO&utm_term=ccswitch",
settingsConfig: {
@@ -184,8 +183,7 @@ export const openclawProviderPresets: OpenClawProviderPreset[] = [
},
{
name: "BytePlus",
websiteUrl:
"https://www.byteplus.com/en/product/modelark?utm_campaign=hw&utm_content=ccswitch&utm_medium=devrel_tool_web&utm_source=OWO&utm_term=ccswitch",
websiteUrl: "https://www.byteplus.com/en/product/modelark",
apiKeyUrl:
"https://www.byteplus.com/en/product/modelark?utm_campaign=hw&utm_content=ccswitch&utm_medium=devrel_tool_web&utm_source=OWO&utm_term=ccswitch",
settingsConfig: {
@@ -221,8 +219,7 @@ export const openclawProviderPresets: OpenClawProviderPreset[] = [
},
{
name: "DouBaoSeed",
websiteUrl:
"https://console.volcengine.com/ark/region:ark+cn-beijing/apiKey?apikey=%7B%7D&utm_campaign=hw&utm_content=ccswitch&utm_medium=devrel_tool_web&utm_source=OWO&utm_term=ccswitch",
websiteUrl: "https://www.volcengine.com/product/doubao",
apiKeyUrl:
"https://console.volcengine.com/ark/region:ark+cn-beijing/apiKey?apikey=%7B%7D&utm_campaign=hw&utm_content=ccswitch&utm_medium=devrel_tool_web&utm_source=OWO&utm_term=ccswitch",
settingsConfig: {
@@ -231,9 +228,9 @@ export const openclawProviderPresets: OpenClawProviderPreset[] = [
api: "openai-completions",
models: [
{
id: "doubao-seed-2-1-pro",
id: "doubao-seed-2-1-pro-260628",
name: "DouBao Seed 2.1 Pro",
contextWindow: 128000,
contextWindow: 262144,
cost: { input: 0.84, output: 4.2 },
},
],
@@ -251,9 +248,9 @@ export const openclawProviderPresets: OpenClawProviderPreset[] = [
},
},
suggestedDefaults: {
model: { primary: "doubaoseed/doubao-seed-2-1-pro" },
model: { primary: "doubaoseed/doubao-seed-2-1-pro-260628" },
modelCatalog: {
"doubaoseed/doubao-seed-2-1-pro": { alias: "DouBao" },
"doubaoseed/doubao-seed-2-1-pro-260628": { alias: "DouBao" },
},
},
},
@@ -330,6 +327,259 @@ export const openclawProviderPresets: OpenClawProviderPreset[] = [
},
},
},
{
name: "Qiniu",
nameKey: "providerForm.presets.qiniu",
websiteUrl: "https://s.qiniu.com/nMvAvy",
apiKeyUrl: "https://s.qiniu.com/nMvAvy",
settingsConfig: {
baseUrl: "https://api.qnaigc.com/v1",
apiKey: "",
api: "openai-completions",
models: [
{
id: "gpt-5.5",
name: "GPT-5.5",
contextWindow: 400000,
},
],
},
category: "aggregator",
isPartner: true,
partnerPromotionKey: "qiniu",
icon: "qiniu",
templateValues: {
apiKey: {
label: "API Key",
placeholder: "",
editorValue: "",
},
},
suggestedDefaults: {
model: {
primary: "qiniu/gpt-5.5",
},
modelCatalog: {
"qiniu/gpt-5.5": { alias: "GPT-5.5" },
},
},
},
{
name: "FennoAI",
websiteUrl: "https://api.fenno.ai",
apiKeyUrl:
"https://api.fenno.ai/register?redirect=/purchase?tab=subscription%26group=16&aff=P9MR3D3PLCNL",
settingsConfig: {
baseUrl: "https://api.fenno.ai/v1",
apiKey: "",
api: "openai-completions",
models: [
{
id: "gpt-5.5",
name: "GPT-5.5",
contextWindow: 400000,
},
],
},
category: "aggregator",
isPartner: true,
partnerPromotionKey: "fenno",
icon: "fenno",
templateValues: {
apiKey: {
label: "API Key",
placeholder: "",
editorValue: "",
},
},
suggestedDefaults: {
model: {
primary: "fenno/gpt-5.5",
},
modelCatalog: {
"fenno/gpt-5.5": { alias: "GPT-5.5" },
},
},
},
{
name: "ZetaAPI",
websiteUrl: "https://zetaapi.ai",
apiKeyUrl: "https://zetaapi.ai/go/ccs",
settingsConfig: {
baseUrl: "https://api.zetaapi.ai/v1",
apiKey: "",
api: "openai-completions",
models: [
{
id: "gpt-5.5",
name: "GPT-5.5",
contextWindow: 400000,
},
],
},
category: "aggregator",
isPartner: true,
partnerPromotionKey: "zetaapi",
icon: "zetaapi",
templateValues: {
apiKey: {
label: "API Key",
placeholder: "",
editorValue: "",
},
},
suggestedDefaults: {
model: {
primary: "zetaapi/gpt-5.5",
},
modelCatalog: {
"zetaapi/gpt-5.5": { alias: "GPT-5.5" },
},
},
},
{
name: "TeamoRouter",
websiteUrl: "https://teamorouter.com",
apiKeyUrl:
"https://teamorouter.com/?utm_source=cc_switch&utm_medium=referral&utm_campaign=ai_directory",
settingsConfig: {
baseUrl: "https://api.teamorouter.com/v1",
apiKey: "",
api: "openai-completions",
models: [
{
id: "gpt-5.5",
name: "GPT-5.5",
contextWindow: 400000,
},
],
},
category: "aggregator",
isPartner: true,
partnerPromotionKey: "teamorouter",
icon: "teamorouter",
templateValues: {
apiKey: {
label: "API Key",
placeholder: "",
editorValue: "",
},
},
suggestedDefaults: {
model: {
primary: "teamorouter/gpt-5.5",
},
modelCatalog: {
"teamorouter/gpt-5.5": { alias: "GPT-5.5" },
},
},
},
{
name: "Amux",
websiteUrl: "https://amux.ai",
apiKeyUrl: "https://amux.ai",
settingsConfig: {
baseUrl: "https://api.amux.ai/v1",
apiKey: "",
api: "openai-completions",
models: [
{
id: "gpt-5.5",
name: "GPT-5.5",
contextWindow: 400000,
},
],
},
category: "aggregator",
icon: "amux",
templateValues: {
apiKey: {
label: "API Key",
placeholder: "",
editorValue: "",
},
},
suggestedDefaults: {
model: {
primary: "amux/gpt-5.5",
},
modelCatalog: {
"amux/gpt-5.5": { alias: "GPT-5.5" },
},
},
},
{
name: "Code0",
websiteUrl: "https://code0.ai",
apiKeyUrl: "https://code0.ai?source=ccswitch",
settingsConfig: {
baseUrl: "https://code0.ai/v1",
apiKey: "",
api: "openai-completions",
models: [
{
id: "gpt-5.5",
name: "GPT-5.5",
contextWindow: 400000,
},
],
},
category: "aggregator",
isPartner: true,
partnerPromotionKey: "code0",
icon: "code0",
templateValues: {
apiKey: {
label: "API Key",
placeholder: "",
editorValue: "",
},
},
suggestedDefaults: {
model: {
primary: "code0/gpt-5.5",
},
modelCatalog: {
"code0/gpt-5.5": { alias: "GPT-5.5" },
},
},
},
{
name: "NekoCode",
websiteUrl: "https://nekocode.ai",
apiKeyUrl: "https://nekocode.ai?aff=CCSWITCH",
settingsConfig: {
baseUrl: "https://nekocode.ai/v1",
apiKey: "",
api: "openai-completions",
models: [
{
id: "gpt-5.5",
name: "GPT-5.5",
contextWindow: 400000,
},
],
},
category: "aggregator",
isPartner: true,
partnerPromotionKey: "nekocode",
icon: "nekocode",
templateValues: {
apiKey: {
label: "API Key",
placeholder: "",
editorValue: "",
},
},
suggestedDefaults: {
model: {
primary: "nekocode/gpt-5.5",
},
modelCatalog: {
"nekocode/gpt-5.5": { alias: "GPT-5.5" },
},
},
},
{
name: "Unity2.ai",
websiteUrl: "https://unity2.ai",
@@ -529,8 +779,8 @@ export const openclawProviderPresets: OpenClawProviderPreset[] = [
{
name: "Kimi",
primePartner: true,
websiteUrl: "https://platform.moonshot.cn/console?aff=cc-switch",
apiKeyUrl: "https://platform.moonshot.cn/console/api-keys",
websiteUrl: "https://platform.kimi.com?aff=cc-switch",
apiKeyUrl: "https://platform.kimi.com/console/api-keys?aff=cc-switch",
settingsConfig: {
baseUrl: "https://api.moonshot.cn/v1",
apiKey: "",
@@ -568,8 +818,8 @@ export const openclawProviderPresets: OpenClawProviderPreset[] = [
{
name: "Kimi For Coding",
primePartner: true,
websiteUrl: "https://www.kimi.com/code/docs/?aff=cc-switch",
apiKeyUrl: "https://platform.moonshot.cn/console/api-keys",
websiteUrl: "https://www.kimi.com/code/?aff=cc-switch",
apiKeyUrl: "https://platform.kimi.com/console/api-keys?aff=cc-switch",
settingsConfig: {
baseUrl: "https://api.kimi.com/v1",
apiKey: "",
@@ -983,8 +1233,8 @@ export const openclawProviderPresets: OpenClawProviderPreset[] = [
cost: { input: 5, output: 25 },
},
{
id: "claude-sonnet-4-6",
name: "Claude Sonnet 4.6",
id: "claude-sonnet-5",
name: "Claude Sonnet 5",
contextWindow: 1000000,
cost: { input: 3, output: 15 },
},
@@ -1003,11 +1253,11 @@ export const openclawProviderPresets: OpenClawProviderPreset[] = [
suggestedDefaults: {
model: {
primary: "aihubmix/claude-opus-4-8",
fallbacks: ["aihubmix/claude-sonnet-4-6"],
fallbacks: ["aihubmix/claude-sonnet-5"],
},
modelCatalog: {
"aihubmix/claude-opus-4-8": { alias: "Opus" },
"aihubmix/claude-sonnet-4-6": { alias: "Sonnet" },
"aihubmix/claude-sonnet-5": { alias: "Sonnet" },
},
},
},
@@ -1026,8 +1276,8 @@ export const openclawProviderPresets: OpenClawProviderPreset[] = [
contextWindow: 1000000,
},
{
id: "anthropic/claude-sonnet-4.6",
name: "Claude Sonnet 4.6",
id: "anthropic/claude-sonnet-5",
name: "Claude Sonnet 5",
contextWindow: 1000000,
},
],
@@ -1044,11 +1294,11 @@ export const openclawProviderPresets: OpenClawProviderPreset[] = [
suggestedDefaults: {
model: {
primary: "cherryin/anthropic/claude-opus-4.8",
fallbacks: ["cherryin/anthropic/claude-sonnet-4.6"],
fallbacks: ["cherryin/anthropic/claude-sonnet-5"],
},
modelCatalog: {
"cherryin/anthropic/claude-opus-4.8": { alias: "Opus" },
"cherryin/anthropic/claude-sonnet-4.6": { alias: "Sonnet" },
"cherryin/anthropic/claude-sonnet-5": { alias: "Sonnet" },
},
},
},
@@ -1068,8 +1318,8 @@ export const openclawProviderPresets: OpenClawProviderPreset[] = [
cost: { input: 5, output: 25 },
},
{
id: "claude-sonnet-4-6",
name: "Claude Sonnet 4.6",
id: "claude-sonnet-5",
name: "Claude Sonnet 5",
contextWindow: 1000000,
cost: { input: 3, output: 15 },
},
@@ -1088,11 +1338,11 @@ export const openclawProviderPresets: OpenClawProviderPreset[] = [
suggestedDefaults: {
model: {
primary: "dmxapi/claude-opus-4-8",
fallbacks: ["dmxapi/claude-sonnet-4-6"],
fallbacks: ["dmxapi/claude-sonnet-5"],
},
modelCatalog: {
"dmxapi/claude-opus-4-8": { alias: "Opus" },
"dmxapi/claude-sonnet-4-6": { alias: "Sonnet" },
"dmxapi/claude-sonnet-5": { alias: "Sonnet" },
},
},
},
@@ -1111,8 +1361,8 @@ export const openclawProviderPresets: OpenClawProviderPreset[] = [
contextWindow: 1000000,
},
{
id: "claude-sonnet-4-6",
name: "Claude Sonnet 4.6",
id: "claude-sonnet-5",
name: "Claude Sonnet 5",
contextWindow: 1000000,
},
{
@@ -1135,11 +1385,11 @@ export const openclawProviderPresets: OpenClawProviderPreset[] = [
},
suggestedDefaults: {
model: {
primary: "claudecn/claude-sonnet-4-6",
primary: "claudecn/claude-sonnet-5",
},
modelCatalog: {
"claudecn/claude-opus-4-8": { alias: "Opus" },
"claudecn/claude-sonnet-4-6": { alias: "Sonnet" },
"claudecn/claude-sonnet-5": { alias: "Sonnet" },
"claudecn/claude-haiku-4-5": { alias: "Haiku" },
},
},
@@ -1159,8 +1409,8 @@ export const openclawProviderPresets: OpenClawProviderPreset[] = [
contextWindow: 1000000,
},
{
id: "claude-sonnet-4-6",
name: "Claude Sonnet 4.6",
id: "claude-sonnet-5",
name: "Claude Sonnet 5",
contextWindow: 1000000,
},
{
@@ -1183,11 +1433,11 @@ export const openclawProviderPresets: OpenClawProviderPreset[] = [
},
suggestedDefaults: {
model: {
primary: "runapi/claude-sonnet-4-6",
primary: "runapi/claude-sonnet-5",
},
modelCatalog: {
"runapi/claude-opus-4-8": { alias: "Opus" },
"runapi/claude-sonnet-4-6": { alias: "Sonnet" },
"runapi/claude-sonnet-5": { alias: "Sonnet" },
"runapi/claude-haiku-4-5": { alias: "Haiku" },
},
},
@@ -1208,8 +1458,8 @@ export const openclawProviderPresets: OpenClawProviderPreset[] = [
cost: { input: 5, output: 25 },
},
{
id: "anthropic/claude-sonnet-4.6",
name: "Claude Sonnet 4.6",
id: "anthropic/claude-sonnet-5",
name: "Claude Sonnet 5",
contextWindow: 1000000,
cost: { input: 3, output: 15 },
},
@@ -1228,11 +1478,11 @@ export const openclawProviderPresets: OpenClawProviderPreset[] = [
suggestedDefaults: {
model: {
primary: "openrouter/anthropic/claude-opus-4.8",
fallbacks: ["openrouter/anthropic/claude-sonnet-4.6"],
fallbacks: ["openrouter/anthropic/claude-sonnet-5"],
},
modelCatalog: {
"openrouter/anthropic/claude-opus-4.8": { alias: "Opus" },
"openrouter/anthropic/claude-sonnet-4.6": { alias: "Sonnet" },
"openrouter/anthropic/claude-sonnet-5": { alias: "Sonnet" },
},
},
},
@@ -1246,8 +1496,8 @@ export const openclawProviderPresets: OpenClawProviderPreset[] = [
api: "openai-completions",
models: [
{
id: "anthropic/claude-sonnet-4.6",
name: "Claude Sonnet 4.6",
id: "anthropic/claude-sonnet-5",
name: "Claude Sonnet 5",
contextWindow: 1000000,
cost: { input: 3, output: 15, cacheRead: 0.3, cacheWrite: 3.75 },
},
@@ -1287,14 +1537,14 @@ export const openclawProviderPresets: OpenClawProviderPreset[] = [
},
suggestedDefaults: {
model: {
primary: "therouter/anthropic/claude-sonnet-4.6",
primary: "therouter/anthropic/claude-sonnet-5",
fallbacks: [
"therouter/openai/gpt-5.2",
"therouter/google/gemini-3.5-flash",
],
},
modelCatalog: {
"therouter/anthropic/claude-sonnet-4.6": { alias: "Sonnet" },
"therouter/anthropic/claude-sonnet-5": { alias: "Sonnet" },
"therouter/openai/gpt-5.2": { alias: "GPT-5.2" },
"therouter/google/gemini-3.5-flash": { alias: "Gemini Flash" },
"therouter/openai/gpt-5.3-codex": { alias: "Codex" },
@@ -1343,7 +1593,7 @@ export const openclawProviderPresets: OpenClawProviderPreset[] = [
{
name: "SiliconFlow",
websiteUrl: "https://siliconflow.cn",
apiKeyUrl: "https://cloud.siliconflow.cn/i/drGuwc9k",
apiKeyUrl: "https://cloud.siliconflow.cn/i/YflgU2Ve",
settingsConfig: {
baseUrl: "https://api.siliconflow.cn/v1",
apiKey: "",
@@ -1379,7 +1629,7 @@ export const openclawProviderPresets: OpenClawProviderPreset[] = [
{
name: "SiliconFlow en",
websiteUrl: "https://siliconflow.com",
apiKeyUrl: "https://cloud.siliconflow.cn/i/drGuwc9k",
apiKeyUrl: "https://cloud.siliconflow.cn/i/YflgU2Ve",
settingsConfig: {
baseUrl: "https://api.siliconflow.com/v1",
apiKey: "",
@@ -1494,8 +1744,8 @@ export const openclawProviderPresets: OpenClawProviderPreset[] = [
cost: { input: 5, output: 25 },
},
{
id: "claude-sonnet-4-6",
name: "claude-sonnet-4-6",
id: "claude-sonnet-5",
name: "claude-sonnet-5",
contextWindow: 1000000,
cost: { input: 3, output: 15 },
},
@@ -1519,11 +1769,11 @@ export const openclawProviderPresets: OpenClawProviderPreset[] = [
suggestedDefaults: {
model: {
primary: "pipellm/claude-opus-4-8",
fallbacks: ["pipellm/claude-sonnet-4-6"],
fallbacks: ["pipellm/claude-sonnet-5"],
},
modelCatalog: {
"pipellm/claude-opus-4-8": { alias: "Opus" },
"pipellm/claude-sonnet-4-6": { alias: "Sonnet" },
"pipellm/claude-sonnet-5": { alias: "Sonnet" },
"pipellm/claude-haiku-4-5-20251001": { alias: "Haiku" },
},
},
@@ -1546,8 +1796,8 @@ export const openclawProviderPresets: OpenClawProviderPreset[] = [
cost: { input: 5, output: 25 },
},
{
id: "claude-sonnet-4-6",
name: "Claude Sonnet 4.6",
id: "claude-sonnet-5",
name: "Claude Sonnet 5",
contextWindow: 1000000,
cost: { input: 3, output: 15 },
},
@@ -1567,11 +1817,11 @@ export const openclawProviderPresets: OpenClawProviderPreset[] = [
suggestedDefaults: {
model: {
primary: "packycode/claude-opus-4-8",
fallbacks: ["packycode/claude-sonnet-4-6"],
fallbacks: ["packycode/claude-sonnet-5"],
},
modelCatalog: {
"packycode/claude-opus-4-8": { alias: "Opus" },
"packycode/claude-sonnet-4-6": { alias: "Sonnet" },
"packycode/claude-sonnet-5": { alias: "Sonnet" },
},
},
},
@@ -1590,8 +1840,8 @@ export const openclawProviderPresets: OpenClawProviderPreset[] = [
contextWindow: 1000000,
},
{
id: "claude-sonnet-4-6",
name: "Claude Sonnet 4.6",
id: "claude-sonnet-5",
name: "Claude Sonnet 5",
contextWindow: 1000000,
},
{
@@ -1615,11 +1865,11 @@ export const openclawProviderPresets: OpenClawProviderPreset[] = [
suggestedDefaults: {
model: {
primary: "apikeyfun/claude-opus-4-8",
fallbacks: ["apikeyfun/claude-sonnet-4-6"],
fallbacks: ["apikeyfun/claude-sonnet-5"],
},
modelCatalog: {
"apikeyfun/claude-opus-4-8": { alias: "Opus" },
"apikeyfun/claude-sonnet-4-6": { alias: "Sonnet" },
"apikeyfun/claude-sonnet-5": { alias: "Sonnet" },
"apikeyfun/claude-haiku-4-5": { alias: "Haiku" },
},
},
@@ -1734,8 +1984,8 @@ export const openclawProviderPresets: OpenClawProviderPreset[] = [
cost: { input: 5, output: 25 },
},
{
id: "claude-sonnet-4-6",
name: "Claude Sonnet 4.6",
id: "claude-sonnet-5",
name: "Claude Sonnet 5",
contextWindow: 1000000,
cost: { input: 3, output: 15 },
},
@@ -1756,11 +2006,11 @@ export const openclawProviderPresets: OpenClawProviderPreset[] = [
suggestedDefaults: {
model: {
primary: "cubence/claude-opus-4-8",
fallbacks: ["cubence/claude-sonnet-4-6"],
fallbacks: ["cubence/claude-sonnet-5"],
},
modelCatalog: {
"cubence/claude-opus-4-8": { alias: "Opus" },
"cubence/claude-sonnet-4-6": { alias: "Sonnet" },
"cubence/claude-sonnet-5": { alias: "Sonnet" },
},
},
},
@@ -1780,8 +2030,8 @@ export const openclawProviderPresets: OpenClawProviderPreset[] = [
cost: { input: 5, output: 25 },
},
{
id: "claude-sonnet-4-6",
name: "Claude Sonnet 4.6",
id: "claude-sonnet-5",
name: "Claude Sonnet 5",
contextWindow: 1000000,
cost: { input: 3, output: 15 },
},
@@ -1802,11 +2052,11 @@ export const openclawProviderPresets: OpenClawProviderPreset[] = [
suggestedDefaults: {
model: {
primary: "aigocode/claude-opus-4-8",
fallbacks: ["aigocode/claude-sonnet-4-6"],
fallbacks: ["aigocode/claude-sonnet-5"],
},
modelCatalog: {
"aigocode/claude-opus-4-8": { alias: "Opus" },
"aigocode/claude-sonnet-4-6": { alias: "Sonnet" },
"aigocode/claude-sonnet-5": { alias: "Sonnet" },
},
},
},
@@ -1826,8 +2076,8 @@ export const openclawProviderPresets: OpenClawProviderPreset[] = [
cost: { input: 5, output: 25 },
},
{
id: "claude-sonnet-4-6",
name: "Claude Sonnet 4.6",
id: "claude-sonnet-5",
name: "Claude Sonnet 5",
contextWindow: 1000000,
cost: { input: 3, output: 15 },
},
@@ -1848,11 +2098,11 @@ export const openclawProviderPresets: OpenClawProviderPreset[] = [
suggestedDefaults: {
model: {
primary: "rightcode/claude-opus-4-8",
fallbacks: ["rightcode/claude-sonnet-4-6"],
fallbacks: ["rightcode/claude-sonnet-5"],
},
modelCatalog: {
"rightcode/claude-opus-4-8": { alias: "Opus" },
"rightcode/claude-sonnet-4-6": { alias: "Sonnet" },
"rightcode/claude-sonnet-5": { alias: "Sonnet" },
},
},
},
@@ -1872,8 +2122,8 @@ export const openclawProviderPresets: OpenClawProviderPreset[] = [
cost: { input: 5, output: 25 },
},
{
id: "claude-sonnet-4-6",
name: "Claude Sonnet 4.6",
id: "claude-sonnet-5",
name: "Claude Sonnet 5",
contextWindow: 1000000,
cost: { input: 3, output: 15 },
},
@@ -1894,11 +2144,11 @@ export const openclawProviderPresets: OpenClawProviderPreset[] = [
suggestedDefaults: {
model: {
primary: "aicodemirror/claude-opus-4-8",
fallbacks: ["aicodemirror/claude-sonnet-4-6"],
fallbacks: ["aicodemirror/claude-sonnet-5"],
},
modelCatalog: {
"aicodemirror/claude-opus-4-8": { alias: "Opus" },
"aicodemirror/claude-sonnet-4-6": { alias: "Sonnet" },
"aicodemirror/claude-sonnet-5": { alias: "Sonnet" },
},
},
},
@@ -1918,8 +2168,8 @@ export const openclawProviderPresets: OpenClawProviderPreset[] = [
cost: { input: 5, output: 25 },
},
{
id: "claude-sonnet-4-6",
name: "Claude Sonnet 4.6",
id: "claude-sonnet-5",
name: "Claude Sonnet 5",
contextWindow: 1000000,
cost: { input: 3, output: 15 },
},
@@ -1940,11 +2190,11 @@ export const openclawProviderPresets: OpenClawProviderPreset[] = [
suggestedDefaults: {
model: {
primary: "crazyrouter/claude-opus-4-8",
fallbacks: ["crazyrouter/claude-sonnet-4-6"],
fallbacks: ["crazyrouter/claude-sonnet-5"],
},
modelCatalog: {
"crazyrouter/claude-opus-4-8": { alias: "Opus" },
"crazyrouter/claude-sonnet-4-6": { alias: "Sonnet" },
"crazyrouter/claude-sonnet-5": { alias: "Sonnet" },
},
},
},
@@ -1964,8 +2214,8 @@ export const openclawProviderPresets: OpenClawProviderPreset[] = [
cost: { input: 5, output: 25 },
},
{
id: "claude-sonnet-4-6",
name: "Claude Sonnet 4.6",
id: "claude-sonnet-5",
name: "Claude Sonnet 5",
contextWindow: 1000000,
cost: { input: 3, output: 15 },
},
@@ -1986,11 +2236,11 @@ export const openclawProviderPresets: OpenClawProviderPreset[] = [
suggestedDefaults: {
model: {
primary: "sssaicode/claude-opus-4-8",
fallbacks: ["sssaicode/claude-sonnet-4-6"],
fallbacks: ["sssaicode/claude-sonnet-5"],
},
modelCatalog: {
"sssaicode/claude-opus-4-8": { alias: "Opus" },
"sssaicode/claude-sonnet-4-6": { alias: "Sonnet" },
"sssaicode/claude-sonnet-5": { alias: "Sonnet" },
},
},
},
@@ -2229,8 +2479,8 @@ export const openclawProviderPresets: OpenClawProviderPreset[] = [
cost: { input: 5, output: 25, cacheRead: 0.5, cacheWrite: 6.25 },
},
{
id: "anthropic.claude-sonnet-4-6",
name: "Claude Sonnet 4.6",
id: "anthropic.claude-sonnet-5",
name: "Claude Sonnet 5",
contextWindow: 1000000,
cost: { input: 3, output: 15, cacheRead: 0.3, cacheWrite: 3.75 },
},
+227 -33
View File
@@ -156,8 +156,8 @@ export const OPENCODE_PRESET_MODEL_VARIANTS: Record<
modalities: { input: ["text", "image", "pdf"], output: ["text"] },
},
{
id: "global.anthropic.claude-sonnet-4-6",
name: "Claude Sonnet 4.6",
id: "global.anthropic.claude-sonnet-5",
name: "Claude Sonnet 5",
contextLimit: 1000000,
outputLimit: 64000,
modalities: { input: ["text", "image", "pdf"], output: ["text"] },
@@ -294,7 +294,7 @@ export const opencodeProviderPresets: OpenCodeProviderPreset[] = [
},
models: {
"anthropic/claude-opus-4.8": { name: "Claude Opus 4.8" },
"anthropic/claude-sonnet-4.6": { name: "Claude Sonnet 4.6" },
"anthropic/claude-sonnet-5": { name: "Claude Sonnet 5" },
},
},
category: "aggregator",
@@ -309,10 +309,206 @@ export const opencodeProviderPresets: OpenCodeProviderPreset[] = [
},
},
},
{
name: "Qiniu",
nameKey: "providerForm.presets.qiniu",
websiteUrl: "https://s.qiniu.com/nMvAvy",
apiKeyUrl: "https://s.qiniu.com/nMvAvy",
settingsConfig: {
npm: "@ai-sdk/openai-compatible",
name: "Qiniu",
options: {
baseURL: "https://api.qnaigc.com/v1",
apiKey: "",
setCacheKey: true,
},
models: {
"gpt-5.5": { name: "GPT-5.5" },
},
},
category: "aggregator",
isPartner: true,
partnerPromotionKey: "qiniu",
icon: "qiniu",
templateValues: {
apiKey: {
label: "API Key",
placeholder: "",
editorValue: "",
},
},
},
{
name: "FennoAI",
websiteUrl: "https://api.fenno.ai",
apiKeyUrl:
"https://api.fenno.ai/register?redirect=/purchase?tab=subscription%26group=16&aff=P9MR3D3PLCNL",
settingsConfig: {
npm: "@ai-sdk/openai-compatible",
name: "FennoAI",
options: {
baseURL: "https://api.fenno.ai/v1",
apiKey: "",
setCacheKey: true,
},
models: {
"gpt-5.5": { name: "GPT-5.5" },
},
},
category: "aggregator",
isPartner: true,
partnerPromotionKey: "fenno",
icon: "fenno",
templateValues: {
apiKey: {
label: "API Key",
placeholder: "",
editorValue: "",
},
},
},
{
name: "ZetaAPI",
websiteUrl: "https://zetaapi.ai",
apiKeyUrl: "https://zetaapi.ai/go/ccs",
settingsConfig: {
npm: "@ai-sdk/openai-compatible",
name: "ZetaAPI",
options: {
baseURL: "https://api.zetaapi.ai/v1",
apiKey: "",
setCacheKey: true,
},
models: {
"gpt-5.5": { name: "GPT-5.5" },
},
},
category: "aggregator",
isPartner: true,
partnerPromotionKey: "zetaapi",
icon: "zetaapi",
templateValues: {
apiKey: {
label: "API Key",
placeholder: "",
editorValue: "",
},
},
},
{
name: "TeamoRouter",
websiteUrl: "https://teamorouter.com",
apiKeyUrl:
"https://teamorouter.com/?utm_source=cc_switch&utm_medium=referral&utm_campaign=ai_directory",
settingsConfig: {
npm: "@ai-sdk/openai-compatible",
name: "TeamoRouter",
options: {
baseURL: "https://api.teamorouter.com/v1",
apiKey: "",
setCacheKey: true,
},
models: {
"gpt-5.5": { name: "GPT-5.5" },
},
},
category: "aggregator",
isPartner: true,
partnerPromotionKey: "teamorouter",
icon: "teamorouter",
templateValues: {
apiKey: {
label: "API Key",
placeholder: "",
editorValue: "",
},
},
},
{
name: "Amux",
websiteUrl: "https://amux.ai",
apiKeyUrl: "https://amux.ai",
settingsConfig: {
npm: "@ai-sdk/openai-compatible",
name: "Amux",
options: {
baseURL: "https://api.amux.ai/v1",
apiKey: "",
setCacheKey: true,
},
models: {
"gpt-5.5": { name: "GPT-5.5" },
},
},
category: "aggregator",
icon: "amux",
templateValues: {
apiKey: {
label: "API Key",
placeholder: "",
editorValue: "",
},
},
},
{
name: "Code0",
websiteUrl: "https://code0.ai",
apiKeyUrl: "https://code0.ai?source=ccswitch",
settingsConfig: {
npm: "@ai-sdk/openai-compatible",
name: "Code0",
options: {
baseURL: "https://code0.ai/v1",
apiKey: "",
setCacheKey: true,
},
models: {
"gpt-5.5": { name: "GPT-5.5" },
},
},
category: "aggregator",
isPartner: true,
partnerPromotionKey: "code0",
icon: "code0",
templateValues: {
apiKey: {
label: "API Key",
placeholder: "",
editorValue: "",
},
},
},
{
name: "NekoCode",
websiteUrl: "https://nekocode.ai",
apiKeyUrl: "https://nekocode.ai?aff=CCSWITCH",
settingsConfig: {
npm: "@ai-sdk/openai-compatible",
name: "NekoCode",
options: {
baseURL: "https://nekocode.ai/v1",
apiKey: "",
setCacheKey: true,
},
models: {
"gpt-5.5": { name: "GPT-5.5" },
},
},
category: "aggregator",
isPartner: true,
partnerPromotionKey: "nekocode",
icon: "nekocode",
templateValues: {
apiKey: {
label: "API Key",
placeholder: "",
editorValue: "",
},
},
},
{
name: "火山Agentplan",
websiteUrl:
"https://www.volcengine.com/activity/codingplan?ac=MMAP8JTTCAQ2&rc=6J6FV5N2&utm_campaign=hw&utm_content=ccswitch&utm_medium=devrel_tool_web&utm_source=OWO&utm_term=ccswitch",
websiteUrl: "https://www.volcengine.com/product/ark",
apiKeyUrl:
"https://www.volcengine.com/activity/codingplan?ac=MMAP8JTTCAQ2&rc=6J6FV5N2&utm_campaign=hw&utm_content=ccswitch&utm_medium=devrel_tool_web&utm_source=OWO&utm_term=ccswitch",
settingsConfig: {
@@ -344,8 +540,7 @@ export const opencodeProviderPresets: OpenCodeProviderPreset[] = [
},
{
name: "BytePlus",
websiteUrl:
"https://www.byteplus.com/en/product/modelark?utm_campaign=hw&utm_content=ccswitch&utm_medium=devrel_tool_web&utm_source=OWO&utm_term=ccswitch",
websiteUrl: "https://www.byteplus.com/en/product/modelark",
apiKeyUrl:
"https://www.byteplus.com/en/product/modelark?utm_campaign=hw&utm_content=ccswitch&utm_medium=devrel_tool_web&utm_source=OWO&utm_term=ccswitch",
settingsConfig: {
@@ -377,8 +572,7 @@ export const opencodeProviderPresets: OpenCodeProviderPreset[] = [
},
{
name: "DouBaoSeed",
websiteUrl:
"https://console.volcengine.com/ark/region:ark+cn-beijing/apiKey?apikey=%7B%7D&utm_campaign=hw&utm_content=ccswitch&utm_medium=devrel_tool_web&utm_source=OWO&utm_term=ccswitch",
websiteUrl: "https://www.volcengine.com/product/doubao",
apiKeyUrl:
"https://console.volcengine.com/ark/region:ark+cn-beijing/apiKey?apikey=%7B%7D&utm_campaign=hw&utm_content=ccswitch&utm_medium=devrel_tool_web&utm_source=OWO&utm_term=ccswitch",
settingsConfig: {
@@ -390,7 +584,7 @@ export const opencodeProviderPresets: OpenCodeProviderPreset[] = [
setCacheKey: true,
},
models: {
"doubao-seed-2-1-pro": {
"doubao-seed-2-1-pro-260628": {
name: "Doubao Seed 2.1 Pro",
},
},
@@ -619,8 +813,8 @@ export const opencodeProviderPresets: OpenCodeProviderPreset[] = [
{
name: "Kimi",
primePartner: true,
websiteUrl: "https://platform.moonshot.cn/console?aff=cc-switch",
apiKeyUrl: "https://platform.moonshot.cn/console/api-keys?aff=cc-switch",
websiteUrl: "https://platform.kimi.com?aff=cc-switch",
apiKeyUrl: "https://platform.kimi.com/console/api-keys?aff=cc-switch",
settingsConfig: {
npm: "@ai-sdk/openai-compatible",
name: "Kimi",
@@ -653,8 +847,8 @@ export const opencodeProviderPresets: OpenCodeProviderPreset[] = [
{
name: "Kimi For Coding",
primePartner: true,
websiteUrl: "https://www.kimi.com/code/docs/?aff=cc-switch",
apiKeyUrl: "https://platform.moonshot.cn/console/api-keys?aff=cc-switch",
websiteUrl: "https://www.kimi.com/code/?aff=cc-switch",
apiKeyUrl: "https://platform.kimi.com/console/api-keys?aff=cc-switch",
settingsConfig: {
npm: "@ai-sdk/anthropic",
name: "Kimi For Coding",
@@ -1091,7 +1285,7 @@ export const opencodeProviderPresets: OpenCodeProviderPreset[] = [
setCacheKey: true,
},
models: {
"claude-sonnet-4-6": { name: "Claude Sonnet 4.6" },
"claude-sonnet-5": { name: "Claude Sonnet 5" },
"claude-opus-4-8": { name: "Claude Opus 4.8" },
},
},
@@ -1119,7 +1313,7 @@ export const opencodeProviderPresets: OpenCodeProviderPreset[] = [
setCacheKey: true,
},
models: {
"anthropic/claude-sonnet-4.6": { name: "Claude Sonnet 4.6" },
"anthropic/claude-sonnet-5": { name: "Claude Sonnet 5" },
"anthropic/claude-opus-4.8": { name: "Claude Opus 4.8" },
},
},
@@ -1146,7 +1340,7 @@ export const opencodeProviderPresets: OpenCodeProviderPreset[] = [
setCacheKey: true,
},
models: {
"claude-sonnet-4-6": { name: "Claude Sonnet 4.6" },
"claude-sonnet-5": { name: "Claude Sonnet 5" },
"claude-opus-4-8": { name: "Claude Opus 4.8" },
},
},
@@ -1174,7 +1368,7 @@ export const opencodeProviderPresets: OpenCodeProviderPreset[] = [
setCacheKey: true,
},
models: {
"anthropic/claude-sonnet-4.6": { name: "Claude Sonnet 4.6" },
"anthropic/claude-sonnet-5": { name: "Claude Sonnet 5" },
"anthropic/claude-opus-4.8": { name: "Claude Opus 4.8" },
},
},
@@ -1202,7 +1396,7 @@ export const opencodeProviderPresets: OpenCodeProviderPreset[] = [
setCacheKey: true,
},
models: {
"anthropic/claude-sonnet-4.6": { name: "Claude Sonnet 4.6" },
"anthropic/claude-sonnet-5": { name: "Claude Sonnet 5" },
"openai/gpt-5.3-codex": { name: "GPT-5.3 Codex" },
"openai/gpt-5.2": { name: "GPT-5.2" },
"google/gemini-3.5-flash": {
@@ -1288,7 +1482,7 @@ export const opencodeProviderPresets: OpenCodeProviderPreset[] = [
},
models: {
"claude-opus-4-8": { name: "claude-opus-4-8" },
"claude-sonnet-4-6": { name: "claude-sonnet-4-6" },
"claude-sonnet-5": { name: "claude-sonnet-5" },
"claude-haiku-4-5-20251001": { name: "claude-haiku-4-5-20251001" },
},
},
@@ -1316,7 +1510,7 @@ export const opencodeProviderPresets: OpenCodeProviderPreset[] = [
setCacheKey: true,
},
models: {
"claude-sonnet-4-6": { name: "Claude Sonnet 4.6" },
"claude-sonnet-5": { name: "Claude Sonnet 5" },
"claude-opus-4-8": { name: "Claude Opus 4.8" },
},
},
@@ -1346,7 +1540,7 @@ export const opencodeProviderPresets: OpenCodeProviderPreset[] = [
},
models: {
"claude-opus-4-8": { name: "Claude Opus 4.8" },
"claude-sonnet-4-6": { name: "Claude Sonnet 4.6" },
"claude-sonnet-5": { name: "Claude Sonnet 5" },
"claude-haiku-4-5": { name: "Claude Haiku 4.5" },
},
},
@@ -1457,7 +1651,7 @@ export const opencodeProviderPresets: OpenCodeProviderPreset[] = [
setCacheKey: true,
},
models: {
"claude-sonnet-4-6": { name: "Claude Sonnet 4.6" },
"claude-sonnet-5": { name: "Claude Sonnet 5" },
"claude-opus-4-8": { name: "Claude Opus 4.8" },
},
},
@@ -1487,7 +1681,7 @@ export const opencodeProviderPresets: OpenCodeProviderPreset[] = [
setCacheKey: true,
},
models: {
"claude-sonnet-4-6": { name: "Claude Sonnet 4.6" },
"claude-sonnet-5": { name: "Claude Sonnet 5" },
"claude-opus-4-8": { name: "Claude Opus 4.8" },
},
},
@@ -1546,7 +1740,7 @@ export const opencodeProviderPresets: OpenCodeProviderPreset[] = [
setCacheKey: true,
},
models: {
"claude-sonnet-4.6": { name: "Claude Sonnet 4.6" },
"claude-sonnet-5": { name: "Claude Sonnet 5" },
"claude-opus-4.8": { name: "Claude Opus 4.8" },
},
},
@@ -1576,7 +1770,7 @@ export const opencodeProviderPresets: OpenCodeProviderPreset[] = [
setCacheKey: true,
},
models: {
"claude-sonnet-4-6": { name: "Claude Sonnet 4.6" },
"claude-sonnet-5": { name: "Claude Sonnet 5" },
"claude-opus-4-8": { name: "Claude Opus 4.8" },
"claude-haiku-4-5": { name: "Claude Haiku 4.5" },
},
@@ -1606,7 +1800,7 @@ export const opencodeProviderPresets: OpenCodeProviderPreset[] = [
setCacheKey: true,
},
models: {
"claude-sonnet-4-6": { name: "Claude Sonnet 4.6" },
"claude-sonnet-5": { name: "Claude Sonnet 5" },
"claude-opus-4-8": { name: "Claude Opus 4.8" },
"claude-haiku-4-5": { name: "Claude Haiku 4.5" },
},
@@ -1636,7 +1830,7 @@ export const opencodeProviderPresets: OpenCodeProviderPreset[] = [
setCacheKey: true,
},
models: {
"claude-sonnet-4-6": { name: "Claude Sonnet 4.6" },
"claude-sonnet-5": { name: "Claude Sonnet 5" },
"claude-opus-4-8": { name: "Claude Opus 4.8" },
},
},
@@ -1666,7 +1860,7 @@ export const opencodeProviderPresets: OpenCodeProviderPreset[] = [
setCacheKey: true,
},
models: {
"claude-sonnet-4-6": { name: "Claude Sonnet 4.6" },
"claude-sonnet-5": { name: "Claude Sonnet 5" },
"claude-opus-4-8": { name: "Claude Opus 4.8" },
},
},
@@ -1697,7 +1891,7 @@ export const opencodeProviderPresets: OpenCodeProviderPreset[] = [
},
models: {
"claude-opus-4-8": { name: "Claude Opus 4.8" },
"claude-sonnet-4-6": { name: "Claude Sonnet 4.6" },
"claude-sonnet-5": { name: "Claude Sonnet 5" },
},
},
category: "third_party",
@@ -1727,7 +1921,7 @@ export const opencodeProviderPresets: OpenCodeProviderPreset[] = [
},
models: {
"claude-opus-4-8": { name: "Claude Opus 4.8" },
"claude-sonnet-4-6": { name: "Claude Sonnet 4.6" },
"claude-sonnet-5": { name: "Claude Sonnet 5" },
},
},
category: "third_party",
@@ -1787,8 +1981,8 @@ export const opencodeProviderPresets: OpenCodeProviderPreset[] = [
},
models: {
"global.anthropic.claude-opus-4-8": { name: "Claude Opus 4.8" },
"global.anthropic.claude-sonnet-4-6": {
name: "Claude Sonnet 4.6",
"global.anthropic.claude-sonnet-5": {
name: "Claude Sonnet 5",
},
"global.anthropic.claude-haiku-4-5-20251001-v1:0": {
name: "Claude Haiku 4.5",
+2 -2
View File
@@ -41,9 +41,9 @@ export interface UniversalProviderPreset {
*/
const NEWAPI_DEFAULT_MODELS: UniversalProviderModels = {
claude: {
model: "claude-sonnet-4-6",
model: "claude-sonnet-5",
haikuModel: "claude-haiku-4-5-20251001",
sonnetModel: "claude-sonnet-4-6",
sonnetModel: "claude-sonnet-5",
opusModel: "claude-opus-4-8",
},
codex: {
+2 -1
View File
@@ -17,6 +17,7 @@ import {
useDeleteProviderMutation,
useSwitchProviderMutation,
} from "@/lib/query";
import { usageKeys } from "@/lib/query/usage";
import { extractErrorMessage } from "@/utils/errorUtils";
import { openclawKeys } from "@/hooks/useOpenClaw";
import {
@@ -309,7 +310,7 @@ export function useProviderActions(
// 🔧 保存用量脚本后,也应该失效该 provider 的用量查询缓存
// 这样主页列表会使用新配置重新查询,而不是使用测试时的缓存
await queryClient.invalidateQueries({
queryKey: ["usage", provider.id, activeApp],
queryKey: usageKeys.script(provider.id, activeApp),
});
await queryClient.invalidateQueries({
queryKey: ["subscription", "quota", activeApp],
+29 -15
View File
@@ -60,7 +60,7 @@
"commonConfig": {
"guideTitle": "What is a Common Config Snippet?",
"guidePurpose": "It lets you share non-sensitive settings (plugins, environment variables, etc.) across different providers. These settings won't be lost when you switch providers.",
"guideUsage": "How to use: ① Click \"Extract from Editor\" to save the common parts ② Check \"Write Common Config\" when creating a new provider",
"guideUsage": "How to use: ① Click \"Extract from Editor\" to save the common parts ② Check \"Apply Common Config\" when creating a new provider",
"guideReExtract": "If you've newly installed plugins or hooks, re-extract the common config so they sync to other providers.",
"guideReassurance": "Don't worry — your original configuration is safely stored in the default provider and won't be lost.",
"emptyTitle": "No common config snippet yet",
@@ -68,10 +68,10 @@
},
"claudeConfig": {
"configLabel": "Claude Code settings.json (JSON) *",
"writeCommonConfig": "Write Common Config",
"writeCommonConfig": "Apply Common Config",
"editCommonConfig": "Edit Common Config",
"editCommonConfigTitle": "Edit Common Config Snippet",
"commonConfigHint": "This snippet will be merged into settings.json when 'Write Common Config' is checked",
"commonConfigHint": "This snippet will be merged into settings.json when 'Apply Common Config' is checked",
"fullSettingsHint": "Full Claude Code settings.json content",
"extractFromCurrent": "Extract from Editor",
"extractNoCommonConfig": "No common config available to extract from editor",
@@ -155,7 +155,7 @@
"notes": "Notes",
"notesPlaceholder": "e.g., Company dedicated account",
"configJson": "Config JSON",
"writeCommonConfig": "Write common config",
"writeCommonConfig": "Apply Common Config",
"editCommonConfigButton": "Edit common config",
"configJsonHint": "Please fill in complete Claude Code configuration",
"editCommonConfigTitle": "Edit common config snippet",
@@ -311,7 +311,7 @@
},
"commonConfig": {
"title": "About Common Config",
"message": "The \"Common Config Snippet\" lets you share plugins, environment variables, and other settings across different providers — so they won't be lost when you switch.\n\nHow to use:\n① Edit a provider → click \"Edit Common Config\" → \"Extract from Editor\"\n② When creating a new provider, check \"Write Common Config\" (enabled by default)\n\nIf you've newly installed plugins or hooks, re-extract the common config to keep them in sync.",
"message": "The \"Common Config Snippet\" lets you share plugins, environment variables, and other settings across different providers — so they won't be lost when you switch.\n\nHow to use:\n① Edit a provider → click \"Edit Common Config\" → \"Extract from Editor\"\n② When creating a new provider, check \"Apply Common Config\" (enabled by default)\n\nIf you've newly installed plugins or hooks, re-extract the common config to keep them in sync.",
"confirm": "Got it"
}
},
@@ -888,7 +888,17 @@
"searchPlaceholder": "Search by content, directory, or ID",
"searchSessions": "Search sessions",
"providerFilterAll": "All",
"providerFilterTooltip": "Provider filter",
"sessionList": "Sessions",
"viewModeTooltip": "View mode",
"viewModeFlat": "List",
"viewModeGrouped": "Grouped",
"unknownDirectory": "Unknown directory",
"toggleProviderGroup": "Expand or collapse {{provider}} provider group",
"toggleDirectoryGroup": "Expand or collapse {{directory}} directory group",
"collapseAllGroups": "Collapse all",
"selectProviderGroupForBatch": "Select sessions in {{provider}} provider group",
"selectDirectoryGroupForBatch": "Select sessions in {{directory}} directory group",
"manageBatchTooltip": "Enter batch management",
"exitBatchModeTooltip": "Exit batch management",
"batchModeHint": "Select sessions to delete",
@@ -996,6 +1006,8 @@
"officialHint": "💡 Official provider uses browser login, no API Key needed",
"getApiKey": "Get API Key",
"partnerPromotion": {
"code0": "code0.ai is an AI coding service platform for developers, supporting Claude Code, Codex, and Gemini. Exclusive for CC Switch users: contact support via the official website to claim free trial credits!",
"nekocode": "NekoCode gives developers a stable, efficient, and reliable API relay for Claude, Codex, and other AI models, with transparent pay-as-you-go pricing. Exclusive 10% off for CC Switch users: register via the link above and enter promo code cc-switch at recharge to save 10%!",
"packycode": "PackyCode is an official partner of CC Switch. Register using this link and enter \"cc-switch\" promo code during recharge to get 10% off",
"apikeyfun": "APIKEY.FUN offers a special deal for CC Switch users. Register through the exclusive link to enjoy up to permanent 5% off top-ups.",
"apinebula": "APINEBULA offers CC Switch users a special discount: register using the link and enter the \"ccswitch\" promo code during your first top-up to get 10% off.",
@@ -1024,12 +1036,17 @@
"byteplus": "Register via this link to get 500,000 tokens of free inference quota per model.",
"ccsub": "CCSub is a stable, affordable AI API relay — a drop-in replacement for Claude.ai & OpenAI subscriptions, with one key for all models at ~30% of direct API cost.",
"subrouter": "SubRouter is an AI relay aggregator that lets one key access multiple models and providers.",
"qiniu": "Qiniu Cloud is a multi-protocol AI relay: one key for native Claude, GPT, and Gemini models, with the Anthropic, OpenAI, and Gemini APIs all supported.",
"fenno": "Fenno.ai is a stable, efficient API relay giving you access to a wide range of mainstream models. Exclusive CC Switch offer: subscribe via this link to the ¥9.9 Coding Plan worth $150 in credits!",
"zetaapi": "ZetaAPI delivers real, undegraded models at as low as 35% of official pricing. Exclusive CC Switch offer: register via this link and use promo code CC-SWITCH for 10% off your first top-up!",
"teamorouter": "TeamoRouter is an enterprise-grade Agentic LLM gateway with pricing up to 90% off. It offers exclusive benefits for CC Switch users: register via this link as a new user for 10% off your first top-up!",
"unity2": "Unity2.ai offers exclusive benefits for CC Switch users: register via this link to get $2 in credits, plus another $10 for joining the official group — up to $12 in free credits!"
},
"presets": {
"ucloud": "Compshare",
"ucloudCoding": "Compshare Coding Plan",
"shengsuanyun": "Shengsuanyun",
"qiniu": "Qiniu",
"openrouter": "OpenRouter",
"deepseek": "DeepSeek",
"together": "Together AI"
@@ -1249,10 +1266,10 @@
"configTomlHint": "Codex config.toml configuration content",
"configTomlStorageHint": "Proxy takeover is showing the stored config, not the live config.toml.",
"proxyTakeoverStorageNotice": "💡 This app is under proxy takeover. You are editing the stored provider config; the live config is already temporarily managed by the proxy.",
"writeCommonConfig": "Write Common Config",
"writeCommonConfig": "Apply Common Config",
"editCommonConfig": "Edit Common Config",
"editCommonConfigTitle": "Edit Codex Common Config Snippet",
"commonConfigHint": "This snippet will be appended to the end of config.toml when 'Write Common Config' is checked",
"commonConfigHint": "This snippet will be appended to the end of config.toml when 'Apply Common Config' is checked",
"enableGoalMode": "Enable Goal mode",
"enableRemoteCompaction": "Enable remote compaction",
"remoteCompactionHint": "When enabled, the active model_providers entry name is written as OpenAI so Codex can try remote compaction.",
@@ -1263,12 +1280,9 @@
"saveFailed": "Save failed: {{error}}",
"modelNameHint": "Specify the model to use, will be auto-updated in config.toml",
"modelName": "Model Name",
"localRoutingToggle": "Needs Local Routing",
"localRoutingOffHint": "Keep off when the provider's model names need no rewriting and you don't need custom names shown in the /model menu; enable it when you need model mapping.",
"localRoutingOnHint": "When on, configure model mapping below: show custom model names in Codex's /model menu and map requests to the real upstream model.",
"upstreamFormatLabel": "Upstream Format",
"upstreamFormatHint": "Pick Responses when your provider is natively a Responses API (direct, no format conversion); pick Chat when it uses the Chat Completions protocol (converts to Chat Completions).",
"upstreamFormatChat": "Chat Completions (convert)",
"upstreamFormatHint": "Pick Responses when your provider is natively a Responses API (direct, no format conversion); pick Chat when it uses the Chat Completions protocol (requires routing takeover to convert to Chat Completions).",
"upstreamFormatChat": "Chat Completions (routing required)",
"upstreamFormatResponses": "Responses (native)",
"upstreamModelName": "Upstream Model Name",
"upstreamModelNameHint": "For Chat format, enter the real upstream model here; routing converts Codex Responses requests to Chat Completions while keeping this model.",
@@ -1297,17 +1311,17 @@
"reasoningEffortHint": "Enable when the upstream supports thinking-depth control such as low/high/max. Enabling this also turns on thinking mode and converts Codex's reasoning.effort into the upstream Chat parameter.",
"reasoningGroupTitle": "Reasoning Capability",
"reasoningSectionHint": "Preset providers are configured automatically; custom providers are inferred from name/URL. Override manually only when auto-detection is wrong.",
"advancedSectionHint": "Includes local routing, upstream format, model mapping, reasoning overrides and custom User-Agent. Enable local routing here when your provider uses the Chat Completions protocol or non-GPT models."
"advancedSectionHint": "Includes upstream format, model mapping, reasoning overrides and custom User-Agent. Providers using the Chat Completions protocol require routing takeover to be enabled."
},
"geminiConfig": {
"envFile": "Environment Variables (.env)",
"envFileHint": "Configure Gemini environment variables in .env format",
"configJson": "Configuration File (config.json)",
"configJsonHint": "Configure Gemini extended parameters in JSON format (optional)",
"writeCommonConfig": "Write Common Config",
"writeCommonConfig": "Apply Common Config",
"editCommonConfig": "Edit Common Config",
"editCommonConfigTitle": "Edit Gemini Common Config Snippet",
"commonConfigHint": "This snippet writes to Gemini .env (GOOGLE_GEMINI_BASE_URL and GEMINI_API_KEY are not allowed)",
"commonConfigHint": "This snippet writes to Gemini .env when 'Apply Common Config' is checked (GOOGLE_GEMINI_BASE_URL and GEMINI_API_KEY are not allowed)",
"extractFromCurrent": "Extract from Editor",
"extractNoCommonConfig": "No common config available to extract from editor",
"extractFailed": "Extract failed: {{error}}",
+29 -15
View File
@@ -60,7 +60,7 @@
"commonConfig": {
"guideTitle": "共通設定スニペットとは?",
"guidePurpose": "異なるプロバイダー間でプラグインや環境変数などの非機密設定を共有するための機能です。プロバイダーを切り替えてもこれらの設定は失われません。",
"guideUsage": "使い方:① 「編集内容から抽出」をクリックして共通部分を保存 ② 新規プロバイダー作成時に「共通設定を書き込む」をチェック",
"guideUsage": "使い方:① 「編集内容から抽出」をクリックして共通部分を保存 ② 新規プロバイダー作成時に「共通設定を適用」をチェック",
"guideReExtract": "新しいプラグインや Hook をインストールした場合は、共通設定を再抽出して他のプロバイダーに同期してください。",
"guideReassurance": "ご安心ください:元の設定はデフォルトプロバイダーに安全に保存されており、失われることはありません。",
"emptyTitle": "共通設定スニペットがまだありません",
@@ -68,10 +68,10 @@
},
"claudeConfig": {
"configLabel": "Claude Code settings.json (JSON) *",
"writeCommonConfig": "共通設定を書き込む",
"writeCommonConfig": "共通設定を適用",
"editCommonConfig": "共通設定を編集",
"editCommonConfigTitle": "共通設定スニペットを編集",
"commonConfigHint": "「共通設定を書き込む」がオンのとき settings.json にマージされます",
"commonConfigHint": "「共通設定を適用」がオンのとき settings.json にマージされます",
"fullSettingsHint": "Claude Code の settings.json 全文",
"extractFromCurrent": "編集内容から抽出",
"extractNoCommonConfig": "編集内容から抽出できる共通設定がありません",
@@ -155,7 +155,7 @@
"notes": "メモ",
"notesPlaceholder": "例: 会社用アカウント",
"configJson": "Config JSON",
"writeCommonConfig": "共通設定を書き込む",
"writeCommonConfig": "共通設定を適用",
"editCommonConfigButton": "共通設定を編集",
"configJsonHint": "Claude Code の設定をすべて入力してください",
"editCommonConfigTitle": "共通設定スニペットを編集",
@@ -311,7 +311,7 @@
},
"commonConfig": {
"title": "共通設定について",
"message": "「共通設定スニペット」を使うと、プラグインや環境変数などの設定を異なるプロバイダー間で共有でき、切り替え時に失われることがありません。\n\n使い方:\n① プロバイダーを編集 →「共通設定を編集」→「編集内容から抽出」\n② 新規プロバイダー作成時に「共通設定を書き込む」をチェック(デフォルトでオン)\n\n新しいプラグインや Hook をインストールした場合は、共通設定を再抽出してください。",
"message": "「共通設定スニペット」を使うと、プラグインや環境変数などの設定を異なるプロバイダー間で共有でき、切り替え時に失われることがありません。\n\n使い方:\n① プロバイダーを編集 →「共通設定を編集」→「編集内容から抽出」\n② 新規プロバイダー作成時に「共通設定を適用」をチェック(デフォルトでオン)\n\n新しいプラグインや Hook をインストールした場合は、共通設定を再抽出してください。",
"confirm": "わかりました"
}
},
@@ -888,7 +888,17 @@
"searchPlaceholder": "内容・ディレクトリ・ID で検索",
"searchSessions": "セッションを検索",
"providerFilterAll": "すべて",
"providerFilterTooltip": "プロバイダーフィルター",
"sessionList": "セッション一覧",
"viewModeTooltip": "表示方式",
"viewModeFlat": "一覧",
"viewModeGrouped": "分類",
"unknownDirectory": "不明なディレクトリ",
"toggleProviderGroup": "{{provider}} プロバイダーグループを開閉",
"toggleDirectoryGroup": "{{directory}} ディレクトリグループを開閉",
"collapseAllGroups": "すべて折りたたむ",
"selectProviderGroupForBatch": "{{provider}} プロバイダーグループ内のセッションを選択",
"selectDirectoryGroupForBatch": "{{directory}} ディレクトリグループ内のセッションを選択",
"manageBatchTooltip": "一括管理に入る",
"exitBatchModeTooltip": "一括管理を終了",
"batchModeHint": "削除するセッションを選択",
@@ -996,6 +1006,8 @@
"officialHint": "💡 公式プロバイダーはブラウザログインで、API Key は不要です",
"getApiKey": "API Key を取得",
"partnerPromotion": {
"code0": "code0.ai は開発者向けの AI コーディングサービスプラットフォームで、Claude Code、Codex、Gemini に対応。CC Switch ユーザー限定特典:公式サイトからサポートに連絡してテストクレジットを受け取れます!",
"nekocode": "NekoCode は Claude や Codex などの AI モデルに対応した、安定・高効率で信頼性の高い API 中継サービスを提供します。明瞭な従量課金制。CC Switch ユーザー限定 10%オフ:上のリンクから登録し、チャージ時にクーポンコード cc-switch を入力すると 10%オフ!",
"packycode": "PackyCode は CC Switch の公式パートナーです。登録後チャージ時に \"cc-switch\" を入力すると 10% オフ",
"apikeyfun": "APIKEY.FUN は CC Switch ユーザー向けに特別優待を提供しています。専用リンクから登録すると、最大でチャージ永久 5% オフを受けられます。",
"apinebula": "APINEBULA は CC Switch ユーザー向けに特別割引を提供しています。専用リンクから登録し、チャージ時にプロモコード「ccswitch」を入力すると、さらに 10% OFF の割引が適用されます。",
@@ -1024,12 +1036,17 @@
"byteplus": "このリンクから登録すると、各モデルごとに50万トークンの無料推論クォータがもらえます。",
"ccsub": "CCSub は安定・低価格の AI API リレーサービスです。Claude Code の公式サブスクリプションの代替として、1つのキーで全モデルを公式比約1/3のコストで利用できます。",
"subrouter": "SubRouter は、1つのキーで複数のモデルとプロバイダーにアクセスできる AI 中継アグリゲーターです。",
"qiniu": "Qiniu Cloud(七牛云)はマルチプロトコル対応の AI 中継プラットフォームです。1つのキーでネイティブの Claude・GPT・Gemini モデルを利用でき、AnthropicOpenAIGemini の各 API に対応します。",
"fenno": "Fenno.ai は安定・高効率な API 中継サービスで、幅広い主要モデルを利用できます。CC Switch 利用者限定特典:このリンクから 9.9 元(150 ドル相当)の Coding Plan を購入できます!",
"zetaapi": "ZetaAPI はモデルを水増し・劣化させず、公式価格の 35% から利用できます。CC Switch 利用者限定特典:このリンクから登録し、初回チャージ時にプロモコード CC-SWITCH を使うと初回チャージ 10% オフ!",
"teamorouter": "TeamoRouter はエンタープライズグレードの Agentic LLM ゲートウェイで、料金は最大 90% オフ。CC Switch ユーザーに特別特典を提供しています:このリンクから新規登録すると初回チャージが 10% オフ!",
"unity2": "Unity2.ai は CC Switch ユーザーに専属特典を提供しています:このリンクから登録すると $2 分のクレジット、公式グループ参加でさらに $10、最大 $12 の無料クレジットがもらえます!"
},
"presets": {
"ucloud": "Compshare",
"ucloudCoding": "Compshare Coding Plan",
"shengsuanyun": "Shengsuanyun",
"qiniu": "Qiniu",
"openrouter": "OpenRouter",
"deepseek": "DeepSeek",
"together": "Together AI"
@@ -1249,10 +1266,10 @@
"configTomlHint": "Codex の config.toml 設定内容",
"configTomlStorageHint": "プロキシ引き継ぎ中は保存済みの config を表示しており、実際の config.toml ではありません。",
"proxyTakeoverStorageNotice": "💡 このアプリはプロキシ引き継ぎ中です。ここで編集しているのは保存済みのプロバイダー設定で、実際の live 設定はすでにプロキシが一時的に引き継いでいます。",
"writeCommonConfig": "共通設定を書き込む",
"writeCommonConfig": "共通設定を適用",
"editCommonConfig": "共通設定を編集",
"editCommonConfigTitle": "Codex 共通設定スニペットを編集",
"commonConfigHint": "「共通設定を書き込む」がオンの場合、config.toml の末尾に追記されます",
"commonConfigHint": "「共通設定を適用」がオンの場合、config.toml の末尾に追記されます",
"enableGoalMode": "Goal mode を有効化",
"enableRemoteCompaction": "リモート圧縮を有効化",
"remoteCompactionHint": "有効にすると、現在の model_providers エントリの name を OpenAI に設定し、Codex がリモート圧縮を試行できるようにします。",
@@ -1263,12 +1280,9 @@
"saveFailed": "保存に失敗しました: {{error}}",
"modelNameHint": "使用するモデルを指定します。config.toml に自動更新されます",
"modelName": "モデル名",
"localRoutingToggle": "ローカルルーティングが必要",
"localRoutingOffHint": "プロバイダーのモデル名を書き換える必要がなく、/model メニューにカスタム名を表示する必要もない場合はオフのままで構いません。モデルマッピングが必要なときにオンにしてください。",
"localRoutingOnHint": "オンにすると下でモデルマッピングを設定できます。Codex の /model メニューにカスタムモデル名を表示し、リクエストを実際の上流モデルにマッピングします。",
"upstreamFormatLabel": "上流フォーマット",
"upstreamFormatHint": "プロバイダーがネイティブ Responses API なら Responses を選択(直結、フォーマット変換なし)。Chat Completions プロトコルなら Chat を選択(Chat Completions へ変換)。",
"upstreamFormatChat": "Chat Completions変換",
"upstreamFormatHint": "プロバイダーがネイティブ Responses API なら Responses を選択(直結、フォーマット変換なし)。Chat Completions プロトコルなら Chat を選択(Chat Completions へ変換するにはルーティング引き継ぎを有効化する必要があります)。",
"upstreamFormatChat": "Chat Completionsルーティング必須",
"upstreamFormatResponses": "Responses(ネイティブ)",
"upstreamModelName": "上流モデル名",
"upstreamModelNameHint": "Chat 形式ではここに実際の上流モデルを入力します。ルーティングで Codex の Responses リクエストを Chat Completions に変換し、このモデルを維持します。",
@@ -1297,17 +1311,17 @@
"reasoningEffortHint": "上流が low/high/max などの思考の深さの制御に対応している場合に有効化します。有効にすると思考モードも自動的にオンになり、Codex の reasoning.effort を上流の Chat パラメータに変換します。",
"reasoningGroupTitle": "思考能力",
"reasoningSectionHint": "プリセットの供給元は自動的に設定され、カスタム供給元は名前/URL から自動推論されます。自動識別が正しくない場合のみ手動で上書きしてください。",
"advancedSectionHint": "ローカルルーティング、上流フォーマット、モデルマッピング、思考能力、カスタム User-Agent の設定を含みます。供給元が Chat Completions プロトコルまたは GPT 以外のモデルを使用する場合は、ここでローカルルーティングを有効にしてください。"
"advancedSectionHint": "上流フォーマット、モデルマッピング、思考能力、カスタム User-Agent の設定を含みます。Chat Completions プロトコルを使用するプロバイダーはルーティング引き継ぎの有効化が必要です。"
},
"geminiConfig": {
"envFile": "環境変数 (.env)",
"envFileHint": ".env 形式で Gemini の環境変数を設定",
"configJson": "設定ファイル (config.json)",
"configJsonHint": "Gemini 拡張パラメーターを JSON 形式で設定(任意)",
"writeCommonConfig": "共通設定を書き込む",
"writeCommonConfig": "共通設定を適用",
"editCommonConfig": "共通設定を編集",
"editCommonConfigTitle": "Gemini 共通設定スニペットを編集",
"commonConfigHint": "このスニペットは Gemini の .env に書き込みます(GOOGLE_GEMINI_BASE_URL、GEMINI_API_KEY は使用できません)",
"commonConfigHint": "「共通設定を適用」がオンのとき Gemini の .env に書き込みます(GOOGLE_GEMINI_BASE_URL、GEMINI_API_KEY は使用できません)",
"extractFromCurrent": "編集内容から抽出",
"extractNoCommonConfig": "編集内容から抽出できる共通設定がありません",
"extractFailed": "抽出に失敗しました: {{error}}",
+29 -15
View File
@@ -60,7 +60,7 @@
"commonConfig": {
"guideTitle": "什麼是通用設定片段?",
"guidePurpose": "用來在不同供應商之間共用外掛程式、環境變數等非敏感設定。切換供應商時不會遺失這些設定。",
"guideUsage": "用法:① 點擊「從編輯內容擷取」儲存通用部分 ② 新增供應商時勾選「寫入通用設定」",
"guideUsage": "用法:① 點擊「從編輯內容擷取」儲存通用部分 ② 新增供應商時勾選「套用通用設定」",
"guideReExtract": "如果您新安裝了外掛程式或 Hook,請重新擷取一次通用設定,以便同步至其他供應商。",
"guideReassurance": "請放心:您的原始設定已儲存在預設供應商中,不會遺失。",
"emptyTitle": "尚未建立通用設定片段",
@@ -68,10 +68,10 @@
},
"claudeConfig": {
"configLabel": "Claude Code 設定 (JSON) *",
"writeCommonConfig": "寫入通用設定",
"writeCommonConfig": "套用通用設定",
"editCommonConfig": "編輯通用設定",
"editCommonConfigTitle": "編輯通用設定片段",
"commonConfigHint": "該片段會在勾選「寫入通用設定」時合併到 settings.json 中",
"commonConfigHint": "該片段會在勾選「套用通用設定」時合併到 settings.json 中",
"fullSettingsHint": "完整的 Claude Code settings.json 設定內容",
"extractFromCurrent": "從編輯內容擷取",
"extractNoCommonConfig": "目前編輯內容沒有可擷取的通用設定",
@@ -155,7 +155,7 @@
"notes": "備註",
"notesPlaceholder": "例如:公司專用帳號",
"configJson": "設定 JSON",
"writeCommonConfig": "寫入通用設定",
"writeCommonConfig": "套用通用設定",
"editCommonConfigButton": "編輯通用設定",
"configJsonHint": "請填寫完整的 Claude Code 設定",
"editCommonConfigTitle": "編輯通用設定片段",
@@ -311,7 +311,7 @@
},
"commonConfig": {
"title": "關於通用設定",
"message": "「通用設定片段」可以在不同供應商之間共用外掛程式、環境變數等設定,避免切換供應商時遺失這些設定。\n\n使用方法:\n① 編輯供應商時點擊「編輯通用設定」→「從編輯內容擷取」\n② 新增供應商時勾選「寫入通用設定」(預設已勾選)\n\n如果您新安裝了外掛程式或 Hook,請重新擷取一次通用設定。",
"message": "「通用設定片段」可以在不同供應商之間共用外掛程式、環境變數等設定,避免切換供應商時遺失這些設定。\n\n使用方法:\n① 編輯供應商時點擊「編輯通用設定」→「從編輯內容擷取」\n② 新增供應商時勾選「套用通用設定」(預設已勾選)\n\n如果您新安裝了外掛程式或 Hook,請重新擷取一次通用設定。",
"confirm": "我了解了"
}
},
@@ -859,7 +859,17 @@
"searchPlaceholder": "搜尋對話內容、目錄或 ID",
"searchSessions": "搜尋工作階段",
"providerFilterAll": "全部",
"providerFilterTooltip": "供應商篩選",
"sessionList": "工作階段清單",
"viewModeTooltip": "檢視方式",
"viewModeFlat": "清單",
"viewModeGrouped": "分類",
"unknownDirectory": "未知目錄",
"toggleProviderGroup": "展開或摺疊 {{provider}} 供應商分組",
"toggleDirectoryGroup": "展開或摺疊 {{directory}} 目錄分組",
"collapseAllGroups": "全部收起",
"selectProviderGroupForBatch": "選擇 {{provider}} 供應商分組內工作階段",
"selectDirectoryGroupForBatch": "選擇 {{directory}} 目錄分組內工作階段",
"manageBatchTooltip": "進入批次管理",
"exitBatchModeTooltip": "退出批次管理",
"batchModeHint": "勾選要刪除的工作階段",
@@ -967,6 +977,8 @@
"officialHint": "官方供應商使用瀏覽器登入,無需設定 API Key",
"getApiKey": "取得 API Key",
"partnerPromotion": {
"code0": "code0.ai 是面向開發者的 AI 程式設計服務平台,支援 Claude Code、Codex、Gemini。CC Switch 使用者專屬福利:透過官網聯繫客服即可領取測試額度!",
"nekocode": "NekoCode 為開發者提供穩定、高效、可靠的 Claude、Codex 等 AI 模型 API 中轉服務,價格透明、按量計費。CC Switch 使用者專享 9 折:透過上方連結註冊,儲值時輸入優惠碼 cc-switch 即享 9 折優惠!",
"packycode": "PackyCode 是 CC Switch 的官方合作夥伴,使用此連結註冊並在儲值時填寫「cc-switch」優惠碼,可以享受 9 折優惠",
"apikeyfun": "APIKEY.FUN 為 CC Switch 的使用者提供了特別優惠,透過專屬連結註冊,可享受最高儲值永久 95 折優惠。",
"apinebula": "APINEBULA 為 CC Switch 使用者提供特別優惠:使用專屬連結註冊並在儲值時填寫「ccswitch」優惠碼,可享 9 折優惠。",
@@ -996,12 +1008,17 @@
"byteplus": "透過此連結註冊即可取得每個模型 50 萬 tokens 的免費推理額度。",
"ccsub": "CCSub 是穩定、實惠的 AI API 中轉平台,Claude Code 官方訂閱的超強平替,一個 Key 覆蓋全部模型,價格約為官方 1/3。",
"subrouter": "SubRouter 是中轉站聚合平台,一個 Key 可接入多個模型與供應商。",
"qiniu": "七牛雲是多協定 AI 中轉平台,一個 Key 即可呼叫原生 Claude、GPT、Gemini 模型,相容 Anthropic、OpenAI、Gemini 多種介面。",
"fenno": "Fenno.ai 是穩定高效的 API 中轉服務商,可接入多種主流模型。CC Switch 使用者專屬福利:透過此連結訂閱 9.9 元 /150 刀額度的超值 Coding Plan",
"zetaapi": "ZetaAPI 主打模型不摻水、不降智,價格低至官方價 35 折。CC Switch 使用者專屬福利:透過此連結註冊,首次充值使用優惠碼 CC-SWITCH 即可享受九折優惠!",
"teamorouter": "TeamoRouter 是企業級 Agentic LLM 閘道,價格最高可達 90% 折扣。為 CC Switch 的使用者提供了特別福利:新使用者透過此連結註冊,首次充值享 10% 折扣!",
"unity2": "Unity2.ai 為 CC Switch 使用者提供專屬福利:透過此連結註冊可領取 $2 餘額,加入官方群再送 $10,最高可領 $12 免費額度!"
},
"presets": {
"ucloud": "優雲智算",
"ucloudCoding": "優雲智算 Coding Plan",
"shengsuanyun": "勝算雲",
"qiniu": "七牛雲",
"openrouter": "OpenRouter",
"deepseek": "DeepSeek",
"together": "Together AI"
@@ -1221,10 +1238,10 @@
"configTomlHint": "Codex config.toml 設定內容",
"configTomlStorageHint": "代理接管中顯示的是儲存的 config 配置,不是即時 config.toml",
"proxyTakeoverStorageNotice": "💡 目前應用處於代理接管狀態。這裡編輯的是儲存的供應商配置,真實 live 配置已由代理暫時接管",
"writeCommonConfig": "寫入通用設定",
"writeCommonConfig": "套用通用設定",
"editCommonConfig": "編輯通用設定",
"editCommonConfigTitle": "編輯 Codex 通用設定片段",
"commonConfigHint": "該片段會在勾選「寫入通用設定」時附加到 config.toml 末尾",
"commonConfigHint": "該片段會在勾選「套用通用設定」時附加到 config.toml 末尾",
"enableGoalMode": "啟用 Goal mode",
"enableRemoteCompaction": "啟用遠端壓縮",
"remoteCompactionHint": "開啟後會將目前 model_providers 條目的 name 寫為 OpenAI,讓 Codex 嘗試使用遠端壓縮。",
@@ -1240,12 +1257,9 @@
"autoCompactLimit": "壓縮閾值",
"autoCompactLimitHint": "上下文 token 數達到此閾值時自動壓縮歷史",
"autoCompactLimitPlaceholder": "例如: 90000",
"localRoutingToggle": "需要本地路由映射",
"localRoutingOnHint": "打開後可在下方設定模型映射:讓 Codex 的 /model 選單顯示自訂模型名,並把請求映射到真實上游模型。",
"localRoutingOffHint": "供應商模型名無需改寫、也無需在 /model 選單顯示自訂名稱時,可保持關閉;需要模型映射時打開。",
"upstreamFormatLabel": "上游格式",
"upstreamFormatHint": "供應商原生為 Responses API 就選 Responses(直連,不轉換格式);使用 Chat Completions 協定就選 Chat(轉換為 Chat Completions)。",
"upstreamFormatChat": "Chat Completions轉換",
"upstreamFormatHint": "供應商原生為 Responses API 就選 Responses(直連,不轉換格式);使用 Chat Completions 協定就選 Chat需開啟路由接管才能轉換為 Chat Completions)。",
"upstreamFormatChat": "Chat Completions需開啟路由",
"upstreamFormatResponses": "Responses(原生)",
"modelMappingTitle": "模型映射",
"modelMappingHint": "產生 Codex model_catalog_json,讓 /model 指令顯示這些第三方模型名;表中條目按填寫內容原樣儲存。修改後需要重新啟動 Codex 才能重新整理模型清單。",
@@ -1269,17 +1283,17 @@
"reasoningEffortHint": "上游支援 low/high/max 等思考深度控制時啟用。啟用後會自動啟用思考模式,並把 Codex 的 reasoning.effort 轉成上游 Chat 參數。",
"reasoningGroupTitle": "思考能力",
"reasoningSectionHint": "預設供應商已自動設定;自訂供應商會按名稱/位址自動推斷。僅當自動識別不準時才需手動覆寫。",
"advancedSectionHint": "包含本地路由映射、上游格式、模型映射、思考能力與自訂 User-Agent。供應商使用 Chat Completions 協定或非 GPT 模型時,需在此開啟本地路由映射。"
"advancedSectionHint": "包含上游格式、模型映射、思考能力與自訂 User-Agent。使用 Chat Completions 協定的供應商需開啟路由接管才能使用。"
},
"geminiConfig": {
"envFile": "環境變數 (.env)",
"envFileHint": "使用 .env 格式設定 Gemini 環境變數",
"configJson": "設定檔 (config.json)",
"configJsonHint": "使用 JSON 格式設定 Gemini 擴充參數(選填)",
"writeCommonConfig": "寫入通用設定",
"writeCommonConfig": "套用通用設定",
"editCommonConfig": "編輯通用設定",
"editCommonConfigTitle": "編輯 Gemini 通用設定片段",
"commonConfigHint": "該片段會寫入 Gemini 的 .env(不允許包含 GOOGLE_GEMINI_BASE_URL、GEMINI_API_KEY",
"commonConfigHint": "該片段會在勾選「套用通用設定」時寫入 Gemini 的 .env(不允許包含 GOOGLE_GEMINI_BASE_URL、GEMINI_API_KEY",
"extractFromCurrent": "從編輯內容擷取",
"extractNoCommonConfig": "目前編輯內容沒有可擷取的通用設定",
"extractFailed": "擷取失敗:{{error}}",
+29 -15
View File
@@ -60,7 +60,7 @@
"commonConfig": {
"guideTitle": "什么是通用配置片段?",
"guidePurpose": "用来在不同供应商之间共享插件、环境变量等非敏感配置。切换供应商时不会丢失这些设置。",
"guideUsage": "用法:① 点击「从编辑内容提取」保存通用部分 ② 新建供应商时勾选「写入通用配置」",
"guideUsage": "用法:① 点击「从编辑内容提取」保存通用部分 ② 新建供应商时勾选「应用通用配置」",
"guideReExtract": "如果您新安装了插件或 Hook,请重新提取一次通用配置,以便同步到其他供应商。",
"guideReassurance": "放心:您的原始配置已保存在默认供应商中,不会丢失。",
"emptyTitle": "还没有通用配置片段",
@@ -68,10 +68,10 @@
},
"claudeConfig": {
"configLabel": "Claude Code 配置 (JSON) *",
"writeCommonConfig": "写入通用配置",
"writeCommonConfig": "应用通用配置",
"editCommonConfig": "编辑通用配置",
"editCommonConfigTitle": "编辑通用配置片段",
"commonConfigHint": "该片段会在勾选\"写入通用配置\"时合并到 settings.json 中",
"commonConfigHint": "该片段会在勾选\"应用通用配置\"时合并到 settings.json 中",
"fullSettingsHint": "完整的 Claude Code settings.json 配置内容",
"extractFromCurrent": "从编辑内容提取",
"extractNoCommonConfig": "当前编辑内容没有可提取的通用配置",
@@ -155,7 +155,7 @@
"notes": "备注",
"notesPlaceholder": "例如:公司专用账号",
"configJson": "配置 JSON",
"writeCommonConfig": "写入通用配置",
"writeCommonConfig": "应用通用配置",
"editCommonConfigButton": "编辑通用配置",
"configJsonHint": "请填写完整的 Claude Code 配置",
"editCommonConfigTitle": "编辑通用配置片段",
@@ -311,7 +311,7 @@
},
"commonConfig": {
"title": "关于通用配置",
"message": "「通用配置片段」可以在不同供应商之间共享插件、环境变量等配置,避免切换供应商时丢失这些设置。\n\n使用方法:\n① 编辑供应商时点击「编辑通用配置」→「从编辑内容提取」\n② 新建供应商时勾选「写入通用配置」(默认已勾选)\n\n如果您新安装了插件或 Hook,请重新提取一次通用配置。",
"message": "「通用配置片段」可以在不同供应商之间共享插件、环境变量等配置,避免切换供应商时丢失这些设置。\n\n使用方法:\n① 编辑供应商时点击「编辑通用配置」→「从编辑内容提取」\n② 新建供应商时勾选「应用通用配置」(默认已勾选)\n\n如果您新安装了插件或 Hook,请重新提取一次通用配置。",
"confirm": "我知道了"
}
},
@@ -888,7 +888,17 @@
"searchPlaceholder": "搜索会话内容、目录或 ID",
"searchSessions": "搜索会话",
"providerFilterAll": "全部",
"providerFilterTooltip": "供应商筛选",
"sessionList": "会话列表",
"viewModeTooltip": "查看方式",
"viewModeFlat": "列表",
"viewModeGrouped": "分类",
"unknownDirectory": "未知目录",
"toggleProviderGroup": "展开或折叠 {{provider}} 供应商分组",
"toggleDirectoryGroup": "展开或折叠 {{directory}} 目录分组",
"collapseAllGroups": "全部收起",
"selectProviderGroupForBatch": "选择 {{provider}} 供应商分组内会话",
"selectDirectoryGroupForBatch": "选择 {{directory}} 目录分组内会话",
"manageBatchTooltip": "进入批量管理",
"exitBatchModeTooltip": "退出批量管理",
"batchModeHint": "勾选要删除的会话",
@@ -996,6 +1006,8 @@
"officialHint": "💡 官方供应商使用浏览器登录,无需配置 API Key",
"getApiKey": "获取 API Key",
"partnerPromotion": {
"code0": "code0.ai 是面向开发者的 AI 编程服务平台,支持 Claude Code、Codex、Gemini。CC Switch 用户专属福利:通过官网联系客服即可领取测试额度!",
"nekocode": "NekoCode 为开发者提供稳定、高效、可靠的 Claude、Codex 等 AI 模型 API 中转服务,价格透明、按量计费。CC Switch 用户专享 9 折:通过上方链接注册,充值时输入优惠码 cc-switch 即享 9 折优惠!",
"packycode": "PackyCode 是 CC Switch 的官方合作伙伴,使用此链接注册并在充值时填写 \"cc-switch\" 优惠码,可以享受9折优惠",
"apikeyfun": "APIKEY.FUN 为 CC Switch 的用户提供了特别优惠,通过专属链接注册,可享受最高充值永久 95 折优惠。",
"apinebula": "APINEBULA 为 CC Switch 用户提供特别优惠:使用专属链接注册并在充值时填写 \"ccswitch\" 优惠码,可享九折优惠。",
@@ -1024,12 +1036,17 @@
"byteplus": "通过此链接注册即可获取每个模型50万tokens的免费推理额度。",
"ccsub": "CCSub 是稳定、实惠的 AI API 中转平台,Claude Code 官方订阅的超强平替,一个 Key 覆盖全部模型,价格约为官方 1/3。",
"subrouter": "SubRouter 是中转站聚合平台,一个 Key 可接入多个模型与供应商。",
"qiniu": "七牛云是多协议 AI 中转平台,一个 Key 即可调用原生 Claude、GPT、Gemini 模型,兼容 Anthropic、OpenAI、Gemini 多种接口。",
"fenno": "Fenno.ai 是稳定高效的 API 中转服务商,可接入多种主流模型。CC Switch 用户专属福利:通过此链接订阅 9.9 元 /150 刀额度的超值 Coding Plan",
"zetaapi": "ZetaAPI 主打模型不掺水、不降智,价格低至官方价 35 折。CC Switch 用户专属福利:通过此链接注册,首次充值使用优惠码 CC-SWITCH 即可享受九折优惠!",
"teamorouter": "TeamoRouter 是企业级 Agentic LLM 网关,价格最高可达 90% 折扣。为 CC Switch 的用户提供了特别福利:新用户通过此链接注册,首次充值享 10% 折扣!",
"unity2": "Unity2.ai 为 CC Switch 用户提供专属福利:通过此链接注册可领取 $2 余额,加入官方群再送 $10,最高可领 $12 免费额度!"
},
"presets": {
"ucloud": "优云智算",
"ucloudCoding": "优云智算Coding Plan",
"shengsuanyun": "胜算云",
"qiniu": "七牛云",
"openrouter": "OpenRouter",
"deepseek": "DeepSeek",
"together": "Together AI"
@@ -1249,10 +1266,10 @@
"configTomlHint": "Codex config.toml 配置内容",
"configTomlStorageHint": "代理接管中显示的是存储的 config 配置,不是实时 config.toml",
"proxyTakeoverStorageNotice": "💡 当前应用处于代理接管状态。这里编辑的是存储的供应商配置,真实 live 配置已由代理临时接管",
"writeCommonConfig": "写入通用配置",
"writeCommonConfig": "应用通用配置",
"editCommonConfig": "编辑通用配置",
"editCommonConfigTitle": "编辑 Codex 通用配置片段",
"commonConfigHint": "该片段会在勾选'写入通用配置'时追加到 config.toml 末尾",
"commonConfigHint": "该片段会在勾选'应用通用配置'时追加到 config.toml 末尾",
"enableGoalMode": "启用 Goal mode",
"enableRemoteCompaction": "启用远程压缩",
"remoteCompactionHint": "开启后会将当前 model_providers 条目的 name 写为 OpenAI,使 Codex 尝试使用远程压缩。",
@@ -1263,12 +1280,9 @@
"saveFailed": "保存失败: {{error}}",
"modelNameHint": "指定使用的模型,将自动更新到 config.toml 中",
"modelName": "模型名称",
"localRoutingToggle": "需要本地路由映射",
"localRoutingOffHint": "供应商模型名无需改写、也无需在 /model 菜单展示自定义名称时,可保持关闭;需要模型映射时打开。",
"localRoutingOnHint": "打开后可在下方配置模型映射:让 Codex 的 /model 菜单显示自定义模型名,并把请求映射到真实上游模型。",
"upstreamFormatLabel": "上游格式",
"upstreamFormatHint": "供应商原生为 Responses API 就选 Responses(直连,不转换格式);使用 Chat Completions 协议就选 Chat(转换为 Chat Completions)。",
"upstreamFormatChat": "Chat Completions转换",
"upstreamFormatHint": "供应商原生为 Responses API 就选 Responses(直连,不转换格式);使用 Chat Completions 协议就选 Chat需开启路由接管才能转换为 Chat Completions)。",
"upstreamFormatChat": "Chat Completions需开启路由",
"upstreamFormatResponses": "Responses(原生)",
"upstreamModelName": "上游模型名称",
"upstreamModelNameHint": "Chat 格式下这里填写真实上游模型;路由会把 Codex 的 Responses 请求转换为 Chat Completions 并保持该模型。",
@@ -1297,17 +1311,17 @@
"reasoningEffortHint": "上游支持 low/high/max 等思考深度控制时启用。启用后会自动启用思考模式,并把 Codex 的 reasoning.effort 转成上游 Chat 参数。",
"reasoningGroupTitle": "思考能力",
"reasoningSectionHint": "预设供应商已自动配置;自定义供应商会按名称/地址自动推断。仅当自动识别不准时才需手动覆盖。",
"advancedSectionHint": "包含本地路由映射、上游格式、模型映射、思考能力与自定义 User-Agent。供应商使用 Chat Completions 协议或非 GPT 模型时,需在此开启本地路由映射。"
"advancedSectionHint": "包含上游格式、模型映射、思考能力与自定义 User-Agent。使用 Chat Completions 协议的供应商需开启路由接管才能使用。"
},
"geminiConfig": {
"envFile": "环境变量 (.env)",
"envFileHint": "使用 .env 格式配置 Gemini 环境变量",
"configJson": "配置文件 (config.json)",
"configJsonHint": "使用 JSON 格式配置 Gemini 扩展参数(可选)",
"writeCommonConfig": "写入通用配置",
"writeCommonConfig": "应用通用配置",
"editCommonConfig": "编辑通用配置",
"editCommonConfigTitle": "编辑 Gemini 通用配置片段",
"commonConfigHint": "该片段会写入 Gemini 的 .env(不允许包含 GOOGLE_GEMINI_BASE_URL、GEMINI_API_KEY",
"commonConfigHint": "该片段会在勾选\"应用通用配置\"时写入 Gemini 的 .env(不允许包含 GOOGLE_GEMINI_BASE_URL、GEMINI_API_KEY",
"extractFromCurrent": "从编辑内容提取",
"extractNoCommonConfig": "当前编辑内容没有可提取的通用配置",
"extractFailed": "提取失败: {{error}}",
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

+13
View File
@@ -0,0 +1,13 @@
<svg width="128" height="128" viewBox="0 0 128 128" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
d="M4 96
C4 96, 24 12, 64 12
C104 12, 124 96, 124 96
Q124 102, 118 102
C94 102, 92 64, 64 64
C36 64, 34 102, 10 102
Q4 102, 4 96
Z"
fill="currentColor"
/>
</svg>

After

Width:  |  Height:  |  Size: 329 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 105 KiB

After

Width:  |  Height:  |  Size: 9.4 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 1.1 MiB

After

Width:  |  Height:  |  Size: 60 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 109 KiB

After

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 1.4 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 125 KiB

After

Width:  |  Height:  |  Size: 38 KiB

+13
View File
@@ -9,24 +9,31 @@ import _byteplus from "./byteplus.png";
import _ccsub from "./ccsub.svg?url";
import _claudecn from "./claudecn.png";
import _cherryin from "./cherryin.png";
import _code0 from "./code0.png";
import _eflowcode from "./eflowcode.png";
import _etok from "./etok.png";
import _fenno from "./fenno-icon.webp";
import _hermes from "./hermes.png";
import _huoshan from "./huoshan.png";
import _nekocode from "./nekocode-icon.png";
import _pateway from "./pateway.jpg";
import _pipellm from "./pipellm.png";
import _qiniu from "./qiniu.png";
import _relaxcode from "./relaxcode.png";
import _runapi from "./runapi.jpg";
import _shengsuanyun from "./shengsuanyun.svg?url";
import _subrouter from "./subrouter.svg?url";
import _sudocode from "./sudocode.png";
import _teamorouter from "./TeamoRouter-icon-dark.png";
import _unity2 from "./unity2.png";
import _zetaapi from "./zetaapi-icon.png";
export const icons: Record<string, string> = {
aicodemirror: `<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" style="flex:none;line-height:1" viewBox="0 0 1017.97 1056.47"><title>AICodeMirror</title><path fill="#E4906E" fill-rule="nonzero" d="M944.92 1014.53c-17.29,-9.23 -33.98,-19.28 -50.08,-30.16 -5.39,-3.65 -14.99,-11.7 -28.81,-24.16 -6.06,-5.47 -14.07,-13.51 -24.03,-24.13 -15.15,-16.17 -29.61,-29.9 -41.69,-40.4 -3.98,-3.46 -14.2,-11.02 -30.68,-22.69 -6.24,-4.42 -12.88,-12.15 -18.63,-19.09 -23.98,-29.04 -49.53,-58.44 -76.66,-88.19 -11.93,-13.1 -25.64,-26.11 -35.61,-36.5 -28.72,-29.92 -51.92,-51.96 -78.23,-79.66 -11.24,-11.83 -20.52,-21.3 -27.85,-28.41 -0.56,-0.53 -1.3,-0.84 -2.08,-0.84 -0.51,0 -1.02,0.14 -1.47,0.39 -9.76,5.54 -17.53,14.33 -24.91,23.31 -10.71,13.01 -21.86,26.65 -33.44,40.91 -4.37,5.38 -7.9,9.46 -10.56,12.24 -5.43,5.67 -9.83,10.88 -15.08,15.39 -9.57,8.23 -19.57,16.01 -29.98,23.31 -14.73,10.32 -29.07,20.29 -43.04,29.9 -5.22,3.61 -13.68,9.81 -20.14,15.41 -25.71,22.32 -53.59,46.12 -83.65,71.41 -9.46,7.95 -19.65,16.88 -34.02,29.22 -25.66,22.03 -52.94,40.06 -81.67,55.65 -7.71,4.19 -14.15,8.2 -19.32,12.01 -19.3,14.23 -33.84,25.65 -43.62,34.28 -25.99,22.91 -43.04,37.82 -51.16,44.73 -9.19,7.8 -18.6,17.05 -28.42,25.54 -2.71,2.35 -5.7,3.03 -8.96,2.01 -0.78,-0.24 -1.25,-1.02 -1.1,-1.82 0.36,-2 1.19,-4.1 2.47,-6.32 6.86,-11.81 14.46,-23.09 19.95,-36.03 3.48,-8.23 7.87,-16.52 13.18,-24.89 2.03,-3.19 4.73,-8.77 8.11,-16.74 2.98,-7.02 7.34,-15.05 13.07,-24.12 5.79,-9.14 16,-23.36 30.63,-42.67 7.66,-10.11 19.49,-23.6 35.49,-40.47 4.9,-5.16 12.21,-11.87 21.92,-20.14 12.12,-10.31 23.53,-21.19 34.23,-32.65 11.73,-12.54 16.99,-22.33 27.39,-40.8 2.37,-4.19 6.49,-9.43 12.37,-15.71 8.27,-8.82 17,-17.23 26.21,-25.23 30.11,-26.18 55.17,-47.43 75.17,-63.76 8.66,-7.08 26.42,-21.39 39.65,-30.77 17.11,-12.13 28.62,-20.44 34.53,-24.91 4.5,-3.4 8.93,-6.6 13.3,-10.56 26.03,-23.54 51.66,-45.71 77.28,-70.7 0.42,-0.41 0.51,-1.06 0.21,-1.58 -6.8,-11.78 -12.84,-21.8 -18.11,-30.06 -10.22,-15.99 -22.07,-29.65 -35.57,-40.99 -7.56,-6.36 -18.41,-13.85 -28.65,-20.43 -15.08,-9.66 -30.62,-21.97 -46.63,-36.9 -35.08,-32.73 -67.65,-71.22 -85.32,-115.42 -5.53,-13.85 -10.8,-29.31 -15.8,-46.37 -5.89,-20.13 -12.37,-35.63 -23.22,-51.27 -8.93,-12.9 -15.77,-21.94 -19.58,-35.93 -1.27,-4.67 -2.93,-12.75 -4.99,-24.23 -2.07,-11.54 -6.54,-22.62 -13.41,-33.25 -7.54,-11.68 -13.66,-21.04 -18.33,-28.08 -3.68,-5.53 -7.02,-12.39 -9.63,-18.53 -3.9,-9.18 -8.14,-15.7 -13.6,-23.37 -3.94,-5.53 -5.07,-12.75 0,-18.32 4.14,-4.57 17.49,-3.02 21.56,-1.13 3.86,1.81 8.1,5.13 12.71,9.94 16.16,16.88 26.41,27.77 30.74,32.66 4.69,5.31 11.21,13.79 16.69,19.94 20.19,22.63 36.17,39.74 47.36,59.71 10.46,18.66 16.41,30.42 29.84,44.67 9.32,9.92 17.94,19.33 25.85,28.23 9.01,10.15 19.25,22.95 30.72,38.39 7.54,10.17 13.89,20.11 19.05,29.84 6.39,12.05 10.8,30.19 15.13,41.41 4.88,12.67 12.52,23.25 22.92,31.75 0.58,0.47 6.79,5.44 18.62,14.89 13.54,10.82 23.74,23.47 30.61,37.96 4.55,9.58 7.82,16.16 9.8,19.74 6.62,11.85 14.64,22.05 24.07,30.59 8.99,8.14 17.47,13.2 31.06,22.64 4.28,2.96 6.68,5.98 10.65,2.54 7.08,-6.11 13.73,-10.71 17.96,-14.53 6.12,-5.54 11.71,-11.84 16.79,-18.92 3.5,-4.88 8.77,-10.16 15.19,-14.42 22.77,-15.02 38.17,-31.11 63.32,-55.15 22.13,-21.17 46.22,-47.56 69.25,-66.8 32.17,-26.89 54.99,-45.9 68.46,-57.01 17.15,-14.15 35.82,-30.97 56.02,-50.46 16.06,-15.5 29.25,-27.72 39.57,-36.66 9.78,-8.47 17.55,-14.8 23.31,-18.98 8.52,-6.2 18.55,-10.61 30.56,-15.03 12.34,-4.55 23.44,-11.06 35.67,-17.61 9.07,-4.85 19.76,-9.89 30.05,-8.84 0.5,0.06 0.97,0.32 1.3,0.72 0.85,1.07 1.01,2.48 0.48,4.23 -2.1,6.99 -5.15,13.55 -9.66,20.87 -6.42,10.42 -11.51,19.46 -15.29,27.11 -6.09,12.35 -9.66,19.49 -10.69,21.43 -7.78,14.65 -17.56,27.97 -29.34,39.97 -4.8,4.89 -12.93,12.92 -24.37,24.07 -14.23,13.87 -25.02,30.77 -37.12,50.78 -15.21,25.13 -29.56,48.47 -43.06,70.01 -5.21,8.29 -13.68,15.13 -21.58,21.47 -25.71,20.7 -46.75,41.48 -70.98,64.67 -1.97,1.88 -4.98,4.47 -9.03,7.76 -22.62,18.36 -45.88,35.93 -69.78,52.74 -5.96,4.2 -13.77,11.24 -23.42,21.11 -17.12,17.5 -25.93,26.56 -26.42,27.19 -1.22,1.54 -1.08,3.09 0.41,4.67 14.11,14.81 34.25,37.65 60.42,68.51 11.89,14.01 24.87,27.08 36.03,39.46 8.75,9.7 16.81,22.11 31.82,42.59 2.69,3.68 13.53,16.07 32.5,37.17 5.17,5.76 11.64,14.47 19.4,26.12 16.37,24.6 36.28,56.2 59.73,94.79 4.2,6.92 7.74,12.33 10.62,16.21 5.41,7.29 10.37,13.74 14.92,20.97 6.26,9.94 11.3,19.92 15.11,29.92 4.29,11.27 7.73,19.49 10.32,24.69 7.21,14.5 14.81,28.41 22.8,41.73 3.44,5.75 6.78,13.03 6.11,20.05 -0.07,0.76 -0.71,1.34 -1.48,1.34 -0.25,0 -0.49,-0.06 -0.71,-0.18l0 0.01z"/></svg>`,
aicoding: `<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" style="flex:none;line-height:1" viewBox="0 0 470 470"><title>AICoding</title><path fill="#a78bfa" d="M 33 73 L 137 13 L 263 83 L 159 143 Z"/><path fill="#a78bfa" opacity="0.92" d="M 33 73 L 33 213 L 159 283 L 159 143 Z"/><path fill="#fff" d="M 207 247 L 311 187 L 431 257 L 327 317 Z"/><path fill="#a78bfa" opacity="0.92" d="M 207 247 L 207 387 L 327 457 L 327 317 Z"/><path fill="#fdba74" d="M 327 317 L 431 257 L 431 397 L 327 457 Z"/><path fill="none" stroke="#2b1b4b" stroke-width="22" stroke-linecap="round" stroke-linejoin="round" d="M 33 73 L 137 13 L 263 83 L 159 143 L 33 73"/><path fill="none" stroke="#2b1b4b" stroke-width="22" stroke-linecap="round" stroke-linejoin="round" d="M 33 73 L 33 213 L 159 283 L 159 143"/><path fill="none" stroke="#2b1b4b" stroke-width="22" stroke-linecap="round" stroke-linejoin="round" d="M 159 143 L 263 83 L 263 223"/><path fill="none" stroke="#2b1b4b" stroke-width="22" stroke-linecap="round" stroke-linejoin="round" d="M 207 247 L 311 187 L 431 257 L 327 317 L 207 247"/><path fill="none" stroke="#2b1b4b" stroke-width="22" stroke-linecap="round" stroke-linejoin="round" d="M 207 247 L 207 387 L 327 457 L 327 317"/><path fill="none" stroke="#2b1b4b" stroke-width="22" stroke-linecap="round" stroke-linejoin="round" d="M 327 317 L 431 257 L 431 397 L 327 457"/><path fill="none" stroke="#2b1b4b" stroke-width="22" stroke-linecap="round" stroke-linejoin="round" d="M 245 163 L 365 163"/><path fill="none" stroke="#2b1b4b" stroke-width="22" stroke-linecap="round" stroke-linejoin="round" d="M 175 197 L 335 117"/><path fill="none" stroke="#2b1b4b" stroke-width="22" stroke-linecap="round" stroke-linejoin="round" d="M 365 163 L 365 241"/></svg>`,
aigocode: `<svg height="1em" width="1em" style="flex:none;line-height:1" viewBox="0 0 648 564" xmlns="http://www.w3.org/2000/svg"><title>AiGoCode</title><g transform="translate(0,564) scale(0.1,-0.1)"><path fill="#7C6AEF" d="M5392 5379 c-26 -10 -36 -28 -56 -108 -24 -94 -47 -140 -101 -199 -56 -62 -117 -96 -219 -121 -101 -25 -116 -35 -116 -75 0 -45 21 -60 123 -89 190 -53 271 -147 336 -384 13 -46 38 -61 85 -49 25 6 36 28 56 111 7 28 23 72 36 99 13 28 21 54 18 58 -3 5 -2 7 3 6 4 -2 29 18 55 44 41 41 145 110 173 114 56 8 126 27 131 36 4 6 7 30 8 54 1 49 -5 54 -104 74 -164 33 -280 148 -320 320 -23 101 -52 129 -108 109z"/><path fill="#5B7FFF" d="M1770 4814 c-138 -25 -301 -93 -425 -177 -80 -55 -227 -197 -280 -272 -98 -138 -161 -279 -201 -447 -16 -66 -18 -164 -21 -1098 -4 -1130 -4 -1144 57 -1331 80 -242 222 -434 444 -598 l56 -42 0 -337 c0 -309 2 -340 19 -379 31 -66 87 -93 158 -74 22 6 202 117 404 249 200 131 398 259 439 285 144 89 48 81 1095 88 912 5 932 6 1012 27 243 64 441 178 599 344 166 176 274 408 304 648 13 110 13 2016 0 2120 -25 190 -83 347 -183 498 -160 240 -384 400 -677 484 l-75 22 -1325 2 c-1086 2 -1339 0 -1400 -12z m1068 -1455 l-3 -751 -71 -18 c-71 -18 -154 -55 -205 -91 -14 -10 -29 -16 -33 -12 -3 3 -6 91 -6 195 l0 188 -306 0 -305 0 -21 -47 c-11 -27 -33 -75 -48 -108 -16 -33 -44 -96 -62 -140 l-34 -80 -172 -3 c-101 -1 -172 1 -172 7 0 5 14 40 31 78 31 68 101 227 254 578 335 769 358 814 434 874 94 74 122 79 449 80 l272 1 -2 -751z m794 717 c41 -13 103 -42 139 -62 67 -40 202 -168 232 -221 l17 -31 -77 -65 c-43 -35 -101 -80 -129 -101 l-52 -36 -39 57 c-79 116 -204 177 -313 154 -66 -14 -105 -42 -130 -91 -19 -37 -20 -60 -20 -400 0 -339 1 -363 20 -399 26 -51 61 -78 128 -97 143 -42 327 52 366 186 l13 45 -163 3 -163 2 -3 157 c-2 111 0 157 9 160 6 2 263 3 570 1 487 -3 562 -5 593 -19 98 -45 125 -159 58 -244 -39 -50 -66 -55 -322 -55 l-236 0 -6 -27 c-18 -83 -29 -115 -62 -183 -69 -143 -230 -269 -402 -316 -81 -22 -271 -25 -350 -5 -173 43 -289 145 -341 298 -19 57 -20 81 -17 534 l3 474 33 67 c55 112 168 199 307 235 79 20 246 10 337 -21z m828 -1515 c67 -71 67 -73 -62 -396 -45 -110 -102 -254 -128 -320 -254 -639 -272 -681 -298 -702 -74 -62 -192 -15 -192 77 0 12 39 116 86 233 48 117 97 239 110 272 119 311 336 833 354 852 36 37 85 32 130 -16z m-1574 -205 c16 -13 19 -29 22 -128 l3 -113 -195 -155 c-108 -85 -196 -158 -196 -161 0 -4 17 -19 38 -35 128 -96 327 -272 339 -301 16 -39 17 -143 2 -177 -15 -33 -34 -39 -67 -22 -48 26 -566 446 -584 474 -23 35 -23 89 1 128 16 27 150 143 376 326 39 31 79 65 90 75 44 41 128 103 139 103 7 0 21 -6 32 -14z m713 -25 c52 -37 53 -33 -92 -551 -36 -129 -80 -289 -98 -355 -39 -143 -62 -175 -129 -175 -47 0 -92 20 -114 52 -24 34 -19 90 18 217 19 64 78 271 131 461 53 190 98 353 101 364 6 16 14 18 79 14 54 -4 81 -11 104 -27z m1116 -351 c55 -45 117 -98 138 -120 61 -64 48 -115 -50 -192 -32 -25 -118 -94 -191 -152 -136 -108 -163 -120 -217 -100 -26 10 -47 62 -39 97 10 46 22 62 94 119 36 29 93 77 128 106 l62 55 -65 59 c-71 65 -77 84 -49 141 24 47 44 67 68 67 12 0 66 -36 121 -80z"/><path fill="#5B7FFF" d="M2328 3755 c-37 -20 -54 -53 -153 -280 -48 -110 -96 -219 -106 -242 l-18 -43 234 0 235 0 0 290 0 290 -82 0 c-53 -1 -93 -6 -110 -15z"/></g></svg>`,
alibaba: `<svg height="1em" style="flex:none;line-height:1" viewBox="0 0 24 24" width="1em" xmlns="http://www.w3.org/2000/svg"><title>Alibaba</title><path d="M24 14.014c-2.8 1.512-5.62 2.896-8.759 3.524-.7.139-1.476.139-2.187.043-.678-.085-1.017-.682-.776-1.31.23-.585.536-1.181.93-1.671.852-1.065 1.814-2.034 2.678-3.088a15.75 15.75 0 001.422-2.054c.306-.511.164-1.129-.372-1.384-.897-.437-1.859-.745-2.81-1.075-.11-.043-.274.074-.492.149.273.244.47.425.743.67-2.821.48-5.49 1.16-8.08 2.098-.012.053-.033.095-.023.117.383.585.208 1.032-.35 1.394a2.365 2.365 0 00-.568.522c1.706.5 3.226.213 4.68-.735-.087-.127-.175-.244-.262-.372.546.096.874.394.918.862.011.107-.054.213-.087.32-.077-.086-.175-.17-.24-.267-.045-.064-.056-.138-.088-.245-1.728 1.15-3.587 1.438-5.632.842 0 .404-.022.745.011 1.075.022.287-.098.415-.36.564-.591.362-1.204.735-1.696 1.214-.59.585-.371 1.299.427 1.597.907.34 1.859.35 2.81.234 1.126-.139 2.23-.32 3.456-.49-1.433.67-2.844 1.14-4.33 1.33-1.04.14-2.078.214-3.106-.084-1.476-.415-2.133-1.501-1.75-2.96.361-1.363 1.236-2.449 2.176-3.45 3.139-3.332 7.108-5.024 11.7-5.365 1.072-.074 2.155.064 3.16.511 1.411.639 2.002 1.99 1.313 3.354-.448.905-1.072 1.735-1.695 2.555-.612.809-1.301 1.554-1.946 2.331-.186.234-.361.48-.503.745-.274.5-.088.83.492.778 1.213-.118 2.45-.213 3.62-.511 1.716-.437 3.389-1.054 5.084-1.597.175-.043.339-.107.492-.17z" fill="#FF6003" fill-rule="evenodd"></path></svg>`,
amux: `<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" style="flex:none;line-height:1" viewBox="0 0 128 128" fill="none"><title>Amux</title><path d="M4 96 C4 96, 24 12, 64 12 C104 12, 124 96, 124 96 Q124 102, 118 102 C94 102, 92 64, 64 64 C36 64, 34 102, 10 102 Q4 102, 4 96 Z" fill="currentColor"/></svg>`,
anthropic: `<svg fill="currentColor" fill-rule="evenodd" height="1em" style="flex:none;line-height:1" viewBox="0 0 24 24" width="1em" xmlns="http://www.w3.org/2000/svg"><title>Anthropic</title><path d="M13.827 3.52h3.603L24 20h-3.603l-6.57-16.48zm-7.258 0h3.767L16.906 20h-3.674l-1.343-3.461H5.017l-1.344 3.46H0L6.57 3.522zm4.132 9.959L8.453 7.687 6.205 13.48H10.7z"></path></svg>`,
aws: `<svg fill="currentColor" fill-rule="evenodd" height="1em" style="flex:none;line-height:1" viewBox="0 0 24 24" width="1em" xmlns="http://www.w3.org/2000/svg"><title>AWS</title><path d="M6.763 11.212c0 .296.032.535.088.71.064.176.144.368.256.576.04.063.056.127.056.183 0 .08-.048.16-.152.24l-.503.335a.383.383 0 01-.208.072c-.08 0-.16-.04-.239-.112a2.47 2.47 0 01-.287-.375 6.18 6.18 0 01-.248-.471c-.622.734-1.405 1.101-2.347 1.101-.67 0-1.205-.191-1.596-.574-.39-.384-.59-.894-.59-1.533 0-.678.24-1.23.726-1.644.487-.415 1.133-.623 1.955-.623.272 0 .551.024.846.064.296.04.6.104.918.176v-.583c0-.607-.127-1.03-.375-1.277-.255-.248-.686-.367-1.3-.367-.28 0-.568.031-.863.103-.295.072-.583.16-.862.272a2.4 2.4 0 01-.28.104.488.488 0 01-.127.023c-.112 0-.168-.08-.168-.247v-.391c0-.128.016-.224.056-.28a.597.597 0 01.224-.167 4.577 4.577 0 011.005-.36 4.84 4.84 0 011.246-.151c.95 0 1.644.216 2.091.647.44.43.662 1.085.662 1.963v2.586h.016zm-3.24 1.214c.263 0 .534-.048.822-.144a1.78 1.78 0 00.758-.51 1.27 1.27 0 00.272-.512c.047-.191.08-.423.08-.694v-.335a6.66 6.66 0 00-.735-.136 6.02 6.02 0 00-.75-.048c-.535 0-.926.104-1.19.32-.263.215-.39.518-.39.917 0 .375.095.655.295.846.191.2.47.296.838.296zm6.41.862c-.144 0-.24-.024-.304-.08-.064-.048-.12-.16-.168-.311L7.586 6.726a1.398 1.398 0 01-.072-.32c0-.128.064-.2.191-.2h.783c.151 0 .255.025.31.08.065.048.113.16.16.312l1.342 5.284 1.245-5.284c.04-.16.088-.264.151-.312a.549.549 0 01.32-.08h.638c.152 0 .256.025.32.08.063.048.12.16.151.312l1.261 5.348 1.381-5.348c.048-.16.104-.264.16-.312a.52.52 0 01.311-.08h.743c.127 0 .2.065.2.2 0 .04-.009.08-.017.128a1.137 1.137 0 01-.056.2l-1.923 6.17c-.048.16-.104.263-.168.311a.51.51 0 01-.303.08h-.687c-.15 0-.255-.024-.32-.08-.063-.056-.119-.16-.15-.32L12.32 7.747l-1.23 5.14c-.04.16-.087.264-.15.32-.065.056-.177.08-.32.08l-.686.001zm10.256.215c-.415 0-.83-.048-1.229-.143-.399-.096-.71-.2-.918-.32-.128-.071-.215-.151-.247-.223a.563.563 0 01-.048-.224v-.407c0-.167.064-.247.183-.247.048 0 .096.008.144.024.048.016.12.048.2.08.271.12.566.215.878.279.32.064.63.096.95.096.502 0 .894-.088 1.165-.264a.86.86 0 00.415-.758.777.777 0 00-.215-.559c-.144-.151-.416-.287-.807-.415l-1.157-.36c-.583-.183-1.014-.454-1.277-.813a1.902 1.902 0 01-.4-1.158c0-.335.073-.63.216-.886.144-.255.335-.479.575-.654.24-.184.51-.32.83-.415.32-.096.655-.136 1.006-.136.175 0 .36.008.535.032.183.024.35.056.518.088.16.04.312.08.455.127.144.048.256.096.336.144a.69.69 0 01.24.2.43.43 0 01.071.263v.375c0 .168-.064.256-.184.256a.83.83 0 01-.303-.096 3.652 3.652 0 00-1.532-.311c-.455 0-.815.071-1.062.223-.248.152-.375.383-.375.71 0 .224.08.416.24.567.16.152.454.304.877.44l1.134.358c.574.184.99.44 1.237.767.247.327.367.702.367 1.117 0 .343-.072.655-.207.926a2.157 2.157 0 01-.583.703c-.248.2-.543.343-.886.447-.36.111-.734.167-1.142.167z"></path><path d="M.378 15.475c3.384 1.963 7.56 3.153 11.877 3.153 2.914 0 6.114-.607 9.06-1.852.44-.2.814.287.383.607-2.626 1.94-6.442 2.969-9.722 2.969-4.598 0-8.74-1.7-11.87-4.526-.247-.223-.024-.527.272-.351zm23.531-.2c.287.36-.08 2.826-1.485 4.007-.215.184-.423.088-.327-.151l.175-.439c.343-.88.802-2.198.52-2.555-.336-.43-2.22-.207-3.074-.103-.255.032-.295-.192-.063-.36 1.5-1.053 3.967-.75 4.254-.399z" fill="#F90"></path></svg>`,
azure: `<svg height="1em" style="flex:none;line-height:1" viewBox="0 0 24 24" width="1em" xmlns="http://www.w3.org/2000/svg"><title>Azure</title><path d="M7.242 1.613A1.11 1.11 0 018.295.857h6.977L8.03 22.316a1.11 1.11 0 01-1.052.755h-5.43a1.11 1.11 0 01-1.053-1.466L7.242 1.613z" fill="url(#lobe-icons-azure-fill-0)"></path><path d="M18.397 15.296H7.4a.51.51 0 00-.347.882l7.066 6.595c.206.192.477.298.758.298h6.226l-2.706-7.775z" fill="#0078D4"></path><path d="M15.272.857H7.497L0 23.071h7.775l1.596-4.73 5.068 4.73h6.665l-2.707-7.775h-7.998L15.272.857z" fill="url(#lobe-icons-azure-fill-1)"></path><path d="M17.193 1.613a1.11 1.11 0 00-1.052-.756h-7.81.035c.477 0 .9.304 1.052.756l6.748 19.992a1.11 1.11 0 01-1.052 1.466h-.12 7.895a1.11 1.11 0 001.052-1.466L17.193 1.613z" fill="url(#lobe-icons-azure-fill-2)"></path><defs><linearGradient gradientUnits="userSpaceOnUse" id="lobe-icons-azure-fill-0" x1="8.247" x2="1.002" y1="1.626" y2="23.03"><stop stop-color="#114A8B"></stop><stop offset="1" stop-color="#0669BC"></stop></linearGradient><linearGradient gradientUnits="userSpaceOnUse" id="lobe-icons-azure-fill-1" x1="14.042" x2="12.324" y1="15.302" y2="15.888"><stop stop-opacity=".3"></stop><stop offset=".071" stop-opacity=".2"></stop><stop offset=".321" stop-opacity=".1"></stop><stop offset=".623" stop-opacity=".05"></stop><stop offset="1" stop-opacity="0"></stop></linearGradient><linearGradient gradientUnits="userSpaceOnUse" id="lobe-icons-azure-fill-2" x1="12.841" x2="20.793" y1="1.626" y2="22.814"><stop stop-color="#3CCBF4"></stop><stop offset="1" stop-color="#2892DF"></stop></linearGradient></defs></svg>`,
@@ -103,18 +110,24 @@ export const iconUrls: Record<string, string> = {
claudeapi: _claudeapi,
claudecn: _claudecn,
cherryin: _cherryin,
code0: _code0,
eflowcode: _eflowcode,
etok: _etok,
fenno: _fenno,
hermes: _hermes,
huoshan: _huoshan,
nekocode: _nekocode,
pateway: _pateway,
pipellm: _pipellm,
qiniu: _qiniu,
relaxcode: _relaxcode,
runapi: _runapi,
shengsuanyun: _shengsuanyun,
subrouter: _subrouter,
sudocode: _sudocode,
teamorouter: _teamorouter,
unity2: _unity2,
zetaapi: _zetaapi,
};
export const iconList = [
+85
View File
@@ -77,6 +77,13 @@ export const iconMetadata: Record<string, IconMetadata> = {
keywords: ["qwen", "tongyi"],
defaultColor: "#FF6A00",
},
amux: {
name: "amux",
displayName: "Amux",
category: "ai-provider",
keywords: ["amux", "amuxapi", "aggregator", "relay", "gateway", "gpt"],
defaultColor: "#000000",
},
anthropic: {
name: "anthropic",
displayName: "Anthropic",
@@ -204,6 +211,13 @@ export const iconMetadata: Record<string, IconMetadata> = {
keywords: ["cloudflare", "cdn"],
defaultColor: "#F38020",
},
code0: {
name: "code0",
displayName: "Code0",
category: "ai-provider",
keywords: ["code0", "code0ai", "aggregator", "relay", "gateway", "gpt"],
defaultColor: "#20C050",
},
cohere: {
name: "cohere",
displayName: "Cohere",
@@ -225,6 +239,52 @@ export const iconMetadata: Record<string, IconMetadata> = {
keywords: ["etok", "ai", "programming"],
defaultColor: "#F97316",
},
fenno: {
name: "fenno",
displayName: "FennoAI",
category: "ai-provider",
keywords: [
"fenno",
"fennoai",
"aggregator",
"relay",
"claude",
"codex",
"gpt",
"gateway",
],
defaultColor: "#000000",
},
zetaapi: {
name: "zetaapi",
displayName: "ZetaAPI",
category: "ai-provider",
keywords: [
"zetaapi",
"zeta",
"aggregator",
"relay",
"claude",
"gpt",
"gateway",
],
defaultColor: "#000000",
},
teamorouter: {
name: "teamorouter",
displayName: "TeamoRouter",
category: "ai-provider",
keywords: [
"teamorouter",
"teamo",
"router",
"aggregator",
"relay",
"gateway",
"gpt",
],
defaultColor: "#000000",
},
cubence: {
name: "cubence",
displayName: "Cubence",
@@ -358,6 +418,13 @@ export const iconMetadata: Record<string, IconMetadata> = {
keywords: ["mistral"],
defaultColor: "#FF7000",
},
nekocode: {
name: "nekocode",
displayName: "NekoCode",
category: "ai-provider",
keywords: ["nekocode", "neko", "aggregator", "relay", "gateway", "gpt"],
defaultColor: "#A64BC4",
},
newapi: {
name: "newapi",
displayName: "newapi",
@@ -511,6 +578,24 @@ export const iconMetadata: Record<string, IconMetadata> = {
keywords: ["pipellm", "pipe"],
defaultColor: "currentColor",
},
qiniu: {
name: "qiniu",
displayName: "七牛云",
category: "ai-provider",
keywords: [
"qiniu",
"qnaigc",
"modelink",
"aggregator",
"relay",
"claude",
"codex",
"gpt",
"gemini",
"gateway",
],
defaultColor: "#00AAE7",
},
runapi: {
name: "runapi",
displayName: "RunAPI",
Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 69 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 MiB

After

Width:  |  Height:  |  Size: 41 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 212 KiB

After

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 432 KiB

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

+33
View File
@@ -191,3 +191,36 @@ input[type="password"]::-ms-reveal,
input[type="password"]::-ms-clear {
display: none;
}
/*
* Usage date-range picker popover.
*
* The two-column layout (date fields | calendar) must depend on how much
* width the popover itself actually has not on the viewport. The popover is
* clamped to `100vw - 2rem` and anchored to its trigger, so a viewport-based
* breakpoint (e.g. Tailwind `sm:`) could enable the wide layout while the
* popover only had room for one column, pushing the calendar off-screen.
*
* A container query keys the layout to the popover's own inline size. The base
* (stacked) layout is the safe fallback when container queries are
* unsupported, keeping the calendar fully visible at any width.
*/
.usage-range-popover {
container-type: inline-size;
}
@container (min-width: 500px) {
.usage-range-layout {
flex-direction: row;
}
.usage-range-fields {
width: 250px;
flex: none;
}
.usage-range-calendar {
min-width: 0;
flex: 1 1 0%;
}
}
+10 -1
View File
@@ -9,6 +9,7 @@ import { extractErrorMessage } from "@/utils/errorUtils";
import { generateUUID } from "@/utils/uuid";
import { openclawKeys } from "@/hooks/useOpenClaw";
import { invalidateHermesProviderCaches } from "@/hooks/useHermes";
import { usageKeys } from "@/lib/query/usage";
export const useAddProviderMutation = (appId: AppId) => {
const queryClient = useQueryClient();
@@ -141,8 +142,16 @@ export const useUpdateProviderMutation = (appId: AppId) => {
await providersApi.update(provider, appId, originalId);
return provider;
},
onSuccess: async () => {
onSuccess: async (provider, variables) => {
await queryClient.invalidateQueries({ queryKey: ["providers", appId] });
await queryClient.invalidateQueries({
queryKey: usageKeys.script(provider.id, appId),
});
if (variables.originalId && variables.originalId !== provider.id) {
await queryClient.invalidateQueries({
queryKey: usageKeys.script(variables.originalId, appId),
});
}
if (appId === "openclaw") {
await queryClient.invalidateQueries({
queryKey: openclawKeys.health,
+9
View File
@@ -258,6 +258,15 @@ export interface CodexCatalogModel {
model: string;
displayName?: string;
contextWindow?: string | number;
// Native Responses (direct) profile overrides for the generated
// model-catalogs.json. Ignored by the chat/proxy profile.
// e.g. MiniMax: supportsParallelToolCalls=true, inputModalities=["text","image"].
supportsParallelToolCalls?: boolean;
inputModalities?: string[];
// Vendor's OFFICIAL base_instructions (model identity / system preamble).
// Codex requires this field in every catalog entry; when omitted the backend
// falls back to a neutral default. e.g. MiMo "developed by Xiaomi".
baseInstructions?: string;
}
// Claude 认证字段类型
+1 -1
View File
@@ -99,7 +99,7 @@ export const updateCommonConfigSnippet = (
} catch (err) {
return {
updatedConfig: jsonString,
error: "配置 JSON 解析失败,无法写入通用配置",
error: "配置 JSON 解析失败,无法应用通用配置",
};
}
@@ -173,7 +173,7 @@ describe("ClaudeDesktopProviderForm", () => {
// claude-old 迁移到 Sonnet;留空的 Opus / Fable / Haiku 回填为 Sonnet 的
// 上游模型,保证落库四档齐全,子 agent 调用的各档始终可解析。
expect(submitted.meta.claudeDesktopModelRoutes).toMatchObject({
"claude-sonnet-4-6": {
"claude-sonnet-5": {
model: "upstream-old",
labelOverride: "upstream-old",
},
@@ -186,7 +186,7 @@ describe("ClaudeDesktopProviderForm", () => {
"claude-fable-5",
"claude-haiku-4-5",
"claude-opus-4-8",
"claude-sonnet-4-6",
"claude-sonnet-5",
],
);
});
@@ -217,7 +217,7 @@ describe("ClaudeDesktopProviderForm", () => {
await waitFor(() => expect(onSubmit).toHaveBeenCalled());
const routes = onSubmit.mock.calls[0][0].meta.claudeDesktopModelRoutes;
// 留空的 Opus / Haiku 回填同一上游模型,1M 声明应与 Sonnet 一致。
expect(routes["claude-sonnet-4-6"]).toMatchObject({
expect(routes["claude-sonnet-5"]).toMatchObject({
model: "deepseek-v4-pro",
supports1m: true,
});
@@ -259,8 +259,8 @@ describe("ClaudeDesktopProviderForm", () => {
await waitFor(() => expect(onSubmit).toHaveBeenCalled());
const submitted = onSubmit.mock.calls[0][0];
expect(submitted.meta.claudeDesktopModelRoutes).toMatchObject({
"claude-sonnet-4-6": {
model: "claude-sonnet-4-6",
"claude-sonnet-5": {
model: "claude-sonnet-5",
},
});
});
@@ -15,4 +15,38 @@ describe("ProviderForm Codex catalog helpers", () => {
{ model: "kimi-k2", contextWindow: 128000 },
]);
});
it("preserves native-profile overrides (parallel tool calls + input modalities + base instructions)", () => {
expect(
normalizeCodexCatalogModelsForSave([
{
model: "MiniMax-M3",
displayName: "MiniMax-M3",
contextWindow: 1000000,
supportsParallelToolCalls: true,
inputModalities: ["text", "image"],
baseInstructions:
" You are Codex, a coding agent based on MiniMax-M3. ",
},
// false must be preserved (not dropped as falsy); empty modalities dropped;
// empty/whitespace baseInstructions dropped
{
model: "mimo-v2.5-pro",
supportsParallelToolCalls: false,
inputModalities: [],
baseInstructions: " ",
},
]),
).toEqual([
{
model: "MiniMax-M3",
displayName: "MiniMax-M3",
contextWindow: 1000000,
supportsParallelToolCalls: true,
inputModalities: ["text", "image"],
baseInstructions: "You are Codex, a coding agent based on MiniMax-M3.",
},
{ model: "mimo-v2.5-pro", supportsParallelToolCalls: false },
]);
});
});
@@ -0,0 +1,29 @@
import fs from "node:fs";
import path from "node:path";
import { describe, expect, it } from "vitest";
const SELECT_TSX = path.resolve(
__dirname,
"..",
"..",
"src",
"components",
"ui",
"select.tsx",
);
describe("SelectContent scroll bounds", () => {
const source = fs.readFileSync(SELECT_TSX, "utf8");
it("limits popper content to the available viewport height", () => {
expect(source).toContain("--radix-select-content-available-height");
expect(source).toContain(
"max-h-[min(24rem,var(--radix-select-content-available-height))]",
);
});
it("allows long option lists to scroll vertically", () => {
expect(source).toContain("overflow-y-auto");
expect(source).toContain("overflow-x-hidden");
});
});
+355 -5
View File
@@ -7,6 +7,7 @@ import {
waitFor,
within,
} from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { beforeEach, describe, expect, it, vi } from "vitest";
import { SessionManagerPage } from "@/components/sessions/SessionManagerPage";
import { sessionsApi } from "@/lib/api/sessions";
@@ -15,6 +16,8 @@ import { setSessionFixtures } from "../msw/state";
const toastSuccessMock = vi.fn();
const toastErrorMock = vi.fn();
const GROUP_EXPANSION_STORAGE_KEY =
"cc-switch.sessionManager.groupExpansionState";
vi.mock("sonner", () => ({
toast: {
@@ -56,7 +59,7 @@ vi.mock("@/components/ConfirmDialog", () => ({
) : null,
}));
const renderPage = () => {
const renderPage = (appId = "codex") => {
const client = new QueryClient({
defaultOptions: {
queries: { retry: false },
@@ -68,7 +71,7 @@ const renderPage = () => {
client,
...render(
<QueryClientProvider client={client}>
<SessionManagerPage appId="codex" />
<SessionManagerPage appId={appId} />
</QueryClientProvider>,
),
};
@@ -87,8 +90,8 @@ const openSearch = () => {
};
const closeSearch = () => {
const closeButton = Array.from(screen.getAllByRole("button")).find(
(button) => button.querySelector(".lucide-x"),
const closeButton = Array.from(screen.getAllByRole("button")).find((button) =>
button.querySelector(".lucide-x"),
);
if (!closeButton) {
@@ -98,11 +101,61 @@ const closeSearch = () => {
fireEvent.click(closeButton);
};
const openViewModeMenu = async () => {
await userEvent.click(screen.getByRole("combobox", { name: /查看方式/i }));
};
const switchToGroupedView = async () => {
await openViewModeMenu();
const groupedOption = await screen.findByRole("option", { name: /分类/i });
await userEvent.click(groupedOption);
await waitFor(() =>
expect(
screen.queryByRole("option", { name: /分类/i }),
).not.toBeInTheDocument(),
);
};
const switchProviderFilter = async (providerLabel: RegExp) => {
const providerFilterTrigger = screen.getByRole("combobox", {
name: /供应商筛选/i,
});
await userEvent.click(providerFilterTrigger);
await userEvent.click(
await screen.findByRole("option", { name: providerLabel }),
);
};
const enterGroupedBatchMode = async () => {
await switchToGroupedView();
fireEvent.click(screen.getByRole("button", { name: /批量管理/i }));
};
const collapseAllGroups = () => {
fireEvent.click(screen.getByRole("button", { name: /全部收起/i }));
};
const expandDirectoryGroup = (provider: string, directory: string) => {
fireEvent.click(
screen.getByRole("button", {
name: new RegExp(`展开或折叠 ${provider} 供应商分组`),
}),
);
fireEvent.click(
screen.getByRole("button", {
name: new RegExp(`展开或折叠 ${directory} 目录分组`),
}),
);
};
describe("SessionManagerPage", () => {
beforeEach(() => {
toastSuccessMock.mockReset();
toastErrorMock.mockReset();
Element.prototype.scrollIntoView = vi.fn();
window.localStorage.removeItem("cc-switch.sessionManager.listViewMode");
window.localStorage.removeItem(GROUP_EXPANSION_STORAGE_KEY);
const sessions: SessionMeta[] = [
{
@@ -127,6 +180,28 @@ describe("SessionManagerPage", () => {
sourcePath: "/mock/codex/session-2.jsonl",
resumeCommand: "codex resume codex-session-2",
},
{
providerId: "claude",
sessionId: "claude-session-1",
title: "Claude Session",
summary: "Claude summary",
projectDir: "/mock/claude",
createdAt: 3,
lastActiveAt: 30,
sourcePath: "/mock/claude/session-1.jsonl",
resumeCommand: "claude --resume claude-session-1",
},
{
providerId: "codex",
sessionId: "codex-session-3",
title: "Gamma Session",
summary: "Gamma summary",
projectDir: null,
createdAt: 0,
lastActiveAt: 5,
sourcePath: "/mock/codex/session-3.jsonl",
resumeCommand: "codex resume codex-session-3",
},
];
const messages: Record<string, SessionMessage[]> = {
"codex:/mock/codex/session-1.jsonl": [
@@ -135,6 +210,12 @@ describe("SessionManagerPage", () => {
"codex:/mock/codex/session-2.jsonl": [
{ role: "user", content: "beta", ts: 10 },
],
"codex:/mock/codex/session-3.jsonl": [
{ role: "user", content: "gamma", ts: 5 },
],
"claude:/mock/claude/session-1.jsonl": [
{ role: "user", content: "claude", ts: 30 },
],
};
setSessionFixtures(sessions, messages);
@@ -275,7 +356,7 @@ describe("SessionManagerPage", () => {
fireEvent.click(screen.getByRole("button", { name: /批量管理/i }));
fireEvent.click(screen.getByRole("button", { name: /全选当前/i }));
expect(screen.getByText("已选 2 项")).toBeInTheDocument();
expect(screen.getByText("已选 3 项")).toBeInTheDocument();
openSearch();
fireEvent.change(screen.getByRole("textbox"), {
@@ -330,4 +411,273 @@ describe("SessionManagerPage", () => {
});
invalidateSpy.mockRestore();
});
it("switches to grouped view collapsed by default and shows collapse control", async () => {
renderPage("all");
await waitFor(() =>
expect(
screen.getByRole("heading", { name: "Claude Session" }),
).toBeInTheDocument(),
);
await switchToGroupedView();
expect(
screen.getByRole("button", { name: /全部收起/i }),
).toBeInTheDocument();
expect(
screen.getByRole("button", {
name: /展开或折叠 codex 供应商分组/,
}),
).toBeInTheDocument();
expect(
screen.getByRole("button", {
name: /展开或折叠 claude 供应商分组/,
}),
).toBeInTheDocument();
expect(
screen.queryByRole("button", { name: /展开或折叠 codex 目录分组/ }),
).not.toBeInTheDocument();
expect(
screen.queryByRole("button", { name: /Alpha Session/ }),
).not.toBeInTheDocument();
});
it("persists manual expansion and collapses all grouped sessions", async () => {
renderPage();
await waitFor(() =>
expect(
screen.getByRole("heading", { name: "Alpha Session" }),
).toBeInTheDocument(),
);
await switchToGroupedView();
expandDirectoryGroup("codex", "codex");
expect(
screen.getByRole("button", { name: /展开或折叠 codex 目录分组/ }),
).toBeInTheDocument();
expect(
screen.getByRole("button", { name: /Alpha Session/ }),
).toBeInTheDocument();
await waitFor(() =>
expect(
JSON.parse(window.localStorage.getItem(GROUP_EXPANSION_STORAGE_KEY)!),
).toEqual({
expandedProviderIds: ["codex"],
expandedDirectoryKeys: ["codex:/mock/codex"],
}),
);
collapseAllGroups();
await waitFor(() =>
expect(
screen.queryByRole("button", { name: /展开或折叠 codex 目录分组/ }),
).not.toBeInTheDocument(),
);
await waitFor(() =>
expect(
JSON.parse(window.localStorage.getItem(GROUP_EXPANSION_STORAGE_KEY)!),
).toEqual({
expandedProviderIds: [],
expandedDirectoryKeys: [],
}),
);
});
it("keeps filtered grouped sessions collapsed until expanding the group", async () => {
renderPage("all");
await waitFor(() =>
expect(screen.getByText("Alpha Session")).toBeInTheDocument(),
);
fireEvent.click(screen.getByRole("button", { name: /Alpha Session/ }));
await switchToGroupedView();
await switchProviderFilter(/Claude Code/i);
await waitFor(() =>
expect(screen.queryByText("Alpha Session")).not.toBeInTheDocument(),
);
expect(
screen.getByRole("heading", { name: "Claude Session" }),
).toBeInTheDocument();
expect(
screen.getByRole("button", {
name: /展开或折叠 claude 供应商分组/,
}),
).toBeInTheDocument();
expect(
screen.queryByRole("button", { name: /展开或折叠 claude 目录分组/ }),
).not.toBeInTheDocument();
expandDirectoryGroup("claude", "claude");
expect(
screen.getByRole("button", { name: /展开或折叠 claude 目录分组/ }),
).toBeInTheDocument();
expect(
screen.getByRole("button", { name: /Claude Session/ }),
).toBeInTheDocument();
expect(screen.queryByText("Gamma Session")).not.toBeInTheDocument();
});
it("supports batch deletion from grouped view", async () => {
renderPage();
await waitFor(() =>
expect(
screen.getByRole("heading", { name: "Alpha Session" }),
).toBeInTheDocument(),
);
await switchToGroupedView();
fireEvent.click(screen.getByRole("button", { name: /批量管理/i }));
fireEvent.click(screen.getByRole("button", { name: /全选当前/i }));
fireEvent.click(screen.getByRole("button", { name: /批量删除/i }));
const dialog = screen.getByTestId("confirm-dialog");
fireEvent.click(
within(dialog).getByRole("button", { name: /删除所选会话/i }),
);
await waitFor(() => {
expect(screen.queryByText("Alpha Session")).not.toBeInTheDocument();
expect(screen.queryByText("Beta Session")).not.toBeInTheDocument();
expect(screen.queryByText("Gamma Session")).not.toBeInTheDocument();
});
expect(toastErrorMock).not.toHaveBeenCalled();
expect(toastSuccessMock).toHaveBeenCalled();
});
it("selects visible deletable sessions by provider group in grouped batch mode", async () => {
renderPage("all");
await waitFor(() =>
expect(
screen.getByRole("heading", { name: "Claude Session" }),
).toBeInTheDocument(),
);
await enterGroupedBatchMode();
const codexProviderCheckbox = screen.getByRole("checkbox", {
name: /选择 codex 供应商分组内会话/,
});
const claudeProviderCheckbox = screen.getByRole("checkbox", {
name: /选择 claude 供应商分组内会话/,
});
fireEvent.click(codexProviderCheckbox);
expect(codexProviderCheckbox).toBeChecked();
expect(claudeProviderCheckbox).not.toBeChecked();
expect(screen.getByText("已选 3 项")).toBeInTheDocument();
fireEvent.click(codexProviderCheckbox);
expect(codexProviderCheckbox).not.toBeChecked();
expect(screen.getByText("已选 0 项")).toBeInTheDocument();
});
it("selects visible deletable sessions by directory group and marks the provider as mixed", async () => {
renderPage();
await waitFor(() =>
expect(
screen.getByRole("heading", { name: "Alpha Session" }),
).toBeInTheDocument(),
);
await enterGroupedBatchMode();
expandDirectoryGroup("codex", "codex");
const providerCheckbox = screen.getByRole("checkbox", {
name: /选择 codex 供应商分组内会话/,
});
const codexDirectoryCheckbox = screen.getByRole("checkbox", {
name: /选择 codex 目录分组内会话/,
});
fireEvent.click(codexDirectoryCheckbox);
expect(codexDirectoryCheckbox).toBeChecked();
expect(providerCheckbox).toHaveAttribute("aria-checked", "mixed");
expect(screen.getByText("已选 2 项")).toBeInTheDocument();
});
it("marks grouped batch checkboxes as mixed when only one session is selected", async () => {
renderPage();
await waitFor(() =>
expect(
screen.getByRole("heading", { name: "Alpha Session" }),
).toBeInTheDocument(),
);
await enterGroupedBatchMode();
expandDirectoryGroup("codex", "codex");
fireEvent.click(screen.getAllByRole("checkbox", { name: "选择会话" })[0]);
expect(
screen.getByRole("checkbox", {
name: /选择 codex 供应商分组内会话/,
}),
).toHaveAttribute("aria-checked", "mixed");
expect(
screen.getByRole("checkbox", { name: /选择 codex 目录分组内会话/ }),
).toHaveAttribute("aria-checked", "mixed");
expect(screen.getByText("已选 1 项")).toBeInTheDocument();
});
it("batch deletes only sessions selected from a grouped directory", async () => {
renderPage();
await waitFor(() =>
expect(
screen.getByRole("heading", { name: "Alpha Session" }),
).toBeInTheDocument(),
);
await enterGroupedBatchMode();
expandDirectoryGroup("codex", "codex");
fireEvent.click(
screen.getByRole("checkbox", {
name: /选择 codex 目录分组内会话/,
}),
);
fireEvent.click(screen.getByRole("button", { name: /批量删除/i }));
const dialog = screen.getByTestId("confirm-dialog");
fireEvent.click(
within(dialog).getByRole("button", { name: /删除所选会话/i }),
);
await waitFor(() => {
expect(screen.queryByText("Alpha Session")).not.toBeInTheDocument();
expect(screen.queryByText("Beta Session")).not.toBeInTheDocument();
});
expect(
screen.getByRole("button", { name: /展开或折叠 未知目录 目录分组/ }),
).toBeInTheDocument();
expect(
screen.queryByRole("checkbox", { name: "选择会话" }),
).not.toBeInTheDocument();
fireEvent.click(
screen.getByRole("button", { name: /展开或折叠 未知目录 目录分组/ }),
);
expect(
screen.getByRole("checkbox", { name: "选择会话" }),
).toBeInTheDocument();
expect(toastErrorMock).not.toHaveBeenCalled();
expect(toastSuccessMock).toHaveBeenCalled();
});
});
+106
View File
@@ -2,8 +2,10 @@ import { describe, expect, it } from "vitest";
import {
extractCodexPromptPreview,
formatSessionMessagePreview,
groupSessionsByProviderAndDirectory,
shouldHideCodexMessageFromToc,
} from "@/components/sessions/utils";
import type { SessionMeta } from "@/types";
describe("session utils", () => {
it("extracts Codex VS Code prompts after the request marker", () => {
@@ -115,4 +117,108 @@ describe("session utils", () => {
`${"a".repeat(50)}...`,
);
});
it("groups sessions by provider and project directory", () => {
const sessions: SessionMeta[] = [
{
providerId: "codex",
sessionId: "codex-1",
projectDir: "/workspace/app",
},
{
providerId: "codex",
sessionId: "codex-2",
projectDir: "/workspace/app",
},
{
providerId: "claude",
sessionId: "claude-1",
projectDir: "/workspace/docs",
},
];
const groups = groupSessionsByProviderAndDirectory(sessions, "未知目录");
expect(groups).toHaveLength(2);
expect(groups[0].providerId).toBe("codex");
expect(groups[0].sessions.map((session) => session.sessionId)).toEqual([
"codex-1",
"codex-2",
]);
expect(groups[0].directories).toHaveLength(1);
expect(groups[0].directories[0]).toMatchObject({
projectDir: "/workspace/app",
label: "app",
});
expect(
groups[0].directories[0].sessions.map((session) => session.sessionId),
).toEqual(["codex-1", "codex-2"]);
expect(groups[1].providerId).toBe("claude");
expect(groups[1].directories[0].label).toBe("docs");
});
it("uses an unknown directory group for sessions without project directories", () => {
const sessions: SessionMeta[] = [
{
providerId: "codex",
sessionId: "codex-1",
projectDir: null,
},
{
providerId: "codex",
sessionId: "codex-2",
projectDir: " ",
},
];
const groups = groupSessionsByProviderAndDirectory(sessions, "未知目录");
expect(groups).toHaveLength(1);
expect(groups[0].directories).toHaveLength(1);
expect(groups[0].directories[0]).toMatchObject({
projectDir: null,
label: "未知目录",
});
expect(
groups[0].directories[0].sessions.map((session) => session.sessionId),
).toEqual(["codex-1", "codex-2"]);
});
it("preserves filtered session order inside provider and directory groups", () => {
const sessions: SessionMeta[] = [
{
providerId: "codex",
sessionId: "newest",
projectDir: "/workspace/app",
lastActiveAt: 30,
},
{
providerId: "codex",
sessionId: "middle",
projectDir: "/workspace/docs",
lastActiveAt: 20,
},
{
providerId: "codex",
sessionId: "oldest",
projectDir: "/workspace/app",
lastActiveAt: 10,
},
];
const groups = groupSessionsByProviderAndDirectory(sessions, "未知目录");
expect(groups[0].sessions.map((session) => session.sessionId)).toEqual([
"newest",
"middle",
"oldest",
]);
expect(groups[0].directories.map((group) => group.label)).toEqual([
"app",
"docs",
]);
expect(
groups[0].directories[0].sessions.map((session) => session.sessionId),
).toEqual(["newest", "oldest"]);
});
});
+46 -12
View File
@@ -154,23 +154,57 @@ describe("Codex Chat provider presets", () => {
});
it("uses native Responses API for migrated CN providers without local route mapping", () => {
const nativeResponsesPresets = [
"DouBaoSeed",
"Bailian",
"Longcat",
"MiniMax",
"MiniMax en",
"Xiaomi MiMo",
"Xiaomi MiMo Token Plan (China)",
];
const nativeResponsesPresets = new Map<
string,
{ contextWindows: Record<string, number> }
>([
[
"DouBaoSeed",
{ contextWindows: { "doubao-seed-2-1-pro-260628": 262144 } },
],
["Bailian", { contextWindows: { "qwen3-coder-plus": 1048576 } }],
["Longcat", { contextWindows: { "LongCat-2.0-Preview": 1048576 } }],
["MiniMax", { contextWindows: { "MiniMax-M3": 1000000 } }],
["MiniMax en", { contextWindows: { "MiniMax-M3": 1000000 } }],
[
"Xiaomi MiMo",
{
contextWindows: {
"mimo-v2.5-pro": 1048576,
"mimo-v2.5": 1048576,
},
},
],
[
"Xiaomi MiMo Token Plan (China)",
{
contextWindows: {
"mimo-v2.5-pro": 1048576,
"mimo-v2.5": 1048576,
},
},
],
]);
for (const name of nativeResponsesPresets) {
for (const [name, expected] of nativeResponsesPresets) {
const preset = codexProviderPresets.find((item) => item.name === name);
expect(preset, `${name} preset`).toBeDefined();
expect(preset?.apiFormat).toBe("openai_responses");
// 原生 Responses 预设必须不带 modelCatalog,否则“本地路由映射”开关会默认勾选
expect(preset?.modelCatalog ?? []).toHaveLength(0);
// 原生 Responses 预设现在带 modelCatalogcc-switch 直连时据此生成
// ~/.codex 的 model-catalogs.jsonshell_command 编辑、不发 freeform
// apply_patch)。带 catalog 不再强制开“本地路由映射”——前端已按
// apiFormat 解耦(openai_responses 默认不开接管)。
expect((preset?.modelCatalog ?? []).length).toBeGreaterThan(0);
expect(
Object.fromEntries(
(preset?.modelCatalog ?? []).map((model) => [
model.model,
model.contextWindow,
]),
),
).toEqual(expected.contextWindows);
// 原生(直连)不走 Chat 转换,因此不需要 codexChatReasoning。
expect(preset?.codexChatReasoning).toBeUndefined();
}
});
+34
View File
@@ -0,0 +1,34 @@
import { describe, expect, it } from "vitest";
import { codexProviderPresets } from "@/config/codexProviderPresets";
import { openclawProviderPresets } from "@/config/openclawProviderPresets";
// 回归:同一个 doubao 模型在 Codex catalog 与 OpenClaw settingsConfig 里都声明了
// contextWindow,二者必须一致。曾出现 OpenClaw 写成 128000、Codex 写成 262144 的
// 漂移,导致 OpenClaw 用户拿到过小的上下文窗口、长上下文提前压缩/截断。
describe("DouBaoSeed preset consistency across apps", () => {
const DOUBAO_MODEL_ID = "doubao-seed-2-1-pro-260628";
const EXPECTED_CONTEXT_WINDOW = 262144;
it("keeps the doubao context window in sync between OpenClaw and Codex", () => {
const codexPreset = codexProviderPresets.find(
(item) => item.name === "DouBaoSeed",
);
const codexModel = (codexPreset?.modelCatalog ?? []).find(
(model) => model.model === DOUBAO_MODEL_ID,
);
expect(codexModel, "Codex DouBaoSeed catalog model").toBeDefined();
expect(codexModel?.contextWindow).toBe(EXPECTED_CONTEXT_WINDOW);
const openclawPreset = openclawProviderPresets.find(
(item) => item.name === "DouBaoSeed",
);
const openclawModel = (openclawPreset?.settingsConfig.models ?? []).find(
(model) => model.id === DOUBAO_MODEL_ID,
);
expect(openclawModel, "OpenClaw DouBaoSeed model").toBeDefined();
expect(openclawModel?.contextWindow).toBe(EXPECTED_CONTEXT_WINDOW);
// 任一侧单独改动都会破坏这条等式 —— 这是真正防漂移的断言。
expect(openclawModel?.contextWindow).toBe(codexModel?.contextWindow);
});
});

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