@trazum/cli 1.50.3 → 1.50.4
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/dist/i18n/en.d.ts.map +1 -1
- package/dist/i18n/en.js +20 -0
- package/dist/i18n/en.js.map +1 -1
- package/dist/i18n/es.d.ts.map +1 -1
- package/dist/i18n/es.js +20 -0
- package/dist/i18n/es.js.map +1 -1
- package/dist/i18n/types.d.ts +15 -0
- package/dist/i18n/types.d.ts.map +1 -1
- package/dist/index.js +73 -1
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
- package/src/i18n/en.ts +26 -0
- package/src/i18n/es.ts +26 -0
- package/src/i18n/types.ts +11 -0
- package/src/index.ts +101 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@trazum/cli",
|
|
3
|
-
"version": "1.50.
|
|
3
|
+
"version": "1.50.4",
|
|
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.50.
|
|
40
|
+
"@trazum/core": "1.50.4"
|
|
41
41
|
},
|
|
42
42
|
"devDependencies": {
|
|
43
43
|
"@types/node": "^26.2.0",
|
package/src/i18n/en.ts
CHANGED
|
@@ -597,6 +597,13 @@ ${bold('CONFIG FILE')}
|
|
|
597
597
|
spend { "maxUsd": 200, "byLabel": { "chat": 40 } } — money budgets for
|
|
598
598
|
"trazum profile", in dollars. A budgeted label with no calls in
|
|
599
599
|
the log is reported as not measured, never as a pass
|
|
600
|
+
outcomes { "values": ["resolved", "escalated"], "success": ["resolved"] } —
|
|
601
|
+
your own vocabulary for what happened, and which of it counts as
|
|
602
|
+
a win. Both required: which words mean success is a judgement
|
|
603
|
+
about your product rather than your bill, and this tool has no
|
|
604
|
+
standing to make it. Use [] if none of them are successes. A
|
|
605
|
+
value in a log that "values" never declares is named as
|
|
606
|
+
undeclared, never counted as a failure
|
|
600
607
|
waive [{ "gate": "maxUsd", "reason": "August migration", "until":
|
|
601
608
|
"2026-09-15" }] — a gate failure decided about, on the record.
|
|
602
609
|
All three fields required: a waiver with no end date is a
|
|
@@ -1631,6 +1638,25 @@ ${bold('EXAMPLES')}
|
|
|
1631
1638
|
coverageHeading: () => 'What this log cannot answer yet',
|
|
1632
1639
|
needsLabel: (seen) =>
|
|
1633
1640
|
`"label" on ${seen} records: without it every workload is one row, so no per-workload spend, no drill-down, and the levers describe a mixture rather than a decision.`,
|
|
1641
|
+
needsOutcome: (seen) =>
|
|
1642
|
+
`an "outcome" — ${seen}. The one field that changes what every other figure here means: without it this tool can say a workload got 40% cheaper and cannot say whether it stopped working. Record your own word for what happened and declare the vocabulary under "outcomes".`,
|
|
1643
|
+
dryRunOutcomes: (share) =>
|
|
1644
|
+
`cost per outcome and a success rate (${share} of records carry an "outcome")`,
|
|
1645
|
+
outcomeHeading: () => 'Outcomes',
|
|
1646
|
+
outcomeRate: (rate, ofUsd) =>
|
|
1647
|
+
`${rate} of ${ofUsd} in declared outcomes succeeded \u2014 by spend rather than by call, because the two diverge exactly when the expensive half is the half that fails.`,
|
|
1648
|
+
outcomeNoRate: (why) =>
|
|
1649
|
+
why === 'nothing-recorded'
|
|
1650
|
+
? 'No success rate: nothing in this log recorded an outcome. That is not a rate of zero \u2014 a rate of zero is a real and terrible measurement, and this is nobody having told us.'
|
|
1651
|
+
: 'No success rate: "outcomes.success" declares no values, so nothing here counts as one. A legitimate thing to declare, and it means the rate is not this tool\u2019s to compute.',
|
|
1652
|
+
outcomeUnrecorded: (share, usd) =>
|
|
1653
|
+
`${share} of the bill (${usd}) carried no outcome, and is in neither half of the rate above.`,
|
|
1654
|
+
outcomeUndeclared: (values) =>
|
|
1655
|
+
`Not declared in "outcomes.values": ${values}. Named rather than counted as failures \u2014 a typo in an exporter should look like a typo, not like a product regression.`,
|
|
1656
|
+
outcomeColumns: { outcome: 'outcome', calls: 'calls', spend: 'spend' },
|
|
1657
|
+
verdictSuccess: () => 'success',
|
|
1658
|
+
verdictOther: () => '\u2014',
|
|
1659
|
+
verdictUndeclared: () => 'undeclared',
|
|
1634
1660
|
needsSession: (seen) =>
|
|
1635
1661
|
`"session" on ${seen} records: without it there is no conversation growth, no per-conversation cost, and no cache-TTL fit. It is grouped by and never printed.`,
|
|
1636
1662
|
needsTs: (seen) =>
|
package/src/i18n/es.ts
CHANGED
|
@@ -617,6 +617,13 @@ ${bold('FICHERO DE CONFIGURACIÓN')}
|
|
|
617
617
|
spend { "maxUsd": 200, "byLabel": { "chat": 40 } } — presupuestos en
|
|
618
618
|
dólares para "trazum profile". Una etiqueta con presupuesto y sin
|
|
619
619
|
llamadas se informa como no medida, nunca como aprobada
|
|
620
|
+
outcomes { "values": ["resolved", "escalated"], "success": ["resolved"] } —
|
|
621
|
+
tu propio vocabulario para lo que pasó, y cuál de él cuenta como
|
|
622
|
+
acierto. Los dos son obligatorios: qué palabras significan éxito
|
|
623
|
+
es un juicio sobre tu producto y no sobre tu factura, y esta
|
|
624
|
+
herramienta no tiene potestad para hacerlo. Usa [] si ninguna lo
|
|
625
|
+
es. Un valor en un log que "values" no declare se nombra como
|
|
626
|
+
sin declarar, nunca se cuenta como fallo
|
|
620
627
|
waive [{ "gate": "maxUsd", "reason": "migración de agosto", "until":
|
|
621
628
|
"2026-09-15" }] — un fallo de gate sobre el que se ha decidido,
|
|
622
629
|
registrado. Los tres campos son obligatorios: un waiver sin
|
|
@@ -1664,6 +1671,25 @@ ${bold('EJEMPLOS')}
|
|
|
1664
1671
|
coverageHeading: () => 'Lo que este registro todavía no puede responder',
|
|
1665
1672
|
needsLabel: (seen) =>
|
|
1666
1673
|
`"label" en ${seen} registros: sin él todas las cargas son una sola fila, así que no hay gasto por carga, ni zoom, y las palancas describen una mezcla en vez de una decisión.`,
|
|
1674
|
+
needsOutcome: (seen) =>
|
|
1675
|
+
`un "outcome" \u2014 ${seen}. El \u00fanico campo que cambia lo que significa cualquier otra cifra de aqu\u00ed: sin \u00e9l esta herramienta puede decir que una carga baj\u00f3 un 40% y no puede decir si dej\u00f3 de funcionar. Registra tu propia palabra para lo que pas\u00f3 y declara el vocabulario en "outcomes".`,
|
|
1676
|
+
dryRunOutcomes: (share) =>
|
|
1677
|
+
`coste por resultado y una tasa de \u00e9xito (${share} de los registros llevan "outcome")`,
|
|
1678
|
+
outcomeHeading: () => 'Resultados',
|
|
1679
|
+
outcomeRate: (rate, ofUsd) =>
|
|
1680
|
+
`${rate} de ${ofUsd} en resultados declarados tuvo \u00e9xito \u2014 por gasto y no por llamada, porque las dos cifras divergen justo cuando la mitad cara es la que falla.`,
|
|
1681
|
+
outcomeNoRate: (why) =>
|
|
1682
|
+
why === 'nothing-recorded'
|
|
1683
|
+
? 'Sin tasa de \u00e9xito: nada en este log registr\u00f3 un resultado. Eso no es una tasa de cero \u2014 una tasa de cero es una medici\u00f3n real y p\u00e9sima, y esto es que nadie nos lo ha dicho.'
|
|
1684
|
+
: 'Sin tasa de \u00e9xito: "outcomes.success" no declara ning\u00fan valor, as\u00ed que aqu\u00ed nada cuenta como \u00e9xito. Es leg\u00edtimo declararlo as\u00ed, y significa que la tasa no le corresponde calcularla a esta herramienta.',
|
|
1685
|
+
outcomeUnrecorded: (share, usd) =>
|
|
1686
|
+
`${share} de la factura (${usd}) no llev\u00f3 resultado, y no est\u00e1 en ninguna de las dos mitades de la tasa de arriba.`,
|
|
1687
|
+
outcomeUndeclared: (values) =>
|
|
1688
|
+
`No declarados en "outcomes.values": ${values}. Nombrados en vez de contados como fallos \u2014 una errata en un exportador debe parecer una errata, no una regresi\u00f3n del producto.`,
|
|
1689
|
+
outcomeColumns: { outcome: 'resultado', calls: 'llamadas', spend: 'gasto' },
|
|
1690
|
+
verdictSuccess: () => '\u00e9xito',
|
|
1691
|
+
verdictOther: () => '\u2014',
|
|
1692
|
+
verdictUndeclared: () => 'sin declarar',
|
|
1667
1693
|
needsSession: (seen) =>
|
|
1668
1694
|
`"session" en ${seen} registros: sin él no hay crecimiento de conversación, ni coste por conversación, ni encaje del TTL de caché. Se agrupa por él y nunca se imprime.`,
|
|
1669
1695
|
needsTs: (seen) =>
|
package/src/i18n/types.ts
CHANGED
|
@@ -1121,6 +1121,17 @@ export interface CliMessages {
|
|
|
1121
1121
|
coverageHeading(): string;
|
|
1122
1122
|
needsLabel(seen: string): string;
|
|
1123
1123
|
needsSession(seen: string): string;
|
|
1124
|
+
needsOutcome(seen: string): string;
|
|
1125
|
+
dryRunOutcomes(share: string): string;
|
|
1126
|
+
outcomeHeading(): string;
|
|
1127
|
+
outcomeRate(rate: string, ofUsd: string): string;
|
|
1128
|
+
outcomeNoRate(why: string): string;
|
|
1129
|
+
outcomeUnrecorded(share: string, usd: string): string;
|
|
1130
|
+
outcomeUndeclared(values: string): string;
|
|
1131
|
+
outcomeColumns: { outcome: string; calls: string; spend: string };
|
|
1132
|
+
verdictSuccess(): string;
|
|
1133
|
+
verdictOther(): string;
|
|
1134
|
+
verdictUndeclared(): string;
|
|
1124
1135
|
needsTs(seen: string): string;
|
|
1125
1136
|
needsStopReason(seen: string): string;
|
|
1126
1137
|
needsCacheTtl(seen: string): string;
|
package/src/index.ts
CHANGED
|
@@ -39,6 +39,7 @@ import {
|
|
|
39
39
|
DEFAULT_USAGE,
|
|
40
40
|
budgetPositions,
|
|
41
41
|
conform,
|
|
42
|
+
outcomeReport,
|
|
42
43
|
FAILURE_POLICIES,
|
|
43
44
|
detectFromSource,
|
|
44
45
|
matchLocale,
|
|
@@ -4585,6 +4586,7 @@ async function commandProfile(
|
|
|
4585
4586
|
can(cov.label > 0, t.profile.dryRunLabels(share(cov.label)));
|
|
4586
4587
|
can(cov.ts > 0, t.profile.dryRunClock(share(cov.ts)));
|
|
4587
4588
|
can(cov.session > 0, t.profile.dryRunSessions(share(cov.session)));
|
|
4589
|
+
can(cov.outcome > 0, t.profile.dryRunOutcomes(share(cov.outcome)));
|
|
4588
4590
|
can(cov.stopReason > 0, t.profile.dryRunStopReason(share(cov.stopReason)));
|
|
4589
4591
|
// "No cache traffic" is not a missing field: the split can only exist on
|
|
4590
4592
|
// records that wrote, and a log that never wrote has nothing to record.
|
|
@@ -6416,6 +6418,97 @@ async function commandProfile(
|
|
|
6416
6418
|
* section at all, because a paragraph of things that are fine is the
|
|
6417
6419
|
* paragraph readers learn to skip.
|
|
6418
6420
|
*/
|
|
6421
|
+
/**
|
|
6422
|
+
* Outcomes — the counterpart, where somebody recorded one.
|
|
6423
|
+
*
|
|
6424
|
+
* Printed above the coverage section rather than below it, because when this
|
|
6425
|
+
* section is present it is the most valuable thing on the page: every other
|
|
6426
|
+
* figure in this report is a cost, and this is the only one that says what
|
|
6427
|
+
* the money bought.
|
|
6428
|
+
*
|
|
6429
|
+
* Silent when nothing recorded an outcome. The coverage section below
|
|
6430
|
+
* already names the missing field and what it would unlock, and printing an
|
|
6431
|
+
* empty Outcomes heading above it would be the same sentence twice.
|
|
6432
|
+
*/
|
|
6433
|
+
{
|
|
6434
|
+
const outcomes = outcomeReport(report.outcomeTally, config.outcomes ?? null);
|
|
6435
|
+
if (outcomes.coverage.recorded > 0) {
|
|
6436
|
+
console.log();
|
|
6437
|
+
console.log(c.bold(t.profile.outcomeHeading()));
|
|
6438
|
+
|
|
6439
|
+
const col = t.profile.outcomeColumns;
|
|
6440
|
+
const rows = [...outcomes.slices, ...outcomes.undeclared].map((slice) => ({
|
|
6441
|
+
value: slice.value,
|
|
6442
|
+
verdict:
|
|
6443
|
+
slice.verdict === 'success'
|
|
6444
|
+
? t.profile.verdictSuccess()
|
|
6445
|
+
: slice.verdict === 'undeclared'
|
|
6446
|
+
? t.profile.verdictUndeclared()
|
|
6447
|
+
: t.profile.verdictOther(),
|
|
6448
|
+
calls: n(slice.calls),
|
|
6449
|
+
spend: formatUsd(slice.usd),
|
|
6450
|
+
}));
|
|
6451
|
+
const w = {
|
|
6452
|
+
value: Math.max(...rows.map((r) => r.value.length), col.outcome.length),
|
|
6453
|
+
verdict: Math.max(...rows.map((r) => r.verdict.length), 0),
|
|
6454
|
+
calls: Math.max(...rows.map((r) => r.calls.length), col.calls.length),
|
|
6455
|
+
spend: Math.max(...rows.map((r) => r.spend.length), col.spend.length),
|
|
6456
|
+
};
|
|
6457
|
+
console.log(
|
|
6458
|
+
c.dim(
|
|
6459
|
+
` ${col.outcome.padEnd(w.value)} ${''.padEnd(w.verdict)} ` +
|
|
6460
|
+
`${col.calls.padStart(w.calls)} ${col.spend.padStart(w.spend)}`,
|
|
6461
|
+
),
|
|
6462
|
+
);
|
|
6463
|
+
for (const row of rows) {
|
|
6464
|
+
const tint =
|
|
6465
|
+
row.verdict === t.profile.verdictUndeclared()
|
|
6466
|
+
? c.yellow
|
|
6467
|
+
: row.verdict === t.profile.verdictSuccess()
|
|
6468
|
+
? c.green
|
|
6469
|
+
: c.dim;
|
|
6470
|
+
console.log(
|
|
6471
|
+
` ${row.value.padEnd(w.value)} ${tint(row.verdict.padEnd(w.verdict))} ` +
|
|
6472
|
+
`${row.calls.padStart(w.calls)} ${row.spend.padStart(w.spend)}`,
|
|
6473
|
+
);
|
|
6474
|
+
}
|
|
6475
|
+
|
|
6476
|
+
console.log();
|
|
6477
|
+
if (outcomes.successShareOfRecordedUsd !== null) {
|
|
6478
|
+
const declaredUsd = outcomes.slices.reduce((sum, slice) => sum + slice.usd, 0);
|
|
6479
|
+
console.log(
|
|
6480
|
+
` ${wrap(t.profile.outcomeRate(pct(outcomes.successShareOfRecordedUsd), formatUsd(declaredUsd)), 74, ' ')}`,
|
|
6481
|
+
);
|
|
6482
|
+
} else if (outcomes.noRate !== null) {
|
|
6483
|
+
console.log(` ${c.dim(wrap(t.profile.outcomeNoRate(outcomes.noRate), 74, ' '))}`);
|
|
6484
|
+
}
|
|
6485
|
+
|
|
6486
|
+
// What the rate does not cover, every time it is printed. A rate over a
|
|
6487
|
+
// twelfth of the bill is a rate about a twelfth of the bill.
|
|
6488
|
+
if (outcomes.coverage.unrecordedUsd > 0 && report.total.totalUsd > 0) {
|
|
6489
|
+
console.log(
|
|
6490
|
+
` ${c.yellow('!')} ${wrap(
|
|
6491
|
+
t.profile.outcomeUnrecorded(
|
|
6492
|
+
pct(outcomes.coverage.unrecordedUsd / report.total.totalUsd),
|
|
6493
|
+
formatUsd(outcomes.coverage.unrecordedUsd),
|
|
6494
|
+
),
|
|
6495
|
+
74,
|
|
6496
|
+
' ',
|
|
6497
|
+
)}`,
|
|
6498
|
+
);
|
|
6499
|
+
}
|
|
6500
|
+
if (outcomes.undeclared.length > 0) {
|
|
6501
|
+
console.log(
|
|
6502
|
+
` ${c.yellow('!')} ${wrap(
|
|
6503
|
+
t.profile.outcomeUndeclared(outcomes.undeclared.map((s) => s.value).join(', ')),
|
|
6504
|
+
74,
|
|
6505
|
+
' ',
|
|
6506
|
+
)}`,
|
|
6507
|
+
);
|
|
6508
|
+
}
|
|
6509
|
+
}
|
|
6510
|
+
}
|
|
6511
|
+
|
|
6419
6512
|
const coverage = report.fieldCoverage;
|
|
6420
6513
|
if (coverage.parsed > 0) {
|
|
6421
6514
|
const missing: string[] = [];
|
|
@@ -6426,6 +6519,14 @@ async function commandProfile(
|
|
|
6426
6519
|
if (coverage.session < coverage.parsed) {
|
|
6427
6520
|
missing.push(t.profile.needsSession(partial(coverage.session)));
|
|
6428
6521
|
}
|
|
6522
|
+
/**
|
|
6523
|
+
* Listed first among the missing when it is missing entirely, because it
|
|
6524
|
+
* is the one field that changes what every other figure here *means*. The
|
|
6525
|
+
* rest sharpen a cost; this one gives it a counterpart.
|
|
6526
|
+
*/
|
|
6527
|
+
if (coverage.outcome < coverage.parsed) {
|
|
6528
|
+
missing.push(t.profile.needsOutcome(partial(coverage.outcome)));
|
|
6529
|
+
}
|
|
6429
6530
|
if (coverage.ts < coverage.parsed) {
|
|
6430
6531
|
missing.push(t.profile.needsTs(partial(coverage.ts)));
|
|
6431
6532
|
}
|