codex-copilot-dx 0.9.12 → 0.9.14

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.
@@ -3,12 +3,13 @@ import { createInterface } from "node:readline/promises";
3
3
  import { loadModelCache } from "./model-cache.mjs";
4
4
  import {
5
5
  DEFAULT_CODEX_AUTO_REVIEW_MODEL,
6
+ autoReviewReasoningEfforts,
6
7
  responsesModelIdsFromCopilotModels,
7
8
  } from "./models.mjs";
8
9
  import { parseAdapterProbeOptions } from "./cli-options.mjs";
9
10
  import { adapterBaseUrl } from "./running-adapter.mjs";
10
11
  import {
11
- autoReviewModelPreference,
12
+ autoReviewPreference,
12
13
  readUserSettings,
13
14
  writeAutoReviewModel,
14
15
  } from "./user-settings.mjs";
@@ -28,6 +29,13 @@ function sortedModelIds(modelIds, currentModel) {
28
29
  });
29
30
  }
30
31
 
32
+ function catalogReasoningEfforts(models, modelIds) {
33
+ const data = Array.isArray(models) ? models : models?.data;
34
+ return Object.fromEntries(modelIds.map((id) => [id,
35
+ autoReviewReasoningEfforts(data?.find((model) => String(model?.id || "").trim() === id)),
36
+ ]));
37
+ }
38
+
31
39
  export async function loadAutoReviewModelCatalog({
32
40
  env = process.env,
33
41
  fetchImpl = fetch,
@@ -52,7 +60,8 @@ export async function loadAutoReviewModelCatalog({
52
60
  const modelIds = responsesModelIdsFromCopilotModels(models);
53
61
  if (modelIds.length) {
54
62
  const fallback = response.headers.get("X-CCDX-Model-Source") === "last-known-good";
55
- return { modelIds, source: fallback ? "running adapter cache" : "running adapter" };
63
+ return { modelIds, reasoningEfforts: catalogReasoningEfforts(models, modelIds),
64
+ source: fallback ? "running adapter cache" : "running adapter" };
56
65
  }
57
66
  liveError = "adapter returned no selectable Responses models";
58
67
  }
@@ -66,7 +75,7 @@ export async function loadAutoReviewModelCatalog({
66
75
 
67
76
  const cached = loadModelCacheFn({ home });
68
77
  const modelIds = responsesModelIdsFromCopilotModels(cached);
69
- if (modelIds.length) return { modelIds, source: "local model cache" };
78
+ if (modelIds.length) return { modelIds, reasoningEfforts: catalogReasoningEfforts(cached, modelIds), source: "local model cache" };
70
79
  throw new Error(`No selectable Responses models are available (${liveError}). Start ccdx once to refresh the model list.`);
71
80
  }
72
81
 
@@ -85,13 +94,14 @@ export async function runAutoReviewModelCommand({
85
94
  }
86
95
 
87
96
  readUserSettings({ env, home, strict: true });
88
- const current = autoReviewModelPreference({ env, home });
97
+ const current = autoReviewPreference({ env, home });
89
98
  const catalog = await loadCatalog({ env, fetchImpl, home });
90
99
  const modelIds = sortedModelIds(catalog.modelIds, current.model);
91
100
  if (!modelIds.length) throw new Error("No selectable Responses models are available");
92
101
 
93
102
  output.write(`${commandName} auto-review-model\n`);
94
103
  output.write(`Current: ${current.model} (${current.source})\n`);
104
+ output.write(`Reasoning: ${current.reasoningEffort || "follow client (low when omitted)"}\n`);
95
105
  output.write(`Responses models from ${catalog.source}:\n`);
96
106
  modelIds.forEach((model, index) => {
97
107
  const markers = [];
@@ -111,6 +121,7 @@ export async function runAutoReviewModelCommand({
111
121
  });
112
122
 
113
123
  let selectedModel;
124
+ let selectedEffort;
114
125
  try {
115
126
  while (!selectedModel) {
116
127
  const answer = String(await ask(`Select a model [${defaultIndex + 1}], or q to cancel: `) || "").trim();
@@ -125,17 +136,45 @@ export async function runAutoReviewModelCommand({
125
136
  output.write(`Enter a number from 1 to ${modelIds.length}, or q to cancel.\n`);
126
137
  }
127
138
  }
139
+ const efforts = catalog.reasoningEfforts?.[selectedModel] || [];
140
+ const choices = [null, ...efforts];
141
+ const currentEffort = selectedModel === current.model ? current.reasoningEffort : undefined;
142
+ const effortIndex = selectedModel === current.model && current.reasoningEffort === null
143
+ ? 0 : currentEffort ? efforts.indexOf(currentEffort) + 1 : efforts.indexOf("low") + 1;
144
+ output.write(`Reasoning for ${selectedModel}:\n`);
145
+ output.write(" 0. Follow client (low when omitted)\n");
146
+ efforts.forEach((effort, index) => {
147
+ const markers = [];
148
+ if (effort === currentEffort) markers.push("current");
149
+ if (effort === "low") markers.push("default");
150
+ output.write(` ${index + 1}. ${effort}${markers.length ? ` [${markers.join(", ")}]` : ""}\n`);
151
+ });
152
+ if (!efforts.length) output.write("No reasoning efforts are advertised; no override will be set.\n");
153
+ while (selectedEffort === undefined) {
154
+ const answer = String(await ask(`Select reasoning [${effortIndex}], or q to cancel: `) || "").trim();
155
+ if (["q", "quit"].includes(answer.toLowerCase())) {
156
+ output.write("No changes made.\n");
157
+ return { changed: false, cancelled: true, model: current.model };
158
+ }
159
+ const selectedIndex = answer === "" ? effortIndex : Number(answer);
160
+ if (Number.isInteger(selectedIndex) && selectedIndex >= 0 && selectedIndex < choices.length) {
161
+ selectedEffort = choices[selectedIndex];
162
+ } else {
163
+ output.write(`Enter a number from 0 to ${efforts.length}, or q to cancel.\n`);
164
+ }
165
+ }
128
166
  } finally {
129
167
  readline?.close();
130
168
  }
131
169
 
132
170
  const savedModel = selectedModel === DEFAULT_CODEX_AUTO_REVIEW_MODEL ? "" : selectedModel;
133
- const result = writeAutoReviewModel(savedModel, { env, home });
171
+ const result = writeAutoReviewModel(savedModel, { env, home, reasoningEffort: selectedEffort });
134
172
  output.write(`${result.changed ? "Saved" : "Kept"} Auto Review model: ${selectedModel}\n`);
173
+ output.write(`Reasoning: ${selectedEffort || "follow client (low when omitted)"}\n`);
135
174
  if (String(env.CCDX_AUTO_REVIEW_MODEL || "").trim()) {
136
- output.write(`CCDX_AUTO_REVIEW_MODEL=${current.model} remains the effective override until it is unset.\n`);
175
+ output.write(`CCDX_AUTO_REVIEW_MODEL=${current.model} remains the effective override until it is unset. Reasoning is bound to ${selectedModel}.\n`);
137
176
  } else {
138
- output.write("The running adapter will use this model on the next Auto Review request.\n");
177
+ output.write("The running adapter will use this model on the next Auto Review request. Reasoning selection requires an adapter running 0.9.13 or newer.\n");
139
178
  }
140
- return { ...result, cancelled: false, model: selectedModel };
179
+ return { ...result, cancelled: false, model: selectedModel, reasoningEffort: selectedEffort };
141
180
  }
@@ -344,7 +344,7 @@ function topicHelp(name, topic) {
344
344
  start: `Usage:\n ${name} [start] [--show-request-id]\n\nStarts or reuses the local adapter, updates Codex configuration, and attempts to open Codex App when auto-launch is enabled and supported.`,
345
345
  auth: `Usage:\n ${name} auth status [--online] [--format table|plain]\n\nShows the saved GitHub Copilot account without exposing credentials. --online also verifies its entitlement and model catalog.`,
346
346
  "auth status": `Usage:\n ${name} auth status [--online] [--format table|plain]\n\nShows the saved GitHub Copilot account without exposing credentials. --online also verifies its entitlement and model catalog. Interactive terminals use a table by default.`,
347
- doctor: `Usage:\n ${name} doctor [--online] [--compat]\n ${name} doctor config\n\nChecks the local credential, Codex configuration, and adapter. --online validates account entitlement; --compat sends minimal inference requests and consumes a small amount of Copilot usage. The config subcommand checks only the local file and previews startup changes, without network, authentication, or file writes.`,
347
+ doctor: `Usage:\n ${name} doctor [--online] [--compat]\n ${name} doctor config\n\nChecks the local credential, Codex configuration, adapter, installed App version, running App identities and observed client catalog. App-internal cache is not externally verified. No processes are killed or client files changed. --online validates account entitlement; --compat sends minimal inference requests and consumes a small amount of Copilot usage. The config subcommand checks only the local file and previews startup changes, without network, authentication, file writes or process inspection.`,
348
348
  "doctor config": `Usage:\n ${name} doctor config\n\nReads ~/.codex/config.toml, checks TOML syntax and CCDX-managed settings, and previews the existing startup writer's changes. Does not merge Codex profiles or validate the complete installed Codex schema. No authentication, network requests, file writes, or adapter/GUI startup. Existing context_management=false is valid. Exit 1 for errors, 0 for OK/warnings; unsupported arguments exit 2.`,
349
349
  status: `Usage:\n ${name} status\n\nShows bounded runtime, routing, performance, queue, and cache metrics from a running adapter.`,
350
350
  models: `Usage:\n ${name} models [--format table|plain]\n\nPerforms a fresh, read-only Copilot model-directory lookup for the saved account. Interactive terminals use a table by default.`,
@@ -355,7 +355,7 @@ function topicHelp(name, topic) {
355
355
  "enable-image": `Usage:\n ${name} enable-image\n\nInteractively configures an HTTPS image API endpoint (base URLs are also accepted) and API key, detects its supported image model and protocol, installs CCDX image guidance, and checks the local image tool for Codex App without generating an image. A blank key preserves the saved key only for the same origin; a new origin requires explicit key entry. Concurrent unrelated config edits are preserved.`,
356
356
  "disable-image": `Usage:\n ${name} disable-image\n\nDisables the local CCDX image tool and removes its stored API credential and unmodified CCDX image guidance.`,
357
357
  "image-status": `Usage:\n ${name} image-status\n\nShows provider configuration and probes local image-tool readiness without generating an image or exposing its API key.`,
358
- "auto-review-model": `Usage:\n ${name} auto-review-model\n\nInteractively selects an advertised Responses model for Codex Auto-review.`,
358
+ "auto-review-model": `Usage:\n ${name} auto-review-model\n\nSelect a Responses model, then an advertised reasoning effort. Enter keeps the default/current choice; q cancels both steps without saving. Effort 0 follows the client (low when omitted). Settings apply only to Auto-review on the next request; no client patch or restart is required for adapters running 0.9.13 or newer.`,
359
359
  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.`,
360
360
  version: `Usage:\n ${name} --version`,
361
361
  };
@@ -398,7 +398,7 @@ Commands:
398
398
  enable-image Configure and enable the optional image provider
399
399
  disable-image Disable the image provider and remove its credential
400
400
  image-status Show the optional image provider status
401
- auto-review-model Select the Codex Auto-review Responses model
401
+ auto-review-model Select the Codex Auto-review model and reasoning effort
402
402
  update Update the global package from npm or GitHub
403
403
 
404
404
  Run ${name} <command> --help for command details.`;
@@ -195,5 +195,8 @@ export function formatAdapterStatus({ baseUrl, data }, { commandName = "ccdx", c
195
195
  lines.push(status("info", `Routing: /v1/responses -> ${routingTarget(data.routing.responses)}`));
196
196
  }
197
197
  lines.push(status("info", `Limits: request body ${mebibytes(limits.max_body_bytes)}, decoded body ${mebibytes(limits.max_decoded_body_bytes)}`));
198
+ const recentContext = data.stream_performance?.recent_requests?.at(-1)?.context;
199
+ const contextValue = (value, format) => value === null || value === undefined ? "n/a" : format(value);
200
+ if (recentContext) lines.push(status("info", `Latest request context: wire ${contextValue(recentContext.payload_bytes, mebibytes)}, ${contextValue(recentContext.images, count)} images, input ${contextValue(recentContext.input_tokens, count)} tokens / window ${contextValue(recentContext.context_window_tokens, count)} (reported metadata, not live session occupancy)`));
198
201
  return lines.join("\n");
199
202
  }
@@ -0,0 +1,67 @@
1
+ import fs from "node:fs";
2
+ import os from "node:os";
3
+ import path from "node:path";
4
+ import { execFile } from "node:child_process";
5
+ import { promisify } from "node:util";
6
+ import { readAdapterStatus } from "./cli-status.mjs";
7
+
8
+ export function parseRunningCodexApps(stdout) {
9
+ const apps = new Map();
10
+ for (const line of String(stdout).split("\n")) {
11
+ const match = /^\s*(\d+)\s+.*\/(ChatGPT|Codex)\.app\/Contents\//.exec(line);
12
+ if (!match) continue;
13
+ const name = `${match[2]}.app`;
14
+ const pids = apps.get(name) || [];
15
+ if (pids.length < 8) pids.push(Number(match[1]));
16
+ apps.set(name, pids);
17
+ }
18
+ return [...apps].map(([name, pids]) => ({ name, pids }));
19
+ }
20
+
21
+ export async function inspectCodexClient({
22
+ home = os.homedir(), platform = process.platform, running,
23
+ host, port, fetchImpl = fetch, readFileFn = fs.readFileSync,
24
+ execFileFn = promisify(execFile), readStatus = readAdapterStatus,
25
+ } = {}) {
26
+ const checks = [];
27
+ if (platform === "darwin") {
28
+ const seen = new Set();
29
+ for (const root of ["/Applications", path.join(home, "Applications")]) {
30
+ for (const name of ["ChatGPT.app", "Codex.app"]) {
31
+ try {
32
+ const plist = readFileFn(path.join(root, name, "Contents", "Info.plist"), "utf8");
33
+ const version = /<key>CFBundleShortVersionString<\/key>\s*<string>([0-9A-Za-z.+_-]{1,64})<\/string>/.exec(plist)?.[1];
34
+ if (version && !seen.has(`${name}/${version}`)) {
35
+ checks.push({ kind: "info", message: `Installed ${name}: App version ${version}` });
36
+ seen.add(`${name}/${version}`);
37
+ }
38
+ } catch {}
39
+ }
40
+ }
41
+ try {
42
+ // comm contains executable names, never command-line arguments or environments.
43
+ const { stdout } = await execFileFn("ps", ["-axo", "pid=,comm="], { timeout: 1500, maxBuffer: 256 * 1024, encoding: "utf8" });
44
+ const apps = parseRunningCodexApps(stdout);
45
+ for (const app of apps) checks.push({ kind: "info", message: `Running ${app.name}: PID ${app.pids.join(", ")}; fully quit with Command-Q before reopening (closing the window may keep it running)` });
46
+ if (!apps.length) checks.push({ kind: "info", message: "No running Codex/ChatGPT App process observed" });
47
+ } catch { checks.push({ kind: "info", message: "Running Codex App processes could not be inspected" }); }
48
+ }
49
+ if (running?.ok || running?.incompatible) {
50
+ try {
51
+ const { data } = await readStatus({ host, port, fetchImpl, timeoutMs: 1500 });
52
+ const source = ["live", "cache", "built-in"].includes(data.models?.source) ? data.models.source : "unknown";
53
+ checks.push({ kind: "info", message: `Running adapter ${data.version}; upstream model directory source: ${source}` });
54
+ const catalog = data.models?.client_catalog;
55
+ const observed = catalog?.last_request;
56
+ if (!catalog) {
57
+ checks.push({ kind: "info", message: "Running adapter does not expose client-catalog diagnostics; upgrade and restart CCDX" });
58
+ } else if (observed) {
59
+ const version = /^[0-9]+\.[0-9]+\.[0-9]+$/.test(observed.client_version || "") ? observed.client_version : "unknown";
60
+ const app = ["ChatGPT.app", "Codex.app", "custom binary"].includes(observed.application) ? observed.application : "unknown App";
61
+ checks.push({ kind: "info", message: `Last catalog request: client ${version}, ${app}, ${observed.source === "bundled" ? "bundled" : "unavailable"}; version match ${observed.matched === true ? "observed" : "not verified"}` });
62
+ } else checks.push({ kind: "info", message: "No client model-catalog request observed by this adapter" });
63
+ } catch { checks.push({ kind: "info", message: "Client model-catalog diagnostics unavailable from the running adapter" }); }
64
+ }
65
+ checks.push({ kind: "info", message: "App-internal model cache is not externally verified; process age alone does not prove a stale catalog" });
66
+ return checks;
67
+ }
@@ -248,6 +248,7 @@ export function createCodexModelCatalog({
248
248
  const catalogFailures = new Map();
249
249
  const catalogPending = new Map();
250
250
  let activeBinaryKeys = new Set();
251
+ let lastObservation = null;
251
252
 
252
253
  function retainCurrentEntries(map) {
253
254
  for (const key of map.keys()) {
@@ -341,8 +342,21 @@ export function createCodexModelCatalog({
341
342
 
342
343
  async function load({ clientVersion = "" } = {}) {
343
344
  const candidate = await selectBinary(clientVersion);
344
- return candidate ? loadCatalog(candidate) : null;
345
+ const catalog = candidate ? await loadCatalog(candidate) : null;
346
+ const app = candidate?.binaryPath.match(/(?:^|\/)(ChatGPT|Codex)\.app\//)?.[1];
347
+ lastObservation = {
348
+ client_version: wholeSemver(clientVersion) || null,
349
+ application: app ? `${app}.app` : candidate ? "custom binary" : null,
350
+ source: catalog ? "bundled" : "unavailable",
351
+ matched: Boolean(catalog && wholeSemver(clientVersion)),
352
+ observed_at: new Date(now()).toISOString(),
353
+ };
354
+ return catalog;
345
355
  }
346
356
 
347
- return { load };
357
+ return { load, snapshot: () => ({
358
+ last_request: lastObservation ? { ...lastObservation } : null,
359
+ observed_versions: [...new Set(versionCache.values())].slice(0, 8),
360
+ client_cache_verified: false,
361
+ }) };
348
362
  }
@@ -5,7 +5,7 @@ import { debugLog } from "./log.mjs";
5
5
  import { enforceResponsesPayloadByteBudget } from "./responses-byte-budget.mjs";
6
6
  import { status } from "./status.mjs";
7
7
  import { withChatStreamUsage } from "./stream-contract.mjs";
8
- import { markUpstreamStarted } from "./stream-performance.mjs";
8
+ import { markRequestObservation, markUpstreamStarted } from "./stream-performance.mjs";
9
9
 
10
10
  function abortError(signal) {
11
11
  if (signal?.reason instanceof Error) return signal.reason;
@@ -178,6 +178,7 @@ export function createCopilotClientRuntime({
178
178
  ? preparedPayload.summary
179
179
  : summarizeReqBody(reqBody);
180
180
  console.log(status("info", `responses payload bytes=${bodyBytes} input_items=${summary.items} images=${summary.images}`));
181
+ markRequestObservation({ payload_bytes: bodyBytes, images: summary.images });
181
182
  if (adapted) debugLog(`responses payload adapted stage=${stage} bytes=${bodyBytes}/${preparedPayload.targetBytes}`);
182
183
  const headers = buildHeaders({ token, version: getVSCodeVersion(), initiator: "user", vision: false });
183
184
  headers["Content-Type"] = "application/json; charset=utf-8";
@@ -44,27 +44,44 @@ function toolName(tool) {
44
44
  // the removal. Revalidate against Copilot before deleting or broadening it.
45
45
  // Reference: https://github.com/Menci/Floway/blob/ac2bbb04352033a7d9d74574a9d465d40395ef06/packages/provider-copilot/src/interceptors/openai-responses/strip-image-generation.ts
46
46
  export function applyCopilotResponsesRequestPolicies(body, options = {}) {
47
- if (!Array.isArray(body?.tools)) return false;
47
+ if (!body || typeof body !== "object") return false;
48
48
  const removedNames = new Set();
49
- const filtered = body.tools.filter((tool) => {
50
- const removed = isCopilotImageNamespaceTool(tool, options);
51
- if (removed) {
49
+ const survivingNames = new Set();
50
+ let survivingTools = 0;
51
+ let changed = false;
52
+ const filter = (container, topLevel = false) => {
53
+ if (!Array.isArray(container?.tools)) return;
54
+ const tools = container.tools;
55
+ const filtered = tools.filter((tool) => {
56
+ const removed = isCopilotImageNamespaceTool(tool, options);
52
57
  const name = toolName(tool);
53
- if (name) removedNames.add(name);
54
- }
55
- return !removed;
56
- });
57
- if (filtered.length === body.tools.length) return false;
58
- if (filtered.length) body.tools = filtered;
59
- else delete body.tools;
58
+ if (removed && name) removedNames.add(name);
59
+ if (!removed) {
60
+ survivingTools += 1;
61
+ if (name) survivingNames.add(name);
62
+ }
63
+ return !removed;
64
+ });
65
+ if (filtered.length === tools.length) return;
66
+ changed = true;
67
+ if (topLevel && !filtered.length) delete container.tools;
68
+ else container.tools = filtered;
69
+ };
70
+ filter(body, true);
71
+ for (const item of Array.isArray(body.input) ? body.input : []) {
72
+ if (["additional_tools", "tool_search_output"].includes(item?.type)) filter(item);
73
+ }
74
+ if (!changed) return false;
60
75
 
61
- const choiceName = toolName(body.tool_choice);
62
- const choiceTargetsRemovedTool = choiceName
63
- && removedNames.has(choiceName)
64
- && !filtered.some((tool) => toolName(tool) === choiceName);
65
- if (isCopilotImageNamespaceTool(body.tool_choice, options)
66
- || choiceTargetsRemovedTool
67
- || (body.tool_choice === "required" && !body.tools)) {
76
+ const removedChoice = (choice) => isCopilotImageNamespaceTool(choice, options)
77
+ || (removedNames.has(toolName(choice)) && !survivingNames.has(toolName(choice)));
78
+ if (body.tool_choice?.type === "allowed_tools" && Array.isArray(body.tool_choice.tools)) {
79
+ const choices = body.tool_choice.tools.filter((choice) => !removedChoice(choice));
80
+ body.tool_choice = choices.length ? { ...body.tool_choice, tools: choices } : "none";
81
+ } else if (removedChoice(body.tool_choice)) {
82
+ if (survivingTools) body.tool_choice = "none";
83
+ else delete body.tool_choice;
84
+ } else if (body.tool_choice === "required" && !survivingTools) {
68
85
  delete body.tool_choice;
69
86
  }
70
87
  return true;
@@ -48,6 +48,13 @@
48
48
  </article>
49
49
  </section>
50
50
 
51
+ <details class="panel dashboard-section" id="request-context">
52
+ <summary data-i18n>RECENT REQUEST CONTEXT</summary>
53
+ <p id="context-note" class="table-note">—</p>
54
+ <div class="table-wrap"><table><thead><tr><th scope="col" data-i18n>MODEL</th><th scope="col" data-i18n>WIRE MiB</th><th scope="col" data-i18n>IMAGES</th><th scope="col" data-i18n>INPUT TOKENS</th><th scope="col" data-i18n>MODEL WINDOW</th><th scope="col" data-i18n>INPUT / WINDOW</th><th scope="col" data-i18n>OUTCOME</th></tr></thead><tbody id="context-body"><tr><td colspan="7" data-i18n>No observed request metadata.</td></tr></tbody></table></div>
55
+ <p class="hint" data-i18n>Recent upstream request metadata, not exact live session occupancy. Missing values are unknown. No prompt or image content is retained.</p>
56
+ </details>
57
+
51
58
  <section class="panel dashboard-section" aria-label="Live upstream models" data-i18n-label>
52
59
  <div class="panel-heading"><h2 data-i18n>UPSTREAM GPT MODELS</h2><div class="heading-actions"><span id="models-state" data-i18n>CHECKING LIVE</span><button id="refresh-models" type="button" data-i18n>Refresh</button></div></div>
53
60
  <p id="models-note" class="table-note" data-i18n>Querying GitHub Copilot…</p>
@@ -10,6 +10,17 @@
10
10
  "Switch to light mode": "切换为日间模式",
11
11
  "Switch to dark mode": "切换为夜间模式",
12
12
  "Overview": "概览",
13
+ "RECENT REQUEST CONTEXT": "近期请求上下文",
14
+ "WIRE MiB": "上游请求 MiB",
15
+ "IMAGES": "图片数",
16
+ "INPUT TOKENS": "输入 Token",
17
+ "MODEL WINDOW": "模型窗口",
18
+ "INPUT / WINDOW": "输入 / 窗口",
19
+ "OUTCOME": "终态",
20
+ "Unknown": "未知",
21
+ "No observed request metadata.": "暂无已观察到的请求元数据。",
22
+ "20 latest finished requests / compactions: {completed} completed, {incomplete} incomplete, {failed} failed": "最近 20 次已结束请求 / 压缩:{completed} 已完成、{incomplete} 未完成、{failed} 失败",
23
+ "Recent upstream request metadata, not exact live session occupancy. Missing values are unknown. No prompt or image content is retained.": "近期上游请求元数据,不等同于会话实时精确占用。缺失值表示未知;不保留提示词或图片内容。",
13
24
  "Runtime details": "运行详情",
14
25
  "ADAPTER": "适配服务",
15
26
  "KNOWN MODELS": "已知模型",
@@ -16,6 +16,8 @@ const finite = (value) => value !== null && value !== undefined && Number.isFini
16
16
  const number = (value) => finite(value) ? Math.max(0, Math.round(Number(value))).toLocaleString() : "—";
17
17
  const mib = (value) => finite(value) ? `${(Number(value) / 1048576).toFixed(1)} MiB` : "—";
18
18
  let authLoading = false;
19
+ let contextRequests = [];
20
+ let contextCompactions = {};
19
21
 
20
22
  function renderAuth(data) {
21
23
  const account = data.login ? `@${data.login}` : data.id ? `ID ${data.id}` : phrase("account unknown");
@@ -156,9 +158,36 @@ function render(data) {
156
158
  : "Setup state unknown / ccdx image-status", { active: number(image?.active), failed: number(image?.failed), delivery: number(image?.delivery_failures) });
157
159
 
158
160
  renderFailures(data.response_failures?.recent, data.stream_performance?.recent_requests);
161
+ renderContext(data.stream_performance);
159
162
  text("updated", "Snapshot {time}", { time: new Date().toLocaleTimeString() });
160
163
  }
161
164
 
165
+ function renderContext(performance) {
166
+ if (performance !== undefined) {
167
+ contextRequests = Array.isArray(performance?.recent_requests) ? performance.recent_requests.slice(-20).reverse() : [];
168
+ contextCompactions = performance?.by_route?.responses_compact?.terminal_outcomes?.totals || {};
169
+ }
170
+ const body = element("context-body");
171
+ if (!body) return;
172
+ text("context-note", "20 latest finished requests / compactions: {completed} completed, {incomplete} incomplete, {failed} failed", {
173
+ completed: number(contextCompactions.completed), incomplete: number(contextCompactions.incomplete), failed: number(contextCompactions.failed),
174
+ });
175
+ if (element("request-context")?.open === false) return;
176
+ body.replaceChildren();
177
+ for (const request of contextRequests) {
178
+ const context = request.context || {};
179
+ const outcome = ["completed", "incomplete", "failed", "cancelled"].includes(request.outcome)
180
+ ? request.outcome[0].toUpperCase() + request.outcome.slice(1) : "Unknown";
181
+ const ratio = finite(context.input_tokens) && finite(context.context_window_tokens) && Number(context.context_window_tokens) > 0
182
+ ? `${(100 * Number(context.input_tokens) / Number(context.context_window_tokens)).toFixed(1)}%` : "—";
183
+ body.append(tableRow([String(request.model || "unknown").slice(0, 80),
184
+ finite(context.payload_bytes) ? (Number(context.payload_bytes) / 1048576).toFixed(2) : "—",
185
+ number(context.images), number(context.input_tokens), number(context.context_window_tokens), ratio,
186
+ outcome], { labels: [6] }));
187
+ }
188
+ if (!contextRequests.length) emptyTable("context-body", 7, "No observed request metadata.");
189
+ }
190
+
162
191
  async function refresh() {
163
192
  const button = element("refresh");
164
193
  button.disabled = true;
@@ -186,6 +215,7 @@ async function refresh() {
186
215
  }
187
216
 
188
217
  element("refresh").addEventListener("click", () => { refresh(); loadAuth(); });
218
+ element("request-context").addEventListener("toggle", () => { if (element("request-context").open) renderContext(); });
189
219
  refresh();
190
220
  loadAuth();
191
221
 
package/src/doctor.mjs CHANGED
@@ -312,6 +312,7 @@ export async function collectDoctorChecks({
312
312
  onlineTimeoutMs = 10000,
313
313
  compatTimeoutMs = 120000,
314
314
  inspectAdapterCompatibilityFn = inspectAdapterCompatibility,
315
+ inspectCodexClientFn,
315
316
  } = {}) {
316
317
  if (configOnly) return inspectCodexConfig({ home, host, port });
317
318
  checkedDoctorProfile(profile);
@@ -353,6 +354,8 @@ export async function collectDoctorChecks({
353
354
  fix: "ccdx start",
354
355
  });
355
356
  }
357
+ const inspectClient = inspectCodexClientFn || (await import("./codex-client-doctor.mjs")).inspectCodexClient;
358
+ checks.push(...await inspectClient({ home, host, port, fetchImpl, running }));
356
359
  }
357
360
 
358
361
  if (compat) {
@@ -546,7 +546,7 @@ export function summarizeReqBody(reqBody) {
546
546
 
547
547
  for (const item of input) {
548
548
  if (["input_image", "image", "image_url"].includes(item?.type)) countImages([item]);
549
- if (item?.type === "message") countImages(item.content);
549
+ if (item?.type === "message" || item?.type === "agent_message") countImages(item.content);
550
550
  const toolOutput = readResponsesToolOutputParts(item);
551
551
  if (toolOutput) countImages(toolOutput.parts);
552
552
  }
package/src/models.mjs CHANGED
@@ -1,6 +1,16 @@
1
1
  export const CODEX_AUTO_REVIEW_MODEL = "codex-auto-review";
2
2
  export const DEFAULT_CODEX_AUTO_REVIEW_MODEL = "gpt-6.1-sol";
3
3
  export const CODEX_GPT6_MODEL = "gpt-6-astra";
4
+ export const AUTO_REVIEW_REASONING_EFFORTS = ["low", "medium", "high", "xhigh", "max", "minimal", "none"];
5
+
6
+ export function autoReviewReasoningEfforts(model) {
7
+ const advertised = model?.capabilities?.supports?.reasoning_effort;
8
+ const supported = new Set(Array.isArray(advertised) ? advertised : []);
9
+ const id = String(model?.id || "").trim();
10
+ const gpt61Sol = id === "gpt-6.1-sol" || id === "gpt-6.1-sol-fast";
11
+ return AUTO_REVIEW_REASONING_EFFORTS.filter((effort) => supported.has(effort)
12
+ && !(gpt61Sol && ["none", "minimal"].includes(effort)));
13
+ }
4
14
 
5
15
  function hasOpenAIEndpoint(model) {
6
16
  const endpoints = Array.isArray(model?.supported_endpoints) ? model.supported_endpoints : [];
@@ -46,6 +56,12 @@ function copilotModelData(models) {
46
56
  return Array.isArray(data) ? data : [];
47
57
  }
48
58
 
59
+ export function modelContextWindowTokens(models, id) {
60
+ const model = copilotModelData(models).find((entry) => String(entry?.id || "").trim() === id);
61
+ const limit = model?.capabilities?.limits?.max_context_window_tokens;
62
+ return Number.isSafeInteger(limit) && limit > 0 ? limit : null;
63
+ }
64
+
49
65
  function uniqueIds(models, predicate) {
50
66
  const ids = [];
51
67
  const seen = new Set();
@@ -178,10 +178,27 @@ export function runtimeStatusPayload({
178
178
  modelRegistry,
179
179
  codexClient,
180
180
  codexModelRegistry,
181
+ codexModelCatalog,
181
182
  } = {}) {
182
183
  const memory = process.memoryUsage();
183
184
  const codexRuntime = safeClientRuntimeStatus(codexClient);
184
185
  const codexModels = modelRegistryStatus(codexModelRegistry || modelRegistry);
186
+ if (typeof codexModelCatalog?.snapshot === "function") {
187
+ const snapshot = codexModelCatalog.snapshot();
188
+ const observed = snapshot?.last_request;
189
+ const version = (value) => /^\d+\.\d+\.\d+$/.test(value || "") ? value : null;
190
+ codexModels.client_catalog = {
191
+ last_request: observed ? {
192
+ client_version: version(observed.client_version),
193
+ application: ["ChatGPT.app", "Codex.app", "custom binary"].includes(observed.application) ? observed.application : null,
194
+ source: observed.source === "bundled" ? "bundled" : "unavailable",
195
+ matched: observed.matched === true,
196
+ observed_at: typeof observed.observed_at === "string" && /^\d{4}-\d{2}-\d{2}T[0-9:.]+Z$/.test(observed.observed_at) ? observed.observed_at : null,
197
+ } : null,
198
+ observed_versions: (Array.isArray(snapshot?.observed_versions) ? snapshot.observed_versions : []).filter(version).slice(0, 8),
199
+ client_cache_verified: false,
200
+ };
201
+ }
185
202
  const history = responseHistoryStats();
186
203
  return {
187
204
  ...adapterHealthPayload(),
@@ -49,7 +49,7 @@ export function responsesBodyUsesCustomTools(body) {
49
49
  if (body?.tool_choice?.type === "custom") return true;
50
50
  if (Array.isArray(body?.tools) && body.tools.some((tool) => tool?.type === "custom")) return true;
51
51
  return Array.isArray(body?.input)
52
- && body.input.some((item) => item?.type === "custom_tool_call" || item?.type === "custom_tool_call_output");
52
+ && body.input.some((item) => ["custom_tool_call", "custom_tool_call_output", "additional_tools", "tool_search_output"].includes(item?.type));
53
53
  }
54
54
 
55
55
  function chatCompatibilityError(kind, itemType) {
@@ -6,7 +6,7 @@ import {
6
6
  import { responsesToChat } from "./responses-bridge.mjs";
7
7
  import { readResponsesImagePart, readResponsesToolOutputParts } from "./responses-content.mjs";
8
8
  import { withChatStreamUsage } from "./stream-contract.mjs";
9
- import { measureRequestStage } from "./stream-performance.mjs";
9
+ import { markRequestObservation, measureRequestStage } from "./stream-performance.mjs";
10
10
 
11
11
  const DEFAULT_MAX_UPSTREAM_BODY_BYTES = 30 * 1024 * 1024;
12
12
 
@@ -74,6 +74,7 @@ function checkedChatPayload(chatReq, payload, limit, stage, adapted, assertActiv
74
74
  stage: stage === "chat" ? stage : `${stage}+chat`,
75
75
  targetBytes: limit,
76
76
  }, { assertActive });
77
+ markRequestObservation({ payload_bytes: payload.bodyBytes });
77
78
  return { chatReq, ...payload, adapted, stage };
78
79
  }
79
80