codex-copilot-dx 0.4.32 → 0.4.33
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/package.json +3 -1
- package/scripts/benchmark.mjs +135 -0
- package/src/adapter.mjs +1 -1
- package/src/anthropic.mjs +9 -2
- package/src/copilot.mjs +56 -2
- package/src/image-optimization.mjs +95 -20
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "codex-copilot-dx",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.33",
|
|
4
4
|
"description": "Use Codex Desktop, Claude Code, and Claude App with GitHub Copilot",
|
|
5
5
|
"author": "Dale Xiao",
|
|
6
6
|
"type": "module",
|
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
"files": [
|
|
11
11
|
"bin/",
|
|
12
12
|
"src/",
|
|
13
|
+
"scripts/benchmark.mjs",
|
|
13
14
|
"README.md"
|
|
14
15
|
],
|
|
15
16
|
"keywords": [
|
|
@@ -41,6 +42,7 @@
|
|
|
41
42
|
"test": "node --test",
|
|
42
43
|
"test:smoke": "node scripts/offline-smoke.mjs",
|
|
43
44
|
"test:e2e": "bash test/e2e.sh",
|
|
45
|
+
"bench": "node scripts/benchmark.mjs",
|
|
44
46
|
"pack:check": "npm pack --dry-run --json",
|
|
45
47
|
"verify": "npm test && npm run test:smoke && npm run pack:check",
|
|
46
48
|
"prepublishOnly": "npm run verify"
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { spawnSync } from "node:child_process";
|
|
4
|
+
import { setTimeout as delay } from "node:timers/promises";
|
|
5
|
+
import { fileURLToPath } from "node:url";
|
|
6
|
+
import sharp from "sharp";
|
|
7
|
+
import { optimizeImagesInBody, prepareResponsesPayload } from "../src/image-optimization.mjs";
|
|
8
|
+
|
|
9
|
+
const packageRoot = fileURLToPath(new URL("../", import.meta.url));
|
|
10
|
+
|
|
11
|
+
function runProbe(source) {
|
|
12
|
+
const result = spawnSync(process.execPath, ["--expose-gc", "--input-type=module", "-e", source], {
|
|
13
|
+
cwd: packageRoot,
|
|
14
|
+
encoding: "utf8",
|
|
15
|
+
});
|
|
16
|
+
if (result.status !== 0) {
|
|
17
|
+
throw new Error(result.stderr.trim() || `benchmark probe exited with ${result.status}`);
|
|
18
|
+
}
|
|
19
|
+
return JSON.parse(result.stdout.trim());
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function deterministicPixels(byteLength, initialSeed) {
|
|
23
|
+
const pixels = Buffer.alloc(byteLength);
|
|
24
|
+
let seed = initialSeed;
|
|
25
|
+
for (let index = 0; index < pixels.length; index += 1) {
|
|
26
|
+
seed = ((seed * 1664525) + 1013904223) >>> 0;
|
|
27
|
+
pixels[index] = seed >>> 24;
|
|
28
|
+
}
|
|
29
|
+
return pixels;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const adapterImport = runProbe(`
|
|
33
|
+
const started = performance.now();
|
|
34
|
+
await import("./src/adapter.mjs");
|
|
35
|
+
globalThis.gc?.();
|
|
36
|
+
const memory = process.memoryUsage();
|
|
37
|
+
console.log(JSON.stringify({
|
|
38
|
+
elapsed_ms: +(performance.now() - started).toFixed(1),
|
|
39
|
+
rss_mib: +(memory.rss / 1048576).toFixed(1),
|
|
40
|
+
heap_used_mib: +(memory.heapUsed / 1048576).toFixed(1),
|
|
41
|
+
}));
|
|
42
|
+
`);
|
|
43
|
+
|
|
44
|
+
const tokenCount = runProbe(`
|
|
45
|
+
const importStarted = performance.now();
|
|
46
|
+
const { countTokens } = await import("./src/anthropic.mjs");
|
|
47
|
+
const importMs = performance.now() - importStarted;
|
|
48
|
+
const body = { messages: [{ role: "user", content: "a ".repeat(50000) }] };
|
|
49
|
+
const firstStarted = performance.now();
|
|
50
|
+
const first = await countTokens(body);
|
|
51
|
+
const firstMs = performance.now() - firstStarted;
|
|
52
|
+
const warmStarted = performance.now();
|
|
53
|
+
const warm = await countTokens(body);
|
|
54
|
+
const warmMs = performance.now() - warmStarted;
|
|
55
|
+
globalThis.gc?.();
|
|
56
|
+
const memory = process.memoryUsage();
|
|
57
|
+
console.log(JSON.stringify({
|
|
58
|
+
module_import_ms: +importMs.toFixed(1),
|
|
59
|
+
first_call_ms: +firstMs.toFixed(1),
|
|
60
|
+
warm_call_ms: +warmMs.toFixed(1),
|
|
61
|
+
input_tokens: first.input_tokens,
|
|
62
|
+
deterministic: first.input_tokens === warm.input_tokens,
|
|
63
|
+
rss_mib: +(memory.rss / 1048576).toFixed(1),
|
|
64
|
+
}));
|
|
65
|
+
`);
|
|
66
|
+
|
|
67
|
+
const repeatedImage = "data:image/png;base64,QUJDRA==";
|
|
68
|
+
const duplicateBody = {
|
|
69
|
+
input: [{
|
|
70
|
+
type: "message",
|
|
71
|
+
role: "user",
|
|
72
|
+
content: Array.from({ length: 8 }, () => ({ type: "input_image", image_url: repeatedImage })),
|
|
73
|
+
}],
|
|
74
|
+
};
|
|
75
|
+
let duplicateCalls = 0;
|
|
76
|
+
const duplicateStarted = performance.now();
|
|
77
|
+
await optimizeImagesInBody(duplicateBody, {
|
|
78
|
+
concurrency: 2,
|
|
79
|
+
optimizeImage: async (value) => {
|
|
80
|
+
duplicateCalls += 1;
|
|
81
|
+
await delay(5);
|
|
82
|
+
return value;
|
|
83
|
+
},
|
|
84
|
+
});
|
|
85
|
+
const duplicateImages = {
|
|
86
|
+
elapsed_ms: +(performance.now() - duplicateStarted).toFixed(1),
|
|
87
|
+
image_occurrences: 8,
|
|
88
|
+
optimizer_calls: duplicateCalls,
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
const pixels = deterministicPixels(1024 * 1024 * 3, 0x87654321);
|
|
92
|
+
const webp = await sharp(pixels, { raw: { width: 1024, height: 1024, channels: 3 } })
|
|
93
|
+
.webp({ quality: 100 })
|
|
94
|
+
.toBuffer();
|
|
95
|
+
const payloadBody = {
|
|
96
|
+
input: [{
|
|
97
|
+
type: "message",
|
|
98
|
+
role: "user",
|
|
99
|
+
content: [{ type: "input_image", image_url: `data:image/webp;base64,${webp.toString("base64")}` }],
|
|
100
|
+
}],
|
|
101
|
+
};
|
|
102
|
+
const originalLog = console.log;
|
|
103
|
+
const originalWarn = console.warn;
|
|
104
|
+
let oversizedPayload;
|
|
105
|
+
let profilePasses = 0;
|
|
106
|
+
try {
|
|
107
|
+
console.log = () => {};
|
|
108
|
+
console.warn = () => { profilePasses += 1; };
|
|
109
|
+
const started = performance.now();
|
|
110
|
+
const result = await prepareResponsesPayload(payloadBody, {
|
|
111
|
+
maxBytes: 300000,
|
|
112
|
+
profiles: [
|
|
113
|
+
{ maxDim: 800, quality: 75 },
|
|
114
|
+
{ maxDim: 640, quality: 65 },
|
|
115
|
+
],
|
|
116
|
+
});
|
|
117
|
+
oversizedPayload = {
|
|
118
|
+
elapsed_ms: +(performance.now() - started).toFixed(1),
|
|
119
|
+
original_image_bytes: webp.length,
|
|
120
|
+
output_body_bytes: result.bodyBytes,
|
|
121
|
+
profile_passes: profilePasses,
|
|
122
|
+
adapted: result.adapted,
|
|
123
|
+
};
|
|
124
|
+
} finally {
|
|
125
|
+
console.log = originalLog;
|
|
126
|
+
console.warn = originalWarn;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
console.log(JSON.stringify({
|
|
130
|
+
note: "Report-only benchmark; timings vary by machine and are not pass/fail thresholds.",
|
|
131
|
+
adapter_import: adapterImport,
|
|
132
|
+
token_count_100kb: tokenCount,
|
|
133
|
+
duplicate_images: duplicateImages,
|
|
134
|
+
oversized_payload: oversizedPayload,
|
|
135
|
+
}, null, 2));
|
package/src/adapter.mjs
CHANGED
|
@@ -890,7 +890,7 @@ export function createAdapterHandler(options = {}) {
|
|
|
890
890
|
try {
|
|
891
891
|
const parsed = await readJsonBody(req);
|
|
892
892
|
res.writeHead(200, { "Content-Type": "application/json" });
|
|
893
|
-
res.end(JSON.stringify(countTokens(parsed)));
|
|
893
|
+
res.end(JSON.stringify(await countTokens(parsed)));
|
|
894
894
|
} catch (e) {
|
|
895
895
|
sendJsonError(res, e);
|
|
896
896
|
} finally {
|
package/src/anthropic.mjs
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
|
-
|
|
2
|
+
|
|
3
|
+
let tokenizerModulePromise = null;
|
|
4
|
+
|
|
5
|
+
async function tokenEncoder() {
|
|
6
|
+
tokenizerModulePromise ||= import("gpt-tokenizer");
|
|
7
|
+
return (await tokenizerModulePromise).encode;
|
|
8
|
+
}
|
|
3
9
|
|
|
4
10
|
// Anthropic Messages API to OpenAI chat/completions translation.
|
|
5
11
|
// Pure functions only; network calls stay in adapter.mjs and copilot.mjs.
|
|
@@ -252,7 +258,7 @@ export async function streamAnthropicFromLines(lineIterator, emit, model, option
|
|
|
252
258
|
}
|
|
253
259
|
|
|
254
260
|
// Copilot has no count_tokens endpoint; estimate locally with gpt-tokenizer.
|
|
255
|
-
export function countTokens(body) {
|
|
261
|
+
export async function countTokens(body) {
|
|
256
262
|
const parts = [];
|
|
257
263
|
const sys = systemToText(body.system);
|
|
258
264
|
if (sys) parts.push(sys);
|
|
@@ -276,5 +282,6 @@ export function countTokens(body) {
|
|
|
276
282
|
}
|
|
277
283
|
|
|
278
284
|
const text = parts.join("\n");
|
|
285
|
+
const encode = await tokenEncoder();
|
|
279
286
|
return { input_tokens: encode(text).length };
|
|
280
287
|
}
|
package/src/copilot.mjs
CHANGED
|
@@ -9,6 +9,7 @@ import { prepareResponsesPayload } from "./image-optimization.mjs";
|
|
|
9
9
|
export {
|
|
10
10
|
optimizeImageDataUrl,
|
|
11
11
|
optimizeImagesInBody,
|
|
12
|
+
createTaskLimiter,
|
|
12
13
|
parseImageConcurrency,
|
|
13
14
|
prepareResponsesPayload,
|
|
14
15
|
runWithConcurrency,
|
|
@@ -266,6 +267,7 @@ export async function refreshVSCodeVersion() {
|
|
|
266
267
|
let copilotToken = null;
|
|
267
268
|
let copilotTokenExpiry = 0;
|
|
268
269
|
let copilotTokenRefresh = null;
|
|
270
|
+
const modelListFlights = new Map();
|
|
269
271
|
|
|
270
272
|
function getGithubToken({ home = os.homedir() } = {}) {
|
|
271
273
|
const GITHUB_TOKEN_PATH = githubTokenPath(home);
|
|
@@ -343,6 +345,7 @@ export function resetCopilotTokenForTests() {
|
|
|
343
345
|
copilotTokenExpiry = 0;
|
|
344
346
|
copilotTokenRefresh = null;
|
|
345
347
|
apiBase = DEFAULT_API_BASE;
|
|
348
|
+
resetModelListSingleflightForTests();
|
|
346
349
|
}
|
|
347
350
|
|
|
348
351
|
// chatReq is already converted by the adapter. The caller parses the raw Response.
|
|
@@ -363,7 +366,7 @@ export async function chatCompletions(chatReq, { signal, fetchImpl, retryOptions
|
|
|
363
366
|
}, { fetchImpl, ...retryOptions });
|
|
364
367
|
}
|
|
365
368
|
|
|
366
|
-
|
|
369
|
+
async function fetchModels({ signal, fetchImpl, retryOptions }) {
|
|
367
370
|
const token = await getCopilotToken({ signal });
|
|
368
371
|
const headers = buildHeaders({
|
|
369
372
|
token, version: getVSCodeVersion(), initiator: "user", vision: false,
|
|
@@ -376,10 +379,61 @@ export async function listModels({ signal, fetchImpl, retryOptions } = {}) {
|
|
|
376
379
|
return { status: resp.status, body };
|
|
377
380
|
}
|
|
378
381
|
|
|
382
|
+
function waitForModelListFlight(flight, signal) {
|
|
383
|
+
if (signal?.aborted) return Promise.reject(abortError(signal));
|
|
384
|
+
flight.waiters += 1;
|
|
385
|
+
|
|
386
|
+
return new Promise((resolve, reject) => {
|
|
387
|
+
let finished = false;
|
|
388
|
+
const finish = (handler, value) => {
|
|
389
|
+
if (finished) return;
|
|
390
|
+
finished = true;
|
|
391
|
+
signal?.removeEventListener("abort", onAbort);
|
|
392
|
+
flight.waiters -= 1;
|
|
393
|
+
if (!flight.settled && flight.waiters === 0) flight.controller.abort();
|
|
394
|
+
handler(value);
|
|
395
|
+
};
|
|
396
|
+
const onAbort = () => finish(reject, abortError(signal));
|
|
397
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
398
|
+
if (signal?.aborted) onAbort();
|
|
399
|
+
flight.promise.then(
|
|
400
|
+
(value) => finish(resolve, value),
|
|
401
|
+
(error) => finish(reject, error),
|
|
402
|
+
);
|
|
403
|
+
});
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
export function listModels({ signal, fetchImpl = fetch, retryOptions } = {}) {
|
|
407
|
+
if (signal?.aborted) return Promise.reject(abortError(signal));
|
|
408
|
+
let flight = modelListFlights.get(fetchImpl);
|
|
409
|
+
if (flight?.controller.signal.aborted) {
|
|
410
|
+
modelListFlights.delete(fetchImpl);
|
|
411
|
+
flight = null;
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
if (!flight) {
|
|
415
|
+
const controller = new AbortController();
|
|
416
|
+
flight = { controller, waiters: 0, settled: false, promise: null };
|
|
417
|
+
flight.promise = fetchModels({ signal: controller.signal, fetchImpl, retryOptions })
|
|
418
|
+
.finally(() => {
|
|
419
|
+
flight.settled = true;
|
|
420
|
+
if (modelListFlights.get(fetchImpl) === flight) modelListFlights.delete(fetchImpl);
|
|
421
|
+
});
|
|
422
|
+
modelListFlights.set(fetchImpl, flight);
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
return waitForModelListFlight(flight, signal);
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
export function resetModelListSingleflightForTests() {
|
|
429
|
+
for (const flight of modelListFlights.values()) flight.controller.abort();
|
|
430
|
+
modelListFlights.clear();
|
|
431
|
+
}
|
|
432
|
+
|
|
379
433
|
// Responses-only models go directly to Copilot's /responses endpoint.
|
|
380
434
|
export async function responses(reqBody, { signal, fetchImpl, retryOptions } = {}) {
|
|
381
435
|
const token = await getCopilotToken({ signal });
|
|
382
|
-
const { bodyText, bodyBytes, summary } = await prepareResponsesPayload(reqBody);
|
|
436
|
+
const { bodyText, bodyBytes, summary } = await prepareResponsesPayload(reqBody, { signal });
|
|
383
437
|
console.log(status("info", `responses payload bytes=${bodyBytes} input_items=${summary.items} images=${summary.images}`));
|
|
384
438
|
const headers = buildHeaders({
|
|
385
439
|
token,
|
|
@@ -35,6 +35,66 @@ export function parseImageConcurrency(value) {
|
|
|
35
35
|
return Number.isFinite(n) && n > 0 ? Math.min(n, IMG_MAX_CONCURRENCY) : DEFAULT_IMG_CONCURRENCY;
|
|
36
36
|
}
|
|
37
37
|
|
|
38
|
+
function abortError(signal) {
|
|
39
|
+
if (signal?.reason instanceof Error) return signal.reason;
|
|
40
|
+
const error = new Error("The operation was aborted");
|
|
41
|
+
error.name = "AbortError";
|
|
42
|
+
return error;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function createTaskLimiter(concurrency) {
|
|
46
|
+
const limit = parseImageConcurrency(concurrency);
|
|
47
|
+
const queue = [];
|
|
48
|
+
let active = 0;
|
|
49
|
+
|
|
50
|
+
const drain = () => {
|
|
51
|
+
while (active < limit && queue.length) {
|
|
52
|
+
const entry = queue.shift();
|
|
53
|
+
if (entry.cancelled) continue;
|
|
54
|
+
entry.cleanup();
|
|
55
|
+
if (entry.signal?.aborted) {
|
|
56
|
+
entry.reject(abortError(entry.signal));
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
59
|
+
active += 1;
|
|
60
|
+
entry.resolve();
|
|
61
|
+
}
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
return async function runLimited(task, { signal } = {}) {
|
|
65
|
+
if (signal?.aborted) throw abortError(signal);
|
|
66
|
+
await new Promise((resolve, reject) => {
|
|
67
|
+
const entry = {
|
|
68
|
+
cancelled: false,
|
|
69
|
+
cleanup: () => signal?.removeEventListener("abort", onAbort),
|
|
70
|
+
reject,
|
|
71
|
+
resolve,
|
|
72
|
+
signal,
|
|
73
|
+
};
|
|
74
|
+
const onAbort = () => {
|
|
75
|
+
entry.cancelled = true;
|
|
76
|
+
entry.cleanup();
|
|
77
|
+
reject(abortError(signal));
|
|
78
|
+
drain();
|
|
79
|
+
};
|
|
80
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
81
|
+
queue.push(entry);
|
|
82
|
+
if (signal?.aborted) onAbort();
|
|
83
|
+
else drain();
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
try {
|
|
87
|
+
if (signal?.aborted) throw abortError(signal);
|
|
88
|
+
return await task();
|
|
89
|
+
} finally {
|
|
90
|
+
active -= 1;
|
|
91
|
+
drain();
|
|
92
|
+
}
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const runGlobalImageTask = createTaskLimiter(IMG_CONCURRENCY);
|
|
97
|
+
|
|
38
98
|
async function sharp() {
|
|
39
99
|
sharpImport ||= import("sharp");
|
|
40
100
|
const mod = await sharpImport;
|
|
@@ -63,6 +123,7 @@ export async function optimizeImageDataUrl(dataUrl, {
|
|
|
63
123
|
maxDim = IMG_MAX_DIM,
|
|
64
124
|
quality = IMG_QUALITY,
|
|
65
125
|
force = false,
|
|
126
|
+
signal,
|
|
66
127
|
} = {}) {
|
|
67
128
|
if (IMG_OPT_DISABLED) return dataUrl;
|
|
68
129
|
if (typeof dataUrl !== "string" || !dataUrl.startsWith("data:")) return dataUrl;
|
|
@@ -75,22 +136,26 @@ export async function optimizeImageDataUrl(dataUrl, {
|
|
|
75
136
|
if (!force && touchOptimizedImage(digest)) return dataUrl;
|
|
76
137
|
|
|
77
138
|
try {
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
139
|
+
return await runGlobalImageTask(async () => {
|
|
140
|
+
const resize = await sharp();
|
|
141
|
+
const image = resize(raw, { failOn: "none", limitInputPixels: IMG_MAX_INPUT_PIXELS });
|
|
142
|
+
const out = await image
|
|
143
|
+
.rotate()
|
|
144
|
+
.resize(positiveInt(maxDim, IMG_MAX_DIM), positiveInt(maxDim, IMG_MAX_DIM), { fit: "inside", withoutEnlargement: true })
|
|
145
|
+
.webp({ quality: positiveInt(quality, IMG_QUALITY, 100), effort: 4 })
|
|
146
|
+
.toBuffer();
|
|
147
|
+
if (signal?.aborted) throw abortError(signal);
|
|
148
|
+
if (out.length >= raw.length) {
|
|
149
|
+
rememberOptimizedImage(digest);
|
|
150
|
+
return dataUrl;
|
|
151
|
+
}
|
|
152
|
+
rememberOptimizedImage(imageDigest(out));
|
|
153
|
+
const ratio = ((out.length / raw.length) * 100).toFixed(1);
|
|
154
|
+
console.log(status("info", `image ${(raw.length / 1024).toFixed(0)}KB ${mime} -> ${(out.length / 1024).toFixed(0)}KB webp (${ratio}%)`));
|
|
155
|
+
return `data:image/webp;base64,${out.toString("base64")}`;
|
|
156
|
+
}, { signal });
|
|
93
157
|
} catch (e) {
|
|
158
|
+
if (signal?.aborted || e?.name === "AbortError") throw e;
|
|
94
159
|
console.warn(status("warn", `image optimize failed (${mime}, ${raw.length}b): ${e.message}`));
|
|
95
160
|
return dataUrl;
|
|
96
161
|
}
|
|
@@ -131,19 +196,27 @@ function visitImageParts(parts, tasks, optimizeImage) {
|
|
|
131
196
|
export async function optimizeImagesInBody(reqBody, {
|
|
132
197
|
concurrency = IMG_CONCURRENCY,
|
|
133
198
|
optimizeImage = optimizeImageDataUrl,
|
|
199
|
+
signal,
|
|
134
200
|
} = {}) {
|
|
135
201
|
if (IMG_OPT_DISABLED || !Array.isArray(reqBody.input)) return reqBody;
|
|
136
202
|
const tasks = [];
|
|
137
203
|
const finalize = [];
|
|
204
|
+
const optimizedByDataUrl = new Map();
|
|
205
|
+
const optimizeOnce = (dataUrl) => {
|
|
206
|
+
if (!optimizedByDataUrl.has(dataUrl)) {
|
|
207
|
+
optimizedByDataUrl.set(dataUrl, Promise.resolve().then(() => optimizeImage(dataUrl, { signal })));
|
|
208
|
+
}
|
|
209
|
+
return optimizedByDataUrl.get(dataUrl);
|
|
210
|
+
};
|
|
138
211
|
|
|
139
212
|
for (const item of reqBody.input) {
|
|
140
213
|
if (!item) continue;
|
|
141
214
|
if (item.type === "message" && Array.isArray(item.content)) {
|
|
142
|
-
visitImageParts(item.content, tasks,
|
|
215
|
+
visitImageParts(item.content, tasks, optimizeOnce);
|
|
143
216
|
}
|
|
144
217
|
if (item.type === "function_call_output") {
|
|
145
218
|
if (Array.isArray(item.output)) {
|
|
146
|
-
visitImageParts(item.output, tasks,
|
|
219
|
+
visitImageParts(item.output, tasks, optimizeOnce);
|
|
147
220
|
} else if (typeof item.output === "string") {
|
|
148
221
|
const trimmed = item.output.trim();
|
|
149
222
|
if (trimmed.startsWith("[")) {
|
|
@@ -151,7 +224,7 @@ export async function optimizeImagesInBody(reqBody, {
|
|
|
151
224
|
const parsed = JSON.parse(trimmed);
|
|
152
225
|
if (Array.isArray(parsed)) {
|
|
153
226
|
const taskCount = tasks.length;
|
|
154
|
-
visitImageParts(parsed, tasks,
|
|
227
|
+
visitImageParts(parsed, tasks, optimizeOnce);
|
|
155
228
|
if (tasks.length > taskCount) finalize.push(() => { item.output = JSON.stringify(parsed); });
|
|
156
229
|
}
|
|
157
230
|
} catch {
|
|
@@ -170,8 +243,9 @@ export async function optimizeImagesInBody(reqBody, {
|
|
|
170
243
|
export async function prepareResponsesPayload(reqBody, {
|
|
171
244
|
maxBytes = MAX_UPSTREAM_BODY_BYTES,
|
|
172
245
|
profiles = OVERSIZE_IMAGE_PROFILES,
|
|
246
|
+
signal,
|
|
173
247
|
} = {}) {
|
|
174
|
-
await optimizeImagesInBody(reqBody);
|
|
248
|
+
await optimizeImagesInBody(reqBody, { signal });
|
|
175
249
|
let bodyText = JSON.stringify(reqBody);
|
|
176
250
|
let bodyBytes = Buffer.byteLength(bodyText);
|
|
177
251
|
const summary = summarizeReqBody(reqBody);
|
|
@@ -183,7 +257,8 @@ export async function prepareResponsesPayload(reqBody, {
|
|
|
183
257
|
const beforeBytes = bodyBytes;
|
|
184
258
|
bodyText = "";
|
|
185
259
|
await optimizeImagesInBody(reqBody, {
|
|
186
|
-
optimizeImage: (dataUrl) => optimizeImageDataUrl(dataUrl, { ...profile, force: true }),
|
|
260
|
+
optimizeImage: (dataUrl) => optimizeImageDataUrl(dataUrl, { ...profile, force: true, signal }),
|
|
261
|
+
signal,
|
|
187
262
|
});
|
|
188
263
|
bodyText = JSON.stringify(reqBody);
|
|
189
264
|
bodyBytes = Buffer.byteLength(bodyText);
|