diff --git a/public/js/admin.js b/public/js/admin.js index c32a93a..a73332c 100644 --- a/public/js/admin.js +++ b/public/js/admin.js @@ -579,11 +579,9 @@ // ============================================================ (function() { - var modelsLoaded = false; - document.addEventListener('tabChanged', function(e) { if (e.detail && e.detail.tab === 'admin') { - if (!modelsLoaded) { loadAdminModels(); modelsLoaded = true; } + loadAdminModels(); } }); @@ -591,6 +589,7 @@ if (e.target.closest('#btn-discover-models')) discoverModels(); if (e.target.closest('#btn-add-custom-model')) addCustomModel(); if (e.target.closest('#btn-save-default-model')) saveDefaultModel(); + if (e.target.closest('#btn-clear-all-models')) clearAllModels(); }); // Allow Enter key to trigger search @@ -640,9 +639,11 @@ var container = document.getElementById('admin-builtin-models'); if (container) { if (data.litellmHint) { - container.innerHTML = '
' + - ' LiteLLM has no built-in model list — your available models depend on your proxy configuration. ' + - 'Use Search API below to discover what models your LiteLLM proxy exposes, then add them.
'; + container.innerHTML = 'LiteLLM mode: No built-in models. ' + + 'Use Search API below to discover models from your proxy, then add them.
' + + 'No built-in models for this provider.
'; } else { @@ -846,6 +847,21 @@ .catch(function() { showToast('Request failed', 'error'); }); } + function clearAllModels() { + if (!confirm('Remove all added models? You will need to re-add them via Search API.')) return; + Promise.all([ + fetch('/api/admin/config/models/clear-all', { method: 'POST', headers: getAuthHeaders() }) + .then(function(r) { return r.json(); }) + ]).then(function(results) { + if (results[0].success) { + showToast('All models cleared', 'info'); + loadAdminModels(); + } else { + showToast(results[0].error || 'Failed', 'error'); + } + }).catch(function() { showToast('Request failed', 'error'); }); + } + function saveDefaultModel() { var sel = document.getElementById('admin-default-model'); if (!sel || !sel.value) { showToast('Select a model', 'error'); return; } diff --git a/src/routes/adminConfig.js b/src/routes/adminConfig.js index d4953e8..15bfbcc 100644 --- a/src/routes/adminConfig.js +++ b/src/routes/adminConfig.js @@ -248,17 +248,6 @@ router.put('/config/models/toggle', async function(req, res) { disabled = disabled.filter(function(id) { return id !== modelId; }); } else { if (!disabled.includes(modelId)) disabled.push(modelId); - - // Safety: prevent disabling all models - var { getAvailableModels } = require('../utils/models'); - var allModels = getAvailableModels(); - var customRaw = await db.getSetting('models.custom') || '[]'; - var custom; - try { custom = JSON.parse(customRaw); } catch(e) { custom = []; } - var enabledCount = allModels.filter(function(m) { return !disabled.includes(m.id); }).length + custom.length; - if (enabledCount === 0) { - return res.status(400).json({ error: 'Cannot disable all models. At least one must remain enabled.' }); - } } await db.setSetting('models.disabled', JSON.stringify(disabled)); @@ -328,6 +317,17 @@ router.delete('/config/models/custom/:modelId(*)', async function(req, res) { } catch (e) { res.status(500).json({ error: e.message }); } }); +// ── POST clear all custom/discovered models ─────────────────────────────── +router.post('/config/models/clear-all', async function(req, res) { + try { + await db.setSetting('models.custom', '[]'); + await db.setSetting('models.disabled', '[]'); + await db.setSetting('models.default', ''); + logger.audit(req.user.id, 'admin_models_clear_all', 'Cleared all custom models and disabled list', req, { category: 'admin' }); + res.json({ success: true }); + } catch (e) { res.status(500).json({ error: e.message }); } +}); + // ── GET discover models from provider API ───────────────────────────────── router.get('/config/models/discover', async function(req, res) { try { diff --git a/src/utils/models.js b/src/utils/models.js index ff93e23..9ee5877 100644 --- a/src/utils/models.js +++ b/src/utils/models.js @@ -135,17 +135,9 @@ var VERTEX_MODELS = [ vertexId: 'meta/llama-3.1-405b-instruct-maas' }, ]; -// ============================================================ -// LITELLM MODELS (default set — admin can discover more via API) -// ============================================================ -var LITELLM_MODELS = [ - { id: 'gpt-4o', name: 'GPT-4o', cost: '~$0.01', tag: 'SMART', category: 'premium' }, - { id: 'gpt-4o-mini', name: 'GPT-4o Mini', cost: '~$0.003', tag: 'FAST', category: 'smart' }, - { id: 'vendor-model-sonnet-4-6-20250514', name: 'vendor model Sonnet 4.6', cost: '~$0.015', tag: 'PREMIUM', category: 'premium' }, - { id: 'vendor-model-haiku-4-5-20251001', name: 'vendor model Haiku 4.5', cost: '~$0.003', tag: 'FAST', category: 'smart' }, - { id: 'gemini/gemini-2.5-flash', name: 'Gemini 2.5 Flash', cost: '~$0.001', tag: 'VALUE', category: 'fast' }, - { id: 'gemini/gemini-2.5-pro', name: 'Gemini 2.5 Pro', cost: '~$0.01', tag: 'SMART', category: 'premium' }, -]; +// LiteLLM has NO built-in models — everything is discovered from the proxy via admin panel. +// This array is intentionally empty. Do not add models here. +var LITELLM_MODELS = []; function getAvailableModels() { switch (activeProvider) { @@ -167,7 +159,7 @@ function getDefaultModel() { case 'bedrock': return 'anthropic/vendor-model-sonnet-4-6'; case 'azure': return process.env.AZURE_DEPLOYMENT_NAME || 'gpt-4o-mini'; case 'vertex': return 'gemini-2.5-flash'; - case 'litellm': return 'gpt-4o-mini'; + case 'litellm': return ''; // No default — set via admin panel or LITELLM_DEFAULT_MODEL env case 'openrouter': default: return 'google/gemini-2.5-flash'; } @@ -178,7 +170,7 @@ function getFallbackModel() { case 'bedrock': return 'anthropic/vendor-model-haiku-4-5'; case 'azure': return process.env.AZURE_DEPLOYMENT_NAME || 'gpt-4o-mini'; case 'vertex': return 'gemini-2.0-flash'; - case 'litellm': return 'gpt-4o'; + case 'litellm': return ''; // No fallback — use whatever the user has configured case 'openrouter': default: return 'deepseek/deepseek-chat-v3-0324'; } @@ -217,8 +209,10 @@ async function getAvailableModelsWithOverrides(db) { try { disabled = JSON.parse(disabledRaw); } catch(e) { disabled = []; } try { custom = JSON.parse(customRaw); } catch(e) { custom = []; } + // For LiteLLM: no built-ins exist — the model list IS the custom/discovered list + if (activeProvider === 'litellm') return custom; + var result = baseModels.filter(function(m) { return !disabled.includes(m.id); }); - // Add custom models custom.forEach(function(m) { if (!result.find(function(r) { return r.id === m.id; })) result.push(m); });