talon-cli 0.4.1

Talon CLI: hybrid retrieval over Obsidian vaults and markdown corpora, with grounded answers, MCP server, and agent-native output.
Documentation
use eyre::{Result, WrapErr as _};
use serde_json::{Map, Value};
use std::collections::BTreeMap;
use talon_core::{AskClient, ChatClient, ReasoningEffort, runtime::build_ask_chat_client};

pub(super) fn build_ask_client(config: &talon_core::TalonConfig, fast: bool) -> Result<AskClient> {
    let expansion = &config.chat.expansion;
    let ask = &config.chat.ask;
    let ask_model = ask.resolved_model(expansion);
    let planning_effort = ask_reasoning_effort(ask.planning_reasoning_effort, fast);
    let synthesis_effort = ask_reasoning_effort(ask.synthesis_reasoning_effort, fast);
    let planning_enable_thinking =
        ask_enable_thinking(planning_effort, ask.planning_enable_thinking, fast);
    let synthesis_enable_thinking =
        ask_enable_thinking(synthesis_effort, ask.synthesis_enable_thinking, fast);
    let planning_chat = ask_chat_client(
        config,
        ask_model,
        Some(ask.max_output_tokens),
        planning_enable_thinking,
        planning_effort,
        ask_kwargs(ask.planning_chat_template_kwargs.as_ref(), fast),
    )?;
    let synthesis_chat = ask_chat_client(
        config,
        ask_model,
        Some(ask.max_output_tokens),
        synthesis_enable_thinking,
        synthesis_effort,
        ask_kwargs(ask.synthesis_chat_template_kwargs.as_ref(), fast),
    )?;
    Ok(AskClient::with_stage_clients(planning_chat, synthesis_chat))
}

const fn ask_reasoning_effort(
    configured: Option<ReasoningEffort>,
    fast: bool,
) -> Option<ReasoningEffort> {
    if fast {
        Some(ReasoningEffort::None)
    } else {
        configured
    }
}

fn ask_enable_thinking(
    reasoning_effort: Option<ReasoningEffort>,
    configured: Option<bool>,
    fast: bool,
) -> Option<bool> {
    if fast {
        Some(false)
    } else {
        reasoning_effort
            .map(ReasoningEffort::enables_thinking)
            .or(configured)
    }
}

const fn ask_kwargs(
    configured: Option<&BTreeMap<String, Value>>,
    fast: bool,
) -> Option<&BTreeMap<String, Value>> {
    if fast { None } else { configured }
}

fn ask_chat_client(
    config: &talon_core::TalonConfig,
    ask_model: &str,
    max_tokens: Option<u32>,
    enable_thinking: Option<bool>,
    reasoning_effort: Option<ReasoningEffort>,
    chat_template_kwargs: Option<&BTreeMap<String, Value>>,
) -> Result<ChatClient> {
    let mut chat = build_ask_chat_client(config, ask_model, max_tokens)
        .wrap_err("building ask chat client")?;
    if let Some(reasoning_effort) = reasoning_effort {
        chat = chat.with_reasoning_effort(reasoning_effort);
    }
    if let Some(kwargs) = merged_chat_template_kwargs(enable_thinking, chat_template_kwargs) {
        chat = chat.with_chat_template_kwargs(kwargs);
    }
    Ok(chat)
}

fn merged_chat_template_kwargs(
    enable_thinking: Option<bool>,
    chat_template_kwargs: Option<&BTreeMap<String, Value>>,
) -> Option<Value> {
    let mut merged: Map<String, Value> = chat_template_kwargs
        .into_iter()
        .flat_map(|kwargs| {
            kwargs
                .iter()
                .map(|(key, value)| (key.clone(), value.clone()))
        })
        .collect();
    if let Some(enable_thinking) = enable_thinking {
        merged.insert("enable_thinking".to_string(), Value::Bool(enable_thinking));
    }
    (!merged.is_empty()).then_some(Value::Object(merged))
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn fast_overrides_reasoning_and_thinking() {
        assert_eq!(
            ask_reasoning_effort(Some(ReasoningEffort::High), true),
            Some(ReasoningEffort::None)
        );
        assert_eq!(
            ask_enable_thinking(Some(ReasoningEffort::High), Some(true), true),
            Some(false)
        );
    }

    #[test]
    fn explicit_enable_thinking_wins_in_fast_merge() {
        let mut configured = BTreeMap::new();
        configured.insert("enable_thinking".to_string(), Value::Bool(true));
        let merged = merged_chat_template_kwargs(Some(false), Some(&configured))
            .unwrap_or_else(|| panic!("merged kwargs"));
        assert_eq!(merged["enable_thinking"].as_bool(), Some(false));
    }
}