use super::*;
const LOCAL_ONLY_EMBEDDING_POLICY_VERSION: &str = "context_bundle_local_embedding_v1";
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
#[serde(rename_all = "snake_case")]
enum LocalOnlyEmbeddingMode {
Local,
FeatureHash,
Skipped,
Blocked,
}
#[derive(Serialize)]
struct LocalOnlyEmbeddingProfile<'a> {
policy_version: &'static str,
mode: LocalOnlyEmbeddingMode,
active_provider: &'a str,
active_model_id: Option<&'a str>,
active_dimensions: Option<usize>,
}
fn local_only_embedding_mode(
config: &EmbeddingConfig,
status: &EmbeddingProviderStatus,
) -> LocalOnlyEmbeddingMode {
if status.active_provider == EmbeddingProvider::OpenAi.label()
|| (status.active_provider == EmbeddingProvider::Off.label()
&& matches!(
config.provider,
EmbeddingProvider::OpenAi | EmbeddingProvider::Off
))
{
return LocalOnlyEmbeddingMode::Skipped;
}
match status.active_provider.as_str() {
"local" if status.unavailable_reason.is_none() => LocalOnlyEmbeddingMode::Local,
"feature-hash" if status.unavailable_reason.is_none() => {
LocalOnlyEmbeddingMode::FeatureHash
}
_ => LocalOnlyEmbeddingMode::Blocked,
}
}
pub(crate) fn local_only_embedding_profile_fingerprint() -> String {
#[cfg(test)]
let _test_env_guard = config::lock_test_env();
let canonical = match resolve_embedding_config() {
Ok(config) => {
let status = status::resolve_provider_status(&config);
serde_json::to_vec(&LocalOnlyEmbeddingProfile {
policy_version: LOCAL_ONLY_EMBEDDING_POLICY_VERSION,
mode: local_only_embedding_mode(&config, &status),
active_provider: &status.active_provider,
active_model_id: status.active_model_id.as_deref(),
active_dimensions: status.active_dimensions,
})
.expect("local-only embedding profile serialization is infallible")
}
Err(error) => serde_json::to_vec(&(
LOCAL_ONLY_EMBEDDING_POLICY_VERSION,
LocalOnlyEmbeddingMode::Blocked,
error.to_string(),
))
.expect("local-only embedding error profile serialization is infallible"),
};
let mut hasher = Sha256::new();
hasher.update(canonical);
format!("{:x}", hasher.finalize())
}
pub(crate) fn embed_query_if_enabled(query: &str) -> Result<Option<TextEmbedding>> {
match embed_query(query) {
Ok(embedding) => Ok(Some(embedding)),
Err(error) if is_embedding_provider_off_error(&error) => Ok(None),
Err(error) => Err(error),
}
}
pub(crate) fn embed_query_local_only_if_enabled(query: &str) -> Result<Option<TextEmbedding>> {
#[cfg(test)]
let _test_env_guard = config::lock_test_env();
let config = resolve_embedding_config()?;
let mut cache = EmbeddingFallbackCache::default();
let status = status::resolve_provider_status(&config);
match local_only_embedding_mode(&config, &status) {
LocalOnlyEmbeddingMode::Local => Ok(Some(fallback::embed_local_with_auto_race_fallback(
query,
LocalEmbeddingInputKind::Query,
&config,
&mut cache,
)?)),
LocalOnlyEmbeddingMode::FeatureHash => Ok(Some(TextEmbedding::new(
FEATURE_HASH_EMBEDDING_MODEL,
embed_text_local(query),
)?)),
LocalOnlyEmbeddingMode::Skipped => {
Ok(None)
}
LocalOnlyEmbeddingMode::Blocked => {
let _ = active_provider(&config)?;
unreachable!("blocked local-only embedding profile must fail provider activation")
}
}
}