model auth

This commit is contained in:
zxhlyh 2025-08-18 18:03:36 +08:00
parent 2e28a64d38
commit 473b465efb
31 changed files with 493 additions and 631 deletions

View File

@ -30,7 +30,7 @@ const BaseField = ({
inputClassName, inputClassName,
formSchema, formSchema,
field, field,
disabled, disabled: propsDisabled,
}: BaseFieldProps) => { }: BaseFieldProps) => {
const renderI18nObject = useRenderI18nObject() const renderI18nObject = useRenderI18nObject()
const { const {
@ -40,7 +40,9 @@ const BaseField = ({
options, options,
labelClassName: formLabelClassName, labelClassName: formLabelClassName,
show_on = [], show_on = [],
disabled: formSchemaDisabled,
} = formSchema } = formSchema
const disabled = propsDisabled || formSchemaDisabled
const memorizedLabel = useMemo(() => { const memorizedLabel = useMemo(() => {
if (isValidElement(label)) if (isValidElement(label))
@ -182,9 +184,10 @@ const BaseField = ({
className={cn( className={cn(
'system-sm-regular hover:bg-components-option-card-option-hover-bg hover:border-components-option-card-option-hover-border flex h-8 flex-[1] grow cursor-pointer items-center justify-center rounded-lg border border-components-option-card-option-border bg-components-option-card-option-bg p-2 text-text-secondary', 'system-sm-regular hover:bg-components-option-card-option-hover-bg hover:border-components-option-card-option-hover-border flex h-8 flex-[1] grow cursor-pointer items-center justify-center rounded-lg border border-components-option-card-option-border bg-components-option-card-option-bg p-2 text-text-secondary',
value === option.value && 'border-components-option-card-option-selected-border bg-components-option-card-option-selected-bg text-text-primary shadow-xs', value === option.value && 'border-components-option-card-option-selected-border bg-components-option-card-option-selected-bg text-text-primary shadow-xs',
disabled && 'cursor-not-allowed opacity-50',
inputClassName, inputClassName,
)} )}
onClick={() => field.handleChange(option.value)} onClick={() => !disabled && field.handleChange(option.value)}
> >
{ {
formSchema.showRadioUI && ( formSchema.showRadioUI && (

View File

@ -59,6 +59,7 @@ export type FormSchema = {
labelClassName?: string labelClassName?: string
validators?: AnyValidators validators?: AnyValidators
showRadioUI?: boolean showRadioUI?: boolean
disabled?: boolean
} }
export type FormValues = Record<string, any> export type FormValues = Record<string, any>

View File

@ -183,7 +183,7 @@ export type QuotaConfiguration = {
export type Credential = { export type Credential = {
credential_id: string credential_id: string
credential_name: string credential_name?: string
} }
export type CustomModel = { export type CustomModel = {

View File

@ -354,6 +354,7 @@ export const useModelModalHandler = () => {
provider: ModelProvider, provider: ModelProvider,
configurationMethod: ConfigurationMethodEnum, configurationMethod: ConfigurationMethodEnum,
CustomConfigurationModelFixedFields?: CustomConfigurationModelFixedFields, CustomConfigurationModelFixedFields?: CustomConfigurationModelFixedFields,
isModelCredential?: boolean,
credential?: Credential, credential?: Credential,
model?: CustomModel, model?: CustomModel,
) => { ) => {
@ -362,6 +363,7 @@ export const useModelModalHandler = () => {
currentProvider: provider, currentProvider: provider,
currentConfigurationMethod: configurationMethod, currentConfigurationMethod: configurationMethod,
currentCustomConfigurationModelFixedFields: CustomConfigurationModelFixedFields, currentCustomConfigurationModelFixedFields: CustomConfigurationModelFixedFields,
isModelCredential,
credential, credential,
model, model,
}, },

View File

@ -8,9 +8,6 @@ import {
import SystemModelSelector from './system-model-selector' import SystemModelSelector from './system-model-selector'
import ProviderAddedCard from './provider-added-card' import ProviderAddedCard from './provider-added-card'
import type { import type {
ConfigurationMethodEnum,
Credential,
CustomConfigurationModelFixedFields,
ModelProvider, ModelProvider,
} from './declarations' } from './declarations'
import { import {
@ -19,7 +16,6 @@ import {
} from './declarations' } from './declarations'
import { import {
useDefaultModel, useDefaultModel,
useModelModalHandler,
} from './hooks' } from './hooks'
import InstallFromMarketplace from './install-from-marketplace' import InstallFromMarketplace from './install-from-marketplace'
import { useProviderContext } from '@/context/provider-context' import { useProviderContext } from '@/context/provider-context'
@ -85,8 +81,6 @@ const ModelProviderPage = ({ searchText }: Props) => {
return [filteredConfiguredProviders, filteredNotConfiguredProviders] return [filteredConfiguredProviders, filteredNotConfiguredProviders]
}, [configuredProviders, debouncedSearchText, notConfiguredProviders]) }, [configuredProviders, debouncedSearchText, notConfiguredProviders])
const handleOpenModal = useModelModalHandler()
return ( return (
<div className='relative -mt-2 pt-1'> <div className='relative -mt-2 pt-1'>
<div className={cn('mb-2 flex items-center')}> <div className={cn('mb-2 flex items-center')}>
@ -127,7 +121,6 @@ const ModelProviderPage = ({ searchText }: Props) => {
<ProviderAddedCard <ProviderAddedCard
key={provider.provider} key={provider.provider}
provider={provider} provider={provider}
onOpenModal={(configurationMethod: ConfigurationMethodEnum, currentCustomConfigurationModelFixedFields?: CustomConfigurationModelFixedFields, credential?: Credential) => handleOpenModal(provider, configurationMethod, currentCustomConfigurationModelFixedFields, credential)}
/> />
))} ))}
</div> </div>
@ -141,7 +134,6 @@ const ModelProviderPage = ({ searchText }: Props) => {
notConfigured notConfigured
key={provider.provider} key={provider.provider}
provider={provider} provider={provider}
onOpenModal={(configurationMethod: ConfigurationMethodEnum, currentCustomConfigurationModelFixedFields?: CustomConfigurationModelFixedFields, credential?: Credential) => handleOpenModal(provider, configurationMethod, currentCustomConfigurationModelFixedFields, credential)}
/> />
))} ))}
</div> </div>

View File

@ -3,39 +3,70 @@ import {
useCallback, useCallback,
} from 'react' } from 'react'
import { RiAddLine } from '@remixicon/react' import { RiAddLine } from '@remixicon/react'
import { useTranslation } from 'react-i18next'
import { Authorized } from '@/app/components/header/account-setting/model-provider-page/model-auth' import { Authorized } from '@/app/components/header/account-setting/model-provider-page/model-auth'
import cn from '@/utils/classnames' import cn from '@/utils/classnames'
import type { import type {
Credential, Credential,
CustomModelCredential,
ModelCredential,
ModelProvider, ModelProvider,
} from '@/app/components/header/account-setting/model-provider-page/declarations' } from '@/app/components/header/account-setting/model-provider-page/declarations'
import { ConfigurationMethodEnum } from '@/app/components/header/account-setting/model-provider-page/declarations'
type AddCredentialInLoadBalancingProps = { type AddCredentialInLoadBalancingProps = {
provider: ModelProvider provider: ModelProvider
onSetup: (credential?: Credential) => void model: CustomModelCredential
configurationMethod: ConfigurationMethodEnum
modelCredential: ModelCredential
onSelectCredential: (credential: Credential) => void
onUpdate?: () => void
} }
const AddCredentialInLoadBalancing = ({ const AddCredentialInLoadBalancing = ({
provider, provider,
onSetup, model,
configurationMethod,
modelCredential,
onSelectCredential,
onUpdate,
}: AddCredentialInLoadBalancingProps) => { }: AddCredentialInLoadBalancingProps) => {
const { t } = useTranslation()
const {
available_credentials,
} = modelCredential
const customModel = configurationMethod === ConfigurationMethodEnum.customizableModel
const renderTrigger = useCallback((open?: boolean) => { const renderTrigger = useCallback((open?: boolean) => {
return ( return (
<div className={cn( <div className={cn(
'flex h-8 items-center rounded-lg px-3 hover:bg-state-base-hover', 'system-sm-medium flex h-8 items-center rounded-lg px-3 text-text-accent hover:bg-state-base-hover',
open && 'bg-state-base-hover', open && 'bg-state-base-hover',
)}> )}>
<RiAddLine className='system-sm-medium h-4 w-4 text-text-accent' /> <RiAddLine className='mr-2 h-4 w-4' />
Add credential {
customModel
? t('common.modelProvider.auth.addCredential')
: t('common.modelProvider.auth.addApiKey')
}
</div> </div>
) )
}, []) }, [])
return ( return (
<Authorized <Authorized
credentials={[]} provider={provider}
provider={provider.provider}
renderTrigger={renderTrigger} renderTrigger={renderTrigger}
onSetup={onSetup} items={[
{
title: customModel ? t('common.modelProvider.auth.modelCredentials') : t('common.modelProvider.auth.apiKeys'),
model,
credentials: available_credentials ?? [],
},
]}
configurationMethod={configurationMethod}
onItemClick={onSelectCredential}
placement='bottom-start'
onUpdate={onUpdate}
isModelCredential={customModel}
/> />
) )
} }

View File

@ -12,52 +12,60 @@ import {
} from '@/app/components/base/button' } from '@/app/components/base/button'
import type { import type {
CustomConfigurationModelFixedFields, CustomConfigurationModelFixedFields,
CustomModelCredential,
ModelProvider, ModelProvider,
} from '@/app/components/header/account-setting/model-provider-page/declarations' } from '@/app/components/header/account-setting/model-provider-page/declarations'
import { ConfigurationMethodEnum } 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 Authorized from './authorized'
import { useAuth } from './hooks' import {
useAuth,
useCustomModels,
} from './hooks'
import cn from '@/utils/classnames' import cn from '@/utils/classnames'
type AddCustomModelProps = { type AddCustomModelProps = {
provider: ModelProvider, provider: ModelProvider,
configurationMethod: ConfigurationMethodEnum, configurationMethod: ConfigurationMethodEnum,
currentCustomConfigurationModelFixedFields?: CustomConfigurationModelFixedFields, currentCustomConfigurationModelFixedFields?: CustomConfigurationModelFixedFields,
models: CustomModelCredential[]
} }
const AddCustomModel = ({ const AddCustomModel = ({
provider, provider,
configurationMethod, configurationMethod,
currentCustomConfigurationModelFixedFields, currentCustomConfigurationModelFixedFields,
models,
}: AddCustomModelProps) => { }: AddCustomModelProps) => {
const { t } = useTranslation() const { t } = useTranslation()
const noModels = !models.length const customModels = useCustomModels(provider)
const noModels = !customModels.length
const { const {
handleOpenModal, handleOpenModal,
} = useAuth(provider, configurationMethod, currentCustomConfigurationModelFixedFields) } = useAuth(provider, configurationMethod, currentCustomConfigurationModelFixedFields, true)
const handleClick = useCallback(() => { const handleClick = useCallback(() => {
if (noModels) handleOpenModal()
handleOpenModal() }, [handleOpenModal])
}, [handleOpenModal, noModels])
const ButtonComponent = useMemo(() => { const ButtonComponent = useMemo(() => {
return ( return (
<Button <Button
variant='ghost-accent' variant='ghost-accent'
size='small' size='small'
onClick={handleClick} onClick={handleClick}
className={cn(noModels && 'text-text-accent')}
> >
<RiAddCircleFill className='mr-1 h-3.5 w-3.5' /> <RiAddCircleFill className='mr-1 h-3.5 w-3.5' />
{t('common.modelProvider.addModel')} {t('common.modelProvider.addModel')}
</Button> </Button>
) )
}, [handleClick, noModels]) }, [handleClick])
const renderTrigger = useCallback(() => { const renderTrigger = useCallback((open?: boolean) => {
return ButtonComponent return (
}, [ButtonComponent]) <Button
variant='ghost'
size='small'
className={cn(open && 'bg-components-button-ghost-bg-hover')}
>
<RiAddCircleFill className='mr-1 h-3.5 w-3.5' />
{t('common.modelProvider.addModel')}
</Button>
)
}, [t])
if (noModels) if (noModels)
return ButtonComponent return ButtonComponent
@ -66,11 +74,14 @@ const AddCustomModel = ({
<Authorized <Authorized
provider={provider} provider={provider}
configurationMethod={ConfigurationMethodEnum.customizableModel} configurationMethod={ConfigurationMethodEnum.customizableModel}
items={models.map(model => ({ items={customModels.map(model => ({
model, model,
credentials: model.available_model_credentials ?? [], credentials: model.available_model_credentials ?? [],
}))} }))}
renderTrigger={renderTrigger} renderTrigger={renderTrigger}
isModelCredential
enableAddModelCredential
bottomAddModelCredentialText={t('common.modelProvider.auth.addNewModel')}
/> />
) )
} }

View File

@ -3,41 +3,51 @@ import {
useCallback, useCallback,
} from 'react' } from 'react'
import { RiAddLine } from '@remixicon/react' import { RiAddLine } from '@remixicon/react'
import { useTranslation } from 'react-i18next'
import CredentialItem from './credential-item' import CredentialItem from './credential-item'
import type { import type {
Credential, Credential,
CustomModel, CustomModel,
CustomModelCredential,
} from '../../declarations' } from '../../declarations'
import Button from '@/app/components/base/button' import Button from '@/app/components/base/button'
import Tooltip from '@/app/components/base/tooltip' import Tooltip from '@/app/components/base/tooltip'
type AuthorizedItemProps = { type AuthorizedItemProps = {
model?: CustomModel model?: CustomModelCredential
title?: string
disabled?: boolean disabled?: boolean
onDelete?: (id: string) => void onDelete?: (credential?: Credential, model?: CustomModel) => void
onEdit?: (model?: CustomModel, credential?: Credential) => void onEdit?: (credential?: Credential, model?: CustomModel) => void
onSetDefault?: (id: string) => void
onItemClick?: (id: string) => void
showItemSelectedIcon?: boolean showItemSelectedIcon?: boolean
selectedCredentialId?: string selectedCredentialId?: string
disableSetDefault?: boolean
credentials: Credential[] credentials: Credential[]
onItemClick?: (credential: Credential, model?: CustomModel) => void
enableAddModelCredential?: boolean
} }
export const AuthorizedItem = ({ export const AuthorizedItem = ({
model, model,
title,
credentials, credentials,
disabled, disabled,
onDelete, onDelete,
onEdit, onEdit,
onSetDefault,
onItemClick,
showItemSelectedIcon, showItemSelectedIcon,
selectedCredentialId, selectedCredentialId,
disableSetDefault, onItemClick,
enableAddModelCredential,
}: AuthorizedItemProps) => { }: AuthorizedItemProps) => {
const { t } = useTranslation()
const handleEdit = useCallback((credential?: Credential) => { const handleEdit = useCallback((credential?: Credential) => {
onEdit?.(model, credential) onEdit?.(credential, model)
}, [onEdit, model]) }, [onEdit, model])
const handleDelete = useCallback((credential?: Credential) => {
onDelete?.(credential, model)
}, [onDelete, model])
const handleItemClick = useCallback((credential: Credential) => {
onItemClick?.(credential, model)
}, [onItemClick, model])
return ( return (
<div className='p-1'> <div className='p-1'>
{ {
@ -47,23 +57,28 @@ export const AuthorizedItem = ({
> >
<div className='h-5 w-5 shrink-0'></div> <div className='h-5 w-5 shrink-0'></div>
<div <div
className='system-md-medium mx-1 truncate text-text-primary' className='system-md-medium mx-1 grow truncate text-text-primary'
title={model.model} title={model.model}
> >
{model.model} {title ?? model.model}
</div> </div>
<Tooltip {
asChild enableAddModelCredential && (
popupContent='Add model credential' <Tooltip
> asChild
<Button popupContent={t('common.modelProvider.auth.addModelCredential')}
className='h-6 w-6 rounded-full p-0' >
size='small' <Button
variant='secondary-accent' className='h-6 w-6 shrink-0 rounded-full p-0'
> size='small'
<RiAddLine className='h-4 w-4' /> variant='secondary-accent'
</Button> onClick={() => handleEdit?.()}
</Tooltip> >
<RiAddLine className='h-4 w-4' />
</Button>
</Tooltip>
)
}
</div> </div>
) )
} }
@ -73,13 +88,11 @@ export const AuthorizedItem = ({
key={credential.credential_id} key={credential.credential_id}
credential={credential} credential={credential}
disabled={disabled} disabled={disabled}
onDelete={onDelete} onDelete={handleDelete}
onEdit={handleEdit} onEdit={handleEdit}
onSetDefault={onSetDefault}
onItemClick={onItemClick}
showSelectedIcon={showItemSelectedIcon} showSelectedIcon={showItemSelectedIcon}
selectedCredentialId={selectedCredentialId} selectedCredentialId={selectedCredentialId}
disableSetDefault={disableSetDefault} onItemClick={handleItemClick}
/> />
)) ))
} }

View File

@ -13,19 +13,16 @@ import ActionButton from '@/app/components/base/action-button'
import Tooltip from '@/app/components/base/tooltip' import Tooltip from '@/app/components/base/tooltip'
import cn from '@/utils/classnames' import cn from '@/utils/classnames'
import type { Credential } from '../../declarations' import type { Credential } from '../../declarations'
import Button from '@/app/components/base/button'
type CredentialItemProps = { type CredentialItemProps = {
credential: Credential credential: Credential
disabled?: boolean disabled?: boolean
onDelete?: (id: string) => void onDelete?: (credential: Credential) => void
onEdit?: (credential?: Credential) => void onEdit?: (credential?: Credential) => void
onSetDefault?: (id: string) => void onItemClick?: (credential: Credential) => void
disableRename?: boolean disableRename?: boolean
disableEdit?: boolean disableEdit?: boolean
disableDelete?: boolean disableDelete?: boolean
disableSetDefault?: boolean
onItemClick?: (id: string) => void
showSelectedIcon?: boolean showSelectedIcon?: boolean
selectedCredentialId?: string selectedCredentialId?: string
} }
@ -34,19 +31,17 @@ const CredentialItem = ({
disabled, disabled,
onDelete, onDelete,
onEdit, onEdit,
onSetDefault, onItemClick,
disableRename, disableRename,
disableEdit, disableEdit,
disableDelete, disableDelete,
disableSetDefault,
onItemClick,
showSelectedIcon, showSelectedIcon,
selectedCredentialId, selectedCredentialId,
}: CredentialItemProps) => { }: CredentialItemProps) => {
const { t } = useTranslation() const { t } = useTranslation()
const showAction = useMemo(() => { const showAction = useMemo(() => {
return !(disableRename && disableEdit && disableDelete && disableSetDefault) return !(disableRename && disableEdit && disableDelete)
}, [disableRename, disableEdit, disableDelete, disableSetDefault]) }, [disableRename, disableEdit, disableDelete])
return ( return (
<div <div
@ -54,7 +49,7 @@ const CredentialItem = ({
className={cn( className={cn(
'group flex h-8 items-center rounded-lg p-1 hover:bg-state-base-hover', 'group flex h-8 items-center rounded-lg p-1 hover:bg-state-base-hover',
)} )}
onClick={() => onItemClick?.(credential.credential_id)} onClick={() => onItemClick?.(credential)}
> >
<div className='flex w-0 grow items-center space-x-1.5'> <div className='flex w-0 grow items-center space-x-1.5'>
{ {
@ -79,20 +74,6 @@ const CredentialItem = ({
{ {
showAction && ( showAction && (
<div className='ml-2 hidden shrink-0 items-center group-hover:flex'> <div className='ml-2 hidden shrink-0 items-center group-hover:flex'>
{
!disableSetDefault && (
<Button
size='small'
disabled={disabled}
onClick={(e) => {
e.stopPropagation()
onSetDefault?.(credential.credential_id)
}}
>
{t('plugin.auth.setDefault')}
</Button>
)
}
{ {
!disableEdit && ( !disableEdit && (
<Tooltip popupContent={t('common.operation.edit')}> <Tooltip popupContent={t('common.operation.edit')}>
@ -116,7 +97,7 @@ const CredentialItem = ({
disabled={disabled} disabled={disabled}
onClick={(e) => { onClick={(e) => {
e.stopPropagation() e.stopPropagation()
onDelete?.(credential.credential_id) onDelete?.(credential)
}} }}
> >
<RiDeleteBinLine className='h-4 w-4 text-text-tertiary hover:text-text-destructive' /> <RiDeleteBinLine className='h-4 w-4 text-text-tertiary hover:text-text-destructive' />

View File

@ -4,6 +4,7 @@ import {
useState, useState,
} from 'react' } from 'react'
import { import {
RiAddLine,
RiEqualizer2Line, RiEqualizer2Line,
} from '@remixicon/react' } from '@remixicon/react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
@ -32,6 +33,7 @@ type AuthorizedProps = {
provider: ModelProvider, provider: ModelProvider,
configurationMethod: ConfigurationMethodEnum, configurationMethod: ConfigurationMethodEnum,
currentCustomConfigurationModelFixedFields?: CustomConfigurationModelFixedFields, currentCustomConfigurationModelFixedFields?: CustomConfigurationModelFixedFields,
isModelCredential?: boolean
items: { items: {
model?: CustomModel model?: CustomModel
credentials: Credential[] credentials: Credential[]
@ -45,16 +47,18 @@ type AuthorizedProps = {
placement?: PortalToFollowElemOptions['placement'] placement?: PortalToFollowElemOptions['placement']
triggerPopupSameWidth?: boolean triggerPopupSameWidth?: boolean
popupClassName?: string popupClassName?: string
onItemClick?: (id: string) => void
showItemSelectedIcon?: boolean showItemSelectedIcon?: boolean
onUpdate?: () => void onUpdate?: () => void
disableSetDefault?: boolean onItemClick?: (credential: Credential, model?: CustomModel) => void
enableAddModelCredential?: boolean
bottomAddModelCredentialText?: string
} }
const Authorized = ({ const Authorized = ({
provider, provider,
configurationMethod, configurationMethod,
currentCustomConfigurationModelFixedFields, currentCustomConfigurationModelFixedFields,
items, items,
isModelCredential,
selectedCredential, selectedCredential,
disabled, disabled,
renderTrigger, renderTrigger,
@ -64,10 +68,11 @@ const Authorized = ({
placement = 'bottom-end', placement = 'bottom-end',
triggerPopupSameWidth = false, triggerPopupSameWidth = false,
popupClassName, popupClassName,
onItemClick,
showItemSelectedIcon, showItemSelectedIcon,
onUpdate, onUpdate,
disableSetDefault, onItemClick,
enableAddModelCredential,
bottomAddModelCredentialText,
}: AuthorizedProps) => { }: AuthorizedProps) => {
const { t } = useTranslation() const { t } = useTranslation()
const [isLocalOpen, setIsLocalOpen] = useState(false) const [isLocalOpen, setIsLocalOpen] = useState(false)
@ -86,13 +91,20 @@ const Authorized = ({
handleConfirmDelete, handleConfirmDelete,
deleteCredentialId, deleteCredentialId,
handleOpenModal, handleOpenModal,
} = useAuth(provider, configurationMethod, currentCustomConfigurationModelFixedFields, onUpdate) } = useAuth(provider, configurationMethod, currentCustomConfigurationModelFixedFields, isModelCredential, onUpdate)
const handleEdit = useCallback((model?: CustomModel, credential?: Credential) => { const handleEdit = useCallback((credential?: Credential, model?: CustomModel) => {
handleOpenModal(model, credential) handleOpenModal(credential, model)
setMergedIsOpen(false) setMergedIsOpen(false)
}, [handleOpenModal, setMergedIsOpen]) }, [handleOpenModal, setMergedIsOpen])
const handleItemClick = useCallback((credential: Credential, model?: CustomModel) => {
if (!onItemClick)
return handleActiveCredential(credential, model)
onItemClick?.(credential, model)
}, [handleActiveCredential, onItemClick])
return ( return (
<> <>
<PortalToFollowElem <PortalToFollowElem
@ -135,24 +147,38 @@ const Authorized = ({
disabled={disabled} disabled={disabled}
onDelete={openConfirmDelete} onDelete={openConfirmDelete}
onEdit={handleEdit} onEdit={handleEdit}
onSetDefault={handleActiveCredential}
onItemClick={onItemClick}
showItemSelectedIcon={showItemSelectedIcon} showItemSelectedIcon={showItemSelectedIcon}
selectedCredentialId={selectedCredential?.credential_id} selectedCredentialId={selectedCredential?.credential_id}
disableSetDefault={disableSetDefault} onItemClick={handleItemClick}
enableAddModelCredential={enableAddModelCredential}
/> />
)) ))
} }
</div> </div>
<div className='h-[1px] bg-divider-subtle'></div> <div className='h-[1px] bg-divider-subtle'></div>
<div className='p-2'> {
<Button isModelCredential && (
onClick={() => handleOpenModal()} <div
className='w-full' onClick={() => handleEdit()}
> className='system-xs-medium flex h-[30px] cursor-pointer items-center px-3 text-text-accent-light-mode-only'
add api key >
</Button> <RiAddLine className='mr-1 h-4 w-4' />
</div> {bottomAddModelCredentialText ?? t('common.modelProvider.auth.addModelCredential')}
</div>
)
}
{
!isModelCredential && (
<div className='p-2'>
<Button
onClick={() => handleEdit()}
className='w-full'
>
{t('common.modelProvider.auth.addApiKey')}
</Button>
</div>
)
}
</div> </div>
</PortalToFollowElemContent> </PortalToFollowElemContent>
</PortalToFollowElem> </PortalToFollowElem>

View File

@ -0,0 +1,32 @@
import { memo } from 'react'
import { RiEqualizer2Line } from '@remixicon/react'
import { useTranslation } from 'react-i18next'
import Button from '@/app/components/base/button'
import cn from '@/utils/classnames'
type ConfigModelProps = {
className?: string
onClick?: () => void
}
const ConfigModel = ({
className,
onClick,
}: ConfigModelProps) => {
const { t } = useTranslation()
return (
<Button
variant='secondary'
size='small'
className={cn(
'shrink-0',
className,
)}
onClick={onClick}
>
<RiEqualizer2Line className='mr-1 h-4 w-4' />
{t('common.operation.config')}
</Button>
)
}
export default memo(ConfigModel)

View File

@ -67,6 +67,9 @@ const ConfigProvider = ({
configurationMethod={ConfigurationMethodEnum.predefinedModel} configurationMethod={ConfigurationMethodEnum.predefinedModel}
items={[ items={[
{ {
model: {
model: t('common.modelProvider.auth.apiKeys'),
} as any,
credentials: available_credentials ?? [], credentials: available_credentials ?? [],
}, },
]} ]}

View File

@ -3,3 +3,4 @@ export * from './use-credential-status'
export * from './use-custom-models' export * from './use-custom-models'
export * from './use-auth' export * from './use-auth'
export * from './use-auth-service' export * from './use-auth-service'
export * from './use-credential-data'

View File

@ -15,10 +15,10 @@ import type {
CustomModel, CustomModel,
} from '@/app/components/header/account-setting/model-provider-page/declarations' } from '@/app/components/header/account-setting/model-provider-page/declarations'
export const useGetCredential = (provider: string, credentialId?: string, model?: CustomModel, configFrom?: string) => { export const useGetCredential = (provider: string, isModelCredential?: boolean, credentialId?: string, model?: CustomModel, configFrom?: string) => {
const providerData = useGetProviderCredential(!model && !!credentialId, provider, credentialId) const providerData = useGetProviderCredential(!isModelCredential && !!credentialId, provider, credentialId)
const modelData = useGetModelCredential(!!model && !!credentialId, provider, credentialId, model?.model, model?.model_type, configFrom) const modelData = useGetModelCredential(!!isModelCredential && !!credentialId, provider, credentialId, model?.model, model?.model_type, configFrom)
return model ? modelData : providerData return isModelCredential ? modelData : providerData
} }
export const useAuthService = (provider: string) => { export const useAuthService = (provider: string) => {

View File

@ -22,6 +22,7 @@ export const useAuth = (
provider: ModelProvider, provider: ModelProvider,
configurationMethod: ConfigurationMethodEnum, configurationMethod: ConfigurationMethodEnum,
currentCustomConfigurationModelFixedFields?: CustomConfigurationModelFixedFields, currentCustomConfigurationModelFixedFields?: CustomConfigurationModelFixedFields,
isModelCredential?: boolean,
onUpdate?: () => void, onUpdate?: () => void,
) => { ) => {
const { t } = useTranslation() const { t } = useTranslation()
@ -37,9 +38,9 @@ export const useAuth = (
const pendingOperationCredentialId = useRef<string | null>(null) const pendingOperationCredentialId = useRef<string | null>(null)
const pendingOperationModel = useRef<CustomModel | null>(null) const pendingOperationModel = useRef<CustomModel | null>(null)
const [deleteCredentialId, setDeleteCredentialId] = useState<string | null>(null) const [deleteCredentialId, setDeleteCredentialId] = useState<string | null>(null)
const openConfirmDelete = useCallback((credentialId?: string, model?: CustomModel) => { const openConfirmDelete = useCallback((credential?: Credential, model?: CustomModel) => {
if (credentialId) if (credential)
pendingOperationCredentialId.current = credentialId pendingOperationCredentialId.current = credential.credential_id
if (model) if (model)
pendingOperationModel.current = model pendingOperationModel.current = model
@ -55,13 +56,13 @@ export const useAuth = (
doingActionRef.current = doing doingActionRef.current = doing
setDoingAction(doing) setDoingAction(doing)
}, []) }, [])
const handleActiveCredential = useCallback(async (id: string, model?: CustomModel) => { const handleActiveCredential = useCallback(async (credential: Credential, model?: CustomModel) => {
if (doingActionRef.current) if (doingActionRef.current)
return return
try { try {
handleSetDoingAction(true) handleSetDoingAction(true)
await getActiveCredentialService(!!model)({ await getActiveCredentialService(!!model)({
credential_id: id, credential_id: credential.credential_id,
model: model?.model, model: model?.model,
model_type: model?.model_type, model_type: model?.model_type,
}) })
@ -70,6 +71,7 @@ export const useAuth = (
message: t('common.api.actionSuccess'), message: t('common.api.actionSuccess'),
}) })
onUpdate?.() onUpdate?.()
handleRefreshModel(provider, configurationMethod, undefined)
} }
finally { finally {
handleSetDoingAction(false) handleSetDoingAction(false)
@ -84,7 +86,7 @@ export const useAuth = (
} }
try { try {
handleSetDoingAction(true) handleSetDoingAction(true)
await getDeleteCredentialService(!!pendingOperationModel.current)({ await getDeleteCredentialService(!!isModelCredential)({
credential_id: pendingOperationCredentialId.current, credential_id: pendingOperationCredentialId.current,
model: pendingOperationModel.current?.model, model: pendingOperationModel.current?.model,
model_type: pendingOperationModel.current?.model_type, model_type: pendingOperationModel.current?.model_type,
@ -97,11 +99,12 @@ export const useAuth = (
handleRefreshModel(provider, configurationMethod, undefined) handleRefreshModel(provider, configurationMethod, undefined)
setDeleteCredentialId(null) setDeleteCredentialId(null)
pendingOperationCredentialId.current = null pendingOperationCredentialId.current = null
pendingOperationModel.current = null
} }
finally { finally {
handleSetDoingAction(false) handleSetDoingAction(false)
} }
}, [onUpdate, notify, t, handleSetDoingAction, getDeleteCredentialService]) }, [onUpdate, notify, t, handleSetDoingAction, getDeleteCredentialService, isModelCredential])
const handleAddCredential = useCallback((model?: CustomModel) => { const handleAddCredential = useCallback((model?: CustomModel) => {
if (model) if (model)
pendingOperationModel.current = model pendingOperationModel.current = model
@ -114,9 +117,9 @@ export const useAuth = (
let res: { result?: string } = {} let res: { result?: string } = {}
if (payload.credential_id) if (payload.credential_id)
res = await getEditCredentialService(!!payload.model)(payload as any) res = await getEditCredentialService(!!isModelCredential)(payload as any)
else else
res = await getAddCredentialService(!!payload.model)(payload as any) res = await getAddCredentialService(!!isModelCredential)(payload as any)
if (res.result === 'success') { if (res.result === 'success') {
notify({ type: 'success', message: t('common.actionMsg.modifiedSuccessfully') }) notify({ type: 'success', message: t('common.actionMsg.modifiedSuccessfully') })
@ -127,15 +130,16 @@ export const useAuth = (
handleSetDoingAction(false) handleSetDoingAction(false)
} }
}, [onUpdate, notify, t, handleSetDoingAction, getEditCredentialService, getAddCredentialService]) }, [onUpdate, notify, t, handleSetDoingAction, getEditCredentialService, getAddCredentialService])
const handleOpenModal = useCallback((model?: CustomModel, credential?: Credential) => { const handleOpenModal = useCallback((credential?: Credential, model?: CustomModel) => {
handleOpenModelModal( handleOpenModelModal(
provider, provider,
configurationMethod, configurationMethod,
currentCustomConfigurationModelFixedFields, currentCustomConfigurationModelFixedFields,
isModelCredential,
credential, credential,
model, model,
) )
}, [handleOpenModelModal, provider, configurationMethod, currentCustomConfigurationModelFixedFields]) }, [handleOpenModelModal, provider, configurationMethod, currentCustomConfigurationModelFixedFields, isModelCredential])
return { return {
pendingOperationCredentialId, pendingOperationCredentialId,

View File

@ -0,0 +1,24 @@
import { useMemo } from 'react'
import { useGetCredential } from './use-auth-service'
import type {
Credential,
CustomModelCredential,
ModelProvider,
} from '@/app/components/header/account-setting/model-provider-page/declarations'
export const useCredentialData = (provider: ModelProvider, providerFormSchemaPredefined: boolean, isModelCredential?: boolean, credential?: Credential, model?: CustomModelCredential) => {
const configFrom = useMemo(() => {
if (providerFormSchemaPredefined)
return 'predefined-model'
return 'custom-model'
}, [providerFormSchemaPredefined])
const {
isLoading,
data: credentialData = {},
} = useGetCredential(provider.provider, isModelCredential, credential?.credential_id, model, configFrom)
return {
isLoading,
credentialData,
}
}

View File

@ -1,6 +1,8 @@
import { useMemo } from 'react' import { useMemo } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import type { import type {
Credential,
CustomModelCredential,
ModelLoadBalancingConfig, ModelLoadBalancingConfig,
ModelProvider, ModelProvider,
} from '../../declarations' } from '../../declarations'
@ -13,6 +15,9 @@ import { FormTypeEnum } from '@/app/components/base/form/types'
export const useModelFormSchemas = ( export const useModelFormSchemas = (
provider: ModelProvider, provider: ModelProvider,
providerFormSchemaPredefined: boolean, providerFormSchemaPredefined: boolean,
credentials?: Record<string, any>,
credential?: Credential,
model?: CustomModelCredential,
draftConfig?: ModelLoadBalancingConfig, draftConfig?: ModelLoadBalancingConfig,
) => { ) => {
const { t } = useTranslation() const { t } = useTranslation()
@ -22,11 +27,17 @@ export const useModelFormSchemas = (
model_credential_schema, model_credential_schema,
} = provider } = provider
const formSchemas = useMemo(() => { const formSchemas = useMemo(() => {
const modelTypeSchema = genModelTypeFormSchema(supported_model_types)
const modelNameSchema = genModelNameFormSchema(model_credential_schema?.model)
if (!!model) {
modelTypeSchema.disabled = true
modelNameSchema.disabled = true
}
return providerFormSchemaPredefined return providerFormSchemaPredefined
? provider_credential_schema.credential_form_schemas ? provider_credential_schema.credential_form_schemas
: [ : [
genModelTypeFormSchema(supported_model_types), modelTypeSchema,
genModelNameFormSchema(model_credential_schema?.model), modelNameSchema,
...(draftConfig?.enabled ? [] : model_credential_schema.credential_form_schemas), ...(draftConfig?.enabled ? [] : model_credential_schema.credential_form_schemas),
] ]
}, [ }, [
@ -36,21 +47,36 @@ export const useModelFormSchemas = (
model_credential_schema?.credential_form_schemas, model_credential_schema?.credential_form_schemas,
model_credential_schema?.model, model_credential_schema?.model,
draftConfig?.enabled, draftConfig?.enabled,
model,
]) ])
const formSchemasWithAuthorizationName = useMemo(() => { const formSchemasWithAuthorizationName = useMemo(() => {
const authorizationNameSchema = {
type: FormTypeEnum.textInput,
variable: '__authorization_name__',
label: t('plugin.auth.authorizationName'),
required: true,
}
return [ return [
{ authorizationNameSchema,
type: FormTypeEnum.textInput,
variable: '__authorization_name__',
label: t('plugin.auth.authorizationName'),
required: true,
},
...formSchemas, ...formSchemas,
] ]
}, [formSchemas, t]) }, [formSchemas, t])
const formValues = useMemo(() => {
let result = {}
if (credentials)
result = { ...credentials }
if (credential)
result = { ...result, __authorization_name__: credential?.credential_name }
if (model)
result = { ...result, __model_name: model?.model, __model_type: model?.model_type }
return result
}, [credentials, credential, model])
return { return {
formSchemas: formSchemasWithAuthorizationName, formSchemas: formSchemasWithAuthorizationName,
formValues,
} }
} }

View File

@ -3,3 +3,4 @@ export { default as SwitchCredentialInLoadBalancing } from './switch-credential-
export { default as AddCredentialInLoadBalancing } from './add-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 AddCustomModel } from './add-custom-model'
export { default as ConfigProvider } from './config-provider' export { default as ConfigProvider } from './config-provider'
export { default as ConfigModel } from './config-model'

View File

@ -10,13 +10,11 @@ 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 Authorized from './authorized'
import type { import type {
Credential,
ModelLoadBalancingConfig, ModelLoadBalancingConfig,
ModelProvider, ModelProvider,
} from '../declarations' } from '../declarations'
import { ConfigurationMethodEnum } from '@/app/components/header/account-setting/model-provider-page/declarations' import { ConfigurationMethodEnum } from '@/app/components/header/account-setting/model-provider-page/declarations'
import { useCredentialStatus } from './hooks' import { useCredentialStatus } from './hooks'
import { useModelModalHandler } from '../hooks'
import cn from '@/utils/classnames' import cn from '@/utils/classnames'
type SwitchCredentialInLoadBalancingProps = { type SwitchCredentialInLoadBalancingProps = {
@ -27,42 +25,16 @@ type SwitchCredentialInLoadBalancingProps = {
const SwitchCredentialInLoadBalancing = ({ const SwitchCredentialInLoadBalancing = ({
provider, provider,
draftConfig, draftConfig,
setDraftConfig,
}: SwitchCredentialInLoadBalancingProps) => { }: SwitchCredentialInLoadBalancingProps) => {
const { t } = useTranslation() const { t } = useTranslation()
const { const {
available_credentials, available_credentials,
current_credential_name, current_credential_name,
} = useCredentialStatus(provider) } = useCredentialStatus(provider)
const handleOpenModal = useModelModalHandler()
console.log(draftConfig, 'draftConfig')
const handleSetup = useCallback((credential?: Credential) => { const handleItemClick = useCallback(() => {
handleOpenModal(provider, ConfigurationMethodEnum.predefinedModel, undefined, credential) console.log('handleItemClick', draftConfig)
}, [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 renderTrigger = useCallback(() => { const renderTrigger = useCallback(() => {
const selectedCredentialId = draftConfig?.configs.find(config => config.name === '__inherit__')?.credential_id const selectedCredentialId = draftConfig?.configs.find(config => config.name === '__inherit__')?.credential_id
@ -96,12 +68,21 @@ const SwitchCredentialInLoadBalancing = ({
return ( return (
<Authorized <Authorized
provider={provider.provider} provider={provider}
credentials={available_credentials || []} configurationMethod={ConfigurationMethodEnum.customizableModel}
onSetup={handleSetup} items={[
{
model: {
model: t('common.modelProvider.modelCredentials'),
} as any,
credentials: available_credentials || [],
},
]}
renderTrigger={renderTrigger} renderTrigger={renderTrigger}
onItemClick={handleItemClick} onItemClick={handleItemClick}
disableSetDefault isModelCredential
enableAddModelCredential
bottomAddModelCredentialText={t('common.modelProvider.addModelCredential')}
/> />
) )
} }

View File

@ -38,33 +38,35 @@ import type {
import Loading from '@/app/components/base/loading' import Loading from '@/app/components/base/loading'
import { import {
useAuth, useAuth,
useGetCredential, useCredentialData,
} from '@/app/components/header/account-setting/model-provider-page/model-auth/hooks' } 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
onCancel: () => void onCancel: () => void
onSave: () => void onSave: () => void
model?: CustomModel
credential?: Credential
isModelCredential?: boolean
} }
const ModelModal: FC<ModelModalProps> = ({ const ModelModal: FC<ModelModalProps> = ({
provider, provider,
model,
configurateMethod, configurateMethod,
currentCustomConfigurationModelFixedFields, currentCustomConfigurationModelFixedFields,
credential,
onCancel, onCancel,
onSave, onSave,
model,
credential,
isModelCredential,
}) => { }) => {
const providerFormSchemaPredefined = configurateMethod === ConfigurationMethodEnum.predefinedModel const providerFormSchemaPredefined = configurateMethod === ConfigurationMethodEnum.predefinedModel
const { const {
isLoading, isLoading,
data: credentialData = {}, credentialData,
} = useGetCredential(provider.provider, credential?.credential_id, model) } = useCredentialData(provider, providerFormSchemaPredefined, isModelCredential, credential, model)
const { const {
handleSaveCredential, handleSaveCredential,
handleConfirmDelete, handleConfirmDelete,
@ -72,7 +74,7 @@ const ModelModal: FC<ModelModalProps> = ({
closeConfirmDelete, closeConfirmDelete,
openConfirmDelete, openConfirmDelete,
doingAction, doingAction,
} = useAuth(provider, configurateMethod, currentCustomConfigurationModelFixedFields, onSave) } = useAuth(provider, configurateMethod, currentCustomConfigurationModelFixedFields, isModelCredential, onSave)
const { const {
credentials: formSchemasValue, credentials: formSchemasValue,
} = credentialData as any } = credentialData as any
@ -81,7 +83,10 @@ const ModelModal: FC<ModelModalProps> = ({
const isEditMode = !!formSchemasValue && isCurrentWorkspaceManager const isEditMode = !!formSchemasValue && isCurrentWorkspaceManager
const { t } = useTranslation() const { t } = useTranslation()
const language = useLanguage() const language = useLanguage()
const { formSchemas } = useModelFormSchemas(provider, providerFormSchemaPredefined) const {
formSchemas,
formValues,
} = useModelFormSchemas(provider, providerFormSchemaPredefined, formSchemasValue, credential, model)
const formRef = useRef<FormRefObject>(null) const formRef = useRef<FormRefObject>(null)
const handleSave = useCallback(async () => { const handleSave = useCallback(async () => {
@ -152,10 +157,7 @@ const ModelModal: FC<ModelModalProps> = ({
showRadioUI: formSchema.type === FormTypeEnum.radio, showRadioUI: formSchema.type === FormTypeEnum.radio,
} }
}) as FormSchema[]} }) as FormSchema[]}
defaultValues={{ defaultValues={formValues}
...formSchemasValue,
__authorization_name__: credential?.credential_name,
}}
inputClassName='justify-start' inputClassName='justify-start'
ref={formRef} ref={formRef}
/> />
@ -186,7 +188,7 @@ const ModelModal: FC<ModelModalProps> = ({
variant='warning' variant='warning'
size='large' size='large'
className='mr-2' className='mr-2'
onClick={() => openConfirmDelete(credential?.credential_id, model)} onClick={() => openConfirmDelete(credential, model)}
> >
{t('common.operation.remove')} {t('common.operation.remove')}
</Button> </Button>

View File

@ -1,264 +0,0 @@
import type { FC } from 'react'
import {
memo,
useEffect,
useMemo,
useRef,
useState,
} from 'react'
import { useTranslation } from 'react-i18next'
import type {
CustomConfigurationModelFixedFields,
ModelLoadBalancingConfigEntry,
ModelProvider,
} from '../declarations'
import {
ConfigurationMethodEnum,
FormTypeEnum,
} from '../declarations'
import {
useLanguage,
} from '../hooks'
import { ValidatedStatus } from '../../key-validator/declarations'
import { validateLoadBalancingCredentials } from '../utils'
import Button from '@/app/components/base/button'
import { Lock01 } from '@/app/components/base/icons/src/vender/solid/security'
import { LinkExternal02 } from '@/app/components/base/icons/src/vender/line/general'
import {
PortalToFollowElem,
PortalToFollowElemContent,
} from '@/app/components/base/portal-to-follow-elem'
import { useToastContext } from '@/app/components/base/toast'
import Confirm from '@/app/components/base/confirm'
import AuthForm from '@/app/components/base/form/form-scenarios/auth'
import type {
FormRefObject,
FormSchema,
} from '@/app/components/base/form/types'
type ModelModalProps = {
provider: ModelProvider
configurationMethod: ConfigurationMethodEnum
currentCustomConfigurationModelFixedFields?: CustomConfigurationModelFixedFields
entry?: ModelLoadBalancingConfigEntry
onCancel: () => void
onSave: (entry: ModelLoadBalancingConfigEntry) => void
onRemove: () => void
}
const ModelLoadBalancingEntryModal: FC<ModelModalProps> = ({
provider,
configurationMethod,
currentCustomConfigurationModelFixedFields,
entry,
onCancel,
onSave,
onRemove,
}) => {
const providerFormSchemaPredefined = configurationMethod === ConfigurationMethodEnum.predefinedModel
const isEditMode = !!entry
const { t } = useTranslation()
const { notify } = useToastContext()
const language = useLanguage()
const [loading, setLoading] = useState(false)
const [showConfirm, setShowConfirm] = useState(false)
const formSchemas = useMemo(() => {
return [
{
type: FormTypeEnum.textInput,
label: {
en_US: 'Config Name',
zh_Hans: '配置名称',
},
variable: 'name',
required: true,
show_on: [],
placeholder: {
en_US: 'Enter your Config Name here',
zh_Hans: '输入配置名称',
},
} as any,
...(
providerFormSchemaPredefined
? provider.provider_credential_schema.credential_form_schemas
: provider.model_credential_schema.credential_form_schemas
),
]
}, [
providerFormSchemaPredefined,
provider.provider_credential_schema?.credential_form_schemas,
provider.model_credential_schema?.credential_form_schemas,
])
const formRef = useRef<FormRefObject>(null)
const [
defaultFormSchemaValue,
] = useMemo(() => {
const defaultFormSchemaValue: Record<string, string | number> = {}
formSchemas.forEach((formSchema) => {
if (formSchema.default)
defaultFormSchemaValue[formSchema.variable] = formSchema.default
})
return [
defaultFormSchemaValue,
]
}, [formSchemas])
const [initialValue, setInitialValue] = useState<ModelLoadBalancingConfigEntry['credentials']>()
useEffect(() => {
if (entry && !initialValue) {
setInitialValue({
...defaultFormSchemaValue,
...entry.credentials,
id: entry.id,
name: entry.name,
} as Record<string, string | undefined | boolean>)
}
}, [entry, defaultFormSchemaValue, initialValue])
const formSchemasValue = useMemo(() => ({
...currentCustomConfigurationModelFixedFields,
...initialValue,
}), [currentCustomConfigurationModelFixedFields, initialValue])
const handleSave = async () => {
try {
setLoading(true)
const {
isCheckValidated,
values,
} = formRef.current?.getFormValues({
needCheckValidatedValues: true,
needTransformWhenSecretFieldIsPristine: true,
}) || { isCheckValidated: false, values: {} }
if (!isCheckValidated)
return
const res = await validateLoadBalancingCredentials(
providerFormSchemaPredefined,
provider.provider,
values,
entry?.id,
)
if (res.status === ValidatedStatus.Success) {
// notify({ type: 'success', message: t('common.actionMsg.modifiedSuccessfully') })
const { __model_type, __model_name, name, ...credentials } = values
onSave({
...(entry || {}),
name: name as string,
credentials: credentials as Record<string, string | boolean | undefined>,
})
// onCancel()
}
else {
notify({ type: 'error', message: res.message || '' })
}
}
finally {
setLoading(false)
}
}
const handleRemove = () => {
onRemove?.()
}
return (
<PortalToFollowElem open>
<PortalToFollowElemContent className='z-[60] h-full w-full'>
<div className='fixed inset-0 flex items-center justify-center bg-black/[.25]'>
<div className='mx-2 max-h-[calc(100vh-120px)] w-[640px] overflow-y-auto rounded-2xl bg-white shadow-xl'>
<div className='px-8 pt-8'>
<div className='mb-2 flex items-center justify-between'>
<div className='text-xl font-semibold text-gray-900'>{t(isEditMode ? 'common.modelProvider.editConfig' : 'common.modelProvider.addConfig')}</div>
</div>
<AuthForm
formSchemas={formSchemas.map((formSchema) => {
return {
...formSchema,
name: formSchema.variable,
showRadioUI: formSchema.type === FormTypeEnum.radio,
}
}) as FormSchema[]}
defaultValues={formSchemasValue}
inputClassName='justify-start'
ref={formRef}
/>
<div className='sticky bottom-0 flex flex-wrap items-center justify-between gap-y-2 bg-white py-6'>
{
(provider.help && (provider.help.title || provider.help.url))
? (
<a
href={provider.help?.url[language] || provider.help?.url.en_US}
target='_blank' rel='noopener noreferrer'
className='inline-flex items-center text-xs text-primary-600'
onClick={e => !provider.help.url && e.preventDefault()}
>
{provider.help.title?.[language] || provider.help.url[language] || provider.help.title?.en_US || provider.help.url.en_US}
<LinkExternal02 className='ml-1 h-3 w-3' />
</a>
)
: <div />
}
<div>
{
isEditMode && (
<Button
size='large'
className='mr-2 text-[#D92D20]'
onClick={() => setShowConfirm(true)}
>
{t('common.operation.remove')}
</Button>
)
}
<Button
size='large'
className='mr-2'
onClick={onCancel}
>
{t('common.operation.cancel')}
</Button>
<Button
size='large'
variant='primary'
onClick={handleSave}
disabled={loading}
>
{t('common.operation.save')}
</Button>
</div>
</div>
</div>
<div className='border-t-[0.5px] border-t-black/5'>
<div className='flex items-center justify-center bg-gray-50 py-3 text-xs text-gray-500'>
<Lock01 className='mr-1 h-3 w-3 text-gray-500' />
{t('common.modelProvider.encrypted.front')}
<a
className='mx-1 text-primary-600'
target='_blank' rel='noopener noreferrer'
href='https://pycryptodome.readthedocs.io/en/latest/src/cipher/oaep.html'
>
PKCS1_OAEP
</a>
{t('common.modelProvider.encrypted.back')}
</div>
</div>
</div>
{
showConfirm && (
<Confirm
title={t('common.modelProvider.confirmDelete')}
isShow={showConfirm}
onCancel={() => setShowConfirm(false)}
onConfirm={handleRemove}
/>
)
}
</div>
</PortalToFollowElemContent>
</PortalToFollowElem>
)
}
export default memo(ModelLoadBalancingEntryModal)

View File

@ -70,11 +70,11 @@ const CredentialPanel = ({
} }
const credentialLabel = useMemo(() => { const credentialLabel = useMemo(() => {
if (!hasCredential) if (!hasCredential)
return t('common.model.unAuthorized') return t('common.modelProvider.auth.unAuthorized')
if (authorized) if (authorized)
return current_credential_name return current_credential_name
if (authRemoved) if (authRemoved)
return t('common.model.authRemoved') return t('common.modelProvider.auth.authRemoved')
return '' return ''
}, [authorized, authRemoved, current_credential_name, hasCredential]) }, [authorized, authRemoved, current_credential_name, hasCredential])

View File

@ -7,12 +7,10 @@ import {
RiLoader2Line, RiLoader2Line,
} from '@remixicon/react' } from '@remixicon/react'
import type { import type {
CustomConfigurationModelFixedFields,
ModelItem, ModelItem,
ModelProvider, ModelProvider,
} from '../declarations' } from '../declarations'
import { ConfigurationMethodEnum } from '../declarations' import { ConfigurationMethodEnum } from '../declarations'
import type { Credential } from '../declarations'
import { import {
MODEL_PROVIDER_QUOTA_GET_PAID, MODEL_PROVIDER_QUOTA_GET_PAID,
modelTypeFormat, modelTypeFormat,
@ -22,23 +20,21 @@ import ModelBadge from '../model-badge'
import CredentialPanel from './credential-panel' import CredentialPanel from './credential-panel'
import QuotaPanel from './quota-panel' import QuotaPanel from './quota-panel'
import ModelList from './model-list' import ModelList from './model-list'
import AddModelButton from './add-model-button'
import { fetchModelProviderModelList } from '@/service/common' import { fetchModelProviderModelList } from '@/service/common'
import { useEventEmitterContextContext } from '@/context/event-emitter' 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 { AddCustomModel } from '@/app/components/header/account-setting/model-provider-page/model-auth'
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 = {
notConfigured?: boolean notConfigured?: boolean
provider: ModelProvider provider: ModelProvider
onOpenModal: (configurationMethod: ConfigurationMethodEnum, currentCustomConfigurationModelFixedFields?: CustomConfigurationModelFixedFields, credential?: Credential) => void
} }
const ProviderAddedCard: FC<ProviderAddedCardProps> = ({ const ProviderAddedCard: FC<ProviderAddedCardProps> = ({
notConfigured, notConfigured,
provider, provider,
onOpenModal,
}) => { }) => {
const { t } = useTranslation() const { t } = useTranslation()
const { eventEmitter } = useEventEmitterContextContext() const { eventEmitter } = useEventEmitterContextContext()
@ -159,9 +155,9 @@ const ProviderAddedCard: FC<ProviderAddedCardProps> = ({
)} )}
{ {
configurationMethods.includes(ConfigurationMethodEnum.customizableModel) && isCurrentWorkspaceManager && ( configurationMethods.includes(ConfigurationMethodEnum.customizableModel) && isCurrentWorkspaceManager && (
<AddModelButton <AddCustomModel
onClick={() => onOpenModal(ConfigurationMethodEnum.customizableModel)} provider={provider}
className='flex' configurationMethod={ConfigurationMethodEnum.customizableModel}
/> />
) )
} }
@ -174,7 +170,6 @@ const ProviderAddedCard: FC<ProviderAddedCardProps> = ({
provider={provider} provider={provider}
models={modelList} models={modelList}
onCollapse={() => setCollapsed(true)} onCollapse={() => setCollapsed(true)}
onConfig={currentCustomConfigurationModelFixedFields => onOpenModal(ConfigurationMethodEnum.customizableModel, currentCustomConfigurationModelFixedFields)}
onChange={(provider: string) => getModelList(provider)} onChange={(provider: string) => getModelList(provider)}
/> />
) )

View File

@ -1,31 +1,29 @@
import { memo, useCallback } from 'react' import { memo, useCallback } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { useDebounceFn } from 'ahooks' import { useDebounceFn } from 'ahooks'
import type { CustomConfigurationModelFixedFields, ModelItem, ModelProvider } from '../declarations' import type { ModelItem, ModelProvider } from '../declarations'
import { ConfigurationMethodEnum, ModelStatusEnum } from '../declarations' import { ModelStatusEnum } from '../declarations'
import ModelBadge from '../model-badge' import ModelBadge from '../model-badge'
import ModelIcon from '../model-icon' import ModelIcon from '../model-icon'
import ModelName from '../model-name' import ModelName from '../model-name'
import classNames from '@/utils/classnames' import classNames from '@/utils/classnames'
import Button from '@/app/components/base/button'
import { Balance } from '@/app/components/base/icons/src/vender/line/financeAndECommerce' import { Balance } from '@/app/components/base/icons/src/vender/line/financeAndECommerce'
import { Settings01 } from '@/app/components/base/icons/src/vender/line/general'
import Switch from '@/app/components/base/switch' import Switch from '@/app/components/base/switch'
import Tooltip from '@/app/components/base/tooltip' import Tooltip from '@/app/components/base/tooltip'
import { useProviderContext, useProviderContextSelector } from '@/context/provider-context' import { useProviderContext, useProviderContextSelector } from '@/context/provider-context'
import { disableModel, enableModel } from '@/service/common' import { disableModel, enableModel } from '@/service/common'
import { Plan } from '@/app/components/billing/type' import { Plan } from '@/app/components/billing/type'
import { useAppContext } from '@/context/app-context' import { useAppContext } from '@/context/app-context'
import { ConfigModel } from '../model-auth'
export type ModelListItemProps = { export type ModelListItemProps = {
model: ModelItem model: ModelItem
provider: ModelProvider provider: ModelProvider
isConfigurable: boolean isConfigurable: boolean
onConfig: (currentCustomConfigurationModelFixedFields?: CustomConfigurationModelFixedFields) => void
onModifyLoadBalancing?: (model: ModelItem) => void onModifyLoadBalancing?: (model: ModelItem) => void
} }
const ModelListItem = ({ model, provider, isConfigurable, onConfig, onModifyLoadBalancing }: ModelListItemProps) => { const ModelListItem = ({ model, provider, isConfigurable, onModifyLoadBalancing }: ModelListItemProps) => {
const { t } = useTranslation() const { t } = useTranslation()
const { plan } = useProviderContext() const { plan } = useProviderContext()
const modelLoadBalancingEnabled = useProviderContextSelector(state => state.modelLoadBalancingEnabled) const modelLoadBalancingEnabled = useProviderContextSelector(state => state.modelLoadBalancingEnabled)
@ -46,7 +44,7 @@ const ModelListItem = ({ model, provider, isConfigurable, onConfig, onModifyLoad
return ( return (
<div <div
key={model.model} key={`${model.model}-${model.fetch_from}`}
className={classNames( className={classNames(
'group flex h-8 items-center rounded-lg pl-2 pr-2.5', 'group flex h-8 items-center rounded-lg pl-2 pr-2.5',
isConfigurable && 'hover:bg-components-panel-on-panel-item-bg-hover', isConfigurable && 'hover:bg-components-panel-on-panel-item-bg-hover',
@ -74,29 +72,12 @@ const ModelListItem = ({ model, provider, isConfigurable, onConfig, onModifyLoad
</ModelName> </ModelName>
<div className='flex shrink-0 items-center'> <div className='flex shrink-0 items-center'>
{ {
model.fetch_from === ConfigurationMethodEnum.customizableModel (isCurrentWorkspaceManager && (modelLoadBalancingEnabled || plan.type === Plan.sandbox) && !model.deprecated && [ModelStatusEnum.active, ModelStatusEnum.disabled].includes(model.status)) && (
? (isCurrentWorkspaceManager && ( <ConfigModel
<Button className='hidden group-hover:flex'
size='small' onClick={() => onModifyLoadBalancing?.(model)}
className='hidden group-hover:flex' />
onClick={() => onConfig({ __model_name: model.model, __model_type: model.model_type })} )
>
<Settings01 className='mr-1 h-3.5 w-3.5' />
{t('common.modelProvider.config')}
</Button>
))
: (isCurrentWorkspaceManager && (modelLoadBalancingEnabled || plan.type === Plan.sandbox) && !model.deprecated && [ModelStatusEnum.active, ModelStatusEnum.disabled].includes(model.status))
? (
<Button
size='small'
className='opacity-0 transition-opacity group-hover:opacity-100'
onClick={() => onModifyLoadBalancing?.(model)}
>
<Balance className='mr-1 h-3.5 w-3.5' />
{t('common.modelProvider.configLoadBalancing')}
</Button>
)
: null
} }
{ {
model.deprecated model.deprecated

View File

@ -6,7 +6,6 @@ import {
} from '@remixicon/react' } from '@remixicon/react'
import type { import type {
Credential, Credential,
CustomConfigurationModelFixedFields,
ModelItem, ModelItem,
ModelProvider, ModelProvider,
} from '../declarations' } from '../declarations'
@ -17,33 +16,30 @@ import {
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' import { AddCustomModel } from '@/app/components/header/account-setting/model-provider-page/model-auth'
type ModelListProps = { type ModelListProps = {
provider: ModelProvider provider: ModelProvider
models: ModelItem[] models: ModelItem[]
onCollapse: () => void onCollapse: () => void
onConfig: (currentCustomConfigurationModelFixedFields?: CustomConfigurationModelFixedFields) => void
onChange?: (provider: string) => void onChange?: (provider: string) => void
} }
const ModelList: FC<ModelListProps> = ({ const ModelList: FC<ModelListProps> = ({
provider, provider,
models, models,
onCollapse, onCollapse,
onConfig,
onChange, onChange,
}) => { }) => {
const { t } = useTranslation() const { t } = useTranslation()
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, credential?: Credential) => { const onModifyLoadBalancing = useCallback((model: ModelItem, credential?: Credential) => {
setShowModelLoadBalancingModal({ setShowModelLoadBalancingModal({
provider, provider,
credential, credential,
configurateMethod: model.fetch_from,
model: model!, model: model!,
open: !!model, open: !!model,
onClose: () => setShowModelLoadBalancingModal(null), onClose: () => setShowModelLoadBalancingModal(null),
@ -75,7 +71,6 @@ const ModelList: FC<ModelListProps> = ({
provider={provider} provider={provider}
configurationMethod={ConfigurationMethodEnum.customizableModel} configurationMethod={ConfigurationMethodEnum.customizableModel}
currentCustomConfigurationModelFixedFields={undefined} currentCustomConfigurationModelFixedFields={undefined}
models={customModels}
/> />
</div> </div>
) )
@ -84,12 +79,11 @@ const ModelList: FC<ModelListProps> = ({
{ {
models.map(model => ( models.map(model => (
<ModelListItem <ModelListItem
key={model.model} key={`${model.model}-${model.fetch_from}`}
{...{ {...{
model, model,
provider, provider,
isConfigurable, isConfigurable,
onConfig,
onModifyLoadBalancing, onModifyLoadBalancing,
}} }}
/> />

View File

@ -3,23 +3,32 @@ import { useCallback } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { import {
RiDeleteBinLine, RiDeleteBinLine,
RiEqualizer2Line,
} from '@remixicon/react' } from '@remixicon/react'
import type { ConfigurationMethodEnum, CustomConfigurationModelFixedFields, ModelLoadBalancingConfig, ModelLoadBalancingConfigEntry, ModelProvider } from '../declarations' import type {
Credential,
CustomConfigurationModelFixedFields,
CustomModelCredential,
ModelCredential,
ModelLoadBalancingConfig,
ModelLoadBalancingConfigEntry,
ModelProvider,
} from '../declarations'
import { ConfigurationMethodEnum } from '../declarations'
import Indicator from '../../../indicator' import Indicator from '../../../indicator'
import CooldownTimer from './cooldown-timer' import CooldownTimer from './cooldown-timer'
import classNames from '@/utils/classnames' 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 } 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 UpgradeBtn from '@/app/components/billing/upgrade-btn' import UpgradeBtn from '@/app/components/billing/upgrade-btn'
import s from '@/app/components/custom/style.module.css' 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' import { AddCredentialInLoadBalancing } from '@/app/components/header/account-setting/model-provider-page/model-auth'
import { useModelModalHandler } from '@/app/components/header/account-setting/model-provider-page/hooks'
export type ModelLoadBalancingConfigsProps = { export type ModelLoadBalancingConfigsProps = {
draftConfig?: ModelLoadBalancingConfig draftConfig?: ModelLoadBalancingConfig
@ -29,19 +38,26 @@ export type ModelLoadBalancingConfigsProps = {
currentCustomConfigurationModelFixedFields?: CustomConfigurationModelFixedFields currentCustomConfigurationModelFixedFields?: CustomConfigurationModelFixedFields
withSwitch?: boolean withSwitch?: boolean
className?: string className?: string
modelCredential: ModelCredential
onUpdate?: () => void
model: CustomModelCredential
} }
const ModelLoadBalancingConfigs = ({ const ModelLoadBalancingConfigs = ({
draftConfig, draftConfig,
setDraftConfig, setDraftConfig,
provider, provider,
model,
configurationMethod, configurationMethod,
currentCustomConfigurationModelFixedFields, currentCustomConfigurationModelFixedFields,
withSwitch = false, withSwitch = false,
className, className,
modelCredential,
onUpdate,
}: ModelLoadBalancingConfigsProps) => { }: ModelLoadBalancingConfigsProps) => {
const { t } = useTranslation() const { t } = useTranslation()
const modelLoadBalancingEnabled = useProviderContextSelector(state => state.modelLoadBalancingEnabled) const modelLoadBalancingEnabled = useProviderContextSelector(state => state.modelLoadBalancingEnabled)
const handleOpenModal = useModelModalHandler()
const updateConfigEntry = useCallback( const updateConfigEntry = useCallback(
( (
@ -66,6 +82,21 @@ const ModelLoadBalancingConfigs = ({
[setDraftConfig], [setDraftConfig],
) )
const addConfigEntry = useCallback((credential: Credential) => {
setDraftConfig((prev: any) => {
if (!prev)
return prev
return {
...prev,
configs: [...prev.configs, {
credential_id: credential.credential_id,
enabled: true,
name: credential.credential_name,
}],
}
})
}, [setDraftConfig])
const toggleModalBalancing = useCallback((enabled: boolean) => { const toggleModalBalancing = useCallback((enabled: boolean) => {
if ((modelLoadBalancingEnabled || !enabled) && draftConfig) { if ((modelLoadBalancingEnabled || !enabled) && draftConfig) {
setDraftConfig({ setDraftConfig({
@ -82,54 +113,6 @@ const ModelLoadBalancingConfigs = ({
})) }))
}, [updateConfigEntry]) }, [updateConfigEntry])
const setShowModelLoadBalancingEntryModal = useModalContextSelector(state => state.setShowModelLoadBalancingEntryModal)
const toggleEntryModal = useCallback((index?: number, entry?: ModelLoadBalancingConfigEntry) => {
setShowModelLoadBalancingEntryModal({
payload: {
currentProvider: provider,
currentConfigurationMethod: configurationMethod,
currentCustomConfigurationModelFixedFields,
entry,
index,
},
onSaveCallback: ({ entry: result }) => {
if (entry) {
// edit
setDraftConfig(prev => ({
...prev,
enabled: !!prev?.enabled,
configs: prev?.configs.map((config, i) => i === index ? result! : config) || [],
}))
}
else {
// add
setDraftConfig(prev => ({
...prev,
enabled: !!prev?.enabled,
configs: (prev?.configs || []).concat([{ ...result!, enabled: true }]),
}))
}
},
onRemoveCallback: ({ index }) => {
if (index !== undefined && (draftConfig?.configs?.length ?? 0) > index) {
setDraftConfig(prev => ({
...prev,
enabled: !!prev?.enabled,
configs: prev?.configs.filter((_, i) => i !== index) || [],
}))
}
},
})
}, [
configurationMethod,
currentCustomConfigurationModelFixedFields,
draftConfig?.configs?.length,
provider,
setDraftConfig,
setShowModelLoadBalancingEntryModal,
])
const clearCountdown = useCallback((index: number) => { const clearCountdown = useCallback((index: number) => {
updateConfigEntry(index, ({ ttl: _, ...entry }) => { updateConfigEntry(index, ({ ttl: _, ...entry }) => {
return { return {
@ -211,9 +194,21 @@ const ModelLoadBalancingConfigs = ({
<div className='flex items-center gap-1 opacity-0 transition-opacity group-hover:opacity-100'> <div className='flex items-center gap-1 opacity-0 transition-opacity group-hover:opacity-100'>
<span <span
className='flex h-8 w-8 cursor-pointer items-center justify-center rounded-lg bg-components-button-secondary-bg text-text-tertiary transition-colors hover:bg-components-button-secondary-bg-hover' className='flex h-8 w-8 cursor-pointer items-center justify-center rounded-lg bg-components-button-secondary-bg text-text-tertiary transition-colors hover:bg-components-button-secondary-bg-hover'
onClick={() => toggleEntryModal(index, config)} onClick={() => {
handleOpenModal(
provider,
configurationMethod,
currentCustomConfigurationModelFixedFields,
configurationMethod === ConfigurationMethodEnum.customizableModel,
(config.credential_id && config.name) ? {
credential_id: config.credential_id,
credential_name: config.name,
} : undefined,
model,
)
}}
> >
<Edit02 className='h-4 w-4' /> <RiEqualizer2Line className='h-4 w-4' />
</span> </span>
<span <span
className='flex h-8 w-8 cursor-pointer items-center justify-center rounded-lg bg-components-button-secondary-bg text-text-tertiary transition-colors hover:bg-components-button-secondary-bg-hover' className='flex h-8 w-8 cursor-pointer items-center justify-center rounded-lg bg-components-button-secondary-bg text-text-tertiary transition-colors hover:bg-components-button-secondary-bg-hover'
@ -237,13 +232,17 @@ const ModelLoadBalancingConfigs = ({
})} })}
<AddCredentialInLoadBalancing <AddCredentialInLoadBalancing
provider={provider} provider={provider}
onSetup={() => toggleEntryModal()} model={model}
configurationMethod={configurationMethod}
modelCredential={modelCredential}
onSelectCredential={addConfigEntry}
onUpdate={onUpdate}
/> />
</div> </div>
)} )}
{ {
draftConfig.enabled && draftConfig.configs.length < 2 && ( draftConfig.enabled && draftConfig.configs.length < 2 && (
<div className='flex h-[34px] items-center border-t border-t-divider-subtle bg-components-panel-bg px-6 text-xs text-text-secondary'> <div className='flex h-[34px] items-center rounded-b-xl border-t border-t-divider-subtle bg-components-panel-bg px-6 text-xs text-text-secondary'>
<AlertTriangle className='mr-1 h-3 w-3 text-[#f79009]' /> <AlertTriangle className='mr-1 h-3 w-3 text-[#f79009]' />
{t('common.modelProvider.loadBalancingLeastKeyWarning')} {t('common.modelProvider.loadBalancingLeastKeyWarning')}
</div> </div>

View File

@ -1,6 +1,5 @@
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 type { import type {
Credential, Credential,
ModelItem, ModelItem,
@ -8,21 +7,27 @@ import type {
ModelLoadBalancingConfigEntry, ModelLoadBalancingConfigEntry,
ModelProvider, ModelProvider,
} from '../declarations' } from '../declarations'
import { FormTypeEnum } from '../declarations' import {
ConfigurationMethodEnum,
FormTypeEnum,
} from '../declarations'
import ModelIcon from '../model-icon' import ModelIcon from '../model-icon'
import ModelName from '../model-name' import ModelName from '../model-name'
import { savePredefinedLoadBalancingConfig } from '../utils'
import ModelLoadBalancingConfigs from './model-load-balancing-configs' import ModelLoadBalancingConfigs from './model-load-balancing-configs'
import classNames from '@/utils/classnames' import classNames from '@/utils/classnames'
import Modal from '@/app/components/base/modal' import Modal from '@/app/components/base/modal'
import Button from '@/app/components/base/button' import Button from '@/app/components/base/button'
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' // import { SwitchCredentialInLoadBalancing } from '@/app/components/header/account-setting/model-provider-page/model-auth'
import {
useGetModelCredential,
useUpdateModelLoadBalancingConfig,
} from '@/service/use-models'
export type ModelLoadBalancingModalProps = { export type ModelLoadBalancingModalProps = {
provider: ModelProvider provider: ModelProvider
configurateMethod: ConfigurationMethodEnum
model: ModelItem model: ModelItem
credential?: Credential credential?: Credential
open?: boolean open?: boolean
@ -33,6 +38,7 @@ export type ModelLoadBalancingModalProps = {
// model balancing config modal // model balancing config modal
const ModelLoadBalancingModal = ({ const ModelLoadBalancingModal = ({
provider, provider,
configurateMethod,
model, model,
credential, credential,
open = false, open = false,
@ -43,13 +49,18 @@ const ModelLoadBalancingModal = ({
const { notify } = useToastContext() const { notify } = useToastContext()
const [loading, setLoading] = useState(false) const [loading, setLoading] = useState(false)
const providerFormSchemaPredefined = configurateMethod === ConfigurationMethodEnum.predefinedModel
const { data, mutate } = useSWR( const configFrom = providerFormSchemaPredefined ? 'predefined-model' : 'custom-model'
`/workspaces/current/model-providers/${provider.provider}/models/credentials?model=${model.model}&model_type=${model.model_type}`, const {
fetchModelLoadBalancingConfig, isLoading,
) data,
refetch,
const originalConfig = data?.load_balancing } = useGetModelCredential(true, provider.provider, credential?.credential_id, model.model, model.model_type, configFrom)
const modelCredential = data
const {
load_balancing,
} = modelCredential ?? {}
const originalConfig = load_balancing
const [draftConfig, setDraftConfig] = useState<ModelLoadBalancingConfig>() const [draftConfig, setDraftConfig] = useState<ModelLoadBalancingConfig>()
const originalConfigMap = useMemo(() => { const originalConfigMap = useMemo(() => {
if (!originalConfig) if (!originalConfig)
@ -90,25 +101,24 @@ const ModelLoadBalancingModal = ({
return result return result
}, [extendedSecretFormSchemas, originalConfigMap]) }, [extendedSecretFormSchemas, originalConfigMap])
const { mutateAsync: updateModelLoadBalancingConfig } = useUpdateModelLoadBalancingConfig(provider.provider)
const handleSave = async () => { const handleSave = async () => {
try { try {
setLoading(true) setLoading(true)
const res = await savePredefinedLoadBalancingConfig( const res = await updateModelLoadBalancingConfig(
provider.provider,
({
...(data?.credentials ?? {}),
__model_type: model.model_type,
__model_name: model.model,
}),
{ {
...draftConfig, config_from: configFrom,
enabled: Boolean(draftConfig?.enabled), model: model.model,
configs: draftConfig!.configs.map(encodeConfigEntrySecretValues), model_type: model.model_type,
load_balancing: {
...draftConfig,
configs: draftConfig!.configs.map(encodeConfigEntrySecretValues),
enabled: Boolean(draftConfig?.enabled),
},
}, },
) )
if (res.result === 'success') { if (res.result === 'success') {
notify({ type: 'success', message: t('common.actionMsg.modifiedSuccessfully') }) notify({ type: 'success', message: t('common.actionMsg.modifiedSuccessfully') })
mutate()
onSave?.(provider.provider) onSave?.(provider.provider)
onClose?.() onClose?.()
} }
@ -125,7 +135,11 @@ const ModelLoadBalancingModal = ({
className='w-[640px] max-w-none px-8 pt-8' className='w-[640px] max-w-none px-8 pt-8'
title={ title={
<div className='pb-3 font-semibold'> <div className='pb-3 font-semibold'>
<div className='h-[30px]'>{t('common.modelProvider.configLoadBalancing')}</div> <div className='h-[30px]'>{
draftConfig?.enabled
? t('common.modelProvider.auth.configLoadBalancing')
: t('common.modelProvider.auth.configModel')
}</div>
{Boolean(model) && ( {Boolean(model) && (
<div className='flex h-5 items-center'> <div className='flex h-5 items-center'>
<ModelIcon <ModelIcon
@ -167,25 +181,34 @@ const ModelLoadBalancingModal = ({
<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 {/* <SwitchCredentialInLoadBalancing
draftConfig={draftConfig} draftConfig={draftConfig}
setDraftConfig={setDraftConfig} setDraftConfig={setDraftConfig}
provider={provider} provider={provider}
/> /> */}
</div> </div>
</div> </div>
{
<ModelLoadBalancingConfigs {...{ modelCredential && (
draftConfig, <ModelLoadBalancingConfigs {...{
setDraftConfig, draftConfig,
provider, setDraftConfig,
currentCustomConfigurationModelFixedFields: { provider,
__model_name: model.model, currentCustomConfigurationModelFixedFields: {
__model_type: model.model_type, __model_name: model.model,
}, __model_type: model.model_type,
configurationMethod: model.fetch_from, },
className: 'mt-2', configurationMethod: model.fetch_from,
}} /> className: 'mt-2',
modelCredential,
onUpdate: refetch,
model: {
model: model.model,
model_type: model.model_type,
},
}} />
)
}
</div> </div>
<div className='mt-6 flex items-center justify-end gap-2'> <div className='mt-6 flex items-center justify-end gap-2'>
@ -196,6 +219,7 @@ const ModelLoadBalancingModal = ({
disabled={ disabled={
loading loading
|| (draftConfig?.enabled && (draftConfig?.configs.filter(config => config.enabled).length ?? 0) < 2) || (draftConfig?.enabled && (draftConfig?.configs.filter(config => config.enabled).length ?? 0) < 2)
|| isLoading
} }
>{t('common.operation.save')}</Button> >{t('common.operation.save')}</Button>
</div> </div>

View File

@ -56,9 +56,6 @@ const ExternalAPIModal = dynamic(() => import('@/app/components/datasets/externa
const ModelLoadBalancingModal = dynamic(() => import('@/app/components/header/account-setting/model-provider-page/provider-added-card/model-load-balancing-modal'), { const ModelLoadBalancingModal = dynamic(() => import('@/app/components/header/account-setting/model-provider-page/provider-added-card/model-load-balancing-modal'), {
ssr: false, ssr: false,
}) })
const ModelLoadBalancingEntryModal = dynamic(() => import('@/app/components/header/account-setting/model-provider-page/model-modal/model-load-balancing-entry-modal'), {
ssr: false,
})
const OpeningSettingModal = dynamic(() => import('@/app/components/base/features/new-feature-panel/conversation-opener/modal'), { const OpeningSettingModal = dynamic(() => import('@/app/components/base/features/new-feature-panel/conversation-opener/modal'), {
ssr: false, ssr: false,
}) })
@ -81,6 +78,7 @@ export type ModelModalType = {
currentProvider: ModelProvider currentProvider: ModelProvider
currentConfigurationMethod: ConfigurationMethodEnum currentConfigurationMethod: ConfigurationMethodEnum
currentCustomConfigurationModelFixedFields?: CustomConfigurationModelFixedFields currentCustomConfigurationModelFixedFields?: CustomConfigurationModelFixedFields
isModelCredential?: boolean
credential?: Credential credential?: Credential
model?: CustomModel model?: CustomModel
} }
@ -99,7 +97,6 @@ export type ModalContextState = {
setShowModelModal: Dispatch<SetStateAction<ModalState<ModelModalType> | null>> setShowModelModal: Dispatch<SetStateAction<ModalState<ModelModalType> | null>>
setShowExternalKnowledgeAPIModal: Dispatch<SetStateAction<ModalState<CreateExternalAPIReq> | null>> setShowExternalKnowledgeAPIModal: Dispatch<SetStateAction<ModalState<CreateExternalAPIReq> | null>>
setShowModelLoadBalancingModal: Dispatch<SetStateAction<ModelLoadBalancingModalProps | null>> setShowModelLoadBalancingModal: Dispatch<SetStateAction<ModelLoadBalancingModalProps | null>>
setShowModelLoadBalancingEntryModal: Dispatch<SetStateAction<ModalState<LoadBalancingEntryModalType> | null>>
setShowOpeningModal: Dispatch<SetStateAction<ModalState<OpeningStatement & { setShowOpeningModal: Dispatch<SetStateAction<ModalState<OpeningStatement & {
promptVariables?: PromptVariable[] promptVariables?: PromptVariable[]
workflowVariables?: InputVar[] workflowVariables?: InputVar[]
@ -117,7 +114,6 @@ const ModalContext = createContext<ModalContextState>({
setShowModelModal: noop, setShowModelModal: noop,
setShowExternalKnowledgeAPIModal: noop, setShowExternalKnowledgeAPIModal: noop,
setShowModelLoadBalancingModal: noop, setShowModelLoadBalancingModal: noop,
setShowModelLoadBalancingEntryModal: noop,
setShowOpeningModal: noop, setShowOpeningModal: noop,
setShowUpdatePluginModal: noop, setShowUpdatePluginModal: noop,
}) })
@ -142,7 +138,6 @@ export const ModalContextProvider = ({
const [showModelModal, setShowModelModal] = useState<ModalState<ModelModalType> | null>(null) const [showModelModal, setShowModelModal] = useState<ModalState<ModelModalType> | null>(null)
const [showExternalKnowledgeAPIModal, setShowExternalKnowledgeAPIModal] = useState<ModalState<CreateExternalAPIReq> | null>(null) const [showExternalKnowledgeAPIModal, setShowExternalKnowledgeAPIModal] = useState<ModalState<CreateExternalAPIReq> | null>(null)
const [showModelLoadBalancingModal, setShowModelLoadBalancingModal] = useState<ModelLoadBalancingModalProps | null>(null) const [showModelLoadBalancingModal, setShowModelLoadBalancingModal] = useState<ModelLoadBalancingModalProps | null>(null)
const [showModelLoadBalancingEntryModal, setShowModelLoadBalancingEntryModal] = useState<ModalState<LoadBalancingEntryModalType> | null>(null)
const [showOpeningModal, setShowOpeningModal] = useState<ModalState<OpeningStatement & { const [showOpeningModal, setShowOpeningModal] = useState<ModalState<OpeningStatement & {
promptVariables?: PromptVariable[] promptVariables?: PromptVariable[]
workflowVariables?: InputVar[] workflowVariables?: InputVar[]
@ -208,30 +203,12 @@ export const ModalContextProvider = ({
setShowExternalKnowledgeAPIModal(null) setShowExternalKnowledgeAPIModal(null)
}, [showExternalKnowledgeAPIModal]) }, [showExternalKnowledgeAPIModal])
const handleCancelModelLoadBalancingEntryModal = useCallback(() => {
showModelLoadBalancingEntryModal?.onCancelCallback?.()
setShowModelLoadBalancingEntryModal(null)
}, [showModelLoadBalancingEntryModal])
const handleCancelOpeningModal = useCallback(() => { const handleCancelOpeningModal = useCallback(() => {
setShowOpeningModal(null) setShowOpeningModal(null)
if (showOpeningModal?.onCancelCallback) if (showOpeningModal?.onCancelCallback)
showOpeningModal.onCancelCallback() showOpeningModal.onCancelCallback()
}, [showOpeningModal]) }, [showOpeningModal])
const handleSaveModelLoadBalancingEntryModal = useCallback((entry: ModelLoadBalancingConfigEntry) => {
showModelLoadBalancingEntryModal?.onSaveCallback?.({
...showModelLoadBalancingEntryModal.payload,
entry,
})
setShowModelLoadBalancingEntryModal(null)
}, [showModelLoadBalancingEntryModal])
const handleRemoveModelLoadBalancingEntry = useCallback(() => {
showModelLoadBalancingEntryModal?.onRemoveCallback?.(showModelLoadBalancingEntryModal.payload)
setShowModelLoadBalancingEntryModal(null)
}, [showModelLoadBalancingEntryModal])
const handleSaveApiBasedExtension = (newApiBasedExtension: ApiBasedExtension) => { const handleSaveApiBasedExtension = (newApiBasedExtension: ApiBasedExtension) => {
if (showApiBasedExtensionModal?.onSaveCallback) if (showApiBasedExtensionModal?.onSaveCallback)
showApiBasedExtensionModal.onSaveCallback(newApiBasedExtension) showApiBasedExtensionModal.onSaveCallback(newApiBasedExtension)
@ -273,7 +250,6 @@ export const ModalContextProvider = ({
setShowModelModal, setShowModelModal,
setShowExternalKnowledgeAPIModal, setShowExternalKnowledgeAPIModal,
setShowModelLoadBalancingModal, setShowModelLoadBalancingModal,
setShowModelLoadBalancingEntryModal,
setShowOpeningModal, setShowOpeningModal,
setShowUpdatePluginModal, setShowUpdatePluginModal,
}}> }}>
@ -340,9 +316,10 @@ 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}
isModelCredential={showModelModal.payload.isModelCredential}
credential={showModelModal.payload.credential} credential={showModelModal.payload.credential}
model={showModelModal.payload.model}
onCancel={handleCancelModelModal} onCancel={handleCancelModelModal}
onSave={handleSaveModelModal} onSave={handleSaveModelModal}
/> />
@ -365,19 +342,6 @@ export const ModalContextProvider = ({
<ModelLoadBalancingModal {...showModelLoadBalancingModal!} /> <ModelLoadBalancingModal {...showModelLoadBalancingModal!} />
) )
} }
{
!!showModelLoadBalancingEntryModal && (
<ModelLoadBalancingEntryModal
provider={showModelLoadBalancingEntryModal.payload.currentProvider}
configurationMethod={showModelLoadBalancingEntryModal.payload.currentConfigurationMethod}
currentCustomConfigurationModelFixedFields={showModelLoadBalancingEntryModal.payload.currentCustomConfigurationModelFixedFields}
entry={showModelLoadBalancingEntryModal.payload.entry}
onCancel={handleCancelModelLoadBalancingEntryModal}
onSave={handleSaveModelLoadBalancingEntryModal}
onRemove={handleRemoveModelLoadBalancingEntry}
/>
)
}
{showOpeningModal && ( {showOpeningModal && (
<OpeningSettingModal <OpeningSettingModal
data={showOpeningModal.payload} data={showOpeningModal.payload}

View File

@ -40,6 +40,7 @@ const translation = {
deleteApp: 'Delete App', deleteApp: 'Delete App',
settings: 'Settings', settings: 'Settings',
setup: 'Setup', setup: 'Setup',
config: 'Config',
getForFree: 'Get for free', getForFree: 'Get for free',
reload: 'Reload', reload: 'Reload',
ok: 'OK', ok: 'OK',
@ -66,7 +67,6 @@ const translation = {
more: 'More', more: 'More',
selectAll: 'Select All', selectAll: 'Select All',
deSelectAll: 'Deselect All', deSelectAll: 'Deselect All',
config: 'Config',
}, },
errorMsg: { errorMsg: {
fieldRequired: '{{field}} is required', fieldRequired: '{{field}} is required',
@ -145,8 +145,6 @@ const translation = {
addMoreModel: 'Go to settings to add more models', addMoreModel: 'Go to settings to add more models',
settingsLink: 'Model Provider Settings', settingsLink: 'Model Provider Settings',
capabilities: 'MultiModal Capabilities', capabilities: 'MultiModal Capabilities',
unAuthorized: 'Unauthorized',
authRemoved: 'Auth removed',
}, },
menus: { menus: {
status: 'beta', status: 'beta',
@ -489,6 +487,18 @@ const translation = {
discoverMore: 'Discover more in ', discoverMore: 'Discover more in ',
emptyProviderTitle: 'Model provider not set up', emptyProviderTitle: 'Model provider not set up',
emptyProviderTip: 'Please install a model provider first.', emptyProviderTip: 'Please install a model provider first.',
auth: {
unAuthorized: 'Unauthorized',
authRemoved: 'Auth removed',
apiKeys: 'API Keys',
addApiKey: 'Add API Key',
addNewModel: 'Add new model',
addCredential: 'Add credential',
addModelCredential: 'Add model credential',
modelCredentials: 'Model credentials',
configModel: 'Config model',
configLoadBalancing: 'Config Load Balancing',
},
}, },
dataSource: { dataSource: {
add: 'Add a data source', add: 'Add a data source',

View File

@ -40,6 +40,7 @@ const translation = {
deleteApp: '删除应用', deleteApp: '删除应用',
settings: '设置', settings: '设置',
setup: '设置', setup: '设置',
config: '配置',
getForFree: '免费获取', getForFree: '免费获取',
reload: '刷新', reload: '刷新',
ok: '好的', ok: '好的',
@ -66,7 +67,6 @@ const translation = {
more: '更多', more: '更多',
selectAll: '全选', selectAll: '全选',
deSelectAll: '取消全选', deSelectAll: '取消全选',
config: '配置',
}, },
errorMsg: { errorMsg: {
fieldRequired: '{{field}} 为必填项', fieldRequired: '{{field}} 为必填项',
@ -145,8 +145,6 @@ const translation = {
addMoreModel: '添加更多模型', addMoreModel: '添加更多模型',
settingsLink: '模型设置', settingsLink: '模型设置',
capabilities: '多模态能力', capabilities: '多模态能力',
unAuthorized: '未授权',
authRemoved: '授权已移除',
}, },
menus: { menus: {
status: 'beta', status: 'beta',
@ -489,6 +487,18 @@ const translation = {
discoverMore: '发现更多就在', discoverMore: '发现更多就在',
emptyProviderTitle: '尚未安装模型供应商', emptyProviderTitle: '尚未安装模型供应商',
emptyProviderTip: '请安装模型供应商。', emptyProviderTip: '请安装模型供应商。',
auth: {
unAuthorized: '未授权',
authRemoved: '授权已移除',
apiKeys: 'API 密钥',
addApiKey: '添加 API 密钥',
addNewModel: '添加新模型',
addCredential: '添加凭据',
addModelCredential: '添加模型凭据',
modelCredentials: '模型凭据',
configModel: '配置模型',
configLoadBalancing: '配置负载均衡',
},
}, },
dataSource: { dataSource: {
add: '添加数据源', add: '添加数据源',

View File

@ -7,6 +7,7 @@ import {
import type { import type {
ModelCredential, ModelCredential,
ModelItem, ModelItem,
ModelLoadBalancingConfig,
ModelTypeEnum, ModelTypeEnum,
ProviderCredential, ProviderCredential,
} from '@/app/components/header/account-setting/model-provider-page/declarations' } from '@/app/components/header/account-setting/model-provider-page/declarations'
@ -29,7 +30,7 @@ export const useGetProviderCredential = (enabled: boolean, provider: string, cre
return useQuery({ return useQuery({
enabled, enabled,
queryKey: [NAME_SPACE, 'model-list', provider, credentialId], queryKey: [NAME_SPACE, 'model-list', provider, credentialId],
queryFn: () => get<{ data: ProviderCredential }>(`/workspaces/current/model-providers/${provider}/credentials${credentialId ? `?credential_id=${credentialId}` : ''}`), queryFn: () => get<ProviderCredential>(`/workspaces/current/model-providers/${provider}/credentials${credentialId ? `?credential_id=${credentialId}` : ''}`),
}) })
} }
@ -82,7 +83,8 @@ export const useGetModelCredential = (
return useQuery({ return useQuery({
enabled, enabled,
queryKey: [NAME_SPACE, 'model-list', provider, model, modelType, credentialId], 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}`), queryFn: () => get<ModelCredential>(`/workspaces/current/model-providers/${provider}/models/credentials?model=${model}&model_type=${modelType}&config_from=${configFrom}${credentialId ? `&credential_id=${credentialId}` : ''}`),
staleTime: 0,
}) })
} }
@ -136,3 +138,16 @@ export const useActiveModelCredential = (provider: string) => {
}), }),
}) })
} }
export const useUpdateModelLoadBalancingConfig = (provider: string) => {
return useMutation({
mutationFn: (data: {
config_from: string
model: string
model_type: ModelTypeEnum
load_balancing: ModelLoadBalancingConfig
}) => post<{ result: string }>(`/workspaces/current/model-providers/${provider}/models`, {
body: data,
}),
})
}