feat: model load balancing (#4926)

This commit is contained in:
Nite Knite
2024-06-05 00:13:29 +08:00
committed by GitHub
parent d1dbbc1e33
commit 37f292ea91
58 changed files with 1896 additions and 304 deletions

View File

@@ -39,7 +39,7 @@ export const MODEL_TYPE_TEXT = {
[ModelTypeEnum.tts]: 'TTS',
}
export enum ConfigurateMethodEnum {
export enum ConfigurationMethodEnum {
predefinedModel = 'predefined-model',
customizableModel = 'customizable-model',
fetchFromRemote = 'fetch-from-remote',
@@ -64,6 +64,7 @@ export enum ModelStatusEnum {
noConfigure = 'no-configure',
quotaExceeded = 'quota-exceeded',
noPermission = 'no-permission',
disabled = 'disabled',
}
export const MODEL_STATUS_TEXT: { [k: string]: TypeWithI18N } = {
@@ -114,9 +115,10 @@ export type ModelItem = {
label: TypeWithI18N
model_type: ModelTypeEnum
features?: ModelFeatureEnum[]
fetch_from: ConfigurateMethodEnum
fetch_from: ConfigurationMethodEnum
status: ModelStatusEnum
model_properties: Record<string, string | number>
load_balancing_enabled: boolean
deprecated?: boolean
}
@@ -158,7 +160,7 @@ export type ModelProvider = {
icon_large: TypeWithI18N
background?: string
supported_model_types: ModelTypeEnum[]
configurate_methods: ConfigurateMethodEnum[]
configurate_methods: ConfigurationMethodEnum[]
provider_credential_schema: {
credential_form_schemas: CredentialFormSchema[]
}
@@ -204,7 +206,7 @@ export type DefaultModel = {
model: string
}
export type CustomConfigrationModelFixedFields = {
export type CustomConfigurationModelFixedFields = {
__model_name: string
__model_type: ModelTypeEnum
}
@@ -223,3 +225,23 @@ export type ModelParameterRule = {
options?: string[]
tagPlaceholder?: TypeWithI18N
}
export type ModelLoadBalancingConfigEntry = {
/** model balancing config entry id */
id?: string
/** is config entry enabled */
enabled?: boolean
/** config entry name */
name: string
/** model balancing credential */
credentials: Record<string, string | undefined | boolean>
/** is config entry currently removed from Round-robin queue */
in_cooldown?: boolean
/** cooldown time (in seconds) */
ttl?: number
}
export type ModelLoadBalancingConfig = {
enabled: boolean
configs: ModelLoadBalancingConfigEntry[]
}

View File

@@ -7,14 +7,14 @@ import {
import useSWR, { useSWRConfig } from 'swr'
import { useContext } from 'use-context-selector'
import type {
CustomConfigrationModelFixedFields,
CustomConfigurationModelFixedFields,
DefaultModel,
DefaultModelResponse,
Model,
ModelTypeEnum,
} from './declarations'
import {
ConfigurateMethodEnum,
ConfigurationMethodEnum,
ModelStatusEnum,
} from './declarations'
import I18n from '@/context/i18n'
@@ -61,42 +61,55 @@ export const useLanguage = () => {
return locale.replace('-', '_')
}
export const useProviderCrenditialsFormSchemasValue = (
export const useProviderCredentialsAndLoadBalancing = (
provider: string,
configurateMethod: ConfigurateMethodEnum,
configurationMethod: ConfigurationMethodEnum,
configured?: boolean,
currentCustomConfigrationModelFixedFields?: CustomConfigrationModelFixedFields,
currentCustomConfigurationModelFixedFields?: CustomConfigurationModelFixedFields,
) => {
const { data: predefinedFormSchemasValue } = useSWR(
(configurateMethod === ConfigurateMethodEnum.predefinedModel && configured)
const { data: predefinedFormSchemasValue, mutate: mutatePredefined } = useSWR(
(configurationMethod === ConfigurationMethodEnum.predefinedModel && configured)
? `/workspaces/current/model-providers/${provider}/credentials`
: null,
fetchModelProviderCredentials,
)
const { data: customFormSchemasValue } = useSWR(
(configurateMethod === ConfigurateMethodEnum.customizableModel && currentCustomConfigrationModelFixedFields)
? `/workspaces/current/model-providers/${provider}/models/credentials?model=${currentCustomConfigrationModelFixedFields?.__model_name}&model_type=${currentCustomConfigrationModelFixedFields?.__model_type}`
const { data: customFormSchemasValue, mutate: mutateCustomized } = useSWR(
(configurationMethod === ConfigurationMethodEnum.customizableModel && currentCustomConfigurationModelFixedFields)
? `/workspaces/current/model-providers/${provider}/models/credentials?model=${currentCustomConfigurationModelFixedFields?.__model_name}&model_type=${currentCustomConfigurationModelFixedFields?.__model_type}`
: null,
fetchModelProviderCredentials,
)
const value = useMemo(() => {
return configurateMethod === ConfigurateMethodEnum.predefinedModel
const credentials = useMemo(() => {
return configurationMethod === ConfigurationMethodEnum.predefinedModel
? predefinedFormSchemasValue?.credentials
: customFormSchemasValue?.credentials
? {
...customFormSchemasValue?.credentials,
...currentCustomConfigrationModelFixedFields,
...currentCustomConfigurationModelFixedFields,
}
: undefined
}, [
configurateMethod,
currentCustomConfigrationModelFixedFields,
configurationMethod,
currentCustomConfigurationModelFixedFields,
customFormSchemasValue?.credentials,
predefinedFormSchemasValue?.credentials,
])
return value
const mutate = useMemo(() => () => {
mutatePredefined()
mutateCustomized()
}, [mutateCustomized, mutatePredefined])
return {
credentials,
loadBalancing: (configurationMethod === ConfigurationMethodEnum.predefinedModel
? predefinedFormSchemasValue
: customFormSchemasValue
)?.load_balancing,
mutate,
}
// as ([Record<string, string | boolean | undefined> | undefined, ModelLoadBalancingConfig | undefined])
}
export const useModelList = (type: ModelTypeEnum) => {

View File

@@ -4,11 +4,11 @@ import SystemModelSelector from './system-model-selector'
import ProviderAddedCard, { UPDATE_MODEL_PROVIDER_CUSTOM_MODEL_LIST } from './provider-added-card'
import ProviderCard from './provider-card'
import type {
CustomConfigrationModelFixedFields,
CustomConfigurationModelFixedFields,
ModelProvider,
} from './declarations'
import {
ConfigurateMethodEnum,
ConfigurationMethodEnum,
CustomConfigurationStatusEnum,
ModelTypeEnum,
} from './declarations'
@@ -19,7 +19,7 @@ import {
} from './hooks'
import { AlertTriangle } from '@/app/components/base/icons/src/vender/solid/alertsAndFeedback'
import { useProviderContext } from '@/context/provider-context'
import { useModalContext } from '@/context/modal-context'
import { useModalContextSelector } from '@/context/modal-context'
import { useEventEmitterContextContext } from '@/context/event-emitter'
const ModelProviderPage = () => {
@@ -33,7 +33,7 @@ const ModelProviderPage = () => {
const { data: speech2textDefaultModel } = useDefaultModel(ModelTypeEnum.speech2text)
const { data: ttsDefaultModel } = useDefaultModel(ModelTypeEnum.tts)
const { modelProviders: providers } = useProviderContext()
const { setShowModelModal } = useModalContext()
const setShowModelModal = useModalContextSelector(state => state.setShowModelModal)
const defaultModelNotConfigured = !textGenerationDefaultModel && !embeddingsDefaultModel && !speech2textDefaultModel && !rerankDefaultModel && !ttsDefaultModel
const [configedProviders, notConfigedProviders] = useMemo(() => {
const configedProviders: ModelProvider[] = []
@@ -57,32 +57,32 @@ const ModelProviderPage = () => {
const handleOpenModal = (
provider: ModelProvider,
configurateMethod: ConfigurateMethodEnum,
customConfigrationModelFixedFields?: CustomConfigrationModelFixedFields,
configurateMethod: ConfigurationMethodEnum,
CustomConfigurationModelFixedFields?: CustomConfigurationModelFixedFields,
) => {
setShowModelModal({
payload: {
currentProvider: provider,
currentConfigurateMethod: configurateMethod,
currentCustomConfigrationModelFixedFields: customConfigrationModelFixedFields,
currentConfigurationMethod: configurateMethod,
currentCustomConfigurationModelFixedFields: CustomConfigurationModelFixedFields,
},
onSaveCallback: () => {
updateModelProviders()
if (configurateMethod === ConfigurateMethodEnum.predefinedModel) {
if (configurateMethod === ConfigurationMethodEnum.predefinedModel) {
provider.supported_model_types.forEach((type) => {
updateModelList(type)
})
}
if (configurateMethod === ConfigurateMethodEnum.customizableModel && provider.custom_configuration.status === CustomConfigurationStatusEnum.active) {
if (configurateMethod === ConfigurationMethodEnum.customizableModel && provider.custom_configuration.status === CustomConfigurationStatusEnum.active) {
eventEmitter?.emit({
type: UPDATE_MODEL_PROVIDER_CUSTOM_MODEL_LIST,
payload: provider.provider,
} as any)
if (customConfigrationModelFixedFields?.__model_type)
updateModelList(customConfigrationModelFixedFields?.__model_type)
if (CustomConfigurationModelFixedFields?.__model_type)
updateModelList(CustomConfigurationModelFixedFields?.__model_type)
}
},
})
@@ -117,7 +117,7 @@ const ModelProviderPage = () => {
<ProviderAddedCard
key={provider.provider}
provider={provider}
onOpenModal={(configurateMethod: ConfigurateMethodEnum, currentCustomConfigrationModelFixedFields?: CustomConfigrationModelFixedFields) => handleOpenModal(provider, configurateMethod, currentCustomConfigrationModelFixedFields)}
onOpenModal={(configurateMethod: ConfigurationMethodEnum, currentCustomConfigurationModelFixedFields?: CustomConfigurationModelFixedFields) => handleOpenModal(provider, configurateMethod, currentCustomConfigurationModelFixedFields)}
/>
))
}
@@ -137,7 +137,7 @@ const ModelProviderPage = () => {
<ProviderCard
key={provider.provider}
provider={provider}
onOpenModal={(configurateMethod: ConfigurateMethodEnum) => handleOpenModal(provider, configurateMethod)}
onOpenModal={(configurateMethod: ConfigurationMethodEnum) => handleOpenModal(provider, configurateMethod)}
/>
))
}

View File

@@ -1,3 +1,4 @@
import classNames from 'classnames'
import type { FC, ReactNode } from 'react'
type ModelBadgeProps = {
@@ -9,11 +10,10 @@ const ModelBadge: FC<ModelBadgeProps> = ({
children,
}) => {
return (
<div className={`
flex items-center px-1 h-[18px] rounded-[5px] border border-black/[0.08] bg-white/[0.48]
text-[10px] font-medium text-gray-500
${className}
`}>
<div className={classNames(
'flex items-center px-1 h-[18px] rounded-[5px] border border-black/8 bg-white/[0.48] text-[10px] font-medium text-gray-500 cursor-default',
className,
)}>
{children}
</div>
)

View File

@@ -11,12 +11,14 @@ import type {
CredentialFormSchema,
CredentialFormSchemaRadio,
CredentialFormSchemaSelect,
CustomConfigrationModelFixedFields,
CustomConfigurationModelFixedFields,
FormValue,
ModelLoadBalancingConfig,
ModelLoadBalancingConfigEntry,
ModelProvider,
} from '../declarations'
import {
ConfigurateMethodEnum,
ConfigurationMethodEnum,
CustomConfigurationStatusEnum,
FormTypeEnum,
} from '../declarations'
@@ -28,11 +30,12 @@ import {
} from '../utils'
import {
useLanguage,
useProviderCrenditialsFormSchemasValue,
useProviderCredentialsAndLoadBalancing,
} from '../hooks'
import ProviderIcon from '../provider-icon'
import { useValidate } from '../../key-validator/hooks'
import { ValidatedStatus } from '../../key-validator/declarations'
import ModelLoadBalancingConfigs from '../provider-added-card/model-load-balancing-configs'
import Form from './Form'
import Button from '@/app/components/base/button'
import { Lock01 } from '@/app/components/base/icons/src/vender/solid/security'
@@ -47,8 +50,8 @@ import ConfirmCommon from '@/app/components/base/confirm/common'
type ModelModalProps = {
provider: ModelProvider
configurateMethod: ConfigurateMethodEnum
currentCustomConfigrationModelFixedFields?: CustomConfigrationModelFixedFields
configurateMethod: ConfigurationMethodEnum
currentCustomConfigurationModelFixedFields?: CustomConfigurationModelFixedFields
onCancel: () => void
onSave: () => void
}
@@ -56,16 +59,20 @@ type ModelModalProps = {
const ModelModal: FC<ModelModalProps> = ({
provider,
configurateMethod,
currentCustomConfigrationModelFixedFields,
currentCustomConfigurationModelFixedFields,
onCancel,
onSave,
}) => {
const providerFormSchemaPredefined = configurateMethod === ConfigurateMethodEnum.predefinedModel
const formSchemasValue = useProviderCrenditialsFormSchemasValue(
const providerFormSchemaPredefined = configurateMethod === ConfigurationMethodEnum.predefinedModel
const {
credentials: formSchemasValue,
loadBalancing: originalConfig,
mutate,
} = useProviderCredentialsAndLoadBalancing(
provider.provider,
configurateMethod,
providerFormSchemaPredefined && provider.custom_configuration.status === CustomConfigurationStatusEnum.active,
currentCustomConfigrationModelFixedFields,
currentCustomConfigurationModelFixedFields,
)
const isEditMode = !!formSchemasValue
const { t } = useTranslation()
@@ -73,13 +80,29 @@ const ModelModal: FC<ModelModalProps> = ({
const language = useLanguage()
const [loading, setLoading] = useState(false)
const [showConfirm, setShowConfirm] = useState(false)
const [draftConfig, setDraftConfig] = useState<ModelLoadBalancingConfig>()
const originalConfigMap = useMemo(() => {
if (!originalConfig)
return {}
return originalConfig?.configs.reduce((prev, config) => {
if (config.id)
prev[config.id] = config
return prev
}, {} as Record<string, ModelLoadBalancingConfigEntry>)
}, [originalConfig])
useEffect(() => {
if (originalConfig && !draftConfig)
setDraftConfig(originalConfig)
}, [draftConfig, originalConfig])
const formSchemas = useMemo(() => {
return providerFormSchemaPredefined
? provider.provider_credential_schema.credential_form_schemas
: [
genModelTypeFormSchema(provider.supported_model_types),
genModelNameFormSchema(provider.model_credential_schema?.model),
...provider.model_credential_schema.credential_form_schemas,
...(draftConfig?.enabled ? [] : provider.model_credential_schema.credential_form_schemas),
]
}, [
providerFormSchemaPredefined,
@@ -87,15 +110,14 @@ const ModelModal: FC<ModelModalProps> = ({
provider.supported_model_types,
provider.model_credential_schema?.credential_form_schemas,
provider.model_credential_schema?.model,
draftConfig?.enabled,
])
const [
requiredFormSchemas,
secretFormSchemas,
defaultFormSchemaValue,
showOnVariableMap,
] = useMemo(() => {
const requiredFormSchemas: CredentialFormSchema[] = []
const secretFormSchemas: CredentialFormSchema[] = []
const defaultFormSchemaValue: Record<string, string | number> = {}
const showOnVariableMap: Record<string, string[]> = {}
@@ -103,9 +125,6 @@ const ModelModal: FC<ModelModalProps> = ({
if (formSchema.required)
requiredFormSchemas.push(formSchema)
if (formSchema.type === FormTypeEnum.secretInput)
secretFormSchemas.push(formSchema)
if (formSchema.default)
defaultFormSchemaValue[formSchema.variable] = formSchema.default
@@ -136,22 +155,21 @@ const ModelModal: FC<ModelModalProps> = ({
return [
requiredFormSchemas,
secretFormSchemas,
defaultFormSchemaValue,
showOnVariableMap,
]
}, [formSchemas])
const initialFormSchemasValue = useMemo(() => {
const initialFormSchemasValue: Record<string, string | number> = useMemo(() => {
return {
...defaultFormSchemaValue,
...formSchemasValue,
}
} as unknown as Record<string, string | number>
}, [formSchemasValue, defaultFormSchemaValue])
const [value, setValue] = useState(initialFormSchemasValue)
useEffect(() => {
setValue(initialFormSchemasValue)
}, [initialFormSchemasValue])
const [validate, validating, validatedStatusState] = useValidate(value)
const [_, validating, validatedStatusState] = useValidate(value)
const filteredRequiredFormSchemas = requiredFormSchemas.filter((requiredFormSchema) => {
if (requiredFormSchema.show_on.length && requiredFormSchema.show_on.every(showOnItem => value[showOnItem.variable] === showOnItem.value))
return true
@@ -161,32 +179,63 @@ const ModelModal: FC<ModelModalProps> = ({
return false
})
const getSecretValues = useCallback((v: FormValue) => {
return secretFormSchemas.reduce((prev, next) => {
if (v[next.variable] === initialFormSchemasValue[next.variable])
prev[next.variable] = '[__HIDDEN__]'
return prev
}, {} as Record<string, string>)
}, [initialFormSchemasValue, secretFormSchemas])
const handleValueChange = (v: FormValue) => {
setValue(v)
}
const extendedSecretFormSchemas = useMemo(
() =>
(providerFormSchemaPredefined
? provider.provider_credential_schema.credential_form_schemas
: [
genModelTypeFormSchema(provider.supported_model_types),
genModelNameFormSchema(provider.model_credential_schema?.model),
...provider.model_credential_schema.credential_form_schemas,
]).filter(({ type }) => type === FormTypeEnum.secretInput),
[
provider.model_credential_schema?.credential_form_schemas,
provider.model_credential_schema?.model,
provider.provider_credential_schema?.credential_form_schemas,
provider.supported_model_types,
providerFormSchemaPredefined,
],
)
const encodeSecretValues = useCallback((v: FormValue) => {
const result = { ...v }
extendedSecretFormSchemas.forEach(({ variable }) => {
if (result[variable] === formSchemasValue?.[variable])
result[variable] = '[__HIDDEN__]'
})
return result
}, [extendedSecretFormSchemas, formSchemasValue])
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 saveCredentials(
providerFormSchemaPredefined,
provider.provider,
encodeSecretValues(value),
{
...value,
...getSecretValues(value),
...draftConfig,
enabled: Boolean(draftConfig?.enabled),
configs: draftConfig?.configs.map(encodeConfigEntrySecretValues) || [],
},
)
if (res.result === 'success') {
notify({ type: 'success', message: t('common.actionMsg.modifiedSuccessfully') })
mutate()
onSave()
onCancel()
}
@@ -207,6 +256,7 @@ const ModelModal: FC<ModelModalProps> = ({
)
if (res.result === 'success') {
notify({ type: 'success', message: t('common.actionMsg.modifiedSuccessfully') })
mutate()
onSave()
onCancel()
}
@@ -217,7 +267,7 @@ const ModelModal: FC<ModelModalProps> = ({
}
const renderTitlePrefix = () => {
const prefix = configurateMethod === ConfigurateMethodEnum.customizableModel ? t('common.operation.add') : t('common.operation.setup')
const prefix = configurateMethod === ConfigurationMethodEnum.customizableModel ? t('common.operation.add') : t('common.operation.setup')
return `${prefix} ${provider.label[language] || provider.label.en_US}`
}
@@ -232,6 +282,7 @@ const ModelModal: FC<ModelModalProps> = ({
<div className='text-xl font-semibold text-gray-900'>{renderTitlePrefix()}</div>
<ProviderIcon provider={provider} />
</div>
<Form
value={value}
onChange={handleValueChange}
@@ -241,7 +292,17 @@ const ModelModal: FC<ModelModalProps> = ({
showOnVariableMap={showOnVariableMap}
isEditMode={isEditMode}
/>
<div className='sticky bottom-0 flex justify-between items-center py-6 flex-wrap gap-y-2 bg-white'>
<div className='mt-1 mb-4 border-t-[0.5px] border-t-gray-100' />
<ModelLoadBalancingConfigs withSwitch {...{
draftConfig,
setDraftConfig,
provider,
currentCustomConfigurationModelFixedFields,
configurationMethod: configurateMethod,
}} />
<div className='sticky bottom-0 flex justify-between items-center mt-2 -mx-2 pt-4 px-2 pb-6 flex-wrap gap-y-2 bg-white z-10'>
{
(provider.help && (provider.help.title || provider.help.url))
? (
@@ -278,7 +339,11 @@ const ModelModal: FC<ModelModalProps> = ({
className='h-9 text-sm font-medium'
type='primary'
onClick={handleSave}
disabled={loading || filteredRequiredFormSchemas.some(item => value[item.variable] === undefined)}
disabled={
loading
|| filteredRequiredFormSchemas.some(item => value[item.variable] === undefined)
|| (draftConfig?.enabled && (draftConfig?.configs.filter(config => config.enabled).length ?? 0) < 2)
}
>
{t('common.operation.save')}
</Button>

View File

@@ -0,0 +1,344 @@
import type { FC } from 'react'
import {
memo,
useCallback,
useEffect,
useMemo,
useState,
} from 'react'
import { useTranslation } from 'react-i18next'
import type {
CredentialFormSchema,
CredentialFormSchemaRadio,
CredentialFormSchemaSelect,
CredentialFormSchemaTextInput,
CustomConfigurationModelFixedFields,
FormValue,
ModelLoadBalancingConfigEntry,
ModelProvider,
} from '../declarations'
import {
ConfigurationMethodEnum,
FormTypeEnum,
} from '../declarations'
import {
useLanguage,
} from '../hooks'
import { useValidate } from '../../key-validator/hooks'
import { ValidatedStatus } from '../../key-validator/declarations'
import { validateLoadBalancingCredentials } from '../utils'
import Form from './Form'
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 { AlertCircle } from '@/app/components/base/icons/src/vender/solid/alertsAndFeedback'
import {
PortalToFollowElem,
PortalToFollowElemContent,
} from '@/app/components/base/portal-to-follow-elem'
import { useToastContext } from '@/app/components/base/toast'
import ConfirmCommon from '@/app/components/base/confirm/common'
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 { credentials: formSchemasValue } = useProviderCredentialsAndLoadBalancing(
// provider.provider,
// configurationMethod,
// providerFormSchemaPredefined && provider.custom_configuration.status === CustomConfigurationStatusEnum.active,
// currentCustomConfigurationModelFixedFields,
// )
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 CredentialFormSchemaTextInput,
...(
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 [
requiredFormSchemas,
secretFormSchemas,
defaultFormSchemaValue,
showOnVariableMap,
] = useMemo(() => {
const requiredFormSchemas: CredentialFormSchema[] = []
const secretFormSchemas: CredentialFormSchema[] = []
const defaultFormSchemaValue: Record<string, string | number> = {}
const showOnVariableMap: Record<string, string[]> = {}
formSchemas.forEach((formSchema) => {
if (formSchema.required)
requiredFormSchemas.push(formSchema)
if (formSchema.type === FormTypeEnum.secretInput)
secretFormSchemas.push(formSchema)
if (formSchema.default)
defaultFormSchemaValue[formSchema.variable] = formSchema.default
if (formSchema.show_on.length) {
formSchema.show_on.forEach((showOnItem) => {
if (!showOnVariableMap[showOnItem.variable])
showOnVariableMap[showOnItem.variable] = []
if (!showOnVariableMap[showOnItem.variable].includes(formSchema.variable))
showOnVariableMap[showOnItem.variable].push(formSchema.variable)
})
}
if (formSchema.type === FormTypeEnum.select || formSchema.type === FormTypeEnum.radio) {
(formSchema as (CredentialFormSchemaRadio | CredentialFormSchemaSelect)).options.forEach((option) => {
if (option.show_on.length) {
option.show_on.forEach((showOnItem) => {
if (!showOnVariableMap[showOnItem.variable])
showOnVariableMap[showOnItem.variable] = []
if (!showOnVariableMap[showOnItem.variable].includes(formSchema.variable))
showOnVariableMap[showOnItem.variable].push(formSchema.variable)
})
}
})
}
})
return [
requiredFormSchemas,
secretFormSchemas,
defaultFormSchemaValue,
showOnVariableMap,
]
}, [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 initialFormSchemasValue: Record<string, string | number> = useMemo(() => {
return {
...defaultFormSchemaValue,
...formSchemasValue,
} as Record<string, string | number>
}, [formSchemasValue, defaultFormSchemaValue])
const [value, setValue] = useState(initialFormSchemasValue)
useEffect(() => {
setValue(initialFormSchemasValue)
}, [initialFormSchemasValue])
const [_, validating, validatedStatusState] = useValidate(value)
const filteredRequiredFormSchemas = requiredFormSchemas.filter((requiredFormSchema) => {
if (requiredFormSchema.show_on.length && requiredFormSchema.show_on.every(showOnItem => value[showOnItem.variable] === showOnItem.value))
return true
if (!requiredFormSchema.show_on.length)
return true
return false
})
const getSecretValues = useCallback((v: FormValue) => {
return secretFormSchemas.reduce((prev, next) => {
if (v[next.variable] === initialFormSchemasValue[next.variable])
prev[next.variable] = '[__HIDDEN__]'
return prev
}, {} as Record<string, string>)
}, [initialFormSchemasValue, secretFormSchemas])
// const handleValueChange = ({ __model_type, __model_name, ...v }: FormValue) => {
const handleValueChange = (v: FormValue) => {
setValue(v)
}
const handleSave = async () => {
try {
setLoading(true)
const res = await validateLoadBalancingCredentials(
providerFormSchemaPredefined,
provider.provider,
{
...value,
...getSecretValues(value),
},
)
if (res.status === ValidatedStatus.Success) {
// notify({ type: 'success', message: t('common.actionMsg.modifiedSuccessfully') })
const { __model_type, __model_name, name, ...credentials } = value
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='w-full h-full z-[60]'>
<div className='fixed inset-0 flex items-center justify-center bg-black/[.25]'>
<div className='mx-2 w-[640px] max-h-[calc(100vh-120px)] bg-white shadow-xl rounded-2xl overflow-y-auto'>
<div className='px-8 pt-8'>
<div className='flex justify-between items-center mb-2'>
<div className='text-xl font-semibold text-gray-900'>{t(isEditMode ? 'common.modelProvider.editConfig' : 'common.modelProvider.addConfig')}</div>
</div>
<Form
value={value}
onChange={handleValueChange}
formSchemas={formSchemas}
validating={validating}
validatedSuccess={validatedStatusState.status === ValidatedStatus.Success}
showOnVariableMap={showOnVariableMap}
isEditMode={isEditMode}
/>
<div className='sticky bottom-0 flex justify-between items-center py-6 flex-wrap gap-y-2 bg-white'>
{
(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 w-3 h-3' />
</a>
)
: <div />
}
<div>
{
isEditMode && (
<Button
className='mr-2 h-9 text-sm font-medium text-[#D92D20]'
onClick={() => setShowConfirm(true)}
>
{t('common.operation.remove')}
</Button>
)
}
<Button
className='mr-2 h-9 text-sm font-medium text-gray-700'
onClick={onCancel}
>
{t('common.operation.cancel')}
</Button>
<Button
className='h-9 text-sm font-medium'
type='primary'
onClick={handleSave}
disabled={loading || filteredRequiredFormSchemas.some(item => value[item.variable] === undefined)}
>
{t('common.operation.save')}
</Button>
</div>
</div>
</div>
<div className='border-t-[0.5px] border-t-black/5'>
{
(validatedStatusState.status === ValidatedStatus.Error && validatedStatusState.message)
? (
<div className='flex px-[10px] py-3 bg-[#FEF3F2] text-xs text-[#D92D20]'>
<AlertCircle className='mt-[1px] mr-2 w-[14px] h-[14px]' />
{validatedStatusState.message}
</div>
)
: (
<div className='flex justify-center items-center py-3 bg-gray-50 text-xs text-gray-500'>
<Lock01 className='mr-1 w-3 h-3 text-gray-500' />
{t('common.modelProvider.encrypted.front')}
<a
className='text-primary-600 mx-1'
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 && (
<ConfirmCommon
title={t('common.modelProvider.confirmDelete')}
isShow={showConfirm}
onCancel={() => setShowConfirm(false)}
onConfirm={handleRemove}
confirmWrapperClassName='z-[70]'
/>
)
}
</div>
</PortalToFollowElemContent>
</PortalToFollowElem>
)
}
export default memo(ModelLoadBalancingEntryModal)

View File

@@ -1,4 +1,5 @@
import type { FC } from 'react'
import type { FC, PropsWithChildren } from 'react'
import classNames from 'classnames'
import {
modelTypeFormat,
sizeFormat,
@@ -8,7 +9,7 @@ import type { ModelItem } from '../declarations'
import ModelBadge from '../model-badge'
import FeatureIcon from '../model-selector/feature-icon'
type ModelNameProps = {
type ModelNameProps = PropsWithChildren<{
modelItem: ModelItem
className?: string
showModelType?: boolean
@@ -18,7 +19,7 @@ type ModelNameProps = {
showFeatures?: boolean
featuresClassName?: string
showContextSize?: boolean
}
}>
const ModelName: FC<ModelNameProps> = ({
modelItem,
className,
@@ -29,6 +30,7 @@ const ModelName: FC<ModelNameProps> = ({
showFeatures,
featuresClassName,
showContextSize,
children,
}) => {
const language = useLanguage()
@@ -42,21 +44,21 @@ const ModelName: FC<ModelNameProps> = ({
`}
>
<div
className='mr-1 truncate'
className='truncate'
title={modelItem.label[language] || modelItem.label.en_US}
>
{modelItem.label[language] || modelItem.label.en_US}
</div>
{
showModelType && modelItem.model_type && (
<ModelBadge className={`mr-0.5 ${modelTypeClassName}`}>
<ModelBadge className={classNames('ml-1', modelTypeClassName)}>
{modelTypeFormat(modelItem.model_type)}
</ModelBadge>
)
}
{
modelItem.model_properties.mode && showMode && (
<ModelBadge className={`mr-0.5 ${modeClassName}`}>
<ModelBadge className={classNames('ml-1', modeClassName)}>
{(modelItem.model_properties.mode as string).toLocaleUpperCase()}
</ModelBadge>
)
@@ -72,11 +74,12 @@ const ModelName: FC<ModelNameProps> = ({
}
{
showContextSize && modelItem.model_properties.context_size && (
<ModelBadge>
<ModelBadge className='ml-1'>
{sizeFormat(modelItem.model_properties.context_size as number)}
</ModelBadge>
)
}
{children}
</div>
)
}

View File

@@ -86,7 +86,7 @@ const ModelParameterModal: FC<ModelParameterModalProps> = ({
isInWorkflow,
}) => {
const { t } = useTranslation()
const { hasSettedApiKey } = useProviderContext()
const { isAPIKeySet } = useProviderContext()
const [open, setOpen] = useState(false)
const { data: parameterRulesData, isLoading } = useSWR((provider && modelId) ? `/workspaces/current/model-providers/${provider}/models/parameter-rules?model=${modelId}` : null, fetchModelParameterRules)
const {
@@ -99,7 +99,7 @@ const ModelParameterModal: FC<ModelParameterModalProps> = ({
const hasDeprecated = !currentProvider || !currentModel
const modelDisabled = currentModel?.status !== ModelStatusEnum.active
const disabled = !hasSettedApiKey || hasDeprecated || modelDisabled
const disabled = !isAPIKeySet || hasDeprecated || modelDisabled
const parameterRules: ModelParameterRule[] = useMemo(() => {
return parameterRulesData?.data || []

View File

@@ -13,7 +13,7 @@ import {
import ModelIcon from '../model-icon'
import ModelName from '../model-name'
import {
ConfigurateMethodEnum,
ConfigurationMethodEnum,
MODEL_STATUS_TEXT,
ModelStatusEnum,
} from '../declarations'
@@ -49,7 +49,7 @@ const PopupItem: FC<PopupItemProps> = ({
setShowModelModal({
payload: {
currentProvider,
currentConfigurateMethod: ConfigurateMethodEnum.predefinedModel,
currentConfigurationMethod: ConfigurationMethodEnum.predefinedModel,
},
onSaveCallback: () => {
updateModelProviders()

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,

View File

@@ -3,7 +3,7 @@ import { useTranslation } from 'react-i18next'
import type {
ModelProvider,
} from '../declarations'
import { ConfigurateMethodEnum } from '../declarations'
import { ConfigurationMethodEnum } from '../declarations'
import {
DEFAULT_BACKGROUND_COLOR,
modelTypeFormat,
@@ -19,7 +19,7 @@ import Button from '@/app/components/base/button'
type ProviderCardProps = {
provider: ModelProvider
onOpenModal: (configurateMethod: ConfigurateMethodEnum) => void
onOpenModal: (configurateMethod: ConfigurationMethodEnum) => void
}
const ProviderCard: FC<ProviderCardProps> = ({
@@ -28,8 +28,7 @@ const ProviderCard: FC<ProviderCardProps> = ({
}) => {
const { t } = useTranslation()
const language = useLanguage()
const configurateMethods = provider.configurate_methods.filter(method => method !== ConfigurateMethodEnum.fetchFromRemote)
const configurateMethods = provider.configurate_methods.filter(method => method !== ConfigurationMethodEnum.fetchFromRemote)
return (
<div
@@ -59,7 +58,7 @@ const ProviderCard: FC<ProviderCardProps> = ({
<div className={`hidden group-hover:grid grid-cols-${configurateMethods.length} gap-1`}>
{
configurateMethods.map((method) => {
if (method === ConfigurateMethodEnum.predefinedModel) {
if (method === ConfigurationMethodEnum.predefinedModel) {
return (
<Button
key={method}

View File

@@ -3,8 +3,10 @@ import type {
CredentialFormSchemaRadio,
CredentialFormSchemaTextInput,
FormValue,
ModelLoadBalancingConfig,
} from './declarations'
import {
ConfigurationMethodEnum,
FormTypeEnum,
MODEL_TYPE_TEXT,
ModelTypeEnum,
@@ -12,6 +14,7 @@ import {
import {
deleteModelProvider,
setModelProvider,
validateModelLoadBalancingCredentials,
validateModelProvider,
} from '@/service/common'
@@ -53,12 +56,38 @@ export const validateCredentials = async (predefined: boolean, provider: string,
}
}
export const saveCredentials = async (predefined: boolean, provider: string, v: FormValue) => {
export const validateLoadBalancingCredentials = async (predefined: boolean, provider: string, v: FormValue): Promise<{
status: ValidatedStatus
message?: string
}> => {
const { __model_name, __model_type, ...credentials } = v
try {
const res = await validateModelLoadBalancingCredentials({
url: `/workspaces/current/model-providers/${provider}/models/load-balancing-configs/credentials-validate`,
body: {
model: __model_name,
model_type: __model_type,
credentials,
},
})
if (res.result === 'success')
return Promise.resolve({ status: ValidatedStatus.Success })
else
return Promise.resolve({ status: ValidatedStatus.Error, message: res.error || 'error' })
}
catch (e: any) {
return Promise.resolve({ status: ValidatedStatus.Error, message: e.message })
}
}
export const saveCredentials = async (predefined: boolean, provider: string, v: FormValue, loadBalancing?: ModelLoadBalancingConfig) => {
let body, url
if (predefined) {
body = {
config_from: ConfigurationMethodEnum.predefinedModel,
credentials: v,
load_balancing: loadBalancing,
}
url = `/workspaces/current/model-providers/${provider}`
}
@@ -68,6 +97,7 @@ export const saveCredentials = async (predefined: boolean, provider: string, v:
model: __model_name,
model_type: __model_type,
credentials,
load_balancing: loadBalancing,
}
url = `/workspaces/current/model-providers/${provider}/models`
}
@@ -75,6 +105,20 @@ export const saveCredentials = async (predefined: boolean, provider: string, v:
return setModelProvider({ url, body })
}
export const savePredefinedLoadBalancingConfig = async (provider: string, v: FormValue, loadBalancing?: ModelLoadBalancingConfig) => {
const { __model_name, __model_type, ...credentials } = v
const body = {
config_from: ConfigurationMethodEnum.predefinedModel,
model: __model_name,
model_type: __model_type,
credentials,
load_balancing: loadBalancing,
}
const url = `/workspaces/current/model-providers/${provider}/models`
return setModelProvider({ url, body })
}
export const removeCredentials = async (predefined: boolean, provider: string, v: FormValue) => {
let url = ''
let body