Validate and filter model completion params on change

Introduces a utility to merge and validate completion parameters against model-specific rules when changing models in configuration and workflow nodes. Invalid or unsupported parameters are removed, and users are notified via toast messages. This ensures only valid parameters are retained for the selected model, improving robustness and user feedback.
pull/21247/head
Kalo Chin 11 months ago
parent 6b1ad634f1
commit 5104fe3c5f

@ -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,8 +456,22 @@ const Configuration: FC = () => {
...visionConfig,
enabled: supportVision,
}, true)
// 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)
const isShowDocumentConfig = !!currModel?.features?.includes(ModelFeatureEnum.document)

@ -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<NodePanelProps<LLMNodeType>> = ({
modelId: string
mode?: string
}) => {
handleCompletionParamsChange({})
(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)
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [])
}
})()
}, [inputs.model.completion_params])
return (
<div className='mt-2'>

@ -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,6 +120,7 @@ const useConfig = (id: string, payload: ParameterExtractorNodeType) => {
} = useModelListAndDefaultModelAndCurrentProviderAndModel(ModelTypeEnum.textGeneration)
const handleModelChanged = useCallback((model: { provider: string; modelId: string; mode?: string }) => {
(async () => {
const newInputs = produce(inputRef.current, (draft) => {
draft.model.provider = model.provider
draft.model.name = model.modelId
@ -125,8 +129,22 @@ const useConfig = (id: string, payload: ParameterExtractorNodeType) => {
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(() => {

@ -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 }) => {
(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(() => {

@ -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<string, string> } => {
if (!oldParams || Object.keys(oldParams).length === 0)
return { params: {}, removedDetails: {} }
const acceptedKeys = new Set(rules.map(r => r.name))
const ruleMap: Record<string, ModelParameterRule> = {}
rules.forEach(r => {
ruleMap[r.name] = r
})
const nextParams: FormValue = {}
const removedDetails: Record<string, string> = {}
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 }
}
Loading…
Cancel
Save