mirror of
https://github.com/R0m1k3/Loki.git
synced 2026-10-19 05:07:20 +02:00
Lot 8 : toutes les routes des modèles tenues par la façade (LOKI_MODELS=rust)
La variable LOKI_DOWNLOADS devient LOKI_MODELS et couvre, en plus des téléchargements, la liste du sélecteur (avec qui référence chaque .gguf), la suppression (refus si en service, 409 avec les presets sans force, tranches emportées), les dossiers de modèles et le catalogue Hugging Face (recherche, fichiers d'un dépôt avec verdict mémoire, jeton vérifié puis enregistré, cache vidé à son changement). Côté Go, facade_models.go remet ces routes à la façade quand elles arrivent par le tunnel du relais. Parité : parity_models_test.go tire du Go les fonctions pures du catalogue et un scénario sur disque de 20 appels (liste, dossiers, suppressions), rejoués à l'identique par tests/models.rs ; catalogue et routes testés de bout en bout contre un faux Hugging Face. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016tcGfJWefW5kUGoC2wL8cv
This commit is contained in:
22 files changed
+3395
-187
No files matched your search
@@ -427,16 +427,17 @@ async fn handle(st: Arc<FakeGo>, req: Request<Incoming>) -> Response<Full<Bytes>
|
||||
/// Lance le faux Go puis la façade devant lui ; rend (adresse de la façade,
|
||||
/// état du faux Go, garde d'arrêt).
|
||||
pub async fn start(home: std::path::PathBuf) -> (SocketAddr, Arc<FakeGo>, oneshot::Sender<()>) {
|
||||
let (f, st, tx, _) = start_opts(home, false, None, false).await;
|
||||
let (f, st, tx, _) = start_opts(home, false, None, None).await;
|
||||
(f, st, tx)
|
||||
}
|
||||
|
||||
/// Comme [`start`], avec les téléchargements de modèles tenus par la façade
|
||||
/// (`LOKI_DOWNLOADS=rust`).
|
||||
pub async fn start_downloads(
|
||||
/// Comme [`start`], avec les routes des modèles tenues par la façade
|
||||
/// (`LOKI_MODELS=rust`) ; `hf` : adresse du faux Hugging Face.
|
||||
pub async fn start_models(
|
||||
home: std::path::PathBuf,
|
||||
hf: &str,
|
||||
) -> (SocketAddr, Arc<FakeGo>, oneshot::Sender<()>) {
|
||||
let (f, st, tx, _) = start_opts(home, false, None, true).await;
|
||||
let (f, st, tx, _) = start_opts(home, false, None, Some(hf.to_string())).await;
|
||||
(f, st, tx)
|
||||
}
|
||||
|
||||
@@ -451,7 +452,7 @@ pub async fn start_chat(
|
||||
oneshot::Sender<()>,
|
||||
Arc<loki_rs::chat::generate::ChatService>,
|
||||
) {
|
||||
let (f, st, tx, chat) = start_opts(home, true, Some(engine_port), false).await;
|
||||
let (f, st, tx, chat) = start_opts(home, true, Some(engine_port), None).await;
|
||||
(f, st, tx, chat.unwrap())
|
||||
}
|
||||
|
||||
@@ -459,7 +460,7 @@ async fn start_opts(
|
||||
home: std::path::PathBuf,
|
||||
with_chat: bool,
|
||||
engine_port: Option<u16>,
|
||||
with_downloads: bool,
|
||||
models_hf: Option<String>,
|
||||
) -> (
|
||||
SocketAddr,
|
||||
Arc<FakeGo>,
|
||||
@@ -516,7 +517,12 @@ async fn start_opts(
|
||||
service: "loki-engine".into(),
|
||||
},
|
||||
chat: chat.clone(),
|
||||
downloads: with_downloads.then(|| Arc::new(loki_rs::downloads::Downloads::new())),
|
||||
models: models_hf.map(|hf| {
|
||||
Arc::new(loki_rs::models::Models {
|
||||
downloads: Arc::new(loki_rs::downloads::Downloads::new()),
|
||||
hf: loki_rs::hf::HfClient::new(&hf),
|
||||
})
|
||||
}),
|
||||
};
|
||||
tokio::spawn(loki_rs::proxy::serve(fl, cfg, async move {
|
||||
let _ = rx.await;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
//! Lot « modèles » : téléchargements tenus par la façade (`LOKI_DOWNLOADS=rust`).
|
||||
//! Lot 8 : téléchargements de modèles tenus par la façade (`LOKI_MODELS=rust`).
|
||||
//!
|
||||
//! - les fonctions pures rejouent le journal tiré du Go
|
||||
//! (`internal/loki/parity_dl_test.go` → `tests/parity_models/download.json`) ;
|
||||
@@ -572,7 +572,7 @@ async fn routes_servies_par_la_facade() {
|
||||
);
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let home = tmp.path().canonicalize().unwrap();
|
||||
let (facade, go, _stop) = fakego::start_downloads(home.clone()).await;
|
||||
let (facade, go, _stop) = fakego::start_models(home.clone(), "http://127.0.0.1:1").await;
|
||||
let url = format!("http://{addr}/u/r/resolve/main/m.gguf");
|
||||
let body = format!(r#"{{"url":"{url}","dir":""}}"#);
|
||||
|
||||
|
||||
@@ -94,7 +94,7 @@ async fn banc(cle: Option<&str>) -> Banc {
|
||||
service: "loki-engine".into(),
|
||||
},
|
||||
chat: None,
|
||||
downloads: None,
|
||||
models: None,
|
||||
};
|
||||
tokio::spawn(loki_rs::proxy::serve(l, cfg, async move {
|
||||
let _ = rx.await;
|
||||
|
||||
@@ -0,0 +1,557 @@
|
||||
//! Lot 8 : routes des modèles tenues par la façade (`LOKI_MODELS=rust`).
|
||||
//!
|
||||
//! - le journal tiré du Go (`internal/loki/parity_models_test.go` →
|
||||
//! `tests/parity_models/models.json`) : fonctions pures du catalogue, puis
|
||||
//! un scénario sur disque (liste, dossiers, suppressions) rejoué à
|
||||
//! l'identique ;
|
||||
//! - le catalogue Hugging Face contre un faux serveur (recherche, dépôt,
|
||||
//! jeton, cache) ;
|
||||
//! - les routes par la façade.
|
||||
|
||||
mod common;
|
||||
|
||||
use std::collections::HashMap;
|
||||
use std::convert::Infallible;
|
||||
use std::net::SocketAddr;
|
||||
use std::path::Path;
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
||||
use bytes::Bytes;
|
||||
use common::fakego;
|
||||
use http_body_util::Full;
|
||||
use hyper::body::Incoming;
|
||||
use hyper::{Request, Response, StatusCode, service::service_fn};
|
||||
use hyper_util::rt::TokioIo;
|
||||
use loki_rs::downloads::ModelDirs;
|
||||
use loki_rs::hf::{self, Hardware, HfEntry, HfEnv, HfListing};
|
||||
use loki_rs::models;
|
||||
use serde_json::Value;
|
||||
|
||||
fn fixture() -> Value {
|
||||
serde_json::from_str(include_str!("parity_models/models.json")).expect("journal")
|
||||
}
|
||||
|
||||
fn mask_free(v: &mut Value) {
|
||||
match v {
|
||||
Value::Object(m) => {
|
||||
for (k, e) in m.iter_mut() {
|
||||
if k == "free" || k == "free_exact" {
|
||||
*e = Value::String("…".into());
|
||||
} else {
|
||||
mask_free(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
Value::Array(a) => a.iter_mut().for_each(mask_free),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parite_des_fonctions_pures() {
|
||||
let fx = fixture();
|
||||
for (n, want) in fx["quant"].as_object().unwrap() {
|
||||
assert_eq!(
|
||||
models::quant_from_name(n),
|
||||
want.as_str().unwrap(),
|
||||
"quant({n})"
|
||||
);
|
||||
}
|
||||
for c in fx["split_args"].as_array().unwrap() {
|
||||
let want: Vec<String> = serde_json::from_value(c["out"].clone()).unwrap();
|
||||
assert_eq!(models::split_args(c["in"].as_str().unwrap()), want, "{c}");
|
||||
}
|
||||
for (t, want) in fx["mask"].as_object().unwrap() {
|
||||
assert_eq!(hf::mask_hf_token(t), want.as_str().unwrap(), "mask({t:?})");
|
||||
}
|
||||
for c in fx["gated"].as_array().unwrap() {
|
||||
assert_eq!(hf::gated_flag(&c["in"]), c["out"].as_bool().unwrap(), "{c}");
|
||||
}
|
||||
let cl = &fx["classify"];
|
||||
let files: Vec<(String, i64)> = cl["files"]
|
||||
.as_array()
|
||||
.unwrap()
|
||||
.iter()
|
||||
.map(|f| {
|
||||
(
|
||||
f["path"].as_str().unwrap().to_string(),
|
||||
f["size"].as_i64().unwrap(),
|
||||
)
|
||||
})
|
||||
.collect();
|
||||
let got = hf::classify(hf::HF_HOST, cl["repo"].as_str().unwrap(), &files);
|
||||
let want: HfListing = serde_json::from_value(cl["out"].clone()).unwrap();
|
||||
assert_eq!(got, want);
|
||||
// Sérialisé comme le Go (listes vides → null).
|
||||
assert_eq!(serde_json::to_value(&got).unwrap(), cl["out"]);
|
||||
for c in fx["projector"].as_array().unwrap() {
|
||||
let list: Vec<HfEntry> = serde_json::from_value(c["in"].clone()).unwrap_or_default();
|
||||
let got = hf::pick_projector(&list)
|
||||
.map(|e| e.name.clone())
|
||||
.unwrap_or_default();
|
||||
assert_eq!(got, c["name"].as_str().unwrap(), "{c}");
|
||||
}
|
||||
for c in fx["verdicts"].as_array().unwrap() {
|
||||
let hw: Hardware = serde_json::from_value(c["hardware"].clone()).unwrap();
|
||||
// Sans GPU : null, comme une tranche nil du Go (24.0 contre 24 : même
|
||||
// nombre en JSON).
|
||||
let gpus = &serde_json::to_value(&hw).unwrap()["gpus"];
|
||||
assert_eq!(gpus.is_null(), c["hardware"]["gpus"].is_null());
|
||||
let (v, why) = hf::fit_verdict(
|
||||
&hw,
|
||||
c["weights"].as_i64().unwrap(),
|
||||
c["mmproj"].as_i64().unwrap(),
|
||||
c["ctx"].as_i64().unwrap(),
|
||||
);
|
||||
assert_eq!(
|
||||
(v.as_str(), why.as_str()),
|
||||
(c["verdict"].as_str().unwrap(), c["why"].as_str().unwrap())
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
fn tree_files(home: &Path) -> Vec<String> {
|
||||
let mut out = Vec::new();
|
||||
for top in ["disque", "models"] {
|
||||
let mut stack = vec![home.join(top)];
|
||||
while let Some(d) = stack.pop() {
|
||||
let Ok(rd) = std::fs::read_dir(&d) else {
|
||||
continue;
|
||||
};
|
||||
for e in rd.filter_map(Result::ok) {
|
||||
let p = e.path();
|
||||
if p.is_dir() {
|
||||
stack.push(p);
|
||||
} else {
|
||||
out.push(p.strip_prefix(home).unwrap().to_string_lossy().into_owned());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
out.sort();
|
||||
out
|
||||
}
|
||||
|
||||
/// Monte l'arborescence du scénario sous `home`.
|
||||
fn setup_tree(sc: &Value, home: &Path) -> (Vec<String>, HashMap<String, String>) {
|
||||
let h = home.to_string_lossy().into_owned();
|
||||
let unhome = |s: &str| s.replace("{{HOME}}", &h);
|
||||
for (rel, size) in sc["files"].as_object().unwrap() {
|
||||
let p = home.join(rel);
|
||||
let size = size.as_i64().unwrap();
|
||||
if size < 0 {
|
||||
std::fs::create_dir_all(&p).unwrap();
|
||||
continue;
|
||||
}
|
||||
std::fs::create_dir_all(p.parent().unwrap()).unwrap();
|
||||
std::fs::write(&p, vec![0u8; size as usize]).unwrap();
|
||||
}
|
||||
std::fs::create_dir_all(home.join("presets")).unwrap();
|
||||
for (id, content) in sc["presets"].as_object().unwrap() {
|
||||
std::fs::write(
|
||||
home.join("presets").join(format!("{id}.env")),
|
||||
unhome(content.as_str().unwrap()),
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
let extra = sc["extra"]
|
||||
.as_array()
|
||||
.unwrap()
|
||||
.iter()
|
||||
.map(|d| unhome(d.as_str().unwrap()))
|
||||
.collect::<Vec<_>>();
|
||||
let cfg = sc["config"]
|
||||
.as_object()
|
||||
.unwrap()
|
||||
.iter()
|
||||
.map(|(k, v)| (k.clone(), unhome(v.as_str().unwrap())))
|
||||
.collect();
|
||||
(extra, cfg)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parite_du_scenario_sur_disque() {
|
||||
let fx = fixture();
|
||||
let sc = &fx["scenario"];
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let home = tmp.path().canonicalize().unwrap();
|
||||
let h = home.to_string_lossy().into_owned();
|
||||
let (extra, cfg) = setup_tree(sc, &home);
|
||||
let mut dirs = ModelDirs {
|
||||
home: home.clone(),
|
||||
// saveExtraModelDirs : nettoyée, dédoublonnée.
|
||||
stored: loki_rs::gguf::dedup_dirs(&extra, &format!("{h}/models")),
|
||||
env: None,
|
||||
};
|
||||
let norm = |v: Value| -> Value {
|
||||
let s = serde_json::to_string(&v).unwrap().replace(&h, "{{HOME}}");
|
||||
let mut v: Value = serde_json::from_str(&s).unwrap();
|
||||
mask_free(&mut v);
|
||||
v
|
||||
};
|
||||
for (i, c) in sc["calls"].as_array().unwrap().iter().enumerate() {
|
||||
let (method, path) = (c["method"].as_str().unwrap(), c["path"].as_str().unwrap());
|
||||
let body: Value = c
|
||||
.get("body")
|
||||
.and_then(Value::as_str)
|
||||
.map(|b| serde_json::from_str(&b.replace("{{HOME}}", &h)).unwrap())
|
||||
.unwrap_or(Value::Null);
|
||||
let s = |k: &str| {
|
||||
body.get(k)
|
||||
.and_then(Value::as_str)
|
||||
.unwrap_or("")
|
||||
.to_string()
|
||||
};
|
||||
let (status, out) = match (method, path) {
|
||||
("GET", "/api/models") => {
|
||||
let refs = models::build_refs(&dirs, &cfg, &[]);
|
||||
(200, models::list_models(&dirs, &refs))
|
||||
}
|
||||
("GET", "/api/models/dirs") => (200, models::dirs_json(&dirs)),
|
||||
("POST", "/api/models/delete") => {
|
||||
let refs = models::build_refs(&dirs, &cfg, &[]);
|
||||
let force = body.get("force").and_then(Value::as_bool).unwrap_or(false);
|
||||
let (code, out) = models::delete_model(&dirs, &refs, &s("name"), force);
|
||||
(code.as_u16(), out)
|
||||
}
|
||||
("POST", "/api/models/dirs") => {
|
||||
match models::dirs_update(&dirs, &s("path"), &s("action")) {
|
||||
Ok(list) => {
|
||||
dirs.stored = list;
|
||||
(200, serde_json::json!({ "ok": true }))
|
||||
}
|
||||
Err(e) => (400, serde_json::json!({ "ok": false, "error": e })),
|
||||
}
|
||||
}
|
||||
_ => panic!("appel inattendu {c}"),
|
||||
};
|
||||
let ctx = format!(
|
||||
"appel {i} : {method} {path} {}",
|
||||
c.get("body").unwrap_or(&Value::Null)
|
||||
);
|
||||
assert_eq!(status as u64, c["status"].as_u64().unwrap(), "{ctx}");
|
||||
assert_eq!(norm(out), c["out"], "{ctx}");
|
||||
if let Some(want) = c.get("files") {
|
||||
let want: Vec<String> = serde_json::from_value(want.clone()).unwrap();
|
||||
assert_eq!(tree_files(&home), want, "{ctx}");
|
||||
}
|
||||
if let Some(want) = c.get("stored") {
|
||||
let want: Vec<String> = serde_json::from_value(want.clone()).unwrap();
|
||||
let got: Vec<String> = dirs
|
||||
.stored
|
||||
.iter()
|
||||
.map(|d| d.replace(&h, "{{HOME}}"))
|
||||
.collect();
|
||||
assert_eq!(got, want, "{ctx}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn adresses_et_echappements_comme_le_go() {
|
||||
assert_eq!(
|
||||
hf::search_endpoint("https://huggingface.co", " qwen 3 & co "),
|
||||
"https://huggingface.co/api/models?direction=-1&expand%5B%5D=gated&expand%5B%5D=downloads&expand%5B%5D=likes&filter=gguf&limit=25&search=qwen+3+%26+co&sort=downloads"
|
||||
);
|
||||
// Tronquée à 100 octets, même au milieu d'un caractère (q[:100] du Go).
|
||||
let long = format!("{}é", "a".repeat(99));
|
||||
assert!(hf::search_endpoint("h", &long).contains(&format!("search={}%C3&", "a".repeat(99))));
|
||||
assert_eq!(
|
||||
hf::go_path_escape("a b+c&(d)/é"),
|
||||
"a%20b+c&%28d%29%2F%C3%A9"
|
||||
);
|
||||
assert!(
|
||||
hf::repo_ok("a/b.c-d_e")
|
||||
&& !hf::repo_ok("../x")
|
||||
&& !hf::repo_ok("a/b/c")
|
||||
&& !hf::repo_ok("-a/b")
|
||||
);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Faux Hugging Face
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
#[derive(Default)]
|
||||
struct FakeHf {
|
||||
log: Mutex<Vec<String>>,
|
||||
auth: Mutex<Vec<String>>,
|
||||
gated: Mutex<Value>,
|
||||
}
|
||||
|
||||
async fn hf_handle(st: Arc<FakeHf>, req: Request<Incoming>) -> Response<Full<Bytes>> {
|
||||
let pq = req
|
||||
.uri()
|
||||
.path_and_query()
|
||||
.map(|p| p.as_str().to_string())
|
||||
.unwrap_or_default();
|
||||
st.log.lock().unwrap().push(pq.clone());
|
||||
let auth = req
|
||||
.headers()
|
||||
.get("authorization")
|
||||
.map(|v| v.to_str().unwrap().to_string())
|
||||
.unwrap_or_default();
|
||||
st.auth.lock().unwrap().push(auth.clone());
|
||||
let path = req.uri().path();
|
||||
let (code, body) = if path == "/api/whoami-v2" {
|
||||
if auth == "Bearer hf_bon_jeton" {
|
||||
(200, r#"{"name":"moi","type":"user"}"#.to_string())
|
||||
} else {
|
||||
(401, r#"{"error":"Invalid credentials"}"#.to_string())
|
||||
}
|
||||
} else if path == "/api/models" {
|
||||
(200, r#"[{"id":"u/a-GGUF","downloads":10,"likes":2,"gated":false},{"id":"u/b-GGUF","downloads":5,"likes":1,"gated":"manual"}]"#.to_string())
|
||||
} else if path == "/api/models/u/a-GGUF/tree/main" {
|
||||
(200, r#"[{"type":"directory","path":"Q4"},{"type":"file","path":"Q4/a-Q4_K_M.gguf","size":134,"lfs":{"size":5368709120}},{"type":"file","path":"mmproj-Q8_0.gguf","size":629145600},{"type":"file","path":"README.md","size":10}]"#.to_string())
|
||||
} else if path == "/api/models/u/a-GGUF" {
|
||||
(
|
||||
200,
|
||||
format!(
|
||||
r#"{{"id":"u/a-GGUF","gated":{}}}"#,
|
||||
st.gated.lock().unwrap()
|
||||
),
|
||||
)
|
||||
} else if path == "/api/models/u/verrou/tree/main" {
|
||||
(401, "{}".to_string())
|
||||
} else {
|
||||
(404, "{}".to_string())
|
||||
};
|
||||
let mut r = Response::new(Full::new(Bytes::from(body)));
|
||||
*r.status_mut() = StatusCode::from_u16(code).unwrap();
|
||||
r
|
||||
}
|
||||
|
||||
async fn fake_hf() -> (SocketAddr, Arc<FakeHf>) {
|
||||
let st = Arc::new(FakeHf::default());
|
||||
*st.gated.lock().unwrap() = Value::String("auto".into());
|
||||
let l = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
|
||||
let addr = l.local_addr().unwrap();
|
||||
let s2 = st.clone();
|
||||
tokio::spawn(async move {
|
||||
while let Ok((s, _)) = l.accept().await {
|
||||
let st = s2.clone();
|
||||
tokio::spawn(async move {
|
||||
let svc = service_fn(move |req| {
|
||||
let st = st.clone();
|
||||
async move { Ok::<_, Infallible>(hf_handle(st, req).await) }
|
||||
});
|
||||
let _ = hyper::server::conn::http1::Builder::new()
|
||||
.serve_connection(TokioIo::new(s), svc)
|
||||
.await;
|
||||
});
|
||||
}
|
||||
});
|
||||
(addr, st)
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn catalogue_contre_un_faux_hugging_face() {
|
||||
let (addr, st) = fake_hf().await;
|
||||
let base = format!("http://{addr}");
|
||||
let c = hf::HfClient::new(&base);
|
||||
let env = HfEnv {
|
||||
token: "hf_x".into(),
|
||||
version: "1.2.3".into(),
|
||||
};
|
||||
let repos = c.search(&env, "qwen").await.unwrap();
|
||||
assert_eq!(repos[1]["gated"], true);
|
||||
assert_eq!(repos[0]["id"], "u/a-GGUF");
|
||||
// Deuxième recherche identique : servie par le cache.
|
||||
c.search(&env, "qwen").await.unwrap();
|
||||
assert_eq!(st.log.lock().unwrap().len(), 1);
|
||||
|
||||
let l = c.files(&env, " /u/a-GGUF/ ").await.unwrap();
|
||||
assert!(l.gated);
|
||||
assert_eq!(l.models.len(), 1);
|
||||
assert_eq!(l.models[0].size, 5368709120, "taille LFS prise");
|
||||
assert_eq!(
|
||||
l.models[0].url,
|
||||
format!("{base}/u/a-GGUF/resolve/main/Q4/a-Q4_K_M.gguf")
|
||||
);
|
||||
assert_eq!(l.projectors[0].quant, "Q8_0");
|
||||
assert!(l.drafts.is_empty());
|
||||
assert!(st.auth.lock().unwrap().iter().all(|a| a == "Bearer hf_x"));
|
||||
|
||||
assert_eq!(
|
||||
c.files(&env, "u/verrou").await.unwrap_err(),
|
||||
"dépôt à accès restreint — renseigne HF_TOKEN pour y accéder"
|
||||
);
|
||||
assert_eq!(
|
||||
c.files(&env, "u/absent").await.unwrap_err(),
|
||||
"dépôt introuvable sur Hugging Face"
|
||||
);
|
||||
assert_eq!(
|
||||
c.files(&env, "../x").await.unwrap_err(),
|
||||
"nom de dépôt invalide (attendu « auteur/dépôt »)"
|
||||
);
|
||||
assert_eq!(c.whoami("1", "hf_bon_jeton").await.unwrap(), "moi");
|
||||
assert_eq!(
|
||||
c.whoami("1", "mauvais").await.unwrap_err(),
|
||||
"jeton refusé par Hugging Face (expiré, révoqué ou mal copié)"
|
||||
);
|
||||
// Vider le cache : la recherche repart vers Hugging Face.
|
||||
let n = st.log.lock().unwrap().len();
|
||||
c.clear_cache();
|
||||
c.search(&env, "qwen").await.unwrap();
|
||||
assert_eq!(st.log.lock().unwrap().len(), n + 1);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn routes_des_modeles_par_la_facade() {
|
||||
let (addr, st) = fake_hf().await;
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let home = tmp.path().canonicalize().unwrap();
|
||||
std::fs::create_dir_all(home.join("models")).unwrap();
|
||||
std::fs::write(home.join("models/a.gguf"), b"0123").unwrap();
|
||||
std::fs::create_dir_all(home.join("presets")).unwrap();
|
||||
std::fs::write(home.join("presets/p.env"), "NAME=P\nMODEL=a.gguf\n").unwrap();
|
||||
let ext = home.join("ext");
|
||||
std::fs::create_dir_all(&ext).unwrap();
|
||||
let (facade, go, _stop) = fakego::start_models(home.clone(), &format!("http://{addr}")).await;
|
||||
|
||||
let (code, _, b) = fakego::get(facade, "/api/models", &[]).await;
|
||||
assert_eq!(code, StatusCode::OK);
|
||||
let v: Value = serde_json::from_slice(&b).unwrap();
|
||||
assert_eq!(v[0]["name"], "a.gguf");
|
||||
assert_eq!(v[0]["used"], serde_json::json!(["P"]));
|
||||
|
||||
// Dossier ajouté : rangé dans la base du Go.
|
||||
let body = format!(r#"{{"path":"{}"}}"#, ext.display());
|
||||
let (code, _) = fakego::send(facade, "POST", "/api/models/dirs", &[], &body).await;
|
||||
assert_eq!(code, StatusCode::OK);
|
||||
let stored: Vec<String> =
|
||||
serde_json::from_str(&go.get("state", "model_dirs").unwrap()).unwrap();
|
||||
assert_eq!(stored, [ext.to_string_lossy().into_owned()]);
|
||||
let (_, _, b) = fakego::get(facade, "/api/models/dirs", &[]).await;
|
||||
let v: Value = serde_json::from_slice(&b).unwrap();
|
||||
assert_eq!(v["dirs"][1]["path"], ext.to_string_lossy().as_ref());
|
||||
assert_eq!(v["dirs"][1]["count"], 0);
|
||||
|
||||
// Utilisé par un preset : 409, puis force.
|
||||
let (code, b) = fakego::send(
|
||||
facade,
|
||||
"POST",
|
||||
"/api/models/delete",
|
||||
&[],
|
||||
r#"{"name":"a.gguf"}"#,
|
||||
)
|
||||
.await;
|
||||
assert_eq!(code, StatusCode::CONFLICT, "{b:?}");
|
||||
let (code, b) = fakego::send(
|
||||
facade,
|
||||
"POST",
|
||||
"/api/models/delete",
|
||||
&[],
|
||||
r#"{"name":"a.gguf","force":true}"#,
|
||||
)
|
||||
.await;
|
||||
assert_eq!(code, StatusCode::OK);
|
||||
assert_eq!(serde_json::from_slice::<Value>(&b).unwrap()["freed"], 4);
|
||||
assert!(!home.join("models/a.gguf").exists());
|
||||
|
||||
// Catalogue : matériel joint, jeton absent.
|
||||
let (code, _, b) = fakego::get(facade, "/api/hf/search?q=qwen+3", &[]).await;
|
||||
assert_eq!(code, StatusCode::OK);
|
||||
let v: Value = serde_json::from_slice(&b).unwrap();
|
||||
assert_eq!(v["hf_token"], false);
|
||||
assert!(v["hardware"]["os"].is_string());
|
||||
assert!(st.log.lock().unwrap()[0].contains("search=qwen+3&"));
|
||||
let (code, _, b) = fakego::get(facade, "/api/hf/search?q=%20", &[]).await;
|
||||
assert_eq!(code, StatusCode::BAD_REQUEST);
|
||||
assert_eq!(
|
||||
serde_json::from_slice::<Value>(&b).unwrap()["error"],
|
||||
"précise ce que tu cherches"
|
||||
);
|
||||
go.set("config", "CTX", "65536");
|
||||
let (code, _, b) = fakego::get(facade, "/api/hf/files?repo=u/a-GGUF", &[]).await;
|
||||
assert_eq!(code, StatusCode::OK);
|
||||
let v: Value = serde_json::from_slice(&b).unwrap();
|
||||
assert_eq!(
|
||||
(
|
||||
v["ctx"].as_i64(),
|
||||
v["vision"].as_bool(),
|
||||
v["gated"].as_bool()
|
||||
),
|
||||
(Some(65536), Some(true), Some(true))
|
||||
);
|
||||
assert_eq!(v["drafts"], Value::Null);
|
||||
assert!(v["models"][0].get("verdict").is_none() || v["models"][0]["verdict"].is_string());
|
||||
let (code, _, _) = fakego::get(facade, "/api/hf/files?repo=u/absent", &[]).await;
|
||||
assert_eq!(code, StatusCode::BAD_GATEWAY);
|
||||
|
||||
// Jeton : refusé, accepté (vérifié, enregistré, masqué), effacé.
|
||||
let (code, b) = fakego::send(
|
||||
facade,
|
||||
"POST",
|
||||
"/api/hf/token",
|
||||
&[],
|
||||
r#"{"token":"mauvais"}"#,
|
||||
)
|
||||
.await;
|
||||
assert_eq!(code, StatusCode::BAD_REQUEST, "{b:?}");
|
||||
let (code, b) = fakego::send(
|
||||
facade,
|
||||
"POST",
|
||||
"/api/hf/token",
|
||||
&[],
|
||||
r#"{"token":" hf_bon_jeton "}"#,
|
||||
)
|
||||
.await;
|
||||
assert_eq!(code, StatusCode::OK);
|
||||
let v: Value = serde_json::from_slice(&b).unwrap();
|
||||
assert_eq!(
|
||||
(
|
||||
v["set"].as_bool(),
|
||||
v["source"].as_str(),
|
||||
v["masked"].as_str(),
|
||||
v["account"].as_str()
|
||||
),
|
||||
(Some(true), Some("config"), Some("hf_…eton"), Some("moi"))
|
||||
);
|
||||
assert_eq!(go.get("state", "hf_token").as_deref(), Some("hf_bon_jeton"));
|
||||
let (_, _, b) = fakego::get(facade, "/api/hf/search?q=autre", &[]).await;
|
||||
assert_eq!(
|
||||
serde_json::from_slice::<Value>(&b).unwrap()["hf_token"],
|
||||
true
|
||||
);
|
||||
assert_eq!(
|
||||
st.auth.lock().unwrap().last().unwrap(),
|
||||
"Bearer hf_bon_jeton"
|
||||
);
|
||||
let (code, b) = fakego::send(facade, "POST", "/api/hf/token", &[], r#"{"token":""}"#).await;
|
||||
assert_eq!(code, StatusCode::OK);
|
||||
assert_eq!(serde_json::from_slice::<Value>(&b).unwrap()["set"], false);
|
||||
assert_eq!(go.get("state", "hf_token"), None);
|
||||
let (code, b) = fakego::send(facade, "POST", "/api/hf/token", &[], "x").await;
|
||||
assert_eq!(
|
||||
(
|
||||
code,
|
||||
serde_json::from_slice::<Value>(&b).unwrap()["error"].as_str()
|
||||
),
|
||||
(StatusCode::BAD_REQUEST, Some("requête illisible"))
|
||||
);
|
||||
|
||||
// Rien n'est parti au Go.
|
||||
let relayed = go.relayed.lock().unwrap().clone();
|
||||
assert!(
|
||||
!relayed
|
||||
.iter()
|
||||
.any(|p| p.starts_with("/api/models") || p.starts_with("/api/hf")),
|
||||
"{relayed:?}"
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn sans_la_variable_les_routes_restent_au_go() {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let (facade, _go, _stop) = fakego::start(tmp.path().to_path_buf()).await;
|
||||
for p in [
|
||||
"/api/models",
|
||||
"/api/models/dirs",
|
||||
"/api/hf/token",
|
||||
"/api/hf/search?q=x",
|
||||
] {
|
||||
let (_, _, b) = fakego::get(facade, p, &[]).await;
|
||||
assert!(b.starts_with(b"go:/api/"), "{p} : {b:?}");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,933 @@
|
||||
{
|
||||
"quant": {
|
||||
"Qwen3-8B-Q4_K_M.gguf": "Q4_K_M",
|
||||
"a-4bpw.gguf": "4BPW",
|
||||
"m-IQ4_XS-00001-of-00002.gguf": "IQ4_XS",
|
||||
"m-Q4.gguf": "Q4",
|
||||
"m-Q4_K_XL-Q5_K_M.gguf": "Q4_K_XL",
|
||||
"mmproj-F16.GGUF": "F16",
|
||||
"model-bf16.gguf": "BF16",
|
||||
"noext": "",
|
||||
"plain.gguf": "",
|
||||
"x.Q8_0.gguf": "Q8_0"
|
||||
},
|
||||
"split_args": [
|
||||
{
|
||||
"in": "",
|
||||
"out": []
|
||||
},
|
||||
{
|
||||
"in": "--mmproj /a/b.gguf -ngl 99",
|
||||
"out": [
|
||||
"--mmproj",
|
||||
"/a/b.gguf",
|
||||
"-ngl",
|
||||
"99"
|
||||
]
|
||||
},
|
||||
{
|
||||
"in": "--chat-template-file \"/etc/x y.jinja\" --x ''",
|
||||
"out": [
|
||||
"--chat-template-file",
|
||||
"/etc/x y.jinja",
|
||||
"--x",
|
||||
""
|
||||
]
|
||||
},
|
||||
{
|
||||
"in": "a\tb\nc",
|
||||
"out": [
|
||||
"a",
|
||||
"b",
|
||||
"c"
|
||||
]
|
||||
},
|
||||
{
|
||||
"in": "'un \"deux\"' trois",
|
||||
"out": [
|
||||
"un \"deux\"",
|
||||
"trois"
|
||||
]
|
||||
},
|
||||
{
|
||||
"in": "--mmproj",
|
||||
"out": [
|
||||
"--mmproj"
|
||||
]
|
||||
}
|
||||
],
|
||||
"mask": {
|
||||
"": "",
|
||||
" ": "",
|
||||
" hf_padded_token ": "hf_…oken",
|
||||
"abc": "…bc",
|
||||
"hf_12345678": "hf_…5678",
|
||||
"hf_abcdefghijklmnop": "hf_…mnop"
|
||||
},
|
||||
"gated": [
|
||||
{
|
||||
"in": true,
|
||||
"out": true
|
||||
},
|
||||
{
|
||||
"in": false,
|
||||
"out": false
|
||||
},
|
||||
{
|
||||
"in": "auto",
|
||||
"out": true
|
||||
},
|
||||
{
|
||||
"in": "manual",
|
||||
"out": true
|
||||
},
|
||||
{
|
||||
"in": "false",
|
||||
"out": false
|
||||
},
|
||||
{
|
||||
"in": "",
|
||||
"out": false
|
||||
},
|
||||
{
|
||||
"in": "None",
|
||||
"out": false
|
||||
},
|
||||
{
|
||||
"in": null,
|
||||
"out": false
|
||||
},
|
||||
{
|
||||
"in": 1,
|
||||
"out": false
|
||||
}
|
||||
],
|
||||
"classify": {
|
||||
"repo": "unsloth/Qwen3-8B-GGUF",
|
||||
"files": [
|
||||
{
|
||||
"path": "README.md",
|
||||
"size": 1000
|
||||
},
|
||||
{
|
||||
"path": "Qwen3-8B-Q8_0.gguf",
|
||||
"size": 8589934592
|
||||
},
|
||||
{
|
||||
"path": "Qwen3-8B-Q4_K_M.gguf",
|
||||
"size": 5368709120
|
||||
},
|
||||
{
|
||||
"path": "mmproj-F16.gguf",
|
||||
"size": 943718400
|
||||
},
|
||||
{
|
||||
"path": "mmproj-Q8_0.gguf",
|
||||
"size": 629145600
|
||||
},
|
||||
{
|
||||
"path": "mtp-Qwen3-8B-Q8_0.gguf",
|
||||
"size": 314572800
|
||||
},
|
||||
{
|
||||
"path": "BF16/Qwen3-8B-BF16-00001-of-00002.gguf",
|
||||
"size": 9663676416
|
||||
},
|
||||
{
|
||||
"path": "BF16/Qwen3-8B-BF16-00002-of-00002.gguf",
|
||||
"size": 7516192768
|
||||
},
|
||||
{
|
||||
"path": "Q2/Qwen3-8B-BF16-00001-of-00002.gguf",
|
||||
"size": 1073741824
|
||||
},
|
||||
{
|
||||
"path": "dossier avec espace/Qwen3 8B Q3.gguf",
|
||||
"size": 4294967296
|
||||
}
|
||||
],
|
||||
"out": {
|
||||
"repo": "unsloth/Qwen3-8B-GGUF",
|
||||
"gated": false,
|
||||
"models": [
|
||||
{
|
||||
"name": "Qwen3-8B-BF16-00001-of-00002.gguf",
|
||||
"url": "https://huggingface.co/unsloth/Qwen3-8B-GGUF/resolve/main/Q2/Qwen3-8B-BF16-00001-of-00002.gguf",
|
||||
"size": 1073741824,
|
||||
"quant": "BF16",
|
||||
"shards": 2
|
||||
},
|
||||
{
|
||||
"name": "Qwen3 8B Q3.gguf",
|
||||
"url": "https://huggingface.co/unsloth/Qwen3-8B-GGUF/resolve/main/dossier%20avec%20espace/Qwen3%208B%20Q3.gguf",
|
||||
"size": 4294967296,
|
||||
"quant": "",
|
||||
"shards": 1
|
||||
},
|
||||
{
|
||||
"name": "Qwen3-8B-Q4_K_M.gguf",
|
||||
"url": "https://huggingface.co/unsloth/Qwen3-8B-GGUF/resolve/main/Qwen3-8B-Q4_K_M.gguf",
|
||||
"size": 5368709120,
|
||||
"quant": "Q4_K_M",
|
||||
"shards": 1
|
||||
},
|
||||
{
|
||||
"name": "Qwen3-8B-Q8_0.gguf",
|
||||
"url": "https://huggingface.co/unsloth/Qwen3-8B-GGUF/resolve/main/Qwen3-8B-Q8_0.gguf",
|
||||
"size": 8589934592,
|
||||
"quant": "Q8_0",
|
||||
"shards": 1
|
||||
},
|
||||
{
|
||||
"name": "Qwen3-8B-BF16-00001-of-00002.gguf",
|
||||
"url": "https://huggingface.co/unsloth/Qwen3-8B-GGUF/resolve/main/BF16/Qwen3-8B-BF16-00001-of-00002.gguf",
|
||||
"size": 17179869184,
|
||||
"quant": "BF16",
|
||||
"shards": 2
|
||||
}
|
||||
],
|
||||
"projectors": [
|
||||
{
|
||||
"name": "mmproj-Q8_0.gguf",
|
||||
"url": "https://huggingface.co/unsloth/Qwen3-8B-GGUF/resolve/main/mmproj-Q8_0.gguf",
|
||||
"size": 629145600,
|
||||
"quant": "Q8_0",
|
||||
"shards": 1
|
||||
},
|
||||
{
|
||||
"name": "mmproj-F16.gguf",
|
||||
"url": "https://huggingface.co/unsloth/Qwen3-8B-GGUF/resolve/main/mmproj-F16.gguf",
|
||||
"size": 943718400,
|
||||
"quant": "F16",
|
||||
"shards": 1
|
||||
}
|
||||
],
|
||||
"drafts": [
|
||||
{
|
||||
"name": "mtp-Qwen3-8B-Q8_0.gguf",
|
||||
"url": "https://huggingface.co/unsloth/Qwen3-8B-GGUF/resolve/main/mtp-Qwen3-8B-Q8_0.gguf",
|
||||
"size": 314572800,
|
||||
"quant": "Q8_0",
|
||||
"shards": 1
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"projector": [
|
||||
{
|
||||
"in": null,
|
||||
"name": ""
|
||||
},
|
||||
{
|
||||
"in": [
|
||||
{
|
||||
"name": "mmproj-F16.gguf",
|
||||
"url": "",
|
||||
"size": 900,
|
||||
"quant": "F16",
|
||||
"shards": 0
|
||||
},
|
||||
{
|
||||
"name": "mmproj-BF16.gguf",
|
||||
"url": "",
|
||||
"size": 800,
|
||||
"quant": "BF16",
|
||||
"shards": 0
|
||||
}
|
||||
],
|
||||
"name": "mmproj-BF16.gguf"
|
||||
},
|
||||
{
|
||||
"in": [
|
||||
{
|
||||
"name": "mmproj-F16.gguf",
|
||||
"url": "",
|
||||
"size": 900,
|
||||
"quant": "F16",
|
||||
"shards": 0
|
||||
},
|
||||
{
|
||||
"name": "mmproj-q8_0.gguf",
|
||||
"url": "",
|
||||
"size": 950,
|
||||
"quant": "q8_0",
|
||||
"shards": 0
|
||||
}
|
||||
],
|
||||
"name": "mmproj-q8_0.gguf"
|
||||
}
|
||||
],
|
||||
"verdicts": [
|
||||
{
|
||||
"hardware": {
|
||||
"os": "linux",
|
||||
"arch": "amd64",
|
||||
"ram_gb": 64,
|
||||
"vram_gb": 24,
|
||||
"gpus": [
|
||||
{
|
||||
"name": "RTX",
|
||||
"vram_gb": 24
|
||||
}
|
||||
]
|
||||
},
|
||||
"weights": 5368709120,
|
||||
"mmproj": 0,
|
||||
"ctx": 32768,
|
||||
"verdict": "ok",
|
||||
"why": "5.0 Go de poids + ~0.2 Go de contexte (estimation) = ~5.2 Go pour 24.0 Go de VRAM"
|
||||
},
|
||||
{
|
||||
"hardware": {
|
||||
"os": "linux",
|
||||
"arch": "amd64",
|
||||
"ram_gb": 64,
|
||||
"vram_gb": 24,
|
||||
"gpus": [
|
||||
{
|
||||
"name": "RTX",
|
||||
"vram_gb": 24
|
||||
}
|
||||
]
|
||||
},
|
||||
"weights": 21474836480,
|
||||
"mmproj": 629145600,
|
||||
"ctx": 32768,
|
||||
"verdict": "ok",
|
||||
"why": "20.0 Go de poids + 0.6 Go de projecteur + ~1.0 Go de contexte (estimation) = ~21.6 Go pour 24.0 Go de VRAM"
|
||||
},
|
||||
{
|
||||
"hardware": {
|
||||
"os": "linux",
|
||||
"arch": "amd64",
|
||||
"ram_gb": 64,
|
||||
"vram_gb": 24,
|
||||
"gpus": [
|
||||
{
|
||||
"name": "RTX",
|
||||
"vram_gb": 24
|
||||
}
|
||||
]
|
||||
},
|
||||
"weights": 23622320128,
|
||||
"mmproj": 0,
|
||||
"ctx": 32768,
|
||||
"verdict": "juste",
|
||||
"why": "ça passe de justesse : 22.0 Go de poids + ~1.1 Go de contexte (estimation) = ~23.1 Go pour 24.0 Go de VRAM"
|
||||
},
|
||||
{
|
||||
"hardware": {
|
||||
"os": "linux",
|
||||
"arch": "amd64",
|
||||
"ram_gb": 64,
|
||||
"vram_gb": 24,
|
||||
"gpus": [
|
||||
{
|
||||
"name": "RTX",
|
||||
"vram_gb": 24
|
||||
}
|
||||
]
|
||||
},
|
||||
"weights": 32212254720,
|
||||
"mmproj": 0,
|
||||
"ctx": 0,
|
||||
"verdict": "trop",
|
||||
"why": "ne tient pas : 30.0 Go de poids + ~1.5 Go de contexte (estimation) = ~31.5 Go pour 24.0 Go de VRAM"
|
||||
},
|
||||
{
|
||||
"hardware": {
|
||||
"os": "linux",
|
||||
"arch": "amd64",
|
||||
"ram_gb": 16,
|
||||
"vram_gb": 0,
|
||||
"gpus": null
|
||||
},
|
||||
"weights": 8589934592,
|
||||
"mmproj": 0,
|
||||
"ctx": 131072,
|
||||
"verdict": "ok",
|
||||
"why": "8.0 Go de poids + ~1.6 Go de contexte (estimation) = ~9.6 Go pour 16.0 Go de RAM"
|
||||
},
|
||||
{
|
||||
"hardware": {
|
||||
"os": "linux",
|
||||
"arch": "amd64",
|
||||
"ram_gb": 0,
|
||||
"vram_gb": 0,
|
||||
"gpus": null
|
||||
},
|
||||
"weights": 1073741824,
|
||||
"mmproj": 0,
|
||||
"ctx": 4096,
|
||||
"verdict": "",
|
||||
"why": ""
|
||||
}
|
||||
],
|
||||
"scenario": {
|
||||
"files": {
|
||||
"disque/a-Q4_K_M.gguf": 100,
|
||||
"disque/ext.GGUF": 400,
|
||||
"models/a-Q4_K_M.gguf": 100,
|
||||
"models/actif.gguf": 50,
|
||||
"models/b.gguf": 200,
|
||||
"models/m-00001-of-00002.gguf": 10,
|
||||
"models/m-00002-of-00002.gguf": 20,
|
||||
"models/notes.txt": 3,
|
||||
"models/sous-dossier.gguf": -1,
|
||||
"models/t-00001-of-00003.gguf": 5,
|
||||
"models/t-00003-of-00003.gguf": 7,
|
||||
"models/vision-mmproj.gguf": 30
|
||||
},
|
||||
"extra": [
|
||||
"{{HOME}}/disque",
|
||||
"{{HOME}}/absent"
|
||||
],
|
||||
"presets": {
|
||||
"alpha": "NAME=Alpha\nMODEL=a-Q4_K_M.gguf\nMMPROJ=vision-mmproj.gguf\n",
|
||||
"beta": "NAME=Beta\nMODEL={{HOME}}/disque/ext.GGUF\n",
|
||||
"delta": "NAME=Delta\nMODEL=m-00001-of-00002.gguf\n",
|
||||
"gamma": "NAME=Gamma\nMODEL=b.gguf\nEXTRA_ARGS=--mmproj \"vision-mmproj.gguf\" -ngl 99 --mmproj b.gguf\n"
|
||||
},
|
||||
"config": {
|
||||
"MODEL": "actif.gguf",
|
||||
"MODEL_DRAFT": "{{HOME}}/disque/a-Q4_K_M.gguf"
|
||||
},
|
||||
"calls": [
|
||||
{
|
||||
"method": "GET",
|
||||
"path": "/api/models",
|
||||
"status": 200,
|
||||
"out": [
|
||||
{
|
||||
"active": false,
|
||||
"dir": "{{HOME}}/models",
|
||||
"home": true,
|
||||
"name": "a-Q4_K_M.gguf",
|
||||
"path": "{{HOME}}/models/a-Q4_K_M.gguf",
|
||||
"size": 100,
|
||||
"used": [
|
||||
"Alpha"
|
||||
],
|
||||
"value": "a-Q4_K_M.gguf"
|
||||
},
|
||||
{
|
||||
"active": true,
|
||||
"dir": "{{HOME}}/models",
|
||||
"home": true,
|
||||
"name": "actif.gguf",
|
||||
"path": "{{HOME}}/models/actif.gguf",
|
||||
"size": 50,
|
||||
"value": "actif.gguf"
|
||||
},
|
||||
{
|
||||
"active": false,
|
||||
"dir": "{{HOME}}/models",
|
||||
"home": true,
|
||||
"name": "b.gguf",
|
||||
"path": "{{HOME}}/models/b.gguf",
|
||||
"size": 200,
|
||||
"used": [
|
||||
"Gamma"
|
||||
],
|
||||
"value": "b.gguf"
|
||||
},
|
||||
{
|
||||
"active": false,
|
||||
"dir": "{{HOME}}/models",
|
||||
"home": true,
|
||||
"name": "m-00001-of-00002.gguf",
|
||||
"path": "{{HOME}}/models/m-00001-of-00002.gguf",
|
||||
"shards": 2,
|
||||
"size": 30,
|
||||
"used": [
|
||||
"Delta"
|
||||
],
|
||||
"value": "m-00001-of-00002.gguf"
|
||||
},
|
||||
{
|
||||
"dir": "{{HOME}}/models",
|
||||
"home": true,
|
||||
"missing": [
|
||||
"t-00002-of-00003.gguf"
|
||||
],
|
||||
"name": "t-00001-of-00003.gguf",
|
||||
"path": "{{HOME}}/models/t-00001-of-00003.gguf",
|
||||
"shards": 3,
|
||||
"size": 12,
|
||||
"value": "t-00001-of-00003.gguf"
|
||||
},
|
||||
{
|
||||
"active": false,
|
||||
"dir": "{{HOME}}/models",
|
||||
"home": true,
|
||||
"name": "vision-mmproj.gguf",
|
||||
"path": "{{HOME}}/models/vision-mmproj.gguf",
|
||||
"size": 30,
|
||||
"used": [
|
||||
"Alpha",
|
||||
"Gamma"
|
||||
],
|
||||
"value": "vision-mmproj.gguf"
|
||||
},
|
||||
{
|
||||
"active": true,
|
||||
"dir": "{{HOME}}/disque",
|
||||
"home": false,
|
||||
"name": "a-Q4_K_M.gguf",
|
||||
"path": "{{HOME}}/disque/a-Q4_K_M.gguf",
|
||||
"size": 100,
|
||||
"value": "{{HOME}}/disque/a-Q4_K_M.gguf"
|
||||
},
|
||||
{
|
||||
"active": false,
|
||||
"dir": "{{HOME}}/disque",
|
||||
"home": false,
|
||||
"name": "ext.GGUF",
|
||||
"path": "{{HOME}}/disque/ext.GGUF",
|
||||
"size": 400,
|
||||
"used": [
|
||||
"Beta"
|
||||
],
|
||||
"value": "{{HOME}}/disque/ext.GGUF"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"method": "GET",
|
||||
"path": "/api/models/dirs",
|
||||
"status": 200,
|
||||
"out": {
|
||||
"dirs": [
|
||||
{
|
||||
"count": 6,
|
||||
"exists": true,
|
||||
"free": "…",
|
||||
"free_exact": "…",
|
||||
"home": true,
|
||||
"path": "{{HOME}}/models"
|
||||
},
|
||||
{
|
||||
"count": 2,
|
||||
"exists": true,
|
||||
"free": "…",
|
||||
"free_exact": "…",
|
||||
"home": false,
|
||||
"path": "{{HOME}}/disque"
|
||||
},
|
||||
{
|
||||
"count": -1,
|
||||
"exists": false,
|
||||
"free": "…",
|
||||
"free_exact": "…",
|
||||
"home": false,
|
||||
"path": "{{HOME}}/absent"
|
||||
}
|
||||
],
|
||||
"env": false,
|
||||
"ok": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"method": "POST",
|
||||
"path": "/api/models/delete",
|
||||
"body": "{\"name\":\"actif.gguf\"}",
|
||||
"status": 409,
|
||||
"out": {
|
||||
"active": true,
|
||||
"error": "modèle en service — bascule sur un autre preset avant de le supprimer",
|
||||
"ok": false
|
||||
},
|
||||
"files": [
|
||||
"disque/a-Q4_K_M.gguf",
|
||||
"disque/ext.GGUF",
|
||||
"models/a-Q4_K_M.gguf",
|
||||
"models/actif.gguf",
|
||||
"models/b.gguf",
|
||||
"models/m-00001-of-00002.gguf",
|
||||
"models/m-00002-of-00002.gguf",
|
||||
"models/notes.txt",
|
||||
"models/t-00001-of-00003.gguf",
|
||||
"models/t-00003-of-00003.gguf",
|
||||
"models/vision-mmproj.gguf"
|
||||
]
|
||||
},
|
||||
{
|
||||
"method": "POST",
|
||||
"path": "/api/models/delete",
|
||||
"body": "{\"name\":\"a-Q4_K_M.gguf\"}",
|
||||
"status": 409,
|
||||
"out": {
|
||||
"error": "modèle utilisé par : Alpha",
|
||||
"ok": false,
|
||||
"used": [
|
||||
"Alpha"
|
||||
]
|
||||
},
|
||||
"files": [
|
||||
"disque/a-Q4_K_M.gguf",
|
||||
"disque/ext.GGUF",
|
||||
"models/a-Q4_K_M.gguf",
|
||||
"models/actif.gguf",
|
||||
"models/b.gguf",
|
||||
"models/m-00001-of-00002.gguf",
|
||||
"models/m-00002-of-00002.gguf",
|
||||
"models/notes.txt",
|
||||
"models/t-00001-of-00003.gguf",
|
||||
"models/t-00003-of-00003.gguf",
|
||||
"models/vision-mmproj.gguf"
|
||||
]
|
||||
},
|
||||
{
|
||||
"method": "POST",
|
||||
"path": "/api/models/delete",
|
||||
"body": "{\"name\":\"{{HOME}}/disque/a-Q4_K_M.gguf\"}",
|
||||
"status": 409,
|
||||
"out": {
|
||||
"active": true,
|
||||
"error": "modèle en service — bascule sur un autre preset avant de le supprimer",
|
||||
"ok": false
|
||||
},
|
||||
"files": [
|
||||
"disque/a-Q4_K_M.gguf",
|
||||
"disque/ext.GGUF",
|
||||
"models/a-Q4_K_M.gguf",
|
||||
"models/actif.gguf",
|
||||
"models/b.gguf",
|
||||
"models/m-00001-of-00002.gguf",
|
||||
"models/m-00002-of-00002.gguf",
|
||||
"models/notes.txt",
|
||||
"models/t-00001-of-00003.gguf",
|
||||
"models/t-00003-of-00003.gguf",
|
||||
"models/vision-mmproj.gguf"
|
||||
]
|
||||
},
|
||||
{
|
||||
"method": "POST",
|
||||
"path": "/api/models/delete",
|
||||
"body": "{\"name\":\"b.gguf\",\"force\":true}",
|
||||
"status": 200,
|
||||
"out": {
|
||||
"freed": 200,
|
||||
"ok": true,
|
||||
"used": [
|
||||
"Gamma"
|
||||
]
|
||||
},
|
||||
"files": [
|
||||
"disque/a-Q4_K_M.gguf",
|
||||
"disque/ext.GGUF",
|
||||
"models/a-Q4_K_M.gguf",
|
||||
"models/actif.gguf",
|
||||
"models/m-00001-of-00002.gguf",
|
||||
"models/m-00002-of-00002.gguf",
|
||||
"models/notes.txt",
|
||||
"models/t-00001-of-00003.gguf",
|
||||
"models/t-00003-of-00003.gguf",
|
||||
"models/vision-mmproj.gguf"
|
||||
]
|
||||
},
|
||||
{
|
||||
"method": "POST",
|
||||
"path": "/api/models/delete",
|
||||
"body": "{\"name\":\"m-00001-of-00002.gguf\",\"force\":true}",
|
||||
"status": 200,
|
||||
"out": {
|
||||
"freed": 30,
|
||||
"ok": true,
|
||||
"used": [
|
||||
"Delta"
|
||||
]
|
||||
},
|
||||
"files": [
|
||||
"disque/a-Q4_K_M.gguf",
|
||||
"disque/ext.GGUF",
|
||||
"models/a-Q4_K_M.gguf",
|
||||
"models/actif.gguf",
|
||||
"models/notes.txt",
|
||||
"models/t-00001-of-00003.gguf",
|
||||
"models/t-00003-of-00003.gguf",
|
||||
"models/vision-mmproj.gguf"
|
||||
]
|
||||
},
|
||||
{
|
||||
"method": "POST",
|
||||
"path": "/api/models/delete",
|
||||
"body": "{\"name\":\"t-00001-of-00003.gguf\"}",
|
||||
"status": 200,
|
||||
"out": {
|
||||
"freed": 12,
|
||||
"ok": true,
|
||||
"used": null
|
||||
},
|
||||
"files": [
|
||||
"disque/a-Q4_K_M.gguf",
|
||||
"disque/ext.GGUF",
|
||||
"models/a-Q4_K_M.gguf",
|
||||
"models/actif.gguf",
|
||||
"models/notes.txt",
|
||||
"models/vision-mmproj.gguf"
|
||||
]
|
||||
},
|
||||
{
|
||||
"method": "POST",
|
||||
"path": "/api/models/delete",
|
||||
"body": "{\"name\":\"inconnu.gguf\"}",
|
||||
"status": 400,
|
||||
"out": {
|
||||
"error": "modèle introuvable: inconnu.gguf",
|
||||
"ok": false
|
||||
},
|
||||
"files": [
|
||||
"disque/a-Q4_K_M.gguf",
|
||||
"disque/ext.GGUF",
|
||||
"models/a-Q4_K_M.gguf",
|
||||
"models/actif.gguf",
|
||||
"models/notes.txt",
|
||||
"models/vision-mmproj.gguf"
|
||||
]
|
||||
},
|
||||
{
|
||||
"method": "POST",
|
||||
"path": "/api/models/delete",
|
||||
"body": "{\"name\":\"/etc/passwd.gguf\"}",
|
||||
"status": 400,
|
||||
"out": {
|
||||
"error": "dossier non autorisé : /etc — ajoute-le dans « Dossiers de modèles »",
|
||||
"ok": false
|
||||
},
|
||||
"files": [
|
||||
"disque/a-Q4_K_M.gguf",
|
||||
"disque/ext.GGUF",
|
||||
"models/a-Q4_K_M.gguf",
|
||||
"models/actif.gguf",
|
||||
"models/notes.txt",
|
||||
"models/vision-mmproj.gguf"
|
||||
]
|
||||
},
|
||||
{
|
||||
"method": "POST",
|
||||
"path": "/api/models/delete",
|
||||
"body": "{\"name\":\"notes.txt\"}",
|
||||
"status": 400,
|
||||
"out": {
|
||||
"error": "le modèle doit être un fichier .gguf",
|
||||
"ok": false
|
||||
},
|
||||
"files": [
|
||||
"disque/a-Q4_K_M.gguf",
|
||||
"disque/ext.GGUF",
|
||||
"models/a-Q4_K_M.gguf",
|
||||
"models/actif.gguf",
|
||||
"models/notes.txt",
|
||||
"models/vision-mmproj.gguf"
|
||||
]
|
||||
},
|
||||
{
|
||||
"method": "POST",
|
||||
"path": "/api/models/delete",
|
||||
"body": "{\"name\":\"\"}",
|
||||
"status": 400,
|
||||
"out": {
|
||||
"error": "nom de modèle invalide",
|
||||
"ok": false
|
||||
},
|
||||
"files": [
|
||||
"disque/a-Q4_K_M.gguf",
|
||||
"disque/ext.GGUF",
|
||||
"models/a-Q4_K_M.gguf",
|
||||
"models/actif.gguf",
|
||||
"models/notes.txt",
|
||||
"models/vision-mmproj.gguf"
|
||||
]
|
||||
},
|
||||
{
|
||||
"method": "POST",
|
||||
"path": "/api/models/dirs",
|
||||
"body": "{\"path\":\"\"}",
|
||||
"status": 400,
|
||||
"out": {
|
||||
"error": "chemin vide",
|
||||
"ok": false
|
||||
},
|
||||
"stored": [
|
||||
"{{HOME}}/disque",
|
||||
"{{HOME}}/absent"
|
||||
]
|
||||
},
|
||||
{
|
||||
"method": "POST",
|
||||
"path": "/api/models/dirs",
|
||||
"body": "{\"path\":\"{{HOME}}/nulle-part\"}",
|
||||
"status": 400,
|
||||
"out": {
|
||||
"error": "dossier introuvable : {{HOME}}/nulle-part",
|
||||
"ok": false
|
||||
},
|
||||
"stored": [
|
||||
"{{HOME}}/disque",
|
||||
"{{HOME}}/absent"
|
||||
]
|
||||
},
|
||||
{
|
||||
"method": "POST",
|
||||
"path": "/api/models/dirs",
|
||||
"body": "{\"path\":\"{{HOME}}/models/sous-dossier.gguf\"}",
|
||||
"status": 200,
|
||||
"out": {
|
||||
"ok": true
|
||||
},
|
||||
"stored": [
|
||||
"{{HOME}}/disque",
|
||||
"{{HOME}}/absent",
|
||||
"{{HOME}}/models/sous-dossier.gguf"
|
||||
]
|
||||
},
|
||||
{
|
||||
"method": "POST",
|
||||
"path": "/api/models/dirs",
|
||||
"body": "{\"path\":\"{{HOME}}/disque/\",\"action\":\"remove\"}",
|
||||
"status": 200,
|
||||
"out": {
|
||||
"ok": true
|
||||
},
|
||||
"stored": [
|
||||
"{{HOME}}/absent",
|
||||
"{{HOME}}/models/sous-dossier.gguf"
|
||||
]
|
||||
},
|
||||
{
|
||||
"method": "POST",
|
||||
"path": "/api/models/dirs",
|
||||
"body": "{\"path\":\"{{HOME}}/disque/../disque\",\"action\":\"add\"}",
|
||||
"status": 200,
|
||||
"out": {
|
||||
"ok": true
|
||||
},
|
||||
"stored": [
|
||||
"{{HOME}}/absent",
|
||||
"{{HOME}}/models/sous-dossier.gguf",
|
||||
"{{HOME}}/disque"
|
||||
]
|
||||
},
|
||||
{
|
||||
"method": "POST",
|
||||
"path": "/api/models/dirs",
|
||||
"body": "{\"path\":\"{{HOME}}/models\",\"action\":\"add\"}",
|
||||
"status": 200,
|
||||
"out": {
|
||||
"ok": true
|
||||
},
|
||||
"stored": [
|
||||
"{{HOME}}/absent",
|
||||
"{{HOME}}/models/sous-dossier.gguf",
|
||||
"{{HOME}}/disque"
|
||||
]
|
||||
},
|
||||
{
|
||||
"method": "GET",
|
||||
"path": "/api/models",
|
||||
"status": 200,
|
||||
"out": [
|
||||
{
|
||||
"active": false,
|
||||
"dir": "{{HOME}}/models",
|
||||
"home": true,
|
||||
"name": "a-Q4_K_M.gguf",
|
||||
"path": "{{HOME}}/models/a-Q4_K_M.gguf",
|
||||
"size": 100,
|
||||
"used": [
|
||||
"Alpha"
|
||||
],
|
||||
"value": "a-Q4_K_M.gguf"
|
||||
},
|
||||
{
|
||||
"active": true,
|
||||
"dir": "{{HOME}}/models",
|
||||
"home": true,
|
||||
"name": "actif.gguf",
|
||||
"path": "{{HOME}}/models/actif.gguf",
|
||||
"size": 50,
|
||||
"value": "actif.gguf"
|
||||
},
|
||||
{
|
||||
"active": false,
|
||||
"dir": "{{HOME}}/models",
|
||||
"home": true,
|
||||
"name": "vision-mmproj.gguf",
|
||||
"path": "{{HOME}}/models/vision-mmproj.gguf",
|
||||
"size": 30,
|
||||
"used": [
|
||||
"Alpha",
|
||||
"Gamma"
|
||||
],
|
||||
"value": "vision-mmproj.gguf"
|
||||
},
|
||||
{
|
||||
"active": true,
|
||||
"dir": "{{HOME}}/disque",
|
||||
"home": false,
|
||||
"name": "a-Q4_K_M.gguf",
|
||||
"path": "{{HOME}}/disque/a-Q4_K_M.gguf",
|
||||
"size": 100,
|
||||
"value": "{{HOME}}/disque/a-Q4_K_M.gguf"
|
||||
},
|
||||
{
|
||||
"active": false,
|
||||
"dir": "{{HOME}}/disque",
|
||||
"home": false,
|
||||
"name": "ext.GGUF",
|
||||
"path": "{{HOME}}/disque/ext.GGUF",
|
||||
"size": 400,
|
||||
"used": [
|
||||
"Beta"
|
||||
],
|
||||
"value": "{{HOME}}/disque/ext.GGUF"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"method": "GET",
|
||||
"path": "/api/models/dirs",
|
||||
"status": 200,
|
||||
"out": {
|
||||
"dirs": [
|
||||
{
|
||||
"count": 3,
|
||||
"exists": true,
|
||||
"free": "…",
|
||||
"free_exact": "…",
|
||||
"home": true,
|
||||
"path": "{{HOME}}/models"
|
||||
},
|
||||
{
|
||||
"count": -1,
|
||||
"exists": false,
|
||||
"free": "…",
|
||||
"free_exact": "…",
|
||||
"home": false,
|
||||
"path": "{{HOME}}/absent"
|
||||
},
|
||||
{
|
||||
"count": 0,
|
||||
"exists": true,
|
||||
"free": "…",
|
||||
"free_exact": "…",
|
||||
"home": false,
|
||||
"path": "{{HOME}}/models/sous-dossier.gguf"
|
||||
},
|
||||
{
|
||||
"count": 2,
|
||||
"exists": true,
|
||||
"free": "…",
|
||||
"free_exact": "…",
|
||||
"home": false,
|
||||
"path": "{{HOME}}/disque"
|
||||
}
|
||||
],
|
||||
"env": false,
|
||||
"ok": true
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -100,7 +100,7 @@ async fn facade(backend: SocketAddr) -> (SocketAddr, oneshot::Sender<()>) {
|
||||
service: "loki-engine".into(),
|
||||
},
|
||||
chat: None,
|
||||
downloads: None,
|
||||
models: None,
|
||||
};
|
||||
tokio::spawn(loki_rs::proxy::serve(l, cfg, async move {
|
||||
let _ = rx.await;
|
||||
|
||||
Reference in new issue
Block a user