CTX passé à 100000 dans l'éditeur, « enregistré »… et la carte du chat
qui affiche toujours 32768. Seul le fichier du preset changeait :
config.env gardait l'ancienne valeur, le moteur tournait avec, et plus
aucun preset n'était détecté actif (empreinte différente). Il fallait
penser à rebasculer dessus à la main.
SavePresetApplying décide AVANT l'écriture si le preset édité est celui
en service (empreinte de l'ancien fichier == configuration courante) ;
si oui, la nouvelle version est installée comme une bascule
(applyPresetFile : réglages machine et moteur préservés) et le service
redémarre en arrière-plan. Un preset inactif ou nouveau reste un simple
fichier. L'UI le dit et rafraîchit l'état — la jauge de contexte suit.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XFhcmBMUXdK6UesdGUgFPH
Le modèle occupe la mémoire vidéo tant que le moteur tourne : une autre
application qui réclame la carte (jeu, encodage, autre serveur d'inférence)
ne trouvait plus rien à prendre. Le geste existait — « arrêter » le service,
au fond des réglages — mais son nom ne disait pas qu'il libérait la VRAM, et
il laissait tourner le serveur de dictée, qui garde la carte lui aussi.
- POST /api/vram/unload : arrête le moteur ET la dictée, attend que le pilote
rende la mémoire (une lecture immédiate rapporte « 0 Mo libérés » après un
déchargement pourtant réussi) et renvoie le bilan chiffré. Refuse pendant une
génération, sauf {force:true} — la couper perdrait la réponse en cours.
- POST /api/vram/reload : relance le moteur, préflight compris (BIN/MODEL
absents = la vraie raison tout de suite, pas un « chargement… » sans fin).
- Bouton sur les jauges du moniteur, là où l'on regarde la VRAM ; il devient
« Recharger le modèle » dès que le moteur est arrêté, d'après /api/status et
non d'un drapeau local (un second onglet afficherait sinon un bouton qui ment).
- Même commande dans Réglages → Moteur → Service du moteur.
- Carte de saisie : « Modèle déchargé — Recharger le modèle » au lieu de
« Le modèle charge », qui promettait un chargement qui ne viendrait jamais.
La lecture nvidia-smi de /api/vram passe dans web_vram.go (gpuStats), partagée
avec le bilan du déchargement.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01V8n9tDn5sZudAcUT8U6uGg
Quatre retouches d'interface :
- Le sélecteur de l'en-tête liste maintenant les MODÈLES du disque (groupe
« Modèles (.gguf) ») en plus des presets : en choisir un le charge —
POST /api/models/use écrit MODEL seul (contexte, NGL et échantillonnage
conservés) et redémarre le service en arrière-plan. Sans preset créé, le
sélecteur n'offrait aucun choix.
- Pourcentage de chargement : sur un redémarrage à chaud, le modèle est déjà
dans le cache disque — llama-server ne lit rien (read_bytes reste à 0) et
la pastille passait de « 0 % » à « prêt » sans jamais monter. On prend le
plus avancé de read_bytes et de la mémoire résidente (VmRSS), qui grandit
cache ou pas.
- Discussions triées par date de CRÉATION (récentes en tête) : une
discussion garde sa place, écrire dans un vieux fil ne le fait plus
remonter.
- Bouton Réglages ancré en pied de barre latérale, juste au-dessus du
moniteur Performance — toujours au même endroit, quel que soit le nombre
de discussions.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Un llama-server qui plantait au démarrage (backtrace __libc_start_main
dans le journal) laissait la pastille sur « chargement 0 % » pour
l'éternité : aucun des motifs de modelLoadError ne reconnaissait un
crash. Ajoutés : backtrace (signal, segfault, abort), mémoire GPU
insuffisante (out of memory / cudaMalloc failed — avec le conseil de
réduire CTX), et erreur CUDA (GPU indisponible après un redémarrage du
serveur). Pas de motif « libggml » : les logs de chargement normaux
citent les .so.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
La pastille « chargement… » restait identique de longues minutes sur un
gros GGUF — indiscernable d'un plantage. llama-server n'expose aucun
progrès, mais charger c'est LIRE le fichier : on rapporte les octets lus
par le process (/proc/<pid>/io, shards compris) à la taille du modèle et
la pastille affiche « chargement 43 % » (rafraîchie toutes les 5 s,
plafonnée à 99 — c'est /health qui dit prêt).
Carte Moteur : bouton « vérifier la version » retiré — « mettre à jour »
fait sa propre vérification et dit s'il n'y a rien de neuf.
Éditeur de preset : en conteneur, l'option de moteur « Personnalisé »
disparaît (rien à compiler dans l'image, la mise à jour passe par la
carte Moteur) ; un BIN non reconnu retombe sur le moteur de l'image.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
- gofmt sur trois fichiers oubliés par le dernier commit (ci/test rouge).
- gopls@latest exige Go ≥ 1.26 alors que l'image de build est en 1.25 avec
GOTOOLCHAIN=local : l'étape Docker échouait net. GOTOOLCHAIN=auto télécharge
le toolchain requis, borné à l'étape de build (build-push GHCR rouge).
- Serveurs MCP : npx/uvx téléchargent leur paquet au premier lancement, et le
délai de connexion de 20 s tombait dessus (« enregistré mais connexion
échouée : context deadline exceeded » depuis le catalogue). Délai à froid
de 3 min pour ces deux lanceurs, et erreur de délai réécrite pour dire quoi
faire (réessayer : le paquet reste en cache).
- README : note sur ce premier lancement, ligne mode Code dans le tableau
des différences.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Modèles, discussions et fichiers disparaissaient à chaque recréation du
conteneur quand /data n'était pas un volume : tout vivait dans la couche
éphémère. Loki le détecte maintenant (mountinfo) et le dit — bandeau rouge
dans l'UI (/api/status warn), avertissement en tête du journal, et
l'entrypoint exporte LOKI_HOME pour ses sous-commandes.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Le niveau de raisonnement n'existait que dans l'éditeur de preset : le changer
demandait d'ouvrir les réglages, éditer, enregistrer. Or ça se décide au moment
d'écrire le message.
- Nouvelle route /api/reasoning-effort (GET/POST), calquée sur /api/memory :
elle écrit REASONING_EFFORT dans la config vivante, relue à chaque requête au
moteur. Aucun redémarrage — la valeur voyage dans le corps de la requête.
- Liste blanche côté serveur : cette clé finit dans une requête au moteur, on
n'y laisse pas passer une chaîne arbitraire venue du navigateur.
- La liste est grisée quand le raisonnement est coupé pour ce modèle, plutôt que
masquée : le réglage reste trouvable, et l'infobulle dit où le rallumer.
- L'éditeur de preset garde le même réglage comme DÉFAUT du modèle. Appliquer un
preset réécrit toute la config, donc il reprend la main sur le choix fait à la
volée — les deux sous-titres le disent, sinon la double présence intrigue.
Angles : les rayons allaient de 5 à 26px selon les composants, ce qui donnait
des cartes très rondes. Tout est ramené à 4px (cartes, boutons, modales) et 3px
(petits éléments), y compris les formes multi-valeurs comme la barre de saisie
(26px 26px 0 0 → 3px 3px 0 0). Les pastilles (999px) et les ronds (50%) sont
laissés intacts : ce sont des interrupteurs et des avatars, pas des cartes.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Ajouter un serveur MCP demandait d'écrire soi-même `npx -y @scope/paquet …`
dans la modale, en devinant le nom du paquet. Le catalogue liste une vingtaine
de serveurs connus, commande déjà renseignée, classés par catégorie.
- internal/loki/mcp_catalog.json est EMBARQUÉ dans le binaire (go:embed), pas
interrogé sur le réseau : Loki tourne hors ligne, et rien de ce qui s'exécute
sur la machine ne vient d'un annuaire distant. Pour proposer un serveur de
plus : éditer le fichier et recompiler.
- Choisir une entrée n'installe rien. Ça préremplit la modale d'ajout existante
et l'utilisateur relit la commande avant d'enregistrer — un serveur stdio
exécute un process arbitraire, au même titre que l'outil bash du mode agent.
Aucune ligne de mcp_client.go n'est touchée : le catalogue s'arrête à
l'affichage, tout le reste passe par l'API MCP déjà en place.
- Une entrée dont le runtime manque (npx ou uvx absent) le signale dans la
liste, plutôt que de laisser l'utilisateur découvrir l'échec au démarrage du
serveur.
- Les entrées qui réclament une clé d'API la rappellent avant l'enregistrement,
au lieu d'enregistrer un serveur qui ne peut pas se connecter.
- Les entrées Python publiées avant le SDK MCP 2.0 sont épinglées avec
`uvx --with "mcp<2"` : sans cela elles plantent sur ImportError: McpError.
- Le Dockerfile gagne uv/uvx (~35 Mo). Sans lui, 9 des 21 entrées s'affichent
sans pouvoir démarrer dans le conteneur.
Intensité du raisonnement (REASONING_EFFORT)
L'interrupteur Raisonnement était binaire. llama-server accepte
`reasoning_effort` dans /v1/chat/completions : `none` coupe le raisonnement,
toute autre valeur est passée au gabarit jinja du modèle.
- Nouvelle clé de preset REASONING_EFFORT — donc réglée par modèle, comme le
reste du preset. Vide = on n'envoie rien et le gabarit garde son comportement.
- Pas de repli à prévoir : un gabarit qui ne lit pas la valeur l'ignore sans
erreur. En pratique seuls gpt-oss et apparentés changent de comportement, et
le sous-titre du réglage le dit — promettre un effet universel serait faux.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
L'endpoint compatible OpenAI n'était pas servi par Loki : le panneau annonçait
l'adresse de llama-server lui-même, http://<ip>:8080/v1. Dans le déploiement de
référence de ce fork, cette adresse ne peut joindre personne — le port 8080
n'est pas publié par le conteneur, l'entrypoint sème HOST=127.0.0.1, et l'IP
annoncée est celle du bridge Docker. L'autre voie proposée, « exposer en public
(ajean.link) », exigeait un jeton de relais que ce fork ne permet plus
d'obtenir : l'interrupteur ne pouvait que renvoyer vers un panneau supprimé.
Désormais, Loki sert /v1/* SUR SON PROPRE PORT et relaie vers le moteur. L'API
est donc joignable partout où l'interface l'est — IP du réseau local, nom de
domaine, reverse proxy — sans publier de second port ni ouvrir le moteur.
Serveur
- mountOAI (llm_oai.go) monte /v1/ sur le mux, et RIEN d'autre : ni /metrics,
ni /props, ni /slots, qui divulgueraient le modèle chargé et l'état des slots.
Le filtre interne d'oaiHandler reste en seconde barrière.
- requireCompletionKey (web_auth.go) garde cette surface avec la clé des
COMPLÉTIONS, pas celle de pilotage : un client OpenAI n'a qu'un en-tête
Authorization, et on veut pouvoir lui donner l'accès au modèle sans le droit
de redémarrer la machine. Erreurs au format d'OpenAI (body.error.message), que
les SDK savent présenter. Le préflight CORS passe sans clé — il n'en porte
jamais, et le refuser casserait tout client tiers de navigateur.
- effectiveAPIKeyErr (backend_config.go) devient la source unique de la clé
exigée : base d'abord, config.env en repli, exactement comme le moteur. Sans
ce miroir, un API_KEY résiduel donnait un endpoint « ouvert » côté Loki et un
401 côté moteur, sans rien pour l'expliquer. Lecture ratée = refus, jamais
ouverture (même raisonnement que readWebKeyErr).
- oaiHandler passe à ReverseProxy.Rewrite : le port du moteur est relu à chaque
requête au lieu d'être figé à la construction — il visait l'ancien port dès
qu'on changeait PORT, jusqu'au redémarrage de Loki.
- withLocalAuth (relay_link.go) n'injecte plus la clé de pilotage sur /v1 : elle
aurait été refusée par la garde, et surtout relayée au moteur. Le trafic du
tunnel est marqué (en-tête effacé avant d'être posé, sinon un client le forge)
et la surface y reste fermée tant que oai_public est faux — la promesse du
tunnel est tenue.
Adresse affichée
- web_public_url.go : normalisation d'une adresse publique saisie à la main
(schéma ajouté, /v1 recopié toléré, chemin refusé), origine de la requête via
Host + X-Forwarded-Proto, et la règle de priorité entre les deux.
- Le calcul quitte le navigateur pour le serveur : c'est la concaténation côté
client qui produisait l'adresse fantôme.
Interface
- Le panneau perd l'interrupteur ajean.link et l'interrupteur d'écoute LAN — ce
dernier n'a plus d'objet, et deux interrupteurs pour « rendre l'IA joignable »
était la confusion à lever. La route /api/network et `loki network` restent
pour qui veut exposer le moteur en direct.
- Il gagne un champ « adresse publique » (facultatif, pour le reverse proxy) et
un avertissement rouge tant qu'aucune clé n'est définie — l'endpoint est
maintenant ouvert PARTOUT où l'interface l'est, ça ne se dit pas à voix basse.
Le démarrage de `loki web` le crie aussi.
Vérifié bout en bout sur le serveur réel : liste des modèles à travers Loki avec
la clé (200), sans la clé (401), et complétion en streaming dont les tokens
arrivent espacés de 120 ms — le flux traverse bien le double proxy.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LPyFxVHNAN9u5pVzSYMwjd
- module github.com/R0m1k3/Loki, cmd/loki, internal/loki (package loki)
- LOKI_HOME, LOKI_MODEL_DIRS, LOKI_SERVICE, LOKI_DL_CONNS ; /etc/loki ;
units loki-engine / loki-ui ; binaire et aide CLI
- updateRepo pointe sur R0m1k3/Loki (l'auto-update ne tirera plus les
binaires AJEAN amont)
Conservé à l'identique : le domaine ajean.link (service de tunnel amont),
les littéraux de migration 0.7.x (migrate_07.go), RELEASE_NOTES.md et
LICENSE (historique et licence de l'amont).
go build/vet/test : verts.