Files
MichaelandClaude Opus 5.5 5dd14c9de5 Version 0.15.0
Version consacrée à la vitesse sans dénaturer le modèle : les lots 1, 2 et 3
de performance. Ce qui est sans perte est actif d'office ; tout ce qui change
ce que voit le modèle, le placement ou les slots attend une clé.

- Version passée à 0.15.0 (run.go, versioninfo.json, ressources Windows
  régénérées par goversioninfo seul — icônes inchangées).
- Notes de release réécrites : actif d'office, opt-in et leurs clés, ordre de
  test conseillé, ce qui a été écarté (cache-reuse, context-shift, KV
  quantifié par défaut) et pourquoi.
- README : une section « Performance » rassemble toutes les clés (défaut,
  effet, prix ou zone grise), les outils sans clé, l'ordre de test et les
  pistes écartées ; le détail de chaque clé y est déplacé depuis
  « Fonctionnalités », qui n'y renvoie plus que par un lien.
- NOTICE inchangé : rien de ces lots ne reprend OpenFox ni AJEAN.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 15:32:27 +02:00

308 lines
11 KiB
Go

// loki — single-binary LLM server manager + web UI for llama.cpp deployments.
// Point d'entrée réel : Main(), appelé par cmd/loki (les métadonnées Windows
// .syso/go:generate vivent là-bas, dans le dossier du package main).
package loki
import (
"errors"
"fmt"
"os"
"path/filepath"
"strings"
)
const Version = "0.15.0"
// Main est le vrai main() du binaire (cmd/loki ne fait que l'appeler).
func Main() {
// Rattache la console du terminal parent si on est lancé depuis un shell
// (Windows : binaire GUI). Retourne false au double-clic (aucune console) →
// on bascule alors sur l'expérience « application ». Hors Windows : toujours
// true. À faire AVANT toute écriture.
haveConsole := setupConsole()
// Nettoie un éventuel binaire .old laissé par une mise à jour Windows.
cleanupOldBinary()
args := os.Args[1:]
noArgs := len(args) == 0
cmd := "help"
if len(args) > 0 {
cmd = args[0]
args = args[1:]
}
// Double-clic sur le binaire (aucun argument, aucune console rattachée) → on
// lance l'expérience « application » (UI web + navigateur + icône tray) plutôt
// que d'afficher l'aide. Le binaire étant compilé en sous-système GUI, il n'y a
// AUCUNE console à ce stade (donc plus de fenêtre noire). Lancé depuis un shell,
// `loki` sans argument garde son comportement d'aide.
if noArgs && !haveConsole {
mustExit(cmdApp(args))
return
}
switch cmd {
case "app":
mustExit(cmdApp(args))
case "start", "restart":
// Un moteur sans BIN ni MODEL démarre, meurt, et systemd le relance en
// boucle : l'utilisateur ne voyait qu'un « activating » rassurant et un
// /health muet. On le dit AVANT de lancer le service.
mustExit(preflightEngine())
mustExit(serviceAction(cmd))
case "stop", "status", "enable", "disable":
mustExit(serviceAction(cmd))
case "logs":
mustExit(serviceLogs())
case "edit":
mustExit(editConfig())
case "config":
mustExit(cmdConfigKV(args))
case "set-api-key":
mustExit(cmdSetAPIKey(args))
case "set-web-key":
mustExit(cmdSetWebKey(args))
case "vram":
mustExit(showVram())
case "gpu":
mustExit(cmdGPU(args))
case "network":
mustExit(cmdNetwork(args))
case "switch":
mustExit(cmdSwitch(args))
case "chat":
mustExit(cmdChat(args))
case "export":
mustExit(cmdExport(args))
case "web":
mustExit(cmdWeb(args))
case "link":
mustExit(cmdLink(args))
case "remote":
mustExit(cmdRemote(args))
case "ui":
mustExit(cmdUI(args))
case "oai":
mustExit(cmdOAI(args))
case "agent":
mustExit(cmdAgent(args))
case "computer":
mustExit(cmdComputer(args))
case "internet":
mustExit(cmdInternet(args))
case "memory":
mustExit(cmdMemory(args))
case "serve":
mustExit(cmdServe(args))
case "test":
mustExit(cmdTest(args))
case "bench":
mustExit(cmdBench(args))
case "tune":
mustExit(cmdTune(args))
case "llamacpp":
mustExit(cmdLlamacpp(args))
case "update":
mustExit(cmdUpdate(args))
case "where":
mustExit(cmdWhere(args))
case restartArg:
// Sous-commande interne, absente de l'aide : l'accompagnateur détaché qui
// attend la fermeture de l'app puis la relance.
mustExit(cmdRestartAfterUpdate(args))
case "install":
mustExit(cmdInstall(args))
case "uninstall":
mustExit(cmdUninstall(args))
case "version", "-v", "--version":
fmt.Println("loki", Version)
case "help", "-h", "--help", "":
printHelp()
default:
fmt.Fprintf(os.Stderr, "commande inconnue: %s\n\n", cmd)
printHelp()
os.Exit(2)
}
}
func printHelp() {
fmt.Printf(`loki %s — manager llama.cpp + UI web (single binary)
Usage: loki <commande> [args]
Loki tourne en deux services : loki-engine (le modèle) et loki-ui
(l'interface web, le tunnel d'accès distant et l'endpoint OpenAI).
Moteur (loki-engine) :
start | stop | restart gérer le service
status | logs état / logs en direct
enable | disable auto-démarrage au boot
edit éditer la configuration dans $EDITOR
switch [N] activer un preset de presets/ (interactif ou par numéro)
test | bench [N] [--full] vérifier que l'IA répond / mesurer prefill + decode (--full : en profondeur)
tune [--placement] [--opt-in] [--budget MIN] [--stages lots,threads…]
optimiseur sans perte : essais (lots, threads, marges --fit…)
sur un moteur privé, mesurés au bench complet ; rien n'est
écrit sans ta réponse (copie du preset ou preset actuel,
vérifié puis rétabli en cas d'échec). --placement : --fit à la
place des experts / --tensor-split placés à la main (réécrit
EXTRA_ARGS) ; --opt-in : SPEC, CUDA_GRAPH_OPT, --backend-sampling
--stages : marges,lots,delestage,threads,files (défaut : toutes)
vram utilisation GPU/VRAM (nvidia-smi)
gpu [index…] liste les GPU / choisit le(s)quel(s) utiliser (gpu all = tous)
set-api-key [clé] protéger llama-server (clé Bearer); vide = générer, "" = retirer
network [on|off|status] rendre l'endpoint OpenAI joignable depuis le réseau local
(HOST + règle de pare-feu sous Windows)
Interface (loki-ui) :
ui [start|stop|restart|status] pilote le service d'interface
web [PORT] sert l'interface au premier plan (défaut :8090) —
ouvre aussi le tunnel si un jeton est enregistré
set-web-key [clé] protéger l'API de pilotage; vide = générer, "" = retirer
Interaction:
chat [system-prompt] chat terminal streamé
export [options] [fichier] exporte la conversation de l'interface web
(Markdown par défaut, « - » = sortie standard)
--json --last N --no-reasoning
--no-tools --no-results
agent [on|off|status] donne à l'IA ses outils (shell, fichiers, mémoire)
memory [off|ondemand|always|status] mode mémoire de l'IA
internet [on|off|status|engine <go|crawl4ai>|url <url>|key <clé>]
accès web de l'IA (moteur intégré ou serveur Crawl4AI)
computer [on|off|status] laisse l'IA piloter un navigateur (browser_open/click/type…)
Accès distant (ajean.link) :
link <token> enregistre le jeton et ouvre le tunnel
link code code d'appairage (10 min, usage unique) pour le portail
link status | logout état du jeton / l'oublier
Poste distant (piloter CE PC depuis l'IA d'un serveur Loki) :
remote install <url> --machine ID --key CLÉ --code CODE [--allow shell,read,write,list]
appaire ce PC + installe le service (démarrage auto)
remote connect <url> … idem en avant-plan (pour tester)
remote status | uninstall | logout
(commande d'appairage prête à copier : UI web → Postes distants)
Backend llama.cpp :
llamacpp install clone + compile llama.cpp (CUDA/ROCm/Metal/CPU), pointe BIN dessus
llamacpp update git pull + recompile le backend existant
llamacpp status commit courant, backend détecté, retard sur origin
Installation:
where affiche l'emplacement du binaire, de la base et des dossiers
install | uninstall installer / désinstaller
update [--check] mettre à jour depuis les releases GitHub
serve entrypoint du service : exec llama-server (usage interne)
Env:
LOKI_HOME racine des données (défaut : /etc/loki, %%ProgramData%%\loki sous Windows)
EDITOR éditeur pour 'loki edit' (défaut : nano, notepad sous Windows)
`, Version)
}
func mustExit(err error) {
if err != nil {
fmt.Fprintln(os.Stderr, "[err]", err)
os.Exit(exitStatusOf(err))
}
}
// exitStatusError : une erreur qui choisit son code de sortie. Seul usage :
// le refus de LOAD_GUARD, que le superviseur du moteur ne doit pas relancer
// (voir loadGuardExitStatus).
type exitStatusError struct {
code int
err error
}
func (e *exitStatusError) Error() string { return e.err.Error() }
func (e *exitStatusError) Unwrap() error { return e.err }
// exitStatusOf : le code choisi par l'erreur, 1 sinon — comme toujours.
func exitStatusOf(err error) int {
var es *exitStatusError
if errors.As(err, &es) {
return es.code
}
return 1
}
// LokiHome resolves the Loki data directory.
// Précédence : $LOKI_HOME → /etc/default/loki (unix) → défaut plateforme.
func LokiHome() string {
if h := os.Getenv("LOKI_HOME"); h != "" {
return h
}
if h := readEtcDefault(); h != "" {
return h
}
return defaultLokiHome()
}
// readEtcDefault parses /etc/default/loki for LOKI_HOME=<path>. Quiet on errors.
func readEtcDefault() string {
b, err := os.ReadFile("/etc/default/loki")
if err != nil {
return ""
}
for _, line := range strings.Split(string(b), "\n") {
s := strings.TrimSpace(line)
if s == "" || strings.HasPrefix(s, "#") {
continue
}
s = strings.TrimPrefix(s, "export ")
if k, v, ok := strings.Cut(s, "="); ok && strings.TrimSpace(k) == "LOKI_HOME" {
return strings.Trim(strings.TrimSpace(v), "\"'")
}
}
return ""
}
// Arborescence de $LOKI_HOME. Elle tient en six dossiers, et rien d'autre :
// tout le reste (config, préférences, conversation, clés, drapeaux) vit dans la
// base loki.db — voir store.go.
func backendsDir() string { return filepath.Join(LokiHome(), "backends") }
func binDir() string { return filepath.Join(LokiHome(), "bin") }
func presetsDir() string { return filepath.Join(LokiHome(), "presets") }
// memoryDir est le dossier mémoire du projet ACTIF. C'est LA couture des projets
// (projects.go) : tout le code mémoire passe par ici, donc il hérite du
// cloisonnement sans avoir à connaître la notion de projet. Avant les projets,
// memory/ était plat ; il est désormais la racine des dossiers de projet, et
// ensureDefaultProject a migré les pages historiques vers memory/generale/.
func memoryDir() string { return projectMemoryDir(activeProjectSlug()) }
func modelsDir() string { return filepath.Join(LokiHome(), "models") }
func workspaceDir() string { return filepath.Join(LokiHome(), "workspace") }
// scriptsDir est le dossier des scripts de l'agent : à côté de memory/presets,
// HORS du workspace. Le workspace est un bac à sable jetable (clones, tests,
// fichiers d'une discussion) qu'un nettoyage — ou la suppression de la
// discussion — peut raser ; les scripts qu'on veut CONSERVER vivent ici.
func scriptsDir() string { return filepath.Join(LokiHome(), "scripts") }
// serviceName est le nom de l'unité qui exécute llama-server. Son pendant est
// uiUnitName (« loki-ui »), qui sert l'interface et le tunnel.
func serviceName() string {
if n := os.Getenv("LOKI_SERVICE"); n != "" {
return n
}
return "loki-engine"
}
// Color helpers (ANSI). Disabled when stdout is not a TTY.
var colorOn = isTerminal()
func col(code, s string) string {
if !colorOn {
return s
}
return "\033[" + code + "m" + s + "\033[0m"
}
func bold(s string) string { return col("1", s) }
func cyan(s string) string { return col("1;36", s) }
func green(s string) string { return col("32", s) }
func red(s string) string { return col("31", s) }
func dim(s string) string { return col("2", s) }
func yellow(s string) string { return col("33", s) }
func magenta(s string) string { return col("35", s) }