codex-copilot-dx 0.4.34 → 0.4.36

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
@@ -70,7 +70,15 @@ To actively verify the protocol path through an already-running adapter, run:
70
70
  codex-copilot-dx doctor --compat
71
71
  ```
72
72
 
73
- The compatibility doctor sends a few minimal Copilot requests to check native Responses, streaming history, compaction, image tool namespace handling, and Anthropic streaming. It consumes a small amount of Copilot usage, never starts the adapter or device flow, and does not change client configuration. Combine it with `--online` when both the saved-token entitlement check and the adapter protocol checks are needed.
73
+ The compatibility doctor sends a few minimal Copilot requests to check Codex Auto-review, native Responses, streaming history, compaction, image tool namespace handling, and Anthropic streaming. It consumes a small amount of Copilot usage, never starts the adapter or device flow, and does not change client configuration. Combine it with `--online` when both the saved-token entitlement check and the adapter protocol checks are needed.
74
+
75
+ For live runtime diagnostics, query the running adapter from the same machine:
76
+
77
+ ```bash
78
+ curl -s http://127.0.0.1:2026/_ccdx/status | jq
79
+ ```
80
+
81
+ The status endpoint is restricted to the socket's real loopback address, even when LAN binding is explicitly enabled. It reports fixed-size request counters, latency totals, admission pressure, memory use, response-history size, image queue state, model cache counts, and token expiry state. It never includes prompts, completions, tool arguments, image content, account names, or token values. API responses and related log lines include the same safe `X-Request-Id`/`request_id` for correlation.
74
82
 
75
83
  When the saved token is missing, `codex-copilot-dx` first looks for compatible local Copilot GitHub tokens, validates them with GitHub and Copilot, and imports a valid one before starting device login. It checks explicit token sources (`CCDX_GITHUB_TOKEN`, `CCDX_GITHUB_TOKEN_PATH`, `CCDX_GITHUB_TOKEN_PATHS`) plus common local `auth.json` layouts under application config directories. It does not rely on a specific app name. Generic discovery refuses to choose silently when valid tokens for multiple GitHub accounts are found. After an account is selected, automatic `401`/`403` recovery and in-process token rotation accept only the same GitHub account. Concurrent callers share token refresh work without sharing cancellation, and a still-valid Copilot token may be used briefly after a transient refresh failure. Explicit token variables remain the intentional way to switch accounts.
76
84
 
@@ -110,12 +118,15 @@ Environment variables:
110
118
  | `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 |
111
119
  | `CCDX_MAX_BODY_BYTES` | `67108864` | Maximum compressed/raw request body size |
112
120
  | `CCDX_MAX_DECODED_BODY_BYTES` | `134217728` | Maximum decoded request body size after decompression |
121
+ | `CCDX_MAX_INFLIGHT_BODY_BYTES` | `33554432` | Shared byte budget for admitted request bodies; a larger single request runs exclusively |
122
+ | `CCDX_MAX_QUEUED_REQUESTS` | `16` | Maximum body requests waiting for the shared byte budget |
123
+ | `CCDX_REQUEST_QUEUE_TIMEOUT_MS` | `120000` | Maximum wait for request-body admission before returning `503` |
113
124
  | `CCDX_MAX_UPSTREAM_BODY_BYTES` | `31457280` | Responses body target; larger image payloads are compressed more aggressively before forwarding |
114
125
  | `CCDX_MAX_SSE_BUFFER_BYTES` | `8388608` | Maximum buffered bytes for one unterminated upstream SSE line/event |
115
126
  | `CCDX_UPSTREAM_TIMEOUT_MS` | `120000` | Timeout for non-streaming upstream Copilot requests |
116
127
  | `CCDX_STREAM_HANDSHAKE_TIMEOUT_MS` | `120000` | Timeout while waiting for upstream streaming response headers |
117
128
  | `CCDX_STREAM_IDLE_TIMEOUT_MS` | `120000` | Maximum idle time between upstream streaming body chunks |
118
- | `CCDX_UPSTREAM_RETRIES` | `2` | Retries for transient Copilot upstream network errors; capped at `5` |
129
+ | `CCDX_UPSTREAM_RETRIES` | `2` | Retries for safe requests and clearly pre-connect POST failures; capped at `5` |
119
130
  | `CCDX_UPSTREAM_RETRY_DELAY_MS` | `300` | Initial upstream retry backoff in milliseconds; capped at `5000` |
120
131
  | `CCDX_AUTO_REVIEW_MODEL` | `gpt-5.5` | Copilot Responses model used for Codex Auto-review requests |
121
132
  | `CCDX_LOG_PATH` | unset | Mirror terminal logs to a file; set to `1` for `~/.local/share/codex-copilot-dx/debug.log` |
@@ -178,4 +189,4 @@ npm run verify
178
189
  npm run bench:payload
179
190
  ```
180
191
 
181
- `npm test` runs the unit and handler-level suite. `npm run test:smoke` starts a real local HTTP adapter with fully injected offline upstreams. `npm run pack:check` verifies the npm tarball contents without publishing. `npm run bench:payload` is a report-only, isolated-process benchmark for 5-60 MiB image payloads and does not contact Copilot. The CI workflow runs the verification checks on supported Node.js release lines.
192
+ `npm test` runs the unit and handler-level suite. `npm run test:smoke` starts a real local HTTP adapter with fully injected offline upstreams. `npm run bench:check` enforces relative token-counting performance and request-admission resource limits. `npm run pack:check` verifies the npm tarball contents without publishing. `npm run bench:payload` remains a report-only, isolated-process benchmark for 5-60 MiB image payloads and does not contact Copilot. The CI workflow runs the verification checks on supported Node.js release lines.
package/bin/cli.mjs CHANGED
@@ -6,13 +6,13 @@ import { ensureClaudeConfig } from "../src/claude-config.mjs";
6
6
  import { applyClaudeDesktopConfig, formatClaudeDesktopApplyResult, generatedClaudeDesktopApiKey, loadManagedClaudeDesktopApiKey } from "../src/claude-desktop-config.mjs";
7
7
  import { startAdapter } from "../src/adapter.mjs";
8
8
  import { cacheModelEndpoints, listModels, refreshVSCodeVersion } from "../src/copilot.mjs";
9
- import { claudeDesktopModelDefsFromCopilotModels, claudeDesktopModelIds, gptModelIdsFromCopilotModels, parseModelAliasEnv } from "../src/models.mjs";
9
+ import { claudeDesktopModelDefsFromCopilotModels, claudeDesktopModelIds, codexAutoReviewModelStatus, gptModelIdsFromCopilotModels, parseModelAliasEnv } from "../src/models.mjs";
10
10
  import { status } from "../src/status.mjs";
11
11
  import { configureLogging } from "../src/log.mjs";
12
12
  import { flushUsageWrites, printUsageSummary } from "../src/usage.mjs";
13
13
  import { checkForUpdate, localPackageVersion } from "../src/version.mjs";
14
14
  import { runDoctor } from "../src/doctor.mjs";
15
- import { checkRunningAdapter } from "../src/running-adapter.mjs";
15
+ import { adapterBaseUrl, checkRunningAdapter } from "../src/running-adapter.mjs";
16
16
  import { assertSafeAdapterHost, isLoopbackHost } from "../src/security.mjs";
17
17
  import { isValidModelList, loadModelCache, saveModelCache } from "../src/model-cache.mjs";
18
18
  import { initializeModelRegistry, runInBackground } from "../src/startup.mjs";
@@ -105,6 +105,10 @@ async function refreshClaudeDesktopModelDefs() {
105
105
  throw new Error(`Copilot models returned HTTP ${httpStatus}`);
106
106
  }
107
107
  const models = JSON.parse(body);
108
+ const autoReview = codexAutoReviewModelStatus(models);
109
+ if (!autoReview.available) {
110
+ console.log(status("warn", `Auto-review target ${autoReview.upstreamModel} is unavailable: ${autoReview.reason}. Run codex-copilot-dx doctor --compat to verify the live path.`));
111
+ }
108
112
  const gptModelIds = gptModelIdsFromCopilotModels(models);
109
113
  if (gptModelIds.length) {
110
114
  console.log(status("ok", `Refreshed GPT models from GitHub Copilot: ${gptModelIds.join(", ")}`));
@@ -152,8 +156,8 @@ async function reuseRunningAdapterIfAvailable() {
152
156
  if (!running.ok) return false;
153
157
 
154
158
  console.log(status("ok", `Using existing adapter at ${running.baseUrl}`));
155
- ensureCodexConfig(ADAPTER_PORT);
156
- ensureClaudeConfig(ADAPTER_PORT);
159
+ ensureCodexConfig(ADAPTER_PORT, { host: ADAPTER_HOST });
160
+ ensureClaudeConfig(ADAPTER_PORT, { host: ADAPTER_HOST });
157
161
 
158
162
  if (CONFIGURE_CLAUDE_DESKTOP) {
159
163
  const claudeDesktopApiKey = currentClaudeDesktopApiKey();
@@ -252,8 +256,8 @@ try {
252
256
  });
253
257
 
254
258
  // Point Codex and Claude Code at the adapter.
255
- ensureCodexConfig(ADAPTER_PORT);
256
- ensureClaudeConfig(ADAPTER_PORT);
259
+ ensureCodexConfig(ADAPTER_PORT, { host: ADAPTER_HOST });
260
+ ensureClaudeConfig(ADAPTER_PORT, { host: ADAPTER_HOST });
257
261
  if (CONFIGURE_CLAUDE_DESKTOP) {
258
262
  const result = applyClaudeDesktopConfig({
259
263
  port: ADAPTER_PORT,
@@ -283,7 +287,7 @@ try {
283
287
  console.log(`
284
288
  ${status("ok", "Ready, Claude Code, Claude App and Codex App are now ready to use")}
285
289
 
286
- Adapter: http://${ADAPTER_HOST}:${ADAPTER_PORT}
290
+ Adapter: ${adapterBaseUrl(ADAPTER_HOST, ADAPTER_PORT)}
287
291
 
288
292
  Press Ctrl+C to stop.
289
293
  `);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codex-copilot-dx",
3
- "version": "0.4.34",
3
+ "version": "0.4.36",
4
4
  "description": "Use Codex Desktop, Claude Code, and Claude App with GitHub Copilot",
5
5
  "author": "Dale Xiao",
6
6
  "type": "module",
@@ -44,8 +44,9 @@
44
44
  "test:e2e": "bash test/e2e.sh",
45
45
  "bench": "node scripts/benchmark.mjs",
46
46
  "bench:payload": "node scripts/benchmark.mjs --large-payload",
47
+ "bench:check": "node scripts/benchmark.mjs --check",
47
48
  "pack:check": "npm pack --dry-run --json",
48
- "verify": "npm test && npm run test:smoke && npm run pack:check",
49
+ "verify": "npm test && npm run test:smoke && npm run bench:check && npm run pack:check",
49
50
  "prepublishOnly": "npm run verify"
50
51
  },
51
52
  "dependencies": {
@@ -72,12 +72,14 @@ function largePayloadProbe(targetMiB, concurrency) {
72
72
  const result = runProbe(`
73
73
  import fs from "node:fs";
74
74
  import { prepareResponsesRequest } from "./src/adapter.mjs";
75
- import { readJsonBody } from "./src/http-transport.mjs";
75
+ import { createRequestAdmission, readJsonBody } from "./src/http-transport.mjs";
76
76
  import { prepareResponsesPayload } from "./src/image-optimization.mjs";
77
77
 
78
78
  const fixtureFiles = ${JSON.stringify(fixtureFiles)};
79
+ const acquireRequest = createRequestAdmission();
79
80
  const memoryFields = ["rss", "heapUsed", "external", "arrayBuffers"];
80
81
  const peak = {};
82
+ let admissionMaxActiveBytes = 0;
81
83
  const sample = () => {
82
84
  const memory = process.memoryUsage();
83
85
  for (const field of memoryFields) peak[field] = Math.max(peak[field] || 0, memory[field] || 0);
@@ -90,15 +92,23 @@ function largePayloadProbe(targetMiB, concurrency) {
90
92
 
91
93
  async function processRequest(filePath) {
92
94
  const inputBytes = fs.statSync(filePath).size;
95
+ const headers = { "content-length": String(inputBytes), "content-encoding": "identity" };
96
+ const releaseRequest = await acquireRequest({ headers });
97
+ admissionMaxActiveBytes = Math.max(admissionMaxActiveBytes, acquireRequest.stats().activeBytes);
93
98
  const req = fs.createReadStream(filePath);
94
- req.headers = { "content-length": String(inputBytes), "content-encoding": "identity" };
95
- const parsed = await readJsonBody(req);
96
- req.destroy();
97
- sample();
98
- const prepared = prepareResponsesRequest(parsed, { mutate: true });
99
- const result = await prepareResponsesPayload(prepared.body);
100
- sample();
101
- return { inputBytes, outputBytes: result.bodyBytes };
99
+ req.headers = headers;
100
+ try {
101
+ const parsed = await readJsonBody(req);
102
+ req.destroy();
103
+ sample();
104
+ const prepared = prepareResponsesRequest(parsed, { mutate: true });
105
+ const result = await prepareResponsesPayload(prepared.body);
106
+ sample();
107
+ return { inputBytes, outputBytes: result.bodyBytes };
108
+ } finally {
109
+ req.destroy();
110
+ releaseRequest();
111
+ }
102
112
  }
103
113
 
104
114
  const originalLog = console.log;
@@ -130,6 +140,8 @@ function largePayloadProbe(targetMiB, concurrency) {
130
140
  input_body_mib: results.map((entry) => mib(entry.inputBytes)),
131
141
  output_body_mib: results.map((entry) => mib(entry.outputBytes)),
132
142
  output_body_kib: results.map((entry) => +(entry.outputBytes / 1024).toFixed(1)),
143
+ admission_max_active_mib: mib(admissionMaxActiveBytes),
144
+ admission_budget_mib: mib(acquireRequest.stats().maxBytes),
133
145
  memory,
134
146
  }));
135
147
  `);
@@ -140,6 +152,30 @@ function largePayloadProbe(targetMiB, concurrency) {
140
152
  }
141
153
  }
142
154
 
155
+ function largeTokenCountProbe(mode) {
156
+ return runProbe(`
157
+ const text = "a ".repeat(2 * 1024 * 1024);
158
+ const started = performance.now();
159
+ let tokens;
160
+ if (${JSON.stringify(mode)} === "proxy") {
161
+ const { countTokens } = await import("./src/anthropic.mjs");
162
+ tokens = (await countTokens({ messages: [{ role: "user", content: text }] })).input_tokens;
163
+ } else {
164
+ const { encode } = await import("gpt-tokenizer");
165
+ const encoded = encode(text);
166
+ tokens = encoded.length;
167
+ }
168
+ const memory = process.memoryUsage();
169
+ process.stdout.write(JSON.stringify({
170
+ input_mib: +(Buffer.byteLength(text) / 1048576).toFixed(1),
171
+ tokens,
172
+ elapsed_ms: +(performance.now() - started).toFixed(1),
173
+ rss_mib: +(memory.rss / 1048576).toFixed(1),
174
+ heap_used_mib: +(memory.heapUsed / 1048576).toFixed(1),
175
+ }));
176
+ `);
177
+ }
178
+
143
179
  const adapterImport = runProbe(`
144
180
  const started = performance.now();
145
181
  await import("./src/adapter.mjs");
@@ -237,15 +273,28 @@ try {
237
273
  console.warn = originalWarn;
238
274
  }
239
275
 
276
+ const checkMode = process.argv.includes("--check");
240
277
  const report = {
241
- note: "Report-only benchmark; timings vary by machine and are not pass/fail thresholds.",
278
+ note: checkMode
279
+ ? "Relative performance and resource checks; absolute timings vary by machine."
280
+ : "Report-only benchmark; timings vary by machine and are not pass/fail thresholds.",
242
281
  adapter_import: adapterImport,
243
282
  token_count_100kb: tokenCount,
244
283
  duplicate_images: duplicateImages,
245
284
  oversized_payload: oversizedPayload,
246
285
  };
247
286
 
248
- if (process.argv.includes("--large-payload")) {
287
+ if (checkMode) {
288
+ report.large_token_count = {
289
+ proxy: largeTokenCountProbe("proxy"),
290
+ materialized_array: largeTokenCountProbe("array"),
291
+ };
292
+ report.large_payload_peak = [
293
+ largePayloadProbe(5, 4),
294
+ largePayloadProbe(30, 1),
295
+ largePayloadProbe(30, 2),
296
+ ];
297
+ } else if (process.argv.includes("--large-payload")) {
249
298
  report.large_payload_peak = [
250
299
  largePayloadProbe(5, 1),
251
300
  largePayloadProbe(5, 4),
@@ -256,3 +305,30 @@ if (process.argv.includes("--large-payload")) {
256
305
  }
257
306
 
258
307
  console.log(JSON.stringify(report, null, 2));
308
+
309
+ if (checkMode) {
310
+ const failures = [];
311
+ const proxy = report.large_token_count.proxy;
312
+ const array = report.large_token_count.materialized_array;
313
+ if (proxy.tokens !== array.tokens) failures.push(`token counts differ: proxy=${proxy.tokens} array=${array.tokens}`);
314
+ if (proxy.elapsed_ms > array.elapsed_ms * 1.25) {
315
+ failures.push(`token counter is materially slower: proxy=${proxy.elapsed_ms}ms array=${array.elapsed_ms}ms`);
316
+ }
317
+ if (proxy.rss_mib > array.rss_mib * 0.9) {
318
+ failures.push(`token counter RSS regression: proxy=${proxy.rss_mib}MiB array=${array.rss_mib}MiB`);
319
+ }
320
+ if (proxy.heap_used_mib > array.heap_used_mib * 0.75) {
321
+ failures.push(`token counter heap regression: proxy=${proxy.heap_used_mib}MiB array=${array.heap_used_mib}MiB`);
322
+ }
323
+ for (const payload of report.large_payload_peak) {
324
+ if (payload.admission_max_active_mib > payload.admission_budget_mib) {
325
+ failures.push(`request admission exceeded its budget at ${payload.target_request_mib}MiB x${payload.concurrency}`);
326
+ }
327
+ }
328
+ if (failures.length) {
329
+ for (const failure of failures) console.error(`[FAIL] ${failure}`);
330
+ process.exitCode = 1;
331
+ } else {
332
+ console.error("[OK] Performance and resource checks passed");
333
+ }
334
+ }