diff --git a/docs-site/src/content/docs/reference/cli.md b/docs-site/src/content/docs/reference/cli.md index 6ed837f2..95e67f74 100644 --- a/docs-site/src/content/docs/reference/cli.md +++ b/docs-site/src/content/docs/reference/cli.md @@ -545,8 +545,11 @@ Tracing is **off by default** and is configured by environment variable at proxy detectable, so treat the store as sensitive. - `full` stores bodies verbatim. Use it deliberately and briefly. -Outbound capture covers adapters that send through the shared upstream fetch helper; inbound and response -capture covers `/v1/responses`, `/v1/messages`, `/v1/chat/completions`, and `/v1/responses/compact`. +Outbound capture covers adapters that send through the shared upstream fetch helper; inbound request and +response capture covers `/v1/responses`, `/v1/messages`, and `/v1/chat/completions`. Responses WebSocket +`response.create` turns are traced as separate logical requests: the inbound hash covers the client WS frame, +while the outbound hash covers the final provider wire body. Voice/realtime live-sideband WebSockets remain +outside this trace lane because they are long-lived bidirectional sessions rather than request/response turns. Proxy response-cache hits are traced without duplicating the cached response body: the usage trace records `cacheHit`, a response hash/byte count, and the request id that originally populated the cache when known. WebSocket/live/realtime traffic is not yet covered. diff --git a/src/server/index.ts b/src/server/index.ts index ec15c648..b73f6064 100644 --- a/src/server/index.ts +++ b/src/server/index.ts @@ -1673,11 +1673,10 @@ export function startServer(port?: number) { ws.close(1009, "message too large"); return; } + const rawText = typeof raw === "string" ? raw : raw.toString(); let frame: Record; try { - frame = JSON.parse( - typeof raw === "string" ? raw : raw.toString(), - ) as Record; + frame = JSON.parse(rawText) as Record; } catch { return; // text-only contract; ignore unparseable frames } @@ -1735,6 +1734,12 @@ export function startServer(port?: number) { headers: fwd, body: JSON.stringify({ ...payload, stream: true }), }); + const traceReq = new Request("http://localhost/v1/responses", { + method: "POST", + headers: { "content-type": "application/json" }, + body: rawText, + }); + await beginTrace(logCtx, traceReq); try { let terminalRecorder: | (( @@ -1742,17 +1747,19 @@ export function startServer(port?: number) { httpStatusOverride?: number, ) => void) | undefined; - const response = await handleResponses(req, config, logCtx, { - forceEmptyResponseId: true, - abortSignal: turnAbort.signal, - onFirstOutput: () => recordFirstOutput(logCtx, start), - onCodexAuthContextResolved: (context) => - updateCodexWebSocketAuthContext(ws, context), - recordTerminalOutcomes: false, - setTerminalOutcomeRecorder: (recorder) => { - terminalRecorder = recorder; - }, - }); + const response = await runWithTrace(logCtx, () => + handleResponses(req, config, logCtx, { + forceEmptyResponseId: true, + abortSignal: turnAbort.signal, + onFirstOutput: () => recordFirstOutput(logCtx, start), + onCodexAuthContextResolved: (context) => + updateCodexWebSocketAuthContext(ws, context), + recordTerminalOutcomes: false, + setTerminalOutcomeRecorder: (recorder) => { + terminalRecorder = recorder; + }, + }), + ); await sendResponseToWebSocket(ws, response, isCurrent, { onSsePayload: (payload) => inspectResponseLogSsePayload(logCtx, payload), diff --git a/tests/openai-provider-option-e2e.test.ts b/tests/openai-provider-option-e2e.test.ts index c865f2eb..406c9ece 100644 --- a/tests/openai-provider-option-e2e.test.ts +++ b/tests/openai-provider-option-e2e.test.ts @@ -118,6 +118,7 @@ describe("OpenAI provider-option integration spine", () => { OPENCODEX_HOME: process.env.OPENCODEX_HOME, CODEX_HOME: process.env.CODEX_HOME, CLAUDE_CONFIG_DIR: process.env.CLAUDE_CONFIG_DIR, + OCX_TRACE: process.env.OCX_TRACE, }; const savedFetch = globalThis.fetch; const captures: Capture[] = []; @@ -154,6 +155,7 @@ describe("OpenAI provider-option integration spine", () => { process.env.OPENCODEX_HOME = opencodexHome; process.env.CODEX_HOME = codexHome; process.env.CLAUDE_CONFIG_DIR = claudeConfigDir; + process.env.OCX_TRACE = "metadata"; const authPath = join(codexHome, "auth.json"); writeFileSync(authPath, JSON.stringify({ tokens: { access_token: "fixture-main-access", account_id: "fixture-main-account" }, @@ -362,6 +364,15 @@ describe("OpenAI provider-option integration spine", () => { body: { model: "gpt-5.6-sol" }, }); expect(websocketRegistry.getTrackedCodexWebSocketCountForAccount("fixture-pool")).toBe(1); + const wsTrace = requestLog.getRequestLogEntries().at(-1)?.trace; + expect(wsTrace).toMatchObject({ + mode: "metadata", + stored: false, + responseBytes: expect.any(Number), + }); + expect(wsTrace?.requestHash).toMatch(/^[0-9a-f]{32}$/); + expect(wsTrace?.outboundHash).toMatch(/^[0-9a-f]{32}$/); + expect(wsTrace?.responseHash).toMatch(/^[0-9a-f]{32}$/); const directPatch = await patchMode("direct"); expect(directPatch.status).toBe(200); @@ -581,6 +592,7 @@ describe("OpenAI provider-option integration spine", () => { restoreEnv("OPENCODEX_HOME", previousEnv.OPENCODEX_HOME); restoreEnv("CODEX_HOME", previousEnv.CODEX_HOME); restoreEnv("CLAUDE_CONFIG_DIR", previousEnv.CLAUDE_CONFIG_DIR); + restoreEnv("OCX_TRACE", previousEnv.OCX_TRACE); rmSync(root, { recursive: true, force: true }); expect(hashTree(realClaudeDir)).toBe(realClaudeHashBefore); } diff --git a/tests/ws-endpoint.test.ts b/tests/ws-endpoint.test.ts index 8da317dc..62cdfd3c 100644 --- a/tests/ws-endpoint.test.ts +++ b/tests/ws-endpoint.test.ts @@ -59,6 +59,8 @@ describe("WS endpoint re-framer (120/132)", () => { expect(source).toContain( "finalizeLog(turnAbort.signal.aborted ? 499 : response.status);", ); + expect(source).toContain("await beginTrace(logCtx, traceReq);"); + expect(source).toContain("const response = await runWithTrace(logCtx, () =>"); }); test("generate=false warmup completes locally without upstream and forces full next request", () => {