fix semantic cache toggle and redesign cache management (#1135)

Integrated into release/v3.6.2
This commit is contained in:
Randi
2026-04-11 08:44:48 -04:00
committed by GitHub
parent e75baed4b6
commit 1d6739b683
12 changed files with 987 additions and 521 deletions

View File

@@ -51,12 +51,13 @@ import {
getModelPreserveOpenAIDeveloperRole,
getModelUpstreamExtraHeaders,
getUpstreamProxyConfig,
getCachedSettings,
} from "@/lib/localDb";
import { getExecutor } from "../executors/index.ts";
import { getCacheControlSettings } from "@/lib/cacheControlSettings";
import {
shouldPreserveCacheControl,
providerSupportsCaching,
type CacheControlMode,
} from "../utils/cacheControlPolicy.ts";
import { getCacheMetrics } from "@/lib/db/settings.ts";
@@ -810,9 +811,16 @@ export async function handleChatCore({
}
const stream = resolveStreamFlag(body?.stream, acceptHeader);
const runtimeSettings = await getCachedSettings().catch(() => ({}) as Record<string, unknown>);
const semanticCacheEnabled = runtimeSettings.semanticCacheEnabled !== false;
const cacheControlMode =
runtimeSettings.alwaysPreserveClientCache === "always" ||
runtimeSettings.alwaysPreserveClientCache === "never"
? (runtimeSettings.alwaysPreserveClientCache as CacheControlMode)
: "auto";
// ── Phase 9.1: Semantic cache check (non-streaming, temp=0 only) ──
if (isCacheable(body, clientRawRequest?.headers)) {
if (semanticCacheEnabled && isCacheable(body, clientRawRequest?.headers)) {
const signature = generateSignature(model, body.messages, body.temperature, body.top_p);
const cached = getCachedResponse(signature);
if (cached) {
@@ -948,8 +956,6 @@ export async function handleChatCore({
let ccSessionId: string | null = null;
// Determine if we should preserve client-side cache_control headers
// Fetch settings from DB to get user preference
const cacheControlMode = await getCacheControlSettings().catch(() => "auto" as const);
const preserveCacheControl = shouldPreserveCacheControl({
userAgent,
isCombo,
@@ -2269,7 +2275,7 @@ export async function handleChatCore({
}
// ── Phase 9.1: Cache store (non-streaming, temp=0) ──
if (isCacheable(body, clientRawRequest?.headers)) {
if (semanticCacheEnabled && isCacheable(body, clientRawRequest?.headers)) {
const signature = generateSignature(model, body.messages, body.temperature, body.top_p);
const tokensSaved = usage?.prompt_tokens + usage?.completion_tokens || 0;
setCachedResponse(signature, model, translatedResponse, tokensSaved);

View File

@@ -884,8 +884,10 @@ export const cacheStatsOutput = z.object({
.object({
totalRequests: z.number(),
requestsWithCacheControl: z.number(),
totalInputTokens: z.number(),
totalCachedTokens: z.number(),
totalCacheCreationTokens: z.number(),
tokensSaved: z.number(),
estimatedCostSaved: z.number(),
})
.nullable(),
@@ -893,6 +895,11 @@ export const cacheStatsOutput = z.object({
activeKeys: z.number(),
windowMs: z.number(),
}),
config: z
.object({
semanticCacheEnabled: z.boolean(),
})
.optional(),
});
export const cacheStatsTool: McpToolDefinition<typeof cacheStatsInput, typeof cacheStatsOutput> = {

View File

@@ -0,0 +1,210 @@
// @vitest-environment jsdom
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
import { render, screen, waitFor, cleanup, fireEvent } from "@testing-library/react";
import "@testing-library/jest-dom/vitest";
import React from "react";
import CachePage from "../page";
const notifications = {
success: vi.fn(),
error: vi.fn(),
};
vi.mock("next-intl", () => ({
useTranslations: (namespace?: string) => {
const getMessage = (key: string) => {
const fullKey = namespace ? `${namespace}.${key}` : key;
const messages: Record<string, string> = {
"cache.title": "Cache Management",
"cache.description":
"Monitor provider prompt cache efficiency and local semantic response reuse.",
"cache.refresh": "Refresh",
"cache.promptCache": "Prompt Cache (Provider-Side)",
"cache.promptCacheSectionDesc": "Prompt cache section",
"cache.lastUpdated": "Last updated",
"cache.withCacheControl": "With Cache Control",
"cache.cacheRateDesc": "of total requests",
"cache.cacheReuseRatio": "Cache Reuse Ratio",
"cache.cacheReuseRatioDesc": "Cache read tokens / Total input tokens",
"cache.cachedTokens": "Cache Read Tokens",
"cache.cachedTokensRead": "Read from cache",
"cache.estCostSaved": "Est. Cost Saved",
"cache.cacheCreationTokens": "Cache Write Tokens",
"cache.cacheRate": "Cache Rate",
"cache.requests": "Requests",
"cache.inputTokens": "Input Tokens",
"cache.tokensSaved": "Tokens Saved",
"cache.trend24h": "Cache Trend (24h)",
"cache.cached": "Cached",
"cache.byProvider": "Breakdown by Provider",
"cache.providerCacheRateDesc": "Provider cache rate description",
"cache.cachedTokensCol": "Cache Read",
"cache.cacheCreation": "Cache Write",
"cache.cacheCreationWrite": "Written to cache",
"cache.inputTokens": "Total Input Tokens",
"cache.semanticCache": "Semantic Cache",
"cache.semanticCacheSectionDesc": "Semantic cache section",
"cache.semanticCacheDisabledDesc": "Semantic cache is disabled.",
"cache.memoryEntries": "Memory Entries",
"cache.memoryEntriesSub": "In-memory LRU",
"cache.dbEntries": "DB Entries",
"cache.dbEntriesSub": "Persisted (SQLite)",
"cache.cacheHits": "Cache Hits",
"cache.cacheHitsSub": "of {total} total",
"cache.tokensSavedSub": "Estimated from hits",
"cache.performance": "Cache Performance",
"cache.autoRefresh": "Auto-refreshes every {seconds}s",
"cache.hitRate": "Hit Rate",
"cache.hits": "Hits",
"cache.misses": "Misses",
"cache.total": "Total",
"cache.behavior": "Cache Behavior",
"cache.behaviorDeterministic": "Only non-streaming requests with temperature=0 are cached.",
"cache.behaviorTwoTier": "Two-tier storage: in-memory LRU + SQLite.",
"cache.behaviorTtl": "TTL via {envVar}.",
"cache.idempotency": "Idempotency Layer",
"cache.activeDedupKeys": "Active Dedup Keys",
"cache.dedupWindow": "Dedup Window",
"cache.entries": "Entries",
"cache.semanticEntriesDesc": "Semantic entries only.",
"cache.searchEntries": "Search entries...",
"cache.search": "Search",
"cache.loading": "Loading...",
"cache.noEntries": "No cache entries found",
"cache.clearAll": "Clear Semantic Cache",
"settings.enabled": "Enabled",
disabled: "Disabled",
};
return messages[fullKey] ?? key;
};
const translate = (key: string, values?: Record<string, unknown>) => {
let message = getMessage(key);
if (values) {
for (const [name, value] of Object.entries(values)) {
message = message.replace(`{${name}}`, String(value));
}
}
return message;
};
translate.rich = (key: string, values?: Record<string, () => React.ReactNode>) => {
if (key === "behaviorBypass") {
return <>Bypass with header {values?.header?.()}.</>;
}
if (key === "behaviorTtl") {
return <>TTL via {values?.envVar?.()}.</>;
}
return translate(key);
};
return translate;
},
}));
vi.mock("@/store/notificationStore", () => ({
useNotificationStore: () => notifications,
}));
describe("CachePage", () => {
const fetchMock = vi.fn();
beforeEach(() => {
fetchMock.mockReset();
notifications.success.mockReset();
notifications.error.mockReset();
vi.stubGlobal("fetch", fetchMock);
});
afterEach(() => {
cleanup();
vi.unstubAllGlobals();
});
it("switches between prompt and semantic cache views", async () => {
fetchMock.mockImplementation(async (input: string) => {
if (input.startsWith("/api/cache/entries")) {
return {
ok: true,
json: async () => ({
entries: [],
pagination: { page: 1, limit: 20, total: 0, totalPages: 0 },
}),
};
}
return {
ok: true,
json: async () => ({
semanticCache: {
memoryEntries: 0,
dbEntries: 0,
hits: 0,
misses: 3,
hitRate: "0.0",
tokensSaved: 0,
},
promptCache: {
totalRequests: 10,
requestsWithCacheControl: 6,
totalInputTokens: 1000,
totalCachedTokens: 550,
totalCacheCreationTokens: 180,
tokensSaved: 550,
estimatedCostSaved: 0.12,
byProvider: {
claude: {
requests: 6,
totalRequests: 10,
cachedRequests: 6,
inputTokens: 1000,
cachedTokens: 550,
cacheCreationTokens: 180,
},
},
byStrategy: {},
lastUpdated: "2026-04-11T05:00:00.000Z",
},
trend: [
{
timestamp: "2026-04-11T05:00:00.000Z",
requests: 10,
cachedRequests: 6,
inputTokens: 1000,
cachedTokens: 550,
cacheCreationTokens: 180,
},
],
idempotency: {
activeKeys: 2,
windowMs: 5000,
},
config: {
semanticCacheEnabled: false,
},
}),
};
});
render(<CachePage />);
await waitFor(() => {
expect(screen.getByText("Prompt Cache (Provider-Side)")).toBeInTheDocument();
});
expect(screen.getByText("Breakdown by Provider")).toBeInTheDocument();
expect(screen.getByText("claude")).toBeInTheDocument();
expect(screen.getAllByText("60.0%").length).toBeGreaterThan(0);
expect(screen.queryByText("Semantic cache is disabled.")).not.toBeInTheDocument();
fireEvent.click(screen.getByRole("button", { name: "Semantic Cache" }));
await waitFor(() => {
expect(screen.getByText("Semantic cache is disabled.")).toBeInTheDocument();
});
expect(screen.getByText("Entries")).toBeInTheDocument();
expect(screen.getByText("Active Dedup Keys")).toBeInTheDocument();
});
});

View File

@@ -33,10 +33,12 @@ export default function CacheEntriesTab() {
const [loading, setLoading] = useState(true);
const [search, setSearch] = useState("");
const [deleting, setDeleting] = useState<string | null>(null);
const [error, setError] = useState<string | null>(null);
const fetchEntries = useCallback(
async (page = 1) => {
setLoading(true);
setError(null);
try {
const params = new URLSearchParams({ page: String(page), limit: String(pagination.limit) });
if (search) params.set("search", search);
@@ -46,14 +48,18 @@ export default function CacheEntriesTab() {
const data = await res.json();
setEntries(data.entries);
setPagination(data.pagination);
} else {
setEntries([]);
setError(t("entriesLoadError"));
}
} catch {
// ignore
setEntries([]);
setError(t("entriesLoadError"));
} finally {
setLoading(false);
}
},
[search, pagination.limit]
[pagination.limit, search, t]
);
useEffect(() => {
@@ -94,6 +100,13 @@ export default function CacheEntriesTab() {
{loading ? (
<div className="text-sm text-text-muted">{t("loading")}</div>
) : error ? (
<div className="flex items-center justify-between gap-3 rounded-xl border border-red-500/20 bg-red-500/5 px-4 py-3">
<div className="text-sm text-red-300">{error}</div>
<Button variant="secondary" size="sm" onClick={() => fetchEntries(pagination.page)}>
{t("refresh")}
</Button>
</div>
) : entries.length === 0 ? (
<div className="text-sm text-text-muted text-center py-8">{t("noEntries")}</div>
) : (

File diff suppressed because it is too large Load Diff

View File

@@ -2,13 +2,13 @@ import { NextRequest, NextResponse } from "next/server";
import {
getCacheStats,
clearCache,
cleanExpiredEntries,
invalidateByModel,
invalidateBySignature,
invalidateStale,
} from "@/lib/semanticCache";
import { getIdempotencyStats } from "@/lib/idempotencyLayer";
import { getCacheMetrics, getCacheTrend } from "@/lib/db/settings";
import { getCachedSettings } from "@/lib/localDb";
import { isAuthenticated } from "@/shared/utils/apiAuth";
function errorMessage(error: unknown): string {
@@ -29,12 +29,16 @@ export async function GET(req: NextRequest) {
const idempotencyStats = await getIdempotencyStats();
const promptCacheMetrics = await getCacheMetrics();
const trend = await getCacheTrend(trendHours);
const settings = await getCachedSettings().catch(() => ({}));
return NextResponse.json({
semanticCache: cacheStats,
promptCache: promptCacheMetrics,
trend,
idempotency: idempotencyStats,
config: {
semanticCacheEnabled: settings.semanticCacheEnabled !== false,
},
});
} catch (error) {
return NextResponse.json({ error: errorMessage(error) }, { status: 500 });
@@ -87,9 +91,8 @@ export async function DELETE(req: NextRequest) {
}
// Full clear
clearCache();
const expiredRemoved = cleanExpiredEntries();
return NextResponse.json({ ok: true, expiredRemoved, scope: "all" });
const cleared = clearCache();
return NextResponse.json({ ok: true, cleared, scope: "all" });
} catch (error) {
return NextResponse.json({ error: errorMessage(error) }, { status: 500 });
}

View File

@@ -3114,9 +3114,9 @@
},
"cache": {
"title": "Cache Management",
"description": "Monitor and manage semantic response cache, hit rates, and token savings.",
"description": "Monitor provider prompt cache efficiency and local semantic response reuse.",
"refresh": "Refresh",
"clearAll": "Clear All",
"clearAll": "Clear Semantic Cache",
"memoryEntries": "Memory Entries",
"memoryEntriesSub": "In-memory LRU",
"dbEntries": "DB Entries",
@@ -3139,22 +3139,34 @@
"idempotency": "Idempotency Layer",
"activeDedupKeys": "Active Dedup Keys",
"dedupWindow": "Dedup Window",
"clearSuccess": "Cache cleared. {count} expired entries removed.",
"clearSuccess": "Semantic cache cleared. {count} entries removed.",
"clearError": "Failed to clear cache.",
"unavailable": "Cache unavailable",
"unavailableDesc": "Could not fetch cache statistics. Make sure the server is running.",
"promptCache": "Prompt Cache (Provider-Side)",
"semanticCache": "Semantic Cache",
"promptCacheSectionDesc": "Shows provider-side prompt caching activity from usage history so you can see where cache control is active and how much input reuse you are getting.",
"promptTrendDesc": "Hourly request volume, cache coverage, and cache read volume across the last 24 hours.",
"cachedRequests": "Cached Requests",
"cachedRequests24h": "Cached Requests (24h)",
"cacheHitRate": "Cache Hit Rate",
"cachedTokens": "Cached Tokens",
"cacheCreationTokens": "Cache Creation Tokens",
"cacheRate": "Cache Rate",
"cacheRateDesc": "of total requests",
"cachedTokens": "Cache Read Tokens",
"cacheCreationTokens": "Cache Write Tokens",
"cacheMetrics": "Prompt Cache Metrics",
"withCacheControl": "With Cache Control",
"cachedTokensRead": "Cached Tokens (Read)",
"cacheCreationWrite": "Cache Creation (Write)",
"cachedTokensRead": "Read from cache",
"cacheCreationWrite": "Written to cache",
"cacheReuseRatio": "Cache Reuse Ratio",
"cacheReuseRatioDesc": "Cached tokens / Total input tokens",
"cacheReuseRatioDesc": "Cache read tokens / Total input tokens",
"estCostSaved": "Est. Cost Saved",
"lastUpdated": "Last updated",
"hoursTracked": "hours tracked",
"busiestHour": "Busiest Hour",
"peakCacheRate": "Peak Cache Rate",
"trendHour": "Hour",
"activityVolume": "Activity",
"requestsShort": "reqs",
"inputShort": "In",
"cachedShort": "Cached",
@@ -3162,20 +3174,27 @@
"resetting": "Resetting...",
"resetMetrics": "Reset Metrics",
"byProvider": "Breakdown by Provider",
"providerCacheRateDesc": "Each provider exposes total input tokens, cache read tokens, and cache write tokens so you can verify the reuse ratio against raw totals.",
"provider": "Provider",
"requests": "Requests",
"inputTokens": "Input Tokens",
"cachedTokensCol": "Cached",
"cacheCreation": "Creation",
"inputTokens": "Total Input Tokens",
"cachedTokensCol": "Cache Read",
"cacheCreation": "Cache Write",
"trend24h": "Cache Trend (24h)",
"peakCached": "Peak cached",
"cached": "Cached",
"overview": "Overview",
"entries": "Entries",
"semanticCacheSectionDesc": "OmniRoute's own deterministic response cache. When enabled, repeated non-streaming temperature=0 requests can be served locally without hitting the upstream provider.",
"semanticCacheDisabledDesc": "Semantic cache is disabled. OmniRoute will skip local response reuse until you turn it back on in Settings.",
"semanticEntriesDesc": "Persisted semantic cache records currently stored in SQLite. Provider-side prompt cache activity is not listed here.",
"searchEntries": "Search entries...",
"search": "Search",
"loading": "Loading...",
"entriesLoadError": "Failed to load semantic cache entries.",
"noEntries": "No cache entries found",
"noPromptCacheData": "No provider-side prompt cache activity has been recorded yet.",
"noTrendData": "No prompt cache activity has been recorded over the last 24 hours.",
"signature": "Signature",
"model": "Model",
"created": "Created",

View File

@@ -3054,9 +3054,9 @@
},
"cache": {
"title": "缓存管理",
"description": "监控和管理语义响应缓存、命中率及 Tokens 节省情况。",
"description": "监控提供商侧 Prompt Cache 的效率,以及本地 Semantic Cache 的响应复用情况。",
"refresh": "刷新",
"clearAll": "清空全部",
"clearAll": "清空语义缓存",
"memoryEntries": "内存条目",
"memoryEntriesSub": "内存 LRU",
"dbEntries": "数据库条目",
@@ -3079,22 +3079,34 @@
"idempotency": "幂等层",
"activeDedupKeys": "活跃去重键",
"dedupWindow": "去重窗口",
"clearSuccess": "缓存已清除。已删除 {count} 条过期条目。",
"clearSuccess": "语义缓存已清空,已删除 {count} 条记录。",
"clearError": "清除缓存失败。",
"unavailable": "缓存不可用",
"unavailableDesc": "无法获取缓存统计信息。请确保服务器正在运行。",
"promptCache": "Prompt 缓存(提供商侧)",
"semanticCache": "语义缓存",
"promptCacheSectionDesc": "基于 usage history 展示提供商侧 prompt cache 的活跃度,让你区分哪些请求真的启用了 cache control以及实际复用了多少输入。",
"promptTrendDesc": "按小时展示最近 24 小时的请求量、缓存覆盖率,以及 cache read token 的变化。",
"cachedRequests": "缓存请求数",
"cachedRequests24h": "24 小时缓存请求数",
"cacheHitRate": "缓存命中率",
"cachedTokens": "缓存 Tokens",
"cacheCreationTokens": "缓存创建 Tokens",
"cacheRate": "缓存",
"cacheRateDesc": "占总请求数",
"cachedTokens": "Cache Read Tokens",
"cacheCreationTokens": "Cache Write Tokens",
"cacheMetrics": "Prompt 缓存指标",
"withCacheControl": "含缓存控制",
"cachedTokensRead": "缓存 Tokens读取",
"cacheCreationWrite": "缓存创建(写入)",
"cachedTokensRead": "Read from cache",
"cacheCreationWrite": "Written to cache",
"cacheReuseRatio": "缓存复用率",
"cacheReuseRatioDesc": "缓存 Tokens / 输入 Tokens 总量",
"cacheReuseRatioDesc": "Cache read tokens / 输入 Tokens 总量",
"estCostSaved": "预估节省费用",
"lastUpdated": "上次更新",
"hoursTracked": "个小时",
"busiestHour": "最繁忙时段",
"peakCacheRate": "最高缓存率",
"trendHour": "小时",
"activityVolume": "活动量",
"requestsShort": "请求",
"inputShort": "输入",
"cachedShort": "已缓存",
@@ -3102,20 +3114,27 @@
"resetting": "正在重置...",
"resetMetrics": "重置指标",
"byProvider": "按提供商分类",
"providerCacheRateDesc": "每个提供商都会直接展示总输入 Tokens、cache read tokens 和 cache write tokens方便你对照原始数据判断 ratio 是否可靠。",
"provider": "提供商",
"requests": "请求数",
"inputTokens": "输入 Tokens",
"cachedTokensCol": "已缓存",
"cacheCreation": "创建",
"inputTokens": "输入 Tokens 总计",
"cachedTokensCol": "Cache Read",
"cacheCreation": "Cache Write",
"trend24h": "缓存趋势24 小时)",
"peakCached": "峰值缓存量",
"cached": "已缓存",
"overview": "概览",
"entries": "条目",
"semanticCacheSectionDesc": "OmniRoute 自己维护的确定性响应缓存。开启后重复的非流式、temperature=0 请求可以直接在本地命中,不再访问上游 provider。",
"semanticCacheDisabledDesc": "Semantic Cache 当前已禁用。重新在设置中开启之前OmniRoute 不会再做本地响应复用。",
"semanticEntriesDesc": "这里展示的是保存在 SQLite 里的 semantic cache 记录,不包含 provider-side prompt cache 的活动。",
"searchEntries": "搜索条目...",
"search": "搜索",
"loading": "加载中...",
"entriesLoadError": "加载语义缓存条目失败。",
"noEntries": "未找到缓存条目",
"noPromptCacheData": "暂时还没有记录到 provider-side prompt cache 活动。",
"noTrendData": "最近 24 小时还没有记录到 prompt cache 活动。",
"signature": "签名",
"model": "模型",
"created": "创建时间",

View File

@@ -582,19 +582,21 @@ export async function getCacheMetrics() {
`
SELECT
provider,
COUNT(*) as requests,
SUM(tokens_input) as inputTokens,
COUNT(*) as totalRequests,
SUM(CASE WHEN tokens_cache_read > 0 OR tokens_cache_creation > 0 THEN 1 ELSE 0 END) as cachedRequests,
SUM(CASE WHEN tokens_cache_read > 0 OR tokens_cache_creation > 0 THEN tokens_input ELSE 0 END) as inputTokens,
SUM(tokens_cache_read) as cachedTokens,
SUM(tokens_cache_creation) as cacheCreationTokens
FROM usage_history
WHERE (tokens_cache_read > 0 OR tokens_cache_creation > 0)
AND provider IS NOT NULL
WHERE provider IS NOT NULL
GROUP BY provider
HAVING cachedRequests > 0
`
)
.all() as Array<{
provider: string;
requests: number;
totalRequests: number;
cachedRequests: number;
inputTokens: number | null;
cachedTokens: number | null;
cacheCreationTokens: number | null;
@@ -638,6 +640,8 @@ export async function getCacheMetrics() {
string,
{
requests: number;
totalRequests: number;
cachedRequests: number;
inputTokens: number;
cachedTokens: number;
cacheCreationTokens: number;
@@ -645,7 +649,9 @@ export async function getCacheMetrics() {
> = {};
for (const row of byProviderRows) {
byProvider[row.provider] = {
requests: row.requests,
requests: row.cachedRequests,
totalRequests: row.totalRequests,
cachedRequests: row.cachedRequests,
inputTokens: row.inputTokens || 0,
cachedTokens: row.cachedTokens || 0,
cacheCreationTokens: row.cacheCreationTokens || 0,

View File

@@ -336,15 +336,18 @@ export function cleanOldMetrics(retentionDays = 90): number {
/**
* Clear all cache entries.
*/
export function clearCache() {
export function clearCache(): number {
getMemoryCache().clear();
let removed = 0;
try {
const db = getDbInstance();
db.prepare("DELETE FROM semantic_cache").run();
const result = db.prepare("DELETE FROM semantic_cache").run();
removed = result.changes || 0;
db.prepare("UPDATE cache_metrics SET value = 0").run();
} catch {
// DB not available
}
return removed;
}
export function getCacheStats() {

View File

@@ -123,6 +123,8 @@ describe("Cache Metrics Database", () => {
// Check provider breakdown
assert.ok(metrics.byProvider["test-provider"]);
assert.ok(metrics.byProvider["test-provider"].requests >= 2);
assert.ok(metrics.byProvider["test-provider"].totalRequests >= 2);
assert.ok(metrics.byProvider["test-provider"].cachedRequests >= 2);
assert.ok(metrics.byProvider["test-provider"].inputTokens >= 1500);
assert.ok(metrics.byProvider["test-provider"].cachedTokens >= 600);
assert.ok(metrics.byProvider["test-provider"].cacheCreationTokens >= 300);

View File

@@ -869,7 +869,9 @@ test("chatCore refreshes GitHub credentials after 401 and retries with the refre
});
const payload = await result.response.json();
const providerCalls = calls.filter((entry) => entry.url.startsWith("https://api.githubcopilot.com/"));
const providerCalls = calls.filter((entry) =>
entry.url.startsWith("https://api.githubcopilot.com/")
);
assert.equal(result.success, true);
assert.equal(providerCalls.length, 2);
@@ -1096,6 +1098,49 @@ test("chatCore returns a semantic cache HIT for repeated deterministic requests"
assert.equal(payload.choices[0].message.content, "cached-once");
});
test("chatCore skips semantic cache when disabled in settings", async () => {
await settingsDb.updateSettings({ semanticCacheEnabled: false });
let upstreamHits = 0;
const sharedBody = {
model: "gpt-4o-mini",
stream: false,
temperature: 0,
messages: [{ role: "user", content: "do not reuse this response locally" }],
};
const first = await invokeChatCore({
provider: "openai",
model: "gpt-4o-mini",
body: sharedBody,
responseFormat: "openai",
responseFactory() {
upstreamHits += 1;
return buildOpenAIResponse(false, `fresh-${upstreamHits}`);
},
});
const second = await invokeChatCore({
provider: "openai",
model: "gpt-4o-mini",
body: sharedBody,
responseFormat: "openai",
responseFactory() {
upstreamHits += 1;
return buildOpenAIResponse(false, `fresh-${upstreamHits}`);
},
});
assert.equal(first.calls.length, 1);
assert.equal(second.calls.length, 1);
assert.equal(upstreamHits, 2);
assert.equal(first.result.response.headers.get("X-OmniRoute-Cache"), "MISS");
assert.equal(second.result.response.headers.get("X-OmniRoute-Cache"), "MISS");
const payload = await second.result.response.json();
assert.equal(payload.choices[0].message.content, "fresh-2");
});
test("chatCore normalizes tool finish reasons and estimates usage when upstream omits it", async () => {
const { result } = await invokeChatCore({
provider: "openai",