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