mirror of
https://github.com/R0m1k3/Loki.git
synced 2026-10-18 04:36:44 +02:00
É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
133 lines
4.1 KiB
Python
133 lines
4.1 KiB
Python
"""Routes liées à Ollama : statut de connexion, modèles, téléchargement."""
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
|
|
import httpx
|
|
from fastapi import APIRouter, HTTPException
|
|
from fastapi.responses import StreamingResponse
|
|
from pydantic import BaseModel
|
|
|
|
from ..config import settings
|
|
from ..ollama_client import ollama
|
|
|
|
router = APIRouter(prefix="/api", tags=["ollama"])
|
|
|
|
|
|
@router.get("/status")
|
|
async def status() -> dict:
|
|
"""État de la connexion Ollama (point vert/rouge de la barre supérieure)."""
|
|
try:
|
|
version = await ollama.ping()
|
|
return {
|
|
"connected": True,
|
|
"host": ollama.host,
|
|
"version": version.get("version"),
|
|
"default_model": settings.default_model,
|
|
}
|
|
except (httpx.HTTPError, OSError) as exc:
|
|
return {
|
|
"connected": False,
|
|
"host": ollama.host,
|
|
"error": str(exc),
|
|
"default_model": settings.default_model,
|
|
}
|
|
|
|
|
|
@router.get("/models")
|
|
async def list_models() -> dict:
|
|
"""Modèles installés localement, formatés pour le sélecteur de l'UI."""
|
|
try:
|
|
raw = await ollama.list_models()
|
|
except (httpx.HTTPError, OSError):
|
|
return {"models": []}
|
|
|
|
models = []
|
|
for m in raw:
|
|
details = m.get("details", {})
|
|
size_go = round(m.get("size", 0) / 1_000_000_000, 1)
|
|
models.append(
|
|
{
|
|
"name": m.get("name"),
|
|
"size_go": size_go,
|
|
"parameter_size": details.get("parameter_size"),
|
|
"quantization": details.get("quantization_level"),
|
|
"family": details.get("family"),
|
|
}
|
|
)
|
|
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):
|
|
name: str
|
|
|
|
|
|
class DeleteModelRequest(BaseModel):
|
|
name: str
|
|
|
|
|
|
@router.delete("/models")
|
|
async def delete_model(req: DeleteModelRequest) -> dict:
|
|
"""Supprime explicitement un modèle de l'instance Ollama."""
|
|
if not req.name.strip():
|
|
raise HTTPException(400, "nom de modèle vide")
|
|
try:
|
|
await ollama.delete_model(req.name.strip())
|
|
except httpx.HTTPStatusError as exc:
|
|
detail = exc.response.text[:500] or str(exc)
|
|
raise HTTPException(502, f"Ollama : {detail}") from exc
|
|
except (httpx.HTTPError, OSError) as exc:
|
|
raise HTTPException(502, f"Ollama injoignable : {exc}") from exc
|
|
return {"deleted": req.name.strip()}
|
|
|
|
|
|
@router.post("/models/pull")
|
|
async def pull_model(req: PullRequest) -> StreamingResponse:
|
|
"""Télécharge un modèle en streamant la progression (SSE)."""
|
|
|
|
async def event_stream():
|
|
try:
|
|
async for chunk in ollama.pull_model(req.name):
|
|
yield f"data: {json.dumps(chunk)}\n\n"
|
|
except (httpx.HTTPError, OSError) as exc:
|
|
yield f"data: {json.dumps({'error': str(exc)})}\n\n"
|
|
yield "data: [DONE]\n\n"
|
|
|
|
return StreamingResponse(event_stream(), media_type="text/event-stream")
|