correct thinking modes for each model
This commit is contained in:
@@ -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(()) => {
|
||||
|
||||
@@ -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");
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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 =
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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")
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
116
src/settings.rs
116
src/settings.rs
@@ -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]
|
||||
|
||||
Reference in New Issue
Block a user