mirror of
https://github.com/MacRimi/ProxMenux.git
synced 2026-04-18 10:02:16 +00:00
Update notification service
This commit is contained in:
@@ -58,7 +58,8 @@ interface NotificationConfig {
|
|||||||
ai_enabled: boolean
|
ai_enabled: boolean
|
||||||
ai_provider: string
|
ai_provider: string
|
||||||
ai_api_keys: Record<string, string> // Per-provider API keys
|
ai_api_keys: Record<string, string> // Per-provider API keys
|
||||||
ai_model: string
|
ai_models: Record<string, string> // Per-provider selected models
|
||||||
|
ai_model: string // Current active model (for the selected provider)
|
||||||
ai_language: string
|
ai_language: string
|
||||||
ai_ollama_url: string
|
ai_ollama_url: string
|
||||||
ai_openai_base_url: string
|
ai_openai_base_url: string
|
||||||
@@ -209,6 +210,14 @@ const DEFAULT_CONFIG: NotificationConfig = {
|
|||||||
openai: "",
|
openai: "",
|
||||||
openrouter: "",
|
openrouter: "",
|
||||||
},
|
},
|
||||||
|
ai_models: {
|
||||||
|
groq: "",
|
||||||
|
ollama: "",
|
||||||
|
gemini: "",
|
||||||
|
anthropic: "",
|
||||||
|
openai: "",
|
||||||
|
openrouter: "",
|
||||||
|
},
|
||||||
ai_model: "",
|
ai_model: "",
|
||||||
ai_language: "en",
|
ai_language: "en",
|
||||||
ai_ollama_url: "http://localhost:11434",
|
ai_ollama_url: "http://localhost:11434",
|
||||||
@@ -260,20 +269,32 @@ export function NotificationSettings() {
|
|||||||
try {
|
try {
|
||||||
const data = await fetchApi<{ success: boolean; config: NotificationConfig }>("/api/notifications/settings")
|
const data = await fetchApi<{ success: boolean; config: NotificationConfig }>("/api/notifications/settings")
|
||||||
if (data.success && data.config) {
|
if (data.success && data.config) {
|
||||||
// Backend automatically migrates deprecated AI models to current versions
|
// Ensure ai_api_keys and ai_models objects exist (fallback for older configs)
|
||||||
// Ensure ai_api_keys object exists (fallback for older configs)
|
const configWithDefaults = {
|
||||||
const configWithKeys = {
|
|
||||||
...data.config,
|
...data.config,
|
||||||
ai_api_keys: data.config.ai_api_keys || {
|
ai_api_keys: data.config.ai_api_keys || {
|
||||||
groq: "",
|
groq: "",
|
||||||
|
ollama: "",
|
||||||
|
gemini: "",
|
||||||
|
anthropic: "",
|
||||||
|
openai: "",
|
||||||
|
openrouter: "",
|
||||||
|
},
|
||||||
|
ai_models: data.config.ai_models || {
|
||||||
|
groq: "",
|
||||||
|
ollama: "",
|
||||||
gemini: "",
|
gemini: "",
|
||||||
anthropic: "",
|
anthropic: "",
|
||||||
openai: "",
|
openai: "",
|
||||||
openrouter: "",
|
openrouter: "",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
setConfig(configWithKeys)
|
// If ai_model exists but ai_models doesn't have it, save it
|
||||||
setOriginalConfig(configWithKeys)
|
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) {
|
} catch (err) {
|
||||||
console.error("Failed to load notification settings:", 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, ...
|
// Flatten channels: { telegram: { enabled, bot_token, chat_id } } -> telegram.enabled, telegram.bot_token, ...
|
||||||
for (const [chName, chCfg] of Object.entries(cfg.channels)) {
|
for (const [chName, chCfg] of Object.entries(cfg.channels)) {
|
||||||
for (const [field, value] of Object.entries(chCfg)) {
|
for (const [field, value] of Object.entries(chCfg)) {
|
||||||
@@ -1452,10 +1481,23 @@ export function NotificationSettings() {
|
|||||||
<Select
|
<Select
|
||||||
value={config.ai_provider}
|
value={config.ai_provider}
|
||||||
onValueChange={v => {
|
onValueChange={v => {
|
||||||
// When changing provider, clear model and models list
|
// Save current model for current provider before switching
|
||||||
// User will need to click "Load" to fetch available models
|
const currentProvider = config.ai_provider
|
||||||
updateConfig(p => ({ ...p, ai_provider: v, ai_model: '' }))
|
const currentModel = config.ai_model
|
||||||
setProviderModels([])
|
|
||||||
|
// Restore previously saved model for the new provider (if any)
|
||||||
|
const savedModel = config.ai_models?.[v] || ''
|
||||||
|
|
||||||
|
updateConfig(p => ({
|
||||||
|
...p,
|
||||||
|
ai_provider: v,
|
||||||
|
ai_model: savedModel,
|
||||||
|
ai_models: {
|
||||||
|
...p.ai_models,
|
||||||
|
[currentProvider]: currentModel // Save old provider's model
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
setProviderModels([]) // Clear loaded models list
|
||||||
}}
|
}}
|
||||||
disabled={!editMode}
|
disabled={!editMode}
|
||||||
>
|
>
|
||||||
@@ -1551,7 +1593,11 @@ export function NotificationSettings() {
|
|||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<Select
|
<Select
|
||||||
value={config.ai_model || ""}
|
value={config.ai_model || ""}
|
||||||
onValueChange={v => 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}
|
disabled={!editMode || loadingProviderModels || providerModels.length === 0}
|
||||||
>
|
>
|
||||||
<SelectTrigger className="h-9 text-sm font-mono flex-1">
|
<SelectTrigger className="h-9 text-sm font-mono flex-1">
|
||||||
|
|||||||
@@ -17,9 +17,22 @@ class GeminiProvider(AIProvider):
|
|||||||
REQUIRES_API_KEY = True
|
REQUIRES_API_KEY = True
|
||||||
API_BASE = "https://generativelanguage.googleapis.com/v1beta/models"
|
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]:
|
def list_models(self) -> List[str]:
|
||||||
"""List available Gemini models that support generateContent.
|
"""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:
|
Returns:
|
||||||
List of model IDs available for text generation.
|
List of model IDs available for text generation.
|
||||||
"""
|
"""
|
||||||
@@ -44,10 +57,28 @@ class GeminiProvider(AIProvider):
|
|||||||
|
|
||||||
# Only include models that support generateContent
|
# Only include models that support generateContent
|
||||||
supported_methods = model.get('supportedGenerationMethods', [])
|
supported_methods = model.get('supportedGenerationMethods', [])
|
||||||
if 'generateContent' in supported_methods:
|
if 'generateContent' not in supported_methods:
|
||||||
models.append(model_id)
|
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:
|
except Exception as e:
|
||||||
print(f"[GeminiProvider] Failed to list models: {e}")
|
print(f"[GeminiProvider] Failed to list models: {e}")
|
||||||
return []
|
return []
|
||||||
|
|||||||
@@ -18,11 +18,19 @@ class GroqProvider(AIProvider):
|
|||||||
API_URL = "https://api.groq.com/openai/v1/chat/completions"
|
API_URL = "https://api.groq.com/openai/v1/chat/completions"
|
||||||
MODELS_URL = "https://api.groq.com/openai/v1/models"
|
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]:
|
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:
|
Returns:
|
||||||
List of model IDs available for chat completions.
|
List of model IDs suitable for chat completions.
|
||||||
"""
|
"""
|
||||||
if not self.api_key:
|
if not self.api_key:
|
||||||
return []
|
return []
|
||||||
@@ -40,10 +48,26 @@ class GroqProvider(AIProvider):
|
|||||||
models = []
|
models = []
|
||||||
for model in data.get('data', []):
|
for model in data.get('data', []):
|
||||||
model_id = model.get('id', '')
|
model_id = model.get('id', '')
|
||||||
if model_id:
|
if not model_id:
|
||||||
models.append(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:
|
except Exception as e:
|
||||||
print(f"[GroqProvider] Failed to list models: {e}")
|
print(f"[GroqProvider] Failed to list models: {e}")
|
||||||
return []
|
return []
|
||||||
|
|||||||
@@ -27,11 +27,29 @@ class OpenAIProvider(AIProvider):
|
|||||||
DEFAULT_API_URL = "https://api.openai.com/v1/chat/completions"
|
DEFAULT_API_URL = "https://api.openai.com/v1/chat/completions"
|
||||||
DEFAULT_MODELS_URL = "https://api.openai.com/v1/models"
|
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]:
|
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:
|
Returns:
|
||||||
List of model IDs available for chat completions.
|
List of model IDs suitable for chat completions.
|
||||||
"""
|
"""
|
||||||
if not self.api_key:
|
if not self.api_key:
|
||||||
return []
|
return []
|
||||||
@@ -58,11 +76,30 @@ class OpenAIProvider(AIProvider):
|
|||||||
models = []
|
models = []
|
||||||
for model in data.get('data', []):
|
for model in data.get('data', []):
|
||||||
model_id = model.get('id', '')
|
model_id = model.get('id', '')
|
||||||
# Filter to chat models only (skip embeddings, etc.)
|
if not model_id:
|
||||||
if model_id and ('gpt' in model_id.lower() or 'turbo' in model_id.lower()):
|
continue
|
||||||
models.append(model_id)
|
|
||||||
|
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:
|
except Exception as e:
|
||||||
print(f"[OpenAIProvider] Failed to list models: {e}")
|
print(f"[OpenAIProvider] Failed to list models: {e}")
|
||||||
return []
|
return []
|
||||||
|
|||||||
@@ -19,12 +19,23 @@ class OpenRouterProvider(AIProvider):
|
|||||||
API_URL = "https://openrouter.ai/api/v1/chat/completions"
|
API_URL = "https://openrouter.ai/api/v1/chat/completions"
|
||||||
MODELS_URL = "https://openrouter.ai/api/v1/models"
|
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]:
|
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:
|
Returns:
|
||||||
List of model IDs available. OpenRouter has 100+ models,
|
List of model IDs suitable for text generation.
|
||||||
this returns only the most popular free/low-cost options.
|
|
||||||
"""
|
"""
|
||||||
if not self.api_key:
|
if not self.api_key:
|
||||||
return []
|
return []
|
||||||
@@ -42,10 +53,26 @@ class OpenRouterProvider(AIProvider):
|
|||||||
models = []
|
models = []
|
||||||
for model in data.get('data', []):
|
for model in data.get('data', []):
|
||||||
model_id = model.get('id', '')
|
model_id = model.get('id', '')
|
||||||
if model_id:
|
if not model_id:
|
||||||
models.append(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:
|
except Exception as e:
|
||||||
print(f"[OpenRouterProvider] Failed to list models: {e}")
|
print(f"[OpenRouterProvider] Failed to list models: {e}")
|
||||||
return []
|
return []
|
||||||
|
|||||||
@@ -1501,12 +1501,23 @@ class NotificationManager:
|
|||||||
current_provider = self._config.get('ai_provider', 'groq')
|
current_provider = self._config.get('ai_provider', 'groq')
|
||||||
ai_api_keys = {
|
ai_api_keys = {
|
||||||
'groq': self._config.get('ai_api_key_groq', ''),
|
'groq': self._config.get('ai_api_key_groq', ''),
|
||||||
|
'ollama': '', # Ollama doesn't need API key
|
||||||
'gemini': self._config.get('ai_api_key_gemini', ''),
|
'gemini': self._config.get('ai_api_key_gemini', ''),
|
||||||
'anthropic': self._config.get('ai_api_key_anthropic', ''),
|
'anthropic': self._config.get('ai_api_key_anthropic', ''),
|
||||||
'openai': self._config.get('ai_api_key_openai', ''),
|
'openai': self._config.get('ai_api_key_openai', ''),
|
||||||
'openrouter': self._config.get('ai_api_key_openrouter', ''),
|
'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
|
# Migrate legacy ai_api_key to per-provider key if exists
|
||||||
legacy_api_key = self._config.get('ai_api_key', '')
|
legacy_api_key = self._config.get('ai_api_key', '')
|
||||||
if legacy_api_key and not ai_api_keys.get(current_provider):
|
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_enabled': self._config.get('ai_enabled', 'false') == 'true',
|
||||||
'ai_provider': current_provider,
|
'ai_provider': current_provider,
|
||||||
'ai_api_keys': ai_api_keys,
|
'ai_api_keys': ai_api_keys,
|
||||||
|
'ai_models': ai_models,
|
||||||
'ai_model': self._config.get('ai_model', ''),
|
'ai_model': self._config.get('ai_model', ''),
|
||||||
'ai_language': self._config.get('ai_language', 'en'),
|
'ai_language': self._config.get('ai_language', 'en'),
|
||||||
'ai_ollama_url': self._config.get('ai_ollama_url', 'http://localhost:11434'),
|
'ai_ollama_url': self._config.get('ai_ollama_url', 'http://localhost:11434'),
|
||||||
|
|||||||
Reference in New Issue
Block a user