use super::super::parse::parse_issues;
use super::super::prompts::{SegmentedPrompt, build_segmented_prompt};
use super::super::providers::call_ai_provider_segmented;
use super::super::{
AgentCliReviewLlm, HttpReviewLlm, ReviewIssueRecord, ReviewLlm, ReviewPerspective,
};
use super::ReviewEngine;
use crate::context::types::PastVerdict;
use crate::errors::CoreError;
use gate4agent::CliTool;
#[derive(sqlx::FromRow)]
pub(in super::super) struct ProviderRow {
#[allow(dead_code)]
pub id: String,
pub name: String,
pub base_url: String,
pub api_key: String,
pub model_mapping: String,
#[allow(dead_code)]
pub is_active: i64,
}
pub(super) fn command_exists_on_path(command: &str) -> bool {
let path = std::path::Path::new(command);
if path.is_absolute() || command.contains('/') || command.contains('\\') {
return path.exists();
}
let probe = if cfg!(windows) { "where" } else { "which" };
std::process::Command::new(probe)
.arg(command)
.output()
.is_ok_and(|o| o.status.success())
}
pub(in super::super) async fn resolve_review_engine(
db: &sqlx::SqlitePool,
) -> crate::Result<ReviewEngine> {
match get_active_provider(db).await {
Ok((provider_name, base_url, api_key, model)) => Ok(ReviewEngine::HttpProvider {
provider_name,
base_url,
api_key,
model,
}),
Err(CoreError::Validation(_)) => {
for (cmd, tool) in [
("claude", CliTool::ClaudeCode),
("codex", CliTool::Codex),
("gemini", CliTool::Gemini),
("opencode", CliTool::OpenCode),
] {
if command_exists_on_path(cmd) {
return Ok(ReviewEngine::AgentCli {
tool,
model: String::new(),
});
}
}
Err(CoreError::Validation(
"no LLM provider configured and no supported agent CLI found on PATH \
(looked for: claude, codex, gemini, opencode).\n\n \
Pick a provider: `difflore providers setup`\n \
(options: Claude Code / Codex / Gemini / OpenCode CLI, Anthropic API, \
or any OpenAI-compatible gateway)\n\n \
Or install Claude Code: https://docs.anthropic.com/en/docs/claude-code"
.into(),
))
}
Err(e) => Err(e),
}
}
pub(super) fn make_review_llm(engine: ReviewEngine) -> Box<dyn ReviewLlm> {
match engine {
ReviewEngine::HttpProvider {
provider_name,
base_url,
api_key,
model,
} => Box::new(HttpReviewLlm {
provider_name,
base_url,
api_key,
model,
}),
ReviewEngine::AgentCli { tool, model } => Box::new(AgentCliReviewLlm { tool, model }),
}
}
pub(super) async fn call_review_engine(
engine: &ReviewEngine,
segmented: &SegmentedPrompt,
user_prompt: &str,
) -> crate::Result<String> {
match engine {
ReviewEngine::HttpProvider {
provider_name,
base_url,
api_key,
model,
} => {
call_ai_provider_segmented(
provider_name,
base_url,
api_key,
model,
segmented,
user_prompt,
)
.await
}
ReviewEngine::AgentCli { tool, model } => {
super::super::providers::call_agent_cli_provider(
*tool,
model,
&segmented.stable_prefix,
&format!("{}\n\n{}", segmented.dynamic_suffix, user_prompt),
)
.await
}
}
}
pub(super) async fn get_active_provider(
db: &sqlx::SqlitePool,
) -> crate::Result<(String, String, String, String)> {
let row = sqlx::query_as!(
ProviderRow,
"SELECT id, name, base_url, api_key, model_mapping, is_active FROM providers WHERE is_active = 1 LIMIT 1"
)
.fetch_optional(db)
.await?
.ok_or_else(|| CoreError::Validation("No active AI provider configured. Run `difflore providers setup` to add one.".into()))?;
let api_key = crate::crypto::decrypt_secret(&row.api_key)
.map_err(|e| CoreError::Internal(format!("Failed to decrypt API key: {e}")))?;
let mapping: std::collections::HashMap<String, String> =
serde_json::from_str(&row.model_mapping).unwrap_or_default();
let model = mapping
.get("review")
.or_else(|| mapping.get("default"))
.cloned()
.unwrap_or_else(|| {
if row.name.to_lowercase().contains("anthropic")
|| row.name.to_lowercase().contains("claude")
{
"claude-sonnet-4-6".to_owned()
} else {
"gpt-4o".to_owned()
}
});
Ok((row.name, row.base_url, api_key, model))
}
pub(super) struct PerspectiveRun<'a> {
pub provider_name: &'a str,
pub base_url: &'a str,
pub api_key: &'a str,
pub model: &'a str,
pub user_prompt: &'a str,
pub perspective: ReviewPerspective,
pub diff_content: &'a str,
pub past_verdicts: &'a [PastVerdict],
}
pub(super) async fn run_one_perspective(run: PerspectiveRun<'_>) -> Vec<ReviewIssueRecord> {
let seg = build_segmented_prompt(
Some(run.perspective),
&[],
run.diff_content,
"",
None,
if run.past_verdicts.is_empty() {
None
} else {
Some(run.past_verdicts)
},
);
match call_ai_provider_segmented(
run.provider_name,
run.base_url,
run.api_key,
run.model,
&seg,
run.user_prompt,
)
.await
{
Ok(ai_response) => {
let parsed = parse_issues(&ai_response);
if crate::env::fix_debug() {
eprintln!(
"[fix-debug] perspective={} raw_response_len={} parsed_issues={}",
run.perspective.name(),
ai_response.len(),
parsed.len(),
);
if parsed.is_empty() && ai_response.len() < 4000 {
eprintln!("[fix-debug] response body: {ai_response}");
}
}
parsed
}
Err(e) => {
eprintln!(
"[review_check_multi] perspective {} failed: {:?}",
run.perspective.name(),
e
);
Vec::new()
}
}
}