codex-copilot-dx 0.4.29 → 0.4.30
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 +1 -0
- package/package.json +1 -1
- package/src/copilot.mjs +3 -11
- package/src/image-optimization.mjs +70 -11
package/README.md
CHANGED
|
@@ -102,6 +102,7 @@ Environment variables:
|
|
|
102
102
|
| `CCDX_ALLOW_LAN` | unset | Set to `1` to allow non-loopback `ADAPTER_HOST` values such as `0.0.0.0`; exposes your Copilot-backed adapter beyond this machine |
|
|
103
103
|
| `CCDX_MAX_BODY_BYTES` | `67108864` | Maximum compressed/raw request body size |
|
|
104
104
|
| `CCDX_MAX_DECODED_BODY_BYTES` | `134217728` | Maximum decoded request body size after decompression |
|
|
105
|
+
| `CCDX_MAX_UPSTREAM_BODY_BYTES` | `31457280` | Responses body target; larger image payloads are compressed more aggressively before forwarding |
|
|
105
106
|
| `CCDX_MAX_SSE_BUFFER_BYTES` | `8388608` | Maximum buffered bytes for one unterminated upstream SSE line/event |
|
|
106
107
|
| `CCDX_UPSTREAM_TIMEOUT_MS` | `120000` | Timeout for non-streaming upstream Copilot requests |
|
|
107
108
|
| `CCDX_STREAM_HANDSHAKE_TIMEOUT_MS` | `120000` | Timeout while waiting for upstream streaming response headers |
|
package/package.json
CHANGED
package/src/copilot.mjs
CHANGED
|
@@ -4,18 +4,13 @@ import { randomUUID } from "node:crypto";
|
|
|
4
4
|
import { status } from "./status.mjs";
|
|
5
5
|
import { debugLog } from "./log.mjs";
|
|
6
6
|
import { ensureGithubTokenMetadata, githubReauthMessage, githubTokenPath, importDiscoveredGithubToken } from "./auth.mjs";
|
|
7
|
-
import {
|
|
8
|
-
optimizeImageDataUrl,
|
|
9
|
-
optimizeImagesInBody,
|
|
10
|
-
parseImageConcurrency,
|
|
11
|
-
runWithConcurrency,
|
|
12
|
-
summarizeReqBody,
|
|
13
|
-
} from "./image-optimization.mjs";
|
|
7
|
+
import { prepareResponsesPayload } from "./image-optimization.mjs";
|
|
14
8
|
|
|
15
9
|
export {
|
|
16
10
|
optimizeImageDataUrl,
|
|
17
11
|
optimizeImagesInBody,
|
|
18
12
|
parseImageConcurrency,
|
|
13
|
+
prepareResponsesPayload,
|
|
19
14
|
runWithConcurrency,
|
|
20
15
|
summarizeReqBody,
|
|
21
16
|
} from "./image-optimization.mjs";
|
|
@@ -384,10 +379,7 @@ export async function listModels({ signal, fetchImpl, retryOptions } = {}) {
|
|
|
384
379
|
// Responses-only models go directly to Copilot's /responses endpoint.
|
|
385
380
|
export async function responses(reqBody, { signal, fetchImpl, retryOptions } = {}) {
|
|
386
381
|
const token = await getCopilotToken({ signal });
|
|
387
|
-
await
|
|
388
|
-
const bodyText = JSON.stringify(reqBody);
|
|
389
|
-
const bodyBytes = Buffer.byteLength(bodyText);
|
|
390
|
-
const summary = summarizeReqBody(reqBody);
|
|
382
|
+
const { bodyText, bodyBytes, summary } = await prepareResponsesPayload(reqBody);
|
|
391
383
|
console.log(status("info", `responses payload bytes=${bodyBytes} input_items=${summary.items} images=${summary.images}`));
|
|
392
384
|
const headers = buildHeaders({
|
|
393
385
|
token,
|
|
@@ -1,14 +1,23 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
1
2
|
import { status } from "./status.mjs";
|
|
2
3
|
|
|
3
4
|
const DEFAULT_IMG_CONCURRENCY = 2;
|
|
4
5
|
const IMG_MAX_CONCURRENCY = 12;
|
|
5
6
|
const DEFAULT_IMG_MAX_INPUT_PIXELS = 40 * 1000 * 1000;
|
|
7
|
+
const DEFAULT_MAX_UPSTREAM_BODY_BYTES = 30 * 1024 * 1024;
|
|
8
|
+
const MAX_OPTIMIZED_IMAGE_DIGESTS = 4096;
|
|
6
9
|
const IMG_MAX_DIM = positiveInt(process.env.CCDX_IMG_MAX_DIM, 2048);
|
|
7
10
|
const IMG_QUALITY = positiveInt(process.env.CCDX_IMG_QUALITY, 85, 100);
|
|
8
11
|
const IMG_MIN_BYTES = nonNegativeInt(process.env.CCDX_IMG_MIN_BYTES, 100000);
|
|
9
12
|
const IMG_MAX_INPUT_PIXELS = positiveInt(process.env.CCDX_IMG_MAX_INPUT_PIXELS, DEFAULT_IMG_MAX_INPUT_PIXELS);
|
|
13
|
+
const MAX_UPSTREAM_BODY_BYTES = positiveInt(process.env.CCDX_MAX_UPSTREAM_BODY_BYTES, DEFAULT_MAX_UPSTREAM_BODY_BYTES);
|
|
10
14
|
const IMG_OPT_DISABLED = process.env.CCDX_DISABLE_IMG_OPT === "1";
|
|
11
15
|
const IMG_CONCURRENCY = parseImageConcurrency(process.env.CCDX_IMG_CONCURRENCY);
|
|
16
|
+
const OVERSIZE_IMAGE_PROFILES = [
|
|
17
|
+
{ maxDim: Math.min(IMG_MAX_DIM, 1600), quality: Math.min(IMG_QUALITY, 75) },
|
|
18
|
+
{ maxDim: Math.min(IMG_MAX_DIM, 1280), quality: Math.min(IMG_QUALITY, 65) },
|
|
19
|
+
];
|
|
20
|
+
const optimizedImageDigests = new Set();
|
|
12
21
|
let sharpImport = null;
|
|
13
22
|
|
|
14
23
|
function positiveInt(value, fallback, max = Number.MAX_SAFE_INTEGER) {
|
|
@@ -32,7 +41,29 @@ async function sharp() {
|
|
|
32
41
|
return mod.default || mod;
|
|
33
42
|
}
|
|
34
43
|
|
|
35
|
-
|
|
44
|
+
function imageDigest(raw) {
|
|
45
|
+
return createHash("sha256").update(raw).digest("base64url");
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function touchOptimizedImage(digest) {
|
|
49
|
+
if (!optimizedImageDigests.delete(digest)) return false;
|
|
50
|
+
optimizedImageDigests.add(digest);
|
|
51
|
+
return true;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function rememberOptimizedImage(digest) {
|
|
55
|
+
optimizedImageDigests.delete(digest);
|
|
56
|
+
optimizedImageDigests.add(digest);
|
|
57
|
+
if (optimizedImageDigests.size > MAX_OPTIMIZED_IMAGE_DIGESTS) {
|
|
58
|
+
optimizedImageDigests.delete(optimizedImageDigests.values().next().value);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export async function optimizeImageDataUrl(dataUrl, {
|
|
63
|
+
maxDim = IMG_MAX_DIM,
|
|
64
|
+
quality = IMG_QUALITY,
|
|
65
|
+
force = false,
|
|
66
|
+
} = {}) {
|
|
36
67
|
if (IMG_OPT_DISABLED) return dataUrl;
|
|
37
68
|
if (typeof dataUrl !== "string" || !dataUrl.startsWith("data:")) return dataUrl;
|
|
38
69
|
const match = /^data:(image\/[a-z+.-]+);base64,(.+)$/i.exec(dataUrl);
|
|
@@ -40,23 +71,22 @@ export async function optimizeImageDataUrl(dataUrl) {
|
|
|
40
71
|
const mime = match[1].toLowerCase();
|
|
41
72
|
const raw = Buffer.from(match[2], "base64");
|
|
42
73
|
if (raw.length < IMG_MIN_BYTES || mime.includes("gif")) return dataUrl;
|
|
74
|
+
const digest = imageDigest(raw);
|
|
75
|
+
if (!force && touchOptimizedImage(digest)) return dataUrl;
|
|
43
76
|
|
|
44
77
|
try {
|
|
45
78
|
const resize = await sharp();
|
|
46
79
|
const image = resize(raw, { failOn: "none", limitInputPixels: IMG_MAX_INPUT_PIXELS });
|
|
47
|
-
const metadata = await image.metadata();
|
|
48
|
-
if (mime === "image/webp"
|
|
49
|
-
&& metadata.width <= IMG_MAX_DIM
|
|
50
|
-
&& metadata.height <= IMG_MAX_DIM
|
|
51
|
-
&& (!metadata.orientation || metadata.orientation === 1)) {
|
|
52
|
-
return dataUrl;
|
|
53
|
-
}
|
|
54
80
|
const out = await image
|
|
55
81
|
.rotate()
|
|
56
|
-
.resize(IMG_MAX_DIM, IMG_MAX_DIM, { fit: "inside", withoutEnlargement: true })
|
|
57
|
-
.webp({ quality: IMG_QUALITY, effort: 4 })
|
|
82
|
+
.resize(positiveInt(maxDim, IMG_MAX_DIM), positiveInt(maxDim, IMG_MAX_DIM), { fit: "inside", withoutEnlargement: true })
|
|
83
|
+
.webp({ quality: positiveInt(quality, IMG_QUALITY, 100), effort: 4 })
|
|
58
84
|
.toBuffer();
|
|
59
|
-
if (out.length >= raw.length)
|
|
85
|
+
if (out.length >= raw.length) {
|
|
86
|
+
rememberOptimizedImage(digest);
|
|
87
|
+
return dataUrl;
|
|
88
|
+
}
|
|
89
|
+
rememberOptimizedImage(imageDigest(out));
|
|
60
90
|
const ratio = ((out.length / raw.length) * 100).toFixed(1);
|
|
61
91
|
console.log(status("info", `image ${(raw.length / 1024).toFixed(0)}KB ${mime} -> ${(out.length / 1024).toFixed(0)}KB webp (${ratio}%)`));
|
|
62
92
|
return `data:image/webp;base64,${out.toString("base64")}`;
|
|
@@ -137,6 +167,35 @@ export async function optimizeImagesInBody(reqBody, {
|
|
|
137
167
|
return reqBody;
|
|
138
168
|
}
|
|
139
169
|
|
|
170
|
+
export async function prepareResponsesPayload(reqBody, {
|
|
171
|
+
maxBytes = MAX_UPSTREAM_BODY_BYTES,
|
|
172
|
+
profiles = OVERSIZE_IMAGE_PROFILES,
|
|
173
|
+
} = {}) {
|
|
174
|
+
await optimizeImagesInBody(reqBody);
|
|
175
|
+
let bodyText = JSON.stringify(reqBody);
|
|
176
|
+
let bodyBytes = Buffer.byteLength(bodyText);
|
|
177
|
+
const summary = summarizeReqBody(reqBody);
|
|
178
|
+
const targetBytes = positiveInt(maxBytes, MAX_UPSTREAM_BODY_BYTES);
|
|
179
|
+
let adapted = false;
|
|
180
|
+
|
|
181
|
+
if (!IMG_OPT_DISABLED && summary.images > 0 && bodyBytes > targetBytes) {
|
|
182
|
+
for (const profile of profiles) {
|
|
183
|
+
const beforeBytes = bodyBytes;
|
|
184
|
+
bodyText = "";
|
|
185
|
+
await optimizeImagesInBody(reqBody, {
|
|
186
|
+
optimizeImage: (dataUrl) => optimizeImageDataUrl(dataUrl, { ...profile, force: true }),
|
|
187
|
+
});
|
|
188
|
+
bodyText = JSON.stringify(reqBody);
|
|
189
|
+
bodyBytes = Buffer.byteLength(bodyText);
|
|
190
|
+
adapted ||= bodyBytes < beforeBytes;
|
|
191
|
+
console.warn(status("warn", `responses payload ${beforeBytes}b exceeds ${targetBytes}b; image profile max_dim=${profile.maxDim} quality=${profile.quality} -> ${bodyBytes}b`));
|
|
192
|
+
if (bodyBytes <= targetBytes) break;
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
return { bodyText, bodyBytes, summary, adapted };
|
|
197
|
+
}
|
|
198
|
+
|
|
140
199
|
export function summarizeReqBody(reqBody) {
|
|
141
200
|
try {
|
|
142
201
|
const input = reqBody.input;
|