use super::*;
use crate::errors::AppError;
use std::path::Path;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum LlmBackendKind {
Codex,
Claude,
Opencode,
OpenRouter,
None,
}
impl LlmBackendKind {
pub fn as_str(self) -> &'static str {
match self {
Self::Codex => "codex",
Self::Claude => "claude",
Self::Opencode => "opencode",
Self::OpenRouter => "openrouter",
Self::None => "none",
}
}
}
pub(crate) fn backend_ready_probe(backend: &LlmBackendKind) -> Result<(), AppError> {
match backend {
LlmBackendKind::None => Ok(()),
LlmBackendKind::OpenRouter => {
if OPENROUTER_CLIENT.get().is_some() {
Ok(())
} else {
Err(AppError::Embedding(
crate::i18n::validation::embedding_openrouter_probe_not_initialised(),
))
}
}
LlmBackendKind::Codex => {
let bin = crate::runtime_config::codex_binary().unwrap_or_else(|| "codex".into());
if which::which(&bin).is_err() && which::which("codex").is_err() {
return Err(AppError::Embedding(
crate::i18n::validation::embedding_codex_probe_binary_not_on_path(),
));
}
let auth = std::env::var_os("CODEX_HOME")
.map(std::path::PathBuf::from)
.or_else(|| {
std::env::var_os("HOME").map(|h| std::path::PathBuf::from(h).join(".codex"))
})
.map(|p| p.join("auth.json"));
match auth {
Some(p) if p.is_file() => Ok(()),
_ => Err(AppError::Embedding(
crate::i18n::validation::embedding_codex_probe_auth_missing(),
)),
}
}
LlmBackendKind::Claude => {
let bin = crate::runtime_config::claude_binary().unwrap_or_else(|| "claude".into());
if which::which(&bin).is_err() && which::which("claude").is_err() {
return Err(AppError::Embedding(
crate::i18n::validation::embedding_claude_probe_binary_not_on_path(),
));
}
Ok(())
}
LlmBackendKind::Opencode => {
let bin = crate::runtime_config::opencode_binary().unwrap_or_else(|| "opencode".into());
if which::which(&bin).is_err() && which::which("opencode").is_err() {
return Err(AppError::Embedding(
crate::i18n::validation::embedding_opencode_probe_binary_not_on_path(),
));
}
Ok(())
}
}
}
pub fn embed_via_backend(
models_dir: &Path,
text: &str,
backend: &LlmBackendKind,
) -> Result<(Vec<f32>, LlmBackendKind), AppError> {
match backend {
LlmBackendKind::None => Ok((Vec::new(), LlmBackendKind::None)),
LlmBackendKind::Codex => embed_passage_local_resolved(models_dir, text),
LlmBackendKind::Claude => {
tracing::debug!(
target: "embedder",
backend = "claude",
"embed_via_backend: forcing claude (ADR-0042 / GAP-002 fix)"
);
embed_via_claude_local_resolved(models_dir, text, None, None)
}
LlmBackendKind::Opencode => {
tracing::debug!(
target: "embedder",
backend = "opencode",
"embed_via_backend: forcing opencode (GAP-OPENCODE-001)"
);
embed_via_opencode_local_resolved(models_dir, text, None, None)
}
LlmBackendKind::OpenRouter => {
tracing::debug!(
target: "embedder",
backend = "openrouter",
"embed_via_backend: using OpenRouter API (v1.0.93)"
);
let client = OPENROUTER_CLIENT.get().ok_or_else(|| {
AppError::Embedding(
crate::i18n::validation::embedding_openrouter_client_not_initialised(),
)
})?;
let vec = match tokio::runtime::Handle::try_current() {
Ok(handle) => tokio::task::block_in_place(|| {
handle.block_on(client.embed_single(text, client.default_input_type()))
})?,
Err(_) => shared_runtime()?
.block_on(client.embed_single(text, client.default_input_type()))?,
};
Ok((vec, LlmBackendKind::OpenRouter))
}
}
}
pub fn embed_via_backend_strict(
models_dir: &Path,
text: &str,
backend: &LlmBackendKind,
last_err: Option<&AppError>,
skip_on_failure: bool,
) -> Result<(Vec<f32>, LlmBackendKind), AppError> {
use crate::llm::exit_code_hints::LlmBackendError;
match backend {
LlmBackendKind::None => {
if last_err.is_none() || skip_on_failure {
Ok((Vec::new(), LlmBackendKind::None))
} else {
Err(match last_err {
Some(e) => AppError::Embedding(crate::i18n::validation::embedding_detail(e)),
None => AppError::Embedding(crate::i18n::validation::embedding_detail(
LlmBackendError::NoBackendsAvailable,
)),
})
}
}
LlmBackendKind::Codex => embed_passage_local_resolved(models_dir, text),
LlmBackendKind::Claude => {
tracing::debug!(
target: "embedder",
backend = "claude",
"embed_via_backend_strict: forcing claude (ADR-0042 / GAP-002 fix)"
);
embed_via_claude_local_resolved(models_dir, text, None, None)
}
LlmBackendKind::Opencode => {
tracing::debug!(
target: "embedder",
backend = "opencode",
"embed_via_backend_strict: forcing opencode (GAP-OPENCODE-001)"
);
embed_via_opencode_local_resolved(models_dir, text, None, None)
}
LlmBackendKind::OpenRouter => embed_via_backend(models_dir, text, backend),
}
}
pub fn embed_via_backend_legacy(
models_dir: &Path,
text: &str,
backend: &LlmBackendKind,
) -> Result<Vec<f32>, AppError> {
embed_via_backend(models_dir, text, backend).map(|(v, _)| v)
}
pub fn f32_to_bytes(v: &[f32]) -> Vec<u8> {
let mut out = Vec::with_capacity(v.len() * 4);
for f in v {
out.extend_from_slice(&f.to_le_bytes());
}
out
}
pub fn bytes_to_f32(bytes: &[u8]) -> Vec<f32> {
let mut out = Vec::with_capacity(bytes.len() / 4);
for chunk in bytes.chunks_exact(4) {
out.push(f32::from_le_bytes([chunk[0], chunk[1], chunk[2], chunk[3]]));
}
out
}
pub fn embedding_dim() -> usize {
crate::constants::embedding_dim()
}
pub(crate) fn validate_dim(v: Vec<f32>) -> Result<Vec<f32>, AppError> {
let dim = crate::constants::embedding_dim();
if v.len() != dim {
return Err(AppError::Embedding(
crate::i18n::validation::embedding_has_dims_expected(v.len(), dim),
));
}
Ok(v)
}