use std::path::PathBuf;
use std::time::Duration;
const EVAL_BASE_DIR: &str = "/tmp/conproxy-eval";
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum EvalProvider {
Ollama,
Claude,
LlamaCpp,
}
impl EvalProvider {
pub fn from_str(s: &str) -> Option<Self> {
match s.trim().to_lowercase().as_str() {
"ollama" => Some(EvalProvider::Ollama),
"claude" => Some(EvalProvider::Claude),
"llamacpp" | "llama" | "openai-compat" => Some(EvalProvider::LlamaCpp),
_ => None,
}
}
}
impl std::fmt::Display for EvalProvider {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
EvalProvider::Ollama => write!(f, "ollama"),
EvalProvider::Claude => write!(f, "claude"),
EvalProvider::LlamaCpp => write!(f, "llamacpp"),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Vertical {
NoContext,
McpTools,
}
impl Vertical {
pub const ALL: &[Vertical] = &[Vertical::NoContext, Vertical::McpTools];
pub fn name(&self) -> &'static str {
match self {
Vertical::NoContext => "no_context",
Vertical::McpTools => "mcp_tools",
}
}
pub fn short_name(&self) -> &'static str {
match self {
Vertical::NoContext => "No Ctx",
Vertical::McpTools => "MCP Tools",
}
}
pub fn from_str(s: &str) -> Option<Self> {
match s.trim() {
"no_context" => Some(Vertical::NoContext),
"mcp_tools" => Some(Vertical::McpTools),
_ => None,
}
}
}
impl std::fmt::Display for Vertical {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.name())
}
}
pub struct EvalConfig {
pub provider: EvalProvider,
pub claude_bin: PathBuf,
pub claude_model: Option<String>,
pub ollama_base_url: String,
pub ollama_model: String,
pub llama_base_url: String,
pub llama_model: String,
pub llama_api_key: String,
pub conproxy_bin: PathBuf,
pub project_root: PathBuf,
pub timeout: Duration,
pub enabled_verticals: Vec<Vertical>,
pub enabled_queries: Option<Vec<String>>,
pub output_dir: Option<PathBuf>,
pub proxy_listen: String,
pub base_dir: PathBuf,
pub query_concurrency: usize,
}
impl EvalConfig {
pub fn from_env() -> Self {
let provider = std::env::var("EVAL_PROVIDER")
.ok()
.and_then(|v| EvalProvider::from_str(&v))
.unwrap_or(EvalProvider::Ollama);
let claude_bin = std::env::var("CLAUDE_BIN")
.map(PathBuf::from)
.unwrap_or_else(|_| PathBuf::from("claude"));
let claude_model = std::env::var("EVAL_CLAUDE_MODEL").ok();
let ollama_base_url = std::env::var("OLLAMA_BASE_URL")
.unwrap_or_else(|_| "http://localhost:11434".to_string());
let ollama_model =
std::env::var("OLLAMA_MODEL").unwrap_or_else(|_| "qwen3:0.6b".to_string());
let llama_base_url =
std::env::var("LLAMA_BASE_URL").unwrap_or_else(|_| "http://localhost:8081".to_string());
let llama_model = std::env::var("LLAMA_MODEL").unwrap_or_else(|_| "auto".to_string());
let llama_api_key =
std::env::var("LLAMA_API_KEY").unwrap_or_else(|_| "sk-local".to_string());
let conproxy_bin = std::env::var("PROXY_BIN")
.map(PathBuf::from)
.unwrap_or_else(|_| {
PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join("target")
.join("release")
.join("conproxy")
});
let project_root = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
let timeout_secs: u64 = std::env::var("EVAL_TIMEOUT_SECS")
.or_else(|_| std::env::var("EVAL_CLAUDE_TIMEOUT_SECS"))
.ok()
.and_then(|v| v.parse().ok())
.unwrap_or(60);
let enabled_verticals = match std::env::var("EVAL_VERTICALS") {
Ok(val) if !val.is_empty() => val.split(',').filter_map(Vertical::from_str).collect(),
_ => Vertical::ALL.to_vec(),
};
let enabled_queries = std::env::var("EVAL_QUERIES").ok().map(|val| {
val.split(',')
.map(|s| s.trim().to_string())
.filter(|s| !s.is_empty())
.collect()
});
let proxy_listen =
std::env::var("EVAL_PROXY_LISTEN").unwrap_or_else(|_| "127.0.0.1:8080".to_string());
let output_dir = std::env::var("EVAL_OUTPUT_DIR").ok().map(PathBuf::from);
let base_dir = PathBuf::from(EVAL_BASE_DIR);
let query_concurrency: usize = std::env::var("EVAL_QUERY_CONCURRENCY")
.ok()
.and_then(|v| v.parse().ok())
.unwrap_or(3)
.max(1);
Self {
provider,
claude_bin,
claude_model,
ollama_base_url,
ollama_model,
llama_base_url,
llama_model,
llama_api_key,
conproxy_bin,
project_root,
timeout: Duration::from_secs(timeout_secs),
enabled_verticals,
enabled_queries,
output_dir,
proxy_listen,
base_dir,
query_concurrency,
}
}
pub fn model_display(&self) -> String {
match self.provider {
EvalProvider::Ollama => self.ollama_model.clone(),
EvalProvider::Claude => self
.claude_model
.clone()
.unwrap_or_else(|| "(default)".into()),
EvalProvider::LlamaCpp => self.llama_model.clone(),
}
}
pub fn init_base_dir(&self) {
let _ = std::fs::remove_dir_all(&self.base_dir);
std::fs::create_dir_all(&self.base_dir).expect("Failed to create eval base dir");
if self.provider == EvalProvider::Claude {
if let Some(home) = std::env::var_os("HOME") {
let claude_projects = PathBuf::from(home).join(".claude").join("projects");
if claude_projects.is_dir() {
if let Ok(entries) = std::fs::read_dir(&claude_projects) {
for entry in entries.flatten() {
let name = entry.file_name();
let name_str = name.to_string_lossy();
if name_str.starts_with("-tmp-conproxy-eval") {
let path = entry.path();
eprintln!(" Cleaning Claude project memory: {}", path.display());
let _ = std::fs::remove_dir_all(&path);
}
}
}
}
}
}
}
}