Skip to content

Commit 0897af3

Browse files
committed
feat(lumen-app): hf_client helper with HF_TOKEN auto-attach + startup detection log
Single `models::hf_client(timeout)` builder consolidates all HuggingFace HTTP touchpoints (`fetch_hub_sha`, `models::download`, periodic `check_model_updates`). When `HF_TOKEN` (or legacy `HUGGING_FACE_HUB_TOKEN`) env var is set, attaches `Authorization: Bearer <token>` as a default header on every request — unlocks gated HF repos and raises the anonymous rate-limit ceiling for the update-check sweep. Token is read from env only; no persistence in config.toml (avoids plaintext-secret storage; future iteration can add Keychain). Startup log reports detection status by env var name only, never the token value, so users see "[lumen-app] HuggingFace token detected via $HF_TOKEN — gated repos accessible" if it's set, or a clear "no HuggingFace token configured" otherwise.
1 parent 26cd2a5 commit 0897af3

3 files changed

Lines changed: 74 additions & 9 deletions

File tree

crates/lumen-app/src/commands.rs

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -288,11 +288,12 @@ pub async fn check_model_updates(
288288
let entries = models::scan_local(&dir, &cat).map_err(err)?;
289289
drop(cat);
290290

291-
let client = reqwest::Client::builder()
292-
.user_agent(concat!("lumen-app/", env!("CARGO_PKG_VERSION")))
293-
.timeout(std::time::Duration::from_secs(8))
294-
.build()
295-
.map_err(|e| e.to_string())?;
291+
// 8s timeout — anonymous HF API replies in <200ms; if we hit the cap
292+
// something is wrong with the network and we'd rather skip the update
293+
// check than wedge the UI. `hf_client` attaches HF_TOKEN automatically
294+
// when set, so private/gated repos work for users who configured one.
295+
let client =
296+
models::hf_client(Some(std::time::Duration::from_secs(8))).map_err(|e| e.to_string())?;
296297

297298
let mut results = Vec::with_capacity(repo_ids.len());
298299
let mut new_outdated = std::collections::HashSet::new();

crates/lumen-app/src/main.rs

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,36 @@ use state::AppState;
1717
use tauri::Manager;
1818

1919
fn main() {
20+
// Surface HF_TOKEN detection at startup so users who configured one
21+
// can see it took effect (no token value is logged — only its
22+
// presence + source env var). Token unlocks gated HF repos and
23+
// raises the anonymous rate limit ceiling for periodic update checks.
24+
let hf_token_src = if std::env::var("HF_TOKEN")
25+
.ok()
26+
.filter(|t| !t.trim().is_empty())
27+
.is_some()
28+
{
29+
Some("HF_TOKEN")
30+
} else if std::env::var("HUGGING_FACE_HUB_TOKEN")
31+
.ok()
32+
.filter(|t| !t.trim().is_empty())
33+
.is_some()
34+
{
35+
Some("HUGGING_FACE_HUB_TOKEN")
36+
} else {
37+
None
38+
};
39+
match hf_token_src {
40+
Some(name) => {
41+
eprintln!("[lumen-app] HuggingFace token detected via ${name} — gated repos accessible");
42+
}
43+
None => {
44+
eprintln!(
45+
"[lumen-app] no HuggingFace token configured (HF_TOKEN / HUGGING_FACE_HUB_TOKEN); anonymous HF access only — gated repos will 401"
46+
);
47+
}
48+
}
49+
2050
tauri::Builder::default()
2151
.plugin(tauri_plugin_dialog::init())
2252
.plugin(tauri_plugin_fs::init())

crates/lumen-app/src/models.rs

Lines changed: 38 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -445,6 +445,43 @@ pub fn local_path_for(models_dir: &Path, repo_id: &str) -> PathBuf {
445445
models_dir.join(flat)
446446
}
447447

448+
/// Build a `reqwest::Client` configured for HuggingFace Hub calls.
449+
///
450+
/// Attaches `Authorization: Bearer <token>` automatically when one of the
451+
/// well-known HF token env vars is set:
452+
/// - `HF_TOKEN` (preferred; matches `huggingface_hub` Python lib + the
453+
/// `huggingface-cli login` convention)
454+
/// - `HUGGING_FACE_HUB_TOKEN` (legacy fallback)
455+
///
456+
/// Without a token the client behaves identically to before — anonymous
457+
/// access works for any non-gated repo.
458+
///
459+
/// Single source of truth so `fetch_hub_sha`, `download`, and the
460+
/// periodic `check_model_updates` path all auth-up consistently. Adding a
461+
/// new HTTP call site? Use this builder, don't roll your own.
462+
pub fn hf_client(timeout: Option<std::time::Duration>) -> Result<reqwest::Client> {
463+
let token = std::env::var("HF_TOKEN")
464+
.ok()
465+
.or_else(|| std::env::var("HUGGING_FACE_HUB_TOKEN").ok())
466+
.map(|t| t.trim().to_string())
467+
.filter(|t| !t.is_empty());
468+
let mut builder = reqwest::Client::builder()
469+
.user_agent(concat!("lumen-app/", env!("CARGO_PKG_VERSION")));
470+
if let Some(d) = timeout {
471+
builder = builder.timeout(d);
472+
}
473+
if let Some(t) = &token {
474+
use reqwest::header::{AUTHORIZATION, HeaderMap, HeaderValue};
475+
let mut headers = HeaderMap::new();
476+
let mut val = HeaderValue::try_from(format!("Bearer {t}"))
477+
.context("invalid HF_TOKEN — must be ASCII (no newline / non-printable bytes)")?;
478+
val.set_sensitive(true);
479+
headers.insert(AUTHORIZATION, val);
480+
builder = builder.default_headers(headers);
481+
}
482+
builder.build().context("init hf http client")
483+
}
484+
448485
/// Latest commit SHA for the `main` branch of an HF Hub repo. Used to detect
449486
/// "same repo id, new weights" the way `hsng95/gemma-4-26b-a4b-mlx-imatrix3plus-awq` was
450487
/// rebuilt in v0.1.3 — the old broken-3bit and the new imatrix mixed-precision
@@ -517,10 +554,7 @@ pub async fn download(
517554
files: Option<Vec<String>>,
518555
tx: tokio::sync::mpsc::Sender<DownloadProgress>,
519556
) -> Result<PathBuf> {
520-
let client = reqwest::Client::builder()
521-
.user_agent(concat!("lumen-app/", env!("CARGO_PKG_VERSION")))
522-
.build()
523-
.context("init http client")?;
557+
let client = hf_client(None).context("init hf http client for download")?;
524558

525559
let target = local_path_for(models_dir, repo_id);
526560
std::fs::create_dir_all(&target)

0 commit comments

Comments
 (0)