diff --git a/src/server/responses/compact.ts b/src/server/responses/compact.ts index 9a49a03f7..447a0371c 100644 --- a/src/server/responses/compact.ts +++ b/src/server/responses/compact.ts @@ -86,10 +86,10 @@ import { redactSecretString } from "../../lib/redact"; import { readBoundedResponseBody } from "../../lib/bounded-body"; import { supportedLadderFor } from "../effort-policy"; import { + applyResponseLogMetadata, beginRequestAttempt, catalogModelSupportsServiceTier, finishRequestAttempt, - inspectResponseLogJson, noteAttemptSend, readConfiguredCodexServiceTier, requestLogSpeedLabel, @@ -506,6 +506,17 @@ export async function handleResponsesCompact( // Always record the real upstream status: a local buffering failure after a // 200 upstream response must not soft-avoid a healthy account or rotate a thread. recordCompactPoolOutcome(outcomeCtx, upstream.status, { retryAfter, resetAt }); + // Lift usage and response metadata from the buffered upstream JSON into the + // request log; the routed branch gets the same through handleResponses. Only + // the parsed fields are read: a compact body is replacement history derived + // from the conversation, so it must never reach the usage debug body sampler. + if (buffered.ok) { + try { + applyResponseLogMetadata(logCtx, JSON.parse(await buffered.clone().text())); + } catch { + /* body may not be JSON; usage stays unreported */ + } + } return buffered; } diff --git a/tests/responses-compaction-routing.test.ts b/tests/responses-compaction-routing.test.ts index 7003fa11f..9b50412dc 100644 --- a/tests/responses-compaction-routing.test.ts +++ b/tests/responses-compaction-routing.test.ts @@ -25,6 +25,7 @@ import { } from "../src/codex/auth-context"; import { supportsNativeResponsesCompactEndpoint } from "../src/providers/openai-tiers"; import { acquireNativeMainProfileDrain, tryAdmitTurn } from "../src/server/lifecycle"; +import type { RequestLogContext } from "../src/server/request-log"; import type { OcxConfig, OcxProviderConfig } from "../src/types"; const originalFetch = globalThis.fetch; @@ -166,6 +167,44 @@ describe("supportsNativeResponsesCompactEndpoint (#422)", () => { }); }); +describe("native compact usage reporting", () => { + test("the buffered upstream body fills the request log usage and stays intact for the client", async () => { + const config = { + defaultProvider: "openai-apikey", + providers: { + "openai-apikey": { + adapter: "openai-responses", + baseUrl: "https://api.openai.com/v1", + authMode: "key", + apiKey: "sk-test", + }, + }, + } as unknown as OcxConfig; + globalThis.fetch = (async () => jsonResponse(completedPayload("native summary"))) as typeof fetch; + const logCtx: RequestLogContext = { model: "", provider: "" }; + const previousUsageDebug = process.env.OPENCODEX_USAGE_DEBUG; + process.env.OPENCODEX_USAGE_DEBUG = "1"; + let response: Response; + try { + response = await handleResponsesCompact( + compactionRequest(baseCompactionBody({ model: "openai-apikey/gpt-5.5" })), + config, + logCtx, + ); + } finally { + if (previousUsageDebug === undefined) delete process.env.OPENCODEX_USAGE_DEBUG; + else process.env.OPENCODEX_USAGE_DEBUG = previousUsageDebug; + } + expect(response.status).toBe(200); + expect(await response.json()).toEqual(completedPayload("native summary")); + expect(logCtx.usage).toMatchObject({ inputTokens: 10, outputTokens: 5, totalTokens: 15 }); + // The compact body is replacement history; even with usage debug on it must + // never be sampled into the debug log. + expect(logCtx.usageDebugBodyKind).toBeUndefined(); + expect(logCtx.usageDebugBodySample).toBeUndefined(); + }); +}); + describe("native Codex pool compaction", () => { test("keeps a Spark reset cooldown separate from a Terra compact request (#590)", async () => { const testDir = mkdtempSync(join(tmpdir(), "ocx-compact-scope-"));