codex-copilot-dx 0.8.3 → 0.8.5
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 +35 -7
- package/bin/cli.mjs +30 -4
- package/package.json +1 -1
- package/src/adapter.mjs +68 -3
- package/src/cache-control.mjs +59 -0
- package/src/cache-limits.mjs +4 -0
- package/src/cli-cache.mjs +147 -0
- package/src/cli-image.mjs +20 -8
- package/src/cli-options.mjs +36 -1
- package/src/cli-status.mjs +11 -0
- package/src/codex-model-catalog.mjs +90 -14
- package/src/diagnostic-text.mjs +11 -0
- package/src/file-rotation.mjs +34 -0
- package/src/http-transport.mjs +11 -2
- package/src/image-mcp.mjs +17 -1
- package/src/image-optimization.mjs +12 -0
- package/src/image-provider.mjs +10 -4
- package/src/log.mjs +62 -8
- package/src/models.mjs +2 -2
- package/src/observability.mjs +11 -3
- package/src/response-failures.mjs +3 -4
- package/src/response-history.mjs +29 -2
- package/src/responses-handler.mjs +6 -5
- package/src/responses-proxy.mjs +2 -2
- package/src/running-adapter.mjs +2 -2
- package/src/stream-errors.mjs +5 -1
- package/src/usage.mjs +28 -7
- package/src/user-settings.mjs +53 -0
package/src/response-history.mjs
CHANGED
|
@@ -203,24 +203,51 @@ function makeRoomFor({ additionalBytes, additionalEntries, protectedRoots }) {
|
|
|
203
203
|
return true;
|
|
204
204
|
}
|
|
205
205
|
|
|
206
|
-
export function
|
|
206
|
+
export function clearResponseHistory() {
|
|
207
|
+
if (pinnedTrees.size > 0) {
|
|
208
|
+
const error = new Error("Response history is active; retry cleanup after current requests finish");
|
|
209
|
+
error.code = "ccdx_cache_busy";
|
|
210
|
+
throw error;
|
|
211
|
+
}
|
|
212
|
+
const removed = { entries: histories.size, bytes: totalBytes };
|
|
207
213
|
histories.clear();
|
|
208
214
|
childrenById.clear();
|
|
209
215
|
treeLru.clear();
|
|
210
216
|
pinnedTrees.clear();
|
|
211
217
|
evictedIds.clear();
|
|
212
218
|
totalBytes = 0;
|
|
219
|
+
return removed;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
export function clearResponseHistoryForTests() {
|
|
223
|
+
clearResponseHistory();
|
|
213
224
|
maxBytes = DEFAULT_MAX_BYTES;
|
|
214
225
|
maxEntries = DEFAULT_MAX_ENTRIES;
|
|
215
226
|
}
|
|
216
227
|
|
|
228
|
+
export function setResponseHistoryMaxBytes(nextMaxBytes) {
|
|
229
|
+
const parsed = Number(nextMaxBytes);
|
|
230
|
+
if (!Number.isSafeInteger(parsed) || parsed <= 0) {
|
|
231
|
+
throw new TypeError("Response history cache limit must be a positive safe integer");
|
|
232
|
+
}
|
|
233
|
+
if (parsed < totalBytes) {
|
|
234
|
+
const error = new Error(`Response history currently uses ${totalBytes} bytes, above the requested ${parsed}-byte limit`);
|
|
235
|
+
error.code = "ccdx_cache_limit_below_usage";
|
|
236
|
+
throw error;
|
|
237
|
+
}
|
|
238
|
+
const previousMaxBytes = maxBytes;
|
|
239
|
+
maxBytes = parsed;
|
|
240
|
+
return { previousMaxBytes, maxBytes };
|
|
241
|
+
}
|
|
242
|
+
|
|
217
243
|
export function configureResponseHistoryForTests({ maxBytes: nextMaxBytes, maxEntries: nextMaxEntries } = {}) {
|
|
218
244
|
if (nextMaxBytes !== undefined) maxBytes = parsePositiveInteger(nextMaxBytes, DEFAULT_MAX_BYTES);
|
|
219
245
|
if (nextMaxEntries !== undefined) maxEntries = parsePositiveInteger(nextMaxEntries, DEFAULT_MAX_ENTRIES);
|
|
220
246
|
}
|
|
221
247
|
|
|
222
248
|
export function responseHistoryStats() {
|
|
223
|
-
return { entries: histories.size, bytes: totalBytes, evicted: evictedIds.size
|
|
249
|
+
return { entries: histories.size, bytes: totalBytes, evicted: evictedIds.size,
|
|
250
|
+
pinnedTrees: pinnedTrees.size, maxBytes, maxEntries };
|
|
224
251
|
}
|
|
225
252
|
|
|
226
253
|
export function responseHistoryRootId(responseId) {
|
|
@@ -89,9 +89,10 @@ function resolveRequestModel(model, openAIModelEnv, autoReviewModelResolver) {
|
|
|
89
89
|
return resolveOpenAIModel(model, openAIModelEnv, options);
|
|
90
90
|
}
|
|
91
91
|
|
|
92
|
-
function stripUnsupportedGpt6ServiceTier(body, requestedModel, upstreamModel) {
|
|
93
|
-
if (requestedModel
|
|
92
|
+
function stripUnsupportedGpt6ServiceTier(body, requestedModel, upstreamModel, priorityTierModel) {
|
|
93
|
+
if (requestedModel.startsWith("gpt-6-")
|
|
94
94
|
&& upstreamModel === requestedModel
|
|
95
|
+
&& !priorityTierModel
|
|
95
96
|
&& body.service_tier === "priority") {
|
|
96
97
|
delete body.service_tier;
|
|
97
98
|
}
|
|
@@ -263,7 +264,7 @@ export function createResponsesHandler(options) {
|
|
|
263
264
|
upstreamModel = priorityTierModel;
|
|
264
265
|
delete prepared.body.service_tier;
|
|
265
266
|
}
|
|
266
|
-
stripUnsupportedGpt6ServiceTier(prepared.body, requestedModel, upstreamModel);
|
|
267
|
+
stripUnsupportedGpt6ServiceTier(prepared.body, requestedModel, upstreamModel, priorityTierModel);
|
|
267
268
|
if (requestedModel === CODEX_AUTO_REVIEW_MODEL) delete prepared.body.service_tier;
|
|
268
269
|
if (upstreamModel !== requestedModel) prepared.body.model = upstreamModel;
|
|
269
270
|
const upstreamLog = upstreamModel === requestedModel ? "" : ` upstream_model=${upstreamModel}`;
|
|
@@ -319,7 +320,7 @@ export function createResponsesHandler(options) {
|
|
|
319
320
|
"Cache-Control": "no-cache",
|
|
320
321
|
Connection: "keep-alive",
|
|
321
322
|
});
|
|
322
|
-
const written = await writeOrDrain(res, `event: ${event}\ndata: ${JSON.stringify(data)}\n\n
|
|
323
|
+
const written = await writeOrDrain(res, `event: ${event}\ndata: ${JSON.stringify(data)}\n\n`, { signal: abort.signal });
|
|
323
324
|
if (!written) return false;
|
|
324
325
|
if (event === "response.completed") {
|
|
325
326
|
rememberResponseHistory(prepared, data.response);
|
|
@@ -516,7 +517,7 @@ export function createResponsesCompactHandler(options) {
|
|
|
516
517
|
upstreamModel = priorityTierModel;
|
|
517
518
|
delete prepared.body.service_tier;
|
|
518
519
|
}
|
|
519
|
-
stripUnsupportedGpt6ServiceTier(prepared.body, requestedModel, upstreamModel);
|
|
520
|
+
stripUnsupportedGpt6ServiceTier(prepared.body, requestedModel, upstreamModel, priorityTierModel);
|
|
520
521
|
if (requestedModel === CODEX_AUTO_REVIEW_MODEL) delete prepared.body.service_tier;
|
|
521
522
|
if (upstreamModel !== requestedModel) prepared.body.model = upstreamModel;
|
|
522
523
|
const upstreamLog = upstreamModel === requestedModel ? "" : ` upstream_model=${upstreamModel}`;
|
package/src/responses-proxy.mjs
CHANGED
|
@@ -403,13 +403,13 @@ async function proxyStreamingResponses(opened, res, upstream, options) {
|
|
|
403
403
|
const body = prelude.length === 1 ? prelude[0] : Buffer.concat(prelude, preludeBytes);
|
|
404
404
|
prelude.length = 0;
|
|
405
405
|
preludeBytes = 0;
|
|
406
|
-
return writeOrDrain(res, body);
|
|
406
|
+
return writeOrDrain(res, body, { signal: options.signal });
|
|
407
407
|
};
|
|
408
408
|
const forwardFrame = async (frame) => {
|
|
409
409
|
if (!frame) return true;
|
|
410
410
|
if (!holdPrelude) {
|
|
411
411
|
writeHeaders();
|
|
412
|
-
return writeOrDrain(res, frame);
|
|
412
|
+
return writeOrDrain(res, frame, { signal: options.signal });
|
|
413
413
|
}
|
|
414
414
|
prelude.push(frame);
|
|
415
415
|
preludeBytes += frame.byteLength;
|
package/src/running-adapter.mjs
CHANGED
|
@@ -3,9 +3,9 @@ import { localPackageVersion } from "./version.mjs";
|
|
|
3
3
|
|
|
4
4
|
export const ADAPTER_HEALTH_PATH = "/_ccdx/health";
|
|
5
5
|
export const ADAPTER_STATUS_PATH = "/_ccdx/status";
|
|
6
|
-
export const ADAPTER_PROTOCOL_VERSION =
|
|
6
|
+
export const ADAPTER_PROTOCOL_VERSION = 4;
|
|
7
7
|
export const ADAPTER_VERSION = localPackageVersion();
|
|
8
|
-
export const ADAPTER_CAPABILITIES = Object.freeze([]);
|
|
8
|
+
export const ADAPTER_CAPABILITIES = Object.freeze(["cache-control-v1"]);
|
|
9
9
|
const ADAPTER_INSTANCE_ID = randomUUID();
|
|
10
10
|
|
|
11
11
|
export function adapterHealthPayload() {
|
package/src/stream-errors.mjs
CHANGED
|
@@ -11,6 +11,10 @@ export async function endStreamWithError(res, error, abort) {
|
|
|
11
11
|
markStreamFailure();
|
|
12
12
|
if (res.destroyed || res.writableEnded) return;
|
|
13
13
|
const data = streamErrorData(error, abort);
|
|
14
|
-
|
|
14
|
+
// Allow a final error frame after an upstream timeout, but never wait forever
|
|
15
|
+
// for a client that has stopped reading (including protocol-error paths).
|
|
16
|
+
await writeOrDrain(res, `event: error\ndata: ${JSON.stringify(data)}\n\n`, {
|
|
17
|
+
signal: AbortSignal.timeout(1000),
|
|
18
|
+
}).catch(() => false);
|
|
15
19
|
if (!res.destroyed && !res.writableEnded) res.end();
|
|
16
20
|
}
|
package/src/usage.mjs
CHANGED
|
@@ -3,7 +3,7 @@ import os from "node:os";
|
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import readline from "node:readline";
|
|
5
5
|
import { cliOutputFormat, cliOutputWidth, formatResponsiveCliTable, terminalCell } from "./cli-table.mjs";
|
|
6
|
-
import {
|
|
6
|
+
import { appendRotatingLines, parseByteLimit, rotatedFilePath } from "./file-rotation.mjs";
|
|
7
7
|
import { withFileLock } from "./lock.mjs";
|
|
8
8
|
|
|
9
9
|
const DEFAULT_USAGE_PATH = path.join(os.homedir(), ".local", "share", "codex-copilot-dx", "usage.jsonl");
|
|
@@ -15,6 +15,17 @@ const USAGE_FLUSH_TIMEOUT_MS = 1_500;
|
|
|
15
15
|
|
|
16
16
|
const pendingWrites = [];
|
|
17
17
|
let writeDrain = null;
|
|
18
|
+
const USAGE_QUEUE_MAX_RECORDS = 4096;
|
|
19
|
+
const USAGE_QUEUE_MAX_BYTES = 4 * 1024 * 1024;
|
|
20
|
+
let pendingRecords = 0;
|
|
21
|
+
let pendingBytes = 0;
|
|
22
|
+
let droppedRecords = 0;
|
|
23
|
+
let writeFailures = 0;
|
|
24
|
+
|
|
25
|
+
export function usageLoggingStats() {
|
|
26
|
+
return { pending_records: pendingRecords, pending_bytes: pendingBytes, dropped_records: droppedRecords,
|
|
27
|
+
write_failures: writeFailures, max_records: USAGE_QUEUE_MAX_RECORDS, max_bytes: USAGE_QUEUE_MAX_BYTES };
|
|
28
|
+
}
|
|
18
29
|
|
|
19
30
|
export function usageLogPath() {
|
|
20
31
|
return process.env.CCDX_USAGE_PATH || DEFAULT_USAGE_PATH;
|
|
@@ -114,10 +125,7 @@ async function writeUsageBatch(batch) {
|
|
|
114
125
|
await fs.promises.mkdir(path.dirname(filePath), { recursive: true });
|
|
115
126
|
await withFileLock(`${path.resolve(filePath)}.lock`, async () => {
|
|
116
127
|
batch.push(...takePendingWrites(filePath, maxBytes));
|
|
117
|
-
|
|
118
|
-
rotateFileIfNeededSync(filePath, Buffer.byteLength(line), maxBytes);
|
|
119
|
-
await fs.promises.appendFile(filePath, line, { encoding: "utf8", mode: 0o600 });
|
|
120
|
-
}
|
|
128
|
+
await appendRotatingLines(filePath, batch.map(({ line }) => line), maxBytes);
|
|
121
129
|
}, {
|
|
122
130
|
timeoutMs: USAGE_WRITE_LOCK_TIMEOUT_MS,
|
|
123
131
|
staleMs: USAGE_WRITE_LOCK_STALE_MS,
|
|
@@ -134,10 +142,15 @@ function scheduleUsageWriteDrain() {
|
|
|
134
142
|
try {
|
|
135
143
|
await writeUsageBatch(batch);
|
|
136
144
|
} catch (error) {
|
|
145
|
+
writeFailures += 1;
|
|
137
146
|
batch.push(...takePendingWrites(first.filePath, first.maxBytes));
|
|
138
147
|
console.error(`codex-copilot-dx usage log write failed: ${error.message}`);
|
|
139
148
|
} finally {
|
|
140
|
-
for (const entry of batch)
|
|
149
|
+
for (const entry of batch) {
|
|
150
|
+
pendingRecords -= 1;
|
|
151
|
+
pendingBytes -= entry.bytes;
|
|
152
|
+
entry.resolve();
|
|
153
|
+
}
|
|
141
154
|
}
|
|
142
155
|
}
|
|
143
156
|
}).finally(() => {
|
|
@@ -150,8 +163,16 @@ export function recordUsage(record) {
|
|
|
150
163
|
if (!record || process.env.CCDX_DISABLE_USAGE === "1") return Promise.resolve();
|
|
151
164
|
const filePath = usageLogPath();
|
|
152
165
|
const line = `${JSON.stringify(record)}\n`;
|
|
166
|
+
const bytes = Buffer.byteLength(line);
|
|
167
|
+
if (pendingRecords >= USAGE_QUEUE_MAX_RECORDS || pendingBytes + bytes > USAGE_QUEUE_MAX_BYTES) {
|
|
168
|
+
droppedRecords += 1;
|
|
169
|
+
if (droppedRecords === 1) console.error("codex-copilot-dx usage queue is full; excess usage records are dropped, inference is unaffected (see ccdx status)");
|
|
170
|
+
return Promise.resolve();
|
|
171
|
+
}
|
|
172
|
+
pendingRecords += 1;
|
|
173
|
+
pendingBytes += bytes;
|
|
153
174
|
const completed = new Promise((resolve) => {
|
|
154
|
-
pendingWrites.push({ filePath, line, maxBytes: usageLogMaxBytes(), resolve });
|
|
175
|
+
pendingWrites.push({ filePath, line, bytes, maxBytes: usageLogMaxBytes(), resolve });
|
|
155
176
|
});
|
|
156
177
|
scheduleUsageWriteDrain();
|
|
157
178
|
return completed;
|
package/src/user-settings.mjs
CHANGED
|
@@ -3,6 +3,13 @@ import os from "node:os";
|
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { atomicWriteFileIfChangedSync } from "./atomic-file.mjs";
|
|
5
5
|
import { DEFAULT_CODEX_AUTO_REVIEW_MODEL } from "./models.mjs";
|
|
6
|
+
import { loadRuntimeConfig } from "./runtime-config.mjs";
|
|
7
|
+
import {
|
|
8
|
+
MIB,
|
|
9
|
+
RESPONSE_HISTORY_DEFAULT_MIB,
|
|
10
|
+
RESPONSE_HISTORY_MAX_MIB,
|
|
11
|
+
RESPONSE_HISTORY_MIN_MIB,
|
|
12
|
+
} from "./cache-limits.mjs";
|
|
6
13
|
import {
|
|
7
14
|
DEFAULT_TERMINAL_ANIMATION_THEME,
|
|
8
15
|
isTerminalAnimationTheme,
|
|
@@ -11,6 +18,8 @@ import {
|
|
|
11
18
|
|
|
12
19
|
const AUTO_REVIEW_MODEL_KEY = "auto_review_model";
|
|
13
20
|
const TERMINAL_ANIMATION_KEY = "terminal_animation";
|
|
21
|
+
const RESPONSE_HISTORY_MAX_MIB_KEY = "response_history_max_mib";
|
|
22
|
+
export { RESPONSE_HISTORY_MAX_MIB, RESPONSE_HISTORY_MIN_MIB };
|
|
14
23
|
|
|
15
24
|
export function userSettingsPath({ env = process.env, home = os.homedir() } = {}) {
|
|
16
25
|
const xdgConfigHome = String(env.XDG_CONFIG_HOME || "").trim();
|
|
@@ -46,6 +55,17 @@ export function readUserSettings({ env = process.env, home = os.homedir(), stric
|
|
|
46
55
|
delete parsed[TERMINAL_ANIMATION_KEY];
|
|
47
56
|
}
|
|
48
57
|
}
|
|
58
|
+
if (Object.hasOwn(parsed, RESPONSE_HISTORY_MAX_MIB_KEY)) {
|
|
59
|
+
const value = parsed[RESPONSE_HISTORY_MAX_MIB_KEY];
|
|
60
|
+
if (!Number.isSafeInteger(value)
|
|
61
|
+
|| value < RESPONSE_HISTORY_MIN_MIB
|
|
62
|
+
|| value > RESPONSE_HISTORY_MAX_MIB) {
|
|
63
|
+
if (strict) {
|
|
64
|
+
throw invalidSettings(filePath, `${RESPONSE_HISTORY_MAX_MIB_KEY} must be an integer from ${RESPONSE_HISTORY_MIN_MIB} to ${RESPONSE_HISTORY_MAX_MIB}`);
|
|
65
|
+
}
|
|
66
|
+
delete parsed[RESPONSE_HISTORY_MAX_MIB_KEY];
|
|
67
|
+
}
|
|
68
|
+
}
|
|
49
69
|
return parsed;
|
|
50
70
|
} catch (error) {
|
|
51
71
|
if (error?.code === "ENOENT") return {};
|
|
@@ -83,6 +103,39 @@ export function terminalAnimationPreference({ env = process.env, home = os.homed
|
|
|
83
103
|
return { theme: DEFAULT_TERMINAL_ANIMATION_THEME, source: "default" };
|
|
84
104
|
}
|
|
85
105
|
|
|
106
|
+
export function responseHistoryLimitPreference({ env = process.env, home = os.homedir() } = {}) {
|
|
107
|
+
const environment = String(env.CCDX_RESPONSE_HISTORY_MAX_BYTES || "").trim();
|
|
108
|
+
if (environment) {
|
|
109
|
+
return { bytes: loadRuntimeConfig(env).responseHistoryMaxBytes, source: "environment" };
|
|
110
|
+
}
|
|
111
|
+
const settings = readUserSettings({ env, home });
|
|
112
|
+
const mib = settings[RESPONSE_HISTORY_MAX_MIB_KEY];
|
|
113
|
+
if (Number.isSafeInteger(mib)) return { bytes: mib * MIB, mib, source: "settings" };
|
|
114
|
+
return { bytes: RESPONSE_HISTORY_DEFAULT_MIB * MIB, mib: RESPONSE_HISTORY_DEFAULT_MIB, source: "default" };
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export function writeResponseHistoryLimitMib(mib, { env = process.env, home = os.homedir() } = {}) {
|
|
118
|
+
const filePath = userSettingsPath({ env, home });
|
|
119
|
+
const settings = readUserSettings({ env, home, strict: true });
|
|
120
|
+
const next = { ...settings };
|
|
121
|
+
if (mib === null || mib === undefined || mib === "") {
|
|
122
|
+
if (!Object.hasOwn(next, RESPONSE_HISTORY_MAX_MIB_KEY)) {
|
|
123
|
+
return { changed: false, filePath, ...responseHistoryLimitPreference({ env, home }) };
|
|
124
|
+
}
|
|
125
|
+
delete next[RESPONSE_HISTORY_MAX_MIB_KEY];
|
|
126
|
+
} else {
|
|
127
|
+
const parsed = Number(mib);
|
|
128
|
+
if (!Number.isSafeInteger(parsed)
|
|
129
|
+
|| parsed < RESPONSE_HISTORY_MIN_MIB
|
|
130
|
+
|| parsed > RESPONSE_HISTORY_MAX_MIB) {
|
|
131
|
+
throw new Error(`History cache limit must be an integer from ${RESPONSE_HISTORY_MIN_MIB} to ${RESPONSE_HISTORY_MAX_MIB} MiB`);
|
|
132
|
+
}
|
|
133
|
+
next[RESPONSE_HISTORY_MAX_MIB_KEY] = parsed;
|
|
134
|
+
}
|
|
135
|
+
const changed = atomicWriteFileIfChangedSync(filePath, `${JSON.stringify(next, null, 2)}\n`, { mode: 0o600 });
|
|
136
|
+
return { changed, filePath, ...responseHistoryLimitPreference({ env, home }) };
|
|
137
|
+
}
|
|
138
|
+
|
|
86
139
|
export function writeAutoReviewModel(model, { env = process.env, home = os.homedir() } = {}) {
|
|
87
140
|
const filePath = userSettingsPath({ env, home });
|
|
88
141
|
const settings = readUserSettings({ env, home, strict: true });
|