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.
This commit is contained in:
+8
-2
@@ -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, ...)
|
||||
|
||||
@@ -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": [],
|
||||
}
|
||||
|
||||
@@ -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 ""),
|
||||
|
||||
+129
-50
@@ -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;}}
|
||||
</style>
|
||||
|
||||
<div class="settings-overlay" x-data="settingsInit()" @keydown.escape="window.history.back()">
|
||||
@@ -874,61 +900,82 @@
|
||||
<!-- My providers (per-user API keys) -->
|
||||
<div class="setting-group">
|
||||
<h3>Mes fournisseurs</h3>
|
||||
<p style="font-size:12px;color:var(--text-dim);margin:0 0 12px;">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).</p>
|
||||
<div class="prov-grid">
|
||||
<template x-for="p in userProviders()" :key="p.id">
|
||||
<div class="prov-card">
|
||||
<div class="prov-card-head">
|
||||
<span class="prov-card-name" x-text="p.name"></span>
|
||||
<span class="llm-badge" :class="llmBadgeClass(p)" x-text="llmBadge(p)"></span>
|
||||
<p style="font-size:12px;color:var(--text-dim);margin:0 0 12px;">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 dans la liste pour le configurer.</p>
|
||||
<div class="llm-toolbar">
|
||||
<div class="llm-search">
|
||||
{{ fd_icon("search",14) }}
|
||||
<input type="text" class="settings-input" x-model="llmSearch" placeholder="Rechercher un fournisseur…">
|
||||
</div>
|
||||
<span class="llm-count" x-text="filteredProviders().length + ' fournisseurs'"></span>
|
||||
</div>
|
||||
<div class="llm-layout">
|
||||
<div class="llm-list">
|
||||
<template x-for="p in filteredProviders()" :key="p.id">
|
||||
<div class="llm-list-item" :class="{active: p.id===selectedProviderId}" @click="selectProvider(p.id)">
|
||||
<span class="llm-dot" :class="llmBadgeClass(p)"></span>
|
||||
<span class="llm-list-name" x-text="p.name"></span>
|
||||
<span class="llm-list-status" x-text="llmShortStatus(p)"></span>
|
||||
</div>
|
||||
<div class="prov-card-sub" x-show="keyForm(p.id).last_error" style="color:var(--danger);" x-text="'Dernier test : ' + keyForm(p.id).last_error"></div>
|
||||
<div class="prov-field-row">
|
||||
<div class="prov-field-label">Clé API <span class="muted" x-text="keyForm(p.id).has_key ? '(enregistrée — laisser vide pour conserver)' : ''"></span></div>
|
||||
<div class="pw-wrapper" style="max-width:100%;">
|
||||
<input :type="keyForm(p.id).keyVisible ? 'text' : 'password'" class="settings-input"
|
||||
x-model="keyForm(p.id).api_key" placeholder="sk-…" style="padding-right:36px;box-sizing:border-box;">
|
||||
<button type="button" class="pw-toggle" @click="keyForm(p.id).keyVisible=!keyForm(p.id).keyVisible" style="top:50%;">
|
||||
<span x-show="keyForm(p.id).keyVisible" x-cloak>{{ fd_icon('eye-off',14) }}</span>
|
||||
<span x-show="!keyForm(p.id).keyVisible" x-cloak>{{ fd_icon('eye',14) }}</span>
|
||||
</template>
|
||||
<div class="llm-list-empty" x-show="!filteredProviders().length">Aucun fournisseur</div>
|
||||
</div>
|
||||
<div class="llm-detail">
|
||||
<template x-if="selectedProvider()">
|
||||
<div>
|
||||
<div class="llm-detail-head">
|
||||
<span class="llm-detail-name" x-text="selectedProvider().name"></span>
|
||||
<span class="llm-badge" :class="llmBadgeClass(selectedProvider())" x-text="llmBadge(selectedProvider())"></span>
|
||||
</div>
|
||||
<div class="llm-detail-base" x-text="keyForm(selectedProviderId).api_base || selectedProvider().base_url || '—'"></div>
|
||||
<div class="prov-card-sub" x-show="keyForm(selectedProviderId).last_error" style="color:var(--danger);margin-bottom:0;" x-text="'Dernier test : ' + keyForm(selectedProviderId).last_error"></div>
|
||||
<div class="prov-field-row">
|
||||
<div class="prov-field-label">Clé API <span class="muted" x-text="keyForm(selectedProviderId).has_key ? '(enregistrée — laisser vide pour conserver)' : (selectedProvider().requires_key ? '(requise)' : '(optionnelle)')"></span></div>
|
||||
<div class="pw-wrapper" style="max-width:100%;">
|
||||
<input :type="keyForm(selectedProviderId).keyVisible ? 'text' : 'password'" class="settings-input"
|
||||
x-model="keyForm(selectedProviderId).api_key" placeholder="sk-…" style="padding-right:36px;box-sizing:border-box;">
|
||||
<button type="button" class="pw-toggle" @click="keyForm(selectedProviderId).keyVisible=!keyForm(selectedProviderId).keyVisible" style="top:50%;">
|
||||
<span x-show="keyForm(selectedProviderId).keyVisible" x-cloak>{{ fd_icon('eye-off',14) }}</span>
|
||||
<span x-show="!keyForm(selectedProviderId).keyVisible" x-cloak>{{ fd_icon('eye',14) }}</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="prov-field-row">
|
||||
<div class="prov-field-label">URL API <span class="muted">(optionnelle)</span></div>
|
||||
<input type="text" class="settings-input" x-model="keyForm(selectedProviderId).api_base" placeholder="https://…" style="box-sizing:border-box;">
|
||||
</div>
|
||||
<div class="prov-field-row">
|
||||
<div class="prov-field-label">Modèle par défaut</div>
|
||||
<select class="settings-input" x-model="keyForm(selectedProviderId).model" style="box-sizing:border-box;">
|
||||
<template x-for="m in keyModels(selectedProviderId)" :key="m">
|
||||
<option :value="m" x-text="m"></option>
|
||||
</template>
|
||||
</select>
|
||||
</div>
|
||||
<div class="prov-actions">
|
||||
<button class="btn btn-primary" @click="saveUserKey(selectedProviderId)" :disabled="keyForm(selectedProviderId).saving" style="font-size:12px;padding:6px 12px;">
|
||||
<span x-show="!keyForm(selectedProviderId).saving">{{ fd_icon("save",14) }} Enregistrer & tester</span>
|
||||
<span x-show="keyForm(selectedProviderId).saving">Enregistrement…</span>
|
||||
</button>
|
||||
<button class="btn" @click="testUserKey(selectedProviderId)" :disabled="keyForm(selectedProviderId).testing" style="font-size:12px;padding:6px 12px;">
|
||||
<span x-show="!keyForm(selectedProviderId).testing">🔌 Tester</span>
|
||||
<span x-show="keyForm(selectedProviderId).testing">Test…</span>
|
||||
</button>
|
||||
<button class="btn" @click="loadProviderModels(selectedProviderId)" :disabled="keyForm(selectedProviderId).modelLoading" style="font-size:12px;padding:6px 12px;">
|
||||
<span x-show="!keyForm(selectedProviderId).modelLoading">{{ fd_icon("refresh",14) }} Modèles</span>
|
||||
<span x-show="keyForm(selectedProviderId).modelLoading">Chargement…</span>
|
||||
</button>
|
||||
<button class="btn" @click="deleteUserKey(selectedProviderId)" :disabled="keyForm(selectedProviderId).deleting" x-show="keyForm(selectedProviderId).has_key" style="font-size:12px;padding:6px 12px;color:var(--danger);">Supprimer</button>
|
||||
</div>
|
||||
<div class="prov-msg">
|
||||
<span x-show="keyForm(selectedProviderId).msg" x-text="keyForm(selectedProviderId).msg" :style="{color: keyForm(selectedProviderId).ok ? 'var(--toast-success-bg)' : 'var(--danger)'}"></span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="prov-field-row">
|
||||
<div class="prov-field-label">URL API <span class="muted">(optionnelle)</span></div>
|
||||
<input type="text" class="settings-input" x-model="keyForm(p.id).api_base" placeholder="https://…" style="box-sizing:border-box;">
|
||||
</div>
|
||||
<div class="prov-field-row">
|
||||
<div class="prov-field-label">Modèle par défaut</div>
|
||||
<select class="settings-input" x-model="keyForm(p.id).model" style="box-sizing:border-box;">
|
||||
<template x-for="m in keyModels(p.id)" :key="m">
|
||||
<option :value="m" x-text="m"></option>
|
||||
</template>
|
||||
</select>
|
||||
</div>
|
||||
<div class="prov-actions">
|
||||
<button class="btn btn-primary" @click="saveUserKey(p.id)" :disabled="keyForm(p.id).saving" style="font-size:12px;padding:6px 12px;">
|
||||
<span x-show="!keyForm(p.id).saving">{{ fd_icon("save",14) }} Enregistrer & tester</span>
|
||||
<span x-show="keyForm(p.id).saving">Enregistrement…</span>
|
||||
</button>
|
||||
<button class="btn" @click="testUserKey(p.id)" :disabled="keyForm(p.id).testing" style="font-size:12px;padding:6px 12px;">
|
||||
<span x-show="!keyForm(p.id).testing">🔌 Tester</span>
|
||||
<span x-show="keyForm(p.id).testing">Test…</span>
|
||||
</button>
|
||||
<button class="btn" @click="loadProviderModels(p.id)" :disabled="keyForm(p.id).modelLoading" style="font-size:12px;padding:6px 12px;">
|
||||
<span x-show="!keyForm(p.id).modelLoading">{{ fd_icon("refresh",14) }} Modèles</span>
|
||||
<span x-show="keyForm(p.id).modelLoading">Chargement…</span>
|
||||
</button>
|
||||
<button class="btn" @click="deleteUserKey(p.id)" :disabled="keyForm(p.id).deleting" x-show="keyForm(p.id).has_key" style="font-size:12px;padding:6px 12px;color:var(--danger);">Supprimer</button>
|
||||
</div>
|
||||
<div class="prov-msg">
|
||||
<span x-show="keyForm(p.id).msg" x-text="keyForm(p.id).msg" :style="{color: keyForm(p.id).ok ? 'var(--toast-success-bg)' : 'var(--danger)'}"></span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</template>
|
||||
<div class="llm-detail-empty" x-show="!selectedProvider()">Sélectionnez un fournisseur pour le configurer.</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="llm-hint">
|
||||
💡 <b>Comment activer un fournisseur ?</b> 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.
|
||||
💡 <b>Comment activer un fournisseur ?</b> 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. <code>http://localhost:11434/v1</code>) puis testez.
|
||||
En mode hors-ligne (aucun fournisseur actif), l'agent reste utilisable mais génère du contenu local, sans appel réseau.
|
||||
</div>
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user