codex-copilot-dx 0.7.3 → 0.7.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -172,6 +172,8 @@ curl -s http://127.0.0.1:2026/_ccdx/status | jq
172
172
 
173
173
  The status endpoint is restricted to the socket's real loopback address even when LAN binding is explicitly enabled. It reports fixed-size request counters, bounded TTFT/TPOT histograms, admission pressure, memory use, response-history size, image queue/cache and adaptive-history state, model-cache counts, and token expiry state. It never retains metric samples or includes prompts, completions, tool arguments, image content, account names, or token values.
174
174
 
175
+ `stream_performance.by_route` also reports `request_ttft_ms`, measured from adapter entry to the first observed output delta, including local preparation. The existing `ttft_ms` continues to start at upstream dispatch. Neither timestamp means the client has drained or displayed the output. `preparation_ms` contains bounded histograms for admission (including history reservations), body reading/decompression/JSON parsing, history materialization/compact preparation, image optimization, and request serialization. Each sample measures one operation, including failed operations; retries can add samples. These are diagnostic intervals rather than a complete breakdown of request time: body reading includes internal decoded-body admission waits; authentication, routing, and other preparation are not individually timed. Non-streaming requests contribute preparation samples without inventing a first-token measurement.
176
+
175
177
  Every API response includes a safe `X-Request-Id` for correlation. To include the same ID in related terminal and file log lines, start with:
176
178
 
177
179
  ```bash
@@ -295,6 +297,8 @@ npm run verify
295
297
  npm run bench:payload
296
298
  ```
297
299
 
300
+ `bench:check` covers both SSE parsers, including native Responses identity rewriting, 1/4/8 MiB fragmented events, write batching, and slow-client backpressure. It also repeats four concurrent image preparations across six rounds using a desktop-sized synthetic screenshot and a public-domain photograph. Checks enforce content preservation, single-pass JSON parsing, bounded copying, cache reuse, settled work, and bounded retained heap/ArrayBuffers after warm-up and GC. Sampled RSS and absolute timings remain report-only because allocators, runtimes, and machines differ; the fixtures do not establish production latency or visual model accuracy.
301
+
298
302
  `npm test` runs unit and handler-level tests. `npm run test:smoke` starts a real local adapter with fully injected offline upstreams. `npm run bench:check` enforces linear SSE scanning, image/tool processing, and request-admission resource limits. `npm run pack:check` verifies npm tarball contents without publishing. `npm run bench:payload` is a report-only isolated-process benchmark for 5–60 MiB image payloads and does not contact Copilot. CI runs verification on the supported Node.js release lines.
299
303
 
300
304
  From a source checkout, `node scripts/codex-message-id-replay.mjs --codex /absolute/path/to/codex` checks message identity against an installed Codex runtime using only synthetic SSE, a local mock server, and automatically cleaned temporary Codex directories. It does not use the real client configuration or call Copilot.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codex-copilot-dx",
3
- "version": "0.7.3",
3
+ "version": "0.7.4",
4
4
  "description": "Use Codex App with GitHub Copilot",
5
5
  "author": "Dale Xiao",
6
6
  "type": "module",
@@ -12,6 +12,8 @@
12
12
  "bin/",
13
13
  "src/",
14
14
  "scripts/benchmark.mjs",
15
+ "scripts/benchmark-probes.mjs",
16
+ "scripts/fixtures/",
15
17
  "README.md"
16
18
  ],
17
19
  "keywords": [
@@ -0,0 +1,204 @@
1
+ import { createHash } from "node:crypto";
2
+ import { EventEmitter } from "node:events";
3
+ import fs from "node:fs";
4
+ import { setImmediate as immediate } from "node:timers/promises";
5
+ import sharp from "sharp";
6
+ import { proxyCopilotResponses } from "../src/responses-proxy.mjs";
7
+ import { clearResponseHistoryForTests } from "../src/response-history.mjs";
8
+ import { imageOptimizationStats, prepareResponsesPayload, resetImageOptimizationCacheForTests } from "../src/image-optimization.mjs";
9
+ import { createStreamPerformanceMetrics } from "../src/stream-performance.mjs";
10
+ import { runWithRequestContext } from "../src/request-context.mjs";
11
+
12
+ const digest = (value) => createHash("sha256").update(value).digest("hex");
13
+ const round = (value) => +value.toFixed(3);
14
+
15
+ function nativeWire(count, deltaBytes, drifting) {
16
+ const delta = "x".repeat(deltaBytes);
17
+ const id = drifting ? "message_changed" : "message_initial";
18
+ const item = { type: "message", id: "message_initial", role: "assistant", content: [] };
19
+ const events = [
20
+ { type: "response.output_item.added", output_index: 0, item },
21
+ ...Array.from({ length: count }, () => ({ type: "response.output_text.delta", output_index: 0, item_id: id, delta })),
22
+ { type: "response.completed", response: {
23
+ id: "resp_benchmark", status: "completed",
24
+ output: [{ ...item, id, content: [{ type: "output_text", text: delta.repeat(count) }] }],
25
+ } },
26
+ ];
27
+ return Buffer.from(events.map((event) => `event: ${event.type}\ndata: ${JSON.stringify(event)}\n\n`).join(""));
28
+ }
29
+
30
+ export async function nativeResponsesProbe() {
31
+ process.env.CCDX_DISABLE_USAGE = "1";
32
+ const cases = [
33
+ { name: "stable-small", count: 5000, deltaBytes: 256 },
34
+ { name: "drifting-small", count: 5000, deltaBytes: 256, drifting: true },
35
+ ...[1, 4, 8].flatMap((mib) => [false, true].map((drifting) => ({
36
+ name: `${drifting ? "drifting" : "stable"}-${mib}MiB`, count: 1, deltaBytes: mib * 1048576 - 1024, drifting,
37
+ }))),
38
+ { name: "slow-client", count: 5000, deltaBytes: 256, drifting: true, slow: true },
39
+ { name: "slow-large-event", count: 1, deltaBytes: 8 * 1048576 - 1024, slow: true },
40
+ ];
41
+ const results = [];
42
+ for (const scenario of cases) {
43
+ clearResponseHistoryForTests();
44
+ const wire = nativeWire(scenario.count, scenario.deltaBytes, scenario.drifting);
45
+ const expectedHash = digest(nativeWire(scenario.count, scenario.deltaBytes, false));
46
+ const outputHash = createHash("sha256");
47
+ let offset = 0;
48
+ let blocked = false;
49
+ let readsWhileBlocked = 0;
50
+ let writes = 0;
51
+ let drains = 0;
52
+ let cancelled = false;
53
+ const response = new Response(new ReadableStream({
54
+ pull(controller) {
55
+ if (blocked) readsWhileBlocked += 1;
56
+ if (offset === wire.length) return controller.close();
57
+ const end = Math.min(wire.length, offset + 16 * 1024);
58
+ controller.enqueue(wire.subarray(offset, end));
59
+ offset = end;
60
+ },
61
+ cancel() { cancelled = true; },
62
+ }, { highWaterMark: 0 }), { headers: { "Content-Type": "text/event-stream" } });
63
+ const res = new EventEmitter();
64
+ Object.assign(res, { destroyed: false, writableEnded: false, headersSent: false });
65
+ res.writeHead = () => { res.headersSent = true; };
66
+ res.end = () => { res.writableEnded = true; };
67
+ res.write = (value) => {
68
+ writes += 1;
69
+ if (scenario.slow) {
70
+ blocked = true;
71
+ setImmediate(() => {
72
+ // Retain the actual Buffer until drain to detect buffer reuse/corruption.
73
+ outputHash.update(value);
74
+ blocked = false;
75
+ drains += 1;
76
+ res.emit("drain");
77
+ });
78
+ return false;
79
+ }
80
+ outputHash.update(value);
81
+ return true;
82
+ };
83
+ const original = { parse: JSON.parse, concat: Buffer.concat, copy: Buffer.prototype.copy, set: Buffer.prototype.set };
84
+ let parseCalls = 0;
85
+ let parsedBytes = 0;
86
+ let copiedBytes = 0;
87
+ JSON.parse = function (value, ...args) {
88
+ parseCalls += 1;
89
+ parsedBytes += Buffer.byteLength(value);
90
+ return original.parse(value, ...args);
91
+ };
92
+ Buffer.concat = function (values, size) {
93
+ copiedBytes += size ?? values.reduce((total, value) => total + value.length, 0);
94
+ return original.concat(values, size);
95
+ };
96
+ Buffer.prototype.copy = function (...args) {
97
+ const copied = original.copy.apply(this, args);
98
+ copiedBytes += copied;
99
+ return copied;
100
+ };
101
+ Buffer.prototype.set = function (value, offset) {
102
+ copiedBytes += value.length;
103
+ return original.set.call(this, value, offset);
104
+ };
105
+ const started = performance.now();
106
+ let result;
107
+ try {
108
+ result = await proxyCopilotResponses({ body: { model: "gpt-benchmark", input: [], stream: true } }, {}, res, async () => response);
109
+ } finally {
110
+ JSON.parse = original.parse;
111
+ Buffer.concat = original.concat;
112
+ Buffer.prototype.copy = original.copy;
113
+ Buffer.prototype.set = original.set;
114
+ clearResponseHistoryForTests();
115
+ }
116
+ results.push({
117
+ name: scenario.name, input_bytes: wire.length, expected_events: scenario.count + 2,
118
+ parse_calls: parseCalls, parse_ratio: round(parsedBytes / wire.length), copy_ratio: round(copiedBytes / wire.length),
119
+ writes, drains, reads_while_blocked: readsWhileBlocked, cancelled,
120
+ content_matches: outputHash.digest("hex") === expectedHash,
121
+ successful: result?.successful === true, elapsed_ms: round(performance.now() - started),
122
+ });
123
+ }
124
+ return results;
125
+ }
126
+
127
+ export async function imageLifecycleProbe() {
128
+ // Public-domain photograph plus a deterministic desktop-sized UI fixture.
129
+ const photo = await sharp(fs.readFileSync(new URL("./fixtures/astronaut.jpg", import.meta.url))).png().toBuffer();
130
+ const rows = Array.from({ length: 65 }, (_, index) => (
131
+ `<text x="48" y="${55 + index * 23}" font-family="sans-serif" font-size="17" fill="#b8d7ef">${index}: const request = await prepareResponsesPayload(input); // screenshot benchmark</text>`
132
+ )).join("");
133
+ const screenshot = await sharp(Buffer.from(`<svg width="2560" height="1600"><rect width="2560" height="1600" fill="#182332"/>${rows}</svg>`)).png().toBuffer();
134
+ const sourceUrls = [photo, screenshot].map((bytes) => `data:image/png;base64,${bytes.toString("base64")}`);
135
+ const concurrency = 4;
136
+ const metrics = createStreamPerformanceMetrics();
137
+ resetImageOptimizationCacheForTests();
138
+ const samples = [];
139
+ let bodyBytes = 0;
140
+ let expectedOutputHash;
141
+ let allOutputsMatch = true;
142
+ let workSettled = true;
143
+ let previousHits = 0;
144
+ const originalLog = console.log;
145
+ const originalWarn = console.warn;
146
+ console.log = () => {};
147
+ console.warn = () => {};
148
+ try {
149
+ for (let iteration = 0; iteration < 6; iteration += 1) {
150
+ const started = performance.now();
151
+ let peakRss = process.memoryUsage().rss;
152
+ const samplePeak = () => { peakRss = Math.max(peakRss, process.memoryUsage().rss); };
153
+ const timer = setInterval(samplePeak, 5);
154
+ let results;
155
+ try {
156
+ results = await Promise.all(Array.from({ length: concurrency }, async () => {
157
+ const body = { model: "gpt-5.6-sol", input: [{ type: "message", role: "user", content:
158
+ Array.from({ length: 6 }, (_, index) => ({ type: "input_image", image_url: sourceUrls[index % sourceUrls.length], detail: "high" })),
159
+ }] };
160
+ bodyBytes = Buffer.byteLength(JSON.stringify(body));
161
+ const tracker = metrics.begin("responses");
162
+ return runWithRequestContext({ streamPerformance: tracker }, async () => {
163
+ try {
164
+ const payload = await prepareResponsesPayload(body);
165
+ samplePeak();
166
+ return { bytes: payload.bodyBytes, hash: digest(payload.bodyText) };
167
+ } finally {
168
+ tracker.finish();
169
+ }
170
+ });
171
+ }));
172
+ } finally {
173
+ clearInterval(timer);
174
+ }
175
+ expectedOutputHash ??= results[0].hash;
176
+ allOutputsMatch &&= results.every((result) => result.hash === expectedOutputHash);
177
+ await immediate();
178
+ globalThis.gc?.();
179
+ const memory = process.memoryUsage();
180
+ const cache = imageOptimizationStats();
181
+ workSettled &&= cache.active === 0 && cache.queued === 0 && cache.cache_inflight === 0;
182
+ samples.push({
183
+ iteration, elapsed_ms: round(performance.now() - started), peak_rss_bytes: peakRss,
184
+ retained_rss_bytes: memory.rss, retained_heap_bytes: memory.heapUsed, retained_array_buffer_bytes: memory.arrayBuffers,
185
+ cache_bytes: cache.cache_bytes, cache_hits_added: cache.cache_hits - previousHits, output_bytes: results[0].bytes,
186
+ });
187
+ previousHits = cache.cache_hits;
188
+ }
189
+ } finally {
190
+ console.log = originalLog;
191
+ console.warn = originalWarn;
192
+ resetImageOptimizationCacheForTests();
193
+ }
194
+ const warm = samples[1];
195
+ const late = samples.slice(2);
196
+ return {
197
+ concurrency, input_bytes: bodyBytes, photo_dimensions: [512, 512], screenshot_dimensions: [2560, 1600],
198
+ all_outputs_match: allOutputsMatch, work_settled: workSettled,
199
+ retained_heap_growth_bytes: Math.max(0, ...late.map((sample) => sample.retained_heap_bytes - warm.retained_heap_bytes)),
200
+ retained_array_buffer_growth_bytes: Math.max(0, ...late.map((sample) => sample.retained_array_buffer_bytes - warm.retained_array_buffer_bytes)),
201
+ retention_budget_bytes: Math.max(8 * 1048576, bodyBytes * concurrency * 2),
202
+ samples,
203
+ };
204
+ }
@@ -395,6 +395,14 @@ if (checkMode) {
395
395
  largePayloadProbe(30, 2),
396
396
  ];
397
397
  report.visual_history_pressure_36x4 = visualHistoryPressureProbe();
398
+ report.native_responses = runProbe(`
399
+ import { nativeResponsesProbe } from "./scripts/benchmark-probes.mjs";
400
+ process.stdout.write(JSON.stringify(await nativeResponsesProbe()));
401
+ `);
402
+ report.image_lifecycle = runProbe(`
403
+ import { imageLifecycleProbe } from "./scripts/benchmark-probes.mjs";
404
+ process.stdout.write(JSON.stringify(await imageLifecycleProbe()));
405
+ `);
398
406
  } else if (process.argv.includes("--large-payload")) {
399
407
  report.large_payload_peak = [
400
408
  largePayloadProbe(5, 1),
@@ -437,6 +445,29 @@ if (checkMode) {
437
445
  || !visualHistory.output_body_mib.every((size, index) => size < visualHistory.input_body_mib[index])) {
438
446
  failures.push("36-image x4 visual history pressure did not preserve the expected 16-image upstream window");
439
447
  }
448
+ for (const sample of report.native_responses) {
449
+ if (!sample.successful || !sample.content_matches || !sample.cancelled) {
450
+ failures.push(`native Responses changed content or failed to close its reader: ${sample.name}`);
451
+ }
452
+ if (sample.parse_calls !== sample.expected_events || sample.parse_ratio > 1.01 || sample.copy_ratio > 8) {
453
+ failures.push(`native Responses repeated parsing or copying: ${sample.name}`);
454
+ }
455
+ if (sample.writes > Math.ceil(sample.input_bytes / (16 * 1024)) + 2) {
456
+ failures.push(`native Responses lost per-read write batching: ${sample.name}`);
457
+ }
458
+ if (sample.name.startsWith("slow") && (sample.drains !== sample.writes || !sample.drains || sample.reads_while_blocked)) {
459
+ failures.push(`native Responses read ahead of downstream drain: ${sample.name}`);
460
+ }
461
+ }
462
+ const lifecycle = report.image_lifecycle;
463
+ if (!lifecycle.all_outputs_match || !lifecycle.work_settled
464
+ || lifecycle.samples.length !== 6 || lifecycle.samples.slice(1).some((sample) => sample.cache_hits_added === 0)) {
465
+ failures.push("repeated image requests did not reuse stable results or settle image work");
466
+ }
467
+ if (lifecycle.retained_heap_growth_bytes > lifecycle.retention_budget_bytes
468
+ || lifecycle.retained_array_buffer_growth_bytes > lifecycle.retention_budget_bytes) {
469
+ failures.push("repeated image requests retained more than two batches after warm-up and GC");
470
+ }
440
471
  if (failures.length) {
441
472
  for (const failure of failures) console.error(`[FAIL] ${failure}`);
442
473
  process.exitCode = 1;
@@ -0,0 +1,13 @@
1
+ # Benchmark photograph
2
+
3
+ `astronaut.jpg` is the 512 x 512 photograph of NASA astronaut Eileen Collins
4
+ distributed by scikit-image, converted from PNG to JPEG at quality 90 without
5
+ resizing. The original is credited to NASA and documented as public domain.
6
+
7
+ - [Original image](https://github.com/scikit-image/scikit-image/blob/v0.19.3/skimage/data/astronaut.png)
8
+ - [Source and public-domain statement](https://github.com/scikit-image/scikit-image/blob/v0.19.3/skimage/data/_fetchers.py#L405-L424)
9
+ - Original PNG SHA-256: `88431cd9653ccd539741b555fb0a46b61558b301d4110412b5bc28b5e3ea6cb5`
10
+
11
+ The benchmark decodes this fixture to PNG before request preparation to exercise
12
+ real image encoding. Its 2560 x 1600 screenshot fixture is generated locally
13
+ from SVG. Neither fixture contains user data or requires a network request.
Binary file
@@ -6,6 +6,7 @@ import {
6
6
  } from "./responses-content.mjs";
7
7
  import { createBoundedResultCache } from "./bounded-result-cache.mjs";
8
8
  import { status } from "./status.mjs";
9
+ import { measureRequestStage, measureRequestStageAsync } from "./stream-performance.mjs";
9
10
 
10
11
  const DEFAULT_IMG_CONCURRENCY = 2;
11
12
  const IMG_MAX_CONCURRENCY = 12;
@@ -430,6 +431,15 @@ export async function optimizeImagesInBody(reqBody, {
430
431
  return reqBody;
431
432
  }
432
433
 
434
+ function serializePayload(reqBody, assertActive) {
435
+ return measureRequestStage("serialization", () => {
436
+ assertActive?.();
437
+ const bodyText = JSON.stringify(reqBody);
438
+ assertActive?.();
439
+ return { bodyText, bodyBytes: Buffer.byteLength(bodyText) };
440
+ });
441
+ }
442
+
433
443
  export async function prepareResponsesPayload(reqBody, {
434
444
  assertActive,
435
445
  currentInputStart = 0,
@@ -444,18 +454,15 @@ export async function prepareResponsesPayload(reqBody, {
444
454
  const collection = shouldOptimizeImages ? collectImageReferences(reqBody, assertActive) : null;
445
455
  const originalImages = collection ? uniqueOriginalImages(collection.references, assertActive) : [];
446
456
  if (collection) {
447
- await optimizeImageReferences(collection, {
457
+ await measureRequestStageAsync("images", () => optimizeImageReferences(collection, {
448
458
  assertActive,
449
459
  concurrency: IMG_CONCURRENCY,
450
460
  model: reqBody.model,
451
461
  optimizeImage: (dataUrl, options) => optimizeImage(dataUrl, { ...options, quality: IMG_QUALITY }),
452
462
  signal,
453
- });
463
+ }));
454
464
  }
455
- assertActive?.();
456
- let bodyText = JSON.stringify(reqBody);
457
- assertActive?.();
458
- let bodyBytes = Buffer.byteLength(bodyText);
465
+ let { bodyText, bodyBytes } = serializePayload(reqBody, assertActive);
459
466
  const summary = summarizeReqBody(reqBody);
460
467
  const targetBytes = positiveInt(maxBytes, MAX_UPSTREAM_BODY_BYTES);
461
468
  let adapted = false;
@@ -467,37 +474,34 @@ export async function prepareResponsesPayload(reqBody, {
467
474
  assertActive?.();
468
475
  const beforeBytes = bodyBytes;
469
476
  let processed = 0;
477
+ let bodyTextDirty = false;
470
478
  stage = `q${positiveInt(profile.quality, IMG_QUALITY, 100)}`;
471
479
  const orderedImages = orderImagesByCurrentWireBytes(originalImages);
472
480
  for (let index = 0; index < orderedImages.length && bodyBytes > targetBytes; index += batchSize) {
473
481
  assertActive?.();
474
482
  const batch = orderedImages.slice(index, index + batchSize);
475
- const candidates = await Promise.all(batch.map((image) => optimizeImage(image.dataUrl, {
483
+ const candidates = await measureRequestStageAsync("images", () => Promise.all(batch.map((image) => optimizeImage(image.dataUrl, {
476
484
  ...profile,
477
485
  force: true,
478
486
  model: reqBody.model,
479
487
  signal,
480
- })));
488
+ }))));
481
489
  assertActive?.();
482
490
  for (let offset = 0; offset < batch.length && bodyBytes > targetBytes; offset += 1) {
483
491
  processed += 1;
484
492
  const applied = applyImageCandidate(collection, batch[offset], candidates[offset], bodyBytes);
485
493
  bodyBytes = applied.bodyBytes;
486
494
  adapted ||= applied.changed;
495
+ bodyTextDirty ||= applied.changed;
487
496
  if (bodyBytes <= targetBytes) {
488
497
  collection.commitDirty();
489
- assertActive?.();
490
- bodyText = JSON.stringify(reqBody);
491
- assertActive?.();
492
- bodyBytes = Buffer.byteLength(bodyText);
498
+ ({ bodyText, bodyBytes } = serializePayload(reqBody, assertActive));
499
+ bodyTextDirty = false;
493
500
  }
494
501
  }
495
502
  }
496
503
  collection.commitDirty();
497
- assertActive?.();
498
- bodyText = JSON.stringify(reqBody);
499
- assertActive?.();
500
- bodyBytes = Buffer.byteLength(bodyText);
504
+ if (bodyTextDirty) ({ bodyText, bodyBytes } = serializePayload(reqBody, assertActive));
501
505
  console.warn(status("warn", `responses payload ${beforeBytes}b exceeds ${targetBytes}b; image profile max_dim=${profile.maxDim} quality=${profile.quality} processed=${processed}/${orderedImages.length} -> ${bodyBytes}b`));
502
506
  if (bodyBytes <= targetBytes) break;
503
507
  }
@@ -3,14 +3,17 @@ import {
3
3
  readResponsesImagePart,
4
4
  readResponsesToolOutputParts,
5
5
  } from "./responses-content.mjs";
6
+ import { measureRequestStage } from "./stream-performance.mjs";
6
7
 
7
8
  const IMAGE_OMISSION_TEXT = "[CCDX: earlier image omitted to fit the upstream request byte budget.]";
8
9
 
9
10
  function bodyPayload(reqBody, assertActive) {
10
- assertActive?.();
11
- const bodyText = JSON.stringify(reqBody);
12
- assertActive?.();
13
- return { bodyText, bodyBytes: Buffer.byteLength(bodyText) };
11
+ return measureRequestStage("serialization", () => {
12
+ assertActive?.();
13
+ const bodyText = JSON.stringify(reqBody);
14
+ assertActive?.();
15
+ return { bodyText, bodyBytes: Buffer.byteLength(bodyText) };
16
+ });
14
17
  }
15
18
 
16
19
  function inlineImageIdentity(part) {
@@ -264,7 +267,7 @@ export function trimResponsesHistoryToByteBudget(reqBody, {
264
267
  initialBodyBytes,
265
268
  } = {}) {
266
269
  assertActive?.();
267
- let bodyText = initialBodyText ?? JSON.stringify(reqBody);
270
+ let bodyText = initialBodyText ?? measureRequestStage("serialization", () => JSON.stringify(reqBody));
268
271
  assertActive?.();
269
272
  let bodyBytes = initialBodyBytes ?? Buffer.byteLength(bodyText);
270
273
  const limit = Number.isFinite(targetBytes) && targetBytes > 0 ? Math.floor(targetBytes) : bodyBytes;
@@ -322,7 +325,7 @@ export function trimResponsesHistoricalImages(reqBody, {
322
325
  initialBodyBytes,
323
326
  } = {}) {
324
327
  assertActive?.();
325
- let bodyText = initialBodyText ?? JSON.stringify(reqBody);
328
+ let bodyText = initialBodyText ?? measureRequestStage("serialization", () => JSON.stringify(reqBody));
326
329
  assertActive?.();
327
330
  let bodyBytes = initialBodyBytes ?? Buffer.byteLength(bodyText);
328
331
  const byteLimit = Number.isFinite(targetBytes) && targetBytes > 0
@@ -6,6 +6,7 @@ import {
6
6
  import { responsesToChat } from "./responses-bridge.mjs";
7
7
  import { readResponsesImagePart, readResponsesToolOutputParts } from "./responses-content.mjs";
8
8
  import { withChatStreamUsage } from "./stream-contract.mjs";
9
+ import { measureRequestStage } from "./stream-performance.mjs";
9
10
 
10
11
  const DEFAULT_MAX_UPSTREAM_BODY_BYTES = 30 * 1024 * 1024;
11
12
 
@@ -23,10 +24,12 @@ function configuredBodyLimit(payloadOptions) {
23
24
  }
24
25
 
25
26
  function jsonPayload(value, assertActive) {
26
- assertActive?.();
27
- const bodyText = JSON.stringify(value);
28
- assertActive?.();
29
- return { bodyText, bodyBytes: Buffer.byteLength(bodyText) };
27
+ return measureRequestStage("serialization", () => {
28
+ assertActive?.();
29
+ const bodyText = JSON.stringify(value);
30
+ assertActive?.();
31
+ return { bodyText, bodyBytes: Buffer.byteLength(bodyText) };
32
+ });
30
33
  }
31
34
 
32
35
  function chatRequest(responsesBody, stream) {
@@ -37,6 +37,7 @@ import { status } from "./status.mjs";
37
37
  import { endStreamWithError } from "./stream-errors.mjs";
38
38
  import { safeUpstreamResponseHeaders } from "./upstream-headers.mjs";
39
39
  import { recordResponsesUsage } from "./usage.mjs";
40
+ import { measureRequestStage, measureRequestStageAsync } from "./stream-performance.mjs";
40
41
 
41
42
  const RESPONSES_ONLY_FALLBACK = new Set([
42
43
  "gpt-5.6-luna",
@@ -183,28 +184,28 @@ export function createResponsesHandler(options) {
183
184
  releaseRequest();
184
185
  };
185
186
  try {
186
- releaseRequest = await acquireRequest(req, { signal: abort.signal });
187
+ releaseRequest = await measureRequestStageAsync("admission", () => acquireRequest(req, { signal: abort.signal }));
187
188
  abort.setTimeout(requestBodyTimeoutMs, "request_body_timeout");
188
- parsed = await readJsonBody(req, { admission: releaseRequest, signal: abort.signal });
189
+ parsed = await measureRequestStageAsync("body", () => readJsonBody(req, { admission: releaseRequest, signal: abort.signal }));
189
190
  activeDeadline = now() + upstreamTimeoutMs;
190
191
  activeTimeoutReason = "responses_prepare_timeout";
191
192
  abort.setTimeout(upstreamTimeoutMs, "responses_prepare_timeout");
192
193
  if (parsed.previous_response_id !== undefined && parsed.previous_response_id !== null) {
193
- historySnapshot = acquireResponseHistorySnapshot(parsed.previous_response_id, {
194
+ historySnapshot = measureRequestStage("history", () => acquireResponseHistorySnapshot(parsed.previous_response_id, {
194
195
  assertActive: assertPrepareActive,
195
196
  signal: abort.signal,
196
- });
197
+ }));
197
198
  if (historySnapshot.bytes > 0) {
198
- await releaseRequest.reserveResponseHistory?.(historySnapshot.bytes, { signal: abort.signal });
199
+ await measureRequestStageAsync("admission", () => releaseRequest.reserveResponseHistory?.(historySnapshot.bytes, { signal: abort.signal }));
199
200
  }
200
201
  assertPrepareActive();
201
202
  }
202
- prepared = prepareResponsesRequest(parsed, {
203
+ prepared = measureRequestStage("history", () => prepareResponsesRequest(parsed, {
203
204
  assertActive: assertPrepareActive,
204
205
  copilotBoundary: false,
205
206
  historySnapshot,
206
207
  mutate: true,
207
- });
208
+ }));
208
209
  parsed = null;
209
210
  assertPrepareActive();
210
211
  imagePressureResult = imagePressure?.apply?.(prepared, { assertActive: assertPrepareActive }) || null;
@@ -435,33 +436,33 @@ export function createResponsesCompactHandler(options) {
435
436
  releaseRequest();
436
437
  };
437
438
  try {
438
- releaseRequest = await acquireRequest(req, { signal: abort.signal });
439
+ releaseRequest = await measureRequestStageAsync("admission", () => acquireRequest(req, { signal: abort.signal }));
439
440
  abort.setTimeout(requestBodyTimeoutMs, "request_body_timeout");
440
- const parsed = await readJsonBody(req, { admission: releaseRequest, signal: abort.signal });
441
+ const parsed = await measureRequestStageAsync("body", () => readJsonBody(req, { admission: releaseRequest, signal: abort.signal }));
441
442
  activeDeadline = now() + upstreamTimeoutMs;
442
443
  activeTimeoutReason = "responses_prepare_timeout";
443
444
  abort.setTimeout(upstreamTimeoutMs, "responses_prepare_timeout");
444
445
  if (parsed.previous_response_id !== undefined && parsed.previous_response_id !== null) {
445
- historySnapshot = acquireResponseHistorySnapshot(parsed.previous_response_id, {
446
+ historySnapshot = measureRequestStage("history", () => acquireResponseHistorySnapshot(parsed.previous_response_id, {
446
447
  assertActive: assertPrepareActive,
447
448
  signal: abort.signal,
448
- });
449
+ }));
449
450
  if (historySnapshot.bytes > 0) {
450
- await releaseRequest.reserveResponseHistory?.(historySnapshot.bytes, { signal: abort.signal });
451
+ await measureRequestStageAsync("admission", () => releaseRequest.reserveResponseHistory?.(historySnapshot.bytes, { signal: abort.signal }));
451
452
  }
452
453
  assertPrepareActive();
453
454
  }
454
- prepared = prepareResponsesRequest(parsed, {
455
+ prepared = measureRequestStage("history", () => prepareResponsesRequest(parsed, {
455
456
  assertActive: assertPrepareActive,
456
457
  copilotBoundary: false,
457
458
  historySnapshot,
458
459
  mutate: true,
459
- });
460
+ }));
460
461
  imagePressureResult = imagePressure?.applyRecovery?.(prepared, { assertActive: assertPrepareActive }) || null;
461
462
  if (imagePressureResult?.adapted) {
462
463
  console.warn(status("warn", `responses compact visual history mode=recovery historical_images=${imagePressureResult.initialHistoricalImages}->${imagePressureResult.historicalImages} omitted=${imagePressureResult.imagesOmitted} bytes=${imagePressureResult.initialBodyBytes}->${imagePressureResult.bodyBytes}`));
463
464
  }
464
- prepared = prepareResponsesCompactionRequest(prepared);
465
+ prepared = measureRequestStage("history", () => prepareResponsesCompactionRequest(prepared));
465
466
  assertPrepareActive();
466
467
  prepared.surface = "responses_compact";
467
468
  const model = parsed.model || "unknown";
@@ -2,6 +2,8 @@ import { performance } from "node:perf_hooks";
2
2
  import { currentRequestContext } from "./request-context.mjs";
3
3
 
4
4
  const PERFORMANCE_ROUTES = Object.freeze(["responses", "responses_compact"]);
5
+ const PREPARATION_STAGES = Object.freeze(["admission", "body", "history", "images", "serialization"]);
6
+ const PREPARATION_EDGES_MS = Object.freeze([1, 2, 5, 10, 20, 50, 100, 250, 500, 1_000, 5_000, 30_000, 120_000]);
5
7
  const TTFT_EDGES_MS = Object.freeze([
6
8
  100, 200, 300, 500, 700, 1_000, 1_400, 2_000, 2_800, 4_000,
7
9
  5_500, 8_000, 12_000, 16_000, 24_000, 36_000, 60_000, 120_000, 300_000,
@@ -80,6 +82,8 @@ function createRoutePerformance() {
80
82
  return {
81
83
  ttft: createHistogram(TTFT_EDGES_MS),
82
84
  tpot: createHistogram(TPOT_EDGES_US),
85
+ requestTtft: createHistogram(TTFT_EDGES_MS),
86
+ preparation: Object.fromEntries(PREPARATION_STAGES.map((stage) => [stage, createHistogram(PREPARATION_EDGES_MS)])),
83
87
  success_with_output: 0,
84
88
  errors_with_output: 0,
85
89
  zero_output_errors: 0,
@@ -95,6 +99,8 @@ function routeSnapshot(route) {
95
99
  neutral: route.neutral,
96
100
  ttft_ms: histogramSnapshot(route.ttft, "ms"),
97
101
  tpot_us: histogramSnapshot(route.tpot, "us"),
102
+ request_ttft_ms: histogramSnapshot(route.requestTtft, "ms"),
103
+ preparation_ms: Object.fromEntries(PREPARATION_STAGES.map((stage) => [stage, histogramSnapshot(route.preparation[stage], "ms")])),
98
104
  };
99
105
  }
100
106
 
@@ -105,6 +111,7 @@ export function createStreamPerformanceMetrics({ now = () => performance.now() }
105
111
  begin(routeName) {
106
112
  const route = routes[routeName];
107
113
  if (!route) return null;
114
+ const requestStartedAt = now();
108
115
  let upstreamStartedAt = null;
109
116
  let firstOutputAt = null;
110
117
  let outputTokens = null;
@@ -112,6 +119,17 @@ export function createStreamPerformanceMetrics({ now = () => performance.now() }
112
119
  let finished = false;
113
120
 
114
121
  return {
122
+ beginStage(stage) {
123
+ const histogram = route.preparation[stage];
124
+ if (!histogram || finished) return null;
125
+ const startedAt = now();
126
+ let ended = false;
127
+ return () => {
128
+ if (ended) return;
129
+ ended = true;
130
+ observe(histogram, Math.max(0, now() - startedAt));
131
+ };
132
+ },
115
133
  upstreamStarted() {
116
134
  if (upstreamStartedAt === null) upstreamStartedAt = now();
117
135
  },
@@ -130,6 +148,7 @@ export function createStreamPerformanceMetrics({ now = () => performance.now() }
130
148
  finished = true;
131
149
  failed ||= finishFailed;
132
150
  const finishedAt = now();
151
+ if (firstOutputAt !== null) observe(route.requestTtft, Math.max(0, firstOutputAt - requestStartedAt));
133
152
  if (upstreamStartedAt === null) {
134
153
  route.neutral += 1;
135
154
  return;
@@ -160,6 +179,24 @@ function tracker() {
160
179
  return currentRequestContext()?.streamPerformance || null;
161
180
  }
162
181
 
182
+ export function measureRequestStage(stage, operation) {
183
+ const finish = tracker()?.beginStage?.(stage);
184
+ try {
185
+ return operation();
186
+ } finally {
187
+ finish?.();
188
+ }
189
+ }
190
+
191
+ export async function measureRequestStageAsync(stage, operation) {
192
+ const finish = tracker()?.beginStage?.(stage);
193
+ try {
194
+ return await operation();
195
+ } finally {
196
+ finish?.();
197
+ }
198
+ }
199
+
163
200
  export function markUpstreamStarted() {
164
201
  tracker()?.upstreamStarted();
165
202
  }