codex-copilot-dx 0.6.15 → 0.6.19

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.
@@ -7,6 +7,8 @@ export const RUNTIME_DEFAULTS = Object.freeze({
7
7
  maxBodyBytes: 64 * 1024 * 1024,
8
8
  maxDecodedBodyBytes: 128 * 1024 * 1024,
9
9
  maxInflightBodyBytes: 32 * 1024 * 1024,
10
+ maxUpstreamChatResponseBytes: 64 * 1024 * 1024,
11
+ maxUpstreamResponsesResponseBytes: 256 * 1024 * 1024,
10
12
  maxQueuedRequests: 16,
11
13
  requestQueueTimeoutMs: 120 * 1000,
12
14
  responseHistoryMaxBytes: 64 * 1024 * 1024,
@@ -30,6 +32,14 @@ export function loadRuntimeConfig(env = process.env) {
30
32
  maxBodyBytes: parsePositiveInteger(env.CCDX_MAX_BODY_BYTES, RUNTIME_DEFAULTS.maxBodyBytes),
31
33
  maxDecodedBodyBytes: parsePositiveInteger(env.CCDX_MAX_DECODED_BODY_BYTES, RUNTIME_DEFAULTS.maxDecodedBodyBytes),
32
34
  maxInflightBodyBytes: parsePositiveInteger(env.CCDX_MAX_INFLIGHT_BODY_BYTES, RUNTIME_DEFAULTS.maxInflightBodyBytes),
35
+ maxUpstreamChatResponseBytes: parsePositiveInteger(
36
+ env.CCDX_MAX_UPSTREAM_CHAT_RESPONSE_BYTES,
37
+ RUNTIME_DEFAULTS.maxUpstreamChatResponseBytes,
38
+ ),
39
+ maxUpstreamResponsesResponseBytes: parsePositiveInteger(
40
+ env.CCDX_MAX_UPSTREAM_RESPONSES_RESPONSE_BYTES,
41
+ RUNTIME_DEFAULTS.maxUpstreamResponsesResponseBytes,
42
+ ),
33
43
  maxQueuedRequests: parsePositiveInteger(env.CCDX_MAX_QUEUED_REQUESTS, RUNTIME_DEFAULTS.maxQueuedRequests),
34
44
  requestQueueTimeoutMs: parsePositiveInteger(env.CCDX_REQUEST_QUEUE_TIMEOUT_MS, RUNTIME_DEFAULTS.requestQueueTimeoutMs),
35
45
  responseHistoryMaxBytes: parsePositiveInteger(env.CCDX_RESPONSE_HISTORY_MAX_BYTES, RUNTIME_DEFAULTS.responseHistoryMaxBytes),
@@ -0,0 +1,188 @@
1
+ import { Worker } from "node:worker_threads";
2
+ import { anthropicTokenText } from "./anthropic.mjs";
3
+
4
+ export const DEFAULT_TOKEN_COUNTER_TIMEOUT_MS = 120 * 1000;
5
+ export const DEFAULT_TOKEN_COUNTER_MAX_QUEUED = 16;
6
+
7
+ function serviceError(message, statusCode) {
8
+ return Object.assign(new Error(message), { statusCode });
9
+ }
10
+
11
+ function positiveInteger(value, fallback) {
12
+ return Number.isFinite(value) && value > 0 ? Math.floor(value) : fallback;
13
+ }
14
+
15
+ function abortError(signal) {
16
+ if (signal?.reason instanceof Error) return signal.reason;
17
+ const error = new Error("The operation was aborted");
18
+ error.name = "AbortError";
19
+ return error;
20
+ }
21
+
22
+ export function tokenCounterWorkerExecArgv(execArgv = process.execArgv) {
23
+ const filtered = [];
24
+ for (let index = 0; index < execArgv.length; index += 1) {
25
+ const value = execArgv[index];
26
+ if (value === "--input-type") {
27
+ index += 1;
28
+ continue;
29
+ }
30
+ if (value.startsWith("--input-type=")) continue;
31
+ filtered.push(value);
32
+ }
33
+ return filtered;
34
+ }
35
+
36
+ function createTokenCounterWorker() {
37
+ const execArgv = tokenCounterWorkerExecArgv();
38
+ const options = execArgv.length === process.execArgv.length ? undefined : { execArgv };
39
+ return new Worker(new URL("./token-counter-worker.mjs", import.meta.url), options);
40
+ }
41
+
42
+ export function createTokenCounterService({
43
+ timeoutMs = DEFAULT_TOKEN_COUNTER_TIMEOUT_MS,
44
+ maxQueued = DEFAULT_TOKEN_COUNTER_MAX_QUEUED,
45
+ workerFactory = createTokenCounterWorker,
46
+ } = {}) {
47
+ timeoutMs = positiveInteger(timeoutMs, DEFAULT_TOKEN_COUNTER_TIMEOUT_MS);
48
+ maxQueued = positiveInteger(maxQueued, DEFAULT_TOKEN_COUNTER_MAX_QUEUED);
49
+ const queue = [];
50
+ let worker = null;
51
+ let active = null;
52
+ let nextId = 1;
53
+ let closed = false;
54
+
55
+ const finish = (job, callback, value) => {
56
+ if (job.finished) return;
57
+ job.finished = true;
58
+ if (job.timer) clearTimeout(job.timer);
59
+ job.signal?.removeEventListener("abort", job.onAbort);
60
+ callback(value);
61
+ };
62
+
63
+ const retireWorker = () => {
64
+ const retired = worker;
65
+ worker = null;
66
+ if (!retired) return;
67
+ retired.removeAllListeners();
68
+ retired.on("error", () => {});
69
+ Promise.resolve(retired.terminate()).catch(() => {});
70
+ };
71
+
72
+ const pump = () => {
73
+ if (closed || active || queue.length === 0) return;
74
+ if (!worker) {
75
+ try {
76
+ worker = workerFactory();
77
+ } catch {
78
+ const job = queue.shift();
79
+ if (job) finish(job, job.reject, serviceError("Token counter worker failed", 500));
80
+ queueMicrotask(pump);
81
+ return;
82
+ }
83
+ worker.on("message", (message) => {
84
+ if (!active || message?.id !== active.id) return;
85
+ const job = active;
86
+ active = null;
87
+ if (message.error) {
88
+ retireWorker();
89
+ finish(job, job.reject, serviceError("Token counter worker failed", 500));
90
+ } else {
91
+ finish(job, job.resolve, message.result);
92
+ }
93
+ pump();
94
+ });
95
+ worker.on("error", () => {
96
+ const job = active;
97
+ active = null;
98
+ retireWorker();
99
+ if (job) finish(job, job.reject, serviceError("Token counter worker failed", 500));
100
+ pump();
101
+ });
102
+ worker.on("exit", () => {
103
+ const job = active;
104
+ active = null;
105
+ worker = null;
106
+ if (job) finish(job, job.reject, serviceError("Token counter worker failed", 500));
107
+ pump();
108
+ });
109
+ worker.unref?.();
110
+ }
111
+ const job = queue.shift();
112
+ if (!job || job.finished) return pump();
113
+ active = job;
114
+ try {
115
+ worker.postMessage({ id: job.id, text: job.text });
116
+ job.text = null;
117
+ } catch {
118
+ active = null;
119
+ retireWorker();
120
+ finish(job, job.reject, serviceError("Token counter worker failed", 500));
121
+ pump();
122
+ }
123
+ };
124
+
125
+ const cancel = (job, error) => {
126
+ if (job.finished) return;
127
+ const queuedIndex = queue.indexOf(job);
128
+ if (queuedIndex >= 0) queue.splice(queuedIndex, 1);
129
+ if (active === job) {
130
+ active = null;
131
+ retireWorker();
132
+ }
133
+ finish(job, job.reject, error);
134
+ pump();
135
+ };
136
+
137
+ return {
138
+ count(body, { signal, deadlineMs = timeoutMs } = {}) {
139
+ if (closed) return Promise.reject(serviceError("Token counter service is closed", 503));
140
+ if (signal?.aborted) return Promise.reject(abortError(signal));
141
+ let text;
142
+ try {
143
+ text = anthropicTokenText(body);
144
+ } catch {
145
+ return Promise.reject(serviceError("Invalid count_tokens request", 400));
146
+ }
147
+ if (active && queue.length >= maxQueued) {
148
+ return Promise.reject(serviceError(`Token counter queue is full (${maxQueued} waiting)`, 503));
149
+ }
150
+ return new Promise((resolve, reject) => {
151
+ deadlineMs = positiveInteger(deadlineMs, timeoutMs);
152
+ const job = {
153
+ id: nextId++,
154
+ text,
155
+ signal,
156
+ resolve,
157
+ reject,
158
+ finished: false,
159
+ timer: null,
160
+ onAbort: null,
161
+ };
162
+ job.onAbort = () => cancel(job, abortError(signal));
163
+ signal?.addEventListener("abort", job.onAbort, { once: true });
164
+ job.timer = setTimeout(() => {
165
+ cancel(job, serviceError(`Token counting timed out after ${deadlineMs}ms`, 504));
166
+ }, deadlineMs);
167
+ queue.push(job);
168
+ if (signal?.aborted) job.onAbort();
169
+ else pump();
170
+ });
171
+ },
172
+ close() {
173
+ if (closed) return;
174
+ closed = true;
175
+ const error = serviceError("Token counter service is closed", 503);
176
+ if (active) {
177
+ const job = active;
178
+ active = null;
179
+ finish(job, job.reject, error);
180
+ }
181
+ for (const job of queue.splice(0)) finish(job, job.reject, error);
182
+ retireWorker();
183
+ },
184
+ stats() {
185
+ return { active: active ? 1 : 0, queued: queue.length, worker: worker ? 1 : 0 };
186
+ },
187
+ };
188
+ }
@@ -0,0 +1,10 @@
1
+ import { parentPort } from "node:worker_threads";
2
+ import { countTokenText } from "./anthropic.mjs";
3
+
4
+ parentPort.on("message", async ({ id, text }) => {
5
+ try {
6
+ parentPort.postMessage({ id, result: await countTokenText(text) });
7
+ } catch {
8
+ parentPort.postMessage({ id, error: true });
9
+ }
10
+ });
package/src/usage.mjs CHANGED
@@ -150,7 +150,7 @@ export async function flushUsageWrites() {
150
150
  await writeQueue;
151
151
  }
152
152
 
153
- async function* iterateUsageRecords(filePath) {
153
+ async function* iterateUsageRecords(filePath, { warn = console.error } = {}) {
154
154
  let input;
155
155
  try {
156
156
  input = fs.createReadStream(filePath, { encoding: "utf8" });
@@ -165,14 +165,31 @@ async function* iterateUsageRecords(filePath) {
165
165
  }
166
166
 
167
167
  const lines = readline.createInterface({ input, crlfDelay: Infinity });
168
+ let invalidRecords = 0;
168
169
  for await (const line of lines) {
169
- if (line) yield JSON.parse(line);
170
+ if (!line) continue;
171
+ try {
172
+ const record = JSON.parse(line);
173
+ if (!record || typeof record !== "object" || Array.isArray(record)) {
174
+ invalidRecords += 1;
175
+ continue;
176
+ }
177
+ yield record;
178
+ } catch {
179
+ invalidRecords += 1;
180
+ }
181
+ }
182
+ if (invalidRecords > 0) {
183
+ try {
184
+ const safePath = terminalCell(JSON.stringify(filePath));
185
+ warn(`codex-copilot-dx usage log ignored ${invalidRecords} invalid record${invalidRecords === 1 ? "" : "s"} in ${safePath}`);
186
+ } catch {}
170
187
  }
171
188
  }
172
189
 
173
- export async function readUsageRecords(filePath = usageLogPath()) {
190
+ export async function readUsageRecords(filePath = usageLogPath(), { warn = console.error } = {}) {
174
191
  const records = [];
175
- for await (const record of iterateUsageRecords(filePath)) records.push(record);
192
+ for await (const record of iterateUsageRecords(filePath, { warn })) records.push(record);
176
193
  return records;
177
194
  }
178
195
 
@@ -200,10 +217,10 @@ export function summarizeUsage(records) {
200
217
  return summary;
201
218
  }
202
219
 
203
- export async function summarizeUsageLogs(filePath = usageLogPath()) {
220
+ export async function summarizeUsageLogs(filePath = usageLogPath(), { warn = console.error } = {}) {
204
221
  const summary = { requests: 0, totals: {}, byModel: {} };
205
222
  for (const candidate of [rotatedFilePath(filePath), filePath]) {
206
- for await (const record of iterateUsageRecords(candidate)) {
223
+ for await (const record of iterateUsageRecords(candidate, { warn })) {
207
224
  const one = summarizeUsage([record]);
208
225
  summary.requests += one.requests;
209
226
  addUsageTotals(summary.totals, one.totals);
@@ -324,7 +341,8 @@ export async function printUsageSummary({
324
341
  format = "auto",
325
342
  output = process.stdout,
326
343
  log = console.log,
344
+ warn = console.error,
327
345
  } = {}) {
328
- const summary = await summarizeUsageLogs(filePath);
346
+ const summary = await summarizeUsageLogs(filePath, { warn });
329
347
  log(formatUsageSummary(summary, { filePath, format, output }));
330
348
  }