- {modelTypes.map((type) => (
-
- {mapModelKey[type.trim() as keyof typeof mapModelKey] || type}
-
- ))}
+
+
+ {modelTypes.map((type) => (
+
+ {mapModelKey[type.trim() as keyof typeof mapModelKey] || type}
+
+ ))}
+
+ {false && (
+
+
+
+ )}
)}
diff --git a/web/src/pages/user-setting/setting-model/hooks.tsx b/web/src/pages/user-setting/setting-model/hooks.tsx
index 07dfc1737a..c0d5713ca7 100644
--- a/web/src/pages/user-setting/setting-model/hooks.tsx
+++ b/web/src/pages/user-setting/setting-model/hooks.tsx
@@ -1,4 +1,3 @@
-import { LLMFactory } from '@/constants/llm';
import { useSetModalState } from '@/hooks/common-hooks';
import {
useAddInstanceModel,
@@ -7,25 +6,22 @@ import {
useFetchProviderInstances,
useVerifyProviderConnection,
} from '@/hooks/use-llm-request';
-import { IAddProviderInstanceRequestBody } from '@/interfaces/request/llm';
-import { getRealModelName } from '@/utils/llm-util';
+import {
+ IAddProviderInstanceRequestBody,
+ IModelInfo,
+} from '@/interfaces/request/llm';
import { useCallback, useMemo, useState } from 'react';
-import { ApiKeyPostBody } from '../interface';
-import { MinerUFormValues } from './modal/mineru-modal';
import { splitProviderPayload } from './payload-utils';
-type SavingParamsState = {
- llm_factory: string;
- llm_name?: string;
- model_type?: string;
- instance_name?: string;
- base_url?: string;
-};
export type VerifyResult = {
isValid: boolean | null;
logs: string;
};
+/**
+ * Unified Provider instance submission hook
+ * Internally handles both verify and save modes
+ */
const useSubmitProviderInstance = () => {
const { addProviderInstance } = useAddProviderInstance();
const { addInstanceModel } = useAddInstanceModel();
@@ -36,6 +32,13 @@ const useSubmitProviderInstance = () => {
return addProviderInstance({ ...payload, verify: true });
}
+ // Multi-model flow: when model_info is provided as an array, the
+ // backend is expected to create the instance and all listed models
+ // in a single addProviderInstance call. Skip the instance/model split.
+ if (Array.isArray((payload as any).model_info)) {
+ return addProviderInstance(payload as IAddProviderInstanceRequestBody);
+ }
+
const { instancePayload, modelPayload } = splitProviderPayload(payload);
const hasModelPayload =
!!modelPayload.model_name && !!modelPayload.model_type;
@@ -88,6 +91,7 @@ export const useHideWhenInstanceExists = (instanceNameSet: Set
) => {
[instanceNameSet],
);
};
+
export const useVerifyConnection = () => {
const { verifyProviderConnection } = useVerifyProviderConnection();
@@ -97,12 +101,14 @@ export const useVerifyConnection = () => {
apiKey: string,
baseUrl?: string,
region?: string,
+ modelInfo?: IModelInfo[],
) => {
const ret = await verifyProviderConnection({
provider_name: providerName,
api_key: apiKey,
base_url: baseUrl,
region: region,
+ model_info: modelInfo,
});
if (ret.code === 0) {
@@ -121,494 +127,13 @@ export const useVerifyConnection = () => {
);
};
-export const useSubmitApiKey = () => {
- const [savingParams, setSavingParams] = useState(
- {} as SavingParamsState,
- );
- const [editMode, setEditMode] = useState(false);
- const submitProviderInstance = useSubmitProviderInstance();
- const verifyConnection = useVerifyConnection();
- const [saveLoading, setSaveLoading] = useState(false);
- const {
- visible: apiKeyVisible,
- hideModal: hideApiKeyModal,
- showModal: showApiKeyModal,
- } = useSetModalState();
-
- const onApiKeySavingOk = useCallback(
- async (postBody: ApiKeyPostBody, isVerify = false) => {
- if (!isVerify) {
- setSaveLoading(true);
- }
- const apiKey: string = postBody.api_key || '';
-
- let region: string | undefined;
- if (savingParams.llm_factory === LLMFactory.SILICONFLOW) {
- const baseUrl = postBody.base_url;
- if (baseUrl) {
- try {
- const parsed = new URL(baseUrl);
- const host = parsed.hostname.toLowerCase();
- if (
- host === 'api.siliconflow.com' ||
- host.endsWith('.api.siliconflow.com')
- ) {
- region = 'intl';
- }
- } catch {
- // ignore invalid URL
- }
- }
- }
-
- // Use dedicated verify API for verification
- if (isVerify) {
- const res = await verifyConnection(
- savingParams.llm_factory,
- postBody.api_key,
- postBody.base_url,
- region,
- );
- return res;
- }
-
- const req: IAddProviderInstanceRequestBody = {
- instance_name:
- postBody.instance_name || savingParams.instance_name || '',
- llm_factory: savingParams.llm_factory,
- llm_name: savingParams.llm_name || '',
- model_type: savingParams.model_type || '',
- api_key: apiKey,
- api_base: postBody.base_url || '',
- max_tokens: 0,
- ...(region ? { region } : {}),
- };
-
- const ret = await submitProviderInstance(req, isVerify);
- if (!isVerify) {
- setSaveLoading(false);
- if (ret.code === 0) {
- hideApiKeyModal();
- setEditMode(false);
- }
- }
- },
- [hideApiKeyModal, submitProviderInstance, savingParams, verifyConnection],
- );
-
- const onShowApiKeyModal = useCallback(
- (savingParams: SavingParamsState, isEdit = false) => {
- setSavingParams(savingParams);
- setEditMode(isEdit);
- showApiKeyModal();
- },
- [showApiKeyModal, setSavingParams],
- );
-
- return {
- saveApiKeyLoading: saveLoading,
- initialApiKey: '',
- llmFactory: savingParams.llm_factory,
- editMode,
- onApiKeySavingOk,
- apiKeyVisible,
- hideApiKeyModal,
- showApiKeyModal: onShowApiKeyModal,
- };
-};
-
-export const useSubmitOllama = () => {
- const [selectedLlmFactory, setSelectedLlmFactory] = useState('');
- const [editMode, setEditMode] = useState(false);
- const [initialValues, setInitialValues] = useState<
- Partial & { provider_order?: string }
- >();
- const [saveLoading, setSaveLoading] = useState(false);
- const submitProviderInstance = useSubmitProviderInstance();
- const {
- visible: llmAddingVisible,
- hideModal: hideLlmAddingModal,
- showModal: showLlmAddingModal,
- } = useSetModalState();
-
- const onLlmAddingOk = useCallback(
- async (payload: IAddProviderInstanceRequestBody, isVerify = false) => {
- if (!isVerify) {
- setSaveLoading(true);
- }
- const cleanedPayload = { ...payload };
- // if (
- // !cleanedPayload.api_key ||
- // (typeof cleanedPayload.api_key === 'string' &&
- // cleanedPayload.api_key.trim() === '')
- // ) {
- // delete cleanedPayload.api_key;
- // }
-
- const ret = await submitProviderInstance(cleanedPayload, isVerify);
- if (!isVerify) {
- setSaveLoading(false);
- if (ret.code === 0) {
- hideLlmAddingModal();
- setEditMode(false);
- setInitialValues(undefined);
- }
- }
- if (isVerify) {
- let res = {} as VerifyResult;
- if (ret.data?.success) {
- res = {
- isValid: true,
- logs: ret.data?.message,
- };
- } else {
- res = {
- isValid: false,
- logs: ret.data?.message,
- };
- }
- return res;
- }
- },
- [hideLlmAddingModal, submitProviderInstance, setSaveLoading],
- );
-
- const handleShowLlmAddingModal = (
- llmFactory: string,
- isEdit = false,
- modelData?: any,
- detailedData?: any,
- ) => {
- setSelectedLlmFactory(llmFactory);
- setEditMode(isEdit);
-
- if (isEdit && detailedData) {
- const initialVals = {
- instance_name:
- detailedData.instance_name || getRealModelName(detailedData.name),
- llm_name: getRealModelName(detailedData.name),
- model_type: detailedData.type,
- api_base: detailedData.api_base || '',
- max_tokens: detailedData.max_tokens || 8192,
- api_key: '',
- is_tools: detailedData.is_tools || false,
- };
- setInitialValues(initialVals);
- } else {
- setInitialValues(undefined);
- }
- showLlmAddingModal();
- };
-
- return {
- llmAddingLoading: saveLoading,
- editMode,
- initialValues,
- onLlmAddingOk,
- llmAddingVisible,
- hideLlmAddingModal,
- showLlmAddingModal: handleShowLlmAddingModal,
- selectedLlmFactory,
- };
-};
-
-export const useSubmitVolcEngine = () => {
- const [saveLoading, setSaveLoading] = useState(false);
- const submitProviderInstance = useSubmitProviderInstance();
- const {
- visible: volcAddingVisible,
- hideModal: hideVolcAddingModal,
- showModal: showVolcAddingModal,
- } = useSetModalState();
-
- const onVolcAddingOk = useCallback(
- async (payload: IAddProviderInstanceRequestBody, isVerify = false) => {
- if (!isVerify) {
- setSaveLoading(true);
- }
- const ret = await submitProviderInstance(payload, isVerify);
- if (!isVerify) {
- setSaveLoading(false);
- if (ret.code === 0) {
- hideVolcAddingModal();
- }
- }
- if (isVerify) {
- let res = {} as VerifyResult;
- if (ret.data?.success) {
- res = {
- isValid: true,
- logs: ret.data?.message,
- };
- } else {
- res = {
- isValid: false,
- logs: ret.data?.message,
- };
- }
- return res;
- }
- },
- [hideVolcAddingModal, submitProviderInstance, setSaveLoading],
- );
-
- return {
- volcAddingLoading: saveLoading,
- onVolcAddingOk,
- volcAddingVisible,
- hideVolcAddingModal,
- showVolcAddingModal,
- };
-};
-
-export const useSubmitTencentCloud = () => {
- const [saveLoading, setSaveLoading] = useState(false);
- const submitProviderInstance = useSubmitProviderInstance();
- const {
- visible: TencentCloudAddingVisible,
- hideModal: hideTencentCloudAddingModal,
- showModal: showTencentCloudAddingModal,
- } = useSetModalState();
-
- const onTencentCloudAddingOk = useCallback(
- async (payload: IAddProviderInstanceRequestBody, isVerify = false) => {
- if (!isVerify) {
- setSaveLoading(true);
- }
- const ret = await submitProviderInstance(payload, isVerify);
- if (!isVerify) {
- setSaveLoading(false);
- if (ret.code === 0) {
- hideTencentCloudAddingModal();
- }
- }
- if (isVerify) {
- let res = {} as VerifyResult;
- if (ret.data?.success) {
- res = {
- isValid: true,
- logs: ret.data?.message,
- };
- } else {
- res = {
- isValid: false,
- logs: ret.data?.message,
- };
- }
- return res;
- }
- },
- [hideTencentCloudAddingModal, submitProviderInstance, setSaveLoading],
- );
-
- return {
- TencentCloudAddingLoading: saveLoading,
- onTencentCloudAddingOk,
- TencentCloudAddingVisible,
- hideTencentCloudAddingModal,
- showTencentCloudAddingModal,
- };
-};
-
-export const useSubmitSpark = () => {
- const [saveLoading, setSaveLoading] = useState(false);
- const submitProviderInstance = useSubmitProviderInstance();
- const {
- visible: SparkAddingVisible,
- hideModal: hideSparkAddingModal,
- showModal: showSparkAddingModal,
- } = useSetModalState();
-
- const onSparkAddingOk = useCallback(
- async (payload: IAddProviderInstanceRequestBody, isVerify = false) => {
- if (!isVerify) {
- setSaveLoading(true);
- }
- const ret = await submitProviderInstance(payload, isVerify);
- if (!isVerify) {
- setSaveLoading(false);
- if (ret.code === 0) {
- hideSparkAddingModal();
- }
- }
- if (isVerify) {
- let res = {} as VerifyResult;
- if (ret.data?.success) {
- res = {
- isValid: true,
- logs: ret.data?.message,
- };
- } else {
- res = {
- isValid: false,
- logs: ret.data?.message,
- };
- }
- return res;
- }
- },
- [hideSparkAddingModal, submitProviderInstance, setSaveLoading],
- );
-
- return {
- SparkAddingLoading: saveLoading,
- onSparkAddingOk,
- SparkAddingVisible,
- hideSparkAddingModal,
- showSparkAddingModal,
- };
-};
-
-export const useSubmityiyan = () => {
- const [saveLoading, setSaveLoading] = useState(false);
- const submitProviderInstance = useSubmitProviderInstance();
- const {
- visible: yiyanAddingVisible,
- hideModal: hideyiyanAddingModal,
- showModal: showyiyanAddingModal,
- } = useSetModalState();
-
- const onyiyanAddingOk = useCallback(
- async (payload: IAddProviderInstanceRequestBody, isVerify = false) => {
- if (!isVerify) {
- setSaveLoading(true);
- }
- const ret = await submitProviderInstance(payload, isVerify);
- if (!isVerify) {
- setSaveLoading(false);
- if (ret.code === 0) {
- hideyiyanAddingModal();
- }
- }
- if (isVerify) {
- let res = {} as VerifyResult;
- if (ret.data?.success) {
- res = {
- isValid: true,
- logs: ret.data?.message,
- };
- } else {
- res = {
- isValid: false,
- logs: ret.data?.message,
- };
- }
- return res;
- }
- },
- [hideyiyanAddingModal, submitProviderInstance, setSaveLoading],
- );
-
- return {
- yiyanAddingLoading: saveLoading,
- onyiyanAddingOk,
- yiyanAddingVisible,
- hideyiyanAddingModal,
- showyiyanAddingModal,
- };
-};
-
-export const useSubmitFishAudio = () => {
- const [saveLoading, setSaveLoading] = useState(false);
- const submitProviderInstance = useSubmitProviderInstance();
- const {
- visible: FishAudioAddingVisible,
- hideModal: hideFishAudioAddingModal,
- showModal: showFishAudioAddingModal,
- } = useSetModalState();
-
- const onFishAudioAddingOk = useCallback(
- async (payload: IAddProviderInstanceRequestBody, isVerify = false) => {
- if (!isVerify) {
- setSaveLoading(true);
- }
- const ret = await submitProviderInstance(payload, isVerify);
- if (!isVerify) {
- setSaveLoading(false);
- if (ret.code === 0) {
- hideFishAudioAddingModal();
- }
- }
- if (isVerify) {
- let res = {} as VerifyResult;
- if (ret.data?.success) {
- res = {
- isValid: true,
- logs: ret.data?.message,
- };
- } else {
- res = {
- isValid: false,
- logs: ret.data?.message,
- };
- }
- return res;
- }
- },
- [hideFishAudioAddingModal, submitProviderInstance, setSaveLoading],
- );
-
- return {
- FishAudioAddingLoading: saveLoading,
- onFishAudioAddingOk,
- FishAudioAddingVisible,
- hideFishAudioAddingModal,
- showFishAudioAddingModal,
- };
-};
-
-export const useSubmitGoogle = () => {
- const [saveLoading, setSaveLoading] = useState(false);
- const submitProviderInstance = useSubmitProviderInstance();
- const {
- visible: GoogleAddingVisible,
- hideModal: hideGoogleAddingModal,
- showModal: showGoogleAddingModal,
- } = useSetModalState();
-
- const onGoogleAddingOk = useCallback(
- async (payload: IAddProviderInstanceRequestBody, isVerify = false) => {
- if (!isVerify) {
- setSaveLoading(true);
- }
- const ret = await submitProviderInstance(payload, isVerify);
- if (!isVerify) {
- setSaveLoading(false);
- if (ret.code === 0) {
- hideGoogleAddingModal();
- }
- }
- if (isVerify) {
- let res = {} as VerifyResult;
- if (ret.data?.success) {
- res = {
- isValid: true,
- logs: ret.data?.message,
- };
- } else {
- res = {
- isValid: false,
- logs: ret.data?.message,
- };
- }
- return res;
- }
- },
- [hideGoogleAddingModal, submitProviderInstance, setSaveLoading],
- );
-
- return {
- GoogleAddingLoading: saveLoading,
- onGoogleAddingOk,
- GoogleAddingVisible,
- hideGoogleAddingModal,
- showGoogleAddingModal,
- };
-};
+// ============ Hooks for the 4 retained special modals ============
+// Bedrock / MinerU / PaddleOCR / OpenDataLoader are not yet merged into ProviderModal
export const useSubmitBedrock = () => {
const [saveLoading, setSaveLoading] = useState(false);
const submitProviderInstance = useSubmitProviderInstance();
+ const verifyConnection = useVerifyConnection();
const {
visible: bedrockAddingVisible,
hideModal: hideBedrockAddingModal,
@@ -620,30 +145,45 @@ export const useSubmitBedrock = () => {
if (!isVerify) {
setSaveLoading(true);
}
- const ret = await submitProviderInstance(payload, isVerify);
- if (!isVerify) {
- setSaveLoading(false);
- if (ret.code === 0) {
- hideBedrockAddingModal();
- }
- }
if (isVerify) {
- let res = {} as VerifyResult;
- if (ret.data?.success) {
- res = {
- isValid: true,
- logs: ret.data?.message,
- };
- } else {
- res = {
- isValid: false,
- logs: ret.data?.message,
- };
- }
- return res;
+ const legacyPayload = payload as any;
+ const modelType = Array.isArray(legacyPayload.model_type)
+ ? (legacyPayload.model_type as string[])
+ : legacyPayload.model_type
+ ? [legacyPayload.model_type as string]
+ : [];
+ const apiKey = JSON.stringify({
+ auth_mode: legacyPayload.auth_mode,
+ bedrock_ak: legacyPayload.bedrock_ak,
+ bedrock_sk: legacyPayload.bedrock_sk,
+ aws_role_arn: legacyPayload.aws_role_arn,
+ });
+ return verifyConnection(
+ payload.llm_factory as string,
+ apiKey,
+ legacyPayload.bedrock_region,
+ undefined,
+ [
+ {
+ model_type: modelType,
+ model_name: (legacyPayload.model_name as string) ?? '',
+ max_tokens: (legacyPayload.max_tokens as number) ?? 0,
+ },
+ ],
+ );
+ }
+ const ret = await submitProviderInstance(payload, false);
+ setSaveLoading(false);
+ if (ret.code === 0) {
+ hideBedrockAddingModal();
}
},
- [hideBedrockAddingModal, submitProviderInstance, setSaveLoading],
+ [
+ hideBedrockAddingModal,
+ submitProviderInstance,
+ setSaveLoading,
+ verifyConnection,
+ ],
);
return {
@@ -655,260 +195,13 @@ export const useSubmitBedrock = () => {
};
};
-export const useSubmitAzure = () => {
- const [saveLoading, setSaveLoading] = useState(false);
- const submitProviderInstance = useSubmitProviderInstance();
- const {
- visible: AzureAddingVisible,
- hideModal: hideAzureAddingModal,
- showModal: showAzureAddingModal,
- } = useSetModalState();
-
- const onAzureAddingOk = useCallback(
- async (payload: IAddProviderInstanceRequestBody, isVerify = false) => {
- if (!isVerify) {
- setSaveLoading(true);
- }
- const ret = await submitProviderInstance(payload, isVerify);
- if (!isVerify) {
- setSaveLoading(false);
- if (ret.code === 0) {
- hideAzureAddingModal();
- }
- }
- if (isVerify) {
- let res = {} as VerifyResult;
- if (ret.data?.success) {
- res = {
- isValid: true,
- logs: ret.data?.message,
- };
- } else {
- res = {
- isValid: false,
- logs: ret.data?.message,
- };
- }
- return res;
- }
- },
- [hideAzureAddingModal, submitProviderInstance, setSaveLoading],
- );
-
- return {
- AzureAddingLoading: saveLoading,
- onAzureAddingOk,
- AzureAddingVisible,
- hideAzureAddingModal,
- showAzureAddingModal,
- };
-};
-
-export const useSubmitMinerU = () => {
- const [saveLoading, setSaveLoading] = useState(false);
- const submitProviderInstance = useSubmitProviderInstance();
- const {
- visible: mineruVisible,
- hideModal: hideMineruModal,
- showModal: showMineruModal,
- } = useSetModalState();
-
- const onMineruOk = useCallback(
- async (
- payload: MinerUFormValues & { instance_name: string },
- isVerify = false,
- ) => {
- if (!isVerify) {
- setSaveLoading(true);
- }
- const cfg: any = {
- ...payload,
- mineru_delete_output:
- (payload.mineru_delete_output ?? true) ? '1' : '0',
- };
- delete cfg.instance_name;
- if (payload.mineru_backend !== 'vlm-http-client') {
- delete cfg.mineru_server_url;
- }
- const req: IAddProviderInstanceRequestBody = {
- instance_name: payload.instance_name,
- llm_factory: LLMFactory.MinerU,
- llm_name: payload.llm_name,
- model_type: 'ocr',
- api_key: cfg,
- api_base: '',
- max_tokens: 0,
- };
- const ret = await submitProviderInstance(req, isVerify);
- if (!isVerify) {
- setSaveLoading(false);
- if (ret.code === 0) {
- hideMineruModal();
- }
- }
- if (isVerify) {
- let res = {} as VerifyResult;
- if (ret.data?.success) {
- res = {
- isValid: true,
- logs: ret.data?.message,
- };
- } else {
- res = {
- isValid: false,
- logs: ret.data?.message,
- };
- }
- return res;
- }
- },
- [submitProviderInstance, hideMineruModal, setSaveLoading],
- );
-
- return {
- mineruVisible,
- hideMineruModal,
- showMineruModal,
- onMineruOk,
- mineruLoading: saveLoading,
- };
-};
-
-export const useSubmitPaddleOCR = () => {
- const [saveLoading, setSaveLoading] = useState(false);
- const submitProviderInstance = useSubmitProviderInstance();
- const {
- visible: paddleocrVisible,
- hideModal: hidePaddleOCRModal,
- showModal: showPaddleOCRModal,
- } = useSetModalState();
-
- const onPaddleOCROk = useCallback(
- async (payload: any, isVerify = false) => {
- if (!isVerify) {
- setSaveLoading(true);
- }
- const cfg: any = {
- ...payload,
- };
- delete cfg.instance_name;
- const req: IAddProviderInstanceRequestBody = {
- instance_name: payload.instance_name,
- llm_factory: LLMFactory.PaddleOCR,
- llm_name: payload.llm_name,
- model_type: 'ocr',
- api_key: cfg,
- api_base: '',
- max_tokens: 0,
- };
- const ret = await submitProviderInstance(req, isVerify);
- if (!isVerify) {
- setSaveLoading(false);
- if (ret.code === 0) {
- hidePaddleOCRModal();
- return true;
- }
- }
- if (isVerify) {
- let res = {} as VerifyResult;
- if (ret.data?.success) {
- res = {
- isValid: true,
- logs: ret.data?.message,
- };
- } else {
- res = {
- isValid: false,
- logs: ret.data?.message,
- };
- }
- return res;
- }
- return false;
- },
- [submitProviderInstance, hidePaddleOCRModal, setSaveLoading],
- );
-
- return {
- paddleocrVisible,
- hidePaddleOCRModal,
- showPaddleOCRModal,
- onPaddleOCROk,
- paddleocrLoading: saveLoading,
- };
-};
-
-export const useSubmitOpenDataLoader = () => {
- const [saveLoading, setSaveLoading] = useState(false);
- const submitProviderInstance = useSubmitProviderInstance();
- const {
- visible: opendataloaderVisible,
- hideModal: hideOpenDataLoaderModal,
- showModal: showOpenDataLoaderModal,
- } = useSetModalState();
-
- const onOpenDataLoaderOk = useCallback(
- async (payload: any, isVerify = false) => {
- if (!isVerify) {
- setSaveLoading(true);
- }
- const cfg: any = { ...payload };
- delete cfg.instance_name;
- const req: IAddProviderInstanceRequestBody = {
- instance_name: payload.instance_name,
- llm_factory: LLMFactory.OpenDataLoader,
- llm_name: payload.llm_name,
- model_type: 'ocr',
- api_key: cfg,
- api_base: '',
- max_tokens: 0,
- };
- const ret = await submitProviderInstance(req, isVerify);
- if (!isVerify) {
- setSaveLoading(false);
- if (ret.code === 0) {
- hideOpenDataLoaderModal();
- return true;
- }
- }
- if (isVerify) {
- return {
- isValid: !!ret.data?.success,
- logs: ret.data?.message,
- } as VerifyResult;
- }
- return false;
- },
- [submitProviderInstance, hideOpenDataLoaderModal, setSaveLoading],
- );
-
- return {
- opendataloaderVisible,
- hideOpenDataLoaderModal,
- showOpenDataLoaderModal,
- onOpenDataLoaderOk,
- opendataloaderLoading: saveLoading,
- };
-};
-
+/**
+ * Wraps the verify callback: provides a unified call with isVerify=true for the Verify button
+ */
export const useVerifySettings = ({
onVerify,
}: {
- onVerify:
- | ((
- postBody: ApiKeyPostBody,
- isVerify?: boolean,
- ) => Promise)
- | ((
- payload: IAddProviderInstanceRequestBody,
- isVerify?: boolean,
- ) => Promise)
- | ((
- payload: MinerUFormValues,
- isVerify?: boolean,
- ) => Promise)
- | ((payload: any, isVerify?: boolean) => Promise)
- | (() => void);
+ onVerify: (postBody: any, isVerify?: boolean) => Promise;
}) => {
const onApiKeyVerifying = useCallback(
async (postBody: any) => {
diff --git a/web/src/pages/user-setting/setting-model/index.tsx b/web/src/pages/user-setting/setting-model/index.tsx
index f32e8fd26c..581fcd6d78 100644
--- a/web/src/pages/user-setting/setting-model/index.tsx
+++ b/web/src/pages/user-setting/setting-model/index.tsx
@@ -1,111 +1,28 @@
import Spotlight from '@/components/spotlight';
import { LLMFactory } from '@/constants/llm';
-// import { LlmItem, useFetchMyLlmListDetailed } from '@/hooks/use-llm-request';
-import { useCallback, useMemo } from 'react';
+import {
+ useAddInstanceModel,
+ useAddProviderInstance,
+ useFetchAvailableProviders,
+ useVerifyProviderConnection,
+} from '@/hooks/use-llm-request';
+import { IInstanceModel, IProviderInstance } from '@/interfaces/database/llm';
+import {
+ IAddProviderInstanceRequestBody,
+ IModelInfo,
+} from '@/interfaces/request/llm';
+import { useCallback, useMemo, useState } from 'react';
import { isLocalLlmFactory } from '../utils';
import SystemSetting from './components/system-setting';
import { AvailableModels } from './components/un-add-model';
import { UsedModel } from './components/used-model';
-import {
- useSubmitApiKey,
- useSubmitAzure,
- useSubmitBedrock,
- useSubmitFishAudio,
- useSubmitGoogle,
- useSubmitMinerU,
- useSubmitOllama,
- useSubmitOpenDataLoader,
- useSubmitPaddleOCR,
- useSubmitSpark,
- useSubmitTencentCloud,
- useSubmitVolcEngine,
- useSubmityiyan,
- useVerifySettings,
-} from './hooks';
-import ApiKeyModal from './modal/api-key-modal';
-import AzureOpenAIModal from './modal/azure-openai-modal';
+import { useSubmitBedrock } from './hooks';
import BedrockModal from './modal/bedrock-modal';
-import FishAudioModal from './modal/fish-audio-modal';
-import GoogleModal from './modal/google-modal';
-import MinerUModal from './modal/mineru-modal';
-import TencentCloudModal from './modal/next-tencent-modal';
-import OllamaModal from './modal/ollama-modal';
-import OpenDataLoaderModal from './modal/opendataloader-modal';
-import PaddleOCRModal from './modal/paddleocr-modal';
-import SparkModal from './modal/spark-modal';
-import VolcEngineModal from './modal/volcengine-modal';
-import YiyanModal from './modal/yiyan-modal';
+import ProviderModal, { IViewModeOkPayload } from './modal/provider-modal';
+import { splitProviderPayload } from './payload-utils';
+
const ModelProviders = () => {
- // const { data: detailedLlmList } = useFetchMyLlmListDetailed();
- const {
- saveApiKeyLoading,
- initialApiKey,
- llmFactory,
- editMode,
- onApiKeySavingOk,
- apiKeyVisible,
- hideApiKeyModal,
- showApiKeyModal,
- } = useSubmitApiKey();
- const {
- llmAddingVisible,
- hideLlmAddingModal,
- showLlmAddingModal,
- onLlmAddingOk,
- llmAddingLoading,
- editMode: llmEditMode,
- initialValues: llmInitialValues,
- selectedLlmFactory,
- } = useSubmitOllama();
-
- const {
- volcAddingVisible,
- hideVolcAddingModal,
- showVolcAddingModal,
- onVolcAddingOk,
- volcAddingLoading,
- } = useSubmitVolcEngine();
-
- const {
- GoogleAddingVisible,
- hideGoogleAddingModal,
- showGoogleAddingModal,
- onGoogleAddingOk,
- GoogleAddingLoading,
- } = useSubmitGoogle();
-
- const {
- TencentCloudAddingVisible,
- hideTencentCloudAddingModal,
- showTencentCloudAddingModal,
- onTencentCloudAddingOk,
- TencentCloudAddingLoading,
- } = useSubmitTencentCloud();
-
- const {
- SparkAddingVisible,
- hideSparkAddingModal,
- showSparkAddingModal,
- onSparkAddingOk,
- SparkAddingLoading,
- } = useSubmitSpark();
-
- const {
- yiyanAddingVisible,
- hideyiyanAddingModal,
- showyiyanAddingModal,
- onyiyanAddingOk,
- yiyanAddingLoading,
- } = useSubmityiyan();
-
- const {
- FishAudioAddingVisible,
- hideFishAudioAddingModal,
- showFishAudioAddingModal,
- onFishAudioAddingOk,
- FishAudioAddingLoading,
- } = useSubmitFishAudio();
-
+ // 4 retained special modals
const {
bedrockAddingLoading,
onBedrockAddingOk,
@@ -114,299 +31,354 @@ const ModelProviders = () => {
showBedrockAddingModal,
} = useSubmitBedrock();
- const {
- AzureAddingVisible,
- hideAzureAddingModal,
- showAzureAddingModal,
- onAzureAddingOk,
- AzureAddingLoading,
- } = useSubmitAzure();
+ // Unified ProviderModal state
+ const [providerVisible, setProviderVisible] = useState(false);
+ const [currentLlmFactory, setCurrentLlmFactory] = useState('');
+ const [providerLoading, setProviderLoading] = useState(false);
- const {
- mineruVisible,
- hideMineruModal,
- showMineruModal,
- onMineruOk,
- mineruLoading,
- } = useSubmitMinerU();
+ // viewMode (edit-models) state: when true, ProviderModal opens in
+ // read-only mode for everything except the model-related fields.
+ // `viewModeInitialValues` carries the existing instance + model data.
+ const [viewMode, setViewMode] = useState(false);
+ const [viewModeInitialValues, setViewModeInitialValues] = useState<
+ Record | undefined
+ >(undefined);
- const {
- paddleocrVisible,
- hidePaddleOCRModal,
- showPaddleOCRModal,
- onPaddleOCROk,
- paddleocrLoading,
- } = useSubmitPaddleOCR();
+ // ProviderModal submission logic: calls addProviderInstance + addInstanceModel
+ const { addProviderInstance } = useAddProviderInstance();
+ const { addInstanceModel } = useAddInstanceModel();
+ const { verifyProviderConnection } = useVerifyProviderConnection();
+ const { data: availableProviders } = useFetchAvailableProviders();
- const {
- opendataloaderVisible,
- hideOpenDataLoaderModal,
- showOpenDataLoaderModal,
- onOpenDataLoaderOk,
- opendataloaderLoading,
- } = useSubmitOpenDataLoader();
+ // Convert IAvailableProvider.url to baseUrlOptions
+ // IAvailableProvider.url looks like { default?: string; cn?: string; intl?: string; ... }
+ // Mapped to [{ value: 'https://...', regionKey: 'default', label: https://...default }, ...]
+ // `regionKey` carries the original key so the modal can map the currently
+ // selected URL back to its key for the `region` submit field.
+ const buildBaseUrlOptions = useCallback(
+ (urlObj?: Record) => {
+ if (!urlObj) return undefined;
+ const options = Object.keys(urlObj)
+ .filter((k) => !!urlObj[k])
+ .map((k) => {
+ const v = urlObj[k] as string;
+ // if (k === 'default') {
+ // return { value: v, label: v };
+ // }
+ return {
+ value: v,
+ regionKey: k,
+ label: (
+
+ {v}
+
+ {k}
+
+
+ ),
+ };
+ });
+ return options.length > 0 ? options : undefined;
+ },
+ [],
+ );
+
+ // baseUrlOptions for the current factory (looked up from availableProviders)
+ const currentProvider = useMemo(
+ () =>
+ currentLlmFactory
+ ? availableProviders.find((p) => p.name === currentLlmFactory)
+ : undefined,
+ [availableProviders, currentLlmFactory],
+ );
+ const currentBaseUrlOptions = useMemo(
+ () => buildBaseUrlOptions(currentProvider?.url),
+ [buildBaseUrlOptions, currentProvider],
+ );
+
+ const handleProviderOk = useCallback(
+ async (payload: IAddProviderInstanceRequestBody, isVerify = false) => {
+ if (!isVerify) setProviderLoading(true);
+ try {
+ if (isVerify) {
+ // Verify mode: call verify API
+ const ret = await addProviderInstance({ ...payload, verify: true });
+ return ret;
+ }
+ // Normal submission
+ const { instancePayload, modelPayload } = splitProviderPayload(payload);
+ const hasModelPayload =
+ !!modelPayload.model_name && !!modelPayload.model_type;
+ const instanceRet = await addProviderInstance({
+ ...instancePayload,
+ llm_factory: payload.llm_factory,
+ instance_name: payload.instance_name,
+ } as IAddProviderInstanceRequestBody);
+ if (instanceRet.code !== 0) {
+ return instanceRet;
+ }
+ // When model information has been submitted nested in the instance via model_info
+ // (e.g., VolcEngine / LocalLLM), addInstanceModel is no longer called separately;
+ // close the modal directly.
+ if (!hasModelPayload) {
+ setProviderVisible(false);
+ return instanceRet;
+ }
+ const modelRet = await addInstanceModel({
+ provider_name: payload.llm_factory,
+ instance_name: payload.instance_name,
+ ...modelPayload,
+ });
+ if (modelRet.code === 0) {
+ setProviderVisible(false);
+ }
+ return modelRet;
+ } finally {
+ if (!isVerify) setProviderLoading(false);
+ }
+ },
+ [addProviderInstance, addInstanceModel],
+ );
+
+ const handleProviderVerify = useCallback(
+ async (params: any) => {
+ // ProviderModal's handleVerify flattens verifyArgs onto params
+ // verifyArgs comes from config.verifyTransform, fields are apiKey/baseUrl/region/modelInfo
+ const apiKey = params.apiKey ?? params.api_key ?? params._apiKey ?? '';
+ const baseUrl = params.baseUrl ?? params.base_url ?? params._baseUrl;
+ const region = params.region ?? params._region;
+ const modelInfo =
+ params.modelInfo ?? params.model_info ?? params._modelInfo;
+ const ret = await verifyProviderConnection({
+ provider_name: params.llm_factory ?? currentLlmFactory,
+ api_key: apiKey,
+ base_url: baseUrl,
+ region: region,
+ model_info: modelInfo,
+ });
+ if (ret.code === 0) {
+ return { isValid: true, logs: ret.message };
+ }
+ return { isValid: false, logs: ret.message };
+ },
+ [verifyProviderConnection, currentLlmFactory],
+ );
const ModalMap = useMemo(
() => ({
[LLMFactory.Bedrock]: showBedrockAddingModal,
- [LLMFactory.VolcEngine]: showVolcAddingModal,
- [LLMFactory.XunFeiSpark]: showSparkAddingModal,
- [LLMFactory.BaiduYiYan]: showyiyanAddingModal,
- [LLMFactory.FishAudio]: showFishAudioAddingModal,
- [LLMFactory.TencentCloud]: showTencentCloudAddingModal,
- [LLMFactory.GoogleCloud]: showGoogleAddingModal,
- [LLMFactory.AzureOpenAI]: showAzureAddingModal,
- [LLMFactory.MinerU]: showMineruModal,
- [LLMFactory.PaddleOCR]: showPaddleOCRModal,
- [LLMFactory.OpenDataLoader]: showOpenDataLoaderModal,
+ [LLMFactory.VolcEngine]: () => {
+ setCurrentLlmFactory(LLMFactory.VolcEngine);
+ setProviderVisible(true);
+ },
+ [LLMFactory.XunFeiSpark]: () => {
+ setCurrentLlmFactory(LLMFactory.XunFeiSpark);
+ setProviderVisible(true);
+ },
+ [LLMFactory.BaiduYiYan]: () => {
+ setCurrentLlmFactory(LLMFactory.BaiduYiYan);
+ setProviderVisible(true);
+ },
+ [LLMFactory.FishAudio]: () => {
+ setCurrentLlmFactory(LLMFactory.FishAudio);
+ setProviderVisible(true);
+ },
+ [LLMFactory.TencentCloud]: () => {
+ setCurrentLlmFactory(LLMFactory.TencentCloud);
+ setProviderVisible(true);
+ },
+ [LLMFactory.GoogleCloud]: () => {
+ setCurrentLlmFactory(LLMFactory.GoogleCloud);
+ setProviderVisible(true);
+ },
+ [LLMFactory.AzureOpenAI]: () => {
+ setCurrentLlmFactory(LLMFactory.AzureOpenAI);
+ setProviderVisible(true);
+ },
+ [LLMFactory.MinerU]: () => {
+ setCurrentLlmFactory(LLMFactory.MinerU);
+ setProviderVisible(true);
+ },
+ [LLMFactory.PaddleOCR]: () => {
+ setCurrentLlmFactory(LLMFactory.PaddleOCR);
+ setProviderVisible(true);
+ },
+ [LLMFactory.OpenDataLoader]: () => {
+ setCurrentLlmFactory(LLMFactory.OpenDataLoader);
+ setProviderVisible(true);
+ },
}),
- [
- showBedrockAddingModal,
- showVolcAddingModal,
- showSparkAddingModal,
- showyiyanAddingModal,
- showFishAudioAddingModal,
- showTencentCloudAddingModal,
- showGoogleAddingModal,
- showAzureAddingModal,
- showMineruModal,
- showPaddleOCRModal,
- showOpenDataLoaderModal,
- ],
+ [showBedrockAddingModal],
);
const handleAddModel = useCallback(
(llmFactory: string) => {
- console.log('handleAddModel', llmFactory);
if (isLocalLlmFactory(llmFactory)) {
- showLlmAddingModal(llmFactory);
+ setCurrentLlmFactory(llmFactory);
+ setProviderVisible(true);
} else if (llmFactory in ModalMap) {
ModalMap[llmFactory as keyof typeof ModalMap]();
} else {
- showApiKeyModal({ llm_factory: llmFactory });
+ setCurrentLlmFactory(llmFactory);
+ setProviderVisible(true);
}
},
- [showApiKeyModal, showLlmAddingModal, ModalMap],
+ [ModalMap],
);
- // const handleEditModel = useCallback(
- // (model: any, factory: LlmItem) => {
- // if (factory) {
- // const detailedFactory = detailedLlmList[factory.name];
- // const detailedModel = detailedFactory?.llm?.find(
- // (m: any) => m.name === model.name,
- // );
+ // Open the ProviderModal in viewMode (read-only) for an existing
+ // instance so the user can edit its model list. The instance's
+ // `api_key`, `baseUrl` and `model_info` are passed as initial values;
+ // the list picker uses `model_info` to pre-check the already-added
+ // models.
+ const handleEditInstance = useCallback(
+ (
+ providerName: string,
+ instance: IProviderInstance,
+ models: IInstanceModel[],
+ ) => {
+ setCurrentLlmFactory(providerName);
+ const modelInfos: IModelInfo[] = models.map((m) => ({
+ model_name: m.name,
+ model_type: m.model_type,
+ max_tokens: m.max_tokens ?? 0,
+ }));
+ // For non-LIST_MODEL_PROVIDERS, the modal renders model_name /
+ // model_type / max_tokens / is_tools as form fields, so seed
+ // them from the first existing model to match what the user sees
+ // in the instance list.
+ const firstModel = models[0];
+ setViewModeInitialValues({
+ instance_name: instance.instance_name,
+ api_key: instance.api_key,
+ // baseUrl is only present when the showProviderInstance endpoint
+ // returned it; pass it as both `base_url` and `api_base` so it
+ // fills the form field regardless of which name the provider
+ // config uses.
+ ...(instance.base_url
+ ? { base_url: instance.base_url, api_base: instance.base_url }
+ : {}),
+ ...(firstModel
+ ? {
+ model_name: firstModel.name,
+ model_type: firstModel.model_type,
+ max_tokens: firstModel.max_tokens,
+ }
+ : {}),
+ model_info: modelInfos,
+ });
+ setViewMode(true);
+ setProviderVisible(true);
+ },
+ [],
+ );
- // const editData = {
- // llm_factory: factory.name,
- // llm_name: model.name,
- // model_type: model.type,
- // };
+ // viewMode save handler: receives the list of selected models (or
+ // the editable model fields for non-LIST_MODEL_PROVIDERS) from the
+ // modal and adds them via `addInstanceModel`. Does NOT call
+ // `addProviderInstance` because the instance itself is unchanged.
+ const handleViewModeOk = useCallback(
+ async (payload: IViewModeOkPayload) => {
+ setProviderLoading(true);
+ try {
+ if (payload.modelInfos.length > 0) {
+ // LIST_MODEL_PROVIDERS: full sync — call addInstanceModel for
+ // every selected model. The backend is idempotent so re-adding
+ // an already-present model is a no-op.
+ for (const model of payload.modelInfos) {
+ const modelType = Array.isArray(model.model_type)
+ ? model.model_type
+ : model.model_type
+ ? [model.model_type as string]
+ : [];
+ const ret = await addInstanceModel({
+ provider_name: payload.llmFactory,
+ instance_name: payload.instanceName,
+ model_name: model.model_name,
+ model_type: modelType,
+ max_tokens: model.max_tokens ?? 0,
+ ...(model.extra ? { extra: model.extra } : {}),
+ });
+ if (ret.code !== 0) {
+ return ret;
+ }
+ }
+ } else if (payload.formValues) {
+ // Non-LIST_MODEL_PROVIDERS: add/update the single model
+ // described by the form values.
+ const fv = payload.formValues;
+ const modelType = Array.isArray(fv.model_type)
+ ? fv.model_type
+ : fv.model_type
+ ? [fv.model_type as string]
+ : [];
+ const ret = await addInstanceModel({
+ provider_name: payload.llmFactory,
+ instance_name: payload.instanceName,
+ model_name: fv.model_name,
+ model_type: modelType,
+ max_tokens: fv.max_tokens ?? 0,
+ ...(fv.is_tools !== undefined
+ ? { extra: { is_tools: !!fv.is_tools } }
+ : {}),
+ });
+ if (ret.code !== 0) {
+ return ret;
+ }
+ }
+ setProviderVisible(false);
+ } finally {
+ setProviderLoading(false);
+ }
+ },
+ [addInstanceModel],
+ );
- // if (isLocalLlmFactory(factory.name)) {
- // showLlmAddingModal(factory.name, true, editData, detailedModel);
- // } else if (factory.name in ModalMap) {
- // ModalMap[factory.name as keyof typeof ModalMap]();
- // } else {
- // showApiKeyModal(editData, true);
- // }
- // }
- // },
- // [showApiKeyModal, showLlmAddingModal, ModalMap, detailedLlmList],
- // );
-
- const handleOk = useMemo(() => {
- if (apiKeyVisible) {
- return onApiKeySavingOk;
- }
- if (llmAddingVisible) {
- return onLlmAddingOk;
- }
- if (volcAddingVisible) {
- return onVolcAddingOk;
- }
- if (TencentCloudAddingVisible) {
- return onTencentCloudAddingOk;
- }
- if (SparkAddingVisible) {
- return onSparkAddingOk;
- }
- if (yiyanAddingVisible) {
- return onyiyanAddingOk;
- }
- if (FishAudioAddingVisible) {
- return onFishAudioAddingOk;
- }
- if (bedrockAddingVisible) {
- return onBedrockAddingOk;
- }
- if (AzureAddingVisible) {
- return onAzureAddingOk;
- }
- if (mineruVisible) {
- return onMineruOk;
- }
- if (paddleocrVisible) {
- return onPaddleOCROk;
- }
- if (opendataloaderVisible) {
- return onOpenDataLoaderOk;
- }
- if (GoogleAddingVisible) {
- return onGoogleAddingOk;
- }
- return () => {};
- }, [
- GoogleAddingVisible,
- onGoogleAddingOk,
- apiKeyVisible,
- onApiKeySavingOk,
- llmAddingVisible,
- onLlmAddingOk,
- volcAddingVisible,
- onVolcAddingOk,
- TencentCloudAddingVisible,
- onTencentCloudAddingOk,
- SparkAddingVisible,
- onSparkAddingOk,
- yiyanAddingVisible,
- onyiyanAddingOk,
- FishAudioAddingVisible,
- onFishAudioAddingOk,
- bedrockAddingVisible,
- onBedrockAddingOk,
- AzureAddingVisible,
- onAzureAddingOk,
- mineruVisible,
- onMineruOk,
- paddleocrVisible,
- onPaddleOCROk,
- opendataloaderVisible,
- onOpenDataLoaderOk,
- ]);
-
- const { onApiKeyVerifying } = useVerifySettings({
- onVerify: handleOk,
- });
+ // Closing the modal also clears the viewMode flag so the next open
+ // starts in the default (add) mode.
+ const hideProviderModal = useCallback(() => {
+ setProviderVisible(false);
+ setViewMode(false);
+ }, []);
return (
-
- {llmAddingVisible && (
-
- )}
-
-
-
-
-
-
+
+ {/* Unified ProviderModal (replaces 9 independent modals) */}
+
+
({ isValid: null, logs: '' })}
>
-
-
-
-
);
};
+
export default ModelProviders;
diff --git a/web/src/pages/user-setting/setting-model/modal/api-key-modal/index.tsx b/web/src/pages/user-setting/setting-model/modal/api-key-modal/index.tsx
deleted file mode 100644
index 09b766d60b..0000000000
--- a/web/src/pages/user-setting/setting-model/modal/api-key-modal/index.tsx
+++ /dev/null
@@ -1,195 +0,0 @@
-import { IModalManagerChildrenProps } from '@/components/modal-manager';
-import { RAGFlowFormItem } from '@/components/ragflow-form';
-import { Form } from '@/components/ui/form';
-import { Input } from '@/components/ui/input';
-import { Modal } from '@/components/ui/modal/modal';
-import { LLMFactory } from '@/constants/llm';
-import { useTranslate } from '@/hooks/common-hooks';
-import { KeyboardEventHandler, useCallback, useEffect } from 'react';
-import { useForm } from 'react-hook-form';
-import { ApiKeyPostBody } from '../../../interface';
-import { LLMHeader } from '../../components/llm-header';
-import { VerifyResult } from '../../hooks';
-import VerifyButton from '../verify-button';
-
-interface IProps extends Omit {
- loading: boolean;
- initialValue: string;
- llmFactory: string;
- editMode?: boolean;
- onOk: (postBody: ApiKeyPostBody) => void;
- onVerify: (
- postBody: any,
- ) => Promise;
- showModal?(): void;
-}
-
-type FieldType = {
- instance_name?: string;
- api_key?: string;
- base_url?: string;
- group_id?: string;
-};
-
-const modelsWithBaseUrl = [
- LLMFactory.OpenAI,
- LLMFactory.AzureOpenAI,
- LLMFactory.TongYiQianWen,
- LLMFactory.MiniMax,
- LLMFactory.SILICONFLOW,
-];
-
-const ApiKeyModal = ({
- visible,
- hideModal,
- llmFactory,
- loading,
- initialValue,
- // editMode = false,
- onOk,
- onVerify,
-}: IProps) => {
- const form = useForm();
- const { t } = useTranslate('setting');
-
- const handleOk = useCallback(async () => {
- await form.handleSubmit((values) => onOk(values as ApiKeyPostBody))();
- }, [form, onOk]);
-
- const handleKeyDown: KeyboardEventHandler = useCallback(
- async (e) => {
- if (e.key === 'Enter') {
- await handleOk();
- }
- },
- [handleOk],
- );
-
- useEffect(() => {
- if (visible) {
- form.setValue('api_key', initialValue);
- } else {
- form.reset();
- }
- }, [initialValue, form, visible]);
-
- return (
- }
- open={visible}
- onOpenChange={(open) => !open && hideModal()}
- onOk={handleOk}
- onCancel={hideModal}
- confirmLoading={loading}
- okText={t('save')}
- cancelText={t('cancel')}
- className="!w-[600px]"
- testId="apikey-modal"
- okButtonTestId="apikey-save"
- >
-
-
- );
-};
-
-export default ApiKeyModal;
diff --git a/web/src/pages/user-setting/setting-model/modal/azure-openai-modal/index.tsx b/web/src/pages/user-setting/setting-model/modal/azure-openai-modal/index.tsx
deleted file mode 100644
index fd4500024b..0000000000
--- a/web/src/pages/user-setting/setting-model/modal/azure-openai-modal/index.tsx
+++ /dev/null
@@ -1,225 +0,0 @@
-import {
- DynamicForm,
- DynamicFormRef,
- FormFieldConfig,
- FormFieldType,
-} from '@/components/dynamic-form';
-import { Modal } from '@/components/ui/modal/modal';
-import { useCommonTranslation, useTranslate } from '@/hooks/common-hooks';
-import { useBuildModelTypeOptions } from '@/hooks/logic-hooks/use-build-options';
-import { IModalProps } from '@/interfaces/common';
-import { IAddProviderInstanceRequestBody } from '@/interfaces/request/llm';
-import {
- useFetchInstanceNameSet,
- useHideWhenInstanceExists,
- VerifyResult,
-} from '@/pages/user-setting/setting-model/hooks';
-import { memo, useCallback, useEffect, useMemo, useRef } from 'react';
-import { FieldValues } from 'react-hook-form';
-import { LLMHeader } from '../../components/llm-header';
-import VerifyButton from '../../modal/verify-button';
-
-const AzureOpenAIModal = ({
- visible,
- hideModal,
- onOk,
- onVerify,
- loading,
- llmFactory,
-}: IModalProps & {
- llmFactory: string;
- onVerify?: (
- postBody: any,
- ) => Promise;
-}) => {
- const { t } = useTranslate('setting');
- const { t: tg } = useCommonTranslation();
- const { buildModelTypeOptions } = useBuildModelTypeOptions();
- const formRef = useRef(null);
- const { instanceNameSet } = useFetchInstanceNameSet(llmFactory);
-
- const hideWhenInstanceExists = useHideWhenInstanceExists(instanceNameSet);
-
- const fields: FormFieldConfig[] = useMemo(
- () => [
- {
- name: 'instance_name',
- label: t('instanceName'),
- type: FormFieldType.Text,
- required: true,
- placeholder: t('instanceNameMessage'),
- tooltip: t('instanceNameTip'),
- validation: {
- message: t('instanceNameMessage'),
- },
- },
- {
- name: 'model_type',
- label: t('modelType'),
- type: FormFieldType.MultiSelect,
- required: true,
- options: buildModelTypeOptions(['chat', 'embedding', 'image2text']),
- defaultValue: ['embedding'],
- },
- {
- name: 'api_base',
- label: t('addLlmBaseUrl'),
- type: FormFieldType.Text,
- required: true,
- placeholder: t('baseUrlNameMessage'),
- validation: {
- message: t('baseUrlNameMessage'),
- },
- shouldRender: hideWhenInstanceExists,
- },
- {
- name: 'api_key',
- label: t('apiKey'),
- type: FormFieldType.Text,
- required: false,
- placeholder: t('apiKeyMessage'),
- shouldRender: hideWhenInstanceExists,
- },
- {
- name: 'llm_name',
- label: t('modelName'),
- type: FormFieldType.Text,
- required: true,
- placeholder: t('modelNameMessage'),
- defaultValue: 'gpt-3.5-turbo',
- validation: {
- message: t('modelNameMessage'),
- },
- },
- {
- name: 'api_version',
- label: t('apiVersion'),
- type: FormFieldType.Text,
- required: false,
- placeholder: t('apiVersionMessage'),
- defaultValue: '2024-02-01',
- },
- {
- name: 'max_tokens',
- label: t('maxTokens'),
- type: FormFieldType.Number,
- required: true,
- placeholder: t('maxTokensTip'),
- validation: {
- min: 0,
- message: t('maxTokensMessage'),
- },
- },
- {
- name: 'vision',
- label: t('vision'),
- type: FormFieldType.Switch,
- defaultValue: false,
- dependencies: ['model_type'],
- shouldRender: (formValues: any) => {
- const modelType = formValues?.model_type;
- if (Array.isArray(modelType)) {
- return modelType.includes('chat');
- }
- return modelType === 'chat';
- },
- },
- ],
- [t, buildModelTypeOptions, hideWhenInstanceExists],
- );
-
- const handleOk = async (values?: FieldValues) => {
- if (!values) return;
-
- const modelType = values.model_type.map((t: string) =>
- t === 'chat' && values.vision ? 'image2text' : t,
- );
-
- const data: IAddProviderInstanceRequestBody & { api_version?: string } = {
- instance_name: values.instance_name as string,
- llm_factory: llmFactory,
- llm_name: values.llm_name as string,
- model_type: modelType,
- api_base: values.api_base as string,
- api_key: values.api_key as string | undefined,
- max_tokens: values.max_tokens as number,
- api_version: values.api_version as string,
- };
-
- await onOk?.(data);
- };
-
- const verifyParamsFunc = useCallback(() => {
- const values = formRef.current?.getValues();
- return {
- llm_factory: llmFactory,
- model_type: values.model_type.map((t: string) =>
- t === 'chat' && values.vision ? 'image2text' : t,
- ),
- };
- }, [llmFactory]);
-
- const handleVerify = useCallback(
- async (params: any) => {
- const verifyParams = verifyParamsFunc();
- const res = await onVerify?.({ ...params, ...verifyParams });
- return (res || { isValid: null, logs: '' }) as VerifyResult;
- },
- [verifyParamsFunc, onVerify],
- );
-
- useEffect(() => {
- if (!visible) {
- formRef.current?.reset();
- }
- }, [visible]);
-
- return (
- }
- open={visible || false}
- onOpenChange={(open) => !open && hideModal?.()}
- maskClosable={false}
- footer={
}
- >
- {
- console.log(data);
- }}
- ref={formRef}
- defaultValues={
- {
- instance_name: '',
- model_type: ['embedding'],
- llm_name: 'gpt-3.5-turbo',
- api_version: '2024-02-01',
- vision: false,
- max_tokens: 8192,
- } as FieldValues
- }
- labelClassName="font-normal"
- >
- <>
- {onVerify && }
-
- {
- hideModal?.();
- }}
- />
- {
- handleOk(values);
- }}
- />
-
- >
-
-
- );
-};
-
-export default memo(AzureOpenAIModal);
diff --git a/web/src/pages/user-setting/setting-model/modal/fish-audio-modal/index.tsx b/web/src/pages/user-setting/setting-model/modal/fish-audio-modal/index.tsx
deleted file mode 100644
index 368ff5bd0e..0000000000
--- a/web/src/pages/user-setting/setting-model/modal/fish-audio-modal/index.tsx
+++ /dev/null
@@ -1,182 +0,0 @@
-import {
- DynamicForm,
- DynamicFormRef,
- FormFieldConfig,
- FormFieldType,
-} from '@/components/dynamic-form';
-import { Modal } from '@/components/ui/modal/modal';
-import { useCommonTranslation, useTranslate } from '@/hooks/common-hooks';
-import { useBuildModelTypeOptions } from '@/hooks/logic-hooks/use-build-options';
-import { IModalProps } from '@/interfaces/common';
-import { IAddProviderInstanceRequestBody } from '@/interfaces/request/llm';
-import {
- useFetchInstanceNameSet,
- useHideWhenInstanceExists,
- VerifyResult,
-} from '@/pages/user-setting/setting-model/hooks';
-import { memo, useCallback, useEffect, useMemo, useRef } from 'react';
-import { FieldValues } from 'react-hook-form';
-import { LLMHeader } from '../../components/llm-header';
-import VerifyButton from '../../modal/verify-button';
-
-const FishAudioModal = ({
- visible,
- hideModal,
- onOk,
- onVerify,
- loading,
- llmFactory,
-}: IModalProps & {
- llmFactory: string;
- onVerify?: (
- postBody: any,
- ) => Promise;
-}) => {
- const { t } = useTranslate('setting');
- const { t: tc } = useCommonTranslation();
- const { buildModelTypeOptions } = useBuildModelTypeOptions();
- const formRef = useRef(null);
- const { instanceNameSet } = useFetchInstanceNameSet(llmFactory);
-
- const hideWhenInstanceExists = useHideWhenInstanceExists(instanceNameSet);
-
- const fields: FormFieldConfig[] = useMemo(
- () => [
- {
- name: 'instance_name',
- label: t('instanceName'),
- type: FormFieldType.Text,
- required: true,
- placeholder: t('instanceNameMessage'),
- tooltip: t('instanceNameTip'),
- validation: { message: t('instanceNameMessage') },
- },
- {
- name: 'model_type',
- label: t('modelType'),
- type: FormFieldType.MultiSelect,
- required: true,
- options: buildModelTypeOptions(['tts']),
- defaultValue: ['tts'],
- },
- {
- name: 'llm_name',
- label: t('modelName'),
- type: FormFieldType.Text,
- required: true,
- placeholder: t('FishAudioModelNameMessage'),
- validation: { message: t('FishAudioModelNameMessage') },
- },
- {
- name: 'fish_audio_ak',
- label: t('addFishAudioAK'),
- type: FormFieldType.Text,
- required: true,
- placeholder: t('FishAudioAKMessage'),
- validation: { message: t('FishAudioAKMessage') },
- shouldRender: hideWhenInstanceExists,
- },
- {
- name: 'fish_audio_refid',
- label: t('addFishAudioRefID'),
- type: FormFieldType.Text,
- required: true,
- placeholder: t('FishAudioRefIDMessage'),
- validation: { message: t('FishAudioRefIDMessage') },
- shouldRender: hideWhenInstanceExists,
- },
- {
- name: 'max_tokens',
- label: t('maxTokens'),
- type: FormFieldType.Number,
- required: true,
- placeholder: t('maxTokensTip'),
- validation: {
- min: 0,
- message: t('maxTokensInvalidMessage'),
- },
- },
- ],
- [t, buildModelTypeOptions, hideWhenInstanceExists],
- );
-
- const handleOk = async (values?: FieldValues) => {
- if (!values) return;
-
- const data: IAddProviderInstanceRequestBody & {
- fish_audio_ak: string;
- fish_audio_refid: string;
- } = {
- instance_name: values.instance_name as string,
- llm_factory: llmFactory,
- llm_name: values.llm_name as string,
- model_type: values.model_type,
- fish_audio_ak: values.fish_audio_ak,
- fish_audio_refid: values.fish_audio_refid,
- max_tokens: values.max_tokens as number,
- };
-
- await onOk?.(data);
- };
-
- const handleVerify = useCallback(
- async (params: any) => {
- const res = await onVerify?.({ ...params, llm_factory: llmFactory });
- return (res || { isValid: null, logs: '' }) as VerifyResult;
- },
- [llmFactory, onVerify],
- );
-
- useEffect(() => {
- if (!visible) {
- formRef.current?.reset();
- }
- }, [visible]);
-
- return (
- }
- open={visible || false}
- onOpenChange={(open) => !open && hideModal?.()}
- maskClosable={false}
- footerClassName="py-1"
- footer={
}
- >
- console.log(data)}
- ref={formRef}
- defaultValues={{
- instance_name: '',
- model_type: ['tts'],
- max_tokens: 8192,
- }}
- labelClassName="font-normal"
- >
- {onVerify && (
-
- )}
-
-
-
- );
-};
-
-export default memo(FishAudioModal);
diff --git a/web/src/pages/user-setting/setting-model/modal/google-modal/index.tsx b/web/src/pages/user-setting/setting-model/modal/google-modal/index.tsx
deleted file mode 100644
index 07dd1c8770..0000000000
--- a/web/src/pages/user-setting/setting-model/modal/google-modal/index.tsx
+++ /dev/null
@@ -1,210 +0,0 @@
-import {
- DynamicForm,
- DynamicFormRef,
- FormFieldConfig,
- FormFieldType,
-} from '@/components/dynamic-form';
-import { Modal } from '@/components/ui/modal/modal';
-import { useCommonTranslation, useTranslate } from '@/hooks/common-hooks';
-import { useBuildModelTypeOptions } from '@/hooks/logic-hooks/use-build-options';
-import { IModalProps } from '@/interfaces/common';
-import { IAddProviderInstanceRequestBody } from '@/interfaces/request/llm';
-import {
- useFetchInstanceNameSet,
- useHideWhenInstanceExists,
- VerifyResult,
-} from '@/pages/user-setting/setting-model/hooks';
-import { memo, useCallback, useEffect, useMemo, useRef } from 'react';
-import { FieldValues } from 'react-hook-form';
-import { LLMHeader } from '../../components/llm-header';
-import VerifyButton from '../../modal/verify-button';
-
-const GoogleModal = ({
- visible,
- hideModal,
- onOk,
- onVerify,
- loading,
- llmFactory,
-}: IModalProps & {
- llmFactory: string;
- onVerify?: (
- postBody: any,
- ) => Promise;
-}) => {
- const { t } = useTranslate('setting');
- const { t: tc } = useCommonTranslation();
- const { buildModelTypeOptions } = useBuildModelTypeOptions();
- const { instanceNameSet } = useFetchInstanceNameSet(llmFactory);
- const formRef = useRef(null);
-
- const hideWhenInstanceExists = useHideWhenInstanceExists(instanceNameSet);
-
- const fields: FormFieldConfig[] = useMemo(
- () => [
- {
- name: 'instance_name',
- label: t('instanceName'),
- type: FormFieldType.Text,
- required: true,
- placeholder: t('instanceNameMessage'),
- tooltip: t('instanceNameTip'),
- validation: { message: t('instanceNameMessage') },
- },
- {
- name: 'model_type',
- label: t('modelType'),
- type: FormFieldType.MultiSelect,
- required: true,
- options: buildModelTypeOptions(['chat', 'image2text']),
- defaultValue: ['chat'],
- },
- {
- name: 'llm_name',
- label: t('modelID'),
- type: FormFieldType.Text,
- required: true,
- placeholder: t('GoogleModelIDMessage'),
- validation: {
- message: t('GoogleModelIDMessage'),
- },
- },
- {
- name: 'google_project_id',
- label: t('addGoogleProjectID'),
- type: FormFieldType.Text,
- required: true,
- placeholder: t('GoogleProjectIDMessage'),
- validation: {
- message: t('GoogleProjectIDMessage'),
- },
- shouldRender: hideWhenInstanceExists,
- },
- {
- name: 'google_region',
- label: t('addGoogleRegion'),
- type: FormFieldType.Text,
- required: true,
- placeholder: t('GoogleRegionMessage'),
- validation: {
- message: t('GoogleRegionMessage'),
- },
- shouldRender: hideWhenInstanceExists,
- },
- {
- name: 'google_service_account_key',
- label: t('addGoogleServiceAccountKey'),
- type: FormFieldType.Text,
- required: true,
- placeholder: t('GoogleServiceAccountKeyMessage'),
- validation: {
- message: t('GoogleServiceAccountKeyMessage'),
- },
- shouldRender: hideWhenInstanceExists,
- },
- {
- name: 'max_tokens',
- label: t('maxTokens'),
- type: FormFieldType.Number,
- required: true,
- placeholder: t('maxTokensTip'),
- validation: {
- min: 0,
- message: t('maxTokensMinMessage'),
- },
- customValidate: (value: any) => {
- if (value === undefined || value === null || value === '') {
- return t('maxTokensMessage');
- }
- if (value < 0) {
- return t('maxTokensMinMessage');
- }
- return true;
- },
- },
- ],
- [t, buildModelTypeOptions, hideWhenInstanceExists],
- );
-
- const handleOk = async (values?: FieldValues) => {
- if (!values) return;
-
- const data = {
- instance_name: values.instance_name as string,
- llm_factory: llmFactory,
- model_type: values.model_type,
- llm_name: values.llm_name,
- google_project_id: values.google_project_id,
- google_region: values.google_region,
- google_service_account_key: values.google_service_account_key,
- max_tokens: values.max_tokens,
- } as IAddProviderInstanceRequestBody;
-
- await onOk?.(data);
- };
-
- const verifyParamsFunc = useCallback(() => {
- return {
- llm_factory: llmFactory,
- };
- }, [llmFactory]);
-
- const handleVerify = useCallback(
- async (params: any) => {
- const verifyParams = verifyParamsFunc();
- const res = await onVerify?.({ ...params, ...verifyParams });
- return (res || { isValid: null, logs: '' }) as VerifyResult;
- },
- [verifyParamsFunc, onVerify],
- );
-
- useEffect(() => {
- if (!visible) {
- formRef.current?.reset();
- }
- }, [visible]);
-
- return (
- }
- open={visible || false}
- onOpenChange={(open) => !open && hideModal?.()}
- maskClosable={false}
- footer={
}
- >
- {
- // Form submission is handled by SavingButton
- }}
- ref={formRef}
- defaultValues={
- {
- instance_name: '',
- model_type: ['chat'],
- max_tokens: 8192,
- } as FieldValues
- }
- labelClassName="font-normal"
- >
- {onVerify && }
-
- {
- hideModal?.();
- }}
- />
- {
- handleOk(values);
- }}
- />
-
-
-
- );
-};
-
-export default memo(GoogleModal);
diff --git a/web/src/pages/user-setting/setting-model/modal/mineru-modal/index.tsx b/web/src/pages/user-setting/setting-model/modal/mineru-modal/index.tsx
deleted file mode 100644
index e3d15ebbf6..0000000000
--- a/web/src/pages/user-setting/setting-model/modal/mineru-modal/index.tsx
+++ /dev/null
@@ -1,200 +0,0 @@
-import { RAGFlowFormItem } from '@/components/ragflow-form';
-import { ButtonLoading } from '@/components/ui/button';
-import {
- Dialog,
- DialogContent,
- DialogFooter,
- DialogHeader,
- DialogTitle,
-} from '@/components/ui/dialog';
-import { Form } from '@/components/ui/form';
-import { Input } from '@/components/ui/input';
-import { RAGFlowSelect } from '@/components/ui/select';
-import { Switch } from '@/components/ui/switch';
-import { LLMFactory } from '@/constants/llm';
-import { IModalProps } from '@/interfaces/common';
-import { VerifyResult } from '@/pages/user-setting/setting-model/hooks';
-import { buildOptions } from '@/utils/form';
-import { zodResolver } from '@hookform/resolvers/zod';
-import { t } from 'i18next';
-import { memo, useEffect } from 'react';
-import { useForm, useWatch } from 'react-hook-form';
-import { useTranslation } from 'react-i18next';
-import { z } from 'zod';
-import { LLMHeader } from '../../components/llm-header';
-import VerifyButton from '../verify-button';
-
-const FormSchema = z.object({
- instance_name: z.string().min(1, {
- message: t('setting.instanceNameMessage'),
- }),
- llm_name: z.string().min(1, {
- message: t('setting.mineru.modelNameRequired'),
- }),
- mineru_apiserver: z.string().url(),
- mineru_output_dir: z.string().optional(),
- mineru_backend: z.enum([
- 'pipeline',
- 'vlm-transformers',
- 'vlm-vllm-engine',
- 'vlm-http-client',
- 'vlm-mlx-engine',
- 'vlm-vllm-async-engine',
- 'vlm-lmdeploy-engine',
- ]),
- mineru_server_url: z.string().url().optional(),
- mineru_delete_output: z.boolean(),
-});
-
-export type MinerUFormValues = z.infer;
-
-const MinerUModal = ({
- visible,
- hideModal,
- onOk,
- onVerify,
- loading,
-}: IModalProps & {
- onVerify?: (
- postBody: any,
- ) => Promise;
-}) => {
- const { t } = useTranslation();
-
- const backendOptions = buildOptions([
- 'pipeline',
- 'vlm-transformers',
- 'vlm-vllm-engine',
- 'vlm-http-client',
- 'vlm-mlx-engine',
- 'vlm-vllm-async-engine',
- 'vlm-lmdeploy-engine',
- ]);
-
- const form = useForm({
- resolver: zodResolver(FormSchema),
- defaultValues: {
- instance_name: '',
- mineru_backend: 'pipeline',
- mineru_delete_output: true,
- },
- });
-
- const backend = useWatch({
- control: form.control,
- name: 'mineru_backend',
- });
-
- const handleOk = async (values: MinerUFormValues) => {
- const ret = await onOk?.(values as any);
- if (ret) {
- hideModal?.();
- }
- };
-
- useEffect(() => {
- if (!visible) {
- form.reset();
- }
- }, [visible, form]);
-
- return (
-
-
-
-
-
-
-
-
-
-
-
-
- {t('common.save', 'Save')}
-
-
-
-
-
- );
-};
-
-export default memo(MinerUModal);
diff --git a/web/src/pages/user-setting/setting-model/modal/next-tencent-modal/index.tsx b/web/src/pages/user-setting/setting-model/modal/next-tencent-modal/index.tsx
deleted file mode 100644
index f95d3ef1d2..0000000000
--- a/web/src/pages/user-setting/setting-model/modal/next-tencent-modal/index.tsx
+++ /dev/null
@@ -1,211 +0,0 @@
-import {
- DynamicForm,
- DynamicFormRef,
- FormFieldConfig,
- FormFieldType,
-} from '@/components/dynamic-form';
-import { Modal } from '@/components/ui/modal/modal';
-import { useCommonTranslation, useTranslate } from '@/hooks/common-hooks';
-import { useBuildModelTypeOptions } from '@/hooks/logic-hooks/use-build-options';
-import { IModalProps } from '@/interfaces/common';
-import { IAddProviderInstanceRequestBody } from '@/interfaces/request/llm';
-import {
- useFetchInstanceNameSet,
- useHideWhenInstanceExists,
- VerifyResult,
-} from '@/pages/user-setting/setting-model/hooks';
-import { memo, useCallback, useEffect, useMemo, useRef } from 'react';
-import { FieldValues } from 'react-hook-form';
-import { LLMHeader } from '../../components/llm-header';
-import VerifyButton from '../../modal/verify-button';
-
-const TencentCloudModal = ({
- visible,
- hideModal,
- onOk,
- onVerify,
- loading,
- llmFactory,
-}: IModalProps> & {
- llmFactory: string;
- onVerify?: (
- postBody: any,
- ) => Promise;
-}) => {
- const { t } = useTranslate('setting');
- const { t: tc } = useCommonTranslation();
- const { buildModelTypeOptions } = useBuildModelTypeOptions();
- const formRef = useRef(null);
- const { instanceNameSet } = useFetchInstanceNameSet(llmFactory);
-
- const hideWhenInstanceExists = useHideWhenInstanceExists(instanceNameSet);
-
- const fields: FormFieldConfig[] = useMemo(
- () => [
- {
- name: 'instance_name',
- label: t('instanceName'),
- type: FormFieldType.Text,
- required: true,
- placeholder: t('instanceNameMessage'),
- tooltip: t('instanceNameTip'),
- validation: { message: t('instanceNameMessage') },
- },
- {
- name: 'model_type',
- label: t('modelType'),
- type: FormFieldType.MultiSelect,
- required: true,
- options: buildModelTypeOptions(['speech2text']),
- defaultValue: ['speech2text'],
- },
- {
- name: 'llm_name',
- label: t('modelName'),
- type: FormFieldType.Select,
- required: true,
- options: [
- { label: '16k_zh', value: '16k_zh' },
- { label: '16k_zh_large', value: '16k_zh_large' },
- { label: '16k_multi_lang', value: '16k_multi_lang' },
- { label: '16k_zh_dialect', value: '16k_zh_dialect' },
- { label: '16k_en', value: '16k_en' },
- { label: '16k_yue', value: '16k_yue' },
- { label: '16k_zh-PY', value: '16k_zh-PY' },
- { label: '16k_ja', value: '16k_ja' },
- { label: '16k_ko', value: '16k_ko' },
- { label: '16k_vi', value: '16k_vi' },
- { label: '16k_ms', value: '16k_ms' },
- { label: '16k_id', value: '16k_id' },
- { label: '16k_fil', value: '16k_fil' },
- { label: '16k_th', value: '16k_th' },
- { label: '16k_pt', value: '16k_pt' },
- { label: '16k_tr', value: '16k_tr' },
- { label: '16k_ar', value: '16k_ar' },
- { label: '16k_es', value: '16k_es' },
- { label: '16k_hi', value: '16k_hi' },
- { label: '16k_fr', value: '16k_fr' },
- { label: '16k_zh_medical', value: '16k_zh_medical' },
- { label: '16k_de', value: '16k_de' },
- ],
- defaultValue: '16k_zh',
- validation: {
- message: t('SparkModelNameMessage'),
- },
- },
- {
- name: 'TencentCloud_sid',
- label: t('addTencentCloudSID'),
- type: FormFieldType.Text,
- required: true,
- placeholder: t('TencentCloudSIDMessage'),
- validation: {
- message: t('TencentCloudSIDMessage'),
- },
- shouldRender: hideWhenInstanceExists,
- },
- {
- name: 'TencentCloud_sk',
- label: t('addTencentCloudSK'),
- type: FormFieldType.Text,
- required: true,
- placeholder: t('TencentCloudSKMessage'),
- validation: {
- message: t('TencentCloudSKMessage'),
- },
- shouldRender: hideWhenInstanceExists,
- },
- ],
- [t, buildModelTypeOptions, hideWhenInstanceExists],
- );
-
- const handleOk = async (values?: FieldValues) => {
- if (!values) return;
-
- const data = {
- instance_name: values.instance_name as string,
- model_type: values.model_type,
- llm_name: values.llm_name as string,
- TencentCloud_sid: values.TencentCloud_sid as string,
- TencentCloud_sk: values.TencentCloud_sk as string,
- llm_factory: llmFactory,
- } as Omit;
-
- await onOk?.(data);
- };
-
- const verifyParamsFunc = useCallback(() => {
- return {
- llm_factory: llmFactory,
- };
- }, [llmFactory]);
-
- const handleVerify = useCallback(
- async (params: any) => {
- const verifyParams = verifyParamsFunc();
- const res = await onVerify?.({ ...params, ...verifyParams });
- return (res || { isValid: null, logs: '' }) as VerifyResult;
- },
- [verifyParamsFunc, onVerify],
- );
-
- useEffect(() => {
- if (!visible) {
- formRef.current?.reset();
- }
- }, [visible]);
-
- return (
- }
- open={visible || false}
- onOpenChange={(open) => !open && hideModal?.()}
- maskClosable={false}
- footer={
}
- >
- {}}
- ref={formRef}
- defaultValues={
- {
- instance_name: '',
- model_type: ['speech2text'],
- llm_name: '16k_zh',
- } as FieldValues
- }
- labelClassName="font-normal"
- >
- {onVerify && (
-
- )}
-
-
-
- );
-};
-
-export default memo(TencentCloudModal);
diff --git a/web/src/pages/user-setting/setting-model/modal/ollama-modal/index.tsx b/web/src/pages/user-setting/setting-model/modal/ollama-modal/index.tsx
deleted file mode 100644
index 08d31b3095..0000000000
--- a/web/src/pages/user-setting/setting-model/modal/ollama-modal/index.tsx
+++ /dev/null
@@ -1,384 +0,0 @@
-import {
- DynamicForm,
- DynamicFormRef,
- FormFieldConfig,
- FormFieldType,
-} from '@/components/dynamic-form';
-import { Modal } from '@/components/ui/modal/modal';
-import { LLMFactory } from '@/constants/llm';
-import { useCommonTranslation, useTranslate } from '@/hooks/common-hooks';
-import { useBuildModelTypeOptions } from '@/hooks/logic-hooks/use-build-options';
-import { IModalProps } from '@/interfaces/common';
-import { IAddProviderInstanceRequestBody } from '@/interfaces/request/llm';
-import {
- useFetchInstanceNameSet,
- useHideWhenInstanceExists,
- VerifyResult,
-} from '@/pages/user-setting/setting-model/hooks';
-import { memo, useCallback, useMemo, useRef } from 'react';
-import { FieldValues } from 'react-hook-form';
-import { LLMHeader } from '../../components/llm-header';
-import VerifyButton from '../../modal/verify-button';
-
-const llmFactoryToUrlMap: Partial> = {
- [LLMFactory.Ollama]:
- 'https://github.com/infiniflow/ragflow/blob/main/docs/guides/models/deploy_local_llm.mdx',
- [LLMFactory.Xinference]:
- 'https://inference.readthedocs.io/en/latest/user_guide',
- [LLMFactory.ModelScope]:
- 'https://www.modelscope.cn/docs/model-service/API-Inference/intro',
- [LLMFactory.LocalAI]: 'https://localai.io/docs/getting-started/models/',
- [LLMFactory.LMStudio]: 'https://lmstudio.ai/docs/basics',
- [LLMFactory.OpenAiAPICompatible]:
- 'https://platform.openai.com/docs/models/gpt-4',
- [LLMFactory.RAGcon]: 'https://www.ragcon.ai/erste-schritte-mit-ragflow/',
- [LLMFactory.TogetherAI]: 'https://docs.together.ai/docs/deployment-options',
- [LLMFactory.Replicate]: 'https://replicate.com/docs/topics/deployments',
- [LLMFactory.OpenRouter]: 'https://openrouter.ai/docs',
- [LLMFactory.HuggingFace]:
- 'https://huggingface.co/docs/text-embeddings-inference/quick_tour',
- [LLMFactory.GPUStack]: 'https://docs.gpustack.ai/latest/quickstart',
- [LLMFactory.VLLM]: 'https://docs.vllm.ai/en/latest/',
- [LLMFactory.TokenPony]: 'https://docs.tokenpony.cn/#/',
-};
-
-function buildModelTypesWithVision(
- modelType: string[] | string,
- vision = false,
-): string[] {
- const modelTypeArray = Array.isArray(modelType) ? modelType : [modelType];
-
- if (modelTypeArray.includes('chat') && vision) {
- return [...modelTypeArray, 'image2text'];
- }
-
- return modelTypeArray;
-}
-
-const OllamaModal = ({
- visible,
- hideModal,
- onOk,
- onVerify,
- loading,
- llmFactory,
- editMode = false,
- initialValues,
-}: IModalProps<
- Partial & { provider_order?: string }
-> & {
- llmFactory: string;
- editMode?: boolean;
- onVerify?: (
- postBody: any,
- ) => Promise;
-}) => {
- const { t } = useTranslate('setting');
- const { t: tc } = useCommonTranslation();
- const { buildModelTypeOptions } = useBuildModelTypeOptions();
- const formRef = useRef(null);
- const { instanceNameSet } = useFetchInstanceNameSet(llmFactory);
-
- const hideWhenInstanceExists = useHideWhenInstanceExists(instanceNameSet);
-
- const optionsMap: Partial<
- Record
- > & {
- Default: { label: string; value: string }[];
- } = {
- [LLMFactory.HuggingFace]: buildModelTypeOptions([
- 'embedding',
- 'chat',
- 'rerank',
- ]),
- [LLMFactory.LMStudio]: buildModelTypeOptions([
- 'chat',
- 'embedding',
- 'image2text',
- ]),
- [LLMFactory.Xinference]: buildModelTypeOptions([
- 'chat',
- 'embedding',
- 'rerank',
- 'image2text',
- 'speech2text',
- 'tts',
- ]),
- [LLMFactory.RAGcon]: buildModelTypeOptions([
- 'chat',
- 'embedding',
- 'rerank',
- 'image2text',
- 'speech2text',
- 'tts',
- ]),
- [LLMFactory.ModelScope]: buildModelTypeOptions(['chat']),
- [LLMFactory.GPUStack]: buildModelTypeOptions([
- 'chat',
- 'embedding',
- 'rerank',
- 'speech2text',
- 'tts',
- ]),
- [LLMFactory.OpenRouter]: buildModelTypeOptions(['chat', 'image2text']),
- Default: buildModelTypeOptions([
- 'chat',
- 'embedding',
- 'rerank',
- 'image2text',
- ]),
- };
-
- const url =
- llmFactoryToUrlMap[llmFactory as LLMFactory] ||
- 'https://github.com/infiniflow/ragflow/blob/main/docs/guides/models/deploy_local_llm.mdx';
-
- const fields = useMemo(() => {
- const getOptions = (factory: string) => {
- return optionsMap[factory as LLMFactory] || optionsMap.Default;
- };
- const defaultToolCallEnabled = initialValues?.is_tools ?? false;
-
- const baseFields: FormFieldConfig[] = [
- {
- name: 'instance_name',
- label: t('instanceName'),
- type: FormFieldType.Text,
- required: true,
- placeholder: t('instanceNameMessage'),
- tooltip: t('instanceNameTip'),
- validation: {
- message: t('instanceNameMessage'),
- },
- },
- {
- name: 'model_type',
- label: t('modelType'),
- type: FormFieldType.MultiSelect,
- required: true,
- options: getOptions(llmFactory),
- },
- {
- name: 'llm_name',
- label: t(llmFactory === 'Xinference' ? 'modelUid' : 'modelName'),
- type: FormFieldType.Text,
- required: true,
- placeholder: t('modelNameMessage'),
- validation: {
- message: t('modelNameMessage'),
- },
- },
- {
- name: 'api_base',
- label: t('addLlmBaseUrl'),
- type: FormFieldType.Text,
- required: true,
- placeholder: t('baseUrlNameMessage'),
- validation: {
- message: t('baseUrlNameMessage'),
- },
- shouldRender: hideWhenInstanceExists,
- },
- {
- name: 'api_key',
- label: t('apiKey'),
- type: FormFieldType.Text,
- required: false,
- placeholder: t('apiKeyMessage'),
- shouldRender: hideWhenInstanceExists,
- },
- {
- name: 'max_tokens',
- label: t('maxTokens'),
- type: FormFieldType.Number,
- required: true,
- placeholder: t('maxTokensTip'),
- validation: {
- message: t('maxTokensMessage'),
- },
- customValidate: (value: any) => {
- if (value !== undefined && value !== null && value !== '') {
- if (typeof value !== 'number') {
- return t('maxTokensInvalidMessage');
- }
- if (value < 0) {
- return t('maxTokensMinMessage');
- }
- }
- return true;
- },
- },
- ];
-
- baseFields.push({
- name: 'is_tools',
- label: t('enableToolCall'),
- type: FormFieldType.Switch,
- required: false,
- dependencies: ['model_type'],
- shouldRender: (formValues: any) => {
- const modelType = formValues?.model_type;
- if (Array.isArray(modelType)) {
- return modelType.includes('chat') || modelType.includes('image2text');
- }
- return modelType === 'chat' || modelType === 'image2text';
- },
- tooltip: t('enableToolCallTip'),
- defaultValue: defaultToolCallEnabled,
- });
-
- // Add provider_order field only for OpenRouter
- if (llmFactory === 'OpenRouter') {
- baseFields.push({
- name: 'provider_order',
- label: 'Provider Order',
- type: FormFieldType.Text,
- required: false,
- tooltip: 'Comma-separated provider list, e.g. Groq,Fireworks',
- placeholder: 'Groq,Fireworks',
- });
- }
-
- // Add vision switch (conditional on model_type === 'chat')
- baseFields.push({
- name: 'vision',
- label: t('vision'),
- type: FormFieldType.Switch,
- required: false,
- dependencies: ['model_type'],
- shouldRender: (formValues: any) => {
- const modelType = formValues?.model_type;
- if (Array.isArray(modelType)) {
- return modelType.includes('chat');
- }
- return modelType === 'chat';
- },
- });
-
- return baseFields;
- }, [llmFactory, t, hideWhenInstanceExists, initialValues?.is_tools]);
-
- const defaultValues: FieldValues = useMemo(() => {
- if (editMode && initialValues) {
- return {
- instance_name: initialValues.instance_name || '',
- llm_name: initialValues.llm_name || '',
- model_type: initialValues.model_type
- ? initialValues.model_type
- : ['chat'],
- api_base: initialValues.api_base || '',
- max_tokens: initialValues.max_tokens || 8192,
- api_key: '',
- vision: initialValues.model_type === 'image2text',
- provider_order: initialValues.provider_order || '',
- is_tools: initialValues.is_tools || false,
- };
- }
- return {
- instance_name: '',
- model_type: [
- llmFactory === LLMFactory.Ollama || llmFactory === LLMFactory.VLLM
- ? 'chat'
- : llmFactory in optionsMap
- ? optionsMap[llmFactory as LLMFactory]?.at(0)?.value
- : 'embedding',
- ],
- vision: false,
- is_tools: false,
- max_tokens: 8192,
- };
- }, [editMode, initialValues, llmFactory]);
-
- const handleOk = async (values?: FieldValues) => {
- if (!values) return;
-
- const modelTypeArray: string[] = Array.isArray(values.model_type)
- ? values.model_type
- : [values.model_type];
- const supportsToolCall =
- modelTypeArray.includes('chat') || modelTypeArray.includes('image2text');
-
- const data: IAddProviderInstanceRequestBody & { provider_order?: string } =
- {
- instance_name: values.instance_name as string,
- llm_factory: llmFactory,
- llm_name: values.llm_name as string,
- model_type: buildModelTypesWithVision(values.model_type, values.vision),
- api_base: values.api_base as string,
- api_key: values.api_key as string,
- max_tokens: values.max_tokens as number,
- };
- if (supportsToolCall) {
- data.is_tools = Boolean(values.is_tools);
- }
-
- // Add provider_order only if it exists (for OpenRouter)
- if (values.provider_order) {
- data.provider_order = values.provider_order as string;
- }
-
- await onOk?.(data);
- };
-
- const verifyParamsFunc = useCallback(() => {
- const values = formRef.current?.getValues();
- return {
- llm_factory: llmFactory,
- model_type: buildModelTypesWithVision(values.model_type, values.vision),
- };
- }, [llmFactory]);
-
- const handleVerify = useCallback(
- async (params: any) => {
- const verifyParams = verifyParamsFunc();
- const res = await onVerify?.({ ...params, ...verifyParams });
- return (res || { isValid: null, logs: '' }) as VerifyResult;
- },
- [verifyParamsFunc, onVerify],
- );
-
- return (
- }
- open={visible || false}
- onOpenChange={(open) => !open && hideModal?.()}
- maskClosable={false}
- footer={<>>}
- footerClassName="py-1"
- >
- {}}
- defaultValues={defaultValues}
- labelClassName="font-normal"
- >
- {onVerify && (
-
- )}
-
-
-
- );
-};
-
-export default memo(OllamaModal);
diff --git a/web/src/pages/user-setting/setting-model/modal/opendataloader-modal/index.tsx b/web/src/pages/user-setting/setting-model/modal/opendataloader-modal/index.tsx
deleted file mode 100644
index 7b919aeb65..0000000000
--- a/web/src/pages/user-setting/setting-model/modal/opendataloader-modal/index.tsx
+++ /dev/null
@@ -1,156 +0,0 @@
-import { RAGFlowFormItem } from '@/components/ragflow-form';
-import { Button, ButtonLoading } from '@/components/ui/button';
-import {
- Dialog,
- DialogContent,
- DialogFooter,
- DialogHeader,
- DialogTitle,
-} from '@/components/ui/dialog';
-import { Form } from '@/components/ui/form';
-import { Input } from '@/components/ui/input';
-import { LLMFactory } from '@/constants/llm';
-import { VerifyResult } from '@/pages/user-setting/setting-model/hooks';
-import { zodResolver } from '@hookform/resolvers/zod';
-import { memo, useEffect, useMemo } from 'react';
-import { useForm } from 'react-hook-form';
-import { useTranslation } from 'react-i18next';
-import { z } from 'zod';
-import { LLMHeader } from '../../components/llm-header';
-import VerifyButton from '../verify-button';
-
-export type OpenDataLoaderFormValues = {
- instance_name: string;
- llm_name: string;
- opendataloader_apiserver: string;
- opendataloader_api_key?: string;
-};
-
-export interface IModalProps {
- visible: boolean;
- hideModal: () => void;
- onOk?: (data: T) => Promise;
- onVerify?: (
- postBody: any,
- ) => Promise;
- loading?: boolean;
-}
-
-const OpenDataLoaderModal = ({
- visible,
- hideModal,
- onOk,
- onVerify,
- loading,
-}: IModalProps) => {
- const { t } = useTranslation();
-
- const FormSchema = useMemo(
- () =>
- z.object({
- instance_name: z.string().min(1, {
- message: t('setting.instanceNameMessage'),
- }),
- llm_name: z.string().min(1, {
- message: t('setting.modelNameMessage'),
- }),
- opendataloader_apiserver: z.string().min(1, {
- message: t('setting.apiServerMessage'),
- }),
- opendataloader_api_key: z.string().optional(),
- }),
- [t],
- );
-
- const form = useForm({
- resolver: zodResolver(FormSchema),
- defaultValues: {
- instance_name: '',
- opendataloader_apiserver: '',
- opendataloader_api_key: '',
- },
- });
-
- const handleOk = async (values: OpenDataLoaderFormValues) => {
- const ret = await onOk?.(values as any);
- if (ret) {
- hideModal?.();
- }
- };
-
- useEffect(() => {
- if (!visible) {
- form.reset();
- }
- }, [visible, form]);
-
- return (
-
-
-
-
-
-
-
-
-
-
-
- {t('common.cancel')}
-
-
- {t('common.add')}
-
-
-
-
- );
-};
-
-export default memo(OpenDataLoaderModal);
diff --git a/web/src/pages/user-setting/setting-model/modal/paddleocr-modal/index.tsx b/web/src/pages/user-setting/setting-model/modal/paddleocr-modal/index.tsx
deleted file mode 100644
index b39fce9434..0000000000
--- a/web/src/pages/user-setting/setting-model/modal/paddleocr-modal/index.tsx
+++ /dev/null
@@ -1,168 +0,0 @@
-import { RAGFlowFormItem } from '@/components/ragflow-form';
-import { Button, ButtonLoading } from '@/components/ui/button';
-import {
- Dialog,
- DialogContent,
- DialogFooter,
- DialogHeader,
- DialogTitle,
-} from '@/components/ui/dialog';
-import { Form } from '@/components/ui/form';
-import { Input } from '@/components/ui/input';
-import { RAGFlowSelect, RAGFlowSelectOptionType } from '@/components/ui/select';
-import { LLMFactory } from '@/constants/llm';
-import { VerifyResult } from '@/pages/user-setting/setting-model/hooks';
-import { zodResolver } from '@hookform/resolvers/zod';
-import { t } from 'i18next';
-import { memo, useEffect } from 'react';
-import { useForm } from 'react-hook-form';
-import { useTranslation } from 'react-i18next';
-import { z } from 'zod';
-import { LLMHeader } from '../../components/llm-header';
-import VerifyButton from '../verify-button';
-
-const FormSchema = z.object({
- instance_name: z.string().min(1, {
- message: t('setting.instanceNameMessage'),
- }),
- llm_name: z.string().min(1, {
- message: t('setting.paddleocr.modelNameRequired'),
- }),
- paddleocr_api_url: z.string().min(1, {
- message: t('setting.paddleocr.apiUrlRequired'),
- }),
- paddleocr_access_token: z.string().optional(),
- paddleocr_algorithm: z.string().default('PaddleOCR-VL'),
-});
-
-export type PaddleOCRFormValues = z.infer;
-
-export interface IModalProps {
- visible: boolean;
- hideModal: () => void;
- onOk?: (data: T) => Promise;
- onVerify?: (
- postBody: any,
- ) => Promise;
- loading?: boolean;
-}
-
-const algorithmOptions: RAGFlowSelectOptionType[] = [
- { label: 'PaddleOCR-VL-1.5', value: 'PaddleOCR-VL-1.5' },
- { label: 'PaddleOCR-VL', value: 'PaddleOCR-VL' },
- { label: 'PP-OCRv5', value: 'PP-OCRv5' },
- { label: 'PP-StructureV3', value: 'PP-StructureV3' },
-];
-
-const PaddleOCRModal = ({
- visible,
- hideModal,
- onOk,
- onVerify,
- loading,
-}: IModalProps) => {
- const { t } = useTranslation();
-
- const form = useForm({
- resolver: zodResolver(FormSchema),
- defaultValues: {
- instance_name: '',
- paddleocr_algorithm: 'PaddleOCR-VL',
- },
- });
-
- const handleOk = async (values: PaddleOCRFormValues) => {
- const ret = await onOk?.(values as any);
- if (ret) {
- hideModal?.();
- }
- };
-
- useEffect(() => {
- if (!visible) {
- form.reset();
- }
- }, [visible, form]);
-
- return (
-
-
-
-
-
-
-
-
-
-
-
- );
-};
-
-export default memo(PaddleOCRModal);
diff --git a/web/src/pages/user-setting/setting-model/modal/provider-modal/constants.ts b/web/src/pages/user-setting/setting-model/modal/provider-modal/constants.ts
new file mode 100644
index 0000000000..751788d673
--- /dev/null
+++ b/web/src/pages/user-setting/setting-model/modal/provider-modal/constants.ts
@@ -0,0 +1,49 @@
+import { LLMFactory } from '@/constants/llm';
+
+/**
+ * Provider factories that opt into the "List Models" picker UI.
+ *
+ * For these factories, the modal hides the traditional model_name,
+ * model_type, max_tokens, and is_tools form fields and instead shows a
+ * "List Models" button that fetches available models from the provider's
+ * `/providers//models` endpoint. The user can multi-select models
+ * from the response; each selected model is converted to an `IModelInfo`
+ * entry and submitted as `model_info`.
+ *
+ * For all other factories the picker is hidden and the form renders the
+ * 4 model_* fields directly.
+ */
+export const LIST_MODEL_PROVIDERS = new Set([
+ LLMFactory.HuggingFace,
+ LLMFactory.Ollama,
+ LLMFactory.GoogleCloud,
+ LLMFactory.OpenRouter,
+ LLMFactory.VLLM,
+ LLMFactory.OpenAiAPICompatible,
+ LLMFactory.LMStudio,
+ LLMFactory.VolcEngine,
+ LLMFactory.Xinference,
+ LLMFactory.LocalAI,
+ LLMFactory.BaiduYiYan,
+ LLMFactory.TencentCloud,
+ LLMFactory.XunFeiSpark,
+ LLMFactory.GPUStack,
+ LLMFactory.FishAudio,
+ LLMFactory.MinerU,
+ LLMFactory.PaddleOCR,
+]);
+
+/**
+ * The set of form-field names that are owned by the list-models picker
+ * (not registered in the dynamic form when the picker is active).
+ *
+ * Doubles as the whitelist of fields that remain editable in viewMode —
+ * in viewMode every other field is disabled so only model-related edits
+ * are possible.
+ */
+export const LIST_MODEL_FIELD_NAMES = new Set([
+ 'model_name',
+ 'model_type',
+ 'max_tokens',
+ 'is_tools',
+]);
diff --git a/web/src/pages/user-setting/setting-model/modal/provider-modal/field-config/generic-api-key-config.ts b/web/src/pages/user-setting/setting-model/modal/provider-modal/field-config/generic-api-key-config.ts
new file mode 100644
index 0000000000..38c947ef9d
--- /dev/null
+++ b/web/src/pages/user-setting/setting-model/modal/provider-modal/field-config/generic-api-key-config.ts
@@ -0,0 +1,82 @@
+import { FormFieldType } from '@/components/dynamic-form';
+import { LLMFactory } from '@/constants/llm';
+import type { ProviderConfig } from '../types';
+
+/**
+ * Generic ApiKey configuration (used for factories not in ProviderConfigMap)
+ */
+export const GenericApiKeyConfig: ProviderConfig = {
+ llmFactory: '__generic__',
+ title: 'API Key',
+ fields: [
+ {
+ name: 'instance_name',
+ label: 'instanceName',
+ type: FormFieldType.Text,
+ required: true,
+ placeholder: 'instanceNameMessage',
+ tooltip: 'instanceNameTip',
+ validation: { message: 'instanceNameMessage' },
+ },
+ {
+ name: 'api_key',
+ label: 'apiKey',
+ type: FormFieldType.Text,
+ required: true,
+ placeholder: 'apiKeyMessage',
+ validation: { message: 'apiKeyMessage' },
+ },
+ {
+ name: 'base_url',
+ label: 'baseUrl',
+ type: 'inputSelect',
+ required: false,
+ tooltip: (factory) => {
+ if (factory === LLMFactory.MiniMax) return 'minimaxBaseUrlTip';
+ if (factory === LLMFactory.TongYiQianWen) return 'tongyiBaseUrlTip';
+ if (factory === LLMFactory.SILICONFLOW) return 'siliconBaseUrlTip';
+ return 'baseUrlTip';
+ },
+ placeholder: (factory) => {
+ if (factory === LLMFactory.MiniMax) return 'minimaxBaseUrlPlaceholder';
+ if (factory === LLMFactory.TongYiQianWen)
+ return 'tongyiBaseUrlPlaceholder';
+ if (factory === LLMFactory.SILICONFLOW)
+ return 'siliconflowBaseUrlPlaceholder';
+ if (factory?.toLowerCase() === 'Anthropic')
+ return 'anthropicBaseUrlPlaceholder';
+ return 'openaiBaseUrlPlaceholder';
+ },
+ shouldRender: 'showBaseUrl',
+ },
+ {
+ name: 'group_id',
+ label: 'groupId',
+ type: FormFieldType.Text,
+ required: false,
+ shouldRender: 'showGroupId',
+ },
+ ],
+ verifyTransform: (values) => ({
+ apiKey: values.api_key,
+ baseUrl: values.base_url,
+ }),
+ submitTransform: (values) => ({
+ instance_name: values.instance_name,
+ api_key: values.api_key,
+ api_base: values.base_url || '',
+ group_id: values.group_id,
+ max_tokens: 0,
+ }),
+};
+
+/**
+ * List of factories supporting base_url (used for the generic ApiKey modal)
+ */
+export const FACTORIES_WITH_BASE_URL = [
+ LLMFactory.OpenAI,
+ LLMFactory.AzureOpenAI,
+ LLMFactory.TongYiQianWen,
+ LLMFactory.MiniMax,
+ LLMFactory.SILICONFLOW,
+];
diff --git a/web/src/pages/user-setting/setting-model/modal/provider-modal/field-config/get-provider-config.ts b/web/src/pages/user-setting/setting-model/modal/provider-modal/field-config/get-provider-config.ts
new file mode 100644
index 0000000000..027e1619d4
--- /dev/null
+++ b/web/src/pages/user-setting/setting-model/modal/provider-modal/field-config/get-provider-config.ts
@@ -0,0 +1,28 @@
+import type { ProviderConfig } from '../types';
+import { GenericApiKeyConfig } from './generic-api-key-config';
+import { LocalLlmConfigs } from './local-llm-configs';
+import { ProviderConfigMap } from './provider-config-map';
+
+/**
+ * Get the configuration for the given factory
+ * First look up in ProviderConfigMap, then LocalLlmConfigs, finally fall back to GenericApiKeyConfig
+ */
+export function getProviderConfig(llmFactory: string): ProviderConfig {
+ // Check whether it is a special factory (11 in ModalMap)
+ // Among which AzureOpenAI/VolcEngine/GoogleCloud/TencentCloud/XunFeiSpark/BaiduYiYan/FishAudio are in ProviderConfigMap
+ // Bedrock/MinerU/PaddleOCR/OpenDataLoader are out of the merge scope and use the original modal
+
+ if (ProviderConfigMap[llmFactory]) {
+ return ProviderConfigMap[llmFactory];
+ }
+
+ if (LocalLlmConfigs[llmFactory]) {
+ return LocalLlmConfigs[llmFactory];
+ }
+
+ // Generic ApiKey modal
+ return {
+ ...GenericApiKeyConfig,
+ llmFactory,
+ };
+}
diff --git a/web/src/pages/user-setting/setting-model/modal/provider-modal/field-config/index.ts b/web/src/pages/user-setting/setting-model/modal/provider-modal/field-config/index.ts
new file mode 100644
index 0000000000..1ef256145a
--- /dev/null
+++ b/web/src/pages/user-setting/setting-model/modal/provider-modal/field-config/index.ts
@@ -0,0 +1,5 @@
+// Public entry point for the field-config folder.
+// Preserves the previous `./field-config` import path used by provider-modal/index.tsx.
+
+export { FACTORIES_WITH_BASE_URL } from './generic-api-key-config';
+export { getProviderConfig } from './get-provider-config';
diff --git a/web/src/pages/user-setting/setting-model/modal/provider-modal/field-config/local-llm-configs.ts b/web/src/pages/user-setting/setting-model/modal/provider-modal/field-config/local-llm-configs.ts
new file mode 100644
index 0000000000..5f175c3874
--- /dev/null
+++ b/web/src/pages/user-setting/setting-model/modal/provider-modal/field-config/local-llm-configs.ts
@@ -0,0 +1,202 @@
+import { FormFieldType } from '@/components/dynamic-form';
+import { LLMFactory } from '@/constants/llm';
+import type { FieldConfig, ProviderConfig } from '../types';
+import { capitalize } from './utils';
+
+/**
+ * Factory configuration for local/compatible factories
+ * Used for scenarios after OllamaModal merge
+ */
+export const LocalLlmConfigs: Record = {
+ [LLMFactory.Ollama]: buildLocalConfig(LLMFactory.Ollama, 'Ollama', [
+ 'chat',
+ 'embedding',
+ 'rerank',
+ 'image2text',
+ ]),
+ [LLMFactory.Xinference]: buildLocalConfig(
+ LLMFactory.Xinference,
+ 'Xinference',
+ ['chat', 'embedding', 'rerank', 'image2text', 'speech2text', 'tts'],
+ 'modelUid',
+ ),
+ [LLMFactory.ModelScope]: buildLocalConfig(
+ LLMFactory.ModelScope,
+ 'ModelScope',
+ ['chat'],
+ ),
+ [LLMFactory.LocalAI]: buildLocalConfig(LLMFactory.LocalAI, 'LocalAI', [
+ 'chat',
+ 'embedding',
+ 'rerank',
+ 'image2text',
+ ]),
+ [LLMFactory.LMStudio]: buildLocalConfig(LLMFactory.LMStudio, 'LMStudio', [
+ 'chat',
+ 'embedding',
+ 'image2text',
+ ]),
+ [LLMFactory.OpenAiAPICompatible]: buildLocalConfig(
+ LLMFactory.OpenAiAPICompatible,
+ 'OpenAiAPICompatible',
+ ['chat', 'embedding', 'rerank', 'image2text'],
+ ),
+ [LLMFactory.RAGcon]: buildLocalConfig(LLMFactory.RAGcon, 'RAGcon', [
+ 'chat',
+ 'embedding',
+ 'rerank',
+ 'image2text',
+ 'speech2text',
+ 'tts',
+ ]),
+ [LLMFactory.TogetherAI]: buildLocalConfig(
+ LLMFactory.TogetherAI,
+ 'TogetherAI',
+ ['chat', 'embedding', 'rerank', 'image2text'],
+ ),
+ [LLMFactory.Replicate]: buildLocalConfig(LLMFactory.Replicate, 'Replicate', [
+ 'chat',
+ 'embedding',
+ 'rerank',
+ 'image2text',
+ ]),
+ [LLMFactory.OpenRouter]: buildLocalConfig(
+ LLMFactory.OpenRouter,
+ 'OpenRouter',
+ ['chat', 'image2text'],
+ undefined,
+ true,
+ ),
+ [LLMFactory.HuggingFace]: buildLocalConfig(
+ LLMFactory.HuggingFace,
+ 'HuggingFace',
+ ['embedding', 'chat', 'rerank'],
+ ),
+ [LLMFactory.GPUStack]: buildLocalConfig(LLMFactory.GPUStack, 'GPUStack', [
+ 'chat',
+ 'embedding',
+ 'rerank',
+ 'speech2text',
+ 'tts',
+ ]),
+ [LLMFactory.VLLM]: buildLocalConfig(LLMFactory.VLLM, 'VLLM', [
+ 'chat',
+ 'embedding',
+ 'rerank',
+ 'image2text',
+ ]),
+ [LLMFactory.TokenPony]: buildLocalConfig(LLMFactory.TokenPony, 'TokenPony', [
+ 'chat',
+ 'embedding',
+ 'rerank',
+ 'image2text',
+ ]),
+};
+
+/**
+ * Build the default configuration for local factories
+ */
+function buildLocalConfig(
+ llmFactory: string,
+ title: string,
+ modelTypes: string[],
+ modelNameLabel?: string,
+ addProviderOrder = false,
+): ProviderConfig {
+ const fields: FieldConfig[] = [
+ {
+ name: 'instance_name',
+ label: 'instanceName',
+ type: FormFieldType.Text,
+ required: true,
+ placeholder: 'instanceNameMessage',
+ tooltip: 'instanceNameTip',
+ },
+ {
+ name: 'model_type',
+ label: 'modelType',
+ type: FormFieldType.MultiSelect,
+ required: true,
+ options: modelTypes.map((t) => ({ label: capitalize(t), value: t })),
+ },
+ {
+ name: 'model_name',
+ label: modelNameLabel ?? 'modelName',
+ type: FormFieldType.Text,
+ required: true,
+ placeholder: 'modelNameMessage',
+ },
+ {
+ name: 'base_url',
+ label: 'addLlmBaseUrl',
+ type: 'inputSelect',
+ required: true,
+ placeholder: 'baseUrlNameMessage',
+ shouldRender: 'hideWhenInstanceExists',
+ },
+ {
+ name: 'api_key',
+ label: 'apiKey',
+ type: FormFieldType.Text,
+ required: false,
+ placeholder: 'apiKeyMessage',
+ shouldRender: 'hideWhenInstanceExists',
+ },
+ {
+ name: 'max_tokens',
+ label: 'maxTokens',
+ type: FormFieldType.Number,
+ required: true,
+ placeholder: 'maxTokensTip',
+ defaultValue: 8192,
+ validation: { min: 0, message: 'maxTokensMessage' },
+ },
+ {
+ name: 'is_tools',
+ label: 'enableToolCall',
+ type: FormFieldType.Switch,
+ required: false,
+ shouldRender: 'modelTypeSupportsToolCall',
+ defaultValue: false,
+ },
+ ];
+
+ if (addProviderOrder) {
+ fields.push({
+ name: 'provider_order',
+ label: 'providerOrder',
+ type: FormFieldType.Text,
+ required: false,
+ });
+ }
+
+ fields.push({
+ name: 'vision',
+ label: 'vision',
+ type: FormFieldType.Switch,
+ required: false,
+ defaultValue: false,
+ shouldRender: 'modelTypeIncludesChat',
+ });
+
+ return {
+ llmFactory,
+ title,
+ fields,
+ verifyTransform: (values, modelInfo) => ({
+ apiKey: values.api_key || '',
+ baseUrl: values.base_url,
+ modelInfo,
+ }),
+ submitTransform: (values, modelInfo) => ({
+ instance_name: values.instance_name,
+ llm_factory: llmFactory,
+ model_info: modelInfo,
+ api_base: values.base_url,
+ api_key: values.api_key,
+ ...(values.provider_order
+ ? { provider_order: values.provider_order }
+ : {}),
+ }),
+ };
+}
diff --git a/web/src/pages/user-setting/setting-model/modal/provider-modal/field-config/provider-config-map.ts b/web/src/pages/user-setting/setting-model/modal/provider-modal/field-config/provider-config-map.ts
new file mode 100644
index 0000000000..ce18a048f5
--- /dev/null
+++ b/web/src/pages/user-setting/setting-model/modal/provider-modal/field-config/provider-config-map.ts
@@ -0,0 +1,873 @@
+import { FormFieldType } from '@/components/dynamic-form';
+import { LLMFactory } from '@/constants/llm';
+import type { ProviderConfig } from '../types';
+
+/**
+ * Factory configuration mapping table
+ * key: LLMFactory value
+ * value: ProviderConfig
+ */
+export const ProviderConfigMap: Record = {
+ // ============ Azure OpenAI ============
+ [LLMFactory.AzureOpenAI]: {
+ llmFactory: LLMFactory.AzureOpenAI,
+ title: 'Azure OpenAI',
+ fields: [
+ {
+ name: 'instance_name',
+ label: 'instanceName',
+ type: FormFieldType.Text,
+ required: true,
+ placeholder: 'instanceNameMessage',
+ tooltip: 'instanceNameTip',
+ validation: { message: 'instanceNameMessage' },
+ },
+ {
+ name: 'model_type',
+ label: 'modelType',
+ type: FormFieldType.MultiSelect,
+ required: true,
+ options: [
+ { label: 'Chat', value: 'chat' },
+ { label: 'Embedding', value: 'embedding' },
+ { label: 'Image2Text', value: 'image2text' },
+ ],
+ defaultValue: ['embedding'],
+ },
+ {
+ name: 'api_base',
+ label: 'addLlmBaseUrl',
+ type: 'inputSelect',
+ required: true,
+ placeholder: 'baseUrlNameMessage',
+ shouldRender: 'hideWhenInstanceExists',
+ validation: { message: 'baseUrlNameMessage' },
+ },
+ {
+ name: 'api_key',
+ label: 'apiKey',
+ type: FormFieldType.Text,
+ required: false,
+ placeholder: 'apiKeyMessage',
+ shouldRender: 'hideWhenInstanceExists',
+ validation: { message: 'apiKeyMessage' },
+ },
+ {
+ name: 'model_name',
+ label: 'modelName',
+ type: FormFieldType.Text,
+ required: true,
+ placeholder: 'modelNameMessage',
+ defaultValue: 'gpt-3.5-turbo',
+ validation: { message: 'modelNameMessage' },
+ },
+ {
+ name: 'api_version',
+ label: 'apiVersion',
+ type: FormFieldType.Text,
+ required: false,
+ placeholder: 'apiVersionMessage',
+ defaultValue: '2024-02-01',
+ },
+ {
+ name: 'max_tokens',
+ label: 'maxTokens',
+ type: FormFieldType.Number,
+ required: true,
+ placeholder: 'maxTokensTip',
+ defaultValue: 8192,
+ validation: { min: 0, message: 'maxTokensMessage' },
+ },
+ {
+ name: 'vision',
+ label: 'vision',
+ type: FormFieldType.Switch,
+ defaultValue: false,
+ shouldRender: 'modelTypeIncludesChat',
+ },
+ ],
+ verifyTransform: (values, modelInfo) => ({
+ apiKey: values.api_key,
+ baseUrl: values.api_base,
+ modelInfo,
+ }),
+ submitTransform: (values, modelInfo) => ({
+ instance_name: values.instance_name,
+ llm_factory: LLMFactory.AzureOpenAI,
+ api_base: values.api_base,
+ api_key: values.api_key,
+ api_version: values.api_version,
+ model_info: modelInfo,
+ }),
+ },
+
+ // ============ VolcEngine ============
+ [LLMFactory.VolcEngine]: {
+ llmFactory: LLMFactory.VolcEngine,
+ title: 'VolcEngine',
+ docLink: 'https://www.volcengine.com/docs/82379/1302008',
+ docLinkI18nKey: 'ollamaLink',
+ fields: [
+ {
+ name: 'instance_name',
+ label: 'instanceName',
+ type: FormFieldType.Text,
+ required: true,
+ placeholder: 'instanceNameMessage',
+ tooltip: 'instanceNameTip',
+ validation: { message: 'instanceNameMessage' },
+ },
+ {
+ name: 'model_type',
+ label: 'modelType',
+ type: FormFieldType.MultiSelect,
+ required: true,
+ options: [
+ { label: 'Chat', value: 'chat' },
+ { label: 'Embedding', value: 'embedding' },
+ { label: 'Image2Text', value: 'image2text' },
+ ],
+ defaultValue: ['chat'],
+ },
+ // {
+ // name: 'model_name',
+ // label: 'modelName',
+ // type: 'text',
+ // required: true,
+ // placeholder: 'volcModelNameMessage',
+ // validation: { message: 'volcModelNameMessage' },
+ // },
+ {
+ name: 'model_name',
+ label: 'addEndpointID',
+ type: FormFieldType.Text,
+ required: true,
+ placeholder: 'endpointIDMessage',
+ shouldRender: 'hideWhenInstanceExists',
+ validation: { message: 'endpointIDMessage' },
+ },
+ {
+ name: 'api_key',
+ label: 'addArkApiKey',
+ type: FormFieldType.Text,
+ required: true,
+ placeholder: 'ArkApiKeyMessage',
+ shouldRender: 'hideWhenInstanceExists',
+ validation: { message: 'ArkApiKeyMessage' },
+ },
+ {
+ name: 'max_tokens',
+ label: 'maxTokens',
+ type: FormFieldType.Number,
+ required: true,
+ placeholder: 'maxTokensTip',
+ defaultValue: 8192,
+ validation: { min: 0 },
+ },
+ ],
+ verifyTransform: (values, modelInfo) => ({
+ apiKey: JSON.stringify({
+ ark_api_key: values.api_key,
+ endpoint_id: values.endpoint_id,
+ }),
+ modelInfo,
+ }),
+ submitTransform: (values, modelInfo) => ({
+ instance_name: values.instance_name,
+ llm_factory: LLMFactory.VolcEngine,
+ endpoint_id: values.endpoint_id,
+ ark_api_key: values.api_key,
+ model_info: modelInfo,
+ }),
+ },
+
+ // ============ Google Cloud ============
+ [LLMFactory.GoogleCloud]: {
+ llmFactory: LLMFactory.GoogleCloud,
+ title: 'Google Cloud',
+ fields: [
+ {
+ name: 'instance_name',
+ label: 'instanceName',
+ type: FormFieldType.Text,
+ required: true,
+ placeholder: 'instanceNameMessage',
+ tooltip: 'instanceNameTip',
+ validation: { message: 'instanceNameMessage' },
+ },
+ {
+ name: 'model_type',
+ label: 'modelType',
+ type: FormFieldType.MultiSelect,
+ required: true,
+ options: [
+ { label: 'Chat', value: 'chat' },
+ { label: 'Image2Text', value: 'image2text' },
+ ],
+ defaultValue: ['chat'],
+ },
+ {
+ name: 'model_name',
+ label: 'modelID',
+ type: FormFieldType.Text,
+ required: true,
+ placeholder: 'GoogleModelIDMessage',
+ validation: { message: 'GoogleModelIDMessage' },
+ },
+ {
+ name: 'google_project_id',
+ label: 'addGoogleProjectID',
+ type: FormFieldType.Text,
+ required: true,
+ placeholder: 'GoogleProjectIDMessage',
+ shouldRender: 'hideWhenInstanceExists',
+ validation: { message: 'GoogleProjectIDMessage' },
+ },
+ {
+ name: 'google_region',
+ label: 'addGoogleRegion',
+ type: FormFieldType.Text,
+ required: true,
+ placeholder: 'GoogleRegionMessage',
+ shouldRender: 'hideWhenInstanceExists',
+ validation: { message: 'GoogleRegionMessage' },
+ },
+ {
+ name: 'google_service_account_key',
+ label: 'addGoogleServiceAccountKey',
+ type: FormFieldType.Text,
+ required: true,
+ placeholder: 'GoogleServiceAccountKeyMessage',
+ shouldRender: 'hideWhenInstanceExists',
+ validation: { message: 'GoogleServiceAccountKeyMessage' },
+ },
+ {
+ name: 'max_tokens',
+ label: 'maxTokens',
+ type: FormFieldType.Number,
+ required: true,
+ placeholder: 'maxTokensTip',
+ defaultValue: 8192,
+ validation: { min: 0, message: 'maxTokensMinMessage' },
+ },
+ ],
+ verifyTransform: (values, modelInfo) => ({
+ apiKey: JSON.stringify({
+ google_project_id: values.google_project_id,
+ google_region: values.google_region,
+ google_service_account_key: values.google_service_account_key,
+ }),
+ modelInfo,
+ }),
+ submitTransform: (values, modelInfo) => ({
+ instance_name: values.instance_name,
+ llm_factory: LLMFactory.GoogleCloud,
+ google_project_id: values.google_project_id,
+ google_region: values.google_region,
+ google_service_account_key: values.google_service_account_key,
+ model_info: modelInfo,
+ }),
+ },
+
+ // ============ Tencent Cloud ============
+ [LLMFactory.TencentCloud]: {
+ llmFactory: LLMFactory.TencentCloud,
+ title: 'Tencent Cloud',
+ docLink: 'https://cloud.tencent.com/document/api/1093/37823',
+ docLinkI18nKey: 'TencentCloudLink',
+ fields: [
+ {
+ name: 'instance_name',
+ label: 'instanceName',
+ type: FormFieldType.Text,
+ required: true,
+ placeholder: 'instanceNameMessage',
+ tooltip: 'instanceNameTip',
+ validation: { message: 'instanceNameMessage' },
+ },
+ {
+ name: 'model_type',
+ label: 'modelType',
+ type: FormFieldType.MultiSelect,
+ required: true,
+ options: [{ label: 'Speech2Text', value: 'speech2text' }],
+ defaultValue: ['speech2text'],
+ },
+ {
+ name: 'model_name',
+ label: 'modelName',
+ type: FormFieldType.Select,
+ required: true,
+ options: [
+ { label: '16k_zh', value: '16k_zh' },
+ { label: '16k_zh_large', value: '16k_zh_large' },
+ { label: '16k_multi_lang', value: '16k_multi_lang' },
+ { label: '16k_zh_dialect', value: '16k_zh_dialect' },
+ { label: '16k_en', value: '16k_en' },
+ { label: '16k_yue', value: '16k_yue' },
+ { label: '16k_zh-PY', value: '16k_zh-PY' },
+ { label: '16k_ja', value: '16k_ja' },
+ { label: '16k_ko', value: '16k_ko' },
+ { label: '16k_vi', value: '16k_vi' },
+ { label: '16k_ms', value: '16k_ms' },
+ { label: '16k_id', value: '16k_id' },
+ { label: '16k_fil', value: '16k_fil' },
+ { label: '16k_th', value: '16k_th' },
+ { label: '16k_pt', value: '16k_pt' },
+ { label: '16k_tr', value: '16k_tr' },
+ { label: '16k_ar', value: '16k_ar' },
+ { label: '16k_es', value: '16k_es' },
+ { label: '16k_hi', value: '16k_hi' },
+ { label: '16k_fr', value: '16k_fr' },
+ { label: '16k_zh_medical', value: '16k_zh_medical' },
+ { label: '16k_de', value: '16k_de' },
+ ],
+ defaultValue: '16k_zh',
+ validation: { message: 'modelNameMessage' },
+ },
+ {
+ name: 'TencentCloud_sid',
+ label: 'addTencentCloudSID',
+ type: FormFieldType.Text,
+ required: true,
+ placeholder: 'TencentCloudSIDMessage',
+ shouldRender: 'hideWhenInstanceExists',
+ validation: { message: 'TencentCloudSIDMessage' },
+ },
+ {
+ name: 'TencentCloud_sk',
+ label: 'addTencentCloudSK',
+ type: FormFieldType.Text,
+ required: true,
+ placeholder: 'TencentCloudSKMessage',
+ shouldRender: 'hideWhenInstanceExists',
+ validation: { message: 'TencentCloudSKMessage' },
+ },
+ ],
+ verifyTransform: (values, modelInfo) => ({
+ apiKey: JSON.stringify({
+ TencentCloud_sid: values.TencentCloud_sid,
+ TencentCloud_sk: values.TencentCloud_sk,
+ }),
+ modelInfo,
+ }),
+ submitTransform: (values, modelInfo) => ({
+ instance_name: values.instance_name,
+ llm_factory: LLMFactory.TencentCloud,
+ TencentCloud_sid: values.TencentCloud_sid,
+ TencentCloud_sk: values.TencentCloud_sk,
+ model_info: modelInfo,
+ }),
+ },
+
+ // ============ XunFei Spark ============
+ [LLMFactory.XunFeiSpark]: {
+ llmFactory: LLMFactory.XunFeiSpark,
+ title: 'XunFei Spark',
+ fields: [
+ {
+ name: 'instance_name',
+ label: 'instanceName',
+ type: FormFieldType.Text,
+ required: true,
+ placeholder: 'instanceNameMessage',
+ tooltip: 'instanceNameTip',
+ validation: { message: 'instanceNameMessage' },
+ },
+ {
+ name: 'model_type',
+ label: 'modelType',
+ type: FormFieldType.MultiSelect,
+ required: true,
+ options: [
+ { label: 'Chat', value: 'chat' },
+ { label: 'TTS', value: 'tts' },
+ ],
+ defaultValue: ['chat'],
+ },
+ {
+ name: 'model_name',
+ label: 'modelName',
+ type: FormFieldType.Text,
+ required: true,
+ placeholder: 'modelNameMessage',
+ validation: { message: 'modelNameMessage' },
+ },
+ {
+ name: 'spark_api_password',
+ label: 'addSparkAPIPassword',
+ type: FormFieldType.Text,
+ required: true,
+ placeholder: 'SparkAPIPasswordMessage',
+ shouldRender: 'hideWhenInstanceExists',
+ validation: { message: 'SparkAPIPasswordMessage' },
+ },
+ {
+ name: 'spark_app_id',
+ label: 'addSparkAPPID',
+ type: FormFieldType.Text,
+ required: true,
+ placeholder: 'SparkAPPIDMessage',
+ shouldRender: 'modelTypeIncludesTtsAndNotExists',
+ validation: { message: 'SparkAPPIDMessage' },
+ },
+ {
+ name: 'spark_api_secret',
+ label: 'addSparkAPISecret',
+ type: FormFieldType.Text,
+ required: true,
+ placeholder: 'SparkAPISecretMessage',
+ shouldRender: 'modelTypeIncludesTtsAndNotExists',
+ validation: { message: 'SparkAPISecretMessage' },
+ },
+ {
+ name: 'spark_api_key',
+ label: 'addSparkAPIKey',
+ type: FormFieldType.Text,
+ required: true,
+ placeholder: 'SparkAPIKeyMessage',
+ shouldRender: 'modelTypeIncludesTtsAndNotExists',
+ validation: { message: 'SparkAPIKeyMessage' },
+ },
+ {
+ name: 'max_tokens',
+ label: 'maxTokens',
+ type: FormFieldType.Number,
+ required: true,
+ placeholder: 'maxTokensTip',
+ defaultValue: 8192,
+ validation: { min: 0, message: 'maxTokensInvalidMessage' },
+ },
+ ],
+ verifyTransform: (values, modelInfo) => ({
+ apiKey: JSON.stringify({
+ spark_api_password: values.spark_api_password,
+ spark_app_id: values.spark_app_id,
+ spark_api_secret: values.spark_api_secret,
+ spark_api_key: values.spark_api_key,
+ }),
+ modelInfo,
+ }),
+ submitTransform: (values, modelInfo) => ({
+ instance_name: values.instance_name,
+ llm_factory: LLMFactory.XunFeiSpark,
+ model_info: modelInfo,
+ }),
+ },
+
+ // ============ Baidu YiYan ============
+ [LLMFactory.BaiduYiYan]: {
+ llmFactory: LLMFactory.BaiduYiYan,
+ title: 'Baidu YiYan',
+ fields: [
+ {
+ name: 'instance_name',
+ label: 'instanceName',
+ type: FormFieldType.Text,
+ required: true,
+ placeholder: 'instanceNameMessage',
+ tooltip: 'instanceNameTip',
+ validation: { message: 'instanceNameMessage' },
+ },
+ {
+ name: 'model_type',
+ label: 'modelType',
+ type: FormFieldType.MultiSelect,
+ required: true,
+ options: [
+ { label: 'Chat', value: 'chat' },
+ { label: 'Embedding', value: 'embedding' },
+ { label: 'Rerank', value: 'rerank' },
+ ],
+ defaultValue: ['chat'],
+ },
+ {
+ name: 'model_name',
+ label: 'modelName',
+ type: FormFieldType.Text,
+ required: true,
+ placeholder: 'yiyanModelNameMessage',
+ validation: { message: 'yiyanModelNameMessage' },
+ },
+ {
+ name: 'yiyan_ak',
+ label: 'addyiyanAK',
+ type: FormFieldType.Text,
+ required: true,
+ placeholder: 'yiyanAKMessage',
+ shouldRender: 'hideWhenInstanceExists',
+ validation: { message: 'yiyanAKMessage' },
+ },
+ {
+ name: 'yiyan_sk',
+ label: 'addyiyanSK',
+ type: FormFieldType.Text,
+ required: true,
+ placeholder: 'yiyanSKMessage',
+ shouldRender: 'hideWhenInstanceExists',
+ validation: { message: 'yiyanSKMessage' },
+ },
+ {
+ name: 'max_tokens',
+ label: 'maxTokens',
+ type: FormFieldType.Number,
+ required: true,
+ placeholder: 'maxTokensTip',
+ defaultValue: 8192,
+ validation: { min: 0 },
+ },
+ ],
+ verifyTransform: (values, modelInfo) => ({
+ apiKey: JSON.stringify({
+ yiyan_ak: values.yiyan_ak,
+ yiyan_sk: values.yiyan_sk,
+ }),
+ modelInfo,
+ }),
+ submitTransform: (values, modelInfo) => ({
+ instance_name: values.instance_name,
+ llm_factory: LLMFactory.BaiduYiYan,
+ api_key: {
+ yiyan_ak: values.yiyan_ak,
+ yiyan_sk: values.yiyan_sk,
+ },
+ model_info: modelInfo,
+ }),
+ },
+
+ // ============ Fish Audio ============
+ [LLMFactory.FishAudio]: {
+ llmFactory: LLMFactory.FishAudio,
+ title: 'Fish Audio',
+ docLink: 'https://fish.audio',
+ docLinkI18nKey: 'FishAudioLink',
+ fields: [
+ {
+ name: 'instance_name',
+ label: 'instanceName',
+ type: FormFieldType.Text,
+ required: true,
+ placeholder: 'instanceNameMessage',
+ tooltip: 'instanceNameTip',
+ validation: { message: 'instanceNameMessage' },
+ },
+ {
+ name: 'model_type',
+ label: 'modelType',
+ type: FormFieldType.MultiSelect,
+ required: true,
+ options: [{ label: 'TTS', value: 'tts' }],
+ defaultValue: ['tts'],
+ },
+ {
+ name: 'model_name',
+ label: 'modelName',
+ type: FormFieldType.Text,
+ required: true,
+ placeholder: 'FishAudioModelNameMessage',
+ validation: { message: 'FishAudioModelNameMessage' },
+ },
+ {
+ name: 'fish_audio_ak',
+ label: 'addFishAudioAK',
+ type: FormFieldType.Text,
+ required: true,
+ placeholder: 'FishAudioAKMessage',
+ shouldRender: 'hideWhenInstanceExists',
+ validation: { message: 'FishAudioAKMessage' },
+ },
+ {
+ name: 'fish_audio_refid',
+ label: 'addFishAudioRefID',
+ type: FormFieldType.Text,
+ required: true,
+ placeholder: 'FishAudioRefIDMessage',
+ shouldRender: 'hideWhenInstanceExists',
+ validation: { message: 'FishAudioRefIDMessage' },
+ },
+ {
+ name: 'max_tokens',
+ label: 'maxTokens',
+ type: FormFieldType.Number,
+ required: true,
+ placeholder: 'maxTokensTip',
+ defaultValue: 8192,
+ validation: { min: 0, message: 'maxTokensInvalidMessage' },
+ },
+ ],
+ verifyTransform: (values, modelInfo) => ({
+ apiKey: JSON.stringify({
+ fish_audio_ak: values.fish_audio_ak,
+ fish_audio_refid: values.fish_audio_refid,
+ }),
+ modelInfo,
+ }),
+ submitTransform: (values, modelInfo) => ({
+ instance_name: values.instance_name,
+ llm_factory: LLMFactory.FishAudio,
+ fish_audio_ak: values.fish_audio_ak,
+ fish_audio_refid: values.fish_audio_refid,
+ model_info: modelInfo,
+ }),
+ },
+
+ // ============ OpenDataLoader ============
+ [LLMFactory.OpenDataLoader]: {
+ llmFactory: LLMFactory.OpenDataLoader,
+ title: 'OpenDataLoader',
+ fields: [
+ {
+ name: 'instance_name',
+ label: 'instanceName',
+ type: FormFieldType.Text,
+ required: true,
+ placeholder: 'instanceNameMessage',
+ tooltip: 'instanceNameTip',
+ validation: { message: 'instanceNameMessage' },
+ },
+ {
+ name: 'model_name',
+ label: 'modelName',
+ type: FormFieldType.Text,
+ required: true,
+ placeholder: 'modelNameMessage',
+ validation: { message: 'modelNameMessage' },
+ },
+ {
+ name: 'opendataloader_apiserver',
+ label: 'opendataloaderApiserver',
+ type: FormFieldType.Text,
+ required: true,
+ placeholder: 'opendataloaderApiserverPlaceholder',
+ validation: { message: 'opendataloaderApiserverMessage' },
+ },
+ {
+ name: 'opendataloader_api_key',
+ label: 'apiKey',
+ type: FormFieldType.Text,
+ required: false,
+ placeholder: 'apiKeyPlaceholder',
+ },
+ ],
+ verifyTransform: (values, modelInfo) => {
+ const cfg: Record = {};
+ if (values.opendataloader_apiserver) {
+ cfg.opendataloader_apiserver = values.opendataloader_apiserver;
+ }
+ if (values.opendataloader_api_key) {
+ cfg.opendataloader_api_key = values.opendataloader_api_key;
+ }
+ return {
+ apiKey: JSON.stringify(cfg),
+ baseUrl: values.opendataloader_apiserver,
+ modelInfo,
+ };
+ },
+ submitTransform: (values, modelInfo) => {
+ const cfg: Record = {};
+ if (values.opendataloader_apiserver) {
+ cfg.opendataloader_apiserver = values.opendataloader_apiserver;
+ }
+ if (values.opendataloader_api_key) {
+ cfg.opendataloader_api_key = values.opendataloader_api_key;
+ }
+ return {
+ instance_name: values.instance_name,
+ llm_factory: LLMFactory.OpenDataLoader,
+ api_key: JSON.stringify(cfg),
+ api_base: '',
+ model_info: modelInfo,
+ };
+ },
+ },
+
+ // ============ PaddleOCR ============
+ [LLMFactory.PaddleOCR]: {
+ llmFactory: LLMFactory.PaddleOCR,
+ title: 'PaddleOCR',
+ fields: [
+ {
+ name: 'instance_name',
+ label: 'instanceName',
+ type: FormFieldType.Text,
+ required: true,
+ placeholder: 'instanceNameMessage',
+ tooltip: 'instanceNameTip',
+ validation: { message: 'instanceNameMessage' },
+ },
+ {
+ name: 'model_name',
+ label: 'modelName',
+ type: FormFieldType.Text,
+ required: true,
+ placeholder: 'modelNameMessage',
+ validation: { message: 'modelNameMessage' },
+ },
+ {
+ name: 'paddleocr_api_url',
+ label: 'paddleocrApiUrl',
+ type: FormFieldType.Text,
+ required: true,
+ placeholder: 'paddleocrApiUrlPlaceholder',
+ validation: { message: 'paddleocrApiUrlMessage' },
+ },
+ {
+ name: 'paddleocr_access_token',
+ label: 'paddleocrAccessToken',
+ type: FormFieldType.Text,
+ required: false,
+ placeholder: 'paddleocrAccessTokenPlaceholder',
+ validation: { message: 'paddleocrAccessTokenMessage' },
+ },
+ {
+ name: 'paddleocr_algorithm',
+ label: 'paddleocrAlgorithm',
+ type: FormFieldType.Select,
+ required: false,
+ defaultValue: 'PaddleOCR-VL',
+ placeholder: 'paddleocrSelectAlgorithm',
+ options: [
+ { label: 'PaddleOCR-VL-1.5', value: 'PaddleOCR-VL-1.5' },
+ { label: 'PaddleOCR-VL', value: 'PaddleOCR-VL' },
+ { label: 'PP-OCRv5', value: 'PP-OCRv5' },
+ { label: 'PP-StructureV3', value: 'PP-StructureV3' },
+ ],
+ },
+ ],
+ verifyTransform: (values, modelInfo) => {
+ const cfg: Record = {};
+ if (values.paddleocr_api_url)
+ cfg.paddleocr_api_url = values.paddleocr_api_url;
+ if (values.paddleocr_access_token)
+ cfg.paddleocr_access_token = values.paddleocr_access_token;
+ if (values.paddleocr_algorithm)
+ cfg.paddleocr_algorithm = values.paddleocr_algorithm;
+ return {
+ apiKey: JSON.stringify(cfg),
+ baseUrl: values.paddleocr_api_url,
+ modelInfo,
+ };
+ },
+ submitTransform: (values, modelInfo) => {
+ const cfg: Record = {};
+ if (values.paddleocr_api_url)
+ cfg.paddleocr_api_url = values.paddleocr_api_url;
+ if (values.paddleocr_access_token)
+ cfg.paddleocr_access_token = values.paddleocr_access_token;
+ if (values.paddleocr_algorithm)
+ cfg.paddleocr_algorithm = values.paddleocr_algorithm;
+ return {
+ instance_name: values.instance_name,
+ llm_factory: LLMFactory.PaddleOCR,
+ api_key: JSON.stringify(cfg),
+ api_base: '',
+ model_info: modelInfo,
+ };
+ },
+ },
+
+ // ============ MinerU ============
+ [LLMFactory.MinerU]: {
+ llmFactory: LLMFactory.MinerU,
+ title: 'MinerU',
+ fields: [
+ {
+ name: 'instance_name',
+ label: 'instanceName',
+ type: FormFieldType.Text,
+ required: true,
+ placeholder: 'instanceNameMessage',
+ tooltip: 'instanceNameTip',
+ validation: { message: 'instanceNameMessage' },
+ },
+ {
+ name: 'model_name',
+ label: 'modelName',
+ type: FormFieldType.Text,
+ required: true,
+ placeholder: 'modelNameMessage',
+ validation: { message: 'modelNameMessage' },
+ },
+ {
+ name: 'mineru_apiserver',
+ label: 'mineruApiserver',
+ type: FormFieldType.Text,
+ required: true,
+ placeholder: 'mineruApiserverPlaceholder',
+ validation: { message: 'mineruApiserverMessage' },
+ },
+ {
+ name: 'mineru_output_dir',
+ label: 'mineruOutputDir',
+ type: FormFieldType.Text,
+ required: false,
+ placeholder: 'mineruOutputDirPlaceholder',
+ },
+ {
+ name: 'mineru_backend',
+ label: 'mineruBackend',
+ type: FormFieldType.Select,
+ required: true,
+ defaultValue: 'pipeline',
+ placeholder: 'mineruSelectBackend',
+ options: [
+ { label: 'pipeline', value: 'pipeline' },
+ { label: 'vlm-transformers', value: 'vlm-transformers' },
+ { label: 'vlm-vllm-engine', value: 'vlm-vllm-engine' },
+ { label: 'vlm-http-client', value: 'vlm-http-client' },
+ { label: 'vlm-mlx-engine', value: 'vlm-mlx-engine' },
+ { label: 'vlm-vllm-async-engine', value: 'vlm-vllm-async-engine' },
+ { label: 'vlm-lmdeploy-engine', value: 'vlm-lmdeploy-engine' },
+ ],
+ validation: { message: 'mineruBackendMessage' },
+ },
+ {
+ name: 'mineru_server_url',
+ label: 'mineruServerUrl',
+ type: FormFieldType.Text,
+ required: false,
+ placeholder: 'mineruServerUrlPlaceholder',
+ shouldRender: (values: any) =>
+ values?.mineru_backend === 'vlm-http-client',
+ validation: { message: 'mineruServerUrlMessage' },
+ },
+ {
+ name: 'mineru_delete_output',
+ label: 'mineruDeleteOutput',
+ type: FormFieldType.Switch,
+ required: false,
+ defaultValue: true,
+ },
+ ],
+ verifyTransform: (values, modelInfo) => {
+ const cfg: Record = { ...values };
+ delete cfg.instance_name;
+ delete cfg.model_name;
+ cfg.mineru_delete_output = values.mineru_delete_output ? '1' : '0';
+ if (values.mineru_backend !== 'vlm-http-client') {
+ delete cfg.mineru_server_url;
+ }
+ return {
+ apiKey: JSON.stringify(cfg),
+ baseUrl: values.mineru_apiserver,
+ modelInfo,
+ };
+ },
+ submitTransform: (values, modelInfo) => {
+ const cfg: Record = { ...values };
+ delete cfg.instance_name;
+ delete cfg.model_name;
+ cfg.mineru_delete_output = values.mineru_delete_output ? '1' : '0';
+ if (values.mineru_backend !== 'vlm-http-client') {
+ delete cfg.mineru_server_url;
+ }
+ return {
+ instance_name: values.instance_name,
+ llm_factory: LLMFactory.MinerU,
+ api_key: JSON.stringify(cfg),
+ api_base: '',
+ model_info: modelInfo,
+ };
+ },
+ },
+};
diff --git a/web/src/pages/user-setting/setting-model/modal/provider-modal/field-config/utils.ts b/web/src/pages/user-setting/setting-model/modal/provider-modal/field-config/utils.ts
new file mode 100644
index 0000000000..53c3482c54
--- /dev/null
+++ b/web/src/pages/user-setting/setting-model/modal/provider-modal/field-config/utils.ts
@@ -0,0 +1,24 @@
+/**
+ * Capitalize the first letter of a string
+ */
+export function capitalize(s: string): string {
+ return s.charAt(0).toUpperCase() + s.slice(1);
+}
+
+/**
+ * When model_type contains chat and vision=true, automatically add image2text
+ */
+export function applyChatToImage2Text(
+ modelType: string[] | string | undefined,
+ vision?: boolean,
+): string[] {
+ const arr = Array.isArray(modelType)
+ ? modelType
+ : modelType
+ ? [modelType]
+ : [];
+ if (arr.includes('chat') && vision) {
+ return [...arr, 'image2text'];
+ }
+ return arr;
+}
diff --git a/web/src/pages/user-setting/setting-model/modal/provider-modal/hooks/index.ts b/web/src/pages/user-setting/setting-model/modal/provider-modal/hooks/index.ts
new file mode 100644
index 0000000000..6ebbee5ee5
--- /dev/null
+++ b/web/src/pages/user-setting/setting-model/modal/provider-modal/hooks/index.ts
@@ -0,0 +1,4 @@
+export { useListModelsOptions } from './use-list-models-options';
+export { useListModelsPicker } from './use-list-models-picker';
+export { useProviderFields } from './use-provider-fields';
+export { useProviderModalActions } from './use-provider-modal-actions';
diff --git a/web/src/pages/user-setting/setting-model/modal/provider-modal/hooks/use-list-models-options.tsx b/web/src/pages/user-setting/setting-model/modal/provider-modal/hooks/use-list-models-options.tsx
new file mode 100644
index 0000000000..0f9973dbd5
--- /dev/null
+++ b/web/src/pages/user-setting/setting-model/modal/provider-modal/hooks/use-list-models-options.tsx
@@ -0,0 +1,103 @@
+import { Checkbox } from '@/components/ui/checkbox';
+import { useTranslate } from '@/hooks/common-hooks';
+import { IProviderModelItem } from '@/interfaces/request/llm';
+import { useMemo } from 'react';
+
+interface UseListModelsOptionsParams {
+ models: IProviderModelItem[];
+ selectedModelItems: IProviderModelItem[];
+ allSelected: boolean;
+ handleSelectModel: (model: IProviderModelItem) => void;
+ handleToggleAll: () => void;
+}
+
+/**
+ * Build ToggleList options from the fetched model list. The first item is
+ * a sentinel "All models" row that toggles the full selection.
+ *
+ * Why the Checkbox uses `onClick` (not `onCheckedChange`):
+ * Radix Checkbox calls `event.stopPropagation()` internally on its onClick
+ * when the Checkbox lives inside a form, so the row's `onClick` (attached
+ * by ToggleList) never fires when the user clicks the Checkbox itself.
+ * To make the Checkbox click toggle selection, we handle it in our own
+ * `onClick` and re-stop propagation to (a) prevent the row's onClick from
+ * double-firing and (b) make Radix's CheckboxBubbleInput dispatch a
+ * non-bubbling synthetic click on its hidden form input — without this,
+ * the dispatched click would bubble back to the row and re-trigger the
+ * toggle, causing "Maximum update depth exceeded".
+ */
+export const useListModelsOptions = ({
+ models,
+ selectedModelItems,
+ allSelected,
+ handleSelectModel,
+ handleToggleAll,
+}: UseListModelsOptionsParams) => {
+ const { t } = useTranslate('setting');
+
+ return useMemo(() => {
+ const allOption = {
+ value: null as string | null,
+ label: (
+
+
+
{
+ e.stopPropagation();
+ handleToggleAll();
+ }}
+ />
+
+ ),
+ onClick: () => handleToggleAll(),
+ };
+
+ const modelOptions = models.map((m) => {
+ const checked = selectedModelItems.some((s) => s.name === m.name);
+ return {
+ value: m.name,
+ label: (
+
+
+
{m.name}
+ {m.model_types &&
+ m.model_types.map((type) => {
+ return (
+
+ {type}
+
+ );
+ })}
+
+
{
+ e.stopPropagation();
+ handleSelectModel(m);
+ }}
+ />
+
+ ),
+ onClick: () => handleSelectModel(m),
+ };
+ });
+ if (modelOptions?.length) {
+ return [allOption, ...modelOptions];
+ } else {
+ return [];
+ }
+ }, [
+ models,
+ selectedModelItems,
+ handleSelectModel,
+ allSelected,
+ handleToggleAll,
+ t,
+ ]);
+};
diff --git a/web/src/pages/user-setting/setting-model/modal/provider-modal/hooks/use-list-models-picker.ts b/web/src/pages/user-setting/setting-model/modal/provider-modal/hooks/use-list-models-picker.ts
new file mode 100644
index 0000000000..618b204833
--- /dev/null
+++ b/web/src/pages/user-setting/setting-model/modal/provider-modal/hooks/use-list-models-picker.ts
@@ -0,0 +1,275 @@
+import { DynamicFormRef } from '@/components/dynamic-form';
+import { useListProviderModels } from '@/hooks/use-llm-request';
+import { IModelInfo, IProviderModelItem } from '@/interfaces/request/llm';
+import {
+ RefObject,
+ useCallback,
+ useEffect,
+ useMemo,
+ useRef,
+ useState,
+} from 'react';
+import type { ProviderConfig } from '../types';
+
+// Derive is_tools from a model descriptor's `features` array. A model is
+// considered tool-capable if it advertises either `tool_call` or
+// `function_call`. Returns `undefined` when the model has no features info.
+const getIsToolsFromFeatures = (
+ features: IProviderModelItem['features'],
+): boolean | undefined => {
+ if (!Array.isArray(features)) return undefined;
+ return features.includes('tool_call') || features.includes('function_call');
+};
+
+// Map a fetched list-model item to the request-side IModelInfo shape.
+// Per-model extras (such as is_tools) live under the `extra` object so the
+// addProviderInstance API receives them in the shape the backend expects.
+const toIModelInfo = (item: IProviderModelItem): IModelInfo => {
+ const is_tools = getIsToolsFromFeatures(item.features);
+ return {
+ model_name: item.name,
+ model_type: item.model_types ?? [],
+ max_tokens: item.max_tokens ?? 0,
+ ...(is_tools !== undefined ? { extra: { is_tools } } : {}),
+ };
+};
+
+// Compare the model_type sets from an initial IModelInfo entry and a
+// freshly fetched list item. Tolerates string-vs-array differences so a
+// legacy single-value `model_type` still matches the list's array shape.
+const modelTypesMatch = (
+ initial: string | string[] | undefined,
+ fetched: string[] | undefined,
+): boolean => {
+ const a = Array.isArray(initial) ? initial : initial ? [initial] : [];
+ const b = Array.isArray(fetched) ? fetched : [];
+ if (a.length !== b.length) return false;
+ const sb = new Set(b);
+ return a.every((t) => sb.has(t));
+};
+
+interface UseListModelsPickerParams {
+ visible: boolean;
+ hasModelNameField: boolean;
+ editMode?: boolean;
+ viewMode?: boolean;
+ initialValues?: Record;
+ llmFactory: string;
+ config: ProviderConfig;
+ formRef: RefObject;
+}
+
+/**
+ * Owns all state for the "List Models" picker:
+ * - fetched model catalog (`models`) and loading flag
+ * - currently checked items (`selectedModelItems`)
+ * - derived `modelInfoList` payload used at verify/submit time
+ * - `allSelected` flag and the all/none/individual toggle handlers
+ * - the API fetch (with edit-mode pre-check seeding) and modal-reset effect
+ *
+ * The picker lives entirely in component state — no form fields are touched.
+ * A reentrancy ref guards against Radix Checkbox's double-click dispatch
+ * (CheckboxBubbleInput re-fires onClick inside a form).
+ */
+export const useListModelsPicker = ({
+ visible,
+ hasModelNameField,
+ editMode,
+ viewMode,
+ initialValues,
+ llmFactory,
+ config,
+ formRef,
+}: UseListModelsPickerParams) => {
+ const [models, setModels] = useState([]);
+ const [listLoading, setListLoading] = useState(false);
+ // Items the user has checked in the picker. Carries the full descriptor
+ // (including `features`) so we can derive is_tools per model at submit time.
+ const [selectedModelItems, setSelectedModelItems] = useState<
+ IProviderModelItem[]
+ >([]);
+ // Edit-mode seed: the model_info array stored on the existing instance.
+ // Used to pre-check list items once the model list is fetched.
+ const initialModelInfoRef = useRef(null);
+ const { listProviderModels } = useListProviderModels();
+
+ // Reentrancy guard for selection toggles. When a Checkbox inside the form is
+ // clicked, Radix's CheckboxBubbleInput dispatches a synthetic click on its
+ // hidden form input that bubbles up to the row's onClick — so each user
+ // click fires the toggle handler twice in the same tick. Calling setState
+ // twice (especially for the all/none toggle below) trips React's
+ // "Maximum update depth exceeded" guard. The ref short-circuits the second
+ // call and is released on the next macrotask.
+ const selectionLockRef = useRef(false);
+
+ // Derived: the model_info array passed to verify/submit. One entry per
+ // checked list item, with is_tools derived from the model's `features`.
+ const modelInfoList: IModelInfo[] = useMemo(
+ () => selectedModelItems.map(toIModelInfo),
+ [selectedModelItems],
+ );
+
+ // "All models" is checked when every fetched model is selected.
+ const allSelected = useMemo(
+ () => models.length > 0 && selectedModelItems.length === models.length,
+ [models.length, selectedModelItems.length],
+ );
+
+ // Capture the initial model_info array (edit mode or viewMode) so we
+ // can match it against the fetched list once it arrives and pre-check
+ // the right items.
+ useEffect(() => {
+ if (!visible) return;
+ initialModelInfoRef.current = null;
+ if ((editMode || viewMode) && initialValues) {
+ const initial = (initialValues as any).model_info;
+ if (Array.isArray(initial)) {
+ initialModelInfoRef.current = initial as IModelInfo[];
+ } else if (
+ (initialValues as any).model_name &&
+ (initialValues as any).model_type
+ ) {
+ // Legacy fallback: a single-model payload may still arrive as flat
+ // fields. Wrap it so the matcher below has a uniform input shape.
+ initialModelInfoRef.current = [
+ {
+ model_name: (initialValues as any).model_name,
+ model_type: (initialValues as any).model_type,
+ max_tokens: (initialValues as any).max_tokens ?? 0,
+ },
+ ];
+ }
+ }
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [visible, editMode, viewMode]);
+
+ // Triggered by ToggleList's onOpenChange — fires the API call with the
+ // same payload shape as verifyProviderConnection. Caches the result in
+ // component state so subsequent opens don't re-fetch. In edit mode, the
+ // fetched list is then matched against `initialModelInfoRef` so any model
+ // that was already configured is pre-checked.
+ const handleListOpenChange = useCallback(
+ async (open: boolean) => {
+ if (!open || !hasModelNameField) return;
+ if (models.length > 0 || listLoading) return;
+ setListLoading(true);
+ try {
+ const values = (formRef.current?.getValues() || {}) as Record<
+ string,
+ any
+ >;
+ // Reuse the verifyTransform to build the request payload — it
+ // already knows how to flatten provider-specific auth (api_key,
+ // base_url, region, model_info). Pass the current modelInfoList
+ // (empty on first load, populated on re-opens) so the backend
+ // sees an array shape consistent with the verify/submit payloads.
+ const verifyArgs = config.verifyTransform
+ ? config.verifyTransform(values, modelInfoList)
+ : { apiKey: values.api_key ?? '', baseUrl: values.base_url };
+ const res = await listProviderModels({
+ provider_name: llmFactory,
+ api_key: (verifyArgs as any).apiKey ?? '',
+ base_url: (verifyArgs as any).baseUrl,
+ region: (verifyArgs as any).region,
+ model_info: (verifyArgs as any).modelInfo ?? modelInfoList,
+ });
+ if (res?.code === 0 && Array.isArray(res.data)) {
+ setModels(res.data);
+ // Edit-mode pre-check: match the initial model_info entries
+ // against the freshly fetched list (by name + model_type set)
+ // and seed `selectedModelItems`.
+ const seed = initialModelInfoRef.current;
+ if (seed && seed.length > 0) {
+ const matched = res.data.filter((m: IProviderModelItem) =>
+ seed.some(
+ (s) =>
+ s.model_name === m.name &&
+ modelTypesMatch(s.model_type, m.model_types),
+ ),
+ );
+ if (matched.length > 0) {
+ setSelectedModelItems(matched);
+ }
+ }
+ }
+ } catch (err) {
+ console.error('Failed to fetch provider models:', err);
+ } finally {
+ setListLoading(false);
+ }
+ },
+ [
+ hasModelNameField,
+ models.length,
+ listLoading,
+ config,
+ listProviderModels,
+ llmFactory,
+ modelInfoList,
+ formRef,
+ ],
+ );
+
+ // Toggling a list item: add it to `selectedModelItems` if absent, remove
+ // it otherwise. No form fields are touched — selection lives entirely
+ // in component state and is surfaced as `modelInfoList` at verify/submit.
+ const handleSelectModel = useCallback((model: IProviderModelItem) => {
+ if (selectionLockRef.current) return;
+ selectionLockRef.current = true;
+ setSelectedModelItems((prev) => {
+ const idx = prev.findIndex((p) => p.name === model.name);
+ if (idx >= 0) {
+ const next = prev.slice();
+ next.splice(idx, 1);
+ return next;
+ }
+ return [...prev, model];
+ });
+ setTimeout(() => {
+ selectionLockRef.current = false;
+ }, 0);
+ }, []);
+
+ // Toggling the "All models" row: select every model when none/all are
+ // unselected, otherwise clear the selection. Mirrors the per-item toggle
+ // semantics so the UI stays consistent (re-clicking all = empty).
+ // The reentrancy guard here is critical: without it, the second call sees
+ // `prev.length === models.length` (from the first call's `models.slice()`)
+ // and returns `[]`, producing two setState calls per click and triggering
+ // the "Maximum update depth exceeded" error.
+ const handleToggleAll = useCallback(() => {
+ if (selectionLockRef.current) return;
+ selectionLockRef.current = true;
+ setSelectedModelItems((prev) => {
+ if (prev.length === models.length) {
+ return [];
+ }
+ return models.slice();
+ });
+ setTimeout(() => {
+ selectionLockRef.current = false;
+ }, 0);
+ }, [models]);
+
+ // Reset everything when the modal is closed.
+ useEffect(() => {
+ if (!visible) {
+ formRef.current?.reset();
+ setModels([]);
+ setSelectedModelItems([]);
+ setListLoading(false);
+ initialModelInfoRef.current = null;
+ }
+ }, [visible, formRef]);
+
+ return {
+ models,
+ listLoading,
+ selectedModelItems,
+ modelInfoList,
+ allSelected,
+ handleListOpenChange,
+ handleSelectModel,
+ handleToggleAll,
+ };
+};
diff --git a/web/src/pages/user-setting/setting-model/modal/provider-modal/hooks/use-provider-fields.tsx b/web/src/pages/user-setting/setting-model/modal/provider-modal/hooks/use-provider-fields.tsx
new file mode 100644
index 0000000000..0529bc0166
--- /dev/null
+++ b/web/src/pages/user-setting/setting-model/modal/provider-modal/hooks/use-provider-fields.tsx
@@ -0,0 +1,297 @@
+import { FormFieldConfig, FormFieldType } from '@/components/dynamic-form';
+import { Input } from '@/components/ui/input';
+import { InputSelect } from '@/components/ui/input-select';
+import { useTranslate } from '@/hooks/common-hooks';
+import { useMemo } from 'react';
+import { ControllerRenderProps, FieldValues } from 'react-hook-form';
+import { LIST_MODEL_FIELD_NAMES, LIST_MODEL_PROVIDERS } from '../constants';
+import { FACTORIES_WITH_BASE_URL, getProviderConfig } from '../field-config';
+import type { FieldConfig, SelectOption } from '../types';
+
+interface UseProviderFieldsParams {
+ llmFactory: string;
+ editMode?: boolean;
+ viewMode?: boolean;
+ initialValues?: Record;
+ baseUrlOptions?: SelectOption[];
+ hideWhenInstanceExists: (values: Record) => boolean;
+}
+
+/**
+ * Resolve a text value that may be a static i18n key or a factory-aware
+ * resolver, then translate it via `t`. Used for `placeholder` and `tooltip`
+ * so that a single FieldConfig entry can render different text per provider
+ * (e.g. the generic `base_url` field has a Minimax-specific tooltip).
+ */
+const resolveText = (
+ val: string | ((factory: string) => string) | undefined,
+ factory: string,
+ t: (key: string) => string,
+): string | undefined => {
+ if (!val) return undefined;
+ const key = typeof val === 'function' ? val(factory) : val;
+ return t(key);
+};
+
+/** Set value by nested path (supports paths like 'model_info.model_type'). */
+const setNestedValue = (obj: any, path: string, value: any) => {
+ const keys = path.split('.');
+ let current = obj;
+ for (let i = 0; i < keys.length - 1; i++) {
+ const key = keys[i];
+ if (!current[key]) {
+ current[key] = {};
+ }
+ current = current[key];
+ }
+ current[keys[keys.length - 1]] = value;
+};
+
+/**
+ * Builds the form field config, default values, and doc-link text for the
+ * Provider modal. Handles:
+ * - Per-provider text resolution (placeholder / tooltip).
+ * - shouldRender token → predicate resolution.
+ * - Hiding the 4 model_* fields when the list-models picker is active.
+ * - Disabling non-model fields in viewMode.
+ * - Custom inputSelect rendering (Input + dropdown of suggestions).
+ */
+export const useProviderFields = ({
+ llmFactory,
+ editMode,
+ viewMode,
+ initialValues,
+ baseUrlOptions,
+ hideWhenInstanceExists,
+}: UseProviderFieldsParams) => {
+ const { t } = useTranslate('setting');
+
+ const config = useMemo(() => getProviderConfig(llmFactory), [llmFactory]);
+
+ // Whether this factory should render the "List Models" picker. Only the
+ // providers listed in `LIST_MODEL_PROVIDERS` opt into the picker; all
+ // others keep the traditional model_name / model_type / max_tokens /
+ // is_tools form fields.
+ const hasModelNameField = useMemo(
+ () => LIST_MODEL_PROVIDERS.has(llmFactory),
+ [llmFactory],
+ );
+
+ // Resolve the shouldRender string token to an actual predicate.
+ const resolveShouldRender = useMemo(() => {
+ return (sr: FieldConfig['shouldRender']) => {
+ if (!sr) return undefined;
+ if (typeof sr === 'function') return sr;
+
+ switch (sr) {
+ case 'hideWhenInstanceExists':
+ return hideWhenInstanceExists;
+ case 'modelTypeIncludesChat':
+ return (values: any) => {
+ const mt = values?.model_type;
+ if (Array.isArray(mt)) return mt.includes('chat');
+ return mt === 'chat';
+ };
+ case 'modelTypeSupportsToolCall':
+ return (values: any) => {
+ const mt = values?.model_type;
+ if (Array.isArray(mt)) {
+ return mt.includes('chat') || mt.includes('image2text');
+ }
+ return mt === 'chat' || mt === 'image2text';
+ };
+ case 'modelTypeIncludesTtsAndNotExists':
+ return (values: any) => {
+ if (!hideWhenInstanceExists(values)) return false;
+ const mt = values?.model_type;
+ if (Array.isArray(mt)) return mt.includes('tts');
+ return mt === 'tts';
+ };
+ case 'showBaseUrl':
+ return () =>
+ FACTORIES_WITH_BASE_URL.some((x) => x === llmFactory) ||
+ llmFactory?.toLowerCase() === 'Anthropic'.toLowerCase();
+ case 'showGroupId':
+ return () => llmFactory?.toLowerCase() === 'Minimax'.toLowerCase();
+ default:
+ return undefined;
+ }
+ };
+ }, [hideWhenInstanceExists, llmFactory]);
+
+ // For each inputSelect field, build a URL → regionKey map from its
+ // options (either inline `field.options` or the shared `baseUrlOptions`).
+ // The map is used both to pick the "default" key's URL as the form's
+ // initial value and (downstream, in useProviderModalActions) to derive
+ // the `region` submit field from the user's currently selected URL.
+ const baseUrlRegionMaps = useMemo(() => {
+ const maps: Record> = {};
+ config.fields.forEach((field) => {
+ if (field.type !== 'inputSelect') return;
+ const options =
+ field.options && field.options.length > 0
+ ? field.options
+ : (baseUrlOptions ?? []);
+ const urlMap = new Map();
+ options.forEach((opt) => {
+ if (opt.regionKey) {
+ urlMap.set(opt.value, opt.regionKey);
+ }
+ });
+ if (urlMap.size > 0) {
+ maps[field.name] = urlMap;
+ }
+ });
+ return maps;
+ }, [config.fields, baseUrlOptions]);
+
+ // Convert FieldConfig to FormFieldConfig (for use by DynamicForm)
+ const fields: FormFieldConfig[] = useMemo(() => {
+ const res = config.fields
+ .filter(
+ // When the list-models picker is active, the 4 model_* fields are
+ // owned by the picker (component state) and must not be registered
+ // in the dynamic form.
+ (field) =>
+ !hasModelNameField || !LIST_MODEL_FIELD_NAMES.has(field.name),
+ )
+ .map((field) => {
+ const placeholderText = resolveText(field.placeholder, llmFactory, t);
+ const tooltipText = resolveText(field.tooltip, llmFactory, t);
+ const validation = field.validation
+ ? {
+ ...field.validation,
+ message: field.validation.message
+ ? t(field.validation.message)
+ : undefined,
+ }
+ : undefined;
+ const baseField: Omit = {
+ name: field.name,
+ label: t(field.label),
+ required: field.required,
+ hidden: false,
+ placeholder: placeholderText,
+ tooltip: tooltipText,
+ options: field.options?.map((o) => ({
+ label: o.label as string,
+ value: o.value,
+ })) as any,
+ defaultValue: field.defaultValue,
+ validation,
+ shouldRender: resolveShouldRender(field.shouldRender),
+ // In viewMode, only the model-related fields are editable.
+ // All other fields (instance_name, api_key, base_url, etc.)
+ // are rendered as disabled.
+ disabled: !!viewMode && !LIST_MODEL_FIELD_NAMES.has(field.name),
+ dependencies:
+ field.shouldRender === 'modelTypeIncludesChat' ||
+ field.shouldRender === 'modelTypeSupportsToolCall' ||
+ field.shouldRender === 'modelTypeIncludesTtsAndNotExists'
+ ? ['model_type']
+ : ['model_type', 'instance_name'].includes(field.name)
+ ? ['model_type', 'instance_name']
+ : undefined,
+ };
+
+ // inputSelect type: use the InputSelect component, options come from baseUrlOptions
+ if (field.type === 'inputSelect') {
+ const inputSelectOptions: SelectOption[] =
+ field.options && field.options.length > 0
+ ? field.options
+ : (baseUrlOptions ?? []);
+ return {
+ ...baseField,
+ type: FormFieldType.Custom,
+ options: inputSelectOptions as any,
+ render: (fieldProps: ControllerRenderProps) => {
+ return inputSelectOptions.length > 0 ? (
+ fieldProps.onChange(value)}
+ options={inputSelectOptions as any}
+ placeholder={placeholderText}
+ />
+ ) : (
+
+ );
+ },
+ };
+ }
+
+ // Other types use the enum value directly from the field config
+ return { ...baseField, type: field.type };
+ });
+ return res;
+ }, [
+ config.fields,
+ resolveShouldRender,
+ t,
+ baseUrlOptions,
+ llmFactory,
+ hasModelNameField,
+ viewMode,
+ ]);
+
+ const defaultValues: FieldValues = useMemo(() => {
+ // In editMode or viewMode, seed the form with the supplied
+ // `initialValues` so the user sees the existing instance/model data.
+ if ((editMode || viewMode) && initialValues) {
+ return initialValues as FieldValues;
+ }
+ const result: FieldValues = {};
+ config.fields.forEach((f) => {
+ if (f.defaultValue !== undefined) {
+ setNestedValue(result, f.name, f.defaultValue);
+ return;
+ }
+ // For inputSelect fields, default the form to the option whose
+ // original key in the URL object is 'default' (e.g.
+ // `availableProviders.url.default`). If no such option exists,
+ // leave the field empty so the user picks one explicitly.
+ if (f.type === 'inputSelect') {
+ const urlMap = baseUrlRegionMaps[f.name];
+ if (urlMap) {
+ for (const [url, regionKey] of urlMap.entries()) {
+ if (regionKey === 'default') {
+ setNestedValue(result, f.name, url);
+ return;
+ }
+ }
+ }
+ }
+ });
+ // For configurations without a default model_type, assign an empty array (multi-select field)
+ const mtField = config.fields.find((f) => f.name === 'model_type');
+ if (mtField && mtField.defaultValue === undefined) {
+ setNestedValue(result, 'model_type', []);
+ }
+ return result;
+ }, [
+ editMode,
+ viewMode,
+ initialValues,
+ config.fields,
+ llmFactory,
+ baseUrlRegionMaps,
+ ]);
+
+ // Documentation link text (rendered at the bottom of the modal)
+ const docLinkText = useMemo(() => {
+ if (config.docLinkText) return config.docLinkText;
+ if (config.docLinkI18nKey) {
+ return t(config.docLinkI18nKey, { name: llmFactory });
+ }
+ return null;
+ }, [config, llmFactory, t]);
+
+ return {
+ config,
+ fields,
+ defaultValues,
+ docLinkText,
+ hasModelNameField,
+ baseUrlRegionMaps,
+ };
+};
diff --git a/web/src/pages/user-setting/setting-model/modal/provider-modal/hooks/use-provider-modal-actions.ts b/web/src/pages/user-setting/setting-model/modal/provider-modal/hooks/use-provider-modal-actions.ts
new file mode 100644
index 0000000000..47c08a37b2
--- /dev/null
+++ b/web/src/pages/user-setting/setting-model/modal/provider-modal/hooks/use-provider-modal-actions.ts
@@ -0,0 +1,170 @@
+import { DynamicFormRef } from '@/components/dynamic-form';
+import { IModelInfo } from '@/interfaces/request/llm';
+import { VerifyResult } from '@/pages/user-setting/setting-model/hooks';
+import { RefObject, useCallback } from 'react';
+import { FieldValues } from 'react-hook-form';
+import type {
+ IViewModeOkPayload,
+ ProviderConfig,
+ ProviderModalProps,
+} from '../types';
+
+type ActionParams = {
+ config: ProviderConfig;
+ viewMode?: boolean;
+ hasModelNameField: boolean;
+ llmFactory: string;
+ initialValues?: Record;
+ modelInfoList: IModelInfo[];
+ formRef: RefObject;
+ /**
+ * URL → regionKey map for each inputSelect field (built in
+ * `useProviderFields`). Used to derive the `region` submit field
+ * from the user's currently selected base URL.
+ */
+ baseUrlRegionMaps?: Record>;
+ onOk: ProviderModalProps['onOk'];
+ onVerify: ProviderModalProps['onVerify'];
+ onViewModeOk: ProviderModalProps['onViewModeOk'];
+};
+
+/**
+ * Look up the `region` key (e.g. 'default', 'intl', 'cn') for the
+ * currently selected base URL of any inputSelect field. Returns
+ * `undefined` when no inputSelect field has a value, or when that
+ * value does not match any option's URL — in those cases the caller
+ * should leave `region` unset.
+ */
+const resolveRegionFromValues = (
+ values: Record | undefined,
+ baseUrlRegionMaps?: Record>,
+): string | undefined => {
+ if (!values || !baseUrlRegionMaps) return undefined;
+ for (const fieldName of Object.keys(baseUrlRegionMaps)) {
+ const url = values[fieldName];
+ if (typeof url !== 'string' || url === '') continue;
+ const regionKey = baseUrlRegionMaps[fieldName].get(url);
+ if (regionKey !== undefined) {
+ return regionKey;
+ }
+ }
+ return undefined;
+};
+
+/**
+ * Build the two outbound handlers for the Provider modal:
+ *
+ * - `handleVerify` reads current form values, runs them through the
+ * provider's `verifyTransform`, and forwards the result to `onVerify`.
+ * Returns a `VerifyResult` (the VerifyButton consumes the shape).
+ *
+ * - `handleSubmit` has two paths:
+ * 1. viewMode → invoke `onViewModeOk` with either the picker's selected
+ * models (LIST_MODEL_PROVIDERS) or the editable form values
+ * (non-LIST_MODEL_PROVIDERS). The instance itself is not re-saved.
+ * 2. normal mode → run values through `submitTransform` (when present)
+ * and forward to `onOk`.
+ *
+ * Both paths inject a `region` field derived from the currently selected
+ * base URL whenever the field is an inputSelect (see `baseUrlRegionMaps`).
+ */
+export const useProviderModalActions = ({
+ config,
+ viewMode,
+ hasModelNameField,
+ llmFactory,
+ initialValues,
+ modelInfoList,
+ formRef,
+ baseUrlRegionMaps,
+ onOk,
+ onVerify,
+ onViewModeOk,
+}: ActionParams) => {
+ const handleVerify = useCallback(
+ async (params: any) => {
+ const values = formRef.current?.getValues() || params;
+ if (!config.verifyTransform) {
+ return { isValid: null, logs: '' } as VerifyResult;
+ }
+ const verifyArgs = config.verifyTransform(values, modelInfoList);
+ const region = resolveRegionFromValues(values, baseUrlRegionMaps);
+ if (region !== undefined) {
+ verifyArgs.region = region;
+ }
+ const res = await onVerify({ ...params, ...verifyArgs });
+ return (res || { isValid: null, logs: '' }) as VerifyResult;
+ },
+ [config, onVerify, modelInfoList, formRef, baseUrlRegionMaps],
+ );
+
+ const handleSubmit = useCallback(
+ async (values?: FieldValues) => {
+ if (!values) return;
+
+ // viewMode: only add/update models. The instance itself is not
+ // re-saved because all instance-level fields are disabled. The
+ // parent receives the selected models (or the model-related form
+ // values for non-list-model providers) via `onViewModeOk`.
+ if (viewMode) {
+ if (!onViewModeOk) {
+ // No viewMode handler provided — nothing to save, just close
+ // (the modal's own hideModal flow handles closing).
+ return;
+ }
+ const instanceName = String(
+ (initialValues as any)?.instance_name ?? '',
+ );
+ const payload: IViewModeOkPayload = hasModelNameField
+ ? {
+ instanceName,
+ llmFactory,
+ modelInfos: modelInfoList,
+ }
+ : {
+ instanceName,
+ llmFactory,
+ modelInfos: [],
+ formValues: values as Record,
+ };
+ await onViewModeOk(payload);
+ return;
+ }
+
+ const transformed = (
+ config.submitTransform
+ ? config.submitTransform(values as Record, modelInfoList)
+ : values
+ ) as Record;
+ const region = resolveRegionFromValues(
+ values as Record,
+ baseUrlRegionMaps,
+ );
+ if (region !== undefined) {
+ transformed.region = region;
+ }
+ // Always include `llm_factory` in the submitted payload. Some
+ // providers' submitTransforms (e.g. GenericApiKeyConfig) omit it,
+ // but the parent uses it to build the request URL
+ // (`/api/v1/providers/${llm_factory}/instances`); without it the
+ // URL becomes `/providers/undefined/instances`.
+ if (!transformed.llm_factory) {
+ transformed.llm_factory = llmFactory;
+ }
+ await onOk?.(transformed, false);
+ },
+ [
+ config,
+ onOk,
+ onViewModeOk,
+ modelInfoList,
+ viewMode,
+ hasModelNameField,
+ llmFactory,
+ initialValues,
+ baseUrlRegionMaps,
+ ],
+ );
+
+ return { handleVerify, handleSubmit };
+};
diff --git a/web/src/pages/user-setting/setting-model/modal/provider-modal/index.tsx b/web/src/pages/user-setting/setting-model/modal/provider-modal/index.tsx
new file mode 100644
index 0000000000..682b3d9cc3
--- /dev/null
+++ b/web/src/pages/user-setting/setting-model/modal/provider-modal/index.tsx
@@ -0,0 +1,199 @@
+import { DynamicForm, DynamicFormRef } from '@/components/dynamic-form';
+import { Modal } from '@/components/ui/modal/modal';
+import { ToggleList } from '@/components/ui/toggle-list';
+import { useCommonTranslation, useTranslate } from '@/hooks/common-hooks';
+import {
+ useFetchInstanceNameSet,
+ useHideWhenInstanceExists,
+} from '@/pages/user-setting/setting-model/hooks';
+import { memo, useRef } from 'react';
+import { FieldValues } from 'react-hook-form';
+import { LLMHeader } from '../../components/llm-header';
+import VerifyButton from '../verify-button';
+import {
+ useListModelsOptions,
+ useListModelsPicker,
+ useProviderFields,
+ useProviderModalActions,
+} from './hooks';
+import type { ProviderModalProps } from './types';
+
+const ProviderModal = ({
+ visible,
+ hideModal,
+ llmFactory,
+ loading,
+ editMode,
+ viewMode,
+ initialValues,
+ baseUrlOptions,
+ onOk,
+ onVerify,
+ onViewModeOk,
+}: ProviderModalProps) => {
+ const { t } = useTranslate('setting');
+ const { t: tc } = useCommonTranslation();
+ const formRef = useRef(null);
+ const { instanceNameSet } = useFetchInstanceNameSet(llmFactory);
+ const hideWhenInstanceExists = useHideWhenInstanceExists(instanceNameSet);
+
+ // Field config, default values, doc link, and the LIST_MODEL_PROVIDERS
+ // flag are all derived from the current llmFactory / mode / initialValues.
+ // `baseUrlRegionMaps` is forwarded to the actions hook so the modal can
+ // populate the `region` submit field from the currently selected base URL.
+ const {
+ config,
+ fields,
+ defaultValues,
+ docLinkText,
+ hasModelNameField,
+ baseUrlRegionMaps,
+ } = useProviderFields({
+ llmFactory,
+ editMode,
+ viewMode,
+ initialValues,
+ baseUrlOptions,
+ hideWhenInstanceExists,
+ });
+
+ // Owns the "List Models" picker state and lifecycle. When
+ // `hasModelNameField` is false the picker is hidden and this hook is
+ // effectively idle (no fetch, no selection state in use).
+ const {
+ models,
+ listLoading,
+ selectedModelItems,
+ modelInfoList,
+ allSelected,
+ handleListOpenChange,
+ handleSelectModel,
+ handleToggleAll,
+ } = useListModelsPicker({
+ visible,
+ hasModelNameField,
+ editMode,
+ viewMode,
+ initialValues,
+ llmFactory,
+ config,
+ formRef,
+ });
+
+ // Render-only: turn the fetched model list into ToggleList options with
+ // the "All models" sentinel row at the top.
+ const listModelsOptions = useListModelsOptions({
+ models,
+ selectedModelItems,
+ allSelected,
+ handleSelectModel,
+ handleToggleAll,
+ });
+
+ // Submit and verify handlers — branch on viewMode and on whether the
+ // picker owns the model fields.
+ const { handleVerify, handleSubmit } = useProviderModalActions({
+ config,
+ viewMode,
+ hasModelNameField,
+ llmFactory,
+ initialValues,
+ modelInfoList,
+ formRef,
+ baseUrlRegionMaps,
+ onOk,
+ onVerify,
+ onViewModeOk,
+ });
+
+ return (
+ }
+ open={visible || false}
+ onOpenChange={(open) => !open && hideModal?.()}
+ maskClosable={false}
+ footer={
}
+ >
+ {
+ // The actual submission is handled by SavingButton
+ }}
+ ref={formRef}
+ defaultValues={defaultValues}
+ labelClassName="font-normal"
+ >
+ {hasModelNameField && (
+ 0
+ ? listModelsOptions
+ : []
+ }
+ searchPlaceholder={t('listModelsSearchPlaceholder')}
+ emptyText={t('listModelsEmpty')}
+ searchLoading={listLoading}
+ onOpenChange={handleListOpenChange}
+ maxHeight={400}
+ closeOnOutsideClick
+ />
+ )}
+
+
+
+
+ {docLinkText && config.docLink && (
+
+ {docLinkText}
+
+ )}
+
+
+ {
+ hideModal?.();
+ }}
+ />
+ {
+ handleSubmit(values);
+ }}
+ />
+
+
+
+
+ );
+};
+
+export default memo(ProviderModal);
+
+// Export field configurations (for use by other modules)
+export { FACTORIES_WITH_BASE_URL, getProviderConfig } from './field-config';
+export type {
+ FieldConfig,
+ IViewModeOkPayload,
+ ProviderConfig,
+ ProviderModalProps,
+ ShouldRenderToken,
+} from './types';
diff --git a/web/src/pages/user-setting/setting-model/modal/provider-modal/types.ts b/web/src/pages/user-setting/setting-model/modal/provider-modal/types.ts
new file mode 100644
index 0000000000..4ebd845104
--- /dev/null
+++ b/web/src/pages/user-setting/setting-model/modal/provider-modal/types.ts
@@ -0,0 +1,194 @@
+import { FormFieldType } from '@/components/dynamic-form';
+import type { IModelInfo } from '@/interfaces/request/llm';
+import type { ReactNode } from 'react';
+
+/**
+ * Form field types.
+ * - `FormFieldType.*` values map 1:1 to DynamicForm's field types.
+ * - `'inputSelect'` is a project-specific token: a text input combined with a
+ * dropdown of suggested values. The ProviderModal resolves it into
+ * `FormFieldType.Custom` with a custom `render` function.
+ */
+export type FieldType = FormFieldType | 'inputSelect';
+
+/**
+ * String tokens for shouldRender
+ * The component resolves these into actual functions based on runtime context (instanceNameSet, etc.)
+ */
+export type ShouldRenderToken =
+ | 'hideWhenInstanceExists'
+ | 'modelTypeIncludesChat'
+ | 'modelTypeSupportsToolCall'
+ | 'modelTypeIncludesTtsAndNotExists'
+ | 'showBaseUrl'
+ | 'showGroupId';
+
+/**
+ * Option label can be a string or ReactNode (used for rich-text labels in InputSelect).
+ * `regionKey` is the original key from the provider's `url` object (e.g. 'default',
+ * 'intl', 'cn'). It is preserved on the option so that the modal can map the
+ * currently selected URL back to its key for the `region` submit field.
+ */
+export type SelectOption = {
+ label: string | ReactNode;
+ value: string;
+ regionKey?: string;
+};
+
+/**
+ * Resolver for a text value that may differ by factory (provider).
+ * Use when a shared FieldConfig entry needs different i18n keys per provider
+ * (e.g. the generic `base_url` field renders different tooltip / placeholder
+ * for Minimax, TongYiQianWen, SILICONFLOW, etc.).
+ */
+export type FactoryTextResolver = (llmFactory: string) => string;
+
+/**
+ * Field config: defines the presentation and behavior of a form field
+ */
+export interface FieldConfig {
+ /** Field name (supports nested paths, e.g. 'model_info.model_type') */
+ name: string;
+ /** Label i18n key */
+ label: string;
+ /** Field type */
+ type: FieldType;
+ /** Whether the field is required */
+ required?: boolean;
+ /**
+ * Placeholder i18n key. May be a static key, or a function that takes the
+ * current `llmFactory` and returns the key (for per-provider placeholders).
+ */
+ placeholder?: string | FactoryTextResolver;
+ /**
+ * Tooltip i18n key. May be a static key, or a function that takes the
+ * current `llmFactory` and returns the key (for per-provider tooltips).
+ */
+ tooltip?: string | FactoryTextResolver;
+ /** Options (used for select/multiSelect/inputSelect) */
+ options?: SelectOption[];
+ /** Default value */
+ defaultValue?: any;
+ /**
+ * Validation rules.
+ * `message` is treated as an i18n key by the ProviderModal and translated
+ * via `t()` at field-build time. In `Number` fields, `min` / `max` bound
+ * the value; the message is shown when the bound is violated.
+ */
+ validation?: {
+ min?: number;
+ max?: number;
+ message?: string;
+ };
+ /**
+ * Conditional rendering: returns true to show the field
+ * @param values current form values
+ */
+ shouldRender?: ((values: Record) => boolean) | ShouldRenderToken;
+}
+
+/**
+ * Provider config: defines the full behavior of a LLM provider modal
+ */
+export interface ProviderConfig {
+ /** Corresponding LLMFactory value (also used as the field-config key) */
+ llmFactory: string;
+ /** Modal title */
+ title: string;
+ /** Field list (in render order) */
+ fields: FieldConfig[];
+ /**
+ * Transform form values into verify API parameters
+ * Used to construct api_key / base_url / region / model_info when the Verify button is clicked.
+ * `modelInfo` is the array of currently selected models from the list-models picker
+ * (one entry per checked list item). Providers without a list-models picker can ignore it.
+ */
+ verifyTransform?: (
+ values: Record,
+ modelInfo: IModelInfo[],
+ ) => {
+ apiKey: string;
+ baseUrl?: string;
+ region?: string;
+ modelInfo?: IModelInfo[];
+ };
+ /**
+ * Transform form values into submit API parameters.
+ * Used to handle special field name mapping (e.g. volcengine's endpoint_id -> ark_api_key).
+ * `modelInfo` is the array of currently selected models from the list-models picker
+ * (one entry per checked list item). Providers without a list-models picker can ignore it.
+ */
+ submitTransform?: (
+ values: Record,
+ modelInfo: IModelInfo[],
+ ) => Record;
+ /**
+ * Optional link at the bottom of the modal
+ * e.g. the official documentation link for Ollama-family providers
+ */
+ docLink?: string;
+ /**
+ * i18n key for the docLink text (optional)
+ * e.g. 'ollamaLink'; the { name: llmFactory } variable is passed in
+ */
+ docLinkI18nKey?: string;
+ /**
+ * Custom docLink text (optional, takes precedence over docLinkI18nKey)
+ */
+ docLinkText?: string;
+}
+
+/**
+ * Payload for the viewMode save callback. The modal calls `onViewModeOk`
+ * (when provided) instead of `onOk` whenever `viewMode` is true.
+ *
+ * - `instanceName` is the pre-existing instance's name (taken from
+ * `initialValues.instance_name`).
+ * - `llmFactory` is the current provider/factory.
+ * - For LIST_MODEL_PROVIDERS, `modelInfos` carries the full list of
+ * currently checked models in the picker (one IModelInfo per checked
+ * item) and `formValues` is undefined.
+ * - For non-LIST_MODEL_PROVIDERS, the picker is hidden so `modelInfos`
+ * is empty and `formValues` carries the editable model-related form
+ * values (model_name, model_type, max_tokens, is_tools).
+ */
+export interface IViewModeOkPayload {
+ instanceName: string;
+ llmFactory: string;
+ modelInfos: IModelInfo[];
+ formValues?: Record;
+}
+
+/**
+ * ProviderModal component props
+ */
+export interface ProviderModalProps {
+ visible: boolean;
+ hideModal: () => void;
+ llmFactory: string;
+ loading: boolean;
+ editMode?: boolean;
+ /**
+ * Read-only "edit models" mode: opens the modal pre-filled with an
+ * existing instance's data and only allows editing the model-related
+ * fields (model_name, model_type, max_tokens, is_tools) plus the
+ * list-models picker (when applicable). All other fields are disabled.
+ * On save, only `addInstanceModel` is invoked (not `addProviderInstance`).
+ */
+ viewMode?: boolean;
+ initialValues?: Record;
+ /**
+ * Base URL options for the input+select combo (from IAvailableProvider.url)
+ * Used by base_url/api_base fields of type inputSelect
+ */
+ baseUrlOptions?: SelectOption[];
+ onOk: (payload: any, isVerify?: boolean) => Promise;
+ onVerify: (payload: any) => Promise;
+ /**
+ * Save handler used when `viewMode` is true. The modal calls this with
+ * the list of selected models (LIST_MODEL_PROVIDERS) or the editable
+ * model-related form values (non-LIST_MODEL_PROVIDERS). If omitted,
+ * the modal falls back to `onOk` and submits the standard payload.
+ */
+ onViewModeOk?: (payload: IViewModeOkPayload) => Promise;
+}
diff --git a/web/src/pages/user-setting/setting-model/modal/spark-modal/index.tsx b/web/src/pages/user-setting/setting-model/modal/spark-modal/index.tsx
deleted file mode 100644
index 0314c8d552..0000000000
--- a/web/src/pages/user-setting/setting-model/modal/spark-modal/index.tsx
+++ /dev/null
@@ -1,234 +0,0 @@
-import {
- DynamicForm,
- DynamicFormRef,
- FormFieldConfig,
- FormFieldType,
-} from '@/components/dynamic-form';
-import { Modal } from '@/components/ui/modal/modal';
-import { useCommonTranslation, useTranslate } from '@/hooks/common-hooks';
-import { useBuildModelTypeOptions } from '@/hooks/logic-hooks/use-build-options';
-import { IModalProps } from '@/interfaces/common';
-import { IAddProviderInstanceRequestBody } from '@/interfaces/request/llm';
-import {
- useFetchInstanceNameSet,
- useHideWhenInstanceExists,
- VerifyResult,
-} from '@/pages/user-setting/setting-model/hooks';
-import { memo, useCallback, useEffect, useMemo, useRef } from 'react';
-import { FieldValues } from 'react-hook-form';
-import { LLMHeader } from '../../components/llm-header';
-import VerifyButton from '../../modal/verify-button';
-
-const SparkModal = ({
- visible,
- hideModal,
- onOk,
- onVerify,
- loading,
- llmFactory,
-}: IModalProps & {
- llmFactory: string;
- onVerify?: (
- postBody: any,
- ) => Promise;
-}) => {
- const { t } = useTranslate('setting');
- const { t: tc } = useCommonTranslation();
- const { buildModelTypeOptions } = useBuildModelTypeOptions();
- const formRef = useRef(null);
- const { instanceNameSet } = useFetchInstanceNameSet(llmFactory);
-
- const hideWhenInstanceExists = useHideWhenInstanceExists(instanceNameSet);
-
- const fields: FormFieldConfig[] = useMemo(
- () => [
- {
- name: 'instance_name',
- label: t('instanceName'),
- type: FormFieldType.Text,
- required: true,
- placeholder: t('instanceNameMessage'),
- tooltip: t('instanceNameTip'),
- validation: { message: t('instanceNameMessage') },
- },
- {
- name: 'model_type',
- label: t('modelType'),
- type: FormFieldType.MultiSelect,
- required: true,
- options: buildModelTypeOptions(['chat', 'tts']),
- defaultValue: ['chat'],
- },
- {
- name: 'llm_name',
- label: t('modelName'),
- type: FormFieldType.Text,
- required: true,
- placeholder: t('modelNameMessage'),
- validation: {
- message: t('SparkModelNameMessage'),
- },
- },
- {
- name: 'spark_api_password',
- label: t('addSparkAPIPassword'),
- type: FormFieldType.Text,
- required: true,
- placeholder: t('SparkAPIPasswordMessage'),
- validation: {
- message: t('SparkAPIPasswordMessage'),
- },
- shouldRender: hideWhenInstanceExists,
- },
- {
- name: 'spark_app_id',
- label: t('addSparkAPPID'),
- type: FormFieldType.Text,
- required: true,
- placeholder: t('SparkAPPIDMessage'),
- validation: {
- message: t('SparkAPPIDMessage'),
- },
- dependencies: ['model_type', 'instance_name'],
- shouldRender: (formValues: any) => {
- if (!hideWhenInstanceExists(formValues)) return false;
- const modelType = formValues?.model_type;
- if (Array.isArray(modelType)) {
- return modelType.includes('tts');
- }
- return modelType === 'tts';
- },
- },
- {
- name: 'spark_api_secret',
- label: t('addSparkAPISecret'),
- type: FormFieldType.Text,
- required: true,
- placeholder: t('SparkAPISecretMessage'),
- validation: {
- message: t('SparkAPISecretMessage'),
- },
- dependencies: ['model_type', 'instance_name'],
- shouldRender: (formValues: any) => {
- if (!hideWhenInstanceExists(formValues)) return false;
- const modelType = formValues?.model_type;
- if (Array.isArray(modelType)) {
- return modelType.includes('tts');
- }
- return modelType === 'tts';
- },
- },
- {
- name: 'spark_api_key',
- label: t('addSparkAPIKey'),
- type: FormFieldType.Text,
- required: true,
- placeholder: t('SparkAPIKeyMessage'),
- validation: {
- message: t('SparkAPIKeyMessage'),
- },
- dependencies: ['model_type', 'instance_name'],
- shouldRender: (formValues: any) => {
- if (!hideWhenInstanceExists(formValues)) return false;
- const modelType = formValues?.model_type;
- if (Array.isArray(modelType)) {
- return modelType.includes('tts');
- }
- return modelType === 'tts';
- },
- },
- {
- name: 'max_tokens',
- label: t('maxTokens'),
- type: FormFieldType.Number,
- required: true,
- placeholder: t('maxTokensTip'),
- validation: {
- min: 0,
- message: t('maxTokensInvalidMessage'),
- },
- },
- ],
- [t, buildModelTypeOptions, hideWhenInstanceExists],
- );
-
- const handleOk = async (values?: FieldValues) => {
- if (!values) return;
-
- const data = {
- instance_name: values.instance_name as string,
- model_type: values.model_type,
- llm_factory: llmFactory,
- max_tokens: values.max_tokens,
- };
-
- await onOk?.(data as IAddProviderInstanceRequestBody);
- };
-
- const verifyParamsFunc = useCallback(() => {
- const values = formRef.current?.getValues();
- return {
- llm_factory: llmFactory,
- model_type: values.model_type,
- };
- }, [llmFactory]);
-
- const handleVerify = useCallback(
- async (params: any) => {
- const verifyParams = verifyParamsFunc();
- const res = await onVerify?.({ ...params, ...verifyParams });
- return (res || { isValid: null, logs: '' }) as VerifyResult;
- },
- [verifyParamsFunc, onVerify],
- );
-
- useEffect(() => {
- if (!visible) {
- formRef.current?.reset();
- }
- }, [visible]);
-
- return (
- }
- open={visible || false}
- onOpenChange={(open) => !open && hideModal?.()}
- maskClosable={false}
- footer={
}
- >
- {
- console.log(data);
- }}
- ref={formRef}
- defaultValues={
- {
- instance_name: '',
- model_type: ['chat'],
- max_tokens: 8192,
- } as FieldValues
- }
- labelClassName="font-normal"
- >
- {onVerify && }
-
- {
- hideModal?.();
- }}
- />
- {
- handleOk(values);
- }}
- />
-
-
-
- );
-};
-
-export default memo(SparkModal);
diff --git a/web/src/pages/user-setting/setting-model/modal/verify-button/index.tsx b/web/src/pages/user-setting/setting-model/modal/verify-button/index.tsx
index 0720c83fec..a03cf4b7ce 100644
--- a/web/src/pages/user-setting/setting-model/modal/verify-button/index.tsx
+++ b/web/src/pages/user-setting/setting-model/modal/verify-button/index.tsx
@@ -12,12 +12,14 @@ interface IVerifyButton {
onVerify: (params: any) => Promise;
isAbsolute?: boolean;
params?: any;
+ className?: string;
}
const VerifyButton: React.FC = ({
onVerify,
isAbsolute = true,
params,
+ className,
}) => {
const { t, i18n } = useTranslate('setting');
const isArabic = (i18n.resolvedLanguage || i18n.language || '')
@@ -82,6 +84,7 @@ const VerifyButton: React.FC = ({
!isAbsolute || (verifyResult && verifyResult.isValid === false)
? 'flex flex-col gap-5 w-full '
: `absolute bottom-6 z-[100] ${isArabic ? 'right-6' : 'left-6'}`,
+ className,
)}
>
diff --git a/web/src/pages/user-setting/setting-model/modal/volcengine-modal/index.tsx b/web/src/pages/user-setting/setting-model/modal/volcengine-modal/index.tsx
deleted file mode 100644
index 49d0a2a105..0000000000
--- a/web/src/pages/user-setting/setting-model/modal/volcengine-modal/index.tsx
+++ /dev/null
@@ -1,197 +0,0 @@
-import {
- DynamicForm,
- DynamicFormRef,
- FormFieldConfig,
- FormFieldType,
-} from '@/components/dynamic-form';
-import { Modal } from '@/components/ui/modal/modal';
-import { useCommonTranslation, useTranslate } from '@/hooks/common-hooks';
-import { useBuildModelTypeOptions } from '@/hooks/logic-hooks/use-build-options';
-import { IModalProps } from '@/interfaces/common';
-import { IAddProviderInstanceRequestBody } from '@/interfaces/request/llm';
-import {
- useFetchInstanceNameSet,
- useHideWhenInstanceExists,
- VerifyResult,
-} from '@/pages/user-setting/setting-model/hooks';
-import { memo, useCallback, useEffect, useMemo, useRef } from 'react';
-import { FieldValues } from 'react-hook-form';
-import { LLMHeader } from '../../components/llm-header';
-import VerifyButton from '../../modal/verify-button';
-
-type VolcEngineLlmRequest = IAddProviderInstanceRequestBody & {
- endpoint_id: string;
- ark_api_key: string;
-};
-
-const VolcEngineModal = ({
- visible,
- hideModal,
- onOk,
- onVerify,
- loading,
- llmFactory,
-}: IModalProps
& {
- llmFactory: string;
- onVerify?: (
- postBody: any,
- ) => Promise;
-}) => {
- const { t } = useTranslate('setting');
- const { t: tc } = useCommonTranslation();
- const { buildModelTypeOptions } = useBuildModelTypeOptions();
- const formRef = useRef(null);
- const { instanceNameSet } = useFetchInstanceNameSet(llmFactory);
-
- const hideWhenInstanceExists = useHideWhenInstanceExists(instanceNameSet);
-
- const fields: FormFieldConfig[] = useMemo(
- () => [
- {
- name: 'instance_name',
- label: t('instanceName'),
- type: FormFieldType.Text,
- required: true,
- placeholder: t('instanceNameMessage'),
- tooltip: t('instanceNameTip'),
- validation: { message: t('instanceNameMessage') },
- },
- {
- name: 'model_type',
- label: t('modelType'),
- type: FormFieldType.MultiSelect,
- required: true,
- options: buildModelTypeOptions(['chat', 'embedding', 'image2text']),
- defaultValue: ['chat'],
- },
- {
- name: 'llm_name',
- label: t('modelName'),
- type: FormFieldType.Text,
- required: true,
- placeholder: t('volcModelNameMessage'),
- },
- {
- name: 'endpoint_id',
- label: t('addEndpointID'),
- type: FormFieldType.Text,
- required: true,
- placeholder: t('endpointIDMessage'),
- shouldRender: hideWhenInstanceExists,
- },
- {
- name: 'ark_api_key',
- label: t('addArkApiKey'),
- type: FormFieldType.Text,
- required: true,
- placeholder: t('ArkApiKeyMessage'),
- shouldRender: hideWhenInstanceExists,
- },
- {
- name: 'max_tokens',
- label: t('maxTokens'),
- type: FormFieldType.Number,
- required: true,
- placeholder: t('maxTokensTip'),
- validation: {
- min: 0,
- },
- },
- ],
- [t, buildModelTypeOptions, hideWhenInstanceExists],
- );
-
- const handleOk = async (values?: FieldValues) => {
- if (!values) return;
-
- const data: VolcEngineLlmRequest = {
- instance_name: values.instance_name as string,
- llm_factory: llmFactory,
- llm_name: values.llm_name as string,
- model_type: values.model_type,
- endpoint_id: values.endpoint_id as string,
- ark_api_key: values.ark_api_key as string,
- max_tokens: values.max_tokens as number,
- };
-
- await onOk?.(data);
- };
-
- const verifyParamsFunc = useCallback(() => {
- const values = formRef.current?.getValues();
- return {
- llm_factory: llmFactory,
- model_type: values.model_type,
- };
- }, [llmFactory]);
-
- const handleVerify = useCallback(
- async (params: any) => {
- const verifyParams = verifyParamsFunc();
- const res = await onVerify?.({ ...params, ...verifyParams });
- return (res || { isValid: null, logs: '' }) as VerifyResult;
- },
- [verifyParamsFunc, onVerify],
- );
-
- useEffect(() => {
- if (!visible) {
- formRef.current?.reset();
- }
- }, [visible]);
-
- return (
- }
- open={visible || false}
- onOpenChange={(open) => !open && hideModal?.()}
- maskClosable={false}
- footer={
}
- >
- {
- console.log(data);
- }}
- ref={formRef}
- defaultValues={
- {
- instance_name: '',
- model_type: ['chat'],
- max_tokens: 8192,
- } as FieldValues
- }
- labelClassName="font-normal"
- >
- {onVerify && (
-
- )}
-
-
-
- );
-};
-
-export default memo(VolcEngineModal);
diff --git a/web/src/pages/user-setting/setting-model/modal/yiyan-modal/index.tsx b/web/src/pages/user-setting/setting-model/modal/yiyan-modal/index.tsx
deleted file mode 100644
index 7babf637c5..0000000000
--- a/web/src/pages/user-setting/setting-model/modal/yiyan-modal/index.tsx
+++ /dev/null
@@ -1,188 +0,0 @@
-import {
- DynamicForm,
- DynamicFormRef,
- FormFieldConfig,
- FormFieldType,
-} from '@/components/dynamic-form';
-import { Modal } from '@/components/ui/modal/modal';
-import { useCommonTranslation, useTranslate } from '@/hooks/common-hooks';
-import { useBuildModelTypeOptions } from '@/hooks/logic-hooks/use-build-options';
-import { IModalProps } from '@/interfaces/common';
-import { IAddProviderInstanceRequestBody } from '@/interfaces/request/llm';
-import {
- useFetchInstanceNameSet,
- useHideWhenInstanceExists,
- VerifyResult,
-} from '@/pages/user-setting/setting-model/hooks';
-import { memo, useCallback, useMemo, useRef } from 'react';
-import { FieldValues } from 'react-hook-form';
-import { LLMHeader } from '../../components/llm-header';
-import VerifyButton from '../../modal/verify-button';
-
-const YiyanModal = ({
- visible,
- hideModal,
- onOk,
- onVerify,
- loading,
- llmFactory,
-}: IModalProps & {
- llmFactory: string;
- onVerify?: (
- postBody: any,
- ) => Promise;
-}) => {
- const { t } = useTranslate('setting');
- const { t: tc } = useCommonTranslation();
- const { buildModelTypeOptions } = useBuildModelTypeOptions();
- const formRef = useRef(null);
- const { instanceNameSet } = useFetchInstanceNameSet(llmFactory);
-
- const hideWhenInstanceExists = useHideWhenInstanceExists(instanceNameSet);
-
- const fields = useMemo(
- () => [
- {
- name: 'instance_name',
- label: t('instanceName'),
- type: FormFieldType.Text,
- required: true,
- placeholder: t('instanceNameMessage'),
- tooltip: t('instanceNameTip'),
- validation: { message: t('instanceNameMessage') },
- },
- {
- name: 'model_type',
- label: t('modelType'),
- type: FormFieldType.MultiSelect,
- required: true,
- options: buildModelTypeOptions(['chat', 'embedding', 'rerank']),
- defaultValue: ['chat'],
- },
- {
- name: 'llm_name',
- label: t('modelName'),
- type: FormFieldType.Text,
- required: true,
- placeholder: t('yiyanModelNameMessage'),
- },
- {
- name: 'yiyan_ak',
- label: t('addyiyanAK'),
- type: FormFieldType.Text,
- required: true,
- placeholder: t('yiyanAKMessage'),
- shouldRender: hideWhenInstanceExists,
- },
- {
- name: 'yiyan_sk',
- label: t('addyiyanSK'),
- type: FormFieldType.Text,
- required: true,
- placeholder: t('yiyanSKMessage'),
- shouldRender: hideWhenInstanceExists,
- },
- {
- name: 'max_tokens',
- label: t('maxTokens'),
- type: FormFieldType.Number,
- required: true,
- placeholder: t('maxTokensTip'),
- validation: {
- min: 0,
- },
- },
- ],
- [t, buildModelTypeOptions, hideWhenInstanceExists],
- );
-
- const handleOk = async (values?: FieldValues) => {
- if (!values) return;
-
- const data: IAddProviderInstanceRequestBody = {
- instance_name: values.instance_name as string,
- llm_factory: llmFactory,
- llm_name: values.llm_name as string,
- model_type: values.model_type,
- api_key: {
- yiyan_ak: values.yiyan_ak,
- yiyan_sk: values.yiyan_sk,
- },
- max_tokens: values.max_tokens as number,
- };
-
- await onOk?.(data);
- };
-
- const verifyParamsFunc = useCallback(() => {
- const values = formRef.current?.getValues();
- return {
- llm_factory: llmFactory,
- llm_name: values.llm_name as string,
- model_type: values.model_type,
- api_key: {
- yiyan_ak: values.yiyan_ak,
- yiyan_sk: values.yiyan_sk,
- },
- max_tokens: values.max_tokens as number,
- };
- }, [llmFactory]);
-
- const handleVerify = useCallback(
- async (params: any) => {
- const verifyParams = verifyParamsFunc();
- const res = await onVerify?.({ ...params, ...verifyParams });
- return (res || { isValid: null, logs: '' }) as VerifyResult;
- },
- [verifyParamsFunc, onVerify],
- );
-
- return (
- }
- open={visible || false}
- onOpenChange={(open) => !open && hideModal?.()}
- maskClosable={false}
- // footer={
}
- footer={<>>}
- footerClassName="pb-10"
- >
- {
- console.log(data);
- }}
- defaultValues={
- {
- instance_name: '',
- model_type: ['chat'],
- max_tokens: 8192,
- } as FieldValues
- }
- labelClassName="font-normal"
- >
-
- {onVerify &&
}
-
- {
- hideModal?.();
- }}
- />
- {
- handleOk(values);
- }}
- />
-
-
-
-
- );
-};
-
-export default memo(YiyanModal);
diff --git a/web/src/pages/user-setting/setting-model/payload-utils.ts b/web/src/pages/user-setting/setting-model/payload-utils.ts
index 10c654921a..812e4aeab1 100644
--- a/web/src/pages/user-setting/setting-model/payload-utils.ts
+++ b/web/src/pages/user-setting/setting-model/payload-utils.ts
@@ -18,6 +18,7 @@ const INSTANCE_RESERVED_KEYS = new Set([
'base_url',
'region',
'verify',
+ 'model_info',
]);
export const MODEL_EXTRA_KEYS = new Set([
@@ -91,6 +92,7 @@ export const splitProviderPayload = (payload: FlatPayload): SplitResult => {
api_key: collectApiKeyExtras(payload),
base_url: (payload.base_url ?? payload.api_base) as string | undefined,
region: (payload.region as string | undefined) || 'default',
+ model_info: payload.model_info,
};
const modelExtra = collectModelExtras(payload);
diff --git a/web/src/services/llm-service.ts b/web/src/services/llm-service.ts
index cd76d06f73..0d46a7cad3 100644
--- a/web/src/services/llm-service.ts
+++ b/web/src/services/llm-service.ts
@@ -8,6 +8,7 @@ const {
addProvider,
addProviderInstance,
verifyProviderConnection,
+ listProviderModels,
listProviderInstances,
listInstanceModels,
showProviderInstance,
@@ -45,6 +46,10 @@ const methods = {
url: verifyProviderConnection,
method: 'post',
},
+ listProviderModels: {
+ url: listProviderModels,
+ method: 'get',
+ },
listProviderInstances: {
url: listProviderInstances,
method: 'get',
diff --git a/web/src/utils/api.ts b/web/src/utils/api.ts
index b369d217e0..d2efe19bda 100644
--- a/web/src/utils/api.ts
+++ b/web/src/utils/api.ts
@@ -32,6 +32,8 @@ export default {
`${restAPIv1}/providers/${llm_factory}/instances`,
verifyProviderConnection: ({ provider_name }: { provider_name: string }) =>
`${restAPIv1}/providers/${provider_name}/connection`,
+ listProviderModels: ({ provider_name }: { provider_name: string }) =>
+ `${restAPIv1}/providers/${provider_name}/models`,
listProviderInstances: ({ provider_name }: { provider_name: string }) =>
`${restAPIv1}/providers/${provider_name}/instances`,
listInstanceModels: ({