@trazum/core 1.10.0 → 1.25.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 (84) hide show
  1. package/dist/against.d.ts +32 -0
  2. package/dist/against.d.ts.map +1 -0
  3. package/dist/against.js +34 -0
  4. package/dist/against.js.map +1 -0
  5. package/dist/config-schema.d.ts +42 -1
  6. package/dist/config-schema.d.ts.map +1 -1
  7. package/dist/config-schema.js +61 -0
  8. package/dist/config-schema.js.map +1 -1
  9. package/dist/conversation.d.ts +121 -0
  10. package/dist/conversation.d.ts.map +1 -0
  11. package/dist/conversation.js +157 -0
  12. package/dist/conversation.js.map +1 -0
  13. package/dist/csv.d.ts +61 -0
  14. package/dist/csv.d.ts.map +1 -0
  15. package/dist/csv.js +149 -0
  16. package/dist/csv.js.map +1 -0
  17. package/dist/evaluate.d.ts +24 -0
  18. package/dist/evaluate.d.ts.map +1 -1
  19. package/dist/evaluate.js +5 -2
  20. package/dist/evaluate.js.map +1 -1
  21. package/dist/index.d.ts +25 -3
  22. package/dist/index.d.ts.map +1 -1
  23. package/dist/index.js +28 -1
  24. package/dist/index.js.map +1 -1
  25. package/dist/input-shape.d.ts +104 -0
  26. package/dist/input-shape.d.ts.map +1 -0
  27. package/dist/input-shape.js +132 -0
  28. package/dist/input-shape.js.map +1 -0
  29. package/dist/levers.d.ts +151 -0
  30. package/dist/levers.d.ts.map +1 -0
  31. package/dist/levers.js +160 -0
  32. package/dist/levers.js.map +1 -0
  33. package/dist/node.d.ts +1 -1
  34. package/dist/node.d.ts.map +1 -1
  35. package/dist/output-shape.d.ts +96 -0
  36. package/dist/output-shape.d.ts.map +1 -0
  37. package/dist/output-shape.js +145 -0
  38. package/dist/output-shape.js.map +1 -0
  39. package/dist/pricing-overlay.d.ts +1 -1
  40. package/dist/pricing-overlay.d.ts.map +1 -1
  41. package/dist/pricing-overlay.js +46 -0
  42. package/dist/pricing-overlay.js.map +1 -1
  43. package/dist/repeats.d.ts +75 -0
  44. package/dist/repeats.d.ts.map +1 -0
  45. package/dist/repeats.js +82 -0
  46. package/dist/repeats.js.map +1 -0
  47. package/dist/reprice.d.ts +143 -0
  48. package/dist/reprice.d.ts.map +1 -0
  49. package/dist/reprice.js +82 -0
  50. package/dist/reprice.js.map +1 -0
  51. package/dist/session-cost.d.ts +70 -0
  52. package/dist/session-cost.d.ts.map +1 -0
  53. package/dist/session-cost.js +90 -0
  54. package/dist/session-cost.js.map +1 -0
  55. package/dist/session-ledger.d.ts +77 -0
  56. package/dist/session-ledger.d.ts.map +1 -0
  57. package/dist/session-ledger.js +99 -0
  58. package/dist/session-ledger.js.map +1 -0
  59. package/dist/ttl-fit.d.ts +103 -0
  60. package/dist/ttl-fit.d.ts.map +1 -0
  61. package/dist/ttl-fit.js +184 -0
  62. package/dist/ttl-fit.js.map +1 -0
  63. package/dist/usage.d.ts +434 -16
  64. package/dist/usage.d.ts.map +1 -1
  65. package/dist/usage.js +383 -23
  66. package/dist/usage.js.map +1 -1
  67. package/package.json +1 -1
  68. package/src/against.ts +48 -0
  69. package/src/config-schema.ts +106 -0
  70. package/src/conversation.ts +305 -0
  71. package/src/csv.ts +184 -0
  72. package/src/evaluate.ts +33 -3
  73. package/src/index.ts +51 -1
  74. package/src/input-shape.ts +259 -0
  75. package/src/levers.ts +331 -0
  76. package/src/node.ts +1 -1
  77. package/src/output-shape.ts +254 -0
  78. package/src/pricing-overlay.ts +52 -1
  79. package/src/repeats.ts +166 -0
  80. package/src/reprice.ts +227 -0
  81. package/src/session-cost.ts +170 -0
  82. package/src/session-ledger.ts +189 -0
  83. package/src/ttl-fit.ts +251 -0
  84. package/src/usage.ts +795 -7
@@ -0,0 +1,82 @@
1
+ import { effectivePricing, multipliersFor } from './pricing.js';
2
+ /**
3
+ * What a set of token counts costs on one model, at the rates in force on a
4
+ * date.
5
+ *
6
+ * The same arithmetic `profileUsage` does per call, over an aggregate — which
7
+ * is only sound because every token class is priced independently of the
8
+ * others and of how many calls produced them.
9
+ */
10
+ export function priceTokensOn(breakdown, model, on = new Date()) {
11
+ const { inputPerMTok, outputPerMTok } = effectivePricing(model, on);
12
+ const rates = multipliersFor(model);
13
+ const per = (tokens, rate) => (tokens / 1_000_000) * rate;
14
+ return (per(breakdown.inputTokens, inputPerMTok) +
15
+ per(breakdown.cacheReadTokens, inputPerMTok * rates.cacheRead) +
16
+ per(breakdown.cacheWrite5mTokens, inputPerMTok * rates.cacheWrite5m) +
17
+ per(breakdown.cacheWrite1hTokens, inputPerMTok * rates.cacheWrite1h) +
18
+ per(breakdown.outputTokens, outputPerMTok));
19
+ }
20
+ /**
21
+ * Reprices a profile's label-and-model slices onto one target model.
22
+ *
23
+ * Returns `null` when the catalogue does not know the target: a comparison
24
+ * against a price nobody has is worse than no comparison, and the caller is
25
+ * better placed to say so in its own words than this is to invent a zero.
26
+ */
27
+ export function repriceProfile(report, targetId, catalogue, on = new Date()) {
28
+ const target = catalogue.byId.get(targetId);
29
+ if (!target)
30
+ return null;
31
+ const slices = [];
32
+ const overContext = [];
33
+ const alreadyOnTarget = { calls: 0, usd: 0 };
34
+ let assumedWriteTtlCalls = 0;
35
+ for (const slice of report.byLabelAndModel) {
36
+ const { breakdown } = slice;
37
+ if (slice.model === target.id) {
38
+ alreadyOnTarget.calls += breakdown.calls;
39
+ alreadyOnTarget.usd += breakdown.totalUsd;
40
+ continue;
41
+ }
42
+ if (breakdown.maxCallInputTokens > target.contextWindow) {
43
+ overContext.push({
44
+ label: slice.label,
45
+ model: slice.model,
46
+ calls: breakdown.calls,
47
+ currentUsd: breakdown.totalUsd,
48
+ maxCallInputTokens: breakdown.maxCallInputTokens,
49
+ });
50
+ continue;
51
+ }
52
+ const targetUsd = priceTokensOn(breakdown, target, on);
53
+ assumedWriteTtlCalls += breakdown.assumedWriteTtlCalls;
54
+ slices.push({
55
+ label: slice.label,
56
+ model: slice.model,
57
+ calls: breakdown.calls,
58
+ currentUsd: breakdown.totalUsd,
59
+ targetUsd,
60
+ deltaUsd: targetUsd - breakdown.totalUsd,
61
+ maxCallInputTokens: breakdown.maxCallInputTokens,
62
+ });
63
+ }
64
+ slices.sort((a, b) => a.deltaUsd - b.deltaUsd || b.currentUsd - a.currentUsd);
65
+ overContext.sort((a, b) => b.currentUsd - a.currentUsd);
66
+ const currentUsd = slices.reduce((sum, s) => sum + s.currentUsd, 0);
67
+ const targetUsd = slices.reduce((sum, s) => sum + s.targetUsd, 0);
68
+ return {
69
+ target: { id: target.id, displayName: target.displayName, contextWindow: target.contextWindow },
70
+ slices,
71
+ currentUsd,
72
+ targetUsd,
73
+ deltaUsd: targetUsd - currentUsd,
74
+ overContext,
75
+ alreadyOnTarget,
76
+ assumedWriteTtlCalls,
77
+ unpricedModels: report.unpricedModels,
78
+ unpricedCalls: report.unpriced.calls,
79
+ sameTokensAssumed: true,
80
+ };
81
+ }
82
+ //# sourceMappingURL=reprice.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"reprice.js","sourceRoot":"","sources":["../src/reprice.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,gBAAgB,EAAE,cAAc,EAAE,MAAM,cAAc,CAAC;AAgIhE;;;;;;;GAOG;AACH,MAAM,UAAU,aAAa,CAC3B,SAGC,EACD,KAAmB,EACnB,EAAE,GAAS,IAAI,IAAI,EAAE;IAErB,MAAM,EAAE,YAAY,EAAE,aAAa,EAAE,GAAG,gBAAgB,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC;IACpE,MAAM,KAAK,GAAG,cAAc,CAAC,KAAK,CAAC,CAAC;IACpC,MAAM,GAAG,GAAG,CAAC,MAAc,EAAE,IAAY,EAAU,EAAE,CAAC,CAAC,MAAM,GAAG,SAAS,CAAC,GAAG,IAAI,CAAC;IAClF,OAAO,CACL,GAAG,CAAC,SAAS,CAAC,WAAW,EAAE,YAAY,CAAC;QACxC,GAAG,CAAC,SAAS,CAAC,eAAe,EAAE,YAAY,GAAG,KAAK,CAAC,SAAS,CAAC;QAC9D,GAAG,CAAC,SAAS,CAAC,kBAAkB,EAAE,YAAY,GAAG,KAAK,CAAC,YAAY,CAAC;QACpE,GAAG,CAAC,SAAS,CAAC,kBAAkB,EAAE,YAAY,GAAG,KAAK,CAAC,YAAY,CAAC;QACpE,GAAG,CAAC,SAAS,CAAC,YAAY,EAAE,aAAa,CAAC,CAC3C,CAAC;AACJ,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,cAAc,CAC5B,MAA0B,EAC1B,QAAgB,EAChB,SAA2B,EAC3B,EAAE,GAAS,IAAI,IAAI,EAAE;IAErB,MAAM,MAAM,GAAG,SAAS,CAAC,IAAI,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC;IAC5C,IAAI,CAAC,MAAM;QAAE,OAAO,IAAI,CAAC;IAEzB,MAAM,MAAM,GAAoB,EAAE,CAAC;IACnC,MAAM,WAAW,GAAuB,EAAE,CAAC;IAC3C,MAAM,eAAe,GAAG,EAAE,KAAK,EAAE,CAAC,EAAE,GAAG,EAAE,CAAC,EAAE,CAAC;IAC7C,IAAI,oBAAoB,GAAG,CAAC,CAAC;IAE7B,KAAK,MAAM,KAAK,IAAI,MAAM,CAAC,eAAe,EAAE,CAAC;QAC3C,MAAM,EAAE,SAAS,EAAE,GAAG,KAAK,CAAC;QAC5B,IAAI,KAAK,CAAC,KAAK,KAAK,MAAM,CAAC,EAAE,EAAE,CAAC;YAC9B,eAAe,CAAC,KAAK,IAAI,SAAS,CAAC,KAAK,CAAC;YACzC,eAAe,CAAC,GAAG,IAAI,SAAS,CAAC,QAAQ,CAAC;YAC1C,SAAS;QACX,CAAC;QACD,IAAI,SAAS,CAAC,kBAAkB,GAAG,MAAM,CAAC,aAAa,EAAE,CAAC;YACxD,WAAW,CAAC,IAAI,CAAC;gBACf,KAAK,EAAE,KAAK,CAAC,KAAK;gBAClB,KAAK,EAAE,KAAK,CAAC,KAAK;gBAClB,KAAK,EAAE,SAAS,CAAC,KAAK;gBACtB,UAAU,EAAE,SAAS,CAAC,QAAQ;gBAC9B,kBAAkB,EAAE,SAAS,CAAC,kBAAkB;aACjD,CAAC,CAAC;YACH,SAAS;QACX,CAAC;QACD,MAAM,SAAS,GAAG,aAAa,CAAC,SAAS,EAAE,MAAM,EAAE,EAAE,CAAC,CAAC;QACvD,oBAAoB,IAAI,SAAS,CAAC,oBAAoB,CAAC;QACvD,MAAM,CAAC,IAAI,CAAC;YACV,KAAK,EAAE,KAAK,CAAC,KAAK;YAClB,KAAK,EAAE,KAAK,CAAC,KAAK;YAClB,KAAK,EAAE,SAAS,CAAC,KAAK;YACtB,UAAU,EAAE,SAAS,CAAC,QAAQ;YAC9B,SAAS;YACT,QAAQ,EAAE,SAAS,GAAG,SAAS,CAAC,QAAQ;YACxC,kBAAkB,EAAE,SAAS,CAAC,kBAAkB;SACjD,CAAC,CAAC;IACL,CAAC;IAED,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,QAAQ,GAAG,CAAC,CAAC,QAAQ,IAAI,CAAC,CAAC,UAAU,GAAG,CAAC,CAAC,UAAU,CAAC,CAAC;IAC9E,WAAW,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,UAAU,GAAG,CAAC,CAAC,UAAU,CAAC,CAAC;IAExD,MAAM,UAAU,GAAG,MAAM,CAAC,MAAM,CAAC,CAAC,GAAG,EAAE,CAAC,EAAE,EAAE,CAAC,GAAG,GAAG,CAAC,CAAC,UAAU,EAAE,CAAC,CAAC,CAAC;IACpE,MAAM,SAAS,GAAG,MAAM,CAAC,MAAM,CAAC,CAAC,GAAG,EAAE,CAAC,EAAE,EAAE,CAAC,GAAG,GAAG,CAAC,CAAC,SAAS,EAAE,CAAC,CAAC,CAAC;IAElE,OAAO;QACL,MAAM,EAAE,EAAE,EAAE,EAAE,MAAM,CAAC,EAAE,EAAE,WAAW,EAAE,MAAM,CAAC,WAAW,EAAE,aAAa,EAAE,MAAM,CAAC,aAAa,EAAE;QAC/F,MAAM;QACN,UAAU;QACV,SAAS;QACT,QAAQ,EAAE,SAAS,GAAG,UAAU;QAChC,WAAW;QACX,eAAe;QACf,oBAAoB;QACpB,cAAc,EAAE,MAAM,CAAC,cAAc;QACrC,aAAa,EAAE,MAAM,CAAC,QAAQ,CAAC,KAAK;QACpC,iBAAiB,EAAE,IAAI;KACxB,CAAC;AACJ,CAAC"}
@@ -0,0 +1,70 @@
1
+ import type { PricingCatalogue } from './pricing.js';
2
+ import type { UsageRecord } from './usage.js';
3
+ /**
4
+ * What one conversation costs.
5
+ *
6
+ * ## The question a total cannot answer
7
+ *
8
+ * "Support cost $4,000 last month" does not say whether that is forty thousand
9
+ * cheap conversations or four hundred expensive ones, and every decision made
10
+ * on top of it needs the answer: what to charge per seat, where to put a quota,
11
+ * whether one runaway agent loop is eating the budget. The bill has the data —
12
+ * the log groups by `session` already — and nothing was reporting it.
13
+ *
14
+ * ## Median and p95, not mean
15
+ *
16
+ * A mean conversation cost is the total divided by the session count, which is
17
+ * the total again wearing a hat: one 400-turn agent loop drags it up and hides
18
+ * the ordinary case. The **median** is the conversation in the middle — what a
19
+ * typical one costs — and the **p95** is the one a quota has to survive. The
20
+ * gap between them is the finding: `$0.02 median, $1.80 p95` is a workload with
21
+ * a tail worth hunting; `$0.40 median, $0.55 p95` is a workload that is simply
22
+ * expensive, and no amount of tail-hunting will fix it.
23
+ *
24
+ * Every figure is **exact** — the provider's own billed counts, summed per
25
+ * conversation at each model's published rates. No counterfactual, no estimate.
26
+ *
27
+ * ## What it refuses to claim
28
+ *
29
+ * A conversation that started before this log or continues after it is counted
30
+ * only for the turns recorded here, so its cost is a floor. That is stated
31
+ * rather than corrected: guessing at unseen turns would be exactly the kind of
32
+ * invention this package exists to end. Session keys group turns and never
33
+ * leave this module, as everywhere the field is touched.
34
+ */
35
+ export interface SessionCostShape {
36
+ label: string;
37
+ model: string;
38
+ modelName: string;
39
+ /** Conversations measured. Never which ones. */
40
+ sessions: number;
41
+ calls: number;
42
+ /** What those conversations cost in total — exact, billed. */
43
+ totalUsd: number;
44
+ /** The conversation in the middle. */
45
+ medianUsd: number;
46
+ /** The conversation a quota has to survive: 95th percentile, by nearest rank. */
47
+ p95Usd: number;
48
+ /** The single most expensive conversation in the slice. */
49
+ maxUsd: number;
50
+ /** Turns in the median conversation, for scale. */
51
+ medianTurns: number;
52
+ }
53
+ export interface SessionCostOptions {
54
+ catalogue: PricingCatalogue;
55
+ on?: Date;
56
+ /**
57
+ * Slices with fewer conversations than this are dropped: a median over three
58
+ * sessions is not a median, it is one of the three, and a p95 over them is
59
+ * the maximum wearing a percentile's name. Default 5.
60
+ */
61
+ minSessions?: number;
62
+ }
63
+ export interface SessionCostTracker {
64
+ add(record: UsageRecord): void;
65
+ finish(): SessionCostShape[];
66
+ }
67
+ export declare function createSessionCostTracker(options: SessionCostOptions): SessionCostTracker;
68
+ /** The same measurement over a list, for a caller holding one already. */
69
+ export declare function sessionCostShapes(records: readonly UsageRecord[], options: SessionCostOptions): SessionCostShape[];
70
+ //# sourceMappingURL=session-cost.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"session-cost.d.ts","sourceRoot":"","sources":["../src/session-cost.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAC;AACrD,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAE9C;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+BG;AAEH,MAAM,WAAW,gBAAgB;IAC/B,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,CAAC;IACd,SAAS,EAAE,MAAM,CAAC;IAClB,gDAAgD;IAChD,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,EAAE,MAAM,CAAC;IACd,8DAA8D;IAC9D,QAAQ,EAAE,MAAM,CAAC;IACjB,sCAAsC;IACtC,SAAS,EAAE,MAAM,CAAC;IAClB,iFAAiF;IACjF,MAAM,EAAE,MAAM,CAAC;IACf,2DAA2D;IAC3D,MAAM,EAAE,MAAM,CAAC;IACf,mDAAmD;IACnD,WAAW,EAAE,MAAM,CAAC;CACrB;AAED,MAAM,WAAW,kBAAkB;IACjC,SAAS,EAAE,gBAAgB,CAAC;IAC5B,EAAE,CAAC,EAAE,IAAI,CAAC;IACV;;;;OAIG;IACH,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAED,MAAM,WAAW,kBAAkB;IACjC,GAAG,CAAC,MAAM,EAAE,WAAW,GAAG,IAAI,CAAC;IAC/B,MAAM,IAAI,gBAAgB,EAAE,CAAC;CAC9B;AAgCD,wBAAgB,wBAAwB,CAAC,OAAO,EAAE,kBAAkB,GAAG,kBAAkB,CAwDxF;AAED,0EAA0E;AAC1E,wBAAgB,iBAAiB,CAC/B,OAAO,EAAE,SAAS,WAAW,EAAE,EAC/B,OAAO,EAAE,kBAAkB,GAC1B,gBAAgB,EAAE,CAIpB"}
@@ -0,0 +1,90 @@
1
+ import { effectivePricing, multipliersFor } from './pricing.js';
2
+ import { UNLABELLED } from './usage.js';
3
+ /** Every billed dollar of one call, at its own model's rates. */
4
+ function costOf(record, catalogue, on) {
5
+ const model = catalogue.byId.get(record.model);
6
+ if (!model)
7
+ return null;
8
+ const { inputPerMTok, outputPerMTok } = effectivePricing(model, on);
9
+ const rates = multipliersFor(model);
10
+ const per = (tokens, rate) => (tokens / 1_000_000) * rate;
11
+ return (per(record.inputTokens, inputPerMTok) +
12
+ per(record.cacheReadTokens, inputPerMTok * rates.cacheRead) +
13
+ per(record.cacheWrite5mTokens, inputPerMTok * rates.cacheWrite5m) +
14
+ per(record.cacheWrite1hTokens, inputPerMTok * rates.cacheWrite1h) +
15
+ per(record.outputTokens, outputPerMTok));
16
+ }
17
+ const median = (sorted) => {
18
+ const mid = Math.floor(sorted.length / 2);
19
+ return sorted.length % 2 === 1 ? sorted[mid] : (sorted[mid - 1] + sorted[mid]) / 2;
20
+ };
21
+ /**
22
+ * The 95th percentile by **nearest rank**: the smallest value at or above which
23
+ * 95% of the conversations sit. Interpolating between two conversations would
24
+ * report a cost no conversation had, and this figure exists to be compared
25
+ * against a real quota.
26
+ */
27
+ const p95 = (sorted) => sorted[Math.min(sorted.length - 1, Math.ceil(sorted.length * 0.95) - 1)];
28
+ export function createSessionCostTracker(options) {
29
+ const { catalogue, on = new Date(), minSessions = 5 } = options;
30
+ const slices = new Map();
31
+ const add = (record) => {
32
+ if (record.session === null)
33
+ return;
34
+ const cost = costOf(record, catalogue, on);
35
+ // An unpriced model contributes no dollars anywhere else either.
36
+ if (cost === null)
37
+ return;
38
+ const sliceKey = `${record.label ?? UNLABELLED}\n${record.model}`;
39
+ let sessions = slices.get(sliceKey);
40
+ if (!sessions) {
41
+ sessions = new Map();
42
+ slices.set(sliceKey, sessions);
43
+ }
44
+ const existing = sessions.get(record.session);
45
+ if (existing) {
46
+ existing.usd += cost;
47
+ existing.turns += 1;
48
+ }
49
+ else {
50
+ sessions.set(record.session, { usd: cost, turns: 1 });
51
+ }
52
+ };
53
+ const finish = () => {
54
+ const out = [];
55
+ for (const [sliceKey, sessions] of slices) {
56
+ if (sessions.size < minSessions)
57
+ continue;
58
+ const split = sliceKey.indexOf('\n');
59
+ const modelId = sliceKey.slice(split + 1);
60
+ const model = catalogue.byId.get(modelId);
61
+ if (!model)
62
+ continue;
63
+ const costs = [...sessions.values()].map((s) => s.usd).sort((a, b) => a - b);
64
+ const turns = [...sessions.values()].map((s) => s.turns).sort((a, b) => a - b);
65
+ out.push({
66
+ label: sliceKey.slice(0, split),
67
+ model: modelId,
68
+ modelName: model.displayName,
69
+ sessions: sessions.size,
70
+ calls: turns.reduce((sum, t) => sum + t, 0),
71
+ totalUsd: costs.reduce((sum, c) => sum + c, 0),
72
+ medianUsd: median(costs),
73
+ p95Usd: p95(costs),
74
+ maxUsd: costs[costs.length - 1],
75
+ medianTurns: median(turns),
76
+ });
77
+ }
78
+ // The most money first — the order somebody would act in.
79
+ return out.sort((a, b) => b.totalUsd - a.totalUsd || a.label.localeCompare(b.label));
80
+ };
81
+ return { add, finish };
82
+ }
83
+ /** The same measurement over a list, for a caller holding one already. */
84
+ export function sessionCostShapes(records, options) {
85
+ const tracker = createSessionCostTracker(options);
86
+ for (const record of records)
87
+ tracker.add(record);
88
+ return tracker.finish();
89
+ }
90
+ //# sourceMappingURL=session-cost.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"session-cost.js","sourceRoot":"","sources":["../src/session-cost.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,77 @@
1
+ import type { PricingCatalogue } from './pricing.js';
2
+ import type { UsageRecord } from './usage.js';
3
+ /**
4
+ * Cache writes made by conversations that never came back.
5
+ *
6
+ * ## The waste the aggregate hides
7
+ *
8
+ * A cache write is a bet: pay 1.25x input now (2x at the 1-hour TTL) so the
9
+ * *next* call reads the prefix at 0.1x. A conversation that ends after its
10
+ * first turn never places that next call — its write bought reuse that its own
11
+ * conversation never made. On a workload with many short sessions this is a
12
+ * steady leak, and it hides inside healthy-looking totals: the long sessions'
13
+ * reads pay for the cache overall, so `cacheEconomics` reports `paid-off`
14
+ * while every one-turn drive-by pays the premium for nothing.
15
+ *
16
+ * ## The caveat that keeps the figure honest
17
+ *
18
+ * The provider's cache is keyed by prefix content, not by conversation. A
19
+ * one-turn session's write **can** be read back by a different session that
20
+ * sends the same prefix within the TTL — a shared system prompt does exactly
21
+ * that — and a usage log cannot see whose write a read hit. So the figure
22
+ * reported here is a **ceiling, named as one**: these writes paid off only if
23
+ * another conversation shared the prefix in time, and the log cannot say
24
+ * whether one did.
25
+ *
26
+ * There is one case where the ceiling collapses into a fact, and the caller
27
+ * can detect it from the slice it already has: when the slice recorded **zero
28
+ * cache reads**, nothing read those writes — within the session, across
29
+ * sessions, at all. The row deliberately does not decide this itself; the
30
+ * breakdown holding the slice's reads belongs to the caller, and deriving it
31
+ * twice is how two figures drift.
32
+ *
33
+ * Session keys group turns and never leave this module, like everywhere else
34
+ * the field is touched.
35
+ */
36
+ export interface SingleTurnCacheWrites {
37
+ label: string;
38
+ model: string;
39
+ modelName: string;
40
+ /** Conversations seen in this slice — with a session key, priced model. */
41
+ sessions: number;
42
+ /** Median turns per conversation, for scale: 1-turn sessions in a sea of 40-turn ones read differently than in a sea of 2s. */
43
+ medianTurns: number;
44
+ /** Conversations that ended after exactly one recorded turn. */
45
+ singleTurnSessions: number;
46
+ /** Cache-write tokens those one-turn conversations paid for. */
47
+ singleTurnWriteTokens: number;
48
+ /**
49
+ * What those writes cost, at the same rates the bill used — the 5-minute
50
+ * rate for writes whose TTL the log did not state, so like the bill it is
51
+ * a floor when `assumedTtlTokens` is non-zero. A **ceiling on the waste**
52
+ * (another conversation may have read the prefix; the log cannot see it)
53
+ * built on a **floor of a price** — both directions named, neither guessed.
54
+ */
55
+ singleTurnWriteUsd: number;
56
+ /** The part of `singleTurnWriteTokens` whose TTL the log did not record. */
57
+ assumedTtlTokens: number;
58
+ }
59
+ export interface SessionLedgerOptions {
60
+ catalogue: PricingCatalogue;
61
+ /** Date the prices are read at, so a promotional rate resolves the same way. */
62
+ on?: Date;
63
+ }
64
+ export interface SessionLedgerTracker {
65
+ add(record: UsageRecord): void;
66
+ finish(): SingleTurnCacheWrites[];
67
+ }
68
+ /**
69
+ * An accumulator, like the TTL-fit tracker and for the same reason: one pass
70
+ * over a log measured in megabytes, holding one small tally per conversation.
71
+ * No timestamp needed — "came back" is a fact about turn count, not the clock,
72
+ * so this measures logs the TTL-fit cannot.
73
+ */
74
+ export declare function createSessionLedgerTracker(options: SessionLedgerOptions): SessionLedgerTracker;
75
+ /** The same measurement over a list, for a caller holding one already. */
76
+ export declare function singleTurnCacheWrites(records: readonly UsageRecord[], options: SessionLedgerOptions): SingleTurnCacheWrites[];
77
+ //# sourceMappingURL=session-ledger.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"session-ledger.d.ts","sourceRoot":"","sources":["../src/session-ledger.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAC;AACrD,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAE9C;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgCG;AAEH,MAAM,WAAW,qBAAqB;IACpC,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,CAAC;IACd,SAAS,EAAE,MAAM,CAAC;IAClB,2EAA2E;IAC3E,QAAQ,EAAE,MAAM,CAAC;IACjB,+HAA+H;IAC/H,WAAW,EAAE,MAAM,CAAC;IACpB,gEAAgE;IAChE,kBAAkB,EAAE,MAAM,CAAC;IAC3B,gEAAgE;IAChE,qBAAqB,EAAE,MAAM,CAAC;IAC9B;;;;;;OAMG;IACH,kBAAkB,EAAE,MAAM,CAAC;IAC3B,4EAA4E;IAC5E,gBAAgB,EAAE,MAAM,CAAC;CAC1B;AAED,MAAM,WAAW,oBAAoB;IACnC,SAAS,EAAE,gBAAgB,CAAC;IAC5B,gFAAgF;IAChF,EAAE,CAAC,EAAE,IAAI,CAAC;CACX;AAED,MAAM,WAAW,oBAAoB;IACnC,GAAG,CAAC,MAAM,EAAE,WAAW,GAAG,IAAI,CAAC;IAC/B,MAAM,IAAI,qBAAqB,EAAE,CAAC;CACnC;AAcD;;;;;GAKG;AACH,wBAAgB,0BAA0B,CAAC,OAAO,EAAE,oBAAoB,GAAG,oBAAoB,CAsF9F;AAED,0EAA0E;AAC1E,wBAAgB,qBAAqB,CACnC,OAAO,EAAE,SAAS,WAAW,EAAE,EAC/B,OAAO,EAAE,oBAAoB,GAC5B,qBAAqB,EAAE,CAIzB"}
@@ -0,0 +1,99 @@
1
+ import { effectivePricing, multipliersFor } from './pricing.js';
2
+ import { UNLABELLED } from './usage.js';
3
+ const median = (sorted) => {
4
+ const mid = Math.floor(sorted.length / 2);
5
+ return sorted.length % 2 === 1 ? sorted[mid] : (sorted[mid - 1] + sorted[mid]) / 2;
6
+ };
7
+ /**
8
+ * An accumulator, like the TTL-fit tracker and for the same reason: one pass
9
+ * over a log measured in megabytes, holding one small tally per conversation.
10
+ * No timestamp needed — "came back" is a fact about turn count, not the clock,
11
+ * so this measures logs the TTL-fit cannot.
12
+ */
13
+ export function createSessionLedgerTracker(options) {
14
+ const { catalogue, on = new Date() } = options;
15
+ const slices = new Map();
16
+ const add = (record) => {
17
+ // An unpriced model has no rate to price the waste at, and contributes no
18
+ // dollars anywhere else either.
19
+ if (record.session === null || !catalogue.byId.has(record.model))
20
+ return;
21
+ const key = `${record.label ?? UNLABELLED}\n${record.model}`;
22
+ let sessions = slices.get(key);
23
+ if (!sessions) {
24
+ sessions = new Map();
25
+ slices.set(key, sessions);
26
+ }
27
+ let tally = sessions.get(record.session);
28
+ if (!tally) {
29
+ tally = { turns: 0, write5mTokens: 0, write1hTokens: 0, assumedTokens: 0 };
30
+ sessions.set(record.session, tally);
31
+ }
32
+ tally.turns += 1;
33
+ if (record.writeTtlKnown) {
34
+ tally.write5mTokens += record.cacheWrite5mTokens;
35
+ tally.write1hTokens += record.cacheWrite1hTokens;
36
+ }
37
+ else {
38
+ // The flat count sits in the 5m bucket by pricing convention; kept
39
+ // apart here so the row can say how much of its price is a floor.
40
+ tally.assumedTokens += record.cacheWrite5mTokens;
41
+ }
42
+ };
43
+ const finish = () => {
44
+ const out = [];
45
+ for (const [key, sessions] of slices) {
46
+ let singleTurnSessions = 0;
47
+ let write5m = 0;
48
+ let write1h = 0;
49
+ let assumed = 0;
50
+ const turnCounts = [];
51
+ for (const tally of sessions.values()) {
52
+ turnCounts.push(tally.turns);
53
+ if (tally.turns !== 1)
54
+ continue;
55
+ singleTurnSessions += 1;
56
+ write5m += tally.write5mTokens;
57
+ write1h += tally.write1hTokens;
58
+ assumed += tally.assumedTokens;
59
+ }
60
+ const singleTurnWriteTokens = write5m + write1h + assumed;
61
+ // One-turn conversations that wrote nothing wasted nothing; a row about
62
+ // them would be a finding about the absence of a finding.
63
+ if (singleTurnWriteTokens === 0)
64
+ continue;
65
+ const split = key.indexOf('\n');
66
+ const modelId = key.slice(split + 1);
67
+ const model = catalogue.byId.get(modelId);
68
+ const { inputPerMTok } = effectivePricing(model, on);
69
+ const rates = multipliersFor(model);
70
+ const per = (tokens, rate) => (tokens / 1_000_000) * inputPerMTok * rate;
71
+ // The bill's own convention: unstated TTLs at the cheaper rate, so this
72
+ // is the same floor the totals already stand on — never a new guess.
73
+ const singleTurnWriteUsd = per(write5m + assumed, rates.cacheWrite5m) + per(write1h, rates.cacheWrite1h);
74
+ turnCounts.sort((a, b) => a - b);
75
+ out.push({
76
+ label: key.slice(0, split),
77
+ model: modelId,
78
+ modelName: model.displayName,
79
+ sessions: sessions.size,
80
+ medianTurns: median(turnCounts),
81
+ singleTurnSessions,
82
+ singleTurnWriteTokens,
83
+ singleTurnWriteUsd,
84
+ assumedTtlTokens: assumed,
85
+ });
86
+ }
87
+ // The most money first — the order somebody would act in.
88
+ return out.sort((a, b) => b.singleTurnWriteUsd - a.singleTurnWriteUsd || a.label.localeCompare(b.label));
89
+ };
90
+ return { add, finish };
91
+ }
92
+ /** The same measurement over a list, for a caller holding one already. */
93
+ export function singleTurnCacheWrites(records, options) {
94
+ const tracker = createSessionLedgerTracker(options);
95
+ for (const record of records)
96
+ tracker.add(record);
97
+ return tracker.finish();
98
+ }
99
+ //# sourceMappingURL=session-ledger.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"session-ledger.js","sourceRoot":"","sources":["../src/session-ledger.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,103 @@
1
+ import type { PricingCatalogue } from './pricing.js';
2
+ import type { UsageRecord } from './usage.js';
3
+ /**
4
+ * Does the cache TTL fit how fast the turns actually arrive?
5
+ *
6
+ * ## The mechanism nothing else can see
7
+ *
8
+ * A cache entry lives 5 minutes, or an hour at twice the write price. Whether
9
+ * either is the right choice depends on one number the bill never shows: **how
10
+ * long the workload waits between turns.** A support agent whose users answer in
11
+ * nine minutes writes a 5-minute entry on every turn and reads it back on none of
12
+ * them — every write expires unread, which from the bill is indistinguishable
13
+ * from any other losing cache. `cacheEconomics` can say *that* money was lost;
14
+ * only the clock can say *why*, and the why decides the fix: the 1-hour TTL, or
15
+ * caching switched off.
16
+ *
17
+ * The opposite mistake is quieter and this is the only place it appears at all:
18
+ * turns arriving seconds apart, written at the 1-hour rate. Those writes work —
19
+ * the verdict above reads `paid-off` — and every one of them pays 2x input for
20
+ * endurance the workload never uses. **Switching them to the 5-minute TTL is the
21
+ * one exact saving in this module**: the same tokens at 1.25x instead of 2x,
22
+ * which is the same-tokens-different-rate arithmetic `cacheEconomics` already
23
+ * draws the counterfactual line at.
24
+ *
25
+ * ## What it measures, and how it stays honest
26
+ *
27
+ * The gap between consecutive turns **of the same conversation**, from the
28
+ * recorded clock — sessions are what a cache entry actually serves, and gaps
29
+ * between unrelated calls of a label say nothing about whether *this*
30
+ * conversation's next turn found the entry alive. Timestamps are sorted within
31
+ * each session before differencing, so the measurement is independent of the
32
+ * order of the log — the property the conversation tracker had to learn the
33
+ * hard way.
34
+ *
35
+ * The reported number is the **median** gap, named as such: a median survives
36
+ * the overnight gap between a user's Tuesday and Wednesday in a way a mean does
37
+ * not, and a verdict hung on a mean would flip on one lunch break.
38
+ *
39
+ * When the log did not record which TTL the writes used, the gap can sit where
40
+ * the verdict depends on the answer — over 5 minutes and under an hour survives
41
+ * one TTL and not the other. That is reported as `unsettled`, the same refusal
42
+ * `cacheEconomics` makes for the same missing field, and never resolved in the
43
+ * flattering direction.
44
+ *
45
+ * The session key is used to group turns and never leaves this module, like
46
+ * everywhere else the field is touched.
47
+ */
48
+ /** Cache-entry lifetimes, in milliseconds. Anthropic's two published TTLs. */
49
+ export declare const TTL_5M_MS: number;
50
+ export declare const TTL_1H_MS: number;
51
+ export type TtlFitVerdict =
52
+ /** The median gap outlives the entry: writes expire before the next turn. */
53
+ 'expires-before-reuse'
54
+ /** 1-hour writes on gaps inside the 5-minute window: paying 2x for nothing. */
55
+ | 'overlong-ttl'
56
+ /** The TTL the log did not record decides the verdict, so nothing does. */
57
+ | 'unsettled'
58
+ /** The entry outlives the gap at the TTL the writes actually used. */
59
+ | 'fits';
60
+ export interface CacheTtlFit {
61
+ label: string;
62
+ model: string;
63
+ modelName: string;
64
+ /** Conversations with at least two timestamped turns. Never which ones. */
65
+ sessions: number;
66
+ /** Gaps measured across them. */
67
+ gaps: number;
68
+ medianGapMs: number;
69
+ /** Write tokens the log said were 5-minute entries. */
70
+ write5mTokens: number;
71
+ /** Write tokens the log said were 1-hour entries. */
72
+ write1hTokens: number;
73
+ /** Write tokens whose TTL the log did not record. */
74
+ assumedTtlTokens: number;
75
+ verdict: TtlFitVerdict;
76
+ /**
77
+ * What the 1-hour writes would save at the 5-minute rate, when the gaps show
78
+ * the hour is never needed. Exact — the same tokens at 1.25x instead of 2x,
79
+ * at the model's own input rate — and zero for every other verdict.
80
+ */
81
+ overpayUsd: number;
82
+ }
83
+ export interface TtlFitOptions {
84
+ catalogue: PricingCatalogue;
85
+ /** Date the prices are read at, so a promotional rate resolves the same way. */
86
+ on?: Date;
87
+ }
88
+ export interface TtlFitTracker {
89
+ /** Feed one parsed record. */
90
+ add(record: UsageRecord): void;
91
+ /** The finished measurement. */
92
+ finish(): CacheTtlFit[];
93
+ }
94
+ /**
95
+ * An accumulator, like the conversation tracker and for the same reason: a
96
+ * usage log is measured in megabytes and `profileUsage` makes one pass. What
97
+ * this holds is one number per timestamped call that belongs to a session,
98
+ * which is the minimum the gaps can be computed from at all.
99
+ */
100
+ export declare function createTtlFitTracker(options: TtlFitOptions): TtlFitTracker;
101
+ /** The same measurement over a list, for a caller holding one already. */
102
+ export declare function cacheTtlFit(records: readonly UsageRecord[], options: TtlFitOptions): CacheTtlFit[];
103
+ //# sourceMappingURL=ttl-fit.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ttl-fit.d.ts","sourceRoot":"","sources":["../src/ttl-fit.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAC;AACrD,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAE9C;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4CG;AAEH,8EAA8E;AAC9E,eAAO,MAAM,SAAS,QAAgB,CAAC;AACvC,eAAO,MAAM,SAAS,QAAiB,CAAC;AAExC,MAAM,MAAM,aAAa;AACvB,6EAA6E;AAC3E,sBAAsB;AACxB,+EAA+E;GAC7E,cAAc;AAChB,2EAA2E;GACzE,WAAW;AACb,sEAAsE;GACpE,MAAM,CAAC;AAEX,MAAM,WAAW,WAAW;IAC1B,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,CAAC;IACd,SAAS,EAAE,MAAM,CAAC;IAClB,2EAA2E;IAC3E,QAAQ,EAAE,MAAM,CAAC;IACjB,iCAAiC;IACjC,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,MAAM,CAAC;IACpB,uDAAuD;IACvD,aAAa,EAAE,MAAM,CAAC;IACtB,qDAAqD;IACrD,aAAa,EAAE,MAAM,CAAC;IACtB,qDAAqD;IACrD,gBAAgB,EAAE,MAAM,CAAC;IACzB,OAAO,EAAE,aAAa,CAAC;IACvB;;;;OAIG;IACH,UAAU,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,aAAa;IAC5B,SAAS,EAAE,gBAAgB,CAAC;IAC5B,gFAAgF;IAChF,EAAE,CAAC,EAAE,IAAI,CAAC;CACX;AAED,MAAM,WAAW,aAAa;IAC5B,8BAA8B;IAC9B,GAAG,CAAC,MAAM,EAAE,WAAW,GAAG,IAAI,CAAC;IAC/B,gCAAgC;IAChC,MAAM,IAAI,WAAW,EAAE,CAAC;CACzB;AAeD;;;;;GAKG;AACH,wBAAgB,mBAAmB,CAAC,OAAO,EAAE,aAAa,GAAG,aAAa,CAuHzE;AAED,0EAA0E;AAC1E,wBAAgB,WAAW,CACzB,OAAO,EAAE,SAAS,WAAW,EAAE,EAC/B,OAAO,EAAE,aAAa,GACrB,WAAW,EAAE,CAIf"}