1pub mod commits;
23pub mod diff;
24pub mod download;
25pub mod files;
26pub mod listing;
27pub mod repo_type;
28pub mod upload;
29
30use std::collections::HashMap;
31use std::str::FromStr;
32
33use bon::bon;
34pub use commits::{CommitAuthor, DiffEntry, GitCommitInfo, GitRefInfo, GitRefs};
35pub use diff::{GitStatus, HFDiffParseError, HFFileDiff};
36pub use files::{
37 AddSource, BlobLfsInfo, BlobSecurityInfo, CommitInfo, CommitOperation, FileMetadataInfo, LastCommitInfo,
38 RepoTreeEntry, SourceByteStream, StreamFactory, StreamSource,
39};
40#[cfg(not(target_family = "wasm"))]
41pub(crate) use files::{extract_file_size, extract_xet_hash};
42use futures::Stream;
43pub use repo_type::{RepoType, RepoTypeAny, RepoTypeDataset, RepoTypeKernel, RepoTypeModel, RepoTypeSpace};
44use serde::de::DeserializeOwned;
45use serde::{Deserialize, Serialize, Serializer};
46use url::Url;
47
48use crate::client::HFClient;
49use crate::error::{HFError, HFResult};
50use crate::{constants, retry};
51
52#[derive(Debug, Clone)]
57pub enum GatedApprovalMode {
58 Disabled,
60 Auto,
62 Manual,
64}
65
66#[derive(Debug, Clone, Serialize)]
68#[serde(rename_all = "kebab-case")]
69pub enum GatedNotificationsMode {
70 Bulk,
72 RealTime,
74}
75
76#[derive(Debug, Clone)]
83pub struct GatedNotifications {
84 pub mode: GatedNotificationsMode,
86 pub email: Option<String>,
89}
90
91impl GatedNotifications {
92 pub fn new(mode: GatedNotificationsMode) -> Self {
94 Self { mode, email: None }
95 }
96
97 pub fn with_email(mut self, email: impl Into<String>) -> Self {
99 self.email = Some(email.into());
100 self
101 }
102}
103
104#[derive(Debug, Clone, Serialize, Deserialize)]
109pub struct RepoSibling {
110 pub rfilename: String,
112 pub size: Option<u64>,
114 pub lfs: Option<BlobLfsInfo>,
117}
118
119#[derive(Debug, Clone, Deserialize, Serialize)]
121pub struct SafeTensorsInfo {
122 pub parameters: HashMap<String, u64>,
124 pub total: u64,
126}
127
128#[derive(Debug, Clone, Deserialize, Serialize)]
130#[serde(rename_all = "snake_case")]
131pub struct TransformersInfo {
132 pub auto_model: String,
134 #[serde(default)]
136 pub custom_class: Option<String>,
137 #[serde(default)]
139 pub pipeline_tag: Option<String>,
140 #[serde(default)]
142 pub processor: Option<String>,
143}
144
145#[derive(Debug, Clone, Deserialize, Serialize)]
150#[serde(rename_all = "camelCase")]
151pub struct InferenceProviderMapping {
152 pub provider: String,
154 pub provider_id: String,
156 pub status: String,
158 pub task: String,
160 #[serde(default)]
162 pub adapter: Option<String>,
163 #[serde(default)]
165 pub adapter_weights_path: Option<String>,
166 #[serde(default, rename = "type")]
168 pub r#type: Option<String>,
169}
170
171fn deserialize_inference_provider_mapping<'de, D>(
178 deserializer: D,
179) -> Result<Option<Vec<InferenceProviderMapping>>, D::Error>
180where
181 D: serde::de::Deserializer<'de>,
182{
183 use serde::de::Error;
184 let value = Option::<serde_json::Value>::deserialize(deserializer)?;
185 let Some(value) = value else { return Ok(None) };
186 match value {
187 serde_json::Value::Null => Ok(None),
188 serde_json::Value::Array(items) => {
189 let mut out = Vec::with_capacity(items.len());
190 for item in items {
191 out.push(serde_json::from_value(item).map_err(D::Error::custom)?);
192 }
193 Ok(Some(out))
194 },
195 serde_json::Value::Object(map) => {
196 let mut out = Vec::with_capacity(map.len());
197 for (provider, mut value) in map {
198 if let serde_json::Value::Object(ref mut obj) = value {
199 obj.insert("provider".to_string(), serde_json::Value::String(provider));
200 }
201 out.push(serde_json::from_value(value).map_err(D::Error::custom)?);
202 }
203 Ok(Some(out))
204 },
205 _ => Err(D::Error::custom("expected list or object for inferenceProviderMapping")),
206 }
207}
208
209#[derive(Debug, Clone, Deserialize, Serialize)]
213pub struct EvalResultEntry {
214 pub dataset: EvalResultDataset,
216 pub value: serde_json::Value,
218 #[serde(default, rename = "verifyToken")]
220 pub verify_token: Option<String>,
221 #[serde(default)]
223 pub date: Option<String>,
224 #[serde(default)]
226 pub source: Option<EvalResultSource>,
227 #[serde(default)]
229 pub notes: Option<String>,
230}
231
232#[derive(Debug, Clone, Deserialize, Serialize)]
234pub struct EvalResultDataset {
235 pub id: String,
237 pub task_id: String,
239 #[serde(default)]
241 pub revision: Option<String>,
242}
243
244#[derive(Debug, Clone, Deserialize, Serialize)]
246pub struct EvalResultSource {
247 #[serde(default)]
249 pub url: Option<String>,
250 #[serde(default)]
252 pub name: Option<String>,
253 #[serde(default)]
255 pub user: Option<String>,
256 #[serde(default)]
258 pub org: Option<String>,
259}
260
261#[derive(Debug, Clone, Deserialize, Serialize)]
267#[serde(rename_all = "camelCase")]
268pub struct ModelInfo {
269 pub id: String,
271 #[serde(rename = "_id")]
273 pub internal_id: Option<String>,
274 pub author: Option<String>,
276 #[serde(default)]
278 pub base_models: Option<Vec<String>>,
279 pub card_data: Option<serde_json::Value>,
282 pub children_model_count: Option<u64>,
284 pub config: Option<serde_json::Value>,
286 pub created_at: Option<String>,
289 pub disabled: Option<bool>,
291 pub downloads: Option<u64>,
293 pub downloads_all_time: Option<u64>,
295 #[serde(default, rename = "evalResults")]
297 pub eval_results: Option<Vec<EvalResultEntry>>,
298 pub gated: Option<serde_json::Value>,
301 pub gguf: Option<serde_json::Value>,
303 pub inference: Option<String>,
306 #[serde(default, deserialize_with = "deserialize_inference_provider_mapping")]
311 pub inference_provider_mapping: Option<Vec<InferenceProviderMapping>>,
312 pub last_modified: Option<String>,
314 #[serde(rename = "library_name")]
316 pub library_name: Option<String>,
317 pub likes: Option<u64>,
319 #[serde(rename = "mask_token")]
321 pub mask_token: Option<String>,
322 #[serde(rename = "model-index")]
324 pub model_index: Option<serde_json::Value>,
325 #[serde(rename = "pipeline_tag")]
327 pub pipeline_tag: Option<String>,
328 pub private: Option<bool>,
330 pub resource_group: Option<serde_json::Value>,
332 pub safetensors: Option<SafeTensorsInfo>,
334 pub security_repo_status: Option<serde_json::Value>,
336 pub sha: Option<String>,
338 pub siblings: Option<Vec<RepoSibling>>,
341 pub spaces: Option<Vec<String>>,
343 pub tags: Option<Vec<String>>,
346 pub transformers_info: Option<TransformersInfo>,
348 pub trending_score: Option<f64>,
350 pub used_storage: Option<u64>,
352 pub widget_data: Option<serde_json::Value>,
354}
355
356#[derive(Debug, Clone, Deserialize, Serialize)]
362#[serde(rename_all = "camelCase")]
363pub struct DatasetInfo {
364 pub id: String,
366 #[serde(rename = "_id")]
368 pub internal_id: Option<String>,
369 pub author: Option<String>,
371 pub sha: Option<String>,
373 pub private: Option<bool>,
375 pub gated: Option<serde_json::Value>,
378 pub disabled: Option<bool>,
380 pub downloads: Option<u64>,
382 pub downloads_all_time: Option<u64>,
384 pub likes: Option<u64>,
386 pub tags: Option<Vec<String>>,
388 pub created_at: Option<String>,
391 pub last_modified: Option<String>,
393 pub siblings: Option<Vec<RepoSibling>>,
396 pub card_data: Option<serde_json::Value>,
398 pub citation: Option<String>,
400 #[serde(rename = "paperswithcode_id")]
402 pub paperswithcode_id: Option<String>,
403 pub resource_group: Option<serde_json::Value>,
405 pub trending_score: Option<f64>,
407 pub description: Option<String>,
409 pub used_storage: Option<u64>,
411}
412
413#[derive(Debug, Clone, Deserialize, Serialize)]
419#[serde(rename_all = "camelCase")]
420pub struct SpaceInfo {
421 pub id: String,
423 #[serde(rename = "_id")]
425 pub internal_id: Option<String>,
426 pub author: Option<String>,
428 pub sha: Option<String>,
430 pub private: Option<bool>,
432 pub gated: Option<serde_json::Value>,
435 pub disabled: Option<bool>,
437 pub likes: Option<u64>,
439 pub tags: Option<Vec<String>>,
441 pub created_at: Option<String>,
444 pub last_modified: Option<String>,
446 pub siblings: Option<Vec<RepoSibling>>,
449 pub card_data: Option<serde_json::Value>,
451 pub sdk: Option<String>,
453 pub trending_score: Option<f64>,
455 pub host: Option<String>,
457 pub subdomain: Option<String>,
459 pub runtime: Option<crate::spaces::SpaceRuntime>,
464 pub datasets: Option<Vec<String>>,
466 pub models: Option<Vec<String>>,
468 pub resource_group: Option<serde_json::Value>,
470 pub used_storage: Option<u64>,
472}
473
474#[derive(Debug, Clone, Deserialize, Serialize)]
486#[serde(rename_all = "camelCase")]
487pub struct KernelInfo {
488 pub id: String,
490 #[serde(rename = "_id")]
492 pub internal_id: Option<String>,
493 pub author: Option<String>,
495 pub sha: Option<String>,
497 pub private: Option<bool>,
499 pub gated: Option<serde_json::Value>,
502 pub downloads: Option<u64>,
504 pub likes: Option<u64>,
506 pub last_modified: Option<String>,
508 pub trusted_publisher: Option<bool>,
511 pub supported_driver_families: Option<Vec<String>>,
514}
515
516#[derive(Debug, Clone, Deserialize)]
518pub struct RepoUrl {
519 pub url: String,
521}
522
523pub struct HFRepository<T: RepoType> {
546 pub(crate) hf_client: HFClient,
547 pub(super) owner: String,
548 pub(super) name: String,
549 pub(super) repo_type: T,
550}
551
552impl<T: RepoType> Clone for HFRepository<T> {
553 fn clone(&self) -> Self {
554 Self {
555 hf_client: self.hf_client.clone(),
556 owner: self.owner.clone(),
557 name: self.name.clone(),
558 repo_type: self.repo_type,
559 }
560 }
561}
562
563impl Serialize for GatedApprovalMode {
564 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
565 where
566 S: Serializer,
567 {
568 match self {
569 GatedApprovalMode::Disabled => serializer.serialize_bool(false),
570 GatedApprovalMode::Auto => serializer.serialize_str("auto"),
571 GatedApprovalMode::Manual => serializer.serialize_str("manual"),
572 }
573 }
574}
575
576impl FromStr for GatedApprovalMode {
577 type Err = HFError;
578
579 fn from_str(s: &str) -> Result<Self, Self::Err> {
580 match s.to_lowercase().as_str() {
581 "false" | "disabled" => Ok(GatedApprovalMode::Disabled),
582 "auto" => Ok(GatedApprovalMode::Auto),
583 "manual" => Ok(GatedApprovalMode::Manual),
584 _ => Err(HFError::InvalidParameter(format!(
585 "unknown gated approval mode: {s:?}. Expected 'auto', 'manual', or 'false'"
586 ))),
587 }
588 }
589}
590
591impl FromStr for GatedNotificationsMode {
592 type Err = HFError;
593
594 fn from_str(s: &str) -> Result<Self, Self::Err> {
595 match s.to_lowercase().as_str() {
596 "bulk" => Ok(GatedNotificationsMode::Bulk),
597 "real-time" | "realtime" => Ok(GatedNotificationsMode::RealTime),
598 _ => Err(HFError::InvalidParameter(format!(
599 "unknown gated notifications mode: {s:?}. Expected 'bulk' or 'real-time'"
600 ))),
601 }
602 }
603}
604
605impl<T: RepoType> std::fmt::Debug for HFRepository<T> {
606 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
607 f.debug_struct("HFRepository")
608 .field("owner", &self.owner)
609 .field("name", &self.name)
610 .field("repo_type", &self.repo_type.singular())
611 .finish()
612 }
613}
614
615impl HFClient {
616 pub fn repository<T: RepoType>(
630 &self,
631 repo_type: T,
632 owner: impl Into<String>,
633 name: impl Into<String>,
634 ) -> HFRepository<T> {
635 HFRepository::new(self.clone(), repo_type, owner, name)
636 }
637
638 pub fn model(&self, owner: impl Into<String>, name: impl Into<String>) -> HFRepository<RepoTypeModel> {
640 self.repository(RepoTypeModel, owner, name)
641 }
642
643 pub fn dataset(&self, owner: impl Into<String>, name: impl Into<String>) -> HFRepository<RepoTypeDataset> {
645 self.repository(RepoTypeDataset, owner, name)
646 }
647
648 pub fn space(&self, owner: impl Into<String>, name: impl Into<String>) -> HFRepository<RepoTypeSpace> {
650 self.repository(RepoTypeSpace, owner, name)
651 }
652
653 pub fn kernel(&self, owner: impl Into<String>, name: impl Into<String>) -> HFRepository<RepoTypeKernel> {
655 self.repository(RepoTypeKernel, owner, name)
656 }
657}
658
659#[bon]
660impl HFClient {
661 #[builder(finish_fn = send, derive(Debug, Clone))]
685 pub fn list_models(
686 &self,
687 #[builder(into)]
690 search: Option<String>,
691 #[builder(into)]
694 author: Option<String>,
695 #[builder(into)]
699 filter: Option<String>,
700 #[builder(into)]
704 sort: Option<String>,
705 #[builder(into)]
708 pipeline_tag: Option<String>,
709 full: Option<bool>,
711 card_data: Option<bool>,
713 fetch_config: Option<bool>,
715 limit: Option<usize>,
718 ) -> HFResult<impl Stream<Item = HFResult<ModelInfo>> + '_> {
719 let url = Url::parse(&format!("{}/api/models", self.endpoint()))?;
720 let mut query: Vec<(String, String)> = Vec::new();
721 if let Some(ref s) = search {
722 query.push(("search".into(), s.clone()));
723 }
724 if let Some(ref a) = author {
725 query.push(("author".into(), a.clone()));
726 }
727 if let Some(ref f) = filter {
728 query.push(("filter".into(), f.clone()));
729 }
730 if let Some(ref s) = sort {
731 query.push(("sort".into(), s.clone()));
732 }
733 if let Some(max) = limit {
734 if max < 1000 {
737 query.push(("limit".into(), max.to_string()));
738 }
739 }
740 if let Some(ref pt) = pipeline_tag {
741 query.push(("pipeline_tag".into(), pt.clone()));
742 }
743 if full == Some(true) {
744 query.push(("full".into(), "true".into()));
745 }
746 if card_data == Some(true) {
747 query.push(("cardData".into(), "true".into()));
748 }
749 if fetch_config == Some(true) {
750 query.push(("config".into(), "true".into()));
751 }
752 Ok(self.paginate(url, query, limit))
753 }
754
755 #[builder(finish_fn = send, derive(Debug, Clone))]
774 pub fn list_datasets(
775 &self,
776 #[builder(into)]
779 search: Option<String>,
780 #[builder(into)]
783 author: Option<String>,
784 #[builder(into)]
788 filter: Option<String>,
789 #[builder(into)]
792 sort: Option<String>,
793 full: Option<bool>,
795 limit: Option<usize>,
798 ) -> HFResult<impl Stream<Item = HFResult<DatasetInfo>> + '_> {
799 let url = Url::parse(&format!("{}/api/datasets", self.endpoint()))?;
800 let mut query: Vec<(String, String)> = Vec::new();
801 if let Some(ref s) = search {
802 query.push(("search".into(), s.clone()));
803 }
804 if let Some(ref a) = author {
805 query.push(("author".into(), a.clone()));
806 }
807 if let Some(ref f) = filter {
808 query.push(("filter".into(), f.clone()));
809 }
810 if let Some(ref s) = sort {
811 query.push(("sort".into(), s.clone()));
812 }
813 if let Some(max) = limit
814 && max < 1000
815 {
816 query.push(("limit".into(), max.to_string()));
817 }
818 if full == Some(true) {
819 query.push(("full".into(), "true".into()));
820 }
821 Ok(self.paginate(url, query, limit))
822 }
823
824 #[builder(finish_fn = send, derive(Debug, Clone))]
843 pub fn list_spaces(
844 &self,
845 #[builder(into)]
848 search: Option<String>,
849 #[builder(into)]
852 author: Option<String>,
853 #[builder(into)]
857 filter: Option<String>,
858 #[builder(into)]
861 sort: Option<String>,
862 full: Option<bool>,
864 limit: Option<usize>,
867 ) -> HFResult<impl Stream<Item = HFResult<SpaceInfo>> + '_> {
868 let url = Url::parse(&format!("{}/api/spaces", self.endpoint()))?;
869 let mut query: Vec<(String, String)> = Vec::new();
870 if let Some(ref s) = search {
871 query.push(("search".into(), s.clone()));
872 }
873 if let Some(ref a) = author {
874 query.push(("author".into(), a.clone()));
875 }
876 if let Some(ref f) = filter {
877 query.push(("filter".into(), f.clone()));
878 }
879 if let Some(ref s) = sort {
880 query.push(("sort".into(), s.clone()));
881 }
882 if let Some(max) = limit
883 && max < 1000
884 {
885 query.push(("limit".into(), max.to_string()));
886 }
887 if full == Some(true) {
888 query.push(("full".into(), "true".into()));
889 }
890 Ok(self.paginate(url, query, limit))
891 }
892
893 #[builder(finish_fn = send, derive(Debug, Clone))]
911 pub async fn create_repository(
912 &self,
913 repo_id: &str,
915 repo_type: impl RepoType,
918 private: Option<bool>,
920 #[builder(default)]
922 exist_ok: bool,
923 #[builder(into)]
926 space_sdk: Option<String>,
927 ) -> HFResult<RepoUrl> {
928 let url = format!("{}/api/repos/create", self.endpoint());
929
930 let (namespace, name) = split_repo_id(repo_id);
931
932 let mut body = serde_json::json!({
933 "name": name,
934 "private": private.unwrap_or(false),
935 "type": repo_type.singular(),
936 });
937
938 if let Some(ns) = namespace {
939 body["organization"] = serde_json::Value::String(ns.to_string());
940 }
941 if let Some(sdk) = space_sdk {
942 body["sdk"] = serde_json::Value::String(sdk);
943 }
944
945 let headers = self.auth_headers();
946 let response = retry::retry(self.retry_config(), || {
947 self.http_client().post(&url).headers(headers.clone()).json(&body).send()
948 })
949 .await?;
950
951 if response.status().as_u16() == 409 && exist_ok {
952 return Ok(RepoUrl {
953 url: format!("{}/{}{}", self.endpoint(), repo_type.url_prefix(), repo_id),
954 });
955 }
956
957 let response = self
958 .check_response(response, None, crate::error::NotFoundContext::Generic)
959 .await?;
960 Ok(response.json().await?)
961 }
962
963 #[builder(finish_fn = send, derive(Debug, Clone))]
977 pub async fn delete_repository(
978 &self,
979 repo_id: &str,
981 repo_type: impl RepoType,
984 #[builder(default)]
986 missing_ok: bool,
987 ) -> HFResult<()> {
988 let url = format!("{}/api/repos/delete", self.endpoint());
989
990 let (namespace, name) = split_repo_id(repo_id);
991
992 let mut body = serde_json::json!({
993 "name": name,
994 "type": repo_type.singular(),
995 });
996 if let Some(ns) = namespace {
997 body["organization"] = serde_json::Value::String(ns.to_string());
998 }
999
1000 let headers = self.auth_headers();
1001 let response = retry::retry(self.retry_config(), || {
1002 self.http_client().delete(&url).headers(headers.clone()).json(&body).send()
1003 })
1004 .await?;
1005
1006 if response.status().as_u16() == 404 && missing_ok {
1007 return Ok(());
1008 }
1009
1010 self.check_response(response, Some(repo_id), crate::error::NotFoundContext::Repo)
1011 .await?;
1012 Ok(())
1013 }
1014
1015 #[builder(finish_fn = send, derive(Debug, Clone))]
1029 pub async fn move_repository(
1030 &self,
1031 from_id: &str,
1033 to_id: &str,
1035 repo_type: impl RepoType,
1038 ) -> HFResult<RepoUrl> {
1039 let url = format!("{}/api/repos/move", self.endpoint());
1040 let body = serde_json::json!({
1041 "fromRepo": from_id,
1042 "toRepo": to_id,
1043 "type": repo_type.singular(),
1044 });
1045
1046 let headers = self.auth_headers();
1047 let response = retry::retry(self.retry_config(), || {
1048 self.http_client().post(&url).headers(headers.clone()).json(&body).send()
1049 })
1050 .await?;
1051
1052 self.check_response(response, None, crate::error::NotFoundContext::Generic)
1053 .await?;
1054 Ok(RepoUrl {
1055 url: format!("{}/{}{}", self.endpoint(), repo_type.url_prefix(), to_id),
1056 })
1057 }
1058}
1059
1060impl<T: RepoType> HFRepository<T> {
1061 pub fn new(client: HFClient, repo_type: T, owner: impl Into<String>, name: impl Into<String>) -> Self {
1063 Self {
1064 hf_client: client,
1065 owner: owner.into(),
1066 name: name.into(),
1067 repo_type,
1068 }
1069 }
1070
1071 pub fn client(&self) -> &HFClient {
1073 &self.hf_client
1074 }
1075
1076 pub fn owner(&self) -> &str {
1078 &self.owner
1079 }
1080
1081 pub fn name(&self) -> &str {
1083 &self.name
1084 }
1085
1086 pub fn repo_path(&self) -> String {
1090 if self.owner.is_empty() {
1091 self.name.clone()
1092 } else {
1093 format!("{}/{}", self.owner, self.name)
1094 }
1095 }
1096
1097 pub fn repo_type(&self) -> &T {
1105 &self.repo_type
1106 }
1107
1108 pub(crate) async fn fetch_repo_info<I: DeserializeOwned>(
1111 &self,
1112 revision: Option<String>,
1113 expand: Option<Vec<String>>,
1114 ) -> HFResult<I> {
1115 let mut url = self.hf_client.api_url(self.repo_type.plural(), &self.repo_path());
1116 if let Some(ref revision) = revision {
1117 url = format!("{url}/revision/{}", crate::client::encode_ref(revision));
1118 }
1119 let headers = self.hf_client.auth_headers();
1120 let expand_params: Option<Vec<(&str, &str)>> =
1121 expand.as_ref().map(|e| e.iter().map(|v| ("expand", v.as_str())).collect());
1122 let response = retry::retry(self.hf_client.retry_config(), || {
1123 let mut req = self.hf_client.http_client().get(&url).headers(headers.clone());
1124 if let Some(ref params) = expand_params {
1125 req = req.query(params);
1126 }
1127 req.send()
1128 })
1129 .await?;
1130 let repo_path = self.repo_path();
1131 let not_found_ctx = match revision {
1132 Some(rev) => crate::error::NotFoundContext::Revision { revision: rev },
1133 None => crate::error::NotFoundContext::Repo,
1134 };
1135 let response = self.hf_client.check_response(response, Some(&repo_path), not_found_ctx).await?;
1136 Ok(response.json().await?)
1137 }
1138}
1139
1140#[bon]
1141impl<T: RepoType> HFRepository<T> {
1142 #[builder(finish_fn = send, derive(Debug, Clone))]
1148 pub async fn exists(&self) -> HFResult<bool> {
1149 let url = self.hf_client.api_url(self.repo_type.plural(), &self.repo_path());
1150 let headers = self.hf_client.auth_headers();
1151 let response = retry::retry(self.hf_client.retry_config(), || {
1152 self.hf_client.http_client().get(&url).headers(headers.clone()).send()
1153 })
1154 .await?;
1155 if response.status() == reqwest::StatusCode::NOT_FOUND {
1156 return Ok(false);
1157 }
1158 self.hf_client
1159 .check_response(response, Some(&self.repo_path()), crate::error::NotFoundContext::Generic)
1160 .await?;
1161 Ok(true)
1162 }
1163
1164 #[builder(finish_fn = send, derive(Debug, Clone))]
1174 pub async fn revision_exists(
1175 &self,
1176 revision: &str,
1178 ) -> HFResult<bool> {
1179 let url = format!(
1180 "{}/revision/{}",
1181 self.hf_client.api_url(self.repo_type.plural(), &self.repo_path()),
1182 crate::client::encode_ref(revision)
1183 );
1184 let headers = self.hf_client.auth_headers();
1185 let response = retry::retry(self.hf_client.retry_config(), || {
1186 self.hf_client.http_client().get(&url).headers(headers.clone()).send()
1187 })
1188 .await?;
1189 if response.status() == reqwest::StatusCode::NOT_FOUND {
1190 return Ok(false);
1191 }
1192 self.hf_client
1193 .check_response(response, Some(&self.repo_path()), crate::error::NotFoundContext::Generic)
1194 .await?;
1195 Ok(true)
1196 }
1197
1198 #[builder(finish_fn = send, derive(Debug, Clone))]
1205 pub async fn file_exists(
1206 &self,
1207 filename: &str,
1209 revision: Option<&str>,
1211 ) -> HFResult<bool> {
1212 let revision = revision.unwrap_or(constants::DEFAULT_REVISION);
1213 let url = self
1214 .hf_client
1215 .download_url(self.repo_type.url_prefix(), &self.repo_path(), revision, filename)?;
1216 let headers = self.hf_client.auth_headers();
1217 let response = retry::retry(self.hf_client.retry_config(), || {
1218 self.hf_client.http_client().head(&url).headers(headers.clone()).send()
1219 })
1220 .await?;
1221 if response.status() == reqwest::StatusCode::NOT_FOUND {
1222 if self.revision_exists().revision(revision).send().await? {
1223 return Ok(false);
1224 }
1225 return Err(HFError::RevisionNotFound {
1226 repo_id: self.repo_path(),
1227 revision: revision.to_string(),
1228 context: None,
1229 });
1230 }
1231 self.hf_client
1232 .check_response(response, Some(&self.repo_path()), crate::error::NotFoundContext::Generic)
1233 .await?;
1234 Ok(true)
1235 }
1236
1237 #[builder(finish_fn = send, derive(Debug, Clone))]
1251 pub async fn update_settings(
1252 &self,
1253 private: Option<bool>,
1255 gated: Option<GatedApprovalMode>,
1257 #[builder(into)]
1259 description: Option<String>,
1260 discussions_disabled: Option<bool>,
1262 gated_notifications: Option<GatedNotifications>,
1264 ) -> HFResult<()> {
1265 #[derive(Serialize)]
1266 #[serde(rename_all = "camelCase")]
1267 struct UpdateSettingsBody<'a> {
1268 #[serde(skip_serializing_if = "Option::is_none")]
1269 private: Option<bool>,
1270 #[serde(skip_serializing_if = "Option::is_none")]
1271 gated: Option<&'a GatedApprovalMode>,
1272 #[serde(skip_serializing_if = "Option::is_none")]
1273 description: Option<&'a str>,
1274 #[serde(skip_serializing_if = "Option::is_none")]
1275 discussions_disabled: Option<bool>,
1276 #[serde(skip_serializing_if = "Option::is_none")]
1277 gated_notifications_email: Option<&'a str>,
1278 #[serde(skip_serializing_if = "Option::is_none")]
1279 gated_notifications_mode: Option<&'a GatedNotificationsMode>,
1280 }
1281
1282 let body = UpdateSettingsBody {
1283 private,
1284 gated: gated.as_ref(),
1285 description: description.as_deref(),
1286 discussions_disabled,
1287 gated_notifications_email: gated_notifications.as_ref().and_then(|g| g.email.as_deref()),
1288 gated_notifications_mode: gated_notifications.as_ref().map(|g| &g.mode),
1289 };
1290
1291 let url = format!("{}/settings", self.hf_client.api_url(self.repo_type.plural(), &self.repo_path()));
1292 let headers = self.hf_client.auth_headers();
1293
1294 let response = retry::retry(self.hf_client.retry_config(), || {
1295 self.hf_client
1296 .http_client()
1297 .put(&url)
1298 .headers(headers.clone())
1299 .json(&body)
1300 .send()
1301 })
1302 .await?;
1303
1304 let repo_path = self.repo_path();
1305 self.hf_client
1306 .check_response(response, Some(&repo_path), crate::error::NotFoundContext::Repo)
1307 .await?;
1308 Ok(())
1309 }
1310}
1311
1312macro_rules! info_method_doc {
1314 () => {
1315 "Fetch repository metadata for this repo kind, returning the concrete info struct.\n\n\
1316 # Parameters\n\n\
1317 - `revision`: Git revision (branch, tag, or commit SHA). Defaults to the main branch.\n\
1318 - `expand`: list of properties to expand in the response (e.g., `\"trendingScore\"`, `\"cardData\"`).\n \
1319 When set, only the listed properties (plus `_id` and `id`) are returned. Kernel info ignores this."
1320 };
1321}
1322
1323#[bon]
1324impl HFRepository<RepoTypeModel> {
1325 #[doc = info_method_doc!()]
1327 #[builder(
1328 finish_fn = send,
1329 builder_type = HFModelInfoBuilder,
1330 state_mod(name = hf_model_info_builder, vis = "pub(crate)"),
1331 derive(Debug, Clone),
1332 )]
1333 pub async fn info(
1334 &self,
1335 #[builder(into)]
1337 revision: Option<String>,
1338 expand: Option<Vec<String>>,
1340 ) -> HFResult<ModelInfo> {
1341 self.fetch_repo_info(revision, expand).await
1342 }
1343}
1344
1345#[bon]
1346impl HFRepository<RepoTypeDataset> {
1347 #[doc = info_method_doc!()]
1349 #[builder(
1350 finish_fn = send,
1351 builder_type = HFDatasetInfoBuilder,
1352 state_mod(name = hf_dataset_info_builder, vis = "pub(crate)"),
1353 derive(Debug, Clone),
1354 )]
1355 pub async fn info(
1356 &self,
1357 #[builder(into)]
1359 revision: Option<String>,
1360 expand: Option<Vec<String>>,
1362 ) -> HFResult<DatasetInfo> {
1363 self.fetch_repo_info(revision, expand).await
1364 }
1365}
1366
1367#[bon]
1368impl HFRepository<RepoTypeSpace> {
1369 #[doc = info_method_doc!()]
1371 #[builder(
1372 finish_fn = send,
1373 builder_type = HFSpaceInfoBuilder,
1374 state_mod(name = hf_space_info_builder, vis = "pub(crate)"),
1375 derive(Debug, Clone),
1376 )]
1377 pub async fn info(
1378 &self,
1379 #[builder(into)]
1381 revision: Option<String>,
1382 expand: Option<Vec<String>>,
1384 ) -> HFResult<SpaceInfo> {
1385 self.fetch_repo_info(revision, expand).await
1386 }
1387}
1388
1389#[bon]
1390impl HFRepository<RepoTypeKernel> {
1391 #[doc = info_method_doc!()]
1398 #[builder(
1399 finish_fn = send,
1400 builder_type = HFKernelInfoBuilder,
1401 state_mod(name = hf_kernel_info_builder, vis = "pub(crate)"),
1402 derive(Debug, Clone),
1403 )]
1404 pub async fn info(
1405 &self,
1406 #[builder(into)]
1408 revision: Option<String>,
1409 expand: Option<Vec<String>>,
1411 ) -> HFResult<KernelInfo> {
1412 self.fetch_repo_info(revision, expand).await
1413 }
1414}
1415
1416fn split_repo_id(repo_id: &str) -> (Option<&str>, &str) {
1418 match repo_id.split_once('/') {
1419 Some((ns, name)) => (Some(ns), name),
1420 None => (None, repo_id),
1421 }
1422}
1423
1424#[cfg(feature = "blocking")]
1425#[bon]
1426impl crate::blocking::HFClientSync {
1427 #[builder(finish_fn = send, derive(Debug, Clone))]
1430 pub fn list_models(
1431 &self,
1432 #[builder(into)] search: Option<String>,
1433 #[builder(into)] author: Option<String>,
1434 #[builder(into)] filter: Option<String>,
1435 #[builder(into)] sort: Option<String>,
1436 #[builder(into)] pipeline_tag: Option<String>,
1437 full: Option<bool>,
1438 card_data: Option<bool>,
1439 fetch_config: Option<bool>,
1440 limit: Option<usize>,
1441 ) -> HFResult<Vec<ModelInfo>> {
1442 use futures::StreamExt;
1443 self.runtime.block_on(async move {
1444 let stream = self
1445 .inner
1446 .list_models()
1447 .maybe_search(search)
1448 .maybe_author(author)
1449 .maybe_filter(filter)
1450 .maybe_sort(sort)
1451 .maybe_pipeline_tag(pipeline_tag)
1452 .maybe_full(full)
1453 .maybe_card_data(card_data)
1454 .maybe_fetch_config(fetch_config)
1455 .maybe_limit(limit)
1456 .send()?;
1457 futures::pin_mut!(stream);
1458 let mut items = Vec::new();
1459 while let Some(item) = stream.next().await {
1460 items.push(item?);
1461 }
1462 Ok(items)
1463 })
1464 }
1465
1466 #[builder(finish_fn = send, derive(Debug, Clone))]
1469 pub fn list_datasets(
1470 &self,
1471 #[builder(into)] search: Option<String>,
1472 #[builder(into)] author: Option<String>,
1473 #[builder(into)] filter: Option<String>,
1474 #[builder(into)] sort: Option<String>,
1475 full: Option<bool>,
1476 limit: Option<usize>,
1477 ) -> HFResult<Vec<DatasetInfo>> {
1478 use futures::StreamExt;
1479 self.runtime.block_on(async move {
1480 let stream = self
1481 .inner
1482 .list_datasets()
1483 .maybe_search(search)
1484 .maybe_author(author)
1485 .maybe_filter(filter)
1486 .maybe_sort(sort)
1487 .maybe_full(full)
1488 .maybe_limit(limit)
1489 .send()?;
1490 futures::pin_mut!(stream);
1491 let mut items = Vec::new();
1492 while let Some(item) = stream.next().await {
1493 items.push(item?);
1494 }
1495 Ok(items)
1496 })
1497 }
1498
1499 #[builder(finish_fn = send, derive(Debug, Clone))]
1502 pub fn list_spaces(
1503 &self,
1504 #[builder(into)] search: Option<String>,
1505 #[builder(into)] author: Option<String>,
1506 #[builder(into)] filter: Option<String>,
1507 #[builder(into)] sort: Option<String>,
1508 full: Option<bool>,
1509 limit: Option<usize>,
1510 ) -> HFResult<Vec<SpaceInfo>> {
1511 use futures::StreamExt;
1512 self.runtime.block_on(async move {
1513 let stream = self
1514 .inner
1515 .list_spaces()
1516 .maybe_search(search)
1517 .maybe_author(author)
1518 .maybe_filter(filter)
1519 .maybe_sort(sort)
1520 .maybe_full(full)
1521 .maybe_limit(limit)
1522 .send()?;
1523 futures::pin_mut!(stream);
1524 let mut items = Vec::new();
1525 while let Some(item) = stream.next().await {
1526 items.push(item?);
1527 }
1528 Ok(items)
1529 })
1530 }
1531
1532 #[builder(finish_fn = send, derive(Debug, Clone))]
1535 pub fn create_repository(
1536 &self,
1537 repo_id: &str,
1538 repo_type: impl RepoType,
1539 private: Option<bool>,
1540 #[builder(default)] exist_ok: bool,
1541 #[builder(into)] space_sdk: Option<String>,
1542 ) -> HFResult<RepoUrl> {
1543 self.runtime.block_on(
1544 self.inner
1545 .create_repository()
1546 .repo_id(repo_id)
1547 .repo_type(repo_type)
1548 .maybe_private(private)
1549 .exist_ok(exist_ok)
1550 .maybe_space_sdk(space_sdk)
1551 .send(),
1552 )
1553 }
1554
1555 #[builder(finish_fn = send, derive(Debug, Clone))]
1558 pub fn delete_repository(
1559 &self,
1560 repo_id: &str,
1561 repo_type: impl RepoType,
1562 #[builder(default)] missing_ok: bool,
1563 ) -> HFResult<()> {
1564 self.runtime.block_on(
1565 self.inner
1566 .delete_repository()
1567 .repo_id(repo_id)
1568 .repo_type(repo_type)
1569 .missing_ok(missing_ok)
1570 .send(),
1571 )
1572 }
1573
1574 #[builder(finish_fn = send, derive(Debug, Clone))]
1577 pub fn move_repository(&self, from_id: &str, to_id: &str, repo_type: impl RepoType) -> HFResult<RepoUrl> {
1578 self.runtime.block_on(
1579 self.inner
1580 .move_repository()
1581 .from_id(from_id)
1582 .to_id(to_id)
1583 .repo_type(repo_type)
1584 .send(),
1585 )
1586 }
1587}
1588
1589#[cfg(feature = "blocking")]
1590#[bon]
1591impl<T: RepoType> crate::blocking::HFRepositorySync<T> {
1592 #[builder(finish_fn = send, derive(Debug, Clone))]
1594 pub fn exists(&self) -> HFResult<bool> {
1595 self.runtime.block_on(self.inner.exists().send())
1596 }
1597
1598 #[builder(finish_fn = send, derive(Debug, Clone))]
1601 pub fn revision_exists(&self, revision: &str) -> HFResult<bool> {
1602 self.runtime.block_on(self.inner.revision_exists().revision(revision).send())
1603 }
1604
1605 #[builder(finish_fn = send, derive(Debug, Clone))]
1608 pub fn file_exists(&self, filename: &str, revision: Option<&str>) -> HFResult<bool> {
1609 self.runtime
1610 .block_on(self.inner.file_exists().filename(filename).maybe_revision(revision).send())
1611 }
1612
1613 #[builder(finish_fn = send, derive(Debug, Clone))]
1616 pub fn update_settings(
1617 &self,
1618 private: Option<bool>,
1619 gated: Option<GatedApprovalMode>,
1620 #[builder(into)] description: Option<String>,
1621 discussions_disabled: Option<bool>,
1622 gated_notifications: Option<GatedNotifications>,
1623 ) -> HFResult<()> {
1624 self.runtime.block_on(
1625 self.inner
1626 .update_settings()
1627 .maybe_private(private)
1628 .maybe_gated(gated)
1629 .maybe_description(description)
1630 .maybe_discussions_disabled(discussions_disabled)
1631 .maybe_gated_notifications(gated_notifications)
1632 .send(),
1633 )
1634 }
1635}
1636
1637#[cfg(feature = "blocking")]
1644#[bon]
1645impl crate::blocking::HFRepositorySync<RepoTypeModel> {
1646 #[builder(
1648 finish_fn = send,
1649 builder_type = HFModelInfoSyncBuilder,
1650 state_mod(name = hf_model_info_sync_builder, vis = "pub(crate)"),
1651 derive(Debug, Clone),
1652 )]
1653 pub fn info(&self, #[builder(into)] revision: Option<String>, expand: Option<Vec<String>>) -> HFResult<ModelInfo> {
1654 self.runtime
1655 .block_on(self.inner.info().maybe_revision(revision).maybe_expand(expand).send())
1656 }
1657}
1658
1659#[cfg(feature = "blocking")]
1660#[bon]
1661impl crate::blocking::HFRepositorySync<RepoTypeDataset> {
1662 #[builder(
1664 finish_fn = send,
1665 builder_type = HFDatasetInfoSyncBuilder,
1666 state_mod(name = hf_dataset_info_sync_builder, vis = "pub(crate)"),
1667 derive(Debug, Clone),
1668 )]
1669 pub fn info(
1670 &self,
1671 #[builder(into)] revision: Option<String>,
1672 expand: Option<Vec<String>>,
1673 ) -> HFResult<DatasetInfo> {
1674 self.runtime
1675 .block_on(self.inner.info().maybe_revision(revision).maybe_expand(expand).send())
1676 }
1677}
1678
1679#[cfg(feature = "blocking")]
1680#[bon]
1681impl crate::blocking::HFRepositorySync<RepoTypeSpace> {
1682 #[builder(
1684 finish_fn = send,
1685 builder_type = HFSpaceInfoSyncBuilder,
1686 state_mod(name = hf_space_info_sync_builder, vis = "pub(crate)"),
1687 derive(Debug, Clone),
1688 )]
1689 pub fn info(&self, #[builder(into)] revision: Option<String>, expand: Option<Vec<String>>) -> HFResult<SpaceInfo> {
1690 self.runtime
1691 .block_on(self.inner.info().maybe_revision(revision).maybe_expand(expand).send())
1692 }
1693}
1694
1695#[cfg(feature = "blocking")]
1696#[bon]
1697impl crate::blocking::HFRepositorySync<RepoTypeKernel> {
1698 #[builder(
1700 finish_fn = send,
1701 builder_type = HFKernelInfoSyncBuilder,
1702 state_mod(name = hf_kernel_info_sync_builder, vis = "pub(crate)"),
1703 derive(Debug, Clone),
1704 )]
1705 pub fn info(&self, #[builder(into)] revision: Option<String>, expand: Option<Vec<String>>) -> HFResult<KernelInfo> {
1706 self.runtime
1707 .block_on(self.inner.info().maybe_revision(revision).maybe_expand(expand).send())
1708 }
1709}
1710
1711#[cfg(test)]
1712mod tests {
1713 use futures::StreamExt;
1714
1715 use super::{
1716 DatasetInfo, EvalResultEntry, HFRepository, InferenceProviderMapping, KernelInfo, ModelInfo, RepoType,
1717 RepoTypeDataset, RepoTypeKernel, RepoTypeModel, RepoTypeSpace, SafeTensorsInfo, SpaceInfo, TransformersInfo,
1718 split_repo_id,
1719 };
1720 use crate::client::HFClient;
1721
1722 #[test]
1723 fn test_repo_path_and_accessors() {
1724 let client = HFClient::builder().build().unwrap();
1725 let repo: HFRepository<RepoTypeModel> = HFRepository::new(client, RepoTypeModel, "openai-community", "gpt2");
1726
1727 assert_eq!(repo.owner(), "openai-community");
1728 assert_eq!(repo.name(), "gpt2");
1729 assert_eq!(repo.repo_path(), "openai-community/gpt2");
1730 assert_eq!(repo.repo_type().singular(), "model");
1731 }
1732
1733 #[test]
1734 fn test_marker_struct_singular_and_plural() {
1735 assert_eq!(RepoTypeModel.singular(), "model");
1736 assert_eq!(RepoTypeDataset.singular(), "dataset");
1737 assert_eq!(RepoTypeSpace.singular(), "space");
1738 assert_eq!(RepoTypeKernel.singular(), "kernel");
1739 assert_eq!(RepoTypeModel.plural(), "models");
1740 assert_eq!(RepoTypeDataset.plural(), "datasets");
1741 assert_eq!(RepoTypeSpace.plural(), "spaces");
1742 assert_eq!(RepoTypeKernel.plural(), "kernels");
1743 }
1744
1745 #[test]
1746 fn test_marker_struct_url_prefix() {
1747 assert_eq!(RepoTypeModel.url_prefix(), "");
1748 assert_eq!(RepoTypeDataset.url_prefix(), "datasets/");
1749 assert_eq!(RepoTypeSpace.url_prefix(), "spaces/");
1750 assert_eq!(RepoTypeKernel.url_prefix(), "kernels/");
1751 }
1752
1753 #[test]
1754 fn test_marker_struct_display() {
1755 assert_eq!(format!("{}", RepoTypeModel), "model");
1756 assert_eq!(format!("{}", RepoTypeDataset), "dataset");
1757 assert_eq!(format!("{}", RepoTypeSpace), "space");
1758 assert_eq!(format!("{}", RepoTypeKernel), "kernel");
1759 }
1760
1761 #[test]
1762 fn test_split_repo_id() {
1763 assert_eq!(split_repo_id("user/repo"), (Some("user"), "repo"));
1764 assert_eq!(split_repo_id("repo"), (None, "repo"));
1765 assert_eq!(split_repo_id("org/sub/repo"), (Some("org"), "sub/repo"));
1766 }
1767
1768 #[tokio::test]
1769 async fn test_list_models_limit_zero_returns_empty() {
1770 let client = HFClient::builder().build().unwrap();
1771 let stream = client.list_models().limit(0_usize).send().unwrap();
1772 futures::pin_mut!(stream);
1773 assert!(stream.next().await.is_none());
1774 }
1775
1776 #[tokio::test]
1777 async fn test_list_datasets_limit_zero_returns_empty() {
1778 let client = HFClient::builder().build().unwrap();
1779 let stream = client.list_datasets().limit(0_usize).send().unwrap();
1780 futures::pin_mut!(stream);
1781 assert!(stream.next().await.is_none());
1782 }
1783
1784 #[tokio::test]
1785 async fn test_list_spaces_limit_zero_returns_empty() {
1786 let client = HFClient::builder().build().unwrap();
1787 let stream = client.list_spaces().limit(0_usize).send().unwrap();
1788 futures::pin_mut!(stream);
1789 assert!(stream.next().await.is_none());
1790 }
1791
1792 #[test]
1793 fn test_safetensors_info_deserialize() {
1794 let json = r#"{"parameters":{"F32":124000000,"BF16":1000000},"total":125000000}"#;
1795 let info: SafeTensorsInfo = serde_json::from_str(json).unwrap();
1796 assert_eq!(info.total, 125_000_000);
1797 assert_eq!(info.parameters.get("F32"), Some(&124_000_000));
1798 }
1799
1800 #[test]
1801 fn test_transformers_info_deserialize() {
1802 let json = r#"{"auto_model":"AutoModelForCausalLM","pipeline_tag":"text-generation"}"#;
1803 let info: TransformersInfo = serde_json::from_str(json).unwrap();
1804 assert_eq!(info.auto_model, "AutoModelForCausalLM");
1805 assert_eq!(info.pipeline_tag.as_deref(), Some("text-generation"));
1806 assert!(info.processor.is_none());
1807 }
1808
1809 #[test]
1810 fn test_eval_result_entry_minimal() {
1811 let json = r#"{"dataset":{"id":"cais/hle","task_id":"default"},"value":20.9}"#;
1812 let entry: EvalResultEntry = serde_json::from_str(json).unwrap();
1813 assert_eq!(entry.dataset.id, "cais/hle");
1814 assert_eq!(entry.dataset.task_id, "default");
1815 assert_eq!(entry.value.as_f64(), Some(20.9));
1816 assert!(entry.source.is_none());
1817 }
1818
1819 #[test]
1820 fn test_eval_result_entry_with_source() {
1821 let json = r#"{"dataset":{"id":"d/x","task_id":"t","revision":"abc"},"value":0.5,"source":{"url":"u","name":"n","org":"o"},"verifyToken":"vt","notes":"n"}"#;
1822 let entry: EvalResultEntry = serde_json::from_str(json).unwrap();
1823 assert_eq!(entry.dataset.id, "d/x");
1824 assert_eq!(entry.dataset.revision.as_deref(), Some("abc"));
1825 let source = entry.source.as_ref().unwrap();
1826 assert_eq!(source.url.as_deref(), Some("u"));
1827 assert_eq!(source.org.as_deref(), Some("o"));
1828 assert_eq!(entry.verify_token.as_deref(), Some("vt"));
1829 }
1830
1831 #[test]
1832 fn test_inference_provider_mapping_list_form() {
1833 let json = r#"{
1834 "id":"o/m",
1835 "inferenceProviderMapping":[
1836 {"provider":"hf-inference","providerId":"o/m","status":"live","task":"text-generation"}
1837 ]
1838 }"#;
1839 let info: ModelInfo = serde_json::from_str(json).unwrap();
1840 let mappings = info.inference_provider_mapping.unwrap();
1841 assert_eq!(mappings.len(), 1);
1842 assert_eq!(mappings[0].provider, "hf-inference");
1843 assert_eq!(mappings[0].provider_id, "o/m");
1844 assert_eq!(mappings[0].status, "live");
1845 }
1846
1847 #[test]
1848 fn test_inference_provider_mapping_dict_form() {
1849 let json = r#"{
1850 "id":"o/m",
1851 "inferenceProviderMapping":{
1852 "together":{"providerId":"o/m","status":"live","task":"text-generation"}
1853 }
1854 }"#;
1855 let info: ModelInfo = serde_json::from_str(json).unwrap();
1856 let mappings = info.inference_provider_mapping.unwrap();
1857 assert_eq!(mappings.len(), 1);
1858 assert_eq!(mappings[0].provider, "together");
1859 assert_eq!(mappings[0].task, "text-generation");
1860 }
1861
1862 #[test]
1863 fn test_inference_provider_mapping_helper_directly() {
1864 let info_helper: InferenceProviderMapping = serde_json::from_str(
1865 r#"{"provider":"x","providerId":"y","status":"live","task":"t","adapterWeightsPath":"w"}"#,
1866 )
1867 .unwrap();
1868 assert_eq!(info_helper.adapter_weights_path.as_deref(), Some("w"));
1869 }
1870
1871 #[test]
1872 fn test_model_info_ignores_unknown_and_legacy_fields() {
1873 let json = r#"{"id":"o/m","modelId":"o/m","brandNewField":42}"#;
1874 let info: ModelInfo = serde_json::from_str(json).unwrap();
1875 assert_eq!(info.id, "o/m");
1876 }
1877
1878 #[test]
1882 fn test_kernel_info_deserializes_real_response() {
1883 let json = r#"{
1884 "_id":"69d02879cbdc347de53cced2",
1885 "author":"kernels-community",
1886 "authorData":{"name":"kernels-community","type":"org"},
1887 "trustedPublisher":false,
1888 "downloads":7199,
1889 "gated":false,
1890 "id":"kernels-community/flash-attn2",
1891 "isLikedByUser":false,
1892 "lastModified":"2026-04-20T20:31:57.000Z",
1893 "likes":6,
1894 "private":false,
1895 "repoType":"kernel",
1896 "sha":"e16b327d7c5b015cac48944d4058f688e4d0c62f",
1897 "supportedDriverFamilies":["cuda","xpu","cpu"]
1898 }"#;
1899 let info: KernelInfo = serde_json::from_str(json).unwrap();
1900 assert_eq!(info.id, "kernels-community/flash-attn2");
1901 assert_eq!(info.author.as_deref(), Some("kernels-community"));
1902 assert_eq!(info.sha.as_deref(), Some("e16b327d7c5b015cac48944d4058f688e4d0c62f"));
1903 assert_eq!(info.downloads, Some(7199));
1904 assert_eq!(info.likes, Some(6));
1905 assert_eq!(info.trusted_publisher, Some(false));
1906 assert_eq!(info.supported_driver_families.as_deref(), Some(&["cuda".into(), "xpu".into(), "cpu".into()][..]));
1907 assert_eq!(info.gated.as_ref().and_then(|v| v.as_bool()), Some(false));
1909 }
1910
1911 #[test]
1913 fn test_kernel_info_missing_supported_driver_families() {
1914 let json = r#"{"id":"o/k","sha":"abc","downloads":0,"likes":0,"private":false,"gated":false}"#;
1915 let info: KernelInfo = serde_json::from_str(json).unwrap();
1916 assert_eq!(info.id, "o/k");
1917 assert!(info.supported_driver_families.is_none());
1918 }
1919
1920 #[test]
1921 fn test_dataset_info_new_fields() {
1922 let json = r#"{
1923 "id":"u/d",
1924 "citation":"Doe et al. 2024",
1925 "paperswithcode_id":"pwc-id",
1926 "resourceGroup":{"id":"rg-1","name":"Team A"}
1927 }"#;
1928 let info: DatasetInfo = serde_json::from_str(json).unwrap();
1929 assert_eq!(info.citation.as_deref(), Some("Doe et al. 2024"));
1930 assert_eq!(info.paperswithcode_id.as_deref(), Some("pwc-id"));
1931 assert!(info.resource_group.is_some());
1932 }
1933
1934 #[test]
1935 fn test_space_info_new_fields() {
1936 let json = r#"{
1937 "id":"u/s",
1938 "models":["org/model-a","org/model-b"],
1939 "datasets":["org/dataset"],
1940 "resourceGroup":{"id":"rg-2"}
1941 }"#;
1942 let info: SpaceInfo = serde_json::from_str(json).unwrap();
1943 assert_eq!(info.models.as_deref(), Some(&["org/model-a".to_string(), "org/model-b".to_string()][..]));
1944 assert_eq!(info.datasets.as_deref(), Some(&["org/dataset".to_string()][..]));
1945 assert!(info.resource_group.is_some());
1946 }
1947}