From 900c7154b633ddca8b8b21607d7797699156aa19 Mon Sep 17 00:00:00 2001 From: MacRimi Date: Fri, 20 Mar 2026 23:21:00 +0100 Subject: [PATCH] Update notification service --- AppImage/components/notification-settings.tsx | 11 ++- AppImage/config/verified_ai_models.json | 64 ++++++++++++++ AppImage/scripts/flask_notification_routes.py | 83 +++++++++++++++---- AppImage/scripts/notification_manager.py | 35 ++++++-- 4 files changed, 167 insertions(+), 26 deletions(-) create mode 100644 AppImage/config/verified_ai_models.json diff --git a/AppImage/components/notification-settings.tsx b/AppImage/components/notification-settings.tsx index b8d2236d..eb3c6576 100644 --- a/AppImage/components/notification-settings.tsx +++ b/AppImage/components/notification-settings.tsx @@ -659,7 +659,7 @@ export function NotificationSettings() { setLoadingProviderModels(true) try { - const data = await fetchApi<{ success: boolean; models: string[]; message: string }>("/api/notifications/provider-models", { + const data = await fetchApi<{ success: boolean; models: string[]; recommended: string; message: string }>("/api/notifications/provider-models", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ @@ -671,10 +671,15 @@ export function NotificationSettings() { }) if (data.success && data.models && data.models.length > 0) { setProviderModels(data.models) - // Auto-select first model if current selection is empty or not in the list + // Auto-select recommended model if current selection is empty or not in the list updateConfig(prev => { if (!prev.ai_model || !data.models.includes(prev.ai_model)) { - return { ...prev, ai_model: data.models[0] } + const modelToSelect = data.recommended || data.models[0] + return { + ...prev, + ai_model: modelToSelect, + ai_models: { ...prev.ai_models, [provider]: modelToSelect } + } } return prev }) diff --git a/AppImage/config/verified_ai_models.json b/AppImage/config/verified_ai_models.json new file mode 100644 index 00000000..c0978a4a --- /dev/null +++ b/AppImage/config/verified_ai_models.json @@ -0,0 +1,64 @@ +{ + "_description": "Verified AI models for ProxMenux notifications. Only models listed here will be shown to users. Models are tested to work with the chat/completions API format.", + "_updated": "2026-03-20", + + "groq": { + "models": [ + "llama-3.3-70b-versatile", + "llama-3.1-70b-versatile", + "llama-3.1-8b-instant", + "llama3-70b-8192", + "llama3-8b-8192", + "mixtral-8x7b-32768", + "gemma2-9b-it" + ], + "recommended": "llama-3.3-70b-versatile" + }, + + "gemini": { + "models": [ + "gemini-2.5-flash-lite", + "gemini-flash-lite-latest" + ], + "recommended": "gemini-2.5-flash-lite" + }, + + "openai": { + "models": [ + "gpt-4.1-mini" + "gpt-4o-mini" + + ], + "recommended": "gpt-4o-mini" + }, + + "anthropic": { + "models": [ + "claude-3-5-haiku-latest", + "claude-3-5-sonnet-latest", + "claude-3-opus-latest" + ], + "recommended": "claude-3-5-haiku-latest" + }, + + "openrouter": { + "models": [ + "meta-llama/llama-3.3-70b-instruct", + "meta-llama/llama-3.1-70b-instruct", + "meta-llama/llama-3.1-8b-instruct", + "anthropic/claude-3.5-haiku", + "anthropic/claude-3.5-sonnet", + "google/gemini-flash-2.5-flash-lite", + "openai/gpt-4o-mini", + "mistralai/mistral-7b-instruct", + "mistralai/mixtral-8x7b-instruct" + ], + "recommended": "meta-llama/llama-3.3-70b-instruct" + }, + + "ollama": { + "_note": "Ollama models are local, we don't filter them. User manages their own models.", + "models": [], + "recommended": "" + } +} diff --git a/AppImage/scripts/flask_notification_routes.py b/AppImage/scripts/flask_notification_routes.py index abbf45f8..0d825105 100644 --- a/AppImage/scripts/flask_notification_routes.py +++ b/AppImage/scripts/flask_notification_routes.py @@ -102,9 +102,25 @@ def test_notification(): return jsonify({'error': str(e)}), 500 +def load_verified_models(): + """Load verified models from config file.""" + try: + config_path = Path(__file__).parent.parent / 'config' / 'verified_ai_models.json' + if config_path.exists(): + with open(config_path, 'r') as f: + return json.load(f) + except Exception as e: + print(f"[flask_notification_routes] Failed to load verified models: {e}") + return {} + + @notification_bp.route('/api/notifications/provider-models', methods=['POST']) def get_provider_models(): - """Fetch available models from any AI provider. + """Fetch available models from AI provider, filtered by verified models list. + + Only returns models that: + 1. Are available from the provider's API + 2. Are in our verified_ai_models.json list (tested to work) Request body: { @@ -118,6 +134,7 @@ def get_provider_models(): { "success": true/false, "models": ["model1", "model2", ...], + "recommended": "recommended-model", "message": "status message" } """ @@ -131,7 +148,13 @@ def get_provider_models(): if not provider: return jsonify({'success': False, 'models': [], 'message': 'Provider not specified'}) - # Handle Ollama separately (local, no API key) + # Load verified models config + verified_config = load_verified_models() + provider_config = verified_config.get(provider, {}) + verified_models = set(provider_config.get('models', [])) + recommended = provider_config.get('recommended', '') + + # Handle Ollama separately (local, no filtering) if provider == 'ollama': import urllib.request import urllib.error @@ -147,25 +170,25 @@ def get_provider_models(): return jsonify({ 'success': True, 'models': models, - 'message': f'Found {len(models)} models' + 'recommended': models[0] if models else '', + 'message': f'Found {len(models)} local models' }) - # Handle Anthropic - no models list API, return known models + # Handle Anthropic - no models list API, return verified models directly if provider == 'anthropic': - # Anthropic doesn't have a models list endpoint - # Return the known stable aliases that auto-update - models = [ + models = list(verified_models) if verified_models else [ 'claude-3-5-haiku-latest', 'claude-3-5-sonnet-latest', 'claude-3-opus-latest', ] return jsonify({ 'success': True, - 'models': models, - 'message': 'Anthropic uses stable aliases that auto-update' + 'models': sorted(models), + 'recommended': recommended or models[0], + 'message': f'{len(models)} verified models' }) - # For other providers, use the provider's list_models method + # For other providers, fetch from API and filter by verified list if not api_key: return jsonify({'success': False, 'models': [], 'message': 'API key required'}) @@ -180,21 +203,51 @@ def get_provider_models(): if not ai_provider: return jsonify({'success': False, 'models': [], 'message': f'Unknown provider: {provider}'}) - models = ai_provider.list_models() + # Get all models from provider API + api_models = ai_provider.list_models() - if not models: + if not api_models: + # API failed, fall back to verified list only + if verified_models: + models = sorted(verified_models) + return jsonify({ + 'success': True, + 'models': models, + 'recommended': recommended or models[0], + 'message': f'{len(models)} verified models (API unavailable)' + }) return jsonify({ 'success': False, 'models': [], 'message': 'Could not retrieve models. Check your API key.' }) - # Sort and return - models = sorted(models) + # Filter: only models that are BOTH in API and verified list + if verified_models: + api_models_set = set(api_models) + filtered_models = [m for m in verified_models if m in api_models_set] + + if not filtered_models: + # No intersection - maybe verified list is outdated + # Return verified list anyway (will fail on use if truly unavailable) + filtered_models = list(verified_models) + + # Sort with recommended first + def sort_key(m): + if m == recommended: + return (0, m) + return (1, m) + + models = sorted(filtered_models, key=sort_key) + else: + # No verified list for this provider, return all from API + models = sorted(api_models) + return jsonify({ 'success': True, 'models': models, - 'message': f'Found {len(models)} models' + 'recommended': recommended if recommended in models else (models[0] if models else ''), + 'message': f'{len(models)} verified models available' }) except Exception as e: diff --git a/AppImage/scripts/notification_manager.py b/AppImage/scripts/notification_manager.py index 5ad29e9d..348b9bc2 100644 --- a/AppImage/scripts/notification_manager.py +++ b/AppImage/scripts/notification_manager.py @@ -1688,17 +1688,37 @@ class NotificationManager: return {'checked': False, 'migrated': False, 'message': 'No API key configured'} try: + # Load verified models from config + verified_models = [] + recommended_model = '' + try: + config_path = Path(__file__).parent.parent / 'config' / 'verified_ai_models.json' + if config_path.exists(): + with open(config_path, 'r') as f: + verified_config = json.load(f) + provider_config = verified_config.get(provider_name, {}) + verified_models = provider_config.get('models', []) + recommended_model = provider_config.get('recommended', '') + except Exception as e: + print(f"[NotificationManager] Failed to load verified models: {e}") + from ai_providers import get_provider provider = get_provider(provider_name, api_key=api_key, model=current_model) if not provider: return {'checked': False, 'migrated': False, 'message': f'Unknown provider: {provider_name}'} - # Get available models - available_models = provider.list_models() + # Get available models from API + api_models = provider.list_models() - if not available_models: - # Can't verify (provider doesn't support listing or API error) + # Combine: use verified models that are also in API (or all verified if API fails) + if api_models and verified_models: + available_models = [m for m in verified_models if m in api_models] + elif verified_models: + available_models = verified_models + elif api_models: + available_models = api_models + else: return {'checked': True, 'migrated': False, 'message': 'Could not retrieve model list'} # Check if current model is available @@ -1710,11 +1730,10 @@ class NotificationManager: 'message': f'Model {current_model} is available' } - # Model not available - find best fallback - recommended = provider.get_recommended_model() + # Model not available - use recommended or first available + recommended = recommended_model if recommended_model in available_models else (available_models[0] if available_models else '') - if recommended == current_model: - # No better option found + if not recommended or recommended == current_model: return { 'checked': True, 'migrated': False,