mirror of
https://github.com/langgenius/dify.git
synced 2026-05-04 09:28:04 +08:00
Merge branch 'feat/model-plugins-implementing' into deploy/dev
This commit is contained in:
@ -36,6 +36,9 @@ const mockMarketplacePlugins = vi.hoisted(() => ({
|
||||
const mockContextModelProviders = vi.hoisted(() => ({
|
||||
current: [] as MockContextProvider[],
|
||||
}))
|
||||
const mockTrialModels = vi.hoisted(() => ({
|
||||
current: ['test-openai', 'test-anthropic'] as string[],
|
||||
}))
|
||||
vi.mock('../hooks', async () => {
|
||||
const actual = await vi.importActual<typeof import('../hooks')>('../hooks')
|
||||
return {
|
||||
@ -56,20 +59,38 @@ vi.mock('@/context/provider-context', () => ({
|
||||
useProviderContext: () => ({ modelProviders: mockContextModelProviders.current }),
|
||||
}))
|
||||
|
||||
vi.mock('../provider-added-card/use-trial-credits', () => ({
|
||||
useTrialCredits: () => ({
|
||||
credits: 200,
|
||||
totalCredits: 200,
|
||||
isExhausted: false,
|
||||
isLoading: false,
|
||||
nextCreditResetDate: undefined,
|
||||
vi.mock('@/context/global-public-context', () => ({
|
||||
useSystemFeaturesQuery: () => ({
|
||||
data: { trial_models: mockTrialModels.current },
|
||||
}),
|
||||
}))
|
||||
|
||||
const mockTrialCredits = vi.hoisted(() => ({
|
||||
credits: 200,
|
||||
totalCredits: 200,
|
||||
isExhausted: false,
|
||||
isLoading: false,
|
||||
nextCreditResetDate: undefined as number | undefined,
|
||||
}))
|
||||
vi.mock('../provider-added-card/use-trial-credits', () => ({
|
||||
useTrialCredits: () => mockTrialCredits,
|
||||
}))
|
||||
|
||||
vi.mock('../provider-added-card/model-auth-dropdown/credits-exhausted-alert', () => ({
|
||||
default: ({ hasApiKeyFallback }: { hasApiKeyFallback: boolean }) => (
|
||||
<div data-testid="credits-exhausted-alert" data-has-api-key-fallback={String(hasApiKeyFallback)} />
|
||||
),
|
||||
}))
|
||||
|
||||
vi.mock('next-themes', () => ({
|
||||
useTheme: () => ({ theme: 'light' }),
|
||||
}))
|
||||
|
||||
vi.mock('@/config', async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import('@/config')>()
|
||||
return { ...actual, IS_CLOUD_EDITION: true }
|
||||
})
|
||||
|
||||
const mockInstallMutateAsync = vi.hoisted(() => vi.fn())
|
||||
vi.mock('@/service/use-plugins', () => ({
|
||||
useInstallPackageFromMarketPlace: () => ({ mutateAsync: mockInstallMutateAsync }),
|
||||
@ -148,6 +169,14 @@ describe('Popup', () => {
|
||||
mockMarketplacePlugins.current = []
|
||||
mockMarketplacePlugins.isLoading = false
|
||||
mockContextModelProviders.current = []
|
||||
mockTrialModels.current = ['test-openai', 'test-anthropic']
|
||||
Object.assign(mockTrialCredits, {
|
||||
credits: 200,
|
||||
totalCredits: 200,
|
||||
isExhausted: false,
|
||||
isLoading: false,
|
||||
nextCreditResetDate: undefined,
|
||||
})
|
||||
})
|
||||
|
||||
it('should filter models by search and allow clearing search', () => {
|
||||
@ -249,6 +278,89 @@ describe('Popup', () => {
|
||||
expect(screen.getByText('openai')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should show credits exhausted alert when an exhausted provider supports credits', () => {
|
||||
Object.assign(mockTrialCredits, {
|
||||
credits: 0,
|
||||
totalCredits: 200,
|
||||
isExhausted: true,
|
||||
})
|
||||
mockContextModelProviders.current = [
|
||||
makeContextProvider({
|
||||
provider: 'test-openai',
|
||||
system_configuration: {
|
||||
enabled: true,
|
||||
} as MockContextProvider['system_configuration'],
|
||||
}),
|
||||
]
|
||||
|
||||
render(
|
||||
<Popup
|
||||
modelList={[makeModel()]}
|
||||
onSelect={vi.fn()}
|
||||
onHide={vi.fn()}
|
||||
/>,
|
||||
)
|
||||
|
||||
expect(screen.getByTestId('credits-exhausted-alert')).toHaveAttribute('data-has-api-key-fallback', 'false')
|
||||
})
|
||||
|
||||
it('should not show credits exhausted alert when only non-trial system providers are exhausted', () => {
|
||||
Object.assign(mockTrialCredits, {
|
||||
credits: 0,
|
||||
totalCredits: 200,
|
||||
isExhausted: true,
|
||||
})
|
||||
mockTrialModels.current = ['test-anthropic']
|
||||
mockContextModelProviders.current = [
|
||||
makeContextProvider({
|
||||
provider: 'test-openai',
|
||||
system_configuration: {
|
||||
enabled: true,
|
||||
} as MockContextProvider['system_configuration'],
|
||||
}),
|
||||
]
|
||||
|
||||
render(
|
||||
<Popup
|
||||
modelList={[makeModel()]}
|
||||
onSelect={vi.fn()}
|
||||
onHide={vi.fn()}
|
||||
/>,
|
||||
)
|
||||
|
||||
expect(screen.queryByTestId('credits-exhausted-alert')).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should not mark api key fallback for non-trial system providers', () => {
|
||||
Object.assign(mockTrialCredits, {
|
||||
credits: 0,
|
||||
totalCredits: 200,
|
||||
isExhausted: true,
|
||||
})
|
||||
mockTrialModels.current = ['test-anthropic']
|
||||
mockContextModelProviders.current = [
|
||||
makeContextProvider({
|
||||
provider: 'test-openai',
|
||||
custom_configuration: {
|
||||
status: 'active',
|
||||
} as MockContextProvider['custom_configuration'],
|
||||
system_configuration: {
|
||||
enabled: true,
|
||||
} as MockContextProvider['system_configuration'],
|
||||
}),
|
||||
]
|
||||
|
||||
render(
|
||||
<Popup
|
||||
modelList={[makeModel()]}
|
||||
onSelect={vi.fn()}
|
||||
onHide={vi.fn()}
|
||||
/>,
|
||||
)
|
||||
|
||||
expect(screen.queryByTestId('credits-exhausted-alert')).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should open provider settings when clicking footer link', () => {
|
||||
const onHide = vi.fn()
|
||||
render(
|
||||
|
||||
@ -12,7 +12,7 @@ import Button from '@/app/components/base/button'
|
||||
import { ACCOUNT_SETTING_TAB } from '@/app/components/header/account-setting/constants'
|
||||
import checkTaskStatus from '@/app/components/plugins/install-plugin/base/check-task-status'
|
||||
import useRefreshPluginList from '@/app/components/plugins/install-plugin/hooks/use-refresh-plugin-list'
|
||||
import { IS_CLOUD_EDITION } from '@/config'
|
||||
import { useSystemFeaturesQuery } from '@/context/global-public-context'
|
||||
import { useModalContext } from '@/context/modal-context'
|
||||
import { useProviderContext } from '@/context/provider-context'
|
||||
import { useInstallPackageFromMarketPlace } from '@/service/use-plugins'
|
||||
@ -23,6 +23,7 @@ import { CustomConfigurationStatusEnum, ModelFeatureEnum } from '../declarations
|
||||
import { useLanguage, useMarketplaceAllPlugins } from '../hooks'
|
||||
import CreditsExhaustedAlert from '../provider-added-card/model-auth-dropdown/credits-exhausted-alert'
|
||||
import { useTrialCredits } from '../provider-added-card/use-trial-credits'
|
||||
import { providerSupportsCredits } from '../supports-credits'
|
||||
import { MODEL_PROVIDER_QUOTA_GET_PAID, modelNameMap, providerIconMap, providerKeyToPluginId } from '../utils'
|
||||
import PopupItem from './popup-item'
|
||||
|
||||
@ -55,16 +56,14 @@ const Popup: FC<PopupProps> = ({
|
||||
const { refreshPluginList } = useRefreshPluginList()
|
||||
const [installingProvider, setInstallingProvider] = useState<ModelProviderQuotaGetPaid | null>(null)
|
||||
const { isExhausted: isCreditsExhausted } = useTrialCredits()
|
||||
const showCreditsExhaustedAlert = useMemo(() => {
|
||||
return isCreditsExhausted && modelProviders.some(p => p.system_configuration.enabled && IS_CLOUD_EDITION)
|
||||
}, [isCreditsExhausted, modelProviders])
|
||||
const hasApiKeyFallback = useMemo(() => {
|
||||
return modelProviders.some((p) => {
|
||||
const isApiKeyActive = p.custom_configuration?.status === CustomConfigurationStatusEnum.active
|
||||
const supportsCredits = p.system_configuration.enabled && IS_CLOUD_EDITION
|
||||
return isApiKeyActive && supportsCredits
|
||||
})
|
||||
}, [modelProviders])
|
||||
const { data: systemFeatures } = useSystemFeaturesQuery()
|
||||
const trialModels = systemFeatures?.trial_models
|
||||
const showCreditsExhaustedAlert = isCreditsExhausted
|
||||
&& modelProviders.some(provider => providerSupportsCredits(provider, trialModels))
|
||||
const hasApiKeyFallback = modelProviders.some((provider) => {
|
||||
const isApiKeyActive = provider.custom_configuration?.status === CustomConfigurationStatusEnum.active
|
||||
return isApiKeyActive && providerSupportsCredits(provider, trialModels)
|
||||
})
|
||||
|
||||
const handleInstallPlugin = useCallback(async (key: ModelProviderQuotaGetPaid) => {
|
||||
if (!allPlugins || isMarketplacePluginsLoading || installingProvider)
|
||||
|
||||
@ -1,10 +1,10 @@
|
||||
import type { ModelProvider } from '../declarations'
|
||||
import { useCredentialStatus } from '@/app/components/header/account-setting/model-provider-page/model-auth/hooks'
|
||||
import { IS_CLOUD_EDITION } from '@/config'
|
||||
import { useSystemFeaturesQuery } from '@/context/global-public-context'
|
||||
import {
|
||||
PreferredProviderTypeEnum,
|
||||
} from '../declarations'
|
||||
import { providerSupportsCredits } from '../supports-credits'
|
||||
import { useTrialCredits } from './use-trial-credits'
|
||||
|
||||
export type UsagePriority = 'credits' | 'apiKey' | 'apiKeyOnly'
|
||||
@ -82,14 +82,9 @@ export function useCredentialPanelState(provider: ModelProvider | undefined): Cr
|
||||
const { data: systemFeatures } = useSystemFeaturesQuery()
|
||||
const trialModels = systemFeatures?.trial_models
|
||||
|
||||
const systemConfig = provider?.system_configuration
|
||||
const preferredType = provider?.preferred_provider_type
|
||||
|
||||
const providerKey = provider?.provider
|
||||
const supportsCredits = !!systemConfig?.enabled
|
||||
&& IS_CLOUD_EDITION
|
||||
&& !!providerKey
|
||||
&& (trialModels as string[] ?? []).includes(providerKey)
|
||||
const supportsCredits = providerSupportsCredits(provider, trialModels)
|
||||
|
||||
const priority: UsagePriority = !supportsCredits
|
||||
? 'apiKeyOnly'
|
||||
|
||||
@ -0,0 +1,42 @@
|
||||
import type { ModelProvider } from './declarations'
|
||||
import { CurrentSystemQuotaTypeEnum } from './declarations'
|
||||
import { providerSupportsCredits } from './supports-credits'
|
||||
|
||||
vi.mock('@/config', async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import('@/config')>()
|
||||
return { ...actual, IS_CLOUD_EDITION: true }
|
||||
})
|
||||
|
||||
const makeProvider = (overrides: Partial<ModelProvider> = {}): ModelProvider => ({
|
||||
provider: 'langgenius/openai/openai',
|
||||
system_configuration: {
|
||||
enabled: true,
|
||||
current_quota_type: CurrentSystemQuotaTypeEnum.trial,
|
||||
quota_configurations: [],
|
||||
},
|
||||
...overrides,
|
||||
} as ModelProvider)
|
||||
|
||||
describe('providerSupportsCredits', () => {
|
||||
it('returns true when the provider is system-enabled and listed in trial_models', () => {
|
||||
expect(providerSupportsCredits(makeProvider(), ['langgenius/openai/openai'])).toBe(true)
|
||||
})
|
||||
|
||||
it('returns false when the provider is not listed in trial_models', () => {
|
||||
expect(providerSupportsCredits(makeProvider(), ['langgenius/anthropic/anthropic'])).toBe(false)
|
||||
})
|
||||
|
||||
it('returns false when system hosting is disabled', () => {
|
||||
expect(providerSupportsCredits(makeProvider({
|
||||
system_configuration: {
|
||||
enabled: false,
|
||||
current_quota_type: CurrentSystemQuotaTypeEnum.trial,
|
||||
quota_configurations: [],
|
||||
},
|
||||
}), ['langgenius/openai/openai'])).toBe(false)
|
||||
})
|
||||
|
||||
it('returns false for an undefined provider', () => {
|
||||
expect(providerSupportsCredits(undefined, ['langgenius/openai/openai'])).toBe(false)
|
||||
})
|
||||
})
|
||||
@ -0,0 +1,14 @@
|
||||
import type { ModelProvider } from './declarations'
|
||||
import { IS_CLOUD_EDITION } from '@/config'
|
||||
|
||||
type CreditAwareProvider = Pick<ModelProvider, 'provider' | 'system_configuration'>
|
||||
|
||||
export const providerSupportsCredits = (
|
||||
provider: CreditAwareProvider | undefined,
|
||||
trialModels: readonly string[] | undefined,
|
||||
): boolean => {
|
||||
if (!IS_CLOUD_EDITION || !provider?.system_configuration.enabled)
|
||||
return false
|
||||
|
||||
return !!provider.provider && !!trialModels?.includes(provider.provider)
|
||||
}
|
||||
Reference in New Issue
Block a user