feat: semantic (embedding-based) response cache
- cache.rs: cosine-similarity store (threshold, TTL, ring buffer), partitioned by routing signature; tracks hit rate and model cost saved - pipeline: embed routing text, look up before the model call (route_source=cache on hit), insert successful responses; stats record cache hits - config [cache] + admin settings card; dashboard shows hit-rate and savings - embedding model resolved from the pool (or first kind="embedding") Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
7dbc39c6a6
commit
a67564bc24
Vendored
+11
@@ -254,4 +254,15 @@ export default {
|
||||
"toast.deleted": "Gelöscht",
|
||||
"toast.probing": "Prüfe…",
|
||||
"toast.imported": "Konfiguration importiert",
|
||||
"dash.cache": "Semantischer Cache",
|
||||
"dash.cache.hitRate": "Trefferquote",
|
||||
"dash.cache.hits": "Treffer",
|
||||
"dash.cache.saved": "Gespart",
|
||||
"dash.cache.disabled": "Semantischer Cache ist deaktiviert — in den Einstellungen aktivieren.",
|
||||
"settings.cache": "Semantischer Cache",
|
||||
"settings.cache.enabled": "Semantischen Cache aktivieren",
|
||||
"settings.cache.threshold": "Ähnlichkeitsschwelle",
|
||||
"settings.cache.ttl": "Eintrags-TTL",
|
||||
"settings.cache.embed": "Embedding-Modell",
|
||||
"settings.cache.note": "Cache-Einstellungen werden nach einem Neustart wirksam.",
|
||||
};
|
||||
|
||||
Vendored
+11
@@ -255,4 +255,15 @@ export default {
|
||||
"toast.deleted": "Deleted",
|
||||
"toast.probing": "Probing…",
|
||||
"toast.imported": "Config imported",
|
||||
"dash.cache": "Semantic cache",
|
||||
"dash.cache.hitRate": "Hit rate",
|
||||
"dash.cache.hits": "Hits",
|
||||
"dash.cache.saved": "Saved",
|
||||
"dash.cache.disabled": "Semantic cache is disabled — enable it in Settings.",
|
||||
"settings.cache": "Semantic cache",
|
||||
"settings.cache.enabled": "Enable semantic cache",
|
||||
"settings.cache.threshold": "Similarity threshold",
|
||||
"settings.cache.ttl": "Entry TTL",
|
||||
"settings.cache.embed": "Embedding model",
|
||||
"settings.cache.note": "Cache settings take effect after a restart.",
|
||||
};
|
||||
|
||||
Vendored
+11
@@ -254,4 +254,15 @@ export default {
|
||||
"toast.deleted": "Eliminado",
|
||||
"toast.probing": "Sondeando…",
|
||||
"toast.imported": "Configuración importada",
|
||||
"dash.cache": "Caché semántico",
|
||||
"dash.cache.hitRate": "Aciertos",
|
||||
"dash.cache.hits": "Aciertos",
|
||||
"dash.cache.saved": "Ahorrado",
|
||||
"dash.cache.disabled": "El caché semántico está desactivado — actívalo en Ajustes.",
|
||||
"settings.cache": "Caché semántico",
|
||||
"settings.cache.enabled": "Activar caché semántico",
|
||||
"settings.cache.threshold": "Umbral de similitud",
|
||||
"settings.cache.ttl": "TTL de entrada",
|
||||
"settings.cache.embed": "Modelo de embeddings",
|
||||
"settings.cache.note": "Los ajustes del caché se aplican tras reiniciar.",
|
||||
};
|
||||
|
||||
Vendored
+11
@@ -254,4 +254,15 @@ export default {
|
||||
"toast.deleted": "Supprimé",
|
||||
"toast.probing": "Sondage…",
|
||||
"toast.imported": "Config importée",
|
||||
"dash.cache": "Cache sémantique",
|
||||
"dash.cache.hitRate": "Taux de succès",
|
||||
"dash.cache.hits": "Succès",
|
||||
"dash.cache.saved": "Économisé",
|
||||
"dash.cache.disabled": "Le cache sémantique est désactivé — activez-le dans les Paramètres.",
|
||||
"settings.cache": "Cache sémantique",
|
||||
"settings.cache.enabled": "Activer le cache sémantique",
|
||||
"settings.cache.threshold": "Seuil de similarité",
|
||||
"settings.cache.ttl": "TTL d'entrée",
|
||||
"settings.cache.embed": "Modèle d'embeddings",
|
||||
"settings.cache.note": "Les paramètres du cache prennent effet après un redémarrage.",
|
||||
};
|
||||
|
||||
Vendored
+11
@@ -254,4 +254,15 @@ export default {
|
||||
"toast.deleted": "Удалено",
|
||||
"toast.probing": "Проверка…",
|
||||
"toast.imported": "Конфигурация импортирована",
|
||||
"dash.cache": "Семантический кэш",
|
||||
"dash.cache.hitRate": "Попаданий",
|
||||
"dash.cache.hits": "Попадания",
|
||||
"dash.cache.saved": "Сэкономлено",
|
||||
"dash.cache.disabled": "Семантический кэш выключен — включите в Настройках.",
|
||||
"settings.cache": "Семантический кэш",
|
||||
"settings.cache.enabled": "Включить семантический кэш",
|
||||
"settings.cache.threshold": "Порог сходства",
|
||||
"settings.cache.ttl": "TTL записи",
|
||||
"settings.cache.embed": "Модель эмбеддингов",
|
||||
"settings.cache.note": "Настройки кэша применяются после перезапуска.",
|
||||
};
|
||||
|
||||
Vendored
+11
@@ -254,4 +254,15 @@ export default {
|
||||
"toast.deleted": "Silindi",
|
||||
"toast.probing": "Sınanıyor…",
|
||||
"toast.imported": "Yapılandırma içe aktarıldı",
|
||||
"dash.cache": "Anlamsal önbellek",
|
||||
"dash.cache.hitRate": "İsabet oranı",
|
||||
"dash.cache.hits": "İsabet",
|
||||
"dash.cache.saved": "Tasarruf",
|
||||
"dash.cache.disabled": "Anlamsal önbellek kapalı — Ayarlar'dan etkinleştirin.",
|
||||
"settings.cache": "Anlamsal önbellek",
|
||||
"settings.cache.enabled": "Anlamsal önbelleği etkinleştir",
|
||||
"settings.cache.threshold": "Benzerlik eşiği",
|
||||
"settings.cache.ttl": "Giriş TTL",
|
||||
"settings.cache.embed": "Gömme modeli",
|
||||
"settings.cache.note": "Anlamsal önbellek ayarları yeniden başlatmadan sonra geçerli olur.",
|
||||
};
|
||||
|
||||
Vendored
+11
@@ -254,4 +254,15 @@ export default {
|
||||
"toast.deleted": "已删除",
|
||||
"toast.probing": "探测中…",
|
||||
"toast.imported": "配置已导入",
|
||||
"dash.cache": "语义缓存",
|
||||
"dash.cache.hitRate": "命中率",
|
||||
"dash.cache.hits": "命中",
|
||||
"dash.cache.saved": "已节省",
|
||||
"dash.cache.disabled": "语义缓存已禁用 — 在设置中启用。",
|
||||
"settings.cache": "语义缓存",
|
||||
"settings.cache.enabled": "启用语义缓存",
|
||||
"settings.cache.threshold": "相似度阈值",
|
||||
"settings.cache.ttl": "条目 TTL",
|
||||
"settings.cache.embed": "嵌入模型",
|
||||
"settings.cache.note": "缓存设置将在重启后生效。",
|
||||
};
|
||||
|
||||
Vendored
+16
@@ -108,6 +108,22 @@ export async function renderDashboard() {
|
||||
barList(breakdown, num)
|
||||
),
|
||||
|
||||
// semantic cache
|
||||
stats.cache && stats.cache.enabled
|
||||
? h(
|
||||
"div",
|
||||
{ class: "card" },
|
||||
h("div", { class: "card-head" }, h("h3", {}, t("dash.cache"))),
|
||||
h(
|
||||
"div",
|
||||
{ class: "kpis" },
|
||||
kpi(pct(stats.cache.hit_rate), t("dash.cache.hitRate")),
|
||||
kpi(num(stats.cache.hits), t("dash.cache.hits")),
|
||||
kpi(money(stats.cache.saved_usd), t("dash.cache.saved"))
|
||||
)
|
||||
)
|
||||
: null,
|
||||
|
||||
// recent
|
||||
h(
|
||||
"div",
|
||||
|
||||
Vendored
+29
@@ -42,6 +42,18 @@ export async function renderSettings() {
|
||||
const statsEnabled = check(t("settings.stats.enabled"), s.stats?.enabled);
|
||||
const statsFile = h("input", { value: s.stats?.file || "" });
|
||||
const statsRet = h("input", { value: s.stats?.retention || "7d" });
|
||||
// cache
|
||||
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);
|
||||
const cacheThresh = h("input", { type: "number", step: "0.01", value: s.cache?.threshold ?? 0.92 });
|
||||
const cacheTtlIn = h("input", { value: s.cache?.ttl || "1h" });
|
||||
const cacheEmbedSel = h(
|
||||
"select",
|
||||
{},
|
||||
h("option", { value: "" }, "auto"),
|
||||
...embedModels.map((id) => h("option", { value: id, selected: id === s.cache?.embed_model }, id))
|
||||
);
|
||||
|
||||
const saveBtn = h("button", { class: "btn primary" }, t("common.save"));
|
||||
saveBtn.addEventListener("click", async () => {
|
||||
@@ -71,11 +83,19 @@ export async function renderSettings() {
|
||||
retention: statsRet.value.trim() || "7d",
|
||||
ring_size: s.stats?.ring_size ?? 500,
|
||||
},
|
||||
cache: {
|
||||
enabled: cacheEnabled.cb.checked,
|
||||
threshold: parseFloat(cacheThresh.value) || 0.92,
|
||||
ttl: cacheTtlIn.value.trim() || "1h",
|
||||
max_entries: s.cache?.max_entries ?? 1000,
|
||||
embed_model: cacheEmbedSel.value || null,
|
||||
},
|
||||
};
|
||||
// serde: drop null optionals
|
||||
if (!patch.router.api_key_env) delete patch.router.api_key_env;
|
||||
if (!patch.router.taxonomy_id) delete patch.router.taxonomy_id;
|
||||
if (!patch.telemetry.otlp_endpoint_env) delete patch.telemetry.otlp_endpoint_env;
|
||||
if (!patch.cache.embed_model) delete patch.cache.embed_model;
|
||||
try {
|
||||
const r = await api.putSettings(patch);
|
||||
toast(r.needs_restart ? t("settings.needsRestart") : t("toast.saved"), r.needs_restart ? "warn" : "good");
|
||||
@@ -170,6 +190,15 @@ export async function renderSettings() {
|
||||
statsEnabled.node,
|
||||
field(t("settings.stats.file"), statsFile),
|
||||
field(t("settings.stats.retention"), statsRet)
|
||||
),
|
||||
h(
|
||||
"div",
|
||||
{ class: "card" },
|
||||
h("div", { class: "card-head" }, h("h3", {}, t("settings.cache"))),
|
||||
cacheEnabled.node,
|
||||
h("div", { class: "row" }, field(t("settings.cache.threshold"), cacheThresh), field(t("settings.cache.ttl"), cacheTtlIn)),
|
||||
field(t("settings.cache.embed"), cacheEmbedSel),
|
||||
h("div", { class: "hint" }, t("settings.cache.note"))
|
||||
)
|
||||
),
|
||||
h(
|
||||
|
||||
Reference in New Issue
Block a user