@trazum/core 2.4.3 → 2.5.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 (121) hide show
  1. package/dist/advisories.d.ts.map +1 -1
  2. package/dist/advisories.js +17 -10
  3. package/dist/advisories.js.map +1 -1
  4. package/dist/answer.d.ts.map +1 -1
  5. package/dist/answer.js +2 -1
  6. package/dist/answer.js.map +1 -1
  7. package/dist/anthropic-usage.d.ts +6 -0
  8. package/dist/anthropic-usage.d.ts.map +1 -1
  9. package/dist/anthropic-usage.js +1 -0
  10. package/dist/anthropic-usage.js.map +1 -1
  11. package/dist/billing.d.ts +20 -0
  12. package/dist/billing.d.ts.map +1 -0
  13. package/dist/billing.js +32 -0
  14. package/dist/billing.js.map +1 -0
  15. package/dist/budget.d.ts.map +1 -1
  16. package/dist/budget.js +18 -8
  17. package/dist/budget.js.map +1 -1
  18. package/dist/connector.d.ts +20 -1
  19. package/dist/connector.d.ts.map +1 -1
  20. package/dist/connector.js +42 -11
  21. package/dist/connector.js.map +1 -1
  22. package/dist/conversation.d.ts.map +1 -1
  23. package/dist/conversation.js +4 -2
  24. package/dist/conversation.js.map +1 -1
  25. package/dist/gateway.d.ts.map +1 -1
  26. package/dist/gateway.js +5 -3
  27. package/dist/gateway.js.map +1 -1
  28. package/dist/guard.d.ts.map +1 -1
  29. package/dist/guard.js +7 -5
  30. package/dist/guard.js.map +1 -1
  31. package/dist/index.d.ts +1 -1
  32. package/dist/index.d.ts.map +1 -1
  33. package/dist/input-shape.d.ts.map +1 -1
  34. package/dist/input-shape.js +4 -2
  35. package/dist/input-shape.js.map +1 -1
  36. package/dist/ladder.d.ts.map +1 -1
  37. package/dist/ladder.js +2 -1
  38. package/dist/ladder.js.map +1 -1
  39. package/dist/levers.d.ts.map +1 -1
  40. package/dist/levers.js +5 -2
  41. package/dist/levers.js.map +1 -1
  42. package/dist/openai-usage.d.ts +18 -0
  43. package/dist/openai-usage.d.ts.map +1 -1
  44. package/dist/openai-usage.js +26 -1
  45. package/dist/openai-usage.js.map +1 -1
  46. package/dist/openrouter-activity.d.ts +6 -0
  47. package/dist/openrouter-activity.d.ts.map +1 -1
  48. package/dist/openrouter-activity.js +7 -0
  49. package/dist/openrouter-activity.js.map +1 -1
  50. package/dist/output-shape.d.ts.map +1 -1
  51. package/dist/output-shape.js +2 -1
  52. package/dist/output-shape.js.map +1 -1
  53. package/dist/pricing.d.ts +35 -3
  54. package/dist/pricing.d.ts.map +1 -1
  55. package/dist/pricing.js +569 -54
  56. package/dist/pricing.js.map +1 -1
  57. package/dist/receipt.d.ts +17 -0
  58. package/dist/receipt.d.ts.map +1 -1
  59. package/dist/receipt.js +10 -0
  60. package/dist/receipt.js.map +1 -1
  61. package/dist/repeats.d.ts.map +1 -1
  62. package/dist/repeats.js +4 -2
  63. package/dist/repeats.js.map +1 -1
  64. package/dist/reprice.d.ts +18 -2
  65. package/dist/reprice.d.ts.map +1 -1
  66. package/dist/reprice.js +34 -12
  67. package/dist/reprice.js.map +1 -1
  68. package/dist/rollup.d.ts.map +1 -1
  69. package/dist/rollup.js +3 -0
  70. package/dist/rollup.js.map +1 -1
  71. package/dist/savings.d.ts.map +1 -1
  72. package/dist/savings.js +10 -3
  73. package/dist/savings.js.map +1 -1
  74. package/dist/session-cost.d.ts.map +1 -1
  75. package/dist/session-cost.js +5 -2
  76. package/dist/session-cost.js.map +1 -1
  77. package/dist/session-ledger.d.ts.map +1 -1
  78. package/dist/session-ledger.js +12 -7
  79. package/dist/session-ledger.js.map +1 -1
  80. package/dist/truncation-retry.d.ts.map +1 -1
  81. package/dist/truncation-retry.js +4 -2
  82. package/dist/truncation-retry.js.map +1 -1
  83. package/dist/ttl-fit.d.ts +8 -0
  84. package/dist/ttl-fit.d.ts.map +1 -1
  85. package/dist/ttl-fit.js +25 -14
  86. package/dist/ttl-fit.js.map +1 -1
  87. package/dist/types.d.ts +24 -0
  88. package/dist/types.d.ts.map +1 -1
  89. package/dist/usage.d.ts +31 -0
  90. package/dist/usage.d.ts.map +1 -1
  91. package/dist/usage.js +79 -10
  92. package/dist/usage.js.map +1 -1
  93. package/package.json +1 -1
  94. package/src/advisories.ts +18 -10
  95. package/src/answer.ts +6 -1
  96. package/src/anthropic-usage.ts +7 -0
  97. package/src/billing.ts +37 -0
  98. package/src/budget.ts +18 -8
  99. package/src/connector.ts +58 -10
  100. package/src/conversation.ts +4 -2
  101. package/src/gateway.ts +5 -3
  102. package/src/guard.ts +7 -5
  103. package/src/index.ts +1 -1
  104. package/src/input-shape.ts +4 -2
  105. package/src/ladder.ts +2 -1
  106. package/src/levers.ts +5 -2
  107. package/src/openai-usage.ts +44 -2
  108. package/src/openrouter-activity.ts +13 -0
  109. package/src/output-shape.ts +2 -1
  110. package/src/pricing.ts +614 -59
  111. package/src/receipt.ts +23 -1
  112. package/src/repeats.ts +4 -2
  113. package/src/reprice.ts +57 -16
  114. package/src/rollup.ts +3 -0
  115. package/src/savings.ts +12 -5
  116. package/src/session-cost.ts +5 -2
  117. package/src/session-ledger.ts +18 -9
  118. package/src/truncation-retry.ts +4 -2
  119. package/src/ttl-fit.ts +39 -15
  120. package/src/types.ts +18 -0
  121. package/src/usage.ts +112 -10
package/src/usage.ts CHANGED
@@ -1,4 +1,5 @@
1
- import { effectivePricing, multipliersFor } from './pricing.js';
1
+ import { cacheTtlOf, effectivePricing, multipliersFor } from './pricing.js';
2
+ import { billedAt, promptSizeOf } from './billing.js';
2
3
  import { createConversationTracker } from './conversation.js';
3
4
  import { createOutputShapeTracker } from './output-shape.js';
4
5
  import { createInputShapeTracker } from './input-shape.js';
@@ -159,6 +160,22 @@ export interface UsageRecord {
159
160
  * must not congratulate a log that never measured.
160
161
  */
161
162
  truncated: boolean | null;
163
+ /**
164
+ * How many requests this record sums, when it sums more than one.
165
+ *
166
+ * Absent on a record that is one call, which is every record a per-call log
167
+ * holds. A number above one, or `null` for "a sum, count not given", is a
168
+ * row out of a usage report: an Anthropic or OpenAI bucket, an OpenRouter
169
+ * day. It matters to a model priced by prompt length, because a request is
170
+ * priced on its own prompt and a row of many requests does not say any one
171
+ * of them. Such a row is priced at the dearer rate unless its whole prompt
172
+ * total is at or under the line, and the report says so.
173
+ *
174
+ * Read from `requests` when it is a positive integer or `null`; anything
175
+ * else is not this field, because a log that already uses the name for
176
+ * something of its own should not lose its records to it.
177
+ */
178
+ requests?: number | null;
162
179
  }
163
180
 
164
181
  /** What a set of calls cost, split by where the money went. */
@@ -239,6 +256,21 @@ export interface UsageBreakdown {
239
256
  truncatedOutputUsd: number;
240
257
  /** Calls that recorded a stop reason at all, truncated or not. */
241
258
  stopReasonCalls: number;
259
+ /**
260
+ * Records billed at the dearer of two rates because which one applied could
261
+ * not be decided, and what they cost each way.
262
+ *
263
+ * A model priced by prompt length bills each request on its own prompt, and
264
+ * a row that sums requests does not say any one prompt's length. Those rows
265
+ * are billed at the dearer rate, so `totalUsd` is a ceiling on them and never
266
+ * a figure that flatters; `tierCeilingUsd` is what they contribute to it and
267
+ * `tierFloorUsd` what the same tokens come to at the cheaper rate. The true
268
+ * bill for them is between the two, and a report that printed only the
269
+ * ceiling would be right and useless.
270
+ */
271
+ tierCeilingCalls: number;
272
+ tierCeilingUsd: number;
273
+ tierFloorUsd: number;
242
274
  }
243
275
 
244
276
  /**
@@ -597,6 +629,9 @@ const EMPTY = (): UsageBreakdown => ({
597
629
  truncatedCalls: 0,
598
630
  truncatedOutputUsd: 0,
599
631
  stopReasonCalls: 0,
632
+ tierCeilingCalls: 0,
633
+ tierCeilingUsd: 0,
634
+ tierFloorUsd: 0,
600
635
  });
601
636
 
602
637
  /**
@@ -713,12 +748,24 @@ export function parseUsageLine(line: string): UsageRecord | null {
713
748
  * in `prompt_tokens`**, while Anthropic reports them separately and does not.
714
749
  * Subtracting in one case and not the other is the difference between a correct
715
750
  * bill and one that charges the cached half twice.
751
+ *
752
+ * **And the Responses API spells it `input_tokens_details`**, under an
753
+ * `input_tokens` that likewise includes the cached tokens. Only the Chat
754
+ * Completions spelling was read, so a Responses log, OpenAI's default API,
755
+ * billed every cached token at the full input rate: a call that read 90,000
756
+ * of its 100,000 tokens from the cache came out four times its price, and the
757
+ * cache verdict said caching was never used.
758
+ *
759
+ * GPT-5.6 and later also report `cache_write_tokens` in the same object,
760
+ * inside the same total, and bill them at 1.25x input. They are subtracted
761
+ * with the cached tokens and land in the write bucket, whose TTL the model
762
+ * states (`cacheTtl`): one kind of entry, so nothing about it is assumed.
716
763
  */
764
+ const detailsRaw = usage.prompt_tokens_details ?? usage.input_tokens_details;
717
765
  const details =
718
- typeof usage.prompt_tokens_details === 'object' && usage.prompt_tokens_details !== null
719
- ? (usage.prompt_tokens_details as Record<string, unknown>)
720
- : null;
766
+ typeof detailsRaw === 'object' && detailsRaw !== null ? (detailsRaw as Record<string, unknown>) : null;
721
767
  const openAiCached = details ? readCount(details.cached_tokens) : ({ kind: 'absent' } as Count);
768
+ const openAiWrite = details ? readCount(details.cache_write_tokens) : ({ kind: 'absent' } as Count);
722
769
 
723
770
  /**
724
771
  * Anthropic splits cache writes by time-to-live, and the two cost different
@@ -769,6 +816,7 @@ export function parseUsageLine(line: string): UsageRecord | null {
769
816
  cacheRead: readCount(usage.cache_read_input_tokens, usage.cacheReadTokens),
770
817
  cacheWrite: readCount(usage.cache_creation_input_tokens, usage.cacheWriteTokens),
771
818
  openAiCached,
819
+ openAiWrite,
772
820
  geminiCached,
773
821
  write5m,
774
822
  write1h,
@@ -791,6 +839,8 @@ export function parseUsageLine(line: string): UsageRecord | null {
791
839
  // Both providers that fold cached tokens into the prompt count, one
792
840
  // subtraction. They cannot coexist on a record: the shapes share no field.
793
841
  const cached = valueOf(counts.openAiCached!) + valueOf(counts.geminiCached!);
842
+ // Inside the OpenAI total like the cached tokens, and taken out of it the same way.
843
+ const openAiWritten = valueOf(counts.openAiWrite!);
794
844
  const flatWrite = valueOf(counts.cacheWrite!);
795
845
  const split5m = valueOf(counts.write5m!);
796
846
  const split1h = valueOf(counts.write1h!);
@@ -798,16 +848,17 @@ export function parseUsageLine(line: string): UsageRecord | null {
798
848
 
799
849
  return {
800
850
  model,
801
- inputTokens: Math.max(0, valueOf(counts.input!) - cached),
851
+ inputTokens: Math.max(0, valueOf(counts.input!) - cached - openAiWritten),
802
852
  cacheReadTokens: counts.cacheRead!.kind === 'ok' ? counts.cacheRead!.value : cached,
803
853
  /**
804
854
  * The split when the log carries it, the flat number otherwise — and
805
855
  * `writeTtlKnown` says which, so the report can admit that a rate was assumed
806
856
  * rather than quietly choosing the cheaper one.
807
857
  */
808
- cacheWrite5mTokens: hasSplit ? split5m : flatWrite,
858
+ cacheWrite5mTokens: (hasSplit ? split5m : flatWrite) + openAiWritten,
809
859
  cacheWrite1hTokens: hasSplit ? split1h : 0,
810
- writeTtlKnown: hasSplit || flatWrite === 0,
860
+ // `cache_write_tokens` names no TTL either; see the note in `profileUsage`.
861
+ writeTtlKnown: hasSplit || (flatWrite === 0 && openAiWritten === 0),
811
862
  outputTokens: valueOf(counts.output!),
812
863
  /**
813
864
  * Trimmed and internally normalised: any whitespace run becomes one space.
@@ -846,9 +897,22 @@ export function parseUsageLine(line: string): UsageRecord | null {
846
897
  if (typeof reason !== 'string') return null;
847
898
  return reason === 'max_tokens' || reason === 'length' || reason === 'MAX_TOKENS';
848
899
  })(),
900
+ ...requestsOf(record.requests),
849
901
  };
850
902
  }
851
903
 
904
+ /**
905
+ * `requests`, when it is this field: a positive integer or `null`.
906
+ *
907
+ * One is the same as absent, so it is left absent and a per-call record stays
908
+ * the shape it always was.
909
+ */
910
+ function requestsOf(value: unknown): { requests?: number | null } {
911
+ if (value === null) return { requests: null };
912
+ if (typeof value === 'number' && Number.isInteger(value) && value > 1) return { requests: value };
913
+ return {};
914
+ }
915
+
852
916
  /**
853
917
  * One slice's outcome tally, in the same shape as the whole log's.
854
918
  *
@@ -954,11 +1018,30 @@ function add(into: UsageBreakdown, record: UsageRecord, catalogue: PricingCatalo
954
1018
  * August log read in October lost its August promotion. `on` is the date for
955
1019
  * a record that carries no clock, which is the only thing it can mean here.
956
1020
  */
957
- const at = record.ts !== null ? new Date(record.ts) : on;
958
- const { inputPerMTok, outputPerMTok } = effectivePricing(model, at);
1021
+ const at = billedAt(record, on);
1022
+ /**
1023
+ * **Each request on its own prompt.** A model priced by prompt length counts
1024
+ * every input token a request carried, cache reads and writes included, and
1025
+ * a record that is one call says exactly that. A record that sums requests
1026
+ * gives only a bound: no request in it was larger than the whole row.
1027
+ */
1028
+ const priced = effectivePricing(model, at, promptSizeOf(record));
1029
+ const { inputPerMTok, outputPerMTok } = priced;
959
1030
  // Dated, like the rate above: a repriced cache read is part of the reprice.
960
1031
  const rates = multipliersFor(model, at);
961
1032
  const per = (tokens: number, rate: number): number => (tokens / 1_000_000) * rate;
1033
+ const recordUsd = (input: number, output: number): number =>
1034
+ per(record.inputTokens, input) +
1035
+ per(record.cacheReadTokens, input * rates.cacheRead) +
1036
+ per(record.cacheWrite5mTokens, input * rates.cacheWrite5m) +
1037
+ per(record.cacheWrite1hTokens, input * rates.cacheWrite1h) +
1038
+ per(record.outputTokens, output);
1039
+ const floor = priced.tier?.decided === false ? priced.tier.floor : undefined;
1040
+ if (floor !== undefined) {
1041
+ into.tierCeilingCalls += 1;
1042
+ into.tierCeilingUsd += recordUsd(inputPerMTok, outputPerMTok);
1043
+ into.tierFloorUsd += recordUsd(floor.inputPerMTok, floor.outputPerMTok);
1044
+ }
962
1045
 
963
1046
  into.inputUsd += per(record.inputTokens, inputPerMTok);
964
1047
  into.cacheReadUsd += per(record.cacheReadTokens, inputPerMTok * rates.cacheRead);
@@ -1105,11 +1188,30 @@ export function profileUsage(text: string, options: UsageProfileOptions): UsageP
1105
1188
  const line = lines[i]!.trim();
1106
1189
  if (line === '') continue;
1107
1190
 
1108
- const record = parseUsageLine(line);
1191
+ let record = parseUsageLine(line);
1109
1192
  if (!record) {
1110
1193
  skippedLines.push(i + 1);
1111
1194
  continue;
1112
1195
  }
1196
+ /**
1197
+ * A write whose TTL the line did not state is an assumption only where
1198
+ * there was a choice. `cache_write_tokens` carries no TTL: on Kimi K3,
1199
+ * which sells a 5-minute and a 1-hour entry, the cheaper is assumed and
1200
+ * the report says so; on OpenAI's GPT-5.6 and later, which sell one, and
1201
+ * on its earlier models, whose writes cost what input costs whatever they
1202
+ * last, there is nothing to assume and nothing is said.
1203
+ */
1204
+ if (!record.writeTtlKnown) {
1205
+ const priced = catalogue.byId.get(record.model);
1206
+ // One kind of entry, or two at one price: an assumption that cannot move the bill is not one.
1207
+ if (
1208
+ priced !== undefined
1209
+ && (cacheTtlOf(priced).longerMs === null
1210
+ || multipliersFor(priced).cacheWrite1h === multipliersFor(priced).cacheWrite5m)
1211
+ ) {
1212
+ record = { ...record, writeTtlKnown: true };
1213
+ }
1214
+ }
1113
1215
 
1114
1216
  // The drill-down: after the skip accounting, so a corrupt line is reported
1115
1217
  // whatever it might have been labelled.