diff --git a/src/components/providers/forms/GrokBuildProviderForm.tsx b/src/components/providers/forms/GrokBuildProviderForm.tsx index 02dab4072..45456e957 100644 --- a/src/components/providers/forms/GrokBuildProviderForm.tsx +++ b/src/components/providers/forms/GrokBuildProviderForm.tsx @@ -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; -// 官方条目与后端 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); diff --git a/src/config/grokBuildProviderPresets.test.ts b/src/config/grokBuildProviderPresets.test.ts new file mode 100644 index 000000000..93363030f --- /dev/null +++ b/src/config/grokBuildProviderPresets.test.ts @@ -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({}); + }); +}); diff --git a/src/config/grokBuildProviderPresets.ts b/src/config/grokBuildProviderPresets.ts new file mode 100644 index 000000000..2f1d73c81 --- /dev/null +++ b/src/config/grokBuildProviderPresets.ts @@ -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; + 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 => ({ 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", + }, +]; diff --git a/tests/components/GrokBuildProviderForm.test.tsx b/tests/components/GrokBuildProviderForm.test.tsx index 17df0540d..d83cb62dc 100644 --- a/tests/components/GrokBuildProviderForm.test.tsx +++ b/tests/components/GrokBuildProviderForm.test.tsx @@ -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( { />, ); + // 国产官方直连(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 () => {