feat(proxy): flag managed-OAuth providers as routing-required

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
This commit is contained in:
Jason
2026-07-21 10:04:55 +08:00
parent dbb5bd1537
commit 6428e993a3
12 changed files with 465 additions and 49 deletions
@@ -70,6 +70,7 @@ import {
} from "@/lib/api/providers";
import { resolveManagedAccountId } from "@/lib/authBinding";
import { useCopilotAuth, useCodexOauth, useXaiOauth } from "./hooks";
import { isOAuthProviderType } from "@/config/constants";
export type ClaudeDesktopProviderFormValues = ProviderFormData & {
presetId?: string;
@@ -258,9 +259,10 @@ export function ClaudeDesktopProviderForm({
showButtons = true,
}: ClaudeDesktopProviderFormProps) {
const { t } = useTranslation();
const initialMode = initialData?.meta?.claudeDesktopMode ?? "direct";
const initialMode = isOAuthProviderType(initialData?.meta?.providerType)
? "proxy"
: (initialData?.meta?.claudeDesktopMode ?? "direct");
const [mode, setMode] = useState<"direct" | "proxy">(initialMode);
const needsModelMapping = mode === "proxy";
const [apiFormat, setApiFormat] = useState<ClaudeApiFormat>(
initialData?.meta?.apiFormat ?? "anthropic",
);
@@ -397,9 +399,9 @@ export function ClaudeDesktopProviderForm({
activePreset?.category === "official";
const usesManagedOAuth =
activePreset?.requiresOAuth === true ||
activeProviderType === "github_copilot" ||
activeProviderType === "codex_oauth" ||
activeProviderType === "xai_oauth";
isOAuthProviderType(activeProviderType);
const effectiveMode: "direct" | "proxy" = usesManagedOAuth ? "proxy" : mode;
const needsModelMapping = effectiveMode === "proxy";
// API Key 获取/邀请链接(与 Claude Code 表单同款,见 ClaudeFormFields
const apiKeyLinkCategory = activePreset?.category ?? initialData?.category;
@@ -428,9 +430,13 @@ export function ClaudeDesktopProviderForm({
setApiKeyField(preset.apiKeyField ?? "ANTHROPIC_AUTH_TOKEN");
setApiFormat(preset.apiFormat ?? "anthropic");
const presetMode =
preset.requiresOAuth === true || isOAuthProviderType(preset.providerType)
? "proxy"
: preset.mode;
didSeedDefaultRoutes.current = true;
setMode(preset.mode);
if (preset.mode === "proxy" && preset.modelRoutes) {
setMode(presetMode);
if (presetMode === "proxy" && preset.modelRoutes) {
setRoutes(
normalizeProxyRows(
preset.modelRoutes.map((r) =>
@@ -485,6 +491,7 @@ export function ClaudeDesktopProviderForm({
};
const handleModelMappingChange = (checked: boolean) => {
if (usesManagedOAuth) return;
setMode(checked ? "proxy" : "direct");
if (checked) {
// 切到 proxy:归一化成固定 Sonnet / Opus / Haiku 三档;
@@ -511,7 +518,7 @@ export function ClaudeDesktopProviderForm({
useEffect(() => {
if (
didSeedDefaultRoutes.current ||
mode !== "proxy" ||
effectiveMode !== "proxy" ||
routes.length > 0 ||
defaultProxyRouteRows.length === 0
) {
@@ -520,7 +527,7 @@ export function ClaudeDesktopProviderForm({
didSeedDefaultRoutes.current = true;
setRoutes(normalizeProxyRows(defaultProxyRouteRows));
}, [defaultProxyRouteRows, mode, routes.length]);
}, [defaultProxyRouteRows, effectiveMode, routes.length]);
const handleFetchModels = async () => {
if (!baseUrl.trim() || !apiKey.trim()) {
@@ -665,7 +672,6 @@ export function ClaudeDesktopProviderForm({
}))
.filter((route) => route.route || route.model);
const effectiveMode = activeProviderType === "xai_oauth" ? "proxy" : mode;
if (effectiveMode === "proxy") {
// 固定四档(Sonnet / Opus / Fable / Haiku),route_id 由 UI 生成、恒合法,
// 因此只要求至少填一个实际请求模型;留空档继承第一个已填档(Sonnet 优先),
@@ -925,7 +931,7 @@ export function ClaudeDesktopProviderForm({
<Switch
checked={needsModelMapping}
onCheckedChange={handleModelMappingChange}
disabled={activeProviderType === "xai_oauth"}
disabled={usesManagedOAuth}
aria-label={t("claudeDesktop.modelMappingToggle", {
defaultValue: "需要模型映射",
})}