use std::sync::Arc;
use super::qwen_thinking_policy::{
effective_qwen_thinking_budget, resolve_qwen_thinking_policy, QwenThinkingDefaults,
QwenToolChainState, RequestedThinkingBudget, ThinkingPolicyError,
};
use super::registry;
use super::schema::{ChatCompletionRequest, ToolChoiceValue};
pub(super) const DROPPED_FIFO_SERIAL: &str = "fifo-serial scheduler";
pub(super) const DROPPED_NO_ROOM: &str = "no room within max_tokens";
const DROPPED_NO_CHANNEL: &str = "no thinking channel on this family";
const DROPPED_NO_OPEN_SPAN: &str = "no open reasoning span";
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub(super) enum EffortLevel {
Off,
Low,
Medium,
High,
Max,
}
impl EffortLevel {
pub(super) fn parse(raw: &str) -> Option<Self> {
match raw.trim().to_ascii_lowercase().as_str() {
"none" | "minimal" => Some(Self::Off),
"low" => Some(Self::Low),
"medium" => Some(Self::Medium),
"high" => Some(Self::High),
"xhigh" | "max" => Some(Self::Max),
_ => None,
}
}
pub(super) const fn budget(self) -> Option<usize> {
match self {
Self::Off | Self::Max => None,
Self::Low => Some(512),
Self::Medium => Some(2_048),
Self::High => Some(8_192),
}
}
pub(super) const fn deepseek_tier(self) -> Option<&'static str> {
match self {
Self::Off => None,
Self::Low => Some("low"),
Self::Medium | Self::High => Some("high"),
Self::Max => Some("max"),
}
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub(super) struct ReasoningControls {
pub(super) effort: Option<EffortLevel>,
effort_param: Option<&'static str>,
pub(super) enabled: Option<bool>,
pub(super) budget: Option<usize>,
pub(super) budget_param: &'static str,
enabled_is_d5_alias: bool,
}
impl Default for ReasoningControls {
fn default() -> Self {
Self {
effort: None,
effort_param: None,
enabled: None,
budget: None,
budget_param: "thinking_token_budget",
enabled_is_d5_alias: false,
}
}
}
impl ReasoningControls {
pub(super) fn d5_control_present(&self) -> bool {
self.effort.is_some() || self.budget.is_some() || self.enabled_is_d5_alias
}
pub(super) fn first_d5_param(&self) -> &'static str {
self.effort_param
.or(if self.enabled_is_d5_alias {
Some("reasoning.enabled")
} else {
None
})
.or_else(|| self.budget.map(|_| self.budget_param))
.unwrap_or("reasoning_effort")
}
pub(super) fn thinking_enabled_override(&self) -> Option<bool> {
match self.effort {
Some(EffortLevel::Off) => Some(false),
Some(_) => Some(true),
None => self.enabled,
}
}
pub(super) fn requested_thinking_off(&self) -> bool {
self.effort == Some(EffortLevel::Off)
&& self.enabled != Some(true)
&& self.budget.is_none()
}
pub(super) fn deepseek_reasoning_effort_tier(&self) -> Option<&'static str> {
self.effort.and_then(EffortLevel::deepseek_tier)
}
}
pub(super) fn reasoning_controls_from_request(
req: &ChatCompletionRequest,
) -> Result<ReasoningControls, ThinkingPolicyError> {
let mut controls = ReasoningControls {
enabled: req.hf2q_enable_thinking,
budget: req.thinking_token_budget,
..ReasoningControls::default()
};
if let Some(raw) = req.reasoning_effort.as_deref() {
controls.effort = Some(parse_effort(raw, "reasoning_effort")?);
controls.effort_param = Some("reasoning_effort");
}
if let Some(raw) = req
.reasoning
.as_ref()
.and_then(|aliases| aliases.effort.as_deref())
{
let level = parse_effort(raw, "reasoning.effort")?;
match controls.effort {
Some(existing) if existing != level => {
return Err(ThinkingPolicyError {
message: "reasoning_effort and reasoning.effort disagree".into(),
param: "reasoning.effort",
});
}
None => {
controls.effort = Some(level);
controls.effort_param = Some("reasoning.effort");
}
_ => {}
}
}
if let Some(value) = req.reasoning.as_ref().and_then(|aliases| aliases.enabled) {
match controls.enabled {
Some(existing) if existing != value => {
return Err(ThinkingPolicyError {
message: "hf2q_enable_thinking and reasoning.enabled disagree".into(),
param: "reasoning.enabled",
});
}
None => {
controls.enabled = Some(value);
controls.enabled_is_d5_alias = true;
}
_ => {}
}
}
if let Some(value) = req
.reasoning
.as_ref()
.and_then(|aliases| aliases.max_tokens)
{
match controls.budget {
Some(existing) if existing != value => {
return Err(ThinkingPolicyError {
message: "thinking_token_budget and reasoning.max_tokens disagree".into(),
param: "reasoning.max_tokens",
});
}
None => {
controls.budget = Some(value);
controls.budget_param = "reasoning.max_tokens";
}
_ => {}
}
}
if controls.enabled == Some(false) {
if let Some(param) = controls
.effort_param
.filter(|_| controls.effort != Some(EffortLevel::Off))
{
return Err(ThinkingPolicyError {
message: format!("{param} requires thinking, but thinking was explicitly disabled"),
param,
});
}
if controls.budget.is_some() {
return Err(ThinkingPolicyError {
message: format!(
"{} requires thinking, but thinking was explicitly disabled",
controls.budget_param
),
param: controls.budget_param,
});
}
}
if controls.effort == Some(EffortLevel::Off) && controls.enabled == Some(true) {
return Err(ThinkingPolicyError {
message:
"reasoning effort `none`/`minimal` turns thinking off; reasoning.enabled=true contradicts it"
.into(),
param: "reasoning.enabled",
});
}
if controls.effort == Some(EffortLevel::Off) && controls.budget.is_some() {
return Err(ThinkingPolicyError {
message: format!(
"{} cannot be combined with reasoning effort `none`/`minimal`, which turns thinking off",
controls.budget_param
),
param: controls.budget_param,
});
}
Ok(controls)
}
fn parse_effort(raw: &str, param: &'static str) -> Result<EffortLevel, ThinkingPolicyError> {
EffortLevel::parse(raw).ok_or_else(|| ThinkingPolicyError {
message: format!(
"{param} must be one of none, minimal, low, medium, high, xhigh, or max; got {raw:?}"
),
param,
})
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub(super) enum ReasoningChannel {
QwenBudget,
DeepSeekTier,
None,
}
pub(super) fn reasoning_channel_for(
registration: Option<®istry::ModelRegistration>,
template_supports_thinking: bool,
) -> ReasoningChannel {
match registration.map(|registration| registration.family) {
Some("qwen35") if template_supports_thinking => ReasoningChannel::QwenBudget,
Some("deepseek4") => ReasoningChannel::DeepSeekTier,
_ => ReasoningChannel::None,
}
}
#[derive(Clone, Debug, Default, Eq, PartialEq)]
pub(super) struct ThinkingBudgetResolution {
pub(super) thinking_budget: Option<usize>,
pub(super) reasoning_end_tokens: Option<Arc<Vec<u32>>>,
pub(super) reasoning_close_tokens: Option<Arc<Vec<u32>>>,
pub(super) qwen_required_tool_mode: bool,
pub(super) deepseek_required_tool_mode: bool,
pub(super) dropped_budget: Option<(usize, &'static str)>,
}
#[allow(clippy::too_many_arguments)]
pub(super) fn resolve_thinking_budget_policy<F>(
registration: Option<®istry::ModelRegistration>,
channel: ReasoningChannel,
controls: &ReasoningControls,
reasoning_forced_open: bool,
tool_choice: &ToolChoiceValue,
max_tokens: usize,
chain: QwenToolChainState,
server_base_budget: Option<u32>,
server_tool_budget: Option<u32>,
deepseek_required_tool_eligible: bool,
slot_aware: bool,
encode: F,
) -> Result<ThinkingBudgetResolution, ThinkingPolicyError>
where
F: FnMut(&str) -> Result<Arc<Vec<u32>>, String>,
{
if controls.budget == Some(0) {
return Err(ThinkingPolicyError {
message: format!("{} must be greater than zero", controls.budget_param),
param: controls.budget_param,
});
}
match channel {
ReasoningChannel::QwenBudget => resolve_qwen_budget_channel(
registration,
controls,
reasoning_forced_open,
tool_choice,
max_tokens,
chain,
server_base_budget,
server_tool_budget,
slot_aware,
encode,
),
ReasoningChannel::DeepSeekTier => resolve_deepseek_budget_channel(
registration,
controls,
reasoning_forced_open,
tool_choice,
max_tokens,
server_tool_budget,
deepseek_required_tool_eligible,
slot_aware,
encode,
),
ReasoningChannel::None => Ok(resolve_no_thinking_channel(
server_base_budget,
server_tool_budget,
)),
}
}
#[allow(clippy::too_many_arguments)]
fn resolve_qwen_budget_channel<F>(
registration: Option<®istry::ModelRegistration>,
controls: &ReasoningControls,
reasoning_forced_open: bool,
tool_choice: &ToolChoiceValue,
max_tokens: usize,
chain: QwenToolChainState,
server_base_budget: Option<u32>,
server_tool_budget: Option<u32>,
slot_aware: bool,
encode: F,
) -> Result<ThinkingBudgetResolution, ThinkingPolicyError>
where
F: FnMut(&str) -> Result<Arc<Vec<u32>>, String>,
{
if !reasoning_forced_open {
if controls.budget.is_some() {
return Err(ThinkingPolicyError {
message: format!(
"{} requires an open reasoning span; enable thinking or use a thinking-mode template",
controls.budget_param
),
param: controls.budget_param,
});
}
return Ok(ThinkingBudgetResolution {
qwen_required_tool_mode: matches!(
tool_choice,
ToolChoiceValue::Required | ToolChoiceValue::Function(_)
),
dropped_budget: controls
.effort
.and_then(EffortLevel::budget)
.map(|budget| (budget, DROPPED_NO_OPEN_SPAN)),
..ThinkingBudgetResolution::default()
});
}
let requested = controls
.budget
.map(|tokens| RequestedThinkingBudget {
tokens,
strict: true,
})
.or_else(|| {
controls
.effort
.and_then(EffortLevel::budget)
.map(|tokens| RequestedThinkingBudget {
tokens,
strict: false,
})
});
let defaults = QwenThinkingDefaults::from_config(server_base_budget, server_tool_budget);
let resolved = resolve_qwen_thinking_policy(
registration,
reasoning_forced_open,
tool_choice,
requested,
controls.budget_param,
max_tokens,
chain,
defaults,
slot_aware,
encode,
)?;
Ok(ThinkingBudgetResolution {
thinking_budget: resolved.effective_budget,
reasoning_end_tokens: resolved.end_tokens,
reasoning_close_tokens: resolved.close_tokens,
qwen_required_tool_mode: resolved.required_tool_mode,
dropped_budget: resolved.dropped_budget,
..ThinkingBudgetResolution::default()
})
}
#[allow(clippy::too_many_arguments)]
fn resolve_deepseek_budget_channel<F>(
registration: Option<®istry::ModelRegistration>,
controls: &ReasoningControls,
reasoning_forced_open: bool,
tool_choice: &ToolChoiceValue,
max_tokens: usize,
server_tool_budget: Option<u32>,
deepseek_required_tool_eligible: bool,
slot_aware: bool,
mut encode: F,
) -> Result<ThinkingBudgetResolution, ThinkingPolicyError>
where
F: FnMut(&str) -> Result<Arc<Vec<u32>>, String>,
{
let client_budget = controls.budget;
if client_budget.is_some() && !reasoning_forced_open {
return Err(ThinkingPolicyError {
message: format!(
"{} requires an open reasoning span; enable thinking first",
controls.budget_param
),
param: controls.budget_param,
});
}
let constrained_tool_choice = matches!(
tool_choice,
ToolChoiceValue::Required | ToolChoiceValue::Function(_)
);
let deepseek_required_tool_mode =
deepseek_required_tool_eligible || (client_budget.is_some() && constrained_tool_choice);
let server_budget = if deepseek_required_tool_eligible && client_budget.is_none() {
enabled_default_thinking_budget(server_tool_budget)
} else {
None
};
let Some(configured_budget) = client_budget.or(server_budget) else {
return Ok(ThinkingBudgetResolution {
deepseek_required_tool_mode,
..ThinkingBudgetResolution::default()
});
};
if !slot_aware {
if client_budget.is_some() {
return Err(ThinkingPolicyError {
message: format!(
"{} requires an inflight-batched scheduler",
controls.budget_param
),
param: controls.budget_param,
});
}
return Ok(ThinkingBudgetResolution {
deepseek_required_tool_mode,
dropped_budget: Some((configured_budget, DROPPED_FIFO_SERIAL)),
..ThinkingBudgetResolution::default()
});
}
let close = registration
.and_then(|registration| registration.reasoning_close)
.ok_or_else(|| ThinkingPolicyError {
message: format!(
"{} requires a registered reasoning close marker",
controls.budget_param
),
param: controls.budget_param,
})?;
let close_tokens = encode(close).map_err(|error| ThinkingPolicyError {
message: format!("failed to tokenize reasoning boundary: {error}"),
param: controls.budget_param,
})?;
if close_tokens.is_empty() {
return Err(ThinkingPolicyError {
message: "reasoning boundary tokenized to an empty sequence".into(),
param: controls.budget_param,
});
}
let effective_budget = effective_qwen_thinking_budget(
Some(configured_budget),
client_budget.is_some(),
max_tokens,
close_tokens.len(),
)
.map_err(|message| ThinkingPolicyError {
message,
param: controls.budget_param,
})?;
Ok(ThinkingBudgetResolution {
thinking_budget: effective_budget,
reasoning_end_tokens: effective_budget.map(|_| Arc::clone(&close_tokens)),
reasoning_close_tokens: effective_budget.map(|_| close_tokens),
qwen_required_tool_mode: false,
deepseek_required_tool_mode,
dropped_budget: (effective_budget.is_none() && client_budget.is_none())
.then_some((configured_budget, DROPPED_NO_ROOM)),
})
}
fn resolve_no_thinking_channel(
server_base_budget: Option<u32>,
server_tool_budget: Option<u32>,
) -> ThinkingBudgetResolution {
ThinkingBudgetResolution {
dropped_budget: server_base_budget
.filter(|&budget| budget > 0)
.or_else(|| server_tool_budget.filter(|&budget| budget > 0))
.map(|budget| (budget as usize, DROPPED_NO_CHANNEL)),
..ThinkingBudgetResolution::default()
}
}
pub(super) fn enabled_default_thinking_budget(value: Option<u32>) -> Option<usize> {
value.and_then(|value| (value > 0).then_some(value as usize))
}