From dd864eed226da8e59f9c929df26966489d3dc278 Mon Sep 17 00:00:00 2001 From: infosave2007 Date: Sat, 8 Aug 2026 13:21:43 +0300 Subject: [PATCH] =?UTF-8?q?fix(cache):=20honest=20settings=20card=20?= =?UTF-8?q?=E2=80=94=20clean=20threshold,=20live=20stats,=20no-embedder=20?= =?UTF-8?q?warning?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Full audit of the semantic cache against a live instance (mock embedder, identical requests): the mechanism works — miss at 3.1s, then hits at 2ms with entries/hits/saved-$ counting. Two honest gaps fixed: - the threshold round-tripped through f32 and rendered as 0.9200000166893005 in both the TOML and the settings input — the config field is f64 now (writes exactly what you typed) and the input rounds legacy values to 2 decimals; - the card now shows proof of life (entries / hits / hit-rate / saved $) and, when the cache is enabled with NO embedding model available, a warning that it is silently doing nothing and how to fix it — previously that state looked identical to a working cache. Also audited in the same pass, no changes needed: Prometheus /metrics (toggle on/off applies live, 404 when off), and all 7 protocol switches (each gates its endpoint instantly both ways). 4 strings ×7 languages. --- src/cache.rs | 2 +- src/config.rs | 7 ++++--- web/js/i18n/de.js | 4 ++++ web/js/i18n/en.js | 4 ++++ web/js/i18n/es.js | 4 ++++ web/js/i18n/fr.js | 4 ++++ web/js/i18n/ru.js | 4 ++++ web/js/i18n/tr.js | 4 ++++ web/js/i18n/zh.js | 4 ++++ web/js/views/settings.js | 25 ++++++++++++++++++++++++- 10 files changed, 57 insertions(+), 5 deletions(-) diff --git a/src/cache.rs b/src/cache.rs index ce444d1..6e03cf1 100644 --- a/src/cache.rs +++ b/src/cache.rs @@ -55,7 +55,7 @@ impl SemanticCache { pub fn new(cfg: &CacheCfg) -> Arc { Arc::new(Self { enabled: cfg.enabled, - threshold: cfg.threshold, + threshold: cfg.threshold as f32, ttl_secs: parse_duration_secs(&cfg.ttl).unwrap_or(3600), max_entries: cfg.max_entries.max(1), embed_model: cfg.embed_model.clone(), diff --git a/src/config.rs b/src/config.rs index 02eb089..4151578 100644 --- a/src/config.rs +++ b/src/config.rs @@ -829,9 +829,10 @@ fn default_shadow_soak() -> usize { pub struct CacheCfg { #[serde(default)] pub enabled: bool, - /// Cosine-similarity threshold for a hit (0..1). + /// Cosine-similarity threshold for a hit (0..1). f64 so the TOML holds + /// the number you typed (an f32 round-trips as 0.9200000166893005). #[serde(default = "default_semcache_threshold")] - pub threshold: f32, + pub threshold: f64, /// Entry time-to-live, e.g. `1h`. #[serde(default = "default_semcache_ttl")] pub ttl: String, @@ -853,7 +854,7 @@ impl Default for CacheCfg { } } } -fn default_semcache_threshold() -> f32 { +fn default_semcache_threshold() -> f64 { 0.92 } fn default_semcache_ttl() -> String { diff --git a/web/js/i18n/de.js b/web/js/i18n/de.js index 72f7709..3cf9684 100644 --- a/web/js/i18n/de.js +++ b/web/js/i18n/de.js @@ -635,4 +635,8 @@ export default { "tier.medium": "mittel", "tier.high": "schwer", "bm.scoreHint": "Der kontinuierliche Komplexitätsscore des Routers (0..1) — die Grenzregler auf der Routing-Seite teilen ihn in Stufen.", + "settings.cache.noEmbed": "Der Cache ist aktiviert, hat aber KEIN Embedding-Modell — er tut nichts. Fügen Sie dem Pool ein Modell mit kind = \"embedding\" hinzu (z. B. OpenAI text-embedding) oder wählen Sie unten eines.", + "settings.cache.entries": "Einträge", + "settings.cache.hits": "Treffer", + "settings.cache.saved": "gespart", }; diff --git a/web/js/i18n/en.js b/web/js/i18n/en.js index 10523f9..f7c292d 100644 --- a/web/js/i18n/en.js +++ b/web/js/i18n/en.js @@ -628,4 +628,8 @@ export default { "tier.medium": "medium", "tier.high": "high", "bm.scoreHint": "The router's continuous complexity score (0..1) — the boundary sliders on the Routing page cut it into tiers.", + "settings.cache.noEmbed": "The cache is enabled but has NO embedding model to work with — it is doing nothing. Add a model with kind = \"embedding\" to the pool (e.g. an OpenAI text-embedding model) or pick one below.", + "settings.cache.entries": "entries", + "settings.cache.hits": "hits", + "settings.cache.saved": "saved", }; diff --git a/web/js/i18n/es.js b/web/js/i18n/es.js index 2908a5a..9fc8ee9 100644 --- a/web/js/i18n/es.js +++ b/web/js/i18n/es.js @@ -635,4 +635,8 @@ export default { "tier.medium": "medio", "tier.high": "difícil", "bm.scoreHint": "La puntuación continua de complejidad del router (0..1) — los deslizadores de límites de la página Enrutado la dividen en niveles.", + "settings.cache.noEmbed": "La caché está activada pero NO tiene modelo de embeddings — no hace nada. Añade al pool un modelo con kind = \"embedding\" (p. ej. OpenAI text-embedding) o elige uno abajo.", + "settings.cache.entries": "entradas", + "settings.cache.hits": "aciertos", + "settings.cache.saved": "ahorrado", }; diff --git a/web/js/i18n/fr.js b/web/js/i18n/fr.js index 7686893..ba87394 100644 --- a/web/js/i18n/fr.js +++ b/web/js/i18n/fr.js @@ -635,4 +635,8 @@ export default { "tier.medium": "moyen", "tier.high": "difficile", "bm.scoreHint": "Le score de complexité continu du routeur (0..1) — les curseurs de limites de la page Routage le découpent en niveaux.", + "settings.cache.noEmbed": "Le cache est activé mais n'a AUCUN modèle d'embedding — il ne fait rien. Ajoutez au pool un modèle avec kind = \"embedding\" (p. ex. OpenAI text-embedding) ou choisissez-en un ci-dessous.", + "settings.cache.entries": "entrées", + "settings.cache.hits": "hits", + "settings.cache.saved": "économisé", }; diff --git a/web/js/i18n/ru.js b/web/js/i18n/ru.js index 0002937..b0cd955 100644 --- a/web/js/i18n/ru.js +++ b/web/js/i18n/ru.js @@ -627,4 +627,8 @@ export default { "tier.medium": "средний", "tier.high": "сложный", "bm.scoreHint": "Непрерывный коэффициент сложности роутера (0..1) — ползунки границ на вкладке Роутинг нарезают его на уровни.", + "settings.cache.noEmbed": "Кэш включён, но embedding-модели нет — он ничего не делает. Добавьте в пул модель с kind = \"embedding\" (например, OpenAI text-embedding) или выберите её ниже.", + "settings.cache.entries": "записей", + "settings.cache.hits": "попаданий", + "settings.cache.saved": "сэкономлено", }; diff --git a/web/js/i18n/tr.js b/web/js/i18n/tr.js index 633fe9b..c92f902 100644 --- a/web/js/i18n/tr.js +++ b/web/js/i18n/tr.js @@ -635,4 +635,8 @@ export default { "tier.medium": "orta", "tier.high": "zor", "bm.scoreHint": "Yönlendiricinin sürekli karmaşıklık puanı (0..1) — Yönlendirme sayfasındaki sınır kaydırıcıları onu katmanlara böler.", + "settings.cache.noEmbed": "Önbellek açık ama çalışacağı bir embedding modeli YOK — hiçbir şey yapmıyor. Havuza kind = \"embedding\" olan bir model ekleyin (örn. OpenAI text-embedding) veya aşağıdan seçin.", + "settings.cache.entries": "kayıt", + "settings.cache.hits": "isabet", + "settings.cache.saved": "tasarruf", }; diff --git a/web/js/i18n/zh.js b/web/js/i18n/zh.js index b0e795f..82b8e75 100644 --- a/web/js/i18n/zh.js +++ b/web/js/i18n/zh.js @@ -635,4 +635,8 @@ export default { "tier.medium": "中等", "tier.high": "困难", "bm.scoreHint": "路由器的连续复杂度得分(0..1)— 路由页面上的边界滑块将其划分为层级。", + "settings.cache.noEmbed": "缓存已启用,但没有可用的 embedding 模型 — 它没有任何作用。请向模型池添加 kind = \"embedding\" 的模型(如 OpenAI text-embedding),或在下方选择。", + "settings.cache.entries": "条目", + "settings.cache.hits": "命中", + "settings.cache.saved": "已节省", }; diff --git a/web/js/views/settings.js b/web/js/views/settings.js index 3224cff..53aac06 100644 --- a/web/js/views/settings.js +++ b/web/js/views/settings.js @@ -194,8 +194,30 @@ export async function renderSettings() { const modelsData = await api.listModels(); const embedModels = (modelsData.models || []).filter((m) => m.kind === "embedding").map((m) => m.id); const cacheEnabled = check(t("settings.cache.enabled"), s.cache?.enabled, t("settings.cache.enabledHelp")); - const cacheThresh = h("input", { type: "number", step: "0.01", value: s.cache?.threshold ?? 0.92 }); + const cacheThresh = h("input", { type: "number", step: "0.01", min: "0.5", max: "0.999", value: Math.round(((s.cache?.threshold ?? 0.92) + Number.EPSILON) * 100) / 100 }); const cacheTtlIn = h("input", { value: s.cache?.ttl || "1h" }); + // Live proof-of-life for the cache card: entries/hits/saved — and an + // honest warning when the cache is on but has no embedding model to work + // with (it silently does nothing in that state). + let cacheStats = null; + try { + cacheStats = (await api.stats("24h", "model")).cache || null; + } catch { /* stats off */ } + function cacheStatus() { + const wrap = h("div", { style: "margin-bottom:10px" }); + if (s.cache?.enabled && embedModels.length === 0 && !s.cache?.embed_model) { + mount(wrap, h("div", { class: "callout warn" }, "⚠ " + t("settings.cache.noEmbed"))); + return wrap; + } + if (cacheStats && s.cache?.enabled) { + mount(wrap, h("div", { class: "flex wrap", style: "gap:8px" }, + h("span", { class: "badge" }, t("settings.cache.entries") + ": " + cacheStats.entries), + h("span", { class: "badge ok" }, t("settings.cache.hits") + ": " + cacheStats.hits), + h("span", { class: "badge" }, "hit-rate " + Math.round((cacheStats.hit_rate || 0) * 100) + "%"), + h("span", { class: "badge" }, t("settings.cache.saved") + " $" + (cacheStats.saved_usd || 0).toFixed(4)))); + } + return wrap; + } const cacheEmbedSel = h( "select", {}, @@ -610,6 +632,7 @@ export async function renderSettings() { "div", { class: "card" }, h("div", { class: "card-head" }, h("h3", {}, t("settings.cache"))), + cacheStatus(), cacheEnabled.node, h("div", { class: "row" }, field(t("settings.cache.threshold"), cacheThresh), field(t("settings.cache.ttl"), cacheTtlIn)), field(t("settings.cache.embed"), cacheEmbedSel),