fix(model-store): 接受 HF tree API 返回的裸 LFS oid,修复本地模型下载失败 (#1072)

* test(model-store): 复现 HF tree API 返回裸 LFS oid 时模型下载被拒

huggingface.co 与 hf-mirror.com 的 /api/models/{repo}/tree 接口里 lfs.oid
是不带 sha256: 前缀的 64 位十六进制,现有解析只认前缀形式,导致本地模型
下载直接报 unsupported Hugging Face LFS oid。

* fix(model-store): 接受 HF tree API 返回的裸 LFS oid,修复本地模型下载失败

2.0 起 parse_lfs_sha256 只接受 sha256: 前缀,但 huggingface.co 与
hf-mirror.com 的 tree 接口返回的是裸 64 位十六进制,导致下载 Qwen3-ASR
等本地模型时直接报 unsupported Hugging Face LFS oid。改为前缀可选,
仍校验 64 位十六进制。
This commit is contained in:
EddieLin authored and GitHub committed 2026-09-13 18:04:51 +08:00
1 parent c8bd7aa295
commit 4d011df66a
1 file changed
+28 -3
@@ -2485,10 +2485,10 @@ fn parse_hf_tree_page_for_entry(
Ok(files)
}
/// The Hugging Face tree API returns `lfs.oid` as a bare SHA-256 hex digest;
/// the `sha256:`-prefixed form (as in Git LFS pointer files) is accepted too.
fn parse_lfs_sha256(oid: &str) -> Result<String, BackendError> {
let digest = oid
.strip_prefix("sha256:")
.ok_or_else(|| invalid("unsupported Hugging Face LFS oid"))?;
let digest = oid.strip_prefix("sha256:").unwrap_or(oid);
if digest.len() != 64 || !digest.bytes().all(|byte| byte.is_ascii_hexdigit()) {
return Err(invalid("invalid Hugging Face LFS sha256 oid"));
}
@@ -2940,6 +2940,31 @@ mod tests {
.is_err());
}
#[test]
fn hf_tree_accepts_bare_lfs_oid_from_live_api() {
// Shape returned by huggingface.co and hf-mirror.com for Qwen/Qwen3-ASR-0.6B
// (2026-09-13): `lfs.oid` is the bare hex digest, without a `sha256:` prefix.
let entries = vec![serde_json::json!({
"type": "file",
"oid": "3d4b2a1f0e9c8b7a6d5e4f3a2b1c0d9e8f7a6b5c",
"size": 1876091704u64,
"path": "model.safetensors",
"lfs": {
"oid": "79d6cbd4c98c7bbffe9db2edac07f56cd6637d0d5944b27f6c2b8353840323ea",
"size": 1876091704u64,
"pointerSize": 135
}
})];
let files = parse_hf_tree_page("Qwen/Qwen3-ASR-0.6B", "qwen3-asr-0.6b", &entries).unwrap();
assert_eq!(files.len(), 1);
assert_eq!(files[0].path, "model.safetensors");
assert_eq!(files[0].size_bytes, 1_876_091_704);
assert_eq!(
files[0].sha256.as_deref(),
Some("79d6cbd4c98c7bbffe9db2edac07f56cd6637d0d5944b27f6c2b8353840323ea")
);
}
#[test]
fn hf_link_pagination_accepts_same_origin_and_rejects_redirected_origin() {
let current = "https://huggingface.co/api/models/org/model/tree/main?limit=1000";