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.
- package/README.md +9 -6
- package/package.json +1 -1
- package/scripts/benchmark.mjs +74 -0
- package/src/adapter.mjs +6 -2
- package/src/anthropic-handler.mjs +38 -2
- package/src/anthropic.mjs +77 -24
- package/src/config.mjs +38 -3
- package/src/copilot-responses-compat.mjs +73 -0
- package/src/copilot-responses-policy.mjs +78 -0
- package/src/doctor.mjs +1 -2
- package/src/model-cache.mjs +36 -6
- package/src/observability.mjs +13 -1
- package/src/pm-studio-relay.mjs +6 -3
- package/src/profile-model-runtime.mjs +87 -13
- package/src/profile-routing.mjs +45 -7
- package/src/response-history.mjs +20 -3
- package/src/responses-bridge.mjs +44 -7
- package/src/responses-compaction.mjs +1 -50
- package/src/responses-handler.mjs +42 -9
- package/src/responses-proxy.mjs +118 -36
- package/src/responses-request.mjs +98 -105
- package/src/route-plan.mjs +51 -0
- package/src/startup.mjs +8 -2
- package/src/stream.mjs +27 -13
|
@@ -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
|
|
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, {
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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, {
|
|
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
|
|
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);
|
package/src/responses-proxy.mjs
CHANGED
|
@@ -5,7 +5,8 @@ import {
|
|
|
5
5
|
sendUpstreamError,
|
|
6
6
|
writeOrDrain,
|
|
7
7
|
} from "./http-transport.mjs";
|
|
8
|
-
import { openCopilotResponse
|
|
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
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
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
|
-
|
|
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)
|
|
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
|
-
|
|
165
|
-
|
|
166
|
-
if (
|
|
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 {
|
|
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 {
|
|
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) =>
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
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
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
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
|
-
|
|
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
|
|
318
|
-
return
|
|
294
|
+
export function responseHistoryPressureRootId(reqContext) {
|
|
295
|
+
return reqContext?.[HISTORY_PRESSURE_ROOT] || reqContext?.historyRootId || null;
|
|
319
296
|
}
|
|
320
297
|
|
|
321
|
-
export function prepareResponsesRequest(reqBody, {
|
|
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, {
|
|
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
|
-
|
|
356
|
-
|
|
357
|
-
|
|
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
|
-
|
|
377
|
-
|
|
378
|
-
reqContext.
|
|
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
|
}
|