From 2f4ea025442be3e22e8ec13c0023edafaae1c9be Mon Sep 17 00:00:00 2001 From: MacRimi Date: Fri, 20 Mar 2026 22:18:56 +0100 Subject: [PATCH] Update notification service --- AppImage/components/notification-settings.tsx | 68 ++++++++++++++++--- .../scripts/ai_providers/gemini_provider.py | 37 +++++++++- .../scripts/ai_providers/groq_provider.py | 34 ++++++++-- .../scripts/ai_providers/openai_provider.py | 49 +++++++++++-- .../ai_providers/openrouter_provider.py | 39 +++++++++-- AppImage/scripts/notification_manager.py | 12 ++++ 6 files changed, 208 insertions(+), 31 deletions(-) diff --git a/AppImage/components/notification-settings.tsx b/AppImage/components/notification-settings.tsx index de56cf90..b8d2236d 100644 --- a/AppImage/components/notification-settings.tsx +++ b/AppImage/components/notification-settings.tsx @@ -58,7 +58,8 @@ interface NotificationConfig { ai_enabled: boolean ai_provider: string ai_api_keys: Record // Per-provider API keys - ai_model: string + ai_models: Record // Per-provider selected models + ai_model: string // Current active model (for the selected provider) ai_language: string ai_ollama_url: string ai_openai_base_url: string @@ -209,6 +210,14 @@ const DEFAULT_CONFIG: NotificationConfig = { openai: "", openrouter: "", }, + ai_models: { + groq: "", + ollama: "", + gemini: "", + anthropic: "", + openai: "", + openrouter: "", + }, ai_model: "", ai_language: "en", ai_ollama_url: "http://localhost:11434", @@ -260,20 +269,32 @@ export function NotificationSettings() { try { const data = await fetchApi<{ success: boolean; config: NotificationConfig }>("/api/notifications/settings") if (data.success && data.config) { - // Backend automatically migrates deprecated AI models to current versions - // Ensure ai_api_keys object exists (fallback for older configs) - const configWithKeys = { + // Ensure ai_api_keys and ai_models objects exist (fallback for older configs) + const configWithDefaults = { ...data.config, ai_api_keys: data.config.ai_api_keys || { groq: "", + ollama: "", + gemini: "", + anthropic: "", + openai: "", + openrouter: "", + }, + ai_models: data.config.ai_models || { + groq: "", + ollama: "", gemini: "", anthropic: "", openai: "", openrouter: "", } } - setConfig(configWithKeys) - setOriginalConfig(configWithKeys) + // If ai_model exists but ai_models doesn't have it, save it + if (configWithDefaults.ai_model && !configWithDefaults.ai_models[configWithDefaults.ai_provider]) { + configWithDefaults.ai_models[configWithDefaults.ai_provider] = configWithDefaults.ai_model + } + setConfig(configWithDefaults) + setOriginalConfig(configWithDefaults) } } catch (err) { console.error("Failed to load notification settings:", err) @@ -497,6 +518,14 @@ export function NotificationSettings() { } } } + // Flatten per-provider selected models + if (cfg.ai_models) { + for (const [provider, model] of Object.entries(cfg.ai_models)) { + if (model) { + flat[`ai_model_${provider}`] = model + } + } + } // Flatten channels: { telegram: { enabled, bot_token, chat_id } } -> telegram.enabled, telegram.bot_token, ... for (const [chName, chCfg] of Object.entries(cfg.channels)) { for (const [field, value] of Object.entries(chCfg)) { @@ -1452,10 +1481,23 @@ export function NotificationSettings() { updateConfig(p => ({ ...p, ai_model: v }))} + onValueChange={v => updateConfig(p => ({ + ...p, + ai_model: v, + ai_models: { ...p.ai_models, [p.ai_provider]: v } // Also save per-provider + }))} disabled={!editMode || loadingProviderModels || providerModels.length === 0} > diff --git a/AppImage/scripts/ai_providers/gemini_provider.py b/AppImage/scripts/ai_providers/gemini_provider.py index cb454f21..04b91d05 100644 --- a/AppImage/scripts/ai_providers/gemini_provider.py +++ b/AppImage/scripts/ai_providers/gemini_provider.py @@ -17,9 +17,22 @@ class GeminiProvider(AIProvider): REQUIRES_API_KEY = True API_BASE = "https://generativelanguage.googleapis.com/v1beta/models" + # Patterns to exclude from model list (experimental, preview, specialized) + EXCLUDED_PATTERNS = [ + 'preview', 'exp', 'experimental', 'computer-use', + 'deep-research', 'image', 'embedding', 'aqa', 'tts', + 'learnlm', 'imagen', 'veo' + ] + def list_models(self) -> List[str]: """List available Gemini models that support generateContent. + Filters to only stable text generation models, excluding: + - Preview/experimental models + - Image generation models + - Embedding models + - Specialized models (computer-use, deep-research, etc.) + Returns: List of model IDs available for text generation. """ @@ -44,10 +57,28 @@ class GeminiProvider(AIProvider): # Only include models that support generateContent supported_methods = model.get('supportedGenerationMethods', []) - if 'generateContent' in supported_methods: - models.append(model_id) + if 'generateContent' not in supported_methods: + continue + + # Exclude experimental, preview, and specialized models + model_lower = model_id.lower() + if any(pattern in model_lower for pattern in self.EXCLUDED_PATTERNS): + continue + + models.append(model_id) - return models + # Sort with recommended models first (flash-lite, flash, pro) + def sort_key(m): + m_lower = m.lower() + if 'flash-lite' in m_lower: + return (0, m) # Best for notifications (fast, cheap) + if 'flash' in m_lower: + return (1, m) + if 'pro' in m_lower: + return (2, m) + return (3, m) + + return sorted(models, key=sort_key) except Exception as e: print(f"[GeminiProvider] Failed to list models: {e}") return [] diff --git a/AppImage/scripts/ai_providers/groq_provider.py b/AppImage/scripts/ai_providers/groq_provider.py index 946ecff3..fada9f15 100644 --- a/AppImage/scripts/ai_providers/groq_provider.py +++ b/AppImage/scripts/ai_providers/groq_provider.py @@ -18,11 +18,19 @@ class GroqProvider(AIProvider): API_URL = "https://api.groq.com/openai/v1/chat/completions" MODELS_URL = "https://api.groq.com/openai/v1/models" + # Exclude non-chat models + EXCLUDED_PATTERNS = ['whisper', 'tts', 'guard', 'tool-use'] + + # Recommended models (in priority order - versatile/large models first) + RECOMMENDED_PREFIXES = ['llama-3.3', 'llama-3.1-70b', 'llama-3.1-8b', 'mixtral', 'gemma'] + def list_models(self) -> List[str]: - """List available Groq models. + """List available Groq models for chat completions. + + Filters out non-chat models (whisper, guard, etc.) Returns: - List of model IDs available for chat completions. + List of model IDs suitable for chat completions. """ if not self.api_key: return [] @@ -40,10 +48,26 @@ class GroqProvider(AIProvider): models = [] for model in data.get('data', []): model_id = model.get('id', '') - if model_id: - models.append(model_id) + if not model_id: + continue + + model_lower = model_id.lower() + + # Exclude non-chat models + if any(pattern in model_lower for pattern in self.EXCLUDED_PATTERNS): + continue + + models.append(model_id) - return models + # Sort with recommended models first + def sort_key(m): + m_lower = m.lower() + for i, prefix in enumerate(self.RECOMMENDED_PREFIXES): + if m_lower.startswith(prefix): + return (i, m) + return (len(self.RECOMMENDED_PREFIXES), m) + + return sorted(models, key=sort_key) except Exception as e: print(f"[GroqProvider] Failed to list models: {e}") return [] diff --git a/AppImage/scripts/ai_providers/openai_provider.py b/AppImage/scripts/ai_providers/openai_provider.py index 167c8203..d5877da5 100644 --- a/AppImage/scripts/ai_providers/openai_provider.py +++ b/AppImage/scripts/ai_providers/openai_provider.py @@ -27,11 +27,29 @@ class OpenAIProvider(AIProvider): DEFAULT_API_URL = "https://api.openai.com/v1/chat/completions" DEFAULT_MODELS_URL = "https://api.openai.com/v1/models" + # Models to exclude (not suitable for chat/text generation) + EXCLUDED_PATTERNS = [ + 'embedding', 'whisper', 'tts', 'dall-e', 'image', + 'instruct', 'realtime', 'audio', 'moderation', + 'search', 'code-search', 'text-similarity', 'babbage', 'davinci', + 'curie', 'ada', 'transcribe' + ] + + # Recommended models for chat (in priority order) + RECOMMENDED_PREFIXES = ['gpt-4o-mini', 'gpt-4o', 'gpt-4-turbo', 'gpt-4', 'gpt-3.5-turbo'] + def list_models(self) -> List[str]: - """List available OpenAI models. + """List available OpenAI models for chat completions. + + Filters to only chat-capable models, excluding: + - Embedding models + - Audio/speech models (whisper, tts) + - Image models (dall-e) + - Instruct models (different API) + - Legacy models (babbage, davinci, etc.) Returns: - List of model IDs available for chat completions. + List of model IDs suitable for chat completions. """ if not self.api_key: return [] @@ -58,11 +76,30 @@ class OpenAIProvider(AIProvider): models = [] for model in data.get('data', []): model_id = model.get('id', '') - # Filter to chat models only (skip embeddings, etc.) - if model_id and ('gpt' in model_id.lower() or 'turbo' in model_id.lower()): - models.append(model_id) + if not model_id: + continue + + model_lower = model_id.lower() + + # Must be a GPT model + if 'gpt' not in model_lower: + continue + + # Exclude non-chat models + if any(pattern in model_lower for pattern in self.EXCLUDED_PATTERNS): + continue + + models.append(model_id) - return models + # Sort with recommended models first + def sort_key(m): + m_lower = m.lower() + for i, prefix in enumerate(self.RECOMMENDED_PREFIXES): + if m_lower.startswith(prefix): + return (i, m) + return (len(self.RECOMMENDED_PREFIXES), m) + + return sorted(models, key=sort_key) except Exception as e: print(f"[OpenAIProvider] Failed to list models: {e}") return [] diff --git a/AppImage/scripts/ai_providers/openrouter_provider.py b/AppImage/scripts/ai_providers/openrouter_provider.py index 1960547d..c2c7362a 100644 --- a/AppImage/scripts/ai_providers/openrouter_provider.py +++ b/AppImage/scripts/ai_providers/openrouter_provider.py @@ -19,12 +19,23 @@ class OpenRouterProvider(AIProvider): API_URL = "https://openrouter.ai/api/v1/chat/completions" MODELS_URL = "https://openrouter.ai/api/v1/models" + # Exclude non-text models + EXCLUDED_PATTERNS = ['image', 'vision', 'audio', 'video', 'embedding', 'moderation'] + + # Recommended model prefixes (popular, reliable, good for notifications) + RECOMMENDED_PREFIXES = [ + 'meta-llama/llama-3', 'anthropic/claude', 'google/gemini', + 'openai/gpt', 'mistralai/mistral', 'mistralai/mixtral' + ] + def list_models(self) -> List[str]: - """List available OpenRouter models. + """List available OpenRouter models for chat completions. + + OpenRouter has 300+ models. This filters to text generation models + and prioritizes popular, reliable options. Returns: - List of model IDs available. OpenRouter has 100+ models, - this returns only the most popular free/low-cost options. + List of model IDs suitable for text generation. """ if not self.api_key: return [] @@ -42,10 +53,26 @@ class OpenRouterProvider(AIProvider): models = [] for model in data.get('data', []): model_id = model.get('id', '') - if model_id: - models.append(model_id) + if not model_id: + continue + + model_lower = model_id.lower() + + # Exclude non-text models + if any(pattern in model_lower for pattern in self.EXCLUDED_PATTERNS): + continue + + models.append(model_id) - return models + # Sort with recommended models first + def sort_key(m): + m_lower = m.lower() + for i, prefix in enumerate(self.RECOMMENDED_PREFIXES): + if m_lower.startswith(prefix): + return (i, m) + return (len(self.RECOMMENDED_PREFIXES), m) + + return sorted(models, key=sort_key) except Exception as e: print(f"[OpenRouterProvider] Failed to list models: {e}") return [] diff --git a/AppImage/scripts/notification_manager.py b/AppImage/scripts/notification_manager.py index f0d3bff6..5ad29e9d 100644 --- a/AppImage/scripts/notification_manager.py +++ b/AppImage/scripts/notification_manager.py @@ -1501,12 +1501,23 @@ class NotificationManager: current_provider = self._config.get('ai_provider', 'groq') ai_api_keys = { 'groq': self._config.get('ai_api_key_groq', ''), + 'ollama': '', # Ollama doesn't need API key 'gemini': self._config.get('ai_api_key_gemini', ''), 'anthropic': self._config.get('ai_api_key_anthropic', ''), 'openai': self._config.get('ai_api_key_openai', ''), 'openrouter': self._config.get('ai_api_key_openrouter', ''), } + # Get per-provider selected models + ai_models = { + 'groq': self._config.get('ai_model_groq', ''), + 'ollama': self._config.get('ai_model_ollama', ''), + 'gemini': self._config.get('ai_model_gemini', ''), + 'anthropic': self._config.get('ai_model_anthropic', ''), + 'openai': self._config.get('ai_model_openai', ''), + 'openrouter': self._config.get('ai_model_openrouter', ''), + } + # Migrate legacy ai_api_key to per-provider key if exists legacy_api_key = self._config.get('ai_api_key', '') if legacy_api_key and not ai_api_keys.get(current_provider): @@ -1543,6 +1554,7 @@ class NotificationManager: 'ai_enabled': self._config.get('ai_enabled', 'false') == 'true', 'ai_provider': current_provider, 'ai_api_keys': ai_api_keys, + 'ai_models': ai_models, 'ai_model': self._config.get('ai_model', ''), 'ai_language': self._config.get('ai_language', 'en'), 'ai_ollama_url': self._config.get('ai_ollama_url', 'http://localhost:11434'),