mirror of
https://github.com/R0m1k3/Loki.git
synced 2026-10-23 06:58:14 +02:00
Préchargement des modèles : warm + keep_alive + indicateur d'état
Évite le rechargement lent quand Ollama a déchargé le modèle de la VRAM : - ollama_client.warm() : précharge un modèle (/api/generate sans prompt) ; chat() accepte keep_alive (durée de rétention en VRAM). - agent.run_agent transmet keep_alive ; route chat le passe depuis la config. - config : champ keep_alive (défaut 30m) par profil de modèle. - routes/models : POST /api/models/warm, GET /api/models/loaded (placement GPU/CPU via /api/ps). - main : préchargement du modèle par défaut au démarrage (arrière-plan, best-effort — n'empêche pas le démarrage si Ollama est absent). - Frontend : préchargement automatique à la sélection d'un modèle, poll des modèles chargés (8s), pastille verte (GPU) / orange (CPU) / blanche (à charger) dans le sélecteur, réglage 'Maintien en VRAM' dans Configuration. Tests : warm/loaded routes, keep_alive transmis, démarrage résilient sans Ollama, build front. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SVay7z3y7q2gEe54ByAE6N
This commit is contained in:
12 files changed
+205
-8
No files matched your search
@@ -115,6 +115,20 @@ npm run dev # http://localhost:5173
|
|||||||
| `web_search` | Recherche web (DuckDuckGo / SearxNG) | désactivé |
|
| `web_search` | Recherche web (DuckDuckGo / SearxNG) | désactivé |
|
||||||
| `run_shell` | Exécuter une commande **(sensible)** | désactivé |
|
| `run_shell` | Exécuter une commande **(sensible)** | désactivé |
|
||||||
|
|
||||||
|
## Préchargement des modèles (réponses instantanées)
|
||||||
|
|
||||||
|
Ollama décharge un modèle de la VRAM après quelques minutes d'inactivité : le
|
||||||
|
message suivant paie alors un rechargement complet (lent). Loki évite ça :
|
||||||
|
|
||||||
|
- **Préchargement à la sélection** : choisir un modèle le charge immédiatement
|
||||||
|
en VRAM (`/api/models/warm`).
|
||||||
|
- **Maintien au chaud** : chaque requête envoie un `keep_alive` (défaut 30 min,
|
||||||
|
réglable dans Configuration → Intelligence : de « décharger aussitôt » à
|
||||||
|
« toujours »).
|
||||||
|
- **Préchargement au démarrage** du modèle par défaut (en arrière-plan).
|
||||||
|
- **Indicateur d'état** : la pastille du sélecteur de modèle est verte quand le
|
||||||
|
modèle est chargé sur GPU, orange sur CPU, blanche s'il reste à charger.
|
||||||
|
|
||||||
## Intelligence augmentée
|
## Intelligence augmentée
|
||||||
|
|
||||||
- **Plan-puis-exécute** : les demandes complexes sont décomposées en 3-5 étapes
|
- **Plan-puis-exécute** : les demandes complexes sont décomposées en 3-5 étapes
|
||||||
|
|||||||
@@ -80,6 +80,7 @@ async def run_agent(
|
|||||||
enabled_tools: list[str] | None = None,
|
enabled_tools: list[str] | None = None,
|
||||||
confirm_shell: bool = True,
|
confirm_shell: bool = True,
|
||||||
think: bool = True,
|
think: bool = True,
|
||||||
|
keep_alive: str | None = None,
|
||||||
) -> AsyncIterator[dict]:
|
) -> AsyncIterator[dict]:
|
||||||
# enabled_tools=None -> tous les outils ; liste vide -> aucun outil.
|
# enabled_tools=None -> tous les outils ; liste vide -> aucun outil.
|
||||||
if enabled_tools is None:
|
if enabled_tools is None:
|
||||||
@@ -127,6 +128,7 @@ async def run_agent(
|
|||||||
tools=active_tools,
|
tools=active_tools,
|
||||||
options=request_options,
|
options=request_options,
|
||||||
think=request_think,
|
think=request_think,
|
||||||
|
keep_alive=keep_alive,
|
||||||
stream=True,
|
stream=True,
|
||||||
):
|
):
|
||||||
msg = chunk.get("message", {})
|
msg = chunk.get("message", {})
|
||||||
@@ -314,7 +316,7 @@ async def run_agent(
|
|||||||
final_chunk = ""
|
final_chunk = ""
|
||||||
async for chunk in ollama.chat(
|
async for chunk in ollama.chat(
|
||||||
model, convo, options=request_options,
|
model, convo, options=request_options,
|
||||||
think=request_think, stream=True
|
think=request_think, keep_alive=keep_alive, stream=True
|
||||||
):
|
):
|
||||||
tok = chunk.get("message", {}).get("content", "")
|
tok = chunk.get("message", {}).get("content", "")
|
||||||
if tok:
|
if tok:
|
||||||
|
|||||||
@@ -64,6 +64,7 @@ PROFILE_FIELDS = {
|
|||||||
"self_review",
|
"self_review",
|
||||||
"rag_enabled",
|
"rag_enabled",
|
||||||
"embed_model",
|
"embed_model",
|
||||||
|
"keep_alive",
|
||||||
*GENERATION_FIELDS,
|
*GENERATION_FIELDS,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -106,6 +107,9 @@ DEFAULT_CONFIG: dict = {
|
|||||||
# Mémoire long-terme (RAG) entre sessions, via un modèle d'embedding.
|
# Mémoire long-terme (RAG) entre sessions, via un modèle d'embedding.
|
||||||
"rag_enabled": True,
|
"rag_enabled": True,
|
||||||
"embed_model": "auto",
|
"embed_model": "auto",
|
||||||
|
# Durée de maintien du modèle en VRAM (préchargement). "0" = décharge
|
||||||
|
# aussitôt, "-1" = jamais, "30m" = 30 minutes.
|
||||||
|
"keep_alive": "30m",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
+25
-1
@@ -12,17 +12,41 @@ from fastapi.middleware.cors import CORSMiddleware
|
|||||||
from fastapi.responses import FileResponse
|
from fastapi.responses import FileResponse
|
||||||
from fastapi.staticfiles import StaticFiles
|
from fastapi.staticfiles import StaticFiles
|
||||||
|
|
||||||
from . import coder, db, rag
|
from . import agent_config, coder, db, rag
|
||||||
from .config import settings
|
from .config import settings
|
||||||
from .routes import benchmark, chat, config, files, models, sessions, shell, system
|
from .routes import benchmark, chat, config, files, models, sessions, shell, system
|
||||||
|
|
||||||
|
|
||||||
|
async def _warm_default_model() -> None:
|
||||||
|
"""Précharge le modèle par défaut en VRAM au démarrage (best-effort)."""
|
||||||
|
import asyncio
|
||||||
|
import logging
|
||||||
|
|
||||||
|
from .ollama_client import OllamaError, ollama
|
||||||
|
|
||||||
|
await asyncio.sleep(2) # laisse le service démarrer
|
||||||
|
try:
|
||||||
|
cfg = agent_config.get_config(settings.default_model)
|
||||||
|
await ollama.warm(settings.default_model, cfg.get("keep_alive", "30m"))
|
||||||
|
logging.getLogger(__name__).info(
|
||||||
|
"Modèle %s préchargé en VRAM", settings.default_model
|
||||||
|
)
|
||||||
|
except (OllamaError, OSError, Exception) as exc: # best-effort
|
||||||
|
logging.getLogger(__name__).info(
|
||||||
|
"Préchargement au démarrage ignoré : %s", exc
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@asynccontextmanager
|
@asynccontextmanager
|
||||||
async def lifespan(_: FastAPI):
|
async def lifespan(_: FastAPI):
|
||||||
|
import asyncio
|
||||||
|
|
||||||
db.init_db()
|
db.init_db()
|
||||||
rag.init_table()
|
rag.init_table()
|
||||||
# Workspace en dépôt git : requis pour les commits du moteur code (Aider).
|
# Workspace en dépôt git : requis pour les commits du moteur code (Aider).
|
||||||
coder.ensure_git(settings.workspace_dir)
|
coder.ensure_git(settings.workspace_dir)
|
||||||
|
# Préchargement du modèle par défaut, sans bloquer le démarrage.
|
||||||
|
asyncio.create_task(_warm_default_model())
|
||||||
yield
|
yield
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -117,6 +117,19 @@ class OllamaClient:
|
|||||||
raise OllamaError(str(chunk["error"]))
|
raise OllamaError(str(chunk["error"]))
|
||||||
yield chunk
|
yield chunk
|
||||||
|
|
||||||
|
async def warm(self, model: str, keep_alive: str = "30m") -> dict:
|
||||||
|
"""Précharge un modèle en VRAM sans générer (/api/generate sans prompt).
|
||||||
|
|
||||||
|
Le paramètre keep_alive fixe la durée de rétention en mémoire.
|
||||||
|
"""
|
||||||
|
async with httpx.AsyncClient(timeout=120.0, follow_redirects=True) as client:
|
||||||
|
resp = await client.post(
|
||||||
|
f"{self.host}/api/generate",
|
||||||
|
json={"model": model, "keep_alive": keep_alive},
|
||||||
|
)
|
||||||
|
resp.raise_for_status()
|
||||||
|
return resp.json()
|
||||||
|
|
||||||
async def chat(
|
async def chat(
|
||||||
self,
|
self,
|
||||||
model: str,
|
model: str,
|
||||||
@@ -125,6 +138,7 @@ class OllamaClient:
|
|||||||
tools: list[dict] | None = None,
|
tools: list[dict] | None = None,
|
||||||
options: dict | None = None,
|
options: dict | None = None,
|
||||||
think: bool | None = None,
|
think: bool | None = None,
|
||||||
|
keep_alive: str | None = None,
|
||||||
stream: bool = True,
|
stream: bool = True,
|
||||||
) -> AsyncIterator[dict]:
|
) -> AsyncIterator[dict]:
|
||||||
"""Conversation avec le modèle, en streaming token par token."""
|
"""Conversation avec le modèle, en streaming token par token."""
|
||||||
@@ -136,6 +150,9 @@ class OllamaClient:
|
|||||||
# think=False désactive le raisonnement des modèles « thinking ».
|
# think=False désactive le raisonnement des modèles « thinking ».
|
||||||
if think is not None:
|
if think is not None:
|
||||||
payload["think"] = think
|
payload["think"] = think
|
||||||
|
# keep_alive : durée de maintien du modèle en VRAM après la réponse.
|
||||||
|
if keep_alive is not None:
|
||||||
|
payload["keep_alive"] = keep_alive
|
||||||
|
|
||||||
async with httpx.AsyncClient(
|
async with httpx.AsyncClient(
|
||||||
timeout=_STREAM_TIMEOUT, follow_redirects=True
|
timeout=_STREAM_TIMEOUT, follow_redirects=True
|
||||||
|
|||||||
@@ -245,6 +245,7 @@ async def chat(req: ChatRequest) -> StreamingResponse:
|
|||||||
enabled_tools=agent_config.enabled_tool_names(cfg),
|
enabled_tools=agent_config.enabled_tool_names(cfg),
|
||||||
confirm_shell=cfg.get("confirm_shell", True),
|
confirm_shell=cfg.get("confirm_shell", True),
|
||||||
think=cfg.get("think", True),
|
think=cfg.get("think", True),
|
||||||
|
keep_alive=cfg.get("keep_alive", "30m"),
|
||||||
):
|
):
|
||||||
await queue.put(event)
|
await queue.put(event)
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
|
|||||||
@@ -58,6 +58,42 @@ async def list_models() -> dict:
|
|||||||
return {"models": models, "default": settings.default_model}
|
return {"models": models, "default": settings.default_model}
|
||||||
|
|
||||||
|
|
||||||
|
class WarmRequest(BaseModel):
|
||||||
|
name: str
|
||||||
|
keep_alive: str = "30m"
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/models/warm")
|
||||||
|
async def warm_model(req: WarmRequest) -> dict:
|
||||||
|
"""Précharge un modèle en VRAM (préchargement)."""
|
||||||
|
if not req.name.strip():
|
||||||
|
raise HTTPException(400, "nom de modèle vide")
|
||||||
|
try:
|
||||||
|
await ollama.warm(req.name.strip(), req.keep_alive)
|
||||||
|
except (httpx.HTTPError, OSError) as exc:
|
||||||
|
raise HTTPException(502, f"préchargement impossible : {exc}") from exc
|
||||||
|
return {"warmed": req.name.strip()}
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/models/loaded")
|
||||||
|
async def loaded_models() -> dict:
|
||||||
|
"""Modèles actuellement chargés en mémoire + placement GPU/CPU (/api/ps)."""
|
||||||
|
try:
|
||||||
|
loaded = await ollama.ps()
|
||||||
|
except (httpx.HTTPError, OSError):
|
||||||
|
return {"loaded": []}
|
||||||
|
result = []
|
||||||
|
for m in loaded:
|
||||||
|
size = m.get("size", 0) or 0
|
||||||
|
vram = m.get("size_vram", 0) or 0
|
||||||
|
result.append({
|
||||||
|
"name": m.get("name") or m.get("model"),
|
||||||
|
"on_gpu": bool(size and vram >= size * 0.99),
|
||||||
|
"gpu_percent": int(vram / size * 100) if size else 0,
|
||||||
|
})
|
||||||
|
return {"loaded": result}
|
||||||
|
|
||||||
|
|
||||||
class PullRequest(BaseModel):
|
class PullRequest(BaseModel):
|
||||||
name: str
|
name: str
|
||||||
|
|
||||||
|
|||||||
+13
-4
@@ -10,22 +10,31 @@ import { useStore } from "./store/useStore";
|
|||||||
|
|
||||||
export default function App() {
|
export default function App() {
|
||||||
const [view, setView] = useState<View>("chat");
|
const [view, setView] = useState<View>("chat");
|
||||||
const { refreshStatus, refreshSystemStats, refreshModels, refreshConfig } =
|
const {
|
||||||
useStore();
|
refreshStatus,
|
||||||
|
refreshSystemStats,
|
||||||
|
refreshModels,
|
||||||
|
refreshConfig,
|
||||||
|
refreshLoadedModels,
|
||||||
|
} = useStore();
|
||||||
|
|
||||||
// Au démarrage : statut Ollama + modèles + config. Poll du statut et des stats système.
|
// Au démarrage : statut Ollama + modèles + config. Poll du statut, des stats
|
||||||
|
// système et des modèles chargés en VRAM (indicateur de préchargement).
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
refreshStatus();
|
refreshStatus();
|
||||||
refreshSystemStats();
|
refreshSystemStats();
|
||||||
refreshModels();
|
refreshModels();
|
||||||
refreshConfig();
|
refreshConfig();
|
||||||
|
refreshLoadedModels();
|
||||||
const statusId = setInterval(refreshStatus, 10000);
|
const statusId = setInterval(refreshStatus, 10000);
|
||||||
const statsId = setInterval(refreshSystemStats, 2000);
|
const statsId = setInterval(refreshSystemStats, 2000);
|
||||||
|
const loadedId = setInterval(refreshLoadedModels, 8000);
|
||||||
return () => {
|
return () => {
|
||||||
clearInterval(statusId);
|
clearInterval(statusId);
|
||||||
clearInterval(statsId);
|
clearInterval(statsId);
|
||||||
|
clearInterval(loadedId);
|
||||||
};
|
};
|
||||||
}, [refreshStatus, refreshSystemStats, refreshModels, refreshConfig]);
|
}, [refreshStatus, refreshSystemStats, refreshModels, refreshConfig, refreshLoadedModels]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex h-full flex-col bg-base text-ink">
|
<div className="flex h-full flex-col bg-base text-ink">
|
||||||
|
|||||||
@@ -41,6 +41,34 @@ export async function getSystemStats(): Promise<SystemStats> {
|
|||||||
return res.json();
|
return res.json();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface LoadedModel {
|
||||||
|
name: string;
|
||||||
|
on_gpu: boolean;
|
||||||
|
gpu_percent: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Précharge un modèle en VRAM (best-effort, ne lève jamais). */
|
||||||
|
export async function warmModel(name: string, keepAlive = "30m"): Promise<void> {
|
||||||
|
try {
|
||||||
|
await fetch("/api/models/warm", {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify({ name, keep_alive: keepAlive }),
|
||||||
|
});
|
||||||
|
} catch {
|
||||||
|
/* préchargement best-effort */
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getLoadedModels(): Promise<LoadedModel[]> {
|
||||||
|
try {
|
||||||
|
const res = await fetch("/api/models/loaded");
|
||||||
|
return (await res.json()).loaded;
|
||||||
|
} catch {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export async function getModels(): Promise<{
|
export async function getModels(): Promise<{
|
||||||
models: OllamaModel[];
|
models: OllamaModel[];
|
||||||
default: string;
|
default: string;
|
||||||
@@ -116,6 +144,7 @@ export interface AgentConfig {
|
|||||||
self_review: boolean;
|
self_review: boolean;
|
||||||
rag_enabled: boolean;
|
rag_enabled: boolean;
|
||||||
embed_model: string;
|
embed_model: string;
|
||||||
|
keep_alive: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Benchmark de modèles ─────────────────────────────────────────────────
|
// ── Benchmark de modèles ─────────────────────────────────────────────────
|
||||||
|
|||||||
@@ -4,10 +4,14 @@ import { ChevronDown } from "./Icon";
|
|||||||
|
|
||||||
/** Sélecteur de modèle Ollama (chip orange de la barre supérieure). */
|
/** Sélecteur de modèle Ollama (chip orange de la barre supérieure). */
|
||||||
export function ModelSelector() {
|
export function ModelSelector() {
|
||||||
const { models, selectedModel, setSelectedModel } = useStore();
|
const { models, selectedModel, setSelectedModel, loadedModels } = useStore();
|
||||||
const [open, setOpen] = useState(false);
|
const [open, setOpen] = useState(false);
|
||||||
const ref = useRef<HTMLDivElement>(null);
|
const ref = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
|
const loadedInfo = (name: string) =>
|
||||||
|
loadedModels.find((m) => m.name === name);
|
||||||
|
const selectedLoaded = loadedInfo(selectedModel);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const onClick = (e: MouseEvent) => {
|
const onClick = (e: MouseEvent) => {
|
||||||
if (ref.current && !ref.current.contains(e.target as Node)) setOpen(false);
|
if (ref.current && !ref.current.contains(e.target as Node)) setOpen(false);
|
||||||
@@ -24,7 +28,22 @@ export function ModelSelector() {
|
|||||||
title={selectedModel || undefined}
|
title={selectedModel || undefined}
|
||||||
style={{ borderRadius: 7 }}
|
style={{ borderRadius: 7 }}
|
||||||
>
|
>
|
||||||
<span className="h-2.5 w-2.5 border-2 border-line bg-white" />
|
<span
|
||||||
|
className={`h-2.5 w-2.5 border-2 border-line ${
|
||||||
|
selectedLoaded
|
||||||
|
? selectedLoaded.on_gpu
|
||||||
|
? "bg-ok"
|
||||||
|
: "bg-warn"
|
||||||
|
: "bg-white"
|
||||||
|
}`}
|
||||||
|
title={
|
||||||
|
selectedLoaded
|
||||||
|
? selectedLoaded.on_gpu
|
||||||
|
? "chargé sur GPU"
|
||||||
|
: `chargé (${selectedLoaded.gpu_percent}% GPU)`
|
||||||
|
: "à charger"
|
||||||
|
}
|
||||||
|
/>
|
||||||
<span className="min-w-0 truncate text-[14px] leading-none">{selectedModel || "—"}</span>
|
<span className="min-w-0 truncate text-[14px] leading-none">{selectedModel || "—"}</span>
|
||||||
<ChevronDown size={12} className="flex-none" />
|
<ChevronDown size={12} className="flex-none" />
|
||||||
</button>
|
</button>
|
||||||
@@ -55,6 +74,14 @@ export function ModelSelector() {
|
|||||||
<span className="min-w-0 flex-1 truncate text-xs text-ink-2" title={m.name}>
|
<span className="min-w-0 flex-1 truncate text-xs text-ink-2" title={m.name}>
|
||||||
{m.name}
|
{m.name}
|
||||||
</span>
|
</span>
|
||||||
|
{loadedInfo(m.name) && (
|
||||||
|
<span
|
||||||
|
className={`h-1.5 w-1.5 border border-line ${
|
||||||
|
loadedInfo(m.name)!.on_gpu ? "bg-ok" : "bg-warn"
|
||||||
|
}`}
|
||||||
|
title={loadedInfo(m.name)!.on_gpu ? "chargé GPU" : "chargé CPU"}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
{m.size_go > 0 && (
|
{m.size_go > 0 && (
|
||||||
<span className="text-[10px] text-muted-2">{m.size_go} Go</span>
|
<span className="text-[10px] text-muted-2">{m.size_go} Go</span>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -405,6 +405,27 @@ export function SettingsView() {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
|
<div className="mt-2 flex items-center gap-3 border-t-2 border-line-soft pt-3">
|
||||||
|
<span className="flex-1">
|
||||||
|
<span className="block text-[14px] text-ink">
|
||||||
|
Maintien en VRAM (préchargement)
|
||||||
|
</span>
|
||||||
|
<span className="block text-[12px] text-muted-2">
|
||||||
|
Garde le modèle chargé pour des réponses instantanées
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
<select
|
||||||
|
value={draft.keep_alive}
|
||||||
|
onChange={(e) => set("keep_alive", e.target.value)}
|
||||||
|
className="h-8 border-[3px] border-line bg-card px-2 text-[13px] text-ink"
|
||||||
|
>
|
||||||
|
<option value="0">Décharger aussitôt</option>
|
||||||
|
<option value="5m">5 min</option>
|
||||||
|
<option value="30m">30 min</option>
|
||||||
|
<option value="2h">2 heures</option>
|
||||||
|
<option value="-1">Toujours</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
<BenchCard />
|
<BenchCard />
|
||||||
|
|||||||
@@ -7,6 +7,8 @@ import {
|
|||||||
getSession,
|
getSession,
|
||||||
getStatus,
|
getStatus,
|
||||||
getSystemStats,
|
getSystemStats,
|
||||||
|
getLoadedModels,
|
||||||
|
warmModel,
|
||||||
fileContent,
|
fileContent,
|
||||||
listFiles,
|
listFiles,
|
||||||
listSessions,
|
listSessions,
|
||||||
@@ -19,6 +21,7 @@ import {
|
|||||||
type OllamaModel,
|
type OllamaModel,
|
||||||
type OllamaStatus,
|
type OllamaStatus,
|
||||||
type Session,
|
type Session,
|
||||||
|
type LoadedModel,
|
||||||
type SystemStats,
|
type SystemStats,
|
||||||
type ToolCall,
|
type ToolCall,
|
||||||
} from "../api/client";
|
} from "../api/client";
|
||||||
@@ -26,6 +29,7 @@ import {
|
|||||||
interface LokiState {
|
interface LokiState {
|
||||||
status: OllamaStatus | null;
|
status: OllamaStatus | null;
|
||||||
systemStats: SystemStats | null;
|
systemStats: SystemStats | null;
|
||||||
|
loadedModels: LoadedModel[];
|
||||||
models: OllamaModel[];
|
models: OllamaModel[];
|
||||||
selectedModel: string;
|
selectedModel: string;
|
||||||
loadingModels: boolean;
|
loadingModels: boolean;
|
||||||
@@ -59,6 +63,7 @@ interface LokiState {
|
|||||||
setSelectedModel: (name: string) => void;
|
setSelectedModel: (name: string) => void;
|
||||||
refreshStatus: () => Promise<void>;
|
refreshStatus: () => Promise<void>;
|
||||||
refreshSystemStats: () => Promise<void>;
|
refreshSystemStats: () => Promise<void>;
|
||||||
|
refreshLoadedModels: () => Promise<void>;
|
||||||
refreshModels: () => Promise<void>;
|
refreshModels: () => Promise<void>;
|
||||||
refreshFiles: () => Promise<void>;
|
refreshFiles: () => Promise<void>;
|
||||||
|
|
||||||
@@ -75,6 +80,7 @@ let activeStreamController: AbortController | null = null;
|
|||||||
export const useStore = create<LokiState>((set, get) => ({
|
export const useStore = create<LokiState>((set, get) => ({
|
||||||
status: null,
|
status: null,
|
||||||
systemStats: null,
|
systemStats: null,
|
||||||
|
loadedModels: [],
|
||||||
models: [],
|
models: [],
|
||||||
selectedModel: "",
|
selectedModel: "",
|
||||||
loadingModels: false,
|
loadingModels: false,
|
||||||
@@ -142,6 +148,9 @@ export const useStore = create<LokiState>((set, get) => ({
|
|||||||
setSelectedModel: (name) => {
|
setSelectedModel: (name) => {
|
||||||
set({ selectedModel: name });
|
set({ selectedModel: name });
|
||||||
void get().refreshConfig();
|
void get().refreshConfig();
|
||||||
|
// Préchargement : le modèle est chargé en VRAM dès la sélection.
|
||||||
|
const ka = get().config?.keep_alive ?? "30m";
|
||||||
|
void warmModel(name, ka).then(() => get().refreshLoadedModels());
|
||||||
},
|
},
|
||||||
|
|
||||||
refreshFiles: async () => {
|
refreshFiles: async () => {
|
||||||
@@ -169,6 +178,10 @@ export const useStore = create<LokiState>((set, get) => ({
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
refreshLoadedModels: async () => {
|
||||||
|
set({ loadedModels: await getLoadedModels() });
|
||||||
|
},
|
||||||
|
|
||||||
refreshModels: async () => {
|
refreshModels: async () => {
|
||||||
set({ loadingModels: true });
|
set({ loadingModels: true });
|
||||||
try {
|
try {
|
||||||
|
|||||||
Reference in new issue
Block a user