mirror of
https://github.com/langgenius/dify.git
synced 2026-05-03 08:58:09 +08:00
feat: model total credits (#30727)
Co-authored-by: CodingOnStar <hanxujiang@dify.ai> Co-authored-by: Stephen Zhou <38493346+hyoban@users.noreply.github.com>
This commit is contained in:
@ -6,8 +6,10 @@ import {
|
||||
RiBrainLine,
|
||||
} from '@remixicon/react'
|
||||
import { useDebounce } from 'ahooks'
|
||||
import { useMemo } from 'react'
|
||||
import { useEffect, useMemo } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { IS_CLOUD_EDITION } from '@/config'
|
||||
import { useAppContext } from '@/context/app-context'
|
||||
import { useGlobalPublicStore } from '@/context/global-public-context'
|
||||
import { useProviderContext } from '@/context/provider-context'
|
||||
import { cn } from '@/utils/classnames'
|
||||
@ -20,6 +22,7 @@ import {
|
||||
} from './hooks'
|
||||
import InstallFromMarketplace from './install-from-marketplace'
|
||||
import ProviderAddedCard from './provider-added-card'
|
||||
import QuotaPanel from './provider-added-card/quota-panel'
|
||||
import SystemModelSelector from './system-model-selector'
|
||||
|
||||
type Props = {
|
||||
@ -31,6 +34,7 @@ const FixedModelProvider = ['langgenius/openai/openai', 'langgenius/anthropic/an
|
||||
const ModelProviderPage = ({ searchText }: Props) => {
|
||||
const debouncedSearchText = useDebounce(searchText, { wait: 500 })
|
||||
const { t } = useTranslation()
|
||||
const { mutateCurrentWorkspace, isValidatingCurrentWorkspace } = useAppContext()
|
||||
const { data: textGenerationDefaultModel, isLoading: isTextGenerationDefaultModelLoading } = useDefaultModel(ModelTypeEnum.textGeneration)
|
||||
const { data: embeddingsDefaultModel, isLoading: isEmbeddingsDefaultModelLoading } = useDefaultModel(ModelTypeEnum.textEmbedding)
|
||||
const { data: rerankDefaultModel, isLoading: isRerankDefaultModelLoading } = useDefaultModel(ModelTypeEnum.rerank)
|
||||
@ -88,6 +92,10 @@ const ModelProviderPage = ({ searchText }: Props) => {
|
||||
return [filteredConfiguredProviders, filteredNotConfiguredProviders]
|
||||
}, [configuredProviders, debouncedSearchText, notConfiguredProviders])
|
||||
|
||||
useEffect(() => {
|
||||
mutateCurrentWorkspace()
|
||||
}, [mutateCurrentWorkspace])
|
||||
|
||||
return (
|
||||
<div className="relative -mt-2 pt-1">
|
||||
<div className={cn('mb-2 flex items-center')}>
|
||||
@ -115,6 +123,7 @@ const ModelProviderPage = ({ searchText }: Props) => {
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{IS_CLOUD_EDITION && <QuotaPanel providers={providers} isLoading={isValidatingCurrentWorkspace} />}
|
||||
{!filteredConfiguredProviders?.length && (
|
||||
<div className="mb-2 rounded-[10px] bg-workflow-process-bg p-4">
|
||||
<div className="flex h-10 w-10 items-center justify-center rounded-[10px] border-[0.5px] border-components-card-border bg-components-card-bg shadow-lg backdrop-blur">
|
||||
|
||||
@ -7,6 +7,7 @@ import { useToastContext } from '@/app/components/base/toast'
|
||||
import { ConfigProvider } from '@/app/components/header/account-setting/model-provider-page/model-auth'
|
||||
import { useCredentialStatus } from '@/app/components/header/account-setting/model-provider-page/model-auth/hooks'
|
||||
import Indicator from '@/app/components/header/indicator'
|
||||
import { IS_CLOUD_EDITION } from '@/config'
|
||||
import { useEventEmitterContextContext } from '@/context/event-emitter'
|
||||
import { changeModelProviderPriority } from '@/service/common'
|
||||
import { cn } from '@/utils/classnames'
|
||||
@ -26,6 +27,7 @@ import PriorityUseTip from './priority-use-tip'
|
||||
type CredentialPanelProps = {
|
||||
provider: ModelProvider
|
||||
}
|
||||
|
||||
const CredentialPanel = ({
|
||||
provider,
|
||||
}: CredentialPanelProps) => {
|
||||
@ -47,6 +49,8 @@ const CredentialPanel = ({
|
||||
notAllowedToUse,
|
||||
} = useCredentialStatus(provider)
|
||||
|
||||
const showPrioritySelector = systemConfig.enabled && isCustomConfigured && IS_CLOUD_EDITION
|
||||
|
||||
const handleChangePriority = async (key: PreferredProviderTypeEnum) => {
|
||||
const res = await changeModelProviderPriority({
|
||||
url: `/workspaces/current/model-providers/${provider.provider}/preferred-provider-type`,
|
||||
@ -114,7 +118,7 @@ const CredentialPanel = ({
|
||||
provider={provider}
|
||||
/>
|
||||
{
|
||||
systemConfig.enabled && isCustomConfigured && (
|
||||
showPrioritySelector && (
|
||||
<PrioritySelector
|
||||
value={priorityUseType}
|
||||
onSelect={handleChangePriority}
|
||||
@ -131,7 +135,7 @@ const CredentialPanel = ({
|
||||
)
|
||||
}
|
||||
{
|
||||
systemConfig.enabled && isCustomConfigured && !provider.provider_credential_schema && (
|
||||
showPrioritySelector && !provider.provider_credential_schema && (
|
||||
<div className="ml-1">
|
||||
<PrioritySelector
|
||||
value={priorityUseType}
|
||||
|
||||
@ -3,6 +3,7 @@ import type {
|
||||
ModelItem,
|
||||
ModelProvider,
|
||||
} from '../declarations'
|
||||
import type { ModelProviderQuotaGetPaid } from '../utils'
|
||||
import {
|
||||
RiArrowRightSLine,
|
||||
RiInformation2Fill,
|
||||
@ -28,7 +29,6 @@ import {
|
||||
} from '../utils'
|
||||
import CredentialPanel from './credential-panel'
|
||||
import ModelList from './model-list'
|
||||
import QuotaPanel from './quota-panel'
|
||||
|
||||
export const UPDATE_MODEL_PROVIDER_CUSTOM_MODEL_LIST = 'UPDATE_MODEL_PROVIDER_CUSTOM_MODEL_LIST'
|
||||
type ProviderAddedCardProps = {
|
||||
@ -49,7 +49,7 @@ const ProviderAddedCard: FC<ProviderAddedCardProps> = ({
|
||||
const systemConfig = provider.system_configuration
|
||||
const hasModelList = fetched && !!modelList.length
|
||||
const { isCurrentWorkspaceManager } = useAppContext()
|
||||
const showQuota = systemConfig.enabled && [...MODEL_PROVIDER_QUOTA_GET_PAID].includes(provider.provider) && !IS_CE_EDITION
|
||||
const showModelProvider = systemConfig.enabled && MODEL_PROVIDER_QUOTA_GET_PAID.includes(provider.provider as ModelProviderQuotaGetPaid) && !IS_CE_EDITION
|
||||
const showCredential = configurationMethods.includes(ConfigurationMethodEnum.predefinedModel) && isCurrentWorkspaceManager
|
||||
|
||||
const getModelList = async (providerName: string) => {
|
||||
@ -104,13 +104,6 @@ const ProviderAddedCard: FC<ProviderAddedCardProps> = ({
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
{
|
||||
showQuota && (
|
||||
<QuotaPanel
|
||||
provider={provider}
|
||||
/>
|
||||
)
|
||||
}
|
||||
{
|
||||
showCredential && (
|
||||
<CredentialPanel
|
||||
@ -122,7 +115,7 @@ const ProviderAddedCard: FC<ProviderAddedCardProps> = ({
|
||||
{
|
||||
collapsed && (
|
||||
<div className="system-xs-medium group flex items-center justify-between border-t border-t-divider-subtle py-1.5 pl-2 pr-[11px] text-text-tertiary">
|
||||
{(showQuota || !notConfigured) && (
|
||||
{(showModelProvider || !notConfigured) && (
|
||||
<>
|
||||
<div className="flex h-6 items-center pl-1 pr-1.5 leading-6 group-hover:hidden">
|
||||
{
|
||||
@ -150,7 +143,7 @@ const ProviderAddedCard: FC<ProviderAddedCardProps> = ({
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
{!showQuota && notConfigured && (
|
||||
{!showModelProvider && notConfigured && (
|
||||
<div className="flex h-6 items-center pl-1 pr-1.5">
|
||||
<RiInformation2Fill className="mr-1 h-4 w-4 text-text-accent" />
|
||||
<span className="system-xs-medium text-text-secondary">{t('modelProvider.configureTip', { ns: 'common' })}</span>
|
||||
|
||||
@ -1,66 +1,163 @@
|
||||
import type { FC } from 'react'
|
||||
import type { ModelProvider } from '../declarations'
|
||||
import type { Plugin } from '@/app/components/plugins/types'
|
||||
import { useBoolean } from 'ahooks'
|
||||
import * as React from 'react'
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { AnthropicShortLight, Deepseek, Gemini, Grok, OpenaiSmall, Tongyi } from '@/app/components/base/icons/src/public/llm'
|
||||
import Loading from '@/app/components/base/loading'
|
||||
import Tooltip from '@/app/components/base/tooltip'
|
||||
import InstallFromMarketplace from '@/app/components/plugins/install-plugin/install-from-marketplace'
|
||||
import { useAppContext } from '@/context/app-context'
|
||||
import useTimestamp from '@/hooks/use-timestamp'
|
||||
import { cn } from '@/utils/classnames'
|
||||
import { formatNumber } from '@/utils/format'
|
||||
import {
|
||||
CustomConfigurationStatusEnum,
|
||||
PreferredProviderTypeEnum,
|
||||
QuotaUnitEnum,
|
||||
} from '../declarations'
|
||||
import {
|
||||
MODEL_PROVIDER_QUOTA_GET_PAID,
|
||||
} from '../utils'
|
||||
import PriorityUseTip from './priority-use-tip'
|
||||
import { PreferredProviderTypeEnum } from '../declarations'
|
||||
import { useMarketplaceAllPlugins } from '../hooks'
|
||||
import { modelNameMap, ModelProviderQuotaGetPaid } from '../utils'
|
||||
|
||||
const allProviders = [
|
||||
{ key: ModelProviderQuotaGetPaid.OPENAI, Icon: OpenaiSmall },
|
||||
{ key: ModelProviderQuotaGetPaid.ANTHROPIC, Icon: AnthropicShortLight },
|
||||
{ key: ModelProviderQuotaGetPaid.GEMINI, Icon: Gemini },
|
||||
{ key: ModelProviderQuotaGetPaid.X, Icon: Grok },
|
||||
{ key: ModelProviderQuotaGetPaid.DEEPSEEK, Icon: Deepseek },
|
||||
{ key: ModelProviderQuotaGetPaid.TONGYI, Icon: Tongyi },
|
||||
] as const
|
||||
|
||||
// Map provider key to plugin ID
|
||||
// provider key format: langgenius/provider/model, plugin ID format: langgenius/provider
|
||||
const providerKeyToPluginId: Record<string, string> = {
|
||||
[ModelProviderQuotaGetPaid.OPENAI]: 'langgenius/openai',
|
||||
[ModelProviderQuotaGetPaid.ANTHROPIC]: 'langgenius/anthropic',
|
||||
[ModelProviderQuotaGetPaid.GEMINI]: 'langgenius/gemini',
|
||||
[ModelProviderQuotaGetPaid.X]: 'langgenius/x',
|
||||
[ModelProviderQuotaGetPaid.DEEPSEEK]: 'langgenius/deepseek',
|
||||
[ModelProviderQuotaGetPaid.TONGYI]: 'langgenius/tongyi',
|
||||
}
|
||||
|
||||
type QuotaPanelProps = {
|
||||
provider: ModelProvider
|
||||
providers: ModelProvider[]
|
||||
isLoading?: boolean
|
||||
}
|
||||
const QuotaPanel: FC<QuotaPanelProps> = ({
|
||||
provider,
|
||||
providers,
|
||||
isLoading = false,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const { currentWorkspace } = useAppContext()
|
||||
const credits = Math.max((currentWorkspace.trial_credits - currentWorkspace.trial_credits_used) || 0, 0)
|
||||
const providerMap = useMemo(() => new Map(
|
||||
providers.map(p => [p.provider, p.preferred_provider_type]),
|
||||
), [providers])
|
||||
const { formatTime } = useTimestamp()
|
||||
const {
|
||||
plugins: allPlugins,
|
||||
} = useMarketplaceAllPlugins(providers, '')
|
||||
const [selectedPlugin, setSelectedPlugin] = useState<Plugin | null>(null)
|
||||
const [isShowInstallModal, {
|
||||
setTrue: showInstallFromMarketplace,
|
||||
setFalse: hideInstallFromMarketplace,
|
||||
}] = useBoolean(false)
|
||||
const selectedPluginIdRef = useRef<string | null>(null)
|
||||
|
||||
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)
|
||||
const openaiOrAnthropic = MODEL_PROVIDER_QUOTA_GET_PAID.includes(provider.provider)
|
||||
const handleIconClick = useCallback((key: string) => {
|
||||
const providerType = providerMap.get(key)
|
||||
if (!providerType && allPlugins) {
|
||||
const pluginId = providerKeyToPluginId[key]
|
||||
const plugin = allPlugins.find(p => p.plugin_id === pluginId)
|
||||
if (plugin) {
|
||||
setSelectedPlugin(plugin)
|
||||
selectedPluginIdRef.current = pluginId
|
||||
showInstallFromMarketplace()
|
||||
}
|
||||
}
|
||||
}, [allPlugins, providerMap, showInstallFromMarketplace])
|
||||
|
||||
useEffect(() => {
|
||||
if (isShowInstallModal && selectedPluginIdRef.current) {
|
||||
const isInstalled = providers.some(p => p.provider.startsWith(selectedPluginIdRef.current!))
|
||||
if (isInstalled) {
|
||||
hideInstallFromMarketplace()
|
||||
selectedPluginIdRef.current = null
|
||||
}
|
||||
}
|
||||
}, [providers, isShowInstallModal, hideInstallFromMarketplace])
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="my-2 flex min-h-[72px] items-center justify-center rounded-xl border-[0.5px] border-components-panel-border bg-third-party-model-bg-default shadow-xs">
|
||||
<Loading />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="group relative min-w-[112px] shrink-0 rounded-lg border-[0.5px] border-components-panel-border bg-white/[0.18] px-3 py-2 shadow-xs">
|
||||
<div className={cn('my-2 min-w-[72px] shrink-0 rounded-xl border-[0.5px] pb-2.5 pl-4 pr-2.5 pt-3 shadow-xs', credits <= 0 ? 'border-state-destructive-border hover:bg-state-destructive-hover' : 'border-components-panel-border bg-third-party-model-bg-default')}>
|
||||
<div className="system-xs-medium-uppercase mb-2 flex h-4 items-center text-text-tertiary">
|
||||
{t('modelProvider.quota', { ns: 'common' })}
|
||||
<Tooltip popupContent={
|
||||
openaiOrAnthropic
|
||||
? t('modelProvider.card.tip', { ns: 'common' })
|
||||
: t('modelProvider.quotaTip', { ns: 'common' })
|
||||
}
|
||||
/>
|
||||
<Tooltip popupContent={t('modelProvider.card.tip', { ns: 'common' })} />
|
||||
</div>
|
||||
{
|
||||
currentQuota && (
|
||||
<div className="flex h-4 items-center text-xs text-text-tertiary">
|
||||
<span className="system-md-semibold-uppercase mr-0.5 text-text-secondary">{formatNumber(Math.max((currentQuota?.quota_limit || 0) - (currentQuota?.quota_used || 0), 0))}</span>
|
||||
{
|
||||
currentQuota?.quota_unit === QuotaUnitEnum.tokens && 'Tokens'
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-1 text-xs text-text-tertiary">
|
||||
<span className="system-md-semibold-uppercase mr-0.5 text-text-secondary">{formatNumber(credits)}</span>
|
||||
<span>{t('modelProvider.credits', { ns: 'common' })}</span>
|
||||
{currentWorkspace.next_credit_reset_date
|
||||
? (
|
||||
<>
|
||||
<span>·</span>
|
||||
<span>
|
||||
{t('modelProvider.resetDate', {
|
||||
ns: 'common',
|
||||
date: formatTime(currentWorkspace.next_credit_reset_date, t('dateFormat', { ns: 'appLog' })),
|
||||
interpolation: { escapeValue: false },
|
||||
})}
|
||||
</span>
|
||||
</>
|
||||
)
|
||||
: null}
|
||||
</div>
|
||||
<div className="flex items-center gap-1">
|
||||
{allProviders.map(({ key, Icon }) => {
|
||||
const providerType = providerMap.get(key)
|
||||
const usingQuota = providerType === PreferredProviderTypeEnum.system
|
||||
const getTooltipKey = () => {
|
||||
if (usingQuota)
|
||||
return 'modelProvider.card.modelSupported'
|
||||
if (providerType === PreferredProviderTypeEnum.custom)
|
||||
return 'modelProvider.card.modelAPI'
|
||||
return 'modelProvider.card.modelNotSupported'
|
||||
}
|
||||
{
|
||||
currentQuota?.quota_unit === QuotaUnitEnum.times && t('modelProvider.callTimes', { ns: 'common' })
|
||||
}
|
||||
{
|
||||
currentQuota?.quota_unit === QuotaUnitEnum.credits && t('modelProvider.credits', { ns: 'common' })
|
||||
}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
{
|
||||
priorityUseType === PreferredProviderTypeEnum.system && customConfig.status === CustomConfigurationStatusEnum.active && (
|
||||
<PriorityUseTip />
|
||||
)
|
||||
}
|
||||
return (
|
||||
<Tooltip
|
||||
key={key}
|
||||
popupContent={t(getTooltipKey(), { modelName: modelNameMap[key], ns: 'common' })}
|
||||
>
|
||||
<div
|
||||
className={cn('relative h-6 w-6', !providerType && 'cursor-pointer hover:opacity-80')}
|
||||
onClick={() => handleIconClick(key)}
|
||||
>
|
||||
<Icon className="h-6 w-6 rounded-lg" />
|
||||
{!usingQuota && (
|
||||
<div className="absolute inset-0 rounded-lg border-[0.5px] border-components-panel-border-subtle bg-background-default-dodge opacity-30" />
|
||||
)}
|
||||
</div>
|
||||
</Tooltip>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
{isShowInstallModal && selectedPlugin && (
|
||||
<InstallFromMarketplace
|
||||
manifest={selectedPlugin}
|
||||
uniqueIdentifier={selectedPlugin.latest_package_identifier}
|
||||
onClose={hideInstallFromMarketplace}
|
||||
onSuccess={hideInstallFromMarketplace}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default QuotaPanel
|
||||
export default React.memo(QuotaPanel)
|
||||
|
||||
@ -17,7 +17,25 @@ import {
|
||||
ModelTypeEnum,
|
||||
} from './declarations'
|
||||
|
||||
export const MODEL_PROVIDER_QUOTA_GET_PAID = ['langgenius/anthropic/anthropic', 'langgenius/openai/openai', 'langgenius/azure_openai/azure_openai']
|
||||
export enum ModelProviderQuotaGetPaid {
|
||||
ANTHROPIC = 'langgenius/anthropic/anthropic',
|
||||
OPENAI = 'langgenius/openai/openai',
|
||||
// AZURE_OPENAI = 'langgenius/azure_openai/azure_openai',
|
||||
GEMINI = 'langgenius/gemini/google',
|
||||
X = 'langgenius/x/x',
|
||||
DEEPSEEK = 'langgenius/deepseek/deepseek',
|
||||
TONGYI = 'langgenius/tongyi/tongyi',
|
||||
}
|
||||
export const MODEL_PROVIDER_QUOTA_GET_PAID = [ModelProviderQuotaGetPaid.ANTHROPIC, ModelProviderQuotaGetPaid.OPENAI, ModelProviderQuotaGetPaid.GEMINI, ModelProviderQuotaGetPaid.X, ModelProviderQuotaGetPaid.DEEPSEEK, ModelProviderQuotaGetPaid.TONGYI]
|
||||
|
||||
export const modelNameMap = {
|
||||
[ModelProviderQuotaGetPaid.OPENAI]: 'OpenAI',
|
||||
[ModelProviderQuotaGetPaid.ANTHROPIC]: 'Anthropic',
|
||||
[ModelProviderQuotaGetPaid.GEMINI]: 'Gemini',
|
||||
[ModelProviderQuotaGetPaid.X]: 'xAI',
|
||||
[ModelProviderQuotaGetPaid.DEEPSEEK]: 'DeepSeek',
|
||||
[ModelProviderQuotaGetPaid.TONGYI]: 'Tongyi',
|
||||
}
|
||||
|
||||
export const isNullOrUndefined = (value: any) => {
|
||||
return value === undefined || value === null
|
||||
|
||||
Reference in New Issue
Block a user