Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 6 additions & 4 deletions docs-site/src/content/docs/reference/cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
5 changes: 4 additions & 1 deletion src/server/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
3 changes: 3 additions & 0 deletions src/server/live.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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,
Expand All @@ -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<string, string> = {};
for (const name of LIVE_RELAY_HEADERS) {
const value = upstreamResponse.headers.get(name);
Expand Down
15 changes: 15 additions & 0 deletions tests/server-live.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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;
Expand All @@ -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();
Expand All @@ -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();
Expand Down Expand Up @@ -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());
Expand Down Expand Up @@ -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);
Expand Down
Loading