codex-copilot-dx 0.6.14 → 0.6.18
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 +30 -16
- package/bin/cli.mjs +5 -0
- package/package.json +1 -1
- package/src/adapter.mjs +34 -9
- package/src/anthropic-handler.mjs +54 -4
- package/src/anthropic.mjs +64 -26
- package/src/atomic-file.mjs +76 -2
- package/src/auth-profile.mjs +11 -34
- package/src/auth.mjs +265 -52
- package/src/cli-auth.mjs +28 -71
- package/src/cli-options.mjs +8 -5
- package/src/copilot-client.mjs +5 -1
- package/src/copilot-responses-compat.mjs +80 -0
- package/src/copilot-responses-policy.mjs +78 -0
- package/src/copilot.mjs +2 -1
- package/src/http-transport.mjs +91 -0
- package/src/lock.mjs +179 -17
- package/src/model-cache.mjs +36 -6
- package/src/models.mjs +17 -0
- package/src/observability.mjs +13 -1
- package/src/pm-studio-asar.mjs +399 -19
- package/src/pm-studio-relay.mjs +6 -3
- package/src/pm-studio-setup.mjs +319 -14
- package/src/pm-studio-status.mjs +5 -1
- package/src/profile-model-runtime.mjs +87 -13
- package/src/profile-routing.mjs +45 -7
- package/src/response-history.mjs +142 -16
- package/src/responses-bridge.mjs +22 -4
- package/src/responses-handler.mjs +154 -39
- package/src/responses-image-pressure.mjs +30 -0
- package/src/responses-proxy.mjs +10 -2
- package/src/responses-request.mjs +109 -103
- package/src/route-plan.mjs +51 -0
- package/src/startup.mjs +8 -2
- package/src/usage.mjs +25 -7
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { httpError } from "./http-transport.mjs";
|
|
1
|
+
import { applyCopilotResponsesRequestPolicies } from "./copilot-responses-policy.mjs";
|
|
3
2
|
import { debugLog } from "./log.mjs";
|
|
4
3
|
import {
|
|
5
4
|
materializeResponseHistory,
|
|
@@ -8,7 +7,20 @@ import {
|
|
|
8
7
|
} from "./response-history.mjs";
|
|
9
8
|
import { isResponsesToolOutputItem, readResponsesToolOutputParts } from "./responses-content.mjs";
|
|
10
9
|
import { enforceResponsesImageLimit } from "./responses-image-limit.mjs";
|
|
11
|
-
import {
|
|
10
|
+
import { routePlanAffinity, sameRoutePlanAffinity } from "./route-plan.mjs";
|
|
11
|
+
|
|
12
|
+
export {
|
|
13
|
+
isEncryptedContentVerificationError,
|
|
14
|
+
isImageNamespaceCollisionError,
|
|
15
|
+
sanitizeImageNamespaceCollisionRequest,
|
|
16
|
+
} from "./copilot-responses-policy.mjs";
|
|
17
|
+
|
|
18
|
+
// Compatibility export for consumers that imported this helper before the
|
|
19
|
+
// Copilot retry boundary moved into its provider-owned module.
|
|
20
|
+
export async function openCopilotResponse(...args) {
|
|
21
|
+
const compatibility = await import("./copilot-responses-compat.mjs");
|
|
22
|
+
return compatibility.openCopilotResponse(...args);
|
|
23
|
+
}
|
|
12
24
|
|
|
13
25
|
function cloneJson(value) {
|
|
14
26
|
return value === undefined ? undefined : structuredClone(value);
|
|
@@ -71,6 +83,8 @@ function isEncryptedContentPart(value) {
|
|
|
71
83
|
|
|
72
84
|
const OMIT_ENCRYPTED_CONTENT = Symbol("omit-encrypted-content");
|
|
73
85
|
const ENCRYPTED_HISTORY_REBASE = Symbol("encrypted-history-rebase");
|
|
86
|
+
const RELEASE_HISTORY_INPUT = Symbol("release-history-input");
|
|
87
|
+
const HISTORY_PRESSURE_ROOT = Symbol("history-pressure-root");
|
|
74
88
|
const ENCRYPTED_TOOL_OUTPUT_MARKER = "[CCDX: encrypted tool output omitted because upstream could not decrypt it.]";
|
|
75
89
|
|
|
76
90
|
function stripEncryptedReasoningValue(value, state) {
|
|
@@ -159,7 +173,21 @@ function isEncryptedReasoningInputItem(item) {
|
|
|
159
173
|
return keys.length === 1 && keys[0] === "encrypted_content";
|
|
160
174
|
}
|
|
161
175
|
|
|
162
|
-
|
|
176
|
+
function responseValueHasOpaqueState(value) {
|
|
177
|
+
if (Array.isArray(value)) return value.some(responseValueHasOpaqueState);
|
|
178
|
+
if (!value || typeof value !== "object") return false;
|
|
179
|
+
if (isEncryptedReasoningInputItem(value)
|
|
180
|
+
|| Object.prototype.hasOwnProperty.call(value, "encrypted_content")) {
|
|
181
|
+
return true;
|
|
182
|
+
}
|
|
183
|
+
if (isResponsesToolOutputItem(value) && typeof value.output === "string") {
|
|
184
|
+
const parsed = readResponsesToolOutputParts(value);
|
|
185
|
+
if (parsed?.parts.some(responseValueHasOpaqueState)) return true;
|
|
186
|
+
}
|
|
187
|
+
return Object.values(value).some(responseValueHasOpaqueState);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
export function sanitizeEncryptedReasoningRequest(reqContext, { historicalOnly = false } = {}) {
|
|
163
191
|
const state = { changed: false };
|
|
164
192
|
let body = cloneJson(reqContext.body);
|
|
165
193
|
let currentInputStart = reqContext.currentInputStart;
|
|
@@ -170,6 +198,10 @@ export function sanitizeEncryptedReasoningRequest(reqContext) {
|
|
|
170
198
|
for (let index = 0; index < body.input.length; index += 1) {
|
|
171
199
|
const item = body.input[index];
|
|
172
200
|
const historical = Number.isFinite(currentInputStart) && index < currentInputStart;
|
|
201
|
+
if (historicalOnly && !historical) {
|
|
202
|
+
input.push(item);
|
|
203
|
+
continue;
|
|
204
|
+
}
|
|
173
205
|
if (isEncryptedReasoningInputItem(item)) {
|
|
174
206
|
state.changed = true;
|
|
175
207
|
if (historical) historicalInputChanged = true;
|
|
@@ -188,11 +220,13 @@ export function sanitizeEncryptedReasoningRequest(reqContext) {
|
|
|
188
220
|
}
|
|
189
221
|
body.input = input;
|
|
190
222
|
if (Number.isFinite(currentInputStart)) currentInputStart = retainedBeforeCurrent;
|
|
191
|
-
} else {
|
|
223
|
+
} else if (!historicalOnly) {
|
|
192
224
|
body = stripEncryptedReasoningValue(body, state);
|
|
193
225
|
}
|
|
194
226
|
if (!state.changed) return null;
|
|
195
|
-
const historyInputItems =
|
|
227
|
+
const historyInputItems = historicalOnly
|
|
228
|
+
? reqContext.historyInputItems
|
|
229
|
+
: Array.isArray(reqContext.historyInputItems)
|
|
196
230
|
? reqContext.historyInputItems.flatMap((item) => {
|
|
197
231
|
if (isEncryptedReasoningInputItem(item)) return [];
|
|
198
232
|
const stripped = stripEncryptedReasoningInputValue(item, { changed: false });
|
|
@@ -210,10 +244,13 @@ export function sanitizeEncryptedReasoningRequest(reqContext) {
|
|
|
210
244
|
currentInputStart,
|
|
211
245
|
inputItems: Array.isArray(body.input) ? body.input : reqContext.inputItems,
|
|
212
246
|
historyInputItems,
|
|
247
|
+
[RELEASE_HISTORY_INPUT]: historicalOnly
|
|
248
|
+
? reqContext[RELEASE_HISTORY_INPUT]
|
|
249
|
+
: historyInputItems,
|
|
213
250
|
};
|
|
214
251
|
}
|
|
215
252
|
|
|
216
|
-
function finalizeEncryptedHistoryRebase(reqContext) {
|
|
253
|
+
export function finalizeEncryptedHistoryRebase(reqContext) {
|
|
217
254
|
if (!reqContext[ENCRYPTED_HISTORY_REBASE]) return reqContext;
|
|
218
255
|
const finalized = {
|
|
219
256
|
...reqContext,
|
|
@@ -226,98 +263,44 @@ function finalizeEncryptedHistoryRebase(reqContext) {
|
|
|
226
263
|
return finalized;
|
|
227
264
|
}
|
|
228
265
|
|
|
229
|
-
export function
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
}
|
|
241
|
-
|
|
242
|
-
export function isImageNamespaceCollisionError(statusCode, text) {
|
|
243
|
-
if (statusCode < 400 || !text) return false;
|
|
244
|
-
const lower = String(text).toLowerCase();
|
|
245
|
-
return lower.includes("namespace")
|
|
246
|
-
&& lower.includes("image_gen")
|
|
247
|
-
&& lower.includes("collid");
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
function isImageNamespaceTool(tool, { collisionFallback = false } = {}) {
|
|
251
|
-
if (!tool || typeof tool !== "object") return false;
|
|
252
|
-
const type = String(tool.type || "").toLowerCase();
|
|
253
|
-
const name = String(tool.name || tool.function?.name || "").toLowerCase();
|
|
254
|
-
const namespace = String(tool.namespace || "").toLowerCase();
|
|
255
|
-
if (["image_gen", "image_generation"].includes(type)) return true;
|
|
256
|
-
if (["image_gen", "image_generation"].includes(name)) return true;
|
|
257
|
-
if (namespace === "image_gen" || namespace === "image_generation") return true;
|
|
258
|
-
return collisionFallback && [type, name, namespace].some((value) => value.startsWith("image_gen"));
|
|
259
|
-
}
|
|
260
|
-
|
|
261
|
-
export function sanitizeImageNamespaceCollisionRequest(reqContext) {
|
|
262
|
-
if (!Array.isArray(reqContext?.body?.tools)) return null;
|
|
263
|
-
const body = cloneJson(reqContext.body);
|
|
264
|
-
const filtered = body.tools.filter((tool) => !isImageNamespaceTool(tool, { collisionFallback: true }));
|
|
265
|
-
if (filtered.length === body.tools.length) return null;
|
|
266
|
-
if (filtered.length) body.tools = filtered;
|
|
267
|
-
else delete body.tools;
|
|
268
|
-
return { ...reqContext, body };
|
|
269
|
-
}
|
|
270
|
-
|
|
271
|
-
export async function openCopilotResponse(reqContext, upstream = copilotResponses, options = {}) {
|
|
272
|
-
let encryptedRetried = false;
|
|
273
|
-
let imageNamespaceRetried = false;
|
|
274
|
-
let payloadPrepared = false;
|
|
275
|
-
for (let attempt = 0; attempt < 3; attempt += 1) {
|
|
276
|
-
options.assertPrepareActive?.();
|
|
277
|
-
const resp = await upstream(reqContext.body, {
|
|
278
|
-
assertActive: options.assertPrepareActive,
|
|
279
|
-
signal: options.signal,
|
|
280
|
-
currentInputStart: reqContext.currentInputStart,
|
|
281
|
-
onUpstreamStart: options.onUpstreamStart,
|
|
282
|
-
payloadPrepared,
|
|
283
|
-
});
|
|
284
|
-
options.assertPrepareActive?.();
|
|
285
|
-
payloadPrepared = true;
|
|
286
|
-
if (resp.ok) {
|
|
287
|
-
reqContext = finalizeEncryptedHistoryRebase(reqContext);
|
|
288
|
-
return { resp, reqContext };
|
|
289
|
-
}
|
|
290
|
-
|
|
291
|
-
const errorText = await resp.text();
|
|
292
|
-
options.assertPrepareActive?.();
|
|
293
|
-
if (!imageNamespaceRetried && isImageNamespaceCollisionError(resp.status, errorText)) {
|
|
294
|
-
const retryContext = sanitizeImageNamespaceCollisionRequest(reqContext);
|
|
295
|
-
if (retryContext) {
|
|
296
|
-
imageNamespaceRetried = true;
|
|
297
|
-
reqContext = retryContext;
|
|
298
|
-
console.warn(status("warn", "image_gen namespace rejected by upstream; retrying without the conflicting image tool"));
|
|
299
|
-
continue;
|
|
300
|
-
}
|
|
301
|
-
}
|
|
302
|
-
if (!encryptedRetried && isEncryptedContentVerificationError(resp.status, errorText)) {
|
|
303
|
-
const retryContext = sanitizeEncryptedReasoningRequest(reqContext);
|
|
304
|
-
if (retryContext) {
|
|
305
|
-
encryptedRetried = true;
|
|
306
|
-
reqContext = retryContext;
|
|
307
|
-
console.warn(status("warn", "encrypted replay content rejected by upstream; retrying without unavailable encrypted content"));
|
|
308
|
-
continue;
|
|
309
|
-
}
|
|
310
|
-
}
|
|
311
|
-
return { resp, reqContext, errorText };
|
|
266
|
+
export function applyResponseHistoryRoutePlan(reqContext, routePlan) {
|
|
267
|
+
const next = reqContext;
|
|
268
|
+
next.routePlan = routePlan;
|
|
269
|
+
if (!next.historyParentId) return next;
|
|
270
|
+
const currentAffinity = routePlanAffinity(routePlan);
|
|
271
|
+
const historyMetadata = next.historyRouteMetadata;
|
|
272
|
+
if (!currentAffinity
|
|
273
|
+
|| !Array.isArray(historyMetadata)
|
|
274
|
+
|| historyMetadata.length === 0
|
|
275
|
+
|| historyMetadata.some((metadata) => metadata.affinity === null)) {
|
|
276
|
+
return next;
|
|
312
277
|
}
|
|
313
|
-
|
|
278
|
+
const hasMismatchedOpaqueState = historyMetadata.some((metadata) => (
|
|
279
|
+
metadata.hasOpaque && !sameRoutePlanAffinity(metadata.affinity, currentAffinity)
|
|
280
|
+
));
|
|
281
|
+
if (!hasMismatchedOpaqueState) {
|
|
282
|
+
return next;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
const sanitized = sanitizeEncryptedReasoningRequest(next, { historicalOnly: true });
|
|
286
|
+
if (!sanitized) return next;
|
|
287
|
+
sanitized.routePlan = routePlan;
|
|
288
|
+
const historyRootId = next.historyRootId;
|
|
289
|
+
const rebased = finalizeEncryptedHistoryRebase(sanitized);
|
|
290
|
+
if (historyRootId) rebased[HISTORY_PRESSURE_ROOT] = historyRootId;
|
|
291
|
+
return rebased;
|
|
314
292
|
}
|
|
315
293
|
|
|
316
|
-
function
|
|
317
|
-
return
|
|
294
|
+
export function responseHistoryPressureRootId(reqContext) {
|
|
295
|
+
return reqContext?.[HISTORY_PRESSURE_ROOT] || reqContext?.historyRootId || null;
|
|
318
296
|
}
|
|
319
297
|
|
|
320
|
-
export function prepareResponsesRequest(reqBody, {
|
|
298
|
+
export function prepareResponsesRequest(reqBody, {
|
|
299
|
+
assertActive,
|
|
300
|
+
copilotBoundary = true,
|
|
301
|
+
historySnapshot,
|
|
302
|
+
mutate = false,
|
|
303
|
+
} = {}) {
|
|
321
304
|
assertActive?.();
|
|
322
305
|
const body = mutate ? reqBody : cloneJson(reqBody);
|
|
323
306
|
assertActive?.();
|
|
@@ -328,10 +311,25 @@ export function prepareResponsesRequest(reqBody, { assertActive, mutate = false
|
|
|
328
311
|
const previousId = body.previous_response_id;
|
|
329
312
|
let historyItems = [];
|
|
330
313
|
let historyRootId = null;
|
|
314
|
+
const historyRouteMetadata = [];
|
|
331
315
|
|
|
332
316
|
if (previousId !== undefined && previousId !== null) {
|
|
333
|
-
|
|
334
|
-
|
|
317
|
+
if (historySnapshot) {
|
|
318
|
+
if (historySnapshot.responseId !== previousId) {
|
|
319
|
+
throw new TypeError("Response history snapshot does not match previous_response_id");
|
|
320
|
+
}
|
|
321
|
+
historyItems = historySnapshot.materialize({
|
|
322
|
+
assertActive,
|
|
323
|
+
routeMetadata: historyRouteMetadata,
|
|
324
|
+
});
|
|
325
|
+
historyRootId = historySnapshot.rootId;
|
|
326
|
+
} else {
|
|
327
|
+
historyItems = materializeResponseHistory(previousId, {
|
|
328
|
+
assertActive,
|
|
329
|
+
routeMetadata: historyRouteMetadata,
|
|
330
|
+
});
|
|
331
|
+
historyRootId = responseHistoryRootId(previousId);
|
|
332
|
+
}
|
|
335
333
|
body.input = [...historyItems, ...currentInputItems];
|
|
336
334
|
} else {
|
|
337
335
|
body.input = currentInputItems;
|
|
@@ -351,10 +349,10 @@ export function prepareResponsesRequest(reqBody, { assertActive, mutate = false
|
|
|
351
349
|
|
|
352
350
|
delete body.previous_response_id;
|
|
353
351
|
delete body.store;
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
352
|
+
// Preserve the historical helper contract for embedders. Runtime handlers
|
|
353
|
+
// pass copilotBoundary:false and apply the policy only after selecting a
|
|
354
|
+
// concrete Copilot RoutePlan.
|
|
355
|
+
if (copilotBoundary) applyCopilotResponsesRequestPolicies(body);
|
|
358
356
|
stripInternalResponsesInputFields(body.input, { assertActive });
|
|
359
357
|
stripInternalResponsesInputFields(historyInputItems, { assertActive });
|
|
360
358
|
assertActive?.();
|
|
@@ -366,15 +364,21 @@ export function prepareResponsesRequest(reqBody, { assertActive, mutate = false
|
|
|
366
364
|
currentInputStart: retainedCurrentInputStart < 0 ? body.input.length : retainedCurrentInputStart,
|
|
367
365
|
historyParentId: previousId ?? null,
|
|
368
366
|
historyRootId,
|
|
367
|
+
historyRouteMetadata,
|
|
369
368
|
historyInputItems,
|
|
369
|
+
...(previousId !== undefined && previousId !== null
|
|
370
|
+
? { [RELEASE_HISTORY_INPUT]: historyInputItems }
|
|
371
|
+
: {}),
|
|
370
372
|
takeHistoryOwnership: mutate,
|
|
371
373
|
};
|
|
372
374
|
}
|
|
373
375
|
|
|
374
376
|
export function dropMaterializedResponseHistory(reqContext) {
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
reqContext.
|
|
377
|
+
const releaseInput = reqContext?.[RELEASE_HISTORY_INPUT];
|
|
378
|
+
if (!Array.isArray(releaseInput)) return false;
|
|
379
|
+
reqContext.body = { ...reqContext.body, input: releaseInput };
|
|
380
|
+
reqContext.inputItems = releaseInput;
|
|
381
|
+
delete reqContext[RELEASE_HISTORY_INPUT];
|
|
378
382
|
reqContext.currentInputStart = 0;
|
|
379
383
|
return true;
|
|
380
384
|
}
|
|
@@ -391,6 +395,8 @@ export function rememberResponseHistory(reqContext, responseJson) {
|
|
|
391
395
|
parentId: compacted ? null : reqContext.historyParentId,
|
|
392
396
|
inputItems: sourceInputItems,
|
|
393
397
|
outputItems: sourceOutputItems,
|
|
398
|
+
hasOpaque: responseValueHasOpaqueState([sourceInputItems, sourceOutputItems]),
|
|
399
|
+
routeAffinity: routePlanAffinity(reqContext.routePlan),
|
|
394
400
|
takeOwnership: reqContext.takeHistoryOwnership,
|
|
395
401
|
});
|
|
396
402
|
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
|
|
3
|
+
const DISPOSITIONS = new Set(["native", "relay", "reject"]);
|
|
4
|
+
const MAX_AFFINITY_MODEL_BYTES = 512;
|
|
5
|
+
|
|
6
|
+
function requiredString(value, name) {
|
|
7
|
+
const normalized = String(value || "").trim();
|
|
8
|
+
if (!normalized) throw new Error(`Route plan ${name} is required`);
|
|
9
|
+
return normalized;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function createRoutePlan({
|
|
13
|
+
disposition = "relay",
|
|
14
|
+
model,
|
|
15
|
+
origin,
|
|
16
|
+
profile,
|
|
17
|
+
protocol,
|
|
18
|
+
surface,
|
|
19
|
+
} = {}) {
|
|
20
|
+
if (!DISPOSITIONS.has(disposition)) {
|
|
21
|
+
throw new Error(`Unsupported route disposition: ${disposition}`);
|
|
22
|
+
}
|
|
23
|
+
return Object.freeze({
|
|
24
|
+
disposition,
|
|
25
|
+
origin: requiredString(origin, "origin"),
|
|
26
|
+
profile: requiredString(profile, "profile"),
|
|
27
|
+
protocol: requiredString(protocol, "protocol"),
|
|
28
|
+
model: requiredString(model, "model"),
|
|
29
|
+
surface: requiredString(surface, "surface"),
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function routePlanAffinity(plan) {
|
|
34
|
+
if (!plan || typeof plan !== "object") return null;
|
|
35
|
+
const { origin, profile, protocol, model } = plan;
|
|
36
|
+
if (![origin, profile, protocol, model].every((value) => typeof value === "string" && value.length > 0)) {
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
const boundedModel = Buffer.byteLength(model) <= MAX_AFFINITY_MODEL_BYTES
|
|
40
|
+
? model
|
|
41
|
+
: `sha256:${createHash("sha256").update(model).digest("hex")}`;
|
|
42
|
+
return Object.freeze({ origin, profile, protocol, model: boundedModel });
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function sameRoutePlanAffinity(left, right) {
|
|
46
|
+
return Boolean(left && right)
|
|
47
|
+
&& left.origin === right.origin
|
|
48
|
+
&& left.profile === right.profile
|
|
49
|
+
&& left.protocol === right.protocol
|
|
50
|
+
&& left.model === right.model;
|
|
51
|
+
}
|
package/src/startup.mjs
CHANGED
|
@@ -2,9 +2,15 @@ export async function initializeModelRegistry({
|
|
|
2
2
|
loadCached,
|
|
3
3
|
currentModelDefs,
|
|
4
4
|
refresh,
|
|
5
|
+
onBackgroundError = () => {},
|
|
5
6
|
}) {
|
|
6
|
-
|
|
7
|
-
|
|
7
|
+
const cached = loadCached();
|
|
8
|
+
const loaded = cached === true || cached?.loaded === true;
|
|
9
|
+
if (loaded) {
|
|
10
|
+
const backgroundRefresh = cached?.stale === true
|
|
11
|
+
? runInBackground(refresh, onBackgroundError)
|
|
12
|
+
: null;
|
|
13
|
+
return { modelDefs: currentModelDefs(), source: "cache", backgroundRefresh };
|
|
8
14
|
}
|
|
9
15
|
|
|
10
16
|
return { modelDefs: await refresh(), source: "live", backgroundRefresh: null };
|
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)
|
|
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
|
}
|