correct thinking modes for each model

This commit is contained in:
Georg Bauer
2026-09-01 21:03:13 +02:00
parent ee515ee824
commit d40e86e5ef
11 changed files with 338 additions and 127 deletions

View File

@@ -1737,15 +1737,14 @@ impl App {
.supported_reasoning_modes(self.config.model)
.contains(&mode)
{
self.error = Some(
"Think Max requires a context window of at least 393216 tokens.".into(),
);
self.error = Some(format!("{mode} is not supported by {}.", self.config.model));
} else {
let mut config = self.config.clone();
let model = config.model;
config
.model_profiles
.entry(config.model)
.or_default()
.entry(model)
.or_insert_with(|| ModelPreferences::defaults_for(model))
.reasoning_mode = mode;
match config.save(&config_path()) {
Ok(()) => {

View File

@@ -196,6 +196,7 @@ impl PreferenceDraft {
}
fn select_generation(&mut self, model: ModelChoice, mode: ReasoningMode) -> Result<(), String> {
model.validate_reasoning_mode(mode)?;
self.store_generation()?;
self.load_generation(model, mode);
Ok(())
@@ -245,14 +246,18 @@ impl PreferenceDraft {
let profile = self
.model_profiles
.entry(self.acceleration_model)
.or_default();
.or_insert_with(|| ModelPreferences::defaults_for(self.acceleration_model));
profile.speculative = speculative;
profile.ssd = ssd;
Ok(())
}
fn load_acceleration(&mut self, model: ModelChoice) {
let profile = self.model_profiles.get(&model).cloned().unwrap_or_default();
let profile = self
.model_profiles
.get(&model)
.cloned()
.unwrap_or_else(|| ModelPreferences::defaults_for(model));
let speculative = profile.speculative;
let ssd = profile.ssd;
self.acceleration_model = model;
@@ -301,9 +306,10 @@ impl PreferenceDraft {
}
pub(super) fn reasoning_mode_for(&self, model: ModelChoice) -> ReasoningMode {
self.model_profiles
.get(&model)
.map_or(ReasoningMode::default(), |profile| profile.reasoning_mode)
self.model_profiles.get(&model).map_or_else(
|| model.default_reasoning_mode(),
|profile| profile.reasoning_mode,
)
}
pub(super) fn runtime(&self) -> Result<RuntimePreferences, String> {
@@ -518,7 +524,7 @@ impl App {
self.preference_draft
.model_profiles
.entry(self.preference_draft.model)
.or_default()
.or_insert_with(|| ModelPreferences::defaults_for(self.preference_draft.model))
.reasoning_mode = self.preference_draft.default_reasoning_mode;
let config = Config {
model: self.preference_draft.model,
@@ -684,7 +690,7 @@ impl App {
self.preference_draft
.model_profiles
.entry(self.preference_draft.model)
.or_default()
.or_insert_with(|| ModelPreferences::defaults_for(self.preference_draft.model))
.reasoning_mode = mode;
self.preference_error = None;
}
@@ -1016,7 +1022,7 @@ mod tests {
assert_eq!(draft.context_tokens, "32768");
draft.context_tokens = "456".into();
draft
.select_generation(ModelChoice::DeepSeekV4Flash0731, ReasoningMode::High)
.select_generation(ModelChoice::DeepSeekV4Flash0731, ReasoningMode::Low)
.unwrap();
assert_eq!(draft.context_tokens, "123");

View File

@@ -19,7 +19,7 @@ use crate::database::{ProjectWithSessions, Session, SessionState};
use crate::model::{
self, DownloadPhase, MODEL_CHOICES, ManagedArtifact, ManagedArtifactState, ModelChoice,
};
use crate::settings::{GIB, REASONING_MODES};
use crate::settings::GIB;
use iced::theme::{Palette, palette};
use iced::widget::{
Button, Space, Svg, Tooltip, button, column, container, image, markdown, mouse_area, opaque,

View File

@@ -114,7 +114,7 @@ impl App {
"Thinking mode selected when this model becomes active. Chat can switch it for later turns.",
),
pick_list(
&REASONING_MODES[..],
self.preference_draft.model.reasoning_modes(),
Some(self.preference_draft.default_reasoning_mode),
Message::PreferenceDefaultReasoningChanged,
)
@@ -462,7 +462,7 @@ impl App {
row![
text("Thinking mode").size(13).width(Length::Fill),
pick_list(
&REASONING_MODES[..],
self.preference_draft.generation_model.reasoning_modes(),
Some(self.preference_draft.generation_reasoning_mode),
Message::PreferenceGenerationReasoningChanged,
)
@@ -513,7 +513,7 @@ impl App {
text_input("Random", &self.preference_draft.seed)
.on_input(Message::PreferenceSeedChanged),
),
text("Blank sampling values retain DS4's model-family defaults. Think Max needs at least 393216 context tokens.")
text("Blank sampling values retain the model-family defaults.")
.size(12),
text(turn.map_or_else(
|| "Effective settings will appear after valid values are entered.".to_owned(),

View File

@@ -7,8 +7,8 @@ use std::path::Path;
use crate::model::{MODEL_CHOICES, ModelChoice};
use crate::settings::{
DEFAULT_SYSTEM_PROMPT, GenerationPreferences, REASONING_MODES, ReasoningMode,
RuntimePreferences, SpeculativePreferences, SsdPreferences,
DEFAULT_SYSTEM_PROMPT, GenerationPreferences, ReasoningMode, RuntimePreferences,
SpeculativePreferences, SsdPreferences,
};
#[derive(Clone, Debug, Default, Deserialize, PartialEq, Serialize)]
@@ -20,10 +20,15 @@ pub(crate) struct ModelPreferences {
}
impl ModelPreferences {
fn defaults_for(model: ModelChoice) -> Self {
let mut preferences = Self::default();
preferences.speculative.dspark_enabled = model == ModelChoice::DeepSeekV4Flash0731;
preferences
pub(crate) fn defaults_for(model: ModelChoice) -> Self {
Self {
reasoning_mode: model.default_reasoning_mode(),
speculative: SpeculativePreferences {
dspark_enabled: model == ModelChoice::DeepSeekV4Flash0731,
..SpeculativePreferences::default()
},
ssd: SsdPreferences::default(),
}
}
}
@@ -63,8 +68,10 @@ impl Default for Config {
.map(|model| {
(
model,
REASONING_MODES
.into_iter()
model
.reasoning_modes()
.iter()
.copied()
.map(|mode| (mode, GenerationPreferences::default()))
.collect(),
)
@@ -302,9 +309,20 @@ impl Config {
drop_legacy_model_settings(&mut value);
let migrated = migrate_deprecated_flash(&mut value);
let dspark_explicit = deepseek_0731_dspark_is_explicit(&value);
let explicit_reasoning =
MODEL_CHOICES.map(|model| (model, model_reasoning_mode_is_explicit(&value, model)));
let mut config: Self = serde_norway::from_value(value)
.map_err(|error| format!("Could not read {}: {error}", path.display()))?;
config.fill_profile_defaults();
for (model, explicit) in explicit_reasoning {
if !explicit {
config
.model_profiles
.get_mut(&model)
.expect("model profile is present")
.reasoning_mode = model.default_reasoning_mode();
}
}
if !dspark_explicit {
config
.model_profiles
@@ -344,6 +362,7 @@ impl Config {
}
for (model, profiles) in &self.generation_profiles {
for (mode, generation) in profiles {
model.validate_reasoning_mode(*mode)?;
let mut generation = generation.clone();
generation.system_prompt = self.system_prompt.clone();
generation.reasoning_mode = *mode;
@@ -355,24 +374,17 @@ impl Config {
for (model, preferences) in &self.model_profiles {
preferences.speculative.validate(*model)?;
preferences.ssd.validate(*model)?;
if preferences.reasoning_mode == ReasoningMode::Max
&& !self
.supported_reasoning_modes(*model)
.contains(&ReasoningMode::Max)
{
return Err(format!(
"Default thinking mode for {model} requires its Think Max profile to use at least 393216 context tokens."
));
}
model.validate_reasoning_mode(preferences.reasoning_mode)?;
}
self.runtime_for(self.model).validate(self.model)?;
self.dev_brain.validate()
}
pub(crate) fn reasoning_mode(&self, model: ModelChoice) -> ReasoningMode {
self.model_profiles
.get(&model)
.map_or(ReasoningMode::default(), |profile| profile.reasoning_mode)
self.model_profiles.get(&model).map_or_else(
|| model.default_reasoning_mode(),
|profile| profile.reasoning_mode,
)
}
pub(crate) fn generation_for(
@@ -405,8 +417,7 @@ impl Config {
}
pub(crate) fn supported_reasoning_modes(&self, model: ModelChoice) -> &'static [ReasoningMode] {
let max = self.generation_for(model, ReasoningMode::Max);
max.supported_reasoning_modes()
model.reasoning_modes()
}
fn fill_profile_defaults(&mut self) {
@@ -415,13 +426,24 @@ impl Config {
.entry(model)
.or_insert_with(|| ModelPreferences::defaults_for(model));
let profiles = self.generation_profiles.entry(model).or_default();
for mode in REASONING_MODES {
for &mode in model.reasoning_modes() {
profiles.entry(mode).or_default();
}
}
}
}
fn model_reasoning_mode_is_explicit(value: &Value, model: ModelChoice) -> bool {
let Value::Mapping(root) = value else {
return false;
};
root.get(Value::String("model_profiles".into()))
.and_then(Value::as_mapping)
.and_then(|profiles| profiles.get(Value::String(model.id().into())))
.and_then(Value::as_mapping)
.is_some_and(|profile| profile.contains_key(Value::String("reasoning_mode".into())))
}
fn deepseek_0731_dspark_is_explicit(value: &Value) -> bool {
let Value::Mapping(root) = value else {
return false;
@@ -529,6 +551,34 @@ mod tests {
fs::remove_dir_all(&directory).unwrap();
}
#[test]
fn model_profiles_expose_only_their_upstream_thinking_modes() {
let config = Config::default();
for model in MODEL_CHOICES {
assert_eq!(config.reasoning_mode(model), model.default_reasoning_mode());
assert_eq!(
config.supported_reasoning_modes(model),
model.reasoning_modes()
);
let profiles = config.generation_profiles.get(&model).unwrap();
assert_eq!(profiles.len(), model.reasoning_modes().len());
assert!(
model
.reasoning_modes()
.iter()
.all(|mode| profiles.contains_key(mode))
);
}
let mut invalid = config;
invalid
.model_profiles
.get_mut(&ModelChoice::Glm53Flash)
.unwrap()
.reasoning_mode = ReasoningMode::Direct;
assert!(invalid.validate().is_err());
}
#[test]
fn explicit_0731_dspark_opt_out_survives_the_default() {
let directory =

View File

@@ -1690,12 +1690,13 @@ fn conversation_key(system: &str, reasoning: ReasoningMode, messages: &[ChatTurn
output.extend_from_slice(value.as_bytes());
}
let mut output = b"DS4Server chat checkpoint v4".to_vec();
let mut output = b"DS4Server chat checkpoint v5".to_vec();
text(&mut output, system);
output.push(match reasoning {
ReasoningMode::Direct => 0,
ReasoningMode::High => 1,
ReasoningMode::Max => 2,
ReasoningMode::Low => 1,
ReasoningMode::High => 2,
ReasoningMode::Max => 3,
});
for message in messages {
output.push(u8::from(message.user));

View File

@@ -6,9 +6,23 @@ use super::{
use crate::settings::ReasoningMode;
use std::collections::HashMap;
const MAX_REASONING_PREFIX: &str = "Reasoning Effort: Absolute maximum with no shortcuts permitted.\n\
const HIGH_REASONING_PREFIX: &str = "Reasoning Effort: Absolute maximum with no shortcuts permitted.\n\
You MUST be very thorough in your thinking and comprehensively decompose the problem to resolve the root cause, rigorously stress-testing your logic against all potential paths, edge cases, and adversarial scenarios.\n\
Explicitly write out your entire deliberation process, documenting every intermediate step, considered alternative, and rejected hypothesis to ensure absolutely no assumption is left unchecked.\n\n";
const MAX_REASONING_PREFIX: &str = "Reasoning Effort: Beyond maximum — exhaustive, relentless, and uncompromising.\n\
You MUST reason with the utmost depth and rigor, leaving absolutely nothing to chance: exhaustively decompose the problem into its most fundamental components, trace every causal chain to its root, and resolve the underlying cause rather than any surface symptom.\n\
Do not stop reasoning until you have independently verified the solution from multiple angles and are certain that no assumption remains unchecked and no error remains undiscovered.\n\n";
fn reasoning_prefix(family: ModelFamily, reasoning: ReasoningMode) -> Option<&'static str> {
match (family, reasoning) {
(ModelFamily::DeepSeek, ReasoningMode::High) => Some(HIGH_REASONING_PREFIX),
(ModelFamily::DeepSeek, ReasoningMode::Max) => Some(MAX_REASONING_PREFIX),
(ModelFamily::Glm, ReasoningMode::Low) => Some("Reasoning Effort: Low"),
(ModelFamily::Glm, ReasoningMode::High) => Some("Reasoning Effort: High"),
(ModelFamily::Glm, ReasoningMode::Max) => Some("Reasoning Effort: Max"),
(_, ReasoningMode::Direct | ReasoningMode::Low) => None,
}
}
pub(super) struct Tokenizer {
family: ModelFamily,
@@ -254,19 +268,11 @@ impl Tokenizer {
if self.family == ModelFamily::Glm && self.sop >= 0 {
output.push(self.sop);
}
match (self.family, reasoning) {
(ModelFamily::Glm, ReasoningMode::High | ReasoningMode::Max) => {
if let Some(prefix) = reasoning_prefix(self.family, reasoning) {
if self.family == ModelFamily::Glm {
output.push(self.system);
output.extend(self.tokenize(if reasoning == ReasoningMode::Max {
"Reasoning Effort: Max"
} else {
"Reasoning Effort: High"
}));
}
(ModelFamily::DeepSeek, ReasoningMode::Max) => {
output.extend(self.tokenize(MAX_REASONING_PREFIX));
}
_ => {}
output.extend(self.tokenize(prefix));
}
if !system_prompt.is_empty() {
if self.family == ModelFamily::Glm {
@@ -843,4 +849,32 @@ mod tests {
assert_eq!(next_char("", 0), 0);
assert!(!cjk_at("", 0));
}
#[test]
fn reasoning_prefixes_match_each_model_family_contract() {
assert_eq!(
reasoning_prefix(ModelFamily::DeepSeek, ReasoningMode::Low),
None
);
assert_eq!(
reasoning_prefix(ModelFamily::DeepSeek, ReasoningMode::High),
Some(HIGH_REASONING_PREFIX)
);
assert_eq!(
reasoning_prefix(ModelFamily::DeepSeek, ReasoningMode::Max),
Some(MAX_REASONING_PREFIX)
);
assert_eq!(
reasoning_prefix(ModelFamily::Glm, ReasoningMode::Low),
Some("Reasoning Effort: Low")
);
assert_eq!(
reasoning_prefix(ModelFamily::Glm, ReasoningMode::High),
Some("Reasoning Effort: High")
);
assert_eq!(
reasoning_prefix(ModelFamily::Glm, ReasoningMode::Max),
Some("Reasoning Effort: Max")
);
}
}

View File

@@ -540,7 +540,12 @@ fn model_json(id: &str, model: ModelChoice, context: i32, default_tokens: i32) -
"supported_parameters": [
"tools", "tool_choice", "max_tokens", "temperature", "top_p",
"top_k", "min_p", "stop", "seed", "stream", "reasoning_effort"
]
],
"supported_reasoning_efforts": model
.reasoning_modes()
.iter()
.map(|mode| mode.as_id())
.collect::<Vec<_>>()
})
}
@@ -555,10 +560,8 @@ fn model_alias(id: &str) -> Option<ModelChoice> {
| "zai/glm-5.2-chat"
| "zai/glm-5.2-reasoner" => Some(ModelChoice::Glm52),
"glm-5.3-flash"
| "glm-5.3-flash-chat"
| "glm-5.3-flash-reasoner"
| "zai/glm-5.3-flash"
| "zai/glm-5.3-flash-chat"
| "zai/glm-5.3-flash-reasoner" => Some(ModelChoice::Glm53Flash),
_ => ModelChoice::from_id(id),
}
@@ -1023,6 +1026,7 @@ mod tests {
model_alias("deepseek-reasoner"),
Some(ModelChoice::DeepSeekV4Flash0731)
);
assert_eq!(model_alias("glm-5.3-flash-chat"), None);
let model = model_json(
"deepseek-reasoner",
ModelChoice::DeepSeekV4Flash0731,
@@ -1031,6 +1035,18 @@ mod tests {
);
assert_eq!(model["id"], "deepseek-reasoner");
assert_eq!(model["top_provider"]["max_completion_tokens"], 32_768);
assert_eq!(
model["supported_reasoning_efforts"],
json!(["low", "high", "max", "none"])
);
assert_eq!(
model_json("glm-5.2", ModelChoice::Glm52, 32_768, 50_000)["supported_reasoning_efforts"],
json!(["high", "max", "none"])
);
assert_eq!(
model_json("glm-5.3-flash", ModelChoice::Glm53Flash, 32_768, 50_000)["supported_reasoning_efforts"],
json!(["low", "high", "max"])
);
}
#[test]

View File

@@ -534,7 +534,7 @@ pub(super) fn parse_chat_request(
return Err((400, format!("model is not installed and verified: {model}")));
}
let reasoning_mode = request_reasoning(&request, requested_id)?;
let reasoning_mode = request_reasoning(&request, model, requested_id)?;
let mut generation = config.generation_for(model, reasoning_mode);
generation.max_generated_tokens = request
.max_completion_tokens
@@ -594,31 +594,43 @@ pub(super) fn parse_chat_request(
fn request_reasoning(
request: &ChatRequest,
model: ModelChoice,
model_id: &str,
) -> Result<ReasoningMode, (u16, String)> {
let explicit_thinking = request
.think
.or_else(|| request.thinking.as_ref().and_then(thinking_enabled));
let mut reasoning = match request.reasoning_effort.as_deref() {
Some("max") => ReasoningMode::Max,
Some("none") => ReasoningMode::Direct,
Some("xhigh" | "high" | "medium" | "low" | "minimal") | None => ReasoningMode::High,
let explicit_effort = match request.reasoning_effort.as_deref() {
Some("none") => Some(ReasoningMode::Direct),
Some("low") => Some(ReasoningMode::Low),
Some("high") => Some(ReasoningMode::High),
Some("max") => Some(ReasoningMode::Max),
None => None,
Some(value) => return Err((400, format!("unsupported reasoning_effort: {value}"))),
};
if explicit_thinking == Some(false)
|| (explicit_thinking.is_none()
&& matches!(
model_id,
"deepseek-chat"
| "glm-5.2-chat"
| "glm-5.2-no-think"
| "glm-5.2-nothink"
| "glm-5.3-flash-chat"
| "zai/glm-5.3-flash-chat"
))
let direct_alias = matches!(
model_id,
"deepseek-chat"
| "glm-5.2-chat"
| "glm-5.2-no-think"
| "glm-5.2-nothink"
| "zai/glm-5.2-chat"
);
if (direct_alias && explicit_thinking == Some(true))
|| ((direct_alias || explicit_thinking == Some(false))
&& explicit_effort.is_some_and(|mode| mode != ReasoningMode::Direct))
|| (explicit_thinking == Some(true) && explicit_effort == Some(ReasoningMode::Direct))
{
reasoning = ReasoningMode::Direct;
return Err((400, "conflicting thinking controls".into()));
}
let reasoning = if direct_alias || explicit_thinking == Some(false) {
ReasoningMode::Direct
} else {
explicit_effort.unwrap_or_else(|| model.default_reasoning_mode())
};
model
.validate_reasoning_mode(reasoning)
.map_err(|error| (400, error))?;
Ok(reasoning)
}
@@ -632,3 +644,70 @@ fn thinking_enabled(value: &Value) -> Option<bool> {
})
})
}
#[cfg(test)]
mod tests {
use super::*;
fn request(effort: Option<&str>, thinking: Option<bool>) -> ChatRequest {
let mut value = json!({"messages": [{"role": "user", "content": "hello"}]});
if let Some(effort) = effort {
value["reasoning_effort"] = Value::String(effort.into());
}
if let Some(thinking) = thinking {
value["thinking"] = Value::Bool(thinking);
}
decode_chat_request(value).unwrap()
}
#[test]
fn reasoning_requests_preserve_each_models_exact_modes() {
let default = request(None, None);
assert_eq!(
request_reasoning(&default, ModelChoice::DeepSeekV4Flash0731, "").unwrap(),
ReasoningMode::Low
);
assert_eq!(
request_reasoning(&default, ModelChoice::DeepSeekV4Pro, "").unwrap(),
ReasoningMode::Low
);
assert_eq!(
request_reasoning(&default, ModelChoice::Glm52, "").unwrap(),
ReasoningMode::Max
);
assert_eq!(
request_reasoning(&default, ModelChoice::Glm53Flash, "").unwrap(),
ReasoningMode::Max
);
for (effort, mode) in [
("none", ReasoningMode::Direct),
("low", ReasoningMode::Low),
("high", ReasoningMode::High),
("max", ReasoningMode::Max),
] {
let explicit = request(Some(effort), None);
for model in crate::model::MODEL_CHOICES {
let result = request_reasoning(&explicit, model, "");
if model.reasoning_modes().contains(&mode) {
assert_eq!(result.unwrap(), mode);
} else {
assert!(result.is_err());
}
}
}
assert!(request_reasoning(&request(Some("medium"), None), ModelChoice::Glm52, "").is_err());
assert!(
request_reasoning(&request(None, Some(false)), ModelChoice::Glm53Flash, "").is_err()
);
assert!(
request_reasoning(
&request(None, Some(true)),
ModelChoice::DeepSeekV4Flash0731,
"deepseek-chat"
)
.is_err()
);
}
}

View File

@@ -3,13 +3,19 @@ use serde::{Deserialize, Serialize};
use std::fmt;
use std::path::Path;
pub(crate) const REASONING_MODES: [ReasoningMode; 3] = [
const DEEPSEEK_REASONING_MODES: [ReasoningMode; 4] = [
ReasoningMode::Low,
ReasoningMode::High,
ReasoningMode::Max,
ReasoningMode::Direct,
];
const STANDARD_REASONING_MODES: [ReasoningMode; 2] = [ReasoningMode::High, ReasoningMode::Direct];
const THINK_MAX_MIN_CONTEXT: i32 = 393_216;
const GLM_52_REASONING_MODES: [ReasoningMode; 3] = [
ReasoningMode::High,
ReasoningMode::Max,
ReasoningMode::Direct,
];
const GLM_53_REASONING_MODES: [ReasoningMode; 3] =
[ReasoningMode::Low, ReasoningMode::High, ReasoningMode::Max];
const MAX_CPU_THREADS: u32 = 32;
pub(crate) const GIB: u64 = 1024 * 1024 * 1024;
/// DS4 disk KV cache defaults, from `ds4_kvstore.h` and `--kv-disk-space-mb`.
@@ -486,6 +492,7 @@ pub(crate) enum ReasoningMode {
#[serde(rename = "none")]
Direct,
#[default]
Low,
High,
Max,
}
@@ -494,12 +501,49 @@ 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::Low => "Think Low",
Self::High => "Think High",
Self::Max => "Think Max",
})
}
}
impl ReasoningMode {
pub(crate) fn as_id(self) -> &'static str {
match self {
Self::Direct => "none",
Self::Low => "low",
Self::High => "high",
Self::Max => "max",
}
}
}
impl ModelChoice {
pub(crate) fn reasoning_modes(self) -> &'static [ReasoningMode] {
match self {
Self::DeepSeekV4Flash0731 | Self::DeepSeekV4Pro => &DEEPSEEK_REASONING_MODES,
Self::Glm52 => &GLM_52_REASONING_MODES,
Self::Glm53Flash => &GLM_53_REASONING_MODES,
}
}
pub(crate) fn default_reasoning_mode(self) -> ReasoningMode {
match self {
Self::DeepSeekV4Flash0731 | Self::DeepSeekV4Pro => ReasoningMode::Low,
Self::Glm52 | Self::Glm53Flash => ReasoningMode::Max,
}
}
pub(crate) fn validate_reasoning_mode(self, mode: ReasoningMode) -> Result<(), String> {
if self.reasoning_modes().contains(&mode) {
Ok(())
} else {
Err(format!("{mode} is not supported by {self}."))
}
}
}
#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
#[serde(default, deny_unknown_fields)]
pub(crate) struct GenerationPreferences {
@@ -531,29 +575,12 @@ impl Default for GenerationPreferences {
top_p: None,
min_p: None,
seed: None,
reasoning_mode: ReasoningMode::High,
reasoning_mode: ReasoningMode::Low,
}
}
}
impl GenerationPreferences {
pub(crate) fn effective_reasoning_mode(&self) -> ReasoningMode {
if self.reasoning_mode == ReasoningMode::Max && self.context_tokens < THINK_MAX_MIN_CONTEXT
{
ReasoningMode::High
} else {
self.reasoning_mode
}
}
pub(crate) fn supported_reasoning_modes(&self) -> &'static [ReasoningMode] {
if self.context_tokens >= THINK_MAX_MIN_CONTEXT {
&REASONING_MODES
} else {
&STANDARD_REASONING_MODES
}
}
pub(crate) fn validate(&self) -> Result<(), String> {
if self.context_tokens <= 0 {
return Err("Context tokens must be a positive whole number.".into());
@@ -587,7 +614,7 @@ impl GenerationPreferences {
top_k: 0,
stops: Vec::new(),
seed: self.seed,
reasoning_mode: self.effective_reasoning_mode(),
reasoning_mode: self.reasoning_mode,
}
}
}
@@ -648,6 +675,7 @@ pub(crate) fn effective_settings(
models_path: &Path,
) -> Result<EffectiveSettings, String> {
generation.validate()?;
model.validate_reasoning_mode(generation.reasoning_mode)?;
Ok(EffectiveSettings {
engine: runtime.engine_settings(model, generation.context_tokens, models_path)?,
turn: generation.turn_settings(model, runtime.kv_cache.settings()),
@@ -673,11 +701,7 @@ mod tests {
let glm = defaults.turn_settings(ModelChoice::Glm52, cache);
assert_eq!((glm.temperature, glm.top_p, glm.min_p), (1.0, 0.95, 0.0));
assert_eq!(
defaults.supported_reasoning_modes(),
&[ReasoningMode::High, ReasoningMode::Direct]
);
assert_eq!(defaults.effective_reasoning_mode(), ReasoningMode::High);
assert_eq!(defaults.reasoning_mode, ReasoningMode::Low);
let explicit = GenerationPreferences {
top_p: Some(0.4),
@@ -687,39 +711,33 @@ mod tests {
};
let effective = explicit.turn_settings(ModelChoice::Glm52, cache);
assert_eq!((effective.top_p, effective.min_p), (0.4, 0.2));
assert_eq!(effective.reasoning_mode, ReasoningMode::High);
let long_context = GenerationPreferences {
context_tokens: THINK_MAX_MIN_CONTEXT,
reasoning_mode: ReasoningMode::Max,
..GenerationPreferences::default()
};
assert_eq!(long_context.supported_reasoning_modes(), &REASONING_MODES);
assert_eq!(long_context.effective_reasoning_mode(), ReasoningMode::Max);
assert_eq!(effective.reasoning_mode, ReasoningMode::Max);
}
#[test]
fn reasoning_selection_reaches_every_model_turn() {
fn reasoning_selection_is_exact_and_model_specific() {
let cache = KvCachePreferences::default().settings();
for model in crate::model::MODEL_CHOICES {
for mode in [ReasoningMode::Direct, ReasoningMode::High] {
for &mode in model.reasoning_modes() {
let generation = GenerationPreferences {
reasoning_mode: mode,
..GenerationPreferences::default()
};
assert_eq!(generation.turn_settings(model, cache).reasoning_mode, mode);
assert!(model.validate_reasoning_mode(mode).is_ok());
}
let generation = GenerationPreferences {
context_tokens: THINK_MAX_MIN_CONTEXT,
reasoning_mode: ReasoningMode::Max,
..GenerationPreferences::default()
};
assert_eq!(
generation.turn_settings(model, cache).reasoning_mode,
ReasoningMode::Max
);
}
assert!(
ModelChoice::Glm52
.validate_reasoning_mode(ReasoningMode::Low)
.is_err()
);
assert!(
ModelChoice::Glm53Flash
.validate_reasoning_mode(ReasoningMode::Direct)
.is_err()
);
}
#[test]