codex-copilot-dx 0.6.13 → 0.6.15

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.
@@ -1,4 +1,5 @@
1
1
  import { getCachedModelEndpoints } from "./copilot.mjs";
2
+ import { applyCopilotResponsesRequestPolicies } from "./copilot-responses-policy.mjs";
2
3
  import {
3
4
  createRequestAbort,
4
5
  logRequestFailure,
@@ -20,10 +21,13 @@ import { clearResponsesToolOutputPartsCache } from "./responses-content.mjs";
20
21
  import { proxyCopilotResponses } from "./responses-proxy.mjs";
21
22
  import { responseHistoryMaterializedBytes } from "./response-history.mjs";
22
23
  import {
24
+ applyResponseHistoryRoutePlan,
23
25
  dropMaterializedResponseHistory,
24
26
  prepareResponsesRequest,
25
27
  rememberResponseHistory,
28
+ responseHistoryPressureRootId,
26
29
  } from "./responses-request.mjs";
30
+ import { createRoutePlan } from "./route-plan.mjs";
27
31
  import { status } from "./status.mjs";
28
32
  import { endStreamWithError } from "./stream-errors.mjs";
29
33
  import { safeUpstreamResponseHeaders } from "./upstream-headers.mjs";
@@ -141,7 +145,7 @@ export function createResponsesHandler(options) {
141
145
  };
142
146
  const markAdaptiveHttpTimeout = (statusCode) => {
143
147
  if (statusCode !== 408) return false;
144
- return imagePressure?.markTimeout?.(prepared?.historyRootId, {
148
+ return imagePressure?.markTimeout?.(responseHistoryPressureRootId(prepared), {
145
149
  eligible: imagePressureResult?.pressureEligible === true,
146
150
  }) || false;
147
151
  };
@@ -172,7 +176,11 @@ export function createResponsesHandler(options) {
172
176
  }
173
177
  assertPrepareActive();
174
178
  }
175
- prepared = prepareResponsesRequest(parsed, { assertActive: assertPrepareActive, mutate: true });
179
+ prepared = prepareResponsesRequest(parsed, {
180
+ assertActive: assertPrepareActive,
181
+ copilotBoundary: false,
182
+ mutate: true,
183
+ });
176
184
  parsed = null;
177
185
  assertPrepareActive();
178
186
  imagePressureResult = imagePressure?.apply?.(prepared, { assertActive: assertPrepareActive }) || null;
@@ -192,7 +200,17 @@ export function createResponsesHandler(options) {
192
200
  || isResponsesOnlyModel(upstreamModel, getCachedModelEndpointsFn)
193
201
  || (usesCustomTools && cachedModelSupportsResponses(upstreamModel, getCachedModelEndpointsFn));
194
202
  if (usesCustomTools && !useNativeResponses) throw unsupportedCustomToolsError(upstreamModel);
195
- if (useNativeResponses) {
203
+ const routePlan = createRoutePlan({
204
+ disposition: "relay",
205
+ origin: "ccdx",
206
+ profile: "codex",
207
+ protocol: useNativeResponses ? "openai-responses" : "openai-chat-completions",
208
+ model: upstreamModel,
209
+ surface: "responses",
210
+ });
211
+ prepared = applyResponseHistoryRoutePlan(prepared, routePlan);
212
+ applyCopilotResponsesRequestPolicies(prepared.body);
213
+ if (routePlan.protocol === "openai-responses") {
196
214
  const result = await proxyCopilotResponses(prepared, req, res, responsesFn, {
197
215
  assertPrepareActive,
198
216
  signal: abort.signal,
@@ -201,9 +219,9 @@ export function createResponsesHandler(options) {
201
219
  releaseRequest: releaseUpstreamPayload,
202
220
  streamIdleTimeoutMs,
203
221
  });
204
- if (result?.successful) imagePressure?.markSuccess?.(prepared.historyRootId);
222
+ if (result?.successful) imagePressure?.markSuccess?.(responseHistoryPressureRootId(prepared));
205
223
  else markAdaptiveHttpTimeout(result?.upstreamStatus);
206
- } else {
224
+ } else if (routePlan.protocol === "openai-chat-completions") {
207
225
  let { chatReq, bodyText } = await prepareResponsesChatPayload(prepared, {
208
226
  assertActive: assertPrepareActive,
209
227
  payloadOptions: responsesPayloadOptions,
@@ -254,7 +272,7 @@ export function createResponsesHandler(options) {
254
272
  });
255
273
  },
256
274
  });
257
- if (successful) imagePressure?.markSuccess?.(prepared.historyRootId);
275
+ if (successful) imagePressure?.markSuccess?.(responseHistoryPressureRootId(prepared));
258
276
  } else {
259
277
  try {
260
278
  startUpstreamTimeout();
@@ -272,7 +290,7 @@ export function createResponsesHandler(options) {
272
290
  }
273
291
  const response = chatToResponses(JSON.parse(data), model);
274
292
  rememberResponseHistory(prepared, response);
275
- imagePressure?.markSuccess?.(prepared.historyRootId);
293
+ imagePressure?.markSuccess?.(responseHistoryPressureRootId(prepared));
276
294
  recordResponsesUsage({ surface: prepared.surface, mode: "json", model, response, event: response });
277
295
  res.writeHead(200, safeUpstreamResponseHeaders(upstream.headers, {
278
296
  contentType: "application/json",
@@ -284,6 +302,8 @@ export function createResponsesHandler(options) {
284
302
  sendJsonError(res, responseError, 502);
285
303
  }
286
304
  }
305
+ } else {
306
+ throw new Error(`Unsupported Responses target protocol: ${routePlan.protocol}`);
287
307
  }
288
308
  } catch (error) {
289
309
  const responseError = applyAdaptiveTimeout(error);
@@ -351,7 +371,11 @@ export function createResponsesCompactHandler(options) {
351
371
  assertPrepareActive();
352
372
  }
353
373
  prepared = prepareResponsesCompactionRequest(
354
- prepareResponsesRequest(parsed, { assertActive: assertPrepareActive, mutate: true }),
374
+ prepareResponsesRequest(parsed, {
375
+ assertActive: assertPrepareActive,
376
+ copilotBoundary: false,
377
+ mutate: true,
378
+ }),
355
379
  );
356
380
  assertPrepareActive();
357
381
  prepared.surface = "responses_compact";
@@ -360,6 +384,15 @@ export function createResponsesCompactHandler(options) {
360
384
  if (upstreamModel !== requestedModel) prepared.body.model = upstreamModel;
361
385
  const upstreamLog = upstreamModel === requestedModel ? "" : ` upstream_model=${upstreamModel}`;
362
386
  console.log(status("info", `responses compact model=${requestedModel}${upstreamLog} stream=false`));
387
+ prepared = applyResponseHistoryRoutePlan(prepared, createRoutePlan({
388
+ disposition: "relay",
389
+ origin: "ccdx",
390
+ profile: "codex",
391
+ protocol: "openai-responses",
392
+ model: upstreamModel,
393
+ surface: "responses-compact",
394
+ }));
395
+ applyCopilotResponsesRequestPolicies(prepared.body);
363
396
  const result = await proxyCopilotResponses(prepared, req, res, responsesCompactFn, {
364
397
  assertPrepareActive,
365
398
  signal: abort.signal,
@@ -367,7 +400,7 @@ export function createResponsesCompactHandler(options) {
367
400
  onUpstreamStart: startUpstreamTimeout,
368
401
  releaseRequest: releaseUpstreamPayload,
369
402
  });
370
- if (result?.compacted) imagePressure?.clear?.(prepared.historyRootId);
403
+ if (result?.compacted) imagePressure?.clear?.(responseHistoryPressureRootId(prepared));
371
404
  } catch (error) {
372
405
  logRequestFailure("Responses compact", error, abort);
373
406
  sendJsonError(res, error, 502);
@@ -5,7 +5,8 @@ import {
5
5
  sendUpstreamError,
6
6
  writeOrDrain,
7
7
  } from "./http-transport.mjs";
8
- import { openCopilotResponse, rememberResponseHistory } from "./responses-request.mjs";
8
+ import { openCopilotResponse } from "./copilot-responses-compat.mjs";
9
+ import { rememberResponseHistory } from "./responses-request.mjs";
9
10
  import {
10
11
  compactionInputWithoutTrigger,
11
12
  parseResponsesCompactionResult,
@@ -81,15 +82,38 @@ function inspectResponseSseEvent(state, eventName, data) {
81
82
  state.completed = { response, event };
82
83
  }
83
84
 
84
- function readSseEvents(buffer, onEvent) {
85
- while (true) {
86
- const match = buffer.match(/\r?\n\r?\n/);
87
- if (!match) return buffer;
88
- const chunk = buffer.slice(0, match.index);
89
- if (Buffer.byteLength(chunk) > MAX_SSE_BUFFER_BYTES) {
85
+ function createResponseSseInspector(state) {
86
+ let eventBuffer = Buffer.allocUnsafe(Math.min(4096, MAX_SSE_BUFFER_BYTES + 4));
87
+ let eventLength = 0;
88
+ let previous1 = -1;
89
+ let previous2 = -1;
90
+ let previous3 = -1;
91
+
92
+ const ensureCapacity = (required) => {
93
+ if (required > MAX_SSE_BUFFER_BYTES + 4) {
94
+ throw httpError(`Upstream SSE buffer exceeds ${MAX_SSE_BUFFER_BYTES} bytes`, 502);
95
+ }
96
+ if (required <= eventBuffer.length) return;
97
+ let capacity = eventBuffer.length;
98
+ while (capacity < required) capacity = Math.min(MAX_SSE_BUFFER_BYTES + 4, Math.max(capacity * 2, required));
99
+ const next = Buffer.allocUnsafe(capacity);
100
+ eventBuffer.copy(next, 0, 0, eventLength);
101
+ eventBuffer = next;
102
+ };
103
+
104
+ const append = (bytes) => {
105
+ if (!bytes.byteLength) return;
106
+ ensureCapacity(eventLength + bytes.byteLength);
107
+ eventBuffer.set(bytes, eventLength);
108
+ eventLength += bytes.byteLength;
109
+ };
110
+
111
+ const inspectEvent = (delimiterLength) => {
112
+ const contentLength = eventLength - delimiterLength;
113
+ if (contentLength > MAX_SSE_BUFFER_BYTES) {
90
114
  throw httpError(`Upstream SSE buffer exceeds ${MAX_SSE_BUFFER_BYTES} bytes`, 502);
91
115
  }
92
- buffer = buffer.slice(match.index + match[0].length);
116
+ const chunk = eventBuffer.subarray(0, contentLength).toString("utf8");
93
117
  const lines = chunk.split(/\r?\n/);
94
118
  const eventName = lines
95
119
  .find((line) => line.startsWith("event:"))
@@ -99,8 +123,73 @@ function readSseEvents(buffer, onEvent) {
99
123
  .filter((line) => line.startsWith("data:"))
100
124
  .map((line) => line.slice(5).trimStart())
101
125
  .join("\n");
102
- if (data) onEvent(eventName, data);
126
+ if (data) inspectResponseSseEvent(state, eventName, data);
127
+ eventLength = 0;
128
+ previous1 = -1;
129
+ previous2 = -1;
130
+ previous3 = -1;
131
+ };
132
+
133
+ return {
134
+ push(value) {
135
+ let segmentStart = 0;
136
+ for (let index = 0; index < value.byteLength; index += 1) {
137
+ const byte = value[index];
138
+ let delimiterLength = 0;
139
+ if (byte === 0x0a && previous1 === 0x0a) {
140
+ delimiterLength = previous2 === 0x0d ? 3 : 2;
141
+ } else if (byte === 0x0a && previous1 === 0x0d && previous2 === 0x0a) {
142
+ delimiterLength = previous3 === 0x0d ? 4 : 3;
143
+ }
144
+ if (delimiterLength) {
145
+ append(value.subarray(segmentStart, index + 1));
146
+ inspectEvent(delimiterLength);
147
+ segmentStart = index + 1;
148
+ if (state.sawTerminal) return index + 1;
149
+ continue;
150
+ }
151
+ previous3 = previous2;
152
+ previous2 = previous1;
153
+ previous1 = byte;
154
+ }
155
+ append(value.subarray(segmentStart));
156
+ if (eventLength > MAX_SSE_BUFFER_BYTES + 3) {
157
+ throw httpError(`Upstream SSE buffer exceeds ${MAX_SSE_BUFFER_BYTES} bytes`, 502);
158
+ }
159
+ return null;
160
+ },
161
+ };
162
+ }
163
+
164
+ function invalidResponsesResponse(message) {
165
+ const error = httpError(message, 502);
166
+ error.code = "ccdx_invalid_responses_response";
167
+ error.jsonBody = {
168
+ error: {
169
+ message,
170
+ type: "upstream_error",
171
+ code: error.code,
172
+ },
173
+ };
174
+ return error;
175
+ }
176
+
177
+ function parseSuccessfulResponsesResult(text) {
178
+ let response;
179
+ try {
180
+ response = JSON.parse(text);
181
+ } catch {
182
+ throw invalidResponsesResponse("Copilot Responses returned invalid JSON");
183
+ }
184
+ if (!response
185
+ || typeof response !== "object"
186
+ || Array.isArray(response)
187
+ || typeof response.id !== "string"
188
+ || response.id.length === 0
189
+ || !Array.isArray(response.output)) {
190
+ throw invalidResponsesResponse("Copilot Responses returned an invalid response envelope");
103
191
  }
192
+ return response;
104
193
  }
105
194
 
106
195
  function storeCompletedResponse(reqContext, completed) {
@@ -138,35 +227,26 @@ export async function proxyCopilotResponses(reqContext, req, res, upstream = cop
138
227
  Connection: "keep-alive",
139
228
  });
140
229
  const reader = resp.body.getReader();
141
- const decoder = new TextDecoder();
142
- let buffer = "";
143
230
  const streamState = {
144
231
  completed: null,
145
232
  sawTerminal: false,
146
233
  sawOutput: false,
147
234
  toolArgumentGuard: new ToolArgumentDeltaGuard(),
148
235
  };
236
+ const inspector = createResponseSseInspector(streamState);
149
237
  try {
150
238
  while (true) {
151
239
  const { done, value } = await reader.read();
152
240
  if (done) {
153
- buffer += decoder.decode();
154
- if (Buffer.byteLength(buffer) > MAX_SSE_BUFFER_BYTES) {
155
- throw httpError(`Upstream SSE buffer exceeds ${MAX_SSE_BUFFER_BYTES} bytes`, 502);
156
- }
157
- readSseEvents(buffer, (eventName, data) => inspectResponseSseEvent(streamState, eventName, data));
158
241
  if (!streamState.sawTerminal) throw incompleteUpstreamStream("a terminal Responses event");
159
242
  storeCompletedResponse(reqContext, streamState.completed);
160
243
  res.end();
161
244
  return { successful: Boolean(streamState.completed), compacted: false };
162
245
  }
163
246
  options.abort?.setTimeout(options.streamIdleTimeoutMs, "stream_idle_timeout");
164
- buffer += decoder.decode(value, { stream: true });
165
- buffer = readSseEvents(buffer, (eventName, data) => inspectResponseSseEvent(streamState, eventName, data));
166
- if (Buffer.byteLength(buffer) > MAX_SSE_BUFFER_BYTES) {
167
- throw httpError(`Upstream SSE buffer exceeds ${MAX_SSE_BUFFER_BYTES} bytes`, 502);
168
- }
169
- if (!await writeOrDrain(res, value)) return;
247
+ const terminalOffset = inspector.push(value);
248
+ const forwarded = terminalOffset === null ? value : value.subarray(0, terminalOffset);
249
+ if (forwarded.byteLength > 0 && !await writeOrDrain(res, forwarded)) return;
170
250
  if (streamState.sawTerminal) {
171
251
  storeCompletedResponse(reqContext, streamState.completed);
172
252
  res.end();
@@ -202,24 +282,26 @@ export async function proxyCopilotResponses(reqContext, req, res, upstream = cop
202
282
  res.end(JSON.stringify(response));
203
283
  return { successful: true, compacted: true };
204
284
  }
285
+ if (resp.ok) {
286
+ const response = parseSuccessfulResponsesResult(data);
287
+ rememberResponseHistory(reqContext, response);
288
+ recordResponsesUsage({
289
+ surface: reqContext.surface,
290
+ mode: "json",
291
+ model: reqContext.body?.model,
292
+ response,
293
+ event: response,
294
+ });
295
+ res.writeHead(resp.status, safeUpstreamResponseHeaders(resp.headers, {
296
+ contentType: "application/json",
297
+ }));
298
+ res.end(data);
299
+ return { successful: true, compacted: false };
300
+ }
205
301
  res.writeHead(resp.status, safeUpstreamResponseHeaders(resp.headers, {
206
302
  contentType: "application/json",
207
303
  }));
208
304
  res.end(data);
209
- if (resp.ok) {
210
- try {
211
- const response = JSON.parse(data);
212
- rememberResponseHistory(reqContext, response);
213
- recordResponsesUsage({
214
- surface: reqContext.surface,
215
- mode: "json",
216
- model: reqContext.body?.model,
217
- response,
218
- event: response,
219
- });
220
- return { successful: true, compacted: false };
221
- } catch {}
222
- }
223
305
  return { successful: false, compacted: false };
224
306
  }
225
307
  }
@@ -1,5 +1,4 @@
1
- import { responses as copilotResponses } from "./copilot.mjs";
2
- import { httpError } from "./http-transport.mjs";
1
+ import { applyCopilotResponsesRequestPolicies } from "./copilot-responses-policy.mjs";
3
2
  import { debugLog } from "./log.mjs";
4
3
  import {
5
4
  materializeResponseHistory,
@@ -8,7 +7,20 @@ import {
8
7
  } from "./response-history.mjs";
9
8
  import { isResponsesToolOutputItem, readResponsesToolOutputParts } from "./responses-content.mjs";
10
9
  import { enforceResponsesImageLimit } from "./responses-image-limit.mjs";
11
- import { status } from "./status.mjs";
10
+ import { routePlanAffinity, sameRoutePlanAffinity } from "./route-plan.mjs";
11
+
12
+ export {
13
+ isEncryptedContentVerificationError,
14
+ isImageNamespaceCollisionError,
15
+ sanitizeImageNamespaceCollisionRequest,
16
+ } from "./copilot-responses-policy.mjs";
17
+
18
+ // Compatibility export for consumers that imported this helper before the
19
+ // Copilot retry boundary moved into its provider-owned module.
20
+ export async function openCopilotResponse(...args) {
21
+ const compatibility = await import("./copilot-responses-compat.mjs");
22
+ return compatibility.openCopilotResponse(...args);
23
+ }
12
24
 
13
25
  function cloneJson(value) {
14
26
  return value === undefined ? undefined : structuredClone(value);
@@ -46,7 +58,7 @@ export function stripInternalResponsesInputFields(inputItems, { assertActive } =
46
58
 
47
59
  function responsesOutputItems(output) {
48
60
  if (!Array.isArray(output)) return [];
49
- return output.filter((item) => ["message", "function_call", "custom_tool_call"].includes(item?.type));
61
+ return output.filter((item) => item && typeof item === "object");
50
62
  }
51
63
 
52
64
  function isCompactedResponsesOutput(output) {
@@ -62,8 +74,7 @@ function isSuccessfulCompactionResponse(response) {
62
74
  }
63
75
 
64
76
  function replayableResponsesOutputItems(output) {
65
- if (!Array.isArray(output)) return [];
66
- return output.filter((item) => item && typeof item === "object");
77
+ return responsesOutputItems(output);
67
78
  }
68
79
 
69
80
  function isEncryptedContentPart(value) {
@@ -72,6 +83,8 @@ function isEncryptedContentPart(value) {
72
83
 
73
84
  const OMIT_ENCRYPTED_CONTENT = Symbol("omit-encrypted-content");
74
85
  const ENCRYPTED_HISTORY_REBASE = Symbol("encrypted-history-rebase");
86
+ const RELEASE_HISTORY_INPUT = Symbol("release-history-input");
87
+ const HISTORY_PRESSURE_ROOT = Symbol("history-pressure-root");
75
88
  const ENCRYPTED_TOOL_OUTPUT_MARKER = "[CCDX: encrypted tool output omitted because upstream could not decrypt it.]";
76
89
 
77
90
  function stripEncryptedReasoningValue(value, state) {
@@ -160,7 +173,21 @@ function isEncryptedReasoningInputItem(item) {
160
173
  return keys.length === 1 && keys[0] === "encrypted_content";
161
174
  }
162
175
 
163
- export function sanitizeEncryptedReasoningRequest(reqContext) {
176
+ function responseValueHasOpaqueState(value) {
177
+ if (Array.isArray(value)) return value.some(responseValueHasOpaqueState);
178
+ if (!value || typeof value !== "object") return false;
179
+ if (isEncryptedReasoningInputItem(value)
180
+ || Object.prototype.hasOwnProperty.call(value, "encrypted_content")) {
181
+ return true;
182
+ }
183
+ if (isResponsesToolOutputItem(value) && typeof value.output === "string") {
184
+ const parsed = readResponsesToolOutputParts(value);
185
+ if (parsed?.parts.some(responseValueHasOpaqueState)) return true;
186
+ }
187
+ return Object.values(value).some(responseValueHasOpaqueState);
188
+ }
189
+
190
+ export function sanitizeEncryptedReasoningRequest(reqContext, { historicalOnly = false } = {}) {
164
191
  const state = { changed: false };
165
192
  let body = cloneJson(reqContext.body);
166
193
  let currentInputStart = reqContext.currentInputStart;
@@ -171,6 +198,10 @@ export function sanitizeEncryptedReasoningRequest(reqContext) {
171
198
  for (let index = 0; index < body.input.length; index += 1) {
172
199
  const item = body.input[index];
173
200
  const historical = Number.isFinite(currentInputStart) && index < currentInputStart;
201
+ if (historicalOnly && !historical) {
202
+ input.push(item);
203
+ continue;
204
+ }
174
205
  if (isEncryptedReasoningInputItem(item)) {
175
206
  state.changed = true;
176
207
  if (historical) historicalInputChanged = true;
@@ -189,11 +220,13 @@ export function sanitizeEncryptedReasoningRequest(reqContext) {
189
220
  }
190
221
  body.input = input;
191
222
  if (Number.isFinite(currentInputStart)) currentInputStart = retainedBeforeCurrent;
192
- } else {
223
+ } else if (!historicalOnly) {
193
224
  body = stripEncryptedReasoningValue(body, state);
194
225
  }
195
226
  if (!state.changed) return null;
196
- const historyInputItems = Array.isArray(reqContext.historyInputItems)
227
+ const historyInputItems = historicalOnly
228
+ ? reqContext.historyInputItems
229
+ : Array.isArray(reqContext.historyInputItems)
197
230
  ? reqContext.historyInputItems.flatMap((item) => {
198
231
  if (isEncryptedReasoningInputItem(item)) return [];
199
232
  const stripped = stripEncryptedReasoningInputValue(item, { changed: false });
@@ -211,10 +244,13 @@ export function sanitizeEncryptedReasoningRequest(reqContext) {
211
244
  currentInputStart,
212
245
  inputItems: Array.isArray(body.input) ? body.input : reqContext.inputItems,
213
246
  historyInputItems,
247
+ [RELEASE_HISTORY_INPUT]: historicalOnly
248
+ ? reqContext[RELEASE_HISTORY_INPUT]
249
+ : historyInputItems,
214
250
  };
215
251
  }
216
252
 
217
- function finalizeEncryptedHistoryRebase(reqContext) {
253
+ export function finalizeEncryptedHistoryRebase(reqContext) {
218
254
  if (!reqContext[ENCRYPTED_HISTORY_REBASE]) return reqContext;
219
255
  const finalized = {
220
256
  ...reqContext,
@@ -227,98 +263,43 @@ function finalizeEncryptedHistoryRebase(reqContext) {
227
263
  return finalized;
228
264
  }
229
265
 
230
- export function isEncryptedContentVerificationError(statusCode, text) {
231
- if (statusCode < 400 || !text) return false;
232
- const lower = String(text).toLowerCase();
233
- const reasoningFailure = lower.includes("encrypted content")
234
- && lower.includes("could not be verified")
235
- && (lower.includes("could not be decrypted") || lower.includes("could not be parsed"));
236
- const functionOutputFailure = lower.includes("encrypted function output content")
237
- && lower.includes("could not be decrypted or decoded");
238
- const missingEncryptedContent = statusCode < 500
239
- && /missing required parameter:\s*(['"]?)input\[\d+\](?:\.[a-z0-9_]+|\[\d+\])*\.encrypted_content\1(?=\.?(?:\s|$|["},\]]))/.test(lower);
240
- return reasoningFailure || functionOutputFailure || missingEncryptedContent;
241
- }
242
-
243
- export function isImageNamespaceCollisionError(statusCode, text) {
244
- if (statusCode < 400 || !text) return false;
245
- const lower = String(text).toLowerCase();
246
- return lower.includes("namespace")
247
- && lower.includes("image_gen")
248
- && lower.includes("collid");
249
- }
250
-
251
- function isImageNamespaceTool(tool, { collisionFallback = false } = {}) {
252
- if (!tool || typeof tool !== "object") return false;
253
- const type = String(tool.type || "").toLowerCase();
254
- const name = String(tool.name || tool.function?.name || "").toLowerCase();
255
- const namespace = String(tool.namespace || "").toLowerCase();
256
- if (["image_gen", "image_generation"].includes(type)) return true;
257
- if (["image_gen", "image_generation"].includes(name)) return true;
258
- if (namespace === "image_gen" || namespace === "image_generation") return true;
259
- return collisionFallback && [type, name, namespace].some((value) => value.startsWith("image_gen"));
260
- }
261
-
262
- export function sanitizeImageNamespaceCollisionRequest(reqContext) {
263
- if (!Array.isArray(reqContext?.body?.tools)) return null;
264
- const body = cloneJson(reqContext.body);
265
- const filtered = body.tools.filter((tool) => !isImageNamespaceTool(tool, { collisionFallback: true }));
266
- if (filtered.length === body.tools.length) return null;
267
- if (filtered.length) body.tools = filtered;
268
- else delete body.tools;
269
- return { ...reqContext, body };
270
- }
271
-
272
- export async function openCopilotResponse(reqContext, upstream = copilotResponses, options = {}) {
273
- let encryptedRetried = false;
274
- let imageNamespaceRetried = false;
275
- let payloadPrepared = false;
276
- for (let attempt = 0; attempt < 3; attempt += 1) {
277
- options.assertPrepareActive?.();
278
- const resp = await upstream(reqContext.body, {
279
- assertActive: options.assertPrepareActive,
280
- signal: options.signal,
281
- currentInputStart: reqContext.currentInputStart,
282
- onUpstreamStart: options.onUpstreamStart,
283
- payloadPrepared,
284
- });
285
- options.assertPrepareActive?.();
286
- payloadPrepared = true;
287
- if (resp.ok) {
288
- reqContext = finalizeEncryptedHistoryRebase(reqContext);
289
- return { resp, reqContext };
290
- }
291
-
292
- const errorText = await resp.text();
293
- options.assertPrepareActive?.();
294
- if (!imageNamespaceRetried && isImageNamespaceCollisionError(resp.status, errorText)) {
295
- const retryContext = sanitizeImageNamespaceCollisionRequest(reqContext);
296
- if (retryContext) {
297
- imageNamespaceRetried = true;
298
- reqContext = retryContext;
299
- console.warn(status("warn", "image_gen namespace rejected by upstream; retrying without the conflicting image tool"));
300
- continue;
301
- }
302
- }
303
- if (!encryptedRetried && isEncryptedContentVerificationError(resp.status, errorText)) {
304
- const retryContext = sanitizeEncryptedReasoningRequest(reqContext);
305
- if (retryContext) {
306
- encryptedRetried = true;
307
- reqContext = retryContext;
308
- console.warn(status("warn", "encrypted replay content rejected by upstream; retrying without unavailable encrypted content"));
309
- continue;
310
- }
311
- }
312
- return { resp, reqContext, errorText };
266
+ export function applyResponseHistoryRoutePlan(reqContext, routePlan) {
267
+ const next = reqContext;
268
+ next.routePlan = routePlan;
269
+ if (!next.historyParentId) return next;
270
+ const currentAffinity = routePlanAffinity(routePlan);
271
+ const historyMetadata = next.historyRouteMetadata;
272
+ if (!currentAffinity
273
+ || !Array.isArray(historyMetadata)
274
+ || historyMetadata.length === 0
275
+ || historyMetadata.some((metadata) => metadata.affinity === null)) {
276
+ return next;
313
277
  }
314
- throw httpError("Responses compatibility retry limit exceeded", 502);
278
+ const hasMismatchedOpaqueState = historyMetadata.some((metadata) => (
279
+ metadata.hasOpaque && !sameRoutePlanAffinity(metadata.affinity, currentAffinity)
280
+ ));
281
+ if (!hasMismatchedOpaqueState) {
282
+ return next;
283
+ }
284
+
285
+ const sanitized = sanitizeEncryptedReasoningRequest(next, { historicalOnly: true });
286
+ if (!sanitized) return next;
287
+ sanitized.routePlan = routePlan;
288
+ const historyRootId = next.historyRootId;
289
+ const rebased = finalizeEncryptedHistoryRebase(sanitized);
290
+ if (historyRootId) rebased[HISTORY_PRESSURE_ROOT] = historyRootId;
291
+ return rebased;
315
292
  }
316
293
 
317
- function isBuiltinImageTool(tool) {
318
- return isImageNamespaceTool(tool);
294
+ export function responseHistoryPressureRootId(reqContext) {
295
+ return reqContext?.[HISTORY_PRESSURE_ROOT] || reqContext?.historyRootId || null;
319
296
  }
320
297
 
321
- export function prepareResponsesRequest(reqBody, { assertActive, mutate = false } = {}) {
298
+ export function prepareResponsesRequest(reqBody, {
299
+ assertActive,
300
+ copilotBoundary = true,
301
+ mutate = false,
302
+ } = {}) {
322
303
  assertActive?.();
323
304
  const body = mutate ? reqBody : cloneJson(reqBody);
324
305
  assertActive?.();
@@ -329,9 +310,13 @@ export function prepareResponsesRequest(reqBody, { assertActive, mutate = false
329
310
  const previousId = body.previous_response_id;
330
311
  let historyItems = [];
331
312
  let historyRootId = null;
313
+ const historyRouteMetadata = [];
332
314
 
333
315
  if (previousId !== undefined && previousId !== null) {
334
- historyItems = materializeResponseHistory(previousId, { assertActive });
316
+ historyItems = materializeResponseHistory(previousId, {
317
+ assertActive,
318
+ routeMetadata: historyRouteMetadata,
319
+ });
335
320
  historyRootId = responseHistoryRootId(previousId);
336
321
  body.input = [...historyItems, ...currentInputItems];
337
322
  } else {
@@ -352,10 +337,10 @@ export function prepareResponsesRequest(reqBody, { assertActive, mutate = false
352
337
 
353
338
  delete body.previous_response_id;
354
339
  delete body.store;
355
- if (Array.isArray(body.tools)) {
356
- body.tools = body.tools.filter((tool) => !isBuiltinImageTool(tool));
357
- if (!body.tools.length) delete body.tools;
358
- }
340
+ // Preserve the historical helper contract for embedders. Runtime handlers
341
+ // pass copilotBoundary:false and apply the policy only after selecting a
342
+ // concrete Copilot RoutePlan.
343
+ if (copilotBoundary) applyCopilotResponsesRequestPolicies(body);
359
344
  stripInternalResponsesInputFields(body.input, { assertActive });
360
345
  stripInternalResponsesInputFields(historyInputItems, { assertActive });
361
346
  assertActive?.();
@@ -367,15 +352,21 @@ export function prepareResponsesRequest(reqBody, { assertActive, mutate = false
367
352
  currentInputStart: retainedCurrentInputStart < 0 ? body.input.length : retainedCurrentInputStart,
368
353
  historyParentId: previousId ?? null,
369
354
  historyRootId,
355
+ historyRouteMetadata,
370
356
  historyInputItems,
357
+ ...(previousId !== undefined && previousId !== null
358
+ ? { [RELEASE_HISTORY_INPUT]: historyInputItems }
359
+ : {}),
371
360
  takeHistoryOwnership: mutate,
372
361
  };
373
362
  }
374
363
 
375
364
  export function dropMaterializedResponseHistory(reqContext) {
376
- if (!reqContext?.historyParentId || !Array.isArray(reqContext.historyInputItems)) return false;
377
- reqContext.body = { ...reqContext.body, input: reqContext.historyInputItems };
378
- reqContext.inputItems = reqContext.historyInputItems;
365
+ const releaseInput = reqContext?.[RELEASE_HISTORY_INPUT];
366
+ if (!Array.isArray(releaseInput)) return false;
367
+ reqContext.body = { ...reqContext.body, input: releaseInput };
368
+ reqContext.inputItems = releaseInput;
369
+ delete reqContext[RELEASE_HISTORY_INPUT];
379
370
  reqContext.currentInputStart = 0;
380
371
  return true;
381
372
  }
@@ -392,6 +383,8 @@ export function rememberResponseHistory(reqContext, responseJson) {
392
383
  parentId: compacted ? null : reqContext.historyParentId,
393
384
  inputItems: sourceInputItems,
394
385
  outputItems: sourceOutputItems,
386
+ hasOpaque: responseValueHasOpaqueState([sourceInputItems, sourceOutputItems]),
387
+ routeAffinity: routePlanAffinity(reqContext.routePlan),
395
388
  takeOwnership: reqContext.takeHistoryOwnership,
396
389
  });
397
390
  }