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