async fn generate_simple_free_commit_message(
config: &mut SimpleFreeOpenRouterConfig,
diff: &str,
cli: &Cli
) -> Result<(String, UsageInfo), String> {
let client = reqwest::Client::builder()
.timeout(std::time::Duration::from_secs(15))
.build()
.unwrap_or_default();
if cli.verbose {
println!("\n=== Getting available free models from OpenRouter ===");
println!("API Key: {}", config.api_key.chars().take(8).collect::<String>() + "..." + &config.api_key.chars().rev().take(4).collect::<String>());
}
let available_models = match get_available_free_models(&config.api_key, cli.simulate_offline).await {
Ok(models) => models,
Err(e) => {
println!("Error fetching models from OpenRouter: {}", e);
println!("This could be due to network connectivity issues or API changes.");
println!("Using fallback predefined free models list...");
match fallback_to_preferred_models() {
Ok(models) => models,
Err(e) => return Err(format!("Failed to get models and fallback also failed: {}", e)),
}
}
};
if cli.verbose {
println!("Found {} free models:", available_models.len());
for (i, model) in available_models.iter().enumerate().take(10) {
println!(" {}. {}", i+1, model);
}
if available_models.len() > 10 {
println!(" ... and {} more", available_models.len() - 10);
}
}
if available_models.is_empty() {
return Err("No free models available on OpenRouter".to_string());
}
let model = find_best_available_model(&available_models, config)
.ok_or_else(|| "Failed to find a suitable model, please try again later".to_string())?;
let processed_diff = process_git_diff_output(diff);
let prompt = format!(
"Generate ONLY the git commit message string based on the provided diff. Follow the Conventional Commits specification (type: description). Do NOT include any introductory phrases, explanations, or markdown formatting like ```.
Examples:
- feat: Add user authentication feature
- fix: Correct calculation error in payment module
- docs: Update README with installation instructions
- style: Format code according to style guide
- refactor: Simplify database query logic
- test: Add unit tests for user service
- chore: Update dependencies
Git Diff:
```diff
{}
```
Commit Message ONLY:",
processed_diff
);
if cli.verbose {
println!("\n=== Context for LLM ===");
println!("Provider: Simple Free OpenRouter");
println!("Model: {}", model);
let model_status = if let Some(stats) = config.model_stats.get(&model) {
if stats.blacklisted {
"BLACKLISTED (being retried)"
} else if stats.jail_until.is_some() && stats.jail_until.unwrap() > chrono::Utc::now() {
"JAILED (being retried)"
} else {
"ACTIVE"
}
} else {
"NEW (no history)"
};
println!("Model status: {}", model_status);
println!("Max tokens: {}", config.max_tokens);
println!("Temperature: {}", config.temperature);
println!("\n=== Prompt ===\n{}", prompt);
println!("\n=== Sending request to API ===");
}
let request_body = json!({
"model": model,
"messages": [
{
"role": "user",
"content": prompt
}
],
"max_tokens": config.max_tokens,
"temperature": config.temperature,
});
let make_request = async {
client
.post("https://openrouter.ai/api/v1/chat/completions")
.header("Authorization", format!("Bearer {}", &config.api_key))
.header("HTTP-Referer", "https://suenot.github.io/aicommit/")
.header("X-Title", "aicommit")
.header("X-Description", "A CLI tool that generates concise and descriptive git commit messages")
.json(&request_body)
.send()
.await
};
let response = match tokio::time::timeout(std::time::Duration::from_secs(30), make_request).await {
Ok(result) => match result {
Ok(response) => response,
Err(e) => {
let stats = config.model_stats.entry(model.clone()).or_default();
record_model_failure(stats);
save_simple_free_config(config)?;
return Err(format!("Request error: {}", e));
},
},
Err(_) => {
let stats = config.model_stats.entry(model.clone()).or_default();
let is_likely_network_issue = stats.failure_count == 0 ||
(stats.last_success.is_some() &&
chrono::Utc::now() - stats.last_success.unwrap() < chrono::Duration::hours(1));
if !is_likely_network_issue {
record_model_failure(stats);
save_simple_free_config(config)?;
}
return Err("Request timed out after 30 seconds".to_string());
},
};
if !response.status().is_success() {
let status_code = response.status();
let error_text = match response.text().await {
Ok(text) => format!("API error response: {}", text),
Err(_) => format!("API returned status code: {}", status_code),
};
if cli.verbose {
println!("Request failed for model {}: {}", model, error_text);
}
let stats = config.model_stats.entry(model.clone()).or_default();
record_model_failure(stats);
save_simple_free_config(config)?;
return Err(format!("API request failed with model {}: {}", model, error_text));
}
let response_text = response.text().await
.map_err(|e| format!("Failed to read response body: {}", e))?;
let response_data: Result<OpenRouterResponse, _> = serde_json::from_str(&response_text);
if let Err(e) = &response_data {
if cli.verbose {
println!("Failed to parse response: {}", e);
println!("Response body: {}", response_text);
}
let stats = config.model_stats.entry(model.clone()).or_default();
record_model_failure(stats);
save_simple_free_config(config)?;
return Err(format!("Failed to parse response JSON: {} (Response: {})", e,
if response_text.len() > 100 {
format!("{}...", &response_text[..100])
} else {
response_text.clone()
}));
}
let response_data = response_data.unwrap();
let raw_message = response_data.choices
.get(0)
.ok_or_else(|| {
let stats = config.model_stats.entry(model.clone()).or_default();
record_model_failure(stats);
let _ = save_simple_free_config(config);
"No choices in response"
})?
.message
.content
.clone();
let message = raw_message
.trim()
.trim_start_matches(['\\', '/', '-', ' '])
.trim_end_matches(['\\', '/', '-', ' ', '.'])
.trim()
.to_string();
if message.is_empty() || message.len() < 3 {
let stats = config.model_stats.entry(model.clone()).or_default();
record_model_failure(stats);
let _ = save_simple_free_config(config);
return Err("Generated commit message is too short or empty".to_string());
}
let usage = UsageInfo {
input_tokens: response_data.usage.prompt_tokens,
output_tokens: response_data.usage.completion_tokens,
total_cost: 0.0, model_used: Some(model.clone()),
};
let stats = config.model_stats.entry(model.clone()).or_default();
record_model_success(stats);
config.last_used_model = Some(model.clone());
save_simple_free_config(config)?;
if cli.verbose {
println!("Successfully generated commit message using model: {}", model);
}
Ok((message, usage))
}