From fb14c7e70987e93d41caa5ebcb521d048d77de18 Mon Sep 17 00:00:00 2001 From: Bruno Charest Date: Mon, 14 Sep 2026 22:25:57 -0400 Subject: [PATCH] feat(agent): 24 fournisseurs LLM + refonte du panneau Agent & IA Enregistre OpenAI, Anthropic, Mistral, Cohere, Google Gemini, Groq, DeepSeek, OpenRouter, NVIDIA NIM, Together, Perplexity, xAI, DashScope, MiniMax, Morph, Fireworks, Cerebras, SambaNova, Chutes, Xiaomi, LTX, SEA-LION, SenseNova et MemTensor (URLs de base + presets de modeles + libelles). Settings: panneau Agent & IA repense en maitre/detail (liste rechercheable + volet de configuration) pour tenir avec des dizaines de fournisseurs, en conservant tous les boutons/fonctions. --- app/config.py | 10 +- app/services/llm_client.py | 93 +++++++++++++++++-- app/services/llm_config.py | 4 +- app/templates/settings.html | 179 ++++++++++++++++++++++++++---------- 4 files changed, 225 insertions(+), 61 deletions(-) diff --git a/app/config.py b/app/config.py index 43305ba..11160db 100644 --- a/app/config.py +++ b/app/config.py @@ -77,8 +77,14 @@ class Settings(BaseSettings): # FlowDeck Agent (v4.10.0) — multi-LLM. Empty keys → offline/mock mode # (deterministic rule-based planner so the agent works without any API key). agent_enabled: bool = True - llm_provider: str = "offline" # openai | anthropic | google | ollama | - # deepseek | qwencloud | nvidia | openrouter | offline + llm_provider: str = "offline" # any id from llm_client.PROVIDERS + # (openai, anthropic, mistral, cohere, + # google, groq, deepseek, openrouter, + # nvidia, together, perplexity, xai, + # qwencloud, minimax, morph, fireworks, + # cerebras, sambanova, chutes, xiaomi, + # ltx, sealion, sensenova, memtensor, + # ollama, offline) llm_model: str = "gpt-4o" llm_api_key: str = "" llm_api_base: str = "" # custom base URL (Ollama, OpenRouter, ...) diff --git a/app/services/llm_client.py b/app/services/llm_client.py index bc5c787..609c2f2 100644 --- a/app/services/llm_client.py +++ b/app/services/llm_client.py @@ -5,8 +5,11 @@ directly — it emits *tool intentions* (function calls) that AgentEngine turns into guarded internal actions. Supported providers (OpenAI-compatible chat-completions JSON response): - openai, anthropic*, google*, deepseek, qwencloud, nvidia, openrouter, ollama. - (* routed through an OpenAI-compatible gateway / any configured api_base) + openai, anthropic, mistral, cohere, google, groq, deepseek, openrouter, + nvidia, together, perplexity, xai, qwencloud, minimax, morph, fireworks, + cerebras, sambanova, chutes, xiaomi, ltx, sealion, sensenova, memtensor, + ollama (local, no key). Anthropic/Google/Cohere expose an OpenAI-compatible + surface at their configured base URL. When no API key is configured (or provider == "offline") the client falls back to a deterministic, dependency-free *mock planner*. This keeps the whole agent @@ -28,30 +31,106 @@ from app.config import settings logger = logging.getLogger(__name__) # Provider → default model + base URL when llm_model/api_base are empty. +# All entries speak the OpenAI-compatible chat-completions protocol (Anthropic, +# Google and Cohere expose an OpenAI-compatible surface at their given base). PROVIDERS = { "openai": ("https://api.openai.com/v1", "gpt-4o"), "anthropic": ("https://api.anthropic.com/v1", "claude-opus-4-8"), + "mistral": ("https://api.mistral.ai/v1", "mistral-large-latest"), + "cohere": ("https://api.cohere.com/v2", "command-r-plus"), "google": ("https://generativelanguage.googleapis.com/v1beta", "gemini-2.0-pro"), + "groq": ("https://api.groq.com/openai/v1", "llama-3.3-70b-versatile"), "deepseek": ("https://api.deepseek.com/v1", "deepseek-chat"), - "qwencloud": ("https://dashscope.aliyuncs.com/compatible-mode/v1", "qwen-max"), - "nvidia": ("https://integrate.api.nvidia.com/v1", "nvidia/nemotron-3-super-120b-a12b"), "openrouter": ("https://openrouter.ai/api/v1", "meta-llama/llama-3.3-70b-instruct"), + "nvidia": ("https://integrate.api.nvidia.com/v1", "nvidia/nemotron-3-super-120b-a12b"), + "together": ("https://api.together.xyz/v1", "meta-llama/Llama-3.3-70B-Instruct-Turbo"), + "perplexity": ("https://api.perplexity.ai/v1", "sonar-pro"), + "xai": ("https://api.x.ai/v1", "grok-2-latest"), + "qwencloud": ("https://dashscope-intl.aliyuncs.com/compatible-mode/v1", "qwen-max"), + "minimax": ("https://api.minimax.chat/v1", "MiniMax-Text-01"), + "morph": ("https://api.morphllm.com/v1", "morph-v3-large"), + "fireworks": ("https://api.fireworks.ai/inference/v1", + "accounts/fireworks/models/llama-v3p3-70b-instruct"), + "cerebras": ("https://api.cerebras.ai/v1", "llama-3.3-70b"), + "sambanova": ("https://api.sambanova.ai/v1", "Meta-Llama-3.3-70B-Instruct"), + "chutes": ("https://api.chutes.ai/v1", "deepseek-ai/DeepSeek-V3"), + "xiaomi": ("https://api.xiaomimimo.com/v1", "mimo-7b-rl"), + "ltx": ("https://api.ltx.io/v1", "ltx-video"), + "sealion": ("https://api.sea-lion.ai/v1", "aisingapore/Llama-SEA-LION-v3-70B-IT"), + "sensenova": ("https://token.sensenova.cn/v1", "SenseChat-5"), + "memtensor": ("https://memos.memtensor.cn/api/openmem/v1", "memos-chat"), "ollama": ("http://localhost:11434/v1", "llama3.1"), "offline": (None, None), } +# Friendly display names for the Settings / Agent UIs. +PROVIDER_LABELS: dict[str, str] = { + "openai": "OpenAI", + "anthropic": "Anthropic", + "mistral": "Mistral", + "cohere": "Cohere", + "google": "Google Gemini", + "groq": "Groq", + "deepseek": "DeepSeek", + "openrouter": "OpenRouter", + "nvidia": "NVIDIA NIM", + "together": "Together AI", + "perplexity": "Perplexity", + "xai": "xAI (Grok)", + "qwencloud": "DashScope (Alibaba)", + "minimax": "MiniMax", + "morph": "Morph", + "fireworks": "Fireworks AI", + "cerebras": "Cerebras", + "sambanova": "SambaNova", + "chutes": "Chutes AI", + "xiaomi": "Xiaomi (MiMo)", + "ltx": "LTX Studio", + "sealion": "SEA-LION", + "sensenova": "SenseNova", + "memtensor": "MemTensor (MemOS)", + "ollama": "Ollama (local)", + "offline": "Hors-ligne (mock)", +} + # Curated model presets surfaced by /api/agent/providers for the UI selectors. PROVIDER_MODELS: dict[str, list[str]] = { "openai": ["gpt-4o", "gpt-4o-mini", "gpt-4.1", "gpt-4.1-mini", "o3-mini", "gpt-4-turbo"], "anthropic": ["claude-opus-4-8", "claude-sonnet-4-5", "claude-3-5-sonnet", "claude-haiku-4-5"], + "mistral": ["mistral-large-latest", "mistral-medium-latest", "mistral-small-latest", + "codestral-latest", "open-mistral-nemo", "pixtral-large-latest"], + "cohere": ["command-r-plus", "command-r", "command-light", "command-a"], "google": ["gemini-2.0-pro", "gemini-2.0-flash", "gemini-1.5-pro", "gemini-1.5-flash"], + "groq": ["llama-3.3-70b-versatile", "llama-3.1-8b-instant", + "mixtral-8x7b-32768", "gemma2-9b-it"], "deepseek": ["deepseek-chat", "deepseek-reasoner"], - "qwencloud": ["qwen-max", "qwen-plus", "qwen-turbo", "qwen-long"], + "openrouter": ["meta-llama/llama-3.3-70b-instruct", "anthropic/claude-3.5-sonnet", + "openai/gpt-4o", "mistralai/mistral-large"], "nvidia": ["nvidia/nemotron-3-super-120b-a12b", "nvidia/nemotron-3-nano-30b-a3b", "meta/llama-3.1-70b-instruct", "nvidia/llama-3.3-nemotron-super-49b-v1.5", "deepseek-ai/deepseek-v4-pro", "z-ai/glm-5.2"], - "openrouter": ["meta-llama/llama-3.3-70b-instruct", "anthropic/claude-3.5-sonnet", - "openai/gpt-4o", "mistralai/mistral-large"], + "together": ["meta-llama/Llama-3.3-70B-Instruct-Turbo", + "meta-llama/Meta-Llama-3.1-405B-Instruct-Turbo", + "Qwen/Qwen2.5-72B-Instruct-Turbo", "mistralai/Mixtral-8x7B-Instruct-v0.1"], + "perplexity": ["sonar-pro", "sonar", "sonar-reasoning", "sonar-deep-research"], + "xai": ["grok-2-latest", "grok-2", "grok-beta", "grok-3"], + "qwencloud": ["qwen-max", "qwen-plus", "qwen-turbo", "qwen-long"], + "minimax": ["MiniMax-Text-01", "abab6.5s-chat", "abab6.5-chat"], + "morph": ["morph-v3-large", "morph-v3-fast"], + "fireworks": ["accounts/fireworks/models/llama-v3p3-70b-instruct", + "accounts/fireworks/models/mixtral-8x22b-instruct", + "accounts/fireworks/models/qwen2p5-72b-instruct"], + "cerebras": ["llama-3.3-70b", "llama3.1-8b", "llama-3.1-70b"], + "sambanova": ["Meta-Llama-3.3-70B-Instruct", "Meta-Llama-3.1-405B-Instruct", + "Qwen2.5-72B-Instruct"], + "chutes": ["deepseek-ai/DeepSeek-V3", "deepseek-ai/DeepSeek-R1", + "Qwen/Qwen2.5-72B-Instruct"], + "xiaomi": ["mimo-7b-rl", "mimo-7b"], + "ltx": ["ltx-video", "ltx-video-2"], + "sealion": ["aisingapore/Llama-SEA-LION-v3-70B-IT", + "aisingapore/Gemma-SEA-LION-v3-9B-IT"], + "sensenova": ["SenseChat-5", "SenseChat-5-Cantonese", "SenseChat-Turbo"], + "memtensor": ["memos-chat"], "ollama": ["llama3.1", "llama3", "mistral", "qwen2.5", "gemma2", "mixtral"], "offline": [], } diff --git a/app/services/llm_config.py b/app/services/llm_config.py index 8d32e1a..7a54568 100644 --- a/app/services/llm_config.py +++ b/app/services/llm_config.py @@ -14,7 +14,7 @@ import json import time from app.config import settings -from app.services.llm_client import PROVIDER_MODELS, PROVIDERS +from app.services.llm_client import PROVIDER_LABELS, PROVIDER_MODELS, PROVIDERS # Providers whose /v1/models lists far more entries than /v1/chat/completions # actually serves. The fetched list is validated (name filter + live probe) @@ -165,7 +165,7 @@ def provider_info() -> list[dict]: models.insert(0, default_model) out.append({ "id": name, - "name": name.capitalize(), + "name": PROVIDER_LABELS.get(name) or name.replace("_", " ").title(), "default_model": default_model, "models": models, "base_url": (base or ""), diff --git a/app/templates/settings.html b/app/templates/settings.html index af89334..4ffd62b 100644 --- a/app/templates/settings.html +++ b/app/templates/settings.html @@ -99,6 +99,32 @@ .prov-card .pw-wrapper .settings-input{padding-right:36px;} .prov-msg{font-size:12px;margin-top:8px;min-height:0;word-break:break-word;} .llm-hint{font-size:12px;color:var(--text-secondary);background:rgba(35,131,226,.07);border:1px solid rgba(35,131,226,.18);border-radius:8px;padding:8px 12px;margin-top:14px;line-height:1.5;} + +/* Agent & IA — provider master/detail (scales to dozens of providers) */ +.llm-toolbar{display:flex;align-items:center;gap:10px;margin-bottom:12px;flex-wrap:wrap;} +.llm-search{position:relative;flex:1;min-width:180px;} +.llm-search svg{position:absolute;left:10px;top:50%;transform:translateY(-50%);color:var(--text-dim);pointer-events:none;} +.llm-search input{padding-left:32px;box-sizing:border-box;} +.llm-count{font-size:11px;color:var(--text-dim);white-space:nowrap;text-transform:uppercase;letter-spacing:.4px;} +.llm-layout{display:grid;grid-template-columns:264px 1fr;gap:14px;align-items:start;} +.llm-list{border:1px solid var(--border);border-radius:12px;background:var(--bg-secondary);max-height:480px;overflow-y:auto;padding:6px;display:flex;flex-direction:column;gap:2px;} +.llm-list-item{display:flex;align-items:center;gap:9px;padding:8px 10px;border-radius:8px;cursor:pointer;transition:background 100ms;border:1px solid transparent;} +.llm-list-item:hover{background:var(--bg-hover);} +.llm-list-item.active{background:rgba(35,131,226,.14);border-color:rgba(35,131,226,.3);} +.llm-dot{width:8px;height:8px;border-radius:50%;flex-shrink:0;background:var(--text-dim);} +.llm-dot.ok{background:#2ea043;} +.llm-dot.warn{background:#d9730d;} +.llm-dot.err{background:#e03e3e;} +.llm-list-name{flex:1;min-width:0;font-size:13px;color:var(--text);overflow:hidden;text-overflow:ellipsis;white-space:nowrap;} +.llm-list-status{font-size:10px;color:var(--text-dim);text-transform:uppercase;letter-spacing:.4px;white-space:nowrap;} +.llm-list-empty{padding:12px;font-size:12px;color:var(--text-dim);text-align:center;} +.llm-detail{border:1px solid var(--border);border-radius:12px;background:var(--bg-secondary);padding:16px;min-height:260px;} +.llm-detail-head{display:flex;align-items:center;gap:10px;margin-bottom:4px;flex-wrap:wrap;} +.llm-detail-name{font-size:15px;font-weight:600;color:var(--text);} +.llm-detail-base{font-size:11px;color:var(--text-dim);font-family:ui-monospace,SFMono-Regular,Menlo,monospace;word-break:break-all;margin-bottom:12px;} +.llm-detail .prov-field-row{margin-top:12px;} +.llm-detail-empty{display:flex;align-items:center;justify-content:center;height:260px;font-size:13px;color:var(--text-dim);text-align:center;padding:0 20px;} +@media (max-width:760px){.llm-layout{grid-template-columns:1fr;}.llm-list{max-height:220px;}}
@@ -874,61 +900,82 @@

Mes fournisseurs

-

Enregistrez vos propres clés : elles sont utilisées quand vous choisissez ce fournisseur dans le panneau Agent, et vous rendent le fournisseur disponible (après test réussi).

-
- +
Sélectionnez un fournisseur pour le configurer.
+
- 💡 Comment activer un fournisseur ? Enregistrez votre clé puis cliquez « Enregistrer & tester » (ou « Tester ») : dès que la connexion réussit, le fournisseur et ses modèles apparaissent dans le sélecteur du panneau Agent. + 💡 Comment activer un fournisseur ? Sélectionnez-le, enregistrez votre clé puis cliquez « Enregistrer & tester » (ou « Tester ») : dès que la connexion réussit, le fournisseur et ses modèles apparaissent dans le sélecteur du panneau Agent. Pour Ollama, aucun fournisseur distant : renseignez l'URL locale (ex. http://localhost:11434/v1) puis testez. En mode hors-ligne (aucun fournisseur actif), l'agent reste utilisable mais génère du contenu local, sans appel réseau.
@@ -994,6 +1041,8 @@ function settingsInit() { llmSummaryDesc: '', // Per-user provider keys (multi-providers) keyForms: {}, + llmSearch: '', + selectedProviderId: null, // Automations (v5.1.0) automations: [], @@ -1218,6 +1267,29 @@ function settingsInit() { userProviders() { return (this.llmProviders || []).filter(x => x.id !== 'offline'); }, + selectedProvider() { + return this.providerById(this.selectedProviderId); + }, + filteredProviders() { + var q = (this.llmSearch || '').trim().toLowerCase(); + var list = this.userProviders(); + if (!q) return list; + return list.filter(function(p){ + return (p.name || '').toLowerCase().indexOf(q) !== -1 || + (p.id || '').toLowerCase().indexOf(q) !== -1; + }); + }, + selectProvider(id) { + this.selectedProviderId = id; + this.keyForm(id); + }, + llmShortStatus(p) { + if (!p) return ''; + if (p.id === 'offline') return 'mock'; + if (p.verified) return 'actif'; + if (p.configured) return 'à tester'; + return '—'; + }, // Set the verified/last_error state on the shared provider entry + its keyForm _setProviderState(id, ok, error) { var p = this.providerById(id); @@ -1307,6 +1379,12 @@ function settingsInit() { f.has_key = !!(k && k.has_key); f.last_error = (k && k.last_error) || p.last_error || ''; }); + if (!this.selectedProviderId || !this.providerById(this.selectedProviderId)) { + var pool = this.userProviders(); + var pick = pool.find(x => x.verified) || pool.find(x => x.configured) || pool[0]; + this.selectedProviderId = pick ? pick.id : null; + } + if (this.selectedProviderId) this.keyForm(this.selectedProviderId); this.syncLlmStatus(); } catch(e) {} }, @@ -1402,7 +1480,8 @@ function settingsInit() { finally { f.testing = false; } }, async deleteUserKey(id) { - var pname = id.charAt(0).toUpperCase() + id.slice(1); + var p = this.providerById(id); + var pname = (p && p.name) || id; if (!confirm('Supprimer la clé API pour ' + pname + ' ?\nLe fournisseur ne sera plus disponible dans le panneau Agent.')) return; var f = this.keyForm(id); f.deleting = true; f.msg = ''; f.ok = false;