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
+16 -35
View File
@@ -21,11 +21,11 @@ import { FailoverPriorityBadge } from "@/components/providers/FailoverPriorityBa
import {
extractCodexBaseUrl,
extractCodexExperimentalBearerToken,
extractCodexWireApi,
isCodexAnthropicWireApi,
isCodexChatWireApi,
} from "@/utils/providerConfigUtils";
import { supportsOfficialProxyTakeover } from "@/utils/providerCapabilities";
import {
supportsOfficialProxyTakeover,
providerNeedsRouting,
} from "@/utils/providerCapabilities";
import { useProviderHealth } from "@/lib/query/failover";
import { useUsageQuery } from "@/lib/query/queries";
import { resolveProviderIcon } from "@/utils/providerIcon";
@@ -227,25 +227,10 @@ export function ProviderCard({
appId === "hermes" && isHermesReadOnlyProvider(provider.settingsConfig);
const isCodexOauth =
provider.meta?.providerType === PROVIDER_TYPES.CODEX_OAUTH;
const codexNeedsRouting = useMemo(() => {
if (appId !== "codex" || provider.category === "official") return false;
if (
provider.meta?.apiFormat === "openai_chat" ||
provider.meta?.apiFormat === "anthropic"
)
return true;
const config = (provider.settingsConfig as Record<string, any>)?.config;
return (
typeof config === "string" &&
(isCodexChatWireApi(extractCodexWireApi(config)) ||
isCodexAnthropicWireApi(extractCodexWireApi(config)))
);
}, [
appId,
provider.category,
provider.meta?.apiFormat,
(provider.settingsConfig as Record<string, any>)?.config,
]);
// 统一权威谓词(详见 providerNeedsRouting):以 providerType 为准,不受
// apiFormat 被改动/缺省影响。此 badge 仅在 Codex 视图渲染,故加 appId 守卫。
const codexNeedsRouting =
appId === "codex" && providerNeedsRouting(appId, provider);
// 获取用量数据以判断是否有多套餐
// 累加模式应用(OpenCode/OpenClaw/Hermes):使用 isInConfig 代替 isCurrent
const shouldAutoQuery =
@@ -381,8 +366,7 @@ export function ProviderCard({
)}
{appId === "claude-desktop" &&
provider.category !== "official" &&
provider.meta?.claudeDesktopMode === "proxy" && (
providerNeedsRouting(appId, provider) && (
<span className="inline-flex items-center rounded-md bg-sky-100 px-1.5 py-0.5 text-[10px] font-semibold text-sky-700 dark:bg-sky-900/40 dark:text-sky-300">
{t("claudeDesktop.modeProxy", {
defaultValue: "需要路由",
@@ -390,16 +374,13 @@ export function ProviderCard({
</span>
)}
{appId === "claude" &&
provider.category !== "official" &&
provider.meta?.apiFormat &&
provider.meta.apiFormat !== "anthropic" && (
<span className="inline-flex items-center rounded-md bg-sky-100 px-1.5 py-0.5 text-[10px] font-semibold text-sky-700 dark:bg-sky-900/40 dark:text-sky-300">
{t("claudeCode.needsRouting", {
defaultValue: "需要路由",
})}
</span>
)}
{appId === "claude" && providerNeedsRouting(appId, provider) && (
<span className="inline-flex items-center rounded-md bg-sky-100 px-1.5 py-0.5 text-[10px] font-semibold text-sky-700 dark:bg-sky-900/40 dark:text-sky-300">
{t("claudeCode.needsRouting", {
defaultValue: "需要路由",
})}
</span>
)}
{codexNeedsRouting && (
<span className="inline-flex items-center rounded-md bg-sky-100 px-1.5 py-0.5 text-[10px] font-semibold text-sky-700 dark:bg-sky-900/40 dark:text-sky-300">