fix: load balancing

This commit is contained in:
zxhlyh 2025-08-19 16:42:07 +08:00
parent 6463b3d051
commit 7bddf323e1
11 changed files with 135 additions and 67 deletions

View File

@ -86,6 +86,7 @@ export enum ModelStatusEnum {
quotaExceeded = 'quota-exceeded', quotaExceeded = 'quota-exceeded',
noPermission = 'no-permission', noPermission = 'no-permission',
disabled = 'disabled', disabled = 'disabled',
credentialRemoved = 'credential-removed',
} }
export const MODEL_STATUS_TEXT: { [k: string]: TypeWithI18N } = { export const MODEL_STATUS_TEXT: { [k: string]: TypeWithI18N } = {
@ -185,6 +186,8 @@ export type QuotaConfiguration = {
export type Credential = { export type Credential = {
credential_id: string credential_id: string
credential_name?: string credential_name?: string
from_enterprise?: boolean
allowed_to_use?: boolean
} }
export type CustomModel = { export type CustomModel = {
@ -316,4 +319,6 @@ export type ModelCredential = {
credentials: Record<string, any> credentials: Record<string, any>
load_balancing: ModelLoadBalancingConfig load_balancing: ModelLoadBalancingConfig
available_credentials: Credential[] available_credentials: Credential[]
current_credential_id?: string
current_credential_name?: string
} }

View File

@ -13,6 +13,7 @@ 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 Badge from '@/app/components/base/badge'
type CredentialItemProps = { type CredentialItemProps = {
credential: Credential credential: Credential
@ -49,7 +50,9 @@ 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)} 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'>
{ {
@ -71,6 +74,13 @@ const CredentialItem = ({
{credential.credential_name} {credential.credential_name}
</div> </div>
</div> </div>
{
credential.from_enterprise && (
<Badge className='shrink-0'>
Enterprise
</Badge>
)
}
{ {
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'>

View File

@ -5,30 +5,32 @@ import {
} from '@remixicon/react' } from '@remixicon/react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import Button from '@/app/components/base/button' import Button from '@/app/components/base/button'
import Indicator from '@/app/components/header/indicator'
import cn from '@/utils/classnames' import cn from '@/utils/classnames'
type ConfigModelProps = { type ConfigModelProps = {
className?: string
onClick?: () => void onClick?: () => void
loadBalancingEnabled?: boolean loadBalancingEnabled?: boolean
loadBalancingInvalid?: boolean loadBalancingInvalid?: boolean
credentialRemoved?: boolean
} }
const ConfigModel = ({ const ConfigModel = ({
className,
onClick, onClick,
loadBalancingEnabled, loadBalancingEnabled,
loadBalancingInvalid, loadBalancingInvalid,
credentialRemoved,
}: ConfigModelProps) => { }: ConfigModelProps) => {
const { t } = useTranslation() const { t } = useTranslation()
if (loadBalancingEnabled && loadBalancingInvalid) { if (loadBalancingEnabled && loadBalancingInvalid && !credentialRemoved) {
return ( return (
<div <div
className='system-2xs-medium-uppercase flex h-[18px] items-center rounded-[5px] border border-text-warning bg-components-badge-bg-dimm px-1.5' className='system-2xs-medium-uppercase relative flex h-[18px] items-center rounded-[5px] border border-text-warning bg-components-badge-bg-dimm px-1.5 text-text-warning'
onClick={onClick} onClick={onClick}
> >
<RiScales3Line className='mr-0.5 h-3 w-3' /> <RiScales3Line className='mr-0.5 h-3 w-3' />
{t('common.modelProvider.auth.authorizationError')} {t('common.modelProvider.auth.authorizationError')}
<Indicator color='orange' className='absolute right-[-1px] top-[-1px] h-1.5 w-1.5' />
</div> </div>
) )
} }
@ -38,13 +40,21 @@ const ConfigModel = ({
variant='secondary' variant='secondary'
size='small' size='small'
className={cn( className={cn(
'shrink-0', 'hidden shrink-0 group-hover:flex',
className, credentialRemoved && 'flex',
)} )}
onClick={onClick} onClick={onClick}
> >
{ {
!loadBalancingEnabled && ( credentialRemoved && (
<>
{t('common.modelProvider.auth.credentialRemoved')}
<Indicator color='red' className='ml-2' />
</>
)
}
{
!loadBalancingEnabled && !credentialRemoved && (
<> <>
<RiEqualizer2Line className='mr-1 h-4 w-4' /> <RiEqualizer2Line className='mr-1 h-4 w-4' />
{t('common.operation.config')} {t('common.operation.config')}
@ -52,7 +62,7 @@ const ConfigModel = ({
) )
} }
{ {
loadBalancingEnabled && !loadBalancingInvalid && ( loadBalancingEnabled && !loadBalancingInvalid && !credentialRemoved && (
<> <>
<RiScales3Line className='mr-1 h-4 w-4' /> <RiScales3Line className='mr-1 h-4 w-4' />
{t('common.modelProvider.auth.configLoadBalancing')} {t('common.modelProvider.auth.configLoadBalancing')}

View File

@ -7,40 +7,38 @@ import { useTranslation } from 'react-i18next'
import { RiArrowDownSLine } from '@remixicon/react' import { RiArrowDownSLine } from '@remixicon/react'
import Button from '@/app/components/base/button' import Button from '@/app/components/base/button'
import Indicator from '@/app/components/header/indicator' import Indicator from '@/app/components/header/indicator'
import Badge from '@/app/components/base/badge'
import Authorized from './authorized' import Authorized from './authorized'
import type { import type {
ModelLoadBalancingConfig, Credential,
CustomModel,
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 cn from '@/utils/classnames' import cn from '@/utils/classnames'
type SwitchCredentialInLoadBalancingProps = { type SwitchCredentialInLoadBalancingProps = {
provider: ModelProvider provider: ModelProvider
draftConfig?: ModelLoadBalancingConfig model: CustomModel
setDraftConfig: Dispatch<SetStateAction<ModelLoadBalancingConfig | undefined>> credentials?: Credential[]
customModelCredential?: Credential
setCustomModelCredential: Dispatch<SetStateAction<Credential | undefined>>
} }
const SwitchCredentialInLoadBalancing = ({ const SwitchCredentialInLoadBalancing = ({
provider, provider,
draftConfig, model,
customModelCredential,
setCustomModelCredential,
credentials,
}: SwitchCredentialInLoadBalancingProps) => { }: SwitchCredentialInLoadBalancingProps) => {
const { t } = useTranslation() const { t } = useTranslation()
const {
available_credentials,
current_credential_name,
} = useCredentialStatus(provider)
const handleItemClick = useCallback(() => { const handleItemClick = useCallback((credential: Credential) => {
console.log('handleItemClick', draftConfig) setCustomModelCredential(credential)
}, []) }, [setCustomModelCredential])
const renderTrigger = useCallback(() => { const renderTrigger = useCallback(() => {
const selectedCredentialId = draftConfig?.configs.find(config => config.name === '__inherit__')?.credential_id const selectedCredentialId = customModelCredential?.credential_id
const selectedCredential = available_credentials?.find(credential => credential.credential_id === selectedCredentialId) const authRemoved = !selectedCredentialId && !!credentials?.length
const name = selectedCredential?.credential_name || current_credential_name
const authRemoved = !!selectedCredentialId && !selectedCredential
return ( return (
<Button <Button
variant='secondary' variant='secondary'
@ -54,17 +52,12 @@ const SwitchCredentialInLoadBalancing = ({
color={authRemoved ? 'red' : 'green'} color={authRemoved ? 'red' : 'green'}
/> />
{ {
authRemoved ? t('common.model.authRemoved') : name authRemoved ? t('common.modelProvider.auth.authRemoved') : customModelCredential?.credential_name
}
{
!authRemoved && (
<Badge>enterprise</Badge>
)
} }
<RiArrowDownSLine className='h-4 w-4' /> <RiArrowDownSLine className='h-4 w-4' />
</Button> </Button>
) )
}, [current_credential_name, t, draftConfig, available_credentials]) }, [customModelCredential, t, credentials])
return ( return (
<Authorized <Authorized
@ -72,17 +65,25 @@ const SwitchCredentialInLoadBalancing = ({
configurationMethod={ConfigurationMethodEnum.customizableModel} configurationMethod={ConfigurationMethodEnum.customizableModel}
items={[ items={[
{ {
model: { title: t('common.modelProvider.auth.modelCredentials'),
model: t('common.modelProvider.modelCredentials'), model,
} as any, credentials: credentials || [],
credentials: available_credentials || [],
}, },
]} ]}
renderTrigger={renderTrigger} renderTrigger={renderTrigger}
onItemClick={handleItemClick} onItemClick={handleItemClick}
isModelCredential isModelCredential
enableAddModelCredential enableAddModelCredential
bottomAddModelCredentialText={t('common.modelProvider.addModelCredential')} bottomAddModelCredentialText={t('common.modelProvider.auth.addModelCredential')}
selectedCredential={
customModelCredential
? {
credential_id: customModelCredential?.credential_id || '',
credential_name: customModelCredential?.credential_name || '',
}
: undefined
}
showItemSelectedIcon
/> />
) )
} }

View File

@ -3,7 +3,6 @@ import { useTranslation } from 'react-i18next'
import { useDebounceFn } from 'ahooks' import { useDebounceFn } from 'ahooks'
import type { ModelItem, ModelProvider } from '../declarations' import type { ModelItem, ModelProvider } from '../declarations'
import { ModelStatusEnum } from '../declarations' import { ModelStatusEnum } from '../declarations'
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'
@ -15,6 +14,7 @@ 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' import { ConfigModel } from '../model-auth'
import Badge from '@/app/components/base/badge'
export type ModelListItemProps = { export type ModelListItemProps = {
model: ModelItem model: ModelItem
@ -63,21 +63,20 @@ const ModelListItem = ({ model, provider, isConfigurable, onModifyLoadBalancing
showMode showMode
showContextSize showContextSize
> >
{modelLoadBalancingEnabled && !model.deprecated && model.load_balancing_enabled && (
<ModelBadge className='ml-1 border-text-accent-secondary uppercase text-text-accent-secondary'>
<Balance className='mr-0.5 h-3 w-3' />
{t('common.modelProvider.loadBalancingHeadline')}
</ModelBadge>
)}
</ModelName> </ModelName>
<div className='flex shrink-0 items-center'> <div className='flex shrink-0 items-center'>
{modelLoadBalancingEnabled && !model.deprecated && model.load_balancing_enabled && (
<Badge className='mr-1 h-[18px] w-[18px] items-center justify-center border-text-accent-secondary p-0'>
<Balance className='h-3 w-3 text-text-accent-secondary' />
</Badge>
)}
{ {
(isCurrentWorkspaceManager && (modelLoadBalancingEnabled || plan.type === Plan.sandbox) && !model.deprecated && [ModelStatusEnum.active, ModelStatusEnum.disabled].includes(model.status)) && ( (isCurrentWorkspaceManager && (modelLoadBalancingEnabled || plan.type === Plan.sandbox) && !model.deprecated && [ModelStatusEnum.active, ModelStatusEnum.disabled].includes(model.status)) && (
<ConfigModel <ConfigModel
className='hidden group-hover:flex'
onClick={() => onModifyLoadBalancing?.(model)} onClick={() => onModifyLoadBalancing?.(model)}
loadBalancingEnabled={model.load_balancing_enabled} loadBalancingEnabled={model.load_balancing_enabled}
loadBalancingInvalid={model.has_invalid_load_balancing_configs} loadBalancingInvalid={model.has_invalid_load_balancing_configs}
credentialRemoved={model.status === ModelStatusEnum.credentialRemoved}
/> />
) )
} }

View File

@ -1,5 +1,5 @@
import type { Dispatch, SetStateAction } from 'react' import type { Dispatch, SetStateAction } from 'react'
import { useCallback } from 'react' import { useCallback, useMemo } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { import {
RiDeleteBinLine, RiDeleteBinLine,
@ -122,6 +122,20 @@ const ModelLoadBalancingConfigs = ({
}) })
}, [updateConfigEntry]) }, [updateConfigEntry])
const validDraftConfigList = useMemo(() => {
if (!draftConfig)
return []
return draftConfig.configs.filter((config) => {
if (config.name === '__inherit__')
return true
if (config.credential_id)
return true
return false
})
}, [draftConfig])
if (!draftConfig) if (!draftConfig)
return null return null
@ -165,15 +179,7 @@ const ModelLoadBalancingConfigs = ({
</div> </div>
{draftConfig.enabled && ( {draftConfig.enabled && (
<div className='flex flex-col gap-1 px-3 pb-3'> <div className='flex flex-col gap-1 px-3 pb-3'>
{draftConfig.configs.filter((config) => { {validDraftConfigList.map((config, index) => {
if (config.name === '__inherit__')
return true
if (config.credential_id)
return true
return false
}).map((config, index) => {
const isProviderManaged = config.name === '__inherit__' const isProviderManaged = config.name === '__inherit__'
return ( return (
<div key={config.id || index} className='group flex h-10 items-center rounded-lg border border-components-panel-border bg-components-panel-on-panel-item-bg px-3 shadow-xs'> <div key={config.id || index} className='group flex h-10 items-center rounded-lg border border-components-panel-border bg-components-panel-on-panel-item-bg px-3 shadow-xs'>
@ -249,7 +255,7 @@ const ModelLoadBalancingConfigs = ({
</div> </div>
)} )}
{ {
draftConfig.enabled && draftConfig.configs.length < 2 && ( draftConfig.enabled && validDraftConfigList.length < 2 && (
<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'> <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')}

View File

@ -19,7 +19,7 @@ import Modal from '@/app/components/base/modal'
import Button from '@/app/components/base/button' import Button from '@/app/components/base/button'
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 { import {
useGetModelCredential, useGetModelCredential,
useUpdateModelLoadBalancingConfig, useUpdateModelLoadBalancingConfig,
@ -59,6 +59,9 @@ const ModelLoadBalancingModal = ({
const modelCredential = data const modelCredential = data
const { const {
load_balancing, load_balancing,
current_credential_id,
available_credentials,
current_credential_name,
} = modelCredential ?? {} } = modelCredential ?? {}
const originalConfig = load_balancing const originalConfig = load_balancing
const [draftConfig, setDraftConfig] = useState<ModelLoadBalancingConfig>() const [draftConfig, setDraftConfig] = useState<ModelLoadBalancingConfig>()
@ -102,11 +105,21 @@ const ModelLoadBalancingModal = ({
}, [extendedSecretFormSchemas, originalConfigMap]) }, [extendedSecretFormSchemas, originalConfigMap])
const { mutateAsync: updateModelLoadBalancingConfig } = useUpdateModelLoadBalancingConfig(provider.provider) const { mutateAsync: updateModelLoadBalancingConfig } = useUpdateModelLoadBalancingConfig(provider.provider)
const initialCustomModelCredential = useMemo(() => {
if (!current_credential_id)
return undefined
return {
credential_id: current_credential_id,
credential_name: current_credential_name,
}
}, [current_credential_id, current_credential_name])
const [customModelCredential, setCustomModelCredential] = useState<Credential | undefined>(initialCustomModelCredential)
const handleSave = async () => { const handleSave = async () => {
try { try {
setLoading(true) setLoading(true)
const res = await updateModelLoadBalancingConfig( const res = await updateModelLoadBalancingConfig(
{ {
credential_id: customModelCredential?.credential_id || current_credential_id,
config_from: configFrom, config_from: configFrom,
model: model.model, model: model.model,
model_type: model.model_type, model_type: model.model_type,
@ -178,14 +191,28 @@ const ModelLoadBalancingModal = ({
)} )}
</div> </div>
<div className='grow'> <div className='grow'>
<div className='text-sm text-text-secondary'>{t('common.modelProvider.providerManaged')}</div> <div className='text-sm text-text-secondary'>{
<div className='text-xs text-text-tertiary'>{t('common.modelProvider.providerManagedDescription')}</div> providerFormSchemaPredefined
? t('common.modelProvider.auth.providerManaged')
: t('common.modelProvider.auth.specifyModelCredential')
}</div>
<div className='text-xs text-text-tertiary'>{
providerFormSchemaPredefined
? t('common.modelProvider.auth.providerManagedTip')
: t('common.modelProvider.auth.specifyModelCredentialTip')
}</div>
</div> </div>
{/* <SwitchCredentialInLoadBalancing {
draftConfig={draftConfig} !providerFormSchemaPredefined && (
setDraftConfig={setDraftConfig} <SwitchCredentialInLoadBalancing
provider={provider} provider={provider}
/> */} customModelCredential={initialCustomModelCredential ?? customModelCredential}
setCustomModelCredential={setCustomModelCredential}
model={model}
credentials={available_credentials}
/>
)
}
</div> </div>
</div> </div>
{ {

View File

@ -23,4 +23,5 @@ export type Credential = {
credentials?: Record<string, any> credentials?: Record<string, any>
isWorkspaceDefault?: boolean isWorkspaceDefault?: boolean
from_enterprise?: boolean from_enterprise?: boolean
allowed_to_use?: boolean
} }

View File

@ -467,7 +467,7 @@ const translation = {
loadPresets: 'Load Presets', loadPresets: 'Load Presets',
parameters: 'PARAMETERS', parameters: 'PARAMETERS',
loadBalancing: 'Load balancing', loadBalancing: 'Load balancing',
loadBalancingDescription: 'Reduce pressure with multiple sets of credentials.', loadBalancingDescription: 'Configure multiple credentials for the model and invoke them automatically. ',
loadBalancingHeadline: 'Load Balancing', loadBalancingHeadline: 'Load Balancing',
configLoadBalancing: 'Config Load Balancing', configLoadBalancing: 'Config Load Balancing',
modelHasBeenDeprecated: 'This model has been deprecated', modelHasBeenDeprecated: 'This model has been deprecated',
@ -499,6 +499,10 @@ const translation = {
configModel: 'Config model', configModel: 'Config model',
configLoadBalancing: 'Config Load Balancing', configLoadBalancing: 'Config Load Balancing',
authorizationError: 'Authorization error', authorizationError: 'Authorization error',
specifyModelCredential: 'Specify model credential',
specifyModelCredentialTip: 'Use a configured model credential.',
providerManaged: 'Provider managed',
providerManagedTip: 'The current configuration is hosted by the provider.',
}, },
}, },
dataSource: { dataSource: {

View File

@ -466,7 +466,7 @@ const translation = {
loadPresets: '加载预设', loadPresets: '加载预设',
parameters: '参数', parameters: '参数',
loadBalancing: '负载均衡', loadBalancing: '负载均衡',
loadBalancingDescription: '为了减轻单组凭据的压力,您可以为模型调用配置多组凭据。', loadBalancingDescription: '为模型配置多组凭据,并自动调用。',
loadBalancingHeadline: '负载均衡', loadBalancingHeadline: '负载均衡',
configLoadBalancing: '设置负载均衡', configLoadBalancing: '设置负载均衡',
modelHasBeenDeprecated: '该模型已废弃', modelHasBeenDeprecated: '该模型已废弃',
@ -499,6 +499,10 @@ const translation = {
configModel: '配置模型', configModel: '配置模型',
configLoadBalancing: '配置负载均衡', configLoadBalancing: '配置负载均衡',
authorizationError: '授权错误', authorizationError: '授权错误',
specifyModelCredential: '指定模型凭据',
specifyModelCredentialTip: '使用已配置的模型凭据。',
providerManaged: '由模型供应商管理',
providerManagedTip: '使用模型供应商提供的单组凭据。',
}, },
}, },
dataSource: { dataSource: {

View File

@ -147,6 +147,7 @@ export const useUpdateModelLoadBalancingConfig = (provider: string) => {
model: string model: string
model_type: ModelTypeEnum model_type: ModelTypeEnum
load_balancing: ModelLoadBalancingConfig load_balancing: ModelLoadBalancingConfig
credential_id?: string
}) => post<{ result: string }>(`/workspaces/current/model-providers/${provider}/models`, { }) => post<{ result: string }>(`/workspaces/current/model-providers/${provider}/models`, {
body: data, body: data,
}), }),