Qwen3.8-27B valide `reasoning_effort` au lieu de l'ignorer : il connaît
xhigh/medium/low, pas « high » — le niveau que l'interface enregistre par
défaut. Chaque message partait donc en 500, avec une trace jinja affichée
en guise d'erreur, et le 500 tombait dans la branche « prompt trop long »
de runChat : loki compactait l'historique pour rien avant d'abandonner.
Le refus dit lui-même ce que le gabarit accepte. On le lit (llm_effort.go),
on traduit le niveau demandé vers le plus proche sur l'échelle
none/minimal/low/medium/high/xhigh — à égalité, le plus fort, dégrader en
silence étant pire que générer un peu plus longtemps — et on rejoue le tour,
historique intact. Sans liste annoncée, le champ est simplement retiré.
« aucune » n'est jamais traduite : c'est une coupure, portée par
`enable_thinking` que tous les gabarits comprennent.
La traduction est retenue par modèle : les messages suivants ne repaient pas
l'aller-retour. Le repli est tracé sur stderr, sinon l'intensité choisie dans
l'interface n'est pas celle qui part au moteur sans que rien ne le dise.
« maximale » (xhigh) rejoint la liste des niveaux proposés : aucun gabarit ne
les connaît toutes, et sans elle le maximum d'un Qwen3.8 restait hors
d'atteinte. Le repli couvre les gabarits qui la refusent.
Le commit précédent a versé backend/ et frontend/ dans le dépôt — 94 Mo
de node_modules et d'artefacts de build, restes du fork pré-Go qui
traînaient encore dans le répertoire de travail. Ils ne sont plus suivis,
et .gitignore les refuse désormais avec les caches Python et npm.
L'image Docker n'était pas concernée : le Dockerfile ne copie que cmd/,
internal/, tools/ et les fichiers de module.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Trois défauts sans rapport entre eux, tous visibles à l'écran.
Jauge de contexte bloquée à « 0 / N (0 %) » : le comptage exact vient de
usage.prompt_tokens (include_usage), qu'un llama-server récent a cessé de
renvoyer. On note désormais si l'usage est arrivé ; sinon la fin de tour
publie l'estimation qui pilote déjà la compaction — approximative, mais
jamais absente. Et la souscription envoie la valeur courante juste après
le rattrapage : une page rechargée affichait zéro sur une discussion
pourtant pleine, faute d'événement ctx_used dans la fenêtre rejouée.
Espace disque : « il ne reste que 11,9 Go » sur un partage Unraid qui en
a des centaines. /mnt/user est servi par shfs, un FUSE qui agrège
plusieurs disques — statfs y renvoie l'espace d'un seul, et ce chiffre
REFUSAIT l'installation du modèle. Les systèmes de fichiers qui
approximent (FUSE, overlay, NFS, CIFS, Ceph, Gluster) sont reconnus par
leur magie : leur chiffre reste affiché, précédé d'un « ~ », mais ne
bloque plus rien.
Panneau Fichiers : le panneau entier défilait, emportant vers le haut
l'en-tête, le fil d'Ariane et le pied (occupation disque, bouton .zip)
dès qu'il y avait quelques fichiers. Seule la liste défile maintenant ;
le pied reste en bas et l'état vide se centre.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Trois défauts d'affichage, une cause commune pour deux d'entre eux.
Le panneau des critères et la puce « mode Code » étaient insérés AVANT
#composer, qui est en position:absolute : ils restaient dans le flux de
la page et passaient donc SOUS la carte de saisie. Ils vivent maintenant
DANS le composeur, alignés sur sa colonne, et les critères s'ouvrent
repliés (cinq lignes dépliées mangeaient la moitié de l'écran).
Chrono du tour complet au-dessus de la carte : le temps entre l'envoi du
message et le moment où l'on peut reparler — appels d'outils, passes de
vérification et compaction compris. Les durées portées par les cartes ne
mesurent que la génération de texte ; celle-ci répond à « ça a pris
combien de temps, en tout ? ». Il avance à la seconde puis se fige sur le
total.
Anneau d'activité de la liste des discussions : l'animation portait sur
le <svg>, dont l'origine de rotation dépend de transform-box — défaut
qui a changé au fil des versions, d'où un anneau figé. Elle porte
désormais sur le conteneur HTML, qui tourne partout.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
La capture n'apparaissait dans le fil QUE si le modèle recopiait la ligne
markdown rendue par l'outil. Un petit modèle l'oublie, et l'utilisateur
ne voyait jamais l'image qu'il avait demandée — la capture existait
pourtant bien sur le disque.
L'événement d'outil porte désormais le chemin de l'image et l'interface
la rend directement dans la bulle web_screenshot (cliquable pour la
loupe, comme les autres images du fil). Ce que le modèle en écrit
ensuite reste possible, mais n'est plus la condition de l'affichage.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Le modèle cherchait à vérifier visuellement son travail, ne trouvait pas
web_screenshot (offert seulement avec l'accès internet), en concluait
« pas de Chromium local » et partait installer puppeteer — alors qu'un
Chromium headless est dans l'image depuis toujours.
L'outil est donc offert aussi au mode code sans internet, mais borné aux
adresses locales (localhost, 127.0.0.1, ::1, *.localhost) : photographier
son propre serveur de dev est une vérification locale, pas une sortie sur
le web — l'interrupteur d'accès internet garde tout son sens. Le prompt
du builder dit explicitement que le navigateur est déjà là et qu'il ne
faut installer aucun paquet de navigateur.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Les llama-server récents ouvrent 4 slots par défaut : des tampons de
calcul GPU multipliés par quatre, pour un serveur qui ne sert qu'un
utilisateur. Résultat vécu après une mise à jour du moteur : une config
27B qui tournait très bien meurt en « cudaMalloc failed: out of
memory » en pleine génération, sans qu'aucun réglage n'ait changé. On
repasse explicitement à un slot — le comportement historique — et
PARALLEL=n reste disponible dans le preset pour qui veut du parallèle.
Co-Authored-By: Claude Opus 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>
Le raisonnement n'étant pas conservé entre les tours (coût de contexte),
un modèle à reasoning fort re-déduisait TOUT son plan avant chaque appel
d'outil — des minutes de <think> pour écrire un fichier de 200 octets. Le
prompt du builder impose maintenant : plan écrit UNE fois dans PLAN.md,
une à deux phrases de réflexion entre les appels, chaque fichier complet
en un seul write.
UI : les outils du mode code (criteria, read, grep, glob, git_*, ask,
bash_bg/bash_tail) ont leurs étiquettes — ils retombaient tous sur
« mémoire ».
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Bouton « .zip » dans le pied du panneau : la discussion entière — dossiers
et sous-dossiers compris — part en une archive, nommée d'après le titre de
la discussion. Même danse en trois temps que le téléchargement de fichier
(meta → brut, ou tranches base64 derrière le tunnel chiffré), liens
symboliques ignorés, parcours borné comme le calcul de taille du panneau.
Le scope « hors discussion » a son propre zip, sans le dossier des
discussions.
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>
Sélecteur Chat|Code par discussion dans le pied du composeur ; en mode
chat, une détection serveur suggère la bascule (puce ignorable, jamais
automatique). Conception reprise d'OpenFox (MIT), réécrite en Go — voir
NOTICE.md.
- Outils fichiers : read (lignes numérotées, borné), grep, glob, et le
tracker « lu avant d'écrire » qui refuse write/edit sur un fichier non
lu ou modifié depuis la lecture ; edit préserve les fins de ligne CRLF.
- Politique d'exécution : commandes catastrophiques refusées (rm -rf /,
mkfs, reboot…), chemins bornés au dossier de la discussion en mode
code, mutex par fichier.
- Critères d'acceptation : contrat posé via l'outil criteria (éditable
dans l'UI), passe de vérification indépendante sur contexte isolé —
seule habilitée à marquer « passed » — puis corrections plafonnées.
- Rôles embarqués (agents/*.md) : builder, planner, verifier, explorer,
code-reviewer ; badge de rôle dans le fil.
- LSP : gopls / typescript-language-server / pyright (inclus dans
l'image), diagnostics injectés dans le retour de write/edit.
- Git natif : git_status, git_diff, git_clone (borné à la discussion).
- Jobs d'arrière-plan bash_bg/bash_tail (serveur de dev, build long).
- Auto-retry : un appel d'outil écrit en texte (default_api:…,
<tool_call>…) relance le tour une fois avec consigne corrective.
- Outil ask : question à choix rendue en carte à boutons.
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 sélecteur envoyait seulement `reasoning_effort` dans le corps de la
requête. Ce champ n'agit que sur les gabarits qui le LISENT (gpt-oss et
apparentés). Un modèle hybride à la Qwen3 ne connaît que `enable_thinking` :
il recevait `reasoning_effort: none`, son gabarit l'ignorait, et il
continuait de réfléchir pendant que l'interface affichait « aucune ». Les
llama-server récents traduisent eux-mêmes `none` en `enable_thinking=false`,
mais les binaires plus anciens laissent simplement tomber le champ.
On joint donc `chat_template_kwargs` à la requête — même procédé que la
compaction, qui coupe déjà la réflexion ainsi :
aucune (ou REASONING=off explicite) → {"enable_thinking": false}
basse/moyenne/haute → {"reasoning_effort": "<niveau>"}
auto → rien, le gabarit garde son défaut
Une clé REASONING absente n'est PAS une interdiction : sans consigne on ne
touche à rien. Une clé inconnue d'un gabarit est ignorée sans erreur.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RodUJoPJDBvVKhA6S5msvb
Deux défauts révélés par un tour d'agent d'une heure (~50 appels d'outils)
sur un modèle très quantifié.
1. La vitesse affichée sous les réponses tombait de 17 tok/s à 0,9 au fil du
tour, ce qui donnait à croire que le moteur s'effondrait. Il n'en était
rien : un tour d'agent ouvre une bulle NEUVE après chaque appel d'outil
(le flux repasse contentEl à null), mais les compteurs n'étaient jamais
remis à zéro. Chaque bulle affichait donc le CUMUL de tout le tour divisé
par le temps écoulé depuis le tout premier token — exécution des outils,
pages web et prefill compris. La vitesse convergeait mécaniquement vers
« tokens générés ÷ durée totale du tour ».
Les compteurs sont maintenant remis à zéro à la CRÉATION de la bulle, ce
qui couvre tout chemin qui en ouvre une neuve, aujourd'hui comme demain.
Rejoué sur un tour synthétique où le moteur décode à 20 tok/s constants
entre deux outils de deux minutes : 20,5 / 0,6 / 0,5 tok/s avant, 20,5 sur
les trois bulles après. La durée « travail », elle, reste bien celle du
tour entier — c'est sa définition.
2. Rien n'exerçait de pression sur un tour qui tourne en rond. Le plafond
d'itérations avait été retiré en v0.6.3 (il coupait des recherches
légitimes) et la déduplication d'appels ne rattrape pas ce cas : sa clé est
« nom + arguments bruts », or relire le même fichier par tranches
(`sed -n '1,80p'` puis `sed -n '80,160p'`) produit des clés différentes.
D'où un budget SOUPLE : au-delà de 24 appels d'outils sur un tour, on
rappelle au modèle combien il en a déjà faits et on lui demande de
conclure. Le rappel revient à chaque palier en durcissant le ton, et ne
coupe jamais le tour. Il est ajouté EN FIN d'historique, ce qui laisse
intact le préfixe déjà en cache côté llama-server, et n'est pas persisté.
`AGENT_BUDGET` dans config.env règle le palier, `off` le désactive.
Élargir plutôt la clé de déduplication à la CIBLE de l'appel a été écarté :
deux tranches d'un même fichier renvoient un contenu différent, les
confondre casserait toute lecture paginée légitime.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01J5UndZ9DedRXPuAoRXbmDb
Deux manques dans la présentation d'un tour :
1. Rien ne disait sur QUELLE discussion un agent était en train de
travailler. La ligne concernée porte maintenant un anneau qui tourne,
à gauche du titre — et l'en-tête aussi, puisque la barre latérale est
escamotée sur téléphone. L'état vient de deux sources : le flux SSE
(instantané) et /api/conversations, qui expose désormais `busy` pour
que la page ouverte en cours de génération s'anime sans attendre le
rejeu du journal. L'indicateur est posé et retiré SANS redessiner la
liste : un redessin relancerait l'animation à chaque événement.
2. Le pied des réponses ne montrait que la vitesse du moteur, jamais le
temps que le tour avait pris. Il porte maintenant « travail 1 min 04 s »
— de la question envoyée à la fin du tour, raisonnement, appels
d'outils et attentes compris. La durée avance à la seconde pendant le
tour et se fige au turn_done ; elle apparaît aussi sur l'indicateur
« … », là où il n'y a pas encore de réponse sous laquelle écrire.
La bulle de raisonnement gagne au passage sa propre durée.
La mesure est prise sur les HORODATAGES SERVEUR (événements `user` et
`turn_done`) : elle est donc juste en direct comme au rejeu, où tout
arrive d'un bloc côté client. Le compteur vivant se recale sur l'écart
entre l'horloge du navigateur et celle du serveur, réévalué à chaque
événement reçu en direct — un téléphone n'est pas à la même heure que la
machine.
« Masquer la vitesse de génération » ne masque plus que la vitesse : la
durée n'est pas une mesure de moteur, c'est ce que la réponse a coûté.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01J5UndZ9DedRXPuAoRXbmDb
Une pièce jointe n'affichait que son nom de fichier, et une image posée
par l'IA sortait à sa taille NATIVE — .chatimg portait la classe mais
aucune règle CSS, donc une capture remplissait la colonne et poussait le
texte hors de vue.
- pièce jointe image : vignette dans le composeur (objectURL local, rien
n'est encore parti au serveur) et tuile 200x160 dans le message envoyé,
hydratée comme les captures (fetch authentifié + blob:) ;
- images de l'IA plafonnées à 280px de haut ;
- clic sur n'importe quelle image = loupe plein écran (Échap ou clic sur
le fond pour fermer). La source est relue AU CLIC, l'hydratation
remplaçant le src par un blob: après coup.
align-items:flex-start sur .msg-files au passage : une pastille texte
voisine d'une vignette s'étirait à sa hauteur et, arrondie à 999px,
devenait un gros ovale.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
engineSeesImages interrogeait /props sans en-tête Bearer. Moteur lancé
avec --api-key (backend_serve.go), la sonde recevait 401, concluait
« pas de vision » et les deux appelants — pièces jointes et captures —
retombaient sur le texte seul : aucune image ne partait, projecteur
chargé ou non. Le modèle brodait à partir du seul chemin de fichier.
Vérifié en production : /props authentifié renvoie modalities.vision
true avec le mmproj bien passé au moteur.
authHeader est le helper déjà utilisé par tous les autres appels
internes ; c'était le seul client.Get nu du paquet.
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>
La refonte les voulait « sans contour » : trait transparent et aplat pris
sur --row-bg. Or --row-bg est exactement le fond des blocs qui les
contiennent (.slist, .rowbody). Un bouton posé dessus n'avait donc ni
trait ni écart de valeur : « démarrer / redémarrer / arrêter » et
« mettre à jour le moteur » se lisaient comme du texte, et rien ne disait
qu'on pouvait cliquer.
On revient au cadre d'avant la refonte — trait de 1 px et aplat un ton
au-dessus du bloc — pour que le bouton se détache quel que soit ce qu'il
y a derrière. La micro-interaction au survol reste : elle était bonne,
il lui manquait seulement quelque chose à animer, et le survol change
maintenant aussi la couleur du trait.
Les boutons-icônes de la saisie et de l'en-tête (envoyer, arrêter,
joindre, ☰) ne bougent pas : ils posent border:0 plus bas dans la
feuille et gardent leur allure nue, qui est la bonne pour un
pictogramme seul.
Vérifié en clair et en sombre, dans la barre latérale, les boîtes de
dialogue et les modales.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LPyFxVHNAN9u5pVzSYMwjd
Les chevrons des sections débordaient dans l'arrondi de la carte, et le
rayon paraissait beaucoup trop fort. La cause n'était pas le rayon : la
carte des réglages avait perdu sa gouttière intérieure.
Le gabarit de carte (commit précédent) a déplacé le retrait vers
#side-scroll pour aligner Discussions, Réglages et Performance sur la
même largeur — mais il a mis le rembourrage de .sidegroup à zéro au lieu
de ne déplacer que le retrait EXTÉRIEUR. Les lignes allaient donc d'un
bord à l'autre : sur la première et la dernière, le chevron tombait dans
le congé de 20 px et semblait en sortir, et les filets de séparation
venaient buter dans la courbe.
On reprend le retrait d'avant la refonte — 16 px, exactement la valeur
qui fonctionnait — et tout rentre dans l'ordre sans toucher au rayon.
Vérifié en clair, en sombre et en largeur téléphone.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LPyFxVHNAN9u5pVzSYMwjd
llama.cpp publie plusieurs versions par jour ; l'image de Loki ne se
reconstruit qu'à une mise à jour de Loki. Le moteur y était donc figé à
la date du dernier build, et le rattraper imposait un rebuild complet de
2,6 Go pour un composant qui en pèse 170 Mo. Le panneau « Moteur » ne le
disait même pas : il annonçait « rien à mettre à jour ici ».
Réglages → Moteur affiche maintenant la version qui tourne (bXXXXX et son
commit, lus dans la bannière du binaire — jusqu'ici invisibles ailleurs
que dans le journal du moteur) et la met à jour en un clic.
Où le moteur est pris. Pas dans les releases GitHub de llama.cpp : elles
ne contiennent AUCUN binaire CUDA pour Linux, et le mode « précompilé »
retomberait sur Vulkan, donc sur une régression pour une carte NVIDIA. La
seule distribution CUDA/Linux officielle et précompilée est l'image de
conteneur — celle-là même dont l'image de Loki hérite. On lit son
manifeste OCI et on ne télécharge que les couches qui portent /app, en
descendant du sommet : le runtime CUDA (2 Go) et la base système sont
déjà là. S'arrêter au binaire ne suffit pas — llama.cpp le range dans une
couche et ses .so dans la précédente — d'où une règle d'arrêt sur
« binaire + libggml-base + libllama », et un garde-fou de taille qui
interdit de descendre jusqu'au runtime.
Le moteur atterrit dans /data/engine/<version>/, donc sur le volume de
données : il survit à un docker compose pull. La variante (CUDA, Vulkan,
SYCL, MUSA, CPU) est déduite des backends ggml posés à côté du moteur
courant — le conteneur ne sait pas de quelle image il vient, et faire
retenir « server-cuda » à l'utilisateur serait un piège.
Le risque, et ce qui le couvre. La mise à jour apporte llama.cpp, pas le
runtime CUDA, qui reste celui de l'image : un llama.cpp compilé pour un
CUDA plus récent ne chargerait pas son backend GPU. Le symptôme serait
silencieux — tout marche, mais sur le processeur. Le nouveau moteur est
donc lancé à blanc avant toute bascule ; il est refusé s'il ne démarre
pas, ET s'il ne voit plus aucune carte alors que le moteur courant en
voyait. Dans les deux cas le moteur courant n'est pas touché, et celui de
l'image reste intact : « revenir au moteur de l'image » y ramène en un
clic, sans réseau.
Vérifié de bout en bout contre le vrai ghcr.io (166 Mo, 7 s, toutes les
bibliothèques et leurs liens de version présents) et, pour les chemins
d'échec, contre un faux registre.
LOKI_OCI_REGISTRY permet de viser un miroir quand ghcr.io n'est pas
joignable.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LPyFxVHNAN9u5pVzSYMwjd
La maquette posait ces deux blocs à plat, pleine largeur — et c'est ce que
j'avais suivi. Dans l'application réelle, ils voisinent avec les sections de
réglages, qui sont des CARTES : deux traitements pour deux listes qui se
touchent, et la barre latérale se lisait comme deux morceaux collés. Ils
reprennent donc le gabarit dominant de la colonne (.sidegroup) : fond de
panneau, rayon de 20, ombre discrète, lignes séparées d'un filet et rognées par
overflow plutôt qu'arrondies une à une.
La gouttière latérale revient (le plein format n'a plus lieu d'être), le titre
« Performance » devient une étiquette de groupe posée AU-DESSUS de sa carte —
comme « Paramètres » au-dessus de la sienne — et la sélection reste un aplat
plein, un cadre ferait un second contour à l'intérieur du premier.
Corrigé au passage : la remise à zéro de marge sur la dernière jauge. Elle
datait d'un temps où il n'y avait qu'un conteneur ; depuis que les cartes
graphiques et la mémoire vive sont deux blocs distincts, la dernière jauge de
CHAQUE bloc perdait sa marge et « Mémoire vive » venait se coller à la VRAM.
C'est le rembourrage bas de la carte qui absorbe désormais la dernière marge.
Vérifié dans les deux thèmes, avec deux cartes graphiques et trois discussions.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LPyFxVHNAN9u5pVzSYMwjd
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
La flèche était décalée de 8 px vers la gauche dans son pavé — mesuré, pas
supposé : centre du bouton 1339, centre du dessin 1331.
La cause n'était pas dans la feuille de style mais dans le JS. syncSendBtn
montrait et cachait les deux boutons avec `style.display = 'inline-block'` ; ce
style INLINE l'emportait sur le `display:flex` de la règle commune. Le bouton
gardait donc sa boîte de 34 px, mais son icône de 18 px se collait au bord
gauche, `justify-content` n'ayant plus rien à centrer. Invisible tant que
l'icône était un glyphe de texte (qui remplit sa ligne), flagrant depuis qu'elle
est un SVG de taille fixe.
L'état « génération en cours » passe désormais par un attribut sur <html>
(data-busy) et c'est la feuille qui décide lequel des deux boutons s'affiche —
comme pour le thème, la barre latérale et le panneau Fichiers. Le style inline
`display:none` du bouton d'arrêt disparaît aussi du gabarit.
Vérifié au pixel dans les deux états : écart nul entre le centre du bouton et
celui du dessin, pour les trois commandes de la barre (dépôt, fichiers, envoi)
comme pour le bouton d'arrêt.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LPyFxVHNAN9u5pVzSYMwjd
Les deux icônes de la carte de saisie venaient de la maquette ; tout le reste
était encore des GLYPHES DE TEXTE — ↻ × ☰ ⌫ 🗑 ✎ ↓ ▸ ▣ ·. C'est le système
d'exploitation qui les dessinait : le même bouton sortait fin sur macOS, gras et
bleuté sur Windows (émojis en couleur), et absent d'une machine sans police
d'émojis. Aucune cohérence possible, et rien à régler depuis la feuille de style.
Elles passent en SVG, tracées comme celles de la maquette : boîte de 24, trait de
1,8, extrémités arrondies, couleur héritée. Un seul fichier les décrit
(03-icons.js) — menu, fermer, rafraîchir, crayon, corbeille, téléchargement,
flèche, dossier, fichier, image, plus, chevron.
Touchées : le ☰ du téléphone, les croix de modale, les deux rafraîchissements,
le « + » des sections, la flèche de retour en bas, le téléchargement Hugging
Face, « vider la discussion », les lignes du panneau Fichiers (dossier, image,
fichier, télécharger, supprimer), renommer/supprimer une discussion, éditer un
serveur MCP.
Deux pièges rencontrés, notés dans la feuille :
- Une règle de centrage qui pose `display:inline-flex` sur ces boutons les rend
VISIBLES en permanence : plusieurs se masquent justement par `display:none`
(le ☰ hors téléphone, le « + » d'une section fermée, la flèche de retour en
bas). Le ☰ s'est ainsi retrouvé par-dessus l'en-tête sur grand écran. Le
centrage passe donc par le rembourrage pour ceux-là.
- Les titres de panneau (.stitle) et l'intertitre « Paramètres » gardaient le
monospace en capitales de la charte précédente ; ils suivent maintenant le
seul style de titre de la maquette, celui de « Performance ».
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LPyFxVHNAN9u5pVzSYMwjd
Le projet Stitch porte deux planches que je n'avais pas vues — « Official
Palette for Loki AI v1 » et « Official Dark Mode Palette for Loki AI ». Leurs
couleurs NOMMÉES font désormais foi, et elles corrigent le relevé précédent, que
la compression JPEG des captures avait décalé :
clair Main BG #F5F6F8 · Sidebar BG #ECECEC · Primary Accent #95A899
Typography #333333
sombre Deep Navy #0F172A · Slate Surface #1E293B · Sage Accent #84A98C
Le thème sombre retrouve donc exactement les deux teintes du cahier des charges,
que mon relevé sur capture avait fait dériver vers #1E2834/#253141. Les surfaces
non nommées (carte de réponse, carte de saisie, piste de jauge, bulle sombre)
restent relevées sur les écrans de la maquette, en pleine résolution cette fois.
Géométrie, elle aussi mesurée sur la maquette :
- Bulles et carte de réponse : rayon 10 px (contre 16/18 et un coin rentré), la
carte sans cadre — c'est l'ombre qui la détache.
- Carte de saisie : rayon 14 px et CADRE SAUGE, le seul contour coloré de
l'écran. Il retombait au gris commun, une règle de coque plus bas dans la
feuille reprenant la main sur la couleur.
- Liste des discussions : lignes pleine largeur séparées d'un filet, bande plate
pour l'active — la maquette n'a ni pastille arrondie ni gouttière.
- Moniteur machine : titre en casse normale (le monospace capitales de la charte
précédente y criait), libellé + pourcentage à la même échelle, jauge de 7 px
entièrement arrondie. La sous-ligne chiffrée passe en infobulle et le bouton
de repli disparaît : la maquette n'en a pas.
- Une jauge de VRAM par carte, en plus de la charge : sur une machine à une
seule carte on retombe sur les trois jauges de la maquette (GPU, VRAM, RAM),
et pour du LLM c'est la VRAM qui décide si un modèle tient.
- Échelle typographique des planches : corps 16 px dans le fil et la saisie,
secondaire 14 px medium dans la barre latérale.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LPyFxVHNAN9u5pVzSYMwjd
Vérification faite contre la source : le projet Stitch « Loki AI Chat Modern
Redesign v1 », écrans clair et sombre. Les couleurs sont désormais MESURÉES sur
la maquette, plus approchées à l'œil — et elles posent une hiérarchie de
surfaces que l'intuition inverse :
maquette clair maquette sombre
barre latérale #E4E8E9 #0F1829 ← la plus sombre des trois
fil #EFF3F4 #1E2834
carte de réponse #E5E9EA #253141 ← s'écarte du fil
carte de saisie #EFF3F6 #1F2734 ← la plus claire
bulle utilisateur #7EA58A #334257
ligne active #D8DCDF #22293B
piste de jauge #D5D9DC #1E2738
Ce qui change concrètement :
- La barre latérale était la surface la plus CLAIRE (#FAFBFA) ; elle est la plus
sombre. Idem pour la carte de réponse, qui était quasi blanche alors que la
maquette la fait descendre sous le fil. En sombre elle monte au-dessus : dans
les deux cas la carte se détache en s'éloignant du fond, jamais en allant vers
le blanc.
- La bulle de l'utilisateur ne suit plus l'accent. Elle a son rôle propre
(--user-bg) : sauge en clair, ARDOISE en sombre — la maquette réserve le vert
aux jauges dès que le fond est sombre, ce que le vert unique ignorait.
- --accent redevient une couleur d'ENCRE (liens, icônes, sélection), lisible sur
les deux fonds ; --accent-fill porte les aplats. Sans cette séparation, la
sauge claire de la maquette (#7EA58A) donnait des icônes à 2:1 de contraste.
- La sélection dans la liste des discussions est un aplat neutre (#D8DCDF) et
non une teinte verte : la maquette n'y met pas de second accent.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LPyFxVHNAN9u5pVzSYMwjd
Depuis que la réponse de l'IA est une carte, le bloc de code avait exactement
son ton (#F4F6F5 dans une bulle #F6F8F7) : l'encadré disparaissait dans la
bulle, et un long prompt collé dans une réponse ne se distinguait plus du texte
qui l'entoure. --code-bg est désormais nettement plus soutenu que --bubble, dans
les deux variantes, avec un filet net. Le code EN LIGNE suit la même règle.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LPyFxVHNAN9u5pVzSYMwjd
Deux retours, deux vrais défauts.
Boutons de la saisie
Ils avaient trois générations de règles empilées — pilule large « Envoyer »,
aplat noir, aplat sauge — si bien que chaque bouton tirait sa taille, sa forme
et son alignement d'une couche différente : rond pour l'envoi et l'arrêt,
carré de 15 px pour l'icône stop contre 16 pour ses voisines, et un
align-self:center sur deux boutons seulement (les deux autres tombaient au
bas de la pilule). Une seule règle les décrit désormais : 34x34, coins doux
de 10 px — jamais un rond, la maquette n'en a pas —, icône de 18 px centrée,
fond seulement au survol. Seule la couleur les distingue : atténué pour
joindre et fichiers, sauge pour envoyer, rouille pour arrêter.
Étiquettes des bulles
Une bulle changeait de nom en cours de route. Envoyée en direct, elle
s'appelait « USER » (confirmPending réécrivait l'étiquette en dur) ; rejouée
au chargement, elle portait l'avatar et le prénom. Même chose côté réponse :
labelTokens remplaçait « Loki » par « assistant · 75 tok · 21.5 tok/s »
pendant la génération, et le nom ne revenait qu'au rechargement de la page.
L'étiquette porte maintenant l'identité DANS TOUS LES CAS ; les mesures de
génération vont dans la ligne dédiée, à l'endroit exact où les stats de fin
de tour les écrivent déjà. L'envoi en cours se lit à la bulle grisée et à son
infobulle, plus à un libellé qui écrase le prénom.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LPyFxVHNAN9u5pVzSYMwjd