fix: show credit-enabled providers in model selector popup

- keep installed providers visible when no models are available
- only apply the fallback when AI credits are available
- add popup tests for visible and exhausted-credit cases
This commit is contained in:
CodingOnStar
2026-03-17 16:26:08 +08:00
parent f5f87b73a8
commit 40e9c19f90
2 changed files with 72 additions and 9 deletions

View File

@ -27,7 +27,7 @@ type MockMarketplacePlugin = {
latest_package_identifier: string
}
type MockContextProvider = Pick<ModelProvider, 'provider' | 'custom_configuration' | 'system_configuration'>
type MockContextProvider = Pick<ModelProvider, 'provider' | 'label' | 'icon_small' | 'icon_small_dark' | 'custom_configuration' | 'system_configuration'>
const mockMarketplacePlugins = vi.hoisted(() => ({
current: [] as MockMarketplacePlugin[],
@ -152,6 +152,9 @@ const makeModel = (overrides: Partial<Model> = {}): Model => ({
const makeContextProvider = (overrides: Partial<MockContextProvider> = {}): 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(
<Popup
modelList={[]}
onSelect={vi.fn()}
onHide={vi.fn()}
/>,
)
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(
<Popup

View File

@ -19,7 +19,11 @@ import { useInstallPackageFromMarketPlace } from '@/service/use-plugins'
import { cn } from '@/utils/classnames'
import { supportFunctionCall } from '@/utils/tool-call'
import { getMarketplaceUrl } from '@/utils/var'
import { CustomConfigurationStatusEnum, ModelFeatureEnum } from '../declarations'
import {
CustomConfigurationStatusEnum,
ModelFeatureEnum,
ModelStatusEnum,
} 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'
@ -58,6 +62,19 @@ const Popup: FC<PopupProps> = ({
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<string>()
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<PopupProps> = ({
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<PopupProps> = ({
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<PopupProps> = ({
}
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))