codex-copilot-dx 0.6.3 → 0.6.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
@@ -31,7 +31,8 @@ ccdx
31
31
 
32
32
  `ccdx` is the only documented command and primary launcher. The old
33
33
  `codex-copilot-dx` executable remains temporarily as a compatibility shim: it
34
- prints a deprecation warning and runs `ccdx`, and will be removed in a future
34
+ prints a deprecation warning at most once every 7 days in interactive terminals,
35
+ runs `ccdx`, and remains silent in scripts. It will be removed in a future
35
36
  breaking release. For a one-off run without a global install, use
36
37
  `npm exec --yes --package=codex-copilot-dx@latest -- ccdx`.
37
38
  Run `ccdx <command> --help` (including `auth login claude --help` and
@@ -330,9 +331,10 @@ Environment variables:
330
331
  | `CCDX_ALLOW_LAN` | unset | Set to `1` to allow non-loopback `ADAPTER_HOST` values such as `0.0.0.0`; exposes your Copilot-backed adapter beyond this machine |
331
332
  | `CCDX_MAX_BODY_BYTES` | `67108864` | Maximum compressed/raw request body size |
332
333
  | `CCDX_MAX_DECODED_BODY_BYTES` | `134217728` | Maximum decoded request body size after decompression |
333
- | `CCDX_MAX_INFLIGHT_BODY_BYTES` | `33554432` | Shared byte budget for admitted request bodies; a larger single request runs exclusively |
334
+ | `CCDX_MAX_INFLIGHT_BODY_BYTES` | `33554432` | Per-pool byte budget for raw bodies, decoded bodies, and materialized response history; a larger item runs exclusively in its pool |
334
335
  | `CCDX_MAX_QUEUED_REQUESTS` | `16` | Maximum body requests waiting for the shared byte budget |
335
336
  | `CCDX_REQUEST_QUEUE_TIMEOUT_MS` | `120000` | Maximum wait for request-body admission before returning `503` |
337
+ | `CCDX_REQUEST_BODY_TIMEOUT_MS` | `120000` | Maximum time to receive and decode one request body before returning `408` |
336
338
  | `CCDX_MAX_UPSTREAM_BODY_BYTES` | `31457280` | Strict maximum forwarded Responses body size; larger payloads are adapted locally or rejected with `413` |
337
339
  | `CCDX_MAX_SSE_BUFFER_BYTES` | `8388608` | Maximum buffered bytes for one unterminated upstream SSE line/event |
338
340
  | `CCDX_UPSTREAM_TIMEOUT_MS` | `120000` | Timeout for non-streaming upstream Copilot requests |
@@ -1,3 +1,10 @@
1
1
  #!/usr/bin/env node
2
- console.warn("codex-copilot-dx is deprecated; use ccdx instead. The compatibility alias will be removed in a future breaking release.");
2
+
3
+ if (process.stderr.isTTY === true) {
4
+ const {
5
+ LEGACY_COMMAND_WARNING,
6
+ shouldShowLegacyCommandWarning,
7
+ } = await import("../src/legacy-command-warning.mjs");
8
+ if (shouldShowLegacyCommandWarning({ interactive: true })) console.warn(LEGACY_COMMAND_WARNING);
9
+ }
3
10
  await import("./cli.mjs");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codex-copilot-dx",
3
- "version": "0.6.3",
3
+ "version": "0.6.4",
4
4
  "description": "Use Codex Desktop, Claude Code, and Claude App with GitHub Copilot",
5
5
  "author": "Dale Xiao",
6
6
  "type": "module",
package/src/adapter.mjs CHANGED
@@ -136,6 +136,7 @@ export function createAdapterHandler(options = {}) {
136
136
  const upstreamTimeoutMs = parsePositiveInteger(options.upstreamTimeoutMs, ADAPTER_RUNTIME_CONFIG.upstreamTimeoutMs);
137
137
  const streamHandshakeTimeoutMs = parsePositiveInteger(options.streamHandshakeTimeoutMs, ADAPTER_RUNTIME_CONFIG.streamHandshakeTimeoutMs);
138
138
  const streamIdleTimeoutMs = parsePositiveInteger(options.streamIdleTimeoutMs, ADAPTER_RUNTIME_CONFIG.streamIdleTimeoutMs);
139
+ const requestBodyTimeoutMs = parsePositiveInteger(options.requestBodyTimeoutMs, ADAPTER_RUNTIME_CONFIG.requestBodyTimeoutMs);
139
140
  const acquireRequest = options.acquireRequest || createRequestAdmission();
140
141
  const requestMetrics = options.requestMetrics || createRequestMetrics();
141
142
  const streamPerformanceMetrics = options.streamPerformanceMetrics || createStreamPerformanceMetrics();
@@ -150,9 +151,11 @@ export function createAdapterHandler(options = {}) {
150
151
  chatCompletionsFn: codexChatCompletionsFn,
151
152
  getCachedModelEndpointsFn,
152
153
  imagePressure,
154
+ now: options.now,
153
155
  openAIModelEnv,
154
156
  responsesPayloadOptions: options.responsesPayloadOptions,
155
157
  responsesFn,
158
+ requestBodyTimeoutMs,
156
159
  streamHandshakeTimeoutMs,
157
160
  streamIdleTimeoutMs,
158
161
  upstreamTimeoutMs,
@@ -161,18 +164,21 @@ export function createAdapterHandler(options = {}) {
161
164
  acquireRequest,
162
165
  autoReviewModelResolver: options.autoReviewModelResolver,
163
166
  imagePressure,
167
+ now: options.now,
164
168
  openAIModelEnv,
169
+ requestBodyTimeoutMs,
165
170
  responsesCompactFn,
166
171
  streamHandshakeTimeoutMs,
167
172
  streamIdleTimeoutMs,
168
173
  upstreamTimeoutMs,
169
174
  });
170
- const anthropicCountTokensHandler = createAnthropicCountTokensHandler({ acquireRequest });
175
+ const anthropicCountTokensHandler = createAnthropicCountTokensHandler({ acquireRequest, requestBodyTimeoutMs });
171
176
  const anthropicMessagesHandler = createAnthropicMessagesHandler({
172
177
  acquireRequest,
173
178
  chatCompletionsFn: claudeChatCompletionsFn,
174
179
  environment: process.env,
175
180
  modelOptions: claudeDesktopModelOptions,
181
+ requestBodyTimeoutMs,
176
182
  streamHandshakeTimeoutMs,
177
183
  streamIdleTimeoutMs,
178
184
  upstreamTimeoutMs,
@@ -184,6 +190,7 @@ export function createAdapterHandler(options = {}) {
184
190
  claudeProfile: options.claudeProfile,
185
191
  claudeModelRegistry,
186
192
  fetchImpl: options.pmStudioFetchImpl,
193
+ requestBodyTimeoutMs,
187
194
  streamHandshakeTimeoutMs,
188
195
  streamIdleTimeoutMs,
189
196
  upstreamTimeoutMs,
@@ -284,13 +291,20 @@ export function createAdapterHandler(options = {}) {
284
291
  };
285
292
 
286
293
  return (req, res) => {
287
- const pathname = requestPath(req.url);
288
294
  const requestId = createRequestId();
289
295
  if (typeof res.setHeader === "function") {
290
296
  res.setHeader("X-Request-Id", requestId);
291
297
  } else if (res.headers && typeof res.headers === "object") {
292
298
  res.headers["X-Request-Id"] = requestId;
293
299
  }
300
+ let pathname;
301
+ try {
302
+ pathname = requestPath(req.url);
303
+ } catch {
304
+ res.writeHead(400, { "Content-Type": "application/json", Connection: "close" });
305
+ res.end(JSON.stringify({ error: "Invalid request target" }));
306
+ return;
307
+ }
294
308
 
295
309
  const trackRequest = pathname !== ADAPTER_HEALTH_PATH && pathname !== ADAPTER_STATUS_PATH;
296
310
  const routeName = classifyAdapterRoute(req.method, pathname);
@@ -21,20 +21,22 @@ import { safeUpstreamResponseHeaders } from "./upstream-headers.mjs";
21
21
  import { recordAnthropicUsage } from "./usage.mjs";
22
22
 
23
23
  export function createAnthropicCountTokensHandler(options) {
24
- const { acquireRequest } = options;
24
+ const { acquireRequest, requestBodyTimeoutMs } = options;
25
25
 
26
26
  return async function handleAnthropicCountTokens(req, res) {
27
27
  const abort = createRequestAbort(req, res);
28
28
  let releaseRequest = () => {};
29
29
  try {
30
30
  releaseRequest = await acquireRequest(req, { signal: abort.signal });
31
- const parsed = await readJsonBody(req);
31
+ abort.setTimeout(requestBodyTimeoutMs, "request_body_timeout");
32
+ const parsed = await readJsonBody(req, { admission: releaseRequest, signal: abort.signal });
33
+ abort.clearTimeout();
32
34
  const result = await countTokens(parsed);
33
35
  releaseRequest();
34
36
  res.writeHead(200, { "Content-Type": "application/json" });
35
37
  res.end(JSON.stringify(result));
36
38
  } catch (error) {
37
- sendJsonError(res, error);
39
+ sendJsonError(res, error, abort.reason === "request_body_timeout" ? 408 : 400);
38
40
  } finally {
39
41
  releaseRequest();
40
42
  abort.cleanup();
@@ -48,6 +50,7 @@ export function createAnthropicMessagesHandler(options) {
48
50
  chatCompletionsFn,
49
51
  environment,
50
52
  modelOptions,
53
+ requestBodyTimeoutMs,
51
54
  streamHandshakeTimeoutMs,
52
55
  streamIdleTimeoutMs,
53
56
  upstreamTimeoutMs,
@@ -58,7 +61,8 @@ export function createAnthropicMessagesHandler(options) {
58
61
  let releaseRequest = () => {};
59
62
  try {
60
63
  releaseRequest = await acquireRequest(req, { signal: abort.signal });
61
- const parsed = await readJsonBody(req);
64
+ abort.setTimeout(requestBodyTimeoutMs, "request_body_timeout");
65
+ const parsed = await readJsonBody(req, { admission: releaseRequest, signal: abort.signal });
62
66
  abort.setTimeout(
63
67
  parsed.stream ? streamHandshakeTimeoutMs : upstreamTimeoutMs,
64
68
  parsed.stream ? "stream_handshake_timeout" : "upstream_timeout",
@@ -47,6 +47,83 @@ function admissionAbortError(signal) {
47
47
  return error;
48
48
  }
49
49
 
50
+ function createSupplementalAdmission({ maxWeight, maxQueued, waitTimeoutMs, label }) {
51
+ const queue = [];
52
+ let activeWeight = 0;
53
+ let activeRequests = 0;
54
+
55
+ const activate = (entry) => {
56
+ entry.cleanup();
57
+ activeWeight += entry.weight;
58
+ activeRequests += 1;
59
+ let released = false;
60
+ entry.resolve(() => {
61
+ if (released) return;
62
+ released = true;
63
+ activeWeight = Math.max(0, activeWeight - entry.weight);
64
+ activeRequests = Math.max(0, activeRequests - 1);
65
+ drain();
66
+ });
67
+ };
68
+
69
+ const drain = () => {
70
+ for (let index = 0; index < queue.length;) {
71
+ const entry = queue[index];
72
+ if (activeWeight + entry.weight > maxWeight) {
73
+ index += 1;
74
+ continue;
75
+ }
76
+ queue.splice(index, 1);
77
+ activate(entry);
78
+ }
79
+ };
80
+
81
+ const acquire = (requestedWeight, { signal } = {}) => {
82
+ if (signal?.aborted) return Promise.reject(admissionAbortError(signal));
83
+ const parsedWeight = Number.isFinite(requestedWeight) && requestedWeight > 0
84
+ ? Math.ceil(requestedWeight)
85
+ : maxWeight;
86
+ const weight = Math.max(1, Math.min(parsedWeight, maxWeight));
87
+ if (queue.length >= maxQueued && activeWeight + weight > maxWeight) {
88
+ return Promise.reject(httpError(`${label} queue is full (${maxQueued} waiting)`, 503));
89
+ }
90
+
91
+ return new Promise((resolve, reject) => {
92
+ let timer;
93
+ const entry = {
94
+ weight,
95
+ resolve,
96
+ cleanup: () => {
97
+ if (timer) clearTimeout(timer);
98
+ signal?.removeEventListener("abort", onAbort);
99
+ },
100
+ };
101
+ const remove = () => {
102
+ const index = queue.indexOf(entry);
103
+ if (index >= 0) queue.splice(index, 1);
104
+ };
105
+ const fail = (error) => {
106
+ entry.cleanup();
107
+ remove();
108
+ reject(error);
109
+ drain();
110
+ };
111
+ const onAbort = () => fail(admissionAbortError(signal));
112
+ signal?.addEventListener("abort", onAbort, { once: true });
113
+ timer = setTimeout(() => {
114
+ fail(httpError(`${label} admission timed out after ${waitTimeoutMs}ms`, 503));
115
+ }, waitTimeoutMs);
116
+ timer.unref?.();
117
+ queue.push(entry);
118
+ if (signal?.aborted) onAbort();
119
+ else drain();
120
+ });
121
+ };
122
+
123
+ acquire.stats = () => ({ activeWeight, activeRequests, queued: queue.length, maxWeight });
124
+ return acquire;
125
+ }
126
+
50
127
  export function createRequestAdmission({
51
128
  maxBytes = MAX_INFLIGHT_BODY_BYTES,
52
129
  maxQueued = MAX_QUEUED_REQUESTS,
@@ -55,6 +132,24 @@ export function createRequestAdmission({
55
132
  const byteLimit = parsePositiveInteger(maxBytes, MAX_INFLIGHT_BODY_BYTES);
56
133
  const queueLimit = parsePositiveInteger(maxQueued, MAX_QUEUED_REQUESTS);
57
134
  const timeoutMs = parsePositiveInteger(waitTimeoutMs, REQUEST_QUEUE_TIMEOUT_MS);
135
+ const acquireDecompression = createSupplementalAdmission({
136
+ maxWeight: 1,
137
+ maxQueued: queueLimit,
138
+ waitTimeoutMs: timeoutMs,
139
+ label: "Request decompression",
140
+ });
141
+ const acquireDecodedBody = createSupplementalAdmission({
142
+ maxWeight: byteLimit,
143
+ maxQueued: queueLimit,
144
+ waitTimeoutMs: timeoutMs,
145
+ label: "Decoded request body",
146
+ });
147
+ const acquireResponseHistory = createSupplementalAdmission({
148
+ maxWeight: byteLimit,
149
+ maxQueued: queueLimit,
150
+ waitTimeoutMs: timeoutMs,
151
+ label: "Response history",
152
+ });
58
153
  const queue = [];
59
154
  let activeBytes = 0;
60
155
  let activeRequests = 0;
@@ -83,13 +178,28 @@ export function createRequestAdmission({
83
178
  counters.waitMsTotal += waitMs;
84
179
  counters.waitMsMax = Math.max(counters.waitMsMax, waitMs);
85
180
  let released = false;
86
- entry.resolve(() => {
181
+ const supplementalReleases = new Set();
182
+ const release = () => {
87
183
  if (released) return;
88
184
  released = true;
185
+ for (const releaseSupplemental of supplementalReleases) releaseSupplemental();
186
+ supplementalReleases.clear();
89
187
  activeBytes = Math.max(0, activeBytes - entry.weight);
90
188
  activeRequests = Math.max(0, activeRequests - 1);
91
189
  drain();
92
- });
190
+ };
191
+ const reserve = async (gate, weight, options) => {
192
+ const releaseSupplemental = await gate(weight, options);
193
+ if (released) {
194
+ releaseSupplemental();
195
+ throw admissionAbortError(options?.signal);
196
+ }
197
+ supplementalReleases.add(releaseSupplemental);
198
+ };
199
+ release.acquireDecompression = (options) => acquireDecompression(1, options);
200
+ release.reserveDecodedBody = (bytes, options) => reserve(acquireDecodedBody, bytes, options);
201
+ release.reserveResponseHistory = (bytes, options) => reserve(acquireResponseHistory, bytes, options);
202
+ entry.resolve(release);
93
203
  };
94
204
 
95
205
  const drain = () => {
@@ -173,11 +283,64 @@ export function createRequestAdmission({
173
283
  ? Number((counters.waitMsTotal / counters.activated).toFixed(1))
174
284
  : 0,
175
285
  waitMsMax: counters.waitMsMax,
286
+ decompressionsActive: acquireDecompression.stats().activeRequests,
287
+ decompressionsQueued: acquireDecompression.stats().queued,
288
+ decodedBodyBytes: acquireDecodedBody.stats().activeWeight,
289
+ decodedBodiesActive: acquireDecodedBody.stats().activeRequests,
290
+ decodedBodiesQueued: acquireDecodedBody.stats().queued,
291
+ responseHistoryBytes: acquireResponseHistory.stats().activeWeight,
292
+ responseHistoriesActive: acquireResponseHistory.stats().activeRequests,
293
+ responseHistoriesQueued: acquireResponseHistory.stats().queued,
176
294
  });
177
295
  return acquire;
178
296
  }
179
297
 
180
- async function readRequestBuffer(req, maxBytes) {
298
+ function consumeRequestChunks(req, { signal, onChunk }) {
299
+ return new Promise((resolve, reject) => {
300
+ let settled = false;
301
+ const cleanup = () => {
302
+ req.off("data", handleData);
303
+ req.off("end", handleEnd);
304
+ req.off("error", handleError);
305
+ req.off("aborted", handleAborted);
306
+ signal?.removeEventListener("abort", handleSignalAbort);
307
+ };
308
+ const finish = (error) => {
309
+ if (settled) return;
310
+ settled = true;
311
+ cleanup();
312
+ if (error) reject(error);
313
+ else resolve();
314
+ };
315
+ const stopReading = (error) => {
316
+ req.pause?.();
317
+ finish(error);
318
+ };
319
+ const handleData = (chunk) => {
320
+ try {
321
+ onChunk(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
322
+ } catch (error) {
323
+ stopReading(error);
324
+ }
325
+ };
326
+ const handleEnd = () => finish();
327
+ const handleError = (error) => finish(error);
328
+ const handleAborted = () => finish(admissionAbortError(signal));
329
+ const handleSignalAbort = () => stopReading(admissionAbortError(signal));
330
+
331
+ req.once("end", handleEnd);
332
+ req.once("error", handleError);
333
+ req.once("aborted", handleAborted);
334
+ signal?.addEventListener("abort", handleSignalAbort, { once: true });
335
+ if (signal?.aborted) handleSignalAbort();
336
+ else {
337
+ req.on("data", handleData);
338
+ req.resume?.();
339
+ }
340
+ });
341
+ }
342
+
343
+ async function readRequestBuffer(req, maxBytes, signal) {
181
344
  const contentLength = requestContentLength(req);
182
345
  if (Number.isFinite(contentLength) && contentLength > maxBytes) {
183
346
  throw payloadTooLarge("Raw", maxBytes);
@@ -185,16 +348,18 @@ async function readRequestBuffer(req, maxBytes) {
185
348
 
186
349
  const chunks = [];
187
350
  let total = 0;
188
- for await (const chunk of req) {
189
- const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
190
- total += buffer.length;
191
- if (total > maxBytes) throw payloadTooLarge("Raw", maxBytes);
192
- chunks.push(buffer);
193
- }
351
+ await consumeRequestChunks(req, {
352
+ signal,
353
+ onChunk(buffer) {
354
+ total += buffer.length;
355
+ if (total > maxBytes) throw payloadTooLarge("Raw", maxBytes);
356
+ chunks.push(buffer);
357
+ },
358
+ });
194
359
  return Buffer.concat(chunks);
195
360
  }
196
361
 
197
- async function readIdentityText(req, maxBodyBytes, maxDecodedBodyBytes) {
362
+ async function readIdentityText(req, maxBodyBytes, maxDecodedBodyBytes, signal) {
198
363
  const contentLength = requestContentLength(req);
199
364
  if (Number.isFinite(contentLength) && contentLength > maxBodyBytes) {
200
365
  throw payloadTooLarge("Raw", maxBodyBytes);
@@ -206,13 +371,15 @@ async function readIdentityText(req, maxBodyBytes, maxDecodedBodyBytes) {
206
371
  const decoder = new TextDecoder();
207
372
  let text = "";
208
373
  let total = 0;
209
- for await (const chunk of req) {
210
- const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
211
- total += buffer.length;
212
- if (total > maxBodyBytes) throw payloadTooLarge("Raw", maxBodyBytes);
213
- if (total > maxDecodedBodyBytes) throw payloadTooLarge("Decoded", maxDecodedBodyBytes);
214
- text += decoder.decode(buffer, { stream: true });
215
- }
374
+ await consumeRequestChunks(req, {
375
+ signal,
376
+ onChunk(buffer) {
377
+ total += buffer.length;
378
+ if (total > maxBodyBytes) throw payloadTooLarge("Raw", maxBodyBytes);
379
+ if (total > maxDecodedBodyBytes) throw payloadTooLarge("Decoded", maxDecodedBodyBytes);
380
+ text += decoder.decode(buffer, { stream: true });
381
+ },
382
+ });
216
383
  return text + decoder.decode();
217
384
  }
218
385
 
@@ -267,22 +434,34 @@ function parseRequestJson(text) {
267
434
  }
268
435
 
269
436
  export async function readJsonBody(req, {
437
+ admission,
270
438
  maxBodyBytes = MAX_BODY_BYTES,
271
439
  maxDecodedBodyBytes = MAX_DECODED_BODY_BYTES,
440
+ signal,
272
441
  } = {}) {
442
+ if (signal?.aborted) throw admissionAbortError(signal);
273
443
  const encodings = contentEncodings(req.headers?.["content-encoding"]);
274
444
  if (encodings.every((encoding) => encoding === "identity")) {
275
- return parseRequestJson(await readIdentityText(req, maxBodyBytes, maxDecodedBodyBytes));
445
+ return parseRequestJson(await readIdentityText(req, maxBodyBytes, maxDecodedBodyBytes, signal));
276
446
  }
277
- const buffer = await readRequestBuffer(req, maxBodyBytes);
278
- let decoded;
447
+ const buffer = await readRequestBuffer(req, maxBodyBytes, signal);
448
+ const releaseDecompression = await admission?.acquireDecompression?.({ signal }) || (() => {});
279
449
  try {
280
- decoded = await decodeRequestBuffer(buffer, req.headers?.["content-encoding"], maxDecodedBodyBytes);
281
- } catch (e) {
282
- if (e?.statusCode) throw e;
283
- throw httpError(`Invalid compressed request body: ${e.message}`, 400);
450
+ let decoded;
451
+ try {
452
+ decoded = await decodeRequestBuffer(buffer, req.headers?.["content-encoding"], maxDecodedBodyBytes);
453
+ } catch (e) {
454
+ if (e?.statusCode) throw e;
455
+ throw httpError(`Invalid compressed request body: ${e.message}`, 400);
456
+ }
457
+ if (signal?.aborted) throw admissionAbortError(signal);
458
+ if (decoded.length > 0) {
459
+ await admission?.reserveDecodedBody?.(decoded.length, { signal });
460
+ }
461
+ return parseRequestJson(decoded.toString("utf8"));
462
+ } finally {
463
+ releaseDecompression();
284
464
  }
285
- return parseRequestJson(decoded.toString("utf8"));
286
465
  }
287
466
 
288
467
  export function sendJsonError(res, err, fallbackStatus = 400) {
@@ -291,7 +470,10 @@ export function sendJsonError(res, err, fallbackStatus = 400) {
291
470
  res.end();
292
471
  return;
293
472
  }
294
- res.writeHead(err?.statusCode || fallbackStatus, { "Content-Type": "application/json" });
473
+ const statusCode = err?.statusCode || fallbackStatus;
474
+ const headers = { "Content-Type": "application/json" };
475
+ if (statusCode === 408 || statusCode === 413) headers.Connection = "close";
476
+ res.writeHead(statusCode, headers);
295
477
  res.end(JSON.stringify(err?.jsonBody || { error: err?.message || "Request failed" }));
296
478
  }
297
479
 
@@ -327,6 +509,7 @@ export function isAbortLikeError(err) {
327
509
  }
328
510
 
329
511
  export function abortErrorStatusCode(reason) {
512
+ if (reason === "request_body_timeout") return 408;
330
513
  if (["responses_prepare_timeout", "upstream_timeout", "stream_handshake_timeout", "stream_idle_timeout"].includes(reason)) return 504;
331
514
  if (reason === "client_aborted" || reason === "client_closed") return 499;
332
515
  return 502;
@@ -352,6 +535,7 @@ export function createRequestAbort(req, res) {
352
535
  return {
353
536
  signal: controller.signal,
354
537
  get reason() { return reason; },
538
+ abort,
355
539
  setTimeout(ms, nextReason = "upstream_timeout") {
356
540
  if (timer) clearTimeout(timer);
357
541
  timer = null;
@@ -0,0 +1,37 @@
1
+ import fs from "node:fs";
2
+ import os from "node:os";
3
+ import path from "node:path";
4
+ import { atomicWriteFileSync } from "./atomic-file.mjs";
5
+
6
+ export const LEGACY_COMMAND_WARNING_INTERVAL_MS = 7 * 24 * 60 * 60 * 1000;
7
+ export const LEGACY_COMMAND_WARNING = "codex-copilot-dx is deprecated; use ccdx instead. The compatibility alias will be removed in a future breaking release.";
8
+
9
+ export function legacyCommandWarningPath({ env = process.env, home = os.homedir() } = {}) {
10
+ const xdgCacheHome = String(env.XDG_CACHE_HOME || "").trim();
11
+ const cacheRoot = xdgCacheHome || path.join(home, ".cache");
12
+ return path.join(cacheRoot, "codex-copilot-dx", "legacy-command-warning");
13
+ }
14
+
15
+ export function shouldShowLegacyCommandWarning({
16
+ env = process.env,
17
+ home = os.homedir(),
18
+ interactive = process.stderr.isTTY === true,
19
+ now = Date.now(),
20
+ } = {}) {
21
+ if (!interactive) return false;
22
+ const filePath = legacyCommandWarningPath({ env, home });
23
+ try {
24
+ const previous = Number.parseInt(fs.readFileSync(filePath, "utf8"), 10);
25
+ if (Number.isFinite(previous) && Math.abs(now - previous) < LEGACY_COMMAND_WARNING_INTERVAL_MS) {
26
+ return false;
27
+ }
28
+ } catch {
29
+ // A missing or unreadable cache must not prevent the compatibility command.
30
+ }
31
+ try {
32
+ atomicWriteFileSync(filePath, `${now}\n`, { mode: 0o600 });
33
+ } catch {
34
+ // The warning remains best effort when the cache directory is read-only.
35
+ }
36
+ return true;
37
+ }
@@ -252,6 +252,9 @@ async function relayUpstreamResponse(response, reqBody, res, abort, streamIdleTi
252
252
  function safeFailure(error, abort) {
253
253
  if (error?.statusCode && !isAbortLikeError(error)) return error;
254
254
  if (abort?.signal.aborted || isAbortLikeError(error)) {
255
+ if (abort?.reason === "request_body_timeout") {
256
+ return localError(408, "request_timeout", "PM Studio relay request body timed out");
257
+ }
255
258
  return localError(
256
259
  abortErrorStatusCode(abort?.reason),
257
260
  "upstream_timeout",
@@ -277,6 +280,7 @@ export function createPmStudioRelayHandler(options = {}) {
277
280
  const upstreamTimeoutMs = parsePositiveInteger(options.upstreamTimeoutMs, runtime.upstreamTimeoutMs);
278
281
  const streamHandshakeTimeoutMs = parsePositiveInteger(options.streamHandshakeTimeoutMs, runtime.streamHandshakeTimeoutMs);
279
282
  const streamIdleTimeoutMs = parsePositiveInteger(options.streamIdleTimeoutMs, runtime.streamIdleTimeoutMs);
283
+ const requestBodyTimeoutMs = parsePositiveInteger(options.requestBodyTimeoutMs, runtime.requestBodyTimeoutMs);
280
284
  const validationTtlMs = parsePositiveInteger(options.validationTtlMs, DEFAULT_VALIDATION_TTL_MS);
281
285
  const validationCacheSize = parsePositiveInteger(options.validationCacheSize, DEFAULT_VALIDATION_CACHE_SIZE, 4096);
282
286
  const validationConcurrency = parsePositiveInteger(
@@ -430,8 +434,10 @@ export function createPmStudioRelayHandler(options = {}) {
430
434
  async function handlePost(req, res, pathname, token, abort) {
431
435
  let releaseRequest = releaseOnce();
432
436
  try {
433
- releaseRequest = releaseOnce(await acquireRequest(req, { signal: abort.signal }));
434
- const body = await readJsonBody(req);
437
+ const admission = await acquireRequest(req, { signal: abort.signal });
438
+ releaseRequest = releaseOnce(admission);
439
+ abort.setTimeout(requestBodyTimeoutMs, "request_body_timeout");
440
+ const body = await readJsonBody(req, { admission, signal: abort.signal });
435
441
  const supportsStream = pathname === "/chat/completions" || pathname === "/responses";
436
442
  const streaming = supportsStream && body?.stream === true;
437
443
  const requestTimeoutMs = streaming ? streamHandshakeTimeoutMs : upstreamTimeoutMs;
@@ -178,7 +178,7 @@ export function responseHistoryRootId(responseId) {
178
178
  return histories.get(responseId)?.rootId || null;
179
179
  }
180
180
 
181
- export function materializeResponseHistory(responseId) {
181
+ function responseHistoryChain(responseId) {
182
182
  const chain = [];
183
183
  const seen = new Set();
184
184
  let currentId = responseId;
@@ -193,7 +193,16 @@ export function materializeResponseHistory(responseId) {
193
193
  chain.push(entry);
194
194
  currentId = entry.parentId;
195
195
  }
196
- const rootId = chain[0]?.rootId;
196
+ return { chain, rootId: chain[0]?.rootId };
197
+ }
198
+
199
+ export function responseHistoryMaterializedBytes(responseId) {
200
+ const { chain } = responseHistoryChain(responseId);
201
+ return chain.reduce((bytes, entry) => bytes + entry.bytes, 0);
202
+ }
203
+
204
+ export function materializeResponseHistory(responseId) {
205
+ const { chain, rootId } = responseHistoryChain(responseId);
197
206
  const items = [];
198
207
  for (const entry of chain.reverse()) items.push(...entry.inputItems, ...entry.outputItems);
199
208
  const materialized = cloneJson(items);
@@ -143,23 +143,38 @@ export function chatToResponses(chatResp, model) {
143
143
 
144
144
  export async function forwardToChat(chatReq, emitEvent, onDone, onError, options = {}) {
145
145
  delete chatReq.max_tokens;
146
- const upstreamReq = withChatStreamUsage(chatReq);
147
- const bodyText = upstreamReq === chatReq ? options.bodyText : undefined;
146
+ const requestModel = chatReq.model || "unknown";
147
+ let upstreamReq = withChatStreamUsage(chatReq);
148
+ let {
149
+ abort,
150
+ bodyText,
151
+ chatCompletionsFn: configuredChatCompletionsFn,
152
+ onUpstreamResponse,
153
+ onUpstreamStart,
154
+ releaseRequest,
155
+ signal,
156
+ streamIdleTimeoutMs,
157
+ } = options;
158
+ options = undefined;
159
+ if (upstreamReq !== chatReq) bodyText = undefined;
148
160
  let resp;
149
161
  try {
150
- const chatCompletionsFn = options.chatCompletionsFn || chatCompletions;
162
+ const chatCompletionsFn = configuredChatCompletionsFn || chatCompletions;
151
163
  try {
152
- options.onUpstreamStart?.();
164
+ onUpstreamStart?.();
153
165
  resp = await chatCompletionsFn(upstreamReq, {
154
- signal: options.signal,
166
+ signal,
155
167
  bodyText,
156
- onUpstreamStart: options.onUpstreamStart,
168
+ onUpstreamStart,
157
169
  });
158
170
  } finally {
159
- options.releaseRequest?.();
171
+ bodyText = undefined;
172
+ chatReq = undefined;
173
+ upstreamReq = undefined;
174
+ releaseRequest?.();
160
175
  }
161
176
  } catch (e) {
162
- const statusCode = isAbortLikeError(e) ? abortErrorStatusCode(options.abort?.reason) : 502;
177
+ const statusCode = isAbortLikeError(e) ? abortErrorStatusCode(abort?.reason) : 502;
163
178
  await onError(statusCode, e.message);
164
179
  return false;
165
180
  }
@@ -173,10 +188,10 @@ export async function forwardToChat(chatReq, emitEvent, onDone, onError, options
173
188
  await onError(error.statusCode, error.message, error);
174
189
  return false;
175
190
  }
176
- options.onUpstreamResponse?.(resp);
177
- options.abort?.setTimeout(options.streamIdleTimeoutMs, "stream_idle_timeout");
191
+ onUpstreamResponse?.(resp);
192
+ abort?.setTimeout(streamIdleTimeoutMs, "stream_idle_timeout");
178
193
  const respId = `resp_${uid()}`;
179
- let actualModel = chatReq.model || "unknown";
194
+ let actualModel = requestModel;
180
195
  let fullText = "";
181
196
  let messageItem = null;
182
197
  let nextOutputIndex = 0;
@@ -280,7 +295,7 @@ export async function forwardToChat(chatReq, emitEvent, onDone, onError, options
280
295
 
281
296
  try {
282
297
  for await (const line of webStreamLines(resp, {
283
- onChunk: () => options.abort?.setTimeout(options.streamIdleTimeoutMs, "stream_idle_timeout"),
298
+ onChunk: () => abort?.setTimeout(streamIdleTimeoutMs, "stream_idle_timeout"),
284
299
  })) {
285
300
  if (!line.startsWith("data: ")) continue;
286
301
  const data = line.slice(6).trim();
@@ -334,7 +349,7 @@ export async function forwardToChat(chatReq, emitEvent, onDone, onError, options
334
349
  }
335
350
  }
336
351
  } catch (e) {
337
- const statusCode = isAbortLikeError(e) ? abortErrorStatusCode(options.abort?.reason) : (e?.statusCode || 502);
352
+ const statusCode = isAbortLikeError(e) ? abortErrorStatusCode(abort?.reason) : (e?.statusCode || 502);
338
353
  await onError(statusCode, e?.message || "upstream stream error", e);
339
354
  return false;
340
355
  }
@@ -17,7 +17,12 @@ import { chatToResponses, forwardToChat, responsesBodyUsesCustomTools } from "./
17
17
  import { prepareResponsesChatPayload } from "./responses-chat-payload.mjs";
18
18
  import { prepareResponsesCompactionRequest } from "./responses-compaction.mjs";
19
19
  import { proxyCopilotResponses } from "./responses-proxy.mjs";
20
- import { prepareResponsesRequest, rememberResponseHistory } from "./responses-request.mjs";
20
+ import { responseHistoryMaterializedBytes } from "./response-history.mjs";
21
+ import {
22
+ dropMaterializedResponseHistory,
23
+ prepareResponsesRequest,
24
+ rememberResponseHistory,
25
+ } from "./responses-request.mjs";
21
26
  import { status } from "./status.mjs";
22
27
  import { endStreamWithError } from "./stream-errors.mjs";
23
28
  import { safeUpstreamResponseHeaders } from "./upstream-headers.mjs";
@@ -81,12 +86,14 @@ export function createResponsesHandler(options) {
81
86
  getCachedModelEndpointsFn = getCachedModelEndpoints,
82
87
  imagePressure,
83
88
  openAIModelEnv,
89
+ requestBodyTimeoutMs,
84
90
  responsesPayloadOptions,
85
91
  responsesFn,
86
92
  streamHandshakeTimeoutMs,
87
93
  streamIdleTimeoutMs,
88
94
  upstreamTimeoutMs,
89
95
  } = options;
96
+ const now = options.now || Date.now;
90
97
 
91
98
  return async function handleResponses(req, res) {
92
99
  const abort = createRequestAbort(req, res);
@@ -94,10 +101,26 @@ export function createResponsesHandler(options) {
94
101
  let prepared = null;
95
102
  let imagePressureResult = null;
96
103
  let parsed = null;
97
- const startUpstreamTimeout = () => abort.setTimeout(
98
- parsed?.stream ? streamHandshakeTimeoutMs : upstreamTimeoutMs,
99
- parsed?.stream ? "stream_handshake_timeout" : "upstream_timeout",
100
- );
104
+ let upstreamPayloadReleased = false;
105
+ let prepareDeadline = Number.POSITIVE_INFINITY;
106
+ let upstreamStarted = false;
107
+ const assertPrepareActive = () => {
108
+ if (abort.signal.aborted) throw abort.signal.reason;
109
+ if (now() < prepareDeadline) return;
110
+ abort.abort("responses_prepare_timeout");
111
+ throw abort.signal.reason;
112
+ };
113
+ const startUpstreamTimeout = () => {
114
+ if (!upstreamStarted) {
115
+ assertPrepareActive();
116
+ upstreamStarted = true;
117
+ }
118
+ const streaming = prepared?.body?.stream === true;
119
+ abort.setTimeout(
120
+ streaming ? streamHandshakeTimeoutMs : upstreamTimeoutMs,
121
+ streaming ? "stream_handshake_timeout" : "upstream_timeout",
122
+ );
123
+ };
101
124
  const applyAdaptiveTimeout = (error) => {
102
125
  if (!["responses_prepare_timeout", "stream_handshake_timeout", "upstream_timeout"].includes(abort.reason)) {
103
126
  return error;
@@ -118,21 +141,47 @@ export function createResponsesHandler(options) {
118
141
  };
119
142
  return error;
120
143
  };
144
+ const markAdaptiveHttpTimeout = (statusCode) => {
145
+ if (statusCode !== 408) return false;
146
+ return imagePressure?.markTimeout?.(prepared?.historyRootId, {
147
+ eligible: imagePressureResult?.pressureEligible === true,
148
+ }) || false;
149
+ };
150
+ const releaseUpstreamPayload = (finalContext) => {
151
+ if (upstreamPayloadReleased) return;
152
+ upstreamPayloadReleased = true;
153
+ if (finalContext && finalContext !== prepared) dropMaterializedResponseHistory(finalContext);
154
+ dropMaterializedResponseHistory(prepared);
155
+ releaseRequest();
156
+ };
121
157
  try {
122
158
  releaseRequest = await acquireRequest(req, { signal: abort.signal });
123
- parsed = await readJsonBody(req);
159
+ abort.setTimeout(requestBodyTimeoutMs, "request_body_timeout");
160
+ parsed = await readJsonBody(req, { admission: releaseRequest, signal: abort.signal });
161
+ prepareDeadline = now() + upstreamTimeoutMs;
124
162
  abort.setTimeout(upstreamTimeoutMs, "responses_prepare_timeout");
163
+ if (parsed.previous_response_id !== undefined && parsed.previous_response_id !== null) {
164
+ const historyBytes = responseHistoryMaterializedBytes(parsed.previous_response_id);
165
+ if (historyBytes > 0) {
166
+ await releaseRequest.reserveResponseHistory?.(historyBytes, { signal: abort.signal });
167
+ }
168
+ assertPrepareActive();
169
+ }
125
170
  prepared = prepareResponsesRequest(parsed, { mutate: true });
171
+ parsed = null;
172
+ assertPrepareActive();
126
173
  imagePressureResult = imagePressure?.apply?.(prepared) || null;
174
+ assertPrepareActive();
127
175
  if (imagePressureResult?.adapted) {
128
176
  console.warn(status("warn", `responses visual history mode=${imagePressureResult.mode} historical_images=${imagePressureResult.initialHistoricalImages}->${imagePressureResult.historicalImages} omitted=${imagePressureResult.imagesOmitted} bytes=${imagePressureResult.initialBodyBytes}->${imagePressureResult.bodyBytes}`));
129
177
  }
130
178
  prepared.surface = "responses";
131
- const model = parsed.model || "unknown";
179
+ const model = prepared.body.model || "unknown";
180
+ const streaming = prepared.body.stream === true;
132
181
  const { requestedModel, upstreamModel } = resolveRequestModel(model, openAIModelEnv, autoReviewModelResolver);
133
182
  if (upstreamModel !== requestedModel) prepared.body.model = upstreamModel;
134
183
  const upstreamLog = upstreamModel === requestedModel ? "" : ` upstream_model=${upstreamModel}`;
135
- console.log(status("info", `responses model=${requestedModel}${upstreamLog} stream=${!!parsed.stream}`));
184
+ console.log(status("info", `responses model=${requestedModel}${upstreamLog} stream=${streaming}`));
136
185
  const usesCustomTools = responsesBodyUsesCustomTools(prepared.body);
137
186
  const useNativeResponses = requestedModel === CODEX_AUTO_REVIEW_MODEL
138
187
  || isResponsesOnlyModel(upstreamModel, getCachedModelEndpointsFn)
@@ -143,18 +192,23 @@ export function createResponsesHandler(options) {
143
192
  signal: abort.signal,
144
193
  abort,
145
194
  onUpstreamStart: startUpstreamTimeout,
146
- releaseRequest,
195
+ releaseRequest: releaseUpstreamPayload,
147
196
  streamIdleTimeoutMs,
148
197
  });
149
198
  if (result?.successful) imagePressure?.markSuccess?.(prepared.historyRootId);
199
+ else markAdaptiveHttpTimeout(result?.upstreamStatus);
150
200
  } else {
151
- const chatPayload = await prepareResponsesChatPayload(prepared, {
201
+ let { chatReq, bodyText } = await prepareResponsesChatPayload(prepared, {
152
202
  payloadOptions: responsesPayloadOptions,
153
203
  signal: abort.signal,
154
- stream: parsed.stream,
204
+ stream: streaming,
155
205
  });
156
- const { chatReq, bodyText } = chatPayload;
157
- if (parsed.stream) {
206
+ const releaseChatPayload = () => {
207
+ chatReq = undefined;
208
+ bodyText = undefined;
209
+ releaseUpstreamPayload();
210
+ };
211
+ if (streaming) {
158
212
  let streamResponseHeaders = null;
159
213
  const successful = await forwardToChat(chatReq, async (event, data) => {
160
214
  if (!res.headersSent) res.writeHead(200, {
@@ -170,6 +224,7 @@ export function createResponsesHandler(options) {
170
224
  }, () => { if (!res.writableEnded) res.end(); }, async (statusCode, errMsg, error, upstreamResponse) => {
171
225
  if (!res.headersSent) {
172
226
  if (upstreamResponse) {
227
+ markAdaptiveHttpTimeout(upstreamResponse.status);
173
228
  sendUpstreamError(res, upstreamResponse, errMsg);
174
229
  return;
175
230
  }
@@ -184,7 +239,7 @@ export function createResponsesHandler(options) {
184
239
  onUpstreamStart: startUpstreamTimeout,
185
240
  streamIdleTimeoutMs,
186
241
  chatCompletionsFn,
187
- releaseRequest,
242
+ releaseRequest: releaseChatPayload,
188
243
  bodyText,
189
244
  onUpstreamResponse: (upstreamResponse) => {
190
245
  streamResponseHeaders = safeUpstreamResponseHeaders(upstreamResponse.headers, {
@@ -201,9 +256,10 @@ export function createResponsesHandler(options) {
201
256
  bodyText,
202
257
  onUpstreamStart: startUpstreamTimeout,
203
258
  });
204
- releaseRequest();
259
+ releaseChatPayload();
205
260
  const data = await upstream.text();
206
261
  if (!upstream.ok) {
262
+ markAdaptiveHttpTimeout(upstream.status);
207
263
  sendUpstreamError(res, upstream, data);
208
264
  return;
209
265
  }
@@ -227,7 +283,7 @@ export function createResponsesHandler(options) {
227
283
  logRequestFailure("Responses", responseError, abort);
228
284
  sendJsonError(res, responseError, 502);
229
285
  } finally {
230
- releaseRequest();
286
+ releaseUpstreamPayload();
231
287
  abort.cleanup();
232
288
  }
233
289
  };
@@ -239,20 +295,46 @@ export function createResponsesCompactHandler(options) {
239
295
  autoReviewModelResolver,
240
296
  imagePressure,
241
297
  openAIModelEnv,
298
+ requestBodyTimeoutMs,
242
299
  responsesCompactFn,
243
300
  upstreamTimeoutMs,
244
301
  } = options;
302
+ const now = options.now || Date.now;
245
303
 
246
304
  return async function handleResponsesCompact(req, res) {
247
305
  const abort = createRequestAbort(req, res);
248
306
  let releaseRequest = () => {};
249
307
  try {
250
308
  releaseRequest = await acquireRequest(req, { signal: abort.signal });
251
- const parsed = await readJsonBody(req);
309
+ abort.setTimeout(requestBodyTimeoutMs, "request_body_timeout");
310
+ const parsed = await readJsonBody(req, { admission: releaseRequest, signal: abort.signal });
311
+ const prepareDeadline = now() + upstreamTimeoutMs;
312
+ const assertPrepareActive = () => {
313
+ if (abort.signal.aborted) throw abort.signal.reason;
314
+ if (now() < prepareDeadline) return;
315
+ abort.abort("responses_prepare_timeout");
316
+ throw abort.signal.reason;
317
+ };
318
+ let upstreamStarted = false;
319
+ const startUpstreamTimeout = () => {
320
+ if (!upstreamStarted) {
321
+ assertPrepareActive();
322
+ upstreamStarted = true;
323
+ }
324
+ abort.setTimeout(upstreamTimeoutMs, "upstream_timeout");
325
+ };
252
326
  abort.setTimeout(upstreamTimeoutMs, "responses_prepare_timeout");
327
+ if (parsed.previous_response_id !== undefined && parsed.previous_response_id !== null) {
328
+ const historyBytes = responseHistoryMaterializedBytes(parsed.previous_response_id);
329
+ if (historyBytes > 0) {
330
+ await releaseRequest.reserveResponseHistory?.(historyBytes, { signal: abort.signal });
331
+ }
332
+ assertPrepareActive();
333
+ }
253
334
  const prepared = prepareResponsesCompactionRequest(
254
335
  prepareResponsesRequest(parsed, { mutate: true }),
255
336
  );
337
+ assertPrepareActive();
256
338
  prepared.surface = "responses_compact";
257
339
  const model = parsed.model || "unknown";
258
340
  const { requestedModel, upstreamModel } = resolveRequestModel(model, openAIModelEnv, autoReviewModelResolver);
@@ -262,7 +344,7 @@ export function createResponsesCompactHandler(options) {
262
344
  const result = await proxyCopilotResponses(prepared, req, res, responsesCompactFn, {
263
345
  signal: abort.signal,
264
346
  abort,
265
- onUpstreamStart: () => abort.setTimeout(upstreamTimeoutMs, "upstream_timeout"),
347
+ onUpstreamStart: startUpstreamTimeout,
266
348
  releaseRequest,
267
349
  });
268
350
  if (result?.compacted) imagePressure?.clear?.(prepared.historyRootId);
@@ -32,8 +32,24 @@ export function applyResponsesImagePressure(reqContext, {
32
32
  const currentInputStart = Number.isFinite(reqContext?.currentInputStart)
33
33
  ? reqContext.currentInputStart
34
34
  : 0;
35
- const initial = bodyPayload(body);
36
35
  const initialImages = responsesHistoricalImageStats(body?.input, currentInputStart);
36
+ if (initialImages.historicalImages === 0) {
37
+ return {
38
+ mode: "normal",
39
+ pressureEligible: false,
40
+ adapted: false,
41
+ imagesOmitted: 0,
42
+ initialBodyBytes: null,
43
+ bodyBytes: null,
44
+ initialHistoricalImages: 0,
45
+ historicalImages: 0,
46
+ currentImages: initialImages.currentImages,
47
+ overBudget: false,
48
+ overImageBudget: false,
49
+ };
50
+ }
51
+
52
+ const initial = bodyPayload(body);
37
53
  const pressureEligible = initialImages.historicalImages > 0 && (
38
54
  initialImages.historicalImages > policy.triggerHistoricalImages
39
55
  || initial.bodyBytes > policy.triggerBodyBytes
@@ -70,12 +86,13 @@ export function applyResponsesImagePressure(reqContext, {
70
86
  initialBodyText: initial.bodyText,
71
87
  initialBodyBytes: initial.bodyBytes,
72
88
  });
89
+ const { bodyText: _bodyText, ...trimmedResult } = trimmed;
73
90
  return {
74
91
  mode: recovery ? "recovery" : "pressure",
75
92
  pressureEligible: pressureEligible || recovery,
76
93
  initialBodyBytes: initial.bodyBytes,
77
94
  initialHistoricalImages: initialImages.historicalImages,
78
- ...trimmed,
95
+ ...trimmedResult,
79
96
  };
80
97
  }
81
98
 
@@ -120,13 +120,13 @@ export async function proxyCopilotResponses(reqContext, req, res, upstream = cop
120
120
  try {
121
121
  opened = await openCopilotResponse(reqContext, upstream, options);
122
122
  } finally {
123
- options.releaseRequest?.();
123
+ options.releaseRequest?.(opened?.reqContext);
124
124
  }
125
125
  const { resp, errorText } = opened;
126
126
  reqContext = opened.reqContext;
127
127
  if (errorText !== undefined) {
128
128
  sendUpstreamError(res, resp, errorText);
129
- return { successful: false, compacted: false };
129
+ return { successful: false, compacted: false, upstreamStatus: resp.status };
130
130
  }
131
131
 
132
132
  if (reqContext.body.stream) {
@@ -245,6 +245,14 @@ export function prepareResponsesRequest(reqBody, { mutate = false } = {}) {
245
245
  };
246
246
  }
247
247
 
248
+ export function dropMaterializedResponseHistory(reqContext) {
249
+ if (!reqContext?.historyParentId || !Array.isArray(reqContext.historyInputItems)) return false;
250
+ reqContext.body = { ...reqContext.body, input: reqContext.historyInputItems };
251
+ reqContext.inputItems = reqContext.historyInputItems;
252
+ reqContext.currentInputStart = 0;
253
+ return true;
254
+ }
255
+
248
256
  export function rememberResponseHistory(reqContext, responseJson) {
249
257
  if (!responseJson?.id || !Array.isArray(reqContext?.historyInputItems || reqContext?.inputItems)) return;
250
258
  const compacted = isSuccessfulCompactionResponse(responseJson);
@@ -2,6 +2,7 @@ export const RUNTIME_DEFAULTS = Object.freeze({
2
2
  upstreamTimeoutMs: 120 * 1000,
3
3
  streamHandshakeTimeoutMs: 120 * 1000,
4
4
  streamIdleTimeoutMs: 120 * 1000,
5
+ requestBodyTimeoutMs: 120 * 1000,
5
6
  maxSseBufferBytes: 8 * 1024 * 1024,
6
7
  maxBodyBytes: 64 * 1024 * 1024,
7
8
  maxDecodedBodyBytes: 128 * 1024 * 1024,
@@ -24,6 +25,7 @@ export function loadRuntimeConfig(env = process.env) {
24
25
  upstreamTimeoutMs: parsePositiveInteger(env.CCDX_UPSTREAM_TIMEOUT_MS, RUNTIME_DEFAULTS.upstreamTimeoutMs),
25
26
  streamHandshakeTimeoutMs: parsePositiveInteger(env.CCDX_STREAM_HANDSHAKE_TIMEOUT_MS, RUNTIME_DEFAULTS.streamHandshakeTimeoutMs),
26
27
  streamIdleTimeoutMs: parsePositiveInteger(env.CCDX_STREAM_IDLE_TIMEOUT_MS, RUNTIME_DEFAULTS.streamIdleTimeoutMs),
28
+ requestBodyTimeoutMs: parsePositiveInteger(env.CCDX_REQUEST_BODY_TIMEOUT_MS, RUNTIME_DEFAULTS.requestBodyTimeoutMs),
27
29
  maxSseBufferBytes: parsePositiveInteger(env.CCDX_MAX_SSE_BUFFER_BYTES, RUNTIME_DEFAULTS.maxSseBufferBytes),
28
30
  maxBodyBytes: parsePositiveInteger(env.CCDX_MAX_BODY_BYTES, RUNTIME_DEFAULTS.maxBodyBytes),
29
31
  maxDecodedBodyBytes: parsePositiveInteger(env.CCDX_MAX_DECODED_BODY_BYTES, RUNTIME_DEFAULTS.maxDecodedBodyBytes),