Files
gt-ai-os-community/apps/control-panel-frontend/src/components/models/EndpointConfigurator.tsx
HackWeasel b9dfb86260 GT AI OS Community Edition v2.0.33
Security hardening release addressing CodeQL and Dependabot alerts:

- Fix stack trace exposure in error responses
- Add SSRF protection with DNS resolution checking
- Implement proper URL hostname validation (replaces substring matching)
- Add centralized path sanitization to prevent path traversal
- Fix ReDoS vulnerability in email validation regex
- Improve HTML sanitization in validation utilities
- Fix capability wildcard matching in auth utilities
- Update glob dependency to address CVE
- Add CodeQL suppression comments for verified false positives

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-12 17:04:45 -05:00

1023 lines
36 KiB
TypeScript

"use client";
import { useState, useEffect } from 'react';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import { Badge } from '@/components/ui/badge';
import { Separator } from '@/components/ui/separator';
import { Switch } from '@/components/ui/switch';
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from '@/components/ui/dialog';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
import { Textarea } from '@/components/ui/textarea';
import {
AlertCircle,
AlertTriangle,
CheckCircle,
TestTube,
RefreshCw,
Wifi,
WifiOff,
ExternalLink,
Clock,
Plus,
Trash2
} from 'lucide-react';
import { useToast } from '@/components/ui/use-toast';
import { Alert, AlertDescription } from '@/components/ui/alert';
interface ProviderConfig {
provider: string;
name: string;
endpoint: string;
enabled: boolean;
health_status: 'healthy' | 'unhealthy' | 'degraded' | 'testing' | 'unknown';
description: string;
is_external: boolean;
requires_api_key: boolean;
last_test?: string;
last_latency_ms?: number;
is_custom?: boolean;
model_type?: 'llm' | 'embedding' | 'both';
is_local_mode?: boolean;
external_endpoint?: string;
}
interface AddEndpointForm {
name: string;
provider: string;
endpoint: string;
description: string;
model_type: 'llm' | 'embedding' | 'both';
is_external: boolean;
requires_api_key: boolean;
}
interface EndpointConfiguratorProps {
showAddDialog?: boolean;
onShowAddDialogChange?: (show: boolean) => void;
}
export default function EndpointConfigurator({
showAddDialog: externalShowAddDialog,
onShowAddDialogChange
}: EndpointConfiguratorProps = {}) {
const [providers, setProviders] = useState<ProviderConfig[]>([]);
const [loading, setLoading] = useState(true);
const [saving, setSaving] = useState<string | null>(null);
const [testing, setTesting] = useState<string | null>(null);
const [internalShowAddDialog, setInternalShowAddDialog] = useState(false);
// Use external state if provided, otherwise use internal state
const showAddDialog = externalShowAddDialog !== undefined ? externalShowAddDialog : internalShowAddDialog;
const setShowAddDialog = onShowAddDialogChange || setInternalShowAddDialog;
const [addForm, setAddForm] = useState<AddEndpointForm>({
name: '',
provider: '',
endpoint: '',
description: '',
model_type: 'llm',
is_external: false,
requires_api_key: true,
});
const { toast } = useToast();
// Initialize with default configurations
useEffect(() => {
const defaultProviders: ProviderConfig[] = [
{
provider: 'nvidia',
name: 'NVIDIA NIM (build.nvidia.com)',
endpoint: 'https://integrate.api.nvidia.com/v1/chat/completions',
enabled: true,
health_status: 'unknown',
description: 'NVIDIA NIM microservices - GPU-accelerated inference on DGX Cloud',
is_external: false,
requires_api_key: true,
is_custom: false,
model_type: 'llm'
},
{
provider: 'ollama-dgx-x86',
name: 'Local Ollama (Ubuntu x86 / DGX ARM)',
endpoint: 'http://ollama-host:11434/v1/chat/completions',
enabled: true,
health_status: 'unknown',
description: 'Local Ollama instance for Ubuntu x86_64 and NVIDIA DGX ARM deployments',
is_external: false,
requires_api_key: false,
is_custom: false,
model_type: 'llm'
},
{
provider: 'ollama-macos',
name: 'Local Ollama (macOS Apple Silicon)',
endpoint: 'http://host.docker.internal:11434/v1/chat/completions',
enabled: true,
health_status: 'unknown',
description: 'Local Ollama instance for macOS Apple Silicon (M1/M2/M3/M4) deployments',
is_external: false,
requires_api_key: false,
is_custom: false,
model_type: 'llm'
},
{
provider: 'groq',
name: 'Groq (api.groq.com)',
endpoint: 'https://api.groq.com/openai/v1/chat/completions',
enabled: true,
health_status: 'healthy',
description: 'Groq Cloud - Ultra-fast LLM inference',
is_external: false,
requires_api_key: true,
last_test: '2025-01-21T10:30:00Z',
is_custom: false,
model_type: 'llm'
},
{
provider: 'bge_m3',
name: 'BGE-M3 Embeddings',
endpoint: 'http://gentwo-vllm-embeddings:8000/v1/embeddings',
enabled: true,
health_status: 'healthy',
description: 'Multilingual embedding model - Local GT Edge deployment',
is_external: false,
requires_api_key: false,
last_test: '2025-01-21T10:32:00Z',
is_custom: false,
model_type: 'embedding',
is_local_mode: true,
external_endpoint: 'http://10.0.1.50:8080'
}
];
// Load custom endpoints from localStorage or API
const savedCustomEndpoints = localStorage.getItem('custom_endpoints');
if (savedCustomEndpoints) {
try {
const customEndpoints = JSON.parse(savedCustomEndpoints);
defaultProviders.push(...customEndpoints);
} catch (error) {
console.error('Failed to load custom endpoints:', error);
}
}
setProviders(defaultProviders);
setLoading(false);
}, []);
// Load BGE-M3 configuration from API
useEffect(() => {
const loadBGEConfig = async () => {
try {
const response = await fetch('/api/v1/models/BAAI%2Fbge-m3', {
headers: {
'Authorization': `Bearer ${localStorage.getItem('authToken')}`,
}
});
if (response.ok) {
const bgeModel = await response.json();
const isLocalMode = bgeModel.config?.is_local_mode ?? true;
const externalEndpoint = bgeModel.config?.external_endpoint || 'http://10.0.1.50:8080';
setProviders(prev => prev.map(p => {
if (p.provider === 'bge_m3') {
return {
...p,
endpoint: bgeModel.endpoint,
enabled: bgeModel.status?.is_active ?? true,
health_status: bgeModel.status?.health_status || 'healthy',
description: isLocalMode
? 'Multilingual embedding model - Local GT Edge deployment'
: 'Multilingual embedding model - External API deployment',
is_external: !isLocalMode,
is_local_mode: isLocalMode,
external_endpoint: externalEndpoint
};
}
return p;
}));
}
} catch (error) {
console.error('Error loading BGE-M3 config from API:', error);
}
};
loadBGEConfig();
}, []);
const getStatusIcon = (status: string) => {
switch (status) {
case 'healthy':
return <CheckCircle className="w-4 h-4 text-green-600" />;
case 'degraded':
return <AlertTriangle className="w-4 h-4 text-yellow-600" />;
case 'unhealthy':
return <AlertCircle className="w-4 h-4 text-red-600" />;
case 'testing':
return <RefreshCw className="w-4 h-4 text-blue-600 animate-spin" />;
default:
return <Clock className="w-4 h-4 text-gray-400" />;
}
};
const getConnectionIcon = (isExternal: boolean) => {
return isExternal ? (
<Wifi className="w-4 h-4 text-blue-600" />
) : (
<WifiOff className="w-4 h-4 text-gray-600" />
);
};
const handleEndpointChange = (provider: string, newEndpoint: string) => {
setProviders(prev => prev.map(p =>
p.provider === provider
? { ...p, endpoint: newEndpoint }
: p
));
};
const handleToggleProvider = async (provider: string, enabled: boolean) => {
setProviders(prev => prev.map(p =>
p.provider === provider
? { ...p, enabled: !enabled }
: p
));
toast({
title: `Provider ${!enabled ? 'Enabled' : 'Disabled'}`,
description: `${provider} has been ${!enabled ? 'enabled' : 'disabled'}`,
});
};
const handleTestEndpoint = async (providerConfig: ProviderConfig) => {
setTesting(providerConfig.provider);
setProviders(prev => prev.map(p =>
p.provider === providerConfig.provider
? { ...p, health_status: 'testing' }
: p
));
try {
// Determine which endpoint to test based on mode
const endpointToTest = providerConfig.is_local_mode
? providerConfig.endpoint
: (providerConfig.external_endpoint || providerConfig.endpoint);
// Test endpoint connectivity via backend API
const response = await fetch('/api/v1/models/test-endpoint', {
method: 'POST',
headers: {
'Authorization': `Bearer ${localStorage.getItem('authToken')}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
endpoint: endpointToTest,
provider: providerConfig.provider
})
});
const result = await response.json();
const healthy = result.healthy || false;
const status = result.status || (healthy ? 'healthy' : 'unhealthy');
setProviders(prev => prev.map(p =>
p.provider === providerConfig.provider
? {
...p,
health_status: status as 'healthy' | 'unhealthy' | 'degraded' | 'testing' | 'unknown',
last_test: new Date().toISOString(),
last_latency_ms: result.latency_ms
}
: p
));
// Build toast message based on status
let toastTitle = "Endpoint Healthy";
let toastDescription = `${providerConfig.name} is responding correctly`;
let toastVariant: "default" | "destructive" = "default";
if (status === 'degraded') {
toastTitle = "Endpoint Degraded";
toastDescription = result.error || `${providerConfig.name} responding with high latency`;
if (result.latency_ms) {
toastDescription += ` (${result.latency_ms.toFixed(0)}ms)`;
}
} else if (status === 'unhealthy' || !healthy) {
toastTitle = "Endpoint Unhealthy";
toastDescription = result.error || `${providerConfig.name} is not responding`;
toastVariant = "destructive";
} else if (result.latency_ms) {
toastDescription += ` (${result.latency_ms.toFixed(0)}ms)`;
}
toast({
title: toastTitle,
description: toastDescription,
variant: toastVariant,
});
} catch (error) {
setProviders(prev => prev.map(p =>
p.provider === providerConfig.provider
? { ...p, health_status: 'unhealthy' }
: p
));
toast({
title: "Test Failed",
description: "Failed to test endpoint",
variant: "destructive",
});
}
setTesting(null);
};
const handleAddCustomEndpoint = async () => {
if (!addForm.name || !addForm.provider || !addForm.endpoint) {
toast({
title: "Missing Information",
description: "Please fill in all required fields",
variant: "destructive",
});
return;
}
const newEndpoint: ProviderConfig = {
provider: addForm.provider.toLowerCase().replace(/\s+/g, '_'),
name: addForm.name,
endpoint: addForm.endpoint,
enabled: true,
health_status: 'unknown',
description: addForm.description,
is_external: addForm.is_external,
requires_api_key: addForm.requires_api_key,
is_custom: true,
model_type: addForm.model_type,
};
const updatedProviders = [...providers, newEndpoint];
setProviders(updatedProviders);
// Save custom endpoints to localStorage
// Security Note: This stores endpoint URLs and configuration, not API keys or secrets.
// API keys are managed server-side in the Control Panel backend.
const customEndpoints = updatedProviders.filter(p => p.is_custom);
localStorage.setItem('custom_endpoints', JSON.stringify(customEndpoints));
toast({
title: "Endpoint Added",
description: `Successfully added ${addForm.name}`,
});
// Reset form and close dialog
setAddForm({
name: '',
provider: '',
endpoint: '',
description: '',
model_type: 'llm',
is_external: false,
requires_api_key: true,
});
setShowAddDialog(false);
};
const handleRemoveCustomEndpoint = (provider: string) => {
const updatedProviders = providers.filter(p => p.provider !== provider);
setProviders(updatedProviders);
// Update localStorage
const customEndpoints = updatedProviders.filter(p => p.is_custom);
localStorage.setItem('custom_endpoints', JSON.stringify(customEndpoints));
toast({
title: "Endpoint Removed",
description: "Custom endpoint has been removed",
});
};
const handleToggleBGEM3Mode = async (isLocal: boolean) => {
const provider = providers.find(p => p.provider === 'bge_m3');
if (!provider) return;
try {
// Get current BGE-M3 model config
const modelResponse = await fetch('/api/v1/models/BAAI%2Fbge-m3', {
method: 'GET',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${localStorage.getItem('authToken')}`,
}
});
if (!modelResponse.ok) {
throw new Error('Failed to get current BGE-M3 model config');
}
const currentModel = await modelResponse.json();
const externalEndpoint = provider.external_endpoint || 'http://10.0.1.50:8080';
// Update the model config with new mode
const response = await fetch(`/api/v1/models/${encodeURIComponent('BAAI/bge-m3')}`, {
method: 'PATCH',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${localStorage.getItem('authToken')}`,
},
body: JSON.stringify({
endpoint: isLocal ? 'http://gentwo-vllm-embeddings:8000/v1/embeddings' : externalEndpoint,
config: {
...currentModel.config,
is_local_mode: isLocal,
external_endpoint: externalEndpoint
}
})
});
if (response.ok) {
const result = await response.json();
// Sync configuration to tenant backend
try {
const syncResponse = await fetch('http://localhost:8002/api/embeddings/config/bge-m3', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
is_local_mode: isLocal,
external_endpoint: externalEndpoint
})
});
if (syncResponse.ok) {
console.log('Configuration synced to tenant backend');
} else {
console.warn('Failed to sync configuration to tenant backend');
}
} catch (syncError) {
console.warn('Error syncing to tenant backend:', syncError);
}
setProviders(prev => prev.map(p => {
if (p.provider === 'bge_m3') {
const updatedProvider = {
...p,
is_local_mode: isLocal,
endpoint: isLocal
? 'http://gentwo-vllm-embeddings:8000/v1/embeddings'
: p.external_endpoint || 'http://10.0.1.50:8080',
is_external: !isLocal,
description: isLocal
? 'Multilingual embedding model - Local GT Edge deployment'
: 'Multilingual embedding model - External API deployment'
};
// Save BGE-M3 configuration to localStorage as backup
localStorage.setItem('bge_m3_config', JSON.stringify({
is_local_mode: isLocal,
external_endpoint: p.external_endpoint || 'http://10.0.1.50:8080'
}));
return updatedProvider;
}
return p;
}));
// Show success message with sync status
const syncStatus = result.sync_status;
if (syncStatus === 'success') {
toast({
title: "BGE-M3 Mode Updated",
description: `Switched to ${isLocal ? 'Local GT Edge' : 'External API'} deployment. Configuration synced to all services.`,
});
} else {
toast({
title: "BGE-M3 Mode Updated",
description: `Switched to ${isLocal ? 'Local GT Edge' : 'External API'} deployment. Warning: Some services may not have received the update.`,
variant: "destructive",
});
}
} else {
throw new Error('Failed to update configuration');
}
} catch (error) {
console.error('Error updating BGE-M3 config:', error);
toast({
title: "Configuration Update Failed",
description: "Failed to save BGE-M3 configuration to server",
variant: "destructive",
});
}
};
// Immediate state update for UI responsiveness
const handleExternalEndpointChange = (newEndpoint: string) => {
// Update UI immediately
setProviders(prev => prev.map(p => {
if (p.provider === 'bge_m3') {
return {
...p,
external_endpoint: newEndpoint,
endpoint: !p.is_local_mode ? newEndpoint : p.endpoint
};
}
return p;
}));
};
// Debounced API call to persist configuration
const handleUpdateExternalEndpoint = async (newEndpoint: string) => {
const provider = providers.find(p => p.provider === 'bge_m3');
if (!provider) return;
try {
// Update the BGE-M3 model config using standard model API
const modelResponse = await fetch('/api/v1/models/BAAI%2Fbge-m3', {
method: 'GET',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${localStorage.getItem('authToken')}`,
}
});
if (!modelResponse.ok) {
throw new Error('Failed to get current BGE-M3 model config');
}
const currentModel = await modelResponse.json();
// Update the model config with new endpoint configuration
const response = await fetch(`/api/v1/models/${encodeURIComponent('BAAI/bge-m3')}`, {
method: 'PATCH',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${localStorage.getItem('authToken')}`,
},
body: JSON.stringify({
endpoint: provider.is_local_mode ? 'http://gentwo-vllm-embeddings:8000/v1/embeddings' : newEndpoint,
config: {
...currentModel.config,
is_local_mode: provider.is_local_mode,
external_endpoint: newEndpoint
}
})
});
if (response.ok) {
const result = await response.json();
console.log('BGE-M3 configuration updated:', result);
// Sync configuration to tenant backend
try {
const syncResponse = await fetch('http://localhost:8002/api/embeddings/config/bge-m3', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
is_local_mode: provider.is_local_mode,
external_endpoint: newEndpoint
})
});
if (syncResponse.ok) {
console.log('Configuration synced to tenant backend');
} else {
console.warn('Failed to sync configuration to tenant backend');
}
} catch (syncError) {
console.warn('Error syncing to tenant backend:', syncError);
}
// Update localStorage as backup
localStorage.setItem('bge_m3_config', JSON.stringify({
is_local_mode: provider.is_local_mode,
external_endpoint: newEndpoint
}));
toast({
title: "Configuration Updated",
description: "BGE-M3 external endpoint updated successfully",
});
}
} catch (error) {
console.error('Error updating BGE-M3 external endpoint:', error);
// Still update locally even if API call fails
setProviders(prev => prev.map(p => {
if (p.provider === 'bge_m3') {
const updatedProvider = {
...p,
external_endpoint: newEndpoint,
endpoint: !p.is_local_mode ? newEndpoint : p.endpoint
};
localStorage.setItem('bge_m3_config', JSON.stringify({
is_local_mode: p.is_local_mode,
external_endpoint: newEndpoint
}));
return updatedProvider;
}
return p;
}));
}
};
const handleSaveConfiguration = async () => {
setSaving('all');
try {
// TODO: API call to save all configurations
await new Promise(resolve => setTimeout(resolve, 1000));
toast({
title: "Configuration Saved",
description: "All endpoint configurations have been saved and synced to resource clusters",
});
} catch (error) {
toast({
title: "Save Failed",
description: "Failed to save configuration changes",
variant: "destructive",
});
}
setSaving(null);
};
if (loading) {
return <div className="flex items-center justify-center p-8">Loading configurations...</div>;
}
return (
<div className="space-y-6">
{/* Provider Configurations */}
<div className="grid gap-4">
{providers.map((provider) => (
<Card key={provider.provider} className="relative">
<CardHeader className="pb-3">
<div className="flex items-center justify-between">
<div className="flex items-center gap-3">
<div className="flex items-center gap-2">
{getConnectionIcon(provider.is_external)}
<CardTitle className="text-lg">{provider.name}</CardTitle>
</div>
<div className="flex items-center gap-2">
{getStatusIcon(provider.health_status)}
<Badge variant={provider.enabled ? "default" : "secondary"}>
{provider.enabled ? "Enabled" : "Disabled"}
</Badge>
{provider.requires_api_key && (
<Badge variant="outline">API Key Required</Badge>
)}
{provider.model_type && (
<Badge variant="secondary">{provider.model_type.toUpperCase()}</Badge>
)}
</div>
</div>
<div className="flex items-center gap-2">
<Switch
checked={provider.enabled}
onCheckedChange={(checked) => handleToggleProvider(provider.provider, provider.enabled)}
/>
{provider.is_custom && (
<Button
variant="ghost"
size="sm"
onClick={() => handleRemoveCustomEndpoint(provider.provider)}
className="text-red-600 hover:text-red-700 hover:bg-red-50"
>
<Trash2 className="w-4 h-4" />
</Button>
)}
</div>
</div>
<CardDescription>{provider.description}</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
{provider.provider === 'bge_m3' ? (
// Special BGE-M3 configuration with local/external toggle
<>
<div className="space-y-4">
<div className="flex items-center justify-between p-3 border rounded-lg bg-blue-50/50">
<div className="flex items-center gap-3">
<div className="flex items-center gap-2">
{provider.is_local_mode ? (
<div className="w-2 h-2 bg-green-500 rounded-full"></div>
) : (
<div className="w-2 h-2 bg-blue-500 rounded-full"></div>
)}
<span className="font-medium">
{provider.is_local_mode ? 'Local GT Edge' : 'External API'}
</span>
</div>
<Badge variant="secondary" className="text-xs">
{provider.is_local_mode ? 'Docker Internal' : 'External Endpoint'}
</Badge>
</div>
<Switch
checked={!provider.is_local_mode}
onCheckedChange={(checked) => handleToggleBGEM3Mode(!checked)}
disabled={!provider.enabled}
/>
</div>
<div className="grid grid-cols-1 gap-4">
<div>
<Label htmlFor={`endpoint-${provider.provider}`}>
{provider.is_local_mode ? 'Local Endpoint (Docker Internal)' : 'External Endpoint URL'}
</Label>
<Input
id={`endpoint-${provider.provider}`}
value={provider.is_local_mode ? provider.endpoint : (provider.external_endpoint || '')}
onChange={(e) => {
if (provider.is_local_mode) {
handleEndpointChange(provider.provider, e.target.value);
} else {
// Update UI immediately
handleExternalEndpointChange(e.target.value);
}
}}
onBlur={(e) => {
// Call API when user finishes editing (loses focus)
if (!provider.is_local_mode) {
handleUpdateExternalEndpoint(e.target.value);
}
}}
placeholder={provider.is_local_mode ? 'http://gentwo-vllm-embeddings:8000/v1/embeddings' : 'http://10.0.1.50:8080'}
disabled={!provider.enabled || provider.is_local_mode}
className={provider.is_local_mode ? "bg-gray-100" : "border-blue-200 bg-blue-50"}
/>
{provider.is_local_mode && (
<p className="text-xs text-muted-foreground mt-1">
🏠 Uses local Docker container for embeddings
</p>
)}
{!provider.is_local_mode && (
<p className="text-xs text-blue-600 mt-1">
🌐 External BGE-M3 API endpoint (same model, external deployment)
</p>
)}
</div>
</div>
</div>
<div className="flex gap-2">
<Button
variant="outline"
onClick={() => handleTestEndpoint(provider)}
disabled={!provider.enabled || testing === provider.provider}
className="flex-1"
>
{testing === provider.provider ? (
<>
<RefreshCw className="w-4 h-4 mr-2 animate-spin" />
Testing...
</>
) : (
<>
<TestTube className="w-4 h-4 mr-2" />
Test {provider.is_local_mode ? 'Local' : 'External'}
</>
)}
</Button>
{provider.endpoint && (
<Button
variant="ghost"
size="icon"
onClick={() => window.open(provider.is_local_mode ? provider.endpoint : provider.external_endpoint, '_blank')}
disabled={!provider.enabled}
>
<ExternalLink className="w-4 h-4" />
</Button>
)}
</div>
</>
) : (
// Regular provider configuration
<>
<div className="grid grid-cols-1 md:grid-cols-3 gap-4 items-end">
<div className="md:col-span-2">
<Label htmlFor={`endpoint-${provider.provider}`}>
Endpoint URL
{provider.is_external && (
<span className="text-blue-600 ml-1">(GT Edge Network)</span>
)}
{!provider.is_custom && (
<span className="text-muted-foreground ml-1 text-xs">(Default)</span>
)}
</Label>
<Input
id={`endpoint-${provider.provider}`}
value={provider.endpoint}
onChange={(e) => provider.is_custom && handleEndpointChange(provider.provider, e.target.value)}
placeholder="https://api.example.com/v1"
disabled={!provider.enabled || !provider.is_custom}
readOnly={!provider.is_custom}
className={`${provider.is_external ? "border-blue-200 bg-blue-50" : ""} ${!provider.is_custom ? "bg-muted cursor-not-allowed" : ""}`}
/>
</div>
<div className="flex gap-2">
<Button
variant="outline"
onClick={() => handleTestEndpoint(provider)}
disabled={!provider.enabled || testing === provider.provider}
className="flex-1"
>
{testing === provider.provider ? (
<>
<RefreshCw className="w-4 h-4 mr-2 animate-spin" />
Testing...
</>
) : (
<>
<TestTube className="w-4 h-4 mr-2" />
Test
</>
)}
</Button>
{provider.endpoint && (
<Button
variant="ghost"
size="icon"
onClick={() => window.open(provider.endpoint, '_blank')}
disabled={!provider.enabled}
>
<ExternalLink className="w-4 h-4" />
</Button>
)}
</div>
</div>
</>
)}
{provider.last_test && (
<div className="text-xs text-muted-foreground">
Last tested: {new Date(provider.last_test).toLocaleString()}
{provider.last_latency_ms && (
<span className="ml-2">({provider.last_latency_ms.toFixed(0)}ms)</span>
)}
</div>
)}
</CardContent>
</Card>
))}
</div>
<Separator />
{/* Global Actions */}
<div className="flex items-center justify-between">
<div className="text-sm text-muted-foreground">
Changes will be automatically synced to all resource clusters
</div>
<div className="flex gap-2">
<Button
variant="outline"
onClick={() => window.location.reload()}
>
<RefreshCw className="w-4 h-4 mr-2" />
Refresh
</Button>
<Button
onClick={handleSaveConfiguration}
disabled={saving === 'all'}
>
{saving === 'all' ? (
<>
<RefreshCw className="w-4 h-4 mr-2 animate-spin" />
Saving...
</>
) : (
'Save Configuration'
)}
</Button>
</div>
</div>
{/* Add Custom Endpoint Dialog */}
<Dialog open={showAddDialog} onOpenChange={setShowAddDialog}>
<DialogContent className="sm:max-w-md">
<DialogHeader>
<DialogTitle>Add Custom Endpoint</DialogTitle>
<DialogDescription>
Create a custom endpoint that can be used when adding models to the registry.
</DialogDescription>
</DialogHeader>
<div className="space-y-4">
<div>
<Label htmlFor="endpoint-name">Endpoint Name *</Label>
<Input
id="endpoint-name"
placeholder="e.g., My Local LLM Server"
value={addForm.name}
onChange={(e) => setAddForm(prev => ({ ...prev, name: e.target.value }))}
/>
</div>
<div>
<Label htmlFor="provider-name">Provider ID *</Label>
<Input
id="provider-name"
placeholder="e.g., local-llm (lowercase, no spaces)"
value={addForm.provider}
onChange={(e) => setAddForm(prev => ({ ...prev, provider: e.target.value }))}
/>
<p className="text-xs text-muted-foreground mt-1">
Used as identifier - lowercase letters, numbers, and underscores only
</p>
</div>
<div>
<Label htmlFor="endpoint-url">Endpoint URL *</Label>
<Input
id="endpoint-url"
placeholder="https://api.example.com/v1 or http://localhost:8080"
value={addForm.endpoint}
onChange={(e) => setAddForm(prev => ({ ...prev, endpoint: e.target.value }))}
/>
</div>
<div>
<Label htmlFor="description">Description</Label>
<Textarea
id="description"
placeholder="Brief description of this endpoint..."
value={addForm.description}
onChange={(e) => setAddForm(prev => ({ ...prev, description: e.target.value }))}
rows={2}
/>
</div>
<div>
<Label htmlFor="model-type">Model Type</Label>
<Select value={addForm.model_type} onValueChange={(value: 'llm' | 'embedding' | 'both') =>
setAddForm(prev => ({ ...prev, model_type: value }))
}>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="llm">LLM (Chat/Completion)</SelectItem>
<SelectItem value="embedding">Embedding</SelectItem>
<SelectItem value="both">Both LLM & Embedding</SelectItem>
</SelectContent>
</Select>
</div>
<div className="space-y-3">
<div className="flex items-center space-x-2">
<input
type="checkbox"
id="external-network"
checked={addForm.is_external}
onChange={(e) => setAddForm(prev => ({ ...prev, is_external: e.target.checked }))}
className="rounded"
/>
<Label htmlFor="external-network" className="text-sm">
External network endpoint (GT Edge, local network)
</Label>
</div>
<div className="flex items-center space-x-2">
<input
type="checkbox"
id="requires-api-key"
checked={addForm.requires_api_key}
onChange={(e) => setAddForm(prev => ({ ...prev, requires_api_key: e.target.checked }))}
className="rounded"
/>
<Label htmlFor="requires-api-key" className="text-sm">
Requires API key authentication
</Label>
</div>
</div>
</div>
<DialogFooter>
<Button variant="outline" onClick={() => setShowAddDialog(false)}>
Cancel
</Button>
<Button onClick={handleAddCustomEndpoint}>
Add Endpoint
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</div>
);
}