Model Runtime (#1858)

Co-authored-by: StyleZhang <jasonapring2015@outlook.com>
Co-authored-by: Garfield Dai <dai.hai@foxmail.com>
Co-authored-by: chenhe <guchenhe@gmail.com>
Co-authored-by: jyong <jyong@dify.ai>
Co-authored-by: Joel <iamjoel007@gmail.com>
Co-authored-by: Yeuoly <admin@srmxy.cn>
This commit is contained in:
takatost 2024-01-02 23:42:00 +08:00 • committed by GitHub
commit d069c668f8
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
807 changed files with 171310 additions and 23806 deletions

View file

@ -0,0 +1,29 @@
import type { FC } from 'react'
import { useTranslation } from 'react-i18next'
import { PlusCircle } from '@/app/components/base/icons/src/vender/solid/general'
type AddModelButtonProps = {
className?: string
onClick: () => void
}
const AddModelButton: FC<AddModelButtonProps> = ({
className,
onClick,
}) => {
const { t } = useTranslation()
return (
<span
className={`
shrink-0 flex items-center px-1.5 h-6 text-xs font-medium text-gray-500 cursor-pointer
hover:bg-primary-50 hover:text-primary-600 rounded-md ${className}
`}
onClick={onClick}
>
<PlusCircle className='mr-1 w-3 h-3' />
{t('common.modelProvider.addModel')}
</span>
)
}
export default AddModelButton

View file

@ -0,0 +1,81 @@
import type { FC } from 'react'
import { useTranslation } from 'react-i18next'
import { useSWRConfig } from 'swr'
import type { ModelProvider } from '../declarations'
import {
CustomConfigurationStatusEnum,
PreferredProviderTypeEnum,
} from '../declarations'
import { useUpdateModelList } from '../hooks'
import PrioritySelector from './priority-selector'
import PriorityUseTip from './priority-use-tip'
import Indicator from '@/app/components/header/indicator'
import { Settings01 } from '@/app/components/base/icons/src/vender/line/general'
import Button from '@/app/components/base/button'
import { changeModelProviderPriority } from '@/service/common'
import { useToastContext } from '@/app/components/base/toast'
type CredentialPanelProps = {
provider: ModelProvider
onSetup: () => void
}
const CredentialPanel: FC<CredentialPanelProps> = ({
provider,
onSetup,
}) => {
const { t } = useTranslation()
const { notify } = useToastContext()
const { mutate } = useSWRConfig()
const updateModelList = useUpdateModelList()
const customConfig = provider.custom_configuration
const systemConfig = provider.system_configuration
const priorityUseType = provider.preferred_provider_type
const customConfiged = customConfig.status === CustomConfigurationStatusEnum.active
const handleChangePriority = async (key: PreferredProviderTypeEnum) => {
const res = await changeModelProviderPriority({
url: `/workspaces/current/model-providers/${provider.provider}/preferred-provider-type`,
body: {
preferred_provider_type: key,
},
})
if (res.result === 'success') {
notify({ type: 'success', message: t('common.actionMsg.modifiedSuccessfully') })
mutate('/workspaces/current/model-providers')
updateModelList(1)
}
}
return (
<div className='shrink-0 relative ml-1 p-1 w-[112px] rounded-lg bg-white/[0.3] border-[0.5px] border-black/5'>
<div className='flex items-center justify-between mb-1 pt-1 pl-2 pr-[7px] h-5 text-xs font-medium text-gray-500'>
API-KEY
<Indicator color={customConfiged ? 'green' : 'gray'} />
</div>
<div className='flex items-center gap-0.5'>
<Button
className='grow px-0 h-6 bg-white text-xs font-medium rounded-md'
onClick={onSetup}
>
<Settings01 className='mr-1 w-3 h-3' />
{t('common.operation.setup')}
</Button>
{
systemConfig.enabled && customConfiged && (
<PrioritySelector
value={priorityUseType}
onSelect={handleChangePriority}
/>
)
}
</div>
{
priorityUseType === PreferredProviderTypeEnum.custom && systemConfig.enabled && (
<PriorityUseTip />
)
}
</div>
)
}
export default CredentialPanel

View file

@ -0,0 +1,161 @@
import type { FC } from 'react'
import { useState } from 'react'
import { useTranslation } from 'react-i18next'
import type {
CustomConfigrationModelFixedFields,
ModelItem,
ModelProvider,
} from '../declarations'
import { ConfigurateMethodEnum } from '../declarations'
import {
DEFAULT_BACKGROUND_COLOR,
modelTypeFormat,
} from '../utils'
import ProviderIcon from '../provider-icon'
import ModelBadge from '../model-badge'
import CredentialPanel from './credential-panel'
import QuotaPanel from './quota-panel'
import ModelList from './model-list'
import AddModelButton from './add-model-button'
import { ChevronDownDouble } from '@/app/components/base/icons/src/vender/line/arrows'
import { Loading02 } from '@/app/components/base/icons/src/vender/line/general'
import { fetchModelProviderModelList } from '@/service/common'
import { useEventEmitterContextContext } from '@/context/event-emitter'
export const UPDATE_MODEL_PROVIDER_CUSTOM_MODEL_LIST = 'UPDATE_MODEL_PROVIDER_CUSTOM_MODEL_LIST'
type ProviderAddedCardProps = {
provider: ModelProvider
onOpenModal: (configurateMethod: ConfigurateMethodEnum, currentCustomConfigrationModelFixedFields?: CustomConfigrationModelFixedFields) => void
}
const ProviderAddedCard: FC<ProviderAddedCardProps> = ({
provider,
onOpenModal,
}) => {
const { t } = useTranslation()
const { eventEmitter } = useEventEmitterContextContext()
const [fetched, setFetched] = useState(false)
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 systemConfig = provider.system_configuration
const hasModelList = fetched && !!modelList.length
const showQuota = systemConfig.enabled || ['minimax', 'spark', 'zhipuai', 'anthropic'].includes(provider.provider)
const getModelList = async (providerName: string) => {
if (loading)
return
try {
setLoading(true)
const modelsData = await fetchModelProviderModelList(`/workspaces/current/model-providers/${providerName}/models`)
setModelList(modelsData.data)
setCollapsed(false)
setFetched(true)
}
finally {
setLoading(false)
}
}
const handleOpenModelList = () => {
if (fetched) {
setCollapsed(false)
return
}
getModelList(provider.provider)
}
eventEmitter?.useSubscription((v: any) => {
if (v?.type === UPDATE_MODEL_PROVIDER_CUSTOM_MODEL_LIST && v.payload === provider.provider)
getModelList(v.payload)
})
return (
<div
className='mb-2 rounded-xl border-[0.5px] border-black/5 shadow-xs'
style={{ background: provider.background || DEFAULT_BACKGROUND_COLOR }}
>
<div className='flex pl-3 py-2 pr-2 rounded-t-xl'>
<div className='grow px-1 pt-1 pb-0.5'>
<ProviderIcon
className='mb-2'
provider={provider}
/>
<div className='flex gap-0.5'>
{
provider.supported_model_types.map(modelType => (
<ModelBadge key={modelType}>
{modelTypeFormat(modelType)}
</ModelBadge>
))
}
</div>
</div>
{
showQuota && (
<QuotaPanel
provider={provider}
/>
)
}
{
configurateMethods.includes(ConfigurateMethodEnum.predefinedModel) && (
<CredentialPanel
onSetup={() => onOpenModal(ConfigurateMethodEnum.predefinedModel)}
provider={provider}
/>
)
}
</div>
{
collapsed && (
<div className='group flex items-center justify-between pl-2 py-1.5 pr-[11px] border-t border-t-black/5 bg-white/30 text-xs font-medium text-gray-500'>
<div className='group-hover:hidden pl-1 pr-1.5 h-6 leading-6'>
{
hasModelList
? t('common.modelProvider.modelsNum', { num: modelList.length })
: t('common.modelProvider.showModels')
}
</div>
<div
className='hidden group-hover:flex items-center pl-1 pr-1.5 h-6 rounded-lg hover:bg-white cursor-pointer'
onClick={handleOpenModelList}
>
<ChevronDownDouble className='mr-0.5 w-3 h-3' />
{
hasModelList
? t('common.modelProvider.showModelsNum', { num: modelList.length })
: t('common.modelProvider.showModels')
}
{
loading && (
<Loading02 className='ml-0.5 animate-spin w-3 h-3' />
)
}
</div>
{
configurateMethods.includes(ConfigurateMethodEnum.customizableModel) && (
<AddModelButton
onClick={() => onOpenModal(ConfigurateMethodEnum.customizableModel)}
className='hidden group-hover:flex group-hover:text-primary-600'
/>
)
}
</div>
)
}
{
!collapsed && (
<ModelList
provider={provider}
models={modelList}
onCollapse={() => setCollapsed(true)}
onConfig={currentCustomConfigrationModelFixedFields => onOpenModal(ConfigurateMethodEnum.customizableModel, currentCustomConfigrationModelFixedFields)}
/>
)
}
</div>
)
}
export default ProviderAddedCard

View file

@ -0,0 +1,121 @@
import type { FC } from 'react'
import { useTranslation } from 'react-i18next'
import type {
CustomConfigrationModelFixedFields,
ModelItem,
ModelProvider,
} from '../declarations'
import {
ConfigurateMethodEnum,
ModelStatusEnum,
} 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 { ChevronDownDouble } from '@/app/components/base/icons/src/vender/line/arrows'
import Button from '@/app/components/base/button'
type ModelListProps = {
provider: ModelProvider
models: ModelItem[]
onCollapse: () => void
onConfig: (currentCustomConfigrationModelFixedFields?: CustomConfigrationModelFixedFields) => void
}
const ModelList: FC<ModelListProps> = ({
provider,
models,
onCollapse,
onConfig,
}) => {
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)
return (
<div className='px-2 pb-2 rounded-b-xl'>
<div className='py-1 bg-white rounded-lg'>
<div className='flex items-center pl-1 pr-[3px]'>
<span className='group shrink-0 flex items-center mr-2'>
<span className='group-hover:hidden pl-1 pr-1.5 h-6 leading-6 text-xs font-medium text-gray-500'>
{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
`}
onClick={() => onCollapse()}
>
<ChevronDownDouble className='mr-0.5 w-3 h-3 rotate-180' />
{t('common.modelProvider.collapse')}
</span>
</span>
{/* {
canCustomConfig && canSystemConfig && (
<span className='flex items-center'>
<Tab active='all' onSelect={() => {}} />
</span>
)
} */}
{
canCustomConfig && (
<div className='grow flex justify-end'>
<AddModelButton onClick={() => onConfig()} />
</div>
)
}
</div>
{
models.map(model => (
<div
key={model.model}
className={`
group flex items-center pl-2 pr-2.5 h-8 rounded-lg
${canCustomConfig && 'hover:bg-gray-50'}
`}
>
<div className='shrink-0 mr-2' style={{ background: provider.icon_small[language] }} />
<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>
))
}
</div>
</div>
)
}
export default ModelList

View file

@ -0,0 +1,72 @@
import { Fragment } from 'react'
import type { FC } from 'react'
import { Popover, Transition } from '@headlessui/react'
import { useTranslation } from 'react-i18next'
import { PreferredProviderTypeEnum } from '../declarations'
import { Check, DotsHorizontal } from '@/app/components/base/icons/src/vender/line/general'
import Button from '@/app/components/base/button'
type SelectorProps = {
value?: string
onSelect: (key: PreferredProviderTypeEnum) => void
}
const Selector: FC<SelectorProps> = ({
value,
onSelect,
}) => {
const { t } = useTranslation()
const options = [
{
key: PreferredProviderTypeEnum.custom,
text: 'API',
},
{
key: PreferredProviderTypeEnum.system,
text: t('common.modelProvider.quota'),
},
]
return (
<Popover className='relative'>
<Popover.Button>
{
({ open }) => (
<Button className={`
px-0 w-6 h-6 bg-white rounded-md
${open && '!bg-gray-100'}
`}>
<DotsHorizontal className='w-3 h-3 text-gray-700' />
</Button>
)
}
</Popover.Button>
<Transition
as={Fragment}
leave='transition ease-in duration-100'
leaveFrom='opacity-100'
leaveTo='opacity-0'
>
<Popover.Panel className='absolute top-7 right-0 w-[144px] bg-white border-[0.5px] border-gray-200 rounded-lg shadow-lg z-10'>
<div className='p-1'>
<div className='px-3 pt-2 pb-1 text-sm font-medium text-gray-700'>{t('common.modelProvider.card.priorityUse')}</div>
{
options.map(option => (
<Popover.Button as={Fragment} key={option.key}>
<div
className='flex items-center justify-between px-3 h-9 text-sm text-gray-700 rounded-lg cursor-pointer hover:bg-gray-50'
onClick={() => onSelect(option.key)}
>
<div className='grow'>{option.text}</div>
{value === option.key && <Check className='w-4 h-4 text-primary-600' />}
</div>
</Popover.Button>
))
}
</div>
</Popover.Panel>
</Transition>
</Popover>
)
}
export default Selector

View file

@ -0,0 +1,20 @@
import { useTranslation } from 'react-i18next'
import { ChevronDownDouble } from '@/app/components/base/icons/src/vender/line/arrows'
import Tooltip from '@/app/components/base/tooltip'
const PriorityUseTip = () => {
const { t } = useTranslation()
return (
<Tooltip
selector='provider-quota-credential-priority-using'
content={t('common.modelProvider.priorityUsing') || ''}
>
<div className='absolute -right-[5px] -top-[5px] bg-indigo-50 rounded-[5px] border-[0.5px] border-indigo-100 cursor-pointer'>
<ChevronDownDouble className='rotate-180 w-3 h-3 text-indigo-600' />
</div>
</Tooltip>
)
}
export default PriorityUseTip

View file

@ -0,0 +1,98 @@
import type { FC } from 'react'
import { useSWRConfig } from 'swr'
import { useTranslation } from 'react-i18next'
import type { ModelProvider } from '../declarations'
import {
CustomConfigurationStatusEnum,
PreferredProviderTypeEnum,
QuotaUnitEnum,
} from '../declarations'
import {
useAnthropicBuyQuota,
useFreeQuota,
} from '../hooks'
import PriorityUseTip from './priority-use-tip'
import { InfoCircle } from '@/app/components/base/icons/src/vender/line/general'
import Button from '@/app/components/base/button'
type QuotaPanelProps = {
provider: ModelProvider
}
const QuotaPanel: FC<QuotaPanelProps> = ({
provider,
}) => {
const { t } = useTranslation()
const { mutate } = useSWRConfig()
const handlePay = useAnthropicBuyQuota()
const handleFreeQuotaSuccess = () => {
mutate('/workspaces/current/model-providers')
}
const handleFreeQuota = useFreeQuota(handleFreeQuotaSuccess)
const customConfig = provider.custom_configuration
const priorityUseType = provider.preferred_provider_type
const systemConfig = provider.system_configuration
const currentQuota = systemConfig.enabled && systemConfig.quota_configurations.find(item => item.quota_type === systemConfig.current_quota_type)
return (
<div className='group relative shrink-0 min-w-[112px] px-3 py-2 rounded-lg bg-white/[0.3] border-[0.5px] border-black/5'>
<div className='flex items-center mb-2 h-4 text-xs font-medium text-gray-500'>
{t('common.modelProvider.quota')}
<InfoCircle className='ml-0.5 w-3 h-3 text-gray-400' />
</div>
{
currentQuota && (
<div className='flex items-center h-4 text-xs text-gray-500'>
<span className='mr-0.5 text-sm font-semibold text-gray-700'>{(currentQuota?.quota_limit || 0) - (currentQuota?.quota_used || 0)}</span>
{
currentQuota?.quota_unit === QuotaUnitEnum.tokens && 'Tokens'
}
{
currentQuota?.quota_unit === QuotaUnitEnum.times && t('common.modelProvider.callTimes')
}
</div>
)
}
{
!currentQuota && provider.provider === 'anthropic' && (
<Button
className='h-6 bg-white text-xs font-medium rounded-md'
onClick={handlePay}
>
{t('common.modelProvider.buyQuota')}
</Button>
)
}
{
!currentQuota && ['minimax', 'spark', 'zhipuai'].includes(provider.provider) && (
<Button
className='h-6 bg-white text-xs font-medium rounded-md'
onClick={() => handleFreeQuota(provider.provider)}
>
{t('common.modelProvider.getFreeTokens')}
</Button>
)
}
{
provider.provider === 'anthropic' && systemConfig.enabled && (
<div
className={`
absolute left-0 bottom-0 hidden group-hover:flex items-center justify-center
w-full h-[30px] backdrop-blur-[2px] bg-gradient-to-r from-[rgba(238,244,255,0.80)] to-[rgba(237,237,240,0.70)]
text-xs font-medium text-primary-600 cursor-pointer rounded-b-lg
`}
onClick={handlePay}
>
{t('common.modelProvider.buyQuota')}
</div>
)
}
{
priorityUseType === PreferredProviderTypeEnum.system && customConfig.status === CustomConfigurationStatusEnum.active && (
<PriorityUseTip />
)
}
</div>
)
}
export default QuotaPanel

View file

@ -0,0 +1,45 @@
import type { FC } from 'react'
type TabProps = {
active: string
onSelect: (active: string) => void
}
const Tab: FC<TabProps> = ({
active,
onSelect,
}) => {
const tabs = [
{
key: 'all',
text: 'All',
},
{
key: 'added',
text: 'Added',
},
{
key: 'build-in',
text: 'Build-in',
},
]
return (
<div className='flex items-center'>
{
tabs.map(tab => (
<div
key={tab.key}
className={`
flex items-center mr-1 px-[5px] h-[18px] rounded-md text-xs cursor-pointer
${active === tab.key ? 'bg-gray-200 font-medium text-gray-900' : 'text-gray-500 font-normal'}
`}
onClick={() => onSelect(tab.key)}
>
{tab.text}
</div>
))
}
</div>
)
}
export default Tab