vtcode-core 0.125.0

Core library for VT Code - a Rust-based terminal coding agent
Documentation
//! Model capability detection for Anthropic Claude models
//!
//! Provides methods to determine what features each Claude model supports:
//! - Reasoning/extended thinking
//! - Vision (image inputs)
//! - Structured outputs
//! - Parallel tool configuration
//! - Context window sizes

use crate::config::constants::{models, reasoning};
use crate::llm::providers::anthropic_types::ThinkingDisplay;

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum ClaudeThinkingMode {
    ManualBudget,
    Adaptive,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) struct ClaudeThinkingProfile {
    pub mode: ClaudeThinkingMode,
    pub supports_manual_budget: bool,
    pub adaptive_only: bool,
    pub default_thinking_enabled: bool,
    pub manual_interleaved_beta: bool,
    pub supports_effort: bool,
    pub supports_task_budget: bool,
    pub default_display: ThinkingDisplay,
    pub default_effort: &'static str,
    pub supports_xhigh_effort: bool,
    pub supports_max_effort: bool,
}

const ANTHROPIC_EFFORTS_UP_TO_HIGH: &[&str] = &[reasoning::LOW, reasoning::MEDIUM, reasoning::HIGH];
const ANTHROPIC_EFFORTS_UP_TO_MAX: &[&str] = &[
    reasoning::LOW,
    reasoning::MEDIUM,
    reasoning::HIGH,
    reasoning::MAX,
];
const ANTHROPIC_EFFORTS_UP_TO_XHIGH_AND_MAX: &[&str] = &[
    reasoning::LOW,
    reasoning::MEDIUM,
    reasoning::HIGH,
    reasoning::XHIGH,
    reasoning::MAX,
];

pub(crate) fn resolve_model_name<'a>(model: &'a str, default_model: &'a str) -> &'a str {
    if model.trim().is_empty() {
        default_model
    } else {
        model
    }
}

pub(crate) fn matches_model(model: &str, candidate: &str) -> bool {
    model == candidate || model.contains(candidate)
}

pub(crate) fn claude_thinking_profile(
    model: &str,
    default_model: &str,
) -> Option<ClaudeThinkingProfile> {
    let requested = resolve_model_name(model, default_model);

    if matches_model(requested, models::anthropic::CLAUDE_FABLE_5) {
        return Some(ClaudeThinkingProfile {
            mode: ClaudeThinkingMode::Adaptive,
            supports_manual_budget: false,
            adaptive_only: true,
            default_thinking_enabled: true,
            manual_interleaved_beta: false,
            supports_effort: true,
            supports_task_budget: true,
            default_display: ThinkingDisplay::Omitted,
            default_effort: reasoning::XHIGH,
            supports_xhigh_effort: true,
            supports_max_effort: true,
        });
    }

    if matches_model(requested, models::anthropic::CLAUDE_OPUS_4_8) {
        return Some(ClaudeThinkingProfile {
            mode: ClaudeThinkingMode::Adaptive,
            supports_manual_budget: false,
            adaptive_only: true,
            default_thinking_enabled: true,
            manual_interleaved_beta: false,
            supports_effort: true,
            supports_task_budget: true,
            default_display: ThinkingDisplay::Omitted,
            default_effort: reasoning::XHIGH,
            supports_xhigh_effort: true,
            supports_max_effort: true,
        });
    }

    if matches_model(requested, models::anthropic::CLAUDE_SONNET_4_6) {
        return Some(ClaudeThinkingProfile {
            mode: ClaudeThinkingMode::Adaptive,
            supports_manual_budget: true,
            adaptive_only: false,
            default_thinking_enabled: false,
            manual_interleaved_beta: true,
            supports_effort: true,
            supports_task_budget: false,
            default_display: ThinkingDisplay::Summarized,
            default_effort: reasoning::HIGH,
            supports_xhigh_effort: false,
            supports_max_effort: true,
        });
    }

    if matches_model(requested, models::anthropic::CLAUDE_HAIKU_4_5) {
        return Some(ClaudeThinkingProfile {
            mode: ClaudeThinkingMode::ManualBudget,
            supports_manual_budget: true,
            adaptive_only: false,
            default_thinking_enabled: false,
            manual_interleaved_beta: true,
            supports_effort: false,
            supports_task_budget: false,
            default_display: ThinkingDisplay::Summarized,
            default_effort: reasoning::HIGH,
            supports_xhigh_effort: false,
            supports_max_effort: false,
        });
    }

    None
}

fn supports_native_1m_context(model: &str) -> bool {
    matches_model(model, models::anthropic::CLAUDE_FABLE_5)
        || matches_model(model, models::anthropic::CLAUDE_SONNET_4_6)
        || matches_model(model, models::anthropic::CLAUDE_OPUS_4_8)
}

pub fn supports_reasoning(model: &str, default_model: &str) -> bool {
    let requested = resolve_model_name(model, default_model);
    if claude_thinking_profile(requested, default_model).is_some() {
        return true;
    }

    models::minimax::SUPPORTED_MODELS.contains(&requested)
}

pub fn supports_reasoning_effort(model: &str, default_model: &str) -> bool {
    let requested = resolve_model_name(model, default_model);

    if claude_thinking_profile(requested, default_model).is_some() {
        return true;
    }

    if models::minimax::SUPPORTED_MODELS.contains(&requested) {
        return true;
    }

    models::anthropic::REASONING_MODELS.contains(&requested)
}

pub fn supports_effort(model: &str, default_model: &str) -> bool {
    claude_thinking_profile(model, default_model).is_some_and(|profile| profile.supports_effort)
}

pub fn supports_task_budget(model: &str, default_model: &str) -> bool {
    claude_thinking_profile(model, default_model)
        .is_some_and(|profile| profile.supports_task_budget)
}

pub(crate) fn supports_manual_thinking_budget(model: &str, default_model: &str) -> bool {
    claude_thinking_profile(model, default_model)
        .is_some_and(|profile| profile.supports_manual_budget)
}

pub(crate) fn supports_manual_interleaved_beta(model: &str, default_model: &str) -> bool {
    claude_thinking_profile(model, default_model)
        .is_some_and(|profile| profile.manual_interleaved_beta)
}

pub(crate) fn supports_assistant_prefill(model: &str, default_model: &str) -> bool {
    let requested = resolve_model_name(model, default_model);

    // Models with thinking profiles are newer and generally do not support prefill,
    // except Haiku 4.5 which explicitly does. For models without a thinking profile
    // (legacy models), prefill is supported.
    match claude_thinking_profile(requested, default_model) {
        Some(profile) => {
            // Haiku 4.5 is the only thinking-profile model that supports prefill.
            // All others (Fable 5, Opus 4.8, Sonnet 4.6) do not.
            !profile.adaptive_only && matches_model(requested, models::anthropic::CLAUDE_HAIKU_4_5)
        }
        None => true,
    }
}

pub(crate) fn supports_mid_conversation_system_messages(model: &str, default_model: &str) -> bool {
    let requested = resolve_model_name(model, default_model);
    matches_model(requested, models::anthropic::CLAUDE_OPUS_4_8)
}

pub(crate) fn adaptive_thinking_is_default(model: &str, default_model: &str) -> bool {
    claude_thinking_profile(model, default_model)
        .is_some_and(|profile| profile.default_thinking_enabled)
}

pub(crate) fn default_effort_for_model(model: &str, default_model: &str) -> Option<&'static str> {
    claude_thinking_profile(model, default_model)
        .filter(|profile| profile.supports_effort)
        .map(|profile| profile.default_effort)
}

pub(crate) fn allowed_efforts_for_model(
    model: &str,
    default_model: &str,
) -> Option<&'static [&'static str]> {
    let profile = claude_thinking_profile(model, default_model)?;
    if !profile.supports_effort {
        return None;
    }

    if profile.supports_xhigh_effort {
        Some(ANTHROPIC_EFFORTS_UP_TO_XHIGH_AND_MAX)
    } else if profile.supports_max_effort {
        Some(ANTHROPIC_EFFORTS_UP_TO_MAX)
    } else {
        Some(ANTHROPIC_EFFORTS_UP_TO_HIGH)
    }
}

pub(crate) fn effort_allowed_for_model(model: &str, default_model: &str, effort: &str) -> bool {
    let normalized = effort.trim().to_ascii_lowercase();
    allowed_efforts_for_model(model, default_model)
        .is_some_and(|allowed| allowed.contains(&normalized.as_str()))
}

pub fn supports_compaction(model: &str) -> bool {
    matches_model(model, models::anthropic::CLAUDE_FABLE_5)
        || matches_model(model, models::anthropic::CLAUDE_OPUS_4_8)
        || matches_model(model, models::anthropic::CLAUDE_SONNET_4_6)
}

pub fn supports_parallel_tool_config(_model: &str) -> bool {
    true
}

pub fn effective_context_size(model: &str) -> usize {
    if supports_native_1m_context(model) {
        1_000_000
    } else {
        200_000
    }
}

pub fn supports_structured_output(model: &str, default_model: &str) -> bool {
    let requested = resolve_model_name(model, default_model);

    // All models with a thinking profile support structured outputs.
    if claude_thinking_profile(requested, default_model).is_some() {
        return true;
    }

    // Legacy models without thinking profiles that support structured outputs.
    matches_model(requested, "claude-sonnet-4-5")
        || matches_model(requested, "claude-opus-4-5")
        || matches_model(requested, "claude-haiku-4-5")
}

pub fn supports_vision(model: &str, default_model: &str) -> bool {
    let requested = resolve_model_name(model, default_model);

    // All models with a thinking profile support vision.
    if claude_thinking_profile(requested, default_model).is_some() {
        return true;
    }

    // Legacy Claude 3 and Claude 4 Sonnet families support vision.
    requested.starts_with("claude-3") || requested.starts_with("claude-4-sonnet")
}

#[expect(dead_code)]
pub fn is_claude_model(model: &str, default_model: &str) -> bool {
    claude_thinking_profile(model, default_model).is_some()
}

pub fn supported_models() -> Vec<String> {
    let mut supported: Vec<String> = models::anthropic::SUPPORTED_MODELS
        .iter()
        .map(|s| s.to_string())
        .collect();

    supported.extend(
        models::minimax::SUPPORTED_MODELS
            .iter()
            .map(|s| s.to_string()),
    );

    supported.sort();
    supported.dedup();
    supported
}