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
6 changes: 4 additions & 2 deletions docs-site/src/content/docs/reference/cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -535,13 +535,15 @@ 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 and response
capture covers `/v1/responses`, `/v1/messages`, `/v1/chat/completions`, and `/v1/responses/compact`.
capture covers `/v1/responses`, `/v1/messages`, `/v1/chat/completions`, `/v1/responses/compact`, and live call-create HTTP
(`/v1/live` and `/v1/realtime/calls`).
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.
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.
request/response turns. Live call-create also records the rewritten provider wire body before its bounded
upstream POST.

Stored traces can be inspected directly from the local SQLite store; the proxy does not need to be running:

Expand Down
5 changes: 4 additions & 1 deletion src/server/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1466,7 +1466,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