Files
OmniRoute/open-sse/handlers/chatCore/semanticCache.ts
Bob.Hou 2b2d947faf fix(cache): add latency marker + per-key bypass for semantic cache (#8984)
* fix(cache): add latency marker + per-key bypass for semantic cache

Semantic cache silently corrupts latency measurements: a 10s upstream
call served from cache looks like 19ms. Three fixes:

A. Latency marker: cache HIT responses now carry
   X-OmniRoute-Cache-Latency: synthetic so measurement tools can
   distinguish real vs cached latency.

B. Per-key bypass: new apiKeys.cacheDefaultMode ('legacy' | 'bypass')
   lets latency-sensitive clients opt out of cache reads entirely.
   - DB column + migration (134)
   - rowParser parseCacheDefaultMode
   - API create default + PATCH update
   - checkSemanticCache returns null on bypass

C. Type safety: ApiKeyRow/ApiKeyView/params updated, superRefine
   guard includes cacheDefaultMode.

Cache write path intentionally unchanged: apiKeyId is already in the
cache signature (semanticCache.ts:140), so per-key isolation prevents
cross-key pollution.

Changed test files:
- tests/unit/chatcore-semantic-cache.test.ts (3 new tests)

Signed-off-by: Minxi Hou <houminxi@gmail.com>

* docs: document semantic cache latency impact + bypass configuration

---------

Signed-off-by: Minxi Hou <houminxi@gmail.com>
2026-08-11 09:55:07 -03:00

108 lines
3.9 KiB
TypeScript

import {
generateSignature,
getCachedResponse,
isCacheableForRead,
} from "@/lib/semanticCache";
import { calculateCost } from "@/lib/usage/costCalculator";
import { trackPendingRequest } from "@/lib/usageDb";
import { synthesizeOpenAiSseFromJson } from "../../utils/jsonToSse.ts";
import { attachOmniRouteMetaHeaders } from "@/domain/omnirouteResponseMeta";
import { extractUsageFromResponse } from "../usageExtractor.ts";
import { OMNIROUTE_RESPONSE_HEADERS } from "@/shared/constants/headers";
export async function checkSemanticCache({
semanticCacheEnabled,
body,
clientRawRequest,
model,
provider,
stream,
reqLogger,
effectiveServiceTier,
connectionId,
startTime,
log,
persistAttemptLogs,
apiKeyId,
cacheDefaultMode,
}: {
semanticCacheEnabled: boolean;
// Only the fields this read path actually touches are named; everything else
// on the request body stays `unknown` via the index signature.
body: Record<string, unknown> & { temperature?: number; top_p?: number };
clientRawRequest: { headers?: unknown } | null;
model: string;
provider: string;
stream: boolean;
reqLogger: { logConvertedResponse: (response: Record<string, unknown>) => void };
effectiveServiceTier: string | null | undefined;
connectionId: string | null;
startTime: number;
log: { debug?: (...args: unknown[]) => void } | null;
persistAttemptLogs: (args: unknown) => void;
apiKeyId?: string | null;
cacheDefaultMode?: "legacy" | "bypass" | null;
}) {
// Per-key bypass: skip cache lookup entirely when the API key opts out.
if (cacheDefaultMode === "bypass") return null;
if (semanticCacheEnabled && isCacheableForRead(body, clientRawRequest?.headers)) {
const signature = generateSignature(
model,
body.messages ?? body.input,
body.temperature,
body.top_p,
apiKeyId ?? undefined
);
const cached = getCachedResponse(signature);
if (cached) {
log?.debug?.("CACHE", `Semantic cache HIT for ${model} (stream=${stream})`);
reqLogger.logConvertedResponse(cached as Record<string, unknown>);
const cachedUsage =
extractUsageFromResponse(cached as Record<string, unknown>, provider) ||
((cached as Record<string, unknown>)?.usage as Record<string, unknown> | undefined);
const cachedCost = cachedUsage
? await calculateCost(provider, model, cachedUsage as Record<string, number>, {
serviceTier: effectiveServiceTier,
})
: 0;
persistAttemptLogs({
status: 200,
tokens: (cached as Record<string, unknown>)?.usage,
responseBody: cached,
providerRequest: null,
providerResponse: null,
clientResponse: cached,
cacheSource: "semantic",
});
trackPendingRequest(model, provider, connectionId, false);
const cachedSse = stream ? synthesizeOpenAiSseFromJson(JSON.stringify(cached)) : "";
const headers: Record<string, string> = {
"Content-Type": cachedSse ? "text/event-stream" : "application/json",
[OMNIROUTE_RESPONSE_HEADERS.cache]: "HIT",
// Marker for latency measurement tools: this response served from cache
// has synthetic (near-zero) latency, not real upstream latency.
[OMNIROUTE_RESPONSE_HEADERS.cacheLatency]: "synthetic",
};
// A cache HIT serves WITHOUT an upstream call, so the incremental cost billed to
// the client is 0 (consumers that sum X-OmniRoute-Response-Cost must not charge for
// hits). The original/would-have-been cost is surfaced via X-OmniRoute-Cost-Saved.
attachOmniRouteMetaHeaders(headers, {
provider,
model,
cacheHit: true,
latencyMs: Date.now() - startTime,
usage: cachedUsage,
costUsd: 0,
costSavedUsd: cachedCost,
});
return {
success: true,
response: new Response(cachedSse || JSON.stringify(cached), {
headers,
}),
};
}
}
return null;
}