mirror of
https://github.com/farion1231/cc-switch.git
synced 2026-10-26 01:34:51 +08:00
fix(claude-desktop): fetch models for managed OAuth providers
The Claude Desktop provider form hid the "Fetch models" button for GitHub Copilot, Codex OAuth and xAI OAuth providers because the fetch path only knew how to call the base URL with an API key, which managed OAuth providers do not have. Claude Code and Codex forms already fetch these lists through the account-scoped APIs. Show the button for managed OAuth providers as well and route the fetch to copilotGetModels / copilotGetModelsForAccount, fetchCodexOauthModels or fetchXaiOauthModels based on the provider type, prompting to log in first when the account is not authenticated. The fetched list is reset and in-flight results are discarded when the provider type or selected account changes, so models from one account never leak into another. Refs #8426
This commit is contained in:
1 parent
b570f137cc
commit
5d04c12d89
2 files changed
+201
-36
No files matched your search
@@ -48,10 +48,16 @@ import {
|
||||
type ClaudeDesktopRoleId,
|
||||
} from "@/config/claudeDesktopProviderPresets";
|
||||
import {
|
||||
fetchCodexOauthModels,
|
||||
fetchModelsForConfig,
|
||||
fetchXaiOauthModels,
|
||||
showFetchModelsError,
|
||||
type FetchedModel,
|
||||
} from "@/lib/api/model-fetch";
|
||||
import {
|
||||
copilotGetModels,
|
||||
copilotGetModelsForAccount,
|
||||
} from "@/lib/api/copilot";
|
||||
import {
|
||||
providersApi,
|
||||
type ClaudeDesktopDefaultRoute,
|
||||
@@ -540,7 +546,100 @@ export function ClaudeDesktopProviderForm({
|
||||
setProxyRoutes(normalizeProxyRows(defaultProxyRouteRows));
|
||||
}, [defaultProxyRouteRows, effectiveMode, proxyRoutes.length]);
|
||||
|
||||
// 托管 OAuth 供应商没有地址和 Key,模型列表走各自账号的接口(与 Claude Code 表单一致)。
|
||||
// 换账号或换供应商(含切回普通供应商)后旧列表作废,在途请求的结果也丢弃。
|
||||
const managedModelsRequestRef = useRef(0);
|
||||
useEffect(() => {
|
||||
managedModelsRequestRef.current += 1;
|
||||
setFetchedModels([]);
|
||||
setIsFetchingModels(false);
|
||||
}, [
|
||||
usesManagedOAuth,
|
||||
activeProviderType,
|
||||
selectedGitHubAccountId,
|
||||
selectedCodexAccountId,
|
||||
selectedXaiAccountId,
|
||||
]);
|
||||
|
||||
const handleFetchManagedModels = async () => {
|
||||
let request: Promise<FetchedModel[]>;
|
||||
if (activeProviderType === "github_copilot") {
|
||||
if (!isCopilotAuthenticated) {
|
||||
toast.error(
|
||||
t("copilot.loginRequired", {
|
||||
defaultValue: "请先登录 GitHub Copilot",
|
||||
}),
|
||||
);
|
||||
return;
|
||||
}
|
||||
request = (
|
||||
selectedGitHubAccountId
|
||||
? copilotGetModelsForAccount(selectedGitHubAccountId)
|
||||
: copilotGetModels()
|
||||
).then((models) =>
|
||||
models.map((m) => ({ id: m.id, ownedBy: m.vendor || null })),
|
||||
);
|
||||
} else if (activeProviderType === "codex_oauth") {
|
||||
if (!isCodexOauthAuthenticated) {
|
||||
toast.error(
|
||||
t("codexOauth.loginRequired", {
|
||||
defaultValue: "请先登录 ChatGPT 账号",
|
||||
}),
|
||||
);
|
||||
return;
|
||||
}
|
||||
request = fetchCodexOauthModels(selectedCodexAccountId);
|
||||
} else if (activeProviderType === "xai_oauth") {
|
||||
if (!isXaiOauthAuthenticated) {
|
||||
toast.error(
|
||||
t("xaiOauth.loginRequired", {
|
||||
defaultValue: "请先登录 xAI 账号",
|
||||
}),
|
||||
);
|
||||
return;
|
||||
}
|
||||
request = fetchXaiOauthModels(selectedXaiAccountId);
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
|
||||
const requestId = managedModelsRequestRef.current + 1;
|
||||
managedModelsRequestRef.current = requestId;
|
||||
setIsFetchingModels(true);
|
||||
try {
|
||||
const models = await request;
|
||||
if (managedModelsRequestRef.current !== requestId) return;
|
||||
setFetchedModels(models);
|
||||
toast.success(
|
||||
t("providerForm.fetchModelsSuccess", {
|
||||
count: models.length,
|
||||
defaultValue: `已获取 ${models.length} 个模型`,
|
||||
}),
|
||||
);
|
||||
} catch (error) {
|
||||
if (managedModelsRequestRef.current !== requestId) return;
|
||||
console.warn("[ClaudeDesktop] Failed to fetch managed models:", error);
|
||||
if (activeProviderType === "github_copilot") {
|
||||
toast.error(
|
||||
t("copilot.loadModelsFailed", {
|
||||
defaultValue: "加载 Copilot 模型列表失败",
|
||||
}),
|
||||
);
|
||||
} else {
|
||||
showFetchModelsError(error, t);
|
||||
}
|
||||
} finally {
|
||||
if (managedModelsRequestRef.current === requestId) {
|
||||
setIsFetchingModels(false);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleFetchModels = async () => {
|
||||
if (usesManagedOAuth) {
|
||||
await handleFetchManagedModels();
|
||||
return;
|
||||
}
|
||||
if (!baseUrl.trim() || !apiKey.trim()) {
|
||||
showFetchModelsError(null, t, {
|
||||
hasBaseUrl: Boolean(baseUrl.trim()),
|
||||
@@ -830,23 +929,21 @@ export function ClaudeDesktopProviderForm({
|
||||
|
||||
const renderActionButtons = (onAdd: () => void, addLabel: string) => (
|
||||
<div className="flex gap-1">
|
||||
{!usesManagedOAuth && (
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={handleFetchModels}
|
||||
disabled={isFetchingModels}
|
||||
className="h-7 gap-1"
|
||||
>
|
||||
{isFetchingModels ? (
|
||||
<Loader2 className="h-3.5 w-3.5 animate-spin" />
|
||||
) : (
|
||||
<Download className="h-3.5 w-3.5" />
|
||||
)}
|
||||
{t("providerForm.fetchModels", { defaultValue: "获取模型" })}
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={handleFetchModels}
|
||||
disabled={isFetchingModels}
|
||||
className="h-7 gap-1"
|
||||
>
|
||||
{isFetchingModels ? (
|
||||
<Loader2 className="h-3.5 w-3.5 animate-spin" />
|
||||
) : (
|
||||
<Download className="h-3.5 w-3.5" />
|
||||
)}
|
||||
{t("providerForm.fetchModels", { defaultValue: "获取模型" })}
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
@@ -1064,25 +1161,23 @@ export function ClaudeDesktopProviderForm({
|
||||
defaultValue: "模型映射",
|
||||
})}
|
||||
</Label>
|
||||
{!usesManagedOAuth && (
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={handleFetchModels}
|
||||
disabled={isFetchingModels}
|
||||
className="h-7 gap-1"
|
||||
>
|
||||
{isFetchingModels ? (
|
||||
<Loader2 className="h-3.5 w-3.5 animate-spin" />
|
||||
) : (
|
||||
<Download className="h-3.5 w-3.5" />
|
||||
)}
|
||||
{t("providerForm.fetchModels", {
|
||||
defaultValue: "获取模型",
|
||||
})}
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={handleFetchModels}
|
||||
disabled={isFetchingModels}
|
||||
className="h-7 gap-1"
|
||||
>
|
||||
{isFetchingModels ? (
|
||||
<Loader2 className="h-3.5 w-3.5 animate-spin" />
|
||||
) : (
|
||||
<Download className="h-3.5 w-3.5" />
|
||||
)}
|
||||
{t("providerForm.fetchModels", {
|
||||
defaultValue: "获取模型",
|
||||
})}
|
||||
</Button>
|
||||
</div>
|
||||
<p className="text-xs leading-relaxed text-fg-2">
|
||||
{t("claudeDesktop.routeMapHint", {
|
||||
|
||||
@@ -12,6 +12,17 @@ const authState = vi.hoisted(() => ({
|
||||
const toastMocks = vi.hoisted(() => ({
|
||||
error: vi.fn(),
|
||||
}));
|
||||
const modelFetchMocks = vi.hoisted(() => ({
|
||||
fetchCodexOauthModels: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("@/lib/api/model-fetch", async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import("@/lib/api/model-fetch")>();
|
||||
return {
|
||||
...actual,
|
||||
fetchCodexOauthModels: modelFetchMocks.fetchCodexOauthModels,
|
||||
};
|
||||
});
|
||||
|
||||
vi.mock("sonner", () => ({
|
||||
toast: {
|
||||
@@ -87,6 +98,65 @@ function renderForm(
|
||||
describe("ClaudeDesktopProviderForm", () => {
|
||||
beforeEach(() => {
|
||||
authState.codexReauthRequired = false;
|
||||
toastMocks.error.mockReset();
|
||||
modelFetchMocks.fetchCodexOauthModels.mockReset();
|
||||
});
|
||||
|
||||
it("托管 OAuth 供应商也能获取模型并在映射行里选择(#8426)", async () => {
|
||||
modelFetchMocks.fetchCodexOauthModels.mockResolvedValue([
|
||||
{ id: "gpt-5.6", ownedBy: "openai" },
|
||||
]);
|
||||
renderForm({
|
||||
name: "Codex OAuth Provider",
|
||||
category: "third_party",
|
||||
settingsConfig: { env: {} },
|
||||
meta: {
|
||||
providerType: "codex_oauth",
|
||||
authBinding: {
|
||||
source: "managed_account",
|
||||
authProvider: "codex_oauth",
|
||||
accountId: "acct-managed",
|
||||
},
|
||||
claudeDesktopMode: "proxy",
|
||||
claudeDesktopModelRoutes: {
|
||||
"claude-sonnet-5": { model: "upstream-model" },
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
fireEvent.click(screen.getByRole("button", { name: /获取模型/ }));
|
||||
|
||||
await waitFor(() =>
|
||||
expect(modelFetchMocks.fetchCodexOauthModels).toHaveBeenCalledWith(
|
||||
"acct-managed",
|
||||
),
|
||||
);
|
||||
await waitFor(() =>
|
||||
expect(
|
||||
screen.getAllByRole("button", { name: "Select model" }).length,
|
||||
).toBeGreaterThan(0),
|
||||
);
|
||||
});
|
||||
|
||||
it("未登录的 Copilot 获取模型时提示先登录", async () => {
|
||||
renderForm({
|
||||
name: "Copilot Provider",
|
||||
category: "third_party",
|
||||
settingsConfig: { env: {} },
|
||||
meta: {
|
||||
providerType: "github_copilot",
|
||||
claudeDesktopMode: "proxy",
|
||||
claudeDesktopModelRoutes: {
|
||||
"claude-sonnet-5": { model: "claude-sonnet-5" },
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
fireEvent.click(screen.getByRole("button", { name: /获取模型/ }));
|
||||
|
||||
await waitFor(() =>
|
||||
expect(toastMocks.error).toHaveBeenCalledWith("请先登录 GitHub Copilot"),
|
||||
);
|
||||
});
|
||||
|
||||
it.each(["github_copilot", "codex_oauth", "xai_oauth"])(
|
||||
|
||||
Reference in new issue
Block a user