mirror of
http://112.124.100.131/huang.ze/ebiz-dify-ai.git
synced 2025-12-09 02:46:52 +08:00
feat: model load balancing (#4926)
This commit is contained in:
@@ -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[]
|
||||
}
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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)}
|
||||
/>
|
||||
))
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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)
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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 || []
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user