Add typed generation preferences
This commit is contained in:
159
src/settings.rs
Normal file
159
src/settings.rs
Normal 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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user