diff --git a/README.md b/README.md index 5206ed4..9be1ed0 100644 --- a/README.md +++ b/README.md @@ -87,7 +87,7 @@ npm run dev # http://localhost:5173 | Variable | Défaut | Rôle | | --------------- | ----------------------------------- | ----------------------------- | | `OLLAMA_HOST` | `http://host.docker.internal:11434` | URL de l'instance Ollama | -| `DEFAULT_MODEL` | `llama3.1:8b` | Modèle sélectionné au démarrage | +| `DEFAULT_MODEL` | `gemma4:12b` | Modèle sélectionné au démarrage | | `WORKSPACE_DIR` | `/workspace` | Dossier de travail de l'agent | | `DATA_DIR` | `/data` | Base SQLite (sessions + config) | | `PORT` | `8717` | Port de l'application (dedans = dehors) | @@ -96,8 +96,7 @@ npm run dev # http://localhost:5173 ## Utilisation 1. Vérifie la pastille **Ollama** (verte = connecté) en haut à droite, et choisis - un modèle qui supporte le *function calling* (ex. `llama3.1:8b`, - `qwen2.5-coder`). + un modèle qui supporte le *function calling* (profil fourni : `gemma4:12b`). 2. Décris une tâche dans le tchat, p. ex. *« Crée une landing page pour un café nommé Café Lumière, avec menu et horaires »*. 3. L'agent lit/écrit des fichiers dans le **workspace** ; chaque appel d'outil @@ -116,25 +115,18 @@ npm run dev # http://localhost:5173 | `web_search` | Recherche web (DuckDuckGo / SearxNG) | désactivé | | `run_shell` | Exécuter une commande **(sensible)** | désactivé | -## Auto-réglage GPU (« Réglage auto ») +## Profil GPU fourni -Dans **Configuration → Génération**, le bouton **⚡ Réglage auto** : -1. détecte le GPU (VRAM) via `nvidia-smi`/`rocm-smi`, ou la valeur déclarée - `GPU_VRAM_MB` ; -2. lit les métadonnées du modèle sélectionné via Ollama (contexte max, - architecture, taille, quantization) ; -3. calcule la **fenêtre de contexte (`num_ctx`)** la plus grande qui tient en - VRAM (estimation du cache KV) et un nombre de **jetons max** cohérent, puis - les applique ; -4. vérifie le **placement réel** du modèle via `/api/ps` d'Ollama (GPU / CPU). +Loki est préconfiguré pour une **RTX 3060 12 Go** avec `gemma4:12b` en Q4 : +contexte 8192, sortie 4096 jetons, batch 256, GPU principal 0 et les 49 couches +du modèle sur le GPU. Les paramètres de génération et de cache/contexte sont +enregistrés séparément pour chaque modèle. -> **Ollama distant** : si Ollama tourne sur une autre machine que Loki, la -> détection `nvidia-smi` ne voit pas ce GPU. Déclare alors la VRAM avec -> `GPU_VRAM_MB` (ex. `12000` pour une carte 12 Go) pour un réglage précis. -> -> **Pourquoi c'est utile** : un `num_ctx` trop grand fait déborder le modèle sur -> le CPU (lent). En l'ajustant à ta VRAM, le modèle reste sur le GPU. Laisse -> `num_ctx = 0` (auto) pour utiliser le défaut du modèle. +La quantification du cache KV reste globale dans Ollama. Pour économiser environ +la moitié de sa VRAM, démarre Ollama avec `OLLAMA_FLASH_ATTENTION=1` et +`OLLAMA_KV_CACHE_TYPE=q8_0`. Un redémarrage d'Ollama est requis. Le conteneur +doit également exposer le GPU (`--gpus=all`) ; Loki ne peut pas contourner une +configuration Docker sans accès CUDA. ## Sécurité diff --git a/backend/app/agent.py b/backend/app/agent.py index 8eaa8eb..0cbb7ce 100644 --- a/backend/app/agent.py +++ b/backend/app/agent.py @@ -22,6 +22,7 @@ from .ollama_client import OllamaError, ollama from .tools import TOOL_DEFINITIONS, ToolError, run_tool MAX_ITERATIONS = 6 +MAX_TOOL_REPAIR_ATTEMPTS = 2 def _tools_not_supported(exc: OllamaError) -> bool: @@ -30,6 +31,15 @@ def _tools_not_supported(exc: OllamaError) -> bool: return "does not support tools" in message or "does not support tool" in message +def _invalid_tool_arguments(exc: OllamaError) -> bool: + message = str(exc).lower() + return ( + "invalid tool call arguments" in message + or "unexpected end of json" in message + or "failed to parse tool" in message + ) + + def _parse_args(raw) -> dict: if isinstance(raw, dict): return raw @@ -62,6 +72,8 @@ async def run_agent( text_parts: list[str] = [] active_tools = tools tool_fallback_used = False + tool_repair_attempts = 0 + request_options = dict(options or {}) try: for _ in range(MAX_ITERATIONS): @@ -75,7 +87,11 @@ async def run_agent( while True: try: async for chunk in ollama.chat( - model, convo, tools=active_tools, options=options, stream=True + model, + convo, + tools=active_tools, + options=request_options, + stream=True, ): msg = chunk.get("message", {}) token = msg.get("content", "") @@ -94,6 +110,38 @@ async def run_agent( break break except OllamaError as exc: + if ( + active_tools + and tool_repair_attempts < MAX_TOOL_REPAIR_ATTEMPTS + and not content_buf + and not tool_calls + and _invalid_tool_arguments(exc) + ): + tool_repair_attempts += 1 + request_options["num_predict"] = max( + int(request_options.get("num_predict", 0)), 4096 + ) + thinking_buf = "" + convo.append( + { + "role": "system", + "content": ( + "L'appel d'outil précédent contenait un JSON " + "tronqué. Réessaie immédiatement avec des arguments " + "JSON valides. Pour un fichier long, utilise write_file " + "en plusieurs appels : overwrite puis append, avec des " + "morceaux courts et complets." + ), + } + ) + yield { + "type": "notice", + "message": ( + "Appel d'outil tronqué : nouvelle tentative " + f"{tool_repair_attempts}/{MAX_TOOL_REPAIR_ATTEMPTS}." + ), + } + continue if ( active_tools and not tool_fallback_used @@ -188,7 +236,9 @@ async def run_agent( if awaiting_confirmation: # Laisse le modèle conclure son tour (message d'attente). final_chunk = "" - async for chunk in ollama.chat(model, convo, options=options, stream=True): + async for chunk in ollama.chat( + model, convo, options=request_options, stream=True + ): tok = chunk.get("message", {}).get("content", "") if tok: final_chunk += tok diff --git a/backend/app/agent_config.py b/backend/app/agent_config.py index d08d719..02a5686 100644 --- a/backend/app/agent_config.py +++ b/backend/app/agent_config.py @@ -8,12 +8,17 @@ from __future__ import annotations from . import db CONFIG_KEY = "agent" +MODEL_PROFILES_KEY = "model_profiles" +PROFILE_STATE_KEY = "model_profiles_state" +PROFILE_VERSION = 3 DEFAULT_SYSTEM_PROMPT = ( "Tu es Loki, un assistant de développement local agentique. Tu disposes " "d'outils pour lire, écrire et lister des fichiers dans le workspace. " "Utilise-les pour accomplir les tâches concrètement, puis réponds de façon " - "concise en français. Après avoir écrit un fichier, propose un aperçu." + "concise en français. Pour un fichier long, appelle write_file en plusieurs " + "morceaux (overwrite puis append) afin de toujours produire un JSON valide. " + "Après avoir écrit un fichier, propose un aperçu." ) # Outils disponibles. Les sensibles (web_search, run_shell) sont désactivés @@ -28,24 +33,73 @@ DEFAULT_TOOL_STATE = { "run_shell": False, } -DEFAULT_CONFIG: dict = { - "system_prompt": DEFAULT_SYSTEM_PROMPT, +GENERATION_FIELDS = { + "temperature", + "top_p", + "top_k", + "max_tokens", + "num_ctx", + "num_gpu", + "num_batch", +} + +DEFAULT_GENERATION: dict = { "temperature": 0.7, "top_p": 0.9, "top_k": 40, "max_tokens": 2048, - # Fenêtre de contexte envoyée à Ollama (0 = laisser le défaut du modèle). - "num_ctx": 0, + "num_ctx": 4096, + "num_gpu": -1, + "num_batch": 256, +} + +RTX_3060_GEMMA4_PROFILE: dict = { + **DEFAULT_GENERATION, + "max_tokens": 4096, + "num_ctx": 8192, + "num_gpu": 49, +} + +DEFAULT_CONFIG: dict = { + "system_prompt": DEFAULT_SYSTEM_PROMPT, + **DEFAULT_GENERATION, "tools": dict(DEFAULT_TOOL_STATE), # Demander une validation utilisateur avant toute commande shell. "confirm_shell": True, } -def get_config() -> dict: - """Config courante = défauts fusionnés avec le stockage.""" +def _default_generation(model: str | None) -> dict: + if model and model.split(":", 1)[0].lower() == "gemma4": + return dict(RTX_3060_GEMMA4_PROFILE) + return dict(DEFAULT_GENERATION) + + +def _migrate_profiles() -> None: + state = db.get_config_value(PROFILE_STATE_KEY) or {} + if state.get("version", 0) >= PROFILE_VERSION: + return + profiles = db.get_config_value(MODEL_PROFILES_KEY) or {} + gemma_profile = { + **RTX_3060_GEMMA4_PROFILE, + **profiles.get("gemma4:12b", {}), + } + if gemma_profile.get("max_tokens", 0) <= 2048: + gemma_profile["max_tokens"] = 4096 + profiles["gemma4:12b"] = gemma_profile + db.set_config_value(MODEL_PROFILES_KEY, profiles) + db.set_config_value(PROFILE_STATE_KEY, {"version": PROFILE_VERSION}) + + +def get_config(model: str | None = None) -> dict: + """Configuration globale complétée par le profil du modèle demandé.""" + _migrate_profiles() stored = db.get_config_value(CONFIG_KEY) or {} - cfg = {**DEFAULT_CONFIG, **stored} + global_stored = {k: v for k, v in stored.items() if k not in GENERATION_FIELDS} + cfg = {**DEFAULT_CONFIG, **global_stored} + if model: + profiles = db.get_config_value(MODEL_PROFILES_KEY) or {} + cfg.update({**_default_generation(model), **profiles.get(model, {})}) cfg["tools"] = { name: bool(stored.get("tools", {}).get(name, DEFAULT_TOOL_STATE[name])) for name in AVAILABLE_TOOLS @@ -53,9 +107,10 @@ def get_config() -> dict: return cfg -def save_config(patch: dict) -> dict: - """Applique une mise à jour partielle et renvoie la config complète.""" - cfg = {**get_config(), **{k: v for k, v in patch.items() if v is not None}} +def save_config(patch: dict, model: str | None = None) -> dict: + """Sauvegarde le comportement global et la génération par modèle.""" + clean = {k: v for k, v in patch.items() if v is not None} + cfg = {**get_config(model), **clean} if "tools" in patch and patch["tools"]: cfg["tools"] = { name: bool( @@ -65,8 +120,14 @@ def save_config(patch: dict) -> dict: ) for name in AVAILABLE_TOOLS } - db.set_config_value(CONFIG_KEY, cfg) - return get_config() + global_cfg = {k: v for k, v in cfg.items() if k not in GENERATION_FIELDS} + db.set_config_value(CONFIG_KEY, global_cfg) + + if model: + profiles = db.get_config_value(MODEL_PROFILES_KEY) or {} + profiles[model] = {field: cfg[field] for field in GENERATION_FIELDS} + db.set_config_value(MODEL_PROFILES_KEY, profiles) + return get_config(model) def ollama_options(cfg: dict) -> dict: @@ -76,6 +137,9 @@ def ollama_options(cfg: dict) -> dict: "top_p": cfg["top_p"], "top_k": cfg["top_k"], "num_predict": cfg["max_tokens"], + "num_gpu": cfg["num_gpu"], + "main_gpu": 0, + "num_batch": cfg["num_batch"], } # num_ctx n'est envoyé que s'il est défini (> 0), sinon défaut du modèle. if cfg.get("num_ctx"): diff --git a/backend/app/config.py b/backend/app/config.py index d3fb6fa..a007458 100644 --- a/backend/app/config.py +++ b/backend/app/config.py @@ -6,16 +6,11 @@ class Settings(BaseSettings): """Réglages globaux de Loki (surchargés par variables d'environnement).""" ollama_host: str = "http://host.docker.internal:11434" - default_model: str = "llama3.1:8b" + default_model: str = "gemma4:12b" workspace_dir: str = "/workspace" data_dir: str = "/data" port: int = 8080 - # Override manuel de la VRAM (Mo) si la détection GPU échoue dans le - # conteneur (utile sur Unraid où le GPU est sur l'hôte / un autre conteneur). - gpu_vram_mb: int = 0 - gpu_name: str = "" - model_config = SettingsConfigDict(env_file=".env", extra="ignore") diff --git a/backend/app/routes/chat.py b/backend/app/routes/chat.py index b5ad87d..7c7e619 100644 --- a/backend/app/routes/chat.py +++ b/backend/app/routes/chat.py @@ -42,7 +42,7 @@ async def chat(req: ChatRequest) -> StreamingResponse: raise HTTPException(404, "session introuvable") model = req.model or session.get("model") or settings.default_model - cfg = agent_config.get_config() + cfg = agent_config.get_config(model) # Premier message : titre la session avec un extrait. if not db.list_messages(req.session_id): diff --git a/backend/app/routes/config.py b/backend/app/routes/config.py index 74cdb6b..8dc7e85 100644 --- a/backend/app/routes/config.py +++ b/backend/app/routes/config.py @@ -1,11 +1,10 @@ -"""Routes de configuration de l'agent (lecture / mise à jour / auto-réglage).""" +"""Routes de lecture et mise à jour de la configuration de l'agent.""" from __future__ import annotations from fastapi import APIRouter from pydantic import BaseModel -from .. import agent_config, autotune -from ..config import settings +from .. import agent_config router = APIRouter(prefix="/api/config", tags=["config"]) @@ -17,48 +16,21 @@ class ConfigPatch(BaseModel): top_k: int | None = None max_tokens: int | None = None num_ctx: int | None = None + num_gpu: int | None = None + num_batch: int | None = None tools: dict[str, bool] | None = None confirm_shell: bool | None = None -class AutoTuneRequest(BaseModel): - model: str - apply: bool = True - - @router.get("") -async def get_config() -> dict: +async def get_config(model: str | None = None) -> dict: return { - "config": agent_config.get_config(), + "config": agent_config.get_config(model), "available_tools": agent_config.AVAILABLE_TOOLS, } @router.put("") -async def put_config(patch: ConfigPatch) -> dict: - cfg = agent_config.save_config(patch.model_dump(exclude_none=True)) +async def put_config(patch: ConfigPatch, model: str | None = None) -> dict: + cfg = agent_config.save_config(patch.model_dump(exclude_none=True), model) return {"config": cfg} - - -@router.post("/auto") -async def auto_tune(req: AutoTuneRequest) -> dict: - """Détecte le GPU + le modèle, calcule les réglages optimaux, et (par - défaut) les applique. Renvoie le détail de la détection pour l'UI.""" - reco = await autotune.recommend(req.model) - placement = await autotune.placement(req.model) - - applied = None - if req.apply: - applied = agent_config.save_config( - { - "num_ctx": reco["recommended"]["num_ctx"], - "max_tokens": reco["recommended"]["max_tokens"], - } - ) - - return { - "detection": reco, - "placement": placement, - "config": applied or agent_config.get_config(), - "vram_override": settings.gpu_vram_mb, - } diff --git a/backend/app/tools.py b/backend/app/tools.py index 794af5b..3489ffa 100644 --- a/backend/app/tools.py +++ b/backend/app/tools.py @@ -50,14 +50,16 @@ def read_file(path: str) -> dict: return {"ok": True, "content": content, "summary": summary} -def write_file(path: str, content: str) -> dict: +def write_file(path: str, content: str, mode: str = "overwrite") -> dict: target = _safe_path(path) + if mode not in {"overwrite", "append"}: + raise ToolError("mode write_file invalide : utilise overwrite ou append") os.makedirs(os.path.dirname(target) or _workspace_root(), exist_ok=True) existed = os.path.isfile(target) - with open(target, "w", encoding="utf-8") as f: + with open(target, "a" if mode == "append" else "w", encoding="utf-8") as f: f.write(content) lines = len(content.splitlines()) - verb = "modifié" if existed else "écrit" + verb = "complété" if mode == "append" else "modifié" if existed else "écrit" return {"ok": True, "summary": f"{verb} · {lines} lignes", "lines": lines} @@ -210,7 +212,17 @@ TOOL_DEFINITIONS = [ "type": "object", "properties": { "path": {"type": "string", "description": "Chemin relatif au workspace"}, - "content": {"type": "string", "description": "Contenu complet du fichier"}, + "content": { + "type": "string", + "description": "Contenu complet ou morceau court du fichier", + }, + "mode": { + "type": "string", + "enum": ["overwrite", "append"], + "description": ( + "overwrite pour le premier morceau, append pour les suivants" + ), + }, }, "required": ["path", "content"], }, diff --git a/docker-compose.unraid.yml b/docker-compose.unraid.yml index ee59d24..0120f76 100644 --- a/docker-compose.unraid.yml +++ b/docker-compose.unraid.yml @@ -29,15 +29,12 @@ services: environment: # >>> À ADAPTER : IP de ton serveur Unraid (Ollama écoute sur :11434) <<< - OLLAMA_HOST=http://192.168.1.10:11434 - - DEFAULT_MODEL=llama3.1:8b + - DEFAULT_MODEL=gemma4:12b - WORKSPACE_DIR=/workspace - DATA_DIR=/data - PORT=8717 # Optionnel : instance SearxNG pour l'outil web_search (sinon DuckDuckGo) - SEARX_URL= - # Optionnel : VRAM (Mo) de ton GPU pour l'auto-réglage, si Ollama est sur - # une autre machine (sinon laisse 0). Ex. 12000 pour une carte 12 Go. - - GPU_VRAM_MB=0 volumes: - /mnt/user/appdata/loki/workspace:/workspace - /mnt/user/appdata/loki/data:/data diff --git a/docker-compose.yml b/docker-compose.yml index cace754..c405603 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -7,7 +7,7 @@ services: - "${PORT:-8717}:${PORT:-8717}" environment: - OLLAMA_HOST=${OLLAMA_HOST:-http://host.docker.internal:11434} - - DEFAULT_MODEL=${DEFAULT_MODEL:-llama3.1:8b} + - DEFAULT_MODEL=${DEFAULT_MODEL:-gemma4:12b} - WORKSPACE_DIR=/workspace - DATA_DIR=/data - PORT=${PORT:-8717} @@ -30,6 +30,11 @@ services: image: ollama/ollama:latest container_name: loki-ollama profiles: ["ollama"] + environment: + - NVIDIA_VISIBLE_DEVICES=all + - NVIDIA_DRIVER_CAPABILITIES=compute,utility + - OLLAMA_FLASH_ATTENTION=1 + - OLLAMA_KV_CACHE_TYPE=q8_0 ports: - "11434:11434" volumes: diff --git a/frontend/src/api/client.ts b/frontend/src/api/client.ts index 9a04038..44b2570 100644 --- a/frontend/src/api/client.ts +++ b/frontend/src/api/client.ts @@ -62,38 +62,12 @@ export interface AgentConfig { top_k: number; max_tokens: number; num_ctx: number; + num_gpu: number; + num_batch: number; tools: Record; confirm_shell: boolean; } -export interface AutoTuneResult { - detection: { - gpu: { available: boolean; name: string; vram_total_mb: number; source: string }; - model_profile: { - context_length: number | null; - parameter_size: string | null; - quantization: string | null; - size_mb: number | null; - }; - recommended: { num_ctx: number; max_tokens: number }; - rationale: string; - }; - placement: { loaded: boolean; where?: string; gpu_percent?: number }; - config: AgentConfig; -} - -export async function autoTune( - model: string, - apply = true -): Promise { - const res = await fetch("/api/config/auto", { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ model, apply }), - }); - return res.json(); -} - export async function runShell( command: string ): Promise<{ command: string; exit_code: number; output: string }> { @@ -105,18 +79,21 @@ export async function runShell( return res.json(); } -export async function getConfig(): Promise<{ +export async function getConfig(model?: string): Promise<{ config: AgentConfig; available_tools: string[]; }> { - const res = await fetch("/api/config"); + const query = model ? `?model=${encodeURIComponent(model)}` : ""; + const res = await fetch(`/api/config${query}`); return res.json(); } export async function saveConfig( - patch: Partial + patch: Partial, + model?: string ): Promise { - const res = await fetch("/api/config", { + const query = model ? `?model=${encodeURIComponent(model)}` : ""; + const res = await fetch(`/api/config${query}`, { method: "PUT", headers: { "Content-Type": "application/json" }, body: JSON.stringify(patch), diff --git a/frontend/src/panels/SettingsView.tsx b/frontend/src/panels/SettingsView.tsx index be7c5c9..e570a7c 100644 --- a/frontend/src/panels/SettingsView.tsx +++ b/frontend/src/panels/SettingsView.tsx @@ -1,7 +1,7 @@ import { useEffect, useState } from "react"; import { useStore } from "../store/useStore"; import { pullModel } from "../api/client"; -import type { AgentConfig, AutoTuneResult } from "../api/client"; +import type { AgentConfig } from "../api/client"; import { DownloadIcon, RefreshIcon } from "../components/Icon"; const TOOL_DESC: Record = { @@ -25,9 +25,6 @@ export function SettingsView() { availableTools, refreshConfig, updateConfig, - tuning, - tuneResult, - runAutoTune, } = useStore(); // Brouillon local édité, synchronisé depuis la config serveur. @@ -36,7 +33,7 @@ export function SettingsView() { useEffect(() => { refreshConfig(); - }, [refreshConfig]); + }, [refreshConfig, selectedModel]); useEffect(() => { if (config) setDraft(config); }, [config]); @@ -224,25 +221,12 @@ export function SettingsView() { {/* Génération + Outils */}
-
-
Génération
- +
Génération
+
+ Profil enregistré pour {selectedModel || "ce modèle"}. + Le contexte détermine directement la taille du cache KV.
- {tuneResult && } - set("max_tokens", Math.round(v))} /> - v === 0 ? "auto" : v >= 1024 ? `${v / 1024}K` : String(v) - } + fmt={(v) => `${Math.round(v / 1024)}K`} onChange={(v) => set("num_ctx", Math.round(v))} + /> + (v < 0 ? "auto" : String(Math.round(v)))} + onChange={(v) => set("num_gpu", Math.round(v))} + /> + String(Math.round(v))} + onChange={(v) => set("num_batch", Math.round(v))} last /> +
+ La précision KV (f16/q8_0) est un + réglage global du serveur Ollama et nécessite son redémarrage. + Les valeurs ci-dessus sont, elles, propres à chaque modèle. +
@@ -384,51 +389,6 @@ export function SettingsView() { ); } -function TuneBanner({ result }: { result: AutoTuneResult }) { - const { gpu, model_profile, recommended, rationale } = result.detection; - const place = result.placement; - - const placeLabel = - place.loaded && place.where - ? place.where === "gpu" - ? `chargé GPU (${place.gpu_percent}%)` - : place.where === "cpu" - ? "chargé CPU ⚠️" - : `mixte GPU ${place.gpu_percent}%` - : "non chargé"; - const placeColor = - place.where === "gpu" ? "text-ok" : place.where === "cpu" ? "text-warn" : "text-muted"; - - return ( -
-
- ⚡ Optimisé - {placeLabel} -
-
- {gpu.available - ? `${gpu.name} · ${(gpu.vram_total_mb / 1024).toFixed(1)} Go VRAM` - : "Aucun GPU détecté"} - {model_profile.context_length - ? ` · ctx modèle ${Math.round(model_profile.context_length / 1024)}K` - : ""} -
-
- → contexte {recommended.num_ctx} · jetons max{" "} - {recommended.max_tokens} -
- {rationale &&
{rationale}
} - {!gpu.available && ( -
- GPU non détecté dans le conteneur. Si Ollama tourne sur une autre - machine, déclare la VRAM via GPU_VRAM_MB (ex. 12000) dans - le compose pour un réglage précis. -
- )} -
- ); -} - function Slider({ label, value, diff --git a/frontend/src/store/useStore.ts b/frontend/src/store/useStore.ts index 84382b9..090494b 100644 --- a/frontend/src/store/useStore.ts +++ b/frontend/src/store/useStore.ts @@ -1,6 +1,5 @@ import { create } from "zustand"; import { - autoTune, createSession, deleteSession, getConfig, @@ -14,7 +13,6 @@ import { saveConfig, streamChat, type AgentConfig, - type AutoTuneResult, type FileNode, type Message, type OllamaModel, @@ -47,10 +45,6 @@ interface LokiState { refreshConfig: () => Promise; updateConfig: (patch: Partial) => Promise; - tuning: boolean; - tuneResult: AutoTuneResult | null; - runAutoTune: () => Promise; - pendingShell: string | null; // commande shell en attente de validation approveShell: () => Promise; rejectShell: () => Promise; @@ -115,36 +109,26 @@ export const useStore = create((set, get) => ({ }, refreshConfig: async () => { - const { config, available_tools } = await getConfig(); + const { config, available_tools } = await getConfig( + get().selectedModel || undefined + ); set({ config, availableTools: available_tools }); }, updateConfig: async (patch) => { - const config = await saveConfig(patch); + const config = await saveConfig(patch, get().selectedModel || undefined); set({ config }); }, - tuning: false, - tuneResult: null, - - runAutoTune: async () => { - const model = get().selectedModel; - if (!model || get().tuning) return; - set({ tuning: true }); - try { - const result = await autoTune(model, true); - set({ tuneResult: result, config: result.config }); - } finally { - set({ tuning: false }); - } - }, - openPreview: async (path) => { const content = await fileContent(path); set({ previewPath: path, previewContent: content }); }, - setSelectedModel: (name) => set({ selectedModel: name }), + setSelectedModel: (name) => { + set({ selectedModel: name }); + void get().refreshConfig(); + }, refreshFiles: async () => { try { @@ -175,6 +159,7 @@ export const useStore = create((set, get) => ({ ? def : models[0]?.name ?? ""; set({ models, selectedModel }); + await get().refreshConfig(); } finally { set({ loadingModels: false }); }