@danypops/jittor 0.16.3 → 0.18.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 (97) hide show
  1. package/README.md +38 -13
  2. package/docs/COMPACTION_EFFECTIVENESS.md +34 -0
  3. package/docs/CONTEXT_SNAPSHOTS.md +53 -0
  4. package/docs/MODEL_CATALOG.md +43 -0
  5. package/docs/OTLP_EXPORT.md +63 -0
  6. package/docs/PROVIDER_RESEARCH.md +1 -1
  7. package/docs/TOKEN_MEASUREMENT.md +53 -0
  8. package/docs/USAGE_BACKFILL.md +35 -0
  9. package/package.json +5 -4
  10. package/src/{providers/anthropic-contracts.ts → anthropic/rate-limits.ts} +1 -1
  11. package/src/{adapters/artificial-analysis-direct-source.ts → artificial-analysis/benchmark-source.ts} +3 -3
  12. package/src/cli-commands/backfill.ts +51 -0
  13. package/src/cli-commands/benchmarks.ts +4 -4
  14. package/src/cli-commands/cache.ts +120 -0
  15. package/src/cli-commands/catalog.ts +123 -0
  16. package/src/cli-commands/compaction.ts +1 -1
  17. package/src/cli-commands/context.ts +77 -4
  18. package/src/cli-commands/export.ts +30 -0
  19. package/src/cli-commands/metrics.ts +8 -2
  20. package/src/cli-commands/op.ts +1 -1
  21. package/src/cli-commands/route-args.ts +2 -2
  22. package/src/cli-commands/router.ts +2 -2
  23. package/src/cli-commands/support.ts +2 -2
  24. package/src/cli.ts +59 -21
  25. package/src/{providers/codex-contracts.ts → codex/contracts.ts} +1 -1
  26. package/src/codex/source.ts +53 -0
  27. package/src/{providers/codex.ts → codex/telemetry.ts} +2 -2
  28. package/src/constants.ts +31 -0
  29. package/src/daemon.ts +53 -23
  30. package/src/{providers/google-vertex-budget-contracts.ts → google-vertex/budget-contracts.ts} +3 -3
  31. package/src/{providers/google-vertex-budget.ts → google-vertex/budget-telemetry.ts} +6 -6
  32. package/src/{providers/google-vertex-contracts.ts → google-vertex/failures.ts} +1 -1
  33. package/src/google-vertex/source.ts +35 -0
  34. package/src/index.ts +149 -55
  35. package/src/{adapters/lmarena-hf-source.ts → lmarena/benchmark-source.ts} +3 -3
  36. package/src/observability/budget.ts +14 -0
  37. package/src/observability/cache-economics.ts +623 -0
  38. package/src/observability/context-delta.ts +309 -0
  39. package/src/{domain → observability}/context-hub.ts +19 -2
  40. package/src/observability/context-snapshot-history.ts +281 -0
  41. package/src/{domain → observability}/context-telemetry.ts +232 -4
  42. package/src/observability/model-identity.ts +42 -0
  43. package/src/{domain → observability}/model-observation.ts +26 -11
  44. package/src/{ports/metric-store.ts → observability/store.ts} +2 -2
  45. package/src/{ports → observability}/telemetry-source.ts +2 -2
  46. package/src/observability/token-measurement.ts +194 -0
  47. package/src/observability/usage-import.ts +122 -0
  48. package/src/openai/token-counter.ts +63 -0
  49. package/src/{adapters/openrouter-benchmark-source.ts → openrouter/benchmark-source.ts} +3 -3
  50. package/src/{providers/openrouter-contracts.ts → openrouter/contracts.ts} +1 -1
  51. package/src/{adapters/openrouter-design-arena-source.ts → openrouter/design-arena-source.ts} +4 -4
  52. package/src/openrouter/source.ts +20 -0
  53. package/src/{providers/openrouter.ts → openrouter/telemetry.ts} +2 -2
  54. package/src/{domain → optimization/model-selection}/benchmark.ts +9 -27
  55. package/src/optimization/model-selection/catalog-store.ts +97 -0
  56. package/src/optimization/model-selection/catalog.ts +478 -0
  57. package/src/{ports/benchmark-controller.ts → optimization/model-selection/controller.ts} +1 -1
  58. package/src/{adapters/metric-benchmark-store.ts → optimization/model-selection/observation-store.ts} +5 -5
  59. package/src/{domain/model-ranking-service.ts → optimization/model-selection/ranker.ts} +6 -12
  60. package/src/{domain/model-ranking.ts → optimization/model-selection/ranking.ts} +8 -2
  61. package/src/{ports/benchmark-source.ts → optimization/model-selection/source.ts} +1 -1
  62. package/src/{ports/benchmark-store.ts → optimization/model-selection/store.ts} +1 -1
  63. package/src/{domain/codex-recovery.ts → optimization/recovery/codex.ts} +1 -1
  64. package/src/{ports/router-controller.ts → optimization/routing/controller.ts} +1 -1
  65. package/src/{policy.ts → optimization/routing/policy.ts} +4 -14
  66. package/src/{router.ts → optimization/routing/router.ts} +4 -10
  67. package/src/otlp/exporter.ts +354 -0
  68. package/src/pi/session-usage-source.ts +203 -0
  69. package/src/{session-identity-service.ts → sessions/identity.ts} +2 -2
  70. package/src/{operations/session-scope.ts → sessions/router-authorization.ts} +1 -1
  71. package/src/{db.ts → sqlite/database.ts} +14 -1
  72. package/src/{adapters/sqlite-metric-store.ts → sqlite/metric-store.ts} +4 -4
  73. package/src/{adapters/sqlite-session-identity-store.ts → sqlite/session-store.ts} +1 -1
  74. package/src/sqlite/usage-import-store.ts +133 -0
  75. package/src/telemetry-export/exporter.ts +84 -0
  76. package/src/{operations → vehicle}/benchmark-operations.ts +3 -3
  77. package/src/vehicle/cache-operations.ts +95 -0
  78. package/src/vehicle/catalog-operations.ts +10 -0
  79. package/src/{client.ts → vehicle/client.ts} +1 -1
  80. package/src/{operations → vehicle}/context-operations.ts +13 -5
  81. package/src/vehicle/export-operations.ts +12 -0
  82. package/src/{operations/metrics-operations.ts → vehicle/metric-operations.ts} +4 -4
  83. package/src/{operations → vehicle}/model-ranking-operations.ts +3 -3
  84. package/src/{vehicle-registration.ts → vehicle/registration.ts} +21 -4
  85. package/src/{operations/router-operations.ts → vehicle/routing-operations.ts} +4 -4
  86. package/src/{service.ts → vehicle/service.ts} +114 -26
  87. package/src/{operations/session-identity-operations.ts → vehicle/session-operations.ts} +3 -3
  88. package/src/vehicle/usage-import-operations.ts +10 -0
  89. package/src/providers/telemetry-sources.ts +0 -108
  90. /package/src/{providers/google-adc-auth.ts → google-vertex/auth.ts} +0 -0
  91. /package/src/{domain → observability}/metric.ts +0 -0
  92. /package/src/{domain → observability}/task-cost.ts +0 -0
  93. /package/src/{domain → observability}/task-focus.ts +0 -0
  94. /package/src/{domain → observability}/usage.ts +0 -0
  95. /package/src/{config.ts → optimization/routing/config.ts} +0 -0
  96. /package/src/{ports/session-identity-store.ts → sessions/store.ts} +0 -0
  97. /package/src/{operations/types.ts → vehicle/operation-types.ts} +0 -0
@@ -0,0 +1,623 @@
1
+ import {
2
+ CACHE_ECONOMICS_LOSS_CORRELATION_WINDOW_MS,
3
+ CACHE_ECONOMICS_MAX_MISSED_OPPORTUNITIES,
4
+ CACHE_ECONOMICS_MAX_MODEL_GROUPS,
5
+ CACHE_ECONOMICS_MAX_STABLE_PREFIX_POINTS,
6
+ CACHE_ECONOMICS_MAX_TASK_GROUPS,
7
+ CATALOG_PRICE_TOKEN_UNIT,
8
+ } from "../constants.ts";
9
+ import type { ContextPrefixResetReason } from "./context-delta.ts";
10
+ import type { StoredMetricObservation } from "./metric.ts";
11
+
12
+ /**
13
+ * How much authority a cache-cost figure carries. `provider-reported` comes straight from the
14
+ * provider's own per-turn `usage.cost.*` breakdown (or is trivially true, e.g. zero tokens cost
15
+ * zero dollars); `catalog-estimate` is derived from models.dev catalog pricing when the provider
16
+ * never itemizes cache cost; `unknown` means neither exists and the figure must stay null rather
17
+ * than being fabricated.
18
+ */
19
+ export type CacheCostBasis = "provider-reported" | "catalog-estimate" | "unknown";
20
+
21
+ /** Flat (already tier-resolved, if applicable) per-token-million catalog prices for one provider/model at one particular request size. */
22
+ export interface CacheEconomicsPricing {
23
+ input?: number;
24
+ cacheRead?: number;
25
+ cacheWrite?: number;
26
+ /** Whether the catalog snapshot this price was resolved from was still fresh at query time. Only meaningful when this pricing is actually used for a catalog-estimate figure; the lookup may omit it (e.g. a fake with no notion of freshness). */
27
+ freshness?: "fresh" | "stale";
28
+ }
29
+
30
+ /**
31
+ * Best-effort catalog pricing lookup; returns null when the catalog has no snapshot or no
32
+ * matching model, never throws. `contextSizeTokens` is the specific request/run's own real size
33
+ * (input + cache-read + cache-write tokens) -- the lookup is free to resolve a tiered or
34
+ * long-context ("contextOver200k") price against it instead of a single flat rate; the domain
35
+ * layer here never assumes which it did.
36
+ */
37
+ export interface CacheEconomicsPricingLookup {
38
+ priceFor(provider: string, model: string, contextSizeTokens: number): CacheEconomicsPricing | null;
39
+ }
40
+
41
+ /**
42
+ * Every derived economics figure this domain produces, independent of which dimension (model,
43
+ * task) it's grouped by -- see CacheEconomicsModelSummary/CacheEconomicsTaskSummary, which each add
44
+ * only their own grouping key on top of this.
45
+ */
46
+ export interface CacheEconomicsAggregateTotals {
47
+ inputTokens: number;
48
+ cacheReadTokens: number;
49
+ cacheWriteTokens: number;
50
+ /** Real dollars actually paid for cache reads in-window, or a catalog estimate, or null when neither is known. */
51
+ cacheReadCostUsd: number | null;
52
+ cacheReadCostBasis: CacheCostBasis;
53
+ /** Real dollars actually paid for cache writes in-window, or a catalog estimate, or null when neither is known. */
54
+ cacheWriteCostUsd: number | null;
55
+ cacheWriteCostBasis: CacheCostBasis;
56
+ /** sum(provider-reported input cost) / sum(input tokens) within the window -- a real per-token rate derived only from provider-reported dollar/token figures, not a catalog guess. Null when the provider never reported itemized input cost in-window. */
57
+ effectiveInputRateUsdPerToken: number | null;
58
+ /** What the cache-read tokens would have cost had they been billed as ordinary input, at the best available rate. */
59
+ counterfactualNoCacheCostUsd: number | null;
60
+ counterfactualBasis: CacheCostBasis;
61
+ /** counterfactualNoCacheCostUsd minus the actual cache-read cost -- the real economic benefit of caching, when derivable. */
62
+ savingsUsd: number | null;
63
+ /** The premium actually paid for cache-write tokens over what plain input billing would have cost. */
64
+ cacheWritePremiumUsd: number | null;
65
+ /** How many cache-read tokens, at the observed/estimated read rate, would be needed to offset the write premium. Zero when no premium was paid; null when undeterminable. An aggregate approximation over the whole window/group, not resolved per run. */
66
+ breakEvenReadTokens: number | null;
67
+ /** Whether observed savings already met or exceeded the write premium. Null when there was no cache write to evaluate, or the comparison is undeterminable. */
68
+ paybackAchieved: boolean | null;
69
+ /** Worst ("stale" wins) freshness among every catalog-estimate figure this group actually used. Null when no catalog estimate was used at all -- everything was provider-reported, or nothing was derivable. Never set from a catalog price that was resolved but ended up unused (e.g. provider-reported cost took precedence). */
70
+ catalogFreshness: "fresh" | "stale" | null;
71
+ }
72
+
73
+ export interface CacheEconomicsModelSummary extends CacheEconomicsAggregateTotals {
74
+ provider: string;
75
+ model: string;
76
+ }
77
+
78
+ /**
79
+ * The same figures as CacheEconomicsModelSummary, rolled up by the Papyrus task focused when each
80
+ * row was recorded instead of by provider/model -- mirrors task-cost.ts's own attributes.taskId
81
+ * grouping for token/cost metrics. A task that stayed on one model gets the same tiered-catalog
82
+ * pricing precision as the model-level rollup; a task that spanned several models still sums
83
+ * correctly (each run was already priced against its own real context size before this rollup ever
84
+ * runs) but skips the single extra break-even catalog lookup that only makes sense for one model.
85
+ */
86
+ export interface CacheEconomicsTaskSummary extends CacheEconomicsAggregateTotals {
87
+ taskId: string;
88
+ }
89
+
90
+ /**
91
+ * Cache activity recorded with no Papyrus task focused. Real spend/activity, just not attributable
92
+ * to any task -- reported separately (mirroring task-cost.ts's own unattributedCostUsd) rather than
93
+ * silently dropped or folded into a fabricated "unknown task" bucket.
94
+ */
95
+ export interface CacheEconomicsUnattributedActivity {
96
+ cacheReadTokens: number;
97
+ cacheWriteTokens: number;
98
+ cacheReadCostUsd: number | null;
99
+ cacheReadCostBasis: CacheCostBasis;
100
+ cacheWriteCostUsd: number | null;
101
+ cacheWriteCostBasis: CacheCostBasis;
102
+ catalogFreshness: "fresh" | "stale" | null;
103
+ }
104
+
105
+ /**
106
+ * One snapshot's own stable-prefix-token measurement at one point in time -- the same raw evidence
107
+ * findMissedOpportunities correlates internally, made directly visible instead of only ever feeding
108
+ * a derived diagnostic. A sharp drop, especially alongside a non-null resetReason, is what a missed
109
+ * cache write actually looks like in this series; still just evidence, not a causal claim.
110
+ */
111
+ export interface CacheEconomicsStablePrefixPoint {
112
+ sessionId: string;
113
+ observedAt: number;
114
+ stablePrefixTokens: number;
115
+ resetReason: ContextPrefixResetReason;
116
+ }
117
+
118
+ /** A context-prefix reset (session/provider/model change) followed shortly by a same-session cache-write is *evidence*, not proof, that the reset forced a cache rewrite -- correlation, never asserted causality. */
119
+ export interface CacheEconomicsMissedOpportunity {
120
+ sessionId: string;
121
+ occurredAt: number;
122
+ resetReason: Exclude<ContextPrefixResetReason, null>;
123
+ cacheWriteTokens: number;
124
+ cacheWriteCostUsd: number | null;
125
+ note: string;
126
+ }
127
+
128
+ export interface CacheEconomicsSummary {
129
+ since: number;
130
+ until: number;
131
+ models: CacheEconomicsModelSummary[];
132
+ tasks: CacheEconomicsTaskSummary[];
133
+ unattributedCacheActivity: CacheEconomicsUnattributedActivity;
134
+ missedOpportunities: CacheEconomicsMissedOpportunity[];
135
+ /** Chronological (oldest first), bounded to the most recent CACHE_ECONOMICS_MAX_STABLE_PREFIX_POINTS. */
136
+ stablePrefixChurn: CacheEconomicsStablePrefixPoint[];
137
+ /** True when the model-group list, the task-group list, the stable-prefix-churn series, or the missed-opportunity list was cut off at its bound. */
138
+ truncated: boolean;
139
+ }
140
+
141
+ export interface CacheEconomicsSummaryOptions {
142
+ since: number;
143
+ until: number;
144
+ }
145
+
146
+ const RESET_REASONS = new Set<Exclude<ContextPrefixResetReason, null>>(["initial", "session-changed", "provider-changed", "model-changed"]);
147
+
148
+ function attributeText(attributes: Record<string, unknown>, key: string): string {
149
+ return typeof attributes[key] === "string" && attributes[key].length > 0 ? (attributes[key] as string) : "unknown";
150
+ }
151
+
152
+ /** A turn's own runId when present, else its shared observedAt timestamp -- rows recorded before runId tagging existed still group correctly as long as they were sent (and therefore timestamped) together. */
153
+ function runKeyFor(row: StoredMetricObservation): string {
154
+ const runId = row.attributes.runId;
155
+ if (typeof runId === "string" && runId.length > 0) return runId;
156
+ return `observedAt:${row.observedAt}`;
157
+ }
158
+
159
+ /** One turn's own token/cost totals -- the unit pricing (including any tiered/long-context catalog rate) is resolved against. */
160
+ interface RunAccumulator {
161
+ provider: string;
162
+ model: string;
163
+ /** The Papyrus task focused when this turn's rows were recorded, or undefined when nothing was focused -- undefined is a real, distinct state from any task id string, never coerced to "unknown". */
164
+ taskId: string | undefined;
165
+ inputTokens: number;
166
+ cacheReadTokens: number;
167
+ cacheWriteTokens: number;
168
+ inputCostUsd: number;
169
+ sawInputCost: boolean;
170
+ cacheReadCostUsd: number;
171
+ sawCacheReadCost: boolean;
172
+ cacheWriteCostUsd: number;
173
+ sawCacheWriteCost: boolean;
174
+ }
175
+
176
+ function newRunAccumulator(provider: string, model: string, taskId: string | undefined): RunAccumulator {
177
+ return {
178
+ provider,
179
+ model,
180
+ taskId,
181
+ inputTokens: 0,
182
+ cacheReadTokens: 0,
183
+ cacheWriteTokens: 0,
184
+ inputCostUsd: 0,
185
+ sawInputCost: false,
186
+ cacheReadCostUsd: 0,
187
+ sawCacheReadCost: false,
188
+ cacheWriteCostUsd: 0,
189
+ sawCacheWriteCost: false,
190
+ };
191
+ }
192
+
193
+ function combineBasis(left: CacheCostBasis, right: CacheCostBasis): CacheCostBasis {
194
+ if (left === "unknown" || right === "unknown") return "unknown";
195
+ if (left === "catalog-estimate" || right === "catalog-estimate") return "catalog-estimate";
196
+ return "provider-reported";
197
+ }
198
+
199
+ /** Sums basis-tagged dollar amounts; a single unknown amount makes the whole sum unknown rather than silently partial. */
200
+ function combineDollarField(entries: Array<{ amount: number | null; basis: CacheCostBasis }>): {
201
+ amount: number | null;
202
+ basis: CacheCostBasis;
203
+ } {
204
+ let total = 0;
205
+ let basis: CacheCostBasis = "provider-reported";
206
+ for (const entry of entries) {
207
+ if (entry.amount === null) return { amount: null, basis: "unknown" };
208
+ total += entry.amount;
209
+ basis = combineBasis(basis, entry.basis);
210
+ }
211
+ return { amount: total, basis };
212
+ }
213
+
214
+ function combineNullableSum(values: Array<number | null>): number | null {
215
+ let total = 0;
216
+ for (const value of values) {
217
+ if (value === null) return null;
218
+ total += value;
219
+ }
220
+ return total;
221
+ }
222
+
223
+ /** "stale" outvotes "fresh"; both outvote "never used a catalog estimate at all" (null). */
224
+ function combineFreshness(values: Array<"fresh" | "stale" | undefined>): "fresh" | "stale" | null {
225
+ let result: "fresh" | "stale" | null = null;
226
+ for (const value of values) {
227
+ if (value === undefined) continue;
228
+ if (value === "stale") return "stale";
229
+ result = "fresh";
230
+ }
231
+ return result;
232
+ }
233
+
234
+ function actualCost(
235
+ tokens: number,
236
+ sawCost: boolean,
237
+ costUsd: number,
238
+ catalogPricePerMillion: number | undefined,
239
+ ): { costUsd: number | null; basis: CacheCostBasis } {
240
+ if (tokens === 0) return { costUsd: 0, basis: "provider-reported" };
241
+ if (sawCost) return { costUsd, basis: "provider-reported" };
242
+ if (catalogPricePerMillion !== undefined)
243
+ return { costUsd: (tokens * catalogPricePerMillion) / CATALOG_PRICE_TOKEN_UNIT, basis: "catalog-estimate" };
244
+ return { costUsd: null, basis: "unknown" };
245
+ }
246
+
247
+ interface RunPricingResult {
248
+ provider: string;
249
+ model: string;
250
+ taskId: string | undefined;
251
+ inputTokens: number;
252
+ cacheReadTokens: number;
253
+ cacheWriteTokens: number;
254
+ inputCostUsd: number;
255
+ sawInputCost: boolean;
256
+ cacheReadCostUsd: number | null;
257
+ cacheReadCostBasis: CacheCostBasis;
258
+ cacheWriteCostUsd: number | null;
259
+ cacheWriteCostBasis: CacheCostBasis;
260
+ counterfactualNoCacheCostUsd: number | null;
261
+ counterfactualBasis: CacheCostBasis;
262
+ cacheWritePremiumUsd: number | null;
263
+ /** This run's own catalog freshness, only when a catalog-estimate price actually ended up used for one of this run's figures; undefined otherwise (never fabricated from an unused lookup result). */
264
+ catalogFreshness: "fresh" | "stale" | undefined;
265
+ }
266
+
267
+ /**
268
+ * Prices exactly one turn, against that turn's own real context size (input + cache-read +
269
+ * cache-write tokens) -- the only level at which a tiered/long-context catalog price can honestly
270
+ * be resolved. Never sums across turns; that happens once, afterward, in aggregateRuns.
271
+ */
272
+ function priceRun(run: RunAccumulator, pricing: CacheEconomicsPricingLookup): RunPricingResult {
273
+ const contextSizeTokens = run.inputTokens + run.cacheReadTokens + run.cacheWriteTokens;
274
+ const catalogPrices = pricing.priceFor(run.provider, run.model, contextSizeTokens);
275
+ const read = actualCost(run.cacheReadTokens, run.sawCacheReadCost, run.cacheReadCostUsd, catalogPrices?.cacheRead);
276
+ const write = actualCost(run.cacheWriteTokens, run.sawCacheWriteCost, run.cacheWriteCostUsd, catalogPrices?.cacheWrite);
277
+
278
+ let baselineRate: number | null = null;
279
+ let baselineBasis: CacheCostBasis = "unknown";
280
+ if (run.sawInputCost && run.inputTokens > 0) {
281
+ baselineRate = run.inputCostUsd / run.inputTokens;
282
+ baselineBasis = "provider-reported";
283
+ } else if (catalogPrices?.input !== undefined) {
284
+ baselineRate = catalogPrices.input / CATALOG_PRICE_TOKEN_UNIT;
285
+ baselineBasis = "catalog-estimate";
286
+ }
287
+
288
+ let counterfactualNoCacheCostUsd: number | null = null;
289
+ let counterfactualBasis: CacheCostBasis = "unknown";
290
+ if (run.cacheReadTokens === 0) {
291
+ counterfactualNoCacheCostUsd = 0;
292
+ counterfactualBasis = "provider-reported";
293
+ } else if (baselineRate !== null) {
294
+ counterfactualNoCacheCostUsd = run.cacheReadTokens * baselineRate;
295
+ counterfactualBasis = baselineBasis;
296
+ }
297
+
298
+ let cacheWritePremiumUsd: number | null = null;
299
+ if (run.cacheWriteTokens === 0) cacheWritePremiumUsd = 0;
300
+ else if (baselineRate !== null && write.costUsd !== null) cacheWritePremiumUsd = write.costUsd - run.cacheWriteTokens * baselineRate;
301
+
302
+ const finalCounterfactualBasis = run.cacheReadTokens === 0 ? "provider-reported" : combineBasis(baselineBasis, counterfactualBasis);
303
+ const usedCatalog =
304
+ read.basis === "catalog-estimate" || write.basis === "catalog-estimate" || finalCounterfactualBasis === "catalog-estimate";
305
+
306
+ return {
307
+ provider: run.provider,
308
+ model: run.model,
309
+ taskId: run.taskId,
310
+ inputTokens: run.inputTokens,
311
+ cacheReadTokens: run.cacheReadTokens,
312
+ cacheWriteTokens: run.cacheWriteTokens,
313
+ inputCostUsd: run.inputCostUsd,
314
+ sawInputCost: run.sawInputCost,
315
+ cacheReadCostUsd: read.costUsd,
316
+ cacheReadCostBasis: read.basis,
317
+ cacheWriteCostUsd: write.costUsd,
318
+ cacheWriteCostBasis: write.basis,
319
+ counterfactualNoCacheCostUsd,
320
+ counterfactualBasis: finalCounterfactualBasis,
321
+ cacheWritePremiumUsd,
322
+ catalogFreshness: usedCatalog ? catalogPrices?.freshness : undefined,
323
+ };
324
+ }
325
+
326
+ /**
327
+ * A single provider/model to resolve one extra, approximate whole-window catalog lookup against
328
+ * for the break-even projection below -- only meaningful when every run being aggregated actually
329
+ * shares this same provider/model (a per-model rollup always does; a per-task rollup only does when
330
+ * that task stayed on one model the whole time). Pass null to skip that refinement rather than
331
+ * guessing which of several different models' rates should stand in for the blend.
332
+ */
333
+ interface SingleModelPricingContext {
334
+ provider: string;
335
+ model: string;
336
+ pricing: CacheEconomicsPricingLookup;
337
+ }
338
+
339
+ /**
340
+ * Sums already-run-priced dollar figures into one group's (model's, or task's) window totals, then
341
+ * derives break-even/payback from those totals plus -- when catalogContext identifies a single real
342
+ * provider/model to resolve against -- one whole-window catalog lookup. This is an intentional,
343
+ * documented approximation: unlike the dollar totals above (correctly tiered per run), a single
344
+ * "how many more read tokens would it take" projection over a blended window has no one real
345
+ * request size to resolve a tier against either.
346
+ */
347
+ function aggregateRunTotals(runs: RunPricingResult[], catalogContext: SingleModelPricingContext | null): CacheEconomicsAggregateTotals {
348
+ const inputTokens = runs.reduce((sum, run) => sum + run.inputTokens, 0);
349
+ const cacheReadTokens = runs.reduce((sum, run) => sum + run.cacheReadTokens, 0);
350
+ const cacheWriteTokens = runs.reduce((sum, run) => sum + run.cacheWriteTokens, 0);
351
+ const read = combineDollarField(runs.map((run) => ({ amount: run.cacheReadCostUsd, basis: run.cacheReadCostBasis })));
352
+ const write = combineDollarField(runs.map((run) => ({ amount: run.cacheWriteCostUsd, basis: run.cacheWriteCostBasis })));
353
+ const counterfactual = combineDollarField(
354
+ runs.map((run) => ({ amount: run.counterfactualNoCacheCostUsd, basis: run.counterfactualBasis })),
355
+ );
356
+ const savingsUsd = counterfactual.amount !== null && read.amount !== null ? counterfactual.amount - read.amount : null;
357
+ const cacheWritePremiumUsd = combineNullableSum(runs.map((run) => run.cacheWritePremiumUsd));
358
+
359
+ const reportingRuns = runs.filter((run) => run.sawInputCost && run.inputTokens > 0);
360
+ const effectiveInputRateUsdPerToken =
361
+ reportingRuns.length > 0
362
+ ? reportingRuns.reduce((sum, run) => sum + run.inputCostUsd, 0) / reportingRuns.reduce((sum, run) => sum + run.inputTokens, 0)
363
+ : null;
364
+
365
+ let breakEvenReadTokens: number | null = null;
366
+ if (cacheWriteTokens === 0) breakEvenReadTokens = 0;
367
+ else if (cacheWritePremiumUsd !== null) {
368
+ const catalogPrices = catalogContext?.pricing.priceFor(
369
+ catalogContext.provider,
370
+ catalogContext.model,
371
+ inputTokens + cacheReadTokens + cacheWriteTokens,
372
+ );
373
+ const baselineRate =
374
+ effectiveInputRateUsdPerToken ?? (catalogPrices?.input !== undefined ? catalogPrices.input / CATALOG_PRICE_TOKEN_UNIT : null);
375
+ const readRate =
376
+ cacheReadTokens > 0 && read.amount !== null
377
+ ? read.amount / cacheReadTokens
378
+ : (catalogPrices?.cacheRead ?? undefined) !== undefined
379
+ ? catalogPrices!.cacheRead! / CATALOG_PRICE_TOKEN_UNIT
380
+ : null;
381
+ const perTokenSavings = baselineRate !== null && readRate !== null ? baselineRate - readRate : null;
382
+ if (cacheWritePremiumUsd <= 0) breakEvenReadTokens = 0;
383
+ else if (perTokenSavings !== null && perTokenSavings > 0) breakEvenReadTokens = Math.ceil(cacheWritePremiumUsd / perTokenSavings);
384
+ }
385
+
386
+ const paybackAchieved =
387
+ cacheWriteTokens === 0 ? null : savingsUsd !== null && cacheWritePremiumUsd !== null ? savingsUsd >= cacheWritePremiumUsd : null;
388
+
389
+ return {
390
+ inputTokens,
391
+ cacheReadTokens,
392
+ cacheWriteTokens,
393
+ cacheReadCostUsd: read.amount,
394
+ cacheReadCostBasis: read.basis,
395
+ cacheWriteCostUsd: write.amount,
396
+ cacheWriteCostBasis: write.basis,
397
+ effectiveInputRateUsdPerToken,
398
+ counterfactualNoCacheCostUsd: counterfactual.amount,
399
+ counterfactualBasis: counterfactual.basis,
400
+ savingsUsd,
401
+ cacheWritePremiumUsd,
402
+ breakEvenReadTokens,
403
+ paybackAchieved,
404
+ catalogFreshness: combineFreshness(runs.map((run) => run.catalogFreshness)),
405
+ };
406
+ }
407
+
408
+ function buildModelSummary(
409
+ provider: string,
410
+ model: string,
411
+ runs: RunPricingResult[],
412
+ pricing: CacheEconomicsPricingLookup,
413
+ ): CacheEconomicsModelSummary {
414
+ return { provider, model, ...aggregateRunTotals(runs, { provider, model, pricing }) };
415
+ }
416
+
417
+ /** A single-model catalog context only when every run in this task's group really did share one provider/model -- never guesses a representative model for a task that switched partway through. */
418
+ function buildTaskSummary(taskId: string, runs: RunPricingResult[], pricing: CacheEconomicsPricingLookup): CacheEconomicsTaskSummary {
419
+ const firstRun = runs[0]!;
420
+ const singleModel = runs.every((run) => run.provider === firstRun.provider && run.model === firstRun.model);
421
+ const catalogContext = singleModel ? { provider: firstRun.provider, model: firstRun.model, pricing } : null;
422
+ return { taskId, ...aggregateRunTotals(runs, catalogContext) };
423
+ }
424
+
425
+ interface ResetEvent {
426
+ sessionId: string;
427
+ occurredAt: number;
428
+ resetReason: Exclude<ContextPrefixResetReason, null>;
429
+ }
430
+
431
+ function resetEventFromRow(row: StoredMetricObservation): ResetEvent | null {
432
+ const resetReason = row.attributes.resetReason;
433
+ if (resetReason === null || resetReason === undefined || !RESET_REASONS.has(resetReason as Exclude<ContextPrefixResetReason, null>))
434
+ return null;
435
+ if (typeof row.scope !== "string" || row.scope.length === 0) return null;
436
+ return { sessionId: row.scope, occurredAt: row.observedAt, resetReason: resetReason as Exclude<ContextPrefixResetReason, null> };
437
+ }
438
+
439
+ function stablePrefixChurnFrom(snapshotRows: StoredMetricObservation[]): CacheEconomicsStablePrefixPoint[] {
440
+ return snapshotRows
441
+ .filter(
442
+ (row): row is StoredMetricObservation =>
443
+ row.source === "pi-context-snapshot" &&
444
+ row.metric === "snapshot" &&
445
+ typeof row.value === "number" &&
446
+ typeof row.scope === "string" &&
447
+ row.scope.length > 0,
448
+ )
449
+ .map((row) => ({
450
+ sessionId: row.scope as string,
451
+ observedAt: row.observedAt,
452
+ stablePrefixTokens: row.value as number,
453
+ resetReason: (row.attributes.resetReason ?? null) as ContextPrefixResetReason,
454
+ }))
455
+ .sort((left, right) => left.observedAt - right.observedAt);
456
+ }
457
+
458
+ function findMissedOpportunities(
459
+ usageRows: StoredMetricObservation[],
460
+ snapshotRows: StoredMetricObservation[],
461
+ ): CacheEconomicsMissedOpportunity[] {
462
+ const resets = snapshotRows
463
+ .filter((row) => row.source === "pi-context-snapshot" && row.metric === "snapshot")
464
+ .map(resetEventFromRow)
465
+ .filter((event): event is ResetEvent => event !== null);
466
+ if (resets.length === 0) return [];
467
+ const writes = usageRows.filter(
468
+ (row) => row.source === "pi" && row.metric === "cache-write-tokens" && typeof row.value === "number" && row.value > 0,
469
+ );
470
+ const found: CacheEconomicsMissedOpportunity[] = [];
471
+ for (const reset of resets) {
472
+ const match = writes
473
+ .filter((row) => attributeText(row.attributes, "sessionId") === reset.sessionId)
474
+ .filter(
475
+ (row) => row.observedAt >= reset.occurredAt && row.observedAt - reset.occurredAt <= CACHE_ECONOMICS_LOSS_CORRELATION_WINDOW_MS,
476
+ )
477
+ .sort((left, right) => left.observedAt - right.observedAt)[0];
478
+ if (!match) continue;
479
+ const costRow = usageRows.find(
480
+ (row) =>
481
+ row.source === "pi" &&
482
+ row.metric === "cache-write-cost" &&
483
+ attributeText(row.attributes, "sessionId") === reset.sessionId &&
484
+ row.observedAt === match.observedAt,
485
+ );
486
+ found.push({
487
+ sessionId: reset.sessionId,
488
+ occurredAt: match.observedAt,
489
+ resetReason: reset.resetReason,
490
+ cacheWriteTokens: match.value as number,
491
+ cacheWriteCostUsd: typeof costRow?.value === "number" ? costRow.value : null,
492
+ note: `Candidate missed-cache opportunity: a ${reset.resetReason} context-prefix reset was followed by a cache write in the same session within the correlation window. This is a correlated pattern, not a proven cause.`,
493
+ });
494
+ }
495
+ return found;
496
+ }
497
+
498
+ /**
499
+ * Pure aggregation over already-fetched, already-bounded rows -- the operation layer owns querying
500
+ * MetricStore and the model catalog; this function only ever combines what it is given. Rows are
501
+ * first grouped into per-turn runs (see runKeyFor) and priced against each run's own real context
502
+ * size, so a tiered/long-context catalog price is resolved honestly instead of guessed against a
503
+ * blended window-wide sum; run-level dollar figures are only summed together afterward. Every
504
+ * derived (non-trivial) dollar figure is explicitly basis-tagged; nothing is fabricated when
505
+ * evidence is absent.
506
+ */
507
+ export function buildCacheEconomicsSummary(
508
+ usageRows: StoredMetricObservation[],
509
+ snapshotRows: StoredMetricObservation[],
510
+ pricing: CacheEconomicsPricingLookup,
511
+ options: CacheEconomicsSummaryOptions,
512
+ ): CacheEconomicsSummary {
513
+ const byRun = new Map<string, RunAccumulator>();
514
+ const admittedModels = new Set<string>();
515
+ let modelGroupsTruncated = false;
516
+ for (const row of usageRows) {
517
+ if (row.source !== "pi" || typeof row.value !== "number" || !Number.isFinite(row.value) || row.value < 0) continue;
518
+ if (row.observedAt < options.since || row.observedAt > options.until) continue;
519
+ const provider = attributeText(row.attributes, "provider");
520
+ const model = attributeText(row.attributes, "model");
521
+ const modelKey = `${provider}\u0000${model}`;
522
+ if (!admittedModels.has(modelKey)) {
523
+ if (admittedModels.size >= CACHE_ECONOMICS_MAX_MODEL_GROUPS) {
524
+ modelGroupsTruncated = true;
525
+ continue;
526
+ }
527
+ admittedModels.add(modelKey);
528
+ }
529
+ const taskId = typeof row.attributes.taskId === "string" && row.attributes.taskId.length > 0 ? row.attributes.taskId : undefined;
530
+ const runKey = `${modelKey}\u0000${runKeyFor(row)}`;
531
+ const run = byRun.get(runKey) ?? newRunAccumulator(provider, model, taskId);
532
+ byRun.set(runKey, run);
533
+ if (row.metric === "input-tokens" && row.unit === "tokens") run.inputTokens += row.value;
534
+ else if (row.metric === "cache-read-tokens" && row.unit === "tokens") run.cacheReadTokens += row.value;
535
+ else if (row.metric === "cache-write-tokens" && row.unit === "tokens") run.cacheWriteTokens += row.value;
536
+ else if (row.metric === "input-cost" && row.unit === "usd") {
537
+ run.inputCostUsd += row.value;
538
+ run.sawInputCost = true;
539
+ } else if (row.metric === "cache-read-cost" && row.unit === "usd") {
540
+ run.cacheReadCostUsd += row.value;
541
+ run.sawCacheReadCost = true;
542
+ } else if (row.metric === "cache-write-cost" && row.unit === "usd") {
543
+ run.cacheWriteCostUsd += row.value;
544
+ run.sawCacheWriteCost = true;
545
+ }
546
+ }
547
+
548
+ const allPriced = [...byRun.values()].map((run) => priceRun(run, pricing));
549
+
550
+ const runsByModel = new Map<string, { provider: string; model: string; runs: RunPricingResult[] }>();
551
+ const runsByTask = new Map<string, RunPricingResult[]>();
552
+ const unattributedRuns: RunPricingResult[] = [];
553
+ for (const priced of allPriced) {
554
+ const modelKey = `${priced.provider}\u0000${priced.model}`;
555
+ const existingModel = runsByModel.get(modelKey);
556
+ if (existingModel) existingModel.runs.push(priced);
557
+ else runsByModel.set(modelKey, { provider: priced.provider, model: priced.model, runs: [priced] });
558
+
559
+ if (priced.taskId === undefined) {
560
+ unattributedRuns.push(priced);
561
+ continue;
562
+ }
563
+ const existingTask = runsByTask.get(priced.taskId);
564
+ if (existingTask) existingTask.push(priced);
565
+ else runsByTask.set(priced.taskId, [priced]);
566
+ }
567
+ const models = [...runsByModel.values()]
568
+ .map(({ provider, model, runs }) => buildModelSummary(provider, model, runs, pricing))
569
+ .sort(
570
+ (left, right) =>
571
+ right.cacheReadTokens + right.cacheWriteTokens - (left.cacheReadTokens + left.cacheWriteTokens) ||
572
+ left.model.localeCompare(right.model),
573
+ );
574
+
575
+ const allTasks = [...runsByTask.entries()]
576
+ .map(([taskId, runs]) => buildTaskSummary(taskId, runs, pricing))
577
+ .sort(
578
+ (left, right) =>
579
+ right.cacheReadTokens + right.cacheWriteTokens - (left.cacheReadTokens + left.cacheWriteTokens) ||
580
+ left.taskId.localeCompare(right.taskId),
581
+ );
582
+ const taskGroupsTruncated = allTasks.length > CACHE_ECONOMICS_MAX_TASK_GROUPS;
583
+ const tasks = allTasks.slice(0, CACHE_ECONOMICS_MAX_TASK_GROUPS);
584
+
585
+ const unattributedCacheRead = combineDollarField(
586
+ unattributedRuns.map((run) => ({ amount: run.cacheReadCostUsd, basis: run.cacheReadCostBasis })),
587
+ );
588
+ const unattributedCacheWrite = combineDollarField(
589
+ unattributedRuns.map((run) => ({ amount: run.cacheWriteCostUsd, basis: run.cacheWriteCostBasis })),
590
+ );
591
+ const unattributedCacheActivity: CacheEconomicsUnattributedActivity = {
592
+ cacheReadTokens: unattributedRuns.reduce((sum, run) => sum + run.cacheReadTokens, 0),
593
+ cacheWriteTokens: unattributedRuns.reduce((sum, run) => sum + run.cacheWriteTokens, 0),
594
+ cacheReadCostUsd: unattributedCacheRead.amount,
595
+ cacheReadCostBasis: unattributedCacheRead.basis,
596
+ cacheWriteCostUsd: unattributedCacheWrite.amount,
597
+ cacheWriteCostBasis: unattributedCacheWrite.basis,
598
+ catalogFreshness: combineFreshness(unattributedRuns.map((run) => run.catalogFreshness)),
599
+ };
600
+
601
+ const windowedSnapshotRows = snapshotRows.filter((row) => row.observedAt >= options.since && row.observedAt <= options.until);
602
+ const allMissed = findMissedOpportunities(
603
+ usageRows.filter((row) => row.observedAt >= options.since && row.observedAt <= options.until),
604
+ windowedSnapshotRows,
605
+ );
606
+ const missedTruncated = allMissed.length > CACHE_ECONOMICS_MAX_MISSED_OPPORTUNITIES;
607
+ const missedOpportunities = allMissed.slice(0, CACHE_ECONOMICS_MAX_MISSED_OPPORTUNITIES);
608
+
609
+ const allChurn = stablePrefixChurnFrom(windowedSnapshotRows);
610
+ const churnTruncated = allChurn.length > CACHE_ECONOMICS_MAX_STABLE_PREFIX_POINTS;
611
+ const stablePrefixChurn = allChurn.slice(-CACHE_ECONOMICS_MAX_STABLE_PREFIX_POINTS);
612
+
613
+ return {
614
+ since: options.since,
615
+ until: options.until,
616
+ models,
617
+ tasks,
618
+ unattributedCacheActivity,
619
+ missedOpportunities,
620
+ stablePrefixChurn,
621
+ truncated: modelGroupsTruncated || taskGroupsTruncated || missedTruncated || churnTruncated,
622
+ };
623
+ }