@trazum/core 1.81.0 → 1.83.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/README.md +34 -18
  2. package/dist/advisories.d.ts +33 -1
  3. package/dist/advisories.d.ts.map +1 -1
  4. package/dist/advisories.js +130 -20
  5. package/dist/advisories.js.map +1 -1
  6. package/dist/band.d.ts +131 -0
  7. package/dist/band.d.ts.map +1 -0
  8. package/dist/band.js +210 -0
  9. package/dist/band.js.map +1 -0
  10. package/dist/conform.d.ts +1 -1
  11. package/dist/conform.d.ts.map +1 -1
  12. package/dist/conform.js +84 -1
  13. package/dist/conform.js.map +1 -1
  14. package/dist/helicone.d.ts +92 -0
  15. package/dist/helicone.d.ts.map +1 -0
  16. package/dist/helicone.js +194 -0
  17. package/dist/helicone.js.map +1 -0
  18. package/dist/i18n/en.d.ts.map +1 -1
  19. package/dist/i18n/en.js +16 -3
  20. package/dist/i18n/en.js.map +1 -1
  21. package/dist/i18n/es.d.ts.map +1 -1
  22. package/dist/i18n/es.js +17 -3
  23. package/dist/i18n/es.js.map +1 -1
  24. package/dist/i18n/types.d.ts +60 -1
  25. package/dist/i18n/types.d.ts.map +1 -1
  26. package/dist/index.d.ts +16 -3
  27. package/dist/index.d.ts.map +1 -1
  28. package/dist/index.js +9 -3
  29. package/dist/index.js.map +1 -1
  30. package/dist/langsmith.d.ts +104 -0
  31. package/dist/langsmith.d.ts.map +1 -0
  32. package/dist/langsmith.js +270 -0
  33. package/dist/langsmith.js.map +1 -0
  34. package/dist/levers.d.ts +1 -1
  35. package/dist/levers.js +2 -2
  36. package/dist/levers.js.map +1 -1
  37. package/dist/litellm.d.ts +98 -0
  38. package/dist/litellm.d.ts.map +1 -0
  39. package/dist/litellm.js +207 -0
  40. package/dist/litellm.js.map +1 -0
  41. package/dist/measured-profile.d.ts +4 -0
  42. package/dist/measured-profile.d.ts.map +1 -1
  43. package/dist/measured-profile.js +22 -1
  44. package/dist/measured-profile.js.map +1 -1
  45. package/dist/pricing.d.ts +98 -3
  46. package/dist/pricing.d.ts.map +1 -1
  47. package/dist/pricing.js +393 -35
  48. package/dist/pricing.js.map +1 -1
  49. package/dist/receipt.d.ts +253 -0
  50. package/dist/receipt.d.ts.map +1 -0
  51. package/dist/receipt.js +111 -0
  52. package/dist/receipt.js.map +1 -0
  53. package/dist/schemas.d.ts.map +1 -1
  54. package/dist/schemas.js +36 -0
  55. package/dist/schemas.js.map +1 -1
  56. package/dist/tokenizer.d.ts +45 -49
  57. package/dist/tokenizer.d.ts.map +1 -1
  58. package/dist/tokenizer.js +100 -51
  59. package/dist/tokenizer.js.map +1 -1
  60. package/dist/types.d.ts +108 -1
  61. package/dist/types.d.ts.map +1 -1
  62. package/dist/verdict-bridge.d.ts +113 -0
  63. package/dist/verdict-bridge.d.ts.map +1 -0
  64. package/dist/verdict-bridge.js +127 -0
  65. package/dist/verdict-bridge.js.map +1 -0
  66. package/package.json +1 -1
  67. package/src/advisories.ts +166 -19
  68. package/src/band.ts +213 -0
  69. package/src/conform.ts +100 -1
  70. package/src/helicone.ts +227 -0
  71. package/src/i18n/en.ts +19 -3
  72. package/src/i18n/es.ts +20 -3
  73. package/src/i18n/types.ts +63 -1
  74. package/src/index.ts +31 -2
  75. package/src/langsmith.ts +315 -0
  76. package/src/levers.ts +3 -3
  77. package/src/litellm.ts +243 -0
  78. package/src/measured-profile.ts +26 -2
  79. package/src/pricing.ts +430 -30
  80. package/src/receipt.ts +351 -0
  81. package/src/schemas.ts +36 -0
  82. package/src/tokenizer.ts +101 -51
  83. package/src/types.ts +104 -0
  84. package/src/verdict-bridge.ts +200 -0
package/src/types.ts CHANGED
@@ -61,8 +61,10 @@ export type AdvisoryId =
61
61
  | 'cache-prefix-reorder'
62
62
  | 'batch-api'
63
63
  | 'model-downgrade'
64
+ | 'tier-signals-conflict'
64
65
  | 'output-dominated'
65
66
  | 'promo-pricing'
67
+ | 'model-retired'
66
68
  | 'contradictory-instructions'
67
69
  | 'redundant-examples'
68
70
  | 'restated-output-format'
@@ -148,6 +150,59 @@ export type CachingMode =
148
150
  */
149
151
  export type Capability = 'small' | 'mid' | 'large' | 'frontier' | 'unknown';
150
152
 
153
+ /**
154
+ * What makes a conditional rate apply, as data a machine can evaluate.
155
+ *
156
+ * Deliberately a closed union rather than a predicate function. A catalogue
157
+ * entry is reviewed the way a price is reviewed, and a reviewer can check
158
+ * "peak is 01:00-04:00 and 06:00-10:00 UTC on weekdays" against a provider's
159
+ * page; nobody can review a closure. It also means the condition survives
160
+ * `JSON.stringify`, which matters because the catalogue can be overlaid from a
161
+ * file.
162
+ */
163
+ export type TierCondition =
164
+ /**
165
+ * The prompt is larger than `tokens` input tokens.
166
+ *
167
+ * Decidable only where the token count is known, which is most of the places
168
+ * that price a specific call and none of the places that merely rank models.
169
+ * `effectivePricing` says which happened rather than guessing.
170
+ */
171
+ | { kind: 'input-tokens-above'; tokens: number }
172
+ /**
173
+ * The call falls inside one of `hours` in UTC, optionally on weekdays only.
174
+ *
175
+ * Decidable everywhere, because every caller already passes the date it is
176
+ * pricing for. `hours` lists the hour each window *starts*, so 01:00-04:00
177
+ * is `[1, 2, 3]` and not `[1, 4]`: an endpoint convention is the kind of
178
+ * thing two readers disagree about silently.
179
+ */
180
+ | { kind: 'utc-hours'; hours: number[]; weekdaysOnly: boolean };
181
+
182
+ /**
183
+ * A rate that applies when its condition holds.
184
+ *
185
+ * Deliberately carries no multipliers of its own, and the first draft did. The
186
+ * reasoning for adding them was that DeepSeek halves the cache-hit price at
187
+ * off-peak along with everything else, so a cache read priced off the base
188
+ * would be wrong about the money — which is true of the *price* and false of
189
+ * the *ratio*: $0.007 against $0.22 and $0.014 against $0.44 are the same
190
+ * 3.18% to the last digit, on both models. Multiplying a tier's own input rate
191
+ * by the model's multiplier already gives the right answer.
192
+ *
193
+ * It was removed because the guard written to prove it mattered could not: with
194
+ * the tier multiplier equal to the base one, dropping it changed nothing and
195
+ * the test passed either way. An option nothing can distinguish from its
196
+ * absence is a claim nobody checks, which is worse than not having it.
197
+ */
198
+ export interface PricingTier {
199
+ /** Short, stable, and printed: a reader has to be able to see which applied. */
200
+ id: string;
201
+ when: TierCondition;
202
+ inputPerMTok: number;
203
+ outputPerMTok: number;
204
+ }
205
+
151
206
  export interface ModelPricing {
152
207
  id: string;
153
208
  displayName: string;
@@ -179,6 +234,32 @@ export interface ModelPricing {
179
234
  * buy is advice that wastes their time.
180
235
  */
181
236
  recommendable?: boolean;
237
+ /**
238
+ * Set when the provider refused a real request for this id.
239
+ *
240
+ * A retired model keeps its price and loses its future. Both halves matter:
241
+ * a log full of `deepseek-v3` calls records money that was genuinely spent,
242
+ * so deleting the row would make somebody's own history unpriceable — and a
243
+ * *new* call on that id will not happen at all, so quoting it as an option is
244
+ * a number for a question nobody can ask.
245
+ *
246
+ * `on` is the day `scripts/check-model-availability.mjs` was refused, and
247
+ * `because` is the provider's own sentence, quoted rather than paraphrased.
248
+ * Neither is a judgement, and neither implies anything about what the
249
+ * replacement costs: that is a price, and a price comes off the provider's
250
+ * own page or not at all.
251
+ *
252
+ * A retired model is never recommended, never offered as a cheaper
253
+ * alternative and never chosen as a tier's representative, in the same places
254
+ * `recommendable: false` is honoured — this is a stronger statement than that
255
+ * one and must not be weaker in effect.
256
+ */
257
+ retired?: {
258
+ /** ISO date the refusal was recorded. */
259
+ on: string;
260
+ /** What the provider answered, in the provider's words. */
261
+ because: string;
262
+ };
182
263
  /** Capability, on a vendor-neutral scale. */
183
264
  capability: Capability;
184
265
  /**
@@ -194,6 +275,29 @@ export interface ModelPricing {
194
275
  /** ISO date (inclusive) the promotional price applies until. */
195
276
  until: string;
196
277
  };
278
+ /**
279
+ * Rates that apply only under a stated condition.
280
+ *
281
+ * The catalogue held one input price and one output price per model until a
282
+ * real credential was pointed at the providers and two of them turned out not
283
+ * to charge that way:
284
+ *
285
+ * - **DeepSeek V4 bills by the clock.** Peak is 01:00-04:00 and 06:00-10:00
286
+ * UTC, Monday to Friday, and off-peak is exactly half. That is 35 hours of
287
+ * 168, so the common case is the cheap one and a single number would have
288
+ * been wrong by 2x in whichever direction it was chosen.
289
+ * - **Gemini 3.1 Pro bills by prompt size**, $2/$12 up to 200k input tokens
290
+ * and $4/$18 above it.
291
+ *
292
+ * Both are *conditions*, so both are written as data rather than as prose in
293
+ * `notes`: a note is something a reader has to apply by hand, and this
294
+ * product exists to stop people doing arithmetic by hand.
295
+ *
296
+ * The base `inputPerMTok` and `outputPerMTok` stay the rate that applies when
297
+ * no condition matches, which keeps every model that has no tiers unchanged
298
+ * and keeps this field optional in effect as well as in the type.
299
+ */
300
+ tiers?: PricingTier[];
197
301
  /**
198
302
  * Other ids that bill at exactly this rate.
199
303
  *
@@ -0,0 +1,200 @@
1
+ /**
2
+ * Reading a dropped `route --json` verdict, so quality can stand beside cost.
3
+ *
4
+ * The bill is arithmetic: every figure in it comes from records already on
5
+ * disk, and it can say exactly what a cheaper model would have saved. It can
6
+ * say nothing at all about whether that model still does the job. That answer
7
+ * costs provider calls and a credential, which is why `trazum route` exists
8
+ * and why no surface that only reads a log will ever produce it.
9
+ *
10
+ * This is the bridge between the two, and it is deliberately one-directional:
11
+ * the measurement is made where the credential is, written out as the
12
+ * `routing-measurement` contract, and read back here by a pure function that
13
+ * touches no network and holds no key. A reader who has run `route` once can
14
+ * carry the verdict to any surface that prices the same workload.
15
+ *
16
+ * It was not possible to write until the document was worth reading. Before
17
+ * the JSON-text sweep, `route --json` printed the whole `EvalReport` — every
18
+ * case input and three model answers per case — so a bridge would have been a
19
+ * feature for carrying prompt text into a browser. The document carries the
20
+ * measurement and nothing else now, and `conform` is what says so rather than
21
+ * this file's own reading: the contract validates, the bridge only maps.
22
+ */
23
+
24
+ import { conform } from './conform.js';
25
+ import type { EvalVerdict } from './evaluate.js';
26
+ import { UNLABELLED } from './usage.js';
27
+
28
+ /** What a dropped routing measurement says, mapped onto what a bill can match. */
29
+ export interface DroppedVerdict {
30
+ /** The workload the measurement was made on, or null when it was unlabelled. */
31
+ label: string | null;
32
+ /**
33
+ * The model those calls go to today, from the bill's own slice.
34
+ *
35
+ * Not the model that answered. `route` builds its baseline provider from
36
+ * the environment, so the model in the measurement is whatever
37
+ * `TRAZUM_LLM_MODEL` names, and it is only the log's model when the reader
38
+ * configured it that way. Pairing on it matched nothing on a real run
39
+ * against an OpenAI-compatible endpoint — `evaluation.model` said
40
+ * `stub-strong` while the log said `claude-opus-5` — and the bill would
41
+ * have shown no verdict at all with no explanation.
42
+ */
43
+ model: string;
44
+ /**
45
+ * The model that actually answered, when it is not the one the log records.
46
+ *
47
+ * Null when they agree, which is the ordinary case. Not null is worth
48
+ * saying out loud rather than smoothing over: a verdict measured on a
49
+ * stand-in is a weaker claim about this workload than one measured on the
50
+ * model the workload actually uses, and only this field can tell the
51
+ * difference.
52
+ */
53
+ measuredOn: string | null;
54
+ /** The model measured against it. */
55
+ candidateModel: string;
56
+ verdict: EvalVerdict;
57
+ /** The model's agreement with itself. The yardstick the other rate is read against. */
58
+ selfAgreement: number;
59
+ /** Agreement between the two models, same prompt on both sides. */
60
+ crossAgreement: number;
61
+ /** Cases the measurement covered, so a two-case verdict cannot pose as a hundred. */
62
+ cases: number;
63
+ /** Calls it cost to reach. */
64
+ callsMade: number;
65
+ /**
66
+ * What the measurement's own bill priced the route at, when the document
67
+ * carried it. Null rather than zero when it did not: a saving nobody
68
+ * measured is not a saving of nothing.
69
+ */
70
+ savingUsd: number | null;
71
+ }
72
+
73
+ /**
74
+ * What the bridge did with a file.
75
+ *
76
+ * `null` is not a failure: it means the file is not a routing measurement at
77
+ * all, and the caller should go on treating it as whatever else it was. A
78
+ * refusal is the other case — it looked like one and could not be read — and
79
+ * it names what is missing, because a refusal with nothing after it is
80
+ * indistinguishable from a bug.
81
+ */
82
+ export type BridgeReading =
83
+ | { kind: 'verdict'; verdict: DroppedVerdict }
84
+ | { kind: 'refusal'; because: string };
85
+
86
+ const isObject = (value: unknown): value is Record<string, unknown> =>
87
+ typeof value === 'object' && value !== null && !Array.isArray(value);
88
+
89
+ /**
90
+ * Whether this file is even claiming to be a routing measurement.
91
+ *
92
+ * Shape only, and deliberately narrow: an `evaluation` object carrying a
93
+ * `verdict`. Anything looser would swallow a usage log or a profile and
94
+ * refuse it in this file's words instead of letting the caller price it.
95
+ */
96
+ function claimsToBeOne(parsed: unknown): boolean {
97
+ if (!isObject(parsed)) return false;
98
+ const evaluation = parsed.evaluation;
99
+ return isObject(evaluation) && typeof evaluation.verdict === 'string';
100
+ }
101
+
102
+ /**
103
+ * Reads a dropped `trazum route --json` document.
104
+ *
105
+ * Returns null when the text is something else entirely. Validation is
106
+ * `conform`'s, against the published `routing-measurement` contract, so this
107
+ * function cannot drift from the schema a connector author builds against —
108
+ * and a document that fails carries the contract's own sentences back to the
109
+ * reader rather than a second opinion written here.
110
+ */
111
+ export function readDroppedVerdict(text: string): BridgeReading | null {
112
+ let parsed: unknown;
113
+ try {
114
+ parsed = JSON.parse(text);
115
+ } catch {
116
+ return null;
117
+ }
118
+ if (!claimsToBeOne(parsed)) return null;
119
+
120
+ const report = conform(text, { contract: 'routing-measurement' });
121
+ if (!report.conforms) {
122
+ const detail =
123
+ report.problems.length > 0
124
+ ? report.problems.map((problem) => `${problem.at}: ${problem.detail}`).join('; ')
125
+ : (report.because ?? 'it does not match the routing-measurement contract');
126
+ return { kind: 'refusal', because: detail };
127
+ }
128
+
129
+ const document = parsed as { slice?: unknown; evaluation?: unknown };
130
+ const slice = isObject(document.slice) ? document.slice : {};
131
+ const evaluation = document.evaluation as Record<string, unknown>;
132
+
133
+ /**
134
+ * Every field below is read defensively even though `conform` has passed,
135
+ * because the contract requires `slice` and `evaluation` to be objects and
136
+ * says nothing about what is inside them. A document from a future version
137
+ * with a thinner slice is a document this reads what it can from, not one
138
+ * it crashes on.
139
+ */
140
+ const answered = typeof evaluation.model === 'string' ? evaluation.model : null;
141
+ // The slice's model first: that is the fact the bill can be matched against.
142
+ const model = typeof slice.model === 'string' ? slice.model : answered;
143
+ const candidateModel =
144
+ typeof evaluation.candidateModel === 'string' ? evaluation.candidateModel : null;
145
+ const verdict = typeof evaluation.verdict === 'string' ? (evaluation.verdict as EvalVerdict) : null;
146
+ if (model === null || candidateModel === null || verdict === null) {
147
+ return {
148
+ kind: 'refusal',
149
+ because:
150
+ 'the evaluation names no model, candidate model or verdict, so there is nothing to set beside a bill',
151
+ };
152
+ }
153
+
154
+ const number = (value: unknown): number => (typeof value === 'number' ? value : 0);
155
+ const route = isObject(slice.route) ? slice.route : null;
156
+ const rawLabel = typeof slice.label === 'string' ? slice.label : null;
157
+
158
+ return {
159
+ kind: 'verdict',
160
+ verdict: {
161
+ // `UNLABELLED` is the profile's own sentinel for calls that carry no
162
+ // label. It travels through the document as that sentinel and comes
163
+ // back as null here, so the bridge speaks the bill's language rather
164
+ // than leaking an internal marker into a caption.
165
+ label: rawLabel === null || rawLabel === UNLABELLED ? null : rawLabel,
166
+ model,
167
+ measuredOn: answered !== null && answered !== model ? answered : null,
168
+ candidateModel,
169
+ verdict,
170
+ selfAgreement: number(evaluation.selfAgreement),
171
+ crossAgreement: number(evaluation.crossAgreement),
172
+ cases: Array.isArray(evaluation.cases) ? evaluation.cases.length : 0,
173
+ callsMade: number(evaluation.callsMade),
174
+ savingUsd: route !== null && typeof route.savingUsd === 'number' ? route.savingUsd : null,
175
+ },
176
+ };
177
+ }
178
+
179
+ /**
180
+ * Whether a verdict describes the route a given slice of the bill is offering.
181
+ *
182
+ * Both halves must agree or the pairing is a lie: the same workload, the same
183
+ * model it goes to now, and the same candidate. A verdict measured on `chat`
184
+ * shown against `summarise`'s saving would be the exact fault this repository
185
+ * keeps finding in itself — a number describing something other than what was
186
+ * measured — and matching on the model alone would produce it on any log with
187
+ * two workloads on one model.
188
+ */
189
+ export function verdictMatchesSlice(
190
+ verdict: DroppedVerdict,
191
+ slice: { label: string; model: string; route: { candidate: { id: string } } | null },
192
+ ): boolean {
193
+ if (slice.route === null) return false;
194
+ const sliceLabel = slice.label === UNLABELLED ? null : slice.label;
195
+ return (
196
+ sliceLabel === verdict.label &&
197
+ slice.model === verdict.model &&
198
+ slice.route.candidate.id === verdict.candidateModel
199
+ );
200
+ }