codex-copilot-dx 0.9.12 → 0.9.14
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +61 -431
- package/bin/cli.mjs +2 -2
- package/docs/README.md +55 -0
- package/docs/REFERENCE.md +481 -0
- package/package.json +4 -3
- package/src/adapter.mjs +2 -0
- package/src/auto-review-model.mjs +47 -8
- package/src/cli-options.mjs +3 -3
- package/src/cli-status.mjs +3 -0
- package/src/codex-client-doctor.mjs +67 -0
- package/src/codex-model-catalog.mjs +16 -2
- package/src/copilot-client.mjs +2 -1
- package/src/copilot-responses-policy.mjs +35 -18
- package/src/dashboard/index.html +7 -0
- package/src/dashboard/language.js +11 -0
- package/src/dashboard/ui.js +30 -0
- package/src/doctor.mjs +3 -0
- package/src/image-optimization.mjs +1 -1
- package/src/models.mjs +16 -0
- package/src/observability.mjs +17 -0
- package/src/responses-bridge.mjs +1 -1
- package/src/responses-chat-payload.mjs +2 -1
- package/src/responses-handler.mjs +23 -10
- package/src/responses-proxy.mjs +10 -0
- package/src/sse-keepalive.mjs +42 -0
- package/src/stream-performance.mjs +14 -1
- package/src/usage.mjs +3 -0
- package/src/user-settings.mjs +32 -7
|
@@ -16,6 +16,7 @@ import {
|
|
|
16
16
|
CODEX_AUTO_REVIEW_MODEL,
|
|
17
17
|
modelIsResponsesOnly,
|
|
18
18
|
modelSupportsChatCompletions,
|
|
19
|
+
modelContextWindowTokens,
|
|
19
20
|
resolveCopilotPriorityTierModel,
|
|
20
21
|
resolveOpenAIModel,
|
|
21
22
|
} from "./models.mjs";
|
|
@@ -35,11 +36,13 @@ import {
|
|
|
35
36
|
import { createRoutePlan } from "./route-plan.mjs";
|
|
36
37
|
import { status } from "./status.mjs";
|
|
37
38
|
import { endStreamWithError } from "./stream-errors.mjs";
|
|
39
|
+
import { createSseKeepalive } from "./sse-keepalive.mjs";
|
|
38
40
|
import { safeUpstreamResponseHeaders } from "./upstream-headers.mjs";
|
|
39
41
|
import { recordResponsesUsage } from "./usage.mjs";
|
|
40
42
|
import {
|
|
41
43
|
markResponseErrorOrigin,
|
|
42
44
|
markResponseModel,
|
|
45
|
+
markRequestObservation,
|
|
43
46
|
markResponseTerminal,
|
|
44
47
|
markUpstreamHeaders,
|
|
45
48
|
measureRequestStage,
|
|
@@ -90,10 +93,13 @@ function unsupportedCustomToolsError(model) {
|
|
|
90
93
|
}
|
|
91
94
|
|
|
92
95
|
function resolveRequestModel(model, openAIModelEnv, autoReviewModelResolver) {
|
|
93
|
-
const
|
|
94
|
-
?
|
|
95
|
-
|
|
96
|
-
|
|
96
|
+
const preference = model === CODEX_AUTO_REVIEW_MODEL && typeof autoReviewModelResolver === "function"
|
|
97
|
+
? autoReviewModelResolver() : undefined;
|
|
98
|
+
const resolved = resolveOpenAIModel(model, openAIModelEnv, {
|
|
99
|
+
autoReviewModel: typeof preference === "string" ? preference : preference?.model,
|
|
100
|
+
});
|
|
101
|
+
return { ...resolved, reasoningEffort: preference?.model === resolved.upstreamModel
|
|
102
|
+
? preference.reasoningEffort : undefined };
|
|
97
103
|
}
|
|
98
104
|
|
|
99
105
|
function stripUnsupportedGpt6ServiceTier(body, requestedModel, upstreamModel, priorityTierModel) {
|
|
@@ -112,6 +118,7 @@ export function createResponsesHandler(options) {
|
|
|
112
118
|
chatCompletionsFn,
|
|
113
119
|
getCachedModelEndpointsFn = getCachedModelEndpoints,
|
|
114
120
|
imagePressure,
|
|
121
|
+
keepaliveIntervalMs,
|
|
115
122
|
modelRegistry,
|
|
116
123
|
openAIModelEnv,
|
|
117
124
|
requestBodyTimeoutMs,
|
|
@@ -274,12 +281,13 @@ export function createResponsesHandler(options) {
|
|
|
274
281
|
stripUnsupportedGpt6ServiceTier(prepared.body, requestedModel, upstreamModel, priorityTierModel);
|
|
275
282
|
if (requestedModel === CODEX_AUTO_REVIEW_MODEL) {
|
|
276
283
|
delete prepared.body.service_tier;
|
|
277
|
-
if (prepared.body.reasoning?.effort === undefined) {
|
|
278
|
-
prepared.body.reasoning = { ...prepared.body.reasoning, effort: "low" };
|
|
284
|
+
if (resolvedModel.reasoningEffort || prepared.body.reasoning?.effort === undefined) {
|
|
285
|
+
prepared.body.reasoning = { ...prepared.body.reasoning, effort: resolvedModel.reasoningEffort || "low" };
|
|
279
286
|
}
|
|
280
287
|
}
|
|
281
288
|
if (upstreamModel !== requestedModel) prepared.body.model = upstreamModel;
|
|
282
289
|
markResponseModel(upstreamModel);
|
|
290
|
+
markRequestObservation({ context_window_tokens: modelContextWindowTokens(modelRegistry?.models, upstreamModel) });
|
|
283
291
|
const upstreamLog = upstreamModel === requestedModel ? "" : ` upstream_model=${upstreamModel}`;
|
|
284
292
|
console.log(status("info", `responses model=${requestedModel}${upstreamLog} stream=${streaming}`));
|
|
285
293
|
const usesCustomTools = responsesBodyUsesCustomTools(prepared.body);
|
|
@@ -310,6 +318,7 @@ export function createResponsesHandler(options) {
|
|
|
310
318
|
releaseRequest: releaseUpstreamPayload,
|
|
311
319
|
responseFailures,
|
|
312
320
|
streamIdleTimeoutMs,
|
|
321
|
+
keepaliveIntervalMs,
|
|
313
322
|
});
|
|
314
323
|
if (result?.successful) imagePressure?.markSuccess?.(responseHistoryPressureRootId(prepared));
|
|
315
324
|
else markAdaptiveHttpTimeout(result?.upstreamStatus);
|
|
@@ -327,12 +336,15 @@ export function createResponsesHandler(options) {
|
|
|
327
336
|
};
|
|
328
337
|
if (streaming) {
|
|
329
338
|
let streamResponseHeaders = null;
|
|
330
|
-
const
|
|
339
|
+
const keepalive = createSseKeepalive({ res, signal: abort.signal, intervalMs: keepaliveIntervalMs });
|
|
340
|
+
let successful;
|
|
341
|
+
try { successful = await forwardToChat(chatReq, async (event, data) => {
|
|
331
342
|
if (!res.headersSent) res.writeHead(200, {
|
|
332
343
|
...streamResponseHeaders,
|
|
333
344
|
"Cache-Control": "no-cache",
|
|
334
345
|
Connection: "keep-alive",
|
|
335
346
|
});
|
|
347
|
+
keepalive.activity();
|
|
336
348
|
const written = await writeOrDrain(res, `event: ${event}\ndata: ${JSON.stringify(data)}\n\n`, { signal: abort.signal });
|
|
337
349
|
if (!written) return false;
|
|
338
350
|
if (event === "response.completed" || event === "response.incomplete") {
|
|
@@ -367,7 +379,7 @@ export function createResponsesHandler(options) {
|
|
|
367
379
|
contentType: "text/event-stream",
|
|
368
380
|
});
|
|
369
381
|
},
|
|
370
|
-
});
|
|
382
|
+
}); } finally { keepalive.stop(); }
|
|
371
383
|
if (successful) imagePressure?.markSuccess?.(responseHistoryPressureRootId(prepared));
|
|
372
384
|
} else {
|
|
373
385
|
try {
|
|
@@ -544,11 +556,12 @@ export function createResponsesCompactHandler(options) {
|
|
|
544
556
|
stripUnsupportedGpt6ServiceTier(prepared.body, requestedModel, upstreamModel, priorityTierModel);
|
|
545
557
|
if (requestedModel === CODEX_AUTO_REVIEW_MODEL) {
|
|
546
558
|
delete prepared.body.service_tier;
|
|
547
|
-
if (prepared.body.reasoning?.effort === undefined) {
|
|
548
|
-
prepared.body.reasoning = { ...prepared.body.reasoning, effort: "low" };
|
|
559
|
+
if (resolvedModel.reasoningEffort || prepared.body.reasoning?.effort === undefined) {
|
|
560
|
+
prepared.body.reasoning = { ...prepared.body.reasoning, effort: resolvedModel.reasoningEffort || "low" };
|
|
549
561
|
}
|
|
550
562
|
}
|
|
551
563
|
if (upstreamModel !== requestedModel) prepared.body.model = upstreamModel;
|
|
564
|
+
markRequestObservation({ context_window_tokens: modelContextWindowTokens(modelRegistry?.models, upstreamModel) });
|
|
552
565
|
const upstreamLog = upstreamModel === requestedModel ? "" : ` upstream_model=${upstreamModel}`;
|
|
553
566
|
console.log(status("info", `responses compact model=${requestedModel}${upstreamLog} stream=false`));
|
|
554
567
|
prepared = applyResponseHistoryRoutePlan(prepared, createRoutePlan({
|
package/src/responses-proxy.mjs
CHANGED
|
@@ -30,6 +30,7 @@ import {
|
|
|
30
30
|
} from "./responses-compaction.mjs";
|
|
31
31
|
import { loadRuntimeConfig } from "./runtime-config.mjs";
|
|
32
32
|
import { endStreamWithError } from "./stream-errors.mjs";
|
|
33
|
+
import { createSseKeepalive } from "./sse-keepalive.mjs";
|
|
33
34
|
import {
|
|
34
35
|
incompleteUpstreamStream,
|
|
35
36
|
invalidUpstreamStream,
|
|
@@ -40,6 +41,7 @@ import {
|
|
|
40
41
|
isResponsesOutputEvent,
|
|
41
42
|
markFirstOutput,
|
|
42
43
|
markOutputTokens,
|
|
44
|
+
markRequestObservation,
|
|
43
45
|
markResponseErrorOrigin,
|
|
44
46
|
markResponseTerminal,
|
|
45
47
|
markStreamFailure,
|
|
@@ -123,6 +125,8 @@ function inspectResponseSseEvent(state, eventName, data) {
|
|
|
123
125
|
const changed = normalizeMessageIds(state, event, eventType);
|
|
124
126
|
const outputTokens = event.response?.usage?.output_tokens ?? event.usage?.output_tokens;
|
|
125
127
|
if (outputTokens !== undefined) markOutputTokens(outputTokens);
|
|
128
|
+
const inputTokens = event.response?.usage?.input_tokens ?? event.usage?.input_tokens;
|
|
129
|
+
if (inputTokens !== undefined) markRequestObservation({ input_tokens: inputTokens });
|
|
126
130
|
if (!state.sawOutput && isResponsesOutputEvent(event, eventType)) {
|
|
127
131
|
state.sawOutput = true;
|
|
128
132
|
markFirstOutput();
|
|
@@ -404,18 +408,23 @@ async function proxyStreamingResponses(opened, res, upstream, options) {
|
|
|
404
408
|
let retryDisabledReason = holdPrelude ? null : "request_not_eligible";
|
|
405
409
|
const prelude = [];
|
|
406
410
|
let preludeBytes = 0;
|
|
411
|
+
const keepalive = createSseKeepalive({ res, signal: options.signal,
|
|
412
|
+
intervalMs: options.keepaliveIntervalMs,
|
|
413
|
+
canWrite: () => !holdPrelude && !streamState.sawTerminal });
|
|
407
414
|
const flushPrelude = async () => {
|
|
408
415
|
if (!preludeBytes) return true;
|
|
409
416
|
writeHeaders();
|
|
410
417
|
const body = prelude.length === 1 ? prelude[0] : Buffer.concat(prelude, preludeBytes);
|
|
411
418
|
prelude.length = 0;
|
|
412
419
|
preludeBytes = 0;
|
|
420
|
+
keepalive.activity();
|
|
413
421
|
return writeOrDrain(res, body, { signal: options.signal });
|
|
414
422
|
};
|
|
415
423
|
const forwardFrame = async (frame) => {
|
|
416
424
|
if (!frame) return true;
|
|
417
425
|
if (!holdPrelude) {
|
|
418
426
|
writeHeaders();
|
|
427
|
+
keepalive.activity();
|
|
419
428
|
return writeOrDrain(res, frame, { signal: options.signal });
|
|
420
429
|
}
|
|
421
430
|
prelude.push(frame);
|
|
@@ -502,6 +511,7 @@ async function proxyStreamingResponses(opened, res, upstream, options) {
|
|
|
502
511
|
await endStreamWithError(res, error, options.abort);
|
|
503
512
|
return { successful: false, compacted: false };
|
|
504
513
|
} finally {
|
|
514
|
+
keepalive.stop();
|
|
505
515
|
await reader.cancel().catch(() => {});
|
|
506
516
|
reader.releaseLock();
|
|
507
517
|
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
export const SSE_KEEPALIVE_INTERVAL_MS = 15_000;
|
|
2
|
+
|
|
3
|
+
// Downstream-only SSE comments. Never read upstream, alter its deadlines, or
|
|
4
|
+
// write before valid stream headers / between pieces of a partially written frame.
|
|
5
|
+
export function createSseKeepalive({
|
|
6
|
+
res, signal, canWrite = () => true, intervalMs = SSE_KEEPALIVE_INTERVAL_MS,
|
|
7
|
+
now = Date.now, setTimer = setTimeout, clearTimer = clearTimeout,
|
|
8
|
+
} = {}) {
|
|
9
|
+
let timer = null;
|
|
10
|
+
let stopped = false;
|
|
11
|
+
let lastWrite = now();
|
|
12
|
+
const stop = () => {
|
|
13
|
+
stopped = true;
|
|
14
|
+
if (timer !== null) clearTimer(timer);
|
|
15
|
+
timer = null;
|
|
16
|
+
signal?.removeEventListener("abort", stop);
|
|
17
|
+
};
|
|
18
|
+
const schedule = (delay) => {
|
|
19
|
+
timer = setTimer(tick, Math.max(1, delay));
|
|
20
|
+
timer?.unref?.();
|
|
21
|
+
};
|
|
22
|
+
const tick = () => {
|
|
23
|
+
timer = null;
|
|
24
|
+
if (stopped || signal?.aborted || res?.destroyed || res?.writableEnded) return stop();
|
|
25
|
+
const idle = Math.max(0, now() - lastWrite);
|
|
26
|
+
try {
|
|
27
|
+
if (idle >= intervalMs && res?.headersSent && !res.writableNeedDrain && canWrite()) {
|
|
28
|
+
// One small, complete comment is bounded even if this write reaches the
|
|
29
|
+
// high-water mark; no further comments are sent while backpressured.
|
|
30
|
+
res.write(": ccdx keepalive\n\n");
|
|
31
|
+
lastWrite = now();
|
|
32
|
+
}
|
|
33
|
+
} catch { return stop(); }
|
|
34
|
+
const elapsed = Math.max(0, now() - lastWrite);
|
|
35
|
+
schedule(elapsed >= intervalMs ? intervalMs : intervalMs - elapsed);
|
|
36
|
+
};
|
|
37
|
+
if (Number.isFinite(intervalMs) && intervalMs > 0 && !signal?.aborted) {
|
|
38
|
+
signal?.addEventListener("abort", stop, { once: true });
|
|
39
|
+
schedule(intervalMs);
|
|
40
|
+
}
|
|
41
|
+
return { activity: () => { lastWrite = now(); }, stop };
|
|
42
|
+
}
|
|
@@ -159,6 +159,7 @@ export function createStreamPerformanceMetrics({ now = () => performance.now(),
|
|
|
159
159
|
let headersAt = null;
|
|
160
160
|
let lastActivityAt = null;
|
|
161
161
|
let terminalAt = null;
|
|
162
|
+
const context = { payload_bytes: null, images: null, input_tokens: null, context_window_tokens: null };
|
|
162
163
|
const elapsed = (time) => time === null ? null : Number(Math.max(0, time - requestStartedAt).toFixed(1));
|
|
163
164
|
|
|
164
165
|
return {
|
|
@@ -205,6 +206,13 @@ export function createStreamPerformanceMetrics({ now = () => performance.now(),
|
|
|
205
206
|
setModel(value) {
|
|
206
207
|
model = modelLabel(value);
|
|
207
208
|
},
|
|
209
|
+
observeContext(values) {
|
|
210
|
+
if (finished || !values || typeof values !== "object") return;
|
|
211
|
+
for (const key of Object.keys(context)) {
|
|
212
|
+
const value = values[key];
|
|
213
|
+
if (Number.isSafeInteger(value) && value >= 0 && (key !== "context_window_tokens" || value > 0)) context[key] = value;
|
|
214
|
+
}
|
|
215
|
+
},
|
|
208
216
|
setErrorOrigin(value) {
|
|
209
217
|
if (TERMINAL_ORIGINS.has(value)) errorOrigin = value;
|
|
210
218
|
},
|
|
@@ -242,6 +250,7 @@ export function createStreamPerformanceMetrics({ now = () => performance.now(),
|
|
|
242
250
|
failed: failed || aborted || (terminalOutcome !== null && terminalOutcome !== "completed"),
|
|
243
251
|
http_status: Number.isInteger(statusCode) && statusCode >= 100 && statusCode <= 599 ? statusCode : null,
|
|
244
252
|
upstream_attempts: attempts,
|
|
253
|
+
context: { ...context },
|
|
245
254
|
timings_ms: {
|
|
246
255
|
...Object.fromEntries(PREPARATION_STAGES.map((stage) => [stage, Number(preparationMs[stage].toFixed(1))])),
|
|
247
256
|
upstream_start: elapsed(firstAttemptAt), upstream_headers: elapsed(headersAt),
|
|
@@ -272,7 +281,7 @@ export function createStreamPerformanceMetrics({ now = () => performance.now(),
|
|
|
272
281
|
snapshot() {
|
|
273
282
|
return {
|
|
274
283
|
by_route: Object.fromEntries(PERFORMANCE_ROUTES.map((name) => [name, routeSnapshot(routes[name])])),
|
|
275
|
-
recent_requests: recentRequests.map((entry) => ({ ...entry, timings_ms: { ...entry.timings_ms } })),
|
|
284
|
+
recent_requests: recentRequests.map((entry) => ({ ...entry, context: { ...entry.context }, timings_ms: { ...entry.timings_ms } })),
|
|
276
285
|
};
|
|
277
286
|
},
|
|
278
287
|
};
|
|
@@ -320,6 +329,10 @@ export function markOutputTokens(value) {
|
|
|
320
329
|
tracker()?.setOutputTokens(value);
|
|
321
330
|
}
|
|
322
331
|
|
|
332
|
+
export function markRequestObservation(values) {
|
|
333
|
+
tracker()?.observeContext?.(values);
|
|
334
|
+
}
|
|
335
|
+
|
|
323
336
|
export function markStreamFailure() {
|
|
324
337
|
tracker()?.fail();
|
|
325
338
|
}
|
package/src/usage.mjs
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
|
+
import { markRequestObservation } from "./stream-performance.mjs";
|
|
2
3
|
import os from "node:os";
|
|
3
4
|
import path from "node:path";
|
|
4
5
|
import readline from "node:readline";
|
|
@@ -180,6 +181,8 @@ export function recordUsage(record) {
|
|
|
180
181
|
}
|
|
181
182
|
|
|
182
183
|
export function recordResponsesUsage(args) {
|
|
184
|
+
const response = args.response || args.event?.response || args.event;
|
|
185
|
+
markRequestObservation({ input_tokens: response?.usage?.input_tokens });
|
|
183
186
|
return recordUsage(buildResponsesUsageRecord(args));
|
|
184
187
|
}
|
|
185
188
|
|
package/src/user-settings.mjs
CHANGED
|
@@ -2,7 +2,7 @@ import fs from "node:fs";
|
|
|
2
2
|
import os from "node:os";
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { atomicWriteFileIfChangedSync } from "./atomic-file.mjs";
|
|
5
|
-
import { DEFAULT_CODEX_AUTO_REVIEW_MODEL } from "./models.mjs";
|
|
5
|
+
import { AUTO_REVIEW_REASONING_EFFORTS, DEFAULT_CODEX_AUTO_REVIEW_MODEL } from "./models.mjs";
|
|
6
6
|
import { loadRuntimeConfig, RUNTIME_DEFAULTS } from "./runtime-config.mjs";
|
|
7
7
|
import {
|
|
8
8
|
MIB,
|
|
@@ -17,6 +17,7 @@ import {
|
|
|
17
17
|
} from "./terminal-animation.mjs";
|
|
18
18
|
|
|
19
19
|
const AUTO_REVIEW_MODEL_KEY = "auto_review_model";
|
|
20
|
+
const AUTO_REVIEW_REASONING_KEY = "auto_review_reasoning";
|
|
20
21
|
const TERMINAL_ANIMATION_KEY = "terminal_animation";
|
|
21
22
|
const RESPONSE_HISTORY_MAX_MIB_KEY = "response_history_max_mib";
|
|
22
23
|
const DECODED_BODY_LIMIT_MIB_KEY = "decoded_body_limit_mib";
|
|
@@ -47,6 +48,15 @@ export function readUserSettings({ env = process.env, home = os.homedir(), stric
|
|
|
47
48
|
throw invalidSettings(filePath, `${AUTO_REVIEW_MODEL_KEY} must be a non-empty string`);
|
|
48
49
|
}
|
|
49
50
|
}
|
|
51
|
+
if (Object.hasOwn(parsed, AUTO_REVIEW_REASONING_KEY)) {
|
|
52
|
+
const value = parsed[AUTO_REVIEW_REASONING_KEY];
|
|
53
|
+
if (!value || typeof value !== "object" || Array.isArray(value)
|
|
54
|
+
|| typeof value.model !== "string" || !value.model.trim()
|
|
55
|
+
|| !(value.effort === null || AUTO_REVIEW_REASONING_EFFORTS.includes(value.effort))) {
|
|
56
|
+
if (strict) throw invalidSettings(filePath, `${AUTO_REVIEW_REASONING_KEY} must contain a model and valid effort`);
|
|
57
|
+
delete parsed[AUTO_REVIEW_REASONING_KEY];
|
|
58
|
+
}
|
|
59
|
+
}
|
|
50
60
|
if (Object.hasOwn(parsed, TERMINAL_ANIMATION_KEY)) {
|
|
51
61
|
const value = parsed[TERMINAL_ANIMATION_KEY];
|
|
52
62
|
// Accept the retired value on read so upgrades can fall back and select a replacement.
|
|
@@ -97,12 +107,18 @@ export function savedAutoReviewModel(options = {}) {
|
|
|
97
107
|
}
|
|
98
108
|
|
|
99
109
|
export function autoReviewModelPreference({ env = process.env, home = os.homedir() } = {}) {
|
|
100
|
-
const
|
|
101
|
-
|
|
110
|
+
const { model, source } = autoReviewPreference({ env, home });
|
|
111
|
+
return { model, source };
|
|
112
|
+
}
|
|
102
113
|
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
114
|
+
export function autoReviewPreference({ env = process.env, home = os.homedir() } = {}) {
|
|
115
|
+
const settings = readUserSettings({ env, home });
|
|
116
|
+
const environmentModel = String(env.CCDX_AUTO_REVIEW_MODEL || "").trim();
|
|
117
|
+
const savedModel = String(settings[AUTO_REVIEW_MODEL_KEY] || "").trim();
|
|
118
|
+
const model = environmentModel || savedModel || DEFAULT_CODEX_AUTO_REVIEW_MODEL;
|
|
119
|
+
const source = environmentModel ? "environment" : savedModel ? "settings" : "default";
|
|
120
|
+
const reasoning = settings[AUTO_REVIEW_REASONING_KEY];
|
|
121
|
+
return { model, source, reasoningEffort: reasoning?.model === model ? reasoning.effort : undefined };
|
|
106
122
|
}
|
|
107
123
|
|
|
108
124
|
export function savedTerminalAnimationTheme(options = {}) {
|
|
@@ -183,13 +199,22 @@ export function writeResponseHistoryLimitMib(mib, { env = process.env, home = os
|
|
|
183
199
|
return { changed, filePath, ...responseHistoryLimitPreference({ env, home }) };
|
|
184
200
|
}
|
|
185
201
|
|
|
186
|
-
export function writeAutoReviewModel(model, { env = process.env, home = os.homedir() } = {}) {
|
|
202
|
+
export function writeAutoReviewModel(model, { env = process.env, home = os.homedir(), reasoningEffort } = {}) {
|
|
203
|
+
if (reasoningEffort !== undefined && reasoningEffort !== null
|
|
204
|
+
&& !AUTO_REVIEW_REASONING_EFFORTS.includes(reasoningEffort)) {
|
|
205
|
+
throw new Error("Auto-review reasoning effort is invalid");
|
|
206
|
+
}
|
|
187
207
|
const filePath = userSettingsPath({ env, home });
|
|
188
208
|
const settings = readUserSettings({ env, home, strict: true });
|
|
189
209
|
const value = String(model || "").trim();
|
|
190
210
|
const next = { ...settings };
|
|
191
211
|
if (value) next[AUTO_REVIEW_MODEL_KEY] = value;
|
|
192
212
|
else delete next[AUTO_REVIEW_MODEL_KEY];
|
|
213
|
+
const selectedModel = value || DEFAULT_CODEX_AUTO_REVIEW_MODEL;
|
|
214
|
+
if (reasoningEffort !== undefined) next[AUTO_REVIEW_REASONING_KEY] = { model: selectedModel, effort: reasoningEffort };
|
|
215
|
+
else if (next[AUTO_REVIEW_REASONING_KEY]?.model !== selectedModel) {
|
|
216
|
+
delete next[AUTO_REVIEW_REASONING_KEY];
|
|
217
|
+
}
|
|
193
218
|
|
|
194
219
|
const changed = atomicWriteFileIfChangedSync(filePath, `${JSON.stringify(next, null, 2)}\n`, { mode: 0o600 });
|
|
195
220
|
return { changed, filePath, model: value || DEFAULT_CODEX_AUTO_REVIEW_MODEL };
|