@trazum/cli 1.29.0 → 1.30.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@trazum/cli",
3
- "version": "1.29.0",
3
+ "version": "1.30.0",
4
4
  "description": "Trazum CLI: find where your LLM bill goes, price every finding per month, and enforce token budgets in CI.",
5
5
  "license": "MIT",
6
6
  "author": "David Mu\u00f1oz Rey",
@@ -37,7 +37,7 @@
37
37
  "prepublishOnly": "npm run build && npm test"
38
38
  },
39
39
  "dependencies": {
40
- "@trazum/core": "1.29.0"
40
+ "@trazum/core": "1.30.0"
41
41
  },
42
42
  "devDependencies": {
43
43
  "@types/node": "^26.2.0",
package/src/i18n/en.ts CHANGED
@@ -1173,6 +1173,21 @@ ${bold('EXAMPLES')}
1173
1173
  `The 95th percentile is ${ratio}x the median there: most conversations are cheap and a few are not, which is a tail a quota can catch. Where median and p95 sit close together the workload is simply expensive and there is no tail to hunt.`,
1174
1174
  sessionSpendOnly: (sessions, max) =>
1175
1175
  `${sessions} ${sessions === '1' ? 'conversation' : 'conversations'} in this log; the most expensive cost ${max}. Too few per workload for a percentile — a maximum is a fact at any count, and it is the figure --max-session-usd judges.`,
1176
+ maxGrowthCoverageLost: (fields, was, now) =>
1177
+ `FAILED — this log stopped recording ${fields} (${was} of records before, ${now} now), so the comparison cannot be made. That is not a pass: a bill whose growth nobody could measure is not a bill that stayed flat, and every finding that needed the field went quiet for a reason that has nothing to do with spend.`,
1178
+ coverageField: (field) =>
1179
+ ({ label: 'label', session: 'session', ts: 'timestamp', stopReason: 'stop reason' })[field] ?? field,
1180
+ coverageSilenced: (field) =>
1181
+ ({
1182
+ label: 'Gone quiet with it: per-workload spend, the drill-down, and levers that describe a decision rather than a mixture.',
1183
+ session: 'Gone quiet with it: conversation growth, per-conversation cost, repeated turns, truncation retries and the cache-TTL fit.',
1184
+ ts: 'Gone quiet with it: the period, the per-day and per-hour shape, the model mix drift, and the cache-TTL question entirely.',
1185
+ stopReason: 'Gone quiet with it: answers cut off at max_tokens, and the retries billed after them.',
1186
+ })[field] ?? '',
1187
+ coverageDrift: (field, was, now) =>
1188
+ `Coverage moved: ${field} was on ${was} of records and is now on ${now}.`,
1189
+ coverageDriftWhy: () =>
1190
+ 'A field the log stopped recording is not a finding that got fixed — every finding that needed it has gone quiet for a reason that has nothing to do with the bill. Reported from a 20-point move in either direction; a field that appeared means this report can see what the previous one could not.',
1176
1191
  againstOverlap: (from, to) =>
1177
1192
  `These two logs both cover ${from} → ${to}, so some of the same calls sit on both sides of this subtraction and part of the change is the same money counted twice. Compare periods that do not overlap — or window both logs with --since/--until.`,
1178
1193
  windowLine: (since, until) =>
package/src/i18n/es.ts CHANGED
@@ -1188,6 +1188,21 @@ ${bold('EJEMPLOS')}
1188
1188
  `El percentil 95 es ${ratio}x la mediana ahí: casi todas las conversaciones son baratas y unas pocas no, y esa es una cola que una cuota puede cazar. Cuando mediana y p95 quedan cerca, la carga es cara sin más y no hay cola que perseguir.`,
1189
1189
  sessionSpendOnly: (sessions, max) =>
1190
1190
  `${sessions} ${sessions === '1' ? 'conversación' : 'conversaciones'} en este registro; la más cara costó ${max}. Demasiado pocas por carga para un percentil — un máximo es un hecho con cualquier recuento, y es la cifra que juzga --max-session-usd.`,
1191
+ maxGrowthCoverageLost: (fields, was, now) =>
1192
+ `FALLÓ — este registro dejó de grabar ${fields} (${was} de los registros antes, ${now} ahora), así que la comparación no se puede hacer. Eso no es un aprobado: una factura cuyo crecimiento nadie pudo medir no es una factura que se mantuvo plana, y todo hallazgo que necesitaba ese campo se calló por un motivo que nada tiene que ver con el gasto.`,
1193
+ coverageField: (field) =>
1194
+ ({ label: 'etiqueta', session: 'sesión', ts: 'marca de tiempo', stopReason: 'razón de parada' })[field] ?? field,
1195
+ coverageSilenced: (field) =>
1196
+ ({
1197
+ label: 'Se callaron con él: el gasto por carga, el desglose y unas palancas que describan una decisión y no una mezcla.',
1198
+ session: 'Se callaron con él: el crecimiento de conversación, el coste por conversación, los turnos repetidos, los reintentos por truncado y el ajuste del TTL de caché.',
1199
+ ts: 'Se callaron con él: el periodo, la forma por día y por hora, la deriva de mezcla de modelos y la pregunta del TTL de caché por completo.',
1200
+ stopReason: 'Se callaron con él: las respuestas cortadas en max_tokens y los reintentos facturados después.',
1201
+ })[field] ?? '',
1202
+ coverageDrift: (field, was, now) =>
1203
+ `La cobertura se movió: ${field} estaba en el ${was} de los registros y ahora está en el ${now}.`,
1204
+ coverageDriftWhy: () =>
1205
+ 'Un campo que el registro dejó de grabar no es un hallazgo arreglado — todo hallazgo que lo necesitaba se ha callado por un motivo que nada tiene que ver con la factura. Se informa a partir de 20 puntos de movimiento en cualquier dirección; un campo que aparece significa que este informe ve lo que el anterior no podía.',
1191
1206
  againstOverlap: (from, to) =>
1192
1207
  `Estos dos registros cubren ambos ${from} → ${to}, así que algunas de las mismas llamadas están a los dos lados de esta resta y parte del cambio es el mismo dinero contado dos veces. Compara periodos que no se solapen — o acota ambos registros con --since/--until.`,
1193
1208
  windowLine: (since, until) =>
package/src/i18n/types.ts CHANGED
@@ -769,6 +769,26 @@ export interface CliMessages {
769
769
  * money on both sides of the subtraction. Only decidable when both logs
770
770
  * carry a clock; unknown stays silent rather than reassuring.
771
771
  */
772
+ /**
773
+ * What the comparison stopped being able to see. Dollars cannot tell a
774
+ * fixed finding from a field the log stopped recording; only coverage can,
775
+ * so the copy names the threshold and states the distinction.
776
+ */
777
+ /**
778
+ * The comparison gate's refusal: the current log stopped recording a field
779
+ * the previous one carried, so "did not grow" is a claim nobody could
780
+ * check. "Not measured" is not "did not grow", as everywhere here.
781
+ */
782
+ maxGrowthCoverageLost(fields: string, was: string, now: string): string;
783
+ coverageField(field: string): string;
784
+ /**
785
+ * Which findings a collapsed field took with it. Named rather than left as
786
+ * "some findings": a reader deciding whether to trust this report needs to
787
+ * know exactly which sections are now silence rather than absence.
788
+ */
789
+ coverageSilenced(field: string): string;
790
+ coverageDrift(field: string, was: string, now: string): string;
791
+ coverageDriftWhy(): string;
772
792
  againstOverlap(from: string, to: string): string;
773
793
  /**
774
794
  * What one conversation costs — median against p95, never a mean: one
package/src/index.ts CHANGED
@@ -20,6 +20,7 @@ import {
20
20
  countTokensAnthropic,
21
21
  DEFAULT_USAGE,
22
22
  detectFromSource,
23
+ coverageDrift,
23
24
  driversBetween,
24
25
  estimateTokens,
25
26
  evaluate,
@@ -2320,7 +2321,34 @@ async function commandProfile(args: Args, config: TrazumConfig, pricing: Pricing
2320
2321
  }
2321
2322
  if (typeof args.flags.get('max-growth-usd') === 'string' && againstDelta !== null) {
2322
2323
  const maxGrowth = numberFlag(args, 'max-growth-usd', 0, t);
2323
- if (againstDelta > maxGrowth) {
2324
+ /**
2325
+ * A comparison that went blind fails before it is judged.
2326
+ *
2327
+ * The dollars can hold flat while the current log stopped recording a
2328
+ * field the previous one carried — and every finding that needed the
2329
+ * field is now silent for a reason that has nothing to do with spend.
2330
+ * A gate passing there would be certifying a comparison it could not
2331
+ * make: "not measured" is not "did not grow", the same refusal
2332
+ * --max-day-usd makes on a clockless log and --max-session-usd on a
2333
+ * sessionless one. Only a collapse fails; a field that appeared means
2334
+ * this side can see more, which is never a reason to refuse.
2335
+ */
2336
+ const blinded = previous !== null
2337
+ ? coverageDrift(previous.fieldCoverage, report.fieldCoverage).filter((d) => d.delta < 0)
2338
+ : [];
2339
+ const worst = blinded[0];
2340
+ if (worst !== undefined) {
2341
+ console.error(
2342
+ c.red(
2343
+ t.profile.maxGrowthCoverageLost(
2344
+ blinded.map((d) => t.profile.coverageField(d.field)).join(', '),
2345
+ pct(worst.was),
2346
+ pct(worst.now),
2347
+ ),
2348
+ ),
2349
+ );
2350
+ process.exitCode = 1;
2351
+ } else if (againstDelta > maxGrowth) {
2324
2352
  console.error(c.red(t.profile.maxGrowthUsdFailed(formatSignedUsd(againstDelta), formatUsd(maxGrowth))));
2325
2353
  process.exitCode = 1;
2326
2354
  }
@@ -3643,6 +3671,46 @@ async function commandProfile(args: Args, config: TrazumConfig, pricing: Pricing
3643
3671
  console.log(` ${d.delta > 0 ? c.yellow(line) : c.dim(line)}`);
3644
3672
  }
3645
3673
  }
3674
+
3675
+ /**
3676
+ * What the comparison stopped being able to see.
3677
+ *
3678
+ * Every figure above is dollars, and dollars cannot tell a finding that
3679
+ * was fixed from a finding whose field the log stopped recording — both
3680
+ * are silence. This is the only section that can, so it is loud: a
3681
+ * collapse in coverage invalidates whichever findings depended on it,
3682
+ * and reading the drop as good news is the specific mistake it exists
3683
+ * to prevent.
3684
+ */
3685
+ const drifts = coverageDrift(previous.fieldCoverage, report.fieldCoverage);
3686
+ if (drifts.length > 0) {
3687
+ console.log();
3688
+ for (const drift of drifts) {
3689
+ const line = t.profile.coverageDrift(
3690
+ t.profile.coverageField(drift.field),
3691
+ pct(drift.was),
3692
+ pct(drift.now),
3693
+ );
3694
+ console.log(
3695
+ drift.delta < 0
3696
+ ? ` ${c.yellow('!')} ${c.bold(wrap(line, 74, ' '))}`
3697
+ : ` ${c.dim(wrap(line, 74, ' '))}`,
3698
+ );
3699
+ /**
3700
+ * Which findings went with it, named. "Some findings are silent" is
3701
+ * not something a reader can act on; knowing that conversation
3702
+ * growth and the cache-TTL fit are now silence rather than absence
3703
+ * tells them exactly which sections of this report to distrust.
3704
+ */
3705
+ if (drift.delta < 0) {
3706
+ const silenced = t.profile.coverageSilenced(drift.field);
3707
+ if (silenced !== '') console.log(` ${c.dim(wrap(silenced, 72, ' '))}`);
3708
+ }
3709
+ }
3710
+ if (drifts.some((d) => d.delta < 0)) {
3711
+ console.log(` ${c.dim(wrap(t.profile.coverageDriftWhy(), 74, ' '))}`);
3712
+ }
3713
+ }
3646
3714
  }
3647
3715
  }
3648
3716