use anyhow::{Context, Result};
use serde_json::{json, Value};
#[derive(Debug)]
struct EndpointInfo {
endpoint: String,
backend: String, model_id: String,
model_root: String,
max_model_len: usize,
native_tool_calling: bool,
supports_thinking: bool,
thinking_uses_all_tokens: bool,
recommended_max_tokens: usize,
recommended_temperature: f32,
}
async fn probe_models(base_url: &str) -> Result<Value> {
let url = format!("{}/models", base_url.trim_end_matches('/'));
let client = reqwest::Client::builder()
.timeout(std::time::Duration::from_secs(10))
.build()?;
let resp = client
.get(&url)
.send()
.await
.with_context(|| format!("Failed to connect to {url}"))?;
let body: Value = resp
.json()
.await
.context("Failed to parse /models response")?;
Ok(body)
}
async fn test_chat(base_url: &str, model: &str, extra_body: Option<Value>) -> Result<Value> {
let url = format!("{}/chat/completions", base_url.trim_end_matches('/'));
let client = reqwest::Client::builder()
.timeout(std::time::Duration::from_secs(30))
.build()?;
let mut body = json!({
"model": model,
"messages": [{"role": "user", "content": "Say exactly: HELLO_SELFWARE"}],
"max_tokens": 64,
"temperature": 0.0,
"stream": false,
});
if let Some(extra) = extra_body {
if let (Some(body_obj), Some(extra_obj)) = (body.as_object_mut(), extra.as_object()) {
for (k, v) in extra_obj {
body_obj.insert(k.clone(), v.clone());
}
}
}
let resp = client.post(&url).json(&body).send().await?;
let result: Value = resp.json().await?;
Ok(result)
}
async fn test_tool_calling(base_url: &str, model: &str) -> Result<(bool, bool)> {
let url = format!("{}/chat/completions", base_url.trim_end_matches('/'));
let client = reqwest::Client::builder()
.timeout(std::time::Duration::from_secs(30))
.build()?;
let body = json!({
"model": model,
"messages": [{"role": "user", "content": "What is the weather in Paris?"}],
"tools": [{
"type": "function",
"function": {
"name": "get_weather",
"description": "Get the weather for a city",
"parameters": {
"type": "object",
"properties": {
"city": {"type": "string", "description": "City name"}
},
"required": ["city"]
}
}
}],
"tool_choice": "auto",
"max_tokens": 256,
"temperature": 0.0,
"stream": false,
});
let resp = client.post(&url).json(&body).send().await;
match resp {
Ok(r) => {
let status = r.status();
let result: Value = r.json().await.unwrap_or(json!({}));
if !status.is_success() {
return Ok((false, false));
}
let has_tool_calls = result["choices"][0]["message"]["tool_calls"]
.as_array()
.map(|a| !a.is_empty())
.unwrap_or(false);
Ok((true, has_tool_calls))
}
Err(_) => Ok((false, false)),
}
}
async fn probe_endpoint(base_url: &str) -> Result<EndpointInfo> {
eprintln!("Probing {base_url}...\n");
eprintln!(" [1/5] Fetching /models...");
let models = probe_models(base_url).await?;
let model_data = &models["data"][0];
let model_id = model_data["id"].as_str().unwrap_or("unknown").to_string();
let model_root = model_data["root"].as_str().unwrap_or(&model_id).to_string();
let max_model_len = model_data["max_model_len"].as_u64().unwrap_or(0) as usize;
let owned_by = model_data["owned_by"]
.as_str()
.unwrap_or("unknown")
.to_string();
let backend = match owned_by.as_str() {
"vllm" => "vllm",
"sglang" => "sglang",
_ if owned_by.contains("ollama") => "ollama",
_ => "unknown",
}
.to_string();
eprintln!(" Model: {model_id}");
eprintln!(" Root: {model_root}");
eprintln!(" Backend: {backend}");
eprintln!(" Max context: {max_model_len}");
eprintln!(" [2/5] Testing chat (thinking mode)...");
let think_result = test_chat(base_url, &model_id, None).await?;
let content = think_result["choices"][0]["message"]["content"]
.as_str()
.unwrap_or("");
let reasoning = think_result["choices"][0]["message"]["reasoning_content"]
.as_str()
.or_else(|| think_result["choices"][0]["message"]["reasoning"].as_str());
let has_reasoning = reasoning.is_some();
let content_empty = content.is_empty() || content == "null";
let thinking_uses_all = has_reasoning && content_empty;
eprintln!(" Has reasoning: {has_reasoning}");
eprintln!(" Content empty (thinking ate tokens): {thinking_uses_all}");
if !content_empty {
eprintln!(" Content: {}", &content[..content.len().min(80)]);
}
eprintln!(" [3/5] Testing chat (no-thinking mode)...");
let no_think_result = test_chat(
base_url,
&model_id,
Some(json!({"chat_template_kwargs": {"enable_thinking": false}})),
)
.await?;
let no_think_content = no_think_result["choices"][0]["message"]["content"]
.as_str()
.unwrap_or("");
let no_think_works = !no_think_content.is_empty() && no_think_content.contains("HELLO");
eprintln!(
" No-thinking response: {}",
&no_think_content[..no_think_content.len().min(80)]
);
eprintln!(" Works correctly: {no_think_works}");
eprintln!(" [4/5] Testing native tool calling...");
let (tools_accepted, native_fc_works) = test_tool_calling(base_url, &model_id).await?;
eprintln!(" Tools API accepted: {tools_accepted}");
eprintln!(" Native FC returns tool_calls: {native_fc_works}");
eprintln!(" [5/5] Computing recommendations...\n");
let supports_thinking = has_reasoning;
let native_tool_calling = native_fc_works;
let recommended_max_tokens = if supports_thinking && !thinking_uses_all {
(max_model_len / 8).min(32768)
} else if thinking_uses_all {
16384
} else {
8192
};
let recommended_temperature = if model_root.to_lowercase().contains("qwen") {
0.6 } else {
0.7
};
let info = EndpointInfo {
endpoint: base_url.to_string(),
backend,
model_id,
model_root,
max_model_len,
native_tool_calling,
supports_thinking,
thinking_uses_all_tokens: thinking_uses_all,
recommended_max_tokens,
recommended_temperature,
};
Ok(info)
}
fn generate_toml(info: &EndpointInfo) -> String {
let mut toml = String::new();
toml.push_str(&format!(
"# Auto-configured for {} ({}) on {}\n",
info.model_id, info.backend, info.endpoint
));
toml.push_str(&format!("endpoint = \"{}\"\n", info.endpoint));
toml.push_str(&format!("model = \"{}\"\n", info.model_id));
toml.push_str(&format!("max_tokens = {}\n", info.recommended_max_tokens));
toml.push_str(&format!("context_length = {}\n", info.max_model_len));
toml.push_str(&format!("temperature = {}\n", info.recommended_temperature));
toml.push('\n');
toml.push_str("[safety]\n");
toml.push_str("allowed_paths = [\"./**\", \"/tmp/**\"]\n");
toml.push_str("denied_paths = [\"**/.env\", \"**/secrets/**\", \"**/.ssh/**\"]\n");
toml.push_str("protected_branches = [\"main\"]\n\n");
toml.push_str("[agent]\n");
toml.push_str("max_iterations = 50\n");
toml.push_str("step_timeout_secs = 300\n");
toml.push_str(&format!(
"native_function_calling = {}\n",
info.native_tool_calling
));
toml.push('\n');
toml.push_str("[continuous_work]\n");
toml.push_str("enabled = true\n");
toml.push_str("checkpoint_interval_tools = 10\n");
toml.push_str("checkpoint_interval_secs = 300\n");
toml.push_str("auto_recovery = true\n");
toml.push_str("max_recovery_attempts = 3\n\n");
if info.supports_thinking && info.thinking_uses_all_tokens {
toml.push_str("# Thinking mode disabled — model consumes all tokens on reasoning\n");
toml.push_str("# with small max_tokens, leaving content empty.\n");
toml.push_str("[extra_body]\n");
toml.push_str("chat_template_kwargs = { enable_thinking = false }\n\n");
} else if info.supports_thinking {
toml.push_str("# Thinking mode available — leave enabled for better reasoning\n");
toml.push_str("# Set enable_thinking = false if responses are empty\n");
toml.push_str("# [extra_body]\n");
toml.push_str("# chat_template_kwargs = { enable_thinking = true }\n\n");
}
toml.push_str("[retry]\n");
toml.push_str("max_retries = 5\n");
toml.push_str("base_delay_ms = 1000\n");
toml.push_str("max_delay_ms = 60000\n");
toml
}
#[tokio::main]
async fn main() -> Result<()> {
let urls: Vec<String> = std::env::args().skip(1).collect();
if urls.is_empty() {
eprintln!("Usage: autoconfig <endpoint_url> [endpoint_url2] ...");
eprintln!("Example: autoconfig http://localhost:8000/v1 http://127.0.0.1:8000/v1");
return Ok(());
}
for url in &urls {
match probe_endpoint(url).await {
Ok(info) => {
eprintln!("=== Configuration for {} ===\n", info.endpoint);
eprintln!("Backend: {}", info.backend);
eprintln!("Model: {}", info.model_id);
eprintln!("Root: {}", info.model_root);
eprintln!("Max context: {} tokens", info.max_model_len);
eprintln!(
"Thinking: {}",
if info.supports_thinking { "yes" } else { "no" }
);
eprintln!("Thinking eats all: {}", info.thinking_uses_all_tokens);
eprintln!("Native FC: {}", info.native_tool_calling);
eprintln!("Recommended tokens: {}", info.recommended_max_tokens);
eprintln!("Recommended temp: {}", info.recommended_temperature);
let toml = generate_toml(&info);
let filename = format!(
"selfware-auto-{}.toml",
info.model_id.replace(['/', '.'], "-")
);
eprintln!("\n--- Generated: {filename} ---\n");
println!("{toml}");
std::fs::write(&filename, &toml)?;
eprintln!("Written to {filename}\n");
}
Err(e) => {
eprintln!("Failed to probe {url}: {e}\n");
}
}
}
Ok(())
}