use aphrody_config::{EmbedBackend, RagConfig, ai_env};
use aphrody_models::{Catalog, LocalRole};
use crate::error::{RagError, Result};
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ResolvedEmbedConfig {
pub backend: EmbedBackend,
pub model: String,
pub dimension: usize,
pub endpoint: Option<String>,
pub api_key: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ResolvedRerankConfig {
pub model: String,
pub endpoint: String,
pub api_key: Option<String>,
}
type EnvReader<'a> = dyn Fn(&str) -> Option<String> + 'a;
fn std_env(name: &str) -> Option<String> {
std::env::var(name).ok()
}
fn api_key_from(env: &EnvReader<'_>) -> Option<String> {
env("APHRODY_RAG_API_KEY").filter(|value| !value.trim().is_empty())
}
fn default_embed_model(backend: EmbedBackend) -> &'static str {
match backend {
EmbedBackend::Native => "bge-small-en-v1.5",
EmbedBackend::Ort => default_ort_model(),
EmbedBackend::LlamaServer | EmbedBackend::OpenaiCompat => LocalRole::Embed.default_model(),
}
}
#[cfg(feature = "ort")]
fn default_ort_model() -> &'static str {
aphrody_embed::EmbeddingModelKind::default().as_str()
}
#[cfg(not(feature = "ort"))]
fn default_ort_model() -> &'static str {
"bge-small-en-v1.5"
}
fn resolve_embed_model(
backend: EmbedBackend,
configured: Option<&str>,
env: &EnvReader<'_>,
) -> String {
ai_env::EMBED_MODEL
.resolve_with(env)
.or_else(|| {
configured
.filter(|value| !value.trim().is_empty())
.map(str::to_owned)
})
.unwrap_or_else(|| default_embed_model(backend).to_owned())
}
fn resolve_rerank_model(configured: Option<&str>, env: &EnvReader<'_>) -> String {
ai_env::RERANK_MODEL
.resolve_with(env)
.or_else(|| {
configured
.filter(|value| !value.trim().is_empty())
.map(str::to_owned)
})
.unwrap_or_else(|| LocalRole::Rerank.default_model().to_owned())
}
fn resolve_dimension(model: &str, configured: Option<usize>, env: &EnvReader<'_>) -> Result<usize> {
let env_dim = match ai_env::EMBED_DIMENSIONS.resolve_source_with(env) {
Some(env_var) => {
let raw = ai_env::EMBED_DIMENSIONS
.resolve_with(env)
.unwrap_or_default();
let value = raw
.parse::<usize>()
.map_err(|source| RagError::InvalidEmbedDimension {
env_var,
raw: raw.clone(),
source,
})?;
Some((env_var, value))
}
None => None,
};
let catalog_dim = Catalog::builtin()
.dimensions(model)
.ok()
.flatten()
.map(|dim| dim as usize);
match (env_dim, catalog_dim) {
(Some((env_var, env_dim)), Some(catalog_dim)) if env_dim != catalog_dim => {
Err(RagError::EmbedDimensionMismatch {
env_var,
env_dim,
model: model.to_owned(),
catalog_dim,
})
}
(Some((_, env_dim)), _) => Ok(env_dim),
(None, Some(catalog_dim)) => Ok(catalog_dim),
(None, None) => configured.ok_or_else(|| RagError::UnknownEmbedDimension {
model: model.to_owned(),
}),
}
}
fn resolve_embed_with(
config: &RagConfig,
env: &EnvReader<'_>,
) -> Result<Option<ResolvedEmbedConfig>> {
let env_endpoint = ai_env::EMBED_BASE_URL.resolve_with(env);
let Some(backend) = config
.embed_backend
.or_else(|| env_endpoint.is_some().then_some(EmbedBackend::LlamaServer))
else {
return Ok(None);
};
let model = resolve_embed_model(backend, config.embed_model.as_deref(), env);
let dimension = resolve_dimension(&model, config.vector_dim, env)?;
let endpoint = match backend {
EmbedBackend::Native | EmbedBackend::Ort => None,
EmbedBackend::LlamaServer | EmbedBackend::OpenaiCompat => {
let endpoint = env_endpoint
.or_else(|| {
config
.embed_endpoint
.as_deref()
.filter(|v| !v.trim().is_empty())
.map(str::to_owned)
})
.ok_or(RagError::MissingEmbedEndpoint { backend })?;
Some(endpoint)
}
};
Ok(Some(ResolvedEmbedConfig {
backend,
model,
dimension,
endpoint,
api_key: api_key_from(env),
}))
}
fn resolve_rerank_with(
config: &RagConfig,
enabled: bool,
env: &EnvReader<'_>,
) -> Result<Option<ResolvedRerankConfig>> {
if !enabled {
return Ok(None);
}
let model = resolve_rerank_model(config.rerank_model.as_deref(), env);
let endpoint = ai_env::RERANK_BASE_URL
.resolve_with(env)
.or_else(|| {
config
.rerank_endpoint
.as_deref()
.filter(|v| !v.trim().is_empty())
.map(str::to_owned)
})
.ok_or(RagError::MissingRerankEndpoint)?;
Ok(Some(ResolvedRerankConfig {
model,
endpoint,
api_key: api_key_from(env),
}))
}
pub fn resolve_embed(config: &RagConfig) -> Result<Option<ResolvedEmbedConfig>> {
resolve_embed_with(config, &std_env)
}
#[must_use]
pub fn rerank_available(config: &RagConfig) -> bool {
rerank_available_with(config, &std_env)
}
fn rerank_available_with(config: &RagConfig, env: &EnvReader<'_>) -> bool {
ai_env::RERANK_BASE_URL.resolve_with(env).is_some()
|| config
.rerank_endpoint
.as_deref()
.is_some_and(|value| !value.trim().is_empty())
}
pub fn resolve_rerank(config: &RagConfig, enabled: bool) -> Result<Option<ResolvedRerankConfig>> {
resolve_rerank_with(config, enabled, &std_env)
}
#[cfg(test)]
#[path = "config_resolve_tests.rs"]
mod config_resolve_tests;