@trazum/core 2.4.2 → 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 +36 -4
  54. package/dist/pricing.d.ts.map +1 -1
  55. package/dist/pricing.js +804 -91
  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 +51 -0
  88. package/dist/types.d.ts.map +1 -1
  89. package/dist/usage.d.ts +36 -1
  90. package/dist/usage.d.ts.map +1 -1
  91. package/dist/usage.js +91 -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 +854 -96
  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 +46 -0
  121. package/src/usage.ts +129 -11
package/src/gateway.ts CHANGED
@@ -216,7 +216,8 @@ function priceCall(
216
216
  on: Date,
217
217
  ): number | null {
218
218
  if (model === undefined || call.inputTokens === null) return null;
219
- const rates = effectivePricing(model, on);
219
+ // The call's own prompt decides a model priced by prompt length.
220
+ const rates = effectivePricing(model, on, { inputTokens: call.inputTokens });
220
221
  const output = call.maxOutputTokens ?? 0;
221
222
  return (call.inputTokens / 1_000_000) * rates.inputPerMTok + (output / 1_000_000) * rates.outputPerMTok;
222
223
  }
@@ -236,11 +237,12 @@ function alternativesFor(
236
237
  const mine = priceCall(model, call, on);
237
238
  if (mine === null) return [];
238
239
  const out: GatewayAlternative[] = [];
239
- const here = effectivePricing(model, on);
240
+ const size = call.inputTokens === null ? {} : { inputTokens: call.inputTokens };
241
+ const here = effectivePricing(model, on, size);
240
242
 
241
243
  for (const candidate of catalogue.byId.values()) {
242
244
  if (candidate.id === model.id || candidate.provider !== model.provider) continue;
243
- const there = effectivePricing(candidate, on);
245
+ const there = effectivePricing(candidate, on, size);
244
246
  if (there.inputPerMTok >= here.inputPerMTok) continue;
245
247
  if (call.inputTokens !== null && candidate.contextWindow < call.inputTokens) continue;
246
248
  const routed = priceCall(candidate, call, on);
package/src/guard.ts CHANGED
@@ -91,18 +91,20 @@ function cheaperThan(
91
91
  inputTokens: number,
92
92
  on: Date,
93
93
  ): ModelPricing[] {
94
- const here = effectivePricing(model, on);
94
+ // At this call's prompt, so a model priced by prompt length is ranked at its real band.
95
+ const size = { inputTokens };
96
+ const here = effectivePricing(model, on, size);
95
97
  return [...catalogue.byId.values()]
96
98
  .filter((candidate) => {
97
99
  if (candidate.id === model.id) return false;
98
100
  if (candidate.provider !== model.provider) return false;
99
- const there = effectivePricing(candidate, on);
101
+ const there = effectivePricing(candidate, on, size);
100
102
  if (there.inputPerMTok >= here.inputPerMTok) return false;
101
103
  // A model the prompt does not fit in is not a cheaper way to make this
102
104
  // call; it is a way not to make it.
103
105
  return candidate.contextWindow >= inputTokens;
104
106
  })
105
- .sort((a, b) => effectivePricing(b, on).inputPerMTok - effectivePricing(a, on).inputPerMTok);
107
+ .sort((a, b) => effectivePricing(b, on, size).inputPerMTok - effectivePricing(a, on, size).inputPerMTok);
106
108
  }
107
109
 
108
110
  export interface GuardRequest extends AnswerRequest {
@@ -133,9 +135,9 @@ export function guardSpend(
133
135
  if (model !== undefined && cost.call !== null) {
134
136
  const inputTokens = cost.call.inputTokens;
135
137
  const outputTokens = cost.call.outputTokens;
136
- const here = effectivePricing(model, on);
138
+ const here = effectivePricing(model, on, { inputTokens });
137
139
  const priceOf = (candidate: ModelPricing): number => {
138
- const rates = effectivePricing(candidate, on);
140
+ const rates = effectivePricing(candidate, on, { inputTokens });
139
141
  return (inputTokens / 1_000_000) * rates.inputPerMTok + (outputTokens / 1_000_000) * rates.outputPerMTok;
140
142
  };
141
143
  const mine = (inputTokens / 1_000_000) * here.inputPerMTok + (outputTokens / 1_000_000) * here.outputPerMTok;
package/src/index.ts CHANGED
@@ -435,7 +435,7 @@ export {
435
435
  multipliersFor,
436
436
  isOffered,
437
437
  } from './pricing.js';
438
- export type { PricingCatalogue } from './pricing.js';
438
+ export type { PricingCatalogue, PromptSize } from './pricing.js';
439
439
  export type { CachingMode, Capability, CostMultipliers } from './types.js';
440
440
 
441
441
  // Local price corrections, so a price change needs no library upgrade.
@@ -1,4 +1,5 @@
1
1
  import { effectivePricing, multipliersFor } from './pricing.js';
2
+ import { billedAt, promptSizeOf } from './billing.js';
2
3
  import { UNLABELLED } from './usage.js';
3
4
  import type { PricingCatalogue } from './pricing.js';
4
5
  import type { UsageRecord } from './usage.js';
@@ -190,8 +191,9 @@ export function createInputShapeTracker(options: InputShapeOptions): InputShapeT
190
191
  slices.set(key, slice);
191
192
  }
192
193
 
193
- const { inputPerMTok } = effectivePricing(model, on);
194
- const rates = multipliersFor(model);
194
+ const at = billedAt(record, on);
195
+ const { inputPerMTok } = effectivePricing(model, at, promptSizeOf(record));
196
+ const rates = multipliersFor(model, at);
195
197
  const per = (count: number, rate: number): number => (count / 1_000_000) * rate;
196
198
 
197
199
  slice.calls += 1;
package/src/ladder.ts CHANGED
@@ -151,7 +151,8 @@ export function ladderArithmetic(
151
151
  if (id === undefined) return null;
152
152
  const model = catalogue.byId.get(id);
153
153
  if (model === undefined) return null;
154
- const rates = effectivePricing(model, on);
154
+ // The shape's own prompt decides a model priced by prompt length.
155
+ const rates = effectivePricing(model, on, { inputTokens: shape.inputTokens });
155
156
  return (
156
157
  (shape.inputTokens / 1_000_000) * rates.inputPerMTok +
157
158
  (shape.outputTokens / 1_000_000) * rates.outputPerMTok
package/src/levers.ts CHANGED
@@ -150,8 +150,11 @@ function repriceAt(
150
150
  model: ModelPricing,
151
151
  on: Date,
152
152
  ): { inputUsd: number; outputUsd: number; cacheUsd: number; totalUsd: number } {
153
- const { inputPerMTok, outputPerMTok } = effectivePricing(model, on);
154
- const rates = multipliersFor(model);
153
+ // The breakdown's largest call bounds every prompt in it: see `PromptSize`.
154
+ const { inputPerMTok, outputPerMTok } = effectivePricing(model, on, {
155
+ inputTokensAtMost: breakdown.maxCallInputTokens,
156
+ });
157
+ const rates = multipliersFor(model, on);
155
158
  const per = (tokens: number, rate: number): number => (tokens / 1_000_000) * rate;
156
159
 
157
160
  const inputUsd = per(breakdown.inputTokens, inputPerMTok);
@@ -95,10 +95,21 @@ export interface OpenaiUsageRecord {
95
95
  model: string;
96
96
  ts: string;
97
97
  label?: string;
98
+ /**
99
+ * `num_model_requests` when the bucket sums more than one, `null` when the
100
+ * row does not carry a usable count, and absent when it is exactly one
101
+ * request, which is then a call like any other. See `UsageRecord.requests`.
102
+ */
103
+ requests?: number | null;
98
104
  usage: {
99
105
  prompt_tokens: number;
100
106
  completion_tokens: number;
101
- prompt_tokens_details?: { cached_tokens: number };
107
+ /**
108
+ * Both inside `prompt_tokens`, as OpenAI counts them, and both taken out of
109
+ * it by `parseUsageLine`. `cache_write_tokens` is the 30-minute write
110
+ * GPT-5.6 and later bill at 1.25x input.
111
+ */
112
+ prompt_tokens_details?: { cached_tokens: number; cache_write_tokens?: number };
102
113
  };
103
114
  }
104
115
 
@@ -128,6 +139,12 @@ export interface OpenaiUsageConversion {
128
139
  nonTextTokens: number;
129
140
  /** Cache-write tokens on mixed rows, left out because the schema gives them no modality. */
130
141
  cacheWriteUnplaced: number;
142
+ /**
143
+ * Tokens written to the 12-hour cache. No price for that write is published
144
+ * anywhere this table can read, so they stay in the prompt total and are
145
+ * priced as plain input, a floor, and named rather than guessed at.
146
+ */
147
+ cacheWrite12hTokens: number;
131
148
  /** Mixed rows the report gave no text split for, refused whole. */
132
149
  unsplitRows: number;
133
150
  /** `has_more`: this is one page and the bill from it is understated. */
@@ -182,6 +199,7 @@ export function openaiUsageRecords(
182
199
  mixedRows: 0,
183
200
  nonTextTokens: 0,
184
201
  cacheWriteUnplaced: 0,
202
+ cacheWrite12hTokens: 0,
185
203
  unsplitRows: 0,
186
204
  truncated: false,
187
205
  labelledByProject: 0,
@@ -213,6 +231,7 @@ export function openaiUsageRecords(
213
231
  let mixedRows = 0;
214
232
  let nonTextTokens = 0;
215
233
  let cacheWriteUnplaced = 0;
234
+ let cacheWrite12hTokens = 0;
216
235
  let unsplitRows = 0;
217
236
  let labelledByProject = 0;
218
237
  let unruledProject = 0;
@@ -279,10 +298,18 @@ export function openaiUsageRecords(
279
298
  const nonText = NON_TEXT.reduce((sum, field) => sum + count(result[field]), 0);
280
299
  let prompt: number;
281
300
  let cached: number;
301
+ let written = 0;
282
302
  let completion: number;
283
303
  if (nonText === 0) {
284
304
  prompt = count(result.input_tokens);
285
305
  cached = count(result.input_cached_tokens);
306
+ /*
307
+ `input_tokens` is uncached + cached + both writes, by the schema's own
308
+ example. The 30-minute write has a price and goes where the parser
309
+ prices it; the 12-hour write has none here and stays in the total.
310
+ */
311
+ written = count(result.input_cache_write_tokens);
312
+ cacheWrite12hTokens += count(result.input_cache_write_12h_tokens);
286
313
  completion = count(result.output_tokens);
287
314
  } else {
288
315
  const split =
@@ -306,10 +333,18 @@ export function openaiUsageRecords(
306
333
  model: result.model,
307
334
  ts,
308
335
  ...(label === undefined ? {} : { label }),
336
+ ...requestsField(result.num_model_requests),
309
337
  usage: {
310
338
  prompt_tokens: prompt,
311
339
  completion_tokens: completion,
312
- ...(cached > 0 ? { prompt_tokens_details: { cached_tokens: cached } } : {}),
340
+ ...(cached > 0 || written > 0
341
+ ? {
342
+ prompt_tokens_details: {
343
+ cached_tokens: cached,
344
+ ...(written > 0 ? { cache_write_tokens: written } : {}),
345
+ },
346
+ }
347
+ : {}),
313
348
  },
314
349
  });
315
350
  rows += 1;
@@ -330,6 +365,7 @@ export function openaiUsageRecords(
330
365
  mixedRows,
331
366
  nonTextTokens,
332
367
  cacheWriteUnplaced,
368
+ cacheWrite12hTokens,
333
369
  unsplitRows,
334
370
  truncated: report.has_more === true,
335
371
  labelledByProject,
@@ -351,3 +387,9 @@ export function looksLikeOpenaiUsage(text: string, prefixBytes = 8192): boolean
351
387
  if (head.includes('"organization.usage.completions.result"')) return true;
352
388
  return head.includes('"num_model_requests"') && head.includes('"start_time"');
353
389
  }
390
+
391
+ /** A bucket's request count as `UsageRecord.requests` reads it: one is a call. */
392
+ function requestsField(value: unknown): { requests?: number | null } {
393
+ if (typeof value !== 'number' || !Number.isInteger(value) || value < 1) return { requests: null };
394
+ return value === 1 ? {} : { requests: value };
395
+ }
@@ -63,6 +63,12 @@ export interface OpenrouterActivityRecord {
63
63
  model: string;
64
64
  ts: string;
65
65
  label?: string;
66
+ /**
67
+ * The day's `requests` when it is more than one, `null` when the row does
68
+ * not carry a usable count, absent when it is exactly one request. See
69
+ * `UsageRecord.requests`.
70
+ */
71
+ requests?: number | null;
66
72
  usage: { prompt_tokens: number; completion_tokens: number };
67
73
  }
68
74
 
@@ -186,6 +192,7 @@ export function openrouterActivityRecords(
186
192
  model: row.model,
187
193
  ts: `${row.date}T00:00:00.000Z`,
188
194
  ...(label === undefined ? {} : { label }),
195
+ ...requestsField(row.requests),
189
196
  usage: {
190
197
  prompt_tokens: count(row.prompt_tokens),
191
198
  completion_tokens: count(row.completion_tokens),
@@ -219,3 +226,9 @@ export function looksLikeOpenrouterActivity(text: string, prefixBytes = 8192): b
219
226
  const head = text.slice(0, prefixBytes);
220
227
  return head.includes('"model_permaslug"') || head.includes('"byok_usage_inference"');
221
228
  }
229
+
230
+ /** A day's request count as `UsageRecord.requests` reads it: one is a call. */
231
+ function requestsField(value: unknown): { requests?: number | null } {
232
+ if (typeof value !== 'number' || !Number.isInteger(value) || value < 1) return { requests: null };
233
+ return value === 1 ? {} : { requests: value };
234
+ }
@@ -1,4 +1,5 @@
1
1
  import { effectivePricing } from './pricing.js';
2
+ import { billedAt, promptSizeOf } from './billing.js';
2
3
  import { UNLABELLED } from './usage.js';
3
4
  import type { PricingCatalogue } from './pricing.js';
4
5
  import type { UsageRecord } from './usage.js';
@@ -172,7 +173,7 @@ export function createOutputShapeTracker(options: OutputShapeOptions): OutputSha
172
173
  slices.set(key, slice);
173
174
  }
174
175
 
175
- const { outputPerMTok } = effectivePricing(model, on);
176
+ const { outputPerMTok } = effectivePricing(model, billedAt(record, on), promptSizeOf(record));
176
177
  slice.calls += 1;
177
178
  slice.outputTokens += record.outputTokens;
178
179
  slice.outputUsd += (record.outputTokens / 1_000_000) * outputPerMTok;