diff --git a/docs-site/src/content/docs/reference/cli.md b/docs-site/src/content/docs/reference/cli.md index 95e67f74..a614cd6e 100644 --- a/docs-site/src/content/docs/reference/cli.md +++ b/docs-site/src/content/docs/reference/cli.md @@ -546,10 +546,12 @@ Tracing is **off by default** and is configured by environment variable at proxy - `full` stores bodies verbatim. Use it deliberately and briefly. 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. +response capture covers `/v1/responses`, `/v1/messages`, `/v1/chat/completions`, and live call-create HTTP +(`/v1/live` and `/v1/realtime/calls`). Live call-create also records the rewritten provider wire body before +its bounded upstream POST. 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 b73f6064..b5efb86b 100644 --- a/src/server/index.ts +++ b/src/server/index.ts @@ -1461,7 +1461,10 @@ export function startServer(port?: number) { model: "gpt-live", provider: "unknown", }; - const response = await handleLive(req, config, logCtx); + await beginTrace(logCtx, req); + const response = await runWithTrace(logCtx, () => + handleLive(req, config, logCtx), + ); addFinalRequestLog( requestId, start, diff --git a/src/server/live.ts b/src/server/live.ts index e6c8361f..539a7678 100644 --- a/src/server/live.ts +++ b/src/server/live.ts @@ -32,6 +32,7 @@ import { import { formatCodexProviderForLog } from "../codex/routing"; import { signalWithTimeout } from "../lib/abort"; import { sidecarEnter } from "../lib/sidecar-tracker"; +import { appendTraceResponse, noteOutboundRequestBody } from "../trace/capture"; import type { OcxConfig } from "../types"; import { resolveFirstUsableOpenAiSidecar, selectOpenAiImagesProvider } from "../providers/openai-sidecar"; import { ForwardAdmissionCredentialError, validateForwardAdmissionCredential } from "./auth-cors"; @@ -490,6 +491,7 @@ export async function handleLive( const linkedSignal = signalWithTimeout(LIVE_UPSTREAM_TIMEOUT_MS, req.signal); const sidecarExit = sidecarEnter("live"); try { + noteOutboundRequestBody(new Uint8Array(outboundBody)); const upstreamResponse = await fetch(url, { method: "POST", headers, @@ -505,6 +507,7 @@ export async function handleLive( total => `live response too large (${total} bytes)`, ); if (payload instanceof Response) return payload; + appendTraceResponse(logCtx.trace, new TextDecoder().decode(payload)); const relayHeaders: Record = {}; for (const name of LIVE_RELAY_HEADERS) { const value = upstreamResponse.headers.get(name); diff --git a/tests/server-live.test.ts b/tests/server-live.test.ts index ba97184b..fcedc799 100644 --- a/tests/server-live.test.ts +++ b/tests/server-live.test.ts @@ -16,6 +16,7 @@ import { } from "../src/codex/routing"; import { saveConfig } from "../src/config"; import { startServer } from "../src/server"; +import { clearRequestLogsForTests, getRequestLogEntries } from "../src/server/request-log"; import type { OcxConfig } from "../src/types"; import { fakeChatGptJwt } from "./helpers/fake-chatgpt-jwt"; import { @@ -25,6 +26,7 @@ import { const previousApiToken = process.env.OPENCODEX_API_AUTH_TOKEN; const previousOpencodexHome = process.env.OPENCODEX_HOME; +const previousTraceMode = process.env.OCX_TRACE; const originalFetch = globalThis.fetch; const TEST_DIR = join(import.meta.dir, ".tmp-server-live-test"); let isolatedCodexHome: IsolatedCodexHome | null = null; @@ -35,6 +37,8 @@ beforeEach(() => { mkdirSync(TEST_DIR, { recursive: true }); process.env.OPENCODEX_HOME = TEST_DIR; delete process.env.OPENCODEX_API_AUTH_TOKEN; + delete process.env.OCX_TRACE; + clearRequestLogsForTests(); isolatedCodexHome = installIsolatedCodexHome("ocx-server-live-codex-"); clearCodexUpstreamHealth(); clearThreadAccountMap(); @@ -50,6 +54,8 @@ afterEach(() => { else process.env.OPENCODEX_API_AUTH_TOKEN = previousApiToken; if (previousOpencodexHome === undefined) delete process.env.OPENCODEX_HOME; else process.env.OPENCODEX_HOME = previousOpencodexHome; + if (previousTraceMode === undefined) delete process.env.OCX_TRACE; + else process.env.OCX_TRACE = previousTraceMode; isolatedCodexHome?.restore(); isolatedCodexHome = null; clearCodexUpstreamHealth(); @@ -167,6 +173,7 @@ function multipartLiveBody( } test("POST /v1/live rewrites ChatGPT multipart into backend realtime/calls JSON", async () => { + process.env.OCX_TRACE = "metadata"; const captured: CapturedRequest[] = []; const upstream = fakeLiveUpstream(captured); saveConfig(forwardConfig()); @@ -205,6 +212,14 @@ test("POST /v1/live rewrites ChatGPT multipart into backend realtime/calls JSON" sdp: "v=0-offer", session: { model: "gpt-live", instructions: "hi" }, }); + const liveTrace = getRequestLogEntries().at(-1)?.trace; + expect(liveTrace).toMatchObject({ mode: "metadata", stored: false }); + expect(liveTrace?.requestHash).toMatch(/^[0-9a-f]{32}$/); + expect(liveTrace?.outboundHash).toMatch(/^[0-9a-f]{32}$/); + expect(liveTrace?.responseHash).toMatch(/^[0-9a-f]{32}$/); + expect(liveTrace?.requestBytes).toBeGreaterThan(0); + expect(liveTrace?.outboundBytes).toBeGreaterThan(0); + expect(liveTrace?.responseBytes).toBeGreaterThan(0); } finally { await server.stop(true); await upstream.stop(true);