use crate::cli::{Cli, LlmBackendChoice};
use crate::errors::AppError;
use crate::extract::llm_embedding::LlmEmbedding;
use crate::output::emit_json_compact;
use crate::spawn::env_whitelist::is_strict_env_clear;
use serde::Serialize;
#[derive(Serialize)]
pub struct DryRunBackendOutput {
pub action: &'static str,
pub backend: &'static str,
pub binary: String,
pub model: String,
pub flavour: &'static str,
pub chain: String,
pub strict_env_clear: bool,
}
pub fn emit_dry_run_backend(cli: &Cli) -> Result<(), AppError> {
let payload = match cli.llm_backend {
LlmBackendChoice::None => DryRunBackendOutput {
action: "dry_run_backend",
backend: "none",
binary: String::new(),
model: String::new(),
flavour: "none",
chain: cli.llm_fallback.clone(),
strict_env_clear: is_strict_env_clear(),
},
LlmBackendChoice::Auto => {
let resolved = LlmEmbedding::detect_available()?;
backend_payload(&resolved, "codex-first-then-claude", cli, true)
}
LlmBackendChoice::Codex => {
let resolved = LlmEmbedding::detect_available()?;
let flavour = resolved.model_label();
if flavour.starts_with("claude:") {
return Err(AppError::Embedding(
"`--llm-backend codex` requested but `codex` was not found on PATH \
(a `claude` binary was detected; refusing silent fallback per ADR-0042). \
Install `codex` (>= 0.130) or pass `--llm-backend claude` explicitly."
.to_string(),
));
}
backend_payload(&resolved, "codex-explicit", cli, false)
}
LlmBackendChoice::Claude => {
let resolved = LlmEmbedding::detect_available()?;
let flavour = resolved.model_label();
if flavour.starts_with("codex:") {
return Err(AppError::Embedding(
"`--llm-backend claude` requested but `claude` was not found on PATH \
(a `codex` binary was detected; refusing silent fallback per ADR-0042). \
Install `claude` (Claude Code >= 2.1) or pass `--llm-backend codex` explicitly."
.to_string(),
));
}
backend_payload(&resolved, "claude-explicit", cli, false)
}
LlmBackendChoice::Opencode => {
let resolved = LlmEmbedding::detect_available()?;
let flavour = resolved.model_label();
if !flavour.starts_with("opencode:") {
let hint = if flavour.starts_with("codex:") || flavour.starts_with("claude:") {
format!(
"`--llm-backend opencode` requested but auto-detect resolved `{flavour}` \
(opencode has lower priority than codex/claude in detect_available). \
Pass `--llm-backend auto` or set SQLITE_GRAPHRAG_OPENCODE_BINARY explicitly."
)
} else {
"`--llm-backend opencode` requested but `opencode` was not found on PATH. \
Install `opencode` (>= 1.17) or pass `--llm-backend auto` to auto-detect."
.to_string()
};
return Err(AppError::Embedding(hint));
}
backend_payload(&resolved, "opencode-explicit", cli, false)
}
LlmBackendChoice::OpenRouter => DryRunBackendOutput {
action: "dry_run_backend",
backend: "openrouter",
binary: String::new(),
model: String::new(),
flavour: "openrouter",
chain: cli.llm_fallback.clone(),
strict_env_clear: is_strict_env_clear(),
},
};
emit_json_compact(&payload)?;
Ok(())
}
fn backend_payload(
resolved: &LlmEmbedding,
chain_label: &str,
cli: &Cli,
is_auto: bool,
) -> DryRunBackendOutput {
let label = resolved.model_label();
let (flavour, model) = match label.split_once(':') {
Some((f, m)) => (f, m.to_string()),
None => ("unknown", label.to_string()),
};
let flavour: &'static str = Box::leak(flavour.to_string().into_boxed_str());
let binary = which::which(if is_auto {
if which::which("codex").is_ok() {
"codex"
} else {
"claude"
}
} else {
flavour
})
.ok()
.and_then(|p| std::fs::canonicalize(&p).ok().or(Some(p)))
.map(|p| p.display().to_string())
.unwrap_or_default();
let backend = match cli.llm_backend {
LlmBackendChoice::Auto => {
if flavour == "codex" {
"codex"
} else if flavour == "opencode" {
"opencode"
} else {
"claude"
}
}
LlmBackendChoice::Codex => "codex",
LlmBackendChoice::Claude => "claude",
LlmBackendChoice::Opencode => "opencode",
LlmBackendChoice::OpenRouter => "openrouter",
LlmBackendChoice::None => "none",
};
DryRunBackendOutput {
action: "dry_run_backend",
backend,
binary,
model,
flavour,
chain: if chain_label == "codex-first-then-claude" {
cli.llm_fallback.clone()
} else {
chain_label.to_string()
},
strict_env_clear: is_strict_env_clear(),
}
}