diff --git a/src/features/providers/descriptors.ts b/src/features/providers/descriptors.ts index a7e7c25..f023f66 100644 --- a/src/features/providers/descriptors.ts +++ b/src/features/providers/descriptors.ts @@ -76,7 +76,7 @@ export const PROVIDER_DESCRIPTORS: Record = { supportsHeaders: true, supportsExcludedModels: true, supportsPriority: true, - supportsTestModel: false, + supportsTestModel: true, supportsWebsockets: false, supportsCloak: true, supportsApiKeyEntries: false, diff --git a/src/features/providers/sheets/forms/BaseProviderForm.tsx b/src/features/providers/sheets/forms/BaseProviderForm.tsx index 1624f48..0ba8c0a 100644 --- a/src/features/providers/sheets/forms/BaseProviderForm.tsx +++ b/src/features/providers/sheets/forms/BaseProviderForm.tsx @@ -1,6 +1,12 @@ import { useId, useMemo, useState } from 'react'; import { useTranslation } from 'react-i18next'; -import { IconPlus, IconX } from '@/components/ui/icons'; +import { + IconAlertTriangle, + IconCheckCircle2, + IconLoader2, + IconPlus, + IconX, +} from '@/components/ui/icons'; import { Collapsible } from '@/components/ui/Collapsible'; import { hasDisableAllModelsRule } from '@/components/providers/utils'; import type { @@ -16,6 +22,11 @@ import type { ProviderEntryFormInput, ProviderResource, } from '../../types'; +import { + useConnectivityTest, + type ConnectivityErrorMessages, + type ConnectivityState, +} from './useConnectivityTest'; import styles from './sharedForm.module.scss'; export interface BaseProviderFormHandle { @@ -70,7 +81,8 @@ function buildInitialForm( brand === 'claude' ? { mode: '', strictMode: false, sensitiveWordsText: '' } : undefined, - testModel: brand === 'openaiCompatibility' ? '' : undefined, + testModel: + brand === 'openaiCompatibility' || brand === 'claude' ? '' : undefined, apiKeyEntries: brand === 'openaiCompatibility' ? [emptyApiKeyEntry()] : undefined, }; @@ -144,9 +156,35 @@ function buildInitialForm( (cfg as ProviderKeyConfig).cloak?.sensitiveWords?.join('\n') ?? '', } : undefined, + testModel: brand === 'claude' ? '' : undefined, }; } +function ConnectivityStatusIcon({ state }: { state: ConnectivityState }) { + if (state === 'loading') { + return ( + + + + ); + } + if (state === 'success') { + return ( + + + + ); + } + if (state === 'error') { + return ( + + + + ); + } + return null; +} + export function BaseProviderForm({ brand, resource, @@ -163,6 +201,38 @@ export function BaseProviderForm({ ); const [error, setError] = useState(null); + const fallbackApiKey = useMemo(() => { + if (brand !== 'claude' || mode !== 'edit' || !resource) return ''; + return (resource.raw as ProviderKeyConfig | undefined)?.apiKey ?? ''; + }, [brand, mode, resource]); + + const connectivityMessages = useMemo( + () => ({ + baseUrlRequired: t('providersPage.connectivity.baseUrlRequired'), + endpointInvalid: t('providersPage.connectivity.endpointInvalid'), + apiKeyRequired: t('providersPage.connectivity.apiKeyRequired'), + modelRequired: t('providersPage.connectivity.modelRequired'), + timeout: (seconds: number) => + t('providersPage.connectivity.timeout', { seconds }), + requestFailed: t('providersPage.connectivity.requestFailed'), + }), + [t] + ); + + const connectivity = useConnectivityTest( + { + brand, + baseUrl: form.baseUrl, + testModel: form.testModel, + models: form.models, + formHeaders: form.headers, + apiKeyEntries: form.apiKeyEntries, + apiKey: form.apiKey, + fallbackApiKey, + }, + connectivityMessages + ); + const updateField = ( key: K, value: ProviderEntryFormInput[K] @@ -359,6 +429,12 @@ export function BaseProviderForm({
updateField('testModel', e.target.value)} disabled={mutating} /> + {brand === 'claude' ? ( +
+ + + {connectivity.claudeStatus.state === 'success' ? ( + + {t('providersPage.connectivity.success')} + + ) : null} +
+ ) : null} + {brand === 'claude' && connectivity.claudeStatus.state === 'error' ? ( +
+ {connectivity.claudeStatus.message} +
+ ) : null}
) : null} @@ -410,91 +514,133 @@ export function BaseProviderForm({ defaultOpen >
- {apiKeyEntries.map((entry, idx) => ( -
-
- - {t('providersPage.form.apiKeyEntry', { index: idx + 1 })} +
+ -
-
- - - updateField( - 'apiKeyEntries', - apiKeyEntries.map((it, i) => - i === idx ? { ...it, apiKey: e.target.value } : it - ) - ) - } - disabled={mutating} - placeholder={t('providersPage.form.apiKeyCreatePlaceholder')} - /> -
-
- - - updateField( - 'apiKeyEntries', - apiKeyEntries.map((it, i) => - i === idx ? { ...it, proxyUrl: e.target.value } : it - ) - ) - } - disabled={mutating} - placeholder="http://127.0.0.1:7890" - /> -
-
-
+ {apiKeyEntries.map((entry, idx) => { + const status = connectivity.openaiStatuses[idx] ?? { + state: 'idle' as ConnectivityState, + message: '', + }; + return ( +
+
+ + {t('providersPage.form.apiKeyEntry', { index: idx + 1 })} - -