codex-copilot-dx 0.6.1 → 0.6.3
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 +42 -21
- package/bin/cli.mjs +12 -4
- package/bin/codex-copilot-dx.mjs +2 -1
- package/package.json +1 -1
- package/scripts/benchmark.mjs +95 -0
- package/src/adapter.mjs +5 -0
- package/src/auth.mjs +5 -0
- package/src/cli-auth.mjs +43 -12
- package/src/cli-options.mjs +141 -59
- package/src/cli-status.mjs +40 -3
- package/src/copilot-client.mjs +5 -0
- package/src/doctor.mjs +56 -3
- package/src/http-transport.mjs +1 -1
- package/src/observability.mjs +4 -4
- package/src/pm-studio-relay.mjs +14 -48
- package/src/pm-studio-status.mjs +130 -0
- package/src/profile-model-runtime.mjs +3 -2
- package/src/profile-routing.mjs +74 -0
- package/src/profile-runtime.mjs +13 -0
- package/src/response-history.mjs +4 -0
- package/src/responses-bridge.mjs +12 -6
- package/src/responses-byte-budget.mjs +63 -5
- package/src/responses-handler.mjs +60 -16
- package/src/responses-image-pressure.mjs +163 -0
- package/src/responses-proxy.mjs +7 -4
- package/src/responses-request.mjs +9 -1
package/README.md
CHANGED
|
@@ -29,11 +29,13 @@ 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 and runs `ccdx`, and will be removed in a future
|
|
35
|
+
breaking release. For a one-off run without a global install, use
|
|
36
|
+
`npm exec --yes --package=codex-copilot-dx@latest -- ccdx`.
|
|
37
|
+
Run `ccdx <command> --help` (including `auth login claude --help` and
|
|
38
|
+
`pms status --help`) for command-specific behavior and side-effect details.
|
|
37
39
|
|
|
38
40
|
To query the models currently advertised as selectable for the saved Codex
|
|
39
41
|
Copilot account, run:
|
|
@@ -47,8 +49,7 @@ starting the adapter, opening device login, reading the local model cache, or
|
|
|
47
49
|
consuming an inference request. It lists model IDs by provider with their
|
|
48
50
|
advertised API capabilities and reports the live Claude/Anthropic count. A model
|
|
49
51
|
being advertised does not guarantee that a later inference request will avoid
|
|
50
|
-
quota, rate-limit, or policy enforcement.
|
|
51
|
-
equivalent.
|
|
52
|
+
quota, rate-limit, or policy enforcement.
|
|
52
53
|
|
|
53
54
|
### Separate GitHub account for Claude
|
|
54
55
|
|
|
@@ -60,11 +61,14 @@ while using a different personal account that has Claude models enabled, run:
|
|
|
60
61
|
ccdx auth login claude --github-login <personal-github-login>
|
|
61
62
|
```
|
|
62
63
|
|
|
63
|
-
This is the only command that
|
|
64
|
+
This is the only command that may start the additional Device Flow. Before
|
|
65
|
+
opening a browser, it checks compatible local Copilot credentials, excludes the
|
|
66
|
+
current Codex identity, and reuses an unambiguous valid account. It then verifies
|
|
64
67
|
the selected GitHub identity, Copilot entitlement, and an enabled Claude model
|
|
65
68
|
before saving anything. It refuses the current Codex account and never changes,
|
|
66
69
|
migrates, or deletes the existing Codex credential. The optional login pin is
|
|
67
|
-
recommended so
|
|
70
|
+
recommended so a machine with multiple accounts selects only the requested
|
|
71
|
+
identity. Use `--reauth` to bypass local discovery and force Device Flow.
|
|
68
72
|
|
|
69
73
|
The existing Codex token remains at
|
|
70
74
|
`~/.local/share/copilot-api/github_token`. The isolated Claude credential is
|
|
@@ -80,7 +84,8 @@ Once enabled, routing is fixed by API surface rather than by model name:
|
|
|
80
84
|
- `/v1/messages/count_tokens` remains local and uses neither account.
|
|
81
85
|
|
|
82
86
|
The two profiles keep separate service tokens, API bases, refresh/backoff state,
|
|
83
|
-
model endpoint metadata, and model caches.
|
|
87
|
+
model endpoint metadata, and credential-bound model caches. Switching either
|
|
88
|
+
GitHub account invalidates that profile's old cache instead of borrowing it. If the isolated Claude profile later
|
|
84
89
|
becomes invalid, Claude requests fail with a reauthentication instruction while
|
|
85
90
|
Codex remains available; CCDX never silently falls back to the enterprise
|
|
86
91
|
account. Reauthorize it explicitly with:
|
|
@@ -114,6 +119,15 @@ ccdx auth login claude --reauth --github-login <personal-github-login>
|
|
|
114
119
|
ccdx pms setup
|
|
115
120
|
```
|
|
116
121
|
|
|
122
|
+
Inspect the installed version, exact patch integrity, isolated Claude profile,
|
|
123
|
+
and relay without changing files:
|
|
124
|
+
|
|
125
|
+
```bash
|
|
126
|
+
ccdx pms status
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
`pm-studio status` and `pm-studio setup` are equivalent long-form aliases.
|
|
130
|
+
|
|
117
131
|
`ccdx pms setup` performs only local preflight, backup, staging, integrity,
|
|
118
132
|
signing, and replacement work. It never starts Device Flow, the adapter,
|
|
119
133
|
Codex, or PM Studio, and it does not modify PM profiles, cached model lists, or
|
|
@@ -178,7 +192,7 @@ It saves the selection in `~/.config/codex-copilot-dx/config.json` (or under
|
|
|
178
192
|
`XDG_CONFIG_HOME` when set); choosing `gpt-5.5` clears the override and restores
|
|
179
193
|
the package default. A running 0.5.1+
|
|
180
194
|
adapter reads the setting on the next Auto-review request, so it does not need to
|
|
181
|
-
be restarted.
|
|
195
|
+
be restarted.
|
|
182
196
|
|
|
183
197
|
To update the globally installed package, choose a source interactively:
|
|
184
198
|
|
|
@@ -201,8 +215,7 @@ latest commit from `DaleXiao/codex-copilot-dx` `main` and opts in to Git fetchin
|
|
|
201
215
|
for that command, as required by npm 12. It does not change npm's persistent
|
|
202
216
|
`allow-git` setting. Both paths use npm's global installer without shell
|
|
203
217
|
interpolation. A currently running adapter keeps its loaded version until it is
|
|
204
|
-
stopped and started again.
|
|
205
|
-
`codex-copilot-dx update` compatibility command behaves identically.
|
|
218
|
+
stopped and started again.
|
|
206
219
|
If a configured npm mirror has not synchronized the current release yet, use
|
|
207
220
|
the GitHub source to install the current `main` revision.
|
|
208
221
|
|
|
@@ -232,7 +245,11 @@ ccdx doctor
|
|
|
232
245
|
|
|
233
246
|
The command exits with status `1` when it finds an invalid configuration and `0` when checks contain only OK or warning results.
|
|
234
247
|
|
|
235
|
-
The doctor checks the GitHub token, Codex config, Claude Code settings, Claude
|
|
248
|
+
The doctor checks the GitHub token, Codex config, Claude Code settings, Claude
|
|
249
|
+
App gateway profile, adapter availability, and an installed PM Studio bundle.
|
|
250
|
+
It ends with passed/warning/error totals and deduplicated next-step commands.
|
|
251
|
+
An absent PM Studio installation is ignored; unsupported versions are reported
|
|
252
|
+
without modification, while integrity drift remains an error.
|
|
236
253
|
|
|
237
254
|
For a read-only live check of the saved GitHub token, Copilot entitlement, and models endpoint, run:
|
|
238
255
|
|
|
@@ -259,15 +276,13 @@ For a read-only summary of the running adapter, use:
|
|
|
259
276
|
ccdx status
|
|
260
277
|
```
|
|
261
278
|
|
|
262
|
-
|
|
263
|
-
`codex-copilot-dx status`. For the complete machine-readable payload, query the
|
|
264
|
-
loopback endpoint directly:
|
|
279
|
+
For the complete machine-readable payload, query the loopback endpoint directly:
|
|
265
280
|
|
|
266
281
|
```bash
|
|
267
282
|
curl -s http://127.0.0.1:2026/_ccdx/status | jq
|
|
268
283
|
```
|
|
269
284
|
|
|
270
|
-
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, model cache counts, 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. To include the same ID as `request_id` in related terminal and file log lines, start the adapter with:
|
|
285
|
+
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:
|
|
271
286
|
|
|
272
287
|
```bash
|
|
273
288
|
ccdx --show-request-id
|
|
@@ -287,13 +302,17 @@ ccdx
|
|
|
287
302
|
Claude App support is opt-in so the default Codex Desktop and Claude Code setup stays unchanged:
|
|
288
303
|
|
|
289
304
|
```bash
|
|
290
|
-
|
|
305
|
+
npm exec --yes --package=codex-copilot-dx@latest -- ccdx start --configure-claude-app
|
|
291
306
|
```
|
|
292
307
|
|
|
308
|
+
The previous `--configure-claude-desktop` spelling remains a compatibility
|
|
309
|
+
alias. Startup reports Claude App as ready only when a matching managed gateway
|
|
310
|
+
key is restored or the profile is configured in that run.
|
|
311
|
+
|
|
293
312
|
Or set:
|
|
294
313
|
|
|
295
314
|
```bash
|
|
296
|
-
CCDX_CONFIGURE_CLAUDE_DESKTOP=1
|
|
315
|
+
CCDX_CONFIGURE_CLAUDE_DESKTOP=1 npm exec --yes --package=codex-copilot-dx@latest -- ccdx
|
|
297
316
|
```
|
|
298
317
|
|
|
299
318
|
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.
|
|
@@ -369,7 +388,7 @@ Set `CCDX_LOG_PATH=1` to mirror terminal logs to `~/.local/share/codex-copilot-d
|
|
|
369
388
|
|
|
370
389
|
### Image optimization
|
|
371
390
|
|
|
372
|
-
Long computer-use sessions can accumulate screenshots inside the conversation history. Each screenshot is shipped on later turns,
|
|
391
|
+
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.
|
|
373
392
|
|
|
374
393
|
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.
|
|
375
394
|
|
|
@@ -377,6 +396,8 @@ The final serialized UTF-8 request body is measured before forwarding. Above the
|
|
|
377
396
|
|
|
378
397
|
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.
|
|
379
398
|
|
|
399
|
+
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.
|
|
400
|
+
|
|
380
401
|
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.
|
|
381
402
|
|
|
382
403
|
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/cli.mjs
CHANGED
|
@@ -39,7 +39,7 @@ try {
|
|
|
39
39
|
}
|
|
40
40
|
|
|
41
41
|
if (CLI.command === "help") {
|
|
42
|
-
console.log(cliHelp(CLI_NAME));
|
|
42
|
+
console.log(cliHelp(CLI_NAME, CLI.helpTopic));
|
|
43
43
|
process.exit(0);
|
|
44
44
|
}
|
|
45
45
|
if (CLI.command === "version") {
|
|
@@ -125,6 +125,11 @@ if (CLI.command === "update") {
|
|
|
125
125
|
}
|
|
126
126
|
if (CLI.command === "pms") {
|
|
127
127
|
try {
|
|
128
|
+
if (CLI.action === "status") {
|
|
129
|
+
const { runPmStudioStatus } = await import("../src/pm-studio-status.mjs");
|
|
130
|
+
const result = await runPmStudioStatus({ commandName: CLI_NAME });
|
|
131
|
+
process.exit(result.ok ? 0 : 1);
|
|
132
|
+
}
|
|
128
133
|
const { runPmStudioSetup } = await import("../src/pm-studio-setup.mjs");
|
|
129
134
|
await runPmStudioSetup({ commandName: CLI_NAME });
|
|
130
135
|
process.exit(0);
|
|
@@ -206,7 +211,7 @@ async function reuseRunningAdapterIfAvailable() {
|
|
|
206
211
|
console.log(status("warn", "Existing adapter is running; skip Claude App profile update unless CCDX_CLAUDE_DESKTOP_API_KEY or CCDX_PROXY_API_KEY is set"));
|
|
207
212
|
}
|
|
208
213
|
} else {
|
|
209
|
-
console.log(status("ok", "Claude App support available with --configure-claude-
|
|
214
|
+
console.log(status("ok", "Claude App support available with --configure-claude-app"));
|
|
210
215
|
}
|
|
211
216
|
|
|
212
217
|
await openCodex();
|
|
@@ -270,6 +275,7 @@ try {
|
|
|
270
275
|
codexClient: profileRuntime.codexClient,
|
|
271
276
|
claudeClient: profileRuntime.claudeClient,
|
|
272
277
|
claudeMode: profileRuntime.claudeMode,
|
|
278
|
+
codexCredentialFingerprint: profileRuntime.codexCredentialFingerprint,
|
|
273
279
|
claudeCredentialFingerprint: profileRuntime.claudeCredentialFingerprint,
|
|
274
280
|
timeoutMs: MODEL_REFRESH_TIMEOUT_MS,
|
|
275
281
|
commandName: CLI_NAME,
|
|
@@ -329,7 +335,7 @@ try {
|
|
|
329
335
|
console.log(status("ok", `Configured Claude App gateway profile at ${result.baseUrl}`));
|
|
330
336
|
console.log(formatClaudeDesktopApplyResult(result));
|
|
331
337
|
} else {
|
|
332
|
-
console.log(status("ok", "Claude App support available with --configure-claude-
|
|
338
|
+
console.log(status("ok", "Claude App support available with --configure-claude-app"));
|
|
333
339
|
}
|
|
334
340
|
|
|
335
341
|
// Launch Codex when available.
|
|
@@ -345,8 +351,10 @@ try {
|
|
|
345
351
|
modelRefreshTimer.unref?.();
|
|
346
352
|
}
|
|
347
353
|
|
|
354
|
+
const readyClients = ["Codex App", "Claude Code"];
|
|
355
|
+
if (claudeDesktopApiKey) readyClients.splice(1, 0, "Claude App");
|
|
348
356
|
console.log(`
|
|
349
|
-
${status("ok",
|
|
357
|
+
${status("ok", `Ready, ${readyClients.join(", ")} ${readyClients.length === 1 ? "is" : "are"} ready to use`)}
|
|
350
358
|
|
|
351
359
|
Adapter: ${adapterBaseUrl(ADAPTER_HOST, ADAPTER_PORT)}
|
|
352
360
|
|
package/bin/codex-copilot-dx.mjs
CHANGED
package/package.json
CHANGED
package/scripts/benchmark.mjs
CHANGED
|
@@ -8,6 +8,7 @@ import { setTimeout as delay } from "node:timers/promises";
|
|
|
8
8
|
import { fileURLToPath } from "node:url";
|
|
9
9
|
import sharp from "sharp";
|
|
10
10
|
import { optimizeImagesInBody, prepareResponsesPayload } from "../src/image-optimization.mjs";
|
|
11
|
+
import { createPmStudioModelRouter } from "../src/profile-routing.mjs";
|
|
11
12
|
|
|
12
13
|
const packageRoot = fileURLToPath(new URL("../", import.meta.url));
|
|
13
14
|
|
|
@@ -176,6 +177,60 @@ function largeTokenCountProbe(mode) {
|
|
|
176
177
|
`);
|
|
177
178
|
}
|
|
178
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
|
+
|
|
179
234
|
const adapterImport = runProbe(`
|
|
180
235
|
const started = performance.now();
|
|
181
236
|
await import("./src/adapter.mjs");
|
|
@@ -235,6 +290,34 @@ const duplicateImages = {
|
|
|
235
290
|
optimizer_calls: duplicateCalls,
|
|
236
291
|
};
|
|
237
292
|
|
|
293
|
+
const routingCatalog = {
|
|
294
|
+
data: Array.from({ length: 1000 }, (_, index) => index % 5 === 0
|
|
295
|
+
? {
|
|
296
|
+
id: `claude-benchmark-${index}`,
|
|
297
|
+
vendor: "Anthropic",
|
|
298
|
+
model_picker_enabled: true,
|
|
299
|
+
supported_endpoints: ["/chat/completions"],
|
|
300
|
+
}
|
|
301
|
+
: { id: `gpt-benchmark-${index}`, vendor: "OpenAI", supported_endpoints: ["/responses"] }),
|
|
302
|
+
};
|
|
303
|
+
const modelRouter = createPmStudioModelRouter({
|
|
304
|
+
getCatalog: () => routingCatalog,
|
|
305
|
+
isClaudeEnabled: () => true,
|
|
306
|
+
});
|
|
307
|
+
const routingIterations = 100000;
|
|
308
|
+
const routingStarted = performance.now();
|
|
309
|
+
let claudeClassifications = 0;
|
|
310
|
+
for (let index = 0; index < routingIterations; index += 1) {
|
|
311
|
+
if (modelRouter.classify("claude-benchmark-0") === "claude") claudeClassifications += 1;
|
|
312
|
+
}
|
|
313
|
+
const pmModelRouting = {
|
|
314
|
+
catalog_models: routingCatalog.data.length,
|
|
315
|
+
iterations: routingIterations,
|
|
316
|
+
elapsed_ms: +(performance.now() - routingStarted).toFixed(1),
|
|
317
|
+
claude_classifications: claudeClassifications,
|
|
318
|
+
availability_rebuilds: modelRouter.diagnostics().rebuilds,
|
|
319
|
+
};
|
|
320
|
+
|
|
238
321
|
const pixels = deterministicPixels(1024 * 1024 * 3, 0x87654321);
|
|
239
322
|
const webp = await sharp(pixels, { raw: { width: 1024, height: 1024, channels: 3 } })
|
|
240
323
|
.webp({ quality: 100 })
|
|
@@ -281,6 +364,7 @@ const report = {
|
|
|
281
364
|
adapter_import: adapterImport,
|
|
282
365
|
token_count_100kb: tokenCount,
|
|
283
366
|
duplicate_images: duplicateImages,
|
|
367
|
+
pm_model_routing: pmModelRouting,
|
|
284
368
|
oversized_payload: oversizedPayload,
|
|
285
369
|
};
|
|
286
370
|
|
|
@@ -294,6 +378,7 @@ if (checkMode) {
|
|
|
294
378
|
largePayloadProbe(30, 1),
|
|
295
379
|
largePayloadProbe(30, 2),
|
|
296
380
|
];
|
|
381
|
+
report.visual_history_pressure_36x4 = visualHistoryPressureProbe();
|
|
297
382
|
} else if (process.argv.includes("--large-payload")) {
|
|
298
383
|
report.large_payload_peak = [
|
|
299
384
|
largePayloadProbe(5, 1),
|
|
@@ -308,6 +393,10 @@ console.log(JSON.stringify(report, null, 2));
|
|
|
308
393
|
|
|
309
394
|
if (checkMode) {
|
|
310
395
|
const failures = [];
|
|
396
|
+
if (report.pm_model_routing.availability_rebuilds !== 1
|
|
397
|
+
|| report.pm_model_routing.claude_classifications !== report.pm_model_routing.iterations) {
|
|
398
|
+
failures.push("PM Studio model routing did not reuse the memoized catalog classification");
|
|
399
|
+
}
|
|
311
400
|
const proxy = report.large_token_count.proxy;
|
|
312
401
|
const array = report.large_token_count.materialized_array;
|
|
313
402
|
if (proxy.tokens !== array.tokens) failures.push(`token counts differ: proxy=${proxy.tokens} array=${array.tokens}`);
|
|
@@ -325,6 +414,12 @@ if (checkMode) {
|
|
|
325
414
|
failures.push(`request admission exceeded its budget at ${payload.target_request_mib}MiB x${payload.concurrency}`);
|
|
326
415
|
}
|
|
327
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
|
+
}
|
|
328
423
|
if (failures.length) {
|
|
329
424
|
for (const failure of failures) console.error(`[FAIL] ${failure}`);
|
|
330
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,
|
|
@@ -138,6 +139,7 @@ export function createAdapterHandler(options = {}) {
|
|
|
138
139
|
const acquireRequest = options.acquireRequest || createRequestAdmission();
|
|
139
140
|
const requestMetrics = options.requestMetrics || createRequestMetrics();
|
|
140
141
|
const streamPerformanceMetrics = options.streamPerformanceMetrics || createStreamPerformanceMetrics();
|
|
142
|
+
const imagePressure = options.imagePressure || createResponsesImagePressureController();
|
|
141
143
|
const claudeDesktopModelOptions = () => {
|
|
142
144
|
const modelDefs = claudeModelRegistry?.modelDefs || options.claudeDesktopModelDefs;
|
|
143
145
|
return Array.isArray(modelDefs) ? { modelDefs } : {};
|
|
@@ -147,6 +149,7 @@ export function createAdapterHandler(options = {}) {
|
|
|
147
149
|
autoReviewModelResolver: options.autoReviewModelResolver,
|
|
148
150
|
chatCompletionsFn: codexChatCompletionsFn,
|
|
149
151
|
getCachedModelEndpointsFn,
|
|
152
|
+
imagePressure,
|
|
150
153
|
openAIModelEnv,
|
|
151
154
|
responsesPayloadOptions: options.responsesPayloadOptions,
|
|
152
155
|
responsesFn,
|
|
@@ -157,6 +160,7 @@ export function createAdapterHandler(options = {}) {
|
|
|
157
160
|
const responsesCompactHandler = createResponsesCompactHandler({
|
|
158
161
|
acquireRequest,
|
|
159
162
|
autoReviewModelResolver: options.autoReviewModelResolver,
|
|
163
|
+
imagePressure,
|
|
160
164
|
openAIModelEnv,
|
|
161
165
|
responsesCompactFn,
|
|
162
166
|
streamHandshakeTimeoutMs,
|
|
@@ -197,6 +201,7 @@ export function createAdapterHandler(options = {}) {
|
|
|
197
201
|
metrics: requestMetrics,
|
|
198
202
|
streamPerformance: streamPerformanceMetrics,
|
|
199
203
|
admission: acquireRequest,
|
|
204
|
+
imagePressure,
|
|
200
205
|
modelRegistry: codexModelRegistry,
|
|
201
206
|
codexClient,
|
|
202
207
|
claudeClient,
|
package/src/auth.mjs
CHANGED
|
@@ -326,6 +326,7 @@ export async function discoverGithubToken({
|
|
|
326
326
|
fetchImpl = fetch,
|
|
327
327
|
signal,
|
|
328
328
|
excludeTokens = [],
|
|
329
|
+
excludeIdentities = [],
|
|
329
330
|
expectedIdentity,
|
|
330
331
|
strictExpectedIdentity = false,
|
|
331
332
|
} = {}) {
|
|
@@ -348,6 +349,10 @@ export async function discoverGithubToken({
|
|
|
348
349
|
|
|
349
350
|
const validation = await validateGithubToken(token, { fetchImpl, signal });
|
|
350
351
|
if (validation.ok) {
|
|
352
|
+
if (excludeIdentities.some((identity) => githubIdentityMatchesExpected(validation, identity))) {
|
|
353
|
+
failures.push({ source, validation, reason: "github_account_excluded" });
|
|
354
|
+
continue;
|
|
355
|
+
}
|
|
351
356
|
const candidate = { ok: true, token, source, validation };
|
|
352
357
|
const explicitSource = source.type === "env" || source.type === "token-file";
|
|
353
358
|
if (expectedIdentity && (strictExpectedIdentity || !explicitSource)) {
|
package/src/cli-auth.mjs
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import os from "node:os";
|
|
2
2
|
import { spawn } from "node:child_process";
|
|
3
3
|
import {
|
|
4
|
+
discoverGithubToken,
|
|
4
5
|
fetchGithubIdentity,
|
|
5
6
|
interpretPoll,
|
|
7
|
+
sourceDescription,
|
|
6
8
|
validateGithubToken,
|
|
7
9
|
} from "./auth.mjs";
|
|
8
10
|
import {
|
|
@@ -19,6 +21,7 @@ import {
|
|
|
19
21
|
normalizeGithubIdentity,
|
|
20
22
|
} from "./github-identity.mjs";
|
|
21
23
|
import { saveModelCache } from "./model-cache.mjs";
|
|
24
|
+
import { profileRouting } from "./profile-routing.mjs";
|
|
22
25
|
import { status } from "./status.mjs";
|
|
23
26
|
|
|
24
27
|
const CLIENT_ID = "Iv1.b507a08c87ecfe98";
|
|
@@ -267,14 +270,45 @@ export async function authorizeClaudeProfile({
|
|
|
267
270
|
throw new Error(`Claude profile is configured but invalid (${current.reason}); run ccdx auth login claude --reauth`);
|
|
268
271
|
}
|
|
269
272
|
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
273
|
+
let githubToken = "";
|
|
274
|
+
if (!reauth) {
|
|
275
|
+
const codex = readAuthProfileCredentials(AUTH_PROFILE_CODEX, { home });
|
|
276
|
+
if (!codex.valid) {
|
|
277
|
+
throw new Error("The existing Codex GitHub account is not configured; refusing to create an unbound Claude profile");
|
|
278
|
+
}
|
|
279
|
+
const existingCodex = await codexIdentity({ home, fetchImpl, signal });
|
|
280
|
+
const pinnedLogin = String(expectedLogin || "").trim();
|
|
281
|
+
const discovered = await discoverGithubToken({
|
|
282
|
+
home,
|
|
283
|
+
env,
|
|
284
|
+
fetchImpl,
|
|
285
|
+
signal,
|
|
286
|
+
excludeTokens: [codex.token],
|
|
287
|
+
excludeIdentities: [existingCodex.identity],
|
|
288
|
+
expectedIdentity: pinnedLogin ? { login: pinnedLogin } : undefined,
|
|
289
|
+
strictExpectedIdentity: Boolean(pinnedLogin),
|
|
290
|
+
});
|
|
291
|
+
if (discovered.ambiguous) {
|
|
292
|
+
const accounts = [...new Set(discovered.candidates
|
|
293
|
+
.map((candidate) => candidate.login || candidate.id || "unknown"))];
|
|
294
|
+
throw new Error(`Multiple reusable GitHub Copilot accounts were found (${accounts.join(", ")}). Use --github-login or CCDX_GITHUB_TOKEN_PATH to select the Claude account.`);
|
|
295
|
+
}
|
|
296
|
+
if (discovered.ok) {
|
|
297
|
+
githubToken = discovered.token;
|
|
298
|
+
const login = discovered.validation?.login ? ` for ${discovered.validation.login}` : "";
|
|
299
|
+
log(status("info", `Reusing a local Copilot credential from ${sourceDescription(discovered.source)}${login}`));
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
if (!githubToken) {
|
|
303
|
+
githubToken = await requestDeviceFlowToken({
|
|
304
|
+
fetchImpl,
|
|
305
|
+
signal,
|
|
306
|
+
log,
|
|
307
|
+
openAndCopyFn,
|
|
308
|
+
sleepImpl,
|
|
309
|
+
now,
|
|
310
|
+
});
|
|
311
|
+
}
|
|
278
312
|
const candidate = await validateClaudeCandidate(githubToken, {
|
|
279
313
|
home,
|
|
280
314
|
expectedLogin,
|
|
@@ -350,10 +384,7 @@ export function authStatus({ home = os.homedir() } = {}) {
|
|
|
350
384
|
codex: publicProfile(codex, "legacy"),
|
|
351
385
|
claude: publicProfile(claude, "isolated"),
|
|
352
386
|
},
|
|
353
|
-
routing: {
|
|
354
|
-
responses: AUTH_PROFILE_CODEX,
|
|
355
|
-
messages: claude.configured ? AUTH_PROFILE_CLAUDE : AUTH_PROFILE_CODEX,
|
|
356
|
-
},
|
|
387
|
+
routing: profileRouting({ claudeConfigured: claude.configured }),
|
|
357
388
|
};
|
|
358
389
|
}
|
|
359
390
|
|