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
1 change: 1 addition & 0 deletions changelog.d/fixes/10769-cache-stats-real-cache.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
- **fix(api):** `/api/cache/stats` reported the prompt-cache LRU, which no request path ever writes to — it answered `0 hit / 0 miss, size 0` while the semantic cache served real traffic, and the Health and Usage dashboards rendered that as fact. It now reports the semantic cache's in-memory entries, with the same response shape ([#PRNUM](https://github.com/diegosouzapw/OmniRoute/pull/10769)) — thanks @Poid-ZA, who first fixed this in #9446.
9 changes: 3 additions & 6 deletions src/app/api/cache/stats/route.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { NextRequest, NextResponse } from "next/server";
import { getPromptCache } from "@/lib/cacheLayer";
import { clearMemoryCache, getMemoryCacheStats } from "@/lib/semanticCache";
import { isAuthenticated } from "@/shared/utils/apiAuth";
import { sanitizeErrorMessage } from "@omniroute/open-sse/utils/error.ts";

Expand All @@ -9,9 +9,7 @@ export async function GET(req: NextRequest) {
}

try {
const cache = getPromptCache();
const stats = cache.getStats();
return NextResponse.json(stats);
return NextResponse.json(getMemoryCacheStats());
} catch (error) {
return NextResponse.json({ error: sanitizeErrorMessage(error) }, { status: 500 });
}
Expand All @@ -23,8 +21,7 @@ export async function DELETE(req: NextRequest) {
}

try {
const cache = getPromptCache();
cache.clear();
clearMemoryCache();
return NextResponse.json({ success: true, message: "Cache cleared" });
} catch (error) {
return NextResponse.json({ error: sanitizeErrorMessage(error) }, { status: 500 });
Expand Down
16 changes: 16 additions & 0 deletions src/lib/semanticCache.ts
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,22 @@ function getMemoryCache() {
return memoryCache;
}

/**
* In-memory LRU stats for the semantic cache.
*
* Exposed for `/api/cache/stats`, which used to report `getPromptCache()` — an
* LRU that nothing writes to, so it always answered 0 hit / 0 miss. Same shape
* as `LRUCache.getStats()`, so callers do not have to change.
*/
export function getMemoryCacheStats(): ReturnType<LRUCache["getStats"]> {
return getMemoryCache().getStats();
}

/** Drop the in-memory LRU without touching the `semantic_cache` table. */
export function clearMemoryCache(): void {
getMemoryCache().clear();
}

// ─── Signature Generation ─────────────────

/**
Expand Down
52 changes: 52 additions & 0 deletions tests/unit/cache-stats-reports-semantic-cache.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import { describe, it } from "node:test";
import assert from "node:assert/strict";

import { getPromptCache } from "../../src/lib/cacheLayer.ts";
import {
clearMemoryCache,
getMemoryCacheStats,
setCachedResponse,
} from "../../src/lib/semanticCache.ts";

// Regression guard for /api/cache/stats.
//
// The route used to read getPromptCache() — an LRU that no request path writes
// to. It answered "0 hit / 0 miss, size 0" no matter how much traffic the
// semantic cache served, and two dashboard pages rendered that as fact.
//
// The first assertion fails against the old wiring: caching a response fills the
// semantic cache and leaves the prompt cache empty.
describe("cache stats report the cache that requests actually use", () => {
it("counts an entry written through the semantic cache", () => {
clearMemoryCache();
getPromptCache().clear();

const before = getMemoryCacheStats();
setCachedResponse("sig-cache-stats-guard", "gpt-4.1", { choices: [] }, 42);
const after = getMemoryCacheStats();

assert.equal(after.size, before.size + 1);
assert.equal(getPromptCache().getStats().size, 0);
});

it("keeps the shape the dashboards read, with a numeric hit rate", () => {
const stats = getMemoryCacheStats();

for (const key of ["size", "maxSize", "hits", "misses", "hitRate"]) {
assert.ok(key in stats, `missing ${key}`);
}
// Both dashboard pages call hitRate.toFixed(1); a string would throw there.
assert.equal(typeof stats.hitRate, "number");
assert.equal(typeof stats.size, "number");
assert.equal(typeof stats.maxSize, "number");
});

it("clears the in-memory entries", () => {
setCachedResponse("sig-cache-stats-clear", "gpt-4.1", { choices: [] }, 1);
assert.ok(getMemoryCacheStats().size > 0);

clearMemoryCache();

assert.equal(getMemoryCacheStats().size, 0);
});
});
Loading