Add per-model GPU and context profiles

This commit is contained in:
Michael committed 2026-06-30 11:58:39 +02:00
1 parent cdbb5f03fa
commit e24d397d31
12 files changed
+224 -215

No files matched your search

+12 -20
View File
@@ -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é
+52 -2
View File
@@ -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
+77 -13
View File
@@ -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"):
+1 -6
View File
@@ -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")
+1 -1
View File
@@ -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):
+8 -36
View File
@@ -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,
}
+16 -4
View File
@@ -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"],
},
+1 -4
View File
@@ -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
+6 -1
View File
@@ -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:
+9 -32
View File
@@ -62,38 +62,12 @@ export interface AgentConfig {
top_k: number;
max_tokens: number;
num_ctx: number;
num_gpu: number;
num_batch: number;
tools: Record<string, boolean>;
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<AutoTuneResult> {
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<AgentConfig>
patch: Partial<AgentConfig>,
model?: string
): Promise<AgentConfig> {
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),
+32 -72
View File
@@ -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<string, string> = {
@@ -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 */}
<div className="flex flex-col gap-5">
<div className="rounded-card border border-line bg-card p-[18px]">
<div className="mb-4 flex items-center justify-between">
<div className="text-sm font-bold">Génération</div>
<button
onClick={runAutoTune}
disabled={tuning || !selectedModel}
className="flex h-[26px] items-center gap-1.5 rounded-[7px] border border-[rgba(240,161,92,.4)] bg-[rgba(240,161,92,.10)] px-2.5 text-[11.5px] font-semibold text-accent disabled:opacity-40"
title="Détecte le GPU et le modèle, puis optimise le contexte et les jetons"
>
{tuning ? (
<span className="h-3 w-3 animate-spin rounded-full border-2 border-accent/40 border-t-accent" />
) : (
<span>⚡</span>
)}
{tuning ? "Détection…" : "Réglage auto"}
</button>
<div className="mb-1 text-sm font-bold">Génération</div>
<div className="mb-4 text-[11.5px] text-muted-2">
Profil enregistré pour <b>{selectedModel || "ce modèle"}</b>.
Le contexte détermine directement la taille du cache KV.
</div>
{tuneResult && <TuneBanner result={tuneResult} />}
<Slider
label="Température"
value={draft.temperature}
@@ -280,17 +264,38 @@ export function SettingsView() {
onChange={(v) => set("max_tokens", Math.round(v))}
/>
<Slider
label="Contexte (num_ctx)"
label="Cache KV / contexte"
value={draft.num_ctx}
min={0}
min={2048}
max={32768}
step={1024}
fmt={(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))}
/>
<Slider
label="Couches GPU"
value={draft.num_gpu}
min={-1}
max={120}
step={1}
fmt={(v) => (v < 0 ? "auto" : String(Math.round(v)))}
onChange={(v) => set("num_gpu", Math.round(v))}
/>
<Slider
label="Batch"
value={draft.num_batch}
min={64}
max={1024}
step={64}
fmt={(v) => String(Math.round(v))}
onChange={(v) => set("num_batch", Math.round(v))}
last
/>
<div className="mt-3 rounded-lg border border-line-soft bg-base px-3 py-2 text-[11px] leading-relaxed text-muted-2">
La précision KV (<code>f16</code>/<code>q8_0</code>) est un
réglage global du serveur Ollama et nécessite son redémarrage.
Les valeurs ci-dessus sont, elles, propres à chaque modèle.
</div>
</div>
<div className="rounded-card border border-line bg-card p-[18px]">
@@ -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 (
<div className="mb-4 rounded-[10px] border border-[rgba(240,161,92,.35)] bg-[rgba(240,161,92,.06)] p-3 text-[11.5px]">
<div className="mb-1.5 flex items-center justify-between">
<span className="font-semibold text-accent">⚡ Optimisé</span>
<span className={`font-mono ${placeColor}`}>{placeLabel}</span>
</div>
<div className="font-mono text-muted">
{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`
: ""}
</div>
<div className="mt-1 text-muted-2">
→ contexte <b className="text-ink-2">{recommended.num_ctx}</b> · jetons max{" "}
<b className="text-ink-2">{recommended.max_tokens}</b>
</div>
{rationale && <div className="mt-1 text-muted-3">{rationale}</div>}
{!gpu.available && (
<div className="mt-1.5 text-muted-3">
GPU non détecté dans le conteneur. Si Ollama tourne sur une autre
machine, déclare la VRAM via <code>GPU_VRAM_MB</code> (ex. 12000) dans
le compose pour un réglage précis.
</div>
)}
</div>
);
}
function Slider({
label,
value,
+9 -24
View File
@@ -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<void>;
updateConfig: (patch: Partial<AgentConfig>) => Promise<void>;
tuning: boolean;
tuneResult: AutoTuneResult | null;
runAutoTune: () => Promise<void>;
pendingShell: string | null; // commande shell en attente de validation
approveShell: () => Promise<void>;
rejectShell: () => Promise<void>;
@@ -115,36 +109,26 @@ export const useStore = create<LokiState>((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<LokiState>((set, get) => ({
? def
: models[0]?.name ?? "";
set({ models, selectedModel });
await get().refreshConfig();
} finally {
set({ loadingModels: false });
}