Support dynamic Gemini AI model selection with model discovery and default to gemini-2.0-flash

This commit is contained in:
Chris 2026-11-01 01:44:40 -04:00
parent a098ea3bca
commit 311c03136c
4 changed files with 217 additions and 19 deletions

View file

@ -25,6 +25,9 @@ const elements = {
settingsModal: document.getElementById('settings-modal'), settingsModal: document.getElementById('settings-modal'),
settingsApiForm: document.getElementById('settings-api-form'), settingsApiForm: document.getElementById('settings-api-form'),
settingsGeminiKeyInput: document.getElementById('settings-gemini-key'), settingsGeminiKeyInput: document.getElementById('settings-gemini-key'),
settingsGeminiModelSelect: document.getElementById('settings-gemini-model'),
btnDiscoverModels: document.getElementById('btn-discover-models'),
modelsStatusHint: document.getElementById('models-status-hint'),
btnToggleGeminiKey: document.getElementById('btn-toggle-gemini-key'), btnToggleGeminiKey: document.getElementById('btn-toggle-gemini-key'),
settingsStatusMsg: document.getElementById('settings-status-msg'), settingsStatusMsg: document.getElementById('settings-status-msg'),
settingsStatusText: document.getElementById('settings-status-text'), settingsStatusText: document.getElementById('settings-status-text'),
@ -259,6 +262,9 @@ function registerEvents() {
elements.btnSettings.addEventListener('click', openSettingsModal); elements.btnSettings.addEventListener('click', openSettingsModal);
elements.settingsApiForm.addEventListener('submit', handleSaveSettings); elements.settingsApiForm.addEventListener('submit', handleSaveSettings);
elements.btnToggleGeminiKey.addEventListener('click', toggleSettingsKeyVisibility); elements.btnToggleGeminiKey.addEventListener('click', toggleSettingsKeyVisibility);
if (elements.btnDiscoverModels) {
elements.btnDiscoverModels.addEventListener('click', () => handleDiscoverModels(true));
}
// Image Scanning Actions // Image Scanning Actions
elements.btnScanImage.addEventListener('click', () => elements.scanImageInput.click()); elements.btnScanImage.addEventListener('click', () => elements.scanImageInput.click());
@ -2024,6 +2030,91 @@ window.toggleRecipeManagerRole = async function(id, makeManager) {
// SYSTEM SETTINGS & GEMINI AI PHOTO SCANNER // SYSTEM SETTINGS & GEMINI AI PHOTO SCANNER
// ------------------------------------------------------------- // -------------------------------------------------------------
function populateModelDropdown(models, selectedModel) {
if (!elements.settingsGeminiModelSelect) return;
const select = elements.settingsGeminiModelSelect;
select.innerHTML = '';
const targetModel = selectedModel || 'gemini-2.0-flash';
let foundTarget = false;
models.forEach(m => {
const opt = document.createElement('option');
opt.value = m.id;
opt.textContent = m.displayName || m.id;
if (m.id === 'gemini-2.0-flash') {
opt.textContent = `${m.displayName || 'Gemini 2.0 Flash'} (Recommended)`;
}
if (m.id === targetModel) {
opt.selected = true;
foundTarget = true;
}
select.appendChild(opt);
});
if (!foundTarget && targetModel) {
const customOpt = document.createElement('option');
customOpt.value = targetModel;
customOpt.textContent = targetModel;
customOpt.selected = true;
select.appendChild(customOpt);
}
}
async function handleDiscoverModels(showNotice = true) {
const key = elements.settingsGeminiKeyInput ? elements.settingsGeminiKeyInput.value.trim() : '';
if (elements.modelsStatusHint) {
elements.modelsStatusHint.textContent = 'Discovering available models from Google API...';
elements.modelsStatusHint.style.color = 'var(--primary)';
}
if (elements.btnDiscoverModels) {
elements.btnDiscoverModels.disabled = true;
}
try {
const res = await fetch('/api/settings/discover-models', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
...getAuthHeaders()
},
body: JSON.stringify({ apiKey: key })
});
const data = await res.json();
if (!res.ok) {
throw new Error(data.error || 'Failed to discover models.');
}
if (data.models && data.models.length > 0) {
const currentSelected = elements.settingsGeminiModelSelect ? elements.settingsGeminiModelSelect.value : 'gemini-2.0-flash';
populateModelDropdown(data.models, currentSelected);
if (elements.modelsStatusHint) {
elements.modelsStatusHint.textContent = `Discovered ${data.models.length} compatible models.`;
elements.modelsStatusHint.style.color = 'var(--text-muted)';
}
} else {
if (elements.modelsStatusHint) {
elements.modelsStatusHint.textContent = 'No compatible models found for this key.';
elements.modelsStatusHint.style.color = '#ef4444';
}
}
} catch (err) {
console.error('Discover models error:', err);
if (elements.modelsStatusHint) {
elements.modelsStatusHint.textContent = `Discovery error: ${err.message}`;
elements.modelsStatusHint.style.color = '#ef4444';
}
if (showNotice) {
alert(`Model Discovery Failed: ${err.message}\n\nPlease check that your Gemini API Key is valid.`);
}
} finally {
if (elements.btnDiscoverModels) {
elements.btnDiscoverModels.disabled = false;
}
}
}
async function openSettingsModal() { async function openSettingsModal() {
// Reset visibility states and statuses // Reset visibility states and statuses
elements.settingsStatusMsg.style.display = 'none'; elements.settingsStatusMsg.style.display = 'none';
@ -2034,6 +2125,10 @@ async function openSettingsModal() {
elements.settingsGeminiKeyInput.value = ''; elements.settingsGeminiKeyInput.value = '';
elements.settingsGeminiKeyInput.placeholder = 'Loading credentials...'; elements.settingsGeminiKeyInput.placeholder = 'Loading credentials...';
if (elements.modelsStatusHint) {
elements.modelsStatusHint.textContent = 'Used for both recipe photo scanning and AI text polishing.';
elements.modelsStatusHint.style.color = 'var(--text-muted)';
}
openModal('settings-modal'); openModal('settings-modal');
@ -2056,6 +2151,25 @@ async function openSettingsModal() {
if (key && !isPlaceholder) { if (key && !isPlaceholder) {
elements.settingsGeminiKeyInput.value = key; elements.settingsGeminiKeyInput.value = key;
} }
if (elements.settingsGeminiModelSelect) {
const savedModel = settings.geminiModel || 'gemini-2.0-flash';
let exists = false;
for (let i = 0; i < elements.settingsGeminiModelSelect.options.length; i++) {
if (elements.settingsGeminiModelSelect.options[i].value === savedModel) {
exists = true;
break;
}
}
if (!exists && savedModel) {
const opt = document.createElement('option');
opt.value = savedModel;
opt.textContent = savedModel;
elements.settingsGeminiModelSelect.appendChild(opt);
}
elements.settingsGeminiModelSelect.value = savedModel;
}
if (elements.settingsDefaultConsolidate && typeof settings.defaultConsolidateLists === 'boolean') { if (elements.settingsDefaultConsolidate && typeof settings.defaultConsolidateLists === 'boolean') {
elements.settingsDefaultConsolidate.checked = settings.defaultConsolidateLists; elements.settingsDefaultConsolidate.checked = settings.defaultConsolidateLists;
applyUserDefaultConsolidationPreference(settings.defaultConsolidateLists); applyUserDefaultConsolidationPreference(settings.defaultConsolidateLists);
@ -2071,6 +2185,7 @@ async function handleSaveSettings(e) {
elements.settingsStatusMsg.style.display = 'none'; elements.settingsStatusMsg.style.display = 'none';
const key = elements.settingsGeminiKeyInput.value.trim(); const key = elements.settingsGeminiKeyInput.value.trim();
const model = elements.settingsGeminiModelSelect ? elements.settingsGeminiModelSelect.value : 'gemini-2.0-flash';
const defaultConsolidate = elements.settingsDefaultConsolidate ? elements.settingsDefaultConsolidate.checked : true; const defaultConsolidate = elements.settingsDefaultConsolidate ? elements.settingsDefaultConsolidate.checked : true;
try { try {
@ -2082,6 +2197,7 @@ async function handleSaveSettings(e) {
}, },
body: JSON.stringify({ body: JSON.stringify({
geminiApiKey: key, geminiApiKey: key,
geminiModel: model,
defaultConsolidateLists: defaultConsolidate defaultConsolidateLists: defaultConsolidate
}) })
}); });
@ -2243,6 +2359,7 @@ async function scanRecipeFile(file) {
} }
const settings = await settingsResponse.json(); const settings = await settingsResponse.json();
const apiKey = settings.geminiApiKey ? settings.geminiApiKey.trim() : ''; const apiKey = settings.geminiApiKey ? settings.geminiApiKey.trim() : '';
const geminiModel = settings.geminiModel ? settings.geminiModel.trim() : 'gemini-2.0-flash';
const isPlaceholder = apiKey === 'PASTE_YOUR_GEMINI_API_KEY_HERE' || apiKey === ''; const isPlaceholder = apiKey === 'PASTE_YOUR_GEMINI_API_KEY_HERE' || apiKey === '';
if (!apiKey || isPlaceholder) { if (!apiKey || isPlaceholder) {
@ -2272,7 +2389,7 @@ async function scanRecipeFile(file) {
// 3. Make direct call to Google Gemini Vision API // 3. Make direct call to Google Gemini Vision API
const promptText = "You are a professional culinary transcriber. Analyze the image of a recipe (which could be a cookbook page, handwritten note, screenshot, or packaging) and extract it accurately. Fill in all fields as completely as possible. The response must be a JSON object matching this schema:\n{\n \"title\": \"Recipe Title\",\n \"description\": \"Brief description or review\",\n \"servings\": \"e.g., 4\",\n \"prepTime\": \"e.g., 15 mins\",\n \"cookTime\": \"e.g., 30 mins\",\n \"totalTime\": \"e.g., 45 mins\",\n \"ingredients\": [\"ingredient 1\", \"ingredient 2\"],\n \"instructions\": [\"step 1\", \"step 2\"]\n}\nDo not include any markdown styling (like ```json) or explanation. Return only the raw JSON string."; const promptText = "You are a professional culinary transcriber. Analyze the image of a recipe (which could be a cookbook page, handwritten note, screenshot, or packaging) and extract it accurately. Fill in all fields as completely as possible. The response must be a JSON object matching this schema:\n{\n \"title\": \"Recipe Title\",\n \"description\": \"Brief description or review\",\n \"servings\": \"e.g., 4\",\n \"prepTime\": \"e.g., 15 mins\",\n \"cookTime\": \"e.g., 30 mins\",\n \"totalTime\": \"e.g., 45 mins\",\n \"ingredients\": [\"ingredient 1\", \"ingredient 2\"],\n \"instructions\": [\"step 1\", \"step 2\"]\n}\nDo not include any markdown styling (like ```json) or explanation. Return only the raw JSON string.";
const geminiUrl = `https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash:generateContent?key=${apiKey}`; const geminiUrl = `https://generativelanguage.googleapis.com/v1beta/models/${geminiModel}:generateContent?key=${apiKey}`;
const geminiResponse = await fetch(geminiUrl, { const geminiResponse = await fetch(geminiUrl, {
method: 'POST', method: 'POST',
@ -2302,15 +2419,16 @@ async function scanRecipeFile(file) {
if (!geminiResponse.ok) { if (!geminiResponse.ok) {
const errorObj = await geminiResponse.json().catch(() => ({})); const errorObj = await geminiResponse.json().catch(() => ({}));
const status = geminiResponse.status; const status = geminiResponse.status;
let errMsg = 'Google Gemini API call failed.'; let errMsg = (errorObj.error && errorObj.error.message)
if (status === 400) { ? errorObj.error.message
errMsg = 'Invalid parameters or model version. Please verify your settings.'; : 'Google Gemini API call failed.';
} else if (status === 403) {
if (status === 400 && (!errorObj.error || !errorObj.error.message)) {
errMsg = `Invalid parameters or model version (${geminiModel}). Please check your settings.`;
} else if (status === 403 && (!errorObj.error || !errorObj.error.message)) {
errMsg = 'Access denied. The Google Gemini API key is invalid or inactive.'; errMsg = 'Access denied. The Google Gemini API key is invalid or inactive.';
} else if (status === 429) { } else if (status === 429 && (!errorObj.error || !errorObj.error.message)) {
errMsg = 'Rate limit exceeded. You have made too many requests or hit your Gemini quota.'; errMsg = 'Rate limit exceeded. You have made too many requests or hit your Gemini quota.';
} else if (errorObj.error && errorObj.error.message) {
errMsg = `Gemini API Error: ${errorObj.error.message}`;
} }
throw new Error(errMsg); throw new Error(errMsg);
} }

View file

@ -1013,11 +1013,11 @@
</div> </div>
<p style="font-size: 0.85rem; line-height: 1.4; color: var(--text-muted); margin-bottom: 16px;"> <p style="font-size: 0.85rem; line-height: 1.4; color: var(--text-muted); margin-bottom: 16px;">
Configure your system-wide AI credentials to enable physical recipe photo scanning. Configure your system-wide Google Gemini credentials and model to enable recipe photo scanning and AI text polishing.
</p> </p>
<form id="settings-api-form" style="margin-bottom: 8px;"> <form id="settings-api-form" style="margin-bottom: 8px;">
<div class="form-group mb-4"> <div class="form-group mb-3">
<label for="settings-gemini-key" style="font-size: 0.85rem; margin-bottom: 6px; display: block; font-weight: 500;">Google Gemini API Key</label> <label for="settings-gemini-key" style="font-size: 0.85rem; margin-bottom: 6px; display: block; font-weight: 500;">Google Gemini API Key</label>
<div class="input-wrapper" style="position: relative; display: flex; align-items: center;"> <div class="input-wrapper" style="position: relative; display: flex; align-items: center;">
<input type="password" id="settings-gemini-key" placeholder="Enter your Gemini API key..." autocomplete="off" style="width: 100%; padding-right: 40px; height: 40px; border-radius: var(--border-radius-md); border: 1px solid var(--border-color); background: rgba(0,0,0,0.2); color: var(--text-main); padding-left: 12px;"> <input type="password" id="settings-gemini-key" placeholder="Enter your Gemini API key..." autocomplete="off" style="width: 100%; padding-right: 40px; height: 40px; border-radius: var(--border-radius-md); border: 1px solid var(--border-color); background: rgba(0,0,0,0.2); color: var(--text-main); padding-left: 12px;">
@ -1026,8 +1026,25 @@
</button> </button>
</div> </div>
</div> </div>
<div class="form-group mb-4">
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 6px;">
<label for="settings-gemini-model" style="font-size: 0.85rem; display: block; font-weight: 500;">AI Model</label>
<button type="button" id="btn-discover-models" class="btn btn-secondary btn-sm" style="font-size: 0.75rem; padding: 2px 10px; height: 28px; display: inline-flex; align-items: center; gap: 5px; cursor: pointer;" title="Query Google API for available models on your key">
<i data-lucide="refresh-cw" style="width: 13px; height: 13px;"></i>
<span>Discover Models</span>
</button>
</div>
<select id="settings-gemini-model" style="width: 100%; height: 40px; border-radius: var(--border-radius-md); border: 1px solid var(--border-color); background: var(--bg-card, #1e2430); color: var(--text-main); padding-left: 12px; padding-right: 12px; font-size: 0.85rem; cursor: pointer;">
<option value="gemini-2.0-flash">Gemini 2.0 Flash (Recommended)</option>
<option value="gemini-1.5-flash">Gemini 1.5 Flash</option>
<option value="gemini-1.5-pro">Gemini 1.5 Pro</option>
</select>
<span id="models-status-hint" style="font-size: 0.75rem; color: var(--text-muted); margin-top: 4px; display: block;">Powers both recipe photo scanning and AI text polishing.</span>
</div>
<button type="submit" class="btn btn-primary" style="width: 100%; height: 40px;"> <button type="submit" class="btn btn-primary" style="width: 100%; height: 40px;">
<i data-lucide="save"></i> <span>Save Credentials</span> <i data-lucide="save"></i> <span>Save Settings</span>
</button> </button>
</form> </form>
</div> </div>

View file

@ -1,3 +1,4 @@
{ {
"geminiApiKey": "PASTE_YOUR_GEMINI_API_KEY_HERE" "geminiApiKey": "PASTE_YOUR_GEMINI_API_KEY_HERE",
"geminiModel": "gemini-2.0-flash"
} }

View file

@ -80,7 +80,8 @@ db.prepare(`
db.prepare(` db.prepare(`
CREATE TABLE IF NOT EXISTS settings ( CREATE TABLE IF NOT EXISTS settings (
geminiApiKey TEXT DEFAULT '' geminiApiKey TEXT DEFAULT '',
geminiModel TEXT DEFAULT 'gemini-2.0-flash'
) )
`).run(); `).run();
@ -190,11 +191,17 @@ try {
try { try {
db.prepare("ALTER TABLE backup_history ADD COLUMN userSummary TEXT NOT NULL DEFAULT '[]'").run(); db.prepare("ALTER TABLE backup_history ADD COLUMN userSummary TEXT NOT NULL DEFAULT '[]'").run();
} catch (e) {} } catch (e) {}
try {
db.prepare("ALTER TABLE settings ADD COLUMN geminiModel TEXT DEFAULT 'gemini-2.0-flash'").run();
} catch (e) {}
try {
db.prepare("UPDATE settings SET geminiModel = 'gemini-2.0-flash' WHERE geminiModel IS NULL OR geminiModel = ''").run();
} catch (e) {}
// Initialize single settings record if empty // Initialize single settings record if empty
const settingsCount = db.prepare('SELECT COUNT(*) as count FROM settings').get().count; const settingsCount = db.prepare('SELECT COUNT(*) as count FROM settings').get().count;
if (settingsCount === 0) { if (settingsCount === 0) {
db.prepare('INSERT INTO settings (geminiApiKey) VALUES (?)').run(''); db.prepare('INSERT INTO settings (geminiApiKey, geminiModel) VALUES (?, ?)').run('', 'gemini-2.0-flash');
} }
// Ensure default system_admin and admin user profiles exist // Ensure default system_admin and admin user profiles exist
@ -1575,8 +1582,9 @@ async function scrapeTikTokRecipe(inputUrl) {
app.post('/api/recipes/ai-polish', requireAuth, async (req, res) => { app.post('/api/recipes/ai-polish', requireAuth, async (req, res) => {
const { rawCaption, title, ingredients, instructions } = req.body; const { rawCaption, title, ingredients, instructions } = req.body;
const settingsRow = db.prepare('SELECT geminiApiKey FROM settings LIMIT 1').get(); const settingsRow = db.prepare('SELECT geminiApiKey, geminiModel FROM settings LIMIT 1').get();
const geminiKey = settingsRow?.geminiApiKey?.trim(); const geminiKey = settingsRow?.geminiApiKey?.trim();
const geminiModel = settingsRow?.geminiModel?.trim() || 'gemini-2.0-flash';
const isPlaceholder = !geminiKey || geminiKey === 'PASTE_YOUR_GEMINI_API_KEY_HERE'; const isPlaceholder = !geminiKey || geminiKey === 'PASTE_YOUR_GEMINI_API_KEY_HERE';
if (isPlaceholder) { if (isPlaceholder) {
@ -1612,7 +1620,7 @@ Return ONLY a valid JSON object matching this schema without markdown code block
"instructions": ["Step 1", "Step 2"] "instructions": ["Step 1", "Step 2"]
}`; }`;
const geminiUrl = `https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash:generateContent?key=${geminiKey}`; const geminiUrl = `https://generativelanguage.googleapis.com/v1beta/models/${geminiModel}:generateContent?key=${geminiKey}`;
const aiRes = await axios.post(geminiUrl, { const aiRes = await axios.post(geminiUrl, {
contents: [{ parts: [{ text: promptText }] }], contents: [{ parts: [{ text: promptText }] }],
generationConfig: { responseMimeType: "application/json" } generationConfig: { responseMimeType: "application/json" }
@ -1638,11 +1646,12 @@ Return ONLY a valid JSON object matching this schema without markdown code block
// API: Load System Settings & User Preferences // API: Load System Settings & User Preferences
app.get('/api/settings', requireAuth, (req, res) => { app.get('/api/settings', requireAuth, (req, res) => {
try { try {
const settings = db.prepare('SELECT geminiApiKey FROM settings LIMIT 1').get(); const settings = db.prepare('SELECT geminiApiKey, geminiModel FROM settings LIMIT 1').get();
const userRow = db.prepare('SELECT defaultConsolidateLists FROM users WHERE id = ?').get(req.user.id); const userRow = db.prepare('SELECT defaultConsolidateLists FROM users WHERE id = ?').get(req.user.id);
const defaultConsolidateLists = userRow ? userRow.defaultConsolidateLists !== 0 : true; const defaultConsolidateLists = userRow ? userRow.defaultConsolidateLists !== 0 : true;
res.json({ res.json({
geminiApiKey: settings ? settings.geminiApiKey : '', geminiApiKey: settings ? settings.geminiApiKey : '',
geminiModel: (settings && settings.geminiModel) ? settings.geminiModel : 'gemini-2.0-flash',
defaultConsolidateLists defaultConsolidateLists
}); });
} catch (e) { } catch (e) {
@ -1653,19 +1662,23 @@ app.get('/api/settings', requireAuth, (req, res) => {
// API: Save System Settings & User Preferences // API: Save System Settings & User Preferences
app.post('/api/settings', requireAuth, (req, res) => { app.post('/api/settings', requireAuth, (req, res) => {
const { geminiApiKey, defaultConsolidateLists } = req.body; const { geminiApiKey, geminiModel, defaultConsolidateLists } = req.body;
try { try {
if (geminiApiKey !== undefined) { if (geminiApiKey !== undefined) {
db.prepare('UPDATE settings SET geminiApiKey = ?').run(geminiApiKey || ''); db.prepare('UPDATE settings SET geminiApiKey = ?').run(geminiApiKey || '');
} }
if (geminiModel !== undefined) {
db.prepare('UPDATE settings SET geminiModel = ?').run(geminiModel || 'gemini-2.0-flash');
}
if (typeof defaultConsolidateLists === 'boolean') { if (typeof defaultConsolidateLists === 'boolean') {
db.prepare('UPDATE users SET defaultConsolidateLists = ? WHERE id = ?').run(defaultConsolidateLists ? 1 : 0, req.user.id); db.prepare('UPDATE users SET defaultConsolidateLists = ? WHERE id = ?').run(defaultConsolidateLists ? 1 : 0, req.user.id);
req.user.defaultConsolidateLists = defaultConsolidateLists; req.user.defaultConsolidateLists = defaultConsolidateLists;
} }
const settings = db.prepare('SELECT geminiApiKey FROM settings LIMIT 1').get(); const settings = db.prepare('SELECT geminiApiKey, geminiModel FROM settings LIMIT 1').get();
const userRow = db.prepare('SELECT defaultConsolidateLists FROM users WHERE id = ?').get(req.user.id); const userRow = db.prepare('SELECT defaultConsolidateLists FROM users WHERE id = ?').get(req.user.id);
res.json({ res.json({
geminiApiKey: settings ? settings.geminiApiKey : '', geminiApiKey: settings ? settings.geminiApiKey : '',
geminiModel: (settings && settings.geminiModel) ? settings.geminiModel : 'gemini-2.0-flash',
defaultConsolidateLists: userRow ? userRow.defaultConsolidateLists !== 0 : true defaultConsolidateLists: userRow ? userRow.defaultConsolidateLists !== 0 : true
}); });
} catch (e) { } catch (e) {
@ -1674,6 +1687,55 @@ app.post('/api/settings', requireAuth, (req, res) => {
} }
}); });
// API: Discover available Gemini models using the saved or provided API key
app.post('/api/settings/discover-models', requireAuth, async (req, res) => {
let { apiKey } = req.body || {};
if (!apiKey) {
const settingsRow = db.prepare('SELECT geminiApiKey FROM settings LIMIT 1').get();
apiKey = settingsRow?.geminiApiKey?.trim();
}
if (!apiKey || apiKey === 'PASTE_YOUR_GEMINI_API_KEY_HERE') {
return res.status(400).json({ error: 'Please enter a Gemini API Key first.' });
}
try {
const response = await axios.get(`https://generativelanguage.googleapis.com/v1beta/models?key=${apiKey}`, {
timeout: 12000
});
if (response.data && Array.isArray(response.data.models)) {
const models = response.data.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 || ''
};
});
// Sort models: Flash models first, then by ID
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 res.json({ models });
} else {
return res.status(500).json({ error: 'Unexpected response from Google Gemini API.' });
}
} catch (err) {
console.error('Model discovery error:', err.response?.data?.error?.message || err.message);
const msg = err.response?.data?.error?.message || err.message;
return res.status(err.response?.status || 500).json({ error: `Failed to fetch models: ${msg}` });
}
});
// API: Update User Preferences directly // API: Update User Preferences directly
app.put('/api/user/preferences', requireAuth, (req, res) => { app.put('/api/user/preferences', requireAuth, (req, res) => {
const { defaultConsolidateLists } = req.body; const { defaultConsolidateLists } = req.body;