codex-copilot-dx 0.6.0 → 0.6.2
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 +100 -8
- package/bin/cli.mjs +23 -4
- package/package.json +1 -1
- package/scripts/benchmark.mjs +34 -0
- package/src/adapter.mjs +16 -0
- package/src/auth.mjs +5 -0
- package/src/cli-auth.mjs +43 -12
- package/src/cli-options.mjs +143 -47
- package/src/cli-status.mjs +35 -2
- package/src/doctor.mjs +56 -3
- package/src/observability.mjs +10 -4
- package/src/pm-studio-asar.mjs +392 -0
- package/src/pm-studio-relay.mjs +523 -0
- package/src/pm-studio-setup.mjs +734 -0
- 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/running-adapter.mjs +16 -0
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# codex-copilot-dx
|
|
2
2
|
|
|
3
|
-
Use [Codex Desktop](https://openai.com/codex), [Claude Code](https://claude.com/claude-code), and optionally Claude App with your **GitHub Copilot** subscription.
|
|
3
|
+
Use [Codex Desktop](https://openai.com/codex), [Claude Code](https://claude.com/claude-code), and optionally Claude App or PM Studio with your **GitHub Copilot** subscription.
|
|
4
4
|
|
|
5
5
|
## How it works
|
|
6
6
|
|
|
@@ -10,6 +10,7 @@ account by default, or over isolated Codex and Claude accounts when configured:
|
|
|
10
10
|
- **Codex** -> OpenAI Responses API (`/v1/responses`, `/v1/responses/compact`); Responses-only models and compaction proxy directly, chat models convert to Chat Completions.
|
|
11
11
|
- **Claude Code** -> Anthropic Messages API (`/v1/messages`, `/v1/messages/count_tokens`), translated to/from Chat Completions.
|
|
12
12
|
- **Claude App** -> optional Claude Desktop App gateway profile using the same local Messages API plus local model discovery for the configured gateway key.
|
|
13
|
+
- **PM Studio** -> optional macOS app patch that keeps the active PM profile for GPT models while routing enabled Claude models through an isolated CCDX Claude account.
|
|
13
14
|
|
|
14
15
|
Supports both HTTP SSE streaming and non-streaming.
|
|
15
16
|
|
|
@@ -19,7 +20,7 @@ Codex Auto-review requests use the hidden `codex-auto-review` model ID. The adap
|
|
|
19
20
|
|
|
20
21
|
- GitHub Copilot subscription (Individual, Business, or Enterprise)
|
|
21
22
|
- Node.js 22.15+ (required for built-in Zstandard request decompression)
|
|
22
|
-
- [Codex Desktop](https://openai.com/codex), [Claude Code](https://claude.com/claude-code), and/or
|
|
23
|
+
- [Codex Desktop](https://openai.com/codex), [Claude Code](https://claude.com/claude-code), Claude App, and/or a supported PM Studio installation
|
|
23
24
|
|
|
24
25
|
## Usage
|
|
25
26
|
|
|
@@ -33,6 +34,8 @@ command remains an equivalent compatibility alias. For a one-off run without a
|
|
|
33
34
|
global install, use `npx codex-copilot-dx@latest`.
|
|
34
35
|
Both installed commands expose the same options and subcommands; help, version,
|
|
35
36
|
doctor, status, and argument-error output use the command name that was invoked.
|
|
37
|
+
Run `ccdx <command> --help` (including `auth login claude --help` and
|
|
38
|
+
`pms status --help`) for command-specific behavior and side-effect details.
|
|
36
39
|
|
|
37
40
|
To query the models currently advertised as selectable for the saved Codex
|
|
38
41
|
Copilot account, run:
|
|
@@ -59,11 +62,14 @@ while using a different personal account that has Claude models enabled, run:
|
|
|
59
62
|
ccdx auth login claude --github-login <personal-github-login>
|
|
60
63
|
```
|
|
61
64
|
|
|
62
|
-
This is the only command that
|
|
65
|
+
This is the only command that may start the additional Device Flow. Before
|
|
66
|
+
opening a browser, it checks compatible local Copilot credentials, excludes the
|
|
67
|
+
current Codex identity, and reuses an unambiguous valid account. It then verifies
|
|
63
68
|
the selected GitHub identity, Copilot entitlement, and an enabled Claude model
|
|
64
69
|
before saving anything. It refuses the current Codex account and never changes,
|
|
65
70
|
migrates, or deletes the existing Codex credential. The optional login pin is
|
|
66
|
-
recommended so
|
|
71
|
+
recommended so a machine with multiple accounts selects only the requested
|
|
72
|
+
identity. Use `--reauth` to bypass local discovery and force Device Flow.
|
|
67
73
|
|
|
68
74
|
The existing Codex token remains at
|
|
69
75
|
`~/.local/share/copilot-api/github_token`. The isolated Claude credential is
|
|
@@ -79,7 +85,8 @@ Once enabled, routing is fixed by API surface rather than by model name:
|
|
|
79
85
|
- `/v1/messages/count_tokens` remains local and uses neither account.
|
|
80
86
|
|
|
81
87
|
The two profiles keep separate service tokens, API bases, refresh/backoff state,
|
|
82
|
-
model endpoint metadata, and model caches.
|
|
88
|
+
model endpoint metadata, and credential-bound model caches. Switching either
|
|
89
|
+
GitHub account invalidates that profile's old cache instead of borrowing it. If the isolated Claude profile later
|
|
83
90
|
becomes invalid, Claude requests fail with a reauthentication instruction while
|
|
84
91
|
Codex remains available; CCDX never silently falls back to the enterprise
|
|
85
92
|
account. Reauthorize it explicitly with:
|
|
@@ -97,6 +104,83 @@ ccdx models --profile claude
|
|
|
97
104
|
ccdx doctor --online --profile all
|
|
98
105
|
```
|
|
99
106
|
|
|
107
|
+
### PM Studio opt-in patch
|
|
108
|
+
|
|
109
|
+
CCDX can add the isolated Claude catalog to PM Studio without switching its
|
|
110
|
+
active account or profile. GPT requests continue to use the bearer supplied by
|
|
111
|
+
the current PM profile; exact enabled Anthropic model IDs use the isolated
|
|
112
|
+
CCDX Claude profile. Neither credential is copied into the other application.
|
|
113
|
+
|
|
114
|
+
The first recipe supports only an exact, unmodified PM Studio 2.9.7 bundle on
|
|
115
|
+
macOS. Authorize the isolated Claude profile first, quit PM Studio and its
|
|
116
|
+
updater, then run the one-time setup command:
|
|
117
|
+
|
|
118
|
+
```bash
|
|
119
|
+
ccdx auth login claude --reauth --github-login <personal-github-login>
|
|
120
|
+
ccdx pms setup
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
Inspect the installed version, exact patch integrity, isolated Claude profile,
|
|
124
|
+
and relay without changing files:
|
|
125
|
+
|
|
126
|
+
```bash
|
|
127
|
+
ccdx pms status
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
`pm-studio status` and `pm-studio setup` are equivalent long-form aliases.
|
|
131
|
+
|
|
132
|
+
`ccdx pms setup` performs only local preflight, backup, staging, integrity,
|
|
133
|
+
signing, and replacement work. It never starts Device Flow, the adapter,
|
|
134
|
+
Codex, or PM Studio, and it does not modify PM profiles, cached model lists, or
|
|
135
|
+
saved PM tokens. Unknown versions, hashes, signatures, partial patches, a
|
|
136
|
+
running PM process, insufficient space, and every pre-replacement verification
|
|
137
|
+
failure leave the installed App unchanged. If the final read-only verification
|
|
138
|
+
after an atomic replacement detects an exceptional filesystem drift, setup
|
|
139
|
+
does not attempt a second automatic write: it preserves the verified backup,
|
|
140
|
+
retains any diagnostic stage still present, and prints recovery paths. Repeating
|
|
141
|
+
setup for the exact installed recipe is idempotent.
|
|
142
|
+
|
|
143
|
+
The patch is App-wide: every PM profile sends its Copilot API traffic through
|
|
144
|
+
the loopback-only `/pm-ccdx/*` relay. Run ordinary `ccdx` before opening PM
|
|
145
|
+
Studio; if port 2026 is unavailable, PM Copilot requests cannot connect. The
|
|
146
|
+
relay explicitly supports only the PM 2.9.7 Copilot paths discovered in the
|
|
147
|
+
validated bundle (`GET /models` and `POST /chat/completions`, `/responses`, and
|
|
148
|
+
`/embeddings`) and is not a general-purpose proxy.
|
|
149
|
+
|
|
150
|
+
Patching replaces the vendor signature with an ad-hoc signature. PM Studio's
|
|
151
|
+
updater is not disabled; an official update may overwrite the patch or may
|
|
152
|
+
require reinstalling the official App. Setup prints the verified backup and
|
|
153
|
+
manifest paths plus the version-matched restore procedure. Never restore a
|
|
154
|
+
2.9.7 backup over a newer PM Studio installation.
|
|
155
|
+
|
|
156
|
+
To restore, first quit PM Studio and its updater. Use the exact verified backup
|
|
157
|
+
path printed by setup as `BACKUP_APP`, then confirm that both the installed App
|
|
158
|
+
and backup still report version/build `2.9.7/2.9.7`:
|
|
159
|
+
|
|
160
|
+
```bash
|
|
161
|
+
BACKUP_APP="<verified-backup-path-from-ccdx-pms-setup>"
|
|
162
|
+
/usr/bin/plutil -extract CFBundleShortVersionString raw -o - "/Applications/PM Studio.app/Contents/Info.plist"
|
|
163
|
+
/usr/bin/plutil -extract CFBundleVersion raw -o - "/Applications/PM Studio.app/Contents/Info.plist"
|
|
164
|
+
/usr/bin/plutil -extract CFBundleShortVersionString raw -o - "$BACKUP_APP/Contents/Info.plist"
|
|
165
|
+
/usr/bin/plutil -extract CFBundleVersion raw -o - "$BACKUP_APP/Contents/Info.plist"
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
Only when all four values match `2.9.7`, move the patched App aside, restore the
|
|
169
|
+
complete vendor-signed backup, and verify it before launch:
|
|
170
|
+
|
|
171
|
+
```bash
|
|
172
|
+
RECOVERY_DIR="$(/usr/bin/mktemp -d '/Applications/CCDX-PM-Studio-recovery.XXXXXX')"
|
|
173
|
+
PATCHED_APP="$RECOVERY_DIR/PM Studio.app"
|
|
174
|
+
mv "/Applications/PM Studio.app" "$PATCHED_APP"
|
|
175
|
+
/usr/bin/ditto --rsrc --extattr --acl "$BACKUP_APP" "/Applications/PM Studio.app"
|
|
176
|
+
/usr/bin/codesign --verify --deep --strict --verbose=2 "/Applications/PM Studio.app"
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
If the version check, copy, or signature verification fails, do not launch the
|
|
180
|
+
restored App. Keep the moved patched bundle at `$PATCHED_APP` and reinstall the official PM Studio
|
|
181
|
+
instead of applying a 2.9.7 backup to unknown content. CCDX never runs these
|
|
182
|
+
restore commands or `sudo` automatically.
|
|
183
|
+
|
|
100
184
|
To change the model used by Codex Auto-review, run:
|
|
101
185
|
|
|
102
186
|
```bash
|
|
@@ -163,7 +247,11 @@ ccdx doctor
|
|
|
163
247
|
|
|
164
248
|
The command exits with status `1` when it finds an invalid configuration and `0` when checks contain only OK or warning results.
|
|
165
249
|
|
|
166
|
-
The doctor checks the GitHub token, Codex config, Claude Code settings, Claude
|
|
250
|
+
The doctor checks the GitHub token, Codex config, Claude Code settings, Claude
|
|
251
|
+
App gateway profile, adapter availability, and an installed PM Studio bundle.
|
|
252
|
+
It ends with passed/warning/error totals and deduplicated next-step commands.
|
|
253
|
+
An absent PM Studio installation is ignored; unsupported versions are reported
|
|
254
|
+
without modification, while integrity drift remains an error.
|
|
167
255
|
|
|
168
256
|
For a read-only live check of the saved GitHub token, Copilot entitlement, and models endpoint, run:
|
|
169
257
|
|
|
@@ -198,7 +286,7 @@ loopback endpoint directly:
|
|
|
198
286
|
curl -s http://127.0.0.1:2026/_ccdx/status | jq
|
|
199
287
|
```
|
|
200
288
|
|
|
201
|
-
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:
|
|
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:
|
|
202
290
|
|
|
203
291
|
```bash
|
|
204
292
|
ccdx --show-request-id
|
|
@@ -218,9 +306,13 @@ ccdx
|
|
|
218
306
|
Claude App support is opt-in so the default Codex Desktop and Claude Code setup stays unchanged:
|
|
219
307
|
|
|
220
308
|
```bash
|
|
221
|
-
npx codex-copilot-dx@latest --configure-claude-
|
|
309
|
+
npx codex-copilot-dx@latest start --configure-claude-app
|
|
222
310
|
```
|
|
223
311
|
|
|
312
|
+
The previous `--configure-claude-desktop` spelling remains a compatibility
|
|
313
|
+
alias. Startup reports Claude App as ready only when a matching managed gateway
|
|
314
|
+
key is restored or the profile is configured in that run.
|
|
315
|
+
|
|
224
316
|
Or set:
|
|
225
317
|
|
|
226
318
|
```bash
|
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") {
|
|
@@ -123,6 +123,21 @@ if (CLI.command === "update") {
|
|
|
123
123
|
process.exit(1);
|
|
124
124
|
}
|
|
125
125
|
}
|
|
126
|
+
if (CLI.command === "pms") {
|
|
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
|
+
}
|
|
133
|
+
const { runPmStudioSetup } = await import("../src/pm-studio-setup.mjs");
|
|
134
|
+
await runPmStudioSetup({ commandName: CLI_NAME });
|
|
135
|
+
process.exit(0);
|
|
136
|
+
} catch (e) {
|
|
137
|
+
console.error(status("err", e.message));
|
|
138
|
+
process.exit(1);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
126
141
|
|
|
127
142
|
let RUNTIME;
|
|
128
143
|
try {
|
|
@@ -196,7 +211,7 @@ async function reuseRunningAdapterIfAvailable() {
|
|
|
196
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"));
|
|
197
212
|
}
|
|
198
213
|
} else {
|
|
199
|
-
console.log(status("ok", "Claude App support available with --configure-claude-
|
|
214
|
+
console.log(status("ok", "Claude App support available with --configure-claude-app"));
|
|
200
215
|
}
|
|
201
216
|
|
|
202
217
|
await openCodex();
|
|
@@ -260,6 +275,7 @@ try {
|
|
|
260
275
|
codexClient: profileRuntime.codexClient,
|
|
261
276
|
claudeClient: profileRuntime.claudeClient,
|
|
262
277
|
claudeMode: profileRuntime.claudeMode,
|
|
278
|
+
codexCredentialFingerprint: profileRuntime.codexCredentialFingerprint,
|
|
263
279
|
claudeCredentialFingerprint: profileRuntime.claudeCredentialFingerprint,
|
|
264
280
|
timeoutMs: MODEL_REFRESH_TIMEOUT_MS,
|
|
265
281
|
commandName: CLI_NAME,
|
|
@@ -296,6 +312,7 @@ try {
|
|
|
296
312
|
autoReviewModelResolver: () => autoReviewModelPreference().model,
|
|
297
313
|
codexClient: profileRuntime.codexClient,
|
|
298
314
|
claudeClient: profileRuntime.claudeClient,
|
|
315
|
+
claudeProfile: profileRuntime.claudeProfile,
|
|
299
316
|
codexModelRegistry: profileModels.codexRegistry,
|
|
300
317
|
claudeModelRegistry: profileModels.claudeRegistry,
|
|
301
318
|
claudeMode: profileRuntime.claudeMode,
|
|
@@ -318,7 +335,7 @@ try {
|
|
|
318
335
|
console.log(status("ok", `Configured Claude App gateway profile at ${result.baseUrl}`));
|
|
319
336
|
console.log(formatClaudeDesktopApplyResult(result));
|
|
320
337
|
} else {
|
|
321
|
-
console.log(status("ok", "Claude App support available with --configure-claude-
|
|
338
|
+
console.log(status("ok", "Claude App support available with --configure-claude-app"));
|
|
322
339
|
}
|
|
323
340
|
|
|
324
341
|
// Launch Codex when available.
|
|
@@ -334,8 +351,10 @@ try {
|
|
|
334
351
|
modelRefreshTimer.unref?.();
|
|
335
352
|
}
|
|
336
353
|
|
|
354
|
+
const readyClients = ["Codex App", "Claude Code"];
|
|
355
|
+
if (claudeDesktopApiKey) readyClients.splice(1, 0, "Claude App");
|
|
337
356
|
console.log(`
|
|
338
|
-
${status("ok",
|
|
357
|
+
${status("ok", `Ready, ${readyClients.join(", ")} ${readyClients.length === 1 ? "is" : "are"} ready to use`)}
|
|
339
358
|
|
|
340
359
|
Adapter: ${adapterBaseUrl(ADAPTER_HOST, ADAPTER_PORT)}
|
|
341
360
|
|
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
|
|
|
@@ -235,6 +236,34 @@ const duplicateImages = {
|
|
|
235
236
|
optimizer_calls: duplicateCalls,
|
|
236
237
|
};
|
|
237
238
|
|
|
239
|
+
const routingCatalog = {
|
|
240
|
+
data: Array.from({ length: 1000 }, (_, index) => index % 5 === 0
|
|
241
|
+
? {
|
|
242
|
+
id: `claude-benchmark-${index}`,
|
|
243
|
+
vendor: "Anthropic",
|
|
244
|
+
model_picker_enabled: true,
|
|
245
|
+
supported_endpoints: ["/chat/completions"],
|
|
246
|
+
}
|
|
247
|
+
: { id: `gpt-benchmark-${index}`, vendor: "OpenAI", supported_endpoints: ["/responses"] }),
|
|
248
|
+
};
|
|
249
|
+
const modelRouter = createPmStudioModelRouter({
|
|
250
|
+
getCatalog: () => routingCatalog,
|
|
251
|
+
isClaudeEnabled: () => true,
|
|
252
|
+
});
|
|
253
|
+
const routingIterations = 100000;
|
|
254
|
+
const routingStarted = performance.now();
|
|
255
|
+
let claudeClassifications = 0;
|
|
256
|
+
for (let index = 0; index < routingIterations; index += 1) {
|
|
257
|
+
if (modelRouter.classify("claude-benchmark-0") === "claude") claudeClassifications += 1;
|
|
258
|
+
}
|
|
259
|
+
const pmModelRouting = {
|
|
260
|
+
catalog_models: routingCatalog.data.length,
|
|
261
|
+
iterations: routingIterations,
|
|
262
|
+
elapsed_ms: +(performance.now() - routingStarted).toFixed(1),
|
|
263
|
+
claude_classifications: claudeClassifications,
|
|
264
|
+
availability_rebuilds: modelRouter.diagnostics().rebuilds,
|
|
265
|
+
};
|
|
266
|
+
|
|
238
267
|
const pixels = deterministicPixels(1024 * 1024 * 3, 0x87654321);
|
|
239
268
|
const webp = await sharp(pixels, { raw: { width: 1024, height: 1024, channels: 3 } })
|
|
240
269
|
.webp({ quality: 100 })
|
|
@@ -281,6 +310,7 @@ const report = {
|
|
|
281
310
|
adapter_import: adapterImport,
|
|
282
311
|
token_count_100kb: tokenCount,
|
|
283
312
|
duplicate_images: duplicateImages,
|
|
313
|
+
pm_model_routing: pmModelRouting,
|
|
284
314
|
oversized_payload: oversizedPayload,
|
|
285
315
|
};
|
|
286
316
|
|
|
@@ -308,6 +338,10 @@ console.log(JSON.stringify(report, null, 2));
|
|
|
308
338
|
|
|
309
339
|
if (checkMode) {
|
|
310
340
|
const failures = [];
|
|
341
|
+
if (report.pm_model_routing.availability_rebuilds !== 1
|
|
342
|
+
|| report.pm_model_routing.claude_classifications !== report.pm_model_routing.iterations) {
|
|
343
|
+
failures.push("PM Studio model routing did not reuse the memoized catalog classification");
|
|
344
|
+
}
|
|
311
345
|
const proxy = report.large_token_count.proxy;
|
|
312
346
|
const array = report.large_token_count.materialized_array;
|
|
313
347
|
if (proxy.tokens !== array.tokens) failures.push(`token counts differ: proxy=${proxy.tokens} array=${array.tokens}`);
|
package/src/adapter.mjs
CHANGED
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
runtimeStatusPayload,
|
|
12
12
|
} from "./observability.mjs";
|
|
13
13
|
import { createRequestId, runWithRequestContext } from "./request-context.mjs";
|
|
14
|
+
import { createPmStudioRelayHandler, PM_STUDIO_RELAY_PREFIX } from "./pm-studio-relay.mjs";
|
|
14
15
|
import { createStreamPerformanceMetrics } from "./stream-performance.mjs";
|
|
15
16
|
import { status } from "./status.mjs";
|
|
16
17
|
import { createTerminalActivityIndicator } from "./terminal-activity.mjs";
|
|
@@ -172,6 +173,17 @@ export function createAdapterHandler(options = {}) {
|
|
|
172
173
|
streamIdleTimeoutMs,
|
|
173
174
|
upstreamTimeoutMs,
|
|
174
175
|
});
|
|
176
|
+
const pmStudioRelayHandler = createPmStudioRelayHandler({
|
|
177
|
+
acquireRequest,
|
|
178
|
+
claudeClient,
|
|
179
|
+
claudeMode,
|
|
180
|
+
claudeProfile: options.claudeProfile,
|
|
181
|
+
claudeModelRegistry,
|
|
182
|
+
fetchImpl: options.pmStudioFetchImpl,
|
|
183
|
+
streamHandshakeTimeoutMs,
|
|
184
|
+
streamIdleTimeoutMs,
|
|
185
|
+
upstreamTimeoutMs,
|
|
186
|
+
});
|
|
175
187
|
|
|
176
188
|
const dispatch = (req, res, pathname) => {
|
|
177
189
|
if (req.method === "GET" && pathname === ADAPTER_STATUS_PATH) {
|
|
@@ -201,6 +213,10 @@ export function createAdapterHandler(options = {}) {
|
|
|
201
213
|
return;
|
|
202
214
|
}
|
|
203
215
|
|
|
216
|
+
if (pathname.startsWith(`${PM_STUDIO_RELAY_PREFIX}/`)) {
|
|
217
|
+
return pmStudioRelayHandler(req, res, pathname);
|
|
218
|
+
}
|
|
219
|
+
|
|
204
220
|
if (req.method === "POST" && pathname === "/v1/responses") {
|
|
205
221
|
return responsesHandler(req, res);
|
|
206
222
|
}
|
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
|
|