magi-code 0.96.1

Repository-aware CLI coding agent for terminal work
Documentation
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,
    },
];

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]> {
    if provider == crate::providers::ANTHROPIC_PROVIDER {
        return Some(&ThinkingLevel::HIGH_MAX);
    }

    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 provider == crate::providers::OPENAI_CODEX_PROVIDER
        && matches!(scope, ThinkingCapabilityScope::BuiltIn)
        && catalog.and_then(|metadata| metadata.supports_reasoning) == Some(false)
    {
        return 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()])
}