mirror of
https://github.com/R0m1k3/Loki.git
synced 2026-10-17 12:16:21 +02:00
Lot 8 : téléchargements de modèles tenus par la façade (LOKI_DOWNLOADS=rust)
La façade Rust mène les téléchargements de .gguf quand LOKI_DOWNLOADS=rust (expérimental ; le Go reste le défaut) : sonde, lancement, suivi, annulation, avec les mêmes contrats et la même mécanique que le Go (tranches rapatriées en entier, plages d'octets parallèles, reprise, place disque vérifiée sur le total, jeton réservé aux hôtes Hugging Face, refus traduits, .part vidés au démarrage). Côté Go : facade_downloads.go remet ces routes à la façade quand elles arrivent par le tunnel du relais, et le Go ne vide plus les .part qu'elle mène. Parité : journal des fonctions pures tiré du Go (parity_dl_test.go) et rejoué en Rust ; téléchargements de bout en bout contre un faux Hugging Face (tests/downloads.rs). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016tcGfJWefW5kUGoC2wL8cv
This commit is contained in:
20 files changed
+2820
-17
No files matched your search
Generated
+1
@@ -686,6 +686,7 @@ dependencies = [
|
||||
"tempfile",
|
||||
"tokio",
|
||||
"tokio-tungstenite",
|
||||
"url",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
||||
@@ -13,6 +13,7 @@ http-body-util = "0.1.5"
|
||||
hyper = { version = "1.12.0", features = ["http1", "server", "client"] }
|
||||
hyper-util = { version = "0.1.21", features = ["tokio", "server", "client-legacy", "http1"] }
|
||||
regex = "1.13.1"
|
||||
url = "2.5.8"
|
||||
reqwest = { version = "0.13.5", default-features = false, features = ["rustls-no-provider", "stream", "http2"] }
|
||||
rustls = { version = "0.23.46", default-features = false, features = ["ring", "std", "tls12"], optional = true }
|
||||
serde = { version = "1.0.229", features = ["derive"] }
|
||||
|
||||
@@ -26,6 +26,9 @@ pub struct Ctx {
|
||||
pub engine: engine::Paths,
|
||||
/// Conversation tenue par la façade (`LOKI_CHAT=rust`) ; `None` : au Go.
|
||||
pub chat: Option<std::sync::Arc<crate::chat::generate::ChatService>>,
|
||||
/// Téléchargements de modèles tenus par la façade (`LOKI_DOWNLOADS=rust`) ;
|
||||
/// `None` : au Go.
|
||||
pub downloads: Option<std::sync::Arc<crate::downloads::Downloads>>,
|
||||
/// Jeton de l'API interne : une requête qui le porte vient du Go.
|
||||
pub internal_token: String,
|
||||
}
|
||||
|
||||
@@ -208,8 +208,9 @@ impl EngineHttp {
|
||||
}
|
||||
}
|
||||
|
||||
/// Fournisseur cryptographique de rustls (ring), posé une fois.
|
||||
fn install_crypto() {
|
||||
/// Fournisseur cryptographique de rustls (ring), posé une fois (tout client
|
||||
/// reqwest de la façade le demande avant d'être construit).
|
||||
pub fn install_crypto() {
|
||||
#[cfg(feature = "tls")]
|
||||
let _ = rustls::crypto::ring::default_provider().install_default();
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large.
Load diff
@@ -15,6 +15,7 @@ pub mod auth;
|
||||
pub mod chat;
|
||||
pub mod chatapi;
|
||||
pub mod child;
|
||||
pub mod downloads;
|
||||
pub mod engine;
|
||||
pub mod engstatus;
|
||||
pub mod gguf;
|
||||
|
||||
@@ -118,9 +118,17 @@ fn serve() -> ExitCode {
|
||||
// sait (il remet alors à la façade le trafic de chat du tunnel) et
|
||||
// connaît son adresse locale.
|
||||
let chat_rust = chat_held_by_facade();
|
||||
// LOKI_DOWNLOADS=rust : les téléchargements de modèles aussi (le Go
|
||||
// remet alors leurs routes du tunnel et ne vide plus les .part).
|
||||
let downloads_rust = downloads_held_by_facade();
|
||||
let mut env = vec![(auth::INTERNAL_ENV.to_string(), token.clone())];
|
||||
if chat_rust {
|
||||
env.push(("LOKI_CHAT".into(), "rust".into()));
|
||||
}
|
||||
if downloads_rust {
|
||||
env.push(("LOKI_DOWNLOADS".into(), "rust".into()));
|
||||
}
|
||||
if chat_rust || downloads_rust {
|
||||
env.push(("LOKI_FACADE_ADDR".into(), format!("127.0.0.1:{web_port}")));
|
||||
}
|
||||
let sup = child::Supervisor::spawn(go_bin, go_port, env);
|
||||
@@ -153,6 +161,11 @@ fn serve() -> ExitCode {
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let downloads = if downloads_rust {
|
||||
Some(start_downloads(backend, &token, &engine_paths.home).await)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let addr = SocketAddr::from(([0, 0, 0, 0], web_port));
|
||||
let listener = match tokio::net::TcpListener::bind(addr).await {
|
||||
@@ -170,6 +183,7 @@ fn serve() -> ExitCode {
|
||||
host_env: auth::HostEnv::from_env(),
|
||||
engine: engine_paths,
|
||||
chat,
|
||||
downloads,
|
||||
};
|
||||
let res = proxy::serve(listener, cfg, shutdown_signal()).await;
|
||||
eprintln!("[loki-rs] arrêt demandé — arrêt du serveur Go");
|
||||
@@ -189,6 +203,37 @@ fn chat_held_by_facade() -> bool {
|
||||
std::env::var("LOKI_CHAT").is_ok_and(|v| v.trim().eq_ignore_ascii_case("rust"))
|
||||
}
|
||||
|
||||
/// `LOKI_DOWNLOADS=rust` : téléchargements de modèles tenus par la façade.
|
||||
fn downloads_held_by_facade() -> bool {
|
||||
std::env::var("LOKI_DOWNLOADS").is_ok_and(|v| v.trim().eq_ignore_ascii_case("rust"))
|
||||
}
|
||||
|
||||
/// Les `.part` d'un téléchargement tué ne reprendront jamais : vidés au
|
||||
/// démarrage (le Go ne le fait plus quand la façade les tient).
|
||||
async fn start_downloads(
|
||||
backend: SocketAddr,
|
||||
token: &str,
|
||||
home: &std::path::Path,
|
||||
) -> std::sync::Arc<loki_rs::downloads::Downloads> {
|
||||
use loki_rs::{downloads, gguf, store};
|
||||
let st = store::Store::new(backend, token.to_string());
|
||||
let stored = gguf::parse_stored_model_dirs(
|
||||
st.get(store::BK_STATE, gguf::STATE_MODEL_DIRS_KEY)
|
||||
.await
|
||||
.ok()
|
||||
.flatten()
|
||||
.as_deref(),
|
||||
);
|
||||
let dirs = gguf::model_dirs(
|
||||
home,
|
||||
&stored,
|
||||
std::env::var(gguf::MODEL_DIRS_ENV).ok().as_deref(),
|
||||
);
|
||||
let _ = tokio::task::spawn_blocking(move || downloads::clean_stale_part_files(&dirs)).await;
|
||||
eprintln!("[loki-rs] téléchargements de modèles tenus par la façade (LOKI_DOWNLOADS=rust)");
|
||||
std::sync::Arc::new(downloads::Downloads::new())
|
||||
}
|
||||
|
||||
/// Le Go garde-t-il la conversation ? (quelques essais : il vient de démarrer)
|
||||
async fn go_keeps_chat(backend: SocketAddr, token: &str) -> Result<Option<String>, String> {
|
||||
let store = loki_rs::store::Store::new(backend, token.to_string());
|
||||
|
||||
@@ -63,6 +63,9 @@ pub struct Config {
|
||||
/// Conversation tenue par la façade (`LOKI_CHAT=rust`) : ses routes sont
|
||||
/// servies ici ; `None` : relayées au Go.
|
||||
pub chat: Option<Arc<crate::chat::generate::ChatService>>,
|
||||
/// Téléchargements de modèles tenus par la façade (`LOKI_DOWNLOADS=rust`) :
|
||||
/// leurs routes sont servies ici ; `None` : relayées au Go.
|
||||
pub downloads: Option<Arc<crate::downloads::Downloads>>,
|
||||
}
|
||||
|
||||
struct Proxy {
|
||||
@@ -90,6 +93,7 @@ pub async fn serve(
|
||||
host_env: cfg.host_env,
|
||||
engine: cfg.engine,
|
||||
chat: cfg.chat,
|
||||
downloads: cfg.downloads,
|
||||
internal_token: cfg.internal_token.clone(),
|
||||
},
|
||||
});
|
||||
@@ -129,8 +133,10 @@ pub async fn serve(
|
||||
impl Proxy {
|
||||
async fn handle(&self, mut req: Request<Incoming>) -> Response<Body> {
|
||||
// Le Go (et lui seul : jeton tiré à chaque démarrage) remet à la
|
||||
// façade le trafic de chat qui lui arrive sans passer par elle.
|
||||
let from_go = self.ctx.chat.is_some() && self.ctx.from_go(req.headers());
|
||||
// façade le trafic de chat et de téléchargements qui lui arrive sans
|
||||
// passer par elle.
|
||||
let from_go = (self.ctx.chat.is_some() || self.ctx.downloads.is_some())
|
||||
&& self.ctx.from_go(req.headers());
|
||||
// Jamais de confiance accordée sur la foi d'un en-tête du client :
|
||||
// ni « je viens du tunnel », ni « je suis la façade ».
|
||||
req.headers_mut().remove(VIA_TUNNEL);
|
||||
@@ -163,6 +169,11 @@ impl Proxy {
|
||||
{
|
||||
return chatapi::serve(&self.ctx, chat, req, from_go).await;
|
||||
}
|
||||
if let Some(dl) = &self.ctx.downloads
|
||||
&& crate::downloads::handles(path)
|
||||
{
|
||||
return crate::downloads::serve(&self.ctx, dl, req, from_go).await;
|
||||
}
|
||||
self.forward(req).await
|
||||
}
|
||||
|
||||
|
||||
@@ -427,7 +427,16 @@ 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).await;
|
||||
let (f, st, tx, _) = start_opts(home, false, None, false).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(
|
||||
home: std::path::PathBuf,
|
||||
) -> (SocketAddr, Arc<FakeGo>, oneshot::Sender<()>) {
|
||||
let (f, st, tx, _) = start_opts(home, false, None, true).await;
|
||||
(f, st, tx)
|
||||
}
|
||||
|
||||
@@ -442,7 +451,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)).await;
|
||||
let (f, st, tx, chat) = start_opts(home, true, Some(engine_port), false).await;
|
||||
(f, st, tx, chat.unwrap())
|
||||
}
|
||||
|
||||
@@ -450,6 +459,7 @@ async fn start_opts(
|
||||
home: std::path::PathBuf,
|
||||
with_chat: bool,
|
||||
engine_port: Option<u16>,
|
||||
with_downloads: bool,
|
||||
) -> (
|
||||
SocketAddr,
|
||||
Arc<FakeGo>,
|
||||
@@ -506,6 +516,7 @@ async fn start_opts(
|
||||
service: "loki-engine".into(),
|
||||
},
|
||||
chat: chat.clone(),
|
||||
downloads: with_downloads.then(|| Arc::new(loki_rs::downloads::Downloads::new())),
|
||||
};
|
||||
tokio::spawn(loki_rs::proxy::serve(fl, cfg, async move {
|
||||
let _ = rx.await;
|
||||
@@ -513,6 +524,32 @@ async fn start_opts(
|
||||
(facade, st, tx, chat)
|
||||
}
|
||||
|
||||
/// Requête via la façade (corps JSON) : (code, corps).
|
||||
pub async fn send(
|
||||
facade: SocketAddr,
|
||||
method: &str,
|
||||
path: &str,
|
||||
headers: &[(&str, &str)],
|
||||
body: &str,
|
||||
) -> (StatusCode, Bytes) {
|
||||
use hyper_util::client::legacy::Client;
|
||||
use hyper_util::rt::TokioExecutor;
|
||||
let mut req = Request::builder()
|
||||
.method(method)
|
||||
.uri(format!("http://{facade}{path}"))
|
||||
.header("content-type", "application/json");
|
||||
for (k, v) in headers {
|
||||
req = req.header(*k, *v);
|
||||
}
|
||||
let client: Client<_, Full<Bytes>> = Client::builder(TokioExecutor::new()).build_http();
|
||||
let r = client
|
||||
.request(req.body(Full::new(Bytes::from(body.to_string()))).unwrap())
|
||||
.await
|
||||
.unwrap();
|
||||
let (parts, body) = r.into_parts();
|
||||
(parts.status, body.collect().await.unwrap().to_bytes())
|
||||
}
|
||||
|
||||
/// GET via la façade : (code, en-têtes, corps).
|
||||
pub async fn get(
|
||||
facade: SocketAddr,
|
||||
|
||||
@@ -0,0 +1,692 @@
|
||||
//! Lot « modèles » : téléchargements tenus par la façade (`LOKI_DOWNLOADS=rust`).
|
||||
//!
|
||||
//! - les fonctions pures rejouent le journal tiré du Go
|
||||
//! (`internal/loki/parity_dl_test.go` → `tests/parity_models/download.json`) ;
|
||||
//! - le téléchargement de bout en bout tourne contre un faux Hugging Face
|
||||
//! (plages d'octets, tranches, dépôt verrouillé, annulation).
|
||||
|
||||
mod common;
|
||||
|
||||
use std::collections::HashMap;
|
||||
use std::convert::Infallible;
|
||||
use std::net::SocketAddr;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::sync::{Arc, Mutex};
|
||||
use std::time::Duration;
|
||||
|
||||
use bytes::Bytes;
|
||||
use common::fakego;
|
||||
use http_body_util::{BodyExt, Full, StreamBody, combinators::BoxBody};
|
||||
use hyper::body::{Frame, Incoming};
|
||||
use hyper::{Request, Response, StatusCode, service::service_fn};
|
||||
use hyper_util::rt::TokioIo;
|
||||
use loki_rs::downloads::{self, DlEnv, DlState, Downloads, ModelDirs};
|
||||
use serde_json::Value;
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Journal de référence du Go
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
#[test]
|
||||
fn parite_des_fonctions_pures() {
|
||||
let fx: Value =
|
||||
serde_json::from_str(include_str!("parity_models/download.json")).expect("journal");
|
||||
let s = |v: &Value, k: &str| v.get(k).and_then(Value::as_str).unwrap_or("").to_string();
|
||||
let list = |v: &Value, k: &str| -> Vec<String> {
|
||||
v.get(k)
|
||||
.and_then(Value::as_array)
|
||||
.map(|a| a.iter().map(|x| x.as_str().unwrap().to_string()).collect())
|
||||
.unwrap_or_default()
|
||||
};
|
||||
|
||||
for c in fx["normalize"].as_array().unwrap() {
|
||||
let input = s(c, "in");
|
||||
match downloads::normalize_hf_url(&input) {
|
||||
Err(e) => assert_eq!(e, s(c, "err"), "normalize({input:?})"),
|
||||
Ok((url, name)) => {
|
||||
assert_eq!(s(c, "err"), "", "normalize({input:?}) aurait dû échouer");
|
||||
assert_eq!(url, s(c, "url"), "url de {input:?}");
|
||||
assert_eq!(name, s(c, "name"), "nom de {input:?}");
|
||||
let (urls, names) = downloads::shard_url_set(&url, &name);
|
||||
assert_eq!(urls, list(c, "urls"), "tranches de {input:?}");
|
||||
assert_eq!(names, list(c, "names"), "noms de {input:?}");
|
||||
}
|
||||
}
|
||||
}
|
||||
for (v, want) in fx["content_range"].as_object().unwrap() {
|
||||
assert_eq!(
|
||||
downloads::content_range_total(v),
|
||||
want.as_i64().unwrap(),
|
||||
"{v:?}"
|
||||
);
|
||||
}
|
||||
for c in fx["source"].as_array().unwrap() {
|
||||
let got = downloads::dl_source_error(
|
||||
c["status"].as_u64().unwrap() as u16,
|
||||
&s(c, "code"),
|
||||
&s(c, "url"),
|
||||
c["token_set"].as_bool().unwrap(),
|
||||
);
|
||||
assert_eq!(got, s(c, "err"), "{c}");
|
||||
}
|
||||
for (h, want) in fx["hf_host"].as_object().unwrap() {
|
||||
assert_eq!(downloads::is_hf_host(h), want.as_bool().unwrap(), "{h}");
|
||||
}
|
||||
for (u, want) in fx["hf_repo"].as_object().unwrap() {
|
||||
assert_eq!(
|
||||
downloads::hf_repo_from_url(u),
|
||||
want.as_str().unwrap(),
|
||||
"{u}"
|
||||
);
|
||||
}
|
||||
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let home = tmp.path().canonicalize().unwrap();
|
||||
let home_s = home.to_string_lossy().into_owned();
|
||||
std::fs::create_dir_all(home.join("disque")).unwrap();
|
||||
let dirs = ModelDirs {
|
||||
home: home.clone(),
|
||||
stored: vec![format!("{home_s}/disque")],
|
||||
env: None,
|
||||
};
|
||||
for c in fx["dest"].as_array().unwrap() {
|
||||
let dir = s(c, "dir").replace("{{HOME}}", &home_s);
|
||||
let got = match dirs.download_dest_path(&s(c, "name"), &dir) {
|
||||
Ok(p) => ("path", p.replace(&home_s, "{{HOME}}")),
|
||||
Err(e) => ("err", e.replace(&home_s, "{{HOME}}")),
|
||||
};
|
||||
let want = if c.get("path").is_some() {
|
||||
("path", s(c, "path"))
|
||||
} else {
|
||||
("err", s(c, "err"))
|
||||
};
|
||||
assert_eq!(got, want, "{c}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tailles_lisibles_comme_le_go() {
|
||||
assert_eq!(downloads::human_bytes(0), "0 o");
|
||||
assert_eq!(downloads::human_bytes((1 << 20) - 1), "1048575 o");
|
||||
assert_eq!(downloads::human_bytes(1 << 20), "1 Mo");
|
||||
assert_eq!(downloads::human_bytes(1536 << 20), "1.5 Go");
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Faux Hugging Face
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
type Body = BoxBody<Bytes, Infallible>;
|
||||
|
||||
#[derive(Default)]
|
||||
struct FakeHf {
|
||||
files: Mutex<HashMap<String, Arc<Vec<u8>>>>,
|
||||
/// Chemin → (statut, X-Error-Code) imposés.
|
||||
refuse: Mutex<HashMap<String, (u16, String)>>,
|
||||
/// Ignore les plages (200, corps entier).
|
||||
no_ranges: Mutex<bool>,
|
||||
/// Corps envoyé au goutte-à-goutte (annulation).
|
||||
slow: Mutex<bool>,
|
||||
/// « chemin Range » de chaque requête, et en-têtes Authorization vus.
|
||||
log: Mutex<Vec<String>>,
|
||||
auth: Mutex<Vec<String>>,
|
||||
}
|
||||
|
||||
fn full(status: u16, body: impl Into<Bytes>) -> Response<Body> {
|
||||
let mut r = Response::new(Full::new(body.into()).map_err(|e| match e {}).boxed());
|
||||
*r.status_mut() = StatusCode::from_u16(status).unwrap();
|
||||
r
|
||||
}
|
||||
|
||||
async fn hf_handle(hf: Arc<FakeHf>, req: Request<Incoming>) -> Response<Body> {
|
||||
let path = req.uri().path().to_string();
|
||||
let range = req
|
||||
.headers()
|
||||
.get("range")
|
||||
.and_then(|v| v.to_str().ok())
|
||||
.unwrap_or("")
|
||||
.to_string();
|
||||
if let Some(a) = req.headers().get("authorization") {
|
||||
hf.auth
|
||||
.lock()
|
||||
.unwrap()
|
||||
.push(a.to_str().unwrap().to_string());
|
||||
}
|
||||
hf.log.lock().unwrap().push(format!("{path} {range}"));
|
||||
if let Some((st, code)) = hf.refuse.lock().unwrap().get(&path).cloned() {
|
||||
let mut r = full(st, "refus");
|
||||
if !code.is_empty() {
|
||||
r.headers_mut()
|
||||
.insert("x-error-code", code.parse().unwrap());
|
||||
}
|
||||
return r;
|
||||
}
|
||||
let Some(data) = hf.files.lock().unwrap().get(&path).cloned() else {
|
||||
return full(404, "absent");
|
||||
};
|
||||
let total = data.len();
|
||||
let (status, start, end) = match range.strip_prefix("bytes=") {
|
||||
Some(r) if !*hf.no_ranges.lock().unwrap() => {
|
||||
let (a, b) = r.split_once('-').unwrap();
|
||||
let a: usize = a.parse().unwrap();
|
||||
let b: usize = if b.is_empty() {
|
||||
total - 1
|
||||
} else {
|
||||
b.parse::<usize>().unwrap().min(total - 1)
|
||||
};
|
||||
(206, a, b)
|
||||
}
|
||||
_ => (200, 0, total - 1),
|
||||
};
|
||||
let chunk = Bytes::copy_from_slice(&data[start..=end]);
|
||||
let mut r = if *hf.slow.lock().unwrap() && chunk.len() > 1 {
|
||||
// 64 Kio toutes les 20 ms : le téléchargement dure, on peut l'annuler.
|
||||
let s = futures_util::stream::unfold(chunk, |mut rest| async move {
|
||||
if rest.is_empty() {
|
||||
return None;
|
||||
}
|
||||
tokio::time::sleep(Duration::from_millis(20)).await;
|
||||
let part = rest.split_to(rest.len().min(64 << 10));
|
||||
Some((Ok::<_, Infallible>(Frame::data(part)), rest))
|
||||
});
|
||||
let mut r = Response::new(StreamBody::new(s).boxed());
|
||||
*r.status_mut() = StatusCode::from_u16(status).unwrap();
|
||||
r
|
||||
} else {
|
||||
full(status, chunk)
|
||||
};
|
||||
if status == 206 {
|
||||
r.headers_mut().insert(
|
||||
"content-range",
|
||||
format!("bytes {start}-{end}/{total}").parse().unwrap(),
|
||||
);
|
||||
}
|
||||
r
|
||||
}
|
||||
|
||||
async fn fake_hf() -> (SocketAddr, Arc<FakeHf>) {
|
||||
let hf = Arc::new(FakeHf::default());
|
||||
let l = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
|
||||
let addr = l.local_addr().unwrap();
|
||||
let h = hf.clone();
|
||||
tokio::spawn(async move {
|
||||
while let Ok((s, _)) = l.accept().await {
|
||||
let h = h.clone();
|
||||
tokio::spawn(async move {
|
||||
let svc = service_fn(move |req| {
|
||||
let h = h.clone();
|
||||
async move { Ok::<_, Infallible>(hf_handle(h, req).await) }
|
||||
});
|
||||
let _ = hyper::server::conn::http1::Builder::new()
|
||||
.serve_connection(TokioIo::new(s), svc)
|
||||
.await;
|
||||
});
|
||||
}
|
||||
});
|
||||
(addr, hf)
|
||||
}
|
||||
|
||||
/// Contenu reconnaissable : un décalage d'octets se verrait.
|
||||
fn payload(n: usize, seed: u8) -> Vec<u8> {
|
||||
(0..n)
|
||||
.map(|i| (i as u32).wrapping_mul(2_654_435_761).to_le_bytes()[1] ^ seed)
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn env() -> DlEnv {
|
||||
DlEnv {
|
||||
hf_token: "hf_secret".into(),
|
||||
version: "9.9.9".into(),
|
||||
}
|
||||
}
|
||||
|
||||
fn dirs_at(home: &Path) -> ModelDirs {
|
||||
ModelDirs {
|
||||
home: home.to_path_buf(),
|
||||
stored: vec![],
|
||||
env: None,
|
||||
}
|
||||
}
|
||||
|
||||
async fn wait_finished(dl: &Downloads, name: &str) -> DlState {
|
||||
for _ in 0..1000 {
|
||||
if let Some(s) = dl.status().into_iter().find(|s| s.filename == name)
|
||||
&& s.finished
|
||||
{
|
||||
return s;
|
||||
}
|
||||
tokio::time::sleep(Duration::from_millis(10)).await;
|
||||
}
|
||||
panic!("téléchargement {name} jamais terminé : {:?}", dl.status());
|
||||
}
|
||||
|
||||
fn models(home: &Path) -> PathBuf {
|
||||
home.join("models")
|
||||
}
|
||||
|
||||
fn entries(dir: &Path) -> Vec<String> {
|
||||
let mut v: Vec<String> = std::fs::read_dir(dir)
|
||||
.map(|rd| {
|
||||
rd.filter_map(Result::ok)
|
||||
.map(|e| e.file_name().to_string_lossy().into_owned())
|
||||
.collect()
|
||||
})
|
||||
.unwrap_or_default();
|
||||
v.sort();
|
||||
v
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// De bout en bout
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
#[tokio::test]
|
||||
async fn un_fichier_sur_plusieurs_connexions() {
|
||||
let (addr, hf) = fake_hf().await;
|
||||
let data = payload(40 << 20, 7); // 40 Mio → 3 parts de 16 Mio au plus
|
||||
hf.files
|
||||
.lock()
|
||||
.unwrap()
|
||||
.insert("/u/r/resolve/main/m.gguf".into(), Arc::new(data.clone()));
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let dl = Arc::new(Downloads::new());
|
||||
let (code, out) = dl.start(
|
||||
env(),
|
||||
&dirs_at(tmp.path()),
|
||||
&format!("http://{addr}/u/r/resolve/main/m.gguf"),
|
||||
"",
|
||||
);
|
||||
assert_eq!(code, StatusCode::OK, "{out}");
|
||||
assert_eq!(out["filename"], "m.gguf");
|
||||
assert_eq!(out["parts"], 1);
|
||||
let st = wait_finished(&dl, "m.gguf").await;
|
||||
assert_eq!(st.err, "");
|
||||
assert!(!st.canceled);
|
||||
assert_eq!(
|
||||
(st.total, st.done, st.conns, st.parts, st.part),
|
||||
(40 << 20, 40 << 20, 3, 1, 1)
|
||||
);
|
||||
assert_eq!(st.dir, models(tmp.path()).to_string_lossy());
|
||||
let got = std::fs::read(models(tmp.path()).join("m.gguf")).unwrap();
|
||||
assert!(got == data, "contenu différent");
|
||||
assert_eq!(entries(&models(tmp.path())), ["m.gguf"]);
|
||||
// Sonde d'un octet, puis trois plages disjointes qui couvrent le fichier.
|
||||
let log = hf.log.lock().unwrap().clone();
|
||||
assert_eq!(log[0], "/u/r/resolve/main/m.gguf bytes=0-0");
|
||||
let mut ranges: Vec<&str> = log[1..]
|
||||
.iter()
|
||||
.map(|l| l.split(' ').nth(1).unwrap())
|
||||
.collect();
|
||||
ranges.sort();
|
||||
assert_eq!(
|
||||
ranges,
|
||||
[
|
||||
"bytes=0-13981012",
|
||||
"bytes=13981013-27962025",
|
||||
"bytes=27962026-41943039"
|
||||
]
|
||||
);
|
||||
// Hôte hors Hugging Face : le jeton ne part pas.
|
||||
assert!(hf.auth.lock().unwrap().is_empty());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn source_sans_plages_un_seul_flux() {
|
||||
let (addr, hf) = fake_hf().await;
|
||||
let data = payload(3 << 20, 1);
|
||||
hf.files
|
||||
.lock()
|
||||
.unwrap()
|
||||
.insert("/m.gguf".into(), Arc::new(data.clone()));
|
||||
*hf.no_ranges.lock().unwrap() = true;
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let dl = Arc::new(Downloads::new());
|
||||
let (code, _) = dl.start(
|
||||
env(),
|
||||
&dirs_at(tmp.path()),
|
||||
&format!("http://{addr}/m.gguf"),
|
||||
"",
|
||||
);
|
||||
assert_eq!(code, StatusCode::OK);
|
||||
let st = wait_finished(&dl, "m.gguf").await;
|
||||
assert_eq!(
|
||||
(st.err.as_str(), st.total, st.done, st.conns),
|
||||
("", 3 << 20, 3 << 20, 1)
|
||||
);
|
||||
assert!(std::fs::read(models(tmp.path()).join("m.gguf")).unwrap() == data);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn modele_en_tranches_rapatrie_en_entier() {
|
||||
let (addr, hf) = fake_hf().await;
|
||||
let parts: Vec<Vec<u8>> = (0..3).map(|i| payload(1 << 20, i as u8)).collect();
|
||||
for (i, p) in parts.iter().enumerate() {
|
||||
hf.files.lock().unwrap().insert(
|
||||
format!("/u/r/resolve/main/Q4/m-0000{}-of-00003.gguf", i + 1),
|
||||
Arc::new(p.clone()),
|
||||
);
|
||||
}
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let m = models(tmp.path());
|
||||
std::fs::create_dir_all(&m).unwrap();
|
||||
// La première est déjà là : pas retéléchargée.
|
||||
std::fs::write(m.join("m-00001-of-00003.gguf"), &parts[0]).unwrap();
|
||||
let dl = Arc::new(Downloads::new());
|
||||
let (code, out) = dl.start(
|
||||
env(),
|
||||
&dirs_at(tmp.path()),
|
||||
&format!("http://{addr}/u/r/resolve/main/Q4/m-00002-of-00003.gguf"),
|
||||
"",
|
||||
);
|
||||
assert_eq!(code, StatusCode::OK, "{out}");
|
||||
assert_eq!(
|
||||
(out["filename"].as_str(), out["parts"].as_i64()),
|
||||
(Some("m-00001-of-00003.gguf"), Some(2))
|
||||
);
|
||||
let st = wait_finished(&dl, "m-00001-of-00003.gguf").await;
|
||||
assert_eq!(
|
||||
(st.err.as_str(), st.total, st.done, st.parts, st.part),
|
||||
("", 2 << 20, 2 << 20, 2, 2)
|
||||
);
|
||||
for (i, p) in parts.iter().enumerate() {
|
||||
let got = std::fs::read(m.join(format!("m-0000{}-of-00003.gguf", i + 1))).unwrap();
|
||||
assert!(&got == p, "tranche {}", i + 1);
|
||||
}
|
||||
assert!(
|
||||
!hf.log
|
||||
.lock()
|
||||
.unwrap()
|
||||
.iter()
|
||||
.any(|l| l.contains("00001-of")),
|
||||
"tranche présente retéléchargée"
|
||||
);
|
||||
// Tout est là : rien à faire, le modèle est à sélectionner.
|
||||
let (code, out) = dl.start(
|
||||
env(),
|
||||
&dirs_at(tmp.path()),
|
||||
&format!("http://{addr}/u/r/resolve/main/Q4/m-00003-of-00003.gguf"),
|
||||
"",
|
||||
);
|
||||
assert_eq!(code, StatusCode::OK);
|
||||
assert_eq!(out["exists"], true);
|
||||
assert_eq!(out["value"], "m-00001-of-00003.gguf");
|
||||
assert_eq!(out["parts"], 0);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn depot_verrouille_rien_ne_reste() {
|
||||
let (addr, hf) = fake_hf().await;
|
||||
for i in 1..=2 {
|
||||
hf.files.lock().unwrap().insert(
|
||||
format!("/g/d/resolve/main/m-0000{i}-of-00002.gguf"),
|
||||
Arc::new(payload(1 << 20, i)),
|
||||
);
|
||||
}
|
||||
hf.refuse.lock().unwrap().insert(
|
||||
"/g/d/resolve/main/m-00002-of-00002.gguf".into(),
|
||||
(401, "GatedRepo".into()),
|
||||
);
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let dl = Arc::new(Downloads::new());
|
||||
let (code, _) = dl.start(
|
||||
env(),
|
||||
&dirs_at(tmp.path()),
|
||||
&format!("http://{addr}/g/d/resolve/main/m-00001-of-00002.gguf"),
|
||||
"",
|
||||
);
|
||||
assert_eq!(code, StatusCode::OK);
|
||||
let st = wait_finished(&dl, "m-00001-of-00002.gguf").await;
|
||||
// Refusé dès la sonde : rien n'a été écrit.
|
||||
assert_eq!(
|
||||
st.err,
|
||||
"dépôt à accès restreint : ses conditions doivent être acceptées sur huggingface.co — le jeton Hugging Face enregistré n'y donne pas accès (expiré, ou conditions non acceptées avec ce compte)"
|
||||
);
|
||||
assert_eq!(entries(&models(tmp.path())), Vec::<String>::new());
|
||||
assert!(
|
||||
hf.log
|
||||
.lock()
|
||||
.unwrap()
|
||||
.iter()
|
||||
.all(|l| l.ends_with("bytes=0-0"))
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn panne_pendant_la_deuxieme_tranche_efface_la_premiere() {
|
||||
let (addr, hf) = fake_hf().await;
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let dl = Arc::new(Downloads::new());
|
||||
for i in 1..=2 {
|
||||
hf.files.lock().unwrap().insert(
|
||||
format!("/m-0000{i}-of-00002.gguf"),
|
||||
Arc::new(payload(1 << 20, i)),
|
||||
);
|
||||
}
|
||||
// La sonde passe, le transfert de la deuxième échoue (fichier retiré).
|
||||
let (code, _) = dl.start(
|
||||
env(),
|
||||
&dirs_at(tmp.path()),
|
||||
&format!("http://{addr}/m-00001-of-00002.gguf"),
|
||||
"",
|
||||
);
|
||||
assert_eq!(code, StatusCode::OK);
|
||||
// Retire la deuxième dès que la première est écrite.
|
||||
let m = models(tmp.path());
|
||||
for _ in 0..500 {
|
||||
if m.join("m-00001-of-00002.gguf").exists() {
|
||||
break;
|
||||
}
|
||||
if dl.status()[0].finished {
|
||||
break;
|
||||
}
|
||||
tokio::time::sleep(Duration::from_millis(2)).await;
|
||||
}
|
||||
let st0 = dl.status()[0].clone();
|
||||
if st0.finished {
|
||||
// Trop rapide pour la machine : le scénario ne se joue pas.
|
||||
return;
|
||||
}
|
||||
hf.files.lock().unwrap().remove("/m-00002-of-00002.gguf");
|
||||
let st = wait_finished(&dl, "m-00001-of-00002.gguf").await;
|
||||
if st.err.is_empty() {
|
||||
return; // deuxième tranche déjà partie avant le retrait
|
||||
}
|
||||
assert_eq!(st.err, "lien introuvable (HTTP 404)");
|
||||
assert_eq!(entries(&m), Vec::<String>::new(), "tranches laissées");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn annulation_efface_le_part() {
|
||||
let (addr, hf) = fake_hf().await;
|
||||
hf.files
|
||||
.lock()
|
||||
.unwrap()
|
||||
.insert("/m.gguf".into(), Arc::new(payload(8 << 20, 3)));
|
||||
*hf.slow.lock().unwrap() = true;
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let dl = Arc::new(Downloads::new());
|
||||
let (code, _) = dl.start(
|
||||
env(),
|
||||
&dirs_at(tmp.path()),
|
||||
&format!("http://{addr}/m.gguf"),
|
||||
"",
|
||||
);
|
||||
assert_eq!(code, StatusCode::OK);
|
||||
// Deuxième lancement pendant le premier : refusé.
|
||||
let (code, out) = dl.start(
|
||||
env(),
|
||||
&dirs_at(tmp.path()),
|
||||
&format!("http://{addr}/m.gguf"),
|
||||
"",
|
||||
);
|
||||
assert_eq!(code, StatusCode::CONFLICT);
|
||||
assert_eq!(out["error"], "téléchargement déjà en cours pour m.gguf");
|
||||
let part = models(tmp.path()).join("m.gguf.part");
|
||||
for _ in 0..500 {
|
||||
if part.exists() {
|
||||
break;
|
||||
}
|
||||
tokio::time::sleep(Duration::from_millis(5)).await;
|
||||
}
|
||||
assert!(part.exists(), "pas de .part en cours");
|
||||
assert!(dl.cancel("m.gguf"));
|
||||
assert!(!dl.cancel("autre.gguf"));
|
||||
let st = wait_finished(&dl, "m.gguf").await;
|
||||
assert!(st.canceled && st.err.is_empty(), "{st:?}");
|
||||
assert_eq!(st.speed, 0);
|
||||
assert_eq!(entries(&models(tmp.path())), Vec::<String>::new());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn refus_avant_de_lancer() {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let dl = Arc::new(Downloads::new());
|
||||
let d = dirs_at(tmp.path());
|
||||
let (code, out) = dl.start(env(), &d, "pas un lien", "");
|
||||
assert_eq!(
|
||||
(code, out["error"].as_str()),
|
||||
(
|
||||
StatusCode::BAD_REQUEST,
|
||||
Some("lien invalide (http/https attendu)")
|
||||
)
|
||||
);
|
||||
let (code, out) = dl.start(env(), &d, "http://h/m.gguf", "/etc");
|
||||
assert_eq!(code, StatusCode::BAD_REQUEST);
|
||||
assert_eq!(
|
||||
out["error"],
|
||||
"dossier non autorisé : /etc — ajoute-le dans « Dossiers de modèles »"
|
||||
);
|
||||
assert!(dl.status().is_empty());
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Par la façade
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
#[tokio::test]
|
||||
async fn routes_servies_par_la_facade() {
|
||||
let (addr, hf) = fake_hf().await;
|
||||
hf.files.lock().unwrap().insert(
|
||||
"/u/r/resolve/main/m.gguf".into(),
|
||||
Arc::new(payload(1 << 20, 9)),
|
||||
);
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let home = tmp.path().canonicalize().unwrap();
|
||||
let (facade, go, _stop) = fakego::start_downloads(home.clone()).await;
|
||||
let url = format!("http://{addr}/u/r/resolve/main/m.gguf");
|
||||
let body = format!(r#"{{"url":"{url}","dir":""}}"#);
|
||||
|
||||
// Sonde : taille, dossier, espace.
|
||||
let (code, b) = fakego::send(facade, "POST", "/api/models/download/probe", &[], &body).await;
|
||||
assert_eq!(code, StatusCode::OK, "{b:?}");
|
||||
let v: Value = serde_json::from_slice(&b).unwrap();
|
||||
assert_eq!(v["ok"], true);
|
||||
assert_eq!(v["size"], 1 << 20);
|
||||
assert_eq!(v["parts"], 1);
|
||||
assert_eq!(v["enough"], true);
|
||||
assert_eq!(v["dir"], models(&home).to_string_lossy().as_ref());
|
||||
|
||||
// Lancement, suivi, puis la sonde dit « déjà là ».
|
||||
let (code, b) = fakego::send(facade, "POST", "/api/models/download", &[], &body).await;
|
||||
assert_eq!(code, StatusCode::OK, "{b:?}");
|
||||
let mut done = false;
|
||||
for _ in 0..500 {
|
||||
let (_, _, b) = fakego::get(facade, "/api/models/download/status", &[]).await;
|
||||
let v: Value = serde_json::from_slice(&b).unwrap();
|
||||
if v[0]["finished"] == true {
|
||||
assert_eq!(v[0]["error"], "");
|
||||
assert_eq!(v[0]["done"], 1 << 20);
|
||||
done = true;
|
||||
break;
|
||||
}
|
||||
tokio::time::sleep(Duration::from_millis(10)).await;
|
||||
}
|
||||
assert!(done);
|
||||
let (_, b) = fakego::send(facade, "POST", "/api/models/download/probe", &[], &body).await;
|
||||
let v: Value = serde_json::from_slice(&b).unwrap();
|
||||
assert_eq!(
|
||||
(v["exists"].as_bool(), v["value"].as_str()),
|
||||
(Some(true), Some("m.gguf"))
|
||||
);
|
||||
assert_eq!(v["size"], 1 << 20);
|
||||
|
||||
// Annuler l'inconnu ; corps vide.
|
||||
let (code, b) = fakego::send(
|
||||
facade,
|
||||
"POST",
|
||||
"/api/models/download/cancel",
|
||||
&[],
|
||||
r#"{"filename":"../x.gguf"}"#,
|
||||
)
|
||||
.await;
|
||||
assert_eq!(code, StatusCode::NOT_FOUND);
|
||||
assert_eq!(
|
||||
serde_json::from_slice::<Value>(&b).unwrap()["error"],
|
||||
"aucun téléchargement pour x.gguf"
|
||||
);
|
||||
let (code, b) = fakego::send(facade, "POST", "/api/models/download", &[], "").await;
|
||||
assert_eq!(
|
||||
(
|
||||
code,
|
||||
serde_json::from_slice::<Value>(&b).unwrap()["error"].as_str()
|
||||
),
|
||||
(StatusCode::BAD_REQUEST, Some("EOF"))
|
||||
);
|
||||
|
||||
// Clé de pilotage posée : refus sans elle, la remise du Go passe.
|
||||
go.set("state", "web_key", "cle");
|
||||
let (code, _, _) = fakego::get(facade, "/api/models/download/status", &[]).await;
|
||||
assert_eq!(code, StatusCode::UNAUTHORIZED);
|
||||
let (code, _, _) = fakego::get(
|
||||
facade,
|
||||
"/api/models/download/status",
|
||||
&[("x-loki-internal", fakego::JETON)],
|
||||
)
|
||||
.await;
|
||||
assert_eq!(code, StatusCode::OK);
|
||||
// Rien n'est parti au Go.
|
||||
assert!(
|
||||
!go.relayed
|
||||
.lock()
|
||||
.unwrap()
|
||||
.iter()
|
||||
.any(|p| p.contains("download")),
|
||||
"{:?}",
|
||||
go.relayed.lock().unwrap()
|
||||
);
|
||||
}
|
||||
|
||||
#[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;
|
||||
let (_, _, b) = fakego::get(facade, "/api/models/download/status", &[]).await;
|
||||
assert_eq!(&b[..], b"go:/api/models/download/status");
|
||||
assert!(
|
||||
go.relayed
|
||||
.lock()
|
||||
.unwrap()
|
||||
.iter()
|
||||
.any(|p| p == "/api/models/download/status")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn part_perimes_vides_dans_chaque_dossier() {
|
||||
let a = tempfile::tempdir().unwrap();
|
||||
let b = tempfile::tempdir().unwrap();
|
||||
for d in [a.path(), b.path()] {
|
||||
std::fs::write(d.join("x.gguf.part"), b"x").unwrap();
|
||||
std::fs::write(d.join("X.GGUF.PART"), b"x").unwrap();
|
||||
std::fs::write(d.join("y.gguf"), b"y").unwrap();
|
||||
}
|
||||
downloads::clean_stale_part_files(&[
|
||||
a.path().to_string_lossy().into_owned(),
|
||||
b.path().to_string_lossy().into_owned(),
|
||||
"/nexiste/pas".into(),
|
||||
]);
|
||||
for d in [a.path(), b.path()] {
|
||||
// Le motif du Go est sensible à la casse.
|
||||
assert_eq!(entries(d), ["X.GGUF.PART", "y.gguf"]);
|
||||
}
|
||||
}
|
||||
@@ -94,6 +94,7 @@ async fn banc(cle: Option<&str>) -> Banc {
|
||||
service: "loki-engine".into(),
|
||||
},
|
||||
chat: None,
|
||||
downloads: None,
|
||||
};
|
||||
tokio::spawn(loki_rs::proxy::serve(l, cfg, async move {
|
||||
let _ = rx.await;
|
||||
|
||||
@@ -0,0 +1,314 @@
|
||||
{
|
||||
"normalize": [
|
||||
{
|
||||
"in": "",
|
||||
"err": "lien vide"
|
||||
},
|
||||
{
|
||||
"in": " ",
|
||||
"err": "lien vide"
|
||||
},
|
||||
{
|
||||
"in": "ftp://h/a.gguf",
|
||||
"err": "lien invalide (http/https attendu)"
|
||||
},
|
||||
{
|
||||
"in": "pas un lien",
|
||||
"err": "lien invalide (http/https attendu)"
|
||||
},
|
||||
{
|
||||
"in": "https://exemple.org/dossier/",
|
||||
"err": "le lien doit pointer vers un fichier .gguf"
|
||||
},
|
||||
{
|
||||
"in": "https://huggingface.co/u/r/blob/main/m.gguf",
|
||||
"url": "https://huggingface.co/u/r/resolve/main/m.gguf",
|
||||
"name": "m.gguf",
|
||||
"urls": [
|
||||
"https://huggingface.co/u/r/resolve/main/m.gguf"
|
||||
],
|
||||
"names": [
|
||||
"m.gguf"
|
||||
]
|
||||
},
|
||||
{
|
||||
"in": "https://huggingface.co/u/r/blob/main/blob/m.gguf",
|
||||
"url": "https://huggingface.co/u/r/resolve/main/blob/m.gguf",
|
||||
"name": "m.gguf",
|
||||
"urls": [
|
||||
"https://huggingface.co/u/r/resolve/main/blob/m.gguf"
|
||||
],
|
||||
"names": [
|
||||
"m.gguf"
|
||||
]
|
||||
},
|
||||
{
|
||||
"in": "https://huggingface.co/u/r/resolve/main/m-Q4_K_M.gguf?download=true",
|
||||
"url": "https://huggingface.co/u/r/resolve/main/m-Q4_K_M.gguf?download=true",
|
||||
"name": "m-Q4_K_M.gguf",
|
||||
"urls": [
|
||||
"https://huggingface.co/u/r/resolve/main/m-Q4_K_M.gguf?download=true"
|
||||
],
|
||||
"names": [
|
||||
"m-Q4_K_M.gguf"
|
||||
]
|
||||
},
|
||||
{
|
||||
"in": " https://huggingface.co/u/r/blob/main/m.bin ",
|
||||
"err": "le lien doit pointer vers un fichier .gguf"
|
||||
},
|
||||
{
|
||||
"in": "http://exemple.org/modeles/M.GGUF",
|
||||
"url": "http://exemple.org/modeles/M.GGUF",
|
||||
"name": "M.GGUF",
|
||||
"urls": [
|
||||
"http://exemple.org/modeles/M.GGUF"
|
||||
],
|
||||
"names": [
|
||||
"M.GGUF"
|
||||
]
|
||||
},
|
||||
{
|
||||
"in": "https://huggingface.co/u/r/blob/main/Q4/m-Q4-00002-of-00003.gguf",
|
||||
"url": "https://huggingface.co/u/r/resolve/main/Q4/m-Q4-00002-of-00003.gguf",
|
||||
"name": "m-Q4-00002-of-00003.gguf",
|
||||
"urls": [
|
||||
"https://huggingface.co/u/r/resolve/main/Q4/m-Q4-00001-of-00003.gguf",
|
||||
"https://huggingface.co/u/r/resolve/main/Q4/m-Q4-00002-of-00003.gguf",
|
||||
"https://huggingface.co/u/r/resolve/main/Q4/m-Q4-00003-of-00003.gguf"
|
||||
],
|
||||
"names": [
|
||||
"m-Q4-00001-of-00003.gguf",
|
||||
"m-Q4-00002-of-00003.gguf",
|
||||
"m-Q4-00003-of-00003.gguf"
|
||||
]
|
||||
},
|
||||
{
|
||||
"in": "https://huggingface.co/u/r/resolve/main/m-00001-of-00012.gguf",
|
||||
"url": "https://huggingface.co/u/r/resolve/main/m-00001-of-00012.gguf",
|
||||
"name": "m-00001-of-00012.gguf",
|
||||
"urls": [
|
||||
"https://huggingface.co/u/r/resolve/main/m-00001-of-00012.gguf",
|
||||
"https://huggingface.co/u/r/resolve/main/m-00002-of-00012.gguf",
|
||||
"https://huggingface.co/u/r/resolve/main/m-00003-of-00012.gguf",
|
||||
"https://huggingface.co/u/r/resolve/main/m-00004-of-00012.gguf",
|
||||
"https://huggingface.co/u/r/resolve/main/m-00005-of-00012.gguf",
|
||||
"https://huggingface.co/u/r/resolve/main/m-00006-of-00012.gguf",
|
||||
"https://huggingface.co/u/r/resolve/main/m-00007-of-00012.gguf",
|
||||
"https://huggingface.co/u/r/resolve/main/m-00008-of-00012.gguf",
|
||||
"https://huggingface.co/u/r/resolve/main/m-00009-of-00012.gguf",
|
||||
"https://huggingface.co/u/r/resolve/main/m-00010-of-00012.gguf",
|
||||
"https://huggingface.co/u/r/resolve/main/m-00011-of-00012.gguf",
|
||||
"https://huggingface.co/u/r/resolve/main/m-00012-of-00012.gguf"
|
||||
],
|
||||
"names": [
|
||||
"m-00001-of-00012.gguf",
|
||||
"m-00002-of-00012.gguf",
|
||||
"m-00003-of-00012.gguf",
|
||||
"m-00004-of-00012.gguf",
|
||||
"m-00005-of-00012.gguf",
|
||||
"m-00006-of-00012.gguf",
|
||||
"m-00007-of-00012.gguf",
|
||||
"m-00008-of-00012.gguf",
|
||||
"m-00009-of-00012.gguf",
|
||||
"m-00010-of-00012.gguf",
|
||||
"m-00011-of-00012.gguf",
|
||||
"m-00012-of-00012.gguf"
|
||||
]
|
||||
},
|
||||
{
|
||||
"in": "https://exemple.org/m-1-of-2.gguf",
|
||||
"url": "https://exemple.org/m-1-of-2.gguf",
|
||||
"name": "m-1-of-2.gguf",
|
||||
"urls": [
|
||||
"https://exemple.org/m-1-of-2.gguf"
|
||||
],
|
||||
"names": [
|
||||
"m-1-of-2.gguf"
|
||||
]
|
||||
},
|
||||
{
|
||||
"in": "https://exemple.org/m-02-of-01.gguf",
|
||||
"url": "https://exemple.org/m-02-of-01.gguf",
|
||||
"name": "m-02-of-01.gguf",
|
||||
"urls": [
|
||||
"https://exemple.org/m-02-of-01.gguf"
|
||||
],
|
||||
"names": [
|
||||
"m-02-of-01.gguf"
|
||||
]
|
||||
}
|
||||
],
|
||||
"content_range": {
|
||||
"": 0,
|
||||
"bytes 0-0/ 99 ": 99,
|
||||
"bytes 0-0/*": 0,
|
||||
"bytes 0-0/0": 0,
|
||||
"bytes 0-0/12345": 12345
|
||||
},
|
||||
"source": [
|
||||
{
|
||||
"status": 401,
|
||||
"code": "GatedRepo",
|
||||
"url": "https://huggingface.co/auteur/depot/resolve/main/m.gguf",
|
||||
"token_set": false,
|
||||
"err": "dépôt à accès restreint : accepte ses conditions sur huggingface.co/auteur/depot — puis renseigne le jeton Hugging Face (réglage « Jeton Hugging Face », ou variable d'environnement HF_TOKEN)"
|
||||
},
|
||||
{
|
||||
"status": 401,
|
||||
"code": "GatedRepo",
|
||||
"url": "https://huggingface.co/auteur/depot/resolve/main/m.gguf",
|
||||
"token_set": true,
|
||||
"err": "dépôt à accès restreint : accepte ses conditions sur huggingface.co/auteur/depot — le jeton Hugging Face enregistré n'y donne pas accès (expiré, ou conditions non acceptées avec ce compte)"
|
||||
},
|
||||
{
|
||||
"status": 403,
|
||||
"code": "RepoNotFound",
|
||||
"url": "https://huggingface.co/auteur/depot/resolve/main/m.gguf",
|
||||
"token_set": false,
|
||||
"err": "dépôt privé ou inexistant : auteur/depot — puis renseigne le jeton Hugging Face (réglage « Jeton Hugging Face », ou variable d'environnement HF_TOKEN)"
|
||||
},
|
||||
{
|
||||
"status": 401,
|
||||
"code": "GatedRepo",
|
||||
"url": "https://exemple.org/m.gguf",
|
||||
"token_set": false,
|
||||
"err": "dépôt à accès restreint : ses conditions doivent être acceptées sur huggingface.co — puis renseigne le jeton Hugging Face (réglage « Jeton Hugging Face », ou variable d'environnement HF_TOKEN)"
|
||||
},
|
||||
{
|
||||
"status": 403,
|
||||
"code": "",
|
||||
"url": "https://huggingface.co/auteur/depot/resolve/main/m.gguf",
|
||||
"token_set": true,
|
||||
"err": "accès refusé par Hugging Face sur auteur/depot — le jeton Hugging Face enregistré n'y donne pas accès (expiré, ou conditions non acceptées avec ce compte)"
|
||||
},
|
||||
{
|
||||
"status": 403,
|
||||
"code": "",
|
||||
"url": "https://exemple.org/m.gguf",
|
||||
"token_set": false,
|
||||
"err": "accès refusé par la source — puis renseigne le jeton Hugging Face (réglage « Jeton Hugging Face », ou variable d'environnement HF_TOKEN)"
|
||||
},
|
||||
{
|
||||
"status": 404,
|
||||
"code": "EntryNotFound",
|
||||
"url": "https://huggingface.co/auteur/depot/resolve/main/m.gguf",
|
||||
"token_set": false,
|
||||
"err": "fichier « m.gguf » absent du dépôt (HTTP 404) — dépôt réorganisé depuis la copie du lien, ou fragment jamais publié (table PLE livrée à part ?)"
|
||||
},
|
||||
{
|
||||
"status": 404,
|
||||
"code": "EntryNotFound",
|
||||
"url": "https://exemple.org/",
|
||||
"token_set": false,
|
||||
"err": "fichier absent du dépôt (HTTP 404) — dépôt réorganisé depuis la copie du lien, ou fragment jamais publié (table PLE livrée à part ?)"
|
||||
},
|
||||
{
|
||||
"status": 404,
|
||||
"code": "RevisionNotFound",
|
||||
"url": "https://huggingface.co/auteur/depot/resolve/main/m.gguf",
|
||||
"token_set": false,
|
||||
"err": "révision introuvable dans le dépôt (HTTP 404)"
|
||||
},
|
||||
{
|
||||
"status": 404,
|
||||
"code": "",
|
||||
"url": "https://huggingface.co/auteur/depot/resolve/main/m.gguf",
|
||||
"token_set": false,
|
||||
"err": "lien introuvable (HTTP 404)"
|
||||
},
|
||||
{
|
||||
"status": 416,
|
||||
"code": "",
|
||||
"url": "https://huggingface.co/auteur/depot/resolve/main/m.gguf",
|
||||
"token_set": false,
|
||||
"err": "la source refuse la plage d'octets demandée (HTTP 416) — fichier modifié pendant le transfert ?"
|
||||
},
|
||||
{
|
||||
"status": 429,
|
||||
"code": "",
|
||||
"url": "https://huggingface.co/auteur/depot/resolve/main/m.gguf",
|
||||
"token_set": false,
|
||||
"err": "trop de requêtes vers la source (HTTP 429) — réessaie dans quelques minutes"
|
||||
},
|
||||
{
|
||||
"status": 502,
|
||||
"code": "",
|
||||
"url": "https://huggingface.co/auteur/depot/resolve/main/m.gguf",
|
||||
"token_set": false,
|
||||
"err": "la source est en panne (HTTP 502) — réessaie plus tard"
|
||||
},
|
||||
{
|
||||
"status": 418,
|
||||
"code": "",
|
||||
"url": "https://huggingface.co/auteur/depot/resolve/main/m.gguf",
|
||||
"token_set": false,
|
||||
"err": "HTTP 418 depuis la source"
|
||||
}
|
||||
],
|
||||
"hf_host": {
|
||||
"HUGGINGFACE.CO:443": true,
|
||||
"cdn-lfs.huggingface.co": true,
|
||||
"evilhuggingface.co": false,
|
||||
"exemple.org": false,
|
||||
"hf.co": true,
|
||||
"huggingface.co": true,
|
||||
"huggingface.co.evil.org": false,
|
||||
"x.hf.co": true
|
||||
},
|
||||
"hf_repo": {
|
||||
"https://exemple.org/a/b/resolve/main/x.gguf": "",
|
||||
"https://huggingface.co/-a/b/resolve/main/x.gguf": "",
|
||||
"https://huggingface.co/a/b": "",
|
||||
"https://huggingface.co/a/b/blob/main/x.gguf": "a/b",
|
||||
"https://huggingface.co/a/b/tree/main": "",
|
||||
"https://huggingface.co/auteur/depot/resolve/main/m.gguf": "auteur/depot"
|
||||
},
|
||||
"dest": [
|
||||
{
|
||||
"name": "a.gguf",
|
||||
"dir": "",
|
||||
"path": "{{HOME}}/models/a.gguf"
|
||||
},
|
||||
{
|
||||
"name": " ../../x.gguf ",
|
||||
"dir": "",
|
||||
"path": "{{HOME}}/models/x.gguf"
|
||||
},
|
||||
{
|
||||
"name": "x.bin",
|
||||
"dir": "",
|
||||
"err": "seuls les fichiers .gguf sont acceptés"
|
||||
},
|
||||
{
|
||||
"name": "",
|
||||
"dir": "",
|
||||
"err": "nom de modèle invalide"
|
||||
},
|
||||
{
|
||||
"name": "/",
|
||||
"dir": "",
|
||||
"err": "nom de modèle invalide"
|
||||
},
|
||||
{
|
||||
"name": "a.gguf",
|
||||
"dir": "/etc",
|
||||
"err": "dossier non autorisé : /etc — ajoute-le dans « Dossiers de modèles »"
|
||||
},
|
||||
{
|
||||
"name": "a.gguf",
|
||||
"dir": "{{HOME}}/disque",
|
||||
"path": "{{HOME}}/disque/a.gguf"
|
||||
},
|
||||
{
|
||||
"name": "a.gguf",
|
||||
"dir": "{{HOME}}/disque/",
|
||||
"path": "{{HOME}}/disque/a.gguf"
|
||||
},
|
||||
{
|
||||
"name": "a.gguf",
|
||||
"dir": "{{HOME}}/models",
|
||||
"path": "{{HOME}}/models/a.gguf"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -100,6 +100,7 @@ async fn facade(backend: SocketAddr) -> (SocketAddr, oneshot::Sender<()>) {
|
||||
service: "loki-engine".into(),
|
||||
},
|
||||
chat: None,
|
||||
downloads: None,
|
||||
};
|
||||
tokio::spawn(loki_rs::proxy::serve(l, cfg, async move {
|
||||
let _ = rx.await;
|
||||
|
||||
Reference in new issue
Block a user