From 23b7936924bb37af0ecc443b662961efcfe84e5a Mon Sep 17 00:00:00 2001 From: jialin Date: Tue, 4 Nov 2025 13:35:47 +0800 Subject: [PATCH] fix: custom backend and versions ux --- src/atoms/models.ts | 12 +- src/locales/en-US/backends.ts | 5 +- src/locales/ja-JP/backends.ts | 5 +- src/locales/ru-RU/backends.ts | 9 +- src/locales/zh-CN/backends.ts | 4 +- src/pages/backends/components/add-modal.tsx | 8 +- src/pages/backends/components/import-yaml.tsx | 17 ++- src/pages/backends/config/index.ts | 6 +- src/pages/backends/config/schema/create.json | 1 + src/pages/backends/forms/basic.tsx | 6 +- src/pages/backends/forms/index.tsx | 13 +- src/pages/backends/forms/versions-config.tsx | 2 +- src/pages/llmodels/config/types.ts | 10 +- src/pages/llmodels/forms/advance-config.tsx | 13 +- src/pages/llmodels/forms/backend.tsx | 118 +++++++++--------- src/pages/llmodels/forms/basic.tsx | 6 +- src/pages/llmodels/forms/custom-backend.tsx | 66 ++++++++++ src/pages/llmodels/forms/index.tsx | 11 +- .../llmodels/hooks/use-query-backends.ts | 39 +++++- 19 files changed, 247 insertions(+), 104 deletions(-) create mode 100644 src/pages/llmodels/forms/custom-backend.tsx diff --git a/src/atoms/models.ts b/src/atoms/models.ts index b4bc1a42..ad55504b 100644 --- a/src/atoms/models.ts +++ b/src/atoms/models.ts @@ -1,3 +1,4 @@ +import { BackendOption } from '@/pages/llmodels/config/types'; import { atom, getDefaultStore } from 'jotai'; // models expand keys: create, update , delete, @@ -24,13 +25,4 @@ export const clusterListAtom = atom< }[] >([]); -export const backendOptionsAtom = atom< - { - value: string; - label: string; - default_backend_param: string[]; - default_version: string; - isBuiltIn: boolean; - versions: { label: string; value: string }[]; - }[] ->([]); +export const backendOptionsAtom = atom([]); diff --git a/src/locales/en-US/backends.ts b/src/locales/en-US/backends.ts index f2c0ba31..1eaed5cd 100644 --- a/src/locales/en-US/backends.ts +++ b/src/locales/en-US/backends.ts @@ -29,5 +29,8 @@ export default { 'backend.versionInfo.autoImage': 'Automatically selected at runtime', 'backend.version.rules.builtin': 'Must end with "-custom"', 'backend.version.no.tips': - 'The name of a custom version must end with "-custom."' + 'The built-in backend’s custom version name must end with "-custom".', + 'backend.backend.rules.custom': + 'The custom backend name must end with "-custom".', + 'backend.quickConfig': 'Quick Config' }; diff --git a/src/locales/ja-JP/backends.ts b/src/locales/ja-JP/backends.ts index f2c0ba31..1eaed5cd 100644 --- a/src/locales/ja-JP/backends.ts +++ b/src/locales/ja-JP/backends.ts @@ -29,5 +29,8 @@ export default { 'backend.versionInfo.autoImage': 'Automatically selected at runtime', 'backend.version.rules.builtin': 'Must end with "-custom"', 'backend.version.no.tips': - 'The name of a custom version must end with "-custom."' + 'The built-in backend’s custom version name must end with "-custom".', + 'backend.backend.rules.custom': + 'The custom backend name must end with "-custom".', + 'backend.quickConfig': 'Quick Config' }; diff --git a/src/locales/ru-RU/backends.ts b/src/locales/ru-RU/backends.ts index be367c0a..954ebdd1 100644 --- a/src/locales/ru-RU/backends.ts +++ b/src/locales/ru-RU/backends.ts @@ -29,10 +29,15 @@ export default { 'backend.versionInfo.autoImage': 'Автоматически выбор во время запуска', 'backend.version.rules.builtin': 'Должен заканчиваться на "-custom"', 'backend.version.no.tips': - 'The name of a custom version must end with "-custom."' + 'The built-in backend’s custom version name must end with "-custom".', + 'backend.backend.rules.custom': + 'The custom backend name must end with "-custom".', + 'backend.quickConfig': 'Quick Config' }; // ========== To-Do: Translate Keys (Remove After Translation) ========== // 1. 'backend.form.defaultExecuteCommand.tips': '{{model_path}} is the model path, and {{port}} is the service port. These variable names cannot be modified.' -// 2. 'backend.version.no.tips': 'The name of a custom version must end with "-custom."' +// 2. 'backend.version.no.tips': 'The built-in backend’s custom version name must end with "-custom".' +// 3. 'backend.backend.rules.custom': 'The custom backend name must end with "-custom".', +// 4. 'backend.quickConfig': 'Quick Config' // ========== End of To-Do List ========== diff --git a/src/locales/zh-CN/backends.ts b/src/locales/zh-CN/backends.ts index e3b2fa31..942a4967 100644 --- a/src/locales/zh-CN/backends.ts +++ b/src/locales/zh-CN/backends.ts @@ -28,5 +28,7 @@ export default { 'backend.noVersion': '未找到版本', 'backend.versionInfo.autoImage': '运行时自动选择', 'backend.version.rules.builtin': '须以 "-custom" 结尾', - 'backend.version.no.tips': '自定义版本名称必须以 "-custom" 结尾。' + 'backend.version.no.tips': '内置后端自定义版本名称必须以 "-custom" 结尾。', + 'backend.backend.rules.custom': '自定义后端名称须以 "-custom" 结尾。', + 'backend.quickConfig': '快速配置' }; diff --git a/src/pages/backends/components/add-modal.tsx b/src/pages/backends/components/add-modal.tsx index 7a6e48c9..5c4139fa 100644 --- a/src/pages/backends/components/add-modal.tsx +++ b/src/pages/backends/components/add-modal.tsx @@ -53,7 +53,8 @@ const AddModal: React.FC = (props) => { const [yamlContent, setYamlContent] = useState(''); const [formContent, setFormContent] = useState({} as FormData); - const genertateCurrentData = (values: ListItem): ListItem => { + // remove '-custom' suffix from version_no in currentData, when action is EDIT + const genertateCurrentVersionData = (values: ListItem): ListItem => { const data = { ...values }; data.version_configs = Object.entries(data.version_configs || {}).reduce( (acc, [key, value]) => { @@ -105,7 +106,7 @@ const AddModal: React.FC = (props) => { useEffect(() => { const iniFormContent = (data: ListItem) => { - const values: any = genertateCurrentData(data); + const values: any = genertateCurrentVersionData(data); // custom versions const versionConfigs = Object.keys(values.version_configs || {}).map( @@ -133,6 +134,7 @@ const AddModal: React.FC = (props) => { return { ...values, + backend_name: values.backend_name.replace(/-custom$/, ''), version_configs: versionConfigs, built_in_version_configs: builtInVersions }; @@ -158,7 +160,7 @@ const AddModal: React.FC = (props) => { if (action === PageAction.EDIT && open) { const yaml = initYamlContent(currentData || {}); - const formData = iniFormContent(currentData || {}); + const formData = iniFormContent(currentData || ({} as ListItem)); setYamlContent(yaml); setFormContent(formData); formRef.current?.setFieldsValue?.(formData); diff --git a/src/pages/backends/components/import-yaml.tsx b/src/pages/backends/components/import-yaml.tsx index 8bf43472..6d177450 100644 --- a/src/pages/backends/components/import-yaml.tsx +++ b/src/pages/backends/components/import-yaml.tsx @@ -104,13 +104,22 @@ const ImportYAML: React.FC = forwardRef( const jsonData = yaml2Json(content); const exsistingVersions = Object.keys(jsonData.version_configs || {}); - const invalidVersion = exsistingVersions.find( - (v) => !v.endsWith('-custom') - ); - if (invalidVersion) { + + if ( + actionStatus.isBuiltIn && + exsistingVersions.find((v) => !v.endsWith('-custom')) + ) { setError(intl.formatMessage({ id: 'backend.version.no.tips' })); return false; } + if ( + !actionStatus.isBuiltIn && + actionStatus.action === PageAction.CREATE && + !jsonData.backend_name.endsWith('-custom') + ) { + setError(intl.formatMessage({ id: 'backend.backend.rules.custom' })); + return false; + } return content; } catch (error) { diff --git a/src/pages/backends/config/index.ts b/src/pages/backends/config/index.ts index a3824e3c..4cc6e9f2 100644 --- a/src/pages/backends/config/index.ts +++ b/src/pages/backends/config/index.ts @@ -144,7 +144,7 @@ export const frameworks = [ ]; export const yamlTemplate = `# backend configuration template -backend_name: my-backend +backend_name: my-backend-custom description: this is my-backend default_version: v0.5.1 health_check_path: /v1/models @@ -152,11 +152,11 @@ default_backend_param: - --host default_run_command: myBackend serve {{model_path}} --port {{port}} version_configs: - v0.0.1-custom: + v0.0.1: image_name: lm/mybackend:latest run_command: myBackend serve {{model_path}} --port {{port}} custom_framework: cuda - v0.0.2-custom: + v0.0.2: image_name: lm/mybackend:test run_command: custom_framework: diff --git a/src/pages/backends/config/schema/create.json b/src/pages/backends/config/schema/create.json index a44ee562..d667e07c 100644 --- a/src/pages/backends/config/schema/create.json +++ b/src/pages/backends/config/schema/create.json @@ -3,6 +3,7 @@ "properties": { "backend_name": { "type": "string", + "pattern": "^-custom$", "description": "backend name", "severity": "error" }, diff --git a/src/pages/backends/forms/basic.tsx b/src/pages/backends/forms/basic.tsx index 31b67108..d9cf31ea 100644 --- a/src/pages/backends/forms/basic.tsx +++ b/src/pages/backends/forms/basic.tsx @@ -7,12 +7,13 @@ import useAppUtils from '@/hooks/use-app-utils'; import { useIntl } from '@umijs/max'; import { Form } from 'antd'; import React from 'react'; -import { FormData } from '../config/types'; +import { FormData, ListItem } from '../config/types'; type AddModalProps = { action: PageActionType; + currentData?: ListItem; }; -const BasicForm: React.FC = ({ action }) => { +const BasicForm: React.FC = ({ action, currentData }) => { const form = Form.useFormInstance(); const intl = useIntl(); const { getRuleMessage } = useAppUtils(); @@ -30,6 +31,7 @@ const BasicForm: React.FC = ({ action }) => { > = forwardRef( }; const handleOnFinish = (values: FormData) => { - const data = { ...values }; + const data = { + ...values, + backend_name: currentData?.is_built_in + ? values.backend_name + : `${values.backend_name}-custom` + }; data.version_configs = data.version_configs?.map((item) => { if (item.version_no) { return { ...item, - version_no: `${item.version_no}-custom` + version_no: currentData?.is_built_in + ? `${item.version_no}-custom` + : item.version_no }; } return item; @@ -82,7 +89,7 @@ const BackendForm: React.FC = forwardRef( initialValues={_.omit(currentData, ['version_configs'])} onFinishFailed={onFinishFailed} > - + = ({ > { const intl = useIntl(); @@ -17,7 +19,7 @@ const AdvanceConfig = () => { const EnviromentVars = Form.useWatch('env', form); const scheduleType = Form.useWatch('scheduleType', form); const backend = Form.useWatch('backend', form); - const { onValuesChange } = useFormContext(); + const { onValuesChange, formKey } = useFormContext(); const handleEnviromentVarsChange = useCallback( (labels: Record) => { @@ -62,8 +64,9 @@ const AdvanceConfig = () => { options={modelCategories} > - - + {formKey === DeployFormKeyMap.CATALOG && } + + name="env"> { const intl = useIntl(); const { getRuleMessage } = useAppUtils(); const form = Form.useFormInstance(); - const { - isGGUF, - formKey, - action, - source, - gpuOptions, - onValuesChange, - backendOptions, - onBackendChange - } = useFormContext(); + const { onValuesChange, backendOptions, onBackendChange } = useFormContext(); const backend = Form.useWatch('backend', form); const handleBackendVersionOnBlur = () => { @@ -45,10 +41,53 @@ const BackendFields: React.FC = () => { if (!backend || backend === backendOptionsMap.custom) { return []; } - return ( - backendOptions.find((item) => item.value === backend)?.versions || [] + + // find the backend item from backendOptions + const backendItem = backendOptions + .flatMap((group) => group.options) + .find((item) => item.value === backend); + + const versions = backendItem?.versions || []; + + // if it's a custom backend, + if (backendItem && !backendItem.isBuiltIn) { + return versions; + } + + // check the value if endts with '-custom', if true, remove the suffix add to "Cutom" group, if not , add to "Built-in" group + const builtInVersions = versions.filter( + (item) => !item.value?.endsWith('-custom') ); - }, [backend, backendOptions]); + const customVersions = versions.filter((item) => + item.value?.endsWith('-custom') + ); + + const options = []; + + if (builtInVersions.length > 0) { + options.push({ + label: intl.formatMessage({ id: 'backend.builtin' }), + options: builtInVersions + }); + } + + if (customVersions.length > 0) { + options.push({ + label: intl.formatMessage({ id: 'backend.custom' }), + options: customVersions + }); + } + + return options; + }, [backend, backendOptions, intl]); + + const optionRender = (option: any) => { + return option.data.title; + }; + + const labelRender = (option: any) => { + return option.title; + }; return ( <> @@ -67,12 +106,16 @@ const BackendFields: React.FC = () => { label={intl.formatMessage({ id: 'models.form.backend' })} description={} options={backendOptions} + optionRender={optionRender} + labelRender={labelRender} > {backendOptionsMap.custom !== backend && ( { > )} - {backend === backendOptionsMap.custom && ( - <> - - name="image_name" - rules={[ - { - required: true, - message: getRuleMessage('input', 'backend.imageName') - } - ]} - > - - - - name="run_command" - rules={[ - { - required: true, - message: getRuleMessage('input', 'backend.runCommand') - } - ]} - > - - - - )} - ); }; diff --git a/src/pages/llmodels/forms/basic.tsx b/src/pages/llmodels/forms/basic.tsx index 10e36f84..0beb3e18 100644 --- a/src/pages/llmodels/forms/basic.tsx +++ b/src/pages/llmodels/forms/basic.tsx @@ -8,8 +8,10 @@ import { import { useIntl } from '@umijs/max'; import { Form } from 'antd'; import { useMemo } from 'react'; -import { sourceOptions } from '../config'; +import { DeployFormKeyMap, sourceOptions } from '../config'; +import { useFormContext } from '../config/form-context'; import { FormData } from '../config/types'; +import BackendForm from './backend'; import LocalPathSource from './local-path-source'; import OnlineSource from './online-source'; @@ -32,6 +34,7 @@ const BasicForm: React.FC = (props) => { onSourceChange } = props; const intl = useIntl(); + const { formKey } = useFormContext(); const { getRuleMessage } = useAppUtils(); const form = Form.useFormInstance(); @@ -115,6 +118,7 @@ const BasicForm: React.FC = (props) => { > } + {formKey === DeployFormKeyMap.DEPLOYMENT && } name="replicas" rules={[ diff --git a/src/pages/llmodels/forms/custom-backend.tsx b/src/pages/llmodels/forms/custom-backend.tsx new file mode 100644 index 00000000..16399ffb --- /dev/null +++ b/src/pages/llmodels/forms/custom-backend.tsx @@ -0,0 +1,66 @@ +import SealInput from '@/components/seal-form/seal-input'; +import SealTextArea from '@/components/seal-form/seal-textarea'; +import useAppUtils from '@/hooks/use-app-utils'; +import { useIntl } from '@umijs/max'; +import { Form } from 'antd'; +import React from 'react'; +import { backendOptionsMap } from '../config/backend-parameters'; +import { FormData } from '../config/types'; + +const CustomBackend: React.FC = () => { + const intl = useIntl(); + const { getRuleMessage } = useAppUtils(); + const form = Form.useFormInstance(); + const backend = Form.useWatch('backend', form); + + return ( + <> + {backend === backendOptionsMap.custom && ( + <> + + name="image_name" + rules={[ + { + required: true, + message: getRuleMessage('input', 'backend.imageName') + } + ]} + > + + + + name="run_command" + rules={[ + { + required: true, + message: getRuleMessage('input', 'backend.runCommand') + } + ]} + > + + + + )} + + ); +}; + +export default CustomBackend; diff --git a/src/pages/llmodels/forms/index.tsx b/src/pages/llmodels/forms/index.tsx index a07fce85..6432c10b 100644 --- a/src/pages/llmodels/forms/index.tsx +++ b/src/pages/llmodels/forms/index.tsx @@ -16,7 +16,7 @@ import { import { backendOptionsMap } from '../config/backend-parameters'; import { FormContext } from '../config/form-context'; import { - BackendOption, + BackendGroupOption, DeployFormKey, FormData, SourceType @@ -188,7 +188,7 @@ const DataForm: React.FC = forwardRef((props, ref) => { }; }; - const updateKVCacheConfig = (backend: string, option: BackendOption) => { + const updateKVCacheConfig = (backend: string, option: BackendGroupOption) => { if ( !option.isBuiltIn && [backendOptionsMap.SGLang, backendOptionsMap.vllm].includes(backend) @@ -202,7 +202,12 @@ const DataForm: React.FC = forwardRef((props, ref) => { return {}; }; - const handleBackendChange = async (val: string, option: BackendOption) => { + const handleBackendChange = async ( + val: string, + option: BackendGroupOption + ) => { + console.log('handleBackendChange option===>', val, option); + const isGGUF = checkIsGGUF(); form.setFieldsValue({ env: null, backend_version: option.default_version || '', diff --git a/src/pages/llmodels/hooks/use-query-backends.ts b/src/pages/llmodels/hooks/use-query-backends.ts index 418586c2..9084acb2 100644 --- a/src/pages/llmodels/hooks/use-query-backends.ts +++ b/src/pages/llmodels/hooks/use-query-backends.ts @@ -1,28 +1,59 @@ import { backendOptionsAtom } from '@/atoms/models'; +import { useIntl } from '@umijs/max'; import { useAtom } from 'jotai'; import { queryBackendList } from '../apis'; +import { backendOptionsMap } from '../config/backend-parameters'; +import { BackendGroupOption } from '../config/types'; export default function useQueryBackends() { const [backendOptions, setBackendOptions] = useAtom(backendOptionsAtom); + const intl = useIntl(); const getBackendOptions = async (params?: { cluster_id: number }) => { try { const res = await queryBackendList(params); - const list = res?.items?.map((item) => { + const list: BackendGroupOption[] = res?.items?.map((item) => { return { value: item.backend_name, - label: item.backend_name, + label: + item.backend_name === backendOptionsMap.custom + ? intl.formatMessage({ id: 'backend.quickConfig' }) + : item.backend_name, + title: + item.backend_name === backendOptionsMap.custom + ? intl.formatMessage({ id: 'backend.quickConfig' }) + : item.backend_name.replace(/-custom$/, ''), default_backend_param: item.default_backend_param || [], default_version: item.default_version, isBuiltIn: item.is_built_in, versions: (item.versions || []).map((vItem) => ({ label: vItem.version, - value: vItem.version + value: vItem.version, + title: vItem.version.replace(/-custom$/, '') })) }; }); + const builtInBackends = list?.filter((item) => item.isBuiltIn); + const customBackends = list?.filter((item) => !item.isBuiltIn); + + const options = []; + + if (builtInBackends && builtInBackends.length > 0) { + options.push({ + label: intl.formatMessage({ id: 'backend.builtin' }), + options: builtInBackends + }); + } + + if (customBackends && customBackends.length > 0) { + options.push({ + label: intl.formatMessage({ id: 'backend.custom' }), + options: customBackends + }); + } + if (res?.items) { - setBackendOptions(list || []); + setBackendOptions(options); } return list || []; } catch (error) {