feat: model load balancing (#4926)
This commit is contained in:
parent
d1dbbc1e33
commit
37f292ea91
58 changed files with 1896 additions and 304 deletions
|
|
@ -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)
|
||||
|
|
@ -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))
|
||||
})
|
||||
|
||||
|
|
|
|||
|
|
@ -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)}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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)
|
||||
|
|
@ -18,7 +18,7 @@ const Selector: FC<SelectorProps> = ({
|
|||
const options = [
|
||||
{
|
||||
key: PreferredProviderTypeEnum.custom,
|
||||
text: 'API',
|
||||
text: t('common.modelProvider.apiKey'),
|
||||
},
|
||||
{
|
||||
key: PreferredProviderTypeEnum.system,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue