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/src/cli-options.mjs
CHANGED
|
@@ -1,7 +1,28 @@
|
|
|
1
1
|
const HELP_COMMANDS = new Set(["help", "--help", "-h"]);
|
|
2
2
|
const VERSION_COMMANDS = new Set(["version", "--version", "-v"]);
|
|
3
|
-
const START_OPTIONS = new
|
|
4
|
-
|
|
3
|
+
const START_OPTIONS = new Map([
|
|
4
|
+
["--configure-claude-desktop", "configure-claude-app"],
|
|
5
|
+
["--configure-claude-app", "configure-claude-app"],
|
|
6
|
+
["--show-request-id", "show-request-id"],
|
|
7
|
+
]);
|
|
8
|
+
const PM_COMMANDS = new Set(["pms", "pm-studio"]);
|
|
9
|
+
const HELP_TOPICS = new Set([
|
|
10
|
+
"",
|
|
11
|
+
"start",
|
|
12
|
+
"auth",
|
|
13
|
+
"auth status",
|
|
14
|
+
"auth login",
|
|
15
|
+
"doctor",
|
|
16
|
+
"status",
|
|
17
|
+
"models",
|
|
18
|
+
"pms",
|
|
19
|
+
"pms setup",
|
|
20
|
+
"pms status",
|
|
21
|
+
"usage",
|
|
22
|
+
"auto-review-model",
|
|
23
|
+
"update",
|
|
24
|
+
"version",
|
|
25
|
+
]);
|
|
5
26
|
|
|
6
27
|
function unexpectedArgs(args) {
|
|
7
28
|
throw new Error(`Unexpected argument${args.length === 1 ? "" : "s"}: ${args.join(" ")}`);
|
|
@@ -39,40 +60,97 @@ function checkedProfile(value, { allowAll = false } = {}) {
|
|
|
39
60
|
return profile;
|
|
40
61
|
}
|
|
41
62
|
|
|
63
|
+
function baseCommand(command, extra = {}) {
|
|
64
|
+
return {
|
|
65
|
+
command,
|
|
66
|
+
configureClaudeDesktop: false,
|
|
67
|
+
showRequestId: false,
|
|
68
|
+
online: false,
|
|
69
|
+
compat: false,
|
|
70
|
+
...extra,
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function checkedHelpTopic(parts = []) {
|
|
75
|
+
const normalized = parts.map((part, index) => {
|
|
76
|
+
if (index === 0 && part === "pm-studio") return "pms";
|
|
77
|
+
return part;
|
|
78
|
+
}).join(" ");
|
|
79
|
+
if (!HELP_TOPICS.has(normalized)) throw new Error(`Unknown help topic: ${parts.join(" ")}`);
|
|
80
|
+
return normalized;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function helpCommand(parts = []) {
|
|
84
|
+
return baseCommand("help", { helpTopic: checkedHelpTopic(parts) });
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function nestedHelp(command, rest) {
|
|
88
|
+
if (!rest.length || !HELP_COMMANDS.has(rest.at(-1))) return null;
|
|
89
|
+
const args = rest.slice(0, -1);
|
|
90
|
+
if (PM_COMMANDS.has(command)) {
|
|
91
|
+
if (args.length > 1) unexpectedArgs(args.slice(1));
|
|
92
|
+
return helpCommand(["pms", ...args]);
|
|
93
|
+
}
|
|
94
|
+
if (command === "auth") {
|
|
95
|
+
if (args[0] === "login" && args[1] === "claude" && args.length === 2) {
|
|
96
|
+
return helpCommand(["auth", "login"]);
|
|
97
|
+
}
|
|
98
|
+
if (args.length > 1) unexpectedArgs(args.slice(1));
|
|
99
|
+
return helpCommand(["auth", ...args]);
|
|
100
|
+
}
|
|
101
|
+
if (args.length) unexpectedArgs(args);
|
|
102
|
+
return helpCommand([command]);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function parseStartOptions(args) {
|
|
106
|
+
const seen = new Set();
|
|
107
|
+
for (const option of args) {
|
|
108
|
+
const kind = START_OPTIONS.get(option);
|
|
109
|
+
if (!kind || seen.has(kind)) unexpectedArgs([option]);
|
|
110
|
+
seen.add(kind);
|
|
111
|
+
}
|
|
112
|
+
return baseCommand("start", {
|
|
113
|
+
configureClaudeDesktop: seen.has("configure-claude-app"),
|
|
114
|
+
showRequestId: seen.has("show-request-id"),
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
|
|
42
118
|
export function parseCliArgs(args = []) {
|
|
43
119
|
const [command, ...rest] = args;
|
|
44
|
-
if (!command) return
|
|
120
|
+
if (!command) return baseCommand("start");
|
|
45
121
|
if (HELP_COMMANDS.has(command)) {
|
|
46
|
-
|
|
47
|
-
|
|
122
|
+
const topic = rest[0] === "auth" && rest[1] === "login" && rest[2] === "claude"
|
|
123
|
+
? ["auth", "login"]
|
|
124
|
+
: rest;
|
|
125
|
+
if (topic.length > 2) unexpectedArgs(topic.slice(2));
|
|
126
|
+
return helpCommand(topic);
|
|
48
127
|
}
|
|
128
|
+
const requestedHelp = nestedHelp(command, rest);
|
|
129
|
+
if (requestedHelp) return requestedHelp;
|
|
49
130
|
if (VERSION_COMMANDS.has(command)) {
|
|
50
131
|
if (rest.length) unexpectedArgs(rest);
|
|
51
|
-
return
|
|
132
|
+
return baseCommand("version");
|
|
52
133
|
}
|
|
134
|
+
if (command === "start") return parseStartOptions(rest);
|
|
53
135
|
if (command === "status" || command === "--status") {
|
|
54
136
|
if (rest.length) unexpectedArgs(rest);
|
|
55
|
-
return
|
|
137
|
+
return baseCommand("status");
|
|
56
138
|
}
|
|
57
139
|
if (command === "usage") {
|
|
58
140
|
if (rest.length) unexpectedArgs(rest);
|
|
59
|
-
return
|
|
141
|
+
return baseCommand("usage");
|
|
60
142
|
}
|
|
61
|
-
if (command
|
|
143
|
+
if (PM_COMMANDS.has(command)) {
|
|
62
144
|
const [action, ...pmsArgs] = rest;
|
|
63
|
-
if (!action) throw new Error("Missing pms action: expected setup");
|
|
64
|
-
if (
|
|
145
|
+
if (!action) throw new Error("Missing pms action: expected setup or status");
|
|
146
|
+
if (!new Set(["setup", "status"]).has(action)) throw new Error(`Unknown pms action: ${action}`);
|
|
65
147
|
if (pmsArgs.length) unexpectedArgs(pmsArgs);
|
|
66
|
-
return
|
|
148
|
+
return baseCommand("pms", { action });
|
|
67
149
|
}
|
|
68
150
|
if (command === "models") {
|
|
69
151
|
const options = parseOptions(rest, new Map([["--profile", "value"]]));
|
|
70
152
|
return {
|
|
71
|
-
|
|
72
|
-
configureClaudeDesktop: false,
|
|
73
|
-
showRequestId: false,
|
|
74
|
-
online: false,
|
|
75
|
-
compat: false,
|
|
153
|
+
...baseCommand("models"),
|
|
76
154
|
profile: checkedProfile(options["--profile"]),
|
|
77
155
|
};
|
|
78
156
|
}
|
|
@@ -82,13 +160,10 @@ export function parseCliArgs(args = []) {
|
|
|
82
160
|
if (action === "status") {
|
|
83
161
|
const options = parseOptions(authArgs, new Map([["--online", "flag"]]));
|
|
84
162
|
return {
|
|
85
|
-
|
|
163
|
+
...baseCommand("auth"),
|
|
86
164
|
action: "status",
|
|
87
165
|
profile: "",
|
|
88
|
-
configureClaudeDesktop: false,
|
|
89
|
-
showRequestId: false,
|
|
90
166
|
online: Boolean(options["--online"]),
|
|
91
|
-
compat: false,
|
|
92
167
|
expectedLogin: "",
|
|
93
168
|
reauth: false,
|
|
94
169
|
};
|
|
@@ -102,13 +177,9 @@ export function parseCliArgs(args = []) {
|
|
|
102
177
|
["--reauth", "flag"],
|
|
103
178
|
]));
|
|
104
179
|
return {
|
|
105
|
-
|
|
180
|
+
...baseCommand("auth"),
|
|
106
181
|
action: "login",
|
|
107
182
|
profile: "claude",
|
|
108
|
-
configureClaudeDesktop: false,
|
|
109
|
-
showRequestId: false,
|
|
110
|
-
online: false,
|
|
111
|
-
compat: false,
|
|
112
183
|
expectedLogin: options["--github-login"] || "",
|
|
113
184
|
reauth: Boolean(options["--reauth"]),
|
|
114
185
|
};
|
|
@@ -117,7 +188,7 @@ export function parseCliArgs(args = []) {
|
|
|
117
188
|
}
|
|
118
189
|
if (command === "auto-review-model") {
|
|
119
190
|
if (rest.length) unexpectedArgs(rest);
|
|
120
|
-
return
|
|
191
|
+
return baseCommand("auto-review-model");
|
|
121
192
|
}
|
|
122
193
|
if (command === "update") {
|
|
123
194
|
if (rest.length > 1) unexpectedArgs(rest.slice(1));
|
|
@@ -126,11 +197,7 @@ export function parseCliArgs(args = []) {
|
|
|
126
197
|
throw new Error(`Update source must be npm or github: ${source}`);
|
|
127
198
|
}
|
|
128
199
|
return {
|
|
129
|
-
|
|
130
|
-
configureClaudeDesktop: false,
|
|
131
|
-
showRequestId: false,
|
|
132
|
-
online: false,
|
|
133
|
-
compat: false,
|
|
200
|
+
...baseCommand("update"),
|
|
134
201
|
updateSource: source === "gh" ? "github" : source,
|
|
135
202
|
};
|
|
136
203
|
}
|
|
@@ -141,29 +208,13 @@ export function parseCliArgs(args = []) {
|
|
|
141
208
|
["--profile", "value"],
|
|
142
209
|
]));
|
|
143
210
|
return {
|
|
144
|
-
|
|
145
|
-
configureClaudeDesktop: false,
|
|
146
|
-
showRequestId: false,
|
|
211
|
+
...baseCommand("doctor"),
|
|
147
212
|
online: Boolean(options["--online"]),
|
|
148
213
|
compat: Boolean(options["--compat"]),
|
|
149
214
|
profile: checkedProfile(options["--profile"], { allowAll: true }),
|
|
150
215
|
};
|
|
151
216
|
}
|
|
152
|
-
if (START_OPTIONS.has(command))
|
|
153
|
-
const options = [command, ...rest];
|
|
154
|
-
const invalid = options.filter((arg) => !START_OPTIONS.has(arg));
|
|
155
|
-
if (invalid.length) unexpectedArgs(invalid);
|
|
156
|
-
for (const option of START_OPTIONS) {
|
|
157
|
-
if (options.filter((arg) => arg === option).length > 1) unexpectedArgs([option]);
|
|
158
|
-
}
|
|
159
|
-
return {
|
|
160
|
-
command: "start",
|
|
161
|
-
configureClaudeDesktop: options.includes("--configure-claude-desktop"),
|
|
162
|
-
showRequestId: options.includes("--show-request-id"),
|
|
163
|
-
online: false,
|
|
164
|
-
compat: false,
|
|
165
|
-
};
|
|
166
|
-
}
|
|
217
|
+
if (START_OPTIONS.has(command)) return parseStartOptions([command, ...rest]);
|
|
167
218
|
throw new Error(`Unknown command or option: ${command}`);
|
|
168
219
|
}
|
|
169
220
|
|
|
@@ -201,22 +252,42 @@ export function parseAdapterProbeOptions(env = process.env) {
|
|
|
201
252
|
};
|
|
202
253
|
}
|
|
203
254
|
|
|
204
|
-
export function cliCommandName(
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
255
|
+
export function cliCommandName() {
|
|
256
|
+
return "ccdx";
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
function topicHelp(name, topic) {
|
|
260
|
+
const sections = {
|
|
261
|
+
start: `Usage:\n ${name} [start] [--configure-claude-app] [--show-request-id]\n\nStarts or reuses the local adapter, updates Codex and Claude Code configuration, and opens Codex.\n--configure-claude-app also creates or updates the managed Claude App gateway profile.`,
|
|
262
|
+
auth: `Usage:\n ${name} auth status [--online]\n ${name} auth login claude [--github-login <login>] [--reauth]\n\nShows the two account profiles or configures the isolated Claude account. Device login starts only when no reusable local Copilot credential is available or --reauth is used.`,
|
|
263
|
+
"auth status": `Usage:\n ${name} auth status [--online]\n\nShows account routing without exposing credentials. --online also verifies both configured Copilot entitlements and model catalogs.`,
|
|
264
|
+
"auth login": `Usage:\n ${name} auth login claude [--github-login <login>] [--reauth]\n\nReuses a compatible local Copilot credential when possible. --reauth forces GitHub Device Flow.`,
|
|
265
|
+
doctor: `Usage:\n ${name} doctor [--online] [--compat] [--profile codex|claude|all]\n\nChecks local credentials, client configuration, PM Studio state, and the adapter. --online validates account entitlement; --compat sends minimal inference requests and consumes a small amount of Copilot usage.`,
|
|
266
|
+
status: `Usage:\n ${name} status\n\nShows bounded runtime, routing, performance, cache, queue, and PM relay metrics from a running adapter.`,
|
|
267
|
+
models: `Usage:\n ${name} models [--profile codex|claude]\n\nPerforms a fresh, read-only Copilot model-directory lookup for one saved profile.`,
|
|
268
|
+
pms: `Usage:\n ${name} pms status\n ${name} pms setup\n ${name} pm-studio status\n ${name} pm-studio setup\n\nInspects or installs the version-locked PM Studio loopback patch. setup modifies the installed app only after all preflight checks pass.`,
|
|
269
|
+
"pms setup": `Usage:\n ${name} pms setup\n\nBacks up, patches, verifies, and ad-hoc signs an exactly supported PM Studio bundle. Unknown versions and drift are never modified.`,
|
|
270
|
+
"pms status": `Usage:\n ${name} pms status\n\nRead-only inspection of the installed PM Studio version, patch integrity, Claude profile, and relay availability.`,
|
|
271
|
+
usage: `Usage:\n ${name} usage\n\nSummarizes local token usage metadata without reading prompt or completion content.`,
|
|
272
|
+
"auto-review-model": `Usage:\n ${name} auto-review-model\n\nInteractively selects an enabled Responses model for Codex Auto-review.`,
|
|
273
|
+
update: `Usage:\n ${name} update [npm|github]\n\nUpdates the global package from the configured npm registry or GitHub main. With no source, an interactive terminal prompts for one.`,
|
|
274
|
+
version: `Usage:\n ${name} --version`,
|
|
275
|
+
};
|
|
276
|
+
return sections[topic];
|
|
208
277
|
}
|
|
209
278
|
|
|
210
|
-
export function cliHelp(commandName = "ccdx") {
|
|
211
|
-
const name =
|
|
212
|
-
const
|
|
279
|
+
export function cliHelp(commandName = "ccdx", topic = "") {
|
|
280
|
+
const name = "ccdx";
|
|
281
|
+
const normalizedTopic = checkedHelpTopic(topic ? topic.split(" ") : []);
|
|
282
|
+
if (normalizedTopic) return topicHelp(name, normalizedTopic);
|
|
213
283
|
return `Usage:
|
|
214
|
-
${name} [--configure-claude-
|
|
284
|
+
${name} [start] [--configure-claude-app] [--show-request-id]
|
|
215
285
|
${name} auth status [--online]
|
|
216
286
|
${name} auth login claude [--github-login <login>] [--reauth]
|
|
217
287
|
${name} doctor [--online] [--compat] [--profile codex|claude|all]
|
|
218
288
|
${name} status
|
|
219
289
|
${name} models [--profile codex|claude]
|
|
290
|
+
${name} pms status
|
|
220
291
|
${name} pms setup
|
|
221
292
|
${name} usage
|
|
222
293
|
${name} auto-review-model
|
|
@@ -224,5 +295,16 @@ export function cliHelp(commandName = "ccdx") {
|
|
|
224
295
|
${name} --version
|
|
225
296
|
${name} --help
|
|
226
297
|
|
|
227
|
-
|
|
298
|
+
Commands:
|
|
299
|
+
start Start or reuse the adapter and configure local clients
|
|
300
|
+
auth Inspect or configure the Codex and Claude accounts
|
|
301
|
+
doctor Diagnose local config; optional live and inference checks
|
|
302
|
+
status Show runtime routing, performance, queue, and cache health
|
|
303
|
+
models Query a saved account's live Copilot model catalog
|
|
304
|
+
pms, pm-studio Inspect or install the PM Studio patch
|
|
305
|
+
usage Summarize locally recorded token usage
|
|
306
|
+
auto-review-model Select the Codex Auto-review Responses model
|
|
307
|
+
update Update the global package from npm or GitHub
|
|
308
|
+
|
|
309
|
+
Run ${name} <command> --help for command details.`;
|
|
228
310
|
}
|
package/src/cli-status.mjs
CHANGED
|
@@ -70,6 +70,33 @@ function routingTarget(value) {
|
|
|
70
70
|
return "unknown";
|
|
71
71
|
}
|
|
72
72
|
|
|
73
|
+
function pmRouteSummary(requests = {}) {
|
|
74
|
+
const routes = requests.by_route;
|
|
75
|
+
if (!routes || typeof routes !== "object") return null;
|
|
76
|
+
const names = ["pm_models", "pm_chat_completions", "pm_responses", "pm_embeddings"];
|
|
77
|
+
const totals = names.reduce((summary, name) => {
|
|
78
|
+
const route = routes[name] || {};
|
|
79
|
+
summary.total += finiteNumber(route.total) || 0;
|
|
80
|
+
summary.active += finiteNumber(route.active) || 0;
|
|
81
|
+
summary.errors += finiteNumber(route.errors) || 0;
|
|
82
|
+
return summary;
|
|
83
|
+
}, { total: 0, active: 0, errors: 0 });
|
|
84
|
+
return `PM relay: ${count(totals.total)} requests, ${count(totals.active)} active, ${count(totals.errors)} errors; models ${count(routes.pm_models?.total)}, chat ${count(routes.pm_chat_completions?.total)}, responses ${count(routes.pm_responses?.total)}, embeddings ${count(routes.pm_embeddings?.total)}`;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function errorCode(error) {
|
|
88
|
+
const seen = new Set();
|
|
89
|
+
const pending = [error];
|
|
90
|
+
while (pending.length) {
|
|
91
|
+
const current = pending.shift();
|
|
92
|
+
if (!current || typeof current !== "object" || seen.has(current)) continue;
|
|
93
|
+
seen.add(current);
|
|
94
|
+
if (typeof current.code === "string") return current.code;
|
|
95
|
+
pending.push(current.cause, ...(Array.isArray(current.errors) ? current.errors : []));
|
|
96
|
+
}
|
|
97
|
+
return "";
|
|
98
|
+
}
|
|
99
|
+
|
|
73
100
|
export async function readAdapterStatus({
|
|
74
101
|
host = "127.0.0.1",
|
|
75
102
|
port = 2026,
|
|
@@ -101,12 +128,16 @@ export async function readAdapterStatus({
|
|
|
101
128
|
}
|
|
102
129
|
return { baseUrl, data };
|
|
103
130
|
} catch (error) {
|
|
131
|
+
const code = errorCode(error);
|
|
104
132
|
const reason = ctrl.signal.aborted
|
|
105
133
|
? `timed out after ${timeoutMs}ms`
|
|
106
|
-
:
|
|
134
|
+
: code === "ECONNREFUSED" || code === "ENETUNREACH"
|
|
107
135
|
? "adapter is not running"
|
|
108
136
|
: error?.message || String(error);
|
|
109
|
-
|
|
137
|
+
const nextStep = reason === "adapter is not running"
|
|
138
|
+
? `. Run ccdx start, then retry ccdx status`
|
|
139
|
+
: "";
|
|
140
|
+
throw new Error(`Could not read adapter status at ${baseUrl}: ${reason}${nextStep}`);
|
|
110
141
|
} finally {
|
|
111
142
|
clearTimeout(timer);
|
|
112
143
|
}
|
|
@@ -118,6 +149,7 @@ export function formatAdapterStatus({ baseUrl, data }, { commandName = "ccdx", c
|
|
|
118
149
|
const admission = data.admission || {};
|
|
119
150
|
const history = data.response_history || {};
|
|
120
151
|
const images = data.image_optimization || {};
|
|
152
|
+
const imageHistory = data.image_history_pressure;
|
|
121
153
|
const models = data.models || {};
|
|
122
154
|
const copilot = data.copilot || {};
|
|
123
155
|
const limits = data.limits || {};
|
|
@@ -135,14 +167,19 @@ export function formatAdapterStatus({ baseUrl, data }, { commandName = "ccdx", c
|
|
|
135
167
|
status("info", `Memory: RSS ${mebibytes(processStats.rss_bytes)}, heap ${mebibytes(processStats.heap_used_bytes)}`),
|
|
136
168
|
status("info", `History: ${mebibytes(history.bytes)} / ${mebibytes(limits.response_history_max_bytes)}, ${count(history.entries)} entries, ${count(history.evicted)} evicted`),
|
|
137
169
|
status("info", `Image cache: ${count(images.cache_entries)} entries, ${cacheHitRate(images)} hits, ${mebibytes(images.cache_bytes)} / ${mebibytes(images.cache_max_bytes)}`),
|
|
138
|
-
status("info", `Models: ${count(models.models)} total, ${count(models.claude_models)} Claude; Copilot token ${copilot.token_cached ? `cached (${duration(copilot.token_expires_in_ms)} remaining)` : "not cached"}`),
|
|
139
170
|
];
|
|
171
|
+
if (imageHistory && typeof imageHistory === "object") {
|
|
172
|
+
lines.push(status("info", `Visual history: ${count(imageHistory.active_recovery_trees)} recovery trees, ${count(imageHistory.adapted_requests)} adapted requests, ${count(imageHistory.historical_images_omitted)} older images omitted, ${count(imageHistory.timeouts_recorded)} timeouts`));
|
|
173
|
+
}
|
|
174
|
+
lines.push(status("info", `Models: ${count(models.models)} total, ${count(models.claude_models)} Claude; Copilot token ${copilot.token_cached ? `cached (${duration(copilot.token_expires_in_ms)} remaining)` : "not cached"}`));
|
|
140
175
|
if (data.profiles?.codex || data.profiles?.claude) {
|
|
141
176
|
lines.push(status("info", `Profiles: ${profileSummary("Codex", data.profiles?.codex)}; ${profileSummary("Claude", data.profiles?.claude)}`));
|
|
142
177
|
}
|
|
143
178
|
if (data.routing) {
|
|
144
179
|
lines.push(status("info", `Routing: /v1/responses -> ${routingTarget(data.routing.responses)}; /v1/messages -> ${routingTarget(data.routing.messages)}`));
|
|
145
180
|
}
|
|
181
|
+
const pmSummary = pmRouteSummary(requests);
|
|
182
|
+
if (pmSummary) lines.push(status("info", pmSummary));
|
|
146
183
|
lines.push(status("info", `Limits: request body ${mebibytes(limits.max_body_bytes)}, decoded body ${mebibytes(limits.max_decoded_body_bytes)}`));
|
|
147
184
|
return lines.join("\n");
|
|
148
185
|
}
|
package/src/copilot-client.mjs
CHANGED
|
@@ -85,6 +85,7 @@ export function createCopilotClientRuntime({
|
|
|
85
85
|
fetchImpl,
|
|
86
86
|
retryOptions,
|
|
87
87
|
bodyText,
|
|
88
|
+
onUpstreamStart,
|
|
88
89
|
} = {}) {
|
|
89
90
|
const token = await tokenSession.getToken({ signal });
|
|
90
91
|
const upstreamReq = chatReq.stream === true ? withChatStreamUsage(chatReq) : chatReq;
|
|
@@ -99,6 +100,7 @@ export function createCopilotClientRuntime({
|
|
|
99
100
|
vision: computeVision(messages),
|
|
100
101
|
});
|
|
101
102
|
headers["Content-Length"] = String(Buffer.byteLength(serializedBody));
|
|
103
|
+
onUpstreamStart?.();
|
|
102
104
|
if (upstreamReq.stream === true) markUpstreamStarted();
|
|
103
105
|
return fetchCopilotUpstream(`${tokenSession.getApiBase()}/chat/completions`, {
|
|
104
106
|
method: "POST",
|
|
@@ -152,6 +154,7 @@ export function createCopilotClientRuntime({
|
|
|
152
154
|
fetchImpl,
|
|
153
155
|
retryOptions,
|
|
154
156
|
currentInputStart = 0,
|
|
157
|
+
onUpstreamStart,
|
|
155
158
|
payloadPrepared = false,
|
|
156
159
|
payloadOptions = {},
|
|
157
160
|
} = {}) {
|
|
@@ -175,6 +178,7 @@ export function createCopilotClientRuntime({
|
|
|
175
178
|
headers["Content-Length"] = String(bodyBytes);
|
|
176
179
|
headers.Accept = reqBody.stream ? "text/event-stream" : "application/json";
|
|
177
180
|
try {
|
|
181
|
+
onUpstreamStart?.();
|
|
178
182
|
if (reqBody.stream === true) markUpstreamStarted();
|
|
179
183
|
return await fetchCopilotUpstream(`${tokenSession.getApiBase()}${responsesEndpointPath()}`, {
|
|
180
184
|
method: "POST",
|
|
@@ -183,6 +187,7 @@ export function createCopilotClientRuntime({
|
|
|
183
187
|
signal,
|
|
184
188
|
}, { fetchImpl, ...retryOptions });
|
|
185
189
|
} catch (error) {
|
|
190
|
+
if (signal?.aborted) throw abortError(signal);
|
|
186
191
|
const cause = error?.cause;
|
|
187
192
|
const causeText = cause ? ` (${[cause.code, cause.message].filter(Boolean).join(": ")})` : "";
|
|
188
193
|
throw new Error(`Copilot responses fetch failed: ${error.message}${causeText}`);
|
package/src/doctor.mjs
CHANGED
|
@@ -251,7 +251,11 @@ export function inspectAuthProfiles({ home = os.homedir() } = {}) {
|
|
|
251
251
|
if (!claude.configured) {
|
|
252
252
|
checks.push({ kind: "ok", message: "Claude authentication profile is not isolated and inherits Codex" });
|
|
253
253
|
} else if (!claude.valid) {
|
|
254
|
-
checks.push({
|
|
254
|
+
checks.push({
|
|
255
|
+
kind: "err",
|
|
256
|
+
message: `Claude isolated authentication profile is invalid: ${claude.reason}`,
|
|
257
|
+
fix: "ccdx auth login claude --reauth",
|
|
258
|
+
});
|
|
255
259
|
} else {
|
|
256
260
|
const account = claude.identity?.login ? ` for ${claude.identity.login}` : "";
|
|
257
261
|
checks.push({ kind: "ok", message: `Claude isolated authentication profile is configured${account}` });
|
|
@@ -455,7 +459,11 @@ export function inspectClaudeAppConfig({
|
|
|
455
459
|
const meta = readJson(paths.metaPath);
|
|
456
460
|
const appliedId = meta.ok ? String(meta.json?.appliedId || "").trim() : "";
|
|
457
461
|
if (!appliedId) {
|
|
458
|
-
checks.push({
|
|
462
|
+
checks.push({
|
|
463
|
+
kind: "warn",
|
|
464
|
+
message: "Claude App gateway profile is not configured",
|
|
465
|
+
fix: "ccdx start --configure-claude-app",
|
|
466
|
+
});
|
|
459
467
|
return checks;
|
|
460
468
|
}
|
|
461
469
|
|
|
@@ -508,6 +516,7 @@ export async function collectDoctorChecks({
|
|
|
508
516
|
host = "127.0.0.1",
|
|
509
517
|
port = 2026,
|
|
510
518
|
checkAdapter = true,
|
|
519
|
+
checkPmStudio = checkAdapter,
|
|
511
520
|
checkAdapterListeningFn = checkAdapterListening,
|
|
512
521
|
checkRunningAdapterFn = checkRunningAdapter,
|
|
513
522
|
online = false,
|
|
@@ -517,6 +526,7 @@ export async function collectDoctorChecks({
|
|
|
517
526
|
onlineTimeoutMs = 10000,
|
|
518
527
|
compatTimeoutMs = 120000,
|
|
519
528
|
inspectAdapterCompatibilityFn = inspectAdapterCompatibility,
|
|
529
|
+
inspectPmStudioHealthFn,
|
|
520
530
|
} = {}) {
|
|
521
531
|
const checks = [
|
|
522
532
|
...inspectAuthProfiles({ home }),
|
|
@@ -552,7 +562,43 @@ export async function collectDoctorChecks({
|
|
|
552
562
|
const listening = await checkAdapterListeningFn({ host, port });
|
|
553
563
|
checks.push(listening
|
|
554
564
|
? { kind: "warn", message: `A service is listening on ${localGatewayBaseUrl(host, port)}, but it is not a compatible codex-copilot-dx adapter` }
|
|
555
|
-
: {
|
|
565
|
+
: {
|
|
566
|
+
kind: "warn",
|
|
567
|
+
message: `Adapter is not listening on ${localGatewayBaseUrl(host, port)}`,
|
|
568
|
+
fix: "ccdx start",
|
|
569
|
+
});
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
if (checkPmStudio && platform === "darwin") {
|
|
574
|
+
const inspect = inspectPmStudioHealthFn || (async (options) => {
|
|
575
|
+
const module = await import("./pm-studio-status.mjs");
|
|
576
|
+
return module.inspectPmStudioStatus(options);
|
|
577
|
+
});
|
|
578
|
+
const pm = await inspect({ home });
|
|
579
|
+
const version = pm.app.metadata ? `${pm.app.metadata.version} build ${pm.app.metadata.build}` : "";
|
|
580
|
+
if (pm.app.state === "patched") {
|
|
581
|
+
checks.push({ kind: "ok", message: `PM Studio ${version} patch is verified` });
|
|
582
|
+
if (!pm.claude.valid) {
|
|
583
|
+
checks.push({
|
|
584
|
+
kind: "warn",
|
|
585
|
+
message: "PM Studio Claude routing requires a valid isolated Claude profile",
|
|
586
|
+
fix: "ccdx auth login claude --reauth",
|
|
587
|
+
});
|
|
588
|
+
}
|
|
589
|
+
if (!pm.adapter?.ok) {
|
|
590
|
+
checks.push({ kind: "warn", message: "PM Studio relay is not currently available", fix: "ccdx start" });
|
|
591
|
+
}
|
|
592
|
+
} else if (pm.app.state === "clean") {
|
|
593
|
+
checks.push({
|
|
594
|
+
kind: "warn",
|
|
595
|
+
message: `PM Studio ${version} is supported but not patched`,
|
|
596
|
+
fix: "ccdx pms setup",
|
|
597
|
+
});
|
|
598
|
+
} else if (pm.app.state === "unsupported") {
|
|
599
|
+
checks.push({ kind: "warn", message: `PM Studio ${version} has no exact patch recipe; no files will be changed` });
|
|
600
|
+
} else if (pm.app.state === "drift" || pm.app.state === "error") {
|
|
601
|
+
checks.push({ kind: "err", message: `PM Studio integrity check failed: ${pm.app.issues.join("; ")}` });
|
|
556
602
|
}
|
|
557
603
|
}
|
|
558
604
|
|
|
@@ -604,5 +650,12 @@ export async function runDoctor(options = {}) {
|
|
|
604
650
|
log(`${options.commandName || "ccdx"} doctor${flags.length ? ` ${flags.join(" ")}` : ""}`);
|
|
605
651
|
const checks = await collectDoctorChecks(options);
|
|
606
652
|
for (const check of checks) log(status(check.kind, check.message));
|
|
653
|
+
const totals = { ok: 0, warn: 0, err: 0 };
|
|
654
|
+
for (const check of checks) totals[check.kind] = (totals[check.kind] || 0) + 1;
|
|
655
|
+
log(status(totals.err ? "err" : totals.warn ? "warn" : "ok",
|
|
656
|
+
`Summary: ${totals.ok} passed, ${totals.warn} warning(s), ${totals.err} error(s)`));
|
|
657
|
+
for (const fix of new Set(checks.map((check) => check.fix).filter(Boolean))) {
|
|
658
|
+
log(status("info", `Next step: ${fix}`));
|
|
659
|
+
}
|
|
607
660
|
return checks;
|
|
608
661
|
}
|
package/src/http-transport.mjs
CHANGED
|
@@ -327,7 +327,7 @@ export function isAbortLikeError(err) {
|
|
|
327
327
|
}
|
|
328
328
|
|
|
329
329
|
export function abortErrorStatusCode(reason) {
|
|
330
|
-
if (["upstream_timeout", "stream_handshake_timeout", "stream_idle_timeout"].includes(reason)) return 504;
|
|
330
|
+
if (["responses_prepare_timeout", "upstream_timeout", "stream_handshake_timeout", "stream_idle_timeout"].includes(reason)) return 504;
|
|
331
331
|
if (reason === "client_aborted" || reason === "client_closed") return 499;
|
|
332
332
|
return 502;
|
|
333
333
|
}
|
package/src/observability.mjs
CHANGED
|
@@ -4,6 +4,7 @@ import { copilotRuntimeStatus } from "./copilot.mjs";
|
|
|
4
4
|
import { imageOptimizationStats } from "./image-optimization.mjs";
|
|
5
5
|
import { responseHistoryStats } from "./response-history.mjs";
|
|
6
6
|
import { loadRuntimeConfig } from "./runtime-config.mjs";
|
|
7
|
+
import { profileRouting } from "./profile-routing.mjs";
|
|
7
8
|
|
|
8
9
|
export { ADAPTER_STATUS_PATH };
|
|
9
10
|
const OBSERVABILITY_RUNTIME_CONFIG = loadRuntimeConfig();
|
|
@@ -153,6 +154,7 @@ export function runtimeStatusPayload({
|
|
|
153
154
|
metrics,
|
|
154
155
|
streamPerformance,
|
|
155
156
|
admission,
|
|
157
|
+
imagePressure,
|
|
156
158
|
modelRegistry,
|
|
157
159
|
codexClient,
|
|
158
160
|
claudeClient,
|
|
@@ -185,16 +187,14 @@ export function runtimeStatusPayload({
|
|
|
185
187
|
admission: admission?.diagnostics?.() || admission?.stats?.() || null,
|
|
186
188
|
response_history: responseHistoryStats(),
|
|
187
189
|
image_optimization: imageOptimizationStats(),
|
|
190
|
+
image_history_pressure: imagePressure?.snapshot?.() || null,
|
|
188
191
|
copilot: codexRuntime,
|
|
189
192
|
models: codexModels,
|
|
190
193
|
profiles: {
|
|
191
194
|
codex: { mode: "legacy", client: codexRuntime, models: codexModels },
|
|
192
195
|
claude: { mode: resolvedClaudeMode, client: claudeRuntime, models: claudeModels },
|
|
193
196
|
},
|
|
194
|
-
routing: {
|
|
195
|
-
responses: "codex",
|
|
196
|
-
messages: resolvedClaudeMode === "isolated" ? "claude" : "codex",
|
|
197
|
-
},
|
|
197
|
+
routing: profileRouting({ claudeMode: resolvedClaudeMode }),
|
|
198
198
|
limits: {
|
|
199
199
|
max_body_bytes: OBSERVABILITY_RUNTIME_CONFIG.maxBodyBytes,
|
|
200
200
|
max_decoded_body_bytes: OBSERVABILITY_RUNTIME_CONFIG.maxDecodedBodyBytes,
|
package/src/pm-studio-relay.mjs
CHANGED
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
writeOrDrain,
|
|
11
11
|
} from "./http-transport.mjs";
|
|
12
12
|
import { isLoopbackAddress } from "./observability.mjs";
|
|
13
|
+
import { createPmStudioModelRouter } from "./profile-routing.mjs";
|
|
13
14
|
import { loadRuntimeConfig, parsePositiveInteger } from "./runtime-config.mjs";
|
|
14
15
|
import { requireUpstreamEventStream } from "./stream-contract.mjs";
|
|
15
16
|
import { safeUpstreamResponseHeaders } from "./upstream-headers.mjs";
|
|
@@ -147,48 +148,6 @@ function catalogData(catalog) {
|
|
|
147
148
|
return Array.isArray(catalog?.data) ? catalog.data : null;
|
|
148
149
|
}
|
|
149
150
|
|
|
150
|
-
function originalClaudeModels(registry) {
|
|
151
|
-
return catalogData(registry?.models) || [];
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
function isClaudeCatalogEntry(model) {
|
|
155
|
-
const id = String(model?.id || "").trim();
|
|
156
|
-
const vendor = String(model?.vendor || "").trim().toLowerCase();
|
|
157
|
-
return Boolean(id) && (vendor === "anthropic" || id.toLowerCase().startsWith("claude-"));
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
function isAllowedClaudeModel(model) {
|
|
161
|
-
return isClaudeCatalogEntry(model)
|
|
162
|
-
&& String(model?.vendor || "").trim().toLowerCase() === "anthropic"
|
|
163
|
-
&& model?.model_picker_enabled !== false
|
|
164
|
-
&& Array.isArray(model?.supported_endpoints)
|
|
165
|
-
&& model.supported_endpoints.includes("/chat/completions");
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
function claudeAvailability(options) {
|
|
169
|
-
const enabled = options.claudeMode === "isolated"
|
|
170
|
-
&& options.claudeProfile?.valid === true
|
|
171
|
-
&& typeof options.claudeClient?.chatCompletions === "function";
|
|
172
|
-
const catalog = originalClaudeModels(options.claudeModelRegistry);
|
|
173
|
-
const allowed = new Map();
|
|
174
|
-
const known = new Set();
|
|
175
|
-
for (const model of catalog) {
|
|
176
|
-
const id = String(model?.id || "").trim();
|
|
177
|
-
if (!id || !isClaudeCatalogEntry(model)) continue;
|
|
178
|
-
known.add(id);
|
|
179
|
-
if (enabled && isAllowedClaudeModel(model) && !allowed.has(id)) allowed.set(id, model);
|
|
180
|
-
}
|
|
181
|
-
return { allowed, known };
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
function classifyModel(modelId, options) {
|
|
185
|
-
const id = String(modelId || "").trim();
|
|
186
|
-
const { allowed, known } = claudeAvailability(options);
|
|
187
|
-
if (allowed.has(id)) return "claude";
|
|
188
|
-
if (known.has(id) || id.toLowerCase().startsWith("claude-")) return "unsupported_claude";
|
|
189
|
-
return "enterprise";
|
|
190
|
-
}
|
|
191
|
-
|
|
192
151
|
async function responseSnapshot(response) {
|
|
193
152
|
return {
|
|
194
153
|
status: response.status,
|
|
@@ -218,10 +177,10 @@ function rejectUpstreamRedirect(response) {
|
|
|
218
177
|
return response;
|
|
219
178
|
}
|
|
220
179
|
|
|
221
|
-
function mergeModelCatalog(snapshot,
|
|
180
|
+
function mergeModelCatalog(snapshot, modelRouter) {
|
|
222
181
|
if (!snapshot.ok) return null;
|
|
223
|
-
const
|
|
224
|
-
if (!allowed.
|
|
182
|
+
const allowed = modelRouter.allowedModels();
|
|
183
|
+
if (!allowed.length) return null;
|
|
225
184
|
let enterprise;
|
|
226
185
|
try {
|
|
227
186
|
enterprise = JSON.parse(snapshot.body.toString("utf8"));
|
|
@@ -232,7 +191,8 @@ function mergeModelCatalog(snapshot, options) {
|
|
|
232
191
|
if (!data) return null;
|
|
233
192
|
const seen = new Set(data.map((model) => String(model?.id || "").trim()).filter(Boolean));
|
|
234
193
|
const additions = [];
|
|
235
|
-
for (const
|
|
194
|
+
for (const model of allowed) {
|
|
195
|
+
const id = String(model?.id || "").trim();
|
|
236
196
|
if (seen.has(id)) continue;
|
|
237
197
|
seen.add(id);
|
|
238
198
|
additions.push(model);
|
|
@@ -332,6 +292,12 @@ export function createPmStudioRelayHandler(options = {}) {
|
|
|
332
292
|
const validated = new Map();
|
|
333
293
|
const validationFlights = new Map();
|
|
334
294
|
let activeModelRequests = 0;
|
|
295
|
+
const modelRouter = createPmStudioModelRouter({
|
|
296
|
+
getCatalog: () => options.claudeModelRegistry?.models,
|
|
297
|
+
isClaudeEnabled: () => options.claudeMode === "isolated"
|
|
298
|
+
&& options.claudeProfile?.valid === true
|
|
299
|
+
&& typeof options.claudeClient?.chatCompletions === "function",
|
|
300
|
+
});
|
|
335
301
|
|
|
336
302
|
function isValidated(fingerprint) {
|
|
337
303
|
const expiresAt = validated.get(fingerprint);
|
|
@@ -452,7 +418,7 @@ export function createPmStudioRelayHandler(options = {}) {
|
|
|
452
418
|
sendSnapshot(res, snapshot);
|
|
453
419
|
return;
|
|
454
420
|
}
|
|
455
|
-
const merged = mergeModelCatalog(snapshot,
|
|
421
|
+
const merged = mergeModelCatalog(snapshot, modelRouter);
|
|
456
422
|
if (!merged) {
|
|
457
423
|
sendSnapshot(res, snapshot);
|
|
458
424
|
return;
|
|
@@ -480,7 +446,7 @@ export function createPmStudioRelayHandler(options = {}) {
|
|
|
480
446
|
}
|
|
481
447
|
abort.setTimeout(requestTimeoutMs, timeoutReason);
|
|
482
448
|
|
|
483
|
-
const modelType =
|
|
449
|
+
const modelType = modelRouter.classify(body?.model);
|
|
484
450
|
if (modelType === "unsupported_claude" || (modelType === "claude" && pathname !== "/chat/completions")) {
|
|
485
451
|
throw localError(400, "model_not_supported", "The requested Claude model is not available for this endpoint");
|
|
486
452
|
}
|