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.
@@ -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 Set(["--configure-claude-desktop", "--show-request-id"]);
4
- const CLI_COMMANDS = new Set(["ccdx", "codex-copilot-dx"]);
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 { command: "start", configureClaudeDesktop: false, showRequestId: false, online: false, compat: false };
120
+ if (!command) return baseCommand("start");
45
121
  if (HELP_COMMANDS.has(command)) {
46
- if (rest.length) unexpectedArgs(rest);
47
- return { command: "help", configureClaudeDesktop: false, showRequestId: false, online: false, compat: false };
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 { command: "version", configureClaudeDesktop: false, showRequestId: false, online: false, compat: false };
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 { command: "status", configureClaudeDesktop: false, showRequestId: false, online: false, compat: false };
137
+ return baseCommand("status");
56
138
  }
57
139
  if (command === "usage") {
58
140
  if (rest.length) unexpectedArgs(rest);
59
- return { command: "usage", configureClaudeDesktop: false, showRequestId: false, online: false, compat: false };
141
+ return baseCommand("usage");
60
142
  }
61
- if (command === "pms") {
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 (action !== "setup") throw new Error(`Unknown pms action: ${action}`);
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 { command: "pms", action: "setup", configureClaudeDesktop: false, showRequestId: false, online: false, compat: false };
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
- command: "models",
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
- command: "auth",
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
- command: "auth",
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 { command: "auto-review-model", configureClaudeDesktop: false, showRequestId: false, online: false, compat: false };
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
- command: "update",
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
- command: "doctor",
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(executable = process.argv[1]) {
205
- const name = String(executable || "").split(/[\\/]/).pop();
206
- if (name === "codex-copilot-dx.mjs") return "codex-copilot-dx";
207
- return CLI_COMMANDS.has(name) ? name : "ccdx";
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 = CLI_COMMANDS.has(commandName) ? commandName : "ccdx";
212
- const alias = name === "ccdx" ? "codex-copilot-dx" : "ccdx";
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-desktop] [--show-request-id]
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
- Equivalent command: ${alias}`;
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
  }
@@ -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
- : error?.cause?.code === "ECONNREFUSED"
134
+ : code === "ECONNREFUSED" || code === "ENETUNREACH"
107
135
  ? "adapter is not running"
108
136
  : error?.message || String(error);
109
- throw new Error(`Could not read adapter status at ${baseUrl}: ${reason}`);
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
  }
@@ -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({ kind: "err", message: `Claude isolated authentication profile is invalid: ${claude.reason}` });
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({ kind: "warn", message: "Claude App gateway profile is not configured; run with --configure-claude-desktop" });
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
- : { kind: "warn", message: `Adapter is not listening on ${localGatewayBaseUrl(host, port)}` });
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
  }
@@ -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
  }
@@ -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,
@@ -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, options) {
180
+ function mergeModelCatalog(snapshot, modelRouter) {
222
181
  if (!snapshot.ok) return null;
223
- const { allowed } = claudeAvailability(options);
224
- if (!allowed.size) return null;
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 [id, model] of allowed) {
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, options);
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 = classifyModel(body?.model, options);
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
  }