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 CHANGED
@@ -29,11 +29,13 @@ npm install -g codex-copilot-dx@latest
29
29
  ccdx
30
30
  ```
31
31
 
32
- The shorter `ccdx` command is the primary launcher. The existing `codex-copilot-dx`
33
- command remains an equivalent compatibility alias. For a one-off run without a
34
- global install, use `npx codex-copilot-dx@latest`.
35
- Both installed commands expose the same options and subcommands; help, version,
36
- doctor, status, and argument-error output use the command name that was invoked.
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. `codex-copilot-dx models` is fully
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 starts the additional Device Flow. It verifies
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 authorizing the wrong browser account fails before activation.
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. If the isolated Claude profile later
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. `codex-copilot-dx auto-review-model` is fully equivalent.
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. The
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 App gateway profile, and whether the local adapter port is listening.
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
- The compatibility alias supports the same command as
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
- npx codex-copilot-dx@latest --configure-claude-desktop
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 npx codex-copilot-dx@latest
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, and GitHub Copilot's `/responses` endpoint can reject oversized requests with `408 Request Timeout` while reading the body or `413 Payload Too Large`.
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-desktop"));
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-desktop"));
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", "Ready, Claude Code, Claude App and Codex App are now ready to use")}
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
 
@@ -1,2 +1,3 @@
1
1
  #!/usr/bin/env node
2
- import "./cli.mjs";
2
+ console.warn("codex-copilot-dx is deprecated; use ccdx instead. The compatibility alias will be removed in a future breaking release.");
3
+ await import("./cli.mjs");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codex-copilot-dx",
3
- "version": "0.6.1",
3
+ "version": "0.6.3",
4
4
  "description": "Use Codex Desktop, Claude Code, and Claude App with GitHub Copilot",
5
5
  "author": "Dale Xiao",
6
6
  "type": "module",
@@ -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
- const githubToken = await requestDeviceFlowToken({
271
- fetchImpl,
272
- signal,
273
- log,
274
- openAndCopyFn,
275
- sleepImpl,
276
- now,
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