codex-copilot-dx 0.9.17 → 0.9.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/package.json +1 -1
- package/src/adapter.mjs +3 -4
- package/src/auth-status.mjs +175 -0
- package/src/cli-auth.mjs +2 -180
- package/src/cli-models.mjs +1 -131
- package/src/cli-table.mjs +3 -11
- package/src/copilot-responses-compat.mjs +1 -1
- package/src/dashboard-api.mjs +4 -4
- package/src/dashboard.mjs +1 -1
- package/src/http-errors.mjs +5 -0
- package/src/http-transport.mjs +6 -350
- package/src/image-mcp.mjs +1 -1
- package/src/live-models.mjs +137 -0
- package/src/observability.mjs +2 -22
- package/src/request-admission.mjs +348 -0
- package/src/response-failures.mjs +1 -1
- package/src/response-history.mjs +1 -1
- package/src/responses-compaction.mjs +1 -1
- package/src/responses-handler.mjs +1 -1
- package/src/responses-proxy.mjs +2 -2
- package/src/security.mjs +22 -0
- package/src/terminal-text.mjs +11 -0
- package/src/usage-analytics.mjs +1 -1
- package/src/usage-store.mjs +369 -0
- package/src/usage.mjs +17 -368
|
@@ -0,0 +1,369 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import { markRequestObservation } from "./stream-performance.mjs";
|
|
3
|
+
import os from "node:os";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import readline from "node:readline";
|
|
6
|
+
import { appendRotatingLines, parseByteLimit, rotatedFilePath } from "./file-rotation.mjs";
|
|
7
|
+
import { withFileLock } from "./lock.mjs";
|
|
8
|
+
import { terminalCell } from "./terminal-text.mjs";
|
|
9
|
+
|
|
10
|
+
const DEFAULT_USAGE_PATH = path.join(os.homedir(), ".local", "share", "codex-copilot-dx", "usage.jsonl");
|
|
11
|
+
const DEFAULT_USAGE_MAX_BYTES = 32 * 1024 * 1024;
|
|
12
|
+
const USAGE_WRITE_LOCK_TIMEOUT_MS = 1_000;
|
|
13
|
+
const USAGE_WRITE_LOCK_STALE_MS = 1_000;
|
|
14
|
+
const USAGE_WRITE_LOCK_POLL_MS = 5;
|
|
15
|
+
const USAGE_FLUSH_TIMEOUT_MS = 1_500;
|
|
16
|
+
|
|
17
|
+
const pendingWrites = [];
|
|
18
|
+
let writeDrain = null;
|
|
19
|
+
const USAGE_QUEUE_MAX_RECORDS = 4096;
|
|
20
|
+
const USAGE_QUEUE_MAX_BYTES = 4 * 1024 * 1024;
|
|
21
|
+
let pendingRecords = 0;
|
|
22
|
+
let pendingBytes = 0;
|
|
23
|
+
let droppedRecords = 0;
|
|
24
|
+
let writeFailures = 0;
|
|
25
|
+
|
|
26
|
+
export function usageLoggingStats() {
|
|
27
|
+
return { pending_records: pendingRecords, pending_bytes: pendingBytes, dropped_records: droppedRecords,
|
|
28
|
+
write_failures: writeFailures, max_records: USAGE_QUEUE_MAX_RECORDS, max_bytes: USAGE_QUEUE_MAX_BYTES };
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function usageLogPath() {
|
|
32
|
+
return process.env.CCDX_USAGE_PATH || DEFAULT_USAGE_PATH;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function usageLogMaxBytes(env = process.env) {
|
|
36
|
+
return parseByteLimit(env.CCDX_USAGE_MAX_BYTES, DEFAULT_USAGE_MAX_BYTES);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function numberOrUndefined(value) {
|
|
40
|
+
return Number.isFinite(value) ? value : undefined;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function positiveNumber(value) {
|
|
44
|
+
const n = numberOrUndefined(value);
|
|
45
|
+
return n && n > 0 ? n : undefined;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function compactObject(obj) {
|
|
49
|
+
return Object.fromEntries(Object.entries(obj).filter(([, v]) => v !== undefined && v !== null));
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function hasPositiveTokenValue(usage) {
|
|
53
|
+
return Object.entries(usage || {}).some(([k, v]) => k.endsWith("_tokens") && Number.isFinite(v) && v > 0);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function normalizeResponsesUsage(usage) {
|
|
57
|
+
if (!usage || typeof usage !== "object") return undefined;
|
|
58
|
+
const cachedInput = numberOrUndefined(usage.cached_input_tokens ?? usage.input_tokens_details?.cached_tokens);
|
|
59
|
+
const out = compactObject({
|
|
60
|
+
input_tokens: positiveNumber(usage.input_tokens),
|
|
61
|
+
cached_input_tokens: cachedInput >= 0 ? cachedInput : undefined,
|
|
62
|
+
output_tokens: positiveNumber(usage.output_tokens),
|
|
63
|
+
reasoning_output_tokens: positiveNumber(usage.reasoning_output_tokens ?? usage.output_tokens_details?.reasoning_tokens),
|
|
64
|
+
total_tokens: positiveNumber(usage.total_tokens),
|
|
65
|
+
});
|
|
66
|
+
if (out.total_tokens === undefined && (out.input_tokens !== undefined || out.output_tokens !== undefined)) {
|
|
67
|
+
out.total_tokens = (out.input_tokens || 0) + (out.output_tokens || 0);
|
|
68
|
+
}
|
|
69
|
+
return hasPositiveTokenValue(out) ? out : undefined;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function normalizeCopilotUsage(copilotUsage) {
|
|
73
|
+
if (!copilotUsage || typeof copilotUsage !== "object") return undefined;
|
|
74
|
+
const out = {};
|
|
75
|
+
for (const detail of copilotUsage.token_details || []) {
|
|
76
|
+
const tokens = positiveNumber(detail?.token_count);
|
|
77
|
+
if (!tokens) continue;
|
|
78
|
+
switch (detail.token_type) {
|
|
79
|
+
case "input":
|
|
80
|
+
out.input_tokens = (out.input_tokens || 0) + tokens;
|
|
81
|
+
break;
|
|
82
|
+
case "cache_read":
|
|
83
|
+
out.cache_read_tokens = (out.cache_read_tokens || 0) + tokens;
|
|
84
|
+
break;
|
|
85
|
+
case "output":
|
|
86
|
+
out.output_tokens = (out.output_tokens || 0) + tokens;
|
|
87
|
+
break;
|
|
88
|
+
default:
|
|
89
|
+
out[`${detail.token_type || "unknown"}_tokens`] = (out[`${detail.token_type || "unknown"}_tokens`] || 0) + tokens;
|
|
90
|
+
break;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
out.total_tokens = (out.input_tokens || 0) + (out.cache_read_tokens || 0) + (out.output_tokens || 0);
|
|
94
|
+
if (Number.isFinite(copilotUsage.total_nano_aiu)) out.total_nano_aiu = copilotUsage.total_nano_aiu;
|
|
95
|
+
return hasPositiveTokenValue(out) ? out : undefined;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export function buildResponsesUsageRecord({ surface = "responses", mode, model, response, event } = {}) {
|
|
99
|
+
const responseObj = response || event?.response;
|
|
100
|
+
const usage = normalizeResponsesUsage(responseObj?.usage);
|
|
101
|
+
const copilotUsage = normalizeCopilotUsage(event?.copilot_usage || responseObj?.copilot_usage);
|
|
102
|
+
if (!usage && !copilotUsage) return null;
|
|
103
|
+
return compactObject({
|
|
104
|
+
ts: new Date().toISOString(),
|
|
105
|
+
surface,
|
|
106
|
+
mode,
|
|
107
|
+
model: responseObj?.model || model,
|
|
108
|
+
response_id: responseObj?.id,
|
|
109
|
+
usage,
|
|
110
|
+
copilot_usage: copilotUsage,
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function takePendingWrites(filePath, maxBytes) {
|
|
115
|
+
const batch = [];
|
|
116
|
+
let retained = 0;
|
|
117
|
+
for (const entry of pendingWrites) {
|
|
118
|
+
if (entry.filePath === filePath && entry.maxBytes === maxBytes) batch.push(entry);
|
|
119
|
+
else pendingWrites[retained++] = entry;
|
|
120
|
+
}
|
|
121
|
+
pendingWrites.length = retained;
|
|
122
|
+
return batch;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
async function writeUsageBatch(batch) {
|
|
126
|
+
const { filePath, maxBytes } = batch[0];
|
|
127
|
+
await fs.promises.mkdir(path.dirname(filePath), { recursive: true });
|
|
128
|
+
await withFileLock(`${path.resolve(filePath)}.lock`, async () => {
|
|
129
|
+
batch.push(...takePendingWrites(filePath, maxBytes));
|
|
130
|
+
await appendRotatingLines(filePath, batch.map(({ line }) => line), maxBytes);
|
|
131
|
+
}, {
|
|
132
|
+
timeoutMs: USAGE_WRITE_LOCK_TIMEOUT_MS,
|
|
133
|
+
staleMs: USAGE_WRITE_LOCK_STALE_MS,
|
|
134
|
+
pollMs: USAGE_WRITE_LOCK_POLL_MS,
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function scheduleUsageWriteDrain() {
|
|
139
|
+
if (writeDrain) return;
|
|
140
|
+
writeDrain = Promise.resolve().then(async () => {
|
|
141
|
+
while (pendingWrites.length > 0) {
|
|
142
|
+
const first = pendingWrites.shift();
|
|
143
|
+
const batch = [first, ...takePendingWrites(first.filePath, first.maxBytes)];
|
|
144
|
+
try {
|
|
145
|
+
await writeUsageBatch(batch);
|
|
146
|
+
} catch (error) {
|
|
147
|
+
writeFailures += 1;
|
|
148
|
+
batch.push(...takePendingWrites(first.filePath, first.maxBytes));
|
|
149
|
+
console.error(`codex-copilot-dx usage log write failed: ${error.message}`);
|
|
150
|
+
} finally {
|
|
151
|
+
for (const entry of batch) {
|
|
152
|
+
pendingRecords -= 1;
|
|
153
|
+
pendingBytes -= entry.bytes;
|
|
154
|
+
entry.resolve();
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
}).finally(() => {
|
|
159
|
+
writeDrain = null;
|
|
160
|
+
if (pendingWrites.length > 0) scheduleUsageWriteDrain();
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
export function recordUsage(record) {
|
|
165
|
+
if (!record || process.env.CCDX_DISABLE_USAGE === "1") return Promise.resolve();
|
|
166
|
+
const filePath = usageLogPath();
|
|
167
|
+
const line = `${JSON.stringify(record)}\n`;
|
|
168
|
+
const bytes = Buffer.byteLength(line);
|
|
169
|
+
if (pendingRecords >= USAGE_QUEUE_MAX_RECORDS || pendingBytes + bytes > USAGE_QUEUE_MAX_BYTES) {
|
|
170
|
+
droppedRecords += 1;
|
|
171
|
+
if (droppedRecords === 1) console.error("codex-copilot-dx usage queue is full; excess usage records are dropped, inference is unaffected (see ccdx status)");
|
|
172
|
+
return Promise.resolve();
|
|
173
|
+
}
|
|
174
|
+
pendingRecords += 1;
|
|
175
|
+
pendingBytes += bytes;
|
|
176
|
+
const completed = new Promise((resolve) => {
|
|
177
|
+
pendingWrites.push({ filePath, line, bytes, maxBytes: usageLogMaxBytes(), resolve });
|
|
178
|
+
});
|
|
179
|
+
scheduleUsageWriteDrain();
|
|
180
|
+
return completed;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
export function recordResponsesUsage(args) {
|
|
184
|
+
const response = args.response || args.event?.response || args.event;
|
|
185
|
+
markRequestObservation({ input_tokens: response?.usage?.input_tokens });
|
|
186
|
+
return recordUsage(buildResponsesUsageRecord(args));
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
export async function flushUsageWritesForTests() {
|
|
190
|
+
while (writeDrain || pendingWrites.length > 0) {
|
|
191
|
+
if (writeDrain) await writeDrain;
|
|
192
|
+
else scheduleUsageWriteDrain();
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
export async function flushUsageWrites({
|
|
197
|
+
timeoutMs = USAGE_FLUSH_TIMEOUT_MS,
|
|
198
|
+
warn = console.error,
|
|
199
|
+
} = {}) {
|
|
200
|
+
const timeout = Number.isFinite(timeoutMs) && timeoutMs >= 0
|
|
201
|
+
? Math.floor(timeoutMs)
|
|
202
|
+
: USAGE_FLUSH_TIMEOUT_MS;
|
|
203
|
+
let timer;
|
|
204
|
+
const completed = await Promise.race([
|
|
205
|
+
flushUsageWritesForTests().then(() => true),
|
|
206
|
+
new Promise((resolve) => { timer = setTimeout(() => resolve(false), timeout); }),
|
|
207
|
+
]);
|
|
208
|
+
if (timer) clearTimeout(timer);
|
|
209
|
+
if (!completed) {
|
|
210
|
+
try { warn(`codex-copilot-dx usage log flush timed out after ${timeout}ms`); } catch {}
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
async function* iterateUsageRecords(filePath, { warn = console.error } = {}) {
|
|
215
|
+
let input;
|
|
216
|
+
try {
|
|
217
|
+
input = fs.createReadStream(filePath, { encoding: "utf8" });
|
|
218
|
+
await new Promise((resolve, reject) => {
|
|
219
|
+
input.once("open", resolve);
|
|
220
|
+
input.once("error", reject);
|
|
221
|
+
});
|
|
222
|
+
} catch (e) {
|
|
223
|
+
input?.destroy();
|
|
224
|
+
if (e?.code === "ENOENT") return;
|
|
225
|
+
throw e;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
const lines = readline.createInterface({ input, crlfDelay: Infinity });
|
|
229
|
+
let invalidRecords = 0;
|
|
230
|
+
for await (const line of lines) {
|
|
231
|
+
if (!line) continue;
|
|
232
|
+
try {
|
|
233
|
+
const record = JSON.parse(line);
|
|
234
|
+
if (!record || typeof record !== "object" || Array.isArray(record)) {
|
|
235
|
+
invalidRecords += 1;
|
|
236
|
+
continue;
|
|
237
|
+
}
|
|
238
|
+
yield record;
|
|
239
|
+
} catch {
|
|
240
|
+
invalidRecords += 1;
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
if (invalidRecords > 0) {
|
|
244
|
+
try {
|
|
245
|
+
const safePath = terminalCell(JSON.stringify(filePath));
|
|
246
|
+
warn(`codex-copilot-dx usage log ignored ${invalidRecords} invalid record${invalidRecords === 1 ? "" : "s"} in ${safePath}`);
|
|
247
|
+
} catch {}
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
export async function readUsageRecords(filePath = usageLogPath(), { warn = console.error } = {}) {
|
|
252
|
+
const records = [];
|
|
253
|
+
for await (const record of iterateUsageRecords(filePath, { warn })) records.push(record);
|
|
254
|
+
return records;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
function addUsageTotals(target, usage = {}) {
|
|
258
|
+
for (const [key, value] of Object.entries(usage || {})) {
|
|
259
|
+
if (Number.isFinite(value)) target[key] = (target[key] || 0) + value;
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
export function summarizeUsage(records) {
|
|
264
|
+
const summary = { requests: 0, totals: {}, byModel: {} };
|
|
265
|
+
for (const record of records) {
|
|
266
|
+
summary.requests += 1;
|
|
267
|
+
addUsageTotals(summary.totals, record.usage);
|
|
268
|
+
if (record.copilot_usage) {
|
|
269
|
+
summary.totals.copilot_total_tokens = (summary.totals.copilot_total_tokens || 0) + (record.copilot_usage.total_tokens || 0);
|
|
270
|
+
summary.totals.total_nano_aiu = (summary.totals.total_nano_aiu || 0) + (record.copilot_usage.total_nano_aiu || 0);
|
|
271
|
+
}
|
|
272
|
+
const model = record.model || "unknown";
|
|
273
|
+
if (!summary.byModel[model]) summary.byModel[model] = { requests: 0 };
|
|
274
|
+
const modelTotals = summary.byModel[model];
|
|
275
|
+
modelTotals.requests += 1;
|
|
276
|
+
addUsageTotals(modelTotals, record.usage);
|
|
277
|
+
const input = record.usage?.input_tokens;
|
|
278
|
+
const cached = cacheReadTokens(record.usage || {});
|
|
279
|
+
if (!Number.isFinite(input) || input < 0 || !Number.isFinite(cached) || cached < 0 || cached > input) {
|
|
280
|
+
summary.totals.cache_hit_unknown_requests = (summary.totals.cache_hit_unknown_requests || 0) + 1;
|
|
281
|
+
modelTotals.cache_hit_unknown_requests = (modelTotals.cache_hit_unknown_requests || 0) + 1;
|
|
282
|
+
}
|
|
283
|
+
const cacheCounts = [record.usage?.cached_input_tokens, record.usage?.cache_read_input_tokens];
|
|
284
|
+
if ((input != null && (!Number.isFinite(input) || input < 0))
|
|
285
|
+
|| cacheCounts.some((value) => value != null && (!Number.isFinite(value) || value < 0))
|
|
286
|
+
|| (Number.isFinite(input) && Number.isFinite(cached) && cached > input)) {
|
|
287
|
+
summary.totals.cache_hit_invalid_requests = (summary.totals.cache_hit_invalid_requests || 0) + 1;
|
|
288
|
+
modelTotals.cache_hit_invalid_requests = (modelTotals.cache_hit_invalid_requests || 0) + 1;
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
return summary;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
async function summarizeUsageLogFiles(filePath, { warn = console.error, onRecord } = {}) {
|
|
295
|
+
const summary = { requests: 0, totals: {}, byModel: {} };
|
|
296
|
+
for (const candidate of [rotatedFilePath(filePath), filePath]) {
|
|
297
|
+
for await (const record of iterateUsageRecords(candidate, { warn })) {
|
|
298
|
+
const one = summarizeUsage([record]);
|
|
299
|
+
onRecord?.(record, one);
|
|
300
|
+
summary.requests += one.requests;
|
|
301
|
+
addUsageTotals(summary.totals, one.totals);
|
|
302
|
+
for (const [model, values] of Object.entries(one.byModel)) {
|
|
303
|
+
if (!summary.byModel[model]) summary.byModel[model] = { requests: 0 };
|
|
304
|
+
addUsageTotals(summary.byModel[model], values);
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
return summary;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
const SUMMARY_CACHE_MAX_BYTES = 256 * 1024;
|
|
312
|
+
const SUMMARY_CACHE_SETTLED_MS = 2000;
|
|
313
|
+
let cachedSummary = null;
|
|
314
|
+
|
|
315
|
+
async function usageSummaryVersion(filePath) {
|
|
316
|
+
try {
|
|
317
|
+
const files = await Promise.all([rotatedFilePath(filePath), filePath].map(async (file) => {
|
|
318
|
+
try {
|
|
319
|
+
const stat = await fs.promises.stat(file, { bigint: true });
|
|
320
|
+
if (!stat.isFile()) return null;
|
|
321
|
+
const recent = Math.max(Number(stat.mtimeMs), Number(stat.ctimeMs));
|
|
322
|
+
if (Date.now() - recent < SUMMARY_CACHE_SETTLED_MS) return null;
|
|
323
|
+
return [stat.dev, stat.ino, stat.size, stat.mtimeNs, stat.ctimeNs].join(":");
|
|
324
|
+
} catch (error) {
|
|
325
|
+
if (error?.code === "ENOENT") return "missing";
|
|
326
|
+
throw error;
|
|
327
|
+
}
|
|
328
|
+
}));
|
|
329
|
+
return files.some((version) => version === null) ? null : `${path.resolve(filePath)}\0${files.join("|")}`;
|
|
330
|
+
} catch { return null; } // Uncacheable metadata never replaces the normal read/error path.
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
export async function summarizeUsageLogs(filePath = usageLogPath(), { warn = console.error, onRecord } = {}) {
|
|
334
|
+
if (onRecord || warn !== console.error) return summarizeUsageLogFiles(filePath, { warn, onRecord });
|
|
335
|
+
const version = await usageSummaryVersion(filePath);
|
|
336
|
+
if (version === null) {
|
|
337
|
+
cachedSummary = null;
|
|
338
|
+
return summarizeUsageLogFiles(filePath, { warn });
|
|
339
|
+
}
|
|
340
|
+
if (cachedSummary?.version === version) return structuredClone(await cachedSummary.promise);
|
|
341
|
+
const entry = { version, promise: null };
|
|
342
|
+
cachedSummary = entry;
|
|
343
|
+
entry.promise = (async () => {
|
|
344
|
+
let warned = false;
|
|
345
|
+
const summary = await summarizeUsageLogFiles(filePath, { warn(message) { warned = true; warn(message); } });
|
|
346
|
+
const after = await usageSummaryVersion(filePath);
|
|
347
|
+
const retain = !warned && after === version && Object.keys(summary.byModel).length <= 128
|
|
348
|
+
&& Buffer.byteLength(JSON.stringify(summary)) <= SUMMARY_CACHE_MAX_BYTES;
|
|
349
|
+
if (!retain && cachedSummary === entry) cachedSummary = null;
|
|
350
|
+
return summary;
|
|
351
|
+
})();
|
|
352
|
+
try { return structuredClone(await entry.promise); }
|
|
353
|
+
catch (error) { if (cachedSummary === entry) cachedSummary = null; throw error; }
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
export function cacheReadTokens(usage = {}) {
|
|
357
|
+
const values = [usage.cache_read_input_tokens, usage.cached_input_tokens]
|
|
358
|
+
.filter((value) => Number.isFinite(value));
|
|
359
|
+
const value = values.length > 0 ? values.reduce((total, current) => total + current, 0) : undefined;
|
|
360
|
+
return Number.isFinite(value) ? value : undefined;
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
export function usageCacheHitRate(usage = {}) {
|
|
364
|
+
const input = usage.input_tokens;
|
|
365
|
+
const cached = cacheReadTokens(usage);
|
|
366
|
+
if (usage.cache_hit_invalid_requests > 0 || !Number.isFinite(input) || input <= 0
|
|
367
|
+
|| !Number.isFinite(cached) || cached < 0 || cached > input) return null;
|
|
368
|
+
return cached / input;
|
|
369
|
+
}
|