diff --git a/oxlint-suppressions.json b/oxlint-suppressions.json index 0fe03d0b3d3..f7842ebfb3e 100644 --- a/oxlint-suppressions.json +++ b/oxlint-suppressions.json @@ -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 diff --git a/web/app/components/header/account-setting/model-provider-page/model-auth/hooks/__tests__/use-auth-service.spec.tsx b/web/app/components/header/account-setting/model-provider-page/model-auth/hooks/__tests__/use-auth-service.spec.tsx index d5bc1a56047..560bc3c4a21 100644 --- a/web/app/components/header/account-setting/model-provider-page/model-auth/hooks/__tests__/use-auth-service.spec.tsx +++ b/web/app/components/header/account-setting/model-provider-page/model-auth/hooks/__tests__/use-auth-service.spec.tsx @@ -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 describe('useAuthService hooks', () => { let queryClient: QueryClient const wrapper = ({ children }: { children: React.ReactNode }) => ( {children} ) + 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, - ) - vi.mocked(useEditProviderCredential).mockReturnValue( - mockMutationReturn as unknown as ReturnType, - ) - vi.mocked(useDeleteProviderCredential).mockReturnValue( - mockMutationReturn as unknown as ReturnType, - ) - vi.mocked(useActiveProviderCredential).mockReturnValue( - mockMutationReturn as unknown as ReturnType, - ) - vi.mocked(useAddModelCredential).mockReturnValue( - mockMutationReturn as unknown as ReturnType, - ) - vi.mocked(useEditModelCredential).mockReturnValue( - mockMutationReturn as unknown as ReturnType, - ) - vi.mocked(useDeleteModelCredential).mockReturnValue( - mockMutationReturn as unknown as ReturnType, - ) - vi.mocked(useActiveModelCredential).mockReturnValue( - mockMutationReturn as unknown as ReturnType, - ) }) - it('useGetCredential selects correct source and params', () => { - const mockData = { data: 'test' } - vi.mocked(useGetProviderCredential).mockReturnValue( - mockData as unknown as ReturnType, - ) - vi.mocked(useGetModelCredential).mockReturnValue( - mockData as unknown as ReturnType, - ) + 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 }) }) }) diff --git a/web/app/components/header/account-setting/model-provider-page/model-auth/hooks/__tests__/use-auth.spec.tsx b/web/app/components/header/account-setting/model-provider-page/model-auth/hooks/__tests__/use-auth.spec.tsx index 14484fd2f4c..d5fdd8b3c8d 100644 --- a/web/app/components/header/account-setting/model-provider-page/model-auth/hooks/__tests__/use-auth.spec.tsx +++ b/web/app/components/header/account-setting/model-provider-page/model-auth/hooks/__tests__/use-auth.spec.tsx @@ -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()), + 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('') }) diff --git a/web/app/components/header/account-setting/model-provider-page/model-auth/hooks/use-auth-service.ts b/web/app/components/header/account-setting/model-provider-page/model-auth/hooks/use-auth-service.ts index a2dfc4f1b13..7498cdde304 100644 --- a/web/app/components/header/account-setting/model-provider-page/model-auth/hooks/use-auth-service.ts +++ b/web/app/components/header/account-setting/model-provider-page/model-auth/hooks/use-auth-service.ts @@ -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) => { diff --git a/web/app/components/header/account-setting/model-provider-page/model-auth/hooks/use-auth.ts b/web/app/components/header/account-setting/model-provider-page/model-auth/hooks/use-auth.ts index e8fc5ec1498..bf4fc3e0251 100644 --- a/web/app/components/header/account-setting/model-provider-page/model-auth/hooks/use-auth.ts +++ b/web/app/components/header/account-setting/model-provider-page/model-auth/hooks/use-auth.ts @@ -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(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 ?? '') diff --git a/web/app/components/header/account-setting/model-provider-page/provider-added-card/__tests__/load-balancing-credential-save.spec.tsx b/web/app/components/header/account-setting/model-provider-page/provider-added-card/__tests__/load-balancing-credential-save.spec.tsx index f670d5c28e5..0b22a003b23 100644 --- a/web/app/components/header/account-setting/model-provider-page/provider-added-card/__tests__/load-balancing-credential-save.spec.tsx +++ b/web/app/components/header/account-setting/model-provider-page/provider-added-card/__tests__/load-balancing-credential-save.spec.tsx @@ -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()), - 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) => ({ diff --git a/web/app/components/header/account-setting/model-provider-page/provider-added-card/__tests__/model-load-balancing-modal.spec.tsx b/web/app/components/header/account-setting/model-provider-page/provider-added-card/__tests__/model-load-balancing-modal.spec.tsx index 93ef2603728..975b56312ea 100644 --- a/web/app/components/header/account-setting/model-provider-page/provider-added-card/__tests__/model-load-balancing-modal.spec.tsx +++ b/web/app/components/header/account-setting/model-provider-page/provider-added-card/__tests__/model-load-balancing-modal.spec.tsx @@ -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() + return { + ...actual, + useQuery: (options: { operation?: string }) => + options.operation === 'model-credential' + ? { isLoading: false, data: mockCredentialData, refetch: mockRefetch } + : actual.useQuery(options as Parameters[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[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 } }> } diff --git a/web/app/components/header/account-setting/model-provider-page/provider-added-card/model-auth-dropdown/__tests__/use-activate-credential.spec.tsx b/web/app/components/header/account-setting/model-provider-page/provider-added-card/model-auth-dropdown/__tests__/use-activate-credential.spec.tsx index 045e54c04b7..b74cfda3071 100644 --- a/web/app/components/header/account-setting/model-provider-page/provider-added-card/model-auth-dropdown/__tests__/use-activate-credential.spec.tsx +++ b/web/app/components/header/account-setting/model-provider-page/provider-added-card/model-auth-dropdown/__tests__/use-activate-credential.spec.tsx @@ -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()), + 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), diff --git a/web/app/components/header/account-setting/model-provider-page/provider-added-card/model-auth-dropdown/use-activate-credential.ts b/web/app/components/header/account-setting/model-provider-page/provider-added-card/model-auth-dropdown/use-activate-credential.ts index 3c01efea48d..7aed0a3cd6a 100644 --- a/web/app/components/header/account-setting/model-provider-page/provider-added-card/model-auth-dropdown/use-activate-credential.ts +++ b/web/app/components/header/account-setting/model-provider-page/provider-added-card/model-auth-dropdown/use-activate-credential.ts @@ -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() 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, diff --git a/web/app/components/header/account-setting/model-provider-page/provider-added-card/model-load-balancing-modal.tsx b/web/app/components/header/account-setting/model-provider-page/provider-added-card/model-load-balancing-modal.tsx index 95f1d9691bf..71e51612b26 100644 --- a/web/app/components/header/account-setting/model-provider-page/provider-added-card/model-load-balancing-modal.tsx +++ b/web/app/components/header/account-setting/model-provider-page/provider-added-card/model-load-balancing-modal.tsx @@ -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( + () => + 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') { diff --git a/web/app/components/plugins/plugin-detail-panel/model-list.tsx b/web/app/components/plugins/plugin-detail-panel/model-list.tsx index 7f4c42c271c..4b166bb5c6b 100644 --- a/web/app/components/plugins/plugin-detail-panel/model-list.tsx +++ b/web/app/components/plugins/plugin-detail-panel/model-list.tsx @@ -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 diff --git a/web/service/__tests__/use-models.spec.tsx b/web/service/__tests__/use-models.spec.tsx deleted file mode 100644 index 29638e20bc3..00000000000 --- a/web/service/__tests__/use-models.spec.tsx +++ /dev/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 }) => ( - {children} - ) -} - -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) - }) - }) -}) diff --git a/web/service/use-models.ts b/web/service/use-models.ts deleted file mode 100644 index 97dd930d4de..00000000000 --- a/web/service/use-models.ts +++ /dev/null @@ -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( - `/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( - `/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, - }), - }) -}