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.
- package/README.md +61 -431
- package/bin/cli.mjs +2 -2
- package/docs/README.md +55 -0
- package/docs/REFERENCE.md +481 -0
- package/package.json +4 -3
- package/src/adapter.mjs +2 -0
- package/src/auto-review-model.mjs +47 -8
- package/src/cli-options.mjs +3 -3
- package/src/cli-status.mjs +3 -0
- package/src/codex-client-doctor.mjs +67 -0
- package/src/codex-model-catalog.mjs +16 -2
- package/src/copilot-client.mjs +2 -1
- package/src/copilot-responses-policy.mjs +35 -18
- package/src/dashboard/index.html +7 -0
- package/src/dashboard/language.js +11 -0
- package/src/dashboard/ui.js +30 -0
- package/src/doctor.mjs +3 -0
- package/src/image-optimization.mjs +1 -1
- package/src/models.mjs +16 -0
- package/src/observability.mjs +17 -0
- package/src/responses-bridge.mjs +1 -1
- package/src/responses-chat-payload.mjs +2 -1
- package/src/responses-handler.mjs +23 -10
- package/src/responses-proxy.mjs +10 -0
- package/src/sse-keepalive.mjs +42 -0
- package/src/stream-performance.mjs +14 -1
- package/src/usage.mjs +3 -0
- package/src/user-settings.mjs +32 -7
|
@@ -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
|
-
|
|
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,
|
|
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 =
|
|
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
|
}
|
package/src/cli-options.mjs
CHANGED
|
@@ -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
|
|
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\
|
|
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
|
|
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.`;
|
package/src/cli-status.mjs
CHANGED
|
@@ -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
|
-
|
|
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
|
}
|
package/src/copilot-client.mjs
CHANGED
|
@@ -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 (!
|
|
47
|
+
if (!body || typeof body !== "object") return false;
|
|
48
48
|
const removedNames = new Set();
|
|
49
|
-
const
|
|
50
|
-
|
|
51
|
-
|
|
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
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
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
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
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;
|
package/src/dashboard/index.html
CHANGED
|
@@ -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": "已知模型",
|
package/src/dashboard/ui.js
CHANGED
|
@@ -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();
|
package/src/observability.mjs
CHANGED
|
@@ -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(),
|
package/src/responses-bridge.mjs
CHANGED
|
@@ -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) =>
|
|
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
|
|