diff --git a/web/app/components/app/configuration/index.tsx b/web/app/components/app/configuration/index.tsx index 2b97a64f5b..dd1faeeaa3 100644 --- a/web/app/components/app/configuration/index.tsx +++ b/web/app/components/app/configuration/index.tsx @@ -80,6 +80,9 @@ import { import PluginDependency from '@/app/components/workflow/plugin-dependency' import { supportFunctionCall } from '@/utils/tool-call' import { MittProvider } from '@/context/mitt-context' +import { mergeValidCompletionParams } from '@/utils/completion-params' +import { fetchModelParameterRules } from '@/service/common' +import Toast from '@/app/components/base/toast' type PublishConfig = { modelConfig: ModelConfig @@ -453,7 +456,21 @@ const Configuration: FC = () => { ...visionConfig, enabled: supportVision, }, true) - setCompletionParams({}) + + // merge and keep only valid completion params for the new model + try { + const url = `/workspaces/current/model-providers/${provider}/models/parameter-rules?model=${modelId}` + const { data: parameterRules } = await fetchModelParameterRules(url) + const { params: filtered, removedDetails } = mergeValidCompletionParams(completionParams, parameterRules ?? []) + if (Object.keys(removedDetails).length) + Toast.notify({ type: 'warning', message: `${t('common.modelProvider.parametersInvalidRemoved')}: ` + Object.entries(removedDetails).map(([k, reason]) => `${k} (${reason})`).join(', ') }) + setCompletionParams(filtered) + } + catch (e) { + // if fetch fails just clear invalid params + Toast.notify({ type: 'error', message: t('common.error') }) + setCompletionParams({}) + } } const isShowVisionConfig = !!currModel?.features?.includes(ModelFeatureEnum.vision) diff --git a/web/app/components/workflow/nodes/llm/panel.tsx b/web/app/components/workflow/nodes/llm/panel.tsx index 29fb4fb2c3..9e9380afaf 100644 --- a/web/app/components/workflow/nodes/llm/panel.tsx +++ b/web/app/components/workflow/nodes/llm/panel.tsx @@ -23,6 +23,9 @@ import Editor from '@/app/components/workflow/nodes/_base/components/prompt/edit import StructureOutput from './components/structure-output' import Switch from '@/app/components/base/switch' import { RiAlertFill, RiQuestionLine } from '@remixicon/react' +import { mergeValidCompletionParams } from '@/utils/completion-params' +import { fetchModelParameterRules } from '@/service/common' +import Toast from '@/app/components/base/toast' const i18nPrefix = 'workflow.nodes.llm' @@ -137,10 +140,21 @@ const Panel: FC> = ({ modelId: string mode?: string }) => { - handleCompletionParamsChange({}) - handleModelChanged(model) - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []) + (async () => { + try { + const url = `/workspaces/current/model-providers/${model.provider}/models/parameter-rules?model=${model.modelId}` + const { data: parameterRules } = await fetchModelParameterRules(url) + const { params: filtered, removedDetails } = mergeValidCompletionParams(inputs.model.completion_params, parameterRules ?? []) + const keys = Object.keys(removedDetails) + if (keys.length) + Toast.notify({ type: 'warning', message: `${t('common.modelProvider.parametersInvalidRemoved')}: ` + keys.map(k => `${k} (${removedDetails[k]})`).join(', ') }) + handleCompletionParamsChange(filtered) + } + finally { + handleModelChanged(model) + } + })() + }, [inputs.model.completion_params]) return (
diff --git a/web/app/components/workflow/nodes/parameter-extractor/use-config.ts b/web/app/components/workflow/nodes/parameter-extractor/use-config.ts index 045737b230..c7535de38e 100644 --- a/web/app/components/workflow/nodes/parameter-extractor/use-config.ts +++ b/web/app/components/workflow/nodes/parameter-extractor/use-config.ts @@ -17,6 +17,9 @@ import useNodeCrud from '@/app/components/workflow/nodes/_base/hooks/use-node-cr import { checkHasQueryBlock } from '@/app/components/base/prompt-editor/constants' import useAvailableVarList from '@/app/components/workflow/nodes/_base/hooks/use-available-var-list' import { supportFunctionCall } from '@/utils/tool-call' +import { mergeValidCompletionParams } from '@/utils/completion-params' +import { fetchModelParameterRules } from '@/service/common' +import Toast from '@/app/components/base/toast' const useConfig = (id: string, payload: ParameterExtractorNodeType) => { const { nodesReadOnly: readOnly } = useNodesReadOnly() @@ -117,16 +120,31 @@ const useConfig = (id: string, payload: ParameterExtractorNodeType) => { } = useModelListAndDefaultModelAndCurrentProviderAndModel(ModelTypeEnum.textGeneration) const handleModelChanged = useCallback((model: { provider: string; modelId: string; mode?: string }) => { - const newInputs = produce(inputRef.current, (draft) => { - draft.model.provider = model.provider - draft.model.name = model.modelId - draft.model.mode = model.mode! - const isModeChange = model.mode !== inputRef.current.model?.mode - if (isModeChange && defaultConfig && Object.keys(defaultConfig).length > 0) - appendDefaultPromptConfig(draft, defaultConfig, model.mode === 'chat') - }) - setInputs(newInputs) - setModelChanged(true) + (async () => { + const newInputs = produce(inputRef.current, (draft) => { + draft.model.provider = model.provider + draft.model.name = model.modelId + draft.model.mode = model.mode! + const isModeChange = model.mode !== inputRef.current.model?.mode + if (isModeChange && defaultConfig && Object.keys(defaultConfig).length > 0) + appendDefaultPromptConfig(draft, defaultConfig, model.mode === 'chat') + }) + + // filter completion params + try { + const url = `/workspaces/current/model-providers/${model.provider}/models/parameter-rules?model=${model.modelId}` + const { data: parameterRules } = await fetchModelParameterRules(url) + const { params: filtered, removedDetails } = mergeValidCompletionParams(inputRef.current.model.completion_params, parameterRules ?? []) + if (Object.keys(removedDetails).length) + Toast.notify({ type: 'warning', message: `${t('common.modelProvider.parametersInvalidRemoved')}: ` + Object.entries(removedDetails).map(([k, reason]) => `${k} (${reason})`).join(', ') }) + newInputs.model.completion_params = filtered + } + catch { + // ignore errors + } + setInputs(newInputs) + setModelChanged(true) + })() }, [setInputs, defaultConfig, appendDefaultPromptConfig]) useEffect(() => { diff --git a/web/app/components/workflow/nodes/question-classifier/use-config.ts b/web/app/components/workflow/nodes/question-classifier/use-config.ts index 7df8293b40..056bacd84c 100644 --- a/web/app/components/workflow/nodes/question-classifier/use-config.ts +++ b/web/app/components/workflow/nodes/question-classifier/use-config.ts @@ -15,6 +15,9 @@ import useOneStepRun from '@/app/components/workflow/nodes/_base/hooks/use-one-s import { useModelListAndDefaultModelAndCurrentProviderAndModel } from '@/app/components/header/account-setting/model-provider-page/hooks' import { ModelTypeEnum } from '@/app/components/header/account-setting/model-provider-page/declarations' import { checkHasQueryBlock } from '@/app/components/base/prompt-editor/constants' +import { mergeValidCompletionParams } from '@/utils/completion-params' +import { fetchModelParameterRules } from '@/service/common' +import Toast from '@/app/components/base/toast' const useConfig = (id: string, payload: QuestionClassifierNodeType) => { const { nodesReadOnly: readOnly } = useNodesReadOnly() @@ -55,13 +58,26 @@ const useConfig = (id: string, payload: QuestionClassifierNodeType) => { }) const handleModelChanged = useCallback((model: { provider: string; modelId: string; mode?: string }) => { - const newInputs = produce(inputRef.current, (draft) => { - draft.model.provider = model.provider - draft.model.name = model.modelId - draft.model.mode = model.mode! - }) - setInputs(newInputs) - setModelChanged(true) + (async () => { + const newInputs = produce(inputRef.current, (draft) => { + draft.model.provider = model.provider + draft.model.name = model.modelId + draft.model.mode = model.mode! + }) + try { + const url = `/workspaces/current/model-providers/${model.provider}/models/parameter-rules?model=${model.modelId}` + const { data: parameterRules } = await fetchModelParameterRules(url) + const { params: filtered, removedDetails } = mergeValidCompletionParams(inputRef.current.model.completion_params, parameterRules ?? []) + if (Object.keys(removedDetails).length) + Toast.notify({ type: 'warning', message: `${t('common.modelProvider.parametersInvalidRemoved')}: ` + Object.entries(removedDetails).map(([k, reason]) => `${k} (${reason})`).join(', ') }) + newInputs.model.completion_params = filtered + } + catch { + // ignore + } + setInputs(newInputs) + setModelChanged(true) + })() }, [setInputs]) useEffect(() => { diff --git a/web/utils/completion-params.ts b/web/utils/completion-params.ts new file mode 100644 index 0000000000..db191e6ed4 --- /dev/null +++ b/web/utils/completion-params.ts @@ -0,0 +1,78 @@ +import type { FormValue, ModelParameterRule } from '@/app/components/header/account-setting/model-provider-page/declarations' + +// Returns a new object that contains only the keys accepted by `rules` and whose +// values conform to the rule's basic validation (range/options). +export const mergeValidCompletionParams = ( + oldParams: FormValue | undefined, + rules: ModelParameterRule[], +): { params: FormValue; removedDetails: Record } => { + if (!oldParams || Object.keys(oldParams).length === 0) + return { params: {}, removedDetails: {} } + + const acceptedKeys = new Set(rules.map(r => r.name)) + const ruleMap: Record = {} + rules.forEach(r => { + ruleMap[r.name] = r + }) + + const nextParams: FormValue = {} + const removedDetails: Record = {} + + Object.entries(oldParams).forEach(([key, value]) => { + if (!acceptedKeys.has(key)) + { removedDetails[key] = 'unsupported'; return } + + const rule = ruleMap[key] + if (!rule) + { removedDetails[key] = 'unsupported'; return } + + switch (rule.type) { + case 'int': + case 'float': { + if (typeof value !== 'number') { removedDetails[key] = 'invalid type'; return } + const min = rule.min ?? Number.NEGATIVE_INFINITY + const max = rule.max ?? Number.POSITIVE_INFINITY + if (value < min || value > max) { removedDetails[key] = `out of range (${min}-${max})`; return } + nextParams[key] = value + return + } + case 'boolean': { + if (typeof value === 'boolean') + nextParams[key] = value + else + removedDetails[key] = 'invalid type' + return + } + case 'string': + case 'text': { + if (typeof value === 'string') { + if (Array.isArray(rule.options) && rule.options.length) { + if (rule.options.some(opt => opt.value === value)) + nextParams[key] = value + else + removedDetails[key] = 'unsupported option' + } + else { + nextParams[key] = value + } + } + else { + removedDetails[key] = 'invalid type' + } + return + } + case 'tag': { + if (Array.isArray(value)) + nextParams[key] = value + else + removedDetails[key] = 'invalid type' + return + } + default: { + nextParams[key] = value + } + } + }) + + return { params: nextParams, removedDetails } +} \ No newline at end of file