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 +4 -0
- package/package.json +3 -1
- package/scripts/benchmark-probes.mjs +204 -0
- package/scripts/benchmark.mjs +31 -0
- package/scripts/fixtures/README.md +13 -0
- package/scripts/fixtures/astronaut.jpg +0 -0
- package/src/image-optimization.mjs +20 -16
- package/src/responses-byte-budget.mjs +9 -6
- package/src/responses-chat-payload.mjs +7 -4
- package/src/responses-handler.mjs +16 -15
- package/src/stream-performance.mjs +37 -0
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
|
+
"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
|
+
}
|
package/scripts/benchmark.mjs
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
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
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
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
|
}
|