mirror of
https://github.com/langgenius/dify.git
synced 2026-10-16 15:35:32 +08:00
refactor(web): migrate model provider credentials to the console contract
Remove the forwarding hooks in service/use-models.ts. Credential queries and mutations use consoleQuery/consoleClient where they are consumed; useAuthService keeps owning the provider-vs-model dispatch. The load balancing modal normalizes the generated response into its form types at one boundary. The model credential query is skipped without a model instead of requesting `model=undefined`. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
b289d8161f
commit
66910779cc
@@ -4229,14 +4229,6 @@
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"web/service/use-models.ts": {
|
||||
"@tanstack/query/prefer-query-options": {
|
||||
"count": 3
|
||||
},
|
||||
"no-restricted-imports": {
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"web/service/use-pipeline.ts": {
|
||||
"@tanstack/query/prefer-query-options": {
|
||||
"count": 8
|
||||
|
||||
+143
-123
@@ -1,150 +1,170 @@
|
||||
import type { CustomModel } from '../../../declarations'
|
||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
|
||||
import { renderHook } from '@testing-library/react'
|
||||
import { QueryClient, QueryClientProvider, skipToken } from '@tanstack/react-query'
|
||||
import { act, renderHook } from '@testing-library/react'
|
||||
import { ModelTypeEnum } from '../../../declarations'
|
||||
import { useAuthService, useGetCredential } from '../use-auth-service'
|
||||
|
||||
vi.mock('@/service/use-models', () => ({
|
||||
useGetProviderCredential: vi.fn(),
|
||||
useGetModelCredential: vi.fn(),
|
||||
useAddProviderCredential: vi.fn(),
|
||||
useEditProviderCredential: vi.fn(),
|
||||
useDeleteProviderCredential: vi.fn(),
|
||||
useActiveProviderCredential: vi.fn(),
|
||||
useAddModelCredential: vi.fn(),
|
||||
useEditModelCredential: vi.fn(),
|
||||
useDeleteModelCredential: vi.fn(),
|
||||
useActiveModelCredential: vi.fn(),
|
||||
const mocks = vi.hoisted(() => ({
|
||||
providerCredentialOptions: vi.fn(),
|
||||
modelCredentialOptions: vi.fn(),
|
||||
addProviderCredential: vi.fn(),
|
||||
editProviderCredential: vi.fn(),
|
||||
deleteProviderCredential: vi.fn(),
|
||||
switchProviderCredential: vi.fn(),
|
||||
addModelCredential: vi.fn(),
|
||||
editModelCredential: vi.fn(),
|
||||
deleteModelCredential: vi.fn(),
|
||||
switchModelCredential: vi.fn(),
|
||||
}))
|
||||
|
||||
const {
|
||||
useGetProviderCredential,
|
||||
useGetModelCredential,
|
||||
useAddProviderCredential,
|
||||
useEditProviderCredential,
|
||||
useDeleteProviderCredential,
|
||||
useActiveProviderCredential,
|
||||
useAddModelCredential,
|
||||
useEditModelCredential,
|
||||
useDeleteModelCredential,
|
||||
useActiveModelCredential,
|
||||
} = await import('@/service/use-models')
|
||||
const { createQueryOptions } = vi.hoisted(() => ({
|
||||
createQueryOptions:
|
||||
(name: string, fetcher: (input: unknown) => void) =>
|
||||
({ input }: { input: unknown }) => {
|
||||
fetcher(input)
|
||||
return {
|
||||
queryKey: [name, input],
|
||||
queryFn: typeof input === 'symbol' ? input : () => ({ source: name }),
|
||||
}
|
||||
},
|
||||
}))
|
||||
|
||||
vi.mock('@/service/console', () => ({
|
||||
consoleQuery: {
|
||||
workspaces: {
|
||||
current: {
|
||||
modelProviders: {
|
||||
byProvider: {
|
||||
credentials: {
|
||||
get: {
|
||||
queryOptions: createQueryOptions('provider', mocks.providerCredentialOptions),
|
||||
},
|
||||
},
|
||||
models: {
|
||||
credentials: {
|
||||
get: { queryOptions: createQueryOptions('model', mocks.modelCredentialOptions) },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
consoleClient: {
|
||||
workspaces: {
|
||||
current: {
|
||||
modelProviders: {
|
||||
byProvider: {
|
||||
credentials: {
|
||||
post: mocks.addProviderCredential,
|
||||
put: mocks.editProviderCredential,
|
||||
delete: mocks.deleteProviderCredential,
|
||||
switch: { post: mocks.switchProviderCredential },
|
||||
},
|
||||
models: {
|
||||
credentials: {
|
||||
post: mocks.addModelCredential,
|
||||
put: mocks.editModelCredential,
|
||||
delete: mocks.deleteModelCredential,
|
||||
switch: { post: mocks.switchModelCredential },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}))
|
||||
|
||||
// The services are selected at runtime, so call sites pass a payload of either shape.
|
||||
type CredentialService = (payload: unknown) => Promise<unknown>
|
||||
|
||||
describe('useAuthService hooks', () => {
|
||||
let queryClient: QueryClient
|
||||
const wrapper = ({ children }: { children: React.ReactNode }) => (
|
||||
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
|
||||
)
|
||||
const model: CustomModel = { model: 'gpt-4', model_type: ModelTypeEnum.textGeneration }
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
queryClient = new QueryClient({ defaultOptions: { queries: { retry: false } } })
|
||||
|
||||
const mockMutationReturn = { mutateAsync: vi.fn() }
|
||||
vi.mocked(useAddProviderCredential).mockReturnValue(
|
||||
mockMutationReturn as unknown as ReturnType<typeof useAddProviderCredential>,
|
||||
)
|
||||
vi.mocked(useEditProviderCredential).mockReturnValue(
|
||||
mockMutationReturn as unknown as ReturnType<typeof useEditProviderCredential>,
|
||||
)
|
||||
vi.mocked(useDeleteProviderCredential).mockReturnValue(
|
||||
mockMutationReturn as unknown as ReturnType<typeof useDeleteProviderCredential>,
|
||||
)
|
||||
vi.mocked(useActiveProviderCredential).mockReturnValue(
|
||||
mockMutationReturn as unknown as ReturnType<typeof useActiveProviderCredential>,
|
||||
)
|
||||
vi.mocked(useAddModelCredential).mockReturnValue(
|
||||
mockMutationReturn as unknown as ReturnType<typeof useAddModelCredential>,
|
||||
)
|
||||
vi.mocked(useEditModelCredential).mockReturnValue(
|
||||
mockMutationReturn as unknown as ReturnType<typeof useEditModelCredential>,
|
||||
)
|
||||
vi.mocked(useDeleteModelCredential).mockReturnValue(
|
||||
mockMutationReturn as unknown as ReturnType<typeof useDeleteModelCredential>,
|
||||
)
|
||||
vi.mocked(useActiveModelCredential).mockReturnValue(
|
||||
mockMutationReturn as unknown as ReturnType<typeof useActiveModelCredential>,
|
||||
)
|
||||
})
|
||||
|
||||
it('useGetCredential selects correct source and params', () => {
|
||||
const mockData = { data: 'test' }
|
||||
vi.mocked(useGetProviderCredential).mockReturnValue(
|
||||
mockData as unknown as ReturnType<typeof useGetProviderCredential>,
|
||||
)
|
||||
vi.mocked(useGetModelCredential).mockReturnValue(
|
||||
mockData as unknown as ReturnType<typeof useGetModelCredential>,
|
||||
)
|
||||
it('should query the provider credential only when a credential id is given', () => {
|
||||
renderHook(() => useGetCredential('openai', false, 'cred-123'), { wrapper })
|
||||
expect(mocks.providerCredentialOptions).toHaveBeenCalledWith({
|
||||
params: { provider: 'openai' },
|
||||
query: { credential_id: 'cred-123' },
|
||||
})
|
||||
|
||||
// Provider case
|
||||
const { result: providerRes } = renderHook(
|
||||
() => useGetCredential('openai', false, 'cred-123'),
|
||||
{ wrapper },
|
||||
)
|
||||
expect(useGetProviderCredential).toHaveBeenCalledWith(true, 'openai', 'cred-123')
|
||||
expect(providerRes.current).toBe(mockData)
|
||||
|
||||
// Model case
|
||||
const mockModel = { model: 'gpt-4', model_type: ModelTypeEnum.textGeneration } as CustomModel
|
||||
const { result: modelRes } = renderHook(
|
||||
() => useGetCredential('openai', true, 'cred-123', mockModel, 'src'),
|
||||
{ wrapper },
|
||||
)
|
||||
expect(useGetModelCredential).toHaveBeenCalledWith(
|
||||
true,
|
||||
'openai',
|
||||
'cred-123',
|
||||
'gpt-4',
|
||||
ModelTypeEnum.textGeneration,
|
||||
'src',
|
||||
)
|
||||
expect(modelRes.current).toBe(mockData)
|
||||
|
||||
// Early return cases
|
||||
renderHook(() => useGetCredential('openai', false), { wrapper })
|
||||
expect(useGetProviderCredential).toHaveBeenCalledWith(false, 'openai', undefined)
|
||||
|
||||
// Branch: isModelCredential true but no id/model
|
||||
renderHook(() => useGetCredential('openai', true), { wrapper })
|
||||
expect(useGetModelCredential).toHaveBeenCalledWith(
|
||||
false,
|
||||
'openai',
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
)
|
||||
expect(mocks.providerCredentialOptions).toHaveBeenLastCalledWith(skipToken)
|
||||
})
|
||||
|
||||
it('useAuthService provides correct services for provider and model', () => {
|
||||
it('should query the model credential only when a model is given', () => {
|
||||
renderHook(() => useGetCredential('openai', true, 'cred-123', model, 'custom-model'), {
|
||||
wrapper,
|
||||
})
|
||||
expect(mocks.modelCredentialOptions).toHaveBeenCalledWith({
|
||||
params: { provider: 'openai' },
|
||||
query: {
|
||||
model: 'gpt-4',
|
||||
model_type: ModelTypeEnum.textGeneration,
|
||||
config_from: 'custom-model',
|
||||
credential_id: 'cred-123',
|
||||
},
|
||||
})
|
||||
|
||||
renderHook(() => useGetCredential('openai', true, 'cred-123'), { wrapper })
|
||||
expect(mocks.modelCredentialOptions).toHaveBeenLastCalledWith(skipToken)
|
||||
})
|
||||
|
||||
it('should send provider credential changes to the provider endpoints', async () => {
|
||||
const { result } = renderHook(() => useAuthService('openai'), { wrapper })
|
||||
const credential = { credential_id: 'cred-1', name: 'Main', credentials: { key: 'v' } }
|
||||
|
||||
// Provider services
|
||||
expect(result.current.getAddCredentialService(false)).toBe(
|
||||
vi.mocked(useAddProviderCredential).mock.results[0]!.value.mutateAsync,
|
||||
)
|
||||
expect(result.current.getEditCredentialService(false)).toBe(
|
||||
vi.mocked(useEditProviderCredential).mock.results[0]!.value.mutateAsync,
|
||||
)
|
||||
expect(result.current.getDeleteCredentialService(false)).toBe(
|
||||
vi.mocked(useDeleteProviderCredential).mock.results[0]!.value.mutateAsync,
|
||||
)
|
||||
expect(result.current.getActiveCredentialService(false)).toBe(
|
||||
vi.mocked(useActiveProviderCredential).mock.results[0]!.value.mutateAsync,
|
||||
)
|
||||
await act(async () => {
|
||||
await (result.current.getAddCredentialService(false) as CredentialService)(credential)
|
||||
await (result.current.getEditCredentialService(false) as CredentialService)(credential)
|
||||
await (result.current.getDeleteCredentialService(false) as CredentialService)({
|
||||
credential_id: 'cred-1',
|
||||
})
|
||||
await (result.current.getActiveCredentialService(false) as CredentialService)({
|
||||
credential_id: 'cred-1',
|
||||
})
|
||||
})
|
||||
|
||||
// Model services
|
||||
expect(result.current.getAddCredentialService(true)).toBe(
|
||||
vi.mocked(useAddModelCredential).mock.results[0]!.value.mutateAsync,
|
||||
)
|
||||
expect(result.current.getEditCredentialService(true)).toBe(
|
||||
vi.mocked(useEditModelCredential).mock.results[0]!.value.mutateAsync,
|
||||
)
|
||||
expect(result.current.getDeleteCredentialService(true)).toBe(
|
||||
vi.mocked(useDeleteModelCredential).mock.results[0]!.value.mutateAsync,
|
||||
)
|
||||
expect(result.current.getActiveCredentialService(true)).toBe(
|
||||
vi.mocked(useActiveModelCredential).mock.results[0]!.value.mutateAsync,
|
||||
)
|
||||
const params = { provider: 'openai' }
|
||||
expect(mocks.addProviderCredential).toHaveBeenCalledWith({ params, body: credential })
|
||||
expect(mocks.editProviderCredential).toHaveBeenCalledWith({ params, body: credential })
|
||||
expect(mocks.deleteProviderCredential).toHaveBeenCalledWith({
|
||||
params,
|
||||
body: { credential_id: 'cred-1' },
|
||||
})
|
||||
expect(mocks.switchProviderCredential).toHaveBeenCalledWith({
|
||||
params,
|
||||
body: { credential_id: 'cred-1' },
|
||||
})
|
||||
})
|
||||
|
||||
it('should send model credential changes to the model endpoints', async () => {
|
||||
const { result } = renderHook(() => useAuthService('openai'), { wrapper })
|
||||
const credential = { ...model, credential_id: 'cred-1', credentials: { key: 'v' } }
|
||||
const ref = { ...model, credential_id: 'cred-1' }
|
||||
|
||||
await act(async () => {
|
||||
await (result.current.getAddCredentialService(true) as CredentialService)(credential)
|
||||
await (result.current.getEditCredentialService(true) as CredentialService)(credential)
|
||||
await (result.current.getDeleteCredentialService(true) as CredentialService)(ref)
|
||||
await (result.current.getActiveCredentialService(true) as CredentialService)(ref)
|
||||
})
|
||||
|
||||
const params = { provider: 'openai' }
|
||||
expect(mocks.addModelCredential).toHaveBeenCalledWith({ params, body: credential })
|
||||
expect(mocks.editModelCredential).toHaveBeenCalledWith({ params, body: credential })
|
||||
// The model credential delete endpoint takes its fields as query parameters.
|
||||
expect(mocks.deleteModelCredential).toHaveBeenCalledWith({ params, query: ref })
|
||||
expect(mocks.switchModelCredential).toHaveBeenCalledWith({ params, body: ref })
|
||||
})
|
||||
})
|
||||
|
||||
+15
-4
@@ -38,8 +38,19 @@ vi.mock('@/app/components/header/account-setting/model-provider-page/hooks', ()
|
||||
useRefreshModel: () => ({ handleRefreshModel: mockHandleRefreshModel }),
|
||||
}))
|
||||
|
||||
vi.mock('@/service/use-models', () => ({
|
||||
useDeleteModel: () => ({ mutateAsync: mockDeleteModelService }),
|
||||
vi.mock('@/service/console', () => ({
|
||||
consoleQuery: {
|
||||
workspaces: {
|
||||
current: {
|
||||
modelProviders: { byProvider: { models: { delete: { mutationOptions: () => ({}) } } } },
|
||||
},
|
||||
},
|
||||
},
|
||||
}))
|
||||
|
||||
vi.mock('@tanstack/react-query', async (importOriginal) => ({
|
||||
...(await importOriginal<typeof import('@tanstack/react-query')>()),
|
||||
useMutation: () => ({ mutateAsync: mockDeleteModelService }),
|
||||
}))
|
||||
|
||||
vi.mock('../use-auth-service', () => ({
|
||||
@@ -204,8 +215,8 @@ describe('useAuth', () => {
|
||||
})
|
||||
|
||||
expect(mockDeleteModelService).toHaveBeenCalledWith({
|
||||
model: 'gpt-4',
|
||||
model_type: ModelTypeEnum.textGeneration,
|
||||
params: { provider: 'openai' },
|
||||
query: { model: 'gpt-4', model_type: ModelTypeEnum.textGeneration },
|
||||
})
|
||||
expect(onRemove).toHaveBeenCalledWith('')
|
||||
})
|
||||
|
||||
+69
-34
@@ -1,17 +1,14 @@
|
||||
import type { CustomModel } from '@/app/components/header/account-setting/model-provider-page/declarations'
|
||||
import type {
|
||||
CustomModel,
|
||||
ModelCredentialPayload,
|
||||
ModelTypeEnum,
|
||||
ProviderCredential,
|
||||
} from '@/app/components/header/account-setting/model-provider-page/declarations'
|
||||
import { skipToken, useMutation, useQuery } from '@tanstack/react-query'
|
||||
import { useCallback } from 'react'
|
||||
import {
|
||||
useActiveModelCredential,
|
||||
useActiveProviderCredential,
|
||||
useAddModelCredential,
|
||||
useAddProviderCredential,
|
||||
useDeleteModelCredential,
|
||||
useDeleteProviderCredential,
|
||||
useEditModelCredential,
|
||||
useEditProviderCredential,
|
||||
useGetModelCredential,
|
||||
useGetProviderCredential,
|
||||
} from '@/service/use-models'
|
||||
import { consoleClient, consoleQuery } from '@/service/console'
|
||||
|
||||
type ModelCredentialRef = { credential_id: string; model: string; model_type: ModelTypeEnum }
|
||||
|
||||
export const useGetCredential = (
|
||||
provider: string,
|
||||
@@ -20,32 +17,70 @@ export const useGetCredential = (
|
||||
model?: CustomModel,
|
||||
configFrom?: string,
|
||||
) => {
|
||||
const providerData = useGetProviderCredential(
|
||||
!isModelCredential && !!credentialId,
|
||||
provider,
|
||||
credentialId,
|
||||
)
|
||||
const modelData = useGetModelCredential(
|
||||
!!isModelCredential && (!!credentialId || !!model),
|
||||
provider,
|
||||
credentialId,
|
||||
model?.model,
|
||||
model?.model_type,
|
||||
configFrom,
|
||||
)
|
||||
const modelProvider = consoleQuery.workspaces.current.modelProviders.byProvider
|
||||
const providerData = useQuery({
|
||||
...modelProvider.credentials.get.queryOptions({
|
||||
input:
|
||||
!isModelCredential && credentialId
|
||||
? { params: { provider }, query: { credential_id: credentialId } }
|
||||
: skipToken,
|
||||
}),
|
||||
staleTime: 0,
|
||||
gcTime: 0,
|
||||
})
|
||||
const modelData = useQuery({
|
||||
...modelProvider.models.credentials.get.queryOptions({
|
||||
input:
|
||||
isModelCredential && model
|
||||
? {
|
||||
params: { provider },
|
||||
query: {
|
||||
model: model.model,
|
||||
model_type: model.model_type,
|
||||
config_from: configFrom,
|
||||
credential_id: credentialId,
|
||||
},
|
||||
}
|
||||
: skipToken,
|
||||
}),
|
||||
staleTime: 0,
|
||||
gcTime: 0,
|
||||
})
|
||||
return isModelCredential ? modelData : providerData
|
||||
}
|
||||
|
||||
export const useAuthService = (provider: string) => {
|
||||
const { mutateAsync: addProviderCredential } = useAddProviderCredential(provider)
|
||||
const { mutateAsync: editProviderCredential } = useEditProviderCredential(provider)
|
||||
const { mutateAsync: deleteProviderCredential } = useDeleteProviderCredential(provider)
|
||||
const { mutateAsync: activeProviderCredential } = useActiveProviderCredential(provider)
|
||||
const client = consoleClient.workspaces.current.modelProviders.byProvider
|
||||
const params = { provider }
|
||||
|
||||
const { mutateAsync: addModelCredential } = useAddModelCredential(provider)
|
||||
const { mutateAsync: activeModelCredential } = useActiveModelCredential(provider)
|
||||
const { mutateAsync: deleteModelCredential } = useDeleteModelCredential(provider)
|
||||
const { mutateAsync: editModelCredential } = useEditModelCredential(provider)
|
||||
const { mutateAsync: addProviderCredential } = useMutation({
|
||||
mutationFn: (body: ProviderCredential) => client.credentials.post({ params, body }),
|
||||
})
|
||||
const { mutateAsync: editProviderCredential } = useMutation({
|
||||
mutationFn: (body: ProviderCredential) => client.credentials.put({ params, body }),
|
||||
})
|
||||
const { mutateAsync: deleteProviderCredential } = useMutation({
|
||||
mutationFn: (body: { credential_id: string }) => client.credentials.delete({ params, body }),
|
||||
})
|
||||
const { mutateAsync: activeProviderCredential } = useMutation({
|
||||
mutationFn: (body: { credential_id: string }) =>
|
||||
client.credentials.switch.post({ params, body }),
|
||||
})
|
||||
|
||||
const { mutateAsync: addModelCredential } = useMutation({
|
||||
mutationFn: (body: ModelCredentialPayload) => client.models.credentials.post({ params, body }),
|
||||
})
|
||||
const { mutateAsync: activeModelCredential } = useMutation({
|
||||
mutationFn: (body: ModelCredentialRef) =>
|
||||
client.models.credentials.switch.post({ params, body }),
|
||||
})
|
||||
const { mutateAsync: deleteModelCredential } = useMutation({
|
||||
mutationFn: (query: ModelCredentialRef) => client.models.credentials.delete({ params, query }),
|
||||
})
|
||||
const { mutateAsync: editModelCredential } = useMutation({
|
||||
mutationFn: (body: ModelCredentialPayload & { credential_id: string }) =>
|
||||
client.models.credentials.put({ params, body }),
|
||||
})
|
||||
|
||||
const getAddCredentialService = useCallback(
|
||||
(isModel: boolean) => {
|
||||
|
||||
+6
-3
@@ -6,6 +6,7 @@ import type {
|
||||
ModelModalModeEnum,
|
||||
ModelProvider,
|
||||
} from '../../declarations'
|
||||
import { useMutation } from '@tanstack/react-query'
|
||||
import { useCallback, useRef, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import {
|
||||
@@ -13,7 +14,7 @@ import {
|
||||
useRefreshModel,
|
||||
} from '@/app/components/header/account-setting/model-provider-page/hooks'
|
||||
import { toast } from '@/app/notifications'
|
||||
import { useDeleteModel } from '@/service/use-models'
|
||||
import { consoleQuery } from '@/service/console'
|
||||
import { useAuthService } from './use-auth-service'
|
||||
|
||||
type ProviderCredentialOperationPayload = {
|
||||
@@ -45,7 +46,9 @@ export const useAuth = (
|
||||
getEditCredentialService,
|
||||
getAddCredentialService,
|
||||
} = useAuthService(provider.provider)
|
||||
const { mutateAsync: deleteModelService } = useDeleteModel(provider.provider)
|
||||
const { mutateAsync: deleteModelService } = useMutation(
|
||||
consoleQuery.workspaces.current.modelProviders.byProvider.models.delete.mutationOptions(),
|
||||
)
|
||||
const handleOpenModelModal = useModelModalHandler()
|
||||
const { handleRefreshModel } = useRefreshModel()
|
||||
const pendingOperationCredentialId = useRef<string | null>(null)
|
||||
@@ -156,7 +159,7 @@ export const useAuth = (
|
||||
model: pendingOperationModel.current.model,
|
||||
model_type: pendingOperationModel.current.model_type,
|
||||
}
|
||||
await deleteModelService(payload)
|
||||
await deleteModelService({ params: { provider: provider.provider }, query: payload })
|
||||
}
|
||||
handleRefreshModel(provider, undefined, true)
|
||||
onRemove?.(pendingOperationCredentialId.current ?? '')
|
||||
|
||||
+8
-2
@@ -22,10 +22,16 @@ import ModelLoadBalancingConfigs from '../model-load-balancing-configs'
|
||||
|
||||
const { mockPost } = vi.hoisted(() => ({ mockPost: vi.fn() }))
|
||||
|
||||
// Requests go through the console client, whose browser transport uses `request`.
|
||||
vi.mock('@/service/base', async (importOriginal) => ({
|
||||
...(await importOriginal<typeof import('@/service/base')>()),
|
||||
get: vi.fn().mockResolvedValue({ credentials: {} }),
|
||||
post: mockPost,
|
||||
request: async (_url: string, _init: RequestInit, { request }: { request: Request }) => {
|
||||
if (request.method !== 'POST') return Response.json({ credentials: {} })
|
||||
mockPost(new URL(request.url).pathname.replace(/^\/console\/api/, ''), {
|
||||
body: await request.json(),
|
||||
})
|
||||
return Response.json({ result: 'success' })
|
||||
},
|
||||
}))
|
||||
|
||||
vi.mock('../../hooks', async (importOriginal) => ({
|
||||
|
||||
+36
-9
@@ -72,17 +72,41 @@ vi.mock('@/app/notifications', async (importOriginal) => {
|
||||
}
|
||||
})
|
||||
|
||||
vi.mock('@/service/use-models', () => ({
|
||||
useGetModelCredential: () => ({
|
||||
isLoading: false,
|
||||
data: mockCredentialData,
|
||||
refetch: mockRefetch,
|
||||
}),
|
||||
useUpdateModelLoadBalancingConfig: () => ({
|
||||
mutateAsync: mockMutateAsync,
|
||||
}),
|
||||
vi.mock('@/service/console', () => ({
|
||||
consoleQuery: {
|
||||
workspaces: {
|
||||
current: {
|
||||
modelProviders: {
|
||||
byProvider: {
|
||||
models: {
|
||||
credentials: { get: { queryOptions: () => ({ operation: 'model-credential' }) } },
|
||||
post: { mutationOptions: () => ({ operation: 'update-load-balancing' }) },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}))
|
||||
|
||||
vi.mock('@tanstack/react-query', async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import('@tanstack/react-query')>()
|
||||
return {
|
||||
...actual,
|
||||
useQuery: (options: { operation?: string }) =>
|
||||
options.operation === 'model-credential'
|
||||
? { isLoading: false, data: mockCredentialData, refetch: mockRefetch }
|
||||
: actual.useQuery(options as Parameters<typeof actual.useQuery>[0]),
|
||||
useMutation: (options: { operation?: string }) =>
|
||||
options.operation === 'update-load-balancing'
|
||||
? {
|
||||
mutateAsync: ({ params, body }: { params: { provider: string }; body: unknown }) =>
|
||||
mockMutateAsync(body, params),
|
||||
}
|
||||
: actual.useMutation(options as Parameters<typeof actual.useMutation>[0]),
|
||||
}
|
||||
})
|
||||
|
||||
vi.mock('../../model-auth/hooks/use-auth', () => ({
|
||||
useAuth: () => ({
|
||||
doingAction: false,
|
||||
@@ -274,6 +298,9 @@ describe('ModelLoadBalancingModal', () => {
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockRefetch).toHaveBeenCalled()
|
||||
expect(mockMutateAsync).toHaveBeenCalledWith(expect.anything(), {
|
||||
provider: 'test-provider',
|
||||
})
|
||||
expect(mockMutateAsync).toHaveBeenCalled()
|
||||
const payload = mockMutateAsync.mock.calls[0]![0] as {
|
||||
load_balancing: { configs: Array<{ credentials: { api_key: string } }> }
|
||||
|
||||
+16
-6
@@ -8,11 +8,21 @@ const mockUpdateModelProviders = vi.fn()
|
||||
const mockUpdateModelList = vi.fn()
|
||||
let mockIsPending = false
|
||||
|
||||
vi.mock('@/service/use-models', () => ({
|
||||
useActiveProviderCredential: () => ({
|
||||
mutate: mockMutate,
|
||||
isPending: mockIsPending,
|
||||
}),
|
||||
vi.mock('@/service/console', () => ({
|
||||
consoleQuery: {
|
||||
workspaces: {
|
||||
current: {
|
||||
modelProviders: {
|
||||
byProvider: { credentials: { switch: { post: { mutationOptions: () => ({}) } } } },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}))
|
||||
|
||||
vi.mock('@tanstack/react-query', async (importOriginal) => ({
|
||||
...(await importOriginal<typeof import('@tanstack/react-query')>()),
|
||||
useMutation: () => ({ mutate: mockMutate, isPending: mockIsPending }),
|
||||
}))
|
||||
|
||||
vi.mock('../../../hooks', () => ({
|
||||
@@ -85,7 +95,7 @@ describe('useActivateCredential', () => {
|
||||
|
||||
expect(result.current.selectedCredentialId).toBe('cred-2')
|
||||
expect(mockMutate).toHaveBeenCalledWith(
|
||||
{ credential_id: 'cred-2' },
|
||||
{ params: { provider: 'langgenius/openai/openai' }, body: { credential_id: 'cred-2' } },
|
||||
expect.objectContaining({
|
||||
onSuccess: expect.any(Function),
|
||||
onError: expect.any(Function),
|
||||
|
||||
+10
-4
@@ -1,15 +1,18 @@
|
||||
import type { Credential, ModelProvider } from '../../declarations'
|
||||
import { useMutation } from '@tanstack/react-query'
|
||||
import { useCallback, useEffect, useRef, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { toast } from '@/app/notifications'
|
||||
import { useActiveProviderCredential } from '@/service/use-models'
|
||||
import { consoleQuery } from '@/service/console'
|
||||
import { useUpdateModelList, useUpdateModelProviders } from '../../hooks'
|
||||
|
||||
export function useActivateCredential(provider: ModelProvider) {
|
||||
const { t } = useTranslation(['common'])
|
||||
const updateModelProviders = useUpdateModelProviders()
|
||||
const updateModelList = useUpdateModelList()
|
||||
const { mutate, isPending } = useActiveProviderCredential(provider.provider)
|
||||
const { mutate, isPending } = useMutation(
|
||||
consoleQuery.workspaces.current.modelProviders.byProvider.credentials.switch.post.mutationOptions(),
|
||||
)
|
||||
const [optimisticId, setOptimisticId] = useState<string>()
|
||||
const currentId = provider.custom_configuration.current_credential_id
|
||||
const selectedCredentialId = optimisticId ?? currentId
|
||||
@@ -25,7 +28,10 @@ export function useActivateCredential(provider: ModelProvider) {
|
||||
if (credential.credential_id === selectedIdRef.current) return
|
||||
setOptimisticId(credential.credential_id)
|
||||
mutate(
|
||||
{ credential_id: credential.credential_id },
|
||||
{
|
||||
params: { provider: provider.provider },
|
||||
body: { credential_id: credential.credential_id },
|
||||
},
|
||||
{
|
||||
onSuccess: () => {
|
||||
updateModelProviders()
|
||||
@@ -38,7 +44,7 @@ export function useActivateCredential(provider: ModelProvider) {
|
||||
},
|
||||
)
|
||||
},
|
||||
[mutate, t, updateModelProviders, updateModelList],
|
||||
[mutate, provider.provider, t, updateModelProviders, updateModelList],
|
||||
)
|
||||
return {
|
||||
selectedCredentialId,
|
||||
|
||||
+50
-19
@@ -1,6 +1,7 @@
|
||||
import type {
|
||||
Credential,
|
||||
CustomConfigurationModelFixedFields,
|
||||
ModelCredential,
|
||||
ModelItem,
|
||||
ModelLoadBalancingConfig,
|
||||
ModelLoadBalancingConfigEntry,
|
||||
@@ -17,12 +18,13 @@ import {
|
||||
import { Button } from '@langgenius/dify-ui/button'
|
||||
import { cn } from '@langgenius/dify-ui/cn'
|
||||
import { Dialog, DialogContent, DialogTitle } from '@langgenius/dify-ui/dialog'
|
||||
import { useMutation, useQuery } from '@tanstack/react-query'
|
||||
import { memo, useCallback, useEffect, useId, useMemo, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { LoadingPlaceholder } from '@/app/components/base/loading-placeholder'
|
||||
import { SwitchCredentialInLoadBalancing } from '@/app/components/header/account-setting/model-provider-page/model-auth'
|
||||
import { toast } from '@/app/notifications'
|
||||
import { useGetModelCredential, useUpdateModelLoadBalancingConfig } from '@/service/use-models'
|
||||
import { consoleQuery } from '@/service/console'
|
||||
import { ConfigurationMethodEnum, FormTypeEnum } from '../declarations'
|
||||
import { useRefreshModel } from '../hooks'
|
||||
import { useAuth } from '../model-auth/hooks/use-auth'
|
||||
@@ -60,15 +62,41 @@ const ModelLoadBalancingModal = ({
|
||||
const [loading, setLoading] = useState(false)
|
||||
const providerFormSchemaPredefined = configurateMethod === ConfigurationMethodEnum.predefinedModel
|
||||
const configFrom = providerFormSchemaPredefined ? 'predefined-model' : 'custom-model'
|
||||
const { isLoading, data, refetch } = useGetModelCredential(
|
||||
true,
|
||||
provider.provider,
|
||||
credential?.credential_id,
|
||||
model.model,
|
||||
model.model_type,
|
||||
configFrom,
|
||||
const { isLoading, data, refetch } = useQuery({
|
||||
...consoleQuery.workspaces.current.modelProviders.byProvider.models.credentials.get.queryOptions(
|
||||
{
|
||||
input: {
|
||||
params: { provider: provider.provider },
|
||||
query: {
|
||||
model: model.model,
|
||||
model_type: model.model_type,
|
||||
config_from: configFrom,
|
||||
credential_id: credential?.credential_id,
|
||||
},
|
||||
},
|
||||
},
|
||||
),
|
||||
staleTime: 0,
|
||||
gcTime: 0,
|
||||
})
|
||||
const modelCredential = useMemo<ModelCredential | undefined>(
|
||||
() =>
|
||||
data && {
|
||||
...data,
|
||||
current_credential_id: data.current_credential_id ?? undefined,
|
||||
current_credential_name: data.current_credential_name ?? undefined,
|
||||
load_balancing: {
|
||||
enabled: data.load_balancing.enabled,
|
||||
configs: data.load_balancing.configs.map((config) => ({
|
||||
...config,
|
||||
credential_id: config.credential_id ?? undefined,
|
||||
// Credential values are untyped JSON form values in the console API response.
|
||||
credentials: config.credentials as ModelLoadBalancingConfigEntry['credentials'],
|
||||
})),
|
||||
},
|
||||
},
|
||||
[data],
|
||||
)
|
||||
const modelCredential = data
|
||||
const { load_balancing, current_credential_id, available_credentials, current_credential_name } =
|
||||
modelCredential ?? {}
|
||||
const originalConfig = load_balancing
|
||||
@@ -129,8 +157,8 @@ const ModelLoadBalancingModal = ({
|
||||
},
|
||||
[extendedSecretFormSchemas, originalConfigMap],
|
||||
)
|
||||
const { mutateAsync: updateModelLoadBalancingConfig } = useUpdateModelLoadBalancingConfig(
|
||||
provider.provider,
|
||||
const { mutateAsync: updateModelLoadBalancingConfig } = useMutation(
|
||||
consoleQuery.workspaces.current.modelProviders.byProvider.models.post.mutationOptions(),
|
||||
)
|
||||
const initialCustomModelCredential = useMemo(() => {
|
||||
if (!current_credential_id) return undefined
|
||||
@@ -147,14 +175,17 @@ const ModelLoadBalancingModal = ({
|
||||
try {
|
||||
setLoading(true)
|
||||
const res = await updateModelLoadBalancingConfig({
|
||||
credential_id: customModelCredential?.credential_id || current_credential_id,
|
||||
config_from: configFrom,
|
||||
model: model.model,
|
||||
model_type: model.model_type,
|
||||
load_balancing: {
|
||||
...draftConfig,
|
||||
configs: draftConfig!.configs.map(encodeConfigEntrySecretValues),
|
||||
enabled: Boolean(draftConfig?.enabled),
|
||||
params: { provider: provider.provider },
|
||||
body: {
|
||||
credential_id: customModelCredential?.credential_id || current_credential_id,
|
||||
config_from: configFrom,
|
||||
model: model.model,
|
||||
model_type: model.model_type,
|
||||
load_balancing: {
|
||||
...draftConfig,
|
||||
configs: draftConfig!.configs.map(encodeConfigEntrySecretValues),
|
||||
enabled: Boolean(draftConfig?.enabled),
|
||||
},
|
||||
},
|
||||
})
|
||||
if (res.result === 'success') {
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import type { PluginDetail } from '@/app/components/plugins/types'
|
||||
import { useQuery } from '@tanstack/react-query'
|
||||
import * as React from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import ModelIcon from '@/app/components/header/account-setting/model-provider-page/model-icon'
|
||||
import ModelName from '@/app/components/header/account-setting/model-provider-page/model-name'
|
||||
import { useModelProviderModelList } from '@/service/use-models'
|
||||
import { consoleQuery } from '@/service/console'
|
||||
|
||||
type Props = Readonly<{
|
||||
detail: PluginDetail
|
||||
@@ -11,8 +12,10 @@ type Props = Readonly<{
|
||||
|
||||
const ModelList = ({ detail }: Props) => {
|
||||
const { t } = useTranslation(['plugin'])
|
||||
const { data: res } = useModelProviderModelList(
|
||||
`${detail.plugin_id}/${detail.declaration.model.provider}`,
|
||||
const { data: res } = useQuery(
|
||||
consoleQuery.workspaces.current.modelProviders.byProvider.models.get.queryOptions({
|
||||
input: { params: { provider: `${detail.plugin_id}/${detail.declaration.model.provider}` } },
|
||||
}),
|
||||
)
|
||||
|
||||
if (!res) return null
|
||||
|
||||
@@ -1,76 +0,0 @@
|
||||
import type { ReactNode } from 'react'
|
||||
import type { ProviderCredential } from '@/app/components/header/account-setting/model-provider-page/declarations'
|
||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
|
||||
import { renderHook, waitFor } from '@testing-library/react'
|
||||
import { useGetProviderCredential } from '../use-models'
|
||||
|
||||
const mockGet = vi.hoisted(() => vi.fn())
|
||||
|
||||
vi.mock('../base', () => ({
|
||||
del: vi.fn(),
|
||||
get: mockGet,
|
||||
post: vi.fn(),
|
||||
put: vi.fn(),
|
||||
}))
|
||||
|
||||
const createWrapper = () => {
|
||||
const queryClient = new QueryClient({
|
||||
defaultOptions: {
|
||||
queries: {
|
||||
retry: false,
|
||||
staleTime: 1000 * 60 * 5,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
return ({ children }: { children: ReactNode }) => (
|
||||
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
|
||||
)
|
||||
}
|
||||
|
||||
describe('useGetProviderCredential', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
it('should refetch credential details after the editor is reopened', async () => {
|
||||
const previousCredential: ProviderCredential = {
|
||||
credential_id: 'credential-id',
|
||||
name: 'Tongyi',
|
||||
credentials: {
|
||||
api_key: '[__HIDDEN__]',
|
||||
endpoint: 'https://old.example.com',
|
||||
enable_request_metadata: false,
|
||||
},
|
||||
}
|
||||
const updatedCredential: ProviderCredential = {
|
||||
...previousCredential,
|
||||
credentials: {
|
||||
api_key: '[__HIDDEN__]',
|
||||
endpoint: 'https://new.example.com',
|
||||
enable_request_metadata: true,
|
||||
},
|
||||
}
|
||||
mockGet.mockResolvedValueOnce(previousCredential).mockResolvedValueOnce(updatedCredential)
|
||||
const wrapper = createWrapper()
|
||||
|
||||
const firstEditor = renderHook(
|
||||
() => useGetProviderCredential(true, 'langgenius/tongyi/tongyi', 'credential-id'),
|
||||
{ wrapper },
|
||||
)
|
||||
await waitFor(() => {
|
||||
expect(firstEditor.result.current.data).toEqual(previousCredential)
|
||||
})
|
||||
firstEditor.unmount()
|
||||
|
||||
const reopenedEditor = renderHook(
|
||||
() => useGetProviderCredential(true, 'langgenius/tongyi/tongyi', 'credential-id'),
|
||||
{ wrapper },
|
||||
)
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockGet).toHaveBeenCalledTimes(2)
|
||||
expect(reopenedEditor.result.current.data).toEqual(updatedCredential)
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -1,172 +0,0 @@
|
||||
import type {
|
||||
ModelCredential,
|
||||
ModelCredentialPayload,
|
||||
ModelItem,
|
||||
ModelLoadBalancingConfig,
|
||||
ModelTypeEnum,
|
||||
ProviderCredential,
|
||||
} from '@/app/components/header/account-setting/model-provider-page/declarations'
|
||||
import {
|
||||
useMutation,
|
||||
useQuery,
|
||||
// useQueryClient,
|
||||
} from '@tanstack/react-query'
|
||||
import { del, get, post, put } from './base'
|
||||
|
||||
const NAME_SPACE = 'models'
|
||||
|
||||
export const useModelProviderModelList = (provider: string) => {
|
||||
return useQuery({
|
||||
queryKey: [NAME_SPACE, 'model-list', provider],
|
||||
queryFn: () =>
|
||||
get<{ data: ModelItem[] }>(`/workspaces/current/model-providers/${provider}/models`),
|
||||
})
|
||||
}
|
||||
|
||||
export const useGetProviderCredential = (
|
||||
enabled: boolean,
|
||||
provider: string,
|
||||
credentialId?: string,
|
||||
) => {
|
||||
return useQuery({
|
||||
enabled,
|
||||
queryKey: [NAME_SPACE, 'model-list', provider, credentialId],
|
||||
queryFn: () =>
|
||||
get<ProviderCredential>(
|
||||
`/workspaces/current/model-providers/${provider}/credentials${credentialId ? `?credential_id=${credentialId}` : ''}`,
|
||||
),
|
||||
staleTime: 0,
|
||||
gcTime: 0,
|
||||
})
|
||||
}
|
||||
|
||||
export const useAddProviderCredential = (provider: string) => {
|
||||
return useMutation({
|
||||
mutationFn: (data: ProviderCredential) =>
|
||||
post<{ result: string }>(`/workspaces/current/model-providers/${provider}/credentials`, {
|
||||
body: data,
|
||||
}),
|
||||
})
|
||||
}
|
||||
|
||||
export const useEditProviderCredential = (provider: string) => {
|
||||
return useMutation({
|
||||
mutationFn: (data: ProviderCredential) =>
|
||||
put<{ result: string }>(`/workspaces/current/model-providers/${provider}/credentials`, {
|
||||
body: data,
|
||||
}),
|
||||
})
|
||||
}
|
||||
|
||||
export const useDeleteProviderCredential = (provider: string) => {
|
||||
return useMutation({
|
||||
mutationFn: (data: { credential_id: string }) =>
|
||||
del<{ result: string }>(`/workspaces/current/model-providers/${provider}/credentials`, {
|
||||
body: data,
|
||||
}),
|
||||
})
|
||||
}
|
||||
|
||||
export const useActiveProviderCredential = (provider: string) => {
|
||||
return useMutation({
|
||||
mutationFn: (data: { credential_id: string; model?: string; model_type?: ModelTypeEnum }) =>
|
||||
post<{ result: string }>(
|
||||
`/workspaces/current/model-providers/${provider}/credentials/switch`,
|
||||
{
|
||||
body: data,
|
||||
},
|
||||
),
|
||||
})
|
||||
}
|
||||
|
||||
export const useGetModelCredential = (
|
||||
enabled: boolean,
|
||||
provider: string,
|
||||
credentialId?: string,
|
||||
model?: string,
|
||||
modelType?: string,
|
||||
configFrom?: string,
|
||||
) => {
|
||||
return useQuery({
|
||||
enabled,
|
||||
queryKey: [NAME_SPACE, 'model-list', provider, model, modelType, credentialId, configFrom],
|
||||
queryFn: () =>
|
||||
get<ModelCredential>(
|
||||
`/workspaces/current/model-providers/${provider}/models/credentials?model=${model}&model_type=${modelType}&config_from=${configFrom}${credentialId ? `&credential_id=${credentialId}` : ''}`,
|
||||
),
|
||||
staleTime: 0,
|
||||
gcTime: 0,
|
||||
})
|
||||
}
|
||||
|
||||
export const useAddModelCredential = (provider: string) => {
|
||||
return useMutation({
|
||||
mutationFn: (data: ModelCredentialPayload) =>
|
||||
post<{ result: string }>(
|
||||
`/workspaces/current/model-providers/${provider}/models/credentials`,
|
||||
{
|
||||
body: data,
|
||||
},
|
||||
),
|
||||
})
|
||||
}
|
||||
|
||||
export const useEditModelCredential = (provider: string) => {
|
||||
return useMutation({
|
||||
mutationFn: (data: ModelCredentialPayload) =>
|
||||
put<{ result: string }>(
|
||||
`/workspaces/current/model-providers/${provider}/models/credentials`,
|
||||
{
|
||||
body: data,
|
||||
},
|
||||
),
|
||||
})
|
||||
}
|
||||
|
||||
export const useDeleteModelCredential = (provider: string) => {
|
||||
return useMutation({
|
||||
mutationFn: (data: { credential_id: string; model: string; model_type: ModelTypeEnum }) =>
|
||||
del<{ result: string }>(
|
||||
`/workspaces/current/model-providers/${provider}/models/credentials`,
|
||||
{
|
||||
params: data,
|
||||
},
|
||||
),
|
||||
})
|
||||
}
|
||||
|
||||
export const useDeleteModel = (provider: string) => {
|
||||
return useMutation({
|
||||
mutationFn: (data: { model: string; model_type: ModelTypeEnum }) =>
|
||||
del<{ result: string }>(`/workspaces/current/model-providers/${provider}/models`, {
|
||||
params: data,
|
||||
}),
|
||||
})
|
||||
}
|
||||
|
||||
export const useActiveModelCredential = (provider: string) => {
|
||||
return useMutation({
|
||||
mutationFn: (data: { credential_id: string; model: string; model_type: ModelTypeEnum }) =>
|
||||
post<{ result: string }>(
|
||||
`/workspaces/current/model-providers/${provider}/models/credentials/switch`,
|
||||
{
|
||||
body: data,
|
||||
},
|
||||
),
|
||||
})
|
||||
}
|
||||
|
||||
export const useUpdateModelLoadBalancingConfig = (provider: string) => {
|
||||
return useMutation({
|
||||
mutationFn: (data: {
|
||||
config_from: string
|
||||
model: string
|
||||
model_type: ModelTypeEnum
|
||||
load_balancing: ModelLoadBalancingConfig
|
||||
credential_id?: string
|
||||
}) =>
|
||||
post<{ result: string }>(`/workspaces/current/model-providers/${provider}/models`, {
|
||||
body: data,
|
||||
}),
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user