@cesarus777/opencode-cgp 0.3.1 → 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.
package/dist/cli.js CHANGED
@@ -44,6 +44,12 @@ function isHistoryRead(value) {
44
44
  function isHistoryResult(value) {
45
45
  return valid(value, historyResultSchema);
46
46
  }
47
+ function isAnalyticsRead(value) {
48
+ return valid(value, analyticsReadSchema) && record(value) && typeof value.from === "number" && typeof value.to === "number" && value.from < value.to && value.to - value.from <= 604800000 && !(value.model !== undefined && value.unknownModel === true) && (!(value.modelCursor || value.requestCursor) || typeof value.snapshotId === "string");
49
+ }
50
+ function isAnalyticsResult(value) {
51
+ return valid(value, analyticsResultSchema);
52
+ }
47
53
  function record(x) {
48
54
  return typeof x === "object" && x !== null && !Array.isArray(x);
49
55
  }
@@ -101,7 +107,7 @@ var count, text, enumeration = (...values) => ({ type: "string", enum: values })
101
107
  type: "array",
102
108
  items,
103
109
  maxItems
104
- }), measurement, row, batchFields, readSchema, batchSchema, resultSchema, monitorRpc, nonnegative, performanceSchema, performanceRow, performanceBatchSchema, performanceResultSchema, performanceRpc, chartPointSchema, chartRequestSchema, historyReadSchema, historyResultSchema, historyRpc;
110
+ }), measurement, row, batchFields, readSchema, batchSchema, resultSchema, monitorRpc, nonnegative, performanceSchema, performanceRow, performanceBatchSchema, performanceResultSchema, performanceRpc, chartPointSchema, chartRequestSchema, historyReadSchema, historyResultSchema, historyRpc, modelSummarySchema, analyticsReadSchema, analyticsResultSchema, analyticsRpc;
105
111
  var init_rpc = __esm(() => {
106
112
  count = {
107
113
  type: "integer",
@@ -337,6 +343,123 @@ var init_rpc = __esm(() => {
337
343
  methods: { read: { input: historyReadSchema, output: historyResultSchema } },
338
344
  events: {}
339
345
  });
346
+ modelSummarySchema = object({
347
+ model: text,
348
+ sessions: count,
349
+ completedRequests: count,
350
+ partialRequests: count,
351
+ legacyAttributionRequests: count,
352
+ tpsRequests: count,
353
+ ttftRequests: count,
354
+ excludedTpsRequests: count,
355
+ excludedTtftRequests: count,
356
+ outputTokens: nonnegative,
357
+ durationMs: nonnegative,
358
+ averageTps: nonnegative,
359
+ meanTtftMs: nonnegative,
360
+ medianTtftMs: nonnegative,
361
+ p95TtftMs: nonnegative
362
+ }, [
363
+ "sessions",
364
+ "completedRequests",
365
+ "partialRequests",
366
+ "legacyAttributionRequests",
367
+ "tpsRequests",
368
+ "ttftRequests",
369
+ "excludedTpsRequests",
370
+ "excludedTtftRequests",
371
+ "outputTokens",
372
+ "durationMs"
373
+ ]);
374
+ analyticsReadSchema = object({
375
+ schemaVersion: { const: 1 },
376
+ from: count,
377
+ to: count,
378
+ buckets: { type: "integer", minimum: 1, maximum: 240 },
379
+ agent: text,
380
+ model: text,
381
+ unknownModel: { type: "boolean" },
382
+ sort: enumeration("requests", "tps", "ttft", "identity"),
383
+ snapshotId: text,
384
+ modelCursor: text,
385
+ requestCursor: text
386
+ }, ["schemaVersion", "from", "to", "buckets"]);
387
+ analyticsResultSchema = object({
388
+ schemaVersion: { const: 1 },
389
+ generatedAt: count,
390
+ expiresAt: count,
391
+ snapshotId: text,
392
+ enabled: { type: "boolean" },
393
+ from: count,
394
+ to: count,
395
+ agent: text,
396
+ models: array(modelSummarySchema, 50),
397
+ totalModels: count,
398
+ nextModelsCursor: text,
399
+ selected: modelSummarySchema,
400
+ buckets: array(object({ from: nonnegative, to: nonnegative, summary: modelSummarySchema }, ["from", "to"]), 240),
401
+ requests: array(object({
402
+ location: text,
403
+ sessionId: text,
404
+ requestId: text,
405
+ startedAt: count,
406
+ completedAt: count,
407
+ model: text,
408
+ agent: text,
409
+ attribution: enumeration("request", "legacy-record", "unknown"),
410
+ partial: { type: "boolean" },
411
+ outputTokens: nonnegative,
412
+ ttftMs: nonnegative
413
+ }, [
414
+ "location",
415
+ "sessionId",
416
+ "requestId",
417
+ "startedAt",
418
+ "completedAt",
419
+ "attribution",
420
+ "partial"
421
+ ]), 50),
422
+ nextRequestsCursor: text,
423
+ coverage: object({
424
+ source: enumeration("retained-request-records"),
425
+ scannedRecords: count,
426
+ unscannedRecords: count,
427
+ duplicateRecords: count,
428
+ conflictingRequests: count,
429
+ missingIdentityRecords: count,
430
+ incompleteRequests: count,
431
+ invalidTimingRequests: count,
432
+ gapRecords: count,
433
+ maxRequests: count,
434
+ partial: { type: "boolean" }
435
+ }),
436
+ metadata: historyResultSchema.properties.metadata ?? {},
437
+ gaps: array(count, 128),
438
+ omittedGaps: count
439
+ }, [
440
+ "schemaVersion",
441
+ "generatedAt",
442
+ "expiresAt",
443
+ "snapshotId",
444
+ "enabled",
445
+ "from",
446
+ "to",
447
+ "models",
448
+ "totalModels",
449
+ "buckets",
450
+ "requests",
451
+ "coverage",
452
+ "metadata",
453
+ "gaps",
454
+ "omittedGaps"
455
+ ]);
456
+ analyticsRpc = Rpc.define({
457
+ id: "context-guard.analytics.v1",
458
+ methods: {
459
+ read: { input: analyticsReadSchema, output: analyticsResultSchema }
460
+ },
461
+ events: {}
462
+ });
340
463
  });
341
464
 
342
465
  // src/history.ts
@@ -529,6 +652,7 @@ function scalarPerformance(p) {
529
652
  async function readPerformanceHistory(directory, filter = {}) {
530
653
  const records = [];
531
654
  const files = await segments(directory);
655
+ filter.signal?.throwIfAborted();
532
656
  const maxBytes = filter.maxBytes ?? 64 * 1024 * 1024;
533
657
  let bytes = 0, skippedBytes = 0, malformedRecords = 0;
534
658
  const selected = [];
@@ -541,6 +665,7 @@ async function readPerformanceHistory(directory, filter = {}) {
541
665
  selected.unshift(file);
542
666
  }
543
667
  for (const file of selected) {
668
+ filter.signal?.throwIfAborted();
544
669
  const handle = await open(join(directory, file.name), "r");
545
670
  let source;
546
671
  try {
@@ -550,7 +675,12 @@ async function readPerformanceHistory(directory, filter = {}) {
550
675
  } finally {
551
676
  await handle.close();
552
677
  }
678
+ let scannedLines = 0;
553
679
  for (const line of source.split("\n")) {
680
+ if (filter.signal && ++scannedLines % 256 === 0) {
681
+ await new Promise((done) => setTimeout(done, 0));
682
+ filter.signal.throwIfAborted();
683
+ }
554
684
  if (!line)
555
685
  continue;
556
686
  try {
@@ -671,8 +801,9 @@ Open a read-only live terminal dashboard for an existing OpenCode server.
671
801
 
672
802
  Start here
673
803
  1. Keep an OpenCode server running with Context Guard registered.
674
- 2. Close OpenCode's native TUI before opening the standalone dashboard.
675
- 3. In the owning project directory, run: context-guard dashboard tui
804
+ 2. Open a separate terminal tab, window or pane for the dashboard.
805
+ 3. In the same project directory, run: context-guard dashboard tui
806
+ Keep working in OpenCode; the dashboard reads live updates from its server.
676
807
 
677
808
  Options
678
809
  --directory PATH Owning project directory (local default: current directory)
@@ -695,8 +826,10 @@ Authentication (explicit servers)
695
826
  CONTEXT_GUARD_USERNAME Server username (default: opencode)
696
827
 
697
828
  Keys
698
- 1/2/3 overview/detail/history; arrows or j/k move; / search; f filters;
699
- l location; w history window; r refresh; ? help; q or Ctrl+C quit.`;
829
+ 1/2/3/4 overview/detail/history/models; arrows or j/k move; Enter detail/chart;
830
+ Models: Left/Right metric, [/] bucket, d requests, n/p pages, s sort;
831
+ / session search; f filters (Models: agent); l location; w time window;
832
+ r refresh; ? help; q or Ctrl+C quit.`;
700
833
  var init_command = () => {
701
834
  };
702
835
 
@@ -1164,6 +1297,249 @@ var init_inventory = __esm(() => {
1164
1297
  });
1165
1298
  });
1166
1299
 
1300
+ // src/dashboard/port.ts
1301
+ var AnalyticsUnavailable;
1302
+ var init_port = __esm(() => {
1303
+ AnalyticsUnavailable = class AnalyticsUnavailable extends Error {
1304
+ reason;
1305
+ constructor(reason) {
1306
+ super(`Model analytics ${reason}`);
1307
+ this.reason = reason;
1308
+ }
1309
+ };
1310
+ });
1311
+
1312
+ // src/dashboard/models.ts
1313
+ class ModelsController {
1314
+ changed;
1315
+ port;
1316
+ active = false;
1317
+ key = "";
1318
+ agent = "";
1319
+ seedModel = "";
1320
+ selection;
1321
+ resolution = 120;
1322
+ from = 0;
1323
+ to = 1;
1324
+ generation = 0;
1325
+ abort;
1326
+ pending;
1327
+ modelPages = [undefined];
1328
+ requestPages = [undefined];
1329
+ state = {
1330
+ status: "idle",
1331
+ stale: false,
1332
+ sort: "requests",
1333
+ windowMs: 300000,
1334
+ modelPage: 0,
1335
+ requestPage: 0
1336
+ };
1337
+ constructor(changed) {
1338
+ this.changed = changed;
1339
+ }
1340
+ getView() {
1341
+ return this.state;
1342
+ }
1343
+ configure(port, active, windowMs, agent, seedModel, now) {
1344
+ const key = JSON.stringify([
1345
+ port.location.directory,
1346
+ windowMs,
1347
+ agent,
1348
+ seedModel
1349
+ ]);
1350
+ const different = key !== this.key;
1351
+ const preserveSelection = this.port?.location.directory === port.location.directory && this.agent === agent && this.seedModel === seedModel;
1352
+ const entering = active && !this.active;
1353
+ if (different || !active)
1354
+ this.cancel();
1355
+ this.port = port;
1356
+ this.active = active;
1357
+ if (different) {
1358
+ this.key = key;
1359
+ this.agent = agent;
1360
+ this.seedModel = seedModel;
1361
+ if (!preserveSelection)
1362
+ this.selection = seedModel || undefined;
1363
+ this.modelPages = [undefined];
1364
+ this.requestPages = [undefined];
1365
+ this.state = {
1366
+ status: "idle",
1367
+ stale: false,
1368
+ sort: this.state.sort,
1369
+ windowMs,
1370
+ modelPage: 0,
1371
+ requestPage: 0
1372
+ };
1373
+ }
1374
+ if (active && (different || entering))
1375
+ this.refresh(now);
1376
+ }
1377
+ cancel() {
1378
+ this.generation++;
1379
+ this.abort?.abort();
1380
+ this.abort = undefined;
1381
+ this.pending = undefined;
1382
+ }
1383
+ stop() {
1384
+ this.cancel();
1385
+ this.active = false;
1386
+ this.state = { ...this.state, stale: Boolean(this.state.result) };
1387
+ }
1388
+ async refresh(now) {
1389
+ if (!this.active || !this.port)
1390
+ return;
1391
+ if (this.pending)
1392
+ return this.pending;
1393
+ this.to = Math.max(1, Math.floor(now));
1394
+ this.from = Math.max(0, this.to - this.state.windowMs);
1395
+ this.modelPages = [undefined];
1396
+ this.requestPages = [undefined];
1397
+ this.state = { ...this.state, modelPage: 0, requestPage: 0 };
1398
+ return this.query(false);
1399
+ }
1400
+ setWidth(width) {
1401
+ const next = Math.max(1, Math.min(120, Math.floor(width) - 10));
1402
+ if (next === this.resolution)
1403
+ return;
1404
+ this.resolution = next;
1405
+ if (this.active && this.state.result)
1406
+ this.query(true);
1407
+ }
1408
+ select(model) {
1409
+ if (!this.active || model === this.selection)
1410
+ return;
1411
+ this.selection = model;
1412
+ this.requestPages = [undefined];
1413
+ this.state = { ...this.state, requestPage: 0 };
1414
+ this.query(true);
1415
+ }
1416
+ sort() {
1417
+ const sorts = ["requests", "tps", "ttft", "identity"];
1418
+ this.modelPages = [undefined];
1419
+ this.state = {
1420
+ ...this.state,
1421
+ sort: sorts[(sorts.indexOf(this.state.sort) + 1) % sorts.length] ?? "requests",
1422
+ modelPage: 0
1423
+ };
1424
+ this.query(true);
1425
+ }
1426
+ page(kind, direction) {
1427
+ if (this.state.status !== "ready")
1428
+ return;
1429
+ const pages = kind === "models" ? this.modelPages : this.requestPages;
1430
+ const index = kind === "models" ? this.state.modelPage : this.state.requestPage;
1431
+ const next = kind === "models" ? this.state.result?.nextModelsCursor : this.state.result?.nextRequestsCursor;
1432
+ if (direction < 0 && index === 0 || direction > 0 && !next)
1433
+ return;
1434
+ if (direction > 0)
1435
+ pages[index + 1] = next;
1436
+ this.state = {
1437
+ ...this.state,
1438
+ ...kind === "models" ? { modelPage: index + direction, requestPage: 0 } : { requestPage: index + direction }
1439
+ };
1440
+ if (kind === "models") {
1441
+ this.selection = undefined;
1442
+ this.requestPages = [undefined];
1443
+ }
1444
+ this.query(true);
1445
+ }
1446
+ query(reuse) {
1447
+ this.cancel();
1448
+ const port = this.port;
1449
+ if (!this.active || !port)
1450
+ return Promise.resolve();
1451
+ if (!port.analytics) {
1452
+ this.state = { ...this.state, status: "unsupported", stale: false };
1453
+ this.changed();
1454
+ return Promise.resolve();
1455
+ }
1456
+ const read = port.analytics.bind(port);
1457
+ const controller = new AbortController;
1458
+ this.abort = controller;
1459
+ const generation = this.generation;
1460
+ const signal = AbortSignal.any([
1461
+ controller.signal,
1462
+ AbortSignal.timeout(15000)
1463
+ ]);
1464
+ const modelCursor = this.modelPages[this.state.modelPage];
1465
+ const requestCursor = this.requestPages[this.state.requestPage];
1466
+ const input = {
1467
+ schemaVersion: 1,
1468
+ from: this.from,
1469
+ to: this.to,
1470
+ buckets: this.resolution,
1471
+ sort: this.state.sort,
1472
+ ...this.agent ? { agent: this.agent } : {},
1473
+ ...this.selection === "" ? { unknownModel: true } : this.selection ? { model: this.selection } : {},
1474
+ ...reuse && this.state.result ? { snapshotId: this.state.result.snapshotId } : {},
1475
+ ...reuse && modelCursor ? { modelCursor } : {},
1476
+ ...reuse && requestCursor ? { requestCursor } : {}
1477
+ };
1478
+ this.state = {
1479
+ ...this.state,
1480
+ status: "loading",
1481
+ stale: Boolean(this.state.result)
1482
+ };
1483
+ this.changed();
1484
+ const task = Promise.resolve().then(async () => {
1485
+ try {
1486
+ let result;
1487
+ try {
1488
+ result = await read(input, signal);
1489
+ } catch (error) {
1490
+ if (!input.snapshotId || signal.aborted || error instanceof AnalyticsUnavailable && error.reason === "unsupported")
1491
+ throw error;
1492
+ const {
1493
+ snapshotId: _id,
1494
+ modelCursor: _m,
1495
+ requestCursor: _r,
1496
+ ...fresh
1497
+ } = input;
1498
+ result = await read(fresh, signal);
1499
+ if (generation !== this.generation)
1500
+ return;
1501
+ signal.throwIfAborted();
1502
+ this.modelPages = [undefined];
1503
+ this.requestPages = [undefined];
1504
+ this.state = { ...this.state, modelPage: 0, requestPage: 0 };
1505
+ }
1506
+ if (generation !== this.generation)
1507
+ return;
1508
+ signal.throwIfAborted();
1509
+ if (!isAnalyticsResult(result) || result.from !== input.from || result.to !== input.to || result.agent !== input.agent || result.requests.some((r) => r.location !== port.location.directory))
1510
+ throw new Error("Invalid analytics response");
1511
+ if (result.selected)
1512
+ this.selection = result.selected.model ?? "";
1513
+ this.state = {
1514
+ ...this.state,
1515
+ result: structuredClone(result),
1516
+ status: result.enabled ? "ready" : "disabled",
1517
+ stale: false
1518
+ };
1519
+ } catch (error) {
1520
+ if (generation !== this.generation || controller.signal.aborted)
1521
+ return;
1522
+ this.state = {
1523
+ ...this.state,
1524
+ status: error instanceof AnalyticsUnavailable && error.reason === "unsupported" && !this.state.result ? "unsupported" : "unavailable",
1525
+ stale: Boolean(this.state.result)
1526
+ };
1527
+ } finally {
1528
+ if (generation === this.generation) {
1529
+ this.pending = undefined;
1530
+ this.changed();
1531
+ }
1532
+ }
1533
+ });
1534
+ this.pending = task;
1535
+ return task;
1536
+ }
1537
+ }
1538
+ var init_models = __esm(() => {
1539
+ init_rpc();
1540
+ init_port();
1541
+ });
1542
+
1167
1543
  // src/dashboard/view.ts
1168
1544
  function immutableView(value) {
1169
1545
  const copy = structuredClone(value);
@@ -1256,6 +1632,7 @@ class DashboardController {
1256
1632
  refreshTimer;
1257
1633
  ageTimer;
1258
1634
  view;
1635
+ models;
1259
1636
  constructor(port, options = {}) {
1260
1637
  this.limits = { ...options.limits ?? inventoryLimits };
1261
1638
  validateInventoryLimits(this.limits);
@@ -1266,6 +1643,7 @@ class DashboardController {
1266
1643
  throw new Error("Invalid dashboard refresh interval");
1267
1644
  this.state = this.createLocation(port);
1268
1645
  this.locations.set(port.location.directory, this.state);
1646
+ this.models = new ModelsController(() => this.publish());
1269
1647
  this.view = this.buildView();
1270
1648
  }
1271
1649
  createLocation(port) {
@@ -1315,6 +1693,7 @@ class DashboardController {
1315
1693
  await this.refresh();
1316
1694
  }
1317
1695
  stop() {
1696
+ this.models.stop();
1318
1697
  this.running = false;
1319
1698
  this.generation++;
1320
1699
  this.scope?.abort();
@@ -1349,6 +1728,7 @@ class DashboardController {
1349
1728
  this.locations.delete(directory);
1350
1729
  this.locations.set(directory, next);
1351
1730
  this.state = next;
1731
+ this.configureModels();
1352
1732
  if (this.locations.size > 4) {
1353
1733
  const oldest = this.locations.keys().next().value;
1354
1734
  if (oldest !== undefined)
@@ -1370,11 +1750,12 @@ class DashboardController {
1370
1750
  this.publish();
1371
1751
  }
1372
1752
  setScreen(screen) {
1373
- if (!["overview", "detail", "history"].includes(screen))
1753
+ if (!["overview", "detail", "history", "models"].includes(screen))
1374
1754
  throw new Error("Invalid dashboard screen");
1375
1755
  if (screen === this.screen)
1376
1756
  return;
1377
1757
  this.screen = screen;
1758
+ this.configureModels();
1378
1759
  this.queueHistory();
1379
1760
  this.publish();
1380
1761
  }
@@ -1384,6 +1765,7 @@ class DashboardController {
1384
1765
  if (windowMs === this.windowMs)
1385
1766
  return;
1386
1767
  this.windowMs = windowMs;
1768
+ this.configureModels();
1387
1769
  this.queueHistory();
1388
1770
  this.publish();
1389
1771
  }
@@ -1396,11 +1778,18 @@ class DashboardController {
1396
1778
  agent: filters.agent ?? this.filters.agent,
1397
1779
  model: filters.model ?? this.filters.model
1398
1780
  };
1781
+ this.configureModels();
1399
1782
  this.publish();
1400
1783
  }
1784
+ configureModels() {
1785
+ this.models.configure(this.state.port, this.running && this.screen === "models", this.windowMs, this.filters.agent, this.filters.model, this.serverNow());
1786
+ }
1401
1787
  async refresh() {
1402
1788
  if (!this.running || !this.scope)
1403
1789
  return;
1790
+ this.configureModels();
1791
+ if (this.screen === "models")
1792
+ this.models.refresh(this.serverNow());
1404
1793
  for (const cached of this.cache.values())
1405
1794
  if (cached.directory === this.state.port.location.directory)
1406
1795
  cached.valid = false;
@@ -1701,6 +2090,7 @@ class DashboardController {
1701
2090
  "maximum-500-points-and-requests"
1702
2091
  ]
1703
2092
  },
2093
+ models: this.models.getView(),
1704
2094
  notices: [
1705
2095
  ...!state.complete ? ["partial-inventory"] : [],
1706
2096
  ...state.connection !== "live" ? ["live-data-stale"] : [],
@@ -1713,6 +2103,7 @@ var init_controller = __esm(() => {
1713
2103
  init_rpc();
1714
2104
  init_transport();
1715
2105
  init_inventory();
2106
+ init_models();
1716
2107
  init_view();
1717
2108
  });
1718
2109
 
@@ -1865,6 +2256,19 @@ async function connectDashboard(options, discovery = Service) {
1865
2256
  }
1866
2257
  };
1867
2258
  },
2259
+ async analytics(input, signal) {
2260
+ if (!isAnalyticsRead(input))
2261
+ throw new Error("Invalid analytics request");
2262
+ try {
2263
+ const result = await client.rpc(analyticsRpc).read(input, { location, signal: deadline(signal) });
2264
+ if (!isAnalyticsResult(result))
2265
+ throw new Error("Invalid analytics response");
2266
+ return result;
2267
+ } catch (error) {
2268
+ const missing = typeof error === "object" && error !== null && "type" in error && (error.type === "rpc.unavailable" || error.type === "rpc.method_not_found");
2269
+ throw new AnalyticsUnavailable(missing ? "unsupported" : "unavailable");
2270
+ }
2271
+ },
1868
2272
  async history(input, signal) {
1869
2273
  if (!isHistoryRead(input))
1870
2274
  throw new Error("History requires one session, at most seven days and 16\u2013500 points");
@@ -1892,6 +2296,7 @@ async function connectDashboard(options, discovery = Service) {
1892
2296
  }
1893
2297
  var init_connection = __esm(() => {
1894
2298
  init_rpc();
2299
+ init_port();
1895
2300
  });
1896
2301
 
1897
2302
  // src/tui/format.ts
@@ -2012,9 +2417,9 @@ var init_format = __esm(() => {
2012
2417
  });
2013
2418
 
2014
2419
  // src/tui/chartformat.ts
2015
- function columns(points, from, to, width, gaps = []) {
2420
+ function columns(points, from, to, width, gaps = [], halfOpen = false) {
2016
2421
  const size = Math.max(1, Math.min(120, Math.floor(width))), values = Array.from({ length: size });
2017
- const column = (time) => Math.max(0, Math.min(size - 1, Math.floor((time - from) / Math.max(1, to - from) * (size - 1))));
2422
+ const column = (time) => Math.max(0, Math.min(size - 1, Math.floor((time - from) / Math.max(1, to - from) * (halfOpen ? size : size - 1))));
2018
2423
  let min = Infinity, max = -Infinity;
2019
2424
  for (const point of points) {
2020
2425
  if (point.time < from || point.time > to || point.value === undefined || !Number.isFinite(point.value) || point.value < 0)
@@ -2038,12 +2443,12 @@ function sparkline(points, from, to, width, gaps = []) {
2038
2443
  chars[column(time)] = "\u2506";
2039
2444
  return { line: chars.join(""), ...min === Infinity ? {} : { min, max } };
2040
2445
  }
2041
- function metricPlot(points, from, to, width, gaps = []) {
2446
+ function metricPlot(points, from, to, width, gaps = [], halfOpen = false) {
2042
2447
  const label = (value) => value >= 1e6 ? `${(value / 1e6).toFixed(1)}M` : value >= 1000 ? `${(value / 1000).toFixed(1)}k` : value.toFixed(1);
2043
2448
  const peak = Math.max(0, ...points.filter((p) => p.time >= from && p.time <= to && Number.isFinite(p.value)).map((p) => p.value ?? 0));
2044
2449
  const scale = Math.max(1, peak);
2045
2450
  const gutter = width >= 20 ? Math.max(label(scale).length, 3) : 0;
2046
- const { values, column } = columns(points, from, to, Math.max(1, width - gutter - 2), gaps);
2451
+ const { values, column } = columns(points, from, to, Math.max(1, halfOpen ? Math.min(points.length, width - gutter - 2) : width - gutter - 2), gaps, halfOpen);
2047
2452
  const gapColumns = new Set(gaps.filter((time) => time >= from && time <= to).map(column));
2048
2453
  const height = 6;
2049
2454
  const blocks = " \u2581\u2582\u2583\u2584\u2585\u2586\u2587\u2588";
@@ -2120,6 +2525,108 @@ var init_chartformat = __esm(() => {
2120
2525
  ];
2121
2526
  });
2122
2527
 
2528
+ // src/dashboard/modelview.ts
2529
+ function metricValue(summary, metric2) {
2530
+ if (metric2.key === "outputTokens" && !summary?.tpsRequests)
2531
+ return;
2532
+ const value = summary?.[metric2.key];
2533
+ return value === undefined ? undefined : value / metric2.divisor;
2534
+ }
2535
+ function terminalModelRow(model, focused) {
2536
+ return [
2537
+ `${focused ? "> " : " "}${model.model ?? "unknown model attribution"}`,
2538
+ ` ${stats(model)}`,
2539
+ ` TPS eligible ${model.tpsRequests}; TTFT eligible ${model.ttftRequests}; partial ${model.partialRequests}; legacy attribution ${model.legacyAttributionRequests}`
2540
+ ];
2541
+ }
2542
+ function terminalModels(view, width, detail, metricIndex, sample, focus, requests) {
2543
+ const state = view.models;
2544
+ const result = state.result;
2545
+ const lines = [
2546
+ `Models \xB7 ${state.status}${state.stale ? " \xB7 STALE cached results" : ""} \xB7 ${state.windowMs / 60000} minutes \xB7 retained history`,
2547
+ `Agent: ${(result?.agent ?? view.filters.agent) || "all"} \xB7 sort ${state.sort} \xB7 location ${view.location}`,
2548
+ "4 Models \xB7 Enter chart \xB7 Esc model list/overview \xB7 w window \xB7 f agent \xB7 r refresh"
2549
+ ];
2550
+ if (state.status === "unsupported")
2551
+ return [
2552
+ ...lines,
2553
+ "Models requires a server with context-guard.analytics.v1; update the server plugin. Overview and session history remain available."
2554
+ ];
2555
+ if (state.status === "disabled")
2556
+ return [
2557
+ ...lines,
2558
+ "Retained performance recording is disabled on this server."
2559
+ ];
2560
+ if (!result)
2561
+ return [
2562
+ ...lines,
2563
+ state.status === "loading" ? "Loading model aggregates\u2026" : "Model analytics unavailable; press r to retry."
2564
+ ];
2565
+ lines.push(`${new Date(result.from).toISOString()} \u2192 ${new Date(result.to).toISOString()} (UTC; completion time)`);
2566
+ const partial = result.coverage.partial || result.metadata.skippedBytes > 0 || result.metadata.malformedRecords > 0 || result.gaps.length > 0 || result.omittedGaps > 0;
2567
+ lines.push(`Coverage: ${partial ? "PARTIAL" : "bounded observations"}; 64 MiB scan limit; ${result.coverage.maxRequests} request identities maximum; no pre-observation reconstruction.`);
2568
+ lines.push(`Archive retained since ${result.metadata.retainedFrom === undefined ? "unknown" : new Date(result.metadata.retainedFrom).toISOString()}; snapshot observed at ${new Date(result.generatedAt).toISOString()}.`);
2569
+ lines.push(`Scan: ${result.coverage.scannedRecords} records; ${result.metadata.skippedBytes} bytes skipped; ${result.metadata.malformedRecords} malformed; ${result.coverage.conflictingRequests} conflicting; ${result.coverage.incompleteRequests} incomplete; ${result.coverage.invalidTimingRequests} invalid timing; gaps ${result.gaps.length}${result.omittedGaps ? ` (+${result.omittedGaps} omitted)` : ""}.`);
2570
+ if (!detail) {
2571
+ lines.push(`Model page ${state.modelPage + 1} \xB7 ${result.totalModels} models \xB7 Up/Down scroll/select \xB7 n/p pages \xB7 s sort`);
2572
+ if (!result.models.length)
2573
+ lines.push("No completed requests match this range and agent filter.");
2574
+ result.models.forEach((model, index) => {
2575
+ lines.push(...terminalModelRow(model, index === focus));
2576
+ });
2577
+ return lines;
2578
+ }
2579
+ const selected = result.selected;
2580
+ if (!selected)
2581
+ return [
2582
+ ...lines,
2583
+ "No completed requests for this model in the selected range."
2584
+ ];
2585
+ lines.push(`Model: ${selected.model ?? "unknown model attribution"}`, stats(selected));
2586
+ lines.push(`Reported output ${number(selected.tpsRequests ? selected.outputTokens : undefined)} tokens; matched duration ${number(selected.tpsRequests ? selected.durationMs / 1000 : undefined)}s; TPS eligible/excluded ${selected.tpsRequests}/${selected.excludedTpsRequests}; TTFT eligible/excluded ${selected.ttftRequests}/${selected.excludedTtftRequests}; partial ${selected.partialRequests}; legacy attribution ${selected.legacyAttributionRequests}`);
2587
+ if (requests) {
2588
+ lines.push(`Request page ${state.requestPage + 1} \xB7 n/p pages \xB7 d chart \xB7 completed stream \u2260 task success`);
2589
+ result.requests.forEach((r) => lines.push(`Request ${r.requestId} \xB7 session ${r.sessionId}`, `Completed ${new Date(r.completedAt).toISOString()} \xB7 ${r.agent ?? "unknown agent"} \xB7 output ${number(r.outputTokens)} \xB7 duration ${number((r.completedAt - r.startedAt) / 1000)}s \xB7 TTFT ${number(r.ttftMs === undefined ? undefined : r.ttftMs / 1000)}s ~ \xB7 ${r.attribution}${r.partial ? " \xB7 partial" : ""}`));
2590
+ return lines;
2591
+ }
2592
+ const metric2 = modelMetrics[metricIndex] ?? modelMetrics[0];
2593
+ lines.push(`\u2039 ${metric2.label} (${metric2.unit}) \u203A \xB7 Left/Right metric \xB7 [/] bucket \xB7 d requests`);
2594
+ lines.push(...metricPlot(result.buckets.map((bucket2) => {
2595
+ const value = metricValue(bucket2.summary, metric2);
2596
+ return {
2597
+ time: (bucket2.from + bucket2.to) / 2,
2598
+ ...value === undefined ? {} : { value }
2599
+ };
2600
+ }), result.from, result.to, width, result.gaps, true));
2601
+ lines.push("UTC buckets \xB7 blank = no eligible observations \xB7 \u2506 = retained gap; partial coverage may hide other gaps.");
2602
+ lines.push("TPS = reported output / summed matched request durations (includes TTFT); concurrent durations add. In-flight requests enter after stream completion.");
2603
+ const bucket = result.buckets[Math.min(sample, Math.max(0, result.buckets.length - 1))];
2604
+ if (bucket) {
2605
+ lines.push(`Bucket ${Math.min(sample + 1, result.buckets.length)}/${result.buckets.length}: ${new Date(bucket.from).toISOString()} \u2192 ${new Date(bucket.to).toISOString()}`);
2606
+ lines.push(bucket.summary ? stats(bucket.summary) : "No eligible observations; measurements unknown.");
2607
+ lines.push(`Selected ${metric2.label}: ${number(metricValue(bucket.summary, metric2))} ${metric2.unit}`);
2608
+ if (bucket.summary)
2609
+ lines.push(`Bucket eligibility: TPS ${bucket.summary.tpsRequests}/${bucket.summary.completedRequests}; TTFT ${bucket.summary.ttftRequests}/${bucket.summary.completedRequests}; partial ${bucket.summary.partialRequests}`);
2610
+ }
2611
+ return lines;
2612
+ }
2613
+ var modelMetrics, number = (n) => n === undefined ? "?" : Number.isInteger(n) ? String(n) : n.toFixed(2), stats = (m) => `${m.sessions} sessions \xB7 ${m.completedRequests} completed \xB7 weighted TPS ${number(m.averageTps)} \xB7 TTFT median/p95 ${number(m.medianTtftMs === undefined ? undefined : m.medianTtftMs / 1000)}/${number(m.p95TtftMs === undefined ? undefined : m.p95TtftMs / 1000)}s ~`;
2614
+ var init_modelview = __esm(() => {
2615
+ init_chartformat();
2616
+ modelMetrics = [
2617
+ { key: "averageTps", label: "Weighted request TPS", unit: "t/s", divisor: 1 },
2618
+ { key: "medianTtftMs", label: "TTFT median ~", unit: "s", divisor: 1000 },
2619
+ { key: "p95TtftMs", label: "TTFT p95 ~", unit: "s", divisor: 1000 },
2620
+ {
2621
+ key: "completedRequests",
2622
+ label: "Completed requests",
2623
+ unit: "requests",
2624
+ divisor: 1
2625
+ },
2626
+ { key: "outputTokens", label: "Reported output", unit: "tokens", divisor: 1 }
2627
+ ];
2628
+ });
2629
+
2123
2630
  // src/dashboard/terminalview.ts
2124
2631
  function overviewRow(row2) {
2125
2632
  const s = row2.snapshot;
@@ -2202,7 +2709,11 @@ function terminalHistory(view, width, metricIndex, sample) {
2202
2709
  ];
2203
2710
  }
2204
2711
  function wrapped(lines, width) {
2205
- return lines.flatMap((line) => line ? wrap(line, Math.max(1, width)) : [""]);
2712
+ const available = Math.max(1, width);
2713
+ return lines.flatMap((line) => {
2714
+ const clean = clip(line, Math.max(available, cells(line)));
2715
+ return cells(clean) <= available ? [clean] : wrap(clean, available);
2716
+ });
2206
2717
  }
2207
2718
  function headerLines(view, width) {
2208
2719
  return [
@@ -2282,7 +2793,10 @@ var init_terminalview = __esm(() => {
2282
2793
  ];
2283
2794
  terminalHelp = [
2284
2795
  "Context Guard standalone dashboard \u2014 keyboard help",
2285
- "1 overview \xB7 2 detail \xB7 3 history \xB7 Enter detail",
2796
+ "1 overview \xB7 2 detail \xB7 3 history \xB7 4 models \xB7 Enter detail/chart",
2797
+ "Models: Up/Down scroll tall rows, then select \xB7 Enter aggregate chart \xB7 Esc model list \xB7 s sort",
2798
+ "Models charts: Left/Right metric \xB7 w window \xB7 [/] bucket \xB7 d request details",
2799
+ "Models list/request details: n/p pages \xB7 f exact agent filter \xB7 r refresh",
2286
2800
  "Up/Down or j/k: select session in overview; scroll other screens",
2287
2801
  "PageUp/PageDown: move by a page \xB7 Home/End: first/last",
2288
2802
  "Left/Right: history metric (six metrics) \xB7 w: cycle 5m/1h/24h/7d",
@@ -2312,6 +2826,11 @@ class TerminalShell {
2312
2826
  scroll = 0;
2313
2827
  metric = 0;
2314
2828
  sample = 0;
2829
+ modelDetail = false;
2830
+ modelRequests = false;
2831
+ modelFocus = 0;
2832
+ modelFocusId;
2833
+ modelViewport;
2315
2834
  help = false;
2316
2835
  editor;
2317
2836
  input = "";
@@ -2357,7 +2876,7 @@ class TerminalShell {
2357
2876
  this.task(this.controller.selectLocation(this.input));
2358
2877
  } else
2359
2878
  this.controller.setFilters({ [editor]: this.input });
2360
- this.editor = editor === "agent" ? "model" : undefined;
2879
+ this.editor = editor === "agent" && view.screen !== "models" ? "model" : undefined;
2361
2880
  this.input = this.editor === "model" ? view.filters.model : "";
2362
2881
  this.scroll = 0;
2363
2882
  this.notice = "";
@@ -2377,6 +2896,11 @@ class TerminalShell {
2377
2896
  if (name === "escape") {
2378
2897
  this.help = false;
2379
2898
  this.scroll = 0;
2899
+ if (view.screen === "models" && this.modelDetail) {
2900
+ this.modelDetail = false;
2901
+ this.modelRequests = false;
2902
+ return;
2903
+ }
2380
2904
  this.controller.setScreen("overview");
2381
2905
  return;
2382
2906
  }
@@ -2399,6 +2923,81 @@ class TerminalShell {
2399
2923
  this.task(this.controller.refresh());
2400
2924
  return;
2401
2925
  }
2926
+ if (name === "4") {
2927
+ this.help = false;
2928
+ this.scroll = 0;
2929
+ this.modelDetail = false;
2930
+ this.modelRequests = false;
2931
+ this.controller.setScreen("models");
2932
+ return;
2933
+ }
2934
+ if (view.screen === "models" && !this.help) {
2935
+ if (name === "enter" || name === "return") {
2936
+ const model = view.models.result?.models[this.modelFocus];
2937
+ if (model) {
2938
+ this.controller.models.select(model.model ?? "");
2939
+ this.modelDetail = true;
2940
+ this.modelRequests = false;
2941
+ this.sample = 0;
2942
+ this.scroll = 0;
2943
+ }
2944
+ return;
2945
+ }
2946
+ if (name === "w") {
2947
+ this.controller.setWindow(historyWindows[(historyWindows.indexOf(view.models.windowMs) + 1) % historyWindows.length] ?? historyWindows[0]);
2948
+ this.sample = 0;
2949
+ this.scroll = 0;
2950
+ return;
2951
+ }
2952
+ if (name === "s" && !this.modelDetail) {
2953
+ this.controller.models.sort();
2954
+ this.scroll = 0;
2955
+ return;
2956
+ }
2957
+ if (name === "n" || name === "p") {
2958
+ if (!this.modelDetail || this.modelRequests) {
2959
+ this.controller.models.page(this.modelDetail ? "requests" : "models", name === "n" ? 1 : -1);
2960
+ if (!this.modelDetail) {
2961
+ this.modelFocus = 0;
2962
+ this.modelFocusId = undefined;
2963
+ }
2964
+ this.scroll = 0;
2965
+ }
2966
+ return;
2967
+ }
2968
+ if (this.modelDetail) {
2969
+ if (name === "d") {
2970
+ this.modelRequests = !this.modelRequests;
2971
+ this.scroll = 0;
2972
+ return;
2973
+ }
2974
+ if (name === "left" || name === "right") {
2975
+ this.metric = (this.metric + modelMetrics.length + (name === "left" ? -1 : 1)) % modelMetrics.length;
2976
+ this.sample = 0;
2977
+ this.scroll = 0;
2978
+ return;
2979
+ }
2980
+ if (name === "[" || name === "]") {
2981
+ this.sample = Math.max(0, Math.min((view.models.result?.buckets.length ?? 1) - 1, this.sample + (name === "[" ? -1 : 1)));
2982
+ return;
2983
+ }
2984
+ } else if (["up", "down", "j", "k", "home", "end", "pageup", "pagedown"].includes(name)) {
2985
+ const models = view.models.result?.models ?? [];
2986
+ const delta2 = name === "down" || name === "j" ? 1 : name === "up" || name === "k" ? -1 : name === "pagedown" ? page : -page;
2987
+ const viewport = this.modelViewport;
2988
+ if (viewport?.focus === this.modelFocus && viewport.end - viewport.start > viewport.height && name !== "home" && name !== "end" && (delta2 > 0 ? this.scroll + viewport.height < viewport.end : this.scroll > viewport.start)) {
2989
+ this.scroll = Math.max(viewport.start, Math.min(viewport.end - viewport.height, this.scroll + delta2));
2990
+ return;
2991
+ }
2992
+ if (name === "home")
2993
+ this.scroll = 0;
2994
+ if (name === "end")
2995
+ this.scroll = Number.MAX_SAFE_INTEGER;
2996
+ this.modelFocus = Math.max(0, Math.min(models.length - 1, name === "home" ? 0 : name === "end" ? models.length - 1 : this.modelFocus + delta2));
2997
+ this.modelFocusId = models[this.modelFocus]?.model ?? "";
2998
+ return;
2999
+ }
3000
+ }
2402
3001
  if (["1", "2", "3", "return", "enter"].includes(name)) {
2403
3002
  this.help = false;
2404
3003
  this.scroll = 0;
@@ -2435,14 +3034,28 @@ class TerminalShell {
2435
3034
  frame(width, height) {
2436
3035
  width = Math.max(1, width);
2437
3036
  height = Math.max(1, height);
3037
+ this.controller.models.setWidth(width);
2438
3038
  const view = this.controller.getView();
2439
- const identity = `${view.location}\0${view.selectedSessionId}\0${view.history.windowMs}`;
3039
+ if (view.screen === "models") {
3040
+ const models = view.models.result?.models ?? [];
3041
+ const identity2 = this.modelFocusId ?? view.models.result?.selected?.model ?? "";
3042
+ const focused = models.findIndex((m) => (m.model ?? "") === identity2);
3043
+ this.modelFocus = Math.max(0, focused < 0 ? Math.min(this.modelFocus, models.length - 1) : focused);
3044
+ if (view.models.status === "ready" && models.length)
3045
+ this.modelFocusId = models[this.modelFocus]?.model ?? "";
3046
+ }
3047
+ const identity = view.screen === "models" ? JSON.stringify([
3048
+ view.location,
3049
+ view.models.windowMs,
3050
+ view.filters.agent,
3051
+ view.models.result?.selected?.model
3052
+ ]) : `${view.location}\0${view.selectedSessionId}\0${view.history.windowMs}`;
2440
3053
  if (identity !== this.historyIdentity) {
2441
3054
  this.sample = 0;
2442
3055
  this.scroll = 0;
2443
3056
  this.historyIdentity = identity;
2444
3057
  }
2445
- const footer = this.editor ? `${this.editor}> ${this.input} [Enter apply; Esc cancel${this.editor === "location" ? "; Tab remembered" : ""}]` : "? help q quit \xB7 1 overview 2 detail 3 history \xB7 / search f filters l location r refresh";
3058
+ const footer = this.editor ? `${this.editor}> ${this.input} [Enter apply; Esc cancel${this.editor === "location" ? "; Tab remembered" : ""}]` : "? help q quit \xB7 1 overview 2 detail 3 history 4 models \xB7 / search f filters l location r refresh";
2446
3059
  if (height < 5)
2447
3060
  return [
2448
3061
  clip("Context Guard \xB7 ? help \xB7 q quit", width),
@@ -2455,7 +3068,7 @@ class TerminalShell {
2455
3068
  ...view.inventory.notices,
2456
3069
  ...view.health?.reasons ?? [],
2457
3070
  ...this.notice ? [this.notice] : [],
2458
- ...Object.values(view.filters).some(Boolean) ? [
3071
+ ...view.screen !== "models" && Object.values(view.filters).some(Boolean) ? [
2459
3072
  `Filters: search=${view.filters.search} agent=${view.filters.agent} model=${view.filters.model}`
2460
3073
  ] : []
2461
3074
  ];
@@ -2490,8 +3103,28 @@ class TerminalShell {
2490
3103
  } else
2491
3104
  body = wrapped([
2492
3105
  ...notices,
2493
- ...view.screen === "detail" ? terminalDetail(view.selected) : terminalHistory(view, width, this.metric, this.sample)
3106
+ ...view.screen === "detail" ? terminalDetail(view.selected) : view.screen === "models" ? terminalModels(view, width, this.modelDetail, this.metric, this.sample, this.modelFocus, this.modelRequests) : terminalHistory(view, width, this.metric, this.sample)
2494
3107
  ], width);
3108
+ this.modelViewport = undefined;
3109
+ if (view.screen === "models" && !this.modelDetail && !this.help && view.models.result?.models.length) {
3110
+ const heights = (view.models.result?.models ?? []).map((model, index) => wrapped(terminalModelRow(model, index === this.modelFocus), width).length);
3111
+ const start = body.length - heights.slice(this.modelFocus).reduce((a, b) => a + b, 0);
3112
+ const end = start + (heights[this.modelFocus] ?? 0);
3113
+ this.modelViewport = {
3114
+ focus: this.modelFocus,
3115
+ start,
3116
+ end,
3117
+ height: available
3118
+ };
3119
+ if (end - start > available) {
3120
+ this.scroll = Math.max(start, Math.min(this.scroll, end - available));
3121
+ } else {
3122
+ if (start < this.scroll)
3123
+ this.scroll = start;
3124
+ if (end > this.scroll + available)
3125
+ this.scroll = end - available;
3126
+ }
3127
+ }
2495
3128
  this.scroll = Math.max(0, Math.min(this.scroll, Math.max(0, body.length - available)));
2496
3129
  return [
2497
3130
  ...header,
@@ -2621,6 +3254,7 @@ var init_terminal = __esm(() => {
2621
3254
  init_connection();
2622
3255
  init_command();
2623
3256
  init_view();
3257
+ init_modelview();
2624
3258
  init_format();
2625
3259
  init_terminalview();
2626
3260
  });
@@ -2668,7 +3302,7 @@ function reconcile(a, b) {
2668
3302
  partial: a.partial || b.partial
2669
3303
  };
2670
3304
  }
2671
- function aggregateModels(records, options) {
3305
+ function analyzeModelRequests(records, options) {
2672
3306
  const maxRequests = options.maxRequests ?? 1e5;
2673
3307
  if (!Number.isSafeInteger(maxRequests) || maxRequests < 1 || maxRequests > 1e6)
2674
3308
  throw new Error("maxRequests must be an integer from 1 to 1000000");
@@ -2729,7 +3363,7 @@ function aggregateModels(records, options) {
2729
3363
  requests.set(key, { fact, conflict: false });
2730
3364
  }
2731
3365
  }
2732
- const groups = new Map;
3366
+ const accepted = [];
2733
3367
  for (const { fact: r, conflict } of requests.values()) {
2734
3368
  if (conflict) {
2735
3369
  coverage.conflictingRequests++;
@@ -2746,6 +3380,22 @@ function aggregateModels(records, options) {
2746
3380
  }
2747
3381
  if (options.from !== undefined && r.completedAt < options.from || options.to !== undefined && r.completedAt >= options.to || options.model !== undefined && r.model !== options.model || options.agent !== undefined && r.agent !== options.agent)
2748
3382
  continue;
3383
+ accepted.push({ ...r, completedAt: r.completedAt });
3384
+ }
3385
+ const models = summarizeModelRequests(accepted);
3386
+ coverage.partial = coverage.unscannedRecords > 0 || coverage.conflictingRequests > 0 || coverage.missingIdentityRecords > 0 || coverage.incompleteRequests > 0 || coverage.invalidTimingRequests > 0 || coverage.gapRecords > 0 || models.some((s) => s.partialRequests > 0 || s.legacyAttributionRequests > 0 || s.excludedTpsRequests > 0 || s.excludedTtftRequests > 0 || !s.model);
3387
+ return {
3388
+ schemaVersion: 1,
3389
+ location: options.location,
3390
+ models,
3391
+ coverage,
3392
+ requests: accepted
3393
+ };
3394
+ }
3395
+ function summarizeModelRequests(requests) {
3396
+ const groups = new Map;
3397
+ for (const r of requests) {
3398
+ const duration = r.completedAt - r.startedAt;
2749
3399
  let group = groups.get(r.model);
2750
3400
  if (!group) {
2751
3401
  group = {
@@ -2800,8 +3450,11 @@ function aggregateModels(records, options) {
2800
3450
  }
2801
3451
  return s;
2802
3452
  }).sort((a, b) => (a.model ?? "").localeCompare(b.model ?? ""));
2803
- coverage.partial = coverage.unscannedRecords > 0 || coverage.conflictingRequests > 0 || coverage.missingIdentityRecords > 0 || coverage.incompleteRequests > 0 || coverage.invalidTimingRequests > 0 || coverage.gapRecords > 0 || models.some((s) => s.partialRequests > 0 || s.legacyAttributionRequests > 0 || s.excludedTpsRequests > 0 || s.excludedTtftRequests > 0 || !s.model);
2804
- return { schemaVersion: 1, location: options.location, models, coverage };
3453
+ return models;
3454
+ }
3455
+ function aggregateModels(records, options) {
3456
+ const { requests: _requests, ...result } = analyzeModelRequests(records, options);
3457
+ return result;
2805
3458
  }
2806
3459
  var nonnegative2 = (n) => n !== undefined && Number.isFinite(n) && n >= 0;
2807
3460