mod binary;
mod builder;
mod models;
mod ops;
mod timeout;
mod types;
mod wire;
pub use binary::resolve_real_binary;
pub use builder::LlmEmbeddingBuilder;
pub use types::EmbeddingFlavour;
use crate::errors::AppError;
use models::{claude_embed_model, codex_embed_model, opencode_embed_model};
use std::sync::Arc;
use timeout::embed_timeout;
use types::CodexSchemaFiles;
#[derive(Clone, Debug)]
pub struct LlmEmbedding {
pub(crate) flavour: EmbeddingFlavour,
pub(crate) binary: std::path::PathBuf,
pub(crate) model: String,
pub(crate) codex_schemas: Arc<parking_lot::Mutex<CodexSchemaFiles>>,
pub(crate) timeout_override: Option<std::time::Duration>,
}
impl LlmEmbedding {
pub fn with_timeout_secs(mut self, secs: u64) -> Self {
let clamped = secs.clamp(1, 3_600);
self.timeout_override = Some(std::time::Duration::from_secs(clamped));
self
}
pub fn detect_available() -> Result<Self, AppError> {
Self::oauth_only_enforce()?;
if let Some(path) = crate::runtime_config::codex_binary()
.map(std::path::PathBuf::from)
.or_else(|| which::which("codex").ok())
{
return Ok(Self::from_parts(
EmbeddingFlavour::Codex,
resolve_real_binary(&path),
codex_embed_model(),
None,
));
}
if let Some(path) = crate::runtime_config::claude_binary()
.map(std::path::PathBuf::from)
.or_else(|| which::which("claude").ok())
{
return Ok(Self::from_parts(
EmbeddingFlavour::Claude,
resolve_real_binary(&path),
claude_embed_model(),
None,
));
}
if let Some(path) = crate::runtime_config::opencode_binary()
.map(std::path::PathBuf::from)
.or_else(|| which::which("opencode").ok())
{
return Ok(Self::from_parts(
EmbeddingFlavour::Opencode,
resolve_real_binary(&path),
opencode_embed_model(),
None,
));
}
Err(AppError::Embedding(
crate::i18n::validation::embedding_no_llm_cli_on_path(),
))
}
pub(crate) fn from_parts(
flavour: EmbeddingFlavour,
binary: std::path::PathBuf,
model: String,
timeout_override: Option<std::time::Duration>,
) -> Self {
Self {
flavour,
binary,
model,
codex_schemas: Arc::new(parking_lot::Mutex::new(CodexSchemaFiles::default())),
timeout_override,
}
}
pub(crate) fn instance_embed_timeout(&self) -> std::time::Duration {
if let Some(d) = self.timeout_override {
return d;
}
embed_timeout()
}
pub(crate) fn instance_embed_timeout_for_batch(&self, batch_size: usize) -> std::time::Duration {
let base = self.instance_embed_timeout();
let extra = std::time::Duration::from_secs(15) * batch_size.saturating_sub(1) as u32;
base + extra
}
pub fn with_codex() -> Result<Self, AppError> {
Self::with_codex_builder().build()
}
pub fn with_claude() -> Result<Self, AppError> {
Self::with_claude_builder().build()
}
pub fn with_codex_builder() -> LlmEmbeddingBuilder {
LlmEmbeddingBuilder::codex_default()
}
pub fn with_claude_builder() -> LlmEmbeddingBuilder {
LlmEmbeddingBuilder::claude_default()
}
pub fn with_opencode() -> Result<Self, AppError> {
Self::with_opencode_builder().build()
}
pub fn with_opencode_builder() -> LlmEmbeddingBuilder {
LlmEmbeddingBuilder::opencode_default()
}
pub(crate) fn oauth_only_enforce() -> Result<(), AppError> {
if std::env::var("ANTHROPIC_API_KEY").is_ok() {
return Err(AppError::Validation(
crate::i18n::validation::anthropic_api_key_oauth_required(),
));
}
if std::env::var("OPENAI_API_KEY").is_ok() {
return Err(AppError::Validation(
crate::i18n::validation::openai_api_key_oauth_required(),
));
}
Ok(())
}
pub fn embed_passage(&self, text: &str) -> Result<Vec<f32>, AppError> {
self.invoke_with_prefix(crate::constants::PASSAGE_PREFIX, text)
}
pub fn embed_query(&self, text: &str) -> Result<Vec<f32>, AppError> {
self.invoke_with_prefix(crate::constants::QUERY_PREFIX, text)
}
pub fn model_label(&self) -> String {
format!("{}:{}", self.flavour.as_str(), self.model)
}
pub fn flavour(&self) -> EmbeddingFlavour {
self.flavour
}
}
#[cfg(test)]
mod tests;