diff --git a/web/app/components/header/account-setting/model-provider-page/provider-added-card/credential-panel.spec.tsx b/web/app/components/header/account-setting/model-provider-page/provider-added-card/credential-panel.spec.tsx index 1b50c30348..852b53047b 100644 --- a/web/app/components/header/account-setting/model-provider-page/provider-added-card/credential-panel.spec.tsx +++ b/web/app/components/header/account-setting/model-provider-page/provider-added-card/credential-panel.spec.tsx @@ -147,7 +147,7 @@ describe('CredentialPanel', () => { expect(screen.getByText(/noAvailableUsage/)).toBeInTheDocument() }) - it('should show "API key required" for api-required-add variant (custom priority, no credentials)', () => { + it('should show "AI credits in use" with warning for credits-fallback (custom priority, no credentials, credits available)', () => { renderWithQueryClient(createProvider({ preferred_provider_type: PreferredProviderTypeEnum.custom, custom_configuration: { @@ -155,10 +155,10 @@ describe('CredentialPanel', () => { available_credentials: [], }, })) - expect(screen.getByText(/apiKeyRequired/)).toBeInTheDocument() + expect(screen.getByText(/aiCreditsInUse/)).toBeInTheDocument() }) - it('should show "API key required" for api-required-configure variant (custom priority, credential exists but name missing)', () => { + it('should show "AI credits in use" with warning for credits-fallback (custom priority, credential unauthorized, credits available)', () => { renderWithQueryClient(createProvider({ preferred_provider_type: PreferredProviderTypeEnum.custom, custom_configuration: { @@ -168,7 +168,18 @@ describe('CredentialPanel', () => { available_credentials: [{ credential_id: 'cred-1' }], }, })) - expect(screen.getByText(/apiKeyRequired/)).toBeInTheDocument() + expect(screen.getByText(/aiCreditsInUse/)).toBeInTheDocument() + }) + + it('should show warning icon for credits-fallback variant', () => { + const { container } = renderWithQueryClient(createProvider({ + preferred_provider_type: PreferredProviderTypeEnum.custom, + custom_configuration: { + status: CustomConfigurationStatusEnum.noConfigure, + available_credentials: [], + }, + })) + expect(container.querySelector('.i-ri-error-warning-fill')).toBeTruthy() }) }) @@ -201,7 +212,8 @@ describe('CredentialPanel', () => { expect(container.querySelector('.i-ri-error-warning-fill')).toBeNull() }) - it('should show red indicator and "Unavailable" for api-unavailable', () => { + it('should show red indicator and "Unavailable" for api-unavailable (exhausted + named unauthorized key)', () => { + mockTrialCredits.isExhausted = true renderWithQueryClient(createProvider({ preferred_provider_type: PreferredProviderTypeEnum.custom, custom_configuration: { @@ -243,6 +255,7 @@ describe('CredentialPanel', () => { }) it('should apply destructive container for api-unavailable variant', () => { + mockTrialCredits.isExhausted = true const { container } = renderWithQueryClient(createProvider({ preferred_provider_type: PreferredProviderTypeEnum.custom, custom_configuration: { @@ -356,7 +369,7 @@ describe('CredentialPanel', () => { expect(screen.getByTestId('model-auth-dropdown')).toHaveAttribute('data-variant', 'api-active') }) - it('should pass api-required-add variant for custom priority with no credentials', () => { + it('should pass credits-fallback variant for custom priority with no credentials and credits available', () => { renderWithQueryClient(createProvider({ preferred_provider_type: PreferredProviderTypeEnum.custom, custom_configuration: { @@ -364,10 +377,10 @@ describe('CredentialPanel', () => { available_credentials: [], }, })) - expect(screen.getByTestId('model-auth-dropdown')).toHaveAttribute('data-variant', 'api-required-add') + expect(screen.getByTestId('model-auth-dropdown')).toHaveAttribute('data-variant', 'credits-fallback') }) - it('should pass api-unavailable variant for custom priority with named but unauthorized key', () => { + it('should pass credits-fallback variant for custom priority with named unauthorized key and credits available', () => { renderWithQueryClient(createProvider({ preferred_provider_type: PreferredProviderTypeEnum.custom, custom_configuration: { @@ -377,7 +390,7 @@ describe('CredentialPanel', () => { available_credentials: [{ credential_id: 'cred-1', credential_name: 'Bad Key' }], }, })) - expect(screen.getByTestId('model-auth-dropdown')).toHaveAttribute('data-variant', 'api-unavailable') + expect(screen.getByTestId('model-auth-dropdown')).toHaveAttribute('data-variant', 'credits-fallback') }) it('should pass no-usage variant when exhausted + credential but unauthorized', () => { diff --git a/web/app/components/header/account-setting/model-provider-page/provider-added-card/credential-panel.tsx b/web/app/components/header/account-setting/model-provider-page/provider-added-card/credential-panel.tsx index 401d07b684..04083eb447 100644 --- a/web/app/components/header/account-setting/model-provider-page/provider-added-card/credential-panel.tsx +++ b/web/app/components/header/account-setting/model-provider-page/provider-added-card/credential-panel.tsx @@ -25,6 +25,7 @@ type CredentialPanelProps = { const TEXT_LABEL_VARIANTS = new Set([ 'credits-active', + 'credits-fallback', 'credits-exhausted', 'no-usage', 'api-required-add', @@ -70,10 +71,11 @@ const CredentialPanel = ({ const { variant, credentialName } = state const isDestructive = isDestructiveVariant(variant) const isTextLabel = TEXT_LABEL_VARIANTS.has(variant) + const needsGap = !isTextLabel || variant === 'credits-fallback' return ( - + {isTextLabel ? : } @@ -92,6 +94,7 @@ const CredentialPanel = ({ const TEXT_LABEL_KEYS = { 'credits-active': 'modelProvider.card.aiCreditsInUse', + 'credits-fallback': 'modelProvider.card.aiCreditsInUse', 'credits-exhausted': 'modelProvider.card.quotaExhausted', 'no-usage': 'modelProvider.card.noAvailableUsage', 'api-required-add': 'modelProvider.card.apiKeyRequired', @@ -104,9 +107,14 @@ function TextLabel({ variant }: { variant: CardVariant }) { const labelKey = TEXT_LABEL_KEYS[variant as keyof typeof TEXT_LABEL_KEYS] return ( - - {t(labelKey, { ns: 'common' })} - + <> + + {t(labelKey, { ns: 'common' })} + + {variant === 'credits-fallback' && ( + + )} + ) } diff --git a/web/app/components/header/account-setting/model-provider-page/provider-added-card/use-credential-panel-state.spec.ts b/web/app/components/header/account-setting/model-provider-page/provider-added-card/use-credential-panel-state.spec.ts index 09094671dd..c8aa4c2a0a 100644 --- a/web/app/components/header/account-setting/model-provider-page/provider-added-card/use-credential-panel-state.spec.ts +++ b/web/app/components/header/account-setting/model-provider-page/provider-added-card/use-credential-panel-state.spec.ts @@ -104,7 +104,7 @@ describe('useCredentialPanelState', () => { expect(result.current.priority).toBe('apiKey') }) - it('should return api-unavailable when API key unauthorized', () => { + it('should return credits-fallback when API key unauthorized and credits available', () => { const provider = createProvider({ preferred_provider_type: PreferredProviderTypeEnum.custom, custom_configuration: { @@ -117,10 +117,10 @@ describe('useCredentialPanelState', () => { const { result } = renderHook(() => useCredentialPanelState(provider)) - expect(result.current.variant).toBe('api-required-configure') + expect(result.current.variant).toBe('credits-fallback') }) - it('should return api-required-add when no credentials exist', () => { + it('should return credits-fallback when no credentials and credits available', () => { const provider = createProvider({ preferred_provider_type: PreferredProviderTypeEnum.custom, custom_configuration: { @@ -131,7 +131,41 @@ describe('useCredentialPanelState', () => { const { result } = renderHook(() => useCredentialPanelState(provider)) - expect(result.current.variant).toBe('api-required-add') + expect(result.current.variant).toBe('credits-fallback') + }) + + it('should return no-usage when no credentials and credits exhausted', () => { + mockTrialCredits.isExhausted = true + mockTrialCredits.credits = 0 + const provider = createProvider({ + preferred_provider_type: PreferredProviderTypeEnum.custom, + custom_configuration: { + status: CustomConfigurationStatusEnum.noConfigure, + available_credentials: [], + }, + }) + + const { result } = renderHook(() => useCredentialPanelState(provider)) + + expect(result.current.variant).toBe('no-usage') + }) + + it('should return api-unavailable when credential with name unauthorized and credits exhausted', () => { + mockTrialCredits.isExhausted = true + mockTrialCredits.credits = 0 + const provider = createProvider({ + preferred_provider_type: PreferredProviderTypeEnum.custom, + custom_configuration: { + status: CustomConfigurationStatusEnum.active, + current_credential_id: undefined, + current_credential_name: 'Bad Key', + available_credentials: [{ credential_id: 'cred-1', credential_name: 'Bad Key' }], + }, + }) + + const { result } = renderHook(() => useCredentialPanelState(provider)) + + expect(result.current.variant).toBe('api-unavailable') }) }) diff --git a/web/app/components/header/account-setting/model-provider-page/provider-added-card/use-credential-panel-state.ts b/web/app/components/header/account-setting/model-provider-page/provider-added-card/use-credential-panel-state.ts index 85c518620d..424837841d 100644 --- a/web/app/components/header/account-setting/model-provider-page/provider-added-card/use-credential-panel-state.ts +++ b/web/app/components/header/account-setting/model-provider-page/provider-added-card/use-credential-panel-state.ts @@ -9,6 +9,7 @@ export type UsagePriority = 'credits' | 'apiKey' | 'apiKeyOnly' export type CardVariant = | 'credits-active' + | 'credits-fallback' | 'credits-exhausted' | 'no-usage' | 'api-fallback' @@ -56,6 +57,13 @@ function deriveVariant( if (hasCredential && authorized) return 'api-active' + + if (priority === 'apiKey' && !isExhausted) + return 'credits-fallback' + + if (priority === 'apiKey' && !hasCredential) + return 'no-usage' + if (hasCredential && !authorized) return credentialName ? 'api-unavailable' : 'api-required-configure' return 'api-required-add'