codex-copilot-dx 0.8.4 → 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 +20 -4
- package/bin/cli.mjs +23 -1
- package/package.json +1 -1
- package/src/adapter.mjs +61 -2
- package/src/cache-control.mjs +59 -0
- package/src/cache-limits.mjs +4 -0
- package/src/cli-cache.mjs +147 -0
- package/src/cli-options.mjs +35 -0
- package/src/codex-model-catalog.mjs +90 -14
- package/src/image-optimization.mjs +12 -0
- package/src/models.mjs +2 -2
- package/src/observability.mjs +4 -3
- package/src/response-history.mjs +29 -2
- package/src/responses-handler.mjs +5 -4
- package/src/running-adapter.mjs +2 -2
- package/src/user-settings.mjs +53 -0
package/README.md
CHANGED
|
@@ -18,15 +18,17 @@ The adapter handles response history, encrypted reasoning state, function/custom
|
|
|
18
18
|
|
|
19
19
|
### Fast and Standard modes
|
|
20
20
|
|
|
21
|
-
Codex can select Standard or Fast without changing CCDX configuration. When the client requests
|
|
21
|
+
Codex can select Standard or Fast without changing CCDX configuration. When the client requests a model with `service_tier: "priority"`, CCDX uses its exact `-fast` model only when the live Copilot catalog advertises that variant as enabled, selectable, OpenAI-owned, and Responses-only. Standard mode continues to use the base model; CCDX does not invent Fast availability when the upstream catalog does not provide it. This retains the established `gpt-5.6-sol` / `gpt-5.6-sol-fast` mapping and applies the same exact-match rule to future models.
|
|
22
22
|
|
|
23
23
|
Codex Auto-review uses the hidden `codex-auto-review` model ID. CCDX maps it to Copilot's `gpt-5.5` Responses model by default, independently of the interactive Fast/Standard selection, and logs both model IDs when the mapping is used. The review target can be changed interactively without reinstalling or restarting CCDX.
|
|
24
24
|
|
|
25
25
|
### GPT-6 model catalog
|
|
26
26
|
|
|
27
|
-
Codex App requests a versioned, capability-rich model catalog that is different from Copilot's raw model list. CCDX reads the complete catalog bundled with the installed Codex App and preserves every other model entry and capability. It exposes
|
|
27
|
+
Codex App requests a versioned, capability-rich model catalog that is different from Copilot's raw model list. CCDX reads the complete catalog bundled with the installed Codex App and preserves every other model entry and capability. It exposes each managed GPT-6 entry only when Copilot advertises that exact model as enabled, selectable, OpenAI-owned, and Responses-capable.
|
|
28
28
|
|
|
29
|
-
|
|
29
|
+
When an installed 0.155.x catalog does not yet contain `gpt-6-sol` or `gpt-6-luna`, CCDX derives their client-compatible entries from that catalog's GPT-6 Astra schema and the published OpenAI Codex model definitions at commit `0a2eb4696c26ac33204bcd255721ab30220a4774`. Only those two missing entries are inserted. Their fallback reasoning controls are intersected with Copilot's advertised efforts; every other bundled entry remains byte-for-byte equivalent. A newer client-provided Sol/Luna entry always wins and is preserved.
|
|
30
|
+
|
|
31
|
+
CCDX exposes GPT-6 Standard paths and removes unverified speed-tier metadata. It leaves bundled reasoning-effort options unchanged, including Ultra when supplied by the installed catalog. Fast follows the exact upstream-variant rule above. Versioned model discovery matches the bundled catalog to the client's `client_version`, caches it by application binary identity, and reloads it after an App update. If a matching usable catalog cannot be loaded, the versioned models request returns `503`; an unversioned request returns the Copilot-style list.
|
|
30
32
|
|
|
31
33
|
`/v1/models` still attempts live discovery first. With a usable in-process catalog, it limits that upstream wait to 1.5 seconds (or a shorter configured upstream timeout) before falling back to last-known-good data. Without a catalog it retains the regular upstream timeout. Received HTTP `401`/`403` responses remain errors, not cached successes; fallback does not grant inference access. This does not change the explicitly live `ccdx models` command.
|
|
32
34
|
|
|
@@ -115,6 +117,20 @@ The selector first queries the running adapter, including its last-known-good mo
|
|
|
115
117
|
|
|
116
118
|
The selection is saved in `~/.config/codex-copilot-dx/config.json`, or under `XDG_CONFIG_HOME` when set. Choosing `gpt-5.5` clears the override and restores the package default. A running adapter reads the setting on the next Auto-review request.
|
|
117
119
|
|
|
120
|
+
### Runtime caches
|
|
121
|
+
|
|
122
|
+
```bash
|
|
123
|
+
ccdx cache
|
|
124
|
+
ccdx cache --limit 128
|
|
125
|
+
ccdx cache --limit default
|
|
126
|
+
ccdx cache --clean
|
|
127
|
+
ccdx cache --clean --history
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
`ccdx cache` separates the shared in-memory Responses history from the rebuildable input-image transform cache. The history limit accepts 16–1024 MiB, is saved in the normal CCDX settings file, and applies immediately to a compatible running adapter. `CCDX_RESPONSE_HISTORY_MAX_BYTES` remains the higher-precedence environment override. Lowering the limit below current usage is rejected rather than evicting tasks silently. `ccdx cache -128` is a short alias for `--limit 128`; `-clean` aliases `--clean`.
|
|
131
|
+
|
|
132
|
+
Plain `--clean` clears only completed image transforms; the next matching request may recompute them. Adding `--history` also invalidates all process-local `previous_response_id` chains and can make a task's next continuation fail, so it requires interactive confirmation or `--yes`. Cleanup waits for active cache users to finish and never deletes Codex transcripts, generated image files, provider credentials, image edit handles, model catalogs, or usage/debug logs. A stopped or older adapter must be restarted before runtime cleanup; a saved limit applies on the next start.
|
|
133
|
+
|
|
118
134
|
### Terminal animation
|
|
119
135
|
|
|
120
136
|
Choose the terminal activity animation interactively:
|
|
@@ -324,7 +340,7 @@ Recovery first tries compatible local credentials and starts Device Flow only wh
|
|
|
324
340
|
| `CCDX_EXISTING_ADAPTER_TIMEOUT_MS` | `500` | Timeout for detecting an already-running local adapter during startup |
|
|
325
341
|
| `CCDX_MODEL_REFRESH_TIMEOUT_MS` | `5000` | Timeout for Copilot model refresh or live Auto-review model lookup |
|
|
326
342
|
| `CCDX_MODEL_REFRESH_INTERVAL_MS` | `7200000` | Periodic model refresh interval; `0` disables periodic refresh, while startup cache/refresh behavior still applies |
|
|
327
|
-
| `CCDX_RESPONSE_HISTORY_MAX_BYTES` | `67108864` |
|
|
343
|
+
| `CCDX_RESPONSE_HISTORY_MAX_BYTES` | `67108864` | Higher-precedence total in-memory byte budget for locally expanded Responses history; `ccdx cache --limit` provides a persistent 16–1024 MiB setting when this is unset |
|
|
328
344
|
| `CCDX_RESPONSE_HISTORY_MAX_ENTRIES` | `4096` | Maximum stored incremental Responses history nodes |
|
|
329
345
|
| `CCDX_USAGE_PATH` | `~/.local/share/codex-copilot-dx/usage.jsonl` | Local JSONL token-usage log |
|
|
330
346
|
| `CCDX_USAGE_MAX_BYTES` | `33554432` | Rotate the usage log at this size and retain one `.1` backup; set to `0` to disable rotation |
|
package/bin/cli.mjs
CHANGED
|
@@ -16,13 +16,14 @@ import { cliCommandName, cliHelp, parseAdapterAddressOptions, parseAdapterProbeO
|
|
|
16
16
|
import { formatAdapterStatus, readAdapterStatus } from "../src/cli-status.mjs";
|
|
17
17
|
import { closeHttpServer } from "../src/shutdown.mjs";
|
|
18
18
|
import { runAutoReviewModelCommand } from "../src/auto-review-model.mjs";
|
|
19
|
-
import { autoReviewModelPreference, terminalAnimationPreference } from "../src/user-settings.mjs";
|
|
19
|
+
import { autoReviewModelPreference, responseHistoryLimitPreference, terminalAnimationPreference } from "../src/user-settings.mjs";
|
|
20
20
|
import { fetchLiveCopilotModels, formatLiveCopilotModels } from "../src/cli-models.mjs";
|
|
21
21
|
import { runAuthCommand } from "../src/cli-auth.mjs";
|
|
22
22
|
import { createProfileRuntime } from "../src/profile-runtime.mjs";
|
|
23
23
|
import { createProfileModelRuntime } from "../src/profile-model-runtime.mjs";
|
|
24
24
|
import { createCodexModelCatalog } from "../src/codex-model-catalog.mjs";
|
|
25
25
|
import { readImageProviderConfig } from "../src/image-provider-config.mjs";
|
|
26
|
+
import { setResponseHistoryMaxBytes } from "../src/response-history.mjs";
|
|
26
27
|
|
|
27
28
|
const LOCAL_VERSION = localPackageVersion();
|
|
28
29
|
const CLI_NAME = cliCommandName();
|
|
@@ -80,6 +81,26 @@ if (CLI.command === "usage") {
|
|
|
80
81
|
});
|
|
81
82
|
process.exit(0);
|
|
82
83
|
}
|
|
84
|
+
if (CLI.command === "cache") {
|
|
85
|
+
try {
|
|
86
|
+
const probe = parseAdapterProbeOptions(process.env);
|
|
87
|
+
const { runCacheCommand } = await import("../src/cli-cache.mjs");
|
|
88
|
+
await runCacheCommand({
|
|
89
|
+
action: CLI.action,
|
|
90
|
+
limitMib: CLI.limitMib,
|
|
91
|
+
resetLimit: CLI.resetLimit,
|
|
92
|
+
history: CLI.history,
|
|
93
|
+
yes: CLI.yes,
|
|
94
|
+
host: probe.adapterHost,
|
|
95
|
+
port: probe.adapterPort,
|
|
96
|
+
timeoutMs: probe.existingAdapterTimeoutMs,
|
|
97
|
+
});
|
|
98
|
+
process.exit(0);
|
|
99
|
+
} catch (e) {
|
|
100
|
+
console.error(status("err", e.message));
|
|
101
|
+
process.exit(1);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
83
104
|
if (CLI.command === "retired") {
|
|
84
105
|
console.error(status("err", `${CLI.integration} integration was retired in ccdx 0.7.0.`));
|
|
85
106
|
if (CLI.integration === "PM Studio") {
|
|
@@ -182,6 +203,7 @@ if (CLI.command === "doctor" && CLI.configOnly) {
|
|
|
182
203
|
let RUNTIME;
|
|
183
204
|
try {
|
|
184
205
|
RUNTIME = parseRuntimeOptions(process.env);
|
|
206
|
+
setResponseHistoryMaxBytes(responseHistoryLimitPreference().bytes);
|
|
185
207
|
} catch (e) {
|
|
186
208
|
console.error(e.message);
|
|
187
209
|
process.exit(2);
|
package/package.json
CHANGED
package/src/adapter.mjs
CHANGED
|
@@ -17,12 +17,19 @@ import { ADAPTER_HEALTH_PATH, adapterHealthPayload } from "./running-adapter.mjs
|
|
|
17
17
|
import { createResponsesCompactHandler, createResponsesHandler } from "./responses-handler.mjs";
|
|
18
18
|
import { createResponsesImagePressureController } from "./responses-image-pressure.mjs";
|
|
19
19
|
import { createResponseFailureDiagnostics } from "./response-failures.mjs";
|
|
20
|
+
import {
|
|
21
|
+
ADAPTER_CACHE_PATH,
|
|
22
|
+
cacheRuntimeSnapshot,
|
|
23
|
+
cleanRuntimeCaches,
|
|
24
|
+
setHistoryCacheLimit,
|
|
25
|
+
} from "./cache-control.mjs";
|
|
20
26
|
import { loadRuntimeConfig, parsePositiveInteger } from "./runtime-config.mjs";
|
|
21
27
|
import {
|
|
22
28
|
createRequestAdmission,
|
|
23
29
|
createRequestAbort,
|
|
24
30
|
httpError,
|
|
25
31
|
logRequestFailure,
|
|
32
|
+
readJsonBody,
|
|
26
33
|
sendJsonError,
|
|
27
34
|
} from "./http-transport.mjs";
|
|
28
35
|
|
|
@@ -192,7 +199,7 @@ export function createAdapterHandler(options = {}) {
|
|
|
192
199
|
streamIdleTimeoutMs,
|
|
193
200
|
upstreamTimeoutMs,
|
|
194
201
|
});
|
|
195
|
-
const dispatch = (req, res, pathname) => {
|
|
202
|
+
const dispatch = async (req, res, pathname) => {
|
|
196
203
|
if (pathname === "/mcp/image") {
|
|
197
204
|
if (imageMcpHandler) return imageMcpHandler(req, res);
|
|
198
205
|
return import("./image-mcp.mjs").then(({ createImageMcpHandler }) => {
|
|
@@ -227,6 +234,56 @@ export function createAdapterHandler(options = {}) {
|
|
|
227
234
|
return;
|
|
228
235
|
}
|
|
229
236
|
|
|
237
|
+
if (pathname === ADAPTER_CACHE_PATH) {
|
|
238
|
+
if (!isLoopbackAddress(req.socket?.remoteAddress)) {
|
|
239
|
+
res.writeHead(403, { "Content-Type": "application/json" });
|
|
240
|
+
res.end(JSON.stringify({ error: "Cache control is available only from loopback" }));
|
|
241
|
+
return;
|
|
242
|
+
}
|
|
243
|
+
if (req.method === "GET") {
|
|
244
|
+
res.writeHead(200, { "Content-Type": "application/json", "Cache-Control": "no-store" });
|
|
245
|
+
res.end(JSON.stringify(cacheRuntimeSnapshot()));
|
|
246
|
+
return;
|
|
247
|
+
}
|
|
248
|
+
if (req.method !== "POST") {
|
|
249
|
+
res.writeHead(405, { "Content-Type": "application/json", Allow: "GET, POST" });
|
|
250
|
+
res.end(JSON.stringify({ error: "Only GET and POST are supported" }));
|
|
251
|
+
return;
|
|
252
|
+
}
|
|
253
|
+
if (Object.hasOwn(req.headers || {}, "origin")) {
|
|
254
|
+
res.writeHead(403, { "Content-Type": "application/json" });
|
|
255
|
+
res.end(JSON.stringify({ error: "Browser origins are not allowed for cache control" }));
|
|
256
|
+
return;
|
|
257
|
+
}
|
|
258
|
+
const mediaType = String(req.headers?.["content-type"] || "").split(";", 1)[0].trim().toLowerCase();
|
|
259
|
+
if (mediaType !== "application/json") {
|
|
260
|
+
res.writeHead(415, { "Content-Type": "application/json" });
|
|
261
|
+
res.end(JSON.stringify({ error: "Cache control requires application/json" }));
|
|
262
|
+
return;
|
|
263
|
+
}
|
|
264
|
+
const abort = createRequestAbort(req, res);
|
|
265
|
+
abort.setTimeout(requestBodyTimeoutMs, "request_body_timeout");
|
|
266
|
+
try {
|
|
267
|
+
const body = await readJsonBody(req, {
|
|
268
|
+
maxBodyBytes: 8 * 1024,
|
|
269
|
+
maxDecodedBodyBytes: 8 * 1024,
|
|
270
|
+
signal: abort.signal,
|
|
271
|
+
});
|
|
272
|
+
let result;
|
|
273
|
+
if (body?.action === "set_limit") result = setHistoryCacheLimit(body.max_bytes);
|
|
274
|
+
else if (body?.action === "clean") result = cleanRuntimeCaches({ history: body.history === true });
|
|
275
|
+
else throw httpError("Unknown cache action", 400);
|
|
276
|
+
res.writeHead(200, { "Content-Type": "application/json", "Cache-Control": "no-store" });
|
|
277
|
+
res.end(JSON.stringify(result));
|
|
278
|
+
} catch (error) {
|
|
279
|
+
if (["ccdx_cache_busy", "ccdx_cache_limit_below_usage"].includes(error?.code)) error.statusCode = 409;
|
|
280
|
+
sendJsonError(res, error, 400);
|
|
281
|
+
} finally {
|
|
282
|
+
abort.cleanup();
|
|
283
|
+
}
|
|
284
|
+
return;
|
|
285
|
+
}
|
|
286
|
+
|
|
230
287
|
if (req.method === "POST" && pathname === "/v1/responses") {
|
|
231
288
|
return responsesHandler(req, res);
|
|
232
289
|
}
|
|
@@ -314,7 +371,9 @@ export function createAdapterHandler(options = {}) {
|
|
|
314
371
|
return;
|
|
315
372
|
}
|
|
316
373
|
|
|
317
|
-
const trackRequest = pathname !== ADAPTER_HEALTH_PATH
|
|
374
|
+
const trackRequest = pathname !== ADAPTER_HEALTH_PATH
|
|
375
|
+
&& pathname !== ADAPTER_STATUS_PATH
|
|
376
|
+
&& pathname !== ADAPTER_CACHE_PATH;
|
|
318
377
|
const routeName = classifyAdapterRoute(req.method, pathname);
|
|
319
378
|
const complete = trackRequest ? requestMetrics.begin(routeName) : () => {};
|
|
320
379
|
const streamPerformance = trackRequest ? streamPerformanceMetrics.begin(routeName) : null;
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import {
|
|
2
|
+
MIB,
|
|
3
|
+
RESPONSE_HISTORY_MAX_MIB,
|
|
4
|
+
RESPONSE_HISTORY_MIN_MIB,
|
|
5
|
+
} from "./cache-limits.mjs";
|
|
6
|
+
import {
|
|
7
|
+
clearResponseHistory,
|
|
8
|
+
responseHistoryStats,
|
|
9
|
+
setResponseHistoryMaxBytes,
|
|
10
|
+
} from "./response-history.mjs";
|
|
11
|
+
import {
|
|
12
|
+
clearImageOptimizationCache,
|
|
13
|
+
imageOptimizationStats,
|
|
14
|
+
} from "./image-optimization.mjs";
|
|
15
|
+
|
|
16
|
+
export const ADAPTER_CACHE_PATH = "/_ccdx/cache";
|
|
17
|
+
|
|
18
|
+
export function cacheRuntimeSnapshot() {
|
|
19
|
+
return {
|
|
20
|
+
response_history: responseHistoryStats(),
|
|
21
|
+
image_optimization: imageOptimizationStats(),
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function setHistoryCacheLimit(maxBytes) {
|
|
26
|
+
const mib = Number(maxBytes) / MIB;
|
|
27
|
+
if (!Number.isSafeInteger(mib)
|
|
28
|
+
|| mib < RESPONSE_HISTORY_MIN_MIB
|
|
29
|
+
|| mib > RESPONSE_HISTORY_MAX_MIB) {
|
|
30
|
+
const error = new Error(`History cache limit must be an integer from ${RESPONSE_HISTORY_MIN_MIB} to ${RESPONSE_HISTORY_MAX_MIB} MiB`);
|
|
31
|
+
error.code = "ccdx_cache_limit_invalid";
|
|
32
|
+
throw error;
|
|
33
|
+
}
|
|
34
|
+
return { changed: setResponseHistoryMaxBytes(maxBytes).previousMaxBytes !== maxBytes, ...cacheRuntimeSnapshot() };
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function cleanRuntimeCaches({ history = false } = {}) {
|
|
38
|
+
const imageStats = imageOptimizationStats();
|
|
39
|
+
if (imageStats.active > 0 || imageStats.queued > 0 || imageStats.cache_inflight > 0) {
|
|
40
|
+
const error = new Error("Image optimization is active; retry cache cleanup after current requests finish");
|
|
41
|
+
error.code = "ccdx_cache_busy";
|
|
42
|
+
throw error;
|
|
43
|
+
}
|
|
44
|
+
const historyStats = responseHistoryStats();
|
|
45
|
+
if (history && historyStats.pinnedTrees > 0) {
|
|
46
|
+
const error = new Error("Response history is active; retry cleanup after current requests finish");
|
|
47
|
+
error.code = "ccdx_cache_busy";
|
|
48
|
+
throw error;
|
|
49
|
+
}
|
|
50
|
+
const imageOptimization = clearImageOptimizationCache();
|
|
51
|
+
const responseHistory = history ? clearResponseHistory() : null;
|
|
52
|
+
return {
|
|
53
|
+
cleaned: {
|
|
54
|
+
image_optimization: imageOptimization,
|
|
55
|
+
...(responseHistory ? { response_history: responseHistory } : {}),
|
|
56
|
+
},
|
|
57
|
+
...cacheRuntimeSnapshot(),
|
|
58
|
+
};
|
|
59
|
+
}
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import { createInterface } from "node:readline/promises";
|
|
2
|
+
import os from "node:os";
|
|
3
|
+
import { ADAPTER_CACHE_PATH } from "./cache-control.mjs";
|
|
4
|
+
import { MIB, RESPONSE_HISTORY_DEFAULT_MIB } from "./cache-limits.mjs";
|
|
5
|
+
import { adapterBaseUrl } from "./running-adapter.mjs";
|
|
6
|
+
import {
|
|
7
|
+
readUserSettings,
|
|
8
|
+
responseHistoryLimitPreference,
|
|
9
|
+
writeResponseHistoryLimitMib,
|
|
10
|
+
} from "./user-settings.mjs";
|
|
11
|
+
|
|
12
|
+
const RESPONSE_HISTORY_MAX_MIB_KEY = "response_history_max_mib";
|
|
13
|
+
|
|
14
|
+
function mib(bytes) {
|
|
15
|
+
const value = Number(bytes);
|
|
16
|
+
return Number.isFinite(value) ? `${(value / MIB).toFixed(value % MIB === 0 ? 0 : 1)} MiB` : "unknown";
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
async function cacheRequest(baseUrl, { body, fetchImpl, timeoutMs }) {
|
|
20
|
+
const response = await fetchImpl(`${baseUrl}${ADAPTER_CACHE_PATH}`, {
|
|
21
|
+
method: body ? "POST" : "GET",
|
|
22
|
+
headers: { Accept: "application/json", ...(body ? { "Content-Type": "application/json" } : {}) },
|
|
23
|
+
...(body ? { body: JSON.stringify(body) } : {}),
|
|
24
|
+
signal: AbortSignal.timeout(timeoutMs),
|
|
25
|
+
});
|
|
26
|
+
let payload;
|
|
27
|
+
try { payload = await response.json(); } catch { payload = null; }
|
|
28
|
+
if (!response.ok) {
|
|
29
|
+
const error = new Error(payload?.error || `Cache control returned HTTP ${response.status}`);
|
|
30
|
+
error.statusCode = response.status;
|
|
31
|
+
throw error;
|
|
32
|
+
}
|
|
33
|
+
return payload;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
async function runningSnapshot(baseUrl, options) {
|
|
37
|
+
try { return { supported: true, data: await cacheRequest(baseUrl, options) }; }
|
|
38
|
+
catch (error) {
|
|
39
|
+
if ([404, 405].includes(error?.statusCode)) return { supported: false, error };
|
|
40
|
+
if (error?.name === "TimeoutError" || error?.cause?.code === "ECONNREFUSED") return { supported: false, offline: true, error };
|
|
41
|
+
throw error;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
async function confirmHistoryCleanup({ input, output, prompt, yes }) {
|
|
46
|
+
if (yes) return;
|
|
47
|
+
if (prompt) {
|
|
48
|
+
if (!await prompt()) throw new Error("History cache cleanup cancelled");
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
if (!input.isTTY || !output.isTTY) {
|
|
52
|
+
throw new Error("History cleanup can break task continuation; rerun with --yes to confirm");
|
|
53
|
+
}
|
|
54
|
+
const readline = createInterface({ input, output });
|
|
55
|
+
try {
|
|
56
|
+
const answer = String(await readline.question(
|
|
57
|
+
"Clear response history for all tasks? Their next continuation may fail [y/N]: ",
|
|
58
|
+
)).trim().toLowerCase();
|
|
59
|
+
if (!["y", "yes"].includes(answer)) throw new Error("History cache cleanup cancelled");
|
|
60
|
+
} finally { readline.close(); }
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function formatStatus(preference, runtime) {
|
|
64
|
+
const lines = ["ccdx cache", `Configured history limit: ${mib(preference.bytes)} (${preference.source})`];
|
|
65
|
+
if (!runtime?.supported) {
|
|
66
|
+
lines.push(runtime?.offline
|
|
67
|
+
? "Runtime cache: adapter is not running"
|
|
68
|
+
: "Runtime cache: restart ccdx to enable cache control");
|
|
69
|
+
return lines.join("\n");
|
|
70
|
+
}
|
|
71
|
+
const history = runtime.data.response_history || {};
|
|
72
|
+
const images = runtime.data.image_optimization || {};
|
|
73
|
+
lines.push(
|
|
74
|
+
`History: ${mib(history.bytes)} / ${mib(history.maxBytes)}, ${history.entries || 0} entries, ${history.evicted || 0} evicted`,
|
|
75
|
+
`Image transforms: ${mib(images.cache_bytes)} / ${mib(images.cache_max_bytes)}, ${images.cache_entries || 0} entries`,
|
|
76
|
+
);
|
|
77
|
+
return lines.join("\n");
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export async function runCacheCommand({
|
|
81
|
+
action = "status",
|
|
82
|
+
limitMib,
|
|
83
|
+
resetLimit = false,
|
|
84
|
+
history = false,
|
|
85
|
+
yes = false,
|
|
86
|
+
env = process.env,
|
|
87
|
+
home = os.homedir(),
|
|
88
|
+
host = "127.0.0.1",
|
|
89
|
+
port = 2026,
|
|
90
|
+
input = process.stdin,
|
|
91
|
+
output = process.stdout,
|
|
92
|
+
prompt,
|
|
93
|
+
fetchImpl = fetch,
|
|
94
|
+
timeoutMs = 1000,
|
|
95
|
+
} = {}) {
|
|
96
|
+
const baseUrl = adapterBaseUrl(host, port);
|
|
97
|
+
const requestOptions = { fetchImpl, timeoutMs };
|
|
98
|
+
const preference = responseHistoryLimitPreference({ env, home });
|
|
99
|
+
const runtime = await runningSnapshot(baseUrl, requestOptions);
|
|
100
|
+
if (action === "status") {
|
|
101
|
+
const text = formatStatus(preference, runtime);
|
|
102
|
+
output.write(`${text}\n`);
|
|
103
|
+
return { preference, runtime };
|
|
104
|
+
}
|
|
105
|
+
if (action === "limit") {
|
|
106
|
+
if (String(env.CCDX_RESPONSE_HISTORY_MAX_BYTES || "").trim()) {
|
|
107
|
+
throw new Error("CCDX_RESPONSE_HISTORY_MAX_BYTES overrides saved cache settings; unset it before using ccdx cache --limit");
|
|
108
|
+
}
|
|
109
|
+
const targetMib = resetLimit ? RESPONSE_HISTORY_DEFAULT_MIB : Number(limitMib);
|
|
110
|
+
const targetBytes = targetMib * MIB;
|
|
111
|
+
if (runtime.supported && targetBytes < runtime.data.response_history.bytes) {
|
|
112
|
+
throw new Error(`History currently uses ${mib(runtime.data.response_history.bytes)}; clean history or choose a larger limit`);
|
|
113
|
+
}
|
|
114
|
+
const previous = readUserSettings({ env, home, strict: true });
|
|
115
|
+
const previousMib = Object.hasOwn(previous, RESPONSE_HISTORY_MAX_MIB_KEY)
|
|
116
|
+
? previous[RESPONSE_HISTORY_MAX_MIB_KEY]
|
|
117
|
+
: null;
|
|
118
|
+
const saved = writeResponseHistoryLimitMib(resetLimit ? null : targetMib, { env, home });
|
|
119
|
+
try {
|
|
120
|
+
if (runtime.supported) {
|
|
121
|
+
await cacheRequest(baseUrl, { ...requestOptions, body: { action: "set_limit", max_bytes: targetBytes } });
|
|
122
|
+
}
|
|
123
|
+
} catch (error) {
|
|
124
|
+
writeResponseHistoryLimitMib(previousMib, { env, home });
|
|
125
|
+
throw error;
|
|
126
|
+
}
|
|
127
|
+
output.write(`History cache limit: ${targetMib} MiB${runtime.supported ? " (active now)" : " (saved; restart ccdx to apply)"}\n`);
|
|
128
|
+
return { ...saved, active: runtime.supported };
|
|
129
|
+
}
|
|
130
|
+
if (action === "clean") {
|
|
131
|
+
if (!runtime.supported) throw new Error("Start or restart ccdx before cleaning runtime caches");
|
|
132
|
+
if (history) await confirmHistoryCleanup({ input, output, prompt, yes });
|
|
133
|
+
const result = await cacheRequest(baseUrl, {
|
|
134
|
+
...requestOptions,
|
|
135
|
+
body: { action: "clean", history },
|
|
136
|
+
});
|
|
137
|
+
const images = result.cleaned.image_optimization;
|
|
138
|
+
output.write(`Cleared image transform cache: ${images.entries} entries, ${mib(images.bytes)}\n`);
|
|
139
|
+
if (history) {
|
|
140
|
+
const cleared = result.cleaned.response_history;
|
|
141
|
+
output.write(`Cleared response history: ${cleared.entries} entries, ${mib(cleared.bytes)}\n`);
|
|
142
|
+
output.write("Saved Codex transcripts and generated image files were not deleted.\n");
|
|
143
|
+
}
|
|
144
|
+
return result;
|
|
145
|
+
}
|
|
146
|
+
throw new Error(`Unknown cache action: ${action}`);
|
|
147
|
+
}
|
package/src/cli-options.mjs
CHANGED
|
@@ -20,6 +20,7 @@ const HELP_TOPICS = new Set([
|
|
|
20
20
|
"status",
|
|
21
21
|
"models",
|
|
22
22
|
"usage",
|
|
23
|
+
"cache",
|
|
23
24
|
"animation",
|
|
24
25
|
"enable-image",
|
|
25
26
|
"disable-image",
|
|
@@ -155,6 +156,37 @@ export function parseCliArgs(args = []) {
|
|
|
155
156
|
const options = parseOptions(rest, new Map([["--format", "value"]]));
|
|
156
157
|
return baseCommand("usage", outputFormatOption(options["--format"]));
|
|
157
158
|
}
|
|
159
|
+
if (command === "cache") {
|
|
160
|
+
let cacheArgs = rest;
|
|
161
|
+
if (rest.length === 1 && /^-\d+$/.test(rest[0])) {
|
|
162
|
+
cacheArgs = ["--limit", rest[0].slice(1)];
|
|
163
|
+
} else if (rest[0] === "-clean") {
|
|
164
|
+
cacheArgs = ["--clean", ...rest.slice(1)];
|
|
165
|
+
}
|
|
166
|
+
const options = parseOptions(cacheArgs, new Map([
|
|
167
|
+
["--limit", "value"],
|
|
168
|
+
["--clean", "flag"],
|
|
169
|
+
["--history", "flag"],
|
|
170
|
+
["--yes", "flag"],
|
|
171
|
+
]));
|
|
172
|
+
if (options["--limit"] && options["--clean"]) {
|
|
173
|
+
throw new Error("Use --limit or --clean in one cache command, not both");
|
|
174
|
+
}
|
|
175
|
+
if ((options["--history"] || options["--yes"]) && !options["--clean"]) {
|
|
176
|
+
throw new Error("--history and --yes require --clean");
|
|
177
|
+
}
|
|
178
|
+
const rawLimit = options["--limit"];
|
|
179
|
+
if (rawLimit && rawLimit !== "default" && !/^\d+$/.test(rawLimit)) {
|
|
180
|
+
throw new Error(`Cache limit must be an integer MiB value or default: ${rawLimit}`);
|
|
181
|
+
}
|
|
182
|
+
return baseCommand("cache", {
|
|
183
|
+
action: rawLimit ? "limit" : options["--clean"] ? "clean" : "status",
|
|
184
|
+
limitMib: rawLimit && rawLimit !== "default" ? Number(rawLimit) : undefined,
|
|
185
|
+
resetLimit: rawLimit === "default",
|
|
186
|
+
history: Boolean(options["--history"]),
|
|
187
|
+
yes: Boolean(options["--yes"]),
|
|
188
|
+
});
|
|
189
|
+
}
|
|
158
190
|
if (PM_COMMANDS.has(command)) {
|
|
159
191
|
return baseCommand("retired", { integration: "PM Studio" });
|
|
160
192
|
}
|
|
@@ -301,6 +333,7 @@ function topicHelp(name, topic) {
|
|
|
301
333
|
status: `Usage:\n ${name} status\n\nShows bounded runtime, routing, performance, queue, and cache metrics from a running adapter.`,
|
|
302
334
|
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.`,
|
|
303
335
|
usage: `Usage:\n ${name} usage [--format table|plain]\n\nSummarizes local token usage metadata without reading prompt or completion content. Interactive terminals use a table by default.`,
|
|
336
|
+
cache: `Usage:\n ${name} cache\n ${name} cache --limit <MiB|default>\n ${name} cache --clean [--history] [--yes]\n\nShows runtime cache use, saves and applies the shared response-history limit, or clears the rebuildable image-transform cache. --history also clears in-memory response history for all tasks and can make their next continuation fail, so interactive confirmation or --yes is required. Generated image files and saved Codex transcripts are never deleted. Short aliases: -128 sets 128 MiB and -clean equals --clean.`,
|
|
304
337
|
animation: `Usage:\n ${name} animation\n\nPreviews all terminal activity animations together and selects the theme used the next time the adapter starts.`,
|
|
305
338
|
"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.`,
|
|
306
339
|
"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.`,
|
|
@@ -324,6 +357,7 @@ export function cliHelp(commandName = "ccdx", topic = "") {
|
|
|
324
357
|
${name} status
|
|
325
358
|
${name} models [--format table|plain]
|
|
326
359
|
${name} usage [--format table|plain]
|
|
360
|
+
${name} cache [--limit <MiB|default> | --clean [--history] [--yes]]
|
|
327
361
|
${name} animation
|
|
328
362
|
${name} enable-image
|
|
329
363
|
${name} disable-image
|
|
@@ -340,6 +374,7 @@ Commands:
|
|
|
340
374
|
status Show runtime routing, performance, queue, and cache health
|
|
341
375
|
models Query a saved account's live Copilot model catalog
|
|
342
376
|
usage Summarize locally recorded token usage
|
|
377
|
+
cache Inspect, size, or clear bounded runtime caches
|
|
343
378
|
animation Select the terminal activity animation
|
|
344
379
|
enable-image Configure and enable the optional image provider
|
|
345
380
|
disable-image Disable the image provider and remove its credential
|
|
@@ -4,6 +4,30 @@ import os from "node:os";
|
|
|
4
4
|
import path from "node:path";
|
|
5
5
|
import { CODEX_GPT6_MODEL } from "./models.mjs";
|
|
6
6
|
|
|
7
|
+
// Compatibility metadata is derived from OpenAI Codex's public models catalog
|
|
8
|
+
// at commit 0a2eb4696c26ac33204bcd255721ab30220a4774. The installed client's
|
|
9
|
+
// GPT-6 Astra entry supplies its own schema and instructions; these overrides
|
|
10
|
+
// describe only the two new model identities and capability differences.
|
|
11
|
+
const GPT6_COMPATIBILITY_SPECS = new Map([
|
|
12
|
+
["gpt-6-sol", Object.freeze({
|
|
13
|
+
display_name: "GPT-6-Sol",
|
|
14
|
+
description: "Workhorse model for coding and everyday work.",
|
|
15
|
+
default_reasoning_level: "medium",
|
|
16
|
+
shell_type: "shell_command",
|
|
17
|
+
priority: 2,
|
|
18
|
+
node_repl_auto_review_required: true,
|
|
19
|
+
})],
|
|
20
|
+
["gpt-6-luna", Object.freeze({
|
|
21
|
+
display_name: "GPT-6-Luna",
|
|
22
|
+
description: "Fast and affordable model for easier tasks.",
|
|
23
|
+
default_reasoning_level: "medium",
|
|
24
|
+
shell_type: "shell_command",
|
|
25
|
+
priority: 3,
|
|
26
|
+
node_repl_auto_review_required: false,
|
|
27
|
+
})],
|
|
28
|
+
]);
|
|
29
|
+
const MANAGED_GPT6_MODELS = new Set([CODEX_GPT6_MODEL, ...GPT6_COMPATIBILITY_SPECS.keys()]);
|
|
30
|
+
|
|
7
31
|
export { CODEX_GPT6_MODEL };
|
|
8
32
|
|
|
9
33
|
export const CODEX_APP_BINARY_PATHS = [
|
|
@@ -50,29 +74,81 @@ function isEligibleOpenAIModel(model, id) {
|
|
|
50
74
|
&& supportsResponses(model);
|
|
51
75
|
}
|
|
52
76
|
|
|
53
|
-
export function isEligibleCopilotGpt6(copilotModels) {
|
|
77
|
+
export function isEligibleCopilotGpt6(copilotModels, id = CODEX_GPT6_MODEL) {
|
|
54
78
|
const data = copilotModelData(copilotModels);
|
|
55
79
|
if (!data) return false;
|
|
56
|
-
const matches = data.filter((model) => String(model?.id || "").trim() ===
|
|
57
|
-
return matches.length === 1 && isEligibleOpenAIModel(matches[0],
|
|
80
|
+
const matches = data.filter((model) => String(model?.id || "").trim() === id);
|
|
81
|
+
return matches.length === 1 && isEligibleOpenAIModel(matches[0], id);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function copilotModel(copilotModels, id) {
|
|
85
|
+
return copilotModelData(copilotModels)?.find((model) => String(model?.id || "").trim() === id);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function advertisedReasoningEfforts(model) {
|
|
89
|
+
const values = model?.capabilities?.supports?.reasoning_effort;
|
|
90
|
+
return new Set(Array.isArray(values) ? values.map((value) => String(value)) : []);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function catalogModelForCopilot(model, copilotModels, { filterReasoning = false } = {}) {
|
|
94
|
+
const upstream = copilotModel(copilotModels, model.slug);
|
|
95
|
+
const reasoning = advertisedReasoningEfforts(upstream);
|
|
96
|
+
const patched = {
|
|
97
|
+
...structuredClone(model),
|
|
98
|
+
visibility: "list",
|
|
99
|
+
supported_reasoning_levels: filterReasoning && reasoning.size
|
|
100
|
+
? model.supported_reasoning_levels.filter(({ effort }) => reasoning.has(effort))
|
|
101
|
+
: model.supported_reasoning_levels,
|
|
102
|
+
};
|
|
103
|
+
// The Codex catalog describes ChatGPT service tiers. Copilot must advertise
|
|
104
|
+
// an exact fast model before CCDX exposes the corresponding selector.
|
|
105
|
+
if (!isEligibleCopilotGpt6(copilotModels, `${model.slug}-fast`)) {
|
|
106
|
+
patched.additional_speed_tiers = [];
|
|
107
|
+
patched.service_tiers = [];
|
|
108
|
+
delete patched.default_service_tier;
|
|
109
|
+
}
|
|
110
|
+
return patched;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function compatibilityModel(codexCatalog, slug) {
|
|
114
|
+
const astra = codexCatalog.models.find((model) => model.slug === CODEX_GPT6_MODEL);
|
|
115
|
+
const spec = GPT6_COMPATIBILITY_SPECS.get(slug);
|
|
116
|
+
if (!astra || !spec) return null;
|
|
117
|
+
return {
|
|
118
|
+
...structuredClone(astra),
|
|
119
|
+
...spec,
|
|
120
|
+
slug,
|
|
121
|
+
supports_parallel_tool_calls: true,
|
|
122
|
+
multi_agent_reasoning_effort: null,
|
|
123
|
+
supports_experimental_context: false,
|
|
124
|
+
minimal_client_version: "0.155.0",
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function insertByPriority(models, model) {
|
|
129
|
+
const priority = Number(model.priority);
|
|
130
|
+
const index = models.findIndex((candidate) => Number.isFinite(priority)
|
|
131
|
+
&& Number.isFinite(Number(candidate.priority))
|
|
132
|
+
&& Number(candidate.priority) > priority);
|
|
133
|
+
if (index < 0) models.push(model);
|
|
134
|
+
else models.splice(index, 0, model);
|
|
58
135
|
}
|
|
59
136
|
|
|
60
137
|
export function buildCodexModelResponse({ copilotModels, codexCatalog } = {}) {
|
|
61
138
|
if (!copilotModelData(copilotModels) || !codexCatalogIsValid(codexCatalog)) return null;
|
|
62
139
|
|
|
63
|
-
const
|
|
140
|
+
const bundledSlugs = new Set(codexCatalog.models.map(({ slug }) => slug));
|
|
64
141
|
const models = codexCatalog.models.map((model) => {
|
|
65
|
-
if (model.slug
|
|
66
|
-
if (!
|
|
67
|
-
|
|
68
|
-
...model,
|
|
69
|
-
visibility: "list",
|
|
70
|
-
additional_speed_tiers: [],
|
|
71
|
-
service_tiers: [],
|
|
72
|
-
};
|
|
73
|
-
delete patched.default_service_tier;
|
|
74
|
-
return patched;
|
|
142
|
+
if (!MANAGED_GPT6_MODELS.has(model.slug)) return model;
|
|
143
|
+
if (!isEligibleCopilotGpt6(copilotModels, model.slug)) return { ...model, visibility: "hide" };
|
|
144
|
+
return catalogModelForCopilot(model, copilotModels);
|
|
75
145
|
});
|
|
146
|
+
for (const slug of GPT6_COMPATIBILITY_SPECS.keys()) {
|
|
147
|
+
if (bundledSlugs.has(slug) || !isEligibleCopilotGpt6(copilotModels, slug)) continue;
|
|
148
|
+
const model = compatibilityModel(codexCatalog, slug);
|
|
149
|
+
if (!model) continue;
|
|
150
|
+
insertByPriority(models, catalogModelForCopilot(model, copilotModels, { filterReasoning: true }));
|
|
151
|
+
}
|
|
76
152
|
|
|
77
153
|
return { ...copilotModels, object: copilotModels.object || "list", models };
|
|
78
154
|
}
|
|
@@ -159,6 +159,18 @@ export function imageOptimizationStats() {
|
|
|
159
159
|
};
|
|
160
160
|
}
|
|
161
161
|
|
|
162
|
+
export function clearImageOptimizationCache() {
|
|
163
|
+
const stats = imageOptimizationStats();
|
|
164
|
+
if (stats.active > 0 || stats.queued > 0 || stats.cache_inflight > 0) {
|
|
165
|
+
const error = new Error("Image optimization is active; retry cache cleanup after current requests finish");
|
|
166
|
+
error.code = "ccdx_cache_busy";
|
|
167
|
+
throw error;
|
|
168
|
+
}
|
|
169
|
+
const removed = { entries: stats.cache_entries, bytes: stats.cache_bytes };
|
|
170
|
+
imageResultCache.clear();
|
|
171
|
+
return removed;
|
|
172
|
+
}
|
|
173
|
+
|
|
162
174
|
export function resetImageOptimizationCacheForTests() {
|
|
163
175
|
imageResultCache.clear();
|
|
164
176
|
}
|
package/src/models.mjs
CHANGED
|
@@ -26,8 +26,8 @@ export function modelIsResponsesOnly(model) {
|
|
|
26
26
|
|
|
27
27
|
export function resolveCopilotPriorityTierModel(model, serviceTier, models) {
|
|
28
28
|
const baseModel = String(model || "").trim();
|
|
29
|
-
if (baseModel
|
|
30
|
-
const fastModelId =
|
|
29
|
+
if (!baseModel.startsWith("gpt-") || serviceTier !== "priority") return null;
|
|
30
|
+
const fastModelId = `${baseModel}-fast`;
|
|
31
31
|
const fastModel = copilotModelData(models)
|
|
32
32
|
.find((entry) => String(entry?.id || "").trim() === fastModelId);
|
|
33
33
|
const vendor = String(fastModel?.vendor || fastModel?.owned_by || "").trim().toLowerCase();
|
package/src/observability.mjs
CHANGED
|
@@ -167,6 +167,7 @@ export function runtimeStatusPayload({
|
|
|
167
167
|
const memory = process.memoryUsage();
|
|
168
168
|
const codexRuntime = safeClientRuntimeStatus(codexClient);
|
|
169
169
|
const codexModels = modelRegistryStatus(codexModelRegistry || modelRegistry);
|
|
170
|
+
const history = responseHistoryStats();
|
|
170
171
|
return {
|
|
171
172
|
...adapterHealthPayload(),
|
|
172
173
|
uptime_ms: Math.round(process.uptime() * 1000),
|
|
@@ -180,7 +181,7 @@ export function runtimeStatusPayload({
|
|
|
180
181
|
requests: metrics?.snapshot?.() || createRequestMetrics().snapshot(),
|
|
181
182
|
stream_performance: streamPerformance?.snapshot?.() || null,
|
|
182
183
|
admission: admission?.diagnostics?.() || admission?.stats?.() || null,
|
|
183
|
-
response_history:
|
|
184
|
+
response_history: history,
|
|
184
185
|
image_optimization: imageOptimizationStats(),
|
|
185
186
|
image_generation: imageGeneration || null,
|
|
186
187
|
logging: { usage: usageLoggingStats(), debug: debugLoggingStats() },
|
|
@@ -196,8 +197,8 @@ export function runtimeStatusPayload({
|
|
|
196
197
|
max_body_bytes: OBSERVABILITY_RUNTIME_CONFIG.maxBodyBytes,
|
|
197
198
|
max_decoded_body_bytes: OBSERVABILITY_RUNTIME_CONFIG.maxDecodedBodyBytes,
|
|
198
199
|
max_sse_buffer_bytes: OBSERVABILITY_RUNTIME_CONFIG.maxSseBufferBytes,
|
|
199
|
-
response_history_max_bytes:
|
|
200
|
-
response_history_max_entries:
|
|
200
|
+
response_history_max_bytes: history.maxBytes,
|
|
201
|
+
response_history_max_entries: history.maxEntries,
|
|
201
202
|
},
|
|
202
203
|
};
|
|
203
204
|
}
|
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}`;
|
|
@@ -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/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/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 });
|