vtcode-core 0.98.2

Core library for VT Code - a Rust-based terminal coding agent
use crate::config::constants::{env_vars, models};
use crate::config::core::AnthropicConfig;
use crate::config::models::Provider;
use crate::config::types::ReasoningEffortLevel;
use crate::llm::provider::LLMRequest;
use crate::llm::providers::anthropic_types::ThinkingConfig;
use crate::llm::rig_adapter::RigProviderCapabilities;
use serde_json::{Value, json};
use std::env;

use super::super::capabilities::{resolve_model_name, supports_reasoning_effort};

pub(crate) fn build_thinking_config(
    request: &LLMRequest,
    anthropic_config: &AnthropicConfig,
    default_model: &str,
) -> (Option<ThinkingConfig>, Option<Value>) {
    let resolved_model = resolve_model_name(&request.model, default_model);
    let thinking_enabled = anthropic_config.extended_thinking_enabled
        && supports_reasoning_effort(resolved_model, default_model);

    if thinking_enabled {
        if resolved_model == models::anthropic::CLAUDE_OPUS_4_6 {
            return (Some(ThinkingConfig::Adaptive), None);
        }

        let max_thinking_tokens: Option<u32> = env::var(env_vars::MAX_THINKING_TOKENS)
            .ok()
            .and_then(|v| v.parse().ok());

        let budget = if let Some(explicit_budget) = request.thinking_budget {
            explicit_budget
        } else if let Some(env_budget) = max_thinking_tokens {
            env_budget
        } else if let Some(effort) = request.reasoning_effort {
            match effort {
                ReasoningEffortLevel::None => 0,
                ReasoningEffortLevel::Minimal => 1024,
                ReasoningEffortLevel::Low => 4096,
                ReasoningEffortLevel::Medium => 8192,
                ReasoningEffortLevel::High => 16384,
                ReasoningEffortLevel::XHigh => 32768,
            }
        } else {
            anthropic_config.interleaved_thinking_budget_tokens
        };

        if budget >= 1024 {
            let max_tokens = request.max_tokens.unwrap_or(16000);
            let effective_budget = budget.min(max_tokens.saturating_sub(100)).max(1024);
            return (
                Some(ThinkingConfig::Enabled {
                    budget_tokens: effective_budget,
                }),
                None,
            );
        }
    } else if let Some(effort) = request.reasoning_effort {
        if let Some(payload) = RigProviderCapabilities::new(Provider::Anthropic, &request.model)
            .reasoning_parameters(effort)
        {
            return (None, Some(payload));
        } else {
            return (None, Some(json!({ "effort": effort.as_str() })));
        }
    }

    (None, None)
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::config::constants::models;

    #[test]
    fn uses_adaptive_thinking_for_opus_4_6_by_default() {
        let request = LLMRequest {
            model: models::anthropic::CLAUDE_OPUS_4_6.to_string(),
            ..Default::default()
        };
        let config = AnthropicConfig::default();
        let (thinking, reasoning) =
            build_thinking_config(&request, &config, models::anthropic::DEFAULT_MODEL);

        assert!(matches!(thinking, Some(ThinkingConfig::Adaptive)));
        assert!(reasoning.is_none());
    }

    #[test]
    fn ignores_explicit_budget_for_opus_4_6() {
        let request = LLMRequest {
            model: models::anthropic::CLAUDE_OPUS_4_6.to_string(),
            thinking_budget: Some(2048),
            ..Default::default()
        };
        let config = AnthropicConfig::default();
        let (thinking, _) =
            build_thinking_config(&request, &config, models::anthropic::DEFAULT_MODEL);

        assert!(matches!(thinking, Some(ThinkingConfig::Adaptive)));
    }
}