fix(agents): bound google prompt cache expiry

This commit is contained in:
Peter Steinberger
2026-05-30 14:02:50 -04:00
parent 30e3ca08a5
commit 1ee751ddb1
2 changed files with 64 additions and 6 deletions

View File

@@ -385,6 +385,53 @@ describe("google prompt cache", () => {
expect(getCapturedPayload()?.cachedContent).toBe("cachedContents/system-cache-3");
});
it("does not bypass failed-cache backoff when the process clock is invalid", async () => {
const systemPromptDigest = crypto.createHash("sha256").update("Follow policy.").digest("hex");
const sessionManager = makeSessionManager([
{
id: "entry-1",
parentId: null,
timestamp: new Date(1_000).toISOString(),
type: "custom",
customType: "openclaw.google-prompt-cache",
data: {
status: "failed",
timestamp: 1_000,
provider: "google",
modelId: "gemini-3.1-pro-preview",
modelApi: "google-generative-ai",
baseUrl: "https://generativelanguage.googleapis.com/v1beta",
systemPromptDigest,
cacheRetention: "long",
retryAfter: Date.parse("2030-01-01T00:00:00.000Z"),
},
},
]);
const fetchMock = createCacheFetchMock({
name: "cachedContents/system-cache-invalid-clock",
expireTime: "2030-01-01T00:00:00.000Z",
});
const innerStreamFn = vi.fn(() => "stream" as never);
const wrapped = await preparePromptCacheStream({
fetchMock,
now: Number.NaN,
sessionManager,
streamFn: innerStreamFn,
});
await Promise.resolve(
wrapped?.(
makeGoogleModel(),
{ systemPrompt: "Follow policy.", messages: [] } as never,
{} as never,
),
);
expect(fetchMock).not.toHaveBeenCalled();
expect(innerStreamFn).toHaveBeenCalledTimes(1);
});
it("stays out of the way when cachedContent is already configured explicitly", async () => {
const fetchMock = vi.fn();

View File

@@ -3,6 +3,11 @@ import { parseGeminiAuth } from "../../infra/gemini-auth.js";
import { normalizeGoogleApiBaseUrl } from "../../infra/google-api-base-url.js";
import { streamWithPayloadPatch } from "../../llm/providers/stream-wrappers/stream-payload-utils.js";
import type { Model } from "../../llm/types.js";
import {
asDateTimestampMs,
isFutureDateTimestampMs,
resolveExpiresAtMsFromDurationMs,
} from "../../shared/number-coercion.js";
import { normalizeOptionalString } from "../../shared/string-coerce.js";
import { buildGuardedModelFetch } from "../provider-transport-fetch.js";
import type { StreamFn } from "../runtime/index.js";
@@ -185,8 +190,7 @@ function parseExpireTimeMs(expireTime: string | undefined): number | null {
if (!expireTime) {
return null;
}
const timestamp = Date.parse(expireTime);
return Number.isFinite(timestamp) ? timestamp : null;
return asDateTimestampMs(Date.parse(expireTime)) ?? null;
}
function convertManagedGoogleTools(tools: NonNullable<GooglePromptCacheContext["tools"]>) {
@@ -342,7 +346,10 @@ async function ensureGooglePromptCache(
deps: GooglePromptCacheDeps,
): Promise<string | null> {
const baseUrl = normalizeGoogleApiBaseUrl(params.model.baseUrl);
const now = deps.now?.() ?? Date.now();
const now = asDateTimestampMs(deps.now?.() ?? Date.now());
if (now === undefined) {
return null;
}
const systemPromptDigest = digestSystemPrompt(params.systemPrompt);
const matchKey = buildGooglePromptCacheMatchKey({
provider: params.provider,
@@ -354,7 +361,10 @@ async function ensureGooglePromptCache(
});
const latestEntry = readLatestGooglePromptCacheEntry(params.sessionManager, matchKey);
if (latestEntry?.status === "failed" && latestEntry.retryAfter > now) {
if (
latestEntry?.status === "failed" &&
isFutureDateTimestampMs(latestEntry.retryAfter, { nowMs: now })
) {
return null;
}
@@ -362,7 +372,7 @@ async function ensureGooglePromptCache(
const refreshWindowMs = resolveGooglePromptCacheRefreshWindowMs(params.cacheRetention);
if (latestEntry?.status === "ready" && latestEntry.cachedContent) {
const expiresAt = parseExpireTimeMs(latestEntry.expireTime);
const isExpired = expiresAt !== null && expiresAt <= now;
const isExpired = expiresAt !== null && !isFutureDateTimestampMs(expiresAt, { nowMs: now });
if (!isExpired) {
const needsRefresh = expiresAt !== null && expiresAt - now <= refreshWindowMs;
if (!needsRefresh) {
@@ -420,7 +430,8 @@ async function ensureGooglePromptCache(
systemPromptDigest,
cacheConfigDigest: params.cacheConfigDigest,
cacheRetention: params.cacheRetention,
retryAfter: now + GOOGLE_PROMPT_CACHE_RETRY_BACKOFF_MS,
retryAfter:
resolveExpiresAtMsFromDurationMs(GOOGLE_PROMPT_CACHE_RETRY_BACKOFF_MS, { nowMs: now }) ?? 0,
});
return null;
}