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.
This commit is contained in:
jialin
2026-07-13 17:14:43 +08:00
committed by jialin
parent 35ed397c2a
commit d0b6498bda
3 changed files with 43 additions and 4 deletions
+6 -2
View File
@@ -85,6 +85,7 @@ interface BasicFormProps {
} }
>[]; >[];
handleClusterChange: (value: number) => void; handleClusterChange: (value: number) => void;
onClusterSeed: (value: number) => void;
onSourceChange?: (value: string) => void; onSourceChange?: (value: string) => void;
} }
@@ -94,6 +95,7 @@ const BasicForm: React.FC<BasicFormProps> = (props) => {
clusterList, clusterList,
sourceDisable, sourceDisable,
handleClusterChange, handleClusterChange,
onClusterSeed,
onSourceChange onSourceChange
} = props; } = props;
const intl = useIntl(); const intl = useIntl();
@@ -148,6 +150,8 @@ const BasicForm: React.FC<BasicFormProps> = (props) => {
// selection and fall back to the scope's default cluster (then a Ready one, // 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 // 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. // 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(() => { useEffect(() => {
if (!clusterOptions?.length) { if (!clusterOptions?.length) {
return; return;
@@ -167,8 +171,8 @@ const BasicForm: React.FC<BasicFormProps> = (props) => {
return; return;
} }
form.setFieldValue('cluster_id', next); form.setFieldValue('cluster_id', next);
handleClusterChange?.(next); onClusterSeed?.(next);
}, [clusterOptions, form, handleClusterChange]); }, [clusterOptions, form, onClusterSeed]);
const clusterOptionRender = (option: any) => { const clusterOptionRender = (option: any) => {
const { data } = option; const { data } = option;
+18 -2
View File
@@ -236,20 +236,35 @@ const DataForm: React.FC<DataFormProps> = forwardRef((props, ref) => {
onOk(allValues); onOk(allValues);
}; };
const handleClusterChange = async (value: number) => { // Shared work when the target cluster changes: refetch the GPU/backend
await onClusterChange?.(value); // options for the new cluster and reset schedule/gpu selection.
const applyClusterScopedOptions = (value: number) => {
getGPUOptionList({ clusterId: value }); getGPUOptionList({ clusterId: value });
getBackendOptions({ cluster_id: value }); getBackendOptions({ cluster_id: value });
form.setFieldsValue({ form.setFieldsValue({
scheduleType: ScheduleValueMap.Auto, scheduleType: ScheduleValueMap.Auto,
gpu_selector: null 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) => { await new Promise((resolve) => {
setTimeout(resolve, 150); setTimeout(resolve, 150);
}); });
onValuesChange?.({}, form.getFieldsValue()); 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<string, any>, prefix = ''): string => { const getFieldPaths = (obj: Record<string, any>, prefix = ''): string => {
const result = Object.entries(obj).flatMap(([key, value]) => { const result = Object.entries(obj).flatMap(([key, value]) => {
const path = prefix ? `${prefix}.${key}` : key; const path = prefix ? `${prefix}.${key}` : key;
@@ -467,6 +482,7 @@ const DataForm: React.FC<DataFormProps> = forwardRef((props, ref) => {
clusterList={clusterList} clusterList={clusterList}
sourceDisable={sourceDisable} sourceDisable={sourceDisable}
handleClusterChange={handleClusterChange} handleClusterChange={handleClusterChange}
onClusterSeed={handleClusterSeed}
onSourceChange={onSourceChange} onSourceChange={onSourceChange}
></BasicForm> ></BasicForm>
<CollapsePanel <CollapsePanel
+19
View File
@@ -401,6 +401,24 @@ export const useCheckCompatibility = () => {
); );
}; };
// 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: { const handleOnValuesChange = async (params: {
changedValues: any; changedValues: any;
allValues: any; allValues: any;
@@ -410,6 +428,7 @@ export const useCheckCompatibility = () => {
if ( if (
_.isEqual(cacheFormValuesRef.current, allValues) || _.isEqual(cacheFormValuesRef.current, allValues) ||
noLocalPathValue(allValues) || noLocalPathValue(allValues) ||
noModelSelected(allValues) ||
!allValues.replicas !allValues.replicas
) { ) {
console.log('No changes detected, skipping evaluation.'); console.log('No changes detected, skipping evaluation.');