agent-usage-all-in-one 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.
Files changed (31) hide show
  1. package/README.md +11 -1
  2. package/README.zh-CN.md +7 -1
  3. package/dist/cli.js +788 -27
  4. package/dist/cli.js.map +1 -1
  5. package/dist/web/_app/immutable/assets/2.BWrFQDDF.css +1 -0
  6. package/dist/web/_app/immutable/chunks/3vuWay1s.js +3 -0
  7. package/dist/web/_app/immutable/chunks/CIxAYfti.js +1 -0
  8. package/dist/web/_app/immutable/chunks/DrMah8OK.js +1 -0
  9. package/dist/web/_app/immutable/entry/app.BJJ5ybn9.js +2 -0
  10. package/dist/web/_app/immutable/entry/start.C8pSj-JI.js +1 -0
  11. package/dist/web/_app/immutable/nodes/0.f03zL-fK.js +1 -0
  12. package/dist/web/_app/immutable/nodes/1.zxn5xzLP.js +1 -0
  13. package/dist/web/_app/immutable/nodes/2.CeZkCUZN.js +27 -0
  14. package/dist/web/_app/version.json +1 -1
  15. package/dist/web/brand/agent-usage-showcase.jpg +0 -0
  16. package/dist/web/brands/README.md +4 -4
  17. package/dist/web/index.html +30 -8
  18. package/docs/open-source.md +1 -1
  19. package/package.json +1 -1
  20. package/static/brand/agent-usage-showcase.jpg +0 -0
  21. package/static/brands/README.md +4 -4
  22. package/dist/web/_app/immutable/assets/2.BiUr1b86.css +0 -1
  23. package/dist/web/_app/immutable/chunks/BW9s3L2z.js +0 -3
  24. package/dist/web/_app/immutable/chunks/CwKqhBmI.js +0 -1
  25. package/dist/web/_app/immutable/entry/app.CJoUfLXD.js +0 -2
  26. package/dist/web/_app/immutable/entry/start.DEFna-Tq.js +0 -1
  27. package/dist/web/_app/immutable/nodes/0.DTcMU-UW.js +0 -1
  28. package/dist/web/_app/immutable/nodes/1.BRZSpfvp.js +0 -1
  29. package/dist/web/_app/immutable/nodes/2.BKHXaqdm.js +0 -43
  30. package/dist/web/brand/agent-usage-banner.svg +0 -27
  31. package/static/brand/agent-usage-banner.svg +0 -27
package/dist/cli.js CHANGED
@@ -749,16 +749,15 @@ function priceObservation(providerId, observation, catalog, calculatedAt) {
749
749
  ratePerMillion: entry.cacheWriteRatesPerMillion.oneHour
750
750
  }
751
751
  ].filter((item) => item.tokens > 0) : null;
752
- if (cacheWriteTokens > 0 && entry.ratesPerMillion["cache-write"] === null && !exactCacheWriteItems) {
753
- return unavailable("pricing-tier-ambiguous");
754
- }
755
- if (cacheWriteTokens > 0 && !exactCacheWriteItems) {
752
+ const cacheWriteFree = cacheWriteTokens > 0 && !exactCacheWriteItems && entry.ratesPerMillion["cache-write"] === null && !entry.cacheWriteRatesPerMillion;
753
+ const cacheWriteBillable = cacheWriteTokens > 0 && !exactCacheWriteItems && !cacheWriteFree;
754
+ if (cacheWriteBillable) {
756
755
  billableTokens.push({ tokenKind: "cache-write", tokens: cacheWriteTokens });
757
756
  }
758
757
  const nonZero = billableTokens.filter((item) => item.tokens > 0);
759
758
  if (nonZero.some((item) => entry.ratesPerMillion[item.tokenKind] === null)) {
760
759
  return unavailable(
761
- normalized.cacheWriteTokens > 0 ? "pricing-tier-ambiguous" : "token-kinds-incomplete"
760
+ normalized.cacheWriteTokens > 0 && !cacheWriteFree ? "pricing-tier-ambiguous" : "token-kinds-incomplete"
762
761
  );
763
762
  }
764
763
  const lineItems = nonZero.map((item) => {
@@ -777,6 +776,14 @@ function priceObservation(providerId, observation, catalog, calculatedAt) {
777
776
  }))
778
777
  );
779
778
  }
779
+ if (cacheWriteFree) {
780
+ lineItems.push({
781
+ tokenKind: "cache-write",
782
+ tokens: cacheWriteTokens,
783
+ ratePerMillion: 0,
784
+ amount: 0
785
+ });
786
+ }
780
787
  const pricedTokens = lineItems.reduce((total, item) => total + item.tokens, 0);
781
788
  if (pricedTokens !== normalized.recordedTokens) return unavailable("token-kinds-incomplete");
782
789
  const amount = preciseMoney(lineItems.reduce((total, item) => total + item.amount, 0));
@@ -1184,6 +1191,382 @@ var init_retail_pricing = __esm({
1184
1191
  }
1185
1192
  });
1186
1193
 
1194
+ // src/core/plan-pricing.ts
1195
+ function buildBillingPeriod(subscription, summary, comparisonCurrency, rates) {
1196
+ if (!summary) return null;
1197
+ const start = new Date(summary.start).getTime();
1198
+ const end = new Date(summary.end).getTime();
1199
+ const observedThrough = new Date(summary.observedThrough).getTime();
1200
+ const totalDays = (end - start) / MILLISECONDS_PER_DAY;
1201
+ const elapsedDays = Math.max(0, Math.min(observedThrough, end) - start) / MILLISECONDS_PER_DAY;
1202
+ const periodCost = planMoneyAmount(
1203
+ preciseAmount(subscription.amount),
1204
+ subscription.currency,
1205
+ comparisonCurrency,
1206
+ // The period's own rate evidence first, so a short rolling window cannot
1207
+ // leave a cycle amount unconverted.
1208
+ [...summary.retailEquivalent.exchangeRates, ...rates],
1209
+ summary.observedThrough
1210
+ );
1211
+ const retailAmount = summary.retailEquivalent.status === "available" ? summary.retailEquivalent.amount : null;
1212
+ const breakEvenRatio = periodCost.amount !== null && periodCost.amount > 0 && retailAmount !== null ? preciseAmount(retailAmount / periodCost.amount) : null;
1213
+ return {
1214
+ start: summary.start,
1215
+ end: summary.end,
1216
+ elapsedDays: preciseAmount(elapsedDays),
1217
+ totalDays: preciseAmount(totalDays),
1218
+ progress: totalDays > 0 ? preciseAmount(Math.min(1, elapsedDays / totalDays)) : 0,
1219
+ periodCost,
1220
+ recordedTokens: summary.observationCount > 0 ? summary.recordedTokens : null,
1221
+ retailEquivalent: summary.retailEquivalent,
1222
+ breakEvenRatio,
1223
+ ratioBound: ratioBoundFor(breakEvenRatio, summary.retailEquivalent.pricingCoverage)
1224
+ };
1225
+ }
1226
+ function planCatalogEntry(catalog, id) {
1227
+ return catalog.entries.find((entry) => entry.id === id) ?? null;
1228
+ }
1229
+ function planCatalogEntriesForDomain(catalog, providerId, billingDomainId) {
1230
+ return catalog.entries.filter(
1231
+ (entry) => entry.providerId === providerId && entry.billingDomainId === billingDomainId
1232
+ );
1233
+ }
1234
+ function addMonths(date, months) {
1235
+ const day = date.getUTCDate();
1236
+ const shifted = new Date(
1237
+ Date.UTC(
1238
+ date.getUTCFullYear(),
1239
+ date.getUTCMonth() + months,
1240
+ 1,
1241
+ date.getUTCHours(),
1242
+ date.getUTCMinutes(),
1243
+ date.getUTCSeconds(),
1244
+ date.getUTCMilliseconds()
1245
+ )
1246
+ );
1247
+ const lastDayOfMonth = new Date(
1248
+ Date.UTC(shifted.getUTCFullYear(), shifted.getUTCMonth() + 1, 0)
1249
+ ).getUTCDate();
1250
+ shifted.setUTCDate(Math.min(day, lastDayOfMonth));
1251
+ return shifted;
1252
+ }
1253
+ function billingPeriodContaining(anchorDate, billingPeriod, at) {
1254
+ const anchor = new Date(anchorDate.length === 10 ? `${anchorDate}T00:00:00.000Z` : anchorDate);
1255
+ if (Number.isNaN(anchor.getTime())) return null;
1256
+ const step = billingPeriod === "monthly" ? 1 : 12;
1257
+ const monthsApart = (at.getUTCFullYear() - anchor.getUTCFullYear()) * 12 + (at.getUTCMonth() - anchor.getUTCMonth());
1258
+ let index = Math.floor(monthsApart / step);
1259
+ let start = addMonths(anchor, index * step);
1260
+ while (start.getTime() > at.getTime()) {
1261
+ index -= 1;
1262
+ start = addMonths(anchor, index * step);
1263
+ }
1264
+ let end = addMonths(anchor, (index + 1) * step);
1265
+ while (end.getTime() <= at.getTime()) {
1266
+ index += 1;
1267
+ start = end;
1268
+ end = addMonths(anchor, (index + 1) * step);
1269
+ }
1270
+ return { start, end };
1271
+ }
1272
+ function windowDays(start, end) {
1273
+ const span = new Date(end).getTime() - new Date(start).getTime();
1274
+ return span > 0 ? span / MILLISECONDS_PER_DAY : 0;
1275
+ }
1276
+ function proratePlanPrice(amount, billingPeriod, days) {
1277
+ return amount * days / PLAN_PERIOD_DAYS[billingPeriod];
1278
+ }
1279
+ function convertPlanAmount(amount, currency, comparisonCurrency, rates, end) {
1280
+ if (currency.toUpperCase() === comparisonCurrency.toUpperCase()) {
1281
+ return { amount, reason: null, rate: null };
1282
+ }
1283
+ const rate = rates.find(
1284
+ (candidate) => candidate.baseCurrency.toUpperCase() === currency.toUpperCase() && candidate.quoteCurrency.toUpperCase() === comparisonCurrency.toUpperCase()
1285
+ );
1286
+ if (!rate) return { amount: null, reason: "missing-rate", rate: null };
1287
+ if (new Date(end).getTime() - new Date(rate.observedAt).getTime() > STALE_RATE_MILLISECONDS) {
1288
+ return { amount: null, reason: "stale-rate", rate };
1289
+ }
1290
+ return { amount: amount * rate.rate, reason: null, rate };
1291
+ }
1292
+ function preciseAmount(value) {
1293
+ return Number(value.toPrecision(12));
1294
+ }
1295
+ function planMoneyAmount(nativeAmount, nativeCurrency, comparisonCurrency, rates, end) {
1296
+ const converted = convertPlanAmount(nativeAmount, nativeCurrency, comparisonCurrency, rates, end);
1297
+ return {
1298
+ status: converted.amount === null ? "unavailable" : "available",
1299
+ amount: converted.amount === null ? null : preciseAmount(converted.amount),
1300
+ nativeAmount,
1301
+ nativeCurrency,
1302
+ comparisonCurrency,
1303
+ conversionUnavailableReason: converted.reason,
1304
+ exchangeRates: converted.rate ? [converted.rate] : []
1305
+ };
1306
+ }
1307
+ function ratioBoundFor(ratio, pricingCoverage) {
1308
+ if (ratio === null) return "unavailable";
1309
+ return pricingCoverage !== null && pricingCoverage < 0.999 ? "lower" : "exact";
1310
+ }
1311
+ function unitPricePerMillion(amount, tokens) {
1312
+ if (amount === null || tokens <= 0) return null;
1313
+ return preciseAmount(amount / (tokens / 1e6));
1314
+ }
1315
+ function buildWorkbenchPlanValue(options) {
1316
+ const { comparisonCurrency, start, end, rates } = options;
1317
+ const days = windowDays(start, end);
1318
+ const subscriptionByDomain = new Map(
1319
+ options.subscriptions.map((subscription) => [
1320
+ `${subscription.providerId}:${subscription.billingDomainId}`,
1321
+ subscription
1322
+ ])
1323
+ );
1324
+ const entries = [];
1325
+ const metered = [];
1326
+ const unconfigured = [];
1327
+ for (const domain of options.domains) {
1328
+ const subscription = subscriptionByDomain.get(`${domain.providerId}:${domain.billingDomainId}`);
1329
+ if (!subscription) {
1330
+ if (domain.actualCost.records > 0) {
1331
+ metered.push({
1332
+ providerId: domain.providerId,
1333
+ providerDisplayName: domain.providerDisplayName,
1334
+ billingDomainId: domain.billingDomainId,
1335
+ billingDomainDisplayName: domain.billingDomainDisplayName,
1336
+ recordedTokens: domain.observationCount > 0 ? domain.recordedTokens : null,
1337
+ actualCost: domain.actualCost,
1338
+ retailEquivalent: domain.retailEquivalent
1339
+ });
1340
+ continue;
1341
+ }
1342
+ if (domain.observationCount > 0) {
1343
+ unconfigured.push({
1344
+ providerId: domain.providerId,
1345
+ providerDisplayName: domain.providerDisplayName,
1346
+ billingDomainId: domain.billingDomainId,
1347
+ billingDomainDisplayName: domain.billingDomainDisplayName,
1348
+ recordedTokens: domain.recordedTokens
1349
+ });
1350
+ }
1351
+ continue;
1352
+ }
1353
+ const windowPlanCost = planMoneyAmount(
1354
+ preciseAmount(proratePlanPrice(subscription.amount, subscription.billingPeriod, days)),
1355
+ subscription.currency,
1356
+ comparisonCurrency,
1357
+ rates,
1358
+ end
1359
+ );
1360
+ const retail = domain.retailEquivalent;
1361
+ const retailAmount = retail.status === "available" ? retail.amount : null;
1362
+ const valueRatio = windowPlanCost.amount !== null && windowPlanCost.amount > 0 && retailAmount !== null ? preciseAmount(retailAmount / windowPlanCost.amount) : null;
1363
+ const ratioBound = ratioBoundFor(valueRatio, retail.pricingCoverage);
1364
+ const billingPeriod = buildBillingPeriod(
1365
+ subscription,
1366
+ options.billingPeriods?.get(`${domain.providerId}:${domain.billingDomainId}`),
1367
+ comparisonCurrency,
1368
+ rates
1369
+ );
1370
+ entries.push({
1371
+ providerId: domain.providerId,
1372
+ providerDisplayName: domain.providerDisplayName,
1373
+ billingDomainId: domain.billingDomainId,
1374
+ billingDomainDisplayName: domain.billingDomainDisplayName,
1375
+ includedInHeadline: domain.includedInHeadline,
1376
+ plan: {
1377
+ planId: subscription.planId,
1378
+ displayName: subscription.displayName,
1379
+ amount: subscription.amount,
1380
+ currency: subscription.currency,
1381
+ billingPeriod: subscription.billingPeriod,
1382
+ anchorDate: subscription.anchorDate,
1383
+ priceSource: subscription.priceSource,
1384
+ updatedAt: subscription.updatedAt
1385
+ },
1386
+ windowDays: preciseAmount(days),
1387
+ windowPlanCost,
1388
+ billingPeriod,
1389
+ recordedTokens: domain.observationCount > 0 ? domain.recordedTokens : null,
1390
+ retailEquivalent: retail,
1391
+ valueRatio,
1392
+ ratioBound,
1393
+ status: valueRatio === null ? "unavailable" : ratioBound === "lower" ? "partial" : "available",
1394
+ effectiveUnitPrice: unitPricePerMillion(windowPlanCost.amount, domain.recordedTokens),
1395
+ retailUnitPrice: unitPricePerMillion(retailAmount, retail.pricedTokens),
1396
+ pricingCoverage: retail.pricingCoverage,
1397
+ authorities: domain.authorities,
1398
+ lastObservedAt: domain.lastObservedAt
1399
+ });
1400
+ }
1401
+ const rank = (entry) => entry.valueRatio ?? -1;
1402
+ entries.sort(
1403
+ (left, right) => rank(right) - rank(left) || `${left.providerId}:${left.billingDomainId}`.localeCompare(
1404
+ `${right.providerId}:${right.billingDomainId}`
1405
+ )
1406
+ );
1407
+ metered.sort(
1408
+ (left, right) => `${left.providerId}:${left.billingDomainId}`.localeCompare(
1409
+ `${right.providerId}:${right.billingDomainId}`
1410
+ )
1411
+ );
1412
+ unconfigured.sort(
1413
+ (left, right) => right.recordedTokens - left.recordedTokens || `${left.providerId}:${left.billingDomainId}`.localeCompare(
1414
+ `${right.providerId}:${right.billingDomainId}`
1415
+ )
1416
+ );
1417
+ return {
1418
+ windowDays: preciseAmount(days),
1419
+ comparisonCurrency,
1420
+ catalogVersion: SUBSCRIPTION_PLAN_CATALOG.version,
1421
+ entries,
1422
+ meteredDomains: metered,
1423
+ unconfiguredDomains: unconfigured
1424
+ };
1425
+ }
1426
+ var PLAN_PERIOD_DAYS, MILLISECONDS_PER_DAY, STALE_RATE_MILLISECONDS, SUBSCRIPTION_PLAN_CATALOG;
1427
+ var init_plan_pricing = __esm({
1428
+ "src/core/plan-pricing.ts"() {
1429
+ "use strict";
1430
+ PLAN_PERIOD_DAYS = {
1431
+ monthly: 365.25 / 12,
1432
+ annual: 365.25
1433
+ };
1434
+ MILLISECONDS_PER_DAY = 24 * 60 * 60 * 1e3;
1435
+ STALE_RATE_MILLISECONDS = 7 * MILLISECONDS_PER_DAY;
1436
+ SUBSCRIPTION_PLAN_CATALOG = {
1437
+ version: "2026-08-30",
1438
+ entries: [
1439
+ {
1440
+ id: "claude-pro-monthly",
1441
+ providerId: "claude-code",
1442
+ billingDomainId: "subscription",
1443
+ displayName: "Claude Pro",
1444
+ amount: 20,
1445
+ currency: "USD",
1446
+ billingPeriod: "monthly",
1447
+ source: {
1448
+ title: "Claude plans and pricing",
1449
+ url: "https://claude.com/pricing",
1450
+ retrievedAt: "2026-08-30"
1451
+ }
1452
+ },
1453
+ {
1454
+ id: "claude-pro-annual",
1455
+ providerId: "claude-code",
1456
+ billingDomainId: "subscription",
1457
+ displayName: "Claude Pro (annual billing)",
1458
+ amount: 17 * 12,
1459
+ currency: "USD",
1460
+ billingPeriod: "annual",
1461
+ source: {
1462
+ title: "Claude plans and pricing",
1463
+ url: "https://claude.com/pricing",
1464
+ retrievedAt: "2026-08-30"
1465
+ }
1466
+ },
1467
+ {
1468
+ id: "claude-max-5x",
1469
+ providerId: "claude-code",
1470
+ billingDomainId: "subscription",
1471
+ displayName: "Claude Max 5x",
1472
+ amount: 100,
1473
+ currency: "USD",
1474
+ billingPeriod: "monthly",
1475
+ source: {
1476
+ title: "What is the Max plan?",
1477
+ url: "https://support.claude.com/en/articles/11049741-what-is-the-max-plan",
1478
+ retrievedAt: "2026-08-30"
1479
+ }
1480
+ },
1481
+ {
1482
+ id: "claude-max-20x",
1483
+ providerId: "claude-code",
1484
+ billingDomainId: "subscription",
1485
+ displayName: "Claude Max 20x",
1486
+ amount: 200,
1487
+ currency: "USD",
1488
+ billingPeriod: "monthly",
1489
+ source: {
1490
+ title: "What is the Max plan?",
1491
+ url: "https://support.claude.com/en/articles/11049741-what-is-the-max-plan",
1492
+ retrievedAt: "2026-08-30"
1493
+ }
1494
+ },
1495
+ {
1496
+ id: "chatgpt-plus",
1497
+ providerId: "codex",
1498
+ billingDomainId: "subscription",
1499
+ displayName: "ChatGPT Plus",
1500
+ amount: 20,
1501
+ currency: "USD",
1502
+ billingPeriod: "monthly",
1503
+ source: {
1504
+ title: "ChatGPT pricing",
1505
+ url: "https://chatgpt.com/pricing/",
1506
+ retrievedAt: "2026-08-30"
1507
+ }
1508
+ },
1509
+ {
1510
+ id: "chatgpt-pro-100",
1511
+ providerId: "codex",
1512
+ billingDomainId: "subscription",
1513
+ displayName: "ChatGPT Pro (5x)",
1514
+ amount: 100,
1515
+ currency: "USD",
1516
+ billingPeriod: "monthly",
1517
+ source: {
1518
+ title: "About ChatGPT Pro tiers",
1519
+ url: "https://help.openai.com/en/articles/9793128-about-chatgpt-pro-tiers",
1520
+ retrievedAt: "2026-08-30"
1521
+ }
1522
+ },
1523
+ {
1524
+ id: "chatgpt-pro-200",
1525
+ providerId: "codex",
1526
+ billingDomainId: "subscription",
1527
+ displayName: "ChatGPT Pro (20x)",
1528
+ amount: 200,
1529
+ currency: "USD",
1530
+ billingPeriod: "monthly",
1531
+ source: {
1532
+ title: "About ChatGPT Pro tiers",
1533
+ url: "https://help.openai.com/en/articles/9793128-about-chatgpt-pro-tiers",
1534
+ retrievedAt: "2026-08-30"
1535
+ }
1536
+ },
1537
+ {
1538
+ id: "chatgpt-business-seat",
1539
+ providerId: "codex",
1540
+ billingDomainId: "subscription",
1541
+ displayName: "ChatGPT Business (one seat)",
1542
+ amount: 25,
1543
+ currency: "USD",
1544
+ billingPeriod: "monthly",
1545
+ source: {
1546
+ title: "ChatGPT Business pricing",
1547
+ url: "https://openai.com/business/chatgpt-pricing/",
1548
+ retrievedAt: "2026-08-30"
1549
+ }
1550
+ },
1551
+ {
1552
+ id: "opencode-go-monthly",
1553
+ providerId: "opencode-go",
1554
+ billingDomainId: "go-subscription",
1555
+ displayName: "OpenCode Go",
1556
+ amount: 10,
1557
+ currency: "USD",
1558
+ billingPeriod: "monthly",
1559
+ source: {
1560
+ title: "OpenCode Go",
1561
+ url: "https://opencode.ai/go",
1562
+ retrievedAt: "2026-08-30"
1563
+ }
1564
+ }
1565
+ ]
1566
+ };
1567
+ }
1568
+ });
1569
+
1187
1570
  // src/core/usage-application.ts
1188
1571
  function safeConnectorFailure(error) {
1189
1572
  if (error instanceof Error && "code" in error && typeof error.code === "string" && "recovery" in error && typeof error.recovery === "string") {
@@ -1289,6 +1672,12 @@ async function withTimeout(promise, timeoutMs, message) {
1289
1672
  function notificationLevel(value) {
1290
1673
  return value === "5" ? 2 : value === "20" ? 1 : 0;
1291
1674
  }
1675
+ function pickAgentProviderIndex(overview) {
1676
+ return {
1677
+ generatedAt: overview.generatedAt,
1678
+ providers: overview.providers.filter((provider) => provider.id !== "opencode").map(({ id, displayName }) => ({ id, displayName }))
1679
+ };
1680
+ }
1292
1681
  var UsageApplication;
1293
1682
  var init_usage_application = __esm({
1294
1683
  "src/core/usage-application.ts"() {
@@ -1297,6 +1686,7 @@ var init_usage_application = __esm({
1297
1686
  init_redaction();
1298
1687
  init_usage_export();
1299
1688
  init_retail_pricing();
1689
+ init_plan_pricing();
1300
1690
  UsageApplication = class {
1301
1691
  #repository;
1302
1692
  #connectors;
@@ -1310,10 +1700,13 @@ var init_usage_application = __esm({
1310
1700
  #notifier;
1311
1701
  #startAtLoginManager;
1312
1702
  #priceCatalog;
1703
+ #planCatalog;
1313
1704
  #refreshPromise = null;
1314
1705
  #refreshMode = null;
1315
1706
  #backgroundPromise = null;
1316
1707
  #queuedHardRebuildPromise = null;
1708
+ #queuedUserProcessingPromise = null;
1709
+ #userInitiatedProcessing = false;
1317
1710
  #databaseWriteQueue = Promise.resolve();
1318
1711
  #processingStatus;
1319
1712
  constructor(options) {
@@ -1329,6 +1722,7 @@ var init_usage_application = __esm({
1329
1722
  this.#notifier = options.notifier;
1330
1723
  this.#startAtLoginManager = options.startAtLoginManager;
1331
1724
  this.#priceCatalog = options.priceCatalog === void 0 ? OFFICIAL_PRICING_CATALOG : options.priceCatalog;
1725
+ this.#planCatalog = options.planCatalog ?? SUBSCRIPTION_PLAN_CATALOG;
1332
1726
  this.#processingStatus = createProcessingStatus(this.#clock().toISOString(), false);
1333
1727
  }
1334
1728
  async #backfillRetailCosts(mode) {
@@ -1375,40 +1769,50 @@ var init_usage_application = __esm({
1375
1769
  getProcessingStatus() {
1376
1770
  return structuredClone(this.#processingStatus);
1377
1771
  }
1378
- startBackgroundProcessing() {
1379
- return this.#startProcessing("incremental");
1772
+ startBackgroundProcessing(options = {}) {
1773
+ return this.#startProcessing("incremental", options.userInitiated === true);
1380
1774
  }
1381
1775
  startHardRebuild() {
1382
- return this.#startProcessing("hard-rebuild");
1776
+ return this.#startProcessing("hard-rebuild", true);
1383
1777
  }
1384
- #startProcessing(mode) {
1778
+ #startProcessing(mode, userInitiated) {
1385
1779
  if (this.#backgroundPromise) {
1386
1780
  if (mode === "hard-rebuild" && !this.#processingStatus.hardRebuild) {
1387
1781
  this.#queuedHardRebuildPromise ??= this.#backgroundPromise.then(
1388
- () => this.#startProcessing("hard-rebuild"),
1389
- () => this.#startProcessing("hard-rebuild")
1782
+ () => this.#startProcessing("hard-rebuild", true),
1783
+ () => this.#startProcessing("hard-rebuild", true)
1390
1784
  ).finally(() => {
1391
1785
  this.#queuedHardRebuildPromise = null;
1392
1786
  });
1393
1787
  return this.#queuedHardRebuildPromise;
1394
1788
  }
1789
+ if (userInitiated && !this.#userInitiatedProcessing) {
1790
+ this.#queuedUserProcessingPromise ??= this.#backgroundPromise.then(
1791
+ () => this.#startProcessing(mode, true),
1792
+ () => this.#startProcessing(mode, true)
1793
+ ).finally(() => {
1794
+ this.#queuedUserProcessingPromise = null;
1795
+ });
1796
+ return this.#queuedUserProcessingPromise;
1797
+ }
1395
1798
  return this.#backgroundPromise;
1396
1799
  }
1800
+ this.#userInitiatedProcessing = userInitiated || mode === "hard-rebuild";
1397
1801
  this.#processingStatus = createProcessingStatus(
1398
1802
  this.#clock().toISOString(),
1399
1803
  mode === "hard-rebuild"
1400
1804
  );
1401
- this.#backgroundPromise = this.#runProcessing(mode).finally(() => {
1402
- this.#backgroundPromise = null;
1403
- });
1805
+ this.#backgroundPromise = this.#runProcessing(mode, this.#userInitiatedProcessing).finally(
1806
+ () => {
1807
+ this.#backgroundPromise = null;
1808
+ this.#userInitiatedProcessing = false;
1809
+ }
1810
+ );
1404
1811
  return this.#backgroundPromise;
1405
1812
  }
1406
- async #runProcessing(mode) {
1813
+ async #runProcessing(mode, userInitiated) {
1407
1814
  await this.#runModule("discovery", () => this.discoverConnectors().then(() => void 0));
1408
- await this.#runModule(
1409
- "usage",
1410
- () => this.refresh({ userInitiated: mode === "hard-rebuild", mode })
1411
- );
1815
+ await this.#runModule("usage", () => this.refresh({ userInitiated, mode }));
1412
1816
  await this.#runModule(
1413
1817
  "pricing",
1414
1818
  () => this.#queueDatabaseWrite(async () => {
@@ -1619,6 +2023,83 @@ var init_usage_application = __esm({
1619
2023
  connectors: this.#repository.getConnectorRuntimeStates()
1620
2024
  };
1621
2025
  }
2026
+ async getPlanSettings() {
2027
+ return {
2028
+ catalogVersion: this.#planCatalog.version,
2029
+ domains: this.#planEligibleDomains(),
2030
+ subscriptions: this.#repository.getPlanSubscriptions()
2031
+ };
2032
+ }
2033
+ async updatePlanSubscription(input) {
2034
+ const domain = this.#planEligibleDomains().find(
2035
+ (candidate) => candidate.providerId === input.providerId && candidate.billingDomainId === input.billingDomainId
2036
+ );
2037
+ if (!domain) throw new Error("Unknown subscription billing domain");
2038
+ if (input.plan === null) {
2039
+ this.#repository.deletePlanSubscription(input.providerId, input.billingDomainId);
2040
+ return this.getPlanSettings();
2041
+ }
2042
+ const preset = input.plan.planId ? planCatalogEntry(this.#planCatalog, input.plan.planId) : null;
2043
+ if (input.plan.planId && !preset) throw new Error("Unknown plan preset");
2044
+ if (preset && (preset.providerId !== input.providerId || preset.billingDomainId !== input.billingDomainId)) {
2045
+ throw new Error("Plan preset belongs to another billing domain");
2046
+ }
2047
+ const amount = input.plan.amount ?? preset?.amount;
2048
+ if (amount === void 0) throw new Error("A plan price is required");
2049
+ if (!Number.isFinite(amount) || amount <= 0) {
2050
+ throw new Error("A plan price must be a positive amount");
2051
+ }
2052
+ const currency = (input.plan.currency ?? preset?.currency ?? "USD").toUpperCase();
2053
+ if (!/^[A-Z]{3}$/.test(currency)) throw new Error("A plan currency must be a 3-letter code");
2054
+ const billingPeriod = input.plan.billingPeriod ?? preset?.billingPeriod ?? "monthly";
2055
+ const anchorDate = input.plan.anchorDate ?? null;
2056
+ if (anchorDate !== null && Number.isNaN((/* @__PURE__ */ new Date(`${anchorDate}T00:00:00.000Z`)).getTime())) {
2057
+ throw new Error("A renewal date must be a calendar date");
2058
+ }
2059
+ const overridesPreset = preset !== null && (amount !== preset.amount || currency !== preset.currency.toUpperCase() || billingPeriod !== preset.billingPeriod);
2060
+ this.#repository.savePlanSubscription({
2061
+ providerId: input.providerId,
2062
+ billingDomainId: input.billingDomainId,
2063
+ planId: preset?.id ?? null,
2064
+ displayName: preset?.displayName ?? "",
2065
+ amount,
2066
+ currency,
2067
+ billingPeriod,
2068
+ anchorDate,
2069
+ priceSource: preset && !overridesPreset ? "catalog-preset" : "user-entered",
2070
+ updatedAt: this.#clock().toISOString()
2071
+ });
2072
+ return this.getPlanSettings();
2073
+ }
2074
+ /**
2075
+ * A billing domain can carry a plan price when its connector does not report
2076
+ * an actual metered charge. Metered domains keep their own billed amounts and
2077
+ * never receive a declared subscription price.
2078
+ */
2079
+ #planEligibleDomains() {
2080
+ const domains = /* @__PURE__ */ new Map();
2081
+ for (const definition of this.#connectorDefinitions) {
2082
+ if (definition.expectedCoverage?.includes("actual-cost")) continue;
2083
+ const key = `${definition.target.provider.id}:${definition.target.billingDomain.id}`;
2084
+ if (domains.has(key)) continue;
2085
+ domains.set(key, {
2086
+ providerId: definition.target.provider.id,
2087
+ providerDisplayName: definition.target.provider.displayName,
2088
+ billingDomainId: definition.target.billingDomain.id,
2089
+ billingDomainDisplayName: definition.target.billingDomain.displayName,
2090
+ presets: planCatalogEntriesForDomain(
2091
+ this.#planCatalog,
2092
+ definition.target.provider.id,
2093
+ definition.target.billingDomain.id
2094
+ )
2095
+ });
2096
+ }
2097
+ return [...domains.values()].sort(
2098
+ (left, right) => `${left.providerId}:${left.billingDomainId}`.localeCompare(
2099
+ `${right.providerId}:${right.billingDomainId}`
2100
+ )
2101
+ );
2102
+ }
1622
2103
  async #sendNotificationTransitions() {
1623
2104
  if (!this.#notifier) return;
1624
2105
  const overview = this.#repository.getOverview(this.#clock());
@@ -1767,6 +2248,14 @@ var init_usage_application = __esm({
1767
2248
  async getOverview(query = {}) {
1768
2249
  return this.#repository.getOverview(this.#clock(), query);
1769
2250
  }
2251
+ async getAgentProviderIndex() {
2252
+ const now = this.#clock();
2253
+ return this.#repository.getAgentProviderIndex?.(now) ?? pickAgentProviderIndex(this.#repository.getOverview(now));
2254
+ }
2255
+ async getProviderOverview(providerId, query = {}) {
2256
+ const now = this.#clock();
2257
+ return this.#repository.getProviderOverview?.(now, providerId, query) ?? this.#repository.getOverview(now, query).providers.find((provider) => provider.id === providerId) ?? null;
2258
+ }
1770
2259
  async exportUsage(request) {
1771
2260
  const overview = this.#repository.getOverview(this.#clock(), request);
1772
2261
  const accountIdentifiers = request.includeAccountIdentifiers ? this.#repository.getProviderAccountIdentifiers() : {};
@@ -5041,6 +5530,25 @@ async function startLocalServer(options) {
5041
5530
  sendOtlpProtobuf(response, 200);
5042
5531
  return;
5043
5532
  }
5533
+ if (request.method === "GET" && requestUrl.pathname === "/api/overview/providers") {
5534
+ sendJson(response, 200, await options.application.getAgentProviderIndex());
5535
+ return;
5536
+ }
5537
+ if (request.method === "GET" && requestUrl.pathname.startsWith("/api/overview/providers/")) {
5538
+ const providerId = decodeURIComponent(
5539
+ requestUrl.pathname.slice("/api/overview/providers/".length)
5540
+ );
5541
+ const provider = await options.application.getProviderOverview(
5542
+ providerId,
5543
+ parseUsageQuery(requestUrl)
5544
+ );
5545
+ if (!provider) {
5546
+ sendJson(response, 404, { error: "provider-not-found" });
5547
+ return;
5548
+ }
5549
+ sendJson(response, 200, provider);
5550
+ return;
5551
+ }
5044
5552
  if (request.method === "GET" && requestUrl.pathname === "/api/overview") {
5045
5553
  const query = parseUsageQuery(requestUrl);
5046
5554
  sendJson(response, 200, await options.application.getOverview(query));
@@ -5051,6 +5559,13 @@ async function startLocalServer(options) {
5051
5559
  sendJson(response, 403, { error: "invalid-origin" });
5052
5560
  return;
5053
5561
  }
5562
+ if (requestUrl.searchParams.get("background") === "true") {
5563
+ void options.application.startBackgroundProcessing({
5564
+ userInitiated: requestUrl.searchParams.get("mode") !== "automatic"
5565
+ });
5566
+ sendJson(response, 202, { accepted: true });
5567
+ return;
5568
+ }
5054
5569
  await options.application.refresh({
5055
5570
  userInitiated: requestUrl.searchParams.get("mode") !== "automatic"
5056
5571
  });
@@ -5103,6 +5618,19 @@ async function startLocalServer(options) {
5103
5618
  sendJson(response, 200, await options.application.clearData(input));
5104
5619
  return;
5105
5620
  }
5621
+ if (request.method === "GET" && requestUrl.pathname === "/api/plans") {
5622
+ sendJson(response, 200, await options.application.getPlanSettings());
5623
+ return;
5624
+ }
5625
+ if (request.method === "PATCH" && requestUrl.pathname === "/api/plans") {
5626
+ if (!validMutationOrigin(authentication, request, origin)) {
5627
+ sendJson(response, 403, { error: "invalid-origin" });
5628
+ return;
5629
+ }
5630
+ const input = planSubscriptionSchema.parse(await readJsonBody(request));
5631
+ sendJson(response, 200, await options.application.updatePlanSubscription(input));
5632
+ return;
5633
+ }
5106
5634
  if (request.method === "GET" && requestUrl.pathname === "/api/monitoring") {
5107
5635
  sendJson(response, 200, await options.application.getMonitoringStatus());
5108
5636
  return;
@@ -5301,7 +5829,7 @@ function sendHtml(response, status, body) {
5301
5829
  response.writeHead(status, { "content-type": "text/html; charset=utf-8" });
5302
5830
  response.end(body);
5303
5831
  }
5304
- var SESSION_COOKIE, connectorActionSchema, usageQuerySchema, monitoringSettingsSchema, clearDataSchema, hardRebuildSchema;
5832
+ var SESSION_COOKIE, connectorActionSchema, usageQuerySchema, monitoringSettingsSchema, planSubscriptionSchema, clearDataSchema, hardRebuildSchema;
5305
5833
  var init_local_server = __esm({
5306
5834
  "src/server/local-server.ts"() {
5307
5835
  "use strict";
@@ -5324,6 +5852,17 @@ var init_local_server = __esm({
5324
5852
  notificationsEnabled: z9.boolean().optional(),
5325
5853
  startAtLogin: z9.boolean().optional()
5326
5854
  }).refine((value) => Object.keys(value).length > 0, "At least one setting is required");
5855
+ planSubscriptionSchema = z9.object({
5856
+ providerId: z9.string().min(1),
5857
+ billingDomainId: z9.string().min(1),
5858
+ plan: z9.object({
5859
+ planId: z9.string().min(1).nullable(),
5860
+ amount: z9.number().positive().finite().optional(),
5861
+ currency: z9.string().length(3).optional(),
5862
+ billingPeriod: z9.enum(["monthly", "annual"]).optional(),
5863
+ anchorDate: z9.string().regex(/^\d{4}-\d{2}-\d{2}$/).nullable().optional()
5864
+ }).nullable()
5865
+ });
5327
5866
  clearDataSchema = z9.object({ deleteProductSecrets: z9.boolean().default(false) });
5328
5867
  hardRebuildSchema = z9.object({ confirmExpensiveOperation: z9.literal(true) });
5329
5868
  }
@@ -6253,7 +6792,7 @@ function buildGlobalSummary(providers, riskSummary, now, query) {
6253
6792
  tokenEvidence,
6254
6793
  apiRetailEquivalent: {
6255
6794
  status: sawRetailEquivalent ? "available" : "unavailable",
6256
- amount: sawRetailEquivalent ? preciseAmount(retailAmount) : null,
6795
+ amount: sawRetailEquivalent ? preciseAmount2(retailAmount) : null,
6257
6796
  currency: "USD",
6258
6797
  pricingCoverage: tokenEvidence.recordedTokens === 0 ? null : retailPricedTokens / tokenEvidence.recordedTokens
6259
6798
  },
@@ -6263,7 +6802,7 @@ function buildGlobalSummary(providers, riskSummary, now, query) {
6263
6802
  contributions
6264
6803
  };
6265
6804
  }
6266
- function buildTokenMoneyWorkbench(providers, now, query) {
6805
+ function buildTokenMoneyWorkbench(providers, now, query, planSubscriptions, planBillingPeriods) {
6267
6806
  const normalized = normalizeUsageQuery(now, query);
6268
6807
  const allHistories = allDomainHistories(providers);
6269
6808
  const headlineHistories = allHistories.filter(({ includedInHeadline }) => includedInHeadline);
@@ -6454,9 +6993,43 @@ function buildTokenMoneyWorkbench(providers, now, query) {
6454
6993
  observationCount > 0 ? recordedTokens : null,
6455
6994
  retailEquivalent,
6456
6995
  reportedEstimate
6457
- )
6996
+ ),
6997
+ planValue: buildWorkbenchPlanValue({
6998
+ domains: planValueDomains(allHistories, comparisonCurrency),
6999
+ subscriptions: planSubscriptions,
7000
+ comparisonCurrency,
7001
+ start: normalized.start.toISOString(),
7002
+ end: normalized.end.toISOString(),
7003
+ rates: uniqueExchangeRates(allHistories.flatMap(({ history }) => history.exchangeRates)),
7004
+ billingPeriods: planBillingPeriods
7005
+ })
6458
7006
  };
6459
7007
  }
7008
+ function planValueDomains(histories, comparisonCurrency) {
7009
+ return histories.map(({ provider, domain, history, includedInHeadline }) => {
7010
+ const domainTokens = history.tokenEvidence.recordedTokens;
7011
+ const metric = (purpose) => buildWorkbenchMetric(
7012
+ history.costs,
7013
+ purpose,
7014
+ comparisonCurrency,
7015
+ domainTokens,
7016
+ history.exchangeRates
7017
+ );
7018
+ return {
7019
+ providerId: provider.id,
7020
+ providerDisplayName: provider.displayName,
7021
+ billingDomainId: domain.id,
7022
+ billingDomainDisplayName: domain.displayName,
7023
+ includedInHeadline,
7024
+ recordedTokens: domainTokens,
7025
+ observationCount: history.tokenEvidence.observationCount,
7026
+ retailEquivalent: metric("retail-equivalent"),
7027
+ actualCost: metric("actual"),
7028
+ authorities: history.authorities ?? [],
7029
+ lastObservedAt: history.lastObservedAt ?? null
7030
+ };
7031
+ });
7032
+ }
6460
7033
  function allDomainHistories(providers) {
6461
7034
  return providers.flatMap(
6462
7035
  (provider) => provider.billingDomains.map((domain) => ({
@@ -6512,11 +7085,11 @@ function buildWorkbenchMetric(costs, purpose, comparisonCurrency, recordedTokens
6512
7085
  return {
6513
7086
  purpose,
6514
7087
  status,
6515
- amount: status === "available" ? preciseAmount(convertedAmount) : null,
7088
+ amount: status === "available" ? preciseAmount2(convertedAmount) : null,
6516
7089
  comparisonCurrency,
6517
7090
  nativeAmounts: [...native.entries()].map(([currency, amount]) => ({
6518
7091
  currency,
6519
- amount: amount.complete ? preciseAmount(amount.amount) : null,
7092
+ amount: amount.complete ? preciseAmount2(amount.amount) : null,
6520
7093
  records: amount.records,
6521
7094
  knownRecords: amount.knownRecords
6522
7095
  })).sort((left, right) => left.currency.localeCompare(right.currency)),
@@ -7108,7 +7681,7 @@ function round(value) {
7108
7681
  function pricingInputsChanged(existing, observation) {
7109
7682
  return existing.billing_domain_id !== observation.billingDomainId || existing.model !== (observation.model?.trim() || "__unclassified__") || existing.observed_at !== observation.observedAt || Number(existing.total_tokens) !== observation.recordedTokens || Number(existing.input_tokens) !== observation.inputTokens || Number(existing.output_tokens) !== observation.outputTokens || Number(existing.reasoning_tokens) !== observation.reasoningTokens || Number(existing.cache_read_tokens) !== observation.cacheReadTokens || Number(existing.cache_write_tokens) !== observation.cacheWriteTokens || (existing.cache_write_5m_tokens === null ? null : Number(existing.cache_write_5m_tokens)) !== (observation.cacheWriteTokenBreakdown?.fiveMinute ?? null) || (existing.cache_write_1h_tokens === null ? null : Number(existing.cache_write_1h_tokens)) !== (observation.cacheWriteTokenBreakdown?.oneHour ?? null) || Number(existing.unclassified_tokens) !== observation.unclassifiedTokens || existing.reasoning_semantics !== observation.tokenSemantics.reasoning || existing.cache_read_semantics !== observation.tokenSemantics.cacheRead || existing.cache_write_semantics !== observation.tokenSemantics.cacheWrite || existing.model_attribution !== observation.modelAttribution || existing.time_precision !== observation.timePrecision || existing.aggregation_temporality !== observation.aggregationTemporality;
7110
7683
  }
7111
- function preciseAmount(value) {
7684
+ function preciseAmount2(value) {
7112
7685
  return Number(value.toFixed(12));
7113
7686
  }
7114
7687
  function tokenAuthority(authorities) {
@@ -7144,6 +7717,7 @@ var init_sqlite_usage_repository = __esm({
7144
7717
  "src/server/sqlite-usage-repository.ts"() {
7145
7718
  "use strict";
7146
7719
  init_token_normalization();
7720
+ init_plan_pricing();
7147
7721
  FRESHNESS_WINDOW_MS = 15 * 60 * 1e3;
7148
7722
  QUERY_INDEXES_SQL = `
7149
7723
  CREATE INDEX IF NOT EXISTS usage_observed_at_idx
@@ -7704,14 +8278,38 @@ var init_sqlite_usage_repository = __esm({
7704
8278
  (provider) => this.#getProviderOverview(provider, now, query)
7705
8279
  );
7706
8280
  const riskSummary = buildRiskSummary(overviews);
8281
+ const planSubscriptions = this.getPlanSubscriptions();
7707
8282
  return {
7708
8283
  generatedAt: now.toISOString(),
7709
8284
  globalSummary: buildGlobalSummary(overviews, riskSummary, now, query),
7710
- workbench: buildTokenMoneyWorkbench(overviews, now, query),
8285
+ workbench: buildTokenMoneyWorkbench(
8286
+ overviews,
8287
+ now,
8288
+ query,
8289
+ planSubscriptions,
8290
+ this.#planBillingPeriodSummaries(planSubscriptions, now, query)
8291
+ ),
7711
8292
  providers: overviews,
7712
8293
  riskSummary
7713
8294
  };
7714
8295
  }
8296
+ getAgentProviderIndex(now) {
8297
+ const providers = this.#database.prepare("SELECT id, display_name FROM providers ORDER BY id").all();
8298
+ return {
8299
+ generatedAt: now.toISOString(),
8300
+ providers: providers.filter(
8301
+ (provider) => provider.id !== "opencode" && (!this.#hideDemoProvider || provider.id !== "demo")
8302
+ ).map((provider) => ({ id: provider.id, displayName: provider.display_name }))
8303
+ };
8304
+ }
8305
+ getProviderOverview(now, providerId, query = {}) {
8306
+ if (this.#hideDemoProvider && providerId === "demo") return null;
8307
+ const provider = this.#database.prepare(
8308
+ `SELECT id, display_name, last_success_at, last_error, last_error_code, last_recovery
8309
+ FROM providers WHERE id = ?`
8310
+ ).get(providerId);
8311
+ return provider ? this.#getProviderOverview(provider, now, query) : null;
8312
+ }
7715
8313
  saveExchangeRateSnapshot(snapshot2) {
7716
8314
  this.#database.prepare(
7717
8315
  `INSERT INTO exchange_rate_snapshots (
@@ -7787,6 +8385,57 @@ var init_sqlite_usage_repository = __esm({
7787
8385
  settings.startAtLogin ? 1 : 0
7788
8386
  );
7789
8387
  }
8388
+ getPlanSubscriptions() {
8389
+ const rows = this.#database.prepare(
8390
+ `SELECT provider_id, billing_domain_id, plan_id, display_name, amount, currency,
8391
+ billing_period, anchor_date, price_source, updated_at
8392
+ FROM plan_subscriptions
8393
+ ORDER BY provider_id, billing_domain_id`
8394
+ ).all();
8395
+ return rows.map((row) => ({
8396
+ providerId: row.provider_id,
8397
+ billingDomainId: row.billing_domain_id,
8398
+ planId: row.plan_id,
8399
+ displayName: row.display_name,
8400
+ amount: row.amount,
8401
+ currency: row.currency,
8402
+ billingPeriod: row.billing_period,
8403
+ anchorDate: row.anchor_date,
8404
+ priceSource: row.price_source,
8405
+ updatedAt: row.updated_at
8406
+ }));
8407
+ }
8408
+ savePlanSubscription(subscription) {
8409
+ this.#database.prepare(
8410
+ `INSERT INTO plan_subscriptions (
8411
+ provider_id, billing_domain_id, plan_id, display_name, amount, currency,
8412
+ billing_period, anchor_date, price_source, updated_at
8413
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
8414
+ ON CONFLICT(provider_id, billing_domain_id) DO UPDATE SET
8415
+ plan_id = excluded.plan_id,
8416
+ display_name = excluded.display_name,
8417
+ amount = excluded.amount,
8418
+ currency = excluded.currency,
8419
+ billing_period = excluded.billing_period,
8420
+ anchor_date = excluded.anchor_date,
8421
+ price_source = excluded.price_source,
8422
+ updated_at = excluded.updated_at`
8423
+ ).run(
8424
+ subscription.providerId,
8425
+ subscription.billingDomainId,
8426
+ subscription.planId,
8427
+ subscription.displayName,
8428
+ subscription.amount,
8429
+ subscription.currency,
8430
+ subscription.billingPeriod,
8431
+ subscription.anchorDate,
8432
+ subscription.priceSource,
8433
+ subscription.updatedAt
8434
+ );
8435
+ }
8436
+ deletePlanSubscription(providerId, billingDomainId) {
8437
+ this.#database.prepare("DELETE FROM plan_subscriptions WHERE provider_id = ? AND billing_domain_id = ?").run(providerId, billingDomainId);
8438
+ }
7790
8439
  getNotificationState(key) {
7791
8440
  const row = this.#database.prepare("SELECT value FROM notification_state WHERE key = ?").get(key);
7792
8441
  return row?.value ?? null;
@@ -8245,6 +8894,101 @@ var init_sqlite_usage_repository = __esm({
8245
8894
  coverageByDomain
8246
8895
  };
8247
8896
  }
8897
+ /**
8898
+ * Cycle-to-date evidence for every subscription that declares a renewal date.
8899
+ * Each Provider is measured over its own billing period, which is why this
8900
+ * cannot reuse the one rolling window the rest of the workbench shares.
8901
+ */
8902
+ #planBillingPeriodSummaries(subscriptions, now, query) {
8903
+ const normalized = normalizeUsageQuery(now, query);
8904
+ const summaries = /* @__PURE__ */ new Map();
8905
+ for (const subscription of subscriptions) {
8906
+ if (!subscription.anchorDate) continue;
8907
+ const period = billingPeriodContaining(
8908
+ subscription.anchorDate,
8909
+ subscription.billingPeriod,
8910
+ now
8911
+ );
8912
+ if (!period) continue;
8913
+ const observedThrough = new Date(Math.min(now.getTime(), period.end.getTime()));
8914
+ summaries.set(`${subscription.providerId}:${subscription.billingDomainId}`, {
8915
+ start: period.start.toISOString(),
8916
+ end: period.end.toISOString(),
8917
+ observedThrough: observedThrough.toISOString(),
8918
+ ...this.#rangeUsageSummary(
8919
+ subscription.providerId,
8920
+ subscription.billingDomainId,
8921
+ period.start,
8922
+ observedThrough,
8923
+ normalized.comparisonCurrency
8924
+ )
8925
+ });
8926
+ }
8927
+ return summaries;
8928
+ }
8929
+ #rangeUsageSummary(providerId, billingDomainId, start, end, comparisonCurrency) {
8930
+ const startIso = start.toISOString();
8931
+ const endIso = end.toISOString();
8932
+ const usage = this.#database.prepare(
8933
+ `SELECT id, model, observed_at, authority, total_tokens, input_tokens, output_tokens,
8934
+ reasoning_tokens, cache_read_tokens, cache_write_tokens, cache_write_5m_tokens,
8935
+ cache_write_1h_tokens, source_reported_total_tokens,
8936
+ unclassified_tokens, total_derivation, model_attribution, time_precision,
8937
+ usage_scope, aggregation_temporality, reasoning_semantics,
8938
+ cache_read_semantics, cache_write_semantics
8939
+ FROM usage_observations
8940
+ WHERE provider_id = ? AND billing_domain_id = ?
8941
+ AND observed_at >= ? AND observed_at < ?
8942
+ AND ${additiveUsagePredicate()}
8943
+ ORDER BY observed_at, id`
8944
+ ).all(providerId, billingDomainId, startIso, endIso);
8945
+ const costs = this.#database.prepare(
8946
+ `SELECT id, source_id, billing_domain_id, observed_at, kind, amount, currency, authority,
8947
+ price_snapshot_id, price_snapshot_version, price_snapshot_source,
8948
+ price_snapshot_canonical_model, price_snapshot_effective_at,
8949
+ price_snapshot_effective_until, price_snapshot_currency,
8950
+ price_snapshot_rates_json, price_snapshot_source_url,
8951
+ price_snapshot_context_tier, model, usage_observation_id, priced_tokens,
8952
+ line_items_json, calculated_at
8953
+ FROM cost_records
8954
+ WHERE provider_id = ? AND billing_domain_id = ?
8955
+ AND observed_at >= ? AND observed_at < ?
8956
+ ORDER BY observed_at, id`
8957
+ ).all(providerId, billingDomainId, startIso, endIso);
8958
+ const rateRows = this.#database.prepare(
8959
+ `SELECT id, base_currency, quote_currency, rate, observed_at, source
8960
+ FROM exchange_rate_snapshots WHERE observed_at < ? ORDER BY observed_at DESC, id`
8961
+ ).all(endIso);
8962
+ const rateByCurrency = /* @__PURE__ */ new Map();
8963
+ for (const row of rateRows) {
8964
+ if (row.quote_currency === comparisonCurrency && !rateByCurrency.has(row.base_currency)) {
8965
+ rateByCurrency.set(row.base_currency, row);
8966
+ }
8967
+ }
8968
+ const evidence = emptyTokenEvidence();
8969
+ for (const row of usage) addTokenEvidence(evidence, row);
8970
+ const finished = finishTokenEvidence(evidence);
8971
+ const usedRates = /* @__PURE__ */ new Map();
8972
+ const historyCosts = summarizeHistoryCosts(
8973
+ costs,
8974
+ finished.recordedTokens,
8975
+ rateByCurrency,
8976
+ comparisonCurrency,
8977
+ end,
8978
+ usedRates
8979
+ );
8980
+ return {
8981
+ recordedTokens: finished.recordedTokens,
8982
+ observationCount: finished.observationCount,
8983
+ retailEquivalent: buildWorkbenchMetric(
8984
+ historyCosts,
8985
+ "retail-equivalent",
8986
+ comparisonCurrency,
8987
+ finished.recordedTokens,
8988
+ [...usedRates.values()]
8989
+ )
8990
+ };
8991
+ }
8248
8992
  #getBillingHistory(providerId, billingDomainId, now, query) {
8249
8993
  const normalized = normalizeUsageQuery(now, query);
8250
8994
  const start = normalized.start.toISOString();
@@ -8653,6 +9397,19 @@ var init_sqlite_usage_repository = __esm({
8653
9397
  key TEXT PRIMARY KEY,
8654
9398
  value TEXT NOT NULL
8655
9399
  );
9400
+ CREATE TABLE IF NOT EXISTS plan_subscriptions (
9401
+ provider_id TEXT NOT NULL,
9402
+ billing_domain_id TEXT NOT NULL,
9403
+ plan_id TEXT,
9404
+ display_name TEXT NOT NULL,
9405
+ amount REAL NOT NULL,
9406
+ currency TEXT NOT NULL,
9407
+ billing_period TEXT NOT NULL,
9408
+ anchor_date TEXT,
9409
+ price_source TEXT NOT NULL,
9410
+ updated_at TEXT NOT NULL,
9411
+ PRIMARY KEY (provider_id, billing_domain_id)
9412
+ );
8656
9413
  `);
8657
9414
  const usageColumns = this.#database.prepare("PRAGMA table_info(usage_observations)").all();
8658
9415
  if (!usageColumns.some((column) => column.name === "total_tokens")) {
@@ -8821,6 +9578,10 @@ var init_sqlite_usage_repository = __esm({
8821
9578
  this.#database.exec(`ALTER TABLE quota_buckets ADD COLUMN ${name} ${type}`);
8822
9579
  }
8823
9580
  }
9581
+ const planColumns = this.#database.prepare("PRAGMA table_info(plan_subscriptions)").all();
9582
+ if (planColumns.length > 0 && !planColumns.some((column) => column.name === "anchor_date")) {
9583
+ this.#database.exec("ALTER TABLE plan_subscriptions ADD COLUMN anchor_date TEXT");
9584
+ }
8824
9585
  }
8825
9586
  };
8826
9587
  }