feat: model load balancing (#4926)

This commit is contained in:
Nite Knite 2024-06-05 00:13:29 +08:00 • committed by GitHub
commit 37f292ea91
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
58 changed files with 1896 additions and 304 deletions

View file

@ -0,0 +1,64 @@
import { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { useLatest } from 'ahooks'
import SimplePieChart from '@/app/components/base/simple-pie-chart'
import TooltipPlus from '@/app/components/base/tooltip-plus'
export type CooldownTimerProps = {
secondsRemaining?: number
onFinish?: () => void
}
const CooldownTimer = ({ secondsRemaining, onFinish }: CooldownTimerProps) => {
const { t } = useTranslation()
const targetTime = useRef<number>(Date.now())
const [currentTime, setCurrentTime] = useState(targetTime.current)
const displayTime = useMemo(
() => Math.ceil((targetTime.current - currentTime) / 1000),
[currentTime],
)
const countdownTimeout = useRef<NodeJS.Timeout>()
const clearCountdown = useCallback(() => {
if (countdownTimeout.current) {
clearTimeout(countdownTimeout.current)
countdownTimeout.current = undefined
}
}, [])
const onFinishRef = useLatest(onFinish)
const countdown = useCallback(() => {
clearCountdown()
countdownTimeout.current = setTimeout(() => {
const now = Date.now()
if (now <= targetTime.current) {
setCurrentTime(Date.now())
countdown()
}
else {
onFinishRef.current?.()
clearCountdown()
}
}, 1000)
}, [clearCountdown, onFinishRef])
useEffect(() => {
const now = Date.now()
targetTime.current = now + (secondsRemaining ?? 0) * 1000
setCurrentTime(now)
countdown()
return clearCountdown
}, [clearCountdown, countdown, secondsRemaining])
return displayTime
? (
<TooltipPlus popupContent={t('common.modelProvider.apiKeyRateLimit', { seconds: displayTime })}>
<SimplePieChart percentage={Math.round(displayTime / 60 * 100)} className='w-3 h-3' />
</TooltipPlus>
)
: null
}
export default memo(CooldownTimer)

View file

@ -2,7 +2,7 @@ import type { FC } from 'react'
import { useTranslation } from 'react-i18next'
import type { ModelProvider } from '../declarations'
import {
ConfigurateMethodEnum,
ConfigurationMethodEnum,
CustomConfigurationStatusEnum,
PreferredProviderTypeEnum,
} from '../declarations'
@ -51,7 +51,7 @@ const CredentialPanel: FC<CredentialPanelProps> = ({
updateModelProviders()
configurateMethods.forEach((method) => {
if (method === ConfigurateMethodEnum.predefinedModel)
if (method === ConfigurationMethodEnum.predefinedModel)
provider.supported_model_types.forEach(modelType => updateModelList(modelType))
})

View file

@ -2,11 +2,11 @@ import type { FC } from 'react'
import { useState } from 'react'
import { useTranslation } from 'react-i18next'
import type {
CustomConfigrationModelFixedFields,
CustomConfigurationModelFixedFields,
ModelItem,
ModelProvider,
} from '../declarations'
import { ConfigurateMethodEnum } from '../declarations'
import { ConfigurationMethodEnum } from '../declarations'
import {
DEFAULT_BACKGROUND_COLOR,
MODEL_PROVIDER_QUOTA_GET_PAID,
@ -27,7 +27,7 @@ import { IS_CE_EDITION } from '@/config'
export const UPDATE_MODEL_PROVIDER_CUSTOM_MODEL_LIST = 'UPDATE_MODEL_PROVIDER_CUSTOM_MODEL_LIST'
type ProviderAddedCardProps = {
provider: ModelProvider
onOpenModal: (configurateMethod: ConfigurateMethodEnum, currentCustomConfigrationModelFixedFields?: CustomConfigrationModelFixedFields) => void
onOpenModal: (configurationMethod: ConfigurationMethodEnum, currentCustomConfigurationModelFixedFields?: CustomConfigurationModelFixedFields) => void
}
const ProviderAddedCard: FC<ProviderAddedCardProps> = ({
provider,
@ -39,7 +39,7 @@ const ProviderAddedCard: FC<ProviderAddedCardProps> = ({
const [loading, setLoading] = useState(false)
const [collapsed, setCollapsed] = useState(true)
const [modelList, setModelList] = useState<ModelItem[]>([])
const configurateMethods = provider.configurate_methods.filter(method => method !== ConfigurateMethodEnum.fetchFromRemote)
const configurationMethods = provider.configurate_methods.filter(method => method !== ConfigurationMethodEnum.fetchFromRemote)
const systemConfig = provider.system_configuration
const hasModelList = fetched && !!modelList.length
const showQuota = systemConfig.enabled && [...MODEL_PROVIDER_QUOTA_GET_PAID].includes(provider.provider) && !IS_CE_EDITION
@ -101,9 +101,9 @@ const ProviderAddedCard: FC<ProviderAddedCardProps> = ({
)
}
{
configurateMethods.includes(ConfigurateMethodEnum.predefinedModel) && (
configurationMethods.includes(ConfigurationMethodEnum.predefinedModel) && (
<CredentialPanel
onSetup={() => onOpenModal(ConfigurateMethodEnum.predefinedModel)}
onSetup={() => onOpenModal(ConfigurationMethodEnum.predefinedModel)}
provider={provider}
/>
)
@ -136,9 +136,9 @@ const ProviderAddedCard: FC<ProviderAddedCardProps> = ({
}
</div>
{
configurateMethods.includes(ConfigurateMethodEnum.customizableModel) && (
configurationMethods.includes(ConfigurationMethodEnum.customizableModel) && (
<AddModelButton
onClick={() => onOpenModal(ConfigurateMethodEnum.customizableModel)}
onClick={() => onOpenModal(ConfigurationMethodEnum.customizableModel)}
className='hidden group-hover:flex group-hover:text-primary-600'
/>
)
@ -152,7 +152,8 @@ const ProviderAddedCard: FC<ProviderAddedCardProps> = ({
provider={provider}
models={modelList}
onCollapse={() => setCollapsed(true)}
onConfig={currentCustomConfigrationModelFixedFields => onOpenModal(ConfigurateMethodEnum.customizableModel, currentCustomConfigrationModelFixedFields)}
onConfig={currentCustomConfigurationModelFixedFields => onOpenModal(ConfigurationMethodEnum.customizableModel, currentCustomConfigurationModelFixedFields)}
onChange={(provider: string) => getModelList(provider)}
/>
)
}

View file

@ -0,0 +1,119 @@
import { memo, useCallback } from 'react'
import { useTranslation } from 'react-i18next'
import classNames from 'classnames'
import { useDebounceFn } from 'ahooks'
import type { CustomConfigurationModelFixedFields, ModelItem, ModelProvider } from '../declarations'
import { ConfigurationMethodEnum, ModelStatusEnum } from '../declarations'
import ModelBadge from '../model-badge'
import ModelIcon from '../model-icon'
import ModelName from '../model-name'
import Button from '@/app/components/base/button'
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 TooltipPlus from '@/app/components/base/tooltip-plus'
import { useProviderContext, useProviderContextSelector } from '@/context/provider-context'
import { disableModel, enableModel } from '@/service/common'
import { Plan } from '@/app/components/billing/type'
export type ModelListItemProps = {
model: ModelItem
provider: ModelProvider
isConfigurable: boolean
onConfig: (currentCustomConfigurationModelFixedFields?: CustomConfigurationModelFixedFields) => void
onModifyLoadBalancing?: (model: ModelItem) => void
}
const ModelListItem = ({ model, provider, isConfigurable, onConfig, onModifyLoadBalancing }: ModelListItemProps) => {
const { t } = useTranslation()
const { plan } = useProviderContext()
const modelLoadBalancingEnabled = useProviderContextSelector(state => state.modelLoadBalancingEnabled)
const toggleModelEnablingStatus = useCallback(async (enabled: boolean) => {
if (enabled)
await enableModel(`/workspaces/current/model-providers/${provider.provider}/models/enable`, { model: model.model, model_type: model.model_type })
else
await disableModel(`/workspaces/current/model-providers/${provider.provider}/models/disable`, { model: model.model, model_type: model.model_type })
}, [model.model, model.model_type, provider.provider])
const { run: debouncedToggleModelEnablingStatus } = useDebounceFn(toggleModelEnablingStatus, { wait: 500 })
const onEnablingStateChange = useCallback(async (value: boolean) => {
debouncedToggleModelEnablingStatus(value)
}, [debouncedToggleModelEnablingStatus])
return (
<div
key={model.model}
className={classNames(
'group flex items-center pl-2 pr-2.5 h-8 rounded-lg',
isConfigurable && 'hover:bg-gray-50',
model.deprecated && 'opacity-60',
)}
>
<ModelIcon
className='shrink-0 mr-2'
provider={provider}
modelName={model.model}
/>
<ModelName
className='grow text-sm font-normal text-gray-900'
modelItem={model}
showModelType
showMode
showContextSize
>
{modelLoadBalancingEnabled && !model.deprecated && model.load_balancing_enabled && (
<ModelBadge className='ml-1 uppercase text-indigo-600 border-indigo-300'>
<Balance className='w-3 h-3 mr-0.5' />
{t('common.modelProvider.loadBalancingHeadline')}
</ModelBadge>
)}
</ModelName>
<div className='shrink-0 flex items-center'>
{
model.fetch_from === ConfigurationMethodEnum.customizableModel
? (
<Button
className='hidden group-hover:flex py-0 h-7 text-xs font-medium text-gray-700'
onClick={() => onConfig({ __model_name: model.model, __model_type: model.model_type })}
>
<Settings01 className='mr-[5px] w-3.5 h-3.5' />
{t('common.modelProvider.config')}
</Button>
)
: ((modelLoadBalancingEnabled || plan.type === Plan.sandbox) && !model.deprecated && [ModelStatusEnum.active, ModelStatusEnum.disabled].includes(model.status))
? (
<Button
className='opacity-0 group-hover:opacity-100 px-3 h-[28px] text-xs text-gray-700 rounded-md transition-opacity'
onClick={() => onModifyLoadBalancing?.(model)}
>
<Balance className='mr-1 w-[14px] h-[14px]' />
{t('common.modelProvider.configLoadBalancing')}
</Button>
)
: null
}
{
model.deprecated
? (
<TooltipPlus popupContent={<span className='font-semibold'>{t('common.modelProvider.modelHasBeenDeprecated')}</span>} offset={{ mainAxis: 4 }}>
<Switch defaultValue={false} disabled size='md' />
</TooltipPlus>
)
: (
<Switch
className='ml-2'
defaultValue={model?.status === ModelStatusEnum.active}
disabled={![ModelStatusEnum.active, ModelStatusEnum.disabled].includes(model.status)}
size='md'
onChange={onEnablingStateChange}
/>
)
}
</div>
</div>
)
}
export default memo(ModelListItem)

View file

@ -1,41 +1,48 @@
import type { FC } from 'react'
import { useCallback } from 'react'
import { useTranslation } from 'react-i18next'
import type {
CustomConfigrationModelFixedFields,
CustomConfigurationModelFixedFields,
ModelItem,
ModelProvider,
} from '../declarations'
import {
ConfigurateMethodEnum,
ModelStatusEnum,
ConfigurationMethodEnum,
} from '../declarations'
import { useLanguage } from '../hooks'
import ModelIcon from '../model-icon'
import ModelName from '../model-name'
// import Tab from './tab'
import AddModelButton from './add-model-button'
import Indicator from '@/app/components/header/indicator'
import { Settings01 } from '@/app/components/base/icons/src/vender/line/general'
import ModelListItem from './model-list-item'
import { ChevronDownDouble } from '@/app/components/base/icons/src/vender/line/arrows'
import Button from '@/app/components/base/button'
import { useModalContextSelector } from '@/context/modal-context'
type ModelListProps = {
provider: ModelProvider
models: ModelItem[]
onCollapse: () => void
onConfig: (currentCustomConfigrationModelFixedFields?: CustomConfigrationModelFixedFields) => void
onConfig: (currentCustomConfigurationModelFixedFields?: CustomConfigurationModelFixedFields) => void
onChange?: (provider: string) => void
}
const ModelList: FC<ModelListProps> = ({
provider,
models,
onCollapse,
onConfig,
onChange,
}) => {
const { t } = useTranslation()
const language = useLanguage()
const configurateMethods = provider.configurate_methods.filter(method => method !== ConfigurateMethodEnum.fetchFromRemote)
const canCustomConfig = configurateMethods.includes(ConfigurateMethodEnum.customizableModel)
// const canSystemConfig = configurateMethods.includes(ConfigurateMethodEnum.predefinedModel)
const configurativeMethods = provider.configurate_methods.filter(method => method !== ConfigurationMethodEnum.fetchFromRemote)
const isConfigurable = configurativeMethods.includes(ConfigurationMethodEnum.customizableModel)
const setShowModelLoadBalancingModal = useModalContextSelector(state => state.setShowModelLoadBalancingModal)
const onModifyLoadBalancing = useCallback((model: ModelItem) => {
setShowModelLoadBalancingModal({
provider,
model: model!,
open: !!model,
onClose: () => setShowModelLoadBalancingModal(null),
onSave: onChange,
})
}, [onChange, provider, setShowModelLoadBalancingModal])
return (
<div className='px-2 pb-2 rounded-b-xl'>
@ -46,10 +53,7 @@ const ModelList: FC<ModelListProps> = ({
{t('common.modelProvider.modelsNum', { num: models.length })}
</span>
<span
className={`
hidden group-hover:inline-flex items-center pl-1 pr-1.5 h-6 bg-gray-50
text-xs font-medium text-gray-500 cursor-pointer rounded-lg
`}
className='hidden group-hover:inline-flex items-center pl-1 pr-1.5 h-6 text-xs font-medium text-gray-500 bg-gray-50 cursor-pointer rounded-lg'
onClick={() => onCollapse()}
>
<ChevronDownDouble className='mr-0.5 w-3 h-3 rotate-180' />
@ -57,14 +61,14 @@ const ModelList: FC<ModelListProps> = ({
</span>
</span>
{/* {
canCustomConfig && canSystemConfig && (
isConfigurable && canSystemConfig && (
<span className='flex items-center'>
<Tab active='all' onSelect={() => {}} />
</span>
)
} */}
{
canCustomConfig && (
isConfigurable && (
<div className='grow flex justify-end'>
<AddModelButton onClick={() => onConfig()} />
</div>
@ -73,44 +77,16 @@ const ModelList: FC<ModelListProps> = ({
</div>
{
models.map(model => (
<div
<ModelListItem
key={model.model}
className={`
group flex items-center pl-2 pr-2.5 h-8 rounded-lg
${canCustomConfig && 'hover:bg-gray-50'}
${model.deprecated && 'opacity-60'}
`}
>
<ModelIcon
className='shrink-0 mr-2'
provider={provider}
modelName={model.model}
/>
<ModelName
className='grow text-sm font-normal text-gray-900'
modelItem={model}
showModelType
showMode
showContextSize
/>
<div className='shrink-0 flex items-center'>
{
model.fetch_from === ConfigurateMethodEnum.customizableModel && (
<Button
className='hidden group-hover:flex py-0 h-7 text-xs font-medium text-gray-700'
onClick={() => onConfig({ __model_name: model.model, __model_type: model.model_type })}
>
<Settings01 className='mr-[5px] w-3.5 h-3.5' />
{t('common.modelProvider.config')}
</Button>
)
}
<Indicator
className='ml-2.5'
color={model.status === ModelStatusEnum.active ? 'green' : 'gray'}
/>
</div>
</div>
{...{
model,
provider,
isConfigurable,
onConfig,
onModifyLoadBalancing,
}}
/>
))
}
</div>

View file

@ -0,0 +1,269 @@
import classNames from 'classnames'
import type { Dispatch, SetStateAction } from 'react'
import { useCallback } from 'react'
import { useTranslation } from 'react-i18next'
import type { ConfigurationMethodEnum, CustomConfigurationModelFixedFields, ModelLoadBalancingConfig, ModelLoadBalancingConfigEntry, ModelProvider } from '../declarations'
import Indicator from '../../../indicator'
import CooldownTimer from './cooldown-timer'
import TooltipPlus from '@/app/components/base/tooltip-plus'
import Switch from '@/app/components/base/switch'
import { Balance } from '@/app/components/base/icons/src/vender/line/financeAndECommerce'
import { Edit02, HelpCircle, Plus02, Trash03 } from '@/app/components/base/icons/src/vender/line/general'
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 s from '@/app/components/custom/style.module.css'
import GridMask from '@/app/components/base/grid-mask'
import { useProviderContextSelector } from '@/context/provider-context'
import { IS_CE_EDITION } from '@/config'
export type ModelLoadBalancingConfigsProps = {
draftConfig?: ModelLoadBalancingConfig
setDraftConfig: Dispatch<SetStateAction<ModelLoadBalancingConfig | undefined>>
provider: ModelProvider
configurationMethod: ConfigurationMethodEnum
currentCustomConfigurationModelFixedFields?: CustomConfigurationModelFixedFields
withSwitch?: boolean
className?: string
}
const ModelLoadBalancingConfigs = ({
draftConfig,
setDraftConfig,
provider,
configurationMethod,
currentCustomConfigurationModelFixedFields,
withSwitch = false,
className,
}: ModelLoadBalancingConfigsProps) => {
const { t } = useTranslation()
const modelLoadBalancingEnabled = useProviderContextSelector(state => state.modelLoadBalancingEnabled)
const updateConfigEntry = useCallback(
(
index: number,
modifier: (entry: ModelLoadBalancingConfigEntry) => ModelLoadBalancingConfigEntry | undefined,
) => {
setDraftConfig((prev) => {
if (!prev)
return prev
const newConfigs = [...prev.configs]
const modifiedConfig = modifier(newConfigs[index])
if (modifiedConfig)
newConfigs[index] = modifiedConfig
else
newConfigs.splice(index, 1)
return {
...prev,
configs: newConfigs,
}
})
},
[setDraftConfig],
)
const toggleModalBalancing = useCallback((enabled: boolean) => {
if ((modelLoadBalancingEnabled || !enabled) && draftConfig) {
setDraftConfig({
...draftConfig,
enabled,
})
}
}, [draftConfig, modelLoadBalancingEnabled, setDraftConfig])
const toggleConfigEntryEnabled = useCallback((index: number, state?: boolean) => {
updateConfigEntry(index, entry => ({
...entry,
enabled: typeof state === 'boolean' ? state : !entry.enabled,
}))
}, [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) => {
updateConfigEntry(index, ({ ttl: _, ...entry }) => {
return {
...entry,
in_cooldown: false,
}
})
}, [updateConfigEntry])
if (!draftConfig)
return null
return (
<>
<div
className={classNames(
'min-h-16 bg-gray-50 border rounded-xl transition-colors',
(withSwitch || !draftConfig.enabled) ? 'border-gray-200' : 'border-primary-400',
(withSwitch || draftConfig.enabled) ? 'cursor-default' : 'cursor-pointer',
className,
)}
onClick={(!withSwitch && !draftConfig.enabled) ? () => toggleModalBalancing(true) : undefined}
>
<div className='flex items-center px-[15px] py-3 gap-2 select-none'>
<div className='grow-0 shrink-0 flex items-center justify-center w-8 h-8 text-primary-600 bg-indigo-50 border border-indigo-100 rounded-lg'>
<Balance className='w-4 h-4' />
</div>
<div className='grow'>
<div className='flex items-center gap-1 text-sm'>
{t('common.modelProvider.loadBalancing')}
<TooltipPlus popupContent={t('common.modelProvider.loadBalancingInfo')} popupClassName='max-w-[300px]'>
<HelpCircle className='w-3 h-3 text-gray-400' />
</TooltipPlus>
</div>
<div className='text-xs text-gray-500'>{t('common.modelProvider.loadBalancingDescription')}</div>
</div>
{
withSwitch && (
<Switch
defaultValue={Boolean(draftConfig.enabled)}
size='l'
className='ml-3 justify-self-end'
disabled={!modelLoadBalancingEnabled && !draftConfig.enabled}
onChange={value => toggleModalBalancing(value)}
/>
)
}
</div>
{draftConfig.enabled && (
<div className='flex flex-col gap-1 px-3 pb-3'>
{draftConfig.configs.map((config, index) => {
const isProviderManaged = config.name === '__inherit__'
return (
<div key={config.id || index} className='group flex items-center px-3 h-10 bg-white border border-gray-200 rounded-lg shadow-xs'>
<div className='grow flex items-center'>
<div className='flex items-center justify-center mr-2 w-3 h-3'>
{(config.in_cooldown && Boolean(config.ttl))
? (
<CooldownTimer secondsRemaining={config.ttl} onFinish={() => clearCountdown(index)} />
)
: (
<TooltipPlus popupContent={t('common.modelProvider.apiKeyStatusNormal')}>
<Indicator color='green' />
</TooltipPlus>
)}
</div>
<div className='text-[13px] mr-1'>
{isProviderManaged ? t('common.modelProvider.defaultConfig') : config.name}
</div>
{isProviderManaged && (
<span className='px-1 text-2xs uppercase text-gray-500 border border-black/8 rounded-[5px]'>{t('common.modelProvider.providerManaged')}</span>
)}
</div>
<div className='flex items-center gap-1'>
{!isProviderManaged && (
<>
<div className='flex items-center gap-1 opacity-0 transition-opacity group-hover:opacity-100'>
<span
className='flex items-center justify-center w-8 h-8 text-gray-500 bg-white rounded-lg transition-colors cursor-pointer hover:bg-black/5'
onClick={() => toggleEntryModal(index, config)}
>
<Edit02 className='w-4 h-4' />
</span>
<span
className='flex items-center justify-center w-8 h-8 text-gray-500 bg-white rounded-lg transition-colors cursor-pointer hover:bg-black/5'
onClick={() => updateConfigEntry(index, () => undefined)}
>
<Trash03 className='w-4 h-4' />
</span>
<span className='mr-2 h-3 border-r border-r-gray-100' />
</div>
</>
)}
<Switch
defaultValue={Boolean(config.enabled)}
size='md'
className='justify-self-end'
onChange={value => toggleConfigEntryEnabled(index, value)}
/>
</div>
</div>
)
})}
<div
className='flex items-center px-3 mt-1 h-8 text-[13px] font-medium text-primary-600'
onClick={() => toggleEntryModal()}
>
<div className='flex items-center cursor-pointer'>
<Plus02 className='mr-2 w-3 h-3' />{t('common.modelProvider.addConfig')}
</div>
</div>
</div>
)}
{
draftConfig.enabled && draftConfig.configs.length < 2 && (
<div className='flex items-center px-6 h-[34px] text-xs text-gray-700 bg-black/2 border-t border-t-black/5'>
<AlertTriangle className='mr-1 w-3 h-3 text-[#f79009]' />
{t('common.modelProvider.loadBalancingLeastKeyWarning')}
</div>
)
}
</div>
{!modelLoadBalancingEnabled && !IS_CE_EDITION && (
<GridMask canvasClassName='!rounded-xl'>
<div className='flex items-center justify-between mt-2 px-4 h-14 border-[0.5px] border-gray-200 rounded-xl shadow-md'>
<div
className={classNames('text-sm font-semibold leading-tight text-gradient', s.textGradient)}
>
{t('common.modelProvider.upgradeForLoadBalancing')}
</div>
<UpgradeBtn />
</div>
</GridMask>
)}
</>
)
}
export default ModelLoadBalancingConfigs

View file

@ -0,0 +1,190 @@
import { memo, useCallback, useEffect, useMemo, useState } from 'react'
import { useTranslation } from 'react-i18next'
import classNames from 'classnames'
import useSWR from 'swr'
import type { ModelItem, ModelLoadBalancingConfig, ModelLoadBalancingConfigEntry, ModelProvider } from '../declarations'
import { FormTypeEnum } from '../declarations'
import ModelIcon from '../model-icon'
import ModelName from '../model-name'
import { savePredefinedLoadBalancingConfig } from '../utils'
import ModelLoadBalancingConfigs from './model-load-balancing-configs'
import Modal from '@/app/components/base/modal'
import Button from '@/app/components/base/button'
import { fetchModelLoadBalancingConfig } from '@/service/common'
import Loading from '@/app/components/base/loading'
import { useToastContext } from '@/app/components/base/toast'
export type ModelLoadBalancingModalProps = {
provider: ModelProvider
model: ModelItem
open?: boolean
onClose?: () => void
onSave?: (provider: string) => void
}
// model balancing config modal
const ModelLoadBalancingModal = ({ provider, model, open = false, onClose, onSave }: ModelLoadBalancingModalProps) => {
const { t } = useTranslation()
const { notify } = useToastContext()
const [loading, setLoading] = useState(false)
const { data, mutate } = useSWR(
`/workspaces/current/model-providers/${provider.provider}/models/credentials?model=${model.model}&model_type=${model.model_type}`,
fetchModelLoadBalancingConfig,
)
const originalConfig = data?.load_balancing
const [draftConfig, setDraftConfig] = useState<ModelLoadBalancingConfig>()
const originalConfigMap = useMemo(() => {
if (!originalConfig)
return {}
return originalConfig?.configs.reduce((prev, config) => {
if (config.id)
prev[config.id] = config
return prev
}, {} as Record<string, ModelLoadBalancingConfigEntry>)
}, [originalConfig])
useEffect(() => {
if (originalConfig)
setDraftConfig(originalConfig)
}, [originalConfig])
const toggleModalBalancing = useCallback((enabled: boolean) => {
if (draftConfig) {
setDraftConfig({
...draftConfig,
enabled,
})
}
}, [draftConfig])
const extendedSecretFormSchemas = useMemo(
() => provider.provider_credential_schema.credential_form_schemas.filter(
({ type }) => type === FormTypeEnum.secretInput,
),
[provider.provider_credential_schema.credential_form_schemas],
)
const encodeConfigEntrySecretValues = useCallback((entry: ModelLoadBalancingConfigEntry) => {
const result = { ...entry }
extendedSecretFormSchemas.forEach(({ variable }) => {
if (entry.id && result.credentials[variable] === originalConfigMap[entry.id]?.credentials?.[variable])
result.credentials[variable] = '[__HIDDEN__]'
})
return result
}, [extendedSecretFormSchemas, originalConfigMap])
const handleSave = async () => {
try {
setLoading(true)
const res = await savePredefinedLoadBalancingConfig(
provider.provider,
({
...(data?.credentials ?? {}),
__model_type: model.model_type,
__model_name: model.model,
}),
{
...draftConfig,
enabled: Boolean(draftConfig?.enabled),
configs: draftConfig!.configs.map(encodeConfigEntrySecretValues),
},
)
if (res.result === 'success') {
notify({ type: 'success', message: t('common.actionMsg.modifiedSuccessfully') })
mutate()
onSave?.(provider.provider)
onClose?.()
}
}
finally {
setLoading(false)
}
}
return (
<Modal
isShow={Boolean(model) && open}
onClose={onClose}
wrapperClassName='!z-30'
className='max-w-none pt-8 px-8 w-[640px]'
title={
<div className='pb-3 font-semibold'>
<div className='h-[30px]'>{t('common.modelProvider.configLoadBalancing')}</div>
{Boolean(model) && (
<div className='flex items-center h-5'>
<ModelIcon
className='shrink-0 mr-2'
provider={provider}
modelName={model!.model}
/>
<ModelName
className='grow text-sm font-normal text-gray-900'
modelItem={model!}
showModelType
showMode
showContextSize
/>
</div>
)}
</div>
}
>
{!draftConfig
? <Loading type='area' />
: (
<>
<div className='py-2'>
<div
className={classNames(
'min-h-16 bg-gray-50 border rounded-xl transition-colors',
draftConfig.enabled ? 'border-gray-200 cursor-pointer' : 'border-primary-400 cursor-default',
)}
onClick={draftConfig.enabled ? () => toggleModalBalancing(false) : undefined}
>
<div className='flex items-center px-[15px] py-3 gap-2 select-none'>
<div className='grow-0 shrink-0 flex items-center justify-center w-8 h-8 bg-white border rounded-lg'>
{Boolean(model) && (
<ModelIcon className='shrink-0' provider={provider} modelName={model!.model} />
)}
</div>
<div className='grow'>
<div className='text-sm'>{t('common.modelProvider.providerManaged')}</div>
<div className='text-xs text-gray-500'>{t('common.modelProvider.providerManagedDescription')}</div>
</div>
</div>
</div>
<ModelLoadBalancingConfigs {...{
draftConfig,
setDraftConfig,
provider,
currentCustomConfigurationModelFixedFields: {
__model_name: model.model,
__model_type: model.model_type,
},
configurationMethod: model.fetch_from,
className: 'mt-2',
}} />
</div>
<div className='flex items-center justify-end gap-2 mt-6'>
<Button onClick={onClose}>{t('common.operation.cancel')}</Button>
<Button
type='primary'
onClick={handleSave}
disabled={
loading
|| (draftConfig?.enabled && (draftConfig?.configs.filter(config => config.enabled).length ?? 0) < 2)
}
>{t('common.operation.save')}</Button>
</div>
</>
)
}
</Modal >
)
}
export default memo(ModelLoadBalancingModal)

View file

@ -18,7 +18,7 @@ const Selector: FC<SelectorProps> = ({
const options = [
{
key: PreferredProviderTypeEnum.custom,
text: 'API',
text: t('common.modelProvider.apiKey'),
},
{
key: PreferredProviderTypeEnum.system,