codex-copilot-dx 0.7.2 → 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
@@ -283,6 +285,8 @@ For `/v1/responses/compact`, CCDX sends one terminal `compaction_trigger`, valid
283
285
 
284
286
  Successful streaming responses are accepted only when the upstream body is SSE and reaches its protocol terminal event. Unexpected EOF becomes a protocol-native stream error, and repeated blank tool-argument deltas are stopped per tool call. Safe quota, retry, model, trace, and upstream request-ID metadata are forwarded; cookies, authorization, body-length, and encoding headers are not.
285
287
 
288
+ CCDX 0.7.3 keeps each native Responses message on its first upstream ID when Copilot changes that ID during streaming. Message deltas, completion events, and cached history stay consistent, preventing duplicate live messages in Codex App. Tool identities, reasoning/encrypted content, message phases, and speed-tier routing are unchanged. Stable events retain their original SSE bytes; changed events are rewritten with bounded, backpressured streaming.
289
+
286
290
  Newer Codex App builds can advertise an `image_gen` namespace that already exists upstream. CCDX removes that exact conflicting client tool before forwarding and retries once only when Copilot explicitly reports an image namespace collision. Image input and screenshot optimization remain enabled.
287
291
 
288
292
  ## Development
@@ -293,8 +297,12 @@ npm run verify
293
297
  npm run bench:payload
294
298
  ```
295
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
+
296
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.
297
303
 
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.
305
+
298
306
  ## License
299
307
 
300
308
  MIT
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codex-copilot-dx",
3
- "version": "0.7.2",
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";
@@ -32,12 +32,56 @@ import { safeUpstreamResponseHeaders } from "./upstream-headers.mjs";
32
32
  import { recordResponsesUsage } from "./usage.mjs";
33
33
 
34
34
  const MAX_SSE_BUFFER_BYTES = loadRuntimeConfig().maxSseBufferBytes;
35
+ const SSE_WRITE_BATCH_BYTES = 64 * 1024;
35
36
  const RESPONSES_TERMINAL_EVENT_TYPES = new Set([
36
37
  "response.completed",
37
38
  "response.incomplete",
38
39
  "response.failed",
39
40
  "error",
40
41
  ]);
42
+ const MESSAGE_CONTENT_EVENT_TYPES = new Set([
43
+ "response.content_part.added",
44
+ "response.content_part.done",
45
+ "response.output_text.delta",
46
+ "response.output_text.done",
47
+ "response.output_text.annotation.added",
48
+ "response.refusal.delta",
49
+ "response.refusal.done",
50
+ ]);
51
+
52
+ function normalizeMessageIds(state, event, eventType) {
53
+ let changed = false;
54
+ const replaceId = (object, field, id) => {
55
+ if (id && typeof object?.[field] === "string" && object[field] !== id) {
56
+ object[field] = id;
57
+ changed = true;
58
+ }
59
+ };
60
+ const index = event.output_index;
61
+ if (Number.isSafeInteger(index) && index >= 0) {
62
+ // Bind only confirmed message items, using the full output index (including tools).
63
+ // IDs are opaque: retain the first upstream ID without decoding or synthesizing one.
64
+ if (eventType === "response.output_item.added" && event.item?.type === "message"
65
+ && typeof event.item.id === "string" && event.item.id.length > 0
66
+ && !state.messageIds.has(index)) {
67
+ state.messageIds.set(index, event.item.id);
68
+ }
69
+ const id = state.messageIds.get(index);
70
+ if (["response.output_item.added", "response.output_item.done"].includes(eventType)
71
+ && event.item?.type === "message") {
72
+ replaceId(event.item, "id", id);
73
+ } else if (MESSAGE_CONTENT_EVENT_TYPES.has(eventType)) {
74
+ replaceId(event, "item_id", id);
75
+ }
76
+ }
77
+ if (RESPONSES_TERMINAL_EVENT_TYPES.has(eventType) && Array.isArray(event.response?.output)) {
78
+ for (let index = 0; index < event.response.output.length; index += 1) {
79
+ const item = event.response.output[index];
80
+ if (item?.type === "message") replaceId(item, "id", state.messageIds.get(index));
81
+ }
82
+ }
83
+ return changed;
84
+ }
41
85
 
42
86
  function inspectResponseSseEvent(state, eventName, data) {
43
87
  if (!data || state.sawTerminal) return;
@@ -57,6 +101,7 @@ function inspectResponseSseEvent(state, eventName, data) {
57
101
  );
58
102
  }
59
103
  const eventType = event.type || eventName;
104
+ const changed = normalizeMessageIds(state, event, eventType);
60
105
  const outputTokens = event.response?.usage?.output_tokens ?? event.usage?.output_tokens;
61
106
  if (outputTokens !== undefined) markOutputTokens(outputTokens);
62
107
  if (!state.sawOutput && isResponsesOutputEvent(event, eventType)) {
@@ -66,7 +111,7 @@ function inspectResponseSseEvent(state, eventName, data) {
66
111
  if (["response.function_call_arguments.delta", "response.custom_tool_call_input.delta"].includes(eventType)) {
67
112
  state.toolArgumentGuard.observe(event.output_index ?? event.item_id ?? event.call_id, event.delta);
68
113
  }
69
- if (!RESPONSES_TERMINAL_EVENT_TYPES.has(eventType)) return;
114
+ if (!RESPONSES_TERMINAL_EVENT_TYPES.has(eventType)) return changed ? event : null;
70
115
  const expectedStatus = {
71
116
  "response.completed": "completed",
72
117
  "response.incomplete": "incomplete",
@@ -80,12 +125,14 @@ function inspectResponseSseEvent(state, eventName, data) {
80
125
  }
81
126
  state.sawTerminal = true;
82
127
  if (eventType === "response.failed" || eventType === "error") markStreamFailure();
83
- if (eventType !== "response.completed") return;
84
- const response = event.response;
85
- state.completed = { response, event };
128
+ if (eventType === "response.completed") {
129
+ const response = event.response;
130
+ state.completed = { response, event };
131
+ }
132
+ return changed ? event : null;
86
133
  }
87
134
 
88
- function createResponseSseInspector(state) {
135
+ function createResponseSseTransformer(state) {
89
136
  let eventBuffer = Buffer.allocUnsafe(Math.min(4096, MAX_SSE_BUFFER_BYTES + 4));
90
137
  let eventLength = 0;
91
138
  let previous1 = -1;
@@ -111,12 +158,12 @@ function createResponseSseInspector(state) {
111
158
  eventLength += bytes.byteLength;
112
159
  };
113
160
 
114
- const inspectEvent = (delimiterLength) => {
115
- const contentLength = eventLength - delimiterLength;
161
+ const inspectEvent = (frame, delimiterLength) => {
162
+ const contentLength = frame.byteLength - delimiterLength;
116
163
  if (contentLength > MAX_SSE_BUFFER_BYTES) {
117
164
  throw httpError(`Upstream SSE buffer exceeds ${MAX_SSE_BUFFER_BYTES} bytes`, 502);
118
165
  }
119
- const chunk = eventBuffer.subarray(0, contentLength).toString("utf8");
166
+ const chunk = frame.subarray(0, contentLength).toString("utf8");
120
167
  const lines = chunk.split(/\r?\n/);
121
168
  const eventName = lines
122
169
  .find((line) => line.startsWith("event:"))
@@ -126,15 +173,31 @@ function createResponseSseInspector(state) {
126
173
  .filter((line) => line.startsWith("data:"))
127
174
  .map((line) => line.slice(5).trimStart())
128
175
  .join("\n");
129
- if (data) inspectResponseSseEvent(state, eventName, data);
130
- eventLength = 0;
131
- previous1 = -1;
132
- previous2 = -1;
133
- previous3 = -1;
176
+ const changed = data && inspectResponseSseEvent(state, eventName, data);
177
+ if (!changed) return frame;
178
+ let wroteData = false;
179
+ const rewritten = lines.filter((line) => {
180
+ if (!line.startsWith("data:")) return true;
181
+ if (wroteData) return false;
182
+ wroteData = true;
183
+ return true;
184
+ }).map((line) => line.startsWith("data:") ? `data: ${JSON.stringify(changed)}` : line);
185
+ return Buffer.from(rewritten.join(chunk.includes("\r\n") ? "\r\n" : "\n")
186
+ + frame.subarray(contentLength).toString("ascii"));
134
187
  };
135
188
 
136
189
  return {
137
190
  push(value) {
191
+ // Fetch exposes Uint8Array chunks; use a zero-copy Buffer view for UTF-8 decoding.
192
+ if (!Buffer.isBuffer(value)) value = Buffer.from(value.buffer, value.byteOffset, value.byteLength);
193
+ let pending = [];
194
+ let pendingBytes = 0;
195
+ const flush = () => {
196
+ const output = pending.length === 1 ? pending[0] : Buffer.concat(pending, pendingBytes);
197
+ pending = [];
198
+ pendingBytes = 0;
199
+ return output;
200
+ };
138
201
  let segmentStart = 0;
139
202
  for (let index = 0; index < value.byteLength; index += 1) {
140
203
  const byte = value[index];
@@ -145,10 +208,32 @@ function createResponseSseInspector(state) {
145
208
  delimiterLength = previous3 === 0x0d ? 4 : 3;
146
209
  }
147
210
  if (delimiterLength) {
148
- append(value.subarray(segmentStart, index + 1));
149
- inspectEvent(delimiterLength);
211
+ let frame = value.subarray(segmentStart, index + 1);
212
+ if (eventLength > 0) {
213
+ append(frame);
214
+ // Writes may retain a Buffer after returning; never expose the reusable buffer.
215
+ if (eventLength > SSE_WRITE_BATCH_BYTES) {
216
+ frame = eventBuffer.subarray(0, eventLength);
217
+ eventBuffer = Buffer.allocUnsafe(Math.min(4096, MAX_SSE_BUFFER_BYTES + 4));
218
+ } else {
219
+ frame = Buffer.from(eventBuffer.subarray(0, eventLength));
220
+ }
221
+ }
222
+ eventLength = 0;
223
+ previous1 = -1;
224
+ previous2 = -1;
225
+ previous3 = -1;
150
226
  segmentStart = index + 1;
151
- if (state.sawTerminal) return index + 1;
227
+ const forwarded = inspectEvent(frame, delimiterLength);
228
+ const last = pending.at(-1);
229
+ if (last && last.buffer === forwarded.buffer
230
+ && last.byteOffset + last.byteLength === forwarded.byteOffset) {
231
+ pending[pending.length - 1] = Buffer.from(last.buffer, last.byteOffset, last.byteLength + forwarded.byteLength);
232
+ } else {
233
+ pending.push(forwarded);
234
+ }
235
+ pendingBytes += forwarded.byteLength;
236
+ if (state.sawTerminal) return flush();
152
237
  continue;
153
238
  }
154
239
  previous3 = previous2;
@@ -159,7 +244,7 @@ function createResponseSseInspector(state) {
159
244
  if (eventLength > MAX_SSE_BUFFER_BYTES + 3) {
160
245
  throw httpError(`Upstream SSE buffer exceeds ${MAX_SSE_BUFFER_BYTES} bytes`, 502);
161
246
  }
162
- return null;
247
+ return pendingBytes > 0 ? flush() : null;
163
248
  },
164
249
  };
165
250
  }
@@ -235,8 +320,9 @@ export async function proxyCopilotResponses(reqContext, req, res, upstream = cop
235
320
  sawTerminal: false,
236
321
  sawOutput: false,
237
322
  toolArgumentGuard: new ToolArgumentDeltaGuard(),
323
+ messageIds: new Map(),
238
324
  };
239
- const inspector = createResponseSseInspector(streamState);
325
+ const transformer = createResponseSseTransformer(streamState);
240
326
  try {
241
327
  while (true) {
242
328
  const { done, value } = await reader.read();
@@ -247,9 +333,12 @@ export async function proxyCopilotResponses(reqContext, req, res, upstream = cop
247
333
  return { successful: Boolean(streamState.completed), compacted: false };
248
334
  }
249
335
  options.abort?.setTimeout(options.streamIdleTimeoutMs, "stream_idle_timeout");
250
- const terminalOffset = inspector.push(value);
251
- const forwarded = terminalOffset === null ? value : value.subarray(0, terminalOffset);
252
- if (forwarded.byteLength > 0 && !await writeOrDrain(res, forwarded)) return;
336
+ // Batch within this read, not across reads, and await drain before processing more.
337
+ for (let offset = 0; offset < value.byteLength; offset += SSE_WRITE_BATCH_BYTES) {
338
+ const frame = transformer.push(value.subarray(offset, offset + SSE_WRITE_BATCH_BYTES));
339
+ if (frame && !await writeOrDrain(res, frame)) return;
340
+ if (streamState.sawTerminal) break;
341
+ }
253
342
  if (streamState.sawTerminal) {
254
343
  storeCompletedResponse(reqContext, streamState.completed);
255
344
  res.end();
@@ -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
  }