codex-copilot-dx 0.4.44 → 0.5.2
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 +47 -3
- package/bin/cli.mjs +55 -7
- package/bin/codex-copilot-dx.mjs +2 -0
- package/package.json +2 -2
- package/src/adapter.mjs +2 -0
- package/src/auto-review-model.mjs +141 -0
- package/src/cli-options.mjs +50 -7
- package/src/cli-status.mjs +122 -0
- package/src/doctor.mjs +1 -1
- package/src/models.mjs +15 -4
- package/src/observability.mjs +2 -2
- package/src/package-update.mjs +97 -0
- package/src/responses-handler.mjs +11 -2
- package/src/running-adapter.mjs +1 -0
- package/src/user-settings.mjs +67 -0
package/README.md
CHANGED
|
@@ -12,7 +12,7 @@ A single in-process adapter (port `2026`) exposes both APIs over your Copilot su
|
|
|
12
12
|
|
|
13
13
|
Supports both HTTP SSE streaming and non-streaming.
|
|
14
14
|
|
|
15
|
-
Codex Auto-review requests use the hidden `codex-auto-review` model ID. The adapter maps it to Copilot's `gpt-5.5` Responses model by default and logs both model IDs when the mapping is used.
|
|
15
|
+
Codex Auto-review requests use the hidden `codex-auto-review` model ID. The adapter maps it to Copilot's `gpt-5.5` Responses model by default and logs both model IDs when the mapping is used. The target can be changed interactively without reinstalling the package.
|
|
16
16
|
|
|
17
17
|
## Prerequisites
|
|
18
18
|
|
|
@@ -30,6 +30,42 @@ ccdx
|
|
|
30
30
|
The shorter `ccdx` command is the primary launcher. The existing `codex-copilot-dx`
|
|
31
31
|
command remains an equivalent compatibility alias. For a one-off run without a
|
|
32
32
|
global install, use `npx codex-copilot-dx@latest`.
|
|
33
|
+
Both installed commands expose the same options and subcommands; help, version,
|
|
34
|
+
doctor, status, and argument-error output use the command name that was invoked.
|
|
35
|
+
|
|
36
|
+
To change the model used by Codex Auto-review, run:
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
ccdx auto-review-model
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
The selector reads the current adapter model list, falling back to the fresh local
|
|
43
|
+
model cache, and offers only enabled models that advertise a Responses endpoint.
|
|
44
|
+
It saves the selection in `~/.config/codex-copilot-dx/config.json` (or under
|
|
45
|
+
`XDG_CONFIG_HOME` when set); choosing `gpt-5.5` clears the override and restores
|
|
46
|
+
the package default. A running 0.5.1+
|
|
47
|
+
adapter reads the setting on the next Auto-review request, so it does not need to
|
|
48
|
+
be restarted. `codex-copilot-dx auto-review-model` is fully equivalent.
|
|
49
|
+
|
|
50
|
+
To update the globally installed package, choose a source interactively:
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
ccdx update
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
For scripts or a direct choice, pass the source explicitly:
|
|
57
|
+
|
|
58
|
+
```bash
|
|
59
|
+
ccdx update npm
|
|
60
|
+
ccdx update github
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
The npm source installs `codex-copilot-dx@latest` through the registry already
|
|
64
|
+
configured for npm, including a company mirror. The GitHub source installs the
|
|
65
|
+
latest commit from `DaleXiao/codex-copilot-dx` `main`. Both paths use npm's
|
|
66
|
+
global installer without shell interpolation. A currently running adapter keeps
|
|
67
|
+
its loaded version until it is stopped and started again. The
|
|
68
|
+
`codex-copilot-dx update` compatibility command behaves identically.
|
|
33
69
|
|
|
34
70
|
On first run, it will:
|
|
35
71
|
1. Authenticate with GitHub via device flow (if needed), after first trying compatible local Copilot token sources
|
|
@@ -75,7 +111,15 @@ ccdx doctor --compat
|
|
|
75
111
|
|
|
76
112
|
The compatibility doctor sends a few minimal Copilot requests to check Codex Auto-review, native Responses, streaming history, compaction, image tool namespace handling, and Anthropic streaming. It consumes a small amount of Copilot usage, never starts the adapter or device flow, and does not change client configuration. Combine it with `--online` when both the saved-token entitlement check and the adapter protocol checks are needed.
|
|
77
113
|
|
|
78
|
-
For
|
|
114
|
+
For a read-only summary of the running adapter, use:
|
|
115
|
+
|
|
116
|
+
```bash
|
|
117
|
+
ccdx status
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
The compatibility alias supports the same command as
|
|
121
|
+
`codex-copilot-dx status`. For the complete machine-readable payload, query the
|
|
122
|
+
loopback endpoint directly:
|
|
79
123
|
|
|
80
124
|
```bash
|
|
81
125
|
curl -s http://127.0.0.1:2026/_ccdx/status | jq
|
|
@@ -135,7 +179,7 @@ Environment variables:
|
|
|
135
179
|
| `CCDX_STREAM_IDLE_TIMEOUT_MS` | `120000` | Maximum idle time between upstream streaming body chunks |
|
|
136
180
|
| `CCDX_UPSTREAM_RETRIES` | `2` | Retries for safe requests and clearly pre-connect POST failures; capped at `5` |
|
|
137
181
|
| `CCDX_UPSTREAM_RETRY_DELAY_MS` | `300` | Initial upstream retry backoff in milliseconds; capped at `5000` |
|
|
138
|
-
| `CCDX_AUTO_REVIEW_MODEL` | `gpt-5.5` | Copilot Responses model used for Codex Auto-review requests |
|
|
182
|
+
| `CCDX_AUTO_REVIEW_MODEL` | saved selection or `gpt-5.5` | Copilot Responses model used for Codex Auto-review requests; overrides the interactive selection |
|
|
139
183
|
| `CCDX_LOG_PATH` | unset | Mirror terminal logs to a file; set to `1` for `~/.local/share/codex-copilot-dx/debug.log` |
|
|
140
184
|
| `CCDX_LOG_LEVEL` | `info` | Set to `debug` to include upstream request attempts, status codes, retry causes, and timings |
|
|
141
185
|
| `CCDX_LOG_MAX_BYTES` | `16777216` | Rotate the debug log at this size, retaining one `.1` backup; set to `0` to disable rotation |
|
package/bin/cli.mjs
CHANGED
|
@@ -16,23 +16,27 @@ import { adapterBaseUrl, checkRunningAdapter } from "../src/running-adapter.mjs"
|
|
|
16
16
|
import { assertSafeAdapterHost, isLoopbackHost } from "../src/security.mjs";
|
|
17
17
|
import { isValidModelList, loadModelCache, saveModelCache } from "../src/model-cache.mjs";
|
|
18
18
|
import { initializeModelRegistry, runInBackground } from "../src/startup.mjs";
|
|
19
|
-
import { cliHelp, parseCliArgs, parseRuntimeOptions } from "../src/cli-options.mjs";
|
|
19
|
+
import { cliCommandName, cliHelp, parseAdapterProbeOptions, parseCliArgs, parseRuntimeOptions } from "../src/cli-options.mjs";
|
|
20
|
+
import { formatAdapterStatus, readAdapterStatus } from "../src/cli-status.mjs";
|
|
20
21
|
import { closeHttpServer } from "../src/shutdown.mjs";
|
|
22
|
+
import { runAutoReviewModelCommand } from "../src/auto-review-model.mjs";
|
|
23
|
+
import { autoReviewModelPreference } from "../src/user-settings.mjs";
|
|
21
24
|
|
|
22
25
|
const LOCAL_VERSION = localPackageVersion();
|
|
23
|
-
const
|
|
26
|
+
const CLI_NAME = cliCommandName();
|
|
27
|
+
const CLI_BANNER = `${CLI_NAME} v${LOCAL_VERSION} by Dale Xiao`;
|
|
24
28
|
|
|
25
29
|
let CLI;
|
|
26
30
|
try {
|
|
27
31
|
CLI = parseCliArgs(process.argv.slice(2));
|
|
28
32
|
} catch (e) {
|
|
29
33
|
console.error(e.message);
|
|
30
|
-
console.error(
|
|
34
|
+
console.error(`Run ${CLI_NAME} --help for usage.`);
|
|
31
35
|
process.exit(2);
|
|
32
36
|
}
|
|
33
37
|
|
|
34
38
|
if (CLI.command === "help") {
|
|
35
|
-
console.log(cliHelp());
|
|
39
|
+
console.log(cliHelp(CLI_NAME));
|
|
36
40
|
process.exit(0);
|
|
37
41
|
}
|
|
38
42
|
if (CLI.command === "version") {
|
|
@@ -43,6 +47,46 @@ if (CLI.command === "usage") {
|
|
|
43
47
|
await printUsageSummary();
|
|
44
48
|
process.exit(0);
|
|
45
49
|
}
|
|
50
|
+
if (CLI.command === "status") {
|
|
51
|
+
let probe;
|
|
52
|
+
try {
|
|
53
|
+
probe = parseAdapterProbeOptions(process.env);
|
|
54
|
+
} catch (e) {
|
|
55
|
+
console.error(e.message);
|
|
56
|
+
process.exit(2);
|
|
57
|
+
}
|
|
58
|
+
try {
|
|
59
|
+
const snapshot = await readAdapterStatus({
|
|
60
|
+
host: probe.adapterHost,
|
|
61
|
+
port: probe.adapterPort,
|
|
62
|
+
timeoutMs: probe.existingAdapterTimeoutMs,
|
|
63
|
+
});
|
|
64
|
+
console.log(formatAdapterStatus(snapshot, { commandName: CLI_NAME, cliVersion: LOCAL_VERSION }));
|
|
65
|
+
process.exit(0);
|
|
66
|
+
} catch (e) {
|
|
67
|
+
console.error(status("err", e.message));
|
|
68
|
+
process.exit(1);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
if (CLI.command === "auto-review-model") {
|
|
72
|
+
try {
|
|
73
|
+
await runAutoReviewModelCommand({ commandName: CLI_NAME });
|
|
74
|
+
process.exit(0);
|
|
75
|
+
} catch (e) {
|
|
76
|
+
console.error(status("err", e.message));
|
|
77
|
+
process.exit(1);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
if (CLI.command === "update") {
|
|
81
|
+
try {
|
|
82
|
+
const { runPackageUpdateCommand } = await import("../src/package-update.mjs");
|
|
83
|
+
await runPackageUpdateCommand({ commandName: CLI_NAME, source: CLI.updateSource });
|
|
84
|
+
process.exit(0);
|
|
85
|
+
} catch (e) {
|
|
86
|
+
console.error(status("err", e.message));
|
|
87
|
+
process.exit(1);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
46
90
|
|
|
47
91
|
let RUNTIME;
|
|
48
92
|
try {
|
|
@@ -105,9 +149,11 @@ async function refreshClaudeDesktopModelDefs() {
|
|
|
105
149
|
throw new Error(`Copilot models returned HTTP ${httpStatus}`);
|
|
106
150
|
}
|
|
107
151
|
const models = JSON.parse(body);
|
|
108
|
-
const autoReview = codexAutoReviewModelStatus(models
|
|
152
|
+
const autoReview = codexAutoReviewModelStatus(models, process.env, {
|
|
153
|
+
autoReviewModel: autoReviewModelPreference().model,
|
|
154
|
+
});
|
|
109
155
|
if (!autoReview.available) {
|
|
110
|
-
console.log(status("warn", `Auto-review target ${autoReview.upstreamModel} is unavailable: ${autoReview.reason}. Run
|
|
156
|
+
console.log(status("warn", `Auto-review target ${autoReview.upstreamModel} is unavailable: ${autoReview.reason}. Run ${CLI_NAME} doctor --compat to verify the live path.`));
|
|
111
157
|
}
|
|
112
158
|
const gptModelIds = gptModelIdsFromCopilotModels(models);
|
|
113
159
|
if (gptModelIds.length) {
|
|
@@ -169,7 +215,7 @@ async function reuseRunningAdapterIfAvailable() {
|
|
|
169
215
|
});
|
|
170
216
|
if (running.incompatible) {
|
|
171
217
|
const found = running.data?.version || "unknown";
|
|
172
|
-
throw new Error(`Adapter ${found} is already running at ${running.baseUrl}, but this CLI is ${LOCAL_VERSION}. Stop the existing process and run
|
|
218
|
+
throw new Error(`Adapter ${found} is already running at ${running.baseUrl}, but this CLI is ${LOCAL_VERSION}. Stop the existing process and run ${CLI_NAME} again.`);
|
|
173
219
|
}
|
|
174
220
|
if (!running.ok) return false;
|
|
175
221
|
|
|
@@ -206,6 +252,7 @@ async function reuseRunningAdapterIfAvailable() {
|
|
|
206
252
|
|
|
207
253
|
if (CLI.command === "doctor") {
|
|
208
254
|
const checks = await runDoctor({
|
|
255
|
+
commandName: CLI_NAME,
|
|
209
256
|
port: ADAPTER_PORT,
|
|
210
257
|
host: ADAPTER_HOST,
|
|
211
258
|
online: CLI.online,
|
|
@@ -268,6 +315,7 @@ try {
|
|
|
268
315
|
activeServer = await startAdapter(ADAPTER_PORT, ADAPTER_HOST, {
|
|
269
316
|
claudeDesktopApiKey,
|
|
270
317
|
claudeDesktopModelDefs,
|
|
318
|
+
autoReviewModelResolver: () => autoReviewModelPreference().model,
|
|
271
319
|
modelRegistry: MODEL_REGISTRY,
|
|
272
320
|
showRequestId: CLI.showRequestId,
|
|
273
321
|
upstreamTimeoutMs: RUNTIME.upstreamTimeoutMs,
|
package/package.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "codex-copilot-dx",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.2",
|
|
4
4
|
"description": "Use Codex Desktop, Claude Code, and Claude App with GitHub Copilot",
|
|
5
5
|
"author": "Dale Xiao",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"bin": {
|
|
8
8
|
"ccdx": "bin/cli.mjs",
|
|
9
|
-
"codex-copilot-dx": "bin/
|
|
9
|
+
"codex-copilot-dx": "bin/codex-copilot-dx.mjs"
|
|
10
10
|
},
|
|
11
11
|
"files": [
|
|
12
12
|
"bin/",
|
package/src/adapter.mjs
CHANGED
|
@@ -106,6 +106,7 @@ export function createAdapterHandler(options = {}) {
|
|
|
106
106
|
};
|
|
107
107
|
const responsesHandler = createResponsesHandler({
|
|
108
108
|
acquireRequest,
|
|
109
|
+
autoReviewModelResolver: options.autoReviewModelResolver,
|
|
109
110
|
chatCompletionsFn,
|
|
110
111
|
openAIModelEnv,
|
|
111
112
|
responsesPayloadOptions: options.responsesPayloadOptions,
|
|
@@ -116,6 +117,7 @@ export function createAdapterHandler(options = {}) {
|
|
|
116
117
|
});
|
|
117
118
|
const responsesCompactHandler = createResponsesCompactHandler({
|
|
118
119
|
acquireRequest,
|
|
120
|
+
autoReviewModelResolver: options.autoReviewModelResolver,
|
|
119
121
|
openAIModelEnv,
|
|
120
122
|
responsesCompactFn,
|
|
121
123
|
streamHandshakeTimeoutMs,
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import os from "node:os";
|
|
2
|
+
import { createInterface } from "node:readline/promises";
|
|
3
|
+
import { loadModelCache } from "./model-cache.mjs";
|
|
4
|
+
import {
|
|
5
|
+
DEFAULT_CODEX_AUTO_REVIEW_MODEL,
|
|
6
|
+
responsesModelIdsFromCopilotModels,
|
|
7
|
+
} from "./models.mjs";
|
|
8
|
+
import { parseAdapterProbeOptions } from "./cli-options.mjs";
|
|
9
|
+
import { adapterBaseUrl } from "./running-adapter.mjs";
|
|
10
|
+
import {
|
|
11
|
+
autoReviewModelPreference,
|
|
12
|
+
readUserSettings,
|
|
13
|
+
writeAutoReviewModel,
|
|
14
|
+
} from "./user-settings.mjs";
|
|
15
|
+
|
|
16
|
+
const DEFAULT_MODEL_FETCH_TIMEOUT_MS = 5000;
|
|
17
|
+
|
|
18
|
+
function modelFetchTimeout(env) {
|
|
19
|
+
const value = Number(env.CCDX_MODEL_REFRESH_TIMEOUT_MS);
|
|
20
|
+
return Number.isSafeInteger(value) && value > 0 ? value : DEFAULT_MODEL_FETCH_TIMEOUT_MS;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function sortedModelIds(modelIds, currentModel) {
|
|
24
|
+
return [...modelIds].sort((left, right) => {
|
|
25
|
+
const leftRank = left === currentModel ? 0 : left === DEFAULT_CODEX_AUTO_REVIEW_MODEL ? 1 : 2;
|
|
26
|
+
const rightRank = right === currentModel ? 0 : right === DEFAULT_CODEX_AUTO_REVIEW_MODEL ? 1 : 2;
|
|
27
|
+
return leftRank - rightRank || left.localeCompare(right);
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export async function loadAutoReviewModelCatalog({
|
|
32
|
+
env = process.env,
|
|
33
|
+
fetchImpl = fetch,
|
|
34
|
+
home = os.homedir(),
|
|
35
|
+
loadModelCacheFn = loadModelCache,
|
|
36
|
+
} = {}) {
|
|
37
|
+
const { adapterHost, adapterPort } = parseAdapterProbeOptions(env);
|
|
38
|
+
const url = `${adapterBaseUrl(adapterHost, adapterPort)}/v1/models`;
|
|
39
|
+
const ctrl = new AbortController();
|
|
40
|
+
const timer = setTimeout(() => ctrl.abort(), modelFetchTimeout(env));
|
|
41
|
+
let liveError = "adapter is not running";
|
|
42
|
+
try {
|
|
43
|
+
const response = await fetchImpl(url, {
|
|
44
|
+
headers: { Accept: "application/json" },
|
|
45
|
+
redirect: "error",
|
|
46
|
+
signal: ctrl.signal,
|
|
47
|
+
});
|
|
48
|
+
if (!response.ok) {
|
|
49
|
+
liveError = `adapter returned HTTP ${response.status}`;
|
|
50
|
+
} else {
|
|
51
|
+
const models = await response.json();
|
|
52
|
+
const modelIds = responsesModelIdsFromCopilotModels(models);
|
|
53
|
+
if (modelIds.length) {
|
|
54
|
+
const fallback = response.headers.get("X-CCDX-Model-Source") === "last-known-good";
|
|
55
|
+
return { modelIds, source: fallback ? "running adapter cache" : "running adapter" };
|
|
56
|
+
}
|
|
57
|
+
liveError = "adapter returned no selectable Responses models";
|
|
58
|
+
}
|
|
59
|
+
} catch (error) {
|
|
60
|
+
liveError = ctrl.signal.aborted
|
|
61
|
+
? `adapter model lookup timed out after ${modelFetchTimeout(env)}ms`
|
|
62
|
+
: error?.message || String(error);
|
|
63
|
+
} finally {
|
|
64
|
+
clearTimeout(timer);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const cached = loadModelCacheFn({ home });
|
|
68
|
+
const modelIds = responsesModelIdsFromCopilotModels(cached);
|
|
69
|
+
if (modelIds.length) return { modelIds, source: "local model cache" };
|
|
70
|
+
throw new Error(`No selectable Responses models are available (${liveError}). Start ccdx once to refresh the model list.`);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export async function runAutoReviewModelCommand({
|
|
74
|
+
commandName = "ccdx",
|
|
75
|
+
env = process.env,
|
|
76
|
+
fetchImpl = fetch,
|
|
77
|
+
home = os.homedir(),
|
|
78
|
+
input = process.stdin,
|
|
79
|
+
loadCatalog = loadAutoReviewModelCatalog,
|
|
80
|
+
output = process.stdout,
|
|
81
|
+
prompt,
|
|
82
|
+
} = {}) {
|
|
83
|
+
if (!prompt && (!input.isTTY || !output.isTTY)) {
|
|
84
|
+
throw new Error(`${commandName} auto-review-model requires an interactive terminal`);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
readUserSettings({ env, home, strict: true });
|
|
88
|
+
const current = autoReviewModelPreference({ env, home });
|
|
89
|
+
const catalog = await loadCatalog({ env, fetchImpl, home });
|
|
90
|
+
const modelIds = sortedModelIds(catalog.modelIds, current.model);
|
|
91
|
+
if (!modelIds.length) throw new Error("No selectable Responses models are available");
|
|
92
|
+
|
|
93
|
+
output.write(`${commandName} auto-review-model\n`);
|
|
94
|
+
output.write(`Current: ${current.model} (${current.source})\n`);
|
|
95
|
+
output.write(`Responses models from ${catalog.source}:\n`);
|
|
96
|
+
modelIds.forEach((model, index) => {
|
|
97
|
+
const markers = [];
|
|
98
|
+
if (model === current.model) markers.push("current");
|
|
99
|
+
if (model === DEFAULT_CODEX_AUTO_REVIEW_MODEL) markers.push("default");
|
|
100
|
+
output.write(` ${index + 1}. ${model}${markers.length ? ` [${markers.join(", ")}]` : ""}\n`);
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
let defaultIndex = modelIds.indexOf(current.model);
|
|
104
|
+
if (defaultIndex < 0) defaultIndex = modelIds.indexOf(DEFAULT_CODEX_AUTO_REVIEW_MODEL);
|
|
105
|
+
if (defaultIndex < 0) defaultIndex = 0;
|
|
106
|
+
|
|
107
|
+
let readline;
|
|
108
|
+
const ask = prompt || (async (question) => {
|
|
109
|
+
readline ||= createInterface({ input, output });
|
|
110
|
+
return readline.question(question);
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
let selectedModel;
|
|
114
|
+
try {
|
|
115
|
+
while (!selectedModel) {
|
|
116
|
+
const answer = String(await ask(`Select a model [${defaultIndex + 1}], or q to cancel: `) || "").trim();
|
|
117
|
+
if (answer.toLowerCase() === "q" || answer.toLowerCase() === "quit") {
|
|
118
|
+
output.write("No changes made.\n");
|
|
119
|
+
return { changed: false, cancelled: true, model: current.model };
|
|
120
|
+
}
|
|
121
|
+
const selectedIndex = answer === "" ? defaultIndex : Number(answer) - 1;
|
|
122
|
+
if (Number.isInteger(selectedIndex) && selectedIndex >= 0 && selectedIndex < modelIds.length) {
|
|
123
|
+
selectedModel = modelIds[selectedIndex];
|
|
124
|
+
} else {
|
|
125
|
+
output.write(`Enter a number from 1 to ${modelIds.length}, or q to cancel.\n`);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
} finally {
|
|
129
|
+
readline?.close();
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const savedModel = selectedModel === DEFAULT_CODEX_AUTO_REVIEW_MODEL ? "" : selectedModel;
|
|
133
|
+
const result = writeAutoReviewModel(savedModel, { env, home });
|
|
134
|
+
output.write(`${result.changed ? "Saved" : "Kept"} Auto Review model: ${selectedModel}\n`);
|
|
135
|
+
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`);
|
|
137
|
+
} else {
|
|
138
|
+
output.write("The running 0.5.1+ adapter will use this model on the next Auto Review request.\n");
|
|
139
|
+
}
|
|
140
|
+
return { ...result, cancelled: false, model: selectedModel };
|
|
141
|
+
}
|
package/src/cli-options.mjs
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
const HELP_COMMANDS = new Set(["help", "--help", "-h"]);
|
|
2
2
|
const VERSION_COMMANDS = new Set(["version", "--version", "-v"]);
|
|
3
3
|
const START_OPTIONS = new Set(["--configure-claude-desktop", "--show-request-id"]);
|
|
4
|
+
const CLI_COMMANDS = new Set(["ccdx", "codex-copilot-dx"]);
|
|
4
5
|
|
|
5
6
|
function unexpectedArgs(args) {
|
|
6
7
|
throw new Error(`Unexpected argument${args.length === 1 ? "" : "s"}: ${args.join(" ")}`);
|
|
@@ -17,10 +18,33 @@ export function parseCliArgs(args = []) {
|
|
|
17
18
|
if (rest.length) unexpectedArgs(rest);
|
|
18
19
|
return { command: "version", configureClaudeDesktop: false, showRequestId: false, online: false, compat: false };
|
|
19
20
|
}
|
|
21
|
+
if (command === "status" || command === "--status") {
|
|
22
|
+
if (rest.length) unexpectedArgs(rest);
|
|
23
|
+
return { command: "status", configureClaudeDesktop: false, showRequestId: false, online: false, compat: false };
|
|
24
|
+
}
|
|
20
25
|
if (command === "usage") {
|
|
21
26
|
if (rest.length) unexpectedArgs(rest);
|
|
22
27
|
return { command: "usage", configureClaudeDesktop: false, showRequestId: false, online: false, compat: false };
|
|
23
28
|
}
|
|
29
|
+
if (command === "auto-review-model") {
|
|
30
|
+
if (rest.length) unexpectedArgs(rest);
|
|
31
|
+
return { command: "auto-review-model", configureClaudeDesktop: false, showRequestId: false, online: false, compat: false };
|
|
32
|
+
}
|
|
33
|
+
if (command === "update") {
|
|
34
|
+
if (rest.length > 1) unexpectedArgs(rest.slice(1));
|
|
35
|
+
const source = rest[0];
|
|
36
|
+
if (source && !new Set(["npm", "github", "gh"]).has(source)) {
|
|
37
|
+
throw new Error(`Update source must be npm or github: ${source}`);
|
|
38
|
+
}
|
|
39
|
+
return {
|
|
40
|
+
command: "update",
|
|
41
|
+
configureClaudeDesktop: false,
|
|
42
|
+
showRequestId: false,
|
|
43
|
+
online: false,
|
|
44
|
+
compat: false,
|
|
45
|
+
updateSource: source === "gh" ? "github" : source,
|
|
46
|
+
};
|
|
47
|
+
}
|
|
24
48
|
if (command === "doctor" || command === "--doctor") {
|
|
25
49
|
const supported = new Set(["--online", "--compat"]);
|
|
26
50
|
const invalid = rest.filter((arg) => !supported.has(arg));
|
|
@@ -80,13 +104,32 @@ export function parseRuntimeOptions(env = process.env) {
|
|
|
80
104
|
};
|
|
81
105
|
}
|
|
82
106
|
|
|
83
|
-
export function
|
|
107
|
+
export function parseAdapterProbeOptions(env = process.env) {
|
|
108
|
+
return {
|
|
109
|
+
adapterPort: integerEnv(env, "ADAPTER_PORT", 2026, { min: 1, max: 65535 }),
|
|
110
|
+
adapterHost: String(env.ADAPTER_HOST || "127.0.0.1").trim() || "127.0.0.1",
|
|
111
|
+
existingAdapterTimeoutMs: integerEnv(env, "CCDX_EXISTING_ADAPTER_TIMEOUT_MS", 500, { min: 1 }),
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export function cliCommandName(executable = process.argv[1]) {
|
|
116
|
+
const name = String(executable || "").split(/[\\/]/).pop();
|
|
117
|
+
if (name === "codex-copilot-dx.mjs") return "codex-copilot-dx";
|
|
118
|
+
return CLI_COMMANDS.has(name) ? name : "ccdx";
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export function cliHelp(commandName = "ccdx") {
|
|
122
|
+
const name = CLI_COMMANDS.has(commandName) ? commandName : "ccdx";
|
|
123
|
+
const alias = name === "ccdx" ? "codex-copilot-dx" : "ccdx";
|
|
84
124
|
return `Usage:
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
125
|
+
${name} [--configure-claude-desktop] [--show-request-id]
|
|
126
|
+
${name} doctor [--online] [--compat]
|
|
127
|
+
${name} status
|
|
128
|
+
${name} usage
|
|
129
|
+
${name} auto-review-model
|
|
130
|
+
${name} update [npm|github]
|
|
131
|
+
${name} --version
|
|
132
|
+
${name} --help
|
|
90
133
|
|
|
91
|
-
|
|
134
|
+
Equivalent command: ${alias}`;
|
|
92
135
|
}
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import { ADAPTER_STATUS_PATH, adapterBaseUrl } from "./running-adapter.mjs";
|
|
2
|
+
import { status } from "./status.mjs";
|
|
3
|
+
|
|
4
|
+
function finiteNumber(value) {
|
|
5
|
+
const number = Number(value);
|
|
6
|
+
return Number.isFinite(number) ? number : null;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
function count(value) {
|
|
10
|
+
const number = finiteNumber(value);
|
|
11
|
+
return number === null ? "n/a" : String(Math.max(0, Math.round(number)));
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function duration(value) {
|
|
15
|
+
const milliseconds = finiteNumber(value);
|
|
16
|
+
if (milliseconds === null) return "n/a";
|
|
17
|
+
const seconds = Math.max(0, Math.round(milliseconds / 1000));
|
|
18
|
+
const days = Math.floor(seconds / 86400);
|
|
19
|
+
const hours = Math.floor((seconds % 86400) / 3600);
|
|
20
|
+
const minutes = Math.floor((seconds % 3600) / 60);
|
|
21
|
+
const remainder = seconds % 60;
|
|
22
|
+
if (days) return `${days}d ${hours}h`;
|
|
23
|
+
if (hours) return `${hours}h ${minutes}m`;
|
|
24
|
+
if (minutes) return `${minutes}m ${remainder}s`;
|
|
25
|
+
return `${remainder}s`;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function latency(value) {
|
|
29
|
+
const milliseconds = finiteNumber(value);
|
|
30
|
+
if (milliseconds === null) return "n/a";
|
|
31
|
+
if (milliseconds >= 1000) return `${(milliseconds / 1000).toFixed(2)}s`;
|
|
32
|
+
return `${milliseconds.toFixed(1)}ms`;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function mebibytes(value) {
|
|
36
|
+
const bytes = finiteNumber(value);
|
|
37
|
+
return bytes === null ? "n/a" : `${(bytes / (1024 * 1024)).toFixed(1)}MiB`;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function tpot(metric) {
|
|
41
|
+
const average = finiteNumber(metric?.avg);
|
|
42
|
+
if (average === null) return "n/a";
|
|
43
|
+
const milliseconds = metric?.unit === "us" ? average / 1000 : average;
|
|
44
|
+
return `${milliseconds.toFixed(2)}ms/token`;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function cacheHitRate(cache = {}) {
|
|
48
|
+
const hits = finiteNumber(cache.cache_hits);
|
|
49
|
+
const misses = finiteNumber(cache.cache_misses);
|
|
50
|
+
if (hits === null || misses === null || hits + misses <= 0) return "n/a";
|
|
51
|
+
return `${((hits / (hits + misses)) * 100).toFixed(1)}%`;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export async function readAdapterStatus({
|
|
55
|
+
host = "127.0.0.1",
|
|
56
|
+
port = 2026,
|
|
57
|
+
timeoutMs = 500,
|
|
58
|
+
fetchImpl = fetch,
|
|
59
|
+
} = {}) {
|
|
60
|
+
const baseUrl = adapterBaseUrl(host, port);
|
|
61
|
+
const ctrl = new AbortController();
|
|
62
|
+
const timer = setTimeout(() => ctrl.abort(), timeoutMs);
|
|
63
|
+
try {
|
|
64
|
+
const response = await fetchImpl(`${baseUrl}${ADAPTER_STATUS_PATH}`, {
|
|
65
|
+
headers: { Accept: "application/json" },
|
|
66
|
+
redirect: "error",
|
|
67
|
+
signal: ctrl.signal,
|
|
68
|
+
});
|
|
69
|
+
if (!response.ok) throw new Error(`returned HTTP ${response.status}`);
|
|
70
|
+
|
|
71
|
+
let data;
|
|
72
|
+
try {
|
|
73
|
+
data = await response.json();
|
|
74
|
+
} catch {
|
|
75
|
+
throw new Error("returned invalid JSON");
|
|
76
|
+
}
|
|
77
|
+
if (data?.ok !== true
|
|
78
|
+
|| data?.name !== "codex-copilot-dx"
|
|
79
|
+
|| typeof data.version !== "string"
|
|
80
|
+
|| !Number.isInteger(data.pid)) {
|
|
81
|
+
throw new Error("returned an incompatible status payload");
|
|
82
|
+
}
|
|
83
|
+
return { baseUrl, data };
|
|
84
|
+
} catch (error) {
|
|
85
|
+
const reason = ctrl.signal.aborted
|
|
86
|
+
? `timed out after ${timeoutMs}ms`
|
|
87
|
+
: error?.cause?.code === "ECONNREFUSED"
|
|
88
|
+
? "adapter is not running"
|
|
89
|
+
: error?.message || String(error);
|
|
90
|
+
throw new Error(`Could not read adapter status at ${baseUrl}: ${reason}`);
|
|
91
|
+
} finally {
|
|
92
|
+
clearTimeout(timer);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export function formatAdapterStatus({ baseUrl, data }, { commandName = "ccdx", cliVersion = data.version } = {}) {
|
|
97
|
+
const requests = data.requests || {};
|
|
98
|
+
const performance = data.stream_performance?.by_route?.responses || {};
|
|
99
|
+
const admission = data.admission || {};
|
|
100
|
+
const history = data.response_history || {};
|
|
101
|
+
const images = data.image_optimization || {};
|
|
102
|
+
const models = data.models || {};
|
|
103
|
+
const copilot = data.copilot || {};
|
|
104
|
+
const limits = data.limits || {};
|
|
105
|
+
const processStats = data.process || {};
|
|
106
|
+
const adapterLine = data.version === cliVersion
|
|
107
|
+
? status("ok", `Adapter ${data.version} is running at ${baseUrl} (PID ${data.pid}, uptime ${duration(data.uptime_ms)})`)
|
|
108
|
+
: status("warn", `Adapter ${data.version} is running at ${baseUrl}, but this CLI is ${cliVersion}; stop the running adapter before switching versions`);
|
|
109
|
+
|
|
110
|
+
return [
|
|
111
|
+
`${commandName} status`,
|
|
112
|
+
adapterLine,
|
|
113
|
+
status("info", `Requests: ${count(requests.completed)}/${count(requests.total)} completed, ${count(requests.active)} active, ${count(requests.status_4xx)} 4xx, ${count(requests.status_5xx)} 5xx, ${count(requests.aborted)} aborted`),
|
|
114
|
+
status("info", `Responses stream: TTFT avg ${latency(performance.ttft_ms?.avg)} (${count(performance.ttft_ms?.samples)} samples), TPOT avg ${tpot(performance.tpot_us)}`),
|
|
115
|
+
status("info", `Admission: ${count(admission.activeRequests)} active, ${count(admission.queued)} queued, ${count(admission.rejected)} rejected, ${count(admission.timedOut)} timed out, wait avg ${latency(admission.waitMsAvg)}`),
|
|
116
|
+
status("info", `Memory: RSS ${mebibytes(processStats.rss_bytes)}, heap ${mebibytes(processStats.heap_used_bytes)}`),
|
|
117
|
+
status("info", `History: ${mebibytes(history.bytes)} / ${mebibytes(limits.response_history_max_bytes)}, ${count(history.entries)} entries, ${count(history.evicted)} evicted`),
|
|
118
|
+
status("info", `Image cache: ${count(images.cache_entries)} entries, ${cacheHitRate(images)} hits, ${mebibytes(images.cache_bytes)} / ${mebibytes(images.cache_max_bytes)}`),
|
|
119
|
+
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"}`),
|
|
120
|
+
status("info", `Limits: request body ${mebibytes(limits.max_body_bytes)}, decoded body ${mebibytes(limits.max_decoded_body_bytes)}`),
|
|
121
|
+
].join("\n");
|
|
122
|
+
}
|
package/src/doctor.mjs
CHANGED
|
@@ -455,7 +455,7 @@ export async function collectDoctorChecks({
|
|
|
455
455
|
export async function runDoctor(options = {}) {
|
|
456
456
|
const log = options.log || console.log;
|
|
457
457
|
const flags = [options.online ? "--online" : "", options.compat ? "--compat" : ""].filter(Boolean);
|
|
458
|
-
log(
|
|
458
|
+
log(`${options.commandName || "ccdx"} doctor${flags.length ? ` ${flags.join(" ")}` : ""}`);
|
|
459
459
|
const checks = await collectDoctorChecks(options);
|
|
460
460
|
for (const check of checks) log(status(check.kind, check.message));
|
|
461
461
|
return checks;
|
package/src/models.mjs
CHANGED
|
@@ -158,6 +158,17 @@ export function gptModelIdsFromCopilotModels(models) {
|
|
|
158
158
|
});
|
|
159
159
|
}
|
|
160
160
|
|
|
161
|
+
export function responsesModelIdsFromCopilotModels(models) {
|
|
162
|
+
return uniqueIds(copilotModelData(models), (model) => {
|
|
163
|
+
const id = String(model?.id || "").trim();
|
|
164
|
+
const endpoints = modelEndpoints(model);
|
|
165
|
+
return id !== CODEX_AUTO_REVIEW_MODEL
|
|
166
|
+
&& /^[A-Za-z0-9][A-Za-z0-9._:/-]*$/.test(id)
|
|
167
|
+
&& model?.model_picker_enabled !== false
|
|
168
|
+
&& (endpoints.includes("/responses") || endpoints.includes("/v1/responses"));
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
|
|
161
172
|
export function claudeDesktopModelDefsFromCopilotModels(models) {
|
|
162
173
|
const data = copilotModelData(models);
|
|
163
174
|
|
|
@@ -212,21 +223,21 @@ export function resolveAnthropicModel(model, env = process.env, options = {}) {
|
|
|
212
223
|
};
|
|
213
224
|
}
|
|
214
225
|
|
|
215
|
-
export function resolveOpenAIModel(model, env = process.env) {
|
|
226
|
+
export function resolveOpenAIModel(model, env = process.env, options = {}) {
|
|
216
227
|
const requestedModel = String(model || "");
|
|
217
228
|
if (requestedModel !== CODEX_AUTO_REVIEW_MODEL) {
|
|
218
229
|
return { requestedModel, upstreamModel: requestedModel };
|
|
219
230
|
}
|
|
220
231
|
|
|
221
|
-
const configuredModel = String(env.CCDX_AUTO_REVIEW_MODEL || "").trim();
|
|
232
|
+
const configuredModel = String(env.CCDX_AUTO_REVIEW_MODEL || options.autoReviewModel || "").trim();
|
|
222
233
|
return {
|
|
223
234
|
requestedModel,
|
|
224
235
|
upstreamModel: configuredModel || DEFAULT_CODEX_AUTO_REVIEW_MODEL,
|
|
225
236
|
};
|
|
226
237
|
}
|
|
227
238
|
|
|
228
|
-
export function codexAutoReviewModelStatus(models, env = process.env) {
|
|
229
|
-
const { upstreamModel } = resolveOpenAIModel(CODEX_AUTO_REVIEW_MODEL, env);
|
|
239
|
+
export function codexAutoReviewModelStatus(models, env = process.env, options = {}) {
|
|
240
|
+
const { upstreamModel } = resolveOpenAIModel(CODEX_AUTO_REVIEW_MODEL, env, options);
|
|
230
241
|
const model = copilotModelData(models).find((entry) => String(entry?.id || "").trim() === upstreamModel);
|
|
231
242
|
if (!model) return { available: false, upstreamModel, reason: "model is not advertised" };
|
|
232
243
|
const endpoints = modelEndpoints(model);
|
package/src/observability.mjs
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import { isIP } from "node:net";
|
|
2
|
-
import { adapterHealthPayload } from "./running-adapter.mjs";
|
|
2
|
+
import { ADAPTER_STATUS_PATH, adapterHealthPayload } from "./running-adapter.mjs";
|
|
3
3
|
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
7
|
|
|
8
|
-
export
|
|
8
|
+
export { ADAPTER_STATUS_PATH };
|
|
9
9
|
const OBSERVABILITY_RUNTIME_CONFIG = loadRuntimeConfig();
|
|
10
10
|
|
|
11
11
|
const ROUTE_NAMES = Object.freeze([
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import { createInterface } from "node:readline/promises";
|
|
3
|
+
|
|
4
|
+
const PACKAGE_NAME = "codex-copilot-dx";
|
|
5
|
+
const UPDATE_SPECS = {
|
|
6
|
+
npm: `${PACKAGE_NAME}@latest`,
|
|
7
|
+
github: "github:DaleXiao/codex-copilot-dx#main",
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
export function normalizeUpdateSource(value) {
|
|
11
|
+
const source = String(value || "").trim().toLowerCase();
|
|
12
|
+
if (source === "gh") return "github";
|
|
13
|
+
return Object.hasOwn(UPDATE_SPECS, source) ? source : "";
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function globalUpdateCommand(source, { platform = process.platform } = {}) {
|
|
17
|
+
const normalized = normalizeUpdateSource(source);
|
|
18
|
+
if (!normalized) throw new Error(`Update source must be npm or github: ${source}`);
|
|
19
|
+
return {
|
|
20
|
+
command: platform === "win32" ? "npm.cmd" : "npm",
|
|
21
|
+
args: ["install", "--global", UPDATE_SPECS[normalized]],
|
|
22
|
+
source: normalized,
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function waitForExit(child) {
|
|
27
|
+
return new Promise((resolve, reject) => {
|
|
28
|
+
let settled = false;
|
|
29
|
+
const finish = (handler, value) => {
|
|
30
|
+
if (settled) return;
|
|
31
|
+
settled = true;
|
|
32
|
+
handler(value);
|
|
33
|
+
};
|
|
34
|
+
child.once("error", (error) => finish(reject, error));
|
|
35
|
+
child.once("exit", (code, signal) => {
|
|
36
|
+
if (signal) finish(reject, new Error(`npm update was interrupted by ${signal}`));
|
|
37
|
+
else if (code !== 0) finish(reject, new Error(`npm update exited with status ${code}`));
|
|
38
|
+
else finish(resolve);
|
|
39
|
+
});
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export async function runPackageUpdateCommand({
|
|
44
|
+
commandName = "ccdx",
|
|
45
|
+
env = process.env,
|
|
46
|
+
input = process.stdin,
|
|
47
|
+
output = process.stdout,
|
|
48
|
+
platform = process.platform,
|
|
49
|
+
prompt,
|
|
50
|
+
source,
|
|
51
|
+
spawnImpl = spawn,
|
|
52
|
+
} = {}) {
|
|
53
|
+
let selectedSource = normalizeUpdateSource(source);
|
|
54
|
+
if (source && !selectedSource) throw new Error(`Update source must be npm or github: ${source}`);
|
|
55
|
+
|
|
56
|
+
if (!selectedSource) {
|
|
57
|
+
if (!prompt && (!input.isTTY || !output.isTTY)) {
|
|
58
|
+
throw new Error(`Choose a source with ${commandName} update npm or ${commandName} update github`);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
output.write(`${commandName} update\n`);
|
|
62
|
+
output.write(" 1. npm registry [default]\n");
|
|
63
|
+
output.write(" 2. GitHub main\n");
|
|
64
|
+
|
|
65
|
+
let readline;
|
|
66
|
+
const ask = prompt || (async (question) => {
|
|
67
|
+
readline ||= createInterface({ input, output });
|
|
68
|
+
return readline.question(question);
|
|
69
|
+
});
|
|
70
|
+
try {
|
|
71
|
+
while (!selectedSource) {
|
|
72
|
+
const answer = String(await ask("Select update source [1], or q to cancel: ") || "").trim().toLowerCase();
|
|
73
|
+
if (answer === "q" || answer === "quit") {
|
|
74
|
+
output.write("No changes made.\n");
|
|
75
|
+
return { cancelled: true, source: "" };
|
|
76
|
+
}
|
|
77
|
+
if (answer === "" || answer === "1" || answer === "npm") selectedSource = "npm";
|
|
78
|
+
else if (answer === "2" || answer === "github" || answer === "gh") selectedSource = "github";
|
|
79
|
+
else output.write("Enter 1 for npm, 2 for GitHub, or q to cancel.\n");
|
|
80
|
+
}
|
|
81
|
+
} finally {
|
|
82
|
+
readline?.close();
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const update = globalUpdateCommand(selectedSource, { platform });
|
|
87
|
+
const sourceLabel = selectedSource === "npm" ? "npm registry" : "GitHub main";
|
|
88
|
+
output.write(`Updating ${PACKAGE_NAME} from ${sourceLabel}...\n`);
|
|
89
|
+
const child = spawnImpl(update.command, update.args, {
|
|
90
|
+
env,
|
|
91
|
+
shell: false,
|
|
92
|
+
stdio: "inherit",
|
|
93
|
+
});
|
|
94
|
+
await waitForExit(child);
|
|
95
|
+
output.write(`Updated ${PACKAGE_NAME} from ${sourceLabel}. Restart the running adapter to use the new version.\n`);
|
|
96
|
+
return { cancelled: false, source: selectedSource };
|
|
97
|
+
}
|
|
@@ -66,9 +66,17 @@ function unsupportedCustomToolsError(model) {
|
|
|
66
66
|
return error;
|
|
67
67
|
}
|
|
68
68
|
|
|
69
|
+
function resolveRequestModel(model, openAIModelEnv, autoReviewModelResolver) {
|
|
70
|
+
const options = model === CODEX_AUTO_REVIEW_MODEL && typeof autoReviewModelResolver === "function"
|
|
71
|
+
? { autoReviewModel: autoReviewModelResolver() }
|
|
72
|
+
: {};
|
|
73
|
+
return resolveOpenAIModel(model, openAIModelEnv, options);
|
|
74
|
+
}
|
|
75
|
+
|
|
69
76
|
export function createResponsesHandler(options) {
|
|
70
77
|
const {
|
|
71
78
|
acquireRequest,
|
|
79
|
+
autoReviewModelResolver,
|
|
72
80
|
chatCompletionsFn,
|
|
73
81
|
openAIModelEnv,
|
|
74
82
|
responsesPayloadOptions,
|
|
@@ -91,7 +99,7 @@ export function createResponsesHandler(options) {
|
|
|
91
99
|
const prepared = prepareResponsesRequest(parsed, { mutate: true });
|
|
92
100
|
prepared.surface = "responses";
|
|
93
101
|
const model = parsed.model || "unknown";
|
|
94
|
-
const { requestedModel, upstreamModel } =
|
|
102
|
+
const { requestedModel, upstreamModel } = resolveRequestModel(model, openAIModelEnv, autoReviewModelResolver);
|
|
95
103
|
if (upstreamModel !== requestedModel) prepared.body.model = upstreamModel;
|
|
96
104
|
const upstreamLog = upstreamModel === requestedModel ? "" : ` upstream_model=${upstreamModel}`;
|
|
97
105
|
console.log(status("info", `responses model=${requestedModel}${upstreamLog} stream=${!!parsed.stream}`));
|
|
@@ -186,6 +194,7 @@ export function createResponsesHandler(options) {
|
|
|
186
194
|
export function createResponsesCompactHandler(options) {
|
|
187
195
|
const {
|
|
188
196
|
acquireRequest,
|
|
197
|
+
autoReviewModelResolver,
|
|
189
198
|
openAIModelEnv,
|
|
190
199
|
responsesCompactFn,
|
|
191
200
|
upstreamTimeoutMs,
|
|
@@ -203,7 +212,7 @@ export function createResponsesCompactHandler(options) {
|
|
|
203
212
|
);
|
|
204
213
|
prepared.surface = "responses_compact";
|
|
205
214
|
const model = parsed.model || "unknown";
|
|
206
|
-
const { requestedModel, upstreamModel } =
|
|
215
|
+
const { requestedModel, upstreamModel } = resolveRequestModel(model, openAIModelEnv, autoReviewModelResolver);
|
|
207
216
|
if (upstreamModel !== requestedModel) prepared.body.model = upstreamModel;
|
|
208
217
|
const upstreamLog = upstreamModel === requestedModel ? "" : ` upstream_model=${upstreamModel}`;
|
|
209
218
|
console.log(status("info", `responses compact model=${requestedModel}${upstreamLog} stream=false`));
|
package/src/running-adapter.mjs
CHANGED
|
@@ -2,6 +2,7 @@ import { randomUUID } from "node:crypto";
|
|
|
2
2
|
import { localPackageVersion } from "./version.mjs";
|
|
3
3
|
|
|
4
4
|
export const ADAPTER_HEALTH_PATH = "/_ccdx/health";
|
|
5
|
+
export const ADAPTER_STATUS_PATH = "/_ccdx/status";
|
|
5
6
|
export const ADAPTER_PROTOCOL_VERSION = 2;
|
|
6
7
|
export const ADAPTER_VERSION = localPackageVersion();
|
|
7
8
|
const ADAPTER_INSTANCE_ID = randomUUID();
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import os from "node:os";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { atomicWriteFileIfChangedSync } from "./atomic-file.mjs";
|
|
5
|
+
import { DEFAULT_CODEX_AUTO_REVIEW_MODEL } from "./models.mjs";
|
|
6
|
+
|
|
7
|
+
const AUTO_REVIEW_MODEL_KEY = "auto_review_model";
|
|
8
|
+
|
|
9
|
+
export function userSettingsPath({ env = process.env, home = os.homedir() } = {}) {
|
|
10
|
+
const xdgConfigHome = String(env.XDG_CONFIG_HOME || "").trim();
|
|
11
|
+
const configRoot = xdgConfigHome || path.join(home, ".config");
|
|
12
|
+
return path.join(configRoot, "codex-copilot-dx", "config.json");
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function invalidSettings(filePath, reason) {
|
|
16
|
+
return new Error(`Invalid ccdx settings at ${filePath}: ${reason}`);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function readUserSettings({ env = process.env, home = os.homedir(), strict = false } = {}) {
|
|
20
|
+
const filePath = userSettingsPath({ env, home });
|
|
21
|
+
try {
|
|
22
|
+
const parsed = JSON.parse(fs.readFileSync(filePath, "utf8"));
|
|
23
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
24
|
+
throw invalidSettings(filePath, "expected a JSON object");
|
|
25
|
+
}
|
|
26
|
+
if (Object.hasOwn(parsed, AUTO_REVIEW_MODEL_KEY)) {
|
|
27
|
+
const value = parsed[AUTO_REVIEW_MODEL_KEY];
|
|
28
|
+
if (typeof value !== "string" || !value.trim()) {
|
|
29
|
+
throw invalidSettings(filePath, `${AUTO_REVIEW_MODEL_KEY} must be a non-empty string`);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
return parsed;
|
|
33
|
+
} catch (error) {
|
|
34
|
+
if (error?.code === "ENOENT") return {};
|
|
35
|
+
if (strict) {
|
|
36
|
+
if (String(error?.message || "").startsWith("Invalid ccdx settings")) throw error;
|
|
37
|
+
throw invalidSettings(filePath, error?.message || String(error));
|
|
38
|
+
}
|
|
39
|
+
return {};
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function savedAutoReviewModel(options = {}) {
|
|
44
|
+
const settings = readUserSettings(options);
|
|
45
|
+
return String(settings[AUTO_REVIEW_MODEL_KEY] || "").trim();
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function autoReviewModelPreference({ env = process.env, home = os.homedir() } = {}) {
|
|
49
|
+
const environmentModel = String(env.CCDX_AUTO_REVIEW_MODEL || "").trim();
|
|
50
|
+
if (environmentModel) return { model: environmentModel, source: "environment" };
|
|
51
|
+
|
|
52
|
+
const savedModel = savedAutoReviewModel({ env, home });
|
|
53
|
+
if (savedModel) return { model: savedModel, source: "settings" };
|
|
54
|
+
return { model: DEFAULT_CODEX_AUTO_REVIEW_MODEL, source: "default" };
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function writeAutoReviewModel(model, { env = process.env, home = os.homedir() } = {}) {
|
|
58
|
+
const filePath = userSettingsPath({ env, home });
|
|
59
|
+
const settings = readUserSettings({ env, home, strict: true });
|
|
60
|
+
const value = String(model || "").trim();
|
|
61
|
+
const next = { ...settings };
|
|
62
|
+
if (value) next[AUTO_REVIEW_MODEL_KEY] = value;
|
|
63
|
+
else delete next[AUTO_REVIEW_MODEL_KEY];
|
|
64
|
+
|
|
65
|
+
const changed = atomicWriteFileIfChangedSync(filePath, `${JSON.stringify(next, null, 2)}\n`, { mode: 0o600 });
|
|
66
|
+
return { changed, filePath, model: value || DEFAULT_CODEX_AUTO_REVIEW_MODEL };
|
|
67
|
+
}
|