mirror of
https://github.com/langgenius/dify.git
synced 2026-05-05 18:08:07 +08:00
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:
@ -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()),
|
||||
|
||||
@ -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'
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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')
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
|
||||
Reference in New Issue
Block a user