codex-copilot-dx 0.6.2 → 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.
@@ -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";
@@ -79,53 +84,133 @@ export function createResponsesHandler(options) {
79
84
  autoReviewModelResolver,
80
85
  chatCompletionsFn,
81
86
  getCachedModelEndpointsFn = getCachedModelEndpoints,
87
+ imagePressure,
82
88
  openAIModelEnv,
89
+ requestBodyTimeoutMs,
83
90
  responsesPayloadOptions,
84
91
  responsesFn,
85
92
  streamHandshakeTimeoutMs,
86
93
  streamIdleTimeoutMs,
87
94
  upstreamTimeoutMs,
88
95
  } = options;
96
+ const now = options.now || Date.now;
89
97
 
90
98
  return async function handleResponses(req, res) {
91
99
  const abort = createRequestAbort(req, res);
92
100
  let releaseRequest = () => {};
93
- try {
94
- releaseRequest = await acquireRequest(req, { signal: abort.signal });
95
- const parsed = await readJsonBody(req);
101
+ let prepared = null;
102
+ let imagePressureResult = null;
103
+ let parsed = null;
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;
96
119
  abort.setTimeout(
97
- parsed.stream ? streamHandshakeTimeoutMs : upstreamTimeoutMs,
98
- parsed.stream ? "stream_handshake_timeout" : "upstream_timeout",
120
+ streaming ? streamHandshakeTimeoutMs : upstreamTimeoutMs,
121
+ streaming ? "stream_handshake_timeout" : "upstream_timeout",
99
122
  );
100
- const prepared = prepareResponsesRequest(parsed, { mutate: true });
123
+ };
124
+ const applyAdaptiveTimeout = (error) => {
125
+ if (!["responses_prepare_timeout", "stream_handshake_timeout", "upstream_timeout"].includes(abort.reason)) {
126
+ return error;
127
+ }
128
+ const activated = imagePressure?.markTimeout?.(prepared?.historyRootId, {
129
+ eligible: imagePressureResult?.pressureEligible === true,
130
+ });
131
+ if (!activated) return error;
132
+ const message = "Responses timed out while processing a large visual history. Retry the request; ccdx will keep fewer earlier images automatically. Restart is not required.";
133
+ error.statusCode = 504;
134
+ error.jsonBody = {
135
+ error: {
136
+ message,
137
+ type: "timeout_error",
138
+ code: "ccdx_visual_history_timeout",
139
+ retryable: true,
140
+ },
141
+ };
142
+ return error;
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
+ };
157
+ try {
158
+ releaseRequest = await acquireRequest(req, { signal: abort.signal });
159
+ abort.setTimeout(requestBodyTimeoutMs, "request_body_timeout");
160
+ parsed = await readJsonBody(req, { admission: releaseRequest, signal: abort.signal });
161
+ prepareDeadline = now() + upstreamTimeoutMs;
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
+ }
170
+ prepared = prepareResponsesRequest(parsed, { mutate: true });
171
+ parsed = null;
172
+ assertPrepareActive();
173
+ imagePressureResult = imagePressure?.apply?.(prepared) || null;
174
+ assertPrepareActive();
175
+ if (imagePressureResult?.adapted) {
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}`));
177
+ }
101
178
  prepared.surface = "responses";
102
- const model = parsed.model || "unknown";
179
+ const model = prepared.body.model || "unknown";
180
+ const streaming = prepared.body.stream === true;
103
181
  const { requestedModel, upstreamModel } = resolveRequestModel(model, openAIModelEnv, autoReviewModelResolver);
104
182
  if (upstreamModel !== requestedModel) prepared.body.model = upstreamModel;
105
183
  const upstreamLog = upstreamModel === requestedModel ? "" : ` upstream_model=${upstreamModel}`;
106
- console.log(status("info", `responses model=${requestedModel}${upstreamLog} stream=${!!parsed.stream}`));
184
+ console.log(status("info", `responses model=${requestedModel}${upstreamLog} stream=${streaming}`));
107
185
  const usesCustomTools = responsesBodyUsesCustomTools(prepared.body);
108
186
  const useNativeResponses = requestedModel === CODEX_AUTO_REVIEW_MODEL
109
187
  || isResponsesOnlyModel(upstreamModel, getCachedModelEndpointsFn)
110
188
  || (usesCustomTools && cachedModelSupportsResponses(upstreamModel, getCachedModelEndpointsFn));
111
189
  if (usesCustomTools && !useNativeResponses) throw unsupportedCustomToolsError(upstreamModel);
112
190
  if (useNativeResponses) {
113
- await proxyCopilotResponses(prepared, req, res, responsesFn, {
191
+ const result = await proxyCopilotResponses(prepared, req, res, responsesFn, {
114
192
  signal: abort.signal,
115
193
  abort,
116
- releaseRequest,
194
+ onUpstreamStart: startUpstreamTimeout,
195
+ releaseRequest: releaseUpstreamPayload,
117
196
  streamIdleTimeoutMs,
118
197
  });
198
+ if (result?.successful) imagePressure?.markSuccess?.(prepared.historyRootId);
199
+ else markAdaptiveHttpTimeout(result?.upstreamStatus);
119
200
  } else {
120
- const chatPayload = await prepareResponsesChatPayload(prepared, {
201
+ let { chatReq, bodyText } = await prepareResponsesChatPayload(prepared, {
121
202
  payloadOptions: responsesPayloadOptions,
122
203
  signal: abort.signal,
123
- stream: parsed.stream,
204
+ stream: streaming,
124
205
  });
125
- const { chatReq, bodyText } = chatPayload;
126
- if (parsed.stream) {
206
+ const releaseChatPayload = () => {
207
+ chatReq = undefined;
208
+ bodyText = undefined;
209
+ releaseUpstreamPayload();
210
+ };
211
+ if (streaming) {
127
212
  let streamResponseHeaders = null;
128
- await forwardToChat(chatReq, async (event, data) => {
213
+ const successful = await forwardToChat(chatReq, async (event, data) => {
129
214
  if (!res.headersSent) res.writeHead(200, {
130
215
  ...streamResponseHeaders,
131
216
  "Cache-Control": "no-cache",
@@ -139,10 +224,11 @@ export function createResponsesHandler(options) {
139
224
  }, () => { if (!res.writableEnded) res.end(); }, async (statusCode, errMsg, error, upstreamResponse) => {
140
225
  if (!res.headersSent) {
141
226
  if (upstreamResponse) {
227
+ markAdaptiveHttpTimeout(upstreamResponse.status);
142
228
  sendUpstreamError(res, upstreamResponse, errMsg);
143
229
  return;
144
230
  }
145
- const responseError = error || Object.assign(new Error(errMsg), { statusCode });
231
+ const responseError = applyAdaptiveTimeout(error || Object.assign(new Error(errMsg), { statusCode }));
146
232
  sendJsonError(res, responseError, statusCode || 500);
147
233
  return;
148
234
  }
@@ -150,9 +236,10 @@ export function createResponsesHandler(options) {
150
236
  }, {
151
237
  signal: abort.signal,
152
238
  abort,
239
+ onUpstreamStart: startUpstreamTimeout,
153
240
  streamIdleTimeoutMs,
154
241
  chatCompletionsFn,
155
- releaseRequest,
242
+ releaseRequest: releaseChatPayload,
156
243
  bodyText,
157
244
  onUpstreamResponse: (upstreamResponse) => {
158
245
  streamResponseHeaders = safeUpstreamResponseHeaders(upstreamResponse.headers, {
@@ -160,33 +247,43 @@ export function createResponsesHandler(options) {
160
247
  });
161
248
  },
162
249
  });
250
+ if (successful) imagePressure?.markSuccess?.(prepared.historyRootId);
163
251
  } else {
164
252
  try {
165
- const upstream = await chatCompletionsFn(chatReq, { signal: abort.signal, bodyText });
166
- releaseRequest();
253
+ startUpstreamTimeout();
254
+ const upstream = await chatCompletionsFn(chatReq, {
255
+ signal: abort.signal,
256
+ bodyText,
257
+ onUpstreamStart: startUpstreamTimeout,
258
+ });
259
+ releaseChatPayload();
167
260
  const data = await upstream.text();
168
261
  if (!upstream.ok) {
262
+ markAdaptiveHttpTimeout(upstream.status);
169
263
  sendUpstreamError(res, upstream, data);
170
264
  return;
171
265
  }
172
266
  const response = chatToResponses(JSON.parse(data), model);
173
267
  rememberResponseHistory(prepared, response);
268
+ imagePressure?.markSuccess?.(prepared.historyRootId);
174
269
  recordResponsesUsage({ surface: prepared.surface, mode: "json", model, response, event: response });
175
270
  res.writeHead(200, safeUpstreamResponseHeaders(upstream.headers, {
176
271
  contentType: "application/json",
177
272
  }));
178
273
  res.end(JSON.stringify(response));
179
274
  } catch (error) {
180
- logRequestFailure("Responses", error, abort);
181
- sendJsonError(res, error, 502);
275
+ const responseError = applyAdaptiveTimeout(error);
276
+ logRequestFailure("Responses", responseError, abort);
277
+ sendJsonError(res, responseError, 502);
182
278
  }
183
279
  }
184
280
  }
185
281
  } catch (error) {
186
- logRequestFailure("Responses", error, abort);
187
- sendJsonError(res, error, 502);
282
+ const responseError = applyAdaptiveTimeout(error);
283
+ logRequestFailure("Responses", responseError, abort);
284
+ sendJsonError(res, responseError, 502);
188
285
  } finally {
189
- releaseRequest();
286
+ releaseUpstreamPayload();
190
287
  abort.cleanup();
191
288
  }
192
289
  };
@@ -196,32 +293,61 @@ export function createResponsesCompactHandler(options) {
196
293
  const {
197
294
  acquireRequest,
198
295
  autoReviewModelResolver,
296
+ imagePressure,
199
297
  openAIModelEnv,
298
+ requestBodyTimeoutMs,
200
299
  responsesCompactFn,
201
300
  upstreamTimeoutMs,
202
301
  } = options;
302
+ const now = options.now || Date.now;
203
303
 
204
304
  return async function handleResponsesCompact(req, res) {
205
305
  const abort = createRequestAbort(req, res);
206
306
  let releaseRequest = () => {};
207
307
  try {
208
308
  releaseRequest = await acquireRequest(req, { signal: abort.signal });
209
- const parsed = await readJsonBody(req);
210
- abort.setTimeout(upstreamTimeoutMs, "upstream_timeout");
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
+ };
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
+ }
211
334
  const prepared = prepareResponsesCompactionRequest(
212
335
  prepareResponsesRequest(parsed, { mutate: true }),
213
336
  );
337
+ assertPrepareActive();
214
338
  prepared.surface = "responses_compact";
215
339
  const model = parsed.model || "unknown";
216
340
  const { requestedModel, upstreamModel } = resolveRequestModel(model, openAIModelEnv, autoReviewModelResolver);
217
341
  if (upstreamModel !== requestedModel) prepared.body.model = upstreamModel;
218
342
  const upstreamLog = upstreamModel === requestedModel ? "" : ` upstream_model=${upstreamModel}`;
219
343
  console.log(status("info", `responses compact model=${requestedModel}${upstreamLog} stream=false`));
220
- await proxyCopilotResponses(prepared, req, res, responsesCompactFn, {
344
+ const result = await proxyCopilotResponses(prepared, req, res, responsesCompactFn, {
221
345
  signal: abort.signal,
222
346
  abort,
347
+ onUpstreamStart: startUpstreamTimeout,
223
348
  releaseRequest,
224
349
  });
350
+ if (result?.compacted) imagePressure?.clear?.(prepared.historyRootId);
225
351
  } catch (error) {
226
352
  logRequestFailure("Responses compact", error, abort);
227
353
  sendJsonError(res, error, 502);
@@ -0,0 +1,180 @@
1
+ import {
2
+ responsesHistoricalImageStats,
3
+ trimResponsesHistoricalImages,
4
+ } from "./responses-byte-budget.mjs";
5
+
6
+ const MIB = 1024 * 1024;
7
+
8
+ export const DEFAULT_RESPONSES_IMAGE_PRESSURE_POLICY = Object.freeze({
9
+ triggerHistoricalImages: 24,
10
+ triggerBodyBytes: 18 * MIB,
11
+ pressureHistoricalImages: 16,
12
+ pressureBodyBytes: 16 * MIB,
13
+ recoveryHistoricalImages: 8,
14
+ recoveryBodyBytes: 10 * MIB,
15
+ });
16
+
17
+ function normalizedPolicy(policy = {}) {
18
+ return { ...DEFAULT_RESPONSES_IMAGE_PRESSURE_POLICY, ...policy };
19
+ }
20
+
21
+ function bodyPayload(body) {
22
+ const bodyText = JSON.stringify(body);
23
+ return { bodyText, bodyBytes: Buffer.byteLength(bodyText) };
24
+ }
25
+
26
+ export function applyResponsesImagePressure(reqContext, {
27
+ mode = "normal",
28
+ policy: policyOverrides,
29
+ } = {}) {
30
+ const policy = normalizedPolicy(policyOverrides);
31
+ const body = reqContext?.body;
32
+ const currentInputStart = Number.isFinite(reqContext?.currentInputStart)
33
+ ? reqContext.currentInputStart
34
+ : 0;
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);
53
+ const pressureEligible = initialImages.historicalImages > 0 && (
54
+ initialImages.historicalImages > policy.triggerHistoricalImages
55
+ || initial.bodyBytes > policy.triggerBodyBytes
56
+ );
57
+ const recovery = mode === "recovery";
58
+ const maxHistoricalImages = recovery
59
+ ? policy.recoveryHistoricalImages
60
+ : policy.pressureHistoricalImages;
61
+ const targetBytes = recovery ? policy.recoveryBodyBytes : policy.pressureBodyBytes;
62
+ const shouldAdapt = initialImages.historicalImages > 0 && (recovery
63
+ ? initialImages.historicalImages > maxHistoricalImages || initial.bodyBytes > targetBytes
64
+ : pressureEligible);
65
+
66
+ if (!shouldAdapt) {
67
+ return {
68
+ mode: "normal",
69
+ pressureEligible,
70
+ adapted: false,
71
+ imagesOmitted: 0,
72
+ initialBodyBytes: initial.bodyBytes,
73
+ bodyBytes: initial.bodyBytes,
74
+ initialHistoricalImages: initialImages.historicalImages,
75
+ historicalImages: initialImages.historicalImages,
76
+ currentImages: initialImages.currentImages,
77
+ overBudget: false,
78
+ overImageBudget: false,
79
+ };
80
+ }
81
+
82
+ const trimmed = trimResponsesHistoricalImages(body, {
83
+ currentInputStart,
84
+ maxHistoricalImages,
85
+ targetBytes,
86
+ initialBodyText: initial.bodyText,
87
+ initialBodyBytes: initial.bodyBytes,
88
+ });
89
+ const { bodyText: _bodyText, ...trimmedResult } = trimmed;
90
+ return {
91
+ mode: recovery ? "recovery" : "pressure",
92
+ pressureEligible: pressureEligible || recovery,
93
+ initialBodyBytes: initial.bodyBytes,
94
+ initialHistoricalImages: initialImages.historicalImages,
95
+ ...trimmedResult,
96
+ };
97
+ }
98
+
99
+ export function createResponsesImagePressureController({
100
+ now = Date.now,
101
+ maxEntries = 256,
102
+ recoveryTtlMs = 10 * 60 * 1000,
103
+ successesToClear = 2,
104
+ policy: policyOverrides,
105
+ } = {}) {
106
+ const policy = normalizedPolicy(policyOverrides);
107
+ const recovery = new Map();
108
+ const counters = {
109
+ adaptedRequests: 0,
110
+ imagesOmitted: 0,
111
+ recoveryActivations: 0,
112
+ timeoutsRecorded: 0,
113
+ };
114
+
115
+ const prune = () => {
116
+ const timestamp = now();
117
+ for (const [rootId, state] of recovery) {
118
+ if (state.expiresAt <= timestamp) recovery.delete(rootId);
119
+ }
120
+ while (recovery.size > maxEntries) recovery.delete(recovery.keys().next().value);
121
+ };
122
+
123
+ const modeFor = (rootId) => {
124
+ prune();
125
+ return rootId && recovery.has(rootId) ? "recovery" : "normal";
126
+ };
127
+
128
+ return Object.freeze({
129
+ apply(reqContext) {
130
+ const result = applyResponsesImagePressure(reqContext, {
131
+ mode: modeFor(reqContext?.historyRootId),
132
+ policy,
133
+ });
134
+ if (result.adapted) {
135
+ counters.adaptedRequests += 1;
136
+ counters.imagesOmitted += result.imagesOmitted;
137
+ }
138
+ return result;
139
+ },
140
+ markTimeout(rootId, { eligible = false } = {}) {
141
+ if (!rootId || !eligible) return false;
142
+ prune();
143
+ const existing = recovery.get(rootId);
144
+ recovery.delete(rootId);
145
+ recovery.set(rootId, { expiresAt: now() + recoveryTtlMs, successes: 0 });
146
+ counters.timeoutsRecorded += 1;
147
+ if (!existing) counters.recoveryActivations += 1;
148
+ prune();
149
+ return true;
150
+ },
151
+ markSuccess(rootId) {
152
+ if (!rootId) return false;
153
+ prune();
154
+ const state = recovery.get(rootId);
155
+ if (!state) return false;
156
+ state.successes += 1;
157
+ if (state.successes >= successesToClear) {
158
+ recovery.delete(rootId);
159
+ return true;
160
+ }
161
+ return false;
162
+ },
163
+ clear(rootId) {
164
+ return rootId ? recovery.delete(rootId) : false;
165
+ },
166
+ snapshot() {
167
+ prune();
168
+ return {
169
+ active_recovery_trees: recovery.size,
170
+ adapted_requests: counters.adaptedRequests,
171
+ historical_images_omitted: counters.imagesOmitted,
172
+ recovery_activations: counters.recoveryActivations,
173
+ timeouts_recorded: counters.timeoutsRecorded,
174
+ recovery_ttl_ms: recoveryTtlMs,
175
+ pressure_image_limit: policy.pressureHistoricalImages,
176
+ recovery_image_limit: policy.recoveryHistoricalImages,
177
+ };
178
+ },
179
+ });
180
+ }
@@ -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;
129
+ return { successful: false, compacted: false, upstreamStatus: resp.status };
130
130
  }
131
131
 
132
132
  if (reqContext.body.stream) {
@@ -158,7 +158,7 @@ export async function proxyCopilotResponses(reqContext, req, res, upstream = cop
158
158
  if (!streamState.sawTerminal) throw incompleteUpstreamStream("a terminal Responses event");
159
159
  storeCompletedResponse(reqContext, streamState.completed);
160
160
  res.end();
161
- return;
161
+ return { successful: Boolean(streamState.completed), compacted: false };
162
162
  }
163
163
  options.abort?.setTimeout(options.streamIdleTimeoutMs, "stream_idle_timeout");
164
164
  buffer += decoder.decode(value, { stream: true });
@@ -170,12 +170,13 @@ export async function proxyCopilotResponses(reqContext, req, res, upstream = cop
170
170
  if (streamState.sawTerminal) {
171
171
  storeCompletedResponse(reqContext, streamState.completed);
172
172
  res.end();
173
- return;
173
+ return { successful: Boolean(streamState.completed), compacted: false };
174
174
  }
175
175
  }
176
176
  } catch (e) {
177
177
  logRequestFailure("Responses", e, options.abort);
178
178
  await endStreamWithError(res, "responses", e, options.abort);
179
+ return { successful: false, compacted: false };
179
180
  } finally {
180
181
  await reader.cancel().catch(() => {});
181
182
  reader.releaseLock();
@@ -199,7 +200,7 @@ export async function proxyCopilotResponses(reqContext, req, res, upstream = cop
199
200
  contentType: "application/json",
200
201
  }));
201
202
  res.end(JSON.stringify(response));
202
- return;
203
+ return { successful: true, compacted: true };
203
204
  }
204
205
  res.writeHead(resp.status, safeUpstreamResponseHeaders(resp.headers, {
205
206
  contentType: "application/json",
@@ -216,7 +217,9 @@ export async function proxyCopilotResponses(reqContext, req, res, upstream = cop
216
217
  response,
217
218
  event: response,
218
219
  });
220
+ return { successful: true, compacted: false };
219
221
  } catch {}
220
222
  }
223
+ return { successful: false, compacted: false };
221
224
  }
222
225
  }
@@ -1,7 +1,11 @@
1
1
  import { responses as copilotResponses } from "./copilot.mjs";
2
2
  import { httpError } from "./http-transport.mjs";
3
3
  import { debugLog } from "./log.mjs";
4
- import { materializeResponseHistory, rememberResponseHistoryNode } from "./response-history.mjs";
4
+ import {
5
+ materializeResponseHistory,
6
+ rememberResponseHistoryNode,
7
+ responseHistoryRootId,
8
+ } from "./response-history.mjs";
5
9
  import { enforceResponsesImageLimit } from "./responses-image-limit.mjs";
6
10
  import { status } from "./status.mjs";
7
11
 
@@ -157,6 +161,7 @@ export async function openCopilotResponse(reqContext, upstream = copilotResponse
157
161
  const resp = await upstream(reqContext.body, {
158
162
  signal: options.signal,
159
163
  currentInputStart: reqContext.currentInputStart,
164
+ onUpstreamStart: options.onUpstreamStart,
160
165
  payloadPrepared,
161
166
  });
162
167
  payloadPrepared = true;
@@ -198,9 +203,11 @@ export function prepareResponsesRequest(reqBody, { mutate = false } = {}) {
198
203
  );
199
204
  const previousId = body.previous_response_id;
200
205
  let historyItems = [];
206
+ let historyRootId = null;
201
207
 
202
208
  if (previousId !== undefined && previousId !== null) {
203
209
  historyItems = materializeResponseHistory(previousId);
210
+ historyRootId = responseHistoryRootId(previousId);
204
211
  body.input = [...historyItems, ...currentInputItems];
205
212
  } else {
206
213
  body.input = currentInputItems;
@@ -232,11 +239,20 @@ export function prepareResponsesRequest(reqBody, { mutate = false } = {}) {
232
239
  inputItems: body.input,
233
240
  currentInputStart: retainedCurrentInputStart < 0 ? body.input.length : retainedCurrentInputStart,
234
241
  historyParentId: previousId ?? null,
242
+ historyRootId,
235
243
  historyInputItems,
236
244
  takeHistoryOwnership: mutate,
237
245
  };
238
246
  }
239
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
+
240
256
  export function rememberResponseHistory(reqContext, responseJson) {
241
257
  if (!responseJson?.id || !Array.isArray(reqContext?.historyInputItems || reqContext?.inputItems)) return;
242
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),