codex-copilot-dx 0.6.15 → 0.6.18
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 +26 -15
- package/bin/cli.mjs +5 -0
- package/package.json +1 -1
- package/src/adapter.mjs +28 -7
- package/src/anthropic-handler.mjs +17 -3
- package/src/anthropic.mjs +9 -4
- package/src/atomic-file.mjs +76 -2
- package/src/auth-profile.mjs +11 -34
- package/src/auth.mjs +265 -52
- package/src/cli-auth.mjs +28 -71
- package/src/cli-options.mjs +8 -5
- package/src/copilot-client.mjs +5 -1
- package/src/copilot-responses-compat.mjs +9 -2
- package/src/copilot.mjs +2 -1
- package/src/http-transport.mjs +91 -0
- package/src/lock.mjs +179 -17
- package/src/models.mjs +17 -0
- package/src/pm-studio-asar.mjs +399 -19
- package/src/pm-studio-setup.mjs +319 -14
- package/src/pm-studio-status.mjs +5 -1
- package/src/response-history.mjs +122 -13
- package/src/responses-bridge.mjs +22 -4
- package/src/responses-handler.mjs +117 -35
- package/src/responses-image-pressure.mjs +30 -0
- package/src/responses-proxy.mjs +8 -1
- package/src/responses-request.mjs +17 -5
- package/src/usage.mjs +25 -7
package/src/responses-bridge.mjs
CHANGED
|
@@ -1,7 +1,12 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
2
|
import { chatCompletions } from "./copilot.mjs";
|
|
3
3
|
import { webStreamLines } from "./stream.mjs";
|
|
4
|
-
import {
|
|
4
|
+
import {
|
|
5
|
+
abortErrorStatusCode,
|
|
6
|
+
isAbortLikeError,
|
|
7
|
+
MAX_UPSTREAM_ERROR_BODY_BYTES,
|
|
8
|
+
readBoundedResponseText,
|
|
9
|
+
} from "./http-transport.mjs";
|
|
5
10
|
import {
|
|
6
11
|
incompleteUpstreamStream,
|
|
7
12
|
invalidUpstreamStream,
|
|
@@ -216,7 +221,10 @@ export async function forwardToChat(chatReq, emitEvent, onDone, onError, options
|
|
|
216
221
|
return false;
|
|
217
222
|
}
|
|
218
223
|
if (!resp.ok) {
|
|
219
|
-
await onError(resp.status, await resp
|
|
224
|
+
await onError(resp.status, await readBoundedResponseText(resp, {
|
|
225
|
+
maxBytes: MAX_UPSTREAM_ERROR_BODY_BYTES,
|
|
226
|
+
label: "Copilot Chat error body",
|
|
227
|
+
}), undefined, resp);
|
|
220
228
|
return false;
|
|
221
229
|
}
|
|
222
230
|
try {
|
|
@@ -235,6 +243,13 @@ export async function forwardToChat(chatReq, emitEvent, onDone, onError, options
|
|
|
235
243
|
let usage = { input_tokens: 0, output_tokens: 0, total_tokens: 0 };
|
|
236
244
|
const toolCalls = new Map();
|
|
237
245
|
const toolArgumentGuard = new ToolArgumentDeltaGuard();
|
|
246
|
+
const downstreamClosed = Symbol("downstream-closed");
|
|
247
|
+
const configuredEmitEvent = emitEvent;
|
|
248
|
+
emitEvent = async (...args) => {
|
|
249
|
+
const written = await configuredEmitEvent(...args);
|
|
250
|
+
if (written === false) throw downstreamClosed;
|
|
251
|
+
return written;
|
|
252
|
+
};
|
|
238
253
|
let performanceOutputSeen = false;
|
|
239
254
|
const markPerformanceOutput = () => {
|
|
240
255
|
if (performanceOutputSeen) return;
|
|
@@ -242,8 +257,6 @@ export async function forwardToChat(chatReq, emitEvent, onDone, onError, options
|
|
|
242
257
|
markFirstOutput();
|
|
243
258
|
};
|
|
244
259
|
|
|
245
|
-
await emitEvent("response.created", { response: { id: respId, object: "response", status: "in_progress", model: actualModel, output: [] } });
|
|
246
|
-
|
|
247
260
|
const ensureMessageItem = async () => {
|
|
248
261
|
if (messageItem) return messageItem;
|
|
249
262
|
messageItem = { id: `msg_${uid()}`, outputIndex: nextOutputIndex++ };
|
|
@@ -331,6 +344,7 @@ export async function forwardToChat(chatReq, emitEvent, onDone, onError, options
|
|
|
331
344
|
};
|
|
332
345
|
|
|
333
346
|
try {
|
|
347
|
+
await emitEvent("response.created", { response: { id: respId, object: "response", status: "in_progress", model: actualModel, output: [] } });
|
|
334
348
|
for await (const line of webStreamLines(resp, {
|
|
335
349
|
onChunk: () => abort?.setTimeout(streamIdleTimeoutMs, "stream_idle_timeout"),
|
|
336
350
|
})) {
|
|
@@ -386,6 +400,10 @@ export async function forwardToChat(chatReq, emitEvent, onDone, onError, options
|
|
|
386
400
|
}
|
|
387
401
|
}
|
|
388
402
|
} catch (e) {
|
|
403
|
+
if (e === downstreamClosed) {
|
|
404
|
+
await resp.body?.cancel?.().catch?.(() => {});
|
|
405
|
+
return false;
|
|
406
|
+
}
|
|
389
407
|
const statusCode = isAbortLikeError(e) ? abortErrorStatusCode(abort?.reason) : (e?.statusCode || 502);
|
|
390
408
|
await onError(statusCode, e?.message || "upstream stream error", e);
|
|
391
409
|
return false;
|
|
@@ -3,7 +3,9 @@ import { applyCopilotResponsesRequestPolicies } from "./copilot-responses-policy
|
|
|
3
3
|
import {
|
|
4
4
|
createRequestAbort,
|
|
5
5
|
logRequestFailure,
|
|
6
|
+
MAX_UPSTREAM_ERROR_BODY_BYTES,
|
|
6
7
|
readJsonBody,
|
|
8
|
+
readBoundedResponseText,
|
|
7
9
|
sendJsonError,
|
|
8
10
|
sendUpstreamError,
|
|
9
11
|
writeOrDrain,
|
|
@@ -12,6 +14,7 @@ import {
|
|
|
12
14
|
CODEX_AUTO_REVIEW_MODEL,
|
|
13
15
|
modelIsResponsesOnly,
|
|
14
16
|
modelSupportsChatCompletions,
|
|
17
|
+
resolveCopilotPriorityTierModel,
|
|
15
18
|
resolveOpenAIModel,
|
|
16
19
|
} from "./models.mjs";
|
|
17
20
|
import { chatToResponses, forwardToChat, responsesBodyUsesCustomTools } from "./responses-bridge.mjs";
|
|
@@ -19,7 +22,7 @@ import { prepareResponsesChatPayload } from "./responses-chat-payload.mjs";
|
|
|
19
22
|
import { prepareResponsesCompactionRequest } from "./responses-compaction.mjs";
|
|
20
23
|
import { clearResponsesToolOutputPartsCache } from "./responses-content.mjs";
|
|
21
24
|
import { proxyCopilotResponses } from "./responses-proxy.mjs";
|
|
22
|
-
import {
|
|
25
|
+
import { acquireResponseHistorySnapshot } from "./response-history.mjs";
|
|
23
26
|
import {
|
|
24
27
|
applyResponseHistoryRoutePlan,
|
|
25
28
|
dropMaterializedResponseHistory,
|
|
@@ -90,6 +93,7 @@ export function createResponsesHandler(options) {
|
|
|
90
93
|
chatCompletionsFn,
|
|
91
94
|
getCachedModelEndpointsFn = getCachedModelEndpoints,
|
|
92
95
|
imagePressure,
|
|
96
|
+
modelRegistry,
|
|
93
97
|
openAIModelEnv,
|
|
94
98
|
requestBodyTimeoutMs,
|
|
95
99
|
responsesPayloadOptions,
|
|
@@ -105,10 +109,15 @@ export function createResponsesHandler(options) {
|
|
|
105
109
|
let releaseRequest = () => {};
|
|
106
110
|
let prepared = null;
|
|
107
111
|
let imagePressureResult = null;
|
|
112
|
+
let historySnapshot = null;
|
|
108
113
|
let parsed = null;
|
|
109
114
|
let upstreamPayloadReleased = false;
|
|
110
115
|
let activeDeadline = Number.POSITIVE_INFINITY;
|
|
111
116
|
let activeTimeoutReason = "responses_prepare_timeout";
|
|
117
|
+
const releaseHistorySnapshot = () => {
|
|
118
|
+
historySnapshot?.release();
|
|
119
|
+
historySnapshot = null;
|
|
120
|
+
};
|
|
112
121
|
const assertPrepareActive = () => {
|
|
113
122
|
if (abort.signal.aborted) throw abort.signal.reason;
|
|
114
123
|
if (now() < activeDeadline) return;
|
|
@@ -152,6 +161,7 @@ export function createResponsesHandler(options) {
|
|
|
152
161
|
const releaseUpstreamPayload = (finalContext) => {
|
|
153
162
|
if (upstreamPayloadReleased) return;
|
|
154
163
|
upstreamPayloadReleased = true;
|
|
164
|
+
if (finalContext?.historyParentId === null) releaseHistorySnapshot();
|
|
155
165
|
clearResponsesToolOutputPartsCache(prepared?.body?.input);
|
|
156
166
|
clearResponsesToolOutputPartsCache(prepared?.historyInputItems);
|
|
157
167
|
if (finalContext && finalContext !== prepared) {
|
|
@@ -170,15 +180,19 @@ export function createResponsesHandler(options) {
|
|
|
170
180
|
activeTimeoutReason = "responses_prepare_timeout";
|
|
171
181
|
abort.setTimeout(upstreamTimeoutMs, "responses_prepare_timeout");
|
|
172
182
|
if (parsed.previous_response_id !== undefined && parsed.previous_response_id !== null) {
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
183
|
+
historySnapshot = acquireResponseHistorySnapshot(parsed.previous_response_id, {
|
|
184
|
+
assertActive: assertPrepareActive,
|
|
185
|
+
signal: abort.signal,
|
|
186
|
+
});
|
|
187
|
+
if (historySnapshot.bytes > 0) {
|
|
188
|
+
await releaseRequest.reserveResponseHistory?.(historySnapshot.bytes, { signal: abort.signal });
|
|
176
189
|
}
|
|
177
190
|
assertPrepareActive();
|
|
178
191
|
}
|
|
179
192
|
prepared = prepareResponsesRequest(parsed, {
|
|
180
193
|
assertActive: assertPrepareActive,
|
|
181
194
|
copilotBoundary: false,
|
|
195
|
+
historySnapshot,
|
|
182
196
|
mutate: true,
|
|
183
197
|
});
|
|
184
198
|
parsed = null;
|
|
@@ -191,12 +205,22 @@ export function createResponsesHandler(options) {
|
|
|
191
205
|
prepared.surface = "responses";
|
|
192
206
|
const model = prepared.body.model || "unknown";
|
|
193
207
|
const streaming = prepared.body.stream === true;
|
|
194
|
-
const
|
|
208
|
+
const resolvedModel = resolveRequestModel(model, openAIModelEnv, autoReviewModelResolver);
|
|
209
|
+
const { requestedModel } = resolvedModel;
|
|
210
|
+
let { upstreamModel } = resolvedModel;
|
|
211
|
+
const priorityTierModel = requestedModel === upstreamModel
|
|
212
|
+
? resolveCopilotPriorityTierModel(upstreamModel, prepared.body.service_tier, modelRegistry?.models)
|
|
213
|
+
: null;
|
|
214
|
+
if (priorityTierModel) {
|
|
215
|
+
upstreamModel = priorityTierModel;
|
|
216
|
+
delete prepared.body.service_tier;
|
|
217
|
+
}
|
|
195
218
|
if (upstreamModel !== requestedModel) prepared.body.model = upstreamModel;
|
|
196
219
|
const upstreamLog = upstreamModel === requestedModel ? "" : ` upstream_model=${upstreamModel}`;
|
|
197
220
|
console.log(status("info", `responses model=${requestedModel}${upstreamLog} stream=${streaming}`));
|
|
198
221
|
const usesCustomTools = responsesBodyUsesCustomTools(prepared.body);
|
|
199
|
-
const useNativeResponses =
|
|
222
|
+
const useNativeResponses = Boolean(priorityTierModel)
|
|
223
|
+
|| requestedModel === CODEX_AUTO_REVIEW_MODEL
|
|
200
224
|
|| isResponsesOnlyModel(upstreamModel, getCachedModelEndpointsFn)
|
|
201
225
|
|| (usesCustomTools && cachedModelSupportsResponses(upstreamModel, getCachedModelEndpointsFn));
|
|
202
226
|
if (usesCustomTools && !useNativeResponses) throw unsupportedCustomToolsError(upstreamModel);
|
|
@@ -209,6 +233,7 @@ export function createResponsesHandler(options) {
|
|
|
209
233
|
surface: "responses",
|
|
210
234
|
});
|
|
211
235
|
prepared = applyResponseHistoryRoutePlan(prepared, routePlan);
|
|
236
|
+
if (prepared.historyParentId === null) releaseHistorySnapshot();
|
|
212
237
|
applyCopilotResponsesRequestPolicies(prepared.body);
|
|
213
238
|
if (routePlan.protocol === "openai-responses") {
|
|
214
239
|
const result = await proxyCopilotResponses(prepared, req, res, responsesFn, {
|
|
@@ -241,11 +266,13 @@ export function createResponsesHandler(options) {
|
|
|
241
266
|
"Cache-Control": "no-cache",
|
|
242
267
|
Connection: "keep-alive",
|
|
243
268
|
});
|
|
244
|
-
await writeOrDrain(res, `event: ${event}\ndata: ${JSON.stringify(data)}\n\n`);
|
|
269
|
+
const written = await writeOrDrain(res, `event: ${event}\ndata: ${JSON.stringify(data)}\n\n`);
|
|
270
|
+
if (!written) return false;
|
|
245
271
|
if (event === "response.completed") {
|
|
246
272
|
rememberResponseHistory(prepared, data.response);
|
|
247
273
|
recordResponsesUsage({ surface: prepared.surface, mode: "stream", model, response: data.response, event: data });
|
|
248
274
|
}
|
|
275
|
+
return true;
|
|
249
276
|
}, () => { if (!res.writableEnded) res.end(); }, async (statusCode, errMsg, error, upstreamResponse) => {
|
|
250
277
|
if (!res.headersSent) {
|
|
251
278
|
if (upstreamResponse) {
|
|
@@ -282,7 +309,12 @@ export function createResponsesHandler(options) {
|
|
|
282
309
|
onUpstreamStart: startUpstreamTimeout,
|
|
283
310
|
});
|
|
284
311
|
releaseChatPayload();
|
|
285
|
-
const data =
|
|
312
|
+
const data = upstream.ok
|
|
313
|
+
? await upstream.text()
|
|
314
|
+
: await readBoundedResponseText(upstream, {
|
|
315
|
+
maxBytes: MAX_UPSTREAM_ERROR_BODY_BYTES,
|
|
316
|
+
label: "Copilot Chat error body",
|
|
317
|
+
});
|
|
286
318
|
if (!upstream.ok) {
|
|
287
319
|
markAdaptiveHttpTimeout(upstream.status);
|
|
288
320
|
sendUpstreamError(res, upstream, data);
|
|
@@ -311,6 +343,7 @@ export function createResponsesHandler(options) {
|
|
|
311
343
|
sendJsonError(res, responseError, 502);
|
|
312
344
|
} finally {
|
|
313
345
|
releaseUpstreamPayload();
|
|
346
|
+
releaseHistorySnapshot();
|
|
314
347
|
abort.cleanup();
|
|
315
348
|
}
|
|
316
349
|
};
|
|
@@ -321,6 +354,7 @@ export function createResponsesCompactHandler(options) {
|
|
|
321
354
|
acquireRequest,
|
|
322
355
|
autoReviewModelResolver,
|
|
323
356
|
imagePressure,
|
|
357
|
+
modelRegistry,
|
|
324
358
|
openAIModelEnv,
|
|
325
359
|
requestBodyTimeoutMs,
|
|
326
360
|
responsesCompactFn,
|
|
@@ -332,7 +366,47 @@ export function createResponsesCompactHandler(options) {
|
|
|
332
366
|
const abort = createRequestAbort(req, res);
|
|
333
367
|
let releaseRequest = () => {};
|
|
334
368
|
let prepared = null;
|
|
369
|
+
let imagePressureResult = null;
|
|
370
|
+
let historySnapshot = null;
|
|
335
371
|
let upstreamPayloadReleased = false;
|
|
372
|
+
let activeDeadline = Number.POSITIVE_INFINITY;
|
|
373
|
+
let activeTimeoutReason = "responses_prepare_timeout";
|
|
374
|
+
const releaseHistorySnapshot = () => {
|
|
375
|
+
historySnapshot?.release();
|
|
376
|
+
historySnapshot = null;
|
|
377
|
+
};
|
|
378
|
+
const assertPrepareActive = () => {
|
|
379
|
+
if (abort.signal.aborted) throw abort.signal.reason;
|
|
380
|
+
if (now() < activeDeadline) return;
|
|
381
|
+
abort.abort(activeTimeoutReason);
|
|
382
|
+
throw abort.signal.reason;
|
|
383
|
+
};
|
|
384
|
+
const startUpstreamTimeout = () => {
|
|
385
|
+
assertPrepareActive();
|
|
386
|
+
activeDeadline = now() + upstreamTimeoutMs;
|
|
387
|
+
activeTimeoutReason = "upstream_timeout";
|
|
388
|
+
abort.setTimeout(upstreamTimeoutMs, "upstream_timeout");
|
|
389
|
+
};
|
|
390
|
+
const pressureRootId = () => responseHistoryPressureRootId(prepared);
|
|
391
|
+
const markCompactTimeout = () => {
|
|
392
|
+
const eligible = imagePressure?.isEligible?.(prepared) === true;
|
|
393
|
+
return imagePressure?.markTimeout?.(pressureRootId(), { eligible }) || false;
|
|
394
|
+
};
|
|
395
|
+
const applyAdaptiveTimeout = (error) => {
|
|
396
|
+
if (!["responses_prepare_timeout", "upstream_timeout"].includes(abort.reason)) return error;
|
|
397
|
+
if (!markCompactTimeout()) return error;
|
|
398
|
+
const message = "Responses compact timed out while processing a large visual history. Retry the request; ccdx will keep fewer earlier images automatically. Restart is not required.";
|
|
399
|
+
error.statusCode = 504;
|
|
400
|
+
error.jsonBody = {
|
|
401
|
+
error: {
|
|
402
|
+
message,
|
|
403
|
+
type: "timeout_error",
|
|
404
|
+
code: "ccdx_visual_history_timeout",
|
|
405
|
+
retryable: true,
|
|
406
|
+
},
|
|
407
|
+
};
|
|
408
|
+
return error;
|
|
409
|
+
};
|
|
336
410
|
const releaseUpstreamPayload = (finalContext) => {
|
|
337
411
|
if (upstreamPayloadReleased) return;
|
|
338
412
|
upstreamPayloadReleased = true;
|
|
@@ -348,39 +422,43 @@ export function createResponsesCompactHandler(options) {
|
|
|
348
422
|
releaseRequest = await acquireRequest(req, { signal: abort.signal });
|
|
349
423
|
abort.setTimeout(requestBodyTimeoutMs, "request_body_timeout");
|
|
350
424
|
const parsed = await readJsonBody(req, { admission: releaseRequest, signal: abort.signal });
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
const assertPrepareActive = () => {
|
|
354
|
-
if (abort.signal.aborted) throw abort.signal.reason;
|
|
355
|
-
if (now() < activeDeadline) return;
|
|
356
|
-
abort.abort(activeTimeoutReason);
|
|
357
|
-
throw abort.signal.reason;
|
|
358
|
-
};
|
|
359
|
-
const startUpstreamTimeout = () => {
|
|
360
|
-
assertPrepareActive();
|
|
361
|
-
activeDeadline = now() + upstreamTimeoutMs;
|
|
362
|
-
activeTimeoutReason = "upstream_timeout";
|
|
363
|
-
abort.setTimeout(upstreamTimeoutMs, "upstream_timeout");
|
|
364
|
-
};
|
|
425
|
+
activeDeadline = now() + upstreamTimeoutMs;
|
|
426
|
+
activeTimeoutReason = "responses_prepare_timeout";
|
|
365
427
|
abort.setTimeout(upstreamTimeoutMs, "responses_prepare_timeout");
|
|
366
428
|
if (parsed.previous_response_id !== undefined && parsed.previous_response_id !== null) {
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
429
|
+
historySnapshot = acquireResponseHistorySnapshot(parsed.previous_response_id, {
|
|
430
|
+
assertActive: assertPrepareActive,
|
|
431
|
+
signal: abort.signal,
|
|
432
|
+
});
|
|
433
|
+
if (historySnapshot.bytes > 0) {
|
|
434
|
+
await releaseRequest.reserveResponseHistory?.(historySnapshot.bytes, { signal: abort.signal });
|
|
370
435
|
}
|
|
371
436
|
assertPrepareActive();
|
|
372
437
|
}
|
|
373
|
-
prepared =
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
);
|
|
438
|
+
prepared = prepareResponsesRequest(parsed, {
|
|
439
|
+
assertActive: assertPrepareActive,
|
|
440
|
+
copilotBoundary: false,
|
|
441
|
+
historySnapshot,
|
|
442
|
+
mutate: true,
|
|
443
|
+
});
|
|
444
|
+
imagePressureResult = imagePressure?.applyRecovery?.(prepared, { assertActive: assertPrepareActive }) || null;
|
|
445
|
+
if (imagePressureResult?.adapted) {
|
|
446
|
+
console.warn(status("warn", `responses compact visual history mode=recovery historical_images=${imagePressureResult.initialHistoricalImages}->${imagePressureResult.historicalImages} omitted=${imagePressureResult.imagesOmitted} bytes=${imagePressureResult.initialBodyBytes}->${imagePressureResult.bodyBytes}`));
|
|
447
|
+
}
|
|
448
|
+
prepared = prepareResponsesCompactionRequest(prepared);
|
|
380
449
|
assertPrepareActive();
|
|
381
450
|
prepared.surface = "responses_compact";
|
|
382
451
|
const model = parsed.model || "unknown";
|
|
383
|
-
const
|
|
452
|
+
const resolvedModel = resolveRequestModel(model, openAIModelEnv, autoReviewModelResolver);
|
|
453
|
+
const { requestedModel } = resolvedModel;
|
|
454
|
+
let { upstreamModel } = resolvedModel;
|
|
455
|
+
const priorityTierModel = requestedModel === upstreamModel
|
|
456
|
+
? resolveCopilotPriorityTierModel(upstreamModel, prepared.body.service_tier, modelRegistry?.models)
|
|
457
|
+
: null;
|
|
458
|
+
if (priorityTierModel) {
|
|
459
|
+
upstreamModel = priorityTierModel;
|
|
460
|
+
delete prepared.body.service_tier;
|
|
461
|
+
}
|
|
384
462
|
if (upstreamModel !== requestedModel) prepared.body.model = upstreamModel;
|
|
385
463
|
const upstreamLog = upstreamModel === requestedModel ? "" : ` upstream_model=${upstreamModel}`;
|
|
386
464
|
console.log(status("info", `responses compact model=${requestedModel}${upstreamLog} stream=false`));
|
|
@@ -392,6 +470,7 @@ export function createResponsesCompactHandler(options) {
|
|
|
392
470
|
model: upstreamModel,
|
|
393
471
|
surface: "responses-compact",
|
|
394
472
|
}));
|
|
473
|
+
releaseHistorySnapshot();
|
|
395
474
|
applyCopilotResponsesRequestPolicies(prepared.body);
|
|
396
475
|
const result = await proxyCopilotResponses(prepared, req, res, responsesCompactFn, {
|
|
397
476
|
assertPrepareActive,
|
|
@@ -401,11 +480,14 @@ export function createResponsesCompactHandler(options) {
|
|
|
401
480
|
releaseRequest: releaseUpstreamPayload,
|
|
402
481
|
});
|
|
403
482
|
if (result?.compacted) imagePressure?.clear?.(responseHistoryPressureRootId(prepared));
|
|
483
|
+
else if (result?.upstreamStatus === 408) markCompactTimeout();
|
|
404
484
|
} catch (error) {
|
|
405
|
-
|
|
406
|
-
|
|
485
|
+
const responseError = applyAdaptiveTimeout(error);
|
|
486
|
+
logRequestFailure("Responses compact", responseError, abort);
|
|
487
|
+
sendJsonError(res, responseError, 502);
|
|
407
488
|
} finally {
|
|
408
489
|
releaseUpstreamPayload();
|
|
490
|
+
releaseHistorySnapshot();
|
|
409
491
|
abort.cleanup();
|
|
410
492
|
}
|
|
411
493
|
};
|
|
@@ -25,6 +25,20 @@ function bodyPayload(body, assertActive) {
|
|
|
25
25
|
return { bodyText, bodyBytes: Buffer.byteLength(bodyText) };
|
|
26
26
|
}
|
|
27
27
|
|
|
28
|
+
export function responsesImagePressureEligible(reqContext, {
|
|
29
|
+
assertActive,
|
|
30
|
+
policy: policyOverrides,
|
|
31
|
+
} = {}) {
|
|
32
|
+
const policy = normalizedPolicy(policyOverrides);
|
|
33
|
+
const currentInputStart = Number.isFinite(reqContext?.currentInputStart)
|
|
34
|
+
? reqContext.currentInputStart
|
|
35
|
+
: 0;
|
|
36
|
+
const images = responsesHistoricalImageStats(reqContext?.body?.input, currentInputStart, { assertActive });
|
|
37
|
+
if (images.historicalImages === 0) return false;
|
|
38
|
+
if (images.historicalImages > policy.triggerHistoricalImages) return true;
|
|
39
|
+
return bodyPayload(reqContext?.body, assertActive).bodyBytes > policy.triggerBodyBytes;
|
|
40
|
+
}
|
|
41
|
+
|
|
28
42
|
export function applyResponsesImagePressure(reqContext, {
|
|
29
43
|
assertActive,
|
|
30
44
|
mode = "normal",
|
|
@@ -143,6 +157,22 @@ export function createResponsesImagePressureController({
|
|
|
143
157
|
}
|
|
144
158
|
return result;
|
|
145
159
|
},
|
|
160
|
+
isEligible(reqContext, { assertActive } = {}) {
|
|
161
|
+
return responsesImagePressureEligible(reqContext, { assertActive, policy });
|
|
162
|
+
},
|
|
163
|
+
applyRecovery(reqContext, { assertActive } = {}) {
|
|
164
|
+
if (modeFor(reqContext?.historyRootId) !== "recovery") return null;
|
|
165
|
+
const result = applyResponsesImagePressure(reqContext, {
|
|
166
|
+
assertActive,
|
|
167
|
+
mode: "recovery",
|
|
168
|
+
policy,
|
|
169
|
+
});
|
|
170
|
+
if (result.adapted) {
|
|
171
|
+
counters.adaptedRequests += 1;
|
|
172
|
+
counters.imagesOmitted += result.imagesOmitted;
|
|
173
|
+
}
|
|
174
|
+
return result;
|
|
175
|
+
},
|
|
146
176
|
markTimeout(rootId, { eligible = false } = {}) {
|
|
147
177
|
if (!rootId || !eligible) return false;
|
|
148
178
|
prune();
|
package/src/responses-proxy.mjs
CHANGED
|
@@ -2,6 +2,8 @@ import { responses as copilotResponses } from "./copilot.mjs";
|
|
|
2
2
|
import {
|
|
3
3
|
httpError,
|
|
4
4
|
logRequestFailure,
|
|
5
|
+
MAX_UPSTREAM_ERROR_BODY_BYTES,
|
|
6
|
+
readBoundedResponseText,
|
|
5
7
|
sendUpstreamError,
|
|
6
8
|
writeOrDrain,
|
|
7
9
|
} from "./http-transport.mjs";
|
|
@@ -262,7 +264,12 @@ export async function proxyCopilotResponses(reqContext, req, res, upstream = cop
|
|
|
262
264
|
reader.releaseLock();
|
|
263
265
|
}
|
|
264
266
|
} else {
|
|
265
|
-
const data =
|
|
267
|
+
const data = resp.ok
|
|
268
|
+
? await resp.text()
|
|
269
|
+
: await readBoundedResponseText(resp, {
|
|
270
|
+
maxBytes: MAX_UPSTREAM_ERROR_BODY_BYTES,
|
|
271
|
+
label: "Copilot Responses error body",
|
|
272
|
+
});
|
|
266
273
|
if (resp.ok && reqContext.surface === "responses_compact") {
|
|
267
274
|
const response = parseResponsesCompactionResult(
|
|
268
275
|
compactionInputWithoutTrigger(reqContext.body.input),
|
|
@@ -298,6 +298,7 @@ export function responseHistoryPressureRootId(reqContext) {
|
|
|
298
298
|
export function prepareResponsesRequest(reqBody, {
|
|
299
299
|
assertActive,
|
|
300
300
|
copilotBoundary = true,
|
|
301
|
+
historySnapshot,
|
|
301
302
|
mutate = false,
|
|
302
303
|
} = {}) {
|
|
303
304
|
assertActive?.();
|
|
@@ -313,11 +314,22 @@ export function prepareResponsesRequest(reqBody, {
|
|
|
313
314
|
const historyRouteMetadata = [];
|
|
314
315
|
|
|
315
316
|
if (previousId !== undefined && previousId !== null) {
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
317
|
+
if (historySnapshot) {
|
|
318
|
+
if (historySnapshot.responseId !== previousId) {
|
|
319
|
+
throw new TypeError("Response history snapshot does not match previous_response_id");
|
|
320
|
+
}
|
|
321
|
+
historyItems = historySnapshot.materialize({
|
|
322
|
+
assertActive,
|
|
323
|
+
routeMetadata: historyRouteMetadata,
|
|
324
|
+
});
|
|
325
|
+
historyRootId = historySnapshot.rootId;
|
|
326
|
+
} else {
|
|
327
|
+
historyItems = materializeResponseHistory(previousId, {
|
|
328
|
+
assertActive,
|
|
329
|
+
routeMetadata: historyRouteMetadata,
|
|
330
|
+
});
|
|
331
|
+
historyRootId = responseHistoryRootId(previousId);
|
|
332
|
+
}
|
|
321
333
|
body.input = [...historyItems, ...currentInputItems];
|
|
322
334
|
} else {
|
|
323
335
|
body.input = currentInputItems;
|
package/src/usage.mjs
CHANGED
|
@@ -150,7 +150,7 @@ export async function flushUsageWrites() {
|
|
|
150
150
|
await writeQueue;
|
|
151
151
|
}
|
|
152
152
|
|
|
153
|
-
async function* iterateUsageRecords(filePath) {
|
|
153
|
+
async function* iterateUsageRecords(filePath, { warn = console.error } = {}) {
|
|
154
154
|
let input;
|
|
155
155
|
try {
|
|
156
156
|
input = fs.createReadStream(filePath, { encoding: "utf8" });
|
|
@@ -165,14 +165,31 @@ async function* iterateUsageRecords(filePath) {
|
|
|
165
165
|
}
|
|
166
166
|
|
|
167
167
|
const lines = readline.createInterface({ input, crlfDelay: Infinity });
|
|
168
|
+
let invalidRecords = 0;
|
|
168
169
|
for await (const line of lines) {
|
|
169
|
-
if (line)
|
|
170
|
+
if (!line) continue;
|
|
171
|
+
try {
|
|
172
|
+
const record = JSON.parse(line);
|
|
173
|
+
if (!record || typeof record !== "object" || Array.isArray(record)) {
|
|
174
|
+
invalidRecords += 1;
|
|
175
|
+
continue;
|
|
176
|
+
}
|
|
177
|
+
yield record;
|
|
178
|
+
} catch {
|
|
179
|
+
invalidRecords += 1;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
if (invalidRecords > 0) {
|
|
183
|
+
try {
|
|
184
|
+
const safePath = terminalCell(JSON.stringify(filePath));
|
|
185
|
+
warn(`codex-copilot-dx usage log ignored ${invalidRecords} invalid record${invalidRecords === 1 ? "" : "s"} in ${safePath}`);
|
|
186
|
+
} catch {}
|
|
170
187
|
}
|
|
171
188
|
}
|
|
172
189
|
|
|
173
|
-
export async function readUsageRecords(filePath = usageLogPath()) {
|
|
190
|
+
export async function readUsageRecords(filePath = usageLogPath(), { warn = console.error } = {}) {
|
|
174
191
|
const records = [];
|
|
175
|
-
for await (const record of iterateUsageRecords(filePath)) records.push(record);
|
|
192
|
+
for await (const record of iterateUsageRecords(filePath, { warn })) records.push(record);
|
|
176
193
|
return records;
|
|
177
194
|
}
|
|
178
195
|
|
|
@@ -200,10 +217,10 @@ export function summarizeUsage(records) {
|
|
|
200
217
|
return summary;
|
|
201
218
|
}
|
|
202
219
|
|
|
203
|
-
export async function summarizeUsageLogs(filePath = usageLogPath()) {
|
|
220
|
+
export async function summarizeUsageLogs(filePath = usageLogPath(), { warn = console.error } = {}) {
|
|
204
221
|
const summary = { requests: 0, totals: {}, byModel: {} };
|
|
205
222
|
for (const candidate of [rotatedFilePath(filePath), filePath]) {
|
|
206
|
-
for await (const record of iterateUsageRecords(candidate)) {
|
|
223
|
+
for await (const record of iterateUsageRecords(candidate, { warn })) {
|
|
207
224
|
const one = summarizeUsage([record]);
|
|
208
225
|
summary.requests += one.requests;
|
|
209
226
|
addUsageTotals(summary.totals, one.totals);
|
|
@@ -324,7 +341,8 @@ export async function printUsageSummary({
|
|
|
324
341
|
format = "auto",
|
|
325
342
|
output = process.stdout,
|
|
326
343
|
log = console.log,
|
|
344
|
+
warn = console.error,
|
|
327
345
|
} = {}) {
|
|
328
|
-
const summary = await summarizeUsageLogs(filePath);
|
|
346
|
+
const summary = await summarizeUsageLogs(filePath, { warn });
|
|
329
347
|
log(formatUsageSummary(summary, { filePath, format, output }));
|
|
330
348
|
}
|