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
2 changes: 1 addition & 1 deletion .agents/skills/uteke-memory/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ description: "Persistent memory engine for AI agents via the uteke CLI — remem
# Uteke Memory Skill

Persistent memory engine for AI agents via the `uteke` CLI.
Version: **0.20.0** — SQLite + HNSW vector index (usearch default, or vecq) + FTS5 hybrid search (RRF k=60); `fusion` (weighted RRF of the vector and hybrid rankings) is the default recall strategy since 0.16.0. `unsafe_code = "forbid"` workspace-wide.
Version: **0.20.1** — SQLite + HNSW vector index (usearch default, or vecq) + FTS5 hybrid search (RRF k=60); `fusion` (weighted RRF of the vector and hybrid rankings) is the default recall strategy since 0.16.0. `unsafe_code = "forbid"` workspace-wide.

> This skill ships with each release — its version tracks the CLI version
> (a CI gate fails when they drift, see `skill-version-parity` test).
Expand Down
9 changes: 9 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,14 @@
# Changelog

## [0.20.1] - 2026-10-09

Patch release. Theme: **use an external OpenAI-compatible or Ollama embedder with `uteke-serve` and the Docker image.** Two fixes, nothing else (no schema change, no API change).

### Fixed

- **`uteke-serve` ignored the embedding configuration (#1399, #1401)** - the server opened its store with a hardcoded `onnx` backend, `ServerFileConfig` had no `[embedding]` section and `UTEKE_EMBEDDING_BACKEND` was only read by the CLI, so an external OpenAI-compatible or Ollama embedder could never be used by the server. The server now reads `[embedding]` (backend, model, api_key, base_url, endpoint_path, dims) and `[vector]` from `uteke.toml`; `UTEKE_EMBEDDING_BACKEND` (non-empty) wins over the file, `onnx` stays the default, and an unsupported backend exits at startup with a message naming the supported ones. The project-local `.uteke/uteke.toml` stays untrusted for `embedding.*`. Deployments without these settings behave exactly as before.
- **Docker entrypoint downloaded the ONNX model for every backend and looped when `/data` was not writable (#1398, #1400)** - with `UTEKE_EMBEDDING_BACKEND=openai|ollama` (env, or `[embedding] backend` in `$UTEKE_HOME/uteke.toml`) the ~208 MB model download is now skipped. When a download is needed but the data dir is not writable, the container exits with a message naming the fixes (chown the volume to uid 1000, pre-populate the model, or use an external embedder) instead of dying on a bare `mkdir` error and restarting forever. The server still needs a writable data dir for `uteke.db` and the index files.

## [0.20.0] - 2026-10-08

Minor release. Theme: **one write policy for the vector index, atomic documents,
Expand Down
8 changes: 4 additions & 4 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ members = [
]

[workspace.package]
version = "0.20.0"
version = "0.20.1"
edition = "2024"
license = "Apache-2.0"
repository = "https://github.com/codecoradev/uteke"
Expand Down
2 changes: 1 addition & 1 deletion README.id.md
Original file line number Diff line number Diff line change
Expand Up @@ -392,7 +392,7 @@ Bisa. Uteke punya MCP server yang langsung pakai dengan Claude Code, Cursor, dan
<details>
<summary><strong>Sudah production-ready?</strong></summary>

Uteke sekarang v0.20.0 dengan 200+ test, CI/CD di setiap commit, dan benchmark harness. Dipakai production oleh tim CodeCora dan early adopter lain. Masih di versi 0.x, jadi mungkin ada rough edges, tapi core-nya udah stabil.
Uteke sekarang v0.20.1 dengan 200+ test, CI/CD di setiap commit, dan benchmark harness. Dipakai production oleh tim CodeCora dan early adopter lain. Masih di versi 0.x, jadi mungkin ada rough edges, tapi core-nya udah stabil.
</details>

---
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -353,7 +353,7 @@ Yes. Uteke ships with an MCP server that works with Claude Code, Cursor, and Her
<details>
<summary><strong>Is it production-ready?</strong></summary>

Uteke is at v0.20.0 with 200+ tests, CI/CD on every commit, and a benchmark harness. It's used in production by the CodeCora team and other early adopters. Still in 0.x, so expect rough edges, but the core is stable.
Uteke is at v0.20.1 with 200+ tests, CI/CD on every commit, and a benchmark harness. It's used in production by the CodeCora team and other early adopters. Still in 0.x, so expect rough edges, but the core is stable.
</details>

---
Expand Down
2 changes: 1 addition & 1 deletion crates/uteke-cli/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ usearch = ["uteke-core/usearch"]
vecq = ["uteke-core/vecq"]

[dependencies]
uteke-core = { path = "../uteke-core", version = "0.20.0", default-features = false, features = ["onnx"] }
uteke-core = { path = "../uteke-core", version = "0.20.1", default-features = false, features = ["onnx"] }
clap = { version = "4", features = ["derive"] }
serde_json = "1"
tracing = "0.1"
Expand Down
2 changes: 1 addition & 1 deletion crates/uteke-cli/assets/uteke-memory-skill.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ description: "Persistent memory engine for AI agents via the uteke CLI — remem
# Uteke Memory Skill

Persistent memory engine for AI agents via the `uteke` CLI.
Version: **0.20.0** — SQLite + HNSW vector index (usearch default, or vecq) + FTS5 hybrid search (RRF k=60); `fusion` (weighted RRF of the vector and hybrid rankings) is the default recall strategy since 0.16.0. `unsafe_code = "forbid"` workspace-wide.
Version: **0.20.1** — SQLite + HNSW vector index (usearch default, or vecq) + FTS5 hybrid search (RRF k=60); `fusion` (weighted RRF of the vector and hybrid rankings) is the default recall strategy since 0.16.0. `unsafe_code = "forbid"` workspace-wide.

> This skill ships with each release — its version tracks the CLI version
> (a CI gate fails when they drift, see `skill-version-parity` test).
Expand Down
2 changes: 1 addition & 1 deletion crates/uteke-mcp/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ usearch = ["uteke-core/usearch"]
vecq = ["uteke-core/vecq"]

[dependencies]
uteke-core = { path = "../uteke-core", version = "0.20.0", default-features = false, features = ["onnx"] }
uteke-core = { path = "../uteke-core", version = "0.20.1", default-features = false, features = ["onnx"] }
serde = { version = "1", features = ["derive"] }
serde_json = "1"
dirs = "7"
Expand Down
4 changes: 2 additions & 2 deletions crates/uteke-server/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,8 @@ name = "uteke-serve"
path = "src/main.rs"

[dependencies]
uteke-core = { path = "../uteke-core", version = "0.20.0", default-features = false, features = ["onnx"] }
uteke-mcp = { path = "../uteke-mcp", version = "0.20.0", default-features = false }
uteke-core = { path = "../uteke-core", version = "0.20.1", default-features = false, features = ["onnx"] }
uteke-mcp = { path = "../uteke-mcp", version = "0.20.1", default-features = false }
serde = { version = "1", features = ["derive"] }
serde_json = "1"
schemars = { version = "1", optional = true }
Expand Down
181 changes: 180 additions & 1 deletion crates/uteke-server/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -258,7 +258,29 @@ fn main() {

info!("Opening store at: {db_path}");
let defaults = uteke_core::DreamConfig::default();
let uteke = match Uteke::open(&db_path) {
// Embedding backend + vector engine from uteke.toml / env (#1399). Fail
// fast on an unsupported backend instead of opening the store with a
// different embedder than the operator asked for.
let embedding = match resolve_embedding(
&config,
std::env::var("UTEKE_EMBEDDING_BACKEND").ok().as_deref(),
) {
Ok(e) => e,
Err(msg) => {
error!("{msg}");
std::process::exit(1);
}
};
info!("Embedding backend: {}", embedding.backend);
let uteke = match Uteke::open_with_embedding_and_graph(
&db_path,
&embedding.backend,
embedding.settings,
uteke_core::TierConfig::default(),
uteke_core::RecallConfig::default(),
uteke_core::GraphRerankConfig::default(),
embedding.vector_backend.as_deref(),
) {
Ok(mut u) => {
// Apply dream pipeline thresholds from config (#731)
if let Some(ref dc) = config.dream {
Expand Down Expand Up @@ -664,6 +686,10 @@ fn main() {
/// Minimal [server] config section for parsing uteke.toml.
#[derive(serde::Deserialize, Default)]
struct ServerFileConfig {
/// `[embedding]`: which embedder the server uses (#1399).
embedding: Option<EmbeddingFileSection>,
/// `[vector]`: which vector engine runs when both are compiled in (#1168).
vector: Option<VectorFileSection>,
server: Option<ServerFileSection>,
recall: Option<RecallFileSection>,
extraction: Option<uteke_core::extraction::ExtractionConfig>,
Expand All @@ -675,6 +701,70 @@ struct ServerFileConfig {
lifecycle: Option<LifecycleFileSection>,
}

/// `[embedding]` in uteke.toml, same keys as the CLI. Empty / missing values
/// fall back to the backend defaults; `UTEKE_EMBEDDING_*` env vars win over
/// these at resolve time inside the core.
#[derive(serde::Deserialize, Default, Clone)]
struct EmbeddingFileSection {
backend: Option<String>,
model: Option<String>,
api_key: Option<String>,
base_url: Option<String>,
endpoint_path: Option<String>,
dims: Option<usize>,
}

/// `[vector]` in uteke.toml.
#[derive(serde::Deserialize, Default, Clone)]
struct VectorFileSection {
backend: Option<String>,
}

/// Backends `Uteke` can initialize lazily (same list as the CLI validates).
const SUPPORTED_EMBEDDING_BACKENDS: &[&str] = &["onnx", "openai", "ollama"];

/// What the server opens the store with.
struct EmbeddingResolution {
backend: String,
settings: uteke_core::EmbeddingSettings,
vector_backend: Option<String>,
}

/// Resolve the embedder: `UTEKE_EMBEDDING_BACKEND` (non-empty) wins over
/// `[embedding] backend`, which wins over the `onnx` default. An unsupported
/// backend is an error with an actionable message (#1399).
fn resolve_embedding(
config: &ServerFileConfig,
env_backend: Option<&str>,
) -> Result<EmbeddingResolution, String> {
let file = config.embedding.clone().unwrap_or_default();
let backend = env_backend
.map(str::trim)
.filter(|b| !b.is_empty())
.map(str::to_string)
.or_else(|| file.backend.clone().filter(|b| !b.trim().is_empty()))
.unwrap_or_else(|| "onnx".to_string());
if !SUPPORTED_EMBEDDING_BACKENDS.contains(&backend.as_str()) {
return Err(format!(
"Unsupported embedding backend '{backend}'. Supported: {}. \
Set UTEKE_EMBEDDING_BACKEND or [embedding] backend in uteke.toml.",
SUPPORTED_EMBEDDING_BACKENDS.join(", ")
));
}
Ok(EmbeddingResolution {
backend,
settings: uteke_core::EmbeddingSettings {
api_key: file.api_key.unwrap_or_default(),
base_url: file.base_url.unwrap_or_default(),
endpoint_path: file.endpoint_path.unwrap_or_default(),
model: file.model.unwrap_or_default(),
dims: file.dims.unwrap_or(0),
},
// UTEKE_VECTOR_BACKEND (higher precedence) is read inside the core.
vector_backend: config.vector.as_ref().and_then(|v| v.backend.clone()),
})
}

/// Deprecated: superseded by [lifecycle] section (#934). Kept for backward-compat deserialization.
#[derive(serde::Deserialize, Default, Clone)]
#[allow(dead_code)]
Expand Down Expand Up @@ -860,3 +950,92 @@ mod config_overlay_tests {
assert_eq!(recall.default_strategy.as_deref(), Some("vector"));
}
}

#[cfg(test)]
mod embedding_config_tests {
//! #1399 — the server honours `[embedding]` / `[vector]` / env.
use super::*;

fn parse(toml_text: &str) -> ServerFileConfig {
toml::from_str(toml_text).expect("valid toml")
}

#[test]
fn default_is_onnx_with_empty_settings() {
let r = resolve_embedding(&ServerFileConfig::default(), None).unwrap();
assert_eq!(r.backend, "onnx");
assert!(r.settings.base_url.is_empty() && r.settings.model.is_empty());
assert_eq!(r.settings.dims, 0);
assert_eq!(r.vector_backend, None);
}

#[test]
fn toml_selects_an_external_backend_and_passes_its_settings() {
let cfg = parse(
"[embedding]\nbackend = \"openai\"\nbase_url = \"http://embor:8355/v1\"\n\
model = \"embeddinggemma-q4\"\ndims = 768\nendpoint_path = \"/embed\"\n\
[vector]\nbackend = \"vecq\"\n",
);
let r = resolve_embedding(&cfg, None).unwrap();
assert_eq!(r.backend, "openai");
assert_eq!(r.settings.base_url, "http://embor:8355/v1");
assert_eq!(r.settings.model, "embeddinggemma-q4");
assert_eq!(r.settings.endpoint_path, "/embed");
assert_eq!(r.settings.dims, 768);
assert_eq!(r.vector_backend.as_deref(), Some("vecq"));
}

#[test]
fn env_backend_wins_over_toml_and_empty_env_is_ignored() {
let cfg = parse("[embedding]\nbackend = \"ollama\"\n");
assert_eq!(
resolve_embedding(&cfg, Some("openai")).unwrap().backend,
"openai"
);
assert_eq!(resolve_embedding(&cfg, Some("")).unwrap().backend, "ollama");
assert_eq!(
resolve_embedding(&cfg, Some(" ")).unwrap().backend,
"ollama"
);
// env alone, no [embedding] section (the labs setup)
let r = resolve_embedding(&ServerFileConfig::default(), Some("openai")).unwrap();
assert_eq!(r.backend, "openai");
}

#[test]
fn unsupported_backend_is_an_actionable_error() {
let err = resolve_embedding(&ServerFileConfig::default(), Some("cohere"))
.err()
.expect("must fail");
assert!(
err.contains("cohere") && err.contains("onnx, openai, ollama"),
"{err}"
);
assert!(err.contains("UTEKE_EMBEDDING_BACKEND"), "{err}");
}

#[test]
fn untrusted_project_file_cannot_pick_the_embedder() {
// The shared layered resolver strips embedding.backend / base_url from
// the project-local file unless the project config is trusted.
let dir = std::env::temp_dir().join(format!("uteke_emb_cfg_{}", std::process::id()));
std::fs::create_dir_all(&dir).unwrap();
let global = dir.join("global.toml");
let project = dir.join("project.toml");
std::fs::write(&global, "[embedding]\nbackend = \"onnx\"\n").unwrap();
std::fs::write(
&project,
"[embedding]\nbackend = \"openai\"\nbase_url = \"https://evil.example\"\n",
)
.unwrap();
let cfg = resolve_server_config(&uteke_core::config_layers::Layers {
global: Some(&global),
project: Some(&project),
trust_project: false,
});
let r = resolve_embedding(&cfg, None).unwrap();
assert_eq!(r.backend, "onnx");
assert!(r.settings.base_url.is_empty());
let _ = std::fs::remove_dir_all(&dir);
}
}
Loading
Loading