use serde::{Deserialize, Serialize};
use crate::error::{OrchestraError, Result};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ModelConfig {
pub name: String,
pub system_instruction: Option<String>,
pub temperature: f32,
pub top_p: f32,
pub top_k: Option<u32>,
pub max_tokens: Option<u32>,
pub thinking_mode: Option<bool>,
pub stop_sequences: Vec<String>,
}
impl Default for ModelConfig {
fn default() -> Self {
ModelConfig {
name: String::new(),
system_instruction: None,
temperature: 1.0,
top_p: 0.95,
top_k: None,
max_tokens: None,
thinking_mode: None,
stop_sequences: Vec::new(),
}
}
}
impl ModelConfig {
pub fn new<S: Into<String>>(name: S) -> Self {
Self {
name: name.into(),
..Default::default()
}
}
pub fn with_name<S: Into<String>>(mut self, name: S) -> Self {
self.name = name.into();
self
}
pub fn with_system_instruction<S: Into<String>>(mut self, instruction: S) -> Self {
self.system_instruction = Some(instruction.into());
self
}
pub fn with_temperature(mut self, temperature: f32) -> Result<Self> {
if !(0.0..=2.0).contains(&temperature) {
return Err(OrchestraError::config("Temperature must be between 0.0 and 2.0"));
}
self.temperature = temperature;
Ok(self)
}
pub fn with_top_p(mut self, top_p: f32) -> Result<Self> {
if !(0.0..=1.0).contains(&top_p) {
return Err(OrchestraError::config("top_p must be between 0.0 and 1.0"));
}
self.top_p = top_p;
Ok(self)
}
pub fn with_top_k(mut self, top_k: u32) -> Self {
self.top_k = Some(top_k);
self
}
pub fn with_max_tokens(mut self, max_tokens: u32) -> Self {
self.max_tokens = Some(max_tokens);
self
}
pub fn with_thinking_mode(mut self, thinking_mode: bool) -> Self {
self.thinking_mode = Some(thinking_mode);
self
}
pub fn with_stop_sequence<S: Into<String>>(mut self, stop_sequence: S) -> Self {
self.stop_sequences.push(stop_sequence.into());
self
}
pub fn with_stop_sequences<I, S>(mut self, stop_sequences: I) -> Self
where
I: IntoIterator<Item = S>,
S: Into<String>,
{
self.stop_sequences = stop_sequences.into_iter().map(|s| s.into()).collect();
self
}
pub fn validate(&self) -> Result<()> {
if self.name.is_empty() {
return Err(OrchestraError::config("Model name cannot be empty"));
}
if !(0.0..=2.0).contains(&self.temperature) {
return Err(OrchestraError::config("Temperature must be between 0.0 and 2.0"));
}
if !(0.0..=1.0).contains(&self.top_p) {
return Err(OrchestraError::config("top_p must be between 0.0 and 1.0"));
}
if let Some(max_tokens) = self.max_tokens {
if max_tokens == 0 {
return Err(OrchestraError::config("max_tokens must be greater than 0"));
}
}
Ok(())
}
pub fn conservative<S: Into<String>>(name: S) -> Self {
Self::new(name)
.with_temperature(0.3)
.unwrap()
.with_top_p(0.8)
.unwrap()
}
pub fn creative<S: Into<String>>(name: S) -> Self {
Self::new(name)
.with_temperature(1.2)
.unwrap()
.with_top_p(0.95)
.unwrap()
}
pub fn balanced<S: Into<String>>(name: S) -> Self {
Self::new(name)
.with_temperature(0.7)
.unwrap()
.with_top_p(0.9)
.unwrap()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_model_config_new() {
let config = ModelConfig::new("test-model");
assert_eq!(config.name, "test-model");
assert_eq!(config.temperature, 1.0);
assert_eq!(config.top_p, 0.95);
assert!(config.system_instruction.is_none());
assert!(config.top_k.is_none());
assert!(config.max_tokens.is_none());
assert!(config.thinking_mode.is_none());
assert!(config.stop_sequences.is_empty());
}
#[test]
fn test_model_config_builder() {
let config = ModelConfig::new("test-model")
.with_system_instruction("You are helpful")
.with_temperature(0.5)
.unwrap()
.with_top_p(0.8)
.unwrap()
.with_top_k(40)
.with_max_tokens(1000)
.with_thinking_mode(true)
.with_stop_sequence("STOP");
assert_eq!(config.name, "test-model");
assert_eq!(config.system_instruction, Some("You are helpful".to_string()));
assert_eq!(config.temperature, 0.5);
assert_eq!(config.top_p, 0.8);
assert_eq!(config.top_k, Some(40));
assert_eq!(config.max_tokens, Some(1000));
assert_eq!(config.thinking_mode, Some(true));
assert_eq!(config.stop_sequences, vec!["STOP"]);
}
#[test]
fn test_model_config_validation() {
let config = ModelConfig::new("test-model");
assert!(config.validate().is_ok());
let mut config = ModelConfig::new("");
assert!(config.validate().is_err());
config = ModelConfig::new("test");
config.temperature = 3.0;
assert!(config.validate().is_err());
config.temperature = -1.0;
assert!(config.validate().is_err());
config = ModelConfig::new("test");
config.top_p = 1.5;
assert!(config.validate().is_err());
config.top_p = -0.1;
assert!(config.validate().is_err());
config = ModelConfig::new("test");
config.max_tokens = Some(0);
assert!(config.validate().is_err());
}
#[test]
fn test_model_config_temperature_validation() {
let config = ModelConfig::new("test");
assert!(config.clone().with_temperature(0.0).is_ok());
assert!(config.clone().with_temperature(1.0).is_ok());
assert!(config.clone().with_temperature(2.0).is_ok());
assert!(config.clone().with_temperature(-0.1).is_err());
assert!(config.clone().with_temperature(2.1).is_err());
}
#[test]
fn test_model_config_top_p_validation() {
let config = ModelConfig::new("test");
assert!(config.clone().with_top_p(0.0).is_ok());
assert!(config.clone().with_top_p(0.5).is_ok());
assert!(config.clone().with_top_p(1.0).is_ok());
assert!(config.clone().with_top_p(-0.1).is_err());
assert!(config.clone().with_top_p(1.1).is_err());
}
#[test]
fn test_model_config_presets() {
let conservative = ModelConfig::conservative("test-model");
assert_eq!(conservative.temperature, 0.3);
assert_eq!(conservative.top_p, 0.8);
let creative = ModelConfig::creative("test-model");
assert_eq!(creative.temperature, 1.2);
assert_eq!(creative.top_p, 0.95);
let balanced = ModelConfig::balanced("test-model");
assert_eq!(balanced.temperature, 0.7);
assert_eq!(balanced.top_p, 0.9);
}
#[test]
fn test_model_config_stop_sequences() {
let config = ModelConfig::new("test")
.with_stop_sequences(vec!["STOP", "END", "FINISH"]);
assert_eq!(config.stop_sequences.len(), 3);
assert!(config.stop_sequences.contains(&"STOP".to_string()));
assert!(config.stop_sequences.contains(&"END".to_string()));
assert!(config.stop_sequences.contains(&"FINISH".to_string()));
}
}