codex-copilot-dx 0.6.15 → 0.6.19

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.
@@ -71,6 +71,7 @@ function jsonByteLength(value, seen = new Set()) {
71
71
  const histories = new Map();
72
72
  const childrenById = new Map();
73
73
  const treeLru = new Map();
74
+ const pinnedTrees = new Map();
74
75
  const evictedIds = new Set();
75
76
  let totalBytes = 0;
76
77
  const HISTORY_RUNTIME_CONFIG = loadRuntimeConfig();
@@ -88,6 +89,21 @@ function touchTree(rootId) {
88
89
  treeLru.set(rootId, true);
89
90
  }
90
91
 
92
+ function pinTree(rootId) {
93
+ if (!rootId) return;
94
+ pinnedTrees.set(rootId, (pinnedTrees.get(rootId) || 0) + 1);
95
+ }
96
+
97
+ function unpinTree(rootId) {
98
+ const count = pinnedTrees.get(rootId) || 0;
99
+ if (count <= 1) pinnedTrees.delete(rootId);
100
+ else pinnedTrees.set(rootId, count - 1);
101
+ }
102
+
103
+ function treeIsPinned(rootId) {
104
+ return (pinnedTrees.get(rootId) || 0) > 0;
105
+ }
106
+
91
107
  function linkChild(parentId, id) {
92
108
  if (!parentId) return;
93
109
  let children = childrenById.get(parentId);
@@ -148,18 +164,50 @@ function assignSubtreeRoot(id, rootId) {
148
164
  }
149
165
  }
150
166
 
151
- function enforceLimits() {
152
- while (histories.size > maxEntries || totalBytes > maxBytes) {
153
- const oldestRootId = treeLru.keys().next().value;
154
- if (!oldestRootId) break;
155
- removeSubtree(oldestRootId);
167
+ function subtreeUsage(rootId) {
168
+ const pending = [rootId];
169
+ const seen = new Set();
170
+ let bytes = 0;
171
+ let entries = 0;
172
+ while (pending.length) {
173
+ const id = pending.pop();
174
+ if (seen.has(id)) continue;
175
+ seen.add(id);
176
+ const entry = histories.get(id);
177
+ if (entry) {
178
+ entries += 1;
179
+ bytes += entry.bytes;
180
+ }
181
+ const children = childrenById.get(id);
182
+ if (children) pending.push(...children);
183
+ }
184
+ return { bytes, entries };
185
+ }
186
+
187
+ function makeRoomFor({ additionalBytes, additionalEntries, protectedRoots }) {
188
+ let projectedBytes = totalBytes + additionalBytes;
189
+ let projectedEntries = histories.size + additionalEntries;
190
+ if (projectedEntries <= maxEntries && projectedBytes <= maxBytes) return true;
191
+
192
+ const candidates = [];
193
+ for (const rootId of treeLru.keys()) {
194
+ if (protectedRoots.has(rootId) || treeIsPinned(rootId)) continue;
195
+ const usage = subtreeUsage(rootId);
196
+ candidates.push(rootId);
197
+ projectedBytes -= usage.bytes;
198
+ projectedEntries -= usage.entries;
199
+ if (projectedEntries <= maxEntries && projectedBytes <= maxBytes) break;
156
200
  }
201
+ if (projectedEntries > maxEntries || projectedBytes > maxBytes) return false;
202
+ for (const rootId of candidates) removeSubtree(rootId);
203
+ return true;
157
204
  }
158
205
 
159
206
  export function clearResponseHistoryForTests() {
160
207
  histories.clear();
161
208
  childrenById.clear();
162
209
  treeLru.clear();
210
+ pinnedTrees.clear();
163
211
  evictedIds.clear();
164
212
  totalBytes = 0;
165
213
  maxBytes = DEFAULT_MAX_BYTES;
@@ -202,6 +250,50 @@ export function responseHistoryMaterializedBytes(responseId) {
202
250
  return chain.reduce((bytes, entry) => bytes + entry.bytes, 0);
203
251
  }
204
252
 
253
+ export function acquireResponseHistorySnapshot(responseId, { assertActive, signal } = {}) {
254
+ assertActive?.();
255
+ if (signal?.aborted) throw signal.reason || new DOMException("The operation was aborted", "AbortError");
256
+ const { chain, rootId } = responseHistoryChain(responseId);
257
+ const entries = chain.slice().reverse();
258
+ const bytes = chain.reduce((total, entry) => total + entry.bytes, 0);
259
+ pinTree(rootId);
260
+
261
+ let released = false;
262
+ const release = () => {
263
+ if (released) return;
264
+ released = true;
265
+ signal?.removeEventListener("abort", release);
266
+ unpinTree(rootId);
267
+ };
268
+ signal?.addEventListener("abort", release, { once: true });
269
+
270
+ return Object.freeze({
271
+ responseId,
272
+ rootId,
273
+ bytes,
274
+ materialize({ assertActive: assertSnapshotActive, routeMetadata } = {}) {
275
+ assertSnapshotActive?.();
276
+ if (released) throw new Error("Response history snapshot has been released");
277
+ const items = [];
278
+ for (let index = 0; index < entries.length; index += 1) {
279
+ if ((index & 63) === 0) assertSnapshotActive?.();
280
+ const entry = entries[index];
281
+ routeMetadata?.push({
282
+ affinity: entry.routeAffinity || null,
283
+ hasOpaque: entry.hasOpaque === true,
284
+ });
285
+ items.push(...entry.inputItems, ...entry.outputItems);
286
+ }
287
+ assertSnapshotActive?.();
288
+ const materialized = cloneJson(items);
289
+ assertSnapshotActive?.();
290
+ touchTree(rootId);
291
+ return materialized;
292
+ },
293
+ release,
294
+ });
295
+ }
296
+
205
297
  export function materializeResponseHistory(responseId, { assertActive, routeMetadata } = {}) {
206
298
  assertActive?.();
207
299
  const { chain, rootId } = responseHistoryChain(responseId);
@@ -231,7 +323,13 @@ export function rememberResponseHistoryNode({
231
323
  routeAffinity = null,
232
324
  takeOwnership = false,
233
325
  }) {
234
- if (!id || !Array.isArray(inputItems)) return;
326
+ if (!id || !Array.isArray(inputItems)) return false;
327
+ const existing = histories.get(id);
328
+ const parentEntry = parentId ? histories.get(parentId) : null;
329
+ if (parentId && !parentEntry) {
330
+ if (!existing) rememberEvictedId(id);
331
+ return false;
332
+ }
235
333
  clearResponsesToolOutputPartsCache(inputItems);
236
334
  clearResponsesToolOutputPartsCache(outputItems);
237
335
  const historyValue = routeAffinity
@@ -239,13 +337,11 @@ export function rememberResponseHistoryNode({
239
337
  : [inputItems, outputItems];
240
338
  const bytes = jsonByteLength(historyValue);
241
339
  if (bytes > maxBytes) {
242
- if (histories.has(id)) removeSubtree(id);
243
- rememberEvictedId(id);
244
- return;
340
+ if (!existing) rememberEvictedId(id);
341
+ else if (!treeIsPinned(existing.rootId)) removeSubtree(id);
342
+ return false;
245
343
  }
246
- const existing = histories.get(id);
247
- const parentEntry = parentId ? histories.get(parentId) : null;
248
- const rootId = parentEntry?.rootId || (parentId ? existing?.rootId : id) || id;
344
+ const rootId = parentEntry?.rootId || id;
249
345
  const entry = {
250
346
  parentId: parentId || null,
251
347
  rootId,
@@ -256,6 +352,19 @@ export function rememberResponseHistoryNode({
256
352
  bytes,
257
353
  };
258
354
  const oldRootId = existing?.rootId;
355
+ const protectedRoots = new Set([rootId]);
356
+ if (oldRootId) protectedRoots.add(oldRootId);
357
+ if (!makeRoomFor({
358
+ additionalBytes: bytes - (existing?.bytes || 0),
359
+ additionalEntries: existing ? 0 : 1,
360
+ protectedRoots,
361
+ })) {
362
+ if (!existing) {
363
+ if (parentEntry && !treeIsPinned(rootId)) removeSubtree(rootId);
364
+ rememberEvictedId(id);
365
+ }
366
+ return false;
367
+ }
259
368
  if (existing) {
260
369
  totalBytes -= existing.bytes;
261
370
  unlinkChild(existing.parentId, id);
@@ -270,5 +379,5 @@ export function rememberResponseHistoryNode({
270
379
  totalBytes += entry.bytes;
271
380
  evictedIds.delete(id);
272
381
  touchTree(rootId);
273
- enforceLimits();
382
+ return true;
274
383
  }
@@ -1,7 +1,12 @@
1
1
  import { randomUUID } from "node:crypto";
2
2
  import { chatCompletions } from "./copilot.mjs";
3
3
  import { webStreamLines } from "./stream.mjs";
4
- import { abortErrorStatusCode, isAbortLikeError } from "./http-transport.mjs";
4
+ import {
5
+ abortErrorStatusCode,
6
+ isAbortLikeError,
7
+ MAX_UPSTREAM_ERROR_BODY_BYTES,
8
+ readBoundedResponseText,
9
+ } from "./http-transport.mjs";
5
10
  import {
6
11
  incompleteUpstreamStream,
7
12
  invalidUpstreamStream,
@@ -216,7 +221,10 @@ export async function forwardToChat(chatReq, emitEvent, onDone, onError, options
216
221
  return false;
217
222
  }
218
223
  if (!resp.ok) {
219
- await onError(resp.status, await resp.text(), undefined, resp);
224
+ await onError(resp.status, await readBoundedResponseText(resp, {
225
+ maxBytes: MAX_UPSTREAM_ERROR_BODY_BYTES,
226
+ label: "Copilot Chat error body",
227
+ }), undefined, resp);
220
228
  return false;
221
229
  }
222
230
  try {
@@ -235,6 +243,13 @@ export async function forwardToChat(chatReq, emitEvent, onDone, onError, options
235
243
  let usage = { input_tokens: 0, output_tokens: 0, total_tokens: 0 };
236
244
  const toolCalls = new Map();
237
245
  const toolArgumentGuard = new ToolArgumentDeltaGuard();
246
+ const downstreamClosed = Symbol("downstream-closed");
247
+ const configuredEmitEvent = emitEvent;
248
+ emitEvent = async (...args) => {
249
+ const written = await configuredEmitEvent(...args);
250
+ if (written === false) throw downstreamClosed;
251
+ return written;
252
+ };
238
253
  let performanceOutputSeen = false;
239
254
  const markPerformanceOutput = () => {
240
255
  if (performanceOutputSeen) return;
@@ -242,8 +257,6 @@ export async function forwardToChat(chatReq, emitEvent, onDone, onError, options
242
257
  markFirstOutput();
243
258
  };
244
259
 
245
- await emitEvent("response.created", { response: { id: respId, object: "response", status: "in_progress", model: actualModel, output: [] } });
246
-
247
260
  const ensureMessageItem = async () => {
248
261
  if (messageItem) return messageItem;
249
262
  messageItem = { id: `msg_${uid()}`, outputIndex: nextOutputIndex++ };
@@ -331,6 +344,7 @@ export async function forwardToChat(chatReq, emitEvent, onDone, onError, options
331
344
  };
332
345
 
333
346
  try {
347
+ await emitEvent("response.created", { response: { id: respId, object: "response", status: "in_progress", model: actualModel, output: [] } });
334
348
  for await (const line of webStreamLines(resp, {
335
349
  onChunk: () => abort?.setTimeout(streamIdleTimeoutMs, "stream_idle_timeout"),
336
350
  })) {
@@ -386,6 +400,10 @@ export async function forwardToChat(chatReq, emitEvent, onDone, onError, options
386
400
  }
387
401
  }
388
402
  } catch (e) {
403
+ if (e === downstreamClosed) {
404
+ await resp.body?.cancel?.().catch?.(() => {});
405
+ return false;
406
+ }
389
407
  const statusCode = isAbortLikeError(e) ? abortErrorStatusCode(abort?.reason) : (e?.statusCode || 502);
390
408
  await onError(statusCode, e?.message || "upstream stream error", e);
391
409
  return false;
@@ -3,7 +3,10 @@ import { applyCopilotResponsesRequestPolicies } from "./copilot-responses-policy
3
3
  import {
4
4
  createRequestAbort,
5
5
  logRequestFailure,
6
+ MAX_UPSTREAM_CHAT_SUCCESS_BODY_BYTES,
7
+ MAX_UPSTREAM_ERROR_BODY_BYTES,
6
8
  readJsonBody,
9
+ readBoundedResponseText,
7
10
  sendJsonError,
8
11
  sendUpstreamError,
9
12
  writeOrDrain,
@@ -12,6 +15,7 @@ import {
12
15
  CODEX_AUTO_REVIEW_MODEL,
13
16
  modelIsResponsesOnly,
14
17
  modelSupportsChatCompletions,
18
+ resolveCopilotPriorityTierModel,
15
19
  resolveOpenAIModel,
16
20
  } from "./models.mjs";
17
21
  import { chatToResponses, forwardToChat, responsesBodyUsesCustomTools } from "./responses-bridge.mjs";
@@ -19,7 +23,7 @@ import { prepareResponsesChatPayload } from "./responses-chat-payload.mjs";
19
23
  import { prepareResponsesCompactionRequest } from "./responses-compaction.mjs";
20
24
  import { clearResponsesToolOutputPartsCache } from "./responses-content.mjs";
21
25
  import { proxyCopilotResponses } from "./responses-proxy.mjs";
22
- import { responseHistoryMaterializedBytes } from "./response-history.mjs";
26
+ import { acquireResponseHistorySnapshot } from "./response-history.mjs";
23
27
  import {
24
28
  applyResponseHistoryRoutePlan,
25
29
  dropMaterializedResponseHistory,
@@ -90,6 +94,7 @@ export function createResponsesHandler(options) {
90
94
  chatCompletionsFn,
91
95
  getCachedModelEndpointsFn = getCachedModelEndpoints,
92
96
  imagePressure,
97
+ modelRegistry,
93
98
  openAIModelEnv,
94
99
  requestBodyTimeoutMs,
95
100
  responsesPayloadOptions,
@@ -105,10 +110,15 @@ export function createResponsesHandler(options) {
105
110
  let releaseRequest = () => {};
106
111
  let prepared = null;
107
112
  let imagePressureResult = null;
113
+ let historySnapshot = null;
108
114
  let parsed = null;
109
115
  let upstreamPayloadReleased = false;
110
116
  let activeDeadline = Number.POSITIVE_INFINITY;
111
117
  let activeTimeoutReason = "responses_prepare_timeout";
118
+ const releaseHistorySnapshot = () => {
119
+ historySnapshot?.release();
120
+ historySnapshot = null;
121
+ };
112
122
  const assertPrepareActive = () => {
113
123
  if (abort.signal.aborted) throw abort.signal.reason;
114
124
  if (now() < activeDeadline) return;
@@ -127,7 +137,7 @@ export function createResponsesHandler(options) {
127
137
  if (!["responses_prepare_timeout", "stream_handshake_timeout", "upstream_timeout"].includes(abort.reason)) {
128
138
  return error;
129
139
  }
130
- const activated = imagePressure?.markTimeout?.(prepared?.historyRootId, {
140
+ const activated = imagePressure?.markTimeout?.(responseHistoryPressureRootId(prepared), {
131
141
  eligible: imagePressureResult?.pressureEligible === true,
132
142
  });
133
143
  if (!activated) return error;
@@ -152,6 +162,7 @@ export function createResponsesHandler(options) {
152
162
  const releaseUpstreamPayload = (finalContext) => {
153
163
  if (upstreamPayloadReleased) return;
154
164
  upstreamPayloadReleased = true;
165
+ if (finalContext?.historyParentId === null) releaseHistorySnapshot();
155
166
  clearResponsesToolOutputPartsCache(prepared?.body?.input);
156
167
  clearResponsesToolOutputPartsCache(prepared?.historyInputItems);
157
168
  if (finalContext && finalContext !== prepared) {
@@ -170,15 +181,19 @@ export function createResponsesHandler(options) {
170
181
  activeTimeoutReason = "responses_prepare_timeout";
171
182
  abort.setTimeout(upstreamTimeoutMs, "responses_prepare_timeout");
172
183
  if (parsed.previous_response_id !== undefined && parsed.previous_response_id !== null) {
173
- const historyBytes = responseHistoryMaterializedBytes(parsed.previous_response_id);
174
- if (historyBytes > 0) {
175
- await releaseRequest.reserveResponseHistory?.(historyBytes, { signal: abort.signal });
184
+ historySnapshot = acquireResponseHistorySnapshot(parsed.previous_response_id, {
185
+ assertActive: assertPrepareActive,
186
+ signal: abort.signal,
187
+ });
188
+ if (historySnapshot.bytes > 0) {
189
+ await releaseRequest.reserveResponseHistory?.(historySnapshot.bytes, { signal: abort.signal });
176
190
  }
177
191
  assertPrepareActive();
178
192
  }
179
193
  prepared = prepareResponsesRequest(parsed, {
180
194
  assertActive: assertPrepareActive,
181
195
  copilotBoundary: false,
196
+ historySnapshot,
182
197
  mutate: true,
183
198
  });
184
199
  parsed = null;
@@ -191,12 +206,22 @@ export function createResponsesHandler(options) {
191
206
  prepared.surface = "responses";
192
207
  const model = prepared.body.model || "unknown";
193
208
  const streaming = prepared.body.stream === true;
194
- const { requestedModel, upstreamModel } = resolveRequestModel(model, openAIModelEnv, autoReviewModelResolver);
209
+ const resolvedModel = resolveRequestModel(model, openAIModelEnv, autoReviewModelResolver);
210
+ const { requestedModel } = resolvedModel;
211
+ let { upstreamModel } = resolvedModel;
212
+ const priorityTierModel = requestedModel === upstreamModel
213
+ ? resolveCopilotPriorityTierModel(upstreamModel, prepared.body.service_tier, modelRegistry?.models)
214
+ : null;
215
+ if (priorityTierModel) {
216
+ upstreamModel = priorityTierModel;
217
+ delete prepared.body.service_tier;
218
+ }
195
219
  if (upstreamModel !== requestedModel) prepared.body.model = upstreamModel;
196
220
  const upstreamLog = upstreamModel === requestedModel ? "" : ` upstream_model=${upstreamModel}`;
197
221
  console.log(status("info", `responses model=${requestedModel}${upstreamLog} stream=${streaming}`));
198
222
  const usesCustomTools = responsesBodyUsesCustomTools(prepared.body);
199
- const useNativeResponses = requestedModel === CODEX_AUTO_REVIEW_MODEL
223
+ const useNativeResponses = Boolean(priorityTierModel)
224
+ || requestedModel === CODEX_AUTO_REVIEW_MODEL
200
225
  || isResponsesOnlyModel(upstreamModel, getCachedModelEndpointsFn)
201
226
  || (usesCustomTools && cachedModelSupportsResponses(upstreamModel, getCachedModelEndpointsFn));
202
227
  if (usesCustomTools && !useNativeResponses) throw unsupportedCustomToolsError(upstreamModel);
@@ -209,6 +234,7 @@ export function createResponsesHandler(options) {
209
234
  surface: "responses",
210
235
  });
211
236
  prepared = applyResponseHistoryRoutePlan(prepared, routePlan);
237
+ if (prepared.historyParentId === null) releaseHistorySnapshot();
212
238
  applyCopilotResponsesRequestPolicies(prepared.body);
213
239
  if (routePlan.protocol === "openai-responses") {
214
240
  const result = await proxyCopilotResponses(prepared, req, res, responsesFn, {
@@ -241,11 +267,13 @@ export function createResponsesHandler(options) {
241
267
  "Cache-Control": "no-cache",
242
268
  Connection: "keep-alive",
243
269
  });
244
- await writeOrDrain(res, `event: ${event}\ndata: ${JSON.stringify(data)}\n\n`);
270
+ const written = await writeOrDrain(res, `event: ${event}\ndata: ${JSON.stringify(data)}\n\n`);
271
+ if (!written) return false;
245
272
  if (event === "response.completed") {
246
273
  rememberResponseHistory(prepared, data.response);
247
274
  recordResponsesUsage({ surface: prepared.surface, mode: "stream", model, response: data.response, event: data });
248
275
  }
276
+ return true;
249
277
  }, () => { if (!res.writableEnded) res.end(); }, async (statusCode, errMsg, error, upstreamResponse) => {
250
278
  if (!res.headersSent) {
251
279
  if (upstreamResponse) {
@@ -282,7 +310,16 @@ export function createResponsesHandler(options) {
282
310
  onUpstreamStart: startUpstreamTimeout,
283
311
  });
284
312
  releaseChatPayload();
285
- const data = await upstream.text();
313
+ const data = upstream.ok
314
+ ? await readBoundedResponseText(upstream, {
315
+ maxBytes: MAX_UPSTREAM_CHAT_SUCCESS_BODY_BYTES,
316
+ label: "Copilot Chat success body",
317
+ signal: abort.signal,
318
+ })
319
+ : await readBoundedResponseText(upstream, {
320
+ maxBytes: MAX_UPSTREAM_ERROR_BODY_BYTES,
321
+ label: "Copilot Chat error body",
322
+ });
286
323
  if (!upstream.ok) {
287
324
  markAdaptiveHttpTimeout(upstream.status);
288
325
  sendUpstreamError(res, upstream, data);
@@ -311,6 +348,7 @@ export function createResponsesHandler(options) {
311
348
  sendJsonError(res, responseError, 502);
312
349
  } finally {
313
350
  releaseUpstreamPayload();
351
+ releaseHistorySnapshot();
314
352
  abort.cleanup();
315
353
  }
316
354
  };
@@ -321,6 +359,7 @@ export function createResponsesCompactHandler(options) {
321
359
  acquireRequest,
322
360
  autoReviewModelResolver,
323
361
  imagePressure,
362
+ modelRegistry,
324
363
  openAIModelEnv,
325
364
  requestBodyTimeoutMs,
326
365
  responsesCompactFn,
@@ -332,7 +371,47 @@ export function createResponsesCompactHandler(options) {
332
371
  const abort = createRequestAbort(req, res);
333
372
  let releaseRequest = () => {};
334
373
  let prepared = null;
374
+ let imagePressureResult = null;
375
+ let historySnapshot = null;
335
376
  let upstreamPayloadReleased = false;
377
+ let activeDeadline = Number.POSITIVE_INFINITY;
378
+ let activeTimeoutReason = "responses_prepare_timeout";
379
+ const releaseHistorySnapshot = () => {
380
+ historySnapshot?.release();
381
+ historySnapshot = null;
382
+ };
383
+ const assertPrepareActive = () => {
384
+ if (abort.signal.aborted) throw abort.signal.reason;
385
+ if (now() < activeDeadline) return;
386
+ abort.abort(activeTimeoutReason);
387
+ throw abort.signal.reason;
388
+ };
389
+ const startUpstreamTimeout = () => {
390
+ assertPrepareActive();
391
+ activeDeadline = now() + upstreamTimeoutMs;
392
+ activeTimeoutReason = "upstream_timeout";
393
+ abort.setTimeout(upstreamTimeoutMs, "upstream_timeout");
394
+ };
395
+ const pressureRootId = () => responseHistoryPressureRootId(prepared);
396
+ const markCompactTimeout = () => {
397
+ const eligible = imagePressure?.isEligible?.(prepared) === true;
398
+ return imagePressure?.markTimeout?.(pressureRootId(), { eligible }) || false;
399
+ };
400
+ const applyAdaptiveTimeout = (error) => {
401
+ if (!["responses_prepare_timeout", "upstream_timeout"].includes(abort.reason)) return error;
402
+ if (!markCompactTimeout()) return error;
403
+ const message = "Responses compact timed out while processing a large visual history. Retry the request; ccdx will keep fewer earlier images automatically. Restart is not required.";
404
+ error.statusCode = 504;
405
+ error.jsonBody = {
406
+ error: {
407
+ message,
408
+ type: "timeout_error",
409
+ code: "ccdx_visual_history_timeout",
410
+ retryable: true,
411
+ },
412
+ };
413
+ return error;
414
+ };
336
415
  const releaseUpstreamPayload = (finalContext) => {
337
416
  if (upstreamPayloadReleased) return;
338
417
  upstreamPayloadReleased = true;
@@ -348,39 +427,43 @@ export function createResponsesCompactHandler(options) {
348
427
  releaseRequest = await acquireRequest(req, { signal: abort.signal });
349
428
  abort.setTimeout(requestBodyTimeoutMs, "request_body_timeout");
350
429
  const parsed = await readJsonBody(req, { admission: releaseRequest, signal: abort.signal });
351
- let activeDeadline = now() + upstreamTimeoutMs;
352
- let activeTimeoutReason = "responses_prepare_timeout";
353
- const assertPrepareActive = () => {
354
- if (abort.signal.aborted) throw abort.signal.reason;
355
- if (now() < activeDeadline) return;
356
- abort.abort(activeTimeoutReason);
357
- throw abort.signal.reason;
358
- };
359
- const startUpstreamTimeout = () => {
360
- assertPrepareActive();
361
- activeDeadline = now() + upstreamTimeoutMs;
362
- activeTimeoutReason = "upstream_timeout";
363
- abort.setTimeout(upstreamTimeoutMs, "upstream_timeout");
364
- };
430
+ activeDeadline = now() + upstreamTimeoutMs;
431
+ activeTimeoutReason = "responses_prepare_timeout";
365
432
  abort.setTimeout(upstreamTimeoutMs, "responses_prepare_timeout");
366
433
  if (parsed.previous_response_id !== undefined && parsed.previous_response_id !== null) {
367
- const historyBytes = responseHistoryMaterializedBytes(parsed.previous_response_id);
368
- if (historyBytes > 0) {
369
- await releaseRequest.reserveResponseHistory?.(historyBytes, { signal: abort.signal });
434
+ historySnapshot = acquireResponseHistorySnapshot(parsed.previous_response_id, {
435
+ assertActive: assertPrepareActive,
436
+ signal: abort.signal,
437
+ });
438
+ if (historySnapshot.bytes > 0) {
439
+ await releaseRequest.reserveResponseHistory?.(historySnapshot.bytes, { signal: abort.signal });
370
440
  }
371
441
  assertPrepareActive();
372
442
  }
373
- prepared = prepareResponsesCompactionRequest(
374
- prepareResponsesRequest(parsed, {
375
- assertActive: assertPrepareActive,
376
- copilotBoundary: false,
377
- mutate: true,
378
- }),
379
- );
443
+ prepared = prepareResponsesRequest(parsed, {
444
+ assertActive: assertPrepareActive,
445
+ copilotBoundary: false,
446
+ historySnapshot,
447
+ mutate: true,
448
+ });
449
+ imagePressureResult = imagePressure?.applyRecovery?.(prepared, { assertActive: assertPrepareActive }) || null;
450
+ if (imagePressureResult?.adapted) {
451
+ console.warn(status("warn", `responses compact visual history mode=recovery historical_images=${imagePressureResult.initialHistoricalImages}->${imagePressureResult.historicalImages} omitted=${imagePressureResult.imagesOmitted} bytes=${imagePressureResult.initialBodyBytes}->${imagePressureResult.bodyBytes}`));
452
+ }
453
+ prepared = prepareResponsesCompactionRequest(prepared);
380
454
  assertPrepareActive();
381
455
  prepared.surface = "responses_compact";
382
456
  const model = parsed.model || "unknown";
383
- const { requestedModel, upstreamModel } = resolveRequestModel(model, openAIModelEnv, autoReviewModelResolver);
457
+ const resolvedModel = resolveRequestModel(model, openAIModelEnv, autoReviewModelResolver);
458
+ const { requestedModel } = resolvedModel;
459
+ let { upstreamModel } = resolvedModel;
460
+ const priorityTierModel = requestedModel === upstreamModel
461
+ ? resolveCopilotPriorityTierModel(upstreamModel, prepared.body.service_tier, modelRegistry?.models)
462
+ : null;
463
+ if (priorityTierModel) {
464
+ upstreamModel = priorityTierModel;
465
+ delete prepared.body.service_tier;
466
+ }
384
467
  if (upstreamModel !== requestedModel) prepared.body.model = upstreamModel;
385
468
  const upstreamLog = upstreamModel === requestedModel ? "" : ` upstream_model=${upstreamModel}`;
386
469
  console.log(status("info", `responses compact model=${requestedModel}${upstreamLog} stream=false`));
@@ -392,6 +475,7 @@ export function createResponsesCompactHandler(options) {
392
475
  model: upstreamModel,
393
476
  surface: "responses-compact",
394
477
  }));
478
+ releaseHistorySnapshot();
395
479
  applyCopilotResponsesRequestPolicies(prepared.body);
396
480
  const result = await proxyCopilotResponses(prepared, req, res, responsesCompactFn, {
397
481
  assertPrepareActive,
@@ -401,11 +485,14 @@ export function createResponsesCompactHandler(options) {
401
485
  releaseRequest: releaseUpstreamPayload,
402
486
  });
403
487
  if (result?.compacted) imagePressure?.clear?.(responseHistoryPressureRootId(prepared));
488
+ else if (result?.upstreamStatus === 408) markCompactTimeout();
404
489
  } catch (error) {
405
- logRequestFailure("Responses compact", error, abort);
406
- sendJsonError(res, error, 502);
490
+ const responseError = applyAdaptiveTimeout(error);
491
+ logRequestFailure("Responses compact", responseError, abort);
492
+ sendJsonError(res, responseError, 502);
407
493
  } finally {
408
494
  releaseUpstreamPayload();
495
+ releaseHistorySnapshot();
409
496
  abort.cleanup();
410
497
  }
411
498
  };
@@ -25,6 +25,20 @@ function bodyPayload(body, assertActive) {
25
25
  return { bodyText, bodyBytes: Buffer.byteLength(bodyText) };
26
26
  }
27
27
 
28
+ export function responsesImagePressureEligible(reqContext, {
29
+ assertActive,
30
+ policy: policyOverrides,
31
+ } = {}) {
32
+ const policy = normalizedPolicy(policyOverrides);
33
+ const currentInputStart = Number.isFinite(reqContext?.currentInputStart)
34
+ ? reqContext.currentInputStart
35
+ : 0;
36
+ const images = responsesHistoricalImageStats(reqContext?.body?.input, currentInputStart, { assertActive });
37
+ if (images.historicalImages === 0) return false;
38
+ if (images.historicalImages > policy.triggerHistoricalImages) return true;
39
+ return bodyPayload(reqContext?.body, assertActive).bodyBytes > policy.triggerBodyBytes;
40
+ }
41
+
28
42
  export function applyResponsesImagePressure(reqContext, {
29
43
  assertActive,
30
44
  mode = "normal",
@@ -143,6 +157,22 @@ export function createResponsesImagePressureController({
143
157
  }
144
158
  return result;
145
159
  },
160
+ isEligible(reqContext, { assertActive } = {}) {
161
+ return responsesImagePressureEligible(reqContext, { assertActive, policy });
162
+ },
163
+ applyRecovery(reqContext, { assertActive } = {}) {
164
+ if (modeFor(reqContext?.historyRootId) !== "recovery") return null;
165
+ const result = applyResponsesImagePressure(reqContext, {
166
+ assertActive,
167
+ mode: "recovery",
168
+ policy,
169
+ });
170
+ if (result.adapted) {
171
+ counters.adaptedRequests += 1;
172
+ counters.imagesOmitted += result.imagesOmitted;
173
+ }
174
+ return result;
175
+ },
146
176
  markTimeout(rootId, { eligible = false } = {}) {
147
177
  if (!rootId || !eligible) return false;
148
178
  prune();
@@ -2,6 +2,9 @@ import { responses as copilotResponses } from "./copilot.mjs";
2
2
  import {
3
3
  httpError,
4
4
  logRequestFailure,
5
+ MAX_UPSTREAM_ERROR_BODY_BYTES,
6
+ MAX_UPSTREAM_RESPONSES_SUCCESS_BODY_BYTES,
7
+ readBoundedResponseText,
5
8
  sendUpstreamError,
6
9
  writeOrDrain,
7
10
  } from "./http-transport.mjs";
@@ -262,7 +265,16 @@ export async function proxyCopilotResponses(reqContext, req, res, upstream = cop
262
265
  reader.releaseLock();
263
266
  }
264
267
  } else {
265
- const data = await resp.text();
268
+ const data = resp.ok
269
+ ? await readBoundedResponseText(resp, {
270
+ maxBytes: MAX_UPSTREAM_RESPONSES_SUCCESS_BODY_BYTES,
271
+ label: "Copilot Responses success body",
272
+ signal: options.signal,
273
+ })
274
+ : await readBoundedResponseText(resp, {
275
+ maxBytes: MAX_UPSTREAM_ERROR_BODY_BYTES,
276
+ label: "Copilot Responses error body",
277
+ });
266
278
  if (resp.ok && reqContext.surface === "responses_compact") {
267
279
  const response = parseResponsesCompactionResult(
268
280
  compactionInputWithoutTrigger(reqContext.body.input),
@@ -298,6 +298,7 @@ export function responseHistoryPressureRootId(reqContext) {
298
298
  export function prepareResponsesRequest(reqBody, {
299
299
  assertActive,
300
300
  copilotBoundary = true,
301
+ historySnapshot,
301
302
  mutate = false,
302
303
  } = {}) {
303
304
  assertActive?.();
@@ -313,11 +314,22 @@ export function prepareResponsesRequest(reqBody, {
313
314
  const historyRouteMetadata = [];
314
315
 
315
316
  if (previousId !== undefined && previousId !== null) {
316
- historyItems = materializeResponseHistory(previousId, {
317
- assertActive,
318
- routeMetadata: historyRouteMetadata,
319
- });
320
- historyRootId = responseHistoryRootId(previousId);
317
+ if (historySnapshot) {
318
+ if (historySnapshot.responseId !== previousId) {
319
+ throw new TypeError("Response history snapshot does not match previous_response_id");
320
+ }
321
+ historyItems = historySnapshot.materialize({
322
+ assertActive,
323
+ routeMetadata: historyRouteMetadata,
324
+ });
325
+ historyRootId = historySnapshot.rootId;
326
+ } else {
327
+ historyItems = materializeResponseHistory(previousId, {
328
+ assertActive,
329
+ routeMetadata: historyRouteMetadata,
330
+ });
331
+ historyRootId = responseHistoryRootId(previousId);
332
+ }
321
333
  body.input = [...historyItems, ...currentInputItems];
322
334
  } else {
323
335
  body.input = currentInputItems;