Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,10 @@ namespace OpenDAL.ServiceConfig
/// </summary>
public sealed class HuggingfaceServiceConfig : IServiceConfig
{
/// <summary>
/// Disable loading config from the environment. When set, the service ignores `HF_ENDPOINT`, `HF_HUB_DISABLE_XET`, `HF_TOKEN`, `HF_TOKEN_PATH`, and the `token` file under `HF_HOME`, `$XDG_CACHE_HOME/huggingface` or `~/.cache/huggingface`.
/// </summary>
public bool? DisableConfigLoad { get; init; }
/// <summary>
/// Download mode. Either `xet` (default) or `http`. When unset, the mode is resolved from the `HF_HUB_DISABLE_XET` environment variable: a non-empty value forces `http`, otherwise it defaults to `xet`. An explicit value here takes precedence. See &lt;https://huggingface.co/docs/huggingface_hub/package_reference/environment_variables#hfhubdisablexet&gt;.
/// </summary>
Expand Down Expand Up @@ -66,6 +70,10 @@ public sealed class HuggingfaceServiceConfig : IServiceConfig
public IReadOnlyDictionary<string, string> ToOptions()
{
var map = new Dictionary<string, string>();
if (DisableConfigLoad is not null)
{
map["disable_config_load"] = Utilities.ToOptionString(DisableConfigLoad);
}
if (DownloadMode is not null)
{
map["download_mode"] = Utilities.ToOptionString(DownloadMode);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1687,6 +1687,13 @@ public Map<String, String> configMap() {
@Data
@RequiredArgsConstructor(access = AccessLevel.PRIVATE)
class Hf implements ServiceConfig {
/**
* <p>Disable loading config from the environment.</p>
* <p>When set, the service ignores <code>HF_ENDPOINT</code>, <code>HF_HUB_DISABLE_XET</code>,
* <code>HF_TOKEN</code>, <code>HF_TOKEN_PATH</code>, and the <code>token</code> file under <code>HF_HOME</code>,
* <code>$XDG_CACHE_HOME/huggingface</code> or <code>~/.cache/huggingface</code>.</p>
*/
public final Boolean disableConfigLoad;
/**
* <p>Download mode. Either <code>xet</code> (default) or <code>http</code>.</p>
* <p>When unset, the mode is resolved from the <code>HF_HUB_DISABLE_XET</code>
Expand Down Expand Up @@ -1743,6 +1750,9 @@ public String scheme() {
@Override
public Map<String, String> configMap() {
final HashMap<String, String> map = new HashMap<>();
if (disableConfigLoad != null) {
map.put("disable_config_load", String.valueOf(disableConfigLoad));
}
if (downloadMode != null) {
map.put("download_mode", downloadMode);
}
Expand Down
2 changes: 2 additions & 0 deletions bindings/python/python/opendal/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -408,6 +408,8 @@ class HfConfig(TypedDict):

scheme: Required[Literal["hf"]]
"""The service scheme; fixed to `"hf"`."""
disable_config_load: NotRequired[bool]
"""Disable loading config from the environment. When set, the service ignores `HF_ENDPOINT`, `HF_HUB_DISABLE_XET`, `HF_TOKEN`, `HF_TOKEN_PATH`, and the `token` file under `HF_HOME`, `$XDG_CACHE_HOME/huggingface` or `~/.cache/huggingface`."""
download_mode: NotRequired[str]
"""Download mode. Either `xet` (default) or `http`. When unset, the mode is resolved from the `HF_HUB_DISABLE_XET` environment variable: a non-empty value forces `http`, otherwise it defaults to `xet`. An explicit value here takes precedence. See <https://huggingface.co/docs/huggingface_hub/package_reference/environment_variables#hfhubdisablexet>."""
enable_resolve_cache: NotRequired[bool]
Expand Down
98 changes: 90 additions & 8 deletions core/services/hf/src/backend.rs
Original file line number Diff line number Diff line change
Expand Up @@ -167,27 +167,43 @@ impl HfBuilder {
self
}

/// Disable loading config from the environment.
///
/// See [`HfConfig::disable_config_load`] for details.
pub fn disable_config_load(mut self) -> Self {
self.config.disable_config_load = true;
self
}

/// Resolve the Hub base URL: an explicit config value wins, then
/// `HF_ENDPOINT`, then the public Hub. A trailing slash is trimmed
/// because every URL is built by appending `/api/...` to this, and HF
/// answers the resulting `//api/...` with a 404.
/// `HF_ENDPOINT` unless disable_config_load is set, then the public Hub.
/// A trailing slash is trimmed because every URL is built by appending
/// `/api/...` to this, and HF answers the resulting `//api/...` with a 404.
fn hf_endpoint(&self) -> String {
self.config
.endpoint
.clone()
.or_else(|| std::env::var("HF_ENDPOINT").ok())
.or_else(|| {
if self.config.disable_config_load {
None
} else {
std::env::var("HF_ENDPOINT").ok()
}
})
.unwrap_or_else(|| "https://huggingface.co".to_string())
.trim_end_matches('/')
.to_string()
}

/// Resolve the download mode: an explicit config value wins; otherwise a set,
/// non-empty HF_HUB_DISABLE_XET (a huggingface_hub env var) forces http; default Xet.
/// Resolve the download mode: an explicit config value wins; otherwise, unless
/// disable_config_load is set, a set, non-empty HF_HUB_DISABLE_XET (a
/// huggingface_hub env var) forces http; default Xet.
fn hf_download_mode(&self) -> HfDownloadMode {
if let Some(mode) = self.config.download_mode {
return mode;
}
if let Ok(val) = std::env::var("HF_HUB_DISABLE_XET")
if !self.config.disable_config_load
&& let Ok(val) = std::env::var("HF_HUB_DISABLE_XET")
&& !val.is_empty()
{
return HfDownloadMode::Http;
Expand All @@ -207,11 +223,15 @@ impl HfBuilder {
}

/// Resolve the authentication token using the same priority order as hf-hub:
/// explicit config → HF_HUB_DISABLE_IMPLICIT_TOKEN check → HF_TOKEN env → token file.
/// explicit config → disable_config_load → HF_HUB_DISABLE_IMPLICIT_TOKEN check →
/// HF_TOKEN env → token file.
fn hf_token(&self) -> Option<String> {
if let Some(t) = self.config.token.clone() {
return Some(t);
}
if self.config.disable_config_load {
return None;
}
if let Ok(val) = std::env::var("HF_HUB_DISABLE_IMPLICIT_TOKEN")
&& !val.is_empty()
{
Expand Down Expand Up @@ -558,6 +578,54 @@ mod tests {
assert_eq!(result, None);
}

#[test]
fn hf_token_disable_config_load_ignores_env() {
let _guard = ENV_LOCK.lock().unwrap();
let dir = std::env::temp_dir().join("opendal-hf-disable-config-load");
std::fs::create_dir_all(dir.join("huggingface")).unwrap();
std::fs::write(dir.join("token"), "file-token").unwrap();
std::fs::write(dir.join("huggingface/token"), "xdg-token").unwrap();

// Each source alone yields a token, unless config loading is disabled.
let keys = ["HF_TOKEN", "HF_TOKEN_PATH", "HF_HOME", "XDG_CACHE_HOME"];
let values = [
std::ffi::OsString::from("env-token"),
dir.join("token").into(),
dir.clone().into(),
dir.clone().into(),
];
let saved = keys.map(std::env::var_os);
unsafe { std::env::remove_var("HF_HUB_DISABLE_IMPLICIT_TOKEN") };
let mut results = Vec::new();
for (key, value) in keys.iter().zip(&values) {
for key in keys {
unsafe { std::env::remove_var(key) };
}
unsafe { std::env::set_var(key, value) };
results.push((
*key,
builder_no_token().hf_token(),
builder_no_token().disable_config_load().hf_token(),
));
}
let explicit = builder_with_token("config-token")
.disable_config_load()
.hf_token();
for (key, value) in keys.iter().zip(saved) {
match value {
Some(value) => unsafe { std::env::set_var(key, value) },
None => unsafe { std::env::remove_var(key) },
}
}
std::fs::remove_dir_all(&dir).ok();

for (key, loaded, disabled) in results {
assert!(loaded.is_some(), "{key} should provide a token");
assert_eq!(disabled, None, "{key} should be ignored");
}
assert_eq!(explicit.as_deref(), Some("config-token"));
}

#[test]
fn hf_token_reads_from_file_via_hf_token_path() {
let _guard = ENV_LOCK.lock().unwrap();
Expand Down Expand Up @@ -646,6 +714,20 @@ mod tests {
assert_eq!(mode, HfDownloadMode::Xet);
}

#[test]
fn disable_config_load_ignores_endpoint_and_download_mode_env() {
let _guard = ENV_LOCK.lock().unwrap();
unsafe { std::env::set_var("HF_ENDPOINT", "https://env.example.com") };
unsafe { std::env::set_var("HF_HUB_DISABLE_XET", "1") };
let builder = HfBuilder::default().disable_config_load();
let endpoint = builder.hf_endpoint();
let mode = builder.hf_download_mode();
unsafe { std::env::remove_var("HF_ENDPOINT") };
unsafe { std::env::remove_var("HF_HUB_DISABLE_XET") };
assert_eq!(endpoint, "https://huggingface.co");
assert_eq!(mode, HfDownloadMode::Xet);
}

#[tokio::test]
async fn build_resolve_cache_requires_opt_in() -> Result<()> {
use super::super::core::test_utils::MockHttpTransport;
Expand Down
40 changes: 40 additions & 0 deletions core/services/hf/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,12 @@ pub struct HfConfig {
/// option is disabled.
/// See [`HfBuilder::enable_resolve_cache`] for freshness semantics.
pub enable_resolve_cache: bool,
/// Disable loading config from the environment.
///
/// When set, the service ignores `HF_ENDPOINT`, `HF_HUB_DISABLE_XET`,
/// `HF_TOKEN`, `HF_TOKEN_PATH`, and the `token` file under `HF_HOME`,
/// `$XDG_CACHE_HOME/huggingface` or `~/.cache/huggingface`.
pub disable_config_load: bool,
}

impl Debug for HfConfig {
Expand All @@ -83,6 +89,7 @@ impl Debug for HfConfig {
.field("root", &self.root)
.field("download_mode", &self.download_mode)
.field("enable_resolve_cache", &self.enable_resolve_cache)
.field("disable_config_load", &self.disable_config_load)
.finish_non_exhaustive()
}
}
Expand Down Expand Up @@ -115,6 +122,18 @@ impl opendal_core::Configurator for HfConfig {
.get("download_mode")
.map(|s| HfDownloadMode::parse(s))
.transpose()?;
let disable_config_load = opts
.get("disable_config_load")
.map(|value| value.parse::<bool>())
.transpose()
.map_err(|err| {
opendal_core::Error::new(
opendal_core::ErrorKind::ConfigInvalid,
"disable_config_load must be true or false",
)
.set_source(err)
})?
.unwrap_or_default();

let enable_resolve_cache = opts
.get("enable_resolve_cache")
Expand All @@ -141,6 +160,7 @@ impl opendal_core::Configurator for HfConfig {
endpoint: opts.get("endpoint").cloned(),
download_mode,
enable_resolve_cache,
disable_config_load,
})
} else {
// Bare scheme from via_iter, all config is in options.
Expand All @@ -163,6 +183,7 @@ impl opendal_core::Configurator for HfConfig {
endpoint: opts.get("endpoint").cloned(),
download_mode,
enable_resolve_cache,
disable_config_load,
})
}
}
Expand Down Expand Up @@ -217,6 +238,25 @@ mod tests {
assert_eq!(cfg.root.as_deref(), Some("/testdata/"));
}

#[test]
fn from_uri_disable_config_load() {
let parse = |value: &str| {
let uri = OperatorUri::new(
"huggingface",
[
("repo_type".to_string(), "dataset".to_string()),
("disable_config_load".to_string(), value.to_string()),
],
)
.unwrap();
HfConfig::from_uri(&uri).map(|cfg| cfg.disable_config_load)
};

assert!(parse("true").unwrap());
assert!(!parse("false").unwrap());
assert!(parse("yes").is_err());
}

#[test]
fn from_uri_download_mode_http() {
let uri = OperatorUri::new(
Expand Down
Loading
Loading