package loki import ( "bytes" "context" "fmt" "os" "os/exec" "path/filepath" "strings" "time" ) // binSupportsReasoningFlag dit si ce llama-server accepte « --reasoning ». // // Le drapeau est récent : les moteurs plus anciens, et certains forks, ne le // connaissent pas et REFUSENT de démarrer sur un argument inconnu. Comme on ne // l'ajoute que pour interdire le raisonnement, mieux vaut demander au binaire // que parier : on lit son aide, une fois, au lancement du moteur. // // L'aide se lit avec le même chemin de bibliothèques que le vrai lancement // (setLibraryPath a déjà été appelé) : sans ça un moteur parfaitement valide // échoue à s'exécuter (« libllama-common.so introuvable ») et on conclurait à // tort qu'il ne gère pas le drapeau. func binSupportsReasoningFlag(bin string) bool { ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel() cmd := exec.CommandContext(ctx, bin, "--help") var out bytes.Buffer cmd.Stdout, cmd.Stderr = &out, &out if err := cmd.Run(); err != nil && out.Len() == 0 { return false // aide illisible : on ne prend pas le risque } return strings.Contains(out.String(), "--reasoning ") } // cmdServe replaces the historic start.sh: read config.env, build the // llama-server invocation, and exec it (replacing this process so systemd // supervises llama-server directly). func cmdServe(args []string) error { cfg := ReadConfig() bin := cfg["BIN"] if bin == "" { return fmt.Errorf("BIN non défini — lance « loki edit »") } model := cfg["MODEL"] if model == "" { return fmt.Errorf("MODEL non défini — lance « loki edit »") } // MODEL vaut soit un simple nom de fichier (le .gguf vit dans LOKI_HOME ou // dans un dossier déclaré — disque externe…), soit un chemin absolu. Sous // systemd/launchd le WorkingDirectory vaut LOKI_HOME, donc le relatif tombait // juste ; lancé depuis une app de bureau, le répertoire courant est « / » et // llama-server ne trouvait rien. On résout donc explicitement, quel que soit // le contexte de lancement. resolved, err := resolveServeModelPath(model) if err != nil { return err } model = resolved if _, err := os.Stat(model); err != nil { return fmt.Errorf("modèle introuvable : %s", model) } // Modèle découpé en tranches : llama-server ouvre les suivantes tout seul, mais // s'il en manque une il démarre puis meurt sur un tenseur introuvable — message // incompréhensible, et systemd relance en boucle. On le dit ici, en clair. if missing := shardFamilyMissing(filepath.Dir(model), filepath.Base(model)); len(missing) > 0 { return fmt.Errorf("modèle incomplet : il manque %s dans %s — ce modèle tient en %d fichiers, télécharge-les tous", strings.Join(missing, ", "), filepath.Dir(model), len(shardFamily(filepath.Base(model)))) } if !filepath.IsAbs(bin) { bin = filepath.Join(LokiHome(), bin) } // Le moteur précompilé s'installe dans un dossier versionné : un preset écrit // avant une mise à jour pointe sur une release qui n'existe plus. On le fait // suivre au moteur courant plutôt que d'échouer en 127. bin = prebuiltResolveBin(bin) // Make sure llama-server can find its bundled shared libraries (the .so/.dll // neighbours of the binary). This is platform-specific: LD_LIBRARY_PATH on // Linux, PATH on Windows — handled inside execServer. setLibraryPath(filepath.Dir(bin)) // Sélection GPU (loki gpu) : on filtre les devices visibles par llama-server. // CUDA_DEVICE_ORDER=PCI_BUS_ID garantit que les index correspondent à ceux // affichés par nvidia-smi (sinon CUDA réordonne par "device le plus rapide"). if v := cfg["CUDA_VISIBLE_DEVICES"]; v != "" { _ = os.Setenv("CUDA_VISIBLE_DEVICES", v) _ = os.Setenv("CUDA_DEVICE_ORDER", "PCI_BUS_ID") } get := func(key, fallback string) string { if v, ok := cfg[key]; ok && v != "" { return v } return fallback } kv := get("KV_TYPE", "") ktv := get("KV_TYPE_K", kv) vtv := get("KV_TYPE_V", kv) llmArgs := []string{bin, "-m", model, "-c", get("CTX", "32768"), "-t", get("THREADS", "0"), "-tb", get("THREADS_BATCH", "0"), "-b", get("BATCH", "2048"), "-ub", get("UBATCH", "512"), "--host", get("HOST", "0.0.0.0"), "--port", get("PORT", "8080"), } // NGL=auto : on n'envoie PAS -ngl, et llama.cpp choisit lui-même combien de // couches tiennent dans la VRAM libre (common_fit_params). Il s'en abstient // dès que la valeur est imposée — « n_gpu_layers already set by user to 999, // abort » — d'où des cudaMalloc en échec, un repli partiel sur le CPU et un // débit effondré alors que le GPU tourne à 100 %. // Le défaut reste 999 (tout sur le GPU) : omettre -ngl sur un llama.cpp // antérieur à cet ajustement automatique le ferait tourner 100 % CPU. if ngl := get("NGL", "999"); !strings.EqualFold(ngl, "auto") { llmArgs = append(llmArgs, "-ngl", ngl) } if ktv != "" { llmArgs = append(llmArgs, "-ctk", ktv) } if vtv != "" { llmArgs = append(llmArgs, "-ctv", vtv) } // Vision : le projecteur multimodal (mmproj-*.gguf) donne des yeux au modèle. // C'est un fichier .gguf À PART du modèle, chargé via --mmproj. On le résout // comme le modèle (nom simple cherché dans les dossiers déclarés, ou chemin // absolu), pour qu'un preset écrit sous Windows reste lançable ailleurs et que // le champ « Vision » de l'interface n'ait qu'à écrire le nom du fichier. // Introuvable = on préfère le dire clairement plutôt que laisser llama-server // mourir en boucle sur un « failed to load mmproj » cryptique. if mm := strings.TrimSpace(cfg["MMPROJ"]); mm != "" { mmPath, err := resolveServeModelPath(mm) if err != nil { return fmt.Errorf("projecteur vision introuvable : %s (%v)", mm, err) } if _, err := os.Stat(mmPath); err != nil { return fmt.Errorf("projecteur vision introuvable : %s", mmPath) } llmArgs = append(llmArgs, "--mmproj", mmPath) } // Raisonnement. Trois cas, et la nuance compte : // // REASONING=on|auto|deepseek → --reasoning // REASONING=off → --reasoning off (interdiction EXPLICITE) // clé absente → aucun drapeau, le moteur fait son défaut // // L'interface écrivait « off » en EFFAÇANT la ligne, ce qui n'est pas du tout // la même chose : sans drapeau, llama-server suit le gabarit du modèle, et un // modèle à raisonnement raisonne. L'interrupteur affichait donc « désactivé » // pendant que le modèle réfléchissait quand même. Il faut le dire au moteur. if r := strings.TrimSpace(cfg["REASONING"]); r != "" { if reasoningActive(r) { // budget illimité par défaut (-1) : on laisse le modèle réfléchir jusqu'au // bout au lieu de le couper à 2048, ce qui tronquait la vraie réponse (la // réflexion atteignait le plafond et il ne restait plus de marge pour le // contenu). L'anti-boucle côté llm_client.go reste le garde-fou. NE PAS forcer 0 : // sur llama.cpp vanilla, 0 = "immediate end" → coupe tout le raisonnement // (le fork ik_llama.cpp l'ignore). Configurable via REASONING_BUDGET. llmArgs = append(llmArgs, "--reasoning", r, "--reasoning-budget", get("REASONING_BUDGET", "-1")) } else if binSupportsReasoningFlag(bin) { // Pas de budget ici : « off » suffit, et un budget sur un moteur qui // n'attend rien d'autre ne ferait qu'ajouter une occasion d'échouer. llmArgs = append(llmArgs, "--reasoning", "off") } else { // Vieux moteur (ou fork) qui ne connaît pas le drapeau : le lui passer // le ferait sortir en erreur au démarrage, donc boucler. On le dit et on // continue sans — mieux vaut un modèle qui réfléchit qu'un moteur mort. fmt.Fprintf(os.Stderr, "[loki serve] ce moteur ne connaît pas --reasoning : impossible de désactiver le raisonnement\n") } } // API_KEY protège le serveur quand il est exposé sur internet : llama-server // exige alors l'en-tête "Authorization: Bearer ". La clé est lue depuis // $LOKI_HOME/.api_key en priorité (elle survit ainsi aux changements de preset // qui réécrivent config.env), avec config.env comme repli rétro-compatible. if k, _ := effectiveAPIKeyErr(); k != "" { llmArgs = append(llmArgs, "--api-key", k) } // EXTRA_ARGS is appended verbatim, split like the shell would — quotes kept // together so a path with spaces stays one argument. llmArgs = append(llmArgs, splitArgs(cfg["EXTRA_ARGS"])...) // Working dir = LOKI_HOME so relative paths in EXTRA_ARGS (e.g. --mmproj // mmproj-F16.gguf) still resolve. _ = os.Chdir(LokiHome()) fmt.Fprintf(os.Stderr, "[loki serve] %s model=%s port=%s\n", bin, filepath.Base(model), get("PORT", "8080")) // Hand off to the llama-server process. On Unix this replaces the current // process (exec); on Windows it runs as a child and waits. See sys_platform_*.go. return execServer(bin, llmArgs) }