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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codex-copilot-dx",
3
- "version": "0.4.32",
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
- import { encode } from "gpt-tokenizer";
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
- export async function listModels({ signal, fetchImpl, retryOptions } = {}) {
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
- const resize = await sharp();
79
- const image = resize(raw, { failOn: "none", limitInputPixels: IMG_MAX_INPUT_PIXELS });
80
- const out = await image
81
- .rotate()
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 })
84
- .toBuffer();
85
- if (out.length >= raw.length) {
86
- rememberOptimizedImage(digest);
87
- return dataUrl;
88
- }
89
- rememberOptimizedImage(imageDigest(out));
90
- const ratio = ((out.length / raw.length) * 100).toFixed(1);
91
- console.log(status("info", `image ${(raw.length / 1024).toFixed(0)}KB ${mime} -> ${(out.length / 1024).toFixed(0)}KB webp (${ratio}%)`));
92
- return `data:image/webp;base64,${out.toString("base64")}`;
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, optimizeImage);
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, optimizeImage);
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, optimizeImage);
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);