Managed-OAuth providers (github_copilot / codex_oauth / xai_oauth) need
proxy takeover to inject credentials, but the "needs routing" badge and
the switch-time warning only keyed off apiFormat — missing OAuth providers
whose upstream format is native (e.g. the new Codex xAI Grok OAuth preset
on openai_responses).
- Add isOAuthProviderType / OAUTH_PROVIDER_TYPES as the SSOT for OAuth types
- Extract providerNeedsRouting() as the authoritative predicate: managed
OAuth always needs routing (the backend rejects these accounts' direct
connection) regardless of apiFormat or Claude Desktop mode
- ProviderCard badges (claude / codex / claude-desktop) and the switch
warning consume the shared predicate instead of raw apiFormat
- Gate the warning on per-app routing readiness: claude-desktop uses
isProxyRunning (backend takeover status has no such field), other apps
use isProxyTakeover — fixes false warnings on Desktop and missing
warnings when the proxy runs but the app isn't taken over
- Force proxy mode for all managed-OAuth providers in the Claude Desktop
form (lock the mode toggle), not only xai_oauth
- Add unit tests for providerNeedsRouting, the switch routing gate, and
Desktop OAuth preset enforcement
- i18n(zh/en/ja/zh-TW): add notifications.proxyReasonManagedOAuth
Add XaiOAuthSection (device-code login, account list, default selection)
backed by the generalized useManagedAuth hook, wire it into the Auth
Center and both Claude provider forms, and expose model fetching for
signed-in accounts.
- Accounts requiring re-auth stay visible in the account selector as
disabled items with an expired badge instead of silently vanishing.
- Auth status refetches periodically so a revoked refresh token surfaces
without reloading the panel.
- All strings ship in zh/en/ja/zh-TW; a locale coverage test pins every
required key in all four locales.
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).
The menu display name and request model columns used mismatched example
brands (DeepSeek vs Kimi), implying a display name maps to an unrelated
request model. Derive both placeholders from the row role so each row
stays brand-consistent, and route the lightweight Haiku tier to a flash
example (deepseek-v4-flash) while other tiers use deepseek-v4-pro.
- Wire claude-fable-5 as a fourth tier on both proxy paths, with a
fable -> opus -> default fallback mirroring the official downgrade.
- Whitelist the fable- prefix for the Desktop 1.12603.1+ validator.
- Clarify fallbackModelHint (zh/en/ja/zh-TW): a blank tier on
third-party endpoints forwards the literal model name and 404s.
Refs #3980, #4026, #4049.
Claude Desktop's 3P validation only accepts claude-{sonnet,opus,haiku}-*
role IDs, so providers must map every tier. Bring the Desktop mapping flow
in line with Claude Code and fix the fallout that broke sub-agent Haiku calls.
- Proxy form: replace the dynamic route list with fixed Sonnet/Opus/Haiku
tiers; blank tiers backfill from the first filled tier (Sonnet first) on
submit and inherit its supports1m flag
- Backend: add a role-keyword fallback in map_proxy_request_model so dated
official names (e.g. claude-haiku-4-5-20251001) resolve to the right tier,
guarded by is_claude_safe_model_id so [1m]-suffixed IDs stay rejected
- Tighten is_claude_safe_model_id / isClaudeSafeRoute to reject degenerate
role IDs like "claude-sonnet-"
- Fix the seed-effect race where normalizing empty routes to three blank
tiers blocked the default-route backfill
- Sync switch hints, placeholders, and the zh/en/ja/zh-TW locales to the
three-role-ID rule
- Update zh/en/ja user manual (2.1, 2.6), calling out legacy Claude IDs
(claude-3-5-sonnet-...) as a rejected example
Tests: 282 frontend + 34 backend claude_desktop; typecheck, clippy, fmt clean.
- Add Claude Desktop Official to the Claude Desktop preset list.
- Treat selected official presets as official mode in the form.
- Cover the official preset with a preset-order regression test.
Remove the category-based grouping logic from ProviderPresetSelector,
letting the array position in each preset config file be the single
source of truth for display order. Move partner presets (PatewayAI,
火山Agentplan, BytePlus, DouBaoSeed) right after Shengsuanyun across
all 6 config files so they appear earlier in the UI.
Adapt to Claude Desktop 1.6259.1+ fail-all validation which only
accepts claude-(sonnet|opus|haiku)-* route IDs. Branded model names
(DeepSeek, Kimi, GLM, etc.) now live in a new labelOverride field
instead of being embedded in route IDs.
- Backend auto-repairs legacy unsafe routes to the next free
sonnet/opus/haiku slot instead of erroring
- Frontend swaps the free-form route input for a role dropdown plus
menu display name field
- Add CLAUDE_DESKTOP_ROLE_ROUTE_IDS as the single source of truth
for role-to-route mapping; presets and form both consume it
- Drop the dead displayName alias on ClaudeDesktopModelRoute and the
ineffective /v1/models display_name injection (UI ignores it)
- Update i18n (en/ja/zh) and form focus test for the new fields
- Derive route keys from the upstream model name (pass-through style)
instead of fixed Claude aliases, and translate the legacy [1M] suffix
into the supports1m field at the import boundary. Three Claude aliases
mapped to the same upstream now collapse to a single route (e.g.
MiniMax-M2 across SONNET/OPUS/HAIKU env produces one
claude-MiniMax-M2 -> MiniMax-M2 row), with [1M] OR-aggregated.
- Add an import-time safety net that rebuilds claude-desktop-official
when missing, so users who deleted it can recover via the normal
import button without losing customizations on other providers.
- Hide API key and endpoint URL inputs in the official provider edit
form to mirror Claude Code's behavior and prevent user confusion.
- Reword the empty-state import button label for clarity.
inferenceModels entries now emit {name, supports1m: true} objects when
1M is enabled (plain strings otherwise), instead of appending a " [1M]"
suffix to model IDs. Route IDs and upstream model IDs are stored
verbatim; the suffix is rejected on input rather than silently stripped,
and proxy request mapping now requires an exact route_id match.
Claude Desktop's new model menu reads model IDs directly and ignores the
display_name field, so a separate displayName slot added UI noise without
any product value. Collapse the routeId / model / displayName tuple down
to routeId / model, and let the route ID carry the user-visible name
through a non-editable claude- prefix rendered next to the input.
Drop display_name from ClaudeDesktopModelRoute, ClaudeDesktopDefaultRoute,
and ResolvedModelRoute on the Rust side plus the matching TS interfaces,
stop emitting it in /v1/models responses, derive route IDs from upstream
model IDs when picked via the model dropdown, and update zh/en/ja copy to
describe the new two-field layout.
- New src/config/claudeDesktopProviderPresets.ts with the Claude Code
preset list re-shaped into Desktop's three-segment route format
(routeId / upstreamModel / displayName); excludes OAuth providers,
AWS Bedrock (no SigV4 support) and KAT-Coder (placeholder URL).
- Non-Claude upstream presets show upstream model id as displayName
(e.g. deepseek-v4-pro) so the Desktop model list reflects what is
actually being requested. OpenRouter/TheRouter/PIPELLM keep
Sonnet/Opus/Haiku since their upstream really is Anthropic Claude.
- Wire ProviderPresetSelector into ClaudeDesktopProviderForm so
selecting a preset back-fills baseUrl, mode, routes and apiFormat.
- Drop the hard-coded ANTHROPIC_AUTH_TOKEN write in handleSubmit so
ANTHROPIC_API_KEY presets (LemonData / AiHubMix / Gemini Native)
save under the correct env key, and clear the opposite key on switch.
- Hide the universal-providers tab for claude-desktop because its
meta-driven routing has no analogue in the universal flat-env shape.
- Add apps."claude-desktop" i18n key (zh/en/ja) so the dialog tab
label resolves instead of showing the literal key.
- Remove "Import from Claude" button from main provider list (keep in empty state)
- Remove "Desktop model" column from proxy mode mapping table; route names are now auto-generated
- Rename "upstream model" label to "requested model" and equalize column widths
- Rewrite model mapping hint and toggle description for end-user clarity
- Update validation messages and remove dead i18n keys (routeModelLabel)
Adds a new ClaudeDesktop AppType that writes Claude Desktop's third-party
inference profile under configLibrary/, sharing _meta.json with other
launchers (Ollama-compatible) so cc-switch can coexist with them.
Two switch modes:
- direct: provider already exposes claude-* / anthropic/claude-* model
ids on Anthropic Messages, Claude Desktop connects to it directly.
- proxy: cc-switch's local proxy acts as the inference gateway,
presenting only claude-* route names to Claude Desktop and mapping
them to real upstream models. Required after Anthropic restricted
Claude Desktop to claude-family ids.
Backend:
- New module claude_desktop_config with snapshot/rollback, official seed
bypass, /claude-desktop/v1/{models,messages} routes, and a single
source of truth for default proxy routes.
- Gateway token persisted in SQLite, validated on every proxied request.
- get_claude_desktop_status surfaces drift signals (stale models,
missing routes, proxy stopped, base URL mismatch, missing token).
Frontend:
- Slim ClaudeDesktopProviderForm independent from ProviderForm,
controlled by a top-level appId guard.
- ProviderList banner consumes the status query (5s polling) and
renders actionable diagnostics.
- ClaudeDesktopRouteToggle in the header to start/stop the local
gateway without touching takeover state.
- Three-locale i18n synchronised.