codex-copilot-dx 0.4.34 → 0.4.36

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.
@@ -1,21 +1,20 @@
1
1
  import { promisify } from "node:util";
2
2
  import * as zlib from "node:zlib";
3
+ import { loadRuntimeConfig, parsePositiveInteger } from "./runtime-config.mjs";
3
4
  import { status } from "./status.mjs";
4
5
 
5
- const DEFAULT_MAX_BODY_BYTES = 64 * 1024 * 1024;
6
- const DEFAULT_MAX_DECODED_BODY_BYTES = 128 * 1024 * 1024;
6
+ const COMPRESSED_BODY_WEIGHT_MULTIPLIER = 4;
7
7
  const gunzipAsync = promisify(zlib.gunzip);
8
8
  const inflateAsync = promisify(zlib.inflate);
9
9
  const brotliDecompressAsync = promisify(zlib.brotliDecompress);
10
10
  const zstdDecompressAsync = zlib.zstdDecompress ? promisify(zlib.zstdDecompress) : null;
11
11
 
12
- function positiveInt(value, fallback) {
13
- const n = Number.parseInt(value, 10);
14
- return Number.isFinite(n) && n > 0 ? n : fallback;
15
- }
16
-
17
- const MAX_BODY_BYTES = positiveInt(process.env.CCDX_MAX_BODY_BYTES, DEFAULT_MAX_BODY_BYTES);
18
- const MAX_DECODED_BODY_BYTES = positiveInt(process.env.CCDX_MAX_DECODED_BODY_BYTES, DEFAULT_MAX_DECODED_BODY_BYTES);
12
+ const HTTP_RUNTIME_CONFIG = loadRuntimeConfig();
13
+ const MAX_BODY_BYTES = HTTP_RUNTIME_CONFIG.maxBodyBytes;
14
+ const MAX_DECODED_BODY_BYTES = HTTP_RUNTIME_CONFIG.maxDecodedBodyBytes;
15
+ const MAX_INFLIGHT_BODY_BYTES = HTTP_RUNTIME_CONFIG.maxInflightBodyBytes;
16
+ const MAX_QUEUED_REQUESTS = HTTP_RUNTIME_CONFIG.maxQueuedRequests;
17
+ const REQUEST_QUEUE_TIMEOUT_MS = HTTP_RUNTIME_CONFIG.requestQueueTimeoutMs;
19
18
 
20
19
  export function httpError(message, statusCode) {
21
20
  const err = new Error(message);
@@ -31,6 +30,152 @@ function requestContentLength(req) {
31
30
  return Number.parseInt(req.headers?.["content-length"] || "", 10);
32
31
  }
33
32
 
33
+ function requestAdmissionWeight(req, maxBytes) {
34
+ const encodings = contentEncodings(req.headers?.["content-encoding"]);
35
+ const contentLength = requestContentLength(req);
36
+ if (!Number.isFinite(contentLength) || contentLength < 0) return maxBytes;
37
+ const compressed = encodings.some((encoding) => encoding !== "identity");
38
+ const weightedLength = compressed ? contentLength * COMPRESSED_BODY_WEIGHT_MULTIPLIER : contentLength;
39
+ return Math.max(1, Math.min(weightedLength, maxBytes));
40
+ }
41
+
42
+ function admissionAbortError(signal) {
43
+ if (signal?.reason instanceof Error) return signal.reason;
44
+ const error = new Error("The operation was aborted");
45
+ error.name = "AbortError";
46
+ return error;
47
+ }
48
+
49
+ export function createRequestAdmission({
50
+ maxBytes = MAX_INFLIGHT_BODY_BYTES,
51
+ maxQueued = MAX_QUEUED_REQUESTS,
52
+ waitTimeoutMs = REQUEST_QUEUE_TIMEOUT_MS,
53
+ } = {}) {
54
+ const byteLimit = parsePositiveInteger(maxBytes, MAX_INFLIGHT_BODY_BYTES);
55
+ const queueLimit = parsePositiveInteger(maxQueued, MAX_QUEUED_REQUESTS);
56
+ const timeoutMs = parsePositiveInteger(waitTimeoutMs, REQUEST_QUEUE_TIMEOUT_MS);
57
+ const queue = [];
58
+ let activeBytes = 0;
59
+ let activeRequests = 0;
60
+ const counters = {
61
+ total: 0,
62
+ activated: 0,
63
+ queued: 0,
64
+ rejected: 0,
65
+ timedOut: 0,
66
+ aborted: 0,
67
+ waitMsTotal: 0,
68
+ waitMsMax: 0,
69
+ };
70
+
71
+ const remove = (entry) => {
72
+ const index = queue.indexOf(entry);
73
+ if (index >= 0) queue.splice(index, 1);
74
+ };
75
+
76
+ const activate = (entry) => {
77
+ entry.cleanup();
78
+ activeBytes += entry.weight;
79
+ activeRequests += 1;
80
+ counters.activated += 1;
81
+ const waitMs = Math.max(0, Date.now() - entry.startedAt);
82
+ counters.waitMsTotal += waitMs;
83
+ counters.waitMsMax = Math.max(counters.waitMsMax, waitMs);
84
+ let released = false;
85
+ entry.resolve(() => {
86
+ if (released) return;
87
+ released = true;
88
+ activeBytes = Math.max(0, activeBytes - entry.weight);
89
+ activeRequests = Math.max(0, activeRequests - 1);
90
+ drain();
91
+ });
92
+ };
93
+
94
+ const drain = () => {
95
+ for (let index = 0; index < queue.length;) {
96
+ const entry = queue[index];
97
+ if (entry.cancelled) {
98
+ queue.splice(index, 1);
99
+ continue;
100
+ }
101
+ if (activeBytes + entry.weight > byteLimit) {
102
+ index += 1;
103
+ continue;
104
+ }
105
+ queue.splice(index, 1);
106
+ activate(entry);
107
+ }
108
+ };
109
+
110
+ const acquire = (req, { signal } = {}) => {
111
+ counters.total += 1;
112
+ if (signal?.aborted) {
113
+ counters.aborted += 1;
114
+ return Promise.reject(admissionAbortError(signal));
115
+ }
116
+ const weight = requestAdmissionWeight(req, byteLimit);
117
+ if (queue.length >= queueLimit && activeBytes + weight > byteLimit) {
118
+ counters.rejected += 1;
119
+ return Promise.reject(httpError(`Request queue is full (${queueLimit} waiting)`, 503));
120
+ }
121
+ if (activeBytes + weight > byteLimit) counters.queued += 1;
122
+
123
+ return new Promise((resolve, reject) => {
124
+ let timer;
125
+ const entry = {
126
+ cancelled: false,
127
+ startedAt: Date.now(),
128
+ weight,
129
+ resolve,
130
+ cleanup: () => {
131
+ if (timer) clearTimeout(timer);
132
+ signal?.removeEventListener("abort", onAbort);
133
+ },
134
+ };
135
+ const cancel = (error, reason) => {
136
+ if (entry.cancelled) return;
137
+ entry.cancelled = true;
138
+ if (reason === "aborted") counters.aborted += 1;
139
+ if (reason === "timed_out") counters.timedOut += 1;
140
+ entry.cleanup();
141
+ remove(entry);
142
+ reject(error);
143
+ drain();
144
+ };
145
+ const onAbort = () => cancel(admissionAbortError(signal), "aborted");
146
+ signal?.addEventListener("abort", onAbort, { once: true });
147
+ timer = setTimeout(() => {
148
+ cancel(httpError(`Request admission timed out after ${timeoutMs}ms`, 503), "timed_out");
149
+ }, timeoutMs);
150
+ timer.unref?.();
151
+ queue.push(entry);
152
+ if (signal?.aborted) onAbort();
153
+ else drain();
154
+ });
155
+ };
156
+
157
+ acquire.stats = () => ({ activeBytes, queued: queue.length, maxBytes: byteLimit });
158
+ acquire.diagnostics = () => ({
159
+ activeBytes,
160
+ activeRequests,
161
+ queued: queue.length,
162
+ maxBytes: byteLimit,
163
+ maxQueued: queueLimit,
164
+ waitTimeoutMs: timeoutMs,
165
+ total: counters.total,
166
+ activated: counters.activated,
167
+ queuedTotal: counters.queued,
168
+ rejected: counters.rejected,
169
+ timedOut: counters.timedOut,
170
+ aborted: counters.aborted,
171
+ waitMsAvg: counters.activated > 0
172
+ ? Number((counters.waitMsTotal / counters.activated).toFixed(1))
173
+ : 0,
174
+ waitMsMax: counters.waitMsMax,
175
+ });
176
+ return acquire;
177
+ }
178
+
34
179
  async function readRequestBuffer(req, maxBytes) {
35
180
  const contentLength = requestContentLength(req);
36
181
  if (Number.isFinite(contentLength) && contentLength > maxBytes) {
@@ -61,7 +61,7 @@ export function createTaskLimiter(concurrency) {
61
61
  }
62
62
  };
63
63
 
64
- return async function runLimited(task, { signal } = {}) {
64
+ const runLimited = async function runLimited(task, { signal } = {}) {
65
65
  if (signal?.aborted) throw abortError(signal);
66
66
  await new Promise((resolve, reject) => {
67
67
  const entry = {
@@ -91,10 +91,25 @@ export function createTaskLimiter(concurrency) {
91
91
  drain();
92
92
  }
93
93
  };
94
+ runLimited.stats = () => ({
95
+ active,
96
+ queued: queue.reduce((count, entry) => count + (entry.cancelled ? 0 : 1), 0),
97
+ limit,
98
+ });
99
+ return runLimited;
94
100
  }
95
101
 
96
102
  const runGlobalImageTask = createTaskLimiter(IMG_CONCURRENCY);
97
103
 
104
+ export function imageOptimizationStats() {
105
+ return {
106
+ ...runGlobalImageTask.stats(),
107
+ disabled: IMG_OPT_DISABLED,
108
+ cache_entries: optimizedImageDigests.size,
109
+ sharp_loaded: sharpImport !== null,
110
+ };
111
+ }
112
+
98
113
  async function sharp() {
99
114
  sharpImport ||= import("sharp");
100
115
  const mod = await sharpImport;
@@ -130,13 +145,16 @@ export async function optimizeImageDataUrl(dataUrl, {
130
145
  const match = /^data:(image\/[a-z+.-]+);base64,(.+)$/i.exec(dataUrl);
131
146
  if (!match) return dataUrl;
132
147
  const mime = match[1].toLowerCase();
133
- const raw = Buffer.from(match[2], "base64");
134
- if (raw.length < IMG_MIN_BYTES || mime.includes("gif")) return dataUrl;
135
- const digest = imageDigest(raw);
136
- if (!force && touchOptimizedImage(digest)) return dataUrl;
148
+ const encoded = match[2];
149
+ const inputBytes = Buffer.byteLength(encoded, "base64");
150
+ if (inputBytes < IMG_MIN_BYTES || mime.includes("gif")) return dataUrl;
137
151
 
138
152
  try {
139
153
  return await runGlobalImageTask(async () => {
154
+ const raw = Buffer.from(encoded, "base64");
155
+ if (raw.length < IMG_MIN_BYTES) return dataUrl;
156
+ const digest = imageDigest(raw);
157
+ if (!force && touchOptimizedImage(digest)) return dataUrl;
140
158
  const resize = await sharp();
141
159
  const image = resize(raw, { failOn: "none", limitInputPixels: IMG_MAX_INPUT_PIXELS });
142
160
  const out = await image
@@ -156,7 +174,7 @@ export async function optimizeImageDataUrl(dataUrl, {
156
174
  }, { signal });
157
175
  } catch (e) {
158
176
  if (signal?.aborted || e?.name === "AbortError") throw e;
159
- console.warn(status("warn", `image optimize failed (${mime}, ${raw.length}b): ${e.message}`));
177
+ console.warn(status("warn", `image optimize failed (${mime}, ${inputBytes}b): ${e.message}`));
160
178
  return dataUrl;
161
179
  }
162
180
  }
package/src/models.mjs CHANGED
@@ -225,6 +225,19 @@ export function resolveOpenAIModel(model, env = process.env) {
225
225
  };
226
226
  }
227
227
 
228
+ export function codexAutoReviewModelStatus(models, env = process.env) {
229
+ const { upstreamModel } = resolveOpenAIModel(CODEX_AUTO_REVIEW_MODEL, env);
230
+ const model = copilotModelData(models).find((entry) => String(entry?.id || "").trim() === upstreamModel);
231
+ if (!model) return { available: false, upstreamModel, reason: "model is not advertised" };
232
+ const endpoints = modelEndpoints(model);
233
+ const available = endpoints.includes("/responses") || endpoints.includes("/v1/responses");
234
+ return {
235
+ available,
236
+ upstreamModel,
237
+ reason: available ? "" : "model does not advertise a Responses endpoint",
238
+ };
239
+ }
240
+
228
241
  export function anthropicModelInfo(model) {
229
242
  return {
230
243
  id: model.id,
@@ -0,0 +1,147 @@
1
+ import { isIP } from "node:net";
2
+ import { adapterHealthPayload } from "./running-adapter.mjs";
3
+ import { copilotRuntimeStatus } from "./copilot.mjs";
4
+ import { imageOptimizationStats } from "./image-optimization.mjs";
5
+ import { responseHistoryStats } from "./response-history.mjs";
6
+ import { loadRuntimeConfig } from "./runtime-config.mjs";
7
+
8
+ export const ADAPTER_STATUS_PATH = "/_ccdx/status";
9
+ const OBSERVABILITY_RUNTIME_CONFIG = loadRuntimeConfig();
10
+
11
+ const ROUTE_NAMES = Object.freeze([
12
+ "responses",
13
+ "responses_compact",
14
+ "models",
15
+ "messages",
16
+ "messages_count_tokens",
17
+ "not_found",
18
+ ]);
19
+
20
+ function emptyCounter() {
21
+ return {
22
+ total: 0,
23
+ active: 0,
24
+ completed: 0,
25
+ errors: 0,
26
+ aborted: 0,
27
+ duration_ms_total: 0,
28
+ duration_ms_max: 0,
29
+ status_2xx: 0,
30
+ status_3xx: 0,
31
+ status_4xx: 0,
32
+ status_5xx: 0,
33
+ };
34
+ }
35
+
36
+ function statusBucket(statusCode) {
37
+ if (statusCode >= 200 && statusCode < 300) return "status_2xx";
38
+ if (statusCode >= 300 && statusCode < 400) return "status_3xx";
39
+ if (statusCode >= 400 && statusCode < 500) return "status_4xx";
40
+ if (statusCode >= 500) return "status_5xx";
41
+ return null;
42
+ }
43
+
44
+ function counterSnapshot(counter) {
45
+ return {
46
+ ...counter,
47
+ duration_ms_avg: counter.completed > 0
48
+ ? Number((counter.duration_ms_total / counter.completed).toFixed(1))
49
+ : 0,
50
+ };
51
+ }
52
+
53
+ export function createRequestMetrics({ now = Date.now } = {}) {
54
+ const total = emptyCounter();
55
+ const routes = Object.fromEntries(ROUTE_NAMES.map((name) => [name, emptyCounter()]));
56
+
57
+ const begin = (routeName) => {
58
+ const route = routes[routeName] || routes.not_found;
59
+ const startedAt = now();
60
+ total.total += 1;
61
+ total.active += 1;
62
+ route.total += 1;
63
+ route.active += 1;
64
+ let completed = false;
65
+
66
+ return ({ statusCode = 0, aborted = false } = {}) => {
67
+ if (completed) return;
68
+ completed = true;
69
+ const durationMs = Math.max(0, now() - startedAt);
70
+ for (const counter of [total, route]) {
71
+ counter.active = Math.max(0, counter.active - 1);
72
+ counter.completed += 1;
73
+ counter.duration_ms_total += durationMs;
74
+ counter.duration_ms_max = Math.max(counter.duration_ms_max, durationMs);
75
+ if (aborted) counter.aborted += 1;
76
+ if (statusCode >= 400) counter.errors += 1;
77
+ const bucket = statusBucket(statusCode);
78
+ if (bucket) counter[bucket] += 1;
79
+ }
80
+ };
81
+ };
82
+
83
+ return {
84
+ begin,
85
+ snapshot() {
86
+ return {
87
+ ...counterSnapshot(total),
88
+ by_route: Object.fromEntries(ROUTE_NAMES.map((name) => [name, counterSnapshot(routes[name])])),
89
+ };
90
+ },
91
+ };
92
+ }
93
+
94
+ export function classifyAdapterRoute(method, pathname) {
95
+ if (method === "POST" && pathname === "/v1/responses") return "responses";
96
+ if (method === "POST" && pathname === "/v1/responses/compact") return "responses_compact";
97
+ if (method === "GET" && pathname === "/v1/models") return "models";
98
+ if (method === "POST" && pathname === "/v1/messages") return "messages";
99
+ if (method === "POST" && pathname === "/v1/messages/count_tokens") return "messages_count_tokens";
100
+ return "not_found";
101
+ }
102
+
103
+ export function isLoopbackAddress(address) {
104
+ const normalized = String(address || "").trim().toLowerCase();
105
+ if (normalized === "::1" || normalized === "[::1]") return true;
106
+ const ipv4 = normalized.startsWith("::ffff:") ? normalized.slice(7) : normalized;
107
+ return isIP(ipv4) === 4 && ipv4.startsWith("127.");
108
+ }
109
+
110
+ function modelRegistryStatus(modelRegistry) {
111
+ const modelData = Array.isArray(modelRegistry?.models)
112
+ ? modelRegistry.models
113
+ : modelRegistry?.models?.data;
114
+ return {
115
+ source: String(modelRegistry?.source || "built-in"),
116
+ models: Array.isArray(modelData) ? modelData.length : 0,
117
+ claude_models: Array.isArray(modelRegistry?.modelDefs) ? modelRegistry.modelDefs.length : 0,
118
+ };
119
+ }
120
+
121
+ export function runtimeStatusPayload({ metrics, admission, modelRegistry } = {}) {
122
+ const memory = process.memoryUsage();
123
+ return {
124
+ ...adapterHealthPayload(),
125
+ uptime_ms: Math.round(process.uptime() * 1000),
126
+ process: {
127
+ rss_bytes: memory.rss,
128
+ heap_used_bytes: memory.heapUsed,
129
+ heap_total_bytes: memory.heapTotal,
130
+ external_bytes: memory.external,
131
+ array_buffers_bytes: memory.arrayBuffers,
132
+ },
133
+ requests: metrics?.snapshot?.() || createRequestMetrics().snapshot(),
134
+ admission: admission?.diagnostics?.() || admission?.stats?.() || null,
135
+ response_history: responseHistoryStats(),
136
+ image_optimization: imageOptimizationStats(),
137
+ copilot: copilotRuntimeStatus(),
138
+ models: modelRegistryStatus(modelRegistry),
139
+ limits: {
140
+ max_body_bytes: OBSERVABILITY_RUNTIME_CONFIG.maxBodyBytes,
141
+ max_decoded_body_bytes: OBSERVABILITY_RUNTIME_CONFIG.maxDecodedBodyBytes,
142
+ max_sse_buffer_bytes: OBSERVABILITY_RUNTIME_CONFIG.maxSseBufferBytes,
143
+ response_history_max_bytes: OBSERVABILITY_RUNTIME_CONFIG.responseHistoryMaxBytes,
144
+ response_history_max_entries: OBSERVABILITY_RUNTIME_CONFIG.responseHistoryMaxEntries,
145
+ },
146
+ };
147
+ }
@@ -0,0 +1,16 @@
1
+ import { AsyncLocalStorage } from "node:async_hooks";
2
+ import { randomUUID } from "node:crypto";
3
+
4
+ const requestContextStorage = new AsyncLocalStorage();
5
+
6
+ export function createRequestId() {
7
+ return randomUUID();
8
+ }
9
+
10
+ export function runWithRequestContext(context, callback) {
11
+ return requestContextStorage.run(context, callback);
12
+ }
13
+
14
+ export function currentRequestContext() {
15
+ return requestContextStorage.getStore() || null;
16
+ }
@@ -1,12 +1,8 @@
1
1
  import { httpError } from "./http-transport.mjs";
2
+ import { loadRuntimeConfig, parsePositiveInteger, RUNTIME_DEFAULTS } from "./runtime-config.mjs";
2
3
 
3
- const DEFAULT_MAX_BYTES = 64 * 1024 * 1024;
4
- const DEFAULT_MAX_ENTRIES = 4096;
5
-
6
- function positiveInt(value, fallback) {
7
- const n = Number.parseInt(value, 10);
8
- return Number.isFinite(n) && n > 0 ? n : fallback;
9
- }
4
+ const DEFAULT_MAX_BYTES = RUNTIME_DEFAULTS.responseHistoryMaxBytes;
5
+ const DEFAULT_MAX_ENTRIES = RUNTIME_DEFAULTS.responseHistoryMaxEntries;
10
6
 
11
7
  function cloneJson(value) {
12
8
  return value === undefined ? undefined : structuredClone(value);
@@ -76,8 +72,9 @@ const childrenById = new Map();
76
72
  const treeLru = new Map();
77
73
  const evictedIds = new Set();
78
74
  let totalBytes = 0;
79
- let maxBytes = positiveInt(process.env.CCDX_RESPONSE_HISTORY_MAX_BYTES, DEFAULT_MAX_BYTES);
80
- let maxEntries = positiveInt(process.env.CCDX_RESPONSE_HISTORY_MAX_ENTRIES, DEFAULT_MAX_ENTRIES);
75
+ const HISTORY_RUNTIME_CONFIG = loadRuntimeConfig();
76
+ let maxBytes = HISTORY_RUNTIME_CONFIG.responseHistoryMaxBytes;
77
+ let maxEntries = HISTORY_RUNTIME_CONFIG.responseHistoryMaxEntries;
81
78
 
82
79
  function rememberEvictedId(id) {
83
80
  evictedIds.add(id);
@@ -169,8 +166,8 @@ export function clearResponseHistoryForTests() {
169
166
  }
170
167
 
171
168
  export function configureResponseHistoryForTests({ maxBytes: nextMaxBytes, maxEntries: nextMaxEntries } = {}) {
172
- if (nextMaxBytes !== undefined) maxBytes = positiveInt(nextMaxBytes, DEFAULT_MAX_BYTES);
173
- if (nextMaxEntries !== undefined) maxEntries = positiveInt(nextMaxEntries, DEFAULT_MAX_ENTRIES);
169
+ if (nextMaxBytes !== undefined) maxBytes = parsePositiveInteger(nextMaxBytes, DEFAULT_MAX_BYTES);
170
+ if (nextMaxEntries !== undefined) maxEntries = parsePositiveInteger(nextMaxEntries, DEFAULT_MAX_ENTRIES);
174
171
  }
175
172
 
176
173
  export function responseHistoryStats() {