use std::collections::BTreeMap;
use std::fmt;
use std::marker::PhantomData;
use std::str::FromStr;
use secrecy::SecretString;
use serde::de::{self, Unexpected, Visitor};
use serde::{Deserialize, Deserializer, Serialize};
#[derive(Debug, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct Settings {
pub project: Project,
#[serde(default)]
pub topics: Vec<Topic>,
#[serde(default)]
pub providers: BTreeMap<String, Provider>,
pub roles: Roles,
#[serde(default)]
pub pipeline: Pipeline,
pub training: Option<Training>,
#[serde(default)]
pub targets: BTreeMap<String, Target>,
#[serde(default)]
pub runpod: Runpod,
pub hf_token: Option<SecretString>,
pub log: Option<String>,
}
#[derive(Debug, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct Project {
pub name: String,
}
#[derive(Debug, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct Topic {
pub name: String,
pub description: Option<String>,
pub subtopics: u32,
pub questions_per_subtopic: u32,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum Protocol {
Openai,
Anthropic,
}
#[derive(Debug, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct Provider {
pub protocol: Protocol,
pub base_url: Option<String>,
pub api_key: Option<SecretString>,
}
#[derive(Debug, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct Roles {
pub generator: RoleModel,
pub parent: RoleModel,
pub embedder: Option<RoleModel>,
}
impl Roles {
#[must_use]
pub fn all(&self) -> Vec<(&'static str, &RoleModel)> {
let mut roles = vec![("generator", &self.generator), ("parent", &self.parent)];
if let Some(embedder) = &self.embedder {
roles.push(("embedder", embedder));
}
roles
}
}
#[derive(Debug, Clone, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct RoleModel {
pub provider: String,
pub model: String,
#[serde(default)]
pub reasoning: bool,
#[serde(default = "default_max_tokens")]
pub max_tokens: u32,
pub temperature: Option<f64>,
pub reasoning_effort: Option<Effort>,
pub thinking_budget: Option<u32>,
}
fn default_max_tokens() -> u32 {
16_384
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum Effort {
Low,
Medium,
High,
}
impl Effort {
#[must_use]
pub fn as_str(self) -> &'static str {
match self {
Self::Low => "low",
Self::Medium => "medium",
Self::High => "high",
}
}
}
#[derive(Debug, Deserialize)]
#[serde(default, deny_unknown_fields)]
pub struct Pipeline {
pub concurrency: usize,
pub max_retries: u32,
pub dedup_threshold: f64,
pub eval_ratio: f64,
pub seed: u64,
pub include_system_prompt: bool,
pub embedding_threshold: f64,
pub question_batch_size: u32,
pub request_timeout_secs: u64,
}
impl Default for Pipeline {
fn default() -> Self {
Self {
concurrency: 8,
max_retries: 5,
dedup_threshold: 0.8,
eval_ratio: 0.1,
seed: 42,
include_system_prompt: false,
embedding_threshold: 0.9,
question_batch_size: 10,
request_timeout_secs: 600,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum Adapter {
Lora,
Qlora,
Full,
}
#[derive(Debug, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct Training {
pub target: String,
pub base_model: String,
pub adapter: Adapter,
#[serde(default = "default_epochs")]
pub epochs: u32,
#[serde(default = "default_learning_rate")]
pub learning_rate: f64,
#[serde(default = "default_lora_r")]
pub lora_r: u32,
#[serde(default = "default_lora_alpha")]
pub lora_alpha: u32,
#[serde(default = "default_lora_dropout")]
pub lora_dropout: f64,
#[serde(default = "default_sequence_len")]
pub sequence_len: u32,
#[serde(default = "default_micro_batch_size")]
pub micro_batch_size: u32,
#[serde(default = "default_gradient_accumulation_steps")]
pub gradient_accumulation_steps: u32,
#[serde(default = "default_optimizer")]
pub optimizer: String,
#[serde(default = "default_lr_scheduler")]
pub lr_scheduler: String,
#[serde(default = "default_sample_packing")]
pub sample_packing: bool,
#[serde(default = "default_evals_per_epoch")]
pub evals_per_epoch: u32,
#[serde(default = "default_saves_per_epoch")]
pub saves_per_epoch: u32,
#[serde(default)]
pub merge: bool,
pub hub_model_id: Option<String>,
#[serde(default)]
pub axolotl_extra: BTreeMap<String, serde_json::Value>,
}
fn default_epochs() -> u32 {
3
}
fn default_learning_rate() -> f64 {
2e-4
}
fn default_lora_r() -> u32 {
16
}
fn default_lora_alpha() -> u32 {
32
}
fn default_lora_dropout() -> f64 {
0.05
}
fn default_sequence_len() -> u32 {
4096
}
fn default_micro_batch_size() -> u32 {
2
}
fn default_gradient_accumulation_steps() -> u32 {
4
}
fn default_optimizer() -> String {
"adamw_torch_fused".to_string()
}
fn default_lr_scheduler() -> String {
"cosine".to_string()
}
fn default_sample_packing() -> bool {
true
}
fn default_evals_per_epoch() -> u32 {
4
}
fn default_saves_per_epoch() -> u32 {
1
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum Runtime {
Docker,
Native,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum Engine {
Docker,
Podman,
}
impl Engine {
#[must_use]
pub fn command(self) -> &'static str {
match self {
Self::Docker => "docker",
Self::Podman => "podman",
}
}
}
pub const DEFAULT_IMAGE: &str = "axolotlai/axolotl:0.19.0-py3.12-cu130-2.12.1@sha256:9de7c7a5b8830480a7d2eb3b6d49759586615f5f8eb1126d5df29f8bd9fa324b";
pub const DEFAULT_WORKDIR: &str = "overbrainer";
#[derive(Debug, Deserialize)]
#[serde(tag = "kind", rename_all = "lowercase", deny_unknown_fields)]
pub enum Target {
Local {
runtime: Runtime,
engine: Option<Engine>,
image: Option<String>,
venv: Option<String>,
},
Ssh {
runtime: Runtime,
host: Option<String>,
workdir: Option<String>,
engine: Option<Engine>,
image: Option<String>,
venv: Option<String>,
},
Runpod {
#[serde(deserialize_with = "string_list")]
gpu_types: Vec<String>,
#[serde(default = "default_gpu_count", deserialize_with = "number")]
gpu_count: u32,
image: Option<String>,
venv: Option<String>,
#[serde(default = "default_container_disk_gb", deserialize_with = "number")]
container_disk_gb: u32,
#[serde(deserialize_with = "number")]
max_hours: f64,
#[serde(default = "default_boot_grace_minutes", deserialize_with = "number")]
boot_grace_minutes: u32,
#[serde(
default = "default_retrieve_grace_minutes",
deserialize_with = "number"
)]
retrieve_grace_minutes: u32,
#[serde(default, deserialize_with = "string_list")]
data_center_ids: Vec<String>,
network_volume_id: Option<String>,
},
}
pub const DEFAULT_RUNPOD_IMAGE: &str = "axolotlai/axolotl-cloud-term:0.19.0-py3.12-cu130-2.12.1@sha256:f7b94da82913920a003e28e091d8528f57f76da7360fca87e95faf62fa32a680";
pub const DEFAULT_RUNPOD_VENV: &str = "/workspace/axolotl-venv";
pub const DEFAULT_RUNPOD_BASE_URL: &str = "https://api.runpod.io/v2";
fn string_list<'de, D>(deserializer: D) -> Result<Vec<String>, D::Error>
where
D: Deserializer<'de>,
{
struct ListVisitor;
impl<'de> Visitor<'de> for ListVisitor {
type Value = Vec<String>;
fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str("a list of strings or a comma-separated string")
}
fn visit_str<E: de::Error>(self, value: &str) -> Result<Vec<String>, E> {
if value.trim().is_empty() {
return Ok(Vec::new());
}
Ok(value
.split(',')
.map(|item| item.trim().to_string())
.collect())
}
fn visit_seq<A: de::SeqAccess<'de>>(self, mut seq: A) -> Result<Vec<String>, A::Error> {
let mut items = Vec::new();
while let Some(item) = seq.next_element::<String>()? {
items.push(item.trim().to_string());
}
Ok(items)
}
}
deserializer.deserialize_any(ListVisitor)
}
fn number<'de, D, T>(deserializer: D) -> Result<T, D::Error>
where
D: Deserializer<'de>,
T: FromStr,
{
struct NumberVisitor<T>(PhantomData<T>);
impl<T: FromStr> NumberVisitor<T> {
fn parse<E: de::Error>(&self, text: &str, unexpected: Unexpected<'_>) -> Result<T, E> {
text.trim()
.parse()
.map_err(|_| E::invalid_value(unexpected, self))
}
}
impl<T: FromStr> Visitor<'_> for NumberVisitor<T> {
type Value = T;
fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str("a number")
}
fn visit_u64<E: de::Error>(self, value: u64) -> Result<T, E> {
self.parse(&value.to_string(), Unexpected::Unsigned(value))
}
fn visit_i64<E: de::Error>(self, value: i64) -> Result<T, E> {
self.parse(&value.to_string(), Unexpected::Signed(value))
}
fn visit_f64<E: de::Error>(self, value: f64) -> Result<T, E> {
self.parse(&value.to_string(), Unexpected::Float(value))
}
fn visit_str<E: de::Error>(self, value: &str) -> Result<T, E> {
self.parse(value, Unexpected::Str(value))
}
}
deserializer.deserialize_any(NumberVisitor(PhantomData))
}
fn default_gpu_count() -> u32 {
1
}
fn default_container_disk_gb() -> u32 {
50
}
fn default_boot_grace_minutes() -> u32 {
30
}
fn default_retrieve_grace_minutes() -> u32 {
60
}
#[derive(Debug, Default, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct Runpod {
pub api_key: Option<SecretString>,
pub base_url: Option<String>,
}