refactor(web): move model lists out of provider context (#41939)

This commit is contained in:
yyh 2026-09-08 03:29:56 +00:00 committed by GitHub
parent e26dcf55a9
commit 1006519012
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
89 changed files with 1356 additions and 1062 deletions

View File

@ -2294,7 +2294,7 @@
},
"web/app/components/header/account-setting/model-provider-page/hooks.ts": {
"@tanstack/query/prefer-query-options": {
"count": 2
"count": 1
}
},
"web/app/components/header/account-setting/model-provider-page/model-auth/add-custom-model.tsx": {
@ -5133,7 +5133,7 @@
},
"web/service/use-common.ts": {
"@tanstack/query/prefer-query-options": {
"count": 11
"count": 10
}
},
"web/service/use-datasource.ts": {

View File

@ -9,8 +9,6 @@ export const baseProviderContextValue: ProviderContextState = {
refreshModelProviders: async () => {},
isLoadingModelProviders: false,
isSuccessModelProviders: false,
textGenerationModelList: [],
isAPIKeySet: true,
}
export const createMockProviderContextValue = (

View File

@ -2,9 +2,10 @@ import { fireEvent, render, screen } from '@testing-library/react'
import * as React from 'react'
import PublishWithMultipleModel from '../publish-with-multiple-model'
const mockUseProviderContext = vi.fn()
vi.mock('@/context/provider-context', () => ({
useProviderContext: () => mockUseProviderContext(),
const mockModelListQuery = vi.fn()
vi.mock('@tanstack/react-query', async (importOriginal) => ({
...(await importOriginal<typeof import('@tanstack/react-query')>()),
useQuery: () => mockModelListQuery(),
}))
vi.mock('@/app/components/header/account-setting/model-provider-page/hooks', () => ({
@ -18,8 +19,8 @@ vi.mock('../../header/account-setting/model-provider-page/model-icon', () => ({
describe('PublishWithMultipleModel', () => {
beforeEach(() => {
vi.clearAllMocks()
mockUseProviderContext.mockReturnValue({
textGenerationModelList: [
mockModelListQuery.mockReturnValue({
data: [
{
provider: 'openai',
models: [

View File

@ -1,9 +1,9 @@
import type {
ProviderModelWithStatusEntity,
ProviderWithModelsResponse,
} from '@dify/contracts/api/console/workspaces/types.gen'
import type { FC } from 'react'
import type { ModelAndParameter } from '../configuration/debug/types'
import type {
Model,
ModelItem,
} from '@/app/components/header/account-setting/model-provider-page/declarations'
import { Button } from '@langgenius/dify-ui/button'
import {
DropdownMenu,
@ -12,31 +12,39 @@ import {
DropdownMenuTrigger,
} from '@langgenius/dify-ui/dropdown-menu'
import { RiArrowDownSLine } from '@remixicon/react'
import { useQuery } from '@tanstack/react-query'
import { useState } from 'react'
import { useTranslation } from 'react-i18next'
import { ModelTypeEnum } from '@/app/components/header/account-setting/model-provider-page/declarations'
import { useLanguage } from '@/app/components/header/account-setting/model-provider-page/hooks'
import { useProviderContext } from '@/context/provider-context'
import { renderI18nObject } from '@/i18n-config'
import { consoleQuery } from '@/service/console'
import ModelIcon from '../../header/account-setting/model-provider-page/model-icon'
type PublishWithMultipleModelProps = {
disabled?: boolean
multipleModelConfigs: ModelAndParameter[]
// textGenerationModelList?: Model[]
onSelect: (v: ModelAndParameter) => void
}
const PublishWithMultipleModel: FC<PublishWithMultipleModelProps> = ({
disabled = false,
multipleModelConfigs,
// textGenerationModelList = [],
onSelect,
}) => {
const { t } = useTranslation()
const language = useLanguage()
const { textGenerationModelList } = useProviderContext()
const { data: textGenerationModelList = [] } = useQuery(
consoleQuery.workspaces.current.models.modelTypes.byModelType.get.queryOptions({
input: { params: { model_type: ModelTypeEnum.textGeneration } },
select: (response) => response.data,
}),
)
const [open, setOpen] = useState(false)
const validModelConfigs: (ModelAndParameter & { modelItem: ModelItem; providerItem: Model })[] =
[]
const validModelConfigs: (ModelAndParameter & {
modelItem: ProviderModelWithStatusEntity
providerItem: ProviderWithModelsResponse
})[] = []
multipleModelConfigs.forEach((item) => {
const provider = textGenerationModelList.find((model) => model.provider === item.provider)
@ -80,9 +88,9 @@ const PublishWithMultipleModel: FC<PublishWithMultipleModelProps> = ({
<ModelIcon modelName={item.model} provider={item.providerItem} className="ml-2" />
<div
className="ml-1 truncate text-text-secondary"
title={item.modelItem.label[language]}
title={renderI18nObject(item.modelItem.label, language)}
>
{item.modelItem.label[language]}
{renderI18nObject(item.modelItem.label, language)}
</div>
</DropdownMenuItem>
))}

View File

@ -98,7 +98,6 @@ const createDeletedAgentTool = (providerId: string): AgentTool => ({
const createContextValue = (): ComponentProps<typeof ConfigContext.Provider>['value'] => ({
appId: 'app-1',
isAPIKeySet: true,
isTrailFinished: false,
mode: AppModeEnum.CHAT,
modelModeType: ModelModeType.chat,

View File

@ -177,7 +177,6 @@ const ConfigurationView: FC<ConfigurationViewModel> = ({
>
<div className="flex grow flex-col rounded-tl-2xl border-t-[0.5px] border-l-[0.5px] border-components-panel-border bg-chatbot-bg">
<Debug
isAPIKeySet={contextValue.isAPIKeySet}
onSetting={onOpenAccountSettings}
inputs={contextValue.inputs}
modelParameterParams={{
@ -260,7 +259,6 @@ const ConfigurationView: FC<ConfigurationViewModel> = ({
/>
</div>
<Debug
isAPIKeySet={contextValue.isAPIKeySet}
onSetting={onOpenAccountSettings}
inputs={contextValue.inputs}
modelParameterParams={{

View File

@ -1,3 +1,5 @@
import type { GetWorkspacesCurrentModelsModelTypesByModelTypeData } from '@dify/contracts/api/console/workspaces/types.gen'
import type { OperationKey } from '@orpc/tanstack-query'
import type { MockedFunction } from 'vite-plus/test'
import type { DataSet } from '@/models/datasets'
import type { RetrievalConfig } from '@/types/app'
@ -53,9 +55,9 @@ const mockOnCancel = vi.fn()
const mockOnSave = vi.fn()
const mockSetSettingsDestination = vi.fn()
const mockUseModelList = vi.fn()
const mockUseModelListAndDefaultModel = vi.fn()
const mockUseModelListAndDefaultModelAndCurrentProviderAndModel = vi.fn()
const mockModelListQuery = vi.fn()
const mockModelListQueryAndDefaultModel = vi.fn()
const mockModelListQueryAndDefaultModelAndCurrentProviderAndModel = vi.fn()
const mockUseCurrentProviderAndModel = vi.fn()
const mockCheckShowMultiModalTip = vi.fn()
@ -94,15 +96,13 @@ vi.mock('@/context/i18n', () => ({
vi.mock('@/context/provider-context', () => ({
useProviderContext: () => ({
modelProviders: [],
textGenerationModelList: [],
}),
}))
vi.mock('@/app/components/header/account-setting/model-provider-page/hooks', () => ({
useModelList: (...args: unknown[]) => mockUseModelList(...args),
useModelListAndDefaultModel: (...args: unknown[]) => mockUseModelListAndDefaultModel(...args),
useModelListAndDefaultModel: (...args: unknown[]) => mockModelListQueryAndDefaultModel(...args),
useModelListAndDefaultModelAndCurrentProviderAndModel: (...args: unknown[]) =>
mockUseModelListAndDefaultModelAndCurrentProviderAndModel(...args),
mockModelListQueryAndDefaultModelAndCurrentProviderAndModel(...args),
useCurrentProviderAndModel: (...args: unknown[]) => mockUseCurrentProviderAndModel(...args),
}))
@ -251,7 +251,7 @@ describe('SettingsModal', () => {
],
},
} as ReturnType<typeof useMembers>)
mockUseModelList.mockImplementation((type: ModelTypeEnum) => {
mockModelListQuery.mockImplementation((type: ModelTypeEnum) => {
if (type === ModelTypeEnum.rerank) {
return {
data: [
@ -264,8 +264,8 @@ describe('SettingsModal', () => {
}
return { data: [{ provider: 'embed-provider', models: [{ model: 'embed-model' }] }] }
})
mockUseModelListAndDefaultModel.mockReturnValue({ modelList: [], defaultModel: null })
mockUseModelListAndDefaultModelAndCurrentProviderAndModel.mockReturnValue({
mockModelListQueryAndDefaultModel.mockReturnValue({ modelList: [], defaultModel: null })
mockModelListQueryAndDefaultModelAndCurrentProviderAndModel.mockReturnValue({
defaultModel: null,
currentModel: null,
})
@ -436,7 +436,7 @@ describe('SettingsModal', () => {
it('should block save when reranking is enabled without model', async () => {
// Arrange
const user = userEvent.setup()
mockUseModelList.mockReturnValue({ data: [] })
mockModelListQuery.mockReturnValue({ data: [] })
const dataset = createDataset(
{},
createRetrievalConfig({
@ -601,3 +601,22 @@ describe('SettingsModal', () => {
})
})
})
vi.mock('@tanstack/react-query', async (importOriginal) => {
const actual = await importOriginal<typeof import('@tanstack/react-query')>()
return {
...actual,
useQuery: (options: {
queryKey: OperationKey<
'query',
{ params: GetWorkspacesCurrentModelsModelTypesByModelTypeData['path'] }
>
}) => {
if (!options.queryKey[0].includes('modelTypes')) return actual.useQuery(options)
const args = options.queryKey[1].input?.params?.model_type
if (!args) throw new Error('Missing model type in query')
return mockModelListQuery(args)
},
}
})

View File

@ -1,3 +1,5 @@
import type { GetWorkspacesCurrentModelsModelTypesByModelTypeData } from '@dify/contracts/api/console/workspaces/types.gen'
import type { OperationKey } from '@orpc/tanstack-query'
import type { ReactElement } from 'react'
import type { DataSet } from '@/models/datasets'
import type { RetrievalConfig } from '@/types/app'
@ -18,9 +20,9 @@ import { withSelectorKey } from '@/test/i18n-mock'
import { RETRIEVE_METHOD } from '@/types/app'
import { RetrievalChangeTip, RetrievalSection } from '../retrieval-section'
const mockUseModelList = vi.fn()
const mockUseModelListAndDefaultModel = vi.fn()
const mockUseModelListAndDefaultModelAndCurrentProviderAndModel = vi.fn()
const mockModelListQuery = vi.fn()
const mockModelListQueryAndDefaultModel = vi.fn()
const mockModelListQueryAndDefaultModelAndCurrentProviderAndModel = vi.fn()
const mockUseCurrentProviderAndModel = vi.fn()
vi.mock('ky', () => {
@ -33,15 +35,14 @@ vi.mock('ky', () => {
vi.mock('@/context/provider-context', () => ({
useProviderContext: () => ({
modelProviders: [],
textGenerationModelList: [],
}),
}))
vi.mock('@/app/components/header/account-setting/model-provider-page/hooks', () => ({
useModelListAndDefaultModelAndCurrentProviderAndModel: (...args: unknown[]) =>
mockUseModelListAndDefaultModelAndCurrentProviderAndModel(...args),
useModelListAndDefaultModel: (...args: unknown[]) => mockUseModelListAndDefaultModel(...args),
useModelList: (...args: unknown[]) => mockUseModelList(...args),
mockModelListQueryAndDefaultModelAndCurrentProviderAndModel(...args),
useModelListAndDefaultModel: (...args: unknown[]) => mockModelListQueryAndDefaultModel(...args),
useCurrentProviderAndModel: (...args: unknown[]) => mockUseCurrentProviderAndModel(...args),
}))
@ -221,13 +222,13 @@ describe('RetrievalSection', () => {
beforeEach(() => {
vi.clearAllMocks()
mockUseModelList.mockImplementation((type: ModelTypeEnum) => {
mockModelListQuery.mockImplementation((type: ModelTypeEnum) => {
if (type === ModelTypeEnum.rerank)
return { data: [{ provider: 'rerank-provider', models: [{ model: 'rerank-model' }] }] }
return { data: [] }
})
mockUseModelListAndDefaultModel.mockReturnValue({ modelList: [], defaultModel: null })
mockUseModelListAndDefaultModelAndCurrentProviderAndModel.mockReturnValue({
mockModelListQueryAndDefaultModel.mockReturnValue({ modelList: [], defaultModel: null })
mockModelListQueryAndDefaultModelAndCurrentProviderAndModel.mockReturnValue({
defaultModel: null,
currentModel: null,
})
@ -338,3 +339,22 @@ describe('RetrievalSection', () => {
)
})
})
vi.mock('@tanstack/react-query', async (importOriginal) => {
const actual = await importOriginal<typeof import('@tanstack/react-query')>()
return {
...actual,
useQuery: (options: {
queryKey: OperationKey<
'query',
{ params: GetWorkspacesCurrentModelsModelTypesByModelTypeData['path'] }
>
}) => {
if (!options.queryKey[0].includes('modelTypes')) return actual.useQuery(options)
const args = options.queryKey[1].input?.params?.model_type
if (!args) throw new Error('Missing model type in query')
return mockModelListQuery(args)
},
}
})

View File

@ -8,6 +8,7 @@ import { cn } from '@langgenius/dify-ui/cn'
import { Input } from '@langgenius/dify-ui/input'
import { Textarea } from '@langgenius/dify-ui/textarea'
import { RiCloseLine } from '@remixicon/react'
import { useQuery } from '@tanstack/react-query'
import { isEqual } from 'es-toolkit/predicate'
import { useQueryState } from 'nuqs'
import { useEffect, useId, useMemo, useRef, useState } from 'react'
@ -19,7 +20,6 @@ import IndexMethod from '@/app/components/datasets/settings/index-method'
import PermissionSelector from '@/app/components/datasets/settings/permission-selector'
import { checkShowMultiModalTip } from '@/app/components/datasets/settings/utils'
import { ModelTypeEnum } from '@/app/components/header/account-setting/model-provider-page/declarations'
import { useModelList } from '@/app/components/header/account-setting/model-provider-page/hooks'
import { ModelSelector } from '@/app/components/header/account-setting/model-provider-page/model-selector'
import {
settingsQueryParamName,
@ -27,6 +27,7 @@ import {
} from '@/app/components/header/account-setting/query-params'
import { useDocLink } from '@/context/i18n'
import { DatasetPermission } from '@/models/datasets'
import { consoleQuery } from '@/service/console'
import { updateDatasetSetting } from '@/service/datasets'
import { useMembers } from '@/service/use-common'
import { RetrievalChangeTip, RetrievalSection } from './retrieval-section'
@ -52,8 +53,18 @@ const SettingsModal: FC<SettingsModalProps> = ({
onCancel,
onSave,
}) => {
const { data: embeddingModelList } = useModelList(ModelTypeEnum.textEmbedding)
const { data: rerankModelList } = useModelList(ModelTypeEnum.rerank)
const { data: embeddingModelList = [] } = useQuery(
consoleQuery.workspaces.current.models.modelTypes.byModelType.get.queryOptions({
input: { params: { model_type: ModelTypeEnum.textEmbedding } },
select: (response) => response.data,
}),
)
const { data: rerankModelList = [] } = useQuery(
consoleQuery.workspaces.current.models.modelTypes.byModelType.get.queryOptions({
input: { params: { model_type: ModelTypeEnum.rerank } },
select: (response) => response.data,
}),
)
const { t } = useTranslation()
const translateRetrieval: RetrievalTranslate = (selector, options) => t(selector, options)
const docLink = useDocLink()

View File

@ -39,9 +39,10 @@ const mockState = vi.hoisted(() => ({
fileUploadConfig: undefined as { image_file_size_limit?: number } | undefined,
},
},
mockProviderContext: {
textGenerationModelList: [] as Array<{
mockModelListResult: {
data: [] as Array<{
provider: string
status: string
models: Array<{
model: string
features?: string[]
@ -209,8 +210,9 @@ vi.mock('@/context/event-emitter', () => ({
}),
}))
vi.mock('@/context/provider-context', () => ({
useProviderContext: () => mockState.mockProviderContext,
vi.mock('@tanstack/react-query', async (importOriginal) => ({
...(await importOriginal<typeof import('@tanstack/react-query')>()),
useQuery: () => mockState.mockModelListResult,
}))
vi.mock('@/service/debug', () => ({
@ -307,7 +309,6 @@ const createContextValue = (overrides: Partial<DebugContextValue> = {}): DebugCo
readonly: false,
canTestAndRun: true,
appId: 'app-id',
isAPIKeySet: true,
isTrailFinished: false,
mode: AppModeEnum.CHAT,
modelModeType: ModelModeType.chat,
@ -454,7 +455,6 @@ const renderDebug = (
) => {
const onSetting = vi.fn()
const props: ComponentProps<typeof Debug> = {
isAPIKeySet: true,
onSetting,
inputs: {},
modelParameterParams: {
@ -499,10 +499,11 @@ describe('Debug', () => {
text2speech: { enabled: false },
file: { enabled: false, allowed_file_upload_methods: [], fileUploadConfig: undefined },
}
mockState.mockProviderContext = {
textGenerationModelList: [
mockState.mockModelListResult = {
data: [
{
provider: 'openai',
status: 'active',
models: [
{
model: 'vision-model',
@ -525,9 +526,7 @@ describe('Debug', () => {
model_id: '',
},
},
props: {
isAPIKeySet: false,
},
props: {},
})
expect(screen.getByText('appDebug.noModelProviderConfigured'))!.toBeInTheDocument()
@ -546,9 +545,7 @@ describe('Debug', () => {
model_id: '',
},
},
props: {
isAPIKeySet: true,
},
props: {},
})
expect(screen.getByText('appDebug.noModelSelected'))!.toBeInTheDocument()

View File

@ -9,7 +9,7 @@ import ChatItem from '../chat-item'
const mockConsoleStateReader = vi.fn()
const mockUseDebugConfigurationContext = vi.fn()
const mockUseProviderContext = vi.fn()
const mockModelListQuery = vi.fn()
const mockUseFeatures = vi.fn()
const mockUseConfigFromDebugContext = vi.fn()
const mockUseFormattingChangedSubscription = vi.fn()
@ -39,8 +39,9 @@ vi.mock('@/context/debug-configuration', () => ({
useDebugConfigurationContext: () => mockUseDebugConfigurationContext(),
}))
vi.mock('@/context/provider-context', () => ({
useProviderContext: () => mockUseProviderContext(),
vi.mock('@tanstack/react-query', async (importOriginal) => ({
...(await importOriginal<typeof import('@tanstack/react-query')>()),
useQuery: () => mockModelListQuery(),
}))
vi.mock('@/app/components/base/features/hooks', () => ({
@ -137,8 +138,8 @@ const createDefaultMocks = () => {
canTestAndRun: true,
})
mockUseProviderContext.mockReturnValue({
textGenerationModelList: [
mockModelListQuery.mockReturnValue({
data: [
{
provider: 'openai',
models: [
@ -429,8 +430,8 @@ describe('ChatItem', () => {
})
it('should not include files when vision is not supported', () => {
mockUseProviderContext.mockReturnValue({
textGenerationModelList: [
mockModelListQuery.mockReturnValue({
data: [
{
provider: 'openai',
models: [
@ -624,8 +625,8 @@ describe('ChatItem', () => {
describe('edge cases', () => {
it('should handle missing provider in textGenerationModelList', () => {
mockUseProviderContext.mockReturnValue({
textGenerationModelList: [],
mockModelListQuery.mockReturnValue({
data: [],
})
const handleSend = vi.fn()

View File

@ -8,7 +8,7 @@ import DebugItem from '../debug-item'
const mockUseDebugConfigurationContext = vi.fn()
const mockUseDebugWithMultipleModelContext = vi.fn()
const mockUseProviderContext = vi.fn()
const mockModelListQuery = vi.fn()
let capturedModelParameterTriggerProps: {
modelAndParameter: ModelAndParameter
@ -22,8 +22,9 @@ vi.mock('../context', () => ({
useDebugWithMultipleModelContext: () => mockUseDebugWithMultipleModelContext(),
}))
vi.mock('@/context/provider-context', () => ({
useProviderContext: () => mockUseProviderContext(),
vi.mock('@tanstack/react-query', async (importOriginal) => ({
...(await importOriginal<typeof import('@tanstack/react-query')>()),
useQuery: () => mockModelListQuery(),
}))
vi.mock('../chat-item', () => ({
@ -106,10 +107,8 @@ describe('DebugItem', () => {
onDebugWithMultipleModelChange: vi.fn(),
})
mockUseProviderContext.mockReturnValue({
textGenerationModelList: createTextGenerationModelList([
{ provider: 'openai', model: 'gpt-3.5-turbo' },
]),
mockModelListQuery.mockReturnValue({
data: createTextGenerationModelList([{ provider: 'openai', model: 'gpt-3.5-turbo' }]),
})
})
@ -158,8 +157,8 @@ describe('DebugItem', () => {
describe('ChatItem rendering', () => {
it('should render ChatItem in CHAT mode with active model', () => {
mockUseDebugConfigurationContext.mockReturnValue({ mode: AppModeEnum.CHAT })
mockUseProviderContext.mockReturnValue({
textGenerationModelList: createTextGenerationModelList([
mockModelListQuery.mockReturnValue({
data: createTextGenerationModelList([
{ provider: 'openai', model: 'gpt-3.5-turbo', status: ModelStatusEnum.active },
]),
})
@ -172,8 +171,8 @@ describe('DebugItem', () => {
it('should render ChatItem in AGENT_CHAT mode with active model', () => {
mockUseDebugConfigurationContext.mockReturnValue({ mode: AppModeEnum.AGENT_CHAT })
mockUseProviderContext.mockReturnValue({
textGenerationModelList: createTextGenerationModelList([
mockModelListQuery.mockReturnValue({
data: createTextGenerationModelList([
{ provider: 'openai', model: 'gpt-3.5-turbo', status: ModelStatusEnum.active },
]),
})
@ -185,8 +184,8 @@ describe('DebugItem', () => {
it('should not render ChatItem when model is not active', () => {
mockUseDebugConfigurationContext.mockReturnValue({ mode: AppModeEnum.CHAT })
mockUseProviderContext.mockReturnValue({
textGenerationModelList: createTextGenerationModelList([
mockModelListQuery.mockReturnValue({
data: createTextGenerationModelList([
{ provider: 'openai', model: 'gpt-3.5-turbo', status: ModelStatusEnum.disabled },
]),
})
@ -198,8 +197,8 @@ describe('DebugItem', () => {
it('should not render ChatItem when provider not found', () => {
mockUseDebugConfigurationContext.mockReturnValue({ mode: AppModeEnum.CHAT })
mockUseProviderContext.mockReturnValue({
textGenerationModelList: createTextGenerationModelList([
mockModelListQuery.mockReturnValue({
data: createTextGenerationModelList([
{ provider: 'anthropic', model: 'claude-3', status: ModelStatusEnum.active },
]),
})
@ -211,8 +210,8 @@ describe('DebugItem', () => {
it('should not render ChatItem when model not found', () => {
mockUseDebugConfigurationContext.mockReturnValue({ mode: AppModeEnum.CHAT })
mockUseProviderContext.mockReturnValue({
textGenerationModelList: createTextGenerationModelList([
mockModelListQuery.mockReturnValue({
data: createTextGenerationModelList([
{ provider: 'openai', model: 'gpt-4', status: ModelStatusEnum.active },
]),
})
@ -226,8 +225,8 @@ describe('DebugItem', () => {
describe('TextGenerationItem rendering', () => {
it('should render TextGenerationItem in COMPLETION mode with active model', () => {
mockUseDebugConfigurationContext.mockReturnValue({ mode: AppModeEnum.COMPLETION })
mockUseProviderContext.mockReturnValue({
textGenerationModelList: createTextGenerationModelList([
mockModelListQuery.mockReturnValue({
data: createTextGenerationModelList([
{ provider: 'openai', model: 'gpt-3.5-turbo', status: ModelStatusEnum.active },
]),
})
@ -240,8 +239,8 @@ describe('DebugItem', () => {
it('should not render TextGenerationItem when provider is not found', () => {
mockUseDebugConfigurationContext.mockReturnValue({ mode: AppModeEnum.COMPLETION })
mockUseProviderContext.mockReturnValue({
textGenerationModelList: createTextGenerationModelList([
mockModelListQuery.mockReturnValue({
data: createTextGenerationModelList([
{ provider: 'anthropic', model: 'claude-3', status: ModelStatusEnum.active },
]),
})
@ -493,8 +492,8 @@ describe('DebugItem', () => {
})
it('should handle empty textGenerationModelList', () => {
mockUseProviderContext.mockReturnValue({
textGenerationModelList: [],
mockModelListQuery.mockReturnValue({
data: [],
})
renderComponent()
@ -505,8 +504,8 @@ describe('DebugItem', () => {
it('should handle model with quotaExceeded status', () => {
mockUseDebugConfigurationContext.mockReturnValue({ mode: AppModeEnum.CHAT })
mockUseProviderContext.mockReturnValue({
textGenerationModelList: createTextGenerationModelList([
mockModelListQuery.mockReturnValue({
data: createTextGenerationModelList([
{ provider: 'anthropic', model: 'not-matching', status: ModelStatusEnum.quotaExceeded },
]),
})

View File

@ -5,7 +5,7 @@ import { APP_CHAT_WITH_MULTIPLE_MODEL } from '../../types'
import TextGenerationItem from '../text-generation-item'
const mockUseDebugConfigurationContext = vi.fn()
const mockUseProviderContext = vi.fn()
const mockModelListQuery = vi.fn()
const mockUseFeatures = vi.fn()
const mockUseTextGeneration = vi.fn()
const mockUseEventEmitterContextContext = vi.fn()
@ -30,8 +30,9 @@ vi.mock('@/context/debug-configuration', () => ({
useDebugConfigurationContext: () => mockUseDebugConfigurationContext(),
}))
vi.mock('@/context/provider-context', () => ({
useProviderContext: () => mockUseProviderContext(),
vi.mock('@tanstack/react-query', async (importOriginal) => ({
...(await importOriginal<typeof import('@tanstack/react-query')>()),
useQuery: () => mockModelListQuery(),
}))
vi.mock('@/app/components/base/features/hooks', () => ({
@ -105,8 +106,8 @@ const createDefaultMocks = () => {
datasetConfigs: { retrieval_model: 'single' },
})
mockUseProviderContext.mockReturnValue({
textGenerationModelList: [
mockModelListQuery.mockReturnValue({
data: [
{
provider: 'openai',
models: [
@ -597,8 +598,8 @@ describe('TextGenerationItem', () => {
messageId: null,
})
mockUseProviderContext.mockReturnValue({
textGenerationModelList: [
mockModelListQuery.mockReturnValue({
data: [
{
provider: 'openai',
models: [
@ -643,8 +644,8 @@ describe('TextGenerationItem', () => {
messageId: null,
})
mockUseProviderContext.mockReturnValue({
textGenerationModelList: [],
mockModelListQuery.mockReturnValue({
data: [],
})
renderComponent()

View File

@ -3,18 +3,21 @@ import type { ModelAndParameter } from '../types'
import type { InputForm } from '@/app/components/base/chat/chat/type'
import type { ChatConfig, OnSend } from '@/app/components/base/chat/types'
import { Avatar } from '@langgenius/dify-ui/avatar'
import { useSuspenseQuery } from '@tanstack/react-query'
import { useQuery, useSuspenseQuery } from '@tanstack/react-query'
import { memo, useCallback, useMemo } from 'react'
import { toast } from '@/app/components/app/configuration/toast'
import Chat from '@/app/components/base/chat/chat'
import { useChat } from '@/app/components/base/chat/chat/hooks'
import { getLastAnswer } from '@/app/components/base/chat/utils'
import { useFeatures } from '@/app/components/base/features/hooks'
import { ModelFeatureEnum } from '@/app/components/header/account-setting/model-provider-page/declarations'
import {
ModelFeatureEnum,
ModelTypeEnum,
} from '@/app/components/header/account-setting/model-provider-page/declarations'
import { useDebugConfigurationContext } from '@/context/debug-configuration'
import { useEventEmitterContextContext } from '@/context/event-emitter'
import { useProviderContext } from '@/context/provider-context'
import { userProfileQueryOptions } from '@/features/account-profile/client'
import { consoleQuery } from '@/service/console'
import {
fetchConversationMessages,
fetchSuggestedQuestions,
@ -39,7 +42,12 @@ const ChatItem: FC<ChatItemProps> = ({ modelAndParameter }) => {
collectionList,
canTestAndRun = false,
} = useDebugConfigurationContext()
const { textGenerationModelList } = useProviderContext()
const { data: textGenerationModelList = [] } = useQuery(
consoleQuery.workspaces.current.models.modelTypes.byModelType.get.queryOptions({
input: { params: { model_type: ModelTypeEnum.textGeneration } },
select: (response) => response.data,
}),
)
const features = useFeatures((s) => s.features)
const configTemplate = useConfigFromDebugContext()
const config = useMemo(() => {

View File

@ -8,11 +8,15 @@ import {
DropdownMenuTrigger,
} from '@langgenius/dify-ui/dropdown-menu'
import { IconButton } from '@langgenius/dify-ui/icon-button'
import { useQuery } from '@tanstack/react-query'
import { memo } from 'react'
import { useTranslation } from 'react-i18next'
import { ModelStatusEnum } from '@/app/components/header/account-setting/model-provider-page/declarations'
import {
ModelStatusEnum,
ModelTypeEnum,
} from '@/app/components/header/account-setting/model-provider-page/declarations'
import { useDebugConfigurationContext } from '@/context/debug-configuration'
import { useProviderContext } from '@/context/provider-context'
import { consoleQuery } from '@/service/console'
import { AppModeEnum } from '@/types/app'
import ChatItem from './chat-item'
import { useDebugWithMultipleModelContext } from './context'
@ -29,7 +33,12 @@ const DebugItem: FC<DebugItemProps> = ({ modelAndParameter, className, style })
const { mode } = useDebugConfigurationContext()
const { multipleModelConfigs, onMultipleModelConfigsChange, onDebugWithMultipleModelChange } =
useDebugWithMultipleModelContext()
const { textGenerationModelList } = useProviderContext()
const { data: textGenerationModelList = [] } = useQuery(
consoleQuery.workspaces.current.models.modelTypes.byModelType.get.queryOptions({
input: { params: { model_type: ModelTypeEnum.textGeneration } },
select: (response) => response.data,
}),
)
const index = multipleModelConfigs.findIndex((v) => v.id === modelAndParameter.id)
const currentProvider = textGenerationModelList.find(

View File

@ -1,6 +1,7 @@
import type { FC } from 'react'
import type { ModelAndParameter } from '../types'
import type { OnSend, TextGenerationConfig } from '@/app/components/base/text-generation/types'
import { useQuery } from '@tanstack/react-query'
import { noop } from 'es-toolkit/function'
import { cloneDeep } from 'es-toolkit/object'
import { memo } from 'react'
@ -9,10 +10,11 @@ import TextGeneration from '@/app/components/app/text-generate/item'
import { TransferMethod } from '@/app/components/base/chat/types'
import { useFeatures } from '@/app/components/base/features/hooks'
import { useTextGeneration } from '@/app/components/base/text-generation/hooks'
import { ModelTypeEnum } from '@/app/components/header/account-setting/model-provider-page/declarations'
import { DEFAULT_CHAT_PROMPT_CONFIG, DEFAULT_COMPLETION_PROMPT_CONFIG } from '@/config'
import { useDebugConfigurationContext } from '@/context/debug-configuration'
import { useEventEmitterContextContext } from '@/context/event-emitter'
import { useProviderContext } from '@/context/provider-context'
import { consoleQuery } from '@/service/console'
import { AppSourceType } from '@/service/share'
import { promptVariablesToUserInputsForm } from '@/utils/model-config'
import { APP_CHAT_WITH_MULTIPLE_MODEL } from '../types'
@ -37,7 +39,12 @@ const TextGenerationItem: FC<TextGenerationItemProps> = ({ modelAndParameter })
dataSets,
datasetConfigs,
} = useDebugConfigurationContext()
const { textGenerationModelList } = useProviderContext()
const { data: textGenerationModelList = [] } = useQuery(
consoleQuery.workspaces.current.models.modelTypes.byModelType.get.queryOptions({
input: { params: { model_type: ModelTypeEnum.textGeneration } },
select: (response) => response.data,
}),
)
const features = useFeatures((s) => s.features)
const postDatasets = dataSets.map(({ id }) => ({
dataset: {

View File

@ -4,17 +4,10 @@ import type { DebugWithSingleModelRefType } from '../index'
import type { ChatItem } from '@/app/components/base/chat/types'
import type { FileEntity } from '@/app/components/base/file-uploader/types'
import type { Collection } from '@/app/components/tools/types'
import type { ProviderContextState } from '@/context/provider-context'
import type { DatasetConfigs, ModelConfig } from '@/models/debug'
import { act, fireEvent, screen, waitFor } from '@testing-library/react'
import { createRef } from 'react'
import { useStore as useAppStore } from '@/app/components/app/store'
import {
ConfigurationMethodEnum,
ModelFeatureEnum,
ModelStatusEnum,
ModelTypeEnum,
} from '@/app/components/header/account-setting/model-provider-page/declarations'
import { CollectionType } from '@/app/components/tools/types'
import { SupportUploadFileTypes } from '@/app/components/workflow/types'
import { PromptMode } from '@/models/debug'
@ -93,43 +86,6 @@ function createMockCollections(collections: Partial<Collection>[] = []): Collect
)
}
/**
* Factory function for creating mock Provider Context
*/
function createMockProviderContext(
overrides: Partial<ProviderContextState> = {},
): ProviderContextState {
return {
textGenerationModelList: [
{
provider: 'openai',
label: { en_US: 'OpenAI', zh_Hans: 'OpenAI' },
icon_small: { en_US: 'icon', zh_Hans: 'icon' },
status: ModelStatusEnum.active,
models: [
{
model: 'gpt-3.5-turbo',
label: { en_US: 'GPT-3.5', zh_Hans: 'GPT-3.5' },
model_type: ModelTypeEnum.textGeneration,
features: [ModelFeatureEnum.vision],
fetch_from: ConfigurationMethodEnum.predefinedModel,
model_properties: {},
deprecated: false,
},
],
},
],
hasSettedApiKey: true,
modelProviders: [],
speech2textDefaultModel: null,
ttsDefaultModel: null,
agentThoughtDefaultModel: null,
updateModelList: vi.fn(),
refreshModelProviders: vi.fn(),
...overrides,
} as ProviderContextState
}
// ============================================================================
// Mock External Dependencies ONLY (Following testing.md guidelines)
// ============================================================================
@ -186,7 +142,6 @@ const mockDebugConfigContext = {
readonly: false,
canTestAndRun: true,
appId: 'test-app-id',
isAPIKeySet: true,
isTrailFinished: false,
mode: AppModeEnum.CHAT,
modelModeType: ModelModeType.chat,
@ -302,18 +257,6 @@ vi.mock('@/context/debug-configuration', () => ({
useDebugConfigurationContext: mockUseDebugConfigurationContext,
}))
const mockProviderContext = createMockProviderContext()
const { mockUseProviderContext } = vi.hoisted(() => ({
mockUseProviderContext: vi.fn(),
}))
mockUseProviderContext.mockReturnValue(mockProviderContext)
vi.mock('@/context/provider-context', () => ({
useProviderContext: mockUseProviderContext,
}))
const mockConsoleState = {
userProfile: {
id: 'user-1',
@ -624,7 +567,6 @@ describe('DebugWithSingleModel', () => {
// Reset mock implementations using module-level mocks
mockUseDebugConfigurationContext.mockReturnValue(mockDebugConfigContext)
mockUseProviderContext.mockReturnValue(mockProviderContext)
mockConsoleStateReader.mockReturnValue(mockConsoleState)
mockUseConfigFromDebugContext.mockReturnValue(mockConfigFromDebugContext)
mockUseFormattingChangedSubscription.mockReturnValue(undefined)
@ -807,52 +749,12 @@ describe('DebugWithSingleModel', () => {
})
it('should handle model without vision support', () => {
mockUseProviderContext.mockReturnValue(
createMockProviderContext({
textGenerationModelList: [
{
provider: 'openai',
label: { en_US: 'OpenAI', zh_Hans: 'OpenAI' },
icon_small: { en_US: 'icon', zh_Hans: 'icon' },
status: ModelStatusEnum.active,
models: [
{
model: 'gpt-3.5-turbo',
label: { en_US: 'GPT-3.5', zh_Hans: 'GPT-3.5' },
model_type: ModelTypeEnum.textGeneration,
features: [], // No vision support
fetch_from: ConfigurationMethodEnum.predefinedModel,
model_properties: {},
deprecated: false,
status: ModelStatusEnum.active,
load_balancing_enabled: false,
},
],
},
],
}),
)
render(<DebugWithSingleModel ref={ref as RefObject<DebugWithSingleModelRefType>} />)
expect(screen.getByTestId('chat-component'))!.toBeInTheDocument()
})
it('should handle missing model in provider list', () => {
mockUseProviderContext.mockReturnValue(
createMockProviderContext({
textGenerationModelList: [
{
provider: 'different-provider',
label: { en_US: 'Different Provider', zh_Hans: '不同提供商' },
icon_small: { en_US: 'icon', zh_Hans: 'icon' },
status: ModelStatusEnum.active,
models: [],
},
],
}),
)
render(<DebugWithSingleModel ref={ref as RefObject<DebugWithSingleModelRefType>} />)
expect(screen.getByTestId('chat-component'))!.toBeInTheDocument()
@ -1026,32 +928,6 @@ describe('DebugWithSingleModel', () => {
}),
})
mockUseProviderContext.mockReturnValue(
createMockProviderContext({
textGenerationModelList: [
{
provider: 'openai',
label: { en_US: 'OpenAI', zh_Hans: 'OpenAI' },
icon_small: { en_US: 'icon', zh_Hans: 'icon' },
status: ModelStatusEnum.active,
models: [
{
model: 'gpt-3.5-turbo',
label: { en_US: 'GPT-3.5', zh_Hans: 'GPT-3.5' },
model_type: ModelTypeEnum.textGeneration,
features: [ModelFeatureEnum.document],
fetch_from: ConfigurationMethodEnum.predefinedModel,
model_properties: {},
deprecated: false,
status: ModelStatusEnum.active,
load_balancing_enabled: false,
},
],
},
],
}),
)
mockFeaturesState = {
...defaultFeatures,
file: { enabled: true },
@ -1084,32 +960,6 @@ describe('DebugWithSingleModel', () => {
}),
})
mockUseProviderContext.mockReturnValue(
createMockProviderContext({
textGenerationModelList: [
{
provider: 'openai',
label: { en_US: 'OpenAI', zh_Hans: 'OpenAI' },
icon_small: { en_US: 'icon', zh_Hans: 'icon' },
status: ModelStatusEnum.active,
models: [
{
model: 'gpt-4-vision',
label: { en_US: 'GPT-4 Vision', zh_Hans: 'GPT-4 Vision' },
model_type: ModelTypeEnum.textGeneration,
features: [ModelFeatureEnum.vision],
fetch_from: ConfigurationMethodEnum.predefinedModel,
model_properties: {},
deprecated: false,
status: ModelStatusEnum.active,
load_balancing_enabled: false,
},
],
},
],
}),
)
mockFeaturesState = {
...defaultFeatures,
file: { enabled: true },

View File

@ -11,7 +11,6 @@ import { useChat } from '@/app/components/base/chat/chat/hooks'
import { getLastAnswer, isValidGeneratedAnswer } from '@/app/components/base/chat/utils'
import { useFeatures } from '@/app/components/base/features/hooks'
import { useDebugConfigurationContext } from '@/context/debug-configuration'
import { useProviderContext } from '@/context/provider-context'
import { userProfileQueryOptions } from '@/features/account-profile/client'
import {
fetchConversationMessages,
@ -49,7 +48,6 @@ const DebugWithSingleModel = ({
} = useDebugConfigurationContext()
const debugInputReadonly = !canTestAndRun
const canManageAnnotation = !readonly && canTestAndRun
const { textGenerationModelList } = useProviderContext()
const features = useFeatures((s) => s.features)
const configTemplate = useConfigFromDebugContext()
const config = useMemo(() => {
@ -140,7 +138,6 @@ const DebugWithSingleModel = ({
modelConfig.mode,
modelConfig.model_id,
modelConfig.provider,
textGenerationModelList,
],
)

View File

@ -9,6 +9,7 @@ import { Button } from '@langgenius/dify-ui/button'
import { Collapsible, CollapsiblePanel, CollapsibleTrigger } from '@langgenius/dify-ui/collapsible'
import { IconButton } from '@langgenius/dify-ui/icon-button'
import { Tooltip, TooltipContent, TooltipTrigger } from '@langgenius/dify-ui/tooltip'
import { useQuery } from '@tanstack/react-query'
import { useBoolean } from 'ahooks'
import { noop } from 'es-toolkit/function'
import { cloneDeep } from 'es-toolkit/object'
@ -34,7 +35,7 @@ import { useDefaultModel } from '@/app/components/header/account-setting/model-p
import { DEFAULT_CHAT_PROMPT_CONFIG, DEFAULT_COMPLETION_PROMPT_CONFIG } from '@/config'
import ConfigContext from '@/context/debug-configuration'
import { useEventEmitterContextContext } from '@/context/event-emitter'
import { useProviderContext } from '@/context/provider-context'
import { consoleQuery } from '@/service/console'
import { sendCompletionMessage } from '@/service/debug'
import { AppSourceType } from '@/service/share'
import { AppModeEnum, ModelModeType, TransferMethod } from '@/types/app'
@ -48,7 +49,7 @@ import DebugWithSingleModel from './debug-with-single-model'
import { APP_CHAT_WITH_MULTIPLE_MODEL, APP_CHAT_WITH_MULTIPLE_MODEL_RESTART } from './types'
type IDebug = {
isAPIKeySet: boolean
isPreview?: boolean
onSetting: () => void
inputs: Inputs
modelParameterParams: Pick<ModelParameterModalProps, 'setModel' | 'onCompletionParamsChange'>
@ -58,7 +59,7 @@ type IDebug = {
}
const Debug: FC<IDebug> = ({
isAPIKeySet = true,
isPreview = false,
onSetting,
inputs,
modelParameterParams,
@ -332,9 +333,16 @@ const Debug: FC<IDebug> = ({
}
})
const { textGenerationModelList } = useProviderContext()
const { data: textGenerationModelList } = useQuery(
consoleQuery.workspaces.current.models.modelTypes.byModelType.get.queryOptions({
input: { params: { model_type: ModelTypeEnum.textGeneration } },
select: (response) => response.data,
}),
)
const hasActiveProvider =
isPreview || !!textGenerationModelList?.some((provider) => provider.status === 'active')
const handleChangeToSingleModel = (item: ModelAndParameter) => {
const currentProvider = textGenerationModelList.find(
const currentProvider = textGenerationModelList?.find(
(modelItem) => modelItem.provider === item.provider,
)
const currentModel = currentProvider?.models.find((model) => model.model === item.model)
@ -342,8 +350,11 @@ const Debug: FC<IDebug> = ({
modelParameterParams.setModel({
modelId: item.model,
provider: item.provider,
mode: currentModel?.model_properties.mode as string,
features: currentModel?.features,
mode:
typeof currentModel?.model_properties.mode === 'string'
? currentModel.model_properties.mode
: undefined,
features: currentModel?.features ?? undefined,
})
modelParameterParams.onCompletionParamsChange(item.parameters)
onMultipleModelConfigsChange(false, [])
@ -352,7 +363,7 @@ const Debug: FC<IDebug> = ({
const handleVisionConfigInMultipleModel = useCallback(() => {
if (debugWithMultipleModel && mode) {
const supportedVision = multipleModelConfigs.some((modelConfig) => {
const currentProvider = textGenerationModelList.find(
const currentProvider = textGenerationModelList?.find(
(modelItem) => modelItem.provider === modelConfig.provider,
)
const currentModel = currentProvider?.models.find(
@ -541,9 +552,11 @@ const Debug: FC<IDebug> = ({
{!debugWithMultipleModel && (
<div className="flex grow flex-col" ref={ref}>
{/* No model provider configured */}
{(!modelConfig.provider || !isAPIKeySet) && <HasNotSetAPIKEY onSetting={onSetting} />}
{(!modelConfig.provider || !hasActiveProvider) && (
<HasNotSetAPIKEY onSetting={onSetting} />
)}
{/* No model selected */}
{modelConfig.provider && isAPIKeySet && !modelConfig.model_id && (
{modelConfig.provider && hasActiveProvider && !modelConfig.model_id && (
<div className="flex grow flex-col items-center justify-center pb-30">
<div className="flex w-full max-w-100 flex-col gap-2 px-4 py-4">
<div className="flex h-10 w-10 items-center justify-center rounded-[10px]">

View File

@ -84,12 +84,6 @@ vi.mock('nuqs', async (importOriginal) => {
return { ...actual, useQueryState: () => [null, mockSetSettingsDestination] }
})
vi.mock('@/context/provider-context', () => ({
useProviderContext: () => ({
isAPIKeySet: true,
}),
}))
vi.mock('@/app/components/app/store', () => ({
useStore: (selector: (state: Record<string, unknown>) => unknown) =>
selector({

View File

@ -29,7 +29,6 @@ type ContextBase = Pick<
| 'isShowDocumentConfig'
| 'isShowVisionConfig'
| 'isTrailFinished'
| 'isAPIKeySet'
| 'mode'
| 'modelModeType'
| 'prevPromptConfig'

View File

@ -22,7 +22,6 @@ import {
settingsQueryParamName,
settingsQueryParser,
} from '@/app/components/header/account-setting/query-params'
import { useProviderContext } from '@/context/provider-context'
import useBreakpoints, { MediaType } from '@/hooks/use-breakpoints'
import { PromptMode } from '@/models/debug'
import { useFileUploadConfig } from '@/service/use-common'
@ -128,7 +127,6 @@ export const useConfiguration = (): ConfigurationViewModel => {
const { currentModel: currentRerankModel, currentProvider: currentRerankProvider } =
useModelListAndDefaultModelAndCurrentProviderAndModel(ModelTypeEnum.rerank)
const { isAPIKeySet } = useProviderContext()
const { currentModel: currModel } = useTextGenerationCurrentProviderAndModelAndModelList({
provider: modelConfig.provider,
model: modelConfig.model_id,
@ -367,7 +365,6 @@ export const useConfiguration = (): ConfigurationViewModel => {
isAdvancedMode,
isAgent,
isAllowVideoUpload,
isAPIKeySet,
isFunctionCall,
isOpenAI: modelConfig.provider === 'langgenius/openai/openai',
isShowAudioConfig,

View File

@ -1,9 +1,9 @@
import type { DeploymentEdition } from '@dify/contracts/api/console/system-features/types.gen'
import type { AvailableModelListResponse } from '@dify/contracts/api/console/workspaces/types.gen'
import type { RenderOptions } from '@testing-library/react'
import type { MockedFunction } from 'vite-plus/test'
import { fireEvent, screen } from '@testing-library/react'
import { useProviderContext as actualUseProviderContext } from '@/context/provider-context'
import { renderWithConsoleQuery } from '@/test/console/query-data'
import { consoleQuery } from '@/service/console'
import { createConsoleQueryClient, renderWithConsoleQuery } from '@/test/console/query-data'
import APIKeyInfoPanel from '../index'
const { mockRouterPush, mockSetSettingsDestination } = vi.hoisted(() => ({
@ -12,9 +12,6 @@ const { mockRouterPush, mockSetSettingsDestination } = vi.hoisted(() => ({
}))
// Mock the modules before importing the functions
vi.mock('@/context/provider-context', () => ({
useProviderContext: vi.fn(),
}))
vi.mock('nuqs', async (importOriginal) => {
const actual = await importOriginal<typeof import('nuqs')>()
@ -30,24 +27,7 @@ vi.mock('@/next/navigation', () => ({
}),
}))
// Type casting for mocks
const mockUseProviderContext = actualUseProviderContext as MockedFunction<
typeof actualUseProviderContext
>
// Default mock data
const defaultProviderContext = {
modelProviders: [],
modelProviderPlugins: {},
refreshModelProviders: async () => {},
isLoadingModelProviders: false,
isSuccessModelProviders: false,
textGenerationModelList: [],
isAPIKeySet: false,
}
type MockOverrides = {
providerContext?: Partial<typeof defaultProviderContext>
}
type MockOverrides = { hasActiveProvider?: boolean }
type APIKeyInfoPanelRenderOptions = {
mockOverrides?: MockOverrides
@ -56,22 +36,33 @@ type APIKeyInfoPanelRenderOptions = {
const mainButtonName = /appOverview\.apiKeyInfo\.setAPIBtn/
let deploymentEdition: DeploymentEdition = 'COMMUNITY'
// Setup function to configure mocks
function setupMocks(overrides: MockOverrides = {}) {
mockUseProviderContext.mockReturnValue({
...defaultProviderContext,
...overrides.providerContext,
})
}
// Custom render function
function renderAPIKeyInfoPanel(options: APIKeyInfoPanelRenderOptions = {}) {
const { mockOverrides, ...renderOptions } = options
setupMocks(mockOverrides)
const queryClient = createConsoleQueryClient()
queryClient.setQueryData(
consoleQuery.workspaces.current.models.modelTypes.byModelType.get.queryKey({
input: { params: { model_type: 'llm' } },
}),
{
data: mockOverrides?.hasActiveProvider
? [
{
provider: 'openai',
tenant_id: 'test-workspace',
label: { en_US: 'OpenAI' },
status: 'active',
models: [],
},
]
: [],
} satisfies AvailableModelListResponse,
)
return renderWithConsoleQuery(<APIKeyInfoPanel />, {
...renderOptions,
queryClient,
systemFeatures: { deployment_edition: deploymentEdition },
})
}
@ -82,7 +73,7 @@ export const scenarios = {
withAPIKeyNotSet: (overrides: MockOverrides = {}) =>
renderAPIKeyInfoPanel({
mockOverrides: {
providerContext: { isAPIKeySet: false },
hasActiveProvider: false,
...overrides,
},
}),
@ -91,7 +82,7 @@ export const scenarios = {
withAPIKeySet: (overrides: MockOverrides = {}) =>
renderAPIKeyInfoPanel({
mockOverrides: {
providerContext: { isAPIKeySet: true },
hasActiveProvider: true,
...overrides,
},
}),

View File

@ -3,7 +3,7 @@ import type { FC } from 'react'
import { Button } from '@langgenius/dify-ui/button'
import { cn } from '@langgenius/dify-ui/cn'
import { RiCloseLine } from '@remixicon/react'
import { useSuspenseQuery } from '@tanstack/react-query'
import { useQuery, useSuspenseQuery } from '@tanstack/react-query'
import { useQueryState } from 'nuqs'
import * as React from 'react'
import { useState } from 'react'
@ -13,8 +13,8 @@ import {
settingsQueryParamName,
settingsQueryParser,
} from '@/app/components/header/account-setting/query-params'
import { useProviderContext } from '@/context/provider-context'
import { systemFeaturesQueryOptions } from '@/features/system-features/client'
import { consoleQuery } from '@/service/console'
const APIKeyInfoPanel: FC = () => {
const { data: deploymentEdition } = useSuspenseQuery({
@ -23,14 +23,19 @@ const APIKeyInfoPanel: FC = () => {
})
const isCloud = deploymentEdition === 'CLOUD'
const { isAPIKeySet } = useProviderContext()
const { data: hasActiveProvider = false } = useQuery(
consoleQuery.workspaces.current.models.modelTypes.byModelType.get.queryOptions({
input: { params: { model_type: 'llm' } },
select: (response) => response.data.some((provider) => provider.status === 'active'),
}),
)
const [, setSettingsDestination] = useQueryState(settingsQueryParamName, settingsQueryParser)
const { t } = useTranslation()
const [isShow, setIsShow] = useState(true)
if (isAPIKeySet) return null
if (hasActiveProvider) return null
if (!isShow) return null

View File

@ -1,7 +1,7 @@
import type {
Model,
ModelItem,
} from '@/app/components/header/account-setting/model-provider-page/declarations'
ProviderModelWithStatusEntity,
ProviderWithModelsResponse,
} from '@dify/contracts/api/console/workspaces/types.gen'
import type { RetrievalConfig } from '@/types/app'
import { describe, expect, it } from 'vite-plus/test'
import {
@ -27,7 +27,7 @@ const createRetrievalConfig = (overrides: Partial<RetrievalConfig> = {}): Retrie
...overrides,
})
const createModelItem = (model: string): ModelItem => ({
const createModelItem = (model: string): ProviderModelWithStatusEntity => ({
model,
label: { en_US: model, zh_Hans: model },
model_type: ModelTypeEnum.rerank,
@ -37,8 +37,9 @@ const createModelItem = (model: string): ModelItem => ({
load_balancing_enabled: false,
})
const createRerankModelList = (): Model[] => [
const createRerankModelList = (): ProviderWithModelsResponse[] => [
{
tenant_id: 'test-workspace',
provider: 'openai',
icon_small: { en_US: '', zh_Hans: '' },
label: { en_US: 'OpenAI', zh_Hans: 'OpenAI' },
@ -46,6 +47,7 @@ const createRerankModelList = (): Model[] => [
status: ModelStatusEnum.active,
},
{
tenant_id: 'test-workspace',
provider: 'cohere',
icon_small: { en_US: '', zh_Hans: '' },
label: { en_US: 'Cohere', zh_Hans: 'Cohere' },

View File

@ -1,4 +1,4 @@
import type { Model } from '@/app/components/header/account-setting/model-provider-page/declarations'
import type { ProviderWithModelsResponse } from '@dify/contracts/api/console/workspaces/types.gen'
import type { RetrievalConfig } from '@/types/app'
import { RerankingModeEnum } from '@/models/datasets'
import { RETRIEVE_METHOD } from '@/types/app'
@ -9,7 +9,7 @@ export const isReRankModelSelected = ({
indexMethod,
}: {
retrievalConfig: RetrievalConfig
rerankModelList: Model[]
rerankModelList: ProviderWithModelsResponse[]
indexMethod?: string
}) => {
const rerankModelSelected = (() => {

View File

@ -1,3 +1,4 @@
import type { ProviderWithModelsResponse } from '@dify/contracts/api/console/workspaces/types.gen'
import { render, screen } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import {
@ -9,7 +10,10 @@ import {
import { MultimodalRetrievalGuidance, MultimodalRetrievalGuidanceLearnMore } from '../index'
import { MULTIMODAL_RETRIEVAL_GUIDANCE_DISMISSED_STORAGE_KEY } from '../storage'
const createEmbeddingModelProvider = (features: ModelFeatureEnum[] = []) => ({
const createEmbeddingModelProvider = (
features: ModelFeatureEnum[] = [],
): ProviderWithModelsResponse => ({
tenant_id: 'test-workspace',
provider: 'test-provider',
icon_small: { en_US: '', zh_Hans: '' },
label: { en_US: 'Test Provider', zh_Hans: 'Test Provider' },

View File

@ -1,9 +1,6 @@
'use client'
import type {
DefaultModel,
Model,
} from '@/app/components/header/account-setting/model-provider-page/declarations'
import type { ProviderWithModelsResponse } from '@dify/contracts/api/console/workspaces/types.gen'
import type { DefaultModel } from '@/app/components/header/account-setting/model-provider-page/declarations'
import { cn } from '@langgenius/dify-ui/cn'
import { useTranslation } from 'react-i18next'
import { ModelFeatureEnum } from '@/app/components/header/account-setting/model-provider-page/declarations'
@ -18,14 +15,17 @@ type MultimodalRetrievalGuidanceVariant = 'create' | 'settings' | 'pipeline'
type MultimodalRetrievalGuidanceProps = {
variant: MultimodalRetrievalGuidanceVariant
embeddingModel?: DefaultModel
embeddingModelList?: Model[]
embeddingModelList?: ProviderWithModelsResponse[]
className?: string
}
const MULTIMODAL_RETRIEVAL_DOC_URL =
'https://dify.ai/blog/multimodal-retrieval-is-now-available-in-the-knowledge-base'
const isVisionEmbeddingModel = (embeddingModel?: DefaultModel, embeddingModelList?: Model[]) => {
const isVisionEmbeddingModel = (
embeddingModel?: DefaultModel,
embeddingModelList?: ProviderWithModelsResponse[],
) => {
if (!embeddingModel?.provider || !embeddingModel.model) return false
const provider = embeddingModelList?.find((item) => item.provider === embeddingModel.provider)

View File

@ -1,4 +1,8 @@
import type { Model } from '@/app/components/header/account-setting/model-provider-page/declarations'
import type {
GetWorkspacesCurrentModelsModelTypesByModelTypeData,
ProviderWithModelsResponse,
} from '@dify/contracts/api/console/workspaces/types.gen'
import type { OperationKey } from '@orpc/tanstack-query'
import type { DataSourceProvider, NotionPage } from '@/models/common'
import type {
CrawlOptions,
@ -75,8 +79,9 @@ const mockDefaultEmbeddingModel = {
model: 'text-embedding-ada-002',
}
// Model[] type structure for rerank model list (simplified mock)
const mockRerankModelList: Model[] = [
const mockRerankModelList: ProviderWithModelsResponse[] = [
{
tenant_id: 'test-workspace',
provider: 'cohere',
icon_small: { en_US: 'cohere-icon', zh_Hans: 'cohere-icon' },
label: { en_US: 'Cohere', zh_Hans: 'Cohere' },
@ -104,7 +109,7 @@ vi.mock('@/app/components/header/account-setting/model-provider-page/hooks', ()
defaultModel: mockRerankDefaultModel,
currentModel: mockIsRerankDefaultModelValid,
}),
useModelList: () => ({ data: mockEmbeddingModelList }),
useDefaultModel: () => ({ data: mockDefaultEmbeddingModel }),
}))
@ -2620,3 +2625,19 @@ describe('StepTwo Component', () => {
})
})
})
vi.mock('@tanstack/react-query', async (importOriginal) => {
const actual = await importOriginal<typeof import('@tanstack/react-query')>()
return {
...actual,
useQuery: (options: {
queryKey: OperationKey<
'query',
{ params: GetWorkspacesCurrentModelsModelTypesByModelTypeData['path'] }
>
}) => {
if (!options.queryKey[0].includes('modelTypes')) return actual.useQuery(options)
return { data: mockEmbeddingModelList }
},
}
})

View File

@ -1,10 +1,7 @@
'use client'
import type { ProviderWithModelsResponse } from '@dify/contracts/api/console/workspaces/types.gen'
import type { FC } from 'react'
import type {
DefaultModel,
Model,
} from '@/app/components/header/account-setting/model-provider-page/declarations'
import type { DefaultModel } from '@/app/components/header/account-setting/model-provider-page/declarations'
import type { RetrievalConfig } from '@/types/app'
import {
AlertDialog,
@ -40,7 +37,7 @@ type IndexingModeSectionProps = {
hasSetIndexType: boolean
docForm: ChunkingMode
embeddingModel: DefaultModel
embeddingModelList?: Model[]
embeddingModelList?: ProviderWithModelsResponse[]
retrievalConfig: RetrievalConfig
showMultiModalTip: boolean
// Flags

View File

@ -1,3 +1,5 @@
import type { GetWorkspacesCurrentModelsModelTypesByModelTypeData } from '@dify/contracts/api/console/workspaces/types.gen'
import type { OperationKey } from '@orpc/tanstack-query'
import { act, renderHook } from '@testing-library/react'
import { beforeEach, describe, expect, it, vi } from 'vite-plus/test'
import { RETRIEVE_METHOD } from '@/types/app'
@ -17,7 +19,7 @@ vi.mock('@/app/components/header/account-setting/model-provider-page/hooks', ()
defaultModel: mocks.rerankDefaultModel,
currentModel: mocks.isRerankDefaultModelValid,
}),
useModelList: () => ({ data: mocks.embeddingModelList }),
useDefaultModel: () => ({ data: mocks.defaultEmbeddingModel }),
}))
@ -159,3 +161,19 @@ describe('useIndexingConfig', () => {
})
})
})
vi.mock('@tanstack/react-query', async (importOriginal) => {
const actual = await importOriginal<typeof import('@tanstack/react-query')>()
return {
...actual,
useQuery: (options: {
queryKey: OperationKey<
'query',
{ params: GetWorkspacesCurrentModelsModelTypesByModelTypeData['path'] }
>
}) => {
if (!options.queryKey[0].includes('modelTypes')) return actual.useQuery(options)
return { data: mocks.embeddingModelList }
},
}
})

View File

@ -1,7 +1,5 @@
import type {
DefaultModel,
Model,
} from '@/app/components/header/account-setting/model-provider-page/declarations'
import type { ProviderWithModelsResponse } from '@dify/contracts/api/console/workspaces/types.gen'
import type { DefaultModel } from '@/app/components/header/account-setting/model-provider-page/declarations'
import type { NotionPage } from '@/models/common'
import type {
ChunkingMode,
@ -60,7 +58,7 @@ type ValidationParams = {
overlap: number
indexType: IndexingType
embeddingModel: DefaultModel
rerankModelList: Model[]
rerankModelList: ProviderWithModelsResponse[]
retrievalConfig: RetrievalConfig
}
export const useDocumentCreation = (options: UseDocumentCreationOptions) => {

View File

@ -1,13 +1,14 @@
import type { DefaultModel } from '@/app/components/header/account-setting/model-provider-page/declarations'
import type { RetrievalConfig } from '@/types/app'
import { useQuery } from '@tanstack/react-query'
import { useEffect, useMemo, useState } from 'react'
import { checkShowMultiModalTip } from '@/app/components/datasets/settings/utils'
import { ModelTypeEnum } from '@/app/components/header/account-setting/model-provider-page/declarations'
import {
useDefaultModel,
useModelList,
useModelListAndDefaultModelAndCurrentProviderAndModel,
} from '@/app/components/header/account-setting/model-provider-page/hooks'
import { consoleQuery } from '@/service/console'
import { RETRIEVE_METHOD } from '@/types/app'
export enum IndexingType {
@ -52,7 +53,12 @@ export const useIndexingConfig = (options: UseIndexingConfigOptions) => {
} = useModelListAndDefaultModelAndCurrentProviderAndModel(ModelTypeEnum.rerank)
// Embedding model list
const { data: embeddingModelList } = useModelList(ModelTypeEnum.textEmbedding)
const { data: embeddingModelList = [] } = useQuery(
consoleQuery.workspaces.current.models.modelTypes.byModelType.get.queryOptions({
input: { params: { model_type: ModelTypeEnum.textEmbedding } },
select: (response) => response.data,
}),
)
const { data: defaultEmbeddingModel } = useDefaultModel(ModelTypeEnum.textEmbedding)
// Index type state

View File

@ -1,3 +1,5 @@
import type { GetWorkspacesCurrentModelsModelTypesByModelTypeData } from '@dify/contracts/api/console/workspaces/types.gen'
import type { OperationKey } from '@orpc/tanstack-query'
import type { ReactNode } from 'react'
import type { DataSet, HitTesting, HitTestingRecord, HitTestingResponse } from '@/models/datasets'
import type { RetrievalConfig } from '@/types/app'
@ -154,11 +156,11 @@ vi.mock('@/service/knowledge/use-dataset', () => ({
vi.mock('@/service/knowledge/use-hit-testing', () => ({
useHitTesting: vi.fn(() => ({
mutateAsync: mockHitTestingMutateAsync,
isPending: false,
isLoading: false,
})),
useExternalKnowledgeBaseHitTesting: vi.fn(() => ({
mutateAsync: mockExternalHitTestingMutateAsync,
isPending: false,
isLoading: false,
})),
}))
@ -263,10 +265,6 @@ vi.mock('@/context/i18n', () => ({
// Mock model list hook - include all exports used by child components
vi.mock('@/app/components/header/account-setting/model-provider-page/hooks', () => ({
useModelList: vi.fn(() => ({
data: [],
isLoading: false,
})),
useModelListAndDefaultModelAndCurrentProviderAndModel: vi.fn(() => ({
modelList: [],
defaultModel: undefined,
@ -400,11 +398,11 @@ describe('HitTestingPage', () => {
await import('@/service/knowledge/use-hit-testing')
vi.mocked(useHitTesting).mockReturnValue({
mutateAsync: mockHitTestingMutateAsync,
isPending: false,
isLoading: false,
} as unknown as ReturnType<typeof useHitTesting>)
vi.mocked(useExternalKnowledgeBaseHitTesting).mockReturnValue({
mutateAsync: mockExternalHitTestingMutateAsync,
isPending: false,
isLoading: false,
} as unknown as ReturnType<typeof useExternalKnowledgeBaseHitTesting>)
const useBreakpoints = await import('@/hooks/use-breakpoints')
@ -578,3 +576,19 @@ describe('HitTestingPage', () => {
expect(await screen.findByText('External content')).toBeInTheDocument()
})
})
vi.mock('@tanstack/react-query', async (importOriginal) => {
const actual = await importOriginal<typeof import('@tanstack/react-query')>()
return {
...actual,
useQuery: (options: {
queryKey: OperationKey<
'query',
{ params: GetWorkspacesCurrentModelsModelTypesByModelTypeData['path'] }
>
}) =>
options.queryKey[0].includes('modelTypes')
? { data: [], isPending: false }
: actual.useQuery(options),
}
})

View File

@ -1,3 +1,5 @@
import type { GetWorkspacesCurrentModelsModelTypesByModelTypeData } from '@dify/contracts/api/console/workspaces/types.gen'
import type { OperationKey } from '@orpc/tanstack-query'
import type { RetrievalConfig } from '@/types/app'
import { fireEvent, screen } from '@testing-library/react'
import { beforeEach, describe, expect, it, vi } from 'vite-plus/test'
@ -50,10 +52,6 @@ vi.mock('@/app/components/datasets/common/economical-retrieval-method-config', (
default: () => <div data-testid="economical-config" />,
}))
vi.mock('@/app/components/header/account-setting/model-provider-page/hooks', () => ({
useModelList: () => ({ data: [] }),
}))
vi.mock('@/context/dataset-detail', () => ({
useDatasetDetailContextWithSelector: () => 'model-name',
}))
@ -125,3 +123,19 @@ describe('ModifyRetrievalModal', () => {
expect(screen.getByText('datasetSettings.form.retrievalSetting.learnMore')).toBeInTheDocument()
})
})
vi.mock('@tanstack/react-query', async (importOriginal) => {
const actual = await importOriginal<typeof import('@tanstack/react-query')>()
return {
...actual,
useQuery: (options: {
queryKey: OperationKey<
'query',
{ params: GetWorkspacesCurrentModelsModelTypesByModelTypeData['path'] }
>
}) => {
if (!options.queryKey[0].includes('modelTypes')) return actual.useQuery(options)
return { data: [] }
},
}
})

View File

@ -5,15 +5,16 @@ import type { RetrievalConfig } from '@/types/app'
import { Button } from '@langgenius/dify-ui/button'
import { toast } from '@langgenius/dify-ui/toast'
import { RiCloseLine } from '@remixicon/react'
import { useQuery } from '@tanstack/react-query'
import * as React from 'react'
import { useMemo, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { isReRankModelSelected } from '@/app/components/datasets/common/check-rerank-model'
import EconomicalRetrievalMethodConfig from '@/app/components/datasets/common/economical-retrieval-method-config'
import RetrievalMethodConfig from '@/app/components/datasets/common/retrieval-method-config'
import { useModelList } from '@/app/components/header/account-setting/model-provider-page/hooks'
import { useDatasetDetailContextWithSelector } from '@/context/dataset-detail'
import { useDocLink } from '@/context/i18n'
import { consoleQuery } from '@/service/console'
import { ModelTypeEnum } from '../../header/account-setting/model-provider-page/declarations'
import { checkShowMultiModalTip } from '../settings/utils'
@ -39,8 +40,18 @@ const ModifyRetrievalModal: FC<Props> = ({ indexMethod, value, isShow, onHide, o
// if (ref)
// onHide()
// }, ref)
const { data: embeddingModelList } = useModelList(ModelTypeEnum.textEmbedding)
const { data: rerankModelList } = useModelList(ModelTypeEnum.rerank)
const { data: embeddingModelList = [] } = useQuery(
consoleQuery.workspaces.current.models.modelTypes.byModelType.get.queryOptions({
input: { params: { model_type: ModelTypeEnum.textEmbedding } },
select: (response) => response.data,
}),
)
const { data: rerankModelList = [] } = useQuery(
consoleQuery.workspaces.current.models.modelTypes.byModelType.get.queryOptions({
input: { params: { model_type: ModelTypeEnum.rerank } },
select: (response) => response.data,
}),
)
const handleSave = () => {
if (
!isReRankModelSelected({

View File

@ -1,21 +1,10 @@
import type { GetWorkspacesCurrentModelsModelTypesByModelTypeData } from '@dify/contracts/api/console/workspaces/types.gen'
import type { OperationKey } from '@orpc/tanstack-query'
import { fireEvent, render, screen } from '@testing-library/react'
import { beforeEach, describe, expect, it, vi } from 'vite-plus/test'
import SummaryIndexSetting from '../summary-index-setting'
// Mock useModelList to return a list of text generation models
vi.mock('@/app/components/header/account-setting/model-provider-page/hooks', () => ({
useModelList: () => ({
data: [
{
provider: 'openai',
label: { en_US: 'OpenAI' },
models: [
{ model: 'gpt-4', label: { en_US: 'GPT-4' }, model_type: 'llm', status: 'active' },
],
},
],
}),
}))
// Mock the model list query.
// Mock ModelSelector (external component from header module)
vi.mock('@/app/components/header/account-setting/model-provider-page/model-selector', () => ({
@ -222,3 +211,30 @@ describe('SummaryIndexSetting', () => {
})
})
})
vi.mock('@tanstack/react-query', async (importOriginal) => {
const actual = await importOriginal<typeof import('@tanstack/react-query')>()
return {
...actual,
useQuery: (options: {
queryKey: OperationKey<
'query',
{ params: GetWorkspacesCurrentModelsModelTypesByModelTypeData['path'] }
>
}) => {
if (!options.queryKey[0].includes('modelTypes')) return actual.useQuery(options)
return {
data: [
{
tenant_id: 'test-workspace',
provider: 'openai',
label: { en_US: 'OpenAI' },
models: [
{ model: 'gpt-4', label: { en_US: 'GPT-4' }, model_type: 'llm', status: 'active' },
],
},
],
}
},
}
})

View File

@ -1,3 +1,5 @@
import type { GetWorkspacesCurrentModelsModelTypesByModelTypeData } from '@dify/contracts/api/console/workspaces/types.gen'
import type { OperationKey } from '@orpc/tanstack-query'
import type { ReactElement } from 'react'
import type { DataSet } from '@/models/datasets'
import type { RetrievalConfig } from '@/types/app'
@ -177,7 +179,6 @@ vi.mock('@/service/use-common', () => ({
}))
vi.mock('@/app/components/header/account-setting/model-provider-page/hooks', () => ({
useModelList: () => ({ data: [], mutate: vi.fn(), isLoading: false }),
useCurrentProviderAndModel: () => ({ currentProvider: undefined, currentModel: undefined }),
useDefaultModel: () => ({ data: undefined, mutate: vi.fn(), isLoading: false }),
useModelListAndDefaultModel: () => ({ modelList: [], defaultModel: undefined }),
@ -206,7 +207,6 @@ vi.mock('@/app/components/header/account-setting/model-provider-page/hooks', ()
// Mock provider-context
vi.mock('@/context/provider-context', () => ({
useProviderContext: () => ({
textGenerationModelList: [],
embeddingsModelList: [],
rerankModelList: [],
agentThoughtModelList: [],
@ -561,3 +561,19 @@ describe('Form', () => {
})
})
})
vi.mock('@tanstack/react-query', async (importOriginal) => {
const actual = await importOriginal<typeof import('@tanstack/react-query')>()
return {
...actual,
useQuery: (options: {
queryKey: OperationKey<
'query',
{ params: GetWorkspacesCurrentModelsModelTypesByModelTypeData['path'] }
>
}) => {
if (!options.queryKey[0].includes('modelTypes')) return actual.useQuery(options)
return { data: [], refetch: vi.fn(), isPending: false }
},
}
})

View File

@ -1,7 +1,5 @@
import type {
DefaultModel,
Model,
} from '@/app/components/header/account-setting/model-provider-page/declarations'
import type { ProviderWithModelsResponse } from '@dify/contracts/api/console/workspaces/types.gen'
import type { DefaultModel } from '@/app/components/header/account-setting/model-provider-page/declarations'
import type { DataSet, SummaryIndexSetting } from '@/models/datasets'
import type { RetrievalConfig } from '@/types/app'
import { fireEvent, screen } from '@testing-library/react'
@ -222,8 +220,9 @@ describe('IndexingSection', () => {
model: 'text-embedding-ada-002',
}
const mockEmbeddingModelList: Model[] = [
const mockEmbeddingModelList: ProviderWithModelsResponse[] = [
{
tenant_id: 'test-workspace',
provider: 'openai',
label: { en_US: 'OpenAI', zh_Hans: 'OpenAI' },
icon_small: { en_US: '', zh_Hans: '' },

View File

@ -1,8 +1,6 @@
'use client'
import type {
DefaultModel,
Model,
} from '@/app/components/header/account-setting/model-provider-page/declarations'
import type { ProviderWithModelsResponse } from '@dify/contracts/api/console/workspaces/types.gen'
import type { DefaultModel } from '@/app/components/header/account-setting/model-provider-page/declarations'
import type { DataSet, SummaryIndexSetting as SummaryIndexSettingType } from '@/models/datasets'
import type { RetrievalConfig } from '@/types/app'
import { useSuspenseQuery } from '@tanstack/react-query'
@ -34,7 +32,7 @@ type IndexingSectionProps = {
setKeywordNumber: (value: number) => void
embeddingModel: DefaultModel
setEmbeddingModel: (value: DefaultModel) => void
embeddingModelList: Model[]
embeddingModelList: ProviderWithModelsResponse[]
retrievalConfig: RetrievalConfig
setRetrievalConfig: (value: RetrievalConfig) => void
summaryIndexSetting: SummaryIndexSettingType | undefined
@ -150,7 +148,7 @@ const IndexingSection = ({
</div>
)}
{/* Embedding Model */}
{/* Embedding ProviderWithModelsResponse */}
{indexMethod === IndexingType.QUALIFIED && (
<div className={rowClass}>
<div className="flex w-45 shrink-0 flex-col pt-1">

View File

@ -1,3 +1,5 @@
import type { GetWorkspacesCurrentModelsModelTypesByModelTypeData } from '@dify/contracts/api/console/workspaces/types.gen'
import type { OperationKey } from '@orpc/tanstack-query'
import type { DataSet } from '@/models/datasets'
import type { RetrievalConfig } from '@/types/app'
import { act, waitFor } from '@testing-library/react'
@ -176,10 +178,6 @@ vi.mock('@/service/use-common', () => ({
}),
}))
vi.mock('@/app/components/header/account-setting/model-provider-page/hooks', () => ({
useModelList: () => ({ data: [] }),
}))
vi.mock('@/app/components/datasets/common/check-rerank-model', () => ({
isReRankModelSelected: () => true,
}))
@ -835,3 +833,19 @@ describe('useFormState', () => {
})
})
})
vi.mock('@tanstack/react-query', async (importOriginal) => {
const actual = await importOriginal<typeof import('@tanstack/react-query')>()
return {
...actual,
useQuery: (options: {
queryKey: OperationKey<
'query',
{ params: GetWorkspacesCurrentModelsModelTypesByModelTypeData['path'] }
>
}) => {
if (!options.queryKey[0].includes('modelTypes')) return actual.useQuery(options)
return { data: [] }
},
}
})

View File

@ -1,22 +1,21 @@
'use client'
import type { AppIconSelection } from '@/app/components/base/app-icon-picker'
import type { DefaultModel } from '@/app/components/header/account-setting/model-provider-page/declarations'
import type { Member } from '@/models/common'
import type { IconInfo, SummaryIndexSetting as SummaryIndexSettingType } from '@/models/datasets'
import type { RetrievalConfig } from '@/types/app'
import { toast } from '@langgenius/dify-ui/toast'
import { useSuspenseQuery } from '@tanstack/react-query'
import { useQuery, useSuspenseQuery } from '@tanstack/react-query'
import { useAtomValue } from 'jotai'
import { useCallback, useMemo, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { isReRankModelSelected } from '@/app/components/datasets/common/check-rerank-model'
import { ModelTypeEnum } from '@/app/components/header/account-setting/model-provider-page/declarations'
import { useModelList } from '@/app/components/header/account-setting/model-provider-page/hooks'
import { useDatasetDetailContextWithSelector } from '@/context/dataset-detail'
import { workspacePermissionKeysAtom } from '@/context/permission-state'
import { userProfileQueryOptions } from '@/features/account-profile/client'
import { DatasetPermission } from '@/models/datasets'
import { consoleQuery } from '@/service/console'
import { updateDatasetSetting } from '@/service/datasets'
import { useInvalidDatasetList } from '@/service/knowledge/use-dataset'
import { useMembers } from '@/service/use-common'
@ -103,8 +102,18 @@ export const useFormState = () => {
)
// Model lists
const { data: rerankModelList } = useModelList(ModelTypeEnum.rerank)
const { data: embeddingModelList } = useModelList(ModelTypeEnum.textEmbedding)
const { data: rerankModelList = [] } = useQuery(
consoleQuery.workspaces.current.models.modelTypes.byModelType.get.queryOptions({
input: { params: { model_type: ModelTypeEnum.rerank } },
select: (response) => response.data,
}),
)
const { data: embeddingModelList = [] } = useQuery(
consoleQuery.workspaces.current.models.modelTypes.byModelType.get.queryOptions({
input: { params: { model_type: ModelTypeEnum.textEmbedding } },
select: (response) => response.data,
}),
)
const { data: membersData } = useMembers()
const invalidDatasetList = useInvalidDatasetList()

View File

@ -2,12 +2,13 @@ import type { DefaultModel } from '@/app/components/header/account-setting/model
import type { SummaryIndexSetting as SummaryIndexSettingType } from '@/models/datasets'
import { Switch } from '@langgenius/dify-ui/switch'
import { Textarea } from '@langgenius/dify-ui/textarea'
import { useQuery } from '@tanstack/react-query'
import { memo, useCallback, useMemo } from 'react'
import { useTranslation } from 'react-i18next'
import { Infotip } from '@/app/components/base/infotip'
import { ModelTypeEnum } from '@/app/components/header/account-setting/model-provider-page/declarations'
import { useModelList } from '@/app/components/header/account-setting/model-provider-page/hooks'
import { ModelSelector } from '@/app/components/header/account-setting/model-provider-page/model-selector'
import { consoleQuery } from '@/service/console'
type SummaryIndexSettingProps = {
entry?: 'knowledge-base' | 'dataset-settings' | 'create-document'
@ -22,7 +23,12 @@ const SummaryIndexSetting = ({
readonly = false,
}: SummaryIndexSettingProps) => {
const { t } = useTranslation()
const { data: textGenerationModelList } = useModelList(ModelTypeEnum.textGeneration)
const { data: textGenerationModelList = [] } = useQuery(
consoleQuery.workspaces.current.models.modelTypes.byModelType.get.queryOptions({
input: { params: { model_type: ModelTypeEnum.textGeneration } },
select: (response) => response.data,
}),
)
const summaryIndexModelConfig = useMemo(() => {
if (!summaryIndexSetting?.model_name || !summaryIndexSetting?.model_provider_name)
return undefined

View File

@ -1,8 +1,8 @@
import type {
DefaultModel,
Model,
ModelItem,
} from '@/app/components/header/account-setting/model-provider-page/declarations'
ProviderModelWithStatusEntity,
ProviderWithModelsResponse,
} from '@dify/contracts/api/console/workspaces/types.gen'
import type { DefaultModel } from '@/app/components/header/account-setting/model-provider-page/declarations'
import {
ConfigurationMethodEnum,
ModelFeatureEnum,
@ -14,7 +14,10 @@ import { checkShowMultiModalTip } from '../index'
describe('checkShowMultiModalTip', () => {
// Helper to create a model item with specific features
const createModelItem = (model: string, features: ModelFeatureEnum[] = []): ModelItem => ({
const createModelItem = (
model: string,
features: ModelFeatureEnum[] = [],
): ProviderModelWithStatusEntity => ({
model,
label: { en_US: model, zh_Hans: model },
model_type: ModelTypeEnum.textEmbedding,
@ -27,7 +30,11 @@ describe('checkShowMultiModalTip', () => {
})
// Helper to create a model provider
const createModelProvider = (provider: string, models: ModelItem[]): Model => ({
const createModelProvider = (
provider: string,
models: ProviderModelWithStatusEntity[],
): ProviderWithModelsResponse => ({
tenant_id: 'test-workspace',
provider,
label: { en_US: provider, zh_Hans: provider },
icon_small: { en_US: '', zh_Hans: '' },
@ -223,7 +230,7 @@ describe('checkShowMultiModalTip', () => {
})
it('should handle model with undefined features', () => {
const modelItem: ModelItem = {
const modelItem: ProviderModelWithStatusEntity = {
model: 'test-model',
label: { en_US: 'test', zh_Hans: 'test' },
model_type: ModelTypeEnum.textEmbedding,
@ -243,7 +250,7 @@ describe('checkShowMultiModalTip', () => {
})
it('should handle model with null features', () => {
const modelItem: ModelItem = {
const modelItem: ProviderModelWithStatusEntity = {
model: 'text-embedding-ada-002',
label: { en_US: 'test', zh_Hans: 'test' },
model_type: ModelTypeEnum.textEmbedding,

View File

@ -1,7 +1,5 @@
import type {
DefaultModel,
Model,
} from '@/app/components/header/account-setting/model-provider-page/declarations'
import type { ProviderWithModelsResponse } from '@dify/contracts/api/console/workspaces/types.gen'
import type { DefaultModel } from '@/app/components/header/account-setting/model-provider-page/declarations'
import { ModelFeatureEnum } from '@/app/components/header/account-setting/model-provider-page/declarations'
import { IndexingType } from '../../create/step-two'
@ -13,8 +11,8 @@ type ShowMultiModalTipProps = {
rerankingModelName: string
}
indexMethod: IndexingType | undefined
embeddingModelList: Model[]
rerankModelList: Model[]
embeddingModelList: ProviderWithModelsResponse[]
rerankModelList: ProviderWithModelsResponse[]
}
export const checkShowMultiModalTip = ({

View File

@ -531,7 +531,6 @@ const BasicAppPreview: FC<Props> = ({ appId }) => {
const value = {
readonly: true,
appId,
isAPIKeySet: true,
isTrailFinished: false,
mode,
modelModeType: '',
@ -619,7 +618,7 @@ const BasicAppPreview: FC<Props> = ({ appId }) => {
>
<div className="flex grow flex-col rounded-tl-2xl border-t-[0.5px] border-l-[0.5px] border-components-panel-border bg-chatbot-bg">
<Debug
isAPIKeySet
isPreview
onSetting={noop}
inputs={inputs}
modelParameterParams={{

View File

@ -1,4 +1,8 @@
import type { Model, ModelItem, ModelProvider } from '../declarations'
import type {
ProviderModelWithStatusEntity,
ProviderWithModelsResponse,
} from '@dify/contracts/api/console/workspaces/types.gen'
import type { ModelProvider } from '../declarations'
import type { CredentialPanelState } from '../provider-added-card/use-credential-panel-state'
import { ConfigurationMethodEnum, ModelStatusEnum, ModelTypeEnum } from '../declarations'
import { deriveModelStatus } from '../derive-model-status'
@ -17,7 +21,9 @@ const createCredentialState = (
...overrides,
})
const createModelItem = (overrides: Partial<ModelItem> = {}): ModelItem => ({
const createModelItem = (
overrides: Partial<ProviderModelWithStatusEntity> = {},
): ProviderModelWithStatusEntity => ({
model: 'text-embedding-3-large',
label: { en_US: 'Text Embedding 3 Large', zh_Hans: 'Text Embedding 3 Large' },
model_type: ModelTypeEnum.textEmbedding,
@ -30,7 +36,10 @@ const createModelItem = (overrides: Partial<ModelItem> = {}): ModelItem => ({
const createModelProvider = (): ModelProvider => ({ provider: 'openai' }) as ModelProvider
const createModel = (overrides: Partial<Model> = {}): Model => ({
const createModel = (
overrides: Partial<ProviderWithModelsResponse> = {},
): ProviderWithModelsResponse => ({
tenant_id: 'test-workspace',
provider: 'openai',
icon_small: { en_US: '', zh_Hans: '' },
label: { en_US: 'OpenAI', zh_Hans: 'OpenAI' },

View File

@ -1,10 +1,10 @@
import type { ProviderWithModelsResponse } from '@dify/contracts/api/console/workspaces/types.gen'
import type { Mock } from 'vite-plus/test'
import type {
Credential,
CustomConfigurationModelFixedFields,
CustomModel,
DefaultModelResponse,
Model,
ModelProvider,
} from '../declarations'
import { act, renderHook } from '@testing-library/react'
@ -26,7 +26,6 @@ import {
useInvalidateDefaultModel,
useLanguage,
useMarketplaceAllPlugins,
useModelList,
useModelListAndDefaultModel,
useModelListAndDefaultModelAndCurrentProviderAndModel,
useModelModalHandler,
@ -51,7 +50,6 @@ vi.mock('@tanstack/react-query', () => ({
vi.mock('@/service/common', () => ({
fetchDefaultModal: vi.fn(),
fetchModelList: vi.fn(),
}))
vi.mock('@/service/use-common', () => ({
@ -133,8 +131,9 @@ describe('hooks', () => {
})
describe('useSystemDefaultModelAndModelList', () => {
const createMockModelList = (): Model[] => [
const createMockModelList = (): ProviderWithModelsResponse[] => [
{
tenant_id: 'test-workspace',
provider: 'openai',
icon_small: { en_US: 'icon', zh_Hans: 'icon' },
label: { en_US: 'OpenAI', zh_Hans: 'OpenAI' },
@ -254,106 +253,6 @@ describe('hooks', () => {
})
})
describe('useModelList', () => {
const mockModelData = [
{ provider: 'openai', models: [{ model: 'gpt-4' }] },
{ provider: 'anthropic', models: [{ model: 'claude-3' }] },
]
it('should use the generated model list key and expose the result', () => {
const refetch = vi.fn()
;(useQuery as Mock).mockReturnValue({
data: { data: mockModelData },
isPending: false,
refetch,
})
const { result } = renderHook(() => useModelList(ModelTypeEnum.textGeneration))
expect(result.current.data).toEqual(mockModelData)
expect(result.current.isLoading).toBe(false)
expect(useQuery).toHaveBeenCalledWith(
expect.objectContaining({
queryKey: getModelListQueryKey(ModelTypeEnum.textGeneration),
}),
)
})
it('should return empty array when data is undefined', () => {
;(useQuery as Mock).mockReturnValue({
data: undefined,
isPending: false,
refetch: vi.fn(),
})
const { result } = renderHook(() => useModelList(ModelTypeEnum.textGeneration))
expect(result.current.data).toEqual([])
})
it('should keep the query disabled when requested', () => {
;(useQuery as Mock).mockReturnValue({
data: undefined,
isPending: true,
refetch: vi.fn(),
})
renderHook(() => useModelList(ModelTypeEnum.textEmbedding, { enabled: false }))
expect(useQuery).toHaveBeenCalledWith(
expect.objectContaining({
enabled: false,
queryKey: getModelListQueryKey(ModelTypeEnum.textEmbedding),
}),
)
})
it('should handle loading state', () => {
;(useQuery as Mock).mockReturnValue({
data: undefined,
isPending: true,
refetch: vi.fn(),
})
const { result } = renderHook(() => useModelList(ModelTypeEnum.textGeneration))
expect(result.current.isLoading).toBe(true)
})
it('should call mutate to refetch data', () => {
const refetch = vi.fn()
;(useQuery as Mock).mockReturnValue({
data: { data: mockModelData },
isPending: false,
refetch,
})
const { result } = renderHook(() => useModelList(ModelTypeEnum.textGeneration))
act(() => {
result.current.mutate()
})
expect(refetch).toHaveBeenCalled()
})
it('should work with different model types', () => {
;(useQuery as Mock).mockReturnValue({
data: { data: [] },
isPending: false,
refetch: vi.fn(),
})
const { result: result1 } = renderHook(() => useModelList(ModelTypeEnum.textEmbedding))
const { result: result2 } = renderHook(() => useModelList(ModelTypeEnum.rerank))
const { result: result3 } = renderHook(() => useModelList(ModelTypeEnum.tts))
expect(result1.current.data).toEqual([])
expect(result2.current.data).toEqual([])
expect(result3.current.data).toEqual([])
})
})
describe('useDefaultModel', () => {
const mockDefaultModel = {
model: 'gpt-4',
@ -447,8 +346,9 @@ describe('hooks', () => {
})
describe('getCurrentProviderAndModel', () => {
const createModelList = (): Model[] => [
const createModelList = (): ProviderWithModelsResponse[] => [
{
tenant_id: 'test-workspace',
provider: 'openai',
icon_small: { en_US: 'icon', zh_Hans: 'icon' },
label: { en_US: 'OpenAI', zh_Hans: 'OpenAI' },
@ -526,8 +426,9 @@ describe('hooks', () => {
})
describe('useTextGenerationCurrentProviderAndModelAndModelList', () => {
const createModelList = (): Model[] => [
const createModelList = (): ProviderWithModelsResponse[] => [
{
tenant_id: 'test-workspace',
provider: 'openai',
icon_small: { en_US: 'icon', zh_Hans: 'icon' },
label: { en_US: 'OpenAI', zh_Hans: 'OpenAI' },
@ -545,6 +446,7 @@ describe('hooks', () => {
status: ModelStatusEnum.active,
},
{
tenant_id: 'test-workspace',
provider: 'anthropic',
icon_small: { en_US: 'icon', zh_Hans: 'icon' },
label: { en_US: 'Anthropic', zh_Hans: 'Anthropic' },
@ -554,19 +456,19 @@ describe('hooks', () => {
label: { en_US: 'Claude 3', zh_Hans: 'Claude 3' },
model_type: ModelTypeEnum.textGeneration,
fetch_from: ConfigurationMethodEnum.predefinedModel,
status: ModelStatusEnum.disabled,
status: 'no-configure',
model_properties: {},
load_balancing_enabled: false,
},
],
status: ModelStatusEnum.disabled,
status: 'no-configure',
},
]
it('should return all text generation model lists', () => {
const modelList = createModelList()
;(useQuery as Mock).mockReturnValue({
data: { data: modelList },
data: modelList,
isPending: false,
refetch: vi.fn(),
})
@ -584,7 +486,7 @@ describe('hooks', () => {
it('should filter active models correctly', () => {
const modelList = createModelList()
;(useQuery as Mock).mockReturnValue({
data: { data: modelList },
data: modelList,
isPending: false,
refetch: vi.fn(),
})
@ -598,7 +500,7 @@ describe('hooks', () => {
it('should find current provider and model', () => {
const modelList = createModelList()
;(useQuery as Mock).mockReturnValue({
data: { data: modelList },
data: modelList,
isPending: false,
refetch: vi.fn(),
})
@ -614,7 +516,7 @@ describe('hooks', () => {
it('should handle empty model list', () => {
;(useQuery as Mock).mockReturnValue({
data: { data: [] },
data: [],
isPending: false,
refetch: vi.fn(),
})
@ -631,7 +533,7 @@ describe('hooks', () => {
const mockModelData = [{ provider: 'openai', models: [] }]
const mockDefaultModel = { model: 'gpt-4', provider: { provider: 'openai' } }
;(useQuery as Mock)
.mockReturnValueOnce({ data: { data: mockModelData }, isPending: false, refetch: vi.fn() })
.mockReturnValueOnce({ data: mockModelData, isPending: false, refetch: vi.fn() })
.mockReturnValueOnce({
data: { data: mockDefaultModel },
isPending: false,
@ -660,6 +562,7 @@ describe('hooks', () => {
it('should return complete data structure', () => {
const mockModelData = [
{
tenant_id: 'test-workspace',
provider: 'openai',
icon_small: { en_US: 'icon', zh_Hans: 'icon' },
label: { en_US: 'OpenAI', zh_Hans: 'OpenAI' },
@ -683,7 +586,7 @@ describe('hooks', () => {
provider: { provider: 'openai', icon_small: { en_US: 'icon', zh_Hans: 'icon' } },
}
;(useQuery as Mock)
.mockReturnValueOnce({ data: { data: mockModelData }, isPending: false, refetch: vi.fn() })
.mockReturnValueOnce({ data: mockModelData, isPending: false, refetch: vi.fn() })
.mockReturnValueOnce({
data: { data: mockDefaultModel },
isPending: false,
@ -709,7 +612,7 @@ describe('hooks', () => {
},
]
;(useQuery as Mock)
.mockReturnValueOnce({ data: { data: mockModelData }, isPending: false, refetch: vi.fn() })
.mockReturnValueOnce({ data: mockModelData, isPending: false, refetch: vi.fn() })
.mockReturnValueOnce({ data: undefined, isPending: false, refetch: vi.fn() })
const { result } = renderHook(() =>

View File

@ -242,15 +242,6 @@ export type ModelProvider = {
allow_custom_token?: boolean
}
export type Model = {
provider: string
icon_small: TypeWithI18N
icon_small_dark?: TypeWithI18N
label: TypeWithI18N
models: ModelItem[]
status: ModelStatusEnum
}
export type DefaultModelResponse = {
model: string
model_type: ModelTypeEnum

View File

@ -1,4 +1,7 @@
import type { ModelType } from '@dify/contracts/api/console/workspaces/types.gen'
import type {
ModelType,
ProviderWithModelsResponse,
} from '@dify/contracts/api/console/workspaces/types.gen'
import type {
ConfigurationMethodEnum,
Credential,
@ -6,7 +9,6 @@ import type {
CustomModel,
DefaultModel,
DefaultModelResponse,
Model,
ModelModalModeEnum,
ModelProvider,
} from './declarations'
@ -20,7 +22,7 @@ import {
import { PluginCategoryEnum } from '@/app/components/plugins/types'
import { useLocale } from '@/context/i18n'
import { useModalContextSelector } from '@/context/modal-context'
import { fetchDefaultModal, fetchModelList } from '@/service/common'
import { fetchDefaultModal } from '@/service/common'
import { consoleQuery } from '@/service/console'
import { commonQueryKeys, modelProviderDetailsQueryOptions } from '@/service/use-common'
import { useExpandModelProviderList } from './atoms'
@ -28,7 +30,7 @@ import { CustomConfigurationStatusEnum, ModelStatusEnum, ModelTypeEnum } from '.
type UseDefaultModelAndModelList = (
defaultModel: DefaultModelResponse | undefined,
modelList: Model[],
modelList: ProviderWithModelsResponse[],
) => [DefaultModel | undefined, (model: DefaultModel) => void]
export const useSystemDefaultModelAndModelList: UseDefaultModelAndModelList = (
defaultModel,
@ -79,26 +81,6 @@ type ModelQueryOptions = {
enabled?: boolean
}
export const useModelList = (type: ModelTypeEnum, { enabled = true }: ModelQueryOptions = {}) => {
const { data, refetch, isPending } = useQuery({
queryKey: consoleQuery.workspaces.current.models.modelTypes.byModelType.get.queryKey({
input: {
params: {
model_type: type,
},
},
}),
queryFn: () => fetchModelList(`/workspaces/current/models/model-types/${type}`),
enabled,
})
return {
data: data?.data || [],
mutate: refetch,
isLoading: isPending,
}
}
export const useDefaultModel = (
type: ModelTypeEnum,
{ enabled = true }: ModelQueryOptions = {},
@ -116,10 +98,6 @@ export const useDefaultModel = (
}
}
type ModelFromProvider<TProvider> = TProvider extends { models: Array<infer TModel> }
? TModel
: never
export const getCurrentProviderAndModel = <
TProvider extends { models: Array<{ model: string }>; provider: string },
>(
@ -127,9 +105,9 @@ export const getCurrentProviderAndModel = <
defaultModel?: DefaultModel,
) => {
const currentProvider = modelList.find((provider) => provider.provider === defaultModel?.provider)
const currentModel = currentProvider?.models.find(
const currentModel: TProvider['models'][number] | undefined = currentProvider?.models.find(
(model) => model.model === defaultModel?.model,
) as ModelFromProvider<TProvider> | undefined
)
return {
currentProvider,
@ -142,7 +120,12 @@ export { getCurrentProviderAndModel as useCurrentProviderAndModel }
export const useTextGenerationCurrentProviderAndModelAndModelList = (
defaultModel?: DefaultModel,
) => {
const { data: textGenerationModelList } = useModelList(ModelTypeEnum.textGeneration)
const { data: textGenerationModelList = [] } = useQuery(
consoleQuery.workspaces.current.models.modelTypes.byModelType.get.queryOptions({
input: { params: { model_type: ModelTypeEnum.textGeneration } },
select: (response) => response.data,
}),
)
const activeTextGenerationModelList = textGenerationModelList.filter(
(model) => model.status === ModelStatusEnum.active,
)
@ -160,7 +143,12 @@ export const useTextGenerationCurrentProviderAndModelAndModelList = (
}
export const useModelListAndDefaultModel = (type: ModelTypeEnum) => {
const { data: modelList } = useModelList(type)
const { data: modelList = [] } = useQuery(
consoleQuery.workspaces.current.models.modelTypes.byModelType.get.queryOptions({
input: { params: { model_type: type } },
select: (response) => response.data,
}),
)
const { data: defaultModel } = useDefaultModel(type)
return {

View File

@ -1,6 +1,9 @@
import type { ModelProviderSummaryResponse } from '@dify/contracts/api/console/workspaces/types.gen'
import type {
ModelProviderSummaryResponse,
ProviderWithModelsResponse,
} from '@dify/contracts/api/console/workspaces/types.gen'
import type { FC } from 'react'
import type { Model, ModelProvider } from '../declarations'
import type { ModelProvider } from '../declarations'
import type { ModelSelectorProvider } from '../model-selector/types'
import { cn } from '@langgenius/dify-ui/cn'
import { OpenaiYellow } from '@/app/components/base/icons/src/public/llm'
@ -10,7 +13,11 @@ import { Theme } from '@/types/app'
import { useLanguage } from '../hooks'
type ModelIconProps = {
provider?: Model | ModelProvider | ModelProviderSummaryResponse | ModelSelectorProvider
provider?:
| ProviderWithModelsResponse
| ModelProvider
| ModelProviderSummaryResponse
| ModelSelectorProvider
modelName?: string
className?: string
iconClassName?: string

View File

@ -1,6 +1,9 @@
import type { ModelProviderSummaryResponse } from '@dify/contracts/api/console/workspaces/types.gen'
import type {
ModelProviderSummaryResponse,
ProviderModelWithStatusEntity,
ProviderWithModelsResponse,
} from '@dify/contracts/api/console/workspaces/types.gen'
import type { ReactNode } from 'react'
import type { Model, ModelItem } from '../../declarations'
import { QueryClientProvider } from '@tanstack/react-query'
import { fireEvent, render, screen } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
@ -9,7 +12,9 @@ import { createConsoleQueryClient } from '@/test/console/query-data'
import { ConfigurationMethodEnum, ModelStatusEnum, ModelTypeEnum } from '../../declarations'
import { ModelSelector, SplitModelSelector } from '../index'
const makeModelItem = (overrides: Partial<ModelItem> = {}): ModelItem => ({
const makeModelItem = (
overrides: Partial<ProviderModelWithStatusEntity> = {},
): ProviderModelWithStatusEntity => ({
model: 'gpt-4',
label: { en_US: 'GPT-4', zh_Hans: 'GPT-4' },
model_type: ModelTypeEnum.textGeneration,
@ -20,7 +25,7 @@ const makeModelItem = (overrides: Partial<ModelItem> = {}): ModelItem => ({
...overrides,
})
const mockModelProviders = vi.hoisted(() => ({ current: [] as Model[] }))
const mockModelProviders = vi.hoisted(() => ({ current: [] as ProviderWithModelsResponse[] }))
const mockSetSettingsDestination = vi.hoisted(() => vi.fn())
vi.mock('nuqs', async (importOriginal) => {
@ -58,7 +63,7 @@ vi.mock('../popup', () => {
onConfigureEmptyState?: () => void
onHide: () => void
onOpenProviderSettings?: () => void
onSelect: (provider: string, model: ModelItem) => void
onSelect: (provider: string, model: ProviderModelWithStatusEntity) => void
}) => (
<>
<button type="button" onClick={() => onSelect('openai', makeModelItem())}>
@ -82,7 +87,10 @@ vi.mock('../popup', () => {
}
})
const makeModel = (overrides: Partial<Model> = {}): Model => ({
const makeModel = (
overrides: Partial<ProviderWithModelsResponse> = {},
): ProviderWithModelsResponse => ({
tenant_id: 'test-workspace',
provider: 'openai',
icon_small: { en_US: '', zh_Hans: '' },
label: { en_US: 'OpenAI', zh_Hans: 'OpenAI' },

View File

@ -1,5 +1,8 @@
import type {
ProviderModelWithStatusEntity,
ProviderWithModelsResponse,
} from '@dify/contracts/api/console/workspaces/types.gen'
import type { ReactNode } from 'react'
import type { Model, ModelItem } from '../../declarations'
import { Popover } from '@langgenius/dify-ui/popover'
import { render as renderComponent, screen } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
@ -23,7 +26,9 @@ vi.mock('../../provider-added-card/use-credential-panel-state', () => ({
useCredentialPanelState: mockUseCredentialPanelState,
}))
const createModelItem = (overrides: Partial<ModelItem> = {}): ModelItem => ({
const createModelItem = (
overrides: Partial<ProviderModelWithStatusEntity> = {},
): ProviderModelWithStatusEntity => ({
model: 'gpt-4',
label: { en_US: 'GPT-4', zh_Hans: 'GPT-4' },
model_type: ModelTypeEnum.textGeneration,
@ -35,7 +40,10 @@ const createModelItem = (overrides: Partial<ModelItem> = {}): ModelItem => ({
...overrides,
})
const createModel = (overrides: Partial<Model> = {}): Model => ({
const createModel = (
overrides: Partial<ProviderWithModelsResponse> = {},
): ProviderWithModelsResponse => ({
tenant_id: 'test-workspace',
provider: 'openai',
icon_small: {
en_US: 'https://example.com/openai-light.png',

View File

@ -1,5 +1,9 @@
import type {
ProviderModelWithStatusEntity,
ProviderWithModelsResponse,
} from '@dify/contracts/api/console/workspaces/types.gen'
import type { ReactElement, ReactNode } from 'react'
import type { DefaultModel, Model, ModelItem } from '../../declarations'
import type { DefaultModel } from '../../declarations'
import type { ModelSelectorPreviewPayload } from '../popup-item'
import { createPreviewCardHandle } from '@langgenius/dify-ui/preview-card'
import { fireEvent, screen, waitFor } from '@testing-library/react'
@ -41,7 +45,7 @@ vi.mock('../../model-name', () => ({
nameClassName,
children,
}: {
modelItem: ModelItem
modelItem: ProviderModelWithStatusEntity
className?: string
nameClassName?: string
children?: ReactNode
@ -107,7 +111,9 @@ vi.mock('@/context/permission-state', async () => {
}))
})
const makeModelItem = (overrides: Partial<ModelItem> = {}): ModelItem => ({
const makeModelItem = (
overrides: Partial<ProviderModelWithStatusEntity> = {},
): ProviderModelWithStatusEntity => ({
model: 'gpt-4',
label: { en_US: 'GPT-4', zh_Hans: 'GPT-4' },
model_type: ModelTypeEnum.textGeneration,
@ -119,7 +125,10 @@ const makeModelItem = (overrides: Partial<ModelItem> = {}): ModelItem => ({
...overrides,
})
const makeModel = (overrides: Partial<Model> = {}): Model => ({
const makeModel = (
overrides: Partial<ProviderWithModelsResponse> = {},
): ProviderWithModelsResponse => ({
tenant_id: 'test-workspace',
provider: 'openai',
icon_small: { en_US: '', zh_Hans: '' },
label: { en_US: 'OpenAI', zh_Hans: 'OpenAI' },
@ -311,7 +320,7 @@ describe('PopupItem', () => {
it('should open model modal when clicking add on unconfigured model', async () => {
const onSelect = vi.fn()
const { rerender } = renderPopupItem(
renderPopupItem(
<PopupItem
{...previewCardProps()}
model={makeModel({ models: [makeModelItem({ status: ModelStatusEnum.noConfigure })] })}
@ -335,35 +344,6 @@ describe('PopupItem', () => {
expect(mockUpdateModelProviders).toHaveBeenCalled()
expect(mockUpdateModelList).toHaveBeenCalledWith(ModelTypeEnum.textGeneration)
rerender(
createPopupItemNode(
<PopupItem
{...previewCardProps()}
model={makeModel({
models: [
makeModelItem({
status: ModelStatusEnum.noConfigure,
model_type: undefined as unknown as ModelTypeEnum,
}),
],
})}
onHide={vi.fn()}
/>,
),
)
fireEvent.click(screen.getByText('COMMON.OPERATION.ADD'))
await waitFor(() => {
expect(mockSetShowModelModal).toHaveBeenCalledTimes(2)
})
const call2 = mockSetShowModelModal.mock.calls.at(-1)?.[0] as
| { onSaveCallback?: () => void }
| undefined
call2?.onSaveCallback?.()
expect(mockUpdateModelProviders).toHaveBeenCalled()
expect(mockUpdateModelList).toHaveBeenCalledTimes(1)
})
it('should show selected state when defaultModel matches', () => {
@ -387,8 +367,8 @@ describe('PopupItem', () => {
<PopupItem
{...previewCardProps()}
model={makeModel({
label: { en_US: 'OpenAI only' } as Model['label'],
models: [makeModelItem({ label: { en_US: 'GPT-4 only' } as ModelItem['label'] })],
label: { en_US: 'OpenAI only' },
models: [makeModelItem({ label: { en_US: 'GPT-4 only' } })],
})}
onHide={vi.fn()}
/>,

View File

@ -1,6 +1,9 @@
import type { ModelProviderSummaryResponse } from '@dify/contracts/api/console/workspaces/types.gen'
import type {
ModelProviderSummaryResponse,
ProviderModelWithStatusEntity,
ProviderWithModelsResponse,
} from '@dify/contracts/api/console/workspaces/types.gen'
import type { ReactElement } from 'react'
import type { Model, ModelItem } from '../../declarations'
import type { PopupProps } from '../popup'
import { Popover, PopoverContent, PopoverTitle, PopoverTrigger } from '@langgenius/dify-ui/popover'
import { fireEvent, screen, waitFor } from '@testing-library/react'
@ -57,7 +60,7 @@ vi.mock('../../hooks', async () => {
})
vi.mock('../popup-item', () => ({
default: ({ model }: { model: Model }) => (
default: ({ model }: { model: ProviderWithModelsResponse }) => (
<div>
<span>{model.provider}</span>
{model.models.map((modelItem) => (
@ -199,7 +202,9 @@ vi.mock('../../utils', async () => {
}
})
const makeModelItem = (overrides: Partial<ModelItem> = {}): ModelItem => ({
const makeModelItem = (
overrides: Partial<ProviderModelWithStatusEntity> = {},
): ProviderModelWithStatusEntity => ({
model: 'gpt-4',
label: { en_US: 'GPT-4', zh_Hans: 'GPT-4' },
model_type: ModelTypeEnum.textGeneration,
@ -210,7 +215,10 @@ const makeModelItem = (overrides: Partial<ModelItem> = {}): ModelItem => ({
...overrides,
})
const makeModel = (overrides: Partial<Model> = {}): Model => ({
const makeModel = (
overrides: Partial<ProviderWithModelsResponse> = {},
): ProviderWithModelsResponse => ({
tenant_id: 'test-workspace',
provider: 'openai',
icon_small: { en_US: '', zh_Hans: '' },
label: { en_US: 'OpenAI', zh_Hans: 'OpenAI' },
@ -523,7 +531,10 @@ describe('Popup', () => {
models: [
makeModelItem({
model: 'openrouter-model',
label: { en_US: 'OpenRouter Model', zh_Hans: 'OpenRouter Model' },
label: {
en_US: 'OpenRouter Model',
zh_Hans: 'OpenRouter Model',
},
}),
],
}),
@ -533,7 +544,10 @@ describe('Popup', () => {
models: [
makeModelItem({
model: 'compatible-model',
label: { en_US: 'Compatible Model', zh_Hans: 'Compatible Model' },
label: {
en_US: 'Compatible Model',
zh_Hans: 'Compatible Model',
},
}),
],
}),

View File

@ -2,6 +2,7 @@ import type { DefaultModelResponse } from '../../declarations'
import { fireEvent, screen, waitFor } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import { vi } from 'vite-plus/test'
import { consoleQuery } from '@/service/console'
import { renderWithNuqs as render } from '@/test/nuqs-testing'
import { ModelTypeEnum } from '../../declarations'
import SystemModel from '../index'
@ -33,7 +34,7 @@ const mockToastSuccess = vi.hoisted(() => vi.fn())
const mockUpdateModelList = vi.hoisted(() => vi.fn())
const mockInvalidateDefaultModel = vi.hoisted(() => vi.fn())
const mockUpdateDefaultModel = vi.hoisted(() => vi.fn(() => Promise.resolve({ result: 'success' })))
const mockUseModelList = vi.hoisted(() => vi.fn())
const mockModelListQuery = vi.hoisted(() => vi.fn())
const mockModelSelectorProps = vi.hoisted(
() =>
[] as Array<{
@ -52,12 +53,6 @@ vi.mock('@/context/permission-state', async () => {
}))
})
vi.mock('@/context/provider-context', () => ({
useProviderContext: () => ({
textGenerationModelList: [],
}),
}))
vi.mock('@langgenius/dify-ui/toast', async (importOriginal) => {
const actual = await importOriginal<typeof import('@langgenius/dify-ui/toast')>()
return {
@ -70,7 +65,6 @@ vi.mock('@langgenius/dify-ui/toast', async (importOriginal) => {
})
vi.mock('../../hooks', () => ({
useModelList: mockUseModelList,
useSystemDefaultModelAndModelList: (defaultModel: DefaultModelResponse | undefined) => [
defaultModel || {
model: '',
@ -123,13 +117,13 @@ const defaultProps = {
speech2textDefaultModel: undefined,
ttsDefaultModel: undefined,
notConfigured: false,
isLoading: false,
isPending: false,
}
describe('SystemModel', () => {
beforeEach(() => {
vi.clearAllMocks()
mockUseModelList.mockReturnValue({ data: [], isLoading: false })
mockModelListQuery.mockReturnValue({ data: [], isPending: false })
mockModelSelectorProps.length = 0
mockWorkspacePermissionKeys = ['plugin.model_config']
})
@ -152,7 +146,14 @@ describe('SystemModel', () => {
render(<SystemModel {...defaultProps} />, { searchParams: '?dialog=system-models' })
expect(await screen.findByRole('button', { name: /save/i })).toBeInTheDocument()
expect(mockUseModelList).toHaveBeenCalledWith(ModelTypeEnum.textEmbedding, { enabled: true })
expect(mockModelListQuery).toHaveBeenCalledWith(
expect.objectContaining({
queryKey: consoleQuery.workspaces.current.models.modelTypes.byModelType.get.queryKey({
input: { params: { model_type: ModelTypeEnum.textEmbedding } },
}),
enabled: true,
}),
)
})
it('clears only the dialog URL state when closed', async () => {
@ -172,24 +173,80 @@ describe('SystemModel', () => {
const user = userEvent.setup()
render(<SystemModel {...defaultProps} />)
expect(mockUseModelList).toHaveBeenCalledWith(ModelTypeEnum.textEmbedding, { enabled: false })
expect(mockUseModelList).toHaveBeenCalledWith(ModelTypeEnum.rerank, { enabled: false })
expect(mockUseModelList).toHaveBeenCalledWith(ModelTypeEnum.speech2text, { enabled: false })
expect(mockUseModelList).toHaveBeenCalledWith(ModelTypeEnum.tts, { enabled: false })
expect(mockModelListQuery).toHaveBeenCalledWith(
expect.objectContaining({
queryKey: consoleQuery.workspaces.current.models.modelTypes.byModelType.get.queryKey({
input: { params: { model_type: ModelTypeEnum.textEmbedding } },
}),
enabled: false,
}),
)
expect(mockModelListQuery).toHaveBeenCalledWith(
expect.objectContaining({
queryKey: consoleQuery.workspaces.current.models.modelTypes.byModelType.get.queryKey({
input: { params: { model_type: ModelTypeEnum.rerank } },
}),
enabled: false,
}),
)
expect(mockModelListQuery).toHaveBeenCalledWith(
expect.objectContaining({
queryKey: consoleQuery.workspaces.current.models.modelTypes.byModelType.get.queryKey({
input: { params: { model_type: ModelTypeEnum.speech2text } },
}),
enabled: false,
}),
)
expect(mockModelListQuery).toHaveBeenCalledWith(
expect.objectContaining({
queryKey: consoleQuery.workspaces.current.models.modelTypes.byModelType.get.queryKey({
input: { params: { model_type: ModelTypeEnum.tts } },
}),
enabled: false,
}),
)
await user.click(screen.getByRole('button', { name: /system model settings/i }))
await waitFor(() => {
expect(mockUseModelList).toHaveBeenCalledWith(ModelTypeEnum.textEmbedding, { enabled: true })
expect(mockUseModelList).toHaveBeenCalledWith(ModelTypeEnum.rerank, { enabled: true })
expect(mockUseModelList).toHaveBeenCalledWith(ModelTypeEnum.speech2text, { enabled: true })
expect(mockUseModelList).toHaveBeenCalledWith(ModelTypeEnum.tts, { enabled: true })
expect(mockModelListQuery).toHaveBeenCalledWith(
expect.objectContaining({
queryKey: consoleQuery.workspaces.current.models.modelTypes.byModelType.get.queryKey({
input: { params: { model_type: ModelTypeEnum.textEmbedding } },
}),
enabled: true,
}),
)
expect(mockModelListQuery).toHaveBeenCalledWith(
expect.objectContaining({
queryKey: consoleQuery.workspaces.current.models.modelTypes.byModelType.get.queryKey({
input: { params: { model_type: ModelTypeEnum.rerank } },
}),
enabled: true,
}),
)
expect(mockModelListQuery).toHaveBeenCalledWith(
expect.objectContaining({
queryKey: consoleQuery.workspaces.current.models.modelTypes.byModelType.get.queryKey({
input: { params: { model_type: ModelTypeEnum.speech2text } },
}),
enabled: true,
}),
)
expect(mockModelListQuery).toHaveBeenCalledWith(
expect.objectContaining({
queryKey: consoleQuery.workspaces.current.models.modelTypes.byModelType.get.queryKey({
input: { params: { model_type: ModelTypeEnum.tts } },
}),
enabled: true,
}),
)
})
})
it('shows loading instead of empty model selectors while model lists load', async () => {
const user = userEvent.setup()
mockUseModelList.mockReturnValue({ data: [], isLoading: true })
mockModelListQuery.mockReturnValue({ data: [], isPending: true })
render(<SystemModel {...defaultProps} />)
await user.click(screen.getByRole('button', { name: /system model settings/i }))
@ -312,3 +369,8 @@ describe('SystemModel', () => {
}
})
})
vi.mock('@tanstack/react-query', async (importOriginal) => {
const actual = await importOriginal<typeof import('@tanstack/react-query')>()
return { ...actual, useQuery: mockModelListQuery }
})

View File

@ -5,19 +5,19 @@ import { cn } from '@langgenius/dify-ui/cn'
import { Dialog, DialogClose, DialogContent, DialogTitle } from '@langgenius/dify-ui/dialog'
import { IconButton } from '@langgenius/dify-ui/icon-button'
import { toast } from '@langgenius/dify-ui/toast'
import { useQuery } from '@tanstack/react-query'
import { useAtomValue } from 'jotai'
import { parseAsStringLiteral, useQueryState } from 'nuqs'
import { useState } from 'react'
import { useTranslation } from 'react-i18next'
import { Infotip } from '@/app/components/base/infotip'
import { workspacePermissionKeysAtom } from '@/context/permission-state'
import { useProviderContext } from '@/context/provider-context'
import { updateDefaultModel } from '@/service/common'
import { consoleQuery } from '@/service/console'
import { hasPermission } from '@/utils/permission'
import { ModelTypeEnum } from '../declarations'
import {
useInvalidateDefaultModel,
useModelList,
useSystemDefaultModelAndModelList,
useUpdateModelList,
} from '../hooks'
@ -66,7 +66,12 @@ const SystemModel: FC<SystemModelSelectorProps> = ({
}) => {
const { t } = useTranslation()
const workspacePermissionKeys = useAtomValue(workspacePermissionKeysAtom)
const { textGenerationModelList } = useProviderContext()
const { data: textGenerationModelList = [] } = useQuery(
consoleQuery.workspaces.current.models.modelTypes.byModelType.get.queryOptions({
input: { params: { model_type: ModelTypeEnum.textGeneration } },
select: (response) => response.data,
}),
)
const canManageSystemDefaultModel = hasPermission(workspacePermissionKeys, 'plugin.model_config')
const updateModelList = useUpdateModelList()
const invalidateDefaultModel = useInvalidateDefaultModel()
@ -77,21 +82,34 @@ const SystemModel: FC<SystemModelSelectorProps> = ({
setManuallyOpen(nextOpen)
if (!nextOpen && activeDialog === 'system-models') void setActiveDialog(null)
}
const { data: embeddingModelList, isLoading: isEmbeddingModelListLoading } = useModelList(
ModelTypeEnum.textEmbedding,
{ enabled: open },
const { data: embeddingModelList = [], isPending: isEmbeddingModelListLoading } = useQuery(
consoleQuery.workspaces.current.models.modelTypes.byModelType.get.queryOptions({
input: { params: { model_type: ModelTypeEnum.textEmbedding } },
select: (response) => response.data,
enabled: open,
}),
)
const { data: rerankModelList, isLoading: isRerankModelListLoading } = useModelList(
ModelTypeEnum.rerank,
{ enabled: open },
const { data: rerankModelList = [], isPending: isRerankModelListLoading } = useQuery(
consoleQuery.workspaces.current.models.modelTypes.byModelType.get.queryOptions({
input: { params: { model_type: ModelTypeEnum.rerank } },
select: (response) => response.data,
enabled: open,
}),
)
const { data: speech2textModelList, isLoading: isSpeech2textModelListLoading } = useModelList(
ModelTypeEnum.speech2text,
{ enabled: open },
const { data: speech2textModelList = [], isPending: isSpeech2textModelListLoading } = useQuery(
consoleQuery.workspaces.current.models.modelTypes.byModelType.get.queryOptions({
input: { params: { model_type: ModelTypeEnum.speech2text } },
select: (response) => response.data,
enabled: open,
}),
)
const { data: ttsModelList = [], isPending: isTTSModelListLoading } = useQuery(
consoleQuery.workspaces.current.models.modelTypes.byModelType.get.queryOptions({
input: { params: { model_type: ModelTypeEnum.tts } },
select: (response) => response.data,
enabled: open,
}),
)
const { data: ttsModelList, isLoading: isTTSModelListLoading } = useModelList(ModelTypeEnum.tts, {
enabled: open,
})
const [changedModelTypes, setChangedModelTypes] = useState<ModelTypeEnum[]>([])
const [currentTextGenerationDefaultModel, changeCurrentTextGenerationDefaultModel] =
useSystemDefaultModelAndModelList(textGenerationDefaultModel, textGenerationModelList)

View File

@ -1,3 +1,5 @@
import type { GetWorkspacesCurrentModelsModelTypesByModelTypeData } from '@dify/contracts/api/console/workspaces/types.gen'
import type { OperationKey } from '@orpc/tanstack-query'
import { renderHook } from '@testing-library/react'
import { beforeEach, describe, expect, it, vi } from 'vite-plus/test'
import { PluginCategoryEnum } from '../../../types'
@ -36,16 +38,6 @@ vi.mock('@/app/components/header/account-setting/model-provider-page/declaration
}))
vi.mock('@/app/components/header/account-setting/model-provider-page/hooks', () => ({
useModelList: (type: string) => {
const map: Record<string, { mutate: ReturnType<typeof vi.fn> }> = {
llm: { mutate: mockRefetchLLMModelList },
'text-embedding': { mutate: mockRefetchEmbeddingModelList },
rerank: { mutate: mockRefetchRerankModelList },
speech2text: { mutate: mockRefetchSpeech2textModelList },
tts: { mutate: mockRefetchTTSModelList },
}
return map[type] ?? { mutate: vi.fn() }
},
useInvalidateDefaultModel: () => mockInvalidateDefaultModel,
}))
@ -194,3 +186,29 @@ describe('useRefreshPluginList', () => {
expect(mockInvalidateStrategyProviders).not.toHaveBeenCalled()
})
})
vi.mock('@tanstack/react-query', async (importOriginal) => {
const actual = await importOriginal<typeof import('@tanstack/react-query')>()
return {
...actual,
useQuery: (options: {
queryKey: OperationKey<
'query',
{ params: GetWorkspacesCurrentModelsModelTypesByModelTypeData['path'] }
>
}) => {
if (!options.queryKey[0].includes('modelTypes')) return actual.useQuery(options)
const type = options.queryKey[1].input?.params?.model_type
if (!type) throw new Error('Missing model type in query')
const map: Record<string, { refetch: ReturnType<typeof vi.fn> }> = {
llm: { refetch: mockRefetchLLMModelList },
'text-embedding': { refetch: mockRefetchEmbeddingModelList },
rerank: { refetch: mockRefetchRerankModelList },
speech2text: { refetch: mockRefetchSpeech2textModelList },
tts: { refetch: mockRefetchTTSModelList },
}
return map[type] ?? { refetch: vi.fn() }
},
}
})

View File

@ -1,10 +1,9 @@
import type { Plugin, PluginDeclaration, PluginManifestInMarket } from '../../types'
import { useQuery } from '@tanstack/react-query'
import { ModelTypeEnum } from '@/app/components/header/account-setting/model-provider-page/declarations'
import {
useInvalidateDefaultModel,
useModelList,
} from '@/app/components/header/account-setting/model-provider-page/hooks'
import { useInvalidateDefaultModel } from '@/app/components/header/account-setting/model-provider-page/hooks'
import { useProviderContext } from '@/context/provider-context'
import { consoleQuery } from '@/service/console'
import { useInvalidDataSourceListAuth } from '@/service/use-datasource'
import { useInvalidDataSourceList } from '@/service/use-pipeline'
import {
@ -35,11 +34,31 @@ const SYSTEM_MODEL_TYPES = [
const useRefreshPluginList = () => {
const invalidateInstalledPluginList = useInvalidateInstalledPluginList()
const invalidateCheckInstalled = useInvalidateCheckInstalled()
const { mutate: refetchLLMModelList } = useModelList(ModelTypeEnum.textGeneration)
const { mutate: refetchEmbeddingModelList } = useModelList(ModelTypeEnum.textEmbedding)
const { mutate: refetchRerankModelList } = useModelList(ModelTypeEnum.rerank)
const { mutate: refetchSpeech2textModelList } = useModelList(ModelTypeEnum.speech2text)
const { mutate: refetchTTSModelList } = useModelList(ModelTypeEnum.tts)
const { refetch: refetchLLMModelList } = useQuery(
consoleQuery.workspaces.current.models.modelTypes.byModelType.get.queryOptions({
input: { params: { model_type: ModelTypeEnum.textGeneration } },
}),
)
const { refetch: refetchEmbeddingModelList } = useQuery(
consoleQuery.workspaces.current.models.modelTypes.byModelType.get.queryOptions({
input: { params: { model_type: ModelTypeEnum.textEmbedding } },
}),
)
const { refetch: refetchRerankModelList } = useQuery(
consoleQuery.workspaces.current.models.modelTypes.byModelType.get.queryOptions({
input: { params: { model_type: ModelTypeEnum.rerank } },
}),
)
const { refetch: refetchSpeech2textModelList } = useQuery(
consoleQuery.workspaces.current.models.modelTypes.byModelType.get.queryOptions({
input: { params: { model_type: ModelTypeEnum.speech2text } },
}),
)
const { refetch: refetchTTSModelList } = useQuery(
consoleQuery.workspaces.current.models.modelTypes.byModelType.get.queryOptions({
input: { params: { model_type: ModelTypeEnum.tts } },
}),
)
const invalidateDefaultModel = useInvalidateDefaultModel()
const { refreshModelProviders } = useProviderContext()

View File

@ -1,7 +1,9 @@
import type {
Model,
ModelItem,
} from '@/app/components/header/account-setting/model-provider-page/declarations'
GetWorkspacesCurrentModelsModelTypesByModelTypeData,
ProviderModelWithStatusEntity,
ProviderWithModelsResponse,
} from '@dify/contracts/api/console/workspaces/types.gen'
import type { OperationKey } from '@orpc/tanstack-query'
import { fireEvent, render, screen, waitFor } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import { beforeEach, describe, expect, it, vi } from 'vite-plus/test'
@ -42,33 +44,12 @@ vi.mock('@/context/provider-context', () => ({
}))
// Mock model list hook
const mockTextGenerationList: Model[] = []
const mockTextEmbeddingList: Model[] = []
const mockRerankList: Model[] = []
const mockModerationList: Model[] = []
const mockSttList: Model[] = []
const mockTtsList: Model[] = []
vi.mock('@/app/components/header/account-setting/model-provider-page/hooks', () => ({
useModelList: (type: ModelTypeEnum) => {
switch (type) {
case ModelTypeEnum.textGeneration:
return { data: mockTextGenerationList }
case ModelTypeEnum.textEmbedding:
return { data: mockTextEmbeddingList }
case ModelTypeEnum.rerank:
return { data: mockRerankList }
case ModelTypeEnum.moderation:
return { data: mockModerationList }
case ModelTypeEnum.speech2text:
return { data: mockSttList }
case ModelTypeEnum.tts:
return { data: mockTtsList }
default:
return { data: [] }
}
},
}))
const mockTextGenerationList: ProviderWithModelsResponse[] = []
const mockTextEmbeddingList: ProviderWithModelsResponse[] = []
const mockRerankList: ProviderWithModelsResponse[] = []
const mockModerationList: ProviderWithModelsResponse[] = []
const mockSttList: ProviderWithModelsResponse[] = []
const mockTtsList: ProviderWithModelsResponse[] = []
// Mock fetchAndMergeValidCompletionParams
const mockFetchAndMergeValidCompletionParams = vi.fn()
@ -88,7 +69,7 @@ vi.mock('@/app/components/header/account-setting/model-provider-page/model-selec
onValueChange,
}: {
value?: { provider?: string; model?: string }
models?: Model[]
models?: ProviderWithModelsResponse[]
scopeFeatures?: string[]
surface?: 'default' | 'workflow'
disabled?: boolean
@ -163,7 +144,7 @@ vi.mock('../tts-params-panel', () => ({
voice,
onChange,
}: {
currentModel?: ModelItem
currentModel?: ProviderModelWithStatusEntity
language?: string
voice?: string
onChange?: (language: string, voice: string) => void
@ -183,9 +164,11 @@ vi.mock('../tts-params-panel', () => ({
// ==================== Test Utilities ====================
/**
* Factory function to create a ModelItem with defaults
* Factory function to create a ProviderModelWithStatusEntity with defaults
*/
const createModelItem = (overrides: Partial<ModelItem> = {}): ModelItem => ({
const createModelItem = (
overrides: Partial<ProviderModelWithStatusEntity> = {},
): ProviderModelWithStatusEntity => ({
model: 'test-model',
label: { en_US: 'Test Model', zh_Hans: 'Test Model' },
model_type: ModelTypeEnum.textGeneration,
@ -198,9 +181,12 @@ const createModelItem = (overrides: Partial<ModelItem> = {}): ModelItem => ({
})
/**
* Factory function to create a Model (provider with models) with defaults
* Factory function to create a ProviderWithModelsResponse (provider with models) with defaults
*/
const createModel = (overrides: Partial<Model> = {}): Model => ({
const createModel = (
overrides: Partial<ProviderWithModelsResponse> = {},
): ProviderWithModelsResponse => ({
tenant_id: 'test-workspace',
provider: 'openai',
icon_small: { en_US: 'icon-small.png', zh_Hans: 'icon-small.png' },
label: { en_US: 'OpenAI', zh_Hans: 'OpenAI' },
@ -226,12 +212,12 @@ const createDefaultProps = (
*/
const setupModelLists = (
config: {
textGeneration?: Model[]
textEmbedding?: Model[]
rerank?: Model[]
moderation?: Model[]
stt?: Model[]
tts?: Model[]
textGeneration?: ProviderWithModelsResponse[]
textEmbedding?: ProviderWithModelsResponse[]
rerank?: ProviderWithModelsResponse[]
moderation?: ProviderWithModelsResponse[]
stt?: ProviderWithModelsResponse[]
tts?: ProviderWithModelsResponse[]
} = {},
) => {
mockTextGenerationList.length = 0
@ -1374,3 +1360,37 @@ describe('ModelParameterModal', () => {
})
})
})
vi.mock('@tanstack/react-query', async (importOriginal) => {
const actual = await importOriginal<typeof import('@tanstack/react-query')>()
return {
...actual,
useQuery: (options: {
queryKey: OperationKey<
'query',
{ params: GetWorkspacesCurrentModelsModelTypesByModelTypeData['path'] }
>
}) => {
if (!options.queryKey[0].includes('modelTypes')) return actual.useQuery(options)
const type = options.queryKey[1].input?.params?.model_type
if (!type) throw new Error('Missing model type in query')
switch (type) {
case ModelTypeEnum.textGeneration:
return { data: mockTextGenerationList }
case ModelTypeEnum.textEmbedding:
return { data: mockTextEmbeddingList }
case ModelTypeEnum.rerank:
return { data: mockRerankList }
case ModelTypeEnum.moderation:
return { data: mockModerationList }
case ModelTypeEnum.speech2text:
return { data: mockSttList }
case ModelTypeEnum.tts:
return { data: mockTtsList }
default:
return { data: [] }
}
},
}
})

View File

@ -7,15 +7,16 @@ import type { ModelSelectorValue } from '@/app/components/header/account-setting
import { cn } from '@langgenius/dify-ui/cn'
import { Popover, PopoverContent } from '@langgenius/dify-ui/popover'
import { toast } from '@langgenius/dify-ui/toast'
import { useQuery } from '@tanstack/react-query'
import { useMemo, useState } from 'react'
import { useTranslation } from 'react-i18next'
import {
ModelStatusEnum,
ModelTypeEnum,
} from '@/app/components/header/account-setting/model-provider-page/declarations'
import { useModelList } from '@/app/components/header/account-setting/model-provider-page/hooks'
import { ModelSettingsTrigger } from '@/app/components/header/account-setting/model-provider-page/model-parameter-modal/model-settings-trigger'
import { SplitModelSelector } from '@/app/components/header/account-setting/model-provider-page/model-selector'
import { consoleQuery } from '@/service/console'
import { fetchAndMergeValidCompletionParams } from '@/utils/completion-params'
import LLMParamsPanel from './llm-params-panel'
import TTSParamsPanel from './tts-params-panel'
@ -69,12 +70,42 @@ const ModelParameterModal: FC<ModelParameterModalProps> = ({
.map((item) => item as ModelFeatureEnum)
}, [scopeArray])
const { data: textGenerationList } = useModelList(ModelTypeEnum.textGeneration)
const { data: textEmbeddingList } = useModelList(ModelTypeEnum.textEmbedding)
const { data: rerankList } = useModelList(ModelTypeEnum.rerank)
const { data: moderationList } = useModelList(ModelTypeEnum.moderation)
const { data: sttList } = useModelList(ModelTypeEnum.speech2text)
const { data: ttsList } = useModelList(ModelTypeEnum.tts)
const { data: textGenerationList = [] } = useQuery(
consoleQuery.workspaces.current.models.modelTypes.byModelType.get.queryOptions({
input: { params: { model_type: ModelTypeEnum.textGeneration } },
select: (response) => response.data,
}),
)
const { data: textEmbeddingList = [] } = useQuery(
consoleQuery.workspaces.current.models.modelTypes.byModelType.get.queryOptions({
input: { params: { model_type: ModelTypeEnum.textEmbedding } },
select: (response) => response.data,
}),
)
const { data: rerankList = [] } = useQuery(
consoleQuery.workspaces.current.models.modelTypes.byModelType.get.queryOptions({
input: { params: { model_type: ModelTypeEnum.rerank } },
select: (response) => response.data,
}),
)
const { data: moderationList = [] } = useQuery(
consoleQuery.workspaces.current.models.modelTypes.byModelType.get.queryOptions({
input: { params: { model_type: ModelTypeEnum.moderation } },
select: (response) => response.data,
}),
)
const { data: sttList = [] } = useQuery(
consoleQuery.workspaces.current.models.modelTypes.byModelType.get.queryOptions({
input: { params: { model_type: ModelTypeEnum.speech2text } },
select: (response) => response.data,
}),
)
const { data: ttsList = [] } = useQuery(
consoleQuery.workspaces.current.models.modelTypes.byModelType.get.queryOptions({
input: { params: { model_type: ModelTypeEnum.tts } },
select: (response) => response.data,
}),
)
const scopedModelList = useMemo(() => {
if (scopeArray.includes('all')) {

View File

@ -1,7 +1,9 @@
import type {
ProviderModelWithStatusEntity,
ProviderWithModelsResponse,
} from '@dify/contracts/api/console/workspaces/types.gen'
import type {
DefaultModel,
Model,
ModelItem,
ModelProvider,
} from '@/app/components/header/account-setting/model-provider-page/declarations'
import type { CredentialPanelState } from '@/app/components/header/account-setting/model-provider-page/provider-added-card/use-credential-panel-state'
@ -14,7 +16,9 @@ import {
PreferredProviderTypeEnum,
} from '@/app/components/header/account-setting/model-provider-page/declarations'
export function createModelItem(overrides: Partial<ModelItem> = {}): ModelItem {
export function createModelItem(
overrides: Partial<ProviderModelWithStatusEntity> = {},
): ProviderModelWithStatusEntity {
return {
model: 'text-embedding-3-large',
label: { en_US: 'Text Embedding 3 Large', zh_Hans: 'Text Embedding 3 Large' },
@ -27,8 +31,11 @@ export function createModelItem(overrides: Partial<ModelItem> = {}): ModelItem {
}
}
export function createModel(overrides: Partial<Model> = {}): Model {
export function createModel(
overrides: Partial<ProviderWithModelsResponse> = {},
): ProviderWithModelsResponse {
return {
tenant_id: 'test-workspace',
provider: 'openai',
icon_small: { en_US: 'icon', zh_Hans: 'icon' },
icon_small_dark: { en_US: 'icon-dark', zh_Hans: 'icon-dark' },

View File

@ -1,4 +1,6 @@
import type { AgentSoulDifyToolConfig } from '@dify/contracts/api/console/apps/types.gen'
import type { GetWorkspacesCurrentModelsModelTypesByModelTypeData } from '@dify/contracts/api/console/workspaces/types.gen'
import type { OperationKey } from '@orpc/tanstack-query'
import type { CommonNodeType, Node } from '../../types'
import type { ChecklistItem } from '../use-checklist'
import type { ToolWithProvider } from '@/app/components/workflow/types'
@ -79,10 +81,6 @@ vi.mock('@/service/use-strategy', () => ({
useStrategyProviders: () => ({ data: [] }),
}))
vi.mock('@/app/components/header/account-setting/model-provider-page/hooks', () => ({
useModelList: () => ({ data: [] }),
}))
type CheckValidFn = (data: CommonNodeType, t: unknown, extra?: unknown) => { errorMessage: string }
const mockNodesMap: Record<
string,
@ -855,3 +853,19 @@ describe('useWorkflowRunValidation', () => {
expect(result.current.validateBeforeRun()).toBe(true)
})
})
vi.mock('@tanstack/react-query', async (importOriginal) => {
const actual = await importOriginal<typeof import('@tanstack/react-query')>()
return {
...actual,
useQuery: (options: {
queryKey: OperationKey<
'query',
{ params: GetWorkspacesCurrentModelsModelTypesByModelTypeData['path'] }
>
}) => {
if (!options.queryKey[0].includes('modelTypes')) return actual.useQuery(options)
return { data: [] }
},
}
})

View File

@ -19,14 +19,13 @@ import type { AgentToolPublishIssue } from '@/features/agent-v2/agent-detail/con
import type { DataSet } from '@/models/datasets'
import type { I18nKeysWithPrefix } from '@/types/i18n'
import { toast } from '@langgenius/dify-ui/toast'
import { useQueries, useQueryClient } from '@tanstack/react-query'
import { useQueries, useQuery, useQueryClient } from '@tanstack/react-query'
import isDeepEqual from 'fast-deep-equal'
import { useCallback, useEffect, useMemo, useRef } from 'react'
import { useTranslation } from 'react-i18next'
import { useEdges, useStoreApi } from 'reactflow'
import { useStore as useAppStore } from '@/app/components/app/store'
import { ModelTypeEnum } from '@/app/components/header/account-setting/model-provider-page/declarations'
import { useModelList } from '@/app/components/header/account-setting/model-provider-page/hooks'
import { normalizeModelProviderModelsResponse } from '@/app/components/header/account-setting/model-provider-page/utils'
import useNodes from '@/app/components/workflow/store/workflow/use-nodes'
import { MAX_TREE_DEPTH } from '@/config'
@ -277,8 +276,18 @@ export const useChecklist = (nodes: Node[], edges: Edge[], options?: { flowType?
inlineAgentIssueTools,
inlineAgentToolProviderCatalog,
)
const { data: embeddingModelList } = useModelList(ModelTypeEnum.textEmbedding)
const { data: rerankModelList } = useModelList(ModelTypeEnum.rerank)
const { data: embeddingModelList = [] } = useQuery(
consoleQuery.workspaces.current.models.modelTypes.byModelType.get.queryOptions({
input: { params: { model_type: ModelTypeEnum.textEmbedding } },
select: (response) => response.data,
}),
)
const { data: rerankModelList = [] } = useQuery(
consoleQuery.workspaces.current.models.modelTypes.byModelType.get.queryOptions({
input: { params: { model_type: ModelTypeEnum.rerank } },
select: (response) => response.data,
}),
)
const knowledgeBaseEmbeddingProviders = useMemo(() => {
const providers = new Set<string>()
@ -614,8 +623,18 @@ export const useChecklistBeforePublish = () => {
const updateTimeRef = useRef(0)
const workflowStore = useWorkflowStore()
const { getNodesAvailableVarList } = useGetNodesAvailableVarList()
const { data: embeddingModelList } = useModelList(ModelTypeEnum.textEmbedding)
const { data: rerankModelList } = useModelList(ModelTypeEnum.rerank)
const { data: embeddingModelList = [] } = useQuery(
consoleQuery.workspaces.current.models.modelTypes.byModelType.get.queryOptions({
input: { params: { model_type: ModelTypeEnum.textEmbedding } },
select: (response) => response.data,
}),
)
const { data: rerankModelList = [] } = useQuery(
consoleQuery.workspaces.current.models.modelTypes.byModelType.get.queryOptions({
input: { params: { model_type: ModelTypeEnum.rerank } },
select: (response) => response.data,
}),
)
const { data: buildInTools } = useAllBuiltInTools()
const { data: customTools } = useAllCustomTools()
const { data: workflowTools } = useAllWorkflowTools()

View File

@ -1,3 +1,5 @@
import type { GetWorkspacesCurrentModelsModelTypesByModelTypeData } from '@dify/contracts/api/console/workspaces/types.gen'
import type { OperationKey } from '@orpc/tanstack-query'
/* oxlint-disable typescript/no-explicit-any */
import type { AgentNodeType } from '../types'
import type { StrategyParamItem } from '@/app/components/plugins/types'
@ -40,17 +42,6 @@ let mockMarketplaceIcon: string | Record<string, string> | undefined
const mockResetEditor = vi.fn()
vi.mock('@/app/components/header/account-setting/model-provider-page/hooks', () => ({
useModelList: (modelType: ModelTypeEnum) => {
if (modelType === ModelTypeEnum.textGeneration) return { data: mockTextGenerationModels }
if (modelType === ModelTypeEnum.moderation) return { data: mockModerationModels }
if (modelType === ModelTypeEnum.rerank) return { data: mockRerankModels }
if (modelType === ModelTypeEnum.speech2text) return { data: mockSpeech2TextModels }
if (modelType === ModelTypeEnum.textEmbedding) return { data: mockTextEmbeddingModels }
return { data: mockTtsModels }
},
}))
vi.mock('@/app/components/header/account-setting/model-provider-page/model-selector', () => ({
ModelSelector: ({ value, models }: any) => (
<div>
@ -383,3 +374,27 @@ describe('agent path', () => {
})
})
})
vi.mock('@tanstack/react-query', async (importOriginal) => {
const actual = await importOriginal<typeof import('@tanstack/react-query')>()
return {
...actual,
useQuery: (options: {
queryKey: OperationKey<
'query',
{ params: GetWorkspacesCurrentModelsModelTypesByModelTypeData['path'] }
>
}) => {
if (!options.queryKey[0].includes('modelTypes')) return actual.useQuery(options)
const modelType = options.queryKey[1].input?.params?.model_type
if (!modelType) throw new Error('Missing model type in query')
if (modelType === ModelTypeEnum.textGeneration) return { data: mockTextGenerationModels }
if (modelType === ModelTypeEnum.moderation) return { data: mockModerationModels }
if (modelType === ModelTypeEnum.rerank) return { data: mockRerankModels }
if (modelType === ModelTypeEnum.speech2text) return { data: mockSpeech2TextModels }
if (modelType === ModelTypeEnum.textEmbedding) return { data: mockTextEmbeddingModels }
return { data: mockTtsModels }
},
}
})

View File

@ -1,3 +1,5 @@
import type { GetWorkspacesCurrentModelsModelTypesByModelTypeData } from '@dify/contracts/api/console/workspaces/types.gen'
import type { OperationKey } from '@orpc/tanstack-query'
import type { ModelTypeEnum } from '@/app/components/header/account-setting/model-provider-page/declarations'
import { render, screen } from '@testing-library/react'
import { ModelBar } from '../model-bar'
@ -7,13 +9,7 @@ type ModelProviderItem = {
models: Array<{ model: string }>
}
const mockModelLists = new Map<ModelTypeEnum, ModelProviderItem[]>()
vi.mock('@/app/components/header/account-setting/model-provider-page/hooks', () => ({
useModelList: (modelType: ModelTypeEnum) => ({
data: mockModelLists.get(modelType) || [],
}),
}))
const mockModelLists = new Map<string, ModelProviderItem[]>()
vi.mock('@/app/components/header/account-setting/model-provider-page/model-selector', () => ({
ModelSelector: ({
@ -72,3 +68,24 @@ describe('agent/model-bar', () => {
expect(screen.getByText('workflow.nodes.agent.modelNotInstallTooltip')).toBeInTheDocument()
})
})
vi.mock('@tanstack/react-query', async (importOriginal) => {
const actual = await importOriginal<typeof import('@tanstack/react-query')>()
return {
...actual,
useQuery: (options: {
queryKey: OperationKey<
'query',
{ params: GetWorkspacesCurrentModelsModelTypesByModelTypeData['path'] }
>
}) => {
if (!options.queryKey[0].includes('modelTypes')) return actual.useQuery(options)
const modelType = options.queryKey[1].input?.params?.model_type
if (!modelType) throw new Error('Missing model type in query')
return {
data: mockModelLists.get(modelType) || [],
}
},
}
})

View File

@ -1,11 +1,12 @@
import type { FC } from 'react'
import { StatusDot } from '@langgenius/dify-ui/status-dot'
import { Tooltip, TooltipContent, TooltipTrigger } from '@langgenius/dify-ui/tooltip'
import { useQuery } from '@tanstack/react-query'
import { useMemo } from 'react'
import { useTranslation } from 'react-i18next'
import { ModelTypeEnum } from '@/app/components/header/account-setting/model-provider-page/declarations'
import { useModelList } from '@/app/components/header/account-setting/model-provider-page/hooks'
import { ModelSelector } from '@/app/components/header/account-setting/model-provider-page/model-selector'
import { consoleQuery } from '@/service/console'
type ModelBarProps =
| {
@ -18,12 +19,42 @@ type ModelBarProps =
}
const useAllModel = () => {
const { data: textGeneration } = useModelList(ModelTypeEnum.textGeneration)
const { data: moderation } = useModelList(ModelTypeEnum.moderation)
const { data: rerank } = useModelList(ModelTypeEnum.rerank)
const { data: speech2text } = useModelList(ModelTypeEnum.speech2text)
const { data: textEmbedding } = useModelList(ModelTypeEnum.textEmbedding)
const { data: tts } = useModelList(ModelTypeEnum.tts)
const { data: textGeneration = [] } = useQuery(
consoleQuery.workspaces.current.models.modelTypes.byModelType.get.queryOptions({
input: { params: { model_type: ModelTypeEnum.textGeneration } },
select: (response) => response.data,
}),
)
const { data: moderation = [] } = useQuery(
consoleQuery.workspaces.current.models.modelTypes.byModelType.get.queryOptions({
input: { params: { model_type: ModelTypeEnum.moderation } },
select: (response) => response.data,
}),
)
const { data: rerank = [] } = useQuery(
consoleQuery.workspaces.current.models.modelTypes.byModelType.get.queryOptions({
input: { params: { model_type: ModelTypeEnum.rerank } },
select: (response) => response.data,
}),
)
const { data: speech2text = [] } = useQuery(
consoleQuery.workspaces.current.models.modelTypes.byModelType.get.queryOptions({
input: { params: { model_type: ModelTypeEnum.speech2text } },
select: (response) => response.data,
}),
)
const { data: textEmbedding = [] } = useQuery(
consoleQuery.workspaces.current.models.modelTypes.byModelType.get.queryOptions({
input: { params: { model_type: ModelTypeEnum.textEmbedding } },
select: (response) => response.data,
}),
)
const { data: tts = [] } = useQuery(
consoleQuery.workspaces.current.models.modelTypes.byModelType.get.queryOptions({
input: { params: { model_type: ModelTypeEnum.tts } },
select: (response) => response.data,
}),
)
const models = useMemo(() => {
return textGeneration
.concat(moderation)

View File

@ -1,8 +1,8 @@
import type { KnowledgeBaseNodeType } from '../types'
import type {
Model,
ModelItem,
} from '@/app/components/header/account-setting/model-provider-page/declarations'
ProviderModelWithStatusEntity,
ProviderWithModelsResponse,
} from '@dify/contracts/api/console/workspaces/types.gen'
import type { KnowledgeBaseNodeType } from '../types'
import {
ConfigurationMethodEnum,
ModelStatusEnum,
@ -14,8 +14,9 @@ import { ChunkStructureEnum, IndexMethodEnum, RetrievalSearchMethodEnum } from '
const t = withSelectorKey((key: string, _options?: Record<string, unknown>) => key)
const makeEmbeddingModelList = (status: ModelStatusEnum): Model[] => [
const makeEmbeddingModelList = (status: ModelStatusEnum): ProviderWithModelsResponse[] => [
{
tenant_id: 'test-workspace',
provider: 'openai',
icon_small: { en_US: '', zh_Hans: '' },
label: { en_US: 'OpenAI', zh_Hans: 'OpenAI' },
@ -30,11 +31,13 @@ const makeEmbeddingModelList = (status: ModelStatusEnum): Model[] => [
load_balancing_enabled: false,
},
],
status,
status: 'active',
},
]
const makeEmbeddingProviderModelList = (status: ModelStatusEnum): ModelItem[] => [
const makeEmbeddingProviderModelList = (
status: ModelStatusEnum,
): ProviderModelWithStatusEntity[] => [
{
model: 'text-embedding-3-large',
label: { en_US: 'Text Embedding 3 Large', zh_Hans: 'Text Embedding 3 Large' },

View File

@ -11,18 +11,10 @@ import { BlockEnum } from '@/app/components/workflow/types'
import Node from '../node'
import { ChunkStructureEnum, IndexMethodEnum, RetrievalSearchMethodEnum } from '../types'
const mockUseModelList = vi.hoisted(() => vi.fn())
const mockModelListQuery = vi.hoisted(() => vi.fn())
const mockUseSettingsDisplay = vi.hoisted(() => vi.fn())
const mockUseEmbeddingModelStatus = vi.hoisted(() => vi.fn())
vi.mock('@tanstack/react-query', async (importOriginal) => {
const actual = await importOriginal<typeof import('@tanstack/react-query')>()
return {
...actual,
useQuery: () => ({ data: undefined }),
}
})
vi.mock(
'@/app/components/header/account-setting/model-provider-page/hooks',
async (importOriginal) => {
@ -33,7 +25,6 @@ vi.mock(
return {
...actual,
useLanguage: () => 'en_US',
useModelList: mockUseModelList,
}
},
)
@ -81,7 +72,7 @@ const createNodeData = (
describe('KnowledgeBaseNode', () => {
beforeEach(() => {
vi.clearAllMocks()
mockUseModelList.mockReturnValue({ data: [] })
mockModelListQuery.mockReturnValue({ data: [] })
mockUseSettingsDisplay.mockReturnValue({
[IndexMethodEnum.QUALIFIED]: 'High Quality',
[RetrievalSearchMethodEnum.semantic]: 'Vector Search',
@ -193,19 +184,22 @@ describe('KnowledgeBaseNode', () => {
})
it('should render a warning value for retrieval settings when reranking is incomplete', () => {
mockUseModelList.mockImplementation((modelType: ModelTypeEnum) => {
if (modelType === ModelTypeEnum.textEmbedding) {
return {
data: [
{
provider: 'openai',
models: [createModelItem()],
},
],
mockModelListQuery.mockImplementation(
({ input }: { input: { params: { model_type: string } } }) => {
const modelType = input.params.model_type
if (modelType === ModelTypeEnum.textEmbedding) {
return {
data: [
{
provider: 'openai',
models: [createModelItem()],
},
],
}
}
}
return { data: [] }
})
return { data: [] }
},
)
render(
<Node
@ -239,3 +233,14 @@ describe('KnowledgeBaseNode', () => {
})
})
})
vi.mock('@tanstack/react-query', async (importOriginal) => {
const actual = await importOriginal<typeof import('@tanstack/react-query')>()
return {
...actual,
useQuery: (options: { queryKey?: readonly [readonly string[], unknown] }) =>
options.queryKey?.[0].includes('modelTypes')
? mockModelListQuery(options)
: { data: undefined },
}
})

View File

@ -5,7 +5,7 @@ import { ModelTypeEnum } from '@/app/components/header/account-setting/model-pro
import Panel from '../panel'
import { ChunkStructureEnum, IndexMethodEnum, RetrievalSearchMethodEnum } from '../types'
const mockUseModelList = vi.hoisted(() => vi.fn())
const mockModelListQuery = vi.hoisted(() => vi.fn())
const mockUseQuery = vi.hoisted(() => vi.fn())
const mockUseEmbeddingModelStatus = vi.hoisted(() => vi.fn())
const mockChunkStructure = vi.hoisted(() => vi.fn(() => <div data-testid="chunk-structure" />))
@ -15,44 +15,36 @@ const mockSummaryIndexSetting = vi.hoisted(() =>
)
const mockQueryOptions = vi.hoisted(() => vi.fn((options: unknown) => options))
vi.mock('@tanstack/react-query', async (importOriginal) => ({
...(await importOriginal<typeof import('@tanstack/react-query')>()),
useQuery: mockUseQuery,
useSuspenseQuery: ({ select }: { select: (value: unknown) => unknown }) => ({
data: select({ deployment_edition: 'COMMUNITY' }),
}),
}))
vi.mock('@/service/console', () => ({
consoleQuery: {
systemFeatures: {
get: {
queryKey: () => ['console', 'systemFeatures', 'get'],
queryOptions: (options?: Record<string, unknown>) => ({
queryKey: ['console', 'systemFeatures', 'get'],
...options,
}),
vi.mock('@/service/console', async (importOriginal) => {
const actual = await importOriginal<typeof import('@/service/console')>()
return {
consoleQuery: {
systemFeatures: {
get: {
queryKey: () => ['console', 'systemFeatures', 'get'],
queryOptions: (options?: Record<string, unknown>) => ({
queryKey: ['console', 'systemFeatures', 'get'],
...options,
}),
},
},
},
workspaces: {
current: {
modelProviders: {
byProvider: {
models: {
get: {
queryOptions: mockQueryOptions,
workspaces: {
current: {
models: actual.consoleQuery.workspaces.current.models,
modelProviders: {
byProvider: {
models: {
get: {
queryOptions: mockQueryOptions,
},
},
},
},
},
},
},
},
}))
vi.mock('@/app/components/header/account-setting/model-provider-page/hooks', () => ({
useModelList: mockUseModelList,
}))
}
})
vi.mock('../../../hooks/use-workflow', async (importOriginal) => {
const actual = await importOriginal<typeof import('../../../hooks/use-workflow')>()
@ -169,19 +161,23 @@ describe('KnowledgeBasePanel', () => {
beforeEach(() => {
vi.clearAllMocks()
mockUseQuery.mockReturnValue({ data: undefined })
mockUseModelList.mockImplementation((modelType: ModelTypeEnum) => {
if (modelType === ModelTypeEnum.textEmbedding) {
return {
data: [
{
provider: 'openai',
models: [{ model: 'text-embedding-3-large' }],
},
],
mockModelListQuery.mockImplementation(
({ input }: { input: { params: { model_type: string } } }) => {
const type = input.params.model_type
const modelType = type
if (modelType === ModelTypeEnum.textEmbedding) {
return {
data: [
{
provider: 'openai',
models: [{ model: 'text-embedding-3-large' }],
},
],
}
}
}
return { data: [] }
})
return { data: [] }
},
)
mockUseEmbeddingModelStatus.mockReturnValue({ status: 'active' })
})
@ -253,3 +249,17 @@ describe('KnowledgeBasePanel', () => {
)
})
})
vi.mock('@tanstack/react-query', async (importOriginal) => {
const actual = await importOriginal<typeof import('@tanstack/react-query')>()
return {
...actual,
useSuspenseQuery: ({ select }: { select: (value: unknown) => unknown }) => ({
data: select({ deployment_edition: 'COMMUNITY' }),
}),
useQuery: (options: { queryKey?: readonly [readonly string[], unknown] }) =>
options.queryKey?.[0].includes('modelTypes')
? mockModelListQuery(options)
: mockUseQuery(options),
}
})

View File

@ -1,9 +1,7 @@
import type { ProviderWithModelsResponse } from '@dify/contracts/api/console/workspaces/types.gen'
import type { TFunction } from 'i18next'
import type { KnowledgeBaseNodeType } from '../types'
import type {
Model,
ModelItem,
} from '@/app/components/header/account-setting/model-provider-page/declarations'
import type { ModelItem } from '@/app/components/header/account-setting/model-provider-page/declarations'
import {
ConfigurationMethodEnum,
ModelStatusEnum,
@ -18,9 +16,10 @@ import {
KnowledgeBaseValidationIssueCode,
} from '../utils'
const makeEmbeddingModelList = (status: ModelStatusEnum): Model[] => {
const makeEmbeddingModelList = (status: ModelStatusEnum): ProviderWithModelsResponse[] => {
return [
{
tenant_id: 'test-workspace',
provider: 'openai',
icon_small: { en_US: '', zh_Hans: '' },
label: { en_US: 'OpenAI', zh_Hans: 'OpenAI' },
@ -35,7 +34,7 @@ const makeEmbeddingModelList = (status: ModelStatusEnum): Model[] => {
load_balancing_enabled: false,
},
],
status,
status: 'active',
},
]
}

View File

@ -3,7 +3,7 @@ import { render } from '@testing-library/react'
import { ModelTypeEnum } from '@/app/components/header/account-setting/model-provider-page/declarations'
import EmbeddingModel from '../embedding-model'
const mockUseModelList = vi.hoisted(() => vi.fn())
const mockModelListQuery = vi.hoisted(() => vi.fn())
const mockModelSelector = vi.hoisted(() =>
vi.fn(() => <div data-testid="model-selector">selector</div>),
)
@ -22,10 +22,6 @@ vi.mock('@/app/components/workflow/nodes/_base/components/layout', () => ({
),
}))
vi.mock('@/app/components/header/account-setting/model-provider-page/hooks', () => ({
useModelList: mockUseModelList,
}))
vi.mock('@/app/components/header/account-setting/model-provider-page/model-selector', () => ({
ModelSelector: mockModelSelector,
}))
@ -33,7 +29,7 @@ vi.mock('@/app/components/header/account-setting/model-provider-page/model-selec
describe('EmbeddingModel', () => {
beforeEach(() => {
vi.clearAllMocks()
mockUseModelList.mockReturnValue({
mockModelListQuery.mockReturnValue({
data: [{ provider: 'openai', model: 'text-embedding-3-large' }],
})
})
@ -50,7 +46,9 @@ describe('EmbeddingModel', () => {
/>,
)
expect(mockUseModelList).toHaveBeenCalledWith(ModelTypeEnum.textEmbedding)
expect(mockModelListQuery).toHaveBeenCalledWith(
expect.objectContaining({ input: { params: { model_type: ModelTypeEnum.textEmbedding } } }),
)
expect(mockModelSelector).toHaveBeenCalledWith(
expect.objectContaining({
value: {
@ -76,3 +74,8 @@ describe('EmbeddingModel', () => {
)
})
})
vi.mock('@tanstack/react-query', async (importOriginal) => {
const actual = await importOriginal<typeof import('@tanstack/react-query')>()
return { ...actual, useQuery: mockModelListQuery }
})

View File

@ -1,4 +1,5 @@
import type { DefaultModel } from '@/app/components/header/account-setting/model-provider-page/declarations'
import { useQuery } from '@tanstack/react-query'
import { memo, useCallback, useMemo } from 'react'
import { useTranslation } from 'react-i18next'
import {
@ -6,9 +7,9 @@ import {
MultimodalRetrievalGuidanceLearnMore,
} from '@/app/components/datasets/common/multimodal-retrieval-guidance'
import { ModelTypeEnum } from '@/app/components/header/account-setting/model-provider-page/declarations'
import { useModelList } from '@/app/components/header/account-setting/model-provider-page/hooks'
import { ModelSelector } from '@/app/components/header/account-setting/model-provider-page/model-selector'
import { Field } from '@/app/components/workflow/nodes/_base/components/layout'
import { consoleQuery } from '@/service/console'
type EmbeddingModelProps = {
embeddingModel?: string
@ -28,7 +29,12 @@ const EmbeddingModel = ({
readonly = false,
}: EmbeddingModelProps) => {
const { t } = useTranslation()
const { data: embeddingModelList } = useModelList(ModelTypeEnum.textEmbedding)
const { data: embeddingModelList = [] } = useQuery(
consoleQuery.workspaces.current.models.modelTypes.byModelType.get.queryOptions({
input: { params: { model_type: ModelTypeEnum.textEmbedding } },
select: (response) => response.data,
}),
)
const embeddingModelConfig = useMemo(() => {
if (!embeddingModel || !embeddingModelProvider) return undefined

View File

@ -1,7 +1,5 @@
import type {
DefaultModel,
Model,
} from '@/app/components/header/account-setting/model-provider-page/declarations'
import type { ProviderWithModelsResponse } from '@dify/contracts/api/console/workspaces/types.gen'
import type { DefaultModel } from '@/app/components/header/account-setting/model-provider-page/declarations'
import { fireEvent, render, screen } from '@testing-library/react'
import { ModelTypeEnum } from '@/app/components/header/account-setting/model-provider-page/declarations'
import {
@ -12,7 +10,7 @@ import RerankingModelSelector from '../reranking-model-selector'
type MockModelSelectorProps = {
value?: DefaultModel
models: Model[]
models: ProviderWithModelsResponse[]
onValueChange?: (model: DefaultModel) => void
}
@ -45,6 +43,7 @@ describe('RerankingModelSelector', () => {
mockUseModelListAndDefaultModel.mockReturnValue({
modelList: [
createModel({
tenant_id: 'test-workspace',
provider: 'cohere',
label: { en_US: 'Cohere', zh_Hans: 'Cohere' },
models: [

View File

@ -1,9 +1,9 @@
import type { ModelProviderSummaryResponse } from '@dify/contracts/api/console/workspaces/types.gen'
import type {
Model,
ModelItem,
ModelProvider,
} from '@/app/components/header/account-setting/model-provider-page/declarations'
ModelProviderSummaryResponse,
ProviderModelWithStatusEntity,
ProviderWithModelsResponse,
} from '@dify/contracts/api/console/workspaces/types.gen'
import type { ModelProvider } from '@/app/components/header/account-setting/model-provider-page/declarations'
import { useMemo } from 'react'
import { deriveModelStatus } from '@/app/components/header/account-setting/model-provider-page/derive-model-status'
import { useCredentialPanelState } from '@/app/components/header/account-setting/model-provider-page/provider-added-card/use-credential-panel-state'
@ -12,13 +12,13 @@ import { useProviderContext } from '@/context/provider-context'
type UseEmbeddingModelStatusProps = {
embeddingModel?: string
embeddingModelProvider?: string
embeddingModelList: Model[]
embeddingModelList: ProviderWithModelsResponse[]
}
type UseEmbeddingModelStatusResult = {
providerMeta: ModelProviderSummaryResponse | ModelProvider | undefined
modelProvider: Model | undefined
currentModel: ModelItem | undefined
modelProvider: ProviderWithModelsResponse | undefined
currentModel: ProviderModelWithStatusEntity | undefined
status: ReturnType<typeof deriveModelStatus>
}

View File

@ -7,11 +7,9 @@ import { memo, useMemo } from 'react'
import { useTranslation } from 'react-i18next'
import { ModelTypeEnum } from '@/app/components/header/account-setting/model-provider-page/declarations'
import { DERIVED_MODEL_STATUS_BADGE_I18N } from '@/app/components/header/account-setting/model-provider-page/derive-model-status'
import {
useLanguage,
useModelList,
} from '@/app/components/header/account-setting/model-provider-page/hooks'
import { useLanguage } from '@/app/components/header/account-setting/model-provider-page/hooks'
import { normalizeModelProviderModelsResponse } from '@/app/components/header/account-setting/model-provider-page/utils'
import { renderI18nObject } from '@/i18n-config'
import { consoleQuery } from '@/service/console'
import { useEmbeddingModelStatus } from './hooks/use-embedding-model-status'
import { useSettingsDisplay } from './hooks/use-settings-display'
@ -62,8 +60,18 @@ const Node: FC<NodeProps<KnowledgeBaseNodeType>> = ({ data }) => {
const { t } = useTranslation()
const language = useLanguage()
const settingsDisplay = useSettingsDisplay()
const { data: embeddingModelList } = useModelList(ModelTypeEnum.textEmbedding)
const { data: rerankModelList } = useModelList(ModelTypeEnum.rerank)
const { data: embeddingModelList = [] } = useQuery(
consoleQuery.workspaces.current.models.modelTypes.byModelType.get.queryOptions({
input: { params: { model_type: ModelTypeEnum.textEmbedding } },
select: (response) => response.data,
}),
)
const { data: rerankModelList = [] } = useQuery(
consoleQuery.workspaces.current.models.modelTypes.byModelType.get.queryOptions({
input: { params: { model_type: ModelTypeEnum.rerank } },
select: (response) => response.data,
}),
)
const chunkStructure = data.chunk_structure
const indexChunkVariableSelector = data.index_chunk_variable_selector
const indexingTechnique = data.indexing_technique
@ -149,8 +157,7 @@ const Node: FC<NodeProps<KnowledgeBaseNodeType>> = ({ data }) => {
}
return (
currentEmbeddingModel?.label[language] ||
currentEmbeddingModel?.label.en_US ||
(currentEmbeddingModel && renderI18nObject(currentEmbeddingModel.label, language)) ||
data.embedding_model ||
'-'
)

View File

@ -7,7 +7,6 @@ import { useTranslation } from 'react-i18next'
import SummaryIndexSetting from '@/app/components/datasets/settings/summary-index-setting'
import { checkShowMultiModalTip } from '@/app/components/datasets/settings/utils'
import { ModelTypeEnum } from '@/app/components/header/account-setting/model-provider-page/declarations'
import { useModelList } from '@/app/components/header/account-setting/model-provider-page/hooks'
import { normalizeModelProviderModelsResponse } from '@/app/components/header/account-setting/model-provider-page/utils'
import {
BoxGroup,
@ -36,8 +35,18 @@ const Panel: FC<NodePanelProps<KnowledgeBaseNodeType>> = ({ id, data }) => {
})
const isNonCloudEdition = deploymentEdition === 'COMMUNITY' || deploymentEdition === 'ENTERPRISE'
const { nodesReadOnly } = useNodesReadOnly()
const { data: embeddingModelList } = useModelList(ModelTypeEnum.textEmbedding)
const { data: rerankModelList } = useModelList(ModelTypeEnum.rerank)
const { data: embeddingModelList = [] } = useQuery(
consoleQuery.workspaces.current.models.modelTypes.byModelType.get.queryOptions({
input: { params: { model_type: ModelTypeEnum.textEmbedding } },
select: (response) => response.data,
}),
)
const { data: rerankModelList = [] } = useQuery(
consoleQuery.workspaces.current.models.modelTypes.byModelType.get.queryOptions({
input: { params: { model_type: ModelTypeEnum.rerank } },
select: (response) => response.data,
}),
)
const chunkStructure = data.chunk_structure
const indexChunkVariableSelector = data.index_chunk_variable_selector
const indexingTechnique = data.indexing_technique

View File

@ -1,8 +1,6 @@
import type { ProviderWithModelsResponse } from '@dify/contracts/api/console/workspaces/types.gen'
import type { IndexingType } from '@/app/components/datasets/create/step-two'
import type {
Model,
ModelItem,
} from '@/app/components/header/account-setting/model-provider-page/declarations'
import type { ModelItem } from '@/app/components/header/account-setting/model-provider-page/declarations'
import type { CommonNodeType } from '@/app/components/workflow/types'
import type { RerankingModeEnum, WeightedScoreEnum } from '@/models/datasets'
import type { RETRIEVE_METHOD } from '@/types/app'
@ -59,8 +57,8 @@ export type KnowledgeBaseNodeType = CommonNodeType & {
embedding_model_provider?: string
keyword_number: number
retrieval_model: RetrievalSetting
_embeddingModelList?: Model[]
_embeddingModelList?: ProviderWithModelsResponse[]
_embeddingProviderModelList?: ModelItem[]
_rerankModelList?: Model[]
_rerankModelList?: ProviderWithModelsResponse[]
summary_index_setting?: SummaryIndexSetting
}

View File

@ -1,18 +1,16 @@
import type { AvailableModelListResponse } from '@dify/contracts/api/console/workspaces/types.gen'
import type { MutableRefObject } from 'react'
import type { LLMNodeType } from '../../types'
import { act, renderHook } from '@testing-library/react'
import { ModelFeatureEnum } from '@/app/components/header/account-setting/model-provider-page/declarations'
import { useModelList } from '@/app/components/header/account-setting/model-provider-page/hooks'
import { BlockEnum } from '@/app/components/workflow/types'
import { AppModeEnum } from '@/types/app'
import { Type } from '../../types'
import useLLMStructuredOutputConfig from '../use-llm-structured-output-config'
vi.mock('@/app/components/header/account-setting/model-provider-page/hooks', () => ({
useModelList: vi.fn(),
}))
const mockUseModelList = vi.mocked(useModelList)
const mockModelListQuery = vi.hoisted(() =>
vi.fn<() => { data: AvailableModelListResponse['data'] }>(),
)
const createPayload = (overrides: Partial<LLMNodeType> = {}): LLMNodeType => ({
type: BlockEnum.LLM,
@ -41,19 +39,27 @@ describe('use-llm-structured-output-config', () => {
})
it('detects supported models and updates structured output state', () => {
mockUseModelList.mockReturnValue({
mockModelListQuery.mockReturnValue({
data: [
{
provider: 'openai',
tenant_id: 'test-workspace',
label: { en_US: 'OpenAI' },
status: 'active',
models: [
{
model: 'gpt-4o',
label: { en_US: 'gpt-4o' },
model_type: 'llm',
fetch_from: 'predefined-model',
status: 'active',
model_properties: {},
features: [ModelFeatureEnum.StructuredOutput],
},
],
},
],
} as ReturnType<typeof useModelList>)
})
const inputRef = {
current: createPayload(),
@ -136,21 +142,27 @@ describe('use-llm-structured-output-config', () => {
})
it('returns undefined support when the model is missing from the list', () => {
mockUseModelList.mockReturnValue({
mockModelListQuery.mockReturnValue({
data: [
{
provider: 'anthropic',
tenant_id: 'test-workspace',
label: { en_US: 'Anthropic' },
status: 'active',
models: [
{
model: 'claude',
label: { en_US: 'claude' },
model_type: 'llm',
fetch_from: 'predefined-model',
status: 'active',
model_properties: {},
features: [],
},
],
},
],
mutate: vi.fn(),
isLoading: false,
} as unknown as ReturnType<typeof useModelList>)
})
const { result } = renderHook(() =>
useLLMStructuredOutputConfig({
@ -165,3 +177,8 @@ describe('use-llm-structured-output-config', () => {
expect(result.current.isModelSupportStructuredOutput).toBeUndefined()
})
})
vi.mock('@tanstack/react-query', async (importOriginal) => {
const actual = await importOriginal<typeof import('@tanstack/react-query')>()
return { ...actual, useQuery: mockModelListQuery }
})

View File

@ -1,12 +1,13 @@
import type { MutableRefObject } from 'react'
import type { LLMNodeType, StructuredOutput } from '../types'
import { useQuery } from '@tanstack/react-query'
import { produce } from 'immer'
import { useCallback, useState } from 'react'
import {
ModelFeatureEnum,
ModelTypeEnum,
} from '@/app/components/header/account-setting/model-provider-page/declarations'
import { useModelList } from '@/app/components/header/account-setting/model-provider-page/hooks'
import { consoleQuery } from '@/service/console'
type Params = {
id: string
@ -23,7 +24,12 @@ const useLLMStructuredOutputConfig = ({
setInputs,
deleteNodeInspectorVars,
}: Params) => {
const { data: modelList } = useModelList(ModelTypeEnum.textGeneration)
const { data: modelList = [] } = useQuery(
consoleQuery.workspaces.current.models.modelTypes.byModelType.get.queryOptions({
input: { params: { model_type: ModelTypeEnum.textGeneration } },
select: (response) => response.data,
}),
)
const isModelSupportStructuredOutput = modelList
?.find((providerItem) => providerItem.provider === model?.provider)
?.models.find((modelItem) => modelItem.model === model?.name)

View File

@ -1,8 +1,8 @@
import type { ReactElement } from 'react'
import type {
Model,
ModelItem,
} from '@/app/components/header/account-setting/model-provider-page/declarations'
ProviderModelWithStatusEntity,
ProviderWithModelsResponse,
} from '@dify/contracts/api/console/workspaces/types.gen'
import type { ReactElement } from 'react'
import type { Shape } from '@/app/components/workflow/store/workflow'
import type { EnvironmentVariable } from '@/app/components/workflow/types'
import { toast } from '@langgenius/dify-ui/toast'
@ -21,12 +21,12 @@ type MockModelParameterModalProps = {
provider: string
modelId: string
completionParams: Record<string, unknown>
modelList?: Model[]
modelList?: ProviderWithModelsResponse[]
setModel: (model: { provider: string; modelId: string; mode?: string }) => void
onCompletionParamsChange: (params: Record<string, unknown>) => void
}
let mockTextGenerationModelList: Model[] = []
let mockTextGenerationModelList: ProviderWithModelsResponse[] = []
let latestModelParameterModalProps: MockModelParameterModalProps | undefined
vi.mock('@/app/components/header/account-setting/model-provider-page/hooks', () => ({
@ -89,7 +89,7 @@ vi.mock('@langgenius/dify-ui/toast', () => ({
const mockToastError = vi.mocked(toast.error)
const createModelItem = (model: string, mode: string): ModelItem => ({
const createModelItem = (model: string, mode: string): ProviderModelWithStatusEntity => ({
model,
label: { en_US: model, zh_Hans: model },
model_type: ModelTypeEnum.textGeneration,
@ -100,7 +100,8 @@ const createModelItem = (model: string, mode: string): ModelItem => ({
load_balancing_enabled: false,
})
const createModelProvider = (): Model => ({
const createModelProvider = (): ProviderWithModelsResponse => ({
tenant_id: 'test-workspace',
provider: 'openai',
icon_small: { en_US: '', zh_Hans: '' },
label: { en_US: 'OpenAI', zh_Hans: 'OpenAI' },

View File

@ -37,7 +37,6 @@ type IDebugConfiguration = {
readonly?: boolean
canTestAndRun?: boolean
appId: string
isAPIKeySet: boolean
isTrailFinished: boolean
mode: AppModeEnum
modelModeType: ModelModeType
@ -120,7 +119,6 @@ const DebugConfigurationContext = createContext<IDebugConfiguration>({
readonly: false,
canTestAndRun: false,
appId: '',
isAPIKeySet: false,
isTrailFinished: false,
mode: AppModeEnum.CHAT,
modelModeType: ModelModeType.chat,

View File

@ -2,12 +2,8 @@
import type { ReactNode } from 'react'
import { useQuery, useQueryClient } from '@tanstack/react-query'
import {
ModelStatusEnum,
ModelTypeEnum,
} from '@/app/components/header/account-setting/model-provider-page/declarations'
import { consoleQuery } from '@/service/console'
import { commonQueryKeys, useModelListByType } from '@/service/use-common'
import { commonQueryKeys } from '@/service/use-common'
import { ProviderContext } from './provider-context'
type ProviderContextProviderProps = {
@ -21,7 +17,6 @@ export const ProviderContextProvider = ({ children }: ProviderContextProviderPro
isLoading: isLoadingModelProviders,
isSuccess: isSuccessModelProviders,
} = useQuery(consoleQuery.workspaces.current.modelProviders.summary.get.queryOptions())
const { data: textGenerationModelList } = useModelListByType(ModelTypeEnum.textGeneration)
const refreshModelProviders = () =>
Promise.all([
@ -39,10 +34,6 @@ export const ProviderContextProvider = ({ children }: ProviderContextProviderPro
isLoadingModelProviders,
isSuccessModelProviders,
refreshModelProviders,
textGenerationModelList: textGenerationModelList?.data || [],
isAPIKeySet: !!textGenerationModelList?.data?.some(
(model) => model.status === ModelStatusEnum.active,
),
}}
>
{children}

View File

@ -4,7 +4,6 @@ import type {
ModelProviderPluginSummaryResponse,
ModelProviderSummaryResponse,
} from '@dify/contracts/api/console/workspaces/types.gen'
import type { Model } from '@/app/components/header/account-setting/model-provider-page/declarations'
import { createContext, useContext, useContextSelector } from 'use-context-selector'
export type ProviderContextState = {
@ -13,8 +12,6 @@ export type ProviderContextState = {
isLoadingModelProviders: boolean
isSuccessModelProviders: boolean
refreshModelProviders: () => Promise<void>
textGenerationModelList: Model[]
isAPIKeySet: boolean
}
const baseProviderContextValue: ProviderContextState = {
@ -23,8 +20,6 @@ const baseProviderContextValue: ProviderContextState = {
isLoadingModelProviders: false,
isSuccessModelProviders: false,
refreshModelProviders: async () => {},
textGenerationModelList: [],
isAPIKeySet: true,
}
export const ProviderContext = createContext<ProviderContextState>(baseProviderContextValue)

View File

@ -1,7 +1,7 @@
import type {
Model,
ModelItem,
} from '@/app/components/header/account-setting/model-provider-page/declarations'
ProviderModelWithStatusEntity,
ProviderWithModelsResponse,
} from '@dify/contracts/api/console/workspaces/types.gen'
import {
ConfigurationMethodEnum,
ModelStatusEnum,
@ -9,7 +9,8 @@ import {
} from '@/app/components/header/account-setting/model-provider-page/declarations'
import { isAgentCompatibleModel, isAgentSuggestedModel } from '../model-compatibility'
const createModel = (provider: string): Model => ({
const createModel = (provider: string): ProviderWithModelsResponse => ({
tenant_id: 'test-workspace',
provider,
icon_small: { en_US: '', zh_Hans: '' },
label: { en_US: provider, zh_Hans: provider },
@ -17,7 +18,10 @@ const createModel = (provider: string): Model => ({
status: ModelStatusEnum.active,
})
const createModelItem = (model: string, overrides: Partial<ModelItem> = {}): ModelItem => ({
const createModelItem = (
model: string,
overrides: Partial<ProviderModelWithStatusEntity> = {},
): ProviderModelWithStatusEntity => ({
model,
label: { en_US: model, zh_Hans: model },
model_type: ModelTypeEnum.textGeneration,
@ -31,8 +35,8 @@ const createModelItem = (model: string, overrides: Partial<ModelItem> = {}): Mod
const createModelItemWithLabel = (
model: string,
label: string,
overrides: Partial<ModelItem> = {},
): ModelItem =>
overrides: Partial<ProviderModelWithStatusEntity> = {},
): ProviderModelWithStatusEntity =>
createModelItem(model, {
label: { en_US: label, zh_Hans: label },
...overrides,

View File

@ -1,8 +1,10 @@
import type {
GetWorkspacesCurrentModelsModelTypesByModelTypeData,
SkillDetailResponse,
SkillReferenceResponse,
SkillVersionResponse,
} from '@dify/contracts/api/console/workspaces/types.gen'
import type { OperationKey } from '@orpc/tanstack-query'
import type userEvent from '@testing-library/user-event'
import type { ReactNode } from 'react'
import { detectPlatform } from '@tanstack/react-hotkeys'
@ -113,10 +115,6 @@ vi.mock('@/app/components/header/account-setting/model-provider-page/hooks', ()
useDefaultModel: () => ({
data: mocks.defaultTextGenerationModel,
}),
useModelList: () => ({
data: mocks.textGenerationModelList,
isLoading: false,
}),
useTextGenerationCurrentProviderAndModelAndModelList: () => ({
currentProvider: mocks.textGenerationModelList[0],
currentModel: mocks.textGenerationModelList[0]?.models[0],
@ -186,98 +184,102 @@ vi.mock('@/utils/download', () => ({
downloadBlob: mocks.downloadBlob,
}))
vi.mock('@/service/console', () => ({
consoleClient: {
workspaces: {
current: {
skills: {
bySkillId: {
get: mocks.skillDetailGetFn,
vi.mock('@/service/console', async (importOriginal) => {
const actual = await importOriginal<typeof import('@/service/console')>()
return {
consoleClient: {
workspaces: {
current: {
skills: {
bySkillId: {
get: mocks.skillDetailGetFn,
},
},
},
},
},
},
consoleQuery: {
workspaces: {
current: {
agents: {
byAgentId: {
skills: {
get: {
key: mocks.agentSkillBindingsKey,
},
},
},
},
skills: {
get: {
key: mocks.skillListKey,
},
tags: {
get: {
key: mocks.skillTagsKey,
queryOptions: mocks.skillTagsQueryOptions,
},
},
bySkillId: {
delete: {
mutationOptions: () => ({ mutationFn: mocks.deleteSkillMutationFn }),
},
duplicate: {
post: {
mutationOptions: () => ({ mutationFn: mocks.duplicateSkillMutationFn }),
},
},
get: {
key: mocks.skillDetailKey,
queryOptions: mocks.skillDetailQueryOptions,
},
patch: {
mutationOptions: () => ({ mutationFn: mocks.skillMetadataMutationFn }),
},
publish: {
post: {
mutationOptions: (options?: unknown) => {
mocks.publishSkillMutationOptions(options)
return { mutationFn: mocks.publishSkillMutationFn }
consoleQuery: {
workspaces: {
current: {
models: actual.consoleQuery.workspaces.current.models,
agents: {
byAgentId: {
skills: {
get: {
key: mocks.agentSkillBindingsKey,
},
},
},
references: {
},
skills: {
get: {
key: mocks.skillListKey,
},
tags: {
get: {
queryOptions: mocks.skillReferencesQueryOptions,
key: mocks.skillTagsKey,
queryOptions: mocks.skillTagsQueryOptions,
},
},
restore: {
post: {
mutationOptions: () => ({ mutationFn: mocks.restoreSkillMutationFn }),
bySkillId: {
delete: {
mutationOptions: () => ({ mutationFn: mocks.deleteSkillMutationFn }),
},
},
files: {
check: {
duplicate: {
post: {
mutationOptions: () => ({ mutationFn: mocks.checkDraftFilesMutationFn }),
mutationOptions: () => ({ mutationFn: mocks.duplicateSkillMutationFn }),
},
},
get: {
key: mocks.skillDetailKey,
queryOptions: mocks.skillDetailQueryOptions,
},
patch: {
mutationOptions: () => ({ mutationFn: mocks.saveDraftFileMutationFn }),
mutationOptions: () => ({ mutationFn: mocks.skillMetadataMutationFn }),
},
},
versions: {
get: {
key: mocks.skillVersionsKey,
queryOptions: mocks.skillVersionsQueryOptions,
publish: {
post: {
mutationOptions: (options?: unknown) => {
mocks.publishSkillMutationOptions(options)
return { mutationFn: mocks.publishSkillMutationFn }
},
},
},
byVersionId: {
references: {
get: {
queryOptions: mocks.skillVersionDetailQueryOptions,
queryOptions: mocks.skillReferencesQueryOptions,
},
},
restore: {
post: {
mutationOptions: () => ({ mutationFn: mocks.restoreSkillMutationFn }),
},
},
files: {
check: {
post: {
mutationOptions: () => ({ mutationFn: mocks.checkDraftFilesMutationFn }),
},
},
patch: {
mutationOptions: () => ({ mutationFn: mocks.versionPatchMutationFn }),
mutationOptions: () => ({ mutationFn: mocks.saveDraftFileMutationFn }),
},
delete: {
mutationOptions: () => ({ mutationFn: mocks.versionDeleteMutationFn }),
},
versions: {
get: {
key: mocks.skillVersionsKey,
queryOptions: mocks.skillVersionsQueryOptions,
},
byVersionId: {
get: {
queryOptions: mocks.skillVersionDetailQueryOptions,
},
patch: {
mutationOptions: () => ({ mutationFn: mocks.versionPatchMutationFn }),
},
delete: {
mutationOptions: () => ({ mutationFn: mocks.versionDeleteMutationFn }),
},
},
},
},
@ -285,8 +287,8 @@ vi.mock('@/service/console', () => ({
},
},
},
},
}))
}
})
vi.mock('../permissions', () => ({
useSkillPermissions: () => ({ canDelete: true, canEdit: true, canPublish: true }),
@ -846,3 +848,19 @@ export function resetDetailPageFixture() {
size: 10,
})
}
vi.mock('@tanstack/react-query', async (importOriginal) => {
const actual = await importOriginal<typeof import('@tanstack/react-query')>()
return {
...actual,
useQuery: (options: {
queryKey: OperationKey<
'query',
{ params: GetWorkspacesCurrentModelsModelTypesByModelTypeData['path'] }
>
}) =>
options.queryKey[0].includes('modelTypes')
? { data: mocks.textGenerationModelList, isPending: false }
: actual.useQuery(options),
}
})

View File

@ -1,20 +1,16 @@
'use client'
/* oxlint-disable eslint-react/set-state-in-effect -- The builder resets its local transcript when the authoritative detail snapshot changes. */
import type {
ProviderWithModelsResponse,
SkillDetailResponse,
SkillFileResponse,
} from '@dify/contracts/api/console/workspaces/types.gen'
/* oxlint-disable eslint-react/set-state-in-effect -- The builder resets its local transcript when the authoritative detail snapshot changes. */
import type { BuilderChatMessage, SkillBuilderAttachment, SkillBuilderModel } from './shared'
import type {
FormValue,
Model,
} from '@/app/components/header/account-setting/model-provider-page/declarations'
import type { FormValue } from '@/app/components/header/account-setting/model-provider-page/declarations'
import { Button } from '@langgenius/dify-ui/button'
import { cn } from '@langgenius/dify-ui/cn'
import { toast } from '@langgenius/dify-ui/toast'
import { useQueryClient } from '@tanstack/react-query'
import { useQuery, useQueryClient } from '@tanstack/react-query'
import { useEffect, useMemo, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { Markdown } from '@/app/components/base/markdown'
@ -22,12 +18,10 @@ import {
ModelStatusEnum,
ModelTypeEnum,
} from '@/app/components/header/account-setting/model-provider-page/declarations'
import {
useDefaultModel,
useModelList,
} from '@/app/components/header/account-setting/model-provider-page/hooks'
import { useDefaultModel } from '@/app/components/header/account-setting/model-provider-page/hooks'
import ModelParameterModal from '@/app/components/header/account-setting/model-provider-page/model-parameter-modal'
import { ModelSelector } from '@/app/components/header/account-setting/model-provider-page/model-selector'
import { consoleQuery } from '@/service/console'
import { sendSkillAssistMessage, uploadSkillFile } from '../client'
import { SkillBuilderGridTexture } from './builder-grid-texture'
import {
@ -78,7 +72,7 @@ function BuilderModelSelector({
onSelect,
}: {
isLoading: boolean
modelList: Model[]
modelList: ProviderWithModelsResponse[]
selectedModel: SkillBuilderModel | undefined
onSelect: (model: SkillBuilderModel) => void
}) {
@ -345,8 +339,13 @@ export function SkillBuilderPanel({
const selectedFileRef = useRef(selectedFile)
const assistAbortControllerRef = useRef<AbortController | null>(null)
const { data: defaultTextGenerationModel } = useDefaultModel(ModelTypeEnum.textGeneration)
const { data: textGenerationModelList, isLoading: isTextGenerationModelListLoading } =
useModelList(ModelTypeEnum.textGeneration)
const { data: textGenerationModelList = [], isPending: isTextGenerationModelListLoading } =
useQuery(
consoleQuery.workspaces.current.models.modelTypes.byModelType.get.queryOptions({
input: { params: { model_type: ModelTypeEnum.textGeneration } },
select: (response) => response.data,
}),
)
const fallbackModel = useMemo<SkillBuilderModel | undefined>(() => {
for (const provider of textGenerationModelList) {
if (provider.status !== ModelStatusEnum.active) continue

View File

@ -5,7 +5,6 @@ import type {
} from '@dify/contracts/api/console/workspaces/types.gen'
import type {
DefaultModelResponse,
Model,
ModelParameterRule,
ModelTypeEnum,
} from '@/app/components/header/account-setting/model-provider-page/declarations'
@ -153,10 +152,6 @@ export const activateMember = ({
return post<LoginResponse>(url, { body })
}
export const fetchModelList = (url: string): Promise<{ data: Model[] }> => {
return get<{ data: Model[] }>(url)
}
export const fetchDefaultModal = (url: string): Promise<{ data: DefaultModelResponse }> => {
return get<{ data: DefaultModelResponse }>(url)
}

View File

@ -1,7 +1,5 @@
import type { ModelType } from '@dify/contracts/api/console/workspaces/types.gen'
import type { FileTypesRes } from './datasets'
import type {
Model,
ModelParameterRule,
ModelProvider,
ModelTypeEnum,
@ -17,7 +15,6 @@ import type {
} from '@/models/common'
import { queryOptions, useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
import { discardRegistrationSessionState } from '@/app/components/base/amplitude/registration-session-state'
import { consoleQuery } from '@/service/console'
// oxlint-disable-next-line no-restricted-imports
import { get, post } from './base'
@ -211,20 +208,6 @@ export const useModelProviderDetails = (enabled = true) => {
})
}
export const useModelListByType = (type: ModelTypeEnum | ModelType, enabled = true) => {
return useQuery<{ data: Model[] }>({
queryKey: consoleQuery.workspaces.current.models.modelTypes.byModelType.get.queryKey({
input: {
params: {
model_type: type,
},
},
}),
queryFn: () => get<{ data: Model[] }>(`/workspaces/current/models/model-types/${type}`),
enabled,
})
}
export const useCodeBasedExtensions = (module: string) => {
return useQuery<CodeBasedExtension>({
queryKey: commonQueryKeys.codeBasedExtensions(module),