#[derive(Clone, Debug, PartialEq)]
pub struct ChatSettings {
pub model: String,
pub temperature: f64,
pub max_tokens: usize,
pub system_prompt: String,
pub stream: bool,
}
impl Default for ChatSettings {
fn default() -> Self {
Self {
model: "deepseek-chat".to_owned(),
temperature: 0.2,
max_tokens: 2048,
system_prompt: "Answer from the indexed corpus and cite sources.".to_owned(),
stream: true,
}
}
}
impl ChatSettings {
pub const MODELS: &'static [(&'static str, &'static str)] = &[
("deepseek-chat", "DeepSeek Chat"),
("qwen3:8b", "Qwen3 8B (Ollama)"),
("gpt-oss:20b", "GPT-OSS 20B (Ollama)"),
];
pub fn validate(&self) -> Result<(), String> {
if self.model.trim().is_empty() {
return Err("choose a chat model".to_owned());
}
if !self.temperature.is_finite() || !(0.0..=2.0).contains(&self.temperature) {
return Err("temperature must be between 0 and 2".to_owned());
}
if self.max_tokens == 0 {
return Err("max tokens must be at least 1".to_owned());
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn defaults_are_valid() {
assert_eq!(ChatSettings::default().validate(), Ok(()));
}
#[test]
fn invalid_values_are_rejected() {
let mut settings = ChatSettings {
model: " ".to_owned(),
..Default::default()
};
assert_eq!(settings.validate(), Err("choose a chat model".to_owned()));
settings = ChatSettings {
temperature: 2.5,
..Default::default()
};
assert_eq!(
settings.validate(),
Err("temperature must be between 0 and 2".to_owned())
);
settings = ChatSettings {
max_tokens: 0,
..Default::default()
};
assert_eq!(
settings.validate(),
Err("max tokens must be at least 1".to_owned())
);
}
#[test]
fn the_default_model_is_in_the_offered_list() {
assert!(
ChatSettings::MODELS
.iter()
.any(|(id, _)| *id == ChatSettings::default().model)
);
}
}