@trazum/cli 1.48.0 → 1.49.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/dist/i18n/en.d.ts.map +1 -1
- package/dist/i18n/en.js +17 -1
- package/dist/i18n/en.js.map +1 -1
- package/dist/i18n/es.d.ts.map +1 -1
- package/dist/i18n/es.js +17 -1
- 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 +72 -24
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
- package/src/i18n/en.ts +24 -1
- package/src/i18n/es.ts +24 -1
- package/src/i18n/types.ts +15 -0
- package/src/index.ts +97 -26
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@trazum/cli",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.49.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.49.0"
|
|
41
41
|
},
|
|
42
42
|
"devDependencies": {
|
|
43
43
|
"@types/node": "^26.2.0",
|
package/src/i18n/en.ts
CHANGED
|
@@ -1666,7 +1666,9 @@ ${bold('EXAMPLES')}
|
|
|
1666
1666
|
nothingMeasured: (dir) =>
|
|
1667
1667
|
`Nothing is measured yet (the store at ${dir} is empty), so the budget half of every answer will say so. The cost half still answers from the catalogue: offline is a mode, not a failure.`,
|
|
1668
1668
|
noBudget: () =>
|
|
1669
|
-
'No spend.
|
|
1669
|
+
'No spend.monthlyUsd is configured, so "is there budget left" has no subject and every answer says so rather than guessing one. spend.maxUsd is deliberately not read here: it gates whatever period a log covers, and reading it as a monthly limit is how two surfaces of this tool come to disagree.',
|
|
1670
|
+
partialCoverage: (measuredDays, elapsedDays, period) =>
|
|
1671
|
+
`Only ${measuredDays} of the ${elapsedDays} elapsed days of ${period} carry any measurement, so the consumed figure is a floor on the period rather than the period. Pull the missing days with trazum connect before treating what is left as headroom.`,
|
|
1670
1672
|
badPort: (value) => `"${value}" is not a port. Give a whole number from 0 to 65535, or use --socket.`,
|
|
1671
1673
|
},
|
|
1672
1674
|
|
|
@@ -1742,6 +1744,27 @@ ${bold('EXAMPLES')}
|
|
|
1742
1744
|
span === null
|
|
1743
1745
|
? `Nothing was older than ${days} days. ${kept} measurements kept, and the append log compacted.`
|
|
1744
1746
|
: `Deleted ${count} measurements older than ${days} days, covering ${span} and ${usd} of measured spend. ${kept} kept, and the append log compacted to what the store already resolved to.`,
|
|
1747
|
+
budgetHeading: (period) => `Budget for ${period}`,
|
|
1748
|
+
budgetStanding: (consumed, limit, share, measuredDays, periodDays) =>
|
|
1749
|
+
`${consumed} of ${limit} (${share}), measured over ${measuredDays} of the month's ${periodDays} days.`,
|
|
1750
|
+
budgetShape: (shape, elapsedPct, coverage) =>
|
|
1751
|
+
shape === 'ahead'
|
|
1752
|
+
? `The money is going faster than the calendar: ${elapsedPct}% of the month has elapsed.`
|
|
1753
|
+
: shape === 'behind'
|
|
1754
|
+
? `The money is going slower than the calendar: ${elapsedPct}% of the month has elapsed.`
|
|
1755
|
+
: shape === 'on-pace'
|
|
1756
|
+
? `Tracking the calendar: ${elapsedPct}% of the month has elapsed.`
|
|
1757
|
+
: coverage === 'partial'
|
|
1758
|
+
? 'Whether that is fast or slow for the month cannot be told from a floor: the unmeasured days spent something, and only an overrun would be unarguable.'
|
|
1759
|
+
: 'There is nothing to compare the spend against yet.',
|
|
1760
|
+
budgetNeverForecast: () =>
|
|
1761
|
+
'That is a shape, not a forecast. Where this goes next depends on what you do next, and no arithmetic here knows that.',
|
|
1762
|
+
budgetNothingMeasured: (elapsedDays) =>
|
|
1763
|
+
`Nothing has been measured this month, across ${elapsedDays} elapsed ${elapsedDays === 1 ? 'day' : 'days'}. That is not a budget under control — it is a budget nobody is watching. Run trazum connect to pull what the provider has.`,
|
|
1764
|
+
budgetPartial: (measuredDays, elapsedDays, days) =>
|
|
1765
|
+
`Only ${measuredDays} of ${elapsedDays} elapsed days carry any measurement, so the figure below is a floor on the month rather than the month. Missing: ${days}.`,
|
|
1766
|
+
budgetScopesUnmeasured: (count) =>
|
|
1767
|
+
`${count} budgeted ${count === 1 ? 'scope' : 'scopes'} (per label or per service) cannot be answered from the store: a store record carries a provider and a model, not a workload label. Gate those with trazum profile against a per-call log.`,
|
|
1745
1768
|
},
|
|
1746
1769
|
|
|
1747
1770
|
connect: {
|
package/src/i18n/es.ts
CHANGED
|
@@ -1696,7 +1696,9 @@ ${bold('EJEMPLOS')}
|
|
|
1696
1696
|
nothingMeasured: (dir) =>
|
|
1697
1697
|
`Todavía no hay nada medido (el almacén de ${dir} está vacío), así que la mitad de presupuesto de cada respuesta lo dirá. La mitad del coste sigue respondiendo desde el catálogo: sin conexión es un modo, no un fallo.`,
|
|
1698
1698
|
noBudget: () =>
|
|
1699
|
-
'No hay spend.
|
|
1699
|
+
'No hay spend.monthlyUsd configurado, así que "queda presupuesto" no tiene sujeto y cada respuesta lo dice en vez de inventarse uno. spend.maxUsd no se lee aquí a propósito: ese controla el periodo que cubra un registro, y leerlo como límite mensual es justo como dos superficies de esta herramienta acaban en desacuerdo.',
|
|
1700
|
+
partialCoverage: (measuredDays, elapsedDays, period) =>
|
|
1701
|
+
`Solo ${measuredDays} de los ${elapsedDays} días transcurridos de ${period} tienen alguna medición, así que la cifra consumida es un suelo del periodo y no el periodo. Descarga los días que faltan con trazum connect antes de tratar lo que queda como margen.`,
|
|
1700
1702
|
badPort: (value) => `"${value}" no es un puerto. Da un número entero de 0 a 65535, o usa --socket.`,
|
|
1701
1703
|
},
|
|
1702
1704
|
|
|
@@ -1772,6 +1774,27 @@ ${bold('EJEMPLOS')}
|
|
|
1772
1774
|
span === null
|
|
1773
1775
|
? `Nada era más antiguo que ${days} días. ${kept} mediciones conservadas, y el log compactado.`
|
|
1774
1776
|
: `Borradas ${count} mediciones de más de ${days} días, que cubren ${span} y ${usd} de gasto medido. ${kept} conservadas, y el log compactado a lo que el almacén ya resolvía.`,
|
|
1777
|
+
budgetHeading: (period) => `Presupuesto de ${period}`,
|
|
1778
|
+
budgetStanding: (consumed, limit, share, measuredDays, periodDays) =>
|
|
1779
|
+
`${consumed} de ${limit} (${share}), medido sobre ${measuredDays} de los ${periodDays} días del mes.`,
|
|
1780
|
+
budgetShape: (shape, elapsedPct, coverage) =>
|
|
1781
|
+
shape === 'ahead'
|
|
1782
|
+
? `El dinero va más rápido que el calendario: ha transcurrido el ${elapsedPct}% del mes.`
|
|
1783
|
+
: shape === 'behind'
|
|
1784
|
+
? `El dinero va más lento que el calendario: ha transcurrido el ${elapsedPct}% del mes.`
|
|
1785
|
+
: shape === 'on-pace'
|
|
1786
|
+
? `Al ritmo del calendario: ha transcurrido el ${elapsedPct}% del mes.`
|
|
1787
|
+
: coverage === 'partial'
|
|
1788
|
+
? 'Si eso es rápido o lento para el mes no se puede saber desde un suelo: los días sin medir gastaron algo, y solo un exceso sería incontestable.'
|
|
1789
|
+
: 'Todavía no hay nada con lo que comparar el gasto.',
|
|
1790
|
+
budgetNeverForecast: () =>
|
|
1791
|
+
'Eso es una forma, no un pronóstico. A dónde va esto después depende de lo que hagas después, y ninguna aritmética de aquí lo sabe.',
|
|
1792
|
+
budgetNothingMeasured: (elapsedDays) =>
|
|
1793
|
+
`No se ha medido nada este mes, en ${elapsedDays} ${elapsedDays === 1 ? 'día transcurrido' : 'días transcurridos'}. Eso no es un presupuesto bajo control — es un presupuesto que nadie está mirando. Ejecuta trazum connect para descargar lo que tenga el proveedor.`,
|
|
1794
|
+
budgetPartial: (measuredDays, elapsedDays, days) =>
|
|
1795
|
+
`Solo ${measuredDays} de ${elapsedDays} días transcurridos tienen alguna medición, así que la cifra de abajo es un suelo del mes y no el mes. Faltan: ${days}.`,
|
|
1796
|
+
budgetScopesUnmeasured: (count) =>
|
|
1797
|
+
`${count} ${count === 1 ? 'ámbito presupuestado' : 'ámbitos presupuestados'} (por etiqueta o por servicio) no se pueden responder desde el almacén: un registro del almacén lleva un proveedor y un modelo, no una etiqueta de flujo. Contrólalos con trazum profile contra un registro por llamada.`,
|
|
1775
1798
|
},
|
|
1776
1799
|
|
|
1777
1800
|
connect: {
|
package/src/i18n/types.ts
CHANGED
|
@@ -1151,6 +1151,12 @@ export interface CliMessages {
|
|
|
1151
1151
|
measuredFrom(usd: string): string;
|
|
1152
1152
|
nothingMeasured(dir: string): string;
|
|
1153
1153
|
noBudget(): string;
|
|
1154
|
+
/**
|
|
1155
|
+
* The period is only partly measured — said out loud, because a position
|
|
1156
|
+
* standing on three days out of thirty must not read as a comfortable
|
|
1157
|
+
* ninety per cent remaining.
|
|
1158
|
+
*/
|
|
1159
|
+
partialCoverage(measuredDays: number, elapsedDays: number, period: string): string;
|
|
1154
1160
|
badPort(value: string): string;
|
|
1155
1161
|
};
|
|
1156
1162
|
|
|
@@ -1205,6 +1211,15 @@ export interface CliMessages {
|
|
|
1205
1211
|
pruneNeedsPolicy(): string;
|
|
1206
1212
|
pruneDryRun(count: string, days: string, span: string | null, usd: string): string;
|
|
1207
1213
|
pruned(count: string, days: string, span: string | null, usd: string, kept: string): string;
|
|
1214
|
+
/** The live budget — the one number `serve` and the MCP guard also read. */
|
|
1215
|
+
budgetHeading(period: string): string;
|
|
1216
|
+
budgetStanding(consumed: string, limit: string, share: string, measuredDays: string, periodDays: string): string;
|
|
1217
|
+
/** The shape of the burn, named. Never a date — see `budgetNeverForecast`. */
|
|
1218
|
+
budgetShape(shape: string, elapsedPct: number, coverage: string): string;
|
|
1219
|
+
budgetNeverForecast(): string;
|
|
1220
|
+
budgetNothingMeasured(elapsedDays: number): string;
|
|
1221
|
+
budgetPartial(measuredDays: number, elapsedDays: number, days: string): string;
|
|
1222
|
+
budgetScopesUnmeasured(count: number): string;
|
|
1208
1223
|
};
|
|
1209
1224
|
|
|
1210
1225
|
/**
|
package/src/index.ts
CHANGED
|
@@ -35,6 +35,7 @@ import {
|
|
|
35
35
|
computeSavings,
|
|
36
36
|
countTokensAnthropic,
|
|
37
37
|
DEFAULT_USAGE,
|
|
38
|
+
budgetPositions,
|
|
38
39
|
detectFromSource,
|
|
39
40
|
matchLocale,
|
|
40
41
|
parsePlanDocument,
|
|
@@ -127,6 +128,7 @@ import type {
|
|
|
127
128
|
UsageProfile,
|
|
128
129
|
} from '@trazum/core';
|
|
129
130
|
import type {
|
|
131
|
+
BudgetReport,
|
|
130
132
|
UsageProfileReport,
|
|
131
133
|
WaiverUse,
|
|
132
134
|
InitDecline,
|
|
@@ -2747,41 +2749,39 @@ async function commandServe(
|
|
|
2747
2749
|
t: CliMessages,
|
|
2748
2750
|
): Promise<void> {
|
|
2749
2751
|
const root = process.cwd();
|
|
2750
|
-
const limitUsd = config.spend?.maxUsd;
|
|
2751
2752
|
|
|
2752
2753
|
const { resolved } = await readStore(root);
|
|
2753
|
-
|
|
2754
|
+
|
|
2754
2755
|
/**
|
|
2755
|
-
* The
|
|
2756
|
+
* The live budget, from `budgetPositions` — the same number `store` prints
|
|
2757
|
+
* and the same one the MCP guard consults.
|
|
2756
2758
|
*
|
|
2757
|
-
*
|
|
2758
|
-
*
|
|
2759
|
-
*
|
|
2759
|
+
* **This used to read `spend.maxUsd` against the whole store**, which is a
|
|
2760
|
+
* per-log gate compared against however much history the store happened to
|
|
2761
|
+
* hold. A year of records against a monthly limit reported as a budget
|
|
2762
|
+
* position, with a straight face and no way for a caller to tell. Same
|
|
2763
|
+
* units, different denominators, and the two surfaces disagreed by exactly
|
|
2764
|
+
* as much history as the machine had. `spend.monthlyUsd` is the key for a
|
|
2765
|
+
* calendar month and nothing infers one key from the other: a repository
|
|
2766
|
+
* with a per-log gate and no monthly budget has no monthly position, and
|
|
2767
|
+
* this says so rather than picking a number that is the right shape.
|
|
2760
2768
|
*/
|
|
2761
|
-
const
|
|
2762
|
-
|
|
2763
|
-
|
|
2764
|
-
|
|
2765
|
-
}
|
|
2766
|
-
: null;
|
|
2767
|
-
const report = bucketedProfile(
|
|
2768
|
-
{
|
|
2769
|
-
provider: 'store',
|
|
2770
|
-
granularity: 'bucketed',
|
|
2771
|
-
buckets: bucketsFromRecords(resolved.records),
|
|
2772
|
-
window,
|
|
2773
|
-
gaps: [],
|
|
2774
|
-
unavailable: [],
|
|
2775
|
-
},
|
|
2776
|
-
{ catalogue: pricing },
|
|
2777
|
-
);
|
|
2769
|
+
const budget = budgetPositions(resolved.records, config.spend, { catalogue: pricing });
|
|
2770
|
+
const standing = budget.positions[0] ?? null;
|
|
2771
|
+
const limitUsd = config.spend?.monthlyUsd;
|
|
2772
|
+
const measured = standing !== null && standing.coverage !== 'none';
|
|
2778
2773
|
|
|
2779
2774
|
const server = buildServer({
|
|
2780
2775
|
catalogue: pricing,
|
|
2781
2776
|
position: () => ({
|
|
2782
|
-
|
|
2777
|
+
// Nothing measured inside the period is `undefined`, never zero: the
|
|
2778
|
+
// endpoint's `cannot-tell` exists for exactly this, and a $0 consumed
|
|
2779
|
+
// would be the healthiest-looking budget a dead store can produce.
|
|
2780
|
+
consumedUsd: measured ? standing.consumedUsd : undefined,
|
|
2783
2781
|
limitUsd,
|
|
2784
|
-
|
|
2782
|
+
// The period, not the store's span. A caller judging staleness needs to
|
|
2783
|
+
// know which month the figure is about.
|
|
2784
|
+
window: standing === null ? null : { fromMs: standing.period.fromMs, toMs: standing.period.toMs },
|
|
2785
2785
|
}),
|
|
2786
2786
|
});
|
|
2787
2787
|
|
|
@@ -2796,8 +2796,13 @@ async function commandServe(
|
|
|
2796
2796
|
console.log(c.bold(t.serve.listening(where)));
|
|
2797
2797
|
console.log(` ${c.dim(wrap(t.serve.loopbackOnly(), 74, ' '))}`);
|
|
2798
2798
|
console.log(
|
|
2799
|
-
` ${c.dim(wrap(measured ? t.serve.measuredFrom(formatUsd(
|
|
2799
|
+
` ${c.dim(wrap(measured ? t.serve.measuredFrom(formatUsd(standing.consumedUsd)) : t.serve.nothingMeasured(STORE_DIR), 74, ' '))}`,
|
|
2800
2800
|
);
|
|
2801
|
+
if (standing !== null && standing.coverage === 'partial') {
|
|
2802
|
+
console.log(
|
|
2803
|
+
` ${c.yellow(wrap(t.serve.partialCoverage(standing.measuredDays, standing.elapsedDays, standing.period.id), 74, ' '))}`,
|
|
2804
|
+
);
|
|
2805
|
+
}
|
|
2801
2806
|
if (limitUsd === undefined) {
|
|
2802
2807
|
console.log(` ${c.dim(wrap(t.serve.noBudget(), 74, ' '))}`);
|
|
2803
2808
|
}
|
|
@@ -3164,6 +3169,72 @@ async function commandStore(
|
|
|
3164
3169
|
console.log(
|
|
3165
3170
|
` ${c.dim(wrap(keepDays === undefined ? t.store.noRetention() : t.store.retention(String(keepDays)), 74, ' '))}`,
|
|
3166
3171
|
);
|
|
3172
|
+
|
|
3173
|
+
/**
|
|
3174
|
+
* The live budget, printed here because this is where the measurement lives.
|
|
3175
|
+
*
|
|
3176
|
+
* The same call `serve` makes and the same call the MCP guard makes, so the
|
|
3177
|
+
* three cannot disagree about how much of the month is gone — which is the
|
|
3178
|
+
* whole point of the number existing in one place.
|
|
3179
|
+
*/
|
|
3180
|
+
renderBudget(budgetPositions(resolved.records, config.spend, { catalogue: pricing }), t, n);
|
|
3181
|
+
}
|
|
3182
|
+
|
|
3183
|
+
/**
|
|
3184
|
+
* One budget standing, rendered.
|
|
3185
|
+
*
|
|
3186
|
+
* Coverage before the money, deliberately. A reader who sees "$61 of $100"
|
|
3187
|
+
* first has already formed a view by the time they reach "over three of
|
|
3188
|
+
* nineteen elapsed days", and the second sentence has to undo the first.
|
|
3189
|
+
*/
|
|
3190
|
+
function renderBudget(report: BudgetReport, t: CliMessages, n: (value: number) => string): void {
|
|
3191
|
+
const standing = report.positions[0];
|
|
3192
|
+
if (standing === undefined) {
|
|
3193
|
+
if (report.unmeasuredScopes.length > 0) {
|
|
3194
|
+
console.log();
|
|
3195
|
+
console.log(
|
|
3196
|
+
` ${c.dim(wrap(t.store.budgetScopesUnmeasured(report.unmeasuredScopes.length), 74, ' '))}`,
|
|
3197
|
+
);
|
|
3198
|
+
}
|
|
3199
|
+
return;
|
|
3200
|
+
}
|
|
3201
|
+
|
|
3202
|
+
console.log();
|
|
3203
|
+
console.log(c.bold(t.store.budgetHeading(standing.period.id)));
|
|
3204
|
+
|
|
3205
|
+
if (standing.coverage === 'none') {
|
|
3206
|
+
// Nothing measured is never rendered as nothing spent. A dead store and a
|
|
3207
|
+
// quiet month produce the same zero, and only one of them is good news.
|
|
3208
|
+
console.log(` ${c.red(wrap(t.store.budgetNothingMeasured(standing.elapsedDays), 74, ' '))}`);
|
|
3209
|
+
return;
|
|
3210
|
+
}
|
|
3211
|
+
if (standing.coverage === 'partial') {
|
|
3212
|
+
console.log(
|
|
3213
|
+
` ${c.yellow(wrap(t.store.budgetPartial(standing.measuredDays, standing.elapsedDays, standing.unmeasuredDays.join(', ')), 74, ' '))}`,
|
|
3214
|
+
);
|
|
3215
|
+
}
|
|
3216
|
+
|
|
3217
|
+
const share = standing.burn.consumedShare;
|
|
3218
|
+
console.log(
|
|
3219
|
+
` ${t.store.budgetStanding(
|
|
3220
|
+
formatUsd(standing.consumedUsd),
|
|
3221
|
+
formatUsd(standing.limitUsd),
|
|
3222
|
+
share === null ? '—' : `${Math.round(share * 100)}%`,
|
|
3223
|
+
n(standing.measuredDays),
|
|
3224
|
+
n(standing.period.days),
|
|
3225
|
+
)}`,
|
|
3226
|
+
);
|
|
3227
|
+
const line = t.store.budgetShape(
|
|
3228
|
+
standing.burn.shape,
|
|
3229
|
+
Math.round(standing.burn.elapsedShare * 100),
|
|
3230
|
+
standing.coverage,
|
|
3231
|
+
);
|
|
3232
|
+
console.log(` ${standing.verdict === 'over' ? c.red(line) : c.dim(wrap(line, 74, ' '))}`);
|
|
3233
|
+
// Only where there is a shape to disclaim. "That is a shape, not a forecast"
|
|
3234
|
+
// under "nothing to compare against" is a disclaimer about nothing.
|
|
3235
|
+
if (standing.burn.shape !== 'cannot-tell') {
|
|
3236
|
+
console.log(` ${c.dim(wrap(t.store.budgetNeverForecast(), 74, ' '))}`);
|
|
3237
|
+
}
|
|
3167
3238
|
}
|
|
3168
3239
|
|
|
3169
3240
|
/**
|