From 5468c4ec96bfa1ac029c8cbf40e9c6c23f1cb28d Mon Sep 17 00:00:00 2001 From: -LAN- Date: Mon, 18 May 2026 17:41:57 +0800 Subject: [PATCH 1/7] fix: can not create empty knowledge (#36336) Co-authored-by: Joel --- .../empty-dataset-creation-modal/index.module.css | 6 +----- .../create/empty-dataset-creation-modal/index.tsx | 2 +- .../create/stop-embedding-modal/index.module.css | 11 ++++------- .../datasets/create/stop-embedding-modal/index.tsx | 2 +- 4 files changed, 7 insertions(+), 14 deletions(-) diff --git a/web/app/components/datasets/create/empty-dataset-creation-modal/index.module.css b/web/app/components/datasets/create/empty-dataset-creation-modal/index.module.css index 435aeaf3c0..803bcac413 100644 --- a/web/app/components/datasets/create/empty-dataset-creation-modal/index.module.css +++ b/web/app/components/datasets/create/empty-dataset-creation-modal/index.module.css @@ -1,9 +1,5 @@ @reference "../../../../styles/globals.css"; -.modal { - position: relative; -} - .modalHeader { @apply flex items-center place-content-between h-8; } @@ -19,7 +15,7 @@ background-size: 16px; } -.modal .tip { +.tip { @apply mt-1 mb-8 text-text-tertiary; font-weight: 400; font-size: 13px; diff --git a/web/app/components/datasets/create/empty-dataset-creation-modal/index.tsx b/web/app/components/datasets/create/empty-dataset-creation-modal/index.tsx index caf84e31ac..1e539ac12b 100644 --- a/web/app/components/datasets/create/empty-dataset-creation-modal/index.tsx +++ b/web/app/components/datasets/create/empty-dataset-creation-modal/index.tsx @@ -53,7 +53,7 @@ const EmptyDatasetCreationModal = ({ show = false, onHide }: IProps) => { onHide() }} > - +
{t('stepOne.modal.title', { ns: 'datasetCreation' })}
diff --git a/web/app/components/datasets/create/stop-embedding-modal/index.module.css b/web/app/components/datasets/create/stop-embedding-modal/index.module.css index 90f300d2ce..3fab1ff377 100644 --- a/web/app/components/datasets/create/stop-embedding-modal/index.module.css +++ b/web/app/components/datasets/create/stop-embedding-modal/index.module.css @@ -1,9 +1,6 @@ @reference "../../../../styles/globals.css"; -.modal { - position: relative; -} -.modal .icon { +.icon { width: 48px; height: 48px; background: rgba(255, 255, 255, 0.9) center no-repeat url(../assets/annotation-info.svg); @@ -12,7 +9,7 @@ box-shadow: 0px 20px 24px -4px rgba(16, 24, 40, 0.08), 0px 8px 8px -4px rgba(16, 24, 40, 0.03); border-radius: 12px; } -.modal .close { +.close { position: absolute; right: 16px; top: 16px; @@ -23,14 +20,14 @@ background-size: 16px; cursor: pointer; } -.modal .title { +.title { @apply mt-3 mb-1; font-weight: 600; font-size: 20px; line-height: 30px; color: #101828; } -.modal .content { +.content { @apply mb-10; font-weight: 400; font-size: 14px; diff --git a/web/app/components/datasets/create/stop-embedding-modal/index.tsx b/web/app/components/datasets/create/stop-embedding-modal/index.tsx index a696f90585..6828ae81a9 100644 --- a/web/app/components/datasets/create/stop-embedding-modal/index.tsx +++ b/web/app/components/datasets/create/stop-embedding-modal/index.tsx @@ -39,7 +39,7 @@ const StopEmbeddingModal = ({ onHide() }} > - +
- ), + default: ({ onSelect, supportedParameterNames }: { onSelect: (id: number) => void, supportedParameterNames?: string[] }) => { + if (supportedParameterNames && !supportedParameterNames.includes('temperature')) + return null + + return + }, +})) + +vi.mock('../presets-parameter-utils', () => ({ + getSupportedPresetConfig: (_toneId: number, supportedParameterNames?: string[]) => { + if (supportedParameterNames && !supportedParameterNames.includes('temperature')) + return {} + + return { temperature: 0.8 } + }, })) vi.mock('../trigger', () => ({ @@ -194,7 +206,28 @@ describe('ModelParameterModal', () => { render() fireEvent.click(screen.getByText('Open Settings')) fireEvent.click(screen.getByText('Preset 1')) - expect(defaultProps.onCompletionParamsChange).toHaveBeenCalled() + expect(defaultProps.onCompletionParamsChange).toHaveBeenCalledWith({ + ...defaultProps.completionParams, + temperature: 0.8, + }) + }) + + it('should not render preset control when visible parameters do not support preset keys', () => { + parameterRules = [ + { + name: 'max_tokens', + label: { en_US: 'Max Tokens' }, + type: 'int', + default: 256, + min: 1, + max: 4096, + }, + ] + + render() + fireEvent.click(screen.getByText('Open Settings')) + + expect(screen.queryByText('Preset 1')).not.toBeInTheDocument() }) it('should call setModel when model selector picks another model', () => { diff --git a/web/app/components/header/account-setting/model-provider-page/model-parameter-modal/__tests__/presets-parameter.spec.tsx b/web/app/components/header/account-setting/model-provider-page/model-parameter-modal/__tests__/presets-parameter.spec.tsx index 33437afbf7..67ad4c6915 100644 --- a/web/app/components/header/account-setting/model-provider-page/model-parameter-modal/__tests__/presets-parameter.spec.tsx +++ b/web/app/components/header/account-setting/model-provider-page/model-parameter-modal/__tests__/presets-parameter.spec.tsx @@ -1,6 +1,7 @@ import { fireEvent, render, screen } from '@testing-library/react' import { vi } from 'vitest' import PresetsParameter from '../presets-parameter' +import { getSupportedPresetConfig } from '../presets-parameter-utils' describe('PresetsParameter', () => { beforeEach(() => { @@ -47,4 +48,22 @@ describe('PresetsParameter', () => { expect(onSelect).toHaveBeenCalledWith(3) }) + + it('should render presets when at least one preset parameter is supported', () => { + render() + + expect(screen.getByRole('button', { name: /common\.modelProvider\.loadPresets/i })).toBeInTheDocument() + }) + + it('should not render presets when no preset parameters are supported', () => { + render() + + expect(screen.queryByRole('button', { name: /common\.modelProvider\.loadPresets/i })).not.toBeInTheDocument() + }) + + it('should return only supported preset config keys', () => { + expect(getSupportedPresetConfig(1, ['temperature'])).toEqual({ + temperature: 0.8, + }) + }) }) diff --git a/web/app/components/header/account-setting/model-provider-page/model-parameter-modal/index.tsx b/web/app/components/header/account-setting/model-provider-page/model-parameter-modal/index.tsx index 6f37775052..648edf195d 100644 --- a/web/app/components/header/account-setting/model-provider-page/model-parameter-modal/index.tsx +++ b/web/app/components/header/account-setting/model-provider-page/model-parameter-modal/index.tsx @@ -24,7 +24,7 @@ import { useMemo, useRef, useState } from 'react' import { useTranslation } from 'react-i18next' import { ArrowNarrowLeft } from '@/app/components/base/icons/src/vender/line/arrows' import Loading from '@/app/components/base/loading' -import { PROVIDER_WITH_PRESET_TONE, STOP_PARAMETER_RULE, TONE_LIST } from '@/config' +import { PROVIDER_WITH_PRESET_TONE, STOP_PARAMETER_RULE } from '@/config' import { useModelParameterRules } from '@/service/use-common' import { useTextGenerationCurrentProviderAndModelAndModelList, @@ -32,6 +32,7 @@ import { import ModelSelector from '../model-selector' import ParameterItem from './parameter-item' import PresetsParameter from './presets-parameter' +import { getSupportedPresetConfig } from './presets-parameter-utils' import Trigger from './trigger' export type ModelParameterModalProps = { @@ -90,6 +91,9 @@ const ModelParameterModal: FC = ({ const parameterRules: ModelParameterRule[] = useMemo(() => { return parameterRulesData?.data || [] }, [parameterRulesData]) + const supportedPresetParameterNames = useMemo(() => { + return parameterRules.map(parameterRule => parameterRule.name) + }, [parameterRules]) const handleParamChange = (key: string, value: ParameterValue) => { onCompletionParamsChange({ @@ -125,13 +129,10 @@ const ModelParameterModal: FC = ({ } const handleSelectPresetParameter = (toneId: number) => { - const tone = TONE_LIST.find(tone => tone.id === toneId) - if (tone) { - onCompletionParamsChange({ - ...completionParams, - ...tone.config, - }) - } + onCompletionParamsChange({ + ...completionParams, + ...getSupportedPresetConfig(toneId, supportedPresetParameterNames), + }) } return ( @@ -199,7 +200,10 @@ const ModelParameterModal: FC = ({
{t('modelProvider.parameters', { ns: 'common' })}
{ PROVIDER_WITH_PRESET_TONE.includes(provider) && ( - + ) }
diff --git a/web/app/components/header/account-setting/model-provider-page/model-parameter-modal/presets-parameter-utils.ts b/web/app/components/header/account-setting/model-provider-page/model-parameter-modal/presets-parameter-utils.ts new file mode 100644 index 0000000000..88aa5fb4ce --- /dev/null +++ b/web/app/components/header/account-setting/model-provider-page/model-parameter-modal/presets-parameter-utils.ts @@ -0,0 +1,19 @@ +import { TONE_LIST } from '@/config' + +export const getSupportedPresetConfig = (toneId: number, supportedParameterNames?: string[]) => { + const tone = TONE_LIST.find(tone => tone.id === toneId) + if (!tone?.config) + return {} + + if (!supportedParameterNames) + return { ...tone.config } + + const supportedParameterNameSet = new Set(supportedParameterNames) + + return Object.entries(tone.config).reduce>((acc, [key, value]) => { + if (supportedParameterNameSet.has(key)) + acc[key] = value + + return acc + }, {}) +} diff --git a/web/app/components/header/account-setting/model-provider-page/model-parameter-modal/presets-parameter.tsx b/web/app/components/header/account-setting/model-provider-page/model-parameter-modal/presets-parameter.tsx index cb537ab18f..6fcca9dab7 100644 --- a/web/app/components/header/account-setting/model-provider-page/model-parameter-modal/presets-parameter.tsx +++ b/web/app/components/header/account-setting/model-provider-page/model-parameter-modal/presets-parameter.tsx @@ -12,6 +12,8 @@ import { Scales02 } from '@/app/components/base/icons/src/vender/solid/FinanceAn import { Target04 } from '@/app/components/base/icons/src/vender/solid/general' import { TONE_LIST } from '@/config' +const PRESET_TONE_LIST = TONE_LIST.slice(0, 3) + const toneI18nKeyMap = { Creative: 'model.tone.Creative', Balanced: 'model.tone.Balanced', @@ -27,10 +29,18 @@ const TONE_ICONS: Record = { type PresetsParameterProps = { onSelect: (toneId: number) => void + supportedParameterNames?: string[] } -function PresetsParameter({ onSelect }: PresetsParameterProps) { +function PresetsParameter({ onSelect, supportedParameterNames }: PresetsParameterProps) { const { t } = useTranslation() + const supportedParameterNameSet = supportedParameterNames ? new Set(supportedParameterNames) : undefined + const visiblePresetTones = supportedParameterNameSet + ? PRESET_TONE_LIST.filter(tone => Object.keys(tone.config ?? {}).some(key => supportedParameterNameSet.has(key))) + : PRESET_TONE_LIST + + if (!visiblePresetTones.length) + return null return ( @@ -47,7 +57,7 @@ function PresetsParameter({ onSelect }: PresetsParameterProps) { - {TONE_LIST.slice(0, 3).map(tone => ( + {visiblePresetTones.map(tone => ( onSelect(tone.id)}> {TONE_ICONS[tone.id]} {t(toneI18nKeyMap[tone.name], { ns: 'common' })} diff --git a/web/app/components/plugins/plugin-detail-panel/model-selector/__tests__/llm-params-panel.spec.tsx b/web/app/components/plugins/plugin-detail-panel/model-selector/__tests__/llm-params-panel.spec.tsx index 17fad8d7a7..5525934d26 100644 --- a/web/app/components/plugins/plugin-detail-panel/model-selector/__tests__/llm-params-panel.spec.tsx +++ b/web/app/components/plugins/plugin-detail-panel/model-selector/__tests__/llm-params-panel.spec.tsx @@ -75,14 +75,58 @@ vi.mock('@/config', () => ({ // Mock PresetsParameter component vi.mock('@/app/components/header/account-setting/model-provider-page/model-parameter-modal/presets-parameter', () => ({ - default: ({ onSelect }: { onSelect: (toneId: number) => void }) => ( -
- - - - -
- ), + default: ({ onSelect, supportedParameterNames }: { onSelect: (toneId: number) => void, supportedParameterNames?: string[] }) => { + const hasSupportedParameter = !supportedParameterNames || supportedParameterNames.some(name => ['temperature', 'top_p', 'presence_penalty', 'frequency_penalty'].includes(name)) + if (!hasSupportedParameter) + return null + + return ( +
+ + + + +
+ ) + }, +})) + +vi.mock('@/app/components/header/account-setting/model-provider-page/model-parameter-modal/presets-parameter-utils', () => ({ + getSupportedPresetConfig: (toneId: number, supportedParameterNames?: string[]) => { + const toneConfigMap: Record | undefined> = { + 1: { + temperature: 0.8, + top_p: 0.9, + presence_penalty: 0.1, + frequency_penalty: 0.1, + }, + 2: { + temperature: 0.5, + top_p: 0.85, + presence_penalty: 0.2, + frequency_penalty: 0.3, + }, + 3: { + temperature: 0.2, + top_p: 0.75, + presence_penalty: 0.5, + frequency_penalty: 0.5, + }, + } + const toneConfig = toneConfigMap[toneId] + if (!toneConfig) + return {} + + if (!supportedParameterNames) + return toneConfig + + return Object.entries(toneConfig).reduce>((acc, [key, value]) => { + if (supportedParameterNames.includes(key)) + acc[key] = value + + return acc + }, {}) + }, })) // Mock ParameterItem component @@ -202,7 +246,7 @@ describe('LLMParamsPanel', () => { it('should render PresetsParameter for openai provider', () => { // Arrange - setupModelParameterRulesMock({ data: [], isPending: false }) + setupModelParameterRulesMock({ data: [createParameterRule({ name: 'temperature' })], isPending: false }) const props = createDefaultProps({ provider: 'langgenius/openai/openai' }) // Act @@ -214,7 +258,7 @@ describe('LLMParamsPanel', () => { it('should render PresetsParameter for azure_openai provider', () => { // Arrange - setupModelParameterRulesMock({ data: [], isPending: false }) + setupModelParameterRulesMock({ data: [createParameterRule({ name: 'temperature' })], isPending: false }) const props = createDefaultProps({ provider: 'langgenius/azure_openai/azure_openai' }) // Act @@ -224,6 +268,18 @@ describe('LLMParamsPanel', () => { expect(screen.getByTestId('presets-parameter')).toBeInTheDocument() }) + it('should not render PresetsParameter when no visible parameter supports presets', () => { + // Arrange + setupModelParameterRulesMock({ data: [createParameterRule({ name: 'max_tokens', type: 'int' })], isPending: false }) + const props = createDefaultProps({ provider: 'langgenius/openai/openai' }) + + // Act + render() + + // Assert + expect(screen.queryByTestId('presets-parameter')).not.toBeInTheDocument() + }) + it('should not render PresetsParameter for non-preset providers', () => { // Arrange setupModelParameterRulesMock({ data: [], isPending: false }) @@ -360,7 +416,15 @@ describe('LLMParamsPanel', () => { it('should apply Creative preset config', () => { // Arrange const onCompletionParamsChange = vi.fn() - setupModelParameterRulesMock({ data: [], isPending: false }) + setupModelParameterRulesMock({ + data: [ + createParameterRule({ name: 'temperature' }), + createParameterRule({ name: 'top_p' }), + createParameterRule({ name: 'presence_penalty' }), + createParameterRule({ name: 'frequency_penalty' }), + ], + isPending: false, + }) const props = createDefaultProps({ provider: 'langgenius/openai/openai', onCompletionParamsChange, @@ -384,7 +448,15 @@ describe('LLMParamsPanel', () => { it('should apply Balanced preset config', () => { // Arrange const onCompletionParamsChange = vi.fn() - setupModelParameterRulesMock({ data: [], isPending: false }) + setupModelParameterRulesMock({ + data: [ + createParameterRule({ name: 'temperature' }), + createParameterRule({ name: 'top_p' }), + createParameterRule({ name: 'presence_penalty' }), + createParameterRule({ name: 'frequency_penalty' }), + ], + isPending: false, + }) const props = createDefaultProps({ provider: 'langgenius/openai/openai', onCompletionParamsChange, @@ -407,7 +479,15 @@ describe('LLMParamsPanel', () => { it('should apply Precise preset config', () => { // Arrange const onCompletionParamsChange = vi.fn() - setupModelParameterRulesMock({ data: [], isPending: false }) + setupModelParameterRulesMock({ + data: [ + createParameterRule({ name: 'temperature' }), + createParameterRule({ name: 'top_p' }), + createParameterRule({ name: 'presence_penalty' }), + createParameterRule({ name: 'frequency_penalty' }), + ], + isPending: false, + }) const props = createDefaultProps({ provider: 'langgenius/openai/openai', onCompletionParamsChange, @@ -430,7 +510,7 @@ describe('LLMParamsPanel', () => { it('should apply empty config for Custom preset (spreads undefined)', () => { // Arrange const onCompletionParamsChange = vi.fn() - setupModelParameterRulesMock({ data: [], isPending: false }) + setupModelParameterRulesMock({ data: [createParameterRule({ name: 'temperature' })], isPending: false }) const props = createDefaultProps({ provider: 'langgenius/openai/openai', onCompletionParamsChange, @@ -444,6 +524,27 @@ describe('LLMParamsPanel', () => { // Assert - Custom preset has no config, so only existing params are kept expect(onCompletionParamsChange).toHaveBeenCalledWith({ existing: 'value' }) }) + + it('should apply only preset config keys supported by visible parameters', () => { + // Arrange + const onCompletionParamsChange = vi.fn() + setupModelParameterRulesMock({ data: [createParameterRule({ name: 'temperature' })], isPending: false }) + const props = createDefaultProps({ + provider: 'langgenius/openai/openai', + onCompletionParamsChange, + completionParams: { existing: 'value' }, + }) + + // Act + render() + fireEvent.click(screen.getByTestId('preset-creative')) + + // Assert + expect(onCompletionParamsChange).toHaveBeenCalledWith({ + existing: 'value', + temperature: 0.8, + }) + }) }) describe('handleParamChange', () => { diff --git a/web/app/components/plugins/plugin-detail-panel/model-selector/llm-params-panel.tsx b/web/app/components/plugins/plugin-detail-panel/model-selector/llm-params-panel.tsx index d699621c22..49933f6138 100644 --- a/web/app/components/plugins/plugin-detail-panel/model-selector/llm-params-panel.tsx +++ b/web/app/components/plugins/plugin-detail-panel/model-selector/llm-params-panel.tsx @@ -10,7 +10,8 @@ import { useTranslation } from 'react-i18next' import Loading from '@/app/components/base/loading' import ParameterItem from '@/app/components/header/account-setting/model-provider-page/model-parameter-modal/parameter-item' import PresetsParameter from '@/app/components/header/account-setting/model-provider-page/model-parameter-modal/presets-parameter' -import { PROVIDER_WITH_PRESET_TONE, STOP_PARAMETER_RULE, TONE_LIST } from '@/config' +import { getSupportedPresetConfig } from '@/app/components/header/account-setting/model-provider-page/model-parameter-modal/presets-parameter-utils' +import { PROVIDER_WITH_PRESET_TONE, STOP_PARAMETER_RULE } from '@/config' import { useModelParameterRules } from '@/service/use-common' type Props = { @@ -34,15 +35,15 @@ const LLMParamsPanel = ({ const parameterRules: ModelParameterRule[] = useMemo(() => { return parameterRulesData?.data || [] }, [parameterRulesData]) + const supportedPresetParameterNames = useMemo(() => { + return parameterRules.map(parameterRule => parameterRule.name) + }, [parameterRules]) const handleSelectPresetParameter = (toneId: number) => { - const tone = TONE_LIST.find(tone => tone.id === toneId) - if (tone) { - onCompletionParamsChange({ - ...completionParams, - ...tone.config, - }) - } + onCompletionParamsChange({ + ...completionParams, + ...getSupportedPresetConfig(toneId, supportedPresetParameterNames), + }) } const handleParamChange = (key: string, value: ParameterValue) => { onCompletionParamsChange({ @@ -77,7 +78,10 @@ const LLMParamsPanel = ({
{t('modelProvider.parameters', { ns: 'common' })}
{ PROVIDER_WITH_PRESET_TONE.includes(provider) && ( - + ) }
From 75d7fc05269b48fff6929035b01d24422923f2e2 Mon Sep 17 00:00:00 2001 From: -LAN- Date: Mon, 18 May 2026 18:03:56 +0800 Subject: [PATCH 3/7] ci: add hotfix cherry-pick provenance check (#36340) --- .github/scripts/check-hotfix-cherry-picks.sh | 73 ++++++++++++++++++++ .github/workflows/hotfix-cherry-pick.yml | 49 +++++++++++++ 2 files changed, 122 insertions(+) create mode 100644 .github/scripts/check-hotfix-cherry-picks.sh create mode 100644 .github/workflows/hotfix-cherry-pick.yml diff --git a/.github/scripts/check-hotfix-cherry-picks.sh b/.github/scripts/check-hotfix-cherry-picks.sh new file mode 100644 index 0000000000..11dc024ccf --- /dev/null +++ b/.github/scripts/check-hotfix-cherry-picks.sh @@ -0,0 +1,73 @@ +#!/usr/bin/env bash +set -euo pipefail + +BASE_SHA=${BASE_SHA:-} +HEAD_SHA=${HEAD_SHA:-} +MAIN_REF=${MAIN_REF:-origin/main} +REMEDIATION_HINT="Changes should be made from the main branch using git cherry-pick -x." + +error() { + printf 'ERROR: %s\n' "$1" >&2 +} + +if [[ -z "$BASE_SHA" || -z "$HEAD_SHA" ]]; then + error "BASE_SHA and HEAD_SHA are required. $REMEDIATION_HINT" + exit 2 +fi + +if ! git rev-parse --verify "$BASE_SHA^{commit}" > /dev/null 2>&1; then + error "Base commit '$BASE_SHA' is not available in the local git checkout." + exit 2 +fi + +if ! git rev-parse --verify "$HEAD_SHA^{commit}" > /dev/null 2>&1; then + error "Head commit '$HEAD_SHA' is not available in the local git checkout." + exit 2 +fi + +if ! git rev-parse --verify "$MAIN_REF^{commit}" > /dev/null 2>&1; then + error "Main ref '$MAIN_REF' is not available in the local git checkout. $REMEDIATION_HINT" + exit 2 +fi + +failed=0 +checked=0 + +while IFS= read -r commit_sha; do + [[ -n "$commit_sha" ]] || continue + + checked=$((checked + 1)) + subject=$(git log -1 --format=%s "$commit_sha") + source_sha=$( + git log -1 --format=%B "$commit_sha" \ + | sed -nE 's/^\(cherry picked from commit ([0-9a-fA-F]{7,64})\)$/\1/p' \ + | tail -n 1 + ) + + if [[ -z "$source_sha" ]]; then + error "Commit $commit_sha ($subject) is missing cherry-pick provenance. $REMEDIATION_HINT" + failed=1 + continue + fi + + if ! git cat-file -e "$source_sha^{commit}" 2> /dev/null; then + error "Commit $commit_sha ($subject) references source $source_sha, but that commit is not available locally. $REMEDIATION_HINT" + failed=1 + continue + fi + + if ! git merge-base --is-ancestor "$source_sha" "$MAIN_REF"; then + error "Commit $commit_sha ($subject) references source $source_sha, but that source is not reachable from main ($MAIN_REF). $REMEDIATION_HINT" + failed=1 + fi +done < <(git rev-list --reverse "$BASE_SHA..$HEAD_SHA") + +if [[ "$failed" -ne 0 ]]; then + exit 1 +fi + +if [[ "$checked" -eq 0 ]]; then + echo "No PR commits to check." +else + echo "Verified $checked PR commit(s) include cherry-pick provenance from main." +fi diff --git a/.github/workflows/hotfix-cherry-pick.yml b/.github/workflows/hotfix-cherry-pick.yml new file mode 100644 index 0000000000..594b10c743 --- /dev/null +++ b/.github/workflows/hotfix-cherry-pick.yml @@ -0,0 +1,49 @@ +name: Hotfix Cherry-Pick Provenance + +on: + pull_request: + branches: + - 'hotfix/**' + - 'lts/**' + types: + - opened + - edited + - reopened + - ready_for_review + - synchronize + +permissions: + contents: read + +concurrency: + group: hotfix-cherry-pick-${{ github.event.pull_request.number || github.run_id }} + cancel-in-progress: true + +jobs: + check-cherry-pick-provenance: + name: Require cherry-pick provenance + runs-on: depot-ubuntu-24.04 + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + fetch-depth: 0 + + - name: Fetch PR base, PR head, and main + env: + BASE_REF: ${{ github.base_ref }} + PR_NUMBER: ${{ github.event.pull_request.number }} + run: | + git fetch --no-tags --prune origin \ + "+refs/heads/main:refs/remotes/origin/main" \ + "+refs/heads/${BASE_REF}:refs/remotes/origin/${BASE_REF}" \ + "+refs/pull/${PR_NUMBER}/head:refs/remotes/pull/${PR_NUMBER}/head" + + - name: Load checker from main + run: git show origin/main:.github/scripts/check-hotfix-cherry-picks.sh > "$RUNNER_TEMP/check-hotfix-cherry-picks.sh" + + - name: Check PR commits + env: + BASE_SHA: ${{ github.event.pull_request.base.sha }} + HEAD_SHA: ${{ github.event.pull_request.head.sha }} + MAIN_REF: origin/main + run: bash "$RUNNER_TEMP/check-hotfix-cherry-picks.sh" From c0f237bf35447029befc511064cd689f4752b050 Mon Sep 17 00:00:00 2001 From: KVOJJJin Date: Mon, 18 May 2026 18:05:13 +0800 Subject: [PATCH 4/7] feat(web): allow annotation reply score threshold below 0.8 (#36337) --- .../__tests__/config-param-modal.spec.tsx | 35 +++++++++++++++++-- .../annotation-reply/__tests__/index.spec.tsx | 16 +++++++++ .../__tests__/use-annotation-config.spec.ts | 31 +++++++++++++++- .../annotation-reply/config-param-modal.tsx | 2 +- .../annotation-reply/index.tsx | 2 +- .../score-slider/__tests__/index.spec.tsx | 9 ++++- .../annotation-reply/score-slider/index.tsx | 13 ++++--- .../annotation-reply/use-annotation-config.ts | 2 +- web/service/annotation.spec.ts | 28 +++++++++++++++ web/service/annotation.ts | 2 +- 10 files changed, 126 insertions(+), 14 deletions(-) create mode 100644 web/service/annotation.spec.ts diff --git a/web/app/components/base/features/new-feature-panel/annotation-reply/__tests__/config-param-modal.spec.tsx b/web/app/components/base/features/new-feature-panel/annotation-reply/__tests__/config-param-modal.spec.tsx index b07f48d4bb..39c1244980 100644 --- a/web/app/components/base/features/new-feature-panel/annotation-reply/__tests__/config-param-modal.spec.tsx +++ b/web/app/components/base/features/new-feature-panel/annotation-reply/__tests__/config-param-modal.spec.tsx @@ -40,7 +40,7 @@ vi.mock('../score-slider', () => ({ onChange(Number((e.target as HTMLInputElement).value))} @@ -272,7 +272,7 @@ describe('ConfigParamModal', () => { ) const slider = screen.getByRole('slider') - expect(slider).toHaveAttribute('min', '80') + expect(slider).toHaveAttribute('min', '0') expect(slider).toHaveAttribute('max', '100') expect(slider).toHaveValue('90') }) @@ -375,7 +375,7 @@ describe('ConfigParamModal', () => { it('should use ANNOTATION_DEFAULT score_threshold when config has no score_threshold', () => { const configWithoutThreshold = { ...defaultAnnotationConfig, - score_threshold: 0, + score_threshold: undefined as unknown as number, } render( { expect(screen.getByRole('slider')).toHaveValue('90') }) + it('should preserve zero score threshold instead of falling back to default', async () => { + const onSave = vi.fn().mockResolvedValue(undefined) + render( + , + ) + + expect(screen.getByRole('slider')).toHaveValue('0') + + const buttons = screen.getAllByRole('button') + const saveBtn = buttons.find(b => b.textContent?.includes('initSetup')) + fireEvent.click(saveBtn!) + + await waitFor(() => { + expect(onSave).toHaveBeenCalledWith( + expect.objectContaining({ embedding_provider_name: 'openai' }), + 0, + ) + }) + }) + it('should set loading state while saving', async () => { let resolveOnSave: () => void const onSave = vi.fn().mockImplementation(() => new Promise((resolve) => { diff --git a/web/app/components/base/features/new-feature-panel/annotation-reply/__tests__/index.spec.tsx b/web/app/components/base/features/new-feature-panel/annotation-reply/__tests__/index.spec.tsx index 03ddbc6322..4aae34387e 100644 --- a/web/app/components/base/features/new-feature-panel/annotation-reply/__tests__/index.spec.tsx +++ b/web/app/components/base/features/new-feature-panel/annotation-reply/__tests__/index.spec.tsx @@ -175,6 +175,22 @@ describe('AnnotationReply', () => { expect(screen.getByText('text-embedding-ada-002')).toBeInTheDocument() }) + it('should show zero score threshold when enabled', () => { + renderWithProvider({}, { + annotationReply: { + enabled: true, + score_threshold: 0, + embedding_model: { + embedding_provider_name: 'openai', + embedding_model_name: 'text-embedding-ada-002', + }, + }, + }) + + expect(screen.getByText('0')).toBeInTheDocument() + expect(screen.getByText('text-embedding-ada-002')).toBeInTheDocument() + }) + it('should show dash when score threshold is not set', () => { renderWithProvider({}, { annotationReply: { diff --git a/web/app/components/base/features/new-feature-panel/annotation-reply/__tests__/use-annotation-config.spec.ts b/web/app/components/base/features/new-feature-panel/annotation-reply/__tests__/use-annotation-config.spec.ts index 14f6d68bda..6f6deb7ae1 100644 --- a/web/app/components/base/features/new-feature-panel/annotation-reply/__tests__/use-annotation-config.spec.ts +++ b/web/app/components/base/features/new-feature-panel/annotation-reply/__tests__/use-annotation-config.spec.ts @@ -1,6 +1,6 @@ import type { AnnotationReplyConfig } from '@/models/debug' import { act, renderHook } from '@testing-library/react' -import { queryAnnotationJobStatus } from '@/service/annotation' +import { queryAnnotationJobStatus, updateAnnotationStatus } from '@/service/annotation' import { sleep } from '@/utils' import useAnnotationConfig from '../use-annotation-config' @@ -162,6 +162,35 @@ describe('useAnnotationConfig', () => { expect(updatedConfig.score_threshold).toBe(0.85) }) + it('should preserve zero score threshold when enabling annotation', async () => { + const zeroScoreConfig = { ...defaultConfig, score_threshold: 0 } + const setAnnotationConfig = vi.fn() + const { result } = renderHook(() => useAnnotationConfig({ + appId: 'test-app', + annotationConfig: zeroScoreConfig, + setAnnotationConfig, + })) + + await act(async () => { + await result.current.handleEnableAnnotation({ + embedding_provider_name: 'openai', + embedding_model_name: 'text-embedding-3-small', + }, 0) + }) + + expect(updateAnnotationStatus).toHaveBeenCalledWith( + 'test-app', + 'enable', + { + embedding_provider_name: 'openai', + embedding_model_name: 'text-embedding-3-small', + }, + 0, + ) + const updatedConfig = setAnnotationConfig.mock.calls[0]![0] + expect(updatedConfig.score_threshold).toBe(0) + }) + it('should set score and embedding model together', () => { const setAnnotationConfig = vi.fn() const { result } = renderHook(() => useAnnotationConfig({ diff --git a/web/app/components/base/features/new-feature-panel/annotation-reply/config-param-modal.tsx b/web/app/components/base/features/new-feature-panel/annotation-reply/config-param-modal.tsx index e8932979a6..9c341d4ec1 100644 --- a/web/app/components/base/features/new-feature-panel/annotation-reply/config-param-modal.tsx +++ b/web/app/components/base/features/new-feature-panel/annotation-reply/config-param-modal.tsx @@ -75,7 +75,7 @@ const ConfigParamModal: FC = ({ isShow, onHide: doHide, onSave, isInit, a { setAnnotationConfig({ ...annotationConfig, diff --git a/web/app/components/base/features/new-feature-panel/annotation-reply/index.tsx b/web/app/components/base/features/new-feature-panel/annotation-reply/index.tsx index 80ebaf9614..b73699359b 100644 --- a/web/app/components/base/features/new-feature-panel/annotation-reply/index.tsx +++ b/web/app/components/base/features/new-feature-panel/annotation-reply/index.tsx @@ -100,7 +100,7 @@ const AnnotationReply = ({
{t('feature.annotation.scoreThreshold.title', { ns: 'appDebug' })}
-
{annotationReply.score_threshold || '-'}
+
{annotationReply.score_threshold ?? '-'}
diff --git a/web/app/components/base/features/new-feature-panel/annotation-reply/score-slider/__tests__/index.spec.tsx b/web/app/components/base/features/new-feature-panel/annotation-reply/score-slider/__tests__/index.spec.tsx index ffa9c33043..97493ab441 100644 --- a/web/app/components/base/features/new-feature-panel/annotation-reply/score-slider/__tests__/index.spec.tsx +++ b/web/app/components/base/features/new-feature-panel/annotation-reply/score-slider/__tests__/index.spec.tsx @@ -17,7 +17,7 @@ describe('ScoreSlider', () => { it('should display easy match and accurate match labels', () => { render() - expect(screen.getByText('0.8')).toBeInTheDocument() + expect(screen.getByText('0.0')).toBeInTheDocument() expect(screen.getByText('1.0')).toBeInTheDocument() expect(screen.getByText(/feature\.annotation\.scoreThreshold\.easyMatch/)).toBeInTheDocument() expect(screen.getByText(/feature\.annotation\.scoreThreshold\.accurateMatch/)).toBeInTheDocument() @@ -36,4 +36,11 @@ describe('ScoreSlider', () => { expect(getSliderInput()).toHaveValue('95') expect(screen.getByText('0.95')).toBeInTheDocument() }) + + it('should allow zero as the minimum score threshold', () => { + render() + + expect(getSliderInput()).toHaveValue('0') + expect(screen.getByText('0.00')).toBeInTheDocument() + }) }) diff --git a/web/app/components/base/features/new-feature-panel/annotation-reply/score-slider/index.tsx b/web/app/components/base/features/new-feature-panel/annotation-reply/score-slider/index.tsx index 8a2dc18fc0..d3b6340c89 100644 --- a/web/app/components/base/features/new-feature-panel/annotation-reply/score-slider/index.tsx +++ b/web/app/components/base/features/new-feature-panel/annotation-reply/score-slider/index.tsx @@ -17,13 +17,16 @@ const clamp = (value: number, min: number, max: number) => { return Math.min(Math.max(value, min), max) } +const SCORE_MIN = 0 +const SCORE_MAX = 100 + const ScoreSlider: FC = ({ className, value, onChange, }) => { const { t } = useTranslation() - const safeValue = clamp(value, 80, 100) + const safeValue = clamp(value, SCORE_MIN, SCORE_MAX) return (
@@ -31,8 +34,8 @@ const ScoreSlider: FC = ({ = ({
@@ -49,7 +52,7 @@ const ScoreSlider: FC = ({
-
0.8
+
0.0
ยท
{t('feature.annotation.scoreThreshold.easyMatch', { ns: 'appDebug' })}
diff --git a/web/app/components/base/features/new-feature-panel/annotation-reply/use-annotation-config.ts b/web/app/components/base/features/new-feature-panel/annotation-reply/use-annotation-config.ts index c74175846d..64c714df4e 100644 --- a/web/app/components/base/features/new-feature-panel/annotation-reply/use-annotation-config.ts +++ b/web/app/components/base/features/new-feature-panel/annotation-reply/use-annotation-config.ts @@ -53,7 +53,7 @@ const useAnnotationConfig = ({ setAnnotationConfig(produce(annotationConfig, (draft: AnnotationReplyConfig) => { draft.enabled = true draft.embedding_model = embeddingModel - if (!draft.score_threshold) + if (draft.score_threshold === undefined || draft.score_threshold === null) draft.score_threshold = ANNOTATION_DEFAULT.score_threshold })) } diff --git a/web/service/annotation.spec.ts b/web/service/annotation.spec.ts new file mode 100644 index 0000000000..60af578108 --- /dev/null +++ b/web/service/annotation.spec.ts @@ -0,0 +1,28 @@ +import { AnnotationEnableStatus } from '@/app/components/app/annotation/type' +import { updateAnnotationStatus } from './annotation' +import { post } from './base' + +vi.mock('./base', () => ({ + post: vi.fn(), +})) + +describe('annotation service', () => { + beforeEach(() => { + vi.clearAllMocks() + }) + + it('should preserve zero score threshold when updating annotation status', () => { + updateAnnotationStatus('app-1', AnnotationEnableStatus.enable, { + embedding_model_name: 'model', + embedding_provider_name: 'provider', + }, 0) + + expect(post).toHaveBeenCalledWith('apps/app-1/annotation-reply/enable', { + body: { + embedding_model_name: 'model', + embedding_provider_name: 'provider', + score_threshold: 0, + }, + }) + }) +}) diff --git a/web/service/annotation.ts b/web/service/annotation.ts index 8a19425044..ba8c560b1f 100644 --- a/web/service/annotation.ts +++ b/web/service/annotation.ts @@ -7,7 +7,7 @@ export const fetchAnnotationConfig = (appId: string) => { } export const updateAnnotationStatus = (appId: string, action: AnnotationEnableStatus, embeddingModel?: EmbeddingModelConfig, score?: number) => { let body: any = { - score_threshold: score || ANNOTATION_DEFAULT.score_threshold, + score_threshold: score ?? ANNOTATION_DEFAULT.score_threshold, } if (embeddingModel) { body = { From 1cee1a25b675c2ac19ce94d03d75843854a1ede0 Mon Sep 17 00:00:00 2001 From: NeatGuyCoding <15627489+NeatGuyCoding@users.noreply.github.com> Date: Mon, 18 May 2026 18:15:51 +0800 Subject: [PATCH 5/7] fix(console): require admin/owner to set default builtin tool credential (#36264) Signed-off-by: NeatGuyCoding <15627489+NeatGuyCoding@users.noreply.github.com> --- api/controllers/console/workspace/tool_providers.py | 1 + 1 file changed, 1 insertion(+) diff --git a/api/controllers/console/workspace/tool_providers.py b/api/controllers/console/workspace/tool_providers.py index e653c9064c..cb01a02318 100644 --- a/api/controllers/console/workspace/tool_providers.py +++ b/api/controllers/console/workspace/tool_providers.py @@ -874,6 +874,7 @@ class ToolBuiltinProviderSetDefaultApi(Resource): @console_ns.expect(console_ns.models[BuiltinProviderDefaultCredentialPayload.__name__]) @setup_required @login_required + @is_admin_or_owner_required @account_initialization_required def post(self, provider): _, current_tenant_id = current_account_with_tenant() From 5b79f7e99d803e955769ad9de9393b6d240ea059 Mon Sep 17 00:00:00 2001 From: Riskey <36894937+RiskeyL@users.noreply.github.com> Date: Mon, 18 May 2026 18:17:49 +0800 Subject: [PATCH 6/7] docs: fix docker README numbering and refresh stale references (#36303) --- docker/README.md | 54 +++++++++++++++++++++++++----------------------- 1 file changed, 28 insertions(+), 26 deletions(-) diff --git a/docker/README.md b/docker/README.md index 26b1dac9ac..2e21a2ce8f 100644 --- a/docker/README.md +++ b/docker/README.md @@ -5,7 +5,7 @@ Welcome to the new `docker` directory for deploying Dify using Docker Compose. T ### What's Updated - **Certbot Container**: `docker-compose.yaml` now contains `certbot` for managing SSL certificates. This container automatically renews certificates and ensures secure HTTPS connections.\ - For more information, refer `docker/certbot/README.md`. + For more information, refer to `docker/certbot/README.md`. - **Persistent Environment Variables**: Essential startup defaults are provided in `.env.example`, while local values are stored in `.env`, ensuring that your configurations persist across deployments. @@ -17,26 +17,26 @@ Welcome to the new `docker` directory for deploying Dify using Docker Compose. T ### How to Deploy Dify with `docker-compose.yaml` 1. **Prerequisites**: Ensure Docker and Docker Compose are installed on your system. -1. **Environment Setup**: +2. **Environment Setup**: - Navigate to the `docker` directory. - Copy `.env.example` to `.env`. - Customize `.env` when you need to change essential startup defaults. Copy optional files from `envs/` without the `.example` suffix when you need advanced settings. - **Optional (for advanced deployments)**: If you maintain a full `.env` file copied from `.env.example`, you may use the environment synchronization tool to keep it aligned with the latest `.env.example` updates while preserving your custom settings. See the [Environment Variables Synchronization](#environment-variables-synchronization) section below. -1. **Running the Services**: +3. **Running the Services**: - Execute `docker compose up -d` from the `docker` directory to start the services. - - To specify a vector database, set the `VECTOR_STORE` variable in your `.env` file to your desired vector database service, such as `milvus`, `weaviate`, or `opensearch`. + - To specify a vector database, set the `VECTOR_STORE` variable in your `.env` file to your desired vector database service, such as `milvus`, `weaviate`, or `opensearch`. See `envs/vectorstores/` for the full list of supported options. ```bash cp .env.example .env docker compose up -d ``` -1. **SSL Certificate Setup**: - - Refer `docker/certbot/README.md` to set up SSL certificates using Certbot. -1. **OpenTelemetry Collector Setup**: - - Change `ENABLE_OTEL` to `true` in `.env`. - - Configure `OTLP_BASE_ENDPOINT` properly. +4. **SSL Certificate Setup**: + - Refer to `docker/certbot/README.md` to set up SSL certificates using Certbot. +5. **OpenTelemetry Collector Setup**: + - Copy `envs/core-services/shared.env.example` to `envs/core-services/shared.env`. + - Set `ENABLE_OTEL=true` and configure `OTLP_BASE_ENDPOINT`. Tune the other `OTEL_*` knobs in the same file if needed. ### How to Deploy Middleware for Developing Dify @@ -44,7 +44,7 @@ Welcome to the new `docker` directory for deploying Dify using Docker Compose. T - Use the `docker-compose.middleware.yaml` for setting up essential middleware services like databases and caches. - Navigate to the `docker` directory. - Ensure the `middleware.env` file is created by running `cp envs/middleware.env.example middleware.env` (refer to the `envs/middleware.env.example` file). -1. **Running Middleware Services**: +2. **Running Middleware Services**: - Navigate to the `docker` directory. - Execute `docker compose --env-file middleware.env -f docker-compose.middleware.yaml -p dify up -d` to start PostgreSQL/MySQL (per `DB_TYPE`) plus the bundled Weaviate instance. @@ -55,9 +55,9 @@ Welcome to the new `docker` directory for deploying Dify using Docker Compose. T For users migrating from the `docker-legacy` setup: 1. **Review Changes**: Familiarize yourself with the new `.env` configuration and Docker Compose setup. -1. **Transfer Customizations**: +2. **Transfer Customizations**: - If you have customized configurations such as `docker-compose.yaml`, `ssrf_proxy/squid.conf`, or `nginx/conf.d/default.conf`, you will need to reflect these changes in the `.env` file you create. -1. **Data Migration**: +3. **Data Migration**: - Ensure that data from services like databases and caches is backed up and migrated appropriately to the new structure if necessary. ### Overview of `.env`, `.env.example`, and `envs/` @@ -80,49 +80,51 @@ The root `.env.example` file contains the essential startup settings. Optional a 1. **Common Variables**: - - `CONSOLE_API_URL`, `SERVICE_API_URL`: URLs for different API services. - - `APP_WEB_URL`: Frontend application URL. - - `FILES_URL`: Base URL for file downloads and previews. + - `CONSOLE_API_URL`, `CONSOLE_WEB_URL`, `SERVICE_API_URL`, `APP_API_URL`, `APP_WEB_URL`: URLs for the API and frontend services. + - `FILES_URL`, `INTERNAL_FILES_URL`: Public and internal base URLs for file downloads and previews. + - `ENDPOINT_URL_TEMPLATE`, `NEXT_PUBLIC_SOCKET_URL`, `TRIGGER_URL`: Additional service URLs. + + See `.env.example` for the full list. -1. **Server Configuration**: +2. **Server Configuration**: - `LOG_LEVEL`, `DEBUG`, `FLASK_DEBUG`: Logging and debug settings. - `SECRET_KEY`: A key for signing sessions, JWTs, and file URLs. Leave it empty to let Dify generate a persistent key in the storage directory, or set a unique value yourself. -1. **Database Configuration**: +3. **Database Configuration**: - `DB_USERNAME`, `DB_PASSWORD`, `DB_HOST`, `DB_PORT`, `DB_DATABASE`: PostgreSQL database credentials and connection details. -1. **Redis Configuration**: +4. **Redis Configuration**: - `REDIS_HOST`, `REDIS_PORT`, `REDIS_PASSWORD`: Redis server connection settings. - `REDIS_KEY_PREFIX`: Optional global namespace prefix for Redis keys, topics, streams, and Celery Redis transport artifacts. -1. **Celery Configuration**: +5. **Celery Configuration**: - `CELERY_BROKER_URL`: Configuration for Celery message broker. -1. **Storage Configuration**: +6. **Storage Configuration**: - `STORAGE_TYPE`, `OPENDAL_SCHEME`, `OPENDAL_FS_ROOT`: Default local file storage settings. Optional storage backends are configured from the files under `envs/`. -1. **Vector Database Configuration**: +7. **Vector Database Configuration**: - - `VECTOR_STORE`: Type of vector database (e.g., `weaviate`, `milvus`). + - `VECTOR_STORE`: Type of vector database (e.g., `weaviate`, `milvus`). See `envs/vectorstores/` for the full list of supported options. - Specific settings for each vector store like `WEAVIATE_ENDPOINT`, `MILVUS_URI`. -1. **CORS Configuration**: +8. **CORS Configuration**: - `WEB_API_CORS_ALLOW_ORIGINS`, `CONSOLE_CORS_ALLOW_ORIGINS`: Settings for cross-origin resource sharing. -1. **OpenTelemetry Configuration**: +9. **OpenTelemetry Configuration**: - `ENABLE_OTEL`: Enable OpenTelemetry collector in api. - `OTLP_BASE_ENDPOINT`: Endpoint for your OTLP exporter. -1. **Other Service-Specific Environment Variables**: +10. **Other Service-Specific Environment Variables**: - - Each service like `nginx`, `redis`, `db`, and vector databases have specific environment variables that are directly referenced in the `docker-compose.yaml`. + - Each service like `nginx`, `redis`, `db`, and vector databases have specific environment variables that are directly referenced in the `docker-compose.yaml`. ### Environment Variables Synchronization From 06f076e0ff47f2e7c69ebc51e756556dd1030d95 Mon Sep 17 00:00:00 2001 From: Joel Date: Mon, 18 May 2026 18:19:52 +0800 Subject: [PATCH 7/7] fix: no model selected but params keep loading (#36342) --- .../__tests__/index.spec.tsx | 23 ++++++++++++++++++- .../model-parameter-modal/index.tsx | 3 +-- .../__tests__/llm-params-panel.spec.tsx | 15 ++++++++++++ .../model-selector/llm-params-panel.tsx | 5 ++-- 4 files changed, 41 insertions(+), 5 deletions(-) diff --git a/web/app/components/header/account-setting/model-provider-page/model-parameter-modal/__tests__/index.spec.tsx b/web/app/components/header/account-setting/model-provider-page/model-parameter-modal/__tests__/index.spec.tsx index 29e5d37980..b3f1ee9be1 100644 --- a/web/app/components/header/account-setting/model-provider-page/model-parameter-modal/__tests__/index.spec.tsx +++ b/web/app/components/header/account-setting/model-provider-page/model-parameter-modal/__tests__/index.spec.tsx @@ -13,6 +13,7 @@ let parameterRules: Array> | undefined = [ }, ] let isRulesLoading = false +let isRulesPending = false let currentProvider: Record | undefined = { provider: 'openai', label: { en_US: 'OpenAI' } } let currentModel: Record | undefined = { model: 'gpt-3.5-turbo', @@ -49,7 +50,7 @@ vi.mock('@/service/use-common', () => ({ data: parameterRules, }, isLoading: isRulesLoading, - isPending: isRulesLoading, + isPending: isRulesPending, }), })) @@ -138,6 +139,7 @@ describe('ModelParameterModal', () => { beforeEach(() => { vi.clearAllMocks() isRulesLoading = false + isRulesPending = false parameterRules = [ { name: 'temperature', @@ -252,11 +254,29 @@ describe('ModelParameterModal', () => { it('should render loading state when parameter rules are loading', () => { isRulesLoading = true + isRulesPending = true render() fireEvent.click(screen.getByText('Open Settings')) expect(screen.getByRole('status')).toBeInTheDocument() }) + it('should not render parameter loading when model is not configured and parameter rules query is pending but disabled', () => { + isRulesPending = true + parameterRules = [] + + render( + , + ) + fireEvent.click(screen.getByText('Open Settings')) + + expect(screen.queryByRole('status')).not.toBeInTheDocument() + expect(screen.getByTestId('model-selector')).toBeInTheDocument() + }) + it('should not open content when readonly is true', () => { render() fireEvent.click(screen.getByText('Open Settings')) @@ -332,6 +352,7 @@ describe('ModelParameterModal', () => { it('should render the empty loading fallback when rules resolve to an empty list', () => { parameterRules = [] isRulesLoading = true + isRulesPending = true render() fireEvent.click(screen.getByText('Open Settings')) diff --git a/web/app/components/header/account-setting/model-provider-page/model-parameter-modal/index.tsx b/web/app/components/header/account-setting/model-provider-page/model-parameter-modal/index.tsx index 648edf195d..f0764fade0 100644 --- a/web/app/components/header/account-setting/model-provider-page/model-parameter-modal/index.tsx +++ b/web/app/components/header/account-setting/model-provider-page/model-parameter-modal/index.tsx @@ -76,10 +76,9 @@ const ModelParameterModal: FC = ({ const settingsIconRef = useRef(null) const { data: parameterRulesData, - isPending, isLoading, } = useModelParameterRules(provider, modelId) - const isRulesLoading = isPending || isLoading + const isRulesLoading = !!provider && !!modelId && isLoading const { currentProvider, currentModel, diff --git a/web/app/components/plugins/plugin-detail-panel/model-selector/__tests__/llm-params-panel.spec.tsx b/web/app/components/plugins/plugin-detail-panel/model-selector/__tests__/llm-params-panel.spec.tsx index 5525934d26..8230ec5b14 100644 --- a/web/app/components/plugins/plugin-detail-panel/model-selector/__tests__/llm-params-panel.spec.tsx +++ b/web/app/components/plugins/plugin-detail-panel/model-selector/__tests__/llm-params-panel.spec.tsx @@ -192,10 +192,12 @@ const createDefaultProps = (overrides: Partial<{ const setupModelParameterRulesMock = (config: { data?: ModelParameterRule[] isPending?: boolean + isLoading?: boolean } = {}) => { mockUseModelParameterRules.mockReturnValue({ data: config.data ? { data: config.data } : undefined, isPending: config.isPending ?? false, + isLoading: config.isLoading ?? config.isPending ?? false, }) } @@ -232,6 +234,19 @@ describe('LLMParamsPanel', () => { expect(screen.getByRole('status')).toBeInTheDocument() }) + it('should not render loading state when model is not configured and parameter rules query is pending but disabled', () => { + // Arrange + setupModelParameterRulesMock({ isPending: true, isLoading: false }) + const props = createDefaultProps({ provider: '', modelId: '' }) + + // Act + render() + + // Assert + expect(screen.queryByRole('status')).not.toBeInTheDocument() + expect(screen.getByText('common.modelProvider.parameters')).toBeInTheDocument() + }) + it('should render parameters header', () => { // Arrange setupModelParameterRulesMock({ data: [], isPending: false }) diff --git a/web/app/components/plugins/plugin-detail-panel/model-selector/llm-params-panel.tsx b/web/app/components/plugins/plugin-detail-panel/model-selector/llm-params-panel.tsx index 49933f6138..4d12efb9b5 100644 --- a/web/app/components/plugins/plugin-detail-panel/model-selector/llm-params-panel.tsx +++ b/web/app/components/plugins/plugin-detail-panel/model-selector/llm-params-panel.tsx @@ -30,7 +30,8 @@ const LLMParamsPanel = ({ onCompletionParamsChange, }: Props) => { const { t } = useTranslation() - const { data: parameterRulesData, isPending: isLoading } = useModelParameterRules(provider, modelId) + const { data: parameterRulesData, isLoading } = useModelParameterRules(provider, modelId) + const isRulesLoading = !!provider && !!modelId && isLoading const parameterRules: ModelParameterRule[] = useMemo(() => { return parameterRulesData?.data || [] @@ -66,7 +67,7 @@ const LLMParamsPanel = ({ } } - if (isLoading) { + if (isRulesLoading) { return (
)