fix: prioritize credits exhausted badge over incompatible in model trigger

- return credits-exhausted when ai credits are exhausted and no api key is configured
- keep incompatible for missing provider plugin and other unsupported cases
- add coverage for derive-model-status and trigger badge rendering
This commit is contained in:
CodingOnStar
2026-03-17 16:38:17 +08:00
parent 40e9c19f90
commit 37367c65c3
6 changed files with 71 additions and 12 deletions

View File

@ -64,6 +64,22 @@ describe('deriveModelStatus', () => {
).toBe('incompatible')
})
it('should return credits-exhausted when model is missing and AI credits are exhausted without api key', () => {
expect(
deriveModelStatus(
'text-embedding-3-large',
'openai',
createModelProvider(),
undefined,
createCredentialState({
priority: 'apiKey',
hasCredentials: false,
isCreditsExhausted: true,
}),
),
).toBe('credits-exhausted')
})
it('should return configure-required when the model status is no-configure', () => {
expect(
deriveModelStatus('text-embedding-3-large', 'openai', createModelProvider(), createModelItem({ status: ModelStatusEnum.noConfigure }), createCredentialState()),

View File

@ -35,14 +35,21 @@ export const deriveModelStatus = (
if (!modelId || !providerName)
return 'empty'
if (!currentModelProvider || !currentModel)
if (!currentModelProvider)
return 'incompatible'
if (credentialState.priority === 'credits'
const isCreditsExhaustedWithoutApiKey = credentialState.supportsCredits
&& credentialState.isCreditsExhausted
&& !credentialState.hasCredentials
const isCreditsPriorityExhausted = credentialState.priority === 'credits'
&& credentialState.supportsCredits
&& credentialState.isCreditsExhausted) {
&& credentialState.isCreditsExhausted
if (isCreditsPriorityExhausted || isCreditsExhaustedWithoutApiKey)
return 'credits-exhausted'
}
if (!currentModel)
return 'incompatible'
if (credentialState.variant === 'api-unavailable')
return 'api-key-unavailable'

View File

@ -224,6 +224,27 @@ describe('Trigger', () => {
expect(screen.getByText('common.modelProvider.selector.incompatible')).toBeInTheDocument()
})
it('should render credits exhausted badge when model is missing and AI credits are exhausted without api key', () => {
mockUseCredentialPanelState.mockReturnValue({
...activeCredentialState,
variant: 'no-usage',
priority: 'apiKey',
hasCredentials: false,
isCreditsExhausted: true,
credentialName: undefined,
})
render(
<Trigger
currentProvider={currentProvider}
providerName="openai"
modelId="gpt-4"
/>,
)
expect(screen.getByText('common.modelProvider.selector.creditsExhausted')).toBeInTheDocument()
})
it('should render configure required badge when model status is no-configure', () => {
render(
<Trigger

View File

@ -60,6 +60,17 @@ describe('deriveTriggerStatus', () => {
expect(deriveTriggerStatus('gpt-4', 'openai', mockProvider, undefined, baseCredentialState)).toBe('incompatible')
})
it('returns credits-exhausted when currentModel is missing and AI credits are exhausted without api key', () => {
const state: CredentialPanelState = {
...baseCredentialState,
priority: 'apiKey',
hasCredentials: false,
isCreditsExhausted: true,
credentialName: undefined,
}
expect(deriveTriggerStatus('gpt-4', 'openai', mockProvider, undefined, state)).toBe('credits-exhausted')
})
it('returns configure-required when model status is no-configure', () => {
const model = { ...mockModel, status: ModelStatusEnum.noConfigure } as ModelItem
expect(deriveTriggerStatus('gpt-4', 'openai', mockProvider, model, baseCredentialState)).toBe('configure-required')

View File

@ -42,6 +42,8 @@ const Trigger: FC<TriggerProps> = ({
const status = deriveTriggerStatus(modelId, providerName, currentModelProvider, currentModel, credentialState)
const badgeKey = TRIGGER_STATUS_BADGE_I18N[status]
const tooltipKey = TRIGGER_STATUS_TOOLTIP_I18N[status]
const badgeLabel = badgeKey ? t(badgeKey, { ns: 'common', defaultValue: badgeKey }) : null
const tooltipLabel = tooltipKey ? t(tooltipKey, { ns: 'common', defaultValue: tooltipKey }) : null
const isActive = status === 'active'
const iconProvider = currentProvider || modelProviders.find(item => item.provider === providerName)
@ -89,7 +91,7 @@ const Trigger: FC<TriggerProps> = ({
: <div className="truncate text-[13px] font-normal text-components-input-text-filled">{modelId}</div>}
</div>
{badgeKey && (
tooltipKey
tooltipLabel
? (
<Tooltip>
<TooltipTrigger
@ -98,14 +100,14 @@ const Trigger: FC<TriggerProps> = ({
<div className="flex min-w-[20px] shrink-0 items-center justify-center gap-[3px] rounded-md border border-text-warning bg-components-badge-bg-dimm px-[5px] py-0.5">
<span className="i-ri-alert-fill h-3 w-3 text-text-warning" />
<span className="whitespace-nowrap text-text-warning system-xs-medium">
{t(badgeKey as 'modelProvider.selector.incompatible', { ns: 'common' })}
{badgeLabel}
</span>
</div>
</div>
)}
/>
<TooltipContent placement="top">
{t(tooltipKey as 'modelProvider.selector.incompatibleTip', { ns: 'common' })}
{tooltipLabel}
</TooltipContent>
</Tooltip>
)
@ -114,7 +116,7 @@ const Trigger: FC<TriggerProps> = ({
<div className="flex min-w-[20px] shrink-0 items-center justify-center gap-[3px] rounded-md border border-text-warning bg-components-badge-bg-dimm px-[5px] py-0.5">
<span className="i-ri-alert-fill h-3 w-3 text-text-warning" />
<span className="whitespace-nowrap text-text-warning system-xs-medium">
{t(badgeKey as 'modelProvider.selector.incompatible', { ns: 'common' })}
{badgeLabel}
</span>
</div>
</div>

View File

@ -64,6 +64,8 @@ const ModelSelectorTrigger: FC<ModelSelectorTriggerProps> = ({
const isDisabled = status !== 'active' && status !== 'empty'
const statusI18nKey = DERIVED_MODEL_STATUS_BADGE_I18N[status]
const tooltipI18nKey = DERIVED_MODEL_STATUS_TOOLTIP_I18N[status]
const statusLabel = statusI18nKey ? t(statusI18nKey, { ns: 'common', defaultValue: statusI18nKey }) : null
const tooltipLabel = tooltipI18nKey ? t(tooltipI18nKey, { ns: 'common', defaultValue: tooltipI18nKey }) : null
const isCreditsExhausted = status === 'credits-exhausted'
const shouldShowModelMeta = status === 'active'
@ -118,7 +120,7 @@ const ModelSelectorTrigger: FC<ModelSelectorTriggerProps> = ({
{isSelected && !readonly && !isActive && statusI18nKey && (
<Tooltip>
<TooltipTrigger
disabled={!tooltipI18nKey}
disabled={!tooltipLabel}
render={(
<div
className={cn(
@ -128,14 +130,14 @@ const ModelSelectorTrigger: FC<ModelSelectorTriggerProps> = ({
>
<span className="i-ri-alert-fill h-3 w-3 text-text-warning" />
<span className="whitespace-nowrap text-text-warning system-xs-medium">
{t(statusI18nKey as 'modelProvider.selector.creditsExhausted', { ns: 'common' })}
{statusLabel}
</span>
</div>
)}
/>
{tooltipI18nKey && (
{tooltipLabel && (
<TooltipContent placement="top">
{t(tooltipI18nKey as 'modelProvider.selector.incompatibleTip', { ns: 'common' })}
{tooltipLabel}
</TooltipContent>
)}
</Tooltip>