diff --git a/open-sse/executors/default.ts b/open-sse/executors/default.ts index 491ea48e75..3dd2ac39af 100644 --- a/open-sse/executors/default.ts +++ b/open-sse/executors/default.ts @@ -201,6 +201,10 @@ export class DefaultExecutor extends BaseExecutor { const baseUrl = credentials?.providerSpecificData?.baseUrl || this.config.baseUrl; return buildMaritalkChatUrl(baseUrl); } + case "siliconflow": { + const baseUrl = credentials?.providerSpecificData?.baseUrl || this.config.baseUrl; + return normalizeOpenAIChatUrl(baseUrl); + } case "lm-studio": case "modal": case "reka": diff --git a/src/app/(dashboard)/dashboard/providers/[id]/page.tsx b/src/app/(dashboard)/dashboard/providers/[id]/page.tsx index 44b672c032..4271047502 100644 --- a/src/app/(dashboard)/dashboard/providers/[id]/page.tsx +++ b/src/app/(dashboard)/dashboard/providers/[id]/page.tsx @@ -788,6 +788,7 @@ interface AddApiKeyModalProps { isOpen: boolean; provider?: string; providerName?: string; + initialBaseUrl?: string; isCompatible?: boolean; isAnthropic?: boolean; isCcCompatible?: boolean; @@ -821,6 +822,11 @@ type CommandCodeAuthFlowState = { message?: string; }; +const SILICONFLOW_ENDPOINTS = [ + { id: "siliconflow", label: "Global", baseUrl: "https://api.siliconflow.com/v1" }, + { id: "siliconflow-cn", label: "China", baseUrl: "https://api.siliconflow.cn/v1" }, +] as const; + interface EditConnectionModalConnection { id?: string; name?: string; @@ -1361,6 +1367,8 @@ export default function ProviderDetailPage() { const [showOAuthModal, _setShowOAuthModal] = useState(false); const [reauthConnection, setReauthConnection] = useState(null); const [showAddApiKeyModal, setShowAddApiKeyModal] = useState(false); + const [showSiliconFlowEndpointModal, setShowSiliconFlowEndpointModal] = useState(false); + const [siliconFlowInitialBaseUrl, setSiliconFlowInitialBaseUrl] = useState(); const [showRiskNoticeModal, setShowRiskNoticeModal] = useState(false); const [commandCodeAuthState, setCommandCodeAuthState] = useState({ phase: "idle", @@ -1959,13 +1967,21 @@ export default function ProviderDetailPage() { setShowOAuthModal(false); }, [fetchConnections]); + const openApiKeyAddFlow = useCallback(() => { + if (providerId === "siliconflow") { + setShowSiliconFlowEndpointModal(true); + return; + } + setShowAddApiKeyModal(true); + }, [providerId]); + const openPrimaryAddFlow = useCallback(() => { if (isOAuth) { setShowOAuthModal(true); return; } - setShowAddApiKeyModal(true); - }, [isOAuth]); + openApiKeyAddFlow(); + }, [isOAuth, openApiKeyAddFlow]); const gateConnectionFlow = useCallback( (callback: () => void) => { @@ -2008,6 +2024,7 @@ export default function ProviderDetailPage() { const handleCloseAddApiKeyModal = useCallback(() => { clearCommandCodeAuthTimer(); + setSiliconFlowInitialBaseUrl(undefined); commandCodeAuthWindowRef.current?.close?.(); commandCodeAuthWindowRef.current = null; setCommandCodeAuthState({ @@ -2255,6 +2272,7 @@ export default function ProviderDetailPage() { const newConnection = connectionData?.connection; await fetchConnections(); setShowAddApiKeyModal(false); + setSiliconFlowInitialBaseUrl(undefined); // For Gemini: show progress dialog and sync models from endpoint if (providerId === "gemini" && newConnection?.id) { @@ -3915,11 +3933,7 @@ export default function ProviderDetailPage() {

- @@ -4143,11 +4157,7 @@ export default function ProviderDetailPage() { ) : ( connections.length === 0 && ( - ) @@ -4182,7 +4192,7 @@ export default function ProviderDetailPage() { @@ -4710,11 +4720,26 @@ export default function ProviderDetailPage() { }} /> ))} + {providerId === "siliconflow" && ( + { + setSiliconFlowInitialBaseUrl(baseUrl); + setShowSiliconFlowEndpointModal(false); + setShowAddApiKeyModal(true); + }} + onClose={() => { + setShowSiliconFlowEndpointModal(false); + setSiliconFlowInitialBaseUrl(undefined); + }} + /> + )} {!isUpstreamProxyProvider && ( = { "azure-ai": "https://example-resource.services.ai.azure.com/openai/v1", "bailian-coding-plan": "https://coding-intl.dashscope.aliyuncs.com/apps/anthropic/v1", "xiaomi-mimo": "https://token-plan-sgp.xiaomimimo.com/v1", + siliconflow: "https://api.siliconflow.com/v1", "searxng-search": "http://localhost:8888/search", petals: "https://chat.petals.dev/api/v1/generate", }; @@ -7385,6 +7412,8 @@ function getProviderBaseUrlPlaceholder(providerId?: string | null) { case "bailian-coding-plan": case "xiaomi-mimo": return getProviderBaseUrlDefault(providerId); + case "siliconflow": + return "https://api.siliconflow.cn/v1"; case "heroku": return "https://us.inference.heroku.com"; case "databricks": @@ -7488,10 +7517,60 @@ function extractCommandCodeCredentialInput(value: string): string { return trimmed; } +function SiliconFlowEndpointModal({ + isOpen, + onSelect, + onClose, +}: { + isOpen: boolean; + onSelect: (baseUrl: string) => void; + onClose: () => void; +}) { + const t = useTranslations("providers"); + + return ( + +
+

+ {providerText(t, "chooseSiliconFlowEndpoint", "Choose your SiliconFlow endpoint:")} +

+ {SILICONFLOW_ENDPOINTS.map((endpoint) => ( + + ))} +
+
+ ); +} + function AddApiKeyModal({ isOpen, provider, providerName, + initialBaseUrl, isCompatible, isAnthropic, isCcCompatible, @@ -7537,7 +7616,7 @@ function AddApiKeyModal({ name: "", apiKey: "", priority: 1, - baseUrl: defaultBaseUrl, + baseUrl: initialBaseUrl || defaultBaseUrl, cx: "", region: showsRegion ? defaultRegion : "", apiRegion: "international", @@ -7556,6 +7635,17 @@ function AddApiKeyModal({ const [saveError, setSaveError] = useState(null); const [showAdvanced, setShowAdvanced] = useState(false); const [copiedCommandCodeField, setCopiedCommandCodeField] = useState(null); + const wasOpenRef = useRef(false); + + useEffect(() => { + const wasOpen = wasOpenRef.current; + wasOpenRef.current = isOpen; + if (!isOpen || wasOpen) return; + setFormData((current) => ({ + ...current, + baseUrl: initialBaseUrl || defaultBaseUrl, + })); + }, [defaultBaseUrl, initialBaseUrl, isOpen]); const bulkSupported = supportsBulkApiKey(provider); const [mode, setMode] = useState<"single" | "bulk">("single"); @@ -7770,6 +7860,16 @@ function AddApiKeyModal({ setSaveError(null); try { + let providerSpecificData: Record | undefined; + if (usesBaseUrl) { + const checked = normalizeAndValidateHttpBaseUrl(formData.baseUrl, defaultBaseUrl); + if (checked.error) { + setSaveError(checked.error); + return; + } + providerSpecificData = { baseUrl: checked.value }; + } + const res = await fetch("/api/providers/bulk", { method: "POST", headers: { "Content-Type": "application/json" }, @@ -7777,6 +7877,7 @@ function AddApiKeyModal({ provider, entries: parsed.entries.map((e) => ({ name: e.name, apiKey: e.apiKey })), priority: formData.priority || 1, + providerSpecificData, validateKeys: bulkValidateKeys, }), }); diff --git a/tests/unit/executor-default-base.test.ts b/tests/unit/executor-default-base.test.ts index ceb4cd2464..fcbf29474e 100644 --- a/tests/unit/executor-default-base.test.ts +++ b/tests/unit/executor-default-base.test.ts @@ -184,6 +184,7 @@ test("DefaultExecutor.buildUrl normalizes configurable chat-openai-compat base U const maritalk = new DefaultExecutor("maritalk"); const snowflake = new DefaultExecutor("snowflake"); const gigachat = new DefaultExecutor("gigachat"); + const siliconflow = new DefaultExecutor("siliconflow"); assert.equal( bailian.buildUrl("qwen3-coder-plus", true, 0, { @@ -273,6 +274,16 @@ test("DefaultExecutor.buildUrl normalizes configurable chat-openai-compat base U }), "https://gigachat.devices.sberbank.ru/api/v1/chat/completions" ); + assert.equal( + siliconflow.buildUrl("deepseek-ai/DeepSeek-V3.2", true), + "https://api.siliconflow.com/v1/chat/completions" + ); + assert.equal( + siliconflow.buildUrl("deepseek-ai/DeepSeek-V3.2", true, 0, { + providerSpecificData: { baseUrl: "https://api.siliconflow.cn/v1" }, + }), + "https://api.siliconflow.cn/v1/chat/completions" + ); }); test("DefaultExecutor.buildUrl falls back to OpenAI config for unknown providers", () => {