use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use std::{fmt, str::FromStr};
#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize, JsonSchema, PartialEq, Eq, Hash)]
#[serde(rename_all = "snake_case")]
pub enum ThinkingLevel {
#[default]
Default,
Low,
Medium,
High,
XHigh,
Max,
}
impl ThinkingLevel {
pub(crate) const ALL: [Self; 6] = [
Self::Default,
Self::Low,
Self::Medium,
Self::High,
Self::XHigh,
Self::Max,
];
pub(crate) const EFFORT_GENERIC: [Self; 4] =
[Self::Default, Self::Low, Self::Medium, Self::High];
pub(crate) const EFFORT_FULL: [Self; 5] = [
Self::Default,
Self::Low,
Self::Medium,
Self::High,
Self::XHigh,
];
pub(crate) const HIGH_MAX: [Self; 3] = [Self::Default, Self::High, Self::Max];
pub(crate) fn as_str(self) -> &'static str {
match self {
Self::Default => "default",
Self::Low => "low",
Self::Medium => "medium",
Self::High => "high",
Self::XHigh => "xhigh",
Self::Max => "max",
}
}
pub(crate) fn label(self) -> &'static str {
match self {
Self::Default => "default",
Self::Low => "Low",
Self::Medium => "Medium",
Self::High => "High",
Self::XHigh => "XHigh",
Self::Max => "Max",
}
}
pub(crate) fn explicit_effort(self) -> Option<&'static str> {
match self {
Self::Default => None,
Self::Low => Some("low"),
Self::Medium => Some("medium"),
Self::High => Some("high"),
Self::XHigh => Some("xhigh"),
Self::Max => Some("max"),
}
}
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub(crate) struct CatalogThinkingMetadata {
pub(crate) reasoning_efforts: Option<Vec<ThinkingLevel>>,
pub(crate) supports_reasoning: Option<bool>,
}
enum ModelMatcher {
Exact(&'static str),
Prefix(&'static str),
}
impl ModelMatcher {
fn matches(&self, model: &str) -> bool {
match self {
Self::Exact(expected) => model == *expected,
Self::Prefix(prefix) => model.starts_with(prefix),
}
}
}
struct ThinkingProfileMatch {
provider: &'static str,
model: ModelMatcher,
levels: &'static [ThinkingLevel],
}
const BUILT_IN_THINKING_PROFILES: &[ThinkingProfileMatch] = &[
ThinkingProfileMatch {
provider: crate::providers::OPENAI_CODEX_PROVIDER,
model: ModelMatcher::Exact("gpt-5.5"),
levels: &ThinkingLevel::EFFORT_FULL,
},
ThinkingProfileMatch {
provider: crate::providers::OPENAI_CODEX_PROVIDER,
model: ModelMatcher::Prefix("gpt-5"),
levels: &ThinkingLevel::EFFORT_GENERIC,
},
ThinkingProfileMatch {
provider: crate::providers::OPENAI_CODEX_PROVIDER,
model: ModelMatcher::Prefix("o"),
levels: &ThinkingLevel::EFFORT_GENERIC,
},
ThinkingProfileMatch {
provider: "zai",
model: ModelMatcher::Exact("glm-5.2"),
levels: &ThinkingLevel::HIGH_MAX,
},
ThinkingProfileMatch {
provider: "*",
model: ModelMatcher::Exact("zai/glm-5.2"),
levels: &ThinkingLevel::HIGH_MAX,
},
ThinkingProfileMatch {
provider: crate::providers::ANTHROPIC_PROVIDER,
model: ModelMatcher::Prefix("claude-sonnet-4"),
levels: &ThinkingLevel::HIGH_MAX,
},
ThinkingProfileMatch {
provider: crate::providers::ANTHROPIC_PROVIDER,
model: ModelMatcher::Prefix("claude-opus-4"),
levels: &ThinkingLevel::HIGH_MAX,
},
];
impl fmt::Display for ThinkingLevel {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.as_str())
}
}
impl FromStr for ThinkingLevel {
type Err = String;
fn from_str(value: &str) -> Result<Self, Self::Err> {
match value.trim() {
"default" => Ok(Self::Default),
"low" => Ok(Self::Low),
"medium" => Ok(Self::Medium),
"high" => Ok(Self::High),
"xhigh" => Ok(Self::XHigh),
"max" => Ok(Self::Max),
other => Err(format!(
"invalid thinking_level '{other}'; expected one of: default, low, medium, high, xhigh, max"
)),
}
}
}
pub(crate) fn built_in_thinking_levels(
provider: &str,
model: &str,
) -> Option<&'static [ThinkingLevel]> {
BUILT_IN_THINKING_PROFILES
.iter()
.find(|profile| {
(profile.provider == provider || profile.provider == "*")
&& profile.model.matches(model)
})
.map(|profile| profile.levels)
}
pub(crate) fn default_thinking_levels() -> Vec<ThinkingLevel> {
vec![ThinkingLevel::Default]
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum ThinkingCapabilityScope {
BuiltIn,
Custom(crate::config::CustomReasoningProtocol),
}
pub(crate) fn capability_scope_for_provider(
custom_providers: &std::collections::BTreeMap<String, crate::config::CustomProviderConfig>,
provider: &str,
) -> ThinkingCapabilityScope {
match custom_providers.get(provider) {
Some(custom) => ThinkingCapabilityScope::Custom(custom.reasoning_protocol),
None => ThinkingCapabilityScope::BuiltIn,
}
}
pub(crate) fn available_thinking_levels(
provider: &str,
model: &str,
catalog: Option<&CatalogThinkingMetadata>,
scope: ThinkingCapabilityScope,
) -> Vec<ThinkingLevel> {
let levels = resolve_capability_levels(provider, model, catalog, scope);
if matches!(
scope,
ThinkingCapabilityScope::Custom(crate::config::CustomReasoningProtocol::AnthropicLike)
) {
levels
.into_iter()
.filter(|level| {
matches!(
level,
ThinkingLevel::Default | ThinkingLevel::High | ThinkingLevel::Max
)
})
.collect()
} else {
levels
}
}
fn resolve_capability_levels(
provider: &str,
model: &str,
catalog: Option<&CatalogThinkingMetadata>,
scope: ThinkingCapabilityScope,
) -> Vec<ThinkingLevel> {
if let Some(levels) = catalog
.and_then(|metadata| metadata.reasoning_efforts.as_deref())
.filter(|levels| !levels.is_empty())
{
return normalize_thinking_levels(levels);
}
if matches!(
scope,
ThinkingCapabilityScope::Custom(crate::config::CustomReasoningProtocol::GptLike)
) {
return if catalog.and_then(|metadata| metadata.supports_reasoning) == Some(true) {
ThinkingLevel::EFFORT_GENERIC.to_vec()
} else {
default_thinking_levels()
};
}
if let Some(levels) = built_in_thinking_levels(provider, model) {
return levels.to_vec();
}
if catalog.and_then(|metadata| metadata.supports_reasoning) == Some(true) {
return ThinkingLevel::EFFORT_GENERIC.to_vec();
}
default_thinking_levels()
}
pub(crate) fn normalize_thinking_levels(levels: &[ThinkingLevel]) -> Vec<ThinkingLevel> {
let mut normalized = Vec::new();
if !levels.is_empty() {
normalized.push(ThinkingLevel::Default);
}
for candidate in ThinkingLevel::ALL {
if candidate != ThinkingLevel::Default
&& levels.contains(&candidate)
&& !normalized.contains(&candidate)
{
normalized.push(candidate);
}
}
if normalized.is_empty() {
default_thinking_levels()
} else {
normalized
}
}
pub(crate) fn resolve_thinking_level(
available: &[ThinkingLevel],
selected: ThinkingLevel,
) -> ThinkingLevel {
if available.contains(&selected) {
selected
} else {
ThinkingLevel::Default
}
}
pub(crate) fn next_thinking_level(
available: &[ThinkingLevel],
selected: ThinkingLevel,
) -> Option<ThinkingLevel> {
if available.len() <= 1 {
return None;
}
let current = available
.iter()
.position(|level| *level == selected)
.unwrap_or(0);
Some(available[(current + 1) % available.len()])
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn thinking_levels_parse_display_and_cycle_in_stable_order() {
assert_eq!(
"default".parse::<ThinkingLevel>().unwrap(),
ThinkingLevel::Default
);
assert_eq!("low".parse::<ThinkingLevel>().unwrap(), ThinkingLevel::Low);
assert_eq!(
"xhigh".parse::<ThinkingLevel>().unwrap(),
ThinkingLevel::XHigh
);
assert_eq!("max".parse::<ThinkingLevel>().unwrap(), ThinkingLevel::Max);
assert_eq!(ThinkingLevel::Medium.to_string(), "medium");
assert_eq!(ThinkingLevel::XHigh.to_string(), "xhigh");
assert_eq!(ThinkingLevel::Max.to_string(), "max");
assert_eq!(ThinkingLevel::High.label(), "High");
assert_eq!(ThinkingLevel::XHigh.label(), "XHigh");
assert!("invalid".parse::<ThinkingLevel>().is_err());
assert_eq!(
ThinkingLevel::ALL,
[
ThinkingLevel::Default,
ThinkingLevel::Low,
ThinkingLevel::Medium,
ThinkingLevel::High,
ThinkingLevel::XHigh,
ThinkingLevel::Max,
]
);
}
#[test]
fn built_in_thinking_levels_match_model_capabilities_for_builtin_scope() {
assert_eq!(
available_thinking_levels(
crate::providers::OPENAI_CODEX_PROVIDER,
"gpt-5.5",
None,
ThinkingCapabilityScope::BuiltIn,
),
ThinkingLevel::EFFORT_FULL.to_vec()
);
assert_eq!(
available_thinking_levels("zai", "glm-5.2", None, ThinkingCapabilityScope::BuiltIn,),
ThinkingLevel::HIGH_MAX.to_vec()
);
assert_eq!(
available_thinking_levels(
"custom",
"zai/glm-5.2",
None,
ThinkingCapabilityScope::BuiltIn,
),
ThinkingLevel::HIGH_MAX.to_vec()
);
assert_eq!(
available_thinking_levels(
crate::providers::ANTHROPIC_PROVIDER,
"claude-sonnet-4-5-20250929",
None,
ThinkingCapabilityScope::BuiltIn,
),
ThinkingLevel::HIGH_MAX.to_vec()
);
for model in ["gpt-5", "gpt-5-nano", "o3", "o4-mini"] {
assert_eq!(
available_thinking_levels(
crate::providers::OPENAI_CODEX_PROVIDER,
model,
None,
ThinkingCapabilityScope::BuiltIn,
),
ThinkingLevel::EFFORT_GENERIC.to_vec()
);
}
}
#[test]
fn unsupported_models_and_unsupported_selected_levels_fall_back_to_default() {
let available = available_thinking_levels(
crate::providers::OPENAI_CODEX_PROVIDER,
"gpt-4.1",
None,
ThinkingCapabilityScope::BuiltIn,
);
assert_eq!(available, vec![ThinkingLevel::Default]);
assert_eq!(
available_thinking_levels("custom", "gpt-5", None, ThinkingCapabilityScope::BuiltIn,),
vec![ThinkingLevel::Default]
);
assert_eq!(
resolve_thinking_level(&available, ThinkingLevel::High),
ThinkingLevel::Default
);
assert_eq!(
next_thinking_level(&available, ThinkingLevel::Default),
None
);
}
#[test]
fn catalog_precedence_uses_explicit_builtin_boolean_then_default() {
let explicit = CatalogThinkingMetadata {
reasoning_efforts: Some(vec![
ThinkingLevel::Max,
ThinkingLevel::High,
ThinkingLevel::High,
]),
supports_reasoning: Some(true),
};
let available = available_thinking_levels(
crate::providers::OPENAI_CODEX_PROVIDER,
"gpt-5.5",
Some(&explicit),
ThinkingCapabilityScope::BuiltIn,
);
assert_eq!(
available,
vec![
ThinkingLevel::Default,
ThinkingLevel::High,
ThinkingLevel::Max
]
);
let boolean = CatalogThinkingMetadata {
reasoning_efforts: None,
supports_reasoning: Some(true),
};
assert_eq!(
available_thinking_levels(
crate::providers::OPENAI_CODEX_PROVIDER,
"gpt-5.5",
Some(&boolean),
ThinkingCapabilityScope::BuiltIn,
),
ThinkingLevel::EFFORT_FULL.to_vec()
);
assert_eq!(
available_thinking_levels(
"zai",
"glm-5.2",
Some(&boolean),
ThinkingCapabilityScope::BuiltIn,
),
ThinkingLevel::HIGH_MAX.to_vec()
);
assert_eq!(
available_thinking_levels(
"custom",
"unknown",
Some(&boolean),
ThinkingCapabilityScope::BuiltIn,
),
ThinkingLevel::EFFORT_GENERIC.to_vec()
);
assert_eq!(
resolve_thinking_level(&available, ThinkingLevel::Max),
ThinkingLevel::Max
);
assert_eq!(
resolve_thinking_level(&available, ThinkingLevel::XHigh),
ThinkingLevel::Default
);
assert_eq!(
next_thinking_level(&available, ThinkingLevel::Default),
Some(ThinkingLevel::High)
);
assert_eq!(
next_thinking_level(&available, ThinkingLevel::High),
Some(ThinkingLevel::Max)
);
assert_eq!(
next_thinking_level(&available, ThinkingLevel::Max),
Some(ThinkingLevel::Default)
);
}
#[test]
fn custom_gpt_like_provider_requires_reasoning_metadata() {
let generic =
ThinkingCapabilityScope::Custom(crate::config::CustomReasoningProtocol::GptLike);
for model in ["unknown", "gpt-5", "gpt-5.6", "zai/glm-5.2", "extra-model"] {
assert_eq!(
available_thinking_levels("custom-provider", model, None, generic),
default_thinking_levels(),
"{model}"
);
}
}
#[test]
fn custom_gpt_like_provider_boolean_metadata_controls_generic_levels() {
let generic =
ThinkingCapabilityScope::Custom(crate::config::CustomReasoningProtocol::GptLike);
for supports_reasoning in [Some(true), Some(false), None] {
let catalog = CatalogThinkingMetadata {
reasoning_efforts: None,
supports_reasoning,
};
assert_eq!(
available_thinking_levels("custom-provider", "alias", Some(&catalog), generic),
if supports_reasoning == Some(true) {
ThinkingLevel::EFFORT_GENERIC.to_vec()
} else {
default_thinking_levels()
},
"supports_reasoning={supports_reasoning:?}"
);
}
}
#[test]
fn explicit_catalog_efforts_normalize_and_override_custom_fallback() {
let generic =
ThinkingCapabilityScope::Custom(crate::config::CustomReasoningProtocol::GptLike);
let catalog = CatalogThinkingMetadata {
reasoning_efforts: Some(vec![
ThinkingLevel::Max,
ThinkingLevel::High,
ThinkingLevel::High,
]),
supports_reasoning: Some(false),
};
assert_eq!(
available_thinking_levels("custom-provider", "alias", Some(&catalog), generic),
vec![
ThinkingLevel::Default,
ThinkingLevel::High,
ThinkingLevel::Max
]
);
let xhigh = CatalogThinkingMetadata {
reasoning_efforts: Some(vec![ThinkingLevel::XHigh]),
supports_reasoning: None,
};
assert_eq!(
available_thinking_levels("custom-provider", "alias", Some(&xhigh), generic),
vec![ThinkingLevel::Default, ThinkingLevel::XHigh]
);
}
#[test]
fn empty_catalog_efforts_behave_as_absent_metadata() {
let generic =
ThinkingCapabilityScope::Custom(crate::config::CustomReasoningProtocol::GptLike);
let empty = CatalogThinkingMetadata {
reasoning_efforts: Some(Vec::new()),
supports_reasoning: Some(false),
};
assert_eq!(
available_thinking_levels("custom-provider", "alias", Some(&empty), generic),
default_thinking_levels()
);
}
#[test]
fn custom_gpt_like_scope_does_not_infer_capability_from_model_name() {
let generic =
ThinkingCapabilityScope::Custom(crate::config::CustomReasoningProtocol::GptLike);
assert_eq!(
available_thinking_levels("custom-provider", "zai/glm-5.2", None, generic),
default_thinking_levels()
);
let anthropic =
ThinkingCapabilityScope::Custom(crate::config::CustomReasoningProtocol::AnthropicLike);
assert_eq!(
available_thinking_levels("custom-provider", "zai/glm-5.2", None, anthropic),
ThinkingLevel::HIGH_MAX.to_vec()
);
}
#[test]
fn custom_anthropic_like_intersects_with_default_high_max() {
let anthropic =
ThinkingCapabilityScope::Custom(crate::config::CustomReasoningProtocol::AnthropicLike);
let catalog = CatalogThinkingMetadata {
reasoning_efforts: Some(vec![
ThinkingLevel::Low,
ThinkingLevel::High,
ThinkingLevel::Max,
]),
supports_reasoning: None,
};
assert_eq!(
available_thinking_levels("custom-provider", "alias", Some(&catalog), anthropic),
vec![
ThinkingLevel::Default,
ThinkingLevel::High,
ThinkingLevel::Max
]
);
assert_eq!(
resolve_thinking_level(
&available_thinking_levels("custom-provider", "alias", Some(&catalog), anthropic,),
ThinkingLevel::Low,
),
ThinkingLevel::Default
);
}
#[test]
fn persisted_unsupported_level_clamps_non_destructively() {
let generic =
ThinkingCapabilityScope::Custom(crate::config::CustomReasoningProtocol::GptLike);
let available = available_thinking_levels("custom-provider", "alias", None, generic);
assert_eq!(
resolve_thinking_level(&available, ThinkingLevel::XHigh),
ThinkingLevel::Default
);
assert!(!available.contains(&ThinkingLevel::High));
assert_eq!(
resolve_thinking_level(&available, ThinkingLevel::High),
ThinkingLevel::Default
);
let xhigh_catalog = CatalogThinkingMetadata {
reasoning_efforts: Some(vec![ThinkingLevel::XHigh]),
supports_reasoning: None,
};
let xhigh_available =
available_thinking_levels("custom-provider", "alias", Some(&xhigh_catalog), generic);
assert_eq!(
resolve_thinking_level(&xhigh_available, ThinkingLevel::XHigh),
ThinkingLevel::XHigh
);
}
#[test]
fn capability_gate_requires_reasoning_metadata() {
let generic =
ThinkingCapabilityScope::Custom(crate::config::CustomReasoningProtocol::GptLike);
let multi = available_thinking_levels(
"custom-provider",
"alias",
Some(&CatalogThinkingMetadata {
reasoning_efforts: None,
supports_reasoning: Some(true),
}),
generic,
);
assert!(multi.len() > 1);
assert!(multi.contains(&ThinkingLevel::High));
assert_eq!(
resolve_thinking_level(&multi, ThinkingLevel::High),
ThinkingLevel::High
);
let single = ThinkingCapabilityScope::BuiltIn;
let none = available_thinking_levels("custom-provider", "alias", None, single);
assert_eq!(none, vec![ThinkingLevel::Default]);
assert!(none.len() <= 1);
}
}