Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 34 additions & 0 deletions docs/CLI-GUIDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,40 @@ Everything under `<repo>/.leio-code/`:

Secrets in `explain` are redacted unless `--show-secrets` on a TTY.

### Encoder model and width

The encoder is described by two values that travel together, in `[embed]` of
`.leio-code/config.toml` (or `LEIO_CODE_EMBED_MODEL` / `LEIO_CODE_EMBED_DIM`,
which win):

```toml
[embed]
url = "http://localhost:8080/embed"
model = "google/embeddinggemma-2"
dim = 768
```

Defaults are `BAAI/bge-m3` / `1024`, the contract every store written before
`dim` existed used, so leaving `[embed]` alone changes nothing. Any width works:
`embeddinggemma-2` is 768 (Matryoshka cuts: 512/256/128), bge-m3 is 1024.

Three rules hold the contract together:

- **Ingest rejects a mismatch.** A response whose width is not `dim` is an
error on all three endpoint forms, never a stored vector that cannot be
compared. Gemini requests send `dim` as `outputDimensionality`.
- **Rows carry real provenance.** `embed_model` / `embed_dim` on each row are
the configured pair that produced it, not a compiled-in default. Rows that
fell through the graceful-degrade path stay `"none"` / `0`.
- **Search skips incompatible vectors.** Stored vectors of a different width
than the query vector are not scored; those candidates fall back to on-demand
embedding or lexical ranking.

Changing `model` or `dim` makes existing stored vectors incomparable. Re-run
`export arrow-nodes` to rebuild them; a store is not migrated in place. Note
that vectors from two *different* models of the same width are not detected
by the width check alone — do not point one store at two encoders.

## Knowledge wiki

Markdown in the repo compiles to a local Arrow wiki — same idea as
Expand Down
94 changes: 87 additions & 7 deletions src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,11 @@
//! [`PLATFORM_CONTRACTS_ROOT_ENV`], [`PLATFORM_CARTRIDGE_ENV`]) add one
//! private checkout beside the neutral defaults. Unset means only those
//! defaults are recognized.
//! - `embed.url` / `embed.model` → `LEIO_CODE_EMBED_URL` /
//! `LEIO_CODE_EMBED_MODEL` (optional BGE-M3 encoder). Direct TEI URLs end in
//! `/embed`; LiteLLM/OpenAI-compatible bases receive `/v1/embeddings`.
//! - `embed.url` / `embed.model` / `embed.dim` → `LEIO_CODE_EMBED_URL` /
//! `LEIO_CODE_EMBED_MODEL` / `LEIO_CODE_EMBED_DIM` (optional dense encoder).
//! Direct TEI URLs end in `/embed`; LiteLLM/OpenAI-compatible bases receive
//! `/v1/embeddings`. Model and dimension travel together: a store embedded at
//! one pair must not be scored against another.
//!
//! [`repo_namespace`] resolves a stable repo-name slug for cross-graph URN
//! generation; falls back to the working-dir basename.
Expand All @@ -35,12 +37,22 @@ pub const PROFILE_EXAMPLE: &str = "example";
pub const DEFAULT_CODE_RDF_NAMESPACE: &str = "https://example.local/leio/code#";
/// Env var that overrides [`DEFAULT_CODE_RDF_NAMESPACE`] / `[rdf] namespace`.
pub const CODE_RDF_NAMESPACE_ENV: &str = "LEIO_CODE_RDF_NAMESPACE";
/// OpenAI-compatible or direct TEI URL for BGE-M3 (`[embed] url`).
/// OpenAI-compatible or direct TEI URL for the dense encoder (`[embed] url`).
pub const EMBED_URL_ENV: &str = "LEIO_CODE_EMBED_URL";
/// Encoder model id. Defaults to `BAAI/bge-m3`.
/// Encoder model id. Defaults to [`DEFAULT_EMBED_MODEL`].
pub const EMBED_MODEL_ENV: &str = "LEIO_CODE_EMBED_MODEL";
/// Workspace-standard dense model. Must stay 1024-d BGE-M3.
/// Encoder output width. Defaults to [`DEFAULT_EMBED_DIM`].
pub const EMBED_DIM_ENV: &str = "LEIO_CODE_EMBED_DIM";
/// Workspace-standard dense model, kept for stores embedded before
/// `[embed] model` became load-bearing.
pub const DEFAULT_EMBED_MODEL: &str = "BAAI/bge-m3";
/// Dense width of [`DEFAULT_EMBED_MODEL`].
///
/// BGE-M3 emits 1024 floats. Every store written before `[embed] dim` existed
/// is this wide, so changing the default would silently reinterpret them;
/// a different encoder sets `[embed] dim` instead. Other widths are ordinary:
/// `google/embeddinggemma-2` is 768, and its Matryoshka cuts are 512/256/128.
pub const DEFAULT_EMBED_DIM: usize = 1024;

/// Neutral API directory recognized for route mounting and Python modules.
///
Expand Down Expand Up @@ -219,22 +231,38 @@ pub struct OrphanFilesConfig {
pub surfaces: Option<Vec<String>>,
}

/// Remote BGE-M3 encoder under `[embed]` in `.leio-code/config.toml`.
/// Remote dense encoder under `[embed]` in `.leio-code/config.toml`.
///
/// Direct TEI endpoints use the `/embed` suffix; LiteLLM/OpenAI-compatible
/// endpoints may use their base URL and receive `/v1/embeddings` automatically.
///
/// `model` and `dim` describe one encoder and must agree with it: the ingest
/// paths reject any response of a different width rather than store a vector
/// that cannot be compared.
///
/// ```toml
/// [embed]
/// url = "http://tei-bge-m3.example:8080/embed"
/// model = "BAAI/bge-m3"
/// dim = 1024
/// ```
///
/// A locally served 768-d encoder looks the same with its own two values:
///
/// ```toml
/// [embed]
/// url = "http://localhost:8080/embed"
/// model = "google/embeddinggemma-2"
/// dim = 768
/// ```
#[derive(Debug, Clone, Default, Deserialize)]
pub struct EmbedConfig {
/// Direct TEI `/embed` or LiteLLM/OpenAI-compatible base. Env [`EMBED_URL_ENV`] wins.
pub url: Option<String>,
/// Model id posted to `/v1/embeddings`. Defaults to [`DEFAULT_EMBED_MODEL`].
pub model: Option<String>,
/// Encoder output width. Defaults to [`DEFAULT_EMBED_DIM`].
pub dim: Option<usize>,
}

pub fn load_repo_config(root: &Path) -> Option<LeioConfig> {
Expand Down Expand Up @@ -299,6 +327,24 @@ pub fn embed_model(root: &Path) -> String {
.unwrap_or_else(|| DEFAULT_EMBED_MODEL.to_string())
}

/// Encoder width (`LEIO_CODE_EMBED_DIM` / `[embed] dim` / [`DEFAULT_EMBED_DIM`]).
///
/// Pairs with [`embed_model`]: both describe the same encoder, and ingest
/// rejects a response that does not match this width. Zero and unparseable
/// values fall back to the default rather than producing empty vectors.
pub fn embed_dim(root: &Path) -> usize {
std::env::var(EMBED_DIM_ENV)
.ok()
.and_then(|value| value.trim().parse::<usize>().ok())
.filter(|dim| *dim > 0)
.or_else(|| {
load_repo_config(root)
.and_then(|config| config.embed.and_then(|embed| embed.dim))
.filter(|dim| *dim > 0)
})
.unwrap_or(DEFAULT_EMBED_DIM)
}

/// Adds `http://` when the value has no scheme. Empty → `None`.
pub fn normalize_http_base(raw: &str) -> Option<String> {
let trimmed = raw.trim().trim_end_matches('/');
Expand Down Expand Up @@ -683,6 +729,40 @@ mod tests {
}
}

#[test]
fn embed_dim_reads_config_and_defaults() {
let dir = tempfile::tempdir().expect("tempdir");
let cfg_dir = dir.path().join(".leio-code");
std::fs::create_dir_all(&cfg_dir).expect("mkdir");

// No `[embed]` table at all: the BGE-M3 width every older store used.
std::fs::write(cfg_dir.join("config.toml"), "version = 1\n").expect("write");
if std::env::var(EMBED_DIM_ENV).is_err() {
assert_eq!(embed_dim(dir.path()), DEFAULT_EMBED_DIM);
}

// A 768-d encoder is configured per repository, not compiled in.
std::fs::write(
cfg_dir.join("config.toml"),
"version = 1\n[embed]\nmodel = \"google/embeddinggemma-2\"\ndim = 768\n",
)
.expect("write");
if std::env::var(EMBED_DIM_ENV).is_err() {
assert_eq!(embed_dim(dir.path()), 768);
assert_eq!(embed_model(dir.path()), "google/embeddinggemma-2");
}

// Zero would produce empty vectors, so it is treated as unset.
std::fs::write(
cfg_dir.join("config.toml"),
"version = 1\n[embed]\ndim = 0\n",
)
.expect("write");
if std::env::var(EMBED_DIM_ENV).is_err() {
assert_eq!(embed_dim(dir.path()), DEFAULT_EMBED_DIM);
}
}

#[test]
fn code_namespace_defaults_without_override() {
assert_eq!(resolve_code_namespace(None), DEFAULT_CODE_RDF_NAMESPACE);
Expand Down
Loading