use crate::rerank_config::RerankConfig;
use serde::{Deserialize, Serialize};
use std::path::{Path, PathBuf};
use thiserror::Error;
use tracing::{info, warn};
#[derive(Error, Debug)]
pub enum ConfigError {
#[error("Config file not found: {0}")]
NotFound(String),
#[error("Failed to read config: {0}")]
ReadError(String),
#[error("Failed to parse config: {0}")]
ParseError(String),
#[error("Invalid configuration: {0}")]
ValidationError(String),
#[error("Model not found: {0}")]
ModelNotFound(String),
}
/// Main configuration for CodeGraph
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct CodeGraphConfig {
/// Embedding provider configuration
#[serde(default)]
pub embedding: EmbeddingConfig,
/// Reranking provider configuration
#[serde(default)]
pub rerank: RerankConfig,
/// LLM configuration for insights
#[serde(default)]
pub llm: LLMConfig,
/// Performance and resource settings
#[serde(default)]
pub performance: PerformanceConfig,
/// Logging configuration
#[serde(default)]
pub logging: LoggingConfig,
/// Daemon configuration for automatic file watching
#[serde(default)]
pub daemon: DaemonConfig,
}
/// Embedding provider configuration
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct EmbeddingConfig {
/// Provider: "onnx", "ollama", "openai", "lmstudio", "jina", or "auto"
#[serde(default = "default_embedding_provider")]
pub provider: String,
/// Model path or identifier
/// For ONNX: path to model directory
/// For Ollama: model name (e.g., "all-minilm:latest")
/// For LM Studio: model name (e.g., "jinaai/jina-embeddings-v3")
/// For OpenAI: model name (e.g., "text-embedding-3-small")
/// For Jina: model name (e.g., "jina-embeddings-v4")
#[serde(default)]
pub model: Option<String>,
/// LM Studio URL (if using LM Studio)
#[serde(default = "default_lmstudio_url")]
pub lmstudio_url: String,
/// Ollama URL (if using Ollama)
#[serde(default = "default_ollama_url")]
pub ollama_url: String,
/// OpenAI API key (if using OpenAI)
#[serde(default)]
pub openai_api_key: Option<String>,
/// Jina API key (if using Jina)
#[serde(default)]
pub jina_api_key: Option<String>,
/// Jina API base URL
#[serde(default = "default_jina_api_base")]
pub jina_api_base: String,
/// Jina late chunking
#[serde(default)]
pub jina_late_chunking: bool,
/// Jina task type
#[serde(default = "default_jina_task")]
pub jina_task: String,
/// Embedding dimension (1024 for jina-embeddings-v4, 1536 for jina-code, 384 for all-MiniLM)
#[serde(default = "default_embedding_dimension")]
pub dimension: usize,
/// Batch size for embedding generation
#[serde(default = "default_batch_size")]
pub batch_size: usize,
}
impl Default for EmbeddingConfig {
fn default() -> Self {
Self {
provider: default_embedding_provider(),
model: None, // Auto-detect
lmstudio_url: default_lmstudio_url(),
ollama_url: default_ollama_url(),
openai_api_key: None,
jina_api_key: None,
jina_api_base: default_jina_api_base(),
jina_late_chunking: false,
jina_task: default_jina_task(),
dimension: default_embedding_dimension(),
batch_size: default_batch_size(),
}
}
}
/// LLM configuration for insights generation
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct LLMConfig {
/// Enable LLM insights (false = context-only mode for agents)
#[serde(default)]
pub enabled: bool,
/// LLM provider: "ollama", "lmstudio", "anthropic", "openai", "xai", "openai-compatible"
#[serde(default = "default_llm_provider")]
pub provider: String,
/// Model identifier
/// For LM Studio: model name (e.g., "lmstudio-community/DeepSeek-Coder-V2-Lite-Instruct-GGUF")
/// For Ollama: model name (e.g., "qwen2.5-coder:14b")
/// For Anthropic: model name (e.g., "claude-3-5-sonnet-20241022")
/// For OpenAI: model name (e.g., "gpt-4o")
/// For xAI: model name (e.g., "grok-4-fast", "grok-4-turbo")
/// For OpenAI-compatible: custom model name
#[serde(default)]
pub model: Option<String>,
/// LM Studio URL
#[serde(default = "default_lmstudio_url")]
pub lmstudio_url: String,
/// Ollama URL
#[serde(default = "default_ollama_url")]
pub ollama_url: String,
/// OpenAI-compatible base URL (for custom endpoints)
#[serde(default)]
pub openai_compatible_url: Option<String>,
/// Anthropic API key
#[serde(default)]
pub anthropic_api_key: Option<String>,
/// OpenAI API key
#[serde(default)]
pub openai_api_key: Option<String>,
/// xAI API key
#[serde(default)]
pub xai_api_key: Option<String>,
/// xAI base URL (default: https://api.x.ai/v1)
#[serde(default = "default_xai_base_url")]
pub xai_base_url: String,
/// Context window size
#[serde(default = "default_context_window")]
pub context_window: usize,
/// Temperature for generation
#[serde(default = "default_temperature")]
pub temperature: f32,
/// Insights mode: "context-only", "balanced", or "deep"
#[serde(default = "default_insights_mode")]
pub insights_mode: String,
/// Maximum tokens to generate (legacy parameter)
#[serde(default = "default_max_tokens")]
pub max_tokens: usize,
/// Maximum output tokens (for Responses API and reasoning models)
#[serde(default)]
pub max_completion_token: Option<usize>,
/// MCP code agent maximum output tokens (for agentic workflows)
/// Overrides tier-based defaults if set
#[serde(default)]
pub mcp_code_agent_max_output_tokens: Option<usize>,
/// Reasoning effort for reasoning models: "minimal", "medium", "high"
#[serde(default)]
pub reasoning_effort: Option<String>,
/// Request timeout in seconds
#[serde(default = "default_timeout_secs")]
pub timeout_secs: u64,
/// Use legacy Chat Completions API instead of modern Responses API
/// Set CODEGRAPH_USE_COMPLETIONS_API=true to enable backward compatibility
#[serde(default)]
pub use_completions_api: bool,
/// LATS-specific multi-provider configuration
#[serde(default)]
pub lats: Option<LATSProviderConfig>,
}
impl Default for LLMConfig {
fn default() -> Self {
Self {
enabled: false, // Default to context-only for speed
provider: default_llm_provider(),
model: None,
lmstudio_url: default_lmstudio_url(),
ollama_url: default_ollama_url(),
openai_compatible_url: None,
anthropic_api_key: None,
openai_api_key: None,
xai_api_key: None,
xai_base_url: default_xai_base_url(),
context_window: default_context_window(),
temperature: default_temperature(),
insights_mode: default_insights_mode(),
max_tokens: default_max_tokens(),
max_completion_token: None, // Will use max_tokens if not set
mcp_code_agent_max_output_tokens: None, // Use tier-based defaults if not set
reasoning_effort: None, // Only for reasoning models
timeout_secs: default_timeout_secs(),
use_completions_api: false, // Default to Responses API
lats: None, // No LATS config by default
}
}
}
/// LATS-specific multi-provider configuration
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct LATSProviderConfig {
/// Provider for selection phase (node scoring)
#[serde(default)]
pub selection_provider: Option<String>,
pub selection_model: Option<String>,
/// Provider for expansion phase (generating new thoughts)
#[serde(default)]
pub expansion_provider: Option<String>,
pub expansion_model: Option<String>,
/// Provider for evaluation phase (assessing quality)
#[serde(default)]
pub evaluation_provider: Option<String>,
pub evaluation_model: Option<String>,
/// Provider for backpropagation phase (updating scores)
#[serde(default)]
pub backprop_provider: Option<String>,
pub backprop_model: Option<String>,
/// LATS algorithm parameters
#[serde(default = "default_lats_beam_width")]
pub beam_width: usize,
#[serde(default = "default_lats_max_depth")]
pub max_depth: usize,
#[serde(default = "default_lats_exploration_weight")]
pub exploration_weight: f32,
}
impl Default for LATSProviderConfig {
fn default() -> Self {
Self {
selection_provider: None,
selection_model: None,
expansion_provider: None,
expansion_model: None,
evaluation_provider: None,
evaluation_model: None,
backprop_provider: None,
backprop_model: None,
beam_width: default_lats_beam_width(),
max_depth: default_lats_max_depth(),
exploration_weight: default_lats_exploration_weight(),
}
}
}
fn default_lats_beam_width() -> usize {
3
}
fn default_lats_max_depth() -> usize {
5
}
fn default_lats_exploration_weight() -> f32 {
1.414
} // sqrt(2) for UCT
/// Validate LATS beam width parameter (must be 1-100)
fn validate_lats_beam_width(w: usize) -> Option<usize> {
if (1..=100).contains(&w) {
Some(w)
} else {
tracing::warn!(
value = w,
"Invalid CODEGRAPH_LATS_BEAM_WIDTH (must be 1-100), using default: {}",
default_lats_beam_width()
);
None
}
}
/// Validate LATS max depth parameter (must be 1-50)
fn validate_lats_max_depth(d: usize) -> Option<usize> {
if (1..=50).contains(&d) {
Some(d)
} else {
tracing::warn!(
value = d,
"Invalid CODEGRAPH_LATS_MAX_DEPTH (must be 1-50), using default: {}",
default_lats_max_depth()
);
None
}
}
/// Validate LATS exploration weight parameter (must be 0.0-10.0)
fn validate_lats_exploration_weight(w: f32) -> Option<f32> {
if (0.0..=10.0).contains(&w) {
Some(w)
} else {
tracing::warn!(
value = w,
"Invalid CODEGRAPH_LATS_EXPLORATION_WEIGHT (must be 0.0-10.0), using default: {}",
default_lats_exploration_weight()
);
None
}
}
/// Performance and resource configuration
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PerformanceConfig {
/// Number of worker threads
#[serde(default = "default_num_threads")]
pub num_threads: usize,
/// Cache size in MB
#[serde(default = "default_cache_size_mb")]
pub cache_size_mb: usize,
/// Enable GPU acceleration
#[serde(default)]
pub enable_gpu: bool,
/// Maximum concurrent requests
#[serde(default = "default_max_concurrent")]
pub max_concurrent_requests: usize,
}
impl Default for PerformanceConfig {
fn default() -> Self {
Self {
num_threads: default_num_threads(),
cache_size_mb: default_cache_size_mb(),
enable_gpu: false, // Conservative default
max_concurrent_requests: default_max_concurrent(),
}
}
}
/// Logging configuration
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct LoggingConfig {
/// Log level: "trace", "debug", "info", "warn", "error"
#[serde(default = "default_log_level")]
pub level: String,
/// Log format: "pretty", "json", "compact"
#[serde(default = "default_log_format")]
pub format: String,
}
impl Default for LoggingConfig {
fn default() -> Self {
Self {
level: default_log_level(),
format: default_log_format(),
}
}
}
/// Daemon configuration for automatic file watching and re-indexing
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DaemonConfig {
/// Enable automatic daemon startup with MCP server
#[serde(default)]
pub auto_start_with_mcp: bool,
/// Project path to watch (defaults to current directory)
#[serde(default)]
pub project_path: Option<PathBuf>,
/// Debounce duration for file changes (ms)
#[serde(default = "default_daemon_debounce_ms")]
pub debounce_ms: u64,
/// Batch timeout for collecting changes (ms)
#[serde(default = "default_daemon_batch_timeout_ms")]
pub batch_timeout_ms: u64,
/// Health check interval (seconds)
#[serde(default = "default_daemon_health_check_interval")]
pub health_check_interval_secs: u64,
/// Languages to watch (empty = all detected)
#[serde(default)]
pub languages: Vec<String>,
/// Exclude patterns (gitignore format)
#[serde(default = "default_daemon_exclude_patterns")]
pub exclude_patterns: Vec<String>,
/// Include patterns (gitignore format)
#[serde(default)]
pub include_patterns: Vec<String>,
}
impl Default for DaemonConfig {
fn default() -> Self {
Self {
auto_start_with_mcp: false, // Opt-in by default
project_path: None,
debounce_ms: default_daemon_debounce_ms(),
batch_timeout_ms: default_daemon_batch_timeout_ms(),
health_check_interval_secs: default_daemon_health_check_interval(),
languages: vec![],
exclude_patterns: default_daemon_exclude_patterns(),
include_patterns: vec![],
}
}
}
// Default value functions
fn default_embedding_provider() -> String {
"auto".to_string()
}
fn default_lmstudio_url() -> String {
"http://localhost:1234".to_string()
}
fn default_ollama_url() -> String {
"http://localhost:11434".to_string()
}
fn default_jina_api_base() -> String {
"https://api.jina.ai/v1".to_string()
}
fn default_jina_task() -> String {
"code.query".to_string()
}
fn default_embedding_dimension() -> usize {
2048
} // jina-embeddings-v4
fn default_batch_size() -> usize {
64
}
fn default_llm_provider() -> String {
"lmstudio".to_string()
}
fn default_xai_base_url() -> String {
"https://api.x.ai/v1".to_string()
}
fn default_context_window() -> usize {
32000
} // DeepSeek Coder v2 Lite
fn default_temperature() -> f32 {
0.1
}
fn default_insights_mode() -> String {
"context-only".to_string()
}
fn default_max_tokens() -> usize {
4096
}
fn default_timeout_secs() -> u64 {
120
}
fn default_num_threads() -> usize {
num_cpus::get()
}
fn default_cache_size_mb() -> usize {
512
}
fn default_max_concurrent() -> usize {
4
}
fn default_log_level() -> String {
"warn".to_string()
} // Clean TUI output during indexing
fn default_log_format() -> String {
"pretty".to_string()
}
// Daemon default functions
fn default_daemon_debounce_ms() -> u64 {
30
}
fn default_daemon_batch_timeout_ms() -> u64 {
200
}
fn default_daemon_health_check_interval() -> u64 {
30
}
fn default_daemon_exclude_patterns() -> Vec<String> {
vec![
"**/node_modules/**".to_string(),
"**/target/**".to_string(),
"**/.git/**".to_string(),
"**/build/**".to_string(),
"**/.codegraph/**".to_string(),
"**/dist/**".to_string(),
"**/__pycache__/**".to_string(),
]
}
/// Configuration manager with smart defaults and auto-detection
pub struct ConfigManager {
config: CodeGraphConfig,
config_path: Option<PathBuf>,
}
impl ConfigManager {
/// Load configuration with the following precedence:
/// 1. Environment variables (.env file)
/// 2. Config file (.codegraph.toml)
/// 3. Sensible defaults
pub fn load() -> Result<Self, ConfigError> {
info!("🔧 Loading CodeGraph configuration...");
// Try to load .env file from current directory or home
Self::load_dotenv();
// Try to find and load config file
let (config, config_path) = Self::load_config_file()?;
// Override with environment variables
let config = Self::apply_env_overrides(config);
// Validate configuration
Self::validate_config(&config)?;
info!("✅ Configuration loaded successfully");
if let Some(ref path) = config_path {
info!(" 📄 Config file: {}", path.display());
} else {
info!(" 📄 Config file: NONE (using defaults)");
}
info!(" 🤖 Embedding provider: {}", config.embedding.provider);
info!(" 🔧 Embedding model: {:?}", config.embedding.model);
info!(" 📐 Embedding dimension: {}", config.embedding.dimension);
info!(" 🌐 Ollama URL: {}", config.embedding.ollama_url);
info!(
" 💬 LLM insights: {}",
if config.llm.enabled {
"enabled"
} else {
"disabled (context-only)"
}
);
Ok(Self {
config,
config_path,
})
}
/// Load .env file if it exists
fn load_dotenv() {
// Try current directory first
if Path::new(".env").exists() {
if let Err(e) = dotenv::from_filename(".env") {
warn!("Failed to load .env file: {}", e);
} else {
info!("📋 Loaded .env file from current directory");
}
return;
}
// Try home directory
if let Some(home) = dirs::home_dir() {
let home_env = home.join(".codegraph.env");
if home_env.exists() {
if let Err(e) = dotenv::from_path(&home_env) {
warn!("Failed to load .codegraph.env: {}", e);
} else {
info!("📋 Loaded .codegraph.env from home directory");
}
}
}
}
/// Find and load config file
/// Search order:
/// 1. ./.codegraph.toml (current directory)
/// 2. ~/.codegraph/config.toml (user config)
/// 3. Use defaults
fn load_config_file() -> Result<(CodeGraphConfig, Option<PathBuf>), ConfigError> {
// Try current directory
let local_config = Path::new(".codegraph.toml");
if local_config.exists() {
let config = Self::read_toml_file(local_config)?;
return Ok((config, Some(local_config.to_path_buf())));
}
// Try user config directory
if let Some(home) = dirs::home_dir() {
let user_config = home.join(".codegraph").join("config.toml");
if user_config.exists() {
let config = Self::read_toml_file(&user_config)?;
return Ok((config, Some(user_config)));
}
}
// Use defaults
info!("📋 No config file found, using defaults");
Ok((CodeGraphConfig::default(), None))
}
/// Read TOML config file
fn read_toml_file(path: &Path) -> Result<CodeGraphConfig, ConfigError> {
let content =
std::fs::read_to_string(path).map_err(|e| ConfigError::ReadError(e.to_string()))?;
let config: CodeGraphConfig =
toml::from_str(&content).map_err(|e| ConfigError::ParseError(e.to_string()))?;
Ok(config)
}
/// Apply environment variable overrides
fn apply_env_overrides(mut config: CodeGraphConfig) -> CodeGraphConfig {
// Embedding configuration
if let Ok(provider) = std::env::var("CODEGRAPH_EMBEDDING_PROVIDER") {
config.embedding.provider = provider;
}
if let Ok(model) = std::env::var("CODEGRAPH_EMBEDDING_MODEL") {
config.embedding.model = Some(model);
}
if let Ok(model) = std::env::var("CODEGRAPH_LOCAL_MODEL") {
config.embedding.model = Some(model);
}
if let Ok(url) = std::env::var("CODEGRAPH_OLLAMA_URL") {
config.embedding.ollama_url = url.clone();
config.llm.ollama_url = url;
}
if let Ok(key) = std::env::var("OPENAI_API_KEY") {
config.embedding.openai_api_key = Some(key);
}
if let Ok(dimension) = std::env::var("CODEGRAPH_EMBEDDING_DIMENSION") {
if let Ok(dim) = dimension.parse() {
config.embedding.dimension = dim;
}
}
if let Ok(batch) = std::env::var("CODEGRAPH_EMBEDDING_BATCH_SIZE") {
if let Ok(size) = batch.parse() {
config.embedding.batch_size = size;
}
}
// Jina configuration
if let Ok(key) = std::env::var("JINA_API_KEY") {
config.embedding.jina_api_key = Some(key);
}
if let Ok(base) = std::env::var("JINA_API_BASE") {
config.embedding.jina_api_base = base;
}
if let Ok(enable) = std::env::var("JINA_ENABLE_RERANKING") {
if enable.to_lowercase() == "true" {
config.rerank.provider = crate::RerankProvider::Jina;
}
}
if let Ok(model) = std::env::var("JINA_RERANKING_MODEL") {
if config.rerank.jina.is_none() {
config.rerank.jina = Some(crate::JinaRerankConfig::default());
}
if let Some(ref mut jina) = config.rerank.jina {
jina.model = model;
}
}
if let Ok(top_n) = std::env::var("JINA_RERANKING_TOP_N") {
if let Ok(n) = top_n.parse() {
config.rerank.top_n = n;
}
}
// Generic rerank toggles (CODEGRAPH_*), higher priority than JINA_ENABLE_RERANKING
if let Ok(enable) = std::env::var("CODEGRAPH_ENABLE_RERANKING") {
if enable.to_lowercase() == "true" {
// Default to Jina unless a provider is explicitly set elsewhere
if matches!(config.rerank.provider, crate::RerankProvider::None) {
config.rerank.provider = crate::RerankProvider::Jina;
}
} else {
config.rerank.provider = crate::RerankProvider::None;
}
}
if let Ok(top_n) = std::env::var("CODEGRAPH_RERANKING_CANDIDATES") {
if let Ok(n) = top_n.parse() {
config.rerank.top_n = n;
}
}
if let Ok(chunking) = std::env::var("JINA_LATE_CHUNKING") {
config.embedding.jina_late_chunking = chunking.to_lowercase() == "true";
}
if let Ok(task) = std::env::var("JINA_TASK") {
config.embedding.jina_task = task;
}
// LLM configuration
if let Ok(provider) =
std::env::var("CODEGRAPH_LLM_PROVIDER").or_else(|_| std::env::var("LLM_PROVIDER"))
{
config.llm.provider = provider;
}
if let Ok(model) = std::env::var("CODEGRAPH_MODEL") {
config.llm.model = Some(model);
config.llm.enabled = true; // Enable if model specified
}
if let Ok(context) = std::env::var("CODEGRAPH_CONTEXT_WINDOW") {
if let Ok(size) = context.parse() {
config.llm.context_window = size;
}
}
if let Ok(temp) = std::env::var("CODEGRAPH_TEMPERATURE") {
if let Ok(t) = temp.parse() {
config.llm.temperature = t;
}
}
if let Ok(effort) = std::env::var("CODEGRAPH_REASONING_EFFORT") {
config.llm.reasoning_effort = Some(effort);
}
if let Ok(max_output) = std::env::var("MCP_CODE_AGENT_MAX_OUTPUT_TOKENS") {
if let Ok(tokens) = max_output.parse() {
config.llm.mcp_code_agent_max_output_tokens = Some(tokens);
}
}
// API selection
if let Ok(use_completions) = std::env::var("CODEGRAPH_USE_COMPLETIONS_API") {
config.llm.use_completions_api =
use_completions.to_lowercase() == "true" || use_completions == "1";
}
// LATS configuration
let mut has_lats_config = false;
let mut lats_config = config.llm.lats.take().unwrap_or_default();
if let Ok(provider) = std::env::var("CODEGRAPH_LATS_SELECTION_PROVIDER") {
lats_config.selection_provider = Some(provider);
has_lats_config = true;
}
if let Ok(model) = std::env::var("CODEGRAPH_LATS_SELECTION_MODEL") {
lats_config.selection_model = Some(model);
has_lats_config = true;
}
if let Ok(provider) = std::env::var("CODEGRAPH_LATS_EXPANSION_PROVIDER") {
lats_config.expansion_provider = Some(provider);
has_lats_config = true;
}
if let Ok(model) = std::env::var("CODEGRAPH_LATS_EXPANSION_MODEL") {
lats_config.expansion_model = Some(model);
has_lats_config = true;
}
if let Ok(provider) = std::env::var("CODEGRAPH_LATS_EVALUATION_PROVIDER") {
lats_config.evaluation_provider = Some(provider);
has_lats_config = true;
}
if let Ok(model) = std::env::var("CODEGRAPH_LATS_EVALUATION_MODEL") {
lats_config.evaluation_model = Some(model);
has_lats_config = true;
}
if let Ok(provider) = std::env::var("CODEGRAPH_LATS_BACKPROP_PROVIDER") {
lats_config.backprop_provider = Some(provider);
has_lats_config = true;
}
if let Ok(model) = std::env::var("CODEGRAPH_LATS_BACKPROP_MODEL") {
lats_config.backprop_model = Some(model);
has_lats_config = true;
}
if let Ok(width) = std::env::var("CODEGRAPH_LATS_BEAM_WIDTH") {
if let Ok(w) = width.parse() {
if let Some(validated) = validate_lats_beam_width(w) {
lats_config.beam_width = validated;
has_lats_config = true;
}
}
}
if let Ok(depth) = std::env::var("CODEGRAPH_LATS_MAX_DEPTH") {
if let Ok(d) = depth.parse() {
if let Some(validated) = validate_lats_max_depth(d) {
lats_config.max_depth = validated;
has_lats_config = true;
}
}
}
if let Ok(weight) = std::env::var("CODEGRAPH_LATS_EXPLORATION_WEIGHT") {
if let Ok(w) = weight.parse() {
if let Some(validated) = validate_lats_exploration_weight(w) {
lats_config.exploration_weight = validated;
has_lats_config = true;
}
}
}
if has_lats_config {
tracing::debug!(
selection_provider = ?lats_config.selection_provider,
beam_width = lats_config.beam_width,
max_depth = lats_config.max_depth,
exploration_weight = lats_config.exploration_weight,
"LATS configuration loaded from environment variables"
);
config.llm.lats = Some(lats_config);
}
// Logging
if let Ok(level) = std::env::var("RUST_LOG") {
config.logging.level = level;
}
// Daemon configuration
if let Ok(auto_start) = std::env::var("CODEGRAPH_DAEMON_AUTO_START") {
config.daemon.auto_start_with_mcp =
auto_start.to_lowercase() == "true" || auto_start == "1";
}
if let Ok(path) = std::env::var("CODEGRAPH_DAEMON_WATCH_PATH") {
config.daemon.project_path = Some(PathBuf::from(path));
}
if let Ok(debounce) = std::env::var("CODEGRAPH_DAEMON_DEBOUNCE_MS") {
if let Ok(ms) = debounce.parse() {
config.daemon.debounce_ms = ms;
}
}
if let Ok(batch) = std::env::var("CODEGRAPH_DAEMON_BATCH_TIMEOUT_MS") {
if let Ok(ms) = batch.parse() {
config.daemon.batch_timeout_ms = ms;
}
}
config
}
/// Validate configuration
fn validate_config(config: &CodeGraphConfig) -> Result<(), ConfigError> {
// Validate embedding provider
match config.embedding.provider.as_str() {
"auto" | "onnx" | "ollama" | "openai" | "jina" | "lmstudio" => {}
other => {
return Err(ConfigError::ValidationError(format!(
"Invalid embedding provider: {}. Must be one of: auto, onnx, ollama, openai, jina, lmstudio",
other
)))
}
}
// Validate insights mode
match config.llm.insights_mode.as_str() {
"context-only" | "balanced" | "deep" => {}
other => {
return Err(ConfigError::ValidationError(format!(
"Invalid insights mode: {}. Must be one of: context-only, balanced, deep",
other
)))
}
}
// Warn about deprecated API usage
if config.llm.use_completions_api {
tracing::warn!(
"⚠️ Using legacy Chat Completions API. \
Consider upgrading to Responses API for better performance. \
Set CODEGRAPH_USE_COMPLETIONS_API=false or remove the variable."
);
}
// Validate log level
match config.logging.level.as_str() {
"trace" | "debug" | "info" | "warn" | "error" => {}
other => {
return Err(ConfigError::ValidationError(format!(
"Invalid log level: {}. Must be one of: trace, debug, info, warn, error",
other
)))
}
}
Ok(())
}
/// Get the loaded configuration
pub fn config(&self) -> &CodeGraphConfig {
&self.config
}
/// Get the path to the config file that was loaded, if any
pub fn config_path(&self) -> Option<&Path> {
self.config_path.as_deref()
}
/// Create a default config file
pub fn create_default_config(path: &Path) -> Result<(), ConfigError> {
let config = CodeGraphConfig::default();
let toml_str =
toml::to_string_pretty(&config).map_err(|e| ConfigError::ParseError(e.to_string()))?;
// Create parent directory if needed
if let Some(parent) = path.parent() {
std::fs::create_dir_all(parent).map_err(|e| ConfigError::ReadError(e.to_string()))?;
}
std::fs::write(path, toml_str).map_err(|e| ConfigError::ReadError(e.to_string()))?;
Ok(())
}
/// Auto-detect available embedding models
pub fn auto_detect_embedding_model() -> Option<String> {
// Check for Ollama models
if Self::check_ollama_available() {
return Some("ollama:all-minilm".to_string());
}
// Check for ONNX models in HuggingFace cache
if let Some(model_path) = Self::find_onnx_model_in_cache() {
return Some(model_path);
}
None
}
/// Check if Ollama is available
fn check_ollama_available() -> bool {
// Try to connect to Ollama
std::process::Command::new("curl")
.args(["-s", "http://localhost:11434/api/tags"])
.output()
.map(|output| output.status.success())
.unwrap_or(false)
}
/// Find ONNX models in HuggingFace cache
fn find_onnx_model_in_cache() -> Option<String> {
let home = dirs::home_dir()?;
let hf_cache = home.join(".cache/huggingface/hub");
if !hf_cache.exists() {
return None;
}
// Look for all-MiniLM-L6-v2-onnx model
let pattern = "models--Qdrant--all-MiniLM-L6-v2-onnx";
if let Ok(entries) = std::fs::read_dir(&hf_cache) {
for entry in entries.flatten() {
let path = entry.path();
if path.file_name()?.to_str()?.contains(pattern) {
// Find snapshots directory
let snapshots = path.join("snapshots");
if let Ok(snapshot_entries) = std::fs::read_dir(&snapshots) {
if let Some(snapshot) = snapshot_entries.flatten().next() {
return Some(snapshot.path().to_string_lossy().to_string());
}
}
}
}
}
None
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_default_config() {
let config = CodeGraphConfig::default();
assert_eq!(config.embedding.provider, "auto");
assert_eq!(config.llm.enabled, false);
assert_eq!(config.llm.insights_mode, "context-only");
}
#[test]
fn test_config_validation() {
let config = CodeGraphConfig::default();
assert!(ConfigManager::validate_config(&config).is_ok());
let mut bad_config = config.clone();
bad_config.embedding.provider = "invalid".to_string();
assert!(ConfigManager::validate_config(&bad_config).is_err());
}
#[test]
fn test_lats_provider_config_default() {
let config = LATSProviderConfig::default();
assert_eq!(config.beam_width, 3);
assert_eq!(config.max_depth, 5);
assert_eq!(config.exploration_weight, 1.414);
assert!(config.selection_provider.is_none());
assert!(config.selection_model.is_none());
assert!(config.expansion_provider.is_none());
assert!(config.expansion_model.is_none());
assert!(config.evaluation_provider.is_none());
assert!(config.evaluation_model.is_none());
assert!(config.backprop_provider.is_none());
assert!(config.backprop_model.is_none());
}
#[test]
fn test_validate_lats_beam_width_valid() {
assert_eq!(validate_lats_beam_width(1), Some(1));
assert_eq!(validate_lats_beam_width(3), Some(3));
assert_eq!(validate_lats_beam_width(50), Some(50));
assert_eq!(validate_lats_beam_width(100), Some(100));
}
#[test]
fn test_validate_lats_beam_width_invalid() {
assert_eq!(validate_lats_beam_width(0), None);
assert_eq!(validate_lats_beam_width(101), None);
assert_eq!(validate_lats_beam_width(1000), None);
}
#[test]
fn test_validate_lats_max_depth_valid() {
assert_eq!(validate_lats_max_depth(1), Some(1));
assert_eq!(validate_lats_max_depth(5), Some(5));
assert_eq!(validate_lats_max_depth(25), Some(25));
assert_eq!(validate_lats_max_depth(50), Some(50));
}
#[test]
fn test_validate_lats_max_depth_invalid() {
assert_eq!(validate_lats_max_depth(0), None);
assert_eq!(validate_lats_max_depth(51), None);
assert_eq!(validate_lats_max_depth(100), None);
}
#[test]
fn test_validate_lats_exploration_weight_valid() {
assert_eq!(validate_lats_exploration_weight(0.0), Some(0.0));
assert_eq!(validate_lats_exploration_weight(1.414), Some(1.414));
assert_eq!(validate_lats_exploration_weight(5.0), Some(5.0));
assert_eq!(validate_lats_exploration_weight(10.0), Some(10.0));
}
#[test]
fn test_validate_lats_exploration_weight_invalid() {
assert_eq!(validate_lats_exploration_weight(-0.1), None);
assert_eq!(validate_lats_exploration_weight(10.1), None);
assert_eq!(validate_lats_exploration_weight(100.0), None);
}
}