// API Settings View // Manages LiteLLM API providers, custom endpoints, and cache settings // ========== State Management ========== let apiSettingsData = null; const providerModels = {}; let currentModal = null; // New state for split layout let selectedProviderId = null; let providerSearchQuery = ''; let activeModelTab = 'llm'; let expandedModelGroups = new Set(); let activeSidebarTab = 'providers'; // 'providers' | 'endpoints' | 'cache' | 'embedding-pool' // Embedding Pool state let embeddingPoolConfig = null; let embeddingPoolAvailableModels = []; let embeddingPoolDiscoveredProviders = []; // Cache for ccw-litellm status (frontend cache with TTL) let ccwLitellmStatusCache = null; let ccwLitellmStatusCacheTime = 0; const CCW_LITELLM_STATUS_CACHE_TTL = 60000; // 60 seconds // Track if this is the first render (force refresh on first load) let isFirstApiSettingsRender = true; // ========== Data Loading ========== /** * Load API configuration * @param {boolean} forceRefresh - Force refresh from server, bypass cache */ async function loadApiSettings(forceRefresh = false) { // If not forcing refresh and data already exists, return cached data if (!forceRefresh && apiSettingsData && apiSettingsData.providers) { console.log('[API Settings] Using cached API settings data'); return apiSettingsData; } try { console.log('[API Settings] Fetching API settings from server...'); const response = await fetch('/api/litellm-api/config'); if (!response.ok) throw new Error('Failed to load API settings'); apiSettingsData = await response.json(); return apiSettingsData; } catch (err) { console.error('Failed to load API settings:', err); showRefreshToast(t('common.error') + ': ' + err.message, 'error'); return null; } } /** * Load available models for a provider type */ async function loadProviderModels(providerType) { try { const response = await fetch('/api/litellm-api/models/' + providerType); if (!response.ok) throw new Error('Failed to load models'); const data = await response.json(); providerModels[providerType] = data.models || []; return data.models; } catch (err) { console.error('Failed to load provider models:', err); return []; } } /** * Load cache statistics */ async function loadCacheStats() { try { const response = await fetch('/api/litellm-api/cache/stats'); if (!response.ok) throw new Error('Failed to load cache stats'); return await response.json(); } catch (err) { console.error('Failed to load cache stats:', err); return { enabled: false, totalSize: 0, maxSize: 104857600, entries: 0 }; } } /** * Load embedding pool configuration and available models */ async function loadEmbeddingPoolConfig() { try { const response = await fetch('/api/litellm-api/embedding-pool'); if (!response.ok) throw new Error('Failed to load embedding pool config'); const data = await response.json(); embeddingPoolConfig = data.poolConfig; embeddingPoolAvailableModels = data.availableModels || []; // If pool is enabled and has a target model, discover providers if (embeddingPoolConfig && embeddingPoolConfig.enabled && embeddingPoolConfig.targetModel) { await discoverProvidersForTargetModel(embeddingPoolConfig.targetModel); } return data; } catch (err) { console.error('Failed to load embedding pool config:', err); showRefreshToast(t('common.error') + ': ' + err.message, 'error'); return null; } } /** * Discover providers for a specific target model */ async function discoverProvidersForTargetModel(targetModel) { try { const response = await fetch('/api/litellm-api/embedding-pool/discover/' + encodeURIComponent(targetModel)); if (!response.ok) throw new Error('Failed to discover providers'); const data = await response.json(); embeddingPoolDiscoveredProviders = data.discovered || []; return data; } catch (err) { console.error('Failed to discover providers:', err); embeddingPoolDiscoveredProviders = []; return null; } } /** * Save embedding pool configuration */ async function saveEmbeddingPoolConfig() { try { const enabled = document.getElementById('embedding-pool-enabled')?.checked || false; const targetModel = document.getElementById('embedding-pool-target-model')?.value || ''; const strategy = document.getElementById('embedding-pool-strategy')?.value || 'round_robin'; const defaultCooldown = parseInt(document.getElementById('embedding-pool-cooldown')?.value || '60'); const defaultMaxConcurrentPerKey = parseInt(document.getElementById('embedding-pool-concurrent')?.value || '4'); const poolConfig = enabled ? { enabled: true, targetModel: targetModel, strategy: strategy, autoDiscover: true, excludedProviderIds: embeddingPoolConfig?.excludedProviderIds || [], defaultCooldown: defaultCooldown, defaultMaxConcurrentPerKey: defaultMaxConcurrentPerKey } : null; const response = await fetch('/api/litellm-api/embedding-pool', { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(poolConfig) }); if (!response.ok) throw new Error('Failed to save embedding pool config'); const result = await response.json(); embeddingPoolConfig = result.poolConfig; const syncCount = result.syncResult?.syncedEndpoints?.length || 0; showRefreshToast(t('apiSettings.poolSaved') + (syncCount > 0 ? ' (' + syncCount + ' endpoints synced)' : ''), 'success'); // Invalidate API settings cache since endpoints may have been synced apiSettingsData = null; // Reload the embedding pool section await renderEmbeddingPoolMainPanel(); // Update sidebar summary const sidebarContainer = document.querySelector('.api-settings-sidebar'); if (sidebarContainer) { const contentArea = sidebarContainer.querySelector('.provider-list, .endpoints-list, .embedding-pool-sidebar-info, .embedding-pool-sidebar-summary, .cache-sidebar-info'); if (contentArea && contentArea.parentElement) { contentArea.parentElement.innerHTML = renderEmbeddingPoolSidebar(); if (window.lucide) lucide.createIcons(); } } } catch (err) { console.error('Failed to save embedding pool config:', err); showRefreshToast(t('common.error') + ': ' + err.message, 'error'); } } /** * Toggle provider exclusion in embedding pool */ async function toggleProviderExclusion(providerId) { if (!embeddingPoolConfig) return; const excludedIds = embeddingPoolConfig.excludedProviderIds || []; const index = excludedIds.indexOf(providerId); if (index > -1) { excludedIds.splice(index, 1); } else { excludedIds.push(providerId); } embeddingPoolConfig.excludedProviderIds = excludedIds; // Re-render the discovered providers section renderDiscoveredProviders(); // Update sidebar summary const sidebarContainer = document.querySelector('.api-settings-sidebar .embedding-pool-sidebar-summary'); if (sidebarContainer && sidebarContainer.parentElement) { sidebarContainer.parentElement.innerHTML = renderEmbeddingPoolSidebar(); if (window.lucide) lucide.createIcons(); } } // ========== Provider Management ========== /** * Show add provider modal */ async function showAddProviderModal() { const modalHtml = '
'; document.body.insertAdjacentHTML('beforeend', modalHtml); document.getElementById('providerForm').addEventListener('submit', async function(e) { e.preventDefault(); await saveProvider(); }); if (window.lucide) lucide.createIcons(); } /** * Show edit provider modal */ async function showEditProviderModal(providerId) { if (!apiSettingsData) return; const provider = apiSettingsData.providers?.find(function(p) { return p.id === providerId; }); if (!provider) return; await showAddProviderModal(); // Update modal title document.querySelector('#providerModal .generic-modal-title').textContent = t('apiSettings.editProvider'); // Populate form document.getElementById('provider-type').value = provider.type; document.getElementById('provider-name').value = provider.name; document.getElementById('provider-apikey').value = provider.apiKey; if (provider.apiBase) { document.getElementById('provider-apibase').value = provider.apiBase; } document.getElementById('provider-enabled').checked = provider.enabled !== false; // Populate advanced settings if they exist if (provider.advancedSettings) { var settings = provider.advancedSettings; if (settings.timeout) { document.getElementById('provider-timeout').value = settings.timeout; } if (settings.maxRetries !== undefined) { document.getElementById('provider-max-retries').value = settings.maxRetries; } if (settings.organization) { document.getElementById('provider-organization').value = settings.organization; } if (settings.apiVersion) { document.getElementById('provider-api-version').value = settings.apiVersion; } if (settings.rpm) { document.getElementById('provider-rpm').value = settings.rpm; } if (settings.tpm) { document.getElementById('provider-tpm').value = settings.tpm; } if (settings.proxy) { document.getElementById('provider-proxy').value = settings.proxy; } if (settings.customHeaders) { document.getElementById('provider-custom-headers').value = JSON.stringify(settings.customHeaders, null, 2); } // Expand advanced settings if any values exist if (Object.keys(settings).length > 0) { toggleAdvancedSettings(); } } // Update provider-specific field visibility updateProviderSpecificFields(); // Store provider ID for update document.getElementById('providerForm').dataset.providerId = providerId; } /** * Save provider (create or update) */ async function saveProvider() { const form = document.getElementById('providerForm'); const providerId = form.dataset.providerId; const useEnvVar = document.getElementById('use-env-var').checked; const apiKey = useEnvVar ? '${' + document.getElementById('env-var-name').value + '}' : document.getElementById('provider-apikey').value; // Collect advanced settings var advancedSettings = {}; var timeout = document.getElementById('provider-timeout').value; if (timeout) advancedSettings.timeout = parseInt(timeout); var maxRetries = document.getElementById('provider-max-retries').value; if (maxRetries) advancedSettings.maxRetries = parseInt(maxRetries); var organization = document.getElementById('provider-organization').value; if (organization) advancedSettings.organization = organization; var apiVersion = document.getElementById('provider-api-version').value; if (apiVersion) advancedSettings.apiVersion = apiVersion; var rpm = document.getElementById('provider-rpm').value; if (rpm) advancedSettings.rpm = parseInt(rpm); var tpm = document.getElementById('provider-tpm').value; if (tpm) advancedSettings.tpm = parseInt(tpm); var proxy = document.getElementById('provider-proxy').value; if (proxy) advancedSettings.proxy = proxy; var customHeadersJson = document.getElementById('provider-custom-headers').value; if (customHeadersJson) { try { advancedSettings.customHeaders = JSON.parse(customHeadersJson); } catch (e) { showRefreshToast(t('apiSettings.invalidJsonHeaders'), 'error'); return; } } const providerData = { type: document.getElementById('provider-type').value, name: document.getElementById('provider-name').value, apiKey: apiKey, apiBase: document.getElementById('provider-apibase').value || undefined, enabled: document.getElementById('provider-enabled').checked, advancedSettings: Object.keys(advancedSettings).length > 0 ? advancedSettings : undefined }; try { const url = providerId ? '/api/litellm-api/providers/' + providerId : '/api/litellm-api/providers'; const method = providerId ? 'PUT' : 'POST'; const response = await fetch(url, { method: method, headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(providerData) }); if (!response.ok) throw new Error('Failed to save provider'); const result = await response.json(); showRefreshToast(t('apiSettings.providerSaved'), 'success'); closeProviderModal(); // Force refresh data after saving apiSettingsData = null; await renderApiSettings(); } catch (err) { console.error('Failed to save provider:', err); showRefreshToast(t('common.error') + ': ' + err.message, 'error'); } } /** * Delete provider */ async function deleteProvider(providerId) { if (!confirm(t('apiSettings.confirmDeleteProvider'))) return; try { const response = await fetch('/api/litellm-api/providers/' + providerId, { method: 'DELETE' }); if (!response.ok) throw new Error('Failed to delete provider'); showRefreshToast(t('apiSettings.providerDeleted'), 'success'); // Force refresh data after deleting apiSettingsData = null; await renderApiSettings(); } catch (err) { console.error('Failed to delete provider:', err); showRefreshToast(t('common.error') + ': ' + err.message, 'error'); } } /** * Test provider connection * @param {string} [providerIdParam] - Optional provider ID. If not provided, uses form context or selectedProviderId */ async function testProviderConnection(providerIdParam) { var providerId = providerIdParam; // Try to get providerId from different sources if (!providerId) { var form = document.getElementById('providerForm'); if (form && form.dataset.providerId) { providerId = form.dataset.providerId; } else if (selectedProviderId) { providerId = selectedProviderId; } } if (!providerId) { showRefreshToast(t('apiSettings.saveProviderFirst'), 'warning'); return; } try { const response = await fetch('/api/litellm-api/providers/' + providerId + '/test', { method: 'POST' }); if (!response.ok) throw new Error('Failed to test provider'); const result = await response.json(); if (result.success) { showRefreshToast(t('apiSettings.connectionSuccess'), 'success'); } else { showRefreshToast(t('apiSettings.connectionFailed') + ': ' + (result.error || 'Unknown error'), 'error'); } } catch (err) { console.error('Failed to test provider:', err); showRefreshToast(t('common.error') + ': ' + err.message, 'error'); } } /** * Close provider modal */ function closeProviderModal() { const modal = document.getElementById('providerModal'); if (modal) modal.remove(); } /** * Toggle API key visibility */ function toggleApiKeyVisibility(inputId) { const input = document.getElementById(inputId); const icon = event.target.closest('button').querySelector('i'); if (input.type === 'password') { input.type = 'text'; icon.setAttribute('data-lucide', 'eye-off'); } else { input.type = 'password'; icon.setAttribute('data-lucide', 'eye'); } if (window.lucide) lucide.createIcons(); } /** * Toggle environment variable input */ function toggleEnvVarInput() { const useEnvVar = document.getElementById('use-env-var').checked; const apiKeyInput = document.getElementById('provider-apikey'); const envVarInput = document.getElementById('env-var-name'); if (useEnvVar) { apiKeyInput.style.display = 'none'; apiKeyInput.required = false; envVarInput.style.display = 'block'; envVarInput.required = true; } else { apiKeyInput.style.display = 'block'; apiKeyInput.required = true; envVarInput.style.display = 'none'; envVarInput.required = false; } } /** * Toggle advanced settings visibility */ function toggleAdvancedSettings() { var content = document.getElementById('advanced-settings-content'); var legend = document.querySelector('.advanced-settings-legend'); var isCollapsed = content.classList.contains('collapsed'); content.classList.toggle('collapsed'); legend.classList.toggle('expanded'); // Update icon var icon = legend.querySelector('.advanced-toggle-icon'); if (icon) { icon.setAttribute('data-lucide', isCollapsed ? 'chevron-down' : 'chevron-right'); if (window.lucide) lucide.createIcons(); } } /** * Update provider-specific fields visibility based on provider type */ function updateProviderSpecificFields() { var providerType = document.getElementById('provider-type').value; // Hide all provider-specific fields first var specificFields = document.querySelectorAll('.provider-specific'); specificFields.forEach(function(el) { el.style.display = 'none'; }); // Show OpenAI-specific fields if (providerType === 'openai') { var openaiFields = document.querySelectorAll('.openai-only'); openaiFields.forEach(function(el) { el.style.display = 'block'; }); } // Show Azure-specific fields if (providerType === 'azure') { var azureFields = document.querySelectorAll('.azure-only'); azureFields.forEach(function(el) { el.style.display = 'block'; }); } } // ========== Endpoint Management ========== /** * Show add endpoint modal */ async function showAddEndpointModal() { if (!apiSettingsData || !apiSettingsData.providers || apiSettingsData.providers.length === 0) { showRefreshToast(t('apiSettings.addProviderFirst'), 'warning'); return; } const providerOptions = apiSettingsData.providers .filter(function(p) { return p.enabled !== false; }) .map(function(p) { return ''; }) .join(''); const modalHtml = ''; document.body.insertAdjacentHTML('beforeend', modalHtml); document.getElementById('endpointForm').addEventListener('submit', async function(e) { e.preventDefault(); await saveEndpoint(); }); // Load models for first provider await loadModelsForProvider(); if (window.lucide) lucide.createIcons(); } /** * Show edit endpoint modal */ async function showEditEndpointModal(endpointId) { if (!apiSettingsData) return; const endpoint = apiSettingsData.endpoints?.find(function(e) { return e.id === endpointId; }); if (!endpoint) return; await showAddEndpointModal(); // Update modal title document.querySelector('#endpointModal .generic-modal-title').textContent = t('apiSettings.editEndpoint'); // Populate form document.getElementById('endpoint-id').value = endpoint.id; document.getElementById('endpoint-id').disabled = true; document.getElementById('endpoint-name').value = endpoint.name; document.getElementById('endpoint-provider').value = endpoint.providerId; await loadModelsForProvider(); document.getElementById('endpoint-model').value = endpoint.model; if (endpoint.cacheStrategy) { document.getElementById('cache-enabled').checked = endpoint.cacheStrategy.enabled; if (endpoint.cacheStrategy.enabled) { toggleCacheSettings(); document.getElementById('cache-ttl').value = endpoint.cacheStrategy.ttlMinutes || 60; document.getElementById('cache-maxsize').value = endpoint.cacheStrategy.maxSizeKB || 512; document.getElementById('cache-patterns').value = endpoint.cacheStrategy.autoCachePatterns?.join(', ') || ''; } } // Store endpoint ID for update document.getElementById('endpointForm').dataset.endpointId = endpointId; } /** * Save endpoint (create or update) */ async function saveEndpoint() { const form = document.getElementById('endpointForm'); const endpointId = form.dataset.endpointId || document.getElementById('endpoint-id').value; const cacheEnabled = document.getElementById('cache-enabled').checked; const cacheStrategy = cacheEnabled ? { enabled: true, ttlMinutes: parseInt(document.getElementById('cache-ttl').value) || 60, maxSizeKB: parseInt(document.getElementById('cache-maxsize').value) || 512, autoCachePatterns: document.getElementById('cache-patterns').value .split(',') .map(function(p) { return p.trim(); }) .filter(function(p) { return p; }) } : { enabled: false }; const endpointData = { id: endpointId, name: document.getElementById('endpoint-name').value, providerId: document.getElementById('endpoint-provider').value, model: document.getElementById('endpoint-model').value, cacheStrategy: cacheStrategy }; try { const url = form.dataset.endpointId ? '/api/litellm-api/endpoints/' + form.dataset.endpointId : '/api/litellm-api/endpoints'; const method = form.dataset.endpointId ? 'PUT' : 'POST'; const response = await fetch(url, { method: method, headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(endpointData) }); if (!response.ok) throw new Error('Failed to save endpoint'); const result = await response.json(); showRefreshToast(t('apiSettings.endpointSaved'), 'success'); closeEndpointModal(); // Force refresh data after saving apiSettingsData = null; await renderApiSettings(); } catch (err) { console.error('Failed to save endpoint:', err); showRefreshToast(t('common.error') + ': ' + err.message, 'error'); } } /** * Delete endpoint */ async function deleteEndpoint(endpointId) { if (!confirm(t('apiSettings.confirmDeleteEndpoint'))) return; try { const response = await fetch('/api/litellm-api/endpoints/' + endpointId, { method: 'DELETE' }); if (!response.ok) throw new Error('Failed to delete endpoint'); showRefreshToast(t('apiSettings.endpointDeleted'), 'success'); // Force refresh data after deleting apiSettingsData = null; await renderApiSettings(); } catch (err) { console.error('Failed to delete endpoint:', err); showRefreshToast(t('common.error') + ': ' + err.message, 'error'); } } /** * Close endpoint modal */ function closeEndpointModal() { const modal = document.getElementById('endpointModal'); if (modal) modal.remove(); } /** * Load models for selected provider */ async function loadModelsForProvider() { const providerSelect = document.getElementById('endpoint-provider'); const modelSelect = document.getElementById('endpoint-model'); if (!providerSelect || !modelSelect) return; const providerId = providerSelect.value; const provider = apiSettingsData.providers.find(function(p) { return p.id === providerId; }); if (!provider) return; // Use LLM models configured for this provider (not static presets) const models = provider.llmModels || []; if (models.length === 0) { modelSelect.innerHTML = ''; return; } modelSelect.innerHTML = '' + models.filter(function(m) { return m.enabled; }).map(function(m) { const contextInfo = m.capabilities && m.capabilities.contextWindow ? ' (' + Math.round(m.capabilities.contextWindow / 1000) + 'K)' : ''; return ''; }).join(''); } /** * Toggle cache settings visibility */ function toggleCacheSettings() { const enabled = document.getElementById('cache-enabled').checked; const settings = document.getElementById('cache-settings'); settings.style.display = enabled ? 'block' : 'none'; } // ========== Cache Management ========== /** * Clear cache */ async function clearCache() { if (!confirm(t('apiSettings.confirmClearCache'))) return; try { const response = await fetch('/api/litellm-api/cache/clear', { method: 'POST' }); if (!response.ok) throw new Error('Failed to clear cache'); const result = await response.json(); showRefreshToast(t('apiSettings.cacheCleared') + ' (' + result.removed + ' entries)', 'success'); // Cache stats might have changed, but apiSettingsData doesn't need refresh await renderApiSettings(); } catch (err) { console.error('Failed to clear cache:', err); showRefreshToast(t('common.error') + ': ' + err.message, 'error'); } } /** * Toggle global cache */ async function toggleGlobalCache() { const enabled = document.getElementById('global-cache-enabled').checked; try { const response = await fetch('/api/litellm-api/config/cache', { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ enabled: enabled }) }); if (!response.ok) throw new Error('Failed to update cache settings'); showRefreshToast(t('apiSettings.cacheSettingsUpdated'), 'success'); } catch (err) { console.error('Failed to update cache settings:', err); showRefreshToast(t('common.error') + ': ' + err.message, 'error'); // Revert checkbox document.getElementById('global-cache-enabled').checked = !enabled; } } // ========== Rendering ========== /** * Render API Settings page - Split Layout */ async function renderApiSettings() { var container = document.getElementById('mainContent'); if (!container) return; // Hide stats grid and search var statsGrid = document.getElementById('statsGrid'); var searchInput = document.getElementById('searchInput'); if (statsGrid) statsGrid.style.display = 'none'; if (searchInput) searchInput.parentElement.style.display = 'none'; // Load data (use cache by default, forceRefresh=false) await loadApiSettings(false); if (!apiSettingsData) { container.innerHTML = '' + (query ? t('apiSettings.noProvidersFound') : t('apiSettings.noProviders')) + '
' + '' + t('apiSettings.selectProviderHint') + '
' + '' + t('apiSettings.noModels') + '
' + '' + t('apiSettings.noProvidersHint') + '
' + '' + t('apiSettings.noEndpointsHint') + '
' + '' + endpoint.id + '' +
'ccw cli -p "..." --model ' + endpoint.id + '' +
'' + t('apiSettings.endpointsDescription') + '
' + '' + t('apiSettings.cacheDescription') + '
' + '