@trazum/cli 1.38.0 → 1.39.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/README.md +1 -0
- package/dist/i18n/en.d.ts.map +1 -1
- package/dist/i18n/en.js +58 -0
- package/dist/i18n/en.js.map +1 -1
- package/dist/i18n/es.d.ts.map +1 -1
- package/dist/i18n/es.js +60 -0
- package/dist/i18n/es.js.map +1 -1
- package/dist/i18n/types.d.ts +32 -1
- package/dist/i18n/types.d.ts.map +1 -1
- package/dist/index.js +118 -1
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
- package/src/i18n/en.ts +75 -0
- package/src/i18n/es.ts +77 -0
- package/src/i18n/types.ts +33 -1
- package/src/index.ts +141 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@trazum/cli",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.39.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.
|
|
40
|
+
"@trazum/core": "1.39.0"
|
|
41
41
|
},
|
|
42
42
|
"devDependencies": {
|
|
43
43
|
"@types/node": "^26.2.0",
|
package/src/i18n/en.ts
CHANGED
|
@@ -40,6 +40,7 @@ ${bold('USAGE')}
|
|
|
40
40
|
trazum eval <file> --cases <file> --export promptfoo -o suite.json
|
|
41
41
|
trazum route <log.jsonl> --prompt-file <file> --cases <file> --yes
|
|
42
42
|
trazum plan <log.jsonl|dir> [options]
|
|
43
|
+
trazum verify <plan.json> --against <newer.jsonl|dir> [options]
|
|
43
44
|
trazum diff <before> <after> [options]
|
|
44
45
|
trazum diff --all <dir> <dir> [options]
|
|
45
46
|
trazum rank <dir> [options]
|
|
@@ -317,6 +318,25 @@ ${bold('OPTIONS FOR plan')}
|
|
|
317
318
|
a plan that hides its assumptions is advice pretending to be arithmetic.
|
|
318
319
|
Projected savings and money already spent are separate totals throughout.
|
|
319
320
|
|
|
321
|
+
${bold('OPTIONS FOR verify')}
|
|
322
|
+
--against <log|dir> The newer usage log the plan is held to. Required.
|
|
323
|
+
--gate Exit 1 when an action did not produce what the
|
|
324
|
+
plan promised, or its fields stopped being
|
|
325
|
+
recorded — "not recorded" must not read as
|
|
326
|
+
"fixed". A vanished workload fails nothing.
|
|
327
|
+
--markdown-out <file> Also write the verdicts as Markdown, for a CI
|
|
328
|
+
job summary or a pull request comment.
|
|
329
|
+
--pricing <file> Local price overlay, as everywhere else.
|
|
330
|
+
--json The verification as data.
|
|
331
|
+
|
|
332
|
+
Holds a saved plan to the log that came after it, with three outcomes and
|
|
333
|
+
never two: the change arrived, it did not, or it cannot be told — because
|
|
334
|
+
the workload vanished, the fields the detection needs stopped being
|
|
335
|
+
recorded, or tokens cannot say which tier billed them. Differences carry
|
|
336
|
+
the world's measured movement (calls, output per call) from the plan's own
|
|
337
|
+
baseline, and a plan priced under a different catalogue says so rather
|
|
338
|
+
than blaming a team for a saving that arithmetic revoked.
|
|
339
|
+
|
|
320
340
|
${bold('OPTIONS FOR route')}
|
|
321
341
|
--prompt-file <file> The prompt those calls send. Not --prompt, which
|
|
322
342
|
names a marked prompt inside a source file.
|
|
@@ -1412,6 +1432,61 @@ ${bold('EXAMPLES')}
|
|
|
1412
1432
|
`Plan written to ${path}, dated. Keep it: a prediction nobody wrote down is a prediction nobody can be held to.`,
|
|
1413
1433
|
},
|
|
1414
1434
|
|
|
1435
|
+
verify: {
|
|
1436
|
+
noTarget: () =>
|
|
1437
|
+
'Point this at a saved plan and a newer log: trazum verify plan.json --against usage.jsonl. It says, per action, whether the change arrived, did not arrive, or cannot be told — and never fewer than those three.',
|
|
1438
|
+
needsAgainst: () =>
|
|
1439
|
+
'--against <newer.jsonl|dir> is required. A plan can only be verified against a log that came after it; without one there is nothing to hold the prediction to.',
|
|
1440
|
+
badPlan: (path) =>
|
|
1441
|
+
`${path} is not a plan document this tool can verify — expected the JSON that "trazum plan -o" writes (schemaVersion 1, with an actions array).`,
|
|
1442
|
+
heading: (actions, planDate) =>
|
|
1443
|
+
planDate === null
|
|
1444
|
+
? `Did it work? ${actions} actions from an undated plan, against this log`
|
|
1445
|
+
: `Did it work? ${actions} actions from the plan of ${planDate}, against this log`,
|
|
1446
|
+
counts: (arrived, notArrived, cannotTell) =>
|
|
1447
|
+
`${arrived} arrived · ${notArrived} did not arrive · ${cannotTell} cannot be told. The third is not a soft version of the second: it means this log cannot answer, which is its own finding.`,
|
|
1448
|
+
pricesChanged: (planReviewed, nowReviewed) =>
|
|
1449
|
+
`Prices were reviewed ${planReviewed} when the plan was made and ${nowReviewed} now, so every dollar comparison here is two price lists, not one measurement — a team must not be blamed for a saving that arithmetic revoked.`,
|
|
1450
|
+
action: (kind, label, model, outcome) => {
|
|
1451
|
+
const what =
|
|
1452
|
+
kind === 'route'
|
|
1453
|
+
? `Route ${label} (${model})`
|
|
1454
|
+
: kind === 'batch'
|
|
1455
|
+
? `Batch ${label} (${model})`
|
|
1456
|
+
: kind === 'route+batch'
|
|
1457
|
+
? `Route and batch ${label} (${model})`
|
|
1458
|
+
: kind === 'fix-truncation'
|
|
1459
|
+
? `Fix the truncation retries on ${label} (${model})`
|
|
1460
|
+
: `Fix the cache on ${label} (${model})`;
|
|
1461
|
+
const verdict =
|
|
1462
|
+
outcome === 'arrived' ? 'ARRIVED' : outcome === 'not-arrived' ? 'DID NOT ARRIVE' : 'CANNOT BE TOLD';
|
|
1463
|
+
return `${what} — ${verdict}`;
|
|
1464
|
+
},
|
|
1465
|
+
reason: (reason) =>
|
|
1466
|
+
reason === 'workload-vanished'
|
|
1467
|
+
? 'the label carries no priced traffic in this log — a vanished workload is not a fixed one, and not a broken one either'
|
|
1468
|
+
: reason === 'fields-stopped'
|
|
1469
|
+
? 'the fields the detection needs are not in this log — "not recorded" must not read as "fixed", so with --gate this fails'
|
|
1470
|
+
: 'the log records tokens, and tokens do not say which tier billed them — the Batch API cannot be seen from here',
|
|
1471
|
+
routeObserved: (dearestModel, onTargetUsd, onOldUsd) =>
|
|
1472
|
+
`the label's dearest model is now ${dearestModel} · ${onTargetUsd} on the target, ${onOldUsd} still on the old model`,
|
|
1473
|
+
batchUnobservable: () =>
|
|
1474
|
+
'the batch half of this action cannot be seen in token counts; the verdict above is the route half alone',
|
|
1475
|
+
truncationObserved: (retryBillUsd) =>
|
|
1476
|
+
`this log still shows ${retryBillUsd} of truncation waste and retries`,
|
|
1477
|
+
cacheObserved: (deltaUsd, outcome) =>
|
|
1478
|
+
outcome === 'arrived'
|
|
1479
|
+
? `caching now pays for itself on this slice (${deltaUsd} against the no-cache bill)`
|
|
1480
|
+
: `caching still adds ${deltaUsd} to this slice's bill`,
|
|
1481
|
+
attribution: (callsBefore, callsAfter, outBefore, outAfter) =>
|
|
1482
|
+
`the world moved too: calls ${callsBefore} → ${callsAfter}, output/call ${outBefore} → ${outAfter} tokens — stated so the verdict is not read as the whole story`,
|
|
1483
|
+
gateFailed: (failures, total) =>
|
|
1484
|
+
`GATE FAILED — ${failures} of ${total} actions did not produce what the plan promised, or stopped being measurable by the team's own log.`,
|
|
1485
|
+
gateOk: () => 'Gate passed: every verifiable action arrived, and nothing became unverifiable.',
|
|
1486
|
+
footer: () =>
|
|
1487
|
+
'Arrived and did-not-arrive are measurements; cannot-be-told is the log refusing to guess. All three are the verification working, not failing.',
|
|
1488
|
+
},
|
|
1489
|
+
|
|
1415
1490
|
route: {
|
|
1416
1491
|
noTarget: () =>
|
|
1417
1492
|
'Point this at a usage log and a prompt: trazum route usage.jsonl --prompt-file prompts/support.txt --cases cases.txt --yes. It finds the slice worth the most, then measures whether the cheaper model still does the job. The flag is --prompt-file and not --prompt, because --prompt names a marked prompt inside a source file everywhere else in this tool.',
|
package/src/i18n/es.ts
CHANGED
|
@@ -27,6 +27,7 @@ ${bold('USO')}
|
|
|
27
27
|
trazum eval <fichero> --cases <fichero> --export promptfoo -o suite.json
|
|
28
28
|
trazum route <log.jsonl> --prompt-file <fichero> --cases <fichero> --yes
|
|
29
29
|
trazum plan <log.jsonl|dir> [opciones]
|
|
30
|
+
trazum verify <plan.json> --against <nuevo.jsonl|dir> [opciones]
|
|
30
31
|
trazum diff <antes> <después> [opciones]
|
|
31
32
|
trazum diff --all <dir> <dir> [opciones]
|
|
32
33
|
trazum rank <dir> [opciones]
|
|
@@ -324,6 +325,27 @@ ${bold('OPCIONES DE plan')}
|
|
|
324
325
|
consejo haciéndose pasar por aritmética. El ahorro proyectado y el dinero ya
|
|
325
326
|
gastado son totales separados en todas partes.
|
|
326
327
|
|
|
328
|
+
${bold('OPCIONES DE verify')}
|
|
329
|
+
--against <log|dir> El registro de uso posterior al que se somete el
|
|
330
|
+
plan. Obligatorio.
|
|
331
|
+
--gate Sale con 1 cuando una acción no produjo lo que el
|
|
332
|
+
plan prometió, o sus campos dejaron de
|
|
333
|
+
registrarse — "no registrado" no puede leerse
|
|
334
|
+
como "arreglado". Una carga de trabajo
|
|
335
|
+
desaparecida no suspende nada.
|
|
336
|
+
--markdown-out <fichero> Escribe además los veredictos como Markdown, para
|
|
337
|
+
un resumen de CI o un comentario de pull request.
|
|
338
|
+
--pricing <fichero> Tarifas locales superpuestas, como en el resto.
|
|
339
|
+
--json La verificación como datos.
|
|
340
|
+
|
|
341
|
+
Somete un plan guardado al registro que vino después, con tres resultados y
|
|
342
|
+
nunca dos: el cambio llegó, no llegó, o no se puede saber — porque la carga
|
|
343
|
+
desapareció, los campos que la detección necesita dejaron de registrarse, o
|
|
344
|
+
los tokens no dicen con qué tarifa se facturaron. Las diferencias llevan el
|
|
345
|
+
movimiento medido del mundo (llamadas, salida por llamada) desde la línea
|
|
346
|
+
base del propio plan, y un plan tasado con otro catálogo lo dice en vez de
|
|
347
|
+
culpar a un equipo por un ahorro que la aritmética revocó.
|
|
348
|
+
|
|
327
349
|
${bold('OPCIONES DE route')}
|
|
328
350
|
--prompt-file <fichero> El prompt que mandan esas llamadas. No --prompt,
|
|
329
351
|
que nombra un prompt marcado dentro de un fuente.
|
|
@@ -1432,6 +1454,61 @@ ${bold('EJEMPLOS')}
|
|
|
1432
1454
|
`Plan escrito en ${path}, con fecha. Guárdalo: una predicción que nadie apuntó es una predicción que no se le puede exigir a nadie.`,
|
|
1433
1455
|
},
|
|
1434
1456
|
|
|
1457
|
+
verify: {
|
|
1458
|
+
noTarget: () =>
|
|
1459
|
+
'Apunta esto a un plan guardado y a un registro posterior: trazum verify plan.json --against usage.jsonl. Dice, por acción, si el cambio llegó, no llegó o no se puede saber — y nunca menos de esos tres.',
|
|
1460
|
+
needsAgainst: () =>
|
|
1461
|
+
'--against <nuevo.jsonl|dir> es obligatorio. Un plan solo puede verificarse contra un registro posterior; sin uno no hay nada a lo que someter la predicción.',
|
|
1462
|
+
badPlan: (path) =>
|
|
1463
|
+
`${path} no es un documento de plan que esta herramienta pueda verificar — se esperaba el JSON que escribe "trazum plan -o" (schemaVersion 1, con un array actions).`,
|
|
1464
|
+
heading: (actions, planDate) =>
|
|
1465
|
+
planDate === null
|
|
1466
|
+
? `¿Funcionó? ${actions} acciones de un plan sin fecha, contra este registro`
|
|
1467
|
+
: `¿Funcionó? ${actions} acciones del plan del ${planDate}, contra este registro`,
|
|
1468
|
+
counts: (arrived, notArrived, cannotTell) =>
|
|
1469
|
+
`${arrived} llegaron · ${notArrived} no llegaron · ${cannotTell} no se puede saber. Lo tercero no es una versión suave de lo segundo: significa que este registro no puede responder, y eso es un hallazgo en sí.`,
|
|
1470
|
+
pricesChanged: (planReviewed, nowReviewed) =>
|
|
1471
|
+
`Las tarifas se revisaron el ${planReviewed} cuando se hizo el plan y el ${nowReviewed} ahora, así que cada comparación en dólares aquí son dos listas de precios, no una medición — no se puede culpar a un equipo por un ahorro que la aritmética revocó.`,
|
|
1472
|
+
action: (kind, label, model, outcome) => {
|
|
1473
|
+
const what =
|
|
1474
|
+
kind === 'route'
|
|
1475
|
+
? `Enrutar ${label} (${model})`
|
|
1476
|
+
: kind === 'batch'
|
|
1477
|
+
? `Agrupar en batch ${label} (${model})`
|
|
1478
|
+
: kind === 'route+batch'
|
|
1479
|
+
? `Enrutar y agrupar ${label} (${model})`
|
|
1480
|
+
: kind === 'fix-truncation'
|
|
1481
|
+
? `Arreglar los reintentos por truncado de ${label} (${model})`
|
|
1482
|
+
: `Arreglar la caché de ${label} (${model})`;
|
|
1483
|
+
const verdict =
|
|
1484
|
+
outcome === 'arrived' ? 'LLEGÓ' : outcome === 'not-arrived' ? 'NO LLEGÓ' : 'NO SE PUEDE SABER';
|
|
1485
|
+
return `${what} — ${verdict}`;
|
|
1486
|
+
},
|
|
1487
|
+
reason: (reason) =>
|
|
1488
|
+
reason === 'workload-vanished'
|
|
1489
|
+
? 'el label no lleva tráfico tasado en este registro — una carga desaparecida no está arreglada, y tampoco rota'
|
|
1490
|
+
: reason === 'fields-stopped'
|
|
1491
|
+
? 'los campos que la detección necesita no están en este registro — "no registrado" no puede leerse como "arreglado", así que con --gate esto suspende'
|
|
1492
|
+
: 'el registro guarda tokens, y los tokens no dicen con qué tarifa se facturaron — el Batch API no se puede ver desde aquí',
|
|
1493
|
+
routeObserved: (dearestModel, onTargetUsd, onOldUsd) =>
|
|
1494
|
+
`el modelo más caro del label ahora es ${dearestModel} · ${onTargetUsd} en el destino, ${onOldUsd} todavía en el modelo antiguo`,
|
|
1495
|
+
batchUnobservable: () =>
|
|
1496
|
+
'la mitad batch de esta acción no se puede ver en los recuentos de tokens; el veredicto de arriba es solo la mitad de la ruta',
|
|
1497
|
+
truncationObserved: (retryBillUsd) =>
|
|
1498
|
+
`este registro todavía muestra ${retryBillUsd} de desperdicio y reintentos por truncado`,
|
|
1499
|
+
cacheObserved: (deltaUsd, outcome) =>
|
|
1500
|
+
outcome === 'arrived'
|
|
1501
|
+
? `la caché ahora se paga sola en esta porción (${deltaUsd} contra la factura sin caché)`
|
|
1502
|
+
: `la caché todavía añade ${deltaUsd} a la factura de esta porción`,
|
|
1503
|
+
attribution: (callsBefore, callsAfter, outBefore, outAfter) =>
|
|
1504
|
+
`el mundo también se movió: llamadas ${callsBefore} → ${callsAfter}, salida/llamada ${outBefore} → ${outAfter} tokens — dicho para que el veredicto no se lea como toda la historia`,
|
|
1505
|
+
gateFailed: (failures, total) =>
|
|
1506
|
+
`GATE SUSPENDIDO — ${failures} de ${total} acciones no produjeron lo que el plan prometió, o dejaron de poder medirse por el propio registro del equipo.`,
|
|
1507
|
+
gateOk: () => 'Gate superado: toda acción verificable llegó, y nada se volvió inverificable.',
|
|
1508
|
+
footer: () =>
|
|
1509
|
+
'Llegó y no-llegó son mediciones; no-se-puede-saber es el registro negándose a adivinar. Los tres son la verificación funcionando, no fallando.',
|
|
1510
|
+
},
|
|
1511
|
+
|
|
1435
1512
|
route: {
|
|
1436
1513
|
noTarget: () =>
|
|
1437
1514
|
'Apunta esto a un registro de uso y a un prompt: trazum route usage.jsonl --prompt-file prompts/soporte.txt --cases casos.txt --yes. Busca la porción que más vale y mide si el modelo más barato sigue haciendo el trabajo. El flag es --prompt-file y no --prompt, porque en el resto de la herramienta --prompt nombra un prompt marcado dentro de un fichero fuente.',
|
package/src/i18n/types.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { EvalVerdict, Locale, PlanActionKind, PlanAssumption, RuleLevel } from '@trazum/core';
|
|
1
|
+
import type { CannotTellReason, EvalVerdict, Locale, PlanActionKind, PlanAssumption, RuleLevel, VerifyOutcome } from '@trazum/core';
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* The CLI's own message catalogue.
|
|
@@ -1072,6 +1072,38 @@ export interface CliMessages {
|
|
|
1072
1072
|
wrote(path: string): string;
|
|
1073
1073
|
};
|
|
1074
1074
|
|
|
1075
|
+
/**
|
|
1076
|
+
* `trazum verify` — the plan held to the log that came after it.
|
|
1077
|
+
*
|
|
1078
|
+
* Three outcomes and never two: arrived, did not arrive, cannot be told.
|
|
1079
|
+
* The third is the honest one, and the one every other tool renders as
|
|
1080
|
+
* the first. Differences carry the world's measured movement, and a plan
|
|
1081
|
+
* priced under another catalogue says so rather than blaming a team for
|
|
1082
|
+
* a saving that arithmetic revoked.
|
|
1083
|
+
*/
|
|
1084
|
+
verify: {
|
|
1085
|
+
noTarget(): string;
|
|
1086
|
+
needsAgainst(): string;
|
|
1087
|
+
/** Not a plan document: wrong shape, wrong version, or not JSON at all. */
|
|
1088
|
+
badPlan(path: string): string;
|
|
1089
|
+
heading(actions: string, planDate: string | null): string;
|
|
1090
|
+
counts(arrived: string, notArrived: string, cannotTell: string): string;
|
|
1091
|
+
/** Two price lists are two measurements; every dollar line inherits this. */
|
|
1092
|
+
pricesChanged(planReviewed: string, nowReviewed: string): string;
|
|
1093
|
+
action(kind: PlanActionKind, label: string, model: string, outcome: VerifyOutcome): string;
|
|
1094
|
+
reason(reason: CannotTellReason): string;
|
|
1095
|
+
routeObserved(dearestModel: string, onTargetUsd: string, onOldUsd: string): string;
|
|
1096
|
+
/** Tokens do not say which tier billed them; named, never assumed arrived. */
|
|
1097
|
+
batchUnobservable(): string;
|
|
1098
|
+
truncationObserved(retryBillUsd: string): string;
|
|
1099
|
+
cacheObserved(deltaUsd: string, outcome: VerifyOutcome): string;
|
|
1100
|
+
/** The world's movement, measured: never a verdict, always both numbers. */
|
|
1101
|
+
attribution(callsBefore: string, callsAfter: string, outBefore: string, outAfter: string): string;
|
|
1102
|
+
gateFailed(failures: string, total: string): string;
|
|
1103
|
+
gateOk(): string;
|
|
1104
|
+
footer(): string;
|
|
1105
|
+
};
|
|
1106
|
+
|
|
1075
1107
|
/**
|
|
1076
1108
|
* `trazum route` — the loop the levers section could only point at.
|
|
1077
1109
|
*
|
package/src/index.ts
CHANGED
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
analyzeCachePrefix,
|
|
13
13
|
billLevers,
|
|
14
14
|
buildPlan,
|
|
15
|
+
verifyPlan,
|
|
15
16
|
cacheEconomics,
|
|
16
17
|
cacheHitRate,
|
|
17
18
|
contextPressure,
|
|
@@ -79,6 +80,8 @@ import { dayOf, formatGap, median, spanDays } from './time.js';
|
|
|
79
80
|
import type {
|
|
80
81
|
FleetSource,
|
|
81
82
|
MeasuredUsage,
|
|
83
|
+
PlanDocument,
|
|
84
|
+
VerifiedAction,
|
|
82
85
|
BaselineBreach,
|
|
83
86
|
BaselineChange,
|
|
84
87
|
BaselineComparison,
|
|
@@ -475,6 +478,7 @@ const COMMAND_FLAGS: Record<string, string[]> = {
|
|
|
475
478
|
baseline: ['model', 'calls', 'output-tokens', 'cache-hit-rate', 'batch', 'exact-tokens', 'out', 'o'],
|
|
476
479
|
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', 'dry-run', 'markdown-summary', 'by-source'],
|
|
477
480
|
plan: ['json', 'out', 'markdown-out', 'min-usd', 'pricing', 'pricing-live'],
|
|
481
|
+
verify: ['against', 'gate', 'json', 'markdown-out', 'pricing', 'pricing-live'],
|
|
478
482
|
route: ['prompt-file', 'cases', 'label', 'concurrency', 'json', 'yes', 'pricing', 'pricing-live'],
|
|
479
483
|
eval: ['cases', 'level', 'concurrency', 'export', 'out', 'o', 'model'],
|
|
480
484
|
prune: ['cases', 'concurrency', 'json', 'yes'],
|
|
@@ -2214,6 +2218,140 @@ function isoDate(): string {
|
|
|
2214
2218
|
* metered API calls somebody was actually billed for — the bill exists wherever
|
|
2215
2219
|
* Trazum happens to be running, so the host has no bearing on it.
|
|
2216
2220
|
*/
|
|
2221
|
+
/**
|
|
2222
|
+
* `trazum verify <plan.json> --against <newer.jsonl|dir>` — did it work?
|
|
2223
|
+
*
|
|
2224
|
+
* The plan predicted; this holds the prediction to the log that came after
|
|
2225
|
+
* it. Three outcomes and never two — arrived, did not arrive, cannot be told
|
|
2226
|
+
* — because "cannot be told" rendered as "arrived" is how every other tool
|
|
2227
|
+
* congratulates a team for a workload that merely vanished. With `--gate`,
|
|
2228
|
+
* a broken promise is a failing exit code: a different and more useful gate
|
|
2229
|
+
* than "spend went up".
|
|
2230
|
+
*/
|
|
2231
|
+
async function commandVerify(
|
|
2232
|
+
args: Args,
|
|
2233
|
+
pricing: PricingCatalogue,
|
|
2234
|
+
t: CliMessages,
|
|
2235
|
+
): Promise<void> {
|
|
2236
|
+
const planPath = args.positional[0];
|
|
2237
|
+
if (planPath === undefined) throw new Error(t.verify.noTarget());
|
|
2238
|
+
const againstPath = stringFlag(args, 'against');
|
|
2239
|
+
if (againstPath === undefined) throw new Error(t.verify.needsAgainst());
|
|
2240
|
+
|
|
2241
|
+
let plan: PlanDocument & { createdAt?: string };
|
|
2242
|
+
try {
|
|
2243
|
+
const parsed = JSON.parse(await readFile(planPath, 'utf8'));
|
|
2244
|
+
if (parsed?.schemaVersion !== 1 || !Array.isArray(parsed.actions)) {
|
|
2245
|
+
throw new Error(t.verify.badPlan(planPath));
|
|
2246
|
+
}
|
|
2247
|
+
plan = parsed;
|
|
2248
|
+
} catch (error) {
|
|
2249
|
+
if (error instanceof SyntaxError) throw new Error(t.verify.badPlan(planPath));
|
|
2250
|
+
throw error;
|
|
2251
|
+
}
|
|
2252
|
+
|
|
2253
|
+
const GZ = LOG_EXTENSIONS.map((ext) => `${ext}.gz`);
|
|
2254
|
+
const READABLE = [...LOG_EXTENSIONS, ...GZ];
|
|
2255
|
+
const target = await stat(againstPath).catch(() => null);
|
|
2256
|
+
let files: string[] = [againstPath];
|
|
2257
|
+
if (target?.isDirectory()) {
|
|
2258
|
+
const entries = await readdir(againstPath, { withFileTypes: true });
|
|
2259
|
+
files = entries
|
|
2260
|
+
.filter((entry) => entry.isFile() && READABLE.some((ext) => entry.name.endsWith(ext)))
|
|
2261
|
+
.map((entry) => join(againstPath, entry.name))
|
|
2262
|
+
.sort((a, b) => a.localeCompare(b));
|
|
2263
|
+
if (files.length === 0) throw new Error(t.profile.noLogsInDirectory(againstPath, READABLE.join(', ')));
|
|
2264
|
+
}
|
|
2265
|
+
const texts = await Promise.all(files.map((file) => readUsageLog(file, t)));
|
|
2266
|
+
const raw = texts.map((text) => (text.endsWith('\n') ? text : `${text}\n`)).join('');
|
|
2267
|
+
const report = profileUsage(raw, { catalogue: pricing });
|
|
2268
|
+
|
|
2269
|
+
const verification = verifyPlan(plan, report, { currentPricingLastReviewed: pricing.lastReviewed });
|
|
2270
|
+
const gate = boolFlag(args, 'gate');
|
|
2271
|
+
const n = (value: number): string => value.toLocaleString(t.numberLocale);
|
|
2272
|
+
|
|
2273
|
+
const lines = (md: boolean): string[] => {
|
|
2274
|
+
const out: string[] = [];
|
|
2275
|
+
const actionLine = (v: VerifiedAction): string[] => {
|
|
2276
|
+
const name = v.action.label === UNLABELLED ? t.profile.unlabelled() : v.action.label;
|
|
2277
|
+
const rows: string[] = [];
|
|
2278
|
+
rows.push(t.verify.action(v.action.kind, name, v.action.model, v.outcome));
|
|
2279
|
+
if (v.outcome === 'cannot-tell' && v.reason !== null) rows.push(t.verify.reason(v.reason));
|
|
2280
|
+
if (v.action.kind === 'route' || v.action.kind === 'route+batch') {
|
|
2281
|
+
if (v.outcome !== 'cannot-tell') {
|
|
2282
|
+
rows.push(
|
|
2283
|
+
t.verify.routeObserved(
|
|
2284
|
+
String(v.observed.dearestModel ?? ''),
|
|
2285
|
+
formatUsd(Number(v.observed.onTargetUsd ?? 0)),
|
|
2286
|
+
formatUsd(Number(v.observed.onOldModelUsd ?? 0)),
|
|
2287
|
+
),
|
|
2288
|
+
);
|
|
2289
|
+
}
|
|
2290
|
+
if (v.action.kind === 'route+batch' && v.outcome !== 'cannot-tell') rows.push(t.verify.batchUnobservable());
|
|
2291
|
+
}
|
|
2292
|
+
if (v.action.kind === 'fix-truncation' && v.outcome === 'not-arrived') {
|
|
2293
|
+
rows.push(t.verify.truncationObserved(formatUsd(Number(v.observed.retryBillUsd ?? 0))));
|
|
2294
|
+
}
|
|
2295
|
+
if (v.action.kind === 'fix-caching' && v.outcome !== 'cannot-tell') {
|
|
2296
|
+
rows.push(t.verify.cacheObserved(formatUsd(Number(v.observed.deltaUsd ?? 0)), v.outcome));
|
|
2297
|
+
}
|
|
2298
|
+
if (v.attribution?.calls !== undefined) {
|
|
2299
|
+
rows.push(
|
|
2300
|
+
t.verify.attribution(
|
|
2301
|
+
n(Math.round(v.attribution.calls.before)),
|
|
2302
|
+
n(Math.round(v.attribution.calls.after)),
|
|
2303
|
+
n(Math.round(v.attribution.outputPerCallTokens?.before ?? 0)),
|
|
2304
|
+
n(Math.round(v.attribution.outputPerCallTokens?.after ?? 0)),
|
|
2305
|
+
),
|
|
2306
|
+
);
|
|
2307
|
+
}
|
|
2308
|
+
return rows;
|
|
2309
|
+
};
|
|
2310
|
+
|
|
2311
|
+
const heading = t.verify.heading(
|
|
2312
|
+
n(verification.actions.length),
|
|
2313
|
+
verification.planCreatedAt === null ? null : verification.planCreatedAt.slice(0, 10),
|
|
2314
|
+
);
|
|
2315
|
+
out.push(md ? `## ${heading}` : heading);
|
|
2316
|
+
out.push(
|
|
2317
|
+
t.verify.counts(n(verification.arrived), n(verification.notArrived), n(verification.cannotTell)),
|
|
2318
|
+
);
|
|
2319
|
+
if (verification.pricesChanged) {
|
|
2320
|
+
out.push(t.verify.pricesChanged(verification.planPricing, verification.currentPricing));
|
|
2321
|
+
}
|
|
2322
|
+
for (const v of verification.actions) {
|
|
2323
|
+
out.push('');
|
|
2324
|
+
const [head, ...rest] = actionLine(v);
|
|
2325
|
+
out.push(md ? `### ${head}` : `→ ${head}`);
|
|
2326
|
+
for (const row of rest) out.push(md ? `- ${row}` : ` · ${row}`);
|
|
2327
|
+
}
|
|
2328
|
+
out.push('');
|
|
2329
|
+
out.push(t.verify.footer());
|
|
2330
|
+
return out;
|
|
2331
|
+
};
|
|
2332
|
+
|
|
2333
|
+
await writeMarkdown(args, () => lines(true).join('\n'));
|
|
2334
|
+
|
|
2335
|
+
if (boolFlag(args, 'json')) {
|
|
2336
|
+
console.log(JSON.stringify(verification, null, 2));
|
|
2337
|
+
} else {
|
|
2338
|
+
const [head, ...rest] = lines(false);
|
|
2339
|
+
console.log(c.bold(head!));
|
|
2340
|
+
for (const row of rest) {
|
|
2341
|
+
console.log(row === '' ? '' : ` ${wrap(row, 74, ' ')}`);
|
|
2342
|
+
}
|
|
2343
|
+
}
|
|
2344
|
+
|
|
2345
|
+
if (gate) {
|
|
2346
|
+
if (verification.gateFailures > 0) {
|
|
2347
|
+
console.error(c.red(t.verify.gateFailed(n(verification.gateFailures), n(verification.actions.length))));
|
|
2348
|
+
process.exitCode = 1;
|
|
2349
|
+
} else {
|
|
2350
|
+
console.log(c.green(t.verify.gateOk()));
|
|
2351
|
+
}
|
|
2352
|
+
}
|
|
2353
|
+
}
|
|
2354
|
+
|
|
2217
2355
|
/**
|
|
2218
2356
|
* `trazum plan <log>` — not a list of findings, a ranked plan of what to do.
|
|
2219
2357
|
*
|
|
@@ -6161,6 +6299,9 @@ async function main(): Promise<void> {
|
|
|
6161
6299
|
case 'plan':
|
|
6162
6300
|
await commandPlan(args, pricing, t);
|
|
6163
6301
|
break;
|
|
6302
|
+
case 'verify':
|
|
6303
|
+
await commandVerify(args, pricing, t);
|
|
6304
|
+
break;
|
|
6164
6305
|
case 'route':
|
|
6165
6306
|
await commandRoute(args, pricing, t);
|
|
6166
6307
|
break;
|