mirror of
https://github.com/R0m1k3/Loki.git
synced 2026-10-11 17:26:57 +02:00
Le jeton ne vivait que dans la variable d'environnement HF_TOKEN : découvrir depuis l'interface qu'un dépôt est verrouillé, c'était devoir éditer un docker-compose et recréer le conteneur pour y répondre. - Éditeur de preset → Modèle → « Jeton Hugging Face » : ligne repliée comme « Dossiers de modèles », qui affiche l'état (aucun / masqué / fourni par l'environnement). Le bandeau d'un dépôt verrouillé y mène d'un clic. - Le jeton est VÉRIFIÉ auprès de /api/whoami-v2 avant d'être enregistré (le compte s'affiche) : un jeton mal collé accepté en silence rendrait le 401 qu'on cherchait à expliquer. Il est rangé avec les secrets en base d'état, pas dans config.env que le changement de preset réécrit en bloc, et n'est jamais renvoyé en clair — seulement masqué. - Priorité : jeton enregistré, puis HF_TOKEN. Rien d'enregistré = comportement d'avant à l'identique. L'enregistrement vide le cache des réponses obtenues sans jeton. - Le jeton n'est envoyé qu'aux adresses Hugging Face : un lien collé vers un autre hébergeur n'a aucune raison de recevoir un secret. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Hz1QWmvZqW3t53YC5SJBKC
134 lines
4.8 KiB
Go
134 lines
4.8 KiB
Go
package loki
|
|
|
|
// web_hf.go — routes de recherche de modèles sur Hugging Face.
|
|
//
|
|
// GET /api/hf/search?q=… dépôts GGUF correspondants
|
|
// GET /api/hf/files?repo=auteur/dépôt modèles, projecteurs et drafts du dépôt
|
|
// GET|POST /api/hf/token jeton Hugging Face (dépôts verrouillés)
|
|
//
|
|
// Les deux renvoient aussi le matériel local, pour que l'interface n'ait pas à
|
|
// le demander séparément et n'affiche jamais une liste de tailles sans le
|
|
// budget en face.
|
|
|
|
import (
|
|
"encoding/json"
|
|
"net/http"
|
|
"strconv"
|
|
"strings"
|
|
)
|
|
|
|
func handleHFSearch(w http.ResponseWriter, r *http.Request) {
|
|
q := strings.TrimSpace(r.URL.Query().Get("q"))
|
|
if q == "" {
|
|
sendJSON(w, 400, map[string]any{"ok": false, "error": "précise ce que tu cherches"})
|
|
return
|
|
}
|
|
repos, err := hfSearch(r.Context(), q)
|
|
if err != nil {
|
|
sendJSON(w, 502, map[string]any{"ok": false, "error": err.Error()})
|
|
return
|
|
}
|
|
sendJSON(w, 200, map[string]any{"ok": true, "hardware": detectHardware(), "repos": repos, "hf_token": hfTokenSet()})
|
|
}
|
|
|
|
// handleHFFiles liste les .gguf d'un dépôt, chaque modèle portant son verdict
|
|
// « ça tient / ça ne tient pas ».
|
|
//
|
|
// Le verdict d'un modèle inclut le projecteur QUE si le dépôt en publie un : sur
|
|
// un dépôt sans vision, compter un projecteur inexistant ferait basculer à tort
|
|
// des modèles en « trop ». Le contexte pris en compte est celui du preset actif
|
|
// (CTX), pas une constante : c'est lui qui sera réellement lancé.
|
|
func handleHFFiles(w http.ResponseWriter, r *http.Request) {
|
|
repo := strings.TrimSpace(r.URL.Query().Get("repo"))
|
|
if repo == "" {
|
|
sendJSON(w, 400, map[string]any{"ok": false, "error": "dépôt manquant"})
|
|
return
|
|
}
|
|
list, err := hfFiles(r.Context(), repo)
|
|
if err != nil {
|
|
sendJSON(w, 502, map[string]any{"ok": false, "error": err.Error()})
|
|
return
|
|
}
|
|
hw := detectHardware()
|
|
|
|
// Contexte du preset actif, avec le même défaut que backend_serve.go — sinon
|
|
// le verdict est calculé sur un contexte que personne n'utilisera.
|
|
ctxTokens := 32768
|
|
if v, convErr := strconv.Atoi(strings.TrimSpace(ReadConfig()["CTX"])); convErr == nil && v > 0 {
|
|
ctxTokens = v
|
|
}
|
|
|
|
var mmSize int64
|
|
if p, ok := hfPickProjector(list.Projectors); ok {
|
|
mmSize = p.Size
|
|
}
|
|
for i := range list.Models {
|
|
list.Models[i].Verdict, list.Models[i].Why = fitVerdict(hw, list.Models[i].Size, mmSize, ctxTokens)
|
|
}
|
|
// `vision` sort d'ici et de nulle part ailleurs : c'est la présence d'un
|
|
// mmproj dans CE dépôt, pas un tag Hugging Face — lequel manque sur des
|
|
// dépôts qui en publient pourtant un.
|
|
// Un projecteur ne reçoit pas de verdict : il ne se charge jamais seul.
|
|
// `gated` et `hf_token` servent le même but que `vision` : dire AVANT le
|
|
// téléchargement ce qui va se passer. Un dépôt verrouillé se lit sans jeton
|
|
// mais ne se télécharge pas — l'avertir ici évite de choisir un quant, de
|
|
// lancer 15 Go et de récolter un 401.
|
|
sendJSON(w, 200, map[string]any{
|
|
"ok": true, "hardware": hw, "ctx": ctxTokens,
|
|
"repo": list.Repo, "models": list.Models,
|
|
"projectors": list.Projectors, "drafts": list.Drafts,
|
|
"vision": len(list.Projectors) > 0,
|
|
"gated": list.Gated,
|
|
"hf_token": hfTokenSet(),
|
|
})
|
|
}
|
|
|
|
// handleHFToken lit et enregistre le jeton Hugging Face.
|
|
//
|
|
// GET → {ok, set, source:"config"|"env"|"", masked, account}
|
|
// POST {token:"hf_…"} → vérifie le jeton PUIS l'enregistre
|
|
// POST {token:""} → efface le jeton enregistré
|
|
//
|
|
// Le jeton n'est jamais renvoyé en clair : il ne sert qu'ici, personne n'a
|
|
// besoin de le recopier depuis Loki. Et il est vérifié AVANT d'être enregistré —
|
|
// un jeton mal collé qu'on accepte silencieusement redonne exactement le 401
|
|
// qu'on cherchait à expliquer.
|
|
func handleHFToken(w http.ResponseWriter, r *http.Request) {
|
|
if r.Method == http.MethodPost {
|
|
var req struct {
|
|
Token string `json:"token"`
|
|
}
|
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
|
sendJSON(w, 400, map[string]any{"ok": false, "error": "requête illisible"})
|
|
return
|
|
}
|
|
tok := strings.TrimSpace(req.Token)
|
|
account := ""
|
|
if tok != "" {
|
|
var err error
|
|
if account, err = hfWhoAmI(r.Context(), tok); err != nil {
|
|
sendJSON(w, 400, map[string]any{"ok": false, "error": err.Error()})
|
|
return
|
|
}
|
|
}
|
|
if err := writeHFToken(tok); err != nil {
|
|
sendJSON(w, 500, map[string]any{"ok": false, "error": err.Error()})
|
|
return
|
|
}
|
|
sendJSON(w, 200, hfTokenState(account))
|
|
return
|
|
}
|
|
sendJSON(w, 200, hfTokenState(""))
|
|
}
|
|
|
|
// hfTokenState décrit le jeton pour l'interface. account n'est renseigné qu'au
|
|
// retour d'un enregistrement : le vérifier à chaque affichage ferait un appel
|
|
// sortant à l'ouverture de la fenêtre des réglages.
|
|
func hfTokenState(account string) map[string]any {
|
|
src := hfTokenSource()
|
|
return map[string]any{
|
|
"ok": true, "set": src != "", "source": src,
|
|
"masked": maskHFToken(hfToken()), "account": account,
|
|
}
|
|
}
|