perf: fix model reload thrash and cut time-to-first-token

Ollama reloaded the chat model mid-message because plan/summary/router
calls sent divergent runner options (no num_ctx/num_batch) and omitted
keep_alive — the main cause of perceived slowness.

- share one keep-alive httpx.AsyncClient for all Ollama calls
- unify runner options (runner_options) + keep_alive on every model
  call, embeddings included
- drop the blocking LLM routing fallback (pure lexical heuristic)
- run RAG recall + plan + code-model pick in parallel inside the SSE
  stream, after the start event
- RAG cosine scoring off the event loop; cache /api/tags 30s and
  nvidia-smi 5s; frontend polls 2s->5s, warm poll backoff, dedup
  config fetch

feat: working session menu in TopBar (switch/create/rename/delete)
feat: workspace file deletion (DELETE /api/files + UI trash buttons)
docs: recommended Ollama env vars (KEEP_ALIVE, MAX_LOADED_MODELS...)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
MichaelandClaude Opus 4.8 committed 2026-07-18 13:58:18 +02:00
1 parent 552548e4b4
commit cb2872c78a
23 files changed
+672 -224

No files matched your search

+9 -42
View File
@@ -1,17 +1,15 @@
"""Routeur automatique : moteur code (Aider) ou boucle agent classique ?
Invisible pour l'utilisateur : chaque message est classé.
1. Heuristique lexicale rapide (gratuite) — tranche les cas évidents.
2. Cas ambigus : micro-classification par le modèle lui-même (3 jetons).
Invisible pour l'utilisateur : chaque message est classé par une heuristique
lexicale instantanée. Les cas ambigus partent vers la boucle agent, qui garde
l'outil `code_task` en secours — plus d'appel LLM bloquant avant le premier
token (l'ancien micro-classifieur coûtait un aller-retour modèle complet et
chargeait un runner divergent).
"""
from __future__ import annotations
import re
import httpx
from .ollama_client import ollama
# Vocabulaire fortement lié au code / au développement.
_STRONG = re.compile(
r"\b(code|coder?|script|fonction|classe|refactor|bug|d[ée]bug|html|css|"
@@ -32,7 +30,7 @@ _ACTION = re.compile(
def score_code_task(message: str) -> int:
"""Score heuristique : >= 3 -> code, <= 1 -> agent, 2 -> ambigu."""
"""Score heuristique : >= 3 -> code, <= 2 -> agent."""
score = 0
if _STRONG.search(message):
score += 2
@@ -45,37 +43,6 @@ def score_code_task(message: str) -> int:
return score
async def is_code_task(message: str, model: str) -> bool:
"""Décision finale : heuristique, puis LLM sur les cas ambigus."""
score = score_code_task(message)
if score >= 3:
return True
if score <= 1:
return False
# Ambigu : on demande au modèle (réponse d'un mot, quasi instantané).
try:
answer = ""
async for chunk in ollama.chat(
model,
[
{
"role": "system",
"content": (
"Tu classes des demandes. Réponds UNIQUEMENT par CODE "
"si la demande nécessite d'écrire ou modifier du code "
"ou des fichiers de projet, sinon CHAT."
),
},
{"role": "user", "content": message[:500]},
],
options={"num_predict": 4, "temperature": 0},
stream=True,
):
answer += chunk.get("message", {}).get("content", "")
if chunk.get("done"):
break
return "CODE" in answer.upper()
except (httpx.HTTPError, OSError):
# Ollama indisponible : la boucle agent gérera l'erreur proprement.
return False
def is_code_task(message: str) -> bool:
"""Décision : heuristique pure, aucune requête modèle."""
return score_code_task(message) >= 3