codex-copilot-dx 0.4.42 → 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 +28 -16
- package/bin/cli.mjs +4 -4
- package/package.json +2 -1
- package/src/adapter.mjs +17 -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 +42 -8
- package/src/doctor.mjs +11 -2
- package/src/http-transport.mjs +5 -2
- package/src/image-optimization.mjs +312 -116
- package/src/observability.mjs +2 -1
- package/src/responses-bridge.mjs +76 -20
- package/src/responses-byte-budget.mjs +314 -0
- package/src/responses-chat-payload.mjs +129 -0
- package/src/responses-compaction.mjs +113 -0
- package/src/responses-content.mjs +86 -0
- package/src/responses-handler.mjs +67 -20
- package/src/responses-image-limit.mjs +15 -26
- package/src/responses-proxy.mjs +123 -23
- package/src/responses-request.mjs +43 -6
- 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
|
@@ -1,15 +1,22 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
|
+
import {
|
|
3
|
+
canonicalInlineImageIdentity,
|
|
4
|
+
readResponsesImagePart,
|
|
5
|
+
readResponsesToolOutputParts,
|
|
6
|
+
} from "./responses-content.mjs";
|
|
7
|
+
import { createBoundedResultCache } from "./bounded-result-cache.mjs";
|
|
2
8
|
import { status } from "./status.mjs";
|
|
3
9
|
|
|
4
10
|
const DEFAULT_IMG_CONCURRENCY = 2;
|
|
5
11
|
const IMG_MAX_CONCURRENCY = 12;
|
|
6
12
|
const DEFAULT_IMG_MAX_INPUT_PIXELS = 40 * 1000 * 1000;
|
|
13
|
+
const DEFAULT_IMG_CACHE_MAX_BYTES = 64 * 1024 * 1024;
|
|
7
14
|
const DEFAULT_MAX_UPSTREAM_BODY_BYTES = 30 * 1024 * 1024;
|
|
8
|
-
const MAX_OPTIMIZED_IMAGE_DIGESTS = 4096;
|
|
9
15
|
const IMG_MAX_DIM = positiveInt(process.env.CCDX_IMG_MAX_DIM, 2048);
|
|
10
|
-
const IMG_QUALITY = positiveInt(process.env.CCDX_IMG_QUALITY,
|
|
16
|
+
const IMG_QUALITY = positiveInt(process.env.CCDX_IMG_QUALITY, 82, 100);
|
|
11
17
|
const IMG_MIN_BYTES = nonNegativeInt(process.env.CCDX_IMG_MIN_BYTES, 100000);
|
|
12
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);
|
|
13
20
|
const MAX_UPSTREAM_BODY_BYTES = positiveInt(process.env.CCDX_MAX_UPSTREAM_BODY_BYTES, DEFAULT_MAX_UPSTREAM_BODY_BYTES);
|
|
14
21
|
const IMG_OPT_DISABLED = process.env.CCDX_DISABLE_IMG_OPT === "1";
|
|
15
22
|
const IMG_CONCURRENCY = parseImageConcurrency(process.env.CCDX_IMG_CONCURRENCY);
|
|
@@ -17,7 +24,8 @@ const OVERSIZE_IMAGE_PROFILES = [
|
|
|
17
24
|
{ maxDim: Math.min(IMG_MAX_DIM, 1600), quality: Math.min(IMG_QUALITY, 75) },
|
|
18
25
|
{ maxDim: Math.min(IMG_MAX_DIM, 1280), quality: Math.min(IMG_QUALITY, 65) },
|
|
19
26
|
];
|
|
20
|
-
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 });
|
|
21
29
|
let sharpImport = null;
|
|
22
30
|
|
|
23
31
|
function positiveInt(value, fallback, max = Number.MAX_SAFE_INTEGER) {
|
|
@@ -30,6 +38,39 @@ function nonNegativeInt(value, fallback) {
|
|
|
30
38
|
return Number.isFinite(n) && n >= 0 ? n : fallback;
|
|
31
39
|
}
|
|
32
40
|
|
|
41
|
+
export function imageConstraintsForModel(modelId, { maxDim = IMG_MAX_DIM } = {}) {
|
|
42
|
+
const model = String(modelId || "").trim().toLowerCase();
|
|
43
|
+
const maxLongEdge = Math.min(positiveInt(maxDim, IMG_MAX_DIM), IMG_MAX_DIM, 2048);
|
|
44
|
+
const fullSizeGpt4 = /^(?:gpt-4o|gpt-4\.1)(?:$|-)/.test(model)
|
|
45
|
+
&& !/(?:^|-)(?:mini|nano)(?:$|-)/.test(model);
|
|
46
|
+
const gpt5Mini = /^gpt-5-mini(?:$|-)/.test(model);
|
|
47
|
+
|
|
48
|
+
if (fullSizeGpt4 || gpt5Mini) {
|
|
49
|
+
return { maxLongEdge, maxShortEdge: Math.min(maxLongEdge, 768) };
|
|
50
|
+
}
|
|
51
|
+
if (/^gpt-5\.(?:4|5|6)(?:$|-)/.test(model)) {
|
|
52
|
+
return { maxLongEdge, maxArea: 2_560_000 };
|
|
53
|
+
}
|
|
54
|
+
return { maxLongEdge };
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function fitImageDimensions(width, height, constraints = {}) {
|
|
58
|
+
const sourceWidth = Number(width);
|
|
59
|
+
const sourceHeight = Number(height);
|
|
60
|
+
if (!(sourceWidth > 0) || !(sourceHeight > 0)) return null;
|
|
61
|
+
|
|
62
|
+
const longEdge = Math.max(sourceWidth, sourceHeight);
|
|
63
|
+
const shortEdge = Math.min(sourceWidth, sourceHeight);
|
|
64
|
+
let scale = 1;
|
|
65
|
+
if (constraints.maxLongEdge > 0) scale = Math.min(scale, constraints.maxLongEdge / longEdge);
|
|
66
|
+
if (constraints.maxShortEdge > 0) scale = Math.min(scale, constraints.maxShortEdge / shortEdge);
|
|
67
|
+
if (constraints.maxArea > 0) scale = Math.min(scale, Math.sqrt(constraints.maxArea / (sourceWidth * sourceHeight)));
|
|
68
|
+
return {
|
|
69
|
+
width: Math.max(1, Math.floor(sourceWidth * scale)),
|
|
70
|
+
height: Math.max(1, Math.floor(sourceHeight * scale)),
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
|
|
33
74
|
export function parseImageConcurrency(value) {
|
|
34
75
|
const n = Number.parseInt(value, 10);
|
|
35
76
|
return Number.isFinite(n) && n > 0 ? Math.min(n, IMG_MAX_CONCURRENCY) : DEFAULT_IMG_CONCURRENCY;
|
|
@@ -102,42 +143,51 @@ export function createTaskLimiter(concurrency) {
|
|
|
102
143
|
const runGlobalImageTask = createTaskLimiter(IMG_CONCURRENCY);
|
|
103
144
|
|
|
104
145
|
export function imageOptimizationStats() {
|
|
146
|
+
const cache = imageResultCache.stats();
|
|
105
147
|
return {
|
|
106
148
|
...runGlobalImageTask.stats(),
|
|
107
149
|
disabled: IMG_OPT_DISABLED,
|
|
108
|
-
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,
|
|
109
157
|
sharp_loaded: sharpImport !== null,
|
|
110
158
|
};
|
|
111
159
|
}
|
|
112
160
|
|
|
161
|
+
export function resetImageOptimizationCacheForTests() {
|
|
162
|
+
imageResultCache.clear();
|
|
163
|
+
}
|
|
164
|
+
|
|
113
165
|
async function sharp() {
|
|
114
166
|
sharpImport ||= import("sharp");
|
|
115
167
|
const mod = await sharpImport;
|
|
116
168
|
return mod.default || mod;
|
|
117
169
|
}
|
|
118
170
|
|
|
119
|
-
function imageDigest(
|
|
120
|
-
return createHash("sha256").update(
|
|
171
|
+
function imageDigest(value, encoding) {
|
|
172
|
+
return createHash("sha256").update(value, encoding).digest("base64url");
|
|
121
173
|
}
|
|
122
174
|
|
|
123
|
-
function
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
optimizedImageDigests.delete(optimizedImageDigests.values().next().value);
|
|
134
|
-
}
|
|
175
|
+
function imageOptimizationKey(digest, mime, constraints, quality) {
|
|
176
|
+
return [
|
|
177
|
+
digest,
|
|
178
|
+
mime,
|
|
179
|
+
constraints.maxLongEdge || "",
|
|
180
|
+
constraints.maxShortEdge || "",
|
|
181
|
+
constraints.maxArea || "",
|
|
182
|
+
quality,
|
|
183
|
+
IMAGE_ENCODER_CONFIG,
|
|
184
|
+
].join(":");
|
|
135
185
|
}
|
|
136
186
|
|
|
137
187
|
export async function optimizeImageDataUrl(dataUrl, {
|
|
138
188
|
maxDim = IMG_MAX_DIM,
|
|
139
189
|
quality = IMG_QUALITY,
|
|
140
|
-
|
|
190
|
+
model,
|
|
141
191
|
signal,
|
|
142
192
|
} = {}) {
|
|
143
193
|
if (IMG_OPT_DISABLED) return dataUrl;
|
|
@@ -147,34 +197,66 @@ export async function optimizeImageDataUrl(dataUrl, {
|
|
|
147
197
|
const mime = match[1].toLowerCase();
|
|
148
198
|
const encoded = match[2];
|
|
149
199
|
const inputBytes = Buffer.byteLength(encoded, "base64");
|
|
150
|
-
if (
|
|
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);
|
|
151
208
|
|
|
152
209
|
try {
|
|
153
|
-
return await
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
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 });
|
|
175
255
|
} catch (e) {
|
|
176
256
|
if (signal?.aborted || e?.name === "AbortError") throw e;
|
|
177
|
-
|
|
257
|
+
if (inputBytes >= IMG_MIN_BYTES) {
|
|
258
|
+
console.warn(status("warn", `image optimize failed (${mime}, ${inputBytes}b): ${e.message}`));
|
|
259
|
+
}
|
|
178
260
|
return dataUrl;
|
|
179
261
|
}
|
|
180
262
|
}
|
|
@@ -191,102 +273,225 @@ export async function runWithConcurrency(taskFns, concurrency) {
|
|
|
191
273
|
}));
|
|
192
274
|
}
|
|
193
275
|
|
|
194
|
-
function visitImageParts(parts,
|
|
276
|
+
function visitImageParts(parts, references, commit = null) {
|
|
195
277
|
if (!Array.isArray(parts)) return;
|
|
196
278
|
for (const part of parts) {
|
|
197
|
-
|
|
198
|
-
if (
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
279
|
+
const image = readResponsesImagePart(part);
|
|
280
|
+
if (!image?.identity) continue;
|
|
281
|
+
references.push({
|
|
282
|
+
commit,
|
|
283
|
+
get: () => image.dataUrl,
|
|
284
|
+
set: (value) => image.setDataUrl(value),
|
|
285
|
+
});
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
function collectImageReferences(reqBody) {
|
|
290
|
+
const references = [];
|
|
291
|
+
const dirtyCommits = new Set();
|
|
292
|
+
if (!Array.isArray(reqBody?.input)) {
|
|
293
|
+
return { references, markDirty() {}, commitDirty() {} };
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
for (const item of reqBody.input) {
|
|
297
|
+
if (!item) continue;
|
|
298
|
+
visitImageParts([item], references);
|
|
299
|
+
if (item.type === "message" && Array.isArray(item.content)) {
|
|
300
|
+
visitImageParts(item.content, references);
|
|
301
|
+
}
|
|
302
|
+
const toolOutput = readResponsesToolOutputParts(item);
|
|
303
|
+
if (toolOutput) {
|
|
304
|
+
visitImageParts(toolOutput.parts, references, toolOutput.commit);
|
|
210
305
|
}
|
|
211
306
|
}
|
|
307
|
+
|
|
308
|
+
return {
|
|
309
|
+
references,
|
|
310
|
+
markDirty(changedReferences) {
|
|
311
|
+
for (const reference of changedReferences) {
|
|
312
|
+
if (reference.commit) dirtyCommits.add(reference.commit);
|
|
313
|
+
}
|
|
314
|
+
},
|
|
315
|
+
commitDirty() {
|
|
316
|
+
for (const apply of dirtyCommits) apply();
|
|
317
|
+
dirtyCommits.clear();
|
|
318
|
+
},
|
|
319
|
+
};
|
|
212
320
|
}
|
|
213
321
|
|
|
214
|
-
|
|
215
|
-
concurrency
|
|
216
|
-
|
|
322
|
+
async function optimizeImageReferences(collection, {
|
|
323
|
+
concurrency,
|
|
324
|
+
model,
|
|
325
|
+
optimizeImage,
|
|
217
326
|
signal,
|
|
218
|
-
}
|
|
219
|
-
if (IMG_OPT_DISABLED || !Array.isArray(reqBody.input)) return reqBody;
|
|
220
|
-
const tasks = [];
|
|
221
|
-
const finalize = [];
|
|
327
|
+
}) {
|
|
222
328
|
const optimizedByDataUrl = new Map();
|
|
223
329
|
const optimizeOnce = (dataUrl) => {
|
|
224
|
-
|
|
225
|
-
|
|
330
|
+
const identity = canonicalInlineImageIdentity(dataUrl) || dataUrl;
|
|
331
|
+
if (!optimizedByDataUrl.has(identity)) {
|
|
332
|
+
optimizedByDataUrl.set(identity, Promise.resolve().then(() => optimizeImage(dataUrl, { model, signal })));
|
|
226
333
|
}
|
|
227
|
-
return optimizedByDataUrl.get(
|
|
334
|
+
return optimizedByDataUrl.get(identity);
|
|
228
335
|
};
|
|
336
|
+
const tasks = collection.references.map((reference) => async () => {
|
|
337
|
+
if (reference.set(await optimizeOnce(reference.get()))) collection.markDirty([reference]);
|
|
338
|
+
});
|
|
339
|
+
if (tasks.length) await runWithConcurrency(tasks, concurrency);
|
|
340
|
+
collection.commitDirty();
|
|
341
|
+
}
|
|
229
342
|
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
} catch {
|
|
249
|
-
// Leave non-JSON tool output untouched.
|
|
250
|
-
}
|
|
251
|
-
}
|
|
252
|
-
}
|
|
343
|
+
function imageInputBytes(dataUrl) {
|
|
344
|
+
if (typeof dataUrl !== "string") return 0;
|
|
345
|
+
const match = /^data:image\/[a-z+.-]+;base64,(.+)$/i.exec(dataUrl);
|
|
346
|
+
return match ? Buffer.byteLength(match[1], "base64") : 0;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
function uniqueOriginalImages(references) {
|
|
350
|
+
const unique = new Map();
|
|
351
|
+
for (const reference of references) {
|
|
352
|
+
const dataUrl = reference.get();
|
|
353
|
+
const identity = canonicalInlineImageIdentity(dataUrl);
|
|
354
|
+
if (!identity) continue;
|
|
355
|
+
const inputBytes = imageInputBytes(dataUrl);
|
|
356
|
+
if (inputBytes <= 0) continue;
|
|
357
|
+
let image = unique.get(identity);
|
|
358
|
+
if (!image) {
|
|
359
|
+
image = { dataUrl, inputBytes, references: [] };
|
|
360
|
+
unique.set(identity, image);
|
|
253
361
|
}
|
|
362
|
+
image.references.push(reference);
|
|
363
|
+
}
|
|
364
|
+
return [...unique.values()];
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
function currentImageWireBytes(image) {
|
|
368
|
+
return image.references.reduce(
|
|
369
|
+
(total, reference) => total + Buffer.byteLength(reference.get()),
|
|
370
|
+
0,
|
|
371
|
+
);
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
function orderImagesByCurrentWireBytes(images) {
|
|
375
|
+
return [...images].sort((left, right) => (
|
|
376
|
+
currentImageWireBytes(right) - currentImageWireBytes(left)
|
|
377
|
+
|| right.inputBytes - left.inputBytes
|
|
378
|
+
));
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
function applyImageCandidate(collection, image, candidate, bodyBytes) {
|
|
382
|
+
if (!canonicalInlineImageIdentity(candidate)) {
|
|
383
|
+
return { bodyBytes, changed: false };
|
|
384
|
+
}
|
|
385
|
+
const previous = image.references.map((reference) => reference.get());
|
|
386
|
+
if (previous.every((value) => value === candidate)) return { bodyBytes, changed: false };
|
|
387
|
+
// Base64 data URLs need no JSON escaping, so their byte delta also applies to the serialized body.
|
|
388
|
+
const candidateBytes = Buffer.byteLength(candidate);
|
|
389
|
+
const byteDelta = previous.reduce(
|
|
390
|
+
(delta, value) => delta + candidateBytes - Buffer.byteLength(value),
|
|
391
|
+
0,
|
|
392
|
+
);
|
|
393
|
+
if (byteDelta >= 0) return { bodyBytes, changed: false };
|
|
394
|
+
const changedReferences = [];
|
|
395
|
+
for (const reference of image.references) {
|
|
396
|
+
if (reference.set(candidate)) changedReferences.push(reference);
|
|
254
397
|
}
|
|
398
|
+
collection.markDirty(changedReferences);
|
|
399
|
+
return { bodyBytes: bodyBytes + byteDelta, changed: true };
|
|
400
|
+
}
|
|
255
401
|
|
|
256
|
-
|
|
257
|
-
|
|
402
|
+
function normalizeCurrentInputStart(value, reqBody) {
|
|
403
|
+
const parsed = Number.parseInt(value, 10);
|
|
404
|
+
const itemCount = Array.isArray(reqBody?.input) ? reqBody.input.length : 0;
|
|
405
|
+
return Number.isFinite(parsed) && parsed >= 0 ? Math.min(parsed, itemCount) : 0;
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
export async function optimizeImagesInBody(reqBody, {
|
|
409
|
+
concurrency = IMG_CONCURRENCY,
|
|
410
|
+
optimizeImage = optimizeImageDataUrl,
|
|
411
|
+
signal,
|
|
412
|
+
} = {}) {
|
|
413
|
+
if (IMG_OPT_DISABLED || !Array.isArray(reqBody.input)) return reqBody;
|
|
414
|
+
const collection = collectImageReferences(reqBody);
|
|
415
|
+
await optimizeImageReferences(collection, {
|
|
416
|
+
concurrency,
|
|
417
|
+
model: reqBody.model,
|
|
418
|
+
optimizeImage,
|
|
419
|
+
signal,
|
|
420
|
+
});
|
|
258
421
|
return reqBody;
|
|
259
422
|
}
|
|
260
423
|
|
|
261
424
|
export async function prepareResponsesPayload(reqBody, {
|
|
425
|
+
currentInputStart = 0,
|
|
262
426
|
maxBytes = MAX_UPSTREAM_BODY_BYTES,
|
|
427
|
+
optimizeImage = optimizeImageDataUrl,
|
|
263
428
|
profiles = OVERSIZE_IMAGE_PROFILES,
|
|
429
|
+
skipInitialOptimization = false,
|
|
264
430
|
signal,
|
|
265
431
|
} = {}) {
|
|
266
|
-
|
|
432
|
+
const shouldOptimizeImages = !IMG_OPT_DISABLED && !skipInitialOptimization;
|
|
433
|
+
const collection = shouldOptimizeImages ? collectImageReferences(reqBody) : null;
|
|
434
|
+
const originalImages = collection ? uniqueOriginalImages(collection.references) : [];
|
|
435
|
+
if (collection) {
|
|
436
|
+
await optimizeImageReferences(collection, {
|
|
437
|
+
concurrency: IMG_CONCURRENCY,
|
|
438
|
+
model: reqBody.model,
|
|
439
|
+
optimizeImage: (dataUrl, options) => optimizeImage(dataUrl, { ...options, quality: IMG_QUALITY }),
|
|
440
|
+
signal,
|
|
441
|
+
});
|
|
442
|
+
}
|
|
267
443
|
let bodyText = JSON.stringify(reqBody);
|
|
268
444
|
let bodyBytes = Buffer.byteLength(bodyText);
|
|
269
445
|
const summary = summarizeReqBody(reqBody);
|
|
270
446
|
const targetBytes = positiveInt(maxBytes, MAX_UPSTREAM_BODY_BYTES);
|
|
271
447
|
let adapted = false;
|
|
448
|
+
let stage = skipInitialOptimization ? "preoptimized" : `q${IMG_QUALITY}`;
|
|
272
449
|
|
|
273
|
-
if (
|
|
274
|
-
|
|
450
|
+
if (collection && originalImages.length > 0 && bodyBytes > targetBytes) {
|
|
451
|
+
const batchSize = Math.max(1, IMG_CONCURRENCY);
|
|
452
|
+
for (const profile of Array.isArray(profiles) ? profiles : []) {
|
|
275
453
|
const beforeBytes = bodyBytes;
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
454
|
+
let processed = 0;
|
|
455
|
+
stage = `q${positiveInt(profile.quality, IMG_QUALITY, 100)}`;
|
|
456
|
+
const orderedImages = orderImagesByCurrentWireBytes(originalImages);
|
|
457
|
+
for (let index = 0; index < orderedImages.length && bodyBytes > targetBytes; index += batchSize) {
|
|
458
|
+
const batch = orderedImages.slice(index, index + batchSize);
|
|
459
|
+
const candidates = await Promise.all(batch.map((image) => optimizeImage(image.dataUrl, {
|
|
460
|
+
...profile,
|
|
461
|
+
force: true,
|
|
462
|
+
model: reqBody.model,
|
|
463
|
+
signal,
|
|
464
|
+
})));
|
|
465
|
+
for (let offset = 0; offset < batch.length && bodyBytes > targetBytes; offset += 1) {
|
|
466
|
+
processed += 1;
|
|
467
|
+
const applied = applyImageCandidate(collection, batch[offset], candidates[offset], bodyBytes);
|
|
468
|
+
bodyBytes = applied.bodyBytes;
|
|
469
|
+
adapted ||= applied.changed;
|
|
470
|
+
if (bodyBytes <= targetBytes) {
|
|
471
|
+
collection.commitDirty();
|
|
472
|
+
bodyText = JSON.stringify(reqBody);
|
|
473
|
+
bodyBytes = Buffer.byteLength(bodyText);
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
collection.commitDirty();
|
|
281
478
|
bodyText = JSON.stringify(reqBody);
|
|
282
479
|
bodyBytes = Buffer.byteLength(bodyText);
|
|
283
|
-
|
|
284
|
-
console.warn(status("warn", `responses payload ${beforeBytes}b exceeds ${targetBytes}b; image profile max_dim=${profile.maxDim} quality=${profile.quality} -> ${bodyBytes}b`));
|
|
480
|
+
console.warn(status("warn", `responses payload ${beforeBytes}b exceeds ${targetBytes}b; image profile max_dim=${profile.maxDim} quality=${profile.quality} processed=${processed}/${orderedImages.length} -> ${bodyBytes}b`));
|
|
285
481
|
if (bodyBytes <= targetBytes) break;
|
|
286
482
|
}
|
|
287
483
|
}
|
|
288
484
|
|
|
289
|
-
return {
|
|
485
|
+
return {
|
|
486
|
+
bodyText,
|
|
487
|
+
bodyBytes,
|
|
488
|
+
summary,
|
|
489
|
+
adapted,
|
|
490
|
+
stage,
|
|
491
|
+
targetBytes,
|
|
492
|
+
overBudget: bodyBytes > targetBytes,
|
|
493
|
+
currentInputStart: normalizeCurrentInputStart(currentInputStart, reqBody),
|
|
494
|
+
};
|
|
290
495
|
}
|
|
291
496
|
|
|
292
497
|
export function summarizeReqBody(reqBody) {
|
|
@@ -297,24 +502,15 @@ export function summarizeReqBody(reqBody) {
|
|
|
297
502
|
const countImages = (parts) => {
|
|
298
503
|
if (!Array.isArray(parts)) return;
|
|
299
504
|
for (const part of parts) {
|
|
300
|
-
if (
|
|
505
|
+
if (["input_image", "image", "image_url"].includes(part?.type)) images += 1;
|
|
301
506
|
}
|
|
302
507
|
};
|
|
303
508
|
|
|
304
509
|
for (const item of input) {
|
|
510
|
+
if (["input_image", "image", "image_url"].includes(item?.type)) countImages([item]);
|
|
305
511
|
if (item?.type === "message") countImages(item.content);
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
countImages(item.output);
|
|
309
|
-
} else if (typeof item.output === "string" && item.output.trim().startsWith("[")) {
|
|
310
|
-
try {
|
|
311
|
-
const parsed = JSON.parse(item.output);
|
|
312
|
-
if (Array.isArray(parsed)) countImages(parsed);
|
|
313
|
-
} catch {
|
|
314
|
-
// Ignore non-JSON tool output.
|
|
315
|
-
}
|
|
316
|
-
}
|
|
317
|
-
}
|
|
512
|
+
const toolOutput = readResponsesToolOutputParts(item);
|
|
513
|
+
if (toolOutput) countImages(toolOutput.parts);
|
|
318
514
|
}
|
|
319
515
|
|
|
320
516
|
return { items: input.length, images };
|
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(),
|