@trazum/cli 1.28.0 → 1.29.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.28.0",
3
+ "version": "1.29.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.28.0"
40
+ "@trazum/core": "1.29.0"
41
41
  },
42
42
  "devDependencies": {
43
43
  "@types/node": "^26.2.0",
package/src/i18n/en.ts CHANGED
@@ -234,6 +234,10 @@ ${bold('OPTIONS FOR profile')}
234
234
  afternoon a loop burned a quarter of it. A log
235
235
  with no timestamps fails: not measured is not
236
236
  under budget.
237
+ --max-session-usd <n> Fail when any single conversation in the log
238
+ cost more than this — the unit an agent product
239
+ blows up in. A log with no sessions fails: not
240
+ measured is not under budget.
237
241
  --since <when> Profile only calls at or after this moment. A UTC
238
242
  --until <when> day (2026-08-14), a full ISO 8601 timestamp, a
239
243
  relative window (7d, 24h) or "now";
@@ -1081,6 +1085,12 @@ ${bold('EXAMPLES')}
1081
1085
  `FAILED — ${day} spent ${usd}, over the --max-day-usd limit of ${max}. A total under budget can hide a single runaway day, which is what this gate exists to catch.`,
1082
1086
  maxDayNoClock: () =>
1083
1087
  'FAILED — --max-day-usd was asked for and no record in this log carries a timestamp, so there are no days to judge. That is not a pass: a bill nobody could measure by day is not a bill that stayed under a daily budget. Add "ts" to the record and the gate arms.',
1088
+ maxSessionOk: (worst, max, sessions) =>
1089
+ `No conversation over budget: the most expensive of ${sessions} cost ${worst}, against --max-session-usd ${max}. A conversation that started before this log is counted only for the turns recorded here, so this is a floor.`,
1090
+ maxSessionFailed: (worst, max, sessions) =>
1091
+ `FAILED — the most expensive of ${sessions} conversations cost ${worst}, over the --max-session-usd limit of ${max}. A month's budget and a day's budget both pass while one conversation loops its way through this; the per-conversation figure is the one that catches it.`,
1092
+ maxSessionNoSessions: () =>
1093
+ 'FAILED — --max-session-usd was asked for and no record in this log carries a session, so there are no conversations to judge. That is not a pass. Add "session" (or "conversation_id") to the record and the gate arms; Trazum groups by it and never prints it.',
1084
1094
  maxDayUndated: (calls) =>
1085
1095
  `${calls} calls carry no timestamp, so they are in the bill and in none of the days above — the worst day is a floor by whatever they held. A failure would stand regardless; this pass is over the part that could be dated.`,
1086
1096
  maxCacheLossWorstCase: (calls, worst, max) =>
@@ -1161,6 +1171,8 @@ ${bold('EXAMPLES')}
1161
1171
  `No usage logs in "${directory}". Looked for files ending in ${extensions}. A directory with nothing readable in it is an error rather than an empty report, which would read as "you spent nothing".`,
1162
1172
  sessionCostTail: (ratio) =>
1163
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
+ sessionSpendOnly: (sessions, max) =>
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.`,
1164
1176
  againstOverlap: (from, to) =>
1165
1177
  `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.`,
1166
1178
  windowLine: (since, until) =>
package/src/i18n/es.ts CHANGED
@@ -232,6 +232,10 @@ ${bold('OPCIONES DE profile')}
232
232
  una cuarta parte. Un registro sin marcas de
233
233
  tiempo falla: no medido no es dentro de
234
234
  presupuesto.
235
+ --max-session-usd <n> Falla cuando una sola conversación del registro
236
+ costó más que esto — la unidad en la que revienta
237
+ un producto de agentes. Un registro sin sesiones
238
+ falla: no medido no es dentro de presupuesto.
235
239
  --since <cuándo> Perfila solo llamadas desde/hasta ese momento. Un
236
240
  --until <cuándo> día UTC (2026-08-14), una marca ISO 8601 completa,
237
241
  una ventana relativa (7d, 24h) o "now";
@@ -1095,6 +1099,12 @@ ${bold('EJEMPLOS')}
1095
1099
  `FALLO — ${day} gastó ${usd}, por encima del límite --max-day-usd de ${max}. Un total dentro de presupuesto puede esconder un solo día desbocado, que es justo lo que vigila esta puerta.`,
1096
1100
  maxDayNoClock: () =>
1097
1101
  'FALLO — se pidió --max-day-usd y ningún registro de este fichero lleva marca de tiempo, así que no hay días que juzgar. Eso no es un aprobado: una factura que nadie pudo medir por días no es una factura que se mantuvo bajo un presupuesto diario. Añade "ts" al registro y la puerta se arma.',
1102
+ maxSessionOk: (worst, max, sessions) =>
1103
+ `Ninguna conversación por encima del presupuesto: la más cara de ${sessions} costó ${worst}, contra --max-session-usd ${max}. Una conversación que empezó antes de este registro solo cuenta los turnos grabados aquí, así que esto es un suelo.`,
1104
+ maxSessionFailed: (worst, max, sessions) =>
1105
+ `FALLO — la más cara de ${sessions} conversaciones costó ${worst}, por encima del límite --max-session-usd de ${max}. El presupuesto del mes y el del día aprueban mientras una conversación en bucle se come esto; la cifra por conversación es la que lo caza.`,
1106
+ maxSessionNoSessions: () =>
1107
+ 'FALLO — se pidió --max-session-usd y ningún registro lleva sesión, así que no hay conversaciones que juzgar. Eso no es un aprobado. Añade "session" (o "conversation_id") al registro y la puerta se arma; Trazum agrupa por ella y nunca la imprime.',
1098
1108
  maxDayUndated: (calls) =>
1099
1109
  `${calls} llamadas no llevan marca de tiempo, así que están en la factura y en ninguno de los días de arriba — el peor día es un suelo por lo que valieran esas llamadas. Un fallo se sostendría igual; este aprobado cubre la parte que se pudo fechar.`,
1100
1110
  maxCacheLossWorstCase: (calls, worst, max) =>
@@ -1176,6 +1186,8 @@ ${bold('EJEMPLOS')}
1176
1186
  `No hay registros de uso en "${directory}". Se buscaron ficheros terminados en ${extensions}. Un directorio sin nada legible es un error, no un informe vacío, que se leería como "no has gastado nada".`,
1177
1187
  sessionCostTail: (ratio) =>
1178
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
+ sessionSpendOnly: (sessions, max) =>
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.`,
1179
1191
  againstOverlap: (from, to) =>
1180
1192
  `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.`,
1181
1193
  windowLine: (since, until) =>
package/src/i18n/types.ts CHANGED
@@ -730,6 +730,15 @@ export interface CliMessages {
730
730
  maxDayFailed(day: string, usd: string, max: string): string;
731
731
  maxDayNoClock(): string;
732
732
  maxDayUndated(calls: string): string;
733
+ /**
734
+ * The per-conversation gate. The single most expensive conversation is
735
+ * the number a per-conversation policy judges; a log with no sessions
736
+ * fails, and a conversation that started before the log makes the pass
737
+ * a floor — said in the pass message. The session key never appears.
738
+ */
739
+ maxSessionOk(worst: string, max: string, sessions: string): string;
740
+ maxSessionFailed(worst: string, max: string, sessions: string): string;
741
+ maxSessionNoSessions(): string;
733
742
  /**
734
743
  * The price table behind every dollar in the report, when it is old
735
744
  * enough to matter. Unlike a skipped line, staleness does not name its
@@ -777,6 +786,13 @@ export interface CliMessages {
777
786
  ): string;
778
787
  /** Said only when the p95 clears ten times the median — a real tail. */
779
788
  sessionCostTail(ratio: string): string;
789
+ /**
790
+ * The whole log's conversations when the per-slice percentiles refused:
791
+ * count and single worst cost, stated because a maximum is a fact at any
792
+ * count while a percentile over four conversations would be the largest
793
+ * of four wearing a percentile's name.
794
+ */
795
+ sessionSpendOnly(sessions: string, max: string): string;
780
796
  /**
781
797
  * Per-workload budgets from the config. A budgeted label with no calls in
782
798
  * the log is "not measured", never a pass: a workload that did not appear
package/src/index.ts CHANGED
@@ -180,6 +180,7 @@ const VALUE_FLAGS = new Set([
180
180
  'max-growth-usd',
181
181
  'max-cache-loss-usd',
182
182
  'max-day-usd',
183
+ 'max-session-usd',
183
184
  'csv-out',
184
185
  'csv-shape',
185
186
  'what-if',
@@ -435,7 +436,7 @@ const COMMAND_FLAGS: Record<string, string[]> = {
435
436
  ],
436
437
  check: ['max-tokens', 'level', 'exact-tokens', 'markdown-out', 'baseline'],
437
438
  baseline: ['model', 'calls', 'output-tokens', 'cache-hit-rate', 'batch', 'exact-tokens', 'out', 'o'],
438
- profile: ['json', 'pricing', 'pricing-live', 'against', 'what-if', 'markdown-out', 'csv-out', 'csv-shape', 'max-usd', 'max-growth-usd', 'max-cache-loss-usd', 'max-day-usd', 'label', 'since', 'until'],
439
+ profile: ['json', 'pricing', 'pricing-live', 'against', 'what-if', 'markdown-out', 'csv-out', 'csv-shape', 'max-usd', 'max-growth-usd', 'max-cache-loss-usd', 'max-day-usd', 'max-session-usd', 'label', 'since', 'until'],
439
440
  route: ['prompt-file', 'cases', 'label', 'concurrency', 'json', 'yes', 'pricing', 'pricing-live'],
440
441
  eval: ['cases', 'level', 'concurrency', 'export', 'out', 'o', 'model'],
441
442
  prune: ['cases', 'concurrency', 'json', 'yes'],
@@ -2262,6 +2263,7 @@ async function commandProfile(args: Args, config: TrazumConfig, pricing: Pricing
2262
2263
  typeof args.flags.get('max-growth-usd') === 'string' ||
2263
2264
  typeof args.flags.get('max-cache-loss-usd') === 'string' ||
2264
2265
  typeof args.flags.get('max-day-usd') === 'string' ||
2266
+ typeof args.flags.get('max-session-usd') === 'string' ||
2265
2267
  config.spend !== undefined;
2266
2268
  if (anyGate) {
2267
2269
  const reasons: string[] = [];
@@ -2412,6 +2414,38 @@ async function commandProfile(args: Args, config: TrazumConfig, pricing: Pricing
2412
2414
  }
2413
2415
  }
2414
2416
  }
2417
+ /**
2418
+ * The per-conversation gate — the unit an agent product actually blows
2419
+ * up in. A month's budget and a day's budget both pass while one
2420
+ * conversation loops its way through $400; the single most expensive
2421
+ * conversation is the number a per-conversation policy has to judge,
2422
+ * and the log already carries it.
2423
+ *
2424
+ * The refusals it inherits: a log with **no sessions** fails rather
2425
+ * than passes ("not measured" is not "under budget"), and a
2426
+ * conversation that started before this log is counted only for the
2427
+ * turns recorded here — so a pass is a floor, and the pass message says
2428
+ * so. The session key itself is never printed, here or anywhere.
2429
+ */
2430
+ if (typeof args.flags.get('max-session-usd') === 'string' || config.spend?.maxSessionUsd !== undefined) {
2431
+ const maxSession =
2432
+ typeof args.flags.get('max-session-usd') === 'string'
2433
+ ? numberFlag(args, 'max-session-usd', 0, t)
2434
+ : config.spend!.maxSessionUsd!;
2435
+ if (report.sessionSpend === null) {
2436
+ console.error(c.red(t.profile.maxSessionNoSessions()));
2437
+ process.exitCode = 1;
2438
+ } else if (report.sessionSpend.maxUsd > maxSession) {
2439
+ console.error(
2440
+ c.red(t.profile.maxSessionFailed(formatUsd(report.sessionSpend.maxUsd), formatUsd(maxSession), n(report.sessionSpend.sessions))),
2441
+ );
2442
+ process.exitCode = 1;
2443
+ } else {
2444
+ console.error(
2445
+ c.dim(t.profile.maxSessionOk(formatUsd(report.sessionSpend.maxUsd), formatUsd(maxSession), n(report.sessionSpend.sessions))),
2446
+ );
2447
+ }
2448
+ }
2415
2449
  };
2416
2450
 
2417
2451
  /**
@@ -3070,6 +3104,19 @@ async function commandProfile(args: Args, config: TrazumConfig, pricing: Pricing
3070
3104
  );
3071
3105
  }
3072
3106
  }
3107
+ /**
3108
+ * The figure that survives a small log. `sessionCosts` refuses slices too
3109
+ * thin for a percentile, and rightly — but a log of four conversations
3110
+ * still has a most expensive one, and that maximum is a fact at any count.
3111
+ * It is also exactly the number `--max-session-usd` judges, so the report
3112
+ * states it rather than going silent where the gate would speak.
3113
+ */
3114
+ if (report.sessionCosts.length === 0 && report.sessionSpend !== null) {
3115
+ console.log();
3116
+ console.log(
3117
+ ` ${c.dim(wrap(t.profile.sessionSpendOnly(n(report.sessionSpend.sessions), formatUsd(report.sessionSpend.maxUsd)), 74, ' '))}`,
3118
+ );
3119
+ }
3073
3120
 
3074
3121
  /**
3075
3122
  * A total that assumed a cache-write rate is a floor, and says so.