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 +14 -3
- package/bin/cli.mjs +11 -7
- package/package.json +3 -2
- package/scripts/benchmark.mjs +87 -11
- package/src/adapter.mjs +115 -837
- package/src/anthropic-handler.mjs +128 -0
- package/src/anthropic.mjs +5 -5
- package/src/auth.mjs +12 -35
- package/src/claude-config.mjs +7 -6
- package/src/config.mjs +11 -10
- package/src/copilot.mjs +38 -29
- package/src/doctor.mjs +18 -8
- package/src/github-identity.mjs +30 -0
- package/src/http-transport.mjs +154 -9
- package/src/image-optimization.mjs +24 -6
- package/src/models.mjs +13 -0
- package/src/observability.mjs +147 -0
- package/src/request-context.mjs +16 -0
- package/src/response-history.mjs +8 -11
- package/src/responses-bridge.mjs +282 -0
- package/src/responses-handler.mjs +176 -0
- package/src/responses-proxy.mjs +122 -0
- package/src/responses-request.mjs +202 -0
- package/src/runtime-config.mjs +38 -0
- package/src/status.mjs +5 -1
- package/src/stream-errors.mjs +17 -0
- package/src/stream.mjs +2 -7
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
|
|
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`
|
|
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:
|
|
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.
|
|
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": {
|
package/scripts/benchmark.mjs
CHANGED
|
@@ -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 =
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
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:
|
|
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 (
|
|
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
|
+
}
|