agent-usage-all-in-one 0.9.0 → 0.10.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/README.md CHANGED
@@ -17,8 +17,6 @@ It offers advice but never switches agents automatically.
17
17
 
18
18
  ## Compared
19
19
 
20
- ![Category comparison: provider UIs, token exporters, FinOps dashboards, and Agent Usage](docs/design/compare-sheet.png)
21
-
22
20
  Honest category axes and when to prefer Agent Usage vs provider UIs, exporters, or FinOps dashboards: [docs/comparison.md](docs/comparison.md).
23
21
 
24
22
  Sourced named competitors (ccusage, CodeBurn, Token Monitor, TokenTracker): [docs/named-competitors.md](docs/named-competitors.md).
@@ -35,7 +33,8 @@ The dashboard has two primary tabs:
35
33
  selected window, metric, and currency sit in the same sticky header as the
36
34
  primary tabs. The summary board leads with the headline amount beside a
37
35
  GitHub-style last-year usage wall of headline-included recorded Tokens, then
38
- charts Provider share and the interactive daily trend, and shows model
36
+ charts each known model separately by agent and billing domain on the share
37
+ pie and the interactive daily trend, and shows model
39
38
  rankings with visual share bars and the public API retail equivalent of
40
39
  eligible token usage. The year wall is independent of the selected 24-hour,
41
40
  7-day, or 30-day window.
@@ -51,8 +50,6 @@ unclassified or unpriced instead of being guessed or displayed as zero.
51
50
  Grok Build/SuperGrok and xAI API are independent billing domains. Their quotas,
52
51
  tokens, and costs are never added together.
53
52
 
54
- ![Cost honesty: actual charges, estimates, subscriptions, and API retail equivalent stay separate](docs/design/cost-semantics-4.png)
55
-
56
53
  ## Fast, progressive startup
57
54
 
58
55
  The loopback web service starts before connector discovery or data processing.
@@ -60,11 +57,13 @@ Cached results are available immediately while discovery, provider usage, model
60
57
  pricing, and retention run as independent background modules. Each dashboard tab
61
58
  shows only its own update indicator and completed sections remain usable.
62
59
 
63
- Transcript scans use a persistent, path-redacted file index. Historical retail
64
- pricing is recalculated only when the pricing catalog version changes. SQLite
65
- time/provider/model indexes and retention compaction run in a worker after
66
- provider collection, while price backfill processes bounded pages. Settings
67
- includes an explicitly confirmed
60
+ Transcript scans use a persistent, path-redacted file index. Refresh derives API
61
+ retail equivalent only for new or changed Token observations. Historical retail
62
+ pricing backfills remaining unpriced observations when the pricing catalog
63
+ version changes. SQLite time/provider/model indexes and retention compaction run
64
+ in a worker after provider collection only when those jobs have work. Price
65
+ backfill processes bounded pages. Token and cost refresh bars follow price
66
+ derivation, not connector collection or retention. Settings includes an explicitly confirmed
68
67
  **Hard rebuild all data** action for troubleshooting. It ignores these caches, can
69
68
  use substantial resources, and may take a long time without blocking the web UI.
70
69
 
@@ -89,8 +88,6 @@ oversight — see [ADR 017](docs/adr/017-macos-node24-npm-runtime.md) and the
89
88
  [platform roadmap](docs/platform-roadmap.md). Linux and Homebrew / DMG are not
90
89
  shipped.
91
90
 
92
- ![Quick start: install and open the local dashboard in three steps](docs/design/install-3-steps.png)
93
-
94
91
  ```bash
95
92
  npx agent-usage-all-in-one
96
93
  ```
@@ -148,9 +145,7 @@ evidence remain visibly distinct.
148
145
 
149
146
  ## First launch / empty state
150
147
 
151
- Until connectors find local client data, the dashboard shows an honest empty state — connect or use an installed agent first:
152
-
153
- ![Empty state when no agent data is connected yet](docs/design/empty-state-unconnected.png)
148
+ Until connectors find local client data, the dashboard shows an honest empty state — connect or use an installed agent first.
154
149
 
155
150
  ## Credentials and privacy
156
151
 
@@ -188,8 +183,6 @@ Linux, Homebrew, and DMG stay off the product until the conditions in
188
183
 
189
184
  > **VISION only — not shipped.** Menu bar / always-available glance is tracked in [issue #19](https://github.com/Blackman99/agent-usage-all-in-one/issues/19). Do not treat the mock as a current feature.
190
185
 
191
- ![VISION (not shipped): menu bar glance concept — see issue #19](docs/design/menubar-vision.png)
192
-
193
186
  ## License and community
194
187
 
195
188
  MIT — see [LICENSE](LICENSE). Also see [CONTRIBUTING.md](CONTRIBUTING.md),
package/README.zh-CN.md CHANGED
@@ -16,8 +16,6 @@ API 对等费用、历史与诊断。它只提供建议,不会自动切换 Age
16
16
 
17
17
  ## 对比说明
18
18
 
19
- ![类别对比:各平台官网、Token 导出工具、FinOps 看板与 Agent Usage](docs/design/compare-sheet.png)
20
-
21
19
  按类别对比何时更适合用 Agent Usage,而非各平台官网、导出工具或通用 FinOps 看板:[docs/comparison.md](docs/comparison.md)。
22
20
 
23
21
  具名竞品对照(ccusage、CodeBurn、Token Monitor、TokenTracker):[docs/named-competitors.md](docs/named-competitors.md)。
@@ -29,7 +27,7 @@ API 对等费用、历史与诊断。它只提供建议,不会自动切换 Age
29
27
  页面只有两个主要标签页:
30
28
 
31
29
  - **Agent 用量**:保留各平台原生的 5 小时、周、月、All models 和 Fable only 等额度标签与刷新时间。
32
- - **Token 与模型费用**:支持 24 小时、7 天和 30 天范围。所选窗口、指标与币种和主标签页共用同一条吸顶导航。汇总卡片左侧是金额或 Token 头条,右侧是过去一年的 GitHub 风格用量墙(只统计计入摘要的已记录 Token),随后用图表展示 Provider 占比和可交互的每日趋势,再以占比条呈现模型排行及符合条件的 Token 按公开 API 价格计算出的对等费用。用量墙与所选的 24 小时 / 7 天 / 30 天窗口相互独立。
30
+ - **Token 与模型费用**:支持 24 小时、7 天和 30 天范围。所选窗口、指标与币种和主标签页共用同一条吸顶导航。汇总卡片左侧是金额或 Token 头条,右侧是过去一年的 GitHub 风格用量墙(只统计计入摘要的已记录 Token),随后用图表按 agent、计费域和模型分开展示占比与可交互的每日趋势,再以占比条呈现模型排行及符合条件的 Token 按公开 API 价格计算出的对等费用。用量墙与所选的 24 小时 / 7 天 / 30 天窗口相互独立。
33
31
  - **设置面板**:采用居中双栏结构,将连接管理、自定义模型费率、运行监控、系统诊断与数据隐私划分为 5 个独立分类,方便高效维护各项配置。
34
32
 
35
33
  实际账单、平台报告估算、固定订阅费和 API 对等零售价是四类互不混合的证据。
@@ -43,9 +41,9 @@ Grok Build/SuperGrok 与 xAI API 是两个独立计费域,其额度、Token
43
41
  本地 Web 服务会先启动,再在后台运行连接发现、平台用量、模型定价和保留整理四个独立模块。
44
42
  已有缓存会立即展示;每个标签页只在自己的数据区域显示更新状态,已经完成的内容始终可用。
45
43
 
46
- 本地会话记录扫描使用跨进程持久化且不保存原始个人路径的文件索引。历史费用只在价格目录版本变化时
47
- 重新计算,并按有限批次处理;时间、平台、模型和计费域索引及保留期整理会在平台采集完成后交给后台
48
- 工作线程执行。设置里提供需要明确确认的
44
+ 本地会话记录扫描使用跨进程持久化且不保存原始个人路径的文件索引。刷新只为新增或定价输入已变化的
45
+ Token 观测计算 API 对等零售价;历史费用只在价格目录版本变化时补算仍未定价的观测,并按有限批次处理。
46
+ 时间、平台、模型和计费域索引及保留期整理会在平台采集完成后交给后台工作线程,且仅在确有工作(索引尚未建立、或存在超过 90 天的原始观测)时执行。Token 与费用区域的刷新条只在价格推导实际运行时显示,不等待连接器采集或保留整理。设置里提供需要明确确认的
49
47
  **硬重算全部数据**:它会忽略缓存、消耗较多资源,并且可能等待很久,但不会阻塞页面。
50
48
 
51
49
  ## 开发调试
package/dist/cli.js CHANGED
@@ -789,17 +789,44 @@ function mergeCatalogWithCustomRates(catalog, customRates) {
789
789
  entries: [...customEntries, ...catalog.entries]
790
790
  };
791
791
  }
792
+ function catalogModelKey(providerId, model) {
793
+ return `${providerId}\0${model.trim().toLowerCase()}`;
794
+ }
795
+ function indexRetailPriceCatalog(catalog) {
796
+ const cached = catalogIndexes.get(catalog);
797
+ if (cached) return cached;
798
+ const byProviderAndModel = /* @__PURE__ */ new Map();
799
+ for (const entry of catalog.entries) {
800
+ const models = /* @__PURE__ */ new Set([entry.canonicalModel, ...entry.aliases]);
801
+ for (const model of models) {
802
+ const key = catalogModelKey(entry.providerId, model);
803
+ const list = byProviderAndModel.get(key);
804
+ if (list) list.push(entry);
805
+ else byProviderAndModel.set(key, [entry]);
806
+ }
807
+ }
808
+ const index = { byProviderAndModel };
809
+ catalogIndexes.set(catalog, index);
810
+ return index;
811
+ }
812
+ function matchingCatalogEntries(index, providerId, observation, normalizedModel) {
813
+ const entries = index.byProviderAndModel.get(catalogModelKey(providerId, normalizedModel)) ?? [];
814
+ return entries.filter(
815
+ (entry) => entry.billingDomainId === observation.billingDomainId || entry.billingDomainId === "*"
816
+ );
817
+ }
792
818
  function deriveRetailEquivalentCosts(snapshot2, catalog = ANTHROPIC_PRICING_CATALOG, calculatedAt = snapshot2.observedAt) {
793
819
  const costs = [];
794
820
  const decisions = [];
821
+ const index = indexRetailPriceCatalog(catalog);
795
822
  for (const observation of snapshot2.usage) {
796
- const result = priceObservation(snapshot2.provider.id, observation, catalog, calculatedAt);
823
+ const result = priceObservation(snapshot2.provider.id, observation, index, calculatedAt);
797
824
  decisions.push(result.decision);
798
825
  if (result.cost) costs.push(result.cost);
799
826
  }
800
827
  return { costs, decisions };
801
828
  }
802
- function priceObservation(providerId, observation, catalog, calculatedAt) {
829
+ function priceObservation(providerId, observation, index, calculatedAt) {
803
830
  const unavailable = (reason) => ({
804
831
  cost: null,
805
832
  decision: {
@@ -814,10 +841,11 @@ function priceObservation(providerId, observation, catalog, calculatedAt) {
814
841
  return unavailable("model-unclassified");
815
842
  }
816
843
  const normalizedModel = normalized.model.trim().toLowerCase();
817
- const matchingCandidates = catalog.entries.filter(
818
- (entry2) => entry2.providerId === providerId && (entry2.billingDomainId === observation.billingDomainId || entry2.billingDomainId === "*") && [entry2.canonicalModel, ...entry2.aliases].some(
819
- (model) => model.trim().toLowerCase() === normalizedModel
820
- )
844
+ const matchingCandidates = matchingCatalogEntries(
845
+ index,
846
+ providerId,
847
+ observation,
848
+ normalizedModel
821
849
  );
822
850
  let selectedEntries;
823
851
  const customExact = matchingCandidates.filter(
@@ -977,7 +1005,7 @@ function matchContext(rule, observation) {
977
1005
  function preciseMoney(value) {
978
1006
  return Number(value.toFixed(12));
979
1007
  }
980
- var ANTHROPIC_PRICING_CATALOG, OPENCODE_GO_SOURCE, XAI_PRICING_SOURCE, OPENAI_GPT_56_SOURCE, OPENAI_GPT_6_ASTRA_SOURCE, OPEN_CODE_GO_SOURCE_PATH, openCodeHistory, OPEN_CODE_BASE_HISTORY, OPEN_CODE_PRICE_HISTORY, OPEN_CODE_FLAT_MODELS, OPEN_CODE_CONTEXT_MODELS, DEEPSEEK_GO_MODELS, WEEKDAY_PEAK_RANGES, DEEPSEEK_OFFICIAL_SOURCE, DEEPSEEK_OFFICIAL_MODELS, DEEPSEEK_OFFICIAL_EFFECTIVE_FROM, GOOGLE_ANTIGRAVITY_SOURCE, GOOGLE_ANTIGRAVITY_EFFECTIVE_FROM, DIRECT_OFFICIAL_PRICING_CATALOG, OFFICIAL_PRICING_CATALOG;
1008
+ var ANTHROPIC_PRICING_CATALOG, OPENCODE_GO_SOURCE, XAI_PRICING_SOURCE, OPENAI_GPT_56_SOURCE, OPENAI_GPT_6_ASTRA_SOURCE, OPEN_CODE_GO_SOURCE_PATH, openCodeHistory, OPEN_CODE_BASE_HISTORY, OPEN_CODE_PRICE_HISTORY, OPEN_CODE_FLAT_MODELS, OPEN_CODE_CONTEXT_MODELS, DEEPSEEK_GO_MODELS, WEEKDAY_PEAK_RANGES, DEEPSEEK_OFFICIAL_SOURCE, DEEPSEEK_OFFICIAL_MODELS, DEEPSEEK_OFFICIAL_EFFECTIVE_FROM, GOOGLE_ANTIGRAVITY_SOURCE, GOOGLE_ANTIGRAVITY_EFFECTIVE_FROM, DIRECT_OFFICIAL_PRICING_CATALOG, OFFICIAL_PRICING_CATALOG, catalogIndexes;
981
1009
  var init_retail_pricing = __esm({
982
1010
  "src/core/retail-pricing.ts"() {
983
1011
  "use strict";
@@ -1447,6 +1475,7 @@ var init_retail_pricing = __esm({
1447
1475
  ...openCodeLocalHistoryEntries(DIRECT_OFFICIAL_PRICING_CATALOG.entries)
1448
1476
  ]
1449
1477
  };
1478
+ catalogIndexes = /* @__PURE__ */ new WeakMap();
1450
1479
  }
1451
1480
  });
1452
1481
 
@@ -1492,7 +1521,7 @@ function yieldToEventLoop() {
1492
1521
  }
1493
1522
  function yieldForPendingHttp() {
1494
1523
  return new Promise((resolve5) => {
1495
- setTimeout(resolve5, PENDING_HTTP_YIELD_MS);
1524
+ setTimeout(() => setImmediate(resolve5), PENDING_HTTP_YIELD_MS);
1496
1525
  });
1497
1526
  }
1498
1527
  function providerForConnector(id, definitions) {
@@ -1639,23 +1668,16 @@ var init_usage_application = __esm({
1639
1668
  let cursor = null;
1640
1669
  do {
1641
1670
  const page = this.#repository.getRetailPricingBackfillPage(cursor, 250);
1642
- for (const snapshot2 of page.snapshots) {
1643
- const costs = deriveRetailEquivalentCosts(
1644
- snapshot2,
1645
- this.#priceCatalog,
1646
- calculatedAt
1647
- ).costs;
1648
- if (costs.length > 0) this.#repository.saveDerivedCosts(snapshot2.provider.id, costs);
1649
- }
1671
+ this.#saveDerivedRetailCosts(page.snapshots, calculatedAt);
1650
1672
  cursor = page.nextCursor;
1651
1673
  if (cursor) await yieldToEventLoop();
1652
1674
  } while (cursor);
1653
1675
  } else {
1654
- for (const snapshot2 of this.#repository.getRetailPricingBackfillSnapshots?.() ?? []) {
1655
- const costs = deriveRetailEquivalentCosts(snapshot2, this.#priceCatalog, calculatedAt).costs;
1656
- if (costs.length > 0) this.#repository.saveDerivedCosts(snapshot2.provider.id, costs);
1657
- await yieldToEventLoop();
1658
- }
1676
+ this.#saveDerivedRetailCosts(
1677
+ this.#repository.getRetailPricingBackfillSnapshots?.() ?? [],
1678
+ calculatedAt
1679
+ );
1680
+ await yieldToEventLoop();
1659
1681
  }
1660
1682
  this.#repository.saveApplicationState?.(catalogVersionKey, this.#priceCatalog.version);
1661
1683
  }
@@ -2195,9 +2217,23 @@ var init_usage_application = __esm({
2195
2217
  );
2196
2218
  return result;
2197
2219
  }
2220
+ #saveDerivedRetailCosts(snapshots, calculatedAt) {
2221
+ if (!this.#priceCatalog) return;
2222
+ for (const snapshot2 of snapshots) {
2223
+ const costs = deriveRetailEquivalentCosts(snapshot2, this.#priceCatalog, calculatedAt).costs;
2224
+ if (costs.length > 0) this.#repository.saveDerivedCosts?.(snapshot2.provider.id, costs);
2225
+ }
2226
+ }
2198
2227
  #withRetailCosts(snapshot2) {
2199
- const retailCosts = this.#priceCatalog ? deriveRetailEquivalentCosts(snapshot2, this.#priceCatalog, this.#clock().toISOString()).costs : [];
2200
- return { ...snapshot2, costs: [...snapshot2.costs, ...retailCosts] };
2228
+ if (!this.#priceCatalog) return snapshot2;
2229
+ const usage = this.#repository.observationsNeedingRetailDerivation ? this.#repository.observationsNeedingRetailDerivation(snapshot2.provider.id, snapshot2.usage) : snapshot2.usage;
2230
+ if (usage.length === 0) return snapshot2;
2231
+ const retailCosts = deriveRetailEquivalentCosts(
2232
+ { ...snapshot2, usage },
2233
+ this.#priceCatalog,
2234
+ this.#clock().toISOString()
2235
+ ).costs;
2236
+ return retailCosts.length === 0 ? snapshot2 : { ...snapshot2, costs: [...snapshot2.costs, ...retailCosts] };
2201
2237
  }
2202
2238
  async discoverConnectors() {
2203
2239
  if (!this.#discoveryProbe) return this.getConnectorStatuses();
@@ -2300,7 +2336,7 @@ var init_usage_application = __esm({
2300
2336
  return status;
2301
2337
  }
2302
2338
  };
2303
- PENDING_HTTP_YIELD_MS = 20;
2339
+ PENDING_HTTP_YIELD_MS = 75;
2304
2340
  }
2305
2341
  });
2306
2342
 
@@ -6006,7 +6042,6 @@ var init_antigravity_sqlite_usage_client = __esm({
6006
6042
  "src/server/antigravity-sqlite-usage-client.ts"() {
6007
6043
  "use strict";
6008
6044
  init_token_normalization();
6009
- init_retail_pricing();
6010
6045
  ({ DatabaseSync: DatabaseSync2 } = createRequire2(import.meta.url)(
6011
6046
  "node:sqlite"
6012
6047
  ));
@@ -6015,7 +6050,6 @@ var init_antigravity_sqlite_usage_client = __esm({
6015
6050
  #clock;
6016
6051
  #lookbackDays;
6017
6052
  #cachePath;
6018
- #priceCatalog;
6019
6053
  #fileCache = /* @__PURE__ */ new Map();
6020
6054
  #cacheLoaded = false;
6021
6055
  constructor(options) {
@@ -6023,7 +6057,6 @@ var init_antigravity_sqlite_usage_client = __esm({
6023
6057
  this.#clock = options.clock ?? (() => /* @__PURE__ */ new Date());
6024
6058
  this.#lookbackDays = options.lookbackDays ?? 90;
6025
6059
  this.#cachePath = options.cachePath;
6026
- this.#priceCatalog = options.priceCatalog ?? OFFICIAL_PRICING_CATALOG;
6027
6060
  }
6028
6061
  async readUsage(options = { mode: "incremental" }) {
6029
6062
  await this.#loadCache();
@@ -6048,21 +6081,10 @@ var init_antigravity_sqlite_usage_client = __esm({
6048
6081
  }
6049
6082
  }
6050
6083
  usage.sort((a, b) => a.observedAt.localeCompare(b.observedAt) || a.id.localeCompare(b.id));
6051
- const { costs } = deriveRetailEquivalentCosts(
6052
- {
6053
- provider: { id: "antigravity", displayName: "Antigravity" },
6054
- billingDomains: [{ id: "code-assist-subscription", displayName: "Gemini Code Assist" }],
6055
- quotaBuckets: [],
6056
- usage,
6057
- costs: [],
6058
- observedAt: this.#clock().toISOString()
6059
- },
6060
- this.#priceCatalog
6061
- );
6062
6084
  await this.#persistCache();
6063
6085
  return {
6064
6086
  usage,
6065
- costs,
6087
+ costs: [],
6066
6088
  complete: allComplete
6067
6089
  };
6068
6090
  }
@@ -8120,6 +8142,23 @@ function pricingBackfillSnapshots(rows) {
8120
8142
  }
8121
8143
  return [...snapshots.values()];
8122
8144
  }
8145
+ function unpricedRetailPredicate(alias) {
8146
+ return `NOT EXISTS (
8147
+ SELECT 1 FROM cost_records priced
8148
+ WHERE priced.provider_id = ${alias}.provider_id
8149
+ AND priced.usage_observation_id = ${alias}.id
8150
+ AND priced.kind = 'retail-equivalent'
8151
+ )`;
8152
+ }
8153
+ function chunked(items, size) {
8154
+ if (items.length === 0) return [];
8155
+ if (items.length <= size) return [items];
8156
+ const chunks = [];
8157
+ for (let index = 0; index < items.length; index += size) {
8158
+ chunks.push(items.slice(index, index + size));
8159
+ }
8160
+ return chunks;
8161
+ }
8123
8162
  function normalizeCostForPersistence(cost) {
8124
8163
  if (cost.kind !== "subscription") return cost;
8125
8164
  return {
@@ -8473,50 +8512,17 @@ function buildTokenMoneyWorkbench(providers, now, query) {
8473
8512
  };
8474
8513
  });
8475
8514
  const buckets = emptyIntervals.map((emptyInterval, index) => {
8476
- const segments = allHistories.flatMap(({ provider, domain, history, includedInHeadline }) => {
8477
- const interval = history.intervals[index];
8478
- if (!interval) return [];
8479
- const retailEquivalent2 = buildWorkbenchMetric(
8480
- interval.costs,
8481
- "retail-equivalent",
8482
- comparisonCurrency,
8483
- interval.tokenEvidence.recordedTokens,
8484
- history.exchangeRates
8485
- );
8486
- const reportedEstimate2 = buildWorkbenchMetric(
8487
- interval.costs,
8488
- "reported-estimate",
8489
- comparisonCurrency,
8490
- interval.tokenEvidence.recordedTokens,
8491
- history.exchangeRates
8492
- );
8493
- if (interval.tokenEvidence.observationCount === 0 && retailEquivalent2.records === 0 && reportedEstimate2.records === 0)
8494
- return [];
8495
- return [
8496
- {
8497
- providerId: provider.id,
8498
- providerDisplayName: provider.displayName,
8499
- billingDomainId: domain.id,
8500
- billingDomainDisplayName: domain.displayName,
8501
- includedInHeadline,
8502
- recordedTokens: interval.tokenEvidence.recordedTokens,
8503
- observationCount: interval.tokenEvidence.observationCount,
8504
- timePrecisions: interval.tokenEvidence.timePrecisions,
8505
- authorities: interval.authorities ?? [],
8506
- lastObservedAt: interval.lastObservedAt ?? null,
8507
- retailEquivalent: {
8508
- status: retailEquivalent2.status,
8509
- amount: retailEquivalent2.amount,
8510
- currency: comparisonCurrency
8511
- },
8512
- reportedEstimate: {
8513
- status: reportedEstimate2.status,
8514
- amount: reportedEstimate2.amount,
8515
- currency: comparisonCurrency
8516
- }
8517
- }
8518
- ];
8519
- });
8515
+ const segments = allHistories.flatMap(
8516
+ ({ provider, domain, history, includedInHeadline }) => workbenchTrendSegmentsForInterval(
8517
+ provider,
8518
+ domain,
8519
+ history,
8520
+ includedInHeadline,
8521
+ emptyInterval,
8522
+ index,
8523
+ comparisonCurrency
8524
+ )
8525
+ );
8520
8526
  return {
8521
8527
  start: emptyInterval.start.toISOString(),
8522
8528
  end: emptyInterval.end.toISOString(),
@@ -8554,6 +8560,133 @@ function buildTokenMoneyWorkbench(providers, now, query) {
8554
8560
  )
8555
8561
  };
8556
8562
  }
8563
+ function workbenchTrendSegmentsForInterval(provider, domain, history, includedInHeadline, emptyInterval, index, comparisonCurrency) {
8564
+ const interval = history.intervals[index];
8565
+ if (!interval) return [];
8566
+ const start = emptyInterval.start.toISOString();
8567
+ const end = emptyInterval.end.toISOString();
8568
+ const models = history.models.flatMap((model) => {
8569
+ const observations = (model.observations ?? []).filter(
8570
+ (observation) => observation.observedAt >= start && observation.observedAt < end
8571
+ );
8572
+ const priceEvidence = (model.priceEvidence ?? []).filter(
8573
+ (cost) => (cost.observedAt ?? "") >= start && (cost.observedAt ?? "") < end
8574
+ );
8575
+ const tokenEvidence = emptyTokenEvidence();
8576
+ const authorities = /* @__PURE__ */ new Set();
8577
+ let lastObservedAt = null;
8578
+ for (const observation of observations) {
8579
+ tokenEvidence.recordedTokens += observation.recordedTokens;
8580
+ tokenEvidence.observationCount += 1;
8581
+ tokenEvidence.timePrecisions.add(observation.timePrecision);
8582
+ authorities.add(observation.authority);
8583
+ if (!lastObservedAt || observation.observedAt > lastObservedAt) {
8584
+ lastObservedAt = observation.observedAt;
8585
+ }
8586
+ }
8587
+ const retailEquivalent = buildWorkbenchMetric(
8588
+ priceEvidence,
8589
+ "retail-equivalent",
8590
+ comparisonCurrency,
8591
+ tokenEvidence.recordedTokens,
8592
+ history.exchangeRates
8593
+ );
8594
+ const reportedEstimate = buildWorkbenchMetric(
8595
+ priceEvidence,
8596
+ "reported-estimate",
8597
+ comparisonCurrency,
8598
+ tokenEvidence.recordedTokens,
8599
+ history.exchangeRates
8600
+ );
8601
+ if (tokenEvidence.observationCount === 0 && retailEquivalent.records === 0 && reportedEstimate.records === 0) {
8602
+ return [];
8603
+ }
8604
+ return [
8605
+ {
8606
+ providerId: provider.id,
8607
+ providerDisplayName: provider.displayName,
8608
+ billingDomainId: domain.id,
8609
+ billingDomainDisplayName: domain.displayName,
8610
+ model: model.model,
8611
+ includedInHeadline,
8612
+ recordedTokens: tokenEvidence.recordedTokens,
8613
+ observationCount: tokenEvidence.observationCount,
8614
+ timePrecisions: [...tokenEvidence.timePrecisions].sort(),
8615
+ authorities: [...authorities].sort(),
8616
+ lastObservedAt,
8617
+ retailEquivalent: {
8618
+ status: retailEquivalent.status,
8619
+ amount: retailEquivalent.amount,
8620
+ currency: comparisonCurrency
8621
+ },
8622
+ reportedEstimate: {
8623
+ status: reportedEstimate.status,
8624
+ amount: reportedEstimate.amount,
8625
+ currency: comparisonCurrency
8626
+ }
8627
+ }
8628
+ ];
8629
+ });
8630
+ const unclassifiedObservations = (history.unclassified.observations ?? []).filter(
8631
+ (observation) => observation.observedAt >= start && observation.observedAt < end
8632
+ );
8633
+ const unclassifiedTokenEvidence = emptyTokenEvidence();
8634
+ const unclassifiedAuthorities = /* @__PURE__ */ new Set();
8635
+ let unclassifiedLastObservedAt = null;
8636
+ for (const observation of unclassifiedObservations) {
8637
+ unclassifiedTokenEvidence.recordedTokens += observation.recordedTokens;
8638
+ unclassifiedTokenEvidence.observationCount += 1;
8639
+ unclassifiedTokenEvidence.timePrecisions.add(observation.timePrecision);
8640
+ unclassifiedAuthorities.add(observation.authority);
8641
+ if (!unclassifiedLastObservedAt || observation.observedAt > unclassifiedLastObservedAt) {
8642
+ unclassifiedLastObservedAt = observation.observedAt;
8643
+ }
8644
+ }
8645
+ const leftoverCosts = models.length === 0 ? interval.costs : [];
8646
+ const leftoverRetailEquivalent = buildWorkbenchMetric(
8647
+ leftoverCosts,
8648
+ "retail-equivalent",
8649
+ comparisonCurrency,
8650
+ unclassifiedTokenEvidence.recordedTokens,
8651
+ history.exchangeRates
8652
+ );
8653
+ const leftoverReportedEstimate = buildWorkbenchMetric(
8654
+ leftoverCosts,
8655
+ "reported-estimate",
8656
+ comparisonCurrency,
8657
+ unclassifiedTokenEvidence.recordedTokens,
8658
+ history.exchangeRates
8659
+ );
8660
+ if (unclassifiedTokenEvidence.observationCount === 0 && leftoverRetailEquivalent.records === 0 && leftoverReportedEstimate.records === 0) {
8661
+ return models;
8662
+ }
8663
+ return [
8664
+ ...models,
8665
+ {
8666
+ providerId: provider.id,
8667
+ providerDisplayName: provider.displayName,
8668
+ billingDomainId: domain.id,
8669
+ billingDomainDisplayName: domain.displayName,
8670
+ model: null,
8671
+ includedInHeadline,
8672
+ recordedTokens: unclassifiedTokenEvidence.recordedTokens,
8673
+ observationCount: unclassifiedTokenEvidence.observationCount,
8674
+ timePrecisions: [...unclassifiedTokenEvidence.timePrecisions].sort(),
8675
+ authorities: [...unclassifiedAuthorities].sort(),
8676
+ lastObservedAt: unclassifiedLastObservedAt,
8677
+ retailEquivalent: {
8678
+ status: leftoverRetailEquivalent.status,
8679
+ amount: leftoverRetailEquivalent.amount,
8680
+ currency: comparisonCurrency
8681
+ },
8682
+ reportedEstimate: {
8683
+ status: leftoverReportedEstimate.status,
8684
+ amount: leftoverReportedEstimate.amount,
8685
+ currency: comparisonCurrency
8686
+ }
8687
+ }
8688
+ ];
8689
+ }
8557
8690
  function allDomainHistories(providers) {
8558
8691
  return providers.flatMap(
8559
8692
  (provider) => provider.billingDomains.map((domain) => ({
@@ -9342,13 +9475,18 @@ function freshnessStatus(lastSuccessAt, now) {
9342
9475
  if (!lastSuccessAt) return "unavailable";
9343
9476
  return now.getTime() - new Date(lastSuccessAt).getTime() <= FRESHNESS_WINDOW_MS ? "fresh" : "stale";
9344
9477
  }
9345
- var FRESHNESS_WINDOW_MS, QUERY_INDEXES_SQL, DELETE_DERIVED_RETAIL_COSTS_SQL, DELETE_DEMO_PROVIDER_DATA_SQL, DatabaseSync3, RETENTION_AGGREGATE_SQL, SqliteUsageRepository;
9478
+ var FRESHNESS_WINDOW_MS, SQLITE_VARIABLE_CHUNK, USAGE_PRICING_INPUT_COLUMNS, QUERY_INDEXES_SQL, DELETE_DERIVED_RETAIL_COSTS_SQL, DELETE_DEMO_PROVIDER_DATA_SQL, DatabaseSync3, RETENTION_AGGREGATE_SQL, SqliteUsageRepository;
9346
9479
  var init_sqlite_usage_repository = __esm({
9347
9480
  "src/server/sqlite-usage-repository.ts"() {
9348
9481
  "use strict";
9349
9482
  init_token_normalization();
9350
9483
  init_quota_normalization();
9351
9484
  FRESHNESS_WINDOW_MS = 15 * 60 * 1e3;
9485
+ SQLITE_VARIABLE_CHUNK = 400;
9486
+ USAGE_PRICING_INPUT_COLUMNS = `id, billing_domain_id, model, observed_at, total_tokens, input_tokens,
9487
+ output_tokens, reasoning_tokens, cache_read_tokens, cache_write_tokens, cache_write_5m_tokens,
9488
+ cache_write_1h_tokens, unclassified_tokens, reasoning_semantics, cache_read_semantics,
9489
+ cache_write_semantics, model_attribution, time_precision, aggregation_temporality`;
9352
9490
  QUERY_INDEXES_SQL = `
9353
9491
  CREATE INDEX IF NOT EXISTS usage_observed_at_idx
9354
9492
  ON usage_observations(observed_at);
@@ -9627,14 +9765,9 @@ var init_sqlite_usage_repository = __esm({
9627
9765
  usage_scope = excluded.usage_scope,
9628
9766
  aggregation_temporality = excluded.aggregation_temporality`
9629
9767
  );
9630
- const existingPricedUsageStatement = this.#database.prepare(
9631
- `SELECT billing_domain_id, model, observed_at, total_tokens, input_tokens, output_tokens,
9632
- reasoning_tokens, cache_read_tokens, cache_write_tokens, cache_write_5m_tokens,
9633
- cache_write_1h_tokens, unclassified_tokens,
9634
- reasoning_semantics, cache_read_semantics, cache_write_semantics, model_attribution,
9635
- time_precision, aggregation_temporality
9636
- FROM usage_observations
9637
- WHERE provider_id = ? AND id = ?`
9768
+ const existingById = this.#usagePricingInputs(
9769
+ snapshot2.provider.id,
9770
+ snapshot2.usage.map((observation) => observation.id)
9638
9771
  );
9639
9772
  const deleteDerivedCostsStatement = this.#database.prepare(
9640
9773
  `DELETE FROM cost_records
@@ -9642,7 +9775,7 @@ var init_sqlite_usage_repository = __esm({
9642
9775
  );
9643
9776
  for (const observation of snapshot2.usage) {
9644
9777
  const normalized = normalizeTokenObservation(observation);
9645
- const existing = existingPricedUsageStatement.get(snapshot2.provider.id, normalized.id);
9778
+ const existing = existingById.get(normalized.id);
9646
9779
  if (existing && pricingInputsChanged(existing, normalized)) {
9647
9780
  deleteDerivedCostsStatement.run(snapshot2.provider.id, normalized.id);
9648
9781
  }
@@ -9812,6 +9945,7 @@ var init_sqlite_usage_repository = __esm({
9812
9945
  JOIN billing_domains b
9813
9946
  ON b.provider_id = u.provider_id AND b.id = u.billing_domain_id
9814
9947
  WHERE ${additiveUsagePredicate("u")}
9948
+ AND ${unpricedRetailPredicate("u")}
9815
9949
  ORDER BY u.provider_id, u.observed_at, u.id`
9816
9950
  ).all();
9817
9951
  return pricingBackfillSnapshots(rows);
@@ -9834,6 +9968,7 @@ var init_sqlite_usage_repository = __esm({
9834
9968
  JOIN billing_domains b
9835
9969
  ON b.provider_id = u.provider_id AND b.id = u.billing_domain_id
9836
9970
  WHERE ${additiveUsagePredicate("u")}
9971
+ AND ${unpricedRetailPredicate("u")}
9837
9972
  ${cursorPredicate}
9838
9973
  ORDER BY u.provider_id, u.observed_at, u.id
9839
9974
  LIMIT ?`
@@ -9851,6 +9986,30 @@ var init_sqlite_usage_repository = __esm({
9851
9986
  } : null
9852
9987
  };
9853
9988
  }
9989
+ observationsNeedingRetailDerivation(providerId, observations) {
9990
+ if (observations.length === 0) return [];
9991
+ const existing = this.#usagePricingInputs(
9992
+ providerId,
9993
+ observations.map((observation) => observation.id)
9994
+ );
9995
+ return observations.filter((observation) => {
9996
+ const normalized = normalizeTokenObservation(observation);
9997
+ const prior = existing.get(normalized.id);
9998
+ return !prior || pricingInputsChanged(prior, normalized);
9999
+ });
10000
+ }
10001
+ #usagePricingInputs(providerId, observationIds) {
10002
+ const byId = /* @__PURE__ */ new Map();
10003
+ for (const ids of chunked(observationIds, SQLITE_VARIABLE_CHUNK)) {
10004
+ const rows = this.#database.prepare(
10005
+ `SELECT ${USAGE_PRICING_INPUT_COLUMNS}
10006
+ FROM usage_observations
10007
+ WHERE provider_id = ? AND id IN (${ids.map(() => "?").join(", ")})`
10008
+ ).all(providerId, ...ids);
10009
+ for (const row of rows) byId.set(String(row.id), row);
10010
+ }
10011
+ return byId;
10012
+ }
9854
10013
  saveDerivedCosts(providerId, costs) {
9855
10014
  if (costs.length === 0) return;
9856
10015
  if (costs.some((cost) => cost.kind !== "retail-equivalent")) {
@@ -10333,6 +10492,7 @@ var init_sqlite_usage_repository = __esm({
10333
10492
  }
10334
10493
  compactUsageHistory(now) {
10335
10494
  const cutoff = new Date(now.getTime() - 90 * 24 * 60 * 60 * 1e3).toISOString();
10495
+ if (!this.#hasObservationsOlderThan(cutoff)) return this.getRetentionStatus();
10336
10496
  this.#database.exec("BEGIN IMMEDIATE");
10337
10497
  try {
10338
10498
  this.#database.prepare(RETENTION_AGGREGATE_SQL).run(cutoff);
@@ -10405,6 +10565,7 @@ var init_sqlite_usage_repository = __esm({
10405
10565
  ).run(key, value);
10406
10566
  }
10407
10567
  async ensureQueryIndexes() {
10568
+ if (this.#queryIndexesReady()) return;
10408
10569
  await runDatabaseWorker({
10409
10570
  databasePath: this.#databasePath,
10410
10571
  operation: "indexes",
@@ -10413,6 +10574,7 @@ var init_sqlite_usage_repository = __esm({
10413
10574
  }
10414
10575
  async maintainUsageHistory(now) {
10415
10576
  const cutoff = new Date(now.getTime() - 90 * 24 * 60 * 60 * 1e3).toISOString();
10577
+ if (!this.#hasObservationsOlderThan(cutoff)) return this.getRetentionStatus();
10416
10578
  return runDatabaseWorker({
10417
10579
  databasePath: this.#databasePath,
10418
10580
  operation: "retention",
@@ -10422,6 +10584,14 @@ var init_sqlite_usage_repository = __esm({
10422
10584
  now: now.toISOString()
10423
10585
  });
10424
10586
  }
10587
+ #queryIndexesReady() {
10588
+ const indexes = this.#database.prepare("PRAGMA index_list('usage_observations')").all();
10589
+ return indexes.some((index) => index.name === "usage_provider_model_time_idx");
10590
+ }
10591
+ #hasObservationsOlderThan(cutoff) {
10592
+ const row = this.#database.prepare("SELECT 1 AS present FROM usage_observations WHERE observed_at < ? LIMIT 1").get(cutoff);
10593
+ return row !== void 0;
10594
+ }
10425
10595
  close() {
10426
10596
  this.#database.close();
10427
10597
  }
@@ -10852,7 +11022,7 @@ var init_sqlite_usage_repository = __esm({
10852
11022
  models: [...byModel.entries()].map(([model, value]) => {
10853
11023
  const observationIds = new Set(value.observations.map((observation) => observation.id));
10854
11024
  const priceEvidence = costs.filter(
10855
- (cost) => (cost.kind === "retail-equivalent" || cost.kind === "reported-estimate") && cost.usage_observation_id !== null && observationIds.has(cost.usage_observation_id)
11025
+ (cost) => (cost.kind === "retail-equivalent" || cost.kind === "reported-estimate") && (cost.usage_observation_id !== null && observationIds.has(cost.usage_observation_id) || cost.usage_observation_id === null && cost.model === model)
10856
11026
  ).map((cost) => {
10857
11027
  const summarized = summarizeCosts([cost], Number(cost.priced_tokens ?? 0))[0];
10858
11028
  return {