@trazum/cli 1.37.0 → 1.38.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 +34 -1
- package/dist/i18n/types.d.ts.map +1 -1
- package/dist/index.js +136 -2
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
- package/src/i18n/en.ts +67 -0
- package/src/i18n/es.ts +69 -0
- package/src/i18n/types.ts +35 -1
- package/src/index.ts +147 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@trazum/cli",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.38.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.38.0"
|
|
41
41
|
},
|
|
42
42
|
"devDependencies": {
|
|
43
43
|
"@types/node": "^26.2.0",
|
package/src/i18n/en.ts
CHANGED
|
@@ -39,6 +39,7 @@ ${bold('USAGE')}
|
|
|
39
39
|
trazum eval <file> --cases <file> [options]
|
|
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
|
+
trazum plan <log.jsonl|dir> [options]
|
|
42
43
|
trazum diff <before> <after> [options]
|
|
43
44
|
trazum diff --all <dir> <dir> [options]
|
|
44
45
|
trazum rank <dir> [options]
|
|
@@ -299,6 +300,23 @@ ${bold('OPTIONS FOR profile')}
|
|
|
299
300
|
"label" (which workload), "session" (which conversation — grouped by, never
|
|
300
301
|
printed), "stop_reason"/"finish_reason" (answers cut off at max_tokens).
|
|
301
302
|
|
|
303
|
+
${bold('OPTIONS FOR plan')}
|
|
304
|
+
--min-usd <n> Leave out actions worth less than n dollars. How
|
|
305
|
+
many were left out is stated, never silent.
|
|
306
|
+
-o, --out <file> Save the plan as dated JSON — the file
|
|
307
|
+
"trazum verify" will later hold it to.
|
|
308
|
+
--markdown-out <file> Also write the plan as Markdown, for a CI job
|
|
309
|
+
summary or a pull request comment.
|
|
310
|
+
--pricing <file> Local price overlay, as everywhere else.
|
|
311
|
+
--json The plan as data.
|
|
312
|
+
|
|
313
|
+
Reads a usage log and turns the report's findings into a ranked plan: route
|
|
314
|
+
this, batch that, fix the truncation pair, look at the cache. The money is
|
|
315
|
+
composed correctly — route and batch on the same slice arrive combined,
|
|
316
|
+
never summed — and every action names what the log cannot confirm, because
|
|
317
|
+
a plan that hides its assumptions is advice pretending to be arithmetic.
|
|
318
|
+
Projected savings and money already spent are separate totals throughout.
|
|
319
|
+
|
|
302
320
|
${bold('OPTIONS FOR route')}
|
|
303
321
|
--prompt-file <file> The prompt those calls send. Not --prompt, which
|
|
304
322
|
names a marked prompt inside a source file.
|
|
@@ -1345,6 +1363,55 @@ ${bold('EXAMPLES')}
|
|
|
1345
1363
|
`${label} on ${model}: ${single} of ${sessions} conversations ended after their first turn and spent ${usd} writing a cache that nothing in this log ever read. Within the conversation, across conversations — no read anywhere, so those writes bought nothing. Caching a one-shot call is pure write premium; stop marking these calls with cache_control.`,
|
|
1346
1364
|
},
|
|
1347
1365
|
|
|
1366
|
+
plan: {
|
|
1367
|
+
noTarget: () =>
|
|
1368
|
+
'Point this at a usage log or a directory of them: trazum plan usage.jsonl. It turns the report into a ranked plan — what to do first, what each action is worth, and what the log cannot confirm about it.',
|
|
1369
|
+
nothingPriced: () =>
|
|
1370
|
+
'This log priced nothing — no call in it matched a model in the catalogue. A plan over zero dollars would be advice about nothing; check the log with "trazum profile" first.',
|
|
1371
|
+
heading: (actions, total) => `The plan: ${actions} actions against a ${total} bill`,
|
|
1372
|
+
totals: (projected, staked) =>
|
|
1373
|
+
`${projected} projected savings, on assumptions listed below. ${staked} already spent on problems this plan names — measured, not projected.`,
|
|
1374
|
+
noClock: () =>
|
|
1375
|
+
'No timestamps in this log, so every figure is per this log, not per any period.',
|
|
1376
|
+
projected: (usd) => `${usd} projected`,
|
|
1377
|
+
staked: (usd) => `${usd} already spent`,
|
|
1378
|
+
action: (kind, label, model) => {
|
|
1379
|
+
const verb =
|
|
1380
|
+
kind === 'route'
|
|
1381
|
+
? 'Route'
|
|
1382
|
+
: kind === 'batch'
|
|
1383
|
+
? 'Batch'
|
|
1384
|
+
: kind === 'route+batch'
|
|
1385
|
+
? 'Route and batch'
|
|
1386
|
+
: kind === 'fix-truncation'
|
|
1387
|
+
? 'Fix the truncation retries on'
|
|
1388
|
+
: 'Look at the cache on';
|
|
1389
|
+
return `${verb} ${label} (${model})`;
|
|
1390
|
+
},
|
|
1391
|
+
routeTo: (model) => `to ${model} — combined with batching where both apply, never summed`,
|
|
1392
|
+
assume: (assumption) => {
|
|
1393
|
+
switch (assumption.kind) {
|
|
1394
|
+
case 'model-capability':
|
|
1395
|
+
return `assumes ${assumption.model} can do this work — the log prices the move, it cannot judge the answers`;
|
|
1396
|
+
case 'batch-window':
|
|
1397
|
+
return 'assumes these calls can wait for a batch window';
|
|
1398
|
+
case 'retry-pattern-real':
|
|
1399
|
+
return 'assumes the retry pattern is real — the log sees shapes, not content';
|
|
1400
|
+
case 'max-tokens-fits':
|
|
1401
|
+
return 'assumes a max_tokens the answers fit in removes the pair';
|
|
1402
|
+
case 'traffic-pattern-holds':
|
|
1403
|
+
return 'assumes the traffic pattern holds — a cache that lost money on this log may pay on different traffic';
|
|
1404
|
+
}
|
|
1405
|
+
},
|
|
1406
|
+
check: (command) => `check it: ${command}`,
|
|
1407
|
+
filtered: (count, minUsd, worth) =>
|
|
1408
|
+
`${count} actions under ${minUsd}, worth ${worth} together, left out by --min-usd — left out of this document entirely, not disproved.`,
|
|
1409
|
+
footer: () =>
|
|
1410
|
+
'Ranked by money, projected or already spent alike. The assumptions are yours to answer: this plan is arithmetic over the log, not knowledge of your product.',
|
|
1411
|
+
wrote: (path) =>
|
|
1412
|
+
`Plan written to ${path}, dated. Keep it: a prediction nobody wrote down is a prediction nobody can be held to.`,
|
|
1413
|
+
},
|
|
1414
|
+
|
|
1348
1415
|
route: {
|
|
1349
1416
|
noTarget: () =>
|
|
1350
1417
|
'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
|
@@ -26,6 +26,7 @@ ${bold('USO')}
|
|
|
26
26
|
trazum eval <fichero> --cases <fichero> [opciones]
|
|
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
|
+
trazum plan <log.jsonl|dir> [opciones]
|
|
29
30
|
trazum diff <antes> <después> [opciones]
|
|
30
31
|
trazum diff --all <dir> <dir> [opciones]
|
|
31
32
|
trazum rank <dir> [opciones]
|
|
@@ -304,6 +305,25 @@ ${bold('OPCIONES DE profile')}
|
|
|
304
305
|
nunca se imprime), "stop_reason"/"finish_reason" (respuestas cortadas en
|
|
305
306
|
max_tokens).
|
|
306
307
|
|
|
308
|
+
${bold('OPCIONES DE plan')}
|
|
309
|
+
--min-usd <n> Deja fuera las acciones que valen menos de n
|
|
310
|
+
dólares. Cuántas quedaron fuera se dice, nunca
|
|
311
|
+
en silencio.
|
|
312
|
+
-o, --out <fichero> Guarda el plan como JSON fechado — el fichero al
|
|
313
|
+
que "trazum verify" lo atará después.
|
|
314
|
+
--markdown-out <fichero> Escribe además el plan como Markdown, para un
|
|
315
|
+
resumen de CI o un comentario de pull request.
|
|
316
|
+
--pricing <fichero> Tarifas locales superpuestas, como en el resto.
|
|
317
|
+
--json El plan como datos.
|
|
318
|
+
|
|
319
|
+
Lee un registro de uso y convierte los hallazgos del informe en un plan
|
|
320
|
+
ordenado: enruta esto, agrupa aquello, arregla el par de truncados, mira la
|
|
321
|
+
caché. El dinero está compuesto correctamente — ruta y batch sobre la misma
|
|
322
|
+
porción llegan combinados, nunca sumados — y cada acción nombra lo que el
|
|
323
|
+
registro no puede confirmar, porque un plan que esconde sus supuestos es un
|
|
324
|
+
consejo haciéndose pasar por aritmética. El ahorro proyectado y el dinero ya
|
|
325
|
+
gastado son totales separados en todas partes.
|
|
326
|
+
|
|
307
327
|
${bold('OPCIONES DE route')}
|
|
308
328
|
--prompt-file <fichero> El prompt que mandan esas llamadas. No --prompt,
|
|
309
329
|
que nombra un prompt marcado dentro de un fuente.
|
|
@@ -1363,6 +1383,55 @@ ${bold('EJEMPLOS')}
|
|
|
1363
1383
|
`${label} en ${model}: ${single} de ${sessions} conversaciones terminaron tras su primer turno y gastaron ${usd} escribiendo una caché que nada en este registro leyó jamás. Dentro de la conversación, entre conversaciones — ninguna lectura en ningún sitio, así que esas escrituras no compraron nada. Cachear una llamada de un solo uso es puro sobreprecio de escritura; deja de marcar estas llamadas con cache_control.`,
|
|
1364
1384
|
},
|
|
1365
1385
|
|
|
1386
|
+
plan: {
|
|
1387
|
+
noTarget: () =>
|
|
1388
|
+
'Apunta esto a un registro de uso o a un directorio de ellos: trazum plan usage.jsonl. Convierte el informe en un plan ordenado — qué hacer primero, cuánto vale cada acción y qué no puede confirmar el registro sobre ella.',
|
|
1389
|
+
nothingPriced: () =>
|
|
1390
|
+
'Este registro no tasó nada — ninguna llamada coincide con un modelo del catálogo. Un plan sobre cero dólares sería un consejo sobre nada; revisa el registro primero con "trazum profile".',
|
|
1391
|
+
heading: (actions, total) => `El plan: ${actions} acciones contra una factura de ${total}`,
|
|
1392
|
+
totals: (projected, staked) =>
|
|
1393
|
+
`${projected} de ahorro proyectado, sobre los supuestos listados abajo. ${staked} ya gastados en problemas que este plan nombra — medido, no proyectado.`,
|
|
1394
|
+
noClock: () =>
|
|
1395
|
+
'Este registro no tiene marcas de tiempo, así que cada cifra es por este registro, no por ningún período.',
|
|
1396
|
+
projected: (usd) => `${usd} proyectados`,
|
|
1397
|
+
staked: (usd) => `${usd} ya gastados`,
|
|
1398
|
+
action: (kind, label, model) => {
|
|
1399
|
+
const verb =
|
|
1400
|
+
kind === 'route'
|
|
1401
|
+
? 'Enruta'
|
|
1402
|
+
: kind === 'batch'
|
|
1403
|
+
? 'Agrupa en batch'
|
|
1404
|
+
: kind === 'route+batch'
|
|
1405
|
+
? 'Enruta y agrupa'
|
|
1406
|
+
: kind === 'fix-truncation'
|
|
1407
|
+
? 'Arregla los reintentos por truncado de'
|
|
1408
|
+
: 'Mira la caché de';
|
|
1409
|
+
return `${verb} ${label} (${model})`;
|
|
1410
|
+
},
|
|
1411
|
+
routeTo: (model) => `a ${model} — combinado con el batch donde aplican los dos, nunca sumado`,
|
|
1412
|
+
assume: (assumption) => {
|
|
1413
|
+
switch (assumption.kind) {
|
|
1414
|
+
case 'model-capability':
|
|
1415
|
+
return `supone que ${assumption.model} puede hacer este trabajo — el registro tasa el cambio, no puede juzgar las respuestas`;
|
|
1416
|
+
case 'batch-window':
|
|
1417
|
+
return 'supone que estas llamadas pueden esperar una ventana de batch';
|
|
1418
|
+
case 'retry-pattern-real':
|
|
1419
|
+
return 'supone que el patrón de reintentos es real — el registro ve formas, no contenido';
|
|
1420
|
+
case 'max-tokens-fits':
|
|
1421
|
+
return 'supone que un max_tokens en el que quepan las respuestas elimina el par';
|
|
1422
|
+
case 'traffic-pattern-holds':
|
|
1423
|
+
return 'supone que el patrón de tráfico se mantiene — una caché que perdió dinero en este registro puede rendir con otro tráfico';
|
|
1424
|
+
}
|
|
1425
|
+
},
|
|
1426
|
+
check: (command) => `compruébalo: ${command}`,
|
|
1427
|
+
filtered: (count, minUsd, worth) =>
|
|
1428
|
+
`${count} acciones por debajo de ${minUsd}, que juntas valen ${worth}, quedaron fuera por --min-usd — fuera de este documento por completo, no refutadas.`,
|
|
1429
|
+
footer: () =>
|
|
1430
|
+
'Ordenado por dinero, proyectado o ya gastado por igual. Los supuestos los respondes tú: este plan es aritmética sobre el registro, no conocimiento de tu producto.',
|
|
1431
|
+
wrote: (path) =>
|
|
1432
|
+
`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
|
+
},
|
|
1434
|
+
|
|
1366
1435
|
route: {
|
|
1367
1436
|
noTarget: () =>
|
|
1368
1437
|
'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, RuleLevel } from '@trazum/core';
|
|
1
|
+
import type { EvalVerdict, Locale, PlanActionKind, PlanAssumption, RuleLevel } from '@trazum/core';
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* The CLI's own message catalogue.
|
|
@@ -1038,6 +1038,40 @@ export interface CliMessages {
|
|
|
1038
1038
|
singleTurnCeiling(label: string, model: string, single: string, sessions: string, usd: string): string;
|
|
1039
1039
|
singleTurnConfirmed(label: string, model: string, single: string, sessions: string, usd: string): string;
|
|
1040
1040
|
};
|
|
1041
|
+
/**
|
|
1042
|
+
* `trazum plan <log>` — not a list of findings, a ranked plan of what to do.
|
|
1043
|
+
*
|
|
1044
|
+
* The report names what it found; the plan ranks what to do about it, with
|
|
1045
|
+
* the money composed correctly (route and batch on one slice combined,
|
|
1046
|
+
* never summed) and every action carrying what the log cannot confirm.
|
|
1047
|
+
* Projected savings and measured stakes are separate columns throughout:
|
|
1048
|
+
* "what you would save" and "what you already paid" merged into one number
|
|
1049
|
+
* is a number that is neither.
|
|
1050
|
+
*/
|
|
1051
|
+
plan: {
|
|
1052
|
+
noTarget(): string;
|
|
1053
|
+
/** The log priced nothing — a plan over zero calls would be advice about nothing. */
|
|
1054
|
+
nothingPriced(): string;
|
|
1055
|
+
heading(actions: string, total: string): string;
|
|
1056
|
+
totals(projected: string, staked: string): string;
|
|
1057
|
+
/** No timestamps: the figures are per this log, not per any period. */
|
|
1058
|
+
noClock(): string;
|
|
1059
|
+
projected(usd: string): string;
|
|
1060
|
+
staked(usd: string): string;
|
|
1061
|
+
/** One action line: what to do, to which workload, on which model. */
|
|
1062
|
+
action(kind: PlanActionKind, label: string, model: string): string;
|
|
1063
|
+
routeTo(model: string): string;
|
|
1064
|
+
/** One assumption the log cannot confirm, localized from its typed form. */
|
|
1065
|
+
assume(assumption: PlanAssumption): string;
|
|
1066
|
+
/** The command that can check the assumption, when one exists. */
|
|
1067
|
+
check(command: string): string;
|
|
1068
|
+
/** Actions below --min-usd, counted out loud rather than dropped silently. */
|
|
1069
|
+
filtered(count: string, minUsd: string, worth: string): string;
|
|
1070
|
+
footer(): string;
|
|
1071
|
+
/** The plan saved as dated JSON — what 1.39's verify will hold it to. */
|
|
1072
|
+
wrote(path: string): string;
|
|
1073
|
+
};
|
|
1074
|
+
|
|
1041
1075
|
/**
|
|
1042
1076
|
* `trazum route` — the loop the levers section could only point at.
|
|
1043
1077
|
*
|
package/src/index.ts
CHANGED
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
cacheableMinimum,
|
|
12
12
|
analyzeCachePrefix,
|
|
13
13
|
billLevers,
|
|
14
|
+
buildPlan,
|
|
14
15
|
cacheEconomics,
|
|
15
16
|
cacheHitRate,
|
|
16
17
|
contextPressure,
|
|
@@ -172,6 +173,7 @@ interface Args {
|
|
|
172
173
|
const VALUE_FLAGS = new Set([
|
|
173
174
|
'against',
|
|
174
175
|
'from-log',
|
|
176
|
+
'min-usd',
|
|
175
177
|
// `route` takes a path here, and the flag is deliberately not `--prompt`:
|
|
176
178
|
// everywhere else in this tool `--prompt` names a marked prompt *inside* a
|
|
177
179
|
// source file, and reusing it for a path would be a trap laid for the reader.
|
|
@@ -312,6 +314,13 @@ function levelFlag(args: Args, config: TrazumConfig, t: CliMessages): RuleLevel
|
|
|
312
314
|
* model id. It beats the default because reading the code is better than
|
|
313
315
|
* assuming, and loses to config because being told is better than reading.
|
|
314
316
|
*/
|
|
317
|
+
/**
|
|
318
|
+
* The file names a usage log answers to, shared by every command that reads a
|
|
319
|
+
* directory of them. One list, because two commands disagreeing on what counts
|
|
320
|
+
* as a log would be the same directory billing differently by verb.
|
|
321
|
+
*/
|
|
322
|
+
const LOG_EXTENSIONS = ['.jsonl', '.ndjson', '.log', '.json'];
|
|
323
|
+
|
|
315
324
|
/**
|
|
316
325
|
* One usage log, gzip included, shared by every command that reads one.
|
|
317
326
|
*
|
|
@@ -465,6 +474,7 @@ const COMMAND_FLAGS: Record<string, string[]> = {
|
|
|
465
474
|
check: ['max-tokens', 'level', 'exact-tokens', 'markdown-out', 'baseline'],
|
|
466
475
|
baseline: ['model', 'calls', 'output-tokens', 'cache-hit-rate', 'batch', 'exact-tokens', 'out', 'o'],
|
|
467
476
|
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
|
+
plan: ['json', 'out', 'markdown-out', 'min-usd', 'pricing', 'pricing-live'],
|
|
468
478
|
route: ['prompt-file', 'cases', 'label', 'concurrency', 'json', 'yes', 'pricing', 'pricing-live'],
|
|
469
479
|
eval: ['cases', 'level', 'concurrency', 'export', 'out', 'o', 'model'],
|
|
470
480
|
prune: ['cases', 'concurrency', 'json', 'yes'],
|
|
@@ -2204,6 +2214,140 @@ function isoDate(): string {
|
|
|
2204
2214
|
* metered API calls somebody was actually billed for — the bill exists wherever
|
|
2205
2215
|
* Trazum happens to be running, so the host has no bearing on it.
|
|
2206
2216
|
*/
|
|
2217
|
+
/**
|
|
2218
|
+
* `trazum plan <log>` — not a list of findings, a ranked plan of what to do.
|
|
2219
|
+
*
|
|
2220
|
+
* The composition (route and batch on one slice never summed) happens in
|
|
2221
|
+
* core's `buildPlan`; this command owns the I/O and the rendering. The plan
|
|
2222
|
+
* saves as a dated JSON file on request, which is what makes verifying it
|
|
2223
|
+
* against a later log possible at all — a prediction nobody wrote down is a
|
|
2224
|
+
* prediction nobody can be held to.
|
|
2225
|
+
*/
|
|
2226
|
+
async function commandPlan(
|
|
2227
|
+
args: Args,
|
|
2228
|
+
pricing: PricingCatalogue,
|
|
2229
|
+
t: CliMessages,
|
|
2230
|
+
): Promise<void> {
|
|
2231
|
+
const path = args.positional[0];
|
|
2232
|
+
if (path === undefined) throw new Error(t.plan.noTarget());
|
|
2233
|
+
|
|
2234
|
+
const GZ = LOG_EXTENSIONS.map((ext) => `${ext}.gz`);
|
|
2235
|
+
const READABLE = [...LOG_EXTENSIONS, ...GZ];
|
|
2236
|
+
const target = await stat(path).catch(() => null);
|
|
2237
|
+
let files: string[] = [path];
|
|
2238
|
+
if (target?.isDirectory()) {
|
|
2239
|
+
const entries = await readdir(path, { withFileTypes: true });
|
|
2240
|
+
files = entries
|
|
2241
|
+
.filter((entry) => entry.isFile() && READABLE.some((ext) => entry.name.endsWith(ext)))
|
|
2242
|
+
.map((entry) => join(path, entry.name))
|
|
2243
|
+
.sort((a, b) => a.localeCompare(b));
|
|
2244
|
+
if (files.length === 0) throw new Error(t.profile.noLogsInDirectory(path, READABLE.join(', ')));
|
|
2245
|
+
}
|
|
2246
|
+
const texts = await Promise.all(files.map((file) => readUsageLog(file, t)));
|
|
2247
|
+
const raw = texts.map((text) => (text.endsWith('\n') ? text : `${text}\n`)).join('');
|
|
2248
|
+
|
|
2249
|
+
const report = profileUsage(raw, { catalogue: pricing });
|
|
2250
|
+
if (report.total.calls === 0) throw new Error(t.plan.nothingPriced());
|
|
2251
|
+
const levers = billLevers(report, { catalogue: pricing });
|
|
2252
|
+
const plan = buildPlan(report, levers, pricing.lastReviewed);
|
|
2253
|
+
|
|
2254
|
+
const minUsd = typeof args.flags.get('min-usd') === 'string' ? numberFlag(args, 'min-usd', 0, t) : 0;
|
|
2255
|
+
const actions = plan.actions.filter((a) => (a.savingUsd ?? a.stakeUsd ?? 0) >= minUsd);
|
|
2256
|
+
const filtered = plan.actions.length - actions.length;
|
|
2257
|
+
const droppedUsd = plan.actions
|
|
2258
|
+
.filter((a) => (a.savingUsd ?? a.stakeUsd ?? 0) < minUsd)
|
|
2259
|
+
.reduce((sum, a) => sum + (a.savingUsd ?? a.stakeUsd ?? 0), 0);
|
|
2260
|
+
|
|
2261
|
+
const n = (value: number): string => value.toLocaleString(t.numberLocale);
|
|
2262
|
+
/**
|
|
2263
|
+
* The document's totals cover the actions the document holds — a filtered
|
|
2264
|
+
* plan whose totals still counted the filtered actions would be a file
|
|
2265
|
+
* that contradicts itself, and 1.39's verify would hold it to money it
|
|
2266
|
+
* cannot see. What --min-usd dropped is stated with its worth, never
|
|
2267
|
+
* silently.
|
|
2268
|
+
*/
|
|
2269
|
+
const stamped = {
|
|
2270
|
+
...plan,
|
|
2271
|
+
actions,
|
|
2272
|
+
projectedSavingUsd: actions.reduce((sum, a) => sum + (a.savingUsd ?? 0), 0),
|
|
2273
|
+
measuredStakeUsd: actions.reduce((sum, a) => sum + (a.stakeUsd ?? 0), 0),
|
|
2274
|
+
createdAt: new Date().toISOString(),
|
|
2275
|
+
};
|
|
2276
|
+
|
|
2277
|
+
const outPath = stringFlag(args, 'out');
|
|
2278
|
+
if (outPath !== undefined) {
|
|
2279
|
+
await writeFile(outPath, `${JSON.stringify(stamped, null, 2)}\n`);
|
|
2280
|
+
}
|
|
2281
|
+
|
|
2282
|
+
await writeMarkdown(args, () => {
|
|
2283
|
+
const lines: string[] = [];
|
|
2284
|
+
lines.push(`## ${t.plan.heading(n(actions.length), formatUsd(plan.totalUsd))}`);
|
|
2285
|
+
lines.push('');
|
|
2286
|
+
lines.push(t.plan.totals(formatUsd(stamped.projectedSavingUsd), formatUsd(stamped.measuredStakeUsd)));
|
|
2287
|
+
if (plan.span === null) {
|
|
2288
|
+
lines.push('');
|
|
2289
|
+
lines.push(`_${t.plan.noClock()}_`);
|
|
2290
|
+
}
|
|
2291
|
+
for (const action of actions) {
|
|
2292
|
+
const name = action.label === UNLABELLED ? t.profile.unlabelled() : action.label;
|
|
2293
|
+
const money =
|
|
2294
|
+
action.savingUsd !== null
|
|
2295
|
+
? t.plan.projected(formatUsd(action.savingUsd))
|
|
2296
|
+
: t.plan.staked(formatUsd(action.stakeUsd ?? 0));
|
|
2297
|
+
lines.push('');
|
|
2298
|
+
lines.push(`### ${t.plan.action(action.kind, name, action.model)} — ${money}`);
|
|
2299
|
+
if (action.detail.routeTo !== undefined) lines.push(`- ${t.plan.routeTo(action.detail.routeTo.displayName)}`);
|
|
2300
|
+
for (const assumption of action.assumes) lines.push(`- ${t.plan.assume(assumption)}`);
|
|
2301
|
+
if (action.check !== null) lines.push(`- ${t.plan.check(action.check)}`);
|
|
2302
|
+
}
|
|
2303
|
+
if (filtered > 0) {
|
|
2304
|
+
lines.push('');
|
|
2305
|
+
lines.push(`_${t.plan.filtered(n(filtered), formatUsd(minUsd), formatUsd(droppedUsd))}_`);
|
|
2306
|
+
}
|
|
2307
|
+
lines.push('');
|
|
2308
|
+
lines.push(`_${t.plan.footer()}_`);
|
|
2309
|
+
return lines.join('\n');
|
|
2310
|
+
});
|
|
2311
|
+
|
|
2312
|
+
if (boolFlag(args, 'json')) {
|
|
2313
|
+
console.log(JSON.stringify(stamped, null, 2));
|
|
2314
|
+
return;
|
|
2315
|
+
}
|
|
2316
|
+
|
|
2317
|
+
console.log(c.bold(t.plan.heading(n(actions.length), formatUsd(plan.totalUsd))));
|
|
2318
|
+
console.log(
|
|
2319
|
+
` ${wrap(t.plan.totals(formatUsd(stamped.projectedSavingUsd), formatUsd(stamped.measuredStakeUsd)), 74, ' ')}`,
|
|
2320
|
+
);
|
|
2321
|
+
if (plan.span === null) {
|
|
2322
|
+
console.log(` ${c.dim(wrap(t.plan.noClock(), 74, ' '))}`);
|
|
2323
|
+
}
|
|
2324
|
+
for (const action of actions) {
|
|
2325
|
+
const name = action.label === UNLABELLED ? t.profile.unlabelled() : action.label;
|
|
2326
|
+
const money =
|
|
2327
|
+
action.savingUsd !== null
|
|
2328
|
+
? t.plan.projected(formatUsd(action.savingUsd))
|
|
2329
|
+
: t.plan.staked(formatUsd(action.stakeUsd ?? 0));
|
|
2330
|
+
console.log();
|
|
2331
|
+
console.log(` ${c.green('→')} ${c.bold(t.plan.action(action.kind, name, action.model))} ${money}`);
|
|
2332
|
+
if (action.detail.routeTo !== undefined) {
|
|
2333
|
+
console.log(` ${c.dim(t.plan.routeTo(action.detail.routeTo.displayName))}`);
|
|
2334
|
+
}
|
|
2335
|
+
for (const assumption of action.assumes) {
|
|
2336
|
+
console.log(` ${c.yellow('?')} ${c.dim(wrap(t.plan.assume(assumption), 72, ' '))}`);
|
|
2337
|
+
}
|
|
2338
|
+
if (action.check !== null) {
|
|
2339
|
+
console.log(` ${c.dim(wrap(t.plan.check(action.check), 72, ' '))}`);
|
|
2340
|
+
}
|
|
2341
|
+
}
|
|
2342
|
+
if (filtered > 0) {
|
|
2343
|
+
console.log();
|
|
2344
|
+
console.log(` ${c.dim(wrap(t.plan.filtered(n(filtered), formatUsd(minUsd), formatUsd(droppedUsd)), 74, ' '))}`);
|
|
2345
|
+
}
|
|
2346
|
+
console.log();
|
|
2347
|
+
console.log(` ${c.dim(wrap(t.plan.footer(), 74, ' '))}`);
|
|
2348
|
+
if (outPath !== undefined) console.log(c.dim(wrap(t.plan.wrote(outPath), 74, '')));
|
|
2349
|
+
}
|
|
2350
|
+
|
|
2207
2351
|
async function commandProfile(args: Args, config: TrazumConfig, pricing: PricingCatalogue, t: CliMessages): Promise<void> {
|
|
2208
2352
|
const path = args.positional[0];
|
|
2209
2353
|
if (path === undefined) {
|
|
@@ -2227,7 +2371,6 @@ async function commandProfile(args: Args, config: TrazumConfig, pricing: Pricing
|
|
|
2227
2371
|
* directory holding nothing readable is an error naming what it looked for,
|
|
2228
2372
|
* not an empty report.
|
|
2229
2373
|
*/
|
|
2230
|
-
const LOG_EXTENSIONS = ['.jsonl', '.ndjson', '.log', '.json'];
|
|
2231
2374
|
/**
|
|
2232
2375
|
* The same names, gzipped — which is what a rotated log actually looks like
|
|
2233
2376
|
* a day after it rotates.
|
|
@@ -6015,6 +6158,9 @@ async function main(): Promise<void> {
|
|
|
6015
6158
|
case 'profile':
|
|
6016
6159
|
await commandProfile(args, config, pricing, t);
|
|
6017
6160
|
break;
|
|
6161
|
+
case 'plan':
|
|
6162
|
+
await commandPlan(args, pricing, t);
|
|
6163
|
+
break;
|
|
6018
6164
|
case 'route':
|
|
6019
6165
|
await commandRoute(args, pricing, t);
|
|
6020
6166
|
break;
|