1use hf_hub::{split_id, HFClientBuilder};
4use std::path::{Path, PathBuf};
5
6use crate::agent::{Agent, AgentBuilder};
7use crate::router::{ModelName, Router};
8use crate::{Error, Result};
9
10const CHECKPOINT_PATTERNS: [&str; 5] = [
12 "model.safetensors",
13 "rl_agent_config.json",
14 "encoder/config.json",
15 "tokenizer/*",
16 "mlx_config.json",
17];
18
19pub fn resolve_model(
24 model_id_or_path: &str,
25 subfolder: Option<&str>,
26 revision: Option<&str>,
27 token: Option<String>,
28) -> Result<PathBuf> {
29 if let Some(subfolder) = subfolder {
30 let path = Path::new(subfolder);
31 if path.is_absolute() || path.components().any(|c| c.as_os_str() == "..") {
32 return Err(Error::Config(
33 "subfolder must be a relative path inside the model repository".into(),
34 ));
35 }
36 }
37
38 let local = PathBuf::from(expand_home(model_id_or_path));
39 if local.exists() {
40 return Ok(match subfolder {
41 Some(sub) => local.join(sub),
42 None => local,
43 });
44 }
45 if model_id_or_path.starts_with('/')
46 || model_id_or_path.starts_with("./")
47 || model_id_or_path.starts_with("../")
48 || model_id_or_path.starts_with('~')
49 {
50 return Err(Error::Config(format!(
51 "Local model directory does not exist: {model_id_or_path}"
52 )));
53 }
54
55 let mut builder = HFClientBuilder::new();
56 if let Some(token) = token.or_else(|| std::env::var("HF_TOKEN").ok()) {
57 builder = builder.token(token);
58 }
59 let client = builder
60 .build_sync()
61 .map_err(|error| Error::Hub(error.to_string()))?;
62
63 let (owner, name) = split_id(model_id_or_path);
64 let repository = client.model(owner, name);
65
66 let patterns: Vec<String> = CHECKPOINT_PATTERNS
69 .iter()
70 .map(|pattern| match subfolder {
71 Some(sub) => format!("{}/{pattern}", sub.trim_end_matches('/')),
72 None => (*pattern).to_string(),
73 })
74 .collect();
75
76 let snapshot = repository
77 .snapshot_download()
78 .maybe_revision(revision.map(str::to_string))
79 .allow_patterns(patterns)
80 .send()
81 .map_err(|error| Error::Hub(format!("{model_id_or_path}: {error}")))?;
82
83 Ok(match subfolder {
84 Some(sub) => snapshot.join(sub),
85 None => snapshot,
86 })
87}
88
89fn expand_home(path: &str) -> String {
90 if let Some(rest) = path.strip_prefix("~/") {
91 if let Some(home) = std::env::var_os("HOME") {
92 return Path::new(&home).join(rest).to_string_lossy().into_owned();
93 }
94 }
95 path.to_string()
96}
97
98impl Agent {
99 pub fn from_pretrained(model_id_or_path: &str) -> Result<Self> {
101 Self::from_pretrained_with(model_id_or_path, None, None, AgentBuilder::new())
102 }
103
104 pub fn from_pretrained_with(
106 model_id_or_path: &str,
107 subfolder: Option<&str>,
108 revision: Option<&str>,
109 options: AgentBuilder,
110 ) -> Result<Self> {
111 let dir = resolve_model(model_id_or_path, subfolder, revision, None)?;
112 options.build(dir)
113 }
114}
115
116impl Router {
117 pub fn load(
119 &mut self,
120 model: ModelName,
121 options: AgentBuilder,
122 ) -> Result<std::sync::Arc<Agent>> {
123 if let Some(agent) = self.get(model) {
124 return Ok(agent);
125 }
126 let (repo, subfolder) = model.bundle_location();
127 let dir = resolve_model(repo, subfolder, None, None)?;
128 let agent = std::sync::Arc::new(options.build(dir)?);
129 self.attach(model, std::sync::Arc::clone(&agent));
130 Ok(agent)
131 }
132
133 pub fn preload(
139 &mut self,
140 models: &[ModelName],
141 options: impl Fn() -> AgentBuilder,
142 ) -> Result<()> {
143 self.set_max_loaded_at_least(models.len());
144 for model in models {
145 self.load(*model, options())?;
146 }
147 Ok(())
148 }
149}