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.
@@ -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 options = model === CODEX_AUTO_REVIEW_MODEL && typeof autoReviewModelResolver === "function"
94
- ? { autoReviewModel: autoReviewModelResolver() }
95
- : {};
96
- return resolveOpenAIModel(model, openAIModelEnv, options);
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 successful = await forwardToChat(chatReq, async (event, data) => {
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({
@@ -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
 
@@ -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 environmentModel = String(env.CCDX_AUTO_REVIEW_MODEL || "").trim();
101
- if (environmentModel) return { model: environmentModel, source: "environment" };
110
+ const { model, source } = autoReviewPreference({ env, home });
111
+ return { model, source };
112
+ }
102
113
 
103
- const savedModel = savedAutoReviewModel({ env, home });
104
- if (savedModel) return { model: savedModel, source: "settings" };
105
- return { model: DEFAULT_CODEX_AUTO_REVIEW_MODEL, source: "default" };
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 };