use crate::types::*;
use crate::{PREFERRED_FREE_MODELS, MAX_CONSECUTIVE_FAILURES, INITIAL_JAIL_HOURS, JAIL_TIME_MULTIPLIER, MAX_JAIL_HOURS, BLACKLIST_AFTER_JAIL_COUNT, BLACKLIST_RETRY_DAYS};
use std::fs;
use chrono;
pub async fn get_available_free_models(api_key: &str, simulate_offline: bool) -> Result<Vec<String>, String> {
if simulate_offline {
println!("Debug: Simulating offline mode, using fallback model list");
return fallback_to_preferred_models();
}
let client = reqwest::Client::builder()
.timeout(std::time::Duration::from_secs(10)) .build()
.unwrap_or_default();
let response = match tokio::time::timeout(
std::time::Duration::from_secs(15),
client.get("https://openrouter.ai/api/v1/models")
.header("Authorization", format!("Bearer {}", api_key))
.header("HTTP-Referer", "https://suenot.github.io/aicommit/")
.header("X-Title", "aicommit")
.send()
).await {
Ok(result) => match result {
Ok(response) => {
if !response.status().is_success() {
println!("Warning: OpenRouter API returned status code: {}", response.status());
return fallback_to_preferred_models();
}
response
},
Err(e) => {
println!("Warning: Failed to connect to OpenRouter API: {}", e);
println!("Using predefined free models as fallback...");
return fallback_to_preferred_models();
}
},
Err(_) => {
println!("Warning: Request to OpenRouter API timed out after 15 seconds");
println!("Using predefined free models as fallback...");
return fallback_to_preferred_models();
}
};
let models_response: Result<serde_json::Value, _> = response.json().await;
if let Err(e) = &models_response {
println!("Warning: Failed to parse OpenRouter API response: {}", e);
println!("Using predefined free models as fallback...");
return fallback_to_preferred_models();
}
let models_response = models_response.unwrap();
let mut free_models = Vec::new();
if let Some(data) = models_response["data"].as_array() {
for model in data {
if let Some(id) = model["id"].as_str() {
if id.contains(":free") {
free_models.push(id.to_string());
continue;
}
if let Some(true) = model["free"].as_bool() {
free_models.push(id.to_string());
continue;
}
if let Some(tokens) = model["free_tokens"].as_u64() {
if tokens > 0 {
free_models.push(id.to_string());
continue;
}
}
if let Some(pricing) = model["pricing"].as_object() {
let prompt_price = pricing.get("prompt")
.and_then(|v| v.as_f64())
.unwrap_or(1.0);
let completion_price = pricing.get("completion")
.and_then(|v| v.as_f64())
.unwrap_or(1.0);
if prompt_price == 0.0 && completion_price == 0.0 {
free_models.push(id.to_string());
continue;
}
}
}
}
if free_models.is_empty() {
for model in data {
if let Some(id) = model["id"].as_str() {
if let Some(pricing) = model["pricing"].as_object() {
let prompt_price = pricing.get("prompt")
.and_then(|v| v.as_f64())
.unwrap_or(1.0);
let completion_price = pricing.get("completion")
.and_then(|v| v.as_f64())
.unwrap_or(1.0);
if prompt_price <= 0.0001 && completion_price <= 0.0001 {
free_models.push(id.to_string());
}
}
}
}
}
}
if free_models.is_empty() {
println!("Warning: No free models found from OpenRouter API");
println!("Using predefined free models as fallback...");
return fallback_to_preferred_models();
}
Ok(free_models)
}
pub fn fallback_to_preferred_models() -> Result<Vec<String>, String> {
let mut models = Vec::new();
for model in PREFERRED_FREE_MODELS {
models.push(model.to_string());
}
if models.is_empty() {
return Err("No free models available, and fallback model list is empty".to_string());
}
Ok(models)
}
pub fn find_best_available_model(available_models: &[String], config: &SimpleFreeOpenRouterConfig) -> Option<String> {
if let Some(last_model) = &config.last_used_model {
if available_models.contains(last_model) {
let stats = config.model_stats.get(last_model);
if is_model_available(&stats) {
return Some(last_model.clone());
}
}
}
let available_candidates: Vec<&String> = available_models
.iter()
.filter(|model| {
let stats = config.model_stats.get(*model);
is_model_available(&stats)
})
.collect();
for preferred in PREFERRED_FREE_MODELS {
let preferred_str = preferred.to_string();
if available_candidates.contains(&&preferred_str) {
return Some(preferred_str);
}
}
if !available_candidates.is_empty() {
let mut sorted_candidates = available_candidates.clone();
sorted_candidates.sort_by(|a, b| {
let a_size = extract_model_size(a);
let b_size = extract_model_size(b);
b_size.cmp(&a_size) });
return Some(sorted_candidates[0].clone());
}
if !available_models.is_empty() {
let mut jailed_models: Vec<(String, chrono::DateTime<chrono::Utc>)> = Vec::new();
for model in available_models {
if let Some(stats) = config.model_stats.get(model) {
if !stats.blacklisted && stats.jail_until.is_some() {
jailed_models.push((model.clone(), stats.jail_until.unwrap()));
}
}
}
if !jailed_models.is_empty() {
jailed_models.sort_by_key(|x| x.1);
return Some(jailed_models[0].0.clone());
}
return Some(available_models[0].clone());
}
None
}
pub fn extract_model_size(model_name: &str) -> u32 {
let lower_name = model_name.to_lowercase();
let patterns = [
"253b", "235b", "200b", "124b",
"70b", "80b", "90b", "72b", "65b",
"40b", "32b", "30b", "24b", "20b",
"16b", "14b", "13b", "12b", "11b", "10b",
"9b", "8b", "7b", "6b", "5b", "4b", "3b", "2b", "1b"
];
for pattern in patterns {
if lower_name.contains(pattern) {
if let Ok(size) = pattern.trim_end_matches(|c| c == 'b' || c == 'B').parse::<u32>() {
return size;
}
}
}
if lower_name.contains("large") || lower_name.contains("ultra") {
return 15; } else if lower_name.contains("medium") {
return 10;
} else if lower_name.contains("small") || lower_name.contains("tiny") {
return 5;
}
0
}
pub fn is_model_available(model_stats: &Option<&ModelStats>) -> bool {
match model_stats {
None => true, Some(stats) => {
if stats.blacklisted {
if let Some(blacklisted_since) = stats.blacklisted_since {
let retry_duration = chrono::Duration::days(BLACKLIST_RETRY_DAYS);
let now = chrono::Utc::now();
if now - blacklisted_since > retry_duration {
return true;
}
return false;
}
return false;
}
if let Some(jail_until) = stats.jail_until {
if chrono::Utc::now() < jail_until {
return false;
}
}
true
}
}
}
pub fn record_model_success(model_stats: &mut ModelStats) {
model_stats.success_count += 1;
model_stats.last_success = Some(chrono::Utc::now());
if model_stats.last_failure.is_none() ||
model_stats.last_success.unwrap() > model_stats.last_failure.unwrap() {
model_stats.jail_until = None;
}
}
pub fn record_model_failure(model_stats: &mut ModelStats) {
let now = chrono::Utc::now();
model_stats.failure_count += 1;
model_stats.last_failure = Some(now);
let has_consecutive_failures = match model_stats.last_success {
None => true, Some(last_success) => {
model_stats.last_failure.unwrap() > last_success
}
};
if has_consecutive_failures {
let consecutive_failures = if let Some(last_success) = model_stats.last_success {
let hours_since_success = (now - last_success).num_hours();
if hours_since_success > 24 {
model_stats.failure_count.min(MAX_CONSECUTIVE_FAILURES)
} else {
1 }
} else {
model_stats.failure_count.min(MAX_CONSECUTIVE_FAILURES)
};
if consecutive_failures >= MAX_CONSECUTIVE_FAILURES {
let jail_hours = INITIAL_JAIL_HOURS * JAIL_TIME_MULTIPLIER.pow(model_stats.jail_count as u32);
let jail_hours = jail_hours.min(MAX_JAIL_HOURS);
model_stats.jail_until = Some(now + chrono::Duration::hours(jail_hours));
model_stats.jail_count += 1;
if model_stats.jail_count >= BLACKLIST_AFTER_JAIL_COUNT {
model_stats.blacklisted = true;
model_stats.blacklisted_since = Some(now);
}
}
}
}
pub fn format_model_status(model: &str, stats: &ModelStats) -> String {
let status = if stats.blacklisted {
"BLACKLISTED".to_string()
} else if let Some(jail_until) = stats.jail_until {
if chrono::Utc::now() < jail_until {
let remaining = jail_until - chrono::Utc::now();
format!("JAILED ({}h remaining)", remaining.num_hours())
} else {
"ACTIVE".to_string()
}
} else {
"ACTIVE".to_string()
};
let last_success = stats.last_success.map_or("Never".to_string(), |ts| {
let ago = chrono::Utc::now() - ts;
if ago.num_days() > 0 {
format!("{} days ago", ago.num_days())
} else if ago.num_hours() > 0 {
format!("{} hours ago", ago.num_hours())
} else {
format!("{} minutes ago", ago.num_minutes())
}
});
let last_failure = stats.last_failure.map_or("Never".to_string(), |ts| {
let ago = chrono::Utc::now() - ts;
if ago.num_days() > 0 {
format!("{} days ago", ago.num_days())
} else if ago.num_hours() > 0 {
format!("{} hours ago", ago.num_hours())
} else {
format!("{} minutes ago", ago.num_minutes())
}
});
format!("{}: {} (Success: {}, Failure: {}, Last success: {}, Last failure: {})",
model, status, stats.success_count, stats.failure_count, last_success, last_failure)
}
pub fn display_model_jail_status(config: &SimpleFreeOpenRouterConfig) -> Result<(), String> {
if config.model_stats.is_empty() {
println!("No model statistics available yet.");
return Ok(());
}
println!("\nModel Status Report:");
println!("===================");
let mut active_models = Vec::new();
let mut jailed_models = Vec::new();
let mut blacklisted_models = Vec::new();
for (model, stats) in &config.model_stats {
if stats.blacklisted {
blacklisted_models.push(format_model_status(model, stats));
} else if let Some(jail_until) = stats.jail_until {
if chrono::Utc::now() < jail_until {
jailed_models.push(format_model_status(model, stats));
} else {
active_models.push(format_model_status(model, stats));
}
} else {
active_models.push(format_model_status(model, stats));
}
}
if !active_models.is_empty() {
println!("\nACTIVE MODELS:");
active_models.sort();
for model in active_models {
println!(" {}", model);
}
}
if !jailed_models.is_empty() {
println!("\nJAILED MODELS:");
jailed_models.sort();
for model in jailed_models {
println!(" {}", model);
}
}
if !blacklisted_models.is_empty() {
println!("\nBLACKLISTED MODELS:");
blacklisted_models.sort();
for model in blacklisted_models {
println!(" {}", model);
}
}
Ok(())
}
pub fn unjail_model(config: &mut SimpleFreeOpenRouterConfig, model_id: &str) -> Result<(), String> {
let model_found = if model_id == "*" {
for (_, stats) in config.model_stats.iter_mut() {
stats.jail_until = None;
stats.blacklisted = false;
stats.jail_count = 0;
}
true
} else {
if let Some(stats) = config.model_stats.get_mut(model_id) {
stats.jail_until = None;
stats.blacklisted = false;
stats.jail_count = 0;
true
} else {
false
}
};
if !model_found {
return Err(format!("Model '{}' not found in statistics", model_id));
}
let config_path = dirs::home_dir()
.ok_or_else(|| "Could not find home directory".to_string())?
.join(".aicommit.json");
let mut full_config = Config::load()?;
for provider in &mut full_config.providers {
if let ProviderConfig::SimpleFreeOpenRouter(simple_config) = provider {
if simple_config.id == config.id {
*simple_config = config.clone();
break;
}
}
}
let content = serde_json::to_string_pretty(&full_config)
.map_err(|e| format!("Failed to serialize config: {}", e))?;
fs::write(&config_path, content)
.map_err(|e| format!("Failed to write config file: {}", e))?;
Ok(())
}
pub fn unjail_all_models(config: &mut SimpleFreeOpenRouterConfig) -> Result<(), String> {
unjail_model(config, "*")
}