From 37f292ea9157dcf7e173755c63178a4727fd3c22 Mon Sep 17 00:00:00 2001 From: Nite Knite Date: Wed, 5 Jun 2024 00:13:29 +0800 Subject: [PATCH] feat: model load balancing (#4926) --- .../config/assistant-type-picker/index.tsx | 2 +- .../app/configuration/debug/index.tsx | 6 +- .../components/app/configuration/index.tsx | 8 +- .../app/overview/apikey-info-panel/index.tsx | 4 +- web/app/components/app/overview/appCard.tsx | 8 +- web/app/components/base/button/index.css | 6 +- web/app/components/base/button/index.tsx | 22 +- .../chat/chat/answer/workflow-process.tsx | 2 +- .../line/financeAndECommerce/balance.svg | 3 + .../line/financeAndECommerce/Balance.json | 29 ++ .../line/financeAndECommerce/Balance.tsx | 16 + .../vender/line/financeAndECommerce/index.ts | 1 + .../base/image-uploader/image-list.tsx | 5 +- .../base/image-uploader/image-preview.tsx | 2 +- web/app/components/base/modal/index.css | 7 + web/app/components/base/modal/index.tsx | 15 +- .../base/simple-pie-chart/index.module.css | 4 + .../base/simple-pie-chart/index.tsx | 66 ++++ web/app/components/base/switch/index.tsx | 8 +- web/app/components/billing/type.ts | 1 + .../custom/custom-app-header-brand/index.tsx | 6 +- .../custom/custom-web-app-brand/index.tsx | 6 +- web/app/components/datasets/create/index.tsx | 2 +- .../datasets/create/step-two/index.tsx | 16 +- .../documents/detail/settings/index.tsx | 2 +- .../model-provider-page/declarations.ts | 30 +- .../model-provider-page/hooks.ts | 45 ++- .../model-provider-page/index.tsx | 28 +- .../model-provider-page/model-badge/index.tsx | 10 +- .../model-provider-page/model-modal/index.tsx | 131 +++++-- .../model-load-balancing-entry-modal.tsx | 344 ++++++++++++++++++ .../model-provider-page/model-name/index.tsx | 17 +- .../model-parameter-modal/index.tsx | 4 +- .../model-selector/popup-item.tsx | 4 +- .../provider-added-card/cooldown-timer.tsx | 64 ++++ .../provider-added-card/credential-panel.tsx | 4 +- .../provider-added-card/index.tsx | 19 +- .../provider-added-card/model-list-item.tsx | 119 ++++++ .../provider-added-card/model-list.tsx | 90 ++--- .../model-load-balancing-configs.tsx | 269 ++++++++++++++ .../model-load-balancing-modal.tsx | 190 ++++++++++ .../provider-added-card/priority-selector.tsx | 2 +- .../provider-card/index.tsx | 9 +- .../model-provider-page/utils.ts | 46 ++- web/app/components/tools/tool-list/index.tsx | 220 +++++++++++ web/app/components/workflow/block-icon.tsx | 4 +- .../components/workflow/header/checklist.tsx | 4 +- .../components/workflow/operator/index.tsx | 2 +- .../workflow/panel/chat-record/index.tsx | 2 +- .../panel/debug-and-preview/index.tsx | 4 +- web/app/styles/globals.css | 3 +- web/context/debug-configuration.ts | 6 +- web/context/modal-context.tsx | 103 ++++-- web/context/provider-context.tsx | 71 ++-- web/i18n/en-US/common.ts | 16 + web/i18n/zh-Hans/common.ts | 15 + web/service/common.ts | 30 +- web/tailwind.config.js | 48 ++- 58 files changed, 1896 insertions(+), 304 deletions(-) create mode 100644 web/app/components/base/icons/assets/vender/line/financeAndECommerce/balance.svg create mode 100644 web/app/components/base/icons/src/vender/line/financeAndECommerce/Balance.json create mode 100644 web/app/components/base/icons/src/vender/line/financeAndECommerce/Balance.tsx create mode 100644 web/app/components/base/modal/index.css create mode 100644 web/app/components/base/simple-pie-chart/index.module.css create mode 100644 web/app/components/base/simple-pie-chart/index.tsx create mode 100644 web/app/components/header/account-setting/model-provider-page/model-modal/model-load-balancing-entry-modal.tsx create mode 100644 web/app/components/header/account-setting/model-provider-page/provider-added-card/cooldown-timer.tsx create mode 100644 web/app/components/header/account-setting/model-provider-page/provider-added-card/model-list-item.tsx create mode 100644 web/app/components/header/account-setting/model-provider-page/provider-added-card/model-load-balancing-configs.tsx create mode 100644 web/app/components/header/account-setting/model-provider-page/provider-added-card/model-load-balancing-modal.tsx create mode 100644 web/app/components/tools/tool-list/index.tsx diff --git a/web/app/components/app/configuration/config/assistant-type-picker/index.tsx b/web/app/components/app/configuration/config/assistant-type-picker/index.tsx index e19e7fffa3..3bdf0cbcf7 100644 --- a/web/app/components/app/configuration/config/assistant-type-picker/index.tsx +++ b/web/app/components/app/configuration/config/assistant-type-picker/index.tsx @@ -123,7 +123,7 @@ const AssistantTypePicker: FC = ({ -
+
{t('appDebug.assistantType.name')}
void inputs: Inputs modelParameterParams: Pick @@ -51,7 +51,7 @@ type IDebug = { } const Debug: FC = ({ - hasSetAPIKEY = true, + isAPIKeySet = true, onSetting, inputs, modelParameterParams, @@ -503,7 +503,7 @@ const Debug: FC = ({ onCancel={handleCancel} /> )} - {!hasSetAPIKEY && ()} + {!isAPIKeySet && ()} ) } diff --git a/web/app/components/app/configuration/index.tsx b/web/app/components/app/configuration/index.tsx index 259f6b2deb..3b61a0b7fe 100644 --- a/web/app/components/app/configuration/index.tsx +++ b/web/app/components/app/configuration/index.tsx @@ -255,7 +255,7 @@ const Configuration: FC = () => { }) } - const { hasSettedApiKey } = useProviderContext() + const { isAPIKeySet } = useProviderContext() const { currentModel: currModel, textGenerationModelList, @@ -678,7 +678,7 @@ const Configuration: FC = () => { return ( { {!isMobile &&
setShowAccountSettingModal({ payload: 'provider' })} inputs={inputs} modelParameterParams={{ @@ -881,7 +881,7 @@ const Configuration: FC = () => { {isMobile && ( setShowAccountSettingModal({ payload: 'provider' })} inputs={inputs} modelParameterParams={{ diff --git a/web/app/components/app/overview/apikey-info-panel/index.tsx b/web/app/components/app/overview/apikey-info-panel/index.tsx index 555be997a1..b91bfd5aaf 100644 --- a/web/app/components/app/overview/apikey-info-panel/index.tsx +++ b/web/app/components/app/overview/apikey-info-panel/index.tsx @@ -12,14 +12,14 @@ import { useModalContext } from '@/context/modal-context' const APIKeyInfoPanel: FC = () => { const isCloud = !IS_CE_EDITION - const { hasSettedApiKey } = useProviderContext() + const { isAPIKeySet } = useProviderContext() const { setShowAccountSettingModal } = useModalContext() const { t } = useTranslation() const [isShow, setIsShow] = useState(true) - if (hasSettedApiKey) + if (isAPIKeySet) return null if (!(isShow)) diff --git a/web/app/components/app/overview/appCard.tsx b/web/app/components/app/overview/appCard.tsx index 7bf13863cf..2426bab942 100644 --- a/web/app/components/app/overview/appCard.tsx +++ b/web/app/components/app/overview/appCard.tsx @@ -132,8 +132,7 @@ function AppCard({ return (
@@ -165,7 +164,7 @@ function AppCard({ ? t('appOverview.overview.appInfo.accessibleAddress') : t('appOverview.overview.apiInfo.accessibleAddress')}
-
+
{isApp ? appUrl : apiUrl} @@ -203,8 +202,7 @@ function AppCard({ onClick={() => setShowConfirmDelete(true)} >
diff --git a/web/app/components/base/button/index.css b/web/app/components/base/button/index.css index 81fb5fda45..94cd71817e 100644 --- a/web/app/components/base/button/index.css +++ b/web/app/components/base/button/index.css @@ -3,10 +3,10 @@ @layer components { .btn { @apply inline-flex justify-center items-center content-center h-9 leading-5 rounded-lg px-4 py-2 text-base cursor-pointer whitespace-nowrap; - } + }; .btn-default { - @apply border-solid border border-gray-200 cursor-pointer text-gray-500 hover:bg-white hover:shadow-sm hover:border-gray-300; + @apply border-solid border border-gray-200 cursor-pointer text-gray-700 hover:bg-white hover:shadow-sm hover:border-gray-300; } .btn-default-disabled { @@ -28,4 +28,4 @@ .btn-warning-disabled { @apply bg-red-600/75 cursor-not-allowed text-white; } -} \ No newline at end of file +} diff --git a/web/app/components/base/button/index.tsx b/web/app/components/base/button/index.tsx index e617a5d12d..34f16ad556 100644 --- a/web/app/components/base/button/index.tsx +++ b/web/app/components/base/button/index.tsx @@ -1,16 +1,16 @@ -import type { FC, MouseEventHandler } from 'react' -import React from 'react' +import type { FC, MouseEventHandler, PropsWithChildren } from 'react' +import React, { memo } from 'react' +import classNames from 'classnames' import Spinner from '../spinner' -export type IButtonProps = { +export type IButtonProps = PropsWithChildren<{ type?: string className?: string disabled?: boolean loading?: boolean tabIndex?: number - children: React.ReactNode onClick?: MouseEventHandler -} +}> const Button: FC = ({ type, @@ -21,22 +21,22 @@ const Button: FC = ({ loading = false, tabIndex, }) => { - let style = 'cursor-pointer' + let typeClassNames = 'cursor-pointer' switch (type) { case 'primary': - style = (disabled || loading) ? 'btn-primary-disabled' : 'btn-primary' + typeClassNames = (disabled || loading) ? 'btn-primary-disabled' : 'btn-primary' break case 'warning': - style = (disabled || loading) ? 'btn-warning-disabled' : 'btn-warning' + typeClassNames = (disabled || loading) ? 'btn-warning-disabled' : 'btn-warning' break default: - style = disabled ? 'btn-default-disabled' : 'btn-default' + typeClassNames = disabled ? 'btn-default-disabled' : 'btn-default' break } return (
@@ -47,4 +47,4 @@ const Button: FC = ({ ) } -export default React.memo(Button) +export default memo(Button) diff --git a/web/app/components/base/chat/chat/answer/workflow-process.tsx b/web/app/components/base/chat/chat/answer/workflow-process.tsx index d47db40437..274af8f95b 100644 --- a/web/app/components/base/chat/chat/answer/workflow-process.tsx +++ b/web/app/components/base/chat/chat/answer/workflow-process.tsx @@ -65,7 +65,7 @@ const WorkflowProcessItem = ({ return (
+ + diff --git a/web/app/components/base/icons/src/vender/line/financeAndECommerce/Balance.json b/web/app/components/base/icons/src/vender/line/financeAndECommerce/Balance.json new file mode 100644 index 0000000000..c04fcda517 --- /dev/null +++ b/web/app/components/base/icons/src/vender/line/financeAndECommerce/Balance.json @@ -0,0 +1,29 @@ +{ + "icon": { + "type": "element", + "isRootNode": true, + "name": "svg", + "attributes": { + "width": "24", + "height": "24", + "viewBox": "0 0 24 24", + "fill": "none", + "xmlns": "http://www.w3.org/2000/svg" + }, + "children": [ + { + "type": "element", + "name": "path", + "attributes": { + "d": "M12 3V20M12 20H6.99999M12 20H17M2.99999 6H7.52785C7.83834 6 8.14457 5.92771 8.42228 5.78885L9.5777 5.21115C9.85541 5.07229 10.1616 5 10.4721 5H13.5279C13.8384 5 14.1446 5.07229 14.4223 5.21115L15.5777 5.78885C15.8554 5.92771 16.1616 6 16.4721 6H21M5.49999 6L3.02043 13.4387C2.71807 14.3458 3.08918 15.3834 4.0053 15.657C5.0117 15.9577 5.98828 15.9577 6.99468 15.657C7.9108 15.3834 8.28191 14.3457 7.97955 13.4387L5.49999 6ZM18.5 6L16.0204 13.4387C15.7181 14.3458 16.0892 15.3834 17.0053 15.657C18.0117 15.9577 18.9883 15.9577 19.9947 15.657C20.9108 15.3834 21.2819 14.3457 20.9796 13.4387L18.5 6Z", + "stroke": "currentColor", + "stroke-width": "2", + "stroke-linecap": "round", + "stroke-linejoin": "round" + }, + "children": [] + } + ] + }, + "name": "Balance" +} \ No newline at end of file diff --git a/web/app/components/base/icons/src/vender/line/financeAndECommerce/Balance.tsx b/web/app/components/base/icons/src/vender/line/financeAndECommerce/Balance.tsx new file mode 100644 index 0000000000..7743f0bd6a --- /dev/null +++ b/web/app/components/base/icons/src/vender/line/financeAndECommerce/Balance.tsx @@ -0,0 +1,16 @@ +// GENERATE BY script +// DON NOT EDIT IT MANUALLY + +import * as React from 'react' +import data from './Balance.json' +import IconBase from '@/app/components/base/icons/IconBase' +import type { IconBaseProps, IconData } from '@/app/components/base/icons/IconBase' + +const Icon = React.forwardRef, Omit>(( + props, + ref, +) => ) + +Icon.displayName = 'Balance' + +export default Icon diff --git a/web/app/components/base/icons/src/vender/line/financeAndECommerce/index.ts b/web/app/components/base/icons/src/vender/line/financeAndECommerce/index.ts index 65c6ef70f3..2223daa1d5 100644 --- a/web/app/components/base/icons/src/vender/line/financeAndECommerce/index.ts +++ b/web/app/components/base/icons/src/vender/line/financeAndECommerce/index.ts @@ -1,3 +1,4 @@ +export { default as Balance } from './Balance' export { default as CoinsStacked01 } from './CoinsStacked01' export { default as GoldCoin } from './GoldCoin' export { default as ReceiptList } from './ReceiptList' diff --git a/web/app/components/base/image-uploader/image-list.tsx b/web/app/components/base/image-uploader/image-list.tsx index 6573815950..fe3227e53a 100644 --- a/web/app/components/base/image-uploader/image-list.tsx +++ b/web/app/components/base/image-uploader/image-list.tsx @@ -77,8 +77,7 @@ const ImageList: FC = ({
= ({ type="button" className={cn( 'absolute z-10 -top-[9px] -right-[9px] items-center justify-center w-[18px] h-[18px]', - 'bg-white hover:bg-gray-50 border-[0.5px] border-black/[0.02] rounded-2xl shadow-lg', + 'bg-white hover:bg-gray-50 border-[0.5px] border-black/2 rounded-2xl shadow-lg', item.progress === -1 ? 'flex' : 'hidden group-hover:flex', )} onClick={() => onRemove && onRemove(item._id)} diff --git a/web/app/components/base/image-uploader/image-preview.tsx b/web/app/components/base/image-uploader/image-preview.tsx index 8670e10354..4fd071fd73 100644 --- a/web/app/components/base/image-uploader/image-preview.tsx +++ b/web/app/components/base/image-uploader/image-preview.tsx @@ -18,7 +18,7 @@ const ImagePreview: FC = ({ className='max-w-full max-h-full' />
diff --git a/web/app/components/base/modal/index.css b/web/app/components/base/modal/index.css new file mode 100644 index 0000000000..b1b4648be6 --- /dev/null +++ b/web/app/components/base/modal/index.css @@ -0,0 +1,7 @@ +.modal-dialog { + @apply relative z-10; +} + +.modal-panel { + @apply w-full max-w-md transform rounded-2xl bg-white p-6 text-left align-middle shadow-xl transition-all; +} diff --git a/web/app/components/base/modal/index.tsx b/web/app/components/base/modal/index.tsx index 0f7bf12b65..4e1f184dab 100644 --- a/web/app/components/base/modal/index.tsx +++ b/web/app/components/base/modal/index.tsx @@ -1,16 +1,17 @@ import { Dialog, Transition } from '@headlessui/react' import { Fragment } from 'react' import { XMarkIcon } from '@heroicons/react/24/outline' +import classNames from 'classnames' // https://headlessui.com/react/dialog type IModal = { className?: string wrapperClassName?: string isShow: boolean - onClose: () => void + onClose?: () => void title?: React.ReactNode description?: React.ReactNode - children: React.ReactNode + children?: React.ReactNode closable?: boolean overflowVisible?: boolean } @@ -19,7 +20,7 @@ export default function Modal({ className, wrapperClassName, isShow, - onClose, + onClose = () => { }, title, description, children, @@ -28,7 +29,7 @@ export default function Modal({ }: IModal) { return ( - + - + {title && { + const option: EChartsOption = useMemo(() => ({ + series: [ + { + type: 'pie', + radius: ['83%', '100%'], + animation: false, + data: [ + { value: 100, itemStyle: { color: stroke } }, + ], + emphasis: { + disabled: true, + }, + labelLine: { + show: false, + }, + cursor: 'default', + }, + { + type: 'pie', + radius: '83%', + animationDuration: 600, + data: [ + { value: percentage, itemStyle: { color: fill } }, + { value: 100 - percentage, itemStyle: { color: '#fff' } }, + ], + emphasis: { + disabled: true, + }, + labelLine: { + show: false, + }, + cursor: 'default', + }, + ], + }), [stroke, fill, percentage]) + + return ( + + ) +} + +export default memo(SimplePieChart) diff --git a/web/app/components/base/switch/index.tsx b/web/app/components/base/switch/index.tsx index 6794e51efd..7d13b0cb9a 100644 --- a/web/app/components/base/switch/index.tsx +++ b/web/app/components/base/switch/index.tsx @@ -4,13 +4,14 @@ import classNames from 'classnames' import { Switch as OriginalSwitch } from '@headlessui/react' type SwitchProps = { - onChange: (value: boolean) => void + onChange?: (value: boolean) => void size?: 'sm' | 'md' | 'lg' | 'l' defaultValue?: boolean disabled?: boolean + className?: string } -const Switch = ({ onChange, size = 'lg', defaultValue = false, disabled = false }: SwitchProps) => { +const Switch = ({ onChange, size = 'lg', defaultValue = false, disabled = false, className }: SwitchProps) => { const [enabled, setEnabled] = useState(defaultValue) useEffect(() => { setEnabled(defaultValue) @@ -42,13 +43,14 @@ const Switch = ({ onChange, size = 'lg', defaultValue = false, disabled = false if (disabled) return setEnabled(checked) - onChange(checked) + onChange?.(checked) }} className={classNames( wrapStyle[size], enabled ? 'bg-blue-600' : 'bg-gray-200', 'relative inline-flex flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out', disabled ? '!opacity-50 !cursor-not-allowed' : '', + className, )} > { return (
{t('custom.app.title')}
-
+
@@ -43,7 +43,7 @@ const CustomAppHeaderBrand = () => {
- {!hasSetAPIKEY && ( + {!isAPIKeySet && (
{t('datasetCreation.stepTwo.warning')}  {t('datasetCreation.stepTwo.click')} diff --git a/web/app/components/datasets/documents/detail/settings/index.tsx b/web/app/components/datasets/documents/detail/settings/index.tsx index 891e284c95..cab0c5d400 100644 --- a/web/app/components/datasets/documents/detail/settings/index.tsx +++ b/web/app/components/datasets/documents/detail/settings/index.tsx @@ -68,7 +68,7 @@ const DocumentSettings = ({ datasetId, documentId }: DocumentSettingsProps) => { {!documentDetail && } {dataset && documentDetail && ( + 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 + /** 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[] +} diff --git a/web/app/components/header/account-setting/model-provider-page/hooks.ts b/web/app/components/header/account-setting/model-provider-page/hooks.ts index d4ffe8f09b..54396cc538 100644 --- a/web/app/components/header/account-setting/model-provider-page/hooks.ts +++ b/web/app/components/header/account-setting/model-provider-page/hooks.ts @@ -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 | undefined, ModelLoadBalancingConfig | undefined]) } export const useModelList = (type: ModelTypeEnum) => { diff --git a/web/app/components/header/account-setting/model-provider-page/index.tsx b/web/app/components/header/account-setting/model-provider-page/index.tsx index 1fff60db88..656c7b0239 100644 --- a/web/app/components/header/account-setting/model-provider-page/index.tsx +++ b/web/app/components/header/account-setting/model-provider-page/index.tsx @@ -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 = () => { handleOpenModal(provider, configurateMethod, currentCustomConfigrationModelFixedFields)} + onOpenModal={(configurateMethod: ConfigurationMethodEnum, currentCustomConfigurationModelFixedFields?: CustomConfigurationModelFixedFields) => handleOpenModal(provider, configurateMethod, currentCustomConfigurationModelFixedFields)} /> )) } @@ -137,7 +137,7 @@ const ModelProviderPage = () => { handleOpenModal(provider, configurateMethod)} + onOpenModal={(configurateMethod: ConfigurationMethodEnum) => handleOpenModal(provider, configurateMethod)} /> )) } diff --git a/web/app/components/header/account-setting/model-provider-page/model-badge/index.tsx b/web/app/components/header/account-setting/model-provider-page/model-badge/index.tsx index e0ea74abe0..28c544d1b7 100644 --- a/web/app/components/header/account-setting/model-provider-page/model-badge/index.tsx +++ b/web/app/components/header/account-setting/model-provider-page/model-badge/index.tsx @@ -1,3 +1,4 @@ +import classNames from 'classnames' import type { FC, ReactNode } from 'react' type ModelBadgeProps = { @@ -9,11 +10,10 @@ const ModelBadge: FC = ({ children, }) => { return ( -
+
{children}
) diff --git a/web/app/components/header/account-setting/model-provider-page/model-modal/index.tsx b/web/app/components/header/account-setting/model-provider-page/model-modal/index.tsx index 25d8af0ac1..2b3944d77f 100644 --- a/web/app/components/header/account-setting/model-provider-page/model-modal/index.tsx +++ b/web/app/components/header/account-setting/model-provider-page/model-modal/index.tsx @@ -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 = ({ 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 = ({ const language = useLanguage() const [loading, setLoading] = useState(false) const [showConfirm, setShowConfirm] = useState(false) + + const [draftConfig, setDraftConfig] = useState() + const originalConfigMap = useMemo(() => { + if (!originalConfig) + return {} + return originalConfig?.configs.reduce((prev, config) => { + if (config.id) + prev[config.id] = config + return prev + }, {} as Record) + }, [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 = ({ 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 = {} const showOnVariableMap: Record = {} @@ -103,9 +125,6 @@ const ModelModal: FC = ({ 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 = ({ return [ requiredFormSchemas, - secretFormSchemas, defaultFormSchemaValue, showOnVariableMap, ] }, [formSchemas]) - const initialFormSchemasValue = useMemo(() => { + const initialFormSchemasValue: Record = useMemo(() => { return { ...defaultFormSchemaValue, ...formSchemasValue, - } + } as unknown as Record }, [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 = ({ 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) - }, [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 = ({ ) if (res.result === 'success') { notify({ type: 'success', message: t('common.actionMsg.modifiedSuccessfully') }) + mutate() onSave() onCancel() } @@ -217,7 +267,7 @@ const ModelModal: FC = ({ } 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 = ({
{renderTitlePrefix()}
+
= ({ showOnVariableMap={showOnVariableMap} isEditMode={isEditMode} /> -
+ +
+ + +
{ (provider.help && (provider.help.title || provider.help.url)) ? ( @@ -278,7 +339,11 @@ const ModelModal: FC = ({ 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')} diff --git a/web/app/components/header/account-setting/model-provider-page/model-modal/model-load-balancing-entry-modal.tsx b/web/app/components/header/account-setting/model-provider-page/model-modal/model-load-balancing-entry-modal.tsx new file mode 100644 index 0000000000..a2c2712653 --- /dev/null +++ b/web/app/components/header/account-setting/model-provider-page/model-modal/model-load-balancing-entry-modal.tsx @@ -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 = ({ + 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 = {} + const showOnVariableMap: Record = {} + + 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() + useEffect(() => { + if (entry && !initialValue) { + setInitialValue({ + ...defaultFormSchemaValue, + ...entry.credentials, + id: entry.id, + name: entry.name, + } as Record) + } + }, [entry, defaultFormSchemaValue, initialValue]) + const formSchemasValue = useMemo(() => ({ + ...currentCustomConfigurationModelFixedFields, + ...initialValue, + }), [currentCustomConfigurationModelFixedFields, initialValue]) + const initialFormSchemasValue: Record = useMemo(() => { + return { + ...defaultFormSchemaValue, + ...formSchemasValue, + } as Record + }, [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) + }, [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, + }) + // onCancel() + } + else { + notify({ type: 'error', message: res.message || '' }) + } + } + finally { + setLoading(false) + } + } + + const handleRemove = () => { + onRemove?.() + } + + return ( + + +
+
+
+
+
{t(isEditMode ? 'common.modelProvider.editConfig' : 'common.modelProvider.addConfig')}
+
+ +
+ { + (provider.help && (provider.help.title || provider.help.url)) + ? ( + !provider.help.url && e.preventDefault()} + > + {provider.help.title?.[language] || provider.help.url[language] || provider.help.title?.en_US || provider.help.url.en_US} + + + ) + :
+ } +
+ { + isEditMode && ( + + ) + } + + +
+
+
+
+ { + (validatedStatusState.status === ValidatedStatus.Error && validatedStatusState.message) + ? ( +
+ + {validatedStatusState.message} +
+ ) + : ( +
+ + {t('common.modelProvider.encrypted.front')} + + PKCS1_OAEP + + {t('common.modelProvider.encrypted.back')} +
+ ) + } +
+
+ { + showConfirm && ( + setShowConfirm(false)} + onConfirm={handleRemove} + confirmWrapperClassName='z-[70]' + /> + ) + } +
+ + + ) +} + +export default memo(ModelLoadBalancingEntryModal) diff --git a/web/app/components/header/account-setting/model-provider-page/model-name/index.tsx b/web/app/components/header/account-setting/model-provider-page/model-name/index.tsx index d828c703db..e4337e96c8 100644 --- a/web/app/components/header/account-setting/model-provider-page/model-name/index.tsx +++ b/web/app/components/header/account-setting/model-provider-page/model-name/index.tsx @@ -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 = ({ modelItem, className, @@ -29,6 +30,7 @@ const ModelName: FC = ({ showFeatures, featuresClassName, showContextSize, + children, }) => { const language = useLanguage() @@ -42,21 +44,21 @@ const ModelName: FC = ({ `} >
{modelItem.label[language] || modelItem.label.en_US}
{ showModelType && modelItem.model_type && ( - + {modelTypeFormat(modelItem.model_type)} ) } { modelItem.model_properties.mode && showMode && ( - + {(modelItem.model_properties.mode as string).toLocaleUpperCase()} ) @@ -72,11 +74,12 @@ const ModelName: FC = ({ } { showContextSize && modelItem.model_properties.context_size && ( - + {sizeFormat(modelItem.model_properties.context_size as number)} ) } + {children}
) } diff --git a/web/app/components/header/account-setting/model-provider-page/model-parameter-modal/index.tsx b/web/app/components/header/account-setting/model-provider-page/model-parameter-modal/index.tsx index f33e5a6a44..15f27cb555 100644 --- a/web/app/components/header/account-setting/model-provider-page/model-parameter-modal/index.tsx +++ b/web/app/components/header/account-setting/model-provider-page/model-parameter-modal/index.tsx @@ -86,7 +86,7 @@ const ModelParameterModal: FC = ({ 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 = ({ 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 || [] diff --git a/web/app/components/header/account-setting/model-provider-page/model-selector/popup-item.tsx b/web/app/components/header/account-setting/model-provider-page/model-selector/popup-item.tsx index cdc8bbd7ae..82672158a9 100644 --- a/web/app/components/header/account-setting/model-provider-page/model-selector/popup-item.tsx +++ b/web/app/components/header/account-setting/model-provider-page/model-selector/popup-item.tsx @@ -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 = ({ setShowModelModal({ payload: { currentProvider, - currentConfigurateMethod: ConfigurateMethodEnum.predefinedModel, + currentConfigurationMethod: ConfigurationMethodEnum.predefinedModel, }, onSaveCallback: () => { updateModelProviders() diff --git a/web/app/components/header/account-setting/model-provider-page/provider-added-card/cooldown-timer.tsx b/web/app/components/header/account-setting/model-provider-page/provider-added-card/cooldown-timer.tsx new file mode 100644 index 0000000000..09cbe52230 --- /dev/null +++ b/web/app/components/header/account-setting/model-provider-page/provider-added-card/cooldown-timer.tsx @@ -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(Date.now()) + const [currentTime, setCurrentTime] = useState(targetTime.current) + const displayTime = useMemo( + () => Math.ceil((targetTime.current - currentTime) / 1000), + [currentTime], + ) + + const countdownTimeout = useRef() + 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 + ? ( + + + + ) + : null +} + +export default memo(CooldownTimer) diff --git a/web/app/components/header/account-setting/model-provider-page/provider-added-card/credential-panel.tsx b/web/app/components/header/account-setting/model-provider-page/provider-added-card/credential-panel.tsx index 5ef51bb581..26714431ed 100644 --- a/web/app/components/header/account-setting/model-provider-page/provider-added-card/credential-panel.tsx +++ b/web/app/components/header/account-setting/model-provider-page/provider-added-card/credential-panel.tsx @@ -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 = ({ updateModelProviders() configurateMethods.forEach((method) => { - if (method === ConfigurateMethodEnum.predefinedModel) + if (method === ConfigurationMethodEnum.predefinedModel) provider.supported_model_types.forEach(modelType => updateModelList(modelType)) }) diff --git a/web/app/components/header/account-setting/model-provider-page/provider-added-card/index.tsx b/web/app/components/header/account-setting/model-provider-page/provider-added-card/index.tsx index 8fa464dbe7..f24e3fa709 100644 --- a/web/app/components/header/account-setting/model-provider-page/provider-added-card/index.tsx +++ b/web/app/components/header/account-setting/model-provider-page/provider-added-card/index.tsx @@ -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 = ({ provider, @@ -39,7 +39,7 @@ const ProviderAddedCard: FC = ({ const [loading, setLoading] = useState(false) const [collapsed, setCollapsed] = useState(true) const [modelList, setModelList] = useState([]) - 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 = ({ ) } { - configurateMethods.includes(ConfigurateMethodEnum.predefinedModel) && ( + configurationMethods.includes(ConfigurationMethodEnum.predefinedModel) && ( onOpenModal(ConfigurateMethodEnum.predefinedModel)} + onSetup={() => onOpenModal(ConfigurationMethodEnum.predefinedModel)} provider={provider} /> ) @@ -136,9 +136,9 @@ const ProviderAddedCard: FC = ({ }
{ - configurateMethods.includes(ConfigurateMethodEnum.customizableModel) && ( + configurationMethods.includes(ConfigurationMethodEnum.customizableModel) && ( onOpenModal(ConfigurateMethodEnum.customizableModel)} + onClick={() => onOpenModal(ConfigurationMethodEnum.customizableModel)} className='hidden group-hover:flex group-hover:text-primary-600' /> ) @@ -152,7 +152,8 @@ const ProviderAddedCard: FC = ({ provider={provider} models={modelList} onCollapse={() => setCollapsed(true)} - onConfig={currentCustomConfigrationModelFixedFields => onOpenModal(ConfigurateMethodEnum.customizableModel, currentCustomConfigrationModelFixedFields)} + onConfig={currentCustomConfigurationModelFixedFields => onOpenModal(ConfigurationMethodEnum.customizableModel, currentCustomConfigurationModelFixedFields)} + onChange={(provider: string) => getModelList(provider)} /> ) } diff --git a/web/app/components/header/account-setting/model-provider-page/provider-added-card/model-list-item.tsx b/web/app/components/header/account-setting/model-provider-page/provider-added-card/model-list-item.tsx new file mode 100644 index 0000000000..3bde1af392 --- /dev/null +++ b/web/app/components/header/account-setting/model-provider-page/provider-added-card/model-list-item.tsx @@ -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 ( +
+ + + {modelLoadBalancingEnabled && !model.deprecated && model.load_balancing_enabled && ( + + + {t('common.modelProvider.loadBalancingHeadline')} + + )} + +
+ { + model.fetch_from === ConfigurationMethodEnum.customizableModel + ? ( + + ) + : ((modelLoadBalancingEnabled || plan.type === Plan.sandbox) && !model.deprecated && [ModelStatusEnum.active, ModelStatusEnum.disabled].includes(model.status)) + ? ( + + ) + : null + } + { + model.deprecated + ? ( + {t('common.modelProvider.modelHasBeenDeprecated')}} offset={{ mainAxis: 4 }}> + + + ) + : ( + + ) + } +
+
+ ) +} + +export default memo(ModelListItem) diff --git a/web/app/components/header/account-setting/model-provider-page/provider-added-card/model-list.tsx b/web/app/components/header/account-setting/model-provider-page/provider-added-card/model-list.tsx index e5c8a7aa6b..87f7ef91a4 100644 --- a/web/app/components/header/account-setting/model-provider-page/provider-added-card/model-list.tsx +++ b/web/app/components/header/account-setting/model-provider-page/provider-added-card/model-list.tsx @@ -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 = ({ 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 (
@@ -46,10 +53,7 @@ const ModelList: FC = ({ {t('common.modelProvider.modelsNum', { num: models.length })} {/* { - canCustomConfig && canSystemConfig && ( + isConfigurable && canSystemConfig && ( {}} /> ) } */} { - canCustomConfig && ( + isConfigurable && (
onConfig()} />
@@ -73,44 +77,16 @@ const ModelList: FC = ({
{ models.map(model => ( -
- - -
- { - model.fetch_from === ConfigurateMethodEnum.customizableModel && ( - - ) - } - -
-
+ {...{ + model, + provider, + isConfigurable, + onConfig, + onModifyLoadBalancing, + }} + /> )) }
diff --git a/web/app/components/header/account-setting/model-provider-page/provider-added-card/model-load-balancing-configs.tsx b/web/app/components/header/account-setting/model-provider-page/provider-added-card/model-load-balancing-configs.tsx new file mode 100644 index 0000000000..ab089360df --- /dev/null +++ b/web/app/components/header/account-setting/model-provider-page/provider-added-card/model-load-balancing-configs.tsx @@ -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> + 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 ( + <> +
toggleModalBalancing(true) : undefined} + > +
+
+ +
+
+
+ {t('common.modelProvider.loadBalancing')} + + + +
+
{t('common.modelProvider.loadBalancingDescription')}
+
+ { + withSwitch && ( + toggleModalBalancing(value)} + /> + ) + } +
+ {draftConfig.enabled && ( +
+ {draftConfig.configs.map((config, index) => { + const isProviderManaged = config.name === '__inherit__' + return ( +
+
+
+ {(config.in_cooldown && Boolean(config.ttl)) + ? ( + clearCountdown(index)} /> + ) + : ( + + + + )} +
+
+ {isProviderManaged ? t('common.modelProvider.defaultConfig') : config.name} +
+ {isProviderManaged && ( + {t('common.modelProvider.providerManaged')} + )} +
+
+ {!isProviderManaged && ( + <> +
+ toggleEntryModal(index, config)} + > + + + updateConfigEntry(index, () => undefined)} + > + + + +
+ + )} + toggleConfigEntryEnabled(index, value)} + /> +
+
+ ) + })} + +
toggleEntryModal()} + > +
+ {t('common.modelProvider.addConfig')} +
+
+
+ )} + { + draftConfig.enabled && draftConfig.configs.length < 2 && ( +
+ + {t('common.modelProvider.loadBalancingLeastKeyWarning')} +
+ ) + } +
+ + {!modelLoadBalancingEnabled && !IS_CE_EDITION && ( + +
+
+ {t('common.modelProvider.upgradeForLoadBalancing')} +
+ +
+
+ )} + + ) +} + +export default ModelLoadBalancingConfigs diff --git a/web/app/components/header/account-setting/model-provider-page/provider-added-card/model-load-balancing-modal.tsx b/web/app/components/header/account-setting/model-provider-page/provider-added-card/model-load-balancing-modal.tsx new file mode 100644 index 0000000000..b18b5c1077 --- /dev/null +++ b/web/app/components/header/account-setting/model-provider-page/provider-added-card/model-load-balancing-modal.tsx @@ -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() + const originalConfigMap = useMemo(() => { + if (!originalConfig) + return {} + return originalConfig?.configs.reduce((prev, config) => { + if (config.id) + prev[config.id] = config + return prev + }, {} as Record) + }, [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 ( + +
{t('common.modelProvider.configLoadBalancing')}
+ {Boolean(model) && ( +
+ + +
+ )} +
+ } + > + {!draftConfig + ? + : ( + <> +
+
toggleModalBalancing(false) : undefined} + > +
+
+ {Boolean(model) && ( + + )} +
+
+
{t('common.modelProvider.providerManaged')}
+
{t('common.modelProvider.providerManagedDescription')}
+
+
+
+ + +
+ +
+ + +
+ + ) + } + + ) +} + +export default memo(ModelLoadBalancingModal) diff --git a/web/app/components/header/account-setting/model-provider-page/provider-added-card/priority-selector.tsx b/web/app/components/header/account-setting/model-provider-page/provider-added-card/priority-selector.tsx index 3b9f73c55c..ee8b5c2aca 100644 --- a/web/app/components/header/account-setting/model-provider-page/provider-added-card/priority-selector.tsx +++ b/web/app/components/header/account-setting/model-provider-page/provider-added-card/priority-selector.tsx @@ -18,7 +18,7 @@ const Selector: FC = ({ const options = [ { key: PreferredProviderTypeEnum.custom, - text: 'API', + text: t('common.modelProvider.apiKey'), }, { key: PreferredProviderTypeEnum.system, diff --git a/web/app/components/header/account-setting/model-provider-page/provider-card/index.tsx b/web/app/components/header/account-setting/model-provider-page/provider-card/index.tsx index 8b3b3c1b67..fc4298f516 100644 --- a/web/app/components/header/account-setting/model-provider-page/provider-card/index.tsx +++ b/web/app/components/header/account-setting/model-provider-page/provider-card/index.tsx @@ -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 = ({ @@ -28,8 +28,7 @@ const ProviderCard: FC = ({ }) => { 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 (
= ({
{ configurateMethods.map((method) => { - if (method === ConfigurateMethodEnum.predefinedModel) { + if (method === ConfigurationMethodEnum.predefinedModel) { return (