pub struct QueryEmbedding {
pub embedding: Option<Vec<f32>>,
pub degraded: bool,
pub error: Option<String>,
pub backend_invoked: Option<&'static str>,
pub reason_code: Option<&'static str>,
}
pub const FALLBACK_FTS_ONLY_REASON: &str = "fallback_fts_only requested";
pub const FALLBACK_FTS_ONLY_CODE: &str = "fallback_fts_only";
pub fn degradation_failure(
fail_on_degraded: bool,
vec_degraded: bool,
reason_code: Option<&str>,
) -> Option<crate::errors::AppError> {
if !fail_on_degraded || !vec_degraded {
return None;
}
let code = reason_code.unwrap_or("unknown");
if code == FALLBACK_FTS_ONLY_CODE {
return None;
}
let detail = format!("query embedding degraded to FTS5-only ({code})");
match code {
"timeout" | "slot_exhausted" | "oauth_quota" | "cancelled" => {
Some(crate::errors::AppError::Timeout {
operation: detail,
duration_secs: 0,
})
}
_ => Some(crate::errors::AppError::Embedding(detail)),
}
}
pub fn resolve_query_embedding(
fallback_fts_only: bool,
models_dir: &std::path::Path,
query: &str,
backends: crate::cli::BackendChoice,
log_target: &'static str,
) -> QueryEmbedding {
if fallback_fts_only {
return QueryEmbedding {
embedding: None,
degraded: true,
error: Some(FALLBACK_FTS_ONLY_REASON.to_string()),
backend_invoked: None,
reason_code: Some(FALLBACK_FTS_ONLY_CODE),
};
}
match crate::embedder::try_embed_query_with_embedding_choice(models_dir, query, backends) {
Ok((v, backend)) => QueryEmbedding {
embedding: Some(v),
degraded: false,
error: None,
backend_invoked: Some(backend.as_str()),
reason_code: None,
},
Err(reason) => {
let msg = reason.to_string();
let code = reason.reason_code();
tracing::warn!(
target: "query_embedding",
command = log_target,
fallback_reason = %msg,
reason_code = %code,
"live embedding failed; falling back to FTS5"
);
QueryEmbedding {
embedding: None,
degraded: true,
error: Some(msg),
backend_invoked: None,
reason_code: Some(code),
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn opting_out_reports_the_named_reason_and_never_names_a_backend() {
let resolved = resolve_query_embedding(
true,
std::path::Path::new("/nonexistent"),
"anything",
crate::cli::BackendChoice::new(
crate::cli::LlmBackendChoice::None,
crate::cli::EmbeddingBackendChoice::Openrouter,
),
"recall",
);
assert!(
resolved.embedding.is_none(),
"opting out must not produce a vector"
);
assert!(
resolved.degraded,
"opting out is still a degradation for the caller"
);
assert_eq!(resolved.error.as_deref(), Some(FALLBACK_FTS_ONLY_REASON));
assert!(
resolved.backend_invoked.is_none(),
"no provider was contacted, so none may be reported as invoked"
);
assert_eq!(
resolved.reason_code,
Some(FALLBACK_FTS_ONLY_CODE),
"opting out must carry its own code, not an absent one"
);
}
#[test]
fn opting_out_survives_fail_on_degraded() {
let resolved = resolve_query_embedding(
true,
std::path::Path::new("/nonexistent"),
"anything",
crate::cli::BackendChoice::new(
crate::cli::LlmBackendChoice::None,
crate::cli::EmbeddingBackendChoice::Openrouter,
),
"recall",
);
assert!(
degradation_failure(true, resolved.degraded, resolved.reason_code).is_none(),
"--fallback-fts-only com --fail-on-degraded deve continuar saindo 0"
);
}
}