diff --git a/public/app.js b/public/app.js index 8888d72..709ec96 100644 --- a/public/app.js +++ b/public/app.js @@ -2061,8 +2061,49 @@ function populateModelDropdown(models, selectedModel) { } } +async function fetchModelsDirectFromGoogle(key) { + if (!key || key === 'PASTE_YOUR_GEMINI_API_KEY_HERE') { + throw new Error('Please enter a valid Gemini API key first.'); + } + const googleRes = await fetch(`https://generativelanguage.googleapis.com/v1beta/models?key=${key}`); + const googleData = await googleRes.json().catch(() => ({})); + if (!googleRes.ok) { + throw new Error(googleData.error?.message || `Google API returned status ${googleRes.status}`); + } + if (!Array.isArray(googleData.models)) { + throw new Error('Unexpected response format from Google API.'); + } + const models = googleData.models + .filter(m => Array.isArray(m.supportedGenerationMethods) && m.supportedGenerationMethods.includes('generateContent')) + .filter(m => !m.name.includes('embedding') && !m.name.includes('aqa') && !m.name.includes('imagen')) + .map(m => { + const modelId = m.name.replace(/^models\//, ''); + return { + id: modelId, + displayName: m.displayName || modelId, + description: m.description || '' + }; + }); + + models.sort((a, b) => { + const aIsFlash = a.id.toLowerCase().includes('flash'); + const bIsFlash = b.id.toLowerCase().includes('flash'); + if (aIsFlash && !bIsFlash) return -1; + if (!aIsFlash && bIsFlash) return 1; + return a.id.localeCompare(b.id); + }); + return models; +} + async function handleDiscoverModels(showNotice = true) { const key = elements.settingsGeminiKeyInput ? elements.settingsGeminiKeyInput.value.trim() : ''; + if (!key || key === 'PASTE_YOUR_GEMINI_API_KEY_HERE') { + if (showNotice) { + alert('Please enter your Google Gemini API key first.'); + } + return; + } + if (elements.modelsStatusHint) { elements.modelsStatusHint.textContent = 'Discovering available models from Google API...'; elements.modelsStatusHint.style.color = 'var(--primary)'; @@ -2072,25 +2113,42 @@ async function handleDiscoverModels(showNotice = true) { } try { - const res = await fetch('/api/settings/discover-models', { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - ...getAuthHeaders() - }, - body: JSON.stringify({ apiKey: key }) - }); + let models = []; - const data = await res.json(); - if (!res.ok) { - throw new Error(data.error || 'Failed to discover models.'); + // 1. Attempt backend endpoint first + try { + const res = await fetch('/api/settings/discover-models', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + ...getAuthHeaders() + }, + body: JSON.stringify({ apiKey: key }) + }); + + if (res.ok) { + const text = await res.text(); + try { + const data = JSON.parse(text); + if (Array.isArray(data.models)) { + models = data.models; + } + } catch (parseErr) {} + } + } catch (serverErr) { + // Backend may not have route if server hasn't restarted yet } - if (data.models && data.models.length > 0) { + // 2. Fallback to direct client-side call to Google API if backend returned HTML / 404 + if (models.length === 0) { + models = await fetchModelsDirectFromGoogle(key); + } + + if (models.length > 0) { const currentSelected = elements.settingsGeminiModelSelect ? elements.settingsGeminiModelSelect.value : 'gemini-2.0-flash'; - populateModelDropdown(data.models, currentSelected); + populateModelDropdown(models, currentSelected); if (elements.modelsStatusHint) { - elements.modelsStatusHint.textContent = `Discovered ${data.models.length} compatible models.`; + elements.modelsStatusHint.textContent = `Discovered ${models.length} compatible models.`; elements.modelsStatusHint.style.color = 'var(--text-muted)'; } } else {