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 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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codex-copilot-dx",
3
- "version": "0.4.29",
3
+ "version": "0.4.30",
4
4
  "description": "Use Codex Desktop, Claude Code, and Claude App with GitHub Copilot",
5
5
  "type": "module",
6
6
  "bin": {
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 optimizeImagesInBody(reqBody);
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
- export async function optimizeImageDataUrl(dataUrl) {
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) return dataUrl;
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;