From d0b6498bdabc434546024a73d4a8d4265ce28d5e Mon Sep 17 00:00:00 2001 From: jialin Date: Mon, 13 Jul 2026 17:10:03 +0800 Subject: [PATCH] fix(llmodels): don't evaluate on cluster seed before a model is selected The basic form seeds a default cluster on open, which fired the full cluster-change handler and triggered an evaluate request before any model was picked. Split off a seed callback that refreshes scoped options without evaluating, and guard the evaluate handler with a model-selected check as a backstop. --- src/pages/llmodels/forms/basic.tsx | 8 ++++++-- src/pages/llmodels/forms/index.tsx | 20 ++++++++++++++++++-- src/pages/llmodels/hooks/index.ts | 19 +++++++++++++++++++ 3 files changed, 43 insertions(+), 4 deletions(-) diff --git a/src/pages/llmodels/forms/basic.tsx b/src/pages/llmodels/forms/basic.tsx index 9e912e05..0561630e 100644 --- a/src/pages/llmodels/forms/basic.tsx +++ b/src/pages/llmodels/forms/basic.tsx @@ -85,6 +85,7 @@ interface BasicFormProps { } >[]; handleClusterChange: (value: number) => void; + onClusterSeed: (value: number) => void; onSourceChange?: (value: string) => void; } @@ -94,6 +95,7 @@ const BasicForm: React.FC = (props) => { clusterList, sourceDisable, handleClusterChange, + onClusterSeed, onSourceChange } = props; const intl = useIntl(); @@ -148,6 +150,8 @@ const BasicForm: React.FC = (props) => { // selection and fall back to the scope's default cluster (then a Ready one, // then the first) so GPU/backend options refetch for it. A selection that's // still valid is left untouched, so a user's (or edit's) choice is kept. + // Use the seed callback (not handleClusterChange) so this auto-pick refreshes + // options without firing an evaluate request before a model is selected. useEffect(() => { if (!clusterOptions?.length) { return; @@ -167,8 +171,8 @@ const BasicForm: React.FC = (props) => { return; } form.setFieldValue('cluster_id', next); - handleClusterChange?.(next); - }, [clusterOptions, form, handleClusterChange]); + onClusterSeed?.(next); + }, [clusterOptions, form, onClusterSeed]); const clusterOptionRender = (option: any) => { const { data } = option; diff --git a/src/pages/llmodels/forms/index.tsx b/src/pages/llmodels/forms/index.tsx index 7d9d96d3..2863905b 100644 --- a/src/pages/llmodels/forms/index.tsx +++ b/src/pages/llmodels/forms/index.tsx @@ -236,20 +236,35 @@ const DataForm: React.FC = forwardRef((props, ref) => { onOk(allValues); }; - const handleClusterChange = async (value: number) => { - await onClusterChange?.(value); + // Shared work when the target cluster changes: refetch the GPU/backend + // options for the new cluster and reset schedule/gpu selection. + const applyClusterScopedOptions = (value: number) => { getGPUOptionList({ clusterId: value }); getBackendOptions({ cluster_id: value }); form.setFieldsValue({ scheduleType: ScheduleValueMap.Auto, gpu_selector: null }); + }; + + // User explicitly picked a cluster: refresh scoped options and re-evaluate. + const handleClusterChange = async (value: number) => { + await onClusterChange?.(value); + applyClusterScopedOptions(value); await new Promise((resolve) => { setTimeout(resolve, 150); }); onValuesChange?.({}, form.getFieldsValue()); }; + // The basic form seeds a default cluster on open, before a model is picked. + // Refresh scoped options for it but don't fire the evaluate request — there + // is no model to evaluate yet. + const handleClusterSeed = async (value: number) => { + await onClusterChange?.(value); + applyClusterScopedOptions(value); + }; + const getFieldPaths = (obj: Record, prefix = ''): string => { const result = Object.entries(obj).flatMap(([key, value]) => { const path = prefix ? `${prefix}.${key}` : key; @@ -467,6 +482,7 @@ const DataForm: React.FC = forwardRef((props, ref) => { clusterList={clusterList} sourceDisable={sourceDisable} handleClusterChange={handleClusterChange} + onClusterSeed={handleClusterSeed} onSourceChange={onSourceChange} > { ); }; + // Evaluation needs a model reference. The basic form seeds a default cluster + // on open, which can fire onValuesChange before the user has picked a model — + // skip evaluation until the current source's model field is filled. + const noModelSelected = (allValues: any) => { + switch (allValues.source) { + case modelSourceMap.huggingface_value: + return !allValues.huggingface_repo_id; + case modelSourceMap.modelscope_value: + return !allValues.model_scope_model_id; + case modelSourceMap.ollama_library_value: + return !allValues.ollama_library_model_name; + case modelSourceMap.local_path_value: + return !allValues.local_path; + default: + return false; + } + }; + const handleOnValuesChange = async (params: { changedValues: any; allValues: any; @@ -410,6 +428,7 @@ export const useCheckCompatibility = () => { if ( _.isEqual(cacheFormValuesRef.current, allValues) || noLocalPathValue(allValues) || + noModelSelected(allValues) || !allValues.replicas ) { console.log('No changes detected, skipping evaluation.');