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:
Stephen Zhou
2026-10-15 22:46:38 +08:00
co-authored by Claude Opus 5.5
parent b289d8161f
commit 66910779cc
13 changed files with 359 additions and 463 deletions
-8
View File
@@ -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
@@ -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 })
})
})
@@ -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('')
})
@@ -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,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 ?? '')
@@ -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) => ({
@@ -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 } }> }
@@ -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),
@@ -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,
@@ -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
-76
View File
@@ -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)
})
})
})
-172
View File
@@ -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,
}),
})
}