tledger 0.3.0 → 0.4.0

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.
@@ -0,0 +1,848 @@
1
+ // Named SVG sections for the standalone cache report. Each builder is pure
2
+ // with respect to its context and returns markup in render order.
3
+
4
+ import {
5
+ buildCacheReportData,
6
+ combinedModelRows,
7
+ priorPeriodSummary,
8
+ } from "./token-ledger-cache-data.mjs";
9
+ import {
10
+ compact,
11
+ escapeXml,
12
+ shiftCalendarDate,
13
+ svgRect,
14
+ svgText,
15
+ textWidth,
16
+ truncateText,
17
+ CACHE_IMAGE_COLORS as COLORS,
18
+ TREND_IMAGE_MODEL_COLORS,
19
+ } from "./token-ledger-image-primitives.mjs";
20
+ import { historyScopeLabel } from "../lib/token-ledger-collection.mjs";
21
+ import { sourceStatusLine } from "./token-ledger-source-status.mjs";
22
+ import { incompleteSourceWarning } from "./token-ledger-terminal.mjs";
23
+
24
+ function percent(value) {
25
+ if (!Number.isFinite(value)) return "—";
26
+ return `${value.toFixed(value >= 10 ? 1 : 2)}%`;
27
+ }
28
+
29
+ function calendarDate(dateString) {
30
+ return new Date(`${dateString}T00:00:00.000Z`);
31
+ }
32
+
33
+ function shortDateLabel(dateString) {
34
+ return new Intl.DateTimeFormat("en-US", {
35
+ timeZone: "UTC",
36
+ month: "short",
37
+ day: "numeric",
38
+ }).format(calendarDate(dateString));
39
+ }
40
+
41
+ function weekdayLabel(dateString) {
42
+ return new Intl.DateTimeFormat("en-US", {
43
+ timeZone: "UTC",
44
+ weekday: "short",
45
+ }).format(calendarDate(dateString)).toUpperCase();
46
+ }
47
+
48
+ function periodLabel(bounds) {
49
+ const startYear = bounds.startDateString.slice(0, 4);
50
+ const endYear = bounds.endDateString.slice(0, 4);
51
+ const start = shortDateLabel(bounds.startDateString);
52
+ const end = shortDateLabel(bounds.endDateString);
53
+ return startYear === endYear
54
+ ? `${start} – ${end}, ${endYear}`
55
+ : `${start}, ${startYear} – ${end}, ${endYear}`;
56
+ }
57
+
58
+ function wrapFooterText(value, maxWidth, size = 12) {
59
+ const words = String(value).split(/\s+/).filter(Boolean);
60
+ const lines = [];
61
+ let current = "";
62
+ for (const word of words) {
63
+ const candidate = current ? `${current} ${word}` : word;
64
+ if (current && textWidth(candidate, size) > maxWidth) {
65
+ lines.push(current);
66
+ current = word;
67
+ } else {
68
+ current = candidate;
69
+ }
70
+ }
71
+ if (current) lines.push(current);
72
+ return lines;
73
+ }
74
+
75
+ function footerQualifierLines(value, fallbackLines, maxWidth) {
76
+ if (textWidth(value, 12) <= maxWidth) return [value];
77
+ return fallbackLines.flatMap((line) => wrapFooterText(line, maxWidth));
78
+ }
79
+
80
+ function binDateLabel(bin) {
81
+ const finalDate = shiftCalendarDate(bin.endDateString, -1);
82
+ if (finalDate === bin.startDateString) return shortDateLabel(bin.startDateString);
83
+ return `${shortDateLabel(bin.startDateString)}–${shortDateLabel(finalDate)}`;
84
+ }
85
+
86
+ function primitiveString(value) {
87
+ try {
88
+ const text = String.prototype.valueOf.call(value);
89
+ return text === value ? text : null;
90
+ } catch {
91
+ return null;
92
+ }
93
+ }
94
+
95
+ function finiteTimestamp(value) {
96
+ const text = primitiveString(value);
97
+ if (text === null) return null;
98
+ const timestampMs = Date.parse(text);
99
+ return Number.isFinite(timestampMs) ? timestampMs : null;
100
+ }
101
+
102
+ function generatedAtLabel(value, timeZone) {
103
+ const timestampMs = finiteTimestamp(value);
104
+ if (timestampMs === null) return "unknown";
105
+ return new Intl.DateTimeFormat("en-US", {
106
+ timeZone,
107
+ month: "short",
108
+ day: "numeric",
109
+ year: "numeric",
110
+ hour: "numeric",
111
+ minute: "2-digit",
112
+ }).format(new Date(timestampMs));
113
+ }
114
+
115
+ function modelColor(model) {
116
+ return TREND_IMAGE_MODEL_COLORS[model] ?? TREND_IMAGE_MODEL_COLORS.Other;
117
+ }
118
+
119
+ function periodComparison(currentRate, priorRate) {
120
+ if (!Number.isFinite(currentRate)) return "No current-period input";
121
+ if (!Number.isFinite(priorRate)) return "No prior-period input";
122
+ const delta = currentRate - priorRate;
123
+ const deltaLabel = Math.abs(delta) < 0.05
124
+ ? "flat"
125
+ : `${delta >= 0 ? "+" : "−"}${Math.abs(delta).toFixed(1)} pp`;
126
+ return `Prior ${percent(priorRate)} · ${deltaLabel}`;
127
+ }
128
+
129
+ const AXIS_LABEL_GAP = 12;
130
+
131
+ function labelEvery(bins, slotWidth) {
132
+ if (bins.length === 0 || slotWidth <= 0) return 1;
133
+ const widestLabel = Math.max(
134
+ ...bins.map((bin) => textWidth(binDateLabel(bin), 13)),
135
+ );
136
+ return Math.max(1, Math.ceil((widestLabel + AXIS_LABEL_GAP) / slotWidth));
137
+ }
138
+
139
+ function pushLegendItem(elements, { x, y, color, label, line = false }) {
140
+ if (line) {
141
+ elements.push(
142
+ `<line x1="${x}" y1="${y - 4}" x2="${x + 22}" y2="${y - 4}" stroke="${color}" stroke-width="2" stroke-dasharray="5 4"/>`,
143
+ );
144
+ } else {
145
+ elements.push(svgRect(x, y - 12, 14, 11, { rx: 2, fill: color }));
146
+ }
147
+ elements.push(svgText({
148
+ x: x + (line ? 31 : 23),
149
+ y,
150
+ value: label,
151
+ fill: COLORS.secondary,
152
+ size: 13,
153
+ }));
154
+ }
155
+
156
+ function createCacheReportContext({
157
+ snapshot,
158
+ bounds,
159
+ days,
160
+ options,
161
+ analysis,
162
+ sourceStatus,
163
+ }) {
164
+ const width = Math.max(900, Math.min(2_400, Number(options.imageWidth) || 1_280));
165
+ const outer = 32;
166
+ const contentRight = width - outer;
167
+ const plotLeft = 82;
168
+ const plotRight = width - 94;
169
+ const plotWidth = plotRight - plotLeft;
170
+ const data = buildCacheReportData(
171
+ snapshot,
172
+ bounds,
173
+ days,
174
+ plotWidth,
175
+ null,
176
+ analysis?.currentEvents ?? null,
177
+ );
178
+ const prior = priorPeriodSummary(
179
+ snapshot,
180
+ bounds,
181
+ days,
182
+ analysis?.priorEvents ?? null,
183
+ );
184
+ const models = combinedModelRows(data.models);
185
+ const columnWidth = (contentRight - outer) / 3;
186
+ const qualifierWidth = columnWidth - 22;
187
+ const measurementCounts = `${data.detailedEventCount.toLocaleString("en-US")} of ${data.eventCount.toLocaleString("en-US")} calls`;
188
+ const measurementQualifier = `${measurementCounts} include component detail`;
189
+ const dataAsOfQualifier = `${bounds.timeZone} · ${days}-day calendar window`;
190
+ const footerItems = [
191
+ {
192
+ label: "RATE DEFINITION",
193
+ value: "cached input ÷ measured input",
194
+ qualifiers: ["weighted by input tokens, not daily averages"],
195
+ },
196
+ {
197
+ label: "MEASUREMENT COVERAGE",
198
+ value: Number.isFinite(data.measurementCoveragePercent)
199
+ ? `${percent(data.measurementCoveragePercent)} of ${data.totalTokens > 0 ? "token volume" : "calls"}`
200
+ : "unknown",
201
+ qualifiers: footerQualifierLines(
202
+ measurementQualifier,
203
+ [measurementCounts, "include component detail"],
204
+ qualifierWidth,
205
+ ),
206
+ },
207
+ {
208
+ label: "DATA AS OF",
209
+ value: generatedAtLabel(snapshot.generatedAt, bounds.timeZone),
210
+ qualifiers: footerQualifierLines(
211
+ dataAsOfQualifier,
212
+ [bounds.timeZone, `${days}-day calendar window`],
213
+ qualifierWidth,
214
+ ),
215
+ },
216
+ ];
217
+ const qualifierLineCount = Math.max(
218
+ ...footerItems.map((item) => item.qualifiers.length),
219
+ );
220
+ const sourceWarning = incompleteSourceWarning(snapshot);
221
+ const headerOffset = sourceWarning ? 24 : 0;
222
+ const headerTitle = "TOKEN LEDGER · CACHE REPORT";
223
+ const history = historyScopeLabel(snapshot);
224
+ const headerMetadata = [periodLabel(bounds), bounds.timeZone, history]
225
+ .filter(Boolean)
226
+ .join(" · ");
227
+ const headerTitleWidth = textWidth(headerTitle, 27, 800) -
228
+ 0.27 * (headerTitle.length - 1);
229
+ const headerMetadataFits = headerTitleWidth + textWidth(headerMetadata, 14) + 24 <=
230
+ contentRight - outer;
231
+ const renderedHeaderMetadata = textWidth(headerMetadata, 14) <= contentRight - outer
232
+ ? headerMetadata
233
+ : truncateText(headerMetadata, contentRight - outer, 14);
234
+ const ratePlotTop = 320 + headerOffset;
235
+ const ratePlotHeight = 270;
236
+ const ratePlotBottom = ratePlotTop + ratePlotHeight;
237
+ const volumeTop = 635 + headerOffset;
238
+ const volumeHeight = 55;
239
+ const volumeBottom = volumeTop + volumeHeight;
240
+ const legendBaseline = 770 + headerOffset;
241
+ const modelRuleY = 800 + headerOffset;
242
+ const modelHeaderBaseline = 830 + headerOffset;
243
+ const modelRowsTop = 858 + headerOffset;
244
+ const modelRowHeight = 44;
245
+ const modelRowCount = Math.max(1, models.length);
246
+ const footerRuleY = modelRowsTop + modelRowCount * modelRowHeight + 28;
247
+ const height = footerRuleY + 118 + Math.max(0, qualifierLineCount - 2) * 16;
248
+ return {
249
+ snapshot,
250
+ bounds,
251
+ days,
252
+ width,
253
+ outer,
254
+ contentRight,
255
+ plotLeft,
256
+ plotRight,
257
+ plotWidth,
258
+ data,
259
+ prior,
260
+ models,
261
+ columnWidth,
262
+ footerItems,
263
+ qualifierLineCount,
264
+ headerTitle,
265
+ headerMetadata: renderedHeaderMetadata,
266
+ headerMetadataFits,
267
+ sourceWarning,
268
+ headerOffset,
269
+ sourceStatus,
270
+ ratePlotTop,
271
+ ratePlotHeight,
272
+ ratePlotBottom,
273
+ volumeTop,
274
+ volumeHeight,
275
+ volumeBottom,
276
+ legendBaseline,
277
+ modelRuleY,
278
+ modelHeaderBaseline,
279
+ modelRowsTop,
280
+ modelRowHeight,
281
+ footerRuleY,
282
+ height,
283
+ };
284
+ }
285
+
286
+ export function buildCacheHeaderSection(context) {
287
+ const {
288
+ days,
289
+ data,
290
+ prior,
291
+ width,
292
+ height,
293
+ outer,
294
+ contentRight,
295
+ headerTitle,
296
+ headerMetadata,
297
+ headerMetadataFits,
298
+ sourceWarning = null,
299
+ headerOffset = 0,
300
+ sourceStatus,
301
+ } = context;
302
+ const elements = [
303
+ `<svg xmlns="http://www.w3.org/2000/svg" width="${width}" height="${height}" viewBox="0 0 ${width} ${height}" role="img" aria-labelledby="cache-title cache-description">`,
304
+ `<title id="cache-title">${escapeXml(`Token Ledger · ${days}-day cache report`)}</title>`,
305
+ `<desc id="cache-description">${escapeXml("Dark cache report with a weighted cached-versus-uncached input split, normalized cache-rate columns with input-volume context, and a secondary model-level cache-rate breakout.")}</desc>`,
306
+ `<rect width="100%" height="100%" fill="${COLORS.background}"/>`,
307
+ svgText({
308
+ x: outer,
309
+ y: 53,
310
+ value: headerTitle,
311
+ size: 27,
312
+ weight: 800,
313
+ spacing: "-0.27",
314
+ }),
315
+ svgText({
316
+ x: contentRight,
317
+ y: headerMetadataFits ? 53 : 77,
318
+ value: headerMetadata,
319
+ fill: COLORS.muted,
320
+ size: 14,
321
+ anchor: "end",
322
+ }),
323
+ svgText({
324
+ x: outer,
325
+ y: 97,
326
+ value: "WEIGHTED INPUT CACHE RATE",
327
+ fill: COLORS.muted,
328
+ size: 12,
329
+ spacing: "1.32",
330
+ }),
331
+ svgText({
332
+ x: contentRight,
333
+ y: 97,
334
+ value: sourceStatusLine(sourceStatus),
335
+ fill: sourceStatus === "verified-current" ? COLORS.secondary : COLORS.weighted,
336
+ size: 12,
337
+ weight: 700,
338
+ anchor: "end",
339
+ spacing: ".72",
340
+ }),
341
+ ];
342
+ if (sourceWarning) {
343
+ elements.push(svgText({
344
+ x: outer,
345
+ y: 116,
346
+ value: sourceWarning,
347
+ fill: COLORS.weighted,
348
+ size: 12,
349
+ weight: 700,
350
+ spacing: ".72",
351
+ }));
352
+ }
353
+ const summaryValue = Number.isFinite(data.rate)
354
+ ? `${percent(data.rate)} cached`
355
+ : "No measured input";
356
+ elements.push(svgText({
357
+ x: outer,
358
+ y: 135 + headerOffset,
359
+ value: summaryValue,
360
+ size: 29,
361
+ weight: 800,
362
+ spacing: "-0.58",
363
+ }));
364
+ elements.push(svgText({
365
+ x: contentRight,
366
+ y: 133 + headerOffset,
367
+ value: periodComparison(data.rate, prior.rate),
368
+ fill: COLORS.secondary,
369
+ size: 14,
370
+ weight: 600,
371
+ anchor: "end",
372
+ }));
373
+ const railTop = 153 + headerOffset;
374
+ const railHeight = 52;
375
+ const railWidth = contentRight - outer;
376
+ elements.push(
377
+ `<defs><clipPath id="cache-coverage-clip">${svgRect(outer, railTop, railWidth, railHeight, { rx: 8 })}</clipPath></defs>`,
378
+ );
379
+ elements.push(`<g clip-path="url(#cache-coverage-clip)">`);
380
+ elements.push(svgRect(outer, railTop, railWidth, railHeight, {
381
+ fill: Number.isFinite(data.rate) ? COLORS.uncached : COLORS.track,
382
+ }));
383
+ const cachedRailWidth = Number.isFinite(data.rate)
384
+ ? railWidth * (data.rate / 100)
385
+ : 0;
386
+ if (cachedRailWidth > 0) {
387
+ elements.push(svgRect(outer, railTop, cachedRailWidth, railHeight, {
388
+ fill: COLORS.cached,
389
+ }));
390
+ }
391
+ elements.push("</g>");
392
+ if (cachedRailWidth >= 150) {
393
+ elements.push(svgText({
394
+ x: outer + cachedRailWidth / 2,
395
+ y: railTop + 32,
396
+ value: `CACHED · ${compact(data.cachedInputTokens)}`,
397
+ fill: COLORS.background,
398
+ size: 13,
399
+ weight: 800,
400
+ anchor: "middle",
401
+ spacing: ".65",
402
+ }));
403
+ }
404
+ const uncachedRailWidth = railWidth - cachedRailWidth;
405
+ if (Number.isFinite(data.rate) && uncachedRailWidth >= 150) {
406
+ elements.push(svgText({
407
+ x: outer + cachedRailWidth + uncachedRailWidth / 2,
408
+ y: railTop + 32,
409
+ value: `UNCACHED · ${compact(data.uncachedInputTokens)}`,
410
+ fill: COLORS.background,
411
+ size: 13,
412
+ weight: 800,
413
+ anchor: "middle",
414
+ spacing: ".65",
415
+ }));
416
+ }
417
+ elements.push(svgText({
418
+ x: outer,
419
+ y: 230 + headerOffset,
420
+ value: Number.isFinite(data.rate)
421
+ ? `${compact(data.cachedInputTokens)} cached · ${compact(data.uncachedInputTokens)} uncached · ${compact(data.inputTokens)} total input`
422
+ : "No events with a usable input-token breakdown in this range",
423
+ fill: COLORS.secondary,
424
+ size: 14,
425
+ }));
426
+ elements.push(svgText({
427
+ x: contentRight,
428
+ y: 230 + headerOffset,
429
+ value: `${data.inputEventCount.toLocaleString("en-US")} measured input-bearing ${data.inputEventCount === 1 ? "call" : "calls"}`,
430
+ fill: COLORS.muted,
431
+ size: 13,
432
+ anchor: "end",
433
+ }));
434
+ elements.push(svgText({
435
+ x: outer,
436
+ y: 283 + headerOffset,
437
+ value: "CACHE RATE BY PERIOD",
438
+ fill: COLORS.muted,
439
+ size: 12,
440
+ spacing: "1.32",
441
+ }));
442
+ elements.push(svgText({
443
+ x: contentRight,
444
+ y: 283 + headerOffset,
445
+ value: "Rate bars are normalized to 100% · input volume below",
446
+ fill: COLORS.muted,
447
+ size: 12.5,
448
+ anchor: "end",
449
+ }));
450
+ return elements;
451
+ }
452
+
453
+ export function buildCacheRateSection(context) {
454
+ const {
455
+ data,
456
+ outer,
457
+ plotLeft,
458
+ plotRight,
459
+ ratePlotTop,
460
+ ratePlotHeight,
461
+ ratePlotBottom,
462
+ plotWidth,
463
+ volumeTop,
464
+ volumeHeight,
465
+ volumeBottom,
466
+ legendBaseline,
467
+ headerOffset = 0,
468
+ } = context;
469
+ const elements = [];
470
+ for (const value of [100, 75, 50, 25, 0]) {
471
+ const y = ratePlotBottom - (value / 100) * ratePlotHeight;
472
+ elements.push(
473
+ `<line x1="${plotLeft}" y1="${y.toFixed(2)}" x2="${plotRight}" y2="${y.toFixed(2)}" stroke="${value === 0 ? COLORS.baseline : COLORS.grid}" stroke-width="1"/>`,
474
+ );
475
+ elements.push(svgText({
476
+ x: plotLeft - 13,
477
+ y: y + 4,
478
+ value: `${value}%`,
479
+ fill: COLORS.muted,
480
+ size: 12.5,
481
+ anchor: "end",
482
+ mono: true,
483
+ }));
484
+ }
485
+ const slotWidth = plotWidth / data.binCount;
486
+ const barWidth = Math.min(72, Math.max(16, slotWidth * 0.58));
487
+ const observedMaxInput = Math.max(
488
+ 0,
489
+ ...data.bins.map((bin) => bin.inputTokens),
490
+ );
491
+ const maxInput = Math.max(1, observedMaxInput);
492
+ const dateStep = labelEvery(data.bins, slotWidth);
493
+ data.bins.forEach((bin, index) => {
494
+ const centerX = plotLeft + (index + 0.5) * slotWidth;
495
+ const barX = centerX - barWidth / 2;
496
+ if (Number.isFinite(bin.rate)) {
497
+ elements.push(svgRect(barX, ratePlotTop, barWidth, ratePlotHeight, {
498
+ rx: 4,
499
+ fill: COLORS.uncached,
500
+ opacity: ".88",
501
+ }));
502
+ const cachedHeight = ratePlotHeight * (bin.rate / 100);
503
+ if (cachedHeight > 0) {
504
+ elements.push(svgRect(
505
+ barX,
506
+ ratePlotBottom - cachedHeight,
507
+ barWidth,
508
+ cachedHeight,
509
+ { rx: 2, fill: COLORS.cached },
510
+ ));
511
+ }
512
+ if (slotWidth >= 50 && data.binCount <= 20) {
513
+ elements.push(svgText({
514
+ x: centerX,
515
+ y: ratePlotTop - 10,
516
+ value: percent(bin.rate),
517
+ fill: COLORS.secondary,
518
+ size: 12.5,
519
+ weight: 700,
520
+ anchor: "middle",
521
+ mono: true,
522
+ }));
523
+ }
524
+ } else {
525
+ elements.push(svgRect(barX, ratePlotTop, barWidth, ratePlotHeight, {
526
+ rx: 4,
527
+ fill: COLORS.track,
528
+ stroke: COLORS.baseline,
529
+ "stroke-width": 1,
530
+ }));
531
+ elements.push(
532
+ `<line x1="${barX + 5}" y1="${ratePlotTop + ratePlotHeight / 2}" x2="${barX + barWidth - 5}" y2="${ratePlotTop + ratePlotHeight / 2}" stroke="${COLORS.muted}" stroke-width="1"/>`,
533
+ );
534
+ }
535
+ const volumeBarHeight = (bin.inputTokens / maxInput) * volumeHeight;
536
+ if (volumeBarHeight > 0) {
537
+ elements.push(svgRect(
538
+ barX,
539
+ volumeBottom - volumeBarHeight,
540
+ barWidth,
541
+ volumeBarHeight,
542
+ { rx: 2, fill: COLORS.volume },
543
+ ));
544
+ }
545
+ const finalBinIndex = data.binCount - 1;
546
+ const showDateLabel = index === finalBinIndex || (
547
+ index % dateStep === 0 && finalBinIndex - index >= dateStep
548
+ );
549
+ if (showDateLabel) {
550
+ const daily = data.binSize === 1;
551
+ if (daily) {
552
+ elements.push(svgText({
553
+ x: centerX,
554
+ y: 718 + headerOffset,
555
+ value: weekdayLabel(bin.startDateString),
556
+ fill: COLORS.muted,
557
+ size: 11.5,
558
+ anchor: "middle",
559
+ spacing: "1.15",
560
+ }));
561
+ }
562
+ elements.push(svgText({
563
+ x: centerX,
564
+ y: (daily ? 739 : 730) + headerOffset,
565
+ value: binDateLabel(bin),
566
+ fill: COLORS.secondary,
567
+ size: 13,
568
+ anchor: "middle",
569
+ }));
570
+ }
571
+ });
572
+ elements.push(svgText({
573
+ x: plotLeft - 13,
574
+ y: volumeTop + 4,
575
+ value: observedMaxInput > 0 ? compact(observedMaxInput) : "—",
576
+ fill: COLORS.muted,
577
+ size: 11.5,
578
+ anchor: "end",
579
+ mono: true,
580
+ }));
581
+ elements.push(svgText({
582
+ x: plotLeft - 13,
583
+ y: volumeBottom + 4,
584
+ value: "0",
585
+ fill: COLORS.muted,
586
+ size: 11.5,
587
+ anchor: "end",
588
+ mono: true,
589
+ }));
590
+ elements.push(svgText({
591
+ x: outer,
592
+ y: volumeTop - 10,
593
+ value: "INPUT",
594
+ fill: COLORS.muted,
595
+ size: 10.5,
596
+ spacing: "1.1",
597
+ }));
598
+ if (Number.isFinite(data.rate)) {
599
+ const lineY = ratePlotBottom - (data.rate / 100) * ratePlotHeight;
600
+ elements.push(
601
+ `<line x1="${plotLeft}" y1="${lineY.toFixed(2)}" x2="${plotRight}" y2="${lineY.toFixed(2)}" stroke="${COLORS.weighted}" stroke-width="1.6" stroke-dasharray="6 5"/>`,
602
+ );
603
+ elements.push(svgText({
604
+ x: plotRight + 8,
605
+ y: lineY + 4,
606
+ value: percent(data.rate),
607
+ fill: COLORS.weighted,
608
+ size: 11.5,
609
+ weight: 700,
610
+ mono: true,
611
+ }));
612
+ }
613
+ pushLegendItem(elements, {
614
+ x: outer,
615
+ y: legendBaseline,
616
+ color: COLORS.cached,
617
+ label: "Cached input",
618
+ });
619
+ pushLegendItem(elements, {
620
+ x: outer + 150,
621
+ y: legendBaseline,
622
+ color: COLORS.uncached,
623
+ label: "Uncached input",
624
+ });
625
+ pushLegendItem(elements, {
626
+ x: outer + 320,
627
+ y: legendBaseline,
628
+ color: COLORS.volume,
629
+ label: "Input volume",
630
+ });
631
+ pushLegendItem(elements, {
632
+ x: outer + 465,
633
+ y: legendBaseline,
634
+ color: COLORS.weighted,
635
+ label: "Weighted rate",
636
+ line: true,
637
+ });
638
+ return elements;
639
+ }
640
+
641
+ export function buildCacheModelSection(context) {
642
+ const {
643
+ data,
644
+ models,
645
+ width,
646
+ outer,
647
+ contentRight,
648
+ modelRuleY,
649
+ modelHeaderBaseline,
650
+ modelRowsTop,
651
+ modelRowHeight,
652
+ } = context;
653
+ const elements = [
654
+ `<line x1="${outer}" y1="${modelRuleY}" x2="${contentRight}" y2="${modelRuleY}" stroke="${COLORS.rule}" stroke-width="1"/>`,
655
+ svgText({
656
+ x: outer,
657
+ y: modelHeaderBaseline,
658
+ value: "MODEL",
659
+ fill: COLORS.muted,
660
+ size: 11,
661
+ spacing: "1.1",
662
+ }),
663
+ ];
664
+ const rateRight = outer + 170;
665
+ const modelBarLeft = outer + 188;
666
+ const modelBarRight = width - 330;
667
+ const modelBarWidth = modelBarRight - modelBarLeft;
668
+ const inputRight = width - 135;
669
+ const shareRight = contentRight;
670
+ elements.push(svgText({
671
+ x: rateRight,
672
+ y: modelHeaderBaseline,
673
+ value: "CACHE RATE",
674
+ fill: COLORS.muted,
675
+ size: 11,
676
+ anchor: "end",
677
+ spacing: "1.1",
678
+ }));
679
+ elements.push(svgText({
680
+ x: modelBarLeft,
681
+ y: modelHeaderBaseline,
682
+ value: "CACHED / UNCACHED INPUT",
683
+ fill: COLORS.muted,
684
+ size: 11,
685
+ spacing: "1.1",
686
+ }));
687
+ elements.push(svgText({
688
+ x: inputRight,
689
+ y: modelHeaderBaseline,
690
+ value: "INPUT",
691
+ fill: COLORS.muted,
692
+ size: 11,
693
+ anchor: "end",
694
+ spacing: "1.1",
695
+ }));
696
+ elements.push(svgText({
697
+ x: shareRight,
698
+ y: modelHeaderBaseline,
699
+ value: "SHARE",
700
+ fill: COLORS.muted,
701
+ size: 11,
702
+ anchor: "end",
703
+ spacing: "1.1",
704
+ }));
705
+ if (models.length === 0) {
706
+ elements.push(svgText({
707
+ x: outer,
708
+ y: modelRowsTop + 27,
709
+ value: "No measured input tokens to break out by model.",
710
+ fill: COLORS.secondary,
711
+ size: 14,
712
+ }));
713
+ }
714
+ models.forEach((model, index) => {
715
+ const centerY = modelRowsTop + index * modelRowHeight + 23;
716
+ const modelShare = data.inputTokens > 0
717
+ ? (model.inputTokens / data.inputTokens) * 100
718
+ : 0;
719
+ elements.push(
720
+ `<circle cx="${outer + 5}" cy="${centerY - 4}" r="4.5" fill="${modelColor(model.model)}"/>`,
721
+ );
722
+ elements.push(svgText({
723
+ x: outer + 18,
724
+ y: centerY,
725
+ value: model.model,
726
+ fill: COLORS.ink,
727
+ size: 14,
728
+ weight: 700,
729
+ }));
730
+ elements.push(svgText({
731
+ x: rateRight,
732
+ y: centerY,
733
+ value: percent(model.rate),
734
+ fill: COLORS.secondary,
735
+ size: 13,
736
+ weight: 700,
737
+ anchor: "end",
738
+ mono: true,
739
+ }));
740
+ elements.push(svgRect(modelBarLeft, centerY - 11, modelBarWidth, 11, {
741
+ rx: 3,
742
+ fill: COLORS.uncached,
743
+ opacity: ".7",
744
+ }));
745
+ const fillWidth = modelBarWidth * (model.rate / 100);
746
+ if (fillWidth > 0) {
747
+ elements.push(svgRect(modelBarLeft, centerY - 11, fillWidth, 11, {
748
+ rx: 3,
749
+ fill: COLORS.cached,
750
+ }));
751
+ }
752
+ elements.push(svgText({
753
+ x: inputRight,
754
+ y: centerY,
755
+ value: compact(model.inputTokens),
756
+ fill: COLORS.secondary,
757
+ size: 13,
758
+ weight: 700,
759
+ anchor: "end",
760
+ mono: true,
761
+ }));
762
+ elements.push(svgText({
763
+ x: shareRight,
764
+ y: centerY,
765
+ value: percent(modelShare),
766
+ fill: COLORS.muted,
767
+ size: 13,
768
+ anchor: "end",
769
+ mono: true,
770
+ }));
771
+ });
772
+ return elements;
773
+ }
774
+
775
+ export function buildCacheFooterSection(context) {
776
+ const {
777
+ outer,
778
+ contentRight,
779
+ footerRuleY,
780
+ columnWidth,
781
+ qualifierLineCount,
782
+ footerItems,
783
+ } = context;
784
+ const elements = [
785
+ `<line x1="${outer}" y1="${footerRuleY}" x2="${contentRight}" y2="${footerRuleY}" stroke="${COLORS.rule}" stroke-width="1"/>`,
786
+ ];
787
+ const footerTop = footerRuleY + 22;
788
+ footerItems.forEach((item, index) => {
789
+ const columnX = outer + index * columnWidth;
790
+ const x = index === 0 ? columnX : columnX + 22;
791
+ if (index > 0) {
792
+ elements.push(
793
+ `<line x1="${columnX.toFixed(2)}" y1="${footerTop}" x2="${columnX.toFixed(2)}" y2="${footerTop + 53 + (qualifierLineCount - 1) * 16}" stroke="${COLORS.rule}" stroke-width="1"/>`,
794
+ );
795
+ }
796
+ elements.push(svgText({
797
+ x,
798
+ y: footerTop + 10,
799
+ value: item.label,
800
+ fill: COLORS.muted,
801
+ size: 11,
802
+ spacing: "1.1",
803
+ }));
804
+ elements.push(svgText({
805
+ x,
806
+ y: footerTop + 33,
807
+ value: item.value,
808
+ fill: COLORS.ink,
809
+ size: 15,
810
+ weight: 700,
811
+ }));
812
+ item.qualifiers.forEach((qualifier, qualifierIndex) => {
813
+ elements.push(svgText({
814
+ x,
815
+ y: footerTop + 53 + qualifierIndex * 16,
816
+ value: qualifier,
817
+ fill: COLORS.muted,
818
+ size: 12,
819
+ }));
820
+ });
821
+ });
822
+ return elements;
823
+ }
824
+
825
+ export function renderCacheReportSections({
826
+ snapshot,
827
+ bounds,
828
+ days = bounds.rangeDays ?? 7,
829
+ options = {},
830
+ analysis = null,
831
+ sourceStatus = "unchecked-cache",
832
+ }) {
833
+ const context = createCacheReportContext({
834
+ snapshot,
835
+ bounds,
836
+ days,
837
+ options,
838
+ analysis,
839
+ sourceStatus,
840
+ });
841
+ return [
842
+ ...buildCacheHeaderSection(context),
843
+ ...buildCacheRateSection(context),
844
+ ...buildCacheModelSection(context),
845
+ ...buildCacheFooterSection(context),
846
+ "</svg>",
847
+ ].join("\n");
848
+ }