codex-copilot-dx 0.6.2 → 0.6.4
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 +18 -18
- package/bin/codex-copilot-dx.mjs +9 -1
- package/package.json +1 -1
- package/scripts/benchmark.mjs +61 -0
- package/src/adapter.mjs +21 -2
- package/src/anthropic-handler.mjs +8 -4
- package/src/cli-options.mjs +5 -11
- package/src/cli-status.mjs +5 -1
- package/src/copilot-client.mjs +5 -0
- package/src/http-transport.mjs +211 -27
- package/src/legacy-command-warning.mjs +37 -0
- package/src/observability.mjs +2 -0
- package/src/pm-studio-relay.mjs +8 -2
- package/src/response-history.mjs +15 -2
- package/src/responses-bridge.mjs +37 -16
- package/src/responses-byte-budget.mjs +63 -5
- package/src/responses-handler.mjs +154 -28
- package/src/responses-image-pressure.mjs +180 -0
- package/src/responses-proxy.mjs +8 -5
- package/src/responses-request.mjs +17 -1
- package/src/runtime-config.mjs +2 -0
package/README.md
CHANGED
|
@@ -29,11 +29,12 @@ npm install -g codex-copilot-dx@latest
|
|
|
29
29
|
ccdx
|
|
30
30
|
```
|
|
31
31
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
32
|
+
`ccdx` is the only documented command and primary launcher. The old
|
|
33
|
+
`codex-copilot-dx` executable remains temporarily as a compatibility shim: it
|
|
34
|
+
prints a deprecation warning at most once every 7 days in interactive terminals,
|
|
35
|
+
runs `ccdx`, and remains silent in scripts. It will be removed in a future
|
|
36
|
+
breaking release. For a one-off run without a global install, use
|
|
37
|
+
`npm exec --yes --package=codex-copilot-dx@latest -- ccdx`.
|
|
37
38
|
Run `ccdx <command> --help` (including `auth login claude --help` and
|
|
38
39
|
`pms status --help`) for command-specific behavior and side-effect details.
|
|
39
40
|
|
|
@@ -49,8 +50,7 @@ starting the adapter, opening device login, reading the local model cache, or
|
|
|
49
50
|
consuming an inference request. It lists model IDs by provider with their
|
|
50
51
|
advertised API capabilities and reports the live Claude/Anthropic count. A model
|
|
51
52
|
being advertised does not guarantee that a later inference request will avoid
|
|
52
|
-
quota, rate-limit, or policy enforcement.
|
|
53
|
-
equivalent.
|
|
53
|
+
quota, rate-limit, or policy enforcement.
|
|
54
54
|
|
|
55
55
|
### Separate GitHub account for Claude
|
|
56
56
|
|
|
@@ -193,7 +193,7 @@ It saves the selection in `~/.config/codex-copilot-dx/config.json` (or under
|
|
|
193
193
|
`XDG_CONFIG_HOME` when set); choosing `gpt-5.5` clears the override and restores
|
|
194
194
|
the package default. A running 0.5.1+
|
|
195
195
|
adapter reads the setting on the next Auto-review request, so it does not need to
|
|
196
|
-
be restarted.
|
|
196
|
+
be restarted.
|
|
197
197
|
|
|
198
198
|
To update the globally installed package, choose a source interactively:
|
|
199
199
|
|
|
@@ -216,8 +216,7 @@ latest commit from `DaleXiao/codex-copilot-dx` `main` and opts in to Git fetchin
|
|
|
216
216
|
for that command, as required by npm 12. It does not change npm's persistent
|
|
217
217
|
`allow-git` setting. Both paths use npm's global installer without shell
|
|
218
218
|
interpolation. A currently running adapter keeps its loaded version until it is
|
|
219
|
-
stopped and started again.
|
|
220
|
-
`codex-copilot-dx update` compatibility command behaves identically.
|
|
219
|
+
stopped and started again.
|
|
221
220
|
If a configured npm mirror has not synchronized the current release yet, use
|
|
222
221
|
the GitHub source to install the current `main` revision.
|
|
223
222
|
|
|
@@ -278,15 +277,13 @@ For a read-only summary of the running adapter, use:
|
|
|
278
277
|
ccdx status
|
|
279
278
|
```
|
|
280
279
|
|
|
281
|
-
|
|
282
|
-
`codex-copilot-dx status`. For the complete machine-readable payload, query the
|
|
283
|
-
loopback endpoint directly:
|
|
280
|
+
For the complete machine-readable payload, query the loopback endpoint directly:
|
|
284
281
|
|
|
285
282
|
```bash
|
|
286
283
|
curl -s http://127.0.0.1:2026/_ccdx/status | jq
|
|
287
284
|
```
|
|
288
285
|
|
|
289
|
-
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, bounded TTFT/TPOT histograms for streaming routes, admission pressure, memory use, response-history size, image queue/cache state, profile model cache counts, PM relay route totals, and token expiry state. It never retains metric samples or includes prompts, completions, tool arguments, image content, account names, or token values. API responses always include a safe `X-Request-Id` for correlation. If the adapter is not running, the CLI prints the exact start-and-retry command. To include the same ID as `request_id` in related terminal and file log lines, start the adapter with:
|
|
286
|
+
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, bounded TTFT/TPOT histograms for streaming routes, admission pressure, memory use, response-history size, image queue/cache and adaptive-history state, profile model cache counts, PM relay route totals, and token expiry state. It never retains metric samples or includes prompts, completions, tool arguments, image content, account names, or token values. API responses always include a safe `X-Request-Id` for correlation. If the adapter is not running, the CLI prints the exact start-and-retry command. To include the same ID as `request_id` in related terminal and file log lines, start the adapter with:
|
|
290
287
|
|
|
291
288
|
```bash
|
|
292
289
|
ccdx --show-request-id
|
|
@@ -306,7 +303,7 @@ ccdx
|
|
|
306
303
|
Claude App support is opt-in so the default Codex Desktop and Claude Code setup stays unchanged:
|
|
307
304
|
|
|
308
305
|
```bash
|
|
309
|
-
|
|
306
|
+
npm exec --yes --package=codex-copilot-dx@latest -- ccdx start --configure-claude-app
|
|
310
307
|
```
|
|
311
308
|
|
|
312
309
|
The previous `--configure-claude-desktop` spelling remains a compatibility
|
|
@@ -316,7 +313,7 @@ key is restored or the profile is configured in that run.
|
|
|
316
313
|
Or set:
|
|
317
314
|
|
|
318
315
|
```bash
|
|
319
|
-
CCDX_CONFIGURE_CLAUDE_DESKTOP=1
|
|
316
|
+
CCDX_CONFIGURE_CLAUDE_DESKTOP=1 npm exec --yes --package=codex-copilot-dx@latest -- ccdx
|
|
320
317
|
```
|
|
321
318
|
|
|
322
319
|
This writes a local Claude App 3P gateway profile that points to the adapter root URL, such as `http://127.0.0.1:2026`. The profile uses a generated local bearer key unless `CCDX_CLAUDE_DESKTOP_API_KEY` is set. Later starts restore that key only from the active managed profile when its gateway URL matches the adapter. Once that managed profile is active, successful Copilot model refreshes automatically keep its `inferenceModels` list current without changing the gateway key or other profile settings. Restart Claude App after the initial setup. Later model changes require no profile reconfiguration, although a running Claude App may need to be reopened if it has not reloaded the updated profile yet.
|
|
@@ -334,9 +331,10 @@ Environment variables:
|
|
|
334
331
|
| `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 |
|
|
335
332
|
| `CCDX_MAX_BODY_BYTES` | `67108864` | Maximum compressed/raw request body size |
|
|
336
333
|
| `CCDX_MAX_DECODED_BODY_BYTES` | `134217728` | Maximum decoded request body size after decompression |
|
|
337
|
-
| `CCDX_MAX_INFLIGHT_BODY_BYTES` | `33554432` |
|
|
334
|
+
| `CCDX_MAX_INFLIGHT_BODY_BYTES` | `33554432` | Per-pool byte budget for raw bodies, decoded bodies, and materialized response history; a larger item runs exclusively in its pool |
|
|
338
335
|
| `CCDX_MAX_QUEUED_REQUESTS` | `16` | Maximum body requests waiting for the shared byte budget |
|
|
339
336
|
| `CCDX_REQUEST_QUEUE_TIMEOUT_MS` | `120000` | Maximum wait for request-body admission before returning `503` |
|
|
337
|
+
| `CCDX_REQUEST_BODY_TIMEOUT_MS` | `120000` | Maximum time to receive and decode one request body before returning `408` |
|
|
340
338
|
| `CCDX_MAX_UPSTREAM_BODY_BYTES` | `31457280` | Strict maximum forwarded Responses body size; larger payloads are adapted locally or rejected with `413` |
|
|
341
339
|
| `CCDX_MAX_SSE_BUFFER_BYTES` | `8388608` | Maximum buffered bytes for one unterminated upstream SSE line/event |
|
|
342
340
|
| `CCDX_UPSTREAM_TIMEOUT_MS` | `120000` | Timeout for non-streaming upstream Copilot requests |
|
|
@@ -392,7 +390,7 @@ Set `CCDX_LOG_PATH=1` to mirror terminal logs to `~/.local/share/codex-copilot-d
|
|
|
392
390
|
|
|
393
391
|
### Image optimization
|
|
394
392
|
|
|
395
|
-
Long computer-use sessions can accumulate screenshots inside the conversation history. Each screenshot is shipped on later turns,
|
|
393
|
+
Long computer-use sessions can accumulate screenshots inside the conversation history. Each screenshot is shipped on later turns, which can increase local preparation time, upstream handshake latency, and the chance of a `413 Payload Too Large` response.
|
|
396
394
|
|
|
397
395
|
The adapter automatically downsamples embedded screenshots to model-appropriate pixel bounds (with a conservative 2048 px long-edge fallback) and initially re-encodes them as WebP quality 82 before forwarding `/v1/responses`. It never replaces an image with a larger encoding and applies one global concurrency limit across direct, function-tool, and custom-tool output images.
|
|
398
396
|
|
|
@@ -400,6 +398,8 @@ The final serialized UTF-8 request body is measured before forwarding. Above the
|
|
|
400
398
|
|
|
401
399
|
Completed image transforms are reused from a byte-bounded process-local LRU cache, and concurrent requests for the same transform share one encode. The cache key includes source content, MIME type, model-aware dimensions, WebP quality, and encoder settings; failures are not cached and request cancellation remains isolated.
|
|
402
400
|
|
|
401
|
+
Before image encoding, CCDX leaves ordinary visual history byte-for-byte unchanged while it has at most 24 historical images and the expanded body is at most 18 MiB. Above either threshold, the temporary upstream view keeps every current-turn image and at most 16 recent historical images, targeting 16 MiB. If that history tree then times out during local preparation or upstream handshake, the next retry automatically uses an 8-image, 10 MiB recovery window for ten minutes. Two successful requests or a successful compaction clear recovery mode. CCDX never transparently repeats the ambiguous timed-out POST, and no local history is deleted; retrying does not require restarting the service.
|
|
402
|
+
|
|
403
403
|
When expanded Responses history exceeds Copilot's 50-image request limit, the adapter removes older duplicate image occurrences first, then keeps the 50 most recent images. This applies only to the forwarded request: local history remains complete, current images are preferred over older history, and requests at or below the limit are unchanged.
|
|
404
404
|
|
|
405
405
|
For `/v1/responses/compact`, the adapter sends one terminal `compaction_trigger` in unary mode, validates that Copilot returned completed compaction state, and rebuilds a replayable snapshot from recent system, developer, user, and assistant messages plus every compaction item. Only a validated snapshot becomes a new local history root. Later turns continue from it without re-expanding pre-compaction history; existing older response branches remain available until normal history eviction.
|
package/bin/codex-copilot-dx.mjs
CHANGED
|
@@ -1,2 +1,10 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
|
|
2
|
+
|
|
3
|
+
if (process.stderr.isTTY === true) {
|
|
4
|
+
const {
|
|
5
|
+
LEGACY_COMMAND_WARNING,
|
|
6
|
+
shouldShowLegacyCommandWarning,
|
|
7
|
+
} = await import("../src/legacy-command-warning.mjs");
|
|
8
|
+
if (shouldShowLegacyCommandWarning({ interactive: true })) console.warn(LEGACY_COMMAND_WARNING);
|
|
9
|
+
}
|
|
10
|
+
await import("./cli.mjs");
|
package/package.json
CHANGED
package/scripts/benchmark.mjs
CHANGED
|
@@ -177,6 +177,60 @@ function largeTokenCountProbe(mode) {
|
|
|
177
177
|
`);
|
|
178
178
|
}
|
|
179
179
|
|
|
180
|
+
function visualHistoryPressureProbe() {
|
|
181
|
+
return runProbe(`
|
|
182
|
+
import { applyResponsesImagePressure } from "./src/responses-image-pressure.mjs";
|
|
183
|
+
|
|
184
|
+
const concurrency = 4;
|
|
185
|
+
const historicalImages = 36;
|
|
186
|
+
const imageBytes = 256 * 1024;
|
|
187
|
+
const dataUrls = Array.from({ length: historicalImages }, (_, index) => (
|
|
188
|
+
\`data:image/png;base64,\${Buffer.alloc(imageBytes, index + 1).toString("base64")}\`
|
|
189
|
+
));
|
|
190
|
+
const contexts = Array.from({ length: concurrency }, (_, requestIndex) => ({
|
|
191
|
+
historyRootId: \`resp_benchmark_\${requestIndex}\`,
|
|
192
|
+
currentInputStart: historicalImages,
|
|
193
|
+
body: {
|
|
194
|
+
model: "gpt-5.6-sol",
|
|
195
|
+
stream: true,
|
|
196
|
+
input: [
|
|
197
|
+
...dataUrls.map((imageUrl) => ({
|
|
198
|
+
type: "message",
|
|
199
|
+
role: "user",
|
|
200
|
+
content: [{ type: "input_image", image_url: imageUrl }],
|
|
201
|
+
})),
|
|
202
|
+
{ type: "message", role: "user", content: "continue" },
|
|
203
|
+
],
|
|
204
|
+
},
|
|
205
|
+
}));
|
|
206
|
+
globalThis.gc?.();
|
|
207
|
+
const before = process.memoryUsage();
|
|
208
|
+
let peakRss = before.rss;
|
|
209
|
+
let peakHeap = before.heapUsed;
|
|
210
|
+
const started = performance.now();
|
|
211
|
+
const results = await Promise.all(contexts.map(async (context) => {
|
|
212
|
+
await Promise.resolve();
|
|
213
|
+
const result = applyResponsesImagePressure(context);
|
|
214
|
+
const memory = process.memoryUsage();
|
|
215
|
+
peakRss = Math.max(peakRss, memory.rss);
|
|
216
|
+
peakHeap = Math.max(peakHeap, memory.heapUsed);
|
|
217
|
+
return result;
|
|
218
|
+
}));
|
|
219
|
+
const mib = (bytes) => +(bytes / 1048576).toFixed(1);
|
|
220
|
+
process.stdout.write(JSON.stringify({
|
|
221
|
+
concurrency,
|
|
222
|
+
historical_images_per_request: historicalImages,
|
|
223
|
+
elapsed_ms: +(performance.now() - started).toFixed(1),
|
|
224
|
+
omitted_images: results.map((result) => result.imagesOmitted),
|
|
225
|
+
retained_historical_images: results.map((result) => result.historicalImages),
|
|
226
|
+
input_body_mib: results.map((result) => mib(result.initialBodyBytes)),
|
|
227
|
+
output_body_mib: results.map((result) => mib(result.bodyBytes)),
|
|
228
|
+
rss_peak_delta_mib: mib(peakRss - before.rss),
|
|
229
|
+
heap_peak_delta_mib: mib(peakHeap - before.heapUsed),
|
|
230
|
+
}));
|
|
231
|
+
`);
|
|
232
|
+
}
|
|
233
|
+
|
|
180
234
|
const adapterImport = runProbe(`
|
|
181
235
|
const started = performance.now();
|
|
182
236
|
await import("./src/adapter.mjs");
|
|
@@ -324,6 +378,7 @@ if (checkMode) {
|
|
|
324
378
|
largePayloadProbe(30, 1),
|
|
325
379
|
largePayloadProbe(30, 2),
|
|
326
380
|
];
|
|
381
|
+
report.visual_history_pressure_36x4 = visualHistoryPressureProbe();
|
|
327
382
|
} else if (process.argv.includes("--large-payload")) {
|
|
328
383
|
report.large_payload_peak = [
|
|
329
384
|
largePayloadProbe(5, 1),
|
|
@@ -359,6 +414,12 @@ if (checkMode) {
|
|
|
359
414
|
failures.push(`request admission exceeded its budget at ${payload.target_request_mib}MiB x${payload.concurrency}`);
|
|
360
415
|
}
|
|
361
416
|
}
|
|
417
|
+
const visualHistory = report.visual_history_pressure_36x4;
|
|
418
|
+
if (!visualHistory.omitted_images.every((count) => count === 20)
|
|
419
|
+
|| !visualHistory.retained_historical_images.every((count) => count === 16)
|
|
420
|
+
|| !visualHistory.output_body_mib.every((size, index) => size < visualHistory.input_body_mib[index])) {
|
|
421
|
+
failures.push("36-image x4 visual history pressure did not preserve the expected 16-image upstream window");
|
|
422
|
+
}
|
|
362
423
|
if (failures.length) {
|
|
363
424
|
for (const failure of failures) console.error(`[FAIL] ${failure}`);
|
|
364
425
|
process.exitCode = 1;
|
package/src/adapter.mjs
CHANGED
|
@@ -17,6 +17,7 @@ import { status } from "./status.mjs";
|
|
|
17
17
|
import { createTerminalActivityIndicator } from "./terminal-activity.mjs";
|
|
18
18
|
import { ADAPTER_HEALTH_PATH, adapterHealthPayload } from "./running-adapter.mjs";
|
|
19
19
|
import { createResponsesCompactHandler, createResponsesHandler } from "./responses-handler.mjs";
|
|
20
|
+
import { createResponsesImagePressureController } from "./responses-image-pressure.mjs";
|
|
20
21
|
import { loadRuntimeConfig, parsePositiveInteger } from "./runtime-config.mjs";
|
|
21
22
|
import {
|
|
22
23
|
createRequestAdmission,
|
|
@@ -135,9 +136,11 @@ export function createAdapterHandler(options = {}) {
|
|
|
135
136
|
const upstreamTimeoutMs = parsePositiveInteger(options.upstreamTimeoutMs, ADAPTER_RUNTIME_CONFIG.upstreamTimeoutMs);
|
|
136
137
|
const streamHandshakeTimeoutMs = parsePositiveInteger(options.streamHandshakeTimeoutMs, ADAPTER_RUNTIME_CONFIG.streamHandshakeTimeoutMs);
|
|
137
138
|
const streamIdleTimeoutMs = parsePositiveInteger(options.streamIdleTimeoutMs, ADAPTER_RUNTIME_CONFIG.streamIdleTimeoutMs);
|
|
139
|
+
const requestBodyTimeoutMs = parsePositiveInteger(options.requestBodyTimeoutMs, ADAPTER_RUNTIME_CONFIG.requestBodyTimeoutMs);
|
|
138
140
|
const acquireRequest = options.acquireRequest || createRequestAdmission();
|
|
139
141
|
const requestMetrics = options.requestMetrics || createRequestMetrics();
|
|
140
142
|
const streamPerformanceMetrics = options.streamPerformanceMetrics || createStreamPerformanceMetrics();
|
|
143
|
+
const imagePressure = options.imagePressure || createResponsesImagePressureController();
|
|
141
144
|
const claudeDesktopModelOptions = () => {
|
|
142
145
|
const modelDefs = claudeModelRegistry?.modelDefs || options.claudeDesktopModelDefs;
|
|
143
146
|
return Array.isArray(modelDefs) ? { modelDefs } : {};
|
|
@@ -147,9 +150,12 @@ export function createAdapterHandler(options = {}) {
|
|
|
147
150
|
autoReviewModelResolver: options.autoReviewModelResolver,
|
|
148
151
|
chatCompletionsFn: codexChatCompletionsFn,
|
|
149
152
|
getCachedModelEndpointsFn,
|
|
153
|
+
imagePressure,
|
|
154
|
+
now: options.now,
|
|
150
155
|
openAIModelEnv,
|
|
151
156
|
responsesPayloadOptions: options.responsesPayloadOptions,
|
|
152
157
|
responsesFn,
|
|
158
|
+
requestBodyTimeoutMs,
|
|
153
159
|
streamHandshakeTimeoutMs,
|
|
154
160
|
streamIdleTimeoutMs,
|
|
155
161
|
upstreamTimeoutMs,
|
|
@@ -157,18 +163,22 @@ export function createAdapterHandler(options = {}) {
|
|
|
157
163
|
const responsesCompactHandler = createResponsesCompactHandler({
|
|
158
164
|
acquireRequest,
|
|
159
165
|
autoReviewModelResolver: options.autoReviewModelResolver,
|
|
166
|
+
imagePressure,
|
|
167
|
+
now: options.now,
|
|
160
168
|
openAIModelEnv,
|
|
169
|
+
requestBodyTimeoutMs,
|
|
161
170
|
responsesCompactFn,
|
|
162
171
|
streamHandshakeTimeoutMs,
|
|
163
172
|
streamIdleTimeoutMs,
|
|
164
173
|
upstreamTimeoutMs,
|
|
165
174
|
});
|
|
166
|
-
const anthropicCountTokensHandler = createAnthropicCountTokensHandler({ acquireRequest });
|
|
175
|
+
const anthropicCountTokensHandler = createAnthropicCountTokensHandler({ acquireRequest, requestBodyTimeoutMs });
|
|
167
176
|
const anthropicMessagesHandler = createAnthropicMessagesHandler({
|
|
168
177
|
acquireRequest,
|
|
169
178
|
chatCompletionsFn: claudeChatCompletionsFn,
|
|
170
179
|
environment: process.env,
|
|
171
180
|
modelOptions: claudeDesktopModelOptions,
|
|
181
|
+
requestBodyTimeoutMs,
|
|
172
182
|
streamHandshakeTimeoutMs,
|
|
173
183
|
streamIdleTimeoutMs,
|
|
174
184
|
upstreamTimeoutMs,
|
|
@@ -180,6 +190,7 @@ export function createAdapterHandler(options = {}) {
|
|
|
180
190
|
claudeProfile: options.claudeProfile,
|
|
181
191
|
claudeModelRegistry,
|
|
182
192
|
fetchImpl: options.pmStudioFetchImpl,
|
|
193
|
+
requestBodyTimeoutMs,
|
|
183
194
|
streamHandshakeTimeoutMs,
|
|
184
195
|
streamIdleTimeoutMs,
|
|
185
196
|
upstreamTimeoutMs,
|
|
@@ -197,6 +208,7 @@ export function createAdapterHandler(options = {}) {
|
|
|
197
208
|
metrics: requestMetrics,
|
|
198
209
|
streamPerformance: streamPerformanceMetrics,
|
|
199
210
|
admission: acquireRequest,
|
|
211
|
+
imagePressure,
|
|
200
212
|
modelRegistry: codexModelRegistry,
|
|
201
213
|
codexClient,
|
|
202
214
|
claudeClient,
|
|
@@ -279,13 +291,20 @@ export function createAdapterHandler(options = {}) {
|
|
|
279
291
|
};
|
|
280
292
|
|
|
281
293
|
return (req, res) => {
|
|
282
|
-
const pathname = requestPath(req.url);
|
|
283
294
|
const requestId = createRequestId();
|
|
284
295
|
if (typeof res.setHeader === "function") {
|
|
285
296
|
res.setHeader("X-Request-Id", requestId);
|
|
286
297
|
} else if (res.headers && typeof res.headers === "object") {
|
|
287
298
|
res.headers["X-Request-Id"] = requestId;
|
|
288
299
|
}
|
|
300
|
+
let pathname;
|
|
301
|
+
try {
|
|
302
|
+
pathname = requestPath(req.url);
|
|
303
|
+
} catch {
|
|
304
|
+
res.writeHead(400, { "Content-Type": "application/json", Connection: "close" });
|
|
305
|
+
res.end(JSON.stringify({ error: "Invalid request target" }));
|
|
306
|
+
return;
|
|
307
|
+
}
|
|
289
308
|
|
|
290
309
|
const trackRequest = pathname !== ADAPTER_HEALTH_PATH && pathname !== ADAPTER_STATUS_PATH;
|
|
291
310
|
const routeName = classifyAdapterRoute(req.method, pathname);
|
|
@@ -21,20 +21,22 @@ import { safeUpstreamResponseHeaders } from "./upstream-headers.mjs";
|
|
|
21
21
|
import { recordAnthropicUsage } from "./usage.mjs";
|
|
22
22
|
|
|
23
23
|
export function createAnthropicCountTokensHandler(options) {
|
|
24
|
-
const { acquireRequest } = options;
|
|
24
|
+
const { acquireRequest, requestBodyTimeoutMs } = options;
|
|
25
25
|
|
|
26
26
|
return async function handleAnthropicCountTokens(req, res) {
|
|
27
27
|
const abort = createRequestAbort(req, res);
|
|
28
28
|
let releaseRequest = () => {};
|
|
29
29
|
try {
|
|
30
30
|
releaseRequest = await acquireRequest(req, { signal: abort.signal });
|
|
31
|
-
|
|
31
|
+
abort.setTimeout(requestBodyTimeoutMs, "request_body_timeout");
|
|
32
|
+
const parsed = await readJsonBody(req, { admission: releaseRequest, signal: abort.signal });
|
|
33
|
+
abort.clearTimeout();
|
|
32
34
|
const result = await countTokens(parsed);
|
|
33
35
|
releaseRequest();
|
|
34
36
|
res.writeHead(200, { "Content-Type": "application/json" });
|
|
35
37
|
res.end(JSON.stringify(result));
|
|
36
38
|
} catch (error) {
|
|
37
|
-
sendJsonError(res, error);
|
|
39
|
+
sendJsonError(res, error, abort.reason === "request_body_timeout" ? 408 : 400);
|
|
38
40
|
} finally {
|
|
39
41
|
releaseRequest();
|
|
40
42
|
abort.cleanup();
|
|
@@ -48,6 +50,7 @@ export function createAnthropicMessagesHandler(options) {
|
|
|
48
50
|
chatCompletionsFn,
|
|
49
51
|
environment,
|
|
50
52
|
modelOptions,
|
|
53
|
+
requestBodyTimeoutMs,
|
|
51
54
|
streamHandshakeTimeoutMs,
|
|
52
55
|
streamIdleTimeoutMs,
|
|
53
56
|
upstreamTimeoutMs,
|
|
@@ -58,7 +61,8 @@ export function createAnthropicMessagesHandler(options) {
|
|
|
58
61
|
let releaseRequest = () => {};
|
|
59
62
|
try {
|
|
60
63
|
releaseRequest = await acquireRequest(req, { signal: abort.signal });
|
|
61
|
-
|
|
64
|
+
abort.setTimeout(requestBodyTimeoutMs, "request_body_timeout");
|
|
65
|
+
const parsed = await readJsonBody(req, { admission: releaseRequest, signal: abort.signal });
|
|
62
66
|
abort.setTimeout(
|
|
63
67
|
parsed.stream ? streamHandshakeTimeoutMs : upstreamTimeoutMs,
|
|
64
68
|
parsed.stream ? "stream_handshake_timeout" : "upstream_timeout",
|
package/src/cli-options.mjs
CHANGED
|
@@ -5,7 +5,6 @@ const START_OPTIONS = new Map([
|
|
|
5
5
|
["--configure-claude-app", "configure-claude-app"],
|
|
6
6
|
["--show-request-id", "show-request-id"],
|
|
7
7
|
]);
|
|
8
|
-
const CLI_COMMANDS = new Set(["ccdx", "codex-copilot-dx"]);
|
|
9
8
|
const PM_COMMANDS = new Set(["pms", "pm-studio"]);
|
|
10
9
|
const HELP_TOPICS = new Set([
|
|
11
10
|
"",
|
|
@@ -253,10 +252,8 @@ export function parseAdapterProbeOptions(env = process.env) {
|
|
|
253
252
|
};
|
|
254
253
|
}
|
|
255
254
|
|
|
256
|
-
export function cliCommandName(
|
|
257
|
-
|
|
258
|
-
if (name === "codex-copilot-dx.mjs") return "codex-copilot-dx";
|
|
259
|
-
return CLI_COMMANDS.has(name) ? name : "ccdx";
|
|
255
|
+
export function cliCommandName() {
|
|
256
|
+
return "ccdx";
|
|
260
257
|
}
|
|
261
258
|
|
|
262
259
|
function topicHelp(name, topic) {
|
|
@@ -280,10 +277,9 @@ function topicHelp(name, topic) {
|
|
|
280
277
|
}
|
|
281
278
|
|
|
282
279
|
export function cliHelp(commandName = "ccdx", topic = "") {
|
|
283
|
-
const name =
|
|
284
|
-
const alias = name === "ccdx" ? "codex-copilot-dx" : "ccdx";
|
|
280
|
+
const name = "ccdx";
|
|
285
281
|
const normalizedTopic = checkedHelpTopic(topic ? topic.split(" ") : []);
|
|
286
|
-
if (normalizedTopic) return
|
|
282
|
+
if (normalizedTopic) return topicHelp(name, normalizedTopic);
|
|
287
283
|
return `Usage:
|
|
288
284
|
${name} [start] [--configure-claude-app] [--show-request-id]
|
|
289
285
|
${name} auth status [--online]
|
|
@@ -310,7 +306,5 @@ Commands:
|
|
|
310
306
|
auto-review-model Select the Codex Auto-review Responses model
|
|
311
307
|
update Update the global package from npm or GitHub
|
|
312
308
|
|
|
313
|
-
Run ${name} <command> --help for command details
|
|
314
|
-
|
|
315
|
-
Equivalent command: ${alias}`;
|
|
309
|
+
Run ${name} <command> --help for command details.`;
|
|
316
310
|
}
|
package/src/cli-status.mjs
CHANGED
|
@@ -149,6 +149,7 @@ export function formatAdapterStatus({ baseUrl, data }, { commandName = "ccdx", c
|
|
|
149
149
|
const admission = data.admission || {};
|
|
150
150
|
const history = data.response_history || {};
|
|
151
151
|
const images = data.image_optimization || {};
|
|
152
|
+
const imageHistory = data.image_history_pressure;
|
|
152
153
|
const models = data.models || {};
|
|
153
154
|
const copilot = data.copilot || {};
|
|
154
155
|
const limits = data.limits || {};
|
|
@@ -166,8 +167,11 @@ export function formatAdapterStatus({ baseUrl, data }, { commandName = "ccdx", c
|
|
|
166
167
|
status("info", `Memory: RSS ${mebibytes(processStats.rss_bytes)}, heap ${mebibytes(processStats.heap_used_bytes)}`),
|
|
167
168
|
status("info", `History: ${mebibytes(history.bytes)} / ${mebibytes(limits.response_history_max_bytes)}, ${count(history.entries)} entries, ${count(history.evicted)} evicted`),
|
|
168
169
|
status("info", `Image cache: ${count(images.cache_entries)} entries, ${cacheHitRate(images)} hits, ${mebibytes(images.cache_bytes)} / ${mebibytes(images.cache_max_bytes)}`),
|
|
169
|
-
status("info", `Models: ${count(models.models)} total, ${count(models.claude_models)} Claude; Copilot token ${copilot.token_cached ? `cached (${duration(copilot.token_expires_in_ms)} remaining)` : "not cached"}`),
|
|
170
170
|
];
|
|
171
|
+
if (imageHistory && typeof imageHistory === "object") {
|
|
172
|
+
lines.push(status("info", `Visual history: ${count(imageHistory.active_recovery_trees)} recovery trees, ${count(imageHistory.adapted_requests)} adapted requests, ${count(imageHistory.historical_images_omitted)} older images omitted, ${count(imageHistory.timeouts_recorded)} timeouts`));
|
|
173
|
+
}
|
|
174
|
+
lines.push(status("info", `Models: ${count(models.models)} total, ${count(models.claude_models)} Claude; Copilot token ${copilot.token_cached ? `cached (${duration(copilot.token_expires_in_ms)} remaining)` : "not cached"}`));
|
|
171
175
|
if (data.profiles?.codex || data.profiles?.claude) {
|
|
172
176
|
lines.push(status("info", `Profiles: ${profileSummary("Codex", data.profiles?.codex)}; ${profileSummary("Claude", data.profiles?.claude)}`));
|
|
173
177
|
}
|
package/src/copilot-client.mjs
CHANGED
|
@@ -85,6 +85,7 @@ export function createCopilotClientRuntime({
|
|
|
85
85
|
fetchImpl,
|
|
86
86
|
retryOptions,
|
|
87
87
|
bodyText,
|
|
88
|
+
onUpstreamStart,
|
|
88
89
|
} = {}) {
|
|
89
90
|
const token = await tokenSession.getToken({ signal });
|
|
90
91
|
const upstreamReq = chatReq.stream === true ? withChatStreamUsage(chatReq) : chatReq;
|
|
@@ -99,6 +100,7 @@ export function createCopilotClientRuntime({
|
|
|
99
100
|
vision: computeVision(messages),
|
|
100
101
|
});
|
|
101
102
|
headers["Content-Length"] = String(Buffer.byteLength(serializedBody));
|
|
103
|
+
onUpstreamStart?.();
|
|
102
104
|
if (upstreamReq.stream === true) markUpstreamStarted();
|
|
103
105
|
return fetchCopilotUpstream(`${tokenSession.getApiBase()}/chat/completions`, {
|
|
104
106
|
method: "POST",
|
|
@@ -152,6 +154,7 @@ export function createCopilotClientRuntime({
|
|
|
152
154
|
fetchImpl,
|
|
153
155
|
retryOptions,
|
|
154
156
|
currentInputStart = 0,
|
|
157
|
+
onUpstreamStart,
|
|
155
158
|
payloadPrepared = false,
|
|
156
159
|
payloadOptions = {},
|
|
157
160
|
} = {}) {
|
|
@@ -175,6 +178,7 @@ export function createCopilotClientRuntime({
|
|
|
175
178
|
headers["Content-Length"] = String(bodyBytes);
|
|
176
179
|
headers.Accept = reqBody.stream ? "text/event-stream" : "application/json";
|
|
177
180
|
try {
|
|
181
|
+
onUpstreamStart?.();
|
|
178
182
|
if (reqBody.stream === true) markUpstreamStarted();
|
|
179
183
|
return await fetchCopilotUpstream(`${tokenSession.getApiBase()}${responsesEndpointPath()}`, {
|
|
180
184
|
method: "POST",
|
|
@@ -183,6 +187,7 @@ export function createCopilotClientRuntime({
|
|
|
183
187
|
signal,
|
|
184
188
|
}, { fetchImpl, ...retryOptions });
|
|
185
189
|
} catch (error) {
|
|
190
|
+
if (signal?.aborted) throw abortError(signal);
|
|
186
191
|
const cause = error?.cause;
|
|
187
192
|
const causeText = cause ? ` (${[cause.code, cause.message].filter(Boolean).join(": ")})` : "";
|
|
188
193
|
throw new Error(`Copilot responses fetch failed: ${error.message}${causeText}`);
|