fix(cache): honest settings card — clean threshold, live stats, no-embedder warning
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.
This commit is contained in:
+1
-1
@@ -55,7 +55,7 @@ impl SemanticCache {
|
||||
pub fn new(cfg: &CacheCfg) -> Arc<Self> {
|
||||
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(),
|
||||
|
||||
+4
-3
@@ -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 {
|
||||
|
||||
Vendored
+4
@@ -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",
|
||||
};
|
||||
|
||||
Vendored
+4
@@ -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",
|
||||
};
|
||||
|
||||
Vendored
+4
@@ -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",
|
||||
};
|
||||
|
||||
Vendored
+4
@@ -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é",
|
||||
};
|
||||
|
||||
Vendored
+4
@@ -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": "сэкономлено",
|
||||
};
|
||||
|
||||
Vendored
+4
@@ -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",
|
||||
};
|
||||
|
||||
Vendored
+4
@@ -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": "已节省",
|
||||
};
|
||||
|
||||
Vendored
+24
-1
@@ -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),
|
||||
|
||||
Reference in New Issue
Block a user