codex-copilot-dx 0.5.6 → 0.6.1
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 +126 -5
- package/bin/cli.mjs +74 -92
- package/package.json +1 -1
- package/src/adapter.mjs +72 -14
- package/src/auth-profile.mjs +205 -0
- package/src/cli-auth.mjs +493 -0
- package/src/cli-models.mjs +38 -13
- package/src/cli-options.mjs +100 -12
- package/src/cli-status.mjs +29 -3
- package/src/copilot-client.mjs +226 -0
- package/src/copilot-token-session.mjs +362 -0
- package/src/copilot.mjs +45 -387
- package/src/doctor.mjs +169 -23
- package/src/model-cache.mjs +22 -6
- package/src/observability.mjs +61 -3
- package/src/pm-studio-asar.mjs +392 -0
- package/src/pm-studio-relay.mjs +557 -0
- package/src/pm-studio-setup.mjs +734 -0
- package/src/profile-model-runtime.mjs +261 -0
- package/src/profile-runtime.mjs +80 -0
- package/src/responses-handler.mjs +7 -6
- package/src/running-adapter.mjs +16 -0
package/README.md
CHANGED
|
@@ -1,14 +1,16 @@
|
|
|
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
|
|
|
7
|
-
A single in-process adapter (port `2026`) exposes both APIs over
|
|
7
|
+
A single in-process adapter (port `2026`) exposes both APIs over one Copilot
|
|
8
|
+
account by default, or over isolated Codex and Claude accounts when configured:
|
|
8
9
|
|
|
9
10
|
- **Codex** -> OpenAI Responses API (`/v1/responses`, `/v1/responses/compact`); Responses-only models and compaction proxy directly, chat models convert to Chat Completions.
|
|
10
11
|
- **Claude Code** -> Anthropic Messages API (`/v1/messages`, `/v1/messages/count_tokens`), translated to/from Chat Completions.
|
|
11
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.
|
|
12
14
|
|
|
13
15
|
Supports both HTTP SSE streaming and non-streaming.
|
|
14
16
|
|
|
@@ -18,7 +20,7 @@ Codex Auto-review requests use the hidden `codex-auto-review` model ID. The adap
|
|
|
18
20
|
|
|
19
21
|
- GitHub Copilot subscription (Individual, Business, or Enterprise)
|
|
20
22
|
- Node.js 22.15+ (required for built-in Zstandard request decompression)
|
|
21
|
-
- [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
|
|
22
24
|
|
|
23
25
|
## Usage
|
|
24
26
|
|
|
@@ -33,8 +35,8 @@ global install, use `npx codex-copilot-dx@latest`.
|
|
|
33
35
|
Both installed commands expose the same options and subcommands; help, version,
|
|
34
36
|
doctor, status, and argument-error output use the command name that was invoked.
|
|
35
37
|
|
|
36
|
-
To query the models currently advertised as selectable for the saved
|
|
37
|
-
account, run:
|
|
38
|
+
To query the models currently advertised as selectable for the saved Codex
|
|
39
|
+
Copilot account, run:
|
|
38
40
|
|
|
39
41
|
```bash
|
|
40
42
|
ccdx models
|
|
@@ -48,6 +50,122 @@ being advertised does not guarantee that a later inference request will avoid
|
|
|
48
50
|
quota, rate-limit, or policy enforcement. `codex-copilot-dx models` is fully
|
|
49
51
|
equivalent.
|
|
50
52
|
|
|
53
|
+
### Separate GitHub account for Claude
|
|
54
|
+
|
|
55
|
+
The default remains fully backward compatible: Codex and Claude requests share
|
|
56
|
+
the existing GitHub authentication. To keep an enterprise account for Codex
|
|
57
|
+
while using a different personal account that has Claude models enabled, run:
|
|
58
|
+
|
|
59
|
+
```bash
|
|
60
|
+
ccdx auth login claude --github-login <personal-github-login>
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
This is the only command that starts the additional Device Flow. It verifies
|
|
64
|
+
the selected GitHub identity, Copilot entitlement, and an enabled Claude model
|
|
65
|
+
before saving anything. It refuses the current Codex account and never changes,
|
|
66
|
+
migrates, or deletes the existing Codex credential. The optional login pin is
|
|
67
|
+
recommended so authorizing the wrong browser account fails before activation.
|
|
68
|
+
|
|
69
|
+
The existing Codex token remains at
|
|
70
|
+
`~/.local/share/copilot-api/github_token`. The isolated Claude credential is
|
|
71
|
+
stored with mode `0600` under
|
|
72
|
+
`~/.local/share/copilot-api/profiles/claude/github_token`, with matching account
|
|
73
|
+
metadata committed last. Restart a running adapter after authentication.
|
|
74
|
+
|
|
75
|
+
Once enabled, routing is fixed by API surface rather than by model name:
|
|
76
|
+
|
|
77
|
+
- `/v1/responses`, `/v1/responses/compact`, their Chat fallback, and normal
|
|
78
|
+
`/v1/models` use the Codex account.
|
|
79
|
+
- `/v1/messages` and Claude App model discovery use the isolated Claude account.
|
|
80
|
+
- `/v1/messages/count_tokens` remains local and uses neither account.
|
|
81
|
+
|
|
82
|
+
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
|
|
84
|
+
becomes invalid, Claude requests fail with a reauthentication instruction while
|
|
85
|
+
Codex remains available; CCDX never silently falls back to the enterprise
|
|
86
|
+
account. Reauthorize it explicitly with:
|
|
87
|
+
|
|
88
|
+
```bash
|
|
89
|
+
ccdx auth login claude --reauth --github-login <personal-github-login>
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
These commands are read-only and never start Device Flow:
|
|
93
|
+
|
|
94
|
+
```bash
|
|
95
|
+
ccdx auth status
|
|
96
|
+
ccdx auth status --online
|
|
97
|
+
ccdx models --profile claude
|
|
98
|
+
ccdx doctor --online --profile all
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
### PM Studio opt-in patch
|
|
102
|
+
|
|
103
|
+
CCDX can add the isolated Claude catalog to PM Studio without switching its
|
|
104
|
+
active account or profile. GPT requests continue to use the bearer supplied by
|
|
105
|
+
the current PM profile; exact enabled Anthropic model IDs use the isolated
|
|
106
|
+
CCDX Claude profile. Neither credential is copied into the other application.
|
|
107
|
+
|
|
108
|
+
The first recipe supports only an exact, unmodified PM Studio 2.9.7 bundle on
|
|
109
|
+
macOS. Authorize the isolated Claude profile first, quit PM Studio and its
|
|
110
|
+
updater, then run the one-time setup command:
|
|
111
|
+
|
|
112
|
+
```bash
|
|
113
|
+
ccdx auth login claude --reauth --github-login <personal-github-login>
|
|
114
|
+
ccdx pms setup
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
`ccdx pms setup` performs only local preflight, backup, staging, integrity,
|
|
118
|
+
signing, and replacement work. It never starts Device Flow, the adapter,
|
|
119
|
+
Codex, or PM Studio, and it does not modify PM profiles, cached model lists, or
|
|
120
|
+
saved PM tokens. Unknown versions, hashes, signatures, partial patches, a
|
|
121
|
+
running PM process, insufficient space, and every pre-replacement verification
|
|
122
|
+
failure leave the installed App unchanged. If the final read-only verification
|
|
123
|
+
after an atomic replacement detects an exceptional filesystem drift, setup
|
|
124
|
+
does not attempt a second automatic write: it preserves the verified backup,
|
|
125
|
+
retains any diagnostic stage still present, and prints recovery paths. Repeating
|
|
126
|
+
setup for the exact installed recipe is idempotent.
|
|
127
|
+
|
|
128
|
+
The patch is App-wide: every PM profile sends its Copilot API traffic through
|
|
129
|
+
the loopback-only `/pm-ccdx/*` relay. Run ordinary `ccdx` before opening PM
|
|
130
|
+
Studio; if port 2026 is unavailable, PM Copilot requests cannot connect. The
|
|
131
|
+
relay explicitly supports only the PM 2.9.7 Copilot paths discovered in the
|
|
132
|
+
validated bundle (`GET /models` and `POST /chat/completions`, `/responses`, and
|
|
133
|
+
`/embeddings`) and is not a general-purpose proxy.
|
|
134
|
+
|
|
135
|
+
Patching replaces the vendor signature with an ad-hoc signature. PM Studio's
|
|
136
|
+
updater is not disabled; an official update may overwrite the patch or may
|
|
137
|
+
require reinstalling the official App. Setup prints the verified backup and
|
|
138
|
+
manifest paths plus the version-matched restore procedure. Never restore a
|
|
139
|
+
2.9.7 backup over a newer PM Studio installation.
|
|
140
|
+
|
|
141
|
+
To restore, first quit PM Studio and its updater. Use the exact verified backup
|
|
142
|
+
path printed by setup as `BACKUP_APP`, then confirm that both the installed App
|
|
143
|
+
and backup still report version/build `2.9.7/2.9.7`:
|
|
144
|
+
|
|
145
|
+
```bash
|
|
146
|
+
BACKUP_APP="<verified-backup-path-from-ccdx-pms-setup>"
|
|
147
|
+
/usr/bin/plutil -extract CFBundleShortVersionString raw -o - "/Applications/PM Studio.app/Contents/Info.plist"
|
|
148
|
+
/usr/bin/plutil -extract CFBundleVersion raw -o - "/Applications/PM Studio.app/Contents/Info.plist"
|
|
149
|
+
/usr/bin/plutil -extract CFBundleShortVersionString raw -o - "$BACKUP_APP/Contents/Info.plist"
|
|
150
|
+
/usr/bin/plutil -extract CFBundleVersion raw -o - "$BACKUP_APP/Contents/Info.plist"
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
Only when all four values match `2.9.7`, move the patched App aside, restore the
|
|
154
|
+
complete vendor-signed backup, and verify it before launch:
|
|
155
|
+
|
|
156
|
+
```bash
|
|
157
|
+
RECOVERY_DIR="$(/usr/bin/mktemp -d '/Applications/CCDX-PM-Studio-recovery.XXXXXX')"
|
|
158
|
+
PATCHED_APP="$RECOVERY_DIR/PM Studio.app"
|
|
159
|
+
mv "/Applications/PM Studio.app" "$PATCHED_APP"
|
|
160
|
+
/usr/bin/ditto --rsrc --extattr --acl "$BACKUP_APP" "/Applications/PM Studio.app"
|
|
161
|
+
/usr/bin/codesign --verify --deep --strict --verbose=2 "/Applications/PM Studio.app"
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
If the version check, copy, or signature verification fails, do not launch the
|
|
165
|
+
restored App. Keep the moved patched bundle at `$PATCHED_APP` and reinstall the official PM Studio
|
|
166
|
+
instead of applying a 2.9.7 backup to unknown content. CCDX never runs these
|
|
167
|
+
restore commands or `sudo` automatically.
|
|
168
|
+
|
|
51
169
|
To change the model used by Codex Auto-review, run:
|
|
52
170
|
|
|
53
171
|
```bash
|
|
@@ -123,6 +241,9 @@ ccdx doctor --online
|
|
|
123
241
|
```
|
|
124
242
|
|
|
125
243
|
The online doctor never starts device flow, scans for replacement tokens, or changes the saved token.
|
|
244
|
+
Use `--profile codex`, `--profile claude`, or `--profile all` to choose which
|
|
245
|
+
account entitlement is checked. Without `--profile`, the legacy Codex check is
|
|
246
|
+
preserved.
|
|
126
247
|
|
|
127
248
|
To actively verify the protocol path through an already-running adapter, run:
|
|
128
249
|
|
package/bin/cli.mjs
CHANGED
|
@@ -5,8 +5,8 @@ import { ensureCodexConfig } from "../src/config.mjs";
|
|
|
5
5
|
import { ensureClaudeConfig } from "../src/claude-config.mjs";
|
|
6
6
|
import { applyClaudeDesktopConfig, formatClaudeDesktopApplyResult, generatedClaudeDesktopApiKey, loadManagedClaudeDesktopApiKey, syncManagedClaudeDesktopModels } from "../src/claude-desktop-config.mjs";
|
|
7
7
|
import { startAdapter } from "../src/adapter.mjs";
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
8
|
+
import { defaultCopilotClient, refreshVSCodeVersion } from "../src/copilot.mjs";
|
|
9
|
+
import { claudeDesktopModelIds } from "../src/models.mjs";
|
|
10
10
|
import { status } from "../src/status.mjs";
|
|
11
11
|
import { configureLogging } from "../src/log.mjs";
|
|
12
12
|
import { flushUsageWrites, printUsageSummary } from "../src/usage.mjs";
|
|
@@ -14,14 +14,16 @@ import { checkForUpdate, localPackageVersion } from "../src/version.mjs";
|
|
|
14
14
|
import { runDoctor } from "../src/doctor.mjs";
|
|
15
15
|
import { adapterBaseUrl, checkRunningAdapter } from "../src/running-adapter.mjs";
|
|
16
16
|
import { assertSafeAdapterHost, isLoopbackHost } from "../src/security.mjs";
|
|
17
|
-
import {
|
|
18
|
-
import { initializeModelRegistry, runInBackground } from "../src/startup.mjs";
|
|
17
|
+
import { runInBackground } from "../src/startup.mjs";
|
|
19
18
|
import { cliCommandName, cliHelp, parseAdapterProbeOptions, parseCliArgs, parseRuntimeOptions } from "../src/cli-options.mjs";
|
|
20
19
|
import { formatAdapterStatus, readAdapterStatus } from "../src/cli-status.mjs";
|
|
21
20
|
import { closeHttpServer } from "../src/shutdown.mjs";
|
|
22
21
|
import { runAutoReviewModelCommand } from "../src/auto-review-model.mjs";
|
|
23
22
|
import { autoReviewModelPreference } from "../src/user-settings.mjs";
|
|
24
23
|
import { fetchLiveCopilotModels, formatLiveCopilotModels } from "../src/cli-models.mjs";
|
|
24
|
+
import { runAuthCommand } from "../src/cli-auth.mjs";
|
|
25
|
+
import { createProfileRuntime } from "../src/profile-runtime.mjs";
|
|
26
|
+
import { createProfileModelRuntime } from "../src/profile-model-runtime.mjs";
|
|
25
27
|
|
|
26
28
|
const LOCAL_VERSION = localPackageVersion();
|
|
27
29
|
const CLI_NAME = cliCommandName();
|
|
@@ -48,6 +50,29 @@ if (CLI.command === "usage") {
|
|
|
48
50
|
await printUsageSummary();
|
|
49
51
|
process.exit(0);
|
|
50
52
|
}
|
|
53
|
+
if (CLI.command === "auth") {
|
|
54
|
+
try {
|
|
55
|
+
const result = await runAuthCommand({
|
|
56
|
+
action: CLI.action,
|
|
57
|
+
profile: CLI.profile,
|
|
58
|
+
online: CLI.online,
|
|
59
|
+
reauth: CLI.reauth,
|
|
60
|
+
expectedLogin: CLI.expectedLogin,
|
|
61
|
+
commandName: CLI_NAME,
|
|
62
|
+
});
|
|
63
|
+
if (result.output) console.log(result.output);
|
|
64
|
+
if (result.action === "login") {
|
|
65
|
+
if (!result.changed) {
|
|
66
|
+
console.log(status("ok", `Claude profile is already configured for ${result.identity?.login || result.identity?.id || "the saved account"}`));
|
|
67
|
+
}
|
|
68
|
+
console.log(status("warn", `Restart the running ${CLI_NAME} adapter to activate the Claude profile`));
|
|
69
|
+
}
|
|
70
|
+
process.exit(0);
|
|
71
|
+
} catch (e) {
|
|
72
|
+
console.error(status("err", e.message));
|
|
73
|
+
process.exit(1);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
51
76
|
if (CLI.command === "status") {
|
|
52
77
|
let probe;
|
|
53
78
|
try {
|
|
@@ -71,7 +96,7 @@ if (CLI.command === "status") {
|
|
|
71
96
|
}
|
|
72
97
|
if (CLI.command === "models") {
|
|
73
98
|
try {
|
|
74
|
-
const catalog = await fetchLiveCopilotModels();
|
|
99
|
+
const catalog = await fetchLiveCopilotModels({ profile: CLI.profile });
|
|
75
100
|
console.log(formatLiveCopilotModels(catalog, { commandName: CLI_NAME }));
|
|
76
101
|
process.exit(0);
|
|
77
102
|
} catch (e) {
|
|
@@ -98,6 +123,16 @@ if (CLI.command === "update") {
|
|
|
98
123
|
process.exit(1);
|
|
99
124
|
}
|
|
100
125
|
}
|
|
126
|
+
if (CLI.command === "pms") {
|
|
127
|
+
try {
|
|
128
|
+
const { runPmStudioSetup } = await import("../src/pm-studio-setup.mjs");
|
|
129
|
+
await runPmStudioSetup({ commandName: CLI_NAME });
|
|
130
|
+
process.exit(0);
|
|
131
|
+
} catch (e) {
|
|
132
|
+
console.error(status("err", e.message));
|
|
133
|
+
process.exit(1);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
101
136
|
|
|
102
137
|
let RUNTIME;
|
|
103
138
|
try {
|
|
@@ -113,7 +148,6 @@ const MODEL_REFRESH_TIMEOUT_MS = RUNTIME.modelRefreshTimeoutMs;
|
|
|
113
148
|
const EXISTING_ADAPTER_TIMEOUT_MS = RUNTIME.existingAdapterTimeoutMs;
|
|
114
149
|
const CONFIGURE_CLAUDE_DESKTOP = CLI.configureClaudeDesktop || process.env.CCDX_CONFIGURE_CLAUDE_DESKTOP === "1";
|
|
115
150
|
const LOGGING = configureLogging();
|
|
116
|
-
const MODEL_REGISTRY = { modelDefs: undefined, models: undefined, source: "built-in" };
|
|
117
151
|
let activeServer = null;
|
|
118
152
|
let modelRefreshTimer = null;
|
|
119
153
|
let shuttingDown = false;
|
|
@@ -123,88 +157,11 @@ if (LOGGING.filePath) {
|
|
|
123
157
|
if (LOGGING.level === "debug") console.log(status("debug", "Debug logging enabled"));
|
|
124
158
|
}
|
|
125
159
|
|
|
126
|
-
function applyModelsToRegistry(models, source, { updateClaudeModels = true } = {}) {
|
|
127
|
-
if (!isValidModelList(models)) throw new Error("Copilot models response contained no valid models");
|
|
128
|
-
const modelDefs = updateClaudeModels
|
|
129
|
-
? claudeDesktopModelDefsFromCopilotModels(models)
|
|
130
|
-
: MODEL_REGISTRY.modelDefs;
|
|
131
|
-
cacheModelEndpoints(models);
|
|
132
|
-
MODEL_REGISTRY.models = models;
|
|
133
|
-
if (updateClaudeModels && modelDefs.length) {
|
|
134
|
-
MODEL_REGISTRY.modelDefs = modelDefs;
|
|
135
|
-
MODEL_REGISTRY.source = source;
|
|
136
|
-
}
|
|
137
|
-
return modelDefs;
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
function loadCachedModelRegistry() {
|
|
141
|
-
const cached = loadModelCache();
|
|
142
|
-
if (!cached) return false;
|
|
143
|
-
try {
|
|
144
|
-
const customAliases = parseModelAliasEnv(process.env.CCDX_CLAUDE_MODEL_ALIASES).length > 0;
|
|
145
|
-
applyModelsToRegistry(cached, "cache", { updateClaudeModels: !customAliases });
|
|
146
|
-
return true;
|
|
147
|
-
} catch {
|
|
148
|
-
return false;
|
|
149
|
-
}
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
async function refreshClaudeDesktopModelDefs() {
|
|
153
|
-
const customAliases = parseModelAliasEnv(process.env.CCDX_CLAUDE_MODEL_ALIASES).length > 0;
|
|
154
|
-
|
|
155
|
-
const ctrl = new AbortController();
|
|
156
|
-
const timer = setTimeout(() => ctrl.abort(), MODEL_REFRESH_TIMEOUT_MS);
|
|
157
|
-
try {
|
|
158
|
-
const { status: httpStatus, body } = await listModels({ signal: ctrl.signal });
|
|
159
|
-
if (httpStatus < 200 || httpStatus >= 300) {
|
|
160
|
-
throw new Error(`Copilot models returned HTTP ${httpStatus}`);
|
|
161
|
-
}
|
|
162
|
-
const models = JSON.parse(body);
|
|
163
|
-
const autoReview = codexAutoReviewModelStatus(models, process.env, {
|
|
164
|
-
autoReviewModel: autoReviewModelPreference().model,
|
|
165
|
-
});
|
|
166
|
-
if (!autoReview.available) {
|
|
167
|
-
console.log(status("warn", `Auto-review target ${autoReview.upstreamModel} is unavailable: ${autoReview.reason}. Run ${CLI_NAME} doctor --compat to verify the live path.`));
|
|
168
|
-
}
|
|
169
|
-
const gptModelIds = gptModelIdsFromCopilotModels(models);
|
|
170
|
-
if (gptModelIds.length) {
|
|
171
|
-
console.log(status("ok", `Refreshed GPT models from GitHub Copilot: ${gptModelIds.join(", ")}`));
|
|
172
|
-
} else {
|
|
173
|
-
console.log(status("warn", "Copilot models response contained no GPT models"));
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
const modelDefs = applyModelsToRegistry(models, "live", { updateClaudeModels: !customAliases });
|
|
177
|
-
try {
|
|
178
|
-
saveModelCache(models);
|
|
179
|
-
} catch (e) {
|
|
180
|
-
console.log(status("warn", `Could not persist the Copilot model cache (${e.message})`));
|
|
181
|
-
}
|
|
182
|
-
if (customAliases) {
|
|
183
|
-
console.log(status("info", "Using CCDX_CLAUDE_MODEL_ALIASES for Claude models"));
|
|
184
|
-
} else if (modelDefs?.length) {
|
|
185
|
-
console.log(status("ok", `Refreshed Claude models from GitHub Copilot: ${modelDefs.map((model) => model.id).join(", ")}`));
|
|
186
|
-
} else {
|
|
187
|
-
console.log(status("warn", "Copilot models response contained no Claude models; using built-in Claude models"));
|
|
188
|
-
}
|
|
189
|
-
syncClaudeDesktopProfileModels(MODEL_REGISTRY.modelDefs);
|
|
190
|
-
return modelDefs;
|
|
191
|
-
} catch (e) {
|
|
192
|
-
const fallback = MODEL_REGISTRY.modelDefs?.length ? `${MODEL_REGISTRY.source} model list` : "built-in model list";
|
|
193
|
-
const message = ctrl.signal.aborted
|
|
194
|
-
? `Model refresh timed out after ${MODEL_REFRESH_TIMEOUT_MS}ms; using ${fallback}`
|
|
195
|
-
: `Could not refresh model list; using ${fallback} (${e.message})`;
|
|
196
|
-
console.log(status("warn", message));
|
|
197
|
-
return MODEL_REGISTRY.modelDefs;
|
|
198
|
-
} finally {
|
|
199
|
-
clearTimeout(timer);
|
|
200
|
-
}
|
|
201
|
-
}
|
|
202
|
-
|
|
203
160
|
function currentClaudeDesktopApiKey() {
|
|
204
161
|
return String(process.env.CCDX_CLAUDE_DESKTOP_API_KEY || process.env.CCDX_PROXY_API_KEY || "").trim();
|
|
205
162
|
}
|
|
206
163
|
|
|
207
|
-
function syncClaudeDesktopProfileModels(modelDefs
|
|
164
|
+
function syncClaudeDesktopProfileModels(modelDefs) {
|
|
208
165
|
const result = syncManagedClaudeDesktopModels({
|
|
209
166
|
port: ADAPTER_PORT,
|
|
210
167
|
host: ADAPTER_HOST,
|
|
@@ -268,6 +225,7 @@ if (CLI.command === "doctor") {
|
|
|
268
225
|
host: ADAPTER_HOST,
|
|
269
226
|
online: CLI.online,
|
|
270
227
|
compat: CLI.compat,
|
|
228
|
+
profile: CLI.profile,
|
|
271
229
|
});
|
|
272
230
|
process.exit(checks.some((check) => check.kind === "err") ? 1 : 0);
|
|
273
231
|
}
|
|
@@ -296,14 +254,33 @@ try {
|
|
|
296
254
|
// Ensure GitHub login, using device flow if no token exists.
|
|
297
255
|
await ensureAuth();
|
|
298
256
|
|
|
257
|
+
const profileRuntime = createProfileRuntime({ codexClient: defaultCopilotClient });
|
|
258
|
+
if (profileRuntime.claudeMode === "isolated") {
|
|
259
|
+
if (profileRuntime.claudeProfile.valid) {
|
|
260
|
+
const account = profileRuntime.claudeProfile.identity?.login
|
|
261
|
+
|| profileRuntime.claudeProfile.identity?.id
|
|
262
|
+
|| "the saved account";
|
|
263
|
+
console.log(status("ok", `Claude requests will use the isolated GitHub account ${account}`));
|
|
264
|
+
} else {
|
|
265
|
+
console.log(status("warn", `Claude profile is configured but invalid (${profileRuntime.claudeProfile.reason}); Codex remains available, but Claude requests will fail until you run ${CLI_NAME} auth login claude --reauth and restart`));
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
const profileModels = createProfileModelRuntime({
|
|
270
|
+
codexClient: profileRuntime.codexClient,
|
|
271
|
+
claudeClient: profileRuntime.claudeClient,
|
|
272
|
+
claudeMode: profileRuntime.claudeMode,
|
|
273
|
+
claudeCredentialFingerprint: profileRuntime.claudeCredentialFingerprint,
|
|
274
|
+
timeoutMs: MODEL_REFRESH_TIMEOUT_MS,
|
|
275
|
+
commandName: CLI_NAME,
|
|
276
|
+
autoReviewModelResolver: () => autoReviewModelPreference().model,
|
|
277
|
+
onClaudeModelsChanged: syncClaudeDesktopProfileModels,
|
|
278
|
+
});
|
|
279
|
+
|
|
299
280
|
// Refresh the VS Code version in the background; fallback is non-blocking.
|
|
300
281
|
void refreshVSCodeVersion();
|
|
301
|
-
const modelInitialization = await
|
|
302
|
-
|
|
303
|
-
currentModelDefs: () => MODEL_REGISTRY.modelDefs,
|
|
304
|
-
refresh: refreshClaudeDesktopModelDefs,
|
|
305
|
-
});
|
|
306
|
-
const claudeDesktopModelDefs = modelInitialization.modelDefs;
|
|
282
|
+
const modelInitialization = await profileModels.initialize();
|
|
283
|
+
const claudeDesktopModelDefs = modelInitialization.claude.modelDefs;
|
|
307
284
|
syncClaudeDesktopProfileModels(claudeDesktopModelDefs);
|
|
308
285
|
|
|
309
286
|
if (!isLoopbackHost(ADAPTER_HOST)) {
|
|
@@ -327,7 +304,12 @@ try {
|
|
|
327
304
|
claudeDesktopApiKey,
|
|
328
305
|
claudeDesktopModelDefs,
|
|
329
306
|
autoReviewModelResolver: () => autoReviewModelPreference().model,
|
|
330
|
-
|
|
307
|
+
codexClient: profileRuntime.codexClient,
|
|
308
|
+
claudeClient: profileRuntime.claudeClient,
|
|
309
|
+
claudeProfile: profileRuntime.claudeProfile,
|
|
310
|
+
codexModelRegistry: profileModels.codexRegistry,
|
|
311
|
+
claudeModelRegistry: profileModels.claudeRegistry,
|
|
312
|
+
claudeMode: profileRuntime.claudeMode,
|
|
331
313
|
showRequestId: CLI.showRequestId,
|
|
332
314
|
upstreamTimeoutMs: RUNTIME.upstreamTimeoutMs,
|
|
333
315
|
streamHandshakeTimeoutMs: RUNTIME.streamHandshakeTimeoutMs,
|
|
@@ -358,7 +340,7 @@ try {
|
|
|
358
340
|
const refreshIntervalMs = RUNTIME.modelRefreshIntervalMs;
|
|
359
341
|
if (Number.isFinite(refreshIntervalMs) && refreshIntervalMs > 0) {
|
|
360
342
|
modelRefreshTimer = setInterval(() => {
|
|
361
|
-
|
|
343
|
+
profileModels.refreshAll().catch(() => {});
|
|
362
344
|
}, refreshIntervalMs);
|
|
363
345
|
modelRefreshTimer.unref?.();
|
|
364
346
|
}
|
package/package.json
CHANGED
package/src/adapter.mjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import http from "node:http";
|
|
2
2
|
import { createAnthropicCountTokensHandler, createAnthropicMessagesHandler } from "./anthropic-handler.mjs";
|
|
3
|
-
import {
|
|
3
|
+
import { defaultCopilotClient } from "./copilot.mjs";
|
|
4
4
|
import { isValidModelList } from "./model-cache.mjs";
|
|
5
5
|
import { claudeDesktopModelsResponse } from "./models.mjs";
|
|
6
6
|
import {
|
|
@@ -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";
|
|
@@ -90,10 +91,46 @@ export function createAdapterHandler(options = {}) {
|
|
|
90
91
|
|| process.env.CCDX_CLAUDE_DESKTOP_API_KEY
|
|
91
92
|
|| process.env.CCDX_PROXY_API_KEY
|
|
92
93
|
|| "";
|
|
93
|
-
const
|
|
94
|
-
const
|
|
95
|
-
|
|
96
|
-
|
|
94
|
+
const codexClient = options.codexClient || defaultCopilotClient;
|
|
95
|
+
const hasExplicitClaudeMode = Object.hasOwn(options, "claudeMode");
|
|
96
|
+
if (hasExplicitClaudeMode && !["inherited", "isolated"].includes(options.claudeMode)) {
|
|
97
|
+
throw new Error(`Unsupported Claude mode: ${options.claudeMode}`);
|
|
98
|
+
}
|
|
99
|
+
if (options.claudeMode === "isolated"
|
|
100
|
+
&& (!options.claudeClient || options.claudeClient === codexClient)) {
|
|
101
|
+
throw new Error("Isolated Claude mode requires a distinct Claude client");
|
|
102
|
+
}
|
|
103
|
+
if (options.claudeMode === "inherited"
|
|
104
|
+
&& options.claudeClient && options.claudeClient !== codexClient) {
|
|
105
|
+
throw new Error("Inherited Claude mode requires the Codex client");
|
|
106
|
+
}
|
|
107
|
+
const claudeClient = options.claudeClient || codexClient;
|
|
108
|
+
// chatCompletionsFn is retained as a compatibility injection and continues to
|
|
109
|
+
// override both surfaces in existing tests and embedders. New dual-profile
|
|
110
|
+
// callers should pass codexClient/claudeClient instead.
|
|
111
|
+
const codexChatCompletionsFn = options.codexChatCompletionsFn
|
|
112
|
+
|| options.chatCompletionsFn
|
|
113
|
+
|| codexClient.chatCompletions;
|
|
114
|
+
const claudeChatCompletionsFn = options.claudeChatCompletionsFn
|
|
115
|
+
|| options.chatCompletionsFn
|
|
116
|
+
|| claudeClient.chatCompletions;
|
|
117
|
+
const responsesFn = options.responsesFn || codexClient.responses;
|
|
118
|
+
const responsesCompactFn = options.responsesCompactFn || codexClient.responsesCompact;
|
|
119
|
+
const listModelsFn = options.listModelsFn || codexClient.listModels;
|
|
120
|
+
const getCachedModelEndpointsFn = options.getCachedModelEndpointsFn
|
|
121
|
+
|| codexClient.getCachedModelEndpoints;
|
|
122
|
+
const claudeMode = hasExplicitClaudeMode
|
|
123
|
+
? options.claudeMode
|
|
124
|
+
: claudeClient === codexClient ? "inherited" : "isolated";
|
|
125
|
+
if (claudeMode === "isolated" && options.chatCompletionsFn) {
|
|
126
|
+
throw new Error("Isolated Claude mode cannot use the shared chatCompletionsFn override; pass codexChatCompletionsFn or claudeChatCompletionsFn explicitly");
|
|
127
|
+
}
|
|
128
|
+
const codexModelRegistry = options.codexModelRegistry || options.modelRegistry;
|
|
129
|
+
const claudeModelRegistry = options.claudeModelRegistry || options.modelRegistry || codexModelRegistry;
|
|
130
|
+
if (claudeMode === "isolated"
|
|
131
|
+
&& (!options.claudeModelRegistry || claudeModelRegistry === codexModelRegistry)) {
|
|
132
|
+
throw new Error("Isolated Claude mode requires a distinct Claude model registry");
|
|
133
|
+
}
|
|
97
134
|
const openAIModelEnv = options.openAIModelEnv || process.env;
|
|
98
135
|
const upstreamTimeoutMs = parsePositiveInteger(options.upstreamTimeoutMs, ADAPTER_RUNTIME_CONFIG.upstreamTimeoutMs);
|
|
99
136
|
const streamHandshakeTimeoutMs = parsePositiveInteger(options.streamHandshakeTimeoutMs, ADAPTER_RUNTIME_CONFIG.streamHandshakeTimeoutMs);
|
|
@@ -102,13 +139,14 @@ export function createAdapterHandler(options = {}) {
|
|
|
102
139
|
const requestMetrics = options.requestMetrics || createRequestMetrics();
|
|
103
140
|
const streamPerformanceMetrics = options.streamPerformanceMetrics || createStreamPerformanceMetrics();
|
|
104
141
|
const claudeDesktopModelOptions = () => {
|
|
105
|
-
const modelDefs =
|
|
142
|
+
const modelDefs = claudeModelRegistry?.modelDefs || options.claudeDesktopModelDefs;
|
|
106
143
|
return Array.isArray(modelDefs) ? { modelDefs } : {};
|
|
107
144
|
};
|
|
108
145
|
const responsesHandler = createResponsesHandler({
|
|
109
146
|
acquireRequest,
|
|
110
147
|
autoReviewModelResolver: options.autoReviewModelResolver,
|
|
111
|
-
chatCompletionsFn,
|
|
148
|
+
chatCompletionsFn: codexChatCompletionsFn,
|
|
149
|
+
getCachedModelEndpointsFn,
|
|
112
150
|
openAIModelEnv,
|
|
113
151
|
responsesPayloadOptions: options.responsesPayloadOptions,
|
|
114
152
|
responsesFn,
|
|
@@ -128,13 +166,24 @@ export function createAdapterHandler(options = {}) {
|
|
|
128
166
|
const anthropicCountTokensHandler = createAnthropicCountTokensHandler({ acquireRequest });
|
|
129
167
|
const anthropicMessagesHandler = createAnthropicMessagesHandler({
|
|
130
168
|
acquireRequest,
|
|
131
|
-
chatCompletionsFn,
|
|
169
|
+
chatCompletionsFn: claudeChatCompletionsFn,
|
|
132
170
|
environment: process.env,
|
|
133
171
|
modelOptions: claudeDesktopModelOptions,
|
|
134
172
|
streamHandshakeTimeoutMs,
|
|
135
173
|
streamIdleTimeoutMs,
|
|
136
174
|
upstreamTimeoutMs,
|
|
137
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
|
+
});
|
|
138
187
|
|
|
139
188
|
const dispatch = (req, res, pathname) => {
|
|
140
189
|
if (req.method === "GET" && pathname === ADAPTER_STATUS_PATH) {
|
|
@@ -148,7 +197,12 @@ export function createAdapterHandler(options = {}) {
|
|
|
148
197
|
metrics: requestMetrics,
|
|
149
198
|
streamPerformance: streamPerformanceMetrics,
|
|
150
199
|
admission: acquireRequest,
|
|
151
|
-
modelRegistry:
|
|
200
|
+
modelRegistry: codexModelRegistry,
|
|
201
|
+
codexClient,
|
|
202
|
+
claudeClient,
|
|
203
|
+
codexModelRegistry,
|
|
204
|
+
claudeModelRegistry,
|
|
205
|
+
claudeMode,
|
|
152
206
|
})));
|
|
153
207
|
return;
|
|
154
208
|
}
|
|
@@ -159,6 +213,10 @@ export function createAdapterHandler(options = {}) {
|
|
|
159
213
|
return;
|
|
160
214
|
}
|
|
161
215
|
|
|
216
|
+
if (pathname.startsWith(`${PM_STUDIO_RELAY_PREFIX}/`)) {
|
|
217
|
+
return pmStudioRelayHandler(req, res, pathname);
|
|
218
|
+
}
|
|
219
|
+
|
|
162
220
|
if (req.method === "POST" && pathname === "/v1/responses") {
|
|
163
221
|
return responsesHandler(req, res);
|
|
164
222
|
}
|
|
@@ -181,24 +239,24 @@ export function createAdapterHandler(options = {}) {
|
|
|
181
239
|
let models;
|
|
182
240
|
try { models = JSON.parse(body); } catch {}
|
|
183
241
|
if (isValidModelList(models)) {
|
|
184
|
-
if (
|
|
185
|
-
|
|
242
|
+
if (codexModelRegistry) {
|
|
243
|
+
codexModelRegistry.models = models;
|
|
186
244
|
}
|
|
187
245
|
res.writeHead(status, { "Content-Type": "application/json" });
|
|
188
246
|
res.end(body);
|
|
189
247
|
return;
|
|
190
248
|
}
|
|
191
|
-
if (sendLastKnownGoodModels(res,
|
|
249
|
+
if (sendLastKnownGoodModels(res, codexModelRegistry)) return;
|
|
192
250
|
sendJsonError(res, httpError("Copilot models response contained no valid models", 502), 502);
|
|
193
251
|
return;
|
|
194
252
|
}
|
|
195
|
-
if (modelListCanUseLastKnownGood(status) && sendLastKnownGoodModels(res,
|
|
253
|
+
if (modelListCanUseLastKnownGood(status) && sendLastKnownGoodModels(res, codexModelRegistry)) return;
|
|
196
254
|
res.writeHead(status, { "Content-Type": "application/json" });
|
|
197
255
|
res.end(body);
|
|
198
256
|
})
|
|
199
257
|
.catch((e) => {
|
|
200
258
|
logRequestFailure("Models", e, abort);
|
|
201
|
-
if (sendLastKnownGoodModels(res,
|
|
259
|
+
if (sendLastKnownGoodModels(res, codexModelRegistry)) return;
|
|
202
260
|
if (!res.destroyed && !res.writableEnded) {
|
|
203
261
|
if (!res.headersSent) res.writeHead(e?.statusCode || 502);
|
|
204
262
|
res.end(JSON.stringify({ error: e.message }));
|