diff --git a/web/app/components/app/configuration/index.tsx b/web/app/components/app/configuration/index.tsx index 2b97a64f5..947467bf8 100644 --- a/web/app/components/app/configuration/index.tsx +++ b/web/app/components/app/configuration/index.tsx @@ -80,6 +80,8 @@ import { import PluginDependency from '@/app/components/workflow/plugin-dependency' import { supportFunctionCall } from '@/utils/tool-call' import { MittProvider } from '@/context/mitt-context' +import { fetchAndMergeValidCompletionParams } from '@/utils/completion-params' +import Toast from '@/app/components/base/toast' type PublishConfig = { modelConfig: ModelConfig @@ -453,7 +455,21 @@ const Configuration: FC = () => { ...visionConfig, enabled: supportVision, }, true) - setCompletionParams({}) + + try { + const { params: filtered, removedDetails } = await fetchAndMergeValidCompletionParams( + provider, + modelId, + completionParams, + ) + 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) { + 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 04acb61ef..2a71dffa1 100644 --- a/web/app/components/workflow/nodes/llm/panel.tsx +++ b/web/app/components/workflow/nodes/llm/panel.tsx @@ -19,6 +19,8 @@ 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 { fetchAndMergeValidCompletionParams } from '@/utils/completion-params' +import Toast from '@/app/components/base/toast' const i18nPrefix = 'workflow.nodes.llm' @@ -68,10 +70,27 @@ const Panel: FC> = ({ modelId: string mode?: string }) => { - handleCompletionParamsChange({}) - handleModelChanged(model) - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []) + (async () => { + try { + const { params: filtered, removedDetails } = await fetchAndMergeValidCompletionParams( + model.provider, + model.modelId, + inputs.model.completion_params, + ) + 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) + } + catch (e) { + Toast.notify({ type: 'error', message: t('common.error') }) + handleCompletionParamsChange({}) + } + finally { + handleModelChanged(model) + } + })() + }, [inputs.model.completion_params]) return (
diff --git a/web/utils/completion-params.ts b/web/utils/completion-params.ts new file mode 100644 index 000000000..b46c3ab72 --- /dev/null +++ b/web/utils/completion-params.ts @@ -0,0 +1,88 @@ +import type { FormValue, ModelParameterRule } from '@/app/components/header/account-setting/model-provider-page/declarations' + +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') { + removedDetails[key] = 'invalid type' + return + } + nextParams[key] = value + return + } + case 'string': + case 'text': { + if (typeof value !== 'string') { + removedDetails[key] = 'invalid type' + return + } + if (Array.isArray(rule.options) && rule.options.length) { + if (!(rule.options as string[]).includes(value)) { + removedDetails[key] = 'unsupported option' + return + } + } + nextParams[key] = value + return + } + default: { + removedDetails[key] = `unsupported rule type: ${(rule as any)?.type ?? 'unknown'}` + } + } + }) + + return { params: nextParams, removedDetails } +} + +export const fetchAndMergeValidCompletionParams = async ( + provider: string, + modelId: string, + oldParams: FormValue | undefined, +): Promise<{ params: FormValue; removedDetails: Record }> => { + const { fetchModelParameterRules } = await import('@/service/common') + const url = `/workspaces/current/model-providers/${provider}/models/parameter-rules?model=${modelId}` + const { data: parameterRules } = await fetchModelParameterRules(url) + return mergeValidCompletionParams(oldParams, parameterRules ?? []) +}