use std::path::Path;
use anyhow::Context;
use secrecy::SecretString;
use crate::config::{
DEFAULT_IMAGE, DEFAULT_RUNPOD_BASE_URL, DEFAULT_RUNPOD_IMAGE, DEFAULT_RUNPOD_VENV,
DEFAULT_WORKDIR, Engine, EnvSource, Runtime, Settings, Target,
};
pub async fn run(project_dir: &Path, resolve: bool) -> anyhow::Result<()> {
let settings = crate::config::load(project_dir, EnvSource::Process)?;
for line in describe(&settings) {
println!("{line}");
}
if resolve {
let resolver = super::resolver();
for (key, secret) in secrets(&settings) {
resolver
.resolve(secret)
.await
.with_context(|| format!("cannot resolve {key}"))?;
println!("{key}: resolved");
}
}
Ok(())
}
fn masked(secret: Option<&SecretString>) -> &'static str {
if secret.is_some() { "***" } else { "(unset)" }
}
fn secrets(settings: &Settings) -> Vec<(String, &SecretString)> {
let mut found = Vec::new();
for (name, provider) in &settings.providers {
if let Some(key) = &provider.api_key {
found.push((format!("providers.{name}.api_key"), key));
}
}
if let Some(key) = &settings.runpod.api_key {
found.push(("runpod.api_key".to_string(), key));
}
if let Some(token) = &settings.hf_token {
found.push(("hf_token".to_string(), token));
}
found
}
fn describe(settings: &Settings) -> Vec<String> {
let mut lines = vec![format!("project.name = {}", settings.project.name)];
for topic in &settings.topics {
lines.push(format!(
"topics.{} = {} subtopics x {} questions",
topic.name, topic.subtopics, topic.questions_per_subtopic
));
}
for (name, provider) in &settings.providers {
lines.push(format!(
"providers.{name}.protocol = {:?}",
provider.protocol
));
lines.push(format!(
"providers.{name}.base_url = {}",
provider.base_url.as_deref().unwrap_or("(unset)")
));
lines.push(format!(
"providers.{name}.api_key = {}",
masked(provider.api_key.as_ref())
));
}
for (role, model) in settings.roles.all() {
lines.push(format!(
"roles.{role} = {}/{} (reasoning: {}, max_tokens: {})",
model.provider, model.model, model.reasoning, model.max_tokens
));
}
lines.push(format!("pipeline = {:?}", settings.pipeline));
if let Some(training) = &settings.training {
lines.push(format!(
"training = {} on {} ({:?})",
training.base_model, training.target, training.adapter
));
}
for (name, target) in &settings.targets {
lines.push(format!("targets.{name} = {}", target_summary(target)));
}
lines.push(format!(
"runpod.api_key = {}",
masked(settings.runpod.api_key.as_ref())
));
lines.push(format!(
"runpod.base_url = {}",
settings
.runpod
.base_url
.as_deref()
.unwrap_or(DEFAULT_RUNPOD_BASE_URL)
));
lines.push(format!("hf_token = {}", masked(settings.hf_token.as_ref())));
lines
}
fn target_summary(target: &Target) -> String {
match target {
Target::Local {
runtime,
engine,
image,
venv,
} => format!(
"local, {}",
runtime_summary(*runtime, *engine, image.as_deref(), venv.as_deref())
),
Target::Ssh {
runtime,
host,
workdir,
engine,
image,
venv,
} => format!(
"ssh {} in {}, {}",
host.as_deref().unwrap_or("(host unset)"),
workdir.as_deref().unwrap_or(DEFAULT_WORKDIR),
runtime_summary(*runtime, *engine, image.as_deref(), venv.as_deref())
),
Target::Runpod { .. } => runpod_summary(target),
}
}
fn runpod_summary(target: &Target) -> String {
let Target::Runpod {
gpu_types,
min_vram_gb,
max_price_per_hour,
gpu_count,
image,
venv,
container_disk_gb,
max_hours,
max_cost_usd,
boot_grace_minutes,
retrieve_grace_minutes,
data_center_ids,
network_volume_id,
max_volume_gb,
} = target
else {
return String::new();
};
let data_centers = if data_center_ids.is_any() {
"any".to_string()
} else {
data_center_ids.to_string()
};
let mut limits = Vec::new();
if let Some(gb) = min_vram_gb {
limits.push(format!("at least {gb} GB"));
}
if let Some(price) = max_price_per_hour {
limits.push(format!("at most ${price}/h per GPU"));
}
let limits = if limits.is_empty() {
String::new()
} else {
format!(" ({})", limits.join(", "))
};
let cost = max_cost_usd.map_or_else(String::new, |usd| format!(", max ${usd}"));
let grow = max_volume_gb.map_or_else(String::new, |gb| format!(" (grows up to {gb} GB)"));
format!(
"runpod {gpu_count}x [{gpu_types}]{limits}, max {max_hours}h{cost}, image {}, venv {}, disk {container_disk_gb} GB, \
boot grace {boot_grace_minutes} min, retrieve grace {retrieve_grace_minutes} min, \
data centers {data_centers}, network volume {}{grow}",
image.as_deref().unwrap_or(DEFAULT_RUNPOD_IMAGE),
venv.as_deref().unwrap_or(DEFAULT_RUNPOD_VENV),
network_volume_id.as_deref().unwrap_or("none"),
)
}
fn runtime_summary(
runtime: Runtime,
engine: Option<Engine>,
image: Option<&str>,
venv: Option<&str>,
) -> String {
match runtime {
Runtime::Docker => format!(
"{} {}",
engine.unwrap_or(Engine::Docker).command(),
image.unwrap_or(DEFAULT_IMAGE)
),
Runtime::Native => format!("native, venv {}", venv.unwrap_or("(axolotl on PATH)")),
}
}