use ferrin_spec::ReasoningEffort;
use ferrin_spec::Warning;
#[must_use]
pub fn is_custom_reasoning(reasoning: ReasoningEffort) -> bool {
reasoning != ReasoningEffort::ProviderDefault
}
pub fn map_reasoning_to_effort<T: Clone + AsRef<str>>(
reasoning: ReasoningEffort,
effort_map: &[(ReasoningEffort, T)],
warnings: &mut Vec<Warning>,
) -> Option<T> {
let mapped = effort_map
.iter()
.find(|(level, _)| *level == reasoning)
.map(|(_, value)| value.clone());
let Some(mapped) = mapped else {
warnings.push(Warning::unsupported_with_details(
"reasoning",
format!(
"reasoning \"{}\" is not supported by this model.",
reasoning.as_str()
),
));
return None;
};
if mapped.as_ref() != reasoning.as_str() {
warnings.push(Warning::compatibility(
"reasoning",
Some(format!(
"reasoning \"{}\" is not directly supported by this model. mapped to effort \"{}\".",
reasoning.as_str(),
mapped.as_ref()
)),
));
}
Some(mapped)
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct BudgetPercentages {
pub minimal: Option<f64>,
pub low: Option<f64>,
pub medium: Option<f64>,
pub high: Option<f64>,
pub xhigh: Option<f64>,
}
impl Default for BudgetPercentages {
fn default() -> Self {
Self {
minimal: Some(0.02),
low: Some(0.1),
medium: Some(0.3),
high: Some(0.6),
xhigh: Some(0.9),
}
}
}
impl BudgetPercentages {
fn for_level(&self, reasoning: ReasoningEffort) -> Option<f64> {
match reasoning {
ReasoningEffort::Minimal => self.minimal,
ReasoningEffort::Low => self.low,
ReasoningEffort::Medium => self.medium,
ReasoningEffort::High => self.high,
ReasoningEffort::XHigh => self.xhigh,
_ => None,
}
}
}
pub const DEFAULT_MIN_REASONING_BUDGET: u32 = 1024;
pub fn map_reasoning_to_budget(
reasoning: ReasoningEffort,
max_output_tokens: u32,
max_budget: u32,
min_budget: u32,
percentages: &BudgetPercentages,
warnings: &mut Vec<Warning>,
) -> Option<u32> {
let Some(pct) = percentages.for_level(reasoning) else {
warnings.push(Warning::unsupported_with_details(
"reasoning",
format!(
"reasoning \"{}\" is not supported by this model.",
reasoning.as_str()
),
));
return None;
};
#[allow(
clippy::cast_possible_truncation,
clippy::cast_sign_loss,
reason = "the product of a u32 and a share in [0, 1] fits in u32"
)]
let scaled = (f64::from(max_output_tokens) * pct).round() as u32;
Some(scaled.max(min_budget).min(max_budget))
}