@trazum/core 2.2.1 → 2.4.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 (41) hide show
  1. package/dist/anthropic-cost.d.ts +173 -0
  2. package/dist/anthropic-cost.d.ts.map +1 -0
  3. package/dist/anthropic-cost.js +222 -0
  4. package/dist/anthropic-cost.js.map +1 -0
  5. package/dist/anthropic-usage.d.ts +192 -0
  6. package/dist/anthropic-usage.d.ts.map +1 -0
  7. package/dist/anthropic-usage.js +260 -0
  8. package/dist/anthropic-usage.js.map +1 -0
  9. package/dist/claude-code.d.ts +29 -1
  10. package/dist/claude-code.d.ts.map +1 -1
  11. package/dist/claude-code.js +33 -1
  12. package/dist/claude-code.js.map +1 -1
  13. package/dist/index.d.ts +12 -2
  14. package/dist/index.d.ts.map +1 -1
  15. package/dist/index.js +6 -1
  16. package/dist/index.js.map +1 -1
  17. package/dist/openai-cost.d.ts +62 -0
  18. package/dist/openai-cost.d.ts.map +1 -0
  19. package/dist/openai-cost.js +151 -0
  20. package/dist/openai-cost.js.map +1 -0
  21. package/dist/openai-usage.d.ts +159 -0
  22. package/dist/openai-usage.d.ts.map +1 -0
  23. package/dist/openai-usage.js +281 -0
  24. package/dist/openai-usage.js.map +1 -0
  25. package/dist/openrouter-activity.d.ts +103 -0
  26. package/dist/openrouter-activity.d.ts.map +1 -0
  27. package/dist/openrouter-activity.js +169 -0
  28. package/dist/openrouter-activity.js.map +1 -0
  29. package/dist/pricing.d.ts +44 -0
  30. package/dist/pricing.d.ts.map +1 -1
  31. package/dist/pricing.js +88 -3
  32. package/dist/pricing.js.map +1 -1
  33. package/package.json +2 -2
  34. package/src/anthropic-cost.ts +303 -0
  35. package/src/anthropic-usage.ts +363 -0
  36. package/src/claude-code.ts +62 -2
  37. package/src/index.ts +20 -1
  38. package/src/openai-cost.ts +170 -0
  39. package/src/openai-usage.ts +353 -0
  40. package/src/openrouter-activity.ts +221 -0
  41. package/src/pricing.ts +91 -3
@@ -0,0 +1,221 @@
1
+ /**
2
+ * OpenRouter's activity report, read as a usage log.
3
+ *
4
+ * The eighth converter, and the third that reads a provider rather than a
5
+ * tool in front of one. OpenRouter is the one provider Trazum already has a
6
+ * price list for by construction: `openrouter.ts` turns its public model
7
+ * catalogue into a pricing overlay, keyed by the same slugs this report
8
+ * carries. So a row here is priceable the moment the overlay is loaded, for
9
+ * hundreds of models the bundled table never typed.
10
+ *
11
+ * ## Derived from the published schema
12
+ *
13
+ * `GET /api/v1/activity` needs a **management key** and answers the last
14
+ * thirty completed UTC days, one row per model per endpoint per day:
15
+ * `date` (`YYYY-MM-DD`), `model` (slug), `model_permaslug` (slug with
16
+ * version), `endpoint_id`, `provider_name`, `usage` (cost in USD),
17
+ * `byok_usage_inference` (BYOK cost in USD), `requests`, `prompt_tokens`,
18
+ * `completion_tokens`, `reasoning_tokens`, and `workspace_id` when the
19
+ * request said `group_by=workspace`. The schema's own example is the
20
+ * fixture.
21
+ *
22
+ * It takes the answer and never the key, for the reason `anthropic-usage.ts`
23
+ * gives: a management key manages the account, and this project holds no
24
+ * provider credential.
25
+ *
26
+ * ## Two figures that are never added
27
+ *
28
+ * `usage` is what OpenRouter charged, in dollars, and it is carried out of
29
+ * this conversion **beside** Trazum's catalogue-priced total, never merged
30
+ * into it — the rule `docs/commands.md` states for LiteLLM's `spend`. They
31
+ * are two measurements of the same rows; a reader is entitled to both and to
32
+ * the gap. `byok_usage_inference` is what an upstream provider charged on the
33
+ * operator's own key through OpenRouter, and is carried separately for the
34
+ * same reason.
35
+ *
36
+ * ## What the record says, and what it declines to say
37
+ *
38
+ * The record is `prompt_tokens` and `completion_tokens` under the model's
39
+ * slug, at the day's start. `model` rather than `model_permaslug`, because
40
+ * the slug is what the pricing overlay is keyed by and a versioned slug
41
+ * would price nothing until somebody mapped it.
42
+ *
43
+ * `reasoning_tokens` are counted and **not added** to the completion count.
44
+ * The schema does not say whether `completion_tokens` already includes
45
+ * them, and adding them if it does would charge reasoning twice on the
46
+ * models where it is the largest line. They are reported so the operator
47
+ * can see the count and settle the question against their own invoice.
48
+ *
49
+ * `endpoint_id` and `provider_name` are read by nothing that reaches the
50
+ * output: a row is priced by model, and which provider served it is what
51
+ * `usage` already reflects. `workspace_id` is read only through a mapping
52
+ * the operator writes, exact match, and unlike Anthropic's a `null` here has
53
+ * one meaning only — the request did not group by workspace.
54
+ */
55
+
56
+ /** One workspace, and the label the operator gives it. Exact match. */
57
+ export interface OpenrouterWorkspaceLabel {
58
+ workspace: string;
59
+ label: string;
60
+ }
61
+
62
+ export interface OpenrouterActivityRecord {
63
+ model: string;
64
+ ts: string;
65
+ label?: string;
66
+ usage: { prompt_tokens: number; completion_tokens: number };
67
+ }
68
+
69
+ export interface OpenrouterActivityConversion {
70
+ records: OpenrouterActivityRecord[];
71
+ /** Rows that became records. */
72
+ rows: number;
73
+ /** `requests`, summed. A row is a day's aggregate, not a call. */
74
+ requests: number;
75
+ /** What OpenRouter charged, summed, in dollars. Never merged into Trazum's total. */
76
+ reportedUsageUsd: number;
77
+ /** What upstream providers charged on the operator's own keys, summed. */
78
+ byokUsd: number;
79
+ /** `reasoning_tokens`, summed, and deliberately not added to any record. */
80
+ reasoningTokens: number;
81
+ /** Rows with no model slug, which nothing can price. */
82
+ unnamedModel: number;
83
+ /** Rows whose date was not a `YYYY-MM-DD` day. */
84
+ undatedRows: number;
85
+ /** Distinct days seen, so a reader knows how much of the thirty this is. */
86
+ days: number;
87
+ labelledByWorkspace: number;
88
+ unruledWorkspace: number;
89
+ /** Rules were given and no row carried a workspace id. */
90
+ workspaceNotGrouped: boolean;
91
+ /** The input was not the JSON this endpoint returns. */
92
+ unparseable: number;
93
+ }
94
+
95
+ const count = (value: unknown): number =>
96
+ typeof value === 'number' && Number.isFinite(value) && value >= 0 ? value : 0;
97
+
98
+ const asObject = (value: unknown): Record<string, unknown> | null =>
99
+ typeof value === 'object' && value !== null && !Array.isArray(value)
100
+ ? (value as Record<string, unknown>)
101
+ : null;
102
+
103
+ const DAY = /^\d{4}-\d{2}-\d{2}$/;
104
+
105
+ export function openrouterActivityRecords(
106
+ text: string,
107
+ options: { label?: string; labelByWorkspace?: readonly OpenrouterWorkspaceLabel[] } = {},
108
+ ): OpenrouterActivityConversion {
109
+ const empty: OpenrouterActivityConversion = {
110
+ records: [],
111
+ rows: 0,
112
+ requests: 0,
113
+ reportedUsageUsd: 0,
114
+ byokUsd: 0,
115
+ reasoningTokens: 0,
116
+ unnamedModel: 0,
117
+ undatedRows: 0,
118
+ days: 0,
119
+ labelledByWorkspace: 0,
120
+ unruledWorkspace: 0,
121
+ workspaceNotGrouped: false,
122
+ unparseable: 0,
123
+ };
124
+
125
+ let parsed: unknown;
126
+ try {
127
+ parsed = JSON.parse(text);
128
+ } catch {
129
+ return { ...empty, unparseable: 1 };
130
+ }
131
+ const report = asObject(parsed);
132
+ if (report === null || !Array.isArray(report.data)) return { ...empty, unparseable: 1 };
133
+
134
+ const rules = options.labelByWorkspace;
135
+ const records: OpenrouterActivityRecord[] = [];
136
+ const days = new Set<string>();
137
+ let rows = 0;
138
+ let requests = 0;
139
+ let reportedUsageUsd = 0;
140
+ let byokUsd = 0;
141
+ let reasoningTokens = 0;
142
+ let unnamedModel = 0;
143
+ let undatedRows = 0;
144
+ let labelledByWorkspace = 0;
145
+ let unruledWorkspace = 0;
146
+ let anyWorkspace = false;
147
+
148
+ for (const found of report.data) {
149
+ const row = asObject(found);
150
+ if (row === null) continue;
151
+
152
+ /* The two billed figures are summed over every row, refused or not: a
153
+ refused row was still charged for, and a total that left it out would
154
+ be understated in the direction nobody questions. */
155
+ reportedUsageUsd += count(row.usage);
156
+ byokUsd += count(row.byok_usage_inference);
157
+ requests += count(row.requests);
158
+ reasoningTokens += count(row.reasoning_tokens);
159
+
160
+ if (typeof row.date !== 'string' || !DAY.test(row.date)) {
161
+ undatedRows += 1;
162
+ continue;
163
+ }
164
+ if (typeof row.model !== 'string' || row.model === '') {
165
+ unnamedModel += 1;
166
+ continue;
167
+ }
168
+ days.add(row.date);
169
+
170
+ let label = options.label;
171
+ if (rules !== undefined) {
172
+ const workspace = typeof row.workspace_id === 'string' ? row.workspace_id : null;
173
+ if (workspace !== null) {
174
+ anyWorkspace = true;
175
+ const named = rules.find((rule) => rule.workspace === workspace);
176
+ if (named !== undefined) {
177
+ label = named.label;
178
+ labelledByWorkspace += 1;
179
+ } else {
180
+ unruledWorkspace += 1;
181
+ }
182
+ }
183
+ }
184
+
185
+ records.push({
186
+ model: row.model,
187
+ ts: `${row.date}T00:00:00.000Z`,
188
+ ...(label === undefined ? {} : { label }),
189
+ usage: {
190
+ prompt_tokens: count(row.prompt_tokens),
191
+ completion_tokens: count(row.completion_tokens),
192
+ },
193
+ });
194
+ rows += 1;
195
+ }
196
+
197
+ return {
198
+ records,
199
+ rows,
200
+ requests,
201
+ reportedUsageUsd,
202
+ byokUsd,
203
+ reasoningTokens,
204
+ unnamedModel,
205
+ undatedRows,
206
+ days: days.size,
207
+ labelledByWorkspace,
208
+ unruledWorkspace,
209
+ workspaceNotGrouped: rules !== undefined && !anyWorkspace,
210
+ unparseable: 0,
211
+ };
212
+ }
213
+
214
+ /**
215
+ * Whether this text is an OpenRouter activity report. `model_permaslug`
216
+ * belongs to no other document, and `byok_usage_inference` likewise.
217
+ */
218
+ export function looksLikeOpenrouterActivity(text: string, prefixBytes = 8192): boolean {
219
+ const head = text.slice(0, prefixBytes);
220
+ return head.includes('"model_permaslug"') || head.includes('"byok_usage_inference"');
221
+ }
package/src/pricing.ts CHANGED
@@ -47,10 +47,30 @@ export const PROVIDER_REVIEWED: Readonly<Record<string, string>> = Object.freeze
47
47
  openai: '2026-08-31',
48
48
  /* ai.google.dev/gemini-api/docs/pricing, read 2026-08-28. */
49
49
  google: '2026-08-28',
50
- moonshot: '2026-06-24',
50
+ /*
51
+ * platform.kimi.ai/docs/pricing/chat (platform.moonshot.ai 301s there),
52
+ * read 2026-09-12. The page prices kimi-k3, kimi-k2.6, kimi-k2.7-code and
53
+ * its highspeed variant; `kimi-k2`, the one id this table carries, is no
54
+ * longer on it. Its price is kept because calls in somebody's log really
55
+ * happened at it, and it is not marked retired because that is recorded
56
+ * from the provider's own refusal to a request, which needs a key this
57
+ * repository does not hold. The newer models are not added: the page states
58
+ * no context window for any of them, and a context window this table did
59
+ * not read is a number it would be inventing.
60
+ */
61
+ moonshot: '2026-09-12',
51
62
  /* api-docs.deepseek.com/quick_start/pricing, read 2026-08-28. */
52
63
  deepseek: '2026-08-28',
53
- xai: '2026-06-24',
64
+ /*
65
+ * docs.x.ai/docs/models, read 2026-09-12. The page prices grok-4.6, 4.5,
66
+ * 4.3 and the 4.20 family, each at two rates split at 200k prompt tokens;
67
+ * `grok-4`, the one id this table carries, is no longer on it. Kept and not
68
+ * marked retired, for the reasons given for kimi-k2 above. The newer models
69
+ * are not added: this table has no way to express a price that changes at
70
+ * 200k prompt tokens, and pricing a 300k-token call at the under-200k rate
71
+ * would understate it by half while looking exact.
72
+ */
73
+ xai: '2026-09-12',
54
74
  /*
55
75
  * `mistral.ai/pricing`, read 2026-08-28 — by a human, because the page
56
76
  * renders its table in the browser and serves a fetch only an FAQ example
@@ -379,7 +399,9 @@ export const MODELS: ModelPricing[] = [
379
399
  multipliers: { cacheRead: 0.1, cacheWrite5m: 1, cacheWrite1h: 1, batch: null },
380
400
  capability: 'mid',
381
401
  tier: 'sonnet',
382
- notes: 'No batch API: the batch advisory stays quiet rather than offering a discount you cannot buy.',
402
+ notes:
403
+ 'No batch API: the batch advisory stays quiet rather than offering a discount you cannot buy.'
404
+ + ' Not on the provider\'s pricing page as of 2026-09-12, which lists kimi-k3 and kimi-k2.6 instead; the price is the last one published for it.',
383
405
  },
384
406
 
385
407
  // ------------------------------------------------------------------------
@@ -475,6 +497,8 @@ export const MODELS: ModelPricing[] = [
475
497
  multipliers: { cacheRead: 0.25, cacheWrite5m: 1, cacheWrite1h: 1, batch: null },
476
498
  capability: 'large',
477
499
  tier: 'opus',
500
+ notes:
501
+ 'Not on the provider\'s model page as of 2026-09-12, which lists grok-4.6 and later instead; the price is the last one published for it.',
478
502
  },
479
503
 
480
504
  // ------------------------------------------------------------------------
@@ -887,6 +911,70 @@ function describe(when: TierCondition): string {
887
911
  return `${windows.join(' and ')} UTC${when.weekdaysOnly ? ', Monday to Friday' : ''}`;
888
912
  }
889
913
 
914
+ /**
915
+ * The date the prices behind one report were reviewed.
916
+ *
917
+ * ## The sentence this exists to make true
918
+ *
919
+ * `PRICING_LAST_REVIEWED` is the **oldest** provider's date, which is the right
920
+ * answer to *how old is this table* and the wrong answer to *how old are the
921
+ * prices in front of me*. Every surface that warns about staleness was using
922
+ * it, and the warning says, in these words, that the table behind **every
923
+ * dollar here** was last reviewed on that date.
924
+ *
925
+ * On 2026-08-31 that sentence was false on a report of Claude and OpenAI
926
+ * calls. It named 2026-06-24 — the date belonging to two models that appear in
927
+ * no such report and whose providers stopped listing them — while the prices
928
+ * actually used had been read four days and zero days earlier. A warning that
929
+ * fires on every run is one people stop reading, and a provenance that does
930
+ * not hold is the one thing this tool exists not to print.
931
+ *
932
+ * `trazum models` had already worked this out and prints the dates per
933
+ * provider, with the reason in a comment. The fix reached one surface and not
934
+ * the three that qualify a figure.
935
+ *
936
+ * ## What it returns, and the direction it errs in
937
+ *
938
+ * The oldest review date among the providers that actually priced these
939
+ * models. Where that cannot be established it returns the catalogue's own
940
+ * `lastReviewed`, which is the **conservative** direction: reporting a fresher
941
+ * date for a report containing a price of unknown provenance would be claiming
942
+ * provenance this table does not have.
943
+ *
944
+ * It cannot be established in three cases, all of which fall back:
945
+ *
946
+ * - **An overlay is in effect.** `--pricing` and `--pricing-live` replace
947
+ * prices with numbers whose provenance is the overlay's own date, and
948
+ * `PROVIDER_REVIEWED` describes the bundled table rather than theirs.
949
+ * - **A model the catalogue does not carry**, or one carrying no provider: a
950
+ * price with no page behind it.
951
+ * - **A provider with no recorded review date.**
952
+ *
953
+ * The fallback is inside this function rather than at each call site on
954
+ * purpose. `isOffered` records why: the fifth call site is always the one
955
+ * written with only the first half of a two-part rule.
956
+ */
957
+ export function reviewedForModels(
958
+ models: Iterable<string>,
959
+ catalogue: PricingCatalogue,
960
+ ): string {
961
+ /* An overlay's prices are not this table's, so this table's dates say
962
+ nothing about them. */
963
+ if (catalogue.lastReviewed !== PRICING_LAST_REVIEWED) return catalogue.lastReviewed;
964
+
965
+ let oldest: string | null = null;
966
+ for (const id of models) {
967
+ const model = catalogue.models.find((m) => m.id === id);
968
+ if (model?.provider === undefined) return catalogue.lastReviewed;
969
+ const date = PROVIDER_REVIEWED[model.provider];
970
+ if (date === undefined) return catalogue.lastReviewed;
971
+ if (oldest === null || date < oldest) oldest = date;
972
+ }
973
+ /* Nothing priced: there is no report-specific answer, so the table's own
974
+ date stands rather than an absence being read as freshness. */
975
+ return oldest ?? catalogue.lastReviewed;
976
+ }
977
+
890
978
  /** Cheapest model of each capability tier, for recommendations. */
891
979
  export function cheapestOfTier(tier: ModelPricing['tier']): ModelPricing {
892
980
  const candidates = MODELS.filter((m) => m.tier === tier && isOffered(m));