mirror of
https://github.com/farion1231/cc-switch.git
synced 2026-07-24 12:44:18 +08:00
feat(grokbuild): curate standalone provider presets
Grok Build previously reused the whole Codex preset list, leaking cn_official providers and Codex default models into the Grok CLI form. Replace the inline filter with a standalone, independently maintained preset module (no data linkage to codexProviderPresets): - drop cn_official direct providers and open-source-only hosts (SiliconFlow/ModelScope/Novita/Nvidia/AtlasCloud/OpenCode Go) that have no Grok models upstream - keep aggregators and third-party relays with grok-4.5 as the default model (x-ai/grok-4.5 for OpenRouter-style namespaced routers) - move the Grok Official seed entry into the presets module - add standalone integrity tests; retarget the form's chat-mapping test since no openai_chat preset remains in the list
This commit is contained in:
@@ -33,9 +33,10 @@ import { BasicFormFields } from "./BasicFormFields";
|
||||
import { CodexFormFields } from "./CodexFormFields";
|
||||
import { ProviderPresetSelector } from "./ProviderPresetSelector";
|
||||
import {
|
||||
codexProviderPresets,
|
||||
type CodexProviderPreset,
|
||||
} from "@/config/codexProviderPresets";
|
||||
grokBuildOfficialPreset,
|
||||
grokBuildProviderPresets,
|
||||
type GrokBuildProviderPreset,
|
||||
} from "@/config/grokBuildProviderPresets";
|
||||
import {
|
||||
codexApiFormatFromWireApi,
|
||||
extractCodexBaseUrl,
|
||||
@@ -53,36 +54,17 @@ import { GROKBUILD_OFFICIAL_PROVIDER_ID } from "@/utils/providerCapabilities";
|
||||
|
||||
type GrokBuildProviderFormProps = Omit<ProviderFormProps, "appId">;
|
||||
|
||||
// 官方条目与后端 seed(providers_seed.rs 的 "Grok Official")对应:
|
||||
// 空 config = 不写自定义模型表,Grok CLI 回落到自带的 xAI OAuth 登录。
|
||||
// 预设 id 复用固定 provider id,AddProviderDialog 据此走 ensure seed 流程。
|
||||
const grokOfficialPreset: CodexProviderPreset = {
|
||||
name: "Grok Official",
|
||||
websiteUrl: "https://x.ai/grok",
|
||||
isOfficial: true,
|
||||
category: "official",
|
||||
auth: {},
|
||||
config: "",
|
||||
icon: "grok",
|
||||
iconColor: "currentColor",
|
||||
};
|
||||
|
||||
// 预设列表见 grokBuildProviderPresets.ts:独立维护(与 Codex 预设无联动),
|
||||
// 不含官方 / OAuth / 国产官方直连 / 纯开源托管站,默认模型为 Grok 系。
|
||||
const grokPresetEntries: Array<{
|
||||
id: string;
|
||||
preset: CodexProviderPreset;
|
||||
preset: GrokBuildProviderPreset;
|
||||
}> = [
|
||||
{ id: GROKBUILD_OFFICIAL_PROVIDER_ID, preset: grokOfficialPreset },
|
||||
...codexProviderPresets
|
||||
.map((preset, index) => ({ id: `grokbuild-${index}`, preset }))
|
||||
.filter(
|
||||
// 托管 OAuth 预设(如 "xAI (Grok) OAuth")只在 Codex 表单接线;
|
||||
// Grok CLI 原生支持订阅登录,这里选中只会产出无 key 的坏配置,故排除。
|
||||
({ preset }) =>
|
||||
preset.category !== "official" &&
|
||||
!preset.isOfficial &&
|
||||
!preset.providerType &&
|
||||
!preset.requiresOAuth,
|
||||
),
|
||||
{ id: GROKBUILD_OFFICIAL_PROVIDER_ID, preset: grokBuildOfficialPreset },
|
||||
...grokBuildProviderPresets.map((preset, index) => ({
|
||||
id: `grokbuild-${index}`,
|
||||
preset,
|
||||
})),
|
||||
];
|
||||
|
||||
export const grokApiBackendFromApiFormat = (format: CodexApiFormat): string => {
|
||||
@@ -205,12 +187,10 @@ export function GrokBuildProviderForm({
|
||||
onSubmittingChange?.(isSubmitting);
|
||||
}, [isSubmitting, onSubmittingChange]);
|
||||
|
||||
// Grok Build 预设已不含 cn_official(国产官方直连无法在 Grok CLI 使用)
|
||||
const presetCategoryLabels = useMemo(
|
||||
() => ({
|
||||
official: t("providerForm.categoryOfficial", { defaultValue: "官方" }),
|
||||
cn_official: t("providerForm.categoryCnOfficial", {
|
||||
defaultValue: "国内官方",
|
||||
}),
|
||||
aggregator: t("providerForm.categoryAggregation", {
|
||||
defaultValue: "聚合服务",
|
||||
}),
|
||||
@@ -261,10 +241,10 @@ export function GrokBuildProviderForm({
|
||||
|
||||
if (presetId === GROKBUILD_OFFICIAL_PROVIDER_ID) {
|
||||
// 官方登录:无 API Key / 地址 / 模型表可填,提交走 ensure seed 流程
|
||||
form.setValue("name", grokOfficialPreset.name);
|
||||
form.setValue("websiteUrl", grokOfficialPreset.websiteUrl);
|
||||
form.setValue("icon", grokOfficialPreset.icon ?? "");
|
||||
form.setValue("iconColor", grokOfficialPreset.iconColor ?? "");
|
||||
form.setValue("name", grokBuildOfficialPreset.name);
|
||||
form.setValue("websiteUrl", grokBuildOfficialPreset.websiteUrl);
|
||||
form.setValue("icon", grokBuildOfficialPreset.icon ?? "");
|
||||
form.setValue("iconColor", grokBuildOfficialPreset.iconColor ?? "");
|
||||
setCategory("official");
|
||||
setIsPartner(false);
|
||||
setPartnerPromotionKey(undefined);
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import {
|
||||
grokBuildOfficialPreset,
|
||||
grokBuildProviderPresets,
|
||||
} from "./grokBuildProviderPresets";
|
||||
import {
|
||||
extractCodexBaseUrl,
|
||||
extractCodexModelName,
|
||||
} from "../utils/providerConfigUtils";
|
||||
import { GROK_BUILD_DEFAULT_MODEL } from "../utils/grokBuildConfig";
|
||||
|
||||
describe("grokBuildProviderPresets", () => {
|
||||
it("has unique preset names", () => {
|
||||
const names = grokBuildProviderPresets.map((p) => p.name);
|
||||
expect(new Set(names).size).toBe(names.length);
|
||||
});
|
||||
|
||||
it("contains no official, managed-OAuth, or cn_official providers", () => {
|
||||
for (const preset of grokBuildProviderPresets) {
|
||||
expect(preset.category, preset.name).not.toBe("official");
|
||||
expect(preset.category, preset.name).not.toBe("cn_official");
|
||||
expect(preset.isOfficial, preset.name).toBeFalsy();
|
||||
}
|
||||
});
|
||||
|
||||
it("excludes providers with no Grok models upstream", () => {
|
||||
const names = new Set(grokBuildProviderPresets.map((p) => p.name));
|
||||
const excluded = [
|
||||
"OpenAI Official",
|
||||
"Azure OpenAI",
|
||||
"xAI (Grok) OAuth",
|
||||
"DeepSeek",
|
||||
"Kimi",
|
||||
"Kimi For Coding",
|
||||
"Zhipu GLM",
|
||||
"MiniMax",
|
||||
"SiliconFlow",
|
||||
"SiliconFlow en",
|
||||
"ModelScope",
|
||||
"Novita AI",
|
||||
"Nvidia",
|
||||
"AtlasCloud",
|
||||
"OpenCode Go",
|
||||
];
|
||||
for (const name of excluded) {
|
||||
expect(names.has(name), name).toBe(false);
|
||||
}
|
||||
});
|
||||
|
||||
it("uses a Grok default model on every preset", () => {
|
||||
for (const preset of grokBuildProviderPresets) {
|
||||
const model = extractCodexModelName(preset.config);
|
||||
expect(
|
||||
model === GROK_BUILD_DEFAULT_MODEL || model === "x-ai/grok-4.5",
|
||||
`${preset.name}: ${model}`,
|
||||
).toBe(true);
|
||||
}
|
||||
});
|
||||
|
||||
it("carries a valid config carrier and empty API key slot", () => {
|
||||
for (const preset of grokBuildProviderPresets) {
|
||||
expect(extractCodexBaseUrl(preset.config), preset.name).toMatch(
|
||||
/^https:\/\//,
|
||||
);
|
||||
expect(preset.auth, preset.name).toEqual({ OPENAI_API_KEY: "" });
|
||||
}
|
||||
});
|
||||
|
||||
it("keeps the official preset as an empty-config seed entry", () => {
|
||||
expect(grokBuildOfficialPreset.category).toBe("official");
|
||||
expect(grokBuildOfficialPreset.isOfficial).toBe(true);
|
||||
expect(grokBuildOfficialPreset.config).toBe("");
|
||||
expect(grokBuildOfficialPreset.auth).toEqual({});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,585 @@
|
||||
/**
|
||||
* Grok Build (Grok CLI) 预设供应商配置模板
|
||||
*
|
||||
* 独立维护,与 codexProviderPresets.ts 无数据联动(Jason 2026-07-21 定)。
|
||||
* 初始条目取自当时的 Codex 预设快照,此后两边各自演进:
|
||||
* 合作伙伴链接 / 图标 / endpoint 变更需要在本文件单独修改。
|
||||
*
|
||||
* 收录规则:
|
||||
* - 不含官方 / 托管 OAuth 预设:Grok CLI 自带 xAI 订阅登录,官方态走
|
||||
* 独立的 "Grok Official" 条目(对应 providers_seed.rs 的 seed,
|
||||
* 空 config = 不写自定义模型表)。
|
||||
* - 不含国产模型官方直连(cn_official)与纯开源模型托管站
|
||||
* (SiliconFlow / ModelScope / Novita / Nvidia / AtlasCloud / OpenCode Go):
|
||||
* 这些上游没有 Grok 模型,无法在 Grok CLI 中使用。
|
||||
* - 只收聚合站与第三方中转站,默认模型统一为 grok-4.5;
|
||||
* OpenRouter 系命名空间的路由站用 "x-ai/grok-4.5"。
|
||||
*
|
||||
* config 字段沿用 Codex 风格 TOML 作为载体:Grok 表单只从中提取
|
||||
* base_url / model / wire_api 三个字段(extractCodex* 工具),再重建
|
||||
* Grok CLI 自己的 config.toml。
|
||||
*/
|
||||
import type { ProviderCategory } from "../types";
|
||||
import type { CodexApiFormat } from "../types";
|
||||
import { GROK_BUILD_DEFAULT_MODEL } from "../utils/grokBuildConfig";
|
||||
|
||||
export interface GrokBuildProviderPreset {
|
||||
name: string;
|
||||
nameKey?: string; // i18n key for localized display name
|
||||
websiteUrl: string;
|
||||
apiKeyUrl?: string;
|
||||
auth: Record<string, any>;
|
||||
config: string; // Codex 风格 TOML 载体(只消费 base_url / model / wire_api)
|
||||
isOfficial?: boolean;
|
||||
isPartner?: boolean;
|
||||
partnerPromotionKey?: string;
|
||||
category?: ProviderCategory;
|
||||
endpointCandidates?: string[];
|
||||
icon?: string;
|
||||
iconColor?: string;
|
||||
apiFormat?: CodexApiFormat;
|
||||
}
|
||||
|
||||
// 官方条目与后端 seed(providers_seed.rs 的 "Grok Official")对应:
|
||||
// 空 config = 不写自定义模型表,Grok CLI 回落到自带的 xAI OAuth 登录。
|
||||
// 预设 id 复用固定 provider id,AddProviderDialog 据此走 ensure seed 流程。
|
||||
export const grokBuildOfficialPreset: GrokBuildProviderPreset = {
|
||||
name: "Grok Official",
|
||||
websiteUrl: "https://x.ai/grok",
|
||||
isOfficial: true,
|
||||
category: "official",
|
||||
auth: {},
|
||||
config: "",
|
||||
icon: "grok",
|
||||
iconColor: "currentColor",
|
||||
};
|
||||
|
||||
/** OpenRouter 系命名空间路由站的 Grok 模型 id */
|
||||
const OPENROUTER_STYLE_GROK_MODEL = "x-ai/grok-4.5";
|
||||
|
||||
const grokAuth = (): Record<string, any> => ({ OPENAI_API_KEY: "" });
|
||||
|
||||
function grokPresetConfig(
|
||||
providerName: string,
|
||||
baseUrl: string,
|
||||
model = GROK_BUILD_DEFAULT_MODEL,
|
||||
): string {
|
||||
const tomlString = (value: string) => JSON.stringify(value);
|
||||
|
||||
return `model_provider = "custom"
|
||||
model = ${tomlString(model)}
|
||||
|
||||
[model_providers.custom]
|
||||
name = ${tomlString(providerName)}
|
||||
base_url = ${tomlString(baseUrl)}
|
||||
wire_api = "responses"
|
||||
requires_openai_auth = true`;
|
||||
}
|
||||
|
||||
export const grokBuildProviderPresets: GrokBuildProviderPreset[] = [
|
||||
{
|
||||
name: "xAI (Grok)",
|
||||
websiteUrl: "https://x.ai/api",
|
||||
apiKeyUrl: "https://console.x.ai",
|
||||
auth: grokAuth(),
|
||||
config: grokPresetConfig("xAI (Grok)", "https://api.x.ai/v1"),
|
||||
endpointCandidates: ["https://api.x.ai/v1"],
|
||||
apiFormat: "openai_responses",
|
||||
category: "third_party",
|
||||
icon: "xai",
|
||||
iconColor: "#000000",
|
||||
},
|
||||
{
|
||||
name: "Shengsuanyun",
|
||||
nameKey: "providerForm.presets.shengsuanyun",
|
||||
websiteUrl: "https://www.shengsuanyun.com/?from=CH_4HHXMRYF",
|
||||
apiKeyUrl: "https://www.shengsuanyun.com/?from=CH_4HHXMRYF",
|
||||
auth: grokAuth(),
|
||||
config: grokPresetConfig(
|
||||
"Shengsuanyun",
|
||||
"https://router.shengsuanyun.com/api/v1",
|
||||
OPENROUTER_STYLE_GROK_MODEL,
|
||||
),
|
||||
category: "aggregator",
|
||||
isPartner: true,
|
||||
partnerPromotionKey: "shengsuanyun",
|
||||
icon: "shengsuanyun",
|
||||
},
|
||||
{
|
||||
name: "PatewayAI",
|
||||
websiteUrl: "https://pateway.ai",
|
||||
apiKeyUrl: "https://pateway.ai/?ch=etzpm8&aff=WB6M6F67#/",
|
||||
auth: grokAuth(),
|
||||
config: grokPresetConfig("PatewayAI", "https://api.pateway.ai/v1"),
|
||||
endpointCandidates: ["https://api.pateway.ai/v1"],
|
||||
category: "third_party",
|
||||
isPartner: true,
|
||||
partnerPromotionKey: "patewayai",
|
||||
icon: "pateway",
|
||||
},
|
||||
{
|
||||
name: "CCSub",
|
||||
websiteUrl: "https://www.ccsub.net",
|
||||
apiKeyUrl: "https://www.ccsub.net/register?ref=Y6Z8DXEA",
|
||||
auth: grokAuth(),
|
||||
config: grokPresetConfig("CCSub", "https://www.ccsub.net/v1"),
|
||||
endpointCandidates: ["https://www.ccsub.net/v1"],
|
||||
category: "aggregator",
|
||||
isPartner: true,
|
||||
partnerPromotionKey: "ccsub",
|
||||
icon: "ccsub",
|
||||
},
|
||||
{
|
||||
name: "SubRouter",
|
||||
websiteUrl: "https://subrouter.ai",
|
||||
apiKeyUrl: "https://subrouter.ai/register?aff=l3ri",
|
||||
auth: grokAuth(),
|
||||
config: grokPresetConfig("SubRouter", "https://subrouter.ai/v1"),
|
||||
endpointCandidates: ["https://subrouter.ai/v1"],
|
||||
category: "aggregator",
|
||||
isPartner: true,
|
||||
partnerPromotionKey: "subrouter",
|
||||
icon: "subrouter",
|
||||
},
|
||||
{
|
||||
name: "Unity2.ai",
|
||||
websiteUrl: "https://unity2.ai",
|
||||
apiKeyUrl: "https://unity2.ai/register?source=ccs",
|
||||
auth: grokAuth(),
|
||||
config: grokPresetConfig("Unity2.ai", "https://api.unity2.ai"),
|
||||
endpointCandidates: ["https://api.unity2.ai"],
|
||||
category: "aggregator",
|
||||
isPartner: true,
|
||||
partnerPromotionKey: "unity2",
|
||||
icon: "unity2",
|
||||
},
|
||||
{
|
||||
name: "Qiniu",
|
||||
nameKey: "providerForm.presets.qiniu",
|
||||
websiteUrl: "https://s.qiniu.com/nMvAvy",
|
||||
apiKeyUrl: "https://s.qiniu.com/nMvAvy",
|
||||
auth: grokAuth(),
|
||||
config: grokPresetConfig(
|
||||
"Qiniu",
|
||||
"https://api.qnaigc.com/bypass/openai/v1",
|
||||
),
|
||||
endpointCandidates: [
|
||||
"https://api.qnaigc.com/bypass/openai/v1",
|
||||
"https://api.modelink.ai/bypass/openai/v1",
|
||||
],
|
||||
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",
|
||||
auth: grokAuth(),
|
||||
config: grokPresetConfig("FennoAI", "https://api.fenno.ai"),
|
||||
endpointCandidates: ["https://api.fenno.ai"],
|
||||
category: "aggregator",
|
||||
isPartner: true,
|
||||
partnerPromotionKey: "fenno",
|
||||
icon: "fenno",
|
||||
},
|
||||
{
|
||||
name: "ZetaAPI",
|
||||
websiteUrl: "https://zetaapi.ai",
|
||||
apiKeyUrl: "https://zetaapi.ai/go/ccs",
|
||||
auth: grokAuth(),
|
||||
config: grokPresetConfig("ZetaAPI", "https://api.zetaapi.ai/v1"),
|
||||
endpointCandidates: ["https://api.zetaapi.ai/v1"],
|
||||
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",
|
||||
auth: grokAuth(),
|
||||
config: grokPresetConfig("TeamoRouter", "https://api.teamorouter.com/v1"),
|
||||
endpointCandidates: ["https://api.teamorouter.com/v1"],
|
||||
category: "aggregator",
|
||||
isPartner: true,
|
||||
partnerPromotionKey: "teamorouter",
|
||||
icon: "teamorouter",
|
||||
},
|
||||
{
|
||||
name: "Amux",
|
||||
websiteUrl: "https://amux.ai",
|
||||
apiKeyUrl: "https://amux.ai",
|
||||
auth: grokAuth(),
|
||||
config: grokPresetConfig("Amux", "https://api.amux.ai/v1"),
|
||||
endpointCandidates: ["https://api.amux.ai/v1"],
|
||||
category: "aggregator",
|
||||
icon: "amux",
|
||||
},
|
||||
{
|
||||
name: "Code0",
|
||||
websiteUrl: "https://code0.ai",
|
||||
apiKeyUrl: "https://code0.ai/agent/register/B2XHxGjGmRvqgznY",
|
||||
auth: grokAuth(),
|
||||
config: grokPresetConfig("Code0", "https://code0.ai/v1"),
|
||||
endpointCandidates: ["https://code0.ai/v1"],
|
||||
category: "aggregator",
|
||||
isPartner: true,
|
||||
partnerPromotionKey: "code0",
|
||||
icon: "code0",
|
||||
},
|
||||
{
|
||||
name: "NekoCode",
|
||||
websiteUrl: "https://nekocode.ai",
|
||||
apiKeyUrl: "https://nekocode.ai?aff=CCSWITCH",
|
||||
auth: grokAuth(),
|
||||
config: grokPresetConfig("NekoCode", "https://nekocode.ai/v1"),
|
||||
endpointCandidates: ["https://nekocode.ai/v1"],
|
||||
category: "aggregator",
|
||||
isPartner: true,
|
||||
partnerPromotionKey: "nekocode",
|
||||
icon: "nekocode",
|
||||
},
|
||||
{
|
||||
name: "AiHubMix",
|
||||
websiteUrl: "https://aihubmix.com",
|
||||
auth: grokAuth(),
|
||||
config: grokPresetConfig("AiHubMix", "https://aihubmix.com/v1"),
|
||||
endpointCandidates: [
|
||||
"https://aihubmix.com/v1",
|
||||
"https://api.aihubmix.com/v1",
|
||||
],
|
||||
category: "aggregator",
|
||||
icon: "aihubmix",
|
||||
iconColor: "#006FFB",
|
||||
},
|
||||
{
|
||||
name: "CherryIN",
|
||||
websiteUrl: "https://open.cherryin.ai",
|
||||
apiKeyUrl: "https://open.cherryin.ai/console/token",
|
||||
auth: grokAuth(),
|
||||
config: grokPresetConfig(
|
||||
"CherryIN",
|
||||
"https://open.cherryin.net/v1",
|
||||
OPENROUTER_STYLE_GROK_MODEL,
|
||||
),
|
||||
endpointCandidates: ["https://open.cherryin.net/v1"],
|
||||
category: "aggregator",
|
||||
icon: "cherryin",
|
||||
},
|
||||
{
|
||||
name: "DMXAPI",
|
||||
websiteUrl: "https://www.dmxapi.cn",
|
||||
auth: grokAuth(),
|
||||
config: grokPresetConfig("DMXAPI", "https://www.dmxapi.cn/v1"),
|
||||
endpointCandidates: ["https://www.dmxapi.cn/v1"],
|
||||
category: "aggregator",
|
||||
isPartner: true,
|
||||
partnerPromotionKey: "dmxapi",
|
||||
},
|
||||
{
|
||||
name: "PackyCode",
|
||||
websiteUrl: "https://www.packyapi.com",
|
||||
apiKeyUrl: "https://www.packyapi.com/register?aff=cc-switch",
|
||||
auth: grokAuth(),
|
||||
config: grokPresetConfig("PackyCode", "https://www.packyapi.com/v1"),
|
||||
endpointCandidates: [
|
||||
"https://www.packyapi.com/v1",
|
||||
"https://api-slb.packyapi.com/v1",
|
||||
],
|
||||
category: "third_party",
|
||||
isPartner: true,
|
||||
partnerPromotionKey: "packycode",
|
||||
icon: "packycode",
|
||||
},
|
||||
{
|
||||
name: "APIKEY.FUN",
|
||||
websiteUrl: "https://apikey.fun",
|
||||
apiKeyUrl: "https://apikey.fun/register?aff=CCSwitch",
|
||||
auth: grokAuth(),
|
||||
config: grokPresetConfig("APIKEY.FUN", "https://api.apikey.fun/v1"),
|
||||
endpointCandidates: [
|
||||
"https://api.apikey.fun/v1",
|
||||
"https://slb.apikey.fun/v1",
|
||||
],
|
||||
apiFormat: "openai_responses",
|
||||
category: "third_party",
|
||||
isPartner: true,
|
||||
partnerPromotionKey: "apikeyfun",
|
||||
icon: "apikeyfun",
|
||||
},
|
||||
{
|
||||
name: "APINebula",
|
||||
websiteUrl: "https://apinebula.com",
|
||||
apiKeyUrl: "https://apinebula.com/02rw5X",
|
||||
auth: grokAuth(),
|
||||
config: grokPresetConfig("APINebula", "https://apinebula.com/v1"),
|
||||
endpointCandidates: ["https://apinebula.com/v1"],
|
||||
apiFormat: "openai_responses",
|
||||
category: "third_party",
|
||||
isPartner: true,
|
||||
partnerPromotionKey: "apinebula",
|
||||
icon: "apinebula",
|
||||
},
|
||||
{
|
||||
name: "SudoCode.chat",
|
||||
websiteUrl: "https://sudocode.chat",
|
||||
apiKeyUrl:
|
||||
"https://sudocode.chat/register?utm_source=ccswitch&utm_medium=partner",
|
||||
auth: grokAuth(),
|
||||
config: grokPresetConfig("SudoCode.chat", "https://api.sudocode.chat/v1"),
|
||||
endpointCandidates: ["https://api.sudocode.chat/v1"],
|
||||
apiFormat: "openai_responses",
|
||||
category: "third_party",
|
||||
isPartner: true,
|
||||
partnerPromotionKey: "sudocode",
|
||||
icon: "sudocode",
|
||||
},
|
||||
{
|
||||
name: "SudoCode.us",
|
||||
websiteUrl: "https://sudocode.us",
|
||||
apiKeyUrl: "https://sudocode.us",
|
||||
auth: grokAuth(),
|
||||
config: grokPresetConfig("SudoCode.us", "https://sudocode.us/v1"),
|
||||
endpointCandidates: ["https://sudocode.us/v1", "https://sudocode.run/v1"],
|
||||
apiFormat: "openai_responses",
|
||||
category: "third_party",
|
||||
isPartner: true,
|
||||
icon: "sudocode-us",
|
||||
},
|
||||
{
|
||||
name: "ClaudeCN",
|
||||
websiteUrl: "https://claudecn.top",
|
||||
apiKeyUrl: "https://claudecn.top/register?aff=ccswitch",
|
||||
auth: grokAuth(),
|
||||
config: grokPresetConfig("ClaudeCN", "https://claudecn.top/v1"),
|
||||
category: "third_party",
|
||||
isPartner: true,
|
||||
partnerPromotionKey: "claudecn",
|
||||
icon: "claudecn",
|
||||
},
|
||||
{
|
||||
name: "RunAPI",
|
||||
websiteUrl: "https://runapi.co",
|
||||
apiKeyUrl: "https://runapi.co",
|
||||
auth: grokAuth(),
|
||||
config: grokPresetConfig("RunAPI", "https://runapi.co/v1"),
|
||||
category: "aggregator",
|
||||
isPartner: true,
|
||||
partnerPromotionKey: "runapi",
|
||||
icon: "runapi",
|
||||
},
|
||||
{
|
||||
name: "RelaxyCode",
|
||||
websiteUrl: "https://www.relaxycode.com",
|
||||
apiKeyUrl: "https://www.relaxycode.com/register",
|
||||
auth: grokAuth(),
|
||||
config: grokPresetConfig("RelaxyCode", "https://www.relaxycode.com/v1"),
|
||||
category: "third_party",
|
||||
icon: "relaxcode",
|
||||
},
|
||||
{
|
||||
name: "Cubence",
|
||||
websiteUrl: "https://cubence.com",
|
||||
apiKeyUrl: "https://cubence.com/signup?code=CCSWITCH&source=ccs",
|
||||
auth: grokAuth(),
|
||||
config: grokPresetConfig("Cubence", "https://api.cubence.com/v1"),
|
||||
endpointCandidates: [
|
||||
"https://api.cubence.com/v1",
|
||||
"https://api-cf.cubence.com/v1",
|
||||
"https://api-dmit.cubence.com/v1",
|
||||
"https://api-bwg.cubence.com/v1",
|
||||
],
|
||||
category: "third_party",
|
||||
isPartner: true,
|
||||
partnerPromotionKey: "cubence",
|
||||
icon: "cubence",
|
||||
iconColor: "#000000",
|
||||
},
|
||||
{
|
||||
name: "AIGoCode",
|
||||
websiteUrl: "https://aigocode.com",
|
||||
apiKeyUrl: "https://aigocode.com/invite/CC-SWITCH",
|
||||
auth: grokAuth(),
|
||||
config: grokPresetConfig("AIGoCode", "https://api.aigocode.com"),
|
||||
endpointCandidates: ["https://api.aigocode.com"],
|
||||
category: "third_party",
|
||||
isPartner: true,
|
||||
partnerPromotionKey: "aigocode",
|
||||
icon: "aigocode",
|
||||
iconColor: "#5B7FFF",
|
||||
},
|
||||
{
|
||||
name: "RightCode",
|
||||
websiteUrl: "https://www.right.codes",
|
||||
apiKeyUrl: "https://www.right.codes/register?aff=CCSWITCH",
|
||||
auth: grokAuth(),
|
||||
config: grokPresetConfig("RightCode", "https://right.codes/codex/v1"),
|
||||
category: "third_party",
|
||||
isPartner: true,
|
||||
partnerPromotionKey: "rightcode",
|
||||
icon: "rc",
|
||||
iconColor: "#E96B2C",
|
||||
},
|
||||
{
|
||||
name: "AICodeMirror",
|
||||
websiteUrl: "https://www.aicodemirror.com",
|
||||
apiKeyUrl: "https://www.aicodemirror.com/register?invitecode=9915W3",
|
||||
auth: grokAuth(),
|
||||
config: grokPresetConfig(
|
||||
"AICodeMirror",
|
||||
"https://api.aicodemirror.com/api/codex/backend-api/codex",
|
||||
),
|
||||
endpointCandidates: [
|
||||
"https://api.aicodemirror.com/api/codex/backend-api/codex",
|
||||
"https://api.claudecode.net.cn/api/codex/backend-api/codex",
|
||||
],
|
||||
isPartner: true,
|
||||
partnerPromotionKey: "aicodemirror",
|
||||
icon: "aicodemirror",
|
||||
iconColor: "#000000",
|
||||
},
|
||||
{
|
||||
name: "CrazyRouter",
|
||||
websiteUrl: "https://www.crazyrouter.com",
|
||||
apiKeyUrl: "https://www.crazyrouter.com/register?aff=OZcm&ref=cc-switch",
|
||||
auth: grokAuth(),
|
||||
config: grokPresetConfig("CrazyRouter", "https://cn.crazyrouter.com/v1"),
|
||||
endpointCandidates: ["https://cn.crazyrouter.com/v1"],
|
||||
isPartner: true,
|
||||
partnerPromotionKey: "crazyrouter",
|
||||
icon: "crazyrouter",
|
||||
iconColor: "#000000",
|
||||
},
|
||||
{
|
||||
name: "SSSAiCode",
|
||||
websiteUrl: "https://sssaicodeapi.com",
|
||||
apiKeyUrl: "https://sssaicodeapi.com/register?ref=DCP0SM",
|
||||
auth: grokAuth(),
|
||||
config: grokPresetConfig(
|
||||
"SSSAiCode",
|
||||
"https://node-hk.sssaicodeapi.com/api/v1",
|
||||
),
|
||||
endpointCandidates: [
|
||||
"https://node-hk.sssaicodeapi.com/api/v1",
|
||||
"https://node-hk.sssaiapi.com/api/v1",
|
||||
"https://node-cf.sssaicodeapi.com/api/v1",
|
||||
],
|
||||
category: "third_party",
|
||||
isPartner: true,
|
||||
partnerPromotionKey: "sssaicode",
|
||||
icon: "sssaicode",
|
||||
iconColor: "#000000",
|
||||
},
|
||||
{
|
||||
name: "Compshare",
|
||||
nameKey: "providerForm.presets.ucloud",
|
||||
websiteUrl: "https://www.compshare.cn",
|
||||
apiKeyUrl:
|
||||
"https://www.compshare.cn/coding-plan?ytag=GPU_YY_YX_git_cc-switch",
|
||||
auth: grokAuth(),
|
||||
config: grokPresetConfig("Compshare", "https://api.modelverse.cn/v1"),
|
||||
endpointCandidates: ["https://api.modelverse.cn/v1"],
|
||||
category: "aggregator",
|
||||
isPartner: true,
|
||||
partnerPromotionKey: "ucloud",
|
||||
icon: "ucloud",
|
||||
iconColor: "#000000",
|
||||
},
|
||||
{
|
||||
name: "Compshare Coding Plan",
|
||||
nameKey: "providerForm.presets.ucloudCoding",
|
||||
websiteUrl: "https://www.compshare.cn",
|
||||
apiKeyUrl:
|
||||
"https://www.compshare.cn/coding-plan?ytag=GPU_YY_YX_git_cc-switch",
|
||||
auth: grokAuth(),
|
||||
config: grokPresetConfig(
|
||||
"Compshare Coding Plan",
|
||||
"https://cp.compshare.cn/v1",
|
||||
),
|
||||
endpointCandidates: ["https://cp.compshare.cn/v1"],
|
||||
category: "aggregator",
|
||||
isPartner: true,
|
||||
partnerPromotionKey: "ucloud",
|
||||
icon: "ucloud",
|
||||
iconColor: "#000000",
|
||||
},
|
||||
{
|
||||
name: "Micu",
|
||||
websiteUrl: "https://www.micuapi.ai",
|
||||
apiKeyUrl: "https://www.micuapi.ai/register?aff=aOYQ",
|
||||
auth: grokAuth(),
|
||||
config: grokPresetConfig("Micu", "https://www.micuapi.ai/v1"),
|
||||
endpointCandidates: ["https://www.micuapi.ai/v1"],
|
||||
category: "third_party",
|
||||
isPartner: true,
|
||||
partnerPromotionKey: "micu",
|
||||
icon: "micu",
|
||||
iconColor: "#000000",
|
||||
},
|
||||
{
|
||||
name: "ETok.ai",
|
||||
websiteUrl: "https://etok.ai",
|
||||
apiKeyUrl: "https://etok.ai",
|
||||
auth: grokAuth(),
|
||||
config: grokPresetConfig("ETok.ai", "https://api.etok.ai/v1"),
|
||||
endpointCandidates: ["https://api.etok.ai/v1"],
|
||||
category: "third_party",
|
||||
isPartner: true,
|
||||
partnerPromotionKey: "etok",
|
||||
icon: "etok",
|
||||
iconColor: "#000000",
|
||||
},
|
||||
{
|
||||
name: "E-FlowCode",
|
||||
websiteUrl: "https://e-flowcode.cc",
|
||||
apiKeyUrl: "https://e-flowcode.cc",
|
||||
auth: grokAuth(),
|
||||
config: grokPresetConfig("E-FlowCode", "https://e-flowcode.cc/v1"),
|
||||
endpointCandidates: ["https://e-flowcode.cc/v1"],
|
||||
category: "third_party",
|
||||
icon: "eflowcode",
|
||||
iconColor: "#000000",
|
||||
},
|
||||
{
|
||||
name: "PIPELLM",
|
||||
websiteUrl: "https://code.pipellm.ai",
|
||||
apiKeyUrl: "https://code.pipellm.ai/login?ref=uvw650za",
|
||||
auth: grokAuth(),
|
||||
config: grokPresetConfig("PIPELLM", "https://cc-api.pipellm.ai/v1"),
|
||||
endpointCandidates: ["https://cc-api.pipellm.ai/v1"],
|
||||
category: "aggregator",
|
||||
icon: "pipellm",
|
||||
},
|
||||
{
|
||||
name: "OpenRouter",
|
||||
websiteUrl: "https://openrouter.ai",
|
||||
apiKeyUrl: "https://openrouter.ai/keys",
|
||||
auth: grokAuth(),
|
||||
config: grokPresetConfig(
|
||||
"OpenRouter",
|
||||
"https://openrouter.ai/api/v1",
|
||||
OPENROUTER_STYLE_GROK_MODEL,
|
||||
),
|
||||
category: "aggregator",
|
||||
icon: "openrouter",
|
||||
iconColor: "#6566F1",
|
||||
},
|
||||
{
|
||||
name: "TheRouter",
|
||||
websiteUrl: "https://therouter.ai",
|
||||
apiKeyUrl: "https://dashboard.therouter.ai",
|
||||
auth: grokAuth(),
|
||||
config: grokPresetConfig(
|
||||
"TheRouter",
|
||||
"https://api.therouter.ai/v1",
|
||||
OPENROUTER_STYLE_GROK_MODEL,
|
||||
),
|
||||
endpointCandidates: ["https://api.therouter.ai/v1"],
|
||||
category: "aggregator",
|
||||
},
|
||||
];
|
||||
@@ -2,7 +2,10 @@ import { fireEvent, render, screen } from "@testing-library/react";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import { parse as parseToml } from "smol-toml";
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import { GrokBuildProviderForm } from "@/components/providers/forms/GrokBuildProviderForm";
|
||||
import {
|
||||
GrokBuildProviderForm,
|
||||
grokApiBackendFromApiFormat,
|
||||
} from "@/components/providers/forms/GrokBuildProviderForm";
|
||||
|
||||
vi.mock("@/components/JsonEditor", () => ({
|
||||
default: ({
|
||||
@@ -21,7 +24,7 @@ vi.mock("@/components/JsonEditor", () => ({
|
||||
}));
|
||||
|
||||
describe("GrokBuildProviderForm", () => {
|
||||
it("offers Codex-compatible provider presets and applies one", async () => {
|
||||
it("offers curated Grok Build presets and applies one", async () => {
|
||||
const user = userEvent.setup();
|
||||
const { container } = render(
|
||||
<GrokBuildProviderForm
|
||||
@@ -31,6 +34,10 @@ describe("GrokBuildProviderForm", () => {
|
||||
/>,
|
||||
);
|
||||
|
||||
// 国产官方直连(cn_official)不在 Grok Build 预设列表里
|
||||
expect(screen.queryByRole("button", { name: /BytePlus/ })).toBeNull();
|
||||
expect(screen.queryByRole("button", { name: /Kimi/ })).toBeNull();
|
||||
|
||||
await user.click(screen.getByRole("button", { name: /PatewayAI/ }));
|
||||
|
||||
const baseUrlInput =
|
||||
@@ -85,7 +92,14 @@ describe("GrokBuildProviderForm", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("maps Chat Completions presets into Grok api_backend", async () => {
|
||||
it("maps preset API formats into Grok api_backend", async () => {
|
||||
// 预设列表已不含 Chat Completions 条目(国产官方直连被移除),
|
||||
// chat/messages 映射分支由纯函数覆盖
|
||||
expect(grokApiBackendFromApiFormat("openai_chat")).toBe("chat_completions");
|
||||
expect(grokApiBackendFromApiFormat("anthropic")).toBe("messages");
|
||||
expect(grokApiBackendFromApiFormat("openai_responses")).toBe("responses");
|
||||
|
||||
// 组件级接线用带显式 apiFormat 的 Responses 预设验证
|
||||
const user = userEvent.setup();
|
||||
const onSubmit = vi.fn();
|
||||
render(
|
||||
@@ -96,7 +110,7 @@ describe("GrokBuildProviderForm", () => {
|
||||
/>,
|
||||
);
|
||||
|
||||
await user.click(screen.getByRole("button", { name: /BytePlus/ }));
|
||||
await user.click(screen.getByRole("button", { name: /APIKEY\.FUN/ }));
|
||||
await user.type(screen.getByLabelText("API Key"), "secret-key");
|
||||
await user.click(screen.getByRole("button", { name: "Save" }));
|
||||
|
||||
@@ -104,10 +118,11 @@ describe("GrokBuildProviderForm", () => {
|
||||
const submitted = onSubmit.mock.calls[0][0];
|
||||
const settings = JSON.parse(submitted.settingsConfig);
|
||||
const config = parseToml(settings.config) as any;
|
||||
expect(submitted.meta.apiFormat).toBe("openai_chat");
|
||||
expect(config.model[config.models.default].api_backend).toBe(
|
||||
"chat_completions",
|
||||
);
|
||||
expect(submitted.meta.apiFormat).toBe("openai_responses");
|
||||
const selected = config.model[config.models.default];
|
||||
expect(selected.api_backend).toBe("responses");
|
||||
expect(selected.model).toBe("grok-4.5");
|
||||
expect(selected.base_url).toBe("https://api.apikey.fun/v1");
|
||||
});
|
||||
|
||||
it("renders localized validation feedback for malformed TOML", async () => {
|
||||
|
||||
Reference in New Issue
Block a user