diff --git a/frontend/src/lib/api/providers.ts b/frontend/src/lib/api/providers.ts index daa6c968..06134895 100644 --- a/frontend/src/lib/api/providers.ts +++ b/frontend/src/lib/api/providers.ts @@ -1,5 +1,10 @@ import { apiClient } from './client'; -import type { ProviderCreateRequest, ProviderResponse, ProviderUpdateRequest } from './types'; +import type { + ProviderCreateRequest, + ProviderResponse, + ProviderTestResult, + ProviderUpdateRequest +} from './types'; export async function listProviders(): Promise { return apiClient.request({ @@ -15,3 +20,14 @@ export async function createProvider(data: ProviderCreateRequest): Promise { return apiClient.request({ method: 'PUT', path: `/providers/${id}`, body: data }); } + +export async function testProvider(id: string): Promise { + return apiClient.request({ + method: 'POST', + path: `/providers/${id}/test` + }); +} + +export async function deleteProvider(id: string): Promise { + return apiClient.request({ method: 'DELETE', path: `/providers/${id}` }); +} diff --git a/frontend/src/lib/api/types.ts b/frontend/src/lib/api/types.ts index 24791f99..1d0593f2 100644 --- a/frontend/src/lib/api/types.ts +++ b/frontend/src/lib/api/types.ts @@ -221,6 +221,12 @@ export interface SystemHealthResponse { } // --- Providers --- +export interface ProviderTestResult { + response_text: string; + model: string; + duration_ms: number; +} + export interface ProviderResponse { id: string; name: string; diff --git a/frontend/src/lib/components/providers/ProviderCard.svelte b/frontend/src/lib/components/providers/ProviderCard.svelte index 90568b5f..abbbc9f5 100644 --- a/frontend/src/lib/components/providers/ProviderCard.svelte +++ b/frontend/src/lib/components/providers/ProviderCard.svelte @@ -4,6 +4,7 @@ import { Card, CardHeader, CardTitle, CardDescription, CardContent } from '$lib/components/ui/card'; import { Badge } from '$lib/components/ui/badge'; import { Globe, Key, Pencil } from 'lucide-svelte'; + import TestButton from './TestButton.svelte'; let { provider, @@ -44,5 +45,8 @@ {/if} +
+ +
diff --git a/frontend/src/lib/components/providers/ProviderForm.svelte b/frontend/src/lib/components/providers/ProviderForm.svelte index ddb5b6b0..098860a8 100644 --- a/frontend/src/lib/components/providers/ProviderForm.svelte +++ b/frontend/src/lib/components/providers/ProviderForm.svelte @@ -4,7 +4,9 @@ import { Label } from '$lib/components/ui/label'; import { Select, SelectOption } from '$lib/components/ui/select'; import { Button } from '$lib/components/ui/button'; - import type { ProviderCreateRequest, ProviderResponse, ProviderUpdateRequest } from '$lib/api/types'; + import { CircleX } from 'lucide-svelte'; + import type { ProviderCreateRequest, ProviderResponse, ProviderTestResult, ProviderUpdateRequest } from '$lib/api/types'; + import { testProvider, deleteProvider } from '$lib/api/providers'; const PROVIDER_TYPES = [ 'openai', 'anthropic', 'google', 'azure', 'bedrock', @@ -52,7 +54,7 @@ }: { open?: boolean; provider?: ProviderResponse; - onsave: (data: ProviderCreateRequest | ProviderUpdateRequest) => Promise; + onsave: (data: ProviderCreateRequest | ProviderUpdateRequest) => Promise; } = $props(); let name = $state(''); @@ -62,6 +64,11 @@ let isSubmitting = $state(false); let errors = $state>({}); + // Test state (create mode only) + let testResult = $state(null); + let testError = $state(null); + let isTesting = $state(false); + // Config field state let timeoutMs = $state(''); let extraHeaders = $state<{ name: string; value: string }[]>([]); @@ -89,6 +96,9 @@ region = ''; errors = {}; isSubmitting = false; + testResult = null; + testError = null; + isTesting = false; } function populateFromProvider(p: ProviderResponse) { @@ -193,20 +203,22 @@ e.preventDefault(); if (!validate()) return; + testError = null; isSubmitting = true; try { - const newConfig = buildConfigJson(); + const configJson = buildConfigJson(); if (isEdit && provider) { const updateData: ProviderUpdateRequest = {}; if (name.trim() !== provider.name) updateData.name = name.trim(); if (baseUrl.trim() !== provider.base_url) updateData.base_url = baseUrl.trim(); if (apiKey.trim()) updateData.api_key = apiKey.trim(); - if (Object.keys(newConfig).length > 0) { - updateData.config_json = newConfig; + if (Object.keys(configJson).length > 0) { + updateData.config_json = configJson; } else if (Object.keys(provider.config_json).length > 0) { updateData.config_json = {}; } await onsave(updateData); + open = false; } else { const createData: ProviderCreateRequest = { name: name.trim(), @@ -214,12 +226,30 @@ base_url: baseUrl.trim(), api_key: apiKey.trim() }; - if (Object.keys(newConfig).length > 0) { - createData.config_json = newConfig; + if (Object.keys(configJson).length > 0) { + createData.config_json = configJson; + } + const created = await onsave(createData); + isSubmitting = false; + isTesting = true; + try { + testResult = await testProvider(created.id); + // Test passed — close dialog + open = false; + } catch (err) { + const msg = err instanceof Error ? err.message : 'Test failed'; + testError = msg; + // Rollback: delete the provider + try { + await deleteProvider(created.id); + } catch { + // Silently ignore delete failure + } + } finally { + isTesting = false; } - await onsave(createData); + return; } - open = false; } catch (err) { errors.form = err instanceof Error ? err.message : 'Save failed'; } finally { @@ -249,6 +279,13 @@

{errors.form}

{/if} + {#if testError} +
+ + Connection failed: {testError}. The provider has been removed. +
+ {/if} +
-
diff --git a/frontend/src/lib/components/providers/TestButton.svelte b/frontend/src/lib/components/providers/TestButton.svelte new file mode 100644 index 00000000..ae7bd4ab --- /dev/null +++ b/frontend/src/lib/components/providers/TestButton.svelte @@ -0,0 +1,28 @@ + + +
+ {#if testState.state === 'idle'} + + {:else if testState.state === 'testing'} + + Testing... + {:else if testState.state === 'success' && testState.result} + + Working + {testState.result.model} + {testState.result.duration_ms}ms + + {:else if testState.state === 'error'} + + {testState.errorMessage} + + {/if} +
diff --git a/frontend/src/lib/components/providers/TestButtonState.svelte.ts b/frontend/src/lib/components/providers/TestButtonState.svelte.ts new file mode 100644 index 00000000..fdd5e3d8 --- /dev/null +++ b/frontend/src/lib/components/providers/TestButtonState.svelte.ts @@ -0,0 +1,36 @@ +import { testProvider } from '$lib/api/providers'; +import type { ProviderTestResult } from '$lib/api/types'; + +export type TestState = 'idle' | 'testing' | 'success' | 'error'; + +export function createTestButtonState(getProviderId: () => string) { + let state = $state('idle'); + let result = $state(null); + let errorMessage = $state(''); + + async function handleTest() { + state = 'testing'; + result = null; + errorMessage = ''; + try { + result = await testProvider(getProviderId()); + state = 'success'; + } catch (err) { + errorMessage = err instanceof Error ? err.message : 'Test failed'; + state = 'error'; + } + } + + return { + get state() { + return state; + }, + get result() { + return result; + }, + get errorMessage() { + return errorMessage; + }, + handleTest + }; +} diff --git a/frontend/src/routes/(app)/providers/+page.svelte b/frontend/src/routes/(app)/providers/+page.svelte index 131b7786..89411f97 100644 --- a/frontend/src/routes/(app)/providers/+page.svelte +++ b/frontend/src/routes/(app)/providers/+page.svelte @@ -36,13 +36,17 @@ showForm = true; } - async function handleSave(data: ProviderCreateRequest | ProviderUpdateRequest) { + async function handleSave(data: ProviderCreateRequest | ProviderUpdateRequest): Promise { if (editingProvider) { - await updateProvider(editingProvider.id, data as ProviderUpdateRequest); + const updated = await updateProvider(editingProvider.id, data as ProviderUpdateRequest); + showForm = false; + await loadProviders(); + return updated; } else { - await createProvider(data as ProviderCreateRequest); + const created = await createProvider(data as ProviderCreateRequest); + await loadProviders(); + return created; } - await loadProviders(); } onMount(() => {