diff --git a/packages/coding-agent/src/cli/bench-cli.ts b/packages/coding-agent/src/cli/bench-cli.ts index 36aedd286..836890615 100644 --- a/packages/coding-agent/src/cli/bench-cli.ts +++ b/packages/coding-agent/src/cli/bench-cli.ts @@ -52,6 +52,7 @@ const BENCH_PROMPT = benchPrompt.trim(); const UTF8_ENCODER = new TextEncoder(); const UTF8_DECODER = new TextDecoder(); const CACHE_PREFIX_CHUNK = cachePrefixChunk; +const CACHE_PREFIX_PLACEHOLDER = "__OMP_CACHE_BENCH_RAW_PREFIX__"; const CACHE_PREFIX_CHUNK_BYTES = UTF8_ENCODER.encode(CACHE_PREFIX_CHUNK).byteLength; const RESPONSE_CACHE_STATUS_HEADERS = ["cf-aig-cache-status"] as const; @@ -341,6 +342,19 @@ function generatedCachePrefix(bytes: number): string { return truncateUtf8(CACHE_PREFIX_CHUNK.repeat(Math.ceil(bytes / CACHE_PREFIX_CHUNK_BYTES)), bytes); } +function renderCacheBenchmarkPrefix(prefix: string, namespace: string): string { + const rendered = prompt.render(cachePrefixTemplate, { + prefix: CACHE_PREFIX_PLACEHOLDER, + namespace, + }); + if (!rendered.includes(CACHE_PREFIX_PLACEHOLDER)) { + throw new Error("Cache benchmark prefix template is missing its raw prefix placeholder"); + } + // Render the static wrapper first, then inject caller bytes so prompt + // normalization cannot trim spaces or collapse blank lines in prefix files. + return rendered.replace(CACHE_PREFIX_PLACEHOLDER, prefix); +} + async function resolveCachePrefix( flags: BenchCommandArgs["flags"], readTextFile: (path: string, maxBytes: number) => Promise, @@ -821,9 +835,7 @@ export async function runBenchCommand(command: BenchCommandArgs, deps: BenchDepe async (pairIndex): Promise => { const cacheNamespace = randomSessionId(); const promptCacheKey = `bench-cache:${cacheNamespace}`; - const stablePrefix = prompt - .render(cachePrefixTemplate, { prefix: cachePrefix!, namespace: cacheNamespace }) - .trimEnd(); + const stablePrefix = renderCacheBenchmarkPrefix(cachePrefix!, cacheNamespace); const coldSuffix = prompt.render(cacheSuffixTemplate, { variant: "A" }).trim(); const warmSuffix = prompt.render(cacheSuffixTemplate, { variant: "B" }).trim(); const coldCapture: CacheRequestCapture = { diff --git a/packages/coding-agent/test/bench-cache.test.ts b/packages/coding-agent/test/bench-cache.test.ts index 5c04dbd99..26cad8383 100644 --- a/packages/coding-agent/test/bench-cache.test.ts +++ b/packages/coding-agent/test/bench-cache.test.ts @@ -494,6 +494,45 @@ describe("bench cache mode", () => { expect(stablePrefixes[0]).not.toContain("\uFFFD"); }); + it("preserves significant whitespace from the default prefix-file reader", async () => { + const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), "omp-bench-cache-prefix-whitespace-")); + const prefixPath = path.join(tempDir, "prefix.txt"); + const exactPrefix = "line one \n\n\nline two\t\n"; + const stablePrefixes: string[] = []; + await Bun.write(prefixPath, exactPrefix); + try { + await runBenchCommand( + { + models: ["openai/gpt-cache-test"], + flags: { cache: true, cachePrefixFile: prefixPath, cachePrefixBytes: 128, json: true }, + }, + { + createRuntime: async () => ({ modelRegistry: registry, close: () => {} }), + randomSessionId: (() => { + let id = 0; + return () => `session-${++id}`; + })(), + writeStdout: () => {}, + writeStderr: () => {}, + setExitCode: () => {}, + streamSimple: (_model, context, options) => { + stablePrefixes.push(context.messages[0]?.content as string); + void options?.onPayload?.({ input: context.messages }); + return streamWithMessage(successfulMessage(0, 0)); + }, + stdoutIsTTY: false, + }, + ); + } finally { + await fs.rm(tempDir, { recursive: true, force: true }); + } + + expect(stablePrefixes).toHaveLength(2); + expect(stablePrefixes[0]).toBe(stablePrefixes[1]); + expect(stablePrefixes[0]?.startsWith(exactPrefix)).toBe(true); + expect(stablePrefixes[0]?.slice(exactPrefix.length)).toStartWith("\n\nPrompt-cache benchmark namespace:"); + }); + it("does not turn zero cache counters into a miss", async () => { const summary = await runBenchCommand( { models: ["openai/gpt-cache-test"], flags: { cache: true, json: true } },