From ce0197b1078bea3edf81403b2bcc9fcb756bf768 Mon Sep 17 00:00:00 2001 From: CodingOnStar Date: Mon, 9 Mar 2026 18:19:45 +0800 Subject: [PATCH] fix(provider): handle undefined provider in credential status and panel state --- .../model-auth/hooks/use-credential-status.spec.tsx | 10 ++++++++++ .../model-auth/hooks/use-credential-status.ts | 4 ++-- .../model-selector/popup-item.spec.tsx | 12 ++++++++++++ .../model-selector/popup-item.tsx | 7 ++++++- .../model-provider-page/model-selector/popup.tsx | 10 +++++----- .../use-change-provider-priority.ts | 8 ++++---- .../use-credential-panel-state.spec.ts | 12 ++++++++++++ .../use-credential-panel-state.ts | 8 ++++---- 8 files changed, 55 insertions(+), 16 deletions(-) diff --git a/web/app/components/header/account-setting/model-provider-page/model-auth/hooks/use-credential-status.spec.tsx b/web/app/components/header/account-setting/model-provider-page/model-auth/hooks/use-credential-status.spec.tsx index c84b452bb2..15b195b83b 100644 --- a/web/app/components/header/account-setting/model-provider-page/model-auth/hooks/use-credential-status.spec.tsx +++ b/web/app/components/header/account-setting/model-provider-page/model-auth/hooks/use-credential-status.spec.tsx @@ -53,4 +53,14 @@ describe('useCredentialStatus', () => { expect(result.current.hasCredential).toBe(false) expect(result.current.available_credentials).toBeUndefined() }) + + it('handles undefined provider gracefully', () => { + const { result } = renderHook(() => useCredentialStatus(undefined)) + expect(result.current.hasCredential).toBe(false) + expect(result.current.authorized).toBeFalsy() + expect(result.current.authRemoved).toBe(false) + expect(result.current.available_credentials).toBeUndefined() + expect(result.current.current_credential_id).toBeUndefined() + expect(result.current.current_credential_name).toBeUndefined() + }) }) diff --git a/web/app/components/header/account-setting/model-provider-page/model-auth/hooks/use-credential-status.ts b/web/app/components/header/account-setting/model-provider-page/model-auth/hooks/use-credential-status.ts index 263940afcb..84ac587656 100644 --- a/web/app/components/header/account-setting/model-provider-page/model-auth/hooks/use-credential-status.ts +++ b/web/app/components/header/account-setting/model-provider-page/model-auth/hooks/use-credential-status.ts @@ -3,12 +3,12 @@ import type { } from '../../declarations' import { useMemo } from 'react' -export const useCredentialStatus = (provider: ModelProvider) => { +export const useCredentialStatus = (provider: ModelProvider | undefined) => { const { current_credential_id, current_credential_name, available_credentials, - } = provider.custom_configuration + } = provider?.custom_configuration ?? {} const hasCredential = !!available_credentials?.length const authorized = current_credential_id && current_credential_name const authRemoved = hasCredential && !current_credential_id && !current_credential_name diff --git a/web/app/components/header/account-setting/model-provider-page/model-selector/popup-item.spec.tsx b/web/app/components/header/account-setting/model-provider-page/model-selector/popup-item.spec.tsx index 8c6909c3bb..b4e9220cfc 100644 --- a/web/app/components/header/account-setting/model-provider-page/model-selector/popup-item.spec.tsx +++ b/web/app/components/header/account-setting/model-provider-page/model-selector/popup-item.spec.tsx @@ -128,6 +128,18 @@ describe('PopupItem', () => { }) }) + it('should render nothing when provider is not found in modelProviders', () => { + mockUseProviderContext.mockReturnValue({ + modelProviders: [], + }) + + const { container } = render( + , + ) + + expect(container.innerHTML).toBe('') + }) + it('should call onSelect when clicking an active model', () => { const onSelect = vi.fn() render() diff --git a/web/app/components/header/account-setting/model-provider-page/model-selector/popup-item.tsx b/web/app/components/header/account-setting/model-provider-page/model-selector/popup-item.tsx index 5ce23c67cd..0ddcef1a37 100644 --- a/web/app/components/header/account-setting/model-provider-page/model-selector/popup-item.tsx +++ b/web/app/components/header/account-setting/model-provider-page/model-selector/popup-item.tsx @@ -59,7 +59,7 @@ const PopupItem: FC = ({ const { modelProviders } = useProviderContext() const updateModelList = useUpdateModelList() const updateModelProviders = useUpdateModelProviders() - const currentProvider = modelProviders.find(provider => provider.provider === model.provider)! + const currentProvider = modelProviders.find(provider => provider.provider === model.provider) const handleSelect = (provider: string, modelItem: ModelItem) => { if (modelItem.status !== ModelStatusEnum.active) return @@ -67,6 +67,8 @@ const PopupItem: FC = ({ onSelect(provider, modelItem) } const handleOpenModelModal = () => { + if (!currentProvider) + return setShowModelModal({ payload: { currentProvider, @@ -96,6 +98,9 @@ const PopupItem: FC = ({ onHide() }, [onHide]) + if (!currentProvider) + return null + return (
diff --git a/web/app/components/header/account-setting/model-provider-page/model-selector/popup.tsx b/web/app/components/header/account-setting/model-provider-page/model-selector/popup.tsx index 6dbeb555da..962174c311 100644 --- a/web/app/components/header/account-setting/model-provider-page/model-selector/popup.tsx +++ b/web/app/components/header/account-setting/model-provider-page/model-selector/popup.tsx @@ -137,11 +137,11 @@ const Popup: FC = ({ }).filter(model => model.models.length > 0) if (defaultModel?.provider) { - const selectedIndex = filtered.findIndex(m => m.provider === defaultModel.provider) - if (selectedIndex > 0) { - const [selected] = filtered.splice(selectedIndex, 1) - filtered.unshift(selected) - } + filtered.sort((a, b) => { + const aSelected = a.provider === defaultModel.provider ? 0 : 1 + const bSelected = b.provider === defaultModel.provider ? 0 : 1 + return aSelected - bSelected + }) } return filtered diff --git a/web/app/components/header/account-setting/model-provider-page/provider-added-card/use-change-provider-priority.ts b/web/app/components/header/account-setting/model-provider-page/provider-added-card/use-change-provider-priority.ts index 10b2c5ed6e..9762f47d74 100644 --- a/web/app/components/header/account-setting/model-provider-page/provider-added-card/use-change-provider-priority.ts +++ b/web/app/components/header/account-setting/model-provider-page/provider-added-card/use-change-provider-priority.ts @@ -6,12 +6,12 @@ import { consoleQuery } from '@/service/client' import { ConfigurationMethodEnum } from '../declarations' import { useUpdateModelList, useUpdateModelProviders } from '../hooks' -export function useChangeProviderPriority(provider: ModelProvider) { +export function useChangeProviderPriority(provider: ModelProvider | undefined) { const { t } = useTranslation() const queryClient = useQueryClient() const updateModelList = useUpdateModelList() const updateModelProviders = useUpdateModelProviders() - const providerName = provider.provider + const providerName = provider?.provider ?? '' const modelProviderModelListQueryKey = consoleQuery.modelProviders.models.queryKey({ input: { @@ -31,9 +31,9 @@ export function useChangeProviderPriority(provider: ModelProvider) { refetchType: 'none', }) updateModelProviders() - provider.configurate_methods.forEach((method) => { + provider?.configurate_methods.forEach((method) => { if (method === ConfigurationMethodEnum.predefinedModel) - provider.supported_model_types.forEach(modelType => updateModelList(modelType)) + provider?.supported_model_types.forEach(modelType => updateModelList(modelType)) }) }, onError: () => { 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 c8aa4c2a0a..7ab55e9616 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 @@ -183,6 +183,18 @@ describe('useCredentialPanelState', () => { }) }) + // Undefined provider + describe('Undefined provider', () => { + it('should return safe defaults when provider is undefined', () => { + const { result } = renderHook(() => useCredentialPanelState(undefined)) + + expect(result.current.priority).toBe('apiKeyOnly') + expect(result.current.supportsCredits).toBe(false) + expect(result.current.hasCredentials).toBe(false) + expect(result.current.credentialName).toBeUndefined() + }) + }) + // Derived metadata describe('Derived metadata', () => { it('should show priority switcher when credits supported and custom config active', () => { 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 4d0f351c6c..16558a0019 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 @@ -70,7 +70,7 @@ function deriveVariant( return 'api-required-add' } -export function useCredentialPanelState(provider: ModelProvider): CredentialPanelState { +export function useCredentialPanelState(provider: ModelProvider | undefined): CredentialPanelState { const { isExhausted, credits } = useTrialCredits() const { hasCredential, @@ -78,10 +78,10 @@ export function useCredentialPanelState(provider: ModelProvider): CredentialPane current_credential_name, } = useCredentialStatus(provider) - const systemConfig = provider.system_configuration - const preferredType = provider.preferred_provider_type + const systemConfig = provider?.system_configuration + const preferredType = provider?.preferred_provider_type - const supportsCredits = systemConfig.enabled && IS_CLOUD_EDITION + const supportsCredits = !!systemConfig?.enabled && IS_CLOUD_EDITION const priority: UsagePriority = !supportsCredits ? 'apiKeyOnly'