diff --git a/web/app/components/header/account-setting/model-provider-page/model-selector/__tests__/popup.spec.tsx b/web/app/components/header/account-setting/model-provider-page/model-selector/__tests__/popup.spec.tsx index 08675b7658..dee16e394e 100644 --- a/web/app/components/header/account-setting/model-provider-page/model-selector/__tests__/popup.spec.tsx +++ b/web/app/components/header/account-setting/model-provider-page/model-selector/__tests__/popup.spec.tsx @@ -27,7 +27,7 @@ type MockMarketplacePlugin = { latest_package_identifier: string } -type MockContextProvider = Pick +type MockContextProvider = Pick const mockMarketplacePlugins = vi.hoisted(() => ({ current: [] as MockMarketplacePlugin[], @@ -152,6 +152,9 @@ const makeModel = (overrides: Partial = {}): Model => ({ const makeContextProvider = (overrides: Partial = {}): MockContextProvider => ({ provider: 'test-openai', + label: { en_US: 'Test OpenAI', zh_Hans: 'Test OpenAI' }, + icon_small: { en_US: '', zh_Hans: '' }, + icon_small_dark: { en_US: '', zh_Hans: '' }, custom_configuration: { status: 'no-configure', } as MockContextProvider['custom_configuration'], @@ -443,8 +446,38 @@ describe('Popup', () => { expect(screen.getByText(/modelProvider\.selector\.discoverMoreInMarketplace/)).toBeInTheDocument() }) - it('should hide installed marketplace providers when they are absent from the current modelList', () => { - mockContextModelProviders.current = [makeContextProvider({ provider: 'test-anthropic' })] + it('should show installed marketplace providers without models when AI credits are available', () => { + mockContextModelProviders.current = [makeContextProvider({ + provider: 'test-anthropic', + system_configuration: { + enabled: true, + } as MockContextProvider['system_configuration'], + })] + + render( + , + ) + + expect(screen.getByText('test-anthropic')).toBeInTheDocument() + expect(screen.getByText('TestOpenAI')).toBeInTheDocument() + }) + + it('should hide installed marketplace providers without models when AI credits are exhausted', () => { + Object.assign(mockTrialCredits, { + credits: 0, + totalCredits: 200, + isExhausted: true, + }) + mockContextModelProviders.current = [makeContextProvider({ + provider: 'test-anthropic', + system_configuration: { + enabled: true, + } as MockContextProvider['system_configuration'], + })] render( = ({ const { isExhausted: isCreditsExhausted } = useTrialCredits() const { data: systemFeatures } = useSystemFeaturesQuery() const trialModels = systemFeatures?.trial_models + const installedProviderMap = useMemo(() => new Map( + modelProviders.map(provider => [provider.provider, provider]), + ), [modelProviders]) + const aiCreditVisibleProviders = useMemo(() => { + if (isCreditsExhausted) + return new Set() + + return new Set( + modelProviders + .filter(provider => providerSupportsCredits(provider, trialModels)) + .map(provider => provider.provider), + ) + }, [isCreditsExhausted, modelProviders, trialModels]) const showCreditsExhaustedAlert = isCreditsExhausted && modelProviders.some(provider => providerSupportsCredits(provider, trialModels)) const hasApiKeyFallback = modelProviders.some((provider) => { @@ -92,18 +109,31 @@ const Popup: FC = ({ const installedModelList = useMemo(() => { const modelMap = new Map(modelList.map(model => [model.provider, model])) const installedMarketplaceModels = MODEL_PROVIDER_QUOTA_GET_PAID.flatMap((providerKey) => { - const installedProvider = modelProviders.find(provider => provider.provider === providerKey) + const installedProvider = installedProviderMap.get(providerKey) if (!installedProvider) return [] const matchedModel = modelMap.get(providerKey) - return matchedModel ? [matchedModel] : [] + if (matchedModel) + return [matchedModel] + + if (!aiCreditVisibleProviders.has(providerKey)) + return [] + + return [{ + provider: installedProvider.provider, + icon_small: installedProvider.icon_small, + icon_small_dark: installedProvider.icon_small_dark, + label: installedProvider.label, + models: [], + status: ModelStatusEnum.active, + }] }) const otherModels = modelList.filter(model => !MODEL_PROVIDER_QUOTA_GET_PAID.includes(model.provider as ModelProviderQuotaGetPaid)) return [...installedMarketplaceModels, ...otherModels] - }, [modelList, modelProviders]) + }, [aiCreditVisibleProviders, installedProviderMap, modelList]) const filteredModelList = useMemo(() => { const filtered = installedModelList.map((model) => { @@ -128,7 +158,7 @@ const Popup: FC = ({ return modelItem.features?.includes(feature) ?? false }) }) - if (!matchesProviderSearch || filteredModels.length === 0) + if (!matchesProviderSearch || (filteredModels.length === 0 && !aiCreditVisibleProviders.has(model.provider))) return null return { ...model, models: filteredModels } @@ -143,7 +173,7 @@ const Popup: FC = ({ } return filtered - }, [defaultModel?.provider, installedModelList, language, scopeFeatures, searchText]) + }, [aiCreditVisibleProviders, defaultModel?.provider, installedModelList, language, scopeFeatures, searchText]) const marketplaceProviders = useMemo(() => { const installedProviders = new Set(modelProviders.map(provider => provider.provider))