ccdeck 3.38.3 → 3.38.5
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 +19 -7
- package/dist/web/assets/{BrowserWatchModal-DTjbVOvo.js → BrowserWatchModal-DCC89YSC.js} +1 -1
- package/dist/web/assets/{UsageHistoryModal-BcXa5ICK.js → UsageHistoryModal-o8amyWjp.js} +1 -1
- package/dist/web/assets/index-BIkuTs0Y.css +1 -0
- package/dist/web/assets/index-Bg99W1Kp.js +309 -0
- package/dist/web/index.html +2 -2
- package/package.json +1 -1
- package/release-notes.json +16 -0
- package/src/server/account-notify.mjs +558 -0
- package/src/server/account-projects.mjs +24 -18
- package/src/server/account-watch.mjs +169 -0
- package/src/server/codex-quota.mjs +13 -0
- package/src/server/cswap-auto-loop.mjs +10 -2
- package/src/server/deck-prefs.mjs +26 -1
- package/src/server/event-pipeline.mjs +27 -5
- package/src/server/index.mjs +7 -0
- package/src/server/quota.mjs +18 -0
- package/src/server/sse-clients.mjs +5 -2
- package/src/server/thermal-metrics.mjs +100 -29
- package/src/server/thermal-sampler.mjs +8 -4
- package/src/server/transcript-scan.mjs +114 -28
- package/dist/web/assets/index-6_mJZ_aP.css +0 -1
- package/dist/web/assets/index-ClJbck-M.js +0 -309
|
@@ -111,10 +111,19 @@ import { run } from "./metrics-run.mjs";
|
|
|
111
111
|
// Reasoned from the documented layout, not yet run on a localised
|
|
112
112
|
// Windows.
|
|
113
113
|
//
|
|
114
|
-
// A
|
|
115
|
-
//
|
|
116
|
-
//
|
|
117
|
-
//
|
|
114
|
+
// A ZONE IS NOT THE CPU, AND IT MAY NOT BE A SENSOR AT ALL. An ACPI
|
|
115
|
+
// zone measures whatever the firmware wired it to — the skin, the
|
|
116
|
+
// board, the embedded controller's idea of the hottest of those —
|
|
117
|
+
// and on a lot of laptops it measures nothing: Intel's reference
|
|
118
|
+
// firmware returns a constant 3010 (drawn as 28°C) when it has no
|
|
119
|
+
// source. So a zone is drawn only once it has been seen to change,
|
|
120
|
+
// and never under the name CPU. See zonesSeenAfter.
|
|
121
|
+
//
|
|
122
|
+
// A MODERN INTEL LAPTOP declares no zone at all, or one that never
|
|
123
|
+
// moves: Intel DTT manages its sensors and publishes them only to an
|
|
124
|
+
// administrator. There the degrees come from LibreHardwareMonitor's
|
|
125
|
+
// local web server when the user already runs it — see
|
|
126
|
+
// hwmonitor.mjs.
|
|
118
127
|
//
|
|
119
128
|
// NEVER INVENT A READING. No sensor means no row, and no rows at all means the
|
|
120
129
|
// section is not rendered: not 0°C, not a dash, not a grey empty bar. Same rule
|
|
@@ -599,31 +608,93 @@ export async function readThermal(platform = process.platform, since = null) {
|
|
|
599
608
|
return darwinThermal({ gpuC, throttle, macmon });
|
|
600
609
|
}
|
|
601
610
|
|
|
602
|
-
if (platform === "win32")
|
|
603
|
-
// One child for both sources rather than two, because the cost here is the
|
|
604
|
-
// PowerShell start and not the queries: the counter is tried first because
|
|
605
|
-
// it needs no administrator, and MSAcpi only when the counter said nothing.
|
|
606
|
-
// Both are wrapped in their own try — "no thermal zone on this machine" is
|
|
607
|
-
// the ordinary answer and arrives as a throw from either.
|
|
608
|
-
const out = await run("powershell.exe", [
|
|
609
|
-
"-NoProfile", "-NonInteractive", "-Command", WIN_THERMAL_PS,
|
|
610
|
-
], 6_000);
|
|
611
|
-
const answer = out ? parseWinThermal(out.trim()) : [];
|
|
612
|
-
if (answer.length) return { celsius: answer, throttle: null };
|
|
613
|
-
|
|
614
|
-
// Windows itself had nothing, which on a modern Intel laptop is every time:
|
|
615
|
-
// the firmware declares no ACPI thermal zone and the sensors sit behind
|
|
616
|
-
// Intel DTT, which an ordinary process may not read. If something on this
|
|
617
|
-
// machine has already gone and got them — LibreHardwareMonitor, with its
|
|
618
|
-
// web server on — they are a plain HTTP read away. Never installed, never
|
|
619
|
-
// asked for. See hwmonitor.mjs.
|
|
620
|
-
const { readHwMonitorTemps } = await import("./hwmonitor.mjs");
|
|
621
|
-
const t = await readHwMonitorTemps();
|
|
622
|
-
const rows = [];
|
|
623
|
-
if (t.cpu != null) rows.push({ label: "CPU", celsius: t.cpu, warnAt: WARN_C, critAt: CRIT_C });
|
|
624
|
-
if (t.gpu != null) rows.push({ label: "GPU", celsius: t.gpu, warnAt: WARN_C, critAt: CRIT_C });
|
|
625
|
-
return rows.length ? { celsius: rows, throttle: null } : null;
|
|
626
|
-
}
|
|
611
|
+
if (platform === "win32") return readWindowsThermal(since);
|
|
627
612
|
|
|
628
613
|
return null;
|
|
629
614
|
}
|
|
615
|
+
|
|
616
|
+
/**
|
|
617
|
+
* The Windows rows, from the PowerShell child and, failing that, from a running
|
|
618
|
+
* LibreHardwareMonitor.
|
|
619
|
+
*
|
|
620
|
+
* `deps` is a seam for the reason sampleThermal's `deps.read` is: the suite
|
|
621
|
+
* must never start PowerShell, so `powershell` stands in for the child's output
|
|
622
|
+
* and `hwMonitor` for the read of the local web server.
|
|
623
|
+
*/
|
|
624
|
+
export async function readWindowsThermal(since = null, deps = {}) {
|
|
625
|
+
// One child for both sources rather than two, because the cost here is the
|
|
626
|
+
// PowerShell start and not the queries: the counter is tried first because
|
|
627
|
+
// it needs no administrator, and MSAcpi only when the counter said nothing.
|
|
628
|
+
// Both are wrapped in their own try — "no thermal zone on this machine" is
|
|
629
|
+
// the ordinary answer and arrives as a throw from either.
|
|
630
|
+
const powershell = deps.powershell ?? (() => run("powershell.exe", [
|
|
631
|
+
"-NoProfile", "-NonInteractive", "-Command", WIN_THERMAL_PS,
|
|
632
|
+
], 6_000));
|
|
633
|
+
const hwMonitor = deps.hwMonitor
|
|
634
|
+
?? (async () => (await import("./hwmonitor.mjs")).readHwMonitorTemps());
|
|
635
|
+
|
|
636
|
+
const out = await powershell();
|
|
637
|
+
const zones = out ? parseWinThermal(out.trim()) : [];
|
|
638
|
+
const zonesSeen = zonesSeenAfter(zones, since?.zonesSeen);
|
|
639
|
+
const memory = Object.keys(zonesSeen).length ? { zonesSeen } : {};
|
|
640
|
+
const moved = zones.filter(z => zonesSeen[z.label].moved);
|
|
641
|
+
if (moved.length) return { celsius: moved, throttle: null, ...memory };
|
|
642
|
+
|
|
643
|
+
// Windows had no zone that has moved, which on a modern Intel laptop is
|
|
644
|
+
// every time: the firmware declares no ACPI thermal zone, or declares one
|
|
645
|
+
// that returns a constant, and the sensors sit behind Intel DTT, which an
|
|
646
|
+
// ordinary process may not read. If something on this machine has already
|
|
647
|
+
// gone and got them — LibreHardwareMonitor, with its web server on — they
|
|
648
|
+
// are a plain HTTP read away. Never installed, never asked for. See
|
|
649
|
+
// hwmonitor.mjs.
|
|
650
|
+
const t = await hwMonitor();
|
|
651
|
+
const rows = [];
|
|
652
|
+
if (t.cpu != null) rows.push({ label: "CPU", celsius: t.cpu, warnAt: WARN_C, critAt: CRIT_C });
|
|
653
|
+
if (t.gpu != null) rows.push({ label: "GPU", celsius: t.gpu, warnAt: WARN_C, critAt: CRIT_C });
|
|
654
|
+
if (rows.length) return { celsius: rows, throttle: null, ...memory };
|
|
655
|
+
|
|
656
|
+
// A zone that has not moved yet is still a zone, so this is an answer with
|
|
657
|
+
// nothing to draw rather than null. Null would let the sampler give up after
|
|
658
|
+
// three readings, and a real zone that idled on one value for half a minute
|
|
659
|
+
// would then never be drawn. thermalSnapshot keeps it off the panel.
|
|
660
|
+
return zones.length ? { celsius: [], throttle: null, zonesSeen } : null;
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
/**
|
|
664
|
+
* Which Windows zones have been seen to change since the deck started, keyed by
|
|
665
|
+
* the zone's row label: the first value each one read, and whether it has
|
|
666
|
+
* since read anything else.
|
|
667
|
+
*
|
|
668
|
+
* A ZONE IS DRAWN ONLY ONCE IT HAS MOVED. Intel's client reference firmware,
|
|
669
|
+
* which most laptop BIOSes are built from, ends the package zone's `_TMP` with
|
|
670
|
+
* "Return a static value if no source is available. Return(3010)" — 27.85°C,
|
|
671
|
+
* drawn as 28 — and returns a static 3000 when legacy thermal management is
|
|
672
|
+
* off. Where the zone does have a source it is the embedded controller's "max
|
|
673
|
+
* platform temperature", never the die. Drawn as it came, a zone stuck on
|
|
674
|
+
* 3010 reads "Thermal zone 28 °C" on a laptop whose CPU is far hotter, which is
|
|
675
|
+
* a reading this module invents by passing it on.
|
|
676
|
+
*
|
|
677
|
+
* The test is Microsoft's own for a zone: "Each thermal zone must report
|
|
678
|
+
* actual temperature on the sensor (_TMP). The test runs a varying workload on
|
|
679
|
+
* the PC, and the temperature is expected to change." Movement rather than a
|
|
680
|
+
* list of known constants, because a constant is a firmware's choice and the
|
|
681
|
+
* next one may choose another — and because a firmware that adds 2730 rather
|
|
682
|
+
* than 2732 reads a real 28°C as 3010, which a list would hide.
|
|
683
|
+
*
|
|
684
|
+
* WHAT IT COSTS: a real zone that holds one value from the moment the deck
|
|
685
|
+
* starts is not drawn until it first changes. Once it has, it stays drawn for
|
|
686
|
+
* the life of the process however still it then sits — `moved` never goes back.
|
|
687
|
+
* Compared in whole degrees, because that is what the row shows: a zone whose
|
|
688
|
+
* row would never change is indistinguishable from a constant to whoever reads
|
|
689
|
+
* it.
|
|
690
|
+
*/
|
|
691
|
+
function zonesSeenAfter(zones, before = {}) {
|
|
692
|
+
const seen = { ...before };
|
|
693
|
+
for (const z of zones) {
|
|
694
|
+
const prior = seen[z.label];
|
|
695
|
+
seen[z.label] = prior
|
|
696
|
+
? { first: prior.first, moved: prior.moved || z.celsius !== prior.first }
|
|
697
|
+
: { first: z.celsius, moved: false };
|
|
698
|
+
}
|
|
699
|
+
return seen;
|
|
700
|
+
}
|
|
@@ -181,12 +181,16 @@ export function lastThermal() {
|
|
|
181
181
|
}
|
|
182
182
|
|
|
183
183
|
/** The thermal row /api/system answers: the last reading, with whether the
|
|
184
|
-
* machine has been held back since the deck started, or null without one
|
|
184
|
+
* machine has been held back since the deck started, or null without one —
|
|
185
|
+
* and null for a reading with nothing to draw, which is a Windows machine
|
|
186
|
+
* whose zones have not moved yet (readWindowsThermal). The panel draws a
|
|
187
|
+
* section for any object it is handed, and an empty one is not a reading. */
|
|
185
188
|
export function thermalSnapshot() {
|
|
186
189
|
if (!thermal) return null;
|
|
187
|
-
// The raw kernel count
|
|
188
|
-
// reading: the route answers what the panel draws.
|
|
189
|
-
const { throttleCount, ...shown } = thermal;
|
|
190
|
+
// The raw kernel count and the zones' first values are the sampler's, for
|
|
191
|
+
// the next reading, and not readings: the route answers what the panel draws.
|
|
192
|
+
const { throttleCount, zonesSeen, ...shown } = thermal;
|
|
193
|
+
if (!shown.celsius?.length && !shown.throttle) return null;
|
|
190
194
|
return { ...shown, heldBack: heldBackSoFar() };
|
|
191
195
|
}
|
|
192
196
|
|
|
@@ -233,13 +233,36 @@ const USAGE_TOTAL_FIELDS = [...USAGE_BLOCK_FIELDS, ...CACHE_SPLIT_FIELDS];
|
|
|
233
233
|
* bucket and not an addition to it. What was charged is added to `charged`,
|
|
234
234
|
* so a request's bill can be taken back when its final record replaces it. */
|
|
235
235
|
function chargeUsage(state, bucket, blob, fields, speedBucket, charged) {
|
|
236
|
-
for (const k of fields)
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
}
|
|
236
|
+
for (const k of fields) chargeTokens(state, bucket, speedBucket, charged, k, grabUsageField(blob, k));
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/** Charge a request's record from its parsed `message.usage`, the same way.
|
|
240
|
+
*
|
|
241
|
+
* Not off the text, as chargeUsage reads a line: USAGE_BLOCK_RE's `[^}]+`
|
|
242
|
+
* stops at the first `}`, and a subagent's streaming snapshot writes its
|
|
243
|
+
* `cache_creation` sub-object before `output_tokens` (all 49,046 snapshots on
|
|
244
|
+
* one machine), so the output a snapshot had streamed read as 0 until the
|
|
245
|
+
* final record came, and stayed 0 for a request that never finished. A
|
|
246
|
+
* request's record names its model, so the line is parsed already; the
|
|
247
|
+
* Projects report reads the same object (countersFrom in account-projects.mjs). */
|
|
248
|
+
function chargeRequestUsage(state, bucket, usage, speedBucket, charged) {
|
|
249
|
+
const split = usage?.cache_creation;
|
|
250
|
+
for (const k of USAGE_BLOCK_FIELDS) chargeTokens(state, bucket, speedBucket, charged, k, tokenCount(usage?.[k]));
|
|
251
|
+
for (const k of CACHE_SPLIT_FIELDS) chargeTokens(state, bucket, speedBucket, charged, k, tokenCount(split?.[k]));
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/** Add `n` tokens of counter `k` to every place one charge goes. */
|
|
255
|
+
function chargeTokens(state, bucket, speedBucket, charged, k, n) {
|
|
256
|
+
state.usage[k] += n;
|
|
257
|
+
if (bucket) bucket[k] += n;
|
|
258
|
+
if (speedBucket) speedBucket[k] += n;
|
|
259
|
+
charged[k] += n;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/** A usage counter as a token count: a whole number of tokens, or 0 for
|
|
263
|
+
* anything else, as USAGE_FIELD_RE reads one off the text. */
|
|
264
|
+
function tokenCount(v) {
|
|
265
|
+
return typeof v === "number" && Number.isFinite(v) && v > 0 ? Math.trunc(v) : 0;
|
|
243
266
|
}
|
|
244
267
|
|
|
245
268
|
/** Add one bucket's counters, and its speed shares, into `dst`. */
|
|
@@ -329,9 +352,10 @@ function newTranscriptState() {
|
|
|
329
352
|
// mirror of it. Costs no extra read: `message.model` and the `"usage"` block
|
|
330
353
|
// are on the same line, and this pass already parses both.
|
|
331
354
|
usageByModel: {},
|
|
332
|
-
//
|
|
333
|
-
//
|
|
334
|
-
|
|
355
|
+
// What this file has read of its requests: the newest request record, and
|
|
356
|
+
// the request billed from a snapshot whose final record has not been read
|
|
357
|
+
// yet, with what it was charged — see requestBilling.
|
|
358
|
+
requests: newRequestLedger(),
|
|
335
359
|
ctx: newContextBreakdown(),
|
|
336
360
|
};
|
|
337
361
|
}
|
|
@@ -469,24 +493,82 @@ const SKIP_LINE = "skip";
|
|
|
469
493
|
* the request it is waiting on, so the two can arrive in different passes,
|
|
470
494
|
* which for a live subagent is the usual case.
|
|
471
495
|
*
|
|
496
|
+
* A resumed subagent writes its history into its file again before the new
|
|
497
|
+
* turn: the same records, with the same `uuid`, `requestId` and `timestamp`,
|
|
498
|
+
* usually byte for byte. Billed as they came, every request in that history was
|
|
499
|
+
* charged once more per replay — on one machine 98 requests in two files, 821
|
|
500
|
+
* times over. A restated record is told apart by its timestamp, which is older
|
|
501
|
+
* than the newest request record the file has already read: nothing Claude
|
|
502
|
+
* Code writes fresh is, none of 162,790 request records on that machine. Telling
|
|
503
|
+
* them apart by request id would need every id a file has billed, kept for as
|
|
504
|
+
* long as the file can grow and persisted with the Projects tally; the newest
|
|
505
|
+
* timestamp is one number. See restatesEarlierRecord.
|
|
506
|
+
*
|
|
507
|
+
* A file that starts in the middle of a request — a fork's file opens with its
|
|
508
|
+
* parent's turn, a resumed agent's with the one it stopped in — has no block 0
|
|
509
|
+
* for it, and that request is not billed here. It is not this file's: on that
|
|
510
|
+
* machine 36 of 38 such openings were requests the parent's or the earlier
|
|
511
|
+
* agent's transcript bills in full, block 0 and final record. The other two
|
|
512
|
+
* were workflow agents whose transcript continued in a second file mid-request,
|
|
513
|
+
* the snapshot billed at the end of the first and the final record opening the
|
|
514
|
+
* second; billing that record here would need to know what the other file
|
|
515
|
+
* charged for it.
|
|
516
|
+
*
|
|
472
517
|
* BILL_LINE a record from before Claude Code marked its blocks, billed
|
|
473
518
|
* line by line as it always was
|
|
474
519
|
* BILL_REQUEST the first record of a request
|
|
475
520
|
* REBILL_REQUEST a later record of the request still streaming: it replaces
|
|
476
521
|
* that request's bill
|
|
477
522
|
* SKIP_LINE a later block of a request already billed from its final
|
|
478
|
-
* record, which restates it
|
|
523
|
+
* record, which restates it, or a record restated by a replay
|
|
479
524
|
*
|
|
480
|
-
*
|
|
481
|
-
*
|
|
525
|
+
* `ledger` is what the file has read of its requests so far (newRequestLedger),
|
|
526
|
+
* or null for none; noteRequestRecord adds each record to it. The Projects
|
|
527
|
+
* report (account-projects.mjs) bills its own pass of the same lines by this
|
|
528
|
+
* rule.
|
|
482
529
|
*/
|
|
483
|
-
function requestBilling(record,
|
|
530
|
+
function requestBilling(record, ledger) {
|
|
484
531
|
if (record?.apiBlockIndex === undefined) return BILL_LINE;
|
|
532
|
+
if (restatesEarlierRecord(record, ledger)) return SKIP_LINE;
|
|
485
533
|
const id = requestIdOf(record);
|
|
486
|
-
if (id !== null && id ===
|
|
534
|
+
if (id !== null && id === ledger?.streaming?.id && isUsageRecord(record)) return REBILL_REQUEST;
|
|
487
535
|
return record.apiBlockIndex === 0 ? BILL_REQUEST : SKIP_LINE;
|
|
488
536
|
}
|
|
489
537
|
|
|
538
|
+
/** What one file has read of its requests, for requestBilling: when the newest
|
|
539
|
+
* request record was written and which request it was, and the request still
|
|
540
|
+
* streaming — `{ id, … }` with whatever its caller needs to take its charge
|
|
541
|
+
* back — or null. Plain data, so the Projects report can persist it. */
|
|
542
|
+
function newRequestLedger() {
|
|
543
|
+
return { newestAt: null, newestId: null, streaming: null };
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
/** Is `record` a copy of one the file has already read, written again by a
|
|
547
|
+
* resumed subagent restating its history? The copy keeps its original
|
|
548
|
+
* timestamp, so it is older than the newest request record read — or, written
|
|
549
|
+
* at that same moment, it is block 0 of the very request that record belongs
|
|
550
|
+
* to, which a request has only one of. A later block at that moment is not a
|
|
551
|
+
* copy: Claude Code writes two blocks of one request within a millisecond
|
|
552
|
+
* often enough (38 times on that machine). A record without a timestamp is
|
|
553
|
+
* never taken for a copy. */
|
|
554
|
+
function restatesEarlierRecord(record, ledger) {
|
|
555
|
+
const at = Date.parse(record.timestamp);
|
|
556
|
+
const newest = ledger?.newestAt ?? null;
|
|
557
|
+
if (newest === null || !Number.isFinite(at)) return false;
|
|
558
|
+
if (at !== newest) return at < newest;
|
|
559
|
+
const id = requestIdOf(record);
|
|
560
|
+
return record.apiBlockIndex === 0 && id !== null && id === ledger.newestId;
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
/** Add a request record to the file's ledger, once it has been billed by
|
|
564
|
+
* requestBilling — skipped or not, it is a record the file has read. */
|
|
565
|
+
function noteRequestRecord(ledger, record) {
|
|
566
|
+
const at = Date.parse(record.timestamp);
|
|
567
|
+
if (!Number.isFinite(at) || (ledger.newestAt !== null && at < ledger.newestAt)) return;
|
|
568
|
+
ledger.newestAt = at;
|
|
569
|
+
ledger.newestId = requestIdOf(record);
|
|
570
|
+
}
|
|
571
|
+
|
|
490
572
|
/** The request a record belongs to while its final record has yet to come —
|
|
491
573
|
* the id requestBilling is next asked with — or null once it is final. */
|
|
492
574
|
function streamingRequestId(record) {
|
|
@@ -509,10 +591,11 @@ function isUsageRecord(record) {
|
|
|
509
591
|
* or null when it names no model. */
|
|
510
592
|
function foldUsageLine(state, line, record) {
|
|
511
593
|
// A request is billed once, from its final record — see requestBilling.
|
|
512
|
-
const
|
|
513
|
-
const billing = requestBilling(record,
|
|
594
|
+
const ledger = state.requests;
|
|
595
|
+
const billing = requestBilling(record, ledger);
|
|
596
|
+
if (billing !== BILL_LINE) noteRequestRecord(ledger, record);
|
|
514
597
|
if (billing === SKIP_LINE) return;
|
|
515
|
-
if (billing === REBILL_REQUEST) refundUsage(state, streaming);
|
|
598
|
+
if (billing === REBILL_REQUEST) refundUsage(state, ledger.streaming);
|
|
516
599
|
// Usage totals sum every block in the file, resets included — every block the
|
|
517
600
|
// model was actually billed for, which is why the `toolUseResult` tail is cut
|
|
518
601
|
// off first (see billedUsageText) — and are summed a second time into the
|
|
@@ -532,21 +615,24 @@ function foldUsageLine(state, line, record) {
|
|
|
532
615
|
// text: a split that read a wider stretch of the line than the total it splits
|
|
533
616
|
// would re-introduce #685's double count on one side of the arithmetic only,
|
|
534
617
|
// and the two would stop summing to each other.
|
|
535
|
-
const billed = billedUsageText(line);
|
|
536
618
|
const bucket = usageBucketFor(state, state.lastModel);
|
|
537
619
|
const speedBucket = speedBucketFor(bucket, billedSpeed(record));
|
|
538
620
|
const charged = newUsageTotals();
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
621
|
+
if (billing === BILL_LINE) {
|
|
622
|
+
const billed = billedUsageText(line);
|
|
623
|
+
for (const m of billed.matchAll(USAGE_BLOCK_RE)) {
|
|
624
|
+
chargeUsage(state, bucket, m[1], USAGE_BLOCK_FIELDS, speedBucket, charged);
|
|
625
|
+
}
|
|
626
|
+
for (const m of billed.replace(ITERATIONS_ARRAY_RE, "").matchAll(CACHE_CREATION_BLOCK_RE)) {
|
|
627
|
+
chargeUsage(state, bucket, m[1], CACHE_SPLIT_FIELDS, speedBucket, charged);
|
|
628
|
+
}
|
|
629
|
+
return;
|
|
544
630
|
}
|
|
545
|
-
|
|
631
|
+
chargeRequestUsage(state, bucket, record.message?.usage, speedBucket, charged);
|
|
546
632
|
// What this request was charged, and where, kept until its final record
|
|
547
633
|
// replaces it.
|
|
548
634
|
const id = streamingRequestId(record);
|
|
549
|
-
|
|
635
|
+
ledger.streaming = id === null ? null : { id, bucket, speedBucket, charged };
|
|
550
636
|
}
|
|
551
637
|
|
|
552
638
|
/** Take back what a request was charged, from every counter it was charged
|
|
@@ -741,6 +827,6 @@ function scanTranscript(path) {
|
|
|
741
827
|
// above reads exactly as it did where it came from.
|
|
742
828
|
export {
|
|
743
829
|
scanTranscript, newUsageTotals, hasSpend, mergeUsageByModel, copyUsageBucket,
|
|
744
|
-
requestBilling,
|
|
745
|
-
billedSpeed, MAX_SPEEDS_PER_MODEL, UNRECOGNISED_SPEED,
|
|
830
|
+
requestBilling, newRequestLedger, noteRequestRecord, streamingRequestId,
|
|
831
|
+
BILL_LINE, REBILL_REQUEST, SKIP_LINE, billedSpeed, MAX_SPEEDS_PER_MODEL, UNRECOGNISED_SPEED,
|
|
746
832
|
};
|