@trazum/core 2.3.0 → 2.4.1

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 (40) 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 +11 -1
  14. package/dist/index.d.ts.map +1 -1
  15. package/dist/index.js +5 -0
  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.map +1 -1
  30. package/dist/pricing.js +25 -3
  31. package/dist/pricing.js.map +1 -1
  32. package/package.json +2 -2
  33. package/src/anthropic-cost.ts +303 -0
  34. package/src/anthropic-usage.ts +363 -0
  35. package/src/claude-code.ts +62 -2
  36. package/src/index.ts +19 -1
  37. package/src/openai-cost.ts +170 -0
  38. package/src/openai-usage.ts +353 -0
  39. package/src/openrouter-activity.ts +221 -0
  40. package/src/pricing.ts +27 -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
  // ------------------------------------------------------------------------