model auth

This commit is contained in:
zxhlyh 2025-08-15 18:34:12 +08:00
parent 3f57e4a643
commit 2e28a64d38
26 changed files with 917 additions and 469 deletions

View File

@ -186,6 +186,22 @@ export type Credential = {
credential_name: string credential_name: string
} }
export type CustomModel = {
model: string
model_type: ModelTypeEnum
}
export type CustomModelCredential = CustomModel & {
credentials?: Record<string, any>
available_model_credentials?: Credential[]
current_credential_id?: string
}
export type CredentialWithModel = Credential & {
model: string
model_type: ModelTypeEnum
}
export type ModelProvider = { export type ModelProvider = {
provider: string provider: string
label: TypeWithI18N label: TypeWithI18N
@ -215,6 +231,7 @@ export type ModelProvider = {
current_credential_id?: string current_credential_id?: string
current_credential_name?: string current_credential_name?: string
available_credentials?: Credential[] available_credentials?: Credential[]
custom_models?: CustomModelCredential[]
} }
system_configuration: { system_configuration: {
enabled: boolean enabled: boolean
@ -280,9 +297,22 @@ export type ModelLoadBalancingConfigEntry = {
in_cooldown?: boolean in_cooldown?: boolean
/** cooldown time (in seconds) */ /** cooldown time (in seconds) */
ttl?: number ttl?: number
credential_id?: string
} }
export type ModelLoadBalancingConfig = { export type ModelLoadBalancingConfig = {
enabled: boolean enabled: boolean
configs: ModelLoadBalancingConfigEntry[] configs: ModelLoadBalancingConfigEntry[]
} }
export type ProviderCredential = {
credentials: Record<string, any>
name: string
credential_id: string
}
export type ModelCredential = {
credentials: Record<string, any>
load_balancing: ModelLoadBalancingConfig
available_credentials: Credential[]
}

View File

@ -9,6 +9,7 @@ import { useContext } from 'use-context-selector'
import type { import type {
Credential, Credential,
CustomConfigurationModelFixedFields, CustomConfigurationModelFixedFields,
CustomModel,
DefaultModel, DefaultModel,
DefaultModelResponse, DefaultModelResponse,
Model, Model,
@ -354,6 +355,7 @@ export const useModelModalHandler = () => {
configurationMethod: ConfigurationMethodEnum, configurationMethod: ConfigurationMethodEnum,
CustomConfigurationModelFixedFields?: CustomConfigurationModelFixedFields, CustomConfigurationModelFixedFields?: CustomConfigurationModelFixedFields,
credential?: Credential, credential?: Credential,
model?: CustomModel,
) => { ) => {
setShowModelModal({ setShowModelModal({
payload: { payload: {
@ -361,6 +363,7 @@ export const useModelModalHandler = () => {
currentConfigurationMethod: configurationMethod, currentConfigurationMethod: configurationMethod,
currentCustomConfigurationModelFixedFields: CustomConfigurationModelFixedFields, currentCustomConfigurationModelFixedFields: CustomConfigurationModelFixedFields,
credential, credential,
model,
}, },
onSaveCallback: () => { onSaveCallback: () => {
handleRefreshModel(provider, configurationMethod, CustomConfigurationModelFixedFields) handleRefreshModel(provider, configurationMethod, CustomConfigurationModelFixedFields)

View File

@ -3,13 +3,21 @@ import {
useCallback, useCallback,
} from 'react' } from 'react'
import { RiAddLine } from '@remixicon/react' import { RiAddLine } from '@remixicon/react'
import { import { Authorized } from '@/app/components/header/account-setting/model-provider-page/model-auth'
AuthCategory,
Authorized,
} from '@/app/components/plugins/plugin-auth'
import cn from '@/utils/classnames' import cn from '@/utils/classnames'
import type {
Credential,
ModelProvider,
} from '@/app/components/header/account-setting/model-provider-page/declarations'
const AddCredentialInLoadBalancing = () => { type AddCredentialInLoadBalancingProps = {
provider: ModelProvider
onSetup: (credential?: Credential) => void
}
const AddCredentialInLoadBalancing = ({
provider,
onSetup,
}: AddCredentialInLoadBalancingProps) => {
const renderTrigger = useCallback((open?: boolean) => { const renderTrigger = useCallback((open?: boolean) => {
return ( return (
<div className={cn( <div className={cn(
@ -25,13 +33,9 @@ const AddCredentialInLoadBalancing = () => {
return ( return (
<Authorized <Authorized
credentials={[]} credentials={[]}
pluginPayload={{ provider={provider.provider}
provider: '',
category: AuthCategory.model,
}}
canApiKey
offset={4}
renderTrigger={renderTrigger} renderTrigger={renderTrigger}
onSetup={onSetup}
/> />
) )
} }

View File

@ -0,0 +1,78 @@
import {
memo,
useCallback,
useMemo,
} from 'react'
import { useTranslation } from 'react-i18next'
import {
RiAddCircleFill,
} from '@remixicon/react'
import {
Button,
} from '@/app/components/base/button'
import type {
CustomConfigurationModelFixedFields,
CustomModelCredential,
ModelProvider,
} from '@/app/components/header/account-setting/model-provider-page/declarations'
import { ConfigurationMethodEnum } from '@/app/components/header/account-setting/model-provider-page/declarations'
import Authorized from './authorized'
import { useAuth } from './hooks'
import cn from '@/utils/classnames'
type AddCustomModelProps = {
provider: ModelProvider,
configurationMethod: ConfigurationMethodEnum,
currentCustomConfigurationModelFixedFields?: CustomConfigurationModelFixedFields,
models: CustomModelCredential[]
}
const AddCustomModel = ({
provider,
configurationMethod,
currentCustomConfigurationModelFixedFields,
models,
}: AddCustomModelProps) => {
const { t } = useTranslation()
const noModels = !models.length
const {
handleOpenModal,
} = useAuth(provider, configurationMethod, currentCustomConfigurationModelFixedFields)
const handleClick = useCallback(() => {
if (noModels)
handleOpenModal()
}, [handleOpenModal, noModels])
const ButtonComponent = useMemo(() => {
return (
<Button
variant='ghost-accent'
size='small'
onClick={handleClick}
className={cn(noModels && 'text-text-accent')}
>
<RiAddCircleFill className='mr-1 h-3.5 w-3.5' />
{t('common.modelProvider.addModel')}
</Button>
)
}, [handleClick, noModels])
const renderTrigger = useCallback(() => {
return ButtonComponent
}, [ButtonComponent])
if (noModels)
return ButtonComponent
return (
<Authorized
provider={provider}
configurationMethod={ConfigurationMethodEnum.customizableModel}
items={models.map(model => ({
model,
credentials: model.available_model_credentials ?? [],
}))}
renderTrigger={renderTrigger}
/>
)
}
export default memo(AddCustomModel)

View File

@ -1,76 +0,0 @@
import {
memo,
useState,
} from 'react'
import { useTranslation } from 'react-i18next'
import {
RiAddCircleFill,
RiAddLine,
} from '@remixicon/react'
import {
PortalToFollowElem,
PortalToFollowElemContent,
PortalToFollowElemTrigger,
} from '@/app/components/base/portal-to-follow-elem'
import Button from '@/app/components/base/button'
import Tooltip from '@/app/components/base/tooltip'
const AddModel = () => {
const { t } = useTranslation()
const [open, setOpen] = useState(false)
return (
<PortalToFollowElem
placement='bottom-end'
offset={{
mainAxis: 4,
crossAxis: -4,
}}
open={open}
onOpenChange={setOpen}
>
<PortalToFollowElemTrigger>
<Button
variant='ghost-accent'
size='small'
>
<RiAddCircleFill className='mr-1 h-3.5 w-3.5' />
{t('common.modelProvider.addModel')}
</Button>
</PortalToFollowElemTrigger>
<PortalToFollowElemContent>
<div className='w-[360px] rounded-xl border-[0.5px] border-components-panel-border bg-components-panel-bg-blur shadow-lg'>
<div className='p-1'>
<div className='flex h-9 items-center'>
<div className='h-5 w-5 shrink-0'></div>
<div
className='system-md-medium mx-1 truncate text-text-primary'
title='chat-finetune-01'
>
chat-finetune-01
</div>
<Tooltip
asChild
popupContent='Add model credential'
>
<Button
className='h-6 w-6 rounded-full p-0'
size='small'
variant='secondary-accent'
>
<RiAddLine className='h-4 w-4' />
</Button>
</Tooltip>
</div>
</div>
<div className='system-xs-medium flex h-10 cursor-pointer items-center border-t border-divider-subtle px-4 text-text-accent-light-mode-only'>
<RiAddLine className='mr-1 h-4 w-4' />
{t('common.modelProvider.addModel')}
</div>
</div>
</PortalToFollowElemContent>
</PortalToFollowElem>
)
}
export default memo(AddModel)

View File

@ -0,0 +1,90 @@
import {
memo,
useCallback,
} from 'react'
import { RiAddLine } from '@remixicon/react'
import CredentialItem from './credential-item'
import type {
Credential,
CustomModel,
} from '../../declarations'
import Button from '@/app/components/base/button'
import Tooltip from '@/app/components/base/tooltip'
type AuthorizedItemProps = {
model?: CustomModel
disabled?: boolean
onDelete?: (id: string) => void
onEdit?: (model?: CustomModel, credential?: Credential) => void
onSetDefault?: (id: string) => void
onItemClick?: (id: string) => void
showItemSelectedIcon?: boolean
selectedCredentialId?: string
disableSetDefault?: boolean
credentials: Credential[]
}
export const AuthorizedItem = ({
model,
credentials,
disabled,
onDelete,
onEdit,
onSetDefault,
onItemClick,
showItemSelectedIcon,
selectedCredentialId,
disableSetDefault,
}: AuthorizedItemProps) => {
const handleEdit = useCallback((credential?: Credential) => {
onEdit?.(model, credential)
}, [onEdit, model])
return (
<div className='p-1'>
{
model && (
<div
className='flex h-9 items-center'
>
<div className='h-5 w-5 shrink-0'></div>
<div
className='system-md-medium mx-1 truncate text-text-primary'
title={model.model}
>
{model.model}
</div>
<Tooltip
asChild
popupContent='Add model credential'
>
<Button
className='h-6 w-6 rounded-full p-0'
size='small'
variant='secondary-accent'
>
<RiAddLine className='h-4 w-4' />
</Button>
</Tooltip>
</div>
)
}
{
credentials.map(credential => (
<CredentialItem
key={credential.credential_id}
credential={credential}
disabled={disabled}
onDelete={onDelete}
onEdit={handleEdit}
onSetDefault={onSetDefault}
onItemClick={onItemClick}
showSelectedIcon={showItemSelectedIcon}
selectedCredentialId={selectedCredentialId}
disableSetDefault={disableSetDefault}
/>
))
}
</div>
)
}
export default memo(AuthorizedItem)

View File

@ -15,11 +15,11 @@ import cn from '@/utils/classnames'
import type { Credential } from '../../declarations' import type { Credential } from '../../declarations'
import Button from '@/app/components/base/button' import Button from '@/app/components/base/button'
type ItemProps = { type CredentialItemProps = {
credential: Credential credential: Credential
disabled?: boolean disabled?: boolean
onDelete?: (id: string) => void onDelete?: (id: string) => void
onEdit?: (credential: Credential) => void onEdit?: (credential?: Credential) => void
onSetDefault?: (id: string) => void onSetDefault?: (id: string) => void
disableRename?: boolean disableRename?: boolean
disableEdit?: boolean disableEdit?: boolean
@ -29,7 +29,7 @@ type ItemProps = {
showSelectedIcon?: boolean showSelectedIcon?: boolean
selectedCredentialId?: string selectedCredentialId?: string
} }
const Item = ({ const CredentialItem = ({
credential, credential,
disabled, disabled,
onDelete, onDelete,
@ -42,7 +42,7 @@ const Item = ({
onItemClick, onItemClick,
showSelectedIcon, showSelectedIcon,
selectedCredentialId, selectedCredentialId,
}: ItemProps) => { }: CredentialItemProps) => {
const { t } = useTranslation() const { t } = useTranslation()
const showAction = useMemo(() => { const showAction = useMemo(() => {
return !(disableRename && disableEdit && disableDelete && disableSetDefault) return !(disableRename && disableEdit && disableDelete && disableSetDefault)
@ -131,4 +131,4 @@ const Item = ({
) )
} }
export default memo(Item) export default memo(CredentialItem)

View File

@ -1,7 +1,6 @@
import { import {
memo, memo,
useCallback, useCallback,
useRef,
useState, useState,
} from 'react' } from 'react'
import { import {
@ -19,17 +18,25 @@ import type {
import Button from '@/app/components/base/button' import Button from '@/app/components/base/button'
import cn from '@/utils/classnames' import cn from '@/utils/classnames'
import Confirm from '@/app/components/base/confirm' import Confirm from '@/app/components/base/confirm'
import Item from './item' import type {
import { useToastContext } from '@/app/components/base/toast' ConfigurationMethodEnum,
import type { Credential } from '../../declarations' Credential,
import { CustomConfigurationModelFixedFields,
useDeleteModelCredential, CustomModel,
useSetModelCredentialDefault, ModelProvider,
} from '@/service/use-models' } from '../../declarations'
import { useAuth } from '../hooks'
import AuthorizedItem from './authorized-item'
type AuthorizedProps = { type AuthorizedProps = {
provider: string provider: ModelProvider,
credentials: Credential[] configurationMethod: ConfigurationMethodEnum,
currentCustomConfigurationModelFixedFields?: CustomConfigurationModelFixedFields,
items: {
model?: CustomModel
credentials: Credential[]
}[]
selectedCredential?: Credential
disabled?: boolean disabled?: boolean
renderTrigger?: (open?: boolean) => React.ReactNode renderTrigger?: (open?: boolean) => React.ReactNode
isOpen?: boolean isOpen?: boolean
@ -40,13 +47,15 @@ type AuthorizedProps = {
popupClassName?: string popupClassName?: string
onItemClick?: (id: string) => void onItemClick?: (id: string) => void
showItemSelectedIcon?: boolean showItemSelectedIcon?: boolean
selectedCredentialId?: string
onUpdate?: () => void onUpdate?: () => void
onSetup: (credential?: Credential) => void disableSetDefault?: boolean
} }
const Authorized = ({ const Authorized = ({
provider, provider,
credentials, configurationMethod,
currentCustomConfigurationModelFixedFields,
items,
selectedCredential,
disabled, disabled,
renderTrigger, renderTrigger,
isOpen, isOpen,
@ -57,12 +66,10 @@ const Authorized = ({
popupClassName, popupClassName,
onItemClick, onItemClick,
showItemSelectedIcon, showItemSelectedIcon,
selectedCredentialId,
onUpdate, onUpdate,
onSetup, disableSetDefault,
}: AuthorizedProps) => { }: AuthorizedProps) => {
const { t } = useTranslation() const { t } = useTranslation()
const { notify } = useToastContext()
const [isLocalOpen, setIsLocalOpen] = useState(false) const [isLocalOpen, setIsLocalOpen] = useState(false)
const mergedIsOpen = isOpen ?? isLocalOpen const mergedIsOpen = isOpen ?? isLocalOpen
const setMergedIsOpen = useCallback((open: boolean) => { const setMergedIsOpen = useCallback((open: boolean) => {
@ -71,68 +78,20 @@ const Authorized = ({
setIsLocalOpen(open) setIsLocalOpen(open)
}, [onOpenChange]) }, [onOpenChange])
const pendingOperationCredentialId = useRef<string | null>(null) const {
const [deleteCredentialId, setDeleteCredentialId] = useState<string | null>(null) openConfirmDelete,
const openConfirm = useCallback((credentialId?: string) => { closeConfirmDelete,
if (credentialId) doingAction,
pendingOperationCredentialId.current = credentialId handleActiveCredential,
handleConfirmDelete,
deleteCredentialId,
handleOpenModal,
} = useAuth(provider, configurationMethod, currentCustomConfigurationModelFixedFields, onUpdate)
setDeleteCredentialId(pendingOperationCredentialId.current) const handleEdit = useCallback((model?: CustomModel, credential?: Credential) => {
}, []) handleOpenModal(model, credential)
const closeConfirm = useCallback(() => {
setDeleteCredentialId(null)
pendingOperationCredentialId.current = null
}, [])
const [doingAction, setDoingAction] = useState(false)
const doingActionRef = useRef(doingAction)
const handleSetDoingAction = useCallback((doing: boolean) => {
doingActionRef.current = doing
setDoingAction(doing)
}, [])
const { mutateAsync: deleteModelCredential } = useDeleteModelCredential(provider)
const { mutateAsync: setModelCredentialDefault } = useSetModelCredentialDefault(provider)
const handleSetDefault = useCallback(async (id: string) => {
if (doingActionRef.current)
return
try {
handleSetDoingAction(true)
await setModelCredentialDefault(id)
notify({
type: 'success',
message: t('common.api.actionSuccess'),
})
onUpdate?.()
}
finally {
handleSetDoingAction(false)
}
}, [setModelCredentialDefault, onUpdate, notify, t, handleSetDoingAction])
const handleConfirm = useCallback(async () => {
if (doingActionRef.current)
return
if (!pendingOperationCredentialId.current) {
setDeleteCredentialId(null)
return
}
try {
handleSetDoingAction(true)
await deleteModelCredential(pendingOperationCredentialId.current)
notify({
type: 'success',
message: t('common.api.actionSuccess'),
})
onUpdate?.()
setDeleteCredentialId(null)
pendingOperationCredentialId.current = null
}
finally {
handleSetDoingAction(false)
}
}, [onUpdate, notify, t, handleSetDoingAction])
const handleOpenSetup = useCallback((credential?: Credential) => {
onSetup(credential)
setMergedIsOpen(false) setMergedIsOpen(false)
}, [onSetup, setMergedIsOpen]) }, [handleOpenModal, setMergedIsOpen])
return ( return (
<> <>
@ -166,39 +125,29 @@ const Authorized = ({
'w-[360px] rounded-xl border-[0.5px] border-components-panel-border bg-components-panel-bg-blur shadow-lg', 'w-[360px] rounded-xl border-[0.5px] border-components-panel-border bg-components-panel-bg-blur shadow-lg',
popupClassName, popupClassName,
)}> )}>
<div className='max-h-[304px] overflow-y-auto py-1'> <div className='max-h-[304px] overflow-y-auto'>
{ {
!!credentials.length && ( items.map((item, index) => (
<div className='p-1'> <AuthorizedItem
<div className={cn( key={index}
'system-xs-medium px-3 pb-0.5 pt-1 text-text-tertiary', model={item.model}
showItemSelectedIcon && 'pl-7', credentials={item.credentials}
)}> disabled={disabled}
API Keys onDelete={openConfirmDelete}
</div> onEdit={handleEdit}
{ onSetDefault={handleActiveCredential}
credentials.map(credential => ( onItemClick={onItemClick}
<Item showItemSelectedIcon={showItemSelectedIcon}
key={credential.credential_id} selectedCredentialId={selectedCredential?.credential_id}
credential={credential} disableSetDefault={disableSetDefault}
disabled={disabled} />
onDelete={openConfirm} ))
onEdit={handleOpenSetup}
onSetDefault={handleSetDefault}
onItemClick={onItemClick}
showSelectedIcon={showItemSelectedIcon}
selectedCredentialId={selectedCredentialId}
/>
))
}
</div>
)
} }
</div> </div>
<div className='h-[1px] bg-divider-subtle'></div> <div className='h-[1px] bg-divider-subtle'></div>
<div className='p-2'> <div className='p-2'>
<Button <Button
onClick={() => handleOpenSetup()} onClick={() => handleOpenModal()}
className='w-full' className='w-full'
> >
add api key add api key
@ -211,10 +160,10 @@ const Authorized = ({
deleteCredentialId && ( deleteCredentialId && (
<Confirm <Confirm
isShow isShow
title={t('datasetDocuments.list.delete.title')} title={t('common.modelProvider.confirmDelete')}
isDisabled={doingAction} isDisabled={doingAction}
onCancel={closeConfirm} onCancel={closeConfirmDelete}
onConfirm={handleConfirm} onConfirm={handleConfirmDelete}
/> />
) )
} }

View File

@ -0,0 +1,82 @@
import {
memo,
useCallback,
useMemo,
} from 'react'
import { useTranslation } from 'react-i18next'
import {
RiEqualizer2Line,
} from '@remixicon/react'
import {
Button,
} from '@/app/components/base/button'
import type {
CustomConfigurationModelFixedFields,
ModelProvider,
} from '@/app/components/header/account-setting/model-provider-page/declarations'
import { ConfigurationMethodEnum } from '@/app/components/header/account-setting/model-provider-page/declarations'
import Authorized from './authorized'
import { useAuth, useCredentialStatus } from './hooks'
type ConfigProviderProps = {
provider: ModelProvider,
configurationMethod: ConfigurationMethodEnum,
currentCustomConfigurationModelFixedFields?: CustomConfigurationModelFixedFields,
}
const ConfigProvider = ({
provider,
configurationMethod,
currentCustomConfigurationModelFixedFields,
}: ConfigProviderProps) => {
const { t } = useTranslation()
const {
handleOpenModal,
} = useAuth(provider, configurationMethod, currentCustomConfigurationModelFixedFields)
const {
hasCredential,
authorized,
current_credential_id,
current_credential_name,
available_credentials,
} = useCredentialStatus(provider)
const handleClick = useCallback(() => {
if (!hasCredential)
handleOpenModal()
}, [handleOpenModal, hasCredential])
const ButtonComponent = useMemo(() => {
return (
<Button
className='grow'
size='small'
onClick={handleClick}
variant={!authorized ? 'secondary-accent' : 'secondary'}
>
<RiEqualizer2Line className='mr-1 h-3.5 w-3.5' />
{t('common.operation.setup')}
</Button>
)
}, [handleClick, authorized])
if (!hasCredential)
return ButtonComponent
return (
<Authorized
provider={provider}
configurationMethod={ConfigurationMethodEnum.predefinedModel}
items={[
{
credentials: available_credentials ?? [],
},
]}
selectedCredential={{
credential_id: current_credential_id ?? '',
credential_name: current_credential_name ?? '',
}}
showItemSelectedIcon
/>
)
}
export default memo(ConfigProvider)

View File

@ -0,0 +1,5 @@
export * from './use-model-form-schemas'
export * from './use-credential-status'
export * from './use-custom-models'
export * from './use-auth'
export * from './use-auth-service'

View File

@ -0,0 +1,57 @@
import { useCallback } from 'react'
import {
useActiveModelCredential,
useActiveProviderCredential,
useAddModelCredential,
useAddProviderCredential,
useDeleteModelCredential,
useDeleteProviderCredential,
useEditModelCredential,
useEditProviderCredential,
useGetModelCredential,
useGetProviderCredential,
} from '@/service/use-models'
import type {
CustomModel,
} from '@/app/components/header/account-setting/model-provider-page/declarations'
export const useGetCredential = (provider: string, credentialId?: string, model?: CustomModel, configFrom?: string) => {
const providerData = useGetProviderCredential(!model && !!credentialId, provider, credentialId)
const modelData = useGetModelCredential(!!model && !!credentialId, provider, credentialId, model?.model, model?.model_type, configFrom)
return model ? 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 { mutateAsync: addModelCredential } = useAddModelCredential(provider)
const { mutateAsync: activeModelCredential } = useActiveModelCredential(provider)
const { mutateAsync: deleteModelCredential } = useDeleteModelCredential(provider)
const { mutateAsync: editModelCredential } = useEditModelCredential(provider)
const getAddCredentialService = useCallback((isModel: boolean) => {
return isModel ? addModelCredential : addProviderCredential
}, [addModelCredential, addProviderCredential])
const getEditCredentialService = useCallback((isModel: boolean) => {
return isModel ? editModelCredential : editProviderCredential
}, [editModelCredential, editProviderCredential])
const getDeleteCredentialService = useCallback((isModel: boolean) => {
return isModel ? deleteModelCredential : deleteProviderCredential
}, [deleteModelCredential, deleteProviderCredential])
const getActiveCredentialService = useCallback((isModel: boolean) => {
return isModel ? activeModelCredential : activeProviderCredential
}, [activeModelCredential, activeProviderCredential])
return {
getAddCredentialService,
getEditCredentialService,
getDeleteCredentialService,
getActiveCredentialService,
}
}

View File

@ -0,0 +1,153 @@
import {
useCallback,
useRef,
useState,
} from 'react'
import { useTranslation } from 'react-i18next'
import { useToastContext } from '@/app/components/base/toast'
import { useAuthService } from './use-auth-service'
import type {
ConfigurationMethodEnum,
Credential,
CustomConfigurationModelFixedFields,
CustomModel,
ModelProvider,
} from '../../declarations'
import {
useModelModalHandler,
useRefreshModel,
} from '@/app/components/header/account-setting/model-provider-page/hooks'
export const useAuth = (
provider: ModelProvider,
configurationMethod: ConfigurationMethodEnum,
currentCustomConfigurationModelFixedFields?: CustomConfigurationModelFixedFields,
onUpdate?: () => void,
) => {
const { t } = useTranslation()
const { notify } = useToastContext()
const {
getDeleteCredentialService,
getActiveCredentialService,
getEditCredentialService,
getAddCredentialService,
} = useAuthService(provider.provider)
const handleOpenModelModal = useModelModalHandler()
const { handleRefreshModel } = useRefreshModel()
const pendingOperationCredentialId = useRef<string | null>(null)
const pendingOperationModel = useRef<CustomModel | null>(null)
const [deleteCredentialId, setDeleteCredentialId] = useState<string | null>(null)
const openConfirmDelete = useCallback((credentialId?: string, model?: CustomModel) => {
if (credentialId)
pendingOperationCredentialId.current = credentialId
if (model)
pendingOperationModel.current = model
setDeleteCredentialId(pendingOperationCredentialId.current)
}, [])
const closeConfirmDelete = useCallback(() => {
setDeleteCredentialId(null)
pendingOperationCredentialId.current = null
}, [])
const [doingAction, setDoingAction] = useState(false)
const doingActionRef = useRef(doingAction)
const handleSetDoingAction = useCallback((doing: boolean) => {
doingActionRef.current = doing
setDoingAction(doing)
}, [])
const handleActiveCredential = useCallback(async (id: string, model?: CustomModel) => {
if (doingActionRef.current)
return
try {
handleSetDoingAction(true)
await getActiveCredentialService(!!model)({
credential_id: id,
model: model?.model,
model_type: model?.model_type,
})
notify({
type: 'success',
message: t('common.api.actionSuccess'),
})
onUpdate?.()
}
finally {
handleSetDoingAction(false)
}
}, [getActiveCredentialService, onUpdate, notify, t, handleSetDoingAction])
const handleConfirmDelete = useCallback(async () => {
if (doingActionRef.current)
return
if (!pendingOperationCredentialId.current) {
setDeleteCredentialId(null)
return
}
try {
handleSetDoingAction(true)
await getDeleteCredentialService(!!pendingOperationModel.current)({
credential_id: pendingOperationCredentialId.current,
model: pendingOperationModel.current?.model,
model_type: pendingOperationModel.current?.model_type,
})
notify({
type: 'success',
message: t('common.api.actionSuccess'),
})
onUpdate?.()
handleRefreshModel(provider, configurationMethod, undefined)
setDeleteCredentialId(null)
pendingOperationCredentialId.current = null
}
finally {
handleSetDoingAction(false)
}
}, [onUpdate, notify, t, handleSetDoingAction, getDeleteCredentialService])
const handleAddCredential = useCallback((model?: CustomModel) => {
if (model)
pendingOperationModel.current = model
}, [])
const handleSaveCredential = useCallback(async (payload: Record<string, any>) => {
if (doingActionRef.current)
return
try {
handleSetDoingAction(true)
let res: { result?: string } = {}
if (payload.credential_id)
res = await getEditCredentialService(!!payload.model)(payload as any)
else
res = await getAddCredentialService(!!payload.model)(payload as any)
if (res.result === 'success') {
notify({ type: 'success', message: t('common.actionMsg.modifiedSuccessfully') })
onUpdate?.()
}
}
finally {
handleSetDoingAction(false)
}
}, [onUpdate, notify, t, handleSetDoingAction, getEditCredentialService, getAddCredentialService])
const handleOpenModal = useCallback((model?: CustomModel, credential?: Credential) => {
handleOpenModelModal(
provider,
configurationMethod,
currentCustomConfigurationModelFixedFields,
credential,
model,
)
}, [handleOpenModelModal, provider, configurationMethod, currentCustomConfigurationModelFixedFields])
return {
pendingOperationCredentialId,
pendingOperationModel,
openConfirmDelete,
closeConfirmDelete,
doingAction,
handleActiveCredential,
handleConfirmDelete,
handleAddCredential,
deleteCredentialId,
handleSaveCredential,
handleOpenModal,
}
}

View File

@ -0,0 +1,24 @@
import { useMemo } from 'react'
import type {
ModelProvider,
} from '../../declarations'
export const useCredentialStatus = (provider: ModelProvider) => {
const {
current_credential_id,
current_credential_name,
available_credentials,
} = provider.custom_configuration
const hasCredential = !!available_credentials?.length
const authorized = current_credential_id && current_credential_name
const authRemoved = hasCredential && !current_credential_id && !current_credential_name
return useMemo(() => ({
hasCredential,
authorized,
authRemoved,
current_credential_id,
current_credential_name,
available_credentials,
}), [hasCredential, authorized, authRemoved, current_credential_id, current_credential_name, available_credentials])
}

View File

@ -0,0 +1,9 @@
import type {
ModelProvider,
} from '../../declarations'
export const useCustomModels = (provider: ModelProvider) => {
const { custom_models } = provider.custom_configuration
return custom_models || []
}

View File

@ -3,11 +3,11 @@ import { useTranslation } from 'react-i18next'
import type { import type {
ModelLoadBalancingConfig, ModelLoadBalancingConfig,
ModelProvider, ModelProvider,
} from '../declarations' } from '../../declarations'
import { import {
genModelNameFormSchema, genModelNameFormSchema,
genModelTypeFormSchema, genModelTypeFormSchema,
} from '../utils' } from '../../utils'
import { FormTypeEnum } from '@/app/components/base/form/types' import { FormTypeEnum } from '@/app/components/base/form/types'
export const useModelFormSchemas = ( export const useModelFormSchemas = (

View File

@ -0,0 +1,5 @@
export { default as Authorized } from './authorized'
export { default as SwitchCredentialInLoadBalancing } from './switch-credential-in-load-balancing'
export { default as AddCredentialInLoadBalancing } from './add-credential-in-load-balancing'
export { default as AddCustomModel } from './add-custom-model'
export { default as ConfigProvider } from './config-provider'

View File

@ -1,45 +1,107 @@
import type { Dispatch, SetStateAction } from 'react'
import { import {
memo, memo,
useCallback, useCallback,
} from 'react' } from 'react'
import { useTranslation } from 'react-i18next'
import { RiArrowDownSLine } from '@remixicon/react' import { RiArrowDownSLine } from '@remixicon/react'
import Button from '@/app/components/base/button' import Button from '@/app/components/base/button'
import {
AuthCategory,
Authorized,
} from '@/app/components/plugins/plugin-auth'
import Indicator from '@/app/components/header/indicator' import Indicator from '@/app/components/header/indicator'
import Badge from '@/app/components/base/badge' import Badge from '@/app/components/base/badge'
import Authorized from './authorized'
import type {
Credential,
ModelLoadBalancingConfig,
ModelProvider,
} from '../declarations'
import { ConfigurationMethodEnum } from '@/app/components/header/account-setting/model-provider-page/declarations'
import { useCredentialStatus } from './hooks'
import { useModelModalHandler } from '../hooks'
import cn from '@/utils/classnames'
type SwitchCredentialInLoadBalancingProps = {
provider: ModelProvider
draftConfig?: ModelLoadBalancingConfig
setDraftConfig: Dispatch<SetStateAction<ModelLoadBalancingConfig | undefined>>
}
const SwitchCredentialInLoadBalancing = ({
provider,
draftConfig,
setDraftConfig,
}: SwitchCredentialInLoadBalancingProps) => {
const { t } = useTranslation()
const {
available_credentials,
current_credential_name,
} = useCredentialStatus(provider)
const handleOpenModal = useModelModalHandler()
console.log(draftConfig, 'draftConfig')
const handleSetup = useCallback((credential?: Credential) => {
handleOpenModal(provider, ConfigurationMethodEnum.predefinedModel, undefined, credential)
}, [handleOpenModal, provider])
const handleItemClick = useCallback((id: string) => {
setDraftConfig((prev) => {
if (!prev)
return prev
const newConfigs = [...prev.configs]
const index = newConfigs.findIndex(config => config.name === '__inherit__')
const inheritConfig = newConfigs[index]
const modifiedConfig = inheritConfig ? {
...inheritConfig,
credential_id: id,
} : {
name: '__inherit__',
credential_id: id,
credentials: {},
}
newConfigs.splice(index, 1, modifiedConfig)
return {
...prev,
configs: newConfigs,
}
})
}, [setDraftConfig])
const SwitchCredentialInLoadBalancing = () => {
const renderTrigger = useCallback(() => { const renderTrigger = useCallback(() => {
const selectedCredentialId = draftConfig?.configs.find(config => config.name === '__inherit__')?.credential_id
const selectedCredential = available_credentials?.find(credential => credential.credential_id === selectedCredentialId)
const name = selectedCredential?.credential_name || current_credential_name
const authRemoved = !!selectedCredentialId && !selectedCredential
return ( return (
<Button <Button
variant='secondary' variant='secondary'
className='space-x-1' className={cn(
'shrink-0 space-x-1',
authRemoved && 'text-components-button-destructive-secondary-text',
)}
> >
<Indicator /> <Indicator
chat-enterprise className='mr-2'
<Badge>enterprise</Badge> color={authRemoved ? 'red' : 'green'}
/>
{
authRemoved ? t('common.model.authRemoved') : name
}
{
!authRemoved && (
<Badge>enterprise</Badge>
)
}
<RiArrowDownSLine className='h-4 w-4' /> <RiArrowDownSLine className='h-4 w-4' />
</Button> </Button>
) )
}, []) }, [current_credential_name, t, draftConfig, available_credentials])
return ( return (
<Authorized <Authorized
credentials={[]} provider={provider.provider}
pluginPayload={{ credentials={available_credentials || []}
provider: '', onSetup={handleSetup}
category: AuthCategory.model,
}}
canApiKey
placement='bottom-end'
offset={{
mainAxis: 4,
crossAxis: -8,
}}
renderTrigger={renderTrigger} renderTrigger={renderTrigger}
onItemClick={handleItemClick}
disableSetDefault
/> />
) )
} }

View File

@ -2,34 +2,20 @@ import type { FC } from 'react'
import { import {
memo, memo,
useCallback, useCallback,
useEffect,
useMemo,
useRef, useRef,
useState,
} from 'react' } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import type { import type {
CustomConfigurationModelFixedFields, CustomConfigurationModelFixedFields,
ModelLoadBalancingConfig,
ModelLoadBalancingConfigEntry,
ModelProvider, ModelProvider,
} from '../declarations' } from '../declarations'
import { import {
ConfigurationMethodEnum, ConfigurationMethodEnum,
CustomConfigurationStatusEnum,
FormTypeEnum, FormTypeEnum,
} from '../declarations' } from '../declarations'
import {
genModelNameFormSchema,
genModelTypeFormSchema,
removeCredentials,
saveCredentials,
} from '../utils'
import { import {
useLanguage, useLanguage,
useProviderCredentialsAndLoadBalancing,
} from '../hooks' } from '../hooks'
import ModelLoadBalancingConfigs from '../provider-added-card/model-load-balancing-configs'
import Button from '@/app/components/base/button' import Button from '@/app/components/base/button'
import { Lock01 } from '@/app/components/base/icons/src/vender/solid/security' import { Lock01 } from '@/app/components/base/icons/src/vender/solid/security'
import { LinkExternal02 } from '@/app/components/base/icons/src/vender/line/general' import { LinkExternal02 } from '@/app/components/base/icons/src/vender/line/general'
@ -37,7 +23,6 @@ import {
PortalToFollowElem, PortalToFollowElem,
PortalToFollowElemContent, PortalToFollowElemContent,
} from '@/app/components/base/portal-to-follow-elem' } from '@/app/components/base/portal-to-follow-elem'
import { useToastContext } from '@/app/components/base/toast'
import Confirm from '@/app/components/base/confirm' import Confirm from '@/app/components/base/confirm'
import { useAppContext } from '@/context/app-context' import { useAppContext } from '@/context/app-context'
import AuthForm from '@/app/components/base/form/form-scenarios/auth' import AuthForm from '@/app/components/base/form/form-scenarios/auth'
@ -46,20 +31,29 @@ import type {
FormSchema, FormSchema,
} from '@/app/components/base/form/types' } from '@/app/components/base/form/types'
import { useModelFormSchemas } from '../model-auth/hooks' import { useModelFormSchemas } from '../model-auth/hooks'
import type { Credential } from '../declarations' import type {
Credential,
CustomModel,
} from '../declarations'
import Loading from '@/app/components/base/loading' import Loading from '@/app/components/base/loading'
import {
useAuth,
useGetCredential,
} from '@/app/components/header/account-setting/model-provider-page/model-auth/hooks'
type ModelModalProps = { type ModelModalProps = {
provider: ModelProvider provider: ModelProvider
model?: CustomModel
credential?: Credential
configurateMethod: ConfigurationMethodEnum configurateMethod: ConfigurationMethodEnum
currentCustomConfigurationModelFixedFields?: CustomConfigurationModelFixedFields currentCustomConfigurationModelFixedFields?: CustomConfigurationModelFixedFields
credential?: Credential
onCancel: () => void onCancel: () => void
onSave: () => void onSave: () => void
} }
const ModelModal: FC<ModelModalProps> = ({ const ModelModal: FC<ModelModalProps> = ({
provider, provider,
model,
configurateMethod, configurateMethod,
currentCustomConfigurationModelFixedFields, currentCustomConfigurationModelFixedFields,
credential, credential,
@ -68,134 +62,62 @@ const ModelModal: FC<ModelModalProps> = ({
}) => { }) => {
const providerFormSchemaPredefined = configurateMethod === ConfigurationMethodEnum.predefinedModel const providerFormSchemaPredefined = configurateMethod === ConfigurationMethodEnum.predefinedModel
const { const {
credentials: formSchemasValue,
loadBalancing: originalConfig,
mutate,
isLoading, isLoading,
} = useProviderCredentialsAndLoadBalancing( data: credentialData = {},
provider.provider, } = useGetCredential(provider.provider, credential?.credential_id, model)
configurateMethod, const {
providerFormSchemaPredefined && provider.custom_configuration.status === CustomConfigurationStatusEnum.active, handleSaveCredential,
currentCustomConfigurationModelFixedFields, handleConfirmDelete,
credential?.credential_id, deleteCredentialId,
) closeConfirmDelete,
openConfirmDelete,
doingAction,
} = useAuth(provider, configurateMethod, currentCustomConfigurationModelFixedFields, onSave)
const {
credentials: formSchemasValue,
} = credentialData as any
const { isCurrentWorkspaceManager } = useAppContext() const { isCurrentWorkspaceManager } = useAppContext()
const isEditMode = !!formSchemasValue && isCurrentWorkspaceManager const isEditMode = !!formSchemasValue && isCurrentWorkspaceManager
const { t } = useTranslation() const { t } = useTranslation()
const { notify } = useToastContext()
const language = useLanguage() const language = useLanguage()
const [loading, setLoading] = useState(false) const { formSchemas } = useModelFormSchemas(provider, providerFormSchemaPredefined)
const [showConfirm, setShowConfirm] = useState(false)
const [draftConfig, setDraftConfig] = useState<ModelLoadBalancingConfig>()
const originalConfigMap = useMemo(() => {
if (!originalConfig)
return {}
return originalConfig?.configs.reduce((prev, config) => {
if (config.id)
prev[config.id] = config
return prev
}, {} as Record<string, ModelLoadBalancingConfigEntry>)
}, [originalConfig])
useEffect(() => {
if (originalConfig && !draftConfig)
setDraftConfig(originalConfig)
}, [draftConfig, originalConfig])
const { formSchemas } = useModelFormSchemas(provider, providerFormSchemaPredefined, draftConfig)
const formRef = useRef<FormRefObject>(null) const formRef = useRef<FormRefObject>(null)
const extendedSecretFormSchemas = useMemo( const handleSave = useCallback(async () => {
() => const {
(providerFormSchemaPredefined isCheckValidated,
? provider.provider_credential_schema.credential_form_schemas values,
: [ } = formRef.current?.getFormValues({
genModelTypeFormSchema(provider.supported_model_types), needCheckValidatedValues: true,
genModelNameFormSchema(provider.model_credential_schema?.model), needTransformWhenSecretFieldIsPristine: true,
...provider.model_credential_schema.credential_form_schemas, }) || { isCheckValidated: false, values: {} }
]).filter(({ type }) => type === FormTypeEnum.secretInput), if (!isCheckValidated)
[ return
provider.model_credential_schema?.credential_form_schemas,
provider.model_credential_schema?.model,
provider.provider_credential_schema?.credential_form_schemas,
provider.supported_model_types,
providerFormSchemaPredefined,
],
)
const encodeConfigEntrySecretValues = useCallback((entry: ModelLoadBalancingConfigEntry) => { const {
const result = { ...entry } __authorization_name__,
extendedSecretFormSchemas.forEach(({ variable }) => { __model_name,
if (entry.id && result.credentials[variable] === originalConfigMap[entry.id]?.credentials?.[variable]) __model_type,
result.credentials[variable] = '[__HIDDEN__]' ...rest
}) } = values
return result if (__model_name && __model_type) {
}, [extendedSecretFormSchemas, originalConfigMap]) handleSaveCredential({
credential_id: credential?.credential_id,
const handleSave = async () => { credentials: rest,
try { name: __authorization_name__,
setLoading(true) model: __model_name,
const { model_type: __model_type,
isCheckValidated, })
values,
} = formRef.current?.getFormValues({
needCheckValidatedValues: true,
needTransformWhenSecretFieldIsPristine: true,
}) || { isCheckValidated: false, values: {} }
if (!isCheckValidated)
return
const res = await saveCredentials(
providerFormSchemaPredefined,
provider.provider,
values,
{
...draftConfig,
enabled: Boolean(draftConfig?.enabled),
configs: draftConfig?.configs.map(encodeConfigEntrySecretValues) || [],
},
)
if (res.result === 'success') {
notify({ type: 'success', message: t('common.actionMsg.modifiedSuccessfully') })
mutate()
onSave()
onCancel()
}
} }
finally { else {
setLoading(false) handleSaveCredential({
credential_id: credential?.credential_id,
credentials: rest,
name: __authorization_name__,
})
} }
} }, [handleSaveCredential, credential?.credential_id, model])
const handleRemove = async () => {
try {
setLoading(true)
const {
isCheckValidated,
values,
} = formRef.current?.getFormValues({
needCheckValidatedValues: true,
needTransformWhenSecretFieldIsPristine: true,
}) || { isCheckValidated: false, values: {} }
if (!isCheckValidated)
return
const res = await removeCredentials(
providerFormSchemaPredefined,
provider.provider,
values,
credential?.credential_id,
)
if (res.result === 'success') {
notify({ type: 'success', message: t('common.actionMsg.modifiedSuccessfully') })
// mutate()
onSave()
onCancel()
}
}
finally {
setLoading(false)
}
}
const renderTitlePrefix = () => { const renderTitlePrefix = () => {
const prefix = isEditMode ? t('common.operation.setup') : t('common.operation.add') const prefix = isEditMode ? t('common.operation.setup') : t('common.operation.add')
@ -239,20 +161,6 @@ const ModelModal: FC<ModelModalProps> = ({
/> />
) )
} }
{
!!draftConfig && (
<>
<div className='mb-4 mt-1 border-t-[0.5px] border-t-divider-regular' />
<ModelLoadBalancingConfigs withSwitch {...{
draftConfig,
setDraftConfig,
provider,
currentCustomConfigurationModelFixedFields,
configurationMethod: configurateMethod,
}} />
</>
)
}
</div> </div>
<div className='sticky bottom-0 -mx-2 mt-2 flex flex-wrap items-center justify-between gap-y-2 bg-components-panel-bg px-2 pb-6 pt-4'> <div className='sticky bottom-0 -mx-2 mt-2 flex flex-wrap items-center justify-between gap-y-2 bg-components-panel-bg px-2 pb-6 pt-4'>
@ -278,7 +186,7 @@ const ModelModal: FC<ModelModalProps> = ({
variant='warning' variant='warning'
size='large' size='large'
className='mr-2' className='mr-2'
onClick={() => setShowConfirm(true)} onClick={() => openConfirmDelete(credential?.credential_id, model)}
> >
{t('common.operation.remove')} {t('common.operation.remove')}
</Button> </Button>
@ -295,11 +203,7 @@ const ModelModal: FC<ModelModalProps> = ({
size='large' size='large'
variant='primary' variant='primary'
onClick={handleSave} onClick={handleSave}
disabled={ disabled={isLoading || doingAction}
loading
|| (draftConfig?.enabled && (draftConfig?.configs.filter(config => config.enabled).length ?? 0) < 2)
}
> >
{t('common.operation.save')} {t('common.operation.save')}
</Button> </Button>
@ -322,12 +226,13 @@ const ModelModal: FC<ModelModalProps> = ({
</div> </div>
</div> </div>
{ {
showConfirm && ( deleteCredentialId && (
<Confirm <Confirm
isShow
title={t('common.modelProvider.confirmDelete')} title={t('common.modelProvider.confirmDelete')}
isShow={showConfirm} isDisabled={doingAction}
onCancel={() => setShowConfirm(false)} onCancel={closeConfirmDelete}
onConfirm={handleRemove} onConfirm={handleConfirmDelete}
/> />
) )
} }

View File

@ -1,8 +1,6 @@
import { useMemo } from 'react' import { useMemo } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { RiEqualizer2Line } from '@remixicon/react'
import type { import type {
Credential,
ModelProvider, ModelProvider,
} from '../declarations' } from '../declarations'
import { import {
@ -18,22 +16,18 @@ import PrioritySelector from './priority-selector'
import PriorityUseTip from './priority-use-tip' import PriorityUseTip from './priority-use-tip'
import { UPDATE_MODEL_PROVIDER_CUSTOM_MODEL_LIST } from './index' import { UPDATE_MODEL_PROVIDER_CUSTOM_MODEL_LIST } from './index'
import Indicator from '@/app/components/header/indicator' import Indicator from '@/app/components/header/indicator'
import Button from '@/app/components/base/button'
import { changeModelProviderPriority } from '@/service/common' import { changeModelProviderPriority } from '@/service/common'
import { useToastContext } from '@/app/components/base/toast' import { useToastContext } from '@/app/components/base/toast'
import { useEventEmitterContextContext } from '@/context/event-emitter' import { useEventEmitterContextContext } from '@/context/event-emitter'
import Authorized from '../model-auth/authorized'
import cn from '@/utils/classnames' import cn from '@/utils/classnames'
import { useCredentialStatus } from '@/app/components/header/account-setting/model-provider-page/model-auth/hooks'
import { ConfigProvider } from '@/app/components/header/account-setting/model-provider-page/model-auth'
type CredentialPanelProps = { type CredentialPanelProps = {
provider: ModelProvider provider: ModelProvider
onSetup: (credential?: Credential) => void
onUpdate: () => void
} }
const CredentialPanel = ({ const CredentialPanel = ({
provider, provider,
onSetup,
onUpdate,
}: CredentialPanelProps) => { }: CredentialPanelProps) => {
const { t } = useTranslation() const { t } = useTranslation()
const { notify } = useToastContext() const { notify } = useToastContext()
@ -46,13 +40,11 @@ const CredentialPanel = ({
const isCustomConfigured = customConfig.status === CustomConfigurationStatusEnum.active const isCustomConfigured = customConfig.status === CustomConfigurationStatusEnum.active
const configurateMethods = provider.configurate_methods const configurateMethods = provider.configurate_methods
const { const {
current_credential_id, hasCredential,
authorized,
authRemoved,
current_credential_name, current_credential_name,
available_credentials, } = useCredentialStatus(provider)
} = provider.custom_configuration
const hasCredential = !!available_credentials?.length
const authorized = current_credential_id && current_credential_name
const authRemoved = hasCredential && !current_credential_id && !current_credential_name
const handleChangePriority = async (key: PreferredProviderTypeEnum) => { const handleChangePriority = async (key: PreferredProviderTypeEnum) => {
const res = await changeModelProviderPriority({ const res = await changeModelProviderPriority({
@ -108,31 +100,10 @@ const CredentialPanel = ({
<Indicator className='shrink-0' color={authorized ? 'green' : 'red'} /> <Indicator className='shrink-0' color={authorized ? 'green' : 'red'} />
</div> </div>
<div className='flex items-center gap-0.5'> <div className='flex items-center gap-0.5'>
{ <ConfigProvider
!hasCredential && ( provider={provider}
<Button configurationMethod={ConfigurationMethodEnum.predefinedModel}
className='grow' />
size='small'
onClick={() => onSetup()}
variant={!authorized ? 'secondary-accent' : 'secondary'}
>
<RiEqualizer2Line className='mr-1 h-3.5 w-3.5' />
{t('common.operation.setup')}
</Button>
)
}
{
(hasCredential || authRemoved) && (
<Authorized
provider={provider.provider}
onSetup={onSetup}
credentials={available_credentials ?? []}
selectedCredentialId={current_credential_id}
showItemSelectedIcon
onUpdate={onUpdate}
/>
)
}
{ {
systemConfig.enabled && isCustomConfigured && ( systemConfig.enabled && isCustomConfigured && (
<PrioritySelector <PrioritySelector

View File

@ -28,7 +28,6 @@ import { useEventEmitterContextContext } from '@/context/event-emitter'
import { IS_CE_EDITION } from '@/config' import { IS_CE_EDITION } from '@/config'
import { useAppContext } from '@/context/app-context' import { useAppContext } from '@/context/app-context'
import cn from '@/utils/classnames' import cn from '@/utils/classnames'
import { useRefreshModel } from '../hooks'
export const UPDATE_MODEL_PROVIDER_CUSTOM_MODEL_LIST = 'UPDATE_MODEL_PROVIDER_CUSTOM_MODEL_LIST' export const UPDATE_MODEL_PROVIDER_CUSTOM_MODEL_LIST = 'UPDATE_MODEL_PROVIDER_CUSTOM_MODEL_LIST'
type ProviderAddedCardProps = { type ProviderAddedCardProps = {
@ -82,8 +81,6 @@ const ProviderAddedCard: FC<ProviderAddedCardProps> = ({
getModelList(v.payload) getModelList(v.payload)
}) })
const { handleRefreshModel } = useRefreshModel()
return ( return (
<div <div
className={cn( className={cn(
@ -118,8 +115,6 @@ const ProviderAddedCard: FC<ProviderAddedCardProps> = ({
{ {
showCredential && ( showCredential && (
<CredentialPanel <CredentialPanel
onSetup={(credential?: Credential) => onOpenModal(ConfigurationMethodEnum.predefinedModel, undefined, credential)}
onUpdate={() => handleRefreshModel(provider, ConfigurationMethodEnum.predefinedModel, undefined)}
provider={provider} provider={provider}
/> />
) )

View File

@ -5,6 +5,7 @@ import {
RiArrowRightSLine, RiArrowRightSLine,
} from '@remixicon/react' } from '@remixicon/react'
import type { import type {
Credential,
CustomConfigurationModelFixedFields, CustomConfigurationModelFixedFields,
ModelItem, ModelItem,
ModelProvider, ModelProvider,
@ -13,10 +14,11 @@ import {
ConfigurationMethodEnum, ConfigurationMethodEnum,
} from '../declarations' } from '../declarations'
// import Tab from './tab' // import Tab from './tab'
import AddModelButton from './add-model-button'
import ModelListItem from './model-list-item' import ModelListItem from './model-list-item'
import { useModalContextSelector } from '@/context/modal-context' import { useModalContextSelector } from '@/context/modal-context'
import { useAppContext } from '@/context/app-context' import { useAppContext } from '@/context/app-context'
import { useCustomModels } from '@/app/components/header/account-setting/model-provider-page/model-auth/hooks'
import { AddCustomModel } from '@/app/components/header/account-setting/model-provider-page/model-auth'
type ModelListProps = { type ModelListProps = {
provider: ModelProvider provider: ModelProvider
@ -36,11 +38,12 @@ const ModelList: FC<ModelListProps> = ({
const configurativeMethods = provider.configurate_methods.filter(method => method !== ConfigurationMethodEnum.fetchFromRemote) const configurativeMethods = provider.configurate_methods.filter(method => method !== ConfigurationMethodEnum.fetchFromRemote)
const { isCurrentWorkspaceManager } = useAppContext() const { isCurrentWorkspaceManager } = useAppContext()
const isConfigurable = configurativeMethods.includes(ConfigurationMethodEnum.customizableModel) const isConfigurable = configurativeMethods.includes(ConfigurationMethodEnum.customizableModel)
const customModels = useCustomModels(provider)
const setShowModelLoadBalancingModal = useModalContextSelector(state => state.setShowModelLoadBalancingModal) const setShowModelLoadBalancingModal = useModalContextSelector(state => state.setShowModelLoadBalancingModal)
const onModifyLoadBalancing = useCallback((model: ModelItem) => { const onModifyLoadBalancing = useCallback((model: ModelItem, credential?: Credential) => {
setShowModelLoadBalancingModal({ setShowModelLoadBalancingModal({
provider, provider,
credential,
model: model!, model: model!,
open: !!model, open: !!model,
onClose: () => setShowModelLoadBalancingModal(null), onClose: () => setShowModelLoadBalancingModal(null),
@ -65,17 +68,15 @@ const ModelList: FC<ModelListProps> = ({
<RiArrowRightSLine className='mr-0.5 h-4 w-4 rotate-90' /> <RiArrowRightSLine className='mr-0.5 h-4 w-4 rotate-90' />
</span> </span>
</span> </span>
{/* {
isConfigurable && canSystemConfig && (
<span className='flex items-center'>
<Tab active='all' onSelect={() => {}} />
</span>
)
} */}
{ {
isConfigurable && isCurrentWorkspaceManager && ( isConfigurable && isCurrentWorkspaceManager && (
<div className='flex grow justify-end'> <div className='flex grow justify-end'>
<AddModelButton onClick={() => onConfig()} /> <AddCustomModel
provider={provider}
configurationMethod={ConfigurationMethodEnum.customizableModel}
currentCustomConfigurationModelFixedFields={undefined}
models={customModels}
/>
</div> </div>
) )
} }

View File

@ -11,7 +11,7 @@ import classNames from '@/utils/classnames'
import Tooltip from '@/app/components/base/tooltip' import Tooltip from '@/app/components/base/tooltip'
import Switch from '@/app/components/base/switch' import Switch from '@/app/components/base/switch'
import { Balance } from '@/app/components/base/icons/src/vender/line/financeAndECommerce' import { Balance } from '@/app/components/base/icons/src/vender/line/financeAndECommerce'
import { Edit02, Plus02 } from '@/app/components/base/icons/src/vender/line/general' import { Edit02 } from '@/app/components/base/icons/src/vender/line/general'
import { AlertTriangle } from '@/app/components/base/icons/src/vender/solid/alertsAndFeedback' import { AlertTriangle } from '@/app/components/base/icons/src/vender/solid/alertsAndFeedback'
import { useModalContextSelector } from '@/context/modal-context' import { useModalContextSelector } from '@/context/modal-context'
import UpgradeBtn from '@/app/components/billing/upgrade-btn' import UpgradeBtn from '@/app/components/billing/upgrade-btn'
@ -19,6 +19,7 @@ import s from '@/app/components/custom/style.module.css'
import GridMask from '@/app/components/base/grid-mask' import GridMask from '@/app/components/base/grid-mask'
import { useProviderContextSelector } from '@/context/provider-context' import { useProviderContextSelector } from '@/context/provider-context'
import { IS_CE_EDITION } from '@/config' import { IS_CE_EDITION } from '@/config'
import { AddCredentialInLoadBalancing } from '@/app/components/header/account-setting/model-provider-page/model-auth'
export type ModelLoadBalancingConfigsProps = { export type ModelLoadBalancingConfigsProps = {
draftConfig?: ModelLoadBalancingConfig draftConfig?: ModelLoadBalancingConfig
@ -234,15 +235,10 @@ const ModelLoadBalancingConfigs = ({
</div> </div>
) )
})} })}
<AddCredentialInLoadBalancing
<div provider={provider}
className='mt-1 flex h-8 items-center px-3 text-[13px] font-medium text-primary-600' onSetup={() => toggleEntryModal()}
onClick={() => toggleEntryModal()} />
>
<div className='flex cursor-pointer items-center'>
<Plus02 className='mr-2 h-3 w-3' />{t('common.modelProvider.addConfig')}
</div>
</div>
</div> </div>
)} )}
{ {

View File

@ -1,7 +1,13 @@
import { memo, useCallback, useEffect, useMemo, useState } from 'react' import { memo, useCallback, useEffect, useMemo, useState } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import useSWR from 'swr' import useSWR from 'swr'
import type { ModelItem, ModelLoadBalancingConfig, ModelLoadBalancingConfigEntry, ModelProvider } from '../declarations' import type {
Credential,
ModelItem,
ModelLoadBalancingConfig,
ModelLoadBalancingConfigEntry,
ModelProvider,
} from '../declarations'
import { FormTypeEnum } from '../declarations' import { FormTypeEnum } from '../declarations'
import ModelIcon from '../model-icon' import ModelIcon from '../model-icon'
import ModelName from '../model-name' import ModelName from '../model-name'
@ -13,17 +19,26 @@ import Button from '@/app/components/base/button'
import { fetchModelLoadBalancingConfig } from '@/service/common' import { fetchModelLoadBalancingConfig } from '@/service/common'
import Loading from '@/app/components/base/loading' import Loading from '@/app/components/base/loading'
import { useToastContext } from '@/app/components/base/toast' import { useToastContext } from '@/app/components/base/toast'
import { SwitchCredentialInLoadBalancing } from '@/app/components/header/account-setting/model-provider-page/model-auth'
export type ModelLoadBalancingModalProps = { export type ModelLoadBalancingModalProps = {
provider: ModelProvider provider: ModelProvider
model: ModelItem model: ModelItem
credential?: Credential
open?: boolean open?: boolean
onClose?: () => void onClose?: () => void
onSave?: (provider: string) => void onSave?: (provider: string) => void
} }
// model balancing config modal // model balancing config modal
const ModelLoadBalancingModal = ({ provider, model, open = false, onClose, onSave }: ModelLoadBalancingModalProps) => { const ModelLoadBalancingModal = ({
provider,
model,
credential,
open = false,
onClose,
onSave,
}: ModelLoadBalancingModalProps) => {
const { t } = useTranslation() const { t } = useTranslation()
const { notify } = useToastContext() const { notify } = useToastContext()
@ -152,6 +167,11 @@ const ModelLoadBalancingModal = ({ provider, model, open = false, onClose, onSav
<div className='text-sm text-text-secondary'>{t('common.modelProvider.providerManaged')}</div> <div className='text-sm text-text-secondary'>{t('common.modelProvider.providerManaged')}</div>
<div className='text-xs text-text-tertiary'>{t('common.modelProvider.providerManagedDescription')}</div> <div className='text-xs text-text-tertiary'>{t('common.modelProvider.providerManagedDescription')}</div>
</div> </div>
<SwitchCredentialInLoadBalancing
draftConfig={draftConfig}
setDraftConfig={setDraftConfig}
provider={provider}
/>
</div> </div>
</div> </div>

View File

@ -1,6 +1,5 @@
import { ValidatedStatus } from '../key-validator/declarations' import { ValidatedStatus } from '../key-validator/declarations'
import type { import type {
CredentialFormSchemaRadio,
CredentialFormSchemaTextInput, CredentialFormSchemaTextInput,
FormValue, FormValue,
ModelLoadBalancingConfig, ModelLoadBalancingConfig,
@ -181,7 +180,7 @@ export const genModelTypeFormSchema = (modelTypes: ModelTypeEnum[]) => {
show_on: [], show_on: [],
} }
}), }),
} as CredentialFormSchemaRadio } as any
} }
export const genModelNameFormSchema = (model?: Pick<CredentialFormSchemaTextInput, 'label' | 'placeholder'>) => { export const genModelNameFormSchema = (model?: Pick<CredentialFormSchemaTextInput, 'label' | 'placeholder'>) => {
@ -198,5 +197,5 @@ export const genModelNameFormSchema = (model?: Pick<CredentialFormSchemaTextInpu
zh_Hans: '请输入模型名称', zh_Hans: '请输入模型名称',
en_US: 'Please enter model name', en_US: 'Please enter model name',
}, },
} as CredentialFormSchemaTextInput } as any
} }

View File

@ -8,6 +8,7 @@ import type {
ConfigurationMethodEnum, ConfigurationMethodEnum,
Credential, Credential,
CustomConfigurationModelFixedFields, CustomConfigurationModelFixedFields,
CustomModel,
ModelLoadBalancingConfigEntry, ModelLoadBalancingConfigEntry,
ModelProvider, ModelProvider,
} from '@/app/components/header/account-setting/model-provider-page/declarations' } from '@/app/components/header/account-setting/model-provider-page/declarations'
@ -81,6 +82,7 @@ export type ModelModalType = {
currentConfigurationMethod: ConfigurationMethodEnum currentConfigurationMethod: ConfigurationMethodEnum
currentCustomConfigurationModelFixedFields?: CustomConfigurationModelFixedFields currentCustomConfigurationModelFixedFields?: CustomConfigurationModelFixedFields
credential?: Credential credential?: Credential
model?: CustomModel
} }
export type LoadBalancingEntryModalType = ModelModalType & { export type LoadBalancingEntryModalType = ModelModalType & {
entry?: ModelLoadBalancingConfigEntry entry?: ModelLoadBalancingConfigEntry
@ -338,6 +340,7 @@ export const ModalContextProvider = ({
<ModelModal <ModelModal
provider={showModelModal.payload.currentProvider} provider={showModelModal.payload.currentProvider}
configurateMethod={showModelModal.payload.currentConfigurationMethod} configurateMethod={showModelModal.payload.currentConfigurationMethod}
model={showModelModal.payload.model}
currentCustomConfigurationModelFixedFields={showModelModal.payload.currentCustomConfigurationModelFixedFields} currentCustomConfigurationModelFixedFields={showModelModal.payload.currentCustomConfigurationModelFixedFields}
credential={showModelModal.payload.credential} credential={showModelModal.payload.credential}
onCancel={handleCancelModelModal} onCancel={handleCancelModelModal}

View File

@ -2,9 +2,13 @@ import {
del, del,
get, get,
post, post,
put,
} from './base' } from './base'
import type { import type {
ModelCredential,
ModelItem, ModelItem,
ModelTypeEnum,
ProviderCredential,
} from '@/app/components/header/account-setting/model-provider-page/declarations' } from '@/app/components/header/account-setting/model-provider-page/declarations'
import { import {
useMutation, useMutation,
@ -21,35 +25,114 @@ export const useModelProviderModelList = (provider: string) => {
}) })
} }
export const useAddModelCredential = (providerName: string) => { export const useGetProviderCredential = (enabled: boolean, provider: string, credentialId?: string) => {
return useMutation({
mutationFn: (data: any) => post<{ result: string }>(`/workspaces/current/model-providers/${providerName}/credentials`, data),
})
}
export const useGetModelCredential = (providerName: string, credentialId: string) => {
return useQuery({ return useQuery({
queryKey: [NAME_SPACE, 'model-credential', providerName, credentialId], enabled,
queryFn: () => get<{ data: Credential[] }>(`/workspaces/current/model-providers/${providerName}/credentials?credential_id=${credentialId}`), queryKey: [NAME_SPACE, 'model-list', provider, credentialId],
queryFn: () => get<{ data: ProviderCredential }>(`/workspaces/current/model-providers/${provider}/credentials${credentialId ? `?credential_id=${credentialId}` : ''}`),
}) })
} }
export const useDeleteModelCredential = (providerName: string) => { export const useAddProviderCredential = (provider: string) => {
return useMutation({ return useMutation({
mutationFn: (credentialId: string) => del<{ result: string }>(`/workspaces/current/model-providers/${providerName}/credentials`, { mutationFn: (data: ProviderCredential) => post<{ result: string }>(`/workspaces/current/model-providers/${provider}/credentials`, {
body: { body: data,
credential_id: credentialId,
},
}), }),
}) })
} }
export const useSetModelCredentialDefault = (providerName: string) => { export const useEditProviderCredential = (provider: string) => {
return useMutation({ return useMutation({
mutationFn: (credentialId: string) => post<{ result: string }>(`/workspaces/current/model-providers/${providerName}/credentials/switch`, { mutationFn: (data: ProviderCredential) => put<{ result: string }>(`/workspaces/current/model-providers/${provider}/credentials`, {
body: { body: data,
credential_id: credentialId, }),
}, })
}
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],
queryFn: () => get<{ data: ModelCredential }>(`/workspaces/current/model-providers/${provider}/models/credentials?model=${model}&model_type=${modelType}$credential_id=${credentialId}$config_from=${configFrom}`),
})
}
export const useAddModelCredential = (provider: string) => {
return useMutation({
mutationFn: (data: ModelCredential) => post<{ result: string }>(`/workspaces/current/model-providers/${provider}/models/credentials`, {
body: data,
}),
})
}
export const useEditModelCredential = (provider: string) => {
return useMutation({
mutationFn: (data: ModelCredential) => 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`, {
body: data,
}),
})
}
export const useDeleteModel = (provider: string) => {
return useMutation({
mutationFn: (data: {
model: string
model_type: ModelTypeEnum
}) => del<{ result: string }>(`/workspaces/current/model-providers/${provider}/models/credentials`, {
body: 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,
}), }),
}) })
} }