feat(embeddings): record the embedding contract and refuse a mismatched config (#189) #217
No files matched your search
@@ -1004,7 +1004,10 @@ tests never touch the process environment. ADR-0003 records the split.
|
|||||||
Sections: `backend`, `unified_search_model`, `[llamacpp]` (`url`, `chat`, `vision`,
|
Sections: `backend`, `unified_search_model`, `[llamacpp]` (`url`, `chat`, `vision`,
|
||||||
`timeout_secs`, `[llamacpp.picker]` `include`/`exclude`), `[ollama]`,
|
`timeout_secs`, `[llamacpp.picker]` `include`/`exclude`), `[ollama]`,
|
||||||
`[openrouter]`, `[embedding]` (`model`, `dim`, `query_prefix`, `document_prefix` —
|
`[openrouter]`, `[embedding]` (`model`, `dim`, `query_prefix`, `document_prefix` —
|
||||||
the corpus contract: change any and run `reembed_embeddings`), `[tts]`
|
the corpus contract: change any and run `reembed_embeddings`; `src/ai/embedding_contract.rs`
|
||||||
|
stores it in `embedding_contract` on first start and refuses every embed, with a startup
|
||||||
|
error naming the changed fields, once the config no longer matches; a full
|
||||||
|
`reembed_embeddings` run records the new one), `[tts]`
|
||||||
(`model`, `voice`, `ref_seconds`, `timeout_secs`), `[agentic]` (`max_iterations`,
|
(`model`, `voice`, `ref_seconds`, `timeout_secs`), `[agentic]` (`max_iterations`,
|
||||||
`chat_max_iterations`, `chat_default_num_ctx`, `chat_turn_timeout_secs`).
|
`chat_max_iterations`, `chat_default_num_ctx`, `chat_turn_timeout_secs`).
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
DROP TABLE embedding_contract;
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
-- The embedding contract the stored vectors were built under (#189): one
|
||||||
|
-- row, written on first start (or by reembed_embeddings), compared with the
|
||||||
|
-- resolved [embedding] config at startup.
|
||||||
|
CREATE TABLE embedding_contract (
|
||||||
|
id INTEGER PRIMARY KEY NOT NULL CHECK (id = 1),
|
||||||
|
backend TEXT NOT NULL,
|
||||||
|
model TEXT NOT NULL,
|
||||||
|
dim INTEGER NOT NULL,
|
||||||
|
query_prefix TEXT NOT NULL,
|
||||||
|
document_prefix TEXT NOT NULL,
|
||||||
|
recorded_at BIGINT NOT NULL
|
||||||
|
);
|
||||||
@@ -0,0 +1,301 @@
|
|||||||
|
//! The embedding contract (#189): what the stored vectors were built with.
|
||||||
|
//!
|
||||||
|
//! Stored vectors are only comparable with query vectors from the same
|
||||||
|
//! backend, model, width and prefixes. Changing `[embedding]` in llm.toml
|
||||||
|
//! (or `backend`) without running `reembed_embeddings` used to break
|
||||||
|
//! similarity search silently; the per-row dim check only caught a change
|
||||||
|
//! of width. The contract is one row in `embedding_contract`:
|
||||||
|
//!
|
||||||
|
//! - **Recorded** on first start (adopting the config the server is running
|
||||||
|
//! with, which is what built the existing corpus) and by a full
|
||||||
|
//! `reembed_embeddings` run, which is the one supported way to change it.
|
||||||
|
//! - **Checked** once per process: at server startup ([`check_at_startup`]),
|
||||||
|
//! or lazily on the first embed in the `src/bin` tools.
|
||||||
|
//! - **Enforced**: on a mismatch every embed fails with a message naming both
|
||||||
|
//! sides and the fix. Mixing vector spaces corrupts the corpus for good (new
|
||||||
|
//! rows can't be told apart from old ones), while refusing only pauses
|
||||||
|
//! semantic search and new embeddings until the config is put back or the
|
||||||
|
//! corpus re-embedded.
|
||||||
|
//!
|
||||||
|
//! `model_version` columns stay as per-row labels; this is the gate.
|
||||||
|
|
||||||
|
use std::sync::OnceLock;
|
||||||
|
use std::sync::atomic::{AtomicBool, Ordering};
|
||||||
|
|
||||||
|
use diesel::prelude::*;
|
||||||
|
use diesel::sqlite::SqliteConnection;
|
||||||
|
|
||||||
|
use crate::ai::config::{LlmConfig, LocalBackend};
|
||||||
|
use crate::ai::ollama::EMBEDDING_MODEL;
|
||||||
|
use crate::database::schema::embedding_contract;
|
||||||
|
|
||||||
|
/// Everything that must agree between the stored vectors and new embeds.
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq, Queryable, Selectable)]
|
||||||
|
#[diesel(table_name = embedding_contract)]
|
||||||
|
pub struct EmbeddingContract {
|
||||||
|
pub backend: String,
|
||||||
|
pub model: String,
|
||||||
|
pub dim: i32,
|
||||||
|
pub query_prefix: String,
|
||||||
|
pub document_prefix: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl EmbeddingContract {
|
||||||
|
/// The contract the resolved llm.toml implies.
|
||||||
|
pub fn current() -> Self {
|
||||||
|
Self::from_config(crate::ai::config::llm_config())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn from_config(cfg: &LlmConfig) -> Self {
|
||||||
|
let (backend, model) = match cfg.backend {
|
||||||
|
LocalBackend::Llamacpp => ("llamacpp", cfg.embedding.model.clone()),
|
||||||
|
// Ollama always embeds with its fixed model (`embedding.model`
|
||||||
|
// names a llama-swap id and is ignored there).
|
||||||
|
LocalBackend::Ollama => ("ollama", EMBEDDING_MODEL.to_string()),
|
||||||
|
};
|
||||||
|
Self {
|
||||||
|
backend: backend.into(),
|
||||||
|
model,
|
||||||
|
dim: cfg.embedding.dim as i32,
|
||||||
|
query_prefix: cfg.embedding.query_prefix.clone(),
|
||||||
|
document_prefix: cfg.embedding.document_prefix.clone(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The fields that differ, as `name: stored → current` (prefixes quoted,
|
||||||
|
/// since a trailing space or newline is the usual difference).
|
||||||
|
pub fn differences(&self, current: &Self) -> Vec<String> {
|
||||||
|
let mut out = Vec::new();
|
||||||
|
if self.backend != current.backend {
|
||||||
|
out.push(format!("backend: {} → {}", self.backend, current.backend));
|
||||||
|
}
|
||||||
|
if self.model != current.model {
|
||||||
|
out.push(format!("model: {} → {}", self.model, current.model));
|
||||||
|
}
|
||||||
|
if self.dim != current.dim {
|
||||||
|
out.push(format!("dim: {} → {}", self.dim, current.dim));
|
||||||
|
}
|
||||||
|
if self.query_prefix != current.query_prefix {
|
||||||
|
out.push(format!(
|
||||||
|
"query_prefix: {:?} → {:?}",
|
||||||
|
self.query_prefix, current.query_prefix
|
||||||
|
));
|
||||||
|
}
|
||||||
|
if self.document_prefix != current.document_prefix {
|
||||||
|
out.push(format!(
|
||||||
|
"document_prefix: {:?} → {:?}",
|
||||||
|
self.document_prefix, current.document_prefix
|
||||||
|
));
|
||||||
|
}
|
||||||
|
out
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The stored contract, if one was recorded.
|
||||||
|
pub fn load(conn: &mut SqliteConnection) -> QueryResult<Option<EmbeddingContract>> {
|
||||||
|
embedding_contract::table
|
||||||
|
.select(EmbeddingContract::as_select())
|
||||||
|
.first(conn)
|
||||||
|
.optional()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Record `contract` as the one the stored vectors follow (the single row).
|
||||||
|
pub fn record(conn: &mut SqliteConnection, contract: &EmbeddingContract) -> QueryResult<()> {
|
||||||
|
use embedding_contract::dsl as c;
|
||||||
|
let values = (
|
||||||
|
c::backend.eq(&contract.backend),
|
||||||
|
c::model.eq(&contract.model),
|
||||||
|
c::dim.eq(contract.dim),
|
||||||
|
c::query_prefix.eq(&contract.query_prefix),
|
||||||
|
c::document_prefix.eq(&contract.document_prefix),
|
||||||
|
c::recorded_at.eq(chrono::Utc::now().timestamp()),
|
||||||
|
);
|
||||||
|
diesel::insert_into(c::embedding_contract)
|
||||||
|
.values((c::id.eq(1), values))
|
||||||
|
.on_conflict(c::id)
|
||||||
|
.do_update()
|
||||||
|
.set(values)
|
||||||
|
.execute(conn)
|
||||||
|
.map(|_| ())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, PartialEq, Eq)]
|
||||||
|
pub enum Verdict {
|
||||||
|
/// The stored contract matches the config.
|
||||||
|
Matches,
|
||||||
|
/// Nothing was stored; the config's contract was recorded.
|
||||||
|
Recorded,
|
||||||
|
/// The config no longer matches the stored vectors.
|
||||||
|
Mismatch(Vec<String>),
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Compare `current` with the stored contract, recording it when none is.
|
||||||
|
pub fn check(conn: &mut SqliteConnection, current: &EmbeddingContract) -> QueryResult<Verdict> {
|
||||||
|
match load(conn)? {
|
||||||
|
None => {
|
||||||
|
record(conn, current)?;
|
||||||
|
Ok(Verdict::Recorded)
|
||||||
|
}
|
||||||
|
Some(stored) if stored == *current => Ok(Verdict::Matches),
|
||||||
|
Some(stored) => Ok(Verdict::Mismatch(stored.differences(current))),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// `Err(message)` when embeds must be refused; set once per process.
|
||||||
|
static STATE: OnceLock<Result<(), String>> = OnceLock::new();
|
||||||
|
static BYPASS: AtomicBool = AtomicBool::new(false);
|
||||||
|
|
||||||
|
fn mismatch_message(diffs: &[String]) -> String {
|
||||||
|
format!(
|
||||||
|
"The [embedding] config doesn't match the stored vectors ({}). Embedding is \
|
||||||
|
refused so the corpus doesn't mix vector spaces. Run `reembed_embeddings` to \
|
||||||
|
rebuild them under the new config (it records it when it finishes), or put \
|
||||||
|
the old settings back in llm.toml.",
|
||||||
|
diffs.join("; ")
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Run the check and turn it into the process state, logging the outcome.
|
||||||
|
/// A DB error leaves embedding allowed: the guard must not take search down
|
||||||
|
/// over its own bookkeeping.
|
||||||
|
fn evaluate(conn: &mut SqliteConnection) -> Result<(), String> {
|
||||||
|
let current = EmbeddingContract::current();
|
||||||
|
match check(conn, ¤t) {
|
||||||
|
Ok(Verdict::Matches) => Ok(()),
|
||||||
|
Ok(Verdict::Recorded) => {
|
||||||
|
log::info!(
|
||||||
|
"Embedding contract recorded: backend={} model={} dim={} (stored vectors \
|
||||||
|
are assumed to follow it; reembed_embeddings if they don't)",
|
||||||
|
current.backend,
|
||||||
|
current.model,
|
||||||
|
current.dim
|
||||||
|
);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
Ok(Verdict::Mismatch(diffs)) => {
|
||||||
|
let message = mismatch_message(&diffs);
|
||||||
|
log::error!("{}", message);
|
||||||
|
Err(message)
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
log::warn!("Embedding contract check skipped: {}", e);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Server startup, after migrations: check the contract once and log it.
|
||||||
|
pub fn check_at_startup(conn: &mut SqliteConnection) {
|
||||||
|
let _ = STATE.get_or_init(|| evaluate(conn));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Called before every embed. Fails when the config doesn't match the stored
|
||||||
|
/// vectors. Processes that skipped [`check_at_startup`] (the `src/bin`
|
||||||
|
/// tools) check here on first use.
|
||||||
|
pub fn ensure() -> anyhow::Result<()> {
|
||||||
|
if BYPASS.load(Ordering::Relaxed) {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
let state = STATE.get_or_init(|| {
|
||||||
|
let Ok(url) = dotenv::var("DATABASE_URL") else {
|
||||||
|
return Ok(());
|
||||||
|
};
|
||||||
|
match SqliteConnection::establish(&url) {
|
||||||
|
Ok(mut conn) => evaluate(&mut conn),
|
||||||
|
Err(e) => {
|
||||||
|
log::warn!("Embedding contract check skipped: {}", e);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
state.clone().map_err(anyhow::Error::msg)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// For `reembed_embeddings` only: embed under the new config even though the
|
||||||
|
/// stored vectors don't follow it yet (that's what it's there to fix).
|
||||||
|
pub fn bypass() {
|
||||||
|
BYPASS.store(true, Ordering::Relaxed);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use crate::ai::config::EmbeddingConfig;
|
||||||
|
use crate::database::test::in_memory_db_connection;
|
||||||
|
|
||||||
|
fn llamacpp(model: &str, dim: usize, query: &str, doc: &str) -> EmbeddingContract {
|
||||||
|
let cfg = LlmConfig {
|
||||||
|
backend: LocalBackend::Llamacpp,
|
||||||
|
embedding: EmbeddingConfig {
|
||||||
|
model: model.into(),
|
||||||
|
dim,
|
||||||
|
query_prefix: query.into(),
|
||||||
|
document_prefix: doc.into(),
|
||||||
|
},
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
EmbeddingContract::from_config(&cfg)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn records_the_first_contract_then_matches_it() {
|
||||||
|
let mut conn = in_memory_db_connection();
|
||||||
|
let c = llamacpp("embed", 768, "search_query: ", "search_document: ");
|
||||||
|
assert_eq!(check(&mut conn, &c).unwrap(), Verdict::Recorded);
|
||||||
|
assert_eq!(load(&mut conn).unwrap(), Some(c.clone()));
|
||||||
|
assert_eq!(check(&mut conn, &c).unwrap(), Verdict::Matches);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn a_same_width_model_or_prefix_swap_is_a_mismatch() {
|
||||||
|
let mut conn = in_memory_db_connection();
|
||||||
|
let stored = llamacpp("embed", 768, "search_query: ", "search_document: ");
|
||||||
|
record(&mut conn, &stored).unwrap();
|
||||||
|
|
||||||
|
let swapped = llamacpp("other-embed", 768, "search_query: ", "");
|
||||||
|
let Verdict::Mismatch(diffs) = check(&mut conn, &swapped).unwrap() else {
|
||||||
|
panic!("expected a mismatch");
|
||||||
|
};
|
||||||
|
assert_eq!(
|
||||||
|
diffs,
|
||||||
|
vec![
|
||||||
|
"model: embed → other-embed".to_string(),
|
||||||
|
"document_prefix: \"search_document: \" → \"\"".to_string(),
|
||||||
|
]
|
||||||
|
);
|
||||||
|
// A mismatch never overwrites the stored contract.
|
||||||
|
assert_eq!(load(&mut conn).unwrap(), Some(stored));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn record_replaces_the_single_row() {
|
||||||
|
let mut conn = in_memory_db_connection();
|
||||||
|
record(&mut conn, &llamacpp("embed", 768, "", "")).unwrap();
|
||||||
|
let qwen = llamacpp("qwen3-embed", 1024, "Instruct: find\nQuery: ", "");
|
||||||
|
record(&mut conn, &qwen).unwrap();
|
||||||
|
assert_eq!(load(&mut conn).unwrap(), Some(qwen.clone()));
|
||||||
|
assert_eq!(check(&mut conn, &qwen).unwrap(), Verdict::Matches);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn ollama_uses_its_fixed_model_whatever_embedding_model_says() {
|
||||||
|
let cfg = LlmConfig {
|
||||||
|
backend: LocalBackend::Ollama,
|
||||||
|
embedding: EmbeddingConfig {
|
||||||
|
model: "ignored".into(),
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
let c = EmbeddingContract::from_config(&cfg);
|
||||||
|
assert_eq!(c.backend, "ollama");
|
||||||
|
assert_eq!(c.model, EMBEDDING_MODEL);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn the_mismatch_message_names_the_fix() {
|
||||||
|
let m = mismatch_message(&["dim: 768 → 1024".into()]);
|
||||||
|
assert!(m.contains("dim: 768 → 1024"));
|
||||||
|
assert!(m.contains("reembed_embeddings"));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -4,6 +4,7 @@ pub mod clip_client;
|
|||||||
pub mod config;
|
pub mod config;
|
||||||
pub mod daily_summary_job;
|
pub mod daily_summary_job;
|
||||||
pub mod date_info;
|
pub mod date_info;
|
||||||
|
pub mod embedding_contract;
|
||||||
pub mod face_client;
|
pub mod face_client;
|
||||||
pub mod gpu;
|
pub mod gpu;
|
||||||
pub mod handlers;
|
pub mod handlers;
|
||||||
@@ -122,6 +123,8 @@ pub async fn embed_many(
|
|||||||
llamacpp: Option<&LlamaCppClient>,
|
llamacpp: Option<&LlamaCppClient>,
|
||||||
texts: &[&str],
|
texts: &[&str],
|
||||||
) -> anyhow::Result<Vec<Vec<f32>>> {
|
) -> anyhow::Result<Vec<Vec<f32>>> {
|
||||||
|
// Refuse to embed under a config the stored vectors weren't built with (#189).
|
||||||
|
embedding_contract::ensure()?;
|
||||||
if local_backend_is_llamacpp() {
|
if local_backend_is_llamacpp() {
|
||||||
if let Some(lc) = llamacpp {
|
if let Some(lc) = llamacpp {
|
||||||
return <LlamaCppClient as LlmClient>::generate_embeddings(lc, texts).await;
|
return <LlamaCppClient as LlmClient>::generate_embeddings(lc, texts).await;
|
||||||
|
|||||||
@@ -25,10 +25,13 @@ use clap::Parser;
|
|||||||
use diesel::prelude::*;
|
use diesel::prelude::*;
|
||||||
use diesel::sql_query;
|
use diesel::sql_query;
|
||||||
use diesel::sqlite::SqliteConnection;
|
use diesel::sqlite::SqliteConnection;
|
||||||
|
use image_api::ai::embedding_contract::{self, EmbeddingContract};
|
||||||
use image_api::ai::{LocalLlm, strip_summary_boilerplate};
|
use image_api::ai::{LocalLlm, strip_summary_boilerplate};
|
||||||
use image_api::bin_progress;
|
use image_api::bin_progress;
|
||||||
use std::env;
|
use std::env;
|
||||||
|
|
||||||
|
const ALL_TABLES: [&str; 4] = ["summaries", "calendar", "search", "entities"];
|
||||||
|
|
||||||
#[derive(Parser, Debug)]
|
#[derive(Parser, Debug)]
|
||||||
#[command(author, version, about = "Re-embed stored corpora via the configured local backend", long_about = None)]
|
#[command(author, version, about = "Re-embed stored corpora via the configured local backend", long_about = None)]
|
||||||
struct Args {
|
struct Args {
|
||||||
@@ -284,6 +287,21 @@ async fn main() -> Result<()> {
|
|||||||
let mut conn = SqliteConnection::establish(&database_url)
|
let mut conn = SqliteConnection::establish(&database_url)
|
||||||
.with_context(|| format!("connecting to {}", database_url))?;
|
.with_context(|| format!("connecting to {}", database_url))?;
|
||||||
|
|
||||||
|
// This tool exists to move the corpus to the configured contract, so it
|
||||||
|
// embeds even while the stored one differs (#189).
|
||||||
|
embedding_contract::bypass();
|
||||||
|
let current = EmbeddingContract::current();
|
||||||
|
match embedding_contract::load(&mut conn) {
|
||||||
|
Ok(Some(stored)) if stored != current => {
|
||||||
|
println!(
|
||||||
|
"Stored embedding contract differs: {}",
|
||||||
|
stored.differences(¤t).join("; ")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
Ok(_) => {}
|
||||||
|
Err(e) => println!("Couldn't read the stored embedding contract: {}", e),
|
||||||
|
}
|
||||||
|
|
||||||
let llm = LocalLlm::from_config();
|
let llm = LocalLlm::from_config();
|
||||||
let model_version = llm.embedding_model_version();
|
let model_version = llm.embedding_model_version();
|
||||||
println!("Embedding via '{}'", model_version);
|
println!("Embedding via '{}'", model_version);
|
||||||
@@ -453,6 +471,25 @@ async fn main() -> Result<()> {
|
|||||||
report_similarity("entities", sims);
|
report_similarity("entities", sims);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Every table re-embedded in full: the corpus now follows the configured
|
||||||
|
// contract, so record it and the server stops refusing embeds.
|
||||||
|
let full_run =
|
||||||
|
!args.dry_run && args.limit.is_none() && ALL_TABLES.iter().all(|t| tables.contains(t));
|
||||||
|
if full_run {
|
||||||
|
embedding_contract::record(&mut conn, ¤t)
|
||||||
|
.context("recording the embedding contract")?;
|
||||||
|
println!(
|
||||||
|
"Recorded the embedding contract: backend={} model={} dim={}",
|
||||||
|
current.backend, current.model, current.dim
|
||||||
|
);
|
||||||
|
} else if !args.dry_run {
|
||||||
|
println!(
|
||||||
|
"Embedding contract not recorded: only a full run (all of {}, no --limit) \
|
||||||
|
leaves the whole corpus on the new config",
|
||||||
|
ALL_TABLES.join(", ")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
println!(
|
println!(
|
||||||
"\n{}",
|
"\n{}",
|
||||||
if args.dry_run {
|
if args.dry_run {
|
||||||
|
|||||||
@@ -41,6 +41,18 @@ diesel::table! {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
diesel::table! {
|
||||||
|
embedding_contract (id) {
|
||||||
|
id -> Integer,
|
||||||
|
backend -> Text,
|
||||||
|
model -> Text,
|
||||||
|
dim -> Integer,
|
||||||
|
query_prefix -> Text,
|
||||||
|
document_prefix -> Text,
|
||||||
|
recorded_at -> BigInt,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
diesel::table! {
|
diesel::table! {
|
||||||
enhance_jobs (id) {
|
enhance_jobs (id) {
|
||||||
id -> Text,
|
id -> Text,
|
||||||
@@ -429,6 +441,7 @@ diesel::allow_tables_to_appear_in_same_query!(
|
|||||||
calendar_events,
|
calendar_events,
|
||||||
clip_failures,
|
clip_failures,
|
||||||
daily_conversation_summaries,
|
daily_conversation_summaries,
|
||||||
|
embedding_contract,
|
||||||
enhance_jobs,
|
enhance_jobs,
|
||||||
entities,
|
entities,
|
||||||
entity_facts,
|
entity_facts,
|
||||||
|
|||||||
@@ -69,6 +69,9 @@ fn main() -> std::io::Result<()> {
|
|||||||
// Load llm.toml (+ legacy env overrides) now, after logging is up, so
|
// Load llm.toml (+ legacy env overrides) now, after logging is up, so
|
||||||
// a bad file fails startup and its deprecation warnings are visible.
|
// a bad file fails startup and its deprecation warnings are visible.
|
||||||
ai::config::llm_config();
|
ai::config::llm_config();
|
||||||
|
// Stored vectors vs the [embedding] config (#189): records it on first
|
||||||
|
// start, refuses embeds (and logs why) on a mismatch.
|
||||||
|
ai::embedding_contract::check_at_startup(&mut connect());
|
||||||
|
|
||||||
// AppState construction loads (and seeds if needed) the libraries
|
// AppState construction loads (and seeds if needed) the libraries
|
||||||
// table; we use that list to drive the initial thumbnail sweep.
|
// table; we use that list to drive the initial thumbnail sweep.
|
||||||
|
|||||||
Reference in new issue
Block a user