1use std::path::PathBuf;
2
3use crate::common::DtypeArg;
4use anyhow::Context;
5use clap::Subcommand;
6
7use zsfm_core::ForecastOutput;
8use zsfm_gguf::GGMLType;
9use zsfm_sundial::config::SundialConfig;
10use zsfm_sundial::convert::{convert, ConvertOptions};
11
12#[derive(Subcommand)]
13pub enum Command {
14 Upload {
16 #[arg(short, long, default_value = "amaye15/sundial-gguf")]
17 repo: String,
18 #[arg(long, env = "HF_TOKEN")]
19 token: String,
20 #[arg(long, default_value = ".")]
22 root: PathBuf,
23 },
24 Convert {
32 #[arg(short, long, default_value = "thuml/sundial-base-128m")]
33 model: String,
34 #[arg(short, long, default_value = "gguf/sundial-f16.gguf")]
35 output: PathBuf,
36 #[arg(long, default_value = "f16")]
37 dtype: DtypeArg,
38 #[arg(long, env = "HF_TOKEN")]
39 token: Option<String>,
40 #[arg(long, default_value = "models")]
41 model_dir: PathBuf,
42 #[arg(long)]
44 redownload: bool,
45 },
46 Infer {
54 #[arg(short, long, default_value = "gguf/sundial-f16.gguf")]
56 gguf: PathBuf,
57 #[arg(long)]
60 steps: Option<u32>,
61 },
62 InspectTensors { path: PathBuf },
65
66 Delete {
73 #[arg(short, long, default_value = "thuml/sundial-base-128m")]
75 model: String,
76 #[arg(long, default_value = "models")]
78 model_dir: PathBuf,
79 #[arg(short, long)]
81 output: Option<PathBuf>,
82 },
83}
84
85pub async fn run(command: Command) -> anyhow::Result<()> {
86 match command {
87 Command::Delete {
88 model,
89 model_dir,
90 output,
91 } => {
92 let canonical = zsfm_hub::canonical_gguf_path(&model_dir, &model);
93 crate::common::delete_cached_model(&canonical, output.as_deref())?;
94 }
95 Command::Upload { repo, token, root } => {
96 zsfm_hub::upload_repo(&repo, &token, &root).await?;
97 }
98
99 Command::Convert {
100 model,
101 output,
102 dtype,
103 token,
104 model_dir,
105 redownload,
106 } => {
107 let output_dtype: GGMLType = dtype.into();
108 let canonical = zsfm_hub::canonical_gguf_path(&model_dir, &model);
109 if crate::common::try_recast_from_cache(&canonical, &output, output_dtype, redownload)?
110 {
111 return Ok(());
112 }
113
114 eprintln!("Downloading {model} into {} …", model_dir.display());
115 let files = zsfm_hub::download_model(&model, token.as_deref(), &model_dir)
116 .await
117 .context("download failed")?;
118
119 let config_str = std::fs::read_to_string(&files.config_json)
120 .with_context(|| format!("read {}", files.config_json.display()))?;
121 let config: SundialConfig =
122 serde_json::from_str(&config_str).context("parse config.json")?;
123
124 convert(
125 &model,
126 &files,
127 &config,
128 &ConvertOptions {
129 output_dtype: GGMLType::F32,
130 },
131 &canonical,
132 )?;
133 eprintln!("Wrote canonical F32 GGUF to {} …", canonical.display());
134 files.cleanup_weights();
135
136 zsfm_checkpoint::recast(&canonical, &output, output_dtype)?;
137 eprintln!("Wrote {}", output.display());
138 }
139
140 Command::Infer { gguf, steps } => {
141 return run_infer_burn(&gguf, steps).await;
142 }
143
144 Command::InspectTensors { path } => crate::common::inspect_tensors(&path)?,
145 }
146 Ok(())
147}
148
149async fn run_infer_burn(gguf: &std::path::PathBuf, steps: Option<u32>) -> anyhow::Result<()> {
150 use zsfm_sundial::infer::BurnSundialModel;
151
152 let buf = zsfm_core::read_stdin_limited()?;
153 let req: serde_json::Value = serde_json::from_str(&buf).context("parse JSON input")?;
154 let contexts = zsfm_core::parse_mv_contexts(req["context"].clone())?;
155 let horizon = zsfm_core::parse_horizon(&req)?;
156
157 eprintln!("Loading Burn model from {} …", gguf.display());
158 let model = BurnSundialModel::load(gguf, steps.map(|s| s as usize)).context("load model")?;
159 eprintln!("Model loaded.");
160
161 let mut fc_outputs = Vec::new();
162 let mut total_ctx = 0usize;
163 for raw_variates in &contexts {
164 anyhow::ensure!(
165 raw_variates.len() == 1,
166 "Sundial only supports univariate forecasting (1 variate per context)"
167 );
168 let ctx = &raw_variates[0];
169 anyhow::ensure!(!ctx.is_empty(), "context series must not be empty");
170 total_ctx += ctx.len();
171 eprintln!("Running forecast (context len = {}) …", ctx.len());
172 let raw = model.forecast(ctx)?;
173 let point: Vec<f32> = raw.into_iter().take(horizon).collect();
174 fc_outputs.push(zsfm_core::ForecastOutput::Univariate {
175 point,
176 quantiles: Default::default(),
177 });
178 }
179 println!(
180 "{}",
181 zsfm_core::forecast_response_json("sundial", total_ctx, horizon, fc_outputs)?
182 );
183 Ok(())
184}