Add typed generation preferences

This commit is contained in:
Georg Bauer
2026-07-24 15:26:15 +02:00
parent ad7167e84a
commit bccce697af
9 changed files with 529 additions and 28 deletions

159
src/settings.rs Normal file
View File

@@ -0,0 +1,159 @@
use crate::model::ModelChoice;
use std::fmt;
pub(crate) const REASONING_MODES: [ReasoningMode; 3] = [
ReasoningMode::High,
ReasoningMode::Max,
ReasoningMode::Direct,
];
const THINK_MAX_MIN_CONTEXT: i32 = 393_216;
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
pub(crate) enum ReasoningMode {
Direct,
#[default]
High,
Max,
}
impl ReasoningMode {
pub(crate) fn id(self) -> &'static str {
match self {
Self::Direct => "none",
Self::High => "high",
Self::Max => "max",
}
}
pub(crate) fn from_id(id: &str) -> Option<Self> {
REASONING_MODES.into_iter().find(|mode| mode.id() == id)
}
}
impl fmt::Display for ReasoningMode {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str(match self {
Self::Direct => "Direct (no thinking)",
Self::High => "Thinking",
Self::Max => "Think Max",
})
}
}
#[derive(Clone, Debug, PartialEq)]
pub(crate) struct GenerationPreferences {
pub(crate) context_tokens: i32,
pub(crate) max_generated_tokens: i32,
pub(crate) system_prompt: String,
pub(crate) temperature: Option<f32>,
pub(crate) top_p: Option<f32>,
pub(crate) min_p: Option<f32>,
pub(crate) seed: Option<u64>,
pub(crate) reasoning_mode: ReasoningMode,
}
impl Default for GenerationPreferences {
fn default() -> Self {
Self {
context_tokens: 32_768,
max_generated_tokens: 50_000,
system_prompt: "You are a helpful assistant".into(),
temperature: None,
top_p: None,
min_p: None,
seed: None,
reasoning_mode: ReasoningMode::High,
}
}
}
impl GenerationPreferences {
pub(crate) fn validate(&self) -> Result<(), String> {
if self.context_tokens <= 0 {
return Err("Context tokens must be a positive whole number.".into());
}
if self.max_generated_tokens <= 0 {
return Err("Maximum generated tokens must be a positive whole number.".into());
}
validate_optional_float("Temperature", self.temperature, 0.0, 100.0)?;
validate_optional_float("Top-p", self.top_p, 0.0, 1.0)?;
validate_optional_float("Min-p", self.min_p, 0.0, 1.0)?;
if self.seed == Some(0) {
return Err("Seed must be a positive whole number.".into());
}
Ok(())
}
pub(crate) fn effective(&self, model: ModelChoice) -> EffectiveGenerationSettings {
let glm = model == ModelChoice::Glm52;
EffectiveGenerationSettings {
context_tokens: self.context_tokens,
max_generated_tokens: self.max_generated_tokens,
system_prompt: self.system_prompt.clone(),
temperature: self.temperature.unwrap_or(1.0),
top_p: self.top_p.unwrap_or(if glm { 0.95 } else { 1.0 }),
min_p: self.min_p.unwrap_or(if glm { 0.0 } else { 0.05 }),
seed: self.seed,
reasoning_mode: if self.reasoning_mode == ReasoningMode::Max
&& self.context_tokens < THINK_MAX_MIN_CONTEXT
{
ReasoningMode::High
} else {
self.reasoning_mode
},
}
}
}
fn validate_optional_float(
name: &str,
value: Option<f32>,
minimum: f32,
maximum: f32,
) -> Result<(), String> {
if value.is_some_and(|value| !value.is_finite() || !(minimum..=maximum).contains(&value)) {
return Err(format!("{name} must be between {minimum} and {maximum}."));
}
Ok(())
}
#[derive(Clone, Debug, PartialEq)]
pub(crate) struct EffectiveGenerationSettings {
pub(crate) context_tokens: i32,
pub(crate) max_generated_tokens: i32,
pub(crate) system_prompt: String,
pub(crate) temperature: f32,
pub(crate) top_p: f32,
pub(crate) min_p: f32,
pub(crate) seed: Option<u64>,
pub(crate) reasoning_mode: ReasoningMode,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn effective_settings_preserve_unset_model_defaults() {
let defaults = GenerationPreferences::default();
let deepseek = defaults.effective(ModelChoice::DeepSeekV4Flash);
assert_eq!(
(deepseek.temperature, deepseek.top_p, deepseek.min_p),
(1.0, 1.0, 0.05)
);
let glm = defaults.effective(ModelChoice::Glm52);
assert_eq!((glm.temperature, glm.top_p, glm.min_p), (1.0, 0.95, 0.0));
let explicit = GenerationPreferences {
top_p: Some(0.4),
min_p: Some(0.2),
reasoning_mode: ReasoningMode::Max,
..defaults
};
let effective = explicit.effective(ModelChoice::Glm52);
assert_eq!((effective.top_p, effective.min_p), (0.4, 0.2));
assert_eq!(effective.reasoning_mode, ReasoningMode::High);
}
}