From 737afa047b2c5a6ced9142618f9fb2292df700a4 Mon Sep 17 00:00:00 2001 From: chefgroep Date: Sun, 4 Oct 2026 23:48:45 +0200 Subject: [PATCH] feat(trace): cover live call create --- docs-site/src/content/docs/reference/cli.md | 6 ++++-- src/server/index.ts | 5 ++++- src/server/live.ts | 3 +++ tests/server-live.test.ts | 15 +++++++++++++++ 4 files changed, 26 insertions(+), 3 deletions(-) diff --git a/docs-site/src/content/docs/reference/cli.md b/docs-site/src/content/docs/reference/cli.md index 4abfab58f..c32050123 100644 --- a/docs-site/src/content/docs/reference/cli.md +++ b/docs-site/src/content/docs/reference/cli.md @@ -534,8 +534,10 @@ 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; the inbound request -and response are captured for `/v1/responses`, `/v1/messages`, and `/v1/chat/completions`. +Outbound capture covers adapters that send through the shared upstream fetch helper; inbound request and +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. Voice/realtime sideband WebSockets remain outside this trace lane. ## Updating diff --git a/src/server/index.ts b/src/server/index.ts index 0156c1510..dc0bd6126 100644 --- a/src/server/index.ts +++ b/src/server/index.ts @@ -1424,7 +1424,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 e6c8361fb..539a7678f 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 ba97184b5..fcedc799b 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);