codex-copilot-dx 0.4.43 → 0.4.44
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 +20 -12
- package/bin/cli.mjs +4 -4
- package/package.json +2 -1
- package/src/adapter.mjs +16 -8
- package/src/anthropic-handler.mjs +13 -5
- package/src/anthropic.mjs +40 -5
- package/src/auth.mjs +2 -2
- package/src/bounded-result-cache.mjs +180 -0
- package/src/claude-config.mjs +1 -1
- package/src/cli-options.mjs +7 -5
- package/src/copilot.mjs +11 -4
- package/src/doctor.mjs +11 -2
- package/src/http-transport.mjs +4 -1
- package/src/image-optimization.mjs +74 -65
- package/src/observability.mjs +2 -1
- package/src/responses-bridge.mjs +46 -10
- package/src/responses-chat-payload.mjs +2 -1
- package/src/responses-compaction.mjs +113 -0
- package/src/responses-handler.mjs +28 -14
- package/src/responses-proxy.mjs +123 -23
- package/src/stream-contract.mjs +102 -0
- package/src/stream-errors.mjs +3 -1
- package/src/stream-performance.mjs +177 -0
- package/src/upstream-headers.mjs +67 -0
package/src/doctor.mjs
CHANGED
|
@@ -124,6 +124,15 @@ function parseResponseObject(text) {
|
|
|
124
124
|
return response;
|
|
125
125
|
}
|
|
126
126
|
|
|
127
|
+
function parseCompactionResponse(text) {
|
|
128
|
+
const response = parseResponseObject(text);
|
|
129
|
+
if (response.object !== "response.compaction"
|
|
130
|
+
|| !response.output.some((item) => item?.type === "compaction")) {
|
|
131
|
+
throw new Error("response body is missing valid compaction state");
|
|
132
|
+
}
|
|
133
|
+
return response;
|
|
134
|
+
}
|
|
135
|
+
|
|
127
136
|
export async function inspectAdapterCompatibility({
|
|
128
137
|
host = "127.0.0.1",
|
|
129
138
|
port = 2026,
|
|
@@ -184,7 +193,7 @@ export async function inspectAdapterCompatibility({
|
|
|
184
193
|
stream: false,
|
|
185
194
|
input: "Compact this short context.",
|
|
186
195
|
}, timeoutMs);
|
|
187
|
-
|
|
196
|
+
parseCompactionResponse(text);
|
|
188
197
|
});
|
|
189
198
|
|
|
190
199
|
if (!claudeModel) {
|
|
@@ -446,7 +455,7 @@ export async function collectDoctorChecks({
|
|
|
446
455
|
export async function runDoctor(options = {}) {
|
|
447
456
|
const log = options.log || console.log;
|
|
448
457
|
const flags = [options.online ? "--online" : "", options.compat ? "--compat" : ""].filter(Boolean);
|
|
449
|
-
log(`
|
|
458
|
+
log(`ccdx doctor${flags.length ? ` ${flags.join(" ")}` : ""}`);
|
|
450
459
|
const checks = await collectDoctorChecks(options);
|
|
451
460
|
for (const check of checks) log(status(check.kind, check.message));
|
|
452
461
|
return checks;
|
package/src/http-transport.mjs
CHANGED
|
@@ -2,6 +2,7 @@ import { promisify } from "node:util";
|
|
|
2
2
|
import * as zlib from "node:zlib";
|
|
3
3
|
import { loadRuntimeConfig, parsePositiveInteger } from "./runtime-config.mjs";
|
|
4
4
|
import { status } from "./status.mjs";
|
|
5
|
+
import { safeUpstreamResponseHeaders } from "./upstream-headers.mjs";
|
|
5
6
|
|
|
6
7
|
const COMPRESSED_BODY_WEIGHT_MULTIPLIER = 4;
|
|
7
8
|
const gunzipAsync = promisify(zlib.gunzip);
|
|
@@ -296,7 +297,9 @@ export function sendJsonError(res, err, fallbackStatus = 400) {
|
|
|
296
297
|
|
|
297
298
|
export function sendUpstreamError(res, response, text) {
|
|
298
299
|
if (!res.headersSent) {
|
|
299
|
-
res.writeHead(response.status || 502,
|
|
300
|
+
res.writeHead(response.status || 502, safeUpstreamResponseHeaders(response.headers, {
|
|
301
|
+
defaultContentType: "application/json",
|
|
302
|
+
}));
|
|
300
303
|
}
|
|
301
304
|
res.end(text || JSON.stringify({ error: "Upstream request failed" }));
|
|
302
305
|
}
|
|
@@ -4,17 +4,19 @@ import {
|
|
|
4
4
|
readResponsesImagePart,
|
|
5
5
|
readResponsesToolOutputParts,
|
|
6
6
|
} from "./responses-content.mjs";
|
|
7
|
+
import { createBoundedResultCache } from "./bounded-result-cache.mjs";
|
|
7
8
|
import { status } from "./status.mjs";
|
|
8
9
|
|
|
9
10
|
const DEFAULT_IMG_CONCURRENCY = 2;
|
|
10
11
|
const IMG_MAX_CONCURRENCY = 12;
|
|
11
12
|
const DEFAULT_IMG_MAX_INPUT_PIXELS = 40 * 1000 * 1000;
|
|
13
|
+
const DEFAULT_IMG_CACHE_MAX_BYTES = 64 * 1024 * 1024;
|
|
12
14
|
const DEFAULT_MAX_UPSTREAM_BODY_BYTES = 30 * 1024 * 1024;
|
|
13
|
-
const MAX_OPTIMIZED_IMAGE_DIGESTS = 4096;
|
|
14
15
|
const IMG_MAX_DIM = positiveInt(process.env.CCDX_IMG_MAX_DIM, 2048);
|
|
15
16
|
const IMG_QUALITY = positiveInt(process.env.CCDX_IMG_QUALITY, 82, 100);
|
|
16
17
|
const IMG_MIN_BYTES = nonNegativeInt(process.env.CCDX_IMG_MIN_BYTES, 100000);
|
|
17
18
|
const IMG_MAX_INPUT_PIXELS = positiveInt(process.env.CCDX_IMG_MAX_INPUT_PIXELS, DEFAULT_IMG_MAX_INPUT_PIXELS);
|
|
19
|
+
const IMG_CACHE_MAX_BYTES = nonNegativeInt(process.env.CCDX_IMG_CACHE_MAX_BYTES, DEFAULT_IMG_CACHE_MAX_BYTES);
|
|
18
20
|
const MAX_UPSTREAM_BODY_BYTES = positiveInt(process.env.CCDX_MAX_UPSTREAM_BODY_BYTES, DEFAULT_MAX_UPSTREAM_BODY_BYTES);
|
|
19
21
|
const IMG_OPT_DISABLED = process.env.CCDX_DISABLE_IMG_OPT === "1";
|
|
20
22
|
const IMG_CONCURRENCY = parseImageConcurrency(process.env.CCDX_IMG_CONCURRENCY);
|
|
@@ -22,7 +24,8 @@ const OVERSIZE_IMAGE_PROFILES = [
|
|
|
22
24
|
{ maxDim: Math.min(IMG_MAX_DIM, 1600), quality: Math.min(IMG_QUALITY, 75) },
|
|
23
25
|
{ maxDim: Math.min(IMG_MAX_DIM, 1280), quality: Math.min(IMG_QUALITY, 65) },
|
|
24
26
|
];
|
|
25
|
-
const
|
|
27
|
+
const IMAGE_ENCODER_CONFIG = "webp:v1:effort=4:autorotate=1:fit=inside:without_enlargement=1";
|
|
28
|
+
const imageResultCache = createBoundedResultCache({ maxBytes: IMG_CACHE_MAX_BYTES });
|
|
26
29
|
let sharpImport = null;
|
|
27
30
|
|
|
28
31
|
function positiveInt(value, fallback, max = Number.MAX_SAFE_INTEGER) {
|
|
@@ -140,52 +143,50 @@ export function createTaskLimiter(concurrency) {
|
|
|
140
143
|
const runGlobalImageTask = createTaskLimiter(IMG_CONCURRENCY);
|
|
141
144
|
|
|
142
145
|
export function imageOptimizationStats() {
|
|
146
|
+
const cache = imageResultCache.stats();
|
|
143
147
|
return {
|
|
144
148
|
...runGlobalImageTask.stats(),
|
|
145
149
|
disabled: IMG_OPT_DISABLED,
|
|
146
|
-
cache_entries:
|
|
150
|
+
cache_entries: cache.entries,
|
|
151
|
+
cache_bytes: cache.bytes,
|
|
152
|
+
cache_max_bytes: cache.max_bytes,
|
|
153
|
+
cache_hits: cache.hits,
|
|
154
|
+
cache_misses: cache.misses,
|
|
155
|
+
cache_evictions: cache.evictions,
|
|
156
|
+
cache_inflight: cache.inflight,
|
|
147
157
|
sharp_loaded: sharpImport !== null,
|
|
148
158
|
};
|
|
149
159
|
}
|
|
150
160
|
|
|
161
|
+
export function resetImageOptimizationCacheForTests() {
|
|
162
|
+
imageResultCache.clear();
|
|
163
|
+
}
|
|
164
|
+
|
|
151
165
|
async function sharp() {
|
|
152
166
|
sharpImport ||= import("sharp");
|
|
153
167
|
const mod = await sharpImport;
|
|
154
168
|
return mod.default || mod;
|
|
155
169
|
}
|
|
156
170
|
|
|
157
|
-
function imageDigest(
|
|
158
|
-
return createHash("sha256").update(
|
|
171
|
+
function imageDigest(value, encoding) {
|
|
172
|
+
return createHash("sha256").update(value, encoding).digest("base64url");
|
|
159
173
|
}
|
|
160
174
|
|
|
161
|
-
function imageOptimizationKey(digest, constraints, quality) {
|
|
175
|
+
function imageOptimizationKey(digest, mime, constraints, quality) {
|
|
162
176
|
return [
|
|
163
177
|
digest,
|
|
178
|
+
mime,
|
|
164
179
|
constraints.maxLongEdge || "",
|
|
165
180
|
constraints.maxShortEdge || "",
|
|
166
181
|
constraints.maxArea || "",
|
|
167
182
|
quality,
|
|
183
|
+
IMAGE_ENCODER_CONFIG,
|
|
168
184
|
].join(":");
|
|
169
185
|
}
|
|
170
186
|
|
|
171
|
-
function touchOptimizedImage(digest) {
|
|
172
|
-
if (!optimizedImageDigests.delete(digest)) return false;
|
|
173
|
-
optimizedImageDigests.add(digest);
|
|
174
|
-
return true;
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
function rememberOptimizedImage(digest) {
|
|
178
|
-
optimizedImageDigests.delete(digest);
|
|
179
|
-
optimizedImageDigests.add(digest);
|
|
180
|
-
if (optimizedImageDigests.size > MAX_OPTIMIZED_IMAGE_DIGESTS) {
|
|
181
|
-
optimizedImageDigests.delete(optimizedImageDigests.values().next().value);
|
|
182
|
-
}
|
|
183
|
-
}
|
|
184
|
-
|
|
185
187
|
export async function optimizeImageDataUrl(dataUrl, {
|
|
186
188
|
maxDim = IMG_MAX_DIM,
|
|
187
189
|
quality = IMG_QUALITY,
|
|
188
|
-
force = false,
|
|
189
190
|
model,
|
|
190
191
|
signal,
|
|
191
192
|
} = {}) {
|
|
@@ -197,52 +198,60 @@ export async function optimizeImageDataUrl(dataUrl, {
|
|
|
197
198
|
const encoded = match[2];
|
|
198
199
|
const inputBytes = Buffer.byteLength(encoded, "base64");
|
|
199
200
|
if (mime.includes("gif")) return dataUrl;
|
|
201
|
+
if (signal?.aborted) throw abortError(signal);
|
|
202
|
+
const constraints = imageConstraintsForModel(model, { maxDim });
|
|
203
|
+
const outputQuality = positiveInt(quality, IMG_QUALITY, 100);
|
|
204
|
+
// Hash decoded source bytes without retaining a decoded Buffer. Cache hits
|
|
205
|
+
// therefore bypass both Sharp and the global image work queue.
|
|
206
|
+
const digest = imageDigest(encoded, "base64");
|
|
207
|
+
const optimizationKey = imageOptimizationKey(digest, mime, constraints, outputQuality);
|
|
200
208
|
|
|
201
209
|
try {
|
|
202
|
-
return await
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
.
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
210
|
+
return await imageResultCache.getOrCreate(optimizationKey, (sharedSignal) => (
|
|
211
|
+
runGlobalImageTask(async () => {
|
|
212
|
+
const raw = Buffer.from(encoded, "base64");
|
|
213
|
+
const resize = await sharp();
|
|
214
|
+
const image = resize(raw, { failOn: "none", limitInputPixels: IMG_MAX_INPUT_PIXELS });
|
|
215
|
+
const metadata = await image.metadata();
|
|
216
|
+
if (sharedSignal.aborted) throw abortError(sharedSignal);
|
|
217
|
+
const orientation = Number(metadata.orientation);
|
|
218
|
+
const rotated = orientation >= 5 && orientation <= 8;
|
|
219
|
+
const sourceWidth = rotated ? metadata.height : metadata.width;
|
|
220
|
+
const sourceHeight = rotated ? metadata.width : metadata.height;
|
|
221
|
+
const target = fitImageDimensions(sourceWidth, sourceHeight, constraints);
|
|
222
|
+
const resizeRequired = target
|
|
223
|
+
&& (target.width < sourceWidth || target.height < sourceHeight);
|
|
224
|
+
if (raw.length < IMG_MIN_BYTES && !resizeRequired) return dataUrl;
|
|
225
|
+
const resizeOptions = target
|
|
226
|
+
? { ...target, fit: "inside", withoutEnlargement: true }
|
|
227
|
+
: {
|
|
228
|
+
width: constraints.maxLongEdge,
|
|
229
|
+
height: constraints.maxLongEdge,
|
|
230
|
+
fit: "inside",
|
|
231
|
+
withoutEnlargement: true,
|
|
232
|
+
};
|
|
233
|
+
const out = await image
|
|
234
|
+
.rotate()
|
|
235
|
+
.resize(resizeOptions)
|
|
236
|
+
.webp({ quality: outputQuality, effort: 4 })
|
|
237
|
+
.toBuffer();
|
|
238
|
+
if (sharedSignal.aborted) throw abortError(sharedSignal);
|
|
239
|
+
if (out.length >= raw.length) return dataUrl;
|
|
240
|
+
const outputBase64 = out.toString("base64");
|
|
241
|
+
const outputDataUrl = `data:image/webp;base64,${outputBase64}`;
|
|
242
|
+
const outputKey = imageOptimizationKey(
|
|
243
|
+
imageDigest(outputBase64, "base64"),
|
|
244
|
+
"image/webp",
|
|
245
|
+
constraints,
|
|
246
|
+
outputQuality,
|
|
247
|
+
);
|
|
248
|
+
// Preserve idempotence when optimized history is submitted again.
|
|
249
|
+
imageResultCache.set(outputKey, outputDataUrl);
|
|
250
|
+
const ratio = ((out.length / raw.length) * 100).toFixed(1);
|
|
251
|
+
console.log(status("info", `image ${(raw.length / 1024).toFixed(0)}KB ${mime} -> ${(out.length / 1024).toFixed(0)}KB webp (${ratio}%)`));
|
|
252
|
+
return outputDataUrl;
|
|
253
|
+
}, { signal: sharedSignal })
|
|
254
|
+
), { signal });
|
|
246
255
|
} catch (e) {
|
|
247
256
|
if (signal?.aborted || e?.name === "AbortError") throw e;
|
|
248
257
|
if (inputBytes >= IMG_MIN_BYTES) {
|
package/src/observability.mjs
CHANGED
|
@@ -118,7 +118,7 @@ function modelRegistryStatus(modelRegistry) {
|
|
|
118
118
|
};
|
|
119
119
|
}
|
|
120
120
|
|
|
121
|
-
export function runtimeStatusPayload({ metrics, admission, modelRegistry } = {}) {
|
|
121
|
+
export function runtimeStatusPayload({ metrics, streamPerformance, admission, modelRegistry } = {}) {
|
|
122
122
|
const memory = process.memoryUsage();
|
|
123
123
|
return {
|
|
124
124
|
...adapterHealthPayload(),
|
|
@@ -131,6 +131,7 @@ export function runtimeStatusPayload({ metrics, admission, modelRegistry } = {})
|
|
|
131
131
|
array_buffers_bytes: memory.arrayBuffers,
|
|
132
132
|
},
|
|
133
133
|
requests: metrics?.snapshot?.() || createRequestMetrics().snapshot(),
|
|
134
|
+
stream_performance: streamPerformance?.snapshot?.() || null,
|
|
134
135
|
admission: admission?.diagnostics?.() || admission?.stats?.() || null,
|
|
135
136
|
response_history: responseHistoryStats(),
|
|
136
137
|
image_optimization: imageOptimizationStats(),
|
package/src/responses-bridge.mjs
CHANGED
|
@@ -2,6 +2,15 @@ import { randomUUID } from "node:crypto";
|
|
|
2
2
|
import { chatCompletions } from "./copilot.mjs";
|
|
3
3
|
import { webStreamLines } from "./stream.mjs";
|
|
4
4
|
import { abortErrorStatusCode, isAbortLikeError } from "./http-transport.mjs";
|
|
5
|
+
import {
|
|
6
|
+
incompleteUpstreamStream,
|
|
7
|
+
invalidUpstreamStream,
|
|
8
|
+
requireUpstreamEventStream,
|
|
9
|
+
ToolArgumentDeltaGuard,
|
|
10
|
+
upstreamChatStreamError,
|
|
11
|
+
withChatStreamUsage,
|
|
12
|
+
} from "./stream-contract.mjs";
|
|
13
|
+
import { isChatOutputDelta, markFirstOutput, markOutputTokens } from "./stream-performance.mjs";
|
|
5
14
|
|
|
6
15
|
function cloneJson(value) {
|
|
7
16
|
return value === undefined ? undefined : structuredClone(value);
|
|
@@ -134,7 +143,7 @@ export function chatToResponses(chatResp, model) {
|
|
|
134
143
|
|
|
135
144
|
export async function forwardToChat(chatReq, emitEvent, onDone, onError, options = {}) {
|
|
136
145
|
delete chatReq.max_tokens;
|
|
137
|
-
const upstreamReq = chatReq
|
|
146
|
+
const upstreamReq = withChatStreamUsage(chatReq);
|
|
138
147
|
const bodyText = upstreamReq === chatReq ? options.bodyText : undefined;
|
|
139
148
|
let resp;
|
|
140
149
|
try {
|
|
@@ -150,9 +159,16 @@ export async function forwardToChat(chatReq, emitEvent, onDone, onError, options
|
|
|
150
159
|
return;
|
|
151
160
|
}
|
|
152
161
|
if (!resp.ok) {
|
|
153
|
-
await onError(resp.status, await resp.text());
|
|
162
|
+
await onError(resp.status, await resp.text(), undefined, resp);
|
|
154
163
|
return;
|
|
155
164
|
}
|
|
165
|
+
try {
|
|
166
|
+
await requireUpstreamEventStream(resp);
|
|
167
|
+
} catch (error) {
|
|
168
|
+
await onError(error.statusCode, error.message, error);
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
options.onUpstreamResponse?.(resp);
|
|
156
172
|
options.abort?.setTimeout(options.streamIdleTimeoutMs, "stream_idle_timeout");
|
|
157
173
|
const respId = `resp_${uid()}`;
|
|
158
174
|
let actualModel = chatReq.model || "unknown";
|
|
@@ -161,6 +177,13 @@ export async function forwardToChat(chatReq, emitEvent, onDone, onError, options
|
|
|
161
177
|
let nextOutputIndex = 0;
|
|
162
178
|
let usage = { input_tokens: 0, output_tokens: 0, total_tokens: 0 };
|
|
163
179
|
const toolCalls = new Map();
|
|
180
|
+
const toolArgumentGuard = new ToolArgumentDeltaGuard();
|
|
181
|
+
let performanceOutputSeen = false;
|
|
182
|
+
const markPerformanceOutput = () => {
|
|
183
|
+
if (performanceOutputSeen) return;
|
|
184
|
+
performanceOutputSeen = true;
|
|
185
|
+
markFirstOutput();
|
|
186
|
+
};
|
|
164
187
|
|
|
165
188
|
await emitEvent("response.created", { response: { id: respId, object: "response", status: "in_progress", model: actualModel, output: [] } });
|
|
166
189
|
|
|
@@ -258,7 +281,16 @@ export async function forwardToChat(chatReq, emitEvent, onDone, onError, options
|
|
|
258
281
|
const data = line.slice(6).trim();
|
|
259
282
|
if (data === "[DONE]") { await emitCompleted(); onDone(); return; }
|
|
260
283
|
let parsed;
|
|
261
|
-
try {
|
|
284
|
+
try {
|
|
285
|
+
parsed = JSON.parse(data);
|
|
286
|
+
} catch {
|
|
287
|
+
throw invalidUpstreamStream(
|
|
288
|
+
"Upstream Chat Completions SSE event contained invalid JSON",
|
|
289
|
+
"upstream_stream_json_invalid",
|
|
290
|
+
);
|
|
291
|
+
}
|
|
292
|
+
const upstreamError = upstreamChatStreamError(parsed);
|
|
293
|
+
if (upstreamError) throw upstreamError;
|
|
262
294
|
if (parsed.model) actualModel = parsed.model;
|
|
263
295
|
if (parsed.usage) {
|
|
264
296
|
usage = {
|
|
@@ -268,9 +300,11 @@ export async function forwardToChat(chatReq, emitEvent, onDone, onError, options
|
|
|
268
300
|
};
|
|
269
301
|
const cached = parsed.usage.prompt_tokens_details?.cached_tokens;
|
|
270
302
|
if (cached) usage.input_tokens_details = { cached_tokens: cached };
|
|
303
|
+
markOutputTokens(usage.output_tokens);
|
|
271
304
|
}
|
|
272
305
|
const delta = parsed.choices?.[0]?.delta;
|
|
273
306
|
if (!delta) continue;
|
|
307
|
+
if (isChatOutputDelta(delta)) markPerformanceOutput();
|
|
274
308
|
if (delta.content) {
|
|
275
309
|
const message = await ensureMessageItem();
|
|
276
310
|
fullText += delta.content;
|
|
@@ -281,22 +315,24 @@ export async function forwardToChat(chatReq, emitEvent, onDone, onError, options
|
|
|
281
315
|
const { tool, created } = await ensureToolCall(tc);
|
|
282
316
|
if (tc.id) tool.callId = tc.id;
|
|
283
317
|
if (!created && tc.function?.name) tool.name += tc.function.name;
|
|
284
|
-
|
|
285
|
-
|
|
318
|
+
const argumentDelta = tc.function?.arguments;
|
|
319
|
+
toolArgumentGuard.observe(tool.outputIndex, argumentDelta);
|
|
320
|
+
if (argumentDelta) {
|
|
321
|
+
tool.arguments += argumentDelta;
|
|
286
322
|
await emitEvent("response.function_call_arguments.delta", {
|
|
287
323
|
output_index: tool.outputIndex,
|
|
288
324
|
item_id: tool.id,
|
|
289
|
-
delta:
|
|
325
|
+
delta: argumentDelta,
|
|
290
326
|
});
|
|
291
327
|
}
|
|
292
328
|
}
|
|
293
329
|
}
|
|
294
330
|
}
|
|
295
331
|
} catch (e) {
|
|
296
|
-
const statusCode = isAbortLikeError(e) ? abortErrorStatusCode(options.abort?.reason) : 502;
|
|
297
|
-
await onError(statusCode, e?.message || "upstream stream error");
|
|
332
|
+
const statusCode = isAbortLikeError(e) ? abortErrorStatusCode(options.abort?.reason) : (e?.statusCode || 502);
|
|
333
|
+
await onError(statusCode, e?.message || "upstream stream error", e);
|
|
298
334
|
return;
|
|
299
335
|
}
|
|
300
|
-
|
|
301
|
-
|
|
336
|
+
const error = incompleteUpstreamStream("[DONE]");
|
|
337
|
+
await onError(error.statusCode, error.message, error);
|
|
302
338
|
}
|
|
@@ -5,6 +5,7 @@ import {
|
|
|
5
5
|
} from "./responses-byte-budget.mjs";
|
|
6
6
|
import { responsesToChat } from "./responses-bridge.mjs";
|
|
7
7
|
import { readResponsesImagePart, readResponsesToolOutputParts } from "./responses-content.mjs";
|
|
8
|
+
import { withChatStreamUsage } from "./stream-contract.mjs";
|
|
8
9
|
|
|
9
10
|
const DEFAULT_MAX_UPSTREAM_BODY_BYTES = 30 * 1024 * 1024;
|
|
10
11
|
|
|
@@ -30,7 +31,7 @@ function chatRequest(responsesBody, stream) {
|
|
|
30
31
|
const chatReq = responsesToChat(responsesBody);
|
|
31
32
|
chatReq.stream = Boolean(stream);
|
|
32
33
|
delete chatReq.max_tokens;
|
|
33
|
-
return chatReq;
|
|
34
|
+
return stream ? withChatStreamUsage(chatReq) : chatReq;
|
|
34
35
|
}
|
|
35
36
|
|
|
36
37
|
function adjustedResponsesTarget(limit, responsesBytes, chatBytes) {
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { httpError } from "./http-transport.mjs";
|
|
3
|
+
|
|
4
|
+
const COMPACTION_TRIGGER_TYPE = "compaction_trigger";
|
|
5
|
+
const RETAINED_MESSAGE_ROLES = new Set(["user", "assistant", "developer", "system"]);
|
|
6
|
+
const RETAINED_MESSAGE_TOKEN_BUDGET = 64_000;
|
|
7
|
+
const APPROX_BYTES_PER_TOKEN = 4;
|
|
8
|
+
|
|
9
|
+
function compactionError(message) {
|
|
10
|
+
const error = httpError(message, 502);
|
|
11
|
+
error.code = "ccdx_invalid_compaction_response";
|
|
12
|
+
error.jsonBody = {
|
|
13
|
+
error: {
|
|
14
|
+
message,
|
|
15
|
+
type: "upstream_error",
|
|
16
|
+
code: error.code,
|
|
17
|
+
},
|
|
18
|
+
};
|
|
19
|
+
return error;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function normalizeMessageContent(content) {
|
|
23
|
+
if (typeof content === "string") return [{ type: "input_text", text: content }];
|
|
24
|
+
if (!Array.isArray(content)) return [];
|
|
25
|
+
return content.map((part) => {
|
|
26
|
+
if (!part || typeof part !== "object") return part;
|
|
27
|
+
return part.type === "output_text"
|
|
28
|
+
? { ...part, type: "input_text" }
|
|
29
|
+
: structuredClone(part);
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function approximateTextTokens(content) {
|
|
34
|
+
return content.reduce((total, part) => {
|
|
35
|
+
if (part?.type !== "input_text" || typeof part.text !== "string") return total;
|
|
36
|
+
return total + Math.ceil(Buffer.byteLength(part.text) / APPROX_BYTES_PER_TOKEN);
|
|
37
|
+
}, 0);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function retainedMessages(inputItems) {
|
|
41
|
+
const kept = [];
|
|
42
|
+
let usedTokens = 0;
|
|
43
|
+
for (let index = inputItems.length - 1; index >= 0; index -= 1) {
|
|
44
|
+
const item = inputItems[index];
|
|
45
|
+
if (item?.type !== "message" || !RETAINED_MESSAGE_ROLES.has(item.role)) continue;
|
|
46
|
+
|
|
47
|
+
const content = normalizeMessageContent(item.content);
|
|
48
|
+
usedTokens += Math.max(approximateTextTokens(content), 1);
|
|
49
|
+
if (usedTokens > RETAINED_MESSAGE_TOKEN_BUDGET && kept.length > 0) break;
|
|
50
|
+
|
|
51
|
+
kept.push({
|
|
52
|
+
type: "message",
|
|
53
|
+
id: `msg_${randomUUID().replaceAll("-", "")}`,
|
|
54
|
+
status: item.status ?? "completed",
|
|
55
|
+
role: item.role,
|
|
56
|
+
content,
|
|
57
|
+
...(item.phase !== undefined ? { phase: structuredClone(item.phase) } : {}),
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
return kept.reverse();
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function prepareResponsesCompactionRequest(reqContext) {
|
|
64
|
+
const input = Array.isArray(reqContext?.body?.input) ? reqContext.body.input : [];
|
|
65
|
+
const compactionInputItems = compactionInputWithoutTrigger(input);
|
|
66
|
+
reqContext.body.input = [...compactionInputItems, { type: COMPACTION_TRIGGER_TYPE }];
|
|
67
|
+
reqContext.body.stream = false;
|
|
68
|
+
return reqContext;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export function compactionInputWithoutTrigger(input) {
|
|
72
|
+
return Array.isArray(input)
|
|
73
|
+
? input.filter((item) => item?.type !== COMPACTION_TRIGGER_TYPE)
|
|
74
|
+
: [];
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export function createResponsesCompactionResult(inputItems, generated) {
|
|
78
|
+
if (!generated || typeof generated !== "object" || !generated.id || !Array.isArray(generated.output)) {
|
|
79
|
+
throw compactionError("Copilot compaction returned an invalid response envelope");
|
|
80
|
+
}
|
|
81
|
+
if (Object.prototype.hasOwnProperty.call(generated, "status") && generated.status !== "completed") {
|
|
82
|
+
throw compactionError(`Copilot compaction returned status ${generated.status}`);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const compactionItems = generated.output.filter((item) => item?.type === "compaction");
|
|
86
|
+
if (!compactionItems.length) {
|
|
87
|
+
throw compactionError("Copilot compaction returned no compaction output item");
|
|
88
|
+
}
|
|
89
|
+
if (compactionItems.some((item) => (
|
|
90
|
+
typeof item.encrypted_content !== "string" || item.encrypted_content.length === 0
|
|
91
|
+
))) {
|
|
92
|
+
throw compactionError("Copilot compaction returned an invalid compaction output item");
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return {
|
|
96
|
+
...generated,
|
|
97
|
+
object: "response.compaction",
|
|
98
|
+
output: [
|
|
99
|
+
...retainedMessages(Array.isArray(inputItems) ? inputItems : []),
|
|
100
|
+
...compactionItems,
|
|
101
|
+
],
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export function parseResponsesCompactionResult(inputItems, text) {
|
|
106
|
+
let generated;
|
|
107
|
+
try {
|
|
108
|
+
generated = JSON.parse(text);
|
|
109
|
+
} catch {
|
|
110
|
+
throw compactionError("Copilot compaction returned invalid JSON");
|
|
111
|
+
}
|
|
112
|
+
return createResponsesCompactionResult(inputItems, generated);
|
|
113
|
+
}
|
|
@@ -15,10 +15,12 @@ import {
|
|
|
15
15
|
} from "./models.mjs";
|
|
16
16
|
import { chatToResponses, forwardToChat, responsesBodyUsesCustomTools } from "./responses-bridge.mjs";
|
|
17
17
|
import { prepareResponsesChatPayload } from "./responses-chat-payload.mjs";
|
|
18
|
+
import { prepareResponsesCompactionRequest } from "./responses-compaction.mjs";
|
|
18
19
|
import { proxyCopilotResponses } from "./responses-proxy.mjs";
|
|
19
20
|
import { prepareResponsesRequest, rememberResponseHistory } from "./responses-request.mjs";
|
|
20
21
|
import { status } from "./status.mjs";
|
|
21
22
|
import { endStreamWithError } from "./stream-errors.mjs";
|
|
23
|
+
import { safeUpstreamResponseHeaders } from "./upstream-headers.mjs";
|
|
22
24
|
import { recordResponsesUsage } from "./usage.mjs";
|
|
23
25
|
|
|
24
26
|
const RESPONSES_ONLY_FALLBACK = new Set([
|
|
@@ -113,20 +115,29 @@ export function createResponsesHandler(options) {
|
|
|
113
115
|
});
|
|
114
116
|
const { chatReq, bodyText } = chatPayload;
|
|
115
117
|
if (parsed.stream) {
|
|
118
|
+
let streamResponseHeaders = null;
|
|
116
119
|
await forwardToChat(chatReq, async (event, data) => {
|
|
117
|
-
if (!res.headersSent) res.writeHead(200, {
|
|
120
|
+
if (!res.headersSent) res.writeHead(200, {
|
|
121
|
+
...streamResponseHeaders,
|
|
122
|
+
"Cache-Control": "no-cache",
|
|
123
|
+
Connection: "keep-alive",
|
|
124
|
+
});
|
|
118
125
|
await writeOrDrain(res, `event: ${event}\ndata: ${JSON.stringify(data)}\n\n`);
|
|
119
126
|
if (event === "response.completed") {
|
|
120
127
|
rememberResponseHistory(prepared, data.response);
|
|
121
128
|
recordResponsesUsage({ surface: prepared.surface, mode: "stream", model, response: data.response, event: data });
|
|
122
129
|
}
|
|
123
|
-
}, () => { if (!res.writableEnded) res.end(); }, async (statusCode, errMsg) => {
|
|
130
|
+
}, () => { if (!res.writableEnded) res.end(); }, async (statusCode, errMsg, error, upstreamResponse) => {
|
|
124
131
|
if (!res.headersSent) {
|
|
125
|
-
|
|
126
|
-
|
|
132
|
+
if (upstreamResponse) {
|
|
133
|
+
sendUpstreamError(res, upstreamResponse, errMsg);
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
const responseError = error || Object.assign(new Error(errMsg), { statusCode });
|
|
137
|
+
sendJsonError(res, responseError, statusCode || 500);
|
|
127
138
|
return;
|
|
128
139
|
}
|
|
129
|
-
await endStreamWithError(res, "responses", new Error(errMsg), abort);
|
|
140
|
+
await endStreamWithError(res, "responses", error || new Error(errMsg), abort);
|
|
130
141
|
}, {
|
|
131
142
|
signal: abort.signal,
|
|
132
143
|
abort,
|
|
@@ -134,6 +145,11 @@ export function createResponsesHandler(options) {
|
|
|
134
145
|
chatCompletionsFn,
|
|
135
146
|
releaseRequest,
|
|
136
147
|
bodyText,
|
|
148
|
+
onUpstreamResponse: (upstreamResponse) => {
|
|
149
|
+
streamResponseHeaders = safeUpstreamResponseHeaders(upstreamResponse.headers, {
|
|
150
|
+
contentType: "text/event-stream",
|
|
151
|
+
});
|
|
152
|
+
},
|
|
137
153
|
});
|
|
138
154
|
} else {
|
|
139
155
|
try {
|
|
@@ -147,7 +163,9 @@ export function createResponsesHandler(options) {
|
|
|
147
163
|
const response = chatToResponses(JSON.parse(data), model);
|
|
148
164
|
rememberResponseHistory(prepared, response);
|
|
149
165
|
recordResponsesUsage({ surface: prepared.surface, mode: "json", model, response, event: response });
|
|
150
|
-
res.writeHead(200, {
|
|
166
|
+
res.writeHead(200, safeUpstreamResponseHeaders(upstream.headers, {
|
|
167
|
+
contentType: "application/json",
|
|
168
|
+
}));
|
|
151
169
|
res.end(JSON.stringify(response));
|
|
152
170
|
} catch (error) {
|
|
153
171
|
logRequestFailure("Responses", error, abort);
|
|
@@ -170,8 +188,6 @@ export function createResponsesCompactHandler(options) {
|
|
|
170
188
|
acquireRequest,
|
|
171
189
|
openAIModelEnv,
|
|
172
190
|
responsesCompactFn,
|
|
173
|
-
streamHandshakeTimeoutMs,
|
|
174
|
-
streamIdleTimeoutMs,
|
|
175
191
|
upstreamTimeoutMs,
|
|
176
192
|
} = options;
|
|
177
193
|
|
|
@@ -181,22 +197,20 @@ export function createResponsesCompactHandler(options) {
|
|
|
181
197
|
try {
|
|
182
198
|
releaseRequest = await acquireRequest(req, { signal: abort.signal });
|
|
183
199
|
const parsed = await readJsonBody(req);
|
|
184
|
-
abort.setTimeout(
|
|
185
|
-
|
|
186
|
-
parsed
|
|
200
|
+
abort.setTimeout(upstreamTimeoutMs, "upstream_timeout");
|
|
201
|
+
const prepared = prepareResponsesCompactionRequest(
|
|
202
|
+
prepareResponsesRequest(parsed, { mutate: true }),
|
|
187
203
|
);
|
|
188
|
-
const prepared = prepareResponsesRequest(parsed, { mutate: true });
|
|
189
204
|
prepared.surface = "responses_compact";
|
|
190
205
|
const model = parsed.model || "unknown";
|
|
191
206
|
const { requestedModel, upstreamModel } = resolveOpenAIModel(model, openAIModelEnv);
|
|
192
207
|
if (upstreamModel !== requestedModel) prepared.body.model = upstreamModel;
|
|
193
208
|
const upstreamLog = upstreamModel === requestedModel ? "" : ` upstream_model=${upstreamModel}`;
|
|
194
|
-
console.log(status("info", `responses compact model=${requestedModel}${upstreamLog} stream
|
|
209
|
+
console.log(status("info", `responses compact model=${requestedModel}${upstreamLog} stream=false`));
|
|
195
210
|
await proxyCopilotResponses(prepared, req, res, responsesCompactFn, {
|
|
196
211
|
signal: abort.signal,
|
|
197
212
|
abort,
|
|
198
213
|
releaseRequest,
|
|
199
|
-
streamIdleTimeoutMs,
|
|
200
214
|
});
|
|
201
215
|
} catch (error) {
|
|
202
216
|
logRequestFailure("Responses compact", error, abort);
|